From 27587fcc6b050c669e63b62b53daeb3e05119104 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 3 Mar 2021 19:18:31 +0800 Subject: [PATCH 001/482] init --- examples/hello_world/supply_chain/hello.py | 22 ++++ .../scenarios/supply_chain/__init__.py | 0 .../scenarios/supply_chain/business_engine.py | 67 ++++++++++ .../scenarios/supply_chain/configs.py | 118 ++++++++++++++++++ .../supply_chain/datamodels/__init__.py | 3 + .../scenarios/supply_chain/datamodels/base.py | 16 +++ .../supply_chain/datamodels/distribution.py | 26 ++++ .../supply_chain/datamodels/storage.py | 34 +++++ .../supply_chain/datamodels/transport.py | 41 ++++++ .../supply_chain/facilities/__init__.py | 1 + .../facilities/warehouse_facility.py | 82 ++++++++++++ .../scenarios/supply_chain/frame_builder.py | 14 +++ .../scenarios/supply_chain/logics/__init__.py | 3 + .../scenarios/supply_chain/logics/base.py | 31 +++++ .../supply_chain/logics/distribution_logic.py | 76 +++++++++++ .../supply_chain/logics/storage_logic.py | 67 ++++++++++ .../supply_chain/logics/transport_logic.py | 109 ++++++++++++++++ .../scenarios/supply_chain/units/__init__.py | 3 + .../supply_chain/units/distributionunit.py | 9 ++ .../supply_chain/units/storageunit.py | 9 ++ .../supply_chain/units/transportunit.py | 9 ++ .../scenarios/supply_chain/units/unit_base.py | 42 +++++++ .../simulator/scenarios/supply_chain/world.py | 56 +++++++++ 23 files changed, 838 insertions(+) create mode 100644 examples/hello_world/supply_chain/hello.py create mode 100644 maro/simulator/scenarios/supply_chain/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/business_engine.py create mode 100644 maro/simulator/scenarios/supply_chain/configs.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/base.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/distribution.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/storage.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/transport.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py create mode 100644 maro/simulator/scenarios/supply_chain/frame_builder.py create mode 100644 maro/simulator/scenarios/supply_chain/logics/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/logics/base.py create mode 100644 maro/simulator/scenarios/supply_chain/logics/distribution_logic.py create mode 100644 maro/simulator/scenarios/supply_chain/logics/storage_logic.py create mode 100644 maro/simulator/scenarios/supply_chain/logics/transport_logic.py create mode 100644 maro/simulator/scenarios/supply_chain/units/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/units/distributionunit.py create mode 100644 maro/simulator/scenarios/supply_chain/units/storageunit.py create mode 100644 maro/simulator/scenarios/supply_chain/units/transportunit.py create mode 100644 maro/simulator/scenarios/supply_chain/units/unit_base.py create mode 100644 maro/simulator/scenarios/supply_chain/world.py diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py new file mode 100644 index 000000000..a80ee9476 --- /dev/null +++ b/examples/hello_world/supply_chain/hello.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.simulator import Env +from maro.simulator.scenarios.cim.common import Action + +start_tick = 0 +durations = 100 # 100 days + +opts = dict() + +# Initialize an environment with a specific scenario, related topology. +env = Env(scenario="supply_chain", topology="toy.5p_ssddd_l0.0", + start_tick=start_tick, durations=durations, options=opts) + +for ep in range(1): + metrics, decision_event, is_done = (None, None, False) + + while not is_done: + metrics, decision_event, is_done = env.step(None) + + env.reset() diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py new file mode 100644 index 000000000..ed142541a --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -0,0 +1,67 @@ + +import os + +from maro.simulator.scenarios import AbsBusinessEngine + +from maro.event_buffer import MaroEvents, CascadeEvent, AtomEvent + +from .world import World +from .configs import test_world_config + + +class SupplyChainBusinessEngine(AbsBusinessEngine): + def __init__(self, **kwargs): + super().__init__(scenario_name="supply_chain", **kwargs) + + self._register_events() + + self._build_world() + + self._frame = self.world.frame + + @property + def frame(self): + return self._frame + + @property + def snapshots(self): + return self._frame.snapshots + + @property + def configs(self): + pass + + def step(self, tick: int): + for _, facility in self.world.facilities.items(): + facility.step(tick) + + def post_step(self, tick: int): + + return tick+1 == self._max_tick + + def reset(self): + self._frame.reset() + self._frame.snapshots.reset() + + # TODO: reset frame nodes. + + def _register_events(self): + self._event_buffer.register_event_handler(MaroEvents.TAKE_ACTION, self._on_action_recieved) + + def _build_world(self): + self.update_config_root_path(__file__) + + config_path = os.path.join(self._config_path, "config.yml") + + self.world = World() + + self.world.build(test_world_config) + + + def _on_action_recieved(self, event): + action = event.payload + + if action: + pass + + # TODO: how to dispatch it to units? diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py new file mode 100644 index 000000000..f284a405c --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -0,0 +1,118 @@ + + +from .datamodels import ( + StorageDataModel, + TransportDataModel, + DistributionDataModel +) + +from .logics import ( + StorageLogic, + SimpleTransportLogic, + DistributionLogic +) + +from .units import ( + StorageUnit, + TransportUnit, + DistributionUnit +) + +from .facilities import ( + WarehouseFacility +) + + +datamodel_mapping = { + "StorageDataModel": { + "alias_in_snapshot": "storages", + "class": StorageDataModel + }, + "TransportDataModel": { + "alias_in_snapshot": "transports", + "class": TransportDataModel + }, + "DistributionDataModel": { + "alias_in_snapshot": "distributions", + "class": DistributionDataModel + } +} + + +logic_mapping = { + "StorageLogic": { + "class": StorageLogic + }, + "TransportLogic": { + "class": SimpleTransportLogic + }, + "DistributionLogic": { + "class": DistributionLogic + } +} + +unit_class_mapping = { + "StorageUnit": { + "class": StorageUnit + }, + "TransportUnit": { + "class": TransportUnit + } +} + + +test_world_config = { + "facilities": { + "warehouse1": { + "class": WarehouseFacility, + "configs": { + "skus": { + "sku1": { + "price": 100, + "cost": 100, + "vlt": 5, + "init_stock": 1000, + "production_rate": 200 + }, + "sku2": { + "price": 100, + "cost": 100, + "vlt": 5, + "init_stock": 1000, + "production_rate": 200 + }, + "sku3": { + "price": 100, + "cost": 100, + "vlt": 5, + "init_stock": 1000, + "production_rate": 200 + }, + }, + "storage": { + "data": { + "capacity": 200, + "unit_storage_cost": 10 + } + }, + "distribution": { + "data": { + "unit_price": 10 + } + }, + "transports": [ + { + "data": { + "patient": 100 + } + }, + { + "data": { + "patient": 100 + } + } + ] + } + } + } +} diff --git a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py new file mode 100644 index 000000000..adae3cf94 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py @@ -0,0 +1,3 @@ +from .storage import StorageDataModel +from .transport import TransportDataModel +from .distribution import DistributionDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py new file mode 100644 index 000000000..b79b42b1b --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/base.py @@ -0,0 +1,16 @@ + + +from abc import abstractmethod +from maro.backends.frame import NodeBase + + +class DataModelBase(NodeBase): + @abstractmethod + def initialize(self, configs): + """Initialize the fields with configs, the config should be a dict.""" + pass + + @abstractmethod + def reset(self): + """Reset after each episode""" + pass diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py new file mode 100644 index 000000000..ea19d0bd6 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py @@ -0,0 +1,26 @@ +from .base import DataModelBase + +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +@node("distribution") +class DistributionDataModel(DataModelBase): + unit_price = NodeAttribute(AttributeType.Int) + + # original stock_levels, used to save proudct and its number + product_list = NodeAttribute(AttributeType.Int, 1, is_list=True) + checkin_price = NodeAttribute(AttributeType.Int, 1, is_list=True) + delay_order_penalty = NodeAttribute(AttributeType.Int, 1, is_list=True) + + def __init__(self): + self._unit_price = 0 + + def initialize(self, configs: dict): + if configs is not None: + self._unit_price = configs.get("unit_price", 0) + + self.reset() + + def reset(self): + self.unit_price = self._unit_price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py new file mode 100644 index 000000000..c7d8e33f6 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/storage.py @@ -0,0 +1,34 @@ +from .base import DataModelBase + +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +@node("storage") +class StorageDataModel(DataModelBase): + unit_storage_cost = NodeAttribute(AttributeType.Int) + remaining_space = NodeAttribute(AttributeType.Int) + capacity = NodeAttribute(AttributeType.Int) + + # original stock_levels, used to save product and its number + product_list = NodeAttribute(AttributeType.Int, 1, is_list=True) + product_number = NodeAttribute(AttributeType.Int, 1, is_list=True) + + def __init__(self): + self._unit_storage_cost = 0 + self._capacity = 0 + + def initialize(self, configs): + if configs is not None: + self._unit_storage_cost = configs.get("unit_storage_cost", 0) + self._capacity = configs.get("capacity", 0) + + self.reset() + + def reset(self): + self.unit_storage_cost = self._unit_storage_cost + self.capacity = self._capacity + + self.remaining_space = self._capacity + + self.product_number[:] = 0 diff --git a/maro/simulator/scenarios/supply_chain/datamodels/transport.py b/maro/simulator/scenarios/supply_chain/datamodels/transport.py new file mode 100644 index 000000000..cf94b3ea1 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/transport.py @@ -0,0 +1,41 @@ +from .base import DataModelBase +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +@node("transport") +class TransportDataModel(DataModelBase): + # Id of current entity + source = NodeAttribute(AttributeType.Int) + + # Id of target entity. + destination = NodeAttribute(AttributeType.Int) + + # Number of product. + payload = NodeAttribute(AttributeType.Int) + + # Index of product. + product_id = NodeAttribute(AttributeType.Int) + + requested_quantity = NodeAttribute(AttributeType.Int) + + # Patient to wait for products ready. + patient = NodeAttribute(AttributeType.Int) + + # Steps to destination. + steps = NodeAttribute(AttributeType.Int) + + # Current location on the way, equal to step means arrive at destination. + location = NodeAttribute(AttributeType.Int) + + def __init__(self): + self._patient = 0 + + def initialize(self, configs: dict): + if configs is not None: + self._patient = configs.get("patient", 100) + + self.reset() + + def reset(self): + self.patient = self._patient diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py new file mode 100644 index 000000000..c3791ef18 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/__init__.py @@ -0,0 +1 @@ +from .warehouse_facility import WarehouseFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py new file mode 100644 index 000000000..e638b6ea5 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -0,0 +1,82 @@ + +from typing import List + +from ..units import ( + StorageUnit, + TransportUnit, + DistributionUnit +) + + +class WarehouseFacility: + world: object + + storage: StorageUnit + distribution: DistributionUnit + transports: List[TransportUnit] + + configs: dict + + def __init__(self): + pass + + def step(self, tick: int): + + self.storage.step(tick) + self.distribution.step(tick) + + for transport in self.transports: + transport.step(tick) + + def build(self, configs: dict): + self.configs = configs + + self.storage = StorageUnit() + + # TODO: from config later + # Choose data model and logic we want to use + + # construct storage + self.storage.datamodel_class = "StorageDataModel" + self.storage.logic_class = "StorageLogic" + + self.storage.world = self.world + self.storage.facility = self + self.storage.datamodel_index = self.world.register_datamodel("StorageDataModel") + self.storage.logic = self.world.build_logic("StorageLogic") + + # construct transport + self.transports = [] + + for facility_conf in configs["transports"]: + transport = TransportUnit() + + transport.datamodel_class = "TransportDataModel" + transport.logic_class = "TransportLogic" + + transport.world = self.world + transport.facility = self + transport.datamodel_index = self.world.register_datamodel("TransportDataModel") + transport.logic = self.world.build_logic("TransportLogic") + + self.transports.append(transport) + + # construct distribution + self.distribution = DistributionUnit() + self.distribution.datamodel_class = "DistributionDataModel" + self.distribution.logic_class = "DistributionLogic" + + self.distribution.world = self.world + self.distribution.facility = self + self.distribution.datamodel_index = self.world.register_datamodel("DistributionDataModel") + self.distribution.logic = self.world.build_logic("DistributionLogic") + + def initialize(self): + self.storage.initialize(self.configs.get("storage", {})) + self.distribution.initialize(self.configs.get("distribution", {})) + + transports_conf = self.configs["transports"] + + for index, transport in enumerate(self.transports): + transport.initialize(transports_conf[index]) + diff --git a/maro/simulator/scenarios/supply_chain/frame_builder.py b/maro/simulator/scenarios/supply_chain/frame_builder.py new file mode 100644 index 000000000..6b3549d27 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/frame_builder.py @@ -0,0 +1,14 @@ +from typing import List, Tuple +from maro.backends.frame import NodeBase, FrameBase, FrameNode + + +def build_frame(enable_snapshot: bool, total_snapshots: int, nodes: List[Tuple[NodeBase, str, int]]): + class Frame(FrameBase): + def __init__(self): + # Inject the node definition to frame to support add node dynamically. + for node_cls, name, number in nodes: + setattr(Frame, name, FrameNode(node_cls, number)) + + super().__init__(enable_snapshot=enable_snapshot, total_snapshot=total_snapshots, backend_name="dynamic") + + return Frame() diff --git a/maro/simulator/scenarios/supply_chain/logics/__init__.py b/maro/simulator/scenarios/supply_chain/logics/__init__.py new file mode 100644 index 000000000..d082b31ff --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/logics/__init__.py @@ -0,0 +1,3 @@ +from .storage_logic import StorageLogic +from .transport_logic import SimpleTransportLogic +from .distribution_logic import DistributionLogic diff --git a/maro/simulator/scenarios/supply_chain/logics/base.py b/maro/simulator/scenarios/supply_chain/logics/base.py new file mode 100644 index 000000000..7ae51010f --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/logics/base.py @@ -0,0 +1,31 @@ + +from abc import ABC, abstractmethod + + +class LogicBase(ABC): + # Entity of current logic. + entity = None + + # Data model instance of current entity. + data = None + + # Current world. + world = None + + facility = None + + @abstractmethod + def initialize(self, config): + pass + + @abstractmethod + def step(self, tick: int): + pass + + @abstractmethod + def get_metrics(self): + pass + + @abstractmethod + def reset(self): + pass diff --git a/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py b/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py new file mode 100644 index 000000000..36e4427c4 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py @@ -0,0 +1,76 @@ + +from collections import deque, defaultdict + +from .base import LogicBase + +from typing import Dict + + +class Order: + destination = None + product_id = None + quantity = None + vlt = None + + +class DistributionLogic(LogicBase): + def __init__(self): + self.config: dict = None + + # TODO: find a way to save it to snapshot + self.order_queue = deque() + + # used to map from product id to slot index + self.product_index_mapping: Dict[int, int] = {} + + def initialize(self, config): + self.config = config + + for index, product_id in self.data.product_list: + self.product_index_mapping[product_id] = index + + def step(self, tick: int): + for vechicle in self.facility.transports: + # if we have vechicle not enroute and pending order + if len(self.order_queue) > 0 and vechicle.datamodel.location == 0: + order = self.order_queue.popleft() + + vechicle.schedule(order.destination, + order.product_id, order.quantity, order.vlt) + + # NOTE: we moved delay_order_penalty from facility to sku, is this ok? + for order in self.order_queue: + sku = self.facility.get_sku(order.product_id) + product_index = self.product_index_mapping[order.product_id] + + self.data.delay_order_penalty[product_index] += sku.delay_order_penalty + + def get_metrics(self): + pass + + def reset(self): + self.order_queue.clear() + + def get_pending_order(self): + counter = defaultdict(int) + + for order in self.order_queue: + counter[order.product_id] += order.quantity + + return counter + + def place_order(self, order): + if order.quantity > 0: + sku = self.facility.get_sku(order.product_id) + + if sku is not None: + self.order_queue.append(order) + + product_index = self.product_index_mapping[order.product_id] + order_total_price = sku.price * order.quantity + + self.data.checkin_price[product_index] += order_total_price + + return order_total_price + + return 0 diff --git a/maro/simulator/scenarios/supply_chain/logics/storage_logic.py b/maro/simulator/scenarios/supply_chain/logics/storage_logic.py new file mode 100644 index 000000000..68837208f --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/logics/storage_logic.py @@ -0,0 +1,67 @@ + + +from .base import LogicBase + + +from typing import Dict + + +class StorageLogic(LogicBase): + def __init__(self): + self.config: dict = None + # used to map from product id to slot index + self.product_index_mapping: Dict[int, int] = {} + + def initialize(self, config): + self.config = config + + for index, product_id in self.data.product_list: + self.product_index_mapping[product_id] = index + + def step(self, tick: int): + pass + + def get_metrics(self): + pass + + def reset(self): + pass + + def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: + if all_or_nothing and self.data.remaining_space < sum(product_quantities.values()): + return {} + + unloaded_quantities = {} + + for product_id, quantity in product_quantities.items(): + unload_quantity = min(self.data.remaining_space, quantity) + + product_index = self.product_index_mapping[product_id] + self.data.product_number[product_index] += unload_quantity + unloaded_quantities[product_id] = unload_quantity + + return unloaded_quantities + + def try_take_units(self, product_quantities: Dict[int, int]): + for product_id, quantity in product_quantities.items(): + product_index = self.product_index_mapping[product_id] + + if self.data.product_number[product_index] < quantity: + return False + + # TODO: refactoring for dup code + for product_id, quantity in product_quantities.items(): + product_index = self.product_index_mapping[product_id] + + self.data.product_number[product_index] -= quantity + + return True + + def take_avaiable(self, product_id: int, quantity: int): + product_index = self.product_index_mapping[product_id] + avaiable = self.data.product_number[product_index] + actual = min(avaiable, quantity) + + self.data.product_number[product_index] -= actual + + return actual diff --git a/maro/simulator/scenarios/supply_chain/logics/transport_logic.py b/maro/simulator/scenarios/supply_chain/logics/transport_logic.py new file mode 100644 index 000000000..196932d98 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/logics/transport_logic.py @@ -0,0 +1,109 @@ + +from .base import LogicBase + + +class SimpleTransportLogic(LogicBase): + def __init__(self): + self.config: dict = None + + # max patient of current one transport + self.max_patient = None + + # current products' destination + self.destination = None + + def initialize(self, config): + self.config = config + + def get_metrics(self): + pass + + def reset(self): + self.destination = None + self.max_patient = None + + def schedule(self, destination, product_id: int, quantity: int, vlt): + self.data.destination = destination.id + self.data.product_id = product_id + self.data.requested_quantity = quantity + self.data.vlt = vlt + + self.destination = destination + # keep the patient, reset it after product unloaded. + self.max_patient = self.data.patient + + # Find the path from current entity to target. + # NOTE: + # destination is a StorageUnit entity. + path = self.world.find_path( + self.entity.x, self.entity.y, destination.parent.x, destination.parent.y) + + if self.path is None: + raise Exception(f"Destination {destination} is unreachable") + + # Steps to destinition. + self.steps = len(path) // vlt + + # We are waiting for product loading. + self.data.location = 0 + + def try_loading(self, quantity: int): + if self.facility.storage.try_take_units({self.data.product_id: quantity}): + self.data.payload = quantity + + return True + else: + self.patient -= 1 + + return False + + def try_unloading(self): + unloaded = self.destination.storage.try_add_units( + {self.data.product_id: self.data.payload}, all_or_nothing=False) + + if len(unloaded) > 0: + unloaded_units = sum(unloaded.values()) + + self.destination.consumer.on_order_reception( + self.facility.id, self.data.product_id, unloaded_units, self.data.payload) + + # reset the transport's state + self.data.payload = 0 + self.data.patient = self.max_patient + + def step(self, tick: int): + # If we have not arrive at destination yet. + if self.data.steps > 0: + if self.data.location == 0 and self.data.payload == 0: + # loading will take one tick. + if self.try_loading(self.data.requested_quantity): + return + else: + # Failed to load, check the patient. + if self.patient < 0: + self.destination.consumer._update_open_orders( + self.facility.id, self.data.product_id, -self.requested_quantity) + + # reset + self.data.steps = 0 + self.data.location = 0 + self.data.destination = 0 + + # Moving to destinition + if self.data.payload > 0: + # Closer to destinition until 0. + self.data.location += 1 + self.data.steps -= 1 + else: + # avoid update under idle state. + if self.data.location > 0: + # try to unload + if self.data.payload > 0: + self.try_unloading() + + # back to source if we unload all + if self.data.payload == 0: + self.destination = 0 + self.data.steps = 0 + self.data.location = 0 + self.data.destination = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py new file mode 100644 index 000000000..9a871cc8b --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -0,0 +1,3 @@ +from .storageunit import StorageUnit +from .transportunit import TransportUnit +from .distributionunit import DistributionUnit diff --git a/maro/simulator/scenarios/supply_chain/units/distributionunit.py b/maro/simulator/scenarios/supply_chain/units/distributionunit.py new file mode 100644 index 000000000..8fba739b2 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/distributionunit.py @@ -0,0 +1,9 @@ + +from .unit_base import UnitBase + + +class DistributionUnit(UnitBase): + def __init__(self): + super().__init__() + + diff --git a/maro/simulator/scenarios/supply_chain/units/storageunit.py b/maro/simulator/scenarios/supply_chain/units/storageunit.py new file mode 100644 index 000000000..4a72883d8 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/storageunit.py @@ -0,0 +1,9 @@ + +from .unit_base import UnitBase + + +class StorageUnit(UnitBase): + def __init__(self): + super().__init__() + + diff --git a/maro/simulator/scenarios/supply_chain/units/transportunit.py b/maro/simulator/scenarios/supply_chain/units/transportunit.py new file mode 100644 index 000000000..9308990a4 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/transportunit.py @@ -0,0 +1,9 @@ + +from .unit_base import UnitBase + + +class TransportUnit(UnitBase): + def __init__(self): + super().__init__() + + diff --git a/maro/simulator/scenarios/supply_chain/units/unit_base.py b/maro/simulator/scenarios/supply_chain/units/unit_base.py new file mode 100644 index 000000000..770fa83a8 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/unit_base.py @@ -0,0 +1,42 @@ + + +class UnitBase: + + logic: object + logic_class: str + datamodel_class: str + datamodel_index: int + + world: object + + facility: object + + def __init__(self): + self._datamodel = None + + @property + def datamodel(self): + if self._datamodel is None: + self._datamodel = self.world.get_datamodel(self.datamodel_class, self.datamodel_index) + + return self._datamodel + + def initialize(self, configs: dict): + self.datamodel.initialize(configs.get("data", {})) + + self.logic.data = self.datamodel + self.logic.entity = self + self.logic.world = self.world + self.logic.facility = self.facility + + self.logic.initialize(configs.get("logic", {})) + + def step(self, tick: int): + if self.logic is not None: + self.logic.step(tick) + + def reset(self): + pass + + def set_action(self, action): + pass diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py new file mode 100644 index 000000000..012848fdc --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -0,0 +1,56 @@ + +from collections import defaultdict + +from .frame_builder import build_frame + +from .configs import logic_mapping, datamodel_mapping, unit_class_mapping + + +class World: + def __init__(self): + self.facilities = {} + self.frame = None + + self._facility_counter = 1 + self._datamodel_collection = defaultdict(int) + + def build_logic(self, name: str): + assert name in logic_mapping + + return logic_mapping[name]["class"]() + + def build(self, configs: dict): + # build facilities first + for fname, facility_conf in configs["facilities"].items(): + # create a new instance of facility + facility = facility_conf["class"]() + + facility.world = self + facility.id = self._facility_counter + + self._facility_counter += 1 + + self.facilities[fname] = facility + + self.facilities[fname].build(facility_conf["configs"]) + + # and buld the frame + self.frame = build_frame(True, 10, [(datamodel_mapping[class_name]["class"], datamodel_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._datamodel_collection.items()]) + + # then initiazlie all facilities + for _, facility in self.facilities.items(): + facility.initialize() + + def register_datamodel(self, name: str): + assert name in datamodel_mapping + + node_index = self._datamodel_collection[name] + + self._datamodel_collection[name] += 1 + + return node_index + + def get_datamodel(self, class_name: str, node_index: int): + alias = datamodel_mapping[class_name]["alias_in_snapshot"] + + return getattr(self.frame, alias)[node_index] From 0291b7d4f2c509b66ce8d48226b0c86bd69d8dfb Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 08:36:07 +0800 Subject: [PATCH 002/482] remove unit, make it same as logic --- .../scenarios/supply_chain/configs.py | 16 ------- .../facilities/warehouse_facility.py | 47 +++++++------------ .../scenarios/supply_chain/logics/base.py | 31 +++++++++--- .../supply_chain/logics/distribution_logic.py | 23 +++++---- .../supply_chain/logics/storage_logic.py | 10 ++-- .../supply_chain/logics/transport_logic.py | 14 +++--- .../scenarios/supply_chain/units/__init__.py | 3 -- .../supply_chain/units/distributionunit.py | 9 ---- .../supply_chain/units/storageunit.py | 9 ---- .../supply_chain/units/transportunit.py | 9 ---- .../scenarios/supply_chain/units/unit_base.py | 42 ----------------- .../simulator/scenarios/supply_chain/world.py | 31 +++++++----- 12 files changed, 89 insertions(+), 155 deletions(-) delete mode 100644 maro/simulator/scenarios/supply_chain/units/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/distributionunit.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/storageunit.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/transportunit.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/unit_base.py diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index f284a405c..f118bda1f 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -12,12 +12,6 @@ DistributionLogic ) -from .units import ( - StorageUnit, - TransportUnit, - DistributionUnit -) - from .facilities import ( WarehouseFacility ) @@ -51,16 +45,6 @@ } } -unit_class_mapping = { - "StorageUnit": { - "class": StorageUnit - }, - "TransportUnit": { - "class": TransportUnit - } -} - - test_world_config = { "facilities": { "warehouse1": { diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index e638b6ea5..72256fbc7 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -1,21 +1,17 @@ from typing import List -from ..units import ( - StorageUnit, - TransportUnit, - DistributionUnit -) - class WarehouseFacility: - world: object + world = None + + storage = None + distribution = None + transports = None - storage: StorageUnit - distribution: DistributionUnit - transports: List[TransportUnit] + configs: dict = None - configs: dict + id: int = None def __init__(self): pass @@ -31,45 +27,34 @@ def step(self, tick: int): def build(self, configs: dict): self.configs = configs - self.storage = StorageUnit() - # TODO: from config later - # Choose data model and logic we want to use - - # construct storage - self.storage.datamodel_class = "StorageDataModel" - self.storage.logic_class = "StorageLogic" + self.storage = self.world.build_logic("StorageLogic") + self.storage.data_class = "StorageDataModel" self.storage.world = self.world self.storage.facility = self - self.storage.datamodel_index = self.world.register_datamodel("StorageDataModel") - self.storage.logic = self.world.build_logic("StorageLogic") + self.storage.data_index = self.world.register_datamodel(self.storage.data_class) # construct transport self.transports = [] for facility_conf in configs["transports"]: - transport = TransportUnit() - - transport.datamodel_class = "TransportDataModel" - transport.logic_class = "TransportLogic" + transport = self.world.build_logic("TransportLogic") + transport.data_class = "TransportDataModel" transport.world = self.world transport.facility = self - transport.datamodel_index = self.world.register_datamodel("TransportDataModel") - transport.logic = self.world.build_logic("TransportLogic") + transport.data_index = self.world.register_datamodel(transport.data_class) self.transports.append(transport) # construct distribution - self.distribution = DistributionUnit() - self.distribution.datamodel_class = "DistributionDataModel" - self.distribution.logic_class = "DistributionLogic" + self.distribution = self.world.build_logic("DistributionLogic") + self.distribution.data_class = "DistributionDataModel" self.distribution.world = self.world self.distribution.facility = self - self.distribution.datamodel_index = self.world.register_datamodel("DistributionDataModel") - self.distribution.logic = self.world.build_logic("DistributionLogic") + self.distribution.data_index = self.world.register_datamodel(self.distribution.data_class) def initialize(self): self.storage.initialize(self.configs.get("storage", {})) diff --git a/maro/simulator/scenarios/supply_chain/logics/base.py b/maro/simulator/scenarios/supply_chain/logics/base.py index 7ae51010f..0897e42b3 100644 --- a/maro/simulator/scenarios/supply_chain/logics/base.py +++ b/maro/simulator/scenarios/supply_chain/logics/base.py @@ -3,20 +3,34 @@ class LogicBase(ABC): - # Entity of current logic. - entity = None + # configured class name + data_class: str = None - # Data model instance of current entity. - data = None + # index of the data model index + data_index: int = None # Current world. world = None facility = None - @abstractmethod - def initialize(self, config): - pass + configs: dict = None + + id: int = None + + def __init__(self): + self._data = None + + @property + def data(self): + if self._data is None: + self._data = self.world.get_datamodel(self.data_class, self.data_index) + + return self._data + + def initialize(self, configs: dict): + self.configs = configs + self.data.initialize(configs.get("data", {})) @abstractmethod def step(self, tick: int): @@ -29,3 +43,6 @@ def get_metrics(self): @abstractmethod def reset(self): pass + + def set_action(self, action): + pass diff --git a/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py b/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py index 36e4427c4..0a2ffe3c1 100644 --- a/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py +++ b/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py @@ -15,7 +15,7 @@ class Order: class DistributionLogic(LogicBase): def __init__(self): - self.config: dict = None + super().__init__() # TODO: find a way to save it to snapshot self.order_queue = deque() @@ -23,20 +23,24 @@ def __init__(self): # used to map from product id to slot index self.product_index_mapping: Dict[int, int] = {} - def initialize(self, config): - self.config = config + def initialize(self, configs: dict): + super().initialize(configs) for index, product_id in self.data.product_list: self.product_index_mapping[product_id] = index def step(self, tick: int): - for vechicle in self.facility.transports: - # if we have vechicle not enroute and pending order - if len(self.order_queue) > 0 and vechicle.datamodel.location == 0: + for vehicle in self.facility.transports: + # if we have vehicle not on the way and there is pending order + if len(self.order_queue) > 0 and vehicle.datamodel.location == 0: order = self.order_queue.popleft() - vechicle.schedule(order.destination, - order.product_id, order.quantity, order.vlt) + vehicle.schedule( + order.destination, + order.product_id, + order.quantity, + order.vlt + ) # NOTE: we moved delay_order_penalty from facility to sku, is this ok? for order in self.order_queue: @@ -48,6 +52,9 @@ def step(self, tick: int): def get_metrics(self): pass + def set_action(self, action): + pass + def reset(self): self.order_queue.clear() diff --git a/maro/simulator/scenarios/supply_chain/logics/storage_logic.py b/maro/simulator/scenarios/supply_chain/logics/storage_logic.py index 68837208f..0094af4bf 100644 --- a/maro/simulator/scenarios/supply_chain/logics/storage_logic.py +++ b/maro/simulator/scenarios/supply_chain/logics/storage_logic.py @@ -8,12 +8,13 @@ class StorageLogic(LogicBase): def __init__(self): - self.config: dict = None + super().__init__() + # used to map from product id to slot index self.product_index_mapping: Dict[int, int] = {} - def initialize(self, config): - self.config = config + def initialize(self, configs): + super().initialize(configs) for index, product_id in self.data.product_list: self.product_index_mapping[product_id] = index @@ -27,6 +28,9 @@ def get_metrics(self): def reset(self): pass + def set_action(self, action): + pass + def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: if all_or_nothing and self.data.remaining_space < sum(product_quantities.values()): return {} diff --git a/maro/simulator/scenarios/supply_chain/logics/transport_logic.py b/maro/simulator/scenarios/supply_chain/logics/transport_logic.py index 196932d98..88f652bd8 100644 --- a/maro/simulator/scenarios/supply_chain/logics/transport_logic.py +++ b/maro/simulator/scenarios/supply_chain/logics/transport_logic.py @@ -4,7 +4,7 @@ class SimpleTransportLogic(LogicBase): def __init__(self): - self.config: dict = None + super().__init__() # max patient of current one transport self.max_patient = None @@ -12,8 +12,8 @@ def __init__(self): # current products' destination self.destination = None - def initialize(self, config): - self.config = config + def initialize(self, configs: dict): + super().initialize(configs) def get_metrics(self): pass @@ -41,8 +41,8 @@ def schedule(self, destination, product_id: int, quantity: int, vlt): if self.path is None: raise Exception(f"Destination {destination} is unreachable") - # Steps to destinition. - self.steps = len(path) // vlt + # Steps to destination. + self.data.steps = len(path) // vlt # We are waiting for product loading. self.data.location = 0 @@ -89,9 +89,9 @@ def step(self, tick: int): self.data.location = 0 self.data.destination = 0 - # Moving to destinition + # Moving to destination if self.data.payload > 0: - # Closer to destinition until 0. + # Closer to destination until 0. self.data.location += 1 self.data.steps -= 1 else: diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py deleted file mode 100644 index 9a871cc8b..000000000 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .storageunit import StorageUnit -from .transportunit import TransportUnit -from .distributionunit import DistributionUnit diff --git a/maro/simulator/scenarios/supply_chain/units/distributionunit.py b/maro/simulator/scenarios/supply_chain/units/distributionunit.py deleted file mode 100644 index 8fba739b2..000000000 --- a/maro/simulator/scenarios/supply_chain/units/distributionunit.py +++ /dev/null @@ -1,9 +0,0 @@ - -from .unit_base import UnitBase - - -class DistributionUnit(UnitBase): - def __init__(self): - super().__init__() - - diff --git a/maro/simulator/scenarios/supply_chain/units/storageunit.py b/maro/simulator/scenarios/supply_chain/units/storageunit.py deleted file mode 100644 index 4a72883d8..000000000 --- a/maro/simulator/scenarios/supply_chain/units/storageunit.py +++ /dev/null @@ -1,9 +0,0 @@ - -from .unit_base import UnitBase - - -class StorageUnit(UnitBase): - def __init__(self): - super().__init__() - - diff --git a/maro/simulator/scenarios/supply_chain/units/transportunit.py b/maro/simulator/scenarios/supply_chain/units/transportunit.py deleted file mode 100644 index 9308990a4..000000000 --- a/maro/simulator/scenarios/supply_chain/units/transportunit.py +++ /dev/null @@ -1,9 +0,0 @@ - -from .unit_base import UnitBase - - -class TransportUnit(UnitBase): - def __init__(self): - super().__init__() - - diff --git a/maro/simulator/scenarios/supply_chain/units/unit_base.py b/maro/simulator/scenarios/supply_chain/units/unit_base.py deleted file mode 100644 index 770fa83a8..000000000 --- a/maro/simulator/scenarios/supply_chain/units/unit_base.py +++ /dev/null @@ -1,42 +0,0 @@ - - -class UnitBase: - - logic: object - logic_class: str - datamodel_class: str - datamodel_index: int - - world: object - - facility: object - - def __init__(self): - self._datamodel = None - - @property - def datamodel(self): - if self._datamodel is None: - self._datamodel = self.world.get_datamodel(self.datamodel_class, self.datamodel_index) - - return self._datamodel - - def initialize(self, configs: dict): - self.datamodel.initialize(configs.get("data", {})) - - self.logic.data = self.datamodel - self.logic.entity = self - self.logic.world = self.world - self.logic.facility = self.facility - - self.logic.initialize(configs.get("logic", {})) - - def step(self, tick: int): - if self.logic is not None: - self.logic.step(tick) - - def reset(self): - pass - - def set_action(self, action): - pass diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 012848fdc..2eb86ae8a 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -3,7 +3,7 @@ from .frame_builder import build_frame -from .configs import logic_mapping, datamodel_mapping, unit_class_mapping +from .configs import logic_mapping, datamodel_mapping class World: @@ -11,33 +11,42 @@ def __init__(self): self.facilities = {} self.frame = None - self._facility_counter = 1 + self._id_counter = 1 self._datamodel_collection = defaultdict(int) def build_logic(self, name: str): assert name in logic_mapping - return logic_mapping[name]["class"]() + logic = logic_mapping[name]["class"]() + + logic.id = self._id_counter + + self._id_counter += 1 + + return logic def build(self, configs: dict): # build facilities first - for fname, facility_conf in configs["facilities"].items(): + for facility_name, facility_conf in configs["facilities"].items(): # create a new instance of facility facility = facility_conf["class"]() facility.world = self - facility.id = self._facility_counter + facility.id = self._id_counter - self._facility_counter += 1 + self._id_counter += 1 - self.facilities[fname] = facility + self.facilities[facility_name] = facility - self.facilities[fname].build(facility_conf["configs"]) + self.facilities[facility_name].build(facility_conf["configs"]) - # and buld the frame - self.frame = build_frame(True, 10, [(datamodel_mapping[class_name]["class"], datamodel_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._datamodel_collection.items()]) + # and build the frame + self.frame = build_frame( + True, + 10, + [(datamodel_mapping[class_name]["class"], datamodel_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._datamodel_collection.items()]) - # then initiazlie all facilities + # then initialize all facilities for _, facility in self.facilities.items(): facility.initialize() From 0e7fdec29b3e8870c4226e9fa2c91859dfe82381 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 09:16:57 +0800 Subject: [PATCH 003/482] init by sku, world sku --- examples/hello_world/supply_chain/hello.py | 5 ++++ .../scenarios/supply_chain/business_engine.py | 10 ++++++-- .../scenarios/supply_chain/configs.py | 15 ++++++++++++ .../facilities/warehouse_facility.py | 23 +++++++++++++++++++ .../scenarios/supply_chain/logics/base.py | 3 +-- .../supply_chain/logics/transport_logic.py | 2 ++ .../simulator/scenarios/supply_chain/world.py | 19 ++++++++++++--- 7 files changed, 70 insertions(+), 7 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index a80ee9476..9739a82b3 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -19,4 +19,9 @@ while not is_done: metrics, decision_event, is_done = env.step(None) + print(env.snapshot_list["storage"][0:0:"product_list"]) + env.reset() + + + diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index ed142541a..5ef9cabe5 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -36,6 +36,11 @@ def step(self, tick: int): facility.step(tick) def post_step(self, tick: int): + # take snapshot + if (tick + 1) % self._snapshot_resolution == 0: + self._frame.take_snapshot(self.frame_index(tick)) + + # TODO: anything need to reset per tick? return tick+1 == self._max_tick @@ -44,6 +49,8 @@ def reset(self): self._frame.snapshots.reset() # TODO: reset frame nodes. + for _, facility in self.world.facilities.items(): + facility.reset() def _register_events(self): self._event_buffer.register_event_handler(MaroEvents.TAKE_ACTION, self._on_action_recieved) @@ -55,8 +62,7 @@ def _build_world(self): self.world = World() - self.world.build(test_world_config) - + self.world.build(test_world_config, self.calc_max_snapshots()) def _on_action_recieved(self, event): action = event.payload diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index f118bda1f..5cc77c457 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -46,6 +46,21 @@ } test_world_config = { + # skus in this world, used to generate id + "skus": [ + { + "id": 1, + "name": "sku1" + }, + { + "id": 2, + "name": "sku2" + }, + { + "id": 3, + "name": "sku3" + } + ], "facilities": { "warehouse1": { "class": WarehouseFacility, diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index 72256fbc7..68f9e0130 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -57,6 +57,7 @@ def build(self, configs: dict): self.distribution.data_index = self.world.register_datamodel(self.distribution.data_class) def initialize(self): + # called after build, here we have the data model, we can initialize them. self.storage.initialize(self.configs.get("storage", {})) self.distribution.initialize(self.configs.get("distribution", {})) @@ -65,3 +66,25 @@ def initialize(self): for index, transport in enumerate(self.transports): transport.initialize(transports_conf[index]) + self._init_by_sku(self.configs.get("skus", None)) + + def reset(self): + self.storage.reset() + self.distribution.reset() + + for vehicle in self.transports: + vehicle.reset() + + self._init_by_sku(self.configs.get("skus", None)) + + def _init_by_sku(self, sku_info: dict): + if sku_info is not None: + for sku_name, sku_config in sku_info.items(): + sku = self.world.get_sku(sku_name) + + self.storage.data.product_list.append(sku.id) + self.storage.data.product_number.append(sku_config["init_stock"]) + + self.distribution.data.product_list.append(sku.id) + self.distribution.data.checkin_price.append(0) + self.distribution.data.delay_order_penalty.append(0) diff --git a/maro/simulator/scenarios/supply_chain/logics/base.py b/maro/simulator/scenarios/supply_chain/logics/base.py index 0897e42b3..6f7c7334d 100644 --- a/maro/simulator/scenarios/supply_chain/logics/base.py +++ b/maro/simulator/scenarios/supply_chain/logics/base.py @@ -40,9 +40,8 @@ def step(self, tick: int): def get_metrics(self): pass - @abstractmethod def reset(self): - pass + self.data.reset() def set_action(self, action): pass diff --git a/maro/simulator/scenarios/supply_chain/logics/transport_logic.py b/maro/simulator/scenarios/supply_chain/logics/transport_logic.py index 88f652bd8..a67069fc6 100644 --- a/maro/simulator/scenarios/supply_chain/logics/transport_logic.py +++ b/maro/simulator/scenarios/supply_chain/logics/transport_logic.py @@ -19,6 +19,8 @@ def get_metrics(self): pass def reset(self): + super().reset() + self.destination = None self.max_patient = None diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 2eb86ae8a..8d77a69a0 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -1,11 +1,14 @@ -from collections import defaultdict +from collections import defaultdict, namedtuple from .frame_builder import build_frame from .configs import logic_mapping, datamodel_mapping +Sku = namedtuple("Sku", ("name", "id")) + + class World: def __init__(self): self.facilities = {} @@ -13,6 +16,7 @@ def __init__(self): self._id_counter = 1 self._datamodel_collection = defaultdict(int) + self._sku_collection = {} def build_logic(self, name: str): assert name in logic_mapping @@ -25,7 +29,13 @@ def build_logic(self, name: str): return logic - def build(self, configs: dict): + def build(self, configs: dict, snapshot_number: int): + # collect sku information first + for sku_conf in configs["skus"]: + sku = Sku(sku_conf["name"], sku_conf["id"]) + + self._sku_collection[sku.name] = sku + # build facilities first for facility_name, facility_conf in configs["facilities"].items(): # create a new instance of facility @@ -43,7 +53,7 @@ def build(self, configs: dict): # and build the frame self.frame = build_frame( True, - 10, + snapshot_number, [(datamodel_mapping[class_name]["class"], datamodel_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._datamodel_collection.items()]) # then initialize all facilities @@ -63,3 +73,6 @@ def get_datamodel(self, class_name: str, node_index: int): alias = datamodel_mapping[class_name]["alias_in_snapshot"] return getattr(self.frame, alias)[node_index] + + def get_sku(self, name: str): + return self._sku_collection.get(name, None) From 0b723df2410c6925bf12415b0a871c536fc3aa38 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 09:35:20 +0800 Subject: [PATCH 004/482] init by sku, world sku --- examples/hello_world/supply_chain/hello.py | 1 + maro/backends/raw/snapshotlist.cpp | 6 +++++- maro/simulator/scenarios/supply_chain/world.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 9739a82b3..5bf3f42de 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -19,6 +19,7 @@ while not is_done: metrics, decision_event, is_done = env.step(None) + print(len(env.snapshot_list)) #["storage"][0:0:"product_list"]) print(env.snapshot_list["storage"][0:0:"product_list"]) env.reset() diff --git a/maro/backends/raw/snapshotlist.cpp b/maro/backends/raw/snapshotlist.cpp index 1fe3b31e4..10bd03a35 100644 --- a/maro/backends/raw/snapshotlist.cpp +++ b/maro/backends/raw/snapshotlist.cpp @@ -115,6 +115,8 @@ namespace maro shape.max_node_number = node_indices == nullptr ? cur_node.get_max_number() : node_length; shape.tick_number = ticks == nullptr ? _snapshots.size() : tick_length; + cout << "before: " << _snapshots.size() << endl; + if (!_query_parameters.is_list) { // If it is not a list attriubte, then accept all attribute except list . @@ -139,6 +141,8 @@ namespace maro // If it is a list attribute, then just use first one as querying attribute, // we only support query 1 list attribute (1st one) for 1 node at 1 tick each time to reduce too much padding. + cout << "after: " << _snapshots.size() << endl; + // Make sure we have at least one tick. if (_snapshots.size() == 0) { @@ -165,7 +169,7 @@ namespace maro if (target_tick_pair == _snapshots.end()) { - throw SnapshotQueryNoSnapshotsError(); + throw SnapshotQueryInvalidTickError(); } auto& snapshot = target_tick_pair->second; diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 8d77a69a0..24babe1b9 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -53,7 +53,7 @@ def build(self, configs: dict, snapshot_number: int): # and build the frame self.frame = build_frame( True, - snapshot_number, + 10, [(datamodel_mapping[class_name]["class"], datamodel_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._datamodel_collection.items()]) # then initialize all facilities From 74c47db78f009ba08abc0c3ce53f12166d3fc00f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 09:35:58 +0800 Subject: [PATCH 005/482] remove debug code --- maro/backends/raw/snapshotlist.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/maro/backends/raw/snapshotlist.cpp b/maro/backends/raw/snapshotlist.cpp index 10bd03a35..cb2b91f14 100644 --- a/maro/backends/raw/snapshotlist.cpp +++ b/maro/backends/raw/snapshotlist.cpp @@ -115,8 +115,6 @@ namespace maro shape.max_node_number = node_indices == nullptr ? cur_node.get_max_number() : node_length; shape.tick_number = ticks == nullptr ? _snapshots.size() : tick_length; - cout << "before: " << _snapshots.size() << endl; - if (!_query_parameters.is_list) { // If it is not a list attriubte, then accept all attribute except list . @@ -141,8 +139,6 @@ namespace maro // If it is a list attribute, then just use first one as querying attribute, // we only support query 1 list attribute (1st one) for 1 node at 1 tick each time to reduce too much padding. - cout << "after: " << _snapshots.size() << endl; - // Make sure we have at least one tick. if (_snapshots.size() == 0) { From b0b723070ff6c47ed2dde9e5b14193a9ea92a304 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 09:39:32 +0800 Subject: [PATCH 006/482] correct snapshot number issue --- examples/hello_world/supply_chain/hello.py | 2 +- maro/simulator/scenarios/supply_chain/world.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 5bf3f42de..753415c0f 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -20,7 +20,7 @@ metrics, decision_event, is_done = env.step(None) print(len(env.snapshot_list)) #["storage"][0:0:"product_list"]) - print(env.snapshot_list["storage"][0:0:"product_list"]) + print(env.snapshot_list["storage"][0:0:"product_list"].flatten()) env.reset() diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 24babe1b9..8d77a69a0 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -53,7 +53,7 @@ def build(self, configs: dict, snapshot_number: int): # and build the frame self.frame = build_frame( True, - 10, + snapshot_number, [(datamodel_mapping[class_name]["class"], datamodel_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._datamodel_collection.items()]) # then initialize all facilities From 8c3cfc816d918f2af4f23111b557b873bc7144ff Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 09:44:25 +0800 Subject: [PATCH 007/482] rename logic to unit, make it meaningful --- examples/hello_world/supply_chain/hello.py | 2 +- .../scenarios/supply_chain/configs.py | 22 +++++++++---------- .../facilities/warehouse_facility.py | 6 ++--- .../scenarios/supply_chain/logics/__init__.py | 3 --- .../scenarios/supply_chain/unit/__init__.py | 3 +++ .../supply_chain/{logics => unit}/base.py | 2 +- .../distribution.py} | 4 ++-- .../storage_logic.py => unit/storage.py} | 4 ++-- .../transport_logic.py => unit/transport.py} | 4 ++-- .../simulator/scenarios/supply_chain/world.py | 8 +++---- 10 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 maro/simulator/scenarios/supply_chain/logics/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/unit/__init__.py rename maro/simulator/scenarios/supply_chain/{logics => unit}/base.py (97%) rename maro/simulator/scenarios/supply_chain/{logics/distribution_logic.py => unit/distribution.py} (97%) rename maro/simulator/scenarios/supply_chain/{logics/storage_logic.py => unit/storage.py} (97%) rename maro/simulator/scenarios/supply_chain/{logics/transport_logic.py => unit/transport.py} (98%) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 753415c0f..ec290efe3 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -19,7 +19,7 @@ while not is_done: metrics, decision_event, is_done = env.step(None) - print(len(env.snapshot_list)) #["storage"][0:0:"product_list"]) + print(len(env.snapshot_list)) print(env.snapshot_list["storage"][0:0:"product_list"].flatten()) env.reset() diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 5cc77c457..79530bd18 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -6,10 +6,10 @@ DistributionDataModel ) -from .logics import ( - StorageLogic, - SimpleTransportLogic, - DistributionLogic +from .unit import ( + StorageUnit, + TransportUnit, + DistributionUnit ) from .facilities import ( @@ -33,15 +33,15 @@ } -logic_mapping = { - "StorageLogic": { - "class": StorageLogic +unit_mapping = { + "StorageUnit": { + "class": StorageUnit }, - "TransportLogic": { - "class": SimpleTransportLogic + "TransportUnit": { + "class": TransportUnit }, - "DistributionLogic": { - "class": DistributionLogic + "DistributionUnit": { + "class": DistributionUnit } } diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index 68f9e0130..f567065d8 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -28,7 +28,7 @@ def build(self, configs: dict): self.configs = configs # TODO: from config later - self.storage = self.world.build_logic("StorageLogic") + self.storage = self.world.build_unit("StorageUnit") self.storage.data_class = "StorageDataModel" self.storage.world = self.world @@ -39,7 +39,7 @@ def build(self, configs: dict): self.transports = [] for facility_conf in configs["transports"]: - transport = self.world.build_logic("TransportLogic") + transport = self.world.build_unit("TransportUnit") transport.data_class = "TransportDataModel" transport.world = self.world @@ -49,7 +49,7 @@ def build(self, configs: dict): self.transports.append(transport) # construct distribution - self.distribution = self.world.build_logic("DistributionLogic") + self.distribution = self.world.build_unit("DistributionUnit") self.distribution.data_class = "DistributionDataModel" self.distribution.world = self.world diff --git a/maro/simulator/scenarios/supply_chain/logics/__init__.py b/maro/simulator/scenarios/supply_chain/logics/__init__.py deleted file mode 100644 index d082b31ff..000000000 --- a/maro/simulator/scenarios/supply_chain/logics/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .storage_logic import StorageLogic -from .transport_logic import SimpleTransportLogic -from .distribution_logic import DistributionLogic diff --git a/maro/simulator/scenarios/supply_chain/unit/__init__.py b/maro/simulator/scenarios/supply_chain/unit/__init__.py new file mode 100644 index 000000000..fb4458adb --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/unit/__init__.py @@ -0,0 +1,3 @@ +from .storage import StorageUnit +from .transport import TransportUnit +from .distribution import DistributionUnit diff --git a/maro/simulator/scenarios/supply_chain/logics/base.py b/maro/simulator/scenarios/supply_chain/unit/base.py similarity index 97% rename from maro/simulator/scenarios/supply_chain/logics/base.py rename to maro/simulator/scenarios/supply_chain/unit/base.py index 6f7c7334d..3c04decb0 100644 --- a/maro/simulator/scenarios/supply_chain/logics/base.py +++ b/maro/simulator/scenarios/supply_chain/unit/base.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod -class LogicBase(ABC): +class UnitBase(ABC): # configured class name data_class: str = None diff --git a/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py b/maro/simulator/scenarios/supply_chain/unit/distribution.py similarity index 97% rename from maro/simulator/scenarios/supply_chain/logics/distribution_logic.py rename to maro/simulator/scenarios/supply_chain/unit/distribution.py index 0a2ffe3c1..791d50c0d 100644 --- a/maro/simulator/scenarios/supply_chain/logics/distribution_logic.py +++ b/maro/simulator/scenarios/supply_chain/unit/distribution.py @@ -1,7 +1,7 @@ from collections import deque, defaultdict -from .base import LogicBase +from .base import UnitBase from typing import Dict @@ -13,7 +13,7 @@ class Order: vlt = None -class DistributionLogic(LogicBase): +class DistributionUnit(UnitBase): def __init__(self): super().__init__() diff --git a/maro/simulator/scenarios/supply_chain/logics/storage_logic.py b/maro/simulator/scenarios/supply_chain/unit/storage.py similarity index 97% rename from maro/simulator/scenarios/supply_chain/logics/storage_logic.py rename to maro/simulator/scenarios/supply_chain/unit/storage.py index 0094af4bf..52a40c015 100644 --- a/maro/simulator/scenarios/supply_chain/logics/storage_logic.py +++ b/maro/simulator/scenarios/supply_chain/unit/storage.py @@ -1,12 +1,12 @@ -from .base import LogicBase +from .base import UnitBase from typing import Dict -class StorageLogic(LogicBase): +class StorageUnit(UnitBase): def __init__(self): super().__init__() diff --git a/maro/simulator/scenarios/supply_chain/logics/transport_logic.py b/maro/simulator/scenarios/supply_chain/unit/transport.py similarity index 98% rename from maro/simulator/scenarios/supply_chain/logics/transport_logic.py rename to maro/simulator/scenarios/supply_chain/unit/transport.py index a67069fc6..392b11ca2 100644 --- a/maro/simulator/scenarios/supply_chain/logics/transport_logic.py +++ b/maro/simulator/scenarios/supply_chain/unit/transport.py @@ -1,8 +1,8 @@ -from .base import LogicBase +from .base import UnitBase -class SimpleTransportLogic(LogicBase): +class TransportUnit(UnitBase): def __init__(self): super().__init__() diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 8d77a69a0..3d8dc39e8 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -3,7 +3,7 @@ from .frame_builder import build_frame -from .configs import logic_mapping, datamodel_mapping +from .configs import unit_mapping, datamodel_mapping Sku = namedtuple("Sku", ("name", "id")) @@ -18,10 +18,10 @@ def __init__(self): self._datamodel_collection = defaultdict(int) self._sku_collection = {} - def build_logic(self, name: str): - assert name in logic_mapping + def build_unit(self, name: str): + assert name in unit_mapping - logic = logic_mapping[name]["class"]() + logic = unit_mapping[name]["class"]() logic.id = self._id_counter From 2a9ef02ee69ba7115f4a527fea2c8a1db7243886 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 09:48:12 +0800 Subject: [PATCH 008/482] add facility base --- .../scenarios/supply_chain/facilities/base.py | 33 +++++++++++++++++++ .../facilities/warehouse_facility.py | 17 ++-------- 2 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/facilities/base.py diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py new file mode 100644 index 000000000..2d9b2a253 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -0,0 +1,33 @@ + +from abc import ABC, abstractmethod + + +class FacilityBase(ABC): + world = None + + storage = None + distribution = None + transports = None + + configs: dict = None + id: int = None + + @abstractmethod + def step(self, tick: int): + # called per tick + pass + + @abstractmethod + def build(self, configs: dict): + # called to build components, but without data model instance. + pass + + @abstractmethod + def initialize(self): + # called after data model instance is ready. + pass + + @abstractmethod + def reset(self): + # called per episode. + pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index f567065d8..89303b7a5 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -1,23 +1,10 @@ from typing import List +from .base import FacilityBase -class WarehouseFacility: - world = None - - storage = None - distribution = None - transports = None - - configs: dict = None - - id: int = None - - def __init__(self): - pass - +class WarehouseFacility(FacilityBase): def step(self, tick: int): - self.storage.step(tick) self.distribution.step(tick) From 5b64ec7540c7753dc99dd4e76e5eb6536fe14949 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 10:06:24 +0800 Subject: [PATCH 009/482] refine naming --- examples/hello_world/supply_chain/hello.py | 2 +- .../scenarios/supply_chain/configs.py | 6 +-- .../supply_chain/datamodels/__init__.py | 3 -- .../scenarios/supply_chain/datamodels/base.py | 16 ------ .../supply_chain/datamodels/distribution.py | 26 --------- .../supply_chain/datamodels/storage.py | 34 ------------ .../supply_chain/datamodels/transport.py | 41 -------------- .../facilities/warehouse_facility.py | 6 +-- .../supply_chain/{unit => units}/__init__.py | 0 .../supply_chain/{unit => units}/base.py | 2 +- .../{unit => units}/distribution.py | 0 .../supply_chain/{unit => units}/storage.py | 0 .../supply_chain/{unit => units}/transport.py | 0 .../simulator/scenarios/supply_chain/world.py | 54 +++++++++++++------ 14 files changed, 46 insertions(+), 144 deletions(-) delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/base.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/distribution.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/storage.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/transport.py rename maro/simulator/scenarios/supply_chain/{unit => units}/__init__.py (100%) rename maro/simulator/scenarios/supply_chain/{unit => units}/base.py (89%) rename maro/simulator/scenarios/supply_chain/{unit => units}/distribution.py (100%) rename maro/simulator/scenarios/supply_chain/{unit => units}/storage.py (100%) rename maro/simulator/scenarios/supply_chain/{unit => units}/transport.py (100%) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index ec290efe3..f8e7e5055 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -20,7 +20,7 @@ metrics, decision_event, is_done = env.step(None) print(len(env.snapshot_list)) - print(env.snapshot_list["storage"][0:0:"product_list"].flatten()) + print(env.snapshot_list["transport"][:0:"patient"].flatten()) env.reset() diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 79530bd18..4add4f5b9 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -1,12 +1,12 @@ -from .datamodels import ( +from .data import ( StorageDataModel, TransportDataModel, DistributionDataModel ) -from .unit import ( +from .units import ( StorageUnit, TransportUnit, DistributionUnit @@ -17,7 +17,7 @@ ) -datamodel_mapping = { +data_class_mapping = { "StorageDataModel": { "alias_in_snapshot": "storages", "class": StorageDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py deleted file mode 100644 index adae3cf94..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .storage import StorageDataModel -from .transport import TransportDataModel -from .distribution import DistributionDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py deleted file mode 100644 index b79b42b1b..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/base.py +++ /dev/null @@ -1,16 +0,0 @@ - - -from abc import abstractmethod -from maro.backends.frame import NodeBase - - -class DataModelBase(NodeBase): - @abstractmethod - def initialize(self, configs): - """Initialize the fields with configs, the config should be a dict.""" - pass - - @abstractmethod - def reset(self): - """Reset after each episode""" - pass diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py deleted file mode 100644 index ea19d0bd6..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py +++ /dev/null @@ -1,26 +0,0 @@ -from .base import DataModelBase - -from maro.backends.frame import node, NodeBase, NodeAttribute -from maro.backends.backend import AttributeType - - -@node("distribution") -class DistributionDataModel(DataModelBase): - unit_price = NodeAttribute(AttributeType.Int) - - # original stock_levels, used to save proudct and its number - product_list = NodeAttribute(AttributeType.Int, 1, is_list=True) - checkin_price = NodeAttribute(AttributeType.Int, 1, is_list=True) - delay_order_penalty = NodeAttribute(AttributeType.Int, 1, is_list=True) - - def __init__(self): - self._unit_price = 0 - - def initialize(self, configs: dict): - if configs is not None: - self._unit_price = configs.get("unit_price", 0) - - self.reset() - - def reset(self): - self.unit_price = self._unit_price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py deleted file mode 100644 index c7d8e33f6..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/storage.py +++ /dev/null @@ -1,34 +0,0 @@ -from .base import DataModelBase - -from maro.backends.frame import node, NodeBase, NodeAttribute -from maro.backends.backend import AttributeType - - -@node("storage") -class StorageDataModel(DataModelBase): - unit_storage_cost = NodeAttribute(AttributeType.Int) - remaining_space = NodeAttribute(AttributeType.Int) - capacity = NodeAttribute(AttributeType.Int) - - # original stock_levels, used to save product and its number - product_list = NodeAttribute(AttributeType.Int, 1, is_list=True) - product_number = NodeAttribute(AttributeType.Int, 1, is_list=True) - - def __init__(self): - self._unit_storage_cost = 0 - self._capacity = 0 - - def initialize(self, configs): - if configs is not None: - self._unit_storage_cost = configs.get("unit_storage_cost", 0) - self._capacity = configs.get("capacity", 0) - - self.reset() - - def reset(self): - self.unit_storage_cost = self._unit_storage_cost - self.capacity = self._capacity - - self.remaining_space = self._capacity - - self.product_number[:] = 0 diff --git a/maro/simulator/scenarios/supply_chain/datamodels/transport.py b/maro/simulator/scenarios/supply_chain/datamodels/transport.py deleted file mode 100644 index cf94b3ea1..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/transport.py +++ /dev/null @@ -1,41 +0,0 @@ -from .base import DataModelBase -from maro.backends.frame import node, NodeBase, NodeAttribute -from maro.backends.backend import AttributeType - - -@node("transport") -class TransportDataModel(DataModelBase): - # Id of current entity - source = NodeAttribute(AttributeType.Int) - - # Id of target entity. - destination = NodeAttribute(AttributeType.Int) - - # Number of product. - payload = NodeAttribute(AttributeType.Int) - - # Index of product. - product_id = NodeAttribute(AttributeType.Int) - - requested_quantity = NodeAttribute(AttributeType.Int) - - # Patient to wait for products ready. - patient = NodeAttribute(AttributeType.Int) - - # Steps to destination. - steps = NodeAttribute(AttributeType.Int) - - # Current location on the way, equal to step means arrive at destination. - location = NodeAttribute(AttributeType.Int) - - def __init__(self): - self._patient = 0 - - def initialize(self, configs: dict): - if configs is not None: - self._patient = configs.get("patient", 100) - - self.reset() - - def reset(self): - self.patient = self._patient diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index 89303b7a5..a19dbe560 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -20,7 +20,7 @@ def build(self, configs: dict): self.storage.world = self.world self.storage.facility = self - self.storage.data_index = self.world.register_datamodel(self.storage.data_class) + self.storage.data_index = self.world.register_data_class(self.storage.data_class) # construct transport self.transports = [] @@ -31,7 +31,7 @@ def build(self, configs: dict): transport.world = self.world transport.facility = self - transport.data_index = self.world.register_datamodel(transport.data_class) + transport.data_index = self.world.register_data_class(transport.data_class) self.transports.append(transport) @@ -41,7 +41,7 @@ def build(self, configs: dict): self.distribution.world = self.world self.distribution.facility = self - self.distribution.data_index = self.world.register_datamodel(self.distribution.data_class) + self.distribution.data_index = self.world.register_data_class(self.distribution.data_class) def initialize(self): # called after build, here we have the data model, we can initialize them. diff --git a/maro/simulator/scenarios/supply_chain/unit/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py similarity index 100% rename from maro/simulator/scenarios/supply_chain/unit/__init__.py rename to maro/simulator/scenarios/supply_chain/units/__init__.py diff --git a/maro/simulator/scenarios/supply_chain/unit/base.py b/maro/simulator/scenarios/supply_chain/units/base.py similarity index 89% rename from maro/simulator/scenarios/supply_chain/unit/base.py rename to maro/simulator/scenarios/supply_chain/units/base.py index 3c04decb0..2864385e3 100644 --- a/maro/simulator/scenarios/supply_chain/unit/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -24,7 +24,7 @@ def __init__(self): @property def data(self): if self._data is None: - self._data = self.world.get_datamodel(self.data_class, self.data_index) + self._data = self.world.get_data_instance(self.data_class, self.data_index) return self._data diff --git a/maro/simulator/scenarios/supply_chain/unit/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py similarity index 100% rename from maro/simulator/scenarios/supply_chain/unit/distribution.py rename to maro/simulator/scenarios/supply_chain/units/distribution.py diff --git a/maro/simulator/scenarios/supply_chain/unit/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py similarity index 100% rename from maro/simulator/scenarios/supply_chain/unit/storage.py rename to maro/simulator/scenarios/supply_chain/units/storage.py diff --git a/maro/simulator/scenarios/supply_chain/unit/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py similarity index 100% rename from maro/simulator/scenarios/supply_chain/unit/transport.py rename to maro/simulator/scenarios/supply_chain/units/transport.py diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 3d8dc39e8..d826dac48 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -3,33 +3,55 @@ from .frame_builder import build_frame -from .configs import unit_mapping, datamodel_mapping +from .configs import unit_mapping, data_class_mapping +# sku definition in world level Sku = namedtuple("Sku", ("name", "id")) class World: def __init__(self): + # all the facilities in this world, key: id, value: facilities self.facilities = {} + + # frame of this world, this is determined by the unit selected. self.frame = None + # id counter for all units and facilities in this world self._id_counter = 1 - self._datamodel_collection = defaultdict(int) + + # collection of data model class used in this world. + self._data_class_collection = defaultdict(int) + + # sku collection of this world self._sku_collection = {} + # configuration of current world + self.configs: dict = None + + def gen_id(self): + """Generate id for facility or unit.""" + new_id = self._id_counter + + self._id_counter += 1 + + return new_id + def build_unit(self, name: str): + """Build an unit instance from it name via current configuration.""" assert name in unit_mapping logic = unit_mapping[name]["class"]() - logic.id = self._id_counter - - self._id_counter += 1 + logic.id = self.gen_id() return logic def build(self, configs: dict, snapshot_number: int): + """Build current world according to configurations.""" + self.configs = configs + # collect sku information first for sku_conf in configs["skus"]: sku = Sku(sku_conf["name"], sku_conf["id"]) @@ -41,36 +63,36 @@ def build(self, configs: dict, snapshot_number: int): # create a new instance of facility facility = facility_conf["class"]() + # NOTE: DO set these fields before other operations. facility.world = self - facility.id = self._id_counter - - self._id_counter += 1 + facility.id = self.gen_id() self.facilities[facility_name] = facility + # build the facility first to create related components. self.facilities[facility_name].build(facility_conf["configs"]) # and build the frame self.frame = build_frame( True, snapshot_number, - [(datamodel_mapping[class_name]["class"], datamodel_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._datamodel_collection.items()]) + [(data_class_mapping[class_name]["class"], data_class_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._data_class_collection.items()]) - # then initialize all facilities + # then initialize all facilities as we have the data instance. for _, facility in self.facilities.items(): facility.initialize() - def register_datamodel(self, name: str): - assert name in datamodel_mapping + def register_data_class(self, name: str): + assert name in data_class_mapping - node_index = self._datamodel_collection[name] + node_index = self._data_class_collection[name] - self._datamodel_collection[name] += 1 + self._data_class_collection[name] += 1 return node_index - def get_datamodel(self, class_name: str, node_index: int): - alias = datamodel_mapping[class_name]["alias_in_snapshot"] + def get_data_instance(self, class_name: str, node_index: int): + alias = data_class_mapping[class_name]["alias_in_snapshot"] return getattr(self.frame, alias)[node_index] From fcd4ffdb01c0d46a7d8619347721666b3442d1d2 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 10:29:10 +0800 Subject: [PATCH 010/482] refine the code, more comment to make it easy to read --- .../scenarios/supply_chain/facilities/base.py | 9 +++++++ .../facilities/warehouse_facility.py | 11 +++++++-- .../scenarios/supply_chain/units/base.py | 24 ++++++++++++------- .../supply_chain/units/distribution.py | 18 +++++++------- .../scenarios/supply_chain/units/storage.py | 20 +++------------- .../scenarios/supply_chain/units/transport.py | 24 +++++++++++++------ 6 files changed, 62 insertions(+), 44 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py index 2d9b2a253..abb008de2 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -3,13 +3,22 @@ class FacilityBase(ABC): + # current world world = None + # storage unit storage = None + + # distribution unit distribution = None + + # vehicle list transports = None + # configuration of this facility configs: dict = None + + # id of this facility id: int = None @abstractmethod diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index a19dbe560..4f881b4de 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -14,7 +14,9 @@ def step(self, tick: int): def build(self, configs: dict): self.configs = configs - # TODO: from config later + # TODO: following strings should from config later + + # construct storage self.storage = self.world.build_unit("StorageUnit") self.storage.data_class = "StorageDataModel" @@ -53,6 +55,7 @@ def initialize(self): for index, transport in enumerate(self.transports): transport.initialize(transports_conf[index]) + # init components that related with sku number self._init_by_sku(self.configs.get("skus", None)) def reset(self): @@ -62,6 +65,8 @@ def reset(self): for vehicle in self.transports: vehicle.reset() + # NOTE: as we are using list attribute now, theirs size will be reset to defined one after frame.reset, + # so we have to init them again. self._init_by_sku(self.configs.get("skus", None)) def _init_by_sku(self, sku_info: dict): @@ -69,9 +74,11 @@ def _init_by_sku(self, sku_info: dict): for sku_name, sku_config in sku_info.items(): sku = self.world.get_sku(sku_name) + # update storage's production info self.storage.data.product_list.append(sku.id) self.storage.data.product_number.append(sku_config["init_stock"]) + # update distribution's production info self.distribution.data.product_list.append(sku.id) - self.distribution.data.checkin_price.append(0) + self.distribution.data.check_in_price.append(0) self.distribution.data.delay_order_penalty.append(0) diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py index 2864385e3..69601697d 100644 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -1,47 +1,53 @@ -from abc import ABC, abstractmethod - - -class UnitBase(ABC): - # configured class name +class UnitBase: + # class name of data model data_class: str = None - # index of the data model index + # index of the data model instance in frame data_index: int = None - # Current world. + # current world. world = None + # which facility belongs to facility = None + # configurations of this unit configs: dict = None + # id of this unit id: int = None def __init__(self): + # data model instance, it is None until the initialize function called. self._data = None @property def data(self): + """Data model install related to this unit, available after initialized function called.""" if self._data is None: self._data = self.world.get_data_instance(self.data_class, self.data_index) return self._data def initialize(self, configs: dict): + """Initialize current unit""" + # called after frame ready self.configs = configs self.data.initialize(configs.get("data", {})) - @abstractmethod def step(self, tick: int): + # called per tick pass - @abstractmethod def get_metrics(self): + # called per step pass def reset(self): + # called per episode self.data.reset() def set_action(self, action): + # called after received an action. pass diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 791d50c0d..0d4a74b6e 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -6,6 +6,8 @@ from typing import Dict +# TODO: original code included the order in raw state, but calculate the price in final state +# so we do not need to put it in the frame, just calculate the total_price per tick/step. class Order: destination = None product_id = None @@ -17,7 +19,6 @@ class DistributionUnit(UnitBase): def __init__(self): super().__init__() - # TODO: find a way to save it to snapshot self.order_queue = deque() # used to map from product id to slot index @@ -26,15 +27,18 @@ def __init__(self): def initialize(self, configs: dict): super().initialize(configs) + # create a production index mapping, used to update product information for index, product_id in self.data.product_list: self.product_index_mapping[product_id] = index def step(self, tick: int): for vehicle in self.facility.transports: - # if we have vehicle not on the way and there is pending order + # if we have vehicle not on the way and there is any pending order if len(self.order_queue) > 0 and vehicle.datamodel.location == 0: order = self.order_queue.popleft() + # schedule a job for vehicle + # TODO: why vlt is determined by order? vehicle.schedule( order.destination, order.product_id, @@ -43,19 +47,15 @@ def step(self, tick: int): ) # NOTE: we moved delay_order_penalty from facility to sku, is this ok? + # update order's delay penalty per tick. for order in self.order_queue: sku = self.facility.get_sku(order.product_id) product_index = self.product_index_mapping[order.product_id] self.data.delay_order_penalty[product_index] += sku.delay_order_penalty - def get_metrics(self): - pass - - def set_action(self, action): - pass - def reset(self): + super(DistributionUnit, self).reset() self.order_queue.clear() def get_pending_order(self): @@ -76,7 +76,7 @@ def place_order(self, order): product_index = self.product_index_mapping[order.product_id] order_total_price = sku.price * order.quantity - self.data.checkin_price[product_index] += order_total_price + self.data.check_in_price[product_index] += order_total_price return order_total_price diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 52a40c015..af77c2c59 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -1,8 +1,6 @@ - from .base import UnitBase - from typing import Dict @@ -19,18 +17,6 @@ def initialize(self, configs): for index, product_id in self.data.product_list: self.product_index_mapping[product_id] = index - def step(self, tick: int): - pass - - def get_metrics(self): - pass - - def reset(self): - pass - - def set_action(self, action): - pass - def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: if all_or_nothing and self.data.remaining_space < sum(product_quantities.values()): return {} @@ -61,10 +47,10 @@ def try_take_units(self, product_quantities: Dict[int, int]): return True - def take_avaiable(self, product_id: int, quantity: int): + def take_available(self, product_id: int, quantity: int): product_index = self.product_index_mapping[product_id] - avaiable = self.data.product_number[product_index] - actual = min(avaiable, quantity) + available = self.data.product_number[product_index] + actual = min(available, quantity) self.data.product_number[product_index] -= actual diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 392b11ca2..d7ad48a76 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -15,11 +15,8 @@ def __init__(self): def initialize(self, configs: dict): super().initialize(configs) - def get_metrics(self): - pass - def reset(self): - super().reset() + super(TransportUnit, self).reset() self.destination = None self.max_patient = None @@ -61,13 +58,21 @@ def try_loading(self, quantity: int): def try_unloading(self): unloaded = self.destination.storage.try_add_units( - {self.data.product_id: self.data.payload}, all_or_nothing=False) + {self.data.product_id: self.data.payload}, + all_or_nothing=False + ) + # update order if we unloaded any if len(unloaded) > 0: unloaded_units = sum(unloaded.values()) + # TODO: not implemented, refactor the name self.destination.consumer.on_order_reception( - self.facility.id, self.data.product_id, unloaded_units, self.data.payload) + self.facility.id, + self.data.product_id, + unloaded_units, + self.data.payload + ) # reset the transport's state self.data.payload = 0 @@ -76,13 +81,18 @@ def try_unloading(self): def step(self, tick: int): # If we have not arrive at destination yet. if self.data.steps > 0: + # if we still not loaded enough productions yet. if self.data.location == 0 and self.data.payload == 0: - # loading will take one tick. + # then try to load by requested. if self.try_loading(self.data.requested_quantity): + # NOTE: here we return to simulate loading return else: + self.data.patient -= 1 + # Failed to load, check the patient. if self.patient < 0: + # TODO: not implemented, refactor the name. self.destination.consumer._update_open_orders( self.facility.id, self.data.product_id, -self.requested_quantity) From 4cfece0548e9b54f1f429fe22f131a2d60c5ce43 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 17:27:53 +0800 Subject: [PATCH 011/482] add supplier facility, logic not tested yet --- maro/backends/frame.pyx | 15 +++ .../scenarios/supply_chain/changes.md | 16 +++ .../scenarios/supply_chain/configs.py | 77 ++++++++--- .../supply_chain/facilities/__init__.py | 1 + .../scenarios/supply_chain/facilities/base.py | 16 +-- .../facilities/supplier_facility.py | 123 ++++++++++++++++++ .../facilities/warehouse_facility.py | 53 ++++++-- .../scenarios/supply_chain/units/__init__.py | 1 + .../scenarios/supply_chain/units/base.py | 3 + .../supply_chain/units/distribution.py | 6 +- .../supply_chain/units/manufacturing.py | 60 +++++++++ .../scenarios/supply_chain/units/storage.py | 5 + .../scenarios/supply_chain/units/transport.py | 8 +- .../simulator/scenarios/supply_chain/world.py | 41 +++++- 14 files changed, 373 insertions(+), 52 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/changes.md create mode 100644 maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py create mode 100644 maro/simulator/scenarios/supply_chain/units/manufacturing.py diff --git a/maro/backends/frame.pyx b/maro/backends/frame.pyx index cc14d33f5..6f43d2765 100644 --- a/maro/backends/frame.pyx +++ b/maro/backends/frame.pyx @@ -151,6 +151,9 @@ cdef class _NodeAttributeAccessor: self._slot_number += 1 + if not self._is_list and "_cb" in self.__dict__: + self._cb(None) + def resize(self, new_size: int): """Resize current list attribute with specified new size. @@ -167,6 +170,9 @@ cdef class _NodeAttributeAccessor: self._slot_number = new_size + if not self._is_list and "_cb" in self.__dict__: + self._cb(None) + def clear(self): """Clear all items in current list attribute. @@ -180,6 +186,9 @@ cdef class _NodeAttributeAccessor: self._slot_number = 0 + if not self._is_list and "_cb" in self.__dict__: + self._cb(None) + def insert(self, slot_index: int, value: object): """Insert a value to specified slot. @@ -194,6 +203,9 @@ cdef class _NodeAttributeAccessor: self._slot_number += 1 + if not self._is_list and "_cb" in self.__dict__: + self._cb(None) + def remove(self, slot_index: int): """Remove specified slot. @@ -207,6 +219,9 @@ cdef class _NodeAttributeAccessor: self._slot_number -= 1 + if not self._is_list and "_cb" in self.__dict__: + self._cb(None) + def where(self, filter_func: callable): """Filter current attribute slots with input function. diff --git a/maro/simulator/scenarios/supply_chain/changes.md b/maro/simulator/scenarios/supply_chain/changes.md new file mode 100644 index 000000000..23ff3251d --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/changes.md @@ -0,0 +1,16 @@ +1. distribution: + move delay_order_penalty configuration from facility to sku + +2. manufacturing: + original: + . bom not configured in configuration file + . source material is same as output product + . hard coded "output_lot_size = 1" and control.production_rate to control the produce rate, + but the sku.production_rate in configuration file not in use. + + changed: + . bom configured in configuration file at world level. later we can support override this at each manufacture unit. + . remove output_lot_size from bom, user sku.production_rate at facility level, then action can change this + . support manufacturing without source material, like oil, just produce output production by configured rate + . add type for sku at facility level to identify if it is an input material, or output production + . remove output_lot_size, always be 1 \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 4add4f5b9..020e1a530 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -3,17 +3,20 @@ from .data import ( StorageDataModel, TransportDataModel, - DistributionDataModel + DistributionDataModel, + ManufactureDataModel ) from .units import ( StorageUnit, TransportUnit, - DistributionUnit + DistributionUnit, + ManufacturingUnit ) from .facilities import ( - WarehouseFacility + WarehouseFacility, + SupplierFacility ) @@ -29,6 +32,10 @@ "DistributionDataModel": { "alias_in_snapshot": "distributions", "class": DistributionDataModel + }, + "ManufactureDataModel": { + "alias_in_snapshot": "manufacture", + "class": ManufactureDataModel } } @@ -42,6 +49,9 @@ }, "DistributionUnit": { "class": DistributionUnit + }, + "ManufacturingUnit": { + "class": ManufacturingUnit } } @@ -50,42 +60,77 @@ "skus": [ { "id": 1, - "name": "sku1" + "name": "sku1", + "output_units_per_lot": 1, # later we can support override per facility + "bom": { # bill of materials to procedure this, we can support facility level override + "sku3": 10 # units per lot + } }, { "id": 2, + "output_units_per_lot": 1, "name": "sku2" }, { "id": 3, + "output_units_per_lot": 1, "name": "sku3" } ], "facilities": { + "Supplier1": { + "class": SupplierFacility, + "configs": { + "skus": { + "sku1": { + "init_in_stock": 100, + "production_rate": 200, + "type": "production", + "cost": 10 + }, + # source material, do not need production rate + "sku3": { + "init_in_stock": 100, + "type": "material" + } + }, + "storage": { + "data": { + "capacity": 20000, + "unit_storage_cost": 10 + } + }, + "distribution": { + "data": { + "unit_price": 10 + } + }, + "transports": [ + { + "data": { + "patient": 100 + } + }, + { + "data": { + "patient": 100 + } + } + ] + } + }, "warehouse1": { "class": WarehouseFacility, "configs": { "skus": { "sku1": { - "price": 100, - "cost": 100, - "vlt": 5, "init_stock": 1000, - "production_rate": 200 }, "sku2": { - "price": 100, - "cost": 100, - "vlt": 5, "init_stock": 1000, - "production_rate": 200 }, "sku3": { - "price": 100, - "cost": 100, - "vlt": 5, "init_stock": 1000, - "production_rate": 200 }, }, "storage": { diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py index c3791ef18..b3489ec75 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/__init__.py +++ b/maro/simulator/scenarios/supply_chain/facilities/__init__.py @@ -1 +1,2 @@ from .warehouse_facility import WarehouseFacility +from .supplier_facility import SupplierFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py index abb008de2..9c783a963 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -6,26 +6,24 @@ class FacilityBase(ABC): # current world world = None - # storage unit - storage = None - - # distribution unit - distribution = None - - # vehicle list - transports = None - # configuration of this facility configs: dict = None # id of this facility id: int = None + # sku information, same as original sku_in_stock + # different facility may contains different data + sku_information: dict = None + @abstractmethod def step(self, tick: int): # called per tick pass + def post_step(self, tick: int): + pass + @abstractmethod def build(self, configs: dict): # called to build components, but without data model instance. diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py new file mode 100644 index 000000000..45ba17349 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py @@ -0,0 +1,123 @@ + +from collections import namedtuple +from .base import FacilityBase + + +class SupplierFacility(FacilityBase): + SkuInfo = namedtuple("SkuInfo", ("name", "id", "init_in_stock", "production_rate", "type", "cost")) + + storage = None + distribution = None + transports = None + suppliers = None + + def step(self, tick: int): + pass + + def build(self, configs: dict): + self.configs = configs + + # TODO: dup code from facilities, refactoring later + + # construct storage + self.storage = self.world.build_unit("StorageUnit") + self.storage.data_class = "StorageDataModel" + + self.storage.world = self.world + self.storage.facility = self + self.storage.data_index = self.world.register_data_class(self.storage.data_class) + + # construct transport + self.transports = [] + + for facility_conf in configs["transports"]: + transport = self.world.build_unit("TransportUnit") + transport.data_class = "TransportDataModel" + + transport.world = self.world + transport.facility = self + transport.data_index = self.world.register_data_class(transport.data_class) + + self.transports.append(transport) + + # construct distribution + self.distribution = self.world.build_unit("DistributionUnit") + self.distribution.data_class = "DistributionDataModel" + + self.distribution.world = self.world + self.distribution.facility = self + self.distribution.data_index = self.world.register_data_class(self.distribution.data_class) + + # sku information + self.sku_information = {} + self.suppliers = {} + + for sku_name, sku_config in configs["skus"].items(): + sku = self.world.get_sku(sku_name) + sku_info = SupplierFacility.SkuInfo( + sku_name, + sku.id, + sku_config["init_in_stock"], + sku_config.get("production_rate", 0), + sku_config["type"], + sku_config.get("cost", 0) + ) + + self.sku_information[sku.id] = sku_info + + # TODO: make it an enum later. + if sku_info.type == "production": + # one supplier per sku + supplier = self.world.build_unit("ManufacturingUnit") + supplier.data_class = "ManufactureDataModel" + + supplier.world = self.world + supplier.facility = self + supplier.data_index = self.world.register_data_class(supplier.data_class) + + self.suppliers[sku.id] = supplier + + def initialize(self): + self.storage.initialize(self.configs.get("storage", {})) + self.distribution.initialize(self.configs.get("distribution", {})) + + transports_conf = self.configs["transports"] + + for index, transport in enumerate(self.transports): + transport.initialize(transports_conf[index]) + + for _, sku in self.sku_information.items(): + if sku.id in self.suppliers: + supplier = self.suppliers[sku.id] + + # build parameters to initialize the data model + supplier.initialize({ + "data": { + "production_rate": sku.production_rate, + "output_product_id": sku.id, + "product_unit_cost": sku.cost + } + }) + + def post_step(self, tick: int): + self.storage.post_step(tick) + self.distribution.post_step(tick) + + for vehicle in self.transports: + vehicle.post_step(tick) + + for supplier in self.suppliers.values(): + supplier.post_step(tick) + + def reset(self): + self.storage.reset() + self.distribution.reset() + + for vehicle in self.transports: + vehicle.reset() + + for supplier in self.suppliers.values(): + supplier.reset() + + + diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index 4f881b4de..d596053fc 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -1,9 +1,21 @@ +from collections import namedtuple from typing import List from .base import FacilityBase class WarehouseFacility(FacilityBase): + SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id")) + + # storage unit + storage = None + + # distribution unit + distribution = None + + # vehicle list + transports = None + def step(self, tick: int): self.storage.step(tick) self.distribution.step(tick) @@ -45,6 +57,15 @@ def build(self, configs: dict): self.distribution.facility = self self.distribution.data_index = self.world.register_data_class(self.distribution.data_class) + # sku information + self.sku_information = {} + + for sku_name, sku_config in configs["skus"].items(): + sku = self.world.get_sku(sku_name) + sku_info = WarehouseFacility.SkuInfo(sku_name, sku_config["init_stock"], sku.id) + + self.sku_information[sku.id] = sku_info + def initialize(self): # called after build, here we have the data model, we can initialize them. self.storage.initialize(self.configs.get("storage", {})) @@ -56,7 +77,7 @@ def initialize(self): transport.initialize(transports_conf[index]) # init components that related with sku number - self._init_by_sku(self.configs.get("skus", None)) + self._init_by_skus() def reset(self): self.storage.reset() @@ -67,18 +88,22 @@ def reset(self): # NOTE: as we are using list attribute now, theirs size will be reset to defined one after frame.reset, # so we have to init them again. - self._init_by_sku(self.configs.get("skus", None)) + self._init_by_skus() - def _init_by_sku(self, sku_info: dict): - if sku_info is not None: - for sku_name, sku_config in sku_info.items(): - sku = self.world.get_sku(sku_name) + def post_step(self, tick: int): + self.storage.post_step(tick) + self.distribution.post_step(tick) - # update storage's production info - self.storage.data.product_list.append(sku.id) - self.storage.data.product_number.append(sku_config["init_stock"]) - - # update distribution's production info - self.distribution.data.product_list.append(sku.id) - self.distribution.data.check_in_price.append(0) - self.distribution.data.delay_order_penalty.append(0) + for vehicle in self.transports: + vehicle.post_step(tick) + + def _init_by_skus(self): + for _, sku in self.sku_information.items(): + # update storage's production info + self.storage.data.product_list.append(sku.id) + self.storage.data.product_number.append(sku.init_in_stock) + + # update distribution's production info + self.distribution.data.product_list.append(sku.id) + self.distribution.data.check_in_price.append(0) + self.distribution.data.delay_order_penalty.append(0) diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index fb4458adb..f408fcc11 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -1,3 +1,4 @@ from .storage import StorageUnit from .transport import TransportUnit from .distribution import DistributionUnit +from .manufacturing import ManufacturingUnit diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py index 69601697d..e6befd9d1 100644 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -40,6 +40,9 @@ def step(self, tick: int): # called per tick pass + def post_step(self, tick: int): + pass + def get_metrics(self): # called per step pass diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 0d4a74b6e..f9b8a28cc 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -17,7 +17,7 @@ class Order: class DistributionUnit(UnitBase): def __init__(self): - super().__init__() + super(DistributionUnit, self).__init__() self.order_queue = deque() @@ -25,7 +25,7 @@ def __init__(self): self.product_index_mapping: Dict[int, int] = {} def initialize(self, configs: dict): - super().initialize(configs) + super(DistributionUnit, self).initialize(configs) # create a production index mapping, used to update product information for index, product_id in self.data.product_list: @@ -68,7 +68,7 @@ def get_pending_order(self): def place_order(self, order): if order.quantity > 0: - sku = self.facility.get_sku(order.product_id) + sku = self.facility.get_sku_by_id(order.product_id) if sku is not None: self.order_queue.append(order) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacturing.py b/maro/simulator/scenarios/supply_chain/units/manufacturing.py new file mode 100644 index 000000000..bfc3646ab --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/manufacturing.py @@ -0,0 +1,60 @@ + +from .base import UnitBase + + +class ManufacturingUnit(UnitBase): + # source material sku and related number per produce cycle + bom = None + + # how many production unit each produce cycle + output_units_per_lot = None + + # how many unit we will consume each produce cycle + input_units_per_lot = 0 + + def __init__(self): + super(ManufacturingUnit, self).__init__() + + def initialize(self, configs: dict): + super().initialize(configs) + + # grab bom of current production + sku = self.world.get_sku_by_id(self.data.output_product_id) + self.bom = sku.bom + self.output_units_per_lot = sku.output_units_per_lot + + if len(self.bom) > 0: + self.input_units_per_lot = sum(self.bom.values()) + + def step(self, tick: int): + # try to produce production if we have positive rate + if self.data.production_rate > 0: + sku_num = len(self.facility.sku_information) + unit_num_upper_bound = self.facility.storage.capacity // sku_num + + # one lot per time, until no enough space to hold output, or no enough source material + for _ in range(self.data.production_rate): + storage_remaining_space = self.facility.storage.data.remaining_space + current_product_number = self.facility.storage.get_product_number(self.data.output_product_id) + space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot + + # if remaining space enough to hold output production + if storage_remaining_space >= space_taken_per_cycle: + # if not reach the storage limitation of current production + if current_product_number < unit_num_upper_bound: + # if we have enough source materials + if self.facility.storage.try_take_units(self.bom): + self.facility.storage.try_add_units({self.data.output_product_id: self.output_units_per_lot}) + + # update manufacturing number in state + self.data.manufacturing_number += 1 + + def post_step(self, tick: int): + # reset the manufacture cost per tick + self.data.manufacturing_cost = 0 + + def set_action(self, action: int): + # we expect production rate number as action + # production_rate <= 0 will stop manufacturing + self.data.production_rate = action + diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index af77c2c59..7bfa47d7c 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -55,3 +55,8 @@ def take_available(self, product_id: int, quantity: int): self.data.product_number[product_index] -= actual return actual + + def get_product_number(self, product_id: int) -> int: + product_index = self.product_index_mapping[product_id] + + return self.data.product_number[product_index] diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index d7ad48a76..18d5aead6 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -32,10 +32,12 @@ def schedule(self, destination, product_id: int, quantity: int, vlt): self.max_patient = self.data.patient # Find the path from current entity to target. - # NOTE: - # destination is a StorageUnit entity. path = self.world.find_path( - self.entity.x, self.entity.y, destination.parent.x, destination.parent.y) + self.entity.x, + self.entity.y, + destination.parent.x, + destination.parent.y + ) if self.path is None: raise Exception(f"Destination {destination} is unreachable") diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index d826dac48..93a7f5950 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -7,7 +7,8 @@ # sku definition in world level -Sku = namedtuple("Sku", ("name", "id")) +# bom is a dictionary, key is the material sku id, value is units per lot +Sku = namedtuple("Sku", ("name", "id", "bom", "output_units_per_lot")) class World: @@ -27,6 +28,9 @@ def __init__(self): # sku collection of this world self._sku_collection = {} + # sku id -> name in collection + self._sku_id2name_mapping = {} + # configuration of current world self.configs: dict = None @@ -54,10 +58,20 @@ def build(self, configs: dict, snapshot_number: int): # collect sku information first for sku_conf in configs["skus"]: - sku = Sku(sku_conf["name"], sku_conf["id"]) + sku = Sku(sku_conf["name"], sku_conf["id"], {}, sku_conf["output_units_per_lot"]) + self._sku_id2name_mapping[sku.id] = sku.name self._sku_collection[sku.name] = sku + # collect bom + for sku_conf in configs["skus"]: + sku = self._sku_collection[sku_conf["name"]] + + bom = sku_conf.get("bom", {}) + + for material_sku_name, units_per_lot in bom.items(): + sku.bom[self._sku_collection[material_sku_name].id] = units_per_lot + # build facilities first for facility_name, facility_conf in configs["facilities"].items(): # create a new instance of facility @@ -72,11 +86,21 @@ def build(self, configs: dict, snapshot_number: int): # build the facility first to create related components. self.facilities[facility_name].build(facility_conf["configs"]) - # and build the frame - self.frame = build_frame( - True, - snapshot_number, - [(data_class_mapping[class_name]["class"], data_class_mapping[class_name]["alias_in_snapshot"], number) for class_name, number in self._data_class_collection.items()]) + # build the frame + # . collect data model class + data_class_in_frame = [] + + for class_name, number in self._data_class_collection.items(): + class_config = data_class_mapping[class_name] + + data_class_in_frame.append(( + class_config["class"], + class_config["alias_in_snapshot"], + number + )) + + # . build the frame + self.frame = build_frame(True, snapshot_number, data_class_in_frame) # then initialize all facilities as we have the data instance. for _, facility in self.facilities.items(): @@ -98,3 +122,6 @@ def get_data_instance(self, class_name: str, node_index: int): def get_sku(self, name: str): return self._sku_collection.get(name, None) + + def get_sku_by_id(self, sku_id: int): + return self._sku_collection[self._sku_id2name_mapping[sku_id]] From 51f9349730b72267ea99348acc8462f9af928090 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 4 Mar 2021 17:58:48 +0800 Subject: [PATCH 012/482] fix bug in facility initialize, add consumerunit not completed --- .../scenarios/supply_chain/configs.py | 17 +++-- .../facilities/supplier_facility.py | 63 ++++++++++++++++--- .../facilities/warehouse_facility.py | 6 +- .../scenarios/supply_chain/units/__init__.py | 1 + .../scenarios/supply_chain/units/consumer.py | 17 +++++ .../supply_chain/units/distribution.py | 2 +- .../supply_chain/units/manufacturing.py | 2 +- .../scenarios/supply_chain/units/storage.py | 2 +- 8 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/units/consumer.py diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 020e1a530..75d322563 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -4,14 +4,16 @@ StorageDataModel, TransportDataModel, DistributionDataModel, - ManufactureDataModel + ManufactureDataModel, + ConsumerDataModel ) from .units import ( StorageUnit, TransportUnit, DistributionUnit, - ManufacturingUnit + ManufacturingUnit, + ConsumerUnit ) from .facilities import ( @@ -34,8 +36,12 @@ "class": DistributionDataModel }, "ManufactureDataModel": { - "alias_in_snapshot": "manufacture", + "alias_in_snapshot": "manufactures", "class": ManufactureDataModel + }, + "ConsumerDataModel": { + "alias_in_snapshot": "consumers", + "class": ConsumerDataModel } } @@ -52,7 +58,10 @@ }, "ManufacturingUnit": { "class": ManufacturingUnit - } + }, + "ConsumerUnit": { + "class": ConsumerUnit + }, } test_world_config = { diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py index 45ba17349..b89e9d859 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py @@ -10,9 +10,20 @@ class SupplierFacility(FacilityBase): distribution = None transports = None suppliers = None + consumers = None def step(self, tick: int): - pass + self.storage.step(tick) + self.distribution.step(tick) + + for vehicle in self.transports: + vehicle.step(tick) + + for supplier in self.suppliers.values(): + supplier.step(tick) + + for consumer in self.consumers.values(): + consumer.step(tick) def build(self, configs: dict): self.configs = configs @@ -51,6 +62,7 @@ def build(self, configs: dict): # sku information self.sku_information = {} self.suppliers = {} + self.consumers = {} for sku_name, sku_config in configs["skus"].items(): sku = self.world.get_sku(sku_name) @@ -77,14 +89,17 @@ def build(self, configs: dict): self.suppliers[sku.id] = supplier - def initialize(self): - self.storage.initialize(self.configs.get("storage", {})) - self.distribution.initialize(self.configs.get("distribution", {})) + consumer = self.world.build_unit("ConsumerUnit") + consumer.data_class = "ConsumerDataModel" - transports_conf = self.configs["transports"] + consumer.world = self.world + consumer.facility = self + consumer.data_index = self.world.register_data_class(consumer.data_class) + + self.consumers[sku.id] = consumer + + def initialize(self): - for index, transport in enumerate(self.transports): - transport.initialize(transports_conf[index]) for _, sku in self.sku_information.items(): if sku.id in self.suppliers: @@ -99,6 +114,24 @@ def initialize(self): } }) + consumer = self.consumers[sku.id] + + consumer.initialize({ + "data": { + "order_cost": self.configs.get("order_cost", 0) + } + }) + + self._init_by_skus() + + self.storage.initialize(self.configs.get("storage", {})) + self.distribution.initialize(self.configs.get("distribution", {})) + + transports_conf = self.configs["transports"] + + for index, transport in enumerate(self.transports): + transport.initialize(transports_conf[index]) + def post_step(self, tick: int): self.storage.post_step(tick) self.distribution.post_step(tick) @@ -109,6 +142,9 @@ def post_step(self, tick: int): for supplier in self.suppliers.values(): supplier.post_step(tick) + for consumer in self.consumers.values(): + consumer.post_step(tick) + def reset(self): self.storage.reset() self.distribution.reset() @@ -119,5 +155,18 @@ def reset(self): for supplier in self.suppliers.values(): supplier.reset() + for consumer in self.consumers.values(): + consumer.reset() + self._init_by_skus() + def _init_by_skus(self): + for _, sku in self.sku_information.items(): + # update storage's production info + self.storage.data.product_list.append(sku.id) + self.storage.data.product_number.append(sku.init_in_stock) + + # update distribution's production info + self.distribution.data.product_list.append(sku.id) + self.distribution.data.check_in_price.append(0) + self.distribution.data.delay_order_penalty.append(0) diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index d596053fc..0f64eacf7 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -67,6 +67,9 @@ def build(self, configs: dict): self.sku_information[sku.id] = sku_info def initialize(self): + # init components that related with sku number + self._init_by_skus() + # called after build, here we have the data model, we can initialize them. self.storage.initialize(self.configs.get("storage", {})) self.distribution.initialize(self.configs.get("distribution", {})) @@ -76,9 +79,6 @@ def initialize(self): for index, transport in enumerate(self.transports): transport.initialize(transports_conf[index]) - # init components that related with sku number - self._init_by_skus() - def reset(self): self.storage.reset() self.distribution.reset() diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index f408fcc11..eff65f5db 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -2,3 +2,4 @@ from .transport import TransportUnit from .distribution import DistributionUnit from .manufacturing import ManufacturingUnit +from .consumer import ConsumerUnit diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py new file mode 100644 index 000000000..5c05b180e --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -0,0 +1,17 @@ + + +from .base import UnitBase + + +class ConsumerUnit(UnitBase): + def __init__(self): + super(ConsumerUnit, self).__init__() + + def initialize(self, configs: dict): + super(ConsumerUnit, self).initialize(configs) + + def step(self, tick: int): + pass + + def reset(self): + super(ConsumerUnit, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index f9b8a28cc..70670c06a 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -28,7 +28,7 @@ def initialize(self, configs: dict): super(DistributionUnit, self).initialize(configs) # create a production index mapping, used to update product information - for index, product_id in self.data.product_list: + for index, product_id in enumerate(self.data.product_list[:]): self.product_index_mapping[product_id] = index def step(self, tick: int): diff --git a/maro/simulator/scenarios/supply_chain/units/manufacturing.py b/maro/simulator/scenarios/supply_chain/units/manufacturing.py index bfc3646ab..422d881ff 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacturing.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacturing.py @@ -30,7 +30,7 @@ def step(self, tick: int): # try to produce production if we have positive rate if self.data.production_rate > 0: sku_num = len(self.facility.sku_information) - unit_num_upper_bound = self.facility.storage.capacity // sku_num + unit_num_upper_bound = self.facility.storage.data.capacity // sku_num # one lot per time, until no enough space to hold output, or no enough source material for _ in range(self.data.production_rate): diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 7bfa47d7c..14c537136 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -14,7 +14,7 @@ def __init__(self): def initialize(self, configs): super().initialize(configs) - for index, product_id in self.data.product_list: + for index, product_id in enumerate(self.data.product_list[:]): self.product_index_mapping[product_id] = index def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: From 9fd1b15dc2cfb47e6b7340964dcc4d39e9066c7b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 10:45:43 +0800 Subject: [PATCH 013/482] refactoring the facilities in world config --- .../scenarios/supply_chain/configs.py | 48 +++++++++++++++++-- .../scenarios/supply_chain/facilities/base.py | 3 ++ .../facilities/supplier_facility.py | 5 +- .../scenarios/supply_chain/units/consumer.py | 6 +++ .../scenarios/supply_chain/units/transport.py | 7 ++- .../simulator/scenarios/supply_chain/world.py | 9 ++-- 6 files changed, 66 insertions(+), 12 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 75d322563..20047cf95 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -86,8 +86,47 @@ "name": "sku3" } ], - "facilities": { - "Supplier1": { + "facilities": [ + { + # a source material supplier without input requirement + "name": "Supplier3", + "class": SupplierFacility, + "configs": { + "skus": { + "sku3": { + "init_in_stock": 100, + "production_rate": 200, + "type": "production", + "cost": 10 + } + }, + "storage": { + "data": { + "capacity": 20000, + "unit_storage_cost": 10 + } + }, + "distribution": { + "data": { + "unit_price": 10 + } + }, + "transports": [ + { + "data": { + "patient": 100 + } + }, + { + "data": { + "patient": 100 + } + } + ] + } + }, + { + "name": "Supplier1", "class": SupplierFacility, "configs": { "skus": { @@ -128,7 +167,8 @@ ] } }, - "warehouse1": { + { + "name": "warehouse1", "class": WarehouseFacility, "configs": { "skus": { @@ -167,5 +207,5 @@ ] } } - } + ] } diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py index 9c783a963..8c74a12e3 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -12,6 +12,9 @@ class FacilityBase(ABC): # id of this facility id: int = None + # name of this facility + name: str = None + # sku information, same as original sku_in_stock # different facility may contains different data sku_information: dict = None diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py index b89e9d859..382acc5c3 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py @@ -41,7 +41,7 @@ def build(self, configs: dict): # construct transport self.transports = [] - for facility_conf in configs["transports"]: + for _ in configs["transports"]: transport = self.world.build_unit("TransportUnit") transport.data_class = "TransportDataModel" @@ -99,8 +99,6 @@ def build(self, configs: dict): self.consumers[sku.id] = consumer def initialize(self): - - for _, sku in self.sku_information.items(): if sku.id in self.suppliers: supplier = self.suppliers[sku.id] @@ -122,6 +120,7 @@ def initialize(self): } }) + # DO init by skus first, as other components may depend on sku information self._init_by_skus() self.storage.initialize(self.configs.get("storage", {})) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 5c05b180e..31195a768 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -15,3 +15,9 @@ def step(self, tick: int): def reset(self): super(ConsumerUnit, self).reset() + + def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): + pass + + def update_open_orders(self, source_id, product_id, qty_delta): + pass diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 18d5aead6..30f39a483 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -95,8 +95,11 @@ def step(self, tick: int): # Failed to load, check the patient. if self.patient < 0: # TODO: not implemented, refactor the name. - self.destination.consumer._update_open_orders( - self.facility.id, self.data.product_id, -self.requested_quantity) + self.destination.consumer.update_open_orders( + self.facility.id, + self.data.product_id, + -self.requested_quantity + ) # reset self.data.steps = 0 diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 93a7f5950..c497c3685 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -73,18 +73,21 @@ def build(self, configs: dict, snapshot_number: int): sku.bom[self._sku_collection[material_sku_name].id] = units_per_lot # build facilities first - for facility_name, facility_conf in configs["facilities"].items(): + for facility_conf in configs["facilities"]: + facility_name = facility_conf["name"] + # create a new instance of facility facility = facility_conf["class"]() # NOTE: DO set these fields before other operations. facility.world = self facility.id = self.gen_id() + facility.name = facility_name - self.facilities[facility_name] = facility + self.facilities[facility.id] = facility # build the facility first to create related components. - self.facilities[facility_name].build(facility_conf["configs"]) + facility.build(facility_conf["configs"]) # build the frame # . collect data model class From dd629406b5aa3ce62ffbb6bd9647b642c43b5d5b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 10:58:56 +0800 Subject: [PATCH 014/482] add consumer for warehouse facility --- .../scenarios/supply_chain/configs.py | 23 ++++++++++++- .../facilities/warehouse_facility.py | 33 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 20047cf95..085d4e7ba 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -207,5 +207,26 @@ ] } } - ] + ], + # topology used to specify the up/downstream for facilities + # we split it from facility, so that we can support configuration inherit to override it + # for a new topology + "topology": { + # key is current facility, value if upstream facilities that will provide a certain sku + "Supplier1": { + # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", + # or any other facility in the list + "sku3": [ + "Supplier3" + ] + }, + "warehouse1": { + "sku1": [ + "Supplier1" + ], + "sku3": [ + "Supplier3" + ] + } + } } diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index 0f64eacf7..90bf81271 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -16,6 +16,10 @@ class WarehouseFacility(FacilityBase): # vehicle list transports = None + # consumers that will generate order to purchase productions + # one sku one consumer + consumers = None + def step(self, tick: int): self.storage.step(tick) self.distribution.step(tick) @@ -23,6 +27,9 @@ def step(self, tick: int): for transport in self.transports: transport.step(tick) + for consumer in self.consumers.values(): + consumer.step(tick) + def build(self, configs: dict): self.configs = configs @@ -59,6 +66,7 @@ def build(self, configs: dict): # sku information self.sku_information = {} + self.consumers = {} for sku_name, sku_config in configs["skus"].items(): sku = self.world.get_sku(sku_name) @@ -66,6 +74,15 @@ def build(self, configs: dict): self.sku_information[sku.id] = sku_info + consumer = self.world.build_unit("ConsumerUnit") + consumer.data_class = "ConsumerDataModel" + + consumer.world = self.world + consumer.facility = self + consumer.data_index = self.world.register_data_class(consumer.data_class) + + self.consumers[sku.id] = consumer + def initialize(self): # init components that related with sku number self._init_by_skus() @@ -79,6 +96,16 @@ def initialize(self): for index, transport in enumerate(self.transports): transport.initialize(transports_conf[index]) + for _, sku in self.sku_information.items(): + if sku.id in self.consumers: + consumer = self.consumers[sku.id] + + consumer.initialize({ + "data": { + "order_cost": self.configs.get("order_cost", 0) + } + }) + def reset(self): self.storage.reset() self.distribution.reset() @@ -86,6 +113,9 @@ def reset(self): for vehicle in self.transports: vehicle.reset() + for consumer in self.consumers.values(): + consumer.reset() + # NOTE: as we are using list attribute now, theirs size will be reset to defined one after frame.reset, # so we have to init them again. self._init_by_skus() @@ -97,6 +127,9 @@ def post_step(self, tick: int): for vehicle in self.transports: vehicle.post_step(tick) + for consumer in self.consumers.values(): + consumer.post_step(tick) + def _init_by_skus(self): for _, sku in self.sku_information.items(): # update storage's production info From f60f680203c63ad6708f40493a6fc7e90e599a74 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 11:43:12 +0800 Subject: [PATCH 015/482] add upstream topology, and save it state --- .../scenarios/supply_chain/facilities/base.py | 5 +++ .../facilities/warehouse_facility.py | 7 ++++ .../simulator/scenarios/supply_chain/world.py | 33 +++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py index 8c74a12e3..d3732282b 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -19,6 +19,11 @@ class FacilityBase(ABC): # different facility may contains different data sku_information: dict = None + # dictionary of upstreams + # key is the source sku id + # value is the list of facility id + upstreams: dict = None + @abstractmethod def step(self, tick: int): # called per tick diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index 90bf81271..c32b9f99b 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -140,3 +140,10 @@ def _init_by_skus(self): self.distribution.data.product_list.append(sku.id) self.distribution.data.check_in_price.append(0) self.distribution.data.delay_order_penalty.append(0) + + # update the source facilities for each consumer + for sku_id, source_facilities in self.upstreams.items(): + consumer = self.consumers[sku_id] + + for facility_id in source_facilities: + consumer.data.sources.append(facility_id) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index c497c3685..c2c77b02a 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -16,6 +16,13 @@ def __init__(self): # all the facilities in this world, key: id, value: facilities self.facilities = {} + # mapping from facility name to id + self._facility_name2id_mapping = {} + + # all the entities (units and facilities) in this world + # id -> instance + self._entities = {} + # frame of this world, this is determined by the unit selected. self.frame = None @@ -50,6 +57,8 @@ def build_unit(self, name: str): logic.id = self.gen_id() + self._entities[logic.id] = logic + return logic def build(self, configs: dict, snapshot_number: int): @@ -84,7 +93,9 @@ def build(self, configs: dict, snapshot_number: int): facility.id = self.gen_id() facility.name = facility_name + self._facility_name2id_mapping[facility_name] = facility.id self.facilities[facility.id] = facility + self._entities[facility.id] = facility # build the facility first to create related components. facility.build(facility_conf["configs"]) @@ -105,10 +116,32 @@ def build(self, configs: dict, snapshot_number: int): # . build the frame self.frame = build_frame(True, snapshot_number, data_class_in_frame) + # construct the upstream topology + topology = configs.get("topology", {}) + + for cur_facility_name, topology_conf in topology.items(): + facility = self.get_facility_by_name(cur_facility_name) + + facility.upstreams = {} + + for sku_name, source_facilities in topology_conf.items(): + sku = self.get_sku(sku_name) + + facility.upstreams[sku.id] = [self.get_facility_by_name(source_name).id for source_name in source_facilities] + # then initialize all facilities as we have the data instance. for _, facility in self.facilities.items(): facility.initialize() + def get_facility_by_id(self, facility_id: int): + return self.facilities[facility_id] + + def get_facility_by_name(self, name: str): + return self.facilities[self._facility_name2id_mapping[name]] + + def get_entity(self, entity_id: int): + return self._entities[entity_id] + def register_data_class(self, name: str): assert name in data_class_mapping From 2aeb78302dfc91c65e2be4cc44f43044725ba31a Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 15:04:53 +0800 Subject: [PATCH 016/482] add mapping from id to data model index --- examples/hello_world/supply_chain/hello.py | 7 +++- .../scenarios/supply_chain/business_engine.py | 5 ++- .../facilities/supplier_facility.py | 40 +++++++++++-------- .../facilities/warehouse_facility.py | 19 ++++----- .../simulator/scenarios/supply_chain/world.py | 14 ++++--- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index f8e7e5055..c82e27089 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -13,14 +13,17 @@ env = Env(scenario="supply_chain", topology="toy.5p_ssddd_l0.0", start_tick=start_tick, durations=durations, options=opts) +print("node info:", env.summary) + for ep in range(1): metrics, decision_event, is_done = (None, None, False) while not is_done: metrics, decision_event, is_done = env.step(None) - print(len(env.snapshot_list)) - print(env.snapshot_list["transport"][:0:"patient"].flatten()) + print("total snapshot:", len(env.snapshot_list)) + print("transport patient:", env.snapshot_list["transport"][:0:"patient"].flatten()) + print("consumer source:", env.snapshot_list["consumer"][0:0:"sources"]) env.reset() diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 5ef9cabe5..3ea52dda7 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -29,7 +29,10 @@ def snapshots(self): @property def configs(self): - pass + return self.world.configs + + def get_node_mapping(self) -> dict: + return self.world.unit_id2index_mapping def step(self, tick: int): for _, facility in self.world.facilities.items(): diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py index 382acc5c3..09c8a9339 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py @@ -36,7 +36,7 @@ def build(self, configs: dict): self.storage.world = self.world self.storage.facility = self - self.storage.data_index = self.world.register_data_class(self.storage.data_class) + self.storage.data_index = self.world.register_data_class(self.storage.id, self.storage.data_class) # construct transport self.transports = [] @@ -47,7 +47,7 @@ def build(self, configs: dict): transport.world = self.world transport.facility = self - transport.data_index = self.world.register_data_class(transport.data_class) + transport.data_index = self.world.register_data_class(transport.id, transport.data_class) self.transports.append(transport) @@ -57,7 +57,7 @@ def build(self, configs: dict): self.distribution.world = self.world self.distribution.facility = self - self.distribution.data_index = self.world.register_data_class(self.distribution.data_class) + self.distribution.data_index = self.world.register_data_class(self.distribution.id, self.distribution.data_class) # sku information self.sku_information = {} @@ -85,20 +85,23 @@ def build(self, configs: dict): supplier.world = self.world supplier.facility = self - supplier.data_index = self.world.register_data_class(supplier.data_class) + supplier.data_index = self.world.register_data_class(supplier.id, supplier.data_class) self.suppliers[sku.id] = supplier - + else: consumer = self.world.build_unit("ConsumerUnit") consumer.data_class = "ConsumerDataModel" consumer.world = self.world consumer.facility = self - consumer.data_index = self.world.register_data_class(consumer.data_class) + consumer.data_index = self.world.register_data_class(consumer.id, consumer.data_class) self.consumers[sku.id] = consumer def initialize(self): + # DO init by skus first, as other components may depend on sku information + self._init_by_skus() + for _, sku in self.sku_information.items(): if sku.id in self.suppliers: supplier = self.suppliers[sku.id] @@ -112,16 +115,13 @@ def initialize(self): } }) - consumer = self.consumers[sku.id] - - consumer.initialize({ - "data": { - "order_cost": self.configs.get("order_cost", 0) - } - }) - - # DO init by skus first, as other components may depend on sku information - self._init_by_skus() + for consumer in self.consumers.values(): + consumer.initialize({ + "data": { + # TODO: move to config + "order_cost": self.configs.get("order_cost", 0) + } + }) self.storage.initialize(self.configs.get("storage", {})) self.distribution.initialize(self.configs.get("distribution", {})) @@ -169,3 +169,11 @@ def _init_by_skus(self): self.distribution.data.product_list.append(sku.id) self.distribution.data.check_in_price.append(0) self.distribution.data.delay_order_penalty.append(0) + + if self.upstreams is not None: + # update the source facilities for each consumer + for sku_id, source_facilities in self.upstreams.items(): + consumer = self.consumers[sku_id] + + for facility_id in source_facilities: + consumer.data.sources.append(facility_id) diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index c32b9f99b..3a1258317 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -41,7 +41,7 @@ def build(self, configs: dict): self.storage.world = self.world self.storage.facility = self - self.storage.data_index = self.world.register_data_class(self.storage.data_class) + self.storage.data_index = self.world.register_data_class(self.storage.id, self.storage.data_class) # construct transport self.transports = [] @@ -52,7 +52,7 @@ def build(self, configs: dict): transport.world = self.world transport.facility = self - transport.data_index = self.world.register_data_class(transport.data_class) + transport.data_index = self.world.register_data_class(transport.id, transport.data_class) self.transports.append(transport) @@ -62,7 +62,7 @@ def build(self, configs: dict): self.distribution.world = self.world self.distribution.facility = self - self.distribution.data_index = self.world.register_data_class(self.distribution.data_class) + self.distribution.data_index = self.world.register_data_class(self.distribution.id, self.distribution.data_class) # sku information self.sku_information = {} @@ -79,7 +79,7 @@ def build(self, configs: dict): consumer.world = self.world consumer.facility = self - consumer.data_index = self.world.register_data_class(consumer.data_class) + consumer.data_index = self.world.register_data_class(consumer.id, consumer.data_class) self.consumers[sku.id] = consumer @@ -141,9 +141,10 @@ def _init_by_skus(self): self.distribution.data.check_in_price.append(0) self.distribution.data.delay_order_penalty.append(0) - # update the source facilities for each consumer - for sku_id, source_facilities in self.upstreams.items(): - consumer = self.consumers[sku_id] + if self.upstreams is not None: + # update the source facilities for each consumer + for sku_id, source_facilities in self.upstreams.items(): + consumer = self.consumers[sku_id] - for facility_id in source_facilities: - consumer.data.sources.append(facility_id) + for facility_id in source_facilities: + consumer.data.sources.append(facility_id) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index c2c77b02a..1db52e6f5 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -41,6 +41,9 @@ def __init__(self): # configuration of current world self.configs: dict = None + # unit id to related data model index + self.unit_id2index_mapping = {} + def gen_id(self): """Generate id for facility or unit.""" new_id = self._id_counter @@ -53,13 +56,13 @@ def build_unit(self, name: str): """Build an unit instance from it name via current configuration.""" assert name in unit_mapping - logic = unit_mapping[name]["class"]() + unit = unit_mapping[name]["class"]() - logic.id = self.gen_id() + unit.id = self.gen_id() - self._entities[logic.id] = logic + self._entities[unit.id] = unit - return logic + return unit def build(self, configs: dict, snapshot_number: int): """Build current world according to configurations.""" @@ -142,12 +145,13 @@ def get_facility_by_name(self, name: str): def get_entity(self, entity_id: int): return self._entities[entity_id] - def register_data_class(self, name: str): + def register_data_class(self, unit_id: int, name: str): assert name in data_class_mapping node_index = self._data_class_collection[name] self._data_class_collection[name] += 1 + self.unit_id2index_mapping[unit_id] = node_index return node_index From 93b3bb54d8f5df8728ad0beaf7b54cb12273b966 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 16:28:13 +0800 Subject: [PATCH 017/482] logic without reward of consumer --- .../scenarios/supply_chain/changes.md | 9 ++- .../scenarios/supply_chain/configs.py | 14 +++- .../facilities/supplier_facility.py | 5 +- .../facilities/warehouse_facility.py | 4 +- .../scenarios/supply_chain/units/consumer.py | 69 +++++++++++++++++-- .../supply_chain/units/distribution.py | 9 +-- .../scenarios/supply_chain/units/order.py | 6 ++ 7 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/units/order.py diff --git a/maro/simulator/scenarios/supply_chain/changes.md b/maro/simulator/scenarios/supply_chain/changes.md index 23ff3251d..b3b43dbcb 100644 --- a/maro/simulator/scenarios/supply_chain/changes.md +++ b/maro/simulator/scenarios/supply_chain/changes.md @@ -13,4 +13,11 @@ . remove output_lot_size from bom, user sku.production_rate at facility level, then action can change this . support manufacturing without source material, like oil, just produce output production by configured rate . add type for sku at facility level to identify if it is an input material, or output production - . remove output_lot_size, always be 1 \ No newline at end of file + . remove output_lot_size, always be 1 + +3. consumer: + split upstreams from facility to a standalone part, so that we can override same world structure but + different topology. + + . there is no consumer_quantity and source by default, we use first source as source_id + but with quantity as 0, means we will wait for action to purchase source sku. \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 085d4e7ba..ad5377ed0 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -97,7 +97,8 @@ "init_in_stock": 100, "production_rate": 200, "type": "production", - "cost": 10 + "cost": 10, + "price": 10 } }, "storage": { @@ -134,12 +135,16 @@ "init_in_stock": 100, "production_rate": 200, "type": "production", - "cost": 10 + "cost": 10, + "price": 100 }, # source material, do not need production rate "sku3": { "init_in_stock": 100, - "type": "material" + "production_rate": 200, + "type": "material", + "cost": 10, + "price": 100 } }, "storage": { @@ -174,12 +179,15 @@ "skus": { "sku1": { "init_stock": 1000, + "price": 100 }, "sku2": { "init_stock": 1000, + "price": 100 }, "sku3": { "init_stock": 1000, + "price": 100 }, }, "storage": { diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py index 09c8a9339..b72580eb7 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py @@ -4,7 +4,7 @@ class SupplierFacility(FacilityBase): - SkuInfo = namedtuple("SkuInfo", ("name", "id", "init_in_stock", "production_rate", "type", "cost")) + SkuInfo = namedtuple("SkuInfo", ("name", "id", "init_in_stock", "production_rate", "type", "cost", "price")) storage = None distribution = None @@ -72,7 +72,8 @@ def build(self, configs: dict): sku_config["init_in_stock"], sku_config.get("production_rate", 0), sku_config["type"], - sku_config.get("cost", 0) + sku_config.get("cost", 0), + sku_config.get("price", 0) ) self.sku_information[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index 3a1258317..51645d2c8 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -5,7 +5,7 @@ class WarehouseFacility(FacilityBase): - SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id")) + SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id", "price")) # storage unit storage = None @@ -70,7 +70,7 @@ def build(self, configs: dict): for sku_name, sku_config in configs["skus"].items(): sku = self.world.get_sku(sku_name) - sku_info = WarehouseFacility.SkuInfo(sku_name, sku_config["init_stock"], sku.id) + sku_info = WarehouseFacility.SkuInfo(sku_name, sku_config["init_stock"], sku.id, sku_config.get("price", 0)) self.sku_information[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 31195a768..8005297cf 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -1,23 +1,82 @@ - +from collections import defaultdict, Counter from .base import UnitBase +from .order import Order class ConsumerUnit(UnitBase): def __init__(self): super(ConsumerUnit, self).__init__() + # TODO: if we need this as state, we can add a new field to consumer + # and fill the field in the post_step method. + self.open_orders = defaultdict(Counter) + def initialize(self, configs: dict): super(ConsumerUnit, self).initialize(configs) + if len(self.data.sources) > 0: + # we use 1st source as default one + self.data.product_id = self.data.sources[0] + def step(self, tick: int): - pass + # NOTE: + # different with original code, we split the control into pieces, + # and put in to frame, so the product_id always has value + # + quantity = self.data.consumer_quantity + + if quantity <= 0 or len(self.data.sources) == 0: + return + + source_id = self.data.consumer_source_id + product_id = self.data.consumer_product_id + + vlt = self.data.consumer_vlt + + # NOTE: + # we are using facility as source id, not the storage + self.update_open_orders(source_id, product_id, quantity) + + sku = self.facility.sku_information[self.data.consumer_product_id] + + order = Order(self.facility, product_id, quantity, vlt) + + source_facility = self.world.get_facility_by_id(source_id) + + source_facility.distribution.place_order(order) + + self.data.total_purchased += quantity + + def post_step(self, tick: int): + self.data.received = 0 + self.data.purchased = 0 def reset(self): super(ConsumerUnit, self).reset() + self.open_orders.clear() + + def set_action(self, action): + # called before step + self.data.consumer_source_id = action.source_id + self.data.consumer_quantity = action.quantity + self.data.consumer_vlt = action.vlt + def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): - pass + self.data.total_received += quantity + self.data.received += quantity + + self.update_open_orders(source_id, product_id, -original_quantity) + + def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): + if qty_delta > 0: + # new order for product + self.open_orders[source_id][product_id] += qty_delta + else: + # an order is completed, update the remaining number + self.open_orders[source_id][product_id] += qty_delta - def update_open_orders(self, source_id, product_id, qty_delta): - pass + # TODO: refine it later, seems like we do not need this + # if len(self.open_orders[source_id]) == 0: + # del self.open_orders[source_id] diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 70670c06a..7d57f149b 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -5,14 +5,7 @@ from typing import Dict - -# TODO: original code included the order in raw state, but calculate the price in final state -# so we do not need to put it in the frame, just calculate the total_price per tick/step. -class Order: - destination = None - product_id = None - quantity = None - vlt = None +from .order import Order class DistributionUnit(UnitBase): diff --git a/maro/simulator/scenarios/supply_chain/units/order.py b/maro/simulator/scenarios/supply_chain/units/order.py new file mode 100644 index 000000000..54a71238d --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/order.py @@ -0,0 +1,6 @@ +# TODO: original code included the order in raw state, but calculate the price in final state +# so we do not need to put it in the frame, just calculate the total_price per tick/step. + +from collections import namedtuple + +Order = namedtuple("Order", ("destination", "product_id", "quantity", "vlt")) From 8ef747c86e06de54abfe54f9bda6ed2455a2cecc Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 16:45:01 +0800 Subject: [PATCH 018/482] bug fix --- .../facilities/supplier_facility.py | 9 +++---- .../facilities/warehouse_facility.py | 24 +++++++++---------- .../scenarios/supply_chain/units/consumer.py | 7 +++++- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py index b72580eb7..6188ccc9d 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py @@ -116,11 +116,12 @@ def initialize(self): } }) - for consumer in self.consumers.values(): + for sku_id, consumer in self.consumers.items(): consumer.initialize({ "data": { # TODO: move to config - "order_cost": self.configs.get("order_cost", 0) + "order_cost": self.configs.get("order_cost", 0), + "consumer_product_id": sku_id } }) @@ -146,6 +147,8 @@ def post_step(self, tick: int): consumer.post_step(tick) def reset(self): + self._init_by_skus() + self.storage.reset() self.distribution.reset() @@ -158,8 +161,6 @@ def reset(self): for consumer in self.consumers.values(): consumer.reset() - self._init_by_skus() - def _init_by_skus(self): for _, sku in self.sku_information.items(): # update storage's production info diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py index 51645d2c8..0d28d87c0 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py @@ -96,17 +96,19 @@ def initialize(self): for index, transport in enumerate(self.transports): transport.initialize(transports_conf[index]) - for _, sku in self.sku_information.items(): - if sku.id in self.consumers: - consumer = self.consumers[sku.id] - - consumer.initialize({ - "data": { - "order_cost": self.configs.get("order_cost", 0) - } - }) + for sku_id, consumer in self.consumers.items(): + consumer.initialize({ + "data": { + "order_cost": self.configs.get("order_cost", 0), + "consumer_product_id": sku_id + } + }) def reset(self): + # NOTE: as we are using list attribute now, theirs size will be reset to defined one after frame.reset, + # so we have to init them again. + self._init_by_skus() + self.storage.reset() self.distribution.reset() @@ -116,10 +118,6 @@ def reset(self): for consumer in self.consumers.values(): consumer.reset() - # NOTE: as we are using list attribute now, theirs size will be reset to defined one after frame.reset, - # so we have to init them again. - self._init_by_skus() - def post_step(self, tick: int): self.storage.post_step(tick) self.distribution.post_step(tick) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 8005297cf..ea6a83fd9 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -4,6 +4,7 @@ from .order import Order +# TODO: we need another consumer type that read data from file or predict from model class ConsumerUnit(UnitBase): def __init__(self): super(ConsumerUnit, self).__init__() @@ -17,7 +18,7 @@ def initialize(self, configs: dict): if len(self.data.sources) > 0: # we use 1st source as default one - self.data.product_id = self.data.sources[0] + self.data.consumer_source_id = self.data.sources[0] def step(self, tick: int): # NOTE: @@ -57,6 +58,10 @@ def reset(self): self.open_orders.clear() + if len(self.data.sources) > 0: + # we use 1st source as default one + self.data.consumer_source_id = self.data.sources[0] + def set_action(self, action): # called before step self.data.consumer_source_id = action.source_id From 0a4417535453c8fb5c1e918397cf97d062bf733d Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 17:09:41 +0800 Subject: [PATCH 019/482] seller unit --- .../scenarios/supply_chain/configs.py | 13 +++++-- .../scenarios/supply_chain/units/__init__.py | 1 + .../scenarios/supply_chain/units/consumer.py | 4 +++ .../supply_chain/units/distribution.py | 6 ++-- .../supply_chain/units/manufacturing.py | 4 +++ .../scenarios/supply_chain/units/seller.py | 36 +++++++++++++++++++ .../scenarios/supply_chain/units/storage.py | 2 ++ .../scenarios/supply_chain/units/transport.py | 5 +-- 8 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/units/seller.py diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index ad5377ed0..fb70153c2 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -5,7 +5,8 @@ TransportDataModel, DistributionDataModel, ManufactureDataModel, - ConsumerDataModel + ConsumerDataModel, + SellerDataModel ) from .units import ( @@ -13,7 +14,8 @@ TransportUnit, DistributionUnit, ManufacturingUnit, - ConsumerUnit + ConsumerUnit, + SellerUnit ) from .facilities import ( @@ -42,6 +44,10 @@ "ConsumerDataModel": { "alias_in_snapshot": "consumers", "class": ConsumerDataModel + }, + "SellerDataModel": { + "alias_in_snapshot": "seller", + "class": SellerDataModel } } @@ -62,6 +68,9 @@ "ConsumerUnit": { "class": ConsumerUnit }, + "SellerUnit": { + "class": SellerUnit + } } test_world_config = { diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index eff65f5db..89ce0204b 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -3,3 +3,4 @@ from .distribution import DistributionUnit from .manufacturing import ManufacturingUnit from .consumer import ConsumerUnit +from .seller import SellerUnit diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index ea6a83fd9..dc112daa2 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -6,6 +6,10 @@ # TODO: we need another consumer type that read data from file or predict from model class ConsumerUnit(UnitBase): + """Unit that used to generate orders to purchase source materials from up stream facility. + + One consumer per sku. + """ def __init__(self): super(ConsumerUnit, self).__init__() diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 7d57f149b..5fc30de8c 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -5,10 +5,12 @@ from typing import Dict -from .order import Order - class DistributionUnit(UnitBase): + """Unit that used to receive and execute orders from downstream facilities. + + One distribution can accept all kind of sku order. + """ def __init__(self): super(DistributionUnit, self).__init__() diff --git a/maro/simulator/scenarios/supply_chain/units/manufacturing.py b/maro/simulator/scenarios/supply_chain/units/manufacturing.py index 422d881ff..be1f92059 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacturing.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacturing.py @@ -3,6 +3,10 @@ class ManufacturingUnit(UnitBase): + """Unit that used to produce certain product(sku) with consume specified source skus. + + One manufacture unit per sku. + """ # source material sku and related number per produce cycle bom = None diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py new file mode 100644 index 000000000..74852c35a --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -0,0 +1,36 @@ + +import numpy as np + +from .base import UnitBase + + +class SellerUnit(UnitBase): + """ + Unit that used to generate product consume demand, and move demand product from current storage. + """ + def __init__(self): + super(SellerUnit, self).__init__() + + def initialize(self, configs: dict): + super(SellerUnit, self).initialize(configs) + + def step(self, tick: int): + product_id = self.data.product_id + sku = self.facility.sku_information[product_id] + demand = self.market_demand() + + sold_qty = self.facility.storage.take_available(product_id, demand) + + self.data.total_sold += sold_qty + self.data.sold = sold_qty + self.data.demand = demand + + def post_step(self, tick: int): + self.data.sold = 0 + self.data.demand = 0 + + def reset(self): + super(SellerUnit, self).reset() + + def market_demand(self): + return int(np.random.gamma(self.data.sale_gamma)) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 14c537136..0d3be9370 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -5,6 +5,8 @@ class StorageUnit(UnitBase): + """Unit that used to store skus. + """ def __init__(self): super().__init__() diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 30f39a483..fda31eabb 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -3,6 +3,7 @@ class TransportUnit(UnitBase): + """Unit used to move production from source to destination by order.""" def __init__(self): super().__init__() @@ -33,8 +34,8 @@ def schedule(self, destination, product_id: int, quantity: int, vlt): # Find the path from current entity to target. path = self.world.find_path( - self.entity.x, - self.entity.y, + self.facility.x, + self.facility.y, destination.parent.x, destination.parent.y ) From dc9c2e56727585641409241fc8f1e0010291498f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 18:28:49 +0800 Subject: [PATCH 020/482] use tcod for path finding --- .../scenarios/supply_chain/configs.py | 27 +++++++++++++++++ .../scenarios/supply_chain/units/transport.py | 4 +-- .../simulator/scenarios/supply_chain/world.py | 29 +++++++++++++++++++ setup.py | 3 +- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index fb70153c2..15d90fea2 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -1,4 +1,5 @@ +from enum import Enum from .data import ( StorageDataModel, @@ -24,6 +25,13 @@ ) +class CellType(Enum): + """Cell type in map grid. + """ + facility = 1 + railroad = 2 # TODO: or railway? + + data_class_mapping = { "StorageDataModel": { "alias_in_snapshot": "storages", @@ -245,5 +253,24 @@ "Supplier3" ] } + }, + "grid": { + "size": [20, 20], + # facility position in grid + "facilities": { + "Supplier1": [0, 0], # (x, y) + "Supplier3": [3, 3], + "warehouse1": [6, 6] + }, + # cells that un-traversable + "blocks": { + # TODO: later we can have different cell have different travel cost + CellType.railroad: [ + [10, 10], + [10, 11], + [10, 12], + [11, 12] + ] + } } } diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index fda31eabb..763834089 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -36,8 +36,8 @@ def schedule(self, destination, product_id: int, quantity: int, vlt): path = self.world.find_path( self.facility.x, self.facility.y, - destination.parent.x, - destination.parent.y + destination.facility.x, + destination.facility.y ) if self.path is None: diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 1db52e6f5..53fc28d87 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -1,10 +1,14 @@ +import numpy as np + from collections import defaultdict, namedtuple from .frame_builder import build_frame from .configs import unit_mapping, data_class_mapping +from tcod.path import AStar + # sku definition in world level # bom is a dictionary, key is the material sku id, value is units per lot @@ -44,6 +48,9 @@ def __init__(self): # unit id to related data model index self.unit_id2index_mapping = {} + # a star path finder + self._path_finder: AStar = None + def gen_id(self): """Generate id for facility or unit.""" new_id = self._id_counter @@ -136,6 +143,25 @@ def build(self, configs: dict, snapshot_number: int): for _, facility in self.facilities.items(): facility.initialize() + # construct the map grid + grid_config = configs["grid"] + + grid_width, grid_height = grid_config["size"] + + # travel cost for a star path finder, 0 means block, > 1 means the cost travel to that cell + # current all traversable cell's cost will be 1. + cost_grid = np.ones(shape=(grid_width, grid_height), dtype=np.int) + + # add blocks to grid + for facility_pos in grid_config["facilities"].values(): + cost_grid[facility_pos[0], facility_pos[1]] = 0 + + for block_pos in grid_config["blocks"].values(): + cost_grid[block_pos[0], block_pos[1]] = 0 + + # 0 for 2nd parameters means disable diagonal movement, so just up, right, down or left. + self._path_finder = AStar(cost_grid, 0) + def get_facility_by_id(self, facility_id: int): return self.facilities[facility_id] @@ -165,3 +191,6 @@ def get_sku(self, name: str): def get_sku_by_id(self, sku_id: int): return self._sku_collection[self._sku_id2name_mapping[sku_id]] + + def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int): + return self._path_finder.get_path(start_x, start_y, goal_x, goal_y) diff --git a/setup.py b/setup.py index 47a9c15ab..e095cce77 100644 --- a/setup.py +++ b/setup.py @@ -134,7 +134,8 @@ "paramiko==2.7.2", "kubernetes==12.0.1", "prompt_toolkit==2.0.10", - "stringcase==1.2.0" + "stringcase==1.2.0", + "tcod==11.19.3" ], entry_points={ "console_scripts": [ From 8847e8fe4c6aa54fa6081d4ce28f4d91eae6953c Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 18:55:13 +0800 Subject: [PATCH 021/482] retailer facility --- .../scenarios/supply_chain/configs.py | 43 +++++- .../supply_chain/facilities/__init__.py | 5 +- .../supply_chain/facilities/retailer.py | 132 ++++++++++++++++++ .../{supplier_facility.py => supplier.py} | 0 .../{warehouse_facility.py => warehouse.py} | 0 5 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/facilities/retailer.py rename maro/simulator/scenarios/supply_chain/facilities/{supplier_facility.py => supplier.py} (100%) rename maro/simulator/scenarios/supply_chain/facilities/{warehouse_facility.py => warehouse.py} (100%) diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 15d90fea2..01781f5bc 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -21,7 +21,8 @@ from .facilities import ( WarehouseFacility, - SupplierFacility + SupplierFacility, + RetailerFacility ) @@ -231,6 +232,38 @@ class CellType(Enum): } ] } + }, + { + "name": "ChaoShiFa01", + "class": RetailerFacility, + "configs": { + "skus": { + "sku1": { + "price": 300, + "cost": 10, + "init_in_stock": 100, + "sale_gamma": 1 + }, + "sku3": { + "price": 200, + "cost": 10, + "init_in_stock": 100, + "sale_gamma": 1 + }, + "sku2": { + "price": 100, + "cost": 10, + "init_in_stock": 100, + "sale_gamma": 1 + }, + }, + "storage": { + "data": { + "capacity": 1000, + "unit_storage_cost": 10 + } + }, + } } ], # topology used to specify the up/downstream for facilities @@ -252,6 +285,14 @@ class CellType(Enum): "sku3": [ "Supplier3" ] + }, + "ChaoShiFa01": { + "sku1": [ + "Supplier1" + ], + "sku3": [ + "Supplier3" + ] } }, "grid": { diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py index b3489ec75..26ceb1e47 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/__init__.py +++ b/maro/simulator/scenarios/supply_chain/facilities/__init__.py @@ -1,2 +1,3 @@ -from .warehouse_facility import WarehouseFacility -from .supplier_facility import SupplierFacility +from .warehouse import WarehouseFacility +from .supplier import SupplierFacility +from .retailer import RetailerFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py new file mode 100644 index 000000000..f3eafd3b1 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -0,0 +1,132 @@ + +from collections import namedtuple + +from .base import FacilityBase + + +class RetailerFacility(FacilityBase): + SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gammas")) + + storage = None + consumers: dict = None + sellers: dict = None + + def step(self, tick: int): + self.storage.step(tick) + + if self.consumers is not None: + for consumer in self.consumers.values(): + consumer.step(tick) + + if self.sellers is not None: + for seller in self.sellers.values(): + seller.step(tick) + + def post_step(self, tick: int): + self.storage.post_step(tick) + + if self.consumers is not None: + for consumer in self.consumers.values(): + consumer.post_step(tick) + + if self.sellers is not None: + for seller in self.sellers.values(): + seller.post_step(tick) + + def build(self, configs: dict): + self.configs = configs + + # construct storage + self.storage = self.world.build_unit("StorageUnit") + self.storage.data_class = "StorageDataModel" + + self.storage.world = self.world + self.storage.facility = self + self.storage.data_index = self.world.register_data_class(self.storage.id, self.storage.data_class) + + self.sku_information = {} + self.consumers = {} + self.sellers = {} + + for sku_name, sku_config in configs["skus"].items(): + sku = self.world.get_sku(sku_name) + sku_info = RetailerFacility.SkuInfo( + sku_name, + sku.id, + sku_config.get("price", 0), + sku_config.get("cost", 0), + sku_config["init_in_stock"], + sku_config["sale_gamma"] + ) + + self.sku_information[sku.id] = sku_info + + # all sku in retail are final production for sale, no material + consumer = self.world.build_unit("ConsumerUnit") + consumer.data_class = "ConsumerDataModel" + + consumer.world = self.world + consumer.facility = self + consumer.data_index = self.world.register_data_class(consumer.id, consumer.data_class) + + self.consumers[sku.id] = consumer + + # seller for this sku + seller = self.world.build_unit("SellerUnit") + seller.data_class = "SellerDataModel" + + seller.world = self.world + seller.facility = self + seller.data_index = self.world.register_data_class(seller.id, seller.data_class) + + def initialize(self): + self._init_by_sku() + + self.storage.initialize(self.configs.get("storage", {})) + + for sku_id, consumer in self.consumers.items(): + consumer.initialize({ + "data": { + # TODO: move to config + "order_cost": self.configs.get("order_cost", 0), + "consumer_product_id": sku_id + } + }) + + for sku_id, seller in self.sellers.items(): + sku = self.sku_information[sku_id] + + seller.initialize({ + "data": { + "unit_price": sku.price, + "sale_gamma": sku.sale_gamma, + "product_id": sku_id + } + }) + + def reset(self): + self._init_by_sku() + + self.storage.reset() + + if self.consumers is not None: + for consumer in self.consumers.values(): + consumer.reset() + + if self.sellers is not None: + for seller in self.sellers.values(): + seller.reset() + + def _init_by_sku(self): + for _, sku in self.sku_information.items(): + # update storage's production info + self.storage.data.product_list.append(sku.id) + self.storage.data.product_number.append(sku.init_in_stock) + + if self.upstreams is not None: + # update the source facilities for each consumer + for sku_id, source_facilities in self.upstreams.items(): + consumer = self.consumers[sku_id] + + for facility_id in source_facilities: + consumer.data.sources.append(facility_id) diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py similarity index 100% rename from maro/simulator/scenarios/supply_chain/facilities/supplier_facility.py rename to maro/simulator/scenarios/supply_chain/facilities/supplier.py diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py similarity index 100% rename from maro/simulator/scenarios/supply_chain/facilities/warehouse_facility.py rename to maro/simulator/scenarios/supply_chain/facilities/warehouse.py From 47a3aa78c48a7ea176533396e45962865a05204d Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 5 Mar 2021 19:04:46 +0800 Subject: [PATCH 022/482] bug fix, show seller demands in example --- examples/hello_world/supply_chain/hello.py | 6 +++++- maro/simulator/scenarios/supply_chain/configs.py | 6 +++--- .../simulator/scenarios/supply_chain/facilities/retailer.py | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index c82e27089..e226c41b7 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -23,7 +23,11 @@ print("total snapshot:", len(env.snapshot_list)) print("transport patient:", env.snapshot_list["transport"][:0:"patient"].flatten()) - print("consumer source:", env.snapshot_list["consumer"][0:0:"sources"]) + # print("consumer source:", env.snapshot_list["consumer"][0:0:"sources"]) + + # seller demand + # since the seller node number will not change, we can reshape it as below + print("seller demand", env.snapshot_list["seller"][::"demand"].flatten().reshape((-1, len(env.snapshot_list["seller"])))) env.reset() diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 01781f5bc..e66846fb6 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -242,19 +242,19 @@ class CellType(Enum): "price": 300, "cost": 10, "init_in_stock": 100, - "sale_gamma": 1 + "sale_gamma": 100 }, "sku3": { "price": 200, "cost": 10, "init_in_stock": 100, - "sale_gamma": 1 + "sale_gamma": 100 }, "sku2": { "price": 100, "cost": 10, "init_in_stock": 100, - "sale_gamma": 1 + "sale_gamma": 100 }, }, "storage": { diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index f3eafd3b1..4a46bae98 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -5,7 +5,7 @@ class RetailerFacility(FacilityBase): - SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gammas")) + SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gamma")) storage = None consumers: dict = None @@ -79,6 +79,8 @@ def build(self, configs: dict): seller.facility = self seller.data_index = self.world.register_data_class(seller.id, seller.data_class) + self.sellers[sku.id] = seller + def initialize(self): self._init_by_sku() From 9d2289a229bf5860ef97f36a05160d181f30013b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 11:25:53 +0800 Subject: [PATCH 023/482] add a interactive and renderable env wrapper to later debugging --- examples/hello_world/supply_chain/Lisence | 25 ++++ .../supply_chain/dejavu10x10_gs_tc.png | Bin 0 -> 8439 bytes examples/hello_world/supply_chain/hello.py | 111 +++++++++++++++--- .../scenarios/supply_chain/business_engine.py | 6 + .../scenarios/supply_chain/units/transport.py | 9 +- 5 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 examples/hello_world/supply_chain/Lisence create mode 100644 examples/hello_world/supply_chain/dejavu10x10_gs_tc.png diff --git a/examples/hello_world/supply_chain/Lisence b/examples/hello_world/supply_chain/Lisence new file mode 100644 index 000000000..b407f8da4 --- /dev/null +++ b/examples/hello_world/supply_chain/Lisence @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2009-2020, Kyle Stewart and the python-tcod contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/hello_world/supply_chain/dejavu10x10_gs_tc.png b/examples/hello_world/supply_chain/dejavu10x10_gs_tc.png new file mode 100644 index 0000000000000000000000000000000000000000..ea3adbe8661bf28ac1199ce662588fa4d9317162 GIT binary patch literal 8439 zcmch5g;y2b_xB)3mvl>9Qo6f4y}*S_mq-f;(v5U?cXuNxCEf5yhct+UbO`+B`~DH{ zS!?FZtT|`T-utub?1=a33Rvi5=l}pk^0{SE{Gj?Wr)e6c7Zo#Akq62bfLS3;_2yUb@Qn@R-Ly1$x!8dO0b=UT}U(XBZ*`d`0MWO%PN; z4Ils{y<(+H0ZDm;fShy&Eg&BWFderr-vQpU0j7-p`}06R&RvE#0$`9#Lx_-{08ryw zhRXuhB0%-DPNWGJCEbsTK>atMcM2Dy5kSQRIHBPo%m9)%U^+ra z=K*}p00`tSbVdKZtHnKF0X3D;DB2DYk_$9IWphT+)n%t*no=TuN5F6P$}Cfoz27r~ zlsk|Y=hx)~0OThSg3?|-c}!u~PEGO0wql#G{q8}2p*1((dET3@asq+da`l^iX60xi z3lKpLaCk1HKSi=O#>oG28}+dfN3sRTKU&efbo!reH0of z^_%qSKH1;xV!1bkFVbOr!pSqQuKSd+vsDgeml2eQ;lkze(Z zu=XNT^u1o|!}w>)|5Y0DqhA_V8vR26#b;CI>L6*R(8+pA7E_M@Bq@1&q1K^EPT1`I zur_QlC!7Z})Z9L%u0S*-sS#9sGn$of1oKEZT`VebM#vpapCS@o6oh&-oB&FzlEA0P zr5UMB4O67N5c~W}D9lj4BSB;Y;12p1+NHpi9M}jI`wz24roCK%?5lhs#>@v-uFN>1 z{H&SJ&17QH`~?RyRn|BwapE$=oV|x6C_F#xIoXE>q#FcT(UL628)&OBtA@zxb*d3N zBd#r`8{Cob0_A$q_>tVDsm-XQ6yd6H^(Dn}Y6=MNn-w$xG{mpaen#4)a+PL=jyLx| zNX)sp!r+RmbPITK_#C+2;W9r2Ab1fn+>Gto?|ZQTzsds&7?VIt4z9wyT`kSY?;n21uM$b zTrV%se!~~1FI`j6s#ymOE63;kk|{J9Or)ezQd5{RPi9SQeQQ&9=-TmSJ%vg=ZAN!) z{7B(2?GW+G9TO{*FnBoJiitFf1ck(k)SaX*UA2^SK8kc$n^S=6IQ<|4C&Lj2XK7{e zAxs!UPf<=`NFit8(5|Q~t=y=@fL+4Iwc{#`wR~&kVLVzp6=?sZR>3NB-XCj`Ygtu@ zl!sMXl{;!n6#MBtMIh@9s19hz6hreHOm@14s4*-^n><))?69grzXbCAU}%_J_HkqB zOn;*6j1sG?C=in2QRdNY(!5OmD1ow*?tGW+s*ZNRRF-z>7r8-j#=Ol;UdpT5&Shz6 zSDBiY3a?VEf>tRUck|btl;xCm%XR8K24YJMD>E80Dp{rYmxgP>mX|}YxA3_zK1?*Yk6y%rBdUreyQGR zYuBd`eIdOPI85KDNxmkhhJK;5rnBr#S-X0YdR6X%)j<8<*3s6AZ>05mOUSFWtq!fe zW>)6JR^J9|2Tqf^lYBdTW1hrNu)>zHt0?s;{al(q6A0@FZ)8qopGx=+UZoy!Siz@p zrxXcWU$ozut%}QY8rEuD9R{LqlXarid9MExCa3tu~ zEY^Q3aj(2JJ0ljq5&xXGBfcXlu))2dXEGts`)%agxW<5Eu1UzIj6dDWhZnDxS)eR{ z9>EGRB2X&O;ML=umk&)(tc38YpJ_FK_%%<|8MFUc6PP^n)i zvZ#3_{z>TK^MnlYl(6o#e`TzQ?^4B5UC$EECCYT8cHkBh?&bQ6qHfBrugdDdTi*e(xr#m`b>)~D2-N@x*vNcBgTYMt8(8> zo=h~2t;P#vpXI%r;PvBeCAOA?s_3#&!uPwYZG{>|ojq64cL-m<3)Mlc7Hx}cWpVha zQFWr5R_oWz;H734Xxujz)YpPwcZ>l;-wW|QURyjd(tva5ZMK*_gyxWo6GlYV608!Z z5&ADNt$FN`hdGY*WUM7OS5*CI|1kY=?&Hz^23xKHc4}qXej4^CtmTzvtWBNflj|F~ zRgSbtrq#~iR+;vq9?`Zg%d*j_Nf8O{V(lyK2W|e-x%C#a#k)_lr{vyUH@ts-t{QaR zK8cThUxgvS)P8>aY5A!1*mNsV8&P?5m-^^nRHN?Td@_tUhxmj-huyl^{@+i^IWcX` zxP^De3fRMMzdz@kxwB#MW5HswhqmdQWIcC}ZqYVxsj!4s+@ig!w@~Kax=~tDa@a~| zUS!H=X=Oedm>YCxOo-1e{NC{Tr?C1KC!1GP&r|tQd(>v-C}=*(>S8)NCnd+%kNtkl zbhWV6`@mvq(-z0^tOM6|YfkPZ>OiDRWXkZO&Gn*jbq`;+O*gYn*u>$t>u=HLq-d=AKt^0jue3}~X zTy|$Xt?AO}GP#mGky@5n5Bn)$@VvHkw&ri!>z;o(yQXl991=YJoNyh7Bl9{gBW^qt zEA%*5P+Ul8TjKN6(Z4d8=?~Kkxx~+f&*QbkL>zv1KaUruX47X2(k{}HdK`R8o__Zp zlh&;c^$$^fANAnjGQQGzJn3^^bEiBj+QiY=^`Z7>da=H5xYn8}drTrtN_s&;OuzhR z!Nax+)|*IHQfg8F&=BkM2u%XtsVx*W)d0YY9smM@0pR`xoR0wD3nu{lHU$9TbO0cB zOf(sk2LK{GC0QwLkCi`02KEF$$@7+54Odo#G58x;`T>W+VitqhW}Ito#d8F;i5n@b zz8|WA#8?n@E|{dnGT$30K}Qr$DTK@o5j_H7nXRh8RK`j_J`24mQBsk~O~IOctR&}U zfOy(JN9e%StyP6sv?uJN2+zTUJNxFfj|=v&C6*Mf>O3aWt3b(KUNtG)wk|0Mo>GZs zai@45M~bo;nhpUyQIs<#<~>&2rGi#Pav>8Od0%jf6H^K|akSWs1ddHC$CaY2l=z{J z1{d|tN1XLBqm&dqL@|htC@P~=X^WRS!c~9(U9Jc!(nm22PD+XY{3KHK<)GTI>&K_c z%KenyLPh$+Ds2`^$K=g30kwHvH5g2(NRp+BDe@ktem;0dP%l;Lht`-p?VD;6xjF{u z>-uKmNvwHZOTE@x;_n?|@?kl!fDj*X6A0ey7fOToFc>SM z2(Oi*yOfg_Nje0<%%=fw^TJqU`F#^pWWoCpbwWb|Bv9&jFFKWxMvACG?o7mvT9j7^ znvS3%%Grpbpz?iKi1P@A*nM-0k%hpzE7qUO`(Jh4lUG6t2+>>--ON1Pd88z5gW_m2 zqVm~)f&ygi7$Cn|k>-Yf(8CUqHiafK#4$xex)a|aBlFXtLDDtCRT|6%@ght&$)g4X z5(wnbkX~i9+NvTmv|k-DxUv!z;o#Zq?@f*Me}D5eq4tFPgT^*AV*hsGvK%@o_J*2mGV2X{AOSKwV9x-~iT;wI2cSlNc zmsVEk^J?P<)Av2D_=`%AvP2S+6c~(9MF`!=yi)}ROB7>{q@`Vb zHvT{|WX@InP{LCWQNqthV^l^Ixms&oE}qI=^FK_@KrIKph7S``B#8Y*)Xr#nB#-V9YXy}YZv_!`t)V$TcTnU z#+1R0YY)-n&Z|sjUP0z42aJl6Wd9RB1L;Z&>~a@VG|8T$vq5a65XFCRx-g_;qF0C( zSVoLPqSmhb?89=Gc7?+4qi^_d5@h&%8wyeDuxDj8}tr*&#T7v&MYk0vZ*x5 zk`ip^t{#Oeof=hi`cm}l@}66t9UVD15z_8r{#pt(MEb7>1%2iRqyf&4)47y!!%8V2 zc$3MlnoS}LuaB%>95NZEFN=GMH6h(&z32YOL~_u?;?hzE)Q5<|bSUd8XS9>xA9g{a z2-(EZ-Tn{^Vs=ARrGy(Yu5^r z4rWlB|Fh>1DZP|0`>MG>d!4-)T5^9g5VD>OO}GjZ+mUwWR~A%2MGkIhX`vWy|K}RK z^Fc3GzR0+4@s#RP^lgm18L1FijCwxu)H?4MO&5O0va~_u^G5&N7XfasbnFi5CQ*B0 zNsA$3MI3L*$B4pmn@ls3TAuO3*>cu#&ua%tAH~C1$ax;0TGOVjt?jF)Ki6J%c6JF7 z;!ts?(xiz|0#JFE2Pf`FCEPX3W)4@sZX6a0cj)ECIA;g8oR|o}P1~Lu$JhJ<0#*+U zYV+HFJ&c*n&Ml01>O22-PLc__L)pU`=@hOzt1^tyS2K#K+{_Gj`^m=g-_G@%G`=%q zJnk+2QA$yRDAH%tgPnbZnH67>|F2r8EPt4oyCZA1Qj@ChnSQ=4DuC-!va#9o)3k6s zzDca+!j@5D!|$&wjLR6LDU@lMXV}Q>Nch;#p4$ z7}c|vTI`5tX>UE`6%OYeLo7qAx48zVQK7R)T z5m6o@Yu@_ZnJrZ79Zcm^m6uEYt)~=m0~1|BLW1w(&C~PanYxcXqjL0i-oYVz zhy#T%HaqKy4?J~zNNR+YHpXu++;PKBO-8`!{@Nl3~tiV(K%giLW$Ax^?f=& zKaap?{Iu2<$QI(jfrrA!#|N_4>2+eL!4>DSs@TQe~+rE^;< z%b(PM*!}(u8q1DgEjX&GFi!m*j>?wWcyFh}93OAbhBr=5Pd9q}Uo!dZghfQ6qM~N6 zx3{-Ju|SE_jJ<3oGx;bfDU%pALdCZPKV9sOf^WwQHBDJj>I+}iYY??`e3^9{3x14t zuC=gFKkYipsHv&x>Jm3!F4P#D7gbeNm6nF~_fpU8Pvu~&b$a|Ye=yKDFi@MfcXSL} zMjOOHM_*Z8g~kVru;Ya^r@a%kQACqM~AAl4p~tTW9QGXP3omlUVYrSSjiI!|$pO-0hy1 z`~CgWu{%0-#@$xaxj(CPL2_{T(#FOVNjeuNmZhcTmoHzgudg#pGfFFL^nHDOjf{-+ z^z=(<#LITuusH-FS)Sf&sdaC`ZVpw5sYDG~V6=5%<%7C*L+fsF*QD1qD5R z^#+!xq!AGj>1b>F+--$DJw0{0pFJPf_0$@5-Yi??nT>y1Zrq!ec(HeIczr77`*1Zf zGIBIu78us^??Ocw@qqRrvGc&?joEh@x}Q5i(zyqqioEX5&0=@hO}=CL<4_8J&Gq;d zh(bnAuA;0Qa12Uhpsybq8VbUeG`d@%RYt@Le{X2Gw!R+onKLRf5*Y=B!>|oEZW#0- zkKD84V}oYv*iTf^V=`ggH$s! zGr>LPD(hXIE~ciY9v*Gw<>dth1(lU!JAc9QkdK$w=lT9%vDRpZgd`g{-kZu1A@;Qi z|1+lu(S*TX>o?cbI9OXBf`GKHhM^J?5}JVFWp6xfzDAGH_>+nX#wgyt;OW!trn@;k zyHUq~5ra@;<5+!OQBll{bTHu!3=B9pI8en|ovgGB$5Iwq|H{qH9U2tnTf!yZ4eii;=i}2%g3WY$!4$NjzQ67s^HbI{lv&W$sg+Ao3u8M|3*>B{k2B zLnUh6^SWUhEF|FOgG^*#axGKOQLent^*()r)Q5+45IqfZO%J9)1oeiGX>Yji%B~5^M7X*-gZBpcK-FqKDY;? zJLumgJ$_GPDesDkia?>ehIhadn`!E1$Cs+&;^MBZ>;q3Y{YJgn0LojRnYFcl|7mGR zU8>DfkdO$v{aHF)@6^Z@6#`vt5wwT)gIpdp{I0#ezCIA#ZtvTEvTNBI#7$p59!^fq z-M5P5Kg-TAqt~Zb&Ta=5CT}Ji=5C)UT@=j6u}Q9;BIn=MJNkr7!FqFhODDbl5m!&NZmdnC#}w9%uI{t;dG`6KdCafx>CADA6m|9JTx(eMHMP8 z=Vkb<0KebaNBz>mPj~F7Ej#;(E>coZL4qE_+F8mR8T6gM!M+biq@<7V55u3-B&XRgzGIgTeQ_ zPU+1d5{fK=$W+)qU)l#56SFJCW=` z*^-hpKi&fF*ggTRu%t!XYB)XvzRjE@H>Qrns>Wd%jLkL^!EwV{BbZ zhAKm!>nkr-G*s$w%mL~aUa}Zz0rPKSU_maPH(l#DFQ+L=<=$kW? zq~(LLubdc0MPp@YXKF>nKJ7_8Lx!Ef8f)J(*L=_n#(qVB$=3F6Nz<=qlQY_R&7GSn z4kPH7*R(LYX_~z?v(9!vT8WK5`}W&eYl77m&Xm_u0Yi+){NHM^tBB7wnvXqGbFv^ej~TH=^X z4BbIuPIwV&9T^cDdED`dIZKUzj86duvsb06_WfhyJW~9P{Kj-ntFWm2WB5ZyxOCD3 zddlDf+2IXt?s~+>Dv;094N|d*?H>;n>%m}S4fZ3b(<5-X2rT^1 z+gtfl5DXZP|>qn0!4Hlj()%!GX?j4wsGM3ec>Ohf;x0X*}__w^@io9G#)d{?{9JsimLI}QK3ZW(P@h$6>- z_9j8nb_h3(*4;9xmh-&x#M46)A)(AP^`1!>M7s-Bdq_(J9&TJ;BW#-cD!o_Nw6t_IY*4tdO&5b`vz%xBoO{ooMM*Rd>Y;tGFZjxJGIRIl98S&>z4i0QSbCTvz_b^dSz%#bI z`$kXYE!`MX^!>)livHbhRAJp=DAP?hwKKK%^ah36JVXiX5S%LoKLtus1A4)5L@c}& zB&qtS(9Tfr8U_@I{_+WZhyUIT%8-}I(KF!!W>NZ#>syj#d$ 0: From 266f10d3ba3ebead41df37c0b1b0287d6ff42213 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 14:41:30 +0800 Subject: [PATCH 024/482] move font to subfolder with lisence to make it more clearly --- .../hello_world/supply_chain/{ => font}/Lisence | 0 .../supply_chain/{ => font}/dejavu10x10_gs_tc.png | Bin examples/hello_world/supply_chain/hello.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename examples/hello_world/supply_chain/{ => font}/Lisence (100%) rename examples/hello_world/supply_chain/{ => font}/dejavu10x10_gs_tc.png (100%) diff --git a/examples/hello_world/supply_chain/Lisence b/examples/hello_world/supply_chain/font/Lisence similarity index 100% rename from examples/hello_world/supply_chain/Lisence rename to examples/hello_world/supply_chain/font/Lisence diff --git a/examples/hello_world/supply_chain/dejavu10x10_gs_tc.png b/examples/hello_world/supply_chain/font/dejavu10x10_gs_tc.png similarity index 100% rename from examples/hello_world/supply_chain/dejavu10x10_gs_tc.png rename to examples/hello_world/supply_chain/font/dejavu10x10_gs_tc.png diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 6f37e4aa3..276265511 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -22,7 +22,7 @@ def start(self): print("Press S for states!!!") # tileset from https://github.com/libtcod/python-tcod. - tileset = tcod.tileset.load_tilesheet("dejavu10x10_gs_tc.png", 32, TILESET_SIZE, tcod.tileset.CHARMAP_TCOD) + tileset = tcod.tileset.load_tilesheet("font/dejavu10x10_gs_tc.png", 32, TILESET_SIZE, tcod.tileset.CHARMAP_TCOD) grid_width, grid_height = self.env.configs["grid"]["size"] From af2ac894da29d5f5a3375fad0193f24978fcf88d Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 15:15:12 +0800 Subject: [PATCH 025/482] add more details for node mapping --- examples/hello_world/supply_chain/hello.py | 7 +++++++ .../scenarios/supply_chain/business_engine.py | 4 +++- .../scenarios/supply_chain/facilities/base.py | 4 ++++ .../supply_chain/facilities/retailer.py | 12 +++++++++++ .../supply_chain/facilities/supplier.py | 14 +++++++++++++ .../supply_chain/facilities/warehouse.py | 13 ++++++++++++ .../scenarios/supply_chain/units/base.py | 8 ++++++++ .../simulator/scenarios/supply_chain/world.py | 20 +++++++++++++++++++ 8 files changed, 81 insertions(+), 1 deletion(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 276265511..464c8f9e7 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import tcod +import pprint from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action @@ -93,11 +94,17 @@ def show_states(self): seller_number = len(self.env.snapshot_list["seller"]) print("seller demand:\n", self.env.snapshot_list["seller"][::"demand"].flatten().reshape((-1, seller_number))) + def main(): start_tick = 0 durations = 100 env = Env(scenario="supply_chain", topology="no", start_tick=start_tick, durations=durations) + pp = pprint.PrettyPrinter(indent=2, depth=8) + + print("env summary:") + pp.pprint(env.summary) + irenv = InteractiveRenderaleEnv(env) for ep in range(1): diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 08f91bbca..22b2f5002 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -17,6 +17,8 @@ def __init__(self, **kwargs): self._build_world() + self._node_mapping = self.world.get_node_mapping() + self._frame = self.world.frame @property @@ -32,7 +34,7 @@ def configs(self): return self.world.configs def get_node_mapping(self) -> dict: - return self.world.unit_id2index_mapping + return self._node_mapping def step(self, tick: int): for _, facility in self.world.facilities.items(): diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py index d3732282b..3c8c47ca5 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -46,3 +46,7 @@ def initialize(self): def reset(self): # called per episode. pass + + @abstractmethod + def get_node_info(self) -> dict: + pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 4a46bae98..330e16cf2 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -119,6 +119,18 @@ def reset(self): for seller in self.sellers.values(): seller.reset() + def get_node_info(self) -> dict: + return { + "id": self.id, + "name": self.name, + "class": type(self), + "units": { + "storage": self.storage.get_unit_info(), + "consumers": [consumer.get_unit_info() for consumer in self.consumers.values()], + "sellers": [seller.get_unit_info() for seller in self.sellers.values()] + } + } + def _init_by_sku(self): for _, sku in self.sku_information.items(): # update storage's production info diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 6188ccc9d..06efec0e5 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -161,6 +161,20 @@ def reset(self): for consumer in self.consumers.values(): consumer.reset() + def get_node_info(self) -> dict: + return { + "id": self.id, + "name": self.name, + "class": type(self), + "units": { + "storage": self.storage.get_unit_info(), + "consumers": [consumer.get_unit_info() for consumer in self.consumers.values()], + "transports": [vehicle.get_unit_info() for vehicle in self.transports], + "distribution": self.distribution.get_unit_info(), + "suppliers": [supplier.get_unit_info() for supplier in self.suppliers.values()] + } + } + def _init_by_skus(self): for _, sku in self.sku_information.items(): # update storage's production info diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 0d28d87c0..006fd3872 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -128,6 +128,19 @@ def post_step(self, tick: int): for consumer in self.consumers.values(): consumer.post_step(tick) + def get_node_info(self) -> dict: + return { + "id": self.id, + "name": self.name, + "class": type(self), + "units": { + "storage": self.storage.get_unit_info(), + "consumers": [consumer.get_unit_info() for consumer in self.consumers.values()], + "transports": [vehicle.get_unit_info() for vehicle in self.transports], + "distribution": self.distribution.get_unit_info() + } + } + def _init_by_skus(self): for _, sku in self.sku_information.items(): # update storage's production info diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py index e6befd9d1..a2d8cb8e8 100644 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -54,3 +54,11 @@ def reset(self): def set_action(self, action): # called after received an action. pass + + def get_unit_info(self) -> dict: + return { + "id": self.id, + "node_name": type(self.data).__node_name__, + "node_index": self.data_index, + "class": type(self) + } diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 53fc28d87..c8bf88469 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -194,3 +194,23 @@ def get_sku_by_id(self, sku_id: int): def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int): return self._path_finder.get_path(start_x, start_y, goal_x, goal_y) + + def get_node_mapping(self): + facility_info_dict = {facility_id: facility.get_node_info() for facility_id, facility in self.facilities.items()} + + # pick unit id and related index and node name + id2index_mapping = {} + + for facility in facility_info_dict.values(): + for units in facility["units"].values(): + if type(units) is dict: + # one unit + id2index_mapping[units["id"]] = (units["node_name"], units["node_index"]) + elif type(units) is list: + for unit in units: + id2index_mapping[unit["id"]] = (unit["node_name"], unit["node_index"]) + + return { + "mapping": id2index_mapping, + "detail": facility_info_dict + } From e6ef16a92f127fe203b36f44744d2e96ca041e0c Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 15:19:47 +0800 Subject: [PATCH 026/482] dispatch action by unit id --- .../scenarios/supply_chain/business_engine.py | 22 ++++++++++++++----- .../scenarios/supply_chain/units/__init__.py | 1 + 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 22b2f5002..b18318f1b 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -5,6 +5,7 @@ from maro.event_buffer import MaroEvents, CascadeEvent, AtomEvent +from .units import UnitBase from .world import World from .configs import test_world_config @@ -53,6 +54,9 @@ def post_step(self, tick: int): # TODO: anything need to reset per tick? + for facility in self.world.facilities.values(): + facility.post_step(tick) + return tick+1 == self._max_tick def reset(self): @@ -64,21 +68,29 @@ def reset(self): facility.reset() def _register_events(self): - self._event_buffer.register_event_handler(MaroEvents.TAKE_ACTION, self._on_action_recieved) + self._event_buffer.register_event_handler(MaroEvents.TAKE_ACTION, self._on_action_received) def _build_world(self): self.update_config_root_path(__file__) - config_path = os.path.join(self._config_path, "config.yml") + # config_path = os.path.join(self._config_path, "config.yml") self.world = World() self.world.build(test_world_config, self.calc_max_snapshots()) - def _on_action_recieved(self, event): + def _on_action_received(self, event): action = event.payload if action: - pass + # NOTE: + # we assume that the action is a dictionary that + # key is the id of unit + # value is the action for specified unit, the type may different by different type + + for unit_id, control_action in action.items(): + # try to find the unit + unit: UnitBase = self.world.get_entity(unit_id) - # TODO: how to dispatch it to units? + # dispatch the action + unit.set_action(control_action) diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index 89ce0204b..230361c8b 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -1,3 +1,4 @@ +from .base import UnitBase from .storage import StorageUnit from .transport import TransportUnit from .distribution import DistributionUnit From 0ce7abb11f67d224fd3a0e0433f91c11b52aac2e Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 17:55:23 +0800 Subject: [PATCH 027/482] merge the frame changes to support data model inherit --- maro/backends/frame.pyx | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/maro/backends/frame.pyx b/maro/backends/frame.pyx index 6f43d2765..6b00f8fd4 100644 --- a/maro/backends/frame.pyx +++ b/maro/backends/frame.pyx @@ -74,6 +74,15 @@ def node(name: str): return node_dec +def try_get_attribute(target, name, default=None): + try: + attr = object.__getattribute__(target, name) + + return attr + except: + return default + + cdef class NodeAttribute: def __cinit__(self, object dtype = None, SLOT_INDEX slot_num = 1, is_const = False, is_list = False): # Check the type of dtype, used to compact with old version @@ -151,9 +160,6 @@ cdef class _NodeAttributeAccessor: self._slot_number += 1 - if not self._is_list and "_cb" in self.__dict__: - self._cb(None) - def resize(self, new_size: int): """Resize current list attribute with specified new size. @@ -170,9 +176,6 @@ cdef class _NodeAttributeAccessor: self._slot_number = new_size - if not self._is_list and "_cb" in self.__dict__: - self._cb(None) - def clear(self): """Clear all items in current list attribute. @@ -186,9 +189,6 @@ cdef class _NodeAttributeAccessor: self._slot_number = 0 - if not self._is_list and "_cb" in self.__dict__: - self._cb(None) - def insert(self, slot_index: int, value: object): """Insert a value to specified slot. @@ -203,9 +203,6 @@ cdef class _NodeAttributeAccessor: self._slot_number += 1 - if not self._is_list and "_cb" in self.__dict__: - self._cb(None) - def remove(self, slot_index: int): """Remove specified slot. @@ -219,9 +216,6 @@ cdef class _NodeAttributeAccessor: self._slot_number -= 1 - if not self._is_list and "_cb" in self.__dict__: - self._cb(None) - def where(self, filter_func: callable): """Filter current attribute slots with input function. @@ -401,7 +395,9 @@ cdef class NodeBase: cdef str cb_name cdef _NodeAttributeAccessor attr_acc - for name, attr in type(self).__dict__.items(): + for name in dir(type(self)): + attr = getattr(self, name) + # Append an attribute access wrapper to current instance. if isinstance(attr, NodeAttribute): # Register attribute. @@ -648,7 +644,9 @@ cdef class FrameBase: cdef NODE_INDEX i # Register node and attribute in backend. - for frame_attr_name, frame_attr in type(self).__dict__.items(): + for frame_attr_name in dir(type(self)): + frame_attr = getattr(self, frame_attr_name) + # We only care about FrameNode instance. if isinstance(frame_attr, FrameNode): node_cls = frame_attr._node_cls @@ -671,8 +669,10 @@ cdef class FrameBase: attr_name_type_dict = {} # Register attributes. - for node_attr_name, node_attr in node_cls.__dict__.items(): - if isinstance(node_attr, NodeAttribute): + for node_attr_name in dir(node_cls): + node_attr = getattr(node_cls, node_attr_name) + + if node_attr and isinstance(node_attr, NodeAttribute): attr_type = self._backend.add_attr(node_type, node_attr_name, node_attr._dtype, node_attr._slot_number, node_attr._is_const, node_attr._is_list) attr_name_type_dict[node_attr_name] = attr_type From 17996b06dcd7d14d82f6ad1274a8e995aff72af4 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 17:56:49 +0800 Subject: [PATCH 028/482] add action for consumer, so that we can push the requirement --- maro/simulator/scenarios/supply_chain/__init__.py | 2 ++ maro/simulator/scenarios/supply_chain/units/actions.py | 7 +++++++ maro/simulator/scenarios/supply_chain/units/transport.py | 2 ++ 3 files changed, 11 insertions(+) create mode 100644 maro/simulator/scenarios/supply_chain/units/actions.py diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index e69de29bb..28bc2a1b2 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -0,0 +1,2 @@ + +from .units.actions import ConsumerAction diff --git a/maro/simulator/scenarios/supply_chain/units/actions.py b/maro/simulator/scenarios/supply_chain/units/actions.py new file mode 100644 index 000000000..3e07934bb --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/actions.py @@ -0,0 +1,7 @@ + +from collections import namedtuple + +# actions for units + + +ConsumerAction = namedtuple("ConsumerAction", ("id", "source_id", "quantity", "vlt")) diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 98023a495..90b296581 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -113,6 +113,8 @@ def step(self, tick: int): # Moving to destination if self.data.payload > 0: # Closer to destination until 0. + + # TODO: BUG, fix it later. self.data.location += 1 self.data.steps -= 1 From 92e8240beb239d7bb984af3551b9c30dbd397e8b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 18:45:20 +0800 Subject: [PATCH 029/482] add unit id and facility in state for unit, add storage id for manufacture unit to simple the state retrieving --- examples/hello_world/supply_chain/hello.py | 75 +++++++++++++++---- .../scenarios/supply_chain/business_engine.py | 6 +- .../scenarios/supply_chain/configs.py | 4 +- .../scenarios/supply_chain/units/base.py | 1 + .../supply_chain/units/manufacturing.py | 10 ++- 5 files changed, 77 insertions(+), 19 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 464c8f9e7..a8eac6ffe 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -4,8 +4,9 @@ import tcod import pprint +from tabulate import tabulate from maro.simulator import Env -from maro.simulator.scenarios.cim.common import Action +from maro.simulator.scenarios.supply_chain import ConsumerAction WINDOW_SCALE_FACTOR = 2 @@ -13,14 +14,27 @@ CHAR_DEFAULT = 0x2610 +# https://stackoverflow.com/questions/287871/how-to-print-colored-text-to-the-terminal +class bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + class InteractiveRenderaleEnv: def __init__(self, env: Env): self.env = env def start(self): - print("Press SPACE for next step!!!") - print("Press S for states!!!") + print(f"{bcolors.WARNING}Press SPACE for next step!!!{bcolors.ENDC}") + print(f"{bcolors.WARNING}Press S to show current debug states!!!{bcolors.ENDC}") + print(f"{bcolors.WARNING}Press M to show env summary!!!{bcolors.ENDC}") # tileset from https://github.com/libtcod/python-tcod. tileset = tcod.tileset.load_tilesheet("font/dejavu10x10_gs_tc.png", 32, TILESET_SIZE, tcod.tileset.CHARMAP_TCOD) @@ -78,21 +92,61 @@ def start(self): # push environment to next step metrics, decision_event, is_done = self.env.step(None) - print("Current environment tick:", self.env.tick) + print(f"{bcolors.OKGREEN}Current environment tick:", self.env.tick, f"{bcolors.ENDC}") if is_done: return elif event.sym == tcod.event.K_s: # show state we predefined self.show_states() + elif event.sym == tcod.event.K_m: + self.show_summary() + + def show_summary(self): + pp = pprint.PrettyPrinter(indent=2, depth=8) + + print(f"{bcolors.HEADER}env summary:{bcolors.ENDC}") + + pp.pprint(self.env.summary) def show_states(self): - print("total snapshots:\n", len(self.env.snapshot_list)) - print("transport patient:\n", self.env.snapshot_list["transport"][:0:"patient"].flatten()) + # print("total snapshots:\n", len(self.env.snapshot_list)) + # print("transport patient:\n", self.env.snapshot_list["transport"][:0:"patient"].flatten()) # since the seller node number will not change, we can reshape it as below - seller_number = len(self.env.snapshot_list["seller"]) - print("seller demand:\n", self.env.snapshot_list["seller"][::"demand"].flatten().reshape((-1, seller_number))) + # seller_number = len(self.env.snapshot_list["seller"]) + # print("seller demand:\n", self.env.snapshot_list["seller"][::"demand"].flatten().reshape((-1, seller_number))) + + self.show_manufacture_states() + + def show_manufacture_states(self): + # This function is used to debug manufacturing logic. + manufactures = self.env.snapshot_list["manufacture"] + manufacture_unit_number = len(manufactures) + + # manufacture number for current tick + features = ("id", "facility_id", "storage_id", "output_product_id", "product_unit_cost", "production_rate", "manufacturing_number") + states = manufactures[self.env.frame_index::features].flatten().reshape(manufacture_unit_number, -1) + + # show manufacture unit data + print(f"{bcolors.HEADER}Manufacture states:{bcolors.ENDC}") + print(tabulate(states, headers=features)) + + # show storage state to see if product changed + print(f"{bcolors.HEADER}Storage states:{bcolors.ENDC}") + + for state in states: + facility_id = int(state[1]) + + print(facility_id) + + + def choose_action(self): + # dummy actions + + # push consumers to generate order + # consumers = + pass def main(): @@ -100,11 +154,6 @@ def main(): durations = 100 env = Env(scenario="supply_chain", topology="no", start_tick=start_tick, durations=durations) - pp = pprint.PrettyPrinter(indent=2, depth=8) - - print("env summary:") - pp.pprint(env.summary) - irenv = InteractiveRenderaleEnv(env) for ep in range(1): diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index b18318f1b..ae0f4470c 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -22,6 +22,8 @@ def __init__(self, **kwargs): self._frame = self.world.frame + self._action_steps = self.world.configs["action_steps"] + @property def frame(self): return self._frame @@ -42,8 +44,8 @@ def step(self, tick: int): facility.step(tick) # TODO: debug only, ask for action per 5 ticks. - if tick % 5 == 0: - decision_event = self._event_buffer.gen_decision_event(tick, "payload") + if tick % self._action_steps == 0: + decision_event = self._event_buffer.gen_decision_event(tick, None) self._event_buffer.insert_event(decision_event) diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index e66846fb6..b8b245c0d 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -313,5 +313,7 @@ class CellType(Enum): [11, 12] ] } - } + }, + # how many ticks to ask for an action. + "action_steps": 1 } diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py index a2d8cb8e8..0b93e2ffa 100644 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -34,6 +34,7 @@ def initialize(self, configs: dict): """Initialize current unit""" # called after frame ready self.configs = configs + self.data.set_id(self.id, self.facility.id) self.data.initialize(configs.get("data", {})) def step(self, tick: int): diff --git a/maro/simulator/scenarios/supply_chain/units/manufacturing.py b/maro/simulator/scenarios/supply_chain/units/manufacturing.py index be1f92059..42b0670d9 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacturing.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacturing.py @@ -20,6 +20,9 @@ def __init__(self): super(ManufacturingUnit, self).__init__() def initialize(self, configs: dict): + # add the storage_id + configs["data"]["storage_id"] = self.facility.storage.id + super().initialize(configs) # grab bom of current production @@ -46,8 +49,9 @@ def step(self, tick: int): if storage_remaining_space >= space_taken_per_cycle: # if not reach the storage limitation of current production if current_product_number < unit_num_upper_bound: - # if we have enough source materials - if self.facility.storage.try_take_units(self.bom): + # if we do not need any material, then just generate the out product. + # or if we have enough source materials + if len(self.bom) == 0 or self.facility.storage.try_take_units(self.bom): self.facility.storage.try_add_units({self.data.output_product_id: self.output_units_per_lot}) # update manufacturing number in state @@ -55,7 +59,7 @@ def step(self, tick: int): def post_step(self, tick: int): # reset the manufacture cost per tick - self.data.manufacturing_cost = 0 + self.data.manufacturing_number = 0 def set_action(self, action: int): # we expect production rate number as action From 27fa332fb56a7aeee6fad557217ca71333107f64 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 19:19:51 +0800 Subject: [PATCH 030/482] show manufacture related debug info step by step --- examples/hello_world/supply_chain/hello.py | 34 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index a8eac6ffe..9e655dd34 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import numpy as np import tcod import pprint @@ -122,11 +123,14 @@ def show_states(self): def show_manufacture_states(self): # This function is used to debug manufacturing logic. manufactures = self.env.snapshot_list["manufacture"] + storages = self.env.snapshot_list["storage"] + manufacture_unit_number = len(manufactures) + storage_unit_number = len(storages) # manufacture number for current tick features = ("id", "facility_id", "storage_id", "output_product_id", "product_unit_cost", "production_rate", "manufacturing_number") - states = manufactures[self.env.frame_index::features].flatten().reshape(manufacture_unit_number, -1) + states = manufactures[self.env.frame_index::features].flatten().reshape(manufacture_unit_number, -1).astype(np.int) # show manufacture unit data print(f"{bcolors.HEADER}Manufacture states:{bcolors.ENDC}") @@ -135,10 +139,34 @@ def show_manufacture_states(self): # show storage state to see if product changed print(f"{bcolors.HEADER}Storage states:{bcolors.ENDC}") + storage_features = ["id", "remaining_space", "capacity"] + storage_states_summary = [] + for state in states: - facility_id = int(state[1]) + # DO NOTE: this is id, CANNOT be used to query + facility_id = state[1] + storage_id = state[2] + + storage_node_name, storage_index = self.env.summary["node_mapping"]["mapping"][storage_id] + + # NOTE: we cannot mix list and normal attribute to query state, + # we have to query one by one + product_list = storages[self.env.frame_index:storage_index:"product_list"].flatten().astype(np.int) + product_number = storages[self.env.frame_index:storage_index:"product_number"].flatten().astype(np.int) + storage_states = storages[self.env.frame_index:storage_index:storage_features].flatten().astype(np.int) + + # print(product_list) + # print(product_number) + # print(storage_states) + + cur_storage_states = [] + cur_storage_states.extend(storage_states) + cur_storage_states.append(product_list) + cur_storage_states.append(product_number) + + storage_states_summary.append(cur_storage_states) - print(facility_id) + print(tabulate(storage_states_summary, headers=storage_features+["product_list", "product_number"])) def choose_action(self): From 617e9c4409906f58edf8ae1af8d64743a7fb67d8 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 19:33:29 +0800 Subject: [PATCH 031/482] add bom info for debug --- examples/hello_world/supply_chain/hello.py | 22 ++++++++++++++----- .../supply_chain/units/manufacturing.py | 1 + 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 9e655dd34..e51c6193d 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -132,6 +132,23 @@ def show_manufacture_states(self): features = ("id", "facility_id", "storage_id", "output_product_id", "product_unit_cost", "production_rate", "manufacturing_number") states = manufactures[self.env.frame_index::features].flatten().reshape(manufacture_unit_number, -1).astype(np.int) + # show bom + bom_info = [] + for state in states: + output_product_id = state[3] + + # NOTE: we are using internal data to simplify the code + sku = self.env._business_engine.world.get_sku_by_id(output_product_id) + + # this sku need source material + if len(sku.bom) > 0: + bom_info.append([output_product_id, [s for s in sku.bom.keys()], [s for s in sku.bom.values()]]) + else: + bom_info.append([output_product_id, "None", "None"]) + + print(f"{bcolors.HEADER}SKU bom info:{bcolors.ENDC}") + print(tabulate(bom_info, ("product id", "source materials", "source material cost per lot"))) + # show manufacture unit data print(f"{bcolors.HEADER}Manufacture states:{bcolors.ENDC}") print(tabulate(states, headers=features)) @@ -155,10 +172,6 @@ def show_manufacture_states(self): product_number = storages[self.env.frame_index:storage_index:"product_number"].flatten().astype(np.int) storage_states = storages[self.env.frame_index:storage_index:storage_features].flatten().astype(np.int) - # print(product_list) - # print(product_number) - # print(storage_states) - cur_storage_states = [] cur_storage_states.extend(storage_states) cur_storage_states.append(product_list) @@ -168,7 +181,6 @@ def show_manufacture_states(self): print(tabulate(storage_states_summary, headers=storage_features+["product_list", "product_number"])) - def choose_action(self): # dummy actions diff --git a/maro/simulator/scenarios/supply_chain/units/manufacturing.py b/maro/simulator/scenarios/supply_chain/units/manufacturing.py index 42b0670d9..bc5026711 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacturing.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacturing.py @@ -40,6 +40,7 @@ def step(self, tick: int): unit_num_upper_bound = self.facility.storage.data.capacity // sku_num # one lot per time, until no enough space to hold output, or no enough source material + # TODO: simplify this part to make it faster for _ in range(self.data.production_rate): storage_remaining_space = self.facility.storage.data.remaining_space current_product_number = self.facility.storage.get_product_number(self.data.output_product_id) From d9719d5f80108bebe3626f386ddae1d605c6f703 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 20:26:06 +0800 Subject: [PATCH 032/482] add x,y to facility, bug fix --- examples/hello_world/supply_chain/hello.py | 44 +++++++++++++++++-- .../scenarios/supply_chain/facilities/base.py | 5 +++ .../scenarios/supply_chain/units/consumer.py | 7 +++ .../supply_chain/units/distribution.py | 4 +- .../scenarios/supply_chain/units/transport.py | 11 ++--- .../simulator/scenarios/supply_chain/world.py | 11 ++++- 6 files changed, 69 insertions(+), 13 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index e51c6193d..834b5e11c 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -36,6 +36,8 @@ def start(self): print(f"{bcolors.WARNING}Press SPACE for next step!!!{bcolors.ENDC}") print(f"{bcolors.WARNING}Press S to show current debug states!!!{bcolors.ENDC}") print(f"{bcolors.WARNING}Press M to show env summary!!!{bcolors.ENDC}") + print(f"{bcolors.WARNING}Press A to choose action, {bcolors.FAIL}NOTE: action must be none at beginning!!!{bcolors.ENDC}") + print(f"{bcolors.WARNING}Press ESCAPE to clear action!!!{bcolors.ENDC}") # tileset from https://github.com/libtcod/python-tcod. tileset = tcod.tileset.load_tilesheet("font/dejavu10x10_gs_tc.png", 32, TILESET_SIZE, tcod.tileset.CHARMAP_TCOD) @@ -61,6 +63,8 @@ def start(self): tileset=tileset, title="Supply chain environment" ) as ctx: + action = None + while True: # clear console.clear() @@ -89,9 +93,10 @@ def start(self): if event.type == "KEYDOWN": # press SPACE to next step - if event.sym == tcod.event.K_SPACE : + if event.sym == tcod.event.K_SPACE: # push environment to next step - metrics, decision_event, is_done = self.env.step(None) + + metrics, decision_event, is_done = self.env.step(action) print(f"{bcolors.OKGREEN}Current environment tick:", self.env.tick, f"{bcolors.ENDC}") @@ -101,7 +106,12 @@ def start(self): # show state we predefined self.show_states() elif event.sym == tcod.event.K_m: + # show summary self.show_summary() + elif event.sym == tcod.event.K_a: + action = self.choose_action() + elif event.sym == tcod.event.K_ESCAPE: + action = None def show_summary(self): pp = pprint.PrettyPrinter(indent=2, depth=8) @@ -182,11 +192,37 @@ def show_manufacture_states(self): print(tabulate(storage_states_summary, headers=storage_features+["product_list", "product_number"])) def choose_action(self): + action = {} + + consumer_action = self.choose_consumer_action() + + action[consumer_action.id] = consumer_action + + print(action) + + return action + + def choose_consumer_action(self): # dummy actions # push consumers to generate order - # consumers = - pass + + # check if lack of any source material + storages = self.env.snapshot_list["storage"] + + for storage_index in range(len(storages)): + product_list = storages[self.env.frame_index:storage_index:"product_list"].flatten().astype(np.int) + product_number = storages[self.env.frame_index:storage_index:"product_number"].flatten().astype(np.int) + + for product_id, product_number in zip(product_list, product_number): + if product_number <= 0: + facility_id = storages[self.env.frame_index:storage_index:"facility_id"].flatten()[0] + + for consumer in self.env.summary["node_mapping"]["detail"][facility_id]["units"]["consumers"]: + if consumer["sku_id"] == product_id: + upstreams = self.env.snapshot_list["consumer"][self.env.frame_index:consumer["node_index"]:"sources"].flatten().astype(np.int) + + return ConsumerAction(consumer["id"], upstreams[0], 30, 1) def main(): diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py index 3c8c47ca5..da84f88b6 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -6,6 +6,10 @@ class FacilityBase(ABC): # current world world = None + # position in the world + x: int = None + y: int = None + # configuration of this facility configs: dict = None @@ -17,6 +21,7 @@ class FacilityBase(ABC): # sku information, same as original sku_in_stock # different facility may contains different data + # TODO: provide sku by methods, not expose sku property sku_information: dict = None # dictionary of upstreams diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index dc112daa2..2fb77f420 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -89,3 +89,10 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): # TODO: refine it later, seems like we do not need this # if len(self.open_orders[source_id]) == 0: # del self.open_orders[source_id] + + def get_unit_info(self) -> dict: + info = super(ConsumerUnit, self).get_unit_info() + + info["sku_id"] = self.data.consumer_product_id + + return info diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 5fc30de8c..ff13b18dc 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -29,7 +29,7 @@ def initialize(self, configs: dict): def step(self, tick: int): for vehicle in self.facility.transports: # if we have vehicle not on the way and there is any pending order - if len(self.order_queue) > 0 and vehicle.datamodel.location == 0: + if len(self.order_queue) > 0 and vehicle.data.location == 0: order = self.order_queue.popleft() # schedule a job for vehicle @@ -63,7 +63,7 @@ def get_pending_order(self): def place_order(self, order): if order.quantity > 0: - sku = self.facility.get_sku_by_id(order.product_id) + sku = self.facility.sku_information[order.product_id] if sku is not None: self.order_queue.append(order) diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 90b296581..b32e3c9e7 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -39,8 +39,8 @@ def schedule(self, destination, product_id: int, quantity: int, vlt): self.path = self.world.find_path( self.facility.x, self.facility.y, - destination.facility.x, - destination.facility.y + destination.x, + destination.y ) if self.path is None: @@ -98,7 +98,6 @@ def step(self, tick: int): # Failed to load, check the patient. if self.patient < 0: - # TODO: not implemented, refactor the name. self.destination.consumer.update_open_orders( self.facility.id, self.data.product_id, @@ -114,10 +113,12 @@ def step(self, tick: int): if self.data.payload > 0: # Closer to destination until 0. - # TODO: BUG, fix it later. - self.data.location += 1 + self.data.location += self.data.vlt self.data.steps -= 1 + if self.data.location >= len(self.path): + self.data.location = len(self.path) - 1 + self.data.position[:] = self.path[self.data.location] else: # avoid update under idle state. diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index c8bf88469..12279dde1 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -153,8 +153,15 @@ def build(self, configs: dict, snapshot_number: int): cost_grid = np.ones(shape=(grid_width, grid_height), dtype=np.int) # add blocks to grid - for facility_pos in grid_config["facilities"].values(): - cost_grid[facility_pos[0], facility_pos[1]] = 0 + for facility_name, facility_pos in grid_config["facilities"].items(): + facility_id = self._facility_name2id_mapping[facility_name] + facility = self.facilities[facility_id] + + facility.x = facility_pos[0] + facility.y = facility_pos[1] + + # facility is a block, so cost is 0 + cost_grid[facility.x, facility.y] = 0 for block_pos in grid_config["blocks"].values(): cost_grid[block_pos[0], block_pos[1]] = 0 From 3f375b90a624ded81a4ef1169f39cc4237438a1f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 20:49:43 +0800 Subject: [PATCH 033/482] fix bugs in transport and distribution unit, correct the path finding issue --- examples/hello_world/supply_chain/hello.py | 20 +++++++++++++------ .../scenarios/supply_chain/configs.py | 6 ++++++ .../supply_chain/facilities/supplier.py | 5 +++-- .../supply_chain/facilities/warehouse.py | 10 ++++++++-- .../supply_chain/units/distribution.py | 2 +- .../scenarios/supply_chain/units/transport.py | 4 +++- .../simulator/scenarios/supply_chain/world.py | 7 ++++--- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 834b5e11c..e53615faa 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -121,14 +121,22 @@ def show_summary(self): pp.pprint(self.env.summary) def show_states(self): - # print("total snapshots:\n", len(self.env.snapshot_list)) - # print("transport patient:\n", self.env.snapshot_list["transport"][:0:"patient"].flatten()) + self.show_manufacture_states() - # since the seller node number will not change, we can reshape it as below - # seller_number = len(self.env.snapshot_list["seller"]) - # print("seller demand:\n", self.env.snapshot_list["seller"][::"demand"].flatten().reshape((-1, seller_number))) + self.show_vehicle_states() - self.show_manufacture_states() + def show_vehicle_states(self): + vehicles = self.env.snapshot_list["transport"] + + vehicle_number = len(vehicles) + + vehicle_features = ("id", "facility_id", "location", "steps", "patient", "source", "destination", "payload", "product_id", ) + + vehicle_states = vehicles[self.env.frame_index::vehicle_features].flatten().reshape(vehicle_number, -1).astype(np.int) + + print(f"{bcolors.HEADER}Vehicle states:{bcolors.ENDC}") + + print(tabulate(vehicle_states, vehicle_features)) def show_manufacture_states(self): # This function is used to debug manufacturing logic. diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index b8b245c0d..ec76eeef5 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -114,6 +114,7 @@ class CellType(Enum): "sku3": { "init_in_stock": 100, "production_rate": 200, + "delay_order_penalty": 10, "type": "production", "cost": 10, "price": 10 @@ -152,6 +153,7 @@ class CellType(Enum): "sku1": { "init_in_stock": 100, "production_rate": 200, + "delay_order_penalty": 10, "type": "production", "cost": 10, "price": 100 @@ -160,6 +162,7 @@ class CellType(Enum): "sku3": { "init_in_stock": 100, "production_rate": 200, + "delay_order_penalty": 10, "type": "material", "cost": 10, "price": 100 @@ -197,14 +200,17 @@ class CellType(Enum): "skus": { "sku1": { "init_stock": 1000, + "delay_order_penalty": 10, "price": 100 }, "sku2": { "init_stock": 1000, + "delay_order_penalty": 10, "price": 100 }, "sku3": { "init_stock": 1000, + "delay_order_penalty": 10, "price": 100 }, }, diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 06efec0e5..0cf2ea051 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -4,7 +4,7 @@ class SupplierFacility(FacilityBase): - SkuInfo = namedtuple("SkuInfo", ("name", "id", "init_in_stock", "production_rate", "type", "cost", "price")) + SkuInfo = namedtuple("SkuInfo", ("name", "id", "init_in_stock", "production_rate", "type", "cost", "price", "delay_order_penalty")) storage = None distribution = None @@ -73,7 +73,8 @@ def build(self, configs: dict): sku_config.get("production_rate", 0), sku_config["type"], sku_config.get("cost", 0), - sku_config.get("price", 0) + sku_config.get("price", 0), + sku_config.get("delay_order_penalty", 0) ) self.sku_information[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 006fd3872..5442334e2 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -5,7 +5,7 @@ class WarehouseFacility(FacilityBase): - SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id", "price")) + SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id", "price", "delay_order_penalty")) # storage unit storage = None @@ -70,7 +70,13 @@ def build(self, configs: dict): for sku_name, sku_config in configs["skus"].items(): sku = self.world.get_sku(sku_name) - sku_info = WarehouseFacility.SkuInfo(sku_name, sku_config["init_stock"], sku.id, sku_config.get("price", 0)) + sku_info = WarehouseFacility.SkuInfo( + sku_name, + sku_config["init_stock"], + sku.id, + sku_config.get("price", 0), + sku_config.get("delay_order_penalty", 0) + ) self.sku_information[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index ff13b18dc..27ffcfa2a 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -44,7 +44,7 @@ def step(self, tick: int): # NOTE: we moved delay_order_penalty from facility to sku, is this ok? # update order's delay penalty per tick. for order in self.order_queue: - sku = self.facility.get_sku(order.product_id) + sku = self.facility.sku_information[order.product_id] product_index = self.product_index_mapping[order.product_id] self.data.delay_order_penalty[product_index] += sku.delay_order_penalty diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index b32e3c9e7..db56200f9 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -43,6 +43,8 @@ def schedule(self, destination, product_id: int, quantity: int, vlt): destination.y ) + print("path", self.path) + if self.path is None: raise Exception(f"Destination {destination} is unreachable") @@ -73,7 +75,7 @@ def try_unloading(self): unloaded_units = sum(unloaded.values()) # TODO: not implemented, refactor the name - self.destination.consumer.on_order_reception( + self.destination.consumers[self.data.product_id].on_order_reception( self.facility.id, self.data.product_id, unloaded_units, diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 12279dde1..be120d327 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -150,7 +150,7 @@ def build(self, configs: dict, snapshot_number: int): # travel cost for a star path finder, 0 means block, > 1 means the cost travel to that cell # current all traversable cell's cost will be 1. - cost_grid = np.ones(shape=(grid_width, grid_height), dtype=np.int) + cost_grid = np.ones(shape=(grid_width, grid_height), dtype=np.int8) # add blocks to grid for facility_name, facility_pos in grid_config["facilities"].items(): @@ -160,8 +160,9 @@ def build(self, configs: dict, snapshot_number: int): facility.x = facility_pos[0] facility.y = facility_pos[1] - # facility is a block, so cost is 0 - cost_grid[facility.x, facility.y] = 0 + # facility cannot be a block, or we cannot find path to it, + # but we can give it a big cost + cost_grid[facility.x, facility.y] = 120 for block_pos in grid_config["blocks"].values(): cost_grid[block_pos[0], block_pos[1]] = 0 From 84807f2535997d305155c4dc151b7c6966085cbc Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 21:13:06 +0800 Subject: [PATCH 034/482] show vehicle movement in screen --- examples/hello_world/supply_chain/hello.py | 26 ++++++++++++++++++- .../scenarios/supply_chain/units/transport.py | 4 +-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index e53615faa..28a95002e 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -64,6 +64,7 @@ def start(self): title="Supply chain environment" ) as ctx: action = None + is_new_step = False while True: # clear @@ -79,6 +80,9 @@ def start(self): for railroad in railroads: console.print(railroad[0], railroad[1], "R", (255, 0, 0)) + # show vehicles that on the way + self.present_vehicles(console) + ctx.present( console, keep_aspect=True, @@ -98,6 +102,8 @@ def start(self): metrics, decision_event, is_done = self.env.step(action) + is_new_step = True + print(f"{bcolors.OKGREEN}Current environment tick:", self.env.tick, f"{bcolors.ENDC}") if is_done: @@ -113,6 +119,24 @@ def start(self): elif event.sym == tcod.event.K_ESCAPE: action = None + def present_vehicles(self, console: tcod.Console): + vehicles = self.env.snapshot_list["transport"] + vehicle_number = len(vehicles) + + # here we query the attributes that slot number ==1, then query position, or snapshot_list will try to padding for id + normal_list = vehicles[self.env.frame_index::("id", "steps", "location")].flatten().reshape(vehicle_number, -1).astype(np.int) + pos_list = vehicles[self.env.frame_index::"position"].flatten().reshape(vehicle_number, -1).astype(np.int) + + for index, state in enumerate(normal_list): + location = state[2] + steps = state[1] + + if steps > 0: + x, y = pos_list[index] + + if x >= 0 and y >= 0: + console.print(x, y, "V", (0, 255, 0), (128, 128, index)) + def show_summary(self): pp = pprint.PrettyPrinter(indent=2, depth=8) @@ -130,7 +154,7 @@ def show_vehicle_states(self): vehicle_number = len(vehicles) - vehicle_features = ("id", "facility_id", "location", "steps", "patient", "source", "destination", "payload", "product_id", ) + vehicle_features = ("id", "facility_id", "location", "steps", "patient", "source", "destination", "payload", "product_id") vehicle_states = vehicles[self.env.frame_index::vehicle_features].flatten().reshape(vehicle_number, -1).astype(np.int) diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index db56200f9..5babb97a5 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -43,8 +43,6 @@ def schedule(self, destination, product_id: int, quantity: int, vlt): destination.y ) - print("path", self.path) - if self.path is None: raise Exception(f"Destination {destination} is unreachable") @@ -110,6 +108,7 @@ def step(self, tick: int): self.data.steps = 0 self.data.location = 0 self.data.destination = 0 + self.data.position[:] = -1 # Moving to destination if self.data.payload > 0: @@ -135,3 +134,4 @@ def step(self, tick: int): self.data.steps = 0 self.data.location = 0 self.data.destination = 0 + self.data.position[:] = -1 From 2aa2637557a011b1cd4f43b7e7806df39ff96dad Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 6 Mar 2021 21:15:38 +0800 Subject: [PATCH 035/482] remove completed todo --- maro/simulator/scenarios/supply_chain/business_engine.py | 4 ---- maro/simulator/scenarios/supply_chain/units/transport.py | 1 - 2 files changed, 5 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index ae0f4470c..4cba21e2f 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -43,7 +43,6 @@ def step(self, tick: int): for _, facility in self.world.facilities.items(): facility.step(tick) - # TODO: debug only, ask for action per 5 ticks. if tick % self._action_steps == 0: decision_event = self._event_buffer.gen_decision_event(tick, None) @@ -54,8 +53,6 @@ def post_step(self, tick: int): if (tick + 1) % self._snapshot_resolution == 0: self._frame.take_snapshot(self.frame_index(tick)) - # TODO: anything need to reset per tick? - for facility in self.world.facilities.values(): facility.post_step(tick) @@ -65,7 +62,6 @@ def reset(self): self._frame.reset() self._frame.snapshots.reset() - # TODO: reset frame nodes. for _, facility in self.world.facilities.items(): facility.reset() diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 5babb97a5..58e12e5d2 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -72,7 +72,6 @@ def try_unloading(self): if len(unloaded) > 0: unloaded_units = sum(unloaded.values()) - # TODO: not implemented, refactor the name self.destination.consumers[self.data.product_id].on_order_reception( self.facility.id, self.data.product_id, From 711b4758543ba3ccf4c32e8c4fd92c8f902ba509 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sun, 7 Mar 2021 08:52:16 +0800 Subject: [PATCH 036/482] fix vehicle location issue, make all units and data model class from configs --- .../scenarios/supply_chain/configs.py | 100 +++++++++++++++--- .../supply_chain/facilities/retailer.py | 12 +-- .../supply_chain/facilities/supplier.py | 24 ++--- .../supply_chain/facilities/warehouse.py | 20 ++-- .../scenarios/supply_chain/units/transport.py | 4 +- 5 files changed, 113 insertions(+), 47 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index ec76eeef5..42c601319 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -122,27 +122,47 @@ class CellType(Enum): }, "storage": { "data": { + "class": "StorageDataModel", "capacity": 20000, "unit_storage_cost": 10 - } + }, + "class": "StorageUnit" }, "distribution": { "data": { + "class": "DistributionDataModel", "unit_price": 10 - } + }, + "class": "DistributionUnit" }, "transports": [ { "data": { + "class": "TransportDataModel", "patient": 100 - } + }, + "class": "TransportUnit" }, { "data": { + "class": "TransportDataModel", "patient": 100 - } + }, + "class": "TransportUnit" } - ] + ], + "consumers": { + "data": { + "class": "ConsumerDataModel" + }, + "class": "ConsumerUnit" + }, + "suppliers": { + "data": { + "class": "ManufactureDataModel" + }, + "class": "ManufacturingUnit" + } } }, { @@ -170,27 +190,47 @@ class CellType(Enum): }, "storage": { "data": { + "class": "StorageDataModel", "capacity": 20000, "unit_storage_cost": 10 - } + }, + "class": "StorageUnit" }, "distribution": { "data": { + "class": "DistributionDataModel", "unit_price": 10 - } + }, + "class": "DistributionUnit" }, "transports": [ { "data": { + "class": "TransportDataModel", "patient": 100 - } + }, + "class": "TransportUnit" }, { "data": { + "class": "TransportDataModel", "patient": 100 - } + }, + "class": "TransportUnit" } - ] + ], + "consumers": { + "data": { + "class": "ConsumerDataModel" + }, + "class": "ConsumerUnit" + }, + "suppliers": { + "data": { + "class": "ManufactureDataModel" + }, + "class": "ManufacturingUnit" + } } }, { @@ -216,27 +256,41 @@ class CellType(Enum): }, "storage": { "data": { + "class": "StorageDataModel", "capacity": 200, "unit_storage_cost": 10 - } + }, + "class": "StorageUnit" }, "distribution": { "data": { + "class": "DistributionDataModel", "unit_price": 10 - } + }, + "class": "DistributionUnit" }, "transports": [ { "data": { + "class": "TransportDataModel", "patient": 100 - } + }, + "class": "TransportUnit" }, { "data": { + "class": "TransportDataModel", "patient": 100 - } + }, + "class": "TransportUnit" } - ] + ], + "consumers": { + "data": { + "class": "ConsumerDataModel" + }, + "class": "ConsumerUnit" + } } }, { @@ -265,10 +319,24 @@ class CellType(Enum): }, "storage": { "data": { + "class": "StorageDataModel", "capacity": 1000, "unit_storage_cost": 10 - } + }, + "class": "StorageUnit" }, + "consumers": { + "data": { + "class": "ConsumerDataModel" + }, + "class": "ConsumerUnit" + }, + "sellers": { + "data": { + "class": "SellerDataModel" + }, + "class": "SellerUnit" + } } } ], diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 330e16cf2..f3100168a 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -37,8 +37,8 @@ def build(self, configs: dict): self.configs = configs # construct storage - self.storage = self.world.build_unit("StorageUnit") - self.storage.data_class = "StorageDataModel" + self.storage = self.world.build_unit(configs["storage"]["class"]) + self.storage.data_class = configs["storage"]["data"]["class"] self.storage.world = self.world self.storage.facility = self @@ -62,8 +62,8 @@ def build(self, configs: dict): self.sku_information[sku.id] = sku_info # all sku in retail are final production for sale, no material - consumer = self.world.build_unit("ConsumerUnit") - consumer.data_class = "ConsumerDataModel" + consumer = self.world.build_unit(configs["consumers"]["class"]) + consumer.data_class = configs["consumers"]["data"]["class"] consumer.world = self.world consumer.facility = self @@ -72,8 +72,8 @@ def build(self, configs: dict): self.consumers[sku.id] = consumer # seller for this sku - seller = self.world.build_unit("SellerUnit") - seller.data_class = "SellerDataModel" + seller = self.world.build_unit(configs["sellers"]["class"]) + seller.data_class = configs["sellers"]["data"]["class"] seller.world = self.world seller.facility = self diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 0cf2ea051..038ebeb3c 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -28,11 +28,11 @@ def step(self, tick: int): def build(self, configs: dict): self.configs = configs - # TODO: dup code from facilities, refactoring later + # TODO: dup code in all facilities, refactoring later # construct storage - self.storage = self.world.build_unit("StorageUnit") - self.storage.data_class = "StorageDataModel" + self.storage = self.world.build_unit(configs["storage"]["class"]) + self.storage.data_class = configs["storage"]["data"]["class"] self.storage.world = self.world self.storage.facility = self @@ -41,9 +41,9 @@ def build(self, configs: dict): # construct transport self.transports = [] - for _ in configs["transports"]: - transport = self.world.build_unit("TransportUnit") - transport.data_class = "TransportDataModel" + for transport_conf in configs["transports"]: + transport = self.world.build_unit(transport_conf["class"]) + transport.data_class = transport_conf["data"]["class"] transport.world = self.world transport.facility = self @@ -52,8 +52,8 @@ def build(self, configs: dict): self.transports.append(transport) # construct distribution - self.distribution = self.world.build_unit("DistributionUnit") - self.distribution.data_class = "DistributionDataModel" + self.distribution = self.world.build_unit(configs["distribution"]["class"]) + self.distribution.data_class = configs["distribution"]["data"]["class"] self.distribution.world = self.world self.distribution.facility = self @@ -82,8 +82,8 @@ def build(self, configs: dict): # TODO: make it an enum later. if sku_info.type == "production": # one supplier per sku - supplier = self.world.build_unit("ManufacturingUnit") - supplier.data_class = "ManufactureDataModel" + supplier = self.world.build_unit(configs["suppliers"]["class"]) + supplier.data_class = configs["suppliers"]["data"]["class"] supplier.world = self.world supplier.facility = self @@ -91,8 +91,8 @@ def build(self, configs: dict): self.suppliers[sku.id] = supplier else: - consumer = self.world.build_unit("ConsumerUnit") - consumer.data_class = "ConsumerDataModel" + consumer = self.world.build_unit(configs["consumers"]["class"]) + consumer.data_class = configs["consumers"]["data"]["class"] consumer.world = self.world consumer.facility = self diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 5442334e2..c4338c637 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -33,11 +33,9 @@ def step(self, tick: int): def build(self, configs: dict): self.configs = configs - # TODO: following strings should from config later - # construct storage - self.storage = self.world.build_unit("StorageUnit") - self.storage.data_class = "StorageDataModel" + self.storage = self.world.build_unit(configs["storage"]["class"]) + self.storage.data_class = configs["storage"]["data"]["class"] self.storage.world = self.world self.storage.facility = self @@ -47,8 +45,8 @@ def build(self, configs: dict): self.transports = [] for facility_conf in configs["transports"]: - transport = self.world.build_unit("TransportUnit") - transport.data_class = "TransportDataModel" + transport = self.world.build_unit(facility_conf["class"]) + transport.data_class = facility_conf["data"]["class"] transport.world = self.world transport.facility = self @@ -57,8 +55,8 @@ def build(self, configs: dict): self.transports.append(transport) # construct distribution - self.distribution = self.world.build_unit("DistributionUnit") - self.distribution.data_class = "DistributionDataModel" + self.distribution = self.world.build_unit(configs["distribution"]["class"]) + self.distribution.data_class = configs["distribution"]["data"]["class"] self.distribution.world = self.world self.distribution.facility = self @@ -80,8 +78,8 @@ def build(self, configs: dict): self.sku_information[sku.id] = sku_info - consumer = self.world.build_unit("ConsumerUnit") - consumer.data_class = "ConsumerDataModel" + consumer = self.world.build_unit(configs["consumers"]["class"]) + consumer.data_class = configs["consumers"]["data"]["class"] consumer.world = self.world consumer.facility = self @@ -97,7 +95,7 @@ def initialize(self): self.storage.initialize(self.configs.get("storage", {})) self.distribution.initialize(self.configs.get("distribution", {})) - transports_conf = self.configs["transports"] + transports_conf = self.configs.get("transports", []) for index, transport in enumerate(self.transports): transport.initialize(transports_conf[index]) diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 58e12e5d2..4544dea5b 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -116,10 +116,10 @@ def step(self, tick: int): self.data.location += self.data.vlt self.data.steps -= 1 - if self.data.location >= len(self.path): + if self.data.location > len(self.path): self.data.location = len(self.path) - 1 - self.data.position[:] = self.path[self.data.location] + self.data.position[:] = self.path[self.data.location-1] else: # avoid update under idle state. if self.data.location > 0: From 83bd035619147e96533b4dede83d330ea8ba42a7 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sun, 7 Mar 2021 09:05:02 +0800 Subject: [PATCH 037/482] show more states --- examples/hello_world/supply_chain/hello.py | 37 ++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 28a95002e..da91d2e29 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -123,7 +123,8 @@ def present_vehicles(self, console: tcod.Console): vehicles = self.env.snapshot_list["transport"] vehicle_number = len(vehicles) - # here we query the attributes that slot number ==1, then query position, or snapshot_list will try to padding for id + # here we query the attributes that slot number ==1, + # then query position, or snapshot_list will try to padding for id normal_list = vehicles[self.env.frame_index::("id", "steps", "location")].flatten().reshape(vehicle_number, -1).astype(np.int) pos_list = vehicles[self.env.frame_index::"position"].flatten().reshape(vehicle_number, -1).astype(np.int) @@ -149,6 +150,38 @@ def show_states(self): self.show_vehicle_states() + self.show_demand_states() + + self.show_storage_states() + + def show_storage_states(self): + storages = self.env.snapshot_list["storage"] + storage_number = len(storages) + + storage_features = ("id", "facility_id", "remaining_space", "capacity", "unit_storage_cost") + storage_states = storages[self.env.frame_index::storage_features].flatten().reshape(storage_number, -1).astype(np.int) + + storage_all_states = [] + + for index, state in enumerate(storage_states): + product_list = storages[self.env.frame_index:index:"product_list"].flatten().astype(np.int) + product_number = storages[self.env.frame_index:index:"product_number"].flatten().astype(np.int) + + storage_all_states.append(list(state) + [product_list, product_number]) + + print(f"{bcolors.HEADER}Storage states:{bcolors.ENDC}") + print(tabulate(storage_all_states, storage_features + ("product_list", "product_number"))) + + def show_demand_states(self): + sellers = self.env.snapshot_list["seller"] + seller_number = len(sellers) + + seller_features = ("id", "facility_id", "product_id", "demand", "sold", "total_sold", "sale_gamma") + seller_states = sellers[self.env.frame_index::seller_features].flatten().reshape(seller_number, -1).astype(np.int) + + print(f"{bcolors.HEADER}Demand states:{bcolors.ENDC}") + print(tabulate(seller_states, seller_features)) + def show_vehicle_states(self): vehicles = self.env.snapshot_list["transport"] @@ -196,7 +229,7 @@ def show_manufacture_states(self): print(tabulate(states, headers=features)) # show storage state to see if product changed - print(f"{bcolors.HEADER}Storage states:{bcolors.ENDC}") + print(f"{bcolors.HEADER}Manufacture storage states:{bcolors.ENDC}") storage_features = ["id", "remaining_space", "capacity"] storage_states_summary = [] From f82022a73ee8ec1f46fdcdf05ec80b8a9a9626d9 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sun, 7 Mar 2021 10:01:40 +0800 Subject: [PATCH 038/482] fix slot number bug for dynamic backend --- maro/backends/_raw_backend_.pyx | 2 + maro/backends/backend.pxd | 7 ++- maro/backends/backend.pyx | 3 ++ maro/backends/frame.pyx | 43 +++++++++++++------ maro/backends/raw_backend.pyx | 2 + .../scenarios/supply_chain/configs.py | 4 +- 6 files changed, 44 insertions(+), 17 deletions(-) diff --git a/maro/backends/_raw_backend_.pyx b/maro/backends/_raw_backend_.pyx index 667bf09c7..118ba6844 100644 --- a/maro/backends/_raw_backend_.pyx +++ b/maro/backends/_raw_backend_.pyx @@ -233,6 +233,8 @@ cdef class RawBackend(BackendAbc): cdef list slots_not_equal(self, NODE_INDEX index, ATTR_TYPE attr_type, object value) except +: return self.where(index, attr_type, lambda x : x != value) + cdef SLOT_INDEX get_slot_number(self, NODE_INDEX index, ATTR_TYPE attr_type) except +: + return self._frame.get_slot_number(index, attr_type) cdef class RawSnapshotList(SnapshotListAbc): def __cinit__(self, RawBackend backend, USHORT total_snapshots): diff --git a/maro/backends/backend.pxd b/maro/backends/backend.pxd index c28fb8fa9..04fc3c4c9 100644 --- a/maro/backends/backend.pxd +++ b/maro/backends/backend.pxd @@ -139,7 +139,7 @@ cdef class BackendAbc: # Filter slots that greater than specified value. cdef list slots_greater_than(self, NODE_INDEX index, ATTR_TYPE attr_type, object value) except + - # Filter slots that greater equeal to specified value. + # Filter slots that greater equal to specified value. cdef list slots_greater_equal(self, NODE_INDEX index, ATTR_TYPE attr_type, object value) except + # Filter slots that less than specified value. @@ -151,5 +151,8 @@ cdef class BackendAbc: # Filter slots that equal to specified value. cdef list slots_equal(self, NODE_INDEX index, ATTR_TYPE attr_type, object value) except + - # Filter slots that not euqal to specified value. + # Filter slots that not equal to specified value. cdef list slots_not_equal(self, NODE_INDEX index, ATTR_TYPE attr_type, object value) except + + + # Get slot number for specified attribute, only support dynamic backend. + cdef SLOT_INDEX get_slot_number(self, NODE_INDEX index, ATTR_TYPE attr_type) except + diff --git a/maro/backends/backend.pyx b/maro/backends/backend.pyx index e452d5351..31a6c0979 100644 --- a/maro/backends/backend.pyx +++ b/maro/backends/backend.pyx @@ -126,3 +126,6 @@ cdef class BackendAbc: cdef list slots_not_equal(self, NODE_INDEX index, ATTR_TYPE attr_type, object value) except +: pass + + cdef SLOT_INDEX get_slot_number(self, NODE_INDEX index, ATTR_TYPE attr_type) except +: + pass diff --git a/maro/backends/frame.pyx b/maro/backends/frame.pyx index 6b00f8fd4..110255e6a 100644 --- a/maro/backends/frame.pyx +++ b/maro/backends/frame.pyx @@ -151,14 +151,17 @@ cdef class _NodeAttributeAccessor: Current attribute must be a list. Args: - value(object): Value to append, the data type must fit the decleared one. + value(object): Value to append, the data type must fit the declared one. """ if not self._is_list: raise BackendsAppendToNonListAttributeException() self._backend.append_to_list(self._node_index, self._attr_type, value) - self._slot_number += 1 + self._slot_number = self._backend.get_slot_number(self._node_index, self._attr_type) + + if "_cb" in self.__dict__: + self._cb(None) def resize(self, new_size: int): """Resize current list attribute with specified new size. @@ -174,7 +177,10 @@ cdef class _NodeAttributeAccessor: self._backend.resize_list(self._node_index, self._attr_type, new_size) - self._slot_number = new_size + self._slot_number = self._backend.get_slot_number(self._node_index, self._attr_type) + + if "_cb" in self.__dict__: + self._cb(None) def clear(self): """Clear all items in current list attribute. @@ -189,6 +195,9 @@ cdef class _NodeAttributeAccessor: self._slot_number = 0 + if "_cb" in self.__dict__: + self._cb(None) + def insert(self, slot_index: int, value: object): """Insert a value to specified slot. @@ -201,7 +210,10 @@ cdef class _NodeAttributeAccessor: self._backend.insert_to_list(self._node_index, self._attr_type, slot_index, value) - self._slot_number += 1 + self._slot_number = self._backend.get_slot_number(self._node_index, self._attr_type) + + if "_cb" in self.__dict__: + self._cb(None) def remove(self, slot_index: int): """Remove specified slot. @@ -214,7 +226,10 @@ cdef class _NodeAttributeAccessor: self._backend.remove_from_list(self._node_index, self._attr_type, slot_index) - self._slot_number -= 1 + self._slot_number = self._backend.get_slot_number(self._node_index, self._attr_type) + + if "_cb" in self.__dict__: + self._cb(None) def where(self, filter_func: callable): """Filter current attribute slots with input function. @@ -223,7 +238,7 @@ cdef class _NodeAttributeAccessor: filter_func (callable): Function to filter slot value. Returns: - List[int]: List of slot index whoes value match the filter function. + List[int]: List of slot index whose value match the filter function. """ return self._backend.where(self._node_index, self._attr_type, filter_func) @@ -354,8 +369,8 @@ cdef class _NodeAttributeAccessor: else: raise BackendsSetItemInvalidException() - # Check and invoke value changed callback, except list attribute. - if not self._is_list and "_cb" in self.__dict__: + # Check and invoke value changed callback. + if "_cb" in self.__dict__: self._cb(value) def __len__(self): @@ -410,12 +425,12 @@ cdef class NodeBase: # Bind a value changed callback if available, named as _on__changed. # Except list attribute. - if not attr_acc._is_list: - cb_name = f"_on_{name}_changed" - cb_func = getattr(self, cb_name, None) + # if not attr_acc._is_list: + cb_name = f"_on_{name}_changed" + cb_func = getattr(self, cb_name, None) - if cb_func is not None: - attr_acc.on_value_changed(cb_func) + if cb_func is not None: + attr_acc.on_value_changed(cb_func) def __setattr__(self, name, value): """Used to avoid attribute overriding, and an easy way to set for 1 slot attribute.""" @@ -526,6 +541,8 @@ cdef class FrameBase: else: node._is_deleted = False + # Also + cpdef void take_snapshot(self, INT tick) except *: """Take snapshot for specified point (tick) for current frame. diff --git a/maro/backends/raw_backend.pyx b/maro/backends/raw_backend.pyx index d20f35a00..947e4eba9 100644 --- a/maro/backends/raw_backend.pyx +++ b/maro/backends/raw_backend.pyx @@ -233,6 +233,8 @@ cdef class RawBackend(BackendAbc): cdef list slots_not_equal(self, NODE_INDEX index, ATTR_TYPE attr_type, object value) except +: return self.where(index, attr_type, lambda x : x != value) + cdef SLOT_INDEX get_slot_number(self, NODE_INDEX index, ATTR_TYPE attr_type) except +: + return self._frame.get_slot_number(index, attr_type) cdef class RawSnapshotList(SnapshotListAbc): def __cinit__(self, RawBackend backend, USHORT total_snapshots): diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 42c601319..5880663e3 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -257,7 +257,7 @@ class CellType(Enum): "storage": { "data": { "class": "StorageDataModel", - "capacity": 200, + "capacity": 20000, "unit_storage_cost": 10 }, "class": "StorageUnit" @@ -320,7 +320,7 @@ class CellType(Enum): "storage": { "data": { "class": "StorageDataModel", - "capacity": 1000, + "capacity": 20000, "unit_storage_cost": 10 }, "class": "StorageUnit" From e8193c64113f840069503d1fde585d4258d2dbf4 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 8 Mar 2021 14:31:00 +0800 Subject: [PATCH 039/482] rename suppliers to manufactures --- .../scenarios/supply_chain/configs.py | 4 ++-- .../supply_chain/facilities/supplier.py | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py index 5880663e3..b211d6f55 100644 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ b/maro/simulator/scenarios/supply_chain/configs.py @@ -157,7 +157,7 @@ class CellType(Enum): }, "class": "ConsumerUnit" }, - "suppliers": { + "manufactures": { "data": { "class": "ManufactureDataModel" }, @@ -225,7 +225,7 @@ class CellType(Enum): }, "class": "ConsumerUnit" }, - "suppliers": { + "manufactures": { "data": { "class": "ManufactureDataModel" }, diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 038ebeb3c..1cc1e9c23 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -9,7 +9,7 @@ class SupplierFacility(FacilityBase): storage = None distribution = None transports = None - suppliers = None + manufactures = None consumers = None def step(self, tick: int): @@ -19,7 +19,7 @@ def step(self, tick: int): for vehicle in self.transports: vehicle.step(tick) - for supplier in self.suppliers.values(): + for supplier in self.manufactures.values(): supplier.step(tick) for consumer in self.consumers.values(): @@ -61,7 +61,7 @@ def build(self, configs: dict): # sku information self.sku_information = {} - self.suppliers = {} + self.manufactures = {} self.consumers = {} for sku_name, sku_config in configs["skus"].items(): @@ -82,14 +82,14 @@ def build(self, configs: dict): # TODO: make it an enum later. if sku_info.type == "production": # one supplier per sku - supplier = self.world.build_unit(configs["suppliers"]["class"]) - supplier.data_class = configs["suppliers"]["data"]["class"] + supplier = self.world.build_unit(configs["manufactures"]["class"]) + supplier.data_class = configs["manufactures"]["data"]["class"] supplier.world = self.world supplier.facility = self supplier.data_index = self.world.register_data_class(supplier.id, supplier.data_class) - self.suppliers[sku.id] = supplier + self.manufactures[sku.id] = supplier else: consumer = self.world.build_unit(configs["consumers"]["class"]) consumer.data_class = configs["consumers"]["data"]["class"] @@ -105,8 +105,8 @@ def initialize(self): self._init_by_skus() for _, sku in self.sku_information.items(): - if sku.id in self.suppliers: - supplier = self.suppliers[sku.id] + if sku.id in self.manufactures: + supplier = self.manufactures[sku.id] # build parameters to initialize the data model supplier.initialize({ @@ -141,7 +141,7 @@ def post_step(self, tick: int): for vehicle in self.transports: vehicle.post_step(tick) - for supplier in self.suppliers.values(): + for supplier in self.manufactures.values(): supplier.post_step(tick) for consumer in self.consumers.values(): @@ -156,7 +156,7 @@ def reset(self): for vehicle in self.transports: vehicle.reset() - for supplier in self.suppliers.values(): + for supplier in self.manufactures.values(): supplier.reset() for consumer in self.consumers.values(): @@ -172,7 +172,7 @@ def get_node_info(self) -> dict: "consumers": [consumer.get_unit_info() for consumer in self.consumers.values()], "transports": [vehicle.get_unit_info() for vehicle in self.transports], "distribution": self.distribution.get_unit_info(), - "suppliers": [supplier.get_unit_info() for supplier in self.suppliers.values()] + "suppliers": [supplier.get_unit_info() for supplier in self.manufactures.values()] } } From e3ad3e5d2513ce8abab0e6ae7623ba248afa57a8 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 8 Mar 2021 16:40:27 +0800 Subject: [PATCH 040/482] add missing file --- .../supply_chain/config_parser/__init__.py | 0 .../supply_chain/config_parser/parser.py | 127 ++++++++++++++++++ .../scenarios/supply_chain/data/__init__.py | 6 + .../scenarios/supply_chain/data/base.py | 33 +++++ .../scenarios/supply_chain/data/consumer.py | 47 +++++++ .../supply_chain/data/distribution.py | 29 ++++ .../supply_chain/data/manufacture.py | 44 ++++++ .../scenarios/supply_chain/data/seller.py | 47 +++++++ .../scenarios/supply_chain/data/storage.py | 42 ++++++ .../scenarios/supply_chain/data/transport.py | 49 +++++++ 10 files changed, 424 insertions(+) create mode 100644 maro/simulator/scenarios/supply_chain/config_parser/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/config_parser/parser.py create mode 100644 maro/simulator/scenarios/supply_chain/data/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/data/base.py create mode 100644 maro/simulator/scenarios/supply_chain/data/consumer.py create mode 100644 maro/simulator/scenarios/supply_chain/data/distribution.py create mode 100644 maro/simulator/scenarios/supply_chain/data/manufacture.py create mode 100644 maro/simulator/scenarios/supply_chain/data/seller.py create mode 100644 maro/simulator/scenarios/supply_chain/data/storage.py create mode 100644 maro/simulator/scenarios/supply_chain/data/transport.py diff --git a/maro/simulator/scenarios/supply_chain/config_parser/__init__.py b/maro/simulator/scenarios/supply_chain/config_parser/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/maro/simulator/scenarios/supply_chain/config_parser/parser.py b/maro/simulator/scenarios/supply_chain/config_parser/parser.py new file mode 100644 index 000000000..6a20c93e4 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/config_parser/parser.py @@ -0,0 +1,127 @@ + +from importlib import import_module + +from yaml import safe_load + +from collections import namedtuple + + +DataModelItem = namedtuple("DataModelItem", ("alias", "module_path", "class_name", "class_type")) +UnitItem = namedtuple("UnitItem", ("alias", "module_path", "class_name", "class_type", "configs")) +FacilityItem = namedtuple("FacilityItem", ("alias", "module_path", "class_name", "class_type", "configs")) + + +def find_class_type(module_path: str, class_name: str): + target_module = import_module(module_path) + + return getattr(target_module, class_name) + + +class SupplyChainConfiguration: + data_models = None + units = None + facilities = None + world = None + + def __init__(self): + self.data_models = {} + self.units = {} + self.facilities = {} + self.world = {} + + def add_data_definition(self, alias: str, class_name: str, module_path: str): + # check conflict + assert alias not in self.data_models + + self.data_models[alias] = DataModelItem(alias, module_path, class_name, find_class_type(module_path, class_name)) + + def add_unit_definition(self, alias: str, class_name: str, module_path: str, configs: dict): + assert alias not in self.units + + self.units[alias] = UnitItem(alias, module_path, class_name, find_class_type(module_path, class_name), configs) + + def add_facility_definition(self, alias: str, class_name: str, module_path: str, configs: dict): + assert alias not in self.facilities + + self.facilities[alias] = FacilityItem(alias, module_path, class_name, find_class_type(module_path, class_name), configs) + + + +class ConfigParser: + def __init__(self, core_file: str, config_file: str): + self._core_file = core_file + self._config_file = config_file + + self._result = SupplyChainConfiguration() + + def parse(self): + self._parse_core() + self._parse_world_config() + + return self._result + + def _parse_core(self): + with open(self._core_file, "rt") as fp: + core_config = safe_load(fp) + + self._read_core_conf(core_config) + + def _read_core_conf(self, core_config: dict): + # data models + if "data" in core_config: + for module_conf in core_config["data"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + self._result.add_data_definition(class_alias, class_def["class"], module_path) + + # units + if "units" in core_config: + for module_conf in core_config["units"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + self._result.add_unit_definition(class_alias, class_def["class"], module_path, class_def) + + # facilities + if "facilities" in core_config: + for module_conf in core_config["facilities"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + self._result.add_facility_definition(class_alias, class_def["class"], module_path, class_def) + + def _parse_world_config(self): + with open(self._config_file, "rt") as fp: + world_conf = safe_load(fp) + + # read and override core part + customized_core_conf = world_conf.get("core", {}) + + self._read_core_conf(customized_core_conf) + + # read the world config first + self._result.world = world_conf["world"] + + # then try to fulfill with core configurations + for facility_conf in self._result.world["facilities"]: + facility_class_alias = facility_conf["class"] + + facility_def = self._result.facilities[facility_class_alias] + + for property_name, property_conf in facility_def.configs.items(): + if property_name == "class": + continue + + if property_name not in facility_conf: + facility_conf["configs"][property_name] = property_conf + else: + pass + + +if __name__ == "__main__": + parser = ConfigParser("maro/simulator/scenarios/supply_chain/topologies/core.yaml", "maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml") + + result = parser.parse() + + print(result.world) diff --git a/maro/simulator/scenarios/supply_chain/data/__init__.py b/maro/simulator/scenarios/supply_chain/data/__init__.py new file mode 100644 index 000000000..46e0c3ec8 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/__init__.py @@ -0,0 +1,6 @@ +from .storage import StorageDataModel +from .transport import TransportDataModel +from .distribution import DistributionDataModel +from .manufacture import ManufactureDataModel +from .consumer import ConsumerDataModel +from .seller import SellerDataModel diff --git a/maro/simulator/scenarios/supply_chain/data/base.py b/maro/simulator/scenarios/supply_chain/data/base.py new file mode 100644 index 000000000..5d97d69ac --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/base.py @@ -0,0 +1,33 @@ + + +from abc import abstractmethod +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeBase, NodeAttribute + + +class DataModelBase(NodeBase): + # id of related unit + id = NodeAttribute(AttributeType.Int) + + # id of facility this unit belongs to + facility_id = NodeAttribute(AttributeType.Int) + + def __init__(self): + self._unit_id = 0 + self._facility_id = 0 + + @abstractmethod + def initialize(self, configs): + """Initialize the fields with configs, the config should be a dict.""" + # called from unit after frame is ready. + pass + + def reset(self): + """Reset after each episode""" + # called per episode. + self.id = self._unit_id + self.facility_id = self._facility_id + + def set_id(self, unit_id: int, facility_id: int): + self._unit_id = unit_id + self._facility_id = facility_id diff --git a/maro/simulator/scenarios/supply_chain/data/consumer.py b/maro/simulator/scenarios/supply_chain/data/consumer.py new file mode 100644 index 000000000..7aeb4552a --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/consumer.py @@ -0,0 +1,47 @@ +from .base import DataModelBase + +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +# NOTE: one sku one consumer +@node("consumer") +class ConsumerDataModel(DataModelBase): + # reward states + order_cost = NodeAttribute(AttributeType.Int) + total_purchased = NodeAttribute(AttributeType.Int) + total_received = NodeAttribute(AttributeType.Int) + + # action states + consumer_product_id = NodeAttribute(AttributeType.Int) + consumer_source_id = NodeAttribute(AttributeType.Int) + consumer_quantity = NodeAttribute(AttributeType.Int) + consumer_vlt = NodeAttribute(AttributeType.Int) + + # id of upstream facilities. + sources = NodeAttribute(AttributeType.Int, 1, is_list=True) + + # per tick states + + # snapshots["consumer"][hist_len::"purchased"] equals to original latest_consumptions + purchased = NodeAttribute(AttributeType.Int) + received = NodeAttribute(AttributeType.Int) + + def __init__(self): + super(ConsumerDataModel, self).__init__() + + self._order_cost = 0 + self._consumer_product_id = 0 + + def initialize(self, configs: dict): + if configs is not None: + self._order_cost = configs["order_cost"] + self._consumer_product_id = configs["consumer_product_id"] + + self.reset() + + def reset(self): + super(ConsumerDataModel, self).reset() + + self.order_cost = self._order_cost + self.consumer_product_id = self._consumer_product_id diff --git a/maro/simulator/scenarios/supply_chain/data/distribution.py b/maro/simulator/scenarios/supply_chain/data/distribution.py new file mode 100644 index 000000000..d9f3c2e51 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/distribution.py @@ -0,0 +1,29 @@ +from .base import DataModelBase + +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +@node("distribution") +class DistributionDataModel(DataModelBase): + unit_price = NodeAttribute(AttributeType.Int) + + # origin is stock_levels, used to save product and its number, we have to split it. + product_list = NodeAttribute(AttributeType.Int, 1, is_list=True) + check_in_price = NodeAttribute(AttributeType.Int, 1, is_list=True) + delay_order_penalty = NodeAttribute(AttributeType.Int, 1, is_list=True) + + def __init__(self): + super(DistributionDataModel, self).__init__() + # TODO: not sure about this. + self._unit_price = 0 + + def initialize(self, configs: dict): + if configs is not None: + self._unit_price = configs.get("unit_price", 0) + + self.reset() + + def reset(self): + super(DistributionDataModel, self).reset() + self.unit_price = self._unit_price diff --git a/maro/simulator/scenarios/supply_chain/data/manufacture.py b/maro/simulator/scenarios/supply_chain/data/manufacture.py new file mode 100644 index 000000000..e9aaa81fc --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/manufacture.py @@ -0,0 +1,44 @@ +from .base import DataModelBase + +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +@node("manufacture") +class ManufactureDataModel(DataModelBase): + # storage related to this manufacture unit, for easy state retrieving. + storage_id = NodeAttribute(AttributeType.Int) + + # cost to produce one output production + product_unit_cost = NodeAttribute(AttributeType.Int) + + # cost per tick, different with original manufacturing cost, we just provide number, and cost + # user can determine how to calculate the cost + manufacturing_number = NodeAttribute(AttributeType.Int) + + # what we will produce + output_product_id = NodeAttribute(AttributeType.Int) + + # original from config, then updated by action + production_rate = NodeAttribute(AttributeType.Int) + + def __init__(self): + super(ManufactureDataModel, self).__init__() + self._output_product_id = 0 + self._production_rate = 0 + self._storage_id = 0 + + def initialize(self, configs: dict): + if configs is not None: + self._output_product_id = configs["output_product_id"] + self._production_rate = configs.get("production_rate", 1) + self._storage_id = configs["storage_id"] + + self.reset() + + def reset(self): + super(ManufactureDataModel, self).reset() + + self.output_product_id = self._output_product_id + self.production_rate = self._production_rate + self.storage_id = self._storage_id diff --git a/maro/simulator/scenarios/supply_chain/data/seller.py b/maro/simulator/scenarios/supply_chain/data/seller.py new file mode 100644 index 000000000..c0ad6a92b --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/seller.py @@ -0,0 +1,47 @@ +from .base import DataModelBase + +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +# NOTE: one sku one seller +@node("seller") +class SellerDataModel(DataModelBase): + # reward states + total_sold = NodeAttribute(AttributeType.Int) + + # action states + unit_price = NodeAttribute(AttributeType.Int) + + # + sale_gamma = NodeAttribute(AttributeType.Int) + + # what we will sell + product_id = NodeAttribute(AttributeType.Int) + + # per tick state, we can use this to support "sale hist" feature in original code. + # original there is only sold state, we add a demand here + demand = NodeAttribute(AttributeType.Int) + sold = NodeAttribute(AttributeType.Int) + + def __init__(self): + super(SellerDataModel, self).__init__() + + self._unit_price = 0 + self._sale_gamma = 0 + self._product_id = 0 + + def initialize(self, configs: dict): + if configs is not None: + self._unit_price = configs.get("unit_price", 0) + self._sale_gamma = configs.get("sale_gamma", 0) + self._product_id = configs.get("product_id", 0) + + self.reset() + + def reset(self): + super(SellerDataModel, self).reset() + + self.unit_price = self._unit_price + self.product_id = self._product_id + self.sale_gamma = self._sale_gamma diff --git a/maro/simulator/scenarios/supply_chain/data/storage.py b/maro/simulator/scenarios/supply_chain/data/storage.py new file mode 100644 index 000000000..2b9e7a2b1 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/storage.py @@ -0,0 +1,42 @@ +from .base import DataModelBase + +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +@node("storage") +class StorageDataModel(DataModelBase): + unit_storage_cost = NodeAttribute(AttributeType.Int) + remaining_space = NodeAttribute(AttributeType.Int) + capacity = NodeAttribute(AttributeType.Int) + + # original is stock_levels, used to save product and its number + product_list = NodeAttribute(AttributeType.Int, 1, is_list=True) + product_number = NodeAttribute(AttributeType.Int, 1, is_list=True) + + def __init__(self): + super(StorageDataModel, self).__init__() + + self._unit_storage_cost = 0 + self._capacity = 0 + + def initialize(self, configs): + if configs is not None: + self._unit_storage_cost = configs.get("unit_storage_cost", 0) + self._capacity = configs.get("capacity", 0) + + self.reset() + + def reset(self): + super(StorageDataModel, self).reset() + + self.unit_storage_cost = self._unit_storage_cost + self.capacity = self._capacity + + self.remaining_space = self._capacity + + def _on_product_number_changed(self, value): + if len(self.product_number) > 0: + taken_number = sum(self.product_number[:]) + + self.remaining_space = self.capacity - taken_number diff --git a/maro/simulator/scenarios/supply_chain/data/transport.py b/maro/simulator/scenarios/supply_chain/data/transport.py new file mode 100644 index 000000000..d70a0897c --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/transport.py @@ -0,0 +1,49 @@ +from .base import DataModelBase +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +@node("transport") +class TransportDataModel(DataModelBase): + # Id of current entity + source = NodeAttribute(AttributeType.Int) + + # Id of target entity. + destination = NodeAttribute(AttributeType.Int) + + # Number of product. + payload = NodeAttribute(AttributeType.Int) + + # Index of product. + product_id = NodeAttribute(AttributeType.Int) + + requested_quantity = NodeAttribute(AttributeType.Int) + + # Patient to wait for products ready. + patient = NodeAttribute(AttributeType.Int) + + # Steps to destination. + steps = NodeAttribute(AttributeType.Int) + + # Current location on the way, equal to step means arrive at destination. + location = NodeAttribute(AttributeType.Int) + + # for debug + position = NodeAttribute(AttributeType.Int, 2) + + def __init__(self): + super(TransportDataModel, self).__init__() + + self._patient = 0 + + def initialize(self, configs: dict): + if configs is not None: + self._patient = configs.get("patient", 100) + + self.reset() + + def reset(self): + super(TransportDataModel, self).reset() + + self.patient = self._patient + self.position[:] = -1 From 19ed33ebccf5e4e40438e023bf820f4fa913bda5 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 8 Mar 2021 18:56:33 +0800 Subject: [PATCH 041/482] remove code config, use yml instead --- examples/hello_world/supply_chain/hello.py | 2 +- .../scenarios/supply_chain/business_engine.py | 10 +- .../supply_chain/config_parser/__init__.py | 1 + .../supply_chain/config_parser/parser.py | 79 +++- .../scenarios/supply_chain/configs.py | 393 ------------------ .../supply_chain/topologies/core.yml | 86 ++++ .../topologies/sample1/config.yml | 191 +++++++++ .../simulator/scenarios/supply_chain/world.py | 50 ++- setup.py | 14 +- 9 files changed, 403 insertions(+), 423 deletions(-) delete mode 100644 maro/simulator/scenarios/supply_chain/configs.py create mode 100644 maro/simulator/scenarios/supply_chain/topologies/core.yml create mode 100644 maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index da91d2e29..020d56c48 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -293,7 +293,7 @@ def choose_consumer_action(self): def main(): start_tick = 0 durations = 100 - env = Env(scenario="supply_chain", topology="no", start_tick=start_tick, durations=durations) + env = Env(scenario="supply_chain", topology="sample1", start_tick=start_tick, durations=durations) irenv = InteractiveRenderaleEnv(env) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 4cba21e2f..a307ea26d 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -7,7 +7,8 @@ from .units import UnitBase from .world import World -from .configs import test_world_config + +from .config_parser import ConfigParser class SupplyChainBusinessEngine(AbsBusinessEngine): @@ -71,11 +72,14 @@ def _register_events(self): def _build_world(self): self.update_config_root_path(__file__) - # config_path = os.path.join(self._config_path, "config.yml") + core_config = os.path.join(self._config_path, "..", "core.yml") + config_path = os.path.join(self._config_path, "config.yml") + + parser = ConfigParser(core_config, config_path) self.world = World() - self.world.build(test_world_config, self.calc_max_snapshots()) + self.world.build(parser.parse(), self.calc_max_snapshots()) def _on_action_received(self, event): action = event.payload diff --git a/maro/simulator/scenarios/supply_chain/config_parser/__init__.py b/maro/simulator/scenarios/supply_chain/config_parser/__init__.py index e69de29bb..032a1f5f2 100644 --- a/maro/simulator/scenarios/supply_chain/config_parser/__init__.py +++ b/maro/simulator/scenarios/supply_chain/config_parser/__init__.py @@ -0,0 +1 @@ +from .parser import ConfigParser, SupplyChainConfiguration diff --git a/maro/simulator/scenarios/supply_chain/config_parser/parser.py b/maro/simulator/scenarios/supply_chain/config_parser/parser.py index 6a20c93e4..01dad04f5 100644 --- a/maro/simulator/scenarios/supply_chain/config_parser/parser.py +++ b/maro/simulator/scenarios/supply_chain/config_parser/parser.py @@ -5,8 +5,9 @@ from collections import namedtuple +# TODO: ugly implementation, refactoring later. -DataModelItem = namedtuple("DataModelItem", ("alias", "module_path", "class_name", "class_type")) +DataModelItem = namedtuple("DataModelItem", ("alias", "module_path", "class_name", "class_type", "name_in_frame")) UnitItem = namedtuple("UnitItem", ("alias", "module_path", "class_name", "class_type", "configs")) FacilityItem = namedtuple("FacilityItem", ("alias", "module_path", "class_name", "class_type", "configs")) @@ -17,6 +18,17 @@ def find_class_type(module_path: str, class_name: str): return getattr(target_module, class_name) +def copy_dict(dest:dict, source: dict): + for k, v in source.items(): + if type(v) != dict: + dest[k] = v + else: + if k not in dest: + dest[k] = {} + + copy_dict(dest[k], v) + + class SupplyChainConfiguration: data_models = None units = None @@ -29,11 +41,17 @@ def __init__(self): self.facilities = {} self.world = {} - def add_data_definition(self, alias: str, class_name: str, module_path: str): + def add_data_definition(self, alias: str, class_name: str, module_path: str, name_in_frame: str): # check conflict assert alias not in self.data_models - self.data_models[alias] = DataModelItem(alias, module_path, class_name, find_class_type(module_path, class_name)) + self.data_models[alias] = DataModelItem( + alias, + module_path, + class_name, + find_class_type(module_path, class_name), + name_in_frame + ) def add_unit_definition(self, alias: str, class_name: str, module_path: str, configs: dict): assert alias not in self.units @@ -46,7 +64,6 @@ def add_facility_definition(self, alias: str, class_name: str, module_path: str, self.facilities[alias] = FacilityItem(alias, module_path, class_name, find_class_type(module_path, class_name), configs) - class ConfigParser: def __init__(self, core_file: str, config_file: str): self._core_file = core_file @@ -73,7 +90,7 @@ def _read_core_conf(self, core_config: dict): module_path = module_conf["path"] for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_data_definition(class_alias, class_def["class"], module_path) + self._result.add_data_definition(class_alias, class_def["class"], module_path, class_def["name_in_frame"]) # units if "units" in core_config: @@ -109,19 +126,61 @@ def _parse_world_config(self): facility_def = self._result.facilities[facility_class_alias] + configs = facility_conf["configs"] + + # components for property_name, property_conf in facility_def.configs.items(): if property_name == "class": continue - if property_name not in facility_conf: - facility_conf["configs"][property_name] = property_conf + # if the config not exist, then copy it + if property_name not in configs: + configs[property_name] = {} + #copy_dict(configs[property_name], property_conf) + configs[property_name] = property_conf else: - pass + # TODO: support more than 1 depth checking + # configurations for components + if type(property_conf) == dict: + #copy_dict(configs[property_name], property_conf) + for sub_key, sub_value in property_conf.items(): + if sub_key not in configs[property_name]: + configs[property_name][sub_key] = sub_value + + # check data field of units + for unit_name, unit_conf in configs.items(): + if type(unit_conf) == dict: + if "class" not in unit_conf: + continue + + unit = self._result.units[unit_conf["class"]] + + if "data" not in unit_conf: + # copy from definition + unit_conf["data"] = unit.configs["data"] + else: + # copy missing fields + for k, v in unit.configs["data"].items(): + if k not in unit_conf["data"]: + unit_conf["data"][k] = v + elif type(unit_conf) == list: + # list is a placeholder, we just need copy the class alias for data + for unit_item in unit_conf: + unit = self._result.units[unit_item["class"]] + + if "data" not in unit_item: + unit_item["data"]["class"] = unit.configs["data"]["class"] + else: + unit_item["data"]["class"] = unit.configs["data"]["class"] if __name__ == "__main__": - parser = ConfigParser("maro/simulator/scenarios/supply_chain/topologies/core.yaml", "maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml") + parser = ConfigParser("maro/simulator/scenarios/supply_chain/topologies/core.yml", "maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml") result = parser.parse() - print(result.world) + import pprint + + pp = pprint.PrettyPrinter(indent=2, depth=8) + + pp.pprint(result.world) diff --git a/maro/simulator/scenarios/supply_chain/configs.py b/maro/simulator/scenarios/supply_chain/configs.py deleted file mode 100644 index b211d6f55..000000000 --- a/maro/simulator/scenarios/supply_chain/configs.py +++ /dev/null @@ -1,393 +0,0 @@ - -from enum import Enum - -from .data import ( - StorageDataModel, - TransportDataModel, - DistributionDataModel, - ManufactureDataModel, - ConsumerDataModel, - SellerDataModel -) - -from .units import ( - StorageUnit, - TransportUnit, - DistributionUnit, - ManufacturingUnit, - ConsumerUnit, - SellerUnit -) - -from .facilities import ( - WarehouseFacility, - SupplierFacility, - RetailerFacility -) - - -class CellType(Enum): - """Cell type in map grid. - """ - facility = 1 - railroad = 2 # TODO: or railway? - - -data_class_mapping = { - "StorageDataModel": { - "alias_in_snapshot": "storages", - "class": StorageDataModel - }, - "TransportDataModel": { - "alias_in_snapshot": "transports", - "class": TransportDataModel - }, - "DistributionDataModel": { - "alias_in_snapshot": "distributions", - "class": DistributionDataModel - }, - "ManufactureDataModel": { - "alias_in_snapshot": "manufactures", - "class": ManufactureDataModel - }, - "ConsumerDataModel": { - "alias_in_snapshot": "consumers", - "class": ConsumerDataModel - }, - "SellerDataModel": { - "alias_in_snapshot": "seller", - "class": SellerDataModel - } -} - - -unit_mapping = { - "StorageUnit": { - "class": StorageUnit - }, - "TransportUnit": { - "class": TransportUnit - }, - "DistributionUnit": { - "class": DistributionUnit - }, - "ManufacturingUnit": { - "class": ManufacturingUnit - }, - "ConsumerUnit": { - "class": ConsumerUnit - }, - "SellerUnit": { - "class": SellerUnit - } -} - -test_world_config = { - # skus in this world, used to generate id - "skus": [ - { - "id": 1, - "name": "sku1", - "output_units_per_lot": 1, # later we can support override per facility - "bom": { # bill of materials to procedure this, we can support facility level override - "sku3": 10 # units per lot - } - }, - { - "id": 2, - "output_units_per_lot": 1, - "name": "sku2" - }, - { - "id": 3, - "output_units_per_lot": 1, - "name": "sku3" - } - ], - "facilities": [ - { - # a source material supplier without input requirement - "name": "Supplier3", - "class": SupplierFacility, - "configs": { - "skus": { - "sku3": { - "init_in_stock": 100, - "production_rate": 200, - "delay_order_penalty": 10, - "type": "production", - "cost": 10, - "price": 10 - } - }, - "storage": { - "data": { - "class": "StorageDataModel", - "capacity": 20000, - "unit_storage_cost": 10 - }, - "class": "StorageUnit" - }, - "distribution": { - "data": { - "class": "DistributionDataModel", - "unit_price": 10 - }, - "class": "DistributionUnit" - }, - "transports": [ - { - "data": { - "class": "TransportDataModel", - "patient": 100 - }, - "class": "TransportUnit" - }, - { - "data": { - "class": "TransportDataModel", - "patient": 100 - }, - "class": "TransportUnit" - } - ], - "consumers": { - "data": { - "class": "ConsumerDataModel" - }, - "class": "ConsumerUnit" - }, - "manufactures": { - "data": { - "class": "ManufactureDataModel" - }, - "class": "ManufacturingUnit" - } - } - }, - { - "name": "Supplier1", - "class": SupplierFacility, - "configs": { - "skus": { - "sku1": { - "init_in_stock": 100, - "production_rate": 200, - "delay_order_penalty": 10, - "type": "production", - "cost": 10, - "price": 100 - }, - # source material, do not need production rate - "sku3": { - "init_in_stock": 100, - "production_rate": 200, - "delay_order_penalty": 10, - "type": "material", - "cost": 10, - "price": 100 - } - }, - "storage": { - "data": { - "class": "StorageDataModel", - "capacity": 20000, - "unit_storage_cost": 10 - }, - "class": "StorageUnit" - }, - "distribution": { - "data": { - "class": "DistributionDataModel", - "unit_price": 10 - }, - "class": "DistributionUnit" - }, - "transports": [ - { - "data": { - "class": "TransportDataModel", - "patient": 100 - }, - "class": "TransportUnit" - }, - { - "data": { - "class": "TransportDataModel", - "patient": 100 - }, - "class": "TransportUnit" - } - ], - "consumers": { - "data": { - "class": "ConsumerDataModel" - }, - "class": "ConsumerUnit" - }, - "manufactures": { - "data": { - "class": "ManufactureDataModel" - }, - "class": "ManufacturingUnit" - } - } - }, - { - "name": "warehouse1", - "class": WarehouseFacility, - "configs": { - "skus": { - "sku1": { - "init_stock": 1000, - "delay_order_penalty": 10, - "price": 100 - }, - "sku2": { - "init_stock": 1000, - "delay_order_penalty": 10, - "price": 100 - }, - "sku3": { - "init_stock": 1000, - "delay_order_penalty": 10, - "price": 100 - }, - }, - "storage": { - "data": { - "class": "StorageDataModel", - "capacity": 20000, - "unit_storage_cost": 10 - }, - "class": "StorageUnit" - }, - "distribution": { - "data": { - "class": "DistributionDataModel", - "unit_price": 10 - }, - "class": "DistributionUnit" - }, - "transports": [ - { - "data": { - "class": "TransportDataModel", - "patient": 100 - }, - "class": "TransportUnit" - }, - { - "data": { - "class": "TransportDataModel", - "patient": 100 - }, - "class": "TransportUnit" - } - ], - "consumers": { - "data": { - "class": "ConsumerDataModel" - }, - "class": "ConsumerUnit" - } - } - }, - { - "name": "ChaoShiFa01", - "class": RetailerFacility, - "configs": { - "skus": { - "sku1": { - "price": 300, - "cost": 10, - "init_in_stock": 100, - "sale_gamma": 100 - }, - "sku3": { - "price": 200, - "cost": 10, - "init_in_stock": 100, - "sale_gamma": 100 - }, - "sku2": { - "price": 100, - "cost": 10, - "init_in_stock": 100, - "sale_gamma": 100 - }, - }, - "storage": { - "data": { - "class": "StorageDataModel", - "capacity": 20000, - "unit_storage_cost": 10 - }, - "class": "StorageUnit" - }, - "consumers": { - "data": { - "class": "ConsumerDataModel" - }, - "class": "ConsumerUnit" - }, - "sellers": { - "data": { - "class": "SellerDataModel" - }, - "class": "SellerUnit" - } - } - } - ], - # topology used to specify the up/downstream for facilities - # we split it from facility, so that we can support configuration inherit to override it - # for a new topology - "topology": { - # key is current facility, value if upstream facilities that will provide a certain sku - "Supplier1": { - # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", - # or any other facility in the list - "sku3": [ - "Supplier3" - ] - }, - "warehouse1": { - "sku1": [ - "Supplier1" - ], - "sku3": [ - "Supplier3" - ] - }, - "ChaoShiFa01": { - "sku1": [ - "Supplier1" - ], - "sku3": [ - "Supplier3" - ] - } - }, - "grid": { - "size": [20, 20], - # facility position in grid - "facilities": { - "Supplier1": [0, 0], # (x, y) - "Supplier3": [3, 3], - "warehouse1": [6, 6] - }, - # cells that un-traversable - "blocks": { - # TODO: later we can have different cell have different travel cost - CellType.railroad: [ - [10, 10], - [10, 11], - [10, 12], - [11, 12] - ] - } - }, - # how many ticks to ask for an action. - "action_steps": 1 -} diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml new file mode 100644 index 000000000..1388e1e64 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/core.yml @@ -0,0 +1,86 @@ + +data: + modules: + - path: "maro.simulator.scenarios.supply_chain.data" + definitions: + StorageDataModel: + class: "StorageDataModel" + name_in_frame: "storage" + TransportDataModel: + class: "TransportDataModel" + name_in_frame: "transport" + DistributionDataModel: + class: "DistributionDataModel" + name_in_frame: "distribution" + ConsumerDataModel: + class: "ConsumerDataModel" + name_in_frame: "consumer" + SellerDataModel: + class: "SellerDataModel" + name_in_frame: "seller" + ManufactureDataModel: + class: "ManufactureDataModel" + name_in_frame: "manufacture" + +units: + modules: + - path: "maro.simulator.scenarios.supply_chain.units" + definitions: + StorageUnit: + class: "StorageUnit" + data: + class: "StorageDataModel" + TransportUnit: + class: "TransportUnit" + data: + class: "TransportDataModel" + DistributionUnit: + class: "DistributionUnit" + data: + class: "DistributionDataModel" + ConsumerUnit: + class: "ConsumerUnit" + data: + class: "ConsumerDataModel" + SellerUnit: + class: "SellerUnit" + data: + class: "SellerDataModel" + ManufactureUnit: + class: "ManufacturingUnit" + data: + class: "ManufactureDataModel" + +facilities: + modules: + - path: "maro.simulator.scenarios.supply_chain.facilities" + definitions: + WarehouseFacility: + class: "WarehouseFacility" + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + consumers: + class: "ConsumerUnit" + transports: [] + SupplierFacility: + class: "SupplierFacility" + distribution: + class: "DistributionUnit" + storage: + class: "StorageUnit" + consumers: + class: "ConsumerUnit" + manufactures: + class: "ManufactureUnit" + # transports should be defined by user, so here is just a empty list + transports: [] + RetailerFacility: + class: "RetailerFacility" + storage: + class: "StorageUnit" + consumers: + class: "ConsumerUnit" + sellers: + class: "SellerUnit" diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml new file mode 100644 index 000000000..fd14a743f --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -0,0 +1,191 @@ + +# TODO: which config to inherit +base: "" + +# TODO: add customized data model, unit or facility definitions, will be combined with builtin ones. +#core: +# data: "xxx" +# units: "xxx" +# facilities: "xxx" + +# world definitions +world: + # how many ticks to ask for an action. + action_steps: 1 + + # sku list in this world + # this list do not contains price, cost or other facility related attributes, + # but just base info, like name, id, bom + skus: + - id: 1 + name: "sku1" + output_units_per_lot: 1 + # bill of material that used produce current sku, empty means do not need source material + bom: + # key is the source sku name, value is quantity needed to use per time to produce current sku + sku3: 10 + - id: 2 + name: "sku2" + output_units_per_lot: 1 + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + # facilities in this world + facilities: + - name: "Supplier3" # name of the facility + class: "SupplierFacility" # class alias of this facility + # config of this facility + configs: + # sku list of this facility + skus: + sku3: # sku name and attributes needed for this facility + init_in_stock: 100 + production_rate: 200 + delay_order_penalty: 10 + type: "production" # production means this is the output production of this facility + cost": 10 + price": 10 + # config for units + # config of storage unit + storage: + # config of data model of this unit + data: + # other config or storage unit + capacity: 20000 + unit_storage_cost: 10 + distribution: + data: + unit_price: 10 + transports: + # definition of each vehicle + - class: "TransportUnit" + data: + patient: 100 + - class: "TransportUnit" + data: + patient: 100 + - name: "Supplier1" + class: "SupplierFacility" + configs: + skus: + sku1: + init_in_stock: 100 + production_rate: 200 + delay_order_penalty: 10 + type: "production" + cost: 10 + price: 100 + sku3: + init_in_stock: 100 + production_rate: 200 + delay_order_penalty: 10 + type: "material" + cost: 10 + price: 100 + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + distribution: + data: + unit_price: 10 + transports: + - class: "TransportUnit" + data: + patient: 100 + - class: "TransportUnit" + data: + patient: 100 + - name: "Warehouse1" + class: "WarehouseFacility" + configs: + skus: + sku1: + init_stock: 1000 + delay_order_penalty: 10 + price: 100 + sku2: + init_stock: 1000 + delay_order_penalty: 10 + price: 100 + sku3: + init_stock: 1000 + delay_order_penalty: 10 + price: 100 + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + distribution: + data: + unit_price: 10 + transports: + - class: "TransportUnit" + data: + patient: 100 + - class: "TransportUnit" + data: + patient: 100 + - name: "ChaoShiFa01" + class: "RetailerFacility" + configs: + skus: + sku1: + price: 300 + cost: 10 + init_in_stock: 100 + sale_gamma: 100 + sku3: + price: 200 + cost: 10 + init_in_stock: 100 + sale_gamma: 100 + sku2: + price: 100 + cost: 10 + init_in_stock: 100 + sale_gamma: 100 + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + + # topology used to specify the up/downstream for facilities + # we split it from facility, so that we can support configuration inherit to override it + # for a new topology + # TODO: change the name? + topology: + # key is current facility, value if upstream facilities that will provide a certain sku + Supplier1: + # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", + # or any other facility in the list + sku3: + - "Supplier3" + Warehouse1: + sku1: + - "Supplier1" + sku3: + - "Supplier3" + ChaoShiFa01: + sku1: + - "Supplier1" + sku3: + - "Supplier3" + + # map grid definitions + grid: + size: [ 20, 20 ] + # facility position in grid + facilities: + Supplier1: [ 0, 0 ] # (x, y) + Supplier3: [ 3, 3 ] + Warehouse1: [ 6, 6 ] + # cells that un-traversable + blocks: + # TODO: later we can have different cell have different travel cost + railroad: + - [ 10, 10 ] + - [ 10, 11 ] + - [ 10, 12 ] + - [ 11, 12 ] diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index be120d327..f55736bde 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -5,10 +5,10 @@ from .frame_builder import build_frame -from .configs import unit_mapping, data_class_mapping - from tcod.path import AStar +from .config_parser import SupplyChainConfiguration + # sku definition in world level # bom is a dictionary, key is the material sku id, value is units per lot @@ -51,6 +51,10 @@ def __init__(self): # a star path finder self._path_finder: AStar = None + self._data_model_definitions = None + self._facility_definitions = None + self._unit_definitions = None + def gen_id(self): """Generate id for facility or unit.""" new_id = self._id_counter @@ -61,9 +65,9 @@ def gen_id(self): def build_unit(self, name: str): """Build an unit instance from it name via current configuration.""" - assert name in unit_mapping + assert name in self._unit_definitions - unit = unit_mapping[name]["class"]() + unit = self._unit_definitions[name].class_type() unit.id = self.gen_id() @@ -71,9 +75,14 @@ def build_unit(self, name: str): return unit - def build(self, configs: dict, snapshot_number: int): + def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: int): """Build current world according to configurations.""" - self.configs = configs + self.configs = all_in_one_config.world + self._facility_definitions = all_in_one_config.facilities + self._unit_definitions = all_in_one_config.units + self._data_model_definitions = all_in_one_config.data_models + + configs = self.configs # collect sku information first for sku_conf in configs["skus"]: @@ -96,7 +105,8 @@ def build(self, configs: dict, snapshot_number: int): facility_name = facility_conf["name"] # create a new instance of facility - facility = facility_conf["class"]() + facility_def = self._facility_definitions[facility_conf["class"]] + facility = facility_def.class_type() # NOTE: DO set these fields before other operations. facility.world = self @@ -115,11 +125,11 @@ def build(self, configs: dict, snapshot_number: int): data_class_in_frame = [] for class_name, number in self._data_class_collection.items(): - class_config = data_class_mapping[class_name] + class_def = self._data_model_definitions[class_name] data_class_in_frame.append(( - class_config["class"], - class_config["alias_in_snapshot"], + class_def.class_type, + class_def.name_in_frame, number )) @@ -180,7 +190,7 @@ def get_entity(self, entity_id: int): return self._entities[entity_id] def register_data_class(self, unit_id: int, name: str): - assert name in data_class_mapping + assert name in self._data_model_definitions node_index = self._data_class_collection[name] @@ -190,7 +200,7 @@ def register_data_class(self, unit_id: int, name: str): return node_index def get_data_instance(self, class_name: str, node_index: int): - alias = data_class_mapping[class_name]["alias_in_snapshot"] + alias = self._data_model_definitions[class_name].name_in_frame return getattr(self.frame, alias)[node_index] @@ -222,3 +232,19 @@ def get_node_mapping(self): "mapping": id2index_mapping, "detail": facility_info_dict } + + def _build_facility(self, conf: dict): + name = conf["name"] + class_alias = conf["class"] + + facility_def = self.configs.facilities[class_alias] + + facility = facility_def.class_type() + + facility.id = self.gen_id() + facility.world = self + facility.name = name + + facility.build(conf["configs"]) + + return facility diff --git a/setup.py b/setup.py index e095cce77..84c31a467 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import io import os +import sys import numpy # NOTE: DO NOT change the import order, as sometimes there is a conflict between setuptools and distutils, @@ -14,6 +15,11 @@ from maro import __version__ +compile_flag = '-std=c++11' + +if sys.platform == "win32": + compile_flag = '/std:c++14' + # Set environment variable to skip deployment process of MARO os.environ["SKIP_DEPLOYMENT"] = "TRUE" @@ -39,7 +45,7 @@ Extension( f"{BASE_MODULE_NAME}.backend", sources=[f"{BASE_SRC_PATH}/backend.cpp"], - extra_compile_args=['-std=c++11']) + extra_compile_args=[compile_flag]) ) @@ -50,7 +56,7 @@ f"{BASE_MODULE_NAME}.np_backend", sources=[f"{BASE_SRC_PATH}/np_backend.cpp"], include_dirs=include_dirs, - extra_compile_args=['-std=c++11']) + extra_compile_args=[compile_flag]) ) # raw implementation @@ -60,7 +66,7 @@ f"{BASE_MODULE_NAME}.raw_backend", sources=[f"{BASE_SRC_PATH}/raw_backend.cpp"], include_dirs=include_dirs, - extra_compile_args=['-std=c++11']) + extra_compile_args=[compile_flag]) ) # frame @@ -69,7 +75,7 @@ f"{BASE_MODULE_NAME}.frame", sources=[f"{BASE_SRC_PATH}/frame.cpp"], include_dirs=include_dirs, - extra_compile_args=['-std=c++11']) + extra_compile_args=[compile_flag]) ) From ee11adf36a849409ea7c0d2a2d50a75c3991531f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 8 Mar 2021 19:06:32 +0800 Subject: [PATCH 042/482] add 2 different step modes --- .../scenarios/supply_chain/business_engine.py | 22 +++++++++++++++++-- .../topologies/sample1/config.yml | 6 +++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index a307ea26d..5b9ed8233 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -1,5 +1,6 @@ import os +import random from maro.simulator.scenarios import AbsBusinessEngine @@ -25,6 +26,9 @@ def __init__(self, **kwargs): self._action_steps = self.world.configs["action_steps"] + # for update by unit + self._unit_id_list = None + @property def frame(self): return self._frame @@ -41,14 +45,28 @@ def get_node_mapping(self) -> dict: return self._node_mapping def step(self, tick: int): - for _, facility in self.world.facilities.items(): - facility.step(tick) + self._step_by_facility(tick) if tick % self._action_steps == 0: decision_event = self._event_buffer.gen_decision_event(tick, None) self._event_buffer.insert_event(decision_event) + def _step_by_facility(self, tick: int): + for _, facility in self.world.facilities.items(): + facility.step(tick) + + def _step_by_units(self, tick: int): + if self._unit_id_list is None: + self._unit_id_list = [i for i in self.world.unit_id2index_mapping.keys()] + + random.shuffle(self._unit_id_list) + + for unit_id in self._unit_id_list: + unit = self.world.get_entity(unit_id) + + unit.step(tick) + def post_step(self, tick: int): # take snapshot if (tick + 1) % self._snapshot_resolution == 0: diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index fd14a743f..cf3fbe01d 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -13,6 +13,12 @@ world: # how many ticks to ask for an action. action_steps: 1 + # how the simulator update the world + # available values: + # facility: update facilities (internal units) one by one + # random: random update units + step_mode: "facility" + # sku list in this world # this list do not contains price, cost or other facility related attributes, # but just base info, like name, id, bom From 99e0b670b6c87d3f91d6fb87ffa5381dd604c7b7 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 8 Mar 2021 19:10:34 +0800 Subject: [PATCH 043/482] update changes --- maro/simulator/scenarios/supply_chain/changes.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/changes.md b/maro/simulator/scenarios/supply_chain/changes.md index b3b43dbcb..4d64c22c1 100644 --- a/maro/simulator/scenarios/supply_chain/changes.md +++ b/maro/simulator/scenarios/supply_chain/changes.md @@ -5,12 +5,12 @@ original: . bom not configured in configuration file . source material is same as output product - . hard coded "output_lot_size = 1" and control.production_rate to control the produce rate, + . hard coded "output_lot_size = 1", use control.production_rate to control the produce rate, but the sku.production_rate in configuration file not in use. changed: . bom configured in configuration file at world level. later we can support override this at each manufacture unit. - . remove output_lot_size from bom, user sku.production_rate at facility level, then action can change this + . remove output_lot_size from bom, # use sku.production_rate at facility level, then action can change this . support manufacturing without source material, like oil, just produce output production by configured rate . add type for sku at facility level to identify if it is an input material, or output production . remove output_lot_size, always be 1 @@ -20,4 +20,10 @@ different topology. . there is no consumer_quantity and source by default, we use first source as source_id - but with quantity as 0, means we will wait for action to purchase source sku. \ No newline at end of file + but with quantity as 0, means we will wait for action to purchase source sku. + +4. facility and unit + now facility own all its related units, not just share a storage/transports unit with units, so + it is a true tree like structure. + + also there is no nested units now, they are all attached to facility now. \ No newline at end of file From af2cfe94028520b7e9ba7c40608c3b33216bd12c Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 9 Mar 2021 08:13:24 +0800 Subject: [PATCH 044/482] rename manufacture --- maro/simulator/scenarios/supply_chain/topologies/core.yml | 2 +- maro/simulator/scenarios/supply_chain/units/__init__.py | 2 +- .../supply_chain/units/{manufacturing.py => manufacture.py} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename maro/simulator/scenarios/supply_chain/units/{manufacturing.py => manufacture.py} (97%) diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml index 1388e1e64..b5d8b2c16 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/core.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/core.yml @@ -47,7 +47,7 @@ units: data: class: "SellerDataModel" ManufactureUnit: - class: "ManufacturingUnit" + class: "ManufactureUnit" data: class: "ManufactureDataModel" diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index 230361c8b..cd93869da 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -2,6 +2,6 @@ from .storage import StorageUnit from .transport import TransportUnit from .distribution import DistributionUnit -from .manufacturing import ManufacturingUnit +from .manufacture import ManufactureUnit from .consumer import ConsumerUnit from .seller import SellerUnit diff --git a/maro/simulator/scenarios/supply_chain/units/manufacturing.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py similarity index 97% rename from maro/simulator/scenarios/supply_chain/units/manufacturing.py rename to maro/simulator/scenarios/supply_chain/units/manufacture.py index bc5026711..37f2180b5 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacturing.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -2,7 +2,7 @@ from .base import UnitBase -class ManufacturingUnit(UnitBase): +class ManufactureUnit(UnitBase): """Unit that used to produce certain product(sku) with consume specified source skus. One manufacture unit per sku. @@ -17,7 +17,7 @@ class ManufacturingUnit(UnitBase): input_units_per_lot = 0 def __init__(self): - super(ManufacturingUnit, self).__init__() + super(ManufactureUnit, self).__init__() def initialize(self, configs: dict): # add the storage_id From f4ec67a0555cf110c45d2e3501503fb0767b500a Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 9 Mar 2021 08:16:50 +0800 Subject: [PATCH 045/482] add action for manufacture unit --- maro/simulator/scenarios/supply_chain/changes.md | 2 +- maro/simulator/scenarios/supply_chain/units/actions.py | 2 ++ maro/simulator/scenarios/supply_chain/units/manufacture.py | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/changes.md b/maro/simulator/scenarios/supply_chain/changes.md index 4d64c22c1..344817b2a 100644 --- a/maro/simulator/scenarios/supply_chain/changes.md +++ b/maro/simulator/scenarios/supply_chain/changes.md @@ -10,7 +10,7 @@ changed: . bom configured in configuration file at world level. later we can support override this at each manufacture unit. - . remove output_lot_size from bom, # use sku.production_rate at facility level, then action can change this + . remove output_lot_size from bom, use sku.production_rate at facility level, then action can change this . support manufacturing without source material, like oil, just produce output production by configured rate . add type for sku at facility level to identify if it is an input material, or output production . remove output_lot_size, always be 1 diff --git a/maro/simulator/scenarios/supply_chain/units/actions.py b/maro/simulator/scenarios/supply_chain/units/actions.py index 3e07934bb..d994d8265 100644 --- a/maro/simulator/scenarios/supply_chain/units/actions.py +++ b/maro/simulator/scenarios/supply_chain/units/actions.py @@ -5,3 +5,5 @@ ConsumerAction = namedtuple("ConsumerAction", ("id", "source_id", "quantity", "vlt")) + +ManufactureAction = namedtuple("ManufactureAction", ("id", "production_rate")) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 37f2180b5..01e95cfc6 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -1,6 +1,8 @@ from .base import UnitBase +from .actions import ManufactureAction + class ManufactureUnit(UnitBase): """Unit that used to produce certain product(sku) with consume specified source skus. @@ -62,8 +64,8 @@ def post_step(self, tick: int): # reset the manufacture cost per tick self.data.manufacturing_number = 0 - def set_action(self, action: int): + def set_action(self, action: ManufactureAction): # we expect production rate number as action # production_rate <= 0 will stop manufacturing - self.data.production_rate = action + self.data.production_rate = action.production_rate From 8edd14c24e3be856c7ebad77706a871bf89a740b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 9 Mar 2021 15:57:00 +0800 Subject: [PATCH 046/482] more attribute for states --- maro/simulator/scenarios/supply_chain/data/consumer.py | 2 ++ maro/simulator/scenarios/supply_chain/data/manufacture.py | 2 +- maro/simulator/scenarios/supply_chain/data/seller.py | 6 ++++++ maro/simulator/scenarios/supply_chain/data/transport.py | 6 ++++++ .../scenarios/supply_chain/topologies/sample1/config.yml | 5 +++++ maro/simulator/scenarios/supply_chain/units/consumer.py | 3 ++- 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/data/consumer.py b/maro/simulator/scenarios/supply_chain/data/consumer.py index 7aeb4552a..40d0444d6 100644 --- a/maro/simulator/scenarios/supply_chain/data/consumer.py +++ b/maro/simulator/scenarios/supply_chain/data/consumer.py @@ -8,6 +8,7 @@ @node("consumer") class ConsumerDataModel(DataModelBase): # reward states + # from config order_cost = NodeAttribute(AttributeType.Int) total_purchased = NodeAttribute(AttributeType.Int) total_received = NodeAttribute(AttributeType.Int) @@ -26,6 +27,7 @@ class ConsumerDataModel(DataModelBase): # snapshots["consumer"][hist_len::"purchased"] equals to original latest_consumptions purchased = NodeAttribute(AttributeType.Int) received = NodeAttribute(AttributeType.Int) + order_product_cost = NodeAttribute(AttributeType.Int) def __init__(self): super(ConsumerDataModel, self).__init__() diff --git a/maro/simulator/scenarios/supply_chain/data/manufacture.py b/maro/simulator/scenarios/supply_chain/data/manufacture.py index e9aaa81fc..6e68d0281 100644 --- a/maro/simulator/scenarios/supply_chain/data/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/data/manufacture.py @@ -12,7 +12,7 @@ class ManufactureDataModel(DataModelBase): # cost to produce one output production product_unit_cost = NodeAttribute(AttributeType.Int) - # cost per tick, different with original manufacturing cost, we just provide number, and cost + # number per tick, different with original manufacturing cost, we just provide number, and cost # user can determine how to calculate the cost manufacturing_number = NodeAttribute(AttributeType.Int) diff --git a/maro/simulator/scenarios/supply_chain/data/seller.py b/maro/simulator/scenarios/supply_chain/data/seller.py index c0ad6a92b..4178d6c38 100644 --- a/maro/simulator/scenarios/supply_chain/data/seller.py +++ b/maro/simulator/scenarios/supply_chain/data/seller.py @@ -10,6 +10,9 @@ class SellerDataModel(DataModelBase): # reward states total_sold = NodeAttribute(AttributeType.Int) + # from config + backlog_ratio = NodeAttribute(AttributeType.Float) + # action states unit_price = NodeAttribute(AttributeType.Int) @@ -30,12 +33,14 @@ def __init__(self): self._unit_price = 0 self._sale_gamma = 0 self._product_id = 0 + self._backlog_ratio = 0 def initialize(self, configs: dict): if configs is not None: self._unit_price = configs.get("unit_price", 0) self._sale_gamma = configs.get("sale_gamma", 0) self._product_id = configs.get("product_id", 0) + self._backlog_ratio = configs.get("backlog_ratio", 0.1) self.reset() @@ -45,3 +50,4 @@ def reset(self): self.unit_price = self._unit_price self.product_id = self._product_id self.sale_gamma = self._sale_gamma + self.backlog_ratio = self._backlog_ratio diff --git a/maro/simulator/scenarios/supply_chain/data/transport.py b/maro/simulator/scenarios/supply_chain/data/transport.py index d70a0897c..809d36278 100644 --- a/maro/simulator/scenarios/supply_chain/data/transport.py +++ b/maro/simulator/scenarios/supply_chain/data/transport.py @@ -31,19 +31,25 @@ class TransportDataModel(DataModelBase): # for debug position = NodeAttribute(AttributeType.Int, 2) + # from config + unit_transport_cost = NodeAttribute(AttributeType.Int) + def __init__(self): super(TransportDataModel, self).__init__() self._patient = 0 + self._unit_transport_cost = 0 def initialize(self, configs: dict): if configs is not None: self._patient = configs.get("patient", 100) + self._unit_transport_cost = configs.get("unit_transport_cost", 1) self.reset() def reset(self): super(TransportDataModel, self).reset() + self.unit_transport_cost = self._unit_transport_cost self.patient = self._patient self.position[:] = -1 diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index cf3fbe01d..433d6c1be 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -130,9 +130,11 @@ world: - class: "TransportUnit" data: patient: 100 + unit_transport_cost: 1 # optional, default is 1 - class: "TransportUnit" data: patient: 100 + unit_transport_cost: 1 - name: "ChaoShiFa01" class: "RetailerFacility" configs: @@ -142,16 +144,19 @@ world: cost: 10 init_in_stock: 100 sale_gamma: 100 + backlog_ratio: 0.1 # optional sku3: price: 200 cost: 10 init_in_stock: 100 sale_gamma: 100 + backlog_ratio: 0.1 sku2: price: 100 cost: 10 init_in_stock: 100 sale_gamma: 100 + backlog_ratio: 0.1 storage: data: capacity: 20000 diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 2fb77f420..326afcfa9 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -49,13 +49,14 @@ def step(self, tick: int): source_facility = self.world.get_facility_by_id(source_id) - source_facility.distribution.place_order(order) + self.data.order_product_cost = source_facility.distribution.place_order(order) self.data.total_purchased += quantity def post_step(self, tick: int): self.data.received = 0 self.data.purchased = 0 + self.data.order_product_cost = 0 def reset(self): super(ConsumerUnit, self).reset() From 24a830adeab3d23211a951ae0255aad91a32a882 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 9 Mar 2021 18:07:27 +0800 Subject: [PATCH 047/482] add balance sheet --- examples/hello_world/supply_chain/hello.py | 27 +++- .../hello_world/supply_chain/state_shaping.py | 127 ++++++++++++++++++ maro/backends/raw_backend.pyx | 2 +- .../scenarios/supply_chain/data/__init__.py | 1 + .../scenarios/supply_chain/data/base.py | 4 + .../supply_chain/data/manufacture.py | 2 + .../scenarios/supply_chain/facilities/base.py | 3 + .../supply_chain/facilities/retailer.py | 15 +++ .../supply_chain/facilities/supplier.py | 89 +++++++----- .../supply_chain/facilities/warehouse.py | 19 ++- .../topologies/sample1/config.yml | 3 + .../scenarios/supply_chain/units/base.py | 5 +- .../scenarios/supply_chain/units/consumer.py | 38 ++++-- .../supply_chain/units/distribution.py | 9 +- .../supply_chain/units/manufacture.py | 16 ++- .../scenarios/supply_chain/units/seller.py | 15 ++- .../scenarios/supply_chain/units/storage.py | 6 + .../scenarios/supply_chain/units/transport.py | 56 ++++---- .../simulator/scenarios/supply_chain/world.py | 16 ++- 19 files changed, 362 insertions(+), 91 deletions(-) create mode 100644 examples/hello_world/supply_chain/state_shaping.py diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 020d56c48..0fec2c410 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -8,6 +8,7 @@ from tabulate import tabulate from maro.simulator import Env from maro.simulator.scenarios.supply_chain import ConsumerAction +from state_shaping import SupplyChainStateShaping WINDOW_SCALE_FACTOR = 2 @@ -15,6 +16,7 @@ CHAR_DEFAULT = 0x2610 + # https://stackoverflow.com/questions/287871/how-to-print-colored-text-to-the-terminal class bcolors: HEADER = '\033[95m' @@ -32,6 +34,8 @@ class InteractiveRenderaleEnv: def __init__(self, env: Env): self.env = env + self._state_shaping = SupplyChainStateShaping(env) + def start(self): print(f"{bcolors.WARNING}Press SPACE for next step!!!{bcolors.ENDC}") print(f"{bcolors.WARNING}Press S to show current debug states!!!{bcolors.ENDC}") @@ -104,6 +108,8 @@ def start(self): is_new_step = True + self.state_shaping() + print(f"{bcolors.OKGREEN}Current environment tick:", self.env.tick, f"{bcolors.ENDC}") if is_done: @@ -138,6 +144,9 @@ def present_vehicles(self, console: tcod.Console): if x >= 0 and y >= 0: console.print(x, y, "V", (0, 255, 0), (128, 128, index)) + def state_shaping(self): + print(self._state_shaping.shape()) + def show_summary(self): pp = pprint.PrettyPrinter(indent=2, depth=8) @@ -259,11 +268,12 @@ def show_manufacture_states(self): def choose_action(self): action = {} - consumer_action = self.choose_consumer_action() + consumer_actions = self.choose_consumer_action() - action[consumer_action.id] = consumer_action + for consumer_action in consumer_actions: + action[consumer_action.id] = consumer_action - print(action) + print(consumer_actions) return action @@ -272,6 +282,8 @@ def choose_consumer_action(self): # push consumers to generate order + actions = [] + # check if lack of any source material storages = self.env.snapshot_list["storage"] @@ -285,9 +297,14 @@ def choose_consumer_action(self): for consumer in self.env.summary["node_mapping"]["detail"][facility_id]["units"]["consumers"]: if consumer["sku_id"] == product_id: - upstreams = self.env.snapshot_list["consumer"][self.env.frame_index:consumer["node_index"]:"sources"].flatten().astype(np.int) + sources = self.env.snapshot_list["consumer"][self.env.frame_index:consumer["node_index"]:"sources"] + + if sources is not None: + upstreams = sources.flatten().astype(np.int) + + actions.append(ConsumerAction(consumer["id"], upstreams[0], 30, 1)) - return ConsumerAction(consumer["id"], upstreams[0], 30, 1) + return actions def main(): diff --git a/examples/hello_world/supply_chain/state_shaping.py b/examples/hello_world/supply_chain/state_shaping.py new file mode 100644 index 000000000..ad130d2dd --- /dev/null +++ b/examples/hello_world/supply_chain/state_shaping.py @@ -0,0 +1,127 @@ + +""" +NOTE: this is used as a state shaping example, without maro.rl + +states we used: + +. is_positive_balance + + +is_over_stock +is_out_of_stock +is_below_rop +echelon_level + +sale_std +storage_capacity +storage_utilization +sale_hist +consumption_hist +total_backlog_demand +inventory_in_stock +inventory_in_distribution +inventory_in_transit +inventory_estimated +inventory_rop + +sku_price +sku_cost + +""" + +import numpy as np + +from maro.simulator import Env + + +# NOTE: copied from original code +class BalanceSheet: + profit: int = 0 + loss: int = 0 + + def __init__(self, profit:int, loss:int): + self.profit = profit + self.loss = loss + + def total(self) -> int: + return self.profit + self.loss + + def __add__(self, other): + return BalanceSheet(self.profit + other.profit, self.loss + other.loss) + + def __sub__(self, other): + return BalanceSheet(self.profit - other.profit, self.loss - other.loss) + + def __repr__(self): + return f"{round(self.profit + self.loss, 0)} ({round(self.profit, 0)} {round(self.loss, 0)})" + + def __radd__(self, other): + if other == 0: + return self + else: + return self.__add__(other) + + +class SupplyChainStateShaping: + def __init__(self, env: Env): + self._env = env + + def shape(self): + self.is_positive_balance() + + def is_positive_balance(self): + features = ("id", "facility_id", "balance_sheet_profit", "balance_sheet_loss") + + storage_nodes = self._env.snapshot_list["storage"] + storage_number = len(storage_nodes) + + storage_balance_sheet = storage_nodes[self._env.frame_index::features] + storage_balance_sheet = storage_balance_sheet.flatten().reshape(storage_number, -1).astype(np.int) + + distribution_nodes = self._env.snapshot_list["distribution"] + distribution_number = len(distribution_nodes) + + distribution_balance_sheet = distribution_nodes[self._env.frame_index::features] + distribution_balance_sheet = distribution_balance_sheet.flatten().reshape(distribution_number, -1).astype(np.int) + + transport_nodes = self._env.snapshot_list["transport"] + transport_number = len(transport_nodes) + + transport_balance_sheet = transport_nodes[self._env.frame_index::features] + transport_balance_sheet = transport_balance_sheet.flatten().reshape(transport_number, -1).astype(np.int) + + manufacture_nodes = self._env.snapshot_list["manufacture"] + manufacture_number = len(manufacture_nodes) + + manufacture_balance_sheet = manufacture_nodes[self._env.frame_index::features] + manufacture_balance_sheet = manufacture_balance_sheet.flatten().reshape(manufacture_number, -1).astype(np.int) + + seller_nodes = self._env.snapshot_list["seller"] + seller_number = len(seller_nodes) + + seller_balance_sheet = seller_nodes[self._env.frame_index::features] + seller_balance_sheet = seller_balance_sheet.flatten().reshape(seller_number, -1).astype(np.int) + + facility_nodes = self._env.snapshot_list["facility"] + facility_number = len(facility_nodes) + + facility_balance_sheet = facility_nodes[self._env.frame_index::features] + facility_balance_sheet = facility_balance_sheet.flatten().reshape(facility_number, -1).astype(np.int) + + print("storage balance sheet") + print(storage_balance_sheet) + + print("distribution balance sheet") + print(distribution_balance_sheet) + + print("transport balance sheet") + print(transport_balance_sheet) + + print("manufacture balance sheet") + print(manufacture_balance_sheet) + + print("seller balance sheet") + print(seller_balance_sheet) + + print("facility balance sheet") + print(facility_balance_sheet) diff --git a/maro/backends/raw_backend.pyx b/maro/backends/raw_backend.pyx index 947e4eba9..584fa0569 100644 --- a/maro/backends/raw_backend.pyx +++ b/maro/backends/raw_backend.pyx @@ -291,7 +291,7 @@ cdef class RawSnapshotList(SnapshotListAbc): cdef QUERY_FLOAT[:, :, :, :] result = view.array(shape=(shape.tick_number, shape.max_node_number, shape.attr_number, shape.max_slot_number), itemsize=sizeof(QUERY_FLOAT), format="f") # Default result value - result[:, :, :, :] = np.nan + result[:, :, :, :] = 0 # Do query self._snapshots.query(&result[0][0][0][0]) diff --git a/maro/simulator/scenarios/supply_chain/data/__init__.py b/maro/simulator/scenarios/supply_chain/data/__init__.py index 46e0c3ec8..64971dc37 100644 --- a/maro/simulator/scenarios/supply_chain/data/__init__.py +++ b/maro/simulator/scenarios/supply_chain/data/__init__.py @@ -4,3 +4,4 @@ from .manufacture import ManufactureDataModel from .consumer import ConsumerDataModel from .seller import SellerDataModel +from .facility import FacilityDataModel diff --git a/maro/simulator/scenarios/supply_chain/data/base.py b/maro/simulator/scenarios/supply_chain/data/base.py index 5d97d69ac..e6c55c9ee 100644 --- a/maro/simulator/scenarios/supply_chain/data/base.py +++ b/maro/simulator/scenarios/supply_chain/data/base.py @@ -12,6 +12,10 @@ class DataModelBase(NodeBase): # id of facility this unit belongs to facility_id = NodeAttribute(AttributeType.Int) + # per tick states + balance_sheet_profit = NodeAttribute(AttributeType.Float) + balance_sheet_loss = NodeAttribute(AttributeType.Float) + def __init__(self): self._unit_id = 0 self._facility_id = 0 diff --git a/maro/simulator/scenarios/supply_chain/data/manufacture.py b/maro/simulator/scenarios/supply_chain/data/manufacture.py index 6e68d0281..62b12a927 100644 --- a/maro/simulator/scenarios/supply_chain/data/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/data/manufacture.py @@ -27,12 +27,14 @@ def __init__(self): self._output_product_id = 0 self._production_rate = 0 self._storage_id = 0 + self._product_unit_cost = 0 def initialize(self, configs: dict): if configs is not None: self._output_product_id = configs["output_product_id"] self._production_rate = configs.get("production_rate", 1) self._storage_id = configs["storage_id"] + self.product_unit_cost = configs.get("product_unit_cost", 1) self.reset() diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py index da84f88b6..b63bcb96e 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -3,6 +3,9 @@ class FacilityBase(ABC): + # data model of this facility + data = None + # current world world = None diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index f3100168a..48b0fbd71 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -14,14 +14,23 @@ class RetailerFacility(FacilityBase): def step(self, tick: int): self.storage.step(tick) + self.data.balance_sheet_profit += self.storage.data.balance_sheet_profit + self.data.balance_sheet_loss += self.storage.data.balance_sheet_loss + if self.consumers is not None: for consumer in self.consumers.values(): consumer.step(tick) + self.data.balance_sheet_profit += consumer.data.balance_sheet_profit + self.data.balance_sheet_loss += consumer.data.balance_sheet_loss + if self.sellers is not None: for seller in self.sellers.values(): seller.step(tick) + self.data.balance_sheet_profit += seller.data.balance_sheet_profit + self.data.balance_sheet_loss += seller.data.balance_sheet_loss + def post_step(self, tick: int): self.storage.post_step(tick) @@ -33,6 +42,9 @@ def post_step(self, tick: int): for seller in self.sellers.values(): seller.post_step(tick) + self.data.balance_sheet_profit = 0 + self.data.balance_sheet_loss = 0 + def build(self, configs: dict): self.configs = configs @@ -82,6 +94,9 @@ def build(self, configs: dict): self.sellers[sku.id] = seller def initialize(self): + self.data.set_id(self.id, self.id) + self.data.initialize({}) + self._init_by_sku() self.storage.initialize(self.configs.get("storage", {})) diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 1cc1e9c23..0f4c435fa 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -4,7 +4,10 @@ class SupplierFacility(FacilityBase): - SkuInfo = namedtuple("SkuInfo", ("name", "id", "init_in_stock", "production_rate", "type", "cost", "price", "delay_order_penalty")) + SkuInfo = namedtuple( + "SkuInfo", + ("name", "id", "init_in_stock", "production_rate", "type", "cost", "price", "delay_order_penalty", "product_unit_cost") + ) storage = None distribution = None @@ -14,7 +17,9 @@ class SupplierFacility(FacilityBase): def step(self, tick: int): self.storage.step(tick) - self.distribution.step(tick) + + self.data.balance_sheet_profit += self.storage.data.balance_sheet_profit + self.data.balance_sheet_loss += self.storage.data.balance_sheet_loss for vehicle in self.transports: vehicle.step(tick) @@ -22,9 +27,52 @@ def step(self, tick: int): for supplier in self.manufactures.values(): supplier.step(tick) + self.data.balance_sheet_profit += supplier.data.balance_sheet_profit + self.data.balance_sheet_loss += supplier.data.balance_sheet_loss + for consumer in self.consumers.values(): consumer.step(tick) + self.data.balance_sheet_profit += consumer.data.balance_sheet_profit + self.data.balance_sheet_loss += consumer.data.balance_sheet_loss + + # to make sure the distribution get correct balance sheet, we should update transports first. + self.distribution.step(tick) + + self.data.balance_sheet_profit += self.distribution.data.balance_sheet_profit + self.data.balance_sheet_loss += self.distribution.data.balance_sheet_loss + + def post_step(self, tick: int): + self.storage.post_step(tick) + self.distribution.post_step(tick) + + for vehicle in self.transports: + vehicle.post_step(tick) + + for supplier in self.manufactures.values(): + supplier.post_step(tick) + + for consumer in self.consumers.values(): + consumer.post_step(tick) + + self.data.balance_sheet_profit = 0 + self.data.balance_sheet_loss = 0 + + def reset(self): + self._init_by_skus() + + self.storage.reset() + self.distribution.reset() + + for vehicle in self.transports: + vehicle.reset() + + for supplier in self.manufactures.values(): + supplier.reset() + + for consumer in self.consumers.values(): + consumer.reset() + def build(self, configs: dict): self.configs = configs @@ -74,7 +122,8 @@ def build(self, configs: dict): sku_config["type"], sku_config.get("cost", 0), sku_config.get("price", 0), - sku_config.get("delay_order_penalty", 0) + sku_config.get("delay_order_penalty", 0), + sku_config.get("product_unit_cost", 1) ) self.sku_information[sku.id] = sku_info @@ -101,6 +150,9 @@ def build(self, configs: dict): self.consumers[sku.id] = consumer def initialize(self): + self.data.set_id(self.id, self.id) + self.data.initialize({}) + # DO init by skus first, as other components may depend on sku information self._init_by_skus() @@ -113,7 +165,8 @@ def initialize(self): "data": { "production_rate": sku.production_rate, "output_product_id": sku.id, - "product_unit_cost": sku.cost + "product_unit_cost": sku.cost, + "product_unit_cost": sku.product_unit_cost } }) @@ -134,34 +187,6 @@ def initialize(self): for index, transport in enumerate(self.transports): transport.initialize(transports_conf[index]) - def post_step(self, tick: int): - self.storage.post_step(tick) - self.distribution.post_step(tick) - - for vehicle in self.transports: - vehicle.post_step(tick) - - for supplier in self.manufactures.values(): - supplier.post_step(tick) - - for consumer in self.consumers.values(): - consumer.post_step(tick) - - def reset(self): - self._init_by_skus() - - self.storage.reset() - self.distribution.reset() - - for vehicle in self.transports: - vehicle.reset() - - for supplier in self.manufactures.values(): - supplier.reset() - - for consumer in self.consumers.values(): - consumer.reset() - def get_node_info(self) -> dict: return { "id": self.id, diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index c4338c637..6199065ea 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -22,7 +22,9 @@ class WarehouseFacility(FacilityBase): def step(self, tick: int): self.storage.step(tick) - self.distribution.step(tick) + + self.data.balance_sheet_profit += self.storage.data.balance_sheet_profit + self.data.balance_sheet_loss += self.storage.data.balance_sheet_loss for transport in self.transports: transport.step(tick) @@ -30,6 +32,14 @@ def step(self, tick: int): for consumer in self.consumers.values(): consumer.step(tick) + self.data.balance_sheet_profit += consumer.data.balance_sheet_profit + self.data.balance_sheet_loss += consumer.data.balance_sheet_loss + + self.distribution.step(tick) + + self.data.balance_sheet_profit += self.distribution.data.balance_sheet_profit + self.data.balance_sheet_loss += self.distribution.data.balance_sheet_loss + def build(self, configs: dict): self.configs = configs @@ -88,6 +98,9 @@ def build(self, configs: dict): self.consumers[sku.id] = consumer def initialize(self): + self.data.set_id(self.id, self.id) + self.data.initialize({}) + # init components that related with sku number self._init_by_skus() @@ -124,6 +137,7 @@ def reset(self): def post_step(self, tick: int): self.storage.post_step(tick) + self.distribution.post_step(tick) for vehicle in self.transports: @@ -132,6 +146,9 @@ def post_step(self, tick: int): for consumer in self.consumers.values(): consumer.post_step(tick) + self.data.balance_sheet_profit = 0 + self.data.balance_sheet_loss = 0 + def get_node_info(self) -> dict: return { "id": self.id, diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index 433d6c1be..a890184a9 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -49,6 +49,7 @@ world: init_in_stock: 100 production_rate: 200 delay_order_penalty: 10 + product_unit_cost: 1 type: "production" # production means this is the output production of this facility cost": 10 price": 10 @@ -79,6 +80,7 @@ world: init_in_stock: 100 production_rate: 200 delay_order_penalty: 10 + product_unit_cost: 1 type: "production" cost: 10 price: 100 @@ -192,6 +194,7 @@ world: Supplier1: [ 0, 0 ] # (x, y) Supplier3: [ 3, 3 ] Warehouse1: [ 6, 6 ] + ChaoShiFa01: [10, 18] # cells that un-traversable blocks: # TODO: later we can have different cell have different travel cost diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py index 0b93e2ffa..9c4c0a4a2 100644 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -42,7 +42,10 @@ def step(self, tick: int): pass def post_step(self, tick: int): - pass + # clear the per tick attributes that all nodes have + if self.data is not None: + self.data.balance_sheet_profit = 0 + self.data.balance_sheet_loss = 0 def get_metrics(self): # called per step diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 326afcfa9..92dd38a7b 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -29,34 +29,42 @@ def step(self, tick: int): # different with original code, we split the control into pieces, # and put in to frame, so the product_id always has value # - quantity = self.data.consumer_quantity + data = self.data + quantity = data.consumer_quantity - if quantity <= 0 or len(self.data.sources) == 0: + if quantity <= 0 or len(data.sources) == 0: return - source_id = self.data.consumer_source_id - product_id = self.data.consumer_product_id + source_id = data.consumer_source_id + product_id = data.consumer_product_id - vlt = self.data.consumer_vlt + vlt = data.consumer_vlt # NOTE: # we are using facility as source id, not the storage self.update_open_orders(source_id, product_id, quantity) - sku = self.facility.sku_information[self.data.consumer_product_id] + sku = self.facility.sku_information[data.consumer_product_id] order = Order(self.facility, product_id, quantity, vlt) source_facility = self.world.get_facility_by_id(source_id) - self.data.order_product_cost = source_facility.distribution.place_order(order) + data.order_product_cost = source_facility.distribution.place_order(order) - self.data.total_purchased += quantity + data.total_purchased += quantity + + # update balance sheet + data.balance_sheet_loss = -(data.order_product_cost + data.order_cost) def post_step(self, tick: int): - self.data.received = 0 - self.data.purchased = 0 - self.data.order_product_cost = 0 + super(ConsumerUnit, self).post_step(tick) + + data = self.data + + data.received = 0 + data.purchased = 0 + data.order_product_cost = 0 def reset(self): super(ConsumerUnit, self).reset() @@ -69,9 +77,11 @@ def reset(self): def set_action(self, action): # called before step - self.data.consumer_source_id = action.source_id - self.data.consumer_quantity = action.quantity - self.data.consumer_vlt = action.vlt + data = self.data + + data.consumer_source_id = action.source_id + data.consumer_quantity = action.quantity + data.consumer_vlt = action.vlt def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): self.data.total_received += quantity diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 27ffcfa2a..fc0572900 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -27,6 +27,8 @@ def initialize(self, configs: dict): self.product_index_mapping[product_id] = index def step(self, tick: int): + data = self.data + for vehicle in self.facility.transports: # if we have vehicle not on the way and there is any pending order if len(self.order_queue) > 0 and vehicle.data.location == 0: @@ -47,7 +49,12 @@ def step(self, tick: int): sku = self.facility.sku_information[order.product_id] product_index = self.product_index_mapping[order.product_id] - self.data.delay_order_penalty[product_index] += sku.delay_order_penalty + data.delay_order_penalty[product_index] += sku.delay_order_penalty + + # update balance sheet, sum of transport balance sheet + if self.facility.transports is not None: + for vehicle in self.facility.transports: + data.balance_sheet_loss += vehicle.data.balance_sheet_loss def reset(self): super(DistributionUnit, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 01e95cfc6..511f4f9a4 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -37,15 +37,17 @@ def initialize(self, configs: dict): def step(self, tick: int): # try to produce production if we have positive rate - if self.data.production_rate > 0: + data = self.data + + if data.production_rate > 0: sku_num = len(self.facility.sku_information) unit_num_upper_bound = self.facility.storage.data.capacity // sku_num # one lot per time, until no enough space to hold output, or no enough source material # TODO: simplify this part to make it faster - for _ in range(self.data.production_rate): + for _ in range(data.production_rate): storage_remaining_space = self.facility.storage.data.remaining_space - current_product_number = self.facility.storage.get_product_number(self.data.output_product_id) + current_product_number = self.facility.storage.get_product_number(data.output_product_id) space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot # if remaining space enough to hold output production @@ -55,12 +57,16 @@ def step(self, tick: int): # if we do not need any material, then just generate the out product. # or if we have enough source materials if len(self.bom) == 0 or self.facility.storage.try_take_units(self.bom): - self.facility.storage.try_add_units({self.data.output_product_id: self.output_units_per_lot}) + self.facility.storage.try_add_units({data.output_product_id: self.output_units_per_lot}) # update manufacturing number in state - self.data.manufacturing_number += 1 + data.manufacturing_number += 1 + + data.balance_sheet_loss = data.manufacturing_number * data.product_unit_cost def post_step(self, tick: int): + super(ManufactureUnit, self).post_step(tick) + # reset the manufacture cost per tick self.data.manufacturing_number = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index 74852c35a..6e3ba55b0 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -15,17 +15,24 @@ def initialize(self, configs: dict): super(SellerUnit, self).initialize(configs) def step(self, tick: int): - product_id = self.data.product_id + data = self.data + + product_id = data.product_id sku = self.facility.sku_information[product_id] demand = self.market_demand() sold_qty = self.facility.storage.take_available(product_id, demand) - self.data.total_sold += sold_qty - self.data.sold = sold_qty - self.data.demand = demand + data.total_sold += sold_qty + data.sold = sold_qty + data.demand = demand + + data.balance_sheet_profit = data.unit_price * sold_qty + data.balance_sheet_loss = -(demand - sold_qty) * data.unit_price * data.backlog_ratio def post_step(self, tick: int): + super(SellerUnit, self).post_step(tick) + self.data.sold = 0 self.data.demand = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 0d3be9370..577a0a67d 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -19,6 +19,12 @@ def initialize(self, configs): for index, product_id in enumerate(self.data.product_list[:]): self.product_index_mapping[product_id] = index + def step(self, tick: int): + super(StorageUnit, self).step(tick) + + data = self.data + data.balance_sheet_loss = -(data.capacity - data.remaining_space) * data.unit_storage_cost + def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: if all_or_nothing and self.data.remaining_space < sum(product_quantities.values()): return {} diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 4544dea5b..9fc30cfe3 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -58,7 +58,7 @@ def try_loading(self, quantity: int): return True else: - self.patient -= 1 + self.data.patient -= 1 return False @@ -84,53 +84,57 @@ def try_unloading(self): self.data.patient = self.max_patient def step(self, tick: int): + data = self.data + # If we have not arrive at destination yet. - if self.data.steps > 0: + if data.steps > 0: # if we still not loaded enough productions yet. - if self.data.location == 0 and self.data.payload == 0: + if data.location == 0 and data.payload == 0: # then try to load by requested. - if self.try_loading(self.data.requested_quantity): + if self.try_loading(data.requested_quantity): # NOTE: here we return to simulate loading return else: - self.data.patient -= 1 + data.patient -= 1 # Failed to load, check the patient. - if self.patient < 0: - self.destination.consumer.update_open_orders( + if data.patient < 0: + self.destination.consumers[data.product_id].update_open_orders( self.facility.id, - self.data.product_id, - -self.requested_quantity + data.product_id, + -data.requested_quantity ) # reset - self.data.steps = 0 - self.data.location = 0 - self.data.destination = 0 - self.data.position[:] = -1 + data.steps = 0 + data.location = 0 + data.destination = 0 + data.position[:] = -1 # Moving to destination - if self.data.payload > 0: + if data.payload > 0: # Closer to destination until 0. - self.data.location += self.data.vlt - self.data.steps -= 1 + data.location += data.vlt + data.steps -= 1 - if self.data.location > len(self.path): - self.data.location = len(self.path) - 1 + if data.location > len(self.path): + data.location = len(self.path) - 1 - self.data.position[:] = self.path[self.data.location-1] + data.position[:] = self.path[data.location-1] else: # avoid update under idle state. - if self.data.location > 0: + if data.location > 0: # try to unload - if self.data.payload > 0: + if data.payload > 0: self.try_unloading() # back to source if we unload all - if self.data.payload == 0: + if data.payload == 0: self.destination = 0 - self.data.steps = 0 - self.data.location = 0 - self.data.destination = 0 - self.data.position[:] = -1 + data.steps = 0 + data.location = 0 + data.destination = 0 + data.position[:] = -1 + + data.balance_sheet_loss = -1 * data.payload * data.unit_transport_cost diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index f55736bde..10c4064da 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -9,6 +9,8 @@ from .config_parser import SupplyChainConfiguration +from .data import FacilityDataModel + # sku definition in world level # bom is a dictionary, key is the material sku id, value is units per lot @@ -133,6 +135,13 @@ def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: in number )) + # add facility data model to frame + data_class_in_frame.append(( + FacilityDataModel, + "facilities", + len(self.facilities), + )) + # . build the frame self.frame = build_frame(True, snapshot_number, data_class_in_frame) @@ -150,7 +159,12 @@ def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: in facility.upstreams[sku.id] = [self.get_facility_by_name(source_name).id for source_name in source_facilities] # then initialize all facilities as we have the data instance. + facility_node_index = 0 + for _, facility in self.facilities.items(): + facility.data = self.frame.facilities[facility_node_index] + facility_node_index += 1 + facility.initialize() # construct the map grid @@ -211,7 +225,7 @@ def get_sku_by_id(self, sku_id: int): return self._sku_collection[self._sku_id2name_mapping[sku_id]] def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int): - return self._path_finder.get_path(start_x, start_y, goal_x, goal_y) + return self._path_finder.get_path(int(start_x), int(start_y), int(goal_x), int(goal_y)) def get_node_mapping(self): facility_info_dict = {facility_id: facility.get_node_info() for facility_id, facility in self.facilities.items()} From d3acb67e4ff952f3d094fa5fb54e5a0df64be725 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 10 Mar 2021 13:55:09 +0800 Subject: [PATCH 048/482] rename attribute to unify the feature name --- .../scenarios/supply_chain/data/consumer.py | 10 ++++----- .../supply_chain/data/manufacture.py | 4 ++-- .../scenarios/supply_chain/units/consumer.py | 22 +++++++++---------- .../supply_chain/units/manufacture.py | 6 ++--- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/data/consumer.py b/maro/simulator/scenarios/supply_chain/data/consumer.py index 40d0444d6..3b1e6a05e 100644 --- a/maro/simulator/scenarios/supply_chain/data/consumer.py +++ b/maro/simulator/scenarios/supply_chain/data/consumer.py @@ -14,10 +14,10 @@ class ConsumerDataModel(DataModelBase): total_received = NodeAttribute(AttributeType.Int) # action states - consumer_product_id = NodeAttribute(AttributeType.Int) - consumer_source_id = NodeAttribute(AttributeType.Int) - consumer_quantity = NodeAttribute(AttributeType.Int) - consumer_vlt = NodeAttribute(AttributeType.Int) + product_id = NodeAttribute(AttributeType.Int) + source_id = NodeAttribute(AttributeType.Int) + quantity = NodeAttribute(AttributeType.Int) + vlt = NodeAttribute(AttributeType.Int) # id of upstream facilities. sources = NodeAttribute(AttributeType.Int, 1, is_list=True) @@ -46,4 +46,4 @@ def reset(self): super(ConsumerDataModel, self).reset() self.order_cost = self._order_cost - self.consumer_product_id = self._consumer_product_id + self.product_id = self._consumer_product_id diff --git a/maro/simulator/scenarios/supply_chain/data/manufacture.py b/maro/simulator/scenarios/supply_chain/data/manufacture.py index 62b12a927..15407c4d0 100644 --- a/maro/simulator/scenarios/supply_chain/data/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/data/manufacture.py @@ -17,7 +17,7 @@ class ManufactureDataModel(DataModelBase): manufacturing_number = NodeAttribute(AttributeType.Int) # what we will produce - output_product_id = NodeAttribute(AttributeType.Int) + product_id = NodeAttribute(AttributeType.Int) # original from config, then updated by action production_rate = NodeAttribute(AttributeType.Int) @@ -41,6 +41,6 @@ def initialize(self, configs: dict): def reset(self): super(ManufactureDataModel, self).reset() - self.output_product_id = self._output_product_id + self.product_id = self._output_product_id self.production_rate = self._production_rate self.storage_id = self._storage_id diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 92dd38a7b..47335c0c7 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -22,7 +22,7 @@ def initialize(self, configs: dict): if len(self.data.sources) > 0: # we use 1st source as default one - self.data.consumer_source_id = self.data.sources[0] + self.data.source_id = self.data.sources[0] def step(self, tick: int): # NOTE: @@ -30,21 +30,21 @@ def step(self, tick: int): # and put in to frame, so the product_id always has value # data = self.data - quantity = data.consumer_quantity + quantity = data.quantity if quantity <= 0 or len(data.sources) == 0: return - source_id = data.consumer_source_id - product_id = data.consumer_product_id + source_id = data.source_id + product_id = data.product_id - vlt = data.consumer_vlt + vlt = data.vlt # NOTE: # we are using facility as source id, not the storage self.update_open_orders(source_id, product_id, quantity) - sku = self.facility.sku_information[data.consumer_product_id] + sku = self.facility.sku_information[data.product_id] order = Order(self.facility, product_id, quantity, vlt) @@ -73,15 +73,15 @@ def reset(self): if len(self.data.sources) > 0: # we use 1st source as default one - self.data.consumer_source_id = self.data.sources[0] + self.data.source_id = self.data.sources[0] def set_action(self, action): # called before step data = self.data - data.consumer_source_id = action.source_id - data.consumer_quantity = action.quantity - data.consumer_vlt = action.vlt + data.source_id = action.source_id + data.quantity = action.quantity + data.vlt = action.vlt def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): self.data.total_received += quantity @@ -104,6 +104,6 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): def get_unit_info(self) -> dict: info = super(ConsumerUnit, self).get_unit_info() - info["sku_id"] = self.data.consumer_product_id + info["sku_id"] = self.data.product_id return info diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 511f4f9a4..7849d5fc8 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -28,7 +28,7 @@ def initialize(self, configs: dict): super().initialize(configs) # grab bom of current production - sku = self.world.get_sku_by_id(self.data.output_product_id) + sku = self.world.get_sku_by_id(self.data.product_id) self.bom = sku.bom self.output_units_per_lot = sku.output_units_per_lot @@ -47,7 +47,7 @@ def step(self, tick: int): # TODO: simplify this part to make it faster for _ in range(data.production_rate): storage_remaining_space = self.facility.storage.data.remaining_space - current_product_number = self.facility.storage.get_product_number(data.output_product_id) + current_product_number = self.facility.storage.get_product_number(data.product_id) space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot # if remaining space enough to hold output production @@ -57,7 +57,7 @@ def step(self, tick: int): # if we do not need any material, then just generate the out product. # or if we have enough source materials if len(self.bom) == 0 or self.facility.storage.try_take_units(self.bom): - self.facility.storage.try_add_units({data.output_product_id: self.output_units_per_lot}) + self.facility.storage.try_add_units({data.product_id: self.output_units_per_lot}) # update manufacturing number in state data.manufacturing_number += 1 From 20c2fe06cd034a3da5fa4312eef75314c09f81c4 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 10 Mar 2021 14:47:38 +0800 Subject: [PATCH 049/482] is positive balance --- .../hello_world/supply_chain/state_shaping.py | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/examples/hello_world/supply_chain/state_shaping.py b/examples/hello_world/supply_chain/state_shaping.py index ad130d2dd..831f0af19 100644 --- a/examples/hello_world/supply_chain/state_shaping.py +++ b/examples/hello_world/supply_chain/state_shaping.py @@ -67,61 +67,67 @@ def __init__(self, env: Env): self._env = env def shape(self): - self.is_positive_balance() + is_positive_balance = self._origin_group_by_sku_is_positive_balance() - def is_positive_balance(self): - features = ("id", "facility_id", "balance_sheet_profit", "balance_sheet_loss") + print(is_positive_balance) - storage_nodes = self._env.snapshot_list["storage"] - storage_number = len(storage_nodes) + def _origin_group_by_sku_is_positive_balance(self): + # original code collect states of facilities and sku related units (manufacture, seller, consumer), + # other's states is added to facility + result = [] - storage_balance_sheet = storage_nodes[self._env.frame_index::features] - storage_balance_sheet = storage_balance_sheet.flatten().reshape(storage_number, -1).astype(np.int) + filter = lambda x: 1 if x > 1 else 0 - distribution_nodes = self._env.snapshot_list["distribution"] - distribution_number = len(distribution_nodes) + cur_frame_index = self._env.frame_index - distribution_balance_sheet = distribution_nodes[self._env.frame_index::features] - distribution_balance_sheet = distribution_balance_sheet.flatten().reshape(distribution_number, -1).astype(np.int) + features = ("id", "facility_id", "balance_sheet_profit", "balance_sheet_loss") + sku_related_features = features + ("product_id",) - transport_nodes = self._env.snapshot_list["transport"] - transport_number = len(transport_nodes) + # facility balance sheet + facility_nodes = self._env.snapshot_list["facility"] + facility_balance_sheet = facility_nodes[cur_frame_index::features] + facility_balance_sheet = facility_balance_sheet.flatten().reshape(len(facility_nodes), -1).astype(np.int) - transport_balance_sheet = transport_nodes[self._env.frame_index::features] - transport_balance_sheet = transport_balance_sheet.flatten().reshape(transport_number, -1).astype(np.int) + facility_balance_sheet_total = facility_balance_sheet[:, 2] + facility_balance_sheet[:, 3] - manufacture_nodes = self._env.snapshot_list["manufacture"] - manufacture_number = len(manufacture_nodes) + facility_is_positive_balance = list(map(filter, facility_balance_sheet_total)) - manufacture_balance_sheet = manufacture_nodes[self._env.frame_index::features] - manufacture_balance_sheet = manufacture_balance_sheet.flatten().reshape(manufacture_number, -1).astype(np.int) + result.extend(facility_is_positive_balance) + # then each sku group for each facility + manufacture_nodes = self._env.snapshot_list["manufacture"] seller_nodes = self._env.snapshot_list["seller"] - seller_number = len(seller_nodes) + consumer_nodes = self._env.snapshot_list["consumer"] - seller_balance_sheet = seller_nodes[self._env.frame_index::features] - seller_balance_sheet = seller_balance_sheet.flatten().reshape(seller_number, -1).astype(np.int) + manufacture_balance_sheet = manufacture_nodes[cur_frame_index::sku_related_features].flatten().reshape(len(manufacture_nodes), -1).astype(np.int) + seller_balance_sheet = seller_nodes[cur_frame_index::sku_related_features].flatten().reshape(len(seller_nodes), -1).astype(np.int) + consumer_balance_sheet = consumer_nodes[cur_frame_index::sku_related_features].flatten().reshape(len(consumer_nodes), -1).astype(np.int) - facility_nodes = self._env.snapshot_list["facility"] - facility_number = len(facility_nodes) + # + for facility_id in facility_balance_sheet[:, 0]: + manufacture_states = manufacture_balance_sheet[manufacture_balance_sheet[:, 1] == facility_id] + seller_states = seller_balance_sheet[seller_balance_sheet[:, 1] == facility_id] + consumer_states = consumer_balance_sheet[consumer_balance_sheet[:, 1] == facility_id] + + length = max(len(manufacture_states), len(seller_states), len(consumer_states)) + + # this facility does not have any sku related units + if length == 0: + continue - facility_balance_sheet = facility_nodes[self._env.frame_index::features] - facility_balance_sheet = facility_balance_sheet.flatten().reshape(facility_number, -1).astype(np.int) + sku_balance_sheet_total = np.zeros((length, ), dtype=np.int) - print("storage balance sheet") - print(storage_balance_sheet) + if len(manufacture_states) > 0: + sku_balance_sheet_total += manufacture_states[:, 2] + manufacture_states[:, 3] - print("distribution balance sheet") - print(distribution_balance_sheet) + if len(seller_states) > 0: + sku_balance_sheet_total += seller_states[:, 2] + seller_states[:, 3] - print("transport balance sheet") - print(transport_balance_sheet) + if len(consumer_states) > 0: + sku_balance_sheet_total += consumer_states[:, 2] + consumer_states[:, 3] - print("manufacture balance sheet") - print(manufacture_balance_sheet) + sku_is_positive_balance = list(map(filter, sku_balance_sheet_total)) - print("seller balance sheet") - print(seller_balance_sheet) + result.extend(sku_is_positive_balance) - print("facility balance sheet") - print(facility_balance_sheet) + return result From 2a72cf89a56d5a305bc892d874153d3ab471fb39 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 11 Mar 2021 16:19:06 +0800 Subject: [PATCH 050/482] add perf test code and related config --- examples/hello_world/supply_chain/pf_test.py | 37 + .../perf_4x1000_facilities/config.yml | 12163 ++++++++++++++++ .../perf_4x400_facilities/config.yml | 4963 +++++++ .../topologies/sample1/config.yml | 4 +- .../supply_chain/topologies/sample1/gen.py | 31 + 5 files changed, 17196 insertions(+), 2 deletions(-) create mode 100644 examples/hello_world/supply_chain/pf_test.py create mode 100644 maro/simulator/scenarios/supply_chain/topologies/perf_4x1000_facilities/config.yml create mode 100644 maro/simulator/scenarios/supply_chain/topologies/perf_4x400_facilities/config.yml create mode 100644 maro/simulator/scenarios/supply_chain/topologies/sample1/gen.py diff --git a/examples/hello_world/supply_chain/pf_test.py b/examples/hello_world/supply_chain/pf_test.py new file mode 100644 index 000000000..defc3ec69 --- /dev/null +++ b/examples/hello_world/supply_chain/pf_test.py @@ -0,0 +1,37 @@ +import os, psutil + +import sys +import numpy as np +import tcod +import pprint + +from tabulate import tabulate +from maro.simulator import Env +from maro.simulator.scenarios.supply_chain import ConsumerAction +from timeit import timeit + + +def go(env: Env): + env.reset() + + is_done = False + + while not is_done: + _, _, is_done = env.step(None) + + +if __name__ == "__main__": + topology = sys.argv[1] + durations = sys.argv[2] if len(sys.argv) > 2 else 100 + + env = Env(scenario="supply_chain", topology=topology, durations=durations) + + print(f"config: f{topology}, steps: {durations}") + + print("avg time cost: ", timeit(lambda : go(env), number=2)) + + process = psutil.Process(os.getpid()) + + memory = process.memory_info().rss/1024/1024 + + print("memory cost: ", memory) \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/topologies/perf_4x1000_facilities/config.yml b/maro/simulator/scenarios/supply_chain/topologies/perf_4x1000_facilities/config.yml new file mode 100644 index 000000000..ad73e64d1 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/perf_4x1000_facilities/config.yml @@ -0,0 +1,12163 @@ +base: '' +world: + action_steps: 1 + facilities: + - class: SupplierFacility + configs: &id001 + distribution: + data: + unit_price: 10 + skus: + sku3: + cost": 10 + delay_order_penalty: 10 + init_in_stock: 100 + price": 10 + product_unit_cost: 1 + production_rate: 0 + type: production + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + transports: + - class: TransportUnit + data: + patient: 100 + - class: TransportUnit + data: + patient: 100 + name: Supplier3 + - class: SupplierFacility + configs: &id002 + distribution: + data: + unit_price: 10 + skus: + sku1: + cost: 10 + delay_order_penalty: 10 + init_in_stock: 100 + price: 100 + product_unit_cost: 1 + production_rate: 0 + type: production + sku3: + cost: 10 + delay_order_penalty: 10 + init_in_stock: 100 + price: 100 + production_rate: 200 + type: material + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + transports: + - class: TransportUnit + data: + patient: 100 + - class: TransportUnit + data: + patient: 100 + name: Supplier1 + - class: WarehouseFacility + configs: &id003 + distribution: + data: + unit_price: 10 + skus: + sku1: + delay_order_penalty: 10 + init_stock: 1000 + price: 100 + sku2: + delay_order_penalty: 10 + init_stock: 1000 + price: 100 + sku3: + delay_order_penalty: 10 + init_stock: 1000 + price: 100 + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + transports: + - class: TransportUnit + data: + patient: 100 + unit_transport_cost: 1 + - class: TransportUnit + data: + patient: 100 + unit_transport_cost: 1 + name: Warehouse1 + - class: RetailerFacility + configs: &id004 + skus: + sku1: + backlog_ratio: 0.1 + cost: 10 + init_in_stock: 100 + price: 300 + sale_gamma: 100 + sku2: + backlog_ratio: 0.1 + cost: 10 + init_in_stock: 100 + price: 100 + sale_gamma: 100 + sku3: + backlog_ratio: 0.1 + cost: 10 + init_in_stock: 100 + price: 200 + sale_gamma: 100 + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + name: ChaoShiFa01 + - class: SupplierFacility + configs: *id001 + name: Supplier3_0 + - class: SupplierFacility + configs: *id002 + name: Supplier1_0 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_0 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_0 + - class: SupplierFacility + configs: *id001 + name: Supplier3_1 + - class: SupplierFacility + configs: *id002 + name: Supplier1_1 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_1 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_1 + - class: SupplierFacility + configs: *id001 + name: Supplier3_2 + - class: SupplierFacility + configs: *id002 + name: Supplier1_2 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_2 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_2 + - class: SupplierFacility + configs: *id001 + name: Supplier3_3 + - class: SupplierFacility + configs: *id002 + name: Supplier1_3 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_3 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_3 + - class: SupplierFacility + configs: *id001 + name: Supplier3_4 + - class: SupplierFacility + configs: *id002 + name: Supplier1_4 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_4 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_4 + - class: SupplierFacility + configs: *id001 + name: Supplier3_5 + - class: SupplierFacility + configs: *id002 + name: Supplier1_5 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_5 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_5 + - class: SupplierFacility + configs: *id001 + name: Supplier3_6 + - class: SupplierFacility + configs: *id002 + name: Supplier1_6 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_6 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_6 + - class: SupplierFacility + configs: *id001 + name: Supplier3_7 + - class: SupplierFacility + configs: *id002 + name: Supplier1_7 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_7 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_7 + - class: SupplierFacility + configs: *id001 + name: Supplier3_8 + - class: SupplierFacility + configs: *id002 + name: Supplier1_8 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_8 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_8 + - class: SupplierFacility + configs: *id001 + name: Supplier3_9 + - class: SupplierFacility + configs: *id002 + name: Supplier1_9 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_9 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_9 + - class: SupplierFacility + configs: *id001 + name: Supplier3_10 + - class: SupplierFacility + configs: *id002 + name: Supplier1_10 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_10 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_10 + - class: SupplierFacility + configs: *id001 + name: Supplier3_11 + - class: SupplierFacility + configs: *id002 + name: Supplier1_11 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_11 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_11 + - class: SupplierFacility + configs: *id001 + name: Supplier3_12 + - class: SupplierFacility + configs: *id002 + name: Supplier1_12 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_12 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_12 + - class: SupplierFacility + configs: *id001 + name: Supplier3_13 + - class: SupplierFacility + configs: *id002 + name: Supplier1_13 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_13 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_13 + - class: SupplierFacility + configs: *id001 + name: Supplier3_14 + - class: SupplierFacility + configs: *id002 + name: Supplier1_14 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_14 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_14 + - class: SupplierFacility + configs: *id001 + name: Supplier3_15 + - class: SupplierFacility + configs: *id002 + name: Supplier1_15 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_15 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_15 + - class: SupplierFacility + configs: *id001 + name: Supplier3_16 + - class: SupplierFacility + configs: *id002 + name: Supplier1_16 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_16 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_16 + - class: SupplierFacility + configs: *id001 + name: Supplier3_17 + - class: SupplierFacility + configs: *id002 + name: Supplier1_17 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_17 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_17 + - class: SupplierFacility + configs: *id001 + name: Supplier3_18 + - class: SupplierFacility + configs: *id002 + name: Supplier1_18 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_18 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_18 + - class: SupplierFacility + configs: *id001 + name: Supplier3_19 + - class: SupplierFacility + configs: *id002 + name: Supplier1_19 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_19 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_19 + - class: SupplierFacility + configs: *id001 + name: Supplier3_20 + - class: SupplierFacility + configs: *id002 + name: Supplier1_20 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_20 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_20 + - class: SupplierFacility + configs: *id001 + name: Supplier3_21 + - class: SupplierFacility + configs: *id002 + name: Supplier1_21 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_21 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_21 + - class: SupplierFacility + configs: *id001 + name: Supplier3_22 + - class: SupplierFacility + configs: *id002 + name: Supplier1_22 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_22 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_22 + - class: SupplierFacility + configs: *id001 + name: Supplier3_23 + - class: SupplierFacility + configs: *id002 + name: Supplier1_23 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_23 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_23 + - class: SupplierFacility + configs: *id001 + name: Supplier3_24 + - class: SupplierFacility + configs: *id002 + name: Supplier1_24 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_24 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_24 + - class: SupplierFacility + configs: *id001 + name: Supplier3_25 + - class: SupplierFacility + configs: *id002 + name: Supplier1_25 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_25 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_25 + - class: SupplierFacility + configs: *id001 + name: Supplier3_26 + - class: SupplierFacility + configs: *id002 + name: Supplier1_26 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_26 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_26 + - class: SupplierFacility + configs: *id001 + name: Supplier3_27 + - class: SupplierFacility + configs: *id002 + name: Supplier1_27 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_27 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_27 + - class: SupplierFacility + configs: *id001 + name: Supplier3_28 + - class: SupplierFacility + configs: *id002 + name: Supplier1_28 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_28 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_28 + - class: SupplierFacility + configs: *id001 + name: Supplier3_29 + - class: SupplierFacility + configs: *id002 + name: Supplier1_29 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_29 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_29 + - class: SupplierFacility + configs: *id001 + name: Supplier3_30 + - class: SupplierFacility + configs: *id002 + name: Supplier1_30 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_30 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_30 + - class: SupplierFacility + configs: *id001 + name: Supplier3_31 + - class: SupplierFacility + configs: *id002 + name: Supplier1_31 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_31 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_31 + - class: SupplierFacility + configs: *id001 + name: Supplier3_32 + - class: SupplierFacility + configs: *id002 + name: Supplier1_32 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_32 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_32 + - class: SupplierFacility + configs: *id001 + name: Supplier3_33 + - class: SupplierFacility + configs: *id002 + name: Supplier1_33 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_33 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_33 + - class: SupplierFacility + configs: *id001 + name: Supplier3_34 + - class: SupplierFacility + configs: *id002 + name: Supplier1_34 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_34 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_34 + - class: SupplierFacility + configs: *id001 + name: Supplier3_35 + - class: SupplierFacility + configs: *id002 + name: Supplier1_35 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_35 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_35 + - class: SupplierFacility + configs: *id001 + name: Supplier3_36 + - class: SupplierFacility + configs: *id002 + name: Supplier1_36 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_36 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_36 + - class: SupplierFacility + configs: *id001 + name: Supplier3_37 + - class: SupplierFacility + configs: *id002 + name: Supplier1_37 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_37 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_37 + - class: SupplierFacility + configs: *id001 + name: Supplier3_38 + - class: SupplierFacility + configs: *id002 + name: Supplier1_38 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_38 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_38 + - class: SupplierFacility + configs: *id001 + name: Supplier3_39 + - class: SupplierFacility + configs: *id002 + name: Supplier1_39 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_39 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_39 + - class: SupplierFacility + configs: *id001 + name: Supplier3_40 + - class: SupplierFacility + configs: *id002 + name: Supplier1_40 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_40 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_40 + - class: SupplierFacility + configs: *id001 + name: Supplier3_41 + - class: SupplierFacility + configs: *id002 + name: Supplier1_41 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_41 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_41 + - class: SupplierFacility + configs: *id001 + name: Supplier3_42 + - class: SupplierFacility + configs: *id002 + name: Supplier1_42 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_42 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_42 + - class: SupplierFacility + configs: *id001 + name: Supplier3_43 + - class: SupplierFacility + configs: *id002 + name: Supplier1_43 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_43 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_43 + - class: SupplierFacility + configs: *id001 + name: Supplier3_44 + - class: SupplierFacility + configs: *id002 + name: Supplier1_44 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_44 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_44 + - class: SupplierFacility + configs: *id001 + name: Supplier3_45 + - class: SupplierFacility + configs: *id002 + name: Supplier1_45 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_45 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_45 + - class: SupplierFacility + configs: *id001 + name: Supplier3_46 + - class: SupplierFacility + configs: *id002 + name: Supplier1_46 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_46 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_46 + - class: SupplierFacility + configs: *id001 + name: Supplier3_47 + - class: SupplierFacility + configs: *id002 + name: Supplier1_47 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_47 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_47 + - class: SupplierFacility + configs: *id001 + name: Supplier3_48 + - class: SupplierFacility + configs: *id002 + name: Supplier1_48 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_48 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_48 + - class: SupplierFacility + configs: *id001 + name: Supplier3_49 + - class: SupplierFacility + configs: *id002 + name: Supplier1_49 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_49 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_49 + - class: SupplierFacility + configs: *id001 + name: Supplier3_50 + - class: SupplierFacility + configs: *id002 + name: Supplier1_50 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_50 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_50 + - class: SupplierFacility + configs: *id001 + name: Supplier3_51 + - class: SupplierFacility + configs: *id002 + name: Supplier1_51 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_51 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_51 + - class: SupplierFacility + configs: *id001 + name: Supplier3_52 + - class: SupplierFacility + configs: *id002 + name: Supplier1_52 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_52 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_52 + - class: SupplierFacility + configs: *id001 + name: Supplier3_53 + - class: SupplierFacility + configs: *id002 + name: Supplier1_53 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_53 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_53 + - class: SupplierFacility + configs: *id001 + name: Supplier3_54 + - class: SupplierFacility + configs: *id002 + name: Supplier1_54 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_54 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_54 + - class: SupplierFacility + configs: *id001 + name: Supplier3_55 + - class: SupplierFacility + configs: *id002 + name: Supplier1_55 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_55 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_55 + - class: SupplierFacility + configs: *id001 + name: Supplier3_56 + - class: SupplierFacility + configs: *id002 + name: Supplier1_56 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_56 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_56 + - class: SupplierFacility + configs: *id001 + name: Supplier3_57 + - class: SupplierFacility + configs: *id002 + name: Supplier1_57 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_57 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_57 + - class: SupplierFacility + configs: *id001 + name: Supplier3_58 + - class: SupplierFacility + configs: *id002 + name: Supplier1_58 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_58 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_58 + - class: SupplierFacility + configs: *id001 + name: Supplier3_59 + - class: SupplierFacility + configs: *id002 + name: Supplier1_59 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_59 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_59 + - class: SupplierFacility + configs: *id001 + name: Supplier3_60 + - class: SupplierFacility + configs: *id002 + name: Supplier1_60 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_60 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_60 + - class: SupplierFacility + configs: *id001 + name: Supplier3_61 + - class: SupplierFacility + configs: *id002 + name: Supplier1_61 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_61 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_61 + - class: SupplierFacility + configs: *id001 + name: Supplier3_62 + - class: SupplierFacility + configs: *id002 + name: Supplier1_62 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_62 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_62 + - class: SupplierFacility + configs: *id001 + name: Supplier3_63 + - class: SupplierFacility + configs: *id002 + name: Supplier1_63 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_63 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_63 + - class: SupplierFacility + configs: *id001 + name: Supplier3_64 + - class: SupplierFacility + configs: *id002 + name: Supplier1_64 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_64 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_64 + - class: SupplierFacility + configs: *id001 + name: Supplier3_65 + - class: SupplierFacility + configs: *id002 + name: Supplier1_65 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_65 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_65 + - class: SupplierFacility + configs: *id001 + name: Supplier3_66 + - class: SupplierFacility + configs: *id002 + name: Supplier1_66 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_66 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_66 + - class: SupplierFacility + configs: *id001 + name: Supplier3_67 + - class: SupplierFacility + configs: *id002 + name: Supplier1_67 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_67 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_67 + - class: SupplierFacility + configs: *id001 + name: Supplier3_68 + - class: SupplierFacility + configs: *id002 + name: Supplier1_68 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_68 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_68 + - class: SupplierFacility + configs: *id001 + name: Supplier3_69 + - class: SupplierFacility + configs: *id002 + name: Supplier1_69 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_69 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_69 + - class: SupplierFacility + configs: *id001 + name: Supplier3_70 + - class: SupplierFacility + configs: *id002 + name: Supplier1_70 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_70 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_70 + - class: SupplierFacility + configs: *id001 + name: Supplier3_71 + - class: SupplierFacility + configs: *id002 + name: Supplier1_71 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_71 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_71 + - class: SupplierFacility + configs: *id001 + name: Supplier3_72 + - class: SupplierFacility + configs: *id002 + name: Supplier1_72 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_72 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_72 + - class: SupplierFacility + configs: *id001 + name: Supplier3_73 + - class: SupplierFacility + configs: *id002 + name: Supplier1_73 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_73 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_73 + - class: SupplierFacility + configs: *id001 + name: Supplier3_74 + - class: SupplierFacility + configs: *id002 + name: Supplier1_74 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_74 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_74 + - class: SupplierFacility + configs: *id001 + name: Supplier3_75 + - class: SupplierFacility + configs: *id002 + name: Supplier1_75 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_75 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_75 + - class: SupplierFacility + configs: *id001 + name: Supplier3_76 + - class: SupplierFacility + configs: *id002 + name: Supplier1_76 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_76 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_76 + - class: SupplierFacility + configs: *id001 + name: Supplier3_77 + - class: SupplierFacility + configs: *id002 + name: Supplier1_77 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_77 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_77 + - class: SupplierFacility + configs: *id001 + name: Supplier3_78 + - class: SupplierFacility + configs: *id002 + name: Supplier1_78 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_78 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_78 + - class: SupplierFacility + configs: *id001 + name: Supplier3_79 + - class: SupplierFacility + configs: *id002 + name: Supplier1_79 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_79 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_79 + - class: SupplierFacility + configs: *id001 + name: Supplier3_80 + - class: SupplierFacility + configs: *id002 + name: Supplier1_80 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_80 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_80 + - class: SupplierFacility + configs: *id001 + name: Supplier3_81 + - class: SupplierFacility + configs: *id002 + name: Supplier1_81 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_81 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_81 + - class: SupplierFacility + configs: *id001 + name: Supplier3_82 + - class: SupplierFacility + configs: *id002 + name: Supplier1_82 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_82 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_82 + - class: SupplierFacility + configs: *id001 + name: Supplier3_83 + - class: SupplierFacility + configs: *id002 + name: Supplier1_83 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_83 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_83 + - class: SupplierFacility + configs: *id001 + name: Supplier3_84 + - class: SupplierFacility + configs: *id002 + name: Supplier1_84 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_84 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_84 + - class: SupplierFacility + configs: *id001 + name: Supplier3_85 + - class: SupplierFacility + configs: *id002 + name: Supplier1_85 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_85 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_85 + - class: SupplierFacility + configs: *id001 + name: Supplier3_86 + - class: SupplierFacility + configs: *id002 + name: Supplier1_86 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_86 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_86 + - class: SupplierFacility + configs: *id001 + name: Supplier3_87 + - class: SupplierFacility + configs: *id002 + name: Supplier1_87 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_87 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_87 + - class: SupplierFacility + configs: *id001 + name: Supplier3_88 + - class: SupplierFacility + configs: *id002 + name: Supplier1_88 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_88 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_88 + - class: SupplierFacility + configs: *id001 + name: Supplier3_89 + - class: SupplierFacility + configs: *id002 + name: Supplier1_89 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_89 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_89 + - class: SupplierFacility + configs: *id001 + name: Supplier3_90 + - class: SupplierFacility + configs: *id002 + name: Supplier1_90 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_90 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_90 + - class: SupplierFacility + configs: *id001 + name: Supplier3_91 + - class: SupplierFacility + configs: *id002 + name: Supplier1_91 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_91 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_91 + - class: SupplierFacility + configs: *id001 + name: Supplier3_92 + - class: SupplierFacility + configs: *id002 + name: Supplier1_92 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_92 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_92 + - class: SupplierFacility + configs: *id001 + name: Supplier3_93 + - class: SupplierFacility + configs: *id002 + name: Supplier1_93 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_93 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_93 + - class: SupplierFacility + configs: *id001 + name: Supplier3_94 + - class: SupplierFacility + configs: *id002 + name: Supplier1_94 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_94 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_94 + - class: SupplierFacility + configs: *id001 + name: Supplier3_95 + - class: SupplierFacility + configs: *id002 + name: Supplier1_95 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_95 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_95 + - class: SupplierFacility + configs: *id001 + name: Supplier3_96 + - class: SupplierFacility + configs: *id002 + name: Supplier1_96 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_96 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_96 + - class: SupplierFacility + configs: *id001 + name: Supplier3_97 + - class: SupplierFacility + configs: *id002 + name: Supplier1_97 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_97 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_97 + - class: SupplierFacility + configs: *id001 + name: Supplier3_98 + - class: SupplierFacility + configs: *id002 + name: Supplier1_98 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_98 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_98 + - class: SupplierFacility + configs: *id001 + name: Supplier3_99 + - class: SupplierFacility + configs: *id002 + name: Supplier1_99 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_99 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_99 + - class: SupplierFacility + configs: *id001 + name: Supplier3_100 + - class: SupplierFacility + configs: *id002 + name: Supplier1_100 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_100 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_100 + - class: SupplierFacility + configs: *id001 + name: Supplier3_101 + - class: SupplierFacility + configs: *id002 + name: Supplier1_101 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_101 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_101 + - class: SupplierFacility + configs: *id001 + name: Supplier3_102 + - class: SupplierFacility + configs: *id002 + name: Supplier1_102 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_102 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_102 + - class: SupplierFacility + configs: *id001 + name: Supplier3_103 + - class: SupplierFacility + configs: *id002 + name: Supplier1_103 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_103 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_103 + - class: SupplierFacility + configs: *id001 + name: Supplier3_104 + - class: SupplierFacility + configs: *id002 + name: Supplier1_104 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_104 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_104 + - class: SupplierFacility + configs: *id001 + name: Supplier3_105 + - class: SupplierFacility + configs: *id002 + name: Supplier1_105 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_105 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_105 + - class: SupplierFacility + configs: *id001 + name: Supplier3_106 + - class: SupplierFacility + configs: *id002 + name: Supplier1_106 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_106 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_106 + - class: SupplierFacility + configs: *id001 + name: Supplier3_107 + - class: SupplierFacility + configs: *id002 + name: Supplier1_107 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_107 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_107 + - class: SupplierFacility + configs: *id001 + name: Supplier3_108 + - class: SupplierFacility + configs: *id002 + name: Supplier1_108 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_108 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_108 + - class: SupplierFacility + configs: *id001 + name: Supplier3_109 + - class: SupplierFacility + configs: *id002 + name: Supplier1_109 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_109 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_109 + - class: SupplierFacility + configs: *id001 + name: Supplier3_110 + - class: SupplierFacility + configs: *id002 + name: Supplier1_110 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_110 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_110 + - class: SupplierFacility + configs: *id001 + name: Supplier3_111 + - class: SupplierFacility + configs: *id002 + name: Supplier1_111 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_111 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_111 + - class: SupplierFacility + configs: *id001 + name: Supplier3_112 + - class: SupplierFacility + configs: *id002 + name: Supplier1_112 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_112 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_112 + - class: SupplierFacility + configs: *id001 + name: Supplier3_113 + - class: SupplierFacility + configs: *id002 + name: Supplier1_113 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_113 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_113 + - class: SupplierFacility + configs: *id001 + name: Supplier3_114 + - class: SupplierFacility + configs: *id002 + name: Supplier1_114 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_114 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_114 + - class: SupplierFacility + configs: *id001 + name: Supplier3_115 + - class: SupplierFacility + configs: *id002 + name: Supplier1_115 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_115 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_115 + - class: SupplierFacility + configs: *id001 + name: Supplier3_116 + - class: SupplierFacility + configs: *id002 + name: Supplier1_116 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_116 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_116 + - class: SupplierFacility + configs: *id001 + name: Supplier3_117 + - class: SupplierFacility + configs: *id002 + name: Supplier1_117 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_117 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_117 + - class: SupplierFacility + configs: *id001 + name: Supplier3_118 + - class: SupplierFacility + configs: *id002 + name: Supplier1_118 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_118 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_118 + - class: SupplierFacility + configs: *id001 + name: Supplier3_119 + - class: SupplierFacility + configs: *id002 + name: Supplier1_119 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_119 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_119 + - class: SupplierFacility + configs: *id001 + name: Supplier3_120 + - class: SupplierFacility + configs: *id002 + name: Supplier1_120 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_120 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_120 + - class: SupplierFacility + configs: *id001 + name: Supplier3_121 + - class: SupplierFacility + configs: *id002 + name: Supplier1_121 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_121 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_121 + - class: SupplierFacility + configs: *id001 + name: Supplier3_122 + - class: SupplierFacility + configs: *id002 + name: Supplier1_122 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_122 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_122 + - class: SupplierFacility + configs: *id001 + name: Supplier3_123 + - class: SupplierFacility + configs: *id002 + name: Supplier1_123 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_123 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_123 + - class: SupplierFacility + configs: *id001 + name: Supplier3_124 + - class: SupplierFacility + configs: *id002 + name: Supplier1_124 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_124 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_124 + - class: SupplierFacility + configs: *id001 + name: Supplier3_125 + - class: SupplierFacility + configs: *id002 + name: Supplier1_125 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_125 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_125 + - class: SupplierFacility + configs: *id001 + name: Supplier3_126 + - class: SupplierFacility + configs: *id002 + name: Supplier1_126 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_126 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_126 + - class: SupplierFacility + configs: *id001 + name: Supplier3_127 + - class: SupplierFacility + configs: *id002 + name: Supplier1_127 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_127 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_127 + - class: SupplierFacility + configs: *id001 + name: Supplier3_128 + - class: SupplierFacility + configs: *id002 + name: Supplier1_128 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_128 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_128 + - class: SupplierFacility + configs: *id001 + name: Supplier3_129 + - class: SupplierFacility + configs: *id002 + name: Supplier1_129 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_129 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_129 + - class: SupplierFacility + configs: *id001 + name: Supplier3_130 + - class: SupplierFacility + configs: *id002 + name: Supplier1_130 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_130 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_130 + - class: SupplierFacility + configs: *id001 + name: Supplier3_131 + - class: SupplierFacility + configs: *id002 + name: Supplier1_131 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_131 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_131 + - class: SupplierFacility + configs: *id001 + name: Supplier3_132 + - class: SupplierFacility + configs: *id002 + name: Supplier1_132 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_132 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_132 + - class: SupplierFacility + configs: *id001 + name: Supplier3_133 + - class: SupplierFacility + configs: *id002 + name: Supplier1_133 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_133 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_133 + - class: SupplierFacility + configs: *id001 + name: Supplier3_134 + - class: SupplierFacility + configs: *id002 + name: Supplier1_134 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_134 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_134 + - class: SupplierFacility + configs: *id001 + name: Supplier3_135 + - class: SupplierFacility + configs: *id002 + name: Supplier1_135 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_135 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_135 + - class: SupplierFacility + configs: *id001 + name: Supplier3_136 + - class: SupplierFacility + configs: *id002 + name: Supplier1_136 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_136 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_136 + - class: SupplierFacility + configs: *id001 + name: Supplier3_137 + - class: SupplierFacility + configs: *id002 + name: Supplier1_137 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_137 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_137 + - class: SupplierFacility + configs: *id001 + name: Supplier3_138 + - class: SupplierFacility + configs: *id002 + name: Supplier1_138 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_138 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_138 + - class: SupplierFacility + configs: *id001 + name: Supplier3_139 + - class: SupplierFacility + configs: *id002 + name: Supplier1_139 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_139 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_139 + - class: SupplierFacility + configs: *id001 + name: Supplier3_140 + - class: SupplierFacility + configs: *id002 + name: Supplier1_140 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_140 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_140 + - class: SupplierFacility + configs: *id001 + name: Supplier3_141 + - class: SupplierFacility + configs: *id002 + name: Supplier1_141 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_141 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_141 + - class: SupplierFacility + configs: *id001 + name: Supplier3_142 + - class: SupplierFacility + configs: *id002 + name: Supplier1_142 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_142 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_142 + - class: SupplierFacility + configs: *id001 + name: Supplier3_143 + - class: SupplierFacility + configs: *id002 + name: Supplier1_143 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_143 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_143 + - class: SupplierFacility + configs: *id001 + name: Supplier3_144 + - class: SupplierFacility + configs: *id002 + name: Supplier1_144 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_144 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_144 + - class: SupplierFacility + configs: *id001 + name: Supplier3_145 + - class: SupplierFacility + configs: *id002 + name: Supplier1_145 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_145 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_145 + - class: SupplierFacility + configs: *id001 + name: Supplier3_146 + - class: SupplierFacility + configs: *id002 + name: Supplier1_146 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_146 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_146 + - class: SupplierFacility + configs: *id001 + name: Supplier3_147 + - class: SupplierFacility + configs: *id002 + name: Supplier1_147 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_147 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_147 + - class: SupplierFacility + configs: *id001 + name: Supplier3_148 + - class: SupplierFacility + configs: *id002 + name: Supplier1_148 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_148 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_148 + - class: SupplierFacility + configs: *id001 + name: Supplier3_149 + - class: SupplierFacility + configs: *id002 + name: Supplier1_149 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_149 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_149 + - class: SupplierFacility + configs: *id001 + name: Supplier3_150 + - class: SupplierFacility + configs: *id002 + name: Supplier1_150 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_150 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_150 + - class: SupplierFacility + configs: *id001 + name: Supplier3_151 + - class: SupplierFacility + configs: *id002 + name: Supplier1_151 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_151 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_151 + - class: SupplierFacility + configs: *id001 + name: Supplier3_152 + - class: SupplierFacility + configs: *id002 + name: Supplier1_152 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_152 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_152 + - class: SupplierFacility + configs: *id001 + name: Supplier3_153 + - class: SupplierFacility + configs: *id002 + name: Supplier1_153 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_153 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_153 + - class: SupplierFacility + configs: *id001 + name: Supplier3_154 + - class: SupplierFacility + configs: *id002 + name: Supplier1_154 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_154 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_154 + - class: SupplierFacility + configs: *id001 + name: Supplier3_155 + - class: SupplierFacility + configs: *id002 + name: Supplier1_155 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_155 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_155 + - class: SupplierFacility + configs: *id001 + name: Supplier3_156 + - class: SupplierFacility + configs: *id002 + name: Supplier1_156 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_156 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_156 + - class: SupplierFacility + configs: *id001 + name: Supplier3_157 + - class: SupplierFacility + configs: *id002 + name: Supplier1_157 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_157 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_157 + - class: SupplierFacility + configs: *id001 + name: Supplier3_158 + - class: SupplierFacility + configs: *id002 + name: Supplier1_158 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_158 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_158 + - class: SupplierFacility + configs: *id001 + name: Supplier3_159 + - class: SupplierFacility + configs: *id002 + name: Supplier1_159 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_159 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_159 + - class: SupplierFacility + configs: *id001 + name: Supplier3_160 + - class: SupplierFacility + configs: *id002 + name: Supplier1_160 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_160 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_160 + - class: SupplierFacility + configs: *id001 + name: Supplier3_161 + - class: SupplierFacility + configs: *id002 + name: Supplier1_161 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_161 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_161 + - class: SupplierFacility + configs: *id001 + name: Supplier3_162 + - class: SupplierFacility + configs: *id002 + name: Supplier1_162 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_162 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_162 + - class: SupplierFacility + configs: *id001 + name: Supplier3_163 + - class: SupplierFacility + configs: *id002 + name: Supplier1_163 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_163 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_163 + - class: SupplierFacility + configs: *id001 + name: Supplier3_164 + - class: SupplierFacility + configs: *id002 + name: Supplier1_164 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_164 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_164 + - class: SupplierFacility + configs: *id001 + name: Supplier3_165 + - class: SupplierFacility + configs: *id002 + name: Supplier1_165 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_165 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_165 + - class: SupplierFacility + configs: *id001 + name: Supplier3_166 + - class: SupplierFacility + configs: *id002 + name: Supplier1_166 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_166 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_166 + - class: SupplierFacility + configs: *id001 + name: Supplier3_167 + - class: SupplierFacility + configs: *id002 + name: Supplier1_167 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_167 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_167 + - class: SupplierFacility + configs: *id001 + name: Supplier3_168 + - class: SupplierFacility + configs: *id002 + name: Supplier1_168 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_168 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_168 + - class: SupplierFacility + configs: *id001 + name: Supplier3_169 + - class: SupplierFacility + configs: *id002 + name: Supplier1_169 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_169 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_169 + - class: SupplierFacility + configs: *id001 + name: Supplier3_170 + - class: SupplierFacility + configs: *id002 + name: Supplier1_170 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_170 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_170 + - class: SupplierFacility + configs: *id001 + name: Supplier3_171 + - class: SupplierFacility + configs: *id002 + name: Supplier1_171 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_171 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_171 + - class: SupplierFacility + configs: *id001 + name: Supplier3_172 + - class: SupplierFacility + configs: *id002 + name: Supplier1_172 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_172 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_172 + - class: SupplierFacility + configs: *id001 + name: Supplier3_173 + - class: SupplierFacility + configs: *id002 + name: Supplier1_173 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_173 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_173 + - class: SupplierFacility + configs: *id001 + name: Supplier3_174 + - class: SupplierFacility + configs: *id002 + name: Supplier1_174 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_174 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_174 + - class: SupplierFacility + configs: *id001 + name: Supplier3_175 + - class: SupplierFacility + configs: *id002 + name: Supplier1_175 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_175 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_175 + - class: SupplierFacility + configs: *id001 + name: Supplier3_176 + - class: SupplierFacility + configs: *id002 + name: Supplier1_176 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_176 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_176 + - class: SupplierFacility + configs: *id001 + name: Supplier3_177 + - class: SupplierFacility + configs: *id002 + name: Supplier1_177 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_177 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_177 + - class: SupplierFacility + configs: *id001 + name: Supplier3_178 + - class: SupplierFacility + configs: *id002 + name: Supplier1_178 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_178 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_178 + - class: SupplierFacility + configs: *id001 + name: Supplier3_179 + - class: SupplierFacility + configs: *id002 + name: Supplier1_179 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_179 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_179 + - class: SupplierFacility + configs: *id001 + name: Supplier3_180 + - class: SupplierFacility + configs: *id002 + name: Supplier1_180 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_180 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_180 + - class: SupplierFacility + configs: *id001 + name: Supplier3_181 + - class: SupplierFacility + configs: *id002 + name: Supplier1_181 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_181 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_181 + - class: SupplierFacility + configs: *id001 + name: Supplier3_182 + - class: SupplierFacility + configs: *id002 + name: Supplier1_182 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_182 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_182 + - class: SupplierFacility + configs: *id001 + name: Supplier3_183 + - class: SupplierFacility + configs: *id002 + name: Supplier1_183 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_183 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_183 + - class: SupplierFacility + configs: *id001 + name: Supplier3_184 + - class: SupplierFacility + configs: *id002 + name: Supplier1_184 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_184 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_184 + - class: SupplierFacility + configs: *id001 + name: Supplier3_185 + - class: SupplierFacility + configs: *id002 + name: Supplier1_185 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_185 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_185 + - class: SupplierFacility + configs: *id001 + name: Supplier3_186 + - class: SupplierFacility + configs: *id002 + name: Supplier1_186 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_186 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_186 + - class: SupplierFacility + configs: *id001 + name: Supplier3_187 + - class: SupplierFacility + configs: *id002 + name: Supplier1_187 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_187 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_187 + - class: SupplierFacility + configs: *id001 + name: Supplier3_188 + - class: SupplierFacility + configs: *id002 + name: Supplier1_188 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_188 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_188 + - class: SupplierFacility + configs: *id001 + name: Supplier3_189 + - class: SupplierFacility + configs: *id002 + name: Supplier1_189 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_189 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_189 + - class: SupplierFacility + configs: *id001 + name: Supplier3_190 + - class: SupplierFacility + configs: *id002 + name: Supplier1_190 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_190 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_190 + - class: SupplierFacility + configs: *id001 + name: Supplier3_191 + - class: SupplierFacility + configs: *id002 + name: Supplier1_191 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_191 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_191 + - class: SupplierFacility + configs: *id001 + name: Supplier3_192 + - class: SupplierFacility + configs: *id002 + name: Supplier1_192 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_192 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_192 + - class: SupplierFacility + configs: *id001 + name: Supplier3_193 + - class: SupplierFacility + configs: *id002 + name: Supplier1_193 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_193 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_193 + - class: SupplierFacility + configs: *id001 + name: Supplier3_194 + - class: SupplierFacility + configs: *id002 + name: Supplier1_194 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_194 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_194 + - class: SupplierFacility + configs: *id001 + name: Supplier3_195 + - class: SupplierFacility + configs: *id002 + name: Supplier1_195 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_195 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_195 + - class: SupplierFacility + configs: *id001 + name: Supplier3_196 + - class: SupplierFacility + configs: *id002 + name: Supplier1_196 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_196 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_196 + - class: SupplierFacility + configs: *id001 + name: Supplier3_197 + - class: SupplierFacility + configs: *id002 + name: Supplier1_197 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_197 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_197 + - class: SupplierFacility + configs: *id001 + name: Supplier3_198 + - class: SupplierFacility + configs: *id002 + name: Supplier1_198 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_198 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_198 + - class: SupplierFacility + configs: *id001 + name: Supplier3_199 + - class: SupplierFacility + configs: *id002 + name: Supplier1_199 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_199 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_199 + - class: SupplierFacility + configs: *id001 + name: Supplier3_200 + - class: SupplierFacility + configs: *id002 + name: Supplier1_200 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_200 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_200 + - class: SupplierFacility + configs: *id001 + name: Supplier3_201 + - class: SupplierFacility + configs: *id002 + name: Supplier1_201 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_201 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_201 + - class: SupplierFacility + configs: *id001 + name: Supplier3_202 + - class: SupplierFacility + configs: *id002 + name: Supplier1_202 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_202 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_202 + - class: SupplierFacility + configs: *id001 + name: Supplier3_203 + - class: SupplierFacility + configs: *id002 + name: Supplier1_203 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_203 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_203 + - class: SupplierFacility + configs: *id001 + name: Supplier3_204 + - class: SupplierFacility + configs: *id002 + name: Supplier1_204 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_204 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_204 + - class: SupplierFacility + configs: *id001 + name: Supplier3_205 + - class: SupplierFacility + configs: *id002 + name: Supplier1_205 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_205 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_205 + - class: SupplierFacility + configs: *id001 + name: Supplier3_206 + - class: SupplierFacility + configs: *id002 + name: Supplier1_206 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_206 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_206 + - class: SupplierFacility + configs: *id001 + name: Supplier3_207 + - class: SupplierFacility + configs: *id002 + name: Supplier1_207 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_207 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_207 + - class: SupplierFacility + configs: *id001 + name: Supplier3_208 + - class: SupplierFacility + configs: *id002 + name: Supplier1_208 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_208 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_208 + - class: SupplierFacility + configs: *id001 + name: Supplier3_209 + - class: SupplierFacility + configs: *id002 + name: Supplier1_209 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_209 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_209 + - class: SupplierFacility + configs: *id001 + name: Supplier3_210 + - class: SupplierFacility + configs: *id002 + name: Supplier1_210 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_210 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_210 + - class: SupplierFacility + configs: *id001 + name: Supplier3_211 + - class: SupplierFacility + configs: *id002 + name: Supplier1_211 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_211 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_211 + - class: SupplierFacility + configs: *id001 + name: Supplier3_212 + - class: SupplierFacility + configs: *id002 + name: Supplier1_212 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_212 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_212 + - class: SupplierFacility + configs: *id001 + name: Supplier3_213 + - class: SupplierFacility + configs: *id002 + name: Supplier1_213 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_213 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_213 + - class: SupplierFacility + configs: *id001 + name: Supplier3_214 + - class: SupplierFacility + configs: *id002 + name: Supplier1_214 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_214 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_214 + - class: SupplierFacility + configs: *id001 + name: Supplier3_215 + - class: SupplierFacility + configs: *id002 + name: Supplier1_215 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_215 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_215 + - class: SupplierFacility + configs: *id001 + name: Supplier3_216 + - class: SupplierFacility + configs: *id002 + name: Supplier1_216 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_216 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_216 + - class: SupplierFacility + configs: *id001 + name: Supplier3_217 + - class: SupplierFacility + configs: *id002 + name: Supplier1_217 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_217 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_217 + - class: SupplierFacility + configs: *id001 + name: Supplier3_218 + - class: SupplierFacility + configs: *id002 + name: Supplier1_218 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_218 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_218 + - class: SupplierFacility + configs: *id001 + name: Supplier3_219 + - class: SupplierFacility + configs: *id002 + name: Supplier1_219 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_219 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_219 + - class: SupplierFacility + configs: *id001 + name: Supplier3_220 + - class: SupplierFacility + configs: *id002 + name: Supplier1_220 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_220 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_220 + - class: SupplierFacility + configs: *id001 + name: Supplier3_221 + - class: SupplierFacility + configs: *id002 + name: Supplier1_221 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_221 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_221 + - class: SupplierFacility + configs: *id001 + name: Supplier3_222 + - class: SupplierFacility + configs: *id002 + name: Supplier1_222 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_222 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_222 + - class: SupplierFacility + configs: *id001 + name: Supplier3_223 + - class: SupplierFacility + configs: *id002 + name: Supplier1_223 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_223 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_223 + - class: SupplierFacility + configs: *id001 + name: Supplier3_224 + - class: SupplierFacility + configs: *id002 + name: Supplier1_224 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_224 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_224 + - class: SupplierFacility + configs: *id001 + name: Supplier3_225 + - class: SupplierFacility + configs: *id002 + name: Supplier1_225 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_225 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_225 + - class: SupplierFacility + configs: *id001 + name: Supplier3_226 + - class: SupplierFacility + configs: *id002 + name: Supplier1_226 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_226 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_226 + - class: SupplierFacility + configs: *id001 + name: Supplier3_227 + - class: SupplierFacility + configs: *id002 + name: Supplier1_227 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_227 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_227 + - class: SupplierFacility + configs: *id001 + name: Supplier3_228 + - class: SupplierFacility + configs: *id002 + name: Supplier1_228 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_228 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_228 + - class: SupplierFacility + configs: *id001 + name: Supplier3_229 + - class: SupplierFacility + configs: *id002 + name: Supplier1_229 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_229 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_229 + - class: SupplierFacility + configs: *id001 + name: Supplier3_230 + - class: SupplierFacility + configs: *id002 + name: Supplier1_230 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_230 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_230 + - class: SupplierFacility + configs: *id001 + name: Supplier3_231 + - class: SupplierFacility + configs: *id002 + name: Supplier1_231 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_231 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_231 + - class: SupplierFacility + configs: *id001 + name: Supplier3_232 + - class: SupplierFacility + configs: *id002 + name: Supplier1_232 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_232 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_232 + - class: SupplierFacility + configs: *id001 + name: Supplier3_233 + - class: SupplierFacility + configs: *id002 + name: Supplier1_233 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_233 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_233 + - class: SupplierFacility + configs: *id001 + name: Supplier3_234 + - class: SupplierFacility + configs: *id002 + name: Supplier1_234 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_234 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_234 + - class: SupplierFacility + configs: *id001 + name: Supplier3_235 + - class: SupplierFacility + configs: *id002 + name: Supplier1_235 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_235 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_235 + - class: SupplierFacility + configs: *id001 + name: Supplier3_236 + - class: SupplierFacility + configs: *id002 + name: Supplier1_236 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_236 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_236 + - class: SupplierFacility + configs: *id001 + name: Supplier3_237 + - class: SupplierFacility + configs: *id002 + name: Supplier1_237 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_237 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_237 + - class: SupplierFacility + configs: *id001 + name: Supplier3_238 + - class: SupplierFacility + configs: *id002 + name: Supplier1_238 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_238 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_238 + - class: SupplierFacility + configs: *id001 + name: Supplier3_239 + - class: SupplierFacility + configs: *id002 + name: Supplier1_239 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_239 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_239 + - class: SupplierFacility + configs: *id001 + name: Supplier3_240 + - class: SupplierFacility + configs: *id002 + name: Supplier1_240 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_240 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_240 + - class: SupplierFacility + configs: *id001 + name: Supplier3_241 + - class: SupplierFacility + configs: *id002 + name: Supplier1_241 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_241 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_241 + - class: SupplierFacility + configs: *id001 + name: Supplier3_242 + - class: SupplierFacility + configs: *id002 + name: Supplier1_242 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_242 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_242 + - class: SupplierFacility + configs: *id001 + name: Supplier3_243 + - class: SupplierFacility + configs: *id002 + name: Supplier1_243 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_243 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_243 + - class: SupplierFacility + configs: *id001 + name: Supplier3_244 + - class: SupplierFacility + configs: *id002 + name: Supplier1_244 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_244 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_244 + - class: SupplierFacility + configs: *id001 + name: Supplier3_245 + - class: SupplierFacility + configs: *id002 + name: Supplier1_245 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_245 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_245 + - class: SupplierFacility + configs: *id001 + name: Supplier3_246 + - class: SupplierFacility + configs: *id002 + name: Supplier1_246 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_246 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_246 + - class: SupplierFacility + configs: *id001 + name: Supplier3_247 + - class: SupplierFacility + configs: *id002 + name: Supplier1_247 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_247 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_247 + - class: SupplierFacility + configs: *id001 + name: Supplier3_248 + - class: SupplierFacility + configs: *id002 + name: Supplier1_248 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_248 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_248 + - class: SupplierFacility + configs: *id001 + name: Supplier3_249 + - class: SupplierFacility + configs: *id002 + name: Supplier1_249 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_249 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_249 + - class: SupplierFacility + configs: *id001 + name: Supplier3_250 + - class: SupplierFacility + configs: *id002 + name: Supplier1_250 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_250 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_250 + - class: SupplierFacility + configs: *id001 + name: Supplier3_251 + - class: SupplierFacility + configs: *id002 + name: Supplier1_251 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_251 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_251 + - class: SupplierFacility + configs: *id001 + name: Supplier3_252 + - class: SupplierFacility + configs: *id002 + name: Supplier1_252 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_252 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_252 + - class: SupplierFacility + configs: *id001 + name: Supplier3_253 + - class: SupplierFacility + configs: *id002 + name: Supplier1_253 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_253 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_253 + - class: SupplierFacility + configs: *id001 + name: Supplier3_254 + - class: SupplierFacility + configs: *id002 + name: Supplier1_254 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_254 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_254 + - class: SupplierFacility + configs: *id001 + name: Supplier3_255 + - class: SupplierFacility + configs: *id002 + name: Supplier1_255 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_255 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_255 + - class: SupplierFacility + configs: *id001 + name: Supplier3_256 + - class: SupplierFacility + configs: *id002 + name: Supplier1_256 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_256 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_256 + - class: SupplierFacility + configs: *id001 + name: Supplier3_257 + - class: SupplierFacility + configs: *id002 + name: Supplier1_257 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_257 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_257 + - class: SupplierFacility + configs: *id001 + name: Supplier3_258 + - class: SupplierFacility + configs: *id002 + name: Supplier1_258 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_258 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_258 + - class: SupplierFacility + configs: *id001 + name: Supplier3_259 + - class: SupplierFacility + configs: *id002 + name: Supplier1_259 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_259 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_259 + - class: SupplierFacility + configs: *id001 + name: Supplier3_260 + - class: SupplierFacility + configs: *id002 + name: Supplier1_260 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_260 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_260 + - class: SupplierFacility + configs: *id001 + name: Supplier3_261 + - class: SupplierFacility + configs: *id002 + name: Supplier1_261 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_261 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_261 + - class: SupplierFacility + configs: *id001 + name: Supplier3_262 + - class: SupplierFacility + configs: *id002 + name: Supplier1_262 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_262 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_262 + - class: SupplierFacility + configs: *id001 + name: Supplier3_263 + - class: SupplierFacility + configs: *id002 + name: Supplier1_263 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_263 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_263 + - class: SupplierFacility + configs: *id001 + name: Supplier3_264 + - class: SupplierFacility + configs: *id002 + name: Supplier1_264 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_264 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_264 + - class: SupplierFacility + configs: *id001 + name: Supplier3_265 + - class: SupplierFacility + configs: *id002 + name: Supplier1_265 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_265 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_265 + - class: SupplierFacility + configs: *id001 + name: Supplier3_266 + - class: SupplierFacility + configs: *id002 + name: Supplier1_266 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_266 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_266 + - class: SupplierFacility + configs: *id001 + name: Supplier3_267 + - class: SupplierFacility + configs: *id002 + name: Supplier1_267 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_267 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_267 + - class: SupplierFacility + configs: *id001 + name: Supplier3_268 + - class: SupplierFacility + configs: *id002 + name: Supplier1_268 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_268 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_268 + - class: SupplierFacility + configs: *id001 + name: Supplier3_269 + - class: SupplierFacility + configs: *id002 + name: Supplier1_269 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_269 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_269 + - class: SupplierFacility + configs: *id001 + name: Supplier3_270 + - class: SupplierFacility + configs: *id002 + name: Supplier1_270 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_270 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_270 + - class: SupplierFacility + configs: *id001 + name: Supplier3_271 + - class: SupplierFacility + configs: *id002 + name: Supplier1_271 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_271 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_271 + - class: SupplierFacility + configs: *id001 + name: Supplier3_272 + - class: SupplierFacility + configs: *id002 + name: Supplier1_272 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_272 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_272 + - class: SupplierFacility + configs: *id001 + name: Supplier3_273 + - class: SupplierFacility + configs: *id002 + name: Supplier1_273 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_273 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_273 + - class: SupplierFacility + configs: *id001 + name: Supplier3_274 + - class: SupplierFacility + configs: *id002 + name: Supplier1_274 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_274 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_274 + - class: SupplierFacility + configs: *id001 + name: Supplier3_275 + - class: SupplierFacility + configs: *id002 + name: Supplier1_275 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_275 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_275 + - class: SupplierFacility + configs: *id001 + name: Supplier3_276 + - class: SupplierFacility + configs: *id002 + name: Supplier1_276 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_276 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_276 + - class: SupplierFacility + configs: *id001 + name: Supplier3_277 + - class: SupplierFacility + configs: *id002 + name: Supplier1_277 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_277 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_277 + - class: SupplierFacility + configs: *id001 + name: Supplier3_278 + - class: SupplierFacility + configs: *id002 + name: Supplier1_278 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_278 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_278 + - class: SupplierFacility + configs: *id001 + name: Supplier3_279 + - class: SupplierFacility + configs: *id002 + name: Supplier1_279 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_279 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_279 + - class: SupplierFacility + configs: *id001 + name: Supplier3_280 + - class: SupplierFacility + configs: *id002 + name: Supplier1_280 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_280 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_280 + - class: SupplierFacility + configs: *id001 + name: Supplier3_281 + - class: SupplierFacility + configs: *id002 + name: Supplier1_281 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_281 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_281 + - class: SupplierFacility + configs: *id001 + name: Supplier3_282 + - class: SupplierFacility + configs: *id002 + name: Supplier1_282 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_282 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_282 + - class: SupplierFacility + configs: *id001 + name: Supplier3_283 + - class: SupplierFacility + configs: *id002 + name: Supplier1_283 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_283 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_283 + - class: SupplierFacility + configs: *id001 + name: Supplier3_284 + - class: SupplierFacility + configs: *id002 + name: Supplier1_284 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_284 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_284 + - class: SupplierFacility + configs: *id001 + name: Supplier3_285 + - class: SupplierFacility + configs: *id002 + name: Supplier1_285 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_285 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_285 + - class: SupplierFacility + configs: *id001 + name: Supplier3_286 + - class: SupplierFacility + configs: *id002 + name: Supplier1_286 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_286 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_286 + - class: SupplierFacility + configs: *id001 + name: Supplier3_287 + - class: SupplierFacility + configs: *id002 + name: Supplier1_287 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_287 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_287 + - class: SupplierFacility + configs: *id001 + name: Supplier3_288 + - class: SupplierFacility + configs: *id002 + name: Supplier1_288 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_288 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_288 + - class: SupplierFacility + configs: *id001 + name: Supplier3_289 + - class: SupplierFacility + configs: *id002 + name: Supplier1_289 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_289 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_289 + - class: SupplierFacility + configs: *id001 + name: Supplier3_290 + - class: SupplierFacility + configs: *id002 + name: Supplier1_290 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_290 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_290 + - class: SupplierFacility + configs: *id001 + name: Supplier3_291 + - class: SupplierFacility + configs: *id002 + name: Supplier1_291 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_291 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_291 + - class: SupplierFacility + configs: *id001 + name: Supplier3_292 + - class: SupplierFacility + configs: *id002 + name: Supplier1_292 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_292 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_292 + - class: SupplierFacility + configs: *id001 + name: Supplier3_293 + - class: SupplierFacility + configs: *id002 + name: Supplier1_293 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_293 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_293 + - class: SupplierFacility + configs: *id001 + name: Supplier3_294 + - class: SupplierFacility + configs: *id002 + name: Supplier1_294 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_294 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_294 + - class: SupplierFacility + configs: *id001 + name: Supplier3_295 + - class: SupplierFacility + configs: *id002 + name: Supplier1_295 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_295 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_295 + - class: SupplierFacility + configs: *id001 + name: Supplier3_296 + - class: SupplierFacility + configs: *id002 + name: Supplier1_296 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_296 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_296 + - class: SupplierFacility + configs: *id001 + name: Supplier3_297 + - class: SupplierFacility + configs: *id002 + name: Supplier1_297 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_297 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_297 + - class: SupplierFacility + configs: *id001 + name: Supplier3_298 + - class: SupplierFacility + configs: *id002 + name: Supplier1_298 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_298 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_298 + - class: SupplierFacility + configs: *id001 + name: Supplier3_299 + - class: SupplierFacility + configs: *id002 + name: Supplier1_299 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_299 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_299 + - class: SupplierFacility + configs: *id001 + name: Supplier3_300 + - class: SupplierFacility + configs: *id002 + name: Supplier1_300 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_300 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_300 + - class: SupplierFacility + configs: *id001 + name: Supplier3_301 + - class: SupplierFacility + configs: *id002 + name: Supplier1_301 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_301 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_301 + - class: SupplierFacility + configs: *id001 + name: Supplier3_302 + - class: SupplierFacility + configs: *id002 + name: Supplier1_302 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_302 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_302 + - class: SupplierFacility + configs: *id001 + name: Supplier3_303 + - class: SupplierFacility + configs: *id002 + name: Supplier1_303 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_303 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_303 + - class: SupplierFacility + configs: *id001 + name: Supplier3_304 + - class: SupplierFacility + configs: *id002 + name: Supplier1_304 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_304 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_304 + - class: SupplierFacility + configs: *id001 + name: Supplier3_305 + - class: SupplierFacility + configs: *id002 + name: Supplier1_305 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_305 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_305 + - class: SupplierFacility + configs: *id001 + name: Supplier3_306 + - class: SupplierFacility + configs: *id002 + name: Supplier1_306 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_306 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_306 + - class: SupplierFacility + configs: *id001 + name: Supplier3_307 + - class: SupplierFacility + configs: *id002 + name: Supplier1_307 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_307 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_307 + - class: SupplierFacility + configs: *id001 + name: Supplier3_308 + - class: SupplierFacility + configs: *id002 + name: Supplier1_308 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_308 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_308 + - class: SupplierFacility + configs: *id001 + name: Supplier3_309 + - class: SupplierFacility + configs: *id002 + name: Supplier1_309 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_309 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_309 + - class: SupplierFacility + configs: *id001 + name: Supplier3_310 + - class: SupplierFacility + configs: *id002 + name: Supplier1_310 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_310 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_310 + - class: SupplierFacility + configs: *id001 + name: Supplier3_311 + - class: SupplierFacility + configs: *id002 + name: Supplier1_311 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_311 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_311 + - class: SupplierFacility + configs: *id001 + name: Supplier3_312 + - class: SupplierFacility + configs: *id002 + name: Supplier1_312 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_312 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_312 + - class: SupplierFacility + configs: *id001 + name: Supplier3_313 + - class: SupplierFacility + configs: *id002 + name: Supplier1_313 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_313 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_313 + - class: SupplierFacility + configs: *id001 + name: Supplier3_314 + - class: SupplierFacility + configs: *id002 + name: Supplier1_314 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_314 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_314 + - class: SupplierFacility + configs: *id001 + name: Supplier3_315 + - class: SupplierFacility + configs: *id002 + name: Supplier1_315 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_315 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_315 + - class: SupplierFacility + configs: *id001 + name: Supplier3_316 + - class: SupplierFacility + configs: *id002 + name: Supplier1_316 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_316 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_316 + - class: SupplierFacility + configs: *id001 + name: Supplier3_317 + - class: SupplierFacility + configs: *id002 + name: Supplier1_317 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_317 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_317 + - class: SupplierFacility + configs: *id001 + name: Supplier3_318 + - class: SupplierFacility + configs: *id002 + name: Supplier1_318 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_318 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_318 + - class: SupplierFacility + configs: *id001 + name: Supplier3_319 + - class: SupplierFacility + configs: *id002 + name: Supplier1_319 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_319 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_319 + - class: SupplierFacility + configs: *id001 + name: Supplier3_320 + - class: SupplierFacility + configs: *id002 + name: Supplier1_320 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_320 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_320 + - class: SupplierFacility + configs: *id001 + name: Supplier3_321 + - class: SupplierFacility + configs: *id002 + name: Supplier1_321 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_321 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_321 + - class: SupplierFacility + configs: *id001 + name: Supplier3_322 + - class: SupplierFacility + configs: *id002 + name: Supplier1_322 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_322 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_322 + - class: SupplierFacility + configs: *id001 + name: Supplier3_323 + - class: SupplierFacility + configs: *id002 + name: Supplier1_323 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_323 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_323 + - class: SupplierFacility + configs: *id001 + name: Supplier3_324 + - class: SupplierFacility + configs: *id002 + name: Supplier1_324 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_324 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_324 + - class: SupplierFacility + configs: *id001 + name: Supplier3_325 + - class: SupplierFacility + configs: *id002 + name: Supplier1_325 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_325 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_325 + - class: SupplierFacility + configs: *id001 + name: Supplier3_326 + - class: SupplierFacility + configs: *id002 + name: Supplier1_326 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_326 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_326 + - class: SupplierFacility + configs: *id001 + name: Supplier3_327 + - class: SupplierFacility + configs: *id002 + name: Supplier1_327 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_327 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_327 + - class: SupplierFacility + configs: *id001 + name: Supplier3_328 + - class: SupplierFacility + configs: *id002 + name: Supplier1_328 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_328 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_328 + - class: SupplierFacility + configs: *id001 + name: Supplier3_329 + - class: SupplierFacility + configs: *id002 + name: Supplier1_329 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_329 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_329 + - class: SupplierFacility + configs: *id001 + name: Supplier3_330 + - class: SupplierFacility + configs: *id002 + name: Supplier1_330 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_330 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_330 + - class: SupplierFacility + configs: *id001 + name: Supplier3_331 + - class: SupplierFacility + configs: *id002 + name: Supplier1_331 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_331 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_331 + - class: SupplierFacility + configs: *id001 + name: Supplier3_332 + - class: SupplierFacility + configs: *id002 + name: Supplier1_332 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_332 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_332 + - class: SupplierFacility + configs: *id001 + name: Supplier3_333 + - class: SupplierFacility + configs: *id002 + name: Supplier1_333 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_333 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_333 + - class: SupplierFacility + configs: *id001 + name: Supplier3_334 + - class: SupplierFacility + configs: *id002 + name: Supplier1_334 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_334 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_334 + - class: SupplierFacility + configs: *id001 + name: Supplier3_335 + - class: SupplierFacility + configs: *id002 + name: Supplier1_335 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_335 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_335 + - class: SupplierFacility + configs: *id001 + name: Supplier3_336 + - class: SupplierFacility + configs: *id002 + name: Supplier1_336 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_336 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_336 + - class: SupplierFacility + configs: *id001 + name: Supplier3_337 + - class: SupplierFacility + configs: *id002 + name: Supplier1_337 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_337 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_337 + - class: SupplierFacility + configs: *id001 + name: Supplier3_338 + - class: SupplierFacility + configs: *id002 + name: Supplier1_338 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_338 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_338 + - class: SupplierFacility + configs: *id001 + name: Supplier3_339 + - class: SupplierFacility + configs: *id002 + name: Supplier1_339 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_339 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_339 + - class: SupplierFacility + configs: *id001 + name: Supplier3_340 + - class: SupplierFacility + configs: *id002 + name: Supplier1_340 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_340 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_340 + - class: SupplierFacility + configs: *id001 + name: Supplier3_341 + - class: SupplierFacility + configs: *id002 + name: Supplier1_341 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_341 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_341 + - class: SupplierFacility + configs: *id001 + name: Supplier3_342 + - class: SupplierFacility + configs: *id002 + name: Supplier1_342 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_342 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_342 + - class: SupplierFacility + configs: *id001 + name: Supplier3_343 + - class: SupplierFacility + configs: *id002 + name: Supplier1_343 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_343 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_343 + - class: SupplierFacility + configs: *id001 + name: Supplier3_344 + - class: SupplierFacility + configs: *id002 + name: Supplier1_344 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_344 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_344 + - class: SupplierFacility + configs: *id001 + name: Supplier3_345 + - class: SupplierFacility + configs: *id002 + name: Supplier1_345 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_345 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_345 + - class: SupplierFacility + configs: *id001 + name: Supplier3_346 + - class: SupplierFacility + configs: *id002 + name: Supplier1_346 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_346 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_346 + - class: SupplierFacility + configs: *id001 + name: Supplier3_347 + - class: SupplierFacility + configs: *id002 + name: Supplier1_347 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_347 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_347 + - class: SupplierFacility + configs: *id001 + name: Supplier3_348 + - class: SupplierFacility + configs: *id002 + name: Supplier1_348 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_348 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_348 + - class: SupplierFacility + configs: *id001 + name: Supplier3_349 + - class: SupplierFacility + configs: *id002 + name: Supplier1_349 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_349 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_349 + - class: SupplierFacility + configs: *id001 + name: Supplier3_350 + - class: SupplierFacility + configs: *id002 + name: Supplier1_350 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_350 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_350 + - class: SupplierFacility + configs: *id001 + name: Supplier3_351 + - class: SupplierFacility + configs: *id002 + name: Supplier1_351 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_351 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_351 + - class: SupplierFacility + configs: *id001 + name: Supplier3_352 + - class: SupplierFacility + configs: *id002 + name: Supplier1_352 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_352 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_352 + - class: SupplierFacility + configs: *id001 + name: Supplier3_353 + - class: SupplierFacility + configs: *id002 + name: Supplier1_353 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_353 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_353 + - class: SupplierFacility + configs: *id001 + name: Supplier3_354 + - class: SupplierFacility + configs: *id002 + name: Supplier1_354 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_354 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_354 + - class: SupplierFacility + configs: *id001 + name: Supplier3_355 + - class: SupplierFacility + configs: *id002 + name: Supplier1_355 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_355 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_355 + - class: SupplierFacility + configs: *id001 + name: Supplier3_356 + - class: SupplierFacility + configs: *id002 + name: Supplier1_356 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_356 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_356 + - class: SupplierFacility + configs: *id001 + name: Supplier3_357 + - class: SupplierFacility + configs: *id002 + name: Supplier1_357 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_357 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_357 + - class: SupplierFacility + configs: *id001 + name: Supplier3_358 + - class: SupplierFacility + configs: *id002 + name: Supplier1_358 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_358 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_358 + - class: SupplierFacility + configs: *id001 + name: Supplier3_359 + - class: SupplierFacility + configs: *id002 + name: Supplier1_359 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_359 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_359 + - class: SupplierFacility + configs: *id001 + name: Supplier3_360 + - class: SupplierFacility + configs: *id002 + name: Supplier1_360 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_360 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_360 + - class: SupplierFacility + configs: *id001 + name: Supplier3_361 + - class: SupplierFacility + configs: *id002 + name: Supplier1_361 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_361 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_361 + - class: SupplierFacility + configs: *id001 + name: Supplier3_362 + - class: SupplierFacility + configs: *id002 + name: Supplier1_362 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_362 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_362 + - class: SupplierFacility + configs: *id001 + name: Supplier3_363 + - class: SupplierFacility + configs: *id002 + name: Supplier1_363 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_363 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_363 + - class: SupplierFacility + configs: *id001 + name: Supplier3_364 + - class: SupplierFacility + configs: *id002 + name: Supplier1_364 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_364 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_364 + - class: SupplierFacility + configs: *id001 + name: Supplier3_365 + - class: SupplierFacility + configs: *id002 + name: Supplier1_365 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_365 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_365 + - class: SupplierFacility + configs: *id001 + name: Supplier3_366 + - class: SupplierFacility + configs: *id002 + name: Supplier1_366 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_366 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_366 + - class: SupplierFacility + configs: *id001 + name: Supplier3_367 + - class: SupplierFacility + configs: *id002 + name: Supplier1_367 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_367 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_367 + - class: SupplierFacility + configs: *id001 + name: Supplier3_368 + - class: SupplierFacility + configs: *id002 + name: Supplier1_368 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_368 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_368 + - class: SupplierFacility + configs: *id001 + name: Supplier3_369 + - class: SupplierFacility + configs: *id002 + name: Supplier1_369 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_369 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_369 + - class: SupplierFacility + configs: *id001 + name: Supplier3_370 + - class: SupplierFacility + configs: *id002 + name: Supplier1_370 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_370 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_370 + - class: SupplierFacility + configs: *id001 + name: Supplier3_371 + - class: SupplierFacility + configs: *id002 + name: Supplier1_371 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_371 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_371 + - class: SupplierFacility + configs: *id001 + name: Supplier3_372 + - class: SupplierFacility + configs: *id002 + name: Supplier1_372 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_372 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_372 + - class: SupplierFacility + configs: *id001 + name: Supplier3_373 + - class: SupplierFacility + configs: *id002 + name: Supplier1_373 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_373 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_373 + - class: SupplierFacility + configs: *id001 + name: Supplier3_374 + - class: SupplierFacility + configs: *id002 + name: Supplier1_374 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_374 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_374 + - class: SupplierFacility + configs: *id001 + name: Supplier3_375 + - class: SupplierFacility + configs: *id002 + name: Supplier1_375 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_375 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_375 + - class: SupplierFacility + configs: *id001 + name: Supplier3_376 + - class: SupplierFacility + configs: *id002 + name: Supplier1_376 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_376 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_376 + - class: SupplierFacility + configs: *id001 + name: Supplier3_377 + - class: SupplierFacility + configs: *id002 + name: Supplier1_377 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_377 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_377 + - class: SupplierFacility + configs: *id001 + name: Supplier3_378 + - class: SupplierFacility + configs: *id002 + name: Supplier1_378 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_378 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_378 + - class: SupplierFacility + configs: *id001 + name: Supplier3_379 + - class: SupplierFacility + configs: *id002 + name: Supplier1_379 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_379 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_379 + - class: SupplierFacility + configs: *id001 + name: Supplier3_380 + - class: SupplierFacility + configs: *id002 + name: Supplier1_380 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_380 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_380 + - class: SupplierFacility + configs: *id001 + name: Supplier3_381 + - class: SupplierFacility + configs: *id002 + name: Supplier1_381 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_381 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_381 + - class: SupplierFacility + configs: *id001 + name: Supplier3_382 + - class: SupplierFacility + configs: *id002 + name: Supplier1_382 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_382 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_382 + - class: SupplierFacility + configs: *id001 + name: Supplier3_383 + - class: SupplierFacility + configs: *id002 + name: Supplier1_383 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_383 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_383 + - class: SupplierFacility + configs: *id001 + name: Supplier3_384 + - class: SupplierFacility + configs: *id002 + name: Supplier1_384 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_384 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_384 + - class: SupplierFacility + configs: *id001 + name: Supplier3_385 + - class: SupplierFacility + configs: *id002 + name: Supplier1_385 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_385 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_385 + - class: SupplierFacility + configs: *id001 + name: Supplier3_386 + - class: SupplierFacility + configs: *id002 + name: Supplier1_386 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_386 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_386 + - class: SupplierFacility + configs: *id001 + name: Supplier3_387 + - class: SupplierFacility + configs: *id002 + name: Supplier1_387 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_387 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_387 + - class: SupplierFacility + configs: *id001 + name: Supplier3_388 + - class: SupplierFacility + configs: *id002 + name: Supplier1_388 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_388 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_388 + - class: SupplierFacility + configs: *id001 + name: Supplier3_389 + - class: SupplierFacility + configs: *id002 + name: Supplier1_389 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_389 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_389 + - class: SupplierFacility + configs: *id001 + name: Supplier3_390 + - class: SupplierFacility + configs: *id002 + name: Supplier1_390 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_390 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_390 + - class: SupplierFacility + configs: *id001 + name: Supplier3_391 + - class: SupplierFacility + configs: *id002 + name: Supplier1_391 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_391 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_391 + - class: SupplierFacility + configs: *id001 + name: Supplier3_392 + - class: SupplierFacility + configs: *id002 + name: Supplier1_392 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_392 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_392 + - class: SupplierFacility + configs: *id001 + name: Supplier3_393 + - class: SupplierFacility + configs: *id002 + name: Supplier1_393 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_393 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_393 + - class: SupplierFacility + configs: *id001 + name: Supplier3_394 + - class: SupplierFacility + configs: *id002 + name: Supplier1_394 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_394 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_394 + - class: SupplierFacility + configs: *id001 + name: Supplier3_395 + - class: SupplierFacility + configs: *id002 + name: Supplier1_395 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_395 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_395 + - class: SupplierFacility + configs: *id001 + name: Supplier3_396 + - class: SupplierFacility + configs: *id002 + name: Supplier1_396 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_396 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_396 + - class: SupplierFacility + configs: *id001 + name: Supplier3_397 + - class: SupplierFacility + configs: *id002 + name: Supplier1_397 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_397 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_397 + - class: SupplierFacility + configs: *id001 + name: Supplier3_398 + - class: SupplierFacility + configs: *id002 + name: Supplier1_398 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_398 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_398 + - class: SupplierFacility + configs: *id001 + name: Supplier3_399 + - class: SupplierFacility + configs: *id002 + name: Supplier1_399 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_399 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_399 + - class: SupplierFacility + configs: *id001 + name: Supplier3_400 + - class: SupplierFacility + configs: *id002 + name: Supplier1_400 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_400 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_400 + - class: SupplierFacility + configs: *id001 + name: Supplier3_401 + - class: SupplierFacility + configs: *id002 + name: Supplier1_401 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_401 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_401 + - class: SupplierFacility + configs: *id001 + name: Supplier3_402 + - class: SupplierFacility + configs: *id002 + name: Supplier1_402 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_402 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_402 + - class: SupplierFacility + configs: *id001 + name: Supplier3_403 + - class: SupplierFacility + configs: *id002 + name: Supplier1_403 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_403 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_403 + - class: SupplierFacility + configs: *id001 + name: Supplier3_404 + - class: SupplierFacility + configs: *id002 + name: Supplier1_404 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_404 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_404 + - class: SupplierFacility + configs: *id001 + name: Supplier3_405 + - class: SupplierFacility + configs: *id002 + name: Supplier1_405 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_405 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_405 + - class: SupplierFacility + configs: *id001 + name: Supplier3_406 + - class: SupplierFacility + configs: *id002 + name: Supplier1_406 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_406 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_406 + - class: SupplierFacility + configs: *id001 + name: Supplier3_407 + - class: SupplierFacility + configs: *id002 + name: Supplier1_407 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_407 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_407 + - class: SupplierFacility + configs: *id001 + name: Supplier3_408 + - class: SupplierFacility + configs: *id002 + name: Supplier1_408 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_408 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_408 + - class: SupplierFacility + configs: *id001 + name: Supplier3_409 + - class: SupplierFacility + configs: *id002 + name: Supplier1_409 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_409 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_409 + - class: SupplierFacility + configs: *id001 + name: Supplier3_410 + - class: SupplierFacility + configs: *id002 + name: Supplier1_410 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_410 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_410 + - class: SupplierFacility + configs: *id001 + name: Supplier3_411 + - class: SupplierFacility + configs: *id002 + name: Supplier1_411 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_411 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_411 + - class: SupplierFacility + configs: *id001 + name: Supplier3_412 + - class: SupplierFacility + configs: *id002 + name: Supplier1_412 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_412 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_412 + - class: SupplierFacility + configs: *id001 + name: Supplier3_413 + - class: SupplierFacility + configs: *id002 + name: Supplier1_413 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_413 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_413 + - class: SupplierFacility + configs: *id001 + name: Supplier3_414 + - class: SupplierFacility + configs: *id002 + name: Supplier1_414 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_414 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_414 + - class: SupplierFacility + configs: *id001 + name: Supplier3_415 + - class: SupplierFacility + configs: *id002 + name: Supplier1_415 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_415 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_415 + - class: SupplierFacility + configs: *id001 + name: Supplier3_416 + - class: SupplierFacility + configs: *id002 + name: Supplier1_416 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_416 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_416 + - class: SupplierFacility + configs: *id001 + name: Supplier3_417 + - class: SupplierFacility + configs: *id002 + name: Supplier1_417 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_417 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_417 + - class: SupplierFacility + configs: *id001 + name: Supplier3_418 + - class: SupplierFacility + configs: *id002 + name: Supplier1_418 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_418 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_418 + - class: SupplierFacility + configs: *id001 + name: Supplier3_419 + - class: SupplierFacility + configs: *id002 + name: Supplier1_419 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_419 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_419 + - class: SupplierFacility + configs: *id001 + name: Supplier3_420 + - class: SupplierFacility + configs: *id002 + name: Supplier1_420 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_420 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_420 + - class: SupplierFacility + configs: *id001 + name: Supplier3_421 + - class: SupplierFacility + configs: *id002 + name: Supplier1_421 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_421 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_421 + - class: SupplierFacility + configs: *id001 + name: Supplier3_422 + - class: SupplierFacility + configs: *id002 + name: Supplier1_422 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_422 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_422 + - class: SupplierFacility + configs: *id001 + name: Supplier3_423 + - class: SupplierFacility + configs: *id002 + name: Supplier1_423 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_423 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_423 + - class: SupplierFacility + configs: *id001 + name: Supplier3_424 + - class: SupplierFacility + configs: *id002 + name: Supplier1_424 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_424 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_424 + - class: SupplierFacility + configs: *id001 + name: Supplier3_425 + - class: SupplierFacility + configs: *id002 + name: Supplier1_425 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_425 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_425 + - class: SupplierFacility + configs: *id001 + name: Supplier3_426 + - class: SupplierFacility + configs: *id002 + name: Supplier1_426 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_426 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_426 + - class: SupplierFacility + configs: *id001 + name: Supplier3_427 + - class: SupplierFacility + configs: *id002 + name: Supplier1_427 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_427 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_427 + - class: SupplierFacility + configs: *id001 + name: Supplier3_428 + - class: SupplierFacility + configs: *id002 + name: Supplier1_428 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_428 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_428 + - class: SupplierFacility + configs: *id001 + name: Supplier3_429 + - class: SupplierFacility + configs: *id002 + name: Supplier1_429 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_429 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_429 + - class: SupplierFacility + configs: *id001 + name: Supplier3_430 + - class: SupplierFacility + configs: *id002 + name: Supplier1_430 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_430 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_430 + - class: SupplierFacility + configs: *id001 + name: Supplier3_431 + - class: SupplierFacility + configs: *id002 + name: Supplier1_431 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_431 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_431 + - class: SupplierFacility + configs: *id001 + name: Supplier3_432 + - class: SupplierFacility + configs: *id002 + name: Supplier1_432 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_432 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_432 + - class: SupplierFacility + configs: *id001 + name: Supplier3_433 + - class: SupplierFacility + configs: *id002 + name: Supplier1_433 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_433 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_433 + - class: SupplierFacility + configs: *id001 + name: Supplier3_434 + - class: SupplierFacility + configs: *id002 + name: Supplier1_434 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_434 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_434 + - class: SupplierFacility + configs: *id001 + name: Supplier3_435 + - class: SupplierFacility + configs: *id002 + name: Supplier1_435 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_435 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_435 + - class: SupplierFacility + configs: *id001 + name: Supplier3_436 + - class: SupplierFacility + configs: *id002 + name: Supplier1_436 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_436 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_436 + - class: SupplierFacility + configs: *id001 + name: Supplier3_437 + - class: SupplierFacility + configs: *id002 + name: Supplier1_437 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_437 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_437 + - class: SupplierFacility + configs: *id001 + name: Supplier3_438 + - class: SupplierFacility + configs: *id002 + name: Supplier1_438 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_438 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_438 + - class: SupplierFacility + configs: *id001 + name: Supplier3_439 + - class: SupplierFacility + configs: *id002 + name: Supplier1_439 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_439 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_439 + - class: SupplierFacility + configs: *id001 + name: Supplier3_440 + - class: SupplierFacility + configs: *id002 + name: Supplier1_440 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_440 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_440 + - class: SupplierFacility + configs: *id001 + name: Supplier3_441 + - class: SupplierFacility + configs: *id002 + name: Supplier1_441 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_441 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_441 + - class: SupplierFacility + configs: *id001 + name: Supplier3_442 + - class: SupplierFacility + configs: *id002 + name: Supplier1_442 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_442 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_442 + - class: SupplierFacility + configs: *id001 + name: Supplier3_443 + - class: SupplierFacility + configs: *id002 + name: Supplier1_443 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_443 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_443 + - class: SupplierFacility + configs: *id001 + name: Supplier3_444 + - class: SupplierFacility + configs: *id002 + name: Supplier1_444 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_444 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_444 + - class: SupplierFacility + configs: *id001 + name: Supplier3_445 + - class: SupplierFacility + configs: *id002 + name: Supplier1_445 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_445 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_445 + - class: SupplierFacility + configs: *id001 + name: Supplier3_446 + - class: SupplierFacility + configs: *id002 + name: Supplier1_446 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_446 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_446 + - class: SupplierFacility + configs: *id001 + name: Supplier3_447 + - class: SupplierFacility + configs: *id002 + name: Supplier1_447 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_447 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_447 + - class: SupplierFacility + configs: *id001 + name: Supplier3_448 + - class: SupplierFacility + configs: *id002 + name: Supplier1_448 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_448 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_448 + - class: SupplierFacility + configs: *id001 + name: Supplier3_449 + - class: SupplierFacility + configs: *id002 + name: Supplier1_449 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_449 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_449 + - class: SupplierFacility + configs: *id001 + name: Supplier3_450 + - class: SupplierFacility + configs: *id002 + name: Supplier1_450 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_450 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_450 + - class: SupplierFacility + configs: *id001 + name: Supplier3_451 + - class: SupplierFacility + configs: *id002 + name: Supplier1_451 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_451 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_451 + - class: SupplierFacility + configs: *id001 + name: Supplier3_452 + - class: SupplierFacility + configs: *id002 + name: Supplier1_452 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_452 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_452 + - class: SupplierFacility + configs: *id001 + name: Supplier3_453 + - class: SupplierFacility + configs: *id002 + name: Supplier1_453 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_453 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_453 + - class: SupplierFacility + configs: *id001 + name: Supplier3_454 + - class: SupplierFacility + configs: *id002 + name: Supplier1_454 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_454 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_454 + - class: SupplierFacility + configs: *id001 + name: Supplier3_455 + - class: SupplierFacility + configs: *id002 + name: Supplier1_455 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_455 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_455 + - class: SupplierFacility + configs: *id001 + name: Supplier3_456 + - class: SupplierFacility + configs: *id002 + name: Supplier1_456 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_456 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_456 + - class: SupplierFacility + configs: *id001 + name: Supplier3_457 + - class: SupplierFacility + configs: *id002 + name: Supplier1_457 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_457 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_457 + - class: SupplierFacility + configs: *id001 + name: Supplier3_458 + - class: SupplierFacility + configs: *id002 + name: Supplier1_458 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_458 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_458 + - class: SupplierFacility + configs: *id001 + name: Supplier3_459 + - class: SupplierFacility + configs: *id002 + name: Supplier1_459 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_459 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_459 + - class: SupplierFacility + configs: *id001 + name: Supplier3_460 + - class: SupplierFacility + configs: *id002 + name: Supplier1_460 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_460 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_460 + - class: SupplierFacility + configs: *id001 + name: Supplier3_461 + - class: SupplierFacility + configs: *id002 + name: Supplier1_461 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_461 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_461 + - class: SupplierFacility + configs: *id001 + name: Supplier3_462 + - class: SupplierFacility + configs: *id002 + name: Supplier1_462 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_462 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_462 + - class: SupplierFacility + configs: *id001 + name: Supplier3_463 + - class: SupplierFacility + configs: *id002 + name: Supplier1_463 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_463 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_463 + - class: SupplierFacility + configs: *id001 + name: Supplier3_464 + - class: SupplierFacility + configs: *id002 + name: Supplier1_464 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_464 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_464 + - class: SupplierFacility + configs: *id001 + name: Supplier3_465 + - class: SupplierFacility + configs: *id002 + name: Supplier1_465 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_465 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_465 + - class: SupplierFacility + configs: *id001 + name: Supplier3_466 + - class: SupplierFacility + configs: *id002 + name: Supplier1_466 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_466 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_466 + - class: SupplierFacility + configs: *id001 + name: Supplier3_467 + - class: SupplierFacility + configs: *id002 + name: Supplier1_467 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_467 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_467 + - class: SupplierFacility + configs: *id001 + name: Supplier3_468 + - class: SupplierFacility + configs: *id002 + name: Supplier1_468 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_468 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_468 + - class: SupplierFacility + configs: *id001 + name: Supplier3_469 + - class: SupplierFacility + configs: *id002 + name: Supplier1_469 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_469 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_469 + - class: SupplierFacility + configs: *id001 + name: Supplier3_470 + - class: SupplierFacility + configs: *id002 + name: Supplier1_470 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_470 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_470 + - class: SupplierFacility + configs: *id001 + name: Supplier3_471 + - class: SupplierFacility + configs: *id002 + name: Supplier1_471 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_471 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_471 + - class: SupplierFacility + configs: *id001 + name: Supplier3_472 + - class: SupplierFacility + configs: *id002 + name: Supplier1_472 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_472 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_472 + - class: SupplierFacility + configs: *id001 + name: Supplier3_473 + - class: SupplierFacility + configs: *id002 + name: Supplier1_473 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_473 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_473 + - class: SupplierFacility + configs: *id001 + name: Supplier3_474 + - class: SupplierFacility + configs: *id002 + name: Supplier1_474 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_474 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_474 + - class: SupplierFacility + configs: *id001 + name: Supplier3_475 + - class: SupplierFacility + configs: *id002 + name: Supplier1_475 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_475 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_475 + - class: SupplierFacility + configs: *id001 + name: Supplier3_476 + - class: SupplierFacility + configs: *id002 + name: Supplier1_476 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_476 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_476 + - class: SupplierFacility + configs: *id001 + name: Supplier3_477 + - class: SupplierFacility + configs: *id002 + name: Supplier1_477 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_477 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_477 + - class: SupplierFacility + configs: *id001 + name: Supplier3_478 + - class: SupplierFacility + configs: *id002 + name: Supplier1_478 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_478 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_478 + - class: SupplierFacility + configs: *id001 + name: Supplier3_479 + - class: SupplierFacility + configs: *id002 + name: Supplier1_479 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_479 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_479 + - class: SupplierFacility + configs: *id001 + name: Supplier3_480 + - class: SupplierFacility + configs: *id002 + name: Supplier1_480 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_480 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_480 + - class: SupplierFacility + configs: *id001 + name: Supplier3_481 + - class: SupplierFacility + configs: *id002 + name: Supplier1_481 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_481 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_481 + - class: SupplierFacility + configs: *id001 + name: Supplier3_482 + - class: SupplierFacility + configs: *id002 + name: Supplier1_482 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_482 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_482 + - class: SupplierFacility + configs: *id001 + name: Supplier3_483 + - class: SupplierFacility + configs: *id002 + name: Supplier1_483 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_483 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_483 + - class: SupplierFacility + configs: *id001 + name: Supplier3_484 + - class: SupplierFacility + configs: *id002 + name: Supplier1_484 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_484 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_484 + - class: SupplierFacility + configs: *id001 + name: Supplier3_485 + - class: SupplierFacility + configs: *id002 + name: Supplier1_485 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_485 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_485 + - class: SupplierFacility + configs: *id001 + name: Supplier3_486 + - class: SupplierFacility + configs: *id002 + name: Supplier1_486 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_486 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_486 + - class: SupplierFacility + configs: *id001 + name: Supplier3_487 + - class: SupplierFacility + configs: *id002 + name: Supplier1_487 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_487 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_487 + - class: SupplierFacility + configs: *id001 + name: Supplier3_488 + - class: SupplierFacility + configs: *id002 + name: Supplier1_488 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_488 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_488 + - class: SupplierFacility + configs: *id001 + name: Supplier3_489 + - class: SupplierFacility + configs: *id002 + name: Supplier1_489 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_489 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_489 + - class: SupplierFacility + configs: *id001 + name: Supplier3_490 + - class: SupplierFacility + configs: *id002 + name: Supplier1_490 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_490 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_490 + - class: SupplierFacility + configs: *id001 + name: Supplier3_491 + - class: SupplierFacility + configs: *id002 + name: Supplier1_491 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_491 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_491 + - class: SupplierFacility + configs: *id001 + name: Supplier3_492 + - class: SupplierFacility + configs: *id002 + name: Supplier1_492 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_492 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_492 + - class: SupplierFacility + configs: *id001 + name: Supplier3_493 + - class: SupplierFacility + configs: *id002 + name: Supplier1_493 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_493 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_493 + - class: SupplierFacility + configs: *id001 + name: Supplier3_494 + - class: SupplierFacility + configs: *id002 + name: Supplier1_494 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_494 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_494 + - class: SupplierFacility + configs: *id001 + name: Supplier3_495 + - class: SupplierFacility + configs: *id002 + name: Supplier1_495 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_495 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_495 + - class: SupplierFacility + configs: *id001 + name: Supplier3_496 + - class: SupplierFacility + configs: *id002 + name: Supplier1_496 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_496 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_496 + - class: SupplierFacility + configs: *id001 + name: Supplier3_497 + - class: SupplierFacility + configs: *id002 + name: Supplier1_497 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_497 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_497 + - class: SupplierFacility + configs: *id001 + name: Supplier3_498 + - class: SupplierFacility + configs: *id002 + name: Supplier1_498 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_498 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_498 + - class: SupplierFacility + configs: *id001 + name: Supplier3_499 + - class: SupplierFacility + configs: *id002 + name: Supplier1_499 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_499 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_499 + - class: SupplierFacility + configs: *id001 + name: Supplier3_500 + - class: SupplierFacility + configs: *id002 + name: Supplier1_500 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_500 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_500 + - class: SupplierFacility + configs: *id001 + name: Supplier3_501 + - class: SupplierFacility + configs: *id002 + name: Supplier1_501 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_501 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_501 + - class: SupplierFacility + configs: *id001 + name: Supplier3_502 + - class: SupplierFacility + configs: *id002 + name: Supplier1_502 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_502 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_502 + - class: SupplierFacility + configs: *id001 + name: Supplier3_503 + - class: SupplierFacility + configs: *id002 + name: Supplier1_503 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_503 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_503 + - class: SupplierFacility + configs: *id001 + name: Supplier3_504 + - class: SupplierFacility + configs: *id002 + name: Supplier1_504 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_504 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_504 + - class: SupplierFacility + configs: *id001 + name: Supplier3_505 + - class: SupplierFacility + configs: *id002 + name: Supplier1_505 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_505 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_505 + - class: SupplierFacility + configs: *id001 + name: Supplier3_506 + - class: SupplierFacility + configs: *id002 + name: Supplier1_506 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_506 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_506 + - class: SupplierFacility + configs: *id001 + name: Supplier3_507 + - class: SupplierFacility + configs: *id002 + name: Supplier1_507 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_507 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_507 + - class: SupplierFacility + configs: *id001 + name: Supplier3_508 + - class: SupplierFacility + configs: *id002 + name: Supplier1_508 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_508 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_508 + - class: SupplierFacility + configs: *id001 + name: Supplier3_509 + - class: SupplierFacility + configs: *id002 + name: Supplier1_509 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_509 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_509 + - class: SupplierFacility + configs: *id001 + name: Supplier3_510 + - class: SupplierFacility + configs: *id002 + name: Supplier1_510 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_510 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_510 + - class: SupplierFacility + configs: *id001 + name: Supplier3_511 + - class: SupplierFacility + configs: *id002 + name: Supplier1_511 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_511 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_511 + - class: SupplierFacility + configs: *id001 + name: Supplier3_512 + - class: SupplierFacility + configs: *id002 + name: Supplier1_512 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_512 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_512 + - class: SupplierFacility + configs: *id001 + name: Supplier3_513 + - class: SupplierFacility + configs: *id002 + name: Supplier1_513 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_513 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_513 + - class: SupplierFacility + configs: *id001 + name: Supplier3_514 + - class: SupplierFacility + configs: *id002 + name: Supplier1_514 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_514 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_514 + - class: SupplierFacility + configs: *id001 + name: Supplier3_515 + - class: SupplierFacility + configs: *id002 + name: Supplier1_515 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_515 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_515 + - class: SupplierFacility + configs: *id001 + name: Supplier3_516 + - class: SupplierFacility + configs: *id002 + name: Supplier1_516 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_516 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_516 + - class: SupplierFacility + configs: *id001 + name: Supplier3_517 + - class: SupplierFacility + configs: *id002 + name: Supplier1_517 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_517 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_517 + - class: SupplierFacility + configs: *id001 + name: Supplier3_518 + - class: SupplierFacility + configs: *id002 + name: Supplier1_518 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_518 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_518 + - class: SupplierFacility + configs: *id001 + name: Supplier3_519 + - class: SupplierFacility + configs: *id002 + name: Supplier1_519 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_519 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_519 + - class: SupplierFacility + configs: *id001 + name: Supplier3_520 + - class: SupplierFacility + configs: *id002 + name: Supplier1_520 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_520 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_520 + - class: SupplierFacility + configs: *id001 + name: Supplier3_521 + - class: SupplierFacility + configs: *id002 + name: Supplier1_521 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_521 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_521 + - class: SupplierFacility + configs: *id001 + name: Supplier3_522 + - class: SupplierFacility + configs: *id002 + name: Supplier1_522 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_522 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_522 + - class: SupplierFacility + configs: *id001 + name: Supplier3_523 + - class: SupplierFacility + configs: *id002 + name: Supplier1_523 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_523 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_523 + - class: SupplierFacility + configs: *id001 + name: Supplier3_524 + - class: SupplierFacility + configs: *id002 + name: Supplier1_524 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_524 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_524 + - class: SupplierFacility + configs: *id001 + name: Supplier3_525 + - class: SupplierFacility + configs: *id002 + name: Supplier1_525 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_525 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_525 + - class: SupplierFacility + configs: *id001 + name: Supplier3_526 + - class: SupplierFacility + configs: *id002 + name: Supplier1_526 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_526 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_526 + - class: SupplierFacility + configs: *id001 + name: Supplier3_527 + - class: SupplierFacility + configs: *id002 + name: Supplier1_527 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_527 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_527 + - class: SupplierFacility + configs: *id001 + name: Supplier3_528 + - class: SupplierFacility + configs: *id002 + name: Supplier1_528 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_528 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_528 + - class: SupplierFacility + configs: *id001 + name: Supplier3_529 + - class: SupplierFacility + configs: *id002 + name: Supplier1_529 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_529 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_529 + - class: SupplierFacility + configs: *id001 + name: Supplier3_530 + - class: SupplierFacility + configs: *id002 + name: Supplier1_530 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_530 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_530 + - class: SupplierFacility + configs: *id001 + name: Supplier3_531 + - class: SupplierFacility + configs: *id002 + name: Supplier1_531 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_531 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_531 + - class: SupplierFacility + configs: *id001 + name: Supplier3_532 + - class: SupplierFacility + configs: *id002 + name: Supplier1_532 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_532 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_532 + - class: SupplierFacility + configs: *id001 + name: Supplier3_533 + - class: SupplierFacility + configs: *id002 + name: Supplier1_533 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_533 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_533 + - class: SupplierFacility + configs: *id001 + name: Supplier3_534 + - class: SupplierFacility + configs: *id002 + name: Supplier1_534 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_534 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_534 + - class: SupplierFacility + configs: *id001 + name: Supplier3_535 + - class: SupplierFacility + configs: *id002 + name: Supplier1_535 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_535 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_535 + - class: SupplierFacility + configs: *id001 + name: Supplier3_536 + - class: SupplierFacility + configs: *id002 + name: Supplier1_536 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_536 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_536 + - class: SupplierFacility + configs: *id001 + name: Supplier3_537 + - class: SupplierFacility + configs: *id002 + name: Supplier1_537 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_537 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_537 + - class: SupplierFacility + configs: *id001 + name: Supplier3_538 + - class: SupplierFacility + configs: *id002 + name: Supplier1_538 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_538 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_538 + - class: SupplierFacility + configs: *id001 + name: Supplier3_539 + - class: SupplierFacility + configs: *id002 + name: Supplier1_539 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_539 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_539 + - class: SupplierFacility + configs: *id001 + name: Supplier3_540 + - class: SupplierFacility + configs: *id002 + name: Supplier1_540 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_540 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_540 + - class: SupplierFacility + configs: *id001 + name: Supplier3_541 + - class: SupplierFacility + configs: *id002 + name: Supplier1_541 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_541 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_541 + - class: SupplierFacility + configs: *id001 + name: Supplier3_542 + - class: SupplierFacility + configs: *id002 + name: Supplier1_542 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_542 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_542 + - class: SupplierFacility + configs: *id001 + name: Supplier3_543 + - class: SupplierFacility + configs: *id002 + name: Supplier1_543 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_543 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_543 + - class: SupplierFacility + configs: *id001 + name: Supplier3_544 + - class: SupplierFacility + configs: *id002 + name: Supplier1_544 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_544 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_544 + - class: SupplierFacility + configs: *id001 + name: Supplier3_545 + - class: SupplierFacility + configs: *id002 + name: Supplier1_545 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_545 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_545 + - class: SupplierFacility + configs: *id001 + name: Supplier3_546 + - class: SupplierFacility + configs: *id002 + name: Supplier1_546 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_546 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_546 + - class: SupplierFacility + configs: *id001 + name: Supplier3_547 + - class: SupplierFacility + configs: *id002 + name: Supplier1_547 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_547 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_547 + - class: SupplierFacility + configs: *id001 + name: Supplier3_548 + - class: SupplierFacility + configs: *id002 + name: Supplier1_548 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_548 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_548 + - class: SupplierFacility + configs: *id001 + name: Supplier3_549 + - class: SupplierFacility + configs: *id002 + name: Supplier1_549 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_549 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_549 + - class: SupplierFacility + configs: *id001 + name: Supplier3_550 + - class: SupplierFacility + configs: *id002 + name: Supplier1_550 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_550 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_550 + - class: SupplierFacility + configs: *id001 + name: Supplier3_551 + - class: SupplierFacility + configs: *id002 + name: Supplier1_551 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_551 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_551 + - class: SupplierFacility + configs: *id001 + name: Supplier3_552 + - class: SupplierFacility + configs: *id002 + name: Supplier1_552 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_552 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_552 + - class: SupplierFacility + configs: *id001 + name: Supplier3_553 + - class: SupplierFacility + configs: *id002 + name: Supplier1_553 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_553 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_553 + - class: SupplierFacility + configs: *id001 + name: Supplier3_554 + - class: SupplierFacility + configs: *id002 + name: Supplier1_554 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_554 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_554 + - class: SupplierFacility + configs: *id001 + name: Supplier3_555 + - class: SupplierFacility + configs: *id002 + name: Supplier1_555 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_555 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_555 + - class: SupplierFacility + configs: *id001 + name: Supplier3_556 + - class: SupplierFacility + configs: *id002 + name: Supplier1_556 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_556 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_556 + - class: SupplierFacility + configs: *id001 + name: Supplier3_557 + - class: SupplierFacility + configs: *id002 + name: Supplier1_557 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_557 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_557 + - class: SupplierFacility + configs: *id001 + name: Supplier3_558 + - class: SupplierFacility + configs: *id002 + name: Supplier1_558 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_558 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_558 + - class: SupplierFacility + configs: *id001 + name: Supplier3_559 + - class: SupplierFacility + configs: *id002 + name: Supplier1_559 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_559 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_559 + - class: SupplierFacility + configs: *id001 + name: Supplier3_560 + - class: SupplierFacility + configs: *id002 + name: Supplier1_560 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_560 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_560 + - class: SupplierFacility + configs: *id001 + name: Supplier3_561 + - class: SupplierFacility + configs: *id002 + name: Supplier1_561 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_561 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_561 + - class: SupplierFacility + configs: *id001 + name: Supplier3_562 + - class: SupplierFacility + configs: *id002 + name: Supplier1_562 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_562 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_562 + - class: SupplierFacility + configs: *id001 + name: Supplier3_563 + - class: SupplierFacility + configs: *id002 + name: Supplier1_563 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_563 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_563 + - class: SupplierFacility + configs: *id001 + name: Supplier3_564 + - class: SupplierFacility + configs: *id002 + name: Supplier1_564 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_564 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_564 + - class: SupplierFacility + configs: *id001 + name: Supplier3_565 + - class: SupplierFacility + configs: *id002 + name: Supplier1_565 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_565 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_565 + - class: SupplierFacility + configs: *id001 + name: Supplier3_566 + - class: SupplierFacility + configs: *id002 + name: Supplier1_566 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_566 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_566 + - class: SupplierFacility + configs: *id001 + name: Supplier3_567 + - class: SupplierFacility + configs: *id002 + name: Supplier1_567 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_567 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_567 + - class: SupplierFacility + configs: *id001 + name: Supplier3_568 + - class: SupplierFacility + configs: *id002 + name: Supplier1_568 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_568 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_568 + - class: SupplierFacility + configs: *id001 + name: Supplier3_569 + - class: SupplierFacility + configs: *id002 + name: Supplier1_569 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_569 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_569 + - class: SupplierFacility + configs: *id001 + name: Supplier3_570 + - class: SupplierFacility + configs: *id002 + name: Supplier1_570 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_570 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_570 + - class: SupplierFacility + configs: *id001 + name: Supplier3_571 + - class: SupplierFacility + configs: *id002 + name: Supplier1_571 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_571 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_571 + - class: SupplierFacility + configs: *id001 + name: Supplier3_572 + - class: SupplierFacility + configs: *id002 + name: Supplier1_572 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_572 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_572 + - class: SupplierFacility + configs: *id001 + name: Supplier3_573 + - class: SupplierFacility + configs: *id002 + name: Supplier1_573 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_573 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_573 + - class: SupplierFacility + configs: *id001 + name: Supplier3_574 + - class: SupplierFacility + configs: *id002 + name: Supplier1_574 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_574 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_574 + - class: SupplierFacility + configs: *id001 + name: Supplier3_575 + - class: SupplierFacility + configs: *id002 + name: Supplier1_575 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_575 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_575 + - class: SupplierFacility + configs: *id001 + name: Supplier3_576 + - class: SupplierFacility + configs: *id002 + name: Supplier1_576 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_576 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_576 + - class: SupplierFacility + configs: *id001 + name: Supplier3_577 + - class: SupplierFacility + configs: *id002 + name: Supplier1_577 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_577 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_577 + - class: SupplierFacility + configs: *id001 + name: Supplier3_578 + - class: SupplierFacility + configs: *id002 + name: Supplier1_578 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_578 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_578 + - class: SupplierFacility + configs: *id001 + name: Supplier3_579 + - class: SupplierFacility + configs: *id002 + name: Supplier1_579 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_579 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_579 + - class: SupplierFacility + configs: *id001 + name: Supplier3_580 + - class: SupplierFacility + configs: *id002 + name: Supplier1_580 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_580 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_580 + - class: SupplierFacility + configs: *id001 + name: Supplier3_581 + - class: SupplierFacility + configs: *id002 + name: Supplier1_581 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_581 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_581 + - class: SupplierFacility + configs: *id001 + name: Supplier3_582 + - class: SupplierFacility + configs: *id002 + name: Supplier1_582 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_582 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_582 + - class: SupplierFacility + configs: *id001 + name: Supplier3_583 + - class: SupplierFacility + configs: *id002 + name: Supplier1_583 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_583 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_583 + - class: SupplierFacility + configs: *id001 + name: Supplier3_584 + - class: SupplierFacility + configs: *id002 + name: Supplier1_584 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_584 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_584 + - class: SupplierFacility + configs: *id001 + name: Supplier3_585 + - class: SupplierFacility + configs: *id002 + name: Supplier1_585 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_585 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_585 + - class: SupplierFacility + configs: *id001 + name: Supplier3_586 + - class: SupplierFacility + configs: *id002 + name: Supplier1_586 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_586 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_586 + - class: SupplierFacility + configs: *id001 + name: Supplier3_587 + - class: SupplierFacility + configs: *id002 + name: Supplier1_587 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_587 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_587 + - class: SupplierFacility + configs: *id001 + name: Supplier3_588 + - class: SupplierFacility + configs: *id002 + name: Supplier1_588 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_588 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_588 + - class: SupplierFacility + configs: *id001 + name: Supplier3_589 + - class: SupplierFacility + configs: *id002 + name: Supplier1_589 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_589 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_589 + - class: SupplierFacility + configs: *id001 + name: Supplier3_590 + - class: SupplierFacility + configs: *id002 + name: Supplier1_590 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_590 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_590 + - class: SupplierFacility + configs: *id001 + name: Supplier3_591 + - class: SupplierFacility + configs: *id002 + name: Supplier1_591 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_591 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_591 + - class: SupplierFacility + configs: *id001 + name: Supplier3_592 + - class: SupplierFacility + configs: *id002 + name: Supplier1_592 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_592 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_592 + - class: SupplierFacility + configs: *id001 + name: Supplier3_593 + - class: SupplierFacility + configs: *id002 + name: Supplier1_593 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_593 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_593 + - class: SupplierFacility + configs: *id001 + name: Supplier3_594 + - class: SupplierFacility + configs: *id002 + name: Supplier1_594 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_594 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_594 + - class: SupplierFacility + configs: *id001 + name: Supplier3_595 + - class: SupplierFacility + configs: *id002 + name: Supplier1_595 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_595 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_595 + - class: SupplierFacility + configs: *id001 + name: Supplier3_596 + - class: SupplierFacility + configs: *id002 + name: Supplier1_596 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_596 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_596 + - class: SupplierFacility + configs: *id001 + name: Supplier3_597 + - class: SupplierFacility + configs: *id002 + name: Supplier1_597 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_597 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_597 + - class: SupplierFacility + configs: *id001 + name: Supplier3_598 + - class: SupplierFacility + configs: *id002 + name: Supplier1_598 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_598 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_598 + - class: SupplierFacility + configs: *id001 + name: Supplier3_599 + - class: SupplierFacility + configs: *id002 + name: Supplier1_599 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_599 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_599 + - class: SupplierFacility + configs: *id001 + name: Supplier3_600 + - class: SupplierFacility + configs: *id002 + name: Supplier1_600 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_600 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_600 + - class: SupplierFacility + configs: *id001 + name: Supplier3_601 + - class: SupplierFacility + configs: *id002 + name: Supplier1_601 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_601 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_601 + - class: SupplierFacility + configs: *id001 + name: Supplier3_602 + - class: SupplierFacility + configs: *id002 + name: Supplier1_602 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_602 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_602 + - class: SupplierFacility + configs: *id001 + name: Supplier3_603 + - class: SupplierFacility + configs: *id002 + name: Supplier1_603 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_603 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_603 + - class: SupplierFacility + configs: *id001 + name: Supplier3_604 + - class: SupplierFacility + configs: *id002 + name: Supplier1_604 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_604 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_604 + - class: SupplierFacility + configs: *id001 + name: Supplier3_605 + - class: SupplierFacility + configs: *id002 + name: Supplier1_605 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_605 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_605 + - class: SupplierFacility + configs: *id001 + name: Supplier3_606 + - class: SupplierFacility + configs: *id002 + name: Supplier1_606 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_606 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_606 + - class: SupplierFacility + configs: *id001 + name: Supplier3_607 + - class: SupplierFacility + configs: *id002 + name: Supplier1_607 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_607 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_607 + - class: SupplierFacility + configs: *id001 + name: Supplier3_608 + - class: SupplierFacility + configs: *id002 + name: Supplier1_608 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_608 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_608 + - class: SupplierFacility + configs: *id001 + name: Supplier3_609 + - class: SupplierFacility + configs: *id002 + name: Supplier1_609 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_609 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_609 + - class: SupplierFacility + configs: *id001 + name: Supplier3_610 + - class: SupplierFacility + configs: *id002 + name: Supplier1_610 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_610 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_610 + - class: SupplierFacility + configs: *id001 + name: Supplier3_611 + - class: SupplierFacility + configs: *id002 + name: Supplier1_611 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_611 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_611 + - class: SupplierFacility + configs: *id001 + name: Supplier3_612 + - class: SupplierFacility + configs: *id002 + name: Supplier1_612 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_612 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_612 + - class: SupplierFacility + configs: *id001 + name: Supplier3_613 + - class: SupplierFacility + configs: *id002 + name: Supplier1_613 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_613 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_613 + - class: SupplierFacility + configs: *id001 + name: Supplier3_614 + - class: SupplierFacility + configs: *id002 + name: Supplier1_614 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_614 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_614 + - class: SupplierFacility + configs: *id001 + name: Supplier3_615 + - class: SupplierFacility + configs: *id002 + name: Supplier1_615 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_615 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_615 + - class: SupplierFacility + configs: *id001 + name: Supplier3_616 + - class: SupplierFacility + configs: *id002 + name: Supplier1_616 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_616 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_616 + - class: SupplierFacility + configs: *id001 + name: Supplier3_617 + - class: SupplierFacility + configs: *id002 + name: Supplier1_617 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_617 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_617 + - class: SupplierFacility + configs: *id001 + name: Supplier3_618 + - class: SupplierFacility + configs: *id002 + name: Supplier1_618 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_618 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_618 + - class: SupplierFacility + configs: *id001 + name: Supplier3_619 + - class: SupplierFacility + configs: *id002 + name: Supplier1_619 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_619 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_619 + - class: SupplierFacility + configs: *id001 + name: Supplier3_620 + - class: SupplierFacility + configs: *id002 + name: Supplier1_620 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_620 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_620 + - class: SupplierFacility + configs: *id001 + name: Supplier3_621 + - class: SupplierFacility + configs: *id002 + name: Supplier1_621 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_621 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_621 + - class: SupplierFacility + configs: *id001 + name: Supplier3_622 + - class: SupplierFacility + configs: *id002 + name: Supplier1_622 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_622 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_622 + - class: SupplierFacility + configs: *id001 + name: Supplier3_623 + - class: SupplierFacility + configs: *id002 + name: Supplier1_623 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_623 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_623 + - class: SupplierFacility + configs: *id001 + name: Supplier3_624 + - class: SupplierFacility + configs: *id002 + name: Supplier1_624 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_624 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_624 + - class: SupplierFacility + configs: *id001 + name: Supplier3_625 + - class: SupplierFacility + configs: *id002 + name: Supplier1_625 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_625 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_625 + - class: SupplierFacility + configs: *id001 + name: Supplier3_626 + - class: SupplierFacility + configs: *id002 + name: Supplier1_626 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_626 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_626 + - class: SupplierFacility + configs: *id001 + name: Supplier3_627 + - class: SupplierFacility + configs: *id002 + name: Supplier1_627 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_627 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_627 + - class: SupplierFacility + configs: *id001 + name: Supplier3_628 + - class: SupplierFacility + configs: *id002 + name: Supplier1_628 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_628 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_628 + - class: SupplierFacility + configs: *id001 + name: Supplier3_629 + - class: SupplierFacility + configs: *id002 + name: Supplier1_629 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_629 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_629 + - class: SupplierFacility + configs: *id001 + name: Supplier3_630 + - class: SupplierFacility + configs: *id002 + name: Supplier1_630 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_630 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_630 + - class: SupplierFacility + configs: *id001 + name: Supplier3_631 + - class: SupplierFacility + configs: *id002 + name: Supplier1_631 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_631 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_631 + - class: SupplierFacility + configs: *id001 + name: Supplier3_632 + - class: SupplierFacility + configs: *id002 + name: Supplier1_632 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_632 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_632 + - class: SupplierFacility + configs: *id001 + name: Supplier3_633 + - class: SupplierFacility + configs: *id002 + name: Supplier1_633 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_633 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_633 + - class: SupplierFacility + configs: *id001 + name: Supplier3_634 + - class: SupplierFacility + configs: *id002 + name: Supplier1_634 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_634 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_634 + - class: SupplierFacility + configs: *id001 + name: Supplier3_635 + - class: SupplierFacility + configs: *id002 + name: Supplier1_635 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_635 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_635 + - class: SupplierFacility + configs: *id001 + name: Supplier3_636 + - class: SupplierFacility + configs: *id002 + name: Supplier1_636 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_636 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_636 + - class: SupplierFacility + configs: *id001 + name: Supplier3_637 + - class: SupplierFacility + configs: *id002 + name: Supplier1_637 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_637 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_637 + - class: SupplierFacility + configs: *id001 + name: Supplier3_638 + - class: SupplierFacility + configs: *id002 + name: Supplier1_638 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_638 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_638 + - class: SupplierFacility + configs: *id001 + name: Supplier3_639 + - class: SupplierFacility + configs: *id002 + name: Supplier1_639 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_639 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_639 + - class: SupplierFacility + configs: *id001 + name: Supplier3_640 + - class: SupplierFacility + configs: *id002 + name: Supplier1_640 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_640 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_640 + - class: SupplierFacility + configs: *id001 + name: Supplier3_641 + - class: SupplierFacility + configs: *id002 + name: Supplier1_641 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_641 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_641 + - class: SupplierFacility + configs: *id001 + name: Supplier3_642 + - class: SupplierFacility + configs: *id002 + name: Supplier1_642 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_642 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_642 + - class: SupplierFacility + configs: *id001 + name: Supplier3_643 + - class: SupplierFacility + configs: *id002 + name: Supplier1_643 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_643 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_643 + - class: SupplierFacility + configs: *id001 + name: Supplier3_644 + - class: SupplierFacility + configs: *id002 + name: Supplier1_644 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_644 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_644 + - class: SupplierFacility + configs: *id001 + name: Supplier3_645 + - class: SupplierFacility + configs: *id002 + name: Supplier1_645 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_645 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_645 + - class: SupplierFacility + configs: *id001 + name: Supplier3_646 + - class: SupplierFacility + configs: *id002 + name: Supplier1_646 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_646 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_646 + - class: SupplierFacility + configs: *id001 + name: Supplier3_647 + - class: SupplierFacility + configs: *id002 + name: Supplier1_647 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_647 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_647 + - class: SupplierFacility + configs: *id001 + name: Supplier3_648 + - class: SupplierFacility + configs: *id002 + name: Supplier1_648 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_648 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_648 + - class: SupplierFacility + configs: *id001 + name: Supplier3_649 + - class: SupplierFacility + configs: *id002 + name: Supplier1_649 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_649 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_649 + - class: SupplierFacility + configs: *id001 + name: Supplier3_650 + - class: SupplierFacility + configs: *id002 + name: Supplier1_650 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_650 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_650 + - class: SupplierFacility + configs: *id001 + name: Supplier3_651 + - class: SupplierFacility + configs: *id002 + name: Supplier1_651 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_651 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_651 + - class: SupplierFacility + configs: *id001 + name: Supplier3_652 + - class: SupplierFacility + configs: *id002 + name: Supplier1_652 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_652 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_652 + - class: SupplierFacility + configs: *id001 + name: Supplier3_653 + - class: SupplierFacility + configs: *id002 + name: Supplier1_653 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_653 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_653 + - class: SupplierFacility + configs: *id001 + name: Supplier3_654 + - class: SupplierFacility + configs: *id002 + name: Supplier1_654 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_654 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_654 + - class: SupplierFacility + configs: *id001 + name: Supplier3_655 + - class: SupplierFacility + configs: *id002 + name: Supplier1_655 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_655 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_655 + - class: SupplierFacility + configs: *id001 + name: Supplier3_656 + - class: SupplierFacility + configs: *id002 + name: Supplier1_656 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_656 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_656 + - class: SupplierFacility + configs: *id001 + name: Supplier3_657 + - class: SupplierFacility + configs: *id002 + name: Supplier1_657 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_657 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_657 + - class: SupplierFacility + configs: *id001 + name: Supplier3_658 + - class: SupplierFacility + configs: *id002 + name: Supplier1_658 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_658 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_658 + - class: SupplierFacility + configs: *id001 + name: Supplier3_659 + - class: SupplierFacility + configs: *id002 + name: Supplier1_659 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_659 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_659 + - class: SupplierFacility + configs: *id001 + name: Supplier3_660 + - class: SupplierFacility + configs: *id002 + name: Supplier1_660 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_660 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_660 + - class: SupplierFacility + configs: *id001 + name: Supplier3_661 + - class: SupplierFacility + configs: *id002 + name: Supplier1_661 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_661 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_661 + - class: SupplierFacility + configs: *id001 + name: Supplier3_662 + - class: SupplierFacility + configs: *id002 + name: Supplier1_662 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_662 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_662 + - class: SupplierFacility + configs: *id001 + name: Supplier3_663 + - class: SupplierFacility + configs: *id002 + name: Supplier1_663 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_663 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_663 + - class: SupplierFacility + configs: *id001 + name: Supplier3_664 + - class: SupplierFacility + configs: *id002 + name: Supplier1_664 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_664 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_664 + - class: SupplierFacility + configs: *id001 + name: Supplier3_665 + - class: SupplierFacility + configs: *id002 + name: Supplier1_665 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_665 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_665 + - class: SupplierFacility + configs: *id001 + name: Supplier3_666 + - class: SupplierFacility + configs: *id002 + name: Supplier1_666 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_666 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_666 + - class: SupplierFacility + configs: *id001 + name: Supplier3_667 + - class: SupplierFacility + configs: *id002 + name: Supplier1_667 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_667 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_667 + - class: SupplierFacility + configs: *id001 + name: Supplier3_668 + - class: SupplierFacility + configs: *id002 + name: Supplier1_668 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_668 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_668 + - class: SupplierFacility + configs: *id001 + name: Supplier3_669 + - class: SupplierFacility + configs: *id002 + name: Supplier1_669 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_669 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_669 + - class: SupplierFacility + configs: *id001 + name: Supplier3_670 + - class: SupplierFacility + configs: *id002 + name: Supplier1_670 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_670 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_670 + - class: SupplierFacility + configs: *id001 + name: Supplier3_671 + - class: SupplierFacility + configs: *id002 + name: Supplier1_671 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_671 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_671 + - class: SupplierFacility + configs: *id001 + name: Supplier3_672 + - class: SupplierFacility + configs: *id002 + name: Supplier1_672 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_672 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_672 + - class: SupplierFacility + configs: *id001 + name: Supplier3_673 + - class: SupplierFacility + configs: *id002 + name: Supplier1_673 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_673 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_673 + - class: SupplierFacility + configs: *id001 + name: Supplier3_674 + - class: SupplierFacility + configs: *id002 + name: Supplier1_674 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_674 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_674 + - class: SupplierFacility + configs: *id001 + name: Supplier3_675 + - class: SupplierFacility + configs: *id002 + name: Supplier1_675 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_675 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_675 + - class: SupplierFacility + configs: *id001 + name: Supplier3_676 + - class: SupplierFacility + configs: *id002 + name: Supplier1_676 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_676 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_676 + - class: SupplierFacility + configs: *id001 + name: Supplier3_677 + - class: SupplierFacility + configs: *id002 + name: Supplier1_677 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_677 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_677 + - class: SupplierFacility + configs: *id001 + name: Supplier3_678 + - class: SupplierFacility + configs: *id002 + name: Supplier1_678 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_678 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_678 + - class: SupplierFacility + configs: *id001 + name: Supplier3_679 + - class: SupplierFacility + configs: *id002 + name: Supplier1_679 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_679 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_679 + - class: SupplierFacility + configs: *id001 + name: Supplier3_680 + - class: SupplierFacility + configs: *id002 + name: Supplier1_680 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_680 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_680 + - class: SupplierFacility + configs: *id001 + name: Supplier3_681 + - class: SupplierFacility + configs: *id002 + name: Supplier1_681 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_681 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_681 + - class: SupplierFacility + configs: *id001 + name: Supplier3_682 + - class: SupplierFacility + configs: *id002 + name: Supplier1_682 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_682 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_682 + - class: SupplierFacility + configs: *id001 + name: Supplier3_683 + - class: SupplierFacility + configs: *id002 + name: Supplier1_683 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_683 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_683 + - class: SupplierFacility + configs: *id001 + name: Supplier3_684 + - class: SupplierFacility + configs: *id002 + name: Supplier1_684 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_684 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_684 + - class: SupplierFacility + configs: *id001 + name: Supplier3_685 + - class: SupplierFacility + configs: *id002 + name: Supplier1_685 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_685 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_685 + - class: SupplierFacility + configs: *id001 + name: Supplier3_686 + - class: SupplierFacility + configs: *id002 + name: Supplier1_686 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_686 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_686 + - class: SupplierFacility + configs: *id001 + name: Supplier3_687 + - class: SupplierFacility + configs: *id002 + name: Supplier1_687 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_687 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_687 + - class: SupplierFacility + configs: *id001 + name: Supplier3_688 + - class: SupplierFacility + configs: *id002 + name: Supplier1_688 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_688 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_688 + - class: SupplierFacility + configs: *id001 + name: Supplier3_689 + - class: SupplierFacility + configs: *id002 + name: Supplier1_689 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_689 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_689 + - class: SupplierFacility + configs: *id001 + name: Supplier3_690 + - class: SupplierFacility + configs: *id002 + name: Supplier1_690 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_690 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_690 + - class: SupplierFacility + configs: *id001 + name: Supplier3_691 + - class: SupplierFacility + configs: *id002 + name: Supplier1_691 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_691 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_691 + - class: SupplierFacility + configs: *id001 + name: Supplier3_692 + - class: SupplierFacility + configs: *id002 + name: Supplier1_692 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_692 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_692 + - class: SupplierFacility + configs: *id001 + name: Supplier3_693 + - class: SupplierFacility + configs: *id002 + name: Supplier1_693 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_693 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_693 + - class: SupplierFacility + configs: *id001 + name: Supplier3_694 + - class: SupplierFacility + configs: *id002 + name: Supplier1_694 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_694 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_694 + - class: SupplierFacility + configs: *id001 + name: Supplier3_695 + - class: SupplierFacility + configs: *id002 + name: Supplier1_695 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_695 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_695 + - class: SupplierFacility + configs: *id001 + name: Supplier3_696 + - class: SupplierFacility + configs: *id002 + name: Supplier1_696 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_696 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_696 + - class: SupplierFacility + configs: *id001 + name: Supplier3_697 + - class: SupplierFacility + configs: *id002 + name: Supplier1_697 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_697 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_697 + - class: SupplierFacility + configs: *id001 + name: Supplier3_698 + - class: SupplierFacility + configs: *id002 + name: Supplier1_698 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_698 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_698 + - class: SupplierFacility + configs: *id001 + name: Supplier3_699 + - class: SupplierFacility + configs: *id002 + name: Supplier1_699 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_699 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_699 + - class: SupplierFacility + configs: *id001 + name: Supplier3_700 + - class: SupplierFacility + configs: *id002 + name: Supplier1_700 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_700 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_700 + - class: SupplierFacility + configs: *id001 + name: Supplier3_701 + - class: SupplierFacility + configs: *id002 + name: Supplier1_701 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_701 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_701 + - class: SupplierFacility + configs: *id001 + name: Supplier3_702 + - class: SupplierFacility + configs: *id002 + name: Supplier1_702 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_702 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_702 + - class: SupplierFacility + configs: *id001 + name: Supplier3_703 + - class: SupplierFacility + configs: *id002 + name: Supplier1_703 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_703 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_703 + - class: SupplierFacility + configs: *id001 + name: Supplier3_704 + - class: SupplierFacility + configs: *id002 + name: Supplier1_704 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_704 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_704 + - class: SupplierFacility + configs: *id001 + name: Supplier3_705 + - class: SupplierFacility + configs: *id002 + name: Supplier1_705 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_705 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_705 + - class: SupplierFacility + configs: *id001 + name: Supplier3_706 + - class: SupplierFacility + configs: *id002 + name: Supplier1_706 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_706 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_706 + - class: SupplierFacility + configs: *id001 + name: Supplier3_707 + - class: SupplierFacility + configs: *id002 + name: Supplier1_707 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_707 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_707 + - class: SupplierFacility + configs: *id001 + name: Supplier3_708 + - class: SupplierFacility + configs: *id002 + name: Supplier1_708 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_708 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_708 + - class: SupplierFacility + configs: *id001 + name: Supplier3_709 + - class: SupplierFacility + configs: *id002 + name: Supplier1_709 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_709 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_709 + - class: SupplierFacility + configs: *id001 + name: Supplier3_710 + - class: SupplierFacility + configs: *id002 + name: Supplier1_710 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_710 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_710 + - class: SupplierFacility + configs: *id001 + name: Supplier3_711 + - class: SupplierFacility + configs: *id002 + name: Supplier1_711 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_711 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_711 + - class: SupplierFacility + configs: *id001 + name: Supplier3_712 + - class: SupplierFacility + configs: *id002 + name: Supplier1_712 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_712 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_712 + - class: SupplierFacility + configs: *id001 + name: Supplier3_713 + - class: SupplierFacility + configs: *id002 + name: Supplier1_713 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_713 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_713 + - class: SupplierFacility + configs: *id001 + name: Supplier3_714 + - class: SupplierFacility + configs: *id002 + name: Supplier1_714 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_714 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_714 + - class: SupplierFacility + configs: *id001 + name: Supplier3_715 + - class: SupplierFacility + configs: *id002 + name: Supplier1_715 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_715 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_715 + - class: SupplierFacility + configs: *id001 + name: Supplier3_716 + - class: SupplierFacility + configs: *id002 + name: Supplier1_716 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_716 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_716 + - class: SupplierFacility + configs: *id001 + name: Supplier3_717 + - class: SupplierFacility + configs: *id002 + name: Supplier1_717 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_717 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_717 + - class: SupplierFacility + configs: *id001 + name: Supplier3_718 + - class: SupplierFacility + configs: *id002 + name: Supplier1_718 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_718 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_718 + - class: SupplierFacility + configs: *id001 + name: Supplier3_719 + - class: SupplierFacility + configs: *id002 + name: Supplier1_719 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_719 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_719 + - class: SupplierFacility + configs: *id001 + name: Supplier3_720 + - class: SupplierFacility + configs: *id002 + name: Supplier1_720 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_720 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_720 + - class: SupplierFacility + configs: *id001 + name: Supplier3_721 + - class: SupplierFacility + configs: *id002 + name: Supplier1_721 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_721 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_721 + - class: SupplierFacility + configs: *id001 + name: Supplier3_722 + - class: SupplierFacility + configs: *id002 + name: Supplier1_722 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_722 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_722 + - class: SupplierFacility + configs: *id001 + name: Supplier3_723 + - class: SupplierFacility + configs: *id002 + name: Supplier1_723 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_723 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_723 + - class: SupplierFacility + configs: *id001 + name: Supplier3_724 + - class: SupplierFacility + configs: *id002 + name: Supplier1_724 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_724 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_724 + - class: SupplierFacility + configs: *id001 + name: Supplier3_725 + - class: SupplierFacility + configs: *id002 + name: Supplier1_725 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_725 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_725 + - class: SupplierFacility + configs: *id001 + name: Supplier3_726 + - class: SupplierFacility + configs: *id002 + name: Supplier1_726 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_726 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_726 + - class: SupplierFacility + configs: *id001 + name: Supplier3_727 + - class: SupplierFacility + configs: *id002 + name: Supplier1_727 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_727 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_727 + - class: SupplierFacility + configs: *id001 + name: Supplier3_728 + - class: SupplierFacility + configs: *id002 + name: Supplier1_728 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_728 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_728 + - class: SupplierFacility + configs: *id001 + name: Supplier3_729 + - class: SupplierFacility + configs: *id002 + name: Supplier1_729 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_729 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_729 + - class: SupplierFacility + configs: *id001 + name: Supplier3_730 + - class: SupplierFacility + configs: *id002 + name: Supplier1_730 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_730 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_730 + - class: SupplierFacility + configs: *id001 + name: Supplier3_731 + - class: SupplierFacility + configs: *id002 + name: Supplier1_731 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_731 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_731 + - class: SupplierFacility + configs: *id001 + name: Supplier3_732 + - class: SupplierFacility + configs: *id002 + name: Supplier1_732 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_732 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_732 + - class: SupplierFacility + configs: *id001 + name: Supplier3_733 + - class: SupplierFacility + configs: *id002 + name: Supplier1_733 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_733 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_733 + - class: SupplierFacility + configs: *id001 + name: Supplier3_734 + - class: SupplierFacility + configs: *id002 + name: Supplier1_734 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_734 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_734 + - class: SupplierFacility + configs: *id001 + name: Supplier3_735 + - class: SupplierFacility + configs: *id002 + name: Supplier1_735 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_735 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_735 + - class: SupplierFacility + configs: *id001 + name: Supplier3_736 + - class: SupplierFacility + configs: *id002 + name: Supplier1_736 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_736 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_736 + - class: SupplierFacility + configs: *id001 + name: Supplier3_737 + - class: SupplierFacility + configs: *id002 + name: Supplier1_737 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_737 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_737 + - class: SupplierFacility + configs: *id001 + name: Supplier3_738 + - class: SupplierFacility + configs: *id002 + name: Supplier1_738 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_738 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_738 + - class: SupplierFacility + configs: *id001 + name: Supplier3_739 + - class: SupplierFacility + configs: *id002 + name: Supplier1_739 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_739 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_739 + - class: SupplierFacility + configs: *id001 + name: Supplier3_740 + - class: SupplierFacility + configs: *id002 + name: Supplier1_740 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_740 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_740 + - class: SupplierFacility + configs: *id001 + name: Supplier3_741 + - class: SupplierFacility + configs: *id002 + name: Supplier1_741 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_741 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_741 + - class: SupplierFacility + configs: *id001 + name: Supplier3_742 + - class: SupplierFacility + configs: *id002 + name: Supplier1_742 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_742 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_742 + - class: SupplierFacility + configs: *id001 + name: Supplier3_743 + - class: SupplierFacility + configs: *id002 + name: Supplier1_743 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_743 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_743 + - class: SupplierFacility + configs: *id001 + name: Supplier3_744 + - class: SupplierFacility + configs: *id002 + name: Supplier1_744 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_744 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_744 + - class: SupplierFacility + configs: *id001 + name: Supplier3_745 + - class: SupplierFacility + configs: *id002 + name: Supplier1_745 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_745 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_745 + - class: SupplierFacility + configs: *id001 + name: Supplier3_746 + - class: SupplierFacility + configs: *id002 + name: Supplier1_746 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_746 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_746 + - class: SupplierFacility + configs: *id001 + name: Supplier3_747 + - class: SupplierFacility + configs: *id002 + name: Supplier1_747 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_747 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_747 + - class: SupplierFacility + configs: *id001 + name: Supplier3_748 + - class: SupplierFacility + configs: *id002 + name: Supplier1_748 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_748 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_748 + - class: SupplierFacility + configs: *id001 + name: Supplier3_749 + - class: SupplierFacility + configs: *id002 + name: Supplier1_749 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_749 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_749 + - class: SupplierFacility + configs: *id001 + name: Supplier3_750 + - class: SupplierFacility + configs: *id002 + name: Supplier1_750 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_750 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_750 + - class: SupplierFacility + configs: *id001 + name: Supplier3_751 + - class: SupplierFacility + configs: *id002 + name: Supplier1_751 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_751 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_751 + - class: SupplierFacility + configs: *id001 + name: Supplier3_752 + - class: SupplierFacility + configs: *id002 + name: Supplier1_752 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_752 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_752 + - class: SupplierFacility + configs: *id001 + name: Supplier3_753 + - class: SupplierFacility + configs: *id002 + name: Supplier1_753 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_753 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_753 + - class: SupplierFacility + configs: *id001 + name: Supplier3_754 + - class: SupplierFacility + configs: *id002 + name: Supplier1_754 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_754 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_754 + - class: SupplierFacility + configs: *id001 + name: Supplier3_755 + - class: SupplierFacility + configs: *id002 + name: Supplier1_755 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_755 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_755 + - class: SupplierFacility + configs: *id001 + name: Supplier3_756 + - class: SupplierFacility + configs: *id002 + name: Supplier1_756 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_756 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_756 + - class: SupplierFacility + configs: *id001 + name: Supplier3_757 + - class: SupplierFacility + configs: *id002 + name: Supplier1_757 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_757 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_757 + - class: SupplierFacility + configs: *id001 + name: Supplier3_758 + - class: SupplierFacility + configs: *id002 + name: Supplier1_758 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_758 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_758 + - class: SupplierFacility + configs: *id001 + name: Supplier3_759 + - class: SupplierFacility + configs: *id002 + name: Supplier1_759 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_759 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_759 + - class: SupplierFacility + configs: *id001 + name: Supplier3_760 + - class: SupplierFacility + configs: *id002 + name: Supplier1_760 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_760 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_760 + - class: SupplierFacility + configs: *id001 + name: Supplier3_761 + - class: SupplierFacility + configs: *id002 + name: Supplier1_761 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_761 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_761 + - class: SupplierFacility + configs: *id001 + name: Supplier3_762 + - class: SupplierFacility + configs: *id002 + name: Supplier1_762 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_762 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_762 + - class: SupplierFacility + configs: *id001 + name: Supplier3_763 + - class: SupplierFacility + configs: *id002 + name: Supplier1_763 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_763 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_763 + - class: SupplierFacility + configs: *id001 + name: Supplier3_764 + - class: SupplierFacility + configs: *id002 + name: Supplier1_764 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_764 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_764 + - class: SupplierFacility + configs: *id001 + name: Supplier3_765 + - class: SupplierFacility + configs: *id002 + name: Supplier1_765 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_765 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_765 + - class: SupplierFacility + configs: *id001 + name: Supplier3_766 + - class: SupplierFacility + configs: *id002 + name: Supplier1_766 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_766 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_766 + - class: SupplierFacility + configs: *id001 + name: Supplier3_767 + - class: SupplierFacility + configs: *id002 + name: Supplier1_767 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_767 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_767 + - class: SupplierFacility + configs: *id001 + name: Supplier3_768 + - class: SupplierFacility + configs: *id002 + name: Supplier1_768 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_768 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_768 + - class: SupplierFacility + configs: *id001 + name: Supplier3_769 + - class: SupplierFacility + configs: *id002 + name: Supplier1_769 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_769 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_769 + - class: SupplierFacility + configs: *id001 + name: Supplier3_770 + - class: SupplierFacility + configs: *id002 + name: Supplier1_770 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_770 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_770 + - class: SupplierFacility + configs: *id001 + name: Supplier3_771 + - class: SupplierFacility + configs: *id002 + name: Supplier1_771 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_771 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_771 + - class: SupplierFacility + configs: *id001 + name: Supplier3_772 + - class: SupplierFacility + configs: *id002 + name: Supplier1_772 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_772 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_772 + - class: SupplierFacility + configs: *id001 + name: Supplier3_773 + - class: SupplierFacility + configs: *id002 + name: Supplier1_773 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_773 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_773 + - class: SupplierFacility + configs: *id001 + name: Supplier3_774 + - class: SupplierFacility + configs: *id002 + name: Supplier1_774 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_774 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_774 + - class: SupplierFacility + configs: *id001 + name: Supplier3_775 + - class: SupplierFacility + configs: *id002 + name: Supplier1_775 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_775 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_775 + - class: SupplierFacility + configs: *id001 + name: Supplier3_776 + - class: SupplierFacility + configs: *id002 + name: Supplier1_776 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_776 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_776 + - class: SupplierFacility + configs: *id001 + name: Supplier3_777 + - class: SupplierFacility + configs: *id002 + name: Supplier1_777 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_777 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_777 + - class: SupplierFacility + configs: *id001 + name: Supplier3_778 + - class: SupplierFacility + configs: *id002 + name: Supplier1_778 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_778 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_778 + - class: SupplierFacility + configs: *id001 + name: Supplier3_779 + - class: SupplierFacility + configs: *id002 + name: Supplier1_779 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_779 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_779 + - class: SupplierFacility + configs: *id001 + name: Supplier3_780 + - class: SupplierFacility + configs: *id002 + name: Supplier1_780 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_780 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_780 + - class: SupplierFacility + configs: *id001 + name: Supplier3_781 + - class: SupplierFacility + configs: *id002 + name: Supplier1_781 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_781 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_781 + - class: SupplierFacility + configs: *id001 + name: Supplier3_782 + - class: SupplierFacility + configs: *id002 + name: Supplier1_782 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_782 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_782 + - class: SupplierFacility + configs: *id001 + name: Supplier3_783 + - class: SupplierFacility + configs: *id002 + name: Supplier1_783 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_783 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_783 + - class: SupplierFacility + configs: *id001 + name: Supplier3_784 + - class: SupplierFacility + configs: *id002 + name: Supplier1_784 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_784 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_784 + - class: SupplierFacility + configs: *id001 + name: Supplier3_785 + - class: SupplierFacility + configs: *id002 + name: Supplier1_785 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_785 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_785 + - class: SupplierFacility + configs: *id001 + name: Supplier3_786 + - class: SupplierFacility + configs: *id002 + name: Supplier1_786 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_786 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_786 + - class: SupplierFacility + configs: *id001 + name: Supplier3_787 + - class: SupplierFacility + configs: *id002 + name: Supplier1_787 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_787 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_787 + - class: SupplierFacility + configs: *id001 + name: Supplier3_788 + - class: SupplierFacility + configs: *id002 + name: Supplier1_788 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_788 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_788 + - class: SupplierFacility + configs: *id001 + name: Supplier3_789 + - class: SupplierFacility + configs: *id002 + name: Supplier1_789 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_789 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_789 + - class: SupplierFacility + configs: *id001 + name: Supplier3_790 + - class: SupplierFacility + configs: *id002 + name: Supplier1_790 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_790 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_790 + - class: SupplierFacility + configs: *id001 + name: Supplier3_791 + - class: SupplierFacility + configs: *id002 + name: Supplier1_791 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_791 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_791 + - class: SupplierFacility + configs: *id001 + name: Supplier3_792 + - class: SupplierFacility + configs: *id002 + name: Supplier1_792 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_792 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_792 + - class: SupplierFacility + configs: *id001 + name: Supplier3_793 + - class: SupplierFacility + configs: *id002 + name: Supplier1_793 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_793 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_793 + - class: SupplierFacility + configs: *id001 + name: Supplier3_794 + - class: SupplierFacility + configs: *id002 + name: Supplier1_794 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_794 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_794 + - class: SupplierFacility + configs: *id001 + name: Supplier3_795 + - class: SupplierFacility + configs: *id002 + name: Supplier1_795 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_795 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_795 + - class: SupplierFacility + configs: *id001 + name: Supplier3_796 + - class: SupplierFacility + configs: *id002 + name: Supplier1_796 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_796 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_796 + - class: SupplierFacility + configs: *id001 + name: Supplier3_797 + - class: SupplierFacility + configs: *id002 + name: Supplier1_797 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_797 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_797 + - class: SupplierFacility + configs: *id001 + name: Supplier3_798 + - class: SupplierFacility + configs: *id002 + name: Supplier1_798 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_798 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_798 + - class: SupplierFacility + configs: *id001 + name: Supplier3_799 + - class: SupplierFacility + configs: *id002 + name: Supplier1_799 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_799 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_799 + - class: SupplierFacility + configs: *id001 + name: Supplier3_800 + - class: SupplierFacility + configs: *id002 + name: Supplier1_800 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_800 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_800 + - class: SupplierFacility + configs: *id001 + name: Supplier3_801 + - class: SupplierFacility + configs: *id002 + name: Supplier1_801 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_801 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_801 + - class: SupplierFacility + configs: *id001 + name: Supplier3_802 + - class: SupplierFacility + configs: *id002 + name: Supplier1_802 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_802 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_802 + - class: SupplierFacility + configs: *id001 + name: Supplier3_803 + - class: SupplierFacility + configs: *id002 + name: Supplier1_803 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_803 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_803 + - class: SupplierFacility + configs: *id001 + name: Supplier3_804 + - class: SupplierFacility + configs: *id002 + name: Supplier1_804 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_804 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_804 + - class: SupplierFacility + configs: *id001 + name: Supplier3_805 + - class: SupplierFacility + configs: *id002 + name: Supplier1_805 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_805 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_805 + - class: SupplierFacility + configs: *id001 + name: Supplier3_806 + - class: SupplierFacility + configs: *id002 + name: Supplier1_806 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_806 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_806 + - class: SupplierFacility + configs: *id001 + name: Supplier3_807 + - class: SupplierFacility + configs: *id002 + name: Supplier1_807 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_807 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_807 + - class: SupplierFacility + configs: *id001 + name: Supplier3_808 + - class: SupplierFacility + configs: *id002 + name: Supplier1_808 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_808 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_808 + - class: SupplierFacility + configs: *id001 + name: Supplier3_809 + - class: SupplierFacility + configs: *id002 + name: Supplier1_809 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_809 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_809 + - class: SupplierFacility + configs: *id001 + name: Supplier3_810 + - class: SupplierFacility + configs: *id002 + name: Supplier1_810 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_810 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_810 + - class: SupplierFacility + configs: *id001 + name: Supplier3_811 + - class: SupplierFacility + configs: *id002 + name: Supplier1_811 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_811 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_811 + - class: SupplierFacility + configs: *id001 + name: Supplier3_812 + - class: SupplierFacility + configs: *id002 + name: Supplier1_812 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_812 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_812 + - class: SupplierFacility + configs: *id001 + name: Supplier3_813 + - class: SupplierFacility + configs: *id002 + name: Supplier1_813 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_813 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_813 + - class: SupplierFacility + configs: *id001 + name: Supplier3_814 + - class: SupplierFacility + configs: *id002 + name: Supplier1_814 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_814 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_814 + - class: SupplierFacility + configs: *id001 + name: Supplier3_815 + - class: SupplierFacility + configs: *id002 + name: Supplier1_815 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_815 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_815 + - class: SupplierFacility + configs: *id001 + name: Supplier3_816 + - class: SupplierFacility + configs: *id002 + name: Supplier1_816 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_816 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_816 + - class: SupplierFacility + configs: *id001 + name: Supplier3_817 + - class: SupplierFacility + configs: *id002 + name: Supplier1_817 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_817 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_817 + - class: SupplierFacility + configs: *id001 + name: Supplier3_818 + - class: SupplierFacility + configs: *id002 + name: Supplier1_818 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_818 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_818 + - class: SupplierFacility + configs: *id001 + name: Supplier3_819 + - class: SupplierFacility + configs: *id002 + name: Supplier1_819 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_819 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_819 + - class: SupplierFacility + configs: *id001 + name: Supplier3_820 + - class: SupplierFacility + configs: *id002 + name: Supplier1_820 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_820 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_820 + - class: SupplierFacility + configs: *id001 + name: Supplier3_821 + - class: SupplierFacility + configs: *id002 + name: Supplier1_821 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_821 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_821 + - class: SupplierFacility + configs: *id001 + name: Supplier3_822 + - class: SupplierFacility + configs: *id002 + name: Supplier1_822 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_822 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_822 + - class: SupplierFacility + configs: *id001 + name: Supplier3_823 + - class: SupplierFacility + configs: *id002 + name: Supplier1_823 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_823 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_823 + - class: SupplierFacility + configs: *id001 + name: Supplier3_824 + - class: SupplierFacility + configs: *id002 + name: Supplier1_824 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_824 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_824 + - class: SupplierFacility + configs: *id001 + name: Supplier3_825 + - class: SupplierFacility + configs: *id002 + name: Supplier1_825 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_825 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_825 + - class: SupplierFacility + configs: *id001 + name: Supplier3_826 + - class: SupplierFacility + configs: *id002 + name: Supplier1_826 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_826 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_826 + - class: SupplierFacility + configs: *id001 + name: Supplier3_827 + - class: SupplierFacility + configs: *id002 + name: Supplier1_827 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_827 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_827 + - class: SupplierFacility + configs: *id001 + name: Supplier3_828 + - class: SupplierFacility + configs: *id002 + name: Supplier1_828 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_828 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_828 + - class: SupplierFacility + configs: *id001 + name: Supplier3_829 + - class: SupplierFacility + configs: *id002 + name: Supplier1_829 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_829 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_829 + - class: SupplierFacility + configs: *id001 + name: Supplier3_830 + - class: SupplierFacility + configs: *id002 + name: Supplier1_830 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_830 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_830 + - class: SupplierFacility + configs: *id001 + name: Supplier3_831 + - class: SupplierFacility + configs: *id002 + name: Supplier1_831 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_831 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_831 + - class: SupplierFacility + configs: *id001 + name: Supplier3_832 + - class: SupplierFacility + configs: *id002 + name: Supplier1_832 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_832 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_832 + - class: SupplierFacility + configs: *id001 + name: Supplier3_833 + - class: SupplierFacility + configs: *id002 + name: Supplier1_833 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_833 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_833 + - class: SupplierFacility + configs: *id001 + name: Supplier3_834 + - class: SupplierFacility + configs: *id002 + name: Supplier1_834 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_834 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_834 + - class: SupplierFacility + configs: *id001 + name: Supplier3_835 + - class: SupplierFacility + configs: *id002 + name: Supplier1_835 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_835 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_835 + - class: SupplierFacility + configs: *id001 + name: Supplier3_836 + - class: SupplierFacility + configs: *id002 + name: Supplier1_836 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_836 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_836 + - class: SupplierFacility + configs: *id001 + name: Supplier3_837 + - class: SupplierFacility + configs: *id002 + name: Supplier1_837 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_837 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_837 + - class: SupplierFacility + configs: *id001 + name: Supplier3_838 + - class: SupplierFacility + configs: *id002 + name: Supplier1_838 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_838 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_838 + - class: SupplierFacility + configs: *id001 + name: Supplier3_839 + - class: SupplierFacility + configs: *id002 + name: Supplier1_839 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_839 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_839 + - class: SupplierFacility + configs: *id001 + name: Supplier3_840 + - class: SupplierFacility + configs: *id002 + name: Supplier1_840 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_840 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_840 + - class: SupplierFacility + configs: *id001 + name: Supplier3_841 + - class: SupplierFacility + configs: *id002 + name: Supplier1_841 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_841 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_841 + - class: SupplierFacility + configs: *id001 + name: Supplier3_842 + - class: SupplierFacility + configs: *id002 + name: Supplier1_842 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_842 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_842 + - class: SupplierFacility + configs: *id001 + name: Supplier3_843 + - class: SupplierFacility + configs: *id002 + name: Supplier1_843 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_843 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_843 + - class: SupplierFacility + configs: *id001 + name: Supplier3_844 + - class: SupplierFacility + configs: *id002 + name: Supplier1_844 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_844 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_844 + - class: SupplierFacility + configs: *id001 + name: Supplier3_845 + - class: SupplierFacility + configs: *id002 + name: Supplier1_845 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_845 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_845 + - class: SupplierFacility + configs: *id001 + name: Supplier3_846 + - class: SupplierFacility + configs: *id002 + name: Supplier1_846 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_846 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_846 + - class: SupplierFacility + configs: *id001 + name: Supplier3_847 + - class: SupplierFacility + configs: *id002 + name: Supplier1_847 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_847 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_847 + - class: SupplierFacility + configs: *id001 + name: Supplier3_848 + - class: SupplierFacility + configs: *id002 + name: Supplier1_848 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_848 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_848 + - class: SupplierFacility + configs: *id001 + name: Supplier3_849 + - class: SupplierFacility + configs: *id002 + name: Supplier1_849 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_849 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_849 + - class: SupplierFacility + configs: *id001 + name: Supplier3_850 + - class: SupplierFacility + configs: *id002 + name: Supplier1_850 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_850 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_850 + - class: SupplierFacility + configs: *id001 + name: Supplier3_851 + - class: SupplierFacility + configs: *id002 + name: Supplier1_851 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_851 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_851 + - class: SupplierFacility + configs: *id001 + name: Supplier3_852 + - class: SupplierFacility + configs: *id002 + name: Supplier1_852 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_852 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_852 + - class: SupplierFacility + configs: *id001 + name: Supplier3_853 + - class: SupplierFacility + configs: *id002 + name: Supplier1_853 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_853 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_853 + - class: SupplierFacility + configs: *id001 + name: Supplier3_854 + - class: SupplierFacility + configs: *id002 + name: Supplier1_854 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_854 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_854 + - class: SupplierFacility + configs: *id001 + name: Supplier3_855 + - class: SupplierFacility + configs: *id002 + name: Supplier1_855 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_855 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_855 + - class: SupplierFacility + configs: *id001 + name: Supplier3_856 + - class: SupplierFacility + configs: *id002 + name: Supplier1_856 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_856 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_856 + - class: SupplierFacility + configs: *id001 + name: Supplier3_857 + - class: SupplierFacility + configs: *id002 + name: Supplier1_857 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_857 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_857 + - class: SupplierFacility + configs: *id001 + name: Supplier3_858 + - class: SupplierFacility + configs: *id002 + name: Supplier1_858 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_858 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_858 + - class: SupplierFacility + configs: *id001 + name: Supplier3_859 + - class: SupplierFacility + configs: *id002 + name: Supplier1_859 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_859 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_859 + - class: SupplierFacility + configs: *id001 + name: Supplier3_860 + - class: SupplierFacility + configs: *id002 + name: Supplier1_860 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_860 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_860 + - class: SupplierFacility + configs: *id001 + name: Supplier3_861 + - class: SupplierFacility + configs: *id002 + name: Supplier1_861 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_861 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_861 + - class: SupplierFacility + configs: *id001 + name: Supplier3_862 + - class: SupplierFacility + configs: *id002 + name: Supplier1_862 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_862 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_862 + - class: SupplierFacility + configs: *id001 + name: Supplier3_863 + - class: SupplierFacility + configs: *id002 + name: Supplier1_863 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_863 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_863 + - class: SupplierFacility + configs: *id001 + name: Supplier3_864 + - class: SupplierFacility + configs: *id002 + name: Supplier1_864 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_864 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_864 + - class: SupplierFacility + configs: *id001 + name: Supplier3_865 + - class: SupplierFacility + configs: *id002 + name: Supplier1_865 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_865 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_865 + - class: SupplierFacility + configs: *id001 + name: Supplier3_866 + - class: SupplierFacility + configs: *id002 + name: Supplier1_866 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_866 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_866 + - class: SupplierFacility + configs: *id001 + name: Supplier3_867 + - class: SupplierFacility + configs: *id002 + name: Supplier1_867 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_867 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_867 + - class: SupplierFacility + configs: *id001 + name: Supplier3_868 + - class: SupplierFacility + configs: *id002 + name: Supplier1_868 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_868 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_868 + - class: SupplierFacility + configs: *id001 + name: Supplier3_869 + - class: SupplierFacility + configs: *id002 + name: Supplier1_869 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_869 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_869 + - class: SupplierFacility + configs: *id001 + name: Supplier3_870 + - class: SupplierFacility + configs: *id002 + name: Supplier1_870 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_870 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_870 + - class: SupplierFacility + configs: *id001 + name: Supplier3_871 + - class: SupplierFacility + configs: *id002 + name: Supplier1_871 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_871 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_871 + - class: SupplierFacility + configs: *id001 + name: Supplier3_872 + - class: SupplierFacility + configs: *id002 + name: Supplier1_872 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_872 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_872 + - class: SupplierFacility + configs: *id001 + name: Supplier3_873 + - class: SupplierFacility + configs: *id002 + name: Supplier1_873 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_873 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_873 + - class: SupplierFacility + configs: *id001 + name: Supplier3_874 + - class: SupplierFacility + configs: *id002 + name: Supplier1_874 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_874 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_874 + - class: SupplierFacility + configs: *id001 + name: Supplier3_875 + - class: SupplierFacility + configs: *id002 + name: Supplier1_875 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_875 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_875 + - class: SupplierFacility + configs: *id001 + name: Supplier3_876 + - class: SupplierFacility + configs: *id002 + name: Supplier1_876 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_876 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_876 + - class: SupplierFacility + configs: *id001 + name: Supplier3_877 + - class: SupplierFacility + configs: *id002 + name: Supplier1_877 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_877 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_877 + - class: SupplierFacility + configs: *id001 + name: Supplier3_878 + - class: SupplierFacility + configs: *id002 + name: Supplier1_878 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_878 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_878 + - class: SupplierFacility + configs: *id001 + name: Supplier3_879 + - class: SupplierFacility + configs: *id002 + name: Supplier1_879 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_879 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_879 + - class: SupplierFacility + configs: *id001 + name: Supplier3_880 + - class: SupplierFacility + configs: *id002 + name: Supplier1_880 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_880 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_880 + - class: SupplierFacility + configs: *id001 + name: Supplier3_881 + - class: SupplierFacility + configs: *id002 + name: Supplier1_881 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_881 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_881 + - class: SupplierFacility + configs: *id001 + name: Supplier3_882 + - class: SupplierFacility + configs: *id002 + name: Supplier1_882 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_882 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_882 + - class: SupplierFacility + configs: *id001 + name: Supplier3_883 + - class: SupplierFacility + configs: *id002 + name: Supplier1_883 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_883 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_883 + - class: SupplierFacility + configs: *id001 + name: Supplier3_884 + - class: SupplierFacility + configs: *id002 + name: Supplier1_884 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_884 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_884 + - class: SupplierFacility + configs: *id001 + name: Supplier3_885 + - class: SupplierFacility + configs: *id002 + name: Supplier1_885 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_885 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_885 + - class: SupplierFacility + configs: *id001 + name: Supplier3_886 + - class: SupplierFacility + configs: *id002 + name: Supplier1_886 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_886 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_886 + - class: SupplierFacility + configs: *id001 + name: Supplier3_887 + - class: SupplierFacility + configs: *id002 + name: Supplier1_887 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_887 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_887 + - class: SupplierFacility + configs: *id001 + name: Supplier3_888 + - class: SupplierFacility + configs: *id002 + name: Supplier1_888 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_888 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_888 + - class: SupplierFacility + configs: *id001 + name: Supplier3_889 + - class: SupplierFacility + configs: *id002 + name: Supplier1_889 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_889 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_889 + - class: SupplierFacility + configs: *id001 + name: Supplier3_890 + - class: SupplierFacility + configs: *id002 + name: Supplier1_890 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_890 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_890 + - class: SupplierFacility + configs: *id001 + name: Supplier3_891 + - class: SupplierFacility + configs: *id002 + name: Supplier1_891 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_891 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_891 + - class: SupplierFacility + configs: *id001 + name: Supplier3_892 + - class: SupplierFacility + configs: *id002 + name: Supplier1_892 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_892 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_892 + - class: SupplierFacility + configs: *id001 + name: Supplier3_893 + - class: SupplierFacility + configs: *id002 + name: Supplier1_893 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_893 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_893 + - class: SupplierFacility + configs: *id001 + name: Supplier3_894 + - class: SupplierFacility + configs: *id002 + name: Supplier1_894 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_894 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_894 + - class: SupplierFacility + configs: *id001 + name: Supplier3_895 + - class: SupplierFacility + configs: *id002 + name: Supplier1_895 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_895 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_895 + - class: SupplierFacility + configs: *id001 + name: Supplier3_896 + - class: SupplierFacility + configs: *id002 + name: Supplier1_896 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_896 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_896 + - class: SupplierFacility + configs: *id001 + name: Supplier3_897 + - class: SupplierFacility + configs: *id002 + name: Supplier1_897 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_897 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_897 + - class: SupplierFacility + configs: *id001 + name: Supplier3_898 + - class: SupplierFacility + configs: *id002 + name: Supplier1_898 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_898 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_898 + - class: SupplierFacility + configs: *id001 + name: Supplier3_899 + - class: SupplierFacility + configs: *id002 + name: Supplier1_899 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_899 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_899 + - class: SupplierFacility + configs: *id001 + name: Supplier3_900 + - class: SupplierFacility + configs: *id002 + name: Supplier1_900 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_900 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_900 + - class: SupplierFacility + configs: *id001 + name: Supplier3_901 + - class: SupplierFacility + configs: *id002 + name: Supplier1_901 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_901 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_901 + - class: SupplierFacility + configs: *id001 + name: Supplier3_902 + - class: SupplierFacility + configs: *id002 + name: Supplier1_902 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_902 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_902 + - class: SupplierFacility + configs: *id001 + name: Supplier3_903 + - class: SupplierFacility + configs: *id002 + name: Supplier1_903 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_903 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_903 + - class: SupplierFacility + configs: *id001 + name: Supplier3_904 + - class: SupplierFacility + configs: *id002 + name: Supplier1_904 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_904 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_904 + - class: SupplierFacility + configs: *id001 + name: Supplier3_905 + - class: SupplierFacility + configs: *id002 + name: Supplier1_905 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_905 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_905 + - class: SupplierFacility + configs: *id001 + name: Supplier3_906 + - class: SupplierFacility + configs: *id002 + name: Supplier1_906 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_906 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_906 + - class: SupplierFacility + configs: *id001 + name: Supplier3_907 + - class: SupplierFacility + configs: *id002 + name: Supplier1_907 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_907 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_907 + - class: SupplierFacility + configs: *id001 + name: Supplier3_908 + - class: SupplierFacility + configs: *id002 + name: Supplier1_908 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_908 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_908 + - class: SupplierFacility + configs: *id001 + name: Supplier3_909 + - class: SupplierFacility + configs: *id002 + name: Supplier1_909 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_909 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_909 + - class: SupplierFacility + configs: *id001 + name: Supplier3_910 + - class: SupplierFacility + configs: *id002 + name: Supplier1_910 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_910 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_910 + - class: SupplierFacility + configs: *id001 + name: Supplier3_911 + - class: SupplierFacility + configs: *id002 + name: Supplier1_911 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_911 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_911 + - class: SupplierFacility + configs: *id001 + name: Supplier3_912 + - class: SupplierFacility + configs: *id002 + name: Supplier1_912 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_912 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_912 + - class: SupplierFacility + configs: *id001 + name: Supplier3_913 + - class: SupplierFacility + configs: *id002 + name: Supplier1_913 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_913 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_913 + - class: SupplierFacility + configs: *id001 + name: Supplier3_914 + - class: SupplierFacility + configs: *id002 + name: Supplier1_914 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_914 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_914 + - class: SupplierFacility + configs: *id001 + name: Supplier3_915 + - class: SupplierFacility + configs: *id002 + name: Supplier1_915 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_915 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_915 + - class: SupplierFacility + configs: *id001 + name: Supplier3_916 + - class: SupplierFacility + configs: *id002 + name: Supplier1_916 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_916 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_916 + - class: SupplierFacility + configs: *id001 + name: Supplier3_917 + - class: SupplierFacility + configs: *id002 + name: Supplier1_917 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_917 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_917 + - class: SupplierFacility + configs: *id001 + name: Supplier3_918 + - class: SupplierFacility + configs: *id002 + name: Supplier1_918 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_918 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_918 + - class: SupplierFacility + configs: *id001 + name: Supplier3_919 + - class: SupplierFacility + configs: *id002 + name: Supplier1_919 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_919 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_919 + - class: SupplierFacility + configs: *id001 + name: Supplier3_920 + - class: SupplierFacility + configs: *id002 + name: Supplier1_920 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_920 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_920 + - class: SupplierFacility + configs: *id001 + name: Supplier3_921 + - class: SupplierFacility + configs: *id002 + name: Supplier1_921 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_921 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_921 + - class: SupplierFacility + configs: *id001 + name: Supplier3_922 + - class: SupplierFacility + configs: *id002 + name: Supplier1_922 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_922 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_922 + - class: SupplierFacility + configs: *id001 + name: Supplier3_923 + - class: SupplierFacility + configs: *id002 + name: Supplier1_923 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_923 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_923 + - class: SupplierFacility + configs: *id001 + name: Supplier3_924 + - class: SupplierFacility + configs: *id002 + name: Supplier1_924 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_924 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_924 + - class: SupplierFacility + configs: *id001 + name: Supplier3_925 + - class: SupplierFacility + configs: *id002 + name: Supplier1_925 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_925 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_925 + - class: SupplierFacility + configs: *id001 + name: Supplier3_926 + - class: SupplierFacility + configs: *id002 + name: Supplier1_926 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_926 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_926 + - class: SupplierFacility + configs: *id001 + name: Supplier3_927 + - class: SupplierFacility + configs: *id002 + name: Supplier1_927 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_927 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_927 + - class: SupplierFacility + configs: *id001 + name: Supplier3_928 + - class: SupplierFacility + configs: *id002 + name: Supplier1_928 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_928 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_928 + - class: SupplierFacility + configs: *id001 + name: Supplier3_929 + - class: SupplierFacility + configs: *id002 + name: Supplier1_929 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_929 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_929 + - class: SupplierFacility + configs: *id001 + name: Supplier3_930 + - class: SupplierFacility + configs: *id002 + name: Supplier1_930 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_930 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_930 + - class: SupplierFacility + configs: *id001 + name: Supplier3_931 + - class: SupplierFacility + configs: *id002 + name: Supplier1_931 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_931 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_931 + - class: SupplierFacility + configs: *id001 + name: Supplier3_932 + - class: SupplierFacility + configs: *id002 + name: Supplier1_932 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_932 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_932 + - class: SupplierFacility + configs: *id001 + name: Supplier3_933 + - class: SupplierFacility + configs: *id002 + name: Supplier1_933 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_933 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_933 + - class: SupplierFacility + configs: *id001 + name: Supplier3_934 + - class: SupplierFacility + configs: *id002 + name: Supplier1_934 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_934 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_934 + - class: SupplierFacility + configs: *id001 + name: Supplier3_935 + - class: SupplierFacility + configs: *id002 + name: Supplier1_935 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_935 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_935 + - class: SupplierFacility + configs: *id001 + name: Supplier3_936 + - class: SupplierFacility + configs: *id002 + name: Supplier1_936 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_936 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_936 + - class: SupplierFacility + configs: *id001 + name: Supplier3_937 + - class: SupplierFacility + configs: *id002 + name: Supplier1_937 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_937 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_937 + - class: SupplierFacility + configs: *id001 + name: Supplier3_938 + - class: SupplierFacility + configs: *id002 + name: Supplier1_938 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_938 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_938 + - class: SupplierFacility + configs: *id001 + name: Supplier3_939 + - class: SupplierFacility + configs: *id002 + name: Supplier1_939 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_939 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_939 + - class: SupplierFacility + configs: *id001 + name: Supplier3_940 + - class: SupplierFacility + configs: *id002 + name: Supplier1_940 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_940 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_940 + - class: SupplierFacility + configs: *id001 + name: Supplier3_941 + - class: SupplierFacility + configs: *id002 + name: Supplier1_941 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_941 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_941 + - class: SupplierFacility + configs: *id001 + name: Supplier3_942 + - class: SupplierFacility + configs: *id002 + name: Supplier1_942 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_942 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_942 + - class: SupplierFacility + configs: *id001 + name: Supplier3_943 + - class: SupplierFacility + configs: *id002 + name: Supplier1_943 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_943 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_943 + - class: SupplierFacility + configs: *id001 + name: Supplier3_944 + - class: SupplierFacility + configs: *id002 + name: Supplier1_944 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_944 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_944 + - class: SupplierFacility + configs: *id001 + name: Supplier3_945 + - class: SupplierFacility + configs: *id002 + name: Supplier1_945 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_945 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_945 + - class: SupplierFacility + configs: *id001 + name: Supplier3_946 + - class: SupplierFacility + configs: *id002 + name: Supplier1_946 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_946 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_946 + - class: SupplierFacility + configs: *id001 + name: Supplier3_947 + - class: SupplierFacility + configs: *id002 + name: Supplier1_947 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_947 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_947 + - class: SupplierFacility + configs: *id001 + name: Supplier3_948 + - class: SupplierFacility + configs: *id002 + name: Supplier1_948 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_948 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_948 + - class: SupplierFacility + configs: *id001 + name: Supplier3_949 + - class: SupplierFacility + configs: *id002 + name: Supplier1_949 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_949 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_949 + - class: SupplierFacility + configs: *id001 + name: Supplier3_950 + - class: SupplierFacility + configs: *id002 + name: Supplier1_950 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_950 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_950 + - class: SupplierFacility + configs: *id001 + name: Supplier3_951 + - class: SupplierFacility + configs: *id002 + name: Supplier1_951 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_951 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_951 + - class: SupplierFacility + configs: *id001 + name: Supplier3_952 + - class: SupplierFacility + configs: *id002 + name: Supplier1_952 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_952 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_952 + - class: SupplierFacility + configs: *id001 + name: Supplier3_953 + - class: SupplierFacility + configs: *id002 + name: Supplier1_953 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_953 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_953 + - class: SupplierFacility + configs: *id001 + name: Supplier3_954 + - class: SupplierFacility + configs: *id002 + name: Supplier1_954 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_954 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_954 + - class: SupplierFacility + configs: *id001 + name: Supplier3_955 + - class: SupplierFacility + configs: *id002 + name: Supplier1_955 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_955 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_955 + - class: SupplierFacility + configs: *id001 + name: Supplier3_956 + - class: SupplierFacility + configs: *id002 + name: Supplier1_956 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_956 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_956 + - class: SupplierFacility + configs: *id001 + name: Supplier3_957 + - class: SupplierFacility + configs: *id002 + name: Supplier1_957 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_957 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_957 + - class: SupplierFacility + configs: *id001 + name: Supplier3_958 + - class: SupplierFacility + configs: *id002 + name: Supplier1_958 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_958 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_958 + - class: SupplierFacility + configs: *id001 + name: Supplier3_959 + - class: SupplierFacility + configs: *id002 + name: Supplier1_959 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_959 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_959 + - class: SupplierFacility + configs: *id001 + name: Supplier3_960 + - class: SupplierFacility + configs: *id002 + name: Supplier1_960 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_960 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_960 + - class: SupplierFacility + configs: *id001 + name: Supplier3_961 + - class: SupplierFacility + configs: *id002 + name: Supplier1_961 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_961 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_961 + - class: SupplierFacility + configs: *id001 + name: Supplier3_962 + - class: SupplierFacility + configs: *id002 + name: Supplier1_962 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_962 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_962 + - class: SupplierFacility + configs: *id001 + name: Supplier3_963 + - class: SupplierFacility + configs: *id002 + name: Supplier1_963 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_963 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_963 + - class: SupplierFacility + configs: *id001 + name: Supplier3_964 + - class: SupplierFacility + configs: *id002 + name: Supplier1_964 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_964 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_964 + - class: SupplierFacility + configs: *id001 + name: Supplier3_965 + - class: SupplierFacility + configs: *id002 + name: Supplier1_965 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_965 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_965 + - class: SupplierFacility + configs: *id001 + name: Supplier3_966 + - class: SupplierFacility + configs: *id002 + name: Supplier1_966 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_966 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_966 + - class: SupplierFacility + configs: *id001 + name: Supplier3_967 + - class: SupplierFacility + configs: *id002 + name: Supplier1_967 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_967 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_967 + - class: SupplierFacility + configs: *id001 + name: Supplier3_968 + - class: SupplierFacility + configs: *id002 + name: Supplier1_968 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_968 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_968 + - class: SupplierFacility + configs: *id001 + name: Supplier3_969 + - class: SupplierFacility + configs: *id002 + name: Supplier1_969 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_969 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_969 + - class: SupplierFacility + configs: *id001 + name: Supplier3_970 + - class: SupplierFacility + configs: *id002 + name: Supplier1_970 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_970 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_970 + - class: SupplierFacility + configs: *id001 + name: Supplier3_971 + - class: SupplierFacility + configs: *id002 + name: Supplier1_971 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_971 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_971 + - class: SupplierFacility + configs: *id001 + name: Supplier3_972 + - class: SupplierFacility + configs: *id002 + name: Supplier1_972 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_972 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_972 + - class: SupplierFacility + configs: *id001 + name: Supplier3_973 + - class: SupplierFacility + configs: *id002 + name: Supplier1_973 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_973 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_973 + - class: SupplierFacility + configs: *id001 + name: Supplier3_974 + - class: SupplierFacility + configs: *id002 + name: Supplier1_974 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_974 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_974 + - class: SupplierFacility + configs: *id001 + name: Supplier3_975 + - class: SupplierFacility + configs: *id002 + name: Supplier1_975 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_975 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_975 + - class: SupplierFacility + configs: *id001 + name: Supplier3_976 + - class: SupplierFacility + configs: *id002 + name: Supplier1_976 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_976 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_976 + - class: SupplierFacility + configs: *id001 + name: Supplier3_977 + - class: SupplierFacility + configs: *id002 + name: Supplier1_977 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_977 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_977 + - class: SupplierFacility + configs: *id001 + name: Supplier3_978 + - class: SupplierFacility + configs: *id002 + name: Supplier1_978 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_978 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_978 + - class: SupplierFacility + configs: *id001 + name: Supplier3_979 + - class: SupplierFacility + configs: *id002 + name: Supplier1_979 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_979 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_979 + - class: SupplierFacility + configs: *id001 + name: Supplier3_980 + - class: SupplierFacility + configs: *id002 + name: Supplier1_980 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_980 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_980 + - class: SupplierFacility + configs: *id001 + name: Supplier3_981 + - class: SupplierFacility + configs: *id002 + name: Supplier1_981 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_981 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_981 + - class: SupplierFacility + configs: *id001 + name: Supplier3_982 + - class: SupplierFacility + configs: *id002 + name: Supplier1_982 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_982 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_982 + - class: SupplierFacility + configs: *id001 + name: Supplier3_983 + - class: SupplierFacility + configs: *id002 + name: Supplier1_983 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_983 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_983 + - class: SupplierFacility + configs: *id001 + name: Supplier3_984 + - class: SupplierFacility + configs: *id002 + name: Supplier1_984 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_984 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_984 + - class: SupplierFacility + configs: *id001 + name: Supplier3_985 + - class: SupplierFacility + configs: *id002 + name: Supplier1_985 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_985 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_985 + - class: SupplierFacility + configs: *id001 + name: Supplier3_986 + - class: SupplierFacility + configs: *id002 + name: Supplier1_986 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_986 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_986 + - class: SupplierFacility + configs: *id001 + name: Supplier3_987 + - class: SupplierFacility + configs: *id002 + name: Supplier1_987 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_987 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_987 + - class: SupplierFacility + configs: *id001 + name: Supplier3_988 + - class: SupplierFacility + configs: *id002 + name: Supplier1_988 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_988 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_988 + - class: SupplierFacility + configs: *id001 + name: Supplier3_989 + - class: SupplierFacility + configs: *id002 + name: Supplier1_989 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_989 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_989 + - class: SupplierFacility + configs: *id001 + name: Supplier3_990 + - class: SupplierFacility + configs: *id002 + name: Supplier1_990 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_990 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_990 + - class: SupplierFacility + configs: *id001 + name: Supplier3_991 + - class: SupplierFacility + configs: *id002 + name: Supplier1_991 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_991 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_991 + - class: SupplierFacility + configs: *id001 + name: Supplier3_992 + - class: SupplierFacility + configs: *id002 + name: Supplier1_992 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_992 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_992 + - class: SupplierFacility + configs: *id001 + name: Supplier3_993 + - class: SupplierFacility + configs: *id002 + name: Supplier1_993 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_993 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_993 + - class: SupplierFacility + configs: *id001 + name: Supplier3_994 + - class: SupplierFacility + configs: *id002 + name: Supplier1_994 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_994 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_994 + - class: SupplierFacility + configs: *id001 + name: Supplier3_995 + - class: SupplierFacility + configs: *id002 + name: Supplier1_995 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_995 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_995 + - class: SupplierFacility + configs: *id001 + name: Supplier3_996 + - class: SupplierFacility + configs: *id002 + name: Supplier1_996 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_996 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_996 + - class: SupplierFacility + configs: *id001 + name: Supplier3_997 + - class: SupplierFacility + configs: *id002 + name: Supplier1_997 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_997 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_997 + - class: SupplierFacility + configs: *id001 + name: Supplier3_998 + - class: SupplierFacility + configs: *id002 + name: Supplier1_998 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_998 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_998 + grid: + blocks: + railroad: + - - 10 + - 10 + - - 10 + - 11 + - - 10 + - 12 + - - 11 + - 12 + facilities: + ChaoShiFa01: + - 10 + - 18 + Supplier1: + - 0 + - 0 + Supplier3: + - 3 + - 3 + Warehouse1: + - 6 + - 6 + size: + - 20 + - 20 + skus: + - bom: + sku3: 10 + id: 1 + name: sku1 + output_units_per_lot: 1 + - id: 2 + name: sku2 + output_units_per_lot: 1 + - id: 3 + name: sku3 + output_units_per_lot: 1 + step_mode: facility + topology: + ChaoShiFa01: + sku1: + - Supplier1 + sku3: + - Supplier3 + Supplier1: + sku3: + - Supplier3 + Warehouse1: + sku1: + - Supplier1 + sku3: + - Supplier3 diff --git a/maro/simulator/scenarios/supply_chain/topologies/perf_4x400_facilities/config.yml b/maro/simulator/scenarios/supply_chain/topologies/perf_4x400_facilities/config.yml new file mode 100644 index 000000000..95886b726 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/perf_4x400_facilities/config.yml @@ -0,0 +1,4963 @@ +base: '' +world: + action_steps: 1 + facilities: + - class: SupplierFacility + configs: &id001 + distribution: + data: + unit_price: 10 + skus: + sku3: + cost": 10 + delay_order_penalty: 10 + init_in_stock: 100 + price": 10 + product_unit_cost: 1 + production_rate: 0 + type: production + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + transports: + - class: TransportUnit + data: + patient: 100 + - class: TransportUnit + data: + patient: 100 + name: Supplier3 + - class: SupplierFacility + configs: &id002 + distribution: + data: + unit_price: 10 + skus: + sku1: + cost: 10 + delay_order_penalty: 10 + init_in_stock: 100 + price: 100 + product_unit_cost: 1 + production_rate: 0 + type: production + sku3: + cost: 10 + delay_order_penalty: 10 + init_in_stock: 100 + price: 100 + production_rate: 200 + type: material + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + transports: + - class: TransportUnit + data: + patient: 100 + - class: TransportUnit + data: + patient: 100 + name: Supplier1 + - class: WarehouseFacility + configs: &id003 + distribution: + data: + unit_price: 10 + skus: + sku1: + delay_order_penalty: 10 + init_stock: 1000 + price: 100 + sku2: + delay_order_penalty: 10 + init_stock: 1000 + price: 100 + sku3: + delay_order_penalty: 10 + init_stock: 1000 + price: 100 + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + transports: + - class: TransportUnit + data: + patient: 100 + unit_transport_cost: 1 + - class: TransportUnit + data: + patient: 100 + unit_transport_cost: 1 + name: Warehouse1 + - class: RetailerFacility + configs: &id004 + skus: + sku1: + backlog_ratio: 0.1 + cost: 10 + init_in_stock: 100 + price: 300 + sale_gamma: 100 + sku2: + backlog_ratio: 0.1 + cost: 10 + init_in_stock: 100 + price: 100 + sale_gamma: 100 + sku3: + backlog_ratio: 0.1 + cost: 10 + init_in_stock: 100 + price: 200 + sale_gamma: 100 + storage: + data: + capacity: 20000 + unit_storage_cost: 10 + name: ChaoShiFa01 + - class: SupplierFacility + configs: *id001 + name: Supplier3_0 + - class: SupplierFacility + configs: *id002 + name: Supplier1_0 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_0 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_0 + - class: SupplierFacility + configs: *id001 + name: Supplier3_1 + - class: SupplierFacility + configs: *id002 + name: Supplier1_1 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_1 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_1 + - class: SupplierFacility + configs: *id001 + name: Supplier3_2 + - class: SupplierFacility + configs: *id002 + name: Supplier1_2 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_2 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_2 + - class: SupplierFacility + configs: *id001 + name: Supplier3_3 + - class: SupplierFacility + configs: *id002 + name: Supplier1_3 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_3 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_3 + - class: SupplierFacility + configs: *id001 + name: Supplier3_4 + - class: SupplierFacility + configs: *id002 + name: Supplier1_4 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_4 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_4 + - class: SupplierFacility + configs: *id001 + name: Supplier3_5 + - class: SupplierFacility + configs: *id002 + name: Supplier1_5 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_5 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_5 + - class: SupplierFacility + configs: *id001 + name: Supplier3_6 + - class: SupplierFacility + configs: *id002 + name: Supplier1_6 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_6 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_6 + - class: SupplierFacility + configs: *id001 + name: Supplier3_7 + - class: SupplierFacility + configs: *id002 + name: Supplier1_7 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_7 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_7 + - class: SupplierFacility + configs: *id001 + name: Supplier3_8 + - class: SupplierFacility + configs: *id002 + name: Supplier1_8 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_8 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_8 + - class: SupplierFacility + configs: *id001 + name: Supplier3_9 + - class: SupplierFacility + configs: *id002 + name: Supplier1_9 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_9 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_9 + - class: SupplierFacility + configs: *id001 + name: Supplier3_10 + - class: SupplierFacility + configs: *id002 + name: Supplier1_10 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_10 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_10 + - class: SupplierFacility + configs: *id001 + name: Supplier3_11 + - class: SupplierFacility + configs: *id002 + name: Supplier1_11 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_11 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_11 + - class: SupplierFacility + configs: *id001 + name: Supplier3_12 + - class: SupplierFacility + configs: *id002 + name: Supplier1_12 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_12 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_12 + - class: SupplierFacility + configs: *id001 + name: Supplier3_13 + - class: SupplierFacility + configs: *id002 + name: Supplier1_13 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_13 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_13 + - class: SupplierFacility + configs: *id001 + name: Supplier3_14 + - class: SupplierFacility + configs: *id002 + name: Supplier1_14 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_14 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_14 + - class: SupplierFacility + configs: *id001 + name: Supplier3_15 + - class: SupplierFacility + configs: *id002 + name: Supplier1_15 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_15 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_15 + - class: SupplierFacility + configs: *id001 + name: Supplier3_16 + - class: SupplierFacility + configs: *id002 + name: Supplier1_16 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_16 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_16 + - class: SupplierFacility + configs: *id001 + name: Supplier3_17 + - class: SupplierFacility + configs: *id002 + name: Supplier1_17 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_17 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_17 + - class: SupplierFacility + configs: *id001 + name: Supplier3_18 + - class: SupplierFacility + configs: *id002 + name: Supplier1_18 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_18 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_18 + - class: SupplierFacility + configs: *id001 + name: Supplier3_19 + - class: SupplierFacility + configs: *id002 + name: Supplier1_19 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_19 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_19 + - class: SupplierFacility + configs: *id001 + name: Supplier3_20 + - class: SupplierFacility + configs: *id002 + name: Supplier1_20 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_20 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_20 + - class: SupplierFacility + configs: *id001 + name: Supplier3_21 + - class: SupplierFacility + configs: *id002 + name: Supplier1_21 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_21 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_21 + - class: SupplierFacility + configs: *id001 + name: Supplier3_22 + - class: SupplierFacility + configs: *id002 + name: Supplier1_22 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_22 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_22 + - class: SupplierFacility + configs: *id001 + name: Supplier3_23 + - class: SupplierFacility + configs: *id002 + name: Supplier1_23 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_23 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_23 + - class: SupplierFacility + configs: *id001 + name: Supplier3_24 + - class: SupplierFacility + configs: *id002 + name: Supplier1_24 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_24 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_24 + - class: SupplierFacility + configs: *id001 + name: Supplier3_25 + - class: SupplierFacility + configs: *id002 + name: Supplier1_25 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_25 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_25 + - class: SupplierFacility + configs: *id001 + name: Supplier3_26 + - class: SupplierFacility + configs: *id002 + name: Supplier1_26 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_26 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_26 + - class: SupplierFacility + configs: *id001 + name: Supplier3_27 + - class: SupplierFacility + configs: *id002 + name: Supplier1_27 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_27 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_27 + - class: SupplierFacility + configs: *id001 + name: Supplier3_28 + - class: SupplierFacility + configs: *id002 + name: Supplier1_28 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_28 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_28 + - class: SupplierFacility + configs: *id001 + name: Supplier3_29 + - class: SupplierFacility + configs: *id002 + name: Supplier1_29 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_29 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_29 + - class: SupplierFacility + configs: *id001 + name: Supplier3_30 + - class: SupplierFacility + configs: *id002 + name: Supplier1_30 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_30 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_30 + - class: SupplierFacility + configs: *id001 + name: Supplier3_31 + - class: SupplierFacility + configs: *id002 + name: Supplier1_31 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_31 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_31 + - class: SupplierFacility + configs: *id001 + name: Supplier3_32 + - class: SupplierFacility + configs: *id002 + name: Supplier1_32 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_32 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_32 + - class: SupplierFacility + configs: *id001 + name: Supplier3_33 + - class: SupplierFacility + configs: *id002 + name: Supplier1_33 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_33 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_33 + - class: SupplierFacility + configs: *id001 + name: Supplier3_34 + - class: SupplierFacility + configs: *id002 + name: Supplier1_34 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_34 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_34 + - class: SupplierFacility + configs: *id001 + name: Supplier3_35 + - class: SupplierFacility + configs: *id002 + name: Supplier1_35 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_35 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_35 + - class: SupplierFacility + configs: *id001 + name: Supplier3_36 + - class: SupplierFacility + configs: *id002 + name: Supplier1_36 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_36 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_36 + - class: SupplierFacility + configs: *id001 + name: Supplier3_37 + - class: SupplierFacility + configs: *id002 + name: Supplier1_37 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_37 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_37 + - class: SupplierFacility + configs: *id001 + name: Supplier3_38 + - class: SupplierFacility + configs: *id002 + name: Supplier1_38 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_38 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_38 + - class: SupplierFacility + configs: *id001 + name: Supplier3_39 + - class: SupplierFacility + configs: *id002 + name: Supplier1_39 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_39 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_39 + - class: SupplierFacility + configs: *id001 + name: Supplier3_40 + - class: SupplierFacility + configs: *id002 + name: Supplier1_40 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_40 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_40 + - class: SupplierFacility + configs: *id001 + name: Supplier3_41 + - class: SupplierFacility + configs: *id002 + name: Supplier1_41 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_41 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_41 + - class: SupplierFacility + configs: *id001 + name: Supplier3_42 + - class: SupplierFacility + configs: *id002 + name: Supplier1_42 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_42 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_42 + - class: SupplierFacility + configs: *id001 + name: Supplier3_43 + - class: SupplierFacility + configs: *id002 + name: Supplier1_43 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_43 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_43 + - class: SupplierFacility + configs: *id001 + name: Supplier3_44 + - class: SupplierFacility + configs: *id002 + name: Supplier1_44 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_44 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_44 + - class: SupplierFacility + configs: *id001 + name: Supplier3_45 + - class: SupplierFacility + configs: *id002 + name: Supplier1_45 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_45 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_45 + - class: SupplierFacility + configs: *id001 + name: Supplier3_46 + - class: SupplierFacility + configs: *id002 + name: Supplier1_46 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_46 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_46 + - class: SupplierFacility + configs: *id001 + name: Supplier3_47 + - class: SupplierFacility + configs: *id002 + name: Supplier1_47 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_47 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_47 + - class: SupplierFacility + configs: *id001 + name: Supplier3_48 + - class: SupplierFacility + configs: *id002 + name: Supplier1_48 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_48 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_48 + - class: SupplierFacility + configs: *id001 + name: Supplier3_49 + - class: SupplierFacility + configs: *id002 + name: Supplier1_49 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_49 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_49 + - class: SupplierFacility + configs: *id001 + name: Supplier3_50 + - class: SupplierFacility + configs: *id002 + name: Supplier1_50 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_50 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_50 + - class: SupplierFacility + configs: *id001 + name: Supplier3_51 + - class: SupplierFacility + configs: *id002 + name: Supplier1_51 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_51 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_51 + - class: SupplierFacility + configs: *id001 + name: Supplier3_52 + - class: SupplierFacility + configs: *id002 + name: Supplier1_52 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_52 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_52 + - class: SupplierFacility + configs: *id001 + name: Supplier3_53 + - class: SupplierFacility + configs: *id002 + name: Supplier1_53 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_53 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_53 + - class: SupplierFacility + configs: *id001 + name: Supplier3_54 + - class: SupplierFacility + configs: *id002 + name: Supplier1_54 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_54 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_54 + - class: SupplierFacility + configs: *id001 + name: Supplier3_55 + - class: SupplierFacility + configs: *id002 + name: Supplier1_55 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_55 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_55 + - class: SupplierFacility + configs: *id001 + name: Supplier3_56 + - class: SupplierFacility + configs: *id002 + name: Supplier1_56 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_56 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_56 + - class: SupplierFacility + configs: *id001 + name: Supplier3_57 + - class: SupplierFacility + configs: *id002 + name: Supplier1_57 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_57 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_57 + - class: SupplierFacility + configs: *id001 + name: Supplier3_58 + - class: SupplierFacility + configs: *id002 + name: Supplier1_58 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_58 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_58 + - class: SupplierFacility + configs: *id001 + name: Supplier3_59 + - class: SupplierFacility + configs: *id002 + name: Supplier1_59 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_59 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_59 + - class: SupplierFacility + configs: *id001 + name: Supplier3_60 + - class: SupplierFacility + configs: *id002 + name: Supplier1_60 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_60 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_60 + - class: SupplierFacility + configs: *id001 + name: Supplier3_61 + - class: SupplierFacility + configs: *id002 + name: Supplier1_61 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_61 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_61 + - class: SupplierFacility + configs: *id001 + name: Supplier3_62 + - class: SupplierFacility + configs: *id002 + name: Supplier1_62 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_62 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_62 + - class: SupplierFacility + configs: *id001 + name: Supplier3_63 + - class: SupplierFacility + configs: *id002 + name: Supplier1_63 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_63 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_63 + - class: SupplierFacility + configs: *id001 + name: Supplier3_64 + - class: SupplierFacility + configs: *id002 + name: Supplier1_64 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_64 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_64 + - class: SupplierFacility + configs: *id001 + name: Supplier3_65 + - class: SupplierFacility + configs: *id002 + name: Supplier1_65 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_65 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_65 + - class: SupplierFacility + configs: *id001 + name: Supplier3_66 + - class: SupplierFacility + configs: *id002 + name: Supplier1_66 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_66 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_66 + - class: SupplierFacility + configs: *id001 + name: Supplier3_67 + - class: SupplierFacility + configs: *id002 + name: Supplier1_67 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_67 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_67 + - class: SupplierFacility + configs: *id001 + name: Supplier3_68 + - class: SupplierFacility + configs: *id002 + name: Supplier1_68 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_68 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_68 + - class: SupplierFacility + configs: *id001 + name: Supplier3_69 + - class: SupplierFacility + configs: *id002 + name: Supplier1_69 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_69 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_69 + - class: SupplierFacility + configs: *id001 + name: Supplier3_70 + - class: SupplierFacility + configs: *id002 + name: Supplier1_70 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_70 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_70 + - class: SupplierFacility + configs: *id001 + name: Supplier3_71 + - class: SupplierFacility + configs: *id002 + name: Supplier1_71 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_71 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_71 + - class: SupplierFacility + configs: *id001 + name: Supplier3_72 + - class: SupplierFacility + configs: *id002 + name: Supplier1_72 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_72 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_72 + - class: SupplierFacility + configs: *id001 + name: Supplier3_73 + - class: SupplierFacility + configs: *id002 + name: Supplier1_73 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_73 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_73 + - class: SupplierFacility + configs: *id001 + name: Supplier3_74 + - class: SupplierFacility + configs: *id002 + name: Supplier1_74 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_74 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_74 + - class: SupplierFacility + configs: *id001 + name: Supplier3_75 + - class: SupplierFacility + configs: *id002 + name: Supplier1_75 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_75 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_75 + - class: SupplierFacility + configs: *id001 + name: Supplier3_76 + - class: SupplierFacility + configs: *id002 + name: Supplier1_76 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_76 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_76 + - class: SupplierFacility + configs: *id001 + name: Supplier3_77 + - class: SupplierFacility + configs: *id002 + name: Supplier1_77 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_77 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_77 + - class: SupplierFacility + configs: *id001 + name: Supplier3_78 + - class: SupplierFacility + configs: *id002 + name: Supplier1_78 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_78 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_78 + - class: SupplierFacility + configs: *id001 + name: Supplier3_79 + - class: SupplierFacility + configs: *id002 + name: Supplier1_79 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_79 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_79 + - class: SupplierFacility + configs: *id001 + name: Supplier3_80 + - class: SupplierFacility + configs: *id002 + name: Supplier1_80 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_80 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_80 + - class: SupplierFacility + configs: *id001 + name: Supplier3_81 + - class: SupplierFacility + configs: *id002 + name: Supplier1_81 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_81 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_81 + - class: SupplierFacility + configs: *id001 + name: Supplier3_82 + - class: SupplierFacility + configs: *id002 + name: Supplier1_82 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_82 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_82 + - class: SupplierFacility + configs: *id001 + name: Supplier3_83 + - class: SupplierFacility + configs: *id002 + name: Supplier1_83 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_83 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_83 + - class: SupplierFacility + configs: *id001 + name: Supplier3_84 + - class: SupplierFacility + configs: *id002 + name: Supplier1_84 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_84 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_84 + - class: SupplierFacility + configs: *id001 + name: Supplier3_85 + - class: SupplierFacility + configs: *id002 + name: Supplier1_85 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_85 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_85 + - class: SupplierFacility + configs: *id001 + name: Supplier3_86 + - class: SupplierFacility + configs: *id002 + name: Supplier1_86 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_86 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_86 + - class: SupplierFacility + configs: *id001 + name: Supplier3_87 + - class: SupplierFacility + configs: *id002 + name: Supplier1_87 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_87 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_87 + - class: SupplierFacility + configs: *id001 + name: Supplier3_88 + - class: SupplierFacility + configs: *id002 + name: Supplier1_88 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_88 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_88 + - class: SupplierFacility + configs: *id001 + name: Supplier3_89 + - class: SupplierFacility + configs: *id002 + name: Supplier1_89 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_89 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_89 + - class: SupplierFacility + configs: *id001 + name: Supplier3_90 + - class: SupplierFacility + configs: *id002 + name: Supplier1_90 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_90 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_90 + - class: SupplierFacility + configs: *id001 + name: Supplier3_91 + - class: SupplierFacility + configs: *id002 + name: Supplier1_91 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_91 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_91 + - class: SupplierFacility + configs: *id001 + name: Supplier3_92 + - class: SupplierFacility + configs: *id002 + name: Supplier1_92 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_92 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_92 + - class: SupplierFacility + configs: *id001 + name: Supplier3_93 + - class: SupplierFacility + configs: *id002 + name: Supplier1_93 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_93 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_93 + - class: SupplierFacility + configs: *id001 + name: Supplier3_94 + - class: SupplierFacility + configs: *id002 + name: Supplier1_94 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_94 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_94 + - class: SupplierFacility + configs: *id001 + name: Supplier3_95 + - class: SupplierFacility + configs: *id002 + name: Supplier1_95 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_95 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_95 + - class: SupplierFacility + configs: *id001 + name: Supplier3_96 + - class: SupplierFacility + configs: *id002 + name: Supplier1_96 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_96 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_96 + - class: SupplierFacility + configs: *id001 + name: Supplier3_97 + - class: SupplierFacility + configs: *id002 + name: Supplier1_97 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_97 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_97 + - class: SupplierFacility + configs: *id001 + name: Supplier3_98 + - class: SupplierFacility + configs: *id002 + name: Supplier1_98 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_98 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_98 + - class: SupplierFacility + configs: *id001 + name: Supplier3_99 + - class: SupplierFacility + configs: *id002 + name: Supplier1_99 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_99 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_99 + - class: SupplierFacility + configs: *id001 + name: Supplier3_100 + - class: SupplierFacility + configs: *id002 + name: Supplier1_100 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_100 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_100 + - class: SupplierFacility + configs: *id001 + name: Supplier3_101 + - class: SupplierFacility + configs: *id002 + name: Supplier1_101 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_101 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_101 + - class: SupplierFacility + configs: *id001 + name: Supplier3_102 + - class: SupplierFacility + configs: *id002 + name: Supplier1_102 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_102 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_102 + - class: SupplierFacility + configs: *id001 + name: Supplier3_103 + - class: SupplierFacility + configs: *id002 + name: Supplier1_103 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_103 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_103 + - class: SupplierFacility + configs: *id001 + name: Supplier3_104 + - class: SupplierFacility + configs: *id002 + name: Supplier1_104 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_104 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_104 + - class: SupplierFacility + configs: *id001 + name: Supplier3_105 + - class: SupplierFacility + configs: *id002 + name: Supplier1_105 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_105 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_105 + - class: SupplierFacility + configs: *id001 + name: Supplier3_106 + - class: SupplierFacility + configs: *id002 + name: Supplier1_106 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_106 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_106 + - class: SupplierFacility + configs: *id001 + name: Supplier3_107 + - class: SupplierFacility + configs: *id002 + name: Supplier1_107 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_107 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_107 + - class: SupplierFacility + configs: *id001 + name: Supplier3_108 + - class: SupplierFacility + configs: *id002 + name: Supplier1_108 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_108 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_108 + - class: SupplierFacility + configs: *id001 + name: Supplier3_109 + - class: SupplierFacility + configs: *id002 + name: Supplier1_109 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_109 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_109 + - class: SupplierFacility + configs: *id001 + name: Supplier3_110 + - class: SupplierFacility + configs: *id002 + name: Supplier1_110 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_110 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_110 + - class: SupplierFacility + configs: *id001 + name: Supplier3_111 + - class: SupplierFacility + configs: *id002 + name: Supplier1_111 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_111 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_111 + - class: SupplierFacility + configs: *id001 + name: Supplier3_112 + - class: SupplierFacility + configs: *id002 + name: Supplier1_112 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_112 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_112 + - class: SupplierFacility + configs: *id001 + name: Supplier3_113 + - class: SupplierFacility + configs: *id002 + name: Supplier1_113 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_113 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_113 + - class: SupplierFacility + configs: *id001 + name: Supplier3_114 + - class: SupplierFacility + configs: *id002 + name: Supplier1_114 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_114 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_114 + - class: SupplierFacility + configs: *id001 + name: Supplier3_115 + - class: SupplierFacility + configs: *id002 + name: Supplier1_115 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_115 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_115 + - class: SupplierFacility + configs: *id001 + name: Supplier3_116 + - class: SupplierFacility + configs: *id002 + name: Supplier1_116 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_116 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_116 + - class: SupplierFacility + configs: *id001 + name: Supplier3_117 + - class: SupplierFacility + configs: *id002 + name: Supplier1_117 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_117 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_117 + - class: SupplierFacility + configs: *id001 + name: Supplier3_118 + - class: SupplierFacility + configs: *id002 + name: Supplier1_118 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_118 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_118 + - class: SupplierFacility + configs: *id001 + name: Supplier3_119 + - class: SupplierFacility + configs: *id002 + name: Supplier1_119 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_119 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_119 + - class: SupplierFacility + configs: *id001 + name: Supplier3_120 + - class: SupplierFacility + configs: *id002 + name: Supplier1_120 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_120 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_120 + - class: SupplierFacility + configs: *id001 + name: Supplier3_121 + - class: SupplierFacility + configs: *id002 + name: Supplier1_121 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_121 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_121 + - class: SupplierFacility + configs: *id001 + name: Supplier3_122 + - class: SupplierFacility + configs: *id002 + name: Supplier1_122 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_122 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_122 + - class: SupplierFacility + configs: *id001 + name: Supplier3_123 + - class: SupplierFacility + configs: *id002 + name: Supplier1_123 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_123 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_123 + - class: SupplierFacility + configs: *id001 + name: Supplier3_124 + - class: SupplierFacility + configs: *id002 + name: Supplier1_124 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_124 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_124 + - class: SupplierFacility + configs: *id001 + name: Supplier3_125 + - class: SupplierFacility + configs: *id002 + name: Supplier1_125 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_125 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_125 + - class: SupplierFacility + configs: *id001 + name: Supplier3_126 + - class: SupplierFacility + configs: *id002 + name: Supplier1_126 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_126 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_126 + - class: SupplierFacility + configs: *id001 + name: Supplier3_127 + - class: SupplierFacility + configs: *id002 + name: Supplier1_127 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_127 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_127 + - class: SupplierFacility + configs: *id001 + name: Supplier3_128 + - class: SupplierFacility + configs: *id002 + name: Supplier1_128 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_128 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_128 + - class: SupplierFacility + configs: *id001 + name: Supplier3_129 + - class: SupplierFacility + configs: *id002 + name: Supplier1_129 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_129 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_129 + - class: SupplierFacility + configs: *id001 + name: Supplier3_130 + - class: SupplierFacility + configs: *id002 + name: Supplier1_130 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_130 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_130 + - class: SupplierFacility + configs: *id001 + name: Supplier3_131 + - class: SupplierFacility + configs: *id002 + name: Supplier1_131 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_131 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_131 + - class: SupplierFacility + configs: *id001 + name: Supplier3_132 + - class: SupplierFacility + configs: *id002 + name: Supplier1_132 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_132 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_132 + - class: SupplierFacility + configs: *id001 + name: Supplier3_133 + - class: SupplierFacility + configs: *id002 + name: Supplier1_133 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_133 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_133 + - class: SupplierFacility + configs: *id001 + name: Supplier3_134 + - class: SupplierFacility + configs: *id002 + name: Supplier1_134 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_134 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_134 + - class: SupplierFacility + configs: *id001 + name: Supplier3_135 + - class: SupplierFacility + configs: *id002 + name: Supplier1_135 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_135 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_135 + - class: SupplierFacility + configs: *id001 + name: Supplier3_136 + - class: SupplierFacility + configs: *id002 + name: Supplier1_136 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_136 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_136 + - class: SupplierFacility + configs: *id001 + name: Supplier3_137 + - class: SupplierFacility + configs: *id002 + name: Supplier1_137 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_137 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_137 + - class: SupplierFacility + configs: *id001 + name: Supplier3_138 + - class: SupplierFacility + configs: *id002 + name: Supplier1_138 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_138 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_138 + - class: SupplierFacility + configs: *id001 + name: Supplier3_139 + - class: SupplierFacility + configs: *id002 + name: Supplier1_139 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_139 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_139 + - class: SupplierFacility + configs: *id001 + name: Supplier3_140 + - class: SupplierFacility + configs: *id002 + name: Supplier1_140 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_140 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_140 + - class: SupplierFacility + configs: *id001 + name: Supplier3_141 + - class: SupplierFacility + configs: *id002 + name: Supplier1_141 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_141 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_141 + - class: SupplierFacility + configs: *id001 + name: Supplier3_142 + - class: SupplierFacility + configs: *id002 + name: Supplier1_142 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_142 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_142 + - class: SupplierFacility + configs: *id001 + name: Supplier3_143 + - class: SupplierFacility + configs: *id002 + name: Supplier1_143 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_143 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_143 + - class: SupplierFacility + configs: *id001 + name: Supplier3_144 + - class: SupplierFacility + configs: *id002 + name: Supplier1_144 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_144 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_144 + - class: SupplierFacility + configs: *id001 + name: Supplier3_145 + - class: SupplierFacility + configs: *id002 + name: Supplier1_145 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_145 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_145 + - class: SupplierFacility + configs: *id001 + name: Supplier3_146 + - class: SupplierFacility + configs: *id002 + name: Supplier1_146 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_146 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_146 + - class: SupplierFacility + configs: *id001 + name: Supplier3_147 + - class: SupplierFacility + configs: *id002 + name: Supplier1_147 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_147 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_147 + - class: SupplierFacility + configs: *id001 + name: Supplier3_148 + - class: SupplierFacility + configs: *id002 + name: Supplier1_148 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_148 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_148 + - class: SupplierFacility + configs: *id001 + name: Supplier3_149 + - class: SupplierFacility + configs: *id002 + name: Supplier1_149 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_149 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_149 + - class: SupplierFacility + configs: *id001 + name: Supplier3_150 + - class: SupplierFacility + configs: *id002 + name: Supplier1_150 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_150 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_150 + - class: SupplierFacility + configs: *id001 + name: Supplier3_151 + - class: SupplierFacility + configs: *id002 + name: Supplier1_151 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_151 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_151 + - class: SupplierFacility + configs: *id001 + name: Supplier3_152 + - class: SupplierFacility + configs: *id002 + name: Supplier1_152 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_152 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_152 + - class: SupplierFacility + configs: *id001 + name: Supplier3_153 + - class: SupplierFacility + configs: *id002 + name: Supplier1_153 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_153 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_153 + - class: SupplierFacility + configs: *id001 + name: Supplier3_154 + - class: SupplierFacility + configs: *id002 + name: Supplier1_154 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_154 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_154 + - class: SupplierFacility + configs: *id001 + name: Supplier3_155 + - class: SupplierFacility + configs: *id002 + name: Supplier1_155 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_155 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_155 + - class: SupplierFacility + configs: *id001 + name: Supplier3_156 + - class: SupplierFacility + configs: *id002 + name: Supplier1_156 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_156 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_156 + - class: SupplierFacility + configs: *id001 + name: Supplier3_157 + - class: SupplierFacility + configs: *id002 + name: Supplier1_157 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_157 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_157 + - class: SupplierFacility + configs: *id001 + name: Supplier3_158 + - class: SupplierFacility + configs: *id002 + name: Supplier1_158 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_158 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_158 + - class: SupplierFacility + configs: *id001 + name: Supplier3_159 + - class: SupplierFacility + configs: *id002 + name: Supplier1_159 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_159 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_159 + - class: SupplierFacility + configs: *id001 + name: Supplier3_160 + - class: SupplierFacility + configs: *id002 + name: Supplier1_160 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_160 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_160 + - class: SupplierFacility + configs: *id001 + name: Supplier3_161 + - class: SupplierFacility + configs: *id002 + name: Supplier1_161 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_161 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_161 + - class: SupplierFacility + configs: *id001 + name: Supplier3_162 + - class: SupplierFacility + configs: *id002 + name: Supplier1_162 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_162 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_162 + - class: SupplierFacility + configs: *id001 + name: Supplier3_163 + - class: SupplierFacility + configs: *id002 + name: Supplier1_163 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_163 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_163 + - class: SupplierFacility + configs: *id001 + name: Supplier3_164 + - class: SupplierFacility + configs: *id002 + name: Supplier1_164 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_164 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_164 + - class: SupplierFacility + configs: *id001 + name: Supplier3_165 + - class: SupplierFacility + configs: *id002 + name: Supplier1_165 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_165 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_165 + - class: SupplierFacility + configs: *id001 + name: Supplier3_166 + - class: SupplierFacility + configs: *id002 + name: Supplier1_166 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_166 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_166 + - class: SupplierFacility + configs: *id001 + name: Supplier3_167 + - class: SupplierFacility + configs: *id002 + name: Supplier1_167 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_167 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_167 + - class: SupplierFacility + configs: *id001 + name: Supplier3_168 + - class: SupplierFacility + configs: *id002 + name: Supplier1_168 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_168 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_168 + - class: SupplierFacility + configs: *id001 + name: Supplier3_169 + - class: SupplierFacility + configs: *id002 + name: Supplier1_169 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_169 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_169 + - class: SupplierFacility + configs: *id001 + name: Supplier3_170 + - class: SupplierFacility + configs: *id002 + name: Supplier1_170 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_170 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_170 + - class: SupplierFacility + configs: *id001 + name: Supplier3_171 + - class: SupplierFacility + configs: *id002 + name: Supplier1_171 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_171 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_171 + - class: SupplierFacility + configs: *id001 + name: Supplier3_172 + - class: SupplierFacility + configs: *id002 + name: Supplier1_172 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_172 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_172 + - class: SupplierFacility + configs: *id001 + name: Supplier3_173 + - class: SupplierFacility + configs: *id002 + name: Supplier1_173 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_173 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_173 + - class: SupplierFacility + configs: *id001 + name: Supplier3_174 + - class: SupplierFacility + configs: *id002 + name: Supplier1_174 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_174 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_174 + - class: SupplierFacility + configs: *id001 + name: Supplier3_175 + - class: SupplierFacility + configs: *id002 + name: Supplier1_175 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_175 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_175 + - class: SupplierFacility + configs: *id001 + name: Supplier3_176 + - class: SupplierFacility + configs: *id002 + name: Supplier1_176 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_176 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_176 + - class: SupplierFacility + configs: *id001 + name: Supplier3_177 + - class: SupplierFacility + configs: *id002 + name: Supplier1_177 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_177 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_177 + - class: SupplierFacility + configs: *id001 + name: Supplier3_178 + - class: SupplierFacility + configs: *id002 + name: Supplier1_178 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_178 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_178 + - class: SupplierFacility + configs: *id001 + name: Supplier3_179 + - class: SupplierFacility + configs: *id002 + name: Supplier1_179 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_179 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_179 + - class: SupplierFacility + configs: *id001 + name: Supplier3_180 + - class: SupplierFacility + configs: *id002 + name: Supplier1_180 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_180 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_180 + - class: SupplierFacility + configs: *id001 + name: Supplier3_181 + - class: SupplierFacility + configs: *id002 + name: Supplier1_181 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_181 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_181 + - class: SupplierFacility + configs: *id001 + name: Supplier3_182 + - class: SupplierFacility + configs: *id002 + name: Supplier1_182 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_182 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_182 + - class: SupplierFacility + configs: *id001 + name: Supplier3_183 + - class: SupplierFacility + configs: *id002 + name: Supplier1_183 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_183 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_183 + - class: SupplierFacility + configs: *id001 + name: Supplier3_184 + - class: SupplierFacility + configs: *id002 + name: Supplier1_184 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_184 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_184 + - class: SupplierFacility + configs: *id001 + name: Supplier3_185 + - class: SupplierFacility + configs: *id002 + name: Supplier1_185 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_185 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_185 + - class: SupplierFacility + configs: *id001 + name: Supplier3_186 + - class: SupplierFacility + configs: *id002 + name: Supplier1_186 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_186 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_186 + - class: SupplierFacility + configs: *id001 + name: Supplier3_187 + - class: SupplierFacility + configs: *id002 + name: Supplier1_187 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_187 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_187 + - class: SupplierFacility + configs: *id001 + name: Supplier3_188 + - class: SupplierFacility + configs: *id002 + name: Supplier1_188 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_188 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_188 + - class: SupplierFacility + configs: *id001 + name: Supplier3_189 + - class: SupplierFacility + configs: *id002 + name: Supplier1_189 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_189 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_189 + - class: SupplierFacility + configs: *id001 + name: Supplier3_190 + - class: SupplierFacility + configs: *id002 + name: Supplier1_190 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_190 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_190 + - class: SupplierFacility + configs: *id001 + name: Supplier3_191 + - class: SupplierFacility + configs: *id002 + name: Supplier1_191 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_191 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_191 + - class: SupplierFacility + configs: *id001 + name: Supplier3_192 + - class: SupplierFacility + configs: *id002 + name: Supplier1_192 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_192 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_192 + - class: SupplierFacility + configs: *id001 + name: Supplier3_193 + - class: SupplierFacility + configs: *id002 + name: Supplier1_193 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_193 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_193 + - class: SupplierFacility + configs: *id001 + name: Supplier3_194 + - class: SupplierFacility + configs: *id002 + name: Supplier1_194 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_194 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_194 + - class: SupplierFacility + configs: *id001 + name: Supplier3_195 + - class: SupplierFacility + configs: *id002 + name: Supplier1_195 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_195 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_195 + - class: SupplierFacility + configs: *id001 + name: Supplier3_196 + - class: SupplierFacility + configs: *id002 + name: Supplier1_196 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_196 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_196 + - class: SupplierFacility + configs: *id001 + name: Supplier3_197 + - class: SupplierFacility + configs: *id002 + name: Supplier1_197 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_197 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_197 + - class: SupplierFacility + configs: *id001 + name: Supplier3_198 + - class: SupplierFacility + configs: *id002 + name: Supplier1_198 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_198 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_198 + - class: SupplierFacility + configs: *id001 + name: Supplier3_199 + - class: SupplierFacility + configs: *id002 + name: Supplier1_199 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_199 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_199 + - class: SupplierFacility + configs: *id001 + name: Supplier3_200 + - class: SupplierFacility + configs: *id002 + name: Supplier1_200 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_200 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_200 + - class: SupplierFacility + configs: *id001 + name: Supplier3_201 + - class: SupplierFacility + configs: *id002 + name: Supplier1_201 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_201 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_201 + - class: SupplierFacility + configs: *id001 + name: Supplier3_202 + - class: SupplierFacility + configs: *id002 + name: Supplier1_202 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_202 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_202 + - class: SupplierFacility + configs: *id001 + name: Supplier3_203 + - class: SupplierFacility + configs: *id002 + name: Supplier1_203 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_203 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_203 + - class: SupplierFacility + configs: *id001 + name: Supplier3_204 + - class: SupplierFacility + configs: *id002 + name: Supplier1_204 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_204 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_204 + - class: SupplierFacility + configs: *id001 + name: Supplier3_205 + - class: SupplierFacility + configs: *id002 + name: Supplier1_205 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_205 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_205 + - class: SupplierFacility + configs: *id001 + name: Supplier3_206 + - class: SupplierFacility + configs: *id002 + name: Supplier1_206 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_206 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_206 + - class: SupplierFacility + configs: *id001 + name: Supplier3_207 + - class: SupplierFacility + configs: *id002 + name: Supplier1_207 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_207 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_207 + - class: SupplierFacility + configs: *id001 + name: Supplier3_208 + - class: SupplierFacility + configs: *id002 + name: Supplier1_208 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_208 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_208 + - class: SupplierFacility + configs: *id001 + name: Supplier3_209 + - class: SupplierFacility + configs: *id002 + name: Supplier1_209 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_209 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_209 + - class: SupplierFacility + configs: *id001 + name: Supplier3_210 + - class: SupplierFacility + configs: *id002 + name: Supplier1_210 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_210 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_210 + - class: SupplierFacility + configs: *id001 + name: Supplier3_211 + - class: SupplierFacility + configs: *id002 + name: Supplier1_211 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_211 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_211 + - class: SupplierFacility + configs: *id001 + name: Supplier3_212 + - class: SupplierFacility + configs: *id002 + name: Supplier1_212 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_212 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_212 + - class: SupplierFacility + configs: *id001 + name: Supplier3_213 + - class: SupplierFacility + configs: *id002 + name: Supplier1_213 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_213 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_213 + - class: SupplierFacility + configs: *id001 + name: Supplier3_214 + - class: SupplierFacility + configs: *id002 + name: Supplier1_214 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_214 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_214 + - class: SupplierFacility + configs: *id001 + name: Supplier3_215 + - class: SupplierFacility + configs: *id002 + name: Supplier1_215 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_215 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_215 + - class: SupplierFacility + configs: *id001 + name: Supplier3_216 + - class: SupplierFacility + configs: *id002 + name: Supplier1_216 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_216 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_216 + - class: SupplierFacility + configs: *id001 + name: Supplier3_217 + - class: SupplierFacility + configs: *id002 + name: Supplier1_217 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_217 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_217 + - class: SupplierFacility + configs: *id001 + name: Supplier3_218 + - class: SupplierFacility + configs: *id002 + name: Supplier1_218 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_218 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_218 + - class: SupplierFacility + configs: *id001 + name: Supplier3_219 + - class: SupplierFacility + configs: *id002 + name: Supplier1_219 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_219 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_219 + - class: SupplierFacility + configs: *id001 + name: Supplier3_220 + - class: SupplierFacility + configs: *id002 + name: Supplier1_220 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_220 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_220 + - class: SupplierFacility + configs: *id001 + name: Supplier3_221 + - class: SupplierFacility + configs: *id002 + name: Supplier1_221 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_221 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_221 + - class: SupplierFacility + configs: *id001 + name: Supplier3_222 + - class: SupplierFacility + configs: *id002 + name: Supplier1_222 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_222 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_222 + - class: SupplierFacility + configs: *id001 + name: Supplier3_223 + - class: SupplierFacility + configs: *id002 + name: Supplier1_223 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_223 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_223 + - class: SupplierFacility + configs: *id001 + name: Supplier3_224 + - class: SupplierFacility + configs: *id002 + name: Supplier1_224 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_224 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_224 + - class: SupplierFacility + configs: *id001 + name: Supplier3_225 + - class: SupplierFacility + configs: *id002 + name: Supplier1_225 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_225 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_225 + - class: SupplierFacility + configs: *id001 + name: Supplier3_226 + - class: SupplierFacility + configs: *id002 + name: Supplier1_226 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_226 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_226 + - class: SupplierFacility + configs: *id001 + name: Supplier3_227 + - class: SupplierFacility + configs: *id002 + name: Supplier1_227 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_227 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_227 + - class: SupplierFacility + configs: *id001 + name: Supplier3_228 + - class: SupplierFacility + configs: *id002 + name: Supplier1_228 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_228 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_228 + - class: SupplierFacility + configs: *id001 + name: Supplier3_229 + - class: SupplierFacility + configs: *id002 + name: Supplier1_229 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_229 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_229 + - class: SupplierFacility + configs: *id001 + name: Supplier3_230 + - class: SupplierFacility + configs: *id002 + name: Supplier1_230 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_230 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_230 + - class: SupplierFacility + configs: *id001 + name: Supplier3_231 + - class: SupplierFacility + configs: *id002 + name: Supplier1_231 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_231 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_231 + - class: SupplierFacility + configs: *id001 + name: Supplier3_232 + - class: SupplierFacility + configs: *id002 + name: Supplier1_232 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_232 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_232 + - class: SupplierFacility + configs: *id001 + name: Supplier3_233 + - class: SupplierFacility + configs: *id002 + name: Supplier1_233 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_233 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_233 + - class: SupplierFacility + configs: *id001 + name: Supplier3_234 + - class: SupplierFacility + configs: *id002 + name: Supplier1_234 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_234 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_234 + - class: SupplierFacility + configs: *id001 + name: Supplier3_235 + - class: SupplierFacility + configs: *id002 + name: Supplier1_235 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_235 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_235 + - class: SupplierFacility + configs: *id001 + name: Supplier3_236 + - class: SupplierFacility + configs: *id002 + name: Supplier1_236 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_236 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_236 + - class: SupplierFacility + configs: *id001 + name: Supplier3_237 + - class: SupplierFacility + configs: *id002 + name: Supplier1_237 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_237 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_237 + - class: SupplierFacility + configs: *id001 + name: Supplier3_238 + - class: SupplierFacility + configs: *id002 + name: Supplier1_238 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_238 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_238 + - class: SupplierFacility + configs: *id001 + name: Supplier3_239 + - class: SupplierFacility + configs: *id002 + name: Supplier1_239 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_239 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_239 + - class: SupplierFacility + configs: *id001 + name: Supplier3_240 + - class: SupplierFacility + configs: *id002 + name: Supplier1_240 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_240 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_240 + - class: SupplierFacility + configs: *id001 + name: Supplier3_241 + - class: SupplierFacility + configs: *id002 + name: Supplier1_241 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_241 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_241 + - class: SupplierFacility + configs: *id001 + name: Supplier3_242 + - class: SupplierFacility + configs: *id002 + name: Supplier1_242 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_242 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_242 + - class: SupplierFacility + configs: *id001 + name: Supplier3_243 + - class: SupplierFacility + configs: *id002 + name: Supplier1_243 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_243 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_243 + - class: SupplierFacility + configs: *id001 + name: Supplier3_244 + - class: SupplierFacility + configs: *id002 + name: Supplier1_244 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_244 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_244 + - class: SupplierFacility + configs: *id001 + name: Supplier3_245 + - class: SupplierFacility + configs: *id002 + name: Supplier1_245 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_245 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_245 + - class: SupplierFacility + configs: *id001 + name: Supplier3_246 + - class: SupplierFacility + configs: *id002 + name: Supplier1_246 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_246 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_246 + - class: SupplierFacility + configs: *id001 + name: Supplier3_247 + - class: SupplierFacility + configs: *id002 + name: Supplier1_247 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_247 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_247 + - class: SupplierFacility + configs: *id001 + name: Supplier3_248 + - class: SupplierFacility + configs: *id002 + name: Supplier1_248 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_248 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_248 + - class: SupplierFacility + configs: *id001 + name: Supplier3_249 + - class: SupplierFacility + configs: *id002 + name: Supplier1_249 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_249 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_249 + - class: SupplierFacility + configs: *id001 + name: Supplier3_250 + - class: SupplierFacility + configs: *id002 + name: Supplier1_250 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_250 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_250 + - class: SupplierFacility + configs: *id001 + name: Supplier3_251 + - class: SupplierFacility + configs: *id002 + name: Supplier1_251 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_251 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_251 + - class: SupplierFacility + configs: *id001 + name: Supplier3_252 + - class: SupplierFacility + configs: *id002 + name: Supplier1_252 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_252 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_252 + - class: SupplierFacility + configs: *id001 + name: Supplier3_253 + - class: SupplierFacility + configs: *id002 + name: Supplier1_253 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_253 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_253 + - class: SupplierFacility + configs: *id001 + name: Supplier3_254 + - class: SupplierFacility + configs: *id002 + name: Supplier1_254 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_254 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_254 + - class: SupplierFacility + configs: *id001 + name: Supplier3_255 + - class: SupplierFacility + configs: *id002 + name: Supplier1_255 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_255 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_255 + - class: SupplierFacility + configs: *id001 + name: Supplier3_256 + - class: SupplierFacility + configs: *id002 + name: Supplier1_256 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_256 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_256 + - class: SupplierFacility + configs: *id001 + name: Supplier3_257 + - class: SupplierFacility + configs: *id002 + name: Supplier1_257 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_257 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_257 + - class: SupplierFacility + configs: *id001 + name: Supplier3_258 + - class: SupplierFacility + configs: *id002 + name: Supplier1_258 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_258 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_258 + - class: SupplierFacility + configs: *id001 + name: Supplier3_259 + - class: SupplierFacility + configs: *id002 + name: Supplier1_259 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_259 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_259 + - class: SupplierFacility + configs: *id001 + name: Supplier3_260 + - class: SupplierFacility + configs: *id002 + name: Supplier1_260 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_260 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_260 + - class: SupplierFacility + configs: *id001 + name: Supplier3_261 + - class: SupplierFacility + configs: *id002 + name: Supplier1_261 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_261 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_261 + - class: SupplierFacility + configs: *id001 + name: Supplier3_262 + - class: SupplierFacility + configs: *id002 + name: Supplier1_262 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_262 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_262 + - class: SupplierFacility + configs: *id001 + name: Supplier3_263 + - class: SupplierFacility + configs: *id002 + name: Supplier1_263 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_263 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_263 + - class: SupplierFacility + configs: *id001 + name: Supplier3_264 + - class: SupplierFacility + configs: *id002 + name: Supplier1_264 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_264 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_264 + - class: SupplierFacility + configs: *id001 + name: Supplier3_265 + - class: SupplierFacility + configs: *id002 + name: Supplier1_265 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_265 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_265 + - class: SupplierFacility + configs: *id001 + name: Supplier3_266 + - class: SupplierFacility + configs: *id002 + name: Supplier1_266 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_266 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_266 + - class: SupplierFacility + configs: *id001 + name: Supplier3_267 + - class: SupplierFacility + configs: *id002 + name: Supplier1_267 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_267 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_267 + - class: SupplierFacility + configs: *id001 + name: Supplier3_268 + - class: SupplierFacility + configs: *id002 + name: Supplier1_268 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_268 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_268 + - class: SupplierFacility + configs: *id001 + name: Supplier3_269 + - class: SupplierFacility + configs: *id002 + name: Supplier1_269 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_269 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_269 + - class: SupplierFacility + configs: *id001 + name: Supplier3_270 + - class: SupplierFacility + configs: *id002 + name: Supplier1_270 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_270 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_270 + - class: SupplierFacility + configs: *id001 + name: Supplier3_271 + - class: SupplierFacility + configs: *id002 + name: Supplier1_271 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_271 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_271 + - class: SupplierFacility + configs: *id001 + name: Supplier3_272 + - class: SupplierFacility + configs: *id002 + name: Supplier1_272 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_272 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_272 + - class: SupplierFacility + configs: *id001 + name: Supplier3_273 + - class: SupplierFacility + configs: *id002 + name: Supplier1_273 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_273 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_273 + - class: SupplierFacility + configs: *id001 + name: Supplier3_274 + - class: SupplierFacility + configs: *id002 + name: Supplier1_274 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_274 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_274 + - class: SupplierFacility + configs: *id001 + name: Supplier3_275 + - class: SupplierFacility + configs: *id002 + name: Supplier1_275 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_275 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_275 + - class: SupplierFacility + configs: *id001 + name: Supplier3_276 + - class: SupplierFacility + configs: *id002 + name: Supplier1_276 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_276 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_276 + - class: SupplierFacility + configs: *id001 + name: Supplier3_277 + - class: SupplierFacility + configs: *id002 + name: Supplier1_277 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_277 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_277 + - class: SupplierFacility + configs: *id001 + name: Supplier3_278 + - class: SupplierFacility + configs: *id002 + name: Supplier1_278 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_278 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_278 + - class: SupplierFacility + configs: *id001 + name: Supplier3_279 + - class: SupplierFacility + configs: *id002 + name: Supplier1_279 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_279 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_279 + - class: SupplierFacility + configs: *id001 + name: Supplier3_280 + - class: SupplierFacility + configs: *id002 + name: Supplier1_280 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_280 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_280 + - class: SupplierFacility + configs: *id001 + name: Supplier3_281 + - class: SupplierFacility + configs: *id002 + name: Supplier1_281 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_281 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_281 + - class: SupplierFacility + configs: *id001 + name: Supplier3_282 + - class: SupplierFacility + configs: *id002 + name: Supplier1_282 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_282 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_282 + - class: SupplierFacility + configs: *id001 + name: Supplier3_283 + - class: SupplierFacility + configs: *id002 + name: Supplier1_283 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_283 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_283 + - class: SupplierFacility + configs: *id001 + name: Supplier3_284 + - class: SupplierFacility + configs: *id002 + name: Supplier1_284 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_284 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_284 + - class: SupplierFacility + configs: *id001 + name: Supplier3_285 + - class: SupplierFacility + configs: *id002 + name: Supplier1_285 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_285 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_285 + - class: SupplierFacility + configs: *id001 + name: Supplier3_286 + - class: SupplierFacility + configs: *id002 + name: Supplier1_286 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_286 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_286 + - class: SupplierFacility + configs: *id001 + name: Supplier3_287 + - class: SupplierFacility + configs: *id002 + name: Supplier1_287 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_287 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_287 + - class: SupplierFacility + configs: *id001 + name: Supplier3_288 + - class: SupplierFacility + configs: *id002 + name: Supplier1_288 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_288 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_288 + - class: SupplierFacility + configs: *id001 + name: Supplier3_289 + - class: SupplierFacility + configs: *id002 + name: Supplier1_289 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_289 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_289 + - class: SupplierFacility + configs: *id001 + name: Supplier3_290 + - class: SupplierFacility + configs: *id002 + name: Supplier1_290 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_290 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_290 + - class: SupplierFacility + configs: *id001 + name: Supplier3_291 + - class: SupplierFacility + configs: *id002 + name: Supplier1_291 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_291 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_291 + - class: SupplierFacility + configs: *id001 + name: Supplier3_292 + - class: SupplierFacility + configs: *id002 + name: Supplier1_292 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_292 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_292 + - class: SupplierFacility + configs: *id001 + name: Supplier3_293 + - class: SupplierFacility + configs: *id002 + name: Supplier1_293 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_293 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_293 + - class: SupplierFacility + configs: *id001 + name: Supplier3_294 + - class: SupplierFacility + configs: *id002 + name: Supplier1_294 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_294 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_294 + - class: SupplierFacility + configs: *id001 + name: Supplier3_295 + - class: SupplierFacility + configs: *id002 + name: Supplier1_295 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_295 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_295 + - class: SupplierFacility + configs: *id001 + name: Supplier3_296 + - class: SupplierFacility + configs: *id002 + name: Supplier1_296 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_296 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_296 + - class: SupplierFacility + configs: *id001 + name: Supplier3_297 + - class: SupplierFacility + configs: *id002 + name: Supplier1_297 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_297 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_297 + - class: SupplierFacility + configs: *id001 + name: Supplier3_298 + - class: SupplierFacility + configs: *id002 + name: Supplier1_298 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_298 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_298 + - class: SupplierFacility + configs: *id001 + name: Supplier3_299 + - class: SupplierFacility + configs: *id002 + name: Supplier1_299 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_299 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_299 + - class: SupplierFacility + configs: *id001 + name: Supplier3_300 + - class: SupplierFacility + configs: *id002 + name: Supplier1_300 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_300 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_300 + - class: SupplierFacility + configs: *id001 + name: Supplier3_301 + - class: SupplierFacility + configs: *id002 + name: Supplier1_301 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_301 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_301 + - class: SupplierFacility + configs: *id001 + name: Supplier3_302 + - class: SupplierFacility + configs: *id002 + name: Supplier1_302 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_302 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_302 + - class: SupplierFacility + configs: *id001 + name: Supplier3_303 + - class: SupplierFacility + configs: *id002 + name: Supplier1_303 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_303 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_303 + - class: SupplierFacility + configs: *id001 + name: Supplier3_304 + - class: SupplierFacility + configs: *id002 + name: Supplier1_304 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_304 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_304 + - class: SupplierFacility + configs: *id001 + name: Supplier3_305 + - class: SupplierFacility + configs: *id002 + name: Supplier1_305 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_305 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_305 + - class: SupplierFacility + configs: *id001 + name: Supplier3_306 + - class: SupplierFacility + configs: *id002 + name: Supplier1_306 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_306 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_306 + - class: SupplierFacility + configs: *id001 + name: Supplier3_307 + - class: SupplierFacility + configs: *id002 + name: Supplier1_307 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_307 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_307 + - class: SupplierFacility + configs: *id001 + name: Supplier3_308 + - class: SupplierFacility + configs: *id002 + name: Supplier1_308 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_308 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_308 + - class: SupplierFacility + configs: *id001 + name: Supplier3_309 + - class: SupplierFacility + configs: *id002 + name: Supplier1_309 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_309 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_309 + - class: SupplierFacility + configs: *id001 + name: Supplier3_310 + - class: SupplierFacility + configs: *id002 + name: Supplier1_310 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_310 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_310 + - class: SupplierFacility + configs: *id001 + name: Supplier3_311 + - class: SupplierFacility + configs: *id002 + name: Supplier1_311 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_311 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_311 + - class: SupplierFacility + configs: *id001 + name: Supplier3_312 + - class: SupplierFacility + configs: *id002 + name: Supplier1_312 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_312 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_312 + - class: SupplierFacility + configs: *id001 + name: Supplier3_313 + - class: SupplierFacility + configs: *id002 + name: Supplier1_313 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_313 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_313 + - class: SupplierFacility + configs: *id001 + name: Supplier3_314 + - class: SupplierFacility + configs: *id002 + name: Supplier1_314 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_314 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_314 + - class: SupplierFacility + configs: *id001 + name: Supplier3_315 + - class: SupplierFacility + configs: *id002 + name: Supplier1_315 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_315 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_315 + - class: SupplierFacility + configs: *id001 + name: Supplier3_316 + - class: SupplierFacility + configs: *id002 + name: Supplier1_316 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_316 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_316 + - class: SupplierFacility + configs: *id001 + name: Supplier3_317 + - class: SupplierFacility + configs: *id002 + name: Supplier1_317 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_317 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_317 + - class: SupplierFacility + configs: *id001 + name: Supplier3_318 + - class: SupplierFacility + configs: *id002 + name: Supplier1_318 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_318 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_318 + - class: SupplierFacility + configs: *id001 + name: Supplier3_319 + - class: SupplierFacility + configs: *id002 + name: Supplier1_319 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_319 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_319 + - class: SupplierFacility + configs: *id001 + name: Supplier3_320 + - class: SupplierFacility + configs: *id002 + name: Supplier1_320 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_320 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_320 + - class: SupplierFacility + configs: *id001 + name: Supplier3_321 + - class: SupplierFacility + configs: *id002 + name: Supplier1_321 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_321 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_321 + - class: SupplierFacility + configs: *id001 + name: Supplier3_322 + - class: SupplierFacility + configs: *id002 + name: Supplier1_322 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_322 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_322 + - class: SupplierFacility + configs: *id001 + name: Supplier3_323 + - class: SupplierFacility + configs: *id002 + name: Supplier1_323 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_323 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_323 + - class: SupplierFacility + configs: *id001 + name: Supplier3_324 + - class: SupplierFacility + configs: *id002 + name: Supplier1_324 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_324 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_324 + - class: SupplierFacility + configs: *id001 + name: Supplier3_325 + - class: SupplierFacility + configs: *id002 + name: Supplier1_325 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_325 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_325 + - class: SupplierFacility + configs: *id001 + name: Supplier3_326 + - class: SupplierFacility + configs: *id002 + name: Supplier1_326 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_326 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_326 + - class: SupplierFacility + configs: *id001 + name: Supplier3_327 + - class: SupplierFacility + configs: *id002 + name: Supplier1_327 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_327 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_327 + - class: SupplierFacility + configs: *id001 + name: Supplier3_328 + - class: SupplierFacility + configs: *id002 + name: Supplier1_328 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_328 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_328 + - class: SupplierFacility + configs: *id001 + name: Supplier3_329 + - class: SupplierFacility + configs: *id002 + name: Supplier1_329 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_329 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_329 + - class: SupplierFacility + configs: *id001 + name: Supplier3_330 + - class: SupplierFacility + configs: *id002 + name: Supplier1_330 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_330 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_330 + - class: SupplierFacility + configs: *id001 + name: Supplier3_331 + - class: SupplierFacility + configs: *id002 + name: Supplier1_331 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_331 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_331 + - class: SupplierFacility + configs: *id001 + name: Supplier3_332 + - class: SupplierFacility + configs: *id002 + name: Supplier1_332 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_332 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_332 + - class: SupplierFacility + configs: *id001 + name: Supplier3_333 + - class: SupplierFacility + configs: *id002 + name: Supplier1_333 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_333 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_333 + - class: SupplierFacility + configs: *id001 + name: Supplier3_334 + - class: SupplierFacility + configs: *id002 + name: Supplier1_334 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_334 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_334 + - class: SupplierFacility + configs: *id001 + name: Supplier3_335 + - class: SupplierFacility + configs: *id002 + name: Supplier1_335 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_335 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_335 + - class: SupplierFacility + configs: *id001 + name: Supplier3_336 + - class: SupplierFacility + configs: *id002 + name: Supplier1_336 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_336 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_336 + - class: SupplierFacility + configs: *id001 + name: Supplier3_337 + - class: SupplierFacility + configs: *id002 + name: Supplier1_337 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_337 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_337 + - class: SupplierFacility + configs: *id001 + name: Supplier3_338 + - class: SupplierFacility + configs: *id002 + name: Supplier1_338 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_338 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_338 + - class: SupplierFacility + configs: *id001 + name: Supplier3_339 + - class: SupplierFacility + configs: *id002 + name: Supplier1_339 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_339 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_339 + - class: SupplierFacility + configs: *id001 + name: Supplier3_340 + - class: SupplierFacility + configs: *id002 + name: Supplier1_340 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_340 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_340 + - class: SupplierFacility + configs: *id001 + name: Supplier3_341 + - class: SupplierFacility + configs: *id002 + name: Supplier1_341 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_341 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_341 + - class: SupplierFacility + configs: *id001 + name: Supplier3_342 + - class: SupplierFacility + configs: *id002 + name: Supplier1_342 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_342 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_342 + - class: SupplierFacility + configs: *id001 + name: Supplier3_343 + - class: SupplierFacility + configs: *id002 + name: Supplier1_343 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_343 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_343 + - class: SupplierFacility + configs: *id001 + name: Supplier3_344 + - class: SupplierFacility + configs: *id002 + name: Supplier1_344 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_344 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_344 + - class: SupplierFacility + configs: *id001 + name: Supplier3_345 + - class: SupplierFacility + configs: *id002 + name: Supplier1_345 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_345 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_345 + - class: SupplierFacility + configs: *id001 + name: Supplier3_346 + - class: SupplierFacility + configs: *id002 + name: Supplier1_346 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_346 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_346 + - class: SupplierFacility + configs: *id001 + name: Supplier3_347 + - class: SupplierFacility + configs: *id002 + name: Supplier1_347 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_347 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_347 + - class: SupplierFacility + configs: *id001 + name: Supplier3_348 + - class: SupplierFacility + configs: *id002 + name: Supplier1_348 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_348 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_348 + - class: SupplierFacility + configs: *id001 + name: Supplier3_349 + - class: SupplierFacility + configs: *id002 + name: Supplier1_349 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_349 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_349 + - class: SupplierFacility + configs: *id001 + name: Supplier3_350 + - class: SupplierFacility + configs: *id002 + name: Supplier1_350 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_350 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_350 + - class: SupplierFacility + configs: *id001 + name: Supplier3_351 + - class: SupplierFacility + configs: *id002 + name: Supplier1_351 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_351 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_351 + - class: SupplierFacility + configs: *id001 + name: Supplier3_352 + - class: SupplierFacility + configs: *id002 + name: Supplier1_352 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_352 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_352 + - class: SupplierFacility + configs: *id001 + name: Supplier3_353 + - class: SupplierFacility + configs: *id002 + name: Supplier1_353 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_353 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_353 + - class: SupplierFacility + configs: *id001 + name: Supplier3_354 + - class: SupplierFacility + configs: *id002 + name: Supplier1_354 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_354 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_354 + - class: SupplierFacility + configs: *id001 + name: Supplier3_355 + - class: SupplierFacility + configs: *id002 + name: Supplier1_355 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_355 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_355 + - class: SupplierFacility + configs: *id001 + name: Supplier3_356 + - class: SupplierFacility + configs: *id002 + name: Supplier1_356 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_356 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_356 + - class: SupplierFacility + configs: *id001 + name: Supplier3_357 + - class: SupplierFacility + configs: *id002 + name: Supplier1_357 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_357 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_357 + - class: SupplierFacility + configs: *id001 + name: Supplier3_358 + - class: SupplierFacility + configs: *id002 + name: Supplier1_358 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_358 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_358 + - class: SupplierFacility + configs: *id001 + name: Supplier3_359 + - class: SupplierFacility + configs: *id002 + name: Supplier1_359 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_359 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_359 + - class: SupplierFacility + configs: *id001 + name: Supplier3_360 + - class: SupplierFacility + configs: *id002 + name: Supplier1_360 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_360 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_360 + - class: SupplierFacility + configs: *id001 + name: Supplier3_361 + - class: SupplierFacility + configs: *id002 + name: Supplier1_361 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_361 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_361 + - class: SupplierFacility + configs: *id001 + name: Supplier3_362 + - class: SupplierFacility + configs: *id002 + name: Supplier1_362 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_362 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_362 + - class: SupplierFacility + configs: *id001 + name: Supplier3_363 + - class: SupplierFacility + configs: *id002 + name: Supplier1_363 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_363 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_363 + - class: SupplierFacility + configs: *id001 + name: Supplier3_364 + - class: SupplierFacility + configs: *id002 + name: Supplier1_364 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_364 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_364 + - class: SupplierFacility + configs: *id001 + name: Supplier3_365 + - class: SupplierFacility + configs: *id002 + name: Supplier1_365 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_365 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_365 + - class: SupplierFacility + configs: *id001 + name: Supplier3_366 + - class: SupplierFacility + configs: *id002 + name: Supplier1_366 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_366 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_366 + - class: SupplierFacility + configs: *id001 + name: Supplier3_367 + - class: SupplierFacility + configs: *id002 + name: Supplier1_367 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_367 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_367 + - class: SupplierFacility + configs: *id001 + name: Supplier3_368 + - class: SupplierFacility + configs: *id002 + name: Supplier1_368 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_368 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_368 + - class: SupplierFacility + configs: *id001 + name: Supplier3_369 + - class: SupplierFacility + configs: *id002 + name: Supplier1_369 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_369 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_369 + - class: SupplierFacility + configs: *id001 + name: Supplier3_370 + - class: SupplierFacility + configs: *id002 + name: Supplier1_370 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_370 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_370 + - class: SupplierFacility + configs: *id001 + name: Supplier3_371 + - class: SupplierFacility + configs: *id002 + name: Supplier1_371 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_371 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_371 + - class: SupplierFacility + configs: *id001 + name: Supplier3_372 + - class: SupplierFacility + configs: *id002 + name: Supplier1_372 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_372 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_372 + - class: SupplierFacility + configs: *id001 + name: Supplier3_373 + - class: SupplierFacility + configs: *id002 + name: Supplier1_373 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_373 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_373 + - class: SupplierFacility + configs: *id001 + name: Supplier3_374 + - class: SupplierFacility + configs: *id002 + name: Supplier1_374 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_374 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_374 + - class: SupplierFacility + configs: *id001 + name: Supplier3_375 + - class: SupplierFacility + configs: *id002 + name: Supplier1_375 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_375 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_375 + - class: SupplierFacility + configs: *id001 + name: Supplier3_376 + - class: SupplierFacility + configs: *id002 + name: Supplier1_376 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_376 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_376 + - class: SupplierFacility + configs: *id001 + name: Supplier3_377 + - class: SupplierFacility + configs: *id002 + name: Supplier1_377 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_377 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_377 + - class: SupplierFacility + configs: *id001 + name: Supplier3_378 + - class: SupplierFacility + configs: *id002 + name: Supplier1_378 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_378 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_378 + - class: SupplierFacility + configs: *id001 + name: Supplier3_379 + - class: SupplierFacility + configs: *id002 + name: Supplier1_379 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_379 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_379 + - class: SupplierFacility + configs: *id001 + name: Supplier3_380 + - class: SupplierFacility + configs: *id002 + name: Supplier1_380 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_380 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_380 + - class: SupplierFacility + configs: *id001 + name: Supplier3_381 + - class: SupplierFacility + configs: *id002 + name: Supplier1_381 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_381 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_381 + - class: SupplierFacility + configs: *id001 + name: Supplier3_382 + - class: SupplierFacility + configs: *id002 + name: Supplier1_382 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_382 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_382 + - class: SupplierFacility + configs: *id001 + name: Supplier3_383 + - class: SupplierFacility + configs: *id002 + name: Supplier1_383 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_383 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_383 + - class: SupplierFacility + configs: *id001 + name: Supplier3_384 + - class: SupplierFacility + configs: *id002 + name: Supplier1_384 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_384 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_384 + - class: SupplierFacility + configs: *id001 + name: Supplier3_385 + - class: SupplierFacility + configs: *id002 + name: Supplier1_385 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_385 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_385 + - class: SupplierFacility + configs: *id001 + name: Supplier3_386 + - class: SupplierFacility + configs: *id002 + name: Supplier1_386 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_386 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_386 + - class: SupplierFacility + configs: *id001 + name: Supplier3_387 + - class: SupplierFacility + configs: *id002 + name: Supplier1_387 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_387 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_387 + - class: SupplierFacility + configs: *id001 + name: Supplier3_388 + - class: SupplierFacility + configs: *id002 + name: Supplier1_388 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_388 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_388 + - class: SupplierFacility + configs: *id001 + name: Supplier3_389 + - class: SupplierFacility + configs: *id002 + name: Supplier1_389 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_389 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_389 + - class: SupplierFacility + configs: *id001 + name: Supplier3_390 + - class: SupplierFacility + configs: *id002 + name: Supplier1_390 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_390 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_390 + - class: SupplierFacility + configs: *id001 + name: Supplier3_391 + - class: SupplierFacility + configs: *id002 + name: Supplier1_391 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_391 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_391 + - class: SupplierFacility + configs: *id001 + name: Supplier3_392 + - class: SupplierFacility + configs: *id002 + name: Supplier1_392 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_392 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_392 + - class: SupplierFacility + configs: *id001 + name: Supplier3_393 + - class: SupplierFacility + configs: *id002 + name: Supplier1_393 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_393 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_393 + - class: SupplierFacility + configs: *id001 + name: Supplier3_394 + - class: SupplierFacility + configs: *id002 + name: Supplier1_394 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_394 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_394 + - class: SupplierFacility + configs: *id001 + name: Supplier3_395 + - class: SupplierFacility + configs: *id002 + name: Supplier1_395 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_395 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_395 + - class: SupplierFacility + configs: *id001 + name: Supplier3_396 + - class: SupplierFacility + configs: *id002 + name: Supplier1_396 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_396 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_396 + - class: SupplierFacility + configs: *id001 + name: Supplier3_397 + - class: SupplierFacility + configs: *id002 + name: Supplier1_397 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_397 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_397 + - class: SupplierFacility + configs: *id001 + name: Supplier3_398 + - class: SupplierFacility + configs: *id002 + name: Supplier1_398 + - class: WarehouseFacility + configs: *id003 + name: Warehouse1_398 + - class: RetailerFacility + configs: *id004 + name: ChaoShiFa01_398 + grid: + blocks: + railroad: + - - 10 + - 10 + - - 10 + - 11 + - - 10 + - 12 + - - 11 + - 12 + facilities: + ChaoShiFa01: + - 10 + - 18 + Supplier1: + - 0 + - 0 + Supplier3: + - 3 + - 3 + Warehouse1: + - 6 + - 6 + size: + - 20 + - 20 + skus: + - bom: + sku3: 10 + id: 1 + name: sku1 + output_units_per_lot: 1 + - id: 2 + name: sku2 + output_units_per_lot: 1 + - id: 3 + name: sku3 + output_units_per_lot: 1 + step_mode: facility + topology: + ChaoShiFa01: + sku1: + - Supplier1 + sku3: + - Supplier3 + Supplier1: + sku3: + - Supplier3 + Warehouse1: + sku1: + - Supplier1 + sku3: + - Supplier3 diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index a890184a9..9c4c7af87 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -47,7 +47,7 @@ world: skus: sku3: # sku name and attributes needed for this facility init_in_stock: 100 - production_rate: 200 + production_rate: 0 delay_order_penalty: 10 product_unit_cost: 1 type: "production" # production means this is the output production of this facility @@ -78,7 +78,7 @@ world: skus: sku1: init_in_stock: 100 - production_rate: 200 + production_rate: 0 delay_order_penalty: 10 product_unit_cost: 1 type: "production" diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/gen.py b/maro/simulator/scenarios/supply_chain/topologies/sample1/gen.py new file mode 100644 index 000000000..69722d0a4 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/gen.py @@ -0,0 +1,31 @@ +from yaml import safe_load, safe_dump + + +def gen(number: int): + config = None + + with open("config.yml", "rt") as fp: + config = safe_load(fp) + + facilities = config["world"]["facilities"] + + exist_facilities = [] + + for facility in facilities: + exist_facilities.append(facility.copy()) + + for i in range(number-1): + # exist facilities + for facility in exist_facilities: + copied_f = facility.copy() + + copied_f["name"] = f"{facility['name']}_{i}" + + facilities.append(copied_f) + + with open(f"config_{number}.yml", "wt+") as ofp: + safe_dump(config, ofp) + + +if __name__ == "__main__": + gen(1000) From e852abba0b04bca00e7eeed3768f6dce5a031535 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 11 Mar 2021 16:31:11 +0800 Subject: [PATCH 051/482] str to int --- examples/hello_world/supply_chain/pf_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hello_world/supply_chain/pf_test.py b/examples/hello_world/supply_chain/pf_test.py index defc3ec69..b3abd5fad 100644 --- a/examples/hello_world/supply_chain/pf_test.py +++ b/examples/hello_world/supply_chain/pf_test.py @@ -22,7 +22,7 @@ def go(env: Env): if __name__ == "__main__": topology = sys.argv[1] - durations = sys.argv[2] if len(sys.argv) > 2 else 100 + durations = int(sys.argv[2] if len(sys.argv) > 2 else 100) env = Env(scenario="supply_chain", topology=topology, durations=durations) From 6cf8379dcfa9f77ac1114317d0a963c8be5b8e7a Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 11 Mar 2021 17:44:00 +0800 Subject: [PATCH 052/482] simplify the manufacture logic to avoid too much loop --- examples/hello_world/supply_chain/hello.py | 2 +- .../hello_world/supply_chain/state_shaping.py | 52 ++++++--------- .../supply_chain/units/manufacture.py | 63 ++++++++++++++----- 3 files changed, 68 insertions(+), 49 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 0fec2c410..048973154 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -213,7 +213,7 @@ def show_manufacture_states(self): storage_unit_number = len(storages) # manufacture number for current tick - features = ("id", "facility_id", "storage_id", "output_product_id", "product_unit_cost", "production_rate", "manufacturing_number") + features = ("id", "facility_id", "storage_id", "product_id", "product_unit_cost", "production_rate", "manufacturing_number") states = manufactures[self.env.frame_index::features].flatten().reshape(manufacture_unit_number, -1).astype(np.int) # show bom diff --git a/examples/hello_world/supply_chain/state_shaping.py b/examples/hello_world/supply_chain/state_shaping.py index 831f0af19..6bce62c84 100644 --- a/examples/hello_world/supply_chain/state_shaping.py +++ b/examples/hello_world/supply_chain/state_shaping.py @@ -4,8 +4,9 @@ states we used: -. is_positive_balance +facility + sku: +is_positive_balance is_over_stock is_out_of_stock @@ -34,42 +35,27 @@ from maro.simulator import Env -# NOTE: copied from original code -class BalanceSheet: - profit: int = 0 - loss: int = 0 - - def __init__(self, profit:int, loss:int): - self.profit = profit - self.loss = loss - - def total(self) -> int: - return self.profit + self.loss - - def __add__(self, other): - return BalanceSheet(self.profit + other.profit, self.loss + other.loss) - - def __sub__(self, other): - return BalanceSheet(self.profit - other.profit, self.loss - other.loss) - - def __repr__(self): - return f"{round(self.profit + self.loss, 0)} ({round(self.profit, 0)} {round(self.loss, 0)})" - - def __radd__(self, other): - if other == 0: - return self - else: - return self.__add__(other) - - class SupplyChainStateShaping: def __init__(self, env: Env): self._env = env def shape(self): - is_positive_balance = self._origin_group_by_sku_is_positive_balance() + """ + result is a dictionary contains agent type and states of each unit(agent): + { + "producer": { + "manufacture id" : [long list of states] + }, + "consumer": { + "consumer id": [long list of states] + } + } + + """ - print(is_positive_balance) + is_positive_balance = self._origin_group_by_sku_is_positive_balance() + # + return is_positive_balance def _origin_group_by_sku_is_positive_balance(self): # original code collect states of facilities and sku related units (manufacture, seller, consumer), @@ -128,6 +114,8 @@ def _origin_group_by_sku_is_positive_balance(self): sku_is_positive_balance = list(map(filter, sku_balance_sheet_total)) - result.extend(sku_is_positive_balance) + # np.stack() + + result.append(sku_is_positive_balance) return result diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 7849d5fc8..4c85f084c 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -43,24 +43,55 @@ def step(self, tick: int): sku_num = len(self.facility.sku_information) unit_num_upper_bound = self.facility.storage.data.capacity // sku_num - # one lot per time, until no enough space to hold output, or no enough source material - # TODO: simplify this part to make it faster - for _ in range(data.production_rate): - storage_remaining_space = self.facility.storage.data.remaining_space - current_product_number = self.facility.storage.get_product_number(data.product_id) + # compare with avg storage number + current_product_number = self.facility.storage.get_product_number(data.product_id) + max_number_to_procedure = min( + unit_num_upper_bound - current_product_number, + data.production_rate * self.output_units_per_lot, + self.facility.storage.data.remaining_space + ) + + if max_number_to_procedure > 0: space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot - # if remaining space enough to hold output production - if storage_remaining_space >= space_taken_per_cycle: - # if not reach the storage limitation of current production - if current_product_number < unit_num_upper_bound: - # if we do not need any material, then just generate the out product. - # or if we have enough source materials - if len(self.bom) == 0 or self.facility.storage.try_take_units(self.bom): - self.facility.storage.try_add_units({data.product_id: self.output_units_per_lot}) - - # update manufacturing number in state - data.manufacturing_number += 1 + # consider about the volume, we can produce all if space take per cycle <=1 + if space_taken_per_cycle > 1: + max_number_to_procedure = max_number_to_procedure // space_taken_per_cycle + + source_sku_to_take = {} + # do we have enough source material? + for source_sku_id, source_sku_cost_number in self.bom.items(): + source_sku_available_number = self.facility.storage.get_product_number(source_sku_id) + + max_number_to_procedure = min(source_sku_available_number // source_sku_cost_number, max_number_to_procedure) + + if max_number_to_procedure <= 0: + break + + source_sku_to_take[source_sku_id] = max_number_to_procedure * source_sku_cost_number + + if max_number_to_procedure > 0: + data.manufacturing_number += max_number_to_procedure + self.facility.storage.try_take_units(source_sku_to_take) + + # one lot per time, until no enough space to hold output, or no enough source material + # TODO: simplify this part to make it faster + # for _ in range(data.production_rate): + # storage_remaining_space = self.facility.storage.data.remaining_space + # current_product_number = self.facility.storage.get_product_number(data.product_id) + # space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot + + # # if remaining space enough to hold output production + # if storage_remaining_space >= space_taken_per_cycle: + # # if not reach the storage limitation of current production + # if current_product_number < unit_num_upper_bound: + # # if we do not need any material, then just generate the out product. + # # or if we have enough source materials + # if len(self.bom) == 0 or self.facility.storage.try_take_units(self.bom): + # self.facility.storage.try_add_units({data.product_id: self.output_units_per_lot}) + + # # update manufacturing number in state + # data.manufacturing_number += 1 data.balance_sheet_loss = data.manufacturing_number * data.product_unit_cost From 75337bb1ab1e20b93892cbb69070e48506a5b1b0 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 11 Mar 2021 18:53:43 +0800 Subject: [PATCH 053/482] add missing file --- .../scenarios/supply_chain/data/facility.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 maro/simulator/scenarios/supply_chain/data/facility.py diff --git a/maro/simulator/scenarios/supply_chain/data/facility.py b/maro/simulator/scenarios/supply_chain/data/facility.py new file mode 100644 index 000000000..9126585a8 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/data/facility.py @@ -0,0 +1,19 @@ +from .base import DataModelBase + +from maro.backends.frame import node, NodeBase, NodeAttribute +from maro.backends.backend import AttributeType + + +@node("facility") +class FacilityDataModel(DataModelBase): + + test = NodeAttribute(AttributeType.Int) + + def __init__(self): + super(FacilityDataModel, self).__init__() + + def initialize(self, configs: dict): + self.reset() + + def reset(self): + super(FacilityDataModel, self).reset() From 2206e3c71bf36ba195c59fd6c70ce32f9371d25c Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 11 Mar 2021 20:05:11 +0800 Subject: [PATCH 054/482] reduce unit.data property callnum --- .../scenarios/supply_chain/business_engine.py | 4 +++- .../scenarios/supply_chain/facilities/retailer.py | 9 +++++++++ .../scenarios/supply_chain/facilities/supplier.py | 12 ++++++++++++ .../scenarios/supply_chain/facilities/warehouse.py | 9 +++++++++ maro/simulator/scenarios/supply_chain/units/base.py | 11 ++++------- .../scenarios/supply_chain/units/consumer.py | 2 -- 6 files changed, 37 insertions(+), 10 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 5b9ed8233..69fb36a2c 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -79,7 +79,9 @@ def post_step(self, tick: int): def reset(self): self._frame.reset() - self._frame.snapshots.reset() + + if self._frame.snapshots: + self._frame.snapshots.reset() for _, facility in self.world.facilities.items(): facility.reset() diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 48b0fbd71..fc0e24fbc 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -97,6 +97,15 @@ def initialize(self): self.data.set_id(self.id, self.id) self.data.initialize({}) + self.storage.prepare_data() + + # prepare data for units + for consumer in self.consumers.values(): + consumer.prepare_data() + + for seller in self.sellers.values(): + seller.prepare_data() + self._init_by_sku() self.storage.initialize(self.configs.get("storage", {})) diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 0f4c435fa..6e46dc3b7 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -153,6 +153,18 @@ def initialize(self): self.data.set_id(self.id, self.id) self.data.initialize({}) + self.storage.prepare_data() + self.distribution.prepare_data() + + for transport in self.transports: + transport.prepare_data() + + for manufacture in self.manufactures.values(): + manufacture.prepare_data() + + for consumer in self.consumers.values(): + consumer.prepare_data() + # DO init by skus first, as other components may depend on sku information self._init_by_skus() diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 6199065ea..994f12a2e 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -100,6 +100,15 @@ def build(self, configs: dict): def initialize(self): self.data.set_id(self.id, self.id) self.data.initialize({}) + + self.storage.prepare_data() + self.distribution.prepare_data() + + for transport in self.transports: + transport.prepare_data() + + for consumer in self.consumers.values(): + consumer.prepare_data() # init components that related with sku number self._init_by_skus() diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py index 9c4c0a4a2..c106818d1 100644 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -20,15 +20,12 @@ class UnitBase: def __init__(self): # data model instance, it is None until the initialize function called. - self._data = None + self.data = None - @property - def data(self): + def prepare_data(self): """Data model install related to this unit, available after initialized function called.""" - if self._data is None: - self._data = self.world.get_data_instance(self.data_class, self.data_index) - - return self._data + if self.data is None: + self.data = self.world.get_data_instance(self.data_class, self.data_index) def initialize(self, configs: dict): """Initialize current unit""" diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 47335c0c7..9e53fc7a0 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -44,8 +44,6 @@ def step(self, tick: int): # we are using facility as source id, not the storage self.update_open_orders(source_id, product_id, quantity) - sku = self.facility.sku_information[data.product_id] - order = Order(self.facility, product_id, quantity, vlt) source_facility = self.world.get_facility_by_id(source_id) From 74a12d62c03ac12c66448a94527ab6b40956a233 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 11 Mar 2021 20:13:54 +0800 Subject: [PATCH 055/482] remove balance sheet from state --- .../scenarios/supply_chain/data/base.py | 4 ---- .../supply_chain/facilities/retailer.py | 9 -------- .../supply_chain/facilities/supplier.py | 12 ---------- .../supply_chain/facilities/warehouse.py | 9 -------- .../scenarios/supply_chain/units/base.py | 5 +--- .../scenarios/supply_chain/units/consumer.py | 7 ++---- .../supply_chain/units/distribution.py | 5 ---- .../supply_chain/units/manufacture.py | 23 +------------------ .../scenarios/supply_chain/units/seller.py | 6 +---- .../scenarios/supply_chain/units/storage.py | 6 ----- 10 files changed, 5 insertions(+), 81 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/data/base.py b/maro/simulator/scenarios/supply_chain/data/base.py index e6c55c9ee..5d97d69ac 100644 --- a/maro/simulator/scenarios/supply_chain/data/base.py +++ b/maro/simulator/scenarios/supply_chain/data/base.py @@ -12,10 +12,6 @@ class DataModelBase(NodeBase): # id of facility this unit belongs to facility_id = NodeAttribute(AttributeType.Int) - # per tick states - balance_sheet_profit = NodeAttribute(AttributeType.Float) - balance_sheet_loss = NodeAttribute(AttributeType.Float) - def __init__(self): self._unit_id = 0 self._facility_id = 0 diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index fc0e24fbc..decd2b842 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -14,23 +14,14 @@ class RetailerFacility(FacilityBase): def step(self, tick: int): self.storage.step(tick) - self.data.balance_sheet_profit += self.storage.data.balance_sheet_profit - self.data.balance_sheet_loss += self.storage.data.balance_sheet_loss - if self.consumers is not None: for consumer in self.consumers.values(): consumer.step(tick) - self.data.balance_sheet_profit += consumer.data.balance_sheet_profit - self.data.balance_sheet_loss += consumer.data.balance_sheet_loss - if self.sellers is not None: for seller in self.sellers.values(): seller.step(tick) - self.data.balance_sheet_profit += seller.data.balance_sheet_profit - self.data.balance_sheet_loss += seller.data.balance_sheet_loss - def post_step(self, tick: int): self.storage.post_step(tick) diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 6e46dc3b7..b2bf206df 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -18,30 +18,18 @@ class SupplierFacility(FacilityBase): def step(self, tick: int): self.storage.step(tick) - self.data.balance_sheet_profit += self.storage.data.balance_sheet_profit - self.data.balance_sheet_loss += self.storage.data.balance_sheet_loss - for vehicle in self.transports: vehicle.step(tick) for supplier in self.manufactures.values(): supplier.step(tick) - self.data.balance_sheet_profit += supplier.data.balance_sheet_profit - self.data.balance_sheet_loss += supplier.data.balance_sheet_loss - for consumer in self.consumers.values(): consumer.step(tick) - self.data.balance_sheet_profit += consumer.data.balance_sheet_profit - self.data.balance_sheet_loss += consumer.data.balance_sheet_loss - # to make sure the distribution get correct balance sheet, we should update transports first. self.distribution.step(tick) - self.data.balance_sheet_profit += self.distribution.data.balance_sheet_profit - self.data.balance_sheet_loss += self.distribution.data.balance_sheet_loss - def post_step(self, tick: int): self.storage.post_step(tick) self.distribution.post_step(tick) diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 994f12a2e..725831d62 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -23,23 +23,14 @@ class WarehouseFacility(FacilityBase): def step(self, tick: int): self.storage.step(tick) - self.data.balance_sheet_profit += self.storage.data.balance_sheet_profit - self.data.balance_sheet_loss += self.storage.data.balance_sheet_loss - for transport in self.transports: transport.step(tick) for consumer in self.consumers.values(): consumer.step(tick) - self.data.balance_sheet_profit += consumer.data.balance_sheet_profit - self.data.balance_sheet_loss += consumer.data.balance_sheet_loss - self.distribution.step(tick) - self.data.balance_sheet_profit += self.distribution.data.balance_sheet_profit - self.data.balance_sheet_loss += self.distribution.data.balance_sheet_loss - def build(self, configs: dict): self.configs = configs diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py index c106818d1..f59de8aa9 100644 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -39,10 +39,7 @@ def step(self, tick: int): pass def post_step(self, tick: int): - # clear the per tick attributes that all nodes have - if self.data is not None: - self.data.balance_sheet_profit = 0 - self.data.balance_sheet_loss = 0 + pass def get_metrics(self): # called per step diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 9e53fc7a0..eba55d8c6 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -18,7 +18,7 @@ def __init__(self): self.open_orders = defaultdict(Counter) def initialize(self, configs: dict): - super(ConsumerUnit, self).initialize(configs) + #super(ConsumerUnit, self).initialize(configs) if len(self.data.sources) > 0: # we use 1st source as default one @@ -52,11 +52,8 @@ def step(self, tick: int): data.total_purchased += quantity - # update balance sheet - data.balance_sheet_loss = -(data.order_product_cost + data.order_cost) - def post_step(self, tick: int): - super(ConsumerUnit, self).post_step(tick) + # super(ConsumerUnit, self).post_step(tick) data = self.data diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index fc0572900..d278b3fbf 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -51,11 +51,6 @@ def step(self, tick: int): data.delay_order_penalty[product_index] += sku.delay_order_penalty - # update balance sheet, sum of transport balance sheet - if self.facility.transports is not None: - for vehicle in self.facility.transports: - data.balance_sheet_loss += vehicle.data.balance_sheet_loss - def reset(self): super(DistributionUnit, self).reset() self.order_queue.clear() diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 4c85f084c..b2a59975c 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -74,29 +74,8 @@ def step(self, tick: int): data.manufacturing_number += max_number_to_procedure self.facility.storage.try_take_units(source_sku_to_take) - # one lot per time, until no enough space to hold output, or no enough source material - # TODO: simplify this part to make it faster - # for _ in range(data.production_rate): - # storage_remaining_space = self.facility.storage.data.remaining_space - # current_product_number = self.facility.storage.get_product_number(data.product_id) - # space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot - - # # if remaining space enough to hold output production - # if storage_remaining_space >= space_taken_per_cycle: - # # if not reach the storage limitation of current production - # if current_product_number < unit_num_upper_bound: - # # if we do not need any material, then just generate the out product. - # # or if we have enough source materials - # if len(self.bom) == 0 or self.facility.storage.try_take_units(self.bom): - # self.facility.storage.try_add_units({data.product_id: self.output_units_per_lot}) - - # # update manufacturing number in state - # data.manufacturing_number += 1 - - data.balance_sheet_loss = data.manufacturing_number * data.product_unit_cost - def post_step(self, tick: int): - super(ManufactureUnit, self).post_step(tick) + # super(ManufactureUnit, self).post_step(tick) # reset the manufacture cost per tick self.data.manufacturing_number = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index 6e3ba55b0..f2985f242 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -18,7 +18,6 @@ def step(self, tick: int): data = self.data product_id = data.product_id - sku = self.facility.sku_information[product_id] demand = self.market_demand() sold_qty = self.facility.storage.take_available(product_id, demand) @@ -27,11 +26,8 @@ def step(self, tick: int): data.sold = sold_qty data.demand = demand - data.balance_sheet_profit = data.unit_price * sold_qty - data.balance_sheet_loss = -(demand - sold_qty) * data.unit_price * data.backlog_ratio - def post_step(self, tick: int): - super(SellerUnit, self).post_step(tick) + # super(SellerUnit, self).post_step(tick) self.data.sold = 0 self.data.demand = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 577a0a67d..0d3be9370 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -19,12 +19,6 @@ def initialize(self, configs): for index, product_id in enumerate(self.data.product_list[:]): self.product_index_mapping[product_id] = index - def step(self, tick: int): - super(StorageUnit, self).step(tick) - - data = self.data - data.balance_sheet_loss = -(data.capacity - data.remaining_space) * data.unit_storage_cost - def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: if all_or_nothing and self.data.remaining_space < sum(product_quantities.values()): return {} From 0bc213f787e2e840e9cb5df5743e207500410910 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 12 Mar 2021 10:03:42 +0800 Subject: [PATCH 056/482] pre calculate sale gamma instead generate one by one --- .../scenarios/supply_chain/business_engine.py | 8 +++- .../scenarios/supply_chain/data/storage.py | 8 ++-- .../scenarios/supply_chain/facilities/base.py | 7 +++- .../supply_chain/facilities/retailer.py | 28 +++++++++----- .../supply_chain/facilities/supplier.py | 38 ++++++++++++------- .../supply_chain/facilities/warehouse.py | 37 +++++++++++------- .../scenarios/supply_chain/units/base.py | 7 +++- .../scenarios/supply_chain/units/consumer.py | 16 +++----- .../supply_chain/units/distribution.py | 4 +- .../supply_chain/units/manufacture.py | 8 ++-- .../scenarios/supply_chain/units/seller.py | 22 ++++++++--- .../scenarios/supply_chain/units/storage.py | 28 ++++++++++---- .../scenarios/supply_chain/units/transport.py | 5 --- .../simulator/scenarios/supply_chain/world.py | 4 +- 14 files changed, 134 insertions(+), 86 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 69fb36a2c..62ef9549a 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -68,12 +68,16 @@ def _step_by_units(self, tick: int): unit.step(tick) def post_step(self, tick: int): + # before taking snapshot + for facility in self.world.facilities.values(): + facility.begin_post_step(tick) + # take snapshot if (tick + 1) % self._snapshot_resolution == 0: self._frame.take_snapshot(self.frame_index(tick)) for facility in self.world.facilities.values(): - facility.post_step(tick) + facility.end_post_step(tick) return tick+1 == self._max_tick @@ -99,7 +103,7 @@ def _build_world(self): self.world = World() - self.world.build(parser.parse(), self.calc_max_snapshots()) + self.world.build(parser.parse(), self.calc_max_snapshots(), self._max_tick) def _on_action_received(self, event): action = event.payload diff --git a/maro/simulator/scenarios/supply_chain/data/storage.py b/maro/simulator/scenarios/supply_chain/data/storage.py index 2b9e7a2b1..5b3b3c034 100644 --- a/maro/simulator/scenarios/supply_chain/data/storage.py +++ b/maro/simulator/scenarios/supply_chain/data/storage.py @@ -35,8 +35,8 @@ def reset(self): self.remaining_space = self._capacity - def _on_product_number_changed(self, value): - if len(self.product_number) > 0: - taken_number = sum(self.product_number[:]) + # def _on_product_number_changed(self, value): + # if len(self.product_number) > 0: + # taken_number = sum(self.product_number[:]) - self.remaining_space = self.capacity - taken_number + # self.remaining_space = self.capacity - taken_number diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py index b63bcb96e..8c6ee715d 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ b/maro/simulator/scenarios/supply_chain/facilities/base.py @@ -37,7 +37,10 @@ def step(self, tick: int): # called per tick pass - def post_step(self, tick: int): + def begin_post_step(self, tick: int): + pass + + def end_post_step(self, tick: int): pass @abstractmethod @@ -46,7 +49,7 @@ def build(self, configs: dict): pass @abstractmethod - def initialize(self): + def initialize(self, durations: int): # called after data model instance is ready. pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index decd2b842..7050a2889 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -22,19 +22,27 @@ def step(self, tick: int): for seller in self.sellers.values(): seller.step(tick) - def post_step(self, tick: int): - self.storage.post_step(tick) + def begin_post_step(self, tick: int): + self.storage.begin_post_step(tick) if self.consumers is not None: for consumer in self.consumers.values(): - consumer.post_step(tick) + consumer.begin_post_step(tick) if self.sellers is not None: for seller in self.sellers.values(): - seller.post_step(tick) + seller.begin_post_step(tick) - self.data.balance_sheet_profit = 0 - self.data.balance_sheet_loss = 0 + def end_post_step(self, tick: int): + self.storage.end_post_step(tick) + + if self.consumers is not None: + for consumer in self.consumers.values(): + consumer.end_post_step(tick) + + if self.sellers is not None: + for seller in self.sellers.values(): + seller.end_post_step(tick) def build(self, configs: dict): self.configs = configs @@ -84,7 +92,7 @@ def build(self, configs: dict): self.sellers[sku.id] = seller - def initialize(self): + def initialize(self, durations: int): self.data.set_id(self.id, self.id) self.data.initialize({}) @@ -99,7 +107,7 @@ def initialize(self): self._init_by_sku() - self.storage.initialize(self.configs.get("storage", {})) + self.storage.initialize(self.configs.get("storage", {}), durations) for sku_id, consumer in self.consumers.items(): consumer.initialize({ @@ -108,7 +116,7 @@ def initialize(self): "order_cost": self.configs.get("order_cost", 0), "consumer_product_id": sku_id } - }) + }, durations) for sku_id, seller in self.sellers.items(): sku = self.sku_information[sku_id] @@ -119,7 +127,7 @@ def initialize(self): "sale_gamma": sku.sale_gamma, "product_id": sku_id } - }) + }, durations) def reset(self): self._init_by_sku() diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index b2bf206df..fa6a045ba 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -30,21 +30,31 @@ def step(self, tick: int): # to make sure the distribution get correct balance sheet, we should update transports first. self.distribution.step(tick) - def post_step(self, tick: int): - self.storage.post_step(tick) - self.distribution.post_step(tick) + def begin_post_step(self, tick: int): + self.storage.begin_post_step(tick) + self.distribution.begin_post_step(tick) for vehicle in self.transports: - vehicle.post_step(tick) + vehicle.begin_post_step(tick) for supplier in self.manufactures.values(): - supplier.post_step(tick) + supplier.begin_post_step(tick) for consumer in self.consumers.values(): - consumer.post_step(tick) + consumer.begin_post_step(tick) - self.data.balance_sheet_profit = 0 - self.data.balance_sheet_loss = 0 + def end_post_step(self, tick: int): + self.storage.end_post_step(tick) + self.distribution.end_post_step(tick) + + for vehicle in self.transports: + vehicle.end_post_step(tick) + + for supplier in self.manufactures.values(): + supplier.end_post_step(tick) + + for consumer in self.consumers.values(): + consumer.end_post_step(tick) def reset(self): self._init_by_skus() @@ -137,7 +147,7 @@ def build(self, configs: dict): self.consumers[sku.id] = consumer - def initialize(self): + def initialize(self, durations: int): self.data.set_id(self.id, self.id) self.data.initialize({}) @@ -168,7 +178,7 @@ def initialize(self): "product_unit_cost": sku.cost, "product_unit_cost": sku.product_unit_cost } - }) + }, durations) for sku_id, consumer in self.consumers.items(): consumer.initialize({ @@ -177,15 +187,15 @@ def initialize(self): "order_cost": self.configs.get("order_cost", 0), "consumer_product_id": sku_id } - }) + }, durations) - self.storage.initialize(self.configs.get("storage", {})) - self.distribution.initialize(self.configs.get("distribution", {})) + self.storage.initialize(self.configs.get("storage", {}), durations) + self.distribution.initialize(self.configs.get("distribution", {}), durations) transports_conf = self.configs["transports"] for index, transport in enumerate(self.transports): - transport.initialize(transports_conf[index]) + transport.initialize(transports_conf[index], durations) def get_node_info(self) -> dict: return { diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 725831d62..f975f5c2f 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -88,16 +88,16 @@ def build(self, configs: dict): self.consumers[sku.id] = consumer - def initialize(self): + def initialize(self, durations: int): self.data.set_id(self.id, self.id) self.data.initialize({}) - + self.storage.prepare_data() self.distribution.prepare_data() for transport in self.transports: transport.prepare_data() - + for consumer in self.consumers.values(): consumer.prepare_data() @@ -105,13 +105,13 @@ def initialize(self): self._init_by_skus() # called after build, here we have the data model, we can initialize them. - self.storage.initialize(self.configs.get("storage", {})) - self.distribution.initialize(self.configs.get("distribution", {})) + self.storage.initialize(self.configs.get("storage", {}), durations) + self.distribution.initialize(self.configs.get("distribution", {}), durations) transports_conf = self.configs.get("transports", []) for index, transport in enumerate(self.transports): - transport.initialize(transports_conf[index]) + transport.initialize(transports_conf[index], durations) for sku_id, consumer in self.consumers.items(): consumer.initialize({ @@ -119,7 +119,7 @@ def initialize(self): "order_cost": self.configs.get("order_cost", 0), "consumer_product_id": sku_id } - }) + }, durations) def reset(self): # NOTE: as we are using list attribute now, theirs size will be reset to defined one after frame.reset, @@ -135,19 +135,28 @@ def reset(self): for consumer in self.consumers.values(): consumer.reset() - def post_step(self, tick: int): - self.storage.post_step(tick) + def begin_post_step(self, tick: int): + self.storage.begin_post_step(tick) - self.distribution.post_step(tick) + self.distribution.begin_post_step(tick) for vehicle in self.transports: - vehicle.post_step(tick) + vehicle.begin_post_step(tick) for consumer in self.consumers.values(): - consumer.post_step(tick) + consumer.begin_post_step(tick) + + + def end_post_step(self, tick: int): + self.storage.end_post_step(tick) - self.data.balance_sheet_profit = 0 - self.data.balance_sheet_loss = 0 + self.distribution.end_post_step(tick) + + for vehicle in self.transports: + vehicle.end_post_step(tick) + + for consumer in self.consumers.values(): + consumer.end_post_step(tick) def get_node_info(self) -> dict: return { diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py index f59de8aa9..39c1f404e 100644 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ b/maro/simulator/scenarios/supply_chain/units/base.py @@ -27,7 +27,7 @@ def prepare_data(self): if self.data is None: self.data = self.world.get_data_instance(self.data_class, self.data_index) - def initialize(self, configs: dict): + def initialize(self, configs: dict, durations: int): """Initialize current unit""" # called after frame ready self.configs = configs @@ -38,7 +38,10 @@ def step(self, tick: int): # called per tick pass - def post_step(self, tick: int): + def begin_post_step(self, tick: int): + pass + + def end_post_step(self, tick: int): pass def get_metrics(self): diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index eba55d8c6..e07a35d7b 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -17,9 +17,7 @@ def __init__(self): # and fill the field in the post_step method. self.open_orders = defaultdict(Counter) - def initialize(self, configs: dict): - #super(ConsumerUnit, self).initialize(configs) - + def initialize(self, configs: dict, durations: int): if len(self.data.sources) > 0: # we use 1st source as default one self.data.source_id = self.data.sources[0] @@ -52,14 +50,10 @@ def step(self, tick: int): data.total_purchased += quantity - def post_step(self, tick: int): - # super(ConsumerUnit, self).post_step(tick) - - data = self.data - - data.received = 0 - data.purchased = 0 - data.order_product_cost = 0 + def end_post_step(self, tick: int): + self.data.received = 0 + self.data.purchased = 0 + self.data.order_product_cost = 0 def reset(self): super(ConsumerUnit, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index d278b3fbf..f57396cfe 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -19,8 +19,8 @@ def __init__(self): # used to map from product id to slot index self.product_index_mapping: Dict[int, int] = {} - def initialize(self, configs: dict): - super(DistributionUnit, self).initialize(configs) + def initialize(self, configs: dict, durations: int): + super(DistributionUnit, self).initialize(configs, durations) # create a production index mapping, used to update product information for index, product_id in enumerate(self.data.product_list[:]): diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index b2a59975c..fdb1ff249 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -21,11 +21,11 @@ class ManufactureUnit(UnitBase): def __init__(self): super(ManufactureUnit, self).__init__() - def initialize(self, configs: dict): + def initialize(self, configs: dict, durations: int): # add the storage_id configs["data"]["storage_id"] = self.facility.storage.id - - super().initialize(configs) + + super().initialize(configs, durations) # grab bom of current production sku = self.world.get_sku_by_id(self.data.product_id) @@ -74,7 +74,7 @@ def step(self, tick: int): data.manufacturing_number += max_number_to_procedure self.facility.storage.try_take_units(source_sku_to_take) - def post_step(self, tick: int): + def end_post_step(self, tick: int): # super(ManufactureUnit, self).post_step(tick) # reset the manufacture cost per tick diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index f2985f242..2188cc841 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -1,5 +1,6 @@ import numpy as np +import random from .base import UnitBase @@ -11,14 +12,23 @@ class SellerUnit(UnitBase): def __init__(self): super(SellerUnit, self).__init__() - def initialize(self, configs: dict): - super(SellerUnit, self).initialize(configs) + self.gamma = 0 + + self.demand_distribution = [] + + def initialize(self, configs: dict, durations: int): + super(SellerUnit, self).initialize(configs, durations) + + self.gamma = self.data.sale_gamma + + for _ in range(durations): + self.demand_distribution.append(np.random.gamma(self.gamma)) def step(self, tick: int): data = self.data product_id = data.product_id - demand = self.market_demand() + demand = self.market_demand(tick) sold_qty = self.facility.storage.take_available(product_id, demand) @@ -26,7 +36,7 @@ def step(self, tick: int): data.sold = sold_qty data.demand = demand - def post_step(self, tick: int): + def end_post_step(self, tick: int): # super(SellerUnit, self).post_step(tick) self.data.sold = 0 @@ -35,5 +45,5 @@ def post_step(self, tick: int): def reset(self): super(SellerUnit, self).reset() - def market_demand(self): - return int(np.random.gamma(self.data.sale_gamma)) + def market_demand(self, tick:int): + return int(self.demand_distribution[tick]) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 0d3be9370..8dc92da19 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -11,12 +11,19 @@ def __init__(self): super().__init__() # used to map from product id to slot index + + # we use this to hold changes at python side, flash to frame before taking snapshot + self.product_number = [] self.product_index_mapping: Dict[int, int] = {} + self.capacity = 0 + + def initialize(self, configs: dict, durations: int): + super().initialize(configs, durations) - def initialize(self, configs): - super().initialize(configs) + self.capacity = self.data.capacity for index, product_id in enumerate(self.data.product_list[:]): + self.product_number.append(product_id) self.product_index_mapping[product_id] = index def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: @@ -29,7 +36,7 @@ def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) unload_quantity = min(self.data.remaining_space, quantity) product_index = self.product_index_mapping[product_id] - self.data.product_number[product_index] += unload_quantity + self.product_number[product_index] += unload_quantity unloaded_quantities[product_id] = unload_quantity return unloaded_quantities @@ -38,27 +45,32 @@ def try_take_units(self, product_quantities: Dict[int, int]): for product_id, quantity in product_quantities.items(): product_index = self.product_index_mapping[product_id] - if self.data.product_number[product_index] < quantity: + if self.product_number[product_index] < quantity: return False # TODO: refactoring for dup code for product_id, quantity in product_quantities.items(): product_index = self.product_index_mapping[product_id] - self.data.product_number[product_index] -= quantity + self.product_number[product_index] -= quantity return True def take_available(self, product_id: int, quantity: int): product_index = self.product_index_mapping[product_id] - available = self.data.product_number[product_index] + available = self.product_number[product_index] actual = min(available, quantity) - self.data.product_number[product_index] -= actual + self.product_number[product_index] -= actual return actual def get_product_number(self, product_id: int) -> int: product_index = self.product_index_mapping[product_id] - return self.data.product_number[product_index] + return self.product_number[product_index] + + def begin_post_step(self, tick: int): + # write the changes to frame + self.data.product_number[:] = self.product_number + self.data.remaining_space = self.capacity - sum(self.product_number) \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py index 9fc30cfe3..6d1a05575 100644 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ b/maro/simulator/scenarios/supply_chain/units/transport.py @@ -15,9 +15,6 @@ def __init__(self): self.path = None - def initialize(self, configs: dict): - super().initialize(configs) - def reset(self): super(TransportUnit, self).reset() @@ -136,5 +133,3 @@ def step(self, tick: int): data.location = 0 data.destination = 0 data.position[:] = -1 - - data.balance_sheet_loss = -1 * data.payload * data.unit_transport_cost diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 10c4064da..ce45da010 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -77,7 +77,7 @@ def build_unit(self, name: str): return unit - def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: int): + def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: int, durations: int): """Build current world according to configurations.""" self.configs = all_in_one_config.world self._facility_definitions = all_in_one_config.facilities @@ -165,7 +165,7 @@ def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: in facility.data = self.frame.facilities[facility_node_index] facility_node_index += 1 - facility.initialize() + facility.initialize(durations) # construct the map grid grid_config = configs["grid"] From f83467326d2da844c25af4cbeda8d6d051f5790b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 12 Mar 2021 10:05:44 +0800 Subject: [PATCH 057/482] regenerate the distribution when reset --- maro/simulator/scenarios/supply_chain/units/seller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index 2188cc841..aecf97b82 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -13,12 +13,13 @@ def __init__(self): super(SellerUnit, self).__init__() self.gamma = 0 - + self.durations = 0 self.demand_distribution = [] def initialize(self, configs: dict, durations: int): super(SellerUnit, self).initialize(configs, durations) + self.durations = durations self.gamma = self.data.sale_gamma for _ in range(durations): @@ -45,5 +46,11 @@ def end_post_step(self, tick: int): def reset(self): super(SellerUnit, self).reset() + # regenerate the demand distribution + self.demand_distribution.clear() + + for _ in range(self.durations): + self.demand_distribution.append(np.random.gamma(self.gamma)) + def market_demand(self, tick:int): return int(self.demand_distribution[tick]) From 6c274eab817cf3227d51cc027586563cec902c2a Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 12 Mar 2021 10:17:31 +0800 Subject: [PATCH 058/482] cache attribution at python to avoid too much frame accessing --- .../scenarios/supply_chain/units/storage.py | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 8dc92da19..6be5c6343 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -7,27 +7,34 @@ class StorageUnit(UnitBase): """Unit that used to store skus. """ + def __init__(self): super().__init__() # used to map from product id to slot index - # we use this to hold changes at python side, flash to frame before taking snapshot + # we use these variables to hold changes at python side, flash to frame before taking snapshot self.product_number = [] self.product_index_mapping: Dict[int, int] = {} self.capacity = 0 + self.remaining_space = 0 def initialize(self, configs: dict, durations: int): super().initialize(configs, durations) self.capacity = self.data.capacity + self.remaining_space = self.capacity for index, product_id in enumerate(self.data.product_list[:]): - self.product_number.append(product_id) + product_number = self.data.product_number[index] + + self.product_number.append(product_number) self.product_index_mapping[product_id] = index + self.remaining_space -= product_number + def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: - if all_or_nothing and self.data.remaining_space < sum(product_quantities.values()): + if all_or_nothing and self.remaining_space < sum(product_quantities.values()): return {} unloaded_quantities = {} @@ -72,5 +79,21 @@ def get_product_number(self, product_id: int) -> int: def begin_post_step(self, tick: int): # write the changes to frame - self.data.product_number[:] = self.product_number - self.data.remaining_space = self.capacity - sum(self.product_number) \ No newline at end of file + for i, number in enumerate(self.product_number): + self.data.product_number[i] = self.product_number[i] + + self.data.remaining_space = self.capacity - sum(self.product_number) + + def reset(self): + super().reset() + + self.remaining_space = self.capacity + + # TODO: dup code + for index, product_id in enumerate(self.data.product_list[:]): + product_number = self.data.product_number[index] + + self.product_number.append(product_number) + self.product_index_mapping[product_id] = index + + self.remaining_space -= product_number From 8c15c62b8ca80204363340ca87049c30e8ed7fc6 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 12 Mar 2021 10:39:21 +0800 Subject: [PATCH 059/482] more attributes caching, avoid taking snapshot at post step, as this is a special scenario that only take action at the end of each step --- .../scenarios/supply_chain/business_engine.py | 15 +++- .../scenarios/supply_chain/units/consumer.py | 87 +++++++++++++------ 2 files changed, 72 insertions(+), 30 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 62ef9549a..4d4b7bbd4 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -53,9 +53,16 @@ def step(self, tick: int): self._event_buffer.insert_event(decision_event) def _step_by_facility(self, tick: int): + # go though all the facilities for _, facility in self.world.facilities.items(): facility.step(tick) + # NOTE: different with other scenarios, we only ask for action at the end ot each step, + # so do not need to take snapshot at post step, + # we we need at post step is check the tick, and clear per tick attributes + for _, facility in self.world.facilities.items(): + facility.begin_post_step(tick) + def _step_by_units(self, tick: int): if self._unit_id_list is None: self._unit_id_list = [i for i in self.world.unit_id2index_mapping.keys()] @@ -69,12 +76,12 @@ def _step_by_units(self, tick: int): def post_step(self, tick: int): # before taking snapshot - for facility in self.world.facilities.values(): - facility.begin_post_step(tick) + # for facility in self.world.facilities.values(): + # facility.begin_post_step(tick) # take snapshot - if (tick + 1) % self._snapshot_resolution == 0: - self._frame.take_snapshot(self.frame_index(tick)) + # if (tick + 1) % self._snapshot_resolution == 0: + # self._frame.take_snapshot(self.frame_index(tick)) for facility in self.world.facilities.values(): facility.end_post_step(tick) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index e07a35d7b..febbf41d8 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -17,43 +17,73 @@ def __init__(self): # and fill the field in the post_step method. self.open_orders = defaultdict(Counter) + # attribution cache + self.source_id = 0 + self.quantity = 0 + self.product_id = 0 + self.recieved = 0 + self.purchased = 0 + self.order_cost = 0 + + def initialize(self, configs: dict, durations: int): if len(self.data.sources) > 0: # we use 1st source as default one - self.data.source_id = self.data.sources[0] + self.source_id = self.data.sources[0] + self.data.source_id = self.source_id + + self.product_id = self.data.product_id def step(self, tick: int): # NOTE: # different with original code, we split the control into pieces, # and put in to frame, so the product_id always has value # - data = self.data - quantity = data.quantity - - if quantity <= 0 or len(data.sources) == 0: + # id == 0 means invalid,as our id is 1 based + if self.quantity <= 0 or self.source_id == 0: return - source_id = data.source_id - product_id = data.product_id - - vlt = data.vlt - # NOTE: # we are using facility as source id, not the storage - self.update_open_orders(source_id, product_id, quantity) + self.update_open_orders(self.source_id, self.product_id, self.quantity) - order = Order(self.facility, product_id, quantity, vlt) + order = Order(self.facility, self.product_id, self.quantity, self.vlt) - source_facility = self.world.get_facility_by_id(source_id) + source_facility = self.world.get_facility_by_id(self.source_id) - data.order_product_cost = source_facility.distribution.place_order(order) + self.order_cost = source_facility.distribution.place_order(order) - data.total_purchased += quantity + self.purchased = self.quantity + + # clear the action, as it should only be executed once. + self.source_id = 0 + self.quantity = 0 + self.vlt = 0 + + def begin_post_step(self, tick: int): + if self.recieved > 0: + self.data.received = self.recieved + self.data.total_received += self.recieved + + if self.purchased > 0: + self.data.purchased = self.purchased + self.data.total_purchased += self.purchased + + if self.order_cost > 0: + self.data.order_product_cost = self.order_cost def end_post_step(self, tick: int): - self.data.received = 0 - self.data.purchased = 0 - self.data.order_product_cost = 0 + if self.recieved > 0: + self.data.received = 0 + self.recieved = 0 + + if self.purchased > 0: + self.data.purchased = 0 + self.purchased = 0 + + if self.order_cost > 0: + self.data.order_product_cost = 0 + self.order_cost = 0 def reset(self): super(ConsumerUnit, self).reset() @@ -62,19 +92,24 @@ def reset(self): if len(self.data.sources) > 0: # we use 1st source as default one - self.data.source_id = self.data.sources[0] + self.source_id = self.data.sources[0] + self.data.source_id = self.source_id def set_action(self, action): # called before step - data = self.data + self.source_id = action.source_id + self.quantity = action.quantity + self.vlt = action.vlt - data.source_id = action.source_id - data.quantity = action.quantity - data.vlt = action.vlt + # record the action + self.data.source_id = action.source_id + self.data.quantity = action.quantity + self.data.vlt = action.vlt def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): - self.data.total_received += quantity - self.data.received += quantity + self.recieved += quantity + # self.data.total_received += quantity + # self.data.received += quantity self.update_open_orders(source_id, product_id, -original_quantity) @@ -93,6 +128,6 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): def get_unit_info(self) -> dict: info = super(ConsumerUnit, self).get_unit_info() - info["sku_id"] = self.data.product_id + info["sku_id"] = self.product_id return info From 4c8ce1fba5a83f40975a49a2b27c55526aa740df Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 12 Mar 2021 10:49:41 +0800 Subject: [PATCH 060/482] cache more --- .../scenarios/supply_chain/units/seller.py | 40 ++++++++++++------- .../scenarios/supply_chain/units/storage.py | 2 +- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index aecf97b82..b693a767f 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -16,41 +16,53 @@ def __init__(self): self.durations = 0 self.demand_distribution = [] + # attribute cache + self.sold = 0 + self.demand = 0 + self.total_sold = 0 + self.product_id = 0 + def initialize(self, configs: dict, durations: int): super(SellerUnit, self).initialize(configs, durations) self.durations = durations self.gamma = self.data.sale_gamma + self.product_id = self.data.product_id for _ in range(durations): self.demand_distribution.append(np.random.gamma(self.gamma)) def step(self, tick: int): - data = self.data - - product_id = data.product_id demand = self.market_demand(tick) - sold_qty = self.facility.storage.take_available(product_id, demand) + # what seller does is just count down the product number. + sold_qty = self.facility.storage.take_available(self.product_id, demand) - data.total_sold += sold_qty - data.sold = sold_qty - data.demand = demand + self.total_sold += sold_qty + self.sold = sold_qty + self.demand = demand + + def begin_post_step(self, tick: int): + self.data.sold = self.sold + self.demand = self.demand + self.data.total_sold = self.total_sold def end_post_step(self, tick: int): # super(SellerUnit, self).post_step(tick) + if self.sold > 0: + self.data.sold = 0 - self.data.sold = 0 - self.data.demand = 0 + if self.demand > 0: + self.data.demand = 0 def reset(self): super(SellerUnit, self).reset() - # regenerate the demand distribution - self.demand_distribution.clear() + # TODO: regenerate the demand distribution? + # self.demand_distribution.clear() - for _ in range(self.durations): - self.demand_distribution.append(np.random.gamma(self.gamma)) + # for _ in range(self.durations): + # self.demand_distribution.append(np.random.gamma(self.gamma)) - def market_demand(self, tick:int): + def market_demand(self, tick: int): return int(self.demand_distribution[tick]) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 6be5c6343..a0ddfd0a0 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -82,7 +82,7 @@ def begin_post_step(self, tick: int): for i, number in enumerate(self.product_number): self.data.product_number[i] = self.product_number[i] - self.data.remaining_space = self.capacity - sum(self.product_number) + self.data.remaining_space = self.remaining_space def reset(self): super().reset() From b2ad48f2ca5144e4cc61f6f4628c3bc68bb524ff Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 12 Mar 2021 11:59:56 +0800 Subject: [PATCH 061/482] remove shaping --- examples/hello_world/supply_chain/hello.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 048973154..1a7f8ade6 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -108,7 +108,7 @@ def start(self): is_new_step = True - self.state_shaping() + # self.state_shaping() print(f"{bcolors.OKGREEN}Current environment tick:", self.env.tick, f"{bcolors.ENDC}") From d92eee1cce55077fb6d4ab1f233268bf3fa48ad8 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 16 Mar 2021 18:25:27 +0800 Subject: [PATCH 062/482] refactor 1st phase --- .../scenarios/supply_chain/__init__.py | 8 +- .../supply_chain/{units => }/actions.py | 5 +- .../scenarios/supply_chain/business_engine.py | 109 +- .../scenarios/supply_chain/changes.md | 29 - .../supply_chain/config_parser/__init__.py | 1 - .../supply_chain/config_parser/parser.py | 186 - .../scenarios/supply_chain/data/consumer.py | 49 - .../supply_chain/data/distribution.py | 29 - .../scenarios/supply_chain/data/storage.py | 42 - .../scenarios/supply_chain/data/transport.py | 55 - .../{data => datamodels}/__init__.py | 12 +- .../supply_chain/{data => datamodels}/base.py | 3 + .../supply_chain/datamodels/consumer.py | 52 + .../supply_chain/datamodels/distribution.py | 33 + .../{data => datamodels}/facility.py | 11 +- .../{data => datamodels}/manufacture.py | 35 +- .../{data => datamodels}/seller.py | 33 +- .../supply_chain/datamodels/storage.py | 44 + .../supply_chain/datamodels/vehicle.py | 54 + .../supply_chain/facilities/__init__.py | 9 +- .../scenarios/supply_chain/facilities/base.py | 63 - .../supply_chain/facilities/facility.py | 121 + .../supply_chain/facilities/retailer.py | 167 +- .../supply_chain/facilities/supplier.py | 229 +- .../supply_chain/facilities/warehouse.py | 190 +- .../scenarios/supply_chain/frame_builder.py | 4 + .../scenarios/supply_chain/parser.py | 220 + .../supply_chain/topologies/core.yml | 57 +- .../perf_4x1000_facilities/config.yml | 12163 ---------------- .../perf_4x400_facilities/config.yml | 4963 ------- .../topologies/sample1/config.yml | 382 +- .../supply_chain/topologies/sample1/gen.py | 31 - .../scenarios/supply_chain/units/__init__.py | 13 +- .../scenarios/supply_chain/units/base.py | 65 - .../scenarios/supply_chain/units/consumer.py | 159 +- .../supply_chain/units/distribution.py | 114 +- .../supply_chain/units/manufacture.py | 85 +- .../scenarios/supply_chain/units/order.py | 4 + .../scenarios/supply_chain/units/product.py | 112 + .../scenarios/supply_chain/units/seller.py | 61 +- .../scenarios/supply_chain/units/skuunit.py | 12 + .../scenarios/supply_chain/units/storage.py | 120 +- .../scenarios/supply_chain/units/transport.py | 135 - .../scenarios/supply_chain/units/unitbase.py | 108 + .../scenarios/supply_chain/units/vehicle.py | 228 + .../simulator/scenarios/supply_chain/world.py | 405 +- 46 files changed, 1990 insertions(+), 19020 deletions(-) rename maro/simulator/scenarios/supply_chain/{units => }/actions.py (73%) delete mode 100644 maro/simulator/scenarios/supply_chain/changes.md delete mode 100644 maro/simulator/scenarios/supply_chain/config_parser/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/config_parser/parser.py delete mode 100644 maro/simulator/scenarios/supply_chain/data/consumer.py delete mode 100644 maro/simulator/scenarios/supply_chain/data/distribution.py delete mode 100644 maro/simulator/scenarios/supply_chain/data/storage.py delete mode 100644 maro/simulator/scenarios/supply_chain/data/transport.py rename maro/simulator/scenarios/supply_chain/{data => datamodels}/__init__.py (68%) rename maro/simulator/scenarios/supply_chain/{data => datamodels}/base.py (92%) create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/consumer.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/distribution.py rename maro/simulator/scenarios/supply_chain/{data => datamodels}/facility.py (67%) rename maro/simulator/scenarios/supply_chain/{data => datamodels}/manufacture.py (51%) rename maro/simulator/scenarios/supply_chain/{data => datamodels}/seller.py (55%) create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/storage.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/vehicle.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/base.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/facility.py create mode 100644 maro/simulator/scenarios/supply_chain/parser.py delete mode 100644 maro/simulator/scenarios/supply_chain/topologies/perf_4x1000_facilities/config.yml delete mode 100644 maro/simulator/scenarios/supply_chain/topologies/perf_4x400_facilities/config.yml delete mode 100644 maro/simulator/scenarios/supply_chain/topologies/sample1/gen.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/base.py create mode 100644 maro/simulator/scenarios/supply_chain/units/product.py create mode 100644 maro/simulator/scenarios/supply_chain/units/skuunit.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/transport.py create mode 100644 maro/simulator/scenarios/supply_chain/units/unitbase.py create mode 100644 maro/simulator/scenarios/supply_chain/units/vehicle.py diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index 28bc2a1b2..4f5076aed 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -1,2 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. -from .units.actions import ConsumerAction + +from .actions import ConsumerAction, ManufactureAction +from .datamodels import * +from .facilities import * +from .units import * diff --git a/maro/simulator/scenarios/supply_chain/units/actions.py b/maro/simulator/scenarios/supply_chain/actions.py similarity index 73% rename from maro/simulator/scenarios/supply_chain/units/actions.py rename to maro/simulator/scenarios/supply_chain/actions.py index d994d8265..f084e7709 100644 --- a/maro/simulator/scenarios/supply_chain/units/actions.py +++ b/maro/simulator/scenarios/supply_chain/actions.py @@ -1,7 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. -from collections import namedtuple -# actions for units +from collections import namedtuple ConsumerAction = namedtuple("ConsumerAction", ("id", "source_id", "quantity", "vlt")) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 4d4b7bbd4..5b6999ab5 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -1,16 +1,15 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + import os -import random +from maro.event_buffer import MaroEvents from maro.simulator.scenarios import AbsBusinessEngine - -from maro.event_buffer import MaroEvents, CascadeEvent, AtomEvent - +from .parser import ConfigParser, SupplyChainConfiguration from .units import UnitBase from .world import World -from .config_parser import ConfigParser - class SupplyChainBusinessEngine(AbsBusinessEngine): def __init__(self, **kwargs): @@ -20,14 +19,9 @@ def __init__(self, **kwargs): self._build_world() - self._node_mapping = self.world.get_node_mapping() - self._frame = self.world.frame - self._action_steps = self.world.configs["action_steps"] - - # for update by unit - self._unit_id_list = None + self._node_mapping = self.world.get_node_mapping() @property def frame(self): @@ -38,63 +32,55 @@ def snapshots(self): return self._frame.snapshots @property - def configs(self): + def configs(self) -> SupplyChainConfiguration: return self.world.configs - def get_node_mapping(self) -> dict: - return self._node_mapping - def step(self, tick: int): self._step_by_facility(tick) - if tick % self._action_steps == 0: - decision_event = self._event_buffer.gen_decision_event(tick, None) + # We do not have payload here. + decision_event = self._event_buffer.gen_decision_event(tick, None) - self._event_buffer.insert_event(decision_event) - - def _step_by_facility(self, tick: int): - # go though all the facilities - for _, facility in self.world.facilities.items(): - facility.step(tick) + self._event_buffer.insert_event(decision_event) - # NOTE: different with other scenarios, we only ask for action at the end ot each step, - # so do not need to take snapshot at post step, - # we we need at post step is check the tick, and clear per tick attributes - for _, facility in self.world.facilities.items(): - facility.begin_post_step(tick) + def post_step(self, tick: int): + self._post_step_by_facility(tick) - def _step_by_units(self, tick: int): - if self._unit_id_list is None: - self._unit_id_list = [i for i in self.world.unit_id2index_mapping.keys()] + return tick + 1 == self._max_tick - random.shuffle(self._unit_id_list) + def reset(self): + self._frame.reset() - for unit_id in self._unit_id_list: - unit = self.world.get_entity(unit_id) + if self._frame.snapshots: + self._frame.snapshots.reset() - unit.step(tick) + self._reset_by_facility() - def post_step(self, tick: int): - # before taking snapshot - # for facility in self.world.facilities.values(): - # facility.begin_post_step(tick) + def get_node_mapping(self) -> dict: + return self._node_mapping - # take snapshot - # if (tick + 1) % self._snapshot_resolution == 0: - # self._frame.take_snapshot(self.frame_index(tick)) + def _step_by_facility(self, tick: int): + """Call step functions by facility. + Args: + tick (int): Current tick. + """ + # Step first. for facility in self.world.facilities.values(): - facility.end_post_step(tick) - - return tick+1 == self._max_tick + facility.step(tick) - def reset(self): - self._frame.reset() + # Then flush states to frame before generate decision event. + for facility in self.world.facilities.values(): + facility.flush_states() - if self._frame.snapshots: - self._frame.snapshots.reset() + def _post_step_by_facility(self, tick: int): + """Call post_step functions by facility.""" + for facility in self.world.facilities.values(): + facility.post_step(tick) - for _, facility in self.world.facilities.items(): + def _reset_by_facility(self): + """Call reset functions by facility.""" + for facility in self.world.facilities.values(): facility.reset() def _register_events(self): @@ -108,22 +94,19 @@ def _build_world(self): parser = ConfigParser(core_config, config_path) + conf = parser.parse() + self.world = World() - self.world.build(parser.parse(), self.calc_max_snapshots(), self._max_tick) + self.world.build(conf, self.calc_max_snapshots(), self._max_tick) def _on_action_received(self, event): action = event.payload - if action: - # NOTE: - # we assume that the action is a dictionary that - # key is the id of unit - # value is the action for specified unit, the type may different by different type - - for unit_id, control_action in action.items(): - # try to find the unit - unit: UnitBase = self.world.get_entity(unit_id) + if action is not None and len(action) > 0: + # NOTE: we assume that the action is dictionary that key is the unit(agent) id, value is the real action. + for unit_id, action_obj in action[0].items(): + entity = self.world.get_entity(unit_id) - # dispatch the action - unit.set_action(control_action) + if entity is not None and type(entity) == UnitBase: + entity.set_action(action_obj) diff --git a/maro/simulator/scenarios/supply_chain/changes.md b/maro/simulator/scenarios/supply_chain/changes.md deleted file mode 100644 index 344817b2a..000000000 --- a/maro/simulator/scenarios/supply_chain/changes.md +++ /dev/null @@ -1,29 +0,0 @@ -1. distribution: - move delay_order_penalty configuration from facility to sku - -2. manufacturing: - original: - . bom not configured in configuration file - . source material is same as output product - . hard coded "output_lot_size = 1", use control.production_rate to control the produce rate, - but the sku.production_rate in configuration file not in use. - - changed: - . bom configured in configuration file at world level. later we can support override this at each manufacture unit. - . remove output_lot_size from bom, use sku.production_rate at facility level, then action can change this - . support manufacturing without source material, like oil, just produce output production by configured rate - . add type for sku at facility level to identify if it is an input material, or output production - . remove output_lot_size, always be 1 - -3. consumer: - split upstreams from facility to a standalone part, so that we can override same world structure but - different topology. - - . there is no consumer_quantity and source by default, we use first source as source_id - but with quantity as 0, means we will wait for action to purchase source sku. - -4. facility and unit - now facility own all its related units, not just share a storage/transports unit with units, so - it is a true tree like structure. - - also there is no nested units now, they are all attached to facility now. \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/config_parser/__init__.py b/maro/simulator/scenarios/supply_chain/config_parser/__init__.py deleted file mode 100644 index 032a1f5f2..000000000 --- a/maro/simulator/scenarios/supply_chain/config_parser/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .parser import ConfigParser, SupplyChainConfiguration diff --git a/maro/simulator/scenarios/supply_chain/config_parser/parser.py b/maro/simulator/scenarios/supply_chain/config_parser/parser.py deleted file mode 100644 index 01dad04f5..000000000 --- a/maro/simulator/scenarios/supply_chain/config_parser/parser.py +++ /dev/null @@ -1,186 +0,0 @@ - -from importlib import import_module - -from yaml import safe_load - -from collections import namedtuple - -# TODO: ugly implementation, refactoring later. - -DataModelItem = namedtuple("DataModelItem", ("alias", "module_path", "class_name", "class_type", "name_in_frame")) -UnitItem = namedtuple("UnitItem", ("alias", "module_path", "class_name", "class_type", "configs")) -FacilityItem = namedtuple("FacilityItem", ("alias", "module_path", "class_name", "class_type", "configs")) - - -def find_class_type(module_path: str, class_name: str): - target_module = import_module(module_path) - - return getattr(target_module, class_name) - - -def copy_dict(dest:dict, source: dict): - for k, v in source.items(): - if type(v) != dict: - dest[k] = v - else: - if k not in dest: - dest[k] = {} - - copy_dict(dest[k], v) - - -class SupplyChainConfiguration: - data_models = None - units = None - facilities = None - world = None - - def __init__(self): - self.data_models = {} - self.units = {} - self.facilities = {} - self.world = {} - - def add_data_definition(self, alias: str, class_name: str, module_path: str, name_in_frame: str): - # check conflict - assert alias not in self.data_models - - self.data_models[alias] = DataModelItem( - alias, - module_path, - class_name, - find_class_type(module_path, class_name), - name_in_frame - ) - - def add_unit_definition(self, alias: str, class_name: str, module_path: str, configs: dict): - assert alias not in self.units - - self.units[alias] = UnitItem(alias, module_path, class_name, find_class_type(module_path, class_name), configs) - - def add_facility_definition(self, alias: str, class_name: str, module_path: str, configs: dict): - assert alias not in self.facilities - - self.facilities[alias] = FacilityItem(alias, module_path, class_name, find_class_type(module_path, class_name), configs) - - -class ConfigParser: - def __init__(self, core_file: str, config_file: str): - self._core_file = core_file - self._config_file = config_file - - self._result = SupplyChainConfiguration() - - def parse(self): - self._parse_core() - self._parse_world_config() - - return self._result - - def _parse_core(self): - with open(self._core_file, "rt") as fp: - core_config = safe_load(fp) - - self._read_core_conf(core_config) - - def _read_core_conf(self, core_config: dict): - # data models - if "data" in core_config: - for module_conf in core_config["data"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_data_definition(class_alias, class_def["class"], module_path, class_def["name_in_frame"]) - - # units - if "units" in core_config: - for module_conf in core_config["units"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_unit_definition(class_alias, class_def["class"], module_path, class_def) - - # facilities - if "facilities" in core_config: - for module_conf in core_config["facilities"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_facility_definition(class_alias, class_def["class"], module_path, class_def) - - def _parse_world_config(self): - with open(self._config_file, "rt") as fp: - world_conf = safe_load(fp) - - # read and override core part - customized_core_conf = world_conf.get("core", {}) - - self._read_core_conf(customized_core_conf) - - # read the world config first - self._result.world = world_conf["world"] - - # then try to fulfill with core configurations - for facility_conf in self._result.world["facilities"]: - facility_class_alias = facility_conf["class"] - - facility_def = self._result.facilities[facility_class_alias] - - configs = facility_conf["configs"] - - # components - for property_name, property_conf in facility_def.configs.items(): - if property_name == "class": - continue - - # if the config not exist, then copy it - if property_name not in configs: - configs[property_name] = {} - #copy_dict(configs[property_name], property_conf) - configs[property_name] = property_conf - else: - # TODO: support more than 1 depth checking - # configurations for components - if type(property_conf) == dict: - #copy_dict(configs[property_name], property_conf) - for sub_key, sub_value in property_conf.items(): - if sub_key not in configs[property_name]: - configs[property_name][sub_key] = sub_value - - # check data field of units - for unit_name, unit_conf in configs.items(): - if type(unit_conf) == dict: - if "class" not in unit_conf: - continue - - unit = self._result.units[unit_conf["class"]] - - if "data" not in unit_conf: - # copy from definition - unit_conf["data"] = unit.configs["data"] - else: - # copy missing fields - for k, v in unit.configs["data"].items(): - if k not in unit_conf["data"]: - unit_conf["data"][k] = v - elif type(unit_conf) == list: - # list is a placeholder, we just need copy the class alias for data - for unit_item in unit_conf: - unit = self._result.units[unit_item["class"]] - - if "data" not in unit_item: - unit_item["data"]["class"] = unit.configs["data"]["class"] - else: - unit_item["data"]["class"] = unit.configs["data"]["class"] - - -if __name__ == "__main__": - parser = ConfigParser("maro/simulator/scenarios/supply_chain/topologies/core.yml", "maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml") - - result = parser.parse() - - import pprint - - pp = pprint.PrettyPrinter(indent=2, depth=8) - - pp.pprint(result.world) diff --git a/maro/simulator/scenarios/supply_chain/data/consumer.py b/maro/simulator/scenarios/supply_chain/data/consumer.py deleted file mode 100644 index 3b1e6a05e..000000000 --- a/maro/simulator/scenarios/supply_chain/data/consumer.py +++ /dev/null @@ -1,49 +0,0 @@ -from .base import DataModelBase - -from maro.backends.frame import node, NodeBase, NodeAttribute -from maro.backends.backend import AttributeType - - -# NOTE: one sku one consumer -@node("consumer") -class ConsumerDataModel(DataModelBase): - # reward states - # from config - order_cost = NodeAttribute(AttributeType.Int) - total_purchased = NodeAttribute(AttributeType.Int) - total_received = NodeAttribute(AttributeType.Int) - - # action states - product_id = NodeAttribute(AttributeType.Int) - source_id = NodeAttribute(AttributeType.Int) - quantity = NodeAttribute(AttributeType.Int) - vlt = NodeAttribute(AttributeType.Int) - - # id of upstream facilities. - sources = NodeAttribute(AttributeType.Int, 1, is_list=True) - - # per tick states - - # snapshots["consumer"][hist_len::"purchased"] equals to original latest_consumptions - purchased = NodeAttribute(AttributeType.Int) - received = NodeAttribute(AttributeType.Int) - order_product_cost = NodeAttribute(AttributeType.Int) - - def __init__(self): - super(ConsumerDataModel, self).__init__() - - self._order_cost = 0 - self._consumer_product_id = 0 - - def initialize(self, configs: dict): - if configs is not None: - self._order_cost = configs["order_cost"] - self._consumer_product_id = configs["consumer_product_id"] - - self.reset() - - def reset(self): - super(ConsumerDataModel, self).reset() - - self.order_cost = self._order_cost - self.product_id = self._consumer_product_id diff --git a/maro/simulator/scenarios/supply_chain/data/distribution.py b/maro/simulator/scenarios/supply_chain/data/distribution.py deleted file mode 100644 index d9f3c2e51..000000000 --- a/maro/simulator/scenarios/supply_chain/data/distribution.py +++ /dev/null @@ -1,29 +0,0 @@ -from .base import DataModelBase - -from maro.backends.frame import node, NodeBase, NodeAttribute -from maro.backends.backend import AttributeType - - -@node("distribution") -class DistributionDataModel(DataModelBase): - unit_price = NodeAttribute(AttributeType.Int) - - # origin is stock_levels, used to save product and its number, we have to split it. - product_list = NodeAttribute(AttributeType.Int, 1, is_list=True) - check_in_price = NodeAttribute(AttributeType.Int, 1, is_list=True) - delay_order_penalty = NodeAttribute(AttributeType.Int, 1, is_list=True) - - def __init__(self): - super(DistributionDataModel, self).__init__() - # TODO: not sure about this. - self._unit_price = 0 - - def initialize(self, configs: dict): - if configs is not None: - self._unit_price = configs.get("unit_price", 0) - - self.reset() - - def reset(self): - super(DistributionDataModel, self).reset() - self.unit_price = self._unit_price diff --git a/maro/simulator/scenarios/supply_chain/data/storage.py b/maro/simulator/scenarios/supply_chain/data/storage.py deleted file mode 100644 index 5b3b3c034..000000000 --- a/maro/simulator/scenarios/supply_chain/data/storage.py +++ /dev/null @@ -1,42 +0,0 @@ -from .base import DataModelBase - -from maro.backends.frame import node, NodeBase, NodeAttribute -from maro.backends.backend import AttributeType - - -@node("storage") -class StorageDataModel(DataModelBase): - unit_storage_cost = NodeAttribute(AttributeType.Int) - remaining_space = NodeAttribute(AttributeType.Int) - capacity = NodeAttribute(AttributeType.Int) - - # original is stock_levels, used to save product and its number - product_list = NodeAttribute(AttributeType.Int, 1, is_list=True) - product_number = NodeAttribute(AttributeType.Int, 1, is_list=True) - - def __init__(self): - super(StorageDataModel, self).__init__() - - self._unit_storage_cost = 0 - self._capacity = 0 - - def initialize(self, configs): - if configs is not None: - self._unit_storage_cost = configs.get("unit_storage_cost", 0) - self._capacity = configs.get("capacity", 0) - - self.reset() - - def reset(self): - super(StorageDataModel, self).reset() - - self.unit_storage_cost = self._unit_storage_cost - self.capacity = self._capacity - - self.remaining_space = self._capacity - - # def _on_product_number_changed(self, value): - # if len(self.product_number) > 0: - # taken_number = sum(self.product_number[:]) - - # self.remaining_space = self.capacity - taken_number diff --git a/maro/simulator/scenarios/supply_chain/data/transport.py b/maro/simulator/scenarios/supply_chain/data/transport.py deleted file mode 100644 index 809d36278..000000000 --- a/maro/simulator/scenarios/supply_chain/data/transport.py +++ /dev/null @@ -1,55 +0,0 @@ -from .base import DataModelBase -from maro.backends.frame import node, NodeBase, NodeAttribute -from maro.backends.backend import AttributeType - - -@node("transport") -class TransportDataModel(DataModelBase): - # Id of current entity - source = NodeAttribute(AttributeType.Int) - - # Id of target entity. - destination = NodeAttribute(AttributeType.Int) - - # Number of product. - payload = NodeAttribute(AttributeType.Int) - - # Index of product. - product_id = NodeAttribute(AttributeType.Int) - - requested_quantity = NodeAttribute(AttributeType.Int) - - # Patient to wait for products ready. - patient = NodeAttribute(AttributeType.Int) - - # Steps to destination. - steps = NodeAttribute(AttributeType.Int) - - # Current location on the way, equal to step means arrive at destination. - location = NodeAttribute(AttributeType.Int) - - # for debug - position = NodeAttribute(AttributeType.Int, 2) - - # from config - unit_transport_cost = NodeAttribute(AttributeType.Int) - - def __init__(self): - super(TransportDataModel, self).__init__() - - self._patient = 0 - self._unit_transport_cost = 0 - - def initialize(self, configs: dict): - if configs is not None: - self._patient = configs.get("patient", 100) - self._unit_transport_cost = configs.get("unit_transport_cost", 1) - - self.reset() - - def reset(self): - super(TransportDataModel, self).reset() - - self.unit_transport_cost = self._unit_transport_cost - self.patient = self._patient - self.position[:] = -1 diff --git a/maro/simulator/scenarios/supply_chain/data/__init__.py b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py similarity index 68% rename from maro/simulator/scenarios/supply_chain/data/__init__.py rename to maro/simulator/scenarios/supply_chain/datamodels/__init__.py index 64971dc37..6373ae47e 100644 --- a/maro/simulator/scenarios/supply_chain/data/__init__.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py @@ -1,7 +1,11 @@ -from .storage import StorageDataModel -from .transport import TransportDataModel +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .consumer import ConsumerDataModel from .distribution import DistributionDataModel +from .facility import FacilityDataModel from .manufacture import ManufactureDataModel -from .consumer import ConsumerDataModel from .seller import SellerDataModel -from .facility import FacilityDataModel +from .storage import StorageDataModel +from .vehicle import VehicleDataModel diff --git a/maro/simulator/scenarios/supply_chain/data/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py similarity index 92% rename from maro/simulator/scenarios/supply_chain/data/base.py rename to maro/simulator/scenarios/supply_chain/datamodels/base.py index 5d97d69ac..3d1e883bd 100644 --- a/maro/simulator/scenarios/supply_chain/data/base.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/base.py @@ -1,6 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. from abc import abstractmethod + from maro.backends.backend import AttributeType from maro.backends.frame import NodeBase, NodeAttribute diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py new file mode 100644 index 000000000..0125163e8 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import node, NodeAttribute + +from .base import DataModelBase + + +# NOTE: one sku one consumer +@node("consumer") +class ConsumerDataModel(DataModelBase): + # reward states + # from config + order_cost = NodeAttribute(AttributeType.UInt) + total_purchased = NodeAttribute(AttributeType.UInt) + total_received = NodeAttribute(AttributeType.UInt) + + # action states + product_id = NodeAttribute(AttributeType.UInt) + source_id = NodeAttribute(AttributeType.UInt) + quantity = NodeAttribute(AttributeType.UInt) + vlt = NodeAttribute(AttributeType.UShort) + + # id of upstream facilities. + sources = NodeAttribute(AttributeType.UInt, 1, is_list=True) + + # per tick states + + # snapshots["consumer"][hist_len::"purchased"] equals to original latest_consumptions + purchased = NodeAttribute(AttributeType.UInt) + received = NodeAttribute(AttributeType.UInt) + order_product_cost = NodeAttribute(AttributeType.UInt) + + def __init__(self): + super(ConsumerDataModel, self).__init__() + + self._order_cost = 0 + self._product_id = 0 + + def initialize(self, order_cost=0, product_id=0): + self._order_cost = order_cost + self._product_id = product_id + + self.reset() + + def reset(self): + super(ConsumerDataModel, self).reset() + + self.order_cost = self._order_cost + self.product_id = self._product_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py new file mode 100644 index 000000000..606327789 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import node, NodeAttribute + +from .base import DataModelBase + + +@node("distribution") +class DistributionDataModel(DataModelBase): + unit_price = NodeAttribute(AttributeType.Int) + + product_list = NodeAttribute(AttributeType.UInt, 1, is_list=True) + check_in_price = NodeAttribute(AttributeType.UInt, 1, is_list=True) + delay_order_penalty = NodeAttribute(AttributeType.UInt, 1, is_list=True) + + def __init__(self): + super(DistributionDataModel, self).__init__() + + # TODO: not sure about this. + self._unit_price = 0 + + def initialize(self, unit_price: int = 0): + self._unit_price = unit_price + + self.reset() + + def reset(self): + super(DistributionDataModel, self).reset() + + self.unit_price = self._unit_price diff --git a/maro/simulator/scenarios/supply_chain/data/facility.py b/maro/simulator/scenarios/supply_chain/datamodels/facility.py similarity index 67% rename from maro/simulator/scenarios/supply_chain/data/facility.py rename to maro/simulator/scenarios/supply_chain/datamodels/facility.py index 9126585a8..b78af24dd 100644 --- a/maro/simulator/scenarios/supply_chain/data/facility.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/facility.py @@ -1,13 +1,16 @@ -from .base import DataModelBase +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + -from maro.backends.frame import node, NodeBase, NodeAttribute from maro.backends.backend import AttributeType +from maro.backends.frame import node, NodeAttribute + +from .base import DataModelBase @node("facility") class FacilityDataModel(DataModelBase): - - test = NodeAttribute(AttributeType.Int) + test = NodeAttribute(AttributeType.UInt) def __init__(self): super(FacilityDataModel, self).__init__() diff --git a/maro/simulator/scenarios/supply_chain/data/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py similarity index 51% rename from maro/simulator/scenarios/supply_chain/data/manufacture.py rename to maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index 15407c4d0..d206c492d 100644 --- a/maro/simulator/scenarios/supply_chain/data/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -1,46 +1,47 @@ -from .base import DataModelBase +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + -from maro.backends.frame import node, NodeBase, NodeAttribute from maro.backends.backend import AttributeType +from maro.backends.frame import node, NodeAttribute + +from .base import DataModelBase @node("manufacture") class ManufactureDataModel(DataModelBase): # storage related to this manufacture unit, for easy state retrieving. - storage_id = NodeAttribute(AttributeType.Int) + storage_id = NodeAttribute(AttributeType.UInt) # cost to produce one output production - product_unit_cost = NodeAttribute(AttributeType.Int) + product_unit_cost = NodeAttribute(AttributeType.UInt) # number per tick, different with original manufacturing cost, we just provide number, and cost # user can determine how to calculate the cost - manufacturing_number = NodeAttribute(AttributeType.Int) + manufacturing_number = NodeAttribute(AttributeType.UInt) # what we will produce - product_id = NodeAttribute(AttributeType.Int) + product_id = NodeAttribute(AttributeType.UInt) # original from config, then updated by action - production_rate = NodeAttribute(AttributeType.Int) + production_rate = NodeAttribute(AttributeType.UInt) def __init__(self): super(ManufactureDataModel, self).__init__() self._output_product_id = 0 - self._production_rate = 0 - self._storage_id = 0 self._product_unit_cost = 0 + self._storage_id = 0 - def initialize(self, configs: dict): - if configs is not None: - self._output_product_id = configs["output_product_id"] - self._production_rate = configs.get("production_rate", 1) - self._storage_id = configs["storage_id"] - self.product_unit_cost = configs.get("product_unit_cost", 1) + def initialize(self, output_product_id: int = 0, product_unit_cost: int = 1, storage_id: int = 0): + self._output_product_id = output_product_id + self._product_unit_cost = product_unit_cost + self._storage_id = storage_id - self.reset() + self.reset() def reset(self): super(ManufactureDataModel, self).reset() self.product_id = self._output_product_id - self.production_rate = self._production_rate + self.product_unit_cost = self._product_unit_cost self.storage_id = self._storage_id diff --git a/maro/simulator/scenarios/supply_chain/data/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py similarity index 55% rename from maro/simulator/scenarios/supply_chain/data/seller.py rename to maro/simulator/scenarios/supply_chain/datamodels/seller.py index 4178d6c38..4bdfc0028 100644 --- a/maro/simulator/scenarios/supply_chain/data/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -1,31 +1,35 @@ -from .base import DataModelBase +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + -from maro.backends.frame import node, NodeBase, NodeAttribute from maro.backends.backend import AttributeType +from maro.backends.frame import node, NodeAttribute + +from .base import DataModelBase # NOTE: one sku one seller @node("seller") class SellerDataModel(DataModelBase): # reward states - total_sold = NodeAttribute(AttributeType.Int) + total_sold = NodeAttribute(AttributeType.UInt) # from config backlog_ratio = NodeAttribute(AttributeType.Float) # action states - unit_price = NodeAttribute(AttributeType.Int) + unit_price = NodeAttribute(AttributeType.UInt) # - sale_gamma = NodeAttribute(AttributeType.Int) + sale_gamma = NodeAttribute(AttributeType.UInt) # what we will sell - product_id = NodeAttribute(AttributeType.Int) + product_id = NodeAttribute(AttributeType.UInt) # per tick state, we can use this to support "sale hist" feature in original code. # original there is only sold state, we add a demand here - demand = NodeAttribute(AttributeType.Int) - sold = NodeAttribute(AttributeType.Int) + demand = NodeAttribute(AttributeType.UInt) + sold = NodeAttribute(AttributeType.UInt) def __init__(self): super(SellerDataModel, self).__init__() @@ -35,14 +39,13 @@ def __init__(self): self._product_id = 0 self._backlog_ratio = 0 - def initialize(self, configs: dict): - if configs is not None: - self._unit_price = configs.get("unit_price", 0) - self._sale_gamma = configs.get("sale_gamma", 0) - self._product_id = configs.get("product_id", 0) - self._backlog_ratio = configs.get("backlog_ratio", 0.1) + def initialize(self, unit_price: int = 0, sale_gamma: int = 0, product_id: int = 0, backlog_ratio: int = 0): + self._unit_price = unit_price + self._sale_gamma = sale_gamma + self._product_id = product_id + self._backlog_ratio = backlog_ratio - self.reset() + self.reset() def reset(self): super(SellerDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py new file mode 100644 index 000000000..7daf0cc07 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/storage.py @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import node, NodeAttribute + +from .base import DataModelBase + + +@node("storage") +class StorageDataModel(DataModelBase): + unit_storage_cost = NodeAttribute(AttributeType.UInt) + remaining_space = NodeAttribute(AttributeType.UInt) + capacity = NodeAttribute(AttributeType.UInt) + + # original is stock_levels, used to save product and its number + product_list = NodeAttribute(AttributeType.UInt, 1, is_list=True) + product_number = NodeAttribute(AttributeType.UInt, 1, is_list=True) + + def __init__(self): + super(StorageDataModel, self).__init__() + + self._unit_storage_cost = 0 + self._capacity = 0 + self._remaining_space = None + + def initialize(self, unit_storage_cost: int = 0, capacity: int = 0, remaining_space: int = None): + self._unit_storage_cost = unit_storage_cost + self._capacity = capacity + self._remaining_space = remaining_space + + self.reset() + + def reset(self): + super(StorageDataModel, self).reset() + + self.unit_storage_cost = self._unit_storage_cost + self.capacity = self._capacity + + if self._remaining_space is not None: + self.remaining_space = self._remaining_space + else: + self.remaining_space = self._capacity diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py new file mode 100644 index 000000000..e8617ba2a --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import node, NodeAttribute + +from .base import DataModelBase + + +@node("vehicle") +class VehicleDataModel(DataModelBase): + # Id of current entity + source = NodeAttribute(AttributeType.UInt) + + # Id of target entity. + destination = NodeAttribute(AttributeType.UInt) + + # Number of product. + payload = NodeAttribute(AttributeType.UInt) + + # Index of product. + product_id = NodeAttribute(AttributeType.UInt) + + requested_quantity = NodeAttribute(AttributeType.UInt) + + # Patient to wait for products ready. + patient = NodeAttribute(AttributeType.UInt) + + # Steps to destination. + steps = NodeAttribute(AttributeType.UInt) + + # from config + unit_transport_cost = NodeAttribute(AttributeType.UInt) + + position = NodeAttribute(AttributeType.Int, 2) + + def __init__(self): + super(VehicleDataModel, self).__init__() + + self._patient = 0 + self._unit_transport_cost = 0 + + def initialize(self, patient: int = 100, unit_transport_cost: int = 1): + self._patient = patient + self._unit_transport_cost = unit_transport_cost + + self.reset() + + def reset(self): + super(VehicleDataModel, self).reset() + + self.unit_transport_cost = self._unit_transport_cost + self.patient = self._patient diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py index 26ceb1e47..71ba82a1b 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/__init__.py +++ b/maro/simulator/scenarios/supply_chain/facilities/__init__.py @@ -1,3 +1,8 @@ -from .warehouse import WarehouseFacility -from .supplier import SupplierFacility +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .facility import FacilityBase from .retailer import RetailerFacility +from .supplier import SupplierFacility +from .warehouse import WarehouseFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/base.py b/maro/simulator/scenarios/supply_chain/facilities/base.py deleted file mode 100644 index 8c6ee715d..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/base.py +++ /dev/null @@ -1,63 +0,0 @@ - -from abc import ABC, abstractmethod - - -class FacilityBase(ABC): - # data model of this facility - data = None - - # current world - world = None - - # position in the world - x: int = None - y: int = None - - # configuration of this facility - configs: dict = None - - # id of this facility - id: int = None - - # name of this facility - name: str = None - - # sku information, same as original sku_in_stock - # different facility may contains different data - # TODO: provide sku by methods, not expose sku property - sku_information: dict = None - - # dictionary of upstreams - # key is the source sku id - # value is the list of facility id - upstreams: dict = None - - @abstractmethod - def step(self, tick: int): - # called per tick - pass - - def begin_post_step(self, tick: int): - pass - - def end_post_step(self, tick: int): - pass - - @abstractmethod - def build(self, configs: dict): - # called to build components, but without data model instance. - pass - - @abstractmethod - def initialize(self, durations: int): - # called after data model instance is ready. - pass - - @abstractmethod - def reset(self): - # called per episode. - pass - - @abstractmethod - def get_node_info(self) -> dict: - pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py new file mode 100644 index 000000000..ed0b5e562 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -0,0 +1,121 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from abc import ABC + + +class FacilityBase(ABC): + """Base of all facilities.""" + + # Id of this facility. + id: int = None + + # Name of this facility. + name: str = None + + # World of this facility belongs to. + world = None + + # Skus in this facility. + skus: dict = None + + # Product units for each sku in this facility. + # Key is sku(product) id, value is the instance of product unit. + products: dict = None + + # Storage unit in this facility. + storage = None + + # Distribution unit in this facility. + distribution = None + + # Upstream facilities. + # Key is sku id, value is the list of product unit from upstream. + upstreams: dict = None + + def parse_skus(self, configs: dict): + """Parse sku information from config. + + Args: + configs (dict): Configuration of skus belongs to this facility. + """ + pass + + def parse_configs(self, configs: dict): + """Parse configuration of this facility. + + Args: + configs (dict): Configuration of this facility. + """ + pass + + def initialize(self): + """Initialize this facility after frame is ready.""" + pass + + def step(self, tick: int): + """Push facility to next step. + + Args: + tick (int): Current simulator tick. + """ + if self.storage is not None: + self.storage.step(tick) + + if self.distribution is not None: + self.distribution.step(tick) + + if self.products is not None: + for product in self.products.values(): + product.step(tick) + + def flush_states(self): + """Flush states into frame.""" + if self.storage is not None: + self.storage.flush_states() + + if self.distribution is not None: + self.distribution.flush_states() + + if self.products is not None: + for product in self.products.values(): + product.flush_states() + + def post_step(self, tick: int): + """Post processing at the end of step.""" + if self.storage is not None: + self.storage.post_step(tick) + + if self.distribution is not None: + self.distribution.post_step(tick) + + if self.products is not None: + for product in self.products.values(): + product.post_step(tick) + + def reset(self): + """Reset facility for new episode.""" + if self.storage is not None: + self.storage.reset() + + if self.products is not None: + for product in self.products.values(): + product.reset() + + def get_node_info(self) -> dict: + products_info = [] + + for product_id, product in self.products.items(): + products_info.append(product.get_unit_info()) + + return { + "id": self.id, + "name": self.name, + "class": type(self), + "units": { + "storage": self.storage.get_unit_info() if self.storage is not None else None, + "distribution": self.distribution.get_unit_info() if self.distribution is not None else None, + "products": products_info + } + } diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 7050a2889..96209505a 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -1,66 +1,31 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from collections import namedtuple +from typing import List -from .base import FacilityBase +from maro.simulator.scenarios.supply_chain.units import ProductUnit, StorageUnit +from .facility import FacilityBase class RetailerFacility(FacilityBase): - SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gamma")) - - storage = None - consumers: dict = None - sellers: dict = None - - def step(self, tick: int): - self.storage.step(tick) - - if self.consumers is not None: - for consumer in self.consumers.values(): - consumer.step(tick) - - if self.sellers is not None: - for seller in self.sellers.values(): - seller.step(tick) - - def begin_post_step(self, tick: int): - self.storage.begin_post_step(tick) - - if self.consumers is not None: - for consumer in self.consumers.values(): - consumer.begin_post_step(tick) - - if self.sellers is not None: - for seller in self.sellers.values(): - seller.begin_post_step(tick) + """Retail facility used to generate order from upstream, and sell products by demand.""" - def end_post_step(self, tick: int): - self.storage.end_post_step(tick) - - if self.consumers is not None: - for consumer in self.consumers.values(): - consumer.end_post_step(tick) - - if self.sellers is not None: - for seller in self.sellers.values(): - seller.end_post_step(tick) - - def build(self, configs: dict): - self.configs = configs + SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gamma")) - # construct storage - self.storage = self.world.build_unit(configs["storage"]["class"]) - self.storage.data_class = configs["storage"]["data"]["class"] + # Product unit list of this facility. + products: List[ProductUnit] - self.storage.world = self.world - self.storage.facility = self - self.storage.data_index = self.world.register_data_class(self.storage.id, self.storage.data_class) + # Storage unit of this facility. + storage: StorageUnit - self.sku_information = {} - self.consumers = {} - self.sellers = {} + def __init__(self): + self.skus = {} - for sku_name, sku_config in configs["skus"].items(): - sku = self.world.get_sku(sku_name) + def parse_skus(self, configs: dict): + for sku_name, sku_config in configs.items(): + sku = self.world.get_sku_by_name(sku_name) sku_info = RetailerFacility.SkuInfo( sku_name, sku.id, @@ -70,100 +35,4 @@ def build(self, configs: dict): sku_config["sale_gamma"] ) - self.sku_information[sku.id] = sku_info - - # all sku in retail are final production for sale, no material - consumer = self.world.build_unit(configs["consumers"]["class"]) - consumer.data_class = configs["consumers"]["data"]["class"] - - consumer.world = self.world - consumer.facility = self - consumer.data_index = self.world.register_data_class(consumer.id, consumer.data_class) - - self.consumers[sku.id] = consumer - - # seller for this sku - seller = self.world.build_unit(configs["sellers"]["class"]) - seller.data_class = configs["sellers"]["data"]["class"] - - seller.world = self.world - seller.facility = self - seller.data_index = self.world.register_data_class(seller.id, seller.data_class) - - self.sellers[sku.id] = seller - - def initialize(self, durations: int): - self.data.set_id(self.id, self.id) - self.data.initialize({}) - - self.storage.prepare_data() - - # prepare data for units - for consumer in self.consumers.values(): - consumer.prepare_data() - - for seller in self.sellers.values(): - seller.prepare_data() - - self._init_by_sku() - - self.storage.initialize(self.configs.get("storage", {}), durations) - - for sku_id, consumer in self.consumers.items(): - consumer.initialize({ - "data": { - # TODO: move to config - "order_cost": self.configs.get("order_cost", 0), - "consumer_product_id": sku_id - } - }, durations) - - for sku_id, seller in self.sellers.items(): - sku = self.sku_information[sku_id] - - seller.initialize({ - "data": { - "unit_price": sku.price, - "sale_gamma": sku.sale_gamma, - "product_id": sku_id - } - }, durations) - - def reset(self): - self._init_by_sku() - - self.storage.reset() - - if self.consumers is not None: - for consumer in self.consumers.values(): - consumer.reset() - - if self.sellers is not None: - for seller in self.sellers.values(): - seller.reset() - - def get_node_info(self) -> dict: - return { - "id": self.id, - "name": self.name, - "class": type(self), - "units": { - "storage": self.storage.get_unit_info(), - "consumers": [consumer.get_unit_info() for consumer in self.consumers.values()], - "sellers": [seller.get_unit_info() for seller in self.sellers.values()] - } - } - - def _init_by_sku(self): - for _, sku in self.sku_information.items(): - # update storage's production info - self.storage.data.product_list.append(sku.id) - self.storage.data.product_number.append(sku.init_in_stock) - - if self.upstreams is not None: - # update the source facilities for each consumer - for sku_id, source_facilities in self.upstreams.items(): - consumer = self.consumers[sku_id] - - for facility_id in source_facilities: - consumer.data.sources.append(facility_id) + self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index fa6a045ba..646503a6f 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -1,122 +1,41 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from collections import namedtuple -from .base import FacilityBase +from typing import List + +from maro.simulator.scenarios.supply_chain.units import StorageUnit, DistributionUnit, ProductUnit +from .facility import FacilityBase class SupplierFacility(FacilityBase): + """Supplier facilities used to produce products with material products.""" + SkuInfo = namedtuple( "SkuInfo", - ("name", "id", "init_in_stock", "production_rate", "type", "cost", "price", "delay_order_penalty", "product_unit_cost") + ("name", "id", "init_in_stock", "type", "cost", "price", "delay_order_penalty", "product_unit_cost") ) - storage = None - distribution = None - transports = None - manufactures = None - consumers = None - - def step(self, tick: int): - self.storage.step(tick) - - for vehicle in self.transports: - vehicle.step(tick) - - for supplier in self.manufactures.values(): - supplier.step(tick) - - for consumer in self.consumers.values(): - consumer.step(tick) - - # to make sure the distribution get correct balance sheet, we should update transports first. - self.distribution.step(tick) - - def begin_post_step(self, tick: int): - self.storage.begin_post_step(tick) - self.distribution.begin_post_step(tick) - - for vehicle in self.transports: - vehicle.begin_post_step(tick) - - for supplier in self.manufactures.values(): - supplier.begin_post_step(tick) - - for consumer in self.consumers.values(): - consumer.begin_post_step(tick) - - def end_post_step(self, tick: int): - self.storage.end_post_step(tick) - self.distribution.end_post_step(tick) - - for vehicle in self.transports: - vehicle.end_post_step(tick) - - for supplier in self.manufactures.values(): - supplier.end_post_step(tick) - - for consumer in self.consumers.values(): - consumer.end_post_step(tick) - - def reset(self): - self._init_by_skus() - - self.storage.reset() - self.distribution.reset() - - for vehicle in self.transports: - vehicle.reset() - - for supplier in self.manufactures.values(): - supplier.reset() - - for consumer in self.consumers.values(): - consumer.reset() - - def build(self, configs: dict): - self.configs = configs - - # TODO: dup code in all facilities, refactoring later - - # construct storage - self.storage = self.world.build_unit(configs["storage"]["class"]) - self.storage.data_class = configs["storage"]["data"]["class"] - - self.storage.world = self.world - self.storage.facility = self - self.storage.data_index = self.world.register_data_class(self.storage.id, self.storage.data_class) - - # construct transport - self.transports = [] + # Storage unit of this facility. + storage: StorageUnit - for transport_conf in configs["transports"]: - transport = self.world.build_unit(transport_conf["class"]) - transport.data_class = transport_conf["data"]["class"] + # Distribution unit of this facility. + distribution: DistributionUnit - transport.world = self.world - transport.facility = self - transport.data_index = self.world.register_data_class(transport.id, transport.data_class) + # Product unit list of this facility. + products: List[ProductUnit] - self.transports.append(transport) + def __init__(self): + self.skus = {} - # construct distribution - self.distribution = self.world.build_unit(configs["distribution"]["class"]) - self.distribution.data_class = configs["distribution"]["data"]["class"] - - self.distribution.world = self.world - self.distribution.facility = self - self.distribution.data_index = self.world.register_data_class(self.distribution.id, self.distribution.data_class) - - # sku information - self.sku_information = {} - self.manufactures = {} - self.consumers = {} - - for sku_name, sku_config in configs["skus"].items(): - sku = self.world.get_sku(sku_name) + def parse_skus(self, configs: dict): + for sku_name, sku_config in configs.items(): + sku = self.world.get_sku_by_name(sku_name) sku_info = SupplierFacility.SkuInfo( sku_name, sku.id, sku_config["init_in_stock"], - sku_config.get("production_rate", 0), sku_config["type"], sku_config.get("cost", 0), sku_config.get("price", 0), @@ -124,108 +43,4 @@ def build(self, configs: dict): sku_config.get("product_unit_cost", 1) ) - self.sku_information[sku.id] = sku_info - - # TODO: make it an enum later. - if sku_info.type == "production": - # one supplier per sku - supplier = self.world.build_unit(configs["manufactures"]["class"]) - supplier.data_class = configs["manufactures"]["data"]["class"] - - supplier.world = self.world - supplier.facility = self - supplier.data_index = self.world.register_data_class(supplier.id, supplier.data_class) - - self.manufactures[sku.id] = supplier - else: - consumer = self.world.build_unit(configs["consumers"]["class"]) - consumer.data_class = configs["consumers"]["data"]["class"] - - consumer.world = self.world - consumer.facility = self - consumer.data_index = self.world.register_data_class(consumer.id, consumer.data_class) - - self.consumers[sku.id] = consumer - - def initialize(self, durations: int): - self.data.set_id(self.id, self.id) - self.data.initialize({}) - - self.storage.prepare_data() - self.distribution.prepare_data() - - for transport in self.transports: - transport.prepare_data() - - for manufacture in self.manufactures.values(): - manufacture.prepare_data() - - for consumer in self.consumers.values(): - consumer.prepare_data() - - # DO init by skus first, as other components may depend on sku information - self._init_by_skus() - - for _, sku in self.sku_information.items(): - if sku.id in self.manufactures: - supplier = self.manufactures[sku.id] - - # build parameters to initialize the data model - supplier.initialize({ - "data": { - "production_rate": sku.production_rate, - "output_product_id": sku.id, - "product_unit_cost": sku.cost, - "product_unit_cost": sku.product_unit_cost - } - }, durations) - - for sku_id, consumer in self.consumers.items(): - consumer.initialize({ - "data": { - # TODO: move to config - "order_cost": self.configs.get("order_cost", 0), - "consumer_product_id": sku_id - } - }, durations) - - self.storage.initialize(self.configs.get("storage", {}), durations) - self.distribution.initialize(self.configs.get("distribution", {}), durations) - - transports_conf = self.configs["transports"] - - for index, transport in enumerate(self.transports): - transport.initialize(transports_conf[index], durations) - - def get_node_info(self) -> dict: - return { - "id": self.id, - "name": self.name, - "class": type(self), - "units": { - "storage": self.storage.get_unit_info(), - "consumers": [consumer.get_unit_info() for consumer in self.consumers.values()], - "transports": [vehicle.get_unit_info() for vehicle in self.transports], - "distribution": self.distribution.get_unit_info(), - "suppliers": [supplier.get_unit_info() for supplier in self.manufactures.values()] - } - } - - def _init_by_skus(self): - for _, sku in self.sku_information.items(): - # update storage's production info - self.storage.data.product_list.append(sku.id) - self.storage.data.product_number.append(sku.init_in_stock) - - # update distribution's production info - self.distribution.data.product_list.append(sku.id) - self.distribution.data.check_in_price.append(0) - self.distribution.data.delay_order_penalty.append(0) - - if self.upstreams is not None: - # update the source facilities for each consumer - for sku_id, source_facilities in self.upstreams.items(): - consumer = self.consumers[sku_id] - - for facility_id in source_facilities: - consumer.data.sources.append(facility_id) + self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index f975f5c2f..189ff9c08 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -1,74 +1,35 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from collections import namedtuple from typing import List -from .base import FacilityBase - - -class WarehouseFacility(FacilityBase): - SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id", "price", "delay_order_penalty")) - - # storage unit - storage = None - - # distribution unit - distribution = None - - # vehicle list - transports = None - - # consumers that will generate order to purchase productions - # one sku one consumer - consumers = None - - def step(self, tick: int): - self.storage.step(tick) - - for transport in self.transports: - transport.step(tick) - - for consumer in self.consumers.values(): - consumer.step(tick) - self.distribution.step(tick) +from maro.simulator.scenarios.supply_chain.units import StorageUnit, DistributionUnit, ProductUnit +from .facility import FacilityBase - def build(self, configs: dict): - self.configs = configs - # construct storage - self.storage = self.world.build_unit(configs["storage"]["class"]) - self.storage.data_class = configs["storage"]["data"]["class"] - - self.storage.world = self.world - self.storage.facility = self - self.storage.data_index = self.world.register_data_class(self.storage.id, self.storage.data_class) - - # construct transport - self.transports = [] +class WarehouseFacility(FacilityBase): + """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" - for facility_conf in configs["transports"]: - transport = self.world.build_unit(facility_conf["class"]) - transport.data_class = facility_conf["data"]["class"] + SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id", "price", "delay_order_penalty")) - transport.world = self.world - transport.facility = self - transport.data_index = self.world.register_data_class(transport.id, transport.data_class) + # Storage unit for this facility, must be a sub class of StorageUnit. + storage: StorageUnit = None - self.transports.append(transport) + # Distribution unit for this facility. + distribution: DistributionUnit = None - # construct distribution - self.distribution = self.world.build_unit(configs["distribution"]["class"]) - self.distribution.data_class = configs["distribution"]["data"]["class"] + # Product unit list for this facility. + products: List[ProductUnit] = None - self.distribution.world = self.world - self.distribution.facility = self - self.distribution.data_index = self.world.register_data_class(self.distribution.id, self.distribution.data_class) + def __init__(self): + self.skus = {} - # sku information - self.sku_information = {} - self.consumers = {} + def parse_skus(self, configs: dict): + for sku_name, sku_config in configs.items(): + sku = self.world.get_sku_by_name(sku_name) - for sku_name, sku_config in configs["skus"].items(): - sku = self.world.get_sku(sku_name) sku_info = WarehouseFacility.SkuInfo( sku_name, sku_config["init_stock"], @@ -77,115 +38,4 @@ def build(self, configs: dict): sku_config.get("delay_order_penalty", 0) ) - self.sku_information[sku.id] = sku_info - - consumer = self.world.build_unit(configs["consumers"]["class"]) - consumer.data_class = configs["consumers"]["data"]["class"] - - consumer.world = self.world - consumer.facility = self - consumer.data_index = self.world.register_data_class(consumer.id, consumer.data_class) - - self.consumers[sku.id] = consumer - - def initialize(self, durations: int): - self.data.set_id(self.id, self.id) - self.data.initialize({}) - - self.storage.prepare_data() - self.distribution.prepare_data() - - for transport in self.transports: - transport.prepare_data() - - for consumer in self.consumers.values(): - consumer.prepare_data() - - # init components that related with sku number - self._init_by_skus() - - # called after build, here we have the data model, we can initialize them. - self.storage.initialize(self.configs.get("storage", {}), durations) - self.distribution.initialize(self.configs.get("distribution", {}), durations) - - transports_conf = self.configs.get("transports", []) - - for index, transport in enumerate(self.transports): - transport.initialize(transports_conf[index], durations) - - for sku_id, consumer in self.consumers.items(): - consumer.initialize({ - "data": { - "order_cost": self.configs.get("order_cost", 0), - "consumer_product_id": sku_id - } - }, durations) - - def reset(self): - # NOTE: as we are using list attribute now, theirs size will be reset to defined one after frame.reset, - # so we have to init them again. - self._init_by_skus() - - self.storage.reset() - self.distribution.reset() - - for vehicle in self.transports: - vehicle.reset() - - for consumer in self.consumers.values(): - consumer.reset() - - def begin_post_step(self, tick: int): - self.storage.begin_post_step(tick) - - self.distribution.begin_post_step(tick) - - for vehicle in self.transports: - vehicle.begin_post_step(tick) - - for consumer in self.consumers.values(): - consumer.begin_post_step(tick) - - - def end_post_step(self, tick: int): - self.storage.end_post_step(tick) - - self.distribution.end_post_step(tick) - - for vehicle in self.transports: - vehicle.end_post_step(tick) - - for consumer in self.consumers.values(): - consumer.end_post_step(tick) - - def get_node_info(self) -> dict: - return { - "id": self.id, - "name": self.name, - "class": type(self), - "units": { - "storage": self.storage.get_unit_info(), - "consumers": [consumer.get_unit_info() for consumer in self.consumers.values()], - "transports": [vehicle.get_unit_info() for vehicle in self.transports], - "distribution": self.distribution.get_unit_info() - } - } - - def _init_by_skus(self): - for _, sku in self.sku_information.items(): - # update storage's production info - self.storage.data.product_list.append(sku.id) - self.storage.data.product_number.append(sku.init_in_stock) - - # update distribution's production info - self.distribution.data.product_list.append(sku.id) - self.distribution.data.check_in_price.append(0) - self.distribution.data.delay_order_penalty.append(0) - - if self.upstreams is not None: - # update the source facilities for each consumer - for sku_id, source_facilities in self.upstreams.items(): - consumer = self.consumers[sku_id] - - for facility_id in source_facilities: - consumer.data.sources.append(facility_id) + self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/frame_builder.py b/maro/simulator/scenarios/supply_chain/frame_builder.py index 6b3549d27..f751c7188 100644 --- a/maro/simulator/scenarios/supply_chain/frame_builder.py +++ b/maro/simulator/scenarios/supply_chain/frame_builder.py @@ -1,4 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from typing import List, Tuple + from maro.backends.frame import NodeBase, FrameBase, FrameNode diff --git a/maro/simulator/scenarios/supply_chain/parser.py b/maro/simulator/scenarios/supply_chain/parser.py new file mode 100644 index 000000000..446fb5756 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/parser.py @@ -0,0 +1,220 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from collections import namedtuple +from importlib import import_module + +from yaml import safe_load + +DataModelDef = namedtuple("DataModelDef", ("alias", "module_path", "class_name", "class_type", "name_in_frame")) +UnitDef = namedtuple("UnitDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) +FacilityDef = namedtuple("FacilityDef", ("alias", "module_path", "class_name", "class_type")) + + +def find_class_type(module_path: str, class_name: str) -> type: + """Find class type by module path and class name. + + Args: + module_path (str): Full path of the module. + class_name (str): Class name to find. + + Returns: + type: Type of specified class. + """ + target_module = import_module(module_path) + + return getattr(target_module, class_name) + + +def copy_dict(target: dict, source: dict): + """Copy values from source to target dict. + + Args: + target (dict): Target dictionary to copy to. + source (dict): Source dictionary to copy from. + """ + for k, v in source.items(): + if type(v) != dict: + target[k] = v + else: + if k not in target: + target[k] = {} + + copy_dict(target[k], v) + + +class SupplyChainConfiguration: + """Configuration of supply chain scenario.""" + + def __init__(self): + # Data model definitions. + self.data_models = {} + + # Unit definitions. + self.units = {} + + # Facility definitions. + self.facilities = {} + + # World configurations. + self.world = {} + + def add_data_definition(self, alias: str, class_name: str, module_path: str, name_in_frame: str): + """Add a data model definition. + + Args: + alias (str): Alias of this data model. + class_name (str): Name of class. + module_path (str): Full path of module. + name_in_frame (str): Data model name in frame. + """ + # Check conflicting. + assert alias not in self.data_models + + self.data_models[alias] = DataModelDef( + alias, + module_path, + class_name, + find_class_type(module_path, class_name), + name_in_frame + ) + + def add_unit_definition(self, alias: str, class_name: str, module_path: str, data_model: str): + """Add unit definition. + + Args: + alias (str): Alias of this data model. + class_name (str): Name of class. + module_path (str): Full path of module. + data_model (str): Data model used for this unit. + """ + assert alias not in self.units + + self.units[alias] = UnitDef( + alias, + module_path, + class_name, + find_class_type(module_path, class_name), + data_model + ) + + def add_facility_definition(self, alias: str, class_name: str, module_path: str): + """Add a facility definition. + + Args: + alias (str): Alias of this facility. + class_name (str): Name of this class. + module_path (str): Full path of the module. + """ + assert alias not in self.facilities + + self.facilities[alias] = FacilityDef( + alias, + module_path, + class_name, + find_class_type(module_path, class_name) + ) + + +class ConfigParser: + """Supply chain configuration parser.""" + + def __init__(self, core_path: str, config_path: str): + self._result = SupplyChainConfiguration() + + self._core_path = core_path + self._config_path = config_path + + def parse(self): + """Parse configuration of current scenario. + + Returns: + SupplyChainConfiguration: Configuration result of this scenario. + """ + self._parse_core() + self._parse_config() + + return self._result + + def _parse_core(self): + """Parse configuration from core.yml.""" + with open(self._core_path, "rt") as fp: + conf = safe_load(fp) + + self._parse_core_conf(conf) + + def _parse_core_conf(self, conf: dict): + # Data models. + if "datamodels" in conf: + for module_conf in conf["datamodels"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + self._result.add_data_definition( + class_alias, + class_def["class"], + module_path, + class_def["name_in_frame"] + ) + + # TODO: dup code + # Units. + if "units" in conf: + for module_conf in conf["units"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + # children not in unit definition + self._result.add_unit_definition( + class_alias, + class_def["class"], + module_path, + class_def.get("datamodel", None) + ) + + # Facilities. + if "facilities" in conf: + for module_conf in conf["facilities"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + self._result.add_facility_definition(class_alias, class_def["class"], module_path) + + def _parse_config(self): + """Parse configurations.""" + with open(self._config_path, "rt") as fp: + conf = safe_load(fp) + + # Read customized core part. + customized_core_conf = conf.get("core", None) + + if customized_core_conf is not None: + self._parse_core_conf(customized_core_conf) + + # Facility definitions is not required, but it would be much simple to config with it + facility_definitions = conf.get("facility_definitions", {}) + world_def = conf["world"] + + # Go through world configurations to generate a full one. + # . Copy other configurations first + for sub_conf_name in ("skus", "topology", "grid"): + self._result.world[sub_conf_name] = world_def[sub_conf_name] + + # . Copy facilities content different if without definition reference. + # or copy from definition first, then override with current. + self._result.world["facilities"] = [] + + for facility_conf in world_def["facilities"]: + facility_ref = facility_conf.get("definition_ref", None) + + facility = {} + + if facility_ref is not None: + # Copy definition from base. + copy_dict(facility, facility_definitions[facility_ref]) + + # Override with current. + copy_dict(facility, facility_conf) + + self._result.world["facilities"].append(facility) diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml index b5d8b2c16..96ac008bd 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/core.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/core.yml @@ -1,14 +1,14 @@ -data: +datamodels: modules: - - path: "maro.simulator.scenarios.supply_chain.data" + - path: "maro.simulator.scenarios.supply_chain.datamodels" definitions: StorageDataModel: class: "StorageDataModel" name_in_frame: "storage" - TransportDataModel: - class: "TransportDataModel" - name_in_frame: "transport" + VehicleDataModel: + class: "VehicleDataModel" + name_in_frame: "vehicle" DistributionDataModel: class: "DistributionDataModel" name_in_frame: "distribution" @@ -28,28 +28,24 @@ units: definitions: StorageUnit: class: "StorageUnit" - data: - class: "StorageDataModel" - TransportUnit: - class: "TransportUnit" - data: - class: "TransportDataModel" + datamodel: "StorageDataModel" + VehicleUnit: + class: "VehicleUnit" + datamodel: "VehicleDataModel" DistributionUnit: class: "DistributionUnit" - data: - class: "DistributionDataModel" + datamodel: "DistributionDataModel" ConsumerUnit: class: "ConsumerUnit" - data: - class: "ConsumerDataModel" + datamodel: "ConsumerDataModel" SellerUnit: class: "SellerUnit" - data: - class: "SellerDataModel" + datamodel: "SellerDataModel" ManufactureUnit: class: "ManufactureUnit" - data: - class: "ManufactureDataModel" + datamodel: "ManufactureDataModel" + ProductUnit: + class: "ProductUnit" facilities: modules: @@ -57,30 +53,7 @@ facilities: definitions: WarehouseFacility: class: "WarehouseFacility" - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - consumers: - class: "ConsumerUnit" - transports: [] SupplierFacility: class: "SupplierFacility" - distribution: - class: "DistributionUnit" - storage: - class: "StorageUnit" - consumers: - class: "ConsumerUnit" - manufactures: - class: "ManufactureUnit" - # transports should be defined by user, so here is just a empty list - transports: [] RetailerFacility: class: "RetailerFacility" - storage: - class: "StorageUnit" - consumers: - class: "ConsumerUnit" - sellers: - class: "SellerUnit" diff --git a/maro/simulator/scenarios/supply_chain/topologies/perf_4x1000_facilities/config.yml b/maro/simulator/scenarios/supply_chain/topologies/perf_4x1000_facilities/config.yml deleted file mode 100644 index ad73e64d1..000000000 --- a/maro/simulator/scenarios/supply_chain/topologies/perf_4x1000_facilities/config.yml +++ /dev/null @@ -1,12163 +0,0 @@ -base: '' -world: - action_steps: 1 - facilities: - - class: SupplierFacility - configs: &id001 - distribution: - data: - unit_price: 10 - skus: - sku3: - cost": 10 - delay_order_penalty: 10 - init_in_stock: 100 - price": 10 - product_unit_cost: 1 - production_rate: 0 - type: production - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - transports: - - class: TransportUnit - data: - patient: 100 - - class: TransportUnit - data: - patient: 100 - name: Supplier3 - - class: SupplierFacility - configs: &id002 - distribution: - data: - unit_price: 10 - skus: - sku1: - cost: 10 - delay_order_penalty: 10 - init_in_stock: 100 - price: 100 - product_unit_cost: 1 - production_rate: 0 - type: production - sku3: - cost: 10 - delay_order_penalty: 10 - init_in_stock: 100 - price: 100 - production_rate: 200 - type: material - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - transports: - - class: TransportUnit - data: - patient: 100 - - class: TransportUnit - data: - patient: 100 - name: Supplier1 - - class: WarehouseFacility - configs: &id003 - distribution: - data: - unit_price: 10 - skus: - sku1: - delay_order_penalty: 10 - init_stock: 1000 - price: 100 - sku2: - delay_order_penalty: 10 - init_stock: 1000 - price: 100 - sku3: - delay_order_penalty: 10 - init_stock: 1000 - price: 100 - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - transports: - - class: TransportUnit - data: - patient: 100 - unit_transport_cost: 1 - - class: TransportUnit - data: - patient: 100 - unit_transport_cost: 1 - name: Warehouse1 - - class: RetailerFacility - configs: &id004 - skus: - sku1: - backlog_ratio: 0.1 - cost: 10 - init_in_stock: 100 - price: 300 - sale_gamma: 100 - sku2: - backlog_ratio: 0.1 - cost: 10 - init_in_stock: 100 - price: 100 - sale_gamma: 100 - sku3: - backlog_ratio: 0.1 - cost: 10 - init_in_stock: 100 - price: 200 - sale_gamma: 100 - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - name: ChaoShiFa01 - - class: SupplierFacility - configs: *id001 - name: Supplier3_0 - - class: SupplierFacility - configs: *id002 - name: Supplier1_0 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_0 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_0 - - class: SupplierFacility - configs: *id001 - name: Supplier3_1 - - class: SupplierFacility - configs: *id002 - name: Supplier1_1 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_1 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_1 - - class: SupplierFacility - configs: *id001 - name: Supplier3_2 - - class: SupplierFacility - configs: *id002 - name: Supplier1_2 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_2 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_2 - - class: SupplierFacility - configs: *id001 - name: Supplier3_3 - - class: SupplierFacility - configs: *id002 - name: Supplier1_3 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_3 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_3 - - class: SupplierFacility - configs: *id001 - name: Supplier3_4 - - class: SupplierFacility - configs: *id002 - name: Supplier1_4 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_4 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_4 - - class: SupplierFacility - configs: *id001 - name: Supplier3_5 - - class: SupplierFacility - configs: *id002 - name: Supplier1_5 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_5 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_5 - - class: SupplierFacility - configs: *id001 - name: Supplier3_6 - - class: SupplierFacility - configs: *id002 - name: Supplier1_6 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_6 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_6 - - class: SupplierFacility - configs: *id001 - name: Supplier3_7 - - class: SupplierFacility - configs: *id002 - name: Supplier1_7 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_7 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_7 - - class: SupplierFacility - configs: *id001 - name: Supplier3_8 - - class: SupplierFacility - configs: *id002 - name: Supplier1_8 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_8 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_8 - - class: SupplierFacility - configs: *id001 - name: Supplier3_9 - - class: SupplierFacility - configs: *id002 - name: Supplier1_9 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_9 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_9 - - class: SupplierFacility - configs: *id001 - name: Supplier3_10 - - class: SupplierFacility - configs: *id002 - name: Supplier1_10 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_10 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_10 - - class: SupplierFacility - configs: *id001 - name: Supplier3_11 - - class: SupplierFacility - configs: *id002 - name: Supplier1_11 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_11 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_11 - - class: SupplierFacility - configs: *id001 - name: Supplier3_12 - - class: SupplierFacility - configs: *id002 - name: Supplier1_12 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_12 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_12 - - class: SupplierFacility - configs: *id001 - name: Supplier3_13 - - class: SupplierFacility - configs: *id002 - name: Supplier1_13 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_13 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_13 - - class: SupplierFacility - configs: *id001 - name: Supplier3_14 - - class: SupplierFacility - configs: *id002 - name: Supplier1_14 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_14 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_14 - - class: SupplierFacility - configs: *id001 - name: Supplier3_15 - - class: SupplierFacility - configs: *id002 - name: Supplier1_15 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_15 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_15 - - class: SupplierFacility - configs: *id001 - name: Supplier3_16 - - class: SupplierFacility - configs: *id002 - name: Supplier1_16 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_16 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_16 - - class: SupplierFacility - configs: *id001 - name: Supplier3_17 - - class: SupplierFacility - configs: *id002 - name: Supplier1_17 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_17 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_17 - - class: SupplierFacility - configs: *id001 - name: Supplier3_18 - - class: SupplierFacility - configs: *id002 - name: Supplier1_18 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_18 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_18 - - class: SupplierFacility - configs: *id001 - name: Supplier3_19 - - class: SupplierFacility - configs: *id002 - name: Supplier1_19 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_19 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_19 - - class: SupplierFacility - configs: *id001 - name: Supplier3_20 - - class: SupplierFacility - configs: *id002 - name: Supplier1_20 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_20 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_20 - - class: SupplierFacility - configs: *id001 - name: Supplier3_21 - - class: SupplierFacility - configs: *id002 - name: Supplier1_21 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_21 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_21 - - class: SupplierFacility - configs: *id001 - name: Supplier3_22 - - class: SupplierFacility - configs: *id002 - name: Supplier1_22 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_22 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_22 - - class: SupplierFacility - configs: *id001 - name: Supplier3_23 - - class: SupplierFacility - configs: *id002 - name: Supplier1_23 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_23 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_23 - - class: SupplierFacility - configs: *id001 - name: Supplier3_24 - - class: SupplierFacility - configs: *id002 - name: Supplier1_24 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_24 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_24 - - class: SupplierFacility - configs: *id001 - name: Supplier3_25 - - class: SupplierFacility - configs: *id002 - name: Supplier1_25 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_25 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_25 - - class: SupplierFacility - configs: *id001 - name: Supplier3_26 - - class: SupplierFacility - configs: *id002 - name: Supplier1_26 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_26 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_26 - - class: SupplierFacility - configs: *id001 - name: Supplier3_27 - - class: SupplierFacility - configs: *id002 - name: Supplier1_27 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_27 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_27 - - class: SupplierFacility - configs: *id001 - name: Supplier3_28 - - class: SupplierFacility - configs: *id002 - name: Supplier1_28 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_28 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_28 - - class: SupplierFacility - configs: *id001 - name: Supplier3_29 - - class: SupplierFacility - configs: *id002 - name: Supplier1_29 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_29 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_29 - - class: SupplierFacility - configs: *id001 - name: Supplier3_30 - - class: SupplierFacility - configs: *id002 - name: Supplier1_30 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_30 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_30 - - class: SupplierFacility - configs: *id001 - name: Supplier3_31 - - class: SupplierFacility - configs: *id002 - name: Supplier1_31 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_31 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_31 - - class: SupplierFacility - configs: *id001 - name: Supplier3_32 - - class: SupplierFacility - configs: *id002 - name: Supplier1_32 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_32 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_32 - - class: SupplierFacility - configs: *id001 - name: Supplier3_33 - - class: SupplierFacility - configs: *id002 - name: Supplier1_33 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_33 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_33 - - class: SupplierFacility - configs: *id001 - name: Supplier3_34 - - class: SupplierFacility - configs: *id002 - name: Supplier1_34 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_34 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_34 - - class: SupplierFacility - configs: *id001 - name: Supplier3_35 - - class: SupplierFacility - configs: *id002 - name: Supplier1_35 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_35 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_35 - - class: SupplierFacility - configs: *id001 - name: Supplier3_36 - - class: SupplierFacility - configs: *id002 - name: Supplier1_36 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_36 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_36 - - class: SupplierFacility - configs: *id001 - name: Supplier3_37 - - class: SupplierFacility - configs: *id002 - name: Supplier1_37 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_37 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_37 - - class: SupplierFacility - configs: *id001 - name: Supplier3_38 - - class: SupplierFacility - configs: *id002 - name: Supplier1_38 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_38 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_38 - - class: SupplierFacility - configs: *id001 - name: Supplier3_39 - - class: SupplierFacility - configs: *id002 - name: Supplier1_39 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_39 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_39 - - class: SupplierFacility - configs: *id001 - name: Supplier3_40 - - class: SupplierFacility - configs: *id002 - name: Supplier1_40 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_40 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_40 - - class: SupplierFacility - configs: *id001 - name: Supplier3_41 - - class: SupplierFacility - configs: *id002 - name: Supplier1_41 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_41 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_41 - - class: SupplierFacility - configs: *id001 - name: Supplier3_42 - - class: SupplierFacility - configs: *id002 - name: Supplier1_42 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_42 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_42 - - class: SupplierFacility - configs: *id001 - name: Supplier3_43 - - class: SupplierFacility - configs: *id002 - name: Supplier1_43 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_43 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_43 - - class: SupplierFacility - configs: *id001 - name: Supplier3_44 - - class: SupplierFacility - configs: *id002 - name: Supplier1_44 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_44 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_44 - - class: SupplierFacility - configs: *id001 - name: Supplier3_45 - - class: SupplierFacility - configs: *id002 - name: Supplier1_45 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_45 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_45 - - class: SupplierFacility - configs: *id001 - name: Supplier3_46 - - class: SupplierFacility - configs: *id002 - name: Supplier1_46 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_46 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_46 - - class: SupplierFacility - configs: *id001 - name: Supplier3_47 - - class: SupplierFacility - configs: *id002 - name: Supplier1_47 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_47 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_47 - - class: SupplierFacility - configs: *id001 - name: Supplier3_48 - - class: SupplierFacility - configs: *id002 - name: Supplier1_48 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_48 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_48 - - class: SupplierFacility - configs: *id001 - name: Supplier3_49 - - class: SupplierFacility - configs: *id002 - name: Supplier1_49 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_49 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_49 - - class: SupplierFacility - configs: *id001 - name: Supplier3_50 - - class: SupplierFacility - configs: *id002 - name: Supplier1_50 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_50 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_50 - - class: SupplierFacility - configs: *id001 - name: Supplier3_51 - - class: SupplierFacility - configs: *id002 - name: Supplier1_51 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_51 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_51 - - class: SupplierFacility - configs: *id001 - name: Supplier3_52 - - class: SupplierFacility - configs: *id002 - name: Supplier1_52 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_52 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_52 - - class: SupplierFacility - configs: *id001 - name: Supplier3_53 - - class: SupplierFacility - configs: *id002 - name: Supplier1_53 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_53 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_53 - - class: SupplierFacility - configs: *id001 - name: Supplier3_54 - - class: SupplierFacility - configs: *id002 - name: Supplier1_54 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_54 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_54 - - class: SupplierFacility - configs: *id001 - name: Supplier3_55 - - class: SupplierFacility - configs: *id002 - name: Supplier1_55 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_55 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_55 - - class: SupplierFacility - configs: *id001 - name: Supplier3_56 - - class: SupplierFacility - configs: *id002 - name: Supplier1_56 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_56 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_56 - - class: SupplierFacility - configs: *id001 - name: Supplier3_57 - - class: SupplierFacility - configs: *id002 - name: Supplier1_57 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_57 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_57 - - class: SupplierFacility - configs: *id001 - name: Supplier3_58 - - class: SupplierFacility - configs: *id002 - name: Supplier1_58 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_58 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_58 - - class: SupplierFacility - configs: *id001 - name: Supplier3_59 - - class: SupplierFacility - configs: *id002 - name: Supplier1_59 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_59 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_59 - - class: SupplierFacility - configs: *id001 - name: Supplier3_60 - - class: SupplierFacility - configs: *id002 - name: Supplier1_60 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_60 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_60 - - class: SupplierFacility - configs: *id001 - name: Supplier3_61 - - class: SupplierFacility - configs: *id002 - name: Supplier1_61 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_61 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_61 - - class: SupplierFacility - configs: *id001 - name: Supplier3_62 - - class: SupplierFacility - configs: *id002 - name: Supplier1_62 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_62 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_62 - - class: SupplierFacility - configs: *id001 - name: Supplier3_63 - - class: SupplierFacility - configs: *id002 - name: Supplier1_63 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_63 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_63 - - class: SupplierFacility - configs: *id001 - name: Supplier3_64 - - class: SupplierFacility - configs: *id002 - name: Supplier1_64 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_64 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_64 - - class: SupplierFacility - configs: *id001 - name: Supplier3_65 - - class: SupplierFacility - configs: *id002 - name: Supplier1_65 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_65 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_65 - - class: SupplierFacility - configs: *id001 - name: Supplier3_66 - - class: SupplierFacility - configs: *id002 - name: Supplier1_66 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_66 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_66 - - class: SupplierFacility - configs: *id001 - name: Supplier3_67 - - class: SupplierFacility - configs: *id002 - name: Supplier1_67 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_67 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_67 - - class: SupplierFacility - configs: *id001 - name: Supplier3_68 - - class: SupplierFacility - configs: *id002 - name: Supplier1_68 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_68 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_68 - - class: SupplierFacility - configs: *id001 - name: Supplier3_69 - - class: SupplierFacility - configs: *id002 - name: Supplier1_69 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_69 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_69 - - class: SupplierFacility - configs: *id001 - name: Supplier3_70 - - class: SupplierFacility - configs: *id002 - name: Supplier1_70 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_70 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_70 - - class: SupplierFacility - configs: *id001 - name: Supplier3_71 - - class: SupplierFacility - configs: *id002 - name: Supplier1_71 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_71 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_71 - - class: SupplierFacility - configs: *id001 - name: Supplier3_72 - - class: SupplierFacility - configs: *id002 - name: Supplier1_72 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_72 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_72 - - class: SupplierFacility - configs: *id001 - name: Supplier3_73 - - class: SupplierFacility - configs: *id002 - name: Supplier1_73 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_73 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_73 - - class: SupplierFacility - configs: *id001 - name: Supplier3_74 - - class: SupplierFacility - configs: *id002 - name: Supplier1_74 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_74 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_74 - - class: SupplierFacility - configs: *id001 - name: Supplier3_75 - - class: SupplierFacility - configs: *id002 - name: Supplier1_75 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_75 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_75 - - class: SupplierFacility - configs: *id001 - name: Supplier3_76 - - class: SupplierFacility - configs: *id002 - name: Supplier1_76 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_76 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_76 - - class: SupplierFacility - configs: *id001 - name: Supplier3_77 - - class: SupplierFacility - configs: *id002 - name: Supplier1_77 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_77 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_77 - - class: SupplierFacility - configs: *id001 - name: Supplier3_78 - - class: SupplierFacility - configs: *id002 - name: Supplier1_78 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_78 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_78 - - class: SupplierFacility - configs: *id001 - name: Supplier3_79 - - class: SupplierFacility - configs: *id002 - name: Supplier1_79 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_79 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_79 - - class: SupplierFacility - configs: *id001 - name: Supplier3_80 - - class: SupplierFacility - configs: *id002 - name: Supplier1_80 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_80 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_80 - - class: SupplierFacility - configs: *id001 - name: Supplier3_81 - - class: SupplierFacility - configs: *id002 - name: Supplier1_81 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_81 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_81 - - class: SupplierFacility - configs: *id001 - name: Supplier3_82 - - class: SupplierFacility - configs: *id002 - name: Supplier1_82 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_82 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_82 - - class: SupplierFacility - configs: *id001 - name: Supplier3_83 - - class: SupplierFacility - configs: *id002 - name: Supplier1_83 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_83 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_83 - - class: SupplierFacility - configs: *id001 - name: Supplier3_84 - - class: SupplierFacility - configs: *id002 - name: Supplier1_84 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_84 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_84 - - class: SupplierFacility - configs: *id001 - name: Supplier3_85 - - class: SupplierFacility - configs: *id002 - name: Supplier1_85 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_85 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_85 - - class: SupplierFacility - configs: *id001 - name: Supplier3_86 - - class: SupplierFacility - configs: *id002 - name: Supplier1_86 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_86 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_86 - - class: SupplierFacility - configs: *id001 - name: Supplier3_87 - - class: SupplierFacility - configs: *id002 - name: Supplier1_87 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_87 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_87 - - class: SupplierFacility - configs: *id001 - name: Supplier3_88 - - class: SupplierFacility - configs: *id002 - name: Supplier1_88 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_88 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_88 - - class: SupplierFacility - configs: *id001 - name: Supplier3_89 - - class: SupplierFacility - configs: *id002 - name: Supplier1_89 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_89 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_89 - - class: SupplierFacility - configs: *id001 - name: Supplier3_90 - - class: SupplierFacility - configs: *id002 - name: Supplier1_90 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_90 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_90 - - class: SupplierFacility - configs: *id001 - name: Supplier3_91 - - class: SupplierFacility - configs: *id002 - name: Supplier1_91 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_91 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_91 - - class: SupplierFacility - configs: *id001 - name: Supplier3_92 - - class: SupplierFacility - configs: *id002 - name: Supplier1_92 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_92 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_92 - - class: SupplierFacility - configs: *id001 - name: Supplier3_93 - - class: SupplierFacility - configs: *id002 - name: Supplier1_93 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_93 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_93 - - class: SupplierFacility - configs: *id001 - name: Supplier3_94 - - class: SupplierFacility - configs: *id002 - name: Supplier1_94 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_94 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_94 - - class: SupplierFacility - configs: *id001 - name: Supplier3_95 - - class: SupplierFacility - configs: *id002 - name: Supplier1_95 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_95 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_95 - - class: SupplierFacility - configs: *id001 - name: Supplier3_96 - - class: SupplierFacility - configs: *id002 - name: Supplier1_96 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_96 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_96 - - class: SupplierFacility - configs: *id001 - name: Supplier3_97 - - class: SupplierFacility - configs: *id002 - name: Supplier1_97 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_97 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_97 - - class: SupplierFacility - configs: *id001 - name: Supplier3_98 - - class: SupplierFacility - configs: *id002 - name: Supplier1_98 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_98 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_98 - - class: SupplierFacility - configs: *id001 - name: Supplier3_99 - - class: SupplierFacility - configs: *id002 - name: Supplier1_99 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_99 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_99 - - class: SupplierFacility - configs: *id001 - name: Supplier3_100 - - class: SupplierFacility - configs: *id002 - name: Supplier1_100 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_100 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_100 - - class: SupplierFacility - configs: *id001 - name: Supplier3_101 - - class: SupplierFacility - configs: *id002 - name: Supplier1_101 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_101 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_101 - - class: SupplierFacility - configs: *id001 - name: Supplier3_102 - - class: SupplierFacility - configs: *id002 - name: Supplier1_102 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_102 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_102 - - class: SupplierFacility - configs: *id001 - name: Supplier3_103 - - class: SupplierFacility - configs: *id002 - name: Supplier1_103 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_103 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_103 - - class: SupplierFacility - configs: *id001 - name: Supplier3_104 - - class: SupplierFacility - configs: *id002 - name: Supplier1_104 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_104 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_104 - - class: SupplierFacility - configs: *id001 - name: Supplier3_105 - - class: SupplierFacility - configs: *id002 - name: Supplier1_105 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_105 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_105 - - class: SupplierFacility - configs: *id001 - name: Supplier3_106 - - class: SupplierFacility - configs: *id002 - name: Supplier1_106 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_106 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_106 - - class: SupplierFacility - configs: *id001 - name: Supplier3_107 - - class: SupplierFacility - configs: *id002 - name: Supplier1_107 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_107 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_107 - - class: SupplierFacility - configs: *id001 - name: Supplier3_108 - - class: SupplierFacility - configs: *id002 - name: Supplier1_108 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_108 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_108 - - class: SupplierFacility - configs: *id001 - name: Supplier3_109 - - class: SupplierFacility - configs: *id002 - name: Supplier1_109 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_109 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_109 - - class: SupplierFacility - configs: *id001 - name: Supplier3_110 - - class: SupplierFacility - configs: *id002 - name: Supplier1_110 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_110 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_110 - - class: SupplierFacility - configs: *id001 - name: Supplier3_111 - - class: SupplierFacility - configs: *id002 - name: Supplier1_111 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_111 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_111 - - class: SupplierFacility - configs: *id001 - name: Supplier3_112 - - class: SupplierFacility - configs: *id002 - name: Supplier1_112 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_112 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_112 - - class: SupplierFacility - configs: *id001 - name: Supplier3_113 - - class: SupplierFacility - configs: *id002 - name: Supplier1_113 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_113 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_113 - - class: SupplierFacility - configs: *id001 - name: Supplier3_114 - - class: SupplierFacility - configs: *id002 - name: Supplier1_114 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_114 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_114 - - class: SupplierFacility - configs: *id001 - name: Supplier3_115 - - class: SupplierFacility - configs: *id002 - name: Supplier1_115 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_115 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_115 - - class: SupplierFacility - configs: *id001 - name: Supplier3_116 - - class: SupplierFacility - configs: *id002 - name: Supplier1_116 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_116 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_116 - - class: SupplierFacility - configs: *id001 - name: Supplier3_117 - - class: SupplierFacility - configs: *id002 - name: Supplier1_117 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_117 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_117 - - class: SupplierFacility - configs: *id001 - name: Supplier3_118 - - class: SupplierFacility - configs: *id002 - name: Supplier1_118 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_118 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_118 - - class: SupplierFacility - configs: *id001 - name: Supplier3_119 - - class: SupplierFacility - configs: *id002 - name: Supplier1_119 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_119 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_119 - - class: SupplierFacility - configs: *id001 - name: Supplier3_120 - - class: SupplierFacility - configs: *id002 - name: Supplier1_120 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_120 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_120 - - class: SupplierFacility - configs: *id001 - name: Supplier3_121 - - class: SupplierFacility - configs: *id002 - name: Supplier1_121 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_121 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_121 - - class: SupplierFacility - configs: *id001 - name: Supplier3_122 - - class: SupplierFacility - configs: *id002 - name: Supplier1_122 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_122 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_122 - - class: SupplierFacility - configs: *id001 - name: Supplier3_123 - - class: SupplierFacility - configs: *id002 - name: Supplier1_123 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_123 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_123 - - class: SupplierFacility - configs: *id001 - name: Supplier3_124 - - class: SupplierFacility - configs: *id002 - name: Supplier1_124 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_124 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_124 - - class: SupplierFacility - configs: *id001 - name: Supplier3_125 - - class: SupplierFacility - configs: *id002 - name: Supplier1_125 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_125 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_125 - - class: SupplierFacility - configs: *id001 - name: Supplier3_126 - - class: SupplierFacility - configs: *id002 - name: Supplier1_126 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_126 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_126 - - class: SupplierFacility - configs: *id001 - name: Supplier3_127 - - class: SupplierFacility - configs: *id002 - name: Supplier1_127 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_127 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_127 - - class: SupplierFacility - configs: *id001 - name: Supplier3_128 - - class: SupplierFacility - configs: *id002 - name: Supplier1_128 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_128 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_128 - - class: SupplierFacility - configs: *id001 - name: Supplier3_129 - - class: SupplierFacility - configs: *id002 - name: Supplier1_129 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_129 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_129 - - class: SupplierFacility - configs: *id001 - name: Supplier3_130 - - class: SupplierFacility - configs: *id002 - name: Supplier1_130 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_130 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_130 - - class: SupplierFacility - configs: *id001 - name: Supplier3_131 - - class: SupplierFacility - configs: *id002 - name: Supplier1_131 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_131 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_131 - - class: SupplierFacility - configs: *id001 - name: Supplier3_132 - - class: SupplierFacility - configs: *id002 - name: Supplier1_132 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_132 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_132 - - class: SupplierFacility - configs: *id001 - name: Supplier3_133 - - class: SupplierFacility - configs: *id002 - name: Supplier1_133 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_133 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_133 - - class: SupplierFacility - configs: *id001 - name: Supplier3_134 - - class: SupplierFacility - configs: *id002 - name: Supplier1_134 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_134 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_134 - - class: SupplierFacility - configs: *id001 - name: Supplier3_135 - - class: SupplierFacility - configs: *id002 - name: Supplier1_135 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_135 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_135 - - class: SupplierFacility - configs: *id001 - name: Supplier3_136 - - class: SupplierFacility - configs: *id002 - name: Supplier1_136 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_136 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_136 - - class: SupplierFacility - configs: *id001 - name: Supplier3_137 - - class: SupplierFacility - configs: *id002 - name: Supplier1_137 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_137 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_137 - - class: SupplierFacility - configs: *id001 - name: Supplier3_138 - - class: SupplierFacility - configs: *id002 - name: Supplier1_138 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_138 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_138 - - class: SupplierFacility - configs: *id001 - name: Supplier3_139 - - class: SupplierFacility - configs: *id002 - name: Supplier1_139 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_139 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_139 - - class: SupplierFacility - configs: *id001 - name: Supplier3_140 - - class: SupplierFacility - configs: *id002 - name: Supplier1_140 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_140 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_140 - - class: SupplierFacility - configs: *id001 - name: Supplier3_141 - - class: SupplierFacility - configs: *id002 - name: Supplier1_141 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_141 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_141 - - class: SupplierFacility - configs: *id001 - name: Supplier3_142 - - class: SupplierFacility - configs: *id002 - name: Supplier1_142 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_142 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_142 - - class: SupplierFacility - configs: *id001 - name: Supplier3_143 - - class: SupplierFacility - configs: *id002 - name: Supplier1_143 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_143 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_143 - - class: SupplierFacility - configs: *id001 - name: Supplier3_144 - - class: SupplierFacility - configs: *id002 - name: Supplier1_144 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_144 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_144 - - class: SupplierFacility - configs: *id001 - name: Supplier3_145 - - class: SupplierFacility - configs: *id002 - name: Supplier1_145 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_145 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_145 - - class: SupplierFacility - configs: *id001 - name: Supplier3_146 - - class: SupplierFacility - configs: *id002 - name: Supplier1_146 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_146 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_146 - - class: SupplierFacility - configs: *id001 - name: Supplier3_147 - - class: SupplierFacility - configs: *id002 - name: Supplier1_147 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_147 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_147 - - class: SupplierFacility - configs: *id001 - name: Supplier3_148 - - class: SupplierFacility - configs: *id002 - name: Supplier1_148 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_148 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_148 - - class: SupplierFacility - configs: *id001 - name: Supplier3_149 - - class: SupplierFacility - configs: *id002 - name: Supplier1_149 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_149 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_149 - - class: SupplierFacility - configs: *id001 - name: Supplier3_150 - - class: SupplierFacility - configs: *id002 - name: Supplier1_150 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_150 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_150 - - class: SupplierFacility - configs: *id001 - name: Supplier3_151 - - class: SupplierFacility - configs: *id002 - name: Supplier1_151 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_151 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_151 - - class: SupplierFacility - configs: *id001 - name: Supplier3_152 - - class: SupplierFacility - configs: *id002 - name: Supplier1_152 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_152 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_152 - - class: SupplierFacility - configs: *id001 - name: Supplier3_153 - - class: SupplierFacility - configs: *id002 - name: Supplier1_153 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_153 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_153 - - class: SupplierFacility - configs: *id001 - name: Supplier3_154 - - class: SupplierFacility - configs: *id002 - name: Supplier1_154 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_154 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_154 - - class: SupplierFacility - configs: *id001 - name: Supplier3_155 - - class: SupplierFacility - configs: *id002 - name: Supplier1_155 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_155 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_155 - - class: SupplierFacility - configs: *id001 - name: Supplier3_156 - - class: SupplierFacility - configs: *id002 - name: Supplier1_156 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_156 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_156 - - class: SupplierFacility - configs: *id001 - name: Supplier3_157 - - class: SupplierFacility - configs: *id002 - name: Supplier1_157 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_157 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_157 - - class: SupplierFacility - configs: *id001 - name: Supplier3_158 - - class: SupplierFacility - configs: *id002 - name: Supplier1_158 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_158 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_158 - - class: SupplierFacility - configs: *id001 - name: Supplier3_159 - - class: SupplierFacility - configs: *id002 - name: Supplier1_159 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_159 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_159 - - class: SupplierFacility - configs: *id001 - name: Supplier3_160 - - class: SupplierFacility - configs: *id002 - name: Supplier1_160 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_160 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_160 - - class: SupplierFacility - configs: *id001 - name: Supplier3_161 - - class: SupplierFacility - configs: *id002 - name: Supplier1_161 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_161 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_161 - - class: SupplierFacility - configs: *id001 - name: Supplier3_162 - - class: SupplierFacility - configs: *id002 - name: Supplier1_162 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_162 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_162 - - class: SupplierFacility - configs: *id001 - name: Supplier3_163 - - class: SupplierFacility - configs: *id002 - name: Supplier1_163 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_163 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_163 - - class: SupplierFacility - configs: *id001 - name: Supplier3_164 - - class: SupplierFacility - configs: *id002 - name: Supplier1_164 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_164 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_164 - - class: SupplierFacility - configs: *id001 - name: Supplier3_165 - - class: SupplierFacility - configs: *id002 - name: Supplier1_165 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_165 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_165 - - class: SupplierFacility - configs: *id001 - name: Supplier3_166 - - class: SupplierFacility - configs: *id002 - name: Supplier1_166 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_166 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_166 - - class: SupplierFacility - configs: *id001 - name: Supplier3_167 - - class: SupplierFacility - configs: *id002 - name: Supplier1_167 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_167 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_167 - - class: SupplierFacility - configs: *id001 - name: Supplier3_168 - - class: SupplierFacility - configs: *id002 - name: Supplier1_168 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_168 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_168 - - class: SupplierFacility - configs: *id001 - name: Supplier3_169 - - class: SupplierFacility - configs: *id002 - name: Supplier1_169 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_169 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_169 - - class: SupplierFacility - configs: *id001 - name: Supplier3_170 - - class: SupplierFacility - configs: *id002 - name: Supplier1_170 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_170 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_170 - - class: SupplierFacility - configs: *id001 - name: Supplier3_171 - - class: SupplierFacility - configs: *id002 - name: Supplier1_171 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_171 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_171 - - class: SupplierFacility - configs: *id001 - name: Supplier3_172 - - class: SupplierFacility - configs: *id002 - name: Supplier1_172 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_172 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_172 - - class: SupplierFacility - configs: *id001 - name: Supplier3_173 - - class: SupplierFacility - configs: *id002 - name: Supplier1_173 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_173 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_173 - - class: SupplierFacility - configs: *id001 - name: Supplier3_174 - - class: SupplierFacility - configs: *id002 - name: Supplier1_174 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_174 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_174 - - class: SupplierFacility - configs: *id001 - name: Supplier3_175 - - class: SupplierFacility - configs: *id002 - name: Supplier1_175 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_175 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_175 - - class: SupplierFacility - configs: *id001 - name: Supplier3_176 - - class: SupplierFacility - configs: *id002 - name: Supplier1_176 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_176 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_176 - - class: SupplierFacility - configs: *id001 - name: Supplier3_177 - - class: SupplierFacility - configs: *id002 - name: Supplier1_177 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_177 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_177 - - class: SupplierFacility - configs: *id001 - name: Supplier3_178 - - class: SupplierFacility - configs: *id002 - name: Supplier1_178 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_178 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_178 - - class: SupplierFacility - configs: *id001 - name: Supplier3_179 - - class: SupplierFacility - configs: *id002 - name: Supplier1_179 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_179 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_179 - - class: SupplierFacility - configs: *id001 - name: Supplier3_180 - - class: SupplierFacility - configs: *id002 - name: Supplier1_180 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_180 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_180 - - class: SupplierFacility - configs: *id001 - name: Supplier3_181 - - class: SupplierFacility - configs: *id002 - name: Supplier1_181 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_181 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_181 - - class: SupplierFacility - configs: *id001 - name: Supplier3_182 - - class: SupplierFacility - configs: *id002 - name: Supplier1_182 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_182 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_182 - - class: SupplierFacility - configs: *id001 - name: Supplier3_183 - - class: SupplierFacility - configs: *id002 - name: Supplier1_183 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_183 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_183 - - class: SupplierFacility - configs: *id001 - name: Supplier3_184 - - class: SupplierFacility - configs: *id002 - name: Supplier1_184 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_184 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_184 - - class: SupplierFacility - configs: *id001 - name: Supplier3_185 - - class: SupplierFacility - configs: *id002 - name: Supplier1_185 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_185 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_185 - - class: SupplierFacility - configs: *id001 - name: Supplier3_186 - - class: SupplierFacility - configs: *id002 - name: Supplier1_186 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_186 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_186 - - class: SupplierFacility - configs: *id001 - name: Supplier3_187 - - class: SupplierFacility - configs: *id002 - name: Supplier1_187 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_187 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_187 - - class: SupplierFacility - configs: *id001 - name: Supplier3_188 - - class: SupplierFacility - configs: *id002 - name: Supplier1_188 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_188 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_188 - - class: SupplierFacility - configs: *id001 - name: Supplier3_189 - - class: SupplierFacility - configs: *id002 - name: Supplier1_189 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_189 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_189 - - class: SupplierFacility - configs: *id001 - name: Supplier3_190 - - class: SupplierFacility - configs: *id002 - name: Supplier1_190 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_190 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_190 - - class: SupplierFacility - configs: *id001 - name: Supplier3_191 - - class: SupplierFacility - configs: *id002 - name: Supplier1_191 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_191 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_191 - - class: SupplierFacility - configs: *id001 - name: Supplier3_192 - - class: SupplierFacility - configs: *id002 - name: Supplier1_192 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_192 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_192 - - class: SupplierFacility - configs: *id001 - name: Supplier3_193 - - class: SupplierFacility - configs: *id002 - name: Supplier1_193 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_193 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_193 - - class: SupplierFacility - configs: *id001 - name: Supplier3_194 - - class: SupplierFacility - configs: *id002 - name: Supplier1_194 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_194 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_194 - - class: SupplierFacility - configs: *id001 - name: Supplier3_195 - - class: SupplierFacility - configs: *id002 - name: Supplier1_195 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_195 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_195 - - class: SupplierFacility - configs: *id001 - name: Supplier3_196 - - class: SupplierFacility - configs: *id002 - name: Supplier1_196 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_196 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_196 - - class: SupplierFacility - configs: *id001 - name: Supplier3_197 - - class: SupplierFacility - configs: *id002 - name: Supplier1_197 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_197 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_197 - - class: SupplierFacility - configs: *id001 - name: Supplier3_198 - - class: SupplierFacility - configs: *id002 - name: Supplier1_198 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_198 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_198 - - class: SupplierFacility - configs: *id001 - name: Supplier3_199 - - class: SupplierFacility - configs: *id002 - name: Supplier1_199 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_199 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_199 - - class: SupplierFacility - configs: *id001 - name: Supplier3_200 - - class: SupplierFacility - configs: *id002 - name: Supplier1_200 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_200 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_200 - - class: SupplierFacility - configs: *id001 - name: Supplier3_201 - - class: SupplierFacility - configs: *id002 - name: Supplier1_201 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_201 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_201 - - class: SupplierFacility - configs: *id001 - name: Supplier3_202 - - class: SupplierFacility - configs: *id002 - name: Supplier1_202 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_202 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_202 - - class: SupplierFacility - configs: *id001 - name: Supplier3_203 - - class: SupplierFacility - configs: *id002 - name: Supplier1_203 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_203 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_203 - - class: SupplierFacility - configs: *id001 - name: Supplier3_204 - - class: SupplierFacility - configs: *id002 - name: Supplier1_204 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_204 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_204 - - class: SupplierFacility - configs: *id001 - name: Supplier3_205 - - class: SupplierFacility - configs: *id002 - name: Supplier1_205 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_205 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_205 - - class: SupplierFacility - configs: *id001 - name: Supplier3_206 - - class: SupplierFacility - configs: *id002 - name: Supplier1_206 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_206 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_206 - - class: SupplierFacility - configs: *id001 - name: Supplier3_207 - - class: SupplierFacility - configs: *id002 - name: Supplier1_207 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_207 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_207 - - class: SupplierFacility - configs: *id001 - name: Supplier3_208 - - class: SupplierFacility - configs: *id002 - name: Supplier1_208 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_208 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_208 - - class: SupplierFacility - configs: *id001 - name: Supplier3_209 - - class: SupplierFacility - configs: *id002 - name: Supplier1_209 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_209 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_209 - - class: SupplierFacility - configs: *id001 - name: Supplier3_210 - - class: SupplierFacility - configs: *id002 - name: Supplier1_210 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_210 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_210 - - class: SupplierFacility - configs: *id001 - name: Supplier3_211 - - class: SupplierFacility - configs: *id002 - name: Supplier1_211 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_211 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_211 - - class: SupplierFacility - configs: *id001 - name: Supplier3_212 - - class: SupplierFacility - configs: *id002 - name: Supplier1_212 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_212 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_212 - - class: SupplierFacility - configs: *id001 - name: Supplier3_213 - - class: SupplierFacility - configs: *id002 - name: Supplier1_213 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_213 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_213 - - class: SupplierFacility - configs: *id001 - name: Supplier3_214 - - class: SupplierFacility - configs: *id002 - name: Supplier1_214 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_214 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_214 - - class: SupplierFacility - configs: *id001 - name: Supplier3_215 - - class: SupplierFacility - configs: *id002 - name: Supplier1_215 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_215 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_215 - - class: SupplierFacility - configs: *id001 - name: Supplier3_216 - - class: SupplierFacility - configs: *id002 - name: Supplier1_216 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_216 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_216 - - class: SupplierFacility - configs: *id001 - name: Supplier3_217 - - class: SupplierFacility - configs: *id002 - name: Supplier1_217 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_217 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_217 - - class: SupplierFacility - configs: *id001 - name: Supplier3_218 - - class: SupplierFacility - configs: *id002 - name: Supplier1_218 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_218 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_218 - - class: SupplierFacility - configs: *id001 - name: Supplier3_219 - - class: SupplierFacility - configs: *id002 - name: Supplier1_219 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_219 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_219 - - class: SupplierFacility - configs: *id001 - name: Supplier3_220 - - class: SupplierFacility - configs: *id002 - name: Supplier1_220 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_220 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_220 - - class: SupplierFacility - configs: *id001 - name: Supplier3_221 - - class: SupplierFacility - configs: *id002 - name: Supplier1_221 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_221 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_221 - - class: SupplierFacility - configs: *id001 - name: Supplier3_222 - - class: SupplierFacility - configs: *id002 - name: Supplier1_222 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_222 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_222 - - class: SupplierFacility - configs: *id001 - name: Supplier3_223 - - class: SupplierFacility - configs: *id002 - name: Supplier1_223 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_223 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_223 - - class: SupplierFacility - configs: *id001 - name: Supplier3_224 - - class: SupplierFacility - configs: *id002 - name: Supplier1_224 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_224 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_224 - - class: SupplierFacility - configs: *id001 - name: Supplier3_225 - - class: SupplierFacility - configs: *id002 - name: Supplier1_225 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_225 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_225 - - class: SupplierFacility - configs: *id001 - name: Supplier3_226 - - class: SupplierFacility - configs: *id002 - name: Supplier1_226 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_226 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_226 - - class: SupplierFacility - configs: *id001 - name: Supplier3_227 - - class: SupplierFacility - configs: *id002 - name: Supplier1_227 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_227 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_227 - - class: SupplierFacility - configs: *id001 - name: Supplier3_228 - - class: SupplierFacility - configs: *id002 - name: Supplier1_228 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_228 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_228 - - class: SupplierFacility - configs: *id001 - name: Supplier3_229 - - class: SupplierFacility - configs: *id002 - name: Supplier1_229 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_229 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_229 - - class: SupplierFacility - configs: *id001 - name: Supplier3_230 - - class: SupplierFacility - configs: *id002 - name: Supplier1_230 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_230 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_230 - - class: SupplierFacility - configs: *id001 - name: Supplier3_231 - - class: SupplierFacility - configs: *id002 - name: Supplier1_231 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_231 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_231 - - class: SupplierFacility - configs: *id001 - name: Supplier3_232 - - class: SupplierFacility - configs: *id002 - name: Supplier1_232 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_232 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_232 - - class: SupplierFacility - configs: *id001 - name: Supplier3_233 - - class: SupplierFacility - configs: *id002 - name: Supplier1_233 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_233 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_233 - - class: SupplierFacility - configs: *id001 - name: Supplier3_234 - - class: SupplierFacility - configs: *id002 - name: Supplier1_234 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_234 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_234 - - class: SupplierFacility - configs: *id001 - name: Supplier3_235 - - class: SupplierFacility - configs: *id002 - name: Supplier1_235 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_235 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_235 - - class: SupplierFacility - configs: *id001 - name: Supplier3_236 - - class: SupplierFacility - configs: *id002 - name: Supplier1_236 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_236 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_236 - - class: SupplierFacility - configs: *id001 - name: Supplier3_237 - - class: SupplierFacility - configs: *id002 - name: Supplier1_237 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_237 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_237 - - class: SupplierFacility - configs: *id001 - name: Supplier3_238 - - class: SupplierFacility - configs: *id002 - name: Supplier1_238 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_238 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_238 - - class: SupplierFacility - configs: *id001 - name: Supplier3_239 - - class: SupplierFacility - configs: *id002 - name: Supplier1_239 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_239 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_239 - - class: SupplierFacility - configs: *id001 - name: Supplier3_240 - - class: SupplierFacility - configs: *id002 - name: Supplier1_240 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_240 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_240 - - class: SupplierFacility - configs: *id001 - name: Supplier3_241 - - class: SupplierFacility - configs: *id002 - name: Supplier1_241 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_241 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_241 - - class: SupplierFacility - configs: *id001 - name: Supplier3_242 - - class: SupplierFacility - configs: *id002 - name: Supplier1_242 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_242 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_242 - - class: SupplierFacility - configs: *id001 - name: Supplier3_243 - - class: SupplierFacility - configs: *id002 - name: Supplier1_243 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_243 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_243 - - class: SupplierFacility - configs: *id001 - name: Supplier3_244 - - class: SupplierFacility - configs: *id002 - name: Supplier1_244 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_244 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_244 - - class: SupplierFacility - configs: *id001 - name: Supplier3_245 - - class: SupplierFacility - configs: *id002 - name: Supplier1_245 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_245 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_245 - - class: SupplierFacility - configs: *id001 - name: Supplier3_246 - - class: SupplierFacility - configs: *id002 - name: Supplier1_246 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_246 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_246 - - class: SupplierFacility - configs: *id001 - name: Supplier3_247 - - class: SupplierFacility - configs: *id002 - name: Supplier1_247 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_247 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_247 - - class: SupplierFacility - configs: *id001 - name: Supplier3_248 - - class: SupplierFacility - configs: *id002 - name: Supplier1_248 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_248 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_248 - - class: SupplierFacility - configs: *id001 - name: Supplier3_249 - - class: SupplierFacility - configs: *id002 - name: Supplier1_249 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_249 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_249 - - class: SupplierFacility - configs: *id001 - name: Supplier3_250 - - class: SupplierFacility - configs: *id002 - name: Supplier1_250 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_250 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_250 - - class: SupplierFacility - configs: *id001 - name: Supplier3_251 - - class: SupplierFacility - configs: *id002 - name: Supplier1_251 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_251 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_251 - - class: SupplierFacility - configs: *id001 - name: Supplier3_252 - - class: SupplierFacility - configs: *id002 - name: Supplier1_252 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_252 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_252 - - class: SupplierFacility - configs: *id001 - name: Supplier3_253 - - class: SupplierFacility - configs: *id002 - name: Supplier1_253 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_253 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_253 - - class: SupplierFacility - configs: *id001 - name: Supplier3_254 - - class: SupplierFacility - configs: *id002 - name: Supplier1_254 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_254 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_254 - - class: SupplierFacility - configs: *id001 - name: Supplier3_255 - - class: SupplierFacility - configs: *id002 - name: Supplier1_255 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_255 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_255 - - class: SupplierFacility - configs: *id001 - name: Supplier3_256 - - class: SupplierFacility - configs: *id002 - name: Supplier1_256 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_256 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_256 - - class: SupplierFacility - configs: *id001 - name: Supplier3_257 - - class: SupplierFacility - configs: *id002 - name: Supplier1_257 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_257 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_257 - - class: SupplierFacility - configs: *id001 - name: Supplier3_258 - - class: SupplierFacility - configs: *id002 - name: Supplier1_258 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_258 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_258 - - class: SupplierFacility - configs: *id001 - name: Supplier3_259 - - class: SupplierFacility - configs: *id002 - name: Supplier1_259 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_259 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_259 - - class: SupplierFacility - configs: *id001 - name: Supplier3_260 - - class: SupplierFacility - configs: *id002 - name: Supplier1_260 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_260 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_260 - - class: SupplierFacility - configs: *id001 - name: Supplier3_261 - - class: SupplierFacility - configs: *id002 - name: Supplier1_261 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_261 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_261 - - class: SupplierFacility - configs: *id001 - name: Supplier3_262 - - class: SupplierFacility - configs: *id002 - name: Supplier1_262 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_262 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_262 - - class: SupplierFacility - configs: *id001 - name: Supplier3_263 - - class: SupplierFacility - configs: *id002 - name: Supplier1_263 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_263 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_263 - - class: SupplierFacility - configs: *id001 - name: Supplier3_264 - - class: SupplierFacility - configs: *id002 - name: Supplier1_264 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_264 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_264 - - class: SupplierFacility - configs: *id001 - name: Supplier3_265 - - class: SupplierFacility - configs: *id002 - name: Supplier1_265 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_265 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_265 - - class: SupplierFacility - configs: *id001 - name: Supplier3_266 - - class: SupplierFacility - configs: *id002 - name: Supplier1_266 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_266 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_266 - - class: SupplierFacility - configs: *id001 - name: Supplier3_267 - - class: SupplierFacility - configs: *id002 - name: Supplier1_267 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_267 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_267 - - class: SupplierFacility - configs: *id001 - name: Supplier3_268 - - class: SupplierFacility - configs: *id002 - name: Supplier1_268 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_268 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_268 - - class: SupplierFacility - configs: *id001 - name: Supplier3_269 - - class: SupplierFacility - configs: *id002 - name: Supplier1_269 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_269 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_269 - - class: SupplierFacility - configs: *id001 - name: Supplier3_270 - - class: SupplierFacility - configs: *id002 - name: Supplier1_270 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_270 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_270 - - class: SupplierFacility - configs: *id001 - name: Supplier3_271 - - class: SupplierFacility - configs: *id002 - name: Supplier1_271 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_271 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_271 - - class: SupplierFacility - configs: *id001 - name: Supplier3_272 - - class: SupplierFacility - configs: *id002 - name: Supplier1_272 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_272 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_272 - - class: SupplierFacility - configs: *id001 - name: Supplier3_273 - - class: SupplierFacility - configs: *id002 - name: Supplier1_273 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_273 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_273 - - class: SupplierFacility - configs: *id001 - name: Supplier3_274 - - class: SupplierFacility - configs: *id002 - name: Supplier1_274 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_274 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_274 - - class: SupplierFacility - configs: *id001 - name: Supplier3_275 - - class: SupplierFacility - configs: *id002 - name: Supplier1_275 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_275 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_275 - - class: SupplierFacility - configs: *id001 - name: Supplier3_276 - - class: SupplierFacility - configs: *id002 - name: Supplier1_276 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_276 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_276 - - class: SupplierFacility - configs: *id001 - name: Supplier3_277 - - class: SupplierFacility - configs: *id002 - name: Supplier1_277 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_277 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_277 - - class: SupplierFacility - configs: *id001 - name: Supplier3_278 - - class: SupplierFacility - configs: *id002 - name: Supplier1_278 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_278 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_278 - - class: SupplierFacility - configs: *id001 - name: Supplier3_279 - - class: SupplierFacility - configs: *id002 - name: Supplier1_279 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_279 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_279 - - class: SupplierFacility - configs: *id001 - name: Supplier3_280 - - class: SupplierFacility - configs: *id002 - name: Supplier1_280 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_280 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_280 - - class: SupplierFacility - configs: *id001 - name: Supplier3_281 - - class: SupplierFacility - configs: *id002 - name: Supplier1_281 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_281 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_281 - - class: SupplierFacility - configs: *id001 - name: Supplier3_282 - - class: SupplierFacility - configs: *id002 - name: Supplier1_282 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_282 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_282 - - class: SupplierFacility - configs: *id001 - name: Supplier3_283 - - class: SupplierFacility - configs: *id002 - name: Supplier1_283 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_283 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_283 - - class: SupplierFacility - configs: *id001 - name: Supplier3_284 - - class: SupplierFacility - configs: *id002 - name: Supplier1_284 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_284 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_284 - - class: SupplierFacility - configs: *id001 - name: Supplier3_285 - - class: SupplierFacility - configs: *id002 - name: Supplier1_285 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_285 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_285 - - class: SupplierFacility - configs: *id001 - name: Supplier3_286 - - class: SupplierFacility - configs: *id002 - name: Supplier1_286 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_286 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_286 - - class: SupplierFacility - configs: *id001 - name: Supplier3_287 - - class: SupplierFacility - configs: *id002 - name: Supplier1_287 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_287 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_287 - - class: SupplierFacility - configs: *id001 - name: Supplier3_288 - - class: SupplierFacility - configs: *id002 - name: Supplier1_288 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_288 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_288 - - class: SupplierFacility - configs: *id001 - name: Supplier3_289 - - class: SupplierFacility - configs: *id002 - name: Supplier1_289 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_289 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_289 - - class: SupplierFacility - configs: *id001 - name: Supplier3_290 - - class: SupplierFacility - configs: *id002 - name: Supplier1_290 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_290 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_290 - - class: SupplierFacility - configs: *id001 - name: Supplier3_291 - - class: SupplierFacility - configs: *id002 - name: Supplier1_291 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_291 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_291 - - class: SupplierFacility - configs: *id001 - name: Supplier3_292 - - class: SupplierFacility - configs: *id002 - name: Supplier1_292 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_292 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_292 - - class: SupplierFacility - configs: *id001 - name: Supplier3_293 - - class: SupplierFacility - configs: *id002 - name: Supplier1_293 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_293 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_293 - - class: SupplierFacility - configs: *id001 - name: Supplier3_294 - - class: SupplierFacility - configs: *id002 - name: Supplier1_294 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_294 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_294 - - class: SupplierFacility - configs: *id001 - name: Supplier3_295 - - class: SupplierFacility - configs: *id002 - name: Supplier1_295 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_295 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_295 - - class: SupplierFacility - configs: *id001 - name: Supplier3_296 - - class: SupplierFacility - configs: *id002 - name: Supplier1_296 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_296 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_296 - - class: SupplierFacility - configs: *id001 - name: Supplier3_297 - - class: SupplierFacility - configs: *id002 - name: Supplier1_297 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_297 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_297 - - class: SupplierFacility - configs: *id001 - name: Supplier3_298 - - class: SupplierFacility - configs: *id002 - name: Supplier1_298 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_298 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_298 - - class: SupplierFacility - configs: *id001 - name: Supplier3_299 - - class: SupplierFacility - configs: *id002 - name: Supplier1_299 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_299 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_299 - - class: SupplierFacility - configs: *id001 - name: Supplier3_300 - - class: SupplierFacility - configs: *id002 - name: Supplier1_300 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_300 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_300 - - class: SupplierFacility - configs: *id001 - name: Supplier3_301 - - class: SupplierFacility - configs: *id002 - name: Supplier1_301 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_301 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_301 - - class: SupplierFacility - configs: *id001 - name: Supplier3_302 - - class: SupplierFacility - configs: *id002 - name: Supplier1_302 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_302 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_302 - - class: SupplierFacility - configs: *id001 - name: Supplier3_303 - - class: SupplierFacility - configs: *id002 - name: Supplier1_303 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_303 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_303 - - class: SupplierFacility - configs: *id001 - name: Supplier3_304 - - class: SupplierFacility - configs: *id002 - name: Supplier1_304 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_304 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_304 - - class: SupplierFacility - configs: *id001 - name: Supplier3_305 - - class: SupplierFacility - configs: *id002 - name: Supplier1_305 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_305 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_305 - - class: SupplierFacility - configs: *id001 - name: Supplier3_306 - - class: SupplierFacility - configs: *id002 - name: Supplier1_306 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_306 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_306 - - class: SupplierFacility - configs: *id001 - name: Supplier3_307 - - class: SupplierFacility - configs: *id002 - name: Supplier1_307 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_307 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_307 - - class: SupplierFacility - configs: *id001 - name: Supplier3_308 - - class: SupplierFacility - configs: *id002 - name: Supplier1_308 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_308 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_308 - - class: SupplierFacility - configs: *id001 - name: Supplier3_309 - - class: SupplierFacility - configs: *id002 - name: Supplier1_309 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_309 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_309 - - class: SupplierFacility - configs: *id001 - name: Supplier3_310 - - class: SupplierFacility - configs: *id002 - name: Supplier1_310 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_310 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_310 - - class: SupplierFacility - configs: *id001 - name: Supplier3_311 - - class: SupplierFacility - configs: *id002 - name: Supplier1_311 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_311 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_311 - - class: SupplierFacility - configs: *id001 - name: Supplier3_312 - - class: SupplierFacility - configs: *id002 - name: Supplier1_312 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_312 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_312 - - class: SupplierFacility - configs: *id001 - name: Supplier3_313 - - class: SupplierFacility - configs: *id002 - name: Supplier1_313 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_313 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_313 - - class: SupplierFacility - configs: *id001 - name: Supplier3_314 - - class: SupplierFacility - configs: *id002 - name: Supplier1_314 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_314 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_314 - - class: SupplierFacility - configs: *id001 - name: Supplier3_315 - - class: SupplierFacility - configs: *id002 - name: Supplier1_315 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_315 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_315 - - class: SupplierFacility - configs: *id001 - name: Supplier3_316 - - class: SupplierFacility - configs: *id002 - name: Supplier1_316 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_316 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_316 - - class: SupplierFacility - configs: *id001 - name: Supplier3_317 - - class: SupplierFacility - configs: *id002 - name: Supplier1_317 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_317 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_317 - - class: SupplierFacility - configs: *id001 - name: Supplier3_318 - - class: SupplierFacility - configs: *id002 - name: Supplier1_318 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_318 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_318 - - class: SupplierFacility - configs: *id001 - name: Supplier3_319 - - class: SupplierFacility - configs: *id002 - name: Supplier1_319 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_319 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_319 - - class: SupplierFacility - configs: *id001 - name: Supplier3_320 - - class: SupplierFacility - configs: *id002 - name: Supplier1_320 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_320 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_320 - - class: SupplierFacility - configs: *id001 - name: Supplier3_321 - - class: SupplierFacility - configs: *id002 - name: Supplier1_321 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_321 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_321 - - class: SupplierFacility - configs: *id001 - name: Supplier3_322 - - class: SupplierFacility - configs: *id002 - name: Supplier1_322 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_322 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_322 - - class: SupplierFacility - configs: *id001 - name: Supplier3_323 - - class: SupplierFacility - configs: *id002 - name: Supplier1_323 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_323 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_323 - - class: SupplierFacility - configs: *id001 - name: Supplier3_324 - - class: SupplierFacility - configs: *id002 - name: Supplier1_324 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_324 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_324 - - class: SupplierFacility - configs: *id001 - name: Supplier3_325 - - class: SupplierFacility - configs: *id002 - name: Supplier1_325 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_325 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_325 - - class: SupplierFacility - configs: *id001 - name: Supplier3_326 - - class: SupplierFacility - configs: *id002 - name: Supplier1_326 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_326 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_326 - - class: SupplierFacility - configs: *id001 - name: Supplier3_327 - - class: SupplierFacility - configs: *id002 - name: Supplier1_327 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_327 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_327 - - class: SupplierFacility - configs: *id001 - name: Supplier3_328 - - class: SupplierFacility - configs: *id002 - name: Supplier1_328 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_328 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_328 - - class: SupplierFacility - configs: *id001 - name: Supplier3_329 - - class: SupplierFacility - configs: *id002 - name: Supplier1_329 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_329 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_329 - - class: SupplierFacility - configs: *id001 - name: Supplier3_330 - - class: SupplierFacility - configs: *id002 - name: Supplier1_330 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_330 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_330 - - class: SupplierFacility - configs: *id001 - name: Supplier3_331 - - class: SupplierFacility - configs: *id002 - name: Supplier1_331 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_331 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_331 - - class: SupplierFacility - configs: *id001 - name: Supplier3_332 - - class: SupplierFacility - configs: *id002 - name: Supplier1_332 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_332 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_332 - - class: SupplierFacility - configs: *id001 - name: Supplier3_333 - - class: SupplierFacility - configs: *id002 - name: Supplier1_333 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_333 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_333 - - class: SupplierFacility - configs: *id001 - name: Supplier3_334 - - class: SupplierFacility - configs: *id002 - name: Supplier1_334 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_334 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_334 - - class: SupplierFacility - configs: *id001 - name: Supplier3_335 - - class: SupplierFacility - configs: *id002 - name: Supplier1_335 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_335 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_335 - - class: SupplierFacility - configs: *id001 - name: Supplier3_336 - - class: SupplierFacility - configs: *id002 - name: Supplier1_336 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_336 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_336 - - class: SupplierFacility - configs: *id001 - name: Supplier3_337 - - class: SupplierFacility - configs: *id002 - name: Supplier1_337 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_337 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_337 - - class: SupplierFacility - configs: *id001 - name: Supplier3_338 - - class: SupplierFacility - configs: *id002 - name: Supplier1_338 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_338 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_338 - - class: SupplierFacility - configs: *id001 - name: Supplier3_339 - - class: SupplierFacility - configs: *id002 - name: Supplier1_339 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_339 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_339 - - class: SupplierFacility - configs: *id001 - name: Supplier3_340 - - class: SupplierFacility - configs: *id002 - name: Supplier1_340 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_340 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_340 - - class: SupplierFacility - configs: *id001 - name: Supplier3_341 - - class: SupplierFacility - configs: *id002 - name: Supplier1_341 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_341 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_341 - - class: SupplierFacility - configs: *id001 - name: Supplier3_342 - - class: SupplierFacility - configs: *id002 - name: Supplier1_342 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_342 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_342 - - class: SupplierFacility - configs: *id001 - name: Supplier3_343 - - class: SupplierFacility - configs: *id002 - name: Supplier1_343 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_343 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_343 - - class: SupplierFacility - configs: *id001 - name: Supplier3_344 - - class: SupplierFacility - configs: *id002 - name: Supplier1_344 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_344 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_344 - - class: SupplierFacility - configs: *id001 - name: Supplier3_345 - - class: SupplierFacility - configs: *id002 - name: Supplier1_345 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_345 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_345 - - class: SupplierFacility - configs: *id001 - name: Supplier3_346 - - class: SupplierFacility - configs: *id002 - name: Supplier1_346 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_346 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_346 - - class: SupplierFacility - configs: *id001 - name: Supplier3_347 - - class: SupplierFacility - configs: *id002 - name: Supplier1_347 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_347 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_347 - - class: SupplierFacility - configs: *id001 - name: Supplier3_348 - - class: SupplierFacility - configs: *id002 - name: Supplier1_348 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_348 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_348 - - class: SupplierFacility - configs: *id001 - name: Supplier3_349 - - class: SupplierFacility - configs: *id002 - name: Supplier1_349 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_349 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_349 - - class: SupplierFacility - configs: *id001 - name: Supplier3_350 - - class: SupplierFacility - configs: *id002 - name: Supplier1_350 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_350 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_350 - - class: SupplierFacility - configs: *id001 - name: Supplier3_351 - - class: SupplierFacility - configs: *id002 - name: Supplier1_351 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_351 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_351 - - class: SupplierFacility - configs: *id001 - name: Supplier3_352 - - class: SupplierFacility - configs: *id002 - name: Supplier1_352 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_352 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_352 - - class: SupplierFacility - configs: *id001 - name: Supplier3_353 - - class: SupplierFacility - configs: *id002 - name: Supplier1_353 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_353 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_353 - - class: SupplierFacility - configs: *id001 - name: Supplier3_354 - - class: SupplierFacility - configs: *id002 - name: Supplier1_354 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_354 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_354 - - class: SupplierFacility - configs: *id001 - name: Supplier3_355 - - class: SupplierFacility - configs: *id002 - name: Supplier1_355 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_355 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_355 - - class: SupplierFacility - configs: *id001 - name: Supplier3_356 - - class: SupplierFacility - configs: *id002 - name: Supplier1_356 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_356 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_356 - - class: SupplierFacility - configs: *id001 - name: Supplier3_357 - - class: SupplierFacility - configs: *id002 - name: Supplier1_357 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_357 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_357 - - class: SupplierFacility - configs: *id001 - name: Supplier3_358 - - class: SupplierFacility - configs: *id002 - name: Supplier1_358 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_358 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_358 - - class: SupplierFacility - configs: *id001 - name: Supplier3_359 - - class: SupplierFacility - configs: *id002 - name: Supplier1_359 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_359 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_359 - - class: SupplierFacility - configs: *id001 - name: Supplier3_360 - - class: SupplierFacility - configs: *id002 - name: Supplier1_360 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_360 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_360 - - class: SupplierFacility - configs: *id001 - name: Supplier3_361 - - class: SupplierFacility - configs: *id002 - name: Supplier1_361 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_361 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_361 - - class: SupplierFacility - configs: *id001 - name: Supplier3_362 - - class: SupplierFacility - configs: *id002 - name: Supplier1_362 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_362 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_362 - - class: SupplierFacility - configs: *id001 - name: Supplier3_363 - - class: SupplierFacility - configs: *id002 - name: Supplier1_363 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_363 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_363 - - class: SupplierFacility - configs: *id001 - name: Supplier3_364 - - class: SupplierFacility - configs: *id002 - name: Supplier1_364 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_364 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_364 - - class: SupplierFacility - configs: *id001 - name: Supplier3_365 - - class: SupplierFacility - configs: *id002 - name: Supplier1_365 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_365 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_365 - - class: SupplierFacility - configs: *id001 - name: Supplier3_366 - - class: SupplierFacility - configs: *id002 - name: Supplier1_366 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_366 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_366 - - class: SupplierFacility - configs: *id001 - name: Supplier3_367 - - class: SupplierFacility - configs: *id002 - name: Supplier1_367 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_367 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_367 - - class: SupplierFacility - configs: *id001 - name: Supplier3_368 - - class: SupplierFacility - configs: *id002 - name: Supplier1_368 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_368 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_368 - - class: SupplierFacility - configs: *id001 - name: Supplier3_369 - - class: SupplierFacility - configs: *id002 - name: Supplier1_369 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_369 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_369 - - class: SupplierFacility - configs: *id001 - name: Supplier3_370 - - class: SupplierFacility - configs: *id002 - name: Supplier1_370 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_370 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_370 - - class: SupplierFacility - configs: *id001 - name: Supplier3_371 - - class: SupplierFacility - configs: *id002 - name: Supplier1_371 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_371 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_371 - - class: SupplierFacility - configs: *id001 - name: Supplier3_372 - - class: SupplierFacility - configs: *id002 - name: Supplier1_372 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_372 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_372 - - class: SupplierFacility - configs: *id001 - name: Supplier3_373 - - class: SupplierFacility - configs: *id002 - name: Supplier1_373 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_373 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_373 - - class: SupplierFacility - configs: *id001 - name: Supplier3_374 - - class: SupplierFacility - configs: *id002 - name: Supplier1_374 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_374 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_374 - - class: SupplierFacility - configs: *id001 - name: Supplier3_375 - - class: SupplierFacility - configs: *id002 - name: Supplier1_375 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_375 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_375 - - class: SupplierFacility - configs: *id001 - name: Supplier3_376 - - class: SupplierFacility - configs: *id002 - name: Supplier1_376 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_376 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_376 - - class: SupplierFacility - configs: *id001 - name: Supplier3_377 - - class: SupplierFacility - configs: *id002 - name: Supplier1_377 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_377 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_377 - - class: SupplierFacility - configs: *id001 - name: Supplier3_378 - - class: SupplierFacility - configs: *id002 - name: Supplier1_378 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_378 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_378 - - class: SupplierFacility - configs: *id001 - name: Supplier3_379 - - class: SupplierFacility - configs: *id002 - name: Supplier1_379 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_379 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_379 - - class: SupplierFacility - configs: *id001 - name: Supplier3_380 - - class: SupplierFacility - configs: *id002 - name: Supplier1_380 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_380 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_380 - - class: SupplierFacility - configs: *id001 - name: Supplier3_381 - - class: SupplierFacility - configs: *id002 - name: Supplier1_381 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_381 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_381 - - class: SupplierFacility - configs: *id001 - name: Supplier3_382 - - class: SupplierFacility - configs: *id002 - name: Supplier1_382 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_382 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_382 - - class: SupplierFacility - configs: *id001 - name: Supplier3_383 - - class: SupplierFacility - configs: *id002 - name: Supplier1_383 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_383 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_383 - - class: SupplierFacility - configs: *id001 - name: Supplier3_384 - - class: SupplierFacility - configs: *id002 - name: Supplier1_384 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_384 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_384 - - class: SupplierFacility - configs: *id001 - name: Supplier3_385 - - class: SupplierFacility - configs: *id002 - name: Supplier1_385 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_385 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_385 - - class: SupplierFacility - configs: *id001 - name: Supplier3_386 - - class: SupplierFacility - configs: *id002 - name: Supplier1_386 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_386 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_386 - - class: SupplierFacility - configs: *id001 - name: Supplier3_387 - - class: SupplierFacility - configs: *id002 - name: Supplier1_387 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_387 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_387 - - class: SupplierFacility - configs: *id001 - name: Supplier3_388 - - class: SupplierFacility - configs: *id002 - name: Supplier1_388 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_388 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_388 - - class: SupplierFacility - configs: *id001 - name: Supplier3_389 - - class: SupplierFacility - configs: *id002 - name: Supplier1_389 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_389 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_389 - - class: SupplierFacility - configs: *id001 - name: Supplier3_390 - - class: SupplierFacility - configs: *id002 - name: Supplier1_390 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_390 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_390 - - class: SupplierFacility - configs: *id001 - name: Supplier3_391 - - class: SupplierFacility - configs: *id002 - name: Supplier1_391 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_391 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_391 - - class: SupplierFacility - configs: *id001 - name: Supplier3_392 - - class: SupplierFacility - configs: *id002 - name: Supplier1_392 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_392 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_392 - - class: SupplierFacility - configs: *id001 - name: Supplier3_393 - - class: SupplierFacility - configs: *id002 - name: Supplier1_393 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_393 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_393 - - class: SupplierFacility - configs: *id001 - name: Supplier3_394 - - class: SupplierFacility - configs: *id002 - name: Supplier1_394 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_394 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_394 - - class: SupplierFacility - configs: *id001 - name: Supplier3_395 - - class: SupplierFacility - configs: *id002 - name: Supplier1_395 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_395 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_395 - - class: SupplierFacility - configs: *id001 - name: Supplier3_396 - - class: SupplierFacility - configs: *id002 - name: Supplier1_396 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_396 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_396 - - class: SupplierFacility - configs: *id001 - name: Supplier3_397 - - class: SupplierFacility - configs: *id002 - name: Supplier1_397 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_397 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_397 - - class: SupplierFacility - configs: *id001 - name: Supplier3_398 - - class: SupplierFacility - configs: *id002 - name: Supplier1_398 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_398 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_398 - - class: SupplierFacility - configs: *id001 - name: Supplier3_399 - - class: SupplierFacility - configs: *id002 - name: Supplier1_399 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_399 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_399 - - class: SupplierFacility - configs: *id001 - name: Supplier3_400 - - class: SupplierFacility - configs: *id002 - name: Supplier1_400 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_400 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_400 - - class: SupplierFacility - configs: *id001 - name: Supplier3_401 - - class: SupplierFacility - configs: *id002 - name: Supplier1_401 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_401 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_401 - - class: SupplierFacility - configs: *id001 - name: Supplier3_402 - - class: SupplierFacility - configs: *id002 - name: Supplier1_402 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_402 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_402 - - class: SupplierFacility - configs: *id001 - name: Supplier3_403 - - class: SupplierFacility - configs: *id002 - name: Supplier1_403 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_403 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_403 - - class: SupplierFacility - configs: *id001 - name: Supplier3_404 - - class: SupplierFacility - configs: *id002 - name: Supplier1_404 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_404 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_404 - - class: SupplierFacility - configs: *id001 - name: Supplier3_405 - - class: SupplierFacility - configs: *id002 - name: Supplier1_405 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_405 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_405 - - class: SupplierFacility - configs: *id001 - name: Supplier3_406 - - class: SupplierFacility - configs: *id002 - name: Supplier1_406 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_406 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_406 - - class: SupplierFacility - configs: *id001 - name: Supplier3_407 - - class: SupplierFacility - configs: *id002 - name: Supplier1_407 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_407 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_407 - - class: SupplierFacility - configs: *id001 - name: Supplier3_408 - - class: SupplierFacility - configs: *id002 - name: Supplier1_408 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_408 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_408 - - class: SupplierFacility - configs: *id001 - name: Supplier3_409 - - class: SupplierFacility - configs: *id002 - name: Supplier1_409 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_409 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_409 - - class: SupplierFacility - configs: *id001 - name: Supplier3_410 - - class: SupplierFacility - configs: *id002 - name: Supplier1_410 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_410 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_410 - - class: SupplierFacility - configs: *id001 - name: Supplier3_411 - - class: SupplierFacility - configs: *id002 - name: Supplier1_411 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_411 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_411 - - class: SupplierFacility - configs: *id001 - name: Supplier3_412 - - class: SupplierFacility - configs: *id002 - name: Supplier1_412 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_412 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_412 - - class: SupplierFacility - configs: *id001 - name: Supplier3_413 - - class: SupplierFacility - configs: *id002 - name: Supplier1_413 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_413 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_413 - - class: SupplierFacility - configs: *id001 - name: Supplier3_414 - - class: SupplierFacility - configs: *id002 - name: Supplier1_414 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_414 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_414 - - class: SupplierFacility - configs: *id001 - name: Supplier3_415 - - class: SupplierFacility - configs: *id002 - name: Supplier1_415 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_415 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_415 - - class: SupplierFacility - configs: *id001 - name: Supplier3_416 - - class: SupplierFacility - configs: *id002 - name: Supplier1_416 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_416 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_416 - - class: SupplierFacility - configs: *id001 - name: Supplier3_417 - - class: SupplierFacility - configs: *id002 - name: Supplier1_417 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_417 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_417 - - class: SupplierFacility - configs: *id001 - name: Supplier3_418 - - class: SupplierFacility - configs: *id002 - name: Supplier1_418 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_418 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_418 - - class: SupplierFacility - configs: *id001 - name: Supplier3_419 - - class: SupplierFacility - configs: *id002 - name: Supplier1_419 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_419 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_419 - - class: SupplierFacility - configs: *id001 - name: Supplier3_420 - - class: SupplierFacility - configs: *id002 - name: Supplier1_420 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_420 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_420 - - class: SupplierFacility - configs: *id001 - name: Supplier3_421 - - class: SupplierFacility - configs: *id002 - name: Supplier1_421 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_421 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_421 - - class: SupplierFacility - configs: *id001 - name: Supplier3_422 - - class: SupplierFacility - configs: *id002 - name: Supplier1_422 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_422 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_422 - - class: SupplierFacility - configs: *id001 - name: Supplier3_423 - - class: SupplierFacility - configs: *id002 - name: Supplier1_423 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_423 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_423 - - class: SupplierFacility - configs: *id001 - name: Supplier3_424 - - class: SupplierFacility - configs: *id002 - name: Supplier1_424 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_424 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_424 - - class: SupplierFacility - configs: *id001 - name: Supplier3_425 - - class: SupplierFacility - configs: *id002 - name: Supplier1_425 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_425 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_425 - - class: SupplierFacility - configs: *id001 - name: Supplier3_426 - - class: SupplierFacility - configs: *id002 - name: Supplier1_426 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_426 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_426 - - class: SupplierFacility - configs: *id001 - name: Supplier3_427 - - class: SupplierFacility - configs: *id002 - name: Supplier1_427 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_427 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_427 - - class: SupplierFacility - configs: *id001 - name: Supplier3_428 - - class: SupplierFacility - configs: *id002 - name: Supplier1_428 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_428 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_428 - - class: SupplierFacility - configs: *id001 - name: Supplier3_429 - - class: SupplierFacility - configs: *id002 - name: Supplier1_429 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_429 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_429 - - class: SupplierFacility - configs: *id001 - name: Supplier3_430 - - class: SupplierFacility - configs: *id002 - name: Supplier1_430 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_430 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_430 - - class: SupplierFacility - configs: *id001 - name: Supplier3_431 - - class: SupplierFacility - configs: *id002 - name: Supplier1_431 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_431 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_431 - - class: SupplierFacility - configs: *id001 - name: Supplier3_432 - - class: SupplierFacility - configs: *id002 - name: Supplier1_432 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_432 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_432 - - class: SupplierFacility - configs: *id001 - name: Supplier3_433 - - class: SupplierFacility - configs: *id002 - name: Supplier1_433 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_433 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_433 - - class: SupplierFacility - configs: *id001 - name: Supplier3_434 - - class: SupplierFacility - configs: *id002 - name: Supplier1_434 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_434 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_434 - - class: SupplierFacility - configs: *id001 - name: Supplier3_435 - - class: SupplierFacility - configs: *id002 - name: Supplier1_435 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_435 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_435 - - class: SupplierFacility - configs: *id001 - name: Supplier3_436 - - class: SupplierFacility - configs: *id002 - name: Supplier1_436 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_436 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_436 - - class: SupplierFacility - configs: *id001 - name: Supplier3_437 - - class: SupplierFacility - configs: *id002 - name: Supplier1_437 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_437 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_437 - - class: SupplierFacility - configs: *id001 - name: Supplier3_438 - - class: SupplierFacility - configs: *id002 - name: Supplier1_438 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_438 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_438 - - class: SupplierFacility - configs: *id001 - name: Supplier3_439 - - class: SupplierFacility - configs: *id002 - name: Supplier1_439 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_439 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_439 - - class: SupplierFacility - configs: *id001 - name: Supplier3_440 - - class: SupplierFacility - configs: *id002 - name: Supplier1_440 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_440 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_440 - - class: SupplierFacility - configs: *id001 - name: Supplier3_441 - - class: SupplierFacility - configs: *id002 - name: Supplier1_441 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_441 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_441 - - class: SupplierFacility - configs: *id001 - name: Supplier3_442 - - class: SupplierFacility - configs: *id002 - name: Supplier1_442 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_442 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_442 - - class: SupplierFacility - configs: *id001 - name: Supplier3_443 - - class: SupplierFacility - configs: *id002 - name: Supplier1_443 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_443 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_443 - - class: SupplierFacility - configs: *id001 - name: Supplier3_444 - - class: SupplierFacility - configs: *id002 - name: Supplier1_444 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_444 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_444 - - class: SupplierFacility - configs: *id001 - name: Supplier3_445 - - class: SupplierFacility - configs: *id002 - name: Supplier1_445 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_445 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_445 - - class: SupplierFacility - configs: *id001 - name: Supplier3_446 - - class: SupplierFacility - configs: *id002 - name: Supplier1_446 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_446 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_446 - - class: SupplierFacility - configs: *id001 - name: Supplier3_447 - - class: SupplierFacility - configs: *id002 - name: Supplier1_447 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_447 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_447 - - class: SupplierFacility - configs: *id001 - name: Supplier3_448 - - class: SupplierFacility - configs: *id002 - name: Supplier1_448 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_448 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_448 - - class: SupplierFacility - configs: *id001 - name: Supplier3_449 - - class: SupplierFacility - configs: *id002 - name: Supplier1_449 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_449 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_449 - - class: SupplierFacility - configs: *id001 - name: Supplier3_450 - - class: SupplierFacility - configs: *id002 - name: Supplier1_450 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_450 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_450 - - class: SupplierFacility - configs: *id001 - name: Supplier3_451 - - class: SupplierFacility - configs: *id002 - name: Supplier1_451 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_451 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_451 - - class: SupplierFacility - configs: *id001 - name: Supplier3_452 - - class: SupplierFacility - configs: *id002 - name: Supplier1_452 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_452 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_452 - - class: SupplierFacility - configs: *id001 - name: Supplier3_453 - - class: SupplierFacility - configs: *id002 - name: Supplier1_453 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_453 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_453 - - class: SupplierFacility - configs: *id001 - name: Supplier3_454 - - class: SupplierFacility - configs: *id002 - name: Supplier1_454 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_454 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_454 - - class: SupplierFacility - configs: *id001 - name: Supplier3_455 - - class: SupplierFacility - configs: *id002 - name: Supplier1_455 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_455 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_455 - - class: SupplierFacility - configs: *id001 - name: Supplier3_456 - - class: SupplierFacility - configs: *id002 - name: Supplier1_456 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_456 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_456 - - class: SupplierFacility - configs: *id001 - name: Supplier3_457 - - class: SupplierFacility - configs: *id002 - name: Supplier1_457 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_457 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_457 - - class: SupplierFacility - configs: *id001 - name: Supplier3_458 - - class: SupplierFacility - configs: *id002 - name: Supplier1_458 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_458 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_458 - - class: SupplierFacility - configs: *id001 - name: Supplier3_459 - - class: SupplierFacility - configs: *id002 - name: Supplier1_459 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_459 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_459 - - class: SupplierFacility - configs: *id001 - name: Supplier3_460 - - class: SupplierFacility - configs: *id002 - name: Supplier1_460 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_460 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_460 - - class: SupplierFacility - configs: *id001 - name: Supplier3_461 - - class: SupplierFacility - configs: *id002 - name: Supplier1_461 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_461 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_461 - - class: SupplierFacility - configs: *id001 - name: Supplier3_462 - - class: SupplierFacility - configs: *id002 - name: Supplier1_462 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_462 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_462 - - class: SupplierFacility - configs: *id001 - name: Supplier3_463 - - class: SupplierFacility - configs: *id002 - name: Supplier1_463 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_463 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_463 - - class: SupplierFacility - configs: *id001 - name: Supplier3_464 - - class: SupplierFacility - configs: *id002 - name: Supplier1_464 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_464 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_464 - - class: SupplierFacility - configs: *id001 - name: Supplier3_465 - - class: SupplierFacility - configs: *id002 - name: Supplier1_465 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_465 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_465 - - class: SupplierFacility - configs: *id001 - name: Supplier3_466 - - class: SupplierFacility - configs: *id002 - name: Supplier1_466 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_466 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_466 - - class: SupplierFacility - configs: *id001 - name: Supplier3_467 - - class: SupplierFacility - configs: *id002 - name: Supplier1_467 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_467 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_467 - - class: SupplierFacility - configs: *id001 - name: Supplier3_468 - - class: SupplierFacility - configs: *id002 - name: Supplier1_468 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_468 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_468 - - class: SupplierFacility - configs: *id001 - name: Supplier3_469 - - class: SupplierFacility - configs: *id002 - name: Supplier1_469 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_469 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_469 - - class: SupplierFacility - configs: *id001 - name: Supplier3_470 - - class: SupplierFacility - configs: *id002 - name: Supplier1_470 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_470 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_470 - - class: SupplierFacility - configs: *id001 - name: Supplier3_471 - - class: SupplierFacility - configs: *id002 - name: Supplier1_471 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_471 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_471 - - class: SupplierFacility - configs: *id001 - name: Supplier3_472 - - class: SupplierFacility - configs: *id002 - name: Supplier1_472 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_472 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_472 - - class: SupplierFacility - configs: *id001 - name: Supplier3_473 - - class: SupplierFacility - configs: *id002 - name: Supplier1_473 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_473 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_473 - - class: SupplierFacility - configs: *id001 - name: Supplier3_474 - - class: SupplierFacility - configs: *id002 - name: Supplier1_474 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_474 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_474 - - class: SupplierFacility - configs: *id001 - name: Supplier3_475 - - class: SupplierFacility - configs: *id002 - name: Supplier1_475 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_475 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_475 - - class: SupplierFacility - configs: *id001 - name: Supplier3_476 - - class: SupplierFacility - configs: *id002 - name: Supplier1_476 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_476 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_476 - - class: SupplierFacility - configs: *id001 - name: Supplier3_477 - - class: SupplierFacility - configs: *id002 - name: Supplier1_477 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_477 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_477 - - class: SupplierFacility - configs: *id001 - name: Supplier3_478 - - class: SupplierFacility - configs: *id002 - name: Supplier1_478 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_478 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_478 - - class: SupplierFacility - configs: *id001 - name: Supplier3_479 - - class: SupplierFacility - configs: *id002 - name: Supplier1_479 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_479 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_479 - - class: SupplierFacility - configs: *id001 - name: Supplier3_480 - - class: SupplierFacility - configs: *id002 - name: Supplier1_480 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_480 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_480 - - class: SupplierFacility - configs: *id001 - name: Supplier3_481 - - class: SupplierFacility - configs: *id002 - name: Supplier1_481 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_481 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_481 - - class: SupplierFacility - configs: *id001 - name: Supplier3_482 - - class: SupplierFacility - configs: *id002 - name: Supplier1_482 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_482 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_482 - - class: SupplierFacility - configs: *id001 - name: Supplier3_483 - - class: SupplierFacility - configs: *id002 - name: Supplier1_483 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_483 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_483 - - class: SupplierFacility - configs: *id001 - name: Supplier3_484 - - class: SupplierFacility - configs: *id002 - name: Supplier1_484 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_484 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_484 - - class: SupplierFacility - configs: *id001 - name: Supplier3_485 - - class: SupplierFacility - configs: *id002 - name: Supplier1_485 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_485 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_485 - - class: SupplierFacility - configs: *id001 - name: Supplier3_486 - - class: SupplierFacility - configs: *id002 - name: Supplier1_486 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_486 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_486 - - class: SupplierFacility - configs: *id001 - name: Supplier3_487 - - class: SupplierFacility - configs: *id002 - name: Supplier1_487 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_487 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_487 - - class: SupplierFacility - configs: *id001 - name: Supplier3_488 - - class: SupplierFacility - configs: *id002 - name: Supplier1_488 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_488 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_488 - - class: SupplierFacility - configs: *id001 - name: Supplier3_489 - - class: SupplierFacility - configs: *id002 - name: Supplier1_489 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_489 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_489 - - class: SupplierFacility - configs: *id001 - name: Supplier3_490 - - class: SupplierFacility - configs: *id002 - name: Supplier1_490 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_490 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_490 - - class: SupplierFacility - configs: *id001 - name: Supplier3_491 - - class: SupplierFacility - configs: *id002 - name: Supplier1_491 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_491 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_491 - - class: SupplierFacility - configs: *id001 - name: Supplier3_492 - - class: SupplierFacility - configs: *id002 - name: Supplier1_492 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_492 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_492 - - class: SupplierFacility - configs: *id001 - name: Supplier3_493 - - class: SupplierFacility - configs: *id002 - name: Supplier1_493 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_493 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_493 - - class: SupplierFacility - configs: *id001 - name: Supplier3_494 - - class: SupplierFacility - configs: *id002 - name: Supplier1_494 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_494 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_494 - - class: SupplierFacility - configs: *id001 - name: Supplier3_495 - - class: SupplierFacility - configs: *id002 - name: Supplier1_495 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_495 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_495 - - class: SupplierFacility - configs: *id001 - name: Supplier3_496 - - class: SupplierFacility - configs: *id002 - name: Supplier1_496 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_496 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_496 - - class: SupplierFacility - configs: *id001 - name: Supplier3_497 - - class: SupplierFacility - configs: *id002 - name: Supplier1_497 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_497 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_497 - - class: SupplierFacility - configs: *id001 - name: Supplier3_498 - - class: SupplierFacility - configs: *id002 - name: Supplier1_498 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_498 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_498 - - class: SupplierFacility - configs: *id001 - name: Supplier3_499 - - class: SupplierFacility - configs: *id002 - name: Supplier1_499 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_499 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_499 - - class: SupplierFacility - configs: *id001 - name: Supplier3_500 - - class: SupplierFacility - configs: *id002 - name: Supplier1_500 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_500 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_500 - - class: SupplierFacility - configs: *id001 - name: Supplier3_501 - - class: SupplierFacility - configs: *id002 - name: Supplier1_501 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_501 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_501 - - class: SupplierFacility - configs: *id001 - name: Supplier3_502 - - class: SupplierFacility - configs: *id002 - name: Supplier1_502 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_502 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_502 - - class: SupplierFacility - configs: *id001 - name: Supplier3_503 - - class: SupplierFacility - configs: *id002 - name: Supplier1_503 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_503 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_503 - - class: SupplierFacility - configs: *id001 - name: Supplier3_504 - - class: SupplierFacility - configs: *id002 - name: Supplier1_504 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_504 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_504 - - class: SupplierFacility - configs: *id001 - name: Supplier3_505 - - class: SupplierFacility - configs: *id002 - name: Supplier1_505 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_505 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_505 - - class: SupplierFacility - configs: *id001 - name: Supplier3_506 - - class: SupplierFacility - configs: *id002 - name: Supplier1_506 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_506 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_506 - - class: SupplierFacility - configs: *id001 - name: Supplier3_507 - - class: SupplierFacility - configs: *id002 - name: Supplier1_507 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_507 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_507 - - class: SupplierFacility - configs: *id001 - name: Supplier3_508 - - class: SupplierFacility - configs: *id002 - name: Supplier1_508 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_508 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_508 - - class: SupplierFacility - configs: *id001 - name: Supplier3_509 - - class: SupplierFacility - configs: *id002 - name: Supplier1_509 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_509 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_509 - - class: SupplierFacility - configs: *id001 - name: Supplier3_510 - - class: SupplierFacility - configs: *id002 - name: Supplier1_510 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_510 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_510 - - class: SupplierFacility - configs: *id001 - name: Supplier3_511 - - class: SupplierFacility - configs: *id002 - name: Supplier1_511 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_511 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_511 - - class: SupplierFacility - configs: *id001 - name: Supplier3_512 - - class: SupplierFacility - configs: *id002 - name: Supplier1_512 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_512 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_512 - - class: SupplierFacility - configs: *id001 - name: Supplier3_513 - - class: SupplierFacility - configs: *id002 - name: Supplier1_513 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_513 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_513 - - class: SupplierFacility - configs: *id001 - name: Supplier3_514 - - class: SupplierFacility - configs: *id002 - name: Supplier1_514 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_514 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_514 - - class: SupplierFacility - configs: *id001 - name: Supplier3_515 - - class: SupplierFacility - configs: *id002 - name: Supplier1_515 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_515 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_515 - - class: SupplierFacility - configs: *id001 - name: Supplier3_516 - - class: SupplierFacility - configs: *id002 - name: Supplier1_516 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_516 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_516 - - class: SupplierFacility - configs: *id001 - name: Supplier3_517 - - class: SupplierFacility - configs: *id002 - name: Supplier1_517 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_517 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_517 - - class: SupplierFacility - configs: *id001 - name: Supplier3_518 - - class: SupplierFacility - configs: *id002 - name: Supplier1_518 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_518 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_518 - - class: SupplierFacility - configs: *id001 - name: Supplier3_519 - - class: SupplierFacility - configs: *id002 - name: Supplier1_519 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_519 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_519 - - class: SupplierFacility - configs: *id001 - name: Supplier3_520 - - class: SupplierFacility - configs: *id002 - name: Supplier1_520 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_520 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_520 - - class: SupplierFacility - configs: *id001 - name: Supplier3_521 - - class: SupplierFacility - configs: *id002 - name: Supplier1_521 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_521 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_521 - - class: SupplierFacility - configs: *id001 - name: Supplier3_522 - - class: SupplierFacility - configs: *id002 - name: Supplier1_522 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_522 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_522 - - class: SupplierFacility - configs: *id001 - name: Supplier3_523 - - class: SupplierFacility - configs: *id002 - name: Supplier1_523 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_523 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_523 - - class: SupplierFacility - configs: *id001 - name: Supplier3_524 - - class: SupplierFacility - configs: *id002 - name: Supplier1_524 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_524 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_524 - - class: SupplierFacility - configs: *id001 - name: Supplier3_525 - - class: SupplierFacility - configs: *id002 - name: Supplier1_525 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_525 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_525 - - class: SupplierFacility - configs: *id001 - name: Supplier3_526 - - class: SupplierFacility - configs: *id002 - name: Supplier1_526 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_526 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_526 - - class: SupplierFacility - configs: *id001 - name: Supplier3_527 - - class: SupplierFacility - configs: *id002 - name: Supplier1_527 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_527 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_527 - - class: SupplierFacility - configs: *id001 - name: Supplier3_528 - - class: SupplierFacility - configs: *id002 - name: Supplier1_528 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_528 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_528 - - class: SupplierFacility - configs: *id001 - name: Supplier3_529 - - class: SupplierFacility - configs: *id002 - name: Supplier1_529 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_529 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_529 - - class: SupplierFacility - configs: *id001 - name: Supplier3_530 - - class: SupplierFacility - configs: *id002 - name: Supplier1_530 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_530 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_530 - - class: SupplierFacility - configs: *id001 - name: Supplier3_531 - - class: SupplierFacility - configs: *id002 - name: Supplier1_531 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_531 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_531 - - class: SupplierFacility - configs: *id001 - name: Supplier3_532 - - class: SupplierFacility - configs: *id002 - name: Supplier1_532 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_532 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_532 - - class: SupplierFacility - configs: *id001 - name: Supplier3_533 - - class: SupplierFacility - configs: *id002 - name: Supplier1_533 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_533 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_533 - - class: SupplierFacility - configs: *id001 - name: Supplier3_534 - - class: SupplierFacility - configs: *id002 - name: Supplier1_534 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_534 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_534 - - class: SupplierFacility - configs: *id001 - name: Supplier3_535 - - class: SupplierFacility - configs: *id002 - name: Supplier1_535 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_535 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_535 - - class: SupplierFacility - configs: *id001 - name: Supplier3_536 - - class: SupplierFacility - configs: *id002 - name: Supplier1_536 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_536 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_536 - - class: SupplierFacility - configs: *id001 - name: Supplier3_537 - - class: SupplierFacility - configs: *id002 - name: Supplier1_537 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_537 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_537 - - class: SupplierFacility - configs: *id001 - name: Supplier3_538 - - class: SupplierFacility - configs: *id002 - name: Supplier1_538 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_538 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_538 - - class: SupplierFacility - configs: *id001 - name: Supplier3_539 - - class: SupplierFacility - configs: *id002 - name: Supplier1_539 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_539 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_539 - - class: SupplierFacility - configs: *id001 - name: Supplier3_540 - - class: SupplierFacility - configs: *id002 - name: Supplier1_540 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_540 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_540 - - class: SupplierFacility - configs: *id001 - name: Supplier3_541 - - class: SupplierFacility - configs: *id002 - name: Supplier1_541 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_541 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_541 - - class: SupplierFacility - configs: *id001 - name: Supplier3_542 - - class: SupplierFacility - configs: *id002 - name: Supplier1_542 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_542 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_542 - - class: SupplierFacility - configs: *id001 - name: Supplier3_543 - - class: SupplierFacility - configs: *id002 - name: Supplier1_543 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_543 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_543 - - class: SupplierFacility - configs: *id001 - name: Supplier3_544 - - class: SupplierFacility - configs: *id002 - name: Supplier1_544 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_544 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_544 - - class: SupplierFacility - configs: *id001 - name: Supplier3_545 - - class: SupplierFacility - configs: *id002 - name: Supplier1_545 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_545 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_545 - - class: SupplierFacility - configs: *id001 - name: Supplier3_546 - - class: SupplierFacility - configs: *id002 - name: Supplier1_546 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_546 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_546 - - class: SupplierFacility - configs: *id001 - name: Supplier3_547 - - class: SupplierFacility - configs: *id002 - name: Supplier1_547 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_547 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_547 - - class: SupplierFacility - configs: *id001 - name: Supplier3_548 - - class: SupplierFacility - configs: *id002 - name: Supplier1_548 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_548 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_548 - - class: SupplierFacility - configs: *id001 - name: Supplier3_549 - - class: SupplierFacility - configs: *id002 - name: Supplier1_549 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_549 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_549 - - class: SupplierFacility - configs: *id001 - name: Supplier3_550 - - class: SupplierFacility - configs: *id002 - name: Supplier1_550 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_550 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_550 - - class: SupplierFacility - configs: *id001 - name: Supplier3_551 - - class: SupplierFacility - configs: *id002 - name: Supplier1_551 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_551 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_551 - - class: SupplierFacility - configs: *id001 - name: Supplier3_552 - - class: SupplierFacility - configs: *id002 - name: Supplier1_552 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_552 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_552 - - class: SupplierFacility - configs: *id001 - name: Supplier3_553 - - class: SupplierFacility - configs: *id002 - name: Supplier1_553 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_553 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_553 - - class: SupplierFacility - configs: *id001 - name: Supplier3_554 - - class: SupplierFacility - configs: *id002 - name: Supplier1_554 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_554 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_554 - - class: SupplierFacility - configs: *id001 - name: Supplier3_555 - - class: SupplierFacility - configs: *id002 - name: Supplier1_555 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_555 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_555 - - class: SupplierFacility - configs: *id001 - name: Supplier3_556 - - class: SupplierFacility - configs: *id002 - name: Supplier1_556 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_556 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_556 - - class: SupplierFacility - configs: *id001 - name: Supplier3_557 - - class: SupplierFacility - configs: *id002 - name: Supplier1_557 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_557 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_557 - - class: SupplierFacility - configs: *id001 - name: Supplier3_558 - - class: SupplierFacility - configs: *id002 - name: Supplier1_558 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_558 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_558 - - class: SupplierFacility - configs: *id001 - name: Supplier3_559 - - class: SupplierFacility - configs: *id002 - name: Supplier1_559 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_559 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_559 - - class: SupplierFacility - configs: *id001 - name: Supplier3_560 - - class: SupplierFacility - configs: *id002 - name: Supplier1_560 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_560 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_560 - - class: SupplierFacility - configs: *id001 - name: Supplier3_561 - - class: SupplierFacility - configs: *id002 - name: Supplier1_561 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_561 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_561 - - class: SupplierFacility - configs: *id001 - name: Supplier3_562 - - class: SupplierFacility - configs: *id002 - name: Supplier1_562 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_562 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_562 - - class: SupplierFacility - configs: *id001 - name: Supplier3_563 - - class: SupplierFacility - configs: *id002 - name: Supplier1_563 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_563 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_563 - - class: SupplierFacility - configs: *id001 - name: Supplier3_564 - - class: SupplierFacility - configs: *id002 - name: Supplier1_564 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_564 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_564 - - class: SupplierFacility - configs: *id001 - name: Supplier3_565 - - class: SupplierFacility - configs: *id002 - name: Supplier1_565 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_565 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_565 - - class: SupplierFacility - configs: *id001 - name: Supplier3_566 - - class: SupplierFacility - configs: *id002 - name: Supplier1_566 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_566 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_566 - - class: SupplierFacility - configs: *id001 - name: Supplier3_567 - - class: SupplierFacility - configs: *id002 - name: Supplier1_567 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_567 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_567 - - class: SupplierFacility - configs: *id001 - name: Supplier3_568 - - class: SupplierFacility - configs: *id002 - name: Supplier1_568 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_568 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_568 - - class: SupplierFacility - configs: *id001 - name: Supplier3_569 - - class: SupplierFacility - configs: *id002 - name: Supplier1_569 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_569 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_569 - - class: SupplierFacility - configs: *id001 - name: Supplier3_570 - - class: SupplierFacility - configs: *id002 - name: Supplier1_570 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_570 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_570 - - class: SupplierFacility - configs: *id001 - name: Supplier3_571 - - class: SupplierFacility - configs: *id002 - name: Supplier1_571 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_571 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_571 - - class: SupplierFacility - configs: *id001 - name: Supplier3_572 - - class: SupplierFacility - configs: *id002 - name: Supplier1_572 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_572 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_572 - - class: SupplierFacility - configs: *id001 - name: Supplier3_573 - - class: SupplierFacility - configs: *id002 - name: Supplier1_573 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_573 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_573 - - class: SupplierFacility - configs: *id001 - name: Supplier3_574 - - class: SupplierFacility - configs: *id002 - name: Supplier1_574 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_574 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_574 - - class: SupplierFacility - configs: *id001 - name: Supplier3_575 - - class: SupplierFacility - configs: *id002 - name: Supplier1_575 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_575 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_575 - - class: SupplierFacility - configs: *id001 - name: Supplier3_576 - - class: SupplierFacility - configs: *id002 - name: Supplier1_576 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_576 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_576 - - class: SupplierFacility - configs: *id001 - name: Supplier3_577 - - class: SupplierFacility - configs: *id002 - name: Supplier1_577 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_577 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_577 - - class: SupplierFacility - configs: *id001 - name: Supplier3_578 - - class: SupplierFacility - configs: *id002 - name: Supplier1_578 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_578 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_578 - - class: SupplierFacility - configs: *id001 - name: Supplier3_579 - - class: SupplierFacility - configs: *id002 - name: Supplier1_579 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_579 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_579 - - class: SupplierFacility - configs: *id001 - name: Supplier3_580 - - class: SupplierFacility - configs: *id002 - name: Supplier1_580 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_580 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_580 - - class: SupplierFacility - configs: *id001 - name: Supplier3_581 - - class: SupplierFacility - configs: *id002 - name: Supplier1_581 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_581 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_581 - - class: SupplierFacility - configs: *id001 - name: Supplier3_582 - - class: SupplierFacility - configs: *id002 - name: Supplier1_582 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_582 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_582 - - class: SupplierFacility - configs: *id001 - name: Supplier3_583 - - class: SupplierFacility - configs: *id002 - name: Supplier1_583 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_583 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_583 - - class: SupplierFacility - configs: *id001 - name: Supplier3_584 - - class: SupplierFacility - configs: *id002 - name: Supplier1_584 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_584 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_584 - - class: SupplierFacility - configs: *id001 - name: Supplier3_585 - - class: SupplierFacility - configs: *id002 - name: Supplier1_585 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_585 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_585 - - class: SupplierFacility - configs: *id001 - name: Supplier3_586 - - class: SupplierFacility - configs: *id002 - name: Supplier1_586 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_586 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_586 - - class: SupplierFacility - configs: *id001 - name: Supplier3_587 - - class: SupplierFacility - configs: *id002 - name: Supplier1_587 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_587 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_587 - - class: SupplierFacility - configs: *id001 - name: Supplier3_588 - - class: SupplierFacility - configs: *id002 - name: Supplier1_588 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_588 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_588 - - class: SupplierFacility - configs: *id001 - name: Supplier3_589 - - class: SupplierFacility - configs: *id002 - name: Supplier1_589 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_589 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_589 - - class: SupplierFacility - configs: *id001 - name: Supplier3_590 - - class: SupplierFacility - configs: *id002 - name: Supplier1_590 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_590 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_590 - - class: SupplierFacility - configs: *id001 - name: Supplier3_591 - - class: SupplierFacility - configs: *id002 - name: Supplier1_591 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_591 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_591 - - class: SupplierFacility - configs: *id001 - name: Supplier3_592 - - class: SupplierFacility - configs: *id002 - name: Supplier1_592 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_592 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_592 - - class: SupplierFacility - configs: *id001 - name: Supplier3_593 - - class: SupplierFacility - configs: *id002 - name: Supplier1_593 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_593 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_593 - - class: SupplierFacility - configs: *id001 - name: Supplier3_594 - - class: SupplierFacility - configs: *id002 - name: Supplier1_594 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_594 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_594 - - class: SupplierFacility - configs: *id001 - name: Supplier3_595 - - class: SupplierFacility - configs: *id002 - name: Supplier1_595 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_595 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_595 - - class: SupplierFacility - configs: *id001 - name: Supplier3_596 - - class: SupplierFacility - configs: *id002 - name: Supplier1_596 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_596 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_596 - - class: SupplierFacility - configs: *id001 - name: Supplier3_597 - - class: SupplierFacility - configs: *id002 - name: Supplier1_597 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_597 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_597 - - class: SupplierFacility - configs: *id001 - name: Supplier3_598 - - class: SupplierFacility - configs: *id002 - name: Supplier1_598 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_598 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_598 - - class: SupplierFacility - configs: *id001 - name: Supplier3_599 - - class: SupplierFacility - configs: *id002 - name: Supplier1_599 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_599 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_599 - - class: SupplierFacility - configs: *id001 - name: Supplier3_600 - - class: SupplierFacility - configs: *id002 - name: Supplier1_600 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_600 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_600 - - class: SupplierFacility - configs: *id001 - name: Supplier3_601 - - class: SupplierFacility - configs: *id002 - name: Supplier1_601 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_601 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_601 - - class: SupplierFacility - configs: *id001 - name: Supplier3_602 - - class: SupplierFacility - configs: *id002 - name: Supplier1_602 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_602 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_602 - - class: SupplierFacility - configs: *id001 - name: Supplier3_603 - - class: SupplierFacility - configs: *id002 - name: Supplier1_603 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_603 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_603 - - class: SupplierFacility - configs: *id001 - name: Supplier3_604 - - class: SupplierFacility - configs: *id002 - name: Supplier1_604 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_604 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_604 - - class: SupplierFacility - configs: *id001 - name: Supplier3_605 - - class: SupplierFacility - configs: *id002 - name: Supplier1_605 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_605 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_605 - - class: SupplierFacility - configs: *id001 - name: Supplier3_606 - - class: SupplierFacility - configs: *id002 - name: Supplier1_606 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_606 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_606 - - class: SupplierFacility - configs: *id001 - name: Supplier3_607 - - class: SupplierFacility - configs: *id002 - name: Supplier1_607 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_607 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_607 - - class: SupplierFacility - configs: *id001 - name: Supplier3_608 - - class: SupplierFacility - configs: *id002 - name: Supplier1_608 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_608 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_608 - - class: SupplierFacility - configs: *id001 - name: Supplier3_609 - - class: SupplierFacility - configs: *id002 - name: Supplier1_609 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_609 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_609 - - class: SupplierFacility - configs: *id001 - name: Supplier3_610 - - class: SupplierFacility - configs: *id002 - name: Supplier1_610 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_610 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_610 - - class: SupplierFacility - configs: *id001 - name: Supplier3_611 - - class: SupplierFacility - configs: *id002 - name: Supplier1_611 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_611 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_611 - - class: SupplierFacility - configs: *id001 - name: Supplier3_612 - - class: SupplierFacility - configs: *id002 - name: Supplier1_612 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_612 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_612 - - class: SupplierFacility - configs: *id001 - name: Supplier3_613 - - class: SupplierFacility - configs: *id002 - name: Supplier1_613 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_613 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_613 - - class: SupplierFacility - configs: *id001 - name: Supplier3_614 - - class: SupplierFacility - configs: *id002 - name: Supplier1_614 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_614 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_614 - - class: SupplierFacility - configs: *id001 - name: Supplier3_615 - - class: SupplierFacility - configs: *id002 - name: Supplier1_615 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_615 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_615 - - class: SupplierFacility - configs: *id001 - name: Supplier3_616 - - class: SupplierFacility - configs: *id002 - name: Supplier1_616 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_616 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_616 - - class: SupplierFacility - configs: *id001 - name: Supplier3_617 - - class: SupplierFacility - configs: *id002 - name: Supplier1_617 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_617 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_617 - - class: SupplierFacility - configs: *id001 - name: Supplier3_618 - - class: SupplierFacility - configs: *id002 - name: Supplier1_618 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_618 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_618 - - class: SupplierFacility - configs: *id001 - name: Supplier3_619 - - class: SupplierFacility - configs: *id002 - name: Supplier1_619 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_619 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_619 - - class: SupplierFacility - configs: *id001 - name: Supplier3_620 - - class: SupplierFacility - configs: *id002 - name: Supplier1_620 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_620 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_620 - - class: SupplierFacility - configs: *id001 - name: Supplier3_621 - - class: SupplierFacility - configs: *id002 - name: Supplier1_621 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_621 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_621 - - class: SupplierFacility - configs: *id001 - name: Supplier3_622 - - class: SupplierFacility - configs: *id002 - name: Supplier1_622 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_622 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_622 - - class: SupplierFacility - configs: *id001 - name: Supplier3_623 - - class: SupplierFacility - configs: *id002 - name: Supplier1_623 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_623 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_623 - - class: SupplierFacility - configs: *id001 - name: Supplier3_624 - - class: SupplierFacility - configs: *id002 - name: Supplier1_624 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_624 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_624 - - class: SupplierFacility - configs: *id001 - name: Supplier3_625 - - class: SupplierFacility - configs: *id002 - name: Supplier1_625 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_625 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_625 - - class: SupplierFacility - configs: *id001 - name: Supplier3_626 - - class: SupplierFacility - configs: *id002 - name: Supplier1_626 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_626 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_626 - - class: SupplierFacility - configs: *id001 - name: Supplier3_627 - - class: SupplierFacility - configs: *id002 - name: Supplier1_627 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_627 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_627 - - class: SupplierFacility - configs: *id001 - name: Supplier3_628 - - class: SupplierFacility - configs: *id002 - name: Supplier1_628 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_628 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_628 - - class: SupplierFacility - configs: *id001 - name: Supplier3_629 - - class: SupplierFacility - configs: *id002 - name: Supplier1_629 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_629 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_629 - - class: SupplierFacility - configs: *id001 - name: Supplier3_630 - - class: SupplierFacility - configs: *id002 - name: Supplier1_630 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_630 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_630 - - class: SupplierFacility - configs: *id001 - name: Supplier3_631 - - class: SupplierFacility - configs: *id002 - name: Supplier1_631 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_631 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_631 - - class: SupplierFacility - configs: *id001 - name: Supplier3_632 - - class: SupplierFacility - configs: *id002 - name: Supplier1_632 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_632 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_632 - - class: SupplierFacility - configs: *id001 - name: Supplier3_633 - - class: SupplierFacility - configs: *id002 - name: Supplier1_633 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_633 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_633 - - class: SupplierFacility - configs: *id001 - name: Supplier3_634 - - class: SupplierFacility - configs: *id002 - name: Supplier1_634 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_634 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_634 - - class: SupplierFacility - configs: *id001 - name: Supplier3_635 - - class: SupplierFacility - configs: *id002 - name: Supplier1_635 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_635 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_635 - - class: SupplierFacility - configs: *id001 - name: Supplier3_636 - - class: SupplierFacility - configs: *id002 - name: Supplier1_636 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_636 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_636 - - class: SupplierFacility - configs: *id001 - name: Supplier3_637 - - class: SupplierFacility - configs: *id002 - name: Supplier1_637 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_637 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_637 - - class: SupplierFacility - configs: *id001 - name: Supplier3_638 - - class: SupplierFacility - configs: *id002 - name: Supplier1_638 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_638 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_638 - - class: SupplierFacility - configs: *id001 - name: Supplier3_639 - - class: SupplierFacility - configs: *id002 - name: Supplier1_639 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_639 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_639 - - class: SupplierFacility - configs: *id001 - name: Supplier3_640 - - class: SupplierFacility - configs: *id002 - name: Supplier1_640 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_640 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_640 - - class: SupplierFacility - configs: *id001 - name: Supplier3_641 - - class: SupplierFacility - configs: *id002 - name: Supplier1_641 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_641 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_641 - - class: SupplierFacility - configs: *id001 - name: Supplier3_642 - - class: SupplierFacility - configs: *id002 - name: Supplier1_642 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_642 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_642 - - class: SupplierFacility - configs: *id001 - name: Supplier3_643 - - class: SupplierFacility - configs: *id002 - name: Supplier1_643 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_643 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_643 - - class: SupplierFacility - configs: *id001 - name: Supplier3_644 - - class: SupplierFacility - configs: *id002 - name: Supplier1_644 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_644 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_644 - - class: SupplierFacility - configs: *id001 - name: Supplier3_645 - - class: SupplierFacility - configs: *id002 - name: Supplier1_645 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_645 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_645 - - class: SupplierFacility - configs: *id001 - name: Supplier3_646 - - class: SupplierFacility - configs: *id002 - name: Supplier1_646 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_646 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_646 - - class: SupplierFacility - configs: *id001 - name: Supplier3_647 - - class: SupplierFacility - configs: *id002 - name: Supplier1_647 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_647 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_647 - - class: SupplierFacility - configs: *id001 - name: Supplier3_648 - - class: SupplierFacility - configs: *id002 - name: Supplier1_648 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_648 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_648 - - class: SupplierFacility - configs: *id001 - name: Supplier3_649 - - class: SupplierFacility - configs: *id002 - name: Supplier1_649 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_649 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_649 - - class: SupplierFacility - configs: *id001 - name: Supplier3_650 - - class: SupplierFacility - configs: *id002 - name: Supplier1_650 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_650 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_650 - - class: SupplierFacility - configs: *id001 - name: Supplier3_651 - - class: SupplierFacility - configs: *id002 - name: Supplier1_651 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_651 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_651 - - class: SupplierFacility - configs: *id001 - name: Supplier3_652 - - class: SupplierFacility - configs: *id002 - name: Supplier1_652 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_652 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_652 - - class: SupplierFacility - configs: *id001 - name: Supplier3_653 - - class: SupplierFacility - configs: *id002 - name: Supplier1_653 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_653 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_653 - - class: SupplierFacility - configs: *id001 - name: Supplier3_654 - - class: SupplierFacility - configs: *id002 - name: Supplier1_654 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_654 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_654 - - class: SupplierFacility - configs: *id001 - name: Supplier3_655 - - class: SupplierFacility - configs: *id002 - name: Supplier1_655 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_655 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_655 - - class: SupplierFacility - configs: *id001 - name: Supplier3_656 - - class: SupplierFacility - configs: *id002 - name: Supplier1_656 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_656 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_656 - - class: SupplierFacility - configs: *id001 - name: Supplier3_657 - - class: SupplierFacility - configs: *id002 - name: Supplier1_657 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_657 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_657 - - class: SupplierFacility - configs: *id001 - name: Supplier3_658 - - class: SupplierFacility - configs: *id002 - name: Supplier1_658 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_658 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_658 - - class: SupplierFacility - configs: *id001 - name: Supplier3_659 - - class: SupplierFacility - configs: *id002 - name: Supplier1_659 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_659 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_659 - - class: SupplierFacility - configs: *id001 - name: Supplier3_660 - - class: SupplierFacility - configs: *id002 - name: Supplier1_660 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_660 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_660 - - class: SupplierFacility - configs: *id001 - name: Supplier3_661 - - class: SupplierFacility - configs: *id002 - name: Supplier1_661 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_661 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_661 - - class: SupplierFacility - configs: *id001 - name: Supplier3_662 - - class: SupplierFacility - configs: *id002 - name: Supplier1_662 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_662 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_662 - - class: SupplierFacility - configs: *id001 - name: Supplier3_663 - - class: SupplierFacility - configs: *id002 - name: Supplier1_663 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_663 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_663 - - class: SupplierFacility - configs: *id001 - name: Supplier3_664 - - class: SupplierFacility - configs: *id002 - name: Supplier1_664 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_664 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_664 - - class: SupplierFacility - configs: *id001 - name: Supplier3_665 - - class: SupplierFacility - configs: *id002 - name: Supplier1_665 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_665 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_665 - - class: SupplierFacility - configs: *id001 - name: Supplier3_666 - - class: SupplierFacility - configs: *id002 - name: Supplier1_666 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_666 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_666 - - class: SupplierFacility - configs: *id001 - name: Supplier3_667 - - class: SupplierFacility - configs: *id002 - name: Supplier1_667 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_667 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_667 - - class: SupplierFacility - configs: *id001 - name: Supplier3_668 - - class: SupplierFacility - configs: *id002 - name: Supplier1_668 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_668 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_668 - - class: SupplierFacility - configs: *id001 - name: Supplier3_669 - - class: SupplierFacility - configs: *id002 - name: Supplier1_669 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_669 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_669 - - class: SupplierFacility - configs: *id001 - name: Supplier3_670 - - class: SupplierFacility - configs: *id002 - name: Supplier1_670 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_670 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_670 - - class: SupplierFacility - configs: *id001 - name: Supplier3_671 - - class: SupplierFacility - configs: *id002 - name: Supplier1_671 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_671 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_671 - - class: SupplierFacility - configs: *id001 - name: Supplier3_672 - - class: SupplierFacility - configs: *id002 - name: Supplier1_672 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_672 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_672 - - class: SupplierFacility - configs: *id001 - name: Supplier3_673 - - class: SupplierFacility - configs: *id002 - name: Supplier1_673 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_673 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_673 - - class: SupplierFacility - configs: *id001 - name: Supplier3_674 - - class: SupplierFacility - configs: *id002 - name: Supplier1_674 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_674 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_674 - - class: SupplierFacility - configs: *id001 - name: Supplier3_675 - - class: SupplierFacility - configs: *id002 - name: Supplier1_675 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_675 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_675 - - class: SupplierFacility - configs: *id001 - name: Supplier3_676 - - class: SupplierFacility - configs: *id002 - name: Supplier1_676 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_676 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_676 - - class: SupplierFacility - configs: *id001 - name: Supplier3_677 - - class: SupplierFacility - configs: *id002 - name: Supplier1_677 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_677 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_677 - - class: SupplierFacility - configs: *id001 - name: Supplier3_678 - - class: SupplierFacility - configs: *id002 - name: Supplier1_678 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_678 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_678 - - class: SupplierFacility - configs: *id001 - name: Supplier3_679 - - class: SupplierFacility - configs: *id002 - name: Supplier1_679 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_679 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_679 - - class: SupplierFacility - configs: *id001 - name: Supplier3_680 - - class: SupplierFacility - configs: *id002 - name: Supplier1_680 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_680 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_680 - - class: SupplierFacility - configs: *id001 - name: Supplier3_681 - - class: SupplierFacility - configs: *id002 - name: Supplier1_681 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_681 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_681 - - class: SupplierFacility - configs: *id001 - name: Supplier3_682 - - class: SupplierFacility - configs: *id002 - name: Supplier1_682 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_682 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_682 - - class: SupplierFacility - configs: *id001 - name: Supplier3_683 - - class: SupplierFacility - configs: *id002 - name: Supplier1_683 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_683 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_683 - - class: SupplierFacility - configs: *id001 - name: Supplier3_684 - - class: SupplierFacility - configs: *id002 - name: Supplier1_684 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_684 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_684 - - class: SupplierFacility - configs: *id001 - name: Supplier3_685 - - class: SupplierFacility - configs: *id002 - name: Supplier1_685 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_685 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_685 - - class: SupplierFacility - configs: *id001 - name: Supplier3_686 - - class: SupplierFacility - configs: *id002 - name: Supplier1_686 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_686 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_686 - - class: SupplierFacility - configs: *id001 - name: Supplier3_687 - - class: SupplierFacility - configs: *id002 - name: Supplier1_687 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_687 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_687 - - class: SupplierFacility - configs: *id001 - name: Supplier3_688 - - class: SupplierFacility - configs: *id002 - name: Supplier1_688 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_688 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_688 - - class: SupplierFacility - configs: *id001 - name: Supplier3_689 - - class: SupplierFacility - configs: *id002 - name: Supplier1_689 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_689 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_689 - - class: SupplierFacility - configs: *id001 - name: Supplier3_690 - - class: SupplierFacility - configs: *id002 - name: Supplier1_690 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_690 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_690 - - class: SupplierFacility - configs: *id001 - name: Supplier3_691 - - class: SupplierFacility - configs: *id002 - name: Supplier1_691 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_691 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_691 - - class: SupplierFacility - configs: *id001 - name: Supplier3_692 - - class: SupplierFacility - configs: *id002 - name: Supplier1_692 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_692 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_692 - - class: SupplierFacility - configs: *id001 - name: Supplier3_693 - - class: SupplierFacility - configs: *id002 - name: Supplier1_693 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_693 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_693 - - class: SupplierFacility - configs: *id001 - name: Supplier3_694 - - class: SupplierFacility - configs: *id002 - name: Supplier1_694 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_694 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_694 - - class: SupplierFacility - configs: *id001 - name: Supplier3_695 - - class: SupplierFacility - configs: *id002 - name: Supplier1_695 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_695 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_695 - - class: SupplierFacility - configs: *id001 - name: Supplier3_696 - - class: SupplierFacility - configs: *id002 - name: Supplier1_696 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_696 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_696 - - class: SupplierFacility - configs: *id001 - name: Supplier3_697 - - class: SupplierFacility - configs: *id002 - name: Supplier1_697 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_697 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_697 - - class: SupplierFacility - configs: *id001 - name: Supplier3_698 - - class: SupplierFacility - configs: *id002 - name: Supplier1_698 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_698 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_698 - - class: SupplierFacility - configs: *id001 - name: Supplier3_699 - - class: SupplierFacility - configs: *id002 - name: Supplier1_699 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_699 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_699 - - class: SupplierFacility - configs: *id001 - name: Supplier3_700 - - class: SupplierFacility - configs: *id002 - name: Supplier1_700 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_700 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_700 - - class: SupplierFacility - configs: *id001 - name: Supplier3_701 - - class: SupplierFacility - configs: *id002 - name: Supplier1_701 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_701 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_701 - - class: SupplierFacility - configs: *id001 - name: Supplier3_702 - - class: SupplierFacility - configs: *id002 - name: Supplier1_702 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_702 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_702 - - class: SupplierFacility - configs: *id001 - name: Supplier3_703 - - class: SupplierFacility - configs: *id002 - name: Supplier1_703 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_703 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_703 - - class: SupplierFacility - configs: *id001 - name: Supplier3_704 - - class: SupplierFacility - configs: *id002 - name: Supplier1_704 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_704 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_704 - - class: SupplierFacility - configs: *id001 - name: Supplier3_705 - - class: SupplierFacility - configs: *id002 - name: Supplier1_705 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_705 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_705 - - class: SupplierFacility - configs: *id001 - name: Supplier3_706 - - class: SupplierFacility - configs: *id002 - name: Supplier1_706 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_706 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_706 - - class: SupplierFacility - configs: *id001 - name: Supplier3_707 - - class: SupplierFacility - configs: *id002 - name: Supplier1_707 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_707 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_707 - - class: SupplierFacility - configs: *id001 - name: Supplier3_708 - - class: SupplierFacility - configs: *id002 - name: Supplier1_708 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_708 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_708 - - class: SupplierFacility - configs: *id001 - name: Supplier3_709 - - class: SupplierFacility - configs: *id002 - name: Supplier1_709 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_709 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_709 - - class: SupplierFacility - configs: *id001 - name: Supplier3_710 - - class: SupplierFacility - configs: *id002 - name: Supplier1_710 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_710 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_710 - - class: SupplierFacility - configs: *id001 - name: Supplier3_711 - - class: SupplierFacility - configs: *id002 - name: Supplier1_711 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_711 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_711 - - class: SupplierFacility - configs: *id001 - name: Supplier3_712 - - class: SupplierFacility - configs: *id002 - name: Supplier1_712 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_712 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_712 - - class: SupplierFacility - configs: *id001 - name: Supplier3_713 - - class: SupplierFacility - configs: *id002 - name: Supplier1_713 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_713 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_713 - - class: SupplierFacility - configs: *id001 - name: Supplier3_714 - - class: SupplierFacility - configs: *id002 - name: Supplier1_714 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_714 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_714 - - class: SupplierFacility - configs: *id001 - name: Supplier3_715 - - class: SupplierFacility - configs: *id002 - name: Supplier1_715 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_715 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_715 - - class: SupplierFacility - configs: *id001 - name: Supplier3_716 - - class: SupplierFacility - configs: *id002 - name: Supplier1_716 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_716 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_716 - - class: SupplierFacility - configs: *id001 - name: Supplier3_717 - - class: SupplierFacility - configs: *id002 - name: Supplier1_717 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_717 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_717 - - class: SupplierFacility - configs: *id001 - name: Supplier3_718 - - class: SupplierFacility - configs: *id002 - name: Supplier1_718 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_718 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_718 - - class: SupplierFacility - configs: *id001 - name: Supplier3_719 - - class: SupplierFacility - configs: *id002 - name: Supplier1_719 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_719 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_719 - - class: SupplierFacility - configs: *id001 - name: Supplier3_720 - - class: SupplierFacility - configs: *id002 - name: Supplier1_720 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_720 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_720 - - class: SupplierFacility - configs: *id001 - name: Supplier3_721 - - class: SupplierFacility - configs: *id002 - name: Supplier1_721 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_721 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_721 - - class: SupplierFacility - configs: *id001 - name: Supplier3_722 - - class: SupplierFacility - configs: *id002 - name: Supplier1_722 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_722 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_722 - - class: SupplierFacility - configs: *id001 - name: Supplier3_723 - - class: SupplierFacility - configs: *id002 - name: Supplier1_723 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_723 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_723 - - class: SupplierFacility - configs: *id001 - name: Supplier3_724 - - class: SupplierFacility - configs: *id002 - name: Supplier1_724 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_724 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_724 - - class: SupplierFacility - configs: *id001 - name: Supplier3_725 - - class: SupplierFacility - configs: *id002 - name: Supplier1_725 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_725 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_725 - - class: SupplierFacility - configs: *id001 - name: Supplier3_726 - - class: SupplierFacility - configs: *id002 - name: Supplier1_726 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_726 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_726 - - class: SupplierFacility - configs: *id001 - name: Supplier3_727 - - class: SupplierFacility - configs: *id002 - name: Supplier1_727 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_727 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_727 - - class: SupplierFacility - configs: *id001 - name: Supplier3_728 - - class: SupplierFacility - configs: *id002 - name: Supplier1_728 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_728 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_728 - - class: SupplierFacility - configs: *id001 - name: Supplier3_729 - - class: SupplierFacility - configs: *id002 - name: Supplier1_729 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_729 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_729 - - class: SupplierFacility - configs: *id001 - name: Supplier3_730 - - class: SupplierFacility - configs: *id002 - name: Supplier1_730 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_730 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_730 - - class: SupplierFacility - configs: *id001 - name: Supplier3_731 - - class: SupplierFacility - configs: *id002 - name: Supplier1_731 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_731 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_731 - - class: SupplierFacility - configs: *id001 - name: Supplier3_732 - - class: SupplierFacility - configs: *id002 - name: Supplier1_732 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_732 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_732 - - class: SupplierFacility - configs: *id001 - name: Supplier3_733 - - class: SupplierFacility - configs: *id002 - name: Supplier1_733 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_733 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_733 - - class: SupplierFacility - configs: *id001 - name: Supplier3_734 - - class: SupplierFacility - configs: *id002 - name: Supplier1_734 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_734 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_734 - - class: SupplierFacility - configs: *id001 - name: Supplier3_735 - - class: SupplierFacility - configs: *id002 - name: Supplier1_735 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_735 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_735 - - class: SupplierFacility - configs: *id001 - name: Supplier3_736 - - class: SupplierFacility - configs: *id002 - name: Supplier1_736 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_736 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_736 - - class: SupplierFacility - configs: *id001 - name: Supplier3_737 - - class: SupplierFacility - configs: *id002 - name: Supplier1_737 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_737 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_737 - - class: SupplierFacility - configs: *id001 - name: Supplier3_738 - - class: SupplierFacility - configs: *id002 - name: Supplier1_738 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_738 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_738 - - class: SupplierFacility - configs: *id001 - name: Supplier3_739 - - class: SupplierFacility - configs: *id002 - name: Supplier1_739 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_739 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_739 - - class: SupplierFacility - configs: *id001 - name: Supplier3_740 - - class: SupplierFacility - configs: *id002 - name: Supplier1_740 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_740 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_740 - - class: SupplierFacility - configs: *id001 - name: Supplier3_741 - - class: SupplierFacility - configs: *id002 - name: Supplier1_741 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_741 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_741 - - class: SupplierFacility - configs: *id001 - name: Supplier3_742 - - class: SupplierFacility - configs: *id002 - name: Supplier1_742 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_742 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_742 - - class: SupplierFacility - configs: *id001 - name: Supplier3_743 - - class: SupplierFacility - configs: *id002 - name: Supplier1_743 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_743 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_743 - - class: SupplierFacility - configs: *id001 - name: Supplier3_744 - - class: SupplierFacility - configs: *id002 - name: Supplier1_744 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_744 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_744 - - class: SupplierFacility - configs: *id001 - name: Supplier3_745 - - class: SupplierFacility - configs: *id002 - name: Supplier1_745 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_745 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_745 - - class: SupplierFacility - configs: *id001 - name: Supplier3_746 - - class: SupplierFacility - configs: *id002 - name: Supplier1_746 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_746 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_746 - - class: SupplierFacility - configs: *id001 - name: Supplier3_747 - - class: SupplierFacility - configs: *id002 - name: Supplier1_747 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_747 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_747 - - class: SupplierFacility - configs: *id001 - name: Supplier3_748 - - class: SupplierFacility - configs: *id002 - name: Supplier1_748 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_748 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_748 - - class: SupplierFacility - configs: *id001 - name: Supplier3_749 - - class: SupplierFacility - configs: *id002 - name: Supplier1_749 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_749 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_749 - - class: SupplierFacility - configs: *id001 - name: Supplier3_750 - - class: SupplierFacility - configs: *id002 - name: Supplier1_750 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_750 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_750 - - class: SupplierFacility - configs: *id001 - name: Supplier3_751 - - class: SupplierFacility - configs: *id002 - name: Supplier1_751 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_751 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_751 - - class: SupplierFacility - configs: *id001 - name: Supplier3_752 - - class: SupplierFacility - configs: *id002 - name: Supplier1_752 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_752 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_752 - - class: SupplierFacility - configs: *id001 - name: Supplier3_753 - - class: SupplierFacility - configs: *id002 - name: Supplier1_753 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_753 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_753 - - class: SupplierFacility - configs: *id001 - name: Supplier3_754 - - class: SupplierFacility - configs: *id002 - name: Supplier1_754 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_754 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_754 - - class: SupplierFacility - configs: *id001 - name: Supplier3_755 - - class: SupplierFacility - configs: *id002 - name: Supplier1_755 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_755 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_755 - - class: SupplierFacility - configs: *id001 - name: Supplier3_756 - - class: SupplierFacility - configs: *id002 - name: Supplier1_756 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_756 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_756 - - class: SupplierFacility - configs: *id001 - name: Supplier3_757 - - class: SupplierFacility - configs: *id002 - name: Supplier1_757 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_757 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_757 - - class: SupplierFacility - configs: *id001 - name: Supplier3_758 - - class: SupplierFacility - configs: *id002 - name: Supplier1_758 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_758 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_758 - - class: SupplierFacility - configs: *id001 - name: Supplier3_759 - - class: SupplierFacility - configs: *id002 - name: Supplier1_759 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_759 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_759 - - class: SupplierFacility - configs: *id001 - name: Supplier3_760 - - class: SupplierFacility - configs: *id002 - name: Supplier1_760 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_760 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_760 - - class: SupplierFacility - configs: *id001 - name: Supplier3_761 - - class: SupplierFacility - configs: *id002 - name: Supplier1_761 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_761 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_761 - - class: SupplierFacility - configs: *id001 - name: Supplier3_762 - - class: SupplierFacility - configs: *id002 - name: Supplier1_762 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_762 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_762 - - class: SupplierFacility - configs: *id001 - name: Supplier3_763 - - class: SupplierFacility - configs: *id002 - name: Supplier1_763 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_763 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_763 - - class: SupplierFacility - configs: *id001 - name: Supplier3_764 - - class: SupplierFacility - configs: *id002 - name: Supplier1_764 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_764 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_764 - - class: SupplierFacility - configs: *id001 - name: Supplier3_765 - - class: SupplierFacility - configs: *id002 - name: Supplier1_765 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_765 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_765 - - class: SupplierFacility - configs: *id001 - name: Supplier3_766 - - class: SupplierFacility - configs: *id002 - name: Supplier1_766 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_766 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_766 - - class: SupplierFacility - configs: *id001 - name: Supplier3_767 - - class: SupplierFacility - configs: *id002 - name: Supplier1_767 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_767 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_767 - - class: SupplierFacility - configs: *id001 - name: Supplier3_768 - - class: SupplierFacility - configs: *id002 - name: Supplier1_768 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_768 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_768 - - class: SupplierFacility - configs: *id001 - name: Supplier3_769 - - class: SupplierFacility - configs: *id002 - name: Supplier1_769 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_769 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_769 - - class: SupplierFacility - configs: *id001 - name: Supplier3_770 - - class: SupplierFacility - configs: *id002 - name: Supplier1_770 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_770 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_770 - - class: SupplierFacility - configs: *id001 - name: Supplier3_771 - - class: SupplierFacility - configs: *id002 - name: Supplier1_771 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_771 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_771 - - class: SupplierFacility - configs: *id001 - name: Supplier3_772 - - class: SupplierFacility - configs: *id002 - name: Supplier1_772 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_772 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_772 - - class: SupplierFacility - configs: *id001 - name: Supplier3_773 - - class: SupplierFacility - configs: *id002 - name: Supplier1_773 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_773 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_773 - - class: SupplierFacility - configs: *id001 - name: Supplier3_774 - - class: SupplierFacility - configs: *id002 - name: Supplier1_774 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_774 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_774 - - class: SupplierFacility - configs: *id001 - name: Supplier3_775 - - class: SupplierFacility - configs: *id002 - name: Supplier1_775 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_775 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_775 - - class: SupplierFacility - configs: *id001 - name: Supplier3_776 - - class: SupplierFacility - configs: *id002 - name: Supplier1_776 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_776 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_776 - - class: SupplierFacility - configs: *id001 - name: Supplier3_777 - - class: SupplierFacility - configs: *id002 - name: Supplier1_777 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_777 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_777 - - class: SupplierFacility - configs: *id001 - name: Supplier3_778 - - class: SupplierFacility - configs: *id002 - name: Supplier1_778 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_778 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_778 - - class: SupplierFacility - configs: *id001 - name: Supplier3_779 - - class: SupplierFacility - configs: *id002 - name: Supplier1_779 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_779 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_779 - - class: SupplierFacility - configs: *id001 - name: Supplier3_780 - - class: SupplierFacility - configs: *id002 - name: Supplier1_780 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_780 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_780 - - class: SupplierFacility - configs: *id001 - name: Supplier3_781 - - class: SupplierFacility - configs: *id002 - name: Supplier1_781 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_781 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_781 - - class: SupplierFacility - configs: *id001 - name: Supplier3_782 - - class: SupplierFacility - configs: *id002 - name: Supplier1_782 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_782 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_782 - - class: SupplierFacility - configs: *id001 - name: Supplier3_783 - - class: SupplierFacility - configs: *id002 - name: Supplier1_783 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_783 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_783 - - class: SupplierFacility - configs: *id001 - name: Supplier3_784 - - class: SupplierFacility - configs: *id002 - name: Supplier1_784 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_784 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_784 - - class: SupplierFacility - configs: *id001 - name: Supplier3_785 - - class: SupplierFacility - configs: *id002 - name: Supplier1_785 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_785 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_785 - - class: SupplierFacility - configs: *id001 - name: Supplier3_786 - - class: SupplierFacility - configs: *id002 - name: Supplier1_786 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_786 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_786 - - class: SupplierFacility - configs: *id001 - name: Supplier3_787 - - class: SupplierFacility - configs: *id002 - name: Supplier1_787 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_787 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_787 - - class: SupplierFacility - configs: *id001 - name: Supplier3_788 - - class: SupplierFacility - configs: *id002 - name: Supplier1_788 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_788 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_788 - - class: SupplierFacility - configs: *id001 - name: Supplier3_789 - - class: SupplierFacility - configs: *id002 - name: Supplier1_789 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_789 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_789 - - class: SupplierFacility - configs: *id001 - name: Supplier3_790 - - class: SupplierFacility - configs: *id002 - name: Supplier1_790 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_790 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_790 - - class: SupplierFacility - configs: *id001 - name: Supplier3_791 - - class: SupplierFacility - configs: *id002 - name: Supplier1_791 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_791 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_791 - - class: SupplierFacility - configs: *id001 - name: Supplier3_792 - - class: SupplierFacility - configs: *id002 - name: Supplier1_792 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_792 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_792 - - class: SupplierFacility - configs: *id001 - name: Supplier3_793 - - class: SupplierFacility - configs: *id002 - name: Supplier1_793 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_793 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_793 - - class: SupplierFacility - configs: *id001 - name: Supplier3_794 - - class: SupplierFacility - configs: *id002 - name: Supplier1_794 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_794 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_794 - - class: SupplierFacility - configs: *id001 - name: Supplier3_795 - - class: SupplierFacility - configs: *id002 - name: Supplier1_795 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_795 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_795 - - class: SupplierFacility - configs: *id001 - name: Supplier3_796 - - class: SupplierFacility - configs: *id002 - name: Supplier1_796 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_796 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_796 - - class: SupplierFacility - configs: *id001 - name: Supplier3_797 - - class: SupplierFacility - configs: *id002 - name: Supplier1_797 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_797 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_797 - - class: SupplierFacility - configs: *id001 - name: Supplier3_798 - - class: SupplierFacility - configs: *id002 - name: Supplier1_798 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_798 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_798 - - class: SupplierFacility - configs: *id001 - name: Supplier3_799 - - class: SupplierFacility - configs: *id002 - name: Supplier1_799 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_799 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_799 - - class: SupplierFacility - configs: *id001 - name: Supplier3_800 - - class: SupplierFacility - configs: *id002 - name: Supplier1_800 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_800 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_800 - - class: SupplierFacility - configs: *id001 - name: Supplier3_801 - - class: SupplierFacility - configs: *id002 - name: Supplier1_801 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_801 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_801 - - class: SupplierFacility - configs: *id001 - name: Supplier3_802 - - class: SupplierFacility - configs: *id002 - name: Supplier1_802 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_802 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_802 - - class: SupplierFacility - configs: *id001 - name: Supplier3_803 - - class: SupplierFacility - configs: *id002 - name: Supplier1_803 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_803 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_803 - - class: SupplierFacility - configs: *id001 - name: Supplier3_804 - - class: SupplierFacility - configs: *id002 - name: Supplier1_804 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_804 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_804 - - class: SupplierFacility - configs: *id001 - name: Supplier3_805 - - class: SupplierFacility - configs: *id002 - name: Supplier1_805 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_805 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_805 - - class: SupplierFacility - configs: *id001 - name: Supplier3_806 - - class: SupplierFacility - configs: *id002 - name: Supplier1_806 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_806 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_806 - - class: SupplierFacility - configs: *id001 - name: Supplier3_807 - - class: SupplierFacility - configs: *id002 - name: Supplier1_807 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_807 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_807 - - class: SupplierFacility - configs: *id001 - name: Supplier3_808 - - class: SupplierFacility - configs: *id002 - name: Supplier1_808 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_808 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_808 - - class: SupplierFacility - configs: *id001 - name: Supplier3_809 - - class: SupplierFacility - configs: *id002 - name: Supplier1_809 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_809 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_809 - - class: SupplierFacility - configs: *id001 - name: Supplier3_810 - - class: SupplierFacility - configs: *id002 - name: Supplier1_810 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_810 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_810 - - class: SupplierFacility - configs: *id001 - name: Supplier3_811 - - class: SupplierFacility - configs: *id002 - name: Supplier1_811 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_811 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_811 - - class: SupplierFacility - configs: *id001 - name: Supplier3_812 - - class: SupplierFacility - configs: *id002 - name: Supplier1_812 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_812 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_812 - - class: SupplierFacility - configs: *id001 - name: Supplier3_813 - - class: SupplierFacility - configs: *id002 - name: Supplier1_813 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_813 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_813 - - class: SupplierFacility - configs: *id001 - name: Supplier3_814 - - class: SupplierFacility - configs: *id002 - name: Supplier1_814 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_814 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_814 - - class: SupplierFacility - configs: *id001 - name: Supplier3_815 - - class: SupplierFacility - configs: *id002 - name: Supplier1_815 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_815 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_815 - - class: SupplierFacility - configs: *id001 - name: Supplier3_816 - - class: SupplierFacility - configs: *id002 - name: Supplier1_816 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_816 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_816 - - class: SupplierFacility - configs: *id001 - name: Supplier3_817 - - class: SupplierFacility - configs: *id002 - name: Supplier1_817 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_817 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_817 - - class: SupplierFacility - configs: *id001 - name: Supplier3_818 - - class: SupplierFacility - configs: *id002 - name: Supplier1_818 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_818 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_818 - - class: SupplierFacility - configs: *id001 - name: Supplier3_819 - - class: SupplierFacility - configs: *id002 - name: Supplier1_819 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_819 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_819 - - class: SupplierFacility - configs: *id001 - name: Supplier3_820 - - class: SupplierFacility - configs: *id002 - name: Supplier1_820 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_820 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_820 - - class: SupplierFacility - configs: *id001 - name: Supplier3_821 - - class: SupplierFacility - configs: *id002 - name: Supplier1_821 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_821 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_821 - - class: SupplierFacility - configs: *id001 - name: Supplier3_822 - - class: SupplierFacility - configs: *id002 - name: Supplier1_822 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_822 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_822 - - class: SupplierFacility - configs: *id001 - name: Supplier3_823 - - class: SupplierFacility - configs: *id002 - name: Supplier1_823 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_823 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_823 - - class: SupplierFacility - configs: *id001 - name: Supplier3_824 - - class: SupplierFacility - configs: *id002 - name: Supplier1_824 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_824 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_824 - - class: SupplierFacility - configs: *id001 - name: Supplier3_825 - - class: SupplierFacility - configs: *id002 - name: Supplier1_825 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_825 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_825 - - class: SupplierFacility - configs: *id001 - name: Supplier3_826 - - class: SupplierFacility - configs: *id002 - name: Supplier1_826 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_826 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_826 - - class: SupplierFacility - configs: *id001 - name: Supplier3_827 - - class: SupplierFacility - configs: *id002 - name: Supplier1_827 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_827 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_827 - - class: SupplierFacility - configs: *id001 - name: Supplier3_828 - - class: SupplierFacility - configs: *id002 - name: Supplier1_828 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_828 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_828 - - class: SupplierFacility - configs: *id001 - name: Supplier3_829 - - class: SupplierFacility - configs: *id002 - name: Supplier1_829 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_829 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_829 - - class: SupplierFacility - configs: *id001 - name: Supplier3_830 - - class: SupplierFacility - configs: *id002 - name: Supplier1_830 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_830 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_830 - - class: SupplierFacility - configs: *id001 - name: Supplier3_831 - - class: SupplierFacility - configs: *id002 - name: Supplier1_831 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_831 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_831 - - class: SupplierFacility - configs: *id001 - name: Supplier3_832 - - class: SupplierFacility - configs: *id002 - name: Supplier1_832 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_832 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_832 - - class: SupplierFacility - configs: *id001 - name: Supplier3_833 - - class: SupplierFacility - configs: *id002 - name: Supplier1_833 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_833 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_833 - - class: SupplierFacility - configs: *id001 - name: Supplier3_834 - - class: SupplierFacility - configs: *id002 - name: Supplier1_834 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_834 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_834 - - class: SupplierFacility - configs: *id001 - name: Supplier3_835 - - class: SupplierFacility - configs: *id002 - name: Supplier1_835 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_835 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_835 - - class: SupplierFacility - configs: *id001 - name: Supplier3_836 - - class: SupplierFacility - configs: *id002 - name: Supplier1_836 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_836 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_836 - - class: SupplierFacility - configs: *id001 - name: Supplier3_837 - - class: SupplierFacility - configs: *id002 - name: Supplier1_837 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_837 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_837 - - class: SupplierFacility - configs: *id001 - name: Supplier3_838 - - class: SupplierFacility - configs: *id002 - name: Supplier1_838 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_838 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_838 - - class: SupplierFacility - configs: *id001 - name: Supplier3_839 - - class: SupplierFacility - configs: *id002 - name: Supplier1_839 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_839 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_839 - - class: SupplierFacility - configs: *id001 - name: Supplier3_840 - - class: SupplierFacility - configs: *id002 - name: Supplier1_840 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_840 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_840 - - class: SupplierFacility - configs: *id001 - name: Supplier3_841 - - class: SupplierFacility - configs: *id002 - name: Supplier1_841 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_841 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_841 - - class: SupplierFacility - configs: *id001 - name: Supplier3_842 - - class: SupplierFacility - configs: *id002 - name: Supplier1_842 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_842 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_842 - - class: SupplierFacility - configs: *id001 - name: Supplier3_843 - - class: SupplierFacility - configs: *id002 - name: Supplier1_843 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_843 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_843 - - class: SupplierFacility - configs: *id001 - name: Supplier3_844 - - class: SupplierFacility - configs: *id002 - name: Supplier1_844 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_844 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_844 - - class: SupplierFacility - configs: *id001 - name: Supplier3_845 - - class: SupplierFacility - configs: *id002 - name: Supplier1_845 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_845 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_845 - - class: SupplierFacility - configs: *id001 - name: Supplier3_846 - - class: SupplierFacility - configs: *id002 - name: Supplier1_846 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_846 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_846 - - class: SupplierFacility - configs: *id001 - name: Supplier3_847 - - class: SupplierFacility - configs: *id002 - name: Supplier1_847 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_847 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_847 - - class: SupplierFacility - configs: *id001 - name: Supplier3_848 - - class: SupplierFacility - configs: *id002 - name: Supplier1_848 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_848 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_848 - - class: SupplierFacility - configs: *id001 - name: Supplier3_849 - - class: SupplierFacility - configs: *id002 - name: Supplier1_849 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_849 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_849 - - class: SupplierFacility - configs: *id001 - name: Supplier3_850 - - class: SupplierFacility - configs: *id002 - name: Supplier1_850 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_850 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_850 - - class: SupplierFacility - configs: *id001 - name: Supplier3_851 - - class: SupplierFacility - configs: *id002 - name: Supplier1_851 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_851 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_851 - - class: SupplierFacility - configs: *id001 - name: Supplier3_852 - - class: SupplierFacility - configs: *id002 - name: Supplier1_852 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_852 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_852 - - class: SupplierFacility - configs: *id001 - name: Supplier3_853 - - class: SupplierFacility - configs: *id002 - name: Supplier1_853 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_853 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_853 - - class: SupplierFacility - configs: *id001 - name: Supplier3_854 - - class: SupplierFacility - configs: *id002 - name: Supplier1_854 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_854 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_854 - - class: SupplierFacility - configs: *id001 - name: Supplier3_855 - - class: SupplierFacility - configs: *id002 - name: Supplier1_855 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_855 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_855 - - class: SupplierFacility - configs: *id001 - name: Supplier3_856 - - class: SupplierFacility - configs: *id002 - name: Supplier1_856 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_856 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_856 - - class: SupplierFacility - configs: *id001 - name: Supplier3_857 - - class: SupplierFacility - configs: *id002 - name: Supplier1_857 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_857 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_857 - - class: SupplierFacility - configs: *id001 - name: Supplier3_858 - - class: SupplierFacility - configs: *id002 - name: Supplier1_858 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_858 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_858 - - class: SupplierFacility - configs: *id001 - name: Supplier3_859 - - class: SupplierFacility - configs: *id002 - name: Supplier1_859 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_859 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_859 - - class: SupplierFacility - configs: *id001 - name: Supplier3_860 - - class: SupplierFacility - configs: *id002 - name: Supplier1_860 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_860 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_860 - - class: SupplierFacility - configs: *id001 - name: Supplier3_861 - - class: SupplierFacility - configs: *id002 - name: Supplier1_861 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_861 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_861 - - class: SupplierFacility - configs: *id001 - name: Supplier3_862 - - class: SupplierFacility - configs: *id002 - name: Supplier1_862 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_862 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_862 - - class: SupplierFacility - configs: *id001 - name: Supplier3_863 - - class: SupplierFacility - configs: *id002 - name: Supplier1_863 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_863 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_863 - - class: SupplierFacility - configs: *id001 - name: Supplier3_864 - - class: SupplierFacility - configs: *id002 - name: Supplier1_864 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_864 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_864 - - class: SupplierFacility - configs: *id001 - name: Supplier3_865 - - class: SupplierFacility - configs: *id002 - name: Supplier1_865 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_865 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_865 - - class: SupplierFacility - configs: *id001 - name: Supplier3_866 - - class: SupplierFacility - configs: *id002 - name: Supplier1_866 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_866 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_866 - - class: SupplierFacility - configs: *id001 - name: Supplier3_867 - - class: SupplierFacility - configs: *id002 - name: Supplier1_867 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_867 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_867 - - class: SupplierFacility - configs: *id001 - name: Supplier3_868 - - class: SupplierFacility - configs: *id002 - name: Supplier1_868 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_868 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_868 - - class: SupplierFacility - configs: *id001 - name: Supplier3_869 - - class: SupplierFacility - configs: *id002 - name: Supplier1_869 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_869 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_869 - - class: SupplierFacility - configs: *id001 - name: Supplier3_870 - - class: SupplierFacility - configs: *id002 - name: Supplier1_870 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_870 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_870 - - class: SupplierFacility - configs: *id001 - name: Supplier3_871 - - class: SupplierFacility - configs: *id002 - name: Supplier1_871 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_871 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_871 - - class: SupplierFacility - configs: *id001 - name: Supplier3_872 - - class: SupplierFacility - configs: *id002 - name: Supplier1_872 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_872 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_872 - - class: SupplierFacility - configs: *id001 - name: Supplier3_873 - - class: SupplierFacility - configs: *id002 - name: Supplier1_873 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_873 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_873 - - class: SupplierFacility - configs: *id001 - name: Supplier3_874 - - class: SupplierFacility - configs: *id002 - name: Supplier1_874 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_874 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_874 - - class: SupplierFacility - configs: *id001 - name: Supplier3_875 - - class: SupplierFacility - configs: *id002 - name: Supplier1_875 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_875 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_875 - - class: SupplierFacility - configs: *id001 - name: Supplier3_876 - - class: SupplierFacility - configs: *id002 - name: Supplier1_876 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_876 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_876 - - class: SupplierFacility - configs: *id001 - name: Supplier3_877 - - class: SupplierFacility - configs: *id002 - name: Supplier1_877 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_877 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_877 - - class: SupplierFacility - configs: *id001 - name: Supplier3_878 - - class: SupplierFacility - configs: *id002 - name: Supplier1_878 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_878 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_878 - - class: SupplierFacility - configs: *id001 - name: Supplier3_879 - - class: SupplierFacility - configs: *id002 - name: Supplier1_879 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_879 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_879 - - class: SupplierFacility - configs: *id001 - name: Supplier3_880 - - class: SupplierFacility - configs: *id002 - name: Supplier1_880 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_880 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_880 - - class: SupplierFacility - configs: *id001 - name: Supplier3_881 - - class: SupplierFacility - configs: *id002 - name: Supplier1_881 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_881 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_881 - - class: SupplierFacility - configs: *id001 - name: Supplier3_882 - - class: SupplierFacility - configs: *id002 - name: Supplier1_882 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_882 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_882 - - class: SupplierFacility - configs: *id001 - name: Supplier3_883 - - class: SupplierFacility - configs: *id002 - name: Supplier1_883 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_883 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_883 - - class: SupplierFacility - configs: *id001 - name: Supplier3_884 - - class: SupplierFacility - configs: *id002 - name: Supplier1_884 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_884 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_884 - - class: SupplierFacility - configs: *id001 - name: Supplier3_885 - - class: SupplierFacility - configs: *id002 - name: Supplier1_885 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_885 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_885 - - class: SupplierFacility - configs: *id001 - name: Supplier3_886 - - class: SupplierFacility - configs: *id002 - name: Supplier1_886 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_886 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_886 - - class: SupplierFacility - configs: *id001 - name: Supplier3_887 - - class: SupplierFacility - configs: *id002 - name: Supplier1_887 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_887 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_887 - - class: SupplierFacility - configs: *id001 - name: Supplier3_888 - - class: SupplierFacility - configs: *id002 - name: Supplier1_888 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_888 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_888 - - class: SupplierFacility - configs: *id001 - name: Supplier3_889 - - class: SupplierFacility - configs: *id002 - name: Supplier1_889 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_889 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_889 - - class: SupplierFacility - configs: *id001 - name: Supplier3_890 - - class: SupplierFacility - configs: *id002 - name: Supplier1_890 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_890 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_890 - - class: SupplierFacility - configs: *id001 - name: Supplier3_891 - - class: SupplierFacility - configs: *id002 - name: Supplier1_891 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_891 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_891 - - class: SupplierFacility - configs: *id001 - name: Supplier3_892 - - class: SupplierFacility - configs: *id002 - name: Supplier1_892 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_892 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_892 - - class: SupplierFacility - configs: *id001 - name: Supplier3_893 - - class: SupplierFacility - configs: *id002 - name: Supplier1_893 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_893 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_893 - - class: SupplierFacility - configs: *id001 - name: Supplier3_894 - - class: SupplierFacility - configs: *id002 - name: Supplier1_894 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_894 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_894 - - class: SupplierFacility - configs: *id001 - name: Supplier3_895 - - class: SupplierFacility - configs: *id002 - name: Supplier1_895 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_895 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_895 - - class: SupplierFacility - configs: *id001 - name: Supplier3_896 - - class: SupplierFacility - configs: *id002 - name: Supplier1_896 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_896 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_896 - - class: SupplierFacility - configs: *id001 - name: Supplier3_897 - - class: SupplierFacility - configs: *id002 - name: Supplier1_897 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_897 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_897 - - class: SupplierFacility - configs: *id001 - name: Supplier3_898 - - class: SupplierFacility - configs: *id002 - name: Supplier1_898 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_898 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_898 - - class: SupplierFacility - configs: *id001 - name: Supplier3_899 - - class: SupplierFacility - configs: *id002 - name: Supplier1_899 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_899 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_899 - - class: SupplierFacility - configs: *id001 - name: Supplier3_900 - - class: SupplierFacility - configs: *id002 - name: Supplier1_900 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_900 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_900 - - class: SupplierFacility - configs: *id001 - name: Supplier3_901 - - class: SupplierFacility - configs: *id002 - name: Supplier1_901 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_901 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_901 - - class: SupplierFacility - configs: *id001 - name: Supplier3_902 - - class: SupplierFacility - configs: *id002 - name: Supplier1_902 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_902 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_902 - - class: SupplierFacility - configs: *id001 - name: Supplier3_903 - - class: SupplierFacility - configs: *id002 - name: Supplier1_903 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_903 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_903 - - class: SupplierFacility - configs: *id001 - name: Supplier3_904 - - class: SupplierFacility - configs: *id002 - name: Supplier1_904 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_904 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_904 - - class: SupplierFacility - configs: *id001 - name: Supplier3_905 - - class: SupplierFacility - configs: *id002 - name: Supplier1_905 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_905 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_905 - - class: SupplierFacility - configs: *id001 - name: Supplier3_906 - - class: SupplierFacility - configs: *id002 - name: Supplier1_906 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_906 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_906 - - class: SupplierFacility - configs: *id001 - name: Supplier3_907 - - class: SupplierFacility - configs: *id002 - name: Supplier1_907 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_907 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_907 - - class: SupplierFacility - configs: *id001 - name: Supplier3_908 - - class: SupplierFacility - configs: *id002 - name: Supplier1_908 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_908 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_908 - - class: SupplierFacility - configs: *id001 - name: Supplier3_909 - - class: SupplierFacility - configs: *id002 - name: Supplier1_909 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_909 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_909 - - class: SupplierFacility - configs: *id001 - name: Supplier3_910 - - class: SupplierFacility - configs: *id002 - name: Supplier1_910 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_910 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_910 - - class: SupplierFacility - configs: *id001 - name: Supplier3_911 - - class: SupplierFacility - configs: *id002 - name: Supplier1_911 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_911 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_911 - - class: SupplierFacility - configs: *id001 - name: Supplier3_912 - - class: SupplierFacility - configs: *id002 - name: Supplier1_912 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_912 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_912 - - class: SupplierFacility - configs: *id001 - name: Supplier3_913 - - class: SupplierFacility - configs: *id002 - name: Supplier1_913 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_913 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_913 - - class: SupplierFacility - configs: *id001 - name: Supplier3_914 - - class: SupplierFacility - configs: *id002 - name: Supplier1_914 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_914 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_914 - - class: SupplierFacility - configs: *id001 - name: Supplier3_915 - - class: SupplierFacility - configs: *id002 - name: Supplier1_915 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_915 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_915 - - class: SupplierFacility - configs: *id001 - name: Supplier3_916 - - class: SupplierFacility - configs: *id002 - name: Supplier1_916 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_916 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_916 - - class: SupplierFacility - configs: *id001 - name: Supplier3_917 - - class: SupplierFacility - configs: *id002 - name: Supplier1_917 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_917 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_917 - - class: SupplierFacility - configs: *id001 - name: Supplier3_918 - - class: SupplierFacility - configs: *id002 - name: Supplier1_918 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_918 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_918 - - class: SupplierFacility - configs: *id001 - name: Supplier3_919 - - class: SupplierFacility - configs: *id002 - name: Supplier1_919 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_919 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_919 - - class: SupplierFacility - configs: *id001 - name: Supplier3_920 - - class: SupplierFacility - configs: *id002 - name: Supplier1_920 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_920 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_920 - - class: SupplierFacility - configs: *id001 - name: Supplier3_921 - - class: SupplierFacility - configs: *id002 - name: Supplier1_921 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_921 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_921 - - class: SupplierFacility - configs: *id001 - name: Supplier3_922 - - class: SupplierFacility - configs: *id002 - name: Supplier1_922 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_922 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_922 - - class: SupplierFacility - configs: *id001 - name: Supplier3_923 - - class: SupplierFacility - configs: *id002 - name: Supplier1_923 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_923 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_923 - - class: SupplierFacility - configs: *id001 - name: Supplier3_924 - - class: SupplierFacility - configs: *id002 - name: Supplier1_924 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_924 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_924 - - class: SupplierFacility - configs: *id001 - name: Supplier3_925 - - class: SupplierFacility - configs: *id002 - name: Supplier1_925 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_925 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_925 - - class: SupplierFacility - configs: *id001 - name: Supplier3_926 - - class: SupplierFacility - configs: *id002 - name: Supplier1_926 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_926 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_926 - - class: SupplierFacility - configs: *id001 - name: Supplier3_927 - - class: SupplierFacility - configs: *id002 - name: Supplier1_927 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_927 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_927 - - class: SupplierFacility - configs: *id001 - name: Supplier3_928 - - class: SupplierFacility - configs: *id002 - name: Supplier1_928 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_928 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_928 - - class: SupplierFacility - configs: *id001 - name: Supplier3_929 - - class: SupplierFacility - configs: *id002 - name: Supplier1_929 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_929 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_929 - - class: SupplierFacility - configs: *id001 - name: Supplier3_930 - - class: SupplierFacility - configs: *id002 - name: Supplier1_930 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_930 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_930 - - class: SupplierFacility - configs: *id001 - name: Supplier3_931 - - class: SupplierFacility - configs: *id002 - name: Supplier1_931 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_931 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_931 - - class: SupplierFacility - configs: *id001 - name: Supplier3_932 - - class: SupplierFacility - configs: *id002 - name: Supplier1_932 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_932 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_932 - - class: SupplierFacility - configs: *id001 - name: Supplier3_933 - - class: SupplierFacility - configs: *id002 - name: Supplier1_933 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_933 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_933 - - class: SupplierFacility - configs: *id001 - name: Supplier3_934 - - class: SupplierFacility - configs: *id002 - name: Supplier1_934 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_934 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_934 - - class: SupplierFacility - configs: *id001 - name: Supplier3_935 - - class: SupplierFacility - configs: *id002 - name: Supplier1_935 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_935 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_935 - - class: SupplierFacility - configs: *id001 - name: Supplier3_936 - - class: SupplierFacility - configs: *id002 - name: Supplier1_936 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_936 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_936 - - class: SupplierFacility - configs: *id001 - name: Supplier3_937 - - class: SupplierFacility - configs: *id002 - name: Supplier1_937 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_937 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_937 - - class: SupplierFacility - configs: *id001 - name: Supplier3_938 - - class: SupplierFacility - configs: *id002 - name: Supplier1_938 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_938 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_938 - - class: SupplierFacility - configs: *id001 - name: Supplier3_939 - - class: SupplierFacility - configs: *id002 - name: Supplier1_939 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_939 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_939 - - class: SupplierFacility - configs: *id001 - name: Supplier3_940 - - class: SupplierFacility - configs: *id002 - name: Supplier1_940 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_940 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_940 - - class: SupplierFacility - configs: *id001 - name: Supplier3_941 - - class: SupplierFacility - configs: *id002 - name: Supplier1_941 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_941 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_941 - - class: SupplierFacility - configs: *id001 - name: Supplier3_942 - - class: SupplierFacility - configs: *id002 - name: Supplier1_942 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_942 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_942 - - class: SupplierFacility - configs: *id001 - name: Supplier3_943 - - class: SupplierFacility - configs: *id002 - name: Supplier1_943 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_943 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_943 - - class: SupplierFacility - configs: *id001 - name: Supplier3_944 - - class: SupplierFacility - configs: *id002 - name: Supplier1_944 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_944 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_944 - - class: SupplierFacility - configs: *id001 - name: Supplier3_945 - - class: SupplierFacility - configs: *id002 - name: Supplier1_945 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_945 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_945 - - class: SupplierFacility - configs: *id001 - name: Supplier3_946 - - class: SupplierFacility - configs: *id002 - name: Supplier1_946 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_946 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_946 - - class: SupplierFacility - configs: *id001 - name: Supplier3_947 - - class: SupplierFacility - configs: *id002 - name: Supplier1_947 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_947 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_947 - - class: SupplierFacility - configs: *id001 - name: Supplier3_948 - - class: SupplierFacility - configs: *id002 - name: Supplier1_948 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_948 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_948 - - class: SupplierFacility - configs: *id001 - name: Supplier3_949 - - class: SupplierFacility - configs: *id002 - name: Supplier1_949 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_949 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_949 - - class: SupplierFacility - configs: *id001 - name: Supplier3_950 - - class: SupplierFacility - configs: *id002 - name: Supplier1_950 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_950 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_950 - - class: SupplierFacility - configs: *id001 - name: Supplier3_951 - - class: SupplierFacility - configs: *id002 - name: Supplier1_951 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_951 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_951 - - class: SupplierFacility - configs: *id001 - name: Supplier3_952 - - class: SupplierFacility - configs: *id002 - name: Supplier1_952 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_952 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_952 - - class: SupplierFacility - configs: *id001 - name: Supplier3_953 - - class: SupplierFacility - configs: *id002 - name: Supplier1_953 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_953 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_953 - - class: SupplierFacility - configs: *id001 - name: Supplier3_954 - - class: SupplierFacility - configs: *id002 - name: Supplier1_954 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_954 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_954 - - class: SupplierFacility - configs: *id001 - name: Supplier3_955 - - class: SupplierFacility - configs: *id002 - name: Supplier1_955 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_955 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_955 - - class: SupplierFacility - configs: *id001 - name: Supplier3_956 - - class: SupplierFacility - configs: *id002 - name: Supplier1_956 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_956 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_956 - - class: SupplierFacility - configs: *id001 - name: Supplier3_957 - - class: SupplierFacility - configs: *id002 - name: Supplier1_957 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_957 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_957 - - class: SupplierFacility - configs: *id001 - name: Supplier3_958 - - class: SupplierFacility - configs: *id002 - name: Supplier1_958 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_958 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_958 - - class: SupplierFacility - configs: *id001 - name: Supplier3_959 - - class: SupplierFacility - configs: *id002 - name: Supplier1_959 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_959 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_959 - - class: SupplierFacility - configs: *id001 - name: Supplier3_960 - - class: SupplierFacility - configs: *id002 - name: Supplier1_960 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_960 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_960 - - class: SupplierFacility - configs: *id001 - name: Supplier3_961 - - class: SupplierFacility - configs: *id002 - name: Supplier1_961 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_961 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_961 - - class: SupplierFacility - configs: *id001 - name: Supplier3_962 - - class: SupplierFacility - configs: *id002 - name: Supplier1_962 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_962 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_962 - - class: SupplierFacility - configs: *id001 - name: Supplier3_963 - - class: SupplierFacility - configs: *id002 - name: Supplier1_963 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_963 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_963 - - class: SupplierFacility - configs: *id001 - name: Supplier3_964 - - class: SupplierFacility - configs: *id002 - name: Supplier1_964 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_964 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_964 - - class: SupplierFacility - configs: *id001 - name: Supplier3_965 - - class: SupplierFacility - configs: *id002 - name: Supplier1_965 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_965 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_965 - - class: SupplierFacility - configs: *id001 - name: Supplier3_966 - - class: SupplierFacility - configs: *id002 - name: Supplier1_966 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_966 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_966 - - class: SupplierFacility - configs: *id001 - name: Supplier3_967 - - class: SupplierFacility - configs: *id002 - name: Supplier1_967 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_967 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_967 - - class: SupplierFacility - configs: *id001 - name: Supplier3_968 - - class: SupplierFacility - configs: *id002 - name: Supplier1_968 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_968 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_968 - - class: SupplierFacility - configs: *id001 - name: Supplier3_969 - - class: SupplierFacility - configs: *id002 - name: Supplier1_969 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_969 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_969 - - class: SupplierFacility - configs: *id001 - name: Supplier3_970 - - class: SupplierFacility - configs: *id002 - name: Supplier1_970 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_970 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_970 - - class: SupplierFacility - configs: *id001 - name: Supplier3_971 - - class: SupplierFacility - configs: *id002 - name: Supplier1_971 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_971 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_971 - - class: SupplierFacility - configs: *id001 - name: Supplier3_972 - - class: SupplierFacility - configs: *id002 - name: Supplier1_972 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_972 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_972 - - class: SupplierFacility - configs: *id001 - name: Supplier3_973 - - class: SupplierFacility - configs: *id002 - name: Supplier1_973 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_973 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_973 - - class: SupplierFacility - configs: *id001 - name: Supplier3_974 - - class: SupplierFacility - configs: *id002 - name: Supplier1_974 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_974 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_974 - - class: SupplierFacility - configs: *id001 - name: Supplier3_975 - - class: SupplierFacility - configs: *id002 - name: Supplier1_975 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_975 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_975 - - class: SupplierFacility - configs: *id001 - name: Supplier3_976 - - class: SupplierFacility - configs: *id002 - name: Supplier1_976 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_976 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_976 - - class: SupplierFacility - configs: *id001 - name: Supplier3_977 - - class: SupplierFacility - configs: *id002 - name: Supplier1_977 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_977 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_977 - - class: SupplierFacility - configs: *id001 - name: Supplier3_978 - - class: SupplierFacility - configs: *id002 - name: Supplier1_978 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_978 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_978 - - class: SupplierFacility - configs: *id001 - name: Supplier3_979 - - class: SupplierFacility - configs: *id002 - name: Supplier1_979 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_979 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_979 - - class: SupplierFacility - configs: *id001 - name: Supplier3_980 - - class: SupplierFacility - configs: *id002 - name: Supplier1_980 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_980 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_980 - - class: SupplierFacility - configs: *id001 - name: Supplier3_981 - - class: SupplierFacility - configs: *id002 - name: Supplier1_981 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_981 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_981 - - class: SupplierFacility - configs: *id001 - name: Supplier3_982 - - class: SupplierFacility - configs: *id002 - name: Supplier1_982 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_982 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_982 - - class: SupplierFacility - configs: *id001 - name: Supplier3_983 - - class: SupplierFacility - configs: *id002 - name: Supplier1_983 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_983 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_983 - - class: SupplierFacility - configs: *id001 - name: Supplier3_984 - - class: SupplierFacility - configs: *id002 - name: Supplier1_984 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_984 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_984 - - class: SupplierFacility - configs: *id001 - name: Supplier3_985 - - class: SupplierFacility - configs: *id002 - name: Supplier1_985 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_985 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_985 - - class: SupplierFacility - configs: *id001 - name: Supplier3_986 - - class: SupplierFacility - configs: *id002 - name: Supplier1_986 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_986 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_986 - - class: SupplierFacility - configs: *id001 - name: Supplier3_987 - - class: SupplierFacility - configs: *id002 - name: Supplier1_987 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_987 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_987 - - class: SupplierFacility - configs: *id001 - name: Supplier3_988 - - class: SupplierFacility - configs: *id002 - name: Supplier1_988 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_988 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_988 - - class: SupplierFacility - configs: *id001 - name: Supplier3_989 - - class: SupplierFacility - configs: *id002 - name: Supplier1_989 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_989 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_989 - - class: SupplierFacility - configs: *id001 - name: Supplier3_990 - - class: SupplierFacility - configs: *id002 - name: Supplier1_990 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_990 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_990 - - class: SupplierFacility - configs: *id001 - name: Supplier3_991 - - class: SupplierFacility - configs: *id002 - name: Supplier1_991 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_991 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_991 - - class: SupplierFacility - configs: *id001 - name: Supplier3_992 - - class: SupplierFacility - configs: *id002 - name: Supplier1_992 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_992 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_992 - - class: SupplierFacility - configs: *id001 - name: Supplier3_993 - - class: SupplierFacility - configs: *id002 - name: Supplier1_993 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_993 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_993 - - class: SupplierFacility - configs: *id001 - name: Supplier3_994 - - class: SupplierFacility - configs: *id002 - name: Supplier1_994 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_994 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_994 - - class: SupplierFacility - configs: *id001 - name: Supplier3_995 - - class: SupplierFacility - configs: *id002 - name: Supplier1_995 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_995 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_995 - - class: SupplierFacility - configs: *id001 - name: Supplier3_996 - - class: SupplierFacility - configs: *id002 - name: Supplier1_996 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_996 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_996 - - class: SupplierFacility - configs: *id001 - name: Supplier3_997 - - class: SupplierFacility - configs: *id002 - name: Supplier1_997 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_997 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_997 - - class: SupplierFacility - configs: *id001 - name: Supplier3_998 - - class: SupplierFacility - configs: *id002 - name: Supplier1_998 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_998 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_998 - grid: - blocks: - railroad: - - - 10 - - 10 - - - 10 - - 11 - - - 10 - - 12 - - - 11 - - 12 - facilities: - ChaoShiFa01: - - 10 - - 18 - Supplier1: - - 0 - - 0 - Supplier3: - - 3 - - 3 - Warehouse1: - - 6 - - 6 - size: - - 20 - - 20 - skus: - - bom: - sku3: 10 - id: 1 - name: sku1 - output_units_per_lot: 1 - - id: 2 - name: sku2 - output_units_per_lot: 1 - - id: 3 - name: sku3 - output_units_per_lot: 1 - step_mode: facility - topology: - ChaoShiFa01: - sku1: - - Supplier1 - sku3: - - Supplier3 - Supplier1: - sku3: - - Supplier3 - Warehouse1: - sku1: - - Supplier1 - sku3: - - Supplier3 diff --git a/maro/simulator/scenarios/supply_chain/topologies/perf_4x400_facilities/config.yml b/maro/simulator/scenarios/supply_chain/topologies/perf_4x400_facilities/config.yml deleted file mode 100644 index 95886b726..000000000 --- a/maro/simulator/scenarios/supply_chain/topologies/perf_4x400_facilities/config.yml +++ /dev/null @@ -1,4963 +0,0 @@ -base: '' -world: - action_steps: 1 - facilities: - - class: SupplierFacility - configs: &id001 - distribution: - data: - unit_price: 10 - skus: - sku3: - cost": 10 - delay_order_penalty: 10 - init_in_stock: 100 - price": 10 - product_unit_cost: 1 - production_rate: 0 - type: production - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - transports: - - class: TransportUnit - data: - patient: 100 - - class: TransportUnit - data: - patient: 100 - name: Supplier3 - - class: SupplierFacility - configs: &id002 - distribution: - data: - unit_price: 10 - skus: - sku1: - cost: 10 - delay_order_penalty: 10 - init_in_stock: 100 - price: 100 - product_unit_cost: 1 - production_rate: 0 - type: production - sku3: - cost: 10 - delay_order_penalty: 10 - init_in_stock: 100 - price: 100 - production_rate: 200 - type: material - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - transports: - - class: TransportUnit - data: - patient: 100 - - class: TransportUnit - data: - patient: 100 - name: Supplier1 - - class: WarehouseFacility - configs: &id003 - distribution: - data: - unit_price: 10 - skus: - sku1: - delay_order_penalty: 10 - init_stock: 1000 - price: 100 - sku2: - delay_order_penalty: 10 - init_stock: 1000 - price: 100 - sku3: - delay_order_penalty: 10 - init_stock: 1000 - price: 100 - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - transports: - - class: TransportUnit - data: - patient: 100 - unit_transport_cost: 1 - - class: TransportUnit - data: - patient: 100 - unit_transport_cost: 1 - name: Warehouse1 - - class: RetailerFacility - configs: &id004 - skus: - sku1: - backlog_ratio: 0.1 - cost: 10 - init_in_stock: 100 - price: 300 - sale_gamma: 100 - sku2: - backlog_ratio: 0.1 - cost: 10 - init_in_stock: 100 - price: 100 - sale_gamma: 100 - sku3: - backlog_ratio: 0.1 - cost: 10 - init_in_stock: 100 - price: 200 - sale_gamma: 100 - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - name: ChaoShiFa01 - - class: SupplierFacility - configs: *id001 - name: Supplier3_0 - - class: SupplierFacility - configs: *id002 - name: Supplier1_0 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_0 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_0 - - class: SupplierFacility - configs: *id001 - name: Supplier3_1 - - class: SupplierFacility - configs: *id002 - name: Supplier1_1 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_1 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_1 - - class: SupplierFacility - configs: *id001 - name: Supplier3_2 - - class: SupplierFacility - configs: *id002 - name: Supplier1_2 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_2 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_2 - - class: SupplierFacility - configs: *id001 - name: Supplier3_3 - - class: SupplierFacility - configs: *id002 - name: Supplier1_3 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_3 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_3 - - class: SupplierFacility - configs: *id001 - name: Supplier3_4 - - class: SupplierFacility - configs: *id002 - name: Supplier1_4 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_4 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_4 - - class: SupplierFacility - configs: *id001 - name: Supplier3_5 - - class: SupplierFacility - configs: *id002 - name: Supplier1_5 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_5 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_5 - - class: SupplierFacility - configs: *id001 - name: Supplier3_6 - - class: SupplierFacility - configs: *id002 - name: Supplier1_6 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_6 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_6 - - class: SupplierFacility - configs: *id001 - name: Supplier3_7 - - class: SupplierFacility - configs: *id002 - name: Supplier1_7 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_7 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_7 - - class: SupplierFacility - configs: *id001 - name: Supplier3_8 - - class: SupplierFacility - configs: *id002 - name: Supplier1_8 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_8 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_8 - - class: SupplierFacility - configs: *id001 - name: Supplier3_9 - - class: SupplierFacility - configs: *id002 - name: Supplier1_9 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_9 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_9 - - class: SupplierFacility - configs: *id001 - name: Supplier3_10 - - class: SupplierFacility - configs: *id002 - name: Supplier1_10 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_10 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_10 - - class: SupplierFacility - configs: *id001 - name: Supplier3_11 - - class: SupplierFacility - configs: *id002 - name: Supplier1_11 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_11 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_11 - - class: SupplierFacility - configs: *id001 - name: Supplier3_12 - - class: SupplierFacility - configs: *id002 - name: Supplier1_12 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_12 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_12 - - class: SupplierFacility - configs: *id001 - name: Supplier3_13 - - class: SupplierFacility - configs: *id002 - name: Supplier1_13 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_13 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_13 - - class: SupplierFacility - configs: *id001 - name: Supplier3_14 - - class: SupplierFacility - configs: *id002 - name: Supplier1_14 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_14 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_14 - - class: SupplierFacility - configs: *id001 - name: Supplier3_15 - - class: SupplierFacility - configs: *id002 - name: Supplier1_15 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_15 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_15 - - class: SupplierFacility - configs: *id001 - name: Supplier3_16 - - class: SupplierFacility - configs: *id002 - name: Supplier1_16 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_16 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_16 - - class: SupplierFacility - configs: *id001 - name: Supplier3_17 - - class: SupplierFacility - configs: *id002 - name: Supplier1_17 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_17 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_17 - - class: SupplierFacility - configs: *id001 - name: Supplier3_18 - - class: SupplierFacility - configs: *id002 - name: Supplier1_18 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_18 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_18 - - class: SupplierFacility - configs: *id001 - name: Supplier3_19 - - class: SupplierFacility - configs: *id002 - name: Supplier1_19 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_19 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_19 - - class: SupplierFacility - configs: *id001 - name: Supplier3_20 - - class: SupplierFacility - configs: *id002 - name: Supplier1_20 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_20 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_20 - - class: SupplierFacility - configs: *id001 - name: Supplier3_21 - - class: SupplierFacility - configs: *id002 - name: Supplier1_21 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_21 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_21 - - class: SupplierFacility - configs: *id001 - name: Supplier3_22 - - class: SupplierFacility - configs: *id002 - name: Supplier1_22 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_22 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_22 - - class: SupplierFacility - configs: *id001 - name: Supplier3_23 - - class: SupplierFacility - configs: *id002 - name: Supplier1_23 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_23 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_23 - - class: SupplierFacility - configs: *id001 - name: Supplier3_24 - - class: SupplierFacility - configs: *id002 - name: Supplier1_24 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_24 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_24 - - class: SupplierFacility - configs: *id001 - name: Supplier3_25 - - class: SupplierFacility - configs: *id002 - name: Supplier1_25 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_25 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_25 - - class: SupplierFacility - configs: *id001 - name: Supplier3_26 - - class: SupplierFacility - configs: *id002 - name: Supplier1_26 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_26 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_26 - - class: SupplierFacility - configs: *id001 - name: Supplier3_27 - - class: SupplierFacility - configs: *id002 - name: Supplier1_27 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_27 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_27 - - class: SupplierFacility - configs: *id001 - name: Supplier3_28 - - class: SupplierFacility - configs: *id002 - name: Supplier1_28 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_28 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_28 - - class: SupplierFacility - configs: *id001 - name: Supplier3_29 - - class: SupplierFacility - configs: *id002 - name: Supplier1_29 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_29 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_29 - - class: SupplierFacility - configs: *id001 - name: Supplier3_30 - - class: SupplierFacility - configs: *id002 - name: Supplier1_30 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_30 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_30 - - class: SupplierFacility - configs: *id001 - name: Supplier3_31 - - class: SupplierFacility - configs: *id002 - name: Supplier1_31 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_31 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_31 - - class: SupplierFacility - configs: *id001 - name: Supplier3_32 - - class: SupplierFacility - configs: *id002 - name: Supplier1_32 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_32 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_32 - - class: SupplierFacility - configs: *id001 - name: Supplier3_33 - - class: SupplierFacility - configs: *id002 - name: Supplier1_33 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_33 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_33 - - class: SupplierFacility - configs: *id001 - name: Supplier3_34 - - class: SupplierFacility - configs: *id002 - name: Supplier1_34 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_34 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_34 - - class: SupplierFacility - configs: *id001 - name: Supplier3_35 - - class: SupplierFacility - configs: *id002 - name: Supplier1_35 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_35 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_35 - - class: SupplierFacility - configs: *id001 - name: Supplier3_36 - - class: SupplierFacility - configs: *id002 - name: Supplier1_36 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_36 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_36 - - class: SupplierFacility - configs: *id001 - name: Supplier3_37 - - class: SupplierFacility - configs: *id002 - name: Supplier1_37 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_37 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_37 - - class: SupplierFacility - configs: *id001 - name: Supplier3_38 - - class: SupplierFacility - configs: *id002 - name: Supplier1_38 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_38 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_38 - - class: SupplierFacility - configs: *id001 - name: Supplier3_39 - - class: SupplierFacility - configs: *id002 - name: Supplier1_39 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_39 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_39 - - class: SupplierFacility - configs: *id001 - name: Supplier3_40 - - class: SupplierFacility - configs: *id002 - name: Supplier1_40 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_40 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_40 - - class: SupplierFacility - configs: *id001 - name: Supplier3_41 - - class: SupplierFacility - configs: *id002 - name: Supplier1_41 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_41 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_41 - - class: SupplierFacility - configs: *id001 - name: Supplier3_42 - - class: SupplierFacility - configs: *id002 - name: Supplier1_42 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_42 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_42 - - class: SupplierFacility - configs: *id001 - name: Supplier3_43 - - class: SupplierFacility - configs: *id002 - name: Supplier1_43 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_43 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_43 - - class: SupplierFacility - configs: *id001 - name: Supplier3_44 - - class: SupplierFacility - configs: *id002 - name: Supplier1_44 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_44 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_44 - - class: SupplierFacility - configs: *id001 - name: Supplier3_45 - - class: SupplierFacility - configs: *id002 - name: Supplier1_45 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_45 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_45 - - class: SupplierFacility - configs: *id001 - name: Supplier3_46 - - class: SupplierFacility - configs: *id002 - name: Supplier1_46 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_46 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_46 - - class: SupplierFacility - configs: *id001 - name: Supplier3_47 - - class: SupplierFacility - configs: *id002 - name: Supplier1_47 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_47 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_47 - - class: SupplierFacility - configs: *id001 - name: Supplier3_48 - - class: SupplierFacility - configs: *id002 - name: Supplier1_48 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_48 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_48 - - class: SupplierFacility - configs: *id001 - name: Supplier3_49 - - class: SupplierFacility - configs: *id002 - name: Supplier1_49 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_49 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_49 - - class: SupplierFacility - configs: *id001 - name: Supplier3_50 - - class: SupplierFacility - configs: *id002 - name: Supplier1_50 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_50 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_50 - - class: SupplierFacility - configs: *id001 - name: Supplier3_51 - - class: SupplierFacility - configs: *id002 - name: Supplier1_51 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_51 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_51 - - class: SupplierFacility - configs: *id001 - name: Supplier3_52 - - class: SupplierFacility - configs: *id002 - name: Supplier1_52 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_52 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_52 - - class: SupplierFacility - configs: *id001 - name: Supplier3_53 - - class: SupplierFacility - configs: *id002 - name: Supplier1_53 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_53 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_53 - - class: SupplierFacility - configs: *id001 - name: Supplier3_54 - - class: SupplierFacility - configs: *id002 - name: Supplier1_54 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_54 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_54 - - class: SupplierFacility - configs: *id001 - name: Supplier3_55 - - class: SupplierFacility - configs: *id002 - name: Supplier1_55 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_55 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_55 - - class: SupplierFacility - configs: *id001 - name: Supplier3_56 - - class: SupplierFacility - configs: *id002 - name: Supplier1_56 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_56 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_56 - - class: SupplierFacility - configs: *id001 - name: Supplier3_57 - - class: SupplierFacility - configs: *id002 - name: Supplier1_57 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_57 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_57 - - class: SupplierFacility - configs: *id001 - name: Supplier3_58 - - class: SupplierFacility - configs: *id002 - name: Supplier1_58 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_58 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_58 - - class: SupplierFacility - configs: *id001 - name: Supplier3_59 - - class: SupplierFacility - configs: *id002 - name: Supplier1_59 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_59 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_59 - - class: SupplierFacility - configs: *id001 - name: Supplier3_60 - - class: SupplierFacility - configs: *id002 - name: Supplier1_60 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_60 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_60 - - class: SupplierFacility - configs: *id001 - name: Supplier3_61 - - class: SupplierFacility - configs: *id002 - name: Supplier1_61 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_61 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_61 - - class: SupplierFacility - configs: *id001 - name: Supplier3_62 - - class: SupplierFacility - configs: *id002 - name: Supplier1_62 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_62 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_62 - - class: SupplierFacility - configs: *id001 - name: Supplier3_63 - - class: SupplierFacility - configs: *id002 - name: Supplier1_63 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_63 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_63 - - class: SupplierFacility - configs: *id001 - name: Supplier3_64 - - class: SupplierFacility - configs: *id002 - name: Supplier1_64 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_64 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_64 - - class: SupplierFacility - configs: *id001 - name: Supplier3_65 - - class: SupplierFacility - configs: *id002 - name: Supplier1_65 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_65 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_65 - - class: SupplierFacility - configs: *id001 - name: Supplier3_66 - - class: SupplierFacility - configs: *id002 - name: Supplier1_66 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_66 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_66 - - class: SupplierFacility - configs: *id001 - name: Supplier3_67 - - class: SupplierFacility - configs: *id002 - name: Supplier1_67 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_67 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_67 - - class: SupplierFacility - configs: *id001 - name: Supplier3_68 - - class: SupplierFacility - configs: *id002 - name: Supplier1_68 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_68 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_68 - - class: SupplierFacility - configs: *id001 - name: Supplier3_69 - - class: SupplierFacility - configs: *id002 - name: Supplier1_69 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_69 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_69 - - class: SupplierFacility - configs: *id001 - name: Supplier3_70 - - class: SupplierFacility - configs: *id002 - name: Supplier1_70 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_70 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_70 - - class: SupplierFacility - configs: *id001 - name: Supplier3_71 - - class: SupplierFacility - configs: *id002 - name: Supplier1_71 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_71 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_71 - - class: SupplierFacility - configs: *id001 - name: Supplier3_72 - - class: SupplierFacility - configs: *id002 - name: Supplier1_72 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_72 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_72 - - class: SupplierFacility - configs: *id001 - name: Supplier3_73 - - class: SupplierFacility - configs: *id002 - name: Supplier1_73 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_73 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_73 - - class: SupplierFacility - configs: *id001 - name: Supplier3_74 - - class: SupplierFacility - configs: *id002 - name: Supplier1_74 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_74 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_74 - - class: SupplierFacility - configs: *id001 - name: Supplier3_75 - - class: SupplierFacility - configs: *id002 - name: Supplier1_75 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_75 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_75 - - class: SupplierFacility - configs: *id001 - name: Supplier3_76 - - class: SupplierFacility - configs: *id002 - name: Supplier1_76 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_76 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_76 - - class: SupplierFacility - configs: *id001 - name: Supplier3_77 - - class: SupplierFacility - configs: *id002 - name: Supplier1_77 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_77 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_77 - - class: SupplierFacility - configs: *id001 - name: Supplier3_78 - - class: SupplierFacility - configs: *id002 - name: Supplier1_78 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_78 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_78 - - class: SupplierFacility - configs: *id001 - name: Supplier3_79 - - class: SupplierFacility - configs: *id002 - name: Supplier1_79 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_79 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_79 - - class: SupplierFacility - configs: *id001 - name: Supplier3_80 - - class: SupplierFacility - configs: *id002 - name: Supplier1_80 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_80 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_80 - - class: SupplierFacility - configs: *id001 - name: Supplier3_81 - - class: SupplierFacility - configs: *id002 - name: Supplier1_81 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_81 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_81 - - class: SupplierFacility - configs: *id001 - name: Supplier3_82 - - class: SupplierFacility - configs: *id002 - name: Supplier1_82 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_82 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_82 - - class: SupplierFacility - configs: *id001 - name: Supplier3_83 - - class: SupplierFacility - configs: *id002 - name: Supplier1_83 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_83 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_83 - - class: SupplierFacility - configs: *id001 - name: Supplier3_84 - - class: SupplierFacility - configs: *id002 - name: Supplier1_84 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_84 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_84 - - class: SupplierFacility - configs: *id001 - name: Supplier3_85 - - class: SupplierFacility - configs: *id002 - name: Supplier1_85 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_85 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_85 - - class: SupplierFacility - configs: *id001 - name: Supplier3_86 - - class: SupplierFacility - configs: *id002 - name: Supplier1_86 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_86 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_86 - - class: SupplierFacility - configs: *id001 - name: Supplier3_87 - - class: SupplierFacility - configs: *id002 - name: Supplier1_87 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_87 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_87 - - class: SupplierFacility - configs: *id001 - name: Supplier3_88 - - class: SupplierFacility - configs: *id002 - name: Supplier1_88 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_88 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_88 - - class: SupplierFacility - configs: *id001 - name: Supplier3_89 - - class: SupplierFacility - configs: *id002 - name: Supplier1_89 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_89 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_89 - - class: SupplierFacility - configs: *id001 - name: Supplier3_90 - - class: SupplierFacility - configs: *id002 - name: Supplier1_90 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_90 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_90 - - class: SupplierFacility - configs: *id001 - name: Supplier3_91 - - class: SupplierFacility - configs: *id002 - name: Supplier1_91 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_91 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_91 - - class: SupplierFacility - configs: *id001 - name: Supplier3_92 - - class: SupplierFacility - configs: *id002 - name: Supplier1_92 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_92 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_92 - - class: SupplierFacility - configs: *id001 - name: Supplier3_93 - - class: SupplierFacility - configs: *id002 - name: Supplier1_93 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_93 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_93 - - class: SupplierFacility - configs: *id001 - name: Supplier3_94 - - class: SupplierFacility - configs: *id002 - name: Supplier1_94 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_94 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_94 - - class: SupplierFacility - configs: *id001 - name: Supplier3_95 - - class: SupplierFacility - configs: *id002 - name: Supplier1_95 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_95 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_95 - - class: SupplierFacility - configs: *id001 - name: Supplier3_96 - - class: SupplierFacility - configs: *id002 - name: Supplier1_96 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_96 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_96 - - class: SupplierFacility - configs: *id001 - name: Supplier3_97 - - class: SupplierFacility - configs: *id002 - name: Supplier1_97 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_97 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_97 - - class: SupplierFacility - configs: *id001 - name: Supplier3_98 - - class: SupplierFacility - configs: *id002 - name: Supplier1_98 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_98 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_98 - - class: SupplierFacility - configs: *id001 - name: Supplier3_99 - - class: SupplierFacility - configs: *id002 - name: Supplier1_99 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_99 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_99 - - class: SupplierFacility - configs: *id001 - name: Supplier3_100 - - class: SupplierFacility - configs: *id002 - name: Supplier1_100 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_100 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_100 - - class: SupplierFacility - configs: *id001 - name: Supplier3_101 - - class: SupplierFacility - configs: *id002 - name: Supplier1_101 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_101 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_101 - - class: SupplierFacility - configs: *id001 - name: Supplier3_102 - - class: SupplierFacility - configs: *id002 - name: Supplier1_102 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_102 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_102 - - class: SupplierFacility - configs: *id001 - name: Supplier3_103 - - class: SupplierFacility - configs: *id002 - name: Supplier1_103 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_103 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_103 - - class: SupplierFacility - configs: *id001 - name: Supplier3_104 - - class: SupplierFacility - configs: *id002 - name: Supplier1_104 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_104 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_104 - - class: SupplierFacility - configs: *id001 - name: Supplier3_105 - - class: SupplierFacility - configs: *id002 - name: Supplier1_105 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_105 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_105 - - class: SupplierFacility - configs: *id001 - name: Supplier3_106 - - class: SupplierFacility - configs: *id002 - name: Supplier1_106 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_106 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_106 - - class: SupplierFacility - configs: *id001 - name: Supplier3_107 - - class: SupplierFacility - configs: *id002 - name: Supplier1_107 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_107 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_107 - - class: SupplierFacility - configs: *id001 - name: Supplier3_108 - - class: SupplierFacility - configs: *id002 - name: Supplier1_108 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_108 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_108 - - class: SupplierFacility - configs: *id001 - name: Supplier3_109 - - class: SupplierFacility - configs: *id002 - name: Supplier1_109 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_109 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_109 - - class: SupplierFacility - configs: *id001 - name: Supplier3_110 - - class: SupplierFacility - configs: *id002 - name: Supplier1_110 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_110 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_110 - - class: SupplierFacility - configs: *id001 - name: Supplier3_111 - - class: SupplierFacility - configs: *id002 - name: Supplier1_111 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_111 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_111 - - class: SupplierFacility - configs: *id001 - name: Supplier3_112 - - class: SupplierFacility - configs: *id002 - name: Supplier1_112 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_112 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_112 - - class: SupplierFacility - configs: *id001 - name: Supplier3_113 - - class: SupplierFacility - configs: *id002 - name: Supplier1_113 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_113 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_113 - - class: SupplierFacility - configs: *id001 - name: Supplier3_114 - - class: SupplierFacility - configs: *id002 - name: Supplier1_114 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_114 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_114 - - class: SupplierFacility - configs: *id001 - name: Supplier3_115 - - class: SupplierFacility - configs: *id002 - name: Supplier1_115 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_115 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_115 - - class: SupplierFacility - configs: *id001 - name: Supplier3_116 - - class: SupplierFacility - configs: *id002 - name: Supplier1_116 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_116 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_116 - - class: SupplierFacility - configs: *id001 - name: Supplier3_117 - - class: SupplierFacility - configs: *id002 - name: Supplier1_117 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_117 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_117 - - class: SupplierFacility - configs: *id001 - name: Supplier3_118 - - class: SupplierFacility - configs: *id002 - name: Supplier1_118 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_118 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_118 - - class: SupplierFacility - configs: *id001 - name: Supplier3_119 - - class: SupplierFacility - configs: *id002 - name: Supplier1_119 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_119 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_119 - - class: SupplierFacility - configs: *id001 - name: Supplier3_120 - - class: SupplierFacility - configs: *id002 - name: Supplier1_120 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_120 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_120 - - class: SupplierFacility - configs: *id001 - name: Supplier3_121 - - class: SupplierFacility - configs: *id002 - name: Supplier1_121 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_121 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_121 - - class: SupplierFacility - configs: *id001 - name: Supplier3_122 - - class: SupplierFacility - configs: *id002 - name: Supplier1_122 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_122 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_122 - - class: SupplierFacility - configs: *id001 - name: Supplier3_123 - - class: SupplierFacility - configs: *id002 - name: Supplier1_123 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_123 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_123 - - class: SupplierFacility - configs: *id001 - name: Supplier3_124 - - class: SupplierFacility - configs: *id002 - name: Supplier1_124 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_124 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_124 - - class: SupplierFacility - configs: *id001 - name: Supplier3_125 - - class: SupplierFacility - configs: *id002 - name: Supplier1_125 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_125 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_125 - - class: SupplierFacility - configs: *id001 - name: Supplier3_126 - - class: SupplierFacility - configs: *id002 - name: Supplier1_126 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_126 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_126 - - class: SupplierFacility - configs: *id001 - name: Supplier3_127 - - class: SupplierFacility - configs: *id002 - name: Supplier1_127 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_127 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_127 - - class: SupplierFacility - configs: *id001 - name: Supplier3_128 - - class: SupplierFacility - configs: *id002 - name: Supplier1_128 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_128 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_128 - - class: SupplierFacility - configs: *id001 - name: Supplier3_129 - - class: SupplierFacility - configs: *id002 - name: Supplier1_129 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_129 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_129 - - class: SupplierFacility - configs: *id001 - name: Supplier3_130 - - class: SupplierFacility - configs: *id002 - name: Supplier1_130 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_130 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_130 - - class: SupplierFacility - configs: *id001 - name: Supplier3_131 - - class: SupplierFacility - configs: *id002 - name: Supplier1_131 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_131 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_131 - - class: SupplierFacility - configs: *id001 - name: Supplier3_132 - - class: SupplierFacility - configs: *id002 - name: Supplier1_132 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_132 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_132 - - class: SupplierFacility - configs: *id001 - name: Supplier3_133 - - class: SupplierFacility - configs: *id002 - name: Supplier1_133 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_133 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_133 - - class: SupplierFacility - configs: *id001 - name: Supplier3_134 - - class: SupplierFacility - configs: *id002 - name: Supplier1_134 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_134 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_134 - - class: SupplierFacility - configs: *id001 - name: Supplier3_135 - - class: SupplierFacility - configs: *id002 - name: Supplier1_135 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_135 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_135 - - class: SupplierFacility - configs: *id001 - name: Supplier3_136 - - class: SupplierFacility - configs: *id002 - name: Supplier1_136 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_136 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_136 - - class: SupplierFacility - configs: *id001 - name: Supplier3_137 - - class: SupplierFacility - configs: *id002 - name: Supplier1_137 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_137 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_137 - - class: SupplierFacility - configs: *id001 - name: Supplier3_138 - - class: SupplierFacility - configs: *id002 - name: Supplier1_138 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_138 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_138 - - class: SupplierFacility - configs: *id001 - name: Supplier3_139 - - class: SupplierFacility - configs: *id002 - name: Supplier1_139 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_139 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_139 - - class: SupplierFacility - configs: *id001 - name: Supplier3_140 - - class: SupplierFacility - configs: *id002 - name: Supplier1_140 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_140 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_140 - - class: SupplierFacility - configs: *id001 - name: Supplier3_141 - - class: SupplierFacility - configs: *id002 - name: Supplier1_141 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_141 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_141 - - class: SupplierFacility - configs: *id001 - name: Supplier3_142 - - class: SupplierFacility - configs: *id002 - name: Supplier1_142 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_142 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_142 - - class: SupplierFacility - configs: *id001 - name: Supplier3_143 - - class: SupplierFacility - configs: *id002 - name: Supplier1_143 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_143 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_143 - - class: SupplierFacility - configs: *id001 - name: Supplier3_144 - - class: SupplierFacility - configs: *id002 - name: Supplier1_144 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_144 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_144 - - class: SupplierFacility - configs: *id001 - name: Supplier3_145 - - class: SupplierFacility - configs: *id002 - name: Supplier1_145 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_145 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_145 - - class: SupplierFacility - configs: *id001 - name: Supplier3_146 - - class: SupplierFacility - configs: *id002 - name: Supplier1_146 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_146 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_146 - - class: SupplierFacility - configs: *id001 - name: Supplier3_147 - - class: SupplierFacility - configs: *id002 - name: Supplier1_147 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_147 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_147 - - class: SupplierFacility - configs: *id001 - name: Supplier3_148 - - class: SupplierFacility - configs: *id002 - name: Supplier1_148 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_148 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_148 - - class: SupplierFacility - configs: *id001 - name: Supplier3_149 - - class: SupplierFacility - configs: *id002 - name: Supplier1_149 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_149 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_149 - - class: SupplierFacility - configs: *id001 - name: Supplier3_150 - - class: SupplierFacility - configs: *id002 - name: Supplier1_150 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_150 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_150 - - class: SupplierFacility - configs: *id001 - name: Supplier3_151 - - class: SupplierFacility - configs: *id002 - name: Supplier1_151 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_151 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_151 - - class: SupplierFacility - configs: *id001 - name: Supplier3_152 - - class: SupplierFacility - configs: *id002 - name: Supplier1_152 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_152 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_152 - - class: SupplierFacility - configs: *id001 - name: Supplier3_153 - - class: SupplierFacility - configs: *id002 - name: Supplier1_153 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_153 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_153 - - class: SupplierFacility - configs: *id001 - name: Supplier3_154 - - class: SupplierFacility - configs: *id002 - name: Supplier1_154 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_154 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_154 - - class: SupplierFacility - configs: *id001 - name: Supplier3_155 - - class: SupplierFacility - configs: *id002 - name: Supplier1_155 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_155 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_155 - - class: SupplierFacility - configs: *id001 - name: Supplier3_156 - - class: SupplierFacility - configs: *id002 - name: Supplier1_156 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_156 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_156 - - class: SupplierFacility - configs: *id001 - name: Supplier3_157 - - class: SupplierFacility - configs: *id002 - name: Supplier1_157 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_157 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_157 - - class: SupplierFacility - configs: *id001 - name: Supplier3_158 - - class: SupplierFacility - configs: *id002 - name: Supplier1_158 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_158 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_158 - - class: SupplierFacility - configs: *id001 - name: Supplier3_159 - - class: SupplierFacility - configs: *id002 - name: Supplier1_159 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_159 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_159 - - class: SupplierFacility - configs: *id001 - name: Supplier3_160 - - class: SupplierFacility - configs: *id002 - name: Supplier1_160 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_160 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_160 - - class: SupplierFacility - configs: *id001 - name: Supplier3_161 - - class: SupplierFacility - configs: *id002 - name: Supplier1_161 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_161 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_161 - - class: SupplierFacility - configs: *id001 - name: Supplier3_162 - - class: SupplierFacility - configs: *id002 - name: Supplier1_162 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_162 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_162 - - class: SupplierFacility - configs: *id001 - name: Supplier3_163 - - class: SupplierFacility - configs: *id002 - name: Supplier1_163 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_163 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_163 - - class: SupplierFacility - configs: *id001 - name: Supplier3_164 - - class: SupplierFacility - configs: *id002 - name: Supplier1_164 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_164 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_164 - - class: SupplierFacility - configs: *id001 - name: Supplier3_165 - - class: SupplierFacility - configs: *id002 - name: Supplier1_165 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_165 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_165 - - class: SupplierFacility - configs: *id001 - name: Supplier3_166 - - class: SupplierFacility - configs: *id002 - name: Supplier1_166 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_166 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_166 - - class: SupplierFacility - configs: *id001 - name: Supplier3_167 - - class: SupplierFacility - configs: *id002 - name: Supplier1_167 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_167 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_167 - - class: SupplierFacility - configs: *id001 - name: Supplier3_168 - - class: SupplierFacility - configs: *id002 - name: Supplier1_168 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_168 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_168 - - class: SupplierFacility - configs: *id001 - name: Supplier3_169 - - class: SupplierFacility - configs: *id002 - name: Supplier1_169 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_169 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_169 - - class: SupplierFacility - configs: *id001 - name: Supplier3_170 - - class: SupplierFacility - configs: *id002 - name: Supplier1_170 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_170 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_170 - - class: SupplierFacility - configs: *id001 - name: Supplier3_171 - - class: SupplierFacility - configs: *id002 - name: Supplier1_171 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_171 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_171 - - class: SupplierFacility - configs: *id001 - name: Supplier3_172 - - class: SupplierFacility - configs: *id002 - name: Supplier1_172 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_172 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_172 - - class: SupplierFacility - configs: *id001 - name: Supplier3_173 - - class: SupplierFacility - configs: *id002 - name: Supplier1_173 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_173 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_173 - - class: SupplierFacility - configs: *id001 - name: Supplier3_174 - - class: SupplierFacility - configs: *id002 - name: Supplier1_174 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_174 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_174 - - class: SupplierFacility - configs: *id001 - name: Supplier3_175 - - class: SupplierFacility - configs: *id002 - name: Supplier1_175 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_175 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_175 - - class: SupplierFacility - configs: *id001 - name: Supplier3_176 - - class: SupplierFacility - configs: *id002 - name: Supplier1_176 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_176 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_176 - - class: SupplierFacility - configs: *id001 - name: Supplier3_177 - - class: SupplierFacility - configs: *id002 - name: Supplier1_177 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_177 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_177 - - class: SupplierFacility - configs: *id001 - name: Supplier3_178 - - class: SupplierFacility - configs: *id002 - name: Supplier1_178 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_178 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_178 - - class: SupplierFacility - configs: *id001 - name: Supplier3_179 - - class: SupplierFacility - configs: *id002 - name: Supplier1_179 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_179 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_179 - - class: SupplierFacility - configs: *id001 - name: Supplier3_180 - - class: SupplierFacility - configs: *id002 - name: Supplier1_180 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_180 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_180 - - class: SupplierFacility - configs: *id001 - name: Supplier3_181 - - class: SupplierFacility - configs: *id002 - name: Supplier1_181 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_181 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_181 - - class: SupplierFacility - configs: *id001 - name: Supplier3_182 - - class: SupplierFacility - configs: *id002 - name: Supplier1_182 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_182 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_182 - - class: SupplierFacility - configs: *id001 - name: Supplier3_183 - - class: SupplierFacility - configs: *id002 - name: Supplier1_183 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_183 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_183 - - class: SupplierFacility - configs: *id001 - name: Supplier3_184 - - class: SupplierFacility - configs: *id002 - name: Supplier1_184 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_184 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_184 - - class: SupplierFacility - configs: *id001 - name: Supplier3_185 - - class: SupplierFacility - configs: *id002 - name: Supplier1_185 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_185 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_185 - - class: SupplierFacility - configs: *id001 - name: Supplier3_186 - - class: SupplierFacility - configs: *id002 - name: Supplier1_186 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_186 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_186 - - class: SupplierFacility - configs: *id001 - name: Supplier3_187 - - class: SupplierFacility - configs: *id002 - name: Supplier1_187 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_187 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_187 - - class: SupplierFacility - configs: *id001 - name: Supplier3_188 - - class: SupplierFacility - configs: *id002 - name: Supplier1_188 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_188 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_188 - - class: SupplierFacility - configs: *id001 - name: Supplier3_189 - - class: SupplierFacility - configs: *id002 - name: Supplier1_189 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_189 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_189 - - class: SupplierFacility - configs: *id001 - name: Supplier3_190 - - class: SupplierFacility - configs: *id002 - name: Supplier1_190 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_190 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_190 - - class: SupplierFacility - configs: *id001 - name: Supplier3_191 - - class: SupplierFacility - configs: *id002 - name: Supplier1_191 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_191 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_191 - - class: SupplierFacility - configs: *id001 - name: Supplier3_192 - - class: SupplierFacility - configs: *id002 - name: Supplier1_192 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_192 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_192 - - class: SupplierFacility - configs: *id001 - name: Supplier3_193 - - class: SupplierFacility - configs: *id002 - name: Supplier1_193 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_193 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_193 - - class: SupplierFacility - configs: *id001 - name: Supplier3_194 - - class: SupplierFacility - configs: *id002 - name: Supplier1_194 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_194 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_194 - - class: SupplierFacility - configs: *id001 - name: Supplier3_195 - - class: SupplierFacility - configs: *id002 - name: Supplier1_195 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_195 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_195 - - class: SupplierFacility - configs: *id001 - name: Supplier3_196 - - class: SupplierFacility - configs: *id002 - name: Supplier1_196 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_196 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_196 - - class: SupplierFacility - configs: *id001 - name: Supplier3_197 - - class: SupplierFacility - configs: *id002 - name: Supplier1_197 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_197 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_197 - - class: SupplierFacility - configs: *id001 - name: Supplier3_198 - - class: SupplierFacility - configs: *id002 - name: Supplier1_198 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_198 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_198 - - class: SupplierFacility - configs: *id001 - name: Supplier3_199 - - class: SupplierFacility - configs: *id002 - name: Supplier1_199 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_199 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_199 - - class: SupplierFacility - configs: *id001 - name: Supplier3_200 - - class: SupplierFacility - configs: *id002 - name: Supplier1_200 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_200 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_200 - - class: SupplierFacility - configs: *id001 - name: Supplier3_201 - - class: SupplierFacility - configs: *id002 - name: Supplier1_201 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_201 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_201 - - class: SupplierFacility - configs: *id001 - name: Supplier3_202 - - class: SupplierFacility - configs: *id002 - name: Supplier1_202 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_202 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_202 - - class: SupplierFacility - configs: *id001 - name: Supplier3_203 - - class: SupplierFacility - configs: *id002 - name: Supplier1_203 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_203 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_203 - - class: SupplierFacility - configs: *id001 - name: Supplier3_204 - - class: SupplierFacility - configs: *id002 - name: Supplier1_204 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_204 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_204 - - class: SupplierFacility - configs: *id001 - name: Supplier3_205 - - class: SupplierFacility - configs: *id002 - name: Supplier1_205 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_205 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_205 - - class: SupplierFacility - configs: *id001 - name: Supplier3_206 - - class: SupplierFacility - configs: *id002 - name: Supplier1_206 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_206 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_206 - - class: SupplierFacility - configs: *id001 - name: Supplier3_207 - - class: SupplierFacility - configs: *id002 - name: Supplier1_207 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_207 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_207 - - class: SupplierFacility - configs: *id001 - name: Supplier3_208 - - class: SupplierFacility - configs: *id002 - name: Supplier1_208 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_208 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_208 - - class: SupplierFacility - configs: *id001 - name: Supplier3_209 - - class: SupplierFacility - configs: *id002 - name: Supplier1_209 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_209 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_209 - - class: SupplierFacility - configs: *id001 - name: Supplier3_210 - - class: SupplierFacility - configs: *id002 - name: Supplier1_210 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_210 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_210 - - class: SupplierFacility - configs: *id001 - name: Supplier3_211 - - class: SupplierFacility - configs: *id002 - name: Supplier1_211 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_211 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_211 - - class: SupplierFacility - configs: *id001 - name: Supplier3_212 - - class: SupplierFacility - configs: *id002 - name: Supplier1_212 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_212 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_212 - - class: SupplierFacility - configs: *id001 - name: Supplier3_213 - - class: SupplierFacility - configs: *id002 - name: Supplier1_213 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_213 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_213 - - class: SupplierFacility - configs: *id001 - name: Supplier3_214 - - class: SupplierFacility - configs: *id002 - name: Supplier1_214 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_214 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_214 - - class: SupplierFacility - configs: *id001 - name: Supplier3_215 - - class: SupplierFacility - configs: *id002 - name: Supplier1_215 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_215 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_215 - - class: SupplierFacility - configs: *id001 - name: Supplier3_216 - - class: SupplierFacility - configs: *id002 - name: Supplier1_216 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_216 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_216 - - class: SupplierFacility - configs: *id001 - name: Supplier3_217 - - class: SupplierFacility - configs: *id002 - name: Supplier1_217 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_217 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_217 - - class: SupplierFacility - configs: *id001 - name: Supplier3_218 - - class: SupplierFacility - configs: *id002 - name: Supplier1_218 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_218 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_218 - - class: SupplierFacility - configs: *id001 - name: Supplier3_219 - - class: SupplierFacility - configs: *id002 - name: Supplier1_219 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_219 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_219 - - class: SupplierFacility - configs: *id001 - name: Supplier3_220 - - class: SupplierFacility - configs: *id002 - name: Supplier1_220 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_220 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_220 - - class: SupplierFacility - configs: *id001 - name: Supplier3_221 - - class: SupplierFacility - configs: *id002 - name: Supplier1_221 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_221 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_221 - - class: SupplierFacility - configs: *id001 - name: Supplier3_222 - - class: SupplierFacility - configs: *id002 - name: Supplier1_222 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_222 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_222 - - class: SupplierFacility - configs: *id001 - name: Supplier3_223 - - class: SupplierFacility - configs: *id002 - name: Supplier1_223 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_223 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_223 - - class: SupplierFacility - configs: *id001 - name: Supplier3_224 - - class: SupplierFacility - configs: *id002 - name: Supplier1_224 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_224 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_224 - - class: SupplierFacility - configs: *id001 - name: Supplier3_225 - - class: SupplierFacility - configs: *id002 - name: Supplier1_225 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_225 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_225 - - class: SupplierFacility - configs: *id001 - name: Supplier3_226 - - class: SupplierFacility - configs: *id002 - name: Supplier1_226 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_226 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_226 - - class: SupplierFacility - configs: *id001 - name: Supplier3_227 - - class: SupplierFacility - configs: *id002 - name: Supplier1_227 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_227 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_227 - - class: SupplierFacility - configs: *id001 - name: Supplier3_228 - - class: SupplierFacility - configs: *id002 - name: Supplier1_228 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_228 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_228 - - class: SupplierFacility - configs: *id001 - name: Supplier3_229 - - class: SupplierFacility - configs: *id002 - name: Supplier1_229 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_229 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_229 - - class: SupplierFacility - configs: *id001 - name: Supplier3_230 - - class: SupplierFacility - configs: *id002 - name: Supplier1_230 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_230 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_230 - - class: SupplierFacility - configs: *id001 - name: Supplier3_231 - - class: SupplierFacility - configs: *id002 - name: Supplier1_231 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_231 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_231 - - class: SupplierFacility - configs: *id001 - name: Supplier3_232 - - class: SupplierFacility - configs: *id002 - name: Supplier1_232 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_232 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_232 - - class: SupplierFacility - configs: *id001 - name: Supplier3_233 - - class: SupplierFacility - configs: *id002 - name: Supplier1_233 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_233 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_233 - - class: SupplierFacility - configs: *id001 - name: Supplier3_234 - - class: SupplierFacility - configs: *id002 - name: Supplier1_234 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_234 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_234 - - class: SupplierFacility - configs: *id001 - name: Supplier3_235 - - class: SupplierFacility - configs: *id002 - name: Supplier1_235 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_235 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_235 - - class: SupplierFacility - configs: *id001 - name: Supplier3_236 - - class: SupplierFacility - configs: *id002 - name: Supplier1_236 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_236 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_236 - - class: SupplierFacility - configs: *id001 - name: Supplier3_237 - - class: SupplierFacility - configs: *id002 - name: Supplier1_237 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_237 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_237 - - class: SupplierFacility - configs: *id001 - name: Supplier3_238 - - class: SupplierFacility - configs: *id002 - name: Supplier1_238 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_238 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_238 - - class: SupplierFacility - configs: *id001 - name: Supplier3_239 - - class: SupplierFacility - configs: *id002 - name: Supplier1_239 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_239 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_239 - - class: SupplierFacility - configs: *id001 - name: Supplier3_240 - - class: SupplierFacility - configs: *id002 - name: Supplier1_240 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_240 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_240 - - class: SupplierFacility - configs: *id001 - name: Supplier3_241 - - class: SupplierFacility - configs: *id002 - name: Supplier1_241 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_241 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_241 - - class: SupplierFacility - configs: *id001 - name: Supplier3_242 - - class: SupplierFacility - configs: *id002 - name: Supplier1_242 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_242 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_242 - - class: SupplierFacility - configs: *id001 - name: Supplier3_243 - - class: SupplierFacility - configs: *id002 - name: Supplier1_243 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_243 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_243 - - class: SupplierFacility - configs: *id001 - name: Supplier3_244 - - class: SupplierFacility - configs: *id002 - name: Supplier1_244 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_244 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_244 - - class: SupplierFacility - configs: *id001 - name: Supplier3_245 - - class: SupplierFacility - configs: *id002 - name: Supplier1_245 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_245 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_245 - - class: SupplierFacility - configs: *id001 - name: Supplier3_246 - - class: SupplierFacility - configs: *id002 - name: Supplier1_246 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_246 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_246 - - class: SupplierFacility - configs: *id001 - name: Supplier3_247 - - class: SupplierFacility - configs: *id002 - name: Supplier1_247 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_247 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_247 - - class: SupplierFacility - configs: *id001 - name: Supplier3_248 - - class: SupplierFacility - configs: *id002 - name: Supplier1_248 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_248 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_248 - - class: SupplierFacility - configs: *id001 - name: Supplier3_249 - - class: SupplierFacility - configs: *id002 - name: Supplier1_249 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_249 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_249 - - class: SupplierFacility - configs: *id001 - name: Supplier3_250 - - class: SupplierFacility - configs: *id002 - name: Supplier1_250 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_250 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_250 - - class: SupplierFacility - configs: *id001 - name: Supplier3_251 - - class: SupplierFacility - configs: *id002 - name: Supplier1_251 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_251 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_251 - - class: SupplierFacility - configs: *id001 - name: Supplier3_252 - - class: SupplierFacility - configs: *id002 - name: Supplier1_252 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_252 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_252 - - class: SupplierFacility - configs: *id001 - name: Supplier3_253 - - class: SupplierFacility - configs: *id002 - name: Supplier1_253 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_253 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_253 - - class: SupplierFacility - configs: *id001 - name: Supplier3_254 - - class: SupplierFacility - configs: *id002 - name: Supplier1_254 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_254 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_254 - - class: SupplierFacility - configs: *id001 - name: Supplier3_255 - - class: SupplierFacility - configs: *id002 - name: Supplier1_255 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_255 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_255 - - class: SupplierFacility - configs: *id001 - name: Supplier3_256 - - class: SupplierFacility - configs: *id002 - name: Supplier1_256 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_256 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_256 - - class: SupplierFacility - configs: *id001 - name: Supplier3_257 - - class: SupplierFacility - configs: *id002 - name: Supplier1_257 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_257 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_257 - - class: SupplierFacility - configs: *id001 - name: Supplier3_258 - - class: SupplierFacility - configs: *id002 - name: Supplier1_258 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_258 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_258 - - class: SupplierFacility - configs: *id001 - name: Supplier3_259 - - class: SupplierFacility - configs: *id002 - name: Supplier1_259 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_259 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_259 - - class: SupplierFacility - configs: *id001 - name: Supplier3_260 - - class: SupplierFacility - configs: *id002 - name: Supplier1_260 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_260 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_260 - - class: SupplierFacility - configs: *id001 - name: Supplier3_261 - - class: SupplierFacility - configs: *id002 - name: Supplier1_261 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_261 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_261 - - class: SupplierFacility - configs: *id001 - name: Supplier3_262 - - class: SupplierFacility - configs: *id002 - name: Supplier1_262 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_262 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_262 - - class: SupplierFacility - configs: *id001 - name: Supplier3_263 - - class: SupplierFacility - configs: *id002 - name: Supplier1_263 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_263 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_263 - - class: SupplierFacility - configs: *id001 - name: Supplier3_264 - - class: SupplierFacility - configs: *id002 - name: Supplier1_264 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_264 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_264 - - class: SupplierFacility - configs: *id001 - name: Supplier3_265 - - class: SupplierFacility - configs: *id002 - name: Supplier1_265 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_265 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_265 - - class: SupplierFacility - configs: *id001 - name: Supplier3_266 - - class: SupplierFacility - configs: *id002 - name: Supplier1_266 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_266 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_266 - - class: SupplierFacility - configs: *id001 - name: Supplier3_267 - - class: SupplierFacility - configs: *id002 - name: Supplier1_267 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_267 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_267 - - class: SupplierFacility - configs: *id001 - name: Supplier3_268 - - class: SupplierFacility - configs: *id002 - name: Supplier1_268 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_268 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_268 - - class: SupplierFacility - configs: *id001 - name: Supplier3_269 - - class: SupplierFacility - configs: *id002 - name: Supplier1_269 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_269 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_269 - - class: SupplierFacility - configs: *id001 - name: Supplier3_270 - - class: SupplierFacility - configs: *id002 - name: Supplier1_270 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_270 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_270 - - class: SupplierFacility - configs: *id001 - name: Supplier3_271 - - class: SupplierFacility - configs: *id002 - name: Supplier1_271 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_271 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_271 - - class: SupplierFacility - configs: *id001 - name: Supplier3_272 - - class: SupplierFacility - configs: *id002 - name: Supplier1_272 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_272 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_272 - - class: SupplierFacility - configs: *id001 - name: Supplier3_273 - - class: SupplierFacility - configs: *id002 - name: Supplier1_273 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_273 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_273 - - class: SupplierFacility - configs: *id001 - name: Supplier3_274 - - class: SupplierFacility - configs: *id002 - name: Supplier1_274 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_274 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_274 - - class: SupplierFacility - configs: *id001 - name: Supplier3_275 - - class: SupplierFacility - configs: *id002 - name: Supplier1_275 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_275 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_275 - - class: SupplierFacility - configs: *id001 - name: Supplier3_276 - - class: SupplierFacility - configs: *id002 - name: Supplier1_276 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_276 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_276 - - class: SupplierFacility - configs: *id001 - name: Supplier3_277 - - class: SupplierFacility - configs: *id002 - name: Supplier1_277 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_277 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_277 - - class: SupplierFacility - configs: *id001 - name: Supplier3_278 - - class: SupplierFacility - configs: *id002 - name: Supplier1_278 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_278 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_278 - - class: SupplierFacility - configs: *id001 - name: Supplier3_279 - - class: SupplierFacility - configs: *id002 - name: Supplier1_279 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_279 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_279 - - class: SupplierFacility - configs: *id001 - name: Supplier3_280 - - class: SupplierFacility - configs: *id002 - name: Supplier1_280 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_280 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_280 - - class: SupplierFacility - configs: *id001 - name: Supplier3_281 - - class: SupplierFacility - configs: *id002 - name: Supplier1_281 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_281 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_281 - - class: SupplierFacility - configs: *id001 - name: Supplier3_282 - - class: SupplierFacility - configs: *id002 - name: Supplier1_282 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_282 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_282 - - class: SupplierFacility - configs: *id001 - name: Supplier3_283 - - class: SupplierFacility - configs: *id002 - name: Supplier1_283 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_283 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_283 - - class: SupplierFacility - configs: *id001 - name: Supplier3_284 - - class: SupplierFacility - configs: *id002 - name: Supplier1_284 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_284 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_284 - - class: SupplierFacility - configs: *id001 - name: Supplier3_285 - - class: SupplierFacility - configs: *id002 - name: Supplier1_285 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_285 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_285 - - class: SupplierFacility - configs: *id001 - name: Supplier3_286 - - class: SupplierFacility - configs: *id002 - name: Supplier1_286 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_286 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_286 - - class: SupplierFacility - configs: *id001 - name: Supplier3_287 - - class: SupplierFacility - configs: *id002 - name: Supplier1_287 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_287 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_287 - - class: SupplierFacility - configs: *id001 - name: Supplier3_288 - - class: SupplierFacility - configs: *id002 - name: Supplier1_288 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_288 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_288 - - class: SupplierFacility - configs: *id001 - name: Supplier3_289 - - class: SupplierFacility - configs: *id002 - name: Supplier1_289 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_289 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_289 - - class: SupplierFacility - configs: *id001 - name: Supplier3_290 - - class: SupplierFacility - configs: *id002 - name: Supplier1_290 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_290 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_290 - - class: SupplierFacility - configs: *id001 - name: Supplier3_291 - - class: SupplierFacility - configs: *id002 - name: Supplier1_291 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_291 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_291 - - class: SupplierFacility - configs: *id001 - name: Supplier3_292 - - class: SupplierFacility - configs: *id002 - name: Supplier1_292 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_292 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_292 - - class: SupplierFacility - configs: *id001 - name: Supplier3_293 - - class: SupplierFacility - configs: *id002 - name: Supplier1_293 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_293 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_293 - - class: SupplierFacility - configs: *id001 - name: Supplier3_294 - - class: SupplierFacility - configs: *id002 - name: Supplier1_294 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_294 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_294 - - class: SupplierFacility - configs: *id001 - name: Supplier3_295 - - class: SupplierFacility - configs: *id002 - name: Supplier1_295 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_295 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_295 - - class: SupplierFacility - configs: *id001 - name: Supplier3_296 - - class: SupplierFacility - configs: *id002 - name: Supplier1_296 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_296 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_296 - - class: SupplierFacility - configs: *id001 - name: Supplier3_297 - - class: SupplierFacility - configs: *id002 - name: Supplier1_297 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_297 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_297 - - class: SupplierFacility - configs: *id001 - name: Supplier3_298 - - class: SupplierFacility - configs: *id002 - name: Supplier1_298 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_298 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_298 - - class: SupplierFacility - configs: *id001 - name: Supplier3_299 - - class: SupplierFacility - configs: *id002 - name: Supplier1_299 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_299 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_299 - - class: SupplierFacility - configs: *id001 - name: Supplier3_300 - - class: SupplierFacility - configs: *id002 - name: Supplier1_300 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_300 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_300 - - class: SupplierFacility - configs: *id001 - name: Supplier3_301 - - class: SupplierFacility - configs: *id002 - name: Supplier1_301 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_301 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_301 - - class: SupplierFacility - configs: *id001 - name: Supplier3_302 - - class: SupplierFacility - configs: *id002 - name: Supplier1_302 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_302 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_302 - - class: SupplierFacility - configs: *id001 - name: Supplier3_303 - - class: SupplierFacility - configs: *id002 - name: Supplier1_303 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_303 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_303 - - class: SupplierFacility - configs: *id001 - name: Supplier3_304 - - class: SupplierFacility - configs: *id002 - name: Supplier1_304 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_304 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_304 - - class: SupplierFacility - configs: *id001 - name: Supplier3_305 - - class: SupplierFacility - configs: *id002 - name: Supplier1_305 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_305 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_305 - - class: SupplierFacility - configs: *id001 - name: Supplier3_306 - - class: SupplierFacility - configs: *id002 - name: Supplier1_306 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_306 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_306 - - class: SupplierFacility - configs: *id001 - name: Supplier3_307 - - class: SupplierFacility - configs: *id002 - name: Supplier1_307 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_307 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_307 - - class: SupplierFacility - configs: *id001 - name: Supplier3_308 - - class: SupplierFacility - configs: *id002 - name: Supplier1_308 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_308 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_308 - - class: SupplierFacility - configs: *id001 - name: Supplier3_309 - - class: SupplierFacility - configs: *id002 - name: Supplier1_309 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_309 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_309 - - class: SupplierFacility - configs: *id001 - name: Supplier3_310 - - class: SupplierFacility - configs: *id002 - name: Supplier1_310 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_310 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_310 - - class: SupplierFacility - configs: *id001 - name: Supplier3_311 - - class: SupplierFacility - configs: *id002 - name: Supplier1_311 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_311 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_311 - - class: SupplierFacility - configs: *id001 - name: Supplier3_312 - - class: SupplierFacility - configs: *id002 - name: Supplier1_312 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_312 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_312 - - class: SupplierFacility - configs: *id001 - name: Supplier3_313 - - class: SupplierFacility - configs: *id002 - name: Supplier1_313 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_313 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_313 - - class: SupplierFacility - configs: *id001 - name: Supplier3_314 - - class: SupplierFacility - configs: *id002 - name: Supplier1_314 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_314 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_314 - - class: SupplierFacility - configs: *id001 - name: Supplier3_315 - - class: SupplierFacility - configs: *id002 - name: Supplier1_315 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_315 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_315 - - class: SupplierFacility - configs: *id001 - name: Supplier3_316 - - class: SupplierFacility - configs: *id002 - name: Supplier1_316 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_316 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_316 - - class: SupplierFacility - configs: *id001 - name: Supplier3_317 - - class: SupplierFacility - configs: *id002 - name: Supplier1_317 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_317 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_317 - - class: SupplierFacility - configs: *id001 - name: Supplier3_318 - - class: SupplierFacility - configs: *id002 - name: Supplier1_318 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_318 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_318 - - class: SupplierFacility - configs: *id001 - name: Supplier3_319 - - class: SupplierFacility - configs: *id002 - name: Supplier1_319 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_319 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_319 - - class: SupplierFacility - configs: *id001 - name: Supplier3_320 - - class: SupplierFacility - configs: *id002 - name: Supplier1_320 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_320 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_320 - - class: SupplierFacility - configs: *id001 - name: Supplier3_321 - - class: SupplierFacility - configs: *id002 - name: Supplier1_321 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_321 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_321 - - class: SupplierFacility - configs: *id001 - name: Supplier3_322 - - class: SupplierFacility - configs: *id002 - name: Supplier1_322 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_322 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_322 - - class: SupplierFacility - configs: *id001 - name: Supplier3_323 - - class: SupplierFacility - configs: *id002 - name: Supplier1_323 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_323 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_323 - - class: SupplierFacility - configs: *id001 - name: Supplier3_324 - - class: SupplierFacility - configs: *id002 - name: Supplier1_324 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_324 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_324 - - class: SupplierFacility - configs: *id001 - name: Supplier3_325 - - class: SupplierFacility - configs: *id002 - name: Supplier1_325 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_325 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_325 - - class: SupplierFacility - configs: *id001 - name: Supplier3_326 - - class: SupplierFacility - configs: *id002 - name: Supplier1_326 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_326 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_326 - - class: SupplierFacility - configs: *id001 - name: Supplier3_327 - - class: SupplierFacility - configs: *id002 - name: Supplier1_327 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_327 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_327 - - class: SupplierFacility - configs: *id001 - name: Supplier3_328 - - class: SupplierFacility - configs: *id002 - name: Supplier1_328 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_328 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_328 - - class: SupplierFacility - configs: *id001 - name: Supplier3_329 - - class: SupplierFacility - configs: *id002 - name: Supplier1_329 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_329 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_329 - - class: SupplierFacility - configs: *id001 - name: Supplier3_330 - - class: SupplierFacility - configs: *id002 - name: Supplier1_330 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_330 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_330 - - class: SupplierFacility - configs: *id001 - name: Supplier3_331 - - class: SupplierFacility - configs: *id002 - name: Supplier1_331 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_331 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_331 - - class: SupplierFacility - configs: *id001 - name: Supplier3_332 - - class: SupplierFacility - configs: *id002 - name: Supplier1_332 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_332 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_332 - - class: SupplierFacility - configs: *id001 - name: Supplier3_333 - - class: SupplierFacility - configs: *id002 - name: Supplier1_333 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_333 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_333 - - class: SupplierFacility - configs: *id001 - name: Supplier3_334 - - class: SupplierFacility - configs: *id002 - name: Supplier1_334 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_334 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_334 - - class: SupplierFacility - configs: *id001 - name: Supplier3_335 - - class: SupplierFacility - configs: *id002 - name: Supplier1_335 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_335 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_335 - - class: SupplierFacility - configs: *id001 - name: Supplier3_336 - - class: SupplierFacility - configs: *id002 - name: Supplier1_336 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_336 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_336 - - class: SupplierFacility - configs: *id001 - name: Supplier3_337 - - class: SupplierFacility - configs: *id002 - name: Supplier1_337 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_337 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_337 - - class: SupplierFacility - configs: *id001 - name: Supplier3_338 - - class: SupplierFacility - configs: *id002 - name: Supplier1_338 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_338 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_338 - - class: SupplierFacility - configs: *id001 - name: Supplier3_339 - - class: SupplierFacility - configs: *id002 - name: Supplier1_339 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_339 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_339 - - class: SupplierFacility - configs: *id001 - name: Supplier3_340 - - class: SupplierFacility - configs: *id002 - name: Supplier1_340 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_340 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_340 - - class: SupplierFacility - configs: *id001 - name: Supplier3_341 - - class: SupplierFacility - configs: *id002 - name: Supplier1_341 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_341 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_341 - - class: SupplierFacility - configs: *id001 - name: Supplier3_342 - - class: SupplierFacility - configs: *id002 - name: Supplier1_342 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_342 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_342 - - class: SupplierFacility - configs: *id001 - name: Supplier3_343 - - class: SupplierFacility - configs: *id002 - name: Supplier1_343 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_343 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_343 - - class: SupplierFacility - configs: *id001 - name: Supplier3_344 - - class: SupplierFacility - configs: *id002 - name: Supplier1_344 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_344 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_344 - - class: SupplierFacility - configs: *id001 - name: Supplier3_345 - - class: SupplierFacility - configs: *id002 - name: Supplier1_345 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_345 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_345 - - class: SupplierFacility - configs: *id001 - name: Supplier3_346 - - class: SupplierFacility - configs: *id002 - name: Supplier1_346 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_346 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_346 - - class: SupplierFacility - configs: *id001 - name: Supplier3_347 - - class: SupplierFacility - configs: *id002 - name: Supplier1_347 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_347 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_347 - - class: SupplierFacility - configs: *id001 - name: Supplier3_348 - - class: SupplierFacility - configs: *id002 - name: Supplier1_348 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_348 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_348 - - class: SupplierFacility - configs: *id001 - name: Supplier3_349 - - class: SupplierFacility - configs: *id002 - name: Supplier1_349 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_349 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_349 - - class: SupplierFacility - configs: *id001 - name: Supplier3_350 - - class: SupplierFacility - configs: *id002 - name: Supplier1_350 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_350 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_350 - - class: SupplierFacility - configs: *id001 - name: Supplier3_351 - - class: SupplierFacility - configs: *id002 - name: Supplier1_351 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_351 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_351 - - class: SupplierFacility - configs: *id001 - name: Supplier3_352 - - class: SupplierFacility - configs: *id002 - name: Supplier1_352 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_352 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_352 - - class: SupplierFacility - configs: *id001 - name: Supplier3_353 - - class: SupplierFacility - configs: *id002 - name: Supplier1_353 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_353 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_353 - - class: SupplierFacility - configs: *id001 - name: Supplier3_354 - - class: SupplierFacility - configs: *id002 - name: Supplier1_354 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_354 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_354 - - class: SupplierFacility - configs: *id001 - name: Supplier3_355 - - class: SupplierFacility - configs: *id002 - name: Supplier1_355 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_355 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_355 - - class: SupplierFacility - configs: *id001 - name: Supplier3_356 - - class: SupplierFacility - configs: *id002 - name: Supplier1_356 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_356 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_356 - - class: SupplierFacility - configs: *id001 - name: Supplier3_357 - - class: SupplierFacility - configs: *id002 - name: Supplier1_357 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_357 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_357 - - class: SupplierFacility - configs: *id001 - name: Supplier3_358 - - class: SupplierFacility - configs: *id002 - name: Supplier1_358 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_358 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_358 - - class: SupplierFacility - configs: *id001 - name: Supplier3_359 - - class: SupplierFacility - configs: *id002 - name: Supplier1_359 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_359 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_359 - - class: SupplierFacility - configs: *id001 - name: Supplier3_360 - - class: SupplierFacility - configs: *id002 - name: Supplier1_360 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_360 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_360 - - class: SupplierFacility - configs: *id001 - name: Supplier3_361 - - class: SupplierFacility - configs: *id002 - name: Supplier1_361 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_361 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_361 - - class: SupplierFacility - configs: *id001 - name: Supplier3_362 - - class: SupplierFacility - configs: *id002 - name: Supplier1_362 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_362 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_362 - - class: SupplierFacility - configs: *id001 - name: Supplier3_363 - - class: SupplierFacility - configs: *id002 - name: Supplier1_363 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_363 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_363 - - class: SupplierFacility - configs: *id001 - name: Supplier3_364 - - class: SupplierFacility - configs: *id002 - name: Supplier1_364 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_364 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_364 - - class: SupplierFacility - configs: *id001 - name: Supplier3_365 - - class: SupplierFacility - configs: *id002 - name: Supplier1_365 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_365 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_365 - - class: SupplierFacility - configs: *id001 - name: Supplier3_366 - - class: SupplierFacility - configs: *id002 - name: Supplier1_366 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_366 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_366 - - class: SupplierFacility - configs: *id001 - name: Supplier3_367 - - class: SupplierFacility - configs: *id002 - name: Supplier1_367 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_367 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_367 - - class: SupplierFacility - configs: *id001 - name: Supplier3_368 - - class: SupplierFacility - configs: *id002 - name: Supplier1_368 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_368 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_368 - - class: SupplierFacility - configs: *id001 - name: Supplier3_369 - - class: SupplierFacility - configs: *id002 - name: Supplier1_369 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_369 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_369 - - class: SupplierFacility - configs: *id001 - name: Supplier3_370 - - class: SupplierFacility - configs: *id002 - name: Supplier1_370 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_370 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_370 - - class: SupplierFacility - configs: *id001 - name: Supplier3_371 - - class: SupplierFacility - configs: *id002 - name: Supplier1_371 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_371 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_371 - - class: SupplierFacility - configs: *id001 - name: Supplier3_372 - - class: SupplierFacility - configs: *id002 - name: Supplier1_372 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_372 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_372 - - class: SupplierFacility - configs: *id001 - name: Supplier3_373 - - class: SupplierFacility - configs: *id002 - name: Supplier1_373 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_373 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_373 - - class: SupplierFacility - configs: *id001 - name: Supplier3_374 - - class: SupplierFacility - configs: *id002 - name: Supplier1_374 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_374 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_374 - - class: SupplierFacility - configs: *id001 - name: Supplier3_375 - - class: SupplierFacility - configs: *id002 - name: Supplier1_375 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_375 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_375 - - class: SupplierFacility - configs: *id001 - name: Supplier3_376 - - class: SupplierFacility - configs: *id002 - name: Supplier1_376 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_376 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_376 - - class: SupplierFacility - configs: *id001 - name: Supplier3_377 - - class: SupplierFacility - configs: *id002 - name: Supplier1_377 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_377 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_377 - - class: SupplierFacility - configs: *id001 - name: Supplier3_378 - - class: SupplierFacility - configs: *id002 - name: Supplier1_378 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_378 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_378 - - class: SupplierFacility - configs: *id001 - name: Supplier3_379 - - class: SupplierFacility - configs: *id002 - name: Supplier1_379 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_379 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_379 - - class: SupplierFacility - configs: *id001 - name: Supplier3_380 - - class: SupplierFacility - configs: *id002 - name: Supplier1_380 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_380 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_380 - - class: SupplierFacility - configs: *id001 - name: Supplier3_381 - - class: SupplierFacility - configs: *id002 - name: Supplier1_381 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_381 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_381 - - class: SupplierFacility - configs: *id001 - name: Supplier3_382 - - class: SupplierFacility - configs: *id002 - name: Supplier1_382 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_382 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_382 - - class: SupplierFacility - configs: *id001 - name: Supplier3_383 - - class: SupplierFacility - configs: *id002 - name: Supplier1_383 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_383 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_383 - - class: SupplierFacility - configs: *id001 - name: Supplier3_384 - - class: SupplierFacility - configs: *id002 - name: Supplier1_384 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_384 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_384 - - class: SupplierFacility - configs: *id001 - name: Supplier3_385 - - class: SupplierFacility - configs: *id002 - name: Supplier1_385 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_385 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_385 - - class: SupplierFacility - configs: *id001 - name: Supplier3_386 - - class: SupplierFacility - configs: *id002 - name: Supplier1_386 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_386 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_386 - - class: SupplierFacility - configs: *id001 - name: Supplier3_387 - - class: SupplierFacility - configs: *id002 - name: Supplier1_387 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_387 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_387 - - class: SupplierFacility - configs: *id001 - name: Supplier3_388 - - class: SupplierFacility - configs: *id002 - name: Supplier1_388 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_388 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_388 - - class: SupplierFacility - configs: *id001 - name: Supplier3_389 - - class: SupplierFacility - configs: *id002 - name: Supplier1_389 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_389 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_389 - - class: SupplierFacility - configs: *id001 - name: Supplier3_390 - - class: SupplierFacility - configs: *id002 - name: Supplier1_390 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_390 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_390 - - class: SupplierFacility - configs: *id001 - name: Supplier3_391 - - class: SupplierFacility - configs: *id002 - name: Supplier1_391 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_391 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_391 - - class: SupplierFacility - configs: *id001 - name: Supplier3_392 - - class: SupplierFacility - configs: *id002 - name: Supplier1_392 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_392 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_392 - - class: SupplierFacility - configs: *id001 - name: Supplier3_393 - - class: SupplierFacility - configs: *id002 - name: Supplier1_393 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_393 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_393 - - class: SupplierFacility - configs: *id001 - name: Supplier3_394 - - class: SupplierFacility - configs: *id002 - name: Supplier1_394 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_394 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_394 - - class: SupplierFacility - configs: *id001 - name: Supplier3_395 - - class: SupplierFacility - configs: *id002 - name: Supplier1_395 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_395 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_395 - - class: SupplierFacility - configs: *id001 - name: Supplier3_396 - - class: SupplierFacility - configs: *id002 - name: Supplier1_396 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_396 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_396 - - class: SupplierFacility - configs: *id001 - name: Supplier3_397 - - class: SupplierFacility - configs: *id002 - name: Supplier1_397 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_397 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_397 - - class: SupplierFacility - configs: *id001 - name: Supplier3_398 - - class: SupplierFacility - configs: *id002 - name: Supplier1_398 - - class: WarehouseFacility - configs: *id003 - name: Warehouse1_398 - - class: RetailerFacility - configs: *id004 - name: ChaoShiFa01_398 - grid: - blocks: - railroad: - - - 10 - - 10 - - - 10 - - 11 - - - 10 - - 12 - - - 11 - - 12 - facilities: - ChaoShiFa01: - - 10 - - 18 - Supplier1: - - 0 - - 0 - Supplier3: - - 3 - - 3 - Warehouse1: - - 6 - - 6 - size: - - 20 - - 20 - skus: - - bom: - sku3: 10 - id: 1 - name: sku1 - output_units_per_lot: 1 - - id: 2 - name: sku2 - output_units_per_lot: 1 - - id: 3 - name: sku3 - output_units_per_lot: 1 - step_mode: facility - topology: - ChaoShiFa01: - sku1: - - Supplier1 - sku3: - - Supplier3 - Supplier1: - sku3: - - Supplier3 - Warehouse1: - sku1: - - Supplier1 - sku3: - - Supplier3 diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index 9c4c7af87..32f495f64 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -1,168 +1,211 @@ # TODO: which config to inherit -base: "" +# base: "" # TODO: add customized data model, unit or facility definitions, will be combined with builtin ones. #core: -# data: "xxx" +# datamodels: "xxx" # units: "xxx" # facilities: "xxx" + +facility_definitions: + # facility definition + WarehouseFacility: &warehouse_facility + class: "WarehouseFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + # if true then will call generate function of class type + is_template: true + # config will be passed to generator as parameters + config: + consumer: + class: "ConsumerUnit" + + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + manufacture: + class: "ManufactureUnit" + + RetailerFacility: &retailer_facility + class: "RetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + seller: + class: "SellerUnit" + +# common entity/unit definition as reference to simplify the file. +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 100 + +# a normal distribution definition +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + transports: + - *normal_vehicle + - *normal_vehicle + +small_storage: &small_storage + # config of data model of this unit + config: + # other config or storage unit + capacity: 10000 + unit_storage_cost: 1 + +midium_storage: &midium_storage + config: + capacity: 20000 + unit_storage_cost: 1 + +huge_storage: &huge_storage + config: + capacity: 30000 + unit_storage_cost: 1 + +# sku list in this world +# this list do not contains price, cost or other facility related attributes, +# but just base info, like name, id, bom +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 12 + # bill of material that used produce current sku, empty means do not need source material + bom: + # key is the source sku name, value is quantity needed to use per time to produce current sku + sku3: 10 + + - id: 2 + name: "sku2" + output_units_per_lot: 1 + + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + # world definitions world: - # how many ticks to ask for an action. - action_steps: 1 - - # how the simulator update the world - # available values: - # facility: update facilities (internal units) one by one - # random: random update units - step_mode: "facility" - - # sku list in this world - # this list do not contains price, cost or other facility related attributes, - # but just base info, like name, id, bom - skus: - - id: 1 - name: "sku1" - output_units_per_lot: 1 - # bill of material that used produce current sku, empty means do not need source material - bom: - # key is the source sku name, value is quantity needed to use per time to produce current sku - sku3: 10 - - id: 2 - name: "sku2" - output_units_per_lot: 1 - - id: 3 - name: "sku3" - output_units_per_lot: 1 + # here we use reference to make it each to edit. + skus: *sku_definitions # facilities in this world facilities: - - name: "Supplier3" # name of the facility - class: "SupplierFacility" # class alias of this facility - # config of this facility - configs: - # sku list of this facility - skus: - sku3: # sku name and attributes needed for this facility - init_in_stock: 100 - production_rate: 0 - delay_order_penalty: 10 - product_unit_cost: 1 - type: "production" # production means this is the output production of this facility - cost": 10 - price": 10 - # config for units + - name: "Supplier_001" # name of the facility + # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level + # use the facility definition as base, then we can override configs partially. + definition_ref: "SupplierFacility" + + # sku list of this facility + skus: + sku3: # sku name and attributes needed for this facility + init_in_stock: 100 + delay_order_penalty: 10 + product_unit_cost: 1 + type: "production" # production means this is the output production of this facility + cost": 10 + price": 10 + + # configuration of child units. + children: # config of storage unit - storage: - # config of data model of this unit - data: - # other config or storage unit - capacity: 20000 - unit_storage_cost: 10 - distribution: - data: - unit_price: 10 - transports: - # definition of each vehicle - - class: "TransportUnit" - data: - patient: 100 - - class: "TransportUnit" - data: - patient: 100 - - name: "Supplier1" - class: "SupplierFacility" - configs: - skus: - sku1: - init_in_stock: 100 - production_rate: 0 - delay_order_penalty: 10 - product_unit_cost: 1 - type: "production" - cost: 10 - price: 100 - sku3: - init_in_stock: 100 - production_rate: 200 - delay_order_penalty: 10 - type: "material" - cost: 10 - price: 100 - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - distribution: - data: - unit_price: 10 - transports: - - class: "TransportUnit" - data: - patient: 100 - - class: "TransportUnit" - data: - patient: 100 - - name: "Warehouse1" - class: "WarehouseFacility" - configs: - skus: - sku1: - init_stock: 1000 - delay_order_penalty: 10 - price: 100 - sku2: - init_stock: 1000 - delay_order_penalty: 10 - price: 100 - sku3: - init_stock: 1000 - delay_order_penalty: 10 - price: 100 - storage: - data: - capacity: 20000 - unit_storage_cost: 10 - distribution: - data: - unit_price: 10 - transports: - - class: "TransportUnit" - data: - patient: 100 - unit_transport_cost: 1 # optional, default is 1 - - class: "TransportUnit" - data: - patient: 100 - unit_transport_cost: 1 - - name: "ChaoShiFa01" - class: "RetailerFacility" - configs: - skus: - sku1: - price: 300 - cost: 10 - init_in_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 # optional - sku3: - price: 200 - cost: 10 - init_in_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - sku2: - price: 100 - cost: 10 - init_in_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - storage: - data: - capacity: 20000 - unit_storage_cost: 10 + storage: *small_storage + distribution: *normal_distribution + + # products use default config in core.yml + - name: "Supplier_002" + definition_ref: "SupplierFacility" + + skus: + sku1: + init_in_stock: 100 + delay_order_penalty: 10 + product_unit_cost: 1 + type: "production" + cost: 10 + price: 100 + sku3: + init_in_stock: 100 + delay_order_penalty: 10 + type: "material" + cost: 10 + price: 100 + + children: + storage: *small_storage + distribution: *normal_distribution + + - name: "Warehouse_001" + definition_ref: "WarehouseFacility" + + skus: + sku1: + init_stock: 1000 + delay_order_penalty: 10 + price: 100 + sku2: + init_stock: 1000 + delay_order_penalty: 10 + price: 100 + sku3: + init_stock: 1000 + delay_order_penalty: 10 + price: 100 + + children: + storage: *huge_storage + distribution: *normal_distribution + + - name: "Retailer_001" + definition_ref: "RetailerFacility" + + skus: + sku1: + price: 300 + cost: 10 + init_in_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 # optional + sku3: + price: 200 + cost: 10 + init_in_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + sku2: + price: 100 + cost: 10 + init_in_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + + children: + storage: *midium_storage # topology used to specify the up/downstream for facilities # we split it from facility, so that we can support configuration inherit to override it @@ -170,36 +213,37 @@ world: # TODO: change the name? topology: # key is current facility, value if upstream facilities that will provide a certain sku - Supplier1: + Supplier_002: # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", # or any other facility in the list sku3: - - "Supplier3" - Warehouse1: + - "Supplier_001" + Warehouse_001: sku1: - - "Supplier1" + - "Supplier_002" sku3: - - "Supplier3" - ChaoShiFa01: + - "Supplier_001" + Retailer_001: sku1: - - "Supplier1" + - "Supplier_002" sku3: - - "Supplier3" + - "Supplier_001" # map grid definitions grid: - size: [ 20, 20 ] + size: [20, 20] + # facility position in grid facilities: - Supplier1: [ 0, 0 ] # (x, y) - Supplier3: [ 3, 3 ] - Warehouse1: [ 6, 6 ] - ChaoShiFa01: [10, 18] + Supplier_001: [0, 0] + Supplier_002: [3, 3] + Warehouse_001: [6, 6] + Retailer_001: [10, 18] + # cells that un-traversable blocks: - # TODO: later we can have different cell have different travel cost railroad: - - [ 10, 10 ] - - [ 10, 11 ] - - [ 10, 12 ] - - [ 11, 12 ] + - [10, 10] + - [10, 11] + - [10, 12] + - [11, 12] diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/gen.py b/maro/simulator/scenarios/supply_chain/topologies/sample1/gen.py deleted file mode 100644 index 69722d0a4..000000000 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/gen.py +++ /dev/null @@ -1,31 +0,0 @@ -from yaml import safe_load, safe_dump - - -def gen(number: int): - config = None - - with open("config.yml", "rt") as fp: - config = safe_load(fp) - - facilities = config["world"]["facilities"] - - exist_facilities = [] - - for facility in facilities: - exist_facilities.append(facility.copy()) - - for i in range(number-1): - # exist facilities - for facility in exist_facilities: - copied_f = facility.copy() - - copied_f["name"] = f"{facility['name']}_{i}" - - facilities.append(copied_f) - - with open(f"config_{number}.yml", "wt+") as ofp: - safe_dump(config, ofp) - - -if __name__ == "__main__": - gen(1000) diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index cd93869da..c1ac1f9dc 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -1,7 +1,12 @@ -from .base import UnitBase -from .storage import StorageUnit -from .transport import TransportUnit +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .consumer import ConsumerUnit from .distribution import DistributionUnit from .manufacture import ManufactureUnit -from .consumer import ConsumerUnit +from .product import ProductUnit from .seller import SellerUnit +from .storage import StorageUnit +from .unitbase import UnitBase +from .vehicle import VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/units/base.py b/maro/simulator/scenarios/supply_chain/units/base.py deleted file mode 100644 index 39c1f404e..000000000 --- a/maro/simulator/scenarios/supply_chain/units/base.py +++ /dev/null @@ -1,65 +0,0 @@ - -class UnitBase: - # class name of data model - data_class: str = None - - # index of the data model instance in frame - data_index: int = None - - # current world. - world = None - - # which facility belongs to - facility = None - - # configurations of this unit - configs: dict = None - - # id of this unit - id: int = None - - def __init__(self): - # data model instance, it is None until the initialize function called. - self.data = None - - def prepare_data(self): - """Data model install related to this unit, available after initialized function called.""" - if self.data is None: - self.data = self.world.get_data_instance(self.data_class, self.data_index) - - def initialize(self, configs: dict, durations: int): - """Initialize current unit""" - # called after frame ready - self.configs = configs - self.data.set_id(self.id, self.facility.id) - self.data.initialize(configs.get("data", {})) - - def step(self, tick: int): - # called per tick - pass - - def begin_post_step(self, tick: int): - pass - - def end_post_step(self, tick: int): - pass - - def get_metrics(self): - # called per step - pass - - def reset(self): - # called per episode - self.data.reset() - - def set_action(self, action): - # called after received an action. - pass - - def get_unit_info(self) -> dict: - return { - "id": self.id, - "node_name": type(self.data).__node_name__, - "node_index": self.data_index, - "class": type(self) - } diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index febbf41d8..9578e0400 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -1,88 +1,107 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from collections import defaultdict, Counter -from .base import UnitBase + from .order import Order +from .skuunit import SkuUnit -# TODO: we need another consumer type that read data from file or predict from model -class ConsumerUnit(UnitBase): - """Unit that used to generate orders to purchase source materials from up stream facility. +class ConsumerUnit(SkuUnit): + """Consumer unit used to generated order to purchase from upstream by action.""" - One consumer per sku. - """ def __init__(self): super(ConsumerUnit, self).__init__() - # TODO: if we need this as state, we can add a new field to consumer - # and fill the field in the post_step method. self.open_orders = defaultdict(Counter) - # attribution cache - self.source_id = 0 - self.quantity = 0 - self.product_id = 0 - self.recieved = 0 + # States in python side. + self.received = 0 self.purchased = 0 self.order_cost = 0 + def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): + """Called after order product is received. + + Args: + source_id (int): Where is the product from. + product_id (int): What product we received. + quantity (int): How many we received. + original_quantity (int): How many we ordered. + """ + self.received += quantity - def initialize(self, configs: dict, durations: int): - if len(self.data.sources) > 0: - # we use 1st source as default one - self.source_id = self.data.sources[0] - self.data.source_id = self.source_id + self.update_open_orders(source_id, product_id, -original_quantity) - self.product_id = self.data.product_id + def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): + """Update the order states. + + Args: + source_id (int): Where is the product from. + product_id (int): What product in the order. + qty_delta (int): Number of product to update (sum). + """ + if qty_delta > 0: + # New order for product. + self.open_orders[source_id][product_id] += qty_delta + else: + # An order is completed, update the remaining number. + self.open_orders[source_id][product_id] += qty_delta + + def initialize(self): + # Update the product_id id in frame, so we will not update later in this episode. + self.data_model.initialize(product_id=self.product_id) def step(self, tick: int): - # NOTE: - # different with original code, we split the control into pieces, - # and put in to frame, so the product_id always has value - # # id == 0 means invalid,as our id is 1 based - if self.quantity <= 0 or self.source_id == 0: + if self.action is None or self.action.quantity <= 0 or self.action.source_id == 0: return - # NOTE: - # we are using facility as source id, not the storage - self.update_open_orders(self.source_id, self.product_id, self.quantity) + # NOTE: we are using product unit as destination, + # so we expect the action.source_id is and id of product unit + self.update_open_orders(self.action.source_id, self.product_id, self.action.quantity) - order = Order(self.facility, self.product_id, self.quantity, self.vlt) + order = Order(self.parent, self.product_id, self.action.quantity, self.action.vlt) - source_facility = self.world.get_facility_by_id(self.source_id) + source_facility = self.world.get_facility_by_id(self.action.source_id) self.order_cost = source_facility.distribution.place_order(order) - self.purchased = self.quantity + self.purchased = self.action.quantity - # clear the action, as it should only be executed once. - self.source_id = 0 - self.quantity = 0 - self.vlt = 0 - - def begin_post_step(self, tick: int): - if self.recieved > 0: - self.data.received = self.recieved - self.data.total_received += self.recieved + def flush_states(self): + if self.received > 0: + self.data_model.received = self.received + self.data_model.total_received += self.received if self.purchased > 0: - self.data.purchased = self.purchased - self.data.total_purchased += self.purchased + self.data_model.purchased = self.purchased + self.data_model.total_purchased += self.purchased if self.order_cost > 0: - self.data.order_product_cost = self.order_cost + self.data_model.order_product_cost = self.order_cost - def end_post_step(self, tick: int): - if self.recieved > 0: - self.data.received = 0 - self.recieved = 0 + def post_step(self, tick: int): + # Clear the action states per step. + if self.action is not None: + self.data_model.source_id = 0 + self.data_model.quantity = 0 + self.data_model.vlt = 0 + + # This will set action to None. + super(ConsumerUnit, self).post_step(tick) + + if self.received > 0: + self.data_model.received = 0 + self.received = 0 if self.purchased > 0: - self.data.purchased = 0 + self.data_model.purchased = 0 self.purchased = 0 if self.order_cost > 0: - self.data.order_product_cost = 0 + self.data_model.order_product_cost = 0 self.order_cost = 0 def reset(self): @@ -90,44 +109,10 @@ def reset(self): self.open_orders.clear() - if len(self.data.sources) > 0: - # we use 1st source as default one - self.source_id = self.data.sources[0] - self.data.source_id = self.source_id - - def set_action(self, action): - # called before step - self.source_id = action.source_id - self.quantity = action.quantity - self.vlt = action.vlt + def set_action(self, action: object): + super(ConsumerUnit, self).set_action(action) # record the action - self.data.source_id = action.source_id - self.data.quantity = action.quantity - self.data.vlt = action.vlt - - def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): - self.recieved += quantity - # self.data.total_received += quantity - # self.data.received += quantity - - self.update_open_orders(source_id, product_id, -original_quantity) - - def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): - if qty_delta > 0: - # new order for product - self.open_orders[source_id][product_id] += qty_delta - else: - # an order is completed, update the remaining number - self.open_orders[source_id][product_id] += qty_delta - - # TODO: refine it later, seems like we do not need this - # if len(self.open_orders[source_id]) == 0: - # del self.open_orders[source_id] - - def get_unit_info(self) -> dict: - info = super(ConsumerUnit, self).get_unit_info() - - info["sku_id"] = self.product_id - - return info + self.data_model.source_id = action.source_id + self.data_model.quantity = action.quantity + self.data_model.vlt = action.vlt diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index f57396cfe..a618d0c97 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -1,37 +1,84 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. -from collections import deque, defaultdict - -from .base import UnitBase +from collections import deque, defaultdict from typing import Dict +from .order import Order +from .unitbase import UnitBase + class DistributionUnit(UnitBase): """Unit that used to receive and execute orders from downstream facilities. One distribution can accept all kind of sku order. """ - def __init__(self): - super(DistributionUnit, self).__init__() + # Transport unit list of this distribution unit. + transports = None + def __init__(self): self.order_queue = deque() - # used to map from product id to slot index + # Used to map from product id to slot index. self.product_index_mapping: Dict[int, int] = {} - def initialize(self, configs: dict, durations: int): - super(DistributionUnit, self).initialize(configs, durations) + self.product_list = [] + + def get_pending_order(self): + """Get orders that states is pending. - # create a production index mapping, used to update product information - for index, product_id in enumerate(self.data.product_list[:]): - self.product_index_mapping[product_id] = index + Returns: + dict: Dictionary of order that key is product id, value is quantity. + """ + counter = defaultdict(int) - def step(self, tick: int): - data = self.data + for order in self.order_queue: + counter[order.product_id] += order.quantity + + return counter + + def place_order(self, order: Order): + """Place an order in the pending queue. + + Args: + order (Order): Order to insert. + """ + if order.quantity > 0: + sku = self.facility.skus[order.product_id] + + if sku is not None: + self.order_queue.append(order) + + order_total_price = sku.price * order.quantity + + # TODO: states related, enable it later if needed. + # product_index = self.product_index_mapping[order.product_id] + # self.data.check_in_price[product_index] += order_total_price + + return order_total_price + + return 0 - for vehicle in self.facility.transports: - # if we have vehicle not on the way and there is any pending order - if len(self.order_queue) > 0 and vehicle.data.location == 0: + def initialize(self): + index = 0 + + # Init product list in data model. + for sku_id, sku in self.facility.skus.items(): + self.product_list.append(sku_id) + + self.data_model.product_list.append(sku_id) + self.data_model.delay_order_penalty.append(0) + self.data_model.check_in_price.append(0) + + self.product_index_mapping[sku_id] = index + + index += 1 + + def step(self, tick: int): + for vehicle in self.transports: + # If we have vehicle not on the way and there is any pending order + if len(self.order_queue) > 0 and vehicle.location == 0: order = self.order_queue.popleft() # schedule a job for vehicle @@ -43,38 +90,23 @@ def step(self, tick: int): order.vlt ) + # Push vehicle. + vehicle.step(tick) + # NOTE: we moved delay_order_penalty from facility to sku, is this ok? # update order's delay penalty per tick. for order in self.order_queue: - sku = self.facility.sku_information[order.product_id] + sku = self.facility.skus[order.product_id] product_index = self.product_index_mapping[order.product_id] - data.delay_order_penalty[product_index] += sku.delay_order_penalty + self.data_model.delay_order_penalty[product_index] += sku.delay_order_penalty def reset(self): super(DistributionUnit, self).reset() - self.order_queue.clear() - - def get_pending_order(self): - counter = defaultdict(int) - - for order in self.order_queue: - counter[order.product_id] += order.quantity - return counter - - def place_order(self, order): - if order.quantity > 0: - sku = self.facility.sku_information[order.product_id] - - if sku is not None: - self.order_queue.append(order) - - product_index = self.product_index_mapping[order.product_id] - order_total_price = sku.price * order.quantity - - self.data.check_in_price[product_index] += order_total_price - - return order_total_price + self.order_queue.clear() - return 0 + for product_id in self.product_list: + self.data_model.product_list.append(product_id) + self.data_model.delay_order_penalty.append(0) + self.data_model.check_in_price.append(0) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index fdb1ff249..370f25d3f 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -1,34 +1,42 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. -from .base import UnitBase -from .actions import ManufactureAction +from maro.simulator.scenarios.supply_chain.actions import ManufactureAction +from .skuunit import SkuUnit -class ManufactureUnit(UnitBase): +class ManufactureUnit(SkuUnit): """Unit that used to produce certain product(sku) with consume specified source skus. One manufacture unit per sku. """ - # source material sku and related number per produce cycle + + # Source material sku and related number per produce cycle. bom = None - # how many production unit each produce cycle + # How many production unit each produce cycle. output_units_per_lot = None - # how many unit we will consume each produce cycle + # How many unit we will consume each produce cycle. input_units_per_lot = 0 - def __init__(self): - super(ManufactureUnit, self).__init__() + # How many we procedure per current step. + manufacture_number = 0 + + def initialize(self): + # TODO: add storage id to data model. + product_unit_cost = self.config.get("product_unit_cost", 0) + + self.data_model.initialize( + output_product_id=self.product_id, + product_unit_cost=product_unit_cost, + storage_id=self.facility.storage.id + ) - def initialize(self, configs: dict, durations: int): - # add the storage_id - configs["data"]["storage_id"] = self.facility.storage.id - - super().initialize(configs, durations) + # Grab bom of current production. + sku = self.world.get_sku_by_id(self.product_id) - # grab bom of current production - sku = self.world.get_sku_by_id(self.data.product_id) self.bom = sku.bom self.output_units_per_lot = sku.output_units_per_lot @@ -36,34 +44,33 @@ def initialize(self, configs: dict, durations: int): self.input_units_per_lot = sum(self.bom.values()) def step(self, tick: int): - # try to produce production if we have positive rate - data = self.data + # Try to produce production if we have positive rate. + if self.action is not None and self.action.production_rate > 0: + sku_num = len(self.facility.skus) + unit_num_upper_bound = self.facility.storage.capacity // sku_num - if data.production_rate > 0: - sku_num = len(self.facility.sku_information) - unit_num_upper_bound = self.facility.storage.data.capacity // sku_num - - # compare with avg storage number - current_product_number = self.facility.storage.get_product_number(data.product_id) + # Compare with avg storage number. + current_product_number = self.facility.storage.get_product_number(self.product_id) max_number_to_procedure = min( unit_num_upper_bound - current_product_number, - data.production_rate * self.output_units_per_lot, - self.facility.storage.data.remaining_space + self.action.production_rate * self.output_units_per_lot, + self.facility.storage.remaining_space ) if max_number_to_procedure > 0: space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot - # consider about the volume, we can produce all if space take per cycle <=1 + # Consider about the volume, we can produce all if space take per cycle <=1. if space_taken_per_cycle > 1: max_number_to_procedure = max_number_to_procedure // space_taken_per_cycle source_sku_to_take = {} - # do we have enough source material? + # Do we have enough source material? for source_sku_id, source_sku_cost_number in self.bom.items(): source_sku_available_number = self.facility.storage.get_product_number(source_sku_id) - max_number_to_procedure = min(source_sku_available_number // source_sku_cost_number, max_number_to_procedure) + max_number_to_procedure = min(source_sku_available_number // source_sku_cost_number, + max_number_to_procedure) if max_number_to_procedure <= 0: break @@ -71,17 +78,21 @@ def step(self, tick: int): source_sku_to_take[source_sku_id] = max_number_to_procedure * source_sku_cost_number if max_number_to_procedure > 0: - data.manufacturing_number += max_number_to_procedure - self.facility.storage.try_take_units(source_sku_to_take) + self.manufacture_number = max_number_to_procedure + self.facility.storage.try_take_products(source_sku_to_take) + + def flush_states(self): + if self.manufacture_number > 0: + self.data_model.manufacturing_number = self.manufacture_number - def end_post_step(self, tick: int): - # super(ManufactureUnit, self).post_step(tick) + def post_step(self, tick: int): + if self.manufacture_number > 0: + self.data_model.manufacturing_number = 0 - # reset the manufacture cost per tick - self.data.manufacturing_number = 0 + if self.action is not None: + self.data_model.production_rate = 0 def set_action(self, action: ManufactureAction): - # we expect production rate number as action - # production_rate <= 0 will stop manufacturing - self.data.production_rate = action.production_rate + super(ManufactureUnit, self).set_action(action) + self.data_model.production_rate = action.production_rate diff --git a/maro/simulator/scenarios/supply_chain/units/order.py b/maro/simulator/scenarios/supply_chain/units/order.py index 54a71238d..8c153ac2b 100644 --- a/maro/simulator/scenarios/supply_chain/units/order.py +++ b/maro/simulator/scenarios/supply_chain/units/order.py @@ -1,3 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + # TODO: original code included the order in raw state, but calculate the price in final state # so we do not need to put it in the frame, just calculate the total_price per tick/step. diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py new file mode 100644 index 000000000..ffb6e766e --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -0,0 +1,112 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .consumer import ConsumerUnit +from .manufacture import ManufactureUnit +from .seller import SellerUnit +from .skuunit import SkuUnit +from .storage import StorageUnit + + +class ProductUnit(SkuUnit): + """Unit that used to group units of one special sku, usually contains consumer, seller and manufacture.""" + + # Consumer unit of current sku. + consumer: ConsumerUnit = None + + # Seller unit of current sku. + seller: SellerUnit = None + + # Manufacture unit of this sku. + manufacture: ManufactureUnit = None + + # Storage of this facility, always a reference of facility.storage. + storage: StorageUnit = None + + def step(self, tick: int): + if self.consumer is not None: + self.consumer.step(tick) + + if self.manufacture is not None: + self.manufacture.step(tick) + + if self.seller is not None: + self.seller.step(tick) + + def flush_states(self): + if self.consumer is not None: + self.consumer.flush_states() + + if self.manufacture is not None: + self.manufacture.flush_states() + + if self.seller is not None: + self.seller.flush_states() + + def post_step(self, tick: int): + super(ProductUnit, self).post_step(tick) + + if self.consumer is not None: + self.consumer.post_step(tick) + + if self.manufacture is not None: + self.manufacture.post_step(tick) + + if self.seller is not None: + self.seller.post_step(tick) + + def reset(self): + super(ProductUnit, self).reset() + + if self.consumer is not None: + self.consumer.reset() + + if self.manufacture is not None: + self.manufacture.reset() + + if self.seller is not None: + self.seller.reset() + + def get_unit_info(self) -> dict: + return { + "id": self.id, + "sku_id": self.product_id, + "node_name": type(self.data_model).__node_name__ if self.data_model is not None else None, + "node_index": self.data_model_index if self.data_model is not None else None, + "class": type(self), + "consumer": self.consumer.get_unit_info() if self.consumer is not None else None, + "seller": self.seller.get_unit_info() if self.seller is not None else None, + "manufacture": self.manufacture.get_unit_info() if self.manufacture is not None else None + } + + @staticmethod + def generate(facility, config: dict): + """Generate product unit by sku information. + + Args: + facility (FacilityBase): Facility this product belongs to. + config (dict): Config of children unit. + """ + instance_list = {} + + if facility.skus is not None and len(facility.skus) > 0: + world = facility.world + + for sku_id, sku in facility.skus.items(): + product = ProductUnit() + + for child_name in ("consumer", "manufacture", "seller"): + conf = config.get(child_name, None) + + if conf is not None: + sub_agent = world.build_unit(facility, None, conf) + sub_agent.product_id = sku_id + + setattr(product, child_name, sub_agent) + + sub_agent.parse_configs(conf) + + instance_list[sku_id] = product + + return instance_list diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index b693a767f..e2cbf24dc 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -1,14 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + import numpy as np -import random -from .base import UnitBase +from .skuunit import SkuUnit -class SellerUnit(UnitBase): +class SellerUnit(SkuUnit): """ Unit that used to generate product consume demand, and move demand product from current storage. """ + def __init__(self): super(SellerUnit, self).__init__() @@ -16,20 +19,38 @@ def __init__(self): self.durations = 0 self.demand_distribution = [] - # attribute cache + # Attribute cache. self.sold = 0 self.demand = 0 self.total_sold = 0 self.product_id = 0 - def initialize(self, configs: dict, durations: int): - super(SellerUnit, self).initialize(configs, durations) + def market_demand(self, tick: int) -> int: + """Generate market demand for current tick. + + Args: + tick (int): Current simulator tick. + + Returns: + int: Demand number. + """ + return int(self.demand_distribution[tick]) + + def initialize(self): + unit_price = self.config.get("unit_price", 0) + self.gamma = self.config.get("sale_gamma", 0) + backlog_ratio = self.config.get("backlog_ratio", 1) + + self.data_model.initialize( + unit_price=unit_price, + sale_gamma=self.gamma, + product_id=self.product_id, + backlog_ratio=backlog_ratio + ) - self.durations = durations - self.gamma = self.data.sale_gamma - self.product_id = self.data.product_id + self.durations = self.world.durations - for _ in range(durations): + for _ in range(self.durations): self.demand_distribution.append(np.random.gamma(self.gamma)) def step(self, tick: int): @@ -42,18 +63,19 @@ def step(self, tick: int): self.sold = sold_qty self.demand = demand - def begin_post_step(self, tick: int): - self.data.sold = self.sold - self.demand = self.demand - self.data.total_sold = self.total_sold + def flush_states(self): + self.data_model.sold = self.sold + self.data_model.demand = self.demand + self.data_model.total_sold = self.total_sold + + def post_step(self, tick: int): + super(SellerUnit, self).post_step(tick) - def end_post_step(self, tick: int): - # super(SellerUnit, self).post_step(tick) if self.sold > 0: - self.data.sold = 0 + self.data_model.sold = 0 if self.demand > 0: - self.data.demand = 0 + self.data_model.demand = 0 def reset(self): super(SellerUnit, self).reset() @@ -63,6 +85,3 @@ def reset(self): # for _ in range(self.durations): # self.demand_distribution.append(np.random.gamma(self.gamma)) - - def market_demand(self, tick: int): - return int(self.demand_distribution[tick]) diff --git a/maro/simulator/scenarios/supply_chain/units/skuunit.py b/maro/simulator/scenarios/supply_chain/units/skuunit.py new file mode 100644 index 000000000..36dcfc61f --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/skuunit.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .unitbase import UnitBase + + +class SkuUnit(UnitBase): + """A sku related unit.""" + + # Product id (sku id), 0 means invalid. + product_id: int = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index a0ddfd0a0..0476d4c4d 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -1,46 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. -from .base import UnitBase from typing import Dict +from .unitbase import UnitBase + class StorageUnit(UnitBase): - """Unit that used to store skus. - """ + """Unit that used to store skus.""" def __init__(self): super().__init__() - # used to map from product id to slot index - - # we use these variables to hold changes at python side, flash to frame before taking snapshot + # We use these variables to hold changes at python side, flash to frame before taking snapshot. self.product_number = [] + self.product_list = [] + + # Used to map from product id to slot index. self.product_index_mapping: Dict[int, int] = {} self.capacity = 0 self.remaining_space = 0 + self.unit_storage_cost = 0 - def initialize(self, configs: dict, durations: int): - super().initialize(configs, durations) - - self.capacity = self.data.capacity - self.remaining_space = self.capacity - - for index, product_id in enumerate(self.data.product_list[:]): - product_number = self.data.product_number[index] - - self.product_number.append(product_number) - self.product_index_mapping[product_id] = index + def try_add_products(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: + """Try to add products into storage. - self.remaining_space -= product_number + Args: + product_quantities (Dict[int, int]): Dictionary of product id and quantity need to add to storage. + all_or_nothing (bool): Failed if all product cannot be added, or add as many as it can. Default is True. - def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: + Returns: + dict: Dictionary of product id and quantity success added. + """ if all_or_nothing and self.remaining_space < sum(product_quantities.values()): return {} unloaded_quantities = {} for product_id, quantity in product_quantities.items(): - unload_quantity = min(self.data.remaining_space, quantity) + unload_quantity = min(self.remaining_space, quantity) product_index = self.product_index_mapping[product_id] self.product_number[product_index] += unload_quantity @@ -48,7 +47,16 @@ def try_add_units(self, product_quantities: Dict[int, int], all_or_nothing=True) return unloaded_quantities - def try_take_units(self, product_quantities: Dict[int, int]): + def try_take_products(self, product_quantities: Dict[int, int]) -> bool: + """Try to take specified number of product. + + Args: + product_quantities (Dict[int, int]): Dictionary of product id and quantity to take from storage. + + Returns: + bool: Is success to take? + """ + # Check if we can take all kinds of products? for product_id, quantity in product_quantities.items(): product_index = self.product_index_mapping[product_id] @@ -56,6 +64,7 @@ def try_take_units(self, product_quantities: Dict[int, int]): return False # TODO: refactoring for dup code + # Take from storage. for product_id, quantity in product_quantities.items(): product_index = self.product_index_mapping[product_id] @@ -63,7 +72,16 @@ def try_take_units(self, product_quantities: Dict[int, int]): return True - def take_available(self, product_id: int, quantity: int): + def take_available(self, product_id: int, quantity: int) -> int: + """Take as much as available specified product from storage. + + Args: + product_id (int): Product to take. + quantity (int): Max quantity to take. + + Returns: + int: Actual quantity taken. + """ product_index = self.product_index_mapping[product_id] available = self.product_number[product_index] actual = min(available, quantity) @@ -73,27 +91,61 @@ def take_available(self, product_id: int, quantity: int): return actual def get_product_number(self, product_id: int) -> int: + """Get product number in storage. + + Args: + product_id (int): Product to check. + + Returns: + int: Available number of product. + """ product_index = self.product_index_mapping[product_id] return self.product_number[product_index] - def begin_post_step(self, tick: int): - # write the changes to frame - for i, number in enumerate(self.product_number): - self.data.product_number[i] = self.product_number[i] - - self.data.remaining_space = self.remaining_space + def initialize(self): + self.capacity = self.config.get("capacity", 100) + self.unit_storage_cost = self.config.get("unit_storage_cost", 1) - def reset(self): - super().reset() + for sku in self.facility.skus.values(): + self.product_list.append(sku.id) + self.product_number.append(sku.init_in_stock) + # TODO: init data model self.remaining_space = self.capacity - # TODO: dup code - for index, product_id in enumerate(self.data.product_list[:]): - product_number = self.data.product_number[index] + for index, product_id in enumerate(self.product_list): + product_number = self.product_number[index] + self.data_model.product_list.append(product_id) + self.data_model.product_number.append(product_number) - self.product_number.append(product_number) self.product_index_mapping[product_id] = index self.remaining_space -= product_number + + self.data_model.initialize( + capacity=self.capacity, + unit_storage_cost=self.unit_storage_cost, + remaining_space=self.remaining_space + ) + + def flush_states(self): + # Write the changes to frame. + for i, number in enumerate(self.product_number): + self.data_model.product_number[i] = self.product_number[i] + + self.data_model.remaining_space = self.remaining_space + + def reset(self): + super(StorageUnit, self).reset() + + self.product_number.clear() + + for sku in self.facility.skus.values(): + self.product_number.append(sku.init_in_stock) + + for index, product_id in enumerate(self.product_list): + product_number = self.product_number[index] + + self.data_model.product_list.append(product_id) + self.data_model.product_number.append(product_number) diff --git a/maro/simulator/scenarios/supply_chain/units/transport.py b/maro/simulator/scenarios/supply_chain/units/transport.py deleted file mode 100644 index 6d1a05575..000000000 --- a/maro/simulator/scenarios/supply_chain/units/transport.py +++ /dev/null @@ -1,135 +0,0 @@ - -from .base import UnitBase - - -class TransportUnit(UnitBase): - """Unit used to move production from source to destination by order.""" - def __init__(self): - super().__init__() - - # max patient of current one transport - self.max_patient = None - - # current products' destination - self.destination = None - - self.path = None - - def reset(self): - super(TransportUnit, self).reset() - - self.destination = None - self.max_patient = None - self.path = None - - def schedule(self, destination, product_id: int, quantity: int, vlt): - self.data.destination = destination.id - self.data.product_id = product_id - self.data.requested_quantity = quantity - self.data.vlt = vlt - - self.destination = destination - # keep the patient, reset it after product unloaded. - self.max_patient = self.data.patient - - # Find the path from current entity to target. - self.path = self.world.find_path( - self.facility.x, - self.facility.y, - destination.x, - destination.y - ) - - if self.path is None: - raise Exception(f"Destination {destination} is unreachable") - - # Steps to destination. - self.data.steps = len(self.path) // vlt - - # We are waiting for product loading. - self.data.location = 0 - - def try_loading(self, quantity: int): - if self.facility.storage.try_take_units({self.data.product_id: quantity}): - self.data.payload = quantity - - return True - else: - self.data.patient -= 1 - - return False - - def try_unloading(self): - unloaded = self.destination.storage.try_add_units( - {self.data.product_id: self.data.payload}, - all_or_nothing=False - ) - - # update order if we unloaded any - if len(unloaded) > 0: - unloaded_units = sum(unloaded.values()) - - self.destination.consumers[self.data.product_id].on_order_reception( - self.facility.id, - self.data.product_id, - unloaded_units, - self.data.payload - ) - - # reset the transport's state - self.data.payload = 0 - self.data.patient = self.max_patient - - def step(self, tick: int): - data = self.data - - # If we have not arrive at destination yet. - if data.steps > 0: - # if we still not loaded enough productions yet. - if data.location == 0 and data.payload == 0: - # then try to load by requested. - if self.try_loading(data.requested_quantity): - # NOTE: here we return to simulate loading - return - else: - data.patient -= 1 - - # Failed to load, check the patient. - if data.patient < 0: - self.destination.consumers[data.product_id].update_open_orders( - self.facility.id, - data.product_id, - -data.requested_quantity - ) - - # reset - data.steps = 0 - data.location = 0 - data.destination = 0 - data.position[:] = -1 - - # Moving to destination - if data.payload > 0: - # Closer to destination until 0. - - data.location += data.vlt - data.steps -= 1 - - if data.location > len(self.path): - data.location = len(self.path) - 1 - - data.position[:] = self.path[data.location-1] - else: - # avoid update under idle state. - if data.location > 0: - # try to unload - if data.payload > 0: - self.try_unloading() - - # back to source if we unload all - if data.payload == 0: - self.destination = 0 - data.steps = 0 - data.location = 0 - data.destination = 0 - data.position[:] = -1 diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py new file mode 100644 index 000000000..e3c4f6927 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -0,0 +1,108 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +class UnitBase: + """Base of all unit used to contain related logic. + + Typically one unit instance should bind to a data model instance, + that used to update states in frame. + + An unit will have following steps to initializing. + . Create instance with default constructor without parameter, means all unit constructor should not have parameter + or with default value. + . Unit.parse_configs is called with configurations from parent or config file. + . After frame is constructed, Unit.initialize is called to do data model related initializing. + . At the beginning of business_engine.step, Unit.step is called to go through related logic, + then after all the agent completed, Unit.flush_state will be called at the end of business_engine.step, + to tell agents to save their states. + . Unit.post_step is called in business_engine.post_step after step is finished. + . Unit.set_action is called when there is any action from out-side. + + """ + # Id of this unit. + id: int = 0 + + # Which this unit belongs to. + facility = None + + # Which world this unit belongs to. + world = None + + # Parent of this unit, it can be a facility or another unit. + parent: object = None + + # Child units, extended unit can add their own child as property, this is used as a collection. + children: list = None + + # Data model name in the frame, used to query binding data model instance. + data_model_name: str = None + + # Data model instance index in the frame, used to query binding data model instance. + data_model_index: int = None + + # Real data model binding with this unit. + data_model = None + + # Current action. + action: object = None + + # Current unit configurations. + config: dict = None + + def parse_configs(self, config: dict): + """Parse configurations from config. + + Args: + config (dict): Configuration from parent or config file. + """ + self.config = config + + def step(self, tick: int): + """Run related logic for current tick. + + Args: + tick (int): Current simulator tick. + """ + pass + + def flush_states(self): + """Flush states into frame for current tick. + """ + pass + + def post_step(self, tick: int): + """Post-processing for current step. + + Args: + tick (int): Current simulator tick. + """ + self.action = None + + def reset(self): + """Reset this unit for a new episode.""" + if self.data_model is not None: + self.data_model.reset() + + def initialize(self): + """Initialize this unit after data model is ready to use. + + NOTE: unit.data_model is available from this step. + """ + pass + + def set_action(self, action: object): + """Set action for this agent. + + Args: + action (object): Action from outside. + """ + self.action = action + + def get_unit_info(self) -> dict: + return { + "id": self.id, + "node_name": type(self.data_model).__node_name__, + "node_index": self.data_model_index, + "class": type(self) + } diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py new file mode 100644 index 000000000..d2b72e2ff --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -0,0 +1,228 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .product import ProductUnit +from .unitbase import UnitBase + + +class VehicleUnit(UnitBase): + """Unit used to move production from source to destination by order.""" + + def __init__(self): + # Max patient of current vehicle. + self.max_patient: int = None + + # Current products' destination. + self.destination: ProductUnit = None + + # Path to destination. + self.path: list = None + + # Product to load + self.product_id = 0 + + # Steps to arrive destination. + self.steps = 0 + + # Payload on vehicle. + self.payload = 0 + + # Which product unit current product related to. + self.product: ProductUnit = None + + # Current location in the path. + self.location = 0 + + # Id of destination. + self.destination_id = 0 + + # Velocity. + self.velocity = 0 + + def schedule(self, destination: ProductUnit, product_id: int, quantity: int, vlt: int): + """Schedule a job for this vehicle. + + Args: + destination (ProductUnit): Destination product unit. + product_id (int): What load from storage. + quantity (int): How many to load. + vlt (int): Velocity of vehicle. + """ + # Keep these in states, we will not update it until we reach the destination or cancelled. + self.data_model.destination = destination.id + self.data_model.product_id = product_id + self.data_model.requested_quantity = quantity + self.data_model.vlt = vlt + + # Cache. + self.product_id = product_id + self.destination = destination + + # Find product related in our facility. + for product in self.facility.products: + if product.product_id == product_id: + self.product = product + + # Keep the patient, reset it after product unloaded. + self.max_patient = self.data_model.patient + + # Find the path from current entity to target. + self.path = self.world.find_path( + self.facility.x, + self.facility.y, + destination.facility.x, + destination.facility.y + ) + + if self.path is None: + raise Exception(f"Destination {destination} is unreachable") + + # Steps to destination. + self.steps = len(self.path) // vlt + + # We are waiting for product loading. + self.location = 0 + + self.velocity = vlt + + def try_load(self, quantity: int) -> bool: + """Try to load specified number of scheduled product. + + Args: + quantity (int): Number to load. + """ + if self.facility.storage.try_take_products({self.product_id: quantity}): + self.payload = quantity + + # Write to frame, as we do not need to update it per tick. + self.data_model.payload = quantity + + return True + else: + self.data_model.patient -= 1 + + return False + + def try_unload(self): + """Try unload products into destination's storage.""" + unloaded = self.destination.storage.try_add_products( + {self.product_id: self.payload}, + all_or_nothing=False + ) + + # Update order if we unloaded any. + if len(unloaded) > 0: + unloaded_units = sum(unloaded.values()) + + self.destination.consumer.on_order_reception( + self.product.id, + self.product_id, + unloaded_units, + self.payload + ) + + # Reset the state. + self.data_model.payload = 0 + self.payload = 0 + self.data_model.patient = self.max_patient + self.data_model.position[:] = -1 + + def initialize(self): + patient = self.config.get("patient", 100) + unit_transport_cost = self.config.get("unit_transport_cost", 1) + + self.data_model.initialize(patient=patient, unit_transport_cost=unit_transport_cost) + + self.data_model.position[:] = -1 + + def step(self, tick: int): + # If we have not arrive at destination yet. + if self.steps > 0: + # if we still not loaded enough productions yet. + if self.location == 0 and self.payload == 0: + # then try to load by requested. + request_quantity = self.data_model.requested_quantity + + if self.try_load(request_quantity): + # NOTE: here we return to simulate loading + return + else: + self.data_model.patient -= 1 + + # Failed to load, check the patient. + if self.data_model.patient < 0: + self.destination.consumer.update_open_orders( + self.product.id, + self.product_id, + -request_quantity + ) + + # reset + self.steps = 0 + self.location = 0 + self.destination = None + self.destination_id = 0 + + self._reset_data_model() + + # Moving to destination + if self.payload > 0: + # Closer to destination until 0. + + self.location += self.velocity + self.steps -= 1 + + if self.location > len(self.path): + self.location = len(self.path) - 1 + + self.data_model.position[:] = self.path[self.location] + else: + # avoid update under idle state. + if self.location > 0: + # try to unload + if self.payload > 0: + self.try_unload() + + # back to source if we unload all + if self.payload == 0: + self.destination = None + self.destination_id = 0 + self.steps = 0 + self.location = 0 + self.destination = 0 + + # Reset data model + self._reset_data_model() + + def flush_states(self): + if self.payload > 0: + self.data_model.payload = self.payload + + if self.steps > 0: + self.data_model.steps = self.steps + + def reset(self): + super(VehicleUnit, self).reset() + + self.destination = None + self.destination_id = 0 + self.max_patient = None + self.path = None + self.product = None + self.payload = 0 + self.product_id = 0 + self.steps = 0 + self.location = 0 + + def _reset_data_model(self): + # Reset data model. + self.data_model.steps = 0 + self.data_model.destination = 0 + self.data_model.product_id = 0 + self.data_model.requested_quantity = 0 + self.data_model.vlt = 0 + self.data_model.payload = 0 + self.data_model.patient = self.max_patient + + self.data_model.position[:] = -1 diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index ce45da010..cf828361a 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -1,100 +1,150 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. -import numpy as np -from collections import defaultdict, namedtuple +from collections import namedtuple +from typing import List, Union, Tuple +import numpy as np +from maro.backends.frame import FrameBase +from tcod.path import AStar + +from .facilities import FacilityBase from .frame_builder import build_frame +from .parser import SupplyChainConfiguration, DataModelDef, UnitDef, FacilityDef +from .units import UnitBase -from tcod.path import AStar +SkuInfo = namedtuple("SkuInfo", ("name", "id", "bom", "output_units_per_lot")) -from .config_parser import SupplyChainConfiguration -from .data import FacilityDataModel +class World: + """Supply chain world contains facilities and grid base map.""" + def __init__(self): + # Frame for current world configuration. + self.frame: FrameBase = None -# sku definition in world level -# bom is a dictionary, key is the material sku id, value is units per lot -Sku = namedtuple("Sku", ("name", "id", "bom", "output_units_per_lot")) + # Current configuration. + self.configs: SupplyChainConfiguration = None + # Durations of current simulation. + self.durations = 0 -class World: - def __init__(self): - # all the facilities in this world, key: id, value: facilities + # All the entities in the world, include unit and facility. + self.entities = {} + + # All the facilities in this world. self.facilities = {} - # mapping from facility name to id + # Entity id counter, every unit and facility have unique id. + self._id_counter = 1 + + # Path finder for production transport. + self._path_finder: AStar = None + + # Sku id to name mapping, used for querying. + self._sku_id2name_mapping = {} + + # All the sku in this world. + self._sku_collection = {} + + # Facility name to id mapping, used for querying. self._facility_name2id_mapping = {} - # all the entities (units and facilities) in this world - # id -> instance - self._entities = {} + # Data model class collection, used to collection data model class and their number in frame. + self._data_class_collection = {} - # frame of this world, this is determined by the unit selected. - self.frame = None + def get_sku_by_name(self, name: str) -> SkuInfo: + """Get sku information by name. - # id counter for all units and facilities in this world - self._id_counter = 1 + Args: + name (str): Sku name to query. - # collection of data model class used in this world. - self._data_class_collection = defaultdict(int) + Returns: + SkuInfo: General information for sku. + """ + return self._sku_collection.get(name, None) - # sku collection of this world - self._sku_collection = {} + def get_sku_by_id(self, sku_id: int) -> SkuInfo: + """Get sku information by sku id. - # sku id -> name in collection - self._sku_id2name_mapping = {} + Args: + sku_id (int): Id of sku to query. - # configuration of current world - self.configs: dict = None + Returns: + SkuInfo: General information for sku. + """ + return self._sku_collection[self._sku_id2name_mapping[sku_id]] - # unit id to related data model index - self.unit_id2index_mapping = {} + def get_facility_by_id(self, facility_id: int) -> FacilityBase: + """Get facility by id. - # a star path finder - self._path_finder: AStar = None + Args: + facility_id (int): Facility id to query. - self._data_model_definitions = None - self._facility_definitions = None - self._unit_definitions = None + Returns: + FacilityBase: Facility instance. + """ + return self.facilities[facility_id] - def gen_id(self): - """Generate id for facility or unit.""" - new_id = self._id_counter + def get_facility_by_name(self, name: str): + """Get facility by name. - self._id_counter += 1 + Args: + name (str): Facility name to query. - return new_id + Returns: + FacilityBase: Facility instance. + """ + return self.facilities[self._facility_name2id_mapping[name]] + + def get_entity(self, entity_id: int) -> Union[FacilityBase, UnitBase]: + """Get an entity (facility or unit) by id. - def build_unit(self, name: str): - """Build an unit instance from it name via current configuration.""" - assert name in self._unit_definitions + Args: + entity_id (int): Id to query. + + Returns: + Union[FacilityBase, UnitBase]: Entity instance. + """ + return self._entities[entity_id] - unit = self._unit_definitions[name].class_type() + def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: + """Find path to specified cell. - unit.id = self.gen_id() + Args: + start_x (int): Start cell position x. + start_y (int): Start cell position y. + goal_x (int): Destination cell position x. + goal_y (int): Destination cell position y. - self._entities[unit.id] = unit + Returns: + List[Tuple[int, int]]: List of (x, y) position to target. + """ + return self._path_finder.get_path(int(start_x), int(start_y), int(goal_x), int(goal_y)) - return unit + def build(self, configs: SupplyChainConfiguration, snapshot_number: int, durations: int): + """Build world with configurations. - def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: int, durations: int): - """Build current world according to configurations.""" - self.configs = all_in_one_config.world - self._facility_definitions = all_in_one_config.facilities - self._unit_definitions = all_in_one_config.units - self._data_model_definitions = all_in_one_config.data_models + Args: + configs (SupplyChainConfiguration): Configuration of current world. + snapshot_number (int): Number of snapshots to keep in memory. + durations (int): Durations of current simulation. + """ + self.durations = durations + self.configs = configs - configs = self.configs + world_config = configs.world - # collect sku information first - for sku_conf in configs["skus"]: - sku = Sku(sku_conf["name"], sku_conf["id"], {}, sku_conf["output_units_per_lot"]) + # Grab sku information for this world. + for sku_conf in world_config["skus"]: + sku = SkuInfo(sku_conf["name"], sku_conf["id"], {}, sku_conf["output_units_per_lot"]) self._sku_id2name_mapping[sku.id] = sku.name self._sku_collection[sku.name] = sku - # collect bom - for sku_conf in configs["skus"]: + # Collect bom info. + for sku_conf in world_config["skus"]: sku = self._sku_collection[sku_conf["name"]] bom = sku_conf.get("bom", {}) @@ -102,51 +152,44 @@ def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: in for material_sku_name, units_per_lot in bom.items(): sku.bom[self._sku_collection[material_sku_name].id] = units_per_lot - # build facilities first - for facility_conf in configs["facilities"]: - facility_name = facility_conf["name"] + # Construct facilities. + for facility_conf in world_config["facilities"]: + facility_class_alias = facility_conf["class"] + facility_def: FacilityDef = self.configs.facilities[facility_class_alias] + facility_class_type = facility_def.class_type - # create a new instance of facility - facility_def = self._facility_definitions[facility_conf["class"]] - facility = facility_def.class_type() + # Instance of facility. + facility = facility_class_type() - # NOTE: DO set these fields before other operations. + # Normal properties. + facility.id = self._gen_id() + facility.name = facility_conf["name"] facility.world = self - facility.id = self.gen_id() - facility.name = facility_name - self._facility_name2id_mapping[facility_name] = facility.id - self.facilities[facility.id] = facility - self._entities[facility.id] = facility + # Parse sku info. + facility.parse_skus(facility_conf["skus"]) - # build the facility first to create related components. - facility.build(facility_conf["configs"]) + # Parse config for facility. + facility.parse_configs(facility_conf.get("config", {})) - # build the frame - # . collect data model class - data_class_in_frame = [] + # Build children (units). + for child_name, child_conf in facility_conf["children"].items(): + setattr(facility, child_name, self.build_unit(facility, facility, child_conf)) - for class_name, number in self._data_class_collection.items(): - class_def = self._data_model_definitions[class_name] + self.facilities[facility.id] = facility - data_class_in_frame.append(( - class_def.class_type, - class_def.name_in_frame, - number - )) + self._facility_name2id_mapping[facility.name] = facility.id - # add facility data model to frame - data_class_in_frame.append(( - FacilityDataModel, - "facilities", - len(self.facilities), - )) + # Build frame. + self.frame = self._build_frame(snapshot_number) - # . build the frame - self.frame = build_frame(True, snapshot_number, data_class_in_frame) + # Assign data model instance. + for agent in self.entities.values(): + if agent.data_model_name is not None: + agent.data_model = getattr(self.frame, agent.data_model_name)[agent.data_model_index] - # construct the upstream topology - topology = configs.get("topology", {}) + # Construct the upstream topology. + topology = world_config["topology"] for cur_facility_name, topology_conf in topology.items(): facility = self.get_facility_by_name(cur_facility_name) @@ -154,29 +197,31 @@ def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: in facility.upstreams = {} for sku_name, source_facilities in topology_conf.items(): - sku = self.get_sku(sku_name) + sku = self.get_sku_by_name(sku_name) - facility.upstreams[sku.id] = [self.get_facility_by_name(source_name).id for source_name in source_facilities] + facility.upstreams[sku.id] = [ + self.get_facility_by_name(source_name).id for source_name in source_facilities + ] - # then initialize all facilities as we have the data instance. - facility_node_index = 0 + # Call initialize method for facilities. + for facility in self.facilities.values(): + facility.initialize() - for _, facility in self.facilities.items(): - facility.data = self.frame.facilities[facility_node_index] - facility_node_index += 1 + # Call initialize method for units. + for agent in self.entities.values(): + agent.initialize() - facility.initialize(durations) - - # construct the map grid - grid_config = configs["grid"] + # TODO: replace tcod with other lib. + # Construct the map grid. + grid_config = world_config["grid"] grid_width, grid_height = grid_config["size"] - # travel cost for a star path finder, 0 means block, > 1 means the cost travel to that cell + # Travel cost for a star path finder, 0 means block, > 1 means the cost travel to that cell # current all traversable cell's cost will be 1. cost_grid = np.ones(shape=(grid_width, grid_height), dtype=np.int8) - # add blocks to grid + # Add blocks to grid. for facility_name, facility_pos in grid_config["facilities"].items(): facility_id = self._facility_name2id_mapping[facility_name] facility = self.facilities[facility_id] @@ -184,7 +229,7 @@ def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: in facility.x = facility_pos[0] facility.y = facility_pos[1] - # facility cannot be a block, or we cannot find path to it, + # Facility cannot be a block, or we cannot find path to it, # but we can give it a big cost cost_grid[facility.x, facility.y] = 120 @@ -194,49 +239,96 @@ def build(self, all_in_one_config: SupplyChainConfiguration, snapshot_number: in # 0 for 2nd parameters means disable diagonal movement, so just up, right, down or left. self._path_finder = AStar(cost_grid, 0) - def get_facility_by_id(self, facility_id: int): - return self.facilities[facility_id] + def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBase], config: dict) -> UnitBase: + """Build an unit by its configuration. - def get_facility_by_name(self, name: str): - return self.facilities[self._facility_name2id_mapping[name]] + Args: + facility (FacilityBase): Facility of this unit belongs to. + parent (Union[FacilityBase, UnitBase]): Parent of this unit belongs to, this may be same with facility, if + this unit is attached to a facility. + config (dict): Configuration of this unit. - def get_entity(self, entity_id: int): - return self._entities[entity_id] + Returns: + UnitBase: Unit instance. + """ + unit_class_alias = config["class"] + unit_def: UnitDef = self.configs.units[unit_class_alias] - def register_data_class(self, unit_id: int, name: str): - assert name in self._data_model_definitions + is_template = config.get("is_template", False) - node_index = self._data_class_collection[name] + # If it is not a template, then just use current configuration to generate unit. + if not is_template: + unit_instance = unit_def.class_type() - self._data_class_collection[name] += 1 - self.unit_id2index_mapping[unit_id] = node_index + # Assign normal properties. + unit_instance.id = self._gen_id() + unit_instance.world = self + unit_instance.facility = facility + unit_instance.parent = parent - return node_index + # Record the id. + self.entities[unit_instance.id] = unit_instance - def get_data_instance(self, class_name: str, node_index: int): - alias = self._data_model_definitions[class_name].name_in_frame + # Due with data model. + data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] - return getattr(self.frame, alias)[node_index] + # Register the data model, so that it will help to generate related instance index. + unit_instance.data_model_index = self._register_data_model(data_model_def.alias) + unit_instance.data_model_name = data_model_def.name_in_frame - def get_sku(self, name: str): - return self._sku_collection.get(name, None) + # Parse the config is there is any. + unit_instance.parse_configs(config.get("config", {})) - def get_sku_by_id(self, sku_id: int): - return self._sku_collection[self._sku_id2name_mapping[sku_id]] + # Prepare children. + children_conf = config.get("children", None) - def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int): - return self._path_finder.get_path(int(start_x), int(start_y), int(goal_x), int(goal_y)) + if children_conf: + for child_name, child_conf in children_conf.items(): + # If child configuration is a dict, then we add it as a property by name (key). + if type(child_conf) == dict: + setattr(unit_instance, child_name, self.build_unit(facility, unit_instance, child_conf)) + elif type(child_conf) == list: + # If child configuration is a list, then will treat it as list property, named same as key. + child_list = [] + for conf in child_conf: + child_list.append(self.build_unit(facility, unit_instance, conf)) + + setattr(unit_instance, child_name, child_list) + + return unit_instance + else: + # If this is template unit, then will use the class' static method 'generate' to generate sub-units. + children = unit_def.class_type.generate(facility, config.get("config")) + + for child in children.values(): + child.id = self._gen_id() + child.world = self + child.facility = facility + child.parent = parent + + # Pass the config if there is any. + child.parse_configs(config.get("config", {})) + + self.entities[child.id] = child + + return children def get_node_mapping(self): - facility_info_dict = {facility_id: facility.get_node_info() for facility_id, facility in self.facilities.items()} + """Collect all the entities information. + + Returns: + dict: A dictionary contains 'mapping' for id to data model index mapping, + 'detail' for detail of units and facilities. + """ + facility_info_dict = { + facility_id: facility.get_node_info() for facility_id, facility in self.facilities.items() + } - # pick unit id and related index and node name id2index_mapping = {} for facility in facility_info_dict.values(): for units in facility["units"].values(): if type(units) is dict: - # one unit id2index_mapping[units["id"]] = (units["node_name"], units["node_index"]) elif type(units) is list: for unit in units: @@ -247,18 +339,51 @@ def get_node_mapping(self): "detail": facility_info_dict } - def _build_facility(self, conf: dict): - name = conf["name"] - class_alias = conf["class"] + def _register_data_model(self, alias: str) -> int: + """Register a data model alias, used to collect data model used in frame. + + Args: + alias (str): Class alias defined in core.yml. - facility_def = self.configs.facilities[class_alias] + Returns: + int: Specified data model instance index after frame is built. + """ + if alias not in self._data_class_collection: + self._data_class_collection[alias] = 0 - facility = facility_def.class_type() + node_index = self._data_class_collection[alias] - facility.id = self.gen_id() - facility.world = self - facility.name = name + self._data_class_collection[alias] += 1 + + return node_index + + def _build_frame(self, snapshot_number: int) -> FrameBase: + """Build frame by current world definitions. + + Args: + snapshot_number (int): Number of snapshots to keep in memory. + + Returns: + FrameBase: Frame instance with data model in current configuration. + """ + data_class_in_frame = [] - facility.build(conf["configs"]) + for alias, number in self._data_class_collection.items(): + data_model_def: DataModelDef = self.configs.data_models[alias] + data_class_in_frame.append(( + data_model_def.class_type, + data_model_def.name_in_frame, + number + )) + + frame = build_frame(True, snapshot_number, data_class_in_frame) + + return frame + + def _gen_id(self): + """Generate id for entities.""" + nid = self._id_counter + + self._id_counter += 1 - return facility + return nid From 11e694e91171813b74e441b5429be0c577596cf5 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 16 Mar 2021 18:26:25 +0800 Subject: [PATCH 063/482] update hello world, but there may still some bugs --- examples/hello_world/supply_chain/hello.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 1a7f8ade6..9848ad35a 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -46,16 +46,16 @@ def start(self): # tileset from https://github.com/libtcod/python-tcod. tileset = tcod.tileset.load_tilesheet("font/dejavu10x10_gs_tc.png", 32, TILESET_SIZE, tcod.tileset.CHARMAP_TCOD) - grid_width, grid_height = self.env.configs["grid"]["size"] + grid_width, grid_height = (20, 20) # self.env.configs["grid"]["size"] # blocks facilities = [] railroads = [] - for facility, pos in self.env.configs["grid"]["facilities"].items(): + for facility, pos in self.env.configs.world["grid"]["facilities"].items(): facilities.append(pos) - for pos_list in self.env.configs["grid"]["blocks"].values(): + for pos_list in self.env.configs.world["grid"]["blocks"].values(): railroads.extend(pos_list) console = tcod.Console(grid_width, grid_height) @@ -126,16 +126,15 @@ def start(self): action = None def present_vehicles(self, console: tcod.Console): - vehicles = self.env.snapshot_list["transport"] + vehicles = self.env.snapshot_list["vehicle"] vehicle_number = len(vehicles) # here we query the attributes that slot number ==1, # then query position, or snapshot_list will try to padding for id - normal_list = vehicles[self.env.frame_index::("id", "steps", "location")].flatten().reshape(vehicle_number, -1).astype(np.int) + normal_list = vehicles[self.env.frame_index::("id", "steps")].flatten().reshape(vehicle_number, -1).astype(np.int) pos_list = vehicles[self.env.frame_index::"position"].flatten().reshape(vehicle_number, -1).astype(np.int) for index, state in enumerate(normal_list): - location = state[2] steps = state[1] if steps > 0: @@ -192,11 +191,11 @@ def show_demand_states(self): print(tabulate(seller_states, seller_features)) def show_vehicle_states(self): - vehicles = self.env.snapshot_list["transport"] + vehicles = self.env.snapshot_list["vehicle"] vehicle_number = len(vehicles) - vehicle_features = ("id", "facility_id", "location", "steps", "patient", "source", "destination", "payload", "product_id") + vehicle_features = ("id", "facility_id", "steps", "patient", "source", "destination", "payload", "product_id") vehicle_states = vehicles[self.env.frame_index::vehicle_features].flatten().reshape(vehicle_number, -1).astype(np.int) From c67e243bdb3a73c8e1eb8b0295ba78a4bb507a79 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 16 Mar 2021 18:39:43 +0800 Subject: [PATCH 064/482] prepare for ut --- tests/data/supply_chain/case1/readme.md | 5 +++++ tests/data/supply_chain/case2/readme.md | 7 +++++++ tests/data/supply_chain/case3/readme.md | 5 +++++ tests/supply_chain/test_supply_chain.py | 10 ++++++++++ 4 files changed, 27 insertions(+) create mode 100644 tests/data/supply_chain/case1/readme.md create mode 100644 tests/data/supply_chain/case2/readme.md create mode 100644 tests/data/supply_chain/case3/readme.md create mode 100644 tests/supply_chain/test_supply_chain.py diff --git a/tests/data/supply_chain/case1/readme.md b/tests/data/supply_chain/case1/readme.md new file mode 100644 index 000000000..8b0b48e08 --- /dev/null +++ b/tests/data/supply_chain/case1/readme.md @@ -0,0 +1,5 @@ + +Manufacture products then check: + +. storage state changes after success procuring. +. storage state should not change if failed. \ No newline at end of file diff --git a/tests/data/supply_chain/case2/readme.md b/tests/data/supply_chain/case2/readme.md new file mode 100644 index 000000000..5bada1b11 --- /dev/null +++ b/tests/data/supply_chain/case2/readme.md @@ -0,0 +1,7 @@ +Demand generation, then check: + +. order should correct +. order should be scheduled correctly +. check storage states +. check purchase, receive and demand states +. check vehicle states \ No newline at end of file diff --git a/tests/data/supply_chain/case3/readme.md b/tests/data/supply_chain/case3/readme.md new file mode 100644 index 000000000..5271003a4 --- /dev/null +++ b/tests/data/supply_chain/case3/readme.md @@ -0,0 +1,5 @@ + +customization check: + +. new seller unit +. new storage unit diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py new file mode 100644 index 000000000..58e5dfe58 --- /dev/null +++ b/tests/supply_chain/test_supply_chain.py @@ -0,0 +1,10 @@ +import unittest + + +class MyTestCase(unittest.TestCase): + def test_something(self): + self.assertEqual(True, False) + + +if __name__ == '__main__': + unittest.main() From 8e9e33e9517d6ebe283da3c595b45f3d0cb9ada5 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 15:17:17 +0800 Subject: [PATCH 065/482] add a sku model as base for sku related data model --- .../scenarios/supply_chain/actions.py | 2 +- .../scenarios/supply_chain/datamodels/base.py | 3 +- .../supply_chain/datamodels/consumer.py | 11 +++---- .../supply_chain/datamodels/manufacture.py | 7 ++-- .../supply_chain/datamodels/seller.py | 7 ++-- .../supply_chain/datamodels/skumodel.py | 32 +++++++++++++++++++ .../scenarios/supply_chain/units/consumer.py | 11 +++---- .../supply_chain/units/manufacture.py | 2 ++ .../scenarios/supply_chain/units/product.py | 11 ++++--- .../scenarios/supply_chain/units/seller.py | 2 ++ .../scenarios/supply_chain/units/skuunit.py | 4 +++ .../simulator/scenarios/supply_chain/world.py | 20 +++++++++--- 12 files changed, 76 insertions(+), 36 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/skumodel.py diff --git a/maro/simulator/scenarios/supply_chain/actions.py b/maro/simulator/scenarios/supply_chain/actions.py index f084e7709..c5ec2c959 100644 --- a/maro/simulator/scenarios/supply_chain/actions.py +++ b/maro/simulator/scenarios/supply_chain/actions.py @@ -5,6 +5,6 @@ from collections import namedtuple -ConsumerAction = namedtuple("ConsumerAction", ("id", "source_id", "quantity", "vlt")) +ConsumerAction = namedtuple("ConsumerAction", ("id", "product_id", "source_id", "quantity", "vlt")) ManufactureAction = namedtuple("ManufactureAction", ("id", "production_rate")) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py index 3d1e883bd..39e3ab3e3 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/base.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/base.py @@ -19,8 +19,7 @@ def __init__(self): self._unit_id = 0 self._facility_id = 0 - @abstractmethod - def initialize(self, configs): + def initialize(self, configs: dict): """Initialize the fields with configs, the config should be a dict.""" # called from unit after frame is ready. pass diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index 0125163e8..088942daf 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -5,12 +5,12 @@ from maro.backends.backend import AttributeType from maro.backends.frame import node, NodeAttribute -from .base import DataModelBase +from .skumodel import SkuDataModel # NOTE: one sku one consumer @node("consumer") -class ConsumerDataModel(DataModelBase): +class ConsumerDataModel(SkuDataModel): # reward states # from config order_cost = NodeAttribute(AttributeType.UInt) @@ -18,7 +18,7 @@ class ConsumerDataModel(DataModelBase): total_received = NodeAttribute(AttributeType.UInt) # action states - product_id = NodeAttribute(AttributeType.UInt) + consumer_product_id = NodeAttribute(AttributeType.UInt) source_id = NodeAttribute(AttributeType.UInt) quantity = NodeAttribute(AttributeType.UInt) vlt = NodeAttribute(AttributeType.UShort) @@ -37,11 +37,9 @@ def __init__(self): super(ConsumerDataModel, self).__init__() self._order_cost = 0 - self._product_id = 0 - def initialize(self, order_cost=0, product_id=0): + def initialize(self, order_cost=0): self._order_cost = order_cost - self._product_id = product_id self.reset() @@ -49,4 +47,3 @@ def reset(self): super(ConsumerDataModel, self).reset() self.order_cost = self._order_cost - self.product_id = self._product_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index d206c492d..b63d7ec32 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -5,11 +5,11 @@ from maro.backends.backend import AttributeType from maro.backends.frame import node, NodeAttribute -from .base import DataModelBase +from .skumodel import SkuDataModel @node("manufacture") -class ManufactureDataModel(DataModelBase): +class ManufactureDataModel(SkuDataModel): # storage related to this manufacture unit, for easy state retrieving. storage_id = NodeAttribute(AttributeType.UInt) @@ -20,9 +20,6 @@ class ManufactureDataModel(DataModelBase): # user can determine how to calculate the cost manufacturing_number = NodeAttribute(AttributeType.UInt) - # what we will produce - product_id = NodeAttribute(AttributeType.UInt) - # original from config, then updated by action production_rate = NodeAttribute(AttributeType.UInt) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index 4bdfc0028..3d079a574 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -5,12 +5,12 @@ from maro.backends.backend import AttributeType from maro.backends.frame import node, NodeAttribute -from .base import DataModelBase +from .skumodel import SkuDataModel # NOTE: one sku one seller @node("seller") -class SellerDataModel(DataModelBase): +class SellerDataModel(SkuDataModel): # reward states total_sold = NodeAttribute(AttributeType.UInt) @@ -23,9 +23,6 @@ class SellerDataModel(DataModelBase): # sale_gamma = NodeAttribute(AttributeType.UInt) - # what we will sell - product_id = NodeAttribute(AttributeType.UInt) - # per tick state, we can use this to support "sale hist" feature in original code. # original there is only sold state, we add a demand here demand = NodeAttribute(AttributeType.UInt) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py b/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py new file mode 100644 index 000000000..166471f8e --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute + +from .base import DataModelBase + + +class SkuDataModel(DataModelBase): + # Product id of this consumer belongs to. + product_id = NodeAttribute(AttributeType.UInt) + + # Parent unit id. + product_unit_id = NodeAttribute(AttributeType.UInt) + + def __int__(self): + super(SkuDataModel, self).__int__() + + self._product_id = 0 + self._product_unit_id = 0 + + def reset(self): + super(SkuDataModel, self).reset() + + self.product_id = self._product_id + self.product_unit_id = self._product_unit_id + + def set_product_id(self, product_id: int, product_unit_id: int): + self._product_id = product_id + self._product_unit_id = product_unit_id diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 9578e0400..9c521d046 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -50,19 +50,18 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): self.open_orders[source_id][product_id] += qty_delta def initialize(self): - # Update the product_id id in frame, so we will not update later in this episode. - self.data_model.initialize(product_id=self.product_id) + super(ConsumerUnit, self).initialize() def step(self, tick: int): - # id == 0 means invalid,as our id is 1 based - if self.action is None or self.action.quantity <= 0 or self.action.source_id == 0: + # NOTE: id == 0 means invalid,as our id is 1 based. + if self.action is None or self.action.quantity <= 0 or self.action.consumer_product_id <= 0 or self.action.source_id == 0: return # NOTE: we are using product unit as destination, # so we expect the action.source_id is and id of product unit - self.update_open_orders(self.action.source_id, self.product_id, self.action.quantity) + self.update_open_orders(self.action.source_id, self.action.consumer_product_id, self.action.quantity) - order = Order(self.parent, self.product_id, self.action.quantity, self.action.vlt) + order = Order(self.parent, self.action.consumer_product_id, self.action.quantity, self.action.vlt) source_facility = self.world.get_facility_by_id(self.action.source_id) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 370f25d3f..da207878b 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -25,6 +25,8 @@ class ManufactureUnit(SkuUnit): manufacture_number = 0 def initialize(self): + super(ManufactureUnit, self).initialize() + # TODO: add storage id to data model. product_unit_cost = self.config.get("product_unit_cost", 0) diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index ffb6e766e..1a49fdbc8 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -94,18 +94,19 @@ def generate(facility, config: dict): world = facility.world for sku_id, sku in facility.skus.items(): - product = ProductUnit() + product: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility) for child_name in ("consumer", "manufacture", "seller"): conf = config.get(child_name, None) if conf is not None: - sub_agent = world.build_unit(facility, None, conf) - sub_agent.product_id = sku_id + child_unit = world.build_unit(facility, product, conf) + child_unit.product_id = sku_id - setattr(product, child_name, sub_agent) + setattr(product, child_name, child_unit) - sub_agent.parse_configs(conf) + # Parse config for unit. + child_unit.parse_configs(conf) instance_list[sku_id] = product diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index e2cbf24dc..43419c96e 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -37,6 +37,8 @@ def market_demand(self, tick: int) -> int: return int(self.demand_distribution[tick]) def initialize(self): + super(SellerUnit, self).initialize() + unit_price = self.config.get("unit_price", 0) self.gamma = self.config.get("sale_gamma", 0) backlog_ratio = self.config.get("backlog_ratio", 1) diff --git a/maro/simulator/scenarios/supply_chain/units/skuunit.py b/maro/simulator/scenarios/supply_chain/units/skuunit.py index 36dcfc61f..e7967b12a 100644 --- a/maro/simulator/scenarios/supply_chain/units/skuunit.py +++ b/maro/simulator/scenarios/supply_chain/units/skuunit.py @@ -10,3 +10,7 @@ class SkuUnit(UnitBase): # Product id (sku id), 0 means invalid. product_id: int = 0 + + def initialize(self): + if self.data_model is not None: + self.data_model.set_product_id(self.product_id, self.parent.id) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index cf828361a..e2bbe74fa 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -184,9 +184,9 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio self.frame = self._build_frame(snapshot_number) # Assign data model instance. - for agent in self.entities.values(): - if agent.data_model_name is not None: - agent.data_model = getattr(self.frame, agent.data_model_name)[agent.data_model_index] + for unit in self.entities.values(): + if unit.data_model_name is not None: + unit.data_model = getattr(self.frame, unit.data_model_name)[unit.data_model_index] # Construct the upstream topology. topology = world_config["topology"] @@ -208,8 +208,8 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio facility.initialize() # Call initialize method for units. - for agent in self.entities.values(): - agent.initialize() + for unit in self.entities.values(): + unit.initialize() # TODO: replace tcod with other lib. # Construct the map grid. @@ -239,6 +239,16 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio # 0 for 2nd parameters means disable diagonal movement, so just up, right, down or left. self._path_finder = AStar(cost_grid, 0) + def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase) -> UnitBase: + unit = unit_type() + + unit.id = self._gen_id() + unit.parent = parent + unit.facility = facility + unit.world = self + + return unit + def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBase], config: dict) -> UnitBase: """Build an unit by its configuration. From 1a105d72bf6c3938a963cdee9ab26bbd48e63629 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 15:49:33 +0800 Subject: [PATCH 066/482] refine cosumer initialize part --- .../topologies/sample1/config.yml | 2 +- .../scenarios/supply_chain/units/consumer.py | 46 ++++++++++++++++++- .../supply_chain/units/distribution.py | 4 +- .../simulator/scenarios/supply_chain/world.py | 2 +- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index 32f495f64..e8372df4b 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -67,7 +67,7 @@ normal_vehicle: &normal_vehicle normal_distribution: &normal_distribution class: "DistributionUnit" children: - transports: + vehicles: - *normal_vehicle - *normal_vehicle diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 9c521d046..af4bf39fa 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. +import warnings from collections import defaultdict, Counter from .order import Order @@ -20,12 +21,13 @@ def __init__(self): self.received = 0 self.purchased = 0 self.order_cost = 0 + self.sources = [] def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): """Called after order product is received. Args: - source_id (int): Where is the product from. + source_id (int): Where is the product from (facility id). product_id (int): What product we received. quantity (int): How many we received. original_quantity (int): How many we ordered. @@ -38,7 +40,7 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): """Update the order states. Args: - source_id (int): Where is the product from. + source_id (int): Where is the product from (facility id). product_id (int): What product in the order. qty_delta (int): Number of product to update (sum). """ @@ -52,7 +54,41 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): def initialize(self): super(ConsumerUnit, self).initialize() + if self.facility.upstreams is not None: + # Construct sources from facility's upstreams. + sources = self.facility.upstreams.get(self.product_id, None) + + if sources is not None: + # Is we are a supplier facility? + is_supplier = self.parent.manufacture is not None + + # Current sku information. + sku = self.world.get_sku_by_id(self.product_id) + + for source_facility in sources: + # We are a supplier unit, then the consumer is used to purchase source materials from upstreams. + # Try to find who will provide this kind of material. + if is_supplier: + if source_facility.products is not None: + warnings.warn(f"Invalid upstream configuration for sku: {sku.id}.") + + for source_sku_id in sku.bom.keys(): + if source_sku_id in source_facility.products: + # This is a valid source facility. + self.sources.append(source_facility.id) + else: + # If we are not a manufacturing, just check if upstream have this sku configuration. + if sku.id in source_facility.skus: + self.sources.append(source_facility.id) + + self._init_data_model() + def step(self, tick: int): + # We must have a source to purchase. + if len(self.sources) == 0: + warnings.warn(f"No sources for consumer: {self.id}, sku: {self.product_id} in facility: {self.facility.id}") + return + # NOTE: id == 0 means invalid,as our id is 1 based. if self.action is None or self.action.quantity <= 0 or self.action.consumer_product_id <= 0 or self.action.source_id == 0: return @@ -108,6 +144,8 @@ def reset(self): self.open_orders.clear() + self._init_data_model() + def set_action(self, action: object): super(ConsumerUnit, self).set_action(action) @@ -115,3 +153,7 @@ def set_action(self, action: object): self.data_model.source_id = action.source_id self.data_model.quantity = action.quantity self.data_model.vlt = action.vlt + + def _init_data_model(self): + for source in self.sources: + self.data_model.sources.append(source) diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index a618d0c97..ba96533ba 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -15,7 +15,7 @@ class DistributionUnit(UnitBase): One distribution can accept all kind of sku order. """ # Transport unit list of this distribution unit. - transports = None + vehicles = None def __init__(self): self.order_queue = deque() @@ -76,7 +76,7 @@ def initialize(self): index += 1 def step(self, tick: int): - for vehicle in self.transports: + for vehicle in self.vehicles: # If we have vehicle not on the way and there is any pending order if len(self.order_queue) > 0 and vehicle.location == 0: order = self.order_queue.popleft() diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index e2bbe74fa..57f6b50af 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -200,7 +200,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio sku = self.get_sku_by_name(sku_name) facility.upstreams[sku.id] = [ - self.get_facility_by_name(source_name).id for source_name in source_facilities + self.get_facility_by_name(source_name) for source_name in source_facilities ] # Call initialize method for facilities. From dcfd0b713c15a4ba934ded2bf852ab23b23bdf9e Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 16:04:14 +0800 Subject: [PATCH 067/482] clear the order source issue --- .../supply_chain/datamodels/manufacture.py | 5 +---- .../supply_chain/datamodels/seller.py | 5 +---- .../scenarios/supply_chain/units/consumer.py | 2 +- .../supply_chain/units/distribution.py | 14 +++++++----- .../supply_chain/units/manufacture.py | 4 +++- .../scenarios/supply_chain/units/seller.py | 4 ++-- .../scenarios/supply_chain/units/storage.py | 4 ++++ .../scenarios/supply_chain/units/vehicle.py | 22 +++++++------------ 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index b63d7ec32..a4d47284e 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -25,12 +25,10 @@ class ManufactureDataModel(SkuDataModel): def __init__(self): super(ManufactureDataModel, self).__init__() - self._output_product_id = 0 self._product_unit_cost = 0 self._storage_id = 0 - def initialize(self, output_product_id: int = 0, product_unit_cost: int = 1, storage_id: int = 0): - self._output_product_id = output_product_id + def initialize(self, product_unit_cost: int = 1, storage_id: int = 0): self._product_unit_cost = product_unit_cost self._storage_id = storage_id @@ -39,6 +37,5 @@ def initialize(self, output_product_id: int = 0, product_unit_cost: int = 1, sto def reset(self): super(ManufactureDataModel, self).reset() - self.product_id = self._output_product_id self.product_unit_cost = self._product_unit_cost self.storage_id = self._storage_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index 3d079a574..1411bc08d 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -33,13 +33,11 @@ def __init__(self): self._unit_price = 0 self._sale_gamma = 0 - self._product_id = 0 self._backlog_ratio = 0 - def initialize(self, unit_price: int = 0, sale_gamma: int = 0, product_id: int = 0, backlog_ratio: int = 0): + def initialize(self, unit_price: int = 0, sale_gamma: int = 0, backlog_ratio: int = 0): self._unit_price = unit_price self._sale_gamma = sale_gamma - self._product_id = product_id self._backlog_ratio = backlog_ratio self.reset() @@ -48,6 +46,5 @@ def reset(self): super(SellerDataModel, self).reset() self.unit_price = self._unit_price - self.product_id = self._product_id self.sale_gamma = self._sale_gamma self.backlog_ratio = self._backlog_ratio diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index af4bf39fa..bf8e5708a 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -97,7 +97,7 @@ def step(self, tick: int): # so we expect the action.source_id is and id of product unit self.update_open_orders(self.action.source_id, self.action.consumer_product_id, self.action.quantity) - order = Order(self.parent, self.action.consumer_product_id, self.action.quantity, self.action.vlt) + order = Order(self.facility, self.action.consumer_product_id, self.action.quantity, self.action.vlt) source_facility = self.world.get_facility_by_id(self.action.source_id) diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index ba96533ba..ab3aa9abd 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -23,6 +23,7 @@ def __init__(self): # Used to map from product id to slot index. self.product_index_mapping: Dict[int, int] = {} + # What product we will carry. self.product_list = [] def get_pending_order(self): @@ -61,27 +62,26 @@ def place_order(self, order: Order): return 0 def initialize(self): - index = 0 + super(DistributionUnit, self).initialize() # Init product list in data model. + index = 0 for sku_id, sku in self.facility.skus.items(): self.product_list.append(sku_id) - self.data_model.product_list.append(sku_id) - self.data_model.delay_order_penalty.append(0) - self.data_model.check_in_price.append(0) - self.product_index_mapping[sku_id] = index index += 1 + self._init_data_model() + def step(self, tick: int): for vehicle in self.vehicles: # If we have vehicle not on the way and there is any pending order if len(self.order_queue) > 0 and vehicle.location == 0: order = self.order_queue.popleft() - # schedule a job for vehicle + # Schedule a job for available vehicle. # TODO: why vlt is determined by order? vehicle.schedule( order.destination, @@ -105,7 +105,9 @@ def reset(self): super(DistributionUnit, self).reset() self.order_queue.clear() + self._init_data_model() + def _init_data_model(self): for product_id in self.product_list: self.data_model.product_list.append(product_id) self.data_model.delay_order_penalty.append(0) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index da207878b..a0e5bc287 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -31,7 +31,6 @@ def initialize(self): product_unit_cost = self.config.get("product_unit_cost", 0) self.data_model.initialize( - output_product_id=self.product_id, product_unit_cost=product_unit_cost, storage_id=self.facility.storage.id ) @@ -94,6 +93,9 @@ def post_step(self, tick: int): if self.action is not None: self.data_model.production_rate = 0 + # NOTE: call super at last, since it will clear the action. + super(ManufactureUnit, self).post_step() + def set_action(self, action: ManufactureAction): super(ManufactureUnit, self).set_action(action) diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index 43419c96e..c052112ef 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -46,19 +46,19 @@ def initialize(self): self.data_model.initialize( unit_price=unit_price, sale_gamma=self.gamma, - product_id=self.product_id, backlog_ratio=backlog_ratio ) self.durations = self.world.durations + # Generate demand distribution of this episode. for _ in range(self.durations): self.demand_distribution.append(np.random.gamma(self.gamma)) def step(self, tick: int): demand = self.market_demand(tick) - # what seller does is just count down the product number. + # What seller does is just count down the product number. sold_qty = self.facility.storage.take_available(self.product_id, demand) self.total_sold += sold_qty diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 0476d4c4d..28c383370 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -144,8 +144,12 @@ def reset(self): for sku in self.facility.skus.values(): self.product_number.append(sku.init_in_stock) + self.remaining_space = self.capacity + for index, product_id in enumerate(self.product_list): product_number = self.product_number[index] self.data_model.product_list.append(product_id) self.data_model.product_number.append(product_number) + + self.remaining_space -= product_number diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index d2b72e2ff..8fcae91af 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -40,11 +40,11 @@ def __init__(self): # Velocity. self.velocity = 0 - def schedule(self, destination: ProductUnit, product_id: int, quantity: int, vlt: int): + def schedule(self, destination: object, product_id: int, quantity: int, vlt: int): """Schedule a job for this vehicle. Args: - destination (ProductUnit): Destination product unit. + destination (FacilityBase): Destination facility. product_id (int): What load from storage. quantity (int): How many to load. vlt (int): Velocity of vehicle. @@ -59,11 +59,6 @@ def schedule(self, destination: ProductUnit, product_id: int, quantity: int, vlt self.product_id = product_id self.destination = destination - # Find product related in our facility. - for product in self.facility.products: - if product.product_id == product_id: - self.product = product - # Keep the patient, reset it after product unloaded. self.max_patient = self.data_model.patient @@ -71,8 +66,8 @@ def schedule(self, destination: ProductUnit, product_id: int, quantity: int, vlt self.path = self.world.find_path( self.facility.x, self.facility.y, - destination.facility.x, - destination.facility.y + destination.x, + destination.y ) if self.path is None: @@ -115,8 +110,8 @@ def try_unload(self): if len(unloaded) > 0: unloaded_units = sum(unloaded.values()) - self.destination.consumer.on_order_reception( - self.product.id, + self.destination.products[self.product_id].consumer.on_order_reception( + self.facility.id, self.product_id, unloaded_units, self.payload @@ -152,8 +147,8 @@ def step(self, tick: int): # Failed to load, check the patient. if self.data_model.patient < 0: - self.destination.consumer.update_open_orders( - self.product.id, + self.destination.products[self.product_id].consumer.update_open_orders( + self.facility.id, self.product_id, -request_quantity ) @@ -209,7 +204,6 @@ def reset(self): self.destination_id = 0 self.max_patient = None self.path = None - self.product = None self.payload = 0 self.product_id = 0 self.steps = 0 From e73260e9eeb25cf28729a4f8bbcf5d31faffbc90 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 16:59:50 +0800 Subject: [PATCH 068/482] change the way to create a product unit --- .../scenarios/supply_chain/business_engine.py | 6 +++++- .../scenarios/supply_chain/units/consumer.py | 12 +++++------ .../scenarios/supply_chain/units/product.py | 19 ++++++++++++----- tests/supply_chain/test_supply_chain.py | 21 +++++++++++++++++++ 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 5b6999ab5..dd7b91932 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -3,6 +3,7 @@ import os +from pathlib import Path from maro.event_buffer import MaroEvents from maro.simulator.scenarios import AbsBusinessEngine @@ -89,7 +90,10 @@ def _register_events(self): def _build_world(self): self.update_config_root_path(__file__) - core_config = os.path.join(self._config_path, "..", "core.yml") + # Core configuration always in topologies folder. + be_root = os.path.split(os.path.realpath(__file__))[0] + core_config = os.path.join(be_root, "topologies", "core.yml") + config_path = os.path.join(self._config_path, "config.yml") parser = ConfigParser(core_config, config_path) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index bf8e5708a..ab882077a 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -70,8 +70,6 @@ def initialize(self): # Try to find who will provide this kind of material. if is_supplier: if source_facility.products is not None: - warnings.warn(f"Invalid upstream configuration for sku: {sku.id}.") - for source_sku_id in sku.bom.keys(): if source_sku_id in source_facility.products: # This is a valid source facility. @@ -81,14 +79,14 @@ def initialize(self): if sku.id in source_facility.skus: self.sources.append(source_facility.id) + if len(self.sources) == 0: + warnings.warn( + f"No sources for consumer: {self.id}, sku: {self.product_id} in facility: {self.facility.name}.") + return + self._init_data_model() def step(self, tick: int): - # We must have a source to purchase. - if len(self.sources) == 0: - warnings.warn(f"No sources for consumer: {self.id}, sku: {self.product_id} in facility: {self.facility.id}") - return - # NOTE: id == 0 means invalid,as our id is 1 based. if self.action is None or self.action.quantity <= 0 or self.action.consumer_product_id <= 0 or self.action.source_id == 0: return diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index 1a49fdbc8..797e3e275 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -94,20 +94,29 @@ def generate(facility, config: dict): world = facility.world for sku_id, sku in facility.skus.items(): - product: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility) + sku_type = getattr(sku, "type", None) - for child_name in ("consumer", "manufacture", "seller"): + product_unit: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility) + + for child_name in ("manufacture", "consumer", "seller"): conf = config.get(child_name, None) if conf is not None: - child_unit = world.build_unit(facility, product, conf) + # Ignore manufacture unit if it is not for a production, even it is configured in config. + if sku_type != "production" and child_name == "manufacture": + continue + + if sku_type == "production" and child_name == "consumer": + continue + + child_unit = world.build_unit(facility, product_unit, conf) child_unit.product_id = sku_id - setattr(product, child_name, child_unit) + setattr(product_unit, child_name, child_unit) # Parse config for unit. child_unit.parse_configs(conf) - instance_list[sku_id] = product + instance_list[sku_id] = product_unit return instance_list diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 58e5dfe58..8d69b1916 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -5,6 +5,27 @@ class MyTestCase(unittest.TestCase): def test_something(self): self.assertEqual(True, False) + def test_manufacture_unit_only(self): + """ + Test if manufacture unit works as expect, like: + + 1. with input sku + . meet the storage limitation + . not meet the storage limitation + . with enough source sku + . without enough source sku + . with product rate + . without product rate + 2. without input sku + . meet the storage limitation + . not meet the storage limitation + . with product rate + . without product rate + + """ + + pass + if __name__ == '__main__': unittest.main() From 5b56b24246793748dcb8cad7a6ea62cd5cb02163 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 17:17:36 +0800 Subject: [PATCH 069/482] fix manufacture issue --- maro/simulator/scenarios/supply_chain/units/manufacture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index a0e5bc287..61f6677a9 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -94,7 +94,7 @@ def post_step(self, tick: int): self.data_model.production_rate = 0 # NOTE: call super at last, since it will clear the action. - super(ManufactureUnit, self).post_step() + super(ManufactureUnit, self).post_step(tick) def set_action(self, action: ManufactureAction): super(ManufactureUnit, self).set_action(action) From aca952061fe854766a34c6f1978489f52c6d87cb Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 17:18:06 +0800 Subject: [PATCH 070/482] manufacture test data --- tests/data/supply_chain/case_01/config.yml | 156 +++++++++++++++++++++ tests/data/supply_chain/case_01/readme.md | 1 + tests/supply_chain/test_supply_chain.py | 23 ++- 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 tests/data/supply_chain/case_01/config.yml create mode 100644 tests/data/supply_chain/case_01/readme.md diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml new file mode 100644 index 000000000..47a648c90 --- /dev/null +++ b/tests/data/supply_chain/case_01/config.yml @@ -0,0 +1,156 @@ + + +facility_definitions: + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + manufacture: + class: "ManufactureUnit" + + +small_storage: &small_storage + config: + capacity: 100 + unit_storage_cost: 1 + +midium_storage: &midium_storage + config: + capacity: 200 + unit_storage_cost: 1 + +huge_storage: &huge_storage + config: + capacity: 300 + unit_storage_cost: 1 + + +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 1 + bom: + sku3: 1 + + - id: 2 + name: "sku2" + output_units_per_lot: 2 + bom: + sku1: 4 + + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + - id: 4 + name: "sku4" + output_units_per_lot: 2 + bom: + sku2: 1 + + +world: + skus: *sku_definitions + + facilities: + # This supplier will keep generate sku3, and should stop at tick 20 as reach the storage limitation + - name: "Supplier_SKU3" + definition_ref: "SupplierFacility" + skus: + sku3: + init_in_stock: 80 + delay_order_penalty: 10 + product_unit_cost: 1 + type: "production" + cost": 10 + price": 10 + children: + storage: *small_storage + - name: "Supplier_SKU1" + definition_ref: "SupplierFacility" + skus: + sku1: + init_in_stock: 50 + delay_order_penalty: 10 + product_unit_cost: 1 + type: "production" + cost: 10 + price: 100 + sku3: + init_in_stock: 80 + delay_order_penalty: 10 + type: "material" + cost: 10 + price: 100 + children: + storage: *midium_storage + - name: "Supplier_SKU2" + definition_ref: "SupplierFacility" + skus: + sku2: + init_in_stock: 50 + delay_order_penalty: 10 + product_unit_cost: 1 + type: "production" + cost: 10 + price: 100 + sku1: + init_in_stock: 50 + delay_order_penalty: 10 + type: "material" + cost: 10 + price: 100 + children: + storage: *midium_storage + - name: "Supplier_SKU4" + definition_ref: "SupplierFacility" + skus: + sku4: + init_in_stock: 50 + delay_order_penalty: 10 + product_unit_cost: 1 + type: "production" + cost: 10 + price: 100 + sku2: + init_in_stock: 0 + delay_order_penalty: 10 + type: "material" + cost: 10 + price: 100 + children: + storage: *midium_storage + + topology: + Supplier_SKU1: + sku3: + - "Supplier_SKU3" + Supplier_SKU2: + sku1: + - "Supplier_SKU1" + Supplier_SKU4: + sku2: + - "Supplier_SKU2" + + grid: + size: [20, 20] + + facilities: + Supplier_SKU1: [0, 0] + Supplier_SKU2: [3, 3] + Supplier_SKU3: [6, 6] + Supplier_SKU4: [10, 18] + + blocks: + railroad: + - [10, 10] + - [10, 11] + - [10, 12] + - [11, 12] diff --git a/tests/data/supply_chain/case_01/readme.md b/tests/data/supply_chain/case_01/readme.md new file mode 100644 index 000000000..a16ffeea1 --- /dev/null +++ b/tests/data/supply_chain/case_01/readme.md @@ -0,0 +1 @@ +This case used to test if manufacture unit work as expected. \ No newline at end of file diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 8d69b1916..c4ce9d879 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -1,10 +1,20 @@ +import os import unittest +from maro.simulator import Env -class MyTestCase(unittest.TestCase): - def test_something(self): - self.assertEqual(True, False) +def build_env(case_name: str, durations: int): + case_folder = os.path.join("tests", "data", "supply_chain", case_name) + + # config_path = os.path.join(case_folder, "config.yml") + + env = Env(scenario="supply_chain", topology=case_folder, durations=durations) + + return env + + +class MyTestCase(unittest.TestCase): def test_manufacture_unit_only(self): """ Test if manufacture unit works as expect, like: @@ -24,7 +34,12 @@ def test_manufacture_unit_only(self): """ - pass + env = build_env("case_01", 100) + + is_done = False + + while not is_done: + _, _, is_done = env.step(None) if __name__ == '__main__': From 7bdccfc75b8730ced96cde64ad267527bb1a91de Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 17:37:33 +0800 Subject: [PATCH 071/482] bug fix, set id for data model in unit initialize --- maro/simulator/scenarios/supply_chain/business_engine.py | 2 +- maro/simulator/scenarios/supply_chain/units/skuunit.py | 2 ++ maro/simulator/scenarios/supply_chain/units/storage.py | 2 ++ maro/simulator/scenarios/supply_chain/units/unitbase.py | 3 ++- maro/simulator/scenarios/supply_chain/units/vehicle.py | 2 ++ maro/simulator/scenarios/supply_chain/world.py | 2 +- 6 files changed, 10 insertions(+), 3 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index dd7b91932..b12a9c8b9 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -109,7 +109,7 @@ def _on_action_received(self, event): if action is not None and len(action) > 0: # NOTE: we assume that the action is dictionary that key is the unit(agent) id, value is the real action. - for unit_id, action_obj in action[0].items(): + for unit_id, action_obj in action.items(): entity = self.world.get_entity(unit_id) if entity is not None and type(entity) == UnitBase: diff --git a/maro/simulator/scenarios/supply_chain/units/skuunit.py b/maro/simulator/scenarios/supply_chain/units/skuunit.py index e7967b12a..1c868b300 100644 --- a/maro/simulator/scenarios/supply_chain/units/skuunit.py +++ b/maro/simulator/scenarios/supply_chain/units/skuunit.py @@ -12,5 +12,7 @@ class SkuUnit(UnitBase): product_id: int = 0 def initialize(self): + super(SkuUnit, self).initialize() + if self.data_model is not None: self.data_model.set_product_id(self.product_id, self.parent.id) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 28c383370..0497076ed 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -104,6 +104,8 @@ def get_product_number(self, product_id: int) -> int: return self.product_number[product_index] def initialize(self): + super(StorageUnit, self).initialize() + self.capacity = self.config.get("capacity", 100) self.unit_storage_cost = self.config.get("unit_storage_cost", 1) diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py index e3c4f6927..c969d8900 100644 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -89,7 +89,8 @@ def initialize(self): NOTE: unit.data_model is available from this step. """ - pass + if self.data_model is not None: + self.data_model.set_id(self.id, self.facility.id) def set_action(self, action: object): """Set action for this agent. diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index 8fcae91af..b3472ff89 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -124,6 +124,8 @@ def try_unload(self): self.data_model.position[:] = -1 def initialize(self): + super(VehicleUnit, self).initialize() + patient = self.config.get("patient", 100) unit_transport_cost = self.config.get("unit_transport_cost", 1) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 57f6b50af..b749631b6 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -107,7 +107,7 @@ def get_entity(self, entity_id: int) -> Union[FacilityBase, UnitBase]: Returns: Union[FacilityBase, UnitBase]: Entity instance. """ - return self._entities[entity_id] + return self.entities[entity_id] def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: """Find path to specified cell. From d06597816a89d7732ede1101db9f7c565e6fb09f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 17:55:34 +0800 Subject: [PATCH 072/482] fix manufacture unit bug --- .../scenarios/supply_chain/business_engine.py | 15 +++- .../supply_chain/units/manufacture.py | 2 + tests/supply_chain/test_supply_chain.py | 81 ++++++++++++++----- 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index b12a9c8b9..ba0a9077e 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -24,6 +24,9 @@ def __init__(self, **kwargs): self._node_mapping = self.world.get_node_mapping() + # Used to cache the action from outside, then dispatch to units at the beginning of step. + self._action_cache = None + @property def frame(self): return self._frame @@ -37,6 +40,8 @@ def configs(self) -> SupplyChainConfiguration: return self.world.configs def step(self, tick: int): + # NOTE: we have to dispatch the action here. + self._dispatch_action() self._step_by_facility(tick) # We do not have payload here. @@ -108,9 +113,15 @@ def _on_action_received(self, event): action = event.payload if action is not None and len(action) > 0: + self._action_cache = action + + def _dispatch_action(self): + if self._action_cache is not None: # NOTE: we assume that the action is dictionary that key is the unit(agent) id, value is the real action. - for unit_id, action_obj in action.items(): + for unit_id, action_obj in self._action_cache.items(): entity = self.world.get_entity(unit_id) - if entity is not None and type(entity) == UnitBase: + if entity is not None and issubclass(type(entity), UnitBase): entity.set_action(action_obj) + + self._action_cache = None diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 61f6677a9..b0cce6164 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -81,6 +81,8 @@ def step(self, tick: int): if max_number_to_procedure > 0: self.manufacture_number = max_number_to_procedure self.facility.storage.try_take_products(source_sku_to_take) + else: + self.manufacture_number = 0 def flush_states(self): if self.manufacture_number > 0: diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index c4ce9d879..f63db7495 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -1,7 +1,10 @@ import os import unittest +import numpy as np + from maro.simulator import Env +from maro.simulator.scenarios.supply_chain import ManufactureAction def build_env(case_name: str, durations: int): @@ -15,31 +18,67 @@ def build_env(case_name: str, durations: int): class MyTestCase(unittest.TestCase): - def test_manufacture_unit_only(self): - """ - Test if manufacture unit works as expect, like: - - 1. with input sku - . meet the storage limitation - . not meet the storage limitation - . with enough source sku - . without enough source sku - . with product rate - . without product rate - 2. without input sku - . meet the storage limitation - . not meet the storage limitation - . with product rate - . without product rate - - """ + """ + manufacture unit testing: + + 1. with input sku + . meet the storage limitation + . not meet the storage limitation + . with enough source sku + . without enough source sku + . with product rate + . without product rate + 2. without input sku + . meet the storage limitation + . not meet the storage limitation + . with product rate + . without product rate + """ + + def test_manufacture_meet_storage_limitation(self): + """Test sku3 manufacturing.""" env = build_env("case_01", 100) - is_done = False + manufacture_nodes = env.snapshot_list["manufacture"] + manufacture_number = len(manufacture_nodes) + features = ( + "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", "product_unit_cost" + ) + + # tick 0 passed, no product manufacturing. + env.step(None) + + # try to find which one is sku3 manufacture unit. + states = manufacture_nodes[env.frame_index::features].flatten().reshape(manufacture_number, -1).astype(np.int) + + for index, state in enumerate(states): + # Id of sku3 is 3. + if state[4] == 3: + sku3_data_model_index = index + sku3_manufacture_id = state[0] + + # all the id is greater than 0 + self.assertGreater(sku3_manufacture_id, 0) + + # pass an action to start manufacturing for this tick. + action = ManufactureAction(sku3_manufacture_id, 1) + + env.step({action.id: action}) + + states = manufacture_nodes[env.frame_index:sku3_data_model_index:features].flatten().astype(np.int) + + # Sku3 produce rate is 1 per tick, so manufacturing_number should be 1. + self.assertEqual(1, states[2]) + + # leave the action as none will cause manufacture unit stop manufacturing. + env.step(None) + + states = manufacture_nodes[env.frame_index:sku3_data_model_index:features].flatten().astype(np.int) + + # so manufacturing_number should be 0 + self.assertEqual(0, states[2]) - while not is_done: - _, _, is_done = env.step(None) if __name__ == '__main__': From 0bb5198ee3f67f484b3e59a19b3c225e1018824b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 17 Mar 2021 18:18:24 +0800 Subject: [PATCH 073/482] fix storage and manufacture bug, update test case --- .../supply_chain/units/manufacture.py | 1 + .../scenarios/supply_chain/units/storage.py | 6 +++ tests/supply_chain/test_supply_chain.py | 42 ++++++++++++++++--- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index b0cce6164..49dbb9431 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -81,6 +81,7 @@ def step(self, tick: int): if max_number_to_procedure > 0: self.manufacture_number = max_number_to_procedure self.facility.storage.try_take_products(source_sku_to_take) + self.facility.storage.try_add_products({self.product_id: self.manufacture_number}) else: self.manufacture_number = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 0497076ed..82381a2ea 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -45,6 +45,8 @@ def try_add_products(self, product_quantities: Dict[int, int], all_or_nothing=Tr self.product_number[product_index] += unload_quantity unloaded_quantities[product_id] = unload_quantity + self.remaining_space -= unload_quantity + return unloaded_quantities def try_take_products(self, product_quantities: Dict[int, int]) -> bool: @@ -70,6 +72,8 @@ def try_take_products(self, product_quantities: Dict[int, int]) -> bool: self.product_number[product_index] -= quantity + self.remaining_space += quantity + return True def take_available(self, product_id: int, quantity: int) -> int: @@ -88,6 +92,8 @@ def take_available(self, product_id: int, quantity: int) -> int: self.product_number[product_index] -= actual + self.remaining_space += actual + return actual def get_product_number(self, product_id: int) -> int: diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index f63db7495..3d0c813e1 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -40,23 +40,38 @@ def test_manufacture_meet_storage_limitation(self): """Test sku3 manufacturing.""" env = build_env("case_01", 100) + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "facility_id", "capacity", "remaining_space", "unit_storage_cost") + manufacture_nodes = env.snapshot_list["manufacture"] manufacture_number = len(manufacture_nodes) - features = ( + manufacture_features = ( "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", "product_unit_cost" ) # tick 0 passed, no product manufacturing. env.step(None) - # try to find which one is sku3 manufacture unit. - states = manufacture_nodes[env.frame_index::features].flatten().reshape(manufacture_number, -1).astype(np.int) + states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, -1).astype(np.int) + # try to find which one is sku3 manufacture unit. for index, state in enumerate(states): # Id of sku3 is 3. if state[4] == 3: sku3_data_model_index = index sku3_manufacture_id = state[0] + sku3_storage_id = state[5] + + # try to find sku3's storage from env.summary + sku3_storage_index = env.summary["node_mapping"]["mapping"][sku3_storage_id][1] + + storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) + + # there should be 80 units been taken at the beginning according to the config file. + # so remaining space should be 20 + self.assertEqual(20, storage_states[3]) + # capacity is 100 by config + self.assertEqual(100, storage_states[2]) # all the id is greater than 0 self.assertGreater(sku3_manufacture_id, 0) @@ -66,19 +81,36 @@ def test_manufacture_meet_storage_limitation(self): env.step({action.id: action}) - states = manufacture_nodes[env.frame_index:sku3_data_model_index:features].flatten().astype(np.int) + states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) # Sku3 produce rate is 1 per tick, so manufacturing_number should be 1. self.assertEqual(1, states[2]) + storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) + + # now remaining space should be 19 + self.assertEqual(19, storage_states[3]) + # leave the action as none will cause manufacture unit stop manufacturing. env.step(None) - states = manufacture_nodes[env.frame_index:sku3_data_model_index:features].flatten().astype(np.int) + states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) # so manufacturing_number should be 0 self.assertEqual(0, states[2]) + # let is generate 20, but actually it can only procedure 19 because the storage will reach the limitation + env.step({sku3_manufacture_id: ManufactureAction(sku3_manufacture_id, 20)}) + + states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) + + # so manufacture_number should be 19 instead 20 + self.assertEqual(19, states[2]) + + storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) + + # now remaining space should be 0 + self.assertEqual(0, storage_states[3]) if __name__ == '__main__': From 0dad81df866de7e5fbf55f00492736876fd284df Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 18 Mar 2021 16:11:11 +0800 Subject: [PATCH 074/482] manufacture unit sku bug fix --- .../supply_chain/units/manufacture.py | 12 ++-- tests/data/supply_chain/case_01/config.yml | 2 +- tests/supply_chain/test_supply_chain.py | 60 ++++++++++++++++++- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 49dbb9431..4ab165231 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -27,19 +27,17 @@ class ManufactureUnit(SkuUnit): def initialize(self): super(ManufactureUnit, self).initialize() - # TODO: add storage id to data model. - product_unit_cost = self.config.get("product_unit_cost", 0) + facility_sku_info = self.facility.skus[self.product_id] self.data_model.initialize( - product_unit_cost=product_unit_cost, + product_unit_cost=facility_sku_info.product_unit_cost, storage_id=self.facility.storage.id ) - # Grab bom of current production. - sku = self.world.get_sku_by_id(self.product_id) + global_sku_info = self.world.get_sku_by_id(self.product_id) - self.bom = sku.bom - self.output_units_per_lot = sku.output_units_per_lot + self.bom = global_sku_info.bom + self.output_units_per_lot = global_sku_info.output_units_per_lot if len(self.bom) > 0: self.input_units_per_lot = sum(self.bom.values()) diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml index 47a648c90..f505e2a86 100644 --- a/tests/data/supply_chain/case_01/config.yml +++ b/tests/data/supply_chain/case_01/config.yml @@ -115,7 +115,7 @@ world: sku4: init_in_stock: 50 delay_order_penalty: 10 - product_unit_cost: 1 + product_unit_cost: 4 type: "production" cost: 10 price: 100 diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 3d0c813e1..2dfad075a 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -46,7 +46,7 @@ def test_manufacture_meet_storage_limitation(self): manufacture_nodes = env.snapshot_list["manufacture"] manufacture_number = len(manufacture_nodes) manufacture_features = ( - "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", "product_unit_cost" + "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", "product_unit_cost" ) # tick 0 passed, no product manufacturing. @@ -112,6 +112,64 @@ def test_manufacture_meet_storage_limitation(self): # now remaining space should be 0 self.assertEqual(0, storage_states[3]) + def test_manufacture_meet_source_lack(self): + """Test sku4 manufacturing, this sku supplier does not have enough source material at the begging + , so it cannot produce anything without consumer purchase.""" + env = build_env("case_01", 100) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "facility_id", "capacity", "remaining_space", "unit_storage_cost") + + manufacture_nodes = env.snapshot_list["manufacture"] + manufacture_number = len(manufacture_nodes) + manufacture_features = ( + "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", + "product_unit_cost" + ) + + # tick 0 passed, no product manufacturing. + env.step(None) + + states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, + -1).astype(np.int) + + # try to find which one is sku3 manufacture unit. + for index, state in enumerate(states): + # Id of sku4 is 4. + if state[4] == 4: + sku4_data_model_index = index + sku4_manufacture_id = state[0] + sku4_storage_id = state[5] + + # try to find sku4's storage from env.summary + sku4_storage_index = env.summary["node_mapping"]["mapping"][sku4_storage_id][1] + + # the storage should be same as initialized (50 + 0). + storage_states = storage_nodes[env.frame_index:sku4_storage_index:storage_features].flatten().astype(np.int) + + # capacity is same as configured. + self.assertEqual(200, storage_states[2]) + + # remaining space should be capacity - (50+0) + self.assertEqual(200 - (50+0), storage_states[3]) + + # no manufacture number as we have not pass any action + manufature_states = manufacture_nodes[env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype(np.int) + + # manufacturing_number should be 0 + self.assertEqual(0, manufature_states[2]) + + # production rate should be 0 + self.assertEqual(0, manufature_states[3]) + + # output product id should be same as configured. + self.assertEqual(4, manufature_states[4]) + + # product unit cost should be same as configured. + self.assertEqual(4, manufature_states[6]) + + # push to the end, the storage should not changed + if __name__ == '__main__': unittest.main() From 692843a3510dc19aa52b8b0324a72c8b58fb1004 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 18 Mar 2021 16:53:20 +0800 Subject: [PATCH 075/482] manufacture test --- .../supply_chain/units/manufacture.py | 1 + .../scenarios/supply_chain/units/seller.py | 2 + tests/data/supply_chain/case_01/config.yml | 6 +- tests/supply_chain/test_supply_chain.py | 185 +++++++++++++++++- 4 files changed, 184 insertions(+), 10 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 4ab165231..513527e91 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -90,6 +90,7 @@ def flush_states(self): def post_step(self, tick: int): if self.manufacture_number > 0: self.data_model.manufacturing_number = 0 + self.manufacture_number = 0 if self.action is not None: self.data_model.production_rate = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index c052112ef..f797f71bd 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -75,9 +75,11 @@ def post_step(self, tick: int): if self.sold > 0: self.data_model.sold = 0 + self.sold = 0 if self.demand > 0: self.data_model.demand = 0 + self.demand = 0 def reset(self): super(SellerUnit, self).reset() diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml index f505e2a86..1db17bd03 100644 --- a/tests/data/supply_chain/case_01/config.yml +++ b/tests/data/supply_chain/case_01/config.yml @@ -37,7 +37,7 @@ skus: &sku_definitions name: "sku1" output_units_per_lot: 1 bom: - sku3: 1 + sku3: 2 - id: 2 name: "sku2" @@ -77,14 +77,14 @@ world: definition_ref: "SupplierFacility" skus: sku1: - init_in_stock: 50 + init_in_stock: 96 delay_order_penalty: 10 product_unit_cost: 1 type: "production" cost: 10 price: 100 sku3: - init_in_stock: 80 + init_in_stock: 100 delay_order_penalty: 10 type: "material" cost: 10 diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 2dfad075a..cb5ba8707 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -17,6 +17,19 @@ def build_env(case_name: str, durations: int): return env +def get_product_dict_from_storage(env: Env, frame_index: int, node_index: int): + product_list = env.snapshot_list["storage"][frame_index:node_index:"product_list"].flatten().astype(np.int) + product_number = env.snapshot_list["storage"][frame_index:node_index:"product_number"].flatten().astype(np.int) + + return {pid: pnum for pid, pnum in zip(product_list, product_number)} + + +SKU1_ID = 1 +SKU2_ID = 2 +SKU3_ID = 3 +SKU4_ID = 4 + + class MyTestCase(unittest.TestCase): """ manufacture unit testing: @@ -46,18 +59,22 @@ def test_manufacture_meet_storage_limitation(self): manufacture_nodes = env.snapshot_list["manufacture"] manufacture_number = len(manufacture_nodes) manufacture_features = ( - "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", "product_unit_cost" + "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", + "product_unit_cost" ) + ############################### TICK: 0 ###################################### + # tick 0 passed, no product manufacturing. env.step(None) - states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, -1).astype(np.int) + states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, + -1).astype(np.int) # try to find which one is sku3 manufacture unit. for index, state in enumerate(states): # Id of sku3 is 3. - if state[4] == 3: + if state[4] == SKU3_ID: sku3_data_model_index = index sku3_manufacture_id = state[0] sku3_storage_id = state[5] @@ -73,9 +90,17 @@ def test_manufacture_meet_storage_limitation(self): # capacity is 100 by config self.assertEqual(100, storage_states[2]) + product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) + + # number should be same as configuration at beginning. + # 80 sku3 + self.assertEqual(80, product_dict[SKU3_ID]) + # all the id is greater than 0 self.assertGreater(sku3_manufacture_id, 0) + ############################### TICK: 1 ###################################### + # pass an action to start manufacturing for this tick. action = ManufactureAction(sku3_manufacture_id, 1) @@ -91,6 +116,13 @@ def test_manufacture_meet_storage_limitation(self): # now remaining space should be 19 self.assertEqual(19, storage_states[3]) + product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) + + # sku3 number should be 80 + 1 + self.assertEqual(80 + 1, product_dict[SKU3_ID]) + + ############################### TICK: 2 ###################################### + # leave the action as none will cause manufacture unit stop manufacturing. env.step(None) @@ -99,6 +131,11 @@ def test_manufacture_meet_storage_limitation(self): # so manufacturing_number should be 0 self.assertEqual(0, states[2]) + product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) + + # sku3 number should be same as last tick + self.assertEqual(80 + 1, product_dict[SKU3_ID]) + # let is generate 20, but actually it can only procedure 19 because the storage will reach the limitation env.step({sku3_manufacture_id: ManufactureAction(sku3_manufacture_id, 20)}) @@ -112,6 +149,11 @@ def test_manufacture_meet_storage_limitation(self): # now remaining space should be 0 self.assertEqual(0, storage_states[3]) + product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) + + # sku3 number should be 100 + self.assertEqual(80 + 1 + 19, product_dict[SKU3_ID]) + def test_manufacture_meet_source_lack(self): """Test sku4 manufacturing, this sku supplier does not have enough source material at the begging , so it cannot produce anything without consumer purchase.""" @@ -127,6 +169,8 @@ def test_manufacture_meet_source_lack(self): "product_unit_cost" ) + ############################### TICK: 0 ###################################### + # tick 0 passed, no product manufacturing. env.step(None) @@ -136,7 +180,7 @@ def test_manufacture_meet_source_lack(self): # try to find which one is sku3 manufacture unit. for index, state in enumerate(states): # Id of sku4 is 4. - if state[4] == 4: + if state[4] == SKU4_ID: sku4_data_model_index = index sku4_manufacture_id = state[0] sku4_storage_id = state[5] @@ -151,10 +195,11 @@ def test_manufacture_meet_source_lack(self): self.assertEqual(200, storage_states[2]) # remaining space should be capacity - (50+0) - self.assertEqual(200 - (50+0), storage_states[3]) + self.assertEqual(200 - (50 + 0), storage_states[3]) # no manufacture number as we have not pass any action - manufature_states = manufacture_nodes[env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype(np.int) + manufature_states = manufacture_nodes[ + env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype(np.int) # manufacturing_number should be 0 self.assertEqual(0, manufature_states[2]) @@ -168,7 +213,133 @@ def test_manufacture_meet_source_lack(self): # product unit cost should be same as configured. self.assertEqual(4, manufature_states[6]) - # push to the end, the storage should not changed + product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) + + # 50 sku4 at beginning + self.assertEqual(50, product_dict[SKU4_ID]) + + # 0 sku2 + self.assertEqual(0, product_dict[SKU2_ID]) + + ############################### TICK: 1 - end ###################################### + + is_done = False + + while not is_done: + # push to the end, the storage should not changed, no matter what production rate we give it. + _, _, is_done = env.step({sku4_manufacture_id: ManufactureAction(sku4_manufacture_id, 10)}) + + manufature_states = manufacture_nodes[ + env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype( + np.int) + + # manufacturing_number should be 0 + self.assertEqual(0, manufature_states[2]) + + # production rate should be 10 + self.assertEqual(10, manufature_states[3]) + + # output product id should be same as configured. + self.assertEqual(SKU4_ID, manufature_states[4]) + + # product unit cost should be same as configured. + self.assertEqual(4, manufature_states[6]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) + + # 50 sku4 at beginning + self.assertEqual(50, product_dict[SKU4_ID]) + + # 0 sku2 + self.assertEqual(0, product_dict[SKU2_ID]) + + def test_manufacture_meet_avg_storage_limitation(self): + """Test on sku1, it is configured with nearly full initial states.""" + + env = build_env("case_01", 100) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "facility_id", "capacity", "remaining_space", "unit_storage_cost") + + manufacture_nodes = env.snapshot_list["manufacture"] + manufacture_number = len(manufacture_nodes) + manufacture_features = ( + "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", + "product_unit_cost" + ) + + ############################### TICK: 0 ###################################### + + # tick 0 passed, no product manufacturing, verified in above case, pass checking it here. + env.step(None) + + states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, + -1).astype(np.int) + # try to find which one is sku3 manufacture unit. + for index, state in enumerate(states): + # Id of sku1 is 1. + if state[4] == SKU1_ID: + sku1_data_model_index = index + sku1_manufacture_id = state[0] + sku1_storage_id = state[5] + + sku1_storage_index = env.summary["node_mapping"]["mapping"][sku1_storage_id][1] + + ############################### TICK: 1 ###################################### + + # ask sku1 manufacture start manufacturing, rate is 10. + env.step({sku1_manufacture_id: ManufactureAction(sku1_storage_index, 10)}) + + storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) + manufacture_states = manufacture_nodes[ + env.frame_index:sku1_data_model_index:manufacture_features].flatten().astype(np.int) + + # we can produce 4 sku1, as it will meet storage avg limitation per sku + self.assertEqual(4, manufacture_states[2]) + + # but the production rate is same as action + self.assertEqual(10, manufacture_states[3]) + + # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) + self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku1_storage_index) + + # number of sku1 should 100, just reach the avg storage capacity limitation + self.assertEqual(100, product_dict[SKU1_ID]) + + # 4 sku1 cost 4*2 source material (sku3) + self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) + + ############################### TICK: 1 ###################################### + + # then fix the product rate to 20 every tick, but the manufacture will do nothing, as we have to enough space + + is_done = False + + while not is_done: + _, _, is_done = env.step({sku1_manufacture_id: ManufactureAction(sku1_storage_index, 20)}) + + storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) + manufacture_states = manufacture_nodes[ + env.frame_index:sku1_data_model_index:manufacture_features].flatten().astype(np.int) + + # but manufacture number is 0 + self.assertEqual(0, manufacture_states[2]) + + # but the production rate is same as action + self.assertEqual(20, manufacture_states[3]) + + # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) + self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku1_storage_index) + + # number of sku1 should 100, just reach the avg storage capacity limitation + self.assertEqual(100, product_dict[SKU1_ID]) + + # 4 sku1 cost 4*2 source material (sku3) + self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) if __name__ == '__main__': From 69b2068b0c628cebb479fdd7f0e23be571463743 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 18 Mar 2021 17:18:23 +0800 Subject: [PATCH 076/482] storage take available test --- tests/supply_chain/test_supply_chain.py | 89 ++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index cb5ba8707..dd04df486 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -5,7 +5,7 @@ from maro.simulator import Env from maro.simulator.scenarios.supply_chain import ManufactureAction - +from maro.simulator.scenarios.supply_chain import StorageUnit def build_env(case_name: str, durations: int): case_folder = os.path.join("tests", "data", "supply_chain", case_name) @@ -341,6 +341,93 @@ def test_manufacture_meet_avg_storage_limitation(self): # 4 sku1 cost 4*2 source material (sku3) self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) + """ + Storage test: + + . take available + . enough + . not enough + . try add products + . meet whole storage capacity limitation + . fail if all + . not fail if all + . meet avg storage limitation + .fail if all + . not fail if all + . enough space + . try take products + . have enough + . not enough + . get product number + + """ + + def test_storage_take_available(self): + env = build_env("case_01", 100) + + env.step(None) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "capacity", "remaining_space") + + # find first storage unit id + storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] + + # get the unit reference from env internal + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + capacity = storage_states[1] + init_remaining_space = storage_states[2] + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + # call take_available for each product in storage. + products_taken = {} + for product_id, product_number in init_product_dict.items(): + num = np.random.randint(0, product_number) + actual_num = storage_unit.take_available(product_id, num) + + # we should get the number we want. + self.assertEqual(num, actual_num) + + products_taken[product_id] = num + + # check if internal state correct + for product_id, num in products_taken.items(): + remaining_num = storage_unit.product_number[storage_unit.product_index_mapping[product_id]] + + self.assertEqual(init_product_dict[product_id] - num, remaining_num) + + # call env.step will cause states write into snapshot + env.step(None) + + product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + for product_id, num in products_taken.items(): + remaining_num = product_dict[product_id] + + self.assertEqual(init_product_dict[product_id] - num, remaining_num) + + # then take more than exist number for 1st product(sku) + lot_taken_product_id, lot_taken_product_number = product_dict.popitem() + + lot_taken_product_number += 100 + + actual_num = storage_unit.take_available(lot_taken_product_id, lot_taken_product_number) + + # we should get all available + self.assertEqual(actual_num, lot_taken_product_number - 100) + + # take snapshot + env.step(None) + + product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + # the product number should be 0, as we took all available + self.assertEqual(0, product_dict[lot_taken_product_id]) + if __name__ == '__main__': unittest.main() From 5739a0e55fbbc654993b4b82f43e98df4acad327 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 18 Mar 2021 18:12:52 +0800 Subject: [PATCH 077/482] test for try_take_products --- tests/supply_chain/test_supply_chain.py | 139 +++++++++++++++++++++++- 1 file changed, 136 insertions(+), 3 deletions(-) diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index dd04df486..98cc6c21a 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -7,6 +7,7 @@ from maro.simulator.scenarios.supply_chain import ManufactureAction from maro.simulator.scenarios.supply_chain import StorageUnit + def build_env(case_name: str, durations: int): case_folder = os.path.join("tests", "data", "supply_chain", case_name) @@ -351,9 +352,6 @@ def test_manufacture_meet_avg_storage_limitation(self): . meet whole storage capacity limitation . fail if all . not fail if all - . meet avg storage limitation - .fail if all - . not fail if all . enough space . try take products . have enough @@ -428,6 +426,141 @@ def test_storage_take_available(self): # the product number should be 0, as we took all available self.assertEqual(0, product_dict[lot_taken_product_id]) + def test_storage_try_add_products(self): + """ + NOTE: + try_add_products method do not check avg storage capacity checking, so we will ignore it here. + + """ + env = build_env("case_01", 100) + + env.step(None) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "capacity", "remaining_space") + + # find first storage unit id + storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] + + # get the unit reference from env internal + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + capacity = storage_states[1] + init_remaining_space = storage_states[2] + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + first_product_id = [id for id in init_product_dict.keys()][0] + + # try put products out of capacity with all_or_nothing == True + products_to_put = {} + + avg_max_product_number = init_remaining_space // len(init_product_dict) + + for product_id in init_product_dict.keys(): + products_to_put[product_id] = avg_max_product_number + 1 + + result = storage_unit.try_add_products(products_to_put, all_or_nothing=True) + + # the method will return an empty dictionary if fail to add + self.assertEqual(0, len(result)) + + # so remaining space should not change + self.assertEqual(init_remaining_space, storage_unit.remaining_space) + + # each product number should be same as before + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, + storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + + # if we set all_or_nothing=False, then part of the product will be added to storage, and cause remaining space being 0 + result = storage_unit.try_add_products(products_to_put, all_or_nothing=False) + + self.assertEqual(0, storage_unit.remaining_space) + + # take snapshot + env.step(None) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + # remaining space in snapshot should be 0 + self.assertEqual(0, storage_states[2]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + # total product number should be same as capacity + self.assertEqual(capacity, sum(product_dict.values())) + + #################################################### + #################################################### + # reset the env for next case + env.reset() + + # check the state after reset + self.assertEqual(capacity, storage_unit.capacity) + self.assertEqual(init_remaining_space, storage_unit.remaining_space) + + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, + storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + + def test_storage_try_take_products(self): + env = build_env("case_01", 100) + + env.step(None) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "capacity", "remaining_space") + + # find first storage unit id + storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] + + # get the unit reference from env internal + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + capacity = storage_states[1] + init_remaining_space = storage_states[2] + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + product_to_take = {} + + for product_id, product_number in init_product_dict.items(): + product_to_take[product_id] = product_number + 1 + + # which this setting, it will return false, as no enough product for ous + self.assertFalse(storage_unit.try_take_products(product_to_take)) + + # so remaining space and product number should same as before + self.assertEqual(init_remaining_space, storage_unit.remaining_space) + + for product_id,product_number in init_product_dict.items(): + self.assertEqual(product_number, storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + + # try to get all products + for product_id, product_number in product_to_take.items(): + product_to_take[product_id] = product_number - 1 + + self.assertTrue(storage_unit.try_take_products(product_to_take)) + + # now the remaining space should be same as capacity as we take all + self.assertEqual(capacity, storage_unit.remaining_space) + + # take snapshot + env.step(None) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + # remaining space should be same as capacity in snapshot + self.assertEqual(storage_states[1], storage_states[2]) + + def test_storage_get_product_number(self): + pass + if __name__ == '__main__': unittest.main() From d5565a147bced06d0444b49c00ccba5852d7ed8e Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 18 Mar 2021 18:13:57 +0800 Subject: [PATCH 078/482] remove todo for storage unit --- maro/simulator/scenarios/supply_chain/units/storage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 82381a2ea..e1ff55d1b 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -119,7 +119,6 @@ def initialize(self): self.product_list.append(sku.id) self.product_number.append(sku.init_in_stock) - # TODO: init data model self.remaining_space = self.capacity for index, product_id in enumerate(self.product_list): From 06fb1040552f049bb98f8c280626c9822dfa24f6 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 18 Mar 2021 18:15:13 +0800 Subject: [PATCH 079/482] remove todo from config --- .../scenarios/supply_chain/topologies/sample1/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index e8372df4b..8ca7e5ff0 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -2,7 +2,6 @@ # TODO: which config to inherit # base: "" -# TODO: add customized data model, unit or facility definitions, will be combined with builtin ones. #core: # datamodels: "xxx" # units: "xxx" From de962182b347117010b7a026ad738638755a586f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 19 Mar 2021 15:11:01 +0800 Subject: [PATCH 080/482] testing --- tests/supply_chain/test_supply_chain.py | 34 ++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 98cc6c21a..607f904cf 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -559,8 +559,40 @@ def test_storage_try_take_products(self): self.assertEqual(storage_states[1], storage_states[2]) def test_storage_get_product_number(self): - pass + env = build_env("case_01", 100) + + env.step(None) + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "capacity", "remaining_space") + + # find first storage unit id + storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] + + # get the unit reference from env internal + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + print(init_product_dict) + # number in object should be same with states + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + + # should not change even after reset + env.reset() + env.step(None) + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + print(init_product_dict) + + print(storage_unit.product_list) + print(storage_unit.product_number) + print(storage_unit.product_index_mapping) + + # number in object should be same with states + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) if __name__ == '__main__': unittest.main() From 383b1cee445d0f51f197eb61f6227b8f2f297e67 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 19 Mar 2021 16:35:09 +0800 Subject: [PATCH 081/482] fix bug that if node contains 2 list attribute, will cause share list after reset --- maro/backends/raw/node.cpp | 21 ++++++++++++++++++++- maro/backends/raw/node.h | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/maro/backends/raw/node.cpp b/maro/backends/raw/node.cpp index 7aa805a7c..b49c04b8f 100644 --- a/maro/backends/raw/node.cpp +++ b/maro/backends/raw/node.cpp @@ -271,6 +271,25 @@ namespace maro list.clear(); } + UINT list_index = 0; + + for (auto& attr_def : _attribute_definitions) + { + if (attr_def.is_list) + { + // Assign each attribute with the index of actual list. + for (NODE_INDEX i = 0; i < _defined_node_number; i++) + { + auto& target_attr = _dynamic_block[_dynamic_size_per_node * i + attr_def.offset]; + + // Save the index of list in list store. + target_attr = list_index; + + list_index++; + } + } + } + // Reset bitset masks. _node_instance_masks.resize(_defined_node_number); _node_instance_masks.reset(true); @@ -413,7 +432,7 @@ namespace maro const auto list_index = target_attr.get_value(); - // Then get the actual list reference for furthure operation. + // Then get the actual list reference for further operation. auto& target_list = _list_store[list_index]; return target_list[slot_index]; diff --git a/maro/backends/raw/node.h b/maro/backends/raw/node.h index fc41197eb..e3f03a37c 100644 --- a/maro/backends/raw/node.h +++ b/maro/backends/raw/node.h @@ -6,6 +6,7 @@ #include #include +#include #include "common.h" #include "attribute.h" From 40263dbe34da2bdbd4ae7dd5bc73c4bd93b199ec Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 19 Mar 2021 16:43:32 +0800 Subject: [PATCH 082/482] fix bug that length of list attribute is not 0 after reset, update tests as set padding value to 0 instead nan --- maro/backends/frame.pyx | 2 +- tests/test_frame.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/maro/backends/frame.pyx b/maro/backends/frame.pyx index 110255e6a..481f45e46 100644 --- a/maro/backends/frame.pyx +++ b/maro/backends/frame.pyx @@ -374,7 +374,7 @@ cdef class _NodeAttributeAccessor: self._cb(value) def __len__(self): - return self._slot_number + return self._backend.get_slot_number(self._node_index, self._attr_type) def on_value_changed(self, cb): """Set the value changed callback.""" diff --git a/tests/test_frame.py b/tests/test_frame.py index 9bcbd939a..ab7fcc523 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -246,7 +246,7 @@ def test_append_nodes(self): self.assertListEqual([0.0, 0.0, 0.0, 0.0, 9.0], list(states)[0:5]) # 2 padding (NAN) in the end - self.assertTrue(np.isnan(states[-2:]).all()) + self.assertTrue((states[-2:].astype(np.int)==0).all()) states = static_snapshot[1::"a3"] @@ -329,7 +329,7 @@ def test_delete_node(self): states = states.flatten() # 2nd is padding value - self.assertTrue(np.isnan(states[1])) + self.assertEqual(0, int(states[1])) self.assertListEqual([0.0, 0.0, 0.0, 123.0], list(states[[0, 2, 3, 4]])) @@ -411,6 +411,7 @@ def test_list_attribute(self): @node("test") class TestNode(NodeBase): a1 = NodeAttribute("i", 1, is_list=True) + a4 = NodeAttribute("i", 1, is_list=True) a2 = NodeAttribute("i", 2, is_const=True) a3 = NodeAttribute("i") @@ -437,6 +438,9 @@ def __init__(self): n1.a1.append(11) n1.a1.append(12) + n1.a4.append(100) + n1.a4.append(101) + expected_value = [10, 11, 12] # check if value set append correct @@ -508,6 +512,14 @@ def __init__(self): self.assertEqual(3, len(states)) self.assertListEqual([10, 11, 12], list(states)) + # check states after reset + frame.reset() + frame.snapshots.reset() + + # list attribute should be cleared + self.assertEqual(0, len(n1.a1)) + self.assertEqual(0, len(n1.a4)) + def test_list_attribute_with_large_size(self): @node("test") class TestNode(NodeBase): From c6c8c56836537c3c960623ed4f83dc8db7dedd6a Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 19 Mar 2021 16:46:03 +0800 Subject: [PATCH 083/482] update frame test to include fixed bug --- tests/test_frame.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_frame.py b/tests/test_frame.py index ab7fcc523..1037fa496 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -520,6 +520,18 @@ def __init__(self): self.assertEqual(0, len(n1.a1)) self.assertEqual(0, len(n1.a4)) + # then append value to each list attribute to test if value will be mixed + n1.a1.append(10) + n1.a1.append(20) + + n1.a4.append(100) + n1.a4.append(200) + + self.assertEqual(10, n1.a1[0]) + self.assertEqual(20, n1.a1[1]) + self.assertEqual(100, n1.a4[0]) + self.assertEqual(200, n1.a4[1]) + def test_list_attribute_with_large_size(self): @node("test") class TestNode(NodeBase): From 08fe94e09473de46cdf2c3bf5ce2a43e88a82d2c Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 19 Mar 2021 17:07:40 +0800 Subject: [PATCH 084/482] refine env.summary information, make it more meaningful --- .../supply_chain/facilities/facility.py | 4 +- .../scenarios/supply_chain/units/product.py | 3 +- .../scenarios/supply_chain/units/skuunit.py | 7 +++ .../simulator/scenarios/supply_chain/world.py | 17 +++--- tests/supply_chain/test_supply_chain.py | 52 ++++++++++++++----- 5 files changed, 57 insertions(+), 26 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index ed0b5e562..d1942dcbd 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -104,10 +104,10 @@ def reset(self): product.reset() def get_node_info(self) -> dict: - products_info = [] + products_info = {} for product_id, product in self.products.items(): - products_info.append(product.get_unit_info()) + products_info[product_id] = product.get_unit_info() return { "id": self.id, diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index 797e3e275..ca2ee0552 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -97,6 +97,7 @@ def generate(facility, config: dict): sku_type = getattr(sku, "type", None) product_unit: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility) + product_unit.product_id = sku_id for child_name in ("manufacture", "consumer", "seller"): conf = config.get(child_name, None) @@ -105,7 +106,7 @@ def generate(facility, config: dict): # Ignore manufacture unit if it is not for a production, even it is configured in config. if sku_type != "production" and child_name == "manufacture": continue - + if sku_type == "production" and child_name == "consumer": continue diff --git a/maro/simulator/scenarios/supply_chain/units/skuunit.py b/maro/simulator/scenarios/supply_chain/units/skuunit.py index 1c868b300..da43bbe80 100644 --- a/maro/simulator/scenarios/supply_chain/units/skuunit.py +++ b/maro/simulator/scenarios/supply_chain/units/skuunit.py @@ -16,3 +16,10 @@ def initialize(self): if self.data_model is not None: self.data_model.set_product_id(self.product_id, self.parent.id) + + def get_unit_info(self) -> dict: + info = super(SkuUnit, self).get_unit_info() + + info["sku_id"] = self.product_id + + return info diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index b749631b6..ca674b0bd 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -336,17 +336,16 @@ def get_node_mapping(self): id2index_mapping = {} - for facility in facility_info_dict.values(): - for units in facility["units"].values(): - if type(units) is dict: - id2index_mapping[units["id"]] = (units["node_name"], units["node_index"]) - elif type(units) is list: - for unit in units: - id2index_mapping[unit["id"]] = (unit["node_name"], unit["node_index"]) + for entity_id, entity in self.entities.items(): + if entity.data_model is not None: + id2index_mapping[entity_id] = (entity.data_model_name, entity.data_model_index) + else: + id2index_mapping[entity_id] = (None, None) return { - "mapping": id2index_mapping, - "detail": facility_info_dict + "entity_mapping": id2index_mapping, + "skus": {sku.name: sku.id for sku in self._sku_collection.values()}, + "facilities": facility_info_dict } def _register_data_model(self, alias: str) -> int: diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 607f904cf..88f13d4d3 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -81,7 +81,7 @@ def test_manufacture_meet_storage_limitation(self): sku3_storage_id = state[5] # try to find sku3's storage from env.summary - sku3_storage_index = env.summary["node_mapping"]["mapping"][sku3_storage_id][1] + sku3_storage_index = env.summary["node_mapping"]["entity_mapping"][sku3_storage_id][1] storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) @@ -187,7 +187,7 @@ def test_manufacture_meet_source_lack(self): sku4_storage_id = state[5] # try to find sku4's storage from env.summary - sku4_storage_index = env.summary["node_mapping"]["mapping"][sku4_storage_id][1] + sku4_storage_index = env.summary["node_mapping"]["entity_mapping"][sku4_storage_id][1] # the storage should be same as initialized (50 + 0). storage_states = storage_nodes[env.frame_index:sku4_storage_index:storage_features].flatten().astype(np.int) @@ -284,7 +284,7 @@ def test_manufacture_meet_avg_storage_limitation(self): sku1_manufacture_id = state[0] sku1_storage_id = state[5] - sku1_storage_index = env.summary["node_mapping"]["mapping"][sku1_storage_id][1] + sku1_storage_index = env.summary["node_mapping"]["entity_mapping"][sku1_storage_id][1] ############################### TICK: 1 ###################################### @@ -538,8 +538,9 @@ def test_storage_try_take_products(self): # so remaining space and product number should same as before self.assertEqual(init_remaining_space, storage_unit.remaining_space) - for product_id,product_number in init_product_dict.items(): - self.assertEqual(product_number, storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, + storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) # try to get all products for product_id, product_number in product_to_take.items(): @@ -573,10 +574,11 @@ def test_storage_get_product_number(self): storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - print(init_product_dict) + # number in object should be same with states for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + self.assertEqual(product_number, + storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) # should not change even after reset env.reset() @@ -584,15 +586,37 @@ def test_storage_get_product_number(self): init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - print(init_product_dict) - - print(storage_unit.product_list) - print(storage_unit.product_number) - print(storage_unit.product_index_mapping) - # number in object should be same with states for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + self.assertEqual(product_number, + storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + + """ + + Consumer test: + + . initial state + . state after reset + . set_action directly from code + . set_action by env.step + . call on_order_reception directly to simulation order arrived + . call update_open_orders directly + + """ + + def test_consumer_init_state(self): + """ + NOTE: we will use consumer on Supplier_SKU1, as it contains a source for sku3 (Supplier_SKU3) + """ + env = build_env("case_01", 100) + + print(env.summary) + # we can get the consumer from env.summary + consumer_unit = None + + for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): + pass + if __name__ == '__main__': unittest.main() From ea4ae9288bdf971d6932dbea59a7b90cc2ce2c85 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 19 Mar 2021 18:01:42 +0800 Subject: [PATCH 085/482] fix bug that not write id to frame --- .../supply_chain/facilities/retailer.py | 5 +- .../supply_chain/facilities/supplier.py | 5 +- .../supply_chain/facilities/warehouse.py | 5 +- .../topologies/sample1/config.yml | 11 +++ .../scenarios/supply_chain/units/consumer.py | 4 + .../supply_chain/units/distribution.py | 2 + .../scenarios/supply_chain/units/product.py | 2 + .../simulator/scenarios/supply_chain/world.py | 2 + tests/data/supply_chain/case_01/config.yml | 7 ++ tests/supply_chain/test_supply_chain.py | 83 ++++++++++++++++++- 10 files changed, 116 insertions(+), 10 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 96209505a..da659a8c7 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -12,7 +12,7 @@ class RetailerFacility(FacilityBase): """Retail facility used to generate order from upstream, and sell products by demand.""" - SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gamma")) + SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gamma", "order_cost")) # Product unit list of this facility. products: List[ProductUnit] @@ -32,7 +32,8 @@ def parse_skus(self, configs: dict): sku_config.get("price", 0), sku_config.get("cost", 0), sku_config["init_in_stock"], - sku_config["sale_gamma"] + sku_config["sale_gamma"], + sku_config.get("order_cost", 0) ) self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 646503a6f..a2a005f50 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -14,7 +14,7 @@ class SupplierFacility(FacilityBase): SkuInfo = namedtuple( "SkuInfo", - ("name", "id", "init_in_stock", "type", "cost", "price", "delay_order_penalty", "product_unit_cost") + ("name", "id", "init_in_stock", "type", "cost", "price", "delay_order_penalty", "product_unit_cost", "order_cost") ) # Storage unit of this facility. @@ -40,7 +40,8 @@ def parse_skus(self, configs: dict): sku_config.get("cost", 0), sku_config.get("price", 0), sku_config.get("delay_order_penalty", 0), - sku_config.get("product_unit_cost", 1) + sku_config.get("product_unit_cost", 1), + sku_config.get("order_cost", 0) ) self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 189ff9c08..1cbbe8549 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -12,7 +12,7 @@ class WarehouseFacility(FacilityBase): """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" - SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id", "price", "delay_order_penalty")) + SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id", "price", "delay_order_penalty", "order_cost")) # Storage unit for this facility, must be a sub class of StorageUnit. storage: StorageUnit = None @@ -35,7 +35,8 @@ def parse_skus(self, configs: dict): sku_config["init_stock"], sku.id, sku_config.get("price", 0), - sku_config.get("delay_order_penalty", 0) + sku_config.get("delay_order_penalty", 0), + sku_config.get("order_cost", 0) ) self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index 8ca7e5ff0..6640145eb 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -69,6 +69,8 @@ normal_distribution: &normal_distribution vehicles: - *normal_vehicle - *normal_vehicle + config: + unit_price: 1 small_storage: &small_storage # config of data model of this unit @@ -126,6 +128,7 @@ world: init_in_stock: 100 delay_order_penalty: 10 product_unit_cost: 1 + order_cost: 0 type: "production" # production means this is the output production of this facility cost": 10 price": 10 @@ -148,12 +151,14 @@ world: type: "production" cost: 10 price: 100 + order_cost: 0 sku3: init_in_stock: 100 delay_order_penalty: 10 type: "material" cost: 10 price: 100 + order_cost: 200 children: storage: *small_storage @@ -167,14 +172,17 @@ world: init_stock: 1000 delay_order_penalty: 10 price: 100 + order_cost: 200 sku2: init_stock: 1000 delay_order_penalty: 10 price: 100 + order_cost: 200 sku3: init_stock: 1000 delay_order_penalty: 10 price: 100 + order_cost: 200 children: storage: *huge_storage @@ -190,18 +198,21 @@ world: init_in_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 # optional + order_cost: 200 sku3: price: 200 cost: 10 init_in_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 + order_cost: 200 sku2: price: 100 cost: 10 init_in_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 + order_cost: 200 children: storage: *midium_storage diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index ab882077a..c0cb71782 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -54,6 +54,10 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): def initialize(self): super(ConsumerUnit, self).initialize() + sku = self.facility.skus[self.product_id] + + self.data_model.initialize(order_cost=sku.order_cost) + if self.facility.upstreams is not None: # Construct sources from facility's upstreams. sources = self.facility.upstreams.get(self.product_id, None) diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index ab3aa9abd..055efec03 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -75,6 +75,8 @@ def initialize(self): self._init_data_model() + self.data_model.initialize(self.config.get("unit_price", 0)) + def step(self, tick: int): for vehicle in self.vehicles: # If we have vehicle not on the way and there is any pending order diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index ca2ee0552..dbf3adfb2 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -99,6 +99,8 @@ def generate(facility, config: dict): product_unit: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility) product_unit.product_id = sku_id + product_unit.parse_configs(config) + for child_name in ("manufacture", "consumer", "seller"): conf = config.get(child_name, None) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index ca674b0bd..b8ee457e6 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -247,6 +247,8 @@ def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBa unit.facility = facility unit.world = self + self.entities[unit.id] = unit + return unit def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBase], config: dict) -> UnitBase: diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml index 1db17bd03..6d18ef7bc 100644 --- a/tests/data/supply_chain/case_01/config.yml +++ b/tests/data/supply_chain/case_01/config.yml @@ -71,6 +71,7 @@ world: type: "production" cost": 10 price": 10 + order_cost: 0 children: storage: *small_storage - name: "Supplier_SKU1" @@ -83,12 +84,14 @@ world: type: "production" cost: 10 price: 100 + order_cost: 0 sku3: init_in_stock: 100 delay_order_penalty: 10 type: "material" cost: 10 price: 100 + order_cost: 200 children: storage: *midium_storage - name: "Supplier_SKU2" @@ -101,12 +104,14 @@ world: type: "production" cost: 10 price: 100 + order_cost: 0 sku1: init_in_stock: 50 delay_order_penalty: 10 type: "material" cost: 10 price: 100 + order_cost: 200 children: storage: *midium_storage - name: "Supplier_SKU4" @@ -119,12 +124,14 @@ world: type: "production" cost: 10 price: 100 + order_cost: 0 sku2: init_in_stock: 0 delay_order_penalty: 10 type: "material" cost: 10 price: 100 + order_cost: 200 children: storage: *midium_storage diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 88f13d4d3..02361c7fe 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -5,7 +5,7 @@ from maro.simulator import Env from maro.simulator.scenarios.supply_chain import ManufactureAction -from maro.simulator.scenarios.supply_chain import StorageUnit +from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase def build_env(case_name: str, durations: int): @@ -610,12 +610,87 @@ def test_consumer_init_state(self): """ env = build_env("case_01", 100) - print(env.summary) + # print(env.summary) # we can get the consumer from env.summary - consumer_unit = None + + # NOTE: though we are test with sku1, but the consumer is for sku3, as it is the source material from source + sku3_consumer_unit: ConsumerUnit + sku3_supplier_faiclity_id: int + sku3_consumer_data_model_index: int + sku3_product_unit_id: int for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): - pass + if facility_defail["name"] == "Supplier_SKU1": + # try to find sku3 consumer + sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] + + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] + + if facility_defail["name"] == "Supplier_SKU3": + sku3_supplier_faiclity_id = facility_defail["id"] + + sku3_consumer_data_model_index = env.summary["node_mapping"]["entity_mapping"][sku3_consumer_unit_id][1] + + # check initial state + self.assertEqual(0, sku3_consumer_unit.received) + self.assertEqual(0, sku3_consumer_unit.purchased) + self.assertEqual(0, sku3_consumer_unit.order_cost) + self.assertEqual(SKU3_ID, sku3_consumer_unit.product_id) + + # check data model state + # order cost from configuration + self.assertEqual(200, sku3_consumer_unit.data_model.order_cost) + self.assertEqual(0, sku3_consumer_unit.data_model.total_purchased) + self.assertEqual(0, sku3_consumer_unit.data_model.total_received) + + # NOTE: 0 is an invalid(initial) id + self.assertEqual(SKU3_ID, sku3_consumer_unit.data_model.product_id) + self.assertEqual(sku3_consumer_unit_id, sku3_consumer_unit.data_model.id) + self.assertEqual(sku3_product_unit_id, sku3_consumer_unit.data_model.product_unit_id) + self.assertEqual(0, sku3_consumer_unit.data_model.consumer_product_id) + self.assertEqual(0, sku3_consumer_unit.data_model.source_id) + self.assertEqual(0, sku3_consumer_unit.data_model.quantity) + self.assertEqual(0, sku3_consumer_unit.data_model.vlt) + self.assertEqual(0, sku3_consumer_unit.data_model.purchased) + self.assertEqual(0, sku3_consumer_unit.data_model.received) + self.assertEqual(0, sku3_consumer_unit.data_model.order_product_cost) + + # check sources + for source_facility_id in sku3_consumer_unit.sources: + source_facility: FacilityBase = env._business_engine.world.get_facility_by_id(source_facility_id) + + # check if source facility contains the sku3 config + self.assertTrue(SKU3_ID in source_facility.skus) + + env.step(None) + + # check state + features = ( + "id", + "facility_id", + "product_id", + "order_cost", + "total_purchased", + "total_received", + "consumer_product_id", + "source_id", + "quantity", + "vlt", + "purchased", + "received", + "order_product_cost" + ) + + consumer_nodes = env.snapshot_list["consumer"] + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # Nothing happened at tick 0, so most states will be 0 + self.assertTrue((states[4:] == 0).all()) + + self.assertEqual(sku3_consumer_unit_id, states[0]) + self.assertEqual(SKU3_ID, states[2]) if __name__ == '__main__': From 3ddcd0b1afdb96cc034c8429bdf8fb711955f6f4 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 19 Mar 2021 18:24:21 +0800 Subject: [PATCH 086/482] remove dup product id from consumer unit, test case for action --- .../supply_chain/datamodels/consumer.py | 1 - .../scenarios/supply_chain/units/consumer.py | 6 +- tests/data/supply_chain/case_01/config.yml | 21 ++- tests/supply_chain/test_supply_chain.py | 140 +++++++++++++++++- 4 files changed, 160 insertions(+), 8 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index 088942daf..f0c674569 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -18,7 +18,6 @@ class ConsumerDataModel(SkuDataModel): total_received = NodeAttribute(AttributeType.UInt) # action states - consumer_product_id = NodeAttribute(AttributeType.UInt) source_id = NodeAttribute(AttributeType.UInt) quantity = NodeAttribute(AttributeType.UInt) vlt = NodeAttribute(AttributeType.UShort) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index c0cb71782..654b9b002 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -92,14 +92,14 @@ def initialize(self): def step(self, tick: int): # NOTE: id == 0 means invalid,as our id is 1 based. - if self.action is None or self.action.quantity <= 0 or self.action.consumer_product_id <= 0 or self.action.source_id == 0: + if self.action is None or self.action.quantity <= 0 or self.action.product_id <= 0 or self.action.source_id == 0: return # NOTE: we are using product unit as destination, # so we expect the action.source_id is and id of product unit - self.update_open_orders(self.action.source_id, self.action.consumer_product_id, self.action.quantity) + self.update_open_orders(self.action.source_id, self.action.product_id, self.action.quantity) - order = Order(self.facility, self.action.consumer_product_id, self.action.quantity, self.action.vlt) + order = Order(self.facility, self.action.product_id, self.action.quantity, self.action.vlt) source_facility = self.world.get_facility_by_id(self.action.source_id) diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml index 6d18ef7bc..d10747daa 100644 --- a/tests/data/supply_chain/case_01/config.yml +++ b/tests/data/supply_chain/case_01/config.yml @@ -1,4 +1,18 @@ +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 100 + +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + vehicles: + - *normal_vehicle + - *normal_vehicle + config: + unit_price: 1 + facility_definitions: SupplierFacility: &supplier_facility @@ -14,7 +28,8 @@ facility_definitions: class: "ConsumerUnit" manufacture: class: "ManufactureUnit" - + distribution: + class: "DistributionUnit" small_storage: &small_storage config: @@ -74,6 +89,7 @@ world: order_cost: 0 children: storage: *small_storage + distribution: *normal_distribution - name: "Supplier_SKU1" definition_ref: "SupplierFacility" skus: @@ -94,6 +110,7 @@ world: order_cost: 200 children: storage: *midium_storage + distribution: *normal_distribution - name: "Supplier_SKU2" definition_ref: "SupplierFacility" skus: @@ -114,6 +131,7 @@ world: order_cost: 200 children: storage: *midium_storage + distribution: *normal_distribution - name: "Supplier_SKU4" definition_ref: "SupplierFacility" skus: @@ -134,6 +152,7 @@ world: order_cost: 200 children: storage: *midium_storage + distribution: *normal_distribution topology: Supplier_SKU1: diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 02361c7fe..a249cb50a 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -4,7 +4,7 @@ import numpy as np from maro.simulator import Env -from maro.simulator.scenarios.supply_chain import ManufactureAction +from maro.simulator.scenarios.supply_chain import ManufactureAction, ConsumerAction from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase @@ -648,7 +648,6 @@ def test_consumer_init_state(self): self.assertEqual(SKU3_ID, sku3_consumer_unit.data_model.product_id) self.assertEqual(sku3_consumer_unit_id, sku3_consumer_unit.data_model.id) self.assertEqual(sku3_product_unit_id, sku3_consumer_unit.data_model.product_unit_id) - self.assertEqual(0, sku3_consumer_unit.data_model.consumer_product_id) self.assertEqual(0, sku3_consumer_unit.data_model.source_id) self.assertEqual(0, sku3_consumer_unit.data_model.quantity) self.assertEqual(0, sku3_consumer_unit.data_model.vlt) @@ -673,7 +672,6 @@ def test_consumer_init_state(self): "order_cost", "total_purchased", "total_received", - "consumer_product_id", "source_id", "quantity", "vlt", @@ -692,6 +690,142 @@ def test_consumer_init_state(self): self.assertEqual(sku3_consumer_unit_id, states[0]) self.assertEqual(SKU3_ID, states[2]) + cur_sources = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"sources"].flatten().astype(np.int) + + # only one source according to configuration + self.assertEqual(1, len(cur_sources)) + self.assertEqual(sku3_supplier_faiclity_id, cur_sources[0]) + + env.reset() + env.step(None) + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # Nothing happened at tick 0, so most states will be 0 + self.assertTrue((states[4:] == 0).all()) + + self.assertEqual(sku3_consumer_unit_id, states[0]) + self.assertEqual(SKU3_ID, states[2]) + + cur_sources = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"sources"].flatten().astype(np.int) + + # only one source according to configuration + self.assertEqual(1, len(cur_sources)) + self.assertEqual(sku3_supplier_faiclity_id, cur_sources[0]) + + def test_consumer_action(self): + env = build_env("case_01", 100) + + sku3_consumer_unit: ConsumerUnit + sku3_supplier_faiclity_id: int + sku3_consumer_data_model_index: int + sku3_product_unit_id: int + + for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): + if facility_defail["name"] == "Supplier_SKU1": + sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] + + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] + + if facility_defail["name"] == "Supplier_SKU3": + sku3_supplier_faiclity_id = facility_defail["id"] + + sku3_consumer_data_model_index = env.summary["node_mapping"]["entity_mapping"][sku3_consumer_unit_id][1] + + # zero quantity will be ignore + action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 0, 1) + + action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 10, 1) + + sku3_consumer_unit.set_action(action_with_zero) + + env.step(None) + + features = ( + "id", + "facility_id", + "product_id", + "order_cost", + "total_purchased", + "total_received", + "product_id", + "source_id", + "quantity", + "vlt", + "purchased", + "received", + "order_product_cost" + ) + + consumer_nodes = env.snapshot_list["consumer"] + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # Nothing happened at tick 0, at the action will be recorded + self.assertEqual(action_with_zero.product_id, states[6]) + self.assertEqual(action_with_zero.source_id, states[7]) + self.assertEqual(action_with_zero.quantity, states[8]) + self.assertEqual(action_with_zero.vlt, states[9]) + self.assertTrue((states[[4, 5, 10, 11, 12]] == 0).all()) + + self.assertEqual(sku3_consumer_unit_id, states[0]) + self.assertEqual(SKU3_ID, states[2]) + + cur_sources = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"sources"].flatten().astype(np.int) + + # only one source according to configuration + self.assertEqual(1, len(cur_sources)) + self.assertEqual(sku3_supplier_faiclity_id, cur_sources[0]) + + # NOTE: we cannot set_action directly here, as post_step will clear the action before starting next tick + env.step({action.id: action}) + + self.assertEqual(action.quantity, sku3_consumer_unit.purchased) + self.assertEqual(0, sku3_consumer_unit.received) + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # action field should be recorded + self.assertEqual(action.product_id, states[6]) + self.assertEqual(action.source_id, states[7]) + self.assertEqual(action.quantity, states[8]) + self.assertEqual(action.vlt, states[9]) + + # total purchased should be same as purchased at this tick. + self.assertEqual(action.quantity, states[4]) + + # no received now + self.assertEqual(0, states[5]) + + # purchased same as quantity + self.assertEqual(action.quantity, states[10]) + + # no receives + self.assertEqual(0, states[11]) + + # same action for next step, so total_XXX will be changed to double + env.step({action.id: action}) + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # action field should be recorded + self.assertEqual(action.product_id, states[6]) + self.assertEqual(action.source_id, states[7]) + self.assertEqual(action.quantity, states[8]) + self.assertEqual(action.vlt, states[9]) + + # total purchased should be same as purchased at this tick. + self.assertEqual(action.quantity * 2, states[4]) + + # no received now + self.assertEqual(0, states[5]) + + # purchased same as quantity + self.assertEqual(action.quantity, states[10]) + + # no receives + self.assertEqual(0, states[11]) if __name__ == '__main__': unittest.main() From 4233982e69c23a441eefe9cd9f003a76d50a0e2e Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 19 Mar 2021 18:34:53 +0800 Subject: [PATCH 087/482] add type checking for action, test done for consumer unit --- .../scenarios/supply_chain/business_engine.py | 2 +- tests/supply_chain/test_supply_chain.py | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index ba0a9077e..3d6c4e27e 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -112,7 +112,7 @@ def _build_world(self): def _on_action_received(self, event): action = event.payload - if action is not None and len(action) > 0: + if action is not None and type(action) == dict and len(action) > 0: self._action_cache = action def _dispatch_action(self): diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index a249cb50a..7813f7d5c 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -827,5 +827,48 @@ def test_consumer_action(self): # no receives self.assertEqual(0, states[11]) + def test_consumer_on_order_reception(self): + env = build_env("case_01", 100) + + sku3_consumer_unit: ConsumerUnit + sku3_supplier_facility_id: int + sku3_consumer_data_model_index: int + sku3_product_unit_id: int + + for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): + if facility_defail["name"] == "Supplier_SKU1": + sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] + + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] + + if facility_defail["name"] == "Supplier_SKU3": + sku3_supplier_facility_id = facility_defail["id"] + + sku3_consumer_data_model_index = env.summary["node_mapping"]["entity_mapping"][sku3_consumer_unit_id][1] + + action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1) + + # 1st step must none action + env.step(None) + + env.step({action.id: action}) + + # simulate purchased product is arrived by vehicle unit + sku3_consumer_unit.on_order_reception(sku3_supplier_facility_id, SKU3_ID, 10, 10) + + # now all order is done + self.assertEqual(0, sku3_consumer_unit.open_orders[sku3_supplier_facility_id][SKU3_ID]) + self.assertEqual(10, sku3_consumer_unit.received) + + env.step(None) + + consumer_nodes = env.snapshot_list["consumer"] + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"received"].flatten().astype(np.int) + + # NOTE: we cannot test the received state by calling on_order_reception directly, + # as it will be cleared by env.step, do it on vehicle unit test. + + if __name__ == '__main__': unittest.main() From 106650519c1c60597a2d976ef7d9f7b40b4e124a Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sat, 20 Mar 2021 00:16:54 +0800 Subject: [PATCH 088/482] added exp sync logic --- docs/source/examples/multi_agent_dqn_cim.rst | 2 +- docs/source/key_components/rl_toolkit.rst | 2 +- examples/cim/ac/main.py | 3 +- examples/cim/common.py | 39 +++-- examples/cim/dqn/main.py | 19 +-- maro/rl/__init__.py | 10 +- maro/rl/storage/simple_store.py | 56 ++++--- maro/rl/training/__init__.py | 4 +- maro/rl/training/actor.py | 139 +++++++++--------- maro/rl/training/actor_proxy.py | 16 +- maro/rl/training/learner.py | 51 ++----- maro/rl/training/message_enums.py | 3 +- maro/rl/training/trajectory.py | 61 +++++++- maro/rl/utils/__init__.py | 8 +- maro/rl/utils/experience_collection.py | 62 -------- maro/rl/utils/trajectory_utils.py | 47 ++++++ .../articles/simple_bike_repositioning.ipynb | 2 +- 17 files changed, 265 insertions(+), 259 deletions(-) delete mode 100644 maro/rl/utils/experience_collection.py diff --git a/docs/source/examples/multi_agent_dqn_cim.rst b/docs/source/examples/multi_agent_dqn_cim.rst index 1a97aa4e9..228f694a8 100644 --- a/docs/source/examples/multi_agent_dqn_cim.rst +++ b/docs/source/examples/multi_agent_dqn_cim.rst @@ -143,7 +143,7 @@ from the learner. env = Env(**training_config["env"]) agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) actor = Actor(env, agent, CIMTrajectoryForDQN, trajectory_kwargs=common_config) - actor.as_worker(training_config["group"]) + actor.worker(training_config["group"]) The learner's side requires a concrete learner class that inherits from ``AbsLearner`` and implements the ``run`` method which contains the main training loop. Here the implementation is similar to the single-threaded version diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index be4811543..e1d8b6e6d 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -103,7 +103,7 @@ The RL toolkit provides tools that make local and distributed training easy: distributed fashion by loading an ``Actor`` or ``ActorProxy`` instance, respectively. * Actor, which implements the ``roll_out`` method where the agent interacts with the environment for one episode. It consists of an environment instance and an agent (a single agent or multiple agents wrapped by - ``MultiAgentWrapper``). The class provides the as_worker() method which turns it to an event loop where roll-outs + ``MultiAgentWrapper``). The class provides the worker() method which turns it to an event loop where roll-outs are performed on the learner's demand. In distributed RL, there are typically many actor processes running simultaneously to parallelize training data collection. * Actor proxy, which also implements the ``roll_out`` method with the same signature, but manages a set of remote diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index 30d831b3b..5d467615b 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -53,7 +53,8 @@ def on_finish(self): if __name__ == "__main__": set_seeds(1024) # for reproducibility env = Env(**training_config["env"]) + trajectory = CIMTrajectoryForAC(env, **common_config) agent = MultiAgentWrapper({name: get_ac_agent() for name in env.agent_idx_list}) - actor = Actor(env, agent, CIMTrajectoryForAC, trajectory_kwargs=common_config) # local actor + actor = Actor(trajectory, agent) # local actor learner = OnPolicyLearner(actor, training_config["max_episode"]) learner.run() diff --git a/examples/cim/common.py b/examples/cim/common.py index bb66ba243..4ff3f4f1a 100644 --- a/examples/cim/common.py +++ b/examples/cim/common.py @@ -5,7 +5,7 @@ import numpy as np -from maro.rl import Trajectory +from maro.rl import AbsTrajectory from maro.simulator.scenarios.cim.common import Action, ActionType common_config = { @@ -26,7 +26,7 @@ } -class CIMTrajectory(Trajectory): +class CIMTrajectory(AbsTrajectory): def __init__( self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream, reward_time_window, fulfillment_factor, shortage_factor, time_decay, @@ -76,24 +76,21 @@ def get_action(self, action_by_agent, event): return {port: Action(vessel, port, actual_action, action_type)} - def get_offline_reward(self, event): - port_snapshots = self.env.snapshot_list["ports"] - start_tick = event.tick + 1 - ticks = list(range(start_tick, start_tick + self.reward_time_window)) + def on_finish(self): + """Compute offline rewards.""" + for i, event in enumerate(self.events): + port_snapshots = self.env.snapshot_list["ports"] + start_tick = event.tick + 1 + ticks = list(range(start_tick, start_tick + self.reward_time_window)) - future_fulfillment = port_snapshots[ticks::"fulfillment"] - future_shortage = port_snapshots[ticks::"shortage"] - decay_list = [ - self.time_decay ** i for i in range(self.reward_time_window) - for _ in range(future_fulfillment.shape[0] // self.reward_time_window) - ] + future_fulfillment = port_snapshots[ticks::"fulfillment"] + future_shortage = port_snapshots[ticks::"shortage"] + decay_list = [ + self.time_decay ** i for i in range(self.reward_time_window) + for _ in range(future_fulfillment.shape[0] // self.reward_time_window) + ] - tot_fulfillment = np.dot(future_fulfillment, decay_list) - tot_shortage = np.dot(future_shortage, decay_list) - - return np.float32(self.fulfillment_factor * tot_fulfillment - self.shortage_factor * tot_shortage) - - def on_env_feedback(self, event, state_by_agent, action_by_agent, reward): - self.trajectory["event"].append(event) - self.trajectory["state"].append(state_by_agent) - self.trajectory["action"].append(action_by_agent) + self.rewards[i] = np.float32( + self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - + self.shortage_factor * np.dot(future_shortage, decay_list) + ) diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index a882bf26d..3df47f339 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -26,20 +26,6 @@ def get_dqn_agent(): return DQN(q_model, DQNConfig(**agent_config["hyper_params"])) -class CIMTrajectoryForDQN(CIMTrajectory): - def on_finish(self): - exp_by_agent = defaultdict(lambda: defaultdict(list)) - for i in range(len(self.trajectory["state"]) - 1): - agent_id = list(self.trajectory["state"][i].keys())[0] - exp = exp_by_agent[agent_id] - exp["S"].append(self.trajectory["state"][i][agent_id]) - exp["A"].append(self.trajectory["action"][i][agent_id]) - exp["R"].append(self.get_offline_reward(self.trajectory["event"][i])) - exp["S_"].append(list(self.trajectory["state"][i + 1].values())[0]) - - return dict(exp_by_agent) - - def cim_dqn_learner(): env = Env(**training_config["env"]) agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) @@ -54,9 +40,10 @@ def cim_dqn_learner(): def cim_dqn_actor(): env = Env(**training_config["env"]) + trajectory = CIMTrajectory(env, **common_config) agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) - actor = Actor(env, agent, CIMTrajectoryForDQN, trajectory_kwargs=common_config) - actor.as_worker(training_config["group"]) + actor = Actor(trajectory, agent, group=training_config["group"]) + actor.run() if __name__ == "__main__": diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 40571c71e..a760127f6 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -10,10 +10,10 @@ from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler from maro.rl.storage import AbsStore, OverwriteType, SimpleStore -from maro.rl.training import AbsLearner, Actor, ActorProxy, OffPolicyLearner, OnPolicyLearner, Trajectory +from maro.rl.training import AbsLearner, AbsTrajectory, Actor, ActorProxy, OffPolicyLearner, OnPolicyLearner from maro.rl.utils import ( - ExperienceCollectionUtils, get_k_step_returns, get_lambda_returns, get_log_prob, get_max, - get_truncated_cumulative_reward, select_by_actions + get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_sars, get_truncated_cumulative_reward, + select_by_actions ) __all__ = [ @@ -23,7 +23,7 @@ "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", "AbsStore", "OverwriteType", "SimpleStore", - "AbsLearner", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner", "Trajectory", - "ExperienceCollectionUtils", "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", + "AbsLearner", "AbsTrajectory", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner", + "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_sars", "get_truncated_cumulative_reward", "select_by_actions" ] diff --git a/maro/rl/storage/simple_store.py b/maro/rl/storage/simple_store.py index 9632cf9d6..87ee2364c 100644 --- a/maro/rl/storage/simple_store.py +++ b/maro/rl/storage/simple_store.py @@ -40,26 +40,14 @@ def __init__(self, keys: list, capacity: int = -1, overwrite_type: OverwriteType self._keys = keys self._capacity = capacity self._overwrite_type = overwrite_type - self._store = {key: [] if self._capacity < 0 else [None] * self._capacity for key in keys} + self.data = {key: [] if self._capacity < 0 else [None] * self._capacity for key in keys} self._size = 0 - self._iter_index = 0 def __len__(self): return self._size - def __iter__(self): - return self - - def __next__(self): - if self._iter_index >= self._size: - self._iter_index = 0 - raise StopIteration - index = self._iter_index - self._iter_index += 1 - return {k: lst[index] for k, lst in self._store.items()} - def __getitem__(self, index: int): - return {k: lst[index] for k, lst in self._store.items()} + return {k: lst[index] for k, lst in self.data.items()} @property def keys(self): @@ -80,7 +68,7 @@ def overwrite_type(self): return self._overwrite_type def get(self, indexes: [int]) -> dict: - return {k: [self._store[k][i] for i in indexes] for k in self._store} + return {k: [self.data[k][i] for i in indexes] for k in self.data} def put(self, contents: Dict[str, List], overwrite_indexes: list = None) -> List[int]: """Put new contents in the store. @@ -95,14 +83,14 @@ def put(self, contents: Dict[str, List], overwrite_indexes: list = None) -> List Returns: The indexes where the newly added entries reside in the store. """ - if len(self._store) > 0 and list(contents.keys()) != self._keys: + if len(self.data) > 0 and list(contents.keys()) != self._keys: raise StoreMisalignment(f"expected keys {self._keys}, got {list(contents.keys())}") self.validate(contents) added = contents[next(iter(contents))] added_size = len(added) if isinstance(added, list) else 1 if self._capacity < 0: for key, val in contents.items(): - self._store[key].extend(val) + self.data[key].extend(val) self._size += added_size return list(range(self._size - added_size, self._size)) else: @@ -126,7 +114,7 @@ def update(self, indexes: list, contents: Dict[str, List]): self.validate(contents) for key, val in contents.items(): for index, value in zip(indexes, val): - self._store[key][index] = value + self.data[key][index] = value return indexes @@ -198,7 +186,7 @@ def sample_by_key(self, key, size: int, replace: bool = True): Returns: Sampled indexes and the corresponding objects. """ - weights = np.asarray(self._store[key][:self._size] if self._size < self._capacity else self._store[key]) + weights = np.asarray(self.data[key][:self._size] if self._size < self._capacity else self.data[key]) indexes = np.random.choice(self._size, size=size, replace=replace, p=weights / np.sum(weights)) return indexes, self.get(indexes) @@ -218,24 +206,23 @@ def sample_by_keys(self, keys: list, sizes: list, replace: bool = True): indexes = range(self._size) for key, size in zip(keys, sizes): - weights = np.asarray([self._store[key][i] for i in indexes]) + weights = np.asarray([self.data[key][i] for i in indexes]) indexes = np.random.choice(indexes, size=size, replace=replace, p=weights / np.sum(weights)) return indexes, self.get(indexes) def clear(self): """Empty the store.""" - self._store = {key: [] if self._capacity < 0 else [None] * self._capacity for key in self._keys} + self.data = {key: [] if self._capacity < 0 else [None] * self._capacity for key in self._keys} self._size = 0 - self._iter_index = 0 def dumps(self): """Return a deep copy of store contents.""" - return clone(dict(self._store)) + return clone(dict(self.data)) def get_by_key(self, key): """Get the contents of the store corresponding to ``key``.""" - return self._store[key] + return self.data[key] def _get_update_indexes(self, added_size: int, overwrite_indexes=None): if added_size > self._capacity: @@ -268,3 +255,24 @@ def validate(contents: Dict[str, List]): reference_val = contents[list(contents.keys())[0]] if any(len(val) != len(reference_val) for val in contents.values()): raise StoreMisalignment("values of contents should consist of lists of the same length") + + @staticmethod + def concatenate(stores: List[SimpleStore], capacity: int = -1, overwrite_type: OverwriteType = None): + """Concatenate a list of stores with the same keys. + + The resulting store is of unbounded capacity. + + Args: + stores (List[SimpleStore]): List of stores to be concatenated. + """ + if not stores: + return + expected_keys = stores[0].data.keys + if any(store.data.keys() != expected_keys for store in stores): + raise ValueError("Stores must have the exact same keys to be concatenated") + + ret_store = SimpleStore(list(expected_keys)) + for store in stores: + ret_store.put(store.data) + + return ret_store diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 4bd6269b3..5b778aa2f 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -4,6 +4,6 @@ from .actor import Actor from .actor_proxy import ActorProxy from .learner import AbsLearner, OffPolicyLearner, OnPolicyLearner -from .trajectory import Trajectory +from .trajectory import AbsTrajectory -__all__ = ["AbsLearner", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner", "Trajectory"] +__all__ = ["AbsLearner", "AbsTrajectory", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner"] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 84e45c7f8..57725bd54 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -1,112 +1,109 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import sys -from typing import Union +from typing import Callable, Union from maro.communication import Message, Proxy from maro.rl.agent import AbsAgent, MultiAgentWrapper -from maro.simulator import Env +from maro.rl.storage import SimpleStore +from maro.rl.utils import get_sars from maro.utils import InternalLogger from .message_enums import MessageTag, PayloadKey +from .trajectory import AbsTrajectory class Actor(object): """Actor class that performs roll-out tasks. Args: - env (Env): An environment instance. + trajectory (AbsTrajectory): An ``AbsTrajectory`` instance with an env wrapped inside. agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. - mode (str): One of "local" and "distributed". Defaults to "local". + group (str): Identifier of the group to which the actor belongs. It must be the same group name + assigned to the learner (and decision clients, if any). + proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to None. + sync_size (int): """ def __init__( self, - env: Env, + trajectory: AbsTrajectory, agent: Union[AbsAgent, MultiAgentWrapper], - trajectory_cls, - trajectory_kwargs: dict = None + group: str = None, + proxy_options: dict = None, + experience_sync_interval: int = None ): - super().__init__() - self.env = env + self.trajectory = trajectory self.agent = agent - if trajectory_kwargs is None: - trajectory_kwargs = {} - self.trajectory = trajectory_cls(self.env, **trajectory_kwargs) - + self.experience_pool = SimpleStore(["S", "A", "R", "S_", "loss"]) + if group is not None: + if proxy_options is None: + proxy_options = {} + self._proxy = Proxy(group, "actor", {"actor_proxy": 1}, **proxy_options) + self._experience_sync_interval = experience_sync_interval + self._logger = InternalLogger(self._proxy.name) + self.exp_sync = bool(getattr(self, "_experience_sync_interval", None)) + def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): - """Perform one episode of roll-out. - Args: - index (int): Externally designated index to identify the roll-out round. - training (bool): If true, the roll-out is for training purposes, which usually means - some kind of training data, e.g., experiences, needs to be collected. Defaults to True. - model_by_agent (dict): Models to use for inference. Defaults to None. - exploration_params: Exploration parameters to use for the current roll-out. Defaults to None. - Returns: - Data collected during the episode. - """ - self.env.reset() self.trajectory.reset() + if not training: + self.trajectory.record_path = False # no need to record the trajectory if roll-out is not for training + + # Load models and exploration parameters if model_by_agent: self.agent.load_model(model_by_agent) if exploration_params: self.agent.set_exploration_params(exploration_params) - _, event, is_done = self.env.step(None) - while not is_done: - state_by_agent = self.trajectory.get_state(event) - action_by_agent = self.agent.choose_action(state_by_agent) - env_action = self.trajectory.get_action(action_by_agent, event) - if len(env_action) == 1: - env_action = list(env_action.values())[0] - _, next_event, is_done = self.env.step(env_action) - reward = self.trajectory.get_reward() - self.trajectory.on_env_feedback( - event, state_by_agent, action_by_agent, reward if reward is not None else self.env.metrics - ) - event = next_event + state = self.trajectory.start(rollout_index=index) # get initial state + while state: + action = self.agent.choose_action(state) + state = self.trajectory.step(action) + if training and self.exp_sync and len(self.trajectory.states) == self._experience_sync_interval: + exp = get_sars(self.trajectory.states, self.trajectory.actions, self.trajectory.rewards) + self.trajectory.flush() + self._proxy.isend( + Message( + MessageTag.EXPERIENCE, self._proxy.name, self._proxy.peers_name["actor_proxy"][0], + payload={PayloadKey.ROLLOUT_INDEX: index, PayloadKey.EXPERIENCE: exp} + ) + ) - return self.env.metrics, self.trajectory.on_finish() if training else None + self.trajectory.on_env_feedback() - def as_worker(self, group: str, proxy_options=None): - """Executes an event loop where roll-outs are performed on demand from a remote learner. + self.trajectory.on_finish() + # If no experience syncing, the experience pool needs to be populated. + if training and not self._experience_sync_interval: + if isinstance(self.agent, AbsAgent): + self.experience_pool.put(get_sars(*self.trajectory.path, multi_agent=False)) + else: + for agent_id, exp in get_sars(*self.trajectory.path).items(): + self.experience_pool[agent_id].put(exp) - Args: - group (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the learner (and decision clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. - """ - if proxy_options is None: - proxy_options = {} - proxy = Proxy(group, "actor", {"learner": 1}, **proxy_options) - logger = InternalLogger(proxy.name) - for msg in proxy.receive(): + return self.trajectory.env.metrics + + def run(self): + assert hasattr(self, "_proxy"), "No proxy found. The `group` parameter should not be None at init." + for msg in self._proxy.receive(): if msg.tag == MessageTag.EXIT: - logger.info("Exiting...") - sys.exit(0) + self._logger.info("Exiting...") + break elif msg.tag == MessageTag.ROLLOUT: ep = msg.payload[PayloadKey.ROLLOUT_INDEX] - logger.info(f"Rolling out ({ep})...") - metrics, rollout_data = self.roll_out( + self._logger.info(f"Rolling out ({ep})...") + metrics = self.roll_out( ep, training=msg.payload[PayloadKey.TRAINING], model_by_agent=msg.payload[PayloadKey.MODEL], exploration_params=msg.payload[PayloadKey.EXPLORATION_PARAMS] ) - if rollout_data is None: - logger.info(f"Roll-out {ep} aborted") - else: - logger.info(f"Roll-out {ep} finished") - rollout_finish_msg = Message( - MessageTag.FINISHED, - proxy.name, - proxy.peers_name["learner"][0], - payload={ - PayloadKey.ROLLOUT_INDEX: ep, - PayloadKey.METRICS: metrics, - PayloadKey.DETAILS: rollout_data - } + self._logger.info(f"Roll-out {ep} finished") + payload = {PayloadKey.ROLLOUT_INDEX: ep, PayloadKey.METRICS: metrics} + if msg.payload[PayloadKey.TRAINING] and not self.exp_sync: + payload[PayloadKey.EXPERIENCE] = self.experience_pool + self._proxy.isend( + Message( + MessageTag.FINISHED, self._proxy.name, self._proxy.peers_name["actor_proxy"][0], + payload=payload ) - proxy.isend(rollout_finish_msg) - self.env.reset() + ) diff --git a/maro/rl/training/actor_proxy.py b/maro/rl/training/actor_proxy.py index ab51f05a7..fe11a0d2a 100644 --- a/maro/rl/training/actor_proxy.py +++ b/maro/rl/training/actor_proxy.py @@ -4,6 +4,7 @@ from typing import List from maro.communication import Message, Proxy, RegisterTable, SessionType +from maro.rl.storage import SimpleStore from maro.utils import InternalLogger from .message_enums import MessageTag, PayloadKey @@ -28,11 +29,10 @@ def __init__( update_trigger: str = None, proxy_options: dict = None ): - self.agent = None peers = {"actor": num_actors} if proxy_options is None: proxy_options = {} - self._proxy = Proxy(group_name, "learner", peers, **proxy_options) + self._proxy = Proxy(group_name, "actor_proxy", peers, **proxy_options) self._actors = self._proxy.peers_name["actor"] # remote actor ID's self._registry_table = RegisterTable(self._proxy.peers_name) if update_trigger is None: @@ -40,6 +40,7 @@ def __init__( self._registry_table.register_event_handler( f"actor:{MessageTag.FINISHED.value}:{update_trigger}", self._on_rollout_finish ) + self.experience_pool = SimpleStore(["S", "A", "R", "S_", "loss"]) self.logger = InternalLogger("ACTOR_PROXY") def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): @@ -73,15 +74,18 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non # the next episode. result = self._registry_table.push(msg) if result: - env_metrics, details = result[0] + env_metrics = result[0] break + elif msg.tag == MessageTag.EXPERIENCE: + self.experience_pool.put(msg.payload[PayloadKey.EXPERIENCE]) - return env_metrics, details + return env_metrics def _on_rollout_finish(self, messages: List[Message]): metrics = {msg.source: msg.payload[PayloadKey.METRICS] for msg in messages} - details = {msg.source: msg.payload[PayloadKey.DETAILS] for msg in messages} - return metrics, details + if PayloadKey.EXPERIENCE in messages[0].payload: + self.experience_pool = SimpleStore.concatenate([msg.payload[PayloadKey.EXPERIENCE] for msg in messages]) + return metrics def terminate(self): """Tell the remote actors to exit.""" diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 6dfccce7a..d0e548c22 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -2,14 +2,13 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from typing import Union +from typing import Callable, Union from numpy import asarray from maro.rl.agent import AbsAgent, MultiAgentWrapper from maro.rl.scheduling import Scheduler from maro.rl.storage import SimpleStore -from maro.rl.utils import ExperienceCollectionUtils from maro.utils import InternalLogger from .actor import Actor @@ -55,15 +54,10 @@ def __init__( def run(self): for ep in range(self.max_episode): - env_metrics, exp = self.actor.roll_out( + env_metrics = self.actor.roll_out( ep, model_by_agent=self.agent.dump_model() if isinstance(self.actor, ActorProxy) else None ) self.logger.info(f"ep-{ep}: {env_metrics}") - exp = ExperienceCollectionUtils.stack( - exp, - is_single_source=isinstance(self.actor, Actor), - is_single_agent=isinstance(self.agent, AbsAgent) - ) if isinstance(self.agent, AbsAgent): for e in exp: self.agent.learn(*e["args"], **e.get("kwargs", {})) @@ -95,12 +89,6 @@ def __init__( ): super().__init__(actor, agent=agent) self.scheduler = scheduler - if isinstance(self.agent, AbsAgent): - self.experience_pool = SimpleStore(["S", "A", "R", "S_", "loss"]) - else: - self.experience_pool = { - agent: SimpleStore(["S", "A", "R", "S_", "loss"]) for agent in self.agent.agent_dict - } self.train_iter = train_iter self.min_experiences_to_train = min_experiences_to_train self.batch_size = batch_size @@ -109,39 +97,26 @@ def __init__( def run(self): for exploration_params in self.scheduler: rollout_index = self.scheduler.iter - env_metrics, exp = self.actor.roll_out( + env_metrics = self.actor.roll_out( rollout_index, model_by_agent=self.agent.dump_model() if isinstance(self.actor, ActorProxy) else None, exploration_params=exploration_params ) self.logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") - # store experiences in the experience pool. - exp = ExperienceCollectionUtils.concat( - exp, - is_single_source=isinstance(self.actor, Actor), - is_single_agent=isinstance(self.agent, AbsAgent) - ) if isinstance(self.agent, AbsAgent): - exp.update({"loss": [MAX_LOSS] * len(list(exp.values())[0])}) - self.experience_pool.put(exp) - for i in range(self.train_iter): + for _ in range(self.train_iter): batch, idx = self.get_batch() loss = self.agent.learn(*batch) - self.experience_pool.update(idx, {"loss": list(loss)}) + self.actor.experience_pool.update(idx, {"loss": list(loss)}) else: - for agent_id, ex in exp.items(): - # ensure new experiences are sampled with the highest priority - ex.update({"loss": [MAX_LOSS] * len(list(ex.values())[0])}) - self.experience_pool[agent_id].put(ex) - - for i in range(self.train_iter): + for _ in range(self.train_iter): batch_by_agent, idx_by_agent = self.get_batch() loss_by_agent = { agent_id: self.agent[agent_id].learn(*batch) for agent_id, batch in batch_by_agent.items() } for agent_id, loss in loss_by_agent.items(): - self.experience_pool[agent_id].update(idx_by_agent[agent_id], {"loss": list(loss)}) + self.actor.experience_pool[agent_id].update(idx_by_agent[agent_id], {"loss": list(loss)}) self.logger.info("Agent learning finished") @@ -151,23 +126,23 @@ def run(self): def get_batch(self): if isinstance(self.agent, AbsAgent): - if len(self.experience_pool) < self.min_experiences_to_train: + if len(self.actor.experience_pool) < self.min_experiences_to_train: return None, None if self.prioritized_sampling_by_loss: - indexes, sample = self.experience_pool.sample_by_key("loss", self.batch_size) + indexes, sample = self.actor.experience_pool.sample_by_key("loss", self.batch_size) else: - indexes, sample = self.experience_pool.sample(self.batch_size) + indexes, sample = self.actor.experience_pool.sample(self.batch_size) batch = asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) return batch, indexes else: idx, batch = {}, {} - for agent_id, pool in self.experience_pool.items(): + for agent_id, pool in self.actor.experience_pool.items(): if len(pool) < self.min_experiences_to_train: continue if self.prioritized_sampling_by_loss: - indexes, sample = self.experience_pool[agent_id].sample_by_key("loss", self.batch_size) + indexes, sample = self.actor.experience_pool[agent_id].sample_by_key("loss", self.batch_size) else: - indexes, sample = self.experience_pool[agent_id].sample(self.batch_size) + indexes, sample = self.actor.experience_pool[agent_id].sample(self.batch_size) batch[agent_id] = ( asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) ) diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index 44302d47e..bdc2b182f 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -5,6 +5,7 @@ class MessageTag(Enum): ROLLOUT = "rollout" CHOOSE_ACTION = "choose_action" ACTION = "action" + EXPERIENCE = "experience_sync" ABORT_ROLLOUT = "abort_rollout" TRAIN = "train" FINISHED = "finished" @@ -17,7 +18,7 @@ class PayloadKey(Enum): ROLLOUT_INDEX = "rollout_index" TIME_STEP = "time_step" METRICS = "metrics" - DETAILS = "details" + EXPERIENCE = "experience" STATE = "state" TRAINING = "training" MODEL = "model" diff --git a/maro/rl/training/trajectory.py b/maro/rl/training/trajectory.py index 5a014c698..751871294 100644 --- a/maro/rl/training/trajectory.py +++ b/maro/rl/training/trajectory.py @@ -1,23 +1,64 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from abc import ABC, abstractmethod from collections import defaultdict +from multiprocessing import Pipe, Process +from typing import Callable +from maro.communication import Message, Proxy -class Trajectory(object): - def __init__(self, env): +from .message_enums import MessageTag, PayloadKey + + +class AbsTrajectory(ABC): + def __init__(self, env, record_path: bool = True): self.env = env - self.trajectory = defaultdict(list) + self.events = [] + self.states = [] + self.actions = [] + self.rewards = [] + self.record_path = record_path + + def start(self, rollout_index: int = None): + _, event, _ = self.env.step(None) + state = self.get_state(event) + if self.record_path: + self.events.append(event) + self.states.append(state) + return state + @property + def path(self): + return self.states, self.actions, self.rewards + + @abstractmethod def get_state(self, event) -> dict: pass - def get_action(self, action_by_agent, event) -> dict: + @abstractmethod + def get_action(self, action, event) -> dict: pass def get_reward(self) -> float: pass + def step(self, action): + assert self.events, "start() must be called first." + env_action = self.get_action(action, self.events[-1]) + if len(env_action) == 1: + env_action = list(env_action.values())[0] + _, event, done = self.env.step(env_action) + if self.record_path: + self.actions.append(action) + self.rewards.append(self.get_reward()) + if not done: + state = self.get_state(event) + if self.record_path: + self.events.append(event) + self.states.append(state) + return state + def on_env_feedback(self): pass @@ -25,4 +66,14 @@ def on_finish(self): pass def reset(self): - self.trajectory = defaultdict(list) + self.env.reset() + self.events = [] + self.states = [] + self.actions = [] + self.rewards = [] + + def flush(self): + self.events = self.events[-1:] + self.states = self.states[-1:] + self.actions = self.actions[-1:] + self.rewards = self.rewards[-1:] diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index e274024a3..f12124c1d 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .experience_collection import ExperienceCollectionUtils -from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_truncated_cumulative_reward +from .trajectory_utils import ( + get_k_step_returns, get_lambda_returns, get_sars, get_truncated_cumulative_reward +) from .value_utils import get_log_prob, get_max, get_td_errors, select_by_actions __all__ = [ - "ExperienceCollectionUtils", "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", - "get_log_prob", "get_max", "get_td_errors", "select_by_actions", + "get_log_prob", "get_max", "get_sars", "get_td_errors", "select_by_actions" ] diff --git a/maro/rl/utils/experience_collection.py b/maro/rl/utils/experience_collection.py deleted file mode 100644 index d190a5d67..000000000 --- a/maro/rl/utils/experience_collection.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict - - -class ExperienceCollectionUtils: - @staticmethod - def concat(exp, is_single_source: bool = False, is_single_agent: bool = False) -> dict: - """Concatenate experiences from multiple sources, by agent ID. - - The experience from each source is expected to be already grouped by agent ID. The result is a single dictionary - of experiences with keys being agent IDs and values being the concatenation of experiences from all sources - for each agent ID. - - Args: - exp: Experiences from one or more sources. - is_single_source (bool): If True, experiences are from a single (actor) source. Defaults to False. - is_single_agent (bool): If True, experiences are from a single agent. Defaults to False. - - Returns: - Concatenated experiences for each agent. - """ - if is_single_source: - return exp - - merged = defaultdict(list) if is_single_agent else defaultdict(lambda: defaultdict(list)) - for ex in exp.values(): - if is_single_agent: - for k, v in ex.items(): - merged[k].extend[v] - else: - for agent_id, e in ex.items(): - for k, v in e.items(): - merged[agent_id][k].extend(v) - - return merged - - @staticmethod - def stack(exp, is_single_source: bool = False, is_single_agent: bool = False) -> dict: - """Collect each agent's trajectories from multiple sources. - - Args: - exp: Experiences from one or more sources. - is_single_source (bool): If True, experiences are from a single (actor) source. Defaults to False. - is_single_agent (bool): If True, the experiences are from a single agent. Defaults to False. - - Returns: - A list of trajectories for each agent. - """ - if is_single_source: - return [exp] if is_single_agent else {agent_id: [ex] for agent_id, ex in exp.items()} - - if is_single_agent: - return list(exp.values()) - - ret = defaultdict(list) - for ex in exp.values(): - for agent_id, e in ex.items(): - ret[agent_id].append(e) - - return ret diff --git a/maro/rl/utils/trajectory_utils.py b/maro/rl/utils/trajectory_utils.py index 3fe348adb..da81d9186 100644 --- a/maro/rl/utils/trajectory_utils.py +++ b/maro/rl/utils/trajectory_utils.py @@ -8,6 +8,53 @@ import torch import torch.nn.functional as F +MAX_LOSS = 1e8 + +def get_sars(states: list, actions: list, rewards: list, multi_agent: bool = True) -> dict: + """Extract experiences from a trajectory. + + Args: + states (list): List of states traversed during a roll-out episode (in order). + actions (list): List of actions taken during a roll-out episode (in order). + rewards (list): List of rewards obtained during a roll-out episode (in order). + + Returns: + Experiences for training, grouped by agent ID. + """ + if multi_agent: + sars = {} + for state, action, reward in zip(states, actions, rewards): + for agent_id in state: + exp = sars.setdefault(agent_id, {"S": [], "A": [], "R": [], "S_": [], "loss": []}) + exp["S"].append(state[agent_id]) + exp["A"].append(action[agent_id]) + exp["R"].append(reward) + exp["loss"].append(MAX_LOSS) + + for exp in sars.values(): + exp["S_"] = exp["S"][1:] + exp["S"].pop() + exp["A"].pop() + exp["R"].pop() + exp["loss"].pop() + + return sars + else: + sars = {"S": [], "A": [], "R": [], "S_": [], "loss": []} + for state, action, reward in zip(states, actions, rewards): + sars["S"].append(state) + sars["A"].append(action) + sars["R"].append(reward) + sars["loss"].append(MAX_LOSS) + + sars["S_"] = exp["S"][1:] + sars["S"].pop() + sars["A"].pop() + sars["R"].pop() + sars["loss"].pop() + + return sars + def get_truncated_cumulative_reward( rewards: Union[list, np.ndarray, torch.Tensor], diff --git a/notebooks/articles/simple_bike_repositioning.ipynb b/notebooks/articles/simple_bike_repositioning.ipynb index 94fa47fa0..74eae4066 100644 --- a/notebooks/articles/simple_bike_repositioning.ipynb +++ b/notebooks/articles/simple_bike_repositioning.ipynb @@ -728,7 +728,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.9" } }, "nbformat": 4, From 42fa638b3d3c671c9d19e356760a825959080f14 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 19 Mar 2021 16:20:08 +0000 Subject: [PATCH 089/482] restored dispatcher and trainer --- maro/rl/training/dispatcher.py | 58 ++++++++++++++++++++++++++++++++++ maro/rl/training/trainer.py | 30 ++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100755 maro/rl/training/dispatcher.py create mode 100755 maro/rl/training/trainer.py diff --git a/maro/rl/training/dispatcher.py b/maro/rl/training/dispatcher.py new file mode 100755 index 000000000..9ce7372e8 --- /dev/null +++ b/maro/rl/training/dispatcher.py @@ -0,0 +1,58 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import deque + +import zmq +from zmq.eventloop.ioloop import IOLoop +from zmq.eventloop.zmqstream import ZMQStream + + +class LRUQueue(object): + """LRUQueue class using ZMQStream/IOLoop for event dispatching. + + Code adapted from https://zguide.zeromq.org/docs/chapter3/#A-High-Level-API-for-ZeroMQ. + """ + def __init__(self): + self._context = zmq.Context.instance() + frontend = self._context.socket(zmq.ROUTER) + frontend.bind("tcp://127.0.0.1:50000") + backend = self._context.socket(zmq.ROUTER) + backend.bind("tcp://127.0.0.1:50001") + self._workers = deque() + + self._frontend = ZMQStream(frontend) + self._backend = ZMQStream(backend) + self._backend.on_recv(self._handle_backend) + + def _handle_backend(self, msg): + # Queue worker ID for LRU routing + worker, empty, client = msg[:3] + # add worker back to the list of workers + self._workers.append(worker) + assert empty == b"" + # Third frame is READY or else a client reply address + # If client reply, send rest back to frontend + if client != b"READY": + empty, reply = msg[3:] + assert empty == b"" + self._frontend.send_multipart([client, b'', reply]) + + # Start accepting frontend messages now that at least one worker is free. + self._frontend.on_recv(self._handle_frontend) + + def _handle_frontend(self, msg): + # Now get next client request, route to LRU worker + # Client request is [address][empty][request] + client, empty, request = msg + assert empty == b"" + # Dequeue and drop the next worker address + self._backend.send_multipart([self._workers.popleft(), b'', client, b'', request]) + if not self._workers: + # stop receiving until workers become available again + self._frontend.stop_on_recv() + + +def start_dispatcher(): + dispatcher = LRUQueue() + IOLoop.instance().start() diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py new file mode 100755 index 000000000..c66d20757 --- /dev/null +++ b/maro/rl/training/trainer.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import pickle +from collections import deque + +import zmq +from zmq.eventloop.ioloop import IOLoop +from zmq.eventloop.zmqstream import ZMQStream + + +def trainer(id_: str): + socket = zmq.Context().socket(zmq.REQ) + socket.setsockopt_string(zmq.IDENTITY, f"Trainer_{id_}") + socket.connect("tcp://127.0.0.1:50001") + socket = ZMQStream(socket) + socket.send(b"READY") # register to the dispatcher + + def train(sock, msg): + client, _, request = msg + request = pickle.loads(request) + info = request["agent"].learn(*request["args"], **request["kwargs"]) + request.update({"model": request["agent"].dump_model(), "info": info}) + del request["agent"] + del request["args"] + del request["kwargs"] + sock.send_multipart([client, b"", pickle.dumps(request)]) + + socket.on_recv_stream(train) + IOLoop.instance().start() From 83ee19533da568db500fbeaf4afe11ee176dfb9d Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 20 Mar 2021 16:53:56 +0800 Subject: [PATCH 090/482] vehicle test --- .../supply_chain/facilities/facility.py | 3 + .../supply_chain/units/distribution.py | 8 + .../scenarios/supply_chain/units/unitbase.py | 3 +- .../scenarios/supply_chain/units/vehicle.py | 62 ++-- .../simulator/scenarios/supply_chain/world.py | 8 +- tests/data/supply_chain/case_01/readme.md | 6 +- tests/supply_chain/test_supply_chain.py | 339 +++++++++++++++++- 7 files changed, 390 insertions(+), 39 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index d1942dcbd..698cc451d 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -99,6 +99,9 @@ def reset(self): if self.storage is not None: self.storage.reset() + if self.distribution is not None: + self.distribution.reset() + if self.products is not None: for product in self.products.values(): product.reset() diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 055efec03..981ecc356 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -103,12 +103,20 @@ def step(self, tick: int): self.data_model.delay_order_penalty[product_index] += sku.delay_order_penalty + def flush_states(self): + for vehicle in self.vehicles: + vehicle.flush_states() + def reset(self): super(DistributionUnit, self).reset() self.order_queue.clear() self._init_data_model() + # Reset vehicles. + for vehicle in self.vehicles: + vehicle.reset() + def _init_data_model(self): for product_id in self.product_list: self.data_model.product_list.append(product_id) diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py index c969d8900..7e94cfa9a 100644 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -105,5 +105,6 @@ def get_unit_info(self) -> dict: "id": self.id, "node_name": type(self.data_model).__node_name__, "node_index": self.data_model_index, - "class": type(self) + "class": type(self), + "children": None if self.children is None else [c.get_unit_info() for c in self.children] } diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index b3472ff89..d7ef50109 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -34,11 +34,10 @@ def __init__(self): # Current location in the path. self.location = 0 - # Id of destination. - self.destination_id = 0 - # Velocity. self.velocity = 0 + self.quantity = 0 + self.patient = 0 def schedule(self, destination: object, product_id: int, quantity: int, vlt: int): """Schedule a job for this vehicle. @@ -50,6 +49,7 @@ def schedule(self, destination: object, product_id: int, quantity: int, vlt: int vlt (int): Velocity of vehicle. """ # Keep these in states, we will not update it until we reach the destination or cancelled. + self.data_model.source = self.facility.id self.data_model.destination = destination.id self.data_model.product_id = product_id self.data_model.requested_quantity = quantity @@ -58,9 +58,7 @@ def schedule(self, destination: object, product_id: int, quantity: int, vlt: int # Cache. self.product_id = product_id self.destination = destination - - # Keep the patient, reset it after product unloaded. - self.max_patient = self.data_model.patient + self.quantity = quantity # Find the path from current entity to target. self.path = self.world.find_path( @@ -75,12 +73,15 @@ def schedule(self, destination: object, product_id: int, quantity: int, vlt: int # Steps to destination. self.steps = len(self.path) // vlt + self.data_model.steps = self.steps # We are waiting for product loading. self.location = 0 self.velocity = vlt + self.patient = self.max_patient + def try_load(self, quantity: int) -> bool: """Try to load specified number of scheduled product. @@ -95,8 +96,6 @@ def try_load(self, quantity: int) -> bool: return True else: - self.data_model.patient -= 1 - return False def try_unload(self): @@ -117,11 +116,7 @@ def try_unload(self): self.payload ) - # Reset the state. - self.data_model.payload = 0 self.payload = 0 - self.data_model.patient = self.max_patient - self.data_model.position[:] = -1 def initialize(self): super(VehicleUnit, self).initialize() @@ -132,6 +127,7 @@ def initialize(self): self.data_model.initialize(patient=patient, unit_transport_cost=unit_transport_cost) self.data_model.position[:] = -1 + self.max_patient = patient def step(self, tick: int): # If we have not arrive at destination yet. @@ -139,28 +135,22 @@ def step(self, tick: int): # if we still not loaded enough productions yet. if self.location == 0 and self.payload == 0: # then try to load by requested. - request_quantity = self.data_model.requested_quantity - if self.try_load(request_quantity): + if self.try_load(self.quantity): # NOTE: here we return to simulate loading return else: - self.data_model.patient -= 1 + self.patient -= 1 # Failed to load, check the patient. - if self.data_model.patient < 0: + if self.patient < 0: self.destination.products[self.product_id].consumer.update_open_orders( self.facility.id, self.product_id, - -request_quantity + -self.quantity ) - # reset - self.steps = 0 - self.location = 0 - self.destination = None - self.destination_id = 0 - + self._reset_internal_states() self._reset_data_model() # Moving to destination @@ -169,8 +159,9 @@ def step(self, tick: int): self.location += self.velocity self.steps -= 1 + self.data_model.steps = self.steps - if self.location > len(self.path): + if self.location >= len(self.path): self.location = len(self.path) - 1 self.data_model.position[:] = self.path[self.location] @@ -183,36 +174,37 @@ def step(self, tick: int): # back to source if we unload all if self.payload == 0: - self.destination = None - self.destination_id = 0 - self.steps = 0 - self.location = 0 - self.destination = 0 - - # Reset data model + self._reset_internal_states() self._reset_data_model() def flush_states(self): if self.payload > 0: self.data_model.payload = self.payload - if self.steps > 0: - self.data_model.steps = self.steps + # Flush if we have an order. + if self.quantity > 0: + self.data_model.patient = self.patient def reset(self): super(VehicleUnit, self).reset() + self._reset_internal_states() + self._reset_data_model() + + def _reset_internal_states(self): self.destination = None - self.destination_id = 0 - self.max_patient = None self.path = None self.payload = 0 self.product_id = 0 self.steps = 0 self.location = 0 + self.quantity = 0 + self.velocity = 0 + self.patient = self.max_patient def _reset_data_model(self): # Reset data model. + self.data_model.source = 0 self.data_model.steps = 0 self.data_model.destination = 0 self.data_model.product_id = 0 diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index b8ee457e6..6e338ed14 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -295,10 +295,15 @@ def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBas children_conf = config.get("children", None) if children_conf: + unit_instance.children = [] + for child_name, child_conf in children_conf.items(): # If child configuration is a dict, then we add it as a property by name (key). if type(child_conf) == dict: - setattr(unit_instance, child_name, self.build_unit(facility, unit_instance, child_conf)) + child_instance = self.build_unit(facility, unit_instance, child_conf) + + setattr(unit_instance, child_name, child_instance) + unit_instance.children.append(child_instance) elif type(child_conf) == list: # If child configuration is a list, then will treat it as list property, named same as key. child_list = [] @@ -306,6 +311,7 @@ def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBas child_list.append(self.build_unit(facility, unit_instance, conf)) setattr(unit_instance, child_name, child_list) + unit_instance.children.extend(child_list) return unit_instance else: diff --git a/tests/data/supply_chain/case_01/readme.md b/tests/data/supply_chain/case_01/readme.md index a16ffeea1..9239123f7 100644 --- a/tests/data/supply_chain/case_01/readme.md +++ b/tests/data/supply_chain/case_01/readme.md @@ -1 +1,5 @@ -This case used to test if manufacture unit work as expected. \ No newline at end of file +This case used to test following units separately. + +. manufacture +. storage +. consumer diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 7813f7d5c..b2188f962 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -5,7 +5,7 @@ from maro.simulator import Env from maro.simulator.scenarios.supply_chain import ManufactureAction, ConsumerAction -from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase +from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase, VehicleUnit def build_env(case_name: str, durations: int): @@ -869,6 +869,343 @@ def test_consumer_on_order_reception(self): # NOTE: we cannot test the received state by calling on_order_reception directly, # as it will be cleared by env.step, do it on vehicle unit test. + """ + Vehicle unit test: + + . initial state + . if vehicle arrive at destination within special vlt + . schedule job + . try_load until patient <= 0 to cancel the schedule + . try_load until patient > 0 to load order + . try_unload + . target storage cannot take all + . target storage can take all + """ + + def test_vehicle_unit_state(self): + env = build_env("case_02", 100) + + # try to find first vehicle unit we meet + vehicle_unit: VehicleUnit + vehicle_unit_id: int + vehicle_unit_data_model_index: int + + for id, info in env.summary["node_mapping"]["entity_mapping"].items(): + if info[0] == "vehicle": + vehicle_unit_id = id + vehicle_unit = env._business_engine.world.get_entity(id) + vehicle_unit_data_model_index = vehicle_unit.data_model_index + + break + + # check initial state according to configuration file + self.assertEqual(10, vehicle_unit.max_patient) + self.assertEqual(10, vehicle_unit.data_model.patient) + + self.assertEqual(0, vehicle_unit.quantity) + # not destination at first + self.assertIsNone(vehicle_unit.destination) + # no path + self.assertIsNone(vehicle_unit.path) + # no product + self.assertEqual(0, vehicle_unit.product_id) + # no steps + self.assertEqual(0, vehicle_unit.steps) + # + self.assertEqual(0, vehicle_unit.payload) + # + self.assertIsNone(vehicle_unit.product) + # + self.assertEqual(0, vehicle_unit.location) + # + self.assertEqual(0, vehicle_unit.velocity) + + # state in frame + self.assertEqual(0, vehicle_unit.data_model.source) + self.assertEqual(0, vehicle_unit.data_model.destination) + self.assertEqual(0, vehicle_unit.data_model.payload) + self.assertEqual(0, vehicle_unit.data_model.product_id) + self.assertEqual(0, vehicle_unit.data_model.requested_quantity) + self.assertEqual(0, vehicle_unit.data_model.steps) + self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) + self.assertListEqual([-1, -1], vehicle_unit.data_model.position[:]) + + # reset to check again + env.step(None) + env.reset() + + # check initial state according to configuration file + self.assertEqual(10, vehicle_unit.max_patient) + self.assertEqual(10, vehicle_unit.data_model.patient) + + # not destination at first + self.assertIsNone(vehicle_unit.destination) + # no path + self.assertIsNone(vehicle_unit.path) + # no product + self.assertEqual(0, vehicle_unit.product_id) + # no steps + self.assertEqual(0, vehicle_unit.steps) + # + self.assertEqual(0, vehicle_unit.payload) + # + self.assertIsNone(vehicle_unit.product) + # + self.assertEqual(0, vehicle_unit.location) + # + self.assertEqual(0, vehicle_unit.velocity) + # + self.assertEqual(0, vehicle_unit.quantity) + + # state in frame + self.assertEqual(0, vehicle_unit.data_model.source) + self.assertEqual(0, vehicle_unit.data_model.destination) + self.assertEqual(0, vehicle_unit.data_model.payload) + self.assertEqual(0, vehicle_unit.data_model.product_id) + self.assertEqual(0, vehicle_unit.data_model.requested_quantity) + self.assertEqual(0, vehicle_unit.data_model.steps) + self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) + self.assertListEqual([-1, -1], vehicle_unit.data_model.position[:]) + + def test_vehicle_unit_schedule(self): + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + vehicle_unit: VehicleUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + for v in info["units"]["distribution"]["children"]: + vehicle_unit = env._business_engine.world.get_entity(v["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # make sure the upstream in the only one supplier in config + self.assertEqual(1, len(dest_facility.upstreams)) + self.assertEqual(1, len(dest_facility.upstreams[SKU3_ID])) + + # schedule job vehicle unit manually, from supplier to warehouse + vehicle_unit.schedule(dest_facility, SKU3_ID, 20, 2) + + # step to take snapshot + env.step(None) + + vehicle_nodes = env.snapshot_list["vehicle"] + + # check internal states + self.assertEqual(dest_facility, vehicle_unit.destination) + self.assertEqual(SKU3_ID, vehicle_unit.product_id) + self.assertEqual(20, vehicle_unit.quantity) + self.assertEqual(2, vehicle_unit.velocity) + # 6/2 + self.assertEqual(3, vehicle_unit.steps) + + features = ( + "id", + "facility_id", + "source", + "destination", + "payload", + "product_id", + "requested_quantity", + "steps", + "unit_transport_cost" + ) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # source id + self.assertEqual(vehicle_unit.facility.id, states[2]) + # destination + self.assertEqual(dest_facility.id, states[3]) + # payload should be 20, as we already env.step + self.assertEqual(20, states[4]) + # product id + self.assertEqual(SKU3_ID, states[5]) + # quantity + self.assertEqual(20, states[6]) + # steps + self.assertEqual(3, states[7]) + + # push the vehicle on the way + env.step(None) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # source id + self.assertEqual(vehicle_unit.facility.id, states[2]) + # destination + self.assertEqual(dest_facility.id, states[3]) + # payload + self.assertEqual(20, states[4]) + # product id + self.assertEqual(SKU3_ID, states[5]) + # quantity + self.assertEqual(20, states[6]) + # steps, one step forward (vlt=2) + self.assertEqual(2, states[7]) + + env.step(None) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # steps, one step forward (vlt=2) + self.assertEqual(1, states[7]) + + env.step(None) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # steps, one step forward (vlt=2) + self.assertEqual(0, states[7]) + + pos = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:"position"].flatten().astype(np.int) + + # the position should be (0, 0) (warehouse) + self.assertListEqual([0, 0], list(pos)) + + # next step vehicle will try to unload the products + env.step(None) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # the product is unloaded, vehicle states will be reset to initial + # not destination at first + self.assertIsNone(vehicle_unit.destination) + self.assertIsNone(vehicle_unit.path) + self.assertEqual(0, vehicle_unit.product_id) + self.assertEqual(0, vehicle_unit.steps) + self.assertEqual(0, vehicle_unit.payload) + self.assertIsNone(vehicle_unit.product) + self.assertEqual(0, vehicle_unit.location) + self.assertEqual(0, vehicle_unit.velocity) + self.assertEqual(0, vehicle_unit.quantity) + + # check states + + self.assertEqual(0, states[2]) + self.assertEqual(0, states[3]) + self.assertEqual(0, states[4]) + self.assertEqual(0, states[5]) + self.assertEqual(0, states[6]) + self.assertEqual(0, states[7]) + self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) + + pos = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:"position"].flatten().astype(np.int) + self.assertListEqual([-1, -1], list(pos)) + + def test_vehicle_unit_no_patient(self): + """ + NOTE: with patient is tried in above case after schedule the job + """ + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + vehicle_unit: VehicleUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + for v in info["units"]["distribution"]["children"]: + vehicle_unit = env._business_engine.world.get_entity(v["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # there is 80 sku3 in supplier, lets schedule a job for 100, to make sure it will fail to try load + vehicle_unit.schedule(dest_facility, SKU3_ID, 100, 3) + + # push env to next step + env.step(None) + + self.assertEqual(100, vehicle_unit.quantity) + + # the patient will -1 as no enough product so load + self.assertEqual(10 - 1, vehicle_unit.patient) + self.assertEqual(10 - 1, vehicle_unit.data_model.patient) + + # no payload + self.assertEqual(0, vehicle_unit.payload) + self.assertEqual(0, vehicle_unit.data_model.payload) + + # step 9 ticks, patient will be 0 + for i in range(10 - 1): + env.step(None) + + self.assertEqual(10 - 1 - (i+1), vehicle_unit.patient) + self.assertEqual(10 - 1 - (i+1), vehicle_unit.data_model.patient) + + vehicle_nodes = env.snapshot_list["vehicle"] + features = ( + "id", + "facility_id", + "source", + "destination", + "payload", + "product_id", + "requested_quantity", + "steps", + "unit_transport_cost" + ) + + states = vehicle_nodes[:vehicle_unit.data_model_index:"patient"].flatten().astype(np.int) + + # check the patient history + self.assertEqual(10, len(states)) + self.assertListEqual([9,8,7,6,5,4,3,2,1,0], list(states)) + + states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) + + # no payload from start to now + self.assertListEqual([0]*10, list(states)) + + # push env to next step, vehicle will be reset to initial state + env.step(None) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # the product is unloaded, vehicle states will be reset to initial + # not destination at first + self.assertIsNone(vehicle_unit.destination) + self.assertIsNone(vehicle_unit.path) + self.assertEqual(0, vehicle_unit.product_id) + self.assertEqual(0, vehicle_unit.steps) + self.assertEqual(0, vehicle_unit.payload) + self.assertIsNone(vehicle_unit.product) + self.assertEqual(0, vehicle_unit.location) + self.assertEqual(0, vehicle_unit.velocity) + self.assertEqual(0, vehicle_unit.quantity) + + # check states + + self.assertEqual(0, states[2]) + self.assertEqual(0, states[3]) + self.assertEqual(0, states[4]) + self.assertEqual(0, states[5]) + self.assertEqual(0, states[6]) + self.assertEqual(0, states[7]) + self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) + + pos = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:"position"].flatten().astype(np.int) + self.assertListEqual([-1, -1], list(pos)) + + + """ + Distribution unit test: + + . initial state + . dispatch orders without available vehicle + . dispatch order with vehicle + """ + + """ + Seller unit test: + . with a customized seller unit + . with built in one + """ + if __name__ == '__main__': unittest.main() From 5df9b8d608672f2e251fc533ac0899ef4d538460 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 20 Mar 2021 16:54:34 +0800 Subject: [PATCH 091/482] add vehicle test data --- tests/data/supply_chain/case_02/config.yml | 130 +++++++++++++++++++++ tests/data/supply_chain/case_02/readme.md | 4 + tests/data/supply_chain/case_03/config.yml | 0 tests/data/supply_chain/case_03/readme.md | 1 + 4 files changed, 135 insertions(+) create mode 100644 tests/data/supply_chain/case_02/config.yml create mode 100644 tests/data/supply_chain/case_02/readme.md create mode 100644 tests/data/supply_chain/case_03/config.yml create mode 100644 tests/data/supply_chain/case_03/readme.md diff --git a/tests/data/supply_chain/case_02/config.yml b/tests/data/supply_chain/case_02/config.yml new file mode 100644 index 000000000..bdd8033e2 --- /dev/null +++ b/tests/data/supply_chain/case_02/config.yml @@ -0,0 +1,130 @@ + +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 10 + unit_transport_cost: 12 + +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + vehicles: + - *normal_vehicle + - *normal_vehicle + config: + unit_price: 1 + +facility_definitions: + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + manufacture: + class: "ManufactureUnit" + distribution: + class: "DistributionUnit" + + WarehouseFacility: &warehouse_facility + class: "WarehouseFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + +small_storage: &small_storage + config: + capacity: 100 + unit_storage_cost: 1 + +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 1 + bom: + sku3: 2 + + - id: 2 + name: "sku2" + output_units_per_lot: 2 + bom: + sku1: 4 + + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + - id: 4 + name: "sku4" + output_units_per_lot: 2 + bom: + sku2: 1 + +world: + skus: *sku_definitions + facilities: + - name: "Supplier_SKU3" + definition_ref: "SupplierFacility" + skus: + sku3: + init_in_stock: 80 + delay_order_penalty: 10 + product_unit_cost: 1 + type: "production" + cost": 10 + price": 10 + order_cost: 0 + children: + storage: *small_storage + distribution: *normal_distribution + - name: "Warehouse_001" + definition_ref: "WarehouseFacility" + skus: + sku1: + init_stock: 10 + delay_order_penalty: 10 + price: 100 + order_cost: 200 + sku2: + init_stock: 10 + delay_order_penalty: 10 + price: 100 + order_cost: 200 + sku3: + init_stock: 10 + delay_order_penalty: 10 + price: 100 + order_cost: 200 + children: + storage: *small_storage + distribution: *normal_distribution + + topology: + Warehouse_001: + sku3: + - "Supplier_SKU3" + grid: + size: [ 20, 20 ] + facilities: + Warehouse_001: [ 0, 0 ] + Supplier_SKU3: [ 3, 3 ] + blocks: + railroad: + - [ 10, 10 ] + - [ 10, 11 ] + - [ 10, 12 ] + - [ 11, 12 ] + diff --git a/tests/data/supply_chain/case_02/readme.md b/tests/data/supply_chain/case_02/readme.md new file mode 100644 index 000000000..d3b3a4643 --- /dev/null +++ b/tests/data/supply_chain/case_02/readme.md @@ -0,0 +1,4 @@ +This case used to test following units: + +. vehicle +. distribution diff --git a/tests/data/supply_chain/case_03/config.yml b/tests/data/supply_chain/case_03/config.yml new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/supply_chain/case_03/readme.md b/tests/data/supply_chain/case_03/readme.md new file mode 100644 index 000000000..b8d483604 --- /dev/null +++ b/tests/data/supply_chain/case_03/readme.md @@ -0,0 +1 @@ +This case used to test seller and customized seller unit. \ No newline at end of file From c40c1a4da51d0569a1dda85ab63b93fb915a49fb Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 20 Mar 2021 17:09:37 +0800 Subject: [PATCH 092/482] one more test case for vehicle --- .../scenarios/supply_chain/units/vehicle.py | 2 +- tests/supply_chain/test_supply_chain.py | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index d7ef50109..3208efbce 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -116,7 +116,7 @@ def try_unload(self): self.payload ) - self.payload = 0 + self.payload -= unloaded_units def initialize(self): super(VehicleUnit, self).initialize() diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index b2188f962..b3379a05b 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -1191,6 +1191,63 @@ def test_vehicle_unit_no_patient(self): pos = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:"position"].flatten().astype(np.int) self.assertListEqual([-1, -1], list(pos)) + def test_vehicle_unit_cannot_unload_at_destination(self): + """ + NOTE: If vehicle cannot unload at destination, it will keep waiting, until success to unload. + + """ + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + vehicle_unit: VehicleUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + for v in info["units"]["distribution"]["children"]: + vehicle_unit = env._business_engine.world.get_entity(v["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # move all 80 sku3 to destination, will cause vehicle keep waiting there + vehicle_unit.schedule(dest_facility, SKU3_ID, 80, 2) + + # step to the end. + is_done = False + + while not is_done: + _, _, is_done = env.step(None) + + vehicle_nodes = env.snapshot_list["vehicle"] + features = ( + "id", + "facility_id", + "source", + "destination", + "payload", + "product_id", + "requested_quantity", + "steps", + "unit_transport_cost" + ) + + # payload should be 80 for first 4 ticks, as it is on the way + # then it will unload 100 - 10 - 10 - 10 = 70 products, as this is the remaining space of destination storage + # so then it will keep waiting to unload remaining 10 + payload_states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) + self.assertListEqual([80]*4 + [10] * 96, list(payload_states)) + + # other states should not be reset as it not finish it task + quantity_states = vehicle_nodes[:vehicle_unit.data_model_index:"requested_quantity"].flatten().astype(np.int) + self.assertListEqual([80]*100, list(quantity_states)) + + # same situation as payload + steps_states = vehicle_nodes[:vehicle_unit.data_model_index:"steps"].flatten().astype(np.int) + self.assertListEqual([3, 2, 1] + [0] * 97, list(steps_states)) + + destination_states = vehicle_nodes[:vehicle_unit.data_model_index:"destination"].flatten().astype(np.int) + self.assertListEqual([dest_facility.id] * 100, list(destination_states)) """ Distribution unit test: @@ -1200,6 +1257,15 @@ def test_vehicle_unit_no_patient(self): . dispatch order with vehicle """ + def test_distribution_unit_initial_state(self): + pass + + def test_distribution_unit_dispatch_order(self): + pass + + def test_distribution_unit_dispatch_order_no_vehicle(self): + pass + """ Seller unit test: . with a customized seller unit From 2f06b52fc5e7099035b5768db1c10bad7985b9ed Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 20 Mar 2021 17:33:25 +0800 Subject: [PATCH 093/482] test case for distribution unit --- .../supply_chain/units/distribution.py | 4 +- tests/data/supply_chain/case_02/config.yml | 2 +- tests/supply_chain/test_supply_chain.py | 110 +++++++++++++++++- 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 981ecc356..488ce50a5 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -55,7 +55,7 @@ def place_order(self, order: Order): # TODO: states related, enable it later if needed. # product_index = self.product_index_mapping[order.product_id] - # self.data.check_in_price[product_index] += order_total_price + # self.data_model.check_in_price[product_index] += order_total_price return order_total_price @@ -80,7 +80,7 @@ def initialize(self): def step(self, tick: int): for vehicle in self.vehicles: # If we have vehicle not on the way and there is any pending order - if len(self.order_queue) > 0 and vehicle.location == 0: + if len(self.order_queue) > 0 and vehicle.quantity == 0: order = self.order_queue.popleft() # Schedule a job for available vehicle. diff --git a/tests/data/supply_chain/case_02/config.yml b/tests/data/supply_chain/case_02/config.yml index bdd8033e2..52ec2f70f 100644 --- a/tests/data/supply_chain/case_02/config.yml +++ b/tests/data/supply_chain/case_02/config.yml @@ -81,7 +81,7 @@ world: skus: sku3: init_in_stock: 80 - delay_order_penalty: 10 + delay_order_penalty: 20 product_unit_cost: 1 type: "production" cost": 10 diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index b3379a05b..29db76fc9 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -5,7 +5,8 @@ from maro.simulator import Env from maro.simulator.scenarios.supply_chain import ManufactureAction, ConsumerAction -from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase, VehicleUnit +from maro.simulator.scenarios.supply_chain.units.order import Order +from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase, VehicleUnit, DistributionUnit def build_env(case_name: str, durations: int): @@ -1253,18 +1254,117 @@ def test_vehicle_unit_cannot_unload_at_destination(self): Distribution unit test: . initial state + . place order . dispatch orders without available vehicle . dispatch order with vehicle """ def test_distribution_unit_initial_state(self): - pass + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + dist_unit: DistributionUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + self.assertEqual(0, len(dist_unit.order_queue)) + self.assertEqual(1, len(dist_unit.product_index_mapping)) + self.assertDictEqual({3:0}, dist_unit.product_index_mapping) + self.assertEqual(1, len(dist_unit.product_list)) + self.assertListEqual([3], dist_unit.product_list) + + # from configuration + self.assertEqual(1, dist_unit.data_model.unit_price) + self.assertListEqual([3], list(dist_unit.data_model.product_list[:])) + self.assertListEqual([0], list(dist_unit.data_model.delay_order_penalty)) + + # reset + env.reset() + + self.assertEqual(0, len(dist_unit.order_queue)) + self.assertEqual(1, len(dist_unit.product_index_mapping)) + self.assertDictEqual({3:0}, dist_unit.product_index_mapping) + self.assertEqual(1, len(dist_unit.product_list)) + self.assertListEqual([3], dist_unit.product_list) + + # from configuration + self.assertEqual(1, dist_unit.data_model.unit_price) + self.assertListEqual([3], list(dist_unit.data_model.product_list[:])) + self.assertListEqual([0], list(dist_unit.data_model.delay_order_penalty)) def test_distribution_unit_dispatch_order(self): - pass + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + dist_unit: DistributionUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + first_vehicle: VehicleUnit = dist_unit.vehicles[0] + + order = Order(dest_facility, SKU3_ID, 10, 2) + + dist_unit.place_order(order) + + # check if order is saved + self.assertEqual(1, len(dist_unit.order_queue)) + + # check get pending order correct + pending_order = dist_unit.get_pending_order() + + self.assertDictEqual({3:10}, pending_order) + + # same as vehicle schedule case, distribution will try to schedule this order to vehicles from beginning to end + # so it will dispatch this order to first vehicle + env.step(None) + + self.assertEqual(dest_facility, first_vehicle.destination) + self.assertEqual(10, first_vehicle.quantity) + self.assertEqual(2, first_vehicle.velocity) + self.assertEqual(SKU3_ID, first_vehicle.product_id) + + # since we already test vehicle unit, do not check the it again here + + # add another order to check pending order + dist_unit.place_order(order) + + pending_order = dist_unit.get_pending_order() + + self.assertDictEqual({3:10}, pending_order) + + # another order, will cause the pending order increase + dist_unit.place_order(order) + + pending_order = dist_unit.get_pending_order() + + # 2 pending orders + self.assertDictEqual({3:20}, pending_order) + + # now we have only one available vehicle, 2 pending order + # next step will cause delay_order_penalty + env.step(None) + + second_vehicle = dist_unit.vehicles[1] + + self.assertEqual(dest_facility, second_vehicle.destination) + self.assertEqual(10, second_vehicle.quantity) + self.assertEqual(2, second_vehicle.velocity) + self.assertEqual(SKU3_ID, second_vehicle.product_id) - def test_distribution_unit_dispatch_order_no_vehicle(self): - pass + # from configuration + self.assertEqual(20, dist_unit.data_model.delay_order_penalty[0]) """ Seller unit test: From 300bf34383d7f78df881cd8433b0e260a5b6415e Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 20 Mar 2021 18:03:02 +0800 Subject: [PATCH 094/482] remove sale gamma from data model --- .../scenarios/supply_chain/datamodels/seller.py | 5 +---- .../scenarios/supply_chain/units/seller.py | 14 +++++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index 1411bc08d..5a7576d20 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -32,12 +32,10 @@ def __init__(self): super(SellerDataModel, self).__init__() self._unit_price = 0 - self._sale_gamma = 0 self._backlog_ratio = 0 - def initialize(self, unit_price: int = 0, sale_gamma: int = 0, backlog_ratio: int = 0): + def initialize(self, unit_price: int = 0, backlog_ratio: int = 0): self._unit_price = unit_price - self._sale_gamma = sale_gamma self._backlog_ratio = backlog_ratio self.reset() @@ -46,5 +44,4 @@ def reset(self): super(SellerDataModel, self).reset() self.unit_price = self._unit_price - self.sale_gamma = self._sale_gamma self.backlog_ratio = self._backlog_ratio diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index f797f71bd..c793abeba 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -23,7 +23,6 @@ def __init__(self): self.sold = 0 self.demand = 0 self.total_sold = 0 - self.product_id = 0 def market_demand(self, tick: int) -> int: """Generate market demand for current tick. @@ -34,18 +33,19 @@ def market_demand(self, tick: int) -> int: Returns: int: Demand number. """ - return int(self.demand_distribution[tick]) + return self.demand_distribution[tick] def initialize(self): super(SellerUnit, self).initialize() - unit_price = self.config.get("unit_price", 0) - self.gamma = self.config.get("sale_gamma", 0) - backlog_ratio = self.config.get("backlog_ratio", 1) + sku = self.facility.skus[self.product_id] + + unit_price = sku.price + self.gamma = sku.sale_gamma + backlog_ratio = sku.backlog_ratio self.data_model.initialize( unit_price=unit_price, - sale_gamma=self.gamma, backlog_ratio=backlog_ratio ) @@ -53,7 +53,7 @@ def initialize(self): # Generate demand distribution of this episode. for _ in range(self.durations): - self.demand_distribution.append(np.random.gamma(self.gamma)) + self.demand_distribution.append(int(np.random.gamma(self.gamma))) def step(self, tick: int): demand = self.market_demand(tick) From 9d1bb087fc6aef220fb104aee824b2e1e34f85eb Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 20 Mar 2021 18:03:13 +0800 Subject: [PATCH 095/482] add missing sku info --- .../scenarios/supply_chain/facilities/retailer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index da659a8c7..07af1a242 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -12,7 +12,7 @@ class RetailerFacility(FacilityBase): """Retail facility used to generate order from upstream, and sell products by demand.""" - SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gamma", "order_cost")) + SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gamma", "order_cost", "backlog_ratio")) # Product unit list of this facility. products: List[ProductUnit] @@ -32,8 +32,9 @@ def parse_skus(self, configs: dict): sku_config.get("price", 0), sku_config.get("cost", 0), sku_config["init_in_stock"], - sku_config["sale_gamma"], - sku_config.get("order_cost", 0) + sku_config.get("sale_gamma", 0), + sku_config.get("order_cost", 0), + sku_config.get("backlog_ratio", 0) ) self.skus[sku.id] = sku_info From f6ce5662445cd69489f0398645e66fe86c894356 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 20 Mar 2021 18:22:46 +0800 Subject: [PATCH 096/482] test case for seller unit --- tests/data/supply_chain/case1/readme.md | 5 - tests/data/supply_chain/case2/readme.md | 7 - tests/data/supply_chain/case3/readme.md | 5 - tests/data/supply_chain/case_02/config.yml | 48 +++++ tests/data/supply_chain/case_03/config.yml | 189 ++++++++++++++++++++ tests/data/supply_chain/case_03/readme.md | 2 +- tests/supply_chain/simple_seller.py | 7 + tests/supply_chain/test_supply_chain.py | 195 +++++++++++++++++++-- 8 files changed, 428 insertions(+), 30 deletions(-) delete mode 100644 tests/data/supply_chain/case1/readme.md delete mode 100644 tests/data/supply_chain/case2/readme.md delete mode 100644 tests/data/supply_chain/case3/readme.md create mode 100644 tests/supply_chain/simple_seller.py diff --git a/tests/data/supply_chain/case1/readme.md b/tests/data/supply_chain/case1/readme.md deleted file mode 100644 index 8b0b48e08..000000000 --- a/tests/data/supply_chain/case1/readme.md +++ /dev/null @@ -1,5 +0,0 @@ - -Manufacture products then check: - -. storage state changes after success procuring. -. storage state should not change if failed. \ No newline at end of file diff --git a/tests/data/supply_chain/case2/readme.md b/tests/data/supply_chain/case2/readme.md deleted file mode 100644 index 5bada1b11..000000000 --- a/tests/data/supply_chain/case2/readme.md +++ /dev/null @@ -1,7 +0,0 @@ -Demand generation, then check: - -. order should correct -. order should be scheduled correctly -. check storage states -. check purchase, receive and demand states -. check vehicle states \ No newline at end of file diff --git a/tests/data/supply_chain/case3/readme.md b/tests/data/supply_chain/case3/readme.md deleted file mode 100644 index 5271003a4..000000000 --- a/tests/data/supply_chain/case3/readme.md +++ /dev/null @@ -1,5 +0,0 @@ - -customization check: - -. new seller unit -. new storage unit diff --git a/tests/data/supply_chain/case_02/config.yml b/tests/data/supply_chain/case_02/config.yml index 52ec2f70f..978a48d23 100644 --- a/tests/data/supply_chain/case_02/config.yml +++ b/tests/data/supply_chain/case_02/config.yml @@ -45,6 +45,20 @@ facility_definitions: consumer: class: "ConsumerUnit" + RetailerFacility: &retailer_facility + class: "RetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + seller: + class: "SellerUnit" + small_storage: &small_storage config: capacity: 100 @@ -111,16 +125,50 @@ world: children: storage: *small_storage distribution: *normal_distribution + - name: "Retailer_001" + definition_ref: "RetailerFacility" + skus: + sku1: + price: 300 + cost: 10 + init_in_stock: 10 + sale_gamma: 10 + backlog_ratio: 0.1 + order_cost: 200 + sku3: + price: 200 + cost: 10 + init_in_stock: 10 + sale_gamma: 10 + backlog_ratio: 0.1 + order_cost: 200 + sku2: + price: 100 + cost: 10 + init_in_stock: 10 + sale_gamma: 10 + backlog_ratio: 0.1 + order_cost: 200 + children: + storage: *small_storage topology: Warehouse_001: sku3: - "Supplier_SKU3" + Retailer_001: + sku1: + - "Warehouse_001" + sku2: + - "Warehouse_001" + sku3: + - "Warehouse_001" grid: size: [ 20, 20 ] facilities: Warehouse_001: [ 0, 0 ] Supplier_SKU3: [ 3, 3 ] + Retailer_001: [8, 8] blocks: railroad: - [ 10, 10 ] diff --git a/tests/data/supply_chain/case_03/config.yml b/tests/data/supply_chain/case_03/config.yml index e69de29bb..88796cabe 100644 --- a/tests/data/supply_chain/case_03/config.yml +++ b/tests/data/supply_chain/case_03/config.yml @@ -0,0 +1,189 @@ + +# add customized units +core: + units: + modules: + - path: "tests.supply_chain.simple_seller" + definitions: + # NOTE: do not conflict with built-in ones + SimpleSellerUnit: + class: "SimpleSellerUnit" + datamodel: "SellerDataModel" + + +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 10 + unit_transport_cost: 12 + +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + vehicles: + - *normal_vehicle + - *normal_vehicle + config: + unit_price: 1 + +facility_definitions: + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + manufacture: + class: "ManufactureUnit" + distribution: + class: "DistributionUnit" + + WarehouseFacility: &warehouse_facility + class: "WarehouseFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + + RetailerFacility: &retailer_facility + class: "RetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + seller: + class: "SimpleSellerUnit" + +small_storage: &small_storage + config: + capacity: 100 + unit_storage_cost: 1 + +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 1 + bom: + sku3: 2 + + - id: 2 + name: "sku2" + output_units_per_lot: 2 + bom: + sku1: 4 + + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + - id: 4 + name: "sku4" + output_units_per_lot: 2 + bom: + sku2: 1 + +world: + skus: *sku_definitions + facilities: + - name: "Supplier_SKU3" + definition_ref: "SupplierFacility" + skus: + sku3: + init_in_stock: 80 + delay_order_penalty: 20 + product_unit_cost: 1 + type: "production" + cost": 10 + price": 10 + order_cost: 0 + children: + storage: *small_storage + distribution: *normal_distribution + - name: "Warehouse_001" + definition_ref: "WarehouseFacility" + skus: + sku1: + init_stock: 10 + delay_order_penalty: 10 + price: 100 + order_cost: 200 + sku2: + init_stock: 10 + delay_order_penalty: 10 + price: 100 + order_cost: 200 + sku3: + init_stock: 10 + delay_order_penalty: 10 + price: 100 + order_cost: 200 + children: + storage: *small_storage + distribution: *normal_distribution + - name: "Retailer_001" + definition_ref: "RetailerFacility" + skus: + sku1: + price: 300 + cost: 10 + init_in_stock: 10 + sale_gamma: 10 + backlog_ratio: 0.1 + order_cost: 200 + sku3: + price: 200 + cost: 10 + init_in_stock: 10 + sale_gamma: 10 + backlog_ratio: 0.1 + order_cost: 200 + sku2: + price: 100 + cost: 10 + init_in_stock: 10 + sale_gamma: 10 + backlog_ratio: 0.1 + order_cost: 200 + children: + storage: *small_storage + + topology: + Warehouse_001: + sku3: + - "Supplier_SKU3" + Retailer_001: + sku1: + - "Warehouse_001" + sku2: + - "Warehouse_001" + sku3: + - "Warehouse_001" + grid: + size: [ 20, 20 ] + facilities: + Warehouse_001: [ 0, 0 ] + Supplier_SKU3: [ 3, 3 ] + Retailer_001: [8, 8] + blocks: + railroad: + - [ 10, 10 ] + - [ 10, 11 ] + - [ 10, 12 ] + - [ 11, 12 ] diff --git a/tests/data/supply_chain/case_03/readme.md b/tests/data/supply_chain/case_03/readme.md index b8d483604..a221fe428 100644 --- a/tests/data/supply_chain/case_03/readme.md +++ b/tests/data/supply_chain/case_03/readme.md @@ -1 +1 @@ -This case used to test seller and customized seller unit. \ No newline at end of file +This case used to test customized seller unit. \ No newline at end of file diff --git a/tests/supply_chain/simple_seller.py b/tests/supply_chain/simple_seller.py new file mode 100644 index 000000000..04e3c72e1 --- /dev/null +++ b/tests/supply_chain/simple_seller.py @@ -0,0 +1,7 @@ + +from maro.simulator.scenarios.supply_chain import SellerUnit + + +class SimpleSellerUnit(SellerUnit): + def market_demand(self, tick: int) -> int: + return tick diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 29db76fc9..a129df2d7 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -5,8 +5,9 @@ from maro.simulator import Env from maro.simulator.scenarios.supply_chain import ManufactureAction, ConsumerAction +from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase, VehicleUnit, \ + DistributionUnit, SellerUnit from maro.simulator.scenarios.supply_chain.units.order import Order -from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase, VehicleUnit, DistributionUnit def build_env(case_name: str, durations: int): @@ -1135,8 +1136,8 @@ def test_vehicle_unit_no_patient(self): for i in range(10 - 1): env.step(None) - self.assertEqual(10 - 1 - (i+1), vehicle_unit.patient) - self.assertEqual(10 - 1 - (i+1), vehicle_unit.data_model.patient) + self.assertEqual(10 - 1 - (i + 1), vehicle_unit.patient) + self.assertEqual(10 - 1 - (i + 1), vehicle_unit.data_model.patient) vehicle_nodes = env.snapshot_list["vehicle"] features = ( @@ -1155,12 +1156,12 @@ def test_vehicle_unit_no_patient(self): # check the patient history self.assertEqual(10, len(states)) - self.assertListEqual([9,8,7,6,5,4,3,2,1,0], list(states)) + self.assertListEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], list(states)) states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) # no payload from start to now - self.assertListEqual([0]*10, list(states)) + self.assertListEqual([0] * 10, list(states)) # push env to next step, vehicle will be reset to initial state env.step(None) @@ -1237,11 +1238,11 @@ def test_vehicle_unit_cannot_unload_at_destination(self): # then it will unload 100 - 10 - 10 - 10 = 70 products, as this is the remaining space of destination storage # so then it will keep waiting to unload remaining 10 payload_states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) - self.assertListEqual([80]*4 + [10] * 96, list(payload_states)) + self.assertListEqual([80] * 4 + [10] * 96, list(payload_states)) # other states should not be reset as it not finish it task quantity_states = vehicle_nodes[:vehicle_unit.data_model_index:"requested_quantity"].flatten().astype(np.int) - self.assertListEqual([80]*100, list(quantity_states)) + self.assertListEqual([80] * 100, list(quantity_states)) # same situation as payload steps_states = vehicle_nodes[:vehicle_unit.data_model_index:"steps"].flatten().astype(np.int) @@ -1275,7 +1276,7 @@ def test_distribution_unit_initial_state(self): self.assertEqual(0, len(dist_unit.order_queue)) self.assertEqual(1, len(dist_unit.product_index_mapping)) - self.assertDictEqual({3:0}, dist_unit.product_index_mapping) + self.assertDictEqual({3: 0}, dist_unit.product_index_mapping) self.assertEqual(1, len(dist_unit.product_list)) self.assertListEqual([3], dist_unit.product_list) @@ -1289,7 +1290,7 @@ def test_distribution_unit_initial_state(self): self.assertEqual(0, len(dist_unit.order_queue)) self.assertEqual(1, len(dist_unit.product_index_mapping)) - self.assertDictEqual({3:0}, dist_unit.product_index_mapping) + self.assertDictEqual({3: 0}, dist_unit.product_index_mapping) self.assertEqual(1, len(dist_unit.product_list)) self.assertListEqual([3], dist_unit.product_list) @@ -1324,7 +1325,7 @@ def test_distribution_unit_dispatch_order(self): # check get pending order correct pending_order = dist_unit.get_pending_order() - self.assertDictEqual({3:10}, pending_order) + self.assertDictEqual({3: 10}, pending_order) # same as vehicle schedule case, distribution will try to schedule this order to vehicles from beginning to end # so it will dispatch this order to first vehicle @@ -1342,7 +1343,7 @@ def test_distribution_unit_dispatch_order(self): pending_order = dist_unit.get_pending_order() - self.assertDictEqual({3:10}, pending_order) + self.assertDictEqual({3: 10}, pending_order) # another order, will cause the pending order increase dist_unit.place_order(order) @@ -1350,7 +1351,7 @@ def test_distribution_unit_dispatch_order(self): pending_order = dist_unit.get_pending_order() # 2 pending orders - self.assertDictEqual({3:20}, pending_order) + self.assertDictEqual({3: 20}, pending_order) # now we have only one available vehicle, 2 pending order # next step will cause delay_order_penalty @@ -1368,10 +1369,180 @@ def test_distribution_unit_dispatch_order(self): """ Seller unit test: + . initial state . with a customized seller unit . with built in one """ + def test_seller_unit_initial_states(self): + env = build_env("case_02", 100) + + # find seller for sku3 from retailer facility + sell_unit: SellerUnit + source_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Retailer_001": + for pid, pdetail in info["units"]["products"].items(): + if pdetail["sku_id"] == SKU3_ID: + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + + if info["name"] == "Warehouse_001": + source_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # from configuration + self.assertEqual(10, sell_unit.gamma) + self.assertEqual(100, sell_unit.durations) + self.assertEqual(100, len(sell_unit.demand_distribution)) + self.assertEqual(0, sell_unit.sold) + self.assertEqual(0, sell_unit.demand) + self.assertEqual(0, sell_unit.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + # + self.assertEqual(0, sell_unit.data_model.sold) + self.assertEqual(0, sell_unit.data_model.demand) + self.assertEqual(0, sell_unit.data_model.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + env.reset() + + # from configuration + self.assertEqual(10, sell_unit.gamma) + self.assertEqual(100, sell_unit.durations) + self.assertEqual(100, len(sell_unit.demand_distribution)) + self.assertEqual(0, sell_unit.sold) + self.assertEqual(0, sell_unit.demand) + self.assertEqual(0, sell_unit.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + # + self.assertEqual(0, sell_unit.data_model.sold) + self.assertEqual(0, sell_unit.data_model.demand) + self.assertEqual(0, sell_unit.data_model.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + def test_seller_unit_demand_states(self): + env = build_env("case_02", 100) + + # find seller for sku3 from retailer facility + sell_unit: SellerUnit + source_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Retailer_001": + for pid, pdetail in info["units"]["products"].items(): + if pdetail["sku_id"] == SKU3_ID: + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + + if info["name"] == "Warehouse_001": + source_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + SKU3_INIT_NUMBER = sell_unit.facility.skus[SKU3_ID].init_in_stock + + env.step(None) + + # seller unit will try to count down the product number base on demand + # default seller use gamma distribution on each tick + demand = sell_unit.demand_distribution[0] + + # demand should be same with original + self.assertEqual(demand, sell_unit.demand) + self.assertEqual(demand, sell_unit.data_model.demand) + + actual_sold = min(demand, SKU3_INIT_NUMBER) + # sold may be not same as demand, depend on remaining number in storage + self.assertEqual(actual_sold, sell_unit.sold) + self.assertEqual(actual_sold, sell_unit.data_model.sold) + self.assertEqual(actual_sold, sell_unit.total_sold) + self.assertEqual(actual_sold, sell_unit.data_model.total_sold) + + states = env.snapshot_list["seller"][ + env.frame_index:sell_unit.data_model_index:("sold", "demand", "total_sold")].flatten().astype(np.int) + + self.assertEqual(actual_sold, states[0]) + self.assertEqual(demand, states[1]) + self.assertEqual(actual_sold, states[2]) + + # move to next step to check if state is correct + env.step(None) + + demand = sell_unit.demand_distribution[1] + + # demand should be same with original + self.assertEqual(demand, sell_unit.demand) + self.assertEqual(demand, sell_unit.data_model.demand) + + actual_sold_2 = min(demand, SKU3_INIT_NUMBER - actual_sold) + + # sold may be not same as demand, depend on remaining number in storage + self.assertEqual(actual_sold_2, sell_unit.sold) + self.assertEqual(actual_sold_2, sell_unit.data_model.sold) + self.assertEqual(actual_sold + actual_sold_2, sell_unit.total_sold) + self.assertEqual(actual_sold + actual_sold_2, sell_unit.data_model.total_sold) + + states = env.snapshot_list["seller"][ + env.frame_index:sell_unit.data_model_index:("sold", "demand", "total_sold")].flatten().astype(np.int) + + self.assertEqual(actual_sold_2, states[0]) + self.assertEqual(demand, states[1]) + self.assertEqual(actual_sold + actual_sold_2, states[2]) + + def test_seller_unit_customized(self): + env = build_env("case_03", 100) + + # find seller for sku3 from retailer facility + sell_unit: SellerUnit + source_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Retailer_001": + for pid, pdetail in info["units"]["products"].items(): + if pdetail["sku_id"] == SKU3_ID: + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + + if info["name"] == "Warehouse_001": + source_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # NOTE: + # this simple seller unit return demands that same as current tick + env.step(None) + + # so tick 0 will have demand == 0 + # from configuration + self.assertEqual(0, sell_unit.sold) + self.assertEqual(0, sell_unit.demand) + self.assertEqual(0, sell_unit.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + # + self.assertEqual(0, sell_unit.data_model.sold) + self.assertEqual(0, sell_unit.data_model.demand) + self.assertEqual(0, sell_unit.data_model.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + is_done = False + + while not is_done: + _, _, is_done = env.step(None) + + # check demand history, it should be same as tick + seller_nodes = env.snapshot_list["seller"] + + demand_states = seller_nodes[:sell_unit.data_model_index:"demand"].flatten().astype(np.int) + + self.assertListEqual([i for i in range(100)], list(demand_states)) + + # check sold states + # it should be 0 after tick 4 + sold_states = seller_nodes[:sell_unit.data_model_index:"sold"].flatten().astype(np.int) + self.assertListEqual([0, 1, 2, 3, 4] + [0] * 95, list(sold_states)) + + # total sold + total_sold_states = seller_nodes[:sell_unit.data_model_index:"total_sold"].flatten().astype(np.int) + # total sold will keep same after tick 4 + self.assertListEqual([0, 1, 3, 6, 10] + [10] * 95, list(total_sold_states)) + if __name__ == '__main__': unittest.main() From b58046be63901fa9da4eb54a418ed65a7afd3518 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Sat, 20 Mar 2021 18:28:20 +0800 Subject: [PATCH 097/482] fix failed casees --- tests/test_env.py | 8 ++++---- tests/test_snapshot.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_env.py b/tests/test_env.py index d8c9a5a8d..41c8e3c34 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -130,7 +130,7 @@ def test_env_interfaces_with_specified_business_engine_cls(self): vals_after_reset = dummies_ss[env.frame_index::"val"] if backend_name == "dynamic": - self.assertTrue(np.isnan(vals_after_reset).all()) + self.assertTrue((vals_after_reset == 0).all()) else: self.assertListEqual(list(vals_after_reset.flatten()), [ 0]*dummy_number, msg=f"we should have padding values") @@ -271,8 +271,8 @@ def test_invalid_scenario(self): def test_get_avaiable_envs(self): scenario_names = get_scenarios() - # we have 2 built-in scenarios - self.assertEqual(3, len(scenario_names)) + # we have 4 built-in scenarios + self.assertEqual(4, len(scenario_names)) self.assertTrue("cim" in scenario_names) self.assertTrue("citi_bike" in scenario_names) @@ -283,7 +283,7 @@ def test_get_avaiable_envs(self): env_list = get_available_envs() - self.assertEqual(len(env_list), len(cim_topoloies) + len(citi_bike_topologies) + len(vm_topoloties)) + self.assertEqual(len(env_list), len(cim_topoloies) + len(citi_bike_topologies) + len(vm_topoloties) + len(get_topologies("supply_chain"))) def test_frame_index_to_ticks(self): ticks = frame_index_to_ticks(0, 10, 2) diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index aca9796ba..33fde23f5 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -68,7 +68,7 @@ def test_slice_quering(self): msg="slicing with 1 tick, 1 node and 1 attr, should return array with 1 result") if backend_name == "dynamic": - self.assertTrue(np.isnan(static_node_a2_states).all()) + self.assertTrue((static_node_a2_states == 0).all()) else: self.assertEqual(0, static_node_a2_states.astype( "i")[0], msg="states before taking snapshot should be 0") @@ -159,7 +159,7 @@ def test_slice_quering(self): 3, len(states), msg="states should contains 3 row") if backend_name == "dynamic": - self.assertTrue(np.isnan(states[0]).all()) + self.assertTrue((states[0] == 0).all()) else: self.assertListEqual([0]*len(frame.static_nodes), list(states[0].astype("i")), msg="over-wrote tick should return 0") @@ -206,7 +206,7 @@ def test_quering_with_not_exist_indices(self): # NOTE: raw backend will padding with nan while numpy padding with 0 if backend_name == "dynamic": # all should be nan - self.assertTrue(np.isnan(states).all()) + self.assertTrue((states==0).all()) else: self.assertListEqual(list(states.astype("I")), [ 0]*STATIC_NODE_NUM) @@ -222,7 +222,7 @@ def test_quering_with_not_exist_indices(self): i for i in range(STATIC_NODE_NUM)]) if backend_name == "dynamic": - self.assertTrue(np.isnan(states[1]).all()) + self.assertTrue((states[1] == 0).all()) else: self.assertListEqual(list(states[1].astype("i")), [ 0]*STATIC_NODE_NUM) From 430549de64932403b893a1d96dcd78b3a15bc4cb Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 11:23:53 +0800 Subject: [PATCH 098/482] fix lint issue --- .../scenarios/supply_chain/__init__.py | 9 ++++++--- .../scenarios/supply_chain/actions.py | 1 - .../scenarios/supply_chain/business_engine.py | 2 +- .../scenarios/supply_chain/datamodels/base.py | 4 +--- .../supply_chain/datamodels/consumer.py | 2 +- .../supply_chain/datamodels/distribution.py | 2 +- .../supply_chain/datamodels/facility.py | 2 +- .../supply_chain/datamodels/manufacture.py | 2 +- .../supply_chain/datamodels/seller.py | 2 +- .../supply_chain/datamodels/storage.py | 2 +- .../supply_chain/datamodels/vehicle.py | 2 +- .../supply_chain/facilities/retailer.py | 18 ++++++++++++++++-- .../supply_chain/facilities/supplier.py | 17 +++++++++++++++-- .../supply_chain/facilities/warehouse.py | 5 ++++- .../scenarios/supply_chain/frame_builder.py | 2 +- .../scenarios/supply_chain/units/__init__.py | 1 + .../scenarios/supply_chain/units/consumer.py | 4 ++-- .../supply_chain/units/distribution.py | 2 +- .../supply_chain/units/manufacture.py | 7 +++++-- maro/simulator/scenarios/supply_chain/world.py | 10 ++++++---- 20 files changed, 66 insertions(+), 30 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index 4f5076aed..d38e59975 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -3,6 +3,9 @@ from .actions import ConsumerAction, ManufactureAction -from .datamodels import * -from .facilities import * -from .units import * +from .datamodels import (ConsumerDataModel, DistributionDataModel, + ManufactureDataModel, SellerDataModel, + StorageDataModel, VehicleDataModel) +from .facilities import RetailerFacility, SupplierFacility, WarehouseFacility +from .units import (ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, + VehicleUnit) diff --git a/maro/simulator/scenarios/supply_chain/actions.py b/maro/simulator/scenarios/supply_chain/actions.py index c5ec2c959..ea46a3f38 100644 --- a/maro/simulator/scenarios/supply_chain/actions.py +++ b/maro/simulator/scenarios/supply_chain/actions.py @@ -4,7 +4,6 @@ from collections import namedtuple - ConsumerAction = namedtuple("ConsumerAction", ("id", "product_id", "source_id", "quantity", "vlt")) ManufactureAction = namedtuple("ManufactureAction", ("id", "production_rate")) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 3d6c4e27e..2c40aa16c 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -3,10 +3,10 @@ import os -from pathlib import Path from maro.event_buffer import MaroEvents from maro.simulator.scenarios import AbsBusinessEngine + from .parser import ConfigParser, SupplyChainConfiguration from .units import UnitBase from .world import World diff --git a/maro/simulator/scenarios/supply_chain/datamodels/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py index 39e3ab3e3..b71d42015 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/base.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/base.py @@ -2,10 +2,8 @@ # Licensed under the MIT license. -from abc import abstractmethod - from maro.backends.backend import AttributeType -from maro.backends.frame import NodeBase, NodeAttribute +from maro.backends.frame import NodeAttribute, NodeBase class DataModelBase(NodeBase): diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index f0c674569..a8c4090cf 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -3,7 +3,7 @@ from maro.backends.backend import AttributeType -from maro.backends.frame import node, NodeAttribute +from maro.backends.frame import NodeAttribute, node from .skumodel import SkuDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py index 606327789..f2bb2242f 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py @@ -3,7 +3,7 @@ from maro.backends.backend import AttributeType -from maro.backends.frame import node, NodeAttribute +from maro.backends.frame import NodeAttribute, node from .base import DataModelBase diff --git a/maro/simulator/scenarios/supply_chain/datamodels/facility.py b/maro/simulator/scenarios/supply_chain/datamodels/facility.py index b78af24dd..a382d0425 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/facility.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/facility.py @@ -3,7 +3,7 @@ from maro.backends.backend import AttributeType -from maro.backends.frame import node, NodeAttribute +from maro.backends.frame import NodeAttribute, node from .base import DataModelBase diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index a4d47284e..149f99b93 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -3,7 +3,7 @@ from maro.backends.backend import AttributeType -from maro.backends.frame import node, NodeAttribute +from maro.backends.frame import NodeAttribute, node from .skumodel import SkuDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index 5a7576d20..433724a5e 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -3,7 +3,7 @@ from maro.backends.backend import AttributeType -from maro.backends.frame import node, NodeAttribute +from maro.backends.frame import NodeAttribute, node from .skumodel import SkuDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py index 7daf0cc07..cbfccb380 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/storage.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/storage.py @@ -3,7 +3,7 @@ from maro.backends.backend import AttributeType -from maro.backends.frame import node, NodeAttribute +from maro.backends.frame import NodeAttribute, node from .base import DataModelBase diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py index e8617ba2a..41fb2d664 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py @@ -3,7 +3,7 @@ from maro.backends.backend import AttributeType -from maro.backends.frame import node, NodeAttribute +from maro.backends.frame import NodeAttribute, node from .base import DataModelBase diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 07af1a242..19c2ee34c 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -5,14 +5,28 @@ from collections import namedtuple from typing import List -from maro.simulator.scenarios.supply_chain.units import ProductUnit, StorageUnit +from maro.simulator.scenarios.supply_chain.units import (ProductUnit, + StorageUnit) + from .facility import FacilityBase class RetailerFacility(FacilityBase): """Retail facility used to generate order from upstream, and sell products by demand.""" - SkuInfo = namedtuple("SkuInfo", ("name", "id", "price", "cost", "init_in_stock", "sale_gamma", "order_cost", "backlog_ratio")) + SkuInfo = namedtuple( + "SkuInfo", + ( + "name", + "id", + "price", + "cost", + "init_in_stock", + "sale_gamma", + "order_cost", + "backlog_ratio" + ) + ) # Product unit list of this facility. products: List[ProductUnit] diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index a2a005f50..123a28fcd 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -5,7 +5,10 @@ from collections import namedtuple from typing import List -from maro.simulator.scenarios.supply_chain.units import StorageUnit, DistributionUnit, ProductUnit +from maro.simulator.scenarios.supply_chain.units import (DistributionUnit, + ProductUnit, + StorageUnit) + from .facility import FacilityBase @@ -14,7 +17,17 @@ class SupplierFacility(FacilityBase): SkuInfo = namedtuple( "SkuInfo", - ("name", "id", "init_in_stock", "type", "cost", "price", "delay_order_penalty", "product_unit_cost", "order_cost") + ( + "name", + "id", + "init_in_stock", + "type", + "cost", + "price", + "delay_order_penalty", + "product_unit_cost", + "order_cost" + ) ) # Storage unit of this facility. diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 1cbbe8549..f9e68dae0 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -5,7 +5,10 @@ from collections import namedtuple from typing import List -from maro.simulator.scenarios.supply_chain.units import StorageUnit, DistributionUnit, ProductUnit +from maro.simulator.scenarios.supply_chain.units import (DistributionUnit, + ProductUnit, + StorageUnit) + from .facility import FacilityBase diff --git a/maro/simulator/scenarios/supply_chain/frame_builder.py b/maro/simulator/scenarios/supply_chain/frame_builder.py index f751c7188..9cde1d97a 100644 --- a/maro/simulator/scenarios/supply_chain/frame_builder.py +++ b/maro/simulator/scenarios/supply_chain/frame_builder.py @@ -3,7 +3,7 @@ from typing import List, Tuple -from maro.backends.frame import NodeBase, FrameBase, FrameNode +from maro.backends.frame import FrameBase, FrameNode, NodeBase def build_frame(enable_snapshot: bool, total_snapshots: int, nodes: List[Tuple[NodeBase, str, int]]): diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index c1ac1f9dc..c7e867607 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -7,6 +7,7 @@ from .manufacture import ManufactureUnit from .product import ProductUnit from .seller import SellerUnit +from .skuunit import SkuUnit from .storage import StorageUnit from .unitbase import UnitBase from .vehicle import VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 654b9b002..a93b69b61 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -3,7 +3,7 @@ import warnings -from collections import defaultdict, Counter +from collections import Counter, defaultdict from .order import Order from .skuunit import SkuUnit @@ -92,7 +92,7 @@ def initialize(self): def step(self, tick: int): # NOTE: id == 0 means invalid,as our id is 1 based. - if self.action is None or self.action.quantity <= 0 or self.action.product_id <= 0 or self.action.source_id == 0: + if not self.action or self.action.quantity <= 0 or self.action.product_id <= 0 or self.action.source_id == 0: return # NOTE: we are using product unit as destination, diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 488ce50a5..ee971f7b3 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. -from collections import deque, defaultdict +from collections import defaultdict, deque from typing import Dict from .order import Order diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 513527e91..5ef2351c1 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -3,6 +3,7 @@ from maro.simulator.scenarios.supply_chain.actions import ManufactureAction + from .skuunit import SkuUnit @@ -68,8 +69,10 @@ def step(self, tick: int): for source_sku_id, source_sku_cost_number in self.bom.items(): source_sku_available_number = self.facility.storage.get_product_number(source_sku_id) - max_number_to_procedure = min(source_sku_available_number // source_sku_cost_number, - max_number_to_procedure) + max_number_to_procedure = min( + source_sku_available_number // source_sku_cost_number, + max_number_to_procedure + ) if max_number_to_procedure <= 0: break diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 6e338ed14..901baf64b 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -3,15 +3,17 @@ from collections import namedtuple -from typing import List, Union, Tuple +from typing import List, Tuple, Union import numpy as np -from maro.backends.frame import FrameBase from tcod.path import AStar +from maro.backends.frame import FrameBase + from .facilities import FacilityBase from .frame_builder import build_frame -from .parser import SupplyChainConfiguration, DataModelDef, UnitDef, FacilityDef +from .parser import (DataModelDef, FacilityDef, SupplyChainConfiguration, + UnitDef) from .units import UnitBase SkuInfo = namedtuple("SkuInfo", ("name", "id", "bom", "output_units_per_lot")) @@ -239,7 +241,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio # 0 for 2nd parameters means disable diagonal movement, so just up, right, down or left. self._path_finder = AStar(cost_grid, 0) - def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase) -> UnitBase: + def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): unit = unit_type() unit.id = self._gen_id() From 17093e20d9ab29434bdb9341abe04f1daeaa124e Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 22 Mar 2021 04:58:15 +0000 Subject: [PATCH 099/482] refinement --- docs/source/key_components/communication.rst | 6 +- examples/cim/dqn/config/training_config.py | 1 + examples/cim/dqn/main.py | 5 +- examples/citi_bike/online_lp/launcher.py | 6 +- examples/proxy/scatter.py | 28 +++---- examples/proxy/send.py | 12 +-- maro/communication/dist_decorator.py | 2 +- maro/communication/message.py | 24 +++--- maro/communication/proxy.py | 54 ++++++------- maro/rl/agent/agent_wrapper.py | 14 +++- maro/rl/agent/dqn.py | 1 + maro/rl/storage/simple_store.py | 21 ----- maro/rl/training/actor.py | 84 +++++++++++--------- maro/rl/training/actor_proxy.py | 74 +++++++++++------ maro/rl/training/learner.py | 66 ++++++--------- maro/rl/training/message_enums.py | 11 +-- maro/rl/training/trajectory.py | 22 ++--- maro/rl/utils/__init__.py | 8 +- maro/rl/utils/trajectory_utils.py | 55 ++++++------- tests/communication/test_decorator.py | 10 +-- tests/communication/test_proxy.py | 16 ++-- tests/communication/test_rejoin.py | 10 +-- tests/communication/test_zmq_driver.py | 10 +-- 23 files changed, 270 insertions(+), 270 deletions(-) diff --git a/docs/source/key_components/communication.rst b/docs/source/key_components/communication.rst index fa6926af1..bd501a051 100644 --- a/docs/source/key_components/communication.rst +++ b/docs/source/key_components/communication.rst @@ -43,7 +43,7 @@ The main attributes of a message instance include: message = Message(tag="check_in", source="worker_001", destination="master", - payload="") + body="") Session Message ^^^^^^^^^^^^^^^ @@ -71,13 +71,13 @@ The stages of each session are maintained internally by the proxy. task_message = SessionMessage(tag="sum", source="master", destination="worker_001", - payload=[0, 1, 2, ...], + body=[0, 1, 2, ...], session_type=SessionType.TASK) notification_message = SessionMessage(tag="check_out", source="worker_001", destination="master", - payload="", + body="", session_type=SessionType.NOTIFICATION) Communication Primitives diff --git a/examples/cim/dqn/config/training_config.py b/examples/cim/dqn/config/training_config.py index b21bbb030..a6dd847f3 100644 --- a/examples/cim/dqn/config/training_config.py +++ b/examples/cim/dqn/config/training_config.py @@ -23,6 +23,7 @@ }, "group": "cim-dqn", "learner_update_trigger": 2, + # "trajectory_sync_interval": 50, "num_actors": 2, "num_trainers": 4, "trainer_id": 0 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 3df47f339..4a78bf733 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -42,7 +42,10 @@ def cim_dqn_actor(): env = Env(**training_config["env"]) trajectory = CIMTrajectory(env, **common_config) agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) - actor = Actor(trajectory, agent, group=training_config["group"]) + actor = Actor( + trajectory, agent, + group=training_config["group"],# trajectory_sync_interval=training_config["trajectory_sync_interval"] + ) actor.run() diff --git a/examples/citi_bike/online_lp/launcher.py b/examples/citi_bike/online_lp/launcher.py index 25278ace3..477cfeb6f 100644 --- a/examples/citi_bike/online_lp/launcher.py +++ b/examples/citi_bike/online_lp/launcher.py @@ -75,10 +75,10 @@ def _record_history(self, env_tick: int, finished_events: List[Event]): event_type = finished_events[self._next_event_idx].event_type if event_type == CitiBikeEvents.RequireBike: # TODO: Replace it with a pre-defined PayLoad. - payload = finished_events[self._next_event_idx].payload + payload = finished_events[self._next_event_idx].body demand_history[interval_idx, payload.src_station] += 1 elif event_type == CitiBikeEvents.ReturnBike: - payload: BikeReturnPayload = finished_events[self._next_event_idx].payload + payload: BikeReturnPayload = finished_events[self._next_event_idx].body supply_history[interval_idx, payload.to_station_idx] += payload.number # Update the index to the finished event that has not been processed. @@ -129,7 +129,7 @@ def __peep_at_the_future(self, env_tick: int): # Process to get the future supply from Pending Events. for pending_event in ENV.get_pending_events(tick=tick): if pending_event.event_type == CitiBikeEvents.ReturnBike: - payload: BikeReturnPayload = pending_event.payload + payload: BikeReturnPayload = pending_event.body supply[interval_idx, payload.to_station_idx] += payload.number return demand, supply diff --git a/examples/proxy/scatter.py b/examples/proxy/scatter.py index 5d2cff8f7..fd0f2b6f7 100644 --- a/examples/proxy/scatter.py +++ b/examples/proxy/scatter.py @@ -22,11 +22,11 @@ def summation_worker(group_name): # Nonrecurring receive the message from the proxy. for msg in proxy.receive(is_continuous=False): - print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.payload}.") + print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.body}.") if msg.tag == "job": - replied_payload = sum(msg.payload) - proxy.reply(message=msg, tag="sum", payload=replied_payload) + replied_payload = sum(msg.body) + proxy.reply(message=msg, tag="sum", body=replied_payload) def multiplication_worker(group_name): @@ -42,11 +42,11 @@ def multiplication_worker(group_name): # Nonrecurring receive the message from the proxy. for msg in proxy.receive(is_continuous=False): - print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.payload}.") + print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.body}.") if msg.tag == "job": - replied_payload = np.prod(msg.payload) - proxy.reply(message=msg, tag="multiply", payload=replied_payload) + replied_payload = np.prod(msg.body) + proxy.reply(message=msg, tag="multiply", body=replied_payload) def master(group_name: str, sum_worker_number: int, multiply_worker_number: int, is_immediate: bool = False): @@ -73,13 +73,13 @@ def master(group_name: str, sum_worker_number: int, multiply_worker_number: int, # Assign sum tasks for summation workers. destination_payload_list = [] - for idx, peer in enumerate(proxy.peers_name["sum_worker"]): - data_length_per_peer = int(len(sum_list) / len(proxy.peers_name["sum_worker"])) + for idx, peer in enumerate(proxy.peers["sum_worker"]): + data_length_per_peer = int(len(sum_list) / len(proxy.peers["sum_worker"])) destination_payload_list.append((peer, sum_list[idx * data_length_per_peer:(idx + 1) * data_length_per_peer])) # Assign multiply tasks for multiplication workers. - for idx, peer in enumerate(proxy.peers_name["multiply_worker"]): - data_length_per_peer = int(len(multiple_list) / len(proxy.peers_name["multiply_worker"])) + for idx, peer in enumerate(proxy.peers["multiply_worker"]): + data_length_per_peer = int(len(multiple_list) / len(proxy.peers["multiply_worker"])) destination_payload_list.append( (peer, multiple_list[idx * data_length_per_peer:(idx + 1) * data_length_per_peer])) @@ -98,11 +98,11 @@ def master(group_name: str, sum_worker_number: int, multiply_worker_number: int, sum_result, multiply_result = 0, 1 for msg in replied_msgs: if msg.tag == "sum": - print(f"{proxy.name} receive message from {msg.source} with the sum result {msg.payload}.") - sum_result += msg.payload + print(f"{proxy.name} receive message from {msg.source} with the sum result {msg.body}.") + sum_result += msg.body elif msg.tag == "multiply": - print(f"{proxy.name} receive message from {msg.source} with the multiply result {msg.payload}.") - multiply_result *= msg.payload + print(f"{proxy.name} receive message from {msg.source} with the multiply result {msg.body}.") + multiply_result *= msg.body # Check task result correction. assert(sum(sum_list) == sum_result) diff --git a/examples/proxy/send.py b/examples/proxy/send.py index 73aa45dba..7b8914143 100644 --- a/examples/proxy/send.py +++ b/examples/proxy/send.py @@ -22,11 +22,11 @@ def worker(group_name): # Nonrecurring receive the message from the proxy. for msg in proxy.receive(is_continuous=False): - print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.payload}.") + print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.body}.") if msg.tag == "sum": - replied_payload = sum(msg.payload) - proxy.reply(message=msg, tag="sum", payload=replied_payload) + replied_payload = sum(msg.body) + proxy.reply(message=msg, tag="sum", body=replied_payload) def master(group_name: str, is_immediate: bool = False): @@ -47,11 +47,11 @@ def master(group_name: str, is_immediate: bool = False): random_integer_list = np.random.randint(0, 100, 5) print(f"generate random integer list: {random_integer_list}.") - for peer in proxy.peers_name["worker"]: + for peer in proxy.peers["worker"]: message = SessionMessage(tag="sum", source=proxy.name, destination=peer, - payload=random_integer_list, + body=random_integer_list, session_type=SessionType.TASK) if is_immediate: session_id = proxy.isend(message) @@ -61,7 +61,7 @@ def master(group_name: str, is_immediate: bool = False): replied_msgs = proxy.send(message, timeout=-1) for msg in replied_msgs: - print(f"{proxy.name} receive {msg.source}, replied payload is {msg.payload}.") + print(f"{proxy.name} receive {msg.source}, replied payload is {msg.body}.") if __name__ == "__main__": diff --git a/maro/communication/dist_decorator.py b/maro/communication/dist_decorator.py index 40839e86d..3cc88a5fb 100644 --- a/maro/communication/dist_decorator.py +++ b/maro/communication/dist_decorator.py @@ -26,7 +26,7 @@ def __init__(self, *args, **kwargs): self.local_instance = cls(*args, **kwargs) self.proxy = proxy self._handler_function = {} - self._registry_table = RegisterTable(self.proxy.peers_name) + self._registry_table = RegisterTable(self.proxy.peers) # Use functools.partial to freeze handling function's local_instance and proxy # arguments to self.local_instance and self.proxy. for constraint, handler_fun in handler_dict.items(): diff --git a/maro/communication/message.py b/maro/communication/message.py index 597526923..2a0971740 100644 --- a/maro/communication/message.py +++ b/maro/communication/message.py @@ -8,13 +8,10 @@ from typing import Union # private lib -from maro.utils import InternalLogger from maro.utils.exit_code import NON_RESTART_EXIT_CODE from .utils import session_id_generator -logger = InternalLogger(component_name="message") - class SessionType(Enum): """Communication session categories. @@ -55,35 +52,35 @@ class Message(object): tag (str|Enum): Message tag, which is customized by the user, for specific application logic. source (str): The sender of message. destination (str): The receiver of message. - payload (object): Message payload, such as model parameters, experiences, etc. Defaults to None. + body (object): Message body, such as model parameters, experiences, etc. Defaults to None. session_id (str): Message belonged session id, it will be generated automatically by default, you can use it to group message based on your application logic. """ - def __init__(self, tag: Union[str, Enum], source: str, destination: str, payload=None): + def __init__(self, tag: Union[str, Enum], source: str, destination: str, body=None): self.tag = tag self.source = source self.destination = destination - self.payload = {} if payload is None else payload + self.body = {} if body is None else body self.session_id = session_id_generator(self.source, self.destination) self.message_id = str(uuid.uuid1()) def __repr__(self): return "; \n".join([f"{k} = {v}" for k, v in vars(self).items()]) - def reply(self, tag: Union[str, Enum] = None, payload=None): + def reply(self, tag: Union[str, Enum] = None, body=None): self.source, self.destination = self.destination, self.source if tag: self.tag = tag - self.payload = payload + self.body = body self.message_id = str(uuid.uuid1()) - def forward(self, destination: str, tag: Union[str, Enum] = None, payload=None): + def forward(self, destination: str, tag: Union[str, Enum] = None, body=None): self.source = self.destination self.destination = destination if tag: self.tag = tag - self.payload = payload + self.body = body self.message_id = str(uuid.uuid1()) @@ -101,11 +98,11 @@ def __init__( self, tag: Union[str, Enum], source: str, destination: str, - payload=None, + body=None, session_type: SessionType = SessionType.TASK, session_stage=None ): - super().__init__(tag, source, destination, payload) + super().__init__(tag, source, destination, body) self.session_type = session_type if self.session_type == SessionType.TASK: @@ -113,5 +110,4 @@ def __init__( elif self.session_type == SessionType.NOTIFICATION: self.session_stage = session_stage if session_stage else NotificationSessionStage.REQUEST else: - logger.error(f"Receive unrecognized session type {self.session_type}, please use the SessionType class.") - sys.exit(NON_RESTART_EXIT_CODE) + raise ValueError(f"Unsupported session type: {self.session_type}") diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 9ed557a2c..3dc2e5a9b 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -95,7 +95,7 @@ def __init__( self._name = os.getenv("COMPONENT_NAME") else: unique_id = str(uuid.uuid1()).replace("-", "") - self._name = f"{self._component_type}_proxy_{unique_id}" + self._name = f"{self._component_type}_{unique_id}" self._max_retries = max_retries self._retry_interval_base_value = retry_interval_base_value self._log_enable = log_enable @@ -288,7 +288,7 @@ def component_type(self) -> str: return self._component_type @property - def peers_name(self) -> Dict: + def peers(self) -> Dict: """Dict: The ``Dict`` of all connected peers' names, stored by peer type.""" return { peer_type: list(self._onboard_peer_dict[peer_type].keys()) for peer_type in self._peers_info_dict.keys() @@ -353,17 +353,17 @@ def _scatter( self, tag: Union[str, Enum], session_type: SessionType, - destination_payload_list: list + destination_body_list: list ) -> List[str]: """Scatters a list of data to peers, and return list of session id.""" session_id_list = [] - for destination, payload in destination_payload_list: + for destination, body in destination_body_list: message = SessionMessage( tag=tag, source=self._name, destination=destination, - payload=payload, + body=body, session_type=session_type ) send_result = self.isend(message) @@ -376,7 +376,7 @@ def scatter( self, tag: Union[str, Enum], session_type: SessionType, - destination_payload_list: list, + destination_body_list: list, timeout: int = -1 ) -> List[Message]: """Scatters a list of data to peers, and return replied messages. @@ -384,15 +384,15 @@ def scatter( Args: tag (str|Enum): Message's tag. session_type (Enum): Message's session type. - destination_payload_list ([Tuple(str, object)]): The destination-payload list. + destination_body_list ([Tuple(str, object)]): The destination-body list. The first item of the tuple in list is the message destination, - and the second item of the tuple in list is the message payload. + and the second item of the tuple in list is the message body. Returns: List[Message]: List of replied message. """ return self.receive_by_id( - targets=self._scatter(tag, session_type, destination_payload_list), + targets=self._scatter(tag, session_type, destination_body_list), timeout=timeout ) @@ -400,28 +400,28 @@ def iscatter( self, tag: Union[str, Enum], session_type: SessionType, - destination_payload_list: list + destination_body_list: list ) -> List[str]: """Scatters a list of data to peers, and return list of message id. Args: tag (str|Enum): Message's tag. session_type (Enum): Message's session type. - destination_payload_list ([Tuple(str, object)]): The destination-payload list. + destination_body_list ([Tuple(str, object)]): The destination-body list. The first item of the tuple in list is the message's destination, - and the second item of the tuple in list is the message's payload. + and the second item of the tuple in list is the message's body. Returns: List[str]: List of message's session id. """ - return self._scatter(tag, session_type, destination_payload_list) + return self._scatter(tag, session_type, destination_body_list) def _broadcast( self, component_type: str, tag: Union[str, Enum], session_type: SessionType, - payload=None + body=None ) -> List[str]: """Broadcast message to all peers, and return list of session id.""" if component_type not in list(self._onboard_peer_dict.keys()): @@ -437,7 +437,7 @@ def _broadcast( tag=tag, source=self._name, destination=component_type, - payload=payload, + body=body, session_type=session_type ) @@ -450,7 +450,7 @@ def broadcast( component_type: str, tag: Union[str, Enum], session_type: SessionType, - payload=None, + body=None, timeout: int = None ) -> List[Message]: """Broadcast message to all peers, and return all replied messages. @@ -459,13 +459,13 @@ def broadcast( component_type (str): Broadcast to all peers in this type. tag (str|Enum): Message's tag. session_type (Enum): Message's session type. - payload (object): The true data. Defaults to None. + body (object): The true data. Defaults to None. Returns: List[Message]: List of replied messages. """ return self.receive_by_id( - targets=self._broadcast(component_type, tag, session_type, payload), + targets=self._broadcast(component_type, tag, session_type, body), timeout=timeout ) @@ -474,7 +474,7 @@ def ibroadcast( component_type: str, tag: Union[str, Enum], session_type: SessionType, - payload=None + body=None ) -> List[str]: """Broadcast message to all subscribers, and return list of message's session id. @@ -482,12 +482,12 @@ def ibroadcast( component_type (str): Broadcast to all peers in this type. tag (str|Enum): Message's tag. session_type (Enum): Message's session type. - payload (object): The true data. Defaults to None. + body (object): The true data. Defaults to None. Returns: List[str]: List of message's session id which related to the replied message. """ - return self._broadcast(component_type, tag, session_type, payload) + return self._broadcast(component_type, tag, session_type, body) def _send(self, message: Message) -> Union[List[str], None]: """Send a message to a remote peer. @@ -563,7 +563,7 @@ def reply( self, message: Union[SessionMessage, Message], tag: Union[str, Enum] = None, - payload=None, + body=None, ack_reply: bool = False ) -> List[str]: """Reply a received message. @@ -571,13 +571,13 @@ def reply( Args: message (Message): The message need to reply. tag (str|Enum): New message tag, if None, keeps the original message's tag. Defaults to None. - payload (object): New message payload, if None, keeps the original message's payload. Defaults to None. + body (object): New message body, if None, keeps the original message's body. Defaults to None. ack_reply (bool): If True, it is acknowledge reply. Defaults to False. Returns: List[str]: Message belonged session id. """ - message.reply(tag=tag, payload=payload) + message.reply(tag=tag, body=body) if isinstance(message, SessionMessage): if message.session_type == SessionType.TASK: session_stage = TaskSessionStage.RECEIVE if ack_reply else TaskSessionStage.COMPLETE @@ -592,7 +592,7 @@ def forward( message: Union[SessionMessage, Message], destination: str, tag: Union[str, Enum] = None, - payload=None + body=None ) -> List[str]: """Forward a received message. @@ -600,12 +600,12 @@ def forward( message (Message): The message need to forward. destination (str): The receiver of message. tag (str|Enum): New message tag, if None, keeps the original message's tag. Defaults to None. - payload (object): Message payload, if None, keeps the original message's payload. Defaults to None. + body (object): Message body, if None, keeps the original message's body. Defaults to None. Returns: List[str]: Message belonged session id. """ - message.forward(destination=destination, tag=tag, payload=payload) + message.forward(destination=destination, tag=tag, body=body) return self.isend(message) def _check_peers_update(self): diff --git a/maro/rl/agent/agent_wrapper.py b/maro/rl/agent/agent_wrapper.py index 26f18b028..2b1b4e579 100644 --- a/maro/rl/agent/agent_wrapper.py +++ b/maro/rl/agent/agent_wrapper.py @@ -4,14 +4,24 @@ import os from typing import List, Union +from .abs_agent import AbsAgent + class MultiAgentWrapper: """Multi-agent wrapper class that exposes the same interfaces as a single agent.""" - def __init__(self, agent_dict: dict): + def __init__(self, agent_dict: Union[AbsAgent, dict]): + if isinstance(agent_dict, AbsAgent): + agent_dict = {"AGENT": agent_dict} self.agent_dict = agent_dict def __getitem__(self, agent_id: str): - return self.agent_dict[agent_id] + if len(self.agent_dict) == 1: + return self.agent_dict["AGENT"] + else: + return self.agent_dict[agent_id] + + def __len__(self): + return len(self.agent_dict) def choose_action(self, state_by_agent: dict): return {agent_id: self.agent_dict[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index d27248d39..f825c092d 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -96,6 +96,7 @@ def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: ]) def learn(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): + print("sampled rewards: ", rewards) states = torch.from_numpy(states) actions = torch.from_numpy(actions) rewards = torch.from_numpy(rewards) diff --git a/maro/rl/storage/simple_store.py b/maro/rl/storage/simple_store.py index 87ee2364c..8a3038f13 100644 --- a/maro/rl/storage/simple_store.py +++ b/maro/rl/storage/simple_store.py @@ -255,24 +255,3 @@ def validate(contents: Dict[str, List]): reference_val = contents[list(contents.keys())[0]] if any(len(val) != len(reference_val) for val in contents.values()): raise StoreMisalignment("values of contents should consist of lists of the same length") - - @staticmethod - def concatenate(stores: List[SimpleStore], capacity: int = -1, overwrite_type: OverwriteType = None): - """Concatenate a list of stores with the same keys. - - The resulting store is of unbounded capacity. - - Args: - stores (List[SimpleStore]): List of stores to be concatenated. - """ - if not stores: - return - expected_keys = stores[0].data.keys - if any(store.data.keys() != expected_keys for store in stores): - raise ValueError("Stores must have the exact same keys to be concatenated") - - ret_store = SimpleStore(list(expected_keys)) - for store in stores: - ret_store.put(store.data) - - return ret_store diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 57725bd54..9ed8d0ad0 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -9,7 +9,7 @@ from maro.rl.utils import get_sars from maro.utils import InternalLogger -from .message_enums import MessageTag, PayloadKey +from .message_enums import MsgTag, MsgKey from .trajectory import AbsTrajectory @@ -23,7 +23,9 @@ class Actor(object): assigned to the learner (and decision clients, if any). proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. - sync_size (int): + trajectory_sync_interval (int): Number of roll-out steps between trajectory syncing calls. + experience_getter (Callable): Custom function to extract experiences from a trajectory for training. + If None, ``get_sars`` will be used. Defaults to None. """ def __init__( self, @@ -31,19 +33,25 @@ def __init__( agent: Union[AbsAgent, MultiAgentWrapper], group: str = None, proxy_options: dict = None, - experience_sync_interval: int = None + trajectory_sync_interval: int = None, + experience_getter: Callable = get_sars ): self.trajectory = trajectory - self.agent = agent - self.experience_pool = SimpleStore(["S", "A", "R", "S_", "loss"]) + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent if group is not None: if proxy_options is None: proxy_options = {} self._proxy = Proxy(group, "actor", {"actor_proxy": 1}, **proxy_options) - self._experience_sync_interval = experience_sync_interval + self._trajectory_sync_interval = trajectory_sync_interval self._logger = InternalLogger(self._proxy.name) - self.exp_sync = bool(getattr(self, "_experience_sync_interval", None)) - + + # Under local mode or disributed mode without experience syncing, an experience pool needs to be created. + if group is None or not self._trajectory_sync_interval: + self.experience_pool = { + agent_id: SimpleStore(["S", "A", "R", "S_", "loss"]) for agent_id in self.agent.agent_dict + } + self.experience_getter = experience_getter + def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): self.trajectory.reset() if not training: @@ -59,51 +67,51 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non while state: action = self.agent.choose_action(state) state = self.trajectory.step(action) - if training and self.exp_sync and len(self.trajectory.states) == self._experience_sync_interval: - exp = get_sars(self.trajectory.states, self.trajectory.actions, self.trajectory.rewards) - self.trajectory.flush() - self._proxy.isend( - Message( - MessageTag.EXPERIENCE, self._proxy.name, self._proxy.peers_name["actor_proxy"][0], - payload={PayloadKey.ROLLOUT_INDEX: index, PayloadKey.EXPERIENCE: exp} - ) - ) - + if training and hasattr(self, "_proxy") and not hasattr(self, "experience_pool"): + self._sync_trajectory(index) self.trajectory.on_env_feedback() self.trajectory.on_finish() # If no experience syncing, the experience pool needs to be populated. - if training and not self._experience_sync_interval: - if isinstance(self.agent, AbsAgent): - self.experience_pool.put(get_sars(*self.trajectory.path, multi_agent=False)) - else: - for agent_id, exp in get_sars(*self.trajectory.path).items(): - self.experience_pool[agent_id].put(exp) + if training and hasattr(self, "experience_pool"): + for agent_id, exp in self.experience_getter(*self.trajectory.path).items(): + self.experience_pool[agent_id].put(exp) + print(agent_id, len(self.experience_pool[agent_id])) return self.trajectory.env.metrics - + def run(self): assert hasattr(self, "_proxy"), "No proxy found. The `group` parameter should not be None at init." for msg in self._proxy.receive(): - if msg.tag == MessageTag.EXIT: + if msg.tag == MsgTag.EXIT: self._logger.info("Exiting...") break - elif msg.tag == MessageTag.ROLLOUT: - ep = msg.payload[PayloadKey.ROLLOUT_INDEX] + elif msg.tag == MsgTag.ROLLOUT: + ep = msg.body[MsgKey.ROLLOUT_INDEX] self._logger.info(f"Rolling out ({ep})...") metrics = self.roll_out( ep, - training=msg.payload[PayloadKey.TRAINING], - model_by_agent=msg.payload[PayloadKey.MODEL], - exploration_params=msg.payload[PayloadKey.EXPLORATION_PARAMS] + training=msg.body[MsgKey.TRAINING], + model_by_agent=msg.body[MsgKey.MODEL], + exploration_params=msg.body[MsgKey.EXPLORATION_PARAMS] ) self._logger.info(f"Roll-out {ep} finished") - payload = {PayloadKey.ROLLOUT_INDEX: ep, PayloadKey.METRICS: metrics} - if msg.payload[PayloadKey.TRAINING] and not self.exp_sync: - payload[PayloadKey.EXPERIENCE] = self.experience_pool + body = {MsgKey.ROLLOUT_INDEX: ep, MsgKey.METRICS: metrics} + if msg.body[MsgKey.TRAINING]: + if hasattr(self, "experience_pool"): + body[MsgKey.EXPERIENCE] = {id_: pool.data for id_, pool in self.experience_pool.items()} + else: + body[MsgKey.TRAJECTORY] = self.trajectory.path self._proxy.isend( - Message( - MessageTag.FINISHED, self._proxy.name, self._proxy.peers_name["actor_proxy"][0], - payload=payload - ) + Message(MsgTag.ROLLOUT_DONE, self._proxy.name, self._proxy.peers["actor_proxy"][0], body=body) + ) + + def _sync_trajectory(self, index): + if (self.trajectory.step_index + 1) % self._trajectory_sync_interval == 0: + self._proxy.isend( + Message( + MsgTag.TRAJECTORY_SYNC, self._proxy.name, self._proxy.peers["actor_proxy"][0], + body={MsgKey.ROLLOUT_INDEX: index, MsgKey.TRAJECTORY: self.trajectory.path} ) + ) + self.trajectory.flush() diff --git a/maro/rl/training/actor_proxy.py b/maro/rl/training/actor_proxy.py index fe11a0d2a..e77134d94 100644 --- a/maro/rl/training/actor_proxy.py +++ b/maro/rl/training/actor_proxy.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import List +from collections import defaultdict +from typing import Callable, List from maro.communication import Message, Proxy, RegisterTable, SessionType -from maro.rl.storage import SimpleStore +from maro.rl.storage import OverwriteType, SimpleStore +from maro.rl.utils import get_sars from maro.utils import InternalLogger -from .message_enums import MessageTag, PayloadKey +from .message_enums import MsgTag, MsgKey class ActorProxy(object): @@ -17,30 +19,44 @@ class ActorProxy(object): group_name (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the actors (and roll-out clients, if any). num_actors (int): Expected number of actors in the group identified by ``group_name``. - update_trigger (str): Number or percentage of ``MessageTag.FINISHED`` messages required to trigger + update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger learner updates, i.e., model training. proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. + experience_getter (Callable): Custom function to extract experiences from a trajectory for training. + If None, ``get_sars`` will be used. Defaults to None. """ def __init__( self, group_name: str, num_actors: int, update_trigger: str = None, - proxy_options: dict = None + proxy_options: dict = None, + experience_getter: Callable = get_sars, + experience_pool_capacity: int = -1, + experience_pool_overwrite: OverwriteType = None ): peers = {"actor": num_actors} if proxy_options is None: proxy_options = {} self._proxy = Proxy(group_name, "actor_proxy", peers, **proxy_options) - self._actors = self._proxy.peers_name["actor"] # remote actor ID's - self._registry_table = RegisterTable(self._proxy.peers_name) + self._actors = self._proxy.peers["actor"] # remote actor ID's + self._registry_table = RegisterTable(self._proxy.peers) if update_trigger is None: update_trigger = len(self._actors) self._registry_table.register_event_handler( - f"actor:{MessageTag.FINISHED.value}:{update_trigger}", self._on_rollout_finish + f"actor:{MsgTag.ROLLOUT_DONE.value}:{update_trigger}", self._on_rollout_finish ) - self.experience_pool = SimpleStore(["S", "A", "R", "S_", "loss"]) + + def get_experience_pool(): + return SimpleStore( + ["S", "A", "R", "S_", "loss"], + capacity=experience_pool_capacity, + overwrite_type=experience_pool_overwrite + ) + + self.experience_pool = defaultdict(lambda: get_experience_pool()) + self.experience_getter = experience_getter self.logger = InternalLogger("ACTOR_PROXY") def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): @@ -52,44 +68,54 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non model_by_agent (dict): Models to be broadcast to remote actors for inference. Defaults to None. exploration_params: Exploration parameters to be used by the remote roll-out actors. Defaults to None. """ - payload = { - PayloadKey.ROLLOUT_INDEX: index, - PayloadKey.TRAINING: training, - PayloadKey.MODEL: model_by_agent, - PayloadKey.EXPLORATION_PARAMS: exploration_params + body = { + MsgKey.ROLLOUT_INDEX: index, + MsgKey.TRAINING: training, + MsgKey.MODEL: model_by_agent, + MsgKey.EXPLORATION_PARAMS: exploration_params } - self._proxy.iscatter(MessageTag.ROLLOUT, SessionType.TASK, [(actor, payload) for actor in self._actors]) + self._proxy.iscatter(MsgTag.ROLLOUT, SessionType.TASK, [(actor, body) for actor in self._actors]) self.logger.info(f"Sent roll-out requests to {self._actors} for ep-{index}") # Receive roll-out results from remote actors for msg in self._proxy.receive(): - if msg.payload[PayloadKey.ROLLOUT_INDEX] != index: + if msg.body[MsgKey.ROLLOUT_INDEX] != index: self.logger.info( - f"Ignore a message of type {msg.tag} with ep {msg.payload[PayloadKey.ROLLOUT_INDEX]} " + f"Ignore a message of type {msg.tag} with ep {msg.body[MsgKey.ROLLOUT_INDEX]} " f"(expected {index} or greater)" ) continue - if msg.tag == MessageTag.FINISHED: + if msg.tag == MsgTag.ROLLOUT_DONE: # If enough update messages have been received, call update() and break out of the loop to start # the next episode. result = self._registry_table.push(msg) if result: env_metrics = result[0] break - elif msg.tag == MessageTag.EXPERIENCE: - self.experience_pool.put(msg.payload[PayloadKey.EXPERIENCE]) + elif msg.tag == MsgTag.TRAJECTORY_SYNC: + for agent_id, exp in self.experience_getter(*msg.body[MsgKey.TRAJECTORY]).items(): + self.experience_pool[agent_id].put(exp) + print(f"received exp from actor {msg.source} ", end="") + print({agent_id: len(pool) for agent_id, pool in self.experience_pool.items()}) return env_metrics def _on_rollout_finish(self, messages: List[Message]): - metrics = {msg.source: msg.payload[PayloadKey.METRICS] for msg in messages} - if PayloadKey.EXPERIENCE in messages[0].payload: - self.experience_pool = SimpleStore.concatenate([msg.payload[PayloadKey.EXPERIENCE] for msg in messages]) + metrics = {msg.source: msg.body[MsgKey.METRICS] for msg in messages} + for msg in messages: + if MsgKey.EXPERIENCE in msg.body: + exp = msg.body[MsgKey.EXPERIENCE] + else: + exp = self.experience_getter(*msg.body[MsgKey.TRAJECTORY]) + for agent_id, ex in exp.items(): + self.experience_pool[agent_id].put(ex) + for agent_id, pool in self.experience_pool.items(): + print(agent_id, len(pool)) return metrics def terminate(self): """Tell the remote actors to exit.""" self._proxy.ibroadcast( - component_type="actor", tag=MessageTag.EXIT, session_type=SessionType.NOTIFICATION + component_type="actor", tag=MsgTag.EXIT, session_type=SessionType.NOTIFICATION ) self.logger.info("Exiting...") diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index d0e548c22..aa8275086 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -29,7 +29,7 @@ def __init__(self, actor: Union[Actor, ActorProxy], agent: Union[AbsAgent, Multi super().__init__() if isinstance(actor, ActorProxy): assert agent, "agent cannot be None when the actor is a proxy." - self.agent = agent + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent else: # The agent passed to __init__ is ignored in this case self.agent = actor.agent @@ -58,13 +58,9 @@ def run(self): ep, model_by_agent=self.agent.dump_model() if isinstance(self.actor, ActorProxy) else None ) self.logger.info(f"ep-{ep}: {env_metrics}") - if isinstance(self.agent, AbsAgent): - for e in exp: - self.agent.learn(*e["args"], **e.get("kwargs", {})) - else: - for agent_id, ex in exp.items(): - for e in ex: - self.agent[agent_id].learn(*e["args"], **e.get("kwargs", {})) + for agent_id, ex in exp.items(): + for e in ex: + self.agent[agent_id].learn(*e["args"], **e.get("kwargs", {})) self.logger.info("Agent learning finished") @@ -104,19 +100,13 @@ def run(self): ) self.logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") - if isinstance(self.agent, AbsAgent): - for _ in range(self.train_iter): - batch, idx = self.get_batch() - loss = self.agent.learn(*batch) - self.actor.experience_pool.update(idx, {"loss": list(loss)}) - else: - for _ in range(self.train_iter): - batch_by_agent, idx_by_agent = self.get_batch() - loss_by_agent = { - agent_id: self.agent[agent_id].learn(*batch) for agent_id, batch in batch_by_agent.items() - } - for agent_id, loss in loss_by_agent.items(): - self.actor.experience_pool[agent_id].update(idx_by_agent[agent_id], {"loss": list(loss)}) + for _ in range(self.train_iter): + batch_by_agent, idx_by_agent = self.get_batch() + loss_by_agent = { + agent_id: self.agent[agent_id].learn(*batch) for agent_id, batch in batch_by_agent.items() + } + for agent_id, loss in loss_by_agent.items(): + self.actor.experience_pool[agent_id].update(idx_by_agent[agent_id], {"loss": list(loss)}) self.logger.info("Agent learning finished") @@ -125,27 +115,17 @@ def run(self): self.actor.terminate() def get_batch(self): - if isinstance(self.agent, AbsAgent): - if len(self.actor.experience_pool) < self.min_experiences_to_train: - return None, None + idx, batch = {}, {} + for agent_id, pool in self.actor.experience_pool.items(): + if len(pool) < self.min_experiences_to_train: + continue if self.prioritized_sampling_by_loss: - indexes, sample = self.actor.experience_pool.sample_by_key("loss", self.batch_size) + indexes, sample = self.actor.experience_pool[agent_id].sample_by_key("loss", self.batch_size) else: - indexes, sample = self.actor.experience_pool.sample(self.batch_size) - batch = asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) - return batch, indexes - else: - idx, batch = {}, {} - for agent_id, pool in self.actor.experience_pool.items(): - if len(pool) < self.min_experiences_to_train: - continue - if self.prioritized_sampling_by_loss: - indexes, sample = self.actor.experience_pool[agent_id].sample_by_key("loss", self.batch_size) - else: - indexes, sample = self.actor.experience_pool[agent_id].sample(self.batch_size) - batch[agent_id] = ( - asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) - ) - idx[agent_id] = indexes - - return batch, idx + indexes, sample = self.actor.experience_pool[agent_id].sample(self.batch_size) + batch[agent_id] = ( + asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) + ) + idx[agent_id] = indexes + + return batch, idx diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index bdc2b182f..69dabc327 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -1,23 +1,24 @@ from enum import Enum -class MessageTag(Enum): +class MsgTag(Enum): ROLLOUT = "rollout" CHOOSE_ACTION = "choose_action" ACTION = "action" - EXPERIENCE = "experience_sync" - ABORT_ROLLOUT = "abort_rollout" + TRAJECTORY_SYNC = "experience_sync" TRAIN = "train" - FINISHED = "finished" + ABORT_ROLLOUT = "abort_rollout" + ROLLOUT_DONE = "rollout_done" EXIT = "exit" -class PayloadKey(Enum): +class MsgKey(Enum): ACTION = "action" AGENT_ID = "agent_id" ROLLOUT_INDEX = "rollout_index" TIME_STEP = "time_step" METRICS = "metrics" + TRAJECTORY = "trajectory" EXPERIENCE = "experience" STATE = "state" TRAINING = "training" diff --git a/maro/rl/training/trajectory.py b/maro/rl/training/trajectory.py index 751871294..6c3a79c5a 100644 --- a/maro/rl/training/trajectory.py +++ b/maro/rl/training/trajectory.py @@ -8,12 +8,13 @@ from maro.communication import Message, Proxy -from .message_enums import MessageTag, PayloadKey +from .message_enums import MsgTag, MsgKey class AbsTrajectory(ABC): def __init__(self, env, record_path: bool = True): self.env = env + self.step_index = None self.events = [] self.states = [] self.actions = [] @@ -21,6 +22,7 @@ def __init__(self, env, record_path: bool = True): self.record_path = record_path def start(self, rollout_index: int = None): + self.step_index = 0 _, event, _ = self.env.step(None) state = self.get_state(event) if self.record_path: @@ -45,6 +47,7 @@ def get_reward(self) -> float: def step(self, action): assert self.events, "start() must be called first." + self.step_index += 1 env_action = self.get_action(action, self.events[-1]) if len(env_action) == 1: env_action = list(env_action.values())[0] @@ -52,6 +55,7 @@ def step(self, action): if self.record_path: self.actions.append(action) self.rewards.append(self.get_reward()) + assert len(self.events) == len(self.states) == len(self.actions) == len(self.rewards) if not done: state = self.get_state(event) if self.record_path: @@ -67,13 +71,13 @@ def on_finish(self): def reset(self): self.env.reset() - self.events = [] - self.states = [] - self.actions = [] - self.rewards = [] + self.events.clear() + self.states.clear() + self.actions.clear() + self.rewards.clear() def flush(self): - self.events = self.events[-1:] - self.states = self.states[-1:] - self.actions = self.actions[-1:] - self.rewards = self.rewards[-1:] + self.events = self.events[-1:] if len(self.events) == len(self.actions) + 1 else [] + self.states = self.states[-1:] if len(self.states) == len(self.actions) + 1 else [] + self.actions.clear() + self.rewards.clear() diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index f12124c1d..ae35a7527 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,12 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .trajectory_utils import ( - get_k_step_returns, get_lambda_returns, get_sars, get_truncated_cumulative_reward -) +from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_sars, get_truncated_cumulative_reward from .value_utils import get_log_prob, get_max, get_td_errors, select_by_actions __all__ = [ - "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", - "get_log_prob", "get_max", "get_sars", "get_td_errors", "select_by_actions" + "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", "get_log_prob", "get_max", + "get_sars", "get_td_errors", "select_by_actions" ] diff --git a/maro/rl/utils/trajectory_utils.py b/maro/rl/utils/trajectory_utils.py index da81d9186..3498c68d6 100644 --- a/maro/rl/utils/trajectory_utils.py +++ b/maro/rl/utils/trajectory_utils.py @@ -10,8 +10,10 @@ MAX_LOSS = 1e8 -def get_sars(states: list, actions: list, rewards: list, multi_agent: bool = True) -> dict: - """Extract experiences from a trajectory. +def get_sars(states: list, actions: list, rewards: list) -> dict: + """Extract experiences from a multi-agent trajectory. + + The top-level keys for each state and action will be treated as agent IDs during the extraction. Args: states (list): List of states traversed during a roll-out episode (in order). @@ -21,39 +23,30 @@ def get_sars(states: list, actions: list, rewards: list, multi_agent: bool = Tru Returns: Experiences for training, grouped by agent ID. """ - if multi_agent: - sars = {} - for state, action, reward in zip(states, actions, rewards): - for agent_id in state: - exp = sars.setdefault(agent_id, {"S": [], "A": [], "R": [], "S_": [], "loss": []}) - exp["S"].append(state[agent_id]) - exp["A"].append(action[agent_id]) - exp["R"].append(reward) - exp["loss"].append(MAX_LOSS) - - for exp in sars.values(): - exp["S_"] = exp["S"][1:] - exp["S"].pop() + sars = {} + for state, action, reward in zip(states, actions, rewards): + assert state.keys() == action.keys(), f"state keys: {list(state.keys())}, action keys: {list(action.keys())}" + for agent_id in state: + exp = sars.setdefault(agent_id, {"S": [], "A": [], "R": [], "S_": [], "loss": []}) + exp["S"].append(state[agent_id]) + exp["A"].append(action[agent_id]) + exp["R"].append(reward) + exp["loss"].append(MAX_LOSS) + + if len(states) == len(actions) + 1: + for agent_id in states[-1]: + if agent_id in sars: + sars[agent_id]["S"].append(states[-1][agent_id]) + + for exp in sars.values(): + exp["S_"] = exp["S"][1:] + exp["S"].pop() + if len(exp["S"]) == len(exp["A"]) - 1: exp["A"].pop() exp["R"].pop() exp["loss"].pop() - return sars - else: - sars = {"S": [], "A": [], "R": [], "S_": [], "loss": []} - for state, action, reward in zip(states, actions, rewards): - sars["S"].append(state) - sars["A"].append(action) - sars["R"].append(reward) - sars["loss"].append(MAX_LOSS) - - sars["S_"] = exp["S"][1:] - sars["S"].pop() - sars["A"].pop() - sars["R"].pop() - sars["loss"].pop() - - return sars + return sars def get_truncated_cumulative_reward( diff --git a/tests/communication/test_decorator.py b/tests/communication/test_decorator.py index 118a604ba..140415e4a 100644 --- a/tests/communication/test_decorator.py +++ b/tests/communication/test_decorator.py @@ -13,8 +13,8 @@ def handler_function(that, proxy, message): - replied_payload = {"counter": message.payload["counter"] + 1} - proxy.reply(message, payload=replied_payload) + replied_payload = {"counter": message.body["counter"] + 1} + proxy.reply(message, body=replied_payload) sys.exit(0) @@ -61,12 +61,12 @@ def test_decorator(self): message = SessionMessage( tag="unittest", source=TestDecorator.sender_proxy.name, - destination=TestDecorator.sender_proxy.peers_name["receiver"][0], - payload={"counter": 0} + destination=TestDecorator.sender_proxy.peers["receiver"][0], + body={"counter": 0} ) replied_message = TestDecorator.sender_proxy.send(message) - self.assertEqual(message.payload["counter"] + 1, replied_message[0].payload["counter"]) + self.assertEqual(message.body["counter"] + 1, replied_message[0].body["counter"]) if __name__ == "__main__": diff --git a/tests/communication/test_proxy.py b/tests/communication/test_proxy.py index e5038588f..715cb80b4 100644 --- a/tests/communication/test_proxy.py +++ b/tests/communication/test_proxy.py @@ -12,7 +12,7 @@ def message_receive(proxy): for received_message in proxy.receive(is_continuous=False): - return received_message.payload + return received_message.body @unittest.skipUnless(os.environ.get("test_with_redis", False), "require redis") @@ -51,12 +51,12 @@ def test_send(self): tag="unit_test", source=TestProxy.master_proxy.name, destination=worker_proxy.name, - payload="hello_world!" + body="hello_world!" ) TestProxy.master_proxy.isend(send_msg) for receive_message in worker_proxy.receive(is_continuous=False): - self.assertEqual(send_msg.payload, receive_message.payload) + self.assertEqual(send_msg.body, receive_message.body) def test_scatter(self): scatter_payload = ["worker_1", "worker_2", "worker_3", "worker_4", "worker_5"] @@ -73,7 +73,7 @@ def test_scatter(self): for i, worker_proxy in enumerate(TestProxy.worker_proxies): for msg in worker_proxy.receive(is_continuous=False): - self.assertEqual(scatter_payload[i], msg.payload) + self.assertEqual(scatter_payload[i], msg.body) def test_broadcast(self): with ThreadPoolExecutor(max_workers=len(TestProxy.worker_proxies)) as executor: @@ -84,7 +84,7 @@ def test_broadcast(self): component_type="worker", tag="unit_test", session_type=SessionType.NOTIFICATION, - payload=payload + body=payload ) for task in all_tasks: @@ -97,15 +97,15 @@ def test_reply(self): tag="unit_test", source=TestProxy.master_proxy.name, destination=worker_proxy.name, - payload="hello " + body="hello " ) session_id_list = TestProxy.master_proxy.isend(send_msg) for receive_message in worker_proxy.receive(is_continuous=False): - worker_proxy.reply(message=receive_message, tag="unit_test", payload="world!") + worker_proxy.reply(message=receive_message, tag="unit_test", body="world!") replied_msg_list = TestProxy.master_proxy.receive_by_id(session_id_list) - self.assertEqual(send_msg.payload + replied_msg_list[0].payload, "hello world!") + self.assertEqual(send_msg.body + replied_msg_list[0].body, "hello world!") if __name__ == "__main__": diff --git a/tests/communication/test_rejoin.py b/tests/communication/test_rejoin.py index 696934317..4043af258 100644 --- a/tests/communication/test_rejoin.py +++ b/tests/communication/test_rejoin.py @@ -34,13 +34,13 @@ def actor_init(queue, redis_port): for msg in proxy.receive(is_continuous=True): print(f"receive message from master. {msg.tag}") if msg.tag == "cont": - proxy.reply(message=msg, tag="recv", payload="successful receive!") + proxy.reply(message=msg, tag="recv", body="successful receive!") elif msg.tag == "stop": - proxy.reply(message=msg, tag="recv", payload=f"{proxy.name} exited!") + proxy.reply(message=msg, tag="recv", body=f"{proxy.name} exited!") queue.put(proxy.name) break elif msg.tag == "finish": - proxy.reply(message=msg, tag="recv", payload=f"{proxy.name} finish!") + proxy.reply(message=msg, tag="recv", body=f"{proxy.name} finish!") sys.exit(0) proxy.__del__() @@ -85,7 +85,7 @@ def setUpClass(cls): **PROXY_PARAMETER ) - cls.peers = cls.master_proxy.peers_name["actor"] + cls.peers = cls.master_proxy.peers["actor"] @classmethod def tearDownClass(cls) -> None: @@ -113,7 +113,7 @@ def test_rejoin(self): tag="stop", source=TestRejoin.master_proxy.name, destination=TestRejoin.peers[1], - payload=None, + body=None, session_type=SessionType.TASK ) TestRejoin.master_proxy.isend(disconnect_message) diff --git a/tests/communication/test_zmq_driver.py b/tests/communication/test_zmq_driver.py index fcedb460a..58308b6b4 100644 --- a/tests/communication/test_zmq_driver.py +++ b/tests/communication/test_zmq_driver.py @@ -10,7 +10,7 @@ def message_receive(driver): for received_message in driver.receive(is_continuous=False): - return received_message.payload + return received_message.body @unittest.skipUnless(os.environ.get("test_with_zmq", False), "require zmq") @@ -45,12 +45,12 @@ def test_send(self): tag="unit_test", source="sender", destination=peer, - payload="hello_world" + body="hello_world" ) TestDriver.sender.send(message) for received_message in TestDriver.receivers[peer].receive(is_continuous=False): - self.assertEqual(received_message.payload, message.payload) + self.assertEqual(received_message.body, message.body) def test_broadcast(self): executor = ThreadPoolExecutor(max_workers=len(TestDriver.peer_list)) @@ -60,13 +60,13 @@ def test_broadcast(self): tag="unit_test", source="sender", destination="*", - payload="hello_world" + body="hello_world" ) TestDriver.sender.broadcast(topic="receiver", message=message) for task in as_completed(all_task): res = task.result() - self.assertEqual(res, message.payload) + self.assertEqual(res, message.body) if __name__ == "__main__": From fcbc8bcb7efd915a90d7d5a719e6f013205be584 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 13:18:40 +0800 Subject: [PATCH 100/482] lint issues --- maro/simulator/scenarios/supply_chain/__init__.py | 14 +++++++++----- .../scenarios/supply_chain/facilities/retailer.py | 5 ++--- .../scenarios/supply_chain/facilities/supplier.py | 4 +--- .../scenarios/supply_chain/facilities/warehouse.py | 4 +--- .../scenarios/supply_chain/units/manufacture.py | 2 +- maro/simulator/scenarios/supply_chain/world.py | 3 +-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index d38e59975..eec9b8742 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -3,9 +3,13 @@ from .actions import ConsumerAction, ManufactureAction -from .datamodels import (ConsumerDataModel, DistributionDataModel, - ManufactureDataModel, SellerDataModel, - StorageDataModel, VehicleDataModel) +from .datamodels import ( + ConsumerDataModel, + DistributionDataModel, + ManufactureDataModel, + SellerDataModel, + StorageDataModel, + VehicleDataModel +) from .facilities import RetailerFacility, SupplierFacility, WarehouseFacility -from .units import (ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, - VehicleUnit) +from .units import ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 19c2ee34c..443340834 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -5,8 +5,7 @@ from collections import namedtuple from typing import List -from maro.simulator.scenarios.supply_chain.units import (ProductUnit, - StorageUnit) +from maro.simulator.scenarios.supply_chain.units import ProductUnit, StorageUnit from .facility import FacilityBase @@ -25,8 +24,8 @@ class RetailerFacility(FacilityBase): "sale_gamma", "order_cost", "backlog_ratio" - ) ) + ) # Product unit list of this facility. products: List[ProductUnit] diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index 123a28fcd..ab4be108d 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -5,9 +5,7 @@ from collections import namedtuple from typing import List -from maro.simulator.scenarios.supply_chain.units import (DistributionUnit, - ProductUnit, - StorageUnit) +from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit from .facility import FacilityBase diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index f9e68dae0..77e7aea4b 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -5,9 +5,7 @@ from collections import namedtuple from typing import List -from maro.simulator.scenarios.supply_chain.units import (DistributionUnit, - ProductUnit, - StorageUnit) +from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit from .facility import FacilityBase diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 5ef2351c1..436f855de 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -72,7 +72,7 @@ def step(self, tick: int): max_number_to_procedure = min( source_sku_available_number // source_sku_cost_number, max_number_to_procedure - ) + ) if max_number_to_procedure <= 0: break diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 901baf64b..c14c63b42 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -12,8 +12,7 @@ from .facilities import FacilityBase from .frame_builder import build_frame -from .parser import (DataModelDef, FacilityDef, SupplyChainConfiguration, - UnitDef) +from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef from .units import UnitBase SkuInfo = namedtuple("SkuInfo", ("name", "id", "bom", "output_units_per_lot")) From aa071b96741b4820b1ab2cdf0e5b25b94c6ccfd3 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 13:30:40 +0800 Subject: [PATCH 101/482] lint issue again --- maro/simulator/scenarios/supply_chain/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index eec9b8742..edce7834e 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -4,12 +4,7 @@ from .actions import ConsumerAction, ManufactureAction from .datamodels import ( - ConsumerDataModel, - DistributionDataModel, - ManufactureDataModel, - SellerDataModel, - StorageDataModel, - VehicleDataModel + ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel ) from .facilities import RetailerFacility, SupplierFacility, WarehouseFacility from .units import ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, VehicleUnit From 995cf8d8e76086653a778adeaeac2e673668a820 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 13:48:48 +0800 Subject: [PATCH 102/482] more comments --- .../scenarios/supply_chain/datamodels/base.py | 26 ++++++++++++++----- .../supply_chain/datamodels/consumer.py | 17 ++++++------ .../supply_chain/datamodels/distribution.py | 7 ++++- .../supply_chain/datamodels/facility.py | 13 +++++----- .../supply_chain/datamodels/manufacture.py | 16 ++++++++---- .../supply_chain/datamodels/seller.py | 8 +----- .../supply_chain/datamodels/skumodel.py | 1 + .../supply_chain/datamodels/storage.py | 3 ++- .../supply_chain/datamodels/vehicle.py | 1 - .../scenarios/supply_chain/units/order.py | 3 --- .../scenarios/supply_chain/units/vehicle.py | 2 +- 11 files changed, 55 insertions(+), 42 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py index b71d42015..834864072 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/base.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/base.py @@ -7,27 +7,39 @@ class DataModelBase(NodeBase): - # id of related unit + """Base of all data model of this scenario.""" + # Id of related unit or facility, 0 is invalid by default. id = NodeAttribute(AttributeType.Int) - # id of facility this unit belongs to + # Id of facility this unit belongs to. facility_id = NodeAttribute(AttributeType.Int) def __init__(self): self._unit_id = 0 self._facility_id = 0 - def initialize(self, configs: dict): - """Initialize the fields with configs, the config should be a dict.""" - # called from unit after frame is ready. + def initialize(self, **kwargs): + """Initialize the fields with configs, the config should be a dict. + + Args: + kwargs (dict): Configuration of related data model, used to hold value to + reset after frame reset. + """ + # Called from unit after frame is ready. pass def reset(self): - """Reset after each episode""" - # called per episode. + """Reset after each episode.""" + # Called per episode. self.id = self._unit_id self.facility_id = self._facility_id def set_id(self, unit_id: int, facility_id: int): + """Used to assign id(s), so that it will be assigned after frame rest. + + Args: + unit_id (int): Id of related unit. + facility_id (int): Id of this unit belongs to. + """ self._unit_id = unit_id self._facility_id = facility_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index a8c4090cf..5e10633e8 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -8,26 +8,20 @@ from .skumodel import SkuDataModel -# NOTE: one sku one consumer @node("consumer") class ConsumerDataModel(SkuDataModel): - # reward states - # from config + """Data model for consumer unit.""" order_cost = NodeAttribute(AttributeType.UInt) total_purchased = NodeAttribute(AttributeType.UInt) total_received = NodeAttribute(AttributeType.UInt) - # action states source_id = NodeAttribute(AttributeType.UInt) quantity = NodeAttribute(AttributeType.UInt) vlt = NodeAttribute(AttributeType.UShort) - # id of upstream facilities. + # Id of upstream facilities. sources = NodeAttribute(AttributeType.UInt, 1, is_list=True) - # per tick states - - # snapshots["consumer"][hist_len::"purchased"] equals to original latest_consumptions purchased = NodeAttribute(AttributeType.UInt) received = NodeAttribute(AttributeType.UInt) order_product_cost = NodeAttribute(AttributeType.UInt) @@ -37,7 +31,12 @@ def __init__(self): self._order_cost = 0 - def initialize(self, order_cost=0): + def initialize(self, order_cost: int = 0): + """Initialize consumer data model with order_cost from configurations. + + Args: + order_cost (int): Order cost from configuration files. + """ self._order_cost = order_cost self.reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py index f2bb2242f..0ae79e74c 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py @@ -10,6 +10,7 @@ @node("distribution") class DistributionDataModel(DataModelBase): + """Distribution data model for distribution unit.""" unit_price = NodeAttribute(AttributeType.Int) product_list = NodeAttribute(AttributeType.UInt, 1, is_list=True) @@ -19,10 +20,14 @@ class DistributionDataModel(DataModelBase): def __init__(self): super(DistributionDataModel, self).__init__() - # TODO: not sure about this. self._unit_price = 0 def initialize(self, unit_price: int = 0): + """Initialize distribution with unit_price from configuration. + + Args: + unit_price (int): Unit price from configuration files. + """ self._unit_price = unit_price self.reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/facility.py b/maro/simulator/scenarios/supply_chain/datamodels/facility.py index a382d0425..468b3813e 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/facility.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/facility.py @@ -10,13 +10,12 @@ @node("facility") class FacilityDataModel(DataModelBase): - test = NodeAttribute(AttributeType.UInt) + """Data model for facilities. + + NOTE: + Not in use for now. + """ + balance_sheet = NodeAttribute(AttributeType.UInt) def __init__(self): super(FacilityDataModel, self).__init__() - - def initialize(self, configs: dict): - self.reset() - - def reset(self): - super(FacilityDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index 149f99b93..f561c5fa0 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -10,17 +10,17 @@ @node("manufacture") class ManufactureDataModel(SkuDataModel): - # storage related to this manufacture unit, for easy state retrieving. + """Data model for manufacture unit.""" + # Storage related to this manufacture unit, for easy state retrieving. storage_id = NodeAttribute(AttributeType.UInt) - # cost to produce one output production + # Cost to produce one output production. product_unit_cost = NodeAttribute(AttributeType.UInt) - # number per tick, different with original manufacturing cost, we just provide number, and cost - # user can determine how to calculate the cost + # Number per tick, different with original manufacturing cost, we just provide number, and cost + # user can determine how to calculate the cost. manufacturing_number = NodeAttribute(AttributeType.UInt) - # original from config, then updated by action production_rate = NodeAttribute(AttributeType.UInt) def __init__(self): @@ -29,6 +29,12 @@ def __init__(self): self._storage_id = 0 def initialize(self, product_unit_cost: int = 1, storage_id: int = 0): + """Initialize data model, used to assign value after frame reset. + + Args: + product_unit_cost (int): Cost per unit, from configuration files. + storage_id (int): Storage id of this manufacture's facility. + """ self._product_unit_cost = product_unit_cost self._storage_id = storage_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index 433724a5e..18e45096a 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -8,23 +8,17 @@ from .skumodel import SkuDataModel -# NOTE: one sku one seller @node("seller") class SellerDataModel(SkuDataModel): - # reward states + """Data model for seller unit.""" total_sold = NodeAttribute(AttributeType.UInt) - # from config backlog_ratio = NodeAttribute(AttributeType.Float) - # action states unit_price = NodeAttribute(AttributeType.UInt) - # sale_gamma = NodeAttribute(AttributeType.UInt) - # per tick state, we can use this to support "sale hist" feature in original code. - # original there is only sold state, we add a demand here demand = NodeAttribute(AttributeType.UInt) sold = NodeAttribute(AttributeType.UInt) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py b/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py index 166471f8e..1029c1d94 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py @@ -9,6 +9,7 @@ class SkuDataModel(DataModelBase): + """Data model for sku related unit.""" # Product id of this consumer belongs to. product_id = NodeAttribute(AttributeType.UInt) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py index cbfccb380..739ac9121 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/storage.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/storage.py @@ -10,11 +10,12 @@ @node("storage") class StorageDataModel(DataModelBase): + """Data model for storage unit.""" unit_storage_cost = NodeAttribute(AttributeType.UInt) remaining_space = NodeAttribute(AttributeType.UInt) capacity = NodeAttribute(AttributeType.UInt) - # original is stock_levels, used to save product and its number + # original is , used to save product and its number product_list = NodeAttribute(AttributeType.UInt, 1, is_list=True) product_number = NodeAttribute(AttributeType.UInt, 1, is_list=True) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py index 41fb2d664..70ef849c6 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py @@ -30,7 +30,6 @@ class VehicleDataModel(DataModelBase): # Steps to destination. steps = NodeAttribute(AttributeType.UInt) - # from config unit_transport_cost = NodeAttribute(AttributeType.UInt) position = NodeAttribute(AttributeType.Int, 2) diff --git a/maro/simulator/scenarios/supply_chain/units/order.py b/maro/simulator/scenarios/supply_chain/units/order.py index 8c153ac2b..47a8335d0 100644 --- a/maro/simulator/scenarios/supply_chain/units/order.py +++ b/maro/simulator/scenarios/supply_chain/units/order.py @@ -2,9 +2,6 @@ # Licensed under the MIT license. -# TODO: original code included the order in raw state, but calculate the price in final state -# so we do not need to put it in the frame, just calculate the total_price per tick/step. - from collections import namedtuple Order = namedtuple("Order", ("destination", "product_id", "quantity", "vlt")) diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index 3208efbce..18e8e6688 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -166,7 +166,7 @@ def step(self, tick: int): self.data_model.position[:] = self.path[self.location] else: - # avoid update under idle state. + # Avoid update under idle state. if self.location > 0: # try to unload if self.payload > 0: From 0361dcfc63fddad6dc8cec66ec19080a7a04e536 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 14:02:17 +0800 Subject: [PATCH 103/482] add missing exported class --- maro/simulator/scenarios/supply_chain/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index edce7834e..784206535 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -6,5 +6,5 @@ from .datamodels import ( ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel ) -from .facilities import RetailerFacility, SupplierFacility, WarehouseFacility -from .units import ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, VehicleUnit +from .facilities import RetailerFacility, SupplierFacility, WarehouseFacility, FacilityBase +from .units import ConsumerUnit, DistributionUnit, ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, VehicleUnit From fa6b40cb5cb90342b5c7a55e055c3a36bd635885 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 15:06:22 +0800 Subject: [PATCH 104/482] add requirements --- requirements.dev.txt | 3 ++- tests/requirements.test.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index e10567083..beec614f6 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -41,4 +41,5 @@ tqdm==4.51.0 editorconfig-checker aria2p==0.9.1 prompt_toolkit==2.0.10 -stringcase==1.2.0 \ No newline at end of file +stringcase==1.2.0 +tcod \ No newline at end of file diff --git a/tests/requirements.test.txt b/tests/requirements.test.txt index 72959d5da..18542fd16 100644 --- a/tests/requirements.test.txt +++ b/tests/requirements.test.txt @@ -20,3 +20,4 @@ paramiko==2.7.2 pytz==2019.3 aria2p==0.9.1 kubernetes +tcod \ No newline at end of file From 1cdabf178f3c50452206b2cbee377f9475ac2557 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 17:25:13 +0800 Subject: [PATCH 105/482] lint fix --- maro/simulator/scenarios/supply_chain/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index 784206535..03990fd5c 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -6,5 +6,5 @@ from .datamodels import ( ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel ) -from .facilities import RetailerFacility, SupplierFacility, WarehouseFacility, FacilityBase +from .facilities import FacilityBase, RetailerFacility, SupplierFacility, WarehouseFacility from .units import ConsumerUnit, DistributionUnit, ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, VehicleUnit From d8293f8c8ccf8ac4f96e8073a91f07f35e42eaec Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 18:24:26 +0800 Subject: [PATCH 106/482] remove tcod, use networkx instead --- .../hello_world/supply_chain/font/Lisence | 25 -- .../supply_chain/font/dejavu10x10_gs_tc.png | Bin 8439 -> 0 bytes examples/hello_world/supply_chain/hello.py | 311 +----------------- examples/hello_world/supply_chain/pf_test.py | 1 - .../hello_world/supply_chain/state_shaping.py | 121 ------- .../simulator/scenarios/supply_chain/world.py | 40 +-- requirements.dev.txt | 1 - setup.py | 3 +- tests/requirements.test.txt | 1 - 9 files changed, 30 insertions(+), 473 deletions(-) delete mode 100644 examples/hello_world/supply_chain/font/Lisence delete mode 100644 examples/hello_world/supply_chain/font/dejavu10x10_gs_tc.png delete mode 100644 examples/hello_world/supply_chain/state_shaping.py diff --git a/examples/hello_world/supply_chain/font/Lisence b/examples/hello_world/supply_chain/font/Lisence deleted file mode 100644 index b407f8da4..000000000 --- a/examples/hello_world/supply_chain/font/Lisence +++ /dev/null @@ -1,25 +0,0 @@ -BSD 2-Clause License - -Copyright (c) 2009-2020, Kyle Stewart and the python-tcod contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/hello_world/supply_chain/font/dejavu10x10_gs_tc.png b/examples/hello_world/supply_chain/font/dejavu10x10_gs_tc.png deleted file mode 100644 index ea3adbe8661bf28ac1199ce662588fa4d9317162..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8439 zcmch5g;y2b_xB)3mvl>9Qo6f4y}*S_mq-f;(v5U?cXuNxCEf5yhct+UbO`+B`~DH{ zS!?FZtT|`T-utub?1=a33Rvi5=l}pk^0{SE{Gj?Wr)e6c7Zo#Akq62bfLS3;_2yUb@Qn@R-Ly1$x!8dO0b=UT}U(XBZ*`d`0MWO%PN; z4Ils{y<(+H0ZDm;fShy&Eg&BWFderr-vQpU0j7-p`}06R&RvE#0$`9#Lx_-{08ryw zhRXuhB0%-DPNWGJCEbsTK>atMcM2Dy5kSQRIHBPo%m9)%U^+ra z=K*}p00`tSbVdKZtHnKF0X3D;DB2DYk_$9IWphT+)n%t*no=TuN5F6P$}Cfoz27r~ zlsk|Y=hx)~0OThSg3?|-c}!u~PEGO0wql#G{q8}2p*1((dET3@asq+da`l^iX60xi z3lKpLaCk1HKSi=O#>oG28}+dfN3sRTKU&efbo!reH0of z^_%qSKH1;xV!1bkFVbOr!pSqQuKSd+vsDgeml2eQ;lkze(Z zu=XNT^u1o|!}w>)|5Y0DqhA_V8vR26#b;CI>L6*R(8+pA7E_M@Bq@1&q1K^EPT1`I zur_QlC!7Z})Z9L%u0S*-sS#9sGn$of1oKEZT`VebM#vpapCS@o6oh&-oB&FzlEA0P zr5UMB4O67N5c~W}D9lj4BSB;Y;12p1+NHpi9M}jI`wz24roCK%?5lhs#>@v-uFN>1 z{H&SJ&17QH`~?RyRn|BwapE$=oV|x6C_F#xIoXE>q#FcT(UL628)&OBtA@zxb*d3N zBd#r`8{Cob0_A$q_>tVDsm-XQ6yd6H^(Dn}Y6=MNn-w$xG{mpaen#4)a+PL=jyLx| zNX)sp!r+RmbPITK_#C+2;W9r2Ab1fn+>Gto?|ZQTzsds&7?VIt4z9wyT`kSY?;n21uM$b zTrV%se!~~1FI`j6s#ymOE63;kk|{J9Or)ezQd5{RPi9SQeQQ&9=-TmSJ%vg=ZAN!) z{7B(2?GW+G9TO{*FnBoJiitFf1ck(k)SaX*UA2^SK8kc$n^S=6IQ<|4C&Lj2XK7{e zAxs!UPf<=`NFit8(5|Q~t=y=@fL+4Iwc{#`wR~&kVLVzp6=?sZR>3NB-XCj`Ygtu@ zl!sMXl{;!n6#MBtMIh@9s19hz6hreHOm@14s4*-^n><))?69grzXbCAU}%_J_HkqB zOn;*6j1sG?C=in2QRdNY(!5OmD1ow*?tGW+s*ZNRRF-z>7r8-j#=Ol;UdpT5&Shz6 zSDBiY3a?VEf>tRUck|btl;xCm%XR8K24YJMD>E80Dp{rYmxgP>mX|}YxA3_zK1?*Yk6y%rBdUreyQGR zYuBd`eIdOPI85KDNxmkhhJK;5rnBr#S-X0YdR6X%)j<8<*3s6AZ>05mOUSFWtq!fe zW>)6JR^J9|2Tqf^lYBdTW1hrNu)>zHt0?s;{al(q6A0@FZ)8qopGx=+UZoy!Siz@p zrxXcWU$ozut%}QY8rEuD9R{LqlXarid9MExCa3tu~ zEY^Q3aj(2JJ0ljq5&xXGBfcXlu))2dXEGts`)%agxW<5Eu1UzIj6dDWhZnDxS)eR{ z9>EGRB2X&O;ML=umk&)(tc38YpJ_FK_%%<|8MFUc6PP^n)i zvZ#3_{z>TK^MnlYl(6o#e`TzQ?^4B5UC$EECCYT8cHkBh?&bQ6qHfBrugdDdTi*e(xr#m`b>)~D2-N@x*vNcBgTYMt8(8> zo=h~2t;P#vpXI%r;PvBeCAOA?s_3#&!uPwYZG{>|ojq64cL-m<3)Mlc7Hx}cWpVha zQFWr5R_oWz;H734Xxujz)YpPwcZ>l;-wW|QURyjd(tva5ZMK*_gyxWo6GlYV608!Z z5&ADNt$FN`hdGY*WUM7OS5*CI|1kY=?&Hz^23xKHc4}qXej4^CtmTzvtWBNflj|F~ zRgSbtrq#~iR+;vq9?`Zg%d*j_Nf8O{V(lyK2W|e-x%C#a#k)_lr{vyUH@ts-t{QaR zK8cThUxgvS)P8>aY5A!1*mNsV8&P?5m-^^nRHN?Td@_tUhxmj-huyl^{@+i^IWcX` zxP^De3fRMMzdz@kxwB#MW5HswhqmdQWIcC}ZqYVxsj!4s+@ig!w@~Kax=~tDa@a~| zUS!H=X=Oedm>YCxOo-1e{NC{Tr?C1KC!1GP&r|tQd(>v-C}=*(>S8)NCnd+%kNtkl zbhWV6`@mvq(-z0^tOM6|YfkPZ>OiDRWXkZO&Gn*jbq`;+O*gYn*u>$t>u=HLq-d=AKt^0jue3}~X zTy|$Xt?AO}GP#mGky@5n5Bn)$@VvHkw&ri!>z;o(yQXl991=YJoNyh7Bl9{gBW^qt zEA%*5P+Ul8TjKN6(Z4d8=?~Kkxx~+f&*QbkL>zv1KaUruX47X2(k{}HdK`R8o__Zp zlh&;c^$$^fANAnjGQQGzJn3^^bEiBj+QiY=^`Z7>da=H5xYn8}drTrtN_s&;OuzhR z!Nax+)|*IHQfg8F&=BkM2u%XtsVx*W)d0YY9smM@0pR`xoR0wD3nu{lHU$9TbO0cB zOf(sk2LK{GC0QwLkCi`02KEF$$@7+54Odo#G58x;`T>W+VitqhW}Ito#d8F;i5n@b zz8|WA#8?n@E|{dnGT$30K}Qr$DTK@o5j_H7nXRh8RK`j_J`24mQBsk~O~IOctR&}U zfOy(JN9e%StyP6sv?uJN2+zTUJNxFfj|=v&C6*Mf>O3aWt3b(KUNtG)wk|0Mo>GZs zai@45M~bo;nhpUyQIs<#<~>&2rGi#Pav>8Od0%jf6H^K|akSWs1ddHC$CaY2l=z{J z1{d|tN1XLBqm&dqL@|htC@P~=X^WRS!c~9(U9Jc!(nm22PD+XY{3KHK<)GTI>&K_c z%KenyLPh$+Ds2`^$K=g30kwHvH5g2(NRp+BDe@ktem;0dP%l;Lht`-p?VD;6xjF{u z>-uKmNvwHZOTE@x;_n?|@?kl!fDj*X6A0ey7fOToFc>SM z2(Oi*yOfg_Nje0<%%=fw^TJqU`F#^pWWoCpbwWb|Bv9&jFFKWxMvACG?o7mvT9j7^ znvS3%%Grpbpz?iKi1P@A*nM-0k%hpzE7qUO`(Jh4lUG6t2+>>--ON1Pd88z5gW_m2 zqVm~)f&ygi7$Cn|k>-Yf(8CUqHiafK#4$xex)a|aBlFXtLDDtCRT|6%@ght&$)g4X z5(wnbkX~i9+NvTmv|k-DxUv!z;o#Zq?@f*Me}D5eq4tFPgT^*AV*hsGvK%@o_J*2mGV2X{AOSKwV9x-~iT;wI2cSlNc zmsVEk^J?P<)Av2D_=`%AvP2S+6c~(9MF`!=yi)}ROB7>{q@`Vb zHvT{|WX@InP{LCWQNqthV^l^Ixms&oE}qI=^FK_@KrIKph7S``B#8Y*)Xr#nB#-V9YXy}YZv_!`t)V$TcTnU z#+1R0YY)-n&Z|sjUP0z42aJl6Wd9RB1L;Z&>~a@VG|8T$vq5a65XFCRx-g_;qF0C( zSVoLPqSmhb?89=Gc7?+4qi^_d5@h&%8wyeDuxDj8}tr*&#T7v&MYk0vZ*x5 zk`ip^t{#Oeof=hi`cm}l@}66t9UVD15z_8r{#pt(MEb7>1%2iRqyf&4)47y!!%8V2 zc$3MlnoS}LuaB%>95NZEFN=GMH6h(&z32YOL~_u?;?hzE)Q5<|bSUd8XS9>xA9g{a z2-(EZ-Tn{^Vs=ARrGy(Yu5^r z4rWlB|Fh>1DZP|0`>MG>d!4-)T5^9g5VD>OO}GjZ+mUwWR~A%2MGkIhX`vWy|K}RK z^Fc3GzR0+4@s#RP^lgm18L1FijCwxu)H?4MO&5O0va~_u^G5&N7XfasbnFi5CQ*B0 zNsA$3MI3L*$B4pmn@ls3TAuO3*>cu#&ua%tAH~C1$ax;0TGOVjt?jF)Ki6J%c6JF7 z;!ts?(xiz|0#JFE2Pf`FCEPX3W)4@sZX6a0cj)ECIA;g8oR|o}P1~Lu$JhJ<0#*+U zYV+HFJ&c*n&Ml01>O22-PLc__L)pU`=@hOzt1^tyS2K#K+{_Gj`^m=g-_G@%G`=%q zJnk+2QA$yRDAH%tgPnbZnH67>|F2r8EPt4oyCZA1Qj@ChnSQ=4DuC-!va#9o)3k6s zzDca+!j@5D!|$&wjLR6LDU@lMXV}Q>Nch;#p4$ z7}c|vTI`5tX>UE`6%OYeLo7qAx48zVQK7R)T z5m6o@Yu@_ZnJrZ79Zcm^m6uEYt)~=m0~1|BLW1w(&C~PanYxcXqjL0i-oYVz zhy#T%HaqKy4?J~zNNR+YHpXu++;PKBO-8`!{@Nl3~tiV(K%giLW$Ax^?f=& zKaap?{Iu2<$QI(jfrrA!#|N_4>2+eL!4>DSs@TQe~+rE^;< z%b(PM*!}(u8q1DgEjX&GFi!m*j>?wWcyFh}93OAbhBr=5Pd9q}Uo!dZghfQ6qM~N6 zx3{-Ju|SE_jJ<3oGx;bfDU%pALdCZPKV9sOf^WwQHBDJj>I+}iYY??`e3^9{3x14t zuC=gFKkYipsHv&x>Jm3!F4P#D7gbeNm6nF~_fpU8Pvu~&b$a|Ye=yKDFi@MfcXSL} zMjOOHM_*Z8g~kVru;Ya^r@a%kQACqM~AAl4p~tTW9QGXP3omlUVYrSSjiI!|$pO-0hy1 z`~CgWu{%0-#@$xaxj(CPL2_{T(#FOVNjeuNmZhcTmoHzgudg#pGfFFL^nHDOjf{-+ z^z=(<#LITuusH-FS)Sf&sdaC`ZVpw5sYDG~V6=5%<%7C*L+fsF*QD1qD5R z^#+!xq!AGj>1b>F+--$DJw0{0pFJPf_0$@5-Yi??nT>y1Zrq!ec(HeIczr77`*1Zf zGIBIu78us^??Ocw@qqRrvGc&?joEh@x}Q5i(zyqqioEX5&0=@hO}=CL<4_8J&Gq;d zh(bnAuA;0Qa12Uhpsybq8VbUeG`d@%RYt@Le{X2Gw!R+onKLRf5*Y=B!>|oEZW#0- zkKD84V}oYv*iTf^V=`ggH$s! zGr>LPD(hXIE~ciY9v*Gw<>dth1(lU!JAc9QkdK$w=lT9%vDRpZgd`g{-kZu1A@;Qi z|1+lu(S*TX>o?cbI9OXBf`GKHhM^J?5}JVFWp6xfzDAGH_>+nX#wgyt;OW!trn@;k zyHUq~5ra@;<5+!OQBll{bTHu!3=B9pI8en|ovgGB$5Iwq|H{qH9U2tnTf!yZ4eii;=i}2%g3WY$!4$NjzQ67s^HbI{lv&W$sg+Ao3u8M|3*>B{k2B zLnUh6^SWUhEF|FOgG^*#axGKOQLent^*()r)Q5+45IqfZO%J9)1oeiGX>Yji%B~5^M7X*-gZBpcK-FqKDY;? zJLumgJ$_GPDesDkia?>ehIhadn`!E1$Cs+&;^MBZ>;q3Y{YJgn0LojRnYFcl|7mGR zU8>DfkdO$v{aHF)@6^Z@6#`vt5wwT)gIpdp{I0#ezCIA#ZtvTEvTNBI#7$p59!^fq z-M5P5Kg-TAqt~Zb&Ta=5CT}Ji=5C)UT@=j6u}Q9;BIn=MJNkr7!FqFhODDbl5m!&NZmdnC#}w9%uI{t;dG`6KdCafx>CADA6m|9JTx(eMHMP8 z=Vkb<0KebaNBz>mPj~F7Ej#;(E>coZL4qE_+F8mR8T6gM!M+biq@<7V55u3-B&XRgzGIgTeQ_ zPU+1d5{fK=$W+)qU)l#56SFJCW=` z*^-hpKi&fF*ggTRu%t!XYB)XvzRjE@H>Qrns>Wd%jLkL^!EwV{BbZ zhAKm!>nkr-G*s$w%mL~aUa}Zz0rPKSU_maPH(l#DFQ+L=<=$kW? zq~(LLubdc0MPp@YXKF>nKJ7_8Lx!Ef8f)J(*L=_n#(qVB$=3F6Nz<=qlQY_R&7GSn z4kPH7*R(LYX_~z?v(9!vT8WK5`}W&eYl77m&Xm_u0Yi+){NHM^tBB7wnvXqGbFv^ej~TH=^X z4BbIuPIwV&9T^cDdED`dIZKUzj86duvsb06_WfhyJW~9P{Kj-ntFWm2WB5ZyxOCD3 zddlDf+2IXt?s~+>Dv;094N|d*?H>;n>%m}S4fZ3b(<5-X2rT^1 z+gtfl5DXZP|>qn0!4Hlj()%!GX?j4wsGM3ec>Ohf;x0X*}__w^@io9G#)d{?{9JsimLI}QK3ZW(P@h$6>- z_9j8nb_h3(*4;9xmh-&x#M46)A)(AP^`1!>M7s-Bdq_(J9&TJ;BW#-cD!o_Nw6t_IY*4tdO&5b`vz%xBoO{ooMM*Rd>Y;tGFZjxJGIRIl98S&>z4i0QSbCTvz_b^dSz%#bI z`$kXYE!`MX^!>)livHbhRAJp=DAP?hwKKK%^ah36JVXiX5S%LoKLtus1A4)5L@c}& zB&qtS(9Tfr8U_@I{_+WZhyUIT%8-}I(KF!!W>NZ#>syj#d$ 0: - x, y = pos_list[index] - - if x >= 0 and y >= 0: - console.print(x, y, "V", (0, 255, 0), (128, 128, index)) - - def state_shaping(self): - print(self._state_shaping.shape()) - - def show_summary(self): - pp = pprint.PrettyPrinter(indent=2, depth=8) - - print(f"{bcolors.HEADER}env summary:{bcolors.ENDC}") - - pp.pprint(self.env.summary) - - def show_states(self): - self.show_manufacture_states() - - self.show_vehicle_states() - - self.show_demand_states() - - self.show_storage_states() - - def show_storage_states(self): - storages = self.env.snapshot_list["storage"] - storage_number = len(storages) - - storage_features = ("id", "facility_id", "remaining_space", "capacity", "unit_storage_cost") - storage_states = storages[self.env.frame_index::storage_features].flatten().reshape(storage_number, -1).astype(np.int) - - storage_all_states = [] - - for index, state in enumerate(storage_states): - product_list = storages[self.env.frame_index:index:"product_list"].flatten().astype(np.int) - product_number = storages[self.env.frame_index:index:"product_number"].flatten().astype(np.int) - - storage_all_states.append(list(state) + [product_list, product_number]) - - print(f"{bcolors.HEADER}Storage states:{bcolors.ENDC}") - print(tabulate(storage_all_states, storage_features + ("product_list", "product_number"))) - - def show_demand_states(self): - sellers = self.env.snapshot_list["seller"] - seller_number = len(sellers) - - seller_features = ("id", "facility_id", "product_id", "demand", "sold", "total_sold", "sale_gamma") - seller_states = sellers[self.env.frame_index::seller_features].flatten().reshape(seller_number, -1).astype(np.int) - - print(f"{bcolors.HEADER}Demand states:{bcolors.ENDC}") - print(tabulate(seller_states, seller_features)) - - def show_vehicle_states(self): - vehicles = self.env.snapshot_list["vehicle"] - - vehicle_number = len(vehicles) - - vehicle_features = ("id", "facility_id", "steps", "patient", "source", "destination", "payload", "product_id") - - vehicle_states = vehicles[self.env.frame_index::vehicle_features].flatten().reshape(vehicle_number, -1).astype(np.int) - - print(f"{bcolors.HEADER}Vehicle states:{bcolors.ENDC}") - - print(tabulate(vehicle_states, vehicle_features)) - - def show_manufacture_states(self): - # This function is used to debug manufacturing logic. - manufactures = self.env.snapshot_list["manufacture"] - storages = self.env.snapshot_list["storage"] - - manufacture_unit_number = len(manufactures) - storage_unit_number = len(storages) - - # manufacture number for current tick - features = ("id", "facility_id", "storage_id", "product_id", "product_unit_cost", "production_rate", "manufacturing_number") - states = manufactures[self.env.frame_index::features].flatten().reshape(manufacture_unit_number, -1).astype(np.int) - - # show bom - bom_info = [] - for state in states: - output_product_id = state[3] - - # NOTE: we are using internal data to simplify the code - sku = self.env._business_engine.world.get_sku_by_id(output_product_id) - - # this sku need source material - if len(sku.bom) > 0: - bom_info.append([output_product_id, [s for s in sku.bom.keys()], [s for s in sku.bom.values()]]) - else: - bom_info.append([output_product_id, "None", "None"]) - - print(f"{bcolors.HEADER}SKU bom info:{bcolors.ENDC}") - print(tabulate(bom_info, ("product id", "source materials", "source material cost per lot"))) - - # show manufacture unit data - print(f"{bcolors.HEADER}Manufacture states:{bcolors.ENDC}") - print(tabulate(states, headers=features)) - - # show storage state to see if product changed - print(f"{bcolors.HEADER}Manufacture storage states:{bcolors.ENDC}") - - storage_features = ["id", "remaining_space", "capacity"] - storage_states_summary = [] - - for state in states: - # DO NOTE: this is id, CANNOT be used to query - facility_id = state[1] - storage_id = state[2] - - storage_node_name, storage_index = self.env.summary["node_mapping"]["mapping"][storage_id] - - # NOTE: we cannot mix list and normal attribute to query state, - # we have to query one by one - product_list = storages[self.env.frame_index:storage_index:"product_list"].flatten().astype(np.int) - product_number = storages[self.env.frame_index:storage_index:"product_number"].flatten().astype(np.int) - storage_states = storages[self.env.frame_index:storage_index:storage_features].flatten().astype(np.int) - - cur_storage_states = [] - cur_storage_states.extend(storage_states) - cur_storage_states.append(product_list) - cur_storage_states.append(product_number) - - storage_states_summary.append(cur_storage_states) - - print(tabulate(storage_states_summary, headers=storage_features+["product_list", "product_number"])) - - def choose_action(self): - action = {} - - consumer_actions = self.choose_consumer_action() - - for consumer_action in consumer_actions: - action[consumer_action.id] = consumer_action - - print(consumer_actions) - - return action - - def choose_consumer_action(self): - # dummy actions - - # push consumers to generate order - - actions = [] - - # check if lack of any source material - storages = self.env.snapshot_list["storage"] - - for storage_index in range(len(storages)): - product_list = storages[self.env.frame_index:storage_index:"product_list"].flatten().astype(np.int) - product_number = storages[self.env.frame_index:storage_index:"product_number"].flatten().astype(np.int) - - for product_id, product_number in zip(product_list, product_number): - if product_number <= 0: - facility_id = storages[self.env.frame_index:storage_index:"facility_id"].flatten()[0] - - for consumer in self.env.summary["node_mapping"]["detail"][facility_id]["units"]["consumers"]: - if consumer["sku_id"] == product_id: - sources = self.env.snapshot_list["consumer"][self.env.frame_index:consumer["node_index"]:"sources"] - - if sources is not None: - upstreams = sources.flatten().astype(np.int) - - actions.append(ConsumerAction(consumer["id"], upstreams[0], 30, 1)) - - return actions def main(): start_tick = 0 durations = 100 env = Env(scenario="supply_chain", topology="sample1", start_tick=start_tick, durations=durations) + total_episodes = 10 + + matrics = None + is_done = False - irenv = InteractiveRenderaleEnv(env) + for ep in range(total_episodes): + print("Current episode:", ep) - for ep in range(1): - irenv.start() + while not is_done: + # This scenario require a dictionary of actions, which the key is the unit id, value if the action. + matrics, _, is_done = env.step(None) env.reset() diff --git a/examples/hello_world/supply_chain/pf_test.py b/examples/hello_world/supply_chain/pf_test.py index b3abd5fad..e28a0c01d 100644 --- a/examples/hello_world/supply_chain/pf_test.py +++ b/examples/hello_world/supply_chain/pf_test.py @@ -2,7 +2,6 @@ import sys import numpy as np -import tcod import pprint from tabulate import tabulate diff --git a/examples/hello_world/supply_chain/state_shaping.py b/examples/hello_world/supply_chain/state_shaping.py deleted file mode 100644 index 6bce62c84..000000000 --- a/examples/hello_world/supply_chain/state_shaping.py +++ /dev/null @@ -1,121 +0,0 @@ - -""" -NOTE: this is used as a state shaping example, without maro.rl - -states we used: - -facility + sku: - -is_positive_balance - -is_over_stock -is_out_of_stock -is_below_rop -echelon_level - -sale_std -storage_capacity -storage_utilization -sale_hist -consumption_hist -total_backlog_demand -inventory_in_stock -inventory_in_distribution -inventory_in_transit -inventory_estimated -inventory_rop - -sku_price -sku_cost - -""" - -import numpy as np - -from maro.simulator import Env - - -class SupplyChainStateShaping: - def __init__(self, env: Env): - self._env = env - - def shape(self): - """ - result is a dictionary contains agent type and states of each unit(agent): - { - "producer": { - "manufacture id" : [long list of states] - }, - "consumer": { - "consumer id": [long list of states] - } - } - - """ - - is_positive_balance = self._origin_group_by_sku_is_positive_balance() - # - return is_positive_balance - - def _origin_group_by_sku_is_positive_balance(self): - # original code collect states of facilities and sku related units (manufacture, seller, consumer), - # other's states is added to facility - result = [] - - filter = lambda x: 1 if x > 1 else 0 - - cur_frame_index = self._env.frame_index - - features = ("id", "facility_id", "balance_sheet_profit", "balance_sheet_loss") - sku_related_features = features + ("product_id",) - - # facility balance sheet - facility_nodes = self._env.snapshot_list["facility"] - facility_balance_sheet = facility_nodes[cur_frame_index::features] - facility_balance_sheet = facility_balance_sheet.flatten().reshape(len(facility_nodes), -1).astype(np.int) - - facility_balance_sheet_total = facility_balance_sheet[:, 2] + facility_balance_sheet[:, 3] - - facility_is_positive_balance = list(map(filter, facility_balance_sheet_total)) - - result.extend(facility_is_positive_balance) - - # then each sku group for each facility - manufacture_nodes = self._env.snapshot_list["manufacture"] - seller_nodes = self._env.snapshot_list["seller"] - consumer_nodes = self._env.snapshot_list["consumer"] - - manufacture_balance_sheet = manufacture_nodes[cur_frame_index::sku_related_features].flatten().reshape(len(manufacture_nodes), -1).astype(np.int) - seller_balance_sheet = seller_nodes[cur_frame_index::sku_related_features].flatten().reshape(len(seller_nodes), -1).astype(np.int) - consumer_balance_sheet = consumer_nodes[cur_frame_index::sku_related_features].flatten().reshape(len(consumer_nodes), -1).astype(np.int) - - # - for facility_id in facility_balance_sheet[:, 0]: - manufacture_states = manufacture_balance_sheet[manufacture_balance_sheet[:, 1] == facility_id] - seller_states = seller_balance_sheet[seller_balance_sheet[:, 1] == facility_id] - consumer_states = consumer_balance_sheet[consumer_balance_sheet[:, 1] == facility_id] - - length = max(len(manufacture_states), len(seller_states), len(consumer_states)) - - # this facility does not have any sku related units - if length == 0: - continue - - sku_balance_sheet_total = np.zeros((length, ), dtype=np.int) - - if len(manufacture_states) > 0: - sku_balance_sheet_total += manufacture_states[:, 2] + manufacture_states[:, 3] - - if len(seller_states) > 0: - sku_balance_sheet_total += seller_states[:, 2] + seller_states[:, 3] - - if len(consumer_states) > 0: - sku_balance_sheet_total += consumer_states[:, 2] + consumer_states[:, 3] - - sku_is_positive_balance = list(map(filter, sku_balance_sheet_total)) - - # np.stack() - - result.append(sku_is_positive_balance) - - return result diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index c14c63b42..a719bb1d5 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -5,8 +5,8 @@ from collections import namedtuple from typing import List, Tuple, Union +import networkx as nx import numpy as np -from tcod.path import AStar from maro.backends.frame import FrameBase @@ -40,8 +40,8 @@ def __init__(self): # Entity id counter, every unit and facility have unique id. self._id_counter = 1 - # Path finder for production transport. - self._path_finder: AStar = None + # Grid of the world + self._graph: nx.Graph = None # Sku id to name mapping, used for querying. self._sku_id2name_mapping = {} @@ -122,7 +122,7 @@ def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> Lis Returns: List[Tuple[int, int]]: List of (x, y) position to target. """ - return self._path_finder.get_path(int(start_x), int(start_y), int(goal_x), int(goal_y)) + return nx.astar_path(self._graph, source=(start_x, start_y), target=(goal_x, goal_y), weight="cost") def build(self, configs: SupplyChainConfiguration, snapshot_number: int, durations: int): """Build world with configurations. @@ -218,27 +218,27 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio grid_width, grid_height = grid_config["size"] - # Travel cost for a star path finder, 0 means block, > 1 means the cost travel to that cell - # current all traversable cell's cost will be 1. - cost_grid = np.ones(shape=(grid_width, grid_height), dtype=np.int8) + # Build our graph base one settings. + # This will create a full connect graph. + self._graph = nx.grid_2d_graph(grid_width, grid_height) - # Add blocks to grid. - for facility_name, facility_pos in grid_config["facilities"].items(): + # All edge weight will be 1 by default. + edge_weights = {e: 1 for e in self._graph.edges()} + + # Facility to cell will have 1 weight, cell to facility will have 4 cost. + for facility_name, pos in grid_config["facilities"].items(): facility_id = self._facility_name2id_mapping[facility_name] facility = self.facilities[facility_id] + facility.x = pos[0] + facility.y = pos[1] + pos = tuple(pos) - facility.x = facility_pos[0] - facility.y = facility_pos[1] - - # Facility cannot be a block, or we cannot find path to it, - # but we can give it a big cost - cost_grid[facility.x, facility.y] = 120 - - for block_pos in grid_config["blocks"].values(): - cost_grid[block_pos[0], block_pos[1]] = 0 + # Neighbors to facility will have hight cost. + for npos in ((pos[0]-1, pos[1]), (pos[0]+1, pos[1]), (pos[0], pos[1]-1), (pos[0], pos[1]+1)): + if npos[0] >= 0 and npos[0] < grid_width and npos[1] >= 0 and npos[1] < grid_height: + edge_weights[(npos, pos)] = 4 - # 0 for 2nd parameters means disable diagonal movement, so just up, right, down or left. - self._path_finder = AStar(cost_grid, 0) + nx.set_edge_attributes(self._graph, edge_weights, "cost") def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): unit = unit_type() diff --git a/requirements.dev.txt b/requirements.dev.txt index beec614f6..3a1856653 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -42,4 +42,3 @@ editorconfig-checker aria2p==0.9.1 prompt_toolkit==2.0.10 stringcase==1.2.0 -tcod \ No newline at end of file diff --git a/setup.py b/setup.py index a923b32b1..5ed079722 100644 --- a/setup.py +++ b/setup.py @@ -140,8 +140,7 @@ "paramiko==2.7.2", "kubernetes==12.0.1", "prompt_toolkit==2.0.10", - "stringcase==1.2.0", - "tcod==11.19.3" + "stringcase==1.2.0" ], entry_points={ "console_scripts": [ diff --git a/tests/requirements.test.txt b/tests/requirements.test.txt index 18542fd16..72959d5da 100644 --- a/tests/requirements.test.txt +++ b/tests/requirements.test.txt @@ -20,4 +20,3 @@ paramiko==2.7.2 pytz==2019.3 aria2p==0.9.1 kubernetes -tcod \ No newline at end of file From 488ba300bf7011ecd81f90d142d97f7521f48f1e Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 19:01:09 +0800 Subject: [PATCH 107/482] add networkx as dependency --- requirements.dev.txt | 1 + setup.py | 3 ++- tests/requirements.test.txt | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index 3a1856653..8bd0a5b19 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -42,3 +42,4 @@ editorconfig-checker aria2p==0.9.1 prompt_toolkit==2.0.10 stringcase==1.2.0 +networkx==2.4 \ No newline at end of file diff --git a/setup.py b/setup.py index 5ed079722..e242d1a53 100644 --- a/setup.py +++ b/setup.py @@ -140,7 +140,8 @@ "paramiko==2.7.2", "kubernetes==12.0.1", "prompt_toolkit==2.0.10", - "stringcase==1.2.0" + "stringcase==1.2.0", + "networkx==2.4" ], entry_points={ "console_scripts": [ diff --git a/tests/requirements.test.txt b/tests/requirements.test.txt index 72959d5da..002db13ae 100644 --- a/tests/requirements.test.txt +++ b/tests/requirements.test.txt @@ -20,3 +20,4 @@ paramiko==2.7.2 pytz==2019.3 aria2p==0.9.1 kubernetes +networkx==2.4 \ No newline at end of file From 379cb6660408bb928ec7c50205f7876e33783cea Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 22 Mar 2021 19:13:35 +0800 Subject: [PATCH 108/482] lint issue --- maro/simulator/scenarios/supply_chain/world.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index a719bb1d5..e448791bc 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -6,7 +6,6 @@ from typing import List, Tuple, Union import networkx as nx -import numpy as np from maro.backends.frame import FrameBase @@ -234,7 +233,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio pos = tuple(pos) # Neighbors to facility will have hight cost. - for npos in ((pos[0]-1, pos[1]), (pos[0]+1, pos[1]), (pos[0], pos[1]-1), (pos[0], pos[1]+1)): + for npos in ((pos[0] - 1, pos[1]), (pos[0] + 1, pos[1]), (pos[0], pos[1] - 1), (pos[0], pos[1] + 1)): if npos[0] >= 0 and npos[0] < grid_width and npos[1] >= 0 and npos[1] < grid_height: edge_weights[(npos, pos)] = 4 From f9ec855274f41dbea4a20220217d46ebc23af756 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 23 Mar 2021 13:59:06 +0800 Subject: [PATCH 109/482] rename entity to unit --- .../scenarios/supply_chain/business_engine.py | 2 +- .../simulator/scenarios/supply_chain/world.py | 34 +++++++------- tests/supply_chain/test_supply_chain.py | 46 +++++++++---------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 2c40aa16c..85348f182 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -119,7 +119,7 @@ def _dispatch_action(self): if self._action_cache is not None: # NOTE: we assume that the action is dictionary that key is the unit(agent) id, value is the real action. for unit_id, action_obj in self._action_cache.items(): - entity = self.world.get_entity(unit_id) + entity = self.world.get_unit(unit_id) if entity is not None and issubclass(type(entity), UnitBase): entity.set_action(action_obj) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index e448791bc..d1b70c53e 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -30,8 +30,8 @@ def __init__(self): # Durations of current simulation. self.durations = 0 - # All the entities in the world, include unit and facility. - self.entities = {} + # All the units in the world. + self.units = {} # All the facilities in this world. self.facilities = {} @@ -98,16 +98,16 @@ def get_facility_by_name(self, name: str): """ return self.facilities[self._facility_name2id_mapping[name]] - def get_entity(self, entity_id: int) -> Union[FacilityBase, UnitBase]: - """Get an entity (facility or unit) by id. + def get_unit(self, unit_id: int) -> Union[FacilityBase, UnitBase]: + """Get an unit by id. Args: - entity_id (int): Id to query. + unit_id (int): Id to query. Returns: - Union[FacilityBase, UnitBase]: Entity instance. + Union[FacilityBase, UnitBase]: Unit instance. """ - return self.entities[entity_id] + return self.units[unit_id] def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: """Find path to specified cell. @@ -184,7 +184,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio self.frame = self._build_frame(snapshot_number) # Assign data model instance. - for unit in self.entities.values(): + for unit in self.units.values(): if unit.data_model_name is not None: unit.data_model = getattr(self.frame, unit.data_model_name)[unit.data_model_index] @@ -208,7 +208,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio facility.initialize() # Call initialize method for units. - for unit in self.entities.values(): + for unit in self.units.values(): unit.initialize() # TODO: replace tcod with other lib. @@ -247,7 +247,7 @@ def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBa unit.facility = facility unit.world = self - self.entities[unit.id] = unit + self.units[unit.id] = unit return unit @@ -279,7 +279,7 @@ def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBas unit_instance.parent = parent # Record the id. - self.entities[unit_instance.id] = unit_instance + self.units[unit_instance.id] = unit_instance # Due with data model. data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] @@ -327,7 +327,7 @@ def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBas # Pass the config if there is any. child.parse_configs(config.get("config", {})) - self.entities[child.id] = child + self.units[child.id] = child return children @@ -344,14 +344,14 @@ def get_node_mapping(self): id2index_mapping = {} - for entity_id, entity in self.entities.items(): - if entity.data_model is not None: - id2index_mapping[entity_id] = (entity.data_model_name, entity.data_model_index) + for unit_id, unit in self.units.items(): + if unit.data_model is not None: + id2index_mapping[unit_id] = (unit.data_model_name, unit.data_model_index) else: - id2index_mapping[entity_id] = (None, None) + id2index_mapping[unit_id] = (None, None) return { - "entity_mapping": id2index_mapping, + "unit_mapping": id2index_mapping, "skus": {sku.name: sku.id for sku in self._sku_collection.values()}, "facilities": facility_info_dict } diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index a129df2d7..bcc07876e 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -83,7 +83,7 @@ def test_manufacture_meet_storage_limitation(self): sku3_storage_id = state[5] # try to find sku3's storage from env.summary - sku3_storage_index = env.summary["node_mapping"]["entity_mapping"][sku3_storage_id][1] + sku3_storage_index = env.summary["node_mapping"]["unit_mapping"][sku3_storage_id][1] storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) @@ -189,7 +189,7 @@ def test_manufacture_meet_source_lack(self): sku4_storage_id = state[5] # try to find sku4's storage from env.summary - sku4_storage_index = env.summary["node_mapping"]["entity_mapping"][sku4_storage_id][1] + sku4_storage_index = env.summary["node_mapping"]["unit_mapping"][sku4_storage_id][1] # the storage should be same as initialized (50 + 0). storage_states = storage_nodes[env.frame_index:sku4_storage_index:storage_features].flatten().astype(np.int) @@ -286,7 +286,7 @@ def test_manufacture_meet_avg_storage_limitation(self): sku1_manufacture_id = state[0] sku1_storage_id = state[5] - sku1_storage_index = env.summary["node_mapping"]["entity_mapping"][sku1_storage_id][1] + sku1_storage_index = env.summary["node_mapping"]["unit_mapping"][sku1_storage_id][1] ############################### TICK: 1 ###################################### @@ -374,7 +374,7 @@ def test_storage_take_available(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + storage_unit: StorageUnit = env._business_engine.world.get_unit(storage_unit_id) storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) @@ -445,7 +445,7 @@ def test_storage_try_add_products(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + storage_unit: StorageUnit = env._business_engine.world.get_unit(storage_unit_id) storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) @@ -520,7 +520,7 @@ def test_storage_try_take_products(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + storage_unit: StorageUnit = env._business_engine.world.get_unit(storage_unit_id) storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) @@ -573,7 +573,7 @@ def test_storage_get_product_number(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + storage_unit: StorageUnit = env._business_engine.world.get_unit(storage_unit_id) init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) @@ -626,13 +626,13 @@ def test_consumer_init_state(self): # try to find sku3 consumer sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_consumer_unit = env._business_engine.world.get_unit(sku3_consumer_unit_id) sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] if facility_defail["name"] == "Supplier_SKU3": sku3_supplier_faiclity_id = facility_defail["id"] - sku3_consumer_data_model_index = env.summary["node_mapping"]["entity_mapping"][sku3_consumer_unit_id][1] + sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] # check initial state self.assertEqual(0, sku3_consumer_unit.received) @@ -727,13 +727,13 @@ def test_consumer_action(self): if facility_defail["name"] == "Supplier_SKU1": sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_consumer_unit = env._business_engine.world.get_unit(sku3_consumer_unit_id) sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] if facility_defail["name"] == "Supplier_SKU3": sku3_supplier_faiclity_id = facility_defail["id"] - sku3_consumer_data_model_index = env.summary["node_mapping"]["entity_mapping"][sku3_consumer_unit_id][1] + sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] # zero quantity will be ignore action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 0, 1) @@ -841,13 +841,13 @@ def test_consumer_on_order_reception(self): if facility_defail["name"] == "Supplier_SKU1": sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_consumer_unit = env._business_engine.world.get_unit(sku3_consumer_unit_id) sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] if facility_defail["name"] == "Supplier_SKU3": sku3_supplier_facility_id = facility_defail["id"] - sku3_consumer_data_model_index = env.summary["node_mapping"]["entity_mapping"][sku3_consumer_unit_id][1] + sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1) @@ -892,10 +892,10 @@ def test_vehicle_unit_state(self): vehicle_unit_id: int vehicle_unit_data_model_index: int - for id, info in env.summary["node_mapping"]["entity_mapping"].items(): + for id, info in env.summary["node_mapping"]["unit_mapping"].items(): if info[0] == "vehicle": vehicle_unit_id = id - vehicle_unit = env._business_engine.world.get_entity(id) + vehicle_unit = env._business_engine.world.get_unit(id) vehicle_unit_data_model_index = vehicle_unit.data_model_index break @@ -979,7 +979,7 @@ def test_vehicle_unit_schedule(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) + vehicle_unit = env._business_engine.world.get_unit(v["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1111,7 +1111,7 @@ def test_vehicle_unit_no_patient(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) + vehicle_unit = env._business_engine.world.get_unit(v["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1207,7 +1207,7 @@ def test_vehicle_unit_cannot_unload_at_destination(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) + vehicle_unit = env._business_engine.world.get_unit(v["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1269,7 +1269,7 @@ def test_distribution_unit_initial_state(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": - dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) + dist_unit = env._business_engine.world.get_unit(info["units"]["distribution"]["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1308,7 +1308,7 @@ def test_distribution_unit_dispatch_order(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": - dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) + dist_unit = env._business_engine.world.get_unit(info["units"]["distribution"]["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1385,7 +1385,7 @@ def test_seller_unit_initial_states(self): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + sell_unit = env._business_engine.world.get_unit(pdetail["seller"]["id"]) if info["name"] == "Warehouse_001": source_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1433,7 +1433,7 @@ def test_seller_unit_demand_states(self): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + sell_unit = env._business_engine.world.get_unit(pdetail["seller"]["id"]) if info["name"] == "Warehouse_001": source_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1499,7 +1499,7 @@ def test_seller_unit_customized(self): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + sell_unit = env._business_engine.world.get_unit(pdetail["seller"]["id"]) if info["name"] == "Warehouse_001": source_facility = env._business_engine.world.get_facility_by_id(info["id"]) From a65abb32b097c1eda1ff3d5b40472b4e462d2dbe Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 23 Mar 2021 14:00:31 +0800 Subject: [PATCH 110/482] correct get_unit comment --- maro/simulator/scenarios/supply_chain/world.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index d1b70c53e..0eb3286dd 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -98,14 +98,14 @@ def get_facility_by_name(self, name: str): """ return self.facilities[self._facility_name2id_mapping[name]] - def get_unit(self, unit_id: int) -> Union[FacilityBase, UnitBase]: + def get_unit(self, unit_id: int) -> UnitBase: """Get an unit by id. Args: unit_id (int): Id to query. Returns: - Union[FacilityBase, UnitBase]: Unit instance. + UnitBase: Unit instance. """ return self.units[unit_id] From d4cd7cd67cdbaeeb3e96942491a3bfe1521d5dcc Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 23 Mar 2021 17:19:41 +0800 Subject: [PATCH 111/482] move delay_order_penalty to facility level --- .../supply_chain/facilities/facility.py | 8 +++++++- maro/simulator/scenarios/supply_chain/parser.py | 5 +++++ .../supply_chain/topologies/sample1/config.yml | 15 ++++++++------- .../scenarios/supply_chain/units/consumer.py | 2 +- .../scenarios/supply_chain/units/distribution.py | 10 +++++++--- tests/data/supply_chain/case_01/config.yml | 16 ++++++++-------- tests/data/supply_chain/case_02/config.yml | 8 ++++---- tests/data/supply_chain/case_03/config.yml | 8 ++++---- 8 files changed, 44 insertions(+), 28 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index 698cc451d..08ea27c96 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -34,6 +34,9 @@ class FacilityBase(ABC): # Key is sku id, value is the list of product unit from upstream. upstreams: dict = None + # Configuration of this facility. + configs: dict = None + def parse_skus(self, configs: dict): """Parse sku information from config. @@ -48,7 +51,10 @@ def parse_configs(self, configs: dict): Args: configs (dict): Configuration of this facility. """ - pass + self.configs = configs + + def get_config(self, key: str): + return None if self.configs is None else self.configs.get(key, None) def initialize(self): """Initialize this facility after frame is ready.""" diff --git a/maro/simulator/scenarios/supply_chain/parser.py b/maro/simulator/scenarios/supply_chain/parser.py index 446fb5756..941db8294 100644 --- a/maro/simulator/scenarios/supply_chain/parser.py +++ b/maro/simulator/scenarios/supply_chain/parser.py @@ -60,6 +60,9 @@ def __init__(self): # World configurations. self.world = {} + # Other settings. + self.settings = {} + def add_data_definition(self, alias: str, class_name: str, module_path: str, name_in_frame: str): """Add a data model definition. @@ -218,3 +221,5 @@ def _parse_config(self): copy_dict(facility, facility_conf) self._result.world["facilities"].append(facility) + + self._result.settings = conf.get("settings", {}) diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index 6640145eb..f43168bd1 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -126,7 +126,6 @@ world: skus: sku3: # sku name and attributes needed for this facility init_in_stock: 100 - delay_order_penalty: 10 product_unit_cost: 1 order_cost: 0 type: "production" # production means this is the output production of this facility @@ -140,13 +139,16 @@ world: distribution: *normal_distribution # products use default config in core.yml + + # config of this facility + config: + delay_order_penalty: 10 - name: "Supplier_002" definition_ref: "SupplierFacility" skus: sku1: init_in_stock: 100 - delay_order_penalty: 10 product_unit_cost: 1 type: "production" cost: 10 @@ -154,7 +156,6 @@ world: order_cost: 0 sku3: init_in_stock: 100 - delay_order_penalty: 10 type: "material" cost: 10 price: 100 @@ -164,30 +165,30 @@ world: storage: *small_storage distribution: *normal_distribution + config: + delay_order_penalty: 10 - name: "Warehouse_001" definition_ref: "WarehouseFacility" skus: sku1: init_stock: 1000 - delay_order_penalty: 10 price: 100 order_cost: 200 sku2: init_stock: 1000 - delay_order_penalty: 10 price: 100 order_cost: 200 sku3: init_stock: 1000 - delay_order_penalty: 10 price: 100 order_cost: 200 children: storage: *huge_storage distribution: *normal_distribution - + config: + delay_order_penalty: 10 - name: "Retailer_001" definition_ref: "RetailerFacility" diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index a93b69b61..eb9ef14c5 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -10,7 +10,7 @@ class ConsumerUnit(SkuUnit): - """Consumer unit used to generated order to purchase from upstream by action.""" + """Consumer unit used to generate order to purchase from upstream by action.""" def __init__(self): super(ConsumerUnit, self).__init__() diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index ee971f7b3..8c40d0759 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -26,7 +26,7 @@ def __init__(self): # What product we will carry. self.product_list = [] - def get_pending_order(self): + def get_pending_order(self) -> Dict[int, int]: """Get orders that states is pending. Returns: @@ -39,11 +39,14 @@ def get_pending_order(self): return counter - def place_order(self, order: Order): + def place_order(self, order: Order) -> int: """Place an order in the pending queue. Args: order (Order): Order to insert. + + Returns: + int: Total price of this order. """ if order.quantity > 0: sku = self.facility.skus[order.product_id] @@ -101,7 +104,8 @@ def step(self, tick: int): sku = self.facility.skus[order.product_id] product_index = self.product_index_mapping[order.product_id] - self.data_model.delay_order_penalty[product_index] += sku.delay_order_penalty + #self.data_model.delay_order_penalty[product_index] += sku.delay_order_penalty + self.data_model.delay_order_penalty[product_index] += self.facility.get_config("delay_order_penalty") def flush_states(self): for vehicle in self.vehicles: diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml index d10747daa..26836041b 100644 --- a/tests/data/supply_chain/case_01/config.yml +++ b/tests/data/supply_chain/case_01/config.yml @@ -81,7 +81,6 @@ world: skus: sku3: init_in_stock: 80 - delay_order_penalty: 10 product_unit_cost: 1 type: "production" cost": 10 @@ -90,12 +89,13 @@ world: children: storage: *small_storage distribution: *normal_distribution + config: + delay_order_penalty: 10 - name: "Supplier_SKU1" definition_ref: "SupplierFacility" skus: sku1: init_in_stock: 96 - delay_order_penalty: 10 product_unit_cost: 1 type: "production" cost: 10 @@ -103,7 +103,6 @@ world: order_cost: 0 sku3: init_in_stock: 100 - delay_order_penalty: 10 type: "material" cost: 10 price: 100 @@ -111,12 +110,13 @@ world: children: storage: *midium_storage distribution: *normal_distribution + config: + delay_order_penalty: 10 - name: "Supplier_SKU2" definition_ref: "SupplierFacility" skus: sku2: init_in_stock: 50 - delay_order_penalty: 10 product_unit_cost: 1 type: "production" cost: 10 @@ -124,7 +124,6 @@ world: order_cost: 0 sku1: init_in_stock: 50 - delay_order_penalty: 10 type: "material" cost: 10 price: 100 @@ -132,12 +131,13 @@ world: children: storage: *midium_storage distribution: *normal_distribution + config: + delay_order_penalty: 10 - name: "Supplier_SKU4" definition_ref: "SupplierFacility" skus: sku4: init_in_stock: 50 - delay_order_penalty: 10 product_unit_cost: 4 type: "production" cost: 10 @@ -145,7 +145,6 @@ world: order_cost: 0 sku2: init_in_stock: 0 - delay_order_penalty: 10 type: "material" cost: 10 price: 100 @@ -153,7 +152,8 @@ world: children: storage: *midium_storage distribution: *normal_distribution - + config: + delay_order_penalty: 10 topology: Supplier_SKU1: sku3: diff --git a/tests/data/supply_chain/case_02/config.yml b/tests/data/supply_chain/case_02/config.yml index 978a48d23..5a044d3da 100644 --- a/tests/data/supply_chain/case_02/config.yml +++ b/tests/data/supply_chain/case_02/config.yml @@ -95,7 +95,6 @@ world: skus: sku3: init_in_stock: 80 - delay_order_penalty: 20 product_unit_cost: 1 type: "production" cost": 10 @@ -104,27 +103,28 @@ world: children: storage: *small_storage distribution: *normal_distribution + config: + delay_order_penalty: 20 - name: "Warehouse_001" definition_ref: "WarehouseFacility" skus: sku1: init_stock: 10 - delay_order_penalty: 10 price: 100 order_cost: 200 sku2: init_stock: 10 - delay_order_penalty: 10 price: 100 order_cost: 200 sku3: init_stock: 10 - delay_order_penalty: 10 price: 100 order_cost: 200 children: storage: *small_storage distribution: *normal_distribution + config: + delay_order_penalty: 20 - name: "Retailer_001" definition_ref: "RetailerFacility" skus: diff --git a/tests/data/supply_chain/case_03/config.yml b/tests/data/supply_chain/case_03/config.yml index 88796cabe..38e004786 100644 --- a/tests/data/supply_chain/case_03/config.yml +++ b/tests/data/supply_chain/case_03/config.yml @@ -107,7 +107,6 @@ world: skus: sku3: init_in_stock: 80 - delay_order_penalty: 20 product_unit_cost: 1 type: "production" cost": 10 @@ -116,27 +115,28 @@ world: children: storage: *small_storage distribution: *normal_distribution + config: + delay_order_penalty: 10 - name: "Warehouse_001" definition_ref: "WarehouseFacility" skus: sku1: init_stock: 10 - delay_order_penalty: 10 price: 100 order_cost: 200 sku2: init_stock: 10 - delay_order_penalty: 10 price: 100 order_cost: 200 sku3: init_stock: 10 - delay_order_penalty: 10 price: 100 order_cost: 200 children: storage: *small_storage distribution: *normal_distribution + config: + delay_order_penalty: 10 - name: "Retailer_001" definition_ref: "RetailerFacility" skus: From 57f5dfe9b47782d1558a19b01fe5ea95377c61fc Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 23 Mar 2021 17:25:30 +0800 Subject: [PATCH 112/482] move order_cost to facility level --- .../scenarios/supply_chain/facilities/facility.py | 4 ++-- .../supply_chain/topologies/sample1/config.yml | 14 +++++--------- .../scenarios/supply_chain/units/consumer.py | 2 +- tests/data/supply_chain/case_01/config.yml | 10 +++------- tests/data/supply_chain/case_02/config.yml | 12 ++++-------- tests/data/supply_chain/case_03/config.yml | 12 ++++-------- 6 files changed, 19 insertions(+), 35 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index 08ea27c96..1c31cfef0 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -53,8 +53,8 @@ def parse_configs(self, configs: dict): """ self.configs = configs - def get_config(self, key: str): - return None if self.configs is None else self.configs.get(key, None) + def get_config(self, key: str, default: object = None): + return default if self.configs is None else self.configs.get(key, default) def initialize(self): """Initialize this facility after frame is ready.""" diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index f43168bd1..dfab944e3 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -127,7 +127,6 @@ world: sku3: # sku name and attributes needed for this facility init_in_stock: 100 product_unit_cost: 1 - order_cost: 0 type: "production" # production means this is the output production of this facility cost": 10 price": 10 @@ -143,6 +142,7 @@ world: # config of this facility config: delay_order_penalty: 10 + order_cost: 0 - name: "Supplier_002" definition_ref: "SupplierFacility" @@ -153,13 +153,11 @@ world: type: "production" cost: 10 price: 100 - order_cost: 0 sku3: init_in_stock: 100 type: "material" cost: 10 price: 100 - order_cost: 200 children: storage: *small_storage @@ -167,6 +165,7 @@ world: config: delay_order_penalty: 10 + order_cost: 0 - name: "Warehouse_001" definition_ref: "WarehouseFacility" @@ -174,21 +173,19 @@ world: sku1: init_stock: 1000 price: 100 - order_cost: 200 sku2: init_stock: 1000 price: 100 - order_cost: 200 sku3: init_stock: 1000 price: 100 - order_cost: 200 children: storage: *huge_storage distribution: *normal_distribution config: delay_order_penalty: 10 + order_cost: 0 - name: "Retailer_001" definition_ref: "RetailerFacility" @@ -199,25 +196,24 @@ world: init_in_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 # optional - order_cost: 200 sku3: price: 200 cost: 10 init_in_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 - order_cost: 200 sku2: price: 100 cost: 10 init_in_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 - order_cost: 200 children: storage: *midium_storage + config: + order_cost: 0 # topology used to specify the up/downstream for facilities # we split it from facility, so that we can support configuration inherit to override it # for a new topology diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index eb9ef14c5..461490a8a 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -56,7 +56,7 @@ def initialize(self): sku = self.facility.skus[self.product_id] - self.data_model.initialize(order_cost=sku.order_cost) + self.data_model.initialize(order_cost=self.facility.get_config("order_cost", 0)) if self.facility.upstreams is not None: # Construct sources from facility's upstreams. diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml index 26836041b..72592f58b 100644 --- a/tests/data/supply_chain/case_01/config.yml +++ b/tests/data/supply_chain/case_01/config.yml @@ -85,7 +85,6 @@ world: type: "production" cost": 10 price": 10 - order_cost: 0 children: storage: *small_storage distribution: *normal_distribution @@ -100,18 +99,17 @@ world: type: "production" cost: 10 price: 100 - order_cost: 0 sku3: init_in_stock: 100 type: "material" cost: 10 price: 100 - order_cost: 200 children: storage: *midium_storage distribution: *normal_distribution config: delay_order_penalty: 10 + order_cost: 200 - name: "Supplier_SKU2" definition_ref: "SupplierFacility" skus: @@ -121,18 +119,17 @@ world: type: "production" cost: 10 price: 100 - order_cost: 0 sku1: init_in_stock: 50 type: "material" cost: 10 price: 100 - order_cost: 200 children: storage: *midium_storage distribution: *normal_distribution config: delay_order_penalty: 10 + order_cost: 200 - name: "Supplier_SKU4" definition_ref: "SupplierFacility" skus: @@ -142,18 +139,17 @@ world: type: "production" cost: 10 price: 100 - order_cost: 0 sku2: init_in_stock: 0 type: "material" cost: 10 price: 100 - order_cost: 200 children: storage: *midium_storage distribution: *normal_distribution config: delay_order_penalty: 10 + order_cost: 200 topology: Supplier_SKU1: sku3: diff --git a/tests/data/supply_chain/case_02/config.yml b/tests/data/supply_chain/case_02/config.yml index 5a044d3da..1ef74e381 100644 --- a/tests/data/supply_chain/case_02/config.yml +++ b/tests/data/supply_chain/case_02/config.yml @@ -99,32 +99,30 @@ world: type: "production" cost": 10 price": 10 - order_cost: 0 children: storage: *small_storage distribution: *normal_distribution config: delay_order_penalty: 20 + order_cost: 0 - name: "Warehouse_001" definition_ref: "WarehouseFacility" skus: sku1: init_stock: 10 price: 100 - order_cost: 200 sku2: init_stock: 10 price: 100 - order_cost: 200 sku3: init_stock: 10 price: 100 - order_cost: 200 children: storage: *small_storage distribution: *normal_distribution config: delay_order_penalty: 20 + order_cost: 200 - name: "Retailer_001" definition_ref: "RetailerFacility" skus: @@ -134,24 +132,22 @@ world: init_in_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 - order_cost: 200 sku3: price: 200 cost: 10 init_in_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 - order_cost: 200 sku2: price: 100 cost: 10 init_in_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 - order_cost: 200 children: storage: *small_storage - + config: + order_cost: 200 topology: Warehouse_001: sku3: diff --git a/tests/data/supply_chain/case_03/config.yml b/tests/data/supply_chain/case_03/config.yml index 38e004786..ea452aace 100644 --- a/tests/data/supply_chain/case_03/config.yml +++ b/tests/data/supply_chain/case_03/config.yml @@ -111,32 +111,30 @@ world: type: "production" cost": 10 price": 10 - order_cost: 0 children: storage: *small_storage distribution: *normal_distribution config: delay_order_penalty: 10 + order_cost: 0 - name: "Warehouse_001" definition_ref: "WarehouseFacility" skus: sku1: init_stock: 10 price: 100 - order_cost: 200 sku2: init_stock: 10 price: 100 - order_cost: 200 sku3: init_stock: 10 price: 100 - order_cost: 200 children: storage: *small_storage distribution: *normal_distribution config: delay_order_penalty: 10 + order_cost: 200 - name: "Retailer_001" definition_ref: "RetailerFacility" skus: @@ -146,24 +144,22 @@ world: init_in_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 - order_cost: 200 sku3: price: 200 cost: 10 init_in_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 - order_cost: 200 sku2: price: 100 cost: 10 init_in_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 - order_cost: 200 children: storage: *small_storage - + config: + order_cost: 200 topology: Warehouse_001: sku3: From ee469090204ef97d84542b2b9751cc6a387ec510 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 23 Mar 2021 18:44:14 +0800 Subject: [PATCH 113/482] add a script to generate random config according to original one, fix incorrect naming in config --- .../supply_chain/facilities/retailer.py | 4 +- .../supply_chain/facilities/supplier.py | 4 +- .../supply_chain/facilities/warehouse.py | 2 +- .../supply_chain/topologies/random_config.py | 363 ++++++++++++++++++ .../topologies/sample1/config.yml | 12 +- .../scenarios/supply_chain/units/storage.py | 4 +- .../simulator/scenarios/supply_chain/world.py | 2 +- tests/data/supply_chain/case_01/config.yml | 14 +- tests/data/supply_chain/case_02/config.yml | 8 +- tests/data/supply_chain/case_03/config.yml | 8 +- tests/supply_chain/test_supply_chain.py | 2 +- 11 files changed, 393 insertions(+), 30 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/topologies/random_config.py diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 443340834..10378c018 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -20,7 +20,7 @@ class RetailerFacility(FacilityBase): "id", "price", "cost", - "init_in_stock", + "init_stock", "sale_gamma", "order_cost", "backlog_ratio" @@ -44,7 +44,7 @@ def parse_skus(self, configs: dict): sku.id, sku_config.get("price", 0), sku_config.get("cost", 0), - sku_config["init_in_stock"], + sku_config["init_stock"], sku_config.get("sale_gamma", 0), sku_config.get("order_cost", 0), sku_config.get("backlog_ratio", 0) diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index ab4be108d..ff763e8dc 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -18,7 +18,7 @@ class SupplierFacility(FacilityBase): ( "name", "id", - "init_in_stock", + "init_stock", "type", "cost", "price", @@ -46,7 +46,7 @@ def parse_skus(self, configs: dict): sku_info = SupplierFacility.SkuInfo( sku_name, sku.id, - sku_config["init_in_stock"], + sku_config["init_stock"], sku_config["type"], sku_config.get("cost", 0), sku_config.get("price", 0), diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 77e7aea4b..b0aa2bb82 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -13,7 +13,7 @@ class WarehouseFacility(FacilityBase): """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" - SkuInfo = namedtuple("SkuInfo", ("name", "init_in_stock", "id", "price", "delay_order_penalty", "order_cost")) + SkuInfo = namedtuple("SkuInfo", ("name", "init_stock", "id", "price", "delay_order_penalty", "order_cost")) # Storage unit for this facility, must be a sub class of StorageUnit. storage: StorageUnit = None diff --git a/maro/simulator/scenarios/supply_chain/topologies/random_config.py b/maro/simulator/scenarios/supply_chain/topologies/random_config.py new file mode 100644 index 000000000..eaeb63d26 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/random_config.py @@ -0,0 +1,363 @@ +""" +A simple script that used to generate random configurations. + +""" + +warehouse_def = """ +class: "WarehouseFacility" +children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" +""" + +supplier_def = """ +class: "SupplierFacility" +children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + manufacture: + class: "ManufactureUnit" +""" + +retail_def = """ +class: "RetailerFacility" +children: + storage: + class: "StorageUnit" + products: + class: "ProductUnit" + is_template: true + config: + consumer: + class: "ConsumerUnit" + seller: + class: "SellerUnit" +""" + +global_sku_template = """ +id: 1 +name: "sku1" +output_units_per_lot: 12 +""" + +supplier_template = """ +name: "Supplier_001" +definition_ref: "SupplierFacility" +skus: {} +children: + storage: + config: + capacity: 10000 + unit_storage_cost: 1 + distribution: + children: + vehicles: [] + config: + unit_price: 1 +config: {} +""" + +warehouse_template = """ +name: "Warehouse_001" +definition_ref: "WarehouseFacility" +skus: {} +children: + storage: + config: + capacity: 10000 + unit_storage_cost: 1 + distribution: + children: + vehicles: [] + config: + unit_price: 1 +config: {} +""" + +retail_template = """ +name: "Retailer_001" +definition_ref: "RetailerFacility" +skus: {} +children: + storage: + config: + capacity: 10000 + unit_storage_cost: 1 +config: {} +""" + +import argparse +import os +import random + +import numpy as np +from flloat.parser.ltlf import LTLfParser +from yaml import safe_load, safe_dump + + +def generate_config( + sku_num: int, + supplier_num: int, + warehouse_num: int, + retailer_num: int, + grid_width: int, + grid_height: int, + output_path: int = None): + constraints = ['G(stock_constraint)', + 'G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint)))', + 'G(low_profit -> low_stock_constraint)'] + + # constraints = ['G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint)))'] + + def construct_formula(constraint): + parser = LTLfParser() + formula = parser(constraint) + return formula + + constraint_formulas = {constraint: construct_formula(constraint) for constraint in constraints} + constraint_automata = {constraint: constraint_formulas[constraint].to_automaton().determinize() for constraint in + constraints} + + max_constraint_states = int(np.max([len(a.states) for a in constraint_automata.values()])) + + # config of vehicle + vehicle_conf = { + "class": "VehicleUnit", + "config": { + "patient": 100, + "unit_transport_cost": 1 + } + } + + config = {} + + # save the vehicle definition in the config, so later distribution will reference to it. + config["normal_vehicle"] = vehicle_conf + config["facility_definitions"] = {} + config["facility_definitions"]["SupplierFacility"] = safe_load(supplier_def) + config["facility_definitions"]["WarehouseFacility"] = safe_load(warehouse_def) + config["facility_definitions"]["RetailerFacility"] = safe_load(retail_def) + + world_conf = {} + + # generate settings first. + world_conf["settings"] = { + 'global_reward_weight_producer': 0.50, + 'global_reward_weight_consumer': 0.50, + "initial_balance": 100000, + "constraint_state_hist_len": max_constraint_states, + "total_echelons": 3, + "replenishment_discount": 0.9, + "reward_normalization": 1e7, + "constraint_violate_reward": -1e7, + } + + sku_names = [f'SKU{i}' for i in range(sku_num)] + + sku_list = [] + for sku_index, sku_name in enumerate(sku_names): + sku_list.append({ + "id": sku_index, + "name": sku_name + }) + + world_conf["skus"] = sku_list + + sku_cost = {f'SKU{i}': random.randint(10, 500) for i in range(sku_num)} + sku_product_cost = {f'SKU{i}': int(sku_cost[f'SKU{i}'] * 0.9) for i in range(sku_num)} + sku_price = {f'SKU{i}': int(sku_cost[f'SKU{i}'] * (1 + random.randint(10, 100) / 100)) for i in range(sku_num)} + sku_gamma = {f'SKU{i}': random.randint(5, 100) for i in range(sku_num)} + total_gamma = sum(list(sku_gamma.values())) + sku_vlt = {f'SKU{i}': random.randint(1, 3) for i in range(sku_num)} + + # generate suppliers. + supplier_facilities = [] + + for i in range(supplier_num): + facility = safe_load(supplier_template) + + facility["name"] = f"SUPPLIER{i}" + facility["children"]["storage"]["config"]["capacity"] = total_gamma * 100 + + for _ in range(10 * sku_num): + # this will save as a reference in the final yaml file + facility["children"]["distribution"]["children"]["vehicles"].append(vehicle_conf) + + # facility config + facility["config"] = {} + facility["config"]["order_cost"] = 200 + facility["config"]["delay_order_penalty"] = 1000 + + sku_list = {} + + for j in range(sku_num): + sku_name = f"SKU{j}" + sku_list[sku_name] = { + "price": sku_cost[sku_name], + "cost": sku_product_cost[sku_name], + "vlt": 3, + "init_stock": int(sku_gamma[sku_name] * 50), + # why this configuration, as manufacture is controlled by action? + "production_rate": int(sku_gamma[sku_name] * 50), + "type": "production" # for this script, all sku is a production that produced by suppliers, no bom. + } + + facility["skus"] = sku_list + + supplier_facilities.append(facility) + + # warehouses + warehouse_list = [] + for i in range(warehouse_num): + facility = safe_load(warehouse_template) + + facility["name"] = f"WAREHOUSE{i}" + facility["children"]["storage"]["config"]["capacity"] = total_gamma * 100 + + for _ in range(10 * sku_num): + facility["children"]["distribution"]["children"]["vehicles"].append(vehicle_conf) + + # facility config + facility["config"] = {} + facility["config"]["order_cost"] = 500 + facility["config"]["delay_order_penalty"] = 1000 + + sku_list = {} + + for j in range(sku_num): + sku_name = f"SKU{j}" + sku_list[sku_name] = { + "price": sku_cost[sku_name], + "cost": sku_product_cost[sku_name], + "vlt": 3, + "init_stock": int(sku_gamma[sku_name] * 20) + } + + facility["skus"] = sku_list + + warehouse_list.append(facility) + + sku_constraints = {} + for i in range(sku_num): + if random.random() <= 0.5: + continue + sku_constraints[f"SKU{i}"] = constraints[random.randint(0, len(constraints) - 1)] + + # retailers + retailer_list = [] + for i in range(retailer_num): + facility = safe_load(retail_template) + + facility["name"] = f"STORE{i}" + facility["children"]["storage"]["config"]["capacity"] = total_gamma * 20 + + # facility config + facility["config"] = {} + facility["config"]["order_cost"] = 500 + + sku_list = {} + + for j in range(sku_num): + sku_name = f"SKU{j}" + sku_list[sku_name] = { + "price": sku_cost[sku_name], + "cost": sku_product_cost[sku_name], + "vlt": 3, + "init_stock": sku_gamma[sku_name] * (sku_vlt[sku_name] + random.randint(1, 5)), + "sale_gamma": sku_gamma[sku_name], + 'max_stock': 1000, + "constraint": sku_constraints.get(sku_name, None) + } + + facility["skus"] = sku_list + + retailer_list.append(facility) + + world_conf["facilities"] = supplier_facilities + warehouse_list + retailer_list + + # according to original code, the upstream relationship is like following: + # supplier <- warehouse <- retailer + # as current configuration supplier and warehouse contain all the sku, so we can just random pick. + world_conf["topology"] = {} + + for store in retailer_list: + store_upstream = {} + + for i in range(sku_num): + sku_name = f"SKU{i}" + store_upstream[sku_name] = [warehouse_list[random.randint(0, warehouse_num - 1)]["name"], ] + + world_conf["topology"][store["name"]] = store_upstream + + for warehouse in warehouse_list: + warehouse_upstream = {} + + for i in range(sku_num): + sku_name = f"SKU{i}" + warehouse_upstream[sku_name] = [supplier_facilities[random.randint(0, supplier_num) - 1]["name"], ] + + world_conf["topology"][warehouse["name"]] = warehouse_upstream + + # grid + world_conf["grid"] = {} + world_conf["grid"]["size"] = [grid_width, grid_height] + + # random pick location + available_cells = [(x, y) for x in range(grid_width) for y in range(grid_height)] + + world_conf["grid"]["facilities"] = {} + for facility in world_conf["facilities"]: + cell = random.randint(0, len(available_cells) - 1) + + world_conf["grid"]["facilities"][facility["name"]] = available_cells[cell] + + del available_cells[cell] + + config["world"] = world_conf + + if output_path is None: + output_path = "." + + with open(os.path.join(output_path, "config.yml"), "wt+") as fp: + safe_dump(config, fp) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument("--sku_num", type=int, default=random.randint(4, 5)) + parser.add_argument("--supplier_num", type=int, default=1) + parser.add_argument("--warehouse_num", type=int, default=1) + parser.add_argument("--retailer_num", type=int, default=1) + parser.add_argument("--grid_width", type=int, default=20) + parser.add_argument("--grid_height", type=int, default=20) + parser.add_argument("--output_path", type=str, default=".") + + arg = parser.parse_args() + + # force it to be 1 to make it same as original. + generate_config( + arg.sku_num, + arg.supplier_num, + arg.warehouse_num, + arg.retailer_num, + arg.grid_width, + arg.grid_height, + arg.output_path + ) diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index dfab944e3..3371f5536 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -125,7 +125,7 @@ world: # sku list of this facility skus: sku3: # sku name and attributes needed for this facility - init_in_stock: 100 + init_stock: 100 product_unit_cost: 1 type: "production" # production means this is the output production of this facility cost": 10 @@ -148,13 +148,13 @@ world: skus: sku1: - init_in_stock: 100 + init_stock: 100 product_unit_cost: 1 type: "production" cost: 10 price: 100 sku3: - init_in_stock: 100 + init_stock: 100 type: "material" cost: 10 price: 100 @@ -193,19 +193,19 @@ world: sku1: price: 300 cost: 10 - init_in_stock: 100 + init_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 # optional sku3: price: 200 cost: 10 - init_in_stock: 100 + init_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 sku2: price: 100 cost: 10 - init_in_stock: 100 + init_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index e1ff55d1b..242700fdc 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -117,7 +117,7 @@ def initialize(self): for sku in self.facility.skus.values(): self.product_list.append(sku.id) - self.product_number.append(sku.init_in_stock) + self.product_number.append(sku.init_stock) self.remaining_space = self.capacity @@ -149,7 +149,7 @@ def reset(self): self.product_number.clear() for sku in self.facility.skus.values(): - self.product_number.append(sku.init_in_stock) + self.product_number.append(sku.init_stock) self.remaining_space = self.capacity diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 0eb3286dd..b685695ab 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -138,7 +138,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio # Grab sku information for this world. for sku_conf in world_config["skus"]: - sku = SkuInfo(sku_conf["name"], sku_conf["id"], {}, sku_conf["output_units_per_lot"]) + sku = SkuInfo(sku_conf["name"], sku_conf["id"], {}, sku_conf.get("output_units_per_lot", 1)) self._sku_id2name_mapping[sku.id] = sku.name self._sku_collection[sku.name] = sku diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml index 72592f58b..30578d93a 100644 --- a/tests/data/supply_chain/case_01/config.yml +++ b/tests/data/supply_chain/case_01/config.yml @@ -80,7 +80,7 @@ world: definition_ref: "SupplierFacility" skus: sku3: - init_in_stock: 80 + init_stock: 80 product_unit_cost: 1 type: "production" cost": 10 @@ -94,13 +94,13 @@ world: definition_ref: "SupplierFacility" skus: sku1: - init_in_stock: 96 + init_stock: 96 product_unit_cost: 1 type: "production" cost: 10 price: 100 sku3: - init_in_stock: 100 + init_stock: 100 type: "material" cost: 10 price: 100 @@ -114,13 +114,13 @@ world: definition_ref: "SupplierFacility" skus: sku2: - init_in_stock: 50 + init_stock: 50 product_unit_cost: 1 type: "production" cost: 10 price: 100 sku1: - init_in_stock: 50 + init_stock: 50 type: "material" cost: 10 price: 100 @@ -134,13 +134,13 @@ world: definition_ref: "SupplierFacility" skus: sku4: - init_in_stock: 50 + init_stock: 50 product_unit_cost: 4 type: "production" cost: 10 price: 100 sku2: - init_in_stock: 0 + init_stock: 0 type: "material" cost: 10 price: 100 diff --git a/tests/data/supply_chain/case_02/config.yml b/tests/data/supply_chain/case_02/config.yml index 1ef74e381..a56ecea70 100644 --- a/tests/data/supply_chain/case_02/config.yml +++ b/tests/data/supply_chain/case_02/config.yml @@ -94,7 +94,7 @@ world: definition_ref: "SupplierFacility" skus: sku3: - init_in_stock: 80 + init_stock: 80 product_unit_cost: 1 type: "production" cost": 10 @@ -129,19 +129,19 @@ world: sku1: price: 300 cost: 10 - init_in_stock: 10 + init_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 sku3: price: 200 cost: 10 - init_in_stock: 10 + init_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 sku2: price: 100 cost: 10 - init_in_stock: 10 + init_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 children: diff --git a/tests/data/supply_chain/case_03/config.yml b/tests/data/supply_chain/case_03/config.yml index ea452aace..c5bda9556 100644 --- a/tests/data/supply_chain/case_03/config.yml +++ b/tests/data/supply_chain/case_03/config.yml @@ -106,7 +106,7 @@ world: definition_ref: "SupplierFacility" skus: sku3: - init_in_stock: 80 + init_stock: 80 product_unit_cost: 1 type: "production" cost": 10 @@ -141,19 +141,19 @@ world: sku1: price: 300 cost: 10 - init_in_stock: 10 + init_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 sku3: price: 200 cost: 10 - init_in_stock: 10 + init_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 sku2: price: 100 cost: 10 - init_in_stock: 10 + init_stock: 10 sale_gamma: 10 backlog_ratio: 0.1 children: diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index bcc07876e..052b722e9 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -1438,7 +1438,7 @@ def test_seller_unit_demand_states(self): if info["name"] == "Warehouse_001": source_facility = env._business_engine.world.get_facility_by_id(info["id"]) - SKU3_INIT_NUMBER = sell_unit.facility.skus[SKU3_ID].init_in_stock + SKU3_INIT_NUMBER = sell_unit.facility.skus[SKU3_ID].init_stock env.step(None) From e48006a217c7b1504dc2f489afb1e1a14984e628 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 23 Mar 2021 15:11:41 +0000 Subject: [PATCH 114/482] refined trajectory logic --- examples/cim/ac/main.py | 2 +- examples/cim/common.py | 36 ++++--- examples/cim/dqn/config/training_config.py | 2 +- examples/cim/dqn/main.py | 12 +-- maro/rl/__init__.py | 12 +-- maro/rl/agent/dqn.py | 1 - maro/rl/training/__init__.py | 6 +- maro/rl/training/actor.py | 110 ++++++++++----------- maro/rl/training/actor_proxy.py | 29 +++--- maro/rl/training/message_enums.py | 5 +- maro/rl/training/trajectory.py | 83 ---------------- maro/rl/utils/__init__.py | 4 +- maro/rl/utils/trajectory_utils.py | 40 -------- 13 files changed, 101 insertions(+), 241 deletions(-) delete mode 100644 maro/rl/training/trajectory.py diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index 5d467615b..72164ce8c 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -17,7 +17,7 @@ from maro.utils import Logger, set_seeds from examples.cim.ac.config import agent_config, training_config -from examples.cim.common import CIMTrajectory, common_config +from examples.cim.common import CIMEnvWrapper, common_config def get_ac_agent(): diff --git a/examples/cim/common.py b/examples/cim/common.py index 4ff3f4f1a..a8dfb83a2 100644 --- a/examples/cim/common.py +++ b/examples/cim/common.py @@ -5,7 +5,7 @@ import numpy as np -from maro.rl import AbsTrajectory +from maro.rl import AbsEnvWrapper from maro.simulator.scenarios.cim.common import Action, ActionType common_config = { @@ -26,19 +26,18 @@ } -class CIMTrajectory(AbsTrajectory): +class CIMEnvWrapper(AbsEnvWrapper): def __init__( self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream, reward_time_window, fulfillment_factor, shortage_factor, time_decay, finite_vessel_space=True, has_early_discharge=True ): - super().__init__(env) + super().__init__(env, hindsight_reward_window=common_config["reward_time_window"]) self.port_attributes = port_attributes self.vessel_attributes = vessel_attributes self.action_space = action_space self.look_back = look_back self.max_ports_downstream = max_ports_downstream - self.reward_time_window = reward_time_window self.fulfillment_factor = fulfillment_factor self.shortage_factor = shortage_factor self.time_decay = time_decay @@ -76,21 +75,20 @@ def get_action(self, action_by_agent, event): return {port: Action(vessel, port, actual_action, action_type)} - def on_finish(self): + def get_hindsight_reward(self, event): """Compute offline rewards.""" - for i, event in enumerate(self.events): - port_snapshots = self.env.snapshot_list["ports"] - start_tick = event.tick + 1 - ticks = list(range(start_tick, start_tick + self.reward_time_window)) + port_snapshots = self.env.snapshot_list["ports"] + start_tick = event.tick + 1 + ticks = list(range(start_tick, start_tick + self.hindsight_reward_window)) - future_fulfillment = port_snapshots[ticks::"fulfillment"] - future_shortage = port_snapshots[ticks::"shortage"] - decay_list = [ - self.time_decay ** i for i in range(self.reward_time_window) - for _ in range(future_fulfillment.shape[0] // self.reward_time_window) - ] + future_fulfillment = port_snapshots[ticks::"fulfillment"] + future_shortage = port_snapshots[ticks::"shortage"] + decay_list = [ + self.time_decay ** i for i in range(self.hindsight_reward_window) + for _ in range(future_fulfillment.shape[0] // self.hindsight_reward_window) + ] - self.rewards[i] = np.float32( - self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - - self.shortage_factor * np.dot(future_shortage, decay_list) - ) + return np.float32( + self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - + self.shortage_factor * np.dot(future_shortage, decay_list) + ) diff --git a/examples/cim/dqn/config/training_config.py b/examples/cim/dqn/config/training_config.py index a6dd847f3..bdc926cb8 100644 --- a/examples/cim/dqn/config/training_config.py +++ b/examples/cim/dqn/config/training_config.py @@ -23,7 +23,7 @@ }, "group": "cim-dqn", "learner_update_trigger": 2, - # "trajectory_sync_interval": 50, + "replay_sync_interval": 50, "num_actors": 2, "num_trainers": 4, "trainer_id": 0 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 4a78bf733..bcbb4ce9e 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -15,7 +15,7 @@ from maro.simulator import Env from maro.utils import Logger, set_seeds -from examples.cim.common import CIMTrajectory, common_config +from examples.cim.common import CIMEnvWrapper, common_config from examples.cim.dqn.config import agent_config, training_config @@ -27,8 +27,7 @@ def get_dqn_agent(): def cim_dqn_learner(): - env = Env(**training_config["env"]) - agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) + agent = MultiAgentWrapper({name: get_dqn_agent() for name in Env(**training_config["env"]).agent_idx_list}) scheduler = TwoPhaseLinearParameterScheduler(training_config["max_episode"], **training_config["exploration"]) actor = ActorProxy( training_config["group"], training_config["num_actors"], @@ -40,11 +39,10 @@ def cim_dqn_learner(): def cim_dqn_actor(): env = Env(**training_config["env"]) - trajectory = CIMTrajectory(env, **common_config) agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) actor = Actor( - trajectory, agent, - group=training_config["group"],# trajectory_sync_interval=training_config["trajectory_sync_interval"] + CIMEnvWrapper(env, **common_config), agent, + group=training_config["group"], replay_sync_interval=training_config["replay_sync_interval"] ) actor.run() @@ -55,7 +53,7 @@ def cim_dqn_actor(): "-w", "--whoami", type=int, choices=[0, 1, 2], default=0, help="Identity of this process: 0 - multi-process mode, 1 - learner, 2 - actor" ) - + args = parser.parse_args() if args.whoami == 0: actor_processes = [Process(target=cim_dqn_actor) for _ in range(training_config["num_actors"])] diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index a760127f6..2d8e12495 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -10,10 +10,9 @@ from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler from maro.rl.storage import AbsStore, OverwriteType, SimpleStore -from maro.rl.training import AbsLearner, AbsTrajectory, Actor, ActorProxy, OffPolicyLearner, OnPolicyLearner +from maro.rl.training import AbsEnvWrapper, AbsLearner, Actor, ActorProxy, OffPolicyLearner, OnPolicyLearner from maro.rl.utils import ( - get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_sars, get_truncated_cumulative_reward, - select_by_actions + get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_truncated_cumulative_reward, select_by_actions ) __all__ = [ @@ -23,7 +22,8 @@ "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", "AbsStore", "OverwriteType", "SimpleStore", - "AbsLearner", "AbsTrajectory", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner", - "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_sars", - "get_truncated_cumulative_reward", "select_by_actions" + "AbsEnvWrapper", "AbsLearner", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner" + "TrajectorySyncActor", + "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_truncated_cumulative_reward", + "select_by_actions" ] diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index f825c092d..d27248d39 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -96,7 +96,6 @@ def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: ]) def learn(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): - print("sampled rewards: ", rewards) states = torch.from_numpy(states) actions = torch.from_numpy(actions) rewards = torch.from_numpy(rewards) diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 5b778aa2f..69a5b5c09 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -3,7 +3,9 @@ from .actor import Actor from .actor_proxy import ActorProxy +from .env_wrapper import AbsEnvWrapper from .learner import AbsLearner, OffPolicyLearner, OnPolicyLearner -from .trajectory import AbsTrajectory -__all__ = ["AbsLearner", "AbsTrajectory", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner"] +__all__ = [ + "AbsEnvWrapper", "AbsLearner", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner" +] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 9ed8d0ad0..65a7fc32c 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -1,61 +1,57 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, Union +from abc import ABC, abstractmethod +from collections import defaultdict +from typing import Callable, List, Union -from maro.communication import Message, Proxy +from maro.communication import Message, Proxy, RegisterTable, SessionType from maro.rl.agent import AbsAgent, MultiAgentWrapper from maro.rl.storage import SimpleStore -from maro.rl.utils import get_sars from maro.utils import InternalLogger +from .env_wrapper import AbsEnvWrapper from .message_enums import MsgTag, MsgKey -from .trajectory import AbsTrajectory class Actor(object): - """Actor class that performs roll-out tasks. + """On-demand roll-out executor. Args: - trajectory (AbsTrajectory): An ``AbsTrajectory`` instance with an env wrapped inside. + env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance with an env wrapped inside. agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. - trajectory_sync_interval (int): Number of roll-out steps between trajectory syncing calls. - experience_getter (Callable): Custom function to extract experiences from a trajectory for training. - If None, ``get_sars`` will be used. Defaults to None. + replay_sync_interval (int): Number of roll-out steps between replay syncing calls. """ def __init__( self, - trajectory: AbsTrajectory, + env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], group: str = None, proxy_options: dict = None, - trajectory_sync_interval: int = None, - experience_getter: Callable = get_sars + replay_sync_interval: int = None, + send_results: bool = True ): - self.trajectory = trajectory + if replay_sync_interval == 0: + raise ValueError("replay_sync_interval must be a positive integer or None") + self.env = env self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent if group is not None: if proxy_options is None: proxy_options = {} self._proxy = Proxy(group, "actor", {"actor_proxy": 1}, **proxy_options) - self._trajectory_sync_interval = trajectory_sync_interval self._logger = InternalLogger(self._proxy.name) - # Under local mode or disributed mode without experience syncing, an experience pool needs to be created. - if group is None or not self._trajectory_sync_interval: - self.experience_pool = { - agent_id: SimpleStore(["S", "A", "R", "S_", "loss"]) for agent_id in self.agent.agent_dict - } - self.experience_getter = experience_getter + self.replay_sync_interval = replay_sync_interval + self.send_results = send_results def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): - self.trajectory.reset() + self.env.reset() if not training: - self.trajectory.record_path = False # no need to record the trajectory if roll-out is not for training + self.env.record_path = False # no need to record the trajectory if roll-out is not for training # Load models and exploration parameters if model_by_agent: @@ -63,22 +59,13 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non if exploration_params: self.agent.set_exploration_params(exploration_params) - state = self.trajectory.start(rollout_index=index) # get initial state + state = self.env.start(rollout_index=index) # get initial state while state: action = self.agent.choose_action(state) - state = self.trajectory.step(action) - if training and hasattr(self, "_proxy") and not hasattr(self, "experience_pool"): - self._sync_trajectory(index) - self.trajectory.on_env_feedback() + state = self.env.step(action) + self.on_env_feedback(index) - self.trajectory.on_finish() - # If no experience syncing, the experience pool needs to be populated. - if training and hasattr(self, "experience_pool"): - for agent_id, exp in self.experience_getter(*self.trajectory.path).items(): - self.experience_pool[agent_id].put(exp) - print(agent_id, len(self.experience_pool[agent_id])) - - return self.trajectory.env.metrics + return self.env.metrics def run(self): assert hasattr(self, "_proxy"), "No proxy found. The `group` parameter should not be None at init." @@ -87,31 +74,38 @@ def run(self): self._logger.info("Exiting...") break elif msg.tag == MsgTag.ROLLOUT: - ep = msg.body[MsgKey.ROLLOUT_INDEX] - self._logger.info(f"Rolling out ({ep})...") - metrics = self.roll_out( - ep, - training=msg.body[MsgKey.TRAINING], - model_by_agent=msg.body[MsgKey.MODEL], - exploration_params=msg.body[MsgKey.EXPLORATION_PARAMS] - ) - self._logger.info(f"Roll-out {ep} finished") - body = {MsgKey.ROLLOUT_INDEX: ep, MsgKey.METRICS: metrics} - if msg.body[MsgKey.TRAINING]: - if hasattr(self, "experience_pool"): - body[MsgKey.EXPERIENCE] = {id_: pool.data for id_, pool in self.experience_pool.items()} - else: - body[MsgKey.TRAJECTORY] = self.trajectory.path - self._proxy.isend( - Message(MsgTag.ROLLOUT_DONE, self._proxy.name, self._proxy.peers["actor_proxy"][0], body=body) - ) + self.on_rollout_request(msg) + self.post_rollout(msg.body[MsgKey.ROLLOUT_INDEX]) + + def on_rollout_request(self, msg: Message): + ep = msg.body[MsgKey.ROLLOUT_INDEX] + self._logger.info(f"Rolling out ({ep})...") + self.roll_out( + ep, + training=msg.body[MsgKey.TRAINING], + model_by_agent=msg.body[MsgKey.MODEL], + exploration_params=msg.body[MsgKey.EXPLORATION_PARAMS] + ) + self._logger.info(f"Roll-out {ep} finished") - def _sync_trajectory(self, index): - if (self.trajectory.step_index + 1) % self._trajectory_sync_interval == 0: + def on_env_feedback(self, index): + if self.replay_sync_interval is not None and (self.env.step_index + 1) % self.replay_sync_interval == 0: self._proxy.isend( Message( - MsgTag.TRAJECTORY_SYNC, self._proxy.name, self._proxy.peers["actor_proxy"][0], - body={MsgKey.ROLLOUT_INDEX: index, MsgKey.TRAJECTORY: self.trajectory.path} + MsgTag.REPLAY_SYNC, self._proxy.name, self._proxy.peers["actor_proxy"][0], + body={MsgKey.ROLLOUT_INDEX: index, MsgKey.REPLAY: self.env.replay} ) ) - self.trajectory.flush() + self.env.flush() + + def post_rollout(self, index: int): + if not self.send_results: + return + + body = {MsgKey.ROLLOUT_INDEX: index, MsgKey.METRICS: self.env.metrics} + if self.env.record_path: + body[MsgKey.REPLAY] = self.env.replay + + self._proxy.isend( + Message(MsgTag.ROLLOUT_DONE, self._proxy.name, self._proxy.peers["actor_proxy"][0], body=body) + ) diff --git a/maro/rl/training/actor_proxy.py b/maro/rl/training/actor_proxy.py index e77134d94..8980cd5e2 100644 --- a/maro/rl/training/actor_proxy.py +++ b/maro/rl/training/actor_proxy.py @@ -2,11 +2,11 @@ # Licensed under the MIT license. from collections import defaultdict -from typing import Callable, List +from typing import Callable, List, Union from maro.communication import Message, Proxy, RegisterTable, SessionType +from maro.rl.agent import AbsAgent, MultiAgentWrapper from maro.rl.storage import OverwriteType, SimpleStore -from maro.rl.utils import get_sars from maro.utils import InternalLogger from .message_enums import MsgTag, MsgKey @@ -23,8 +23,6 @@ class ActorProxy(object): learner updates, i.e., model training. proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. - experience_getter (Callable): Custom function to extract experiences from a trajectory for training. - If None, ``get_sars`` will be used. Defaults to None. """ def __init__( self, @@ -32,7 +30,6 @@ def __init__( num_actors: int, update_trigger: str = None, proxy_options: dict = None, - experience_getter: Callable = get_sars, experience_pool_capacity: int = -1, experience_pool_overwrite: OverwriteType = None ): @@ -56,7 +53,6 @@ def get_experience_pool(): ) self.experience_pool = defaultdict(lambda: get_experience_pool()) - self.experience_getter = experience_getter self.logger = InternalLogger("ACTOR_PROXY") def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): @@ -92,25 +88,22 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non if result: env_metrics = result[0] break - elif msg.tag == MsgTag.TRAJECTORY_SYNC: - for agent_id, exp in self.experience_getter(*msg.body[MsgKey.TRAJECTORY]).items(): + elif msg.tag == MsgTag.REPLAY_SYNC: + # print(f"received exp from actor {msg.source} ") + # print({agent_id: {k: len(v) for k, v in exp.items()} for agent_id, exp in msg.body[MsgKey.REPLAY].items()}) + for agent_id, exp in msg.body[MsgKey.REPLAY].items(): self.experience_pool[agent_id].put(exp) - print(f"received exp from actor {msg.source} ", end="") - print({agent_id: len(pool) for agent_id, pool in self.experience_pool.items()}) + # print({agent_id: len(pool) for agent_id, pool in self.experience_pool.items()}) return env_metrics def _on_rollout_finish(self, messages: List[Message]): metrics = {msg.source: msg.body[MsgKey.METRICS] for msg in messages} for msg in messages: - if MsgKey.EXPERIENCE in msg.body: - exp = msg.body[MsgKey.EXPERIENCE] - else: - exp = self.experience_getter(*msg.body[MsgKey.TRAJECTORY]) - for agent_id, ex in exp.items(): - self.experience_pool[agent_id].put(ex) - for agent_id, pool in self.experience_pool.items(): - print(agent_id, len(pool)) + print({agent_id: {k: len(v) for k, v in exp.items()} for agent_id, exp in msg.body[MsgKey.REPLAY].items()}) + for agent_id, exp in msg.body[MsgKey.REPLAY].items(): + self.experience_pool[agent_id].put(exp) + print({agent_id: len(pool) for agent_id, pool in self.experience_pool.items()}) return metrics def terminate(self): diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index 69dabc327..61c375c7f 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -5,7 +5,7 @@ class MsgTag(Enum): ROLLOUT = "rollout" CHOOSE_ACTION = "choose_action" ACTION = "action" - TRAJECTORY_SYNC = "experience_sync" + REPLAY_SYNC = "replay_sync" TRAIN = "train" ABORT_ROLLOUT = "abort_rollout" ROLLOUT_DONE = "rollout_done" @@ -18,8 +18,7 @@ class MsgKey(Enum): ROLLOUT_INDEX = "rollout_index" TIME_STEP = "time_step" METRICS = "metrics" - TRAJECTORY = "trajectory" - EXPERIENCE = "experience" + REPLAY = "replay" STATE = "state" TRAINING = "training" MODEL = "model" diff --git a/maro/rl/training/trajectory.py b/maro/rl/training/trajectory.py deleted file mode 100644 index 6c3a79c5a..000000000 --- a/maro/rl/training/trajectory.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from collections import defaultdict -from multiprocessing import Pipe, Process -from typing import Callable - -from maro.communication import Message, Proxy - -from .message_enums import MsgTag, MsgKey - - -class AbsTrajectory(ABC): - def __init__(self, env, record_path: bool = True): - self.env = env - self.step_index = None - self.events = [] - self.states = [] - self.actions = [] - self.rewards = [] - self.record_path = record_path - - def start(self, rollout_index: int = None): - self.step_index = 0 - _, event, _ = self.env.step(None) - state = self.get_state(event) - if self.record_path: - self.events.append(event) - self.states.append(state) - return state - - @property - def path(self): - return self.states, self.actions, self.rewards - - @abstractmethod - def get_state(self, event) -> dict: - pass - - @abstractmethod - def get_action(self, action, event) -> dict: - pass - - def get_reward(self) -> float: - pass - - def step(self, action): - assert self.events, "start() must be called first." - self.step_index += 1 - env_action = self.get_action(action, self.events[-1]) - if len(env_action) == 1: - env_action = list(env_action.values())[0] - _, event, done = self.env.step(env_action) - if self.record_path: - self.actions.append(action) - self.rewards.append(self.get_reward()) - assert len(self.events) == len(self.states) == len(self.actions) == len(self.rewards) - if not done: - state = self.get_state(event) - if self.record_path: - self.events.append(event) - self.states.append(state) - return state - - def on_env_feedback(self): - pass - - def on_finish(self): - pass - - def reset(self): - self.env.reset() - self.events.clear() - self.states.clear() - self.actions.clear() - self.rewards.clear() - - def flush(self): - self.events = self.events[-1:] if len(self.events) == len(self.actions) + 1 else [] - self.states = self.states[-1:] if len(self.states) == len(self.actions) + 1 else [] - self.actions.clear() - self.rewards.clear() diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index ae35a7527..bad6e602a 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_sars, get_truncated_cumulative_reward +from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_truncated_cumulative_reward from .value_utils import get_log_prob, get_max, get_td_errors, select_by_actions __all__ = [ "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", "get_log_prob", "get_max", - "get_sars", "get_td_errors", "select_by_actions" + "get_td_errors", "select_by_actions" ] diff --git a/maro/rl/utils/trajectory_utils.py b/maro/rl/utils/trajectory_utils.py index 3498c68d6..3fe348adb 100644 --- a/maro/rl/utils/trajectory_utils.py +++ b/maro/rl/utils/trajectory_utils.py @@ -8,46 +8,6 @@ import torch import torch.nn.functional as F -MAX_LOSS = 1e8 - -def get_sars(states: list, actions: list, rewards: list) -> dict: - """Extract experiences from a multi-agent trajectory. - - The top-level keys for each state and action will be treated as agent IDs during the extraction. - - Args: - states (list): List of states traversed during a roll-out episode (in order). - actions (list): List of actions taken during a roll-out episode (in order). - rewards (list): List of rewards obtained during a roll-out episode (in order). - - Returns: - Experiences for training, grouped by agent ID. - """ - sars = {} - for state, action, reward in zip(states, actions, rewards): - assert state.keys() == action.keys(), f"state keys: {list(state.keys())}, action keys: {list(action.keys())}" - for agent_id in state: - exp = sars.setdefault(agent_id, {"S": [], "A": [], "R": [], "S_": [], "loss": []}) - exp["S"].append(state[agent_id]) - exp["A"].append(action[agent_id]) - exp["R"].append(reward) - exp["loss"].append(MAX_LOSS) - - if len(states) == len(actions) + 1: - for agent_id in states[-1]: - if agent_id in sars: - sars[agent_id]["S"].append(states[-1][agent_id]) - - for exp in sars.values(): - exp["S_"] = exp["S"][1:] - exp["S"].pop() - if len(exp["S"]) == len(exp["A"]) - 1: - exp["A"].pop() - exp["R"].pop() - exp["loss"].pop() - - return sars - def get_truncated_cumulative_reward( rewards: Union[list, np.ndarray, torch.Tensor], From a53ec07cc1ece6c20f830ff563cc0aa54731138e Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 23 Mar 2021 16:08:22 +0000 Subject: [PATCH 115/482] added missing file --- maro/rl/training/env_wrapper.py | 144 ++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 maro/rl/training/env_wrapper.py diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py new file mode 100644 index 000000000..b118b806f --- /dev/null +++ b/maro/rl/training/env_wrapper.py @@ -0,0 +1,144 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from collections import defaultdict +from multiprocessing import Pipe, Process +from typing import Callable + +from maro.communication import Message, Proxy +from maro.simulator import Env + +from .message_enums import MsgTag, MsgKey + +MAX_LOSS = 1e8 + + +class AbsEnvWrapper(ABC): + """Environment wrapper that performs various shaping and other roll-out related logic. + + Args: + env (Env): Environment instance. + record_path (bool): If True, the steps during roll-out will be recorded sequentially. This + includes states, actions and rewards. The decision events themselves will also be recorded + for hindsight reward evaluation purposes. Defaults to True. + hindsight_reward_window (int): Number of ticks required after a decision event to evaluate + the reward for the action taken for that event. Defaults to 0, which rewards are evaluated immediately + after executing an action. + """ + def __init__(self, env: Env, record_path: bool = True, hindsight_reward_window: int = 0): + self.env = env + self.step_index = None + self.replay_memory = defaultdict(lambda: {key: [] for key in ["S", "A", "R", "S_", "loss"]}) + self.events = [] + self.acting_agents = [] + self.record_path = record_path + self.hindsight_reward_window = hindsight_reward_window + self._pending_reward_idx = 0 + + def start(self, rollout_index: int = None): + self.step_index = 0 + self._pending_reward_idx = 0 + _, event, _ = self.env.step(None) + return self._on_new_event(event) + + @property + def replay(self): + return { + agent_id: {k: vals[:len(replay["R"])] for k, vals in replay.items()} + for agent_id, replay in self.replay_memory.items() + } + + @property + def metrics(self): + return self.env.metrics + + @abstractmethod + def get_state(self, event) -> dict: + pass + + @abstractmethod + def get_action(self, action, event) -> dict: + pass + + def get_reward(self) -> float: + """Get the immediate reward for an action. + + This can be left blank if rewards are evaluated in hindsight. + """ + pass + + def get_hindsight_reward(self, event): + """Get the reward for an action that occurred a certain number of ticks ago. + + If implemented, whatever value ``get_reward`` gives will be ignored in the output of ``get_path``. + If left blank, ``get_reward`` must be implemented. + """ + pass + + def step(self, action_by_agent: dict): + assert self.events, "start() must be called first." + self.step_index += 1 + env_action = self.get_action(action_by_agent, self.events[-1][0]) + if len(env_action) == 1: + env_action = list(env_action.values())[0] + _, event, done = self.env.step(env_action) + + if self.record_path: + if self.hindsight_reward_window: + for agent_id, action in action_by_agent.items(): + self.replay_memory[agent_id]["A"].append(action) + self._assign_hindsight_rewards(tick=event.tick if not done else None) + else: + reward = self.get_reward() + for agent_id, action in action_by_agent.items(): + self.replay_memory[agent_id]["A"].append(action) + self.replay_memory[agent_id]["R"].append(reward) + self._pending_reward_idx += 1 + + if not done: + return self._on_new_event(event) + + def reset(self): + self.env.reset() + self.events.clear() + self.replay_memory = defaultdict(lambda: {key: [] for key in ["S", "A", "R", "S_", "loss"]}) + + def flush(self): + for agent_id in self.replay_memory: + num_complete = len(self.replay_memory[agent_id]["R"]) + del self.replay_memory[agent_id]["S"][:num_complete] + del self.replay_memory[agent_id]["A"][:num_complete] + del self.replay_memory[agent_id]["R"][:num_complete] + del self.replay_memory[agent_id]["S_"][:num_complete] + del self.replay_memory[agent_id]["loss"][:num_complete] + + del self.events[:self._pending_reward_idx] + self._pending_reward_idx = 0 + + def _on_new_event(self, event): + state_by_agent = self.get_state(event) + if self.record_path: + self.events.append((event, list(state_by_agent.keys()))) + for agent_id, state in state_by_agent.items(): + if self.replay_memory[agent_id]["S"]: + self.replay_memory[agent_id]["S_"].append(state) + self.replay_memory[agent_id]["loss"].append(MAX_LOSS) + self.replay_memory[agent_id]["S"].append(state) + + # for agent_id, exp in self.replay_memory.items(): + # ns, na, nr, ns_ = len(exp["S"]), len(exp["A"]), len(exp["R"]), len(exp["S_"]) + # print(f"agent_id: {agent_id}, state: {ns}, action: {na}, reward: {nr}, state_: {ns_}") + + return state_by_agent + + def _assign_hindsight_rewards(self, tick=None): + while ( + self._pending_reward_idx < len(self.events) and + (tick is None or tick - self.events[self._pending_reward_idx][0].tick >= self.hindsight_reward_window) + ): + event, acting_agents = self.events[self._pending_reward_idx] + hindsight_reward = self.get_hindsight_reward(event) + for agent_id in acting_agents: + self.replay_memory[agent_id]["R"].append(hindsight_reward) + self._pending_reward_idx += 1 From 1c8ed72cac5b3da9cc08ae35dee0ffeb53aa56f6 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 24 Mar 2021 09:15:23 +0800 Subject: [PATCH 116/482] move config generator to scripts --- .../topologies => scripts}/random_config.py | 123 ++++++++++-------- 1 file changed, 68 insertions(+), 55 deletions(-) rename {maro/simulator/scenarios/supply_chain/topologies => scripts}/random_config.py (81%) diff --git a/maro/simulator/scenarios/supply_chain/topologies/random_config.py b/scripts/random_config.py similarity index 81% rename from maro/simulator/scenarios/supply_chain/topologies/random_config.py rename to scripts/random_config.py index eaeb63d26..f34de68e5 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/random_config.py +++ b/scripts/random_config.py @@ -1,8 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + """ A simple script that used to generate random configurations. - """ +import argparse +import os +import random +from typing import Optional + +import numpy as np +from flloat.parser.ltlf import LTLfParser +from yaml import safe_load, safe_dump + +# Definition of warehouse. warehouse_def = """ class: "WarehouseFacility" children: @@ -18,6 +30,7 @@ class: "ConsumerUnit" """ +# Definition of supplier. supplier_def = """ class: "SupplierFacility" children: @@ -35,7 +48,8 @@ class: "ManufactureUnit" """ -retail_def = """ +# Definition of retailer. +retailer_def = """ class: "RetailerFacility" children: storage: @@ -50,12 +64,12 @@ class: "SellerUnit" """ -global_sku_template = """ -id: 1 -name: "sku1" -output_units_per_lot: 12 -""" - +# Template to generate a supplier facility. +# Properties to change: +# . name +# . skus +# . vehicles +# . config (optional) supplier_template = """ name: "Supplier_001" definition_ref: "SupplierFacility" @@ -73,6 +87,12 @@ config: {} """ +# Template to generate warehouse facility. +# Property to change: +# . name +# . skus +# . vehicles +# . config (optional) warehouse_template = """ name: "Warehouse_001" definition_ref: "WarehouseFacility" @@ -90,7 +110,12 @@ config: {} """ -retail_template = """ +# Template to generate retailer. +# Property to change: +# . name +# . skus +# . config (optional) +retailer_template = """ name: "Retailer_001" definition_ref: "RetailerFacility" skus: {} @@ -102,23 +127,9 @@ config: {} """ -import argparse -import os -import random -import numpy as np -from flloat.parser.ltlf import LTLfParser -from yaml import safe_load, safe_dump - - -def generate_config( - sku_num: int, - supplier_num: int, - warehouse_num: int, - retailer_num: int, - grid_width: int, - grid_height: int, - output_path: int = None): +def generate_config(sku_num: int, supplier_num: int, warehouse_num: int, retailer_num: int, grid_width: int, + grid_height: int, output_path: Optional[str] = None): constraints = ['G(stock_constraint)', 'G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint)))', 'G(low_profit -> low_stock_constraint)'] @@ -136,7 +147,7 @@ def construct_formula(constraint): max_constraint_states = int(np.max([len(a.states) for a in constraint_automata.values()])) - # config of vehicle + # Base configuration of vehicle used in all facility. vehicle_conf = { "class": "VehicleUnit", "config": { @@ -145,19 +156,16 @@ def construct_formula(constraint): } } - config = {} + # Save the vehicle definition in the config, so later distribution will reference to it. + config = {"normal_vehicle": vehicle_conf, "facility_definitions": {}} - # save the vehicle definition in the config, so later distribution will reference to it. - config["normal_vehicle"] = vehicle_conf - config["facility_definitions"] = {} + # Add the facility definitions. config["facility_definitions"]["SupplierFacility"] = safe_load(supplier_def) config["facility_definitions"]["WarehouseFacility"] = safe_load(warehouse_def) - config["facility_definitions"]["RetailerFacility"] = safe_load(retail_def) - - world_conf = {} + config["facility_definitions"]["RetailerFacility"] = safe_load(retailer_def) - # generate settings first. - world_conf["settings"] = { + # Generate settings first. + world_conf = {"settings": { 'global_reward_weight_producer': 0.50, 'global_reward_weight_consumer': 0.50, "initial_balance": 100000, @@ -166,7 +174,7 @@ def construct_formula(constraint): "replenishment_discount": 0.9, "reward_normalization": 1e7, "constraint_violate_reward": -1e7, - } + }} sku_names = [f'SKU{i}' for i in range(sku_num)] @@ -177,8 +185,10 @@ def construct_formula(constraint): "name": sku_name }) + # Add the sku list to the world configuration. world_conf["skus"] = sku_list + # Generate sku information. sku_cost = {f'SKU{i}': random.randint(10, 500) for i in range(sku_num)} sku_product_cost = {f'SKU{i}': int(sku_cost[f'SKU{i}'] * 0.9) for i in range(sku_num)} sku_price = {f'SKU{i}': int(sku_cost[f'SKU{i}'] * (1 + random.randint(10, 100) / 100)) for i in range(sku_num)} @@ -186,7 +196,7 @@ def construct_formula(constraint): total_gamma = sum(list(sku_gamma.values())) sku_vlt = {f'SKU{i}': random.randint(1, 3) for i in range(sku_num)} - # generate suppliers. + # Generate suppliers. supplier_facilities = [] for i in range(supplier_num): @@ -199,11 +209,12 @@ def construct_formula(constraint): # this will save as a reference in the final yaml file facility["children"]["distribution"]["children"]["vehicles"].append(vehicle_conf) - # facility config + # Facility config. facility["config"] = {} facility["config"]["order_cost"] = 200 facility["config"]["delay_order_penalty"] = 1000 + # Sku list of this facility. sku_list = {} for j in range(sku_num): @@ -211,18 +222,20 @@ def construct_formula(constraint): sku_list[sku_name] = { "price": sku_cost[sku_name], "cost": sku_product_cost[sku_name], + "service_level": .95, "vlt": 3, "init_stock": int(sku_gamma[sku_name] * 50), - # why this configuration, as manufacture is controlled by action? + # Why this configuration, as manufacture is controlled by action? "production_rate": int(sku_gamma[sku_name] * 50), - "type": "production" # for this script, all sku is a production that produced by suppliers, no bom. + # For this script, all sku is a production that produced by suppliers, no bom. + "type": "production" } facility["skus"] = sku_list supplier_facilities.append(facility) - # warehouses + # Warehouses. warehouse_list = [] for i in range(warehouse_num): facility = safe_load(warehouse_template) @@ -233,7 +246,6 @@ def construct_formula(constraint): for _ in range(10 * sku_num): facility["children"]["distribution"]["children"]["vehicles"].append(vehicle_conf) - # facility config facility["config"] = {} facility["config"]["order_cost"] = 500 facility["config"]["delay_order_penalty"] = 1000 @@ -244,9 +256,10 @@ def construct_formula(constraint): sku_name = f"SKU{j}" sku_list[sku_name] = { "price": sku_cost[sku_name], - "cost": sku_product_cost[sku_name], - "vlt": 3, - "init_stock": int(sku_gamma[sku_name] * 20) + "cost": sku_cost[sku_name], + "vlt": sku_vlt[sku_name], + "init_stock": int(sku_gamma[sku_name] * 20), + "service_level": .96 } facility["skus"] = sku_list @@ -259,15 +272,14 @@ def construct_formula(constraint): continue sku_constraints[f"SKU{i}"] = constraints[random.randint(0, len(constraints) - 1)] - # retailers + # Retailers. retailer_list = [] for i in range(retailer_num): - facility = safe_load(retail_template) + facility = safe_load(retailer_template) facility["name"] = f"STORE{i}" facility["children"]["storage"]["config"]["capacity"] = total_gamma * 20 - # facility config facility["config"] = {} facility["config"]["order_cost"] = 500 @@ -276,9 +288,9 @@ def construct_formula(constraint): for j in range(sku_num): sku_name = f"SKU{j}" sku_list[sku_name] = { - "price": sku_cost[sku_name], - "cost": sku_product_cost[sku_name], - "vlt": 3, + "price": sku_price[sku_name], + "service_level": 0.95, + "cost": sku_cost[sku_name], "init_stock": sku_gamma[sku_name] * (sku_vlt[sku_name] + random.randint(1, 5)), "sale_gamma": sku_gamma[sku_name], 'max_stock': 1000, @@ -291,11 +303,12 @@ def construct_formula(constraint): world_conf["facilities"] = supplier_facilities + warehouse_list + retailer_list - # according to original code, the upstream relationship is like following: + # According to original code, the upstream relationship is like following: # supplier <- warehouse <- retailer # as current configuration supplier and warehouse contain all the sku, so we can just random pick. world_conf["topology"] = {} + # Random pick upstreams for retailers from warehouses. for store in retailer_list: store_upstream = {} @@ -305,6 +318,7 @@ def construct_formula(constraint): world_conf["topology"][store["name"]] = store_upstream + # Random pick upstreams for warehouses from suppliers. for warehouse in warehouse_list: warehouse_upstream = {} @@ -314,11 +328,11 @@ def construct_formula(constraint): world_conf["topology"][warehouse["name"]] = warehouse_upstream - # grid + # Grid settings. world_conf["grid"] = {} world_conf["grid"]["size"] = [grid_width, grid_height] - # random pick location + # Random pick location. available_cells = [(x, y) for x in range(grid_width) for y in range(grid_height)] world_conf["grid"]["facilities"] = {} @@ -351,7 +365,6 @@ def construct_formula(constraint): arg = parser.parse_args() - # force it to be 1 to make it same as original. generate_config( arg.sku_num, arg.supplier_num, From 09158e95b254eee0724cbbaf05afeeeb0e95f7e1 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sat, 27 Mar 2021 16:12:08 +0000 Subject: [PATCH 117/482] refactored maro/rl and examples --- docs/source/examples/multi_agent_dqn_cim.rst | 10 +- examples/cim/ac/config.py | 60 ++++++++ examples/cim/ac/config/__init__.py | 7 - examples/cim/ac/config/agent_config.py | 52 ------- examples/cim/ac/config/training_config.py | 11 -- examples/cim/ac/main.py | 49 ++----- examples/cim/common.py | 14 +- examples/cim/common_config.py | 21 +++ examples/cim/dqn/config.py | 71 ++++++++++ examples/cim/dqn/config/__init__.py | 7 - examples/cim/dqn/config/agent_config.py | 38 ------ examples/cim/dqn/config/training_config.py | 30 ---- examples/cim/dqn/main.py | 52 ++++--- maro/communication/registry_table.py | 1 - maro/rl/__init__.py | 7 +- maro/rl/agent/ac.py | 28 ++-- maro/rl/agent/ddpg.py | 2 +- maro/rl/agent/dqn.py | 2 +- maro/rl/distributed/__init__.py | 8 ++ maro/rl/{training => distributed}/actor.py | 44 +++--- .../{training => distributed}/actor_proxy.py | 40 +++--- .../{training => distributed}/dispatcher.py | 0 maro/rl/distributed/learner.py | 100 ++++++++++++++ .../message_enums.py | 0 maro/rl/{training => distributed}/trainer.py | 0 maro/rl/storage/simple_store.py | 60 ++------ maro/rl/training/__init__.py | 6 +- maro/rl/training/env_wrapper.py | 129 +++++++++--------- maro/rl/training/learner.py | 112 +++++++-------- .../rl_formulation.ipynb | 12 +- 30 files changed, 504 insertions(+), 469 deletions(-) create mode 100644 examples/cim/ac/config.py delete mode 100644 examples/cim/ac/config/__init__.py delete mode 100644 examples/cim/ac/config/agent_config.py delete mode 100644 examples/cim/ac/config/training_config.py create mode 100644 examples/cim/common_config.py create mode 100644 examples/cim/dqn/config.py delete mode 100644 examples/cim/dqn/config/__init__.py delete mode 100644 examples/cim/dqn/config/agent_config.py delete mode 100644 examples/cim/dqn/config/training_config.py create mode 100644 maro/rl/distributed/__init__.py rename maro/rl/{training => distributed}/actor.py (73%) rename maro/rl/{training => distributed}/actor_proxy.py (82%) rename maro/rl/{training => distributed}/dispatcher.py (100%) create mode 100644 maro/rl/distributed/learner.py rename maro/rl/{training => distributed}/message_enums.py (100%) rename maro/rl/{training => distributed}/trainer.py (100%) diff --git a/docs/source/examples/multi_agent_dqn_cim.rst b/docs/source/examples/multi_agent_dqn_cim.rst index 228f694a8..c6a71d410 100644 --- a/docs/source/examples/multi_agent_dqn_cim.rst +++ b/docs/source/examples/multi_agent_dqn_cim.rst @@ -26,7 +26,7 @@ in the roll-out loop. In this example, class CIMTrajectoryForDQN(Trajectory): def __init__( self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream, - reward_time_window, fulfillment_factor, shortage_factor, time_decay, + reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, finite_vessel_space=True, has_early_discharge=True ): super().__init__(env) @@ -35,7 +35,7 @@ in the roll-out loop. In this example, self.action_space = action_space self.look_back = look_back self.max_ports_downstream = max_ports_downstream - self.reward_time_window = reward_time_window + self.reward_eval_delay = reward_eval_delay self.fulfillment_factor = fulfillment_factor self.shortage_factor = shortage_factor self.time_decay = time_decay @@ -76,13 +76,13 @@ in the roll-out loop. In this example, def get_offline_reward(self, event): port_snapshots = self.env.snapshot_list["ports"] start_tick = event.tick + 1 - ticks = list(range(start_tick, start_tick + self.reward_time_window)) + ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) future_fulfillment = port_snapshots[ticks::"fulfillment"] future_shortage = port_snapshots[ticks::"shortage"] decay_list = [ - self.time_decay ** i for i in range(self.reward_time_window) - for _ in range(future_fulfillment.shape[0] // self.reward_time_window) + self.time_decay ** i for i in range(self.reward_eval_delay) + for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay) ] tot_fulfillment = np.dot(future_fulfillment, decay_list) diff --git a/examples/cim/ac/config.py b/examples/cim/ac/config.py new file mode 100644 index 000000000..54ace3937 --- /dev/null +++ b/examples/cim/ac/config.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from torch import nn +from torch.optim import Adam, RMSprop + +from maro.rl import OptimOption + +from examples.cim.common import common_config + +input_dim = ( + (common_config["look_back"] + 1) * + (common_config["max_ports_downstream"] + 1) * + len(common_config["port_attributes"]) + + len(common_config["vessel_attributes"]) +) + +config = { + "agent": { + "model": { + "actor": { + "input_dim": input_dim, + "output_dim": len(common_config["action_space"]), + "hidden_dims": [256, 128, 64], + "activation": nn.Tanh, + "softmax": True, + "batch_norm": False, + "head": True + }, + "critic": { + "input_dim": input_dim, + "output_dim": 1, + "hidden_dims": [256, 128, 64], + "activation": nn.LeakyReLU, + "softmax": False, + "batch_norm": True, + "head": True + } + }, + "optimization": { + "actor": OptimOption(optim_cls=Adam, optim_params={"lr": 0.001}), + "critic": OptimOption(optim_cls=RMSprop, optim_params={"lr": 0.001}) + }, + "hyper_params": { + "reward_discount": .0, + "critic_loss_func": nn.SmoothL1Loss(), + "train_iters": 10, + "actor_loss_coefficient": 0.1, + # "clip_ratio": 0.8 + } + }, + "training": { + "env": { + "scenario": "cim", + "topology": "toy.4p_ssdd_l0.0", + "durations": 1120, + }, + "max_episode": 50 + } +} diff --git a/examples/cim/ac/config/__init__.py b/examples/cim/ac/config/__init__.py deleted file mode 100644 index 4492cf223..000000000 --- a/examples/cim/ac/config/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .agent_config import agent_config -from .training_config import training_config - -__all__ = ["agent_config", "training_config"] diff --git a/examples/cim/ac/config/agent_config.py b/examples/cim/ac/config/agent_config.py deleted file mode 100644 index ecc87a80f..000000000 --- a/examples/cim/ac/config/agent_config.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from torch import nn -from torch.optim import Adam, RMSprop - -from maro.rl import OptimOption - -from examples.cim.common import common_config - -input_dim = ( - (common_config["look_back"] + 1) * - (common_config["max_ports_downstream"] + 1) * - len(common_config["port_attributes"]) + - len(common_config["vessel_attributes"]) -) - -agent_config = { - "model": { - "actor": { - "input_dim": input_dim, - "output_dim": len(common_config["action_space"]), - "hidden_dims": [256, 128, 64], - "activation": nn.Tanh, - "softmax": True, - "batch_norm": False, - "head": True - }, - "critic": { - "input_dim": input_dim, - "output_dim": 1, - "hidden_dims": [256, 128, 64], - "activation": nn.LeakyReLU, - "softmax": False, - "batch_norm": True, - "head": True - } - }, - "optimization": { - "actor": OptimOption(optim_cls=Adam, optim_params={"lr": 0.001}), - "critic": OptimOption(optim_cls=RMSprop, optim_params={"lr": 0.001}) - }, - "hyper_params": { - "reward_discount": .0, - "critic_loss_func": nn.SmoothL1Loss(), - "train_iters": 10, - "actor_loss_coefficient": 0.1, - "k": 1, - "lam": 0.0 - # "clip_ratio": 0.8 - } -} diff --git a/examples/cim/ac/config/training_config.py b/examples/cim/ac/config/training_config.py deleted file mode 100644 index c93b2c56d..000000000 --- a/examples/cim/ac/config/training_config.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -training_config = { - "env": { - "scenario": "cim", - "topology": "toy.4p_ssdd_l0.0", - "durations": 1120, - }, - "max_episode": 50 -} diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index 72164ce8c..d7f399655 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -1,60 +1,33 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import defaultdict, deque -from os import makedirs, system -from os.path import dirname, join, realpath - import numpy as np -from torch import nn -from torch.optim import Adam, RMSprop from maro.rl import ( Actor, ActorCritic, ActorCriticConfig, FullyConnectedBlock, MultiAgentWrapper, SimpleMultiHeadModel, - Scheduler, OnPolicyLearner + OnPolicyLearner ) from maro.simulator import Env -from maro.utils import Logger, set_seeds +from maro.utils import set_seeds -from examples.cim.ac.config import agent_config, training_config -from examples.cim.common import CIMEnvWrapper, common_config +from examples.cim.common import CIMEnvWrapper +from examples.cim.common_config import common_config +from examples.cim.ac.config import config def get_ac_agent(): - actor_net = FullyConnectedBlock(**agent_config["model"]["actor"]) - critic_net = FullyConnectedBlock(**agent_config["model"]["critic"]) + actor_net = FullyConnectedBlock(**config["agent"]["model"]["actor"]) + critic_net = FullyConnectedBlock(**config["agent"]["model"]["critic"]) ac_model = SimpleMultiHeadModel( - {"actor": actor_net, "critic": critic_net}, optim_option=agent_config["optimization"], + {"actor": actor_net, "critic": critic_net}, optim_option=config["agent"]["optimization"], ) - return ActorCritic(ac_model, ActorCriticConfig(**agent_config["hyper_params"])) - - -class CIMTrajectoryForAC(CIMTrajectory): - def on_finish(self): - training_data = {} - for event, state, action in zip(self.trajectory["event"], self.trajectory["state"], self.trajectory["action"]): - agent_id = list(state.keys())[0] - data = training_data.setdefault(agent_id, {"args": [[] for _ in range(4)]}) - data["args"][0].append(state[agent_id]) # state - data["args"][1].append(action[agent_id][0]) # action - data["args"][2].append(action[agent_id][1]) # log_p - data["args"][3].append(self.get_offline_reward(event)) # reward - - for agent_id in training_data: - training_data[agent_id]["args"] = [ - np.asarray(vals, dtype=np.float32 if i == 3 else None) - for i, vals in enumerate(training_data[agent_id]["args"]) - ] - - return training_data + return ActorCritic(ac_model, ActorCriticConfig(**config["agent"]["hyper_params"])) # Single-threaded launcher if __name__ == "__main__": set_seeds(1024) # for reproducibility - env = Env(**training_config["env"]) - trajectory = CIMTrajectoryForAC(env, **common_config) + env = Env(**config["training"]["env"]) agent = MultiAgentWrapper({name: get_ac_agent() for name in env.agent_idx_list}) - actor = Actor(trajectory, agent) # local actor - learner = OnPolicyLearner(actor, training_config["max_episode"]) + learner = OnPolicyLearner(CIMEnvWrapper(env, **common_config), agent, config["training"]["max_episode"]) learner.run() diff --git a/examples/cim/common.py b/examples/cim/common.py index a8dfb83a2..66b75aa90 100644 --- a/examples/cim/common.py +++ b/examples/cim/common.py @@ -19,7 +19,7 @@ "finite_vessel_space": True, "has_early_discharge": True, # Parameters for computing rewards - "reward_time_window": 99, + "reward_eval_delay": 99, "fulfillment_factor": 1.0, "shortage_factor": 1.0, "time_decay": 0.97 @@ -29,10 +29,10 @@ class CIMEnvWrapper(AbsEnvWrapper): def __init__( self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream, - reward_time_window, fulfillment_factor, shortage_factor, time_decay, + reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, finite_vessel_space=True, has_early_discharge=True ): - super().__init__(env, hindsight_reward_window=common_config["reward_time_window"]) + super().__init__(env, reward_eval_delay=common_config["reward_eval_delay"]) self.port_attributes = port_attributes self.vessel_attributes = vessel_attributes self.action_space = action_space @@ -75,17 +75,17 @@ def get_action(self, action_by_agent, event): return {port: Action(vessel, port, actual_action, action_type)} - def get_hindsight_reward(self, event): + def get_reward_for(self, event): """Compute offline rewards.""" port_snapshots = self.env.snapshot_list["ports"] start_tick = event.tick + 1 - ticks = list(range(start_tick, start_tick + self.hindsight_reward_window)) + ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) future_fulfillment = port_snapshots[ticks::"fulfillment"] future_shortage = port_snapshots[ticks::"shortage"] decay_list = [ - self.time_decay ** i for i in range(self.hindsight_reward_window) - for _ in range(future_fulfillment.shape[0] // self.hindsight_reward_window) + self.time_decay ** i for i in range(self.reward_eval_delay) + for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay) ] return np.float32( diff --git a/examples/cim/common_config.py b/examples/cim/common_config.py new file mode 100644 index 000000000..765ed90a3 --- /dev/null +++ b/examples/cim/common_config.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import numpy as np + +common_config = { + "port_attributes": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], + "vessel_attributes": ["empty", "full", "remaining_space"], + "action_space": list(np.linspace(-1.0, 1.0, 21)), + # Parameters for computing states + "look_back": 7, + "max_ports_downstream": 2, + # Parameters for computing actions + "finite_vessel_space": True, + "has_early_discharge": True, + # Parameters for computing rewards + "reward_eval_delay": 99, + "fulfillment_factor": 1.0, + "shortage_factor": 1.0, + "time_decay": 0.97 +} diff --git a/examples/cim/dqn/config.py b/examples/cim/dqn/config.py new file mode 100644 index 000000000..9fa50048f --- /dev/null +++ b/examples/cim/dqn/config.py @@ -0,0 +1,71 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from torch import nn +from torch.optim import RMSprop + +from maro.rl import OptimOption, OverwriteType + +from examples.cim.common import common_config + +input_dim = ( + (common_config["look_back"] + 1) * + (common_config["max_ports_downstream"] + 1) * + len(common_config["port_attributes"]) + + len(common_config["vessel_attributes"]) +) + +config = { + "agent": { + "model": { + "input_dim": input_dim, + "output_dim": len(common_config["action_space"]), # number of possible actions + "hidden_dims": [256, 128, 64], + "activation": nn.LeakyReLU, + "softmax": False, + "batch_norm": True, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 + }, + "optimization": OptimOption(optim_cls=RMSprop, optim_params={"lr": 0.05}), + "hyper_params": { + "reward_discount": .0, + "loss_cls": nn.SmoothL1Loss, + "target_update_freq": 5, + "tau": 0.1, + "double": False + } + }, + "training": { + "env": { + "scenario": "cim", + "topology": "toy.4p_ssdd_l0.0", + "durations": 1120, + }, + "max_episode": 100, + "min_experiences_to_train": 1024, + "train_iter": 10, + "batch_size": 128, + "replay_memory": { + "replay_memory_size": 2000, + "replay_memory_overwrite_type": OverwriteType.RANDOM, + }, + # "prioritized_sampling_by_loss": True, + "exploration": { + "parameter_names": ["epsilon"], + "split": 0.5, + "start": 0.4, + "mid": 0.32, + "end": 0.0 + }, + }, + "distributed": { + "group": "cim-dqn", + "num_actors": 2, + "redis_host": "localhost", + "redis_port": 6379, + "learner_update_trigger": 2, + "replay_sync_interval": 100 + } +} diff --git a/examples/cim/dqn/config/__init__.py b/examples/cim/dqn/config/__init__.py deleted file mode 100644 index 4492cf223..000000000 --- a/examples/cim/dqn/config/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .agent_config import agent_config -from .training_config import training_config - -__all__ = ["agent_config", "training_config"] diff --git a/examples/cim/dqn/config/agent_config.py b/examples/cim/dqn/config/agent_config.py deleted file mode 100644 index 6aef90643..000000000 --- a/examples/cim/dqn/config/agent_config.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from torch import nn -from torch.optim import RMSprop - -from maro.rl import DQN, DQNConfig, FullyConnectedBlock, OptimOption, PolicyGradient, SimpleMultiHeadModel - -from examples.cim.common import common_config - -input_dim = ( - (common_config["look_back"] + 1) * - (common_config["max_ports_downstream"] + 1) * - len(common_config["port_attributes"]) + - len(common_config["vessel_attributes"]) -) - -agent_config = { - "model": { - "input_dim": input_dim, - "output_dim": len(common_config["action_space"]), # number of possible actions - "hidden_dims": [256, 128, 64], - "activation": nn.LeakyReLU, - "softmax": False, - "batch_norm": True, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": OptimOption(optim_cls=RMSprop, optim_params={"lr": 0.05}), - "hyper_params": { - "reward_discount": .0, - "loss_cls": nn.SmoothL1Loss, - "target_update_freq": 5, - "tau": 0.1, - "double": False - } -} diff --git a/examples/cim/dqn/config/training_config.py b/examples/cim/dqn/config/training_config.py deleted file mode 100644 index bdc926cb8..000000000 --- a/examples/cim/dqn/config/training_config.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -training_config = { - "env": { - "scenario": "cim", - "topology": "toy.4p_ssdd_l0.0", - "durations": 1120, - }, - "max_episode": 100, - "exploration": { - "parameter_names": ["epsilon"], - "split": 0.5, - "start": 0.4, - "mid": 0.32, - "end": 0.0 - }, - "training": { - "min_experiences_to_train": 1024, - "train_iter": 10, - "batch_size": 128, - "prioritized_sampling_by_loss": True - }, - "group": "cim-dqn", - "learner_update_trigger": 2, - "replay_sync_interval": 50, - "num_actors": 2, - "num_trainers": 4, - "trainer_id": 0 -} diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index bcbb4ce9e..f4909cd95 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -2,47 +2,59 @@ # Licensed under the MIT license. import argparse -import time -from collections import defaultdict from multiprocessing import Process -from os import makedirs -from os.path import dirname, join, realpath +from os import environ from maro.rl import ( - Actor, ActorProxy, DQN, DQNConfig, FullyConnectedBlock, MultiAgentWrapper, OffPolicyLearner, + Actor, ActorProxy, DQN, DQNConfig, FullyConnectedBlock, MultiAgentWrapper, OffPolicyDistLearner, SimpleMultiHeadModel, TwoPhaseLinearParameterScheduler ) from maro.simulator import Env -from maro.utils import Logger, set_seeds +from maro.utils import set_seeds -from examples.cim.common import CIMEnvWrapper, common_config -from examples.cim.dqn.config import agent_config, training_config +from examples.cim.common import CIMEnvWrapper +from examples.cim.common_config import common_config +from examples.cim.dqn.config import config + + +GROUP = environ.get("GROUP", config["distributed"]["group"]) +REDIS_HOST = environ.get("REDISHOST", config["distributed"]["redis_host"]) +REDIS_PORT = environ.get("REDISPORT", config["distributed"]["redis_port"]) +NUM_ACTORS = environ.get("NUMACTORS", config["distributed"]["num_actors"]) def get_dqn_agent(): q_model = SimpleMultiHeadModel( - FullyConnectedBlock(**agent_config["model"]), optim_option=agent_config["optimization"] + FullyConnectedBlock(**config["agent"]["model"]), optim_option=config["agent"]["optimization"] ) - return DQN(q_model, DQNConfig(**agent_config["hyper_params"])) + return DQN(q_model, DQNConfig(**config["agent"]["hyper_params"])) def cim_dqn_learner(): - agent = MultiAgentWrapper({name: get_dqn_agent() for name in Env(**training_config["env"]).agent_idx_list}) - scheduler = TwoPhaseLinearParameterScheduler(training_config["max_episode"], **training_config["exploration"]) - actor = ActorProxy( - training_config["group"], training_config["num_actors"], - update_trigger=training_config["learner_update_trigger"] + agent = MultiAgentWrapper({name: get_dqn_agent() for name in Env(**config["training"]["env"]).agent_idx_list}) + scheduler = TwoPhaseLinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) + actor_proxy = ActorProxy( + NUM_ACTORS, GROUP, + proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, + update_trigger=config["distributed"]["learner_update_trigger"], + **config["training"]["replay_memory"] + ) + learner = OffPolicyDistLearner( + actor_proxy, agent, scheduler, + min_experiences_to_train=config["training"]["min_experiences_to_train"], + train_iter=config["training"]["train_iter"], + batch_size=config["training"]["batch_size"] ) - learner = OffPolicyLearner(actor, scheduler, agent, **training_config["training"]) learner.run() def cim_dqn_actor(): - env = Env(**training_config["env"]) + env = Env(**config["training"]["env"]) agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) actor = Actor( - CIMEnvWrapper(env, **common_config), agent, - group=training_config["group"], replay_sync_interval=training_config["replay_sync_interval"] + CIMEnvWrapper(env, **common_config), agent, GROUP, + replay_sync_interval=config["distributed"]["replay_sync_interval"], + proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)} ) actor.run() @@ -56,7 +68,7 @@ def cim_dqn_actor(): args = parser.parse_args() if args.whoami == 0: - actor_processes = [Process(target=cim_dqn_actor) for _ in range(training_config["num_actors"])] + actor_processes = [Process(target=cim_dqn_actor) for i in range(NUM_ACTORS)] learner_process = Process(target=cim_dqn_learner) for i, actor_process in enumerate(actor_processes): diff --git a/maro/communication/registry_table.py b/maro/communication/registry_table.py index 5a0197c8f..0c85843ff 100644 --- a/maro/communication/registry_table.py +++ b/maro/communication/registry_table.py @@ -272,7 +272,6 @@ def get(self) -> List[Tuple[callable, List[Message]]]: for event, handler_fn in self._event_handler_dict.items(): message_list = event.get_qualified_message() - if message_list: satisfied_handler_fn.append((handler_fn, message_list)) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 2d8e12495..ab30439a5 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -4,13 +4,14 @@ from maro.rl.agent import ( DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient ) +from maro.rl.distributed import AbsDistLearner, Actor, ActorProxy, OffPolicyDistLearner, OnPolicyDistLearner from maro.rl.exploration import ( AbsExplorer, EpsilonGreedyExplorer, GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer ) from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler from maro.rl.storage import AbsStore, OverwriteType, SimpleStore -from maro.rl.training import AbsEnvWrapper, AbsLearner, Actor, ActorProxy, OffPolicyLearner, OnPolicyLearner +from maro.rl.training import AbsEnvWrapper, AbsLearner, OffPolicyLearner, OnPolicyLearner from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_truncated_cumulative_reward, select_by_actions ) @@ -18,12 +19,12 @@ __all__ = [ "AbsAgent", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", "PolicyGradient", + "AbsDistLearner", "Actor", "ActorProxy", "OffPolicyDistLearner", "OnPolicyDistLearner", "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", "AbsStore", "OverwriteType", "SimpleStore", - "AbsEnvWrapper", "AbsLearner", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner" - "TrajectorySyncActor", + "AbsEnvWrapper", "AbsLearner", "OffPolicyLearner", "OnPolicyLearner", "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_truncated_cumulative_reward", "select_by_actions" ] diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py index 0c80a6a6e..015061b9f 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/agent/ac.py @@ -9,7 +9,7 @@ from torch.nn import MSELoss from maro.rl.model import SimpleMultiHeadModel -from maro.rl.utils import get_lambda_returns, get_log_prob +from maro.rl.utils import get_log_prob from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask from .abs_agent import AbsAgent @@ -24,10 +24,6 @@ class ActorCriticConfig: train_iters (int): Number of gradient descent steps per call to ``train``. actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. - k (int): Number of time steps used in computing returns or return estimates. Defaults to -1, in which case - rewards are accumulated until the end of the trajectory. - lam (float): Lambda coefficient used in computing lambda returns. Defaults to 1.0, in which case the usual - k-step return is computed. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. """ @@ -41,16 +37,12 @@ def __init__( train_iters: int, critic_loss_func: Callable = MSELoss(), actor_loss_coefficient: float = 1.0, - k: int = -1, - lam: float = 1.0, clip_ratio: float = None ): self.reward_discount = reward_discount self.critic_loss_func = critic_loss_func self.train_iters = train_iters self.actor_loss_coefficient = actor_loss_coefficient - self.k = k - self.lam = lam self.clip_ratio = clip_ratio @@ -90,20 +82,25 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: log_p = action_prob.log_prob(action) action, log_p = action.cpu().numpy(), log_p.cpu().numpy() return (action[0], log_p[0]) if is_single else (action, log_p) - + def learn( - self, states: np.ndarray, actions: np.ndarray, log_p: np.ndarray, rewards: np.ndarray + self, + states: np.ndarray, + actions: np.ndarray, + log_p: np.ndarray, + rewards: np.ndarray, + next_states: np.ndarray ): states = torch.from_numpy(states).to(self.device) actions = torch.from_numpy(actions).to(self.device) log_p = torch.from_numpy(log_p).to(self.device) rewards = torch.from_numpy(rewards).to(self.device) + next_states = torch.from_numpy(next_states).to(self.device) state_values = self.model(states, task_name="critic").detach().squeeze() - return_est = get_lambda_returns( - rewards, state_values, self.config.reward_discount, self.config.lam, k=self.config.k - ) - advantages = return_est - state_values + next_state_values = self.model(next_states, task_name="critic").detach().squeeze() + return_est = rewards + self.config.reward_discount * next_state_values + advantages = return_est - state_values for i in range(self.config.train_iters): # actor loss @@ -119,4 +116,5 @@ def learn( state_values = self.model(states, task_name="critic").squeeze() critic_loss = self.config.critic_loss_func(state_values, return_est) loss = critic_loss + self.config.actor_loss_coefficient * actor_loss + self.model.step(loss) diff --git a/maro/rl/agent/ddpg.py b/maro/rl/agent/ddpg.py index 880bc3ea2..a4be15e85 100644 --- a/maro/rl/agent/ddpg.py +++ b/maro/rl/agent/ddpg.py @@ -67,7 +67,7 @@ def choose_action(self, state) -> Union[float, np.ndarray]: if is_single: state = state.unsqueeze(dim=0) - action = self.model(state, task_name="policy", training=False).data.numpy() + action = self.model(state, task_name="policy", training=False).data.cpu().numpy() action_dim = action.shape[1] if self._explorer: action = self._explorer(action) diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index d27248d39..ff6408598 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -81,7 +81,7 @@ def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: q_values = self._get_q_values(state, training=False) num_actions = q_values.shape[1] - greedy_action = q_values.argmax(dim=1).data + greedy_action = q_values.argmax(dim=1).data.cpu() # No exploration if self.config.epsilon == .0: return greedy_action.item() if is_single else greedy_action.numpy() diff --git a/maro/rl/distributed/__init__.py b/maro/rl/distributed/__init__.py new file mode 100644 index 000000000..1abf4e3e7 --- /dev/null +++ b/maro/rl/distributed/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .actor import Actor +from .actor_proxy import ActorProxy +from .learner import AbsDistLearner, OffPolicyDistLearner, OnPolicyDistLearner + +__all__ = ["AbsDistLearner", "Actor", "ActorProxy", "OffPolicyDistLearner", "OnPolicyDistLearner"] diff --git a/maro/rl/training/actor.py b/maro/rl/distributed/actor.py similarity index 73% rename from maro/rl/training/actor.py rename to maro/rl/distributed/actor.py index 65a7fc32c..3672a4973 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/distributed/actor.py @@ -7,10 +7,10 @@ from maro.communication import Message, Proxy, RegisterTable, SessionType from maro.rl.agent import AbsAgent, MultiAgentWrapper -from maro.rl.storage import SimpleStore +from maro.rl.storage import OverwriteType, SimpleStore +from maro.rl.training import AbsEnvWrapper from maro.utils import InternalLogger -from .env_wrapper import AbsEnvWrapper from .message_enums import MsgTag, MsgKey @@ -18,7 +18,8 @@ class Actor(object): """On-demand roll-out executor. Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance with an env wrapped inside. + env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific + processing logic and stores transitions during roll-outs in a replay memory. agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). @@ -30,7 +31,7 @@ def __init__( self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], - group: str = None, + group: str, proxy_options: dict = None, replay_sync_interval: int = None, send_results: bool = True @@ -39,14 +40,12 @@ def __init__( raise ValueError("replay_sync_interval must be a positive integer or None") self.env = env self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent - if group is not None: - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group, "actor", {"actor_proxy": 1}, **proxy_options) - self._logger = InternalLogger(self._proxy.name) - + if proxy_options is None: + proxy_options = {} + self._proxy = Proxy(group, "actor", {"actor_proxy": 1}, **proxy_options) self.replay_sync_interval = replay_sync_interval self.send_results = send_results + self._logger = InternalLogger(self._proxy.name) def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): self.env.reset() @@ -63,12 +62,18 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non while state: action = self.agent.choose_action(state) state = self.env.step(action) - self.on_env_feedback(index) + if self.replay_sync_interval is not None and (self.env.step_index + 1) % self.replay_sync_interval == 0: + self._proxy.isend( + Message( + MsgTag.REPLAY_SYNC, self._proxy.name, self._proxy.peers["actor_proxy"][0], + body={MsgKey.ROLLOUT_INDEX: index, MsgKey.REPLAY: self.env.replay_memory} + ) + ) + self.env.flush() return self.env.metrics def run(self): - assert hasattr(self, "_proxy"), "No proxy found. The `group` parameter should not be None at init." for msg in self._proxy.receive(): if msg.tag == MsgTag.EXIT: self._logger.info("Exiting...") @@ -88,24 +93,15 @@ def on_rollout_request(self, msg: Message): ) self._logger.info(f"Roll-out {ep} finished") - def on_env_feedback(self, index): - if self.replay_sync_interval is not None and (self.env.step_index + 1) % self.replay_sync_interval == 0: - self._proxy.isend( - Message( - MsgTag.REPLAY_SYNC, self._proxy.name, self._proxy.peers["actor_proxy"][0], - body={MsgKey.ROLLOUT_INDEX: index, MsgKey.REPLAY: self.env.replay} - ) - ) - self.env.flush() - def post_rollout(self, index: int): if not self.send_results: return body = {MsgKey.ROLLOUT_INDEX: index, MsgKey.METRICS: self.env.metrics} if self.env.record_path: - body[MsgKey.REPLAY] = self.env.replay + body[MsgKey.REPLAY] = self.env.replay_memory + actor_proxy_addr = self._proxy.peers["actor_proxy"][0] self._proxy.isend( - Message(MsgTag.ROLLOUT_DONE, self._proxy.name, self._proxy.peers["actor_proxy"][0], body=body) + Message(MsgTag.ROLLOUT_DONE, self._proxy.name, actor_proxy_addr, body=body) ) diff --git a/maro/rl/training/actor_proxy.py b/maro/rl/distributed/actor_proxy.py similarity index 82% rename from maro/rl/training/actor_proxy.py rename to maro/rl/distributed/actor_proxy.py index 8980cd5e2..95e72afec 100644 --- a/maro/rl/training/actor_proxy.py +++ b/maro/rl/distributed/actor_proxy.py @@ -16,22 +16,22 @@ class ActorProxy(object): """Actor proxy that manages a set of remote actors. Args: + num_actors (int): Expected number of actors in the group identified by ``group_name``. group_name (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the actors (and roll-out clients, if any). - num_actors (int): Expected number of actors in the group identified by ``group_name``. - update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger - learner updates, i.e., model training. proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. + update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger + learner updates, i.e., model training. """ def __init__( self, - group_name: str, num_actors: int, - update_trigger: str = None, + group_name: str, proxy_options: dict = None, - experience_pool_capacity: int = -1, - experience_pool_overwrite: OverwriteType = None + update_trigger: str = None, + replay_memory_size: int = -1, + replay_memory_overwrite_type: OverwriteType = None ): peers = {"actor": num_actors} if proxy_options is None: @@ -44,16 +44,10 @@ def __init__( self._registry_table.register_event_handler( f"actor:{MsgTag.ROLLOUT_DONE.value}:{update_trigger}", self._on_rollout_finish ) - - def get_experience_pool(): - return SimpleStore( - ["S", "A", "R", "S_", "loss"], - capacity=experience_pool_capacity, - overwrite_type=experience_pool_overwrite - ) - - self.experience_pool = defaultdict(lambda: get_experience_pool()) - self.logger = InternalLogger("ACTOR_PROXY") + self.replay_memory = defaultdict( + lambda: SimpleStore(capacity=replay_memory_size, overwrite_type=replay_memory_overwrite_type) + ) + self.logger = InternalLogger(self._proxy.name) def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): """Collect roll-out data from remote actors. @@ -86,24 +80,22 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non # the next episode. result = self._registry_table.push(msg) if result: - env_metrics = result[0] + env_metrics = result break elif msg.tag == MsgTag.REPLAY_SYNC: # print(f"received exp from actor {msg.source} ") # print({agent_id: {k: len(v) for k, v in exp.items()} for agent_id, exp in msg.body[MsgKey.REPLAY].items()}) for agent_id, exp in msg.body[MsgKey.REPLAY].items(): - self.experience_pool[agent_id].put(exp) - # print({agent_id: len(pool) for agent_id, pool in self.experience_pool.items()}) + self.replay_memory[agent_id].put(exp) + # print({agent_id: len(pool) for agent_id, pool in self.replay_memory.items()}) return env_metrics def _on_rollout_finish(self, messages: List[Message]): metrics = {msg.source: msg.body[MsgKey.METRICS] for msg in messages} for msg in messages: - print({agent_id: {k: len(v) for k, v in exp.items()} for agent_id, exp in msg.body[MsgKey.REPLAY].items()}) - for agent_id, exp in msg.body[MsgKey.REPLAY].items(): - self.experience_pool[agent_id].put(exp) - print({agent_id: len(pool) for agent_id, pool in self.experience_pool.items()}) + for agent_id, replay in msg.body[MsgKey.REPLAY].items(): + self.replay_memory[agent_id].put(replay) return metrics def terminate(self): diff --git a/maro/rl/training/dispatcher.py b/maro/rl/distributed/dispatcher.py similarity index 100% rename from maro/rl/training/dispatcher.py rename to maro/rl/distributed/dispatcher.py diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py new file mode 100644 index 000000000..35e5b451f --- /dev/null +++ b/maro/rl/distributed/learner.py @@ -0,0 +1,100 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from typing import Callable, Union + +from numpy import asarray + +from maro.rl.agent import AbsAgent, MultiAgentWrapper +from maro.rl.scheduling import Scheduler +from maro.rl.storage import SimpleStore +from maro.utils import InternalLogger + +from .actor_proxy import ActorProxy + + +class AbsDistLearner(ABC): + """Learner class for distributed training. + + Args: + actor_proxy (ActorProxy): ``ActorProxy`` instance that manages a set of remote actors to collect roll-out + data for learning purposes. + agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. + """ + def __init__(self, actor_proxy: ActorProxy, agent: Union[AbsAgent, MultiAgentWrapper]): + super().__init__() + self.actor_proxy = actor_proxy + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent + self.logger = InternalLogger("LEARNER") + + @abstractmethod + def run(self): + """Main learning loop is implemented here.""" + return NotImplementedError + + +class OnPolicyDistLearner(AbsDistLearner): + def __init__(self, actor_proxy: ActorProxy, agent: Union[AbsAgent, MultiAgentWrapper], max_episode: int): + super().__init__(actor_proxy, agent) + self.max_episode = max_episode + + def run(self): + for ep in range(self.max_episode): + env_metrics = self.actor_proxy.roll_out(ep, model_by_agent=self.agent.dump_model()) + self.logger.info(f"ep-{ep}: {env_metrics}") + for agent_id, replay in self.actor_proxy.replay_memory.items(): + self.agent[agent_id].learn(replay["S"], replay["A"], replay["LOGP"], replay["R"], replay["S_"]) + + self.logger.info("Agent learning finished") + + # Signal remote actors to quit + self.actor_proxy.terminate() + + +class OffPolicyDistLearner(AbsDistLearner): + def __init__( + self, + actor_proxy: ActorProxy, + agent: Union[AbsAgent, MultiAgentWrapper], + scheduler: Scheduler, + train_iter: int = 1, + min_experiences_to_train: int = 0, + batch_size: int = 128 + ): + super().__init__(actor_proxy, agent) + self.scheduler = scheduler + self.train_iter = train_iter + self.min_experiences_to_train = min_experiences_to_train + self.batch_size = batch_size + + def run(self): + for exploration_params in self.scheduler: + rollout_index = self.scheduler.iter + env_metrics = self.actor_proxy.roll_out( + rollout_index, model_by_agent=self.agent.dump_model(), exploration_params=exploration_params + ) + self.logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") + + for _ in range(self.train_iter): + batch_by_agent, idx_by_agent = self.get_batch() + for agent_id, batch in batch_by_agent.items(): + self.agent[agent_id].learn(*batch) + + self.logger.info("Agent learning finished") + + # Signal remote actors to quit + self.actor_proxy.terminate() + + def get_batch(self): + idx, batch = {}, {} + for agent_id, mem in self.actor_proxy.replay_memory.items(): + if len(mem) < self.min_experiences_to_train: + continue + indexes, sample = mem.sample(self.batch_size) + batch[agent_id] = ( + asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) + ) + idx[agent_id] = indexes + + return batch, idx diff --git a/maro/rl/training/message_enums.py b/maro/rl/distributed/message_enums.py similarity index 100% rename from maro/rl/training/message_enums.py rename to maro/rl/distributed/message_enums.py diff --git a/maro/rl/training/trainer.py b/maro/rl/distributed/trainer.py similarity index 100% rename from maro/rl/training/trainer.py rename to maro/rl/distributed/trainer.py diff --git a/maro/rl/storage/simple_store.py b/maro/rl/storage/simple_store.py index 8a3038f13..e3a5633db 100644 --- a/maro/rl/storage/simple_store.py +++ b/maro/rl/storage/simple_store.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import defaultdict from enum import Enum from typing import Callable, Dict, List, Tuple, Union @@ -27,7 +28,6 @@ class SimpleStore(AbsStore): and limited storage are supported. Args: - keys (list): List of keys identifying each column. capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. overwrite_type (OverwriteType): If storage capacity is bounded, this specifies how existing entries are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: @@ -35,12 +35,11 @@ class SimpleStore(AbsStore): - Random, where overwrite occurs randomly among filled positions. Alternatively, the user may also specify overwrite positions (see ``put``). """ - def __init__(self, keys: list, capacity: int = -1, overwrite_type: OverwriteType = None): + def __init__(self, capacity: int = -1, overwrite_type: OverwriteType = None): super().__init__() - self._keys = keys self._capacity = capacity self._overwrite_type = overwrite_type - self.data = {key: [] if self._capacity < 0 else [None] * self._capacity for key in keys} + self.data = defaultdict(list) if capacity == -1 else defaultdict(lambda: [None] * capacity) self._size = 0 def __len__(self): @@ -49,10 +48,6 @@ def __len__(self): def __getitem__(self, index: int): return {k: lst[index] for k, lst in self.data.items()} - @property - def keys(self): - return self._keys - @property def capacity(self): """Store capacity. @@ -83,12 +78,14 @@ def put(self, contents: Dict[str, List], overwrite_indexes: list = None) -> List Returns: The indexes where the newly added entries reside in the store. """ - if len(self.data) > 0 and list(contents.keys()) != self._keys: - raise StoreMisalignment(f"expected keys {self._keys}, got {list(contents.keys())}") + if len(self.data) > 0: + expected_keys, actual_keys = list(self.data.keys()), list(contents.keys()) + if expected_keys != actual_keys: + raise StoreMisalignment(f"expected keys {expected_keys}, got {actual_keys}") self.validate(contents) added = contents[next(iter(contents))] added_size = len(added) if isinstance(added, list) else 1 - if self._capacity < 0: + if self._capacity == -1: for key, val in contents.items(): self.data[key].extend(val) self._size += added_size @@ -175,45 +172,9 @@ def sample(self, size, weights: Union[list, np.ndarray] = None, replace: bool = indexes = np.random.choice(self._size, size=size, replace=replace, p=weights) return indexes, self.get(indexes) - def sample_by_key(self, key, size: int, replace: bool = True): - """ - Obtain a random sample from the store using one of the columns as sampling weights. - - Args: - key: The column whose values are to be used as sampling weights. - size (int): Sample size. - replace (bool): If True, sampling is performed with replacement. - Returns: - Sampled indexes and the corresponding objects. - """ - weights = np.asarray(self.data[key][:self._size] if self._size < self._capacity else self.data[key]) - indexes = np.random.choice(self._size, size=size, replace=replace, p=weights / np.sum(weights)) - return indexes, self.get(indexes) - - def sample_by_keys(self, keys: list, sizes: list, replace: bool = True): - """ - Obtain a random sample from the store by chained sampling using multiple columns as sampling weights. - - Args: - keys (list): The column whose values are to be used as sampling weights. - sizes (list): Sample size. - replace (bool): If True, sampling is performed with replacement. - Returns: - Sampled indexes and the corresponding objects. - """ - if len(keys) != len(sizes): - raise ValueError(f"expected sizes of length {len(keys)}, got {len(sizes)}") - - indexes = range(self._size) - for key, size in zip(keys, sizes): - weights = np.asarray([self.data[key][i] for i in indexes]) - indexes = np.random.choice(indexes, size=size, replace=replace, p=weights / np.sum(weights)) - - return indexes, self.get(indexes) - def clear(self): """Empty the store.""" - self.data = {key: [] if self._capacity < 0 else [None] * self._capacity for key in self._keys} + self.data = defaultdict(list) if self._capacity == -1 else defaultdict(lambda: [None] * self._capacity) self._size = 0 def dumps(self): @@ -224,6 +185,9 @@ def get_by_key(self, key): """Get the contents of the store corresponding to ``key``.""" return self.data[key] + def insert(self, key: str, default_val=None): + self.data[key] = [default_val for _ in range(self._size)] + def _get_update_indexes(self, added_size: int, overwrite_indexes=None): if added_size > self._capacity: raise ValueError("size of added items should not exceed the store capacity.") diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 69a5b5c09..382eb87ec 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -1,11 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .actor import Actor -from .actor_proxy import ActorProxy from .env_wrapper import AbsEnvWrapper from .learner import AbsLearner, OffPolicyLearner, OnPolicyLearner -__all__ = [ - "AbsEnvWrapper", "AbsLearner", "Actor", "ActorProxy", "OffPolicyLearner", "OnPolicyLearner" -] +__all__ = ["AbsEnvWrapper", "AbsLearner", "OffPolicyLearner", "OnPolicyLearner"] diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index b118b806f..0de98c2c9 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -3,16 +3,10 @@ from abc import ABC, abstractmethod from collections import defaultdict -from multiprocessing import Pipe, Process from typing import Callable -from maro.communication import Message, Proxy from maro.simulator import Env -from .message_enums import MsgTag, MsgKey - -MAX_LOSS = 1e8 - class AbsEnvWrapper(ABC): """Environment wrapper that performs various shaping and other roll-out related logic. @@ -21,32 +15,42 @@ class AbsEnvWrapper(ABC): env (Env): Environment instance. record_path (bool): If True, the steps during roll-out will be recorded sequentially. This includes states, actions and rewards. The decision events themselves will also be recorded - for hindsight reward evaluation purposes. Defaults to True. - hindsight_reward_window (int): Number of ticks required after a decision event to evaluate - the reward for the action taken for that event. Defaults to 0, which rewards are evaluated immediately + for delayed reward evaluation purposes. Defaults to True. + reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward + for the action taken for that event. Defaults to 0, which rewards are evaluated immediately after executing an action. """ - def __init__(self, env: Env, record_path: bool = True, hindsight_reward_window: int = 0): + def __init__(self, env: Env, record_path: bool = True, reward_eval_delay: int = 0): self.env = env self.step_index = None - self.replay_memory = defaultdict(lambda: {key: [] for key in ["S", "A", "R", "S_", "loss"]}) + self.replay = defaultdict(lambda: defaultdict(list)) self.events = [] self.acting_agents = [] self.record_path = record_path - self.hindsight_reward_window = hindsight_reward_window + self.reward_eval_delay = reward_eval_delay self._pending_reward_idx = 0 def start(self, rollout_index: int = None): self.step_index = 0 self._pending_reward_idx = 0 _, event, _ = self.env.step(None) - return self._on_new_event(event) + state_by_agent = self.get_state(event) + if self.record_path: + self.events.append((event, list(state_by_agent.keys()))) + for agent_id, state in state_by_agent.items(): + replay = self.replay[agent_id] + if replay["S"]: + replay["S_"].append(state) + replay["S"].append(state) + assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 + + return state_by_agent @property - def replay(self): + def replay_memory(self): return { agent_id: {k: vals[:len(replay["R"])] for k, vals in replay.items()} - for agent_id, replay in self.replay_memory.items() + for agent_id, replay in self.replay.items() } @property @@ -64,14 +68,14 @@ def get_action(self, action, event) -> dict: def get_reward(self) -> float: """Get the immediate reward for an action. - This can be left blank if rewards are evaluated in hindsight. + This can be left blank if ``get_reward_for`` is implemented. """ pass - def get_hindsight_reward(self, event): - """Get the reward for an action that occurred a certain number of ticks ago. + def get_reward_for(self, event) -> float: + """Get the reward for an action in response to an event that occurred a certain number of ticks ago. - If implemented, whatever value ``get_reward`` gives will be ignored in the output of ``get_path``. + If implemented, whatever value ``get_reward`` gives will be ignored in the output of ``replay_memory``. If left blank, ``get_reward`` must be implemented. """ pass @@ -85,60 +89,61 @@ def step(self, action_by_agent: dict): _, event, done = self.env.step(env_action) if self.record_path: - if self.hindsight_reward_window: + if self.reward_eval_delay: for agent_id, action in action_by_agent.items(): - self.replay_memory[agent_id]["A"].append(action) - self._assign_hindsight_rewards(tick=event.tick if not done else None) + if isinstance(action, tuple): + self.replay[agent_id]["A"].append(action[0]) + self.replay[agent_id]["LOGP"].append(action[1]) + else: + self.replay[agent_id]["A"].append(action) + """ + If roll-out is complete, evaluate rewards for all remaining events except the last. + Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. + """ + for i, (evt, agents) in enumerate(self.events[self._pending_reward_idx:]): + if not done and event.tick - evt.tick < self.reward_eval_delay: + self._pending_reward_idx += i + break + reward = self.get_reward_for(evt) + for agent_id in agents: + if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): + self.replay[agent_id]["R"].append(reward) + + if done: + self._pending_reward_idx = len(self.events) - 1 else: - reward = self.get_reward() for agent_id, action in action_by_agent.items(): - self.replay_memory[agent_id]["A"].append(action) - self.replay_memory[agent_id]["R"].append(reward) - self._pending_reward_idx += 1 + if isinstance(action, tuple): + self.replay[agent_id]["A"].append(action[0]) + self.replay[agent_id]["LOGP"].append(action[1]) + else: + self.replay[agent_id]["A"].append(action) + self.replay[agent_id]["R"].append(reward) + self._pending_reward_idx += 1 if not done: - return self._on_new_event(event) + state_by_agent = self.get_state(event) + if self.record_path: + self.events.append((event, list(state_by_agent.keys()))) + for agent_id, state in state_by_agent.items(): + replay = self.replay[agent_id] + if replay["S"]: + replay["S_"].append(state) + replay["S"].append(state) + assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 + + return state_by_agent def reset(self): self.env.reset() self.events.clear() - self.replay_memory = defaultdict(lambda: {key: [] for key in ["S", "A", "R", "S_", "loss"]}) + self.replay = defaultdict(lambda: defaultdict(list)) def flush(self): - for agent_id in self.replay_memory: - num_complete = len(self.replay_memory[agent_id]["R"]) - del self.replay_memory[agent_id]["S"][:num_complete] - del self.replay_memory[agent_id]["A"][:num_complete] - del self.replay_memory[agent_id]["R"][:num_complete] - del self.replay_memory[agent_id]["S_"][:num_complete] - del self.replay_memory[agent_id]["loss"][:num_complete] + for replay in self.replay.values(): + num_complete = len(replay["R"]) + for vals in replay.values(): + del vals[:num_complete] del self.events[:self._pending_reward_idx] self._pending_reward_idx = 0 - - def _on_new_event(self, event): - state_by_agent = self.get_state(event) - if self.record_path: - self.events.append((event, list(state_by_agent.keys()))) - for agent_id, state in state_by_agent.items(): - if self.replay_memory[agent_id]["S"]: - self.replay_memory[agent_id]["S_"].append(state) - self.replay_memory[agent_id]["loss"].append(MAX_LOSS) - self.replay_memory[agent_id]["S"].append(state) - - # for agent_id, exp in self.replay_memory.items(): - # ns, na, nr, ns_ = len(exp["S"]), len(exp["A"]), len(exp["R"]), len(exp["S_"]) - # print(f"agent_id: {agent_id}, state: {ns}, action: {na}, reward: {nr}, state_: {ns_}") - - return state_by_agent - - def _assign_hindsight_rewards(self, tick=None): - while ( - self._pending_reward_idx < len(self.events) and - (tick is None or tick - self.events[self._pending_reward_idx][0].tick >= self.hindsight_reward_window) - ): - event, acting_agents = self.events[self._pending_reward_idx] - hindsight_reward = self.get_hindsight_reward(event) - for agent_id in acting_agents: - self.replay_memory[agent_id]["R"].append(hindsight_reward) - self._pending_reward_idx += 1 diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index aa8275086..347a3207c 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -11,31 +11,33 @@ from maro.rl.storage import SimpleStore from maro.utils import InternalLogger -from .actor import Actor -from .actor_proxy import ActorProxy +from .env_wrapper import AbsEnvWrapper class AbsLearner(ABC): - """Learner class. + """Learner class for distributed training. Args: - actor (Union[Actor, ActorProxy]): ``Actor`` or ``ActorProxy`` instance responsible for collecting roll-out - data for learning purposes. If it is an ``Actor``, it will perform roll-outs locally. If it is an - ``ActorProxy``, it will coordinate a set of remote actors to perform roll-outs in parallel. - agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. If None, the actor must be an ``Actor`` that - contains actual agents, rather than an ``ActorProxy``. Defaults to None. + env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific + processing logic and stores transitions during roll-outs in a replay memory. + agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. """ - def __init__(self, actor: Union[Actor, ActorProxy], agent: Union[AbsAgent, MultiAgentWrapper] = None): + def __init__(self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper]): super().__init__() - if isinstance(actor, ActorProxy): - assert agent, "agent cannot be None when the actor is a proxy." - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent - else: - # The agent passed to __init__ is ignored in this case - self.agent = actor.agent - self.actor = actor + self.env = env + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent self.logger = InternalLogger("LEARNER") + def roll_out(self, index: int, training: bool = True): + self.env.reset() + if not training: + self.env.record_path = False # no need to record the trajectory if roll-out is not for training + + state = self.env.start(rollout_index=index) # get initial state + while state: + action = self.agent.choose_action(state) + state = self.env.step(action) + @abstractmethod def run(self): """Main learning loop is implemented here.""" @@ -43,86 +45,68 @@ def run(self): class OnPolicyLearner(AbsLearner): - def __init__( - self, - actor: Union[Actor, ActorProxy], - max_episode: int, - agent: Union[AbsAgent, MultiAgentWrapper] = None - ): - super().__init__(actor, agent=agent) + def __init__(self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], max_episode: int): + super().__init__(env, agent) self.max_episode = max_episode def run(self): for ep in range(self.max_episode): - env_metrics = self.actor.roll_out( - ep, model_by_agent=self.agent.dump_model() if isinstance(self.actor, ActorProxy) else None - ) - self.logger.info(f"ep-{ep}: {env_metrics}") - for agent_id, ex in exp.items(): - for e in ex: - self.agent[agent_id].learn(*e["args"], **e.get("kwargs", {})) + self.roll_out(ep) + self.logger.info(f"ep-{ep}: {self.env.metrics}") + for agent_id, replay in self.env.replay_memory.items(): + self.agent[agent_id].learn( + asarray(replay["S"]), + asarray(replay["A"]), + asarray(replay["LOGP"]), + asarray(replay["R"]), + asarray(replay["S_"]) + ) self.logger.info("Agent learning finished") - # Signal remote actors to quit - if isinstance(self.actor, ActorProxy): - self.actor.terminate() - - -MAX_LOSS = 1e8 - class OffPolicyLearner(AbsLearner): def __init__( self, - actor: Union[Actor, ActorProxy], + env: AbsEnvWrapper, + agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, - agent: Union[AbsAgent, MultiAgentWrapper] = None, train_iter: int = 1, min_experiences_to_train: int = 0, - batch_size: int = 128, - prioritized_sampling_by_loss: bool = False + batch_size: int = 128 ): - super().__init__(actor, agent=agent) + super().__init__(env, agent) self.scheduler = scheduler self.train_iter = train_iter self.min_experiences_to_train = min_experiences_to_train self.batch_size = batch_size - self.prioritized_sampling_by_loss = prioritized_sampling_by_loss + self.replay_memory = defaultdict( + lambda: SimpleStore(capacity=replay_memory_size, overwrite_type=replay_memory_overwrite_type) + ) def run(self): for exploration_params in self.scheduler: rollout_index = self.scheduler.iter - env_metrics = self.actor.roll_out( - rollout_index, - model_by_agent=self.agent.dump_model() if isinstance(self.actor, ActorProxy) else None, - exploration_params=exploration_params - ) - self.logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") + self.roll_out(rollout_index) + self.logger.info(f"ep-{rollout_index}: {self.env.metrics} ({exploration_params})") + # Add the latest transitions to the replay memory + for agent_id, mem in self.env.replay_memory.items(): + self.replay_memory[agent_id].put(mem) + # Training for _ in range(self.train_iter): batch_by_agent, idx_by_agent = self.get_batch() - loss_by_agent = { - agent_id: self.agent[agent_id].learn(*batch) for agent_id, batch in batch_by_agent.items() - } - for agent_id, loss in loss_by_agent.items(): - self.actor.experience_pool[agent_id].update(idx_by_agent[agent_id], {"loss": list(loss)}) + for agent_id, batch in batch_by_agent.items(): + self.agent[agent_id].learn(*batch) self.logger.info("Agent learning finished") - # Signal remote actors to quit - if isinstance(self.actor, ActorProxy): - self.actor.terminate() - def get_batch(self): idx, batch = {}, {} - for agent_id, pool in self.actor.experience_pool.items(): - if len(pool) < self.min_experiences_to_train: + for agent_id, mem in self.replay_memory.items(): + if len(mem) < self.min_experiences_to_train: continue - if self.prioritized_sampling_by_loss: - indexes, sample = self.actor.experience_pool[agent_id].sample_by_key("loss", self.batch_size) - else: - indexes, sample = self.actor.experience_pool[agent_id].sample(self.batch_size) + indexes, sample = mem.sample(self.batch_size) batch[agent_id] = ( asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) ) diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index b0838f0d8..22f5736a9 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -29,7 +29,7 @@ " \"finite_vessel_space\": True,\n", " \"has_early_discharge\": True,\n", " # Parameters for computing rewards\n", - " \"reward_time_window\": 99,\n", + " \"reward_eval_delay\": 99,\n", " \"fulfillment_factor\": 1.0,\n", " \"shortage_factor\": 1.0,\n", " \"time_decay\": 0.97\n", @@ -58,7 +58,7 @@ "class CIMTrajectory(Trajectory):\n", " def __init__(\n", " self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream,\n", - " reward_time_window, fulfillment_factor, shortage_factor, time_decay,\n", + " reward_eval_delay, fulfillment_factor, shortage_factor, time_decay,\n", " finite_vessel_space=True, has_early_discharge=True \n", " ):\n", " super().__init__(env)\n", @@ -67,7 +67,7 @@ " self.action_space = action_space\n", " self.look_back = look_back\n", " self.max_ports_downstream = max_ports_downstream\n", - " self.reward_time_window = reward_time_window\n", + " self.reward_eval_delay = reward_eval_delay\n", " self.fulfillment_factor = fulfillment_factor\n", " self.shortage_factor = shortage_factor\n", " self.time_decay = time_decay\n", @@ -108,13 +108,13 @@ " def get_offline_reward(self, event):\n", " port_snapshots = self.env.snapshot_list[\"ports\"]\n", " start_tick = event.tick + 1\n", - " ticks = list(range(start_tick, start_tick + self.reward_time_window))\n", + " ticks = list(range(start_tick, start_tick + self.reward_eval_delay))\n", "\n", " future_fulfillment = port_snapshots[ticks::\"fulfillment\"]\n", " future_shortage = port_snapshots[ticks::\"shortage\"]\n", " decay_list = [\n", - " self.time_decay ** i for i in range(self.reward_time_window)\n", - " for _ in range(future_fulfillment.shape[0] // self.reward_time_window)\n", + " self.time_decay ** i for i in range(self.reward_eval_delay)\n", + " for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay)\n", " ]\n", "\n", " tot_fulfillment = np.dot(future_fulfillment, decay_list)\n", From 6bad1c1a3b1d1b4956c0033b83910423cd0a4840 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 29 Mar 2021 11:02:51 +0800 Subject: [PATCH 118/482] fix lint issue --- maro/simulator/scenarios/supply_chain/units/distribution.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 8c40d0759..bac1c1a23 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -101,10 +101,8 @@ def step(self, tick: int): # NOTE: we moved delay_order_penalty from facility to sku, is this ok? # update order's delay penalty per tick. for order in self.order_queue: - sku = self.facility.skus[order.product_id] product_index = self.product_index_mapping[order.product_id] - #self.data_model.delay_order_penalty[product_index] += sku.delay_order_penalty self.data_model.delay_order_penalty[product_index] += self.facility.get_config("delay_order_penalty") def flush_states(self): From e542af4248e04c59722ce23f6f14075e2122c7fd Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 29 Mar 2021 06:05:23 +0000 Subject: [PATCH 119/482] 1. added torch cls index; 2. refactored CIM example configs to yml --- examples/cim/ac/config.py | 60 ----------- examples/cim/ac/config.yml | 69 ++++++++++++ examples/cim/ac/main.py | 36 +++++-- examples/cim/common_config.py | 21 ---- examples/cim/dqn/config.py | 71 ------------- examples/cim/dqn/config.yml | 77 ++++++++++++++ examples/cim/dqn/main.py | 40 ++++--- examples/cim/{common.py => env_wrapper.py} | 23 +--- maro/rl/__init__.py | 11 +- maro/rl/agent/ac.py | 9 +- maro/rl/agent/ddpg.py | 11 +- maro/rl/agent/dqn.py | 9 +- maro/rl/distributed/actor.py | 2 +- maro/rl/distributed/actor_proxy.py | 4 +- maro/rl/model/fc_block.py | 12 ++- maro/rl/model/learning_model.py | 12 ++- maro/rl/storage/__init__.py | 4 +- maro/rl/storage/simple_store.py | 19 ++-- maro/rl/utils/__init__.py | 4 + maro/rl/utils/torch_cls_index.py | 118 +++++++++++++++++++++ tests/test_store.py | 6 +- 21 files changed, 382 insertions(+), 236 deletions(-) delete mode 100644 examples/cim/ac/config.py create mode 100644 examples/cim/ac/config.yml delete mode 100644 examples/cim/common_config.py delete mode 100644 examples/cim/dqn/config.py create mode 100644 examples/cim/dqn/config.yml rename examples/cim/{common.py => env_wrapper.py} (81%) create mode 100644 maro/rl/utils/torch_cls_index.py diff --git a/examples/cim/ac/config.py b/examples/cim/ac/config.py deleted file mode 100644 index 54ace3937..000000000 --- a/examples/cim/ac/config.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from torch import nn -from torch.optim import Adam, RMSprop - -from maro.rl import OptimOption - -from examples.cim.common import common_config - -input_dim = ( - (common_config["look_back"] + 1) * - (common_config["max_ports_downstream"] + 1) * - len(common_config["port_attributes"]) + - len(common_config["vessel_attributes"]) -) - -config = { - "agent": { - "model": { - "actor": { - "input_dim": input_dim, - "output_dim": len(common_config["action_space"]), - "hidden_dims": [256, 128, 64], - "activation": nn.Tanh, - "softmax": True, - "batch_norm": False, - "head": True - }, - "critic": { - "input_dim": input_dim, - "output_dim": 1, - "hidden_dims": [256, 128, 64], - "activation": nn.LeakyReLU, - "softmax": False, - "batch_norm": True, - "head": True - } - }, - "optimization": { - "actor": OptimOption(optim_cls=Adam, optim_params={"lr": 0.001}), - "critic": OptimOption(optim_cls=RMSprop, optim_params={"lr": 0.001}) - }, - "hyper_params": { - "reward_discount": .0, - "critic_loss_func": nn.SmoothL1Loss(), - "train_iters": 10, - "actor_loss_coefficient": 0.1, - # "clip_ratio": 0.8 - } - }, - "training": { - "env": { - "scenario": "cim", - "topology": "toy.4p_ssdd_l0.0", - "durations": 1120, - }, - "max_episode": 50 - } -} diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml new file mode 100644 index 000000000..25c62cce0 --- /dev/null +++ b/examples/cim/ac/config.yml @@ -0,0 +1,69 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +shaping: + port_attributes: + - empty + - full + - on_shipper + - on_consignee + - booking + - shortage + - fulfillment + vessel_attributes: + - empty + - full + - remaining_space + num_actions: 21 + # Parameters for computing states + look_back: 7 + max_ports_downstream: 2 + # Parameters for computing actions + finite_vessel_space: true + has_early_discharge: true + # Parameters for computing rewards + reward_eval_delay: 99 + fulfillment_factor: 1.0 + shortage_factor: 1.0 + time_decay: 0.97 +agent: + model: + actor: + hidden_dims: + - 256 + - 128 + - 64 + activation: tanh + softmax: true + batch_norm: False + head: true + critic: + hidden_dims: + - 256 + - 128 + - 64 + activation: leaky_relu + softmax: false + batch_norm: true + head: true + optimization: + actor: + optim_cls: adam + optim_params: + lr: 0.001 + critic: + optim_cls: rmsprop + optim_params: + lr: 0.001 + hyper_params: + reward_discount: .0 + critic_loss_cls: smooth_l1 + train_iters: 10 + actor_loss_coefficient: 0.1 + # clip_ratio: 0.8 # for PPO +training: + env: + scenario: cim + topology: toy.4p_ssdd_l0.0 + durations: 1120 + max_episode: 50 \ No newline at end of file diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index d7f399655..383f19195 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -1,25 +1,45 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import yaml +from os import getenv +from os.path import dirname, join, realpath + import numpy as np from maro.rl import ( Actor, ActorCritic, ActorCriticConfig, FullyConnectedBlock, MultiAgentWrapper, SimpleMultiHeadModel, - OnPolicyLearner + OnPolicyLearner, OptimOption ) from maro.simulator import Env from maro.utils import set_seeds -from examples.cim.common import CIMEnvWrapper -from examples.cim.common_config import common_config -from examples.cim.ac.config import config +from examples.cim.env_wrapper import CIMEnvWrapper + + +DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") +with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: + config = yaml.safe_load(config_file) + +# model input and output dimensions +IN_DIM = ( + (config["shaping"]["look_back"] + 1) * + (config["shaping"]["max_ports_downstream"] + 1) * + len(config["shaping"]["port_attributes"]) + + len(config["shaping"]["vessel_attributes"]) +) +OUT_DIM = config["shaping"]["num_actions"] def get_ac_agent(): - actor_net = FullyConnectedBlock(**config["agent"]["model"]["actor"]) - critic_net = FullyConnectedBlock(**config["agent"]["model"]["critic"]) + actor_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **config["agent"]["model"]["actor"]) + critic_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **config["agent"]["model"]["critic"]) ac_model = SimpleMultiHeadModel( - {"actor": actor_net, "critic": critic_net}, optim_option=config["agent"]["optimization"], + {"actor": actor_net, "critic": critic_net}, + optim_option={ + "actor": OptimOption(**config["agent"]["optimization"]["actor"]), + "critic": OptimOption(**config["agent"]["optimization"]["critic"]) + } ) return ActorCritic(ac_model, ActorCriticConfig(**config["agent"]["hyper_params"])) @@ -29,5 +49,5 @@ def get_ac_agent(): set_seeds(1024) # for reproducibility env = Env(**config["training"]["env"]) agent = MultiAgentWrapper({name: get_ac_agent() for name in env.agent_idx_list}) - learner = OnPolicyLearner(CIMEnvWrapper(env, **common_config), agent, config["training"]["max_episode"]) + learner = OnPolicyLearner(CIMEnvWrapper(env, **config["shaping"]), agent, config["training"]["max_episode"]) learner.run() diff --git a/examples/cim/common_config.py b/examples/cim/common_config.py deleted file mode 100644 index 765ed90a3..000000000 --- a/examples/cim/common_config.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import numpy as np - -common_config = { - "port_attributes": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], - "vessel_attributes": ["empty", "full", "remaining_space"], - "action_space": list(np.linspace(-1.0, 1.0, 21)), - # Parameters for computing states - "look_back": 7, - "max_ports_downstream": 2, - # Parameters for computing actions - "finite_vessel_space": True, - "has_early_discharge": True, - # Parameters for computing rewards - "reward_eval_delay": 99, - "fulfillment_factor": 1.0, - "shortage_factor": 1.0, - "time_decay": 0.97 -} diff --git a/examples/cim/dqn/config.py b/examples/cim/dqn/config.py deleted file mode 100644 index 9fa50048f..000000000 --- a/examples/cim/dqn/config.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from torch import nn -from torch.optim import RMSprop - -from maro.rl import OptimOption, OverwriteType - -from examples.cim.common import common_config - -input_dim = ( - (common_config["look_back"] + 1) * - (common_config["max_ports_downstream"] + 1) * - len(common_config["port_attributes"]) + - len(common_config["vessel_attributes"]) -) - -config = { - "agent": { - "model": { - "input_dim": input_dim, - "output_dim": len(common_config["action_space"]), # number of possible actions - "hidden_dims": [256, 128, 64], - "activation": nn.LeakyReLU, - "softmax": False, - "batch_norm": True, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": OptimOption(optim_cls=RMSprop, optim_params={"lr": 0.05}), - "hyper_params": { - "reward_discount": .0, - "loss_cls": nn.SmoothL1Loss, - "target_update_freq": 5, - "tau": 0.1, - "double": False - } - }, - "training": { - "env": { - "scenario": "cim", - "topology": "toy.4p_ssdd_l0.0", - "durations": 1120, - }, - "max_episode": 100, - "min_experiences_to_train": 1024, - "train_iter": 10, - "batch_size": 128, - "replay_memory": { - "replay_memory_size": 2000, - "replay_memory_overwrite_type": OverwriteType.RANDOM, - }, - # "prioritized_sampling_by_loss": True, - "exploration": { - "parameter_names": ["epsilon"], - "split": 0.5, - "start": 0.4, - "mid": 0.32, - "end": 0.0 - }, - }, - "distributed": { - "group": "cim-dqn", - "num_actors": 2, - "redis_host": "localhost", - "redis_port": 6379, - "learner_update_trigger": 2, - "replay_sync_interval": 100 - } -} diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml new file mode 100644 index 000000000..f3a064fea --- /dev/null +++ b/examples/cim/dqn/config.yml @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +shaping: + port_attributes: + - empty + - full + - on_shipper + - on_consignee + - booking + - shortage + - fulfillment + vessel_attributes: + - empty + - full + - remaining_space + num_actions: 21 + # Parameters for computing states + look_back: 7 + max_ports_downstream: 2 + # Parameters for computing actions + finite_vessel_space: true + has_early_discharge: true + # Parameters for computing rewards + reward_eval_delay: 99 + fulfillment_factor: 1.0 + shortage_factor: 1.0 + time_decay: 0.97 +agent: + model: + hidden_dims: + - 256 + - 128 + - 64 + activation: + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop + optim_params: + lr: 0.05 + hyper_params: + reward_discount: .0 + loss_cls: smooth_l1 + target_update_freq: 5 + tau: 0.1 + double: false +training: + env: + scenario: cim + topology: toy.4p_ssdd_l0.0 + durations: 1120 + max_episode: 100 + min_experiences_to_train: 1024 + train_iter: 10 + batch_size: 128 + replay_memory: + size: -1 + overwrite_type: random + # prioritized_sampling_by_loss: true + exploration: + parameter_names: + - epsilon + split: 0.5 + start: 0.4 + mid: 0.32 + end: 0.0 +distributed: + group: cim-dqn + num_actors: 2 + redis_host: maro-redis + redis_port: 7379 + learner_update_trigger: 2 + replay_sync_interval: 100 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index f4909cd95..3e4850b6c 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -2,30 +2,45 @@ # Licensed under the MIT license. import argparse +import yaml from multiprocessing import Process -from os import environ +from os import getenv +from os.path import dirname, join, realpath from maro.rl import ( Actor, ActorProxy, DQN, DQNConfig, FullyConnectedBlock, MultiAgentWrapper, OffPolicyDistLearner, - SimpleMultiHeadModel, TwoPhaseLinearParameterScheduler + OptimOption, SimpleMultiHeadModel, TwoPhaseLinearParameterScheduler ) from maro.simulator import Env from maro.utils import set_seeds -from examples.cim.common import CIMEnvWrapper -from examples.cim.common_config import common_config -from examples.cim.dqn.config import config +from examples.cim.env_wrapper import CIMEnvWrapper -GROUP = environ.get("GROUP", config["distributed"]["group"]) -REDIS_HOST = environ.get("REDISHOST", config["distributed"]["redis_host"]) -REDIS_PORT = environ.get("REDISPORT", config["distributed"]["redis_port"]) -NUM_ACTORS = environ.get("NUMACTORS", config["distributed"]["num_actors"]) +DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") +with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: + config = yaml.safe_load(config_file) + +# model input and output dimensions +IN_DIM = ( + (config["shaping"]["look_back"] + 1) * + (config["shaping"]["max_ports_downstream"] + 1) * + len(config["shaping"]["port_attributes"]) + + len(config["shaping"]["vessel_attributes"]) +) +OUT_DIM = config["shaping"]["num_actions"] + +# for distributed / multi-process training +GROUP = getenv("GROUP", default=config["distributed"]["group"]) +REDIS_HOST = getenv("REDISHOST", default=config["distributed"]["redis_host"]) +REDIS_PORT = getenv("REDISPORT", default=config["distributed"]["redis_port"]) +NUM_ACTORS = int(getenv("NUMACTORS", default=config["distributed"]["num_actors"])) def get_dqn_agent(): q_model = SimpleMultiHeadModel( - FullyConnectedBlock(**config["agent"]["model"]), optim_option=config["agent"]["optimization"] + FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **config["agent"]["model"]), + optim_option=OptimOption(**config["agent"]["optimization"]) ) return DQN(q_model, DQNConfig(**config["agent"]["hyper_params"])) @@ -37,7 +52,8 @@ def cim_dqn_learner(): NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, update_trigger=config["distributed"]["learner_update_trigger"], - **config["training"]["replay_memory"] + replay_memory_size=config["training"]["replay_memory"]["size"], + replay_memory_overwrite_type=config["training"]["replay_memory"]["overwrite_type"] ) learner = OffPolicyDistLearner( actor_proxy, agent, scheduler, @@ -52,7 +68,7 @@ def cim_dqn_actor(): env = Env(**config["training"]["env"]) agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) actor = Actor( - CIMEnvWrapper(env, **common_config), agent, GROUP, + CIMEnvWrapper(env, **config["shaping"]), agent, GROUP, replay_sync_interval=config["distributed"]["replay_sync_interval"], proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)} ) diff --git a/examples/cim/common.py b/examples/cim/env_wrapper.py similarity index 81% rename from examples/cim/common.py rename to examples/cim/env_wrapper.py index 66b75aa90..762917ed4 100644 --- a/examples/cim/common.py +++ b/examples/cim/env_wrapper.py @@ -8,34 +8,17 @@ from maro.rl import AbsEnvWrapper from maro.simulator.scenarios.cim.common import Action, ActionType -common_config = { - "port_attributes": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], - "vessel_attributes": ["empty", "full", "remaining_space"], - "action_space": list(np.linspace(-1.0, 1.0, 21)), - # Parameters for computing states - "look_back": 7, - "max_ports_downstream": 2, - # Parameters for computing actions - "finite_vessel_space": True, - "has_early_discharge": True, - # Parameters for computing rewards - "reward_eval_delay": 99, - "fulfillment_factor": 1.0, - "shortage_factor": 1.0, - "time_decay": 0.97 -} - class CIMEnvWrapper(AbsEnvWrapper): def __init__( - self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream, + self, env, *, port_attributes, vessel_attributes, num_actions, look_back, max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, finite_vessel_space=True, has_early_discharge=True ): - super().__init__(env, reward_eval_delay=common_config["reward_eval_delay"]) + super().__init__(env, reward_eval_delay=reward_eval_delay) self.port_attributes = port_attributes self.vessel_attributes = vessel_attributes - self.action_space = action_space + self.action_space = list(np.linspace(-1.0, 1.0, num_actions)) self.look_back = look_back self.max_ports_downstream = max_ports_downstream self.fulfillment_factor = fulfillment_factor diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index ab30439a5..3fc568907 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -10,10 +10,11 @@ ) from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler -from maro.rl.storage import AbsStore, OverwriteType, SimpleStore +from maro.rl.storage import AbsStore, SimpleStore from maro.rl.training import AbsEnvWrapper, AbsLearner, OffPolicyLearner, OnPolicyLearner from maro.rl.utils import ( - get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_truncated_cumulative_reward, select_by_actions + get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_torch_activation_cls, get_torch_loss_cls, + get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward, select_by_actions ) __all__ = [ @@ -23,8 +24,8 @@ "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", - "AbsStore", "OverwriteType", "SimpleStore", + "AbsStore", "SimpleStore", "AbsEnvWrapper", "AbsLearner", "OffPolicyLearner", "OnPolicyLearner", - "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_truncated_cumulative_reward", - "select_by_actions" + "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_torch_activation_cls", "get_torch_loss_cls", + "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward", "select_by_actions" ] diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py index 015061b9f..8460a7764 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/agent/ac.py @@ -9,7 +9,7 @@ from torch.nn import MSELoss from maro.rl.model import SimpleMultiHeadModel -from maro.rl.utils import get_log_prob +from maro.rl.utils import get_log_prob, get_torch_loss_cls from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask from .abs_agent import AbsAgent @@ -20,7 +20,8 @@ class ActorCriticConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. - critic_loss_func (Callable): Loss function for the critic model. + critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing + the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". train_iters (int): Number of gradient descent steps per call to ``train``. actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. @@ -35,12 +36,12 @@ def __init__( self, reward_discount: float, train_iters: int, - critic_loss_func: Callable = MSELoss(), + critic_loss_cls="mse", actor_loss_coefficient: float = 1.0, clip_ratio: float = None ): self.reward_discount = reward_discount - self.critic_loss_func = critic_loss_func + self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.train_iters = train_iters self.actor_loss_coefficient = actor_loss_coefficient self.clip_ratio = clip_ratio diff --git a/maro/rl/agent/ddpg.py b/maro/rl/agent/ddpg.py index a4be15e85..2059d0da0 100644 --- a/maro/rl/agent/ddpg.py +++ b/maro/rl/agent/ddpg.py @@ -8,6 +8,7 @@ from maro.rl.exploration import NoiseExplorer from maro.rl.model import SimpleMultiHeadModel +from maro.rl.utils import get_torch_loss_cls from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask from .abs_agent import AbsAgent @@ -15,11 +16,13 @@ class DDPGConfig: """Configuration for the DDPG algorithm. + Args: reward_discount (float): Reward decay as defined in standard RL terminology. - q_value_loss_func (Callable): Loss function for the Q-value estimator. target_update_freq (int): Number of training rounds between policy target model updates. - actor_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., + q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for + the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". + policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., loss = q_value_loss + ``policy_loss_coefficient`` * policy_loss. Defaults to 1.0. tau (float): Soft update coefficient, e.g., target_model = tau * eval_model + (1-tau) * target_model. Defaults to 1.0. @@ -29,14 +32,14 @@ class DDPGConfig: def __init__( self, reward_discount: float, - q_value_loss_func: Callable, target_update_freq: int, + q_value_loss_cls="mse", policy_loss_coefficient: float = 1.0, tau: float = 1.0, ): self.reward_discount = reward_discount - self.q_value_loss_func = q_value_loss_func self.target_update_freq = target_update_freq + self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() self.policy_loss_coefficient = policy_loss_coefficient self.tau = tau diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index ff6408598..831b89af3 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -7,7 +7,7 @@ import torch from maro.rl.model import SimpleMultiHeadModel -from maro.rl.utils import get_max, get_td_errors, select_by_actions +from maro.rl.utils import get_max, get_td_errors, get_torch_loss_cls, select_by_actions from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask from .abs_agent import AbsAgent @@ -25,7 +25,8 @@ class DQNConfig: See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. advantage_type (str): Advantage mode for the dueling architecture. Defaults to None, in which case it is assumed that the regular Q-value model is used. - loss_cls: Loss function class for evaluating TD errors. Defaults to torch.nn.MSELoss. + loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class. If it is a string, + it must be a key in ``TORCH_LOSS``. Defaults to "mse". target_update_freq (int): Number of training rounds between target model updates. """ __slots__ = [ @@ -40,7 +41,7 @@ def __init__( tau: float = 0.1, double: bool = True, advantage_type: str = None, - loss_cls=torch.nn.MSELoss + loss_cls="mse" ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq @@ -48,7 +49,7 @@ def __init__( self.tau = tau self.double = double self.advantage_type = advantage_type - self.loss_func = loss_cls(reduction="none") + self.loss_func = get_torch_loss_cls(loss_cls)() class DQN(AbsAgent): diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 3672a4973..885e5ae79 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -7,7 +7,7 @@ from maro.communication import Message, Proxy, RegisterTable, SessionType from maro.rl.agent import AbsAgent, MultiAgentWrapper -from maro.rl.storage import OverwriteType, SimpleStore +from maro.rl.storage import SimpleStore from maro.rl.training import AbsEnvWrapper from maro.utils import InternalLogger diff --git a/maro/rl/distributed/actor_proxy.py b/maro/rl/distributed/actor_proxy.py index 95e72afec..4f75ed540 100644 --- a/maro/rl/distributed/actor_proxy.py +++ b/maro/rl/distributed/actor_proxy.py @@ -6,7 +6,7 @@ from maro.communication import Message, Proxy, RegisterTable, SessionType from maro.rl.agent import AbsAgent, MultiAgentWrapper -from maro.rl.storage import OverwriteType, SimpleStore +from maro.rl.storage import SimpleStore from maro.utils import InternalLogger from .message_enums import MsgTag, MsgKey @@ -31,7 +31,7 @@ def __init__( proxy_options: dict = None, update_trigger: str = None, replay_memory_size: int = -1, - replay_memory_overwrite_type: OverwriteType = None + replay_memory_overwrite_type: str = None ): peers = {"actor": num_actors} if proxy_options is None: diff --git a/maro/rl/model/fc_block.py b/maro/rl/model/fc_block.py index 829b7d581..a648a3fd3 100644 --- a/maro/rl/model/fc_block.py +++ b/maro/rl/model/fc_block.py @@ -6,6 +6,8 @@ import torch import torch.nn as nn +from maro.rl.utils import get_torch_activation_cls + from .abs_block import AbsBlock @@ -17,7 +19,9 @@ class FullyConnectedBlock(AbsBlock): input_dim (int): Network input dimension. output_dim (int): Network output dimension. hidden_dims ([int]): Dimensions of hidden layers. Its length is the number of hidden layers. - activation: A ``torch.nn`` activation type. If None, there will be no activation. Defaults to LeakyReLU. + activation: A string indicatinfg an activation class provided by ``torch.nn`` or a custom activation class. + If it is a string, it must be a key in ``TORCH_ACTIVATION``. If None, there will be no activation. + Defaults to "relu". head (bool): If true, this block will be the top block of the full model and the top layer of this block will be the final output layer. Defaults to False. softmax (bool): If true, the output of the net will be a softmax transformation of the top layer's @@ -34,7 +38,7 @@ def __init__( input_dim: int, output_dim: int, hidden_dims: [int], - activation=nn.LeakyReLU, + activation="relu", head: bool = False, softmax: bool = False, batch_norm: bool = False, @@ -49,7 +53,7 @@ def __init__( self._output_dim = output_dim # network features - self._activation = activation + self._activation = get_torch_activation_cls(activation)() if activation else None self._head = head self._softmax = nn.Softmax(dim=1) if softmax else None self._batch_norm = batch_norm @@ -106,7 +110,7 @@ def _build_layer(self, input_dim, output_dim, head: bool = False): components.append(("batch_norm", nn.BatchNorm1d(input_dim))) components.append(("linear", nn.Linear(input_dim, output_dim))) if not head and self._activation is not None: - components.append(("activation", self._activation())) + components.append(("activation", self._activation)) if not head and self._dropout_p: components.append(("dropout", nn.Dropout(p=self._dropout_p))) return nn.Sequential(OrderedDict(components)) diff --git a/maro/rl/model/learning_model.py b/maro/rl/model/learning_model.py index 6d2721ac7..68a46d5f2 100644 --- a/maro/rl/model/learning_model.py +++ b/maro/rl/model/learning_model.py @@ -7,6 +7,7 @@ import torch import torch.nn as nn +from maro.rl.utils import get_torch_lr_scheduler_cls, get_torch_optim_cls from maro.utils import clone from maro.utils.exception.rl_toolkit_exception import MissingOptimizer @@ -14,17 +15,20 @@ class OptimOption: """Model optimization options. Args: - optim_cls: Subclass of torch.optim.Optimizer. + optim_cls: A string indicating an optimizer class provided by torch.optim or custom subclass of + torch.optim.Optimizer. If a string is provided, it must be present in the ``TORCH_OPTIM`` index. optim_params (dict): Parameters for the optimizer class. - scheduler_cls: torch lr_scheduler class. Defaults to None. + scheduler_cls: A string indicating an lr-scheduler class provided by torch.optim.lr_scheduler or custom + subclass of torch.optim.lr_scheduler. If a string is provided, it must be present in the + ``TORCH_LR_SCHEDULER`` index. Defaults to None. scheduler_params (dict): Parameters for the scheduler class. Defaults to None. """ __slots__ = ["optim_cls", "optim_params", "scheduler_cls", "scheduler_params"] def __init__(self, optim_cls, optim_params: dict, scheduler_cls=None, scheduler_params: dict = None): - self.optim_cls = optim_cls + self.optim_cls = get_torch_optim_cls(optim_cls) self.optim_params = optim_params - self.scheduler_cls = scheduler_cls + self.scheduler_cls = get_torch_lr_scheduler_cls(scheduler_cls) self.scheduler_params = scheduler_params diff --git a/maro/rl/storage/__init__.py b/maro/rl/storage/__init__.py index 4ea19a059..c916fcf76 100644 --- a/maro/rl/storage/__init__.py +++ b/maro/rl/storage/__init__.py @@ -2,6 +2,6 @@ # Licensed under the MIT license. from .abs_store import AbsStore -from .simple_store import OverwriteType, SimpleStore +from .simple_store import SimpleStore -__all__ = ["AbsStore", "OverwriteType", "SimpleStore"] +__all__ = ["AbsStore", "SimpleStore"] diff --git a/maro/rl/storage/simple_store.py b/maro/rl/storage/simple_store.py index e3a5633db..bb3db0682 100644 --- a/maro/rl/storage/simple_store.py +++ b/maro/rl/storage/simple_store.py @@ -13,11 +13,6 @@ from .abs_store import AbsStore -class OverwriteType(Enum): - ROLLING = "rolling" - RANDOM = "random" - - class SimpleStore(AbsStore): """ An implementation of ``AbsStore`` for experience storage in RL. @@ -29,14 +24,16 @@ class SimpleStore(AbsStore): Args: capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. - overwrite_type (OverwriteType): If storage capacity is bounded, this specifies how existing entries + overwrite_type (str): If storage capacity is bounded, this specifies how existing entries are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - - Rolling, where overwrite occurs sequentially with wrap-around. - - Random, where overwrite occurs randomly among filled positions. + - "rolling", where overwrite occurs sequentially with wrap-around. + - "random", where overwrite occurs randomly among filled positions. Alternatively, the user may also specify overwrite positions (see ``put``). """ - def __init__(self, capacity: int = -1, overwrite_type: OverwriteType = None): + def __init__(self, capacity: int = -1, overwrite_type: str = None): super().__init__() + if overwrite_type not in {"rolling", "random"}: + raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") self._capacity = capacity self._overwrite_type = overwrite_type self.data = defaultdict(list) if capacity == -1 else defaultdict(lambda: [None] * capacity) @@ -59,7 +56,7 @@ def capacity(self): @property def overwrite_type(self): - """An ``OverwriteType`` member indicating the overwrite behavior when the store capacity is exceeded.""" + """An string indicating the overwrite behavior when the store capacity is exceeded.""" return self._overwrite_type def get(self, indexes: [int]) -> dict: @@ -200,7 +197,7 @@ def _get_update_indexes(self, added_size: int, overwrite_indexes=None): write_indexes = list(range(self._size, self._capacity)) + list(overwrite_indexes) else: # follow the overwrite rule set at init - if self._overwrite_type == OverwriteType.ROLLING: + if self._overwrite_type == "rolling": # using the negative index convention for convenience start_index = self._size - self._capacity write_indexes = list(range(start_index, start_index + added_size)) diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index bad6e602a..9dcb9994f 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,10 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .torch_cls_index import ( + get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls +) from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_truncated_cumulative_reward from .value_utils import get_log_prob, get_max, get_td_errors, select_by_actions __all__ = [ + "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", "get_log_prob", "get_max", "get_td_errors", "select_by_actions" ] diff --git a/maro/rl/utils/torch_cls_index.py b/maro/rl/utils/torch_cls_index.py new file mode 100644 index 000000000..428b3879d --- /dev/null +++ b/maro/rl/utils/torch_cls_index.py @@ -0,0 +1,118 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from torch import nn, optim +from torch.optim import lr_scheduler + +# For details, see https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity +TORCH_ACTIVATION = { + "elu": nn.ELU, + "hard_shrink": nn.Hardshrink, + "hard_sigmoid": nn.Hardsigmoid, + "hard_tanh": nn.Hardtanh, + "hardswish": nn.Hardswish, + "leaky_relu": nn.LeakyReLU, + "log_sigmoid": nn.LogSigmoid, + "multihead_attention": nn.MultiheadAttention, + "prelu": nn.PReLU, + "relu": nn.ReLU, + "relu6": nn.ReLU6, + "rrelu": nn.RReLU, + "selu": nn.SELU, + "celu": nn.CELU, + "gelu": nn.GELU, + "sigmoid": nn.Sigmoid, + "soft_plus": nn.Softplus, + "soft_shrink": nn.Softshrink, + "soft_sign": nn.Softsign, + "tanh": nn.Tanh, + "tanh_shrink": nn.Tanhshrink, + "threshold": nn.Threshold +} + +# For details, see https://pytorch.org/docs/stable/nn.html#loss-functions +TORCH_LOSS = { + "l1": nn.L1Loss, + "mse": nn.MSELoss, + "cross_entropy": nn.CrossEntropyLoss, + "ctc": nn.CTCLoss, + "nll": nn.NLLLoss, + "poisson_nll": nn.PoissonNLLLoss, + "kl": nn.KLDivLoss, + "bce": nn.BCELoss, + "bce_logits": nn.BCEWithLogitsLoss, + "margin_ranking": nn.MarginRankingLoss, + "hinge_embedding": nn.HingeEmbeddingLoss, + "multi_label_margin": nn.MultiLabelMarginLoss, + "multi_label_soft_margin": nn.MultiLabelSoftMarginLoss, + "smooth_l1": nn.SmoothL1Loss, + "soft_margin": nn.SoftMarginLoss, + "cosine_embedding": nn.CosineEmbeddingLoss, + "multi_margin": nn.MultiMarginLoss, + "triplet_margin": nn.TripletMarginLoss, +} + +# For details, see https://pytorch.org/docs/stable/optim.html +TORCH_OPTIM = { + "sgd": optim.SGD, + "asgd": optim.ASGD, + "adadelta": optim.Adadelta, + "adagrad": optim.Adagrad, + "adam": optim.Adam, + "adamax": optim.Adamax, + "adamw": optim.AdamW, + "sparse_adam": optim.SparseAdam, + "lbfgs": optim.LBFGS, + "rmsprop": optim.RMSprop, + "rprop": optim.Rprop +} + +# For details, see https://pytorch.org/docs/stable/optim.html +TORCH_LR_SCHEDULER = { + "lambda": lr_scheduler.LambdaLR, + "multiplicative": lr_scheduler.MultiplicativeLR, + "step": lr_scheduler.StepLR, + "multi_step": lr_scheduler.MultiStepLR, + "exponential": lr_scheduler.ExponentialLR, + "cosine_annealing": lr_scheduler.CosineAnnealingLR, + "reduce_on_plateau": lr_scheduler.ReduceLROnPlateau, + "cyclic": lr_scheduler.CyclicLR, + "one_cycle": lr_scheduler.OneCycleLR, + "cosine_annealing_warm_restarts": lr_scheduler.CosineAnnealingWarmRestarts +} + + +def get_torch_activation_cls(activation_type): + if isinstance(activation_type, str): + if activation_type not in TORCH_ACTIVATION: + raise KeyError(f"A string optim_cls must be one of {list(TORCH_ACTIVATION.keys())}.") + return TORCH_ACTIVATION[activation_type] + + return activation_type + + +def get_torch_loss_cls(loss_type): + if isinstance(loss_type, str): + if loss_type not in TORCH_LOSS: + raise KeyError(f"A string optim_cls must be one of {list(TORCH_LOSS.keys())}.") + return TORCH_LOSS[loss_type] + + return loss_type + + +def get_torch_optim_cls(optim_type): + if isinstance(optim_type, str): + if optim_type not in TORCH_OPTIM: + raise KeyError(f"A string optim_cls must be one of {list(TORCH_OPTIM.keys())}.") + return TORCH_OPTIM[optim_type] + + return optim_type + + +def get_torch_lr_scheduler_cls(lr_scheduler_type): + if isinstance(lr_scheduler_type, str): + if lr_scheduler_type not in TORCH_LR_SCHEDULER: + raise KeyError(f"A string optim_cls must be one of {list(TORCH_LR_SCHEDULER.keys())}.") + return TORCH_LR_SCHEDULER[lr_scheduler_type] + + return lr_scheduler_type diff --git a/tests/test_store.py b/tests/test_store.py index fb6084277..8d848dbe6 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -3,7 +3,7 @@ import unittest -from maro.rl import SimpleStore, OverwriteType +from maro.rl import SimpleStore class TestUnboundedStore(unittest.TestCase): @@ -44,7 +44,7 @@ def test_filter(self): class TestFixedSizeStore(unittest.TestCase): def test_put_with_rolling_overwrite(self): - store = SimpleStore(["a", "b", "c"], capacity=5, overwrite_type=OverwriteType.ROLLING) + store = SimpleStore(["a", "b", "c"], capacity=5, overwrite_type="rolling") indexes = store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) expected = [0, 1, 2] self.assertEqual(indexes, expected, msg=f"expected indexes = {expected}, got {indexes}") @@ -56,7 +56,7 @@ def test_put_with_rolling_overwrite(self): self.assertEqual(actual, expected, msg=f"expected store content = {expected}, got {actual}") def test_put_with_random_overwrite(self): - store = SimpleStore(["a", "b", "c"], capacity=5, overwrite_type=OverwriteType.RANDOM) + store = SimpleStore(["a", "b", "c"], capacity=5, overwrite_type="random") indexes = store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) indexes_2 = store.put({"a": [10, 11, 12, 13], "b": [14, 15, 16, 17], "c": [18, 19, 20, 21]}) for i in indexes_2[2:]: From e9c15f0ca5c77efa23130f3c4c763b7b899a5e31 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 29 Mar 2021 09:51:53 +0000 Subject: [PATCH 120/482] refined env_wrapper --- examples/cim/env_wrapper.py | 23 +++++++++------ maro/rl/training/env_wrapper.py | 50 ++++++++++++++++----------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index 762917ed4..c3d304398 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -34,34 +34,41 @@ def get_state(self, event): future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] + self.state_info.append( + {"tick": tick, "action_scope": event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx} + ) return {port_idx: np.concatenate((port_features, vessel_features))} - def get_action(self, action_by_agent, event): + def get_action(self, action_by_agent): vessel_snapshots = self.env.snapshot_list["vessels"] + state_info = self.state_info[-1] action_info = list(action_by_agent.values())[0] model_action = action_info[0] if isinstance(action_info, tuple) else action_info - scope, tick, port, vessel = event.action_scope, event.tick, event.port_idx, event.vessel_idx + tick, port, vessel = state_info["tick"], state_info["port_idx"], state_info["vessel_idx"] zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 percent = abs(self.action_space[model_action]) + action_scope = state_info["action_scope"] if model_action < zero_action_idx: action_type = ActionType.LOAD - actual_action = min(round(percent * scope.load), vessel_space) + actual_action = min(round(percent * action_scope.load), vessel_space) elif model_action > zero_action_idx: action_type = ActionType.DISCHARGE - plan_action = percent * (scope.discharge + early_discharge) - early_discharge - actual_action = round(plan_action) if plan_action > 0 else round(percent * scope.discharge) + plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge + actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) else: actual_action, action_type = 0, None return {port: Action(vessel, port, actual_action, action_type)} - def get_reward_for(self, event): - """Compute offline rewards.""" + def get_reward(self, tick=None): + """Delayed reward evaluation.""" + if tick is None: + tick = self.env.tick port_snapshots = self.env.snapshot_list["ports"] - start_tick = event.tick + 1 + start_tick = tick + 1 ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) future_fulfillment = port_snapshots[ticks::"fulfillment"] diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index 0de98c2c9..bad23beea 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -24,11 +24,12 @@ def __init__(self, env: Env, record_path: bool = True, reward_eval_delay: int = self.env = env self.step_index = None self.replay = defaultdict(lambda: defaultdict(list)) - self.events = [] - self.acting_agents = [] + self.state_info = [] self.record_path = record_path self.reward_eval_delay = reward_eval_delay self._pending_reward_idx = 0 + self._event_ticks = [] # for delayed reward evaluation + self._action_history = [] # for delayed reward evaluation def start(self, rollout_index: int = None): self.step_index = 0 @@ -36,7 +37,6 @@ def start(self, rollout_index: int = None): _, event, _ = self.env.step(None) state_by_agent = self.get_state(event) if self.record_path: - self.events.append((event, list(state_by_agent.keys()))) for agent_id, state in state_by_agent.items(): replay = self.replay[agent_id] if replay["S"]: @@ -62,28 +62,25 @@ def get_state(self, event) -> dict: pass @abstractmethod - def get_action(self, action, event) -> dict: - pass - - def get_reward(self) -> float: - """Get the immediate reward for an action. - - This can be left blank if ``get_reward_for`` is implemented. - """ + def get_action(self, action) -> dict: pass + + @abstractmethod + def get_reward(self, tick: int = None) -> float: + """User-defined reward evaluation. - def get_reward_for(self, event) -> float: - """Get the reward for an action in response to an event that occurred a certain number of ticks ago. + Args: + tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed reward + evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. - If implemented, whatever value ``get_reward`` gives will be ignored in the output of ``replay_memory``. - If left blank, ``get_reward`` must be implemented. """ pass def step(self, action_by_agent: dict): - assert self.events, "start() must be called first." self.step_index += 1 - env_action = self.get_action(action_by_agent, self.events[-1][0]) + self._event_ticks.append(self.env.tick) + env_action = self.get_action(action_by_agent) + self._action_history.append(env_action) if len(env_action) == 1: env_action = list(env_action.values())[0] _, event, done = self.env.step(env_action) @@ -100,17 +97,19 @@ def step(self, action_by_agent: dict): If roll-out is complete, evaluate rewards for all remaining events except the last. Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ - for i, (evt, agents) in enumerate(self.events[self._pending_reward_idx:]): - if not done and event.tick - evt.tick < self.reward_eval_delay: + for i, (tick, action) in enumerate( + zip(self._event_ticks[self._pending_reward_idx:], self._action_history[self._pending_reward_idx:]) + ): + if not done and self.env.tick - tick < self.reward_eval_delay: self._pending_reward_idx += i break - reward = self.get_reward_for(evt) - for agent_id in agents: + reward = self.get_reward(tick=tick) + for agent_id in action: if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): self.replay[agent_id]["R"].append(reward) if done: - self._pending_reward_idx = len(self.events) - 1 + self._pending_reward_idx = len(self._event_ticks) - 1 else: for agent_id, action in action_by_agent.items(): if isinstance(action, tuple): @@ -124,7 +123,6 @@ def step(self, action_by_agent: dict): if not done: state_by_agent = self.get_state(event) if self.record_path: - self.events.append((event, list(state_by_agent.keys()))) for agent_id, state in state_by_agent.items(): replay = self.replay[agent_id] if replay["S"]: @@ -136,7 +134,9 @@ def step(self, action_by_agent: dict): def reset(self): self.env.reset() - self.events.clear() + self.state_info.clear() + self._event_ticks.clear() + self._action_history.clear() self.replay = defaultdict(lambda: defaultdict(list)) def flush(self): @@ -145,5 +145,5 @@ def flush(self): for vals in replay.values(): del vals[:num_complete] - del self.events[:self._pending_reward_idx] + del self.state_info[:self._pending_reward_idx] self._pending_reward_idx = 0 From 4a1a2cd0c858097fdb59710585f9c773a8c49b42 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 29 Mar 2021 09:52:31 +0000 Subject: [PATCH 121/482] refined env_wrapper --- examples/cim/env_wrapper.py | 23 +++++++++------ maro/rl/training/env_wrapper.py | 50 ++++++++++++++++----------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index 762917ed4..c3d304398 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -34,34 +34,41 @@ def get_state(self, event): future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] + self.state_info.append( + {"tick": tick, "action_scope": event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx} + ) return {port_idx: np.concatenate((port_features, vessel_features))} - def get_action(self, action_by_agent, event): + def get_action(self, action_by_agent): vessel_snapshots = self.env.snapshot_list["vessels"] + state_info = self.state_info[-1] action_info = list(action_by_agent.values())[0] model_action = action_info[0] if isinstance(action_info, tuple) else action_info - scope, tick, port, vessel = event.action_scope, event.tick, event.port_idx, event.vessel_idx + tick, port, vessel = state_info["tick"], state_info["port_idx"], state_info["vessel_idx"] zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 percent = abs(self.action_space[model_action]) + action_scope = state_info["action_scope"] if model_action < zero_action_idx: action_type = ActionType.LOAD - actual_action = min(round(percent * scope.load), vessel_space) + actual_action = min(round(percent * action_scope.load), vessel_space) elif model_action > zero_action_idx: action_type = ActionType.DISCHARGE - plan_action = percent * (scope.discharge + early_discharge) - early_discharge - actual_action = round(plan_action) if plan_action > 0 else round(percent * scope.discharge) + plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge + actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) else: actual_action, action_type = 0, None return {port: Action(vessel, port, actual_action, action_type)} - def get_reward_for(self, event): - """Compute offline rewards.""" + def get_reward(self, tick=None): + """Delayed reward evaluation.""" + if tick is None: + tick = self.env.tick port_snapshots = self.env.snapshot_list["ports"] - start_tick = event.tick + 1 + start_tick = tick + 1 ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) future_fulfillment = port_snapshots[ticks::"fulfillment"] diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index 0de98c2c9..bad23beea 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -24,11 +24,12 @@ def __init__(self, env: Env, record_path: bool = True, reward_eval_delay: int = self.env = env self.step_index = None self.replay = defaultdict(lambda: defaultdict(list)) - self.events = [] - self.acting_agents = [] + self.state_info = [] self.record_path = record_path self.reward_eval_delay = reward_eval_delay self._pending_reward_idx = 0 + self._event_ticks = [] # for delayed reward evaluation + self._action_history = [] # for delayed reward evaluation def start(self, rollout_index: int = None): self.step_index = 0 @@ -36,7 +37,6 @@ def start(self, rollout_index: int = None): _, event, _ = self.env.step(None) state_by_agent = self.get_state(event) if self.record_path: - self.events.append((event, list(state_by_agent.keys()))) for agent_id, state in state_by_agent.items(): replay = self.replay[agent_id] if replay["S"]: @@ -62,28 +62,25 @@ def get_state(self, event) -> dict: pass @abstractmethod - def get_action(self, action, event) -> dict: - pass - - def get_reward(self) -> float: - """Get the immediate reward for an action. - - This can be left blank if ``get_reward_for`` is implemented. - """ + def get_action(self, action) -> dict: pass + + @abstractmethod + def get_reward(self, tick: int = None) -> float: + """User-defined reward evaluation. - def get_reward_for(self, event) -> float: - """Get the reward for an action in response to an event that occurred a certain number of ticks ago. + Args: + tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed reward + evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. - If implemented, whatever value ``get_reward`` gives will be ignored in the output of ``replay_memory``. - If left blank, ``get_reward`` must be implemented. """ pass def step(self, action_by_agent: dict): - assert self.events, "start() must be called first." self.step_index += 1 - env_action = self.get_action(action_by_agent, self.events[-1][0]) + self._event_ticks.append(self.env.tick) + env_action = self.get_action(action_by_agent) + self._action_history.append(env_action) if len(env_action) == 1: env_action = list(env_action.values())[0] _, event, done = self.env.step(env_action) @@ -100,17 +97,19 @@ def step(self, action_by_agent: dict): If roll-out is complete, evaluate rewards for all remaining events except the last. Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ - for i, (evt, agents) in enumerate(self.events[self._pending_reward_idx:]): - if not done and event.tick - evt.tick < self.reward_eval_delay: + for i, (tick, action) in enumerate( + zip(self._event_ticks[self._pending_reward_idx:], self._action_history[self._pending_reward_idx:]) + ): + if not done and self.env.tick - tick < self.reward_eval_delay: self._pending_reward_idx += i break - reward = self.get_reward_for(evt) - for agent_id in agents: + reward = self.get_reward(tick=tick) + for agent_id in action: if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): self.replay[agent_id]["R"].append(reward) if done: - self._pending_reward_idx = len(self.events) - 1 + self._pending_reward_idx = len(self._event_ticks) - 1 else: for agent_id, action in action_by_agent.items(): if isinstance(action, tuple): @@ -124,7 +123,6 @@ def step(self, action_by_agent: dict): if not done: state_by_agent = self.get_state(event) if self.record_path: - self.events.append((event, list(state_by_agent.keys()))) for agent_id, state in state_by_agent.items(): replay = self.replay[agent_id] if replay["S"]: @@ -136,7 +134,9 @@ def step(self, action_by_agent: dict): def reset(self): self.env.reset() - self.events.clear() + self.state_info.clear() + self._event_ticks.clear() + self._action_history.clear() self.replay = defaultdict(lambda: defaultdict(list)) def flush(self): @@ -145,5 +145,5 @@ def flush(self): for vals in replay.values(): del vals[:num_complete] - del self.events[:self._pending_reward_idx] + del self.state_info[:self._pending_reward_idx] self._pending_reward_idx = 0 From 6b732c6449839066f1fc04b16ae57716ab826d1f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 30 Mar 2021 10:46:48 +0800 Subject: [PATCH 122/482] add agent id list --- .../scenarios/supply_chain/business_engine.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 85348f182..76545223b 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -6,6 +6,7 @@ from maro.event_buffer import MaroEvents from maro.simulator.scenarios import AbsBusinessEngine +from typing import List, Tuple from .parser import ConfigParser, SupplyChainConfiguration from .units import UnitBase @@ -65,6 +66,16 @@ def reset(self): def get_node_mapping(self) -> dict: return self._node_mapping + def get_agent_idx_list(self) -> List[Tuple[str, int]]: + """Get a list of agent index. + + Returns: + list: List of agent index. + """ + return [ + (unit.data_model_name, id) for id, unit in self.world.units.items() if unit.data_model_name in ("consumer","manufacture") + ] + def _step_by_facility(self, tick: int): """Call step functions by facility. From 030c46919998f5aca936776dcc194d4a157a38a1 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 30 Mar 2021 17:23:11 +0800 Subject: [PATCH 123/482] add storage data model index for manufacture to simplify state shaping --- .../scenarios/supply_chain/datamodels/manufacture.py | 6 +++++- maro/simulator/scenarios/supply_chain/units/manufacture.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index f561c5fa0..345467368 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -13,6 +13,7 @@ class ManufactureDataModel(SkuDataModel): """Data model for manufacture unit.""" # Storage related to this manufacture unit, for easy state retrieving. storage_id = NodeAttribute(AttributeType.UInt) + storage_data_model_index = NodeAttribute(AttributeType.UInt) # Cost to produce one output production. product_unit_cost = NodeAttribute(AttributeType.UInt) @@ -27,8 +28,9 @@ def __init__(self): super(ManufactureDataModel, self).__init__() self._product_unit_cost = 0 self._storage_id = 0 + self._storage_data_model_index = 0 - def initialize(self, product_unit_cost: int = 1, storage_id: int = 0): + def initialize(self, storage_data_model_index: int, product_unit_cost: int = 1, storage_id: int = 0): """Initialize data model, used to assign value after frame reset. Args: @@ -37,6 +39,7 @@ def initialize(self, product_unit_cost: int = 1, storage_id: int = 0): """ self._product_unit_cost = product_unit_cost self._storage_id = storage_id + self._storage_data_model_index = storage_data_model_index self.reset() @@ -45,3 +48,4 @@ def reset(self): self.product_unit_cost = self._product_unit_cost self.storage_id = self._storage_id + self.storage_data_model_index = self._storage_data_model_index diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 436f855de..d39edc42c 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -31,6 +31,7 @@ def initialize(self): facility_sku_info = self.facility.skus[self.product_id] self.data_model.initialize( + self.facility.storage.data_model_index, product_unit_cost=facility_sku_info.product_unit_cost, storage_id=self.facility.storage.id ) From e26a1041b9a7f84cc22bb2ace9dd481ef478c097 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 30 Mar 2021 09:54:38 +0000 Subject: [PATCH 124/482] fixed issues with env_wrapper --- examples/cim/dqn/config.yml | 4 ++-- examples/cim/env_wrapper.py | 14 +++++++------- maro/rl/distributed/actor.py | 4 ++-- maro/rl/training/env_wrapper.py | 22 ++++++++++++---------- maro/rl/training/learner.py | 2 +- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index f3a064fea..a48f16016 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -71,7 +71,7 @@ training: distributed: group: cim-dqn num_actors: 2 - redis_host: maro-redis - redis_port: 7379 + redis_host: localhost + redis_port: 6379 learner_update_trigger: 2 replay_sync_interval: 100 diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index c3d304398..838c33cb3 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -34,23 +34,23 @@ def get_state(self, event): future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] - self.state_info.append( - {"tick": tick, "action_scope": event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx} - ) - return {port_idx: np.concatenate((port_features, vessel_features))} + self.state_info = { + "tick": tick, "action_scope": event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx + } + state = np.concatenate((port_features, vessel_features)) + return {port_idx: state} def get_action(self, action_by_agent): vessel_snapshots = self.env.snapshot_list["vessels"] - state_info = self.state_info[-1] action_info = list(action_by_agent.values())[0] model_action = action_info[0] if isinstance(action_info, tuple) else action_info - tick, port, vessel = state_info["tick"], state_info["port_idx"], state_info["vessel_idx"] + tick, port, vessel = self.state_info["tick"], self.state_info["port_idx"], self.state_info["vessel_idx"] zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 percent = abs(self.action_space[model_action]) - action_scope = state_info["action_scope"] + action_scope = self.state_info["action_scope"] if model_action < zero_action_idx: action_type = ActionType.LOAD actual_action = min(round(percent * action_scope.load), vessel_space) diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 885e5ae79..53cefd8e4 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -50,7 +50,7 @@ def __init__( def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): self.env.reset() if not training: - self.env.record_path = False # no need to record the trajectory if roll-out is not for training + self.env.save_replay = False # no need to record the trajectory if roll-out is not for training # Load models and exploration parameters if model_by_agent: @@ -98,7 +98,7 @@ def post_rollout(self, index: int): return body = {MsgKey.ROLLOUT_INDEX: index, MsgKey.METRICS: self.env.metrics} - if self.env.record_path: + if self.env.save_replay: body[MsgKey.REPLAY] = self.env.replay_memory actor_proxy_addr = self._proxy.peers["actor_proxy"][0] diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index bad23beea..580b90cc3 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -13,19 +13,19 @@ class AbsEnvWrapper(ABC): Args: env (Env): Environment instance. - record_path (bool): If True, the steps during roll-out will be recorded sequentially. This + save_replay (bool): If True, the steps during roll-out will be recorded sequentially. This includes states, actions and rewards. The decision events themselves will also be recorded for delayed reward evaluation purposes. Defaults to True. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which rewards are evaluated immediately after executing an action. """ - def __init__(self, env: Env, record_path: bool = True, reward_eval_delay: int = 0): + def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): self.env = env self.step_index = None self.replay = defaultdict(lambda: defaultdict(list)) - self.state_info = [] - self.record_path = record_path + self.state_info = None # context for converting model output to actions that can be executed by the env + self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay self._pending_reward_idx = 0 self._event_ticks = [] # for delayed reward evaluation @@ -36,7 +36,7 @@ def start(self, rollout_index: int = None): self._pending_reward_idx = 0 _, event, _ = self.env.step(None) state_by_agent = self.get_state(event) - if self.record_path: + if self.save_replay: for agent_id, state in state_by_agent.items(): replay = self.replay[agent_id] if replay["S"]: @@ -85,7 +85,7 @@ def step(self, action_by_agent: dict): env_action = list(env_action.values())[0] _, event, done = self.env.step(env_action) - if self.record_path: + if self.save_replay: if self.reward_eval_delay: for agent_id, action in action_by_agent.items(): if isinstance(action, tuple): @@ -117,12 +117,13 @@ def step(self, action_by_agent: dict): self.replay[agent_id]["LOGP"].append(action[1]) else: self.replay[agent_id]["A"].append(action) - self.replay[agent_id]["R"].append(reward) + if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): + self.replay[agent_id]["R"].append(self.get_reward()) self._pending_reward_idx += 1 if not done: state_by_agent = self.get_state(event) - if self.record_path: + if self.save_replay: for agent_id, state in state_by_agent.items(): replay = self.replay[agent_id] if replay["S"]: @@ -134,7 +135,7 @@ def step(self, action_by_agent: dict): def reset(self): self.env.reset() - self.state_info.clear() + self.state_info = None self._event_ticks.clear() self._action_history.clear() self.replay = defaultdict(lambda: defaultdict(list)) @@ -145,5 +146,6 @@ def flush(self): for vals in replay.values(): del vals[:num_complete] - del self.state_info[:self._pending_reward_idx] + del self._event_ticks[:self._pending_reward_idx] + del self._action_history[:self._pending_reward_idx] self._pending_reward_idx = 0 diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 347a3207c..116c438ba 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -31,7 +31,7 @@ def __init__(self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper] def roll_out(self, index: int, training: bool = True): self.env.reset() if not training: - self.env.record_path = False # no need to record the trajectory if roll-out is not for training + self.env.save_replay = False # no need to record the trajectory if roll-out is not for training state = self.env.start(rollout_index=index) # get initial state while state: From d22f23857839a65a9e0894fa843af6fab2269b12 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 30 Mar 2021 13:06:49 +0000 Subject: [PATCH 125/482] supply chain DQN workflow ready --- examples/supply_chain/dqn/config.yml | 49 ++++++++++++ examples/supply_chain/dqn/main.py | 112 +++++++++++++++++++++++++++ examples/supply_chain/env_wrapper.py | 84 ++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 examples/supply_chain/dqn/config.yml create mode 100644 examples/supply_chain/dqn/main.py create mode 100644 examples/supply_chain/env_wrapper.py diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml new file mode 100644 index 000000000..e1400b7a3 --- /dev/null +++ b/examples/supply_chain/dqn/config.yml @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +agent: + model: + hidden_dims: + - 16 + - 8 + activation: + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop + optim_params: + lr: 0.001 + hyper_params: + reward_discount: .0 + loss_cls: smooth_l1 + target_update_freq: 5 + tau: 0.1 + double: false +training: + env: + scenario: supply_chain + topology: sample1 + durations: 100 + max_episode: 100 + min_experiences_to_train: 10 + train_iter: 10 + batch_size: 128 + replay_memory: + size: -1 + overwrite_type: random + # prioritized_sampling_by_loss: true + exploration: + parameter_names: + - epsilon + start: 0.4 + end: 0.0 +distributed: + group: sc-dqn + num_actors: 2 + redis_host: localhost + redis_port: 6379 + learner_update_trigger: 2 + replay_sync_interval: 30 diff --git a/examples/supply_chain/dqn/main.py b/examples/supply_chain/dqn/main.py new file mode 100644 index 000000000..dbc5d8d96 --- /dev/null +++ b/examples/supply_chain/dqn/main.py @@ -0,0 +1,112 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import argparse +import yaml +from multiprocessing import Process +from os import getenv +from os.path import dirname, join, realpath + +from maro.rl import ( + Actor, ActorProxy, DQN, DQNConfig, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, + OffPolicyDistLearner, OptimOption, SimpleMultiHeadModel +) +from maro.simulator import Env +from maro.utils import set_seeds + +from examples.supply_chain.env_wrapper import SCEnvWrapper + + +DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") +with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: + config = yaml.safe_load(config_file) + +# model input and output dimensions +MANUFACTURER_IN_DIM = 6 +MANUFACTURER_OUT_DIM = 10 +CONSUMER_IN_DIM = 8 +CONSUMER_OUT_DIM = 100 + +# for distributed / multi-process training +GROUP = getenv("GROUP", default=config["distributed"]["group"]) +REDIS_HOST = config["distributed"]["redis_host"] +REDIS_PORT = config["distributed"]["redis_port"] +NUM_ACTORS = int(getenv("NUMACTORS", default=config["distributed"]["num_actors"])) + + +def get_dqn_agent(in_dim, out_dim): + q_model = SimpleMultiHeadModel( + FullyConnectedBlock(input_dim=in_dim, output_dim=out_dim, **config["agent"]["model"]), + optim_option=OptimOption(**config["agent"]["optimization"]) + ) + return DQN(q_model, DQNConfig(**config["agent"]["hyper_params"])) + + +def get_sc_agents(agent_ids): + manufacturer_agents = { + id_: get_dqn_agent(MANUFACTURER_IN_DIM, MANUFACTURER_OUT_DIM) + for type_, id_ in agent_ids if type_ == "manufacture" + } + consumer_agents = { + id_: get_dqn_agent(CONSUMER_IN_DIM, CONSUMER_OUT_DIM) + for type_, id_ in agent_ids if type_ == "consumer" + } + return MultiAgentWrapper({**manufacturer_agents, **consumer_agents}) + + +def sc_dqn_learner(): + agent = get_sc_agents(Env(**config["training"]["env"]).agent_idx_list) + scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) + actor_proxy = ActorProxy( + NUM_ACTORS, GROUP, + proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, + update_trigger=config["distributed"]["learner_update_trigger"], + replay_memory_size=config["training"]["replay_memory"]["size"], + replay_memory_overwrite_type=config["training"]["replay_memory"]["overwrite_type"] + ) + learner = OffPolicyDistLearner( + actor_proxy, agent, scheduler, + min_experiences_to_train=config["training"]["min_experiences_to_train"], + train_iter=config["training"]["train_iter"], + batch_size=config["training"]["batch_size"] + ) + learner.run() + + +def sc_dqn_actor(): + env = Env(**config["training"]["env"]) + agent = get_sc_agents(env.agent_idx_list) + actor = Actor( + SCEnvWrapper(env), agent, GROUP, + replay_sync_interval=config["distributed"]["replay_sync_interval"], + proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)} + ) + actor.run() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "-w", "--whoami", type=int, choices=[0, 1, 2], default=0, + help="Identity of this process: 0 - multi-process mode, 1 - learner, 2 - actor" + ) + + args = parser.parse_args() + if args.whoami == 0: + actor_processes = [Process(target=sc_dqn_actor) for i in range(NUM_ACTORS)] + learner_process = Process(target=sc_dqn_learner) + + for i, actor_process in enumerate(actor_processes): + set_seeds(i) # this is to ensure that the actors explore differently. + actor_process.start() + + learner_process.start() + + for actor_process in actor_processes: + actor_process.join() + + learner_process.join() + elif args.whoami == 1: + sc_dqn_learner() + elif args.whoami == 2: + sc_dqn_actor() diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py new file mode 100644 index 000000000..1db12fc70 --- /dev/null +++ b/examples/supply_chain/env_wrapper.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import random +from collections import defaultdict + +import numpy as np + +from maro.rl import AbsEnvWrapper +from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction + + +class SCEnvWrapper(AbsEnvWrapper): + manufacturer_features = [ + "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", + "product_unit_cost", "storage_data_model_index" + ] + + consumer_features = [ + "id", "facility_id", "product_id", "order_cost", "total_purchased", "total_received", "source_id", + "quantity", "vlt", "purchased", "received", "order_product_cost" + ] + + storage_features = ["id", "facility_id", "capacity", "remaining_space", "unit_storage_cost"] + + # Only keep non-ID features in states + manufacturer_state_indexes = [2, 3, 6] + consumer_state_indexes = [3, 4, 5, 7, 8, 9, 10, 11] + + def get_state(self, event): + self.state_info = {} + # manufacturer state shaping + manufacturer_snapshots = self.env.snapshot_list["manufacture"] + storage_snapshots = self.env.snapshot_list["storage"] + manufacturer_features = manufacturer_snapshots[self.env.frame_index::SCEnvWrapper.manufacturer_features] + manufacturer_features = manufacturer_features.flatten().reshape(len(manufacturer_snapshots), -1).astype(np.int32) + + # combine manufacture state and the corresponding storage state + state = { + feature[0]: np.concatenate([ + feature[SCEnvWrapper.manufacturer_state_indexes], + storage_snapshots[self.env.frame_index:feature[-1]:SCEnvWrapper.storage_features[2:]].flatten() + ]).astype(np.float32) + for feature in manufacturer_features + } + + # consumer state shaping + consumer_snapshots = self.env.snapshot_list["consumer"] + consumer_features = consumer_snapshots[self.env.frame_index::SCEnvWrapper.consumer_features] + consumer_features = consumer_features.flatten().reshape(len(consumer_snapshots), -1).astype(np.int32) + state.update({ + feature[0]: np.asarray(feature[SCEnvWrapper.consumer_state_indexes], dtype=np.float32) + for feature in consumer_features + }) + self.state_info = {feature[0]: {"product_id": feature[2]} for feature in consumer_features} + + return state + + def get_action(self, action_by_agent): + # cache the sources for each consumer if not yet cached + if not hasattr(self, "consumer_source"): + self.consumer_source = {} + for id_, (type_, idx) in self.env.summary["node_mapping"]["unit_mapping"].items(): + if type_ == "consumer": + sources = self.env.snapshot_list["consumer"][self.env.frame_index:idx:"sources"] + if sources: + sources = sources.flatten().astype(np.int) + self.consumer_source[id_] = sources + + env_action = {} + for agent_id, action in action_by_agent.items(): + # consumer action + if agent_id in self.state_info: + source_id = random.choice(self.consumer_source[agent_id]) if agent_id in self.consumer_source else 0 + product_id = self.state_info[agent_id]["product_id"] + env_action[agent_id] = ConsumerAction(agent_id, product_id, source_id, action, 1) + # manufacturer action + else: + env_action[agent_id] = ManufactureAction(agent_id, action) + + return env_action + + def get_reward(self, tick=None): + return np.float32(np.random.rand()) From 2d8dc931285069870890783c3c3b06e94f909a73 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 30 Mar 2021 13:11:29 +0000 Subject: [PATCH 126/482] refined env_wrapper --- examples/cim/dqn/config.yml | 4 ++-- examples/cim/env_wrapper.py | 14 ++++++------- maro/rl/distributed/actor.py | 4 ++-- maro/rl/training/env_wrapper.py | 35 +++++++++++++++++---------------- maro/rl/training/learner.py | 2 +- 5 files changed, 30 insertions(+), 29 deletions(-) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index f3a064fea..a48f16016 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -71,7 +71,7 @@ training: distributed: group: cim-dqn num_actors: 2 - redis_host: maro-redis - redis_port: 7379 + redis_host: localhost + redis_port: 6379 learner_update_trigger: 2 replay_sync_interval: 100 diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index c3d304398..838c33cb3 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -34,23 +34,23 @@ def get_state(self, event): future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] - self.state_info.append( - {"tick": tick, "action_scope": event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx} - ) - return {port_idx: np.concatenate((port_features, vessel_features))} + self.state_info = { + "tick": tick, "action_scope": event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx + } + state = np.concatenate((port_features, vessel_features)) + return {port_idx: state} def get_action(self, action_by_agent): vessel_snapshots = self.env.snapshot_list["vessels"] - state_info = self.state_info[-1] action_info = list(action_by_agent.values())[0] model_action = action_info[0] if isinstance(action_info, tuple) else action_info - tick, port, vessel = state_info["tick"], state_info["port_idx"], state_info["vessel_idx"] + tick, port, vessel = self.state_info["tick"], self.state_info["port_idx"], self.state_info["vessel_idx"] zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 percent = abs(self.action_space[model_action]) - action_scope = state_info["action_scope"] + action_scope = self.state_info["action_scope"] if model_action < zero_action_idx: action_type = ActionType.LOAD actual_action = min(round(percent * action_scope.load), vessel_space) diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 885e5ae79..53cefd8e4 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -50,7 +50,7 @@ def __init__( def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): self.env.reset() if not training: - self.env.record_path = False # no need to record the trajectory if roll-out is not for training + self.env.save_replay = False # no need to record the trajectory if roll-out is not for training # Load models and exploration parameters if model_by_agent: @@ -98,7 +98,7 @@ def post_rollout(self, index: int): return body = {MsgKey.ROLLOUT_INDEX: index, MsgKey.METRICS: self.env.metrics} - if self.env.record_path: + if self.env.save_replay: body[MsgKey.REPLAY] = self.env.replay_memory actor_proxy_addr = self._proxy.peers["actor_proxy"][0] diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index bad23beea..4fba22cad 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -3,7 +3,6 @@ from abc import ABC, abstractmethod from collections import defaultdict -from typing import Callable from maro.simulator import Env @@ -13,19 +12,19 @@ class AbsEnvWrapper(ABC): Args: env (Env): Environment instance. - record_path (bool): If True, the steps during roll-out will be recorded sequentially. This + save_replay (bool): If True, the steps during roll-out will be recorded sequentially. This includes states, actions and rewards. The decision events themselves will also be recorded - for delayed reward evaluation purposes. Defaults to True. + for delayed reward evaluation purposes. Defaults to True. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which rewards are evaluated immediately after executing an action. """ - def __init__(self, env: Env, record_path: bool = True, reward_eval_delay: int = 0): + def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): self.env = env self.step_index = None self.replay = defaultdict(lambda: defaultdict(list)) - self.state_info = [] - self.record_path = record_path + self.state_info = None # context for converting model output to actions that can be executed by the env + self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay self._pending_reward_idx = 0 self._event_ticks = [] # for delayed reward evaluation @@ -36,7 +35,7 @@ def start(self, rollout_index: int = None): self._pending_reward_idx = 0 _, event, _ = self.env.step(None) state_by_agent = self.get_state(event) - if self.record_path: + if self.save_replay: for agent_id, state in state_by_agent.items(): replay = self.replay[agent_id] if replay["S"]: @@ -64,7 +63,7 @@ def get_state(self, event) -> dict: @abstractmethod def get_action(self, action) -> dict: pass - + @abstractmethod def get_reward(self, tick: int = None) -> float: """User-defined reward evaluation. @@ -85,7 +84,7 @@ def step(self, action_by_agent: dict): env_action = list(env_action.values())[0] _, event, done = self.env.step(env_action) - if self.record_path: + if self.save_replay: if self.reward_eval_delay: for agent_id, action in action_by_agent.items(): if isinstance(action, tuple): @@ -96,12 +95,12 @@ def step(self, action_by_agent: dict): """ If roll-out is complete, evaluate rewards for all remaining events except the last. Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ + """ for i, (tick, action) in enumerate( zip(self._event_ticks[self._pending_reward_idx:], self._action_history[self._pending_reward_idx:]) ): if not done and self.env.tick - tick < self.reward_eval_delay: - self._pending_reward_idx += i + self._pending_reward_idx += i break reward = self.get_reward(tick=tick) for agent_id in action: @@ -117,24 +116,25 @@ def step(self, action_by_agent: dict): self.replay[agent_id]["LOGP"].append(action[1]) else: self.replay[agent_id]["A"].append(action) - self.replay[agent_id]["R"].append(reward) + if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): + self.replay[agent_id]["R"].append(self.get_reward()) self._pending_reward_idx += 1 if not done: state_by_agent = self.get_state(event) - if self.record_path: + if self.save_replay: for agent_id, state in state_by_agent.items(): replay = self.replay[agent_id] if replay["S"]: replay["S_"].append(state) replay["S"].append(state) - assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 - + assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 + return state_by_agent def reset(self): self.env.reset() - self.state_info.clear() + self.state_info = None self._event_ticks.clear() self._action_history.clear() self.replay = defaultdict(lambda: defaultdict(list)) @@ -145,5 +145,6 @@ def flush(self): for vals in replay.values(): del vals[:num_complete] - del self.state_info[:self._pending_reward_idx] + del self._event_ticks[:self._pending_reward_idx] + del self._action_history[:self._pending_reward_idx] self._pending_reward_idx = 0 diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 347a3207c..116c438ba 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -31,7 +31,7 @@ def __init__(self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper] def roll_out(self, index: int, training: bool = True): self.env.reset() if not training: - self.env.record_path = False # no need to record the trajectory if roll-out is not for training + self.env.save_replay = False # no need to record the trajectory if roll-out is not for training state = self.env.start(rollout_index=index) # get initial state while state: From c0b6781b70bf97158b199fa9664da45e9ec1d4eb Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 30 Mar 2021 15:00:12 +0000 Subject: [PATCH 127/482] fixed lint issues --- maro/rl/distributed/actor.py | 11 ++++------- maro/rl/training/learner.py | 5 ++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 53cefd8e4..b1b6dbb30 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -1,17 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from abc import ABC, abstractmethod -from collections import defaultdict -from typing import Callable, List, Union +from typing import Union -from maro.communication import Message, Proxy, RegisterTable, SessionType +from maro.communication import Message, Proxy from maro.rl.agent import AbsAgent, MultiAgentWrapper -from maro.rl.storage import SimpleStore from maro.rl.training import AbsEnvWrapper from maro.utils import InternalLogger -from .message_enums import MsgTag, MsgKey +from .message_enums import MsgKey, MsgTag class Actor(object): @@ -71,7 +68,7 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non ) self.env.flush() - return self.env.metrics + return self.env.metrics def run(self): for msg in self._proxy.receive(): diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 116c438ba..da8c92e33 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -2,7 +2,8 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from typing import Callable, Union +from collections import defaultdict +from typing import Union from numpy import asarray @@ -71,6 +72,8 @@ def __init__( env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, + replay_memory_size: int, + replay_memory_overwrite_type: str, train_iter: int = 1, min_experiences_to_train: int = 0, batch_size: int = 128 From 3d6227c142ed93726ae61b8aaebab245d40901fc Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 31 Mar 2021 05:31:47 +0000 Subject: [PATCH 128/482] added single_thread_launcher for suuply chain --- examples/supply_chain/dqn/__init__.py | 2 ++ examples/supply_chain/dqn/agent.py | 31 +++++++++++++++++ .../dqn/{main.py => distributed_launcher.py} | 31 ++--------------- .../dqn/single_thread_launcher.py | 33 +++++++++++++++++++ maro/rl/training/learner.py | 4 +++ 5 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 examples/supply_chain/dqn/__init__.py create mode 100644 examples/supply_chain/dqn/agent.py rename examples/supply_chain/dqn/{main.py => distributed_launcher.py} (76%) create mode 100644 examples/supply_chain/dqn/single_thread_launcher.py diff --git a/examples/supply_chain/dqn/__init__.py b/examples/supply_chain/dqn/__init__.py new file mode 100644 index 000000000..b14b47650 --- /dev/null +++ b/examples/supply_chain/dqn/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. diff --git a/examples/supply_chain/dqn/agent.py b/examples/supply_chain/dqn/agent.py new file mode 100644 index 000000000..f0638c10a --- /dev/null +++ b/examples/supply_chain/dqn/agent.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.rl import DQN, DQNConfig, FullyConnectedBlock, MultiAgentWrapper, OptimOption, SimpleMultiHeadModel + + +# model input and output dimensions +MANUFACTURER_IN_DIM = 6 +MANUFACTURER_OUT_DIM = 10 +CONSUMER_IN_DIM = 8 +CONSUMER_OUT_DIM = 100 + + +def get_dqn_agent(in_dim, out_dim, config): + q_model = SimpleMultiHeadModel( + FullyConnectedBlock(input_dim=in_dim, output_dim=out_dim, **config["model"]), + optim_option=OptimOption(**config["optimization"]) + ) + return DQN(q_model, DQNConfig(**config["hyper_params"])) + + +def get_sc_agents(agent_ids, config): + manufacturer_agents = { + id_: get_dqn_agent(MANUFACTURER_IN_DIM, MANUFACTURER_OUT_DIM, config) + for type_, id_ in agent_ids if type_ == "manufacture" + } + consumer_agents = { + id_: get_dqn_agent(CONSUMER_IN_DIM, CONSUMER_OUT_DIM, config) + for type_, id_ in agent_ids if type_ == "consumer" + } + return MultiAgentWrapper({**manufacturer_agents, **consumer_agents}) diff --git a/examples/supply_chain/dqn/main.py b/examples/supply_chain/dqn/distributed_launcher.py similarity index 76% rename from examples/supply_chain/dqn/main.py rename to examples/supply_chain/dqn/distributed_launcher.py index dbc5d8d96..fa09b6b10 100644 --- a/examples/supply_chain/dqn/main.py +++ b/examples/supply_chain/dqn/distributed_launcher.py @@ -15,18 +15,13 @@ from maro.utils import set_seeds from examples.supply_chain.env_wrapper import SCEnvWrapper +from examples.supply_chain.dqn.agent import get_sc_agents DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: config = yaml.safe_load(config_file) -# model input and output dimensions -MANUFACTURER_IN_DIM = 6 -MANUFACTURER_OUT_DIM = 10 -CONSUMER_IN_DIM = 8 -CONSUMER_OUT_DIM = 100 - # for distributed / multi-process training GROUP = getenv("GROUP", default=config["distributed"]["group"]) REDIS_HOST = config["distributed"]["redis_host"] @@ -34,28 +29,8 @@ NUM_ACTORS = int(getenv("NUMACTORS", default=config["distributed"]["num_actors"])) -def get_dqn_agent(in_dim, out_dim): - q_model = SimpleMultiHeadModel( - FullyConnectedBlock(input_dim=in_dim, output_dim=out_dim, **config["agent"]["model"]), - optim_option=OptimOption(**config["agent"]["optimization"]) - ) - return DQN(q_model, DQNConfig(**config["agent"]["hyper_params"])) - - -def get_sc_agents(agent_ids): - manufacturer_agents = { - id_: get_dqn_agent(MANUFACTURER_IN_DIM, MANUFACTURER_OUT_DIM) - for type_, id_ in agent_ids if type_ == "manufacture" - } - consumer_agents = { - id_: get_dqn_agent(CONSUMER_IN_DIM, CONSUMER_OUT_DIM) - for type_, id_ in agent_ids if type_ == "consumer" - } - return MultiAgentWrapper({**manufacturer_agents, **consumer_agents}) - - def sc_dqn_learner(): - agent = get_sc_agents(Env(**config["training"]["env"]).agent_idx_list) + agent = get_sc_agents(Env(**config["training"]["env"]).agent_idx_list, config["agent"]) scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) actor_proxy = ActorProxy( NUM_ACTORS, GROUP, @@ -75,7 +50,7 @@ def sc_dqn_learner(): def sc_dqn_actor(): env = Env(**config["training"]["env"]) - agent = get_sc_agents(env.agent_idx_list) + agent = get_sc_agents(env.agent_idx_list, config["agent"]) actor = Actor( SCEnvWrapper(env), agent, GROUP, replay_sync_interval=config["distributed"]["replay_sync_interval"], diff --git a/examples/supply_chain/dqn/single_thread_launcher.py b/examples/supply_chain/dqn/single_thread_launcher.py new file mode 100644 index 000000000..17fc174b3 --- /dev/null +++ b/examples/supply_chain/dqn/single_thread_launcher.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import yaml +from os import getenv +from os.path import dirname, join, realpath + +import numpy as np + +from maro.rl import LinearParameterScheduler, OffPolicyLearner +from maro.simulator import Env +from maro.utils import set_seeds + +from examples.supply_chain.env_wrapper import SCEnvWrapper + +from examples.supply_chain.dqn.agent import get_sc_agents + + +# Single-threaded launcher +if __name__ == "__main__": + DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") + with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: + config = yaml.safe_load(config_file) + set_seeds(1024) # for reproducibility + env = Env(**config["training"]["env"]) + agent = get_sc_agents(env.agent_idx_list, config["agent"]) + scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) + learner = OffPolicyLearner( + SCEnvWrapper(env), agent, scheduler, + replay_memory_size=config["training"]["replay_memory"]["size"], + replay_memory_overwrite_type=config["training"]["replay_memory"]["overwrite_type"] + ) + learner.run() diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 116c438ba..e88cac728 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod +from collections import defaultdict from typing import Callable, Union from numpy import asarray @@ -71,6 +72,9 @@ def __init__( env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, + *, + replay_memory_size: int, + replay_memory_overwrite_type: str, train_iter: int = 1, min_experiences_to_train: int = 0, batch_size: int = 128 From 50a382d7445c20de752a738fbbf157d8b1079129 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 31 Mar 2021 05:32:49 +0000 Subject: [PATCH 129/482] updated single-thread learner --- maro/rl/training/learner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index da8c92e33..e88cac728 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from collections import defaultdict -from typing import Union +from typing import Callable, Union from numpy import asarray @@ -72,6 +72,7 @@ def __init__( env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, + *, replay_memory_size: int, replay_memory_overwrite_type: str, train_iter: int = 1, From 076e3b056a2bb89001d2a8872e1971ac18dbd775 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Apr 2021 06:16:53 +0000 Subject: [PATCH 130/482] misc issue fixes --- examples/supply_chain/dqn/config.yml | 6 +- maro/rl/agent/agent_wrapper.py | 74 ------------------- maro/rl/agent/dqn.py | 8 +- .../scheduling/simple_parameter_scheduler.py | 7 +- maro/rl/storage/abs_store.py | 8 +- maro/rl/training/env_wrapper.py | 16 +++- maro/rl/training/learner.py | 4 + 7 files changed, 35 insertions(+), 88 deletions(-) delete mode 100644 maro/rl/agent/agent_wrapper.py diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index e1400b7a3..4612224d8 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -26,13 +26,13 @@ training: env: scenario: supply_chain topology: sample1 - durations: 100 - max_episode: 100 + durations: 10000 + max_episode: 1 min_experiences_to_train: 10 train_iter: 10 batch_size: 128 replay_memory: - size: -1 + size: 50000 overwrite_type: random # prioritized_sampling_by_loss: true exploration: diff --git a/maro/rl/agent/agent_wrapper.py b/maro/rl/agent/agent_wrapper.py deleted file mode 100644 index 2b1b4e579..000000000 --- a/maro/rl/agent/agent_wrapper.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -from typing import List, Union - -from .abs_agent import AbsAgent - - -class MultiAgentWrapper: - """Multi-agent wrapper class that exposes the same interfaces as a single agent.""" - def __init__(self, agent_dict: Union[AbsAgent, dict]): - if isinstance(agent_dict, AbsAgent): - agent_dict = {"AGENT": agent_dict} - self.agent_dict = agent_dict - - def __getitem__(self, agent_id: str): - if len(self.agent_dict) == 1: - return self.agent_dict["AGENT"] - else: - return self.agent_dict[agent_id] - - def __len__(self): - return len(self.agent_dict) - - def choose_action(self, state_by_agent: dict): - return {agent_id: self.agent_dict[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} - - def set_exploration_params(self, params): - # Per-agent exploration parameters - if isinstance(params, dict) and params.keys() <= self.agent_dict.keys(): - for agent_id, params in params.items(): - self.agent_dict[agent_id].set_exploration_params(**params) - # Shared exploration parameters for all agents - else: - for agent in self.agent_dict.values(): - agent.set_exploration_params(**params) - - def load_model(self, model_dict: dict): - """Load models from memory for each agent.""" - for agent_id, model in model_dict.items(): - self.agent_dict[agent_id].load_model(model) - - def dump_model(self, agent_ids: Union[str, List[str]] = None): - """Get agents' underlying models. - - This is usually used in distributed mode where models need to be broadcast to remote roll-out actors. - """ - if agent_ids is None: - return {agent_id: agent.dump_model() for agent_id, agent in self.agent_dict.items()} - elif isinstance(agent_ids, str): - return self.agent_dict[agent_ids].dump_model() - else: - return {agent_id: self.agent_dict[agent_id].dump_model() for agent_id in self.agent_dict} - - def load_model_from_file(self, dir_path): - """Load models from disk for each agent.""" - for agent_id, agent in self.agent_dict.items(): - agent.load_model_from_file(os.path.join(dir_path, agent_id)) - - def dump_model_to_file(self, dir_path: str, agent_ids: Union[str, List[str]] = None): - """Dump agents' models to disk. - - Each agent will use its own name to create a separate file under ``dir_path`` for dumping. - """ - os.makedirs(dir_path, exist_ok=True) - if agent_ids is None: - for agent_id, agent in self.agent_dict.items(): - agent.dump_model_to_file(os.path.join(dir_path, agent_id)) - elif isinstance(agent_ids, str): - self.agent_dict[agent_ids].dump_model_to_file(os.path.join(dir_path, agent_ids)) - else: - for agent_id in agent_ids: - self.agent_dict[agent_id].dump_model_to_file(os.path.join(dir_path, agent_id)) diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index 831b89af3..b3446a13c 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -110,11 +110,13 @@ def learn(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, ne q_all = self._get_q_values(states) q = select_by_actions(q_all, actions) - next_q_all = self._get_q_values(next_states, is_eval=False, training=False) + next_q_all_target = self._get_q_values(next_states, is_eval=False, training=False) if self.config.double: - next_q = select_by_actions(next_q_all) # (N,) + print("double DQN") + next_q_all_eval = self._get_q_values(next_states, training=False) + next_q = select_by_actions(next_q_all_target, next_q_all_eval.max(dim=1)[1]) # (N,) else: - next_q, _ = get_max(next_q_all) # (N,) + next_q, _ = get_max(next_q_all_target) # (N,) loss = get_td_errors(q, next_q, rewards, self.config.reward_discount, loss_func=self.config.loss_func) self.model.step(loss.mean()) diff --git a/maro/rl/scheduling/simple_parameter_scheduler.py b/maro/rl/scheduling/simple_parameter_scheduler.py index 0e4e04550..ffe0c4988 100644 --- a/maro/rl/scheduling/simple_parameter_scheduler.py +++ b/maro/rl/scheduling/simple_parameter_scheduler.py @@ -40,7 +40,7 @@ def __init__( elif isinstance(end, (list, tuple)): end = np.asarray(end) - self._delta = (end - self._current_values) / (self._max_iter - 1) + self._delta = (end - self._current_values) / (self._max_iter - 1) if self._max_iter != 1 else 0 def next_params(self): current_values = self._current_values.copy() @@ -97,8 +97,9 @@ def __init__( elif isinstance(end, (list, tuple)): end = np.asarray(end) - self._delta_1 = (mid - self._current_values) / self._split - self._delta_2 = (end - mid) / (max_iter - self._split - 1) + self._delta_1 = (mid - self._current_values) / self._split if self._split else 0 + phase_2_eps = self._max_iter - self._split - 1 + self._delta_2 = (end - mid) / phase_2_eps if phase_2_eps else 0 def next_params(self): current_values = self._current_values.copy() diff --git a/maro/rl/storage/abs_store.py b/maro/rl/storage/abs_store.py index 0c32fb1e4..021f002cd 100644 --- a/maro/rl/storage/abs_store.py +++ b/maro/rl/storage/abs_store.py @@ -57,13 +57,13 @@ def filter(self, filters: Sequence[Callable]): pass @abstractmethod - def sample(self, size: int, weights: Sequence, replace: bool = True): + def sample(self, size: int, weights: Sequence = None, replace: bool = True): """Obtain a random sample from the experience pool. Args: - size (int): Sample sizes for each round of sampling in the chain. If this is a single integer, it is - used as the sample size for all samplers in the chain. - weights (Sequence): A sequence of sampling weights. + size (int): Sample size. + weights (Sequence): A sequence of sampling weights. If None, uniform sampling is performed. + Defaults to None. replace (bool): If True, sampling is performed with replacement. Defaults to True. Returns: A random sample from the experience pool. diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index 4fba22cad..3b8b9c185 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import time from abc import ABC, abstractmethod from collections import defaultdict @@ -29,6 +30,8 @@ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = self._pending_reward_idx = 0 self._event_ticks = [] # for delayed reward evaluation self._action_history = [] # for delayed reward evaluation + self._tot_raw_step_time = 0 + self._tot_step_time = 0 def start(self, rollout_index: int = None): self.step_index = 0 @@ -76,13 +79,17 @@ def get_reward(self, tick: int = None) -> float: pass def step(self, action_by_agent: dict): + t0 = time.time() self.step_index += 1 self._event_ticks.append(self.env.tick) env_action = self.get_action(action_by_agent) self._action_history.append(env_action) if len(env_action) == 1: env_action = list(env_action.values())[0] - _, event, done = self.env.step(env_action) + t1 = time.time() + _, event, done = self.env.step(None) + t2 = time.time() + self._tot_raw_step_time += t2 - t1 if self.save_replay: if self.reward_eval_delay: @@ -130,8 +137,15 @@ def step(self, action_by_agent: dict): replay["S"].append(state) assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 + t3 = time.time() + self._tot_step_time += t3 - t0 return state_by_agent + print(f"total raw step time: {self._tot_raw_step_time}") + print(f"total step time: {self._tot_step_time}") + self._tot_raw_step_time = 0 + self._tot_step_time = 0 + def reset(self): self.env.reset() self.state_info = None diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index e88cac728..f793f4031 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import time from abc import ABC, abstractmethod from collections import defaultdict from typing import Callable, Union @@ -30,6 +31,7 @@ def __init__(self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper] self.logger = InternalLogger("LEARNER") def roll_out(self, index: int, training: bool = True): + t0 = time.time() self.env.reset() if not training: self.env.save_replay = False # no need to record the trajectory if roll-out is not for training @@ -38,6 +40,8 @@ def roll_out(self, index: int, training: bool = True): while state: action = self.agent.choose_action(state) state = self.env.step(action) + t1 = time.time() + print(f"roll-out time: {t1 - t0}") @abstractmethod def run(self): From 499a7f129e2dfd79869ae422ba785edffc72ad78 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 1 Apr 2021 14:19:11 +0800 Subject: [PATCH 131/482] bug fix --- .../hello_world/supply_chain/env_wrapper.py | 529 ++++++++++++++++++ .../scenarios/supply_chain/business_engine.py | 53 +- .../supply_chain/datamodels/__init__.py | 1 + .../supply_chain/datamodels/consumer.py | 25 +- .../supply_chain/datamodels/distribution.py | 19 +- .../supply_chain/datamodels/facility.py | 9 +- .../supply_chain/datamodels/manufacture.py | 29 - .../supply_chain/datamodels/product.py | 18 + .../supply_chain/datamodels/seller.py | 20 +- .../supply_chain/datamodels/storage.py | 6 +- .../supply_chain/datamodels/vehicle.py | 23 +- .../supply_chain/facilities/facility.py | 98 ++-- .../supply_chain/facilities/retailer.py | 9 +- .../supply_chain/facilities/supplier.py | 10 +- .../supply_chain/facilities/warehouse.py | 17 +- .../scenarios/supply_chain/parser.py | 9 +- .../supply_chain/topologies/core.yml | 13 + .../topologies/sample1/config.yml | 31 +- .../scenarios/supply_chain/units/__init__.py | 1 + .../supply_chain/units/balancesheet.py | 23 + .../scenarios/supply_chain/units/consumer.py | 64 ++- .../supply_chain/units/distribution.py | 53 +- .../supply_chain/units/manufacture.py | 22 +- .../scenarios/supply_chain/units/product.py | 162 ++++-- .../scenarios/supply_chain/units/seller.py | 41 +- .../scenarios/supply_chain/units/storage.py | 4 +- .../supply_chain/units/storeproduct.py | 19 + .../scenarios/supply_chain/units/unitbase.py | 9 + .../scenarios/supply_chain/units/vehicle.py | 41 +- .../simulator/scenarios/supply_chain/world.py | 77 ++- 30 files changed, 1117 insertions(+), 318 deletions(-) create mode 100644 examples/hello_world/supply_chain/env_wrapper.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/product.py create mode 100644 maro/simulator/scenarios/supply_chain/units/balancesheet.py create mode 100644 maro/simulator/scenarios/supply_chain/units/storeproduct.py diff --git a/examples/hello_world/supply_chain/env_wrapper.py b/examples/hello_world/supply_chain/env_wrapper.py new file mode 100644 index 000000000..af72d762a --- /dev/null +++ b/examples/hello_world/supply_chain/env_wrapper.py @@ -0,0 +1,529 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.simulator import Env +from collections import defaultdict, namedtuple +import scipy.stats as st +import numpy as np + +from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction + + +def stock_constraint(f_state): + return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt']+7)*f_state['sale_mean']) + + +def is_replenish_constraint(f_state): + return (f_state['consumption_hist'][-1] > 0) + + +def low_profit(f_state): + return ((f_state['sku_price']-f_state['sku_cost']) * f_state['sale_mean'] <= 1000) + + +def low_stock_constraint(f_state): + return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt']+3)*f_state['sale_mean']) + + +def out_of_stock(f_state): + return (0 < f_state['inventory_in_stock']) + + +atoms = { + 'stock_constraint': stock_constraint, + 'is_replenish_constraint': is_replenish_constraint, + 'low_profit': low_profit, + 'low_stock_constraint': low_stock_constraint, + 'out_of_stock': out_of_stock +} + + +class UnitBaseInfo: + id: int = None + node_index: int = None + config: dict = None + summary: dict = None + + def __init__(self, unit_summary): + self.id = unit_summary["id"] + self.node_index = unit_summary["node_index"] + self.config = unit_summary.get("config", {}) + self.summary = unit_summary + + def __getitem__(self, key, default=None): + if key in self.summary: + return self.summary[key] + + return default + + +class SCEnvWrapper: + def __init__(self, env: Env): + self.env = env + self.storage_ss = env.snapshot_list["storage"] + + self._summary = env.summary['node_mapping'] + self._configs = env.configs + self._agent_types = self._summary["agent_types"] + self._units_mapping = self._summary["unit_mapping"] + self._agent_list = env.agent_idx_list + + self._sku_number = len(self._summary["skus"]) + 1 + + # state for each tick + self._cur_metrics = env.metrics + self._cur_facility_storage_products = None + + # TODO: this is fixed after env setup + self._cur_facility_storage_product_mapping = {} + + self._max_sources_per_facility = self._cur_metrics["max_sources_per_facility"] + + # facility -> { + # data_model_index:int, + # storage:UnitBaseInfo, + # distribution: UnitBaseInfo, + # product_id: { + # consumer: UnitBaseInfo, + # seller: UnitBaseInfo, + # manufacture: UnitBaseInfo + # } + # } + self.facility_levels = {} + + # unit id -> (facility id) + self.unit_2_facility_dict = {} + + for facility_id, facility in self._summary["facilities"].items(): + self.facility_levels[facility_id] = { + "node_index": facility["node_index"], + "config": facility['configs'], + "upstreams": facility["upstreams"], + "skus": facility["skus"] + } + + units = facility["units"] + + storage = units["storage"] + if storage is not None: + self.facility_levels[facility_id]["storage"] = UnitBaseInfo(storage) + + self.unit_2_facility_dict[storage["id"]] = facility_id + + distribution = units["distribution"] + + if distribution is not None: + self.facility_levels[facility_id]["distribution"] = UnitBaseInfo(distribution) + self.unit_2_facility_dict[distribution["id"]] = facility_id + + products = units["products"] + + if products: + for product_id, product in products.items(): + product_info = { + "skuproduct": UnitBaseInfo(product) + } + + self.unit_2_facility_dict[product["id"]] = facility_id + + seller = product['seller'] + + if seller is not None: + product_info["seller"] = UnitBaseInfo(seller) + self.unit_2_facility_dict[seller["id"]] = facility_id + + consumer = product["consumer"] + + if consumer is not None: + product_info["consumer"] = UnitBaseInfo(consumer) + self.unit_2_facility_dict[consumer["id"]] = facility_id + + manufacture = product["manufacture"] + + if manufacture is not None: + product_info["manufacture"] = UnitBaseInfo(manufacture) + self.unit_2_facility_dict[manufacture["id"] + ] = facility_id + + self.facility_levels[facility_id][product_id] = product_info + + def get_state(self, event): + return self._get_state() + + def get_action(self, action_by_agent): + env_action = {} + for agent_id, action in action_by_agent.items(): + # consumer action + if agent_id in self.state_info: + pd_id, sr_id = self.state_info[agent_id]["product_id"], self.state_info[agent_id]["source_id"] + env_action[agent_id] = ConsumerAction( + agent_id, pd_id, sr_id, action, 1) + # manufacturer action + else: + env_action[agent_id] = ManufactureAction(agent_id, action) + + return env_action + + def get_reward(self, tick=None): + step_rewards = self._cur_metrics["step_rewards"] + step_balance_sheet = self._cur_metrics["step_balance_sheet"] + + wc = self.env.configs.settings["global_reward_weight_consumer"] + + parent_facility_balance = {} + + for f_id, sheet in step_balance_sheet.items(): + if f_id in self.unit_2_facility_dict: + # it is a product unit + parent_facility_balance[f_id] = step_balance_sheet[self.unit_2_facility_dict[f_id]] + else: + parent_facility_balance[f_id] = sheet + + consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id] + (1 - wc) * reward for f_id, reward in step_balance_sheet.items() } + + rewards_by_agent = { + "consumer": {}, + "producer": {} + } + + for f_id, reward in step_balance_sheet.items(): + rewards_by_agent["producer"][f_id] = reward + + for f_id, reward in consumer_reward_by_facility.items(): + rewards_by_agent["consumer"][f_id] = reward + + return rewards_by_agent + + def _get_state(self): + self._cur_metrics = self.env.metrics + + state = { + "consumer": {}, + "producer": {} + } + + for agent_info in self._agent_list: + storage_index = self.facility_levels[agent_info.facility_id]['storage'].node_index + + storage_product_list = self.storage_ss[self.env.tick:storage_index:"product_list"].flatten().astype(np.int) + storage_product_number = self.storage_ss[self.env.tick:storage_index:"product_number"].flatten().astype(np.int) + + self._cur_facility_storage_products = { pid:pnum for pid, pnum in zip(storage_product_list, storage_product_number)} + + self._cur_facility_storage_product_mapping = {product_id: i for i, product_id in enumerate(storage_product_list)} + + f_state = self._state(agent_info) + + self._add_global_features(f_state) + + state['consumer'][agent_info.id] = f_state + state['producer'][agent_info.id] = f_state + + return self._serialize_state(state) + + def _state(self, agent_info): + state = {} + + self._add_facility_features(state, agent_info) + self._add_storage_features(state, agent_info) + self._add_bom_features(state, agent_info) + self._add_distributor_features(state, agent_info) + self._add_sale_features(state, agent_info) + self._add_vlt_features(state, agent_info) + self._add_consumer_features(state, agent_info) + self._add_price_features(state, agent_info) + + return state + + def _add_global_features(self, state): + state['global_time'] = self.env.tick + + def _add_facility_features(self, state, agent_info): + state['facility_type'] = [ + 1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] + + # NOTE: We cannot provide facility instance + state["facility"] = None + + state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] + + # NOTE: we have no constraint now + state['constraint_idx'] = [0] + + for atom_name in atoms.keys(): + state[atom_name] = list(np.ones(self._configs.settings['constraint_state_hist_len'])) + + # NOTE: named as facility_id but actually sku id + # NOTE: as our sku id start from 1, so we need expend one-hot length + state['facility_id'] = [0] * self._sku_number + + facility = self.facility_levels[agent_info.facility_id] + + if agent_info.is_facility: + # truely facility + state['facility_info'] = facility['config'] + state['sku_info'] = {} + + metrics = self._cur_metrics["facilities"][agent_info.facility_id] + + state['is_positive_balance'] = 1 if metrics["total_balance_sheet"].total() > 0 else 0 + else: + # a product unit + # 3rd slot is the facility id of this unit + state['facility_info'] = facility['config'] + state['sku_info'] = agent_info.sku + + metrics = self._cur_metrics["products"][agent_info.id] + state['is_positive_balance'] = 1 if metrics["total_balance_sheet"].total() > 0 else 0 + + # NOTE: ignore constraint here + + state['facility_id'][agent_info.sku.id] = 1 + + # NOTE: ignore atom here + + # NOTE: ignore this as we do not implement it now + state['echelon_level'] = 0 + + def _add_storage_features(self, state, agent_info): + facility = self.facility_levels[agent_info.facility_id] + + state['storage_levels'] = [0] * self._sku_number + + state['storage_capacity'] = facility['storage'].config["capacity"] + + state['storage_utilization'] = 0 + + for product_id, product_number in self._cur_facility_storage_products.items(): + state['storage_levels'][product_id] = product_number + state['storage_utilization'] += product_number + + def _add_bom_features(self, state, agent_info): + state['bom_inputs'] = [0] * self._sku_number + state['bom_outputs'] = [0] * self._sku_number + + if not agent_info.is_facility: + state['bom_inputs'][agent_info.sku.id] = 1 + state['bom_outputs'][agent_info.sku.id] = 1 + + def _add_vlt_features(self, state, agent_info): + sku_list = self._summary["skus"] + facility = self.facility_levels[agent_info.facility_id] + + current_source_list = [] + + # only for product unit + if agent_info.sku is not None: + current_source_list = facility["upstreams"].get(agent_info.sku.id, []) + + state['vlt'] = [0] * (self._max_sources_per_facility * self._sku_number) + state['max_vlt'] = 0 + + if not agent_info.is_facility: + # only for sku product + product_info = facility[agent_info.sku.id] + + if "consumer" in product_info and len(current_source_list) > 0: + state['max_vlt'] = product_info["skuproduct"]["max_vlt"] + + for i, source in enumerate(current_source_list): + for j, sku in enumerate(sku_list.values()): + # NOTE: different with original code, our config can make sure that source has product we need + + if sku.id == agent_info.sku.id: + state['vlt'][i * len(sku_list) + j + 1] = facility["skus"][sku.id].vlt + + def _add_sale_features(self, state, agent_info): + state['sale_mean'] = 1.0 + state['sale_std'] = 1.0 + state['sale_gamma'] = 1.0 + state['service_level'] = 0.95 + state['total_backlog_demand'] = 0 + + settings = self.env.configs.settings + + hist_len = settings['sale_hist_len'] + consumption_hist_len = settings['consumption_hist_len'] + + state['sale_hist'] = [0] * hist_len + state['backlog_demand_hist'] = [0] * hist_len + state['consumption_hist'] = [0] * consumption_hist_len + state['pending_order'] = [0] * settings['pending_order_len'] + + if agent_info.is_facility: + return + + product_metrics = self._cur_metrics["products"][agent_info.id] + + # for product unit only + state['service_level'] = agent_info.sku.service_level + state['sale_mean'] = product_metrics["sale_mean"] + state['sale_gamma'] = state['sale_mean'] + state['sale_std'] = product_metrics["sale_std"] + + facility = self.facility_levels[agent_info.facility_id] + product_info = facility[agent_info.sku.id] + + if "consumer" in product_info: + # TODO: implement later + consumer_index = product_info["consumer"].node_index + + consumption_hist = self.env.snapshot_list["consumer"][[self.env.tick - i for i in range(consumption_hist_len)]:consumer_index:"latest_consumptions"] + consumption_hist = consumption_hist.flatten() + + state['consumption_hist'] = list(consumption_hist) + state['pending_order'] = list(product_metrics["pending_order_daily"]) + + if "seller" in product_info: + seller_index = product_info["seller"].node_index + seller_ss = self.env.snapshot_list["seller"] + + single_states = seller_ss[self.env.tick:seller_index:("total_demand")].flatten().astype(np.int) + hist_states = seller_ss[[self.env.tick - i for i in range(hist_len)]:seller_index:("sold", "demand")].flatten().reshape(2, -1).astype(np.int) + + state['total_backlog_demand'] = single_states[0] + state['sale_hist'] = list(hist_states[0]) + state['backlog_demand_hist'] = list(hist_states[1]) + state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma + + def _add_distributor_features(self, state, agent_info): + state['distributor_in_transit_orders'] = 0 + state['distributor_in_transit_orders_qty'] = 0 + + facility = self.facility_levels[agent_info.facility_id] + + distribution = facility.get("distribution", None) + + if distribution is not None: + dist_states = self.env.snapshot_list["distribution"][self.env.tick:distribution.id:("remaining_order_quantity", "remaining_order_number")] + dist_states = dist_states.flatten().astype(np.int) + + state['distributor_in_transit_orders'] = dist_states[1] + state['distributor_in_transit_orders_qty'] = dist_states[0] + + def _add_consumer_features(self, state, agent_info): + state['consumer_source_export_mask'] = [0] * (self._max_sources_per_facility * self._sku_number) + state['consumer_source_inventory'] = [0] * self._sku_number + state['consumer_in_transit_orders'] = [0] * self._sku_number + + state['inventory_in_stock'] = 0 + state['inventory_in_transit'] = 0 + state['inventory_in_distribution'] = 0 + state['inventory_estimated'] = 0 + state['inventory_rop'] = 0 + state['is_over_stock'] = 0 + state['is_out_of_stock'] = 0 + state['is_below_rop'] = 0 + + if agent_info.is_facility: + return + + facility = self.facility_levels[agent_info.facility_id] + product_info = facility[agent_info.sku.id] + + if "consumer" not in product_info: + return + + source_list = facility["upstreams"].get(agent_info.sku.id, []) + + if len(source_list) == 0: + return + + sku_list = self._summary["skus"] + + for i, source in enumerate(source_list): + for j, sku in enumerate(sku_list.values()): + if sku.id == agent_info.sku.id: + state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = self.facility_levels[source]["skus"][sku.id].vlt + + in_transit_orders = self._cur_metrics['facilities'][agent_info.facility_id]["in_transit_orders"] + + for i, sku in enumerate(sku_list.values()): + state['consumer_in_transit_orders'][sku.id] += in_transit_orders[sku.id] + + state['inventory_in_stock'] = self._cur_facility_storage_products[agent_info.sku.id] + state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] + + pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] + + if pending_order is not None: + state['inventory_in_distribution'] = pending_order[agent_info.sku.id] + + state['inventory_estimated'] = (state['inventory_in_stock'] + + state['inventory_in_transit'] + - state['inventory_in_distribution']) + if (state['inventory_estimated'] >= 0.5*state['storage_capacity']): + state['is_over_stock'] = 1 + + if (state['inventory_estimated'] <= 0): + state['is_out_of_stock'] = 1 + + state['inventory_rop'] = (state['max_vlt']*state['sale_mean'] + + np.sqrt(state['max_vlt'])*state['sale_std']*st.norm.ppf(state['service_level'])) + + if state['inventory_estimated'] < state['inventory_rop']: + state['is_below_rop'] = 1 + + def _add_price_features(self, state, agent_info): + state['max_price'] = self._cur_metrics["max_price"] + state['sku_price'] = 0 + state['sku_cost'] = 0 + + if not agent_info.is_facility: + state['sku_price'] = agent_info.sku.price + state['sku_cost'] = agent_info.sku.cost + + def _serialize_state(self, state): + result = { + "consumer": {}, + "producer": {} + } + + keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', + 'constraint_idx', 'is_accepted', 'consumption_hist']), + ('storage_capacity', ['storage_utilization']), + ('sale_gamma', ['sale_std', + 'sale_hist', + 'pending_order', + 'inventory_in_stock', + 'inventory_in_transit', + 'inventory_estimated', + 'inventory_rop']), + ('max_price', ['sku_price', 'sku_cost'])] + + for _type, agents_dict in state.items(): + for agent_id, agent_raw_state in agents_dict.items(): + result[_type][agent_id] = [] + + for norm, fields in keys_in_state: + for field in fields: + vals = agent_raw_state[field] + + if not isinstance(vals, list): + vals = [vals] + if norm is not None: + vals = [ + max(0.0, min(100.0, x/(agent_raw_state[norm]+0.01))) for x in vals] + + result[_type][agent_id].extend(vals) + result[_type][agent_id] = np.array(result[_type][agent_id]) + + return result + + +if __name__ == "__main__": + + start_tick = 0 + durations = 100 + env = Env(scenario="supply_chain", topology="sample1", + start_tick=start_tick, durations=durations) + + ss = SCEnvWrapper(env) + + env.step(None) + + states = ss.get_state(None) + rewards = ss.get_reward(None) + + print(states) + print(rewards) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 76545223b..5348695c9 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -9,7 +9,7 @@ from typing import List, Tuple from .parser import ConfigParser, SupplyChainConfiguration -from .units import UnitBase +from .units import UnitBase, ProductUnit from .world import World @@ -21,6 +21,12 @@ def __init__(self, **kwargs): self._build_world() + self._product_units = [] + + for unit in self.world.units.values(): + if type(unit) == ProductUnit: + self._product_units.append(unit) + self._frame = self.world.frame self._node_mapping = self.world.get_node_mapping() @@ -72,9 +78,7 @@ def get_agent_idx_list(self) -> List[Tuple[str, int]]: Returns: list: List of agent index. """ - return [ - (unit.data_model_name, id) for id, unit in self.world.units.items() if unit.data_model_name in ("consumer","manufacture") - ] + return self.world.agent_list def _step_by_facility(self, tick: int): """Call step functions by facility. @@ -101,7 +105,8 @@ def _reset_by_facility(self): facility.reset() def _register_events(self): - self._event_buffer.register_event_handler(MaroEvents.TAKE_ACTION, self._on_action_received) + self._event_buffer.register_event_handler( + MaroEvents.TAKE_ACTION, self._on_action_received) def _build_world(self): self.update_config_root_path(__file__) @@ -136,3 +141,41 @@ def _dispatch_action(self): entity.set_action(action_obj) self._action_cache = None + + def get_metrics(self): + step_rewards = {} + step_balance_sheet = {} + + for facility in self.world.facilities.values(): + step_rewards[facility.id] = facility.step_reward + step_balance_sheet[facility.id] = facility.step_balance_sheet.total() + + for unit in self._product_units: + step_rewards[unit.id] = unit.step_reward + step_balance_sheet[unit.id] = unit.step_balance_sheet.total() + + return { + "step_rewards": step_rewards, + "step_balance_sheet": step_balance_sheet, + # TODO: move fields that will not change to summary + "max_price": self.world.max_price, + "max_sources_per_facility": self.world.max_sources_per_facility, + "products": { + product.id: { + "total_balance_sheet": product.total_step_balance, + "step_reward": product.step_reward, + "sale_mean": product.get_sale_mean(), + "sale_std": product.get_sale_std(), + "selling_price": product.get_selling_price(), + "pending_order_daily": None if product.consumer is None else product.consumer.pending_order_daily + } for product in self._product_units + }, + "facilities": { + facility.id: { + "total_balance_sheet": facility.total_balance_sheet, + "step_reward": facility.step_reward, + "in_transit_orders": facility.get_in_transit_orders(), + "pending_order": None if facility.distribution is None else facility.distribution.get_pending_order() + } for facility in self.world.facilities.values() + } + } diff --git a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py index 6373ae47e..99cc9e873 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py @@ -9,3 +9,4 @@ from .seller import SellerDataModel from .storage import StorageDataModel from .vehicle import VehicleDataModel +from .product import ProductDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index 5e10633e8..28e94be7a 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -11,37 +11,18 @@ @node("consumer") class ConsumerDataModel(SkuDataModel): """Data model for consumer unit.""" - order_cost = NodeAttribute(AttributeType.UInt) total_purchased = NodeAttribute(AttributeType.UInt) total_received = NodeAttribute(AttributeType.UInt) - source_id = NodeAttribute(AttributeType.UInt) - quantity = NodeAttribute(AttributeType.UInt) - vlt = NodeAttribute(AttributeType.UShort) - - # Id of upstream facilities. - sources = NodeAttribute(AttributeType.UInt, 1, is_list=True) - purchased = NodeAttribute(AttributeType.UInt) received = NodeAttribute(AttributeType.UInt) order_product_cost = NodeAttribute(AttributeType.UInt) + latest_consumptions = NodeAttribute(AttributeType.Float) + pending_order_daily = NodeAttribute(AttributeType.UInt) + def __init__(self): super(ConsumerDataModel, self).__init__() - self._order_cost = 0 - - def initialize(self, order_cost: int = 0): - """Initialize consumer data model with order_cost from configurations. - - Args: - order_cost (int): Order cost from configuration files. - """ - self._order_cost = order_cost - - self.reset() - def reset(self): super(ConsumerDataModel, self).reset() - - self.order_cost = self._order_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py index 0ae79e74c..5acc6c109 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py @@ -11,28 +11,15 @@ @node("distribution") class DistributionDataModel(DataModelBase): """Distribution data model for distribution unit.""" - unit_price = NodeAttribute(AttributeType.Int) - - product_list = NodeAttribute(AttributeType.UInt, 1, is_list=True) - check_in_price = NodeAttribute(AttributeType.UInt, 1, is_list=True) delay_order_penalty = NodeAttribute(AttributeType.UInt, 1, is_list=True) + remaining_order_quantity = NodeAttribute(AttributeType.UInt) + remaining_order_number = NodeAttribute(AttributeType.UInt) + def __init__(self): super(DistributionDataModel, self).__init__() self._unit_price = 0 - def initialize(self, unit_price: int = 0): - """Initialize distribution with unit_price from configuration. - - Args: - unit_price (int): Unit price from configuration files. - """ - self._unit_price = unit_price - - self.reset() - def reset(self): super(DistributionDataModel, self).reset() - - self.unit_price = self._unit_price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/facility.py b/maro/simulator/scenarios/supply_chain/datamodels/facility.py index 468b3813e..64b99463e 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/facility.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/facility.py @@ -10,12 +10,9 @@ @node("facility") class FacilityDataModel(DataModelBase): - """Data model for facilities. - - NOTE: - Not in use for now. - """ - balance_sheet = NodeAttribute(AttributeType.UInt) def __init__(self): super(FacilityDataModel, self).__init__() + + def reset(self): + super(FacilityDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index 345467368..195cdc059 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -11,41 +11,12 @@ @node("manufacture") class ManufactureDataModel(SkuDataModel): """Data model for manufacture unit.""" - # Storage related to this manufacture unit, for easy state retrieving. - storage_id = NodeAttribute(AttributeType.UInt) - storage_data_model_index = NodeAttribute(AttributeType.UInt) - - # Cost to produce one output production. - product_unit_cost = NodeAttribute(AttributeType.UInt) - # Number per tick, different with original manufacturing cost, we just provide number, and cost # user can determine how to calculate the cost. manufacturing_number = NodeAttribute(AttributeType.UInt) - production_rate = NodeAttribute(AttributeType.UInt) - def __init__(self): super(ManufactureDataModel, self).__init__() - self._product_unit_cost = 0 - self._storage_id = 0 - self._storage_data_model_index = 0 - - def initialize(self, storage_data_model_index: int, product_unit_cost: int = 1, storage_id: int = 0): - """Initialize data model, used to assign value after frame reset. - - Args: - product_unit_cost (int): Cost per unit, from configuration files. - storage_id (int): Storage id of this manufacture's facility. - """ - self._product_unit_cost = product_unit_cost - self._storage_id = storage_id - self._storage_data_model_index = storage_data_model_index - - self.reset() def reset(self): super(ManufactureDataModel, self).reset() - - self.product_unit_cost = self._product_unit_cost - self.storage_id = self._storage_id - self.storage_data_model_index = self._storage_data_model_index diff --git a/maro/simulator/scenarios/supply_chain/datamodels/product.py b/maro/simulator/scenarios/supply_chain/datamodels/product.py new file mode 100644 index 000000000..2ab04ebf5 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/product.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, node + +from .skumodel import SkuDataModel + + +@node("product") +class ProductDataModel(SkuDataModel): + + def __init__(self): + super(ProductDataModel, self).__init__() + + def reset(self): + super(ProductDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index 18e45096a..e8ba78fda 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -13,29 +13,13 @@ class SellerDataModel(SkuDataModel): """Data model for seller unit.""" total_sold = NodeAttribute(AttributeType.UInt) - backlog_ratio = NodeAttribute(AttributeType.Float) - - unit_price = NodeAttribute(AttributeType.UInt) - - sale_gamma = NodeAttribute(AttributeType.UInt) - demand = NodeAttribute(AttributeType.UInt) sold = NodeAttribute(AttributeType.UInt) + total_demand = NodeAttribute(AttributeType.UInt) + total_sold = NodeAttribute(AttributeType.UInt) def __init__(self): super(SellerDataModel, self).__init__() - self._unit_price = 0 - self._backlog_ratio = 0 - - def initialize(self, unit_price: int = 0, backlog_ratio: int = 0): - self._unit_price = unit_price - self._backlog_ratio = backlog_ratio - - self.reset() - def reset(self): super(SellerDataModel, self).reset() - - self.unit_price = self._unit_price - self.backlog_ratio = self._backlog_ratio diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py index 739ac9121..07bf57e2b 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/storage.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/storage.py @@ -11,7 +11,6 @@ @node("storage") class StorageDataModel(DataModelBase): """Data model for storage unit.""" - unit_storage_cost = NodeAttribute(AttributeType.UInt) remaining_space = NodeAttribute(AttributeType.UInt) capacity = NodeAttribute(AttributeType.UInt) @@ -22,12 +21,10 @@ class StorageDataModel(DataModelBase): def __init__(self): super(StorageDataModel, self).__init__() - self._unit_storage_cost = 0 self._capacity = 0 self._remaining_space = None - def initialize(self, unit_storage_cost: int = 0, capacity: int = 0, remaining_space: int = None): - self._unit_storage_cost = unit_storage_cost + def initialize(self, capacity: int = 0, remaining_space: int = None): self._capacity = capacity self._remaining_space = remaining_space @@ -36,7 +33,6 @@ def initialize(self, unit_storage_cost: int = 0, capacity: int = 0, remaining_sp def reset(self): super(StorageDataModel, self).reset() - self.unit_storage_cost = self._unit_storage_cost self.capacity = self._capacity if self._remaining_space is not None: diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py index 70ef849c6..693dee29c 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py @@ -10,44 +10,23 @@ @node("vehicle") class VehicleDataModel(DataModelBase): - # Id of current entity - source = NodeAttribute(AttributeType.UInt) - - # Id of target entity. - destination = NodeAttribute(AttributeType.UInt) - # Number of product. payload = NodeAttribute(AttributeType.UInt) - # Index of product. - product_id = NodeAttribute(AttributeType.UInt) - - requested_quantity = NodeAttribute(AttributeType.UInt) - # Patient to wait for products ready. patient = NodeAttribute(AttributeType.UInt) - # Steps to destination. - steps = NodeAttribute(AttributeType.UInt) - - unit_transport_cost = NodeAttribute(AttributeType.UInt) - - position = NodeAttribute(AttributeType.Int, 2) - def __init__(self): super(VehicleDataModel, self).__init__() self._patient = 0 - self._unit_transport_cost = 0 - def initialize(self, patient: int = 100, unit_transport_cost: int = 1): + def initialize(self, patient: int = 100): self._patient = patient - self._unit_transport_cost = unit_transport_cost self.reset() def reset(self): super(VehicleDataModel, self).reset() - self.unit_transport_cost = self._unit_transport_cost self.patient = self._patient diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index 1c31cfef0..313ecbd89 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -1,9 +1,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - +from collections import defaultdict from abc import ABC - +from maro.simulator.scenarios.supply_chain.units.balancesheet import BalanceSheet class FacilityBase(ABC): """Base of all facilities.""" @@ -33,10 +33,27 @@ class FacilityBase(ABC): # Upstream facilities. # Key is sku id, value is the list of product unit from upstream. upstreams: dict = None + downstreams: dict = None # Configuration of this facility. configs: dict = None + data_model_name: str = None + data_model_index: int = 0 + + step_balance_sheet: BalanceSheet = None + total_balance_sheet: BalanceSheet = None + children: list = None + + def __init__(self): + self.upstreams = {} + self.downstreams = {} + + self.step_balance_sheet = BalanceSheet() + self.total_balance_sheet = BalanceSheet() + self.step_reward = 0 + self.children = [] + def parse_skus(self, configs: dict): """Parse sku information from config. @@ -58,7 +75,20 @@ def get_config(self, key: str, default: object = None): def initialize(self): """Initialize this facility after frame is ready.""" - pass + has_storage = self.storage is not None + has_distribution = self.distribution is not None + + self.data_model.initialize() + + if self.storage is not None: + self.children.append(self.storage) + + if self.distribution is not None: + self.children.append(self.distribution) + + if self.products is not None: + for product in self.products.values(): + self.children.append(product) def step(self, tick: int): """Push facility to next step. @@ -66,51 +96,43 @@ def step(self, tick: int): Args: tick (int): Current simulator tick. """ - if self.storage is not None: - self.storage.step(tick) + rewards = [] + balance_sheets = [] - if self.distribution is not None: - self.distribution.step(tick) + for unit in self.children: + unit.step(tick) - if self.products is not None: - for product in self.products.values(): - product.step(tick) + balance_sheets.append(unit.step_balance_sheet) + rewards.append(unit.step_reward) - def flush_states(self): - """Flush states into frame.""" - if self.storage is not None: - self.storage.flush_states() + self.step_balance_sheet = sum(balance_sheets) + self.step_reward = sum(rewards) - if self.distribution is not None: - self.distribution.flush_states() + self.total_balance_sheet += self.step_balance_sheet - if self.products is not None: - for product in self.products.values(): - product.flush_states() + def flush_states(self): + """Flush states into frame.""" + for unit in self.children: + unit.flush_states() def post_step(self, tick: int): """Post processing at the end of step.""" - if self.storage is not None: - self.storage.post_step(tick) - - if self.distribution is not None: - self.distribution.post_step(tick) - - if self.products is not None: - for product in self.products.values(): - product.post_step(tick) + for unit in self.children: + unit.post_step(tick) def reset(self): """Reset facility for new episode.""" - if self.storage is not None: - self.storage.reset() + for unit in self.children: + unit.reset() - if self.distribution is not None: - self.distribution.reset() + def get_in_transit_orders(self): + in_transit_orders = defaultdict(int) - if self.products is not None: - for product in self.products.values(): - product.reset() + for product_id, product in self.products.items(): + if product.consumer is not None: + in_transit_orders[product_id] = product.consumer.get_in_transit_quantity() + + return in_transit_orders def get_node_info(self) -> dict: products_info = {} @@ -122,9 +144,13 @@ def get_node_info(self) -> dict: "id": self.id, "name": self.name, "class": type(self), + "node_index": self.data_model_index, "units": { "storage": self.storage.get_unit_info() if self.storage is not None else None, "distribution": self.distribution.get_unit_info() if self.distribution is not None else None, "products": products_info - } + }, + "configs": self.configs, + "skus": self.skus, + "upstreams": { product_id: [f.id for f in source_list] for product_id, source_list in self.upstreams.items()} } diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index 10378c018..c236c9010 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -23,7 +23,9 @@ class RetailerFacility(FacilityBase): "init_stock", "sale_gamma", "order_cost", - "backlog_ratio" + "backlog_ratio", + "vlt", + "service_level" ) ) @@ -34,6 +36,7 @@ class RetailerFacility(FacilityBase): storage: StorageUnit def __init__(self): + super().__init__() self.skus = {} def parse_skus(self, configs: dict): @@ -47,7 +50,9 @@ def parse_skus(self, configs: dict): sku_config["init_stock"], sku_config.get("sale_gamma", 0), sku_config.get("order_cost", 0), - sku_config.get("backlog_ratio", 0) + sku_config.get("backlog_ratio", 0), + sku_config.get("vlt", 1), + sku_config.get("service_level", 0.9) ) self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index ff763e8dc..f9c58d0b0 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -24,7 +24,9 @@ class SupplierFacility(FacilityBase): "price", "delay_order_penalty", "product_unit_cost", - "order_cost" + "order_cost", + "vlt", + "service_level" ) ) @@ -38,6 +40,8 @@ class SupplierFacility(FacilityBase): products: List[ProductUnit] def __init__(self): + super().__init__() + self.skus = {} def parse_skus(self, configs: dict): @@ -52,7 +56,9 @@ def parse_skus(self, configs: dict): sku_config.get("price", 0), sku_config.get("delay_order_penalty", 0), sku_config.get("product_unit_cost", 1), - sku_config.get("order_cost", 0) + sku_config.get("order_cost", 0), + sku_config.get("vlt", 1), + sku_config.get("service_level", 0.9) ) self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index b0aa2bb82..eaac2f669 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -13,7 +13,15 @@ class WarehouseFacility(FacilityBase): """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" - SkuInfo = namedtuple("SkuInfo", ("name", "init_stock", "id", "price", "delay_order_penalty", "order_cost")) + SkuInfo = namedtuple("SkuInfo", ("name", + "init_stock", + "id", + "price", + "delay_order_penalty", + "order_cost", + "vlt", + "service_level", + "cost")) # Storage unit for this facility, must be a sub class of StorageUnit. storage: StorageUnit = None @@ -25,6 +33,8 @@ class WarehouseFacility(FacilityBase): products: List[ProductUnit] = None def __init__(self): + super().__init__() + self.skus = {} def parse_skus(self, configs: dict): @@ -37,7 +47,10 @@ def parse_skus(self, configs: dict): sku.id, sku_config.get("price", 0), sku_config.get("delay_order_penalty", 0), - sku_config.get("order_cost", 0) + sku_config.get("order_cost", 0), + sku_config.get("vlt", 1), + sku_config.get("service_level", 0.9), + sku_config.get("cost", 0) ) self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/parser.py b/maro/simulator/scenarios/supply_chain/parser.py index 941db8294..9413376c2 100644 --- a/maro/simulator/scenarios/supply_chain/parser.py +++ b/maro/simulator/scenarios/supply_chain/parser.py @@ -9,7 +9,7 @@ DataModelDef = namedtuple("DataModelDef", ("alias", "module_path", "class_name", "class_type", "name_in_frame")) UnitDef = namedtuple("UnitDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) -FacilityDef = namedtuple("FacilityDef", ("alias", "module_path", "class_name", "class_type")) +FacilityDef = namedtuple("FacilityDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) def find_class_type(module_path: str, class_name: str) -> type: @@ -102,7 +102,7 @@ def add_unit_definition(self, alias: str, class_name: str, module_path: str, dat data_model ) - def add_facility_definition(self, alias: str, class_name: str, module_path: str): + def add_facility_definition(self, alias: str, class_name: str, module_path: str, data_model_alias: str): """Add a facility definition. Args: @@ -116,7 +116,8 @@ def add_facility_definition(self, alias: str, class_name: str, module_path: str) alias, module_path, class_name, - find_class_type(module_path, class_name) + find_class_type(module_path, class_name), + data_model_alias ) @@ -182,7 +183,7 @@ def _parse_core_conf(self, conf: dict): module_path = module_conf["path"] for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_facility_definition(class_alias, class_def["class"], module_path) + self._result.add_facility_definition(class_alias, class_def["class"], module_path, class_def.get("datamodel", None)) def _parse_config(self): """Parse configurations.""" diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml index 96ac008bd..712ede252 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/core.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/core.yml @@ -21,6 +21,12 @@ datamodels: ManufactureDataModel: class: "ManufactureDataModel" name_in_frame: "manufacture" + ProductDataModel: + class: "ProductDataModel" + name_in_frame: "product" + FacilityDataModel: + class: "FacilityDataModel" + name_in_frame: "facility" units: modules: @@ -46,6 +52,10 @@ units: datamodel: "ManufactureDataModel" ProductUnit: class: "ProductUnit" + datamodel: "ProductDataModel" + StoreProductUnit: + class: "StoreProductUnit" + datamodel: "ProductDataModel" facilities: modules: @@ -53,7 +63,10 @@ facilities: definitions: WarehouseFacility: class: "WarehouseFacility" + datamodel: "FacilityDataModel" SupplierFacility: class: "SupplierFacility" + datamodel: "FacilityDataModel" RetailerFacility: class: "RetailerFacility" + datamodel: "FacilityDataModel" diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index 3371f5536..e346f6b9e 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -23,8 +23,11 @@ facility_definitions: is_template: true # config will be passed to generator as parameters config: + agent_type: 4 consumer: class: "ConsumerUnit" + config: + agent_type: 1 SupplierFacility: &supplier_facility class: "SupplierFacility" @@ -37,10 +40,13 @@ facility_definitions: class: "ProductUnit" is_template: true config: + agent_type: 3 consumer: class: "ConsumerUnit" manufacture: class: "ManufactureUnit" + config: + agent_type: 0 RetailerFacility: &retailer_facility class: "RetailerFacility" @@ -48,13 +54,18 @@ facility_definitions: storage: class: "StorageUnit" products: - class: "ProductUnit" + class: "StoreProductUnit" is_template: true config: + agent_type: 5 consumer: class: "ConsumerUnit" seller: class: "SellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: 2 # common entity/unit definition as reference to simplify the file. normal_vehicle: &normal_vehicle @@ -254,3 +265,21 @@ world: - [10, 11] - [10, 12] - [11, 12] + +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e7 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index c7e867607..644bb72c1 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -11,3 +11,4 @@ from .storage import StorageUnit from .unitbase import UnitBase from .vehicle import VehicleUnit +from .storeproduct import StoreProductUnit \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/units/balancesheet.py b/maro/simulator/scenarios/supply_chain/units/balancesheet.py new file mode 100644 index 000000000..c01358e13 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/balancesheet.py @@ -0,0 +1,23 @@ + +class BalanceSheet: + def __init__(self, profit: int = 0, loss: int = 0): + self.profit = profit + self.loss = loss + + def total(self) -> int: + return self.profit + self.loss + + def __add__(self, other): + return BalanceSheet(self.profit + other.profit, self.loss + other.loss) + + def __sub__(self, other): + return BalanceSheet(self.profit - other.profit, self.loss - other.loss) + + def __repr__(self): + return f"{round(self.profit+self.loss, 0)} ({round(self.profit, 0)} {round(self.loss, 0)})" + + def __radd__(self, other): + if other == 0: + return self + else: + return self.__add__(other) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 461490a8a..ec0a4e037 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -4,7 +4,7 @@ import warnings from collections import Counter, defaultdict - +from scipy.ndimage.interpolation import shift from .order import Order from .skuunit import SkuUnit @@ -22,6 +22,11 @@ def __init__(self): self.purchased = 0 self.order_cost = 0 self.sources = [] + self.pending_order_daily = None + + # NOTE: this value is not set + self.reward_discount = 0.0 + self.price = 0 def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): """Called after order product is received. @@ -54,9 +59,15 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): def initialize(self): super(ConsumerUnit, self).initialize() + self.pending_order_daily = [0] * self.world.configs.settings["pending_order_len"] + sku = self.facility.skus[self.product_id] - self.data_model.initialize(order_cost=self.facility.get_config("order_cost", 0)) + self.price = sku.price + + self.order_cost = self.facility.get_config("order_cost") + + self.data_model.initialize() if self.facility.upstreams is not None: # Construct sources from facility's upstreams. @@ -88,9 +99,9 @@ def initialize(self): f"No sources for consumer: {self.id}, sku: {self.product_id} in facility: {self.facility.name}.") return - self._init_data_model() - def step(self, tick: int): + self._update_pending_order() + # NOTE: id == 0 means invalid,as our id is 1 based. if not self.action or self.action.quantity <= 0 or self.action.product_id <= 0 or self.action.source_id == 0: return @@ -103,10 +114,19 @@ def step(self, tick: int): source_facility = self.world.get_facility_by_id(self.action.source_id) - self.order_cost = source_facility.distribution.place_order(order) + order_product_cost = source_facility.distribution.place_order(order) self.purchased = self.action.quantity + self.data_model.latest_consumptions = 1.0 + + if order.vlt < len(self.pending_order_daily): + self.pending_order_daily[order.vlt-1] += order.quantity + + order_profit = self.price * order.quantity + self.step_balance_sheet.loss = -self.order_cost - order_product_cost + self.step_reward = -self.order_cost - order_product_cost + self.reward_discount * order_profit + def flush_states(self): if self.received > 0: self.data_model.received = self.received @@ -116,15 +136,10 @@ def flush_states(self): self.data_model.purchased = self.purchased self.data_model.total_purchased += self.purchased - if self.order_cost > 0: - self.data_model.order_product_cost = self.order_cost - def post_step(self, tick: int): # Clear the action states per step. if self.action is not None: - self.data_model.source_id = 0 - self.data_model.quantity = 0 - self.data_model.vlt = 0 + self.data_model.latest_consumptions = 0 # This will set action to None. super(ConsumerUnit, self).post_step(tick) @@ -144,18 +159,27 @@ def post_step(self, tick: int): def reset(self): super(ConsumerUnit, self).reset() - self.open_orders.clear() + self.pending_order_daily = [0] * self.world.configs.settings["pending_order_len"] - self._init_data_model() + self.open_orders.clear() def set_action(self, action: object): super(ConsumerUnit, self).set_action(action) - # record the action - self.data_model.source_id = action.source_id - self.data_model.quantity = action.quantity - self.data_model.vlt = action.vlt + def get_in_transit_quantity(self): + quantity = 0 + + for source_id, orders in self.open_orders.items(): + quantity += orders.get(self.product_id, 0) + + return quantity + + def _update_pending_order(self): + self.pending_order_daily = shift(self.pending_order_daily, -1, cval=0) + + def get_unit_info(self): + info = super().get_unit_info() + + info["sources"] = self.sources - def _init_data_model(self): - for source in self.sources: - self.data_model.sources.append(source) + return info diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index bac1c1a23..65e969e08 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. -from collections import defaultdict, deque +from collections import defaultdict, deque, Counter from typing import Dict from .order import Order @@ -18,13 +18,12 @@ class DistributionUnit(UnitBase): vehicles = None def __init__(self): + super().__init__() self.order_queue = deque() - # Used to map from product id to slot index. - self.product_index_mapping: Dict[int, int] = {} - - # What product we will carry. - self.product_list = [] + self.transportation_cost = Counter() + self.delay_order_penalty = Counter() + self.check_in_order = Counter() def get_pending_order(self) -> Dict[int, int]: """Get orders that states is pending. @@ -37,6 +36,10 @@ def get_pending_order(self) -> Dict[int, int]: for order in self.order_queue: counter[order.product_id] += order.quantity + for vehicle in self.vehicles: + if vehicle.is_enroute(): + counter[vehicle.product_id] += (vehicle.requested_quantity - vehicle.payload) + return counter def place_order(self, order: Order) -> int: @@ -57,8 +60,7 @@ def place_order(self, order: Order) -> int: order_total_price = sku.price * order.quantity # TODO: states related, enable it later if needed. - # product_index = self.product_index_mapping[order.product_id] - # self.data_model.check_in_price[product_index] += order_total_price + self.check_in_order[order.product_id] += order.quantity return order_total_price @@ -67,19 +69,6 @@ def place_order(self, order: Order) -> int: def initialize(self): super(DistributionUnit, self).initialize() - # Init product list in data model. - index = 0 - for sku_id, sku in self.facility.skus.items(): - self.product_list.append(sku_id) - - self.product_index_mapping[sku_id] = index - - index += 1 - - self._init_data_model() - - self.data_model.initialize(self.config.get("unit_price", 0)) - def step(self, tick: int): for vehicle in self.vehicles: # If we have vehicle not on the way and there is any pending order @@ -98,29 +87,31 @@ def step(self, tick: int): # Push vehicle. vehicle.step(tick) - # NOTE: we moved delay_order_penalty from facility to sku, is this ok? + self.transportation_cost[vehicle.product_id] += abs(vehicle.step_reward) + + self.step_balance_sheet += vehicle.step_balance_sheet + # update order's delay penalty per tick. for order in self.order_queue: - product_index = self.product_index_mapping[order.product_id] - - self.data_model.delay_order_penalty[product_index] += self.facility.get_config("delay_order_penalty") + self.delay_order_penalty[order.product_id] += self.facility.get_config("delay_order_penalty") def flush_states(self): for vehicle in self.vehicles: vehicle.flush_states() + # TODO: optimize it later, only update if there is any changes + self.data_model.remaining_order_quantity = sum(order.quantity for order in self.order_queue) + self.data_model.remaining_order_number = len(self.order_queue) + def reset(self): super(DistributionUnit, self).reset() self.order_queue.clear() - self._init_data_model() # Reset vehicles. for vehicle in self.vehicles: vehicle.reset() - def _init_data_model(self): - for product_id in self.product_list: - self.data_model.product_list.append(product_id) - self.data_model.delay_order_penalty.append(0) - self.data_model.check_in_price.append(0) + self.transportation_cost.clear() + self.check_in_order.clear() + self.delay_order_penalty.clear() diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index d39edc42c..4460674bc 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -25,16 +25,18 @@ class ManufactureUnit(SkuUnit): # How many we procedure per current step. manufacture_number = 0 + product_unit_cost = 0 + + def __init__(self): + super().__init__() + def initialize(self): super(ManufactureUnit, self).initialize() facility_sku_info = self.facility.skus[self.product_id] + self.product_unit_cost = facility_sku_info.product_unit_cost - self.data_model.initialize( - self.facility.storage.data_model_index, - product_unit_cost=facility_sku_info.product_unit_cost, - storage_id=self.facility.storage.id - ) + self.data_model.initialize() global_sku_info = self.world.get_sku_by_id(self.product_id) @@ -87,6 +89,11 @@ def step(self, tick: int): else: self.manufacture_number = 0 + cost = self.manufacture_number * self.product_unit_cost + + self.step_balance_sheet.loss = -cost + self.step_reward = -cost + def flush_states(self): if self.manufacture_number > 0: self.data_model.manufacturing_number = self.manufacture_number @@ -96,13 +103,8 @@ def post_step(self, tick: int): self.data_model.manufacturing_number = 0 self.manufacture_number = 0 - if self.action is not None: - self.data_model.production_rate = 0 - # NOTE: call super at last, since it will clear the action. super(ManufactureUnit, self).post_step(tick) def set_action(self, action: ManufactureAction): super(ManufactureUnit, self).set_action(action) - - self.data_model.production_rate = action.production_rate diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index dbf3adfb2..e8345f236 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - +import numpy as np +from .balancesheet import BalanceSheet from .consumer import ConsumerUnit from .manufacture import ManufactureUnit from .seller import SellerUnit from .skuunit import SkuUnit from .storage import StorageUnit - +from .distribution import DistributionUnit class ProductUnit(SkuUnit): """Unit that used to group units of one special sku, usually contains consumer, seller and manufacture.""" @@ -24,64 +25,151 @@ class ProductUnit(SkuUnit): # Storage of this facility, always a reference of facility.storage. storage: StorageUnit = None + distribution: DistributionUnit = None + + total_step_balance: BalanceSheet = None + + def __init__(self): + super().__init__() + self.total_step_balance = BalanceSheet() + + def initialize(self): + super(ProductUnit, self).initialize() + def step(self, tick: int): - if self.consumer is not None: - self.consumer.step(tick) + for unit in self.children: + unit.step(tick) - if self.manufacture is not None: - self.manufacture.step(tick) + self.deposit() - if self.seller is not None: - self.seller.step(tick) + def deposit(self): + balance_sheets = [] + rewards = [] - def flush_states(self): - if self.consumer is not None: - self.consumer.flush_states() + for unit in self.children: + balance_sheets.append(unit.step_balance_sheet) + rewards.append(unit.step_reward) - if self.manufacture is not None: - self.manufacture.flush_states() + storage_reward = 0 + if self.storage is not None: + storage_reward = - self.storage.product_number[self.storage.product_index_mapping[self.product_id]] * self.storage.unit_storage_cost - if self.seller is not None: - self.seller.flush_states() + storage_balance = BalanceSheet(0, storage_reward) - def post_step(self, tick: int): - super(ProductUnit, self).post_step(tick) + balance_sheets.append(storage_balance) + rewards.append(storage_reward) - if self.consumer is not None: - self.consumer.post_step(tick) + distribution_balance = BalanceSheet() - if self.manufacture is not None: - self.manufacture.post_step(tick) + if self.distribution is not None: + check_order = self.distribution.check_in_order[self.product_id] + transport_cost = self.distribution.transportation_cost[self.product_id] + delay_order_penalty = self.distribution.delay_order_penalty[self.product_id] + distribution_cost = -(transport_cost + delay_order_penalty) - if self.seller is not None: - self.seller.post_step(tick) + self.distribution.transportation_cost[self.product_id] = 0 + self.distribution.delay_order_penalty[self.product_id] = 0 - def reset(self): - super(ProductUnit, self).reset() + distribution_balance.profit = check_order * self.get_selling_price() + distribution_balance.loss = distribution_cost - if self.consumer is not None: - self.consumer.reset() + balance_sheets.append(distribution_balance) + rewards.append(distribution_balance.total()) + + if self.product_id in self.facility.downstreams: + for facility in self.facility.downstreams[self.product_id]: + downstream_product = facility.products[self.product_id] + balance_sheets.append(downstream_product.step_balance_sheet) + rewards.append(downstream_product.step_reward) + + self.step_balance_sheet = sum(balance_sheets) + + self.step_reward = sum(rewards) + + self.total_step_balance += self.step_balance_sheet + + def flush_states(self): + for unit in self.children: + unit.flush_states() + + def post_step(self, tick: int): + super(ProductUnit, self).post_step(tick) - if self.manufacture is not None: - self.manufacture.reset() + for unit in self.children: + unit.post_step(tick) - if self.seller is not None: - self.seller.reset() + def reset(self): + super(ProductUnit, self).reset() + + for unit in self.children: + unit.reset() def get_unit_info(self) -> dict: return { "id": self.id, "sku_id": self.product_id, + "max_vlt": self._get_max_vlt(), "node_name": type(self.data_model).__node_name__ if self.data_model is not None else None, "node_index": self.data_model_index if self.data_model is not None else None, "class": type(self), + "config": self.config, "consumer": self.consumer.get_unit_info() if self.consumer is not None else None, "seller": self.seller.get_unit_info() if self.seller is not None else None, "manufacture": self.manufacture.get_unit_info() if self.manufacture is not None else None } + def get_latest_sale(self): + sale = 0 + downstreams = self.facility.downstreams.get(self.product_id, []) + + for facility in downstreams: + sale += facility.products[self.product_id].get_latest_sale() + + return sale + + def get_sale_mean(self): + sale_mean = 0 + downstreams = self.facility.downstreams.get(self.product_id, []) + + for facility in downstreams: + sale_mean += facility.products[self.product_id].get_sale_mean() + + return sale_mean + + def get_sale_std(self): + sale_std = 0 + + downstreams = self.facility.downstreams.get(self.product_id, []) + + for facility in downstreams: + sale_std += facility.products[self.product_id].get_sale_std() + + return sale_std / np.sqrt(max(1, len(downstreams))) + + def get_selling_price(self): + price = 0.0 + downstreams = self.facility.downstreams.get(self.product_id, []) + + for facility in downstreams: + price = max(price, facility.products[self.product_id].get_selling_price()) + + return price + + def _get_max_vlt(self): + vlt = 1 + + if self.consumer is not None: + for source_facility_id in self.consumer.sources: + source_facility = self.world.get_facility_by_id(source_facility_id) + + source_vlt = source_facility.skus[self.product_id].vlt + + vlt = max(vlt, source_vlt) + + return vlt + @staticmethod - def generate(facility, config: dict): + def generate(facility, config: dict, unit_def): """Generate product unit by sku information. Args: @@ -96,10 +184,12 @@ def generate(facility, config: dict): for sku_id, sku in facility.skus.items(): sku_type = getattr(sku, "type", None) - product_unit: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility) + product_unit: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility, unit_def) product_unit.product_id = sku_id - + product_unit.children = [] product_unit.parse_configs(config) + product_unit.storage = product_unit.facility.storage + product_unit.distribution = product_unit.facility.distribution for child_name in ("manufacture", "consumer", "seller"): conf = config.get(child_name, None) @@ -118,7 +208,9 @@ def generate(facility, config: dict): setattr(product_unit, child_name, child_unit) # Parse config for unit. - child_unit.parse_configs(conf) + child_unit.parse_configs(conf.get("config", {})) + + product_unit.children.append(child_unit) instance_list[sku_id] = product_unit diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index c793abeba..83521d030 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -23,6 +23,11 @@ def __init__(self): self.sold = 0 self.demand = 0 self.total_sold = 0 + self.total_demand = 0 + self.price = 0 + + self.sale_hist = [] + self.backlog_ratio = 0 def market_demand(self, tick: int) -> int: """Generate market demand for current tick. @@ -40,21 +45,17 @@ def initialize(self): sku = self.facility.skus[self.product_id] - unit_price = sku.price + self.price = sku.price self.gamma = sku.sale_gamma - backlog_ratio = sku.backlog_ratio - - self.data_model.initialize( - unit_price=unit_price, - backlog_ratio=backlog_ratio - ) - + self.backlog_ratio = sku.backlog_ratio self.durations = self.world.durations # Generate demand distribution of this episode. for _ in range(self.durations): self.demand_distribution.append(int(np.random.gamma(self.gamma))) + self.sale_hist = [self.gamma] * self.config["sale_hist_len"] + def step(self, tick: int): demand = self.market_demand(tick) @@ -64,11 +65,23 @@ def step(self, tick: int): self.total_sold += sold_qty self.sold = sold_qty self.demand = demand + self.total_demand += demand + + self.sale_hist.append(demand) + self.sale_hist = self.sale_hist[1:] + + self.step_balance_sheet.profit = sold_qty * self.price + self.step_balance_sheet.loss = -demand * self.price * self.backlog_ratio + self.step_reward = self.step_balance_sheet.total() def flush_states(self): - self.data_model.sold = self.sold - self.data_model.demand = self.demand - self.data_model.total_sold = self.total_sold + if self.sold > 0: + self.data_model.sold = self.sold + self.data_model.total_sold = self.total_sold + + if self.demand > 0: + self.data_model.demand = self.demand + self.data_model.total_demand = self.total_demand def post_step(self, tick: int): super(SellerUnit, self).post_step(tick) @@ -89,3 +102,9 @@ def reset(self): # for _ in range(self.durations): # self.demand_distribution.append(np.random.gamma(self.gamma)) + + def sale_mean(self): + return np.mean(self.sale_hist) + + def sale_std(self): + return np.std(self.sale_hist) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 242700fdc..b95a669ab 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -109,6 +109,9 @@ def get_product_number(self, product_id: int) -> int: return self.product_number[product_index] + def step(self, tick: int): + self.step_balance_sheet.loss = -(self.capacity - self.remaining_space) * self.unit_storage_cost + def initialize(self): super(StorageUnit, self).initialize() @@ -132,7 +135,6 @@ def initialize(self): self.data_model.initialize( capacity=self.capacity, - unit_storage_cost=self.unit_storage_cost, remaining_space=self.remaining_space ) diff --git a/maro/simulator/scenarios/supply_chain/units/storeproduct.py b/maro/simulator/scenarios/supply_chain/units/storeproduct.py new file mode 100644 index 000000000..6ee1ef8ad --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/storeproduct.py @@ -0,0 +1,19 @@ + +from .product import ProductUnit + + +class StoreProductUnit(ProductUnit): + def get_latest_sale(self): + sale_hist = self.seller.config.get("sale_hist", 0) + + # TODO: why demand not sold? + return 0 if sale_hist == 0 else self.seller.demand + + def get_sale_mean(self): + return self.seller.sale_mean() + + def get_sale_std(self): + return self.seller.sale_std() + + def get_selling_price(self): + return self.facility.skus[self.product_id].price diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py index 7e94cfa9a..d8cc53500 100644 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .balancesheet import BalanceSheet class UnitBase: """Base of all unit used to contain related logic. @@ -50,6 +51,13 @@ class UnitBase: # Current unit configurations. config: dict = None + step_balance_sheet: BalanceSheet = None + step_reward: float = None + + def __init__(self): + self.step_balance_sheet = BalanceSheet() + self.step_reward = 0 + def parse_configs(self, config: dict): """Parse configurations from config. @@ -106,5 +114,6 @@ def get_unit_info(self) -> dict: "node_name": type(self.data_model).__node_name__, "node_index": self.data_model_index, "class": type(self), + "config": self.config, "children": None if self.children is None else [c.get_unit_info() for c in self.children] } diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index 18e8e6688..cdccdb1e6 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -10,6 +10,7 @@ class VehicleUnit(UnitBase): """Unit used to move production from source to destination by order.""" def __init__(self): + super().__init__() # Max patient of current vehicle. self.max_patient: int = None @@ -39,6 +40,8 @@ def __init__(self): self.quantity = 0 self.patient = 0 + self.unit_transport_cost = 0 + def schedule(self, destination: object, product_id: int, quantity: int, vlt: int): """Schedule a job for this vehicle. @@ -48,14 +51,6 @@ def schedule(self, destination: object, product_id: int, quantity: int, vlt: int quantity (int): How many to load. vlt (int): Velocity of vehicle. """ - # Keep these in states, we will not update it until we reach the destination or cancelled. - self.data_model.source = self.facility.id - self.data_model.destination = destination.id - self.data_model.product_id = product_id - self.data_model.requested_quantity = quantity - self.data_model.vlt = vlt - - # Cache. self.product_id = product_id self.destination = destination self.quantity = quantity @@ -73,7 +68,6 @@ def schedule(self, destination: object, product_id: int, quantity: int, vlt: int # Steps to destination. self.steps = len(self.path) // vlt - self.data_model.steps = self.steps # We are waiting for product loading. self.location = 0 @@ -118,16 +112,18 @@ def try_unload(self): self.payload -= unloaded_units + def is_enroute(self): + return self.destination is not None + def initialize(self): super(VehicleUnit, self).initialize() patient = self.config.get("patient", 100) - unit_transport_cost = self.config.get("unit_transport_cost", 1) - self.data_model.initialize(patient=patient, unit_transport_cost=unit_transport_cost) + self.data_model.initialize(patient=patient) - self.data_model.position[:] = -1 self.max_patient = patient + self.unit_transport_cost = self.config.get("unit_transport_cost", 1) def step(self, tick: int): # If we have not arrive at destination yet. @@ -159,12 +155,9 @@ def step(self, tick: int): self.location += self.velocity self.steps -= 1 - self.data_model.steps = self.steps if self.location >= len(self.path): self.location = len(self.path) - 1 - - self.data_model.position[:] = self.path[self.location] else: # Avoid update under idle state. if self.location > 0: @@ -177,13 +170,11 @@ def step(self, tick: int): self._reset_internal_states() self._reset_data_model() - def flush_states(self): - if self.payload > 0: - self.data_model.payload = self.payload + self.step_balance_sheet.loss = -self.payload * self.unit_transport_cost + self.step_reward = -self.payload * self.unit_transport_cost - # Flush if we have an order. - if self.quantity > 0: - self.data_model.patient = self.patient + def flush_states(self): + pass def reset(self): super(VehicleUnit, self).reset() @@ -204,13 +195,5 @@ def _reset_internal_states(self): def _reset_data_model(self): # Reset data model. - self.data_model.source = 0 - self.data_model.steps = 0 - self.data_model.destination = 0 - self.data_model.product_id = 0 - self.data_model.requested_quantity = 0 - self.data_model.vlt = 0 self.data_model.payload = 0 self.data_model.patient = self.max_patient - - self.data_model.position[:] = -1 diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index b685695ab..1fe86c8f3 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -12,9 +12,10 @@ from .facilities import FacilityBase from .frame_builder import build_frame from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef -from .units import UnitBase +from .units import UnitBase, ProductUnit SkuInfo = namedtuple("SkuInfo", ("name", "id", "bom", "output_units_per_lot")) +AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id")) class World: @@ -54,6 +55,13 @@ def __init__(self): # Data model class collection, used to collection data model class and their number in frame. self._data_class_collection = {} + self.agent_list = [] + + self.agent_type_dict = {} + + self.max_sources_per_facility = 0 + self.max_price = 0 + def get_sku_by_name(self, name: str) -> SkuInfo: """Get sku information by name. @@ -172,6 +180,13 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio # Parse config for facility. facility.parse_configs(facility_conf.get("config", {})) + # Due with data model. + data_model_def: DataModelDef = self.configs.data_models[facility_def.data_model_alias] + + # Register the data model, so that it will help to generate related instance index. + facility.data_model_index = self._register_data_model(data_model_def.alias) + facility.data_model_name = data_model_def.name_in_frame + # Build children (units). for child_name, child_conf in facility_conf["children"].items(): setattr(facility, child_name, self.build_unit(facility, facility, child_conf)) @@ -188,20 +203,31 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio if unit.data_model_name is not None: unit.data_model = getattr(self.frame, unit.data_model_name)[unit.data_model_index] + for facility in self.facilities.values(): + if facility.data_model_name is not None: + facility.data_model = getattr(self.frame, facility.data_model_name)[facility.data_model_index] + # Construct the upstream topology. topology = world_config["topology"] for cur_facility_name, topology_conf in topology.items(): facility = self.get_facility_by_name(cur_facility_name) - facility.upstreams = {} - for sku_name, source_facilities in topology_conf.items(): sku = self.get_sku_by_name(sku_name) + facility.upstreams[sku.id] = [] + + self.max_sources_per_facility = max(self.max_sources_per_facility, len(source_facilities)) + + for source_name in source_facilities: + source_facility = self.get_facility_by_name(source_name) + + facility.upstreams[sku.id].append(source_facility) + + if sku.id not in source_facility.downstreams: + source_facility.downstreams[sku.id] = [] - facility.upstreams[sku.id] = [ - self.get_facility_by_name(source_name) for source_name in source_facilities - ] + source_facility.downstreams[sku.id].append(facility) # Call initialize method for facilities. for facility in self.facilities.values(): @@ -239,7 +265,27 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio nx.set_edge_attributes(self._graph, edge_weights, "cost") - def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): + # Collection agent list + for facility in self.facilities.values(): + agent_type = facility.configs["agent_type"] + + self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id)) + + self.agent_type_dict[agent_type] = True + + for sku in facility.skus.values(): + self.max_price = max(self.max_price, sku.price) + + for unit in self.units.values(): + if type(unit) == ProductUnit: + agent_type = unit.config["agent_type"] + + # unit or failicty id, agent type, is facility, sku info, facility id + self.agent_list.append(AgentInfo(unit.id, agent_type, False, unit.facility.skus[unit.product_id], unit.facility.id)) + + self.agent_type_dict[agent_type] = True + + def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase, unit_def: UnitDef): unit = unit_type() unit.id = self._gen_id() @@ -247,6 +293,14 @@ def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBa unit.facility = facility unit.world = self + if unit_def.data_model_alias is not None: + # Due with data model. + data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] + + # Register the data model, so that it will help to generate related instance index. + unit.data_model_index = self._register_data_model(data_model_def.alias) + unit.data_model_name = data_model_def.name_in_frame + self.units[unit.id] = unit return unit @@ -316,7 +370,7 @@ def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBas return unit_instance else: # If this is template unit, then will use the class' static method 'generate' to generate sub-units. - children = unit_def.class_type.generate(facility, config.get("config")) + children = unit_def.class_type.generate(facility, config.get("config"), unit_def) for child in children.values(): child.id = self._gen_id() @@ -346,13 +400,14 @@ def get_node_mapping(self): for unit_id, unit in self.units.items(): if unit.data_model is not None: - id2index_mapping[unit_id] = (unit.data_model_name, unit.data_model_index) + id2index_mapping[unit_id] = (unit.data_model_name, unit.data_model_index, unit.facility.id) else: - id2index_mapping[unit_id] = (None, None) + id2index_mapping[unit_id] = (None, None, unit.facility.id) return { + "agent_types": [k for k in self.agent_type_dict.keys()], "unit_mapping": id2index_mapping, - "skus": {sku.name: sku.id for sku in self._sku_collection.values()}, + "skus": {sku.id: sku for sku in self._sku_collection.values()}, "facilities": facility_info_dict } From 12b66465be9eef3f2a5b43cae6054788edf384e2 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 1 Apr 2021 20:07:49 +0800 Subject: [PATCH 132/482] remove balance sheet and reward in scenario --- .../hello_world/supply_chain/env_wrapper.py | 256 +- .../scenarios/supply_chain/business_engine.py | 17 - .../supply_chain/datamodels/consumer.py | 17 + .../supply_chain/datamodels/manufacture.py | 11 + .../supply_chain/datamodels/product.py | 15 + .../supply_chain/datamodels/seller.py | 13 + .../supply_chain/datamodels/vehicle.py | 7 +- .../supply_chain/facilities/facility.py | 20 +- .../topologies/random2/config.yml | 48130 ++++++++++++++++ .../supply_chain/units/balancesheet.py | 23 - .../scenarios/supply_chain/units/consumer.py | 25 +- .../supply_chain/units/distribution.py | 5 +- .../supply_chain/units/manufacture.py | 11 +- .../scenarios/supply_chain/units/product.py | 54 +- .../scenarios/supply_chain/units/seller.py | 9 +- .../scenarios/supply_chain/units/storage.py | 5 - .../scenarios/supply_chain/units/unitbase.py | 6 +- .../scenarios/supply_chain/units/vehicle.py | 9 +- scripts/random_config.py | 40 +- 19 files changed, 48487 insertions(+), 186 deletions(-) create mode 100644 maro/simulator/scenarios/supply_chain/topologies/random2/config.yml delete mode 100644 maro/simulator/scenarios/supply_chain/units/balancesheet.py diff --git a/examples/hello_world/supply_chain/env_wrapper.py b/examples/hello_world/supply_chain/env_wrapper.py index af72d762a..5ea567018 100644 --- a/examples/hello_world/supply_chain/env_wrapper.py +++ b/examples/hello_world/supply_chain/env_wrapper.py @@ -60,6 +60,8 @@ def __getitem__(self, key, default=None): class SCEnvWrapper: def __init__(self, env: Env): self.env = env + self.balance_cal = BalanceSheetCalculator(env) + self.cur_balance_sheet_reward = None self.storage_ss = env.snapshot_list["storage"] self._summary = env.summary['node_mapping'] @@ -148,6 +150,8 @@ def __init__(self, env: Env): self.facility_levels[facility_id][product_id] = product_info def get_state(self, event): + self.cur_balance_sheet_reward = self.balance_cal.calc() + return self._get_state() def get_action(self, action_by_agent): @@ -165,9 +169,6 @@ def get_action(self, action_by_agent): return env_action def get_reward(self, tick=None): - step_rewards = self._cur_metrics["step_rewards"] - step_balance_sheet = self._cur_metrics["step_balance_sheet"] - wc = self.env.configs.settings["global_reward_weight_consumer"] parent_facility_balance = {} @@ -179,16 +180,16 @@ def get_reward(self, tick=None): else: parent_facility_balance[f_id] = sheet - consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id] + (1 - wc) * reward for f_id, reward in step_balance_sheet.items() } + consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items() } rewards_by_agent = { "consumer": {}, "producer": {} } - for f_id, reward in step_balance_sheet.items(): - rewards_by_agent["producer"][f_id] = reward - + for f_id, bsw in self.cur_balance_sheet_reward.items(): + rewards_by_agent["producer"][f_id] = bsw[1] + for f_id, reward in consumer_reward_by_facility.items(): rewards_by_agent["consumer"][f_id] = reward @@ -266,7 +267,7 @@ def _add_facility_features(self, state, agent_info): metrics = self._cur_metrics["facilities"][agent_info.facility_id] - state['is_positive_balance'] = 1 if metrics["total_balance_sheet"].total() > 0 else 0 + state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 else: # a product unit # 3rd slot is the facility id of this unit @@ -274,7 +275,7 @@ def _add_facility_features(self, state, agent_info): state['sku_info'] = agent_info.sku metrics = self._cur_metrics["products"][agent_info.id] - state['is_positive_balance'] = 1 if metrics["total_balance_sheet"].total() > 0 else 0 + state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 # NOTE: ignore constraint here @@ -510,8 +511,225 @@ def _serialize_state(self, state): return result +class BalanceSheetCalculator: + consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") + seller_features = ("id", "sold", "demand", "price", "backlog_ratio") + manufacture_features = ("id", "manufacturing_number", "product_unit_cost") + product_features = ("id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") + storage_features = ("capacity", "remaining_space") + vehicle_features= ("id", "payload", "unit_transport_cost") + + def __init__(self, env: Env): + self.env = env + self.consumer_ss = env.snapshot_list["consumer"] + self.seller_ss = env.snapshot_list["seller"] + self.manufacture_ss = env.snapshot_list["manufacture"] + self.storage_ss = env.snapshot_list["storage"] + self.distribution_ss = env.snapshot_list["distribution"] + self.vehicle_ss = env.snapshot_list["vehicle"] + self.product_ss = env.snapshot_list["product"] + self.products = [] + self.product_id2index_dict = {} + self.facility_levels = [] + + self.facilities = env.summary["node_mapping"]["facilities"] + + for facility_id, facility in self.facilities.items(): + pid_list = [] + distribution = facility["units"]["distribution"] + + for product_id, product in facility["units"]["products"].items(): + pid_list.append(product["id"]) + consumer = product["consumer"] + seller = product["seller"] + manufacture = product["manufacture"] + + self.product_id2index_dict[product["id"]] = len(self.products) + + self.products.append(( + product["id"], + product_id, + facility["units"]["storage"]["node_index"], + facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution["node_index"] if distribution is not None else None, + facility["downstreams"], + None if consumer is None else (consumer["id"], consumer["node_index"]), + None if seller is None else (seller["id"], seller["node_index"]), + None if manufacture is None else (manufacture["id"], manufacture["node_index"]), + )) + + self.facility_levels.append(( + facility_id, + pid_list, + facility["units"]["storage"]["node_index"], + facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution["node_index"] if distribution is not None else None, + [v["node_index"] for v in distribution["children"]] if distribution is not None else [] + )) + + self.total_balance_sheet = defaultdict(int) + + def calc(self): + tick = self.env.tick + # consumer + consumer_bs_states = self.consumer_ss[tick::self.consumer_features].flatten().reshape(-1, len(self.consumer_features)) + + # quantity * price + consumer_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] + + # balance_sheet_profit = 0 + # order_cost + order_product_cost + consumer_step_balance_sheet_loss = -1 * (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) + + # not sure about this. + reward_discount = 0 + + # consumer step reward: balance sheet los + profile * discount + consumer_step_reward = consumer_step_balance_sheet_loss + consumer_profit * reward_discount + + # seller + seller_bs_states = self.seller_ss[tick::self.seller_features].flatten().reshape(-1, len(self.seller_features)) + + # profit = sold * price + seller_balance_sheet_profit = seller_bs_states[:, 1] * seller_bs_states[:, 3] + + # loss = demand * price * backlog_ratio + seller_balance_sheet_loss = -1 * seller_bs_states[:, 2] * seller_bs_states[:, 3] * seller_bs_states[:, 4] + + # step reward = loss + profit + seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit + + # manufacture + man_bs_states = self.manufacture_ss[tick::self.manufacture_features].flatten().reshape(-1, len(self.manufacture_features)) + + # loss = manufacture number * cost + man_balance_sheet_profit_loss = -1 * man_bs_states[:, 1] * man_bs_states[:, 2] + + # step reward = loss + man_step_reward = man_balance_sheet_profit_loss + + # product + product_bs_states = self.product_ss[tick::self.product_features].flatten().reshape(-1, len(self.product_features)) + + # product distribution loss = check order + delay order penalty + product_distribution_balance_sheet_loss= -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) + + # product distribution profit = check order * price + product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] + + # result we need + product_step_reward = np.zeros((len(self.products,))) + product_balance_sheet_profit = np.zeros((len(self.products,))) + product_balance_sheet_loss = np.zeros((len(self.products, ))) + + # create product number mapping for storages + storages_product_map = {} + for storage_index in range(len(self.storage_ss)): + product_list = self.storage_ss[tick:storage_index:"product_list"].flatten().astype(np.int) + product_number = self.storage_ss[tick:storage_index:"product_number"].flatten().astype(np.int) + + storages_product_map[storage_index] = {pid:pnum for pid, pnum in zip(product_list, product_number)} + + # product balance sheet and reward + # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss + # profit = same as above + # reward = same as above + for i, product in enumerate(self.products): + id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = product + + if consumer: + product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer[1]] + product_step_reward[i] += consumer_step_reward[consumer[1]] + + if seller: + product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller[1]] + product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller[1]] + product_step_reward[i] += seller_step_reward[seller[1]] + + if manufacture: + product_balance_sheet_loss[i] += man_balance_sheet_profit_loss[manufacture[1]] + product_step_reward[i] += man_step_reward[manufacture[1]] + + storage_reward = -1 * storages_product_map[storage_index][product_id] * unit_storage_cost + + product_step_reward[i] += storage_reward + + product_balance_sheet_loss[i] += storage_reward + + if distribution_index is not None: + product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[distribution_index] + product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[distribution_index] + + product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + product_distribution_balance_sheet_profit[distribution_index] + + if downstreams and len(downstreams) > 0: + if product_id in downstreams: + for dfacility in downstreams[product_id]: + dproducts = self.facilities[dfacility]["units"]["products"] + + did = dproducts[product_id]["id"] + + product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] + product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] + product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] + + product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss + + # storage + storage_states = self.storage_ss[tick::self.storage_features].flatten().reshape(-1, len(self.storage_features)) + + # loss = (capacity-remaining space) * cost + storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) + + # vehicles + vehicle_states = self.vehicle_ss[tick::self.vehicle_features].flatten().reshape(-1, len(self.vehicle_features)) + + # loss = cost * payload + vehicle_balance_sheet_loss = -1 * vehicle_states[:, 1] * vehicle_states[:, 2] + vehicle_step_reward = vehicle_balance_sheet_loss + + facility_balance_sheet_loss = np.zeros((len(self.facility_levels),)) + facility_balance_sheet_profit = np.zeros((len(self.facility_levels),)) + facility_step_reward = np.zeros((len(self.facility_levels),)) + + # for facilities + for i, facility in enumerate(self.facility_levels): + id, pid_list, storage_index, unit_storage_cost, distribution_index, vehicle_indices = facility + + # storage balance sheet + # profit=0 + facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * unit_storage_cost + + # distribution balance sheet + if distribution_index is not None: + for vindex in vehicle_indices: + facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vindex] + # distribution unit do not provide reward + + # sku product unit balance sheet + for pid in pid_list: + facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] + facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] + facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] + + result = {} + + for id, bs, rw in zip([item[0] for item in self.products], product_balance_sheet, product_step_reward): + result[id] = (bs, rw) + + self.total_balance_sheet[id] += bs + + facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit + + for id, bs, rw in zip([item[0] for item in self.facility_levels], facility_balance_sheet, facility_step_reward): + result[id] = (bs, rw) + + self.total_balance_sheet[id] += bs + + return result if __name__ == "__main__": + from time import time start_tick = 0 durations = 100 @@ -522,8 +740,20 @@ def _serialize_state(self, state): env.step(None) - states = ss.get_state(None) - rewards = ss.get_reward(None) + # bbs = BalanceSheetCalculator(env) + + # start_time = time() + # ss = bbs.calc() + # end_time = time() + + # print(ss) + + # print("time cost: ", end_time - start_time) + + start_time = time() + ss.get_state(None) + # ss.get_reward(None) + end_time = time() - print(states) - print(rewards) + print("time cost: ", end_time - start_time) + # print(env.metrics) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 5348695c9..1abe4a0cb 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -143,27 +143,12 @@ def _dispatch_action(self): self._action_cache = None def get_metrics(self): - step_rewards = {} - step_balance_sheet = {} - - for facility in self.world.facilities.values(): - step_rewards[facility.id] = facility.step_reward - step_balance_sheet[facility.id] = facility.step_balance_sheet.total() - - for unit in self._product_units: - step_rewards[unit.id] = unit.step_reward - step_balance_sheet[unit.id] = unit.step_balance_sheet.total() - return { - "step_rewards": step_rewards, - "step_balance_sheet": step_balance_sheet, # TODO: move fields that will not change to summary "max_price": self.world.max_price, "max_sources_per_facility": self.world.max_sources_per_facility, "products": { product.id: { - "total_balance_sheet": product.total_step_balance, - "step_reward": product.step_reward, "sale_mean": product.get_sale_mean(), "sale_std": product.get_sale_std(), "selling_price": product.get_selling_price(), @@ -172,8 +157,6 @@ def get_metrics(self): }, "facilities": { facility.id: { - "total_balance_sheet": facility.total_balance_sheet, - "step_reward": facility.step_reward, "in_transit_orders": facility.get_in_transit_orders(), "pending_order": None if facility.distribution is None else facility.distribution.get_pending_order() } for facility in self.world.facilities.values() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index 28e94be7a..023a6eab5 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -21,8 +21,25 @@ class ConsumerDataModel(SkuDataModel): latest_consumptions = NodeAttribute(AttributeType.Float) pending_order_daily = NodeAttribute(AttributeType.UInt) + order_quantity = NodeAttribute(AttributeType.UInt) + + price = NodeAttribute(AttributeType.Float) + order_cost = NodeAttribute(AttributeType.Float) + def __init__(self): super(ConsumerDataModel, self).__init__() + self._price = 0 + self._order_cost = 0 + + def initialize(self, price:int, order_cost: int): + self._price = price + self._order_cost = order_cost + + self.reset() + def reset(self): super(ConsumerDataModel, self).reset() + + self.price = self._price + self.order_cost = self._order_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index 195cdc059..0d056143e 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -15,8 +15,19 @@ class ManufactureDataModel(SkuDataModel): # user can determine how to calculate the cost. manufacturing_number = NodeAttribute(AttributeType.UInt) + product_unit_cost = NodeAttribute(AttributeType.Float) + def __init__(self): super(ManufactureDataModel, self).__init__() + self._product_unit_cost = 0 + + def initialize(self, product_unit_cost): + self._product_unit_cost = product_unit_cost + + self.reset() + def reset(self): super(ManufactureDataModel, self).reset() + + self.product_unit_cost = self._product_unit_cost \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/datamodels/product.py b/maro/simulator/scenarios/supply_chain/datamodels/product.py index 2ab04ebf5..385bd6b97 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/product.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/product.py @@ -11,8 +11,23 @@ @node("product") class ProductDataModel(SkuDataModel): + distribution_check_order = NodeAttribute(AttributeType.UInt) + distribution_transport_cost = NodeAttribute(AttributeType.Float) + distribution_delay_order_penalty = NodeAttribute(AttributeType.Float) + + price = NodeAttribute(AttributeType.Float) + def __init__(self): super(ProductDataModel, self).__init__() + self._price = 0 + + def initialize(self, price: float): + self._price = price + + self.reset() + def reset(self): super(ProductDataModel, self).reset() + + self.price = self._price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index e8ba78fda..c187cb786 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -17,9 +17,22 @@ class SellerDataModel(SkuDataModel): sold = NodeAttribute(AttributeType.UInt) total_demand = NodeAttribute(AttributeType.UInt) total_sold = NodeAttribute(AttributeType.UInt) + price = NodeAttribute(AttributeType.Float) + backlog_ratio = NodeAttribute(AttributeType.Float) def __init__(self): super(SellerDataModel, self).__init__() + self._price = 0 + self._backlog_ratio = 0 + + def initialize(self, price:int, backlog_ratio: float): + self._price = price + self._backlog_ratio = backlog_ratio + + self.reset() def reset(self): super(SellerDataModel, self).reset() + + self.backlog_ratio = self._backlog_ratio + self.price = self._price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py index 693dee29c..dac99a9b3 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py @@ -16,13 +16,17 @@ class VehicleDataModel(DataModelBase): # Patient to wait for products ready. patient = NodeAttribute(AttributeType.UInt) + unit_transport_cost = NodeAttribute(AttributeType.Float) + def __init__(self): super(VehicleDataModel, self).__init__() self._patient = 0 + self._unit_transport_cost = 1 - def initialize(self, patient: int = 100): + def initialize(self, patient: int = 100, unit_transport_cost: int = 1): self._patient = patient + self._unit_transport_cost = unit_transport_cost self.reset() @@ -30,3 +34,4 @@ def reset(self): super(VehicleDataModel, self).reset() self.patient = self._patient + self.unit_transport_cost = self._unit_transport_cost diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index 313ecbd89..f0a2b7268 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -3,7 +3,6 @@ from collections import defaultdict from abc import ABC -from maro.simulator.scenarios.supply_chain.units.balancesheet import BalanceSheet class FacilityBase(ABC): """Base of all facilities.""" @@ -41,17 +40,12 @@ class FacilityBase(ABC): data_model_name: str = None data_model_index: int = 0 - step_balance_sheet: BalanceSheet = None - total_balance_sheet: BalanceSheet = None children: list = None def __init__(self): self.upstreams = {} self.downstreams = {} - self.step_balance_sheet = BalanceSheet() - self.total_balance_sheet = BalanceSheet() - self.step_reward = 0 self.children = [] def parse_skus(self, configs: dict): @@ -96,20 +90,9 @@ def step(self, tick: int): Args: tick (int): Current simulator tick. """ - rewards = [] - balance_sheets = [] - for unit in self.children: unit.step(tick) - balance_sheets.append(unit.step_balance_sheet) - rewards.append(unit.step_reward) - - self.step_balance_sheet = sum(balance_sheets) - self.step_reward = sum(rewards) - - self.total_balance_sheet += self.step_balance_sheet - def flush_states(self): """Flush states into frame.""" for unit in self.children: @@ -152,5 +135,6 @@ def get_node_info(self) -> dict: }, "configs": self.configs, "skus": self.skus, - "upstreams": { product_id: [f.id for f in source_list] for product_id, source_list in self.upstreams.items()} + "upstreams": { product_id: [f.id for f in source_list] for product_id, source_list in self.upstreams.items()}, + "downstreams": { product_id: [f.id for f in source_list] for product_id, source_list in self.downstreams.items() } } diff --git a/maro/simulator/scenarios/supply_chain/topologies/random2/config.yml b/maro/simulator/scenarios/supply_chain/topologies/random2/config.yml new file mode 100644 index 000000000..a2d1f1506 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/random2/config.yml @@ -0,0 +1,48130 @@ +facility_definitions: + RetailerFacility: + children: + products: + class: StoreProductUnit + config: + agent_type: 5 + consumer: + class: ConsumerUnit + seller: + class: SellerUnit + config: + sale_hist_len: 4 + is_template: true + storage: + class: StorageUnit + class: RetailerFacility + config: + agent_type: 2 + SupplierFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: 3 + consumer: + class: ConsumerUnit + manufacture: + class: ManufactureUnit + is_template: true + storage: + class: StorageUnit + class: SupplierFacility + config: + agent_type: 0 + WarehouseFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: 4 + consumer: + class: ConsumerUnit + is_template: true + storage: + class: StorageUnit + class: WarehouseFacility + config: + agent_type: 1 +normal_vehicle: &id001 + class: VehicleUnit + config: + patient: 100 + unit_transport_cost: 1 +settings: + constraint_state_hist_len: 4 + constraint_violate_reward: -10000000.0 + global_reward_weight_consumer: 0.5 + global_reward_weight_producer: 0.5 + initial_balance: 100000 + pending_order_len: 5 + replenishment_discount: 0.9 + reward_normalization: 10000000.0 + total_echelons: 3 +world: + facilities: + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5139100 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 200 + definition_ref: SupplierFacility + name: SUPPLIER0 + skus: + SKU0: + cost: 364 + init_stock: 4250 + price: 405 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU1: + cost: 37 + init_stock: 2100 + price: 42 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU10: + cost: 112 + init_stock: 4550 + price: 125 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU100: + cost: 304 + init_stock: 4600 + price: 338 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU101: + cost: 392 + init_stock: 3450 + price: 436 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU102: + cost: 81 + init_stock: 3350 + price: 90 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU103: + cost: 144 + init_stock: 4050 + price: 161 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU104: + cost: 423 + init_stock: 2000 + price: 470 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU105: + cost: 192 + init_stock: 2700 + price: 214 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU106: + cost: 33 + init_stock: 2550 + price: 37 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU107: + cost: 57 + init_stock: 3050 + price: 64 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU108: + cost: 426 + init_stock: 1500 + price: 474 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU109: + cost: 358 + init_stock: 2550 + price: 398 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU11: + cost: 95 + init_stock: 4400 + price: 106 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU110: + cost: 63 + init_stock: 1250 + price: 71 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU111: + cost: 164 + init_stock: 2500 + price: 183 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU112: + cost: 70 + init_stock: 550 + price: 78 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU113: + cost: 260 + init_stock: 2650 + price: 289 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU114: + cost: 299 + init_stock: 3250 + price: 333 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU115: + cost: 393 + init_stock: 1250 + price: 437 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU116: + cost: 230 + init_stock: 3400 + price: 256 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU117: + cost: 110 + init_stock: 1100 + price: 123 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU118: + cost: 162 + init_stock: 2450 + price: 181 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU119: + cost: 52 + init_stock: 250 + price: 58 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU12: + cost: 283 + init_stock: 3800 + price: 315 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU120: + cost: 27 + init_stock: 600 + price: 31 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU121: + cost: 229 + init_stock: 3350 + price: 255 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU122: + cost: 199 + init_stock: 1850 + price: 222 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU123: + cost: 364 + init_stock: 3100 + price: 405 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU124: + cost: 152 + init_stock: 900 + price: 169 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU125: + cost: 309 + init_stock: 1750 + price: 344 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU126: + cost: 325 + init_stock: 4700 + price: 362 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU127: + cost: 134 + init_stock: 1550 + price: 149 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU128: + cost: 193 + init_stock: 4350 + price: 215 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU129: + cost: 74 + init_stock: 2650 + price: 83 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU13: + cost: 426 + init_stock: 2250 + price: 474 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU130: + cost: 384 + init_stock: 4450 + price: 427 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU131: + cost: 52 + init_stock: 2450 + price: 58 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU132: + cost: 273 + init_stock: 750 + price: 304 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU133: + cost: 144 + init_stock: 1450 + price: 161 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU134: + cost: 343 + init_stock: 3700 + price: 382 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU135: + cost: 113 + init_stock: 4400 + price: 126 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU136: + cost: 387 + init_stock: 350 + price: 431 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU137: + cost: 243 + init_stock: 1100 + price: 270 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU138: + cost: 9 + init_stock: 3150 + price: 10 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU139: + cost: 233 + init_stock: 4100 + price: 259 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU14: + cost: 163 + init_stock: 4550 + price: 182 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU140: + cost: 330 + init_stock: 2750 + price: 367 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU141: + cost: 292 + init_stock: 500 + price: 325 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU142: + cost: 395 + init_stock: 4050 + price: 439 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU143: + cost: 234 + init_stock: 3450 + price: 260 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU144: + cost: 411 + init_stock: 300 + price: 457 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU145: + cost: 432 + init_stock: 3750 + price: 480 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU146: + cost: 218 + init_stock: 4800 + price: 243 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU147: + cost: 300 + init_stock: 3700 + price: 334 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU148: + cost: 128 + init_stock: 2000 + price: 143 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU149: + cost: 226 + init_stock: 2550 + price: 252 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU15: + cost: 288 + init_stock: 1800 + price: 320 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU150: + cost: 74 + init_stock: 3100 + price: 83 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU151: + cost: 52 + init_stock: 1250 + price: 58 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU152: + cost: 228 + init_stock: 1750 + price: 254 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU153: + cost: 44 + init_stock: 3600 + price: 49 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU154: + cost: 351 + init_stock: 500 + price: 391 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU155: + cost: 32 + init_stock: 1150 + price: 36 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU156: + cost: 126 + init_stock: 1850 + price: 140 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU157: + cost: 336 + init_stock: 1950 + price: 374 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU158: + cost: 163 + init_stock: 1350 + price: 182 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU159: + cost: 385 + init_stock: 2000 + price: 428 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU16: + cost: 249 + init_stock: 3550 + price: 277 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU160: + cost: 116 + init_stock: 3900 + price: 129 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU161: + cost: 28 + init_stock: 4800 + price: 32 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU162: + cost: 61 + init_stock: 3050 + price: 68 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU163: + cost: 116 + init_stock: 2400 + price: 129 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU164: + cost: 172 + init_stock: 3400 + price: 192 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU165: + cost: 284 + init_stock: 3100 + price: 316 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU166: + cost: 408 + init_stock: 350 + price: 454 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU167: + cost: 97 + init_stock: 3550 + price: 108 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU168: + cost: 387 + init_stock: 1500 + price: 431 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU169: + cost: 385 + init_stock: 3600 + price: 428 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU17: + cost: 353 + init_stock: 2000 + price: 393 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU170: + cost: 96 + init_stock: 700 + price: 107 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU171: + cost: 12 + init_stock: 4750 + price: 14 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU172: + cost: 261 + init_stock: 4300 + price: 291 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU173: + cost: 171 + init_stock: 750 + price: 190 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU174: + cost: 387 + init_stock: 1300 + price: 431 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU175: + cost: 319 + init_stock: 4350 + price: 355 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU176: + cost: 274 + init_stock: 2450 + price: 305 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU177: + cost: 261 + init_stock: 1150 + price: 291 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU178: + cost: 375 + init_stock: 1000 + price: 417 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU179: + cost: 16 + init_stock: 3050 + price: 18 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU18: + cost: 200 + init_stock: 2600 + price: 223 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU180: + cost: 335 + init_stock: 1850 + price: 373 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU181: + cost: 437 + init_stock: 2900 + price: 486 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU182: + cost: 346 + init_stock: 1700 + price: 385 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU183: + cost: 175 + init_stock: 4000 + price: 195 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU184: + cost: 214 + init_stock: 250 + price: 238 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU185: + cost: 45 + init_stock: 2250 + price: 50 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU186: + cost: 128 + init_stock: 4700 + price: 143 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU187: + cost: 256 + init_stock: 1900 + price: 285 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU188: + cost: 162 + init_stock: 1650 + price: 180 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU189: + cost: 264 + init_stock: 900 + price: 294 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU19: + cost: 270 + init_stock: 1400 + price: 301 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU190: + cost: 352 + init_stock: 1850 + price: 392 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU191: + cost: 111 + init_stock: 850 + price: 124 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU192: + cost: 106 + init_stock: 1850 + price: 118 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU193: + cost: 151 + init_stock: 4100 + price: 168 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU194: + cost: 297 + init_stock: 1800 + price: 331 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU195: + cost: 72 + init_stock: 1650 + price: 81 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU196: + cost: 84 + init_stock: 600 + price: 94 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU197: + cost: 148 + init_stock: 2100 + price: 165 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU198: + cost: 408 + init_stock: 3900 + price: 454 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU199: + cost: 163 + init_stock: 1850 + price: 182 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU2: + cost: 54 + init_stock: 2850 + price: 60 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU20: + cost: 111 + init_stock: 4200 + price: 124 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU200: + cost: 319 + init_stock: 2950 + price: 355 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU201: + cost: 236 + init_stock: 2300 + price: 263 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU202: + cost: 284 + init_stock: 1050 + price: 316 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU203: + cost: 130 + init_stock: 300 + price: 145 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU204: + cost: 25 + init_stock: 1500 + price: 28 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU205: + cost: 171 + init_stock: 4750 + price: 190 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU206: + cost: 299 + init_stock: 3100 + price: 333 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU207: + cost: 289 + init_stock: 3450 + price: 322 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU208: + cost: 258 + init_stock: 800 + price: 287 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU209: + cost: 250 + init_stock: 850 + price: 278 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU21: + cost: 367 + init_stock: 900 + price: 408 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU210: + cost: 289 + init_stock: 2050 + price: 322 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU211: + cost: 410 + init_stock: 2600 + price: 456 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU212: + cost: 223 + init_stock: 850 + price: 248 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU213: + cost: 258 + init_stock: 250 + price: 287 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU214: + cost: 80 + init_stock: 3150 + price: 89 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU215: + cost: 159 + init_stock: 4800 + price: 177 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU216: + cost: 208 + init_stock: 4450 + price: 232 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU217: + cost: 28 + init_stock: 4250 + price: 32 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU218: + cost: 46 + init_stock: 900 + price: 52 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU219: + cost: 447 + init_stock: 2300 + price: 497 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU22: + cost: 447 + init_stock: 1500 + price: 497 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU220: + cost: 392 + init_stock: 2200 + price: 436 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU221: + cost: 52 + init_stock: 3700 + price: 58 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU222: + cost: 156 + init_stock: 700 + price: 174 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU223: + cost: 126 + init_stock: 2300 + price: 140 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU224: + cost: 221 + init_stock: 1350 + price: 246 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU225: + cost: 15 + init_stock: 2650 + price: 17 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU226: + cost: 72 + init_stock: 2800 + price: 81 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU227: + cost: 427 + init_stock: 1500 + price: 475 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU228: + cost: 266 + init_stock: 3250 + price: 296 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU229: + cost: 418 + init_stock: 2450 + price: 465 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU23: + cost: 270 + init_stock: 3400 + price: 301 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU230: + cost: 434 + init_stock: 1750 + price: 483 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU231: + cost: 155 + init_stock: 650 + price: 173 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU232: + cost: 283 + init_stock: 1100 + price: 315 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU233: + cost: 51 + init_stock: 250 + price: 57 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU234: + cost: 283 + init_stock: 3450 + price: 315 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU235: + cost: 82 + init_stock: 400 + price: 92 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU236: + cost: 112 + init_stock: 900 + price: 125 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU237: + cost: 180 + init_stock: 1050 + price: 201 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU238: + cost: 94 + init_stock: 1900 + price: 105 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU239: + cost: 59 + init_stock: 4300 + price: 66 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU24: + cost: 89 + init_stock: 3500 + price: 99 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU240: + cost: 235 + init_stock: 3050 + price: 262 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU241: + cost: 324 + init_stock: 3200 + price: 361 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU242: + cost: 198 + init_stock: 4400 + price: 221 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU243: + cost: 126 + init_stock: 3100 + price: 141 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU244: + cost: 173 + init_stock: 2550 + price: 193 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU245: + cost: 148 + init_stock: 2750 + price: 165 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU246: + cost: 426 + init_stock: 800 + price: 474 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU247: + cost: 59 + init_stock: 4500 + price: 66 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU248: + cost: 122 + init_stock: 650 + price: 136 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU249: + cost: 74 + init_stock: 3000 + price: 83 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU25: + cost: 9 + init_stock: 2750 + price: 11 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU250: + cost: 279 + init_stock: 4250 + price: 310 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU251: + cost: 222 + init_stock: 2200 + price: 247 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU252: + cost: 172 + init_stock: 2400 + price: 192 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU253: + cost: 243 + init_stock: 3000 + price: 270 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU254: + cost: 148 + init_stock: 4950 + price: 165 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU255: + cost: 44 + init_stock: 2700 + price: 49 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU256: + cost: 282 + init_stock: 4300 + price: 314 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU257: + cost: 421 + init_stock: 1200 + price: 468 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU258: + cost: 135 + init_stock: 500 + price: 151 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU259: + cost: 350 + init_stock: 950 + price: 389 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU26: + cost: 313 + init_stock: 1850 + price: 348 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU260: + cost: 81 + init_stock: 4250 + price: 90 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU261: + cost: 109 + init_stock: 3750 + price: 122 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU262: + cost: 13 + init_stock: 900 + price: 15 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU263: + cost: 315 + init_stock: 1950 + price: 350 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU264: + cost: 9 + init_stock: 1550 + price: 10 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU265: + cost: 72 + init_stock: 950 + price: 81 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU266: + cost: 248 + init_stock: 4750 + price: 276 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU267: + cost: 110 + init_stock: 2100 + price: 123 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU268: + cost: 57 + init_stock: 1700 + price: 64 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU269: + cost: 391 + init_stock: 500 + price: 435 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU27: + cost: 176 + init_stock: 2850 + price: 196 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU270: + cost: 61 + init_stock: 2900 + price: 68 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU271: + cost: 12 + init_stock: 400 + price: 14 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU272: + cost: 224 + init_stock: 4150 + price: 249 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU273: + cost: 35 + init_stock: 1000 + price: 39 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU274: + cost: 392 + init_stock: 450 + price: 436 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU275: + cost: 55 + init_stock: 4650 + price: 62 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU276: + cost: 252 + init_stock: 3750 + price: 281 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU277: + cost: 397 + init_stock: 4550 + price: 442 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU278: + cost: 261 + init_stock: 4200 + price: 290 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU279: + cost: 157 + init_stock: 2950 + price: 175 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU28: + cost: 54 + init_stock: 1800 + price: 61 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU280: + cost: 400 + init_stock: 4100 + price: 445 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU281: + cost: 44 + init_stock: 350 + price: 49 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU282: + cost: 449 + init_stock: 1550 + price: 499 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU283: + cost: 279 + init_stock: 2100 + price: 310 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU284: + cost: 342 + init_stock: 2100 + price: 381 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU285: + cost: 340 + init_stock: 1750 + price: 378 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU286: + cost: 329 + init_stock: 4450 + price: 366 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU287: + cost: 258 + init_stock: 3350 + price: 287 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU288: + cost: 21 + init_stock: 600 + price: 24 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU289: + cost: 395 + init_stock: 4950 + price: 439 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU29: + cost: 441 + init_stock: 3600 + price: 490 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU290: + cost: 9 + init_stock: 3850 + price: 11 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU291: + cost: 233 + init_stock: 900 + price: 259 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU292: + cost: 350 + init_stock: 5000 + price: 389 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU293: + cost: 221 + init_stock: 2800 + price: 246 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU294: + cost: 68 + init_stock: 650 + price: 76 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU295: + cost: 19 + init_stock: 4300 + price: 22 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU296: + cost: 350 + init_stock: 4750 + price: 389 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU297: + cost: 45 + init_stock: 3200 + price: 51 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU298: + cost: 242 + init_stock: 4450 + price: 269 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU299: + cost: 12 + init_stock: 4450 + price: 14 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU3: + cost: 423 + init_stock: 4400 + price: 470 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU30: + cost: 196 + init_stock: 2250 + price: 218 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU300: + cost: 84 + init_stock: 1100 + price: 94 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU301: + cost: 375 + init_stock: 1600 + price: 417 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU302: + cost: 144 + init_stock: 4600 + price: 161 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU303: + cost: 193 + init_stock: 3950 + price: 215 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU304: + cost: 130 + init_stock: 2900 + price: 145 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU305: + cost: 396 + init_stock: 400 + price: 441 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU306: + cost: 255 + init_stock: 3450 + price: 284 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU307: + cost: 370 + init_stock: 2800 + price: 412 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU308: + cost: 368 + init_stock: 4000 + price: 409 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU309: + cost: 234 + init_stock: 2550 + price: 260 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU31: + cost: 126 + init_stock: 4400 + price: 140 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU310: + cost: 330 + init_stock: 1400 + price: 367 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU311: + cost: 128 + init_stock: 4550 + price: 143 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU312: + cost: 237 + init_stock: 900 + price: 264 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU313: + cost: 170 + init_stock: 4750 + price: 189 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU314: + cost: 286 + init_stock: 750 + price: 318 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU315: + cost: 27 + init_stock: 2850 + price: 31 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU316: + cost: 193 + init_stock: 4300 + price: 215 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU317: + cost: 64 + init_stock: 1550 + price: 72 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU318: + cost: 436 + init_stock: 4500 + price: 485 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU319: + cost: 196 + init_stock: 1200 + price: 218 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU32: + cost: 342 + init_stock: 1600 + price: 380 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU320: + cost: 400 + init_stock: 5000 + price: 445 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU321: + cost: 65 + init_stock: 4400 + price: 73 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU322: + cost: 108 + init_stock: 3850 + price: 120 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU323: + cost: 216 + init_stock: 4750 + price: 241 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU324: + cost: 242 + init_stock: 5000 + price: 269 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU325: + cost: 265 + init_stock: 1200 + price: 295 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU326: + cost: 20 + init_stock: 4150 + price: 23 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU327: + cost: 300 + init_stock: 3300 + price: 334 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU328: + cost: 96 + init_stock: 550 + price: 107 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU329: + cost: 234 + init_stock: 3000 + price: 260 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU33: + cost: 72 + init_stock: 4200 + price: 81 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU330: + cost: 327 + init_stock: 2200 + price: 364 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU331: + cost: 391 + init_stock: 1400 + price: 435 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU332: + cost: 204 + init_stock: 4150 + price: 227 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU333: + cost: 241 + init_stock: 3150 + price: 268 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU334: + cost: 223 + init_stock: 2150 + price: 248 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU335: + cost: 306 + init_stock: 4700 + price: 341 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU336: + cost: 440 + init_stock: 2500 + price: 489 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU337: + cost: 374 + init_stock: 4900 + price: 416 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU338: + cost: 188 + init_stock: 3700 + price: 209 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU339: + cost: 170 + init_stock: 3100 + price: 189 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU34: + cost: 236 + init_stock: 2350 + price: 263 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU340: + cost: 337 + init_stock: 950 + price: 375 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU341: + cost: 149 + init_stock: 2800 + price: 166 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU342: + cost: 159 + init_stock: 2050 + price: 177 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU343: + cost: 430 + init_stock: 4200 + price: 478 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU344: + cost: 441 + init_stock: 4050 + price: 491 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU345: + cost: 244 + init_stock: 1650 + price: 272 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU346: + cost: 180 + init_stock: 3550 + price: 200 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU347: + cost: 162 + init_stock: 1550 + price: 180 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU348: + cost: 266 + init_stock: 1700 + price: 296 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU349: + cost: 72 + init_stock: 4550 + price: 81 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU35: + cost: 216 + init_stock: 2950 + price: 240 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU350: + cost: 95 + init_stock: 600 + price: 106 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU351: + cost: 91 + init_stock: 3700 + price: 102 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU352: + cost: 246 + init_stock: 800 + price: 274 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU353: + cost: 132 + init_stock: 3400 + price: 147 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU354: + cost: 369 + init_stock: 3900 + price: 410 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU355: + cost: 375 + init_stock: 2800 + price: 417 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU356: + cost: 172 + init_stock: 1750 + price: 192 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU357: + cost: 72 + init_stock: 5000 + price: 80 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU358: + cost: 117 + init_stock: 1600 + price: 130 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU359: + cost: 294 + init_stock: 1600 + price: 327 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU36: + cost: 120 + init_stock: 4600 + price: 134 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU360: + cost: 410 + init_stock: 2750 + price: 456 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU361: + cost: 164 + init_stock: 3350 + price: 183 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU362: + cost: 316 + init_stock: 2050 + price: 352 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU363: + cost: 28 + init_stock: 250 + price: 32 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU364: + cost: 297 + init_stock: 3250 + price: 331 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU365: + cost: 51 + init_stock: 4100 + price: 57 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU366: + cost: 165 + init_stock: 1200 + price: 184 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU367: + cost: 346 + init_stock: 4000 + price: 385 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU368: + cost: 351 + init_stock: 1550 + price: 391 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU369: + cost: 414 + init_stock: 1600 + price: 460 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU37: + cost: 255 + init_stock: 3450 + price: 284 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU370: + cost: 92 + init_stock: 1400 + price: 103 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU371: + cost: 162 + init_stock: 4200 + price: 181 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU372: + cost: 439 + init_stock: 1000 + price: 488 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU373: + cost: 139 + init_stock: 1350 + price: 155 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU374: + cost: 11 + init_stock: 4750 + price: 13 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU375: + cost: 389 + init_stock: 3100 + price: 433 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU376: + cost: 27 + init_stock: 4050 + price: 30 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU377: + cost: 90 + init_stock: 1800 + price: 100 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU378: + cost: 440 + init_stock: 450 + price: 489 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU379: + cost: 415 + init_stock: 2050 + price: 462 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU38: + cost: 293 + init_stock: 4050 + price: 326 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU380: + cost: 413 + init_stock: 650 + price: 459 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU381: + cost: 248 + init_stock: 3800 + price: 276 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU382: + cost: 424 + init_stock: 850 + price: 472 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU383: + cost: 224 + init_stock: 1650 + price: 249 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU384: + cost: 33 + init_stock: 1200 + price: 37 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU385: + cost: 247 + init_stock: 4000 + price: 275 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU386: + cost: 321 + init_stock: 2550 + price: 357 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU387: + cost: 423 + init_stock: 1850 + price: 471 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU388: + cost: 237 + init_stock: 3350 + price: 264 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU389: + cost: 11 + init_stock: 2450 + price: 13 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU39: + cost: 342 + init_stock: 1100 + price: 381 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU390: + cost: 131 + init_stock: 250 + price: 146 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU391: + cost: 328 + init_stock: 4600 + price: 365 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU392: + cost: 440 + init_stock: 2300 + price: 489 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU393: + cost: 419 + init_stock: 550 + price: 466 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU394: + cost: 167 + init_stock: 850 + price: 186 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU395: + cost: 291 + init_stock: 1500 + price: 324 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU396: + cost: 352 + init_stock: 3400 + price: 392 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU397: + cost: 231 + init_stock: 1150 + price: 257 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU398: + cost: 409 + init_stock: 3550 + price: 455 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU399: + cost: 13 + init_stock: 3900 + price: 15 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU4: + cost: 260 + init_stock: 4650 + price: 289 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU40: + cost: 194 + init_stock: 400 + price: 216 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU400: + cost: 319 + init_stock: 4750 + price: 355 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU401: + cost: 135 + init_stock: 400 + price: 150 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU402: + cost: 229 + init_stock: 4600 + price: 255 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU403: + cost: 299 + init_stock: 4150 + price: 333 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU404: + cost: 84 + init_stock: 1800 + price: 94 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU405: + cost: 217 + init_stock: 2900 + price: 242 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU406: + cost: 361 + init_stock: 2750 + price: 402 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU407: + cost: 311 + init_stock: 1950 + price: 346 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU408: + cost: 246 + init_stock: 4100 + price: 274 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU409: + cost: 241 + init_stock: 4700 + price: 268 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU41: + cost: 280 + init_stock: 3400 + price: 312 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU410: + cost: 386 + init_stock: 2400 + price: 429 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU411: + cost: 9 + init_stock: 1500 + price: 11 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU412: + cost: 81 + init_stock: 2950 + price: 91 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU413: + cost: 398 + init_stock: 550 + price: 443 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU414: + cost: 60 + init_stock: 3850 + price: 67 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU415: + cost: 312 + init_stock: 500 + price: 347 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU416: + cost: 26 + init_stock: 2550 + price: 29 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU417: + cost: 439 + init_stock: 3400 + price: 488 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU418: + cost: 388 + init_stock: 4850 + price: 432 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU419: + cost: 34 + init_stock: 650 + price: 38 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU42: + cost: 247 + init_stock: 4150 + price: 275 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU420: + cost: 284 + init_stock: 350 + price: 316 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU421: + cost: 260 + init_stock: 4000 + price: 289 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU422: + cost: 74 + init_stock: 1700 + price: 83 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU423: + cost: 156 + init_stock: 600 + price: 174 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU424: + cost: 445 + init_stock: 2050 + price: 495 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU425: + cost: 127 + init_stock: 2200 + price: 142 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU426: + cost: 210 + init_stock: 800 + price: 234 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU427: + cost: 145 + init_stock: 4200 + price: 162 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU428: + cost: 380 + init_stock: 3100 + price: 423 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU429: + cost: 289 + init_stock: 4950 + price: 322 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU43: + cost: 385 + init_stock: 2700 + price: 428 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU430: + cost: 260 + init_stock: 2250 + price: 289 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU431: + cost: 26 + init_stock: 650 + price: 29 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU432: + cost: 314 + init_stock: 1150 + price: 349 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU433: + cost: 342 + init_stock: 1300 + price: 380 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU434: + cost: 335 + init_stock: 2050 + price: 373 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU435: + cost: 41 + init_stock: 2500 + price: 46 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU436: + cost: 344 + init_stock: 1900 + price: 383 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU437: + cost: 68 + init_stock: 4100 + price: 76 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU438: + cost: 389 + init_stock: 1350 + price: 433 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU439: + cost: 36 + init_stock: 2850 + price: 40 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU44: + cost: 229 + init_stock: 950 + price: 255 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU440: + cost: 111 + init_stock: 2350 + price: 124 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU441: + cost: 205 + init_stock: 2700 + price: 228 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU442: + cost: 17 + init_stock: 2050 + price: 19 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU443: + cost: 401 + init_stock: 3100 + price: 446 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU444: + cost: 190 + init_stock: 2700 + price: 212 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU445: + cost: 356 + init_stock: 1000 + price: 396 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU446: + cost: 113 + init_stock: 3200 + price: 126 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU447: + cost: 18 + init_stock: 2000 + price: 20 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU448: + cost: 328 + init_stock: 3300 + price: 365 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU449: + cost: 295 + init_stock: 400 + price: 328 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU45: + cost: 142 + init_stock: 1400 + price: 158 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU450: + cost: 54 + init_stock: 300 + price: 61 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU451: + cost: 346 + init_stock: 1800 + price: 385 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU452: + cost: 405 + init_stock: 3650 + price: 450 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU453: + cost: 117 + init_stock: 4050 + price: 131 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU454: + cost: 363 + init_stock: 2950 + price: 404 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU455: + cost: 300 + init_stock: 3700 + price: 334 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU456: + cost: 113 + init_stock: 2050 + price: 126 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU457: + cost: 211 + init_stock: 2800 + price: 235 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU458: + cost: 253 + init_stock: 2350 + price: 282 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU459: + cost: 162 + init_stock: 1100 + price: 180 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU46: + cost: 306 + init_stock: 1000 + price: 341 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU460: + cost: 243 + init_stock: 250 + price: 271 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU461: + cost: 117 + init_stock: 1350 + price: 131 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU462: + cost: 88 + init_stock: 450 + price: 98 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU463: + cost: 105 + init_stock: 1150 + price: 117 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU464: + cost: 265 + init_stock: 1200 + price: 295 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU465: + cost: 257 + init_stock: 3850 + price: 286 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU466: + cost: 250 + init_stock: 2650 + price: 278 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU467: + cost: 264 + init_stock: 400 + price: 294 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU468: + cost: 36 + init_stock: 500 + price: 40 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU469: + cost: 432 + init_stock: 5000 + price: 481 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU47: + cost: 414 + init_stock: 550 + price: 460 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU470: + cost: 423 + init_stock: 3050 + price: 470 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU471: + cost: 88 + init_stock: 1550 + price: 98 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU472: + cost: 19 + init_stock: 3950 + price: 22 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU473: + cost: 372 + init_stock: 4100 + price: 414 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU474: + cost: 85 + init_stock: 2050 + price: 95 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU475: + cost: 351 + init_stock: 1800 + price: 390 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU476: + cost: 421 + init_stock: 4300 + price: 468 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU477: + cost: 81 + init_stock: 2000 + price: 91 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU478: + cost: 217 + init_stock: 2550 + price: 242 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU479: + cost: 142 + init_stock: 2500 + price: 158 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU48: + cost: 246 + init_stock: 600 + price: 274 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU480: + cost: 381 + init_stock: 3400 + price: 424 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU481: + cost: 179 + init_stock: 2450 + price: 199 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU482: + cost: 195 + init_stock: 1300 + price: 217 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU483: + cost: 108 + init_stock: 600 + price: 120 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU484: + cost: 219 + init_stock: 4000 + price: 244 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU485: + cost: 239 + init_stock: 2850 + price: 266 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU486: + cost: 218 + init_stock: 600 + price: 243 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU487: + cost: 247 + init_stock: 3500 + price: 275 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU488: + cost: 380 + init_stock: 650 + price: 423 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU489: + cost: 216 + init_stock: 1900 + price: 241 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU49: + cost: 165 + init_stock: 3600 + price: 184 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU490: + cost: 256 + init_stock: 800 + price: 285 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU491: + cost: 300 + init_stock: 3900 + price: 334 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU492: + cost: 333 + init_stock: 3400 + price: 371 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU493: + cost: 361 + init_stock: 4750 + price: 402 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU494: + cost: 75 + init_stock: 4350 + price: 84 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU495: + cost: 312 + init_stock: 1850 + price: 347 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU496: + cost: 159 + init_stock: 2650 + price: 177 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU497: + cost: 234 + init_stock: 1550 + price: 260 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU498: + cost: 402 + init_stock: 4700 + price: 447 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU499: + cost: 382 + init_stock: 3900 + price: 425 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU5: + cost: 430 + init_stock: 3700 + price: 478 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU50: + cost: 384 + init_stock: 900 + price: 427 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU500: + cost: 371 + init_stock: 500 + price: 413 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU501: + cost: 66 + init_stock: 3900 + price: 74 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU502: + cost: 369 + init_stock: 450 + price: 411 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU503: + cost: 413 + init_stock: 2200 + price: 459 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU504: + cost: 292 + init_stock: 2000 + price: 325 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU505: + cost: 291 + init_stock: 3600 + price: 324 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU506: + cost: 429 + init_stock: 400 + price: 477 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU507: + cost: 32 + init_stock: 3550 + price: 36 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU508: + cost: 351 + init_stock: 2850 + price: 390 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU509: + cost: 417 + init_stock: 3550 + price: 464 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU51: + cost: 390 + init_stock: 2450 + price: 434 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU510: + cost: 209 + init_stock: 2750 + price: 233 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU511: + cost: 285 + init_stock: 1350 + price: 317 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU512: + cost: 364 + init_stock: 3600 + price: 405 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU513: + cost: 29 + init_stock: 2450 + price: 33 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU514: + cost: 396 + init_stock: 1200 + price: 441 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU515: + cost: 410 + init_stock: 1750 + price: 456 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU516: + cost: 388 + init_stock: 600 + price: 432 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU517: + cost: 200 + init_stock: 3950 + price: 223 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU518: + cost: 139 + init_stock: 3850 + price: 155 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU519: + cost: 248 + init_stock: 2350 + price: 276 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU52: + cost: 306 + init_stock: 4950 + price: 340 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU520: + cost: 16 + init_stock: 1450 + price: 18 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU521: + cost: 320 + init_stock: 4550 + price: 356 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU522: + cost: 350 + init_stock: 1500 + price: 389 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU523: + cost: 323 + init_stock: 1250 + price: 359 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU524: + cost: 191 + init_stock: 1950 + price: 213 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU525: + cost: 117 + init_stock: 550 + price: 131 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU526: + cost: 213 + init_stock: 1800 + price: 237 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU527: + cost: 144 + init_stock: 600 + price: 161 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU528: + cost: 345 + init_stock: 2450 + price: 384 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU529: + cost: 288 + init_stock: 650 + price: 321 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU53: + cost: 18 + init_stock: 2100 + price: 20 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU530: + cost: 269 + init_stock: 4800 + price: 299 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU531: + cost: 301 + init_stock: 3000 + price: 335 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU532: + cost: 139 + init_stock: 4400 + price: 155 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU533: + cost: 399 + init_stock: 2050 + price: 444 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU534: + cost: 131 + init_stock: 4850 + price: 146 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU535: + cost: 345 + init_stock: 1750 + price: 384 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU536: + cost: 77 + init_stock: 5000 + price: 86 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU537: + cost: 155 + init_stock: 2350 + price: 173 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU538: + cost: 36 + init_stock: 2350 + price: 40 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU539: + cost: 351 + init_stock: 500 + price: 390 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU54: + cost: 145 + init_stock: 3850 + price: 162 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU540: + cost: 261 + init_stock: 2400 + price: 290 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU541: + cost: 172 + init_stock: 1800 + price: 192 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU542: + cost: 86 + init_stock: 2300 + price: 96 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU543: + cost: 177 + init_stock: 2000 + price: 197 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU544: + cost: 367 + init_stock: 3200 + price: 408 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU545: + cost: 387 + init_stock: 3250 + price: 431 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU546: + cost: 79 + init_stock: 850 + price: 88 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU547: + cost: 408 + init_stock: 4800 + price: 454 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU548: + cost: 373 + init_stock: 4550 + price: 415 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU549: + cost: 276 + init_stock: 2900 + price: 307 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU55: + cost: 95 + init_stock: 1050 + price: 106 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU550: + cost: 401 + init_stock: 950 + price: 446 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU551: + cost: 353 + init_stock: 3000 + price: 393 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU552: + cost: 102 + init_stock: 4200 + price: 114 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU553: + cost: 63 + init_stock: 1750 + price: 71 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU554: + cost: 282 + init_stock: 650 + price: 314 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU555: + cost: 442 + init_stock: 850 + price: 492 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU556: + cost: 86 + init_stock: 3300 + price: 96 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU557: + cost: 272 + init_stock: 3600 + price: 303 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU558: + cost: 387 + init_stock: 1050 + price: 430 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU559: + cost: 199 + init_stock: 2950 + price: 222 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU56: + cost: 391 + init_stock: 1150 + price: 435 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU560: + cost: 131 + init_stock: 4450 + price: 146 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU561: + cost: 142 + init_stock: 2050 + price: 158 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU562: + cost: 45 + init_stock: 3700 + price: 51 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU563: + cost: 376 + init_stock: 1250 + price: 418 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU564: + cost: 397 + init_stock: 1200 + price: 442 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU565: + cost: 279 + init_stock: 4350 + price: 310 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU566: + cost: 30 + init_stock: 300 + price: 34 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU567: + cost: 108 + init_stock: 2150 + price: 120 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU568: + cost: 87 + init_stock: 400 + price: 97 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU569: + cost: 256 + init_stock: 4700 + price: 285 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU57: + cost: 360 + init_stock: 1500 + price: 400 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU570: + cost: 144 + init_stock: 2150 + price: 161 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU571: + cost: 45 + init_stock: 4100 + price: 50 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU572: + cost: 440 + init_stock: 2400 + price: 489 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU573: + cost: 246 + init_stock: 1850 + price: 274 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU574: + cost: 321 + init_stock: 1600 + price: 357 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU575: + cost: 238 + init_stock: 4900 + price: 265 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU576: + cost: 123 + init_stock: 3350 + price: 137 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU577: + cost: 378 + init_stock: 3200 + price: 420 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU578: + cost: 66 + init_stock: 750 + price: 74 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU579: + cost: 279 + init_stock: 2800 + price: 310 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU58: + cost: 195 + init_stock: 4050 + price: 217 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU580: + cost: 137 + init_stock: 4950 + price: 153 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU581: + cost: 444 + init_stock: 2550 + price: 494 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU582: + cost: 428 + init_stock: 650 + price: 476 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU583: + cost: 94 + init_stock: 550 + price: 105 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU584: + cost: 328 + init_stock: 2350 + price: 365 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU585: + cost: 165 + init_stock: 2700 + price: 184 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU586: + cost: 423 + init_stock: 4700 + price: 471 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU587: + cost: 177 + init_stock: 4000 + price: 197 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU588: + cost: 13 + init_stock: 1350 + price: 15 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU589: + cost: 166 + init_stock: 1250 + price: 185 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU59: + cost: 35 + init_stock: 2400 + price: 39 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU590: + cost: 313 + init_stock: 3100 + price: 348 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU591: + cost: 146 + init_stock: 1850 + price: 163 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU592: + cost: 274 + init_stock: 4600 + price: 305 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU593: + cost: 431 + init_stock: 2550 + price: 479 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU594: + cost: 430 + init_stock: 2550 + price: 478 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU595: + cost: 31 + init_stock: 2750 + price: 35 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU596: + cost: 404 + init_stock: 1350 + price: 449 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU597: + cost: 18 + init_stock: 4800 + price: 21 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU598: + cost: 44 + init_stock: 1950 + price: 49 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU599: + cost: 180 + init_stock: 3800 + price: 201 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU6: + cost: 424 + init_stock: 2050 + price: 472 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU60: + cost: 21 + init_stock: 4250 + price: 24 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU600: + cost: 288 + init_stock: 2750 + price: 321 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU601: + cost: 166 + init_stock: 500 + price: 185 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU602: + cost: 162 + init_stock: 4700 + price: 180 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU603: + cost: 295 + init_stock: 4750 + price: 328 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU604: + cost: 128 + init_stock: 4800 + price: 143 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU605: + cost: 303 + init_stock: 1100 + price: 337 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU606: + cost: 149 + init_stock: 1250 + price: 166 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU607: + cost: 257 + init_stock: 4050 + price: 286 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU608: + cost: 16 + init_stock: 2050 + price: 18 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU609: + cost: 297 + init_stock: 3900 + price: 330 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU61: + cost: 379 + init_stock: 500 + price: 422 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU610: + cost: 440 + init_stock: 1900 + price: 489 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU611: + cost: 341 + init_stock: 1750 + price: 379 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU612: + cost: 25 + init_stock: 1900 + price: 28 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU613: + cost: 272 + init_stock: 2450 + price: 303 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU614: + cost: 237 + init_stock: 1850 + price: 264 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU615: + cost: 303 + init_stock: 2050 + price: 337 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU616: + cost: 278 + init_stock: 3050 + price: 309 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU617: + cost: 213 + init_stock: 2600 + price: 237 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU618: + cost: 128 + init_stock: 2050 + price: 143 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU619: + cost: 54 + init_stock: 1800 + price: 60 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU62: + cost: 82 + init_stock: 2050 + price: 92 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU620: + cost: 430 + init_stock: 2450 + price: 478 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU621: + cost: 194 + init_stock: 1500 + price: 216 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU622: + cost: 243 + init_stock: 1200 + price: 270 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU623: + cost: 403 + init_stock: 1900 + price: 448 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU624: + cost: 386 + init_stock: 2750 + price: 429 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU625: + cost: 212 + init_stock: 3900 + price: 236 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU626: + cost: 121 + init_stock: 1600 + price: 135 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU627: + cost: 70 + init_stock: 1950 + price: 78 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU628: + cost: 102 + init_stock: 4400 + price: 114 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU629: + cost: 351 + init_stock: 2700 + price: 390 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU63: + cost: 306 + init_stock: 3450 + price: 341 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU630: + cost: 450 + init_stock: 4200 + price: 500 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU631: + cost: 347 + init_stock: 3650 + price: 386 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU632: + cost: 64 + init_stock: 1700 + price: 72 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU633: + cost: 275 + init_stock: 2900 + price: 306 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU634: + cost: 122 + init_stock: 4200 + price: 136 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU635: + cost: 416 + init_stock: 2250 + price: 463 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU636: + cost: 218 + init_stock: 2850 + price: 243 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU637: + cost: 448 + init_stock: 1500 + price: 498 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU638: + cost: 83 + init_stock: 3300 + price: 93 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU639: + cost: 428 + init_stock: 500 + price: 476 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU64: + cost: 135 + init_stock: 4800 + price: 151 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU640: + cost: 315 + init_stock: 2800 + price: 350 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU641: + cost: 390 + init_stock: 1800 + price: 434 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU642: + cost: 314 + init_stock: 3600 + price: 349 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU643: + cost: 15 + init_stock: 1350 + price: 17 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU644: + cost: 24 + init_stock: 4650 + price: 27 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU645: + cost: 128 + init_stock: 450 + price: 143 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU646: + cost: 288 + init_stock: 4350 + price: 320 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU647: + cost: 266 + init_stock: 1300 + price: 296 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU648: + cost: 225 + init_stock: 2400 + price: 251 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU649: + cost: 153 + init_stock: 4300 + price: 171 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU65: + cost: 43 + init_stock: 4000 + price: 48 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU650: + cost: 365 + init_stock: 2350 + price: 406 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU651: + cost: 188 + init_stock: 3550 + price: 209 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU652: + cost: 150 + init_stock: 3400 + price: 167 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU653: + cost: 109 + init_stock: 300 + price: 122 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU654: + cost: 309 + init_stock: 2300 + price: 344 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU655: + cost: 343 + init_stock: 4450 + price: 382 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU656: + cost: 206 + init_stock: 4750 + price: 229 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU657: + cost: 426 + init_stock: 3850 + price: 474 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU658: + cost: 276 + init_stock: 2150 + price: 307 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU659: + cost: 352 + init_stock: 1250 + price: 392 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU66: + cost: 442 + init_stock: 2500 + price: 492 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU660: + cost: 185 + init_stock: 600 + price: 206 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU661: + cost: 90 + init_stock: 5000 + price: 100 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU662: + cost: 254 + init_stock: 300 + price: 283 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU663: + cost: 374 + init_stock: 2200 + price: 416 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU664: + cost: 83 + init_stock: 3500 + price: 93 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU665: + cost: 373 + init_stock: 4950 + price: 415 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU666: + cost: 340 + init_stock: 250 + price: 378 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU667: + cost: 102 + init_stock: 3900 + price: 114 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU668: + cost: 103 + init_stock: 800 + price: 115 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU669: + cost: 372 + init_stock: 1000 + price: 414 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU67: + cost: 14 + init_stock: 3100 + price: 16 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU670: + cost: 153 + init_stock: 3300 + price: 171 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU671: + cost: 146 + init_stock: 1950 + price: 163 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU672: + cost: 60 + init_stock: 550 + price: 67 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU673: + cost: 393 + init_stock: 3700 + price: 437 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU674: + cost: 45 + init_stock: 950 + price: 50 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU675: + cost: 431 + init_stock: 1000 + price: 479 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU676: + cost: 414 + init_stock: 2450 + price: 460 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU677: + cost: 352 + init_stock: 4700 + price: 392 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU678: + cost: 128 + init_stock: 3350 + price: 143 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU679: + cost: 117 + init_stock: 4100 + price: 131 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU68: + cost: 97 + init_stock: 2300 + price: 108 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU680: + cost: 392 + init_stock: 1200 + price: 436 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU681: + cost: 304 + init_stock: 650 + price: 338 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU682: + cost: 131 + init_stock: 800 + price: 146 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU683: + cost: 437 + init_stock: 1800 + price: 486 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU684: + cost: 333 + init_stock: 3600 + price: 370 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU685: + cost: 62 + init_stock: 3850 + price: 69 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU686: + cost: 50 + init_stock: 1350 + price: 56 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU687: + cost: 277 + init_stock: 2400 + price: 308 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU688: + cost: 180 + init_stock: 2650 + price: 200 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU689: + cost: 32 + init_stock: 800 + price: 36 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU69: + cost: 296 + init_stock: 1400 + price: 329 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU690: + cost: 254 + init_stock: 350 + price: 283 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU691: + cost: 379 + init_stock: 4900 + price: 422 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU692: + cost: 411 + init_stock: 3250 + price: 457 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU693: + cost: 315 + init_stock: 2400 + price: 351 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU694: + cost: 396 + init_stock: 1600 + price: 441 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU695: + cost: 309 + init_stock: 4900 + price: 344 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU696: + cost: 340 + init_stock: 1750 + price: 378 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU697: + cost: 434 + init_stock: 4600 + price: 483 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU698: + cost: 47 + init_stock: 2500 + price: 53 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU699: + cost: 238 + init_stock: 3200 + price: 265 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU7: + cost: 350 + init_stock: 600 + price: 389 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU70: + cost: 126 + init_stock: 1900 + price: 140 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU700: + cost: 109 + init_stock: 1000 + price: 122 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU701: + cost: 354 + init_stock: 1900 + price: 394 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU702: + cost: 70 + init_stock: 1350 + price: 78 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU703: + cost: 424 + init_stock: 3550 + price: 472 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU704: + cost: 345 + init_stock: 2200 + price: 384 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU705: + cost: 437 + init_stock: 2800 + price: 486 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU706: + cost: 199 + init_stock: 1850 + price: 222 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU707: + cost: 175 + init_stock: 1850 + price: 195 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU708: + cost: 117 + init_stock: 4950 + price: 131 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU709: + cost: 123 + init_stock: 1750 + price: 137 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU71: + cost: 342 + init_stock: 1850 + price: 381 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU710: + cost: 167 + init_stock: 700 + price: 186 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU711: + cost: 171 + init_stock: 3050 + price: 190 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU712: + cost: 345 + init_stock: 1800 + price: 384 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU713: + cost: 206 + init_stock: 3450 + price: 229 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU714: + cost: 327 + init_stock: 4500 + price: 364 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU715: + cost: 211 + init_stock: 450 + price: 235 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU716: + cost: 97 + init_stock: 4450 + price: 108 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU717: + cost: 10 + init_stock: 600 + price: 12 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU718: + cost: 357 + init_stock: 1100 + price: 397 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU719: + cost: 18 + init_stock: 1850 + price: 21 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU72: + cost: 205 + init_stock: 4850 + price: 228 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU720: + cost: 108 + init_stock: 2400 + price: 120 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU721: + cost: 238 + init_stock: 4150 + price: 265 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU722: + cost: 317 + init_stock: 2900 + price: 353 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU723: + cost: 129 + init_stock: 4250 + price: 144 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU724: + cost: 108 + init_stock: 3550 + price: 120 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU725: + cost: 37 + init_stock: 4450 + price: 42 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU726: + cost: 118 + init_stock: 4350 + price: 132 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU727: + cost: 92 + init_stock: 1750 + price: 103 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU728: + cost: 26 + init_stock: 1700 + price: 29 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU729: + cost: 225 + init_stock: 4850 + price: 251 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU73: + cost: 300 + init_stock: 3650 + price: 334 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU730: + cost: 63 + init_stock: 3200 + price: 71 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU731: + cost: 432 + init_stock: 4600 + price: 480 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU732: + cost: 207 + init_stock: 3350 + price: 231 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU733: + cost: 220 + init_stock: 1400 + price: 245 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU734: + cost: 105 + init_stock: 2900 + price: 117 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU735: + cost: 177 + init_stock: 1300 + price: 197 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU736: + cost: 198 + init_stock: 3100 + price: 221 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU737: + cost: 234 + init_stock: 4250 + price: 260 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU738: + cost: 440 + init_stock: 2300 + price: 489 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU739: + cost: 405 + init_stock: 1900 + price: 451 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU74: + cost: 326 + init_stock: 500 + price: 363 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU740: + cost: 225 + init_stock: 250 + price: 250 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU741: + cost: 125 + init_stock: 850 + price: 139 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU742: + cost: 156 + init_stock: 4450 + price: 174 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU743: + cost: 71 + init_stock: 4100 + price: 79 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU744: + cost: 273 + init_stock: 1950 + price: 304 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU745: + cost: 94 + init_stock: 4650 + price: 105 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU746: + cost: 357 + init_stock: 4700 + price: 397 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU747: + cost: 383 + init_stock: 1800 + price: 426 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU748: + cost: 152 + init_stock: 2350 + price: 169 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU749: + cost: 389 + init_stock: 2100 + price: 433 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU75: + cost: 161 + init_stock: 3250 + price: 179 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU750: + cost: 335 + init_stock: 3400 + price: 373 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU751: + cost: 176 + init_stock: 1700 + price: 196 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU752: + cost: 60 + init_stock: 500 + price: 67 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU753: + cost: 145 + init_stock: 3050 + price: 162 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU754: + cost: 185 + init_stock: 3200 + price: 206 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU755: + cost: 112 + init_stock: 2150 + price: 125 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU756: + cost: 193 + init_stock: 4100 + price: 215 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU757: + cost: 236 + init_stock: 1100 + price: 263 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU758: + cost: 63 + init_stock: 4350 + price: 70 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU759: + cost: 279 + init_stock: 3950 + price: 310 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU76: + cost: 417 + init_stock: 4200 + price: 464 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU760: + cost: 438 + init_stock: 1100 + price: 487 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU761: + cost: 259 + init_stock: 1550 + price: 288 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU762: + cost: 336 + init_stock: 3100 + price: 374 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU763: + cost: 108 + init_stock: 300 + price: 121 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU764: + cost: 444 + init_stock: 1900 + price: 494 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU765: + cost: 413 + init_stock: 4450 + price: 459 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU766: + cost: 224 + init_stock: 2700 + price: 249 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU767: + cost: 220 + init_stock: 4400 + price: 245 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU768: + cost: 386 + init_stock: 4250 + price: 429 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU769: + cost: 291 + init_stock: 2400 + price: 324 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU77: + cost: 252 + init_stock: 1400 + price: 281 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU770: + cost: 325 + init_stock: 3800 + price: 362 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU771: + cost: 209 + init_stock: 1350 + price: 233 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU772: + cost: 136 + init_stock: 800 + price: 152 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU773: + cost: 303 + init_stock: 4800 + price: 337 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU774: + cost: 208 + init_stock: 450 + price: 232 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU775: + cost: 353 + init_stock: 600 + price: 393 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU776: + cost: 307 + init_stock: 2400 + price: 342 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU777: + cost: 235 + init_stock: 2950 + price: 262 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU778: + cost: 294 + init_stock: 3700 + price: 327 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU779: + cost: 98 + init_stock: 2200 + price: 109 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU78: + cost: 437 + init_stock: 1850 + price: 486 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU780: + cost: 137 + init_stock: 1250 + price: 153 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU781: + cost: 113 + init_stock: 350 + price: 126 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU782: + cost: 232 + init_stock: 4600 + price: 258 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU783: + cost: 192 + init_stock: 3500 + price: 214 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU784: + cost: 383 + init_stock: 1750 + price: 426 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU785: + cost: 437 + init_stock: 1450 + price: 486 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU786: + cost: 98 + init_stock: 2600 + price: 109 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU787: + cost: 66 + init_stock: 1050 + price: 74 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU788: + cost: 22 + init_stock: 2100 + price: 25 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU789: + cost: 238 + init_stock: 1950 + price: 265 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU79: + cost: 437 + init_stock: 2100 + price: 486 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU790: + cost: 199 + init_stock: 1400 + price: 222 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU791: + cost: 445 + init_stock: 2700 + price: 495 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU792: + cost: 369 + init_stock: 1600 + price: 410 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU793: + cost: 397 + init_stock: 4000 + price: 442 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU794: + cost: 199 + init_stock: 300 + price: 222 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU795: + cost: 251 + init_stock: 2900 + price: 279 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU796: + cost: 109 + init_stock: 1300 + price: 122 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU797: + cost: 96 + init_stock: 5000 + price: 107 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU798: + cost: 344 + init_stock: 2500 + price: 383 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU799: + cost: 107 + init_stock: 1750 + price: 119 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU8: + cost: 380 + init_stock: 4250 + price: 423 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU80: + cost: 291 + init_stock: 1800 + price: 324 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU800: + cost: 284 + init_stock: 4450 + price: 316 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU801: + cost: 58 + init_stock: 4200 + price: 65 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU802: + cost: 119 + init_stock: 4700 + price: 133 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU803: + cost: 188 + init_stock: 1750 + price: 209 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU804: + cost: 245 + init_stock: 4250 + price: 273 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU805: + cost: 273 + init_stock: 1900 + price: 304 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU806: + cost: 208 + init_stock: 450 + price: 232 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU807: + cost: 152 + init_stock: 4350 + price: 169 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU808: + cost: 315 + init_stock: 450 + price: 350 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU809: + cost: 76 + init_stock: 4850 + price: 85 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU81: + cost: 287 + init_stock: 3000 + price: 319 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU810: + cost: 45 + init_stock: 2550 + price: 51 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU811: + cost: 47 + init_stock: 2700 + price: 53 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU812: + cost: 126 + init_stock: 2850 + price: 141 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU813: + cost: 327 + init_stock: 1950 + price: 364 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU814: + cost: 71 + init_stock: 4800 + price: 79 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU815: + cost: 160 + init_stock: 800 + price: 178 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU816: + cost: 316 + init_stock: 3300 + price: 352 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU817: + cost: 107 + init_stock: 850 + price: 119 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU818: + cost: 99 + init_stock: 650 + price: 110 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU819: + cost: 21 + init_stock: 250 + price: 24 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU82: + cost: 371 + init_stock: 3500 + price: 413 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU820: + cost: 383 + init_stock: 2600 + price: 426 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU821: + cost: 206 + init_stock: 4600 + price: 229 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU822: + cost: 253 + init_stock: 600 + price: 282 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU823: + cost: 198 + init_stock: 4550 + price: 221 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU824: + cost: 286 + init_stock: 1800 + price: 318 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU825: + cost: 258 + init_stock: 4650 + price: 287 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU826: + cost: 250 + init_stock: 2400 + price: 278 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU827: + cost: 280 + init_stock: 3150 + price: 312 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU828: + cost: 126 + init_stock: 4650 + price: 140 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU829: + cost: 373 + init_stock: 550 + price: 415 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU83: + cost: 294 + init_stock: 3850 + price: 327 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU830: + cost: 107 + init_stock: 1150 + price: 119 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU831: + cost: 338 + init_stock: 3500 + price: 376 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU832: + cost: 260 + init_stock: 2300 + price: 289 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU833: + cost: 164 + init_stock: 800 + price: 183 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU834: + cost: 358 + init_stock: 3300 + price: 398 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU835: + cost: 366 + init_stock: 3700 + price: 407 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU836: + cost: 270 + init_stock: 2050 + price: 301 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU837: + cost: 191 + init_stock: 1300 + price: 213 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU838: + cost: 20 + init_stock: 2100 + price: 23 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU839: + cost: 318 + init_stock: 2650 + price: 354 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU84: + cost: 195 + init_stock: 1600 + price: 217 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU840: + cost: 17 + init_stock: 4350 + price: 19 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU841: + cost: 325 + init_stock: 4700 + price: 362 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU842: + cost: 65 + init_stock: 4550 + price: 73 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU843: + cost: 152 + init_stock: 2150 + price: 169 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU844: + cost: 292 + init_stock: 1800 + price: 325 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU845: + cost: 146 + init_stock: 750 + price: 163 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU846: + cost: 165 + init_stock: 4900 + price: 184 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU847: + cost: 396 + init_stock: 1700 + price: 441 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU848: + cost: 405 + init_stock: 1250 + price: 450 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU849: + cost: 431 + init_stock: 1750 + price: 479 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU85: + cost: 44 + init_stock: 3800 + price: 49 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU850: + cost: 94 + init_stock: 3650 + price: 105 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU851: + cost: 329 + init_stock: 3850 + price: 366 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU852: + cost: 352 + init_stock: 3900 + price: 392 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU853: + cost: 84 + init_stock: 500 + price: 94 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU854: + cost: 99 + init_stock: 3250 + price: 110 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU855: + cost: 228 + init_stock: 2050 + price: 254 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU856: + cost: 391 + init_stock: 1450 + price: 435 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU857: + cost: 90 + init_stock: 1200 + price: 101 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU858: + cost: 438 + init_stock: 1400 + price: 487 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU859: + cost: 446 + init_stock: 4650 + price: 496 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU86: + cost: 53 + init_stock: 3750 + price: 59 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU860: + cost: 315 + init_stock: 5000 + price: 350 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU861: + cost: 265 + init_stock: 1250 + price: 295 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU862: + cost: 51 + init_stock: 3650 + price: 57 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU863: + cost: 279 + init_stock: 2850 + price: 311 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU864: + cost: 86 + init_stock: 3200 + price: 96 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU865: + cost: 96 + init_stock: 2900 + price: 107 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU866: + cost: 357 + init_stock: 1050 + price: 397 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU867: + cost: 430 + init_stock: 3200 + price: 478 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU868: + cost: 308 + init_stock: 1050 + price: 343 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU869: + cost: 151 + init_stock: 3050 + price: 168 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU87: + cost: 83 + init_stock: 3550 + price: 93 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU870: + cost: 231 + init_stock: 3150 + price: 257 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU871: + cost: 61 + init_stock: 1000 + price: 68 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU872: + cost: 427 + init_stock: 3550 + price: 475 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU873: + cost: 349 + init_stock: 3250 + price: 388 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU874: + cost: 240 + init_stock: 300 + price: 267 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU875: + cost: 298 + init_stock: 1750 + price: 332 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU876: + cost: 352 + init_stock: 2000 + price: 392 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU877: + cost: 336 + init_stock: 4550 + price: 374 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU878: + cost: 267 + init_stock: 1450 + price: 297 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU879: + cost: 447 + init_stock: 3900 + price: 497 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU88: + cost: 17 + init_stock: 2100 + price: 19 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU880: + cost: 179 + init_stock: 2350 + price: 199 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU881: + cost: 295 + init_stock: 3000 + price: 328 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU882: + cost: 276 + init_stock: 4500 + price: 307 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU883: + cost: 14 + init_stock: 1400 + price: 16 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU884: + cost: 296 + init_stock: 3350 + price: 329 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU885: + cost: 158 + init_stock: 3550 + price: 176 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU886: + cost: 43 + init_stock: 4450 + price: 48 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU887: + cost: 216 + init_stock: 750 + price: 241 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU888: + cost: 102 + init_stock: 2650 + price: 114 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU889: + cost: 234 + init_stock: 4750 + price: 261 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU89: + cost: 120 + init_stock: 4050 + price: 134 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU890: + cost: 289 + init_stock: 3550 + price: 322 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU891: + cost: 312 + init_stock: 2850 + price: 347 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU892: + cost: 426 + init_stock: 4200 + price: 474 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU893: + cost: 389 + init_stock: 4900 + price: 433 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU894: + cost: 359 + init_stock: 1300 + price: 399 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU895: + cost: 301 + init_stock: 2400 + price: 335 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU896: + cost: 82 + init_stock: 2750 + price: 92 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU897: + cost: 363 + init_stock: 1100 + price: 404 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU898: + cost: 64 + init_stock: 4900 + price: 72 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU899: + cost: 81 + init_stock: 4450 + price: 91 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU9: + cost: 207 + init_stock: 1050 + price: 231 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU90: + cost: 34 + init_stock: 1500 + price: 38 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU900: + cost: 10 + init_stock: 2950 + price: 12 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU901: + cost: 276 + init_stock: 3900 + price: 307 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU902: + cost: 318 + init_stock: 2800 + price: 354 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU903: + cost: 12 + init_stock: 4650 + price: 14 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU904: + cost: 270 + init_stock: 4850 + price: 300 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU905: + cost: 120 + init_stock: 2550 + price: 134 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU906: + cost: 381 + init_stock: 4400 + price: 424 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU907: + cost: 324 + init_stock: 1600 + price: 360 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU908: + cost: 324 + init_stock: 1950 + price: 361 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU909: + cost: 58 + init_stock: 3450 + price: 65 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU91: + cost: 200 + init_stock: 2150 + price: 223 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU910: + cost: 144 + init_stock: 500 + price: 161 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU911: + cost: 167 + init_stock: 2500 + price: 186 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU912: + cost: 211 + init_stock: 2700 + price: 235 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU913: + cost: 109 + init_stock: 3400 + price: 122 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU914: + cost: 392 + init_stock: 800 + price: 436 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU915: + cost: 209 + init_stock: 1150 + price: 233 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU916: + cost: 23 + init_stock: 2450 + price: 26 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU917: + cost: 126 + init_stock: 4900 + price: 141 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU918: + cost: 101 + init_stock: 4450 + price: 113 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU919: + cost: 61 + init_stock: 4350 + price: 68 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU92: + cost: 292 + init_stock: 1900 + price: 325 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU920: + cost: 385 + init_stock: 1050 + price: 428 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU921: + cost: 441 + init_stock: 3800 + price: 491 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU922: + cost: 9 + init_stock: 2550 + price: 10 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU923: + cost: 72 + init_stock: 500 + price: 80 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU924: + cost: 147 + init_stock: 3800 + price: 164 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU925: + cost: 198 + init_stock: 3050 + price: 221 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU926: + cost: 270 + init_stock: 350 + price: 300 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU927: + cost: 115 + init_stock: 3150 + price: 128 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU928: + cost: 113 + init_stock: 400 + price: 126 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU929: + cost: 339 + init_stock: 750 + price: 377 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU93: + cost: 396 + init_stock: 450 + price: 441 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU930: + cost: 243 + init_stock: 4950 + price: 270 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU931: + cost: 370 + init_stock: 3250 + price: 412 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU932: + cost: 106 + init_stock: 2400 + price: 118 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU933: + cost: 48 + init_stock: 4550 + price: 54 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU934: + cost: 312 + init_stock: 1950 + price: 347 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU935: + cost: 197 + init_stock: 850 + price: 219 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU936: + cost: 254 + init_stock: 3850 + price: 283 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU937: + cost: 411 + init_stock: 3650 + price: 457 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU938: + cost: 405 + init_stock: 4500 + price: 450 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU939: + cost: 60 + init_stock: 1350 + price: 67 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU94: + cost: 251 + init_stock: 850 + price: 279 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU940: + cost: 58 + init_stock: 1450 + price: 65 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU941: + cost: 191 + init_stock: 250 + price: 213 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU942: + cost: 135 + init_stock: 1350 + price: 151 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU943: + cost: 76 + init_stock: 2500 + price: 85 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU944: + cost: 138 + init_stock: 300 + price: 154 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU945: + cost: 240 + init_stock: 750 + price: 267 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU946: + cost: 357 + init_stock: 450 + price: 397 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU947: + cost: 247 + init_stock: 2050 + price: 275 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU948: + cost: 424 + init_stock: 4400 + price: 472 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU949: + cost: 247 + init_stock: 4900 + price: 275 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU95: + cost: 164 + init_stock: 2550 + price: 183 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU950: + cost: 336 + init_stock: 1250 + price: 374 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU951: + cost: 244 + init_stock: 1600 + price: 272 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU952: + cost: 174 + init_stock: 3150 + price: 194 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU953: + cost: 385 + init_stock: 500 + price: 428 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU954: + cost: 244 + init_stock: 800 + price: 272 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU955: + cost: 75 + init_stock: 4600 + price: 84 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU956: + cost: 326 + init_stock: 1750 + price: 363 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU957: + cost: 198 + init_stock: 4500 + price: 221 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU958: + cost: 301 + init_stock: 1800 + price: 335 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU959: + cost: 384 + init_stock: 3550 + price: 427 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU96: + cost: 288 + init_stock: 550 + price: 320 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU960: + cost: 168 + init_stock: 3850 + price: 187 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU961: + cost: 81 + init_stock: 3300 + price: 90 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU962: + cost: 63 + init_stock: 4900 + price: 70 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU963: + cost: 165 + init_stock: 1850 + price: 184 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU964: + cost: 162 + init_stock: 500 + price: 180 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU965: + cost: 396 + init_stock: 800 + price: 441 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU966: + cost: 192 + init_stock: 1600 + price: 214 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU967: + cost: 63 + init_stock: 3100 + price: 71 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU968: + cost: 127 + init_stock: 250 + price: 142 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU969: + cost: 391 + init_stock: 2900 + price: 435 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU97: + cost: 13 + init_stock: 1350 + price: 15 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU970: + cost: 352 + init_stock: 4600 + price: 392 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU971: + cost: 367 + init_stock: 650 + price: 408 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU972: + cost: 19 + init_stock: 1000 + price: 22 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU973: + cost: 428 + init_stock: 4450 + price: 476 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU974: + cost: 320 + init_stock: 2500 + price: 356 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU975: + cost: 269 + init_stock: 850 + price: 299 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU976: + cost: 332 + init_stock: 1750 + price: 369 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU977: + cost: 127 + init_stock: 2650 + price: 142 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU978: + cost: 217 + init_stock: 4900 + price: 242 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU979: + cost: 197 + init_stock: 4950 + price: 219 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU98: + cost: 150 + init_stock: 3900 + price: 167 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU980: + cost: 326 + init_stock: 4100 + price: 363 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU981: + cost: 374 + init_stock: 4200 + price: 416 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU982: + cost: 146 + init_stock: 1150 + price: 163 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU983: + cost: 135 + init_stock: 3000 + price: 151 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU984: + cost: 110 + init_stock: 4200 + price: 123 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU985: + cost: 102 + init_stock: 2050 + price: 114 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU986: + cost: 96 + init_stock: 3800 + price: 107 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU987: + cost: 50 + init_stock: 4650 + price: 56 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU988: + cost: 59 + init_stock: 1950 + price: 66 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU989: + cost: 424 + init_stock: 3350 + price: 472 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU99: + cost: 27 + init_stock: 4450 + price: 31 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU990: + cost: 12 + init_stock: 2350 + price: 14 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU991: + cost: 405 + init_stock: 2450 + price: 450 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU992: + cost: 16 + init_stock: 3050 + price: 18 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU993: + cost: 315 + init_stock: 2350 + price: 350 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU994: + cost: 40 + init_stock: 4550 + price: 45 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU995: + cost: 365 + init_stock: 4300 + price: 406 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU996: + cost: 424 + init_stock: 2400 + price: 472 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU997: + cost: 313 + init_stock: 1350 + price: 348 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU998: + cost: 215 + init_stock: 750 + price: 239 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU999: + cost: 391 + init_stock: 5000 + price: 435 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5139100 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 500 + definition_ref: WarehouseFacility + name: WAREHOUSE0 + skus: + SKU0: + cost: 405 + init_stock: 1700 + price: 405 + service_level: 0.96 + vlt: 2 + SKU1: + cost: 42 + init_stock: 840 + price: 42 + service_level: 0.96 + vlt: 1 + SKU10: + cost: 125 + init_stock: 1820 + price: 125 + service_level: 0.96 + vlt: 1 + SKU100: + cost: 338 + init_stock: 1840 + price: 338 + service_level: 0.96 + vlt: 3 + SKU101: + cost: 436 + init_stock: 1380 + price: 436 + service_level: 0.96 + vlt: 1 + SKU102: + cost: 90 + init_stock: 1340 + price: 90 + service_level: 0.96 + vlt: 2 + SKU103: + cost: 161 + init_stock: 1620 + price: 161 + service_level: 0.96 + vlt: 2 + SKU104: + cost: 470 + init_stock: 800 + price: 470 + service_level: 0.96 + vlt: 2 + SKU105: + cost: 214 + init_stock: 1080 + price: 214 + service_level: 0.96 + vlt: 2 + SKU106: + cost: 37 + init_stock: 1020 + price: 37 + service_level: 0.96 + vlt: 1 + SKU107: + cost: 64 + init_stock: 1220 + price: 64 + service_level: 0.96 + vlt: 1 + SKU108: + cost: 474 + init_stock: 600 + price: 474 + service_level: 0.96 + vlt: 3 + SKU109: + cost: 398 + init_stock: 1020 + price: 398 + service_level: 0.96 + vlt: 1 + SKU11: + cost: 106 + init_stock: 1760 + price: 106 + service_level: 0.96 + vlt: 3 + SKU110: + cost: 71 + init_stock: 500 + price: 71 + service_level: 0.96 + vlt: 3 + SKU111: + cost: 183 + init_stock: 1000 + price: 183 + service_level: 0.96 + vlt: 3 + SKU112: + cost: 78 + init_stock: 220 + price: 78 + service_level: 0.96 + vlt: 1 + SKU113: + cost: 289 + init_stock: 1060 + price: 289 + service_level: 0.96 + vlt: 2 + SKU114: + cost: 333 + init_stock: 1300 + price: 333 + service_level: 0.96 + vlt: 3 + SKU115: + cost: 437 + init_stock: 500 + price: 437 + service_level: 0.96 + vlt: 2 + SKU116: + cost: 256 + init_stock: 1360 + price: 256 + service_level: 0.96 + vlt: 1 + SKU117: + cost: 123 + init_stock: 440 + price: 123 + service_level: 0.96 + vlt: 2 + SKU118: + cost: 181 + init_stock: 980 + price: 181 + service_level: 0.96 + vlt: 3 + SKU119: + cost: 58 + init_stock: 100 + price: 58 + service_level: 0.96 + vlt: 2 + SKU12: + cost: 315 + init_stock: 1520 + price: 315 + service_level: 0.96 + vlt: 3 + SKU120: + cost: 31 + init_stock: 240 + price: 31 + service_level: 0.96 + vlt: 3 + SKU121: + cost: 255 + init_stock: 1340 + price: 255 + service_level: 0.96 + vlt: 2 + SKU122: + cost: 222 + init_stock: 740 + price: 222 + service_level: 0.96 + vlt: 1 + SKU123: + cost: 405 + init_stock: 1240 + price: 405 + service_level: 0.96 + vlt: 2 + SKU124: + cost: 169 + init_stock: 360 + price: 169 + service_level: 0.96 + vlt: 1 + SKU125: + cost: 344 + init_stock: 700 + price: 344 + service_level: 0.96 + vlt: 3 + SKU126: + cost: 362 + init_stock: 1880 + price: 362 + service_level: 0.96 + vlt: 1 + SKU127: + cost: 149 + init_stock: 620 + price: 149 + service_level: 0.96 + vlt: 3 + SKU128: + cost: 215 + init_stock: 1740 + price: 215 + service_level: 0.96 + vlt: 2 + SKU129: + cost: 83 + init_stock: 1060 + price: 83 + service_level: 0.96 + vlt: 2 + SKU13: + cost: 474 + init_stock: 900 + price: 474 + service_level: 0.96 + vlt: 2 + SKU130: + cost: 427 + init_stock: 1780 + price: 427 + service_level: 0.96 + vlt: 1 + SKU131: + cost: 58 + init_stock: 980 + price: 58 + service_level: 0.96 + vlt: 1 + SKU132: + cost: 304 + init_stock: 300 + price: 304 + service_level: 0.96 + vlt: 2 + SKU133: + cost: 161 + init_stock: 580 + price: 161 + service_level: 0.96 + vlt: 2 + SKU134: + cost: 382 + init_stock: 1480 + price: 382 + service_level: 0.96 + vlt: 3 + SKU135: + cost: 126 + init_stock: 1760 + price: 126 + service_level: 0.96 + vlt: 3 + SKU136: + cost: 431 + init_stock: 140 + price: 431 + service_level: 0.96 + vlt: 2 + SKU137: + cost: 270 + init_stock: 440 + price: 270 + service_level: 0.96 + vlt: 3 + SKU138: + cost: 10 + init_stock: 1260 + price: 10 + service_level: 0.96 + vlt: 2 + SKU139: + cost: 259 + init_stock: 1640 + price: 259 + service_level: 0.96 + vlt: 3 + SKU14: + cost: 182 + init_stock: 1820 + price: 182 + service_level: 0.96 + vlt: 3 + SKU140: + cost: 367 + init_stock: 1100 + price: 367 + service_level: 0.96 + vlt: 2 + SKU141: + cost: 325 + init_stock: 200 + price: 325 + service_level: 0.96 + vlt: 3 + SKU142: + cost: 439 + init_stock: 1620 + price: 439 + service_level: 0.96 + vlt: 2 + SKU143: + cost: 260 + init_stock: 1380 + price: 260 + service_level: 0.96 + vlt: 2 + SKU144: + cost: 457 + init_stock: 120 + price: 457 + service_level: 0.96 + vlt: 3 + SKU145: + cost: 480 + init_stock: 1500 + price: 480 + service_level: 0.96 + vlt: 1 + SKU146: + cost: 243 + init_stock: 1920 + price: 243 + service_level: 0.96 + vlt: 1 + SKU147: + cost: 334 + init_stock: 1480 + price: 334 + service_level: 0.96 + vlt: 3 + SKU148: + cost: 143 + init_stock: 800 + price: 143 + service_level: 0.96 + vlt: 1 + SKU149: + cost: 252 + init_stock: 1020 + price: 252 + service_level: 0.96 + vlt: 2 + SKU15: + cost: 320 + init_stock: 720 + price: 320 + service_level: 0.96 + vlt: 2 + SKU150: + cost: 83 + init_stock: 1240 + price: 83 + service_level: 0.96 + vlt: 3 + SKU151: + cost: 58 + init_stock: 500 + price: 58 + service_level: 0.96 + vlt: 3 + SKU152: + cost: 254 + init_stock: 700 + price: 254 + service_level: 0.96 + vlt: 1 + SKU153: + cost: 49 + init_stock: 1440 + price: 49 + service_level: 0.96 + vlt: 2 + SKU154: + cost: 391 + init_stock: 200 + price: 391 + service_level: 0.96 + vlt: 2 + SKU155: + cost: 36 + init_stock: 460 + price: 36 + service_level: 0.96 + vlt: 3 + SKU156: + cost: 140 + init_stock: 740 + price: 140 + service_level: 0.96 + vlt: 3 + SKU157: + cost: 374 + init_stock: 780 + price: 374 + service_level: 0.96 + vlt: 1 + SKU158: + cost: 182 + init_stock: 540 + price: 182 + service_level: 0.96 + vlt: 1 + SKU159: + cost: 428 + init_stock: 800 + price: 428 + service_level: 0.96 + vlt: 2 + SKU16: + cost: 277 + init_stock: 1420 + price: 277 + service_level: 0.96 + vlt: 2 + SKU160: + cost: 129 + init_stock: 1560 + price: 129 + service_level: 0.96 + vlt: 2 + SKU161: + cost: 32 + init_stock: 1920 + price: 32 + service_level: 0.96 + vlt: 2 + SKU162: + cost: 68 + init_stock: 1220 + price: 68 + service_level: 0.96 + vlt: 2 + SKU163: + cost: 129 + init_stock: 960 + price: 129 + service_level: 0.96 + vlt: 2 + SKU164: + cost: 192 + init_stock: 1360 + price: 192 + service_level: 0.96 + vlt: 1 + SKU165: + cost: 316 + init_stock: 1240 + price: 316 + service_level: 0.96 + vlt: 1 + SKU166: + cost: 454 + init_stock: 140 + price: 454 + service_level: 0.96 + vlt: 1 + SKU167: + cost: 108 + init_stock: 1420 + price: 108 + service_level: 0.96 + vlt: 2 + SKU168: + cost: 431 + init_stock: 600 + price: 431 + service_level: 0.96 + vlt: 2 + SKU169: + cost: 428 + init_stock: 1440 + price: 428 + service_level: 0.96 + vlt: 3 + SKU17: + cost: 393 + init_stock: 800 + price: 393 + service_level: 0.96 + vlt: 1 + SKU170: + cost: 107 + init_stock: 280 + price: 107 + service_level: 0.96 + vlt: 3 + SKU171: + cost: 14 + init_stock: 1900 + price: 14 + service_level: 0.96 + vlt: 3 + SKU172: + cost: 291 + init_stock: 1720 + price: 291 + service_level: 0.96 + vlt: 1 + SKU173: + cost: 190 + init_stock: 300 + price: 190 + service_level: 0.96 + vlt: 2 + SKU174: + cost: 431 + init_stock: 520 + price: 431 + service_level: 0.96 + vlt: 3 + SKU175: + cost: 355 + init_stock: 1740 + price: 355 + service_level: 0.96 + vlt: 1 + SKU176: + cost: 305 + init_stock: 980 + price: 305 + service_level: 0.96 + vlt: 3 + SKU177: + cost: 291 + init_stock: 460 + price: 291 + service_level: 0.96 + vlt: 2 + SKU178: + cost: 417 + init_stock: 400 + price: 417 + service_level: 0.96 + vlt: 2 + SKU179: + cost: 18 + init_stock: 1220 + price: 18 + service_level: 0.96 + vlt: 2 + SKU18: + cost: 223 + init_stock: 1040 + price: 223 + service_level: 0.96 + vlt: 1 + SKU180: + cost: 373 + init_stock: 740 + price: 373 + service_level: 0.96 + vlt: 2 + SKU181: + cost: 486 + init_stock: 1160 + price: 486 + service_level: 0.96 + vlt: 3 + SKU182: + cost: 385 + init_stock: 680 + price: 385 + service_level: 0.96 + vlt: 3 + SKU183: + cost: 195 + init_stock: 1600 + price: 195 + service_level: 0.96 + vlt: 2 + SKU184: + cost: 238 + init_stock: 100 + price: 238 + service_level: 0.96 + vlt: 3 + SKU185: + cost: 50 + init_stock: 900 + price: 50 + service_level: 0.96 + vlt: 2 + SKU186: + cost: 143 + init_stock: 1880 + price: 143 + service_level: 0.96 + vlt: 3 + SKU187: + cost: 285 + init_stock: 760 + price: 285 + service_level: 0.96 + vlt: 3 + SKU188: + cost: 180 + init_stock: 660 + price: 180 + service_level: 0.96 + vlt: 2 + SKU189: + cost: 294 + init_stock: 360 + price: 294 + service_level: 0.96 + vlt: 2 + SKU19: + cost: 301 + init_stock: 560 + price: 301 + service_level: 0.96 + vlt: 2 + SKU190: + cost: 392 + init_stock: 740 + price: 392 + service_level: 0.96 + vlt: 1 + SKU191: + cost: 124 + init_stock: 340 + price: 124 + service_level: 0.96 + vlt: 2 + SKU192: + cost: 118 + init_stock: 740 + price: 118 + service_level: 0.96 + vlt: 3 + SKU193: + cost: 168 + init_stock: 1640 + price: 168 + service_level: 0.96 + vlt: 1 + SKU194: + cost: 331 + init_stock: 720 + price: 331 + service_level: 0.96 + vlt: 2 + SKU195: + cost: 81 + init_stock: 660 + price: 81 + service_level: 0.96 + vlt: 3 + SKU196: + cost: 94 + init_stock: 240 + price: 94 + service_level: 0.96 + vlt: 3 + SKU197: + cost: 165 + init_stock: 840 + price: 165 + service_level: 0.96 + vlt: 2 + SKU198: + cost: 454 + init_stock: 1560 + price: 454 + service_level: 0.96 + vlt: 3 + SKU199: + cost: 182 + init_stock: 740 + price: 182 + service_level: 0.96 + vlt: 3 + SKU2: + cost: 60 + init_stock: 1140 + price: 60 + service_level: 0.96 + vlt: 1 + SKU20: + cost: 124 + init_stock: 1680 + price: 124 + service_level: 0.96 + vlt: 1 + SKU200: + cost: 355 + init_stock: 1180 + price: 355 + service_level: 0.96 + vlt: 3 + SKU201: + cost: 263 + init_stock: 920 + price: 263 + service_level: 0.96 + vlt: 2 + SKU202: + cost: 316 + init_stock: 420 + price: 316 + service_level: 0.96 + vlt: 1 + SKU203: + cost: 145 + init_stock: 120 + price: 145 + service_level: 0.96 + vlt: 2 + SKU204: + cost: 28 + init_stock: 600 + price: 28 + service_level: 0.96 + vlt: 3 + SKU205: + cost: 190 + init_stock: 1900 + price: 190 + service_level: 0.96 + vlt: 1 + SKU206: + cost: 333 + init_stock: 1240 + price: 333 + service_level: 0.96 + vlt: 3 + SKU207: + cost: 322 + init_stock: 1380 + price: 322 + service_level: 0.96 + vlt: 2 + SKU208: + cost: 287 + init_stock: 320 + price: 287 + service_level: 0.96 + vlt: 2 + SKU209: + cost: 278 + init_stock: 340 + price: 278 + service_level: 0.96 + vlt: 2 + SKU21: + cost: 408 + init_stock: 360 + price: 408 + service_level: 0.96 + vlt: 2 + SKU210: + cost: 322 + init_stock: 820 + price: 322 + service_level: 0.96 + vlt: 3 + SKU211: + cost: 456 + init_stock: 1040 + price: 456 + service_level: 0.96 + vlt: 1 + SKU212: + cost: 248 + init_stock: 340 + price: 248 + service_level: 0.96 + vlt: 3 + SKU213: + cost: 287 + init_stock: 100 + price: 287 + service_level: 0.96 + vlt: 3 + SKU214: + cost: 89 + init_stock: 1260 + price: 89 + service_level: 0.96 + vlt: 2 + SKU215: + cost: 177 + init_stock: 1920 + price: 177 + service_level: 0.96 + vlt: 3 + SKU216: + cost: 232 + init_stock: 1780 + price: 232 + service_level: 0.96 + vlt: 3 + SKU217: + cost: 32 + init_stock: 1700 + price: 32 + service_level: 0.96 + vlt: 2 + SKU218: + cost: 52 + init_stock: 360 + price: 52 + service_level: 0.96 + vlt: 2 + SKU219: + cost: 497 + init_stock: 920 + price: 497 + service_level: 0.96 + vlt: 1 + SKU22: + cost: 497 + init_stock: 600 + price: 497 + service_level: 0.96 + vlt: 2 + SKU220: + cost: 436 + init_stock: 880 + price: 436 + service_level: 0.96 + vlt: 2 + SKU221: + cost: 58 + init_stock: 1480 + price: 58 + service_level: 0.96 + vlt: 3 + SKU222: + cost: 174 + init_stock: 280 + price: 174 + service_level: 0.96 + vlt: 3 + SKU223: + cost: 140 + init_stock: 920 + price: 140 + service_level: 0.96 + vlt: 3 + SKU224: + cost: 246 + init_stock: 540 + price: 246 + service_level: 0.96 + vlt: 2 + SKU225: + cost: 17 + init_stock: 1060 + price: 17 + service_level: 0.96 + vlt: 1 + SKU226: + cost: 81 + init_stock: 1120 + price: 81 + service_level: 0.96 + vlt: 3 + SKU227: + cost: 475 + init_stock: 600 + price: 475 + service_level: 0.96 + vlt: 1 + SKU228: + cost: 296 + init_stock: 1300 + price: 296 + service_level: 0.96 + vlt: 3 + SKU229: + cost: 465 + init_stock: 980 + price: 465 + service_level: 0.96 + vlt: 2 + SKU23: + cost: 301 + init_stock: 1360 + price: 301 + service_level: 0.96 + vlt: 1 + SKU230: + cost: 483 + init_stock: 700 + price: 483 + service_level: 0.96 + vlt: 2 + SKU231: + cost: 173 + init_stock: 260 + price: 173 + service_level: 0.96 + vlt: 3 + SKU232: + cost: 315 + init_stock: 440 + price: 315 + service_level: 0.96 + vlt: 2 + SKU233: + cost: 57 + init_stock: 100 + price: 57 + service_level: 0.96 + vlt: 1 + SKU234: + cost: 315 + init_stock: 1380 + price: 315 + service_level: 0.96 + vlt: 2 + SKU235: + cost: 92 + init_stock: 160 + price: 92 + service_level: 0.96 + vlt: 2 + SKU236: + cost: 125 + init_stock: 360 + price: 125 + service_level: 0.96 + vlt: 1 + SKU237: + cost: 201 + init_stock: 420 + price: 201 + service_level: 0.96 + vlt: 1 + SKU238: + cost: 105 + init_stock: 760 + price: 105 + service_level: 0.96 + vlt: 2 + SKU239: + cost: 66 + init_stock: 1720 + price: 66 + service_level: 0.96 + vlt: 1 + SKU24: + cost: 99 + init_stock: 1400 + price: 99 + service_level: 0.96 + vlt: 3 + SKU240: + cost: 262 + init_stock: 1220 + price: 262 + service_level: 0.96 + vlt: 1 + SKU241: + cost: 361 + init_stock: 1280 + price: 361 + service_level: 0.96 + vlt: 3 + SKU242: + cost: 221 + init_stock: 1760 + price: 221 + service_level: 0.96 + vlt: 1 + SKU243: + cost: 141 + init_stock: 1240 + price: 141 + service_level: 0.96 + vlt: 2 + SKU244: + cost: 193 + init_stock: 1020 + price: 193 + service_level: 0.96 + vlt: 2 + SKU245: + cost: 165 + init_stock: 1100 + price: 165 + service_level: 0.96 + vlt: 2 + SKU246: + cost: 474 + init_stock: 320 + price: 474 + service_level: 0.96 + vlt: 1 + SKU247: + cost: 66 + init_stock: 1800 + price: 66 + service_level: 0.96 + vlt: 1 + SKU248: + cost: 136 + init_stock: 260 + price: 136 + service_level: 0.96 + vlt: 3 + SKU249: + cost: 83 + init_stock: 1200 + price: 83 + service_level: 0.96 + vlt: 1 + SKU25: + cost: 11 + init_stock: 1100 + price: 11 + service_level: 0.96 + vlt: 2 + SKU250: + cost: 310 + init_stock: 1700 + price: 310 + service_level: 0.96 + vlt: 2 + SKU251: + cost: 247 + init_stock: 880 + price: 247 + service_level: 0.96 + vlt: 3 + SKU252: + cost: 192 + init_stock: 960 + price: 192 + service_level: 0.96 + vlt: 2 + SKU253: + cost: 270 + init_stock: 1200 + price: 270 + service_level: 0.96 + vlt: 3 + SKU254: + cost: 165 + init_stock: 1980 + price: 165 + service_level: 0.96 + vlt: 3 + SKU255: + cost: 49 + init_stock: 1080 + price: 49 + service_level: 0.96 + vlt: 3 + SKU256: + cost: 314 + init_stock: 1720 + price: 314 + service_level: 0.96 + vlt: 2 + SKU257: + cost: 468 + init_stock: 480 + price: 468 + service_level: 0.96 + vlt: 3 + SKU258: + cost: 151 + init_stock: 200 + price: 151 + service_level: 0.96 + vlt: 2 + SKU259: + cost: 389 + init_stock: 380 + price: 389 + service_level: 0.96 + vlt: 3 + SKU26: + cost: 348 + init_stock: 740 + price: 348 + service_level: 0.96 + vlt: 3 + SKU260: + cost: 90 + init_stock: 1700 + price: 90 + service_level: 0.96 + vlt: 1 + SKU261: + cost: 122 + init_stock: 1500 + price: 122 + service_level: 0.96 + vlt: 3 + SKU262: + cost: 15 + init_stock: 360 + price: 15 + service_level: 0.96 + vlt: 2 + SKU263: + cost: 350 + init_stock: 780 + price: 350 + service_level: 0.96 + vlt: 1 + SKU264: + cost: 10 + init_stock: 620 + price: 10 + service_level: 0.96 + vlt: 3 + SKU265: + cost: 81 + init_stock: 380 + price: 81 + service_level: 0.96 + vlt: 3 + SKU266: + cost: 276 + init_stock: 1900 + price: 276 + service_level: 0.96 + vlt: 1 + SKU267: + cost: 123 + init_stock: 840 + price: 123 + service_level: 0.96 + vlt: 1 + SKU268: + cost: 64 + init_stock: 680 + price: 64 + service_level: 0.96 + vlt: 3 + SKU269: + cost: 435 + init_stock: 200 + price: 435 + service_level: 0.96 + vlt: 3 + SKU27: + cost: 196 + init_stock: 1140 + price: 196 + service_level: 0.96 + vlt: 1 + SKU270: + cost: 68 + init_stock: 1160 + price: 68 + service_level: 0.96 + vlt: 3 + SKU271: + cost: 14 + init_stock: 160 + price: 14 + service_level: 0.96 + vlt: 1 + SKU272: + cost: 249 + init_stock: 1660 + price: 249 + service_level: 0.96 + vlt: 1 + SKU273: + cost: 39 + init_stock: 400 + price: 39 + service_level: 0.96 + vlt: 3 + SKU274: + cost: 436 + init_stock: 180 + price: 436 + service_level: 0.96 + vlt: 2 + SKU275: + cost: 62 + init_stock: 1860 + price: 62 + service_level: 0.96 + vlt: 1 + SKU276: + cost: 281 + init_stock: 1500 + price: 281 + service_level: 0.96 + vlt: 1 + SKU277: + cost: 442 + init_stock: 1820 + price: 442 + service_level: 0.96 + vlt: 2 + SKU278: + cost: 290 + init_stock: 1680 + price: 290 + service_level: 0.96 + vlt: 2 + SKU279: + cost: 175 + init_stock: 1180 + price: 175 + service_level: 0.96 + vlt: 2 + SKU28: + cost: 61 + init_stock: 720 + price: 61 + service_level: 0.96 + vlt: 1 + SKU280: + cost: 445 + init_stock: 1640 + price: 445 + service_level: 0.96 + vlt: 3 + SKU281: + cost: 49 + init_stock: 140 + price: 49 + service_level: 0.96 + vlt: 3 + SKU282: + cost: 499 + init_stock: 620 + price: 499 + service_level: 0.96 + vlt: 3 + SKU283: + cost: 310 + init_stock: 840 + price: 310 + service_level: 0.96 + vlt: 1 + SKU284: + cost: 381 + init_stock: 840 + price: 381 + service_level: 0.96 + vlt: 1 + SKU285: + cost: 378 + init_stock: 700 + price: 378 + service_level: 0.96 + vlt: 2 + SKU286: + cost: 366 + init_stock: 1780 + price: 366 + service_level: 0.96 + vlt: 1 + SKU287: + cost: 287 + init_stock: 1340 + price: 287 + service_level: 0.96 + vlt: 1 + SKU288: + cost: 24 + init_stock: 240 + price: 24 + service_level: 0.96 + vlt: 1 + SKU289: + cost: 439 + init_stock: 1980 + price: 439 + service_level: 0.96 + vlt: 2 + SKU29: + cost: 490 + init_stock: 1440 + price: 490 + service_level: 0.96 + vlt: 2 + SKU290: + cost: 11 + init_stock: 1540 + price: 11 + service_level: 0.96 + vlt: 3 + SKU291: + cost: 259 + init_stock: 360 + price: 259 + service_level: 0.96 + vlt: 1 + SKU292: + cost: 389 + init_stock: 2000 + price: 389 + service_level: 0.96 + vlt: 3 + SKU293: + cost: 246 + init_stock: 1120 + price: 246 + service_level: 0.96 + vlt: 1 + SKU294: + cost: 76 + init_stock: 260 + price: 76 + service_level: 0.96 + vlt: 3 + SKU295: + cost: 22 + init_stock: 1720 + price: 22 + service_level: 0.96 + vlt: 3 + SKU296: + cost: 389 + init_stock: 1900 + price: 389 + service_level: 0.96 + vlt: 2 + SKU297: + cost: 51 + init_stock: 1280 + price: 51 + service_level: 0.96 + vlt: 3 + SKU298: + cost: 269 + init_stock: 1780 + price: 269 + service_level: 0.96 + vlt: 2 + SKU299: + cost: 14 + init_stock: 1780 + price: 14 + service_level: 0.96 + vlt: 1 + SKU3: + cost: 470 + init_stock: 1760 + price: 470 + service_level: 0.96 + vlt: 1 + SKU30: + cost: 218 + init_stock: 900 + price: 218 + service_level: 0.96 + vlt: 1 + SKU300: + cost: 94 + init_stock: 440 + price: 94 + service_level: 0.96 + vlt: 3 + SKU301: + cost: 417 + init_stock: 640 + price: 417 + service_level: 0.96 + vlt: 2 + SKU302: + cost: 161 + init_stock: 1840 + price: 161 + service_level: 0.96 + vlt: 1 + SKU303: + cost: 215 + init_stock: 1580 + price: 215 + service_level: 0.96 + vlt: 2 + SKU304: + cost: 145 + init_stock: 1160 + price: 145 + service_level: 0.96 + vlt: 3 + SKU305: + cost: 441 + init_stock: 160 + price: 441 + service_level: 0.96 + vlt: 2 + SKU306: + cost: 284 + init_stock: 1380 + price: 284 + service_level: 0.96 + vlt: 1 + SKU307: + cost: 412 + init_stock: 1120 + price: 412 + service_level: 0.96 + vlt: 2 + SKU308: + cost: 409 + init_stock: 1600 + price: 409 + service_level: 0.96 + vlt: 2 + SKU309: + cost: 260 + init_stock: 1020 + price: 260 + service_level: 0.96 + vlt: 2 + SKU31: + cost: 140 + init_stock: 1760 + price: 140 + service_level: 0.96 + vlt: 3 + SKU310: + cost: 367 + init_stock: 560 + price: 367 + service_level: 0.96 + vlt: 2 + SKU311: + cost: 143 + init_stock: 1820 + price: 143 + service_level: 0.96 + vlt: 3 + SKU312: + cost: 264 + init_stock: 360 + price: 264 + service_level: 0.96 + vlt: 3 + SKU313: + cost: 189 + init_stock: 1900 + price: 189 + service_level: 0.96 + vlt: 3 + SKU314: + cost: 318 + init_stock: 300 + price: 318 + service_level: 0.96 + vlt: 3 + SKU315: + cost: 31 + init_stock: 1140 + price: 31 + service_level: 0.96 + vlt: 3 + SKU316: + cost: 215 + init_stock: 1720 + price: 215 + service_level: 0.96 + vlt: 1 + SKU317: + cost: 72 + init_stock: 620 + price: 72 + service_level: 0.96 + vlt: 2 + SKU318: + cost: 485 + init_stock: 1800 + price: 485 + service_level: 0.96 + vlt: 1 + SKU319: + cost: 218 + init_stock: 480 + price: 218 + service_level: 0.96 + vlt: 1 + SKU32: + cost: 380 + init_stock: 640 + price: 380 + service_level: 0.96 + vlt: 2 + SKU320: + cost: 445 + init_stock: 2000 + price: 445 + service_level: 0.96 + vlt: 3 + SKU321: + cost: 73 + init_stock: 1760 + price: 73 + service_level: 0.96 + vlt: 1 + SKU322: + cost: 120 + init_stock: 1540 + price: 120 + service_level: 0.96 + vlt: 2 + SKU323: + cost: 241 + init_stock: 1900 + price: 241 + service_level: 0.96 + vlt: 1 + SKU324: + cost: 269 + init_stock: 2000 + price: 269 + service_level: 0.96 + vlt: 1 + SKU325: + cost: 295 + init_stock: 480 + price: 295 + service_level: 0.96 + vlt: 3 + SKU326: + cost: 23 + init_stock: 1660 + price: 23 + service_level: 0.96 + vlt: 3 + SKU327: + cost: 334 + init_stock: 1320 + price: 334 + service_level: 0.96 + vlt: 1 + SKU328: + cost: 107 + init_stock: 220 + price: 107 + service_level: 0.96 + vlt: 2 + SKU329: + cost: 260 + init_stock: 1200 + price: 260 + service_level: 0.96 + vlt: 3 + SKU33: + cost: 81 + init_stock: 1680 + price: 81 + service_level: 0.96 + vlt: 3 + SKU330: + cost: 364 + init_stock: 880 + price: 364 + service_level: 0.96 + vlt: 2 + SKU331: + cost: 435 + init_stock: 560 + price: 435 + service_level: 0.96 + vlt: 3 + SKU332: + cost: 227 + init_stock: 1660 + price: 227 + service_level: 0.96 + vlt: 3 + SKU333: + cost: 268 + init_stock: 1260 + price: 268 + service_level: 0.96 + vlt: 3 + SKU334: + cost: 248 + init_stock: 860 + price: 248 + service_level: 0.96 + vlt: 1 + SKU335: + cost: 341 + init_stock: 1880 + price: 341 + service_level: 0.96 + vlt: 2 + SKU336: + cost: 489 + init_stock: 1000 + price: 489 + service_level: 0.96 + vlt: 2 + SKU337: + cost: 416 + init_stock: 1960 + price: 416 + service_level: 0.96 + vlt: 2 + SKU338: + cost: 209 + init_stock: 1480 + price: 209 + service_level: 0.96 + vlt: 3 + SKU339: + cost: 189 + init_stock: 1240 + price: 189 + service_level: 0.96 + vlt: 2 + SKU34: + cost: 263 + init_stock: 940 + price: 263 + service_level: 0.96 + vlt: 2 + SKU340: + cost: 375 + init_stock: 380 + price: 375 + service_level: 0.96 + vlt: 2 + SKU341: + cost: 166 + init_stock: 1120 + price: 166 + service_level: 0.96 + vlt: 3 + SKU342: + cost: 177 + init_stock: 820 + price: 177 + service_level: 0.96 + vlt: 1 + SKU343: + cost: 478 + init_stock: 1680 + price: 478 + service_level: 0.96 + vlt: 2 + SKU344: + cost: 491 + init_stock: 1620 + price: 491 + service_level: 0.96 + vlt: 3 + SKU345: + cost: 272 + init_stock: 660 + price: 272 + service_level: 0.96 + vlt: 3 + SKU346: + cost: 200 + init_stock: 1420 + price: 200 + service_level: 0.96 + vlt: 2 + SKU347: + cost: 180 + init_stock: 620 + price: 180 + service_level: 0.96 + vlt: 2 + SKU348: + cost: 296 + init_stock: 680 + price: 296 + service_level: 0.96 + vlt: 1 + SKU349: + cost: 81 + init_stock: 1820 + price: 81 + service_level: 0.96 + vlt: 2 + SKU35: + cost: 240 + init_stock: 1180 + price: 240 + service_level: 0.96 + vlt: 1 + SKU350: + cost: 106 + init_stock: 240 + price: 106 + service_level: 0.96 + vlt: 2 + SKU351: + cost: 102 + init_stock: 1480 + price: 102 + service_level: 0.96 + vlt: 3 + SKU352: + cost: 274 + init_stock: 320 + price: 274 + service_level: 0.96 + vlt: 2 + SKU353: + cost: 147 + init_stock: 1360 + price: 147 + service_level: 0.96 + vlt: 1 + SKU354: + cost: 410 + init_stock: 1560 + price: 410 + service_level: 0.96 + vlt: 3 + SKU355: + cost: 417 + init_stock: 1120 + price: 417 + service_level: 0.96 + vlt: 3 + SKU356: + cost: 192 + init_stock: 700 + price: 192 + service_level: 0.96 + vlt: 2 + SKU357: + cost: 80 + init_stock: 2000 + price: 80 + service_level: 0.96 + vlt: 1 + SKU358: + cost: 130 + init_stock: 640 + price: 130 + service_level: 0.96 + vlt: 1 + SKU359: + cost: 327 + init_stock: 640 + price: 327 + service_level: 0.96 + vlt: 2 + SKU36: + cost: 134 + init_stock: 1840 + price: 134 + service_level: 0.96 + vlt: 2 + SKU360: + cost: 456 + init_stock: 1100 + price: 456 + service_level: 0.96 + vlt: 2 + SKU361: + cost: 183 + init_stock: 1340 + price: 183 + service_level: 0.96 + vlt: 3 + SKU362: + cost: 352 + init_stock: 820 + price: 352 + service_level: 0.96 + vlt: 3 + SKU363: + cost: 32 + init_stock: 100 + price: 32 + service_level: 0.96 + vlt: 3 + SKU364: + cost: 331 + init_stock: 1300 + price: 331 + service_level: 0.96 + vlt: 1 + SKU365: + cost: 57 + init_stock: 1640 + price: 57 + service_level: 0.96 + vlt: 3 + SKU366: + cost: 184 + init_stock: 480 + price: 184 + service_level: 0.96 + vlt: 3 + SKU367: + cost: 385 + init_stock: 1600 + price: 385 + service_level: 0.96 + vlt: 1 + SKU368: + cost: 391 + init_stock: 620 + price: 391 + service_level: 0.96 + vlt: 3 + SKU369: + cost: 460 + init_stock: 640 + price: 460 + service_level: 0.96 + vlt: 3 + SKU37: + cost: 284 + init_stock: 1380 + price: 284 + service_level: 0.96 + vlt: 2 + SKU370: + cost: 103 + init_stock: 560 + price: 103 + service_level: 0.96 + vlt: 2 + SKU371: + cost: 181 + init_stock: 1680 + price: 181 + service_level: 0.96 + vlt: 3 + SKU372: + cost: 488 + init_stock: 400 + price: 488 + service_level: 0.96 + vlt: 3 + SKU373: + cost: 155 + init_stock: 540 + price: 155 + service_level: 0.96 + vlt: 2 + SKU374: + cost: 13 + init_stock: 1900 + price: 13 + service_level: 0.96 + vlt: 3 + SKU375: + cost: 433 + init_stock: 1240 + price: 433 + service_level: 0.96 + vlt: 3 + SKU376: + cost: 30 + init_stock: 1620 + price: 30 + service_level: 0.96 + vlt: 2 + SKU377: + cost: 100 + init_stock: 720 + price: 100 + service_level: 0.96 + vlt: 1 + SKU378: + cost: 489 + init_stock: 180 + price: 489 + service_level: 0.96 + vlt: 2 + SKU379: + cost: 462 + init_stock: 820 + price: 462 + service_level: 0.96 + vlt: 3 + SKU38: + cost: 326 + init_stock: 1620 + price: 326 + service_level: 0.96 + vlt: 3 + SKU380: + cost: 459 + init_stock: 260 + price: 459 + service_level: 0.96 + vlt: 3 + SKU381: + cost: 276 + init_stock: 1520 + price: 276 + service_level: 0.96 + vlt: 1 + SKU382: + cost: 472 + init_stock: 340 + price: 472 + service_level: 0.96 + vlt: 2 + SKU383: + cost: 249 + init_stock: 660 + price: 249 + service_level: 0.96 + vlt: 2 + SKU384: + cost: 37 + init_stock: 480 + price: 37 + service_level: 0.96 + vlt: 3 + SKU385: + cost: 275 + init_stock: 1600 + price: 275 + service_level: 0.96 + vlt: 3 + SKU386: + cost: 357 + init_stock: 1020 + price: 357 + service_level: 0.96 + vlt: 2 + SKU387: + cost: 471 + init_stock: 740 + price: 471 + service_level: 0.96 + vlt: 3 + SKU388: + cost: 264 + init_stock: 1340 + price: 264 + service_level: 0.96 + vlt: 2 + SKU389: + cost: 13 + init_stock: 980 + price: 13 + service_level: 0.96 + vlt: 2 + SKU39: + cost: 381 + init_stock: 440 + price: 381 + service_level: 0.96 + vlt: 1 + SKU390: + cost: 146 + init_stock: 100 + price: 146 + service_level: 0.96 + vlt: 1 + SKU391: + cost: 365 + init_stock: 1840 + price: 365 + service_level: 0.96 + vlt: 1 + SKU392: + cost: 489 + init_stock: 920 + price: 489 + service_level: 0.96 + vlt: 3 + SKU393: + cost: 466 + init_stock: 220 + price: 466 + service_level: 0.96 + vlt: 3 + SKU394: + cost: 186 + init_stock: 340 + price: 186 + service_level: 0.96 + vlt: 2 + SKU395: + cost: 324 + init_stock: 600 + price: 324 + service_level: 0.96 + vlt: 1 + SKU396: + cost: 392 + init_stock: 1360 + price: 392 + service_level: 0.96 + vlt: 3 + SKU397: + cost: 257 + init_stock: 460 + price: 257 + service_level: 0.96 + vlt: 1 + SKU398: + cost: 455 + init_stock: 1420 + price: 455 + service_level: 0.96 + vlt: 1 + SKU399: + cost: 15 + init_stock: 1560 + price: 15 + service_level: 0.96 + vlt: 3 + SKU4: + cost: 289 + init_stock: 1860 + price: 289 + service_level: 0.96 + vlt: 1 + SKU40: + cost: 216 + init_stock: 160 + price: 216 + service_level: 0.96 + vlt: 2 + SKU400: + cost: 355 + init_stock: 1900 + price: 355 + service_level: 0.96 + vlt: 2 + SKU401: + cost: 150 + init_stock: 160 + price: 150 + service_level: 0.96 + vlt: 3 + SKU402: + cost: 255 + init_stock: 1840 + price: 255 + service_level: 0.96 + vlt: 2 + SKU403: + cost: 333 + init_stock: 1660 + price: 333 + service_level: 0.96 + vlt: 2 + SKU404: + cost: 94 + init_stock: 720 + price: 94 + service_level: 0.96 + vlt: 3 + SKU405: + cost: 242 + init_stock: 1160 + price: 242 + service_level: 0.96 + vlt: 1 + SKU406: + cost: 402 + init_stock: 1100 + price: 402 + service_level: 0.96 + vlt: 3 + SKU407: + cost: 346 + init_stock: 780 + price: 346 + service_level: 0.96 + vlt: 3 + SKU408: + cost: 274 + init_stock: 1640 + price: 274 + service_level: 0.96 + vlt: 2 + SKU409: + cost: 268 + init_stock: 1880 + price: 268 + service_level: 0.96 + vlt: 2 + SKU41: + cost: 312 + init_stock: 1360 + price: 312 + service_level: 0.96 + vlt: 3 + SKU410: + cost: 429 + init_stock: 960 + price: 429 + service_level: 0.96 + vlt: 2 + SKU411: + cost: 11 + init_stock: 600 + price: 11 + service_level: 0.96 + vlt: 1 + SKU412: + cost: 91 + init_stock: 1180 + price: 91 + service_level: 0.96 + vlt: 3 + SKU413: + cost: 443 + init_stock: 220 + price: 443 + service_level: 0.96 + vlt: 3 + SKU414: + cost: 67 + init_stock: 1540 + price: 67 + service_level: 0.96 + vlt: 3 + SKU415: + cost: 347 + init_stock: 200 + price: 347 + service_level: 0.96 + vlt: 2 + SKU416: + cost: 29 + init_stock: 1020 + price: 29 + service_level: 0.96 + vlt: 3 + SKU417: + cost: 488 + init_stock: 1360 + price: 488 + service_level: 0.96 + vlt: 1 + SKU418: + cost: 432 + init_stock: 1940 + price: 432 + service_level: 0.96 + vlt: 3 + SKU419: + cost: 38 + init_stock: 260 + price: 38 + service_level: 0.96 + vlt: 1 + SKU42: + cost: 275 + init_stock: 1660 + price: 275 + service_level: 0.96 + vlt: 3 + SKU420: + cost: 316 + init_stock: 140 + price: 316 + service_level: 0.96 + vlt: 2 + SKU421: + cost: 289 + init_stock: 1600 + price: 289 + service_level: 0.96 + vlt: 3 + SKU422: + cost: 83 + init_stock: 680 + price: 83 + service_level: 0.96 + vlt: 2 + SKU423: + cost: 174 + init_stock: 240 + price: 174 + service_level: 0.96 + vlt: 2 + SKU424: + cost: 495 + init_stock: 820 + price: 495 + service_level: 0.96 + vlt: 1 + SKU425: + cost: 142 + init_stock: 880 + price: 142 + service_level: 0.96 + vlt: 1 + SKU426: + cost: 234 + init_stock: 320 + price: 234 + service_level: 0.96 + vlt: 3 + SKU427: + cost: 162 + init_stock: 1680 + price: 162 + service_level: 0.96 + vlt: 2 + SKU428: + cost: 423 + init_stock: 1240 + price: 423 + service_level: 0.96 + vlt: 1 + SKU429: + cost: 322 + init_stock: 1980 + price: 322 + service_level: 0.96 + vlt: 2 + SKU43: + cost: 428 + init_stock: 1080 + price: 428 + service_level: 0.96 + vlt: 3 + SKU430: + cost: 289 + init_stock: 900 + price: 289 + service_level: 0.96 + vlt: 1 + SKU431: + cost: 29 + init_stock: 260 + price: 29 + service_level: 0.96 + vlt: 3 + SKU432: + cost: 349 + init_stock: 460 + price: 349 + service_level: 0.96 + vlt: 3 + SKU433: + cost: 380 + init_stock: 520 + price: 380 + service_level: 0.96 + vlt: 3 + SKU434: + cost: 373 + init_stock: 820 + price: 373 + service_level: 0.96 + vlt: 3 + SKU435: + cost: 46 + init_stock: 1000 + price: 46 + service_level: 0.96 + vlt: 3 + SKU436: + cost: 383 + init_stock: 760 + price: 383 + service_level: 0.96 + vlt: 2 + SKU437: + cost: 76 + init_stock: 1640 + price: 76 + service_level: 0.96 + vlt: 3 + SKU438: + cost: 433 + init_stock: 540 + price: 433 + service_level: 0.96 + vlt: 3 + SKU439: + cost: 40 + init_stock: 1140 + price: 40 + service_level: 0.96 + vlt: 1 + SKU44: + cost: 255 + init_stock: 380 + price: 255 + service_level: 0.96 + vlt: 1 + SKU440: + cost: 124 + init_stock: 940 + price: 124 + service_level: 0.96 + vlt: 3 + SKU441: + cost: 228 + init_stock: 1080 + price: 228 + service_level: 0.96 + vlt: 2 + SKU442: + cost: 19 + init_stock: 820 + price: 19 + service_level: 0.96 + vlt: 3 + SKU443: + cost: 446 + init_stock: 1240 + price: 446 + service_level: 0.96 + vlt: 1 + SKU444: + cost: 212 + init_stock: 1080 + price: 212 + service_level: 0.96 + vlt: 2 + SKU445: + cost: 396 + init_stock: 400 + price: 396 + service_level: 0.96 + vlt: 2 + SKU446: + cost: 126 + init_stock: 1280 + price: 126 + service_level: 0.96 + vlt: 3 + SKU447: + cost: 20 + init_stock: 800 + price: 20 + service_level: 0.96 + vlt: 2 + SKU448: + cost: 365 + init_stock: 1320 + price: 365 + service_level: 0.96 + vlt: 3 + SKU449: + cost: 328 + init_stock: 160 + price: 328 + service_level: 0.96 + vlt: 1 + SKU45: + cost: 158 + init_stock: 560 + price: 158 + service_level: 0.96 + vlt: 3 + SKU450: + cost: 61 + init_stock: 120 + price: 61 + service_level: 0.96 + vlt: 1 + SKU451: + cost: 385 + init_stock: 720 + price: 385 + service_level: 0.96 + vlt: 2 + SKU452: + cost: 450 + init_stock: 1460 + price: 450 + service_level: 0.96 + vlt: 3 + SKU453: + cost: 131 + init_stock: 1620 + price: 131 + service_level: 0.96 + vlt: 1 + SKU454: + cost: 404 + init_stock: 1180 + price: 404 + service_level: 0.96 + vlt: 2 + SKU455: + cost: 334 + init_stock: 1480 + price: 334 + service_level: 0.96 + vlt: 1 + SKU456: + cost: 126 + init_stock: 820 + price: 126 + service_level: 0.96 + vlt: 3 + SKU457: + cost: 235 + init_stock: 1120 + price: 235 + service_level: 0.96 + vlt: 1 + SKU458: + cost: 282 + init_stock: 940 + price: 282 + service_level: 0.96 + vlt: 1 + SKU459: + cost: 180 + init_stock: 440 + price: 180 + service_level: 0.96 + vlt: 3 + SKU46: + cost: 341 + init_stock: 400 + price: 341 + service_level: 0.96 + vlt: 1 + SKU460: + cost: 271 + init_stock: 100 + price: 271 + service_level: 0.96 + vlt: 3 + SKU461: + cost: 131 + init_stock: 540 + price: 131 + service_level: 0.96 + vlt: 1 + SKU462: + cost: 98 + init_stock: 180 + price: 98 + service_level: 0.96 + vlt: 2 + SKU463: + cost: 117 + init_stock: 460 + price: 117 + service_level: 0.96 + vlt: 3 + SKU464: + cost: 295 + init_stock: 480 + price: 295 + service_level: 0.96 + vlt: 1 + SKU465: + cost: 286 + init_stock: 1540 + price: 286 + service_level: 0.96 + vlt: 2 + SKU466: + cost: 278 + init_stock: 1060 + price: 278 + service_level: 0.96 + vlt: 2 + SKU467: + cost: 294 + init_stock: 160 + price: 294 + service_level: 0.96 + vlt: 3 + SKU468: + cost: 40 + init_stock: 200 + price: 40 + service_level: 0.96 + vlt: 2 + SKU469: + cost: 481 + init_stock: 2000 + price: 481 + service_level: 0.96 + vlt: 2 + SKU47: + cost: 460 + init_stock: 220 + price: 460 + service_level: 0.96 + vlt: 2 + SKU470: + cost: 470 + init_stock: 1220 + price: 470 + service_level: 0.96 + vlt: 2 + SKU471: + cost: 98 + init_stock: 620 + price: 98 + service_level: 0.96 + vlt: 3 + SKU472: + cost: 22 + init_stock: 1580 + price: 22 + service_level: 0.96 + vlt: 3 + SKU473: + cost: 414 + init_stock: 1640 + price: 414 + service_level: 0.96 + vlt: 3 + SKU474: + cost: 95 + init_stock: 820 + price: 95 + service_level: 0.96 + vlt: 2 + SKU475: + cost: 390 + init_stock: 720 + price: 390 + service_level: 0.96 + vlt: 2 + SKU476: + cost: 468 + init_stock: 1720 + price: 468 + service_level: 0.96 + vlt: 2 + SKU477: + cost: 91 + init_stock: 800 + price: 91 + service_level: 0.96 + vlt: 1 + SKU478: + cost: 242 + init_stock: 1020 + price: 242 + service_level: 0.96 + vlt: 3 + SKU479: + cost: 158 + init_stock: 1000 + price: 158 + service_level: 0.96 + vlt: 2 + SKU48: + cost: 274 + init_stock: 240 + price: 274 + service_level: 0.96 + vlt: 3 + SKU480: + cost: 424 + init_stock: 1360 + price: 424 + service_level: 0.96 + vlt: 1 + SKU481: + cost: 199 + init_stock: 980 + price: 199 + service_level: 0.96 + vlt: 2 + SKU482: + cost: 217 + init_stock: 520 + price: 217 + service_level: 0.96 + vlt: 1 + SKU483: + cost: 120 + init_stock: 240 + price: 120 + service_level: 0.96 + vlt: 1 + SKU484: + cost: 244 + init_stock: 1600 + price: 244 + service_level: 0.96 + vlt: 2 + SKU485: + cost: 266 + init_stock: 1140 + price: 266 + service_level: 0.96 + vlt: 3 + SKU486: + cost: 243 + init_stock: 240 + price: 243 + service_level: 0.96 + vlt: 1 + SKU487: + cost: 275 + init_stock: 1400 + price: 275 + service_level: 0.96 + vlt: 1 + SKU488: + cost: 423 + init_stock: 260 + price: 423 + service_level: 0.96 + vlt: 1 + SKU489: + cost: 241 + init_stock: 760 + price: 241 + service_level: 0.96 + vlt: 3 + SKU49: + cost: 184 + init_stock: 1440 + price: 184 + service_level: 0.96 + vlt: 1 + SKU490: + cost: 285 + init_stock: 320 + price: 285 + service_level: 0.96 + vlt: 3 + SKU491: + cost: 334 + init_stock: 1560 + price: 334 + service_level: 0.96 + vlt: 3 + SKU492: + cost: 371 + init_stock: 1360 + price: 371 + service_level: 0.96 + vlt: 2 + SKU493: + cost: 402 + init_stock: 1900 + price: 402 + service_level: 0.96 + vlt: 3 + SKU494: + cost: 84 + init_stock: 1740 + price: 84 + service_level: 0.96 + vlt: 2 + SKU495: + cost: 347 + init_stock: 740 + price: 347 + service_level: 0.96 + vlt: 2 + SKU496: + cost: 177 + init_stock: 1060 + price: 177 + service_level: 0.96 + vlt: 1 + SKU497: + cost: 260 + init_stock: 620 + price: 260 + service_level: 0.96 + vlt: 3 + SKU498: + cost: 447 + init_stock: 1880 + price: 447 + service_level: 0.96 + vlt: 1 + SKU499: + cost: 425 + init_stock: 1560 + price: 425 + service_level: 0.96 + vlt: 1 + SKU5: + cost: 478 + init_stock: 1480 + price: 478 + service_level: 0.96 + vlt: 2 + SKU50: + cost: 427 + init_stock: 360 + price: 427 + service_level: 0.96 + vlt: 1 + SKU500: + cost: 413 + init_stock: 200 + price: 413 + service_level: 0.96 + vlt: 3 + SKU501: + cost: 74 + init_stock: 1560 + price: 74 + service_level: 0.96 + vlt: 3 + SKU502: + cost: 411 + init_stock: 180 + price: 411 + service_level: 0.96 + vlt: 1 + SKU503: + cost: 459 + init_stock: 880 + price: 459 + service_level: 0.96 + vlt: 2 + SKU504: + cost: 325 + init_stock: 800 + price: 325 + service_level: 0.96 + vlt: 3 + SKU505: + cost: 324 + init_stock: 1440 + price: 324 + service_level: 0.96 + vlt: 1 + SKU506: + cost: 477 + init_stock: 160 + price: 477 + service_level: 0.96 + vlt: 3 + SKU507: + cost: 36 + init_stock: 1420 + price: 36 + service_level: 0.96 + vlt: 1 + SKU508: + cost: 390 + init_stock: 1140 + price: 390 + service_level: 0.96 + vlt: 2 + SKU509: + cost: 464 + init_stock: 1420 + price: 464 + service_level: 0.96 + vlt: 1 + SKU51: + cost: 434 + init_stock: 980 + price: 434 + service_level: 0.96 + vlt: 3 + SKU510: + cost: 233 + init_stock: 1100 + price: 233 + service_level: 0.96 + vlt: 3 + SKU511: + cost: 317 + init_stock: 540 + price: 317 + service_level: 0.96 + vlt: 2 + SKU512: + cost: 405 + init_stock: 1440 + price: 405 + service_level: 0.96 + vlt: 1 + SKU513: + cost: 33 + init_stock: 980 + price: 33 + service_level: 0.96 + vlt: 3 + SKU514: + cost: 441 + init_stock: 480 + price: 441 + service_level: 0.96 + vlt: 3 + SKU515: + cost: 456 + init_stock: 700 + price: 456 + service_level: 0.96 + vlt: 1 + SKU516: + cost: 432 + init_stock: 240 + price: 432 + service_level: 0.96 + vlt: 3 + SKU517: + cost: 223 + init_stock: 1580 + price: 223 + service_level: 0.96 + vlt: 2 + SKU518: + cost: 155 + init_stock: 1540 + price: 155 + service_level: 0.96 + vlt: 3 + SKU519: + cost: 276 + init_stock: 940 + price: 276 + service_level: 0.96 + vlt: 2 + SKU52: + cost: 340 + init_stock: 1980 + price: 340 + service_level: 0.96 + vlt: 3 + SKU520: + cost: 18 + init_stock: 580 + price: 18 + service_level: 0.96 + vlt: 1 + SKU521: + cost: 356 + init_stock: 1820 + price: 356 + service_level: 0.96 + vlt: 2 + SKU522: + cost: 389 + init_stock: 600 + price: 389 + service_level: 0.96 + vlt: 2 + SKU523: + cost: 359 + init_stock: 500 + price: 359 + service_level: 0.96 + vlt: 3 + SKU524: + cost: 213 + init_stock: 780 + price: 213 + service_level: 0.96 + vlt: 1 + SKU525: + cost: 131 + init_stock: 220 + price: 131 + service_level: 0.96 + vlt: 1 + SKU526: + cost: 237 + init_stock: 720 + price: 237 + service_level: 0.96 + vlt: 1 + SKU527: + cost: 161 + init_stock: 240 + price: 161 + service_level: 0.96 + vlt: 3 + SKU528: + cost: 384 + init_stock: 980 + price: 384 + service_level: 0.96 + vlt: 2 + SKU529: + cost: 321 + init_stock: 260 + price: 321 + service_level: 0.96 + vlt: 1 + SKU53: + cost: 20 + init_stock: 840 + price: 20 + service_level: 0.96 + vlt: 2 + SKU530: + cost: 299 + init_stock: 1920 + price: 299 + service_level: 0.96 + vlt: 1 + SKU531: + cost: 335 + init_stock: 1200 + price: 335 + service_level: 0.96 + vlt: 3 + SKU532: + cost: 155 + init_stock: 1760 + price: 155 + service_level: 0.96 + vlt: 1 + SKU533: + cost: 444 + init_stock: 820 + price: 444 + service_level: 0.96 + vlt: 3 + SKU534: + cost: 146 + init_stock: 1940 + price: 146 + service_level: 0.96 + vlt: 1 + SKU535: + cost: 384 + init_stock: 700 + price: 384 + service_level: 0.96 + vlt: 2 + SKU536: + cost: 86 + init_stock: 2000 + price: 86 + service_level: 0.96 + vlt: 1 + SKU537: + cost: 173 + init_stock: 940 + price: 173 + service_level: 0.96 + vlt: 1 + SKU538: + cost: 40 + init_stock: 940 + price: 40 + service_level: 0.96 + vlt: 3 + SKU539: + cost: 390 + init_stock: 200 + price: 390 + service_level: 0.96 + vlt: 2 + SKU54: + cost: 162 + init_stock: 1540 + price: 162 + service_level: 0.96 + vlt: 2 + SKU540: + cost: 290 + init_stock: 960 + price: 290 + service_level: 0.96 + vlt: 1 + SKU541: + cost: 192 + init_stock: 720 + price: 192 + service_level: 0.96 + vlt: 3 + SKU542: + cost: 96 + init_stock: 920 + price: 96 + service_level: 0.96 + vlt: 3 + SKU543: + cost: 197 + init_stock: 800 + price: 197 + service_level: 0.96 + vlt: 3 + SKU544: + cost: 408 + init_stock: 1280 + price: 408 + service_level: 0.96 + vlt: 2 + SKU545: + cost: 431 + init_stock: 1300 + price: 431 + service_level: 0.96 + vlt: 1 + SKU546: + cost: 88 + init_stock: 340 + price: 88 + service_level: 0.96 + vlt: 1 + SKU547: + cost: 454 + init_stock: 1920 + price: 454 + service_level: 0.96 + vlt: 3 + SKU548: + cost: 415 + init_stock: 1820 + price: 415 + service_level: 0.96 + vlt: 2 + SKU549: + cost: 307 + init_stock: 1160 + price: 307 + service_level: 0.96 + vlt: 2 + SKU55: + cost: 106 + init_stock: 420 + price: 106 + service_level: 0.96 + vlt: 3 + SKU550: + cost: 446 + init_stock: 380 + price: 446 + service_level: 0.96 + vlt: 3 + SKU551: + cost: 393 + init_stock: 1200 + price: 393 + service_level: 0.96 + vlt: 2 + SKU552: + cost: 114 + init_stock: 1680 + price: 114 + service_level: 0.96 + vlt: 3 + SKU553: + cost: 71 + init_stock: 700 + price: 71 + service_level: 0.96 + vlt: 3 + SKU554: + cost: 314 + init_stock: 260 + price: 314 + service_level: 0.96 + vlt: 1 + SKU555: + cost: 492 + init_stock: 340 + price: 492 + service_level: 0.96 + vlt: 3 + SKU556: + cost: 96 + init_stock: 1320 + price: 96 + service_level: 0.96 + vlt: 2 + SKU557: + cost: 303 + init_stock: 1440 + price: 303 + service_level: 0.96 + vlt: 3 + SKU558: + cost: 430 + init_stock: 420 + price: 430 + service_level: 0.96 + vlt: 1 + SKU559: + cost: 222 + init_stock: 1180 + price: 222 + service_level: 0.96 + vlt: 1 + SKU56: + cost: 435 + init_stock: 460 + price: 435 + service_level: 0.96 + vlt: 3 + SKU560: + cost: 146 + init_stock: 1780 + price: 146 + service_level: 0.96 + vlt: 2 + SKU561: + cost: 158 + init_stock: 820 + price: 158 + service_level: 0.96 + vlt: 2 + SKU562: + cost: 51 + init_stock: 1480 + price: 51 + service_level: 0.96 + vlt: 1 + SKU563: + cost: 418 + init_stock: 500 + price: 418 + service_level: 0.96 + vlt: 2 + SKU564: + cost: 442 + init_stock: 480 + price: 442 + service_level: 0.96 + vlt: 2 + SKU565: + cost: 310 + init_stock: 1740 + price: 310 + service_level: 0.96 + vlt: 2 + SKU566: + cost: 34 + init_stock: 120 + price: 34 + service_level: 0.96 + vlt: 3 + SKU567: + cost: 120 + init_stock: 860 + price: 120 + service_level: 0.96 + vlt: 2 + SKU568: + cost: 97 + init_stock: 160 + price: 97 + service_level: 0.96 + vlt: 1 + SKU569: + cost: 285 + init_stock: 1880 + price: 285 + service_level: 0.96 + vlt: 1 + SKU57: + cost: 400 + init_stock: 600 + price: 400 + service_level: 0.96 + vlt: 3 + SKU570: + cost: 161 + init_stock: 860 + price: 161 + service_level: 0.96 + vlt: 3 + SKU571: + cost: 50 + init_stock: 1640 + price: 50 + service_level: 0.96 + vlt: 3 + SKU572: + cost: 489 + init_stock: 960 + price: 489 + service_level: 0.96 + vlt: 2 + SKU573: + cost: 274 + init_stock: 740 + price: 274 + service_level: 0.96 + vlt: 2 + SKU574: + cost: 357 + init_stock: 640 + price: 357 + service_level: 0.96 + vlt: 2 + SKU575: + cost: 265 + init_stock: 1960 + price: 265 + service_level: 0.96 + vlt: 2 + SKU576: + cost: 137 + init_stock: 1340 + price: 137 + service_level: 0.96 + vlt: 2 + SKU577: + cost: 420 + init_stock: 1280 + price: 420 + service_level: 0.96 + vlt: 2 + SKU578: + cost: 74 + init_stock: 300 + price: 74 + service_level: 0.96 + vlt: 3 + SKU579: + cost: 310 + init_stock: 1120 + price: 310 + service_level: 0.96 + vlt: 2 + SKU58: + cost: 217 + init_stock: 1620 + price: 217 + service_level: 0.96 + vlt: 1 + SKU580: + cost: 153 + init_stock: 1980 + price: 153 + service_level: 0.96 + vlt: 1 + SKU581: + cost: 494 + init_stock: 1020 + price: 494 + service_level: 0.96 + vlt: 2 + SKU582: + cost: 476 + init_stock: 260 + price: 476 + service_level: 0.96 + vlt: 2 + SKU583: + cost: 105 + init_stock: 220 + price: 105 + service_level: 0.96 + vlt: 1 + SKU584: + cost: 365 + init_stock: 940 + price: 365 + service_level: 0.96 + vlt: 2 + SKU585: + cost: 184 + init_stock: 1080 + price: 184 + service_level: 0.96 + vlt: 2 + SKU586: + cost: 471 + init_stock: 1880 + price: 471 + service_level: 0.96 + vlt: 1 + SKU587: + cost: 197 + init_stock: 1600 + price: 197 + service_level: 0.96 + vlt: 1 + SKU588: + cost: 15 + init_stock: 540 + price: 15 + service_level: 0.96 + vlt: 3 + SKU589: + cost: 185 + init_stock: 500 + price: 185 + service_level: 0.96 + vlt: 3 + SKU59: + cost: 39 + init_stock: 960 + price: 39 + service_level: 0.96 + vlt: 1 + SKU590: + cost: 348 + init_stock: 1240 + price: 348 + service_level: 0.96 + vlt: 2 + SKU591: + cost: 163 + init_stock: 740 + price: 163 + service_level: 0.96 + vlt: 2 + SKU592: + cost: 305 + init_stock: 1840 + price: 305 + service_level: 0.96 + vlt: 1 + SKU593: + cost: 479 + init_stock: 1020 + price: 479 + service_level: 0.96 + vlt: 2 + SKU594: + cost: 478 + init_stock: 1020 + price: 478 + service_level: 0.96 + vlt: 2 + SKU595: + cost: 35 + init_stock: 1100 + price: 35 + service_level: 0.96 + vlt: 1 + SKU596: + cost: 449 + init_stock: 540 + price: 449 + service_level: 0.96 + vlt: 2 + SKU597: + cost: 21 + init_stock: 1920 + price: 21 + service_level: 0.96 + vlt: 3 + SKU598: + cost: 49 + init_stock: 780 + price: 49 + service_level: 0.96 + vlt: 1 + SKU599: + cost: 201 + init_stock: 1520 + price: 201 + service_level: 0.96 + vlt: 3 + SKU6: + cost: 472 + init_stock: 820 + price: 472 + service_level: 0.96 + vlt: 3 + SKU60: + cost: 24 + init_stock: 1700 + price: 24 + service_level: 0.96 + vlt: 2 + SKU600: + cost: 321 + init_stock: 1100 + price: 321 + service_level: 0.96 + vlt: 2 + SKU601: + cost: 185 + init_stock: 200 + price: 185 + service_level: 0.96 + vlt: 1 + SKU602: + cost: 180 + init_stock: 1880 + price: 180 + service_level: 0.96 + vlt: 2 + SKU603: + cost: 328 + init_stock: 1900 + price: 328 + service_level: 0.96 + vlt: 3 + SKU604: + cost: 143 + init_stock: 1920 + price: 143 + service_level: 0.96 + vlt: 2 + SKU605: + cost: 337 + init_stock: 440 + price: 337 + service_level: 0.96 + vlt: 1 + SKU606: + cost: 166 + init_stock: 500 + price: 166 + service_level: 0.96 + vlt: 1 + SKU607: + cost: 286 + init_stock: 1620 + price: 286 + service_level: 0.96 + vlt: 3 + SKU608: + cost: 18 + init_stock: 820 + price: 18 + service_level: 0.96 + vlt: 1 + SKU609: + cost: 330 + init_stock: 1560 + price: 330 + service_level: 0.96 + vlt: 3 + SKU61: + cost: 422 + init_stock: 200 + price: 422 + service_level: 0.96 + vlt: 2 + SKU610: + cost: 489 + init_stock: 760 + price: 489 + service_level: 0.96 + vlt: 2 + SKU611: + cost: 379 + init_stock: 700 + price: 379 + service_level: 0.96 + vlt: 1 + SKU612: + cost: 28 + init_stock: 760 + price: 28 + service_level: 0.96 + vlt: 2 + SKU613: + cost: 303 + init_stock: 980 + price: 303 + service_level: 0.96 + vlt: 2 + SKU614: + cost: 264 + init_stock: 740 + price: 264 + service_level: 0.96 + vlt: 1 + SKU615: + cost: 337 + init_stock: 820 + price: 337 + service_level: 0.96 + vlt: 2 + SKU616: + cost: 309 + init_stock: 1220 + price: 309 + service_level: 0.96 + vlt: 1 + SKU617: + cost: 237 + init_stock: 1040 + price: 237 + service_level: 0.96 + vlt: 2 + SKU618: + cost: 143 + init_stock: 820 + price: 143 + service_level: 0.96 + vlt: 3 + SKU619: + cost: 60 + init_stock: 720 + price: 60 + service_level: 0.96 + vlt: 1 + SKU62: + cost: 92 + init_stock: 820 + price: 92 + service_level: 0.96 + vlt: 2 + SKU620: + cost: 478 + init_stock: 980 + price: 478 + service_level: 0.96 + vlt: 2 + SKU621: + cost: 216 + init_stock: 600 + price: 216 + service_level: 0.96 + vlt: 1 + SKU622: + cost: 270 + init_stock: 480 + price: 270 + service_level: 0.96 + vlt: 3 + SKU623: + cost: 448 + init_stock: 760 + price: 448 + service_level: 0.96 + vlt: 3 + SKU624: + cost: 429 + init_stock: 1100 + price: 429 + service_level: 0.96 + vlt: 1 + SKU625: + cost: 236 + init_stock: 1560 + price: 236 + service_level: 0.96 + vlt: 1 + SKU626: + cost: 135 + init_stock: 640 + price: 135 + service_level: 0.96 + vlt: 1 + SKU627: + cost: 78 + init_stock: 780 + price: 78 + service_level: 0.96 + vlt: 3 + SKU628: + cost: 114 + init_stock: 1760 + price: 114 + service_level: 0.96 + vlt: 3 + SKU629: + cost: 390 + init_stock: 1080 + price: 390 + service_level: 0.96 + vlt: 1 + SKU63: + cost: 341 + init_stock: 1380 + price: 341 + service_level: 0.96 + vlt: 1 + SKU630: + cost: 500 + init_stock: 1680 + price: 500 + service_level: 0.96 + vlt: 1 + SKU631: + cost: 386 + init_stock: 1460 + price: 386 + service_level: 0.96 + vlt: 2 + SKU632: + cost: 72 + init_stock: 680 + price: 72 + service_level: 0.96 + vlt: 3 + SKU633: + cost: 306 + init_stock: 1160 + price: 306 + service_level: 0.96 + vlt: 1 + SKU634: + cost: 136 + init_stock: 1680 + price: 136 + service_level: 0.96 + vlt: 3 + SKU635: + cost: 463 + init_stock: 900 + price: 463 + service_level: 0.96 + vlt: 1 + SKU636: + cost: 243 + init_stock: 1140 + price: 243 + service_level: 0.96 + vlt: 3 + SKU637: + cost: 498 + init_stock: 600 + price: 498 + service_level: 0.96 + vlt: 2 + SKU638: + cost: 93 + init_stock: 1320 + price: 93 + service_level: 0.96 + vlt: 3 + SKU639: + cost: 476 + init_stock: 200 + price: 476 + service_level: 0.96 + vlt: 2 + SKU64: + cost: 151 + init_stock: 1920 + price: 151 + service_level: 0.96 + vlt: 2 + SKU640: + cost: 350 + init_stock: 1120 + price: 350 + service_level: 0.96 + vlt: 3 + SKU641: + cost: 434 + init_stock: 720 + price: 434 + service_level: 0.96 + vlt: 2 + SKU642: + cost: 349 + init_stock: 1440 + price: 349 + service_level: 0.96 + vlt: 3 + SKU643: + cost: 17 + init_stock: 540 + price: 17 + service_level: 0.96 + vlt: 1 + SKU644: + cost: 27 + init_stock: 1860 + price: 27 + service_level: 0.96 + vlt: 1 + SKU645: + cost: 143 + init_stock: 180 + price: 143 + service_level: 0.96 + vlt: 3 + SKU646: + cost: 320 + init_stock: 1740 + price: 320 + service_level: 0.96 + vlt: 2 + SKU647: + cost: 296 + init_stock: 520 + price: 296 + service_level: 0.96 + vlt: 3 + SKU648: + cost: 251 + init_stock: 960 + price: 251 + service_level: 0.96 + vlt: 1 + SKU649: + cost: 171 + init_stock: 1720 + price: 171 + service_level: 0.96 + vlt: 2 + SKU65: + cost: 48 + init_stock: 1600 + price: 48 + service_level: 0.96 + vlt: 1 + SKU650: + cost: 406 + init_stock: 940 + price: 406 + service_level: 0.96 + vlt: 1 + SKU651: + cost: 209 + init_stock: 1420 + price: 209 + service_level: 0.96 + vlt: 3 + SKU652: + cost: 167 + init_stock: 1360 + price: 167 + service_level: 0.96 + vlt: 3 + SKU653: + cost: 122 + init_stock: 120 + price: 122 + service_level: 0.96 + vlt: 3 + SKU654: + cost: 344 + init_stock: 920 + price: 344 + service_level: 0.96 + vlt: 2 + SKU655: + cost: 382 + init_stock: 1780 + price: 382 + service_level: 0.96 + vlt: 3 + SKU656: + cost: 229 + init_stock: 1900 + price: 229 + service_level: 0.96 + vlt: 2 + SKU657: + cost: 474 + init_stock: 1540 + price: 474 + service_level: 0.96 + vlt: 3 + SKU658: + cost: 307 + init_stock: 860 + price: 307 + service_level: 0.96 + vlt: 3 + SKU659: + cost: 392 + init_stock: 500 + price: 392 + service_level: 0.96 + vlt: 3 + SKU66: + cost: 492 + init_stock: 1000 + price: 492 + service_level: 0.96 + vlt: 1 + SKU660: + cost: 206 + init_stock: 240 + price: 206 + service_level: 0.96 + vlt: 3 + SKU661: + cost: 100 + init_stock: 2000 + price: 100 + service_level: 0.96 + vlt: 1 + SKU662: + cost: 283 + init_stock: 120 + price: 283 + service_level: 0.96 + vlt: 1 + SKU663: + cost: 416 + init_stock: 880 + price: 416 + service_level: 0.96 + vlt: 3 + SKU664: + cost: 93 + init_stock: 1400 + price: 93 + service_level: 0.96 + vlt: 2 + SKU665: + cost: 415 + init_stock: 1980 + price: 415 + service_level: 0.96 + vlt: 3 + SKU666: + cost: 378 + init_stock: 100 + price: 378 + service_level: 0.96 + vlt: 3 + SKU667: + cost: 114 + init_stock: 1560 + price: 114 + service_level: 0.96 + vlt: 1 + SKU668: + cost: 115 + init_stock: 320 + price: 115 + service_level: 0.96 + vlt: 1 + SKU669: + cost: 414 + init_stock: 400 + price: 414 + service_level: 0.96 + vlt: 3 + SKU67: + cost: 16 + init_stock: 1240 + price: 16 + service_level: 0.96 + vlt: 3 + SKU670: + cost: 171 + init_stock: 1320 + price: 171 + service_level: 0.96 + vlt: 2 + SKU671: + cost: 163 + init_stock: 780 + price: 163 + service_level: 0.96 + vlt: 3 + SKU672: + cost: 67 + init_stock: 220 + price: 67 + service_level: 0.96 + vlt: 1 + SKU673: + cost: 437 + init_stock: 1480 + price: 437 + service_level: 0.96 + vlt: 3 + SKU674: + cost: 50 + init_stock: 380 + price: 50 + service_level: 0.96 + vlt: 2 + SKU675: + cost: 479 + init_stock: 400 + price: 479 + service_level: 0.96 + vlt: 1 + SKU676: + cost: 460 + init_stock: 980 + price: 460 + service_level: 0.96 + vlt: 2 + SKU677: + cost: 392 + init_stock: 1880 + price: 392 + service_level: 0.96 + vlt: 2 + SKU678: + cost: 143 + init_stock: 1340 + price: 143 + service_level: 0.96 + vlt: 2 + SKU679: + cost: 131 + init_stock: 1640 + price: 131 + service_level: 0.96 + vlt: 3 + SKU68: + cost: 108 + init_stock: 920 + price: 108 + service_level: 0.96 + vlt: 2 + SKU680: + cost: 436 + init_stock: 480 + price: 436 + service_level: 0.96 + vlt: 1 + SKU681: + cost: 338 + init_stock: 260 + price: 338 + service_level: 0.96 + vlt: 3 + SKU682: + cost: 146 + init_stock: 320 + price: 146 + service_level: 0.96 + vlt: 2 + SKU683: + cost: 486 + init_stock: 720 + price: 486 + service_level: 0.96 + vlt: 2 + SKU684: + cost: 370 + init_stock: 1440 + price: 370 + service_level: 0.96 + vlt: 3 + SKU685: + cost: 69 + init_stock: 1540 + price: 69 + service_level: 0.96 + vlt: 1 + SKU686: + cost: 56 + init_stock: 540 + price: 56 + service_level: 0.96 + vlt: 1 + SKU687: + cost: 308 + init_stock: 960 + price: 308 + service_level: 0.96 + vlt: 1 + SKU688: + cost: 200 + init_stock: 1060 + price: 200 + service_level: 0.96 + vlt: 2 + SKU689: + cost: 36 + init_stock: 320 + price: 36 + service_level: 0.96 + vlt: 2 + SKU69: + cost: 329 + init_stock: 560 + price: 329 + service_level: 0.96 + vlt: 1 + SKU690: + cost: 283 + init_stock: 140 + price: 283 + service_level: 0.96 + vlt: 2 + SKU691: + cost: 422 + init_stock: 1960 + price: 422 + service_level: 0.96 + vlt: 1 + SKU692: + cost: 457 + init_stock: 1300 + price: 457 + service_level: 0.96 + vlt: 3 + SKU693: + cost: 351 + init_stock: 960 + price: 351 + service_level: 0.96 + vlt: 1 + SKU694: + cost: 441 + init_stock: 640 + price: 441 + service_level: 0.96 + vlt: 3 + SKU695: + cost: 344 + init_stock: 1960 + price: 344 + service_level: 0.96 + vlt: 2 + SKU696: + cost: 378 + init_stock: 700 + price: 378 + service_level: 0.96 + vlt: 1 + SKU697: + cost: 483 + init_stock: 1840 + price: 483 + service_level: 0.96 + vlt: 1 + SKU698: + cost: 53 + init_stock: 1000 + price: 53 + service_level: 0.96 + vlt: 3 + SKU699: + cost: 265 + init_stock: 1280 + price: 265 + service_level: 0.96 + vlt: 2 + SKU7: + cost: 389 + init_stock: 240 + price: 389 + service_level: 0.96 + vlt: 2 + SKU70: + cost: 140 + init_stock: 760 + price: 140 + service_level: 0.96 + vlt: 2 + SKU700: + cost: 122 + init_stock: 400 + price: 122 + service_level: 0.96 + vlt: 3 + SKU701: + cost: 394 + init_stock: 760 + price: 394 + service_level: 0.96 + vlt: 2 + SKU702: + cost: 78 + init_stock: 540 + price: 78 + service_level: 0.96 + vlt: 1 + SKU703: + cost: 472 + init_stock: 1420 + price: 472 + service_level: 0.96 + vlt: 3 + SKU704: + cost: 384 + init_stock: 880 + price: 384 + service_level: 0.96 + vlt: 2 + SKU705: + cost: 486 + init_stock: 1120 + price: 486 + service_level: 0.96 + vlt: 2 + SKU706: + cost: 222 + init_stock: 740 + price: 222 + service_level: 0.96 + vlt: 1 + SKU707: + cost: 195 + init_stock: 740 + price: 195 + service_level: 0.96 + vlt: 2 + SKU708: + cost: 131 + init_stock: 1980 + price: 131 + service_level: 0.96 + vlt: 1 + SKU709: + cost: 137 + init_stock: 700 + price: 137 + service_level: 0.96 + vlt: 3 + SKU71: + cost: 381 + init_stock: 740 + price: 381 + service_level: 0.96 + vlt: 2 + SKU710: + cost: 186 + init_stock: 280 + price: 186 + service_level: 0.96 + vlt: 3 + SKU711: + cost: 190 + init_stock: 1220 + price: 190 + service_level: 0.96 + vlt: 2 + SKU712: + cost: 384 + init_stock: 720 + price: 384 + service_level: 0.96 + vlt: 2 + SKU713: + cost: 229 + init_stock: 1380 + price: 229 + service_level: 0.96 + vlt: 3 + SKU714: + cost: 364 + init_stock: 1800 + price: 364 + service_level: 0.96 + vlt: 1 + SKU715: + cost: 235 + init_stock: 180 + price: 235 + service_level: 0.96 + vlt: 2 + SKU716: + cost: 108 + init_stock: 1780 + price: 108 + service_level: 0.96 + vlt: 3 + SKU717: + cost: 12 + init_stock: 240 + price: 12 + service_level: 0.96 + vlt: 3 + SKU718: + cost: 397 + init_stock: 440 + price: 397 + service_level: 0.96 + vlt: 3 + SKU719: + cost: 21 + init_stock: 740 + price: 21 + service_level: 0.96 + vlt: 1 + SKU72: + cost: 228 + init_stock: 1940 + price: 228 + service_level: 0.96 + vlt: 1 + SKU720: + cost: 120 + init_stock: 960 + price: 120 + service_level: 0.96 + vlt: 2 + SKU721: + cost: 265 + init_stock: 1660 + price: 265 + service_level: 0.96 + vlt: 1 + SKU722: + cost: 353 + init_stock: 1160 + price: 353 + service_level: 0.96 + vlt: 2 + SKU723: + cost: 144 + init_stock: 1700 + price: 144 + service_level: 0.96 + vlt: 2 + SKU724: + cost: 120 + init_stock: 1420 + price: 120 + service_level: 0.96 + vlt: 1 + SKU725: + cost: 42 + init_stock: 1780 + price: 42 + service_level: 0.96 + vlt: 3 + SKU726: + cost: 132 + init_stock: 1740 + price: 132 + service_level: 0.96 + vlt: 3 + SKU727: + cost: 103 + init_stock: 700 + price: 103 + service_level: 0.96 + vlt: 2 + SKU728: + cost: 29 + init_stock: 680 + price: 29 + service_level: 0.96 + vlt: 3 + SKU729: + cost: 251 + init_stock: 1940 + price: 251 + service_level: 0.96 + vlt: 2 + SKU73: + cost: 334 + init_stock: 1460 + price: 334 + service_level: 0.96 + vlt: 3 + SKU730: + cost: 71 + init_stock: 1280 + price: 71 + service_level: 0.96 + vlt: 3 + SKU731: + cost: 480 + init_stock: 1840 + price: 480 + service_level: 0.96 + vlt: 1 + SKU732: + cost: 231 + init_stock: 1340 + price: 231 + service_level: 0.96 + vlt: 3 + SKU733: + cost: 245 + init_stock: 560 + price: 245 + service_level: 0.96 + vlt: 2 + SKU734: + cost: 117 + init_stock: 1160 + price: 117 + service_level: 0.96 + vlt: 1 + SKU735: + cost: 197 + init_stock: 520 + price: 197 + service_level: 0.96 + vlt: 2 + SKU736: + cost: 221 + init_stock: 1240 + price: 221 + service_level: 0.96 + vlt: 3 + SKU737: + cost: 260 + init_stock: 1700 + price: 260 + service_level: 0.96 + vlt: 2 + SKU738: + cost: 489 + init_stock: 920 + price: 489 + service_level: 0.96 + vlt: 3 + SKU739: + cost: 451 + init_stock: 760 + price: 451 + service_level: 0.96 + vlt: 3 + SKU74: + cost: 363 + init_stock: 200 + price: 363 + service_level: 0.96 + vlt: 3 + SKU740: + cost: 250 + init_stock: 100 + price: 250 + service_level: 0.96 + vlt: 1 + SKU741: + cost: 139 + init_stock: 340 + price: 139 + service_level: 0.96 + vlt: 3 + SKU742: + cost: 174 + init_stock: 1780 + price: 174 + service_level: 0.96 + vlt: 3 + SKU743: + cost: 79 + init_stock: 1640 + price: 79 + service_level: 0.96 + vlt: 3 + SKU744: + cost: 304 + init_stock: 780 + price: 304 + service_level: 0.96 + vlt: 3 + SKU745: + cost: 105 + init_stock: 1860 + price: 105 + service_level: 0.96 + vlt: 2 + SKU746: + cost: 397 + init_stock: 1880 + price: 397 + service_level: 0.96 + vlt: 1 + SKU747: + cost: 426 + init_stock: 720 + price: 426 + service_level: 0.96 + vlt: 1 + SKU748: + cost: 169 + init_stock: 940 + price: 169 + service_level: 0.96 + vlt: 3 + SKU749: + cost: 433 + init_stock: 840 + price: 433 + service_level: 0.96 + vlt: 2 + SKU75: + cost: 179 + init_stock: 1300 + price: 179 + service_level: 0.96 + vlt: 1 + SKU750: + cost: 373 + init_stock: 1360 + price: 373 + service_level: 0.96 + vlt: 2 + SKU751: + cost: 196 + init_stock: 680 + price: 196 + service_level: 0.96 + vlt: 2 + SKU752: + cost: 67 + init_stock: 200 + price: 67 + service_level: 0.96 + vlt: 1 + SKU753: + cost: 162 + init_stock: 1220 + price: 162 + service_level: 0.96 + vlt: 1 + SKU754: + cost: 206 + init_stock: 1280 + price: 206 + service_level: 0.96 + vlt: 1 + SKU755: + cost: 125 + init_stock: 860 + price: 125 + service_level: 0.96 + vlt: 1 + SKU756: + cost: 215 + init_stock: 1640 + price: 215 + service_level: 0.96 + vlt: 1 + SKU757: + cost: 263 + init_stock: 440 + price: 263 + service_level: 0.96 + vlt: 3 + SKU758: + cost: 70 + init_stock: 1740 + price: 70 + service_level: 0.96 + vlt: 3 + SKU759: + cost: 310 + init_stock: 1580 + price: 310 + service_level: 0.96 + vlt: 3 + SKU76: + cost: 464 + init_stock: 1680 + price: 464 + service_level: 0.96 + vlt: 3 + SKU760: + cost: 487 + init_stock: 440 + price: 487 + service_level: 0.96 + vlt: 2 + SKU761: + cost: 288 + init_stock: 620 + price: 288 + service_level: 0.96 + vlt: 2 + SKU762: + cost: 374 + init_stock: 1240 + price: 374 + service_level: 0.96 + vlt: 1 + SKU763: + cost: 121 + init_stock: 120 + price: 121 + service_level: 0.96 + vlt: 1 + SKU764: + cost: 494 + init_stock: 760 + price: 494 + service_level: 0.96 + vlt: 1 + SKU765: + cost: 459 + init_stock: 1780 + price: 459 + service_level: 0.96 + vlt: 1 + SKU766: + cost: 249 + init_stock: 1080 + price: 249 + service_level: 0.96 + vlt: 1 + SKU767: + cost: 245 + init_stock: 1760 + price: 245 + service_level: 0.96 + vlt: 1 + SKU768: + cost: 429 + init_stock: 1700 + price: 429 + service_level: 0.96 + vlt: 3 + SKU769: + cost: 324 + init_stock: 960 + price: 324 + service_level: 0.96 + vlt: 2 + SKU77: + cost: 281 + init_stock: 560 + price: 281 + service_level: 0.96 + vlt: 3 + SKU770: + cost: 362 + init_stock: 1520 + price: 362 + service_level: 0.96 + vlt: 1 + SKU771: + cost: 233 + init_stock: 540 + price: 233 + service_level: 0.96 + vlt: 1 + SKU772: + cost: 152 + init_stock: 320 + price: 152 + service_level: 0.96 + vlt: 1 + SKU773: + cost: 337 + init_stock: 1920 + price: 337 + service_level: 0.96 + vlt: 3 + SKU774: + cost: 232 + init_stock: 180 + price: 232 + service_level: 0.96 + vlt: 3 + SKU775: + cost: 393 + init_stock: 240 + price: 393 + service_level: 0.96 + vlt: 3 + SKU776: + cost: 342 + init_stock: 960 + price: 342 + service_level: 0.96 + vlt: 3 + SKU777: + cost: 262 + init_stock: 1180 + price: 262 + service_level: 0.96 + vlt: 2 + SKU778: + cost: 327 + init_stock: 1480 + price: 327 + service_level: 0.96 + vlt: 2 + SKU779: + cost: 109 + init_stock: 880 + price: 109 + service_level: 0.96 + vlt: 3 + SKU78: + cost: 486 + init_stock: 740 + price: 486 + service_level: 0.96 + vlt: 3 + SKU780: + cost: 153 + init_stock: 500 + price: 153 + service_level: 0.96 + vlt: 1 + SKU781: + cost: 126 + init_stock: 140 + price: 126 + service_level: 0.96 + vlt: 1 + SKU782: + cost: 258 + init_stock: 1840 + price: 258 + service_level: 0.96 + vlt: 2 + SKU783: + cost: 214 + init_stock: 1400 + price: 214 + service_level: 0.96 + vlt: 1 + SKU784: + cost: 426 + init_stock: 700 + price: 426 + service_level: 0.96 + vlt: 2 + SKU785: + cost: 486 + init_stock: 580 + price: 486 + service_level: 0.96 + vlt: 3 + SKU786: + cost: 109 + init_stock: 1040 + price: 109 + service_level: 0.96 + vlt: 1 + SKU787: + cost: 74 + init_stock: 420 + price: 74 + service_level: 0.96 + vlt: 1 + SKU788: + cost: 25 + init_stock: 840 + price: 25 + service_level: 0.96 + vlt: 2 + SKU789: + cost: 265 + init_stock: 780 + price: 265 + service_level: 0.96 + vlt: 2 + SKU79: + cost: 486 + init_stock: 840 + price: 486 + service_level: 0.96 + vlt: 3 + SKU790: + cost: 222 + init_stock: 560 + price: 222 + service_level: 0.96 + vlt: 1 + SKU791: + cost: 495 + init_stock: 1080 + price: 495 + service_level: 0.96 + vlt: 3 + SKU792: + cost: 410 + init_stock: 640 + price: 410 + service_level: 0.96 + vlt: 3 + SKU793: + cost: 442 + init_stock: 1600 + price: 442 + service_level: 0.96 + vlt: 1 + SKU794: + cost: 222 + init_stock: 120 + price: 222 + service_level: 0.96 + vlt: 3 + SKU795: + cost: 279 + init_stock: 1160 + price: 279 + service_level: 0.96 + vlt: 2 + SKU796: + cost: 122 + init_stock: 520 + price: 122 + service_level: 0.96 + vlt: 2 + SKU797: + cost: 107 + init_stock: 2000 + price: 107 + service_level: 0.96 + vlt: 1 + SKU798: + cost: 383 + init_stock: 1000 + price: 383 + service_level: 0.96 + vlt: 3 + SKU799: + cost: 119 + init_stock: 700 + price: 119 + service_level: 0.96 + vlt: 3 + SKU8: + cost: 423 + init_stock: 1700 + price: 423 + service_level: 0.96 + vlt: 2 + SKU80: + cost: 324 + init_stock: 720 + price: 324 + service_level: 0.96 + vlt: 3 + SKU800: + cost: 316 + init_stock: 1780 + price: 316 + service_level: 0.96 + vlt: 3 + SKU801: + cost: 65 + init_stock: 1680 + price: 65 + service_level: 0.96 + vlt: 3 + SKU802: + cost: 133 + init_stock: 1880 + price: 133 + service_level: 0.96 + vlt: 2 + SKU803: + cost: 209 + init_stock: 700 + price: 209 + service_level: 0.96 + vlt: 3 + SKU804: + cost: 273 + init_stock: 1700 + price: 273 + service_level: 0.96 + vlt: 3 + SKU805: + cost: 304 + init_stock: 760 + price: 304 + service_level: 0.96 + vlt: 2 + SKU806: + cost: 232 + init_stock: 180 + price: 232 + service_level: 0.96 + vlt: 2 + SKU807: + cost: 169 + init_stock: 1740 + price: 169 + service_level: 0.96 + vlt: 3 + SKU808: + cost: 350 + init_stock: 180 + price: 350 + service_level: 0.96 + vlt: 2 + SKU809: + cost: 85 + init_stock: 1940 + price: 85 + service_level: 0.96 + vlt: 1 + SKU81: + cost: 319 + init_stock: 1200 + price: 319 + service_level: 0.96 + vlt: 3 + SKU810: + cost: 51 + init_stock: 1020 + price: 51 + service_level: 0.96 + vlt: 3 + SKU811: + cost: 53 + init_stock: 1080 + price: 53 + service_level: 0.96 + vlt: 1 + SKU812: + cost: 141 + init_stock: 1140 + price: 141 + service_level: 0.96 + vlt: 1 + SKU813: + cost: 364 + init_stock: 780 + price: 364 + service_level: 0.96 + vlt: 2 + SKU814: + cost: 79 + init_stock: 1920 + price: 79 + service_level: 0.96 + vlt: 3 + SKU815: + cost: 178 + init_stock: 320 + price: 178 + service_level: 0.96 + vlt: 2 + SKU816: + cost: 352 + init_stock: 1320 + price: 352 + service_level: 0.96 + vlt: 2 + SKU817: + cost: 119 + init_stock: 340 + price: 119 + service_level: 0.96 + vlt: 1 + SKU818: + cost: 110 + init_stock: 260 + price: 110 + service_level: 0.96 + vlt: 3 + SKU819: + cost: 24 + init_stock: 100 + price: 24 + service_level: 0.96 + vlt: 3 + SKU82: + cost: 413 + init_stock: 1400 + price: 413 + service_level: 0.96 + vlt: 3 + SKU820: + cost: 426 + init_stock: 1040 + price: 426 + service_level: 0.96 + vlt: 1 + SKU821: + cost: 229 + init_stock: 1840 + price: 229 + service_level: 0.96 + vlt: 1 + SKU822: + cost: 282 + init_stock: 240 + price: 282 + service_level: 0.96 + vlt: 1 + SKU823: + cost: 221 + init_stock: 1820 + price: 221 + service_level: 0.96 + vlt: 2 + SKU824: + cost: 318 + init_stock: 720 + price: 318 + service_level: 0.96 + vlt: 3 + SKU825: + cost: 287 + init_stock: 1860 + price: 287 + service_level: 0.96 + vlt: 1 + SKU826: + cost: 278 + init_stock: 960 + price: 278 + service_level: 0.96 + vlt: 2 + SKU827: + cost: 312 + init_stock: 1260 + price: 312 + service_level: 0.96 + vlt: 2 + SKU828: + cost: 140 + init_stock: 1860 + price: 140 + service_level: 0.96 + vlt: 3 + SKU829: + cost: 415 + init_stock: 220 + price: 415 + service_level: 0.96 + vlt: 3 + SKU83: + cost: 327 + init_stock: 1540 + price: 327 + service_level: 0.96 + vlt: 3 + SKU830: + cost: 119 + init_stock: 460 + price: 119 + service_level: 0.96 + vlt: 3 + SKU831: + cost: 376 + init_stock: 1400 + price: 376 + service_level: 0.96 + vlt: 3 + SKU832: + cost: 289 + init_stock: 920 + price: 289 + service_level: 0.96 + vlt: 2 + SKU833: + cost: 183 + init_stock: 320 + price: 183 + service_level: 0.96 + vlt: 2 + SKU834: + cost: 398 + init_stock: 1320 + price: 398 + service_level: 0.96 + vlt: 2 + SKU835: + cost: 407 + init_stock: 1480 + price: 407 + service_level: 0.96 + vlt: 1 + SKU836: + cost: 301 + init_stock: 820 + price: 301 + service_level: 0.96 + vlt: 2 + SKU837: + cost: 213 + init_stock: 520 + price: 213 + service_level: 0.96 + vlt: 3 + SKU838: + cost: 23 + init_stock: 840 + price: 23 + service_level: 0.96 + vlt: 2 + SKU839: + cost: 354 + init_stock: 1060 + price: 354 + service_level: 0.96 + vlt: 1 + SKU84: + cost: 217 + init_stock: 640 + price: 217 + service_level: 0.96 + vlt: 1 + SKU840: + cost: 19 + init_stock: 1740 + price: 19 + service_level: 0.96 + vlt: 2 + SKU841: + cost: 362 + init_stock: 1880 + price: 362 + service_level: 0.96 + vlt: 2 + SKU842: + cost: 73 + init_stock: 1820 + price: 73 + service_level: 0.96 + vlt: 2 + SKU843: + cost: 169 + init_stock: 860 + price: 169 + service_level: 0.96 + vlt: 1 + SKU844: + cost: 325 + init_stock: 720 + price: 325 + service_level: 0.96 + vlt: 2 + SKU845: + cost: 163 + init_stock: 300 + price: 163 + service_level: 0.96 + vlt: 3 + SKU846: + cost: 184 + init_stock: 1960 + price: 184 + service_level: 0.96 + vlt: 2 + SKU847: + cost: 441 + init_stock: 680 + price: 441 + service_level: 0.96 + vlt: 1 + SKU848: + cost: 450 + init_stock: 500 + price: 450 + service_level: 0.96 + vlt: 3 + SKU849: + cost: 479 + init_stock: 700 + price: 479 + service_level: 0.96 + vlt: 3 + SKU85: + cost: 49 + init_stock: 1520 + price: 49 + service_level: 0.96 + vlt: 1 + SKU850: + cost: 105 + init_stock: 1460 + price: 105 + service_level: 0.96 + vlt: 3 + SKU851: + cost: 366 + init_stock: 1540 + price: 366 + service_level: 0.96 + vlt: 3 + SKU852: + cost: 392 + init_stock: 1560 + price: 392 + service_level: 0.96 + vlt: 3 + SKU853: + cost: 94 + init_stock: 200 + price: 94 + service_level: 0.96 + vlt: 2 + SKU854: + cost: 110 + init_stock: 1300 + price: 110 + service_level: 0.96 + vlt: 2 + SKU855: + cost: 254 + init_stock: 820 + price: 254 + service_level: 0.96 + vlt: 2 + SKU856: + cost: 435 + init_stock: 580 + price: 435 + service_level: 0.96 + vlt: 2 + SKU857: + cost: 101 + init_stock: 480 + price: 101 + service_level: 0.96 + vlt: 3 + SKU858: + cost: 487 + init_stock: 560 + price: 487 + service_level: 0.96 + vlt: 3 + SKU859: + cost: 496 + init_stock: 1860 + price: 496 + service_level: 0.96 + vlt: 1 + SKU86: + cost: 59 + init_stock: 1500 + price: 59 + service_level: 0.96 + vlt: 1 + SKU860: + cost: 350 + init_stock: 2000 + price: 350 + service_level: 0.96 + vlt: 2 + SKU861: + cost: 295 + init_stock: 500 + price: 295 + service_level: 0.96 + vlt: 2 + SKU862: + cost: 57 + init_stock: 1460 + price: 57 + service_level: 0.96 + vlt: 2 + SKU863: + cost: 311 + init_stock: 1140 + price: 311 + service_level: 0.96 + vlt: 3 + SKU864: + cost: 96 + init_stock: 1280 + price: 96 + service_level: 0.96 + vlt: 2 + SKU865: + cost: 107 + init_stock: 1160 + price: 107 + service_level: 0.96 + vlt: 2 + SKU866: + cost: 397 + init_stock: 420 + price: 397 + service_level: 0.96 + vlt: 3 + SKU867: + cost: 478 + init_stock: 1280 + price: 478 + service_level: 0.96 + vlt: 3 + SKU868: + cost: 343 + init_stock: 420 + price: 343 + service_level: 0.96 + vlt: 2 + SKU869: + cost: 168 + init_stock: 1220 + price: 168 + service_level: 0.96 + vlt: 3 + SKU87: + cost: 93 + init_stock: 1420 + price: 93 + service_level: 0.96 + vlt: 1 + SKU870: + cost: 257 + init_stock: 1260 + price: 257 + service_level: 0.96 + vlt: 3 + SKU871: + cost: 68 + init_stock: 400 + price: 68 + service_level: 0.96 + vlt: 3 + SKU872: + cost: 475 + init_stock: 1420 + price: 475 + service_level: 0.96 + vlt: 1 + SKU873: + cost: 388 + init_stock: 1300 + price: 388 + service_level: 0.96 + vlt: 3 + SKU874: + cost: 267 + init_stock: 120 + price: 267 + service_level: 0.96 + vlt: 2 + SKU875: + cost: 332 + init_stock: 700 + price: 332 + service_level: 0.96 + vlt: 2 + SKU876: + cost: 392 + init_stock: 800 + price: 392 + service_level: 0.96 + vlt: 3 + SKU877: + cost: 374 + init_stock: 1820 + price: 374 + service_level: 0.96 + vlt: 2 + SKU878: + cost: 297 + init_stock: 580 + price: 297 + service_level: 0.96 + vlt: 3 + SKU879: + cost: 497 + init_stock: 1560 + price: 497 + service_level: 0.96 + vlt: 3 + SKU88: + cost: 19 + init_stock: 840 + price: 19 + service_level: 0.96 + vlt: 2 + SKU880: + cost: 199 + init_stock: 940 + price: 199 + service_level: 0.96 + vlt: 1 + SKU881: + cost: 328 + init_stock: 1200 + price: 328 + service_level: 0.96 + vlt: 1 + SKU882: + cost: 307 + init_stock: 1800 + price: 307 + service_level: 0.96 + vlt: 2 + SKU883: + cost: 16 + init_stock: 560 + price: 16 + service_level: 0.96 + vlt: 3 + SKU884: + cost: 329 + init_stock: 1340 + price: 329 + service_level: 0.96 + vlt: 2 + SKU885: + cost: 176 + init_stock: 1420 + price: 176 + service_level: 0.96 + vlt: 2 + SKU886: + cost: 48 + init_stock: 1780 + price: 48 + service_level: 0.96 + vlt: 2 + SKU887: + cost: 241 + init_stock: 300 + price: 241 + service_level: 0.96 + vlt: 1 + SKU888: + cost: 114 + init_stock: 1060 + price: 114 + service_level: 0.96 + vlt: 3 + SKU889: + cost: 261 + init_stock: 1900 + price: 261 + service_level: 0.96 + vlt: 1 + SKU89: + cost: 134 + init_stock: 1620 + price: 134 + service_level: 0.96 + vlt: 2 + SKU890: + cost: 322 + init_stock: 1420 + price: 322 + service_level: 0.96 + vlt: 2 + SKU891: + cost: 347 + init_stock: 1140 + price: 347 + service_level: 0.96 + vlt: 1 + SKU892: + cost: 474 + init_stock: 1680 + price: 474 + service_level: 0.96 + vlt: 1 + SKU893: + cost: 433 + init_stock: 1960 + price: 433 + service_level: 0.96 + vlt: 1 + SKU894: + cost: 399 + init_stock: 520 + price: 399 + service_level: 0.96 + vlt: 2 + SKU895: + cost: 335 + init_stock: 960 + price: 335 + service_level: 0.96 + vlt: 3 + SKU896: + cost: 92 + init_stock: 1100 + price: 92 + service_level: 0.96 + vlt: 3 + SKU897: + cost: 404 + init_stock: 440 + price: 404 + service_level: 0.96 + vlt: 2 + SKU898: + cost: 72 + init_stock: 1960 + price: 72 + service_level: 0.96 + vlt: 3 + SKU899: + cost: 91 + init_stock: 1780 + price: 91 + service_level: 0.96 + vlt: 2 + SKU9: + cost: 231 + init_stock: 420 + price: 231 + service_level: 0.96 + vlt: 2 + SKU90: + cost: 38 + init_stock: 600 + price: 38 + service_level: 0.96 + vlt: 1 + SKU900: + cost: 12 + init_stock: 1180 + price: 12 + service_level: 0.96 + vlt: 2 + SKU901: + cost: 307 + init_stock: 1560 + price: 307 + service_level: 0.96 + vlt: 3 + SKU902: + cost: 354 + init_stock: 1120 + price: 354 + service_level: 0.96 + vlt: 1 + SKU903: + cost: 14 + init_stock: 1860 + price: 14 + service_level: 0.96 + vlt: 3 + SKU904: + cost: 300 + init_stock: 1940 + price: 300 + service_level: 0.96 + vlt: 1 + SKU905: + cost: 134 + init_stock: 1020 + price: 134 + service_level: 0.96 + vlt: 2 + SKU906: + cost: 424 + init_stock: 1760 + price: 424 + service_level: 0.96 + vlt: 3 + SKU907: + cost: 360 + init_stock: 640 + price: 360 + service_level: 0.96 + vlt: 2 + SKU908: + cost: 361 + init_stock: 780 + price: 361 + service_level: 0.96 + vlt: 2 + SKU909: + cost: 65 + init_stock: 1380 + price: 65 + service_level: 0.96 + vlt: 3 + SKU91: + cost: 223 + init_stock: 860 + price: 223 + service_level: 0.96 + vlt: 1 + SKU910: + cost: 161 + init_stock: 200 + price: 161 + service_level: 0.96 + vlt: 3 + SKU911: + cost: 186 + init_stock: 1000 + price: 186 + service_level: 0.96 + vlt: 3 + SKU912: + cost: 235 + init_stock: 1080 + price: 235 + service_level: 0.96 + vlt: 1 + SKU913: + cost: 122 + init_stock: 1360 + price: 122 + service_level: 0.96 + vlt: 2 + SKU914: + cost: 436 + init_stock: 320 + price: 436 + service_level: 0.96 + vlt: 2 + SKU915: + cost: 233 + init_stock: 460 + price: 233 + service_level: 0.96 + vlt: 2 + SKU916: + cost: 26 + init_stock: 980 + price: 26 + service_level: 0.96 + vlt: 3 + SKU917: + cost: 141 + init_stock: 1960 + price: 141 + service_level: 0.96 + vlt: 3 + SKU918: + cost: 113 + init_stock: 1780 + price: 113 + service_level: 0.96 + vlt: 3 + SKU919: + cost: 68 + init_stock: 1740 + price: 68 + service_level: 0.96 + vlt: 3 + SKU92: + cost: 325 + init_stock: 760 + price: 325 + service_level: 0.96 + vlt: 1 + SKU920: + cost: 428 + init_stock: 420 + price: 428 + service_level: 0.96 + vlt: 1 + SKU921: + cost: 491 + init_stock: 1520 + price: 491 + service_level: 0.96 + vlt: 2 + SKU922: + cost: 10 + init_stock: 1020 + price: 10 + service_level: 0.96 + vlt: 1 + SKU923: + cost: 80 + init_stock: 200 + price: 80 + service_level: 0.96 + vlt: 1 + SKU924: + cost: 164 + init_stock: 1520 + price: 164 + service_level: 0.96 + vlt: 3 + SKU925: + cost: 221 + init_stock: 1220 + price: 221 + service_level: 0.96 + vlt: 3 + SKU926: + cost: 300 + init_stock: 140 + price: 300 + service_level: 0.96 + vlt: 1 + SKU927: + cost: 128 + init_stock: 1260 + price: 128 + service_level: 0.96 + vlt: 2 + SKU928: + cost: 126 + init_stock: 160 + price: 126 + service_level: 0.96 + vlt: 1 + SKU929: + cost: 377 + init_stock: 300 + price: 377 + service_level: 0.96 + vlt: 1 + SKU93: + cost: 441 + init_stock: 180 + price: 441 + service_level: 0.96 + vlt: 2 + SKU930: + cost: 270 + init_stock: 1980 + price: 270 + service_level: 0.96 + vlt: 1 + SKU931: + cost: 412 + init_stock: 1300 + price: 412 + service_level: 0.96 + vlt: 3 + SKU932: + cost: 118 + init_stock: 960 + price: 118 + service_level: 0.96 + vlt: 3 + SKU933: + cost: 54 + init_stock: 1820 + price: 54 + service_level: 0.96 + vlt: 1 + SKU934: + cost: 347 + init_stock: 780 + price: 347 + service_level: 0.96 + vlt: 3 + SKU935: + cost: 219 + init_stock: 340 + price: 219 + service_level: 0.96 + vlt: 1 + SKU936: + cost: 283 + init_stock: 1540 + price: 283 + service_level: 0.96 + vlt: 1 + SKU937: + cost: 457 + init_stock: 1460 + price: 457 + service_level: 0.96 + vlt: 1 + SKU938: + cost: 450 + init_stock: 1800 + price: 450 + service_level: 0.96 + vlt: 2 + SKU939: + cost: 67 + init_stock: 540 + price: 67 + service_level: 0.96 + vlt: 1 + SKU94: + cost: 279 + init_stock: 340 + price: 279 + service_level: 0.96 + vlt: 1 + SKU940: + cost: 65 + init_stock: 580 + price: 65 + service_level: 0.96 + vlt: 1 + SKU941: + cost: 213 + init_stock: 100 + price: 213 + service_level: 0.96 + vlt: 3 + SKU942: + cost: 151 + init_stock: 540 + price: 151 + service_level: 0.96 + vlt: 3 + SKU943: + cost: 85 + init_stock: 1000 + price: 85 + service_level: 0.96 + vlt: 3 + SKU944: + cost: 154 + init_stock: 120 + price: 154 + service_level: 0.96 + vlt: 3 + SKU945: + cost: 267 + init_stock: 300 + price: 267 + service_level: 0.96 + vlt: 3 + SKU946: + cost: 397 + init_stock: 180 + price: 397 + service_level: 0.96 + vlt: 3 + SKU947: + cost: 275 + init_stock: 820 + price: 275 + service_level: 0.96 + vlt: 2 + SKU948: + cost: 472 + init_stock: 1760 + price: 472 + service_level: 0.96 + vlt: 3 + SKU949: + cost: 275 + init_stock: 1960 + price: 275 + service_level: 0.96 + vlt: 1 + SKU95: + cost: 183 + init_stock: 1020 + price: 183 + service_level: 0.96 + vlt: 1 + SKU950: + cost: 374 + init_stock: 500 + price: 374 + service_level: 0.96 + vlt: 3 + SKU951: + cost: 272 + init_stock: 640 + price: 272 + service_level: 0.96 + vlt: 3 + SKU952: + cost: 194 + init_stock: 1260 + price: 194 + service_level: 0.96 + vlt: 2 + SKU953: + cost: 428 + init_stock: 200 + price: 428 + service_level: 0.96 + vlt: 2 + SKU954: + cost: 272 + init_stock: 320 + price: 272 + service_level: 0.96 + vlt: 1 + SKU955: + cost: 84 + init_stock: 1840 + price: 84 + service_level: 0.96 + vlt: 1 + SKU956: + cost: 363 + init_stock: 700 + price: 363 + service_level: 0.96 + vlt: 2 + SKU957: + cost: 221 + init_stock: 1800 + price: 221 + service_level: 0.96 + vlt: 1 + SKU958: + cost: 335 + init_stock: 720 + price: 335 + service_level: 0.96 + vlt: 1 + SKU959: + cost: 427 + init_stock: 1420 + price: 427 + service_level: 0.96 + vlt: 3 + SKU96: + cost: 320 + init_stock: 220 + price: 320 + service_level: 0.96 + vlt: 1 + SKU960: + cost: 187 + init_stock: 1540 + price: 187 + service_level: 0.96 + vlt: 1 + SKU961: + cost: 90 + init_stock: 1320 + price: 90 + service_level: 0.96 + vlt: 1 + SKU962: + cost: 70 + init_stock: 1960 + price: 70 + service_level: 0.96 + vlt: 1 + SKU963: + cost: 184 + init_stock: 740 + price: 184 + service_level: 0.96 + vlt: 3 + SKU964: + cost: 180 + init_stock: 200 + price: 180 + service_level: 0.96 + vlt: 2 + SKU965: + cost: 441 + init_stock: 320 + price: 441 + service_level: 0.96 + vlt: 3 + SKU966: + cost: 214 + init_stock: 640 + price: 214 + service_level: 0.96 + vlt: 2 + SKU967: + cost: 71 + init_stock: 1240 + price: 71 + service_level: 0.96 + vlt: 3 + SKU968: + cost: 142 + init_stock: 100 + price: 142 + service_level: 0.96 + vlt: 1 + SKU969: + cost: 435 + init_stock: 1160 + price: 435 + service_level: 0.96 + vlt: 2 + SKU97: + cost: 15 + init_stock: 540 + price: 15 + service_level: 0.96 + vlt: 2 + SKU970: + cost: 392 + init_stock: 1840 + price: 392 + service_level: 0.96 + vlt: 1 + SKU971: + cost: 408 + init_stock: 260 + price: 408 + service_level: 0.96 + vlt: 2 + SKU972: + cost: 22 + init_stock: 400 + price: 22 + service_level: 0.96 + vlt: 3 + SKU973: + cost: 476 + init_stock: 1780 + price: 476 + service_level: 0.96 + vlt: 3 + SKU974: + cost: 356 + init_stock: 1000 + price: 356 + service_level: 0.96 + vlt: 2 + SKU975: + cost: 299 + init_stock: 340 + price: 299 + service_level: 0.96 + vlt: 3 + SKU976: + cost: 369 + init_stock: 700 + price: 369 + service_level: 0.96 + vlt: 1 + SKU977: + cost: 142 + init_stock: 1060 + price: 142 + service_level: 0.96 + vlt: 1 + SKU978: + cost: 242 + init_stock: 1960 + price: 242 + service_level: 0.96 + vlt: 1 + SKU979: + cost: 219 + init_stock: 1980 + price: 219 + service_level: 0.96 + vlt: 1 + SKU98: + cost: 167 + init_stock: 1560 + price: 167 + service_level: 0.96 + vlt: 2 + SKU980: + cost: 363 + init_stock: 1640 + price: 363 + service_level: 0.96 + vlt: 1 + SKU981: + cost: 416 + init_stock: 1680 + price: 416 + service_level: 0.96 + vlt: 3 + SKU982: + cost: 163 + init_stock: 460 + price: 163 + service_level: 0.96 + vlt: 3 + SKU983: + cost: 151 + init_stock: 1200 + price: 151 + service_level: 0.96 + vlt: 2 + SKU984: + cost: 123 + init_stock: 1680 + price: 123 + service_level: 0.96 + vlt: 3 + SKU985: + cost: 114 + init_stock: 820 + price: 114 + service_level: 0.96 + vlt: 1 + SKU986: + cost: 107 + init_stock: 1520 + price: 107 + service_level: 0.96 + vlt: 3 + SKU987: + cost: 56 + init_stock: 1860 + price: 56 + service_level: 0.96 + vlt: 3 + SKU988: + cost: 66 + init_stock: 780 + price: 66 + service_level: 0.96 + vlt: 3 + SKU989: + cost: 472 + init_stock: 1340 + price: 472 + service_level: 0.96 + vlt: 2 + SKU99: + cost: 31 + init_stock: 1780 + price: 31 + service_level: 0.96 + vlt: 2 + SKU990: + cost: 14 + init_stock: 940 + price: 14 + service_level: 0.96 + vlt: 3 + SKU991: + cost: 450 + init_stock: 980 + price: 450 + service_level: 0.96 + vlt: 2 + SKU992: + cost: 18 + init_stock: 1220 + price: 18 + service_level: 0.96 + vlt: 3 + SKU993: + cost: 350 + init_stock: 940 + price: 350 + service_level: 0.96 + vlt: 1 + SKU994: + cost: 45 + init_stock: 1820 + price: 45 + service_level: 0.96 + vlt: 2 + SKU995: + cost: 406 + init_stock: 1720 + price: 406 + service_level: 0.96 + vlt: 1 + SKU996: + cost: 472 + init_stock: 960 + price: 472 + service_level: 0.96 + vlt: 3 + SKU997: + cost: 348 + init_stock: 540 + price: 348 + service_level: 0.96 + vlt: 2 + SKU998: + cost: 239 + init_stock: 300 + price: 239 + service_level: 0.96 + vlt: 1 + SKU999: + cost: 435 + init_stock: 2000 + price: 435 + service_level: 0.96 + vlt: 2 + - children: + storage: + config: + capacity: 1027820 + unit_storage_cost: 1 + config: + order_cost: 500 + definition_ref: RetailerFacility + name: STORE0 + skus: + SKU0: + constraint: G(stock_constraint) + cost: 405 + init_stock: 340 + max_stock: 1000 + price: 498 + sale_gamma: 85 + service_level: 0.95 + SKU1: + constraint: null + cost: 42 + init_stock: 126 + max_stock: 1000 + price: 49 + sale_gamma: 42 + service_level: 0.95 + SKU10: + constraint: null + cost: 125 + init_stock: 273 + max_stock: 1000 + price: 191 + sale_gamma: 91 + service_level: 0.95 + SKU100: + constraint: G(low_profit -> low_stock_constraint) + cost: 338 + init_stock: 552 + max_stock: 1000 + price: 469 + sale_gamma: 92 + service_level: 0.95 + SKU101: + constraint: G(low_profit -> low_stock_constraint) + cost: 436 + init_stock: 276 + max_stock: 1000 + price: 536 + sale_gamma: 69 + service_level: 0.95 + SKU102: + constraint: null + cost: 90 + init_stock: 335 + max_stock: 1000 + price: 119 + sale_gamma: 67 + service_level: 0.95 + SKU103: + constraint: null + cost: 161 + init_stock: 324 + max_stock: 1000 + price: 251 + sale_gamma: 81 + service_level: 0.95 + SKU104: + constraint: G(stock_constraint) + cost: 470 + init_stock: 240 + max_stock: 1000 + price: 531 + sale_gamma: 40 + service_level: 0.95 + SKU105: + constraint: G(stock_constraint) + cost: 214 + init_stock: 162 + max_stock: 1000 + price: 316 + sale_gamma: 54 + service_level: 0.95 + SKU106: + constraint: null + cost: 37 + init_stock: 255 + max_stock: 1000 + price: 49 + sale_gamma: 51 + service_level: 0.95 + SKU107: + constraint: null + cost: 64 + init_stock: 305 + max_stock: 1000 + price: 81 + sale_gamma: 61 + service_level: 0.95 + SKU108: + constraint: G(low_profit -> low_stock_constraint) + cost: 474 + init_stock: 180 + max_stock: 1000 + price: 639 + sale_gamma: 30 + service_level: 0.95 + SKU109: + constraint: null + cost: 398 + init_stock: 255 + max_stock: 1000 + price: 537 + sale_gamma: 51 + service_level: 0.95 + SKU11: + constraint: null + cost: 106 + init_stock: 440 + max_stock: 1000 + price: 184 + sale_gamma: 88 + service_level: 0.95 + SKU110: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 71 + init_stock: 200 + max_stock: 1000 + price: 79 + sale_gamma: 25 + service_level: 0.95 + SKU111: + constraint: null + cost: 183 + init_stock: 250 + max_stock: 1000 + price: 223 + sale_gamma: 50 + service_level: 0.95 + SKU112: + constraint: null + cost: 78 + init_stock: 33 + max_stock: 1000 + price: 114 + sale_gamma: 11 + service_level: 0.95 + SKU113: + constraint: null + cost: 289 + init_stock: 318 + max_stock: 1000 + price: 338 + sale_gamma: 53 + service_level: 0.95 + SKU114: + constraint: null + cost: 333 + init_stock: 325 + max_stock: 1000 + price: 509 + sale_gamma: 65 + service_level: 0.95 + SKU115: + constraint: G(low_profit -> low_stock_constraint) + cost: 437 + init_stock: 75 + max_stock: 1000 + price: 777 + sale_gamma: 25 + service_level: 0.95 + SKU116: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 408 + max_stock: 1000 + price: 353 + sale_gamma: 68 + service_level: 0.95 + SKU117: + constraint: G(stock_constraint) + cost: 123 + init_stock: 132 + max_stock: 1000 + price: 233 + sale_gamma: 22 + service_level: 0.95 + SKU118: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 181 + init_stock: 196 + max_stock: 1000 + price: 289 + sale_gamma: 49 + service_level: 0.95 + SKU119: + constraint: null + cost: 58 + init_stock: 20 + max_stock: 1000 + price: 70 + sale_gamma: 5 + service_level: 0.95 + SKU12: + constraint: null + cost: 315 + init_stock: 304 + max_stock: 1000 + price: 441 + sale_gamma: 76 + service_level: 0.95 + SKU120: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 96 + max_stock: 1000 + price: 43 + sale_gamma: 12 + service_level: 0.95 + SKU121: + constraint: G(stock_constraint) + cost: 255 + init_stock: 469 + max_stock: 1000 + price: 433 + sale_gamma: 67 + service_level: 0.95 + SKU122: + constraint: null + cost: 222 + init_stock: 148 + max_stock: 1000 + price: 326 + sale_gamma: 37 + service_level: 0.95 + SKU123: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 405 + init_stock: 372 + max_stock: 1000 + price: 797 + sale_gamma: 62 + service_level: 0.95 + SKU124: + constraint: null + cost: 169 + init_stock: 72 + max_stock: 1000 + price: 327 + sale_gamma: 18 + service_level: 0.95 + SKU125: + constraint: G(low_profit -> low_stock_constraint) + cost: 344 + init_stock: 140 + max_stock: 1000 + price: 646 + sale_gamma: 35 + service_level: 0.95 + SKU126: + constraint: G(stock_constraint) + cost: 362 + init_stock: 188 + max_stock: 1000 + price: 658 + sale_gamma: 94 + service_level: 0.95 + SKU127: + constraint: G(stock_constraint) + cost: 149 + init_stock: 124 + max_stock: 1000 + price: 289 + sale_gamma: 31 + service_level: 0.95 + SKU128: + constraint: null + cost: 215 + init_stock: 522 + max_stock: 1000 + price: 242 + sale_gamma: 87 + service_level: 0.95 + SKU129: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 83 + init_stock: 371 + max_stock: 1000 + price: 93 + sale_gamma: 53 + service_level: 0.95 + SKU13: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 474 + init_stock: 135 + max_stock: 1000 + price: 810 + sale_gamma: 45 + service_level: 0.95 + SKU130: + constraint: null + cost: 427 + init_stock: 445 + max_stock: 1000 + price: 623 + sale_gamma: 89 + service_level: 0.95 + SKU131: + constraint: null + cost: 58 + init_stock: 294 + max_stock: 1000 + price: 104 + sale_gamma: 49 + service_level: 0.95 + SKU132: + constraint: G(low_profit -> low_stock_constraint) + cost: 304 + init_stock: 90 + max_stock: 1000 + price: 376 + sale_gamma: 15 + service_level: 0.95 + SKU133: + constraint: null + cost: 161 + init_stock: 87 + max_stock: 1000 + price: 252 + sale_gamma: 29 + service_level: 0.95 + SKU134: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 382 + init_stock: 370 + max_stock: 1000 + price: 446 + sale_gamma: 74 + service_level: 0.95 + SKU135: + constraint: null + cost: 126 + init_stock: 616 + max_stock: 1000 + price: 209 + sale_gamma: 88 + service_level: 0.95 + SKU136: + constraint: null + cost: 431 + init_stock: 49 + max_stock: 1000 + price: 659 + sale_gamma: 7 + service_level: 0.95 + SKU137: + constraint: null + cost: 270 + init_stock: 110 + max_stock: 1000 + price: 448 + sale_gamma: 22 + service_level: 0.95 + SKU138: + constraint: G(low_profit -> low_stock_constraint) + cost: 10 + init_stock: 252 + max_stock: 1000 + price: 11 + sale_gamma: 63 + service_level: 0.95 + SKU139: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 259 + init_stock: 574 + max_stock: 1000 + price: 292 + sale_gamma: 82 + service_level: 0.95 + SKU14: + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 728 + max_stock: 1000 + price: 307 + sale_gamma: 91 + service_level: 0.95 + SKU140: + constraint: null + cost: 367 + init_stock: 330 + max_stock: 1000 + price: 554 + sale_gamma: 55 + service_level: 0.95 + SKU141: + constraint: null + cost: 325 + init_stock: 50 + max_stock: 1000 + price: 500 + sale_gamma: 10 + service_level: 0.95 + SKU142: + constraint: null + cost: 439 + init_stock: 567 + max_stock: 1000 + price: 583 + sale_gamma: 81 + service_level: 0.95 + SKU143: + constraint: G(stock_constraint) + cost: 260 + init_stock: 207 + max_stock: 1000 + price: 364 + sale_gamma: 69 + service_level: 0.95 + SKU144: + constraint: null + cost: 457 + init_stock: 30 + max_stock: 1000 + price: 525 + sale_gamma: 6 + service_level: 0.95 + SKU145: + constraint: G(stock_constraint) + cost: 480 + init_stock: 450 + max_stock: 1000 + price: 686 + sale_gamma: 75 + service_level: 0.95 + SKU146: + constraint: null + cost: 243 + init_stock: 576 + max_stock: 1000 + price: 396 + sale_gamma: 96 + service_level: 0.95 + SKU147: + constraint: null + cost: 334 + init_stock: 518 + max_stock: 1000 + price: 534 + sale_gamma: 74 + service_level: 0.95 + SKU148: + constraint: null + cost: 143 + init_stock: 200 + max_stock: 1000 + price: 250 + sale_gamma: 40 + service_level: 0.95 + SKU149: + constraint: G(low_profit -> low_stock_constraint) + cost: 252 + init_stock: 204 + max_stock: 1000 + price: 423 + sale_gamma: 51 + service_level: 0.95 + SKU15: + constraint: G(stock_constraint) + cost: 320 + init_stock: 144 + max_stock: 1000 + price: 470 + sale_gamma: 36 + service_level: 0.95 + SKU150: + constraint: G(stock_constraint) + cost: 83 + init_stock: 310 + max_stock: 1000 + price: 106 + sale_gamma: 62 + service_level: 0.95 + SKU151: + constraint: null + cost: 58 + init_stock: 175 + max_stock: 1000 + price: 64 + sale_gamma: 25 + service_level: 0.95 + SKU152: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 254 + init_stock: 210 + max_stock: 1000 + price: 373 + sale_gamma: 35 + service_level: 0.95 + SKU153: + constraint: null + cost: 49 + init_stock: 216 + max_stock: 1000 + price: 63 + sale_gamma: 72 + service_level: 0.95 + SKU154: + constraint: null + cost: 391 + init_stock: 70 + max_stock: 1000 + price: 680 + sale_gamma: 10 + service_level: 0.95 + SKU155: + constraint: G(stock_constraint) + cost: 36 + init_stock: 138 + max_stock: 1000 + price: 68 + sale_gamma: 23 + service_level: 0.95 + SKU156: + constraint: G(stock_constraint) + cost: 140 + init_stock: 296 + max_stock: 1000 + price: 219 + sale_gamma: 37 + service_level: 0.95 + SKU157: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 374 + init_stock: 195 + max_stock: 1000 + price: 489 + sale_gamma: 39 + service_level: 0.95 + SKU158: + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 135 + max_stock: 1000 + price: 274 + sale_gamma: 27 + service_level: 0.95 + SKU159: + constraint: G(stock_constraint) + cost: 428 + init_stock: 160 + max_stock: 1000 + price: 701 + sale_gamma: 40 + service_level: 0.95 + SKU16: + constraint: null + cost: 277 + init_stock: 497 + max_stock: 1000 + price: 357 + sale_gamma: 71 + service_level: 0.95 + SKU160: + constraint: null + cost: 129 + init_stock: 312 + max_stock: 1000 + price: 247 + sale_gamma: 78 + service_level: 0.95 + SKU161: + constraint: G(stock_constraint) + cost: 32 + init_stock: 480 + max_stock: 1000 + price: 58 + sale_gamma: 96 + service_level: 0.95 + SKU162: + constraint: null + cost: 68 + init_stock: 366 + max_stock: 1000 + price: 99 + sale_gamma: 61 + service_level: 0.95 + SKU163: + constraint: G(low_profit -> low_stock_constraint) + cost: 129 + init_stock: 240 + max_stock: 1000 + price: 167 + sale_gamma: 48 + service_level: 0.95 + SKU164: + constraint: null + cost: 192 + init_stock: 136 + max_stock: 1000 + price: 320 + sale_gamma: 68 + service_level: 0.95 + SKU165: + constraint: G(stock_constraint) + cost: 316 + init_stock: 124 + max_stock: 1000 + price: 527 + sale_gamma: 62 + service_level: 0.95 + SKU166: + constraint: G(stock_constraint) + cost: 454 + init_stock: 14 + max_stock: 1000 + price: 867 + sale_gamma: 7 + service_level: 0.95 + SKU167: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 108 + init_stock: 497 + max_stock: 1000 + price: 170 + sale_gamma: 71 + service_level: 0.95 + SKU168: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 431 + init_stock: 210 + max_stock: 1000 + price: 642 + sale_gamma: 30 + service_level: 0.95 + SKU169: + constraint: G(low_profit -> low_stock_constraint) + cost: 428 + init_stock: 432 + max_stock: 1000 + price: 706 + sale_gamma: 72 + service_level: 0.95 + SKU17: + constraint: G(low_profit -> low_stock_constraint) + cost: 393 + init_stock: 160 + max_stock: 1000 + price: 471 + sale_gamma: 40 + service_level: 0.95 + SKU170: + constraint: null + cost: 107 + init_stock: 98 + max_stock: 1000 + price: 176 + sale_gamma: 14 + service_level: 0.95 + SKU171: + constraint: null + cost: 14 + init_stock: 665 + max_stock: 1000 + price: 21 + sale_gamma: 95 + service_level: 0.95 + SKU172: + constraint: null + cost: 291 + init_stock: 344 + max_stock: 1000 + price: 538 + sale_gamma: 86 + service_level: 0.95 + SKU173: + constraint: G(low_profit -> low_stock_constraint) + cost: 190 + init_stock: 75 + max_stock: 1000 + price: 286 + sale_gamma: 15 + service_level: 0.95 + SKU174: + constraint: G(stock_constraint) + cost: 431 + init_stock: 104 + max_stock: 1000 + price: 612 + sale_gamma: 26 + service_level: 0.95 + SKU175: + constraint: G(stock_constraint) + cost: 355 + init_stock: 348 + max_stock: 1000 + price: 578 + sale_gamma: 87 + service_level: 0.95 + SKU176: + constraint: null + cost: 305 + init_stock: 196 + max_stock: 1000 + price: 524 + sale_gamma: 49 + service_level: 0.95 + SKU177: + constraint: G(low_profit -> low_stock_constraint) + cost: 291 + init_stock: 115 + max_stock: 1000 + price: 477 + sale_gamma: 23 + service_level: 0.95 + SKU178: + constraint: null + cost: 417 + init_stock: 120 + max_stock: 1000 + price: 704 + sale_gamma: 20 + service_level: 0.95 + SKU179: + constraint: G(low_profit -> low_stock_constraint) + cost: 18 + init_stock: 427 + max_stock: 1000 + price: 28 + sale_gamma: 61 + service_level: 0.95 + SKU18: + constraint: null + cost: 223 + init_stock: 312 + max_stock: 1000 + price: 399 + sale_gamma: 52 + service_level: 0.95 + SKU180: + constraint: G(stock_constraint) + cost: 373 + init_stock: 222 + max_stock: 1000 + price: 712 + sale_gamma: 37 + service_level: 0.95 + SKU181: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 486 + init_stock: 464 + max_stock: 1000 + price: 865 + sale_gamma: 58 + service_level: 0.95 + SKU182: + constraint: null + cost: 385 + init_stock: 238 + max_stock: 1000 + price: 735 + sale_gamma: 34 + service_level: 0.95 + SKU183: + constraint: G(low_profit -> low_stock_constraint) + cost: 195 + init_stock: 560 + max_stock: 1000 + price: 234 + sale_gamma: 80 + service_level: 0.95 + SKU184: + constraint: null + cost: 238 + init_stock: 20 + max_stock: 1000 + price: 402 + sale_gamma: 5 + service_level: 0.95 + SKU185: + constraint: null + cost: 50 + init_stock: 135 + max_stock: 1000 + price: 89 + sale_gamma: 45 + service_level: 0.95 + SKU186: + constraint: null + cost: 143 + init_stock: 376 + max_stock: 1000 + price: 264 + sale_gamma: 94 + service_level: 0.95 + SKU187: + constraint: G(stock_constraint) + cost: 285 + init_stock: 228 + max_stock: 1000 + price: 495 + sale_gamma: 38 + service_level: 0.95 + SKU188: + constraint: null + cost: 180 + init_stock: 165 + max_stock: 1000 + price: 198 + sale_gamma: 33 + service_level: 0.95 + SKU189: + constraint: null + cost: 294 + init_stock: 126 + max_stock: 1000 + price: 438 + sale_gamma: 18 + service_level: 0.95 + SKU19: + constraint: null + cost: 301 + init_stock: 168 + max_stock: 1000 + price: 352 + sale_gamma: 28 + service_level: 0.95 + SKU190: + constraint: null + cost: 392 + init_stock: 222 + max_stock: 1000 + price: 756 + sale_gamma: 37 + service_level: 0.95 + SKU191: + constraint: null + cost: 124 + init_stock: 68 + max_stock: 1000 + price: 193 + sale_gamma: 17 + service_level: 0.95 + SKU192: + constraint: null + cost: 118 + init_stock: 185 + max_stock: 1000 + price: 177 + sale_gamma: 37 + service_level: 0.95 + SKU193: + constraint: G(stock_constraint) + cost: 168 + init_stock: 410 + max_stock: 1000 + price: 304 + sale_gamma: 82 + service_level: 0.95 + SKU194: + constraint: G(stock_constraint) + cost: 331 + init_stock: 216 + max_stock: 1000 + price: 562 + sale_gamma: 36 + service_level: 0.95 + SKU195: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 81 + init_stock: 132 + max_stock: 1000 + price: 155 + sale_gamma: 33 + service_level: 0.95 + SKU196: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 94 + init_stock: 72 + max_stock: 1000 + price: 112 + sale_gamma: 12 + service_level: 0.95 + SKU197: + constraint: null + cost: 165 + init_stock: 210 + max_stock: 1000 + price: 206 + sale_gamma: 42 + service_level: 0.95 + SKU198: + constraint: G(stock_constraint) + cost: 454 + init_stock: 546 + max_stock: 1000 + price: 699 + sale_gamma: 78 + service_level: 0.95 + SKU199: + constraint: null + cost: 182 + init_stock: 222 + max_stock: 1000 + price: 307 + sale_gamma: 37 + service_level: 0.95 + SKU2: + constraint: G(low_profit -> low_stock_constraint) + cost: 60 + init_stock: 228 + max_stock: 1000 + price: 99 + sale_gamma: 57 + service_level: 0.95 + SKU20: + constraint: G(stock_constraint) + cost: 124 + init_stock: 252 + max_stock: 1000 + price: 243 + sale_gamma: 84 + service_level: 0.95 + SKU200: + constraint: G(low_profit -> low_stock_constraint) + cost: 355 + init_stock: 354 + max_stock: 1000 + price: 507 + sale_gamma: 59 + service_level: 0.95 + SKU201: + constraint: null + cost: 263 + init_stock: 322 + max_stock: 1000 + price: 373 + sale_gamma: 46 + service_level: 0.95 + SKU202: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 316 + init_stock: 126 + max_stock: 1000 + price: 423 + sale_gamma: 21 + service_level: 0.95 + SKU203: + constraint: null + cost: 145 + init_stock: 18 + max_stock: 1000 + price: 221 + sale_gamma: 6 + service_level: 0.95 + SKU204: + constraint: null + cost: 28 + init_stock: 240 + max_stock: 1000 + price: 48 + sale_gamma: 30 + service_level: 0.95 + SKU205: + constraint: null + cost: 190 + init_stock: 475 + max_stock: 1000 + price: 271 + sale_gamma: 95 + service_level: 0.95 + SKU206: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 496 + max_stock: 1000 + price: 439 + sale_gamma: 62 + service_level: 0.95 + SKU207: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 322 + init_stock: 276 + max_stock: 1000 + price: 421 + sale_gamma: 69 + service_level: 0.95 + SKU208: + constraint: null + cost: 287 + init_stock: 64 + max_stock: 1000 + price: 384 + sale_gamma: 16 + service_level: 0.95 + SKU209: + constraint: G(low_profit -> low_stock_constraint) + cost: 278 + init_stock: 68 + max_stock: 1000 + price: 556 + sale_gamma: 17 + service_level: 0.95 + SKU21: + constraint: G(stock_constraint) + cost: 408 + init_stock: 54 + max_stock: 1000 + price: 644 + sale_gamma: 18 + service_level: 0.95 + SKU210: + constraint: null + cost: 322 + init_stock: 164 + max_stock: 1000 + price: 540 + sale_gamma: 41 + service_level: 0.95 + SKU211: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 456 + init_stock: 260 + max_stock: 1000 + price: 579 + sale_gamma: 52 + service_level: 0.95 + SKU212: + constraint: null + cost: 248 + init_stock: 68 + max_stock: 1000 + price: 394 + sale_gamma: 17 + service_level: 0.95 + SKU213: + constraint: null + cost: 287 + init_stock: 40 + max_stock: 1000 + price: 450 + sale_gamma: 5 + service_level: 0.95 + SKU214: + constraint: G(low_profit -> low_stock_constraint) + cost: 89 + init_stock: 252 + max_stock: 1000 + price: 127 + sale_gamma: 63 + service_level: 0.95 + SKU215: + constraint: null + cost: 177 + init_stock: 576 + max_stock: 1000 + price: 341 + sale_gamma: 96 + service_level: 0.95 + SKU216: + constraint: G(low_profit -> low_stock_constraint) + cost: 232 + init_stock: 623 + max_stock: 1000 + price: 424 + sale_gamma: 89 + service_level: 0.95 + SKU217: + constraint: null + cost: 32 + init_stock: 510 + max_stock: 1000 + price: 52 + sale_gamma: 85 + service_level: 0.95 + SKU218: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 126 + max_stock: 1000 + price: 57 + sale_gamma: 18 + service_level: 0.95 + SKU219: + constraint: G(low_profit -> low_stock_constraint) + cost: 497 + init_stock: 230 + max_stock: 1000 + price: 919 + sale_gamma: 46 + service_level: 0.95 + SKU22: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 497 + init_stock: 180 + max_stock: 1000 + price: 954 + sale_gamma: 30 + service_level: 0.95 + SKU220: + constraint: null + cost: 436 + init_stock: 264 + max_stock: 1000 + price: 745 + sale_gamma: 44 + service_level: 0.95 + SKU221: + constraint: G(low_profit -> low_stock_constraint) + cost: 58 + init_stock: 518 + max_stock: 1000 + price: 107 + sale_gamma: 74 + service_level: 0.95 + SKU222: + constraint: G(low_profit -> low_stock_constraint) + cost: 174 + init_stock: 56 + max_stock: 1000 + price: 220 + sale_gamma: 14 + service_level: 0.95 + SKU223: + constraint: null + cost: 140 + init_stock: 322 + max_stock: 1000 + price: 169 + sale_gamma: 46 + service_level: 0.95 + SKU224: + constraint: null + cost: 246 + init_stock: 162 + max_stock: 1000 + price: 455 + sale_gamma: 27 + service_level: 0.95 + SKU225: + constraint: G(stock_constraint) + cost: 17 + init_stock: 318 + max_stock: 1000 + price: 31 + sale_gamma: 53 + service_level: 0.95 + SKU226: + constraint: null + cost: 81 + init_stock: 224 + max_stock: 1000 + price: 119 + sale_gamma: 56 + service_level: 0.95 + SKU227: + constraint: null + cost: 475 + init_stock: 180 + max_stock: 1000 + price: 650 + sale_gamma: 30 + service_level: 0.95 + SKU228: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 390 + max_stock: 1000 + price: 438 + sale_gamma: 65 + service_level: 0.95 + SKU229: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 465 + init_stock: 245 + max_stock: 1000 + price: 799 + sale_gamma: 49 + service_level: 0.95 + SKU23: + constraint: null + cost: 301 + init_stock: 204 + max_stock: 1000 + price: 553 + sale_gamma: 68 + service_level: 0.95 + SKU230: + constraint: null + cost: 483 + init_stock: 140 + max_stock: 1000 + price: 632 + sale_gamma: 35 + service_level: 0.95 + SKU231: + constraint: null + cost: 173 + init_stock: 65 + max_stock: 1000 + price: 257 + sale_gamma: 13 + service_level: 0.95 + SKU232: + constraint: G(low_profit -> low_stock_constraint) + cost: 315 + init_stock: 154 + max_stock: 1000 + price: 368 + sale_gamma: 22 + service_level: 0.95 + SKU233: + constraint: G(stock_constraint) + cost: 57 + init_stock: 15 + max_stock: 1000 + price: 94 + sale_gamma: 5 + service_level: 0.95 + SKU234: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 315 + init_stock: 276 + max_stock: 1000 + price: 349 + sale_gamma: 69 + service_level: 0.95 + SKU235: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 92 + init_stock: 48 + max_stock: 1000 + price: 115 + sale_gamma: 8 + service_level: 0.95 + SKU236: + constraint: G(low_profit -> low_stock_constraint) + cost: 125 + init_stock: 54 + max_stock: 1000 + price: 227 + sale_gamma: 18 + service_level: 0.95 + SKU237: + constraint: null + cost: 201 + init_stock: 63 + max_stock: 1000 + price: 335 + sale_gamma: 21 + service_level: 0.95 + SKU238: + constraint: null + cost: 105 + init_stock: 114 + max_stock: 1000 + price: 155 + sale_gamma: 38 + service_level: 0.95 + SKU239: + constraint: null + cost: 66 + init_stock: 258 + max_stock: 1000 + price: 83 + sale_gamma: 86 + service_level: 0.95 + SKU24: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 99 + init_stock: 420 + max_stock: 1000 + price: 188 + sale_gamma: 70 + service_level: 0.95 + SKU240: + constraint: null + cost: 262 + init_stock: 122 + max_stock: 1000 + price: 301 + sale_gamma: 61 + service_level: 0.95 + SKU241: + constraint: G(low_profit -> low_stock_constraint) + cost: 361 + init_stock: 448 + max_stock: 1000 + price: 534 + sale_gamma: 64 + service_level: 0.95 + SKU242: + constraint: G(low_profit -> low_stock_constraint) + cost: 221 + init_stock: 352 + max_stock: 1000 + price: 322 + sale_gamma: 88 + service_level: 0.95 + SKU243: + constraint: null + cost: 141 + init_stock: 372 + max_stock: 1000 + price: 222 + sale_gamma: 62 + service_level: 0.95 + SKU244: + constraint: G(stock_constraint) + cost: 193 + init_stock: 306 + max_stock: 1000 + price: 264 + sale_gamma: 51 + service_level: 0.95 + SKU245: + constraint: null + cost: 165 + init_stock: 165 + max_stock: 1000 + price: 287 + sale_gamma: 55 + service_level: 0.95 + SKU246: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 474 + init_stock: 48 + max_stock: 1000 + price: 734 + sale_gamma: 16 + service_level: 0.95 + SKU247: + constraint: G(stock_constraint) + cost: 66 + init_stock: 180 + max_stock: 1000 + price: 114 + sale_gamma: 90 + service_level: 0.95 + SKU248: + constraint: null + cost: 136 + init_stock: 52 + max_stock: 1000 + price: 221 + sale_gamma: 13 + service_level: 0.95 + SKU249: + constraint: null + cost: 83 + init_stock: 180 + max_stock: 1000 + price: 165 + sale_gamma: 60 + service_level: 0.95 + SKU25: + constraint: G(stock_constraint) + cost: 11 + init_stock: 165 + max_stock: 1000 + price: 21 + sale_gamma: 55 + service_level: 0.95 + SKU250: + constraint: null + cost: 310 + init_stock: 595 + max_stock: 1000 + price: 480 + sale_gamma: 85 + service_level: 0.95 + SKU251: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 247 + init_stock: 176 + max_stock: 1000 + price: 489 + sale_gamma: 44 + service_level: 0.95 + SKU252: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 192 + init_stock: 288 + max_stock: 1000 + price: 336 + sale_gamma: 48 + service_level: 0.95 + SKU253: + constraint: null + cost: 270 + init_stock: 240 + max_stock: 1000 + price: 475 + sale_gamma: 60 + service_level: 0.95 + SKU254: + constraint: G(stock_constraint) + cost: 165 + init_stock: 396 + max_stock: 1000 + price: 290 + sale_gamma: 99 + service_level: 0.95 + SKU255: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 49 + init_stock: 324 + max_stock: 1000 + price: 67 + sale_gamma: 54 + service_level: 0.95 + SKU256: + constraint: null + cost: 314 + init_stock: 516 + max_stock: 1000 + price: 439 + sale_gamma: 86 + service_level: 0.95 + SKU257: + constraint: null + cost: 468 + init_stock: 168 + max_stock: 1000 + price: 631 + sale_gamma: 24 + service_level: 0.95 + SKU258: + constraint: G(low_profit -> low_stock_constraint) + cost: 151 + init_stock: 70 + max_stock: 1000 + price: 292 + sale_gamma: 10 + service_level: 0.95 + SKU259: + constraint: null + cost: 389 + init_stock: 95 + max_stock: 1000 + price: 626 + sale_gamma: 19 + service_level: 0.95 + SKU26: + constraint: null + cost: 348 + init_stock: 222 + max_stock: 1000 + price: 685 + sale_gamma: 37 + service_level: 0.95 + SKU260: + constraint: G(stock_constraint) + cost: 90 + init_stock: 170 + max_stock: 1000 + price: 125 + sale_gamma: 85 + service_level: 0.95 + SKU261: + constraint: null + cost: 122 + init_stock: 375 + max_stock: 1000 + price: 207 + sale_gamma: 75 + service_level: 0.95 + SKU262: + constraint: null + cost: 15 + init_stock: 108 + max_stock: 1000 + price: 18 + sale_gamma: 18 + service_level: 0.95 + SKU263: + constraint: G(stock_constraint) + cost: 350 + init_stock: 234 + max_stock: 1000 + price: 539 + sale_gamma: 39 + service_level: 0.95 + SKU264: + constraint: null + cost: 10 + init_stock: 186 + max_stock: 1000 + price: 11 + sale_gamma: 31 + service_level: 0.95 + SKU265: + constraint: null + cost: 81 + init_stock: 95 + max_stock: 1000 + price: 135 + sale_gamma: 19 + service_level: 0.95 + SKU266: + constraint: null + cost: 276 + init_stock: 475 + max_stock: 1000 + price: 414 + sale_gamma: 95 + service_level: 0.95 + SKU267: + constraint: null + cost: 123 + init_stock: 168 + max_stock: 1000 + price: 199 + sale_gamma: 42 + service_level: 0.95 + SKU268: + constraint: null + cost: 64 + init_stock: 272 + max_stock: 1000 + price: 90 + sale_gamma: 34 + service_level: 0.95 + SKU269: + constraint: null + cost: 435 + init_stock: 40 + max_stock: 1000 + price: 574 + sale_gamma: 10 + service_level: 0.95 + SKU27: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 196 + init_stock: 285 + max_stock: 1000 + price: 248 + sale_gamma: 57 + service_level: 0.95 + SKU270: + constraint: G(stock_constraint) + cost: 68 + init_stock: 290 + max_stock: 1000 + price: 121 + sale_gamma: 58 + service_level: 0.95 + SKU271: + constraint: G(stock_constraint) + cost: 14 + init_stock: 48 + max_stock: 1000 + price: 22 + sale_gamma: 8 + service_level: 0.95 + SKU272: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 249 + init_stock: 415 + max_stock: 1000 + price: 478 + sale_gamma: 83 + service_level: 0.95 + SKU273: + constraint: null + cost: 39 + init_stock: 140 + max_stock: 1000 + price: 78 + sale_gamma: 20 + service_level: 0.95 + SKU274: + constraint: null + cost: 436 + init_stock: 63 + max_stock: 1000 + price: 592 + sale_gamma: 9 + service_level: 0.95 + SKU275: + constraint: G(stock_constraint) + cost: 62 + init_stock: 372 + max_stock: 1000 + price: 75 + sale_gamma: 93 + service_level: 0.95 + SKU276: + constraint: G(stock_constraint) + cost: 281 + init_stock: 225 + max_stock: 1000 + price: 435 + sale_gamma: 75 + service_level: 0.95 + SKU277: + constraint: G(low_profit -> low_stock_constraint) + cost: 442 + init_stock: 546 + max_stock: 1000 + price: 693 + sale_gamma: 91 + service_level: 0.95 + SKU278: + constraint: null + cost: 290 + init_stock: 420 + max_stock: 1000 + price: 490 + sale_gamma: 84 + service_level: 0.95 + SKU279: + constraint: G(low_profit -> low_stock_constraint) + cost: 175 + init_stock: 177 + max_stock: 1000 + price: 252 + sale_gamma: 59 + service_level: 0.95 + SKU28: + constraint: null + cost: 61 + init_stock: 72 + max_stock: 1000 + price: 119 + sale_gamma: 36 + service_level: 0.95 + SKU280: + constraint: null + cost: 445 + init_stock: 410 + max_stock: 1000 + price: 565 + sale_gamma: 82 + service_level: 0.95 + SKU281: + constraint: null + cost: 49 + init_stock: 35 + max_stock: 1000 + price: 60 + sale_gamma: 7 + service_level: 0.95 + SKU282: + constraint: G(stock_constraint) + cost: 499 + init_stock: 155 + max_stock: 1000 + price: 898 + sale_gamma: 31 + service_level: 0.95 + SKU283: + constraint: null + cost: 310 + init_stock: 126 + max_stock: 1000 + price: 607 + sale_gamma: 42 + service_level: 0.95 + SKU284: + constraint: G(low_profit -> low_stock_constraint) + cost: 381 + init_stock: 252 + max_stock: 1000 + price: 701 + sale_gamma: 42 + service_level: 0.95 + SKU285: + constraint: G(stock_constraint) + cost: 378 + init_stock: 105 + max_stock: 1000 + price: 627 + sale_gamma: 35 + service_level: 0.95 + SKU286: + constraint: G(low_profit -> low_stock_constraint) + cost: 366 + init_stock: 534 + max_stock: 1000 + price: 706 + sale_gamma: 89 + service_level: 0.95 + SKU287: + constraint: G(stock_constraint) + cost: 287 + init_stock: 402 + max_stock: 1000 + price: 378 + sale_gamma: 67 + service_level: 0.95 + SKU288: + constraint: null + cost: 24 + init_stock: 72 + max_stock: 1000 + price: 43 + sale_gamma: 12 + service_level: 0.95 + SKU289: + constraint: null + cost: 439 + init_stock: 297 + max_stock: 1000 + price: 698 + sale_gamma: 99 + service_level: 0.95 + SKU29: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 490 + init_stock: 216 + max_stock: 1000 + price: 862 + sale_gamma: 72 + service_level: 0.95 + SKU290: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 11 + init_stock: 385 + max_stock: 1000 + price: 13 + sale_gamma: 77 + service_level: 0.95 + SKU291: + constraint: G(stock_constraint) + cost: 259 + init_stock: 90 + max_stock: 1000 + price: 321 + sale_gamma: 18 + service_level: 0.95 + SKU292: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 389 + init_stock: 800 + max_stock: 1000 + price: 427 + sale_gamma: 100 + service_level: 0.95 + SKU293: + constraint: null + cost: 246 + init_stock: 336 + max_stock: 1000 + price: 314 + sale_gamma: 56 + service_level: 0.95 + SKU294: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 76 + init_stock: 91 + max_stock: 1000 + price: 133 + sale_gamma: 13 + service_level: 0.95 + SKU295: + constraint: G(low_profit -> low_stock_constraint) + cost: 22 + init_stock: 688 + max_stock: 1000 + price: 35 + sale_gamma: 86 + service_level: 0.95 + SKU296: + constraint: null + cost: 389 + init_stock: 570 + max_stock: 1000 + price: 540 + sale_gamma: 95 + service_level: 0.95 + SKU297: + constraint: null + cost: 51 + init_stock: 448 + max_stock: 1000 + price: 82 + sale_gamma: 64 + service_level: 0.95 + SKU298: + constraint: G(low_profit -> low_stock_constraint) + cost: 269 + init_stock: 267 + max_stock: 1000 + price: 301 + sale_gamma: 89 + service_level: 0.95 + SKU299: + constraint: null + cost: 14 + init_stock: 356 + max_stock: 1000 + price: 16 + sale_gamma: 89 + service_level: 0.95 + SKU3: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 470 + init_stock: 176 + max_stock: 1000 + price: 893 + sale_gamma: 88 + service_level: 0.95 + SKU30: + constraint: G(low_profit -> low_stock_constraint) + cost: 218 + init_stock: 90 + max_stock: 1000 + price: 420 + sale_gamma: 45 + service_level: 0.95 + SKU300: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 88 + max_stock: 1000 + price: 169 + sale_gamma: 22 + service_level: 0.95 + SKU301: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 417 + init_stock: 160 + max_stock: 1000 + price: 467 + sale_gamma: 32 + service_level: 0.95 + SKU302: + constraint: G(stock_constraint) + cost: 161 + init_stock: 276 + max_stock: 1000 + price: 310 + sale_gamma: 92 + service_level: 0.95 + SKU303: + constraint: G(stock_constraint) + cost: 215 + init_stock: 316 + max_stock: 1000 + price: 320 + sale_gamma: 79 + service_level: 0.95 + SKU304: + constraint: null + cost: 145 + init_stock: 348 + max_stock: 1000 + price: 288 + sale_gamma: 58 + service_level: 0.95 + SKU305: + constraint: null + cost: 441 + init_stock: 32 + max_stock: 1000 + price: 498 + sale_gamma: 8 + service_level: 0.95 + SKU306: + constraint: G(low_profit -> low_stock_constraint) + cost: 284 + init_stock: 138 + max_stock: 1000 + price: 357 + sale_gamma: 69 + service_level: 0.95 + SKU307: + constraint: G(stock_constraint) + cost: 412 + init_stock: 280 + max_stock: 1000 + price: 811 + sale_gamma: 56 + service_level: 0.95 + SKU308: + constraint: null + cost: 409 + init_stock: 480 + max_stock: 1000 + price: 609 + sale_gamma: 80 + service_level: 0.95 + SKU309: + constraint: G(low_profit -> low_stock_constraint) + cost: 260 + init_stock: 153 + max_stock: 1000 + price: 403 + sale_gamma: 51 + service_level: 0.95 + SKU31: + constraint: null + cost: 140 + init_stock: 704 + max_stock: 1000 + price: 232 + sale_gamma: 88 + service_level: 0.95 + SKU310: + constraint: null + cost: 367 + init_stock: 196 + max_stock: 1000 + price: 480 + sale_gamma: 28 + service_level: 0.95 + SKU311: + constraint: null + cost: 143 + init_stock: 455 + max_stock: 1000 + price: 208 + sale_gamma: 91 + service_level: 0.95 + SKU312: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 264 + init_stock: 144 + max_stock: 1000 + price: 306 + sale_gamma: 18 + service_level: 0.95 + SKU313: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 475 + max_stock: 1000 + price: 272 + sale_gamma: 95 + service_level: 0.95 + SKU314: + constraint: null + cost: 318 + init_stock: 60 + max_stock: 1000 + price: 597 + sale_gamma: 15 + service_level: 0.95 + SKU315: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 285 + max_stock: 1000 + price: 39 + sale_gamma: 57 + service_level: 0.95 + SKU316: + constraint: null + cost: 215 + init_stock: 258 + max_stock: 1000 + price: 367 + sale_gamma: 86 + service_level: 0.95 + SKU317: + constraint: null + cost: 72 + init_stock: 93 + max_stock: 1000 + price: 138 + sale_gamma: 31 + service_level: 0.95 + SKU318: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 485 + init_stock: 540 + max_stock: 1000 + price: 562 + sale_gamma: 90 + service_level: 0.95 + SKU319: + constraint: null + cost: 218 + init_stock: 48 + max_stock: 1000 + price: 420 + sale_gamma: 24 + service_level: 0.95 + SKU32: + constraint: null + cost: 380 + init_stock: 160 + max_stock: 1000 + price: 478 + sale_gamma: 32 + service_level: 0.95 + SKU320: + constraint: G(stock_constraint) + cost: 445 + init_stock: 700 + max_stock: 1000 + price: 685 + sale_gamma: 100 + service_level: 0.95 + SKU321: + constraint: G(stock_constraint) + cost: 73 + init_stock: 352 + max_stock: 1000 + price: 115 + sale_gamma: 88 + service_level: 0.95 + SKU322: + constraint: G(low_profit -> low_stock_constraint) + cost: 120 + init_stock: 385 + max_stock: 1000 + price: 207 + sale_gamma: 77 + service_level: 0.95 + SKU323: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 241 + init_stock: 570 + max_stock: 1000 + price: 409 + sale_gamma: 95 + service_level: 0.95 + SKU324: + constraint: G(low_profit -> low_stock_constraint) + cost: 269 + init_stock: 500 + max_stock: 1000 + price: 435 + sale_gamma: 100 + service_level: 0.95 + SKU325: + constraint: null + cost: 295 + init_stock: 144 + max_stock: 1000 + price: 421 + sale_gamma: 24 + service_level: 0.95 + SKU326: + constraint: G(stock_constraint) + cost: 23 + init_stock: 332 + max_stock: 1000 + price: 28 + sale_gamma: 83 + service_level: 0.95 + SKU327: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 334 + init_stock: 264 + max_stock: 1000 + price: 367 + sale_gamma: 66 + service_level: 0.95 + SKU328: + constraint: null + cost: 107 + init_stock: 77 + max_stock: 1000 + price: 191 + sale_gamma: 11 + service_level: 0.95 + SKU329: + constraint: G(stock_constraint) + cost: 260 + init_stock: 420 + max_stock: 1000 + price: 520 + sale_gamma: 60 + service_level: 0.95 + SKU33: + constraint: G(stock_constraint) + cost: 81 + init_stock: 672 + max_stock: 1000 + price: 127 + sale_gamma: 84 + service_level: 0.95 + SKU330: + constraint: G(stock_constraint) + cost: 364 + init_stock: 264 + max_stock: 1000 + price: 498 + sale_gamma: 44 + service_level: 0.95 + SKU331: + constraint: G(low_profit -> low_stock_constraint) + cost: 435 + init_stock: 168 + max_stock: 1000 + price: 600 + sale_gamma: 28 + service_level: 0.95 + SKU332: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 227 + init_stock: 664 + max_stock: 1000 + price: 283 + sale_gamma: 83 + service_level: 0.95 + SKU333: + constraint: null + cost: 268 + init_stock: 252 + max_stock: 1000 + price: 391 + sale_gamma: 63 + service_level: 0.95 + SKU334: + constraint: G(low_profit -> low_stock_constraint) + cost: 248 + init_stock: 172 + max_stock: 1000 + price: 327 + sale_gamma: 43 + service_level: 0.95 + SKU335: + constraint: G(low_profit -> low_stock_constraint) + cost: 341 + init_stock: 376 + max_stock: 1000 + price: 675 + sale_gamma: 94 + service_level: 0.95 + SKU336: + constraint: G(low_profit -> low_stock_constraint) + cost: 489 + init_stock: 200 + max_stock: 1000 + price: 738 + sale_gamma: 50 + service_level: 0.95 + SKU337: + constraint: G(stock_constraint) + cost: 416 + init_stock: 294 + max_stock: 1000 + price: 736 + sale_gamma: 98 + service_level: 0.95 + SKU338: + constraint: null + cost: 209 + init_stock: 518 + max_stock: 1000 + price: 330 + sale_gamma: 74 + service_level: 0.95 + SKU339: + constraint: null + cost: 189 + init_stock: 186 + max_stock: 1000 + price: 336 + sale_gamma: 62 + service_level: 0.95 + SKU34: + constraint: G(low_profit -> low_stock_constraint) + cost: 263 + init_stock: 188 + max_stock: 1000 + price: 526 + sale_gamma: 47 + service_level: 0.95 + SKU340: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 375 + init_stock: 133 + max_stock: 1000 + price: 442 + sale_gamma: 19 + service_level: 0.95 + SKU341: + constraint: G(low_profit -> low_stock_constraint) + cost: 166 + init_stock: 336 + max_stock: 1000 + price: 325 + sale_gamma: 56 + service_level: 0.95 + SKU342: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 177 + init_stock: 82 + max_stock: 1000 + price: 242 + sale_gamma: 41 + service_level: 0.95 + SKU343: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 478 + init_stock: 336 + max_stock: 1000 + price: 616 + sale_gamma: 84 + service_level: 0.95 + SKU344: + constraint: null + cost: 491 + init_stock: 324 + max_stock: 1000 + price: 903 + sale_gamma: 81 + service_level: 0.95 + SKU345: + constraint: null + cost: 272 + init_stock: 165 + max_stock: 1000 + price: 470 + sale_gamma: 33 + service_level: 0.95 + SKU346: + constraint: null + cost: 200 + init_stock: 355 + max_stock: 1000 + price: 366 + sale_gamma: 71 + service_level: 0.95 + SKU347: + constraint: null + cost: 180 + init_stock: 217 + max_stock: 1000 + price: 221 + sale_gamma: 31 + service_level: 0.95 + SKU348: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 102 + max_stock: 1000 + price: 559 + sale_gamma: 34 + service_level: 0.95 + SKU349: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 81 + init_stock: 273 + max_stock: 1000 + price: 151 + sale_gamma: 91 + service_level: 0.95 + SKU35: + constraint: G(stock_constraint) + cost: 240 + init_stock: 118 + max_stock: 1000 + price: 350 + sale_gamma: 59 + service_level: 0.95 + SKU350: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 106 + init_stock: 36 + max_stock: 1000 + price: 136 + sale_gamma: 12 + service_level: 0.95 + SKU351: + constraint: G(stock_constraint) + cost: 102 + init_stock: 444 + max_stock: 1000 + price: 192 + sale_gamma: 74 + service_level: 0.95 + SKU352: + constraint: null + cost: 274 + init_stock: 112 + max_stock: 1000 + price: 337 + sale_gamma: 16 + service_level: 0.95 + SKU353: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 147 + init_stock: 408 + max_stock: 1000 + price: 238 + sale_gamma: 68 + service_level: 0.95 + SKU354: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 410 + init_stock: 390 + max_stock: 1000 + price: 508 + sale_gamma: 78 + service_level: 0.95 + SKU355: + constraint: null + cost: 417 + init_stock: 336 + max_stock: 1000 + price: 575 + sale_gamma: 56 + service_level: 0.95 + SKU356: + constraint: null + cost: 192 + init_stock: 175 + max_stock: 1000 + price: 211 + sale_gamma: 35 + service_level: 0.95 + SKU357: + constraint: G(stock_constraint) + cost: 80 + init_stock: 400 + max_stock: 1000 + price: 122 + sale_gamma: 100 + service_level: 0.95 + SKU358: + constraint: G(stock_constraint) + cost: 130 + init_stock: 160 + max_stock: 1000 + price: 153 + sale_gamma: 32 + service_level: 0.95 + SKU359: + constraint: null + cost: 327 + init_stock: 128 + max_stock: 1000 + price: 578 + sale_gamma: 32 + service_level: 0.95 + SKU36: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 134 + init_stock: 644 + max_stock: 1000 + price: 222 + sale_gamma: 92 + service_level: 0.95 + SKU360: + constraint: G(low_profit -> low_stock_constraint) + cost: 456 + init_stock: 165 + max_stock: 1000 + price: 693 + sale_gamma: 55 + service_level: 0.95 + SKU361: + constraint: G(stock_constraint) + cost: 183 + init_stock: 536 + max_stock: 1000 + price: 336 + sale_gamma: 67 + service_level: 0.95 + SKU362: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 352 + init_stock: 164 + max_stock: 1000 + price: 520 + sale_gamma: 41 + service_level: 0.95 + SKU363: + constraint: null + cost: 32 + init_stock: 20 + max_stock: 1000 + price: 62 + sale_gamma: 5 + service_level: 0.95 + SKU364: + constraint: null + cost: 331 + init_stock: 260 + max_stock: 1000 + price: 483 + sale_gamma: 65 + service_level: 0.95 + SKU365: + constraint: G(low_profit -> low_stock_constraint) + cost: 57 + init_stock: 492 + max_stock: 1000 + price: 100 + sale_gamma: 82 + service_level: 0.95 + SKU366: + constraint: null + cost: 184 + init_stock: 168 + max_stock: 1000 + price: 211 + sale_gamma: 24 + service_level: 0.95 + SKU367: + constraint: G(stock_constraint) + cost: 385 + init_stock: 400 + max_stock: 1000 + price: 469 + sale_gamma: 80 + service_level: 0.95 + SKU368: + constraint: G(low_profit -> low_stock_constraint) + cost: 391 + init_stock: 217 + max_stock: 1000 + price: 609 + sale_gamma: 31 + service_level: 0.95 + SKU369: + constraint: null + cost: 460 + init_stock: 256 + max_stock: 1000 + price: 662 + sale_gamma: 32 + service_level: 0.95 + SKU37: + constraint: G(low_profit -> low_stock_constraint) + cost: 284 + init_stock: 207 + max_stock: 1000 + price: 423 + sale_gamma: 69 + service_level: 0.95 + SKU370: + constraint: null + cost: 103 + init_stock: 168 + max_stock: 1000 + price: 180 + sale_gamma: 28 + service_level: 0.95 + SKU371: + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 588 + max_stock: 1000 + price: 280 + sale_gamma: 84 + service_level: 0.95 + SKU372: + constraint: G(stock_constraint) + cost: 488 + init_stock: 160 + max_stock: 1000 + price: 707 + sale_gamma: 20 + service_level: 0.95 + SKU373: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 155 + init_stock: 189 + max_stock: 1000 + price: 175 + sale_gamma: 27 + service_level: 0.95 + SKU374: + constraint: G(low_profit -> low_stock_constraint) + cost: 13 + init_stock: 760 + max_stock: 1000 + price: 19 + sale_gamma: 95 + service_level: 0.95 + SKU375: + constraint: G(stock_constraint) + cost: 433 + init_stock: 434 + max_stock: 1000 + price: 588 + sale_gamma: 62 + service_level: 0.95 + SKU376: + constraint: null + cost: 30 + init_stock: 324 + max_stock: 1000 + price: 54 + sale_gamma: 81 + service_level: 0.95 + SKU377: + constraint: null + cost: 100 + init_stock: 72 + max_stock: 1000 + price: 159 + sale_gamma: 36 + service_level: 0.95 + SKU378: + constraint: null + cost: 489 + init_stock: 45 + max_stock: 1000 + price: 699 + sale_gamma: 9 + service_level: 0.95 + SKU379: + constraint: G(stock_constraint) + cost: 462 + init_stock: 328 + max_stock: 1000 + price: 808 + sale_gamma: 41 + service_level: 0.95 + SKU38: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 326 + init_stock: 486 + max_stock: 1000 + price: 394 + sale_gamma: 81 + service_level: 0.95 + SKU380: + constraint: G(stock_constraint) + cost: 459 + init_stock: 78 + max_stock: 1000 + price: 573 + sale_gamma: 13 + service_level: 0.95 + SKU381: + constraint: null + cost: 276 + init_stock: 304 + max_stock: 1000 + price: 521 + sale_gamma: 76 + service_level: 0.95 + SKU382: + constraint: null + cost: 472 + init_stock: 85 + max_stock: 1000 + price: 674 + sale_gamma: 17 + service_level: 0.95 + SKU383: + constraint: null + cost: 249 + init_stock: 99 + max_stock: 1000 + price: 410 + sale_gamma: 33 + service_level: 0.95 + SKU384: + constraint: G(stock_constraint) + cost: 37 + init_stock: 192 + max_stock: 1000 + price: 66 + sale_gamma: 24 + service_level: 0.95 + SKU385: + constraint: null + cost: 275 + init_stock: 400 + max_stock: 1000 + price: 368 + sale_gamma: 80 + service_level: 0.95 + SKU386: + constraint: null + cost: 357 + init_stock: 357 + max_stock: 1000 + price: 639 + sale_gamma: 51 + service_level: 0.95 + SKU387: + constraint: G(low_profit -> low_stock_constraint) + cost: 471 + init_stock: 185 + max_stock: 1000 + price: 744 + sale_gamma: 37 + service_level: 0.95 + SKU388: + constraint: null + cost: 264 + init_stock: 469 + max_stock: 1000 + price: 485 + sale_gamma: 67 + service_level: 0.95 + SKU389: + constraint: G(stock_constraint) + cost: 13 + init_stock: 343 + max_stock: 1000 + price: 18 + sale_gamma: 49 + service_level: 0.95 + SKU39: + constraint: G(stock_constraint) + cost: 381 + init_stock: 44 + max_stock: 1000 + price: 605 + sale_gamma: 22 + service_level: 0.95 + SKU390: + constraint: null + cost: 146 + init_stock: 10 + max_stock: 1000 + price: 188 + sale_gamma: 5 + service_level: 0.95 + SKU391: + constraint: null + cost: 365 + init_stock: 276 + max_stock: 1000 + price: 657 + sale_gamma: 92 + service_level: 0.95 + SKU392: + constraint: null + cost: 489 + init_stock: 368 + max_stock: 1000 + price: 855 + sale_gamma: 46 + service_level: 0.95 + SKU393: + constraint: null + cost: 466 + init_stock: 88 + max_stock: 1000 + price: 573 + sale_gamma: 11 + service_level: 0.95 + SKU394: + constraint: null + cost: 186 + init_stock: 51 + max_stock: 1000 + price: 303 + sale_gamma: 17 + service_level: 0.95 + SKU395: + constraint: G(low_profit -> low_stock_constraint) + cost: 324 + init_stock: 60 + max_stock: 1000 + price: 563 + sale_gamma: 30 + service_level: 0.95 + SKU396: + constraint: null + cost: 392 + init_stock: 544 + max_stock: 1000 + price: 678 + sale_gamma: 68 + service_level: 0.95 + SKU397: + constraint: null + cost: 257 + init_stock: 69 + max_stock: 1000 + price: 493 + sale_gamma: 23 + service_level: 0.95 + SKU398: + constraint: G(stock_constraint) + cost: 455 + init_stock: 426 + max_stock: 1000 + price: 609 + sale_gamma: 71 + service_level: 0.95 + SKU399: + constraint: null + cost: 15 + init_stock: 546 + max_stock: 1000 + price: 30 + sale_gamma: 78 + service_level: 0.95 + SKU4: + constraint: null + cost: 289 + init_stock: 186 + max_stock: 1000 + price: 372 + sale_gamma: 93 + service_level: 0.95 + SKU40: + constraint: null + cost: 216 + init_stock: 32 + max_stock: 1000 + price: 319 + sale_gamma: 8 + service_level: 0.95 + SKU400: + constraint: null + cost: 355 + init_stock: 570 + max_stock: 1000 + price: 553 + sale_gamma: 95 + service_level: 0.95 + SKU401: + constraint: null + cost: 150 + init_stock: 64 + max_stock: 1000 + price: 282 + sale_gamma: 8 + service_level: 0.95 + SKU402: + constraint: null + cost: 255 + init_stock: 552 + max_stock: 1000 + price: 339 + sale_gamma: 92 + service_level: 0.95 + SKU403: + constraint: null + cost: 333 + init_stock: 332 + max_stock: 1000 + price: 479 + sale_gamma: 83 + service_level: 0.95 + SKU404: + constraint: G(stock_constraint) + cost: 94 + init_stock: 216 + max_stock: 1000 + price: 172 + sale_gamma: 36 + service_level: 0.95 + SKU405: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 242 + init_stock: 348 + max_stock: 1000 + price: 396 + sale_gamma: 58 + service_level: 0.95 + SKU406: + constraint: G(stock_constraint) + cost: 402 + init_stock: 220 + max_stock: 1000 + price: 695 + sale_gamma: 55 + service_level: 0.95 + SKU407: + constraint: G(low_profit -> low_stock_constraint) + cost: 346 + init_stock: 273 + max_stock: 1000 + price: 494 + sale_gamma: 39 + service_level: 0.95 + SKU408: + constraint: null + cost: 274 + init_stock: 574 + max_stock: 1000 + price: 405 + sale_gamma: 82 + service_level: 0.95 + SKU409: + constraint: G(stock_constraint) + cost: 268 + init_stock: 376 + max_stock: 1000 + price: 530 + sale_gamma: 94 + service_level: 0.95 + SKU41: + constraint: null + cost: 312 + init_stock: 544 + max_stock: 1000 + price: 343 + sale_gamma: 68 + service_level: 0.95 + SKU410: + constraint: G(low_profit -> low_stock_constraint) + cost: 429 + init_stock: 192 + max_stock: 1000 + price: 673 + sale_gamma: 48 + service_level: 0.95 + SKU411: + constraint: G(stock_constraint) + cost: 11 + init_stock: 120 + max_stock: 1000 + price: 20 + sale_gamma: 30 + service_level: 0.95 + SKU412: + constraint: null + cost: 91 + init_stock: 236 + max_stock: 1000 + price: 181 + sale_gamma: 59 + service_level: 0.95 + SKU413: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 443 + init_stock: 66 + max_stock: 1000 + price: 815 + sale_gamma: 11 + service_level: 0.95 + SKU414: + constraint: null + cost: 67 + init_stock: 385 + max_stock: 1000 + price: 112 + sale_gamma: 77 + service_level: 0.95 + SKU415: + constraint: null + cost: 347 + init_stock: 50 + max_stock: 1000 + price: 548 + sale_gamma: 10 + service_level: 0.95 + SKU416: + constraint: null + cost: 29 + init_stock: 204 + max_stock: 1000 + price: 51 + sale_gamma: 51 + service_level: 0.95 + SKU417: + constraint: null + cost: 488 + init_stock: 204 + max_stock: 1000 + price: 800 + sale_gamma: 68 + service_level: 0.95 + SKU418: + constraint: null + cost: 432 + init_stock: 776 + max_stock: 1000 + price: 781 + sale_gamma: 97 + service_level: 0.95 + SKU419: + constraint: G(low_profit -> low_stock_constraint) + cost: 38 + init_stock: 39 + max_stock: 1000 + price: 65 + sale_gamma: 13 + service_level: 0.95 + SKU42: + constraint: null + cost: 275 + init_stock: 581 + max_stock: 1000 + price: 418 + sale_gamma: 83 + service_level: 0.95 + SKU420: + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 21 + max_stock: 1000 + price: 401 + sale_gamma: 7 + service_level: 0.95 + SKU421: + constraint: null + cost: 289 + init_stock: 640 + max_stock: 1000 + price: 442 + sale_gamma: 80 + service_level: 0.95 + SKU422: + constraint: null + cost: 83 + init_stock: 102 + max_stock: 1000 + price: 101 + sale_gamma: 34 + service_level: 0.95 + SKU423: + constraint: null + cost: 174 + init_stock: 84 + max_stock: 1000 + price: 234 + sale_gamma: 12 + service_level: 0.95 + SKU424: + constraint: G(low_profit -> low_stock_constraint) + cost: 495 + init_stock: 82 + max_stock: 1000 + price: 935 + sale_gamma: 41 + service_level: 0.95 + SKU425: + constraint: null + cost: 142 + init_stock: 264 + max_stock: 1000 + price: 230 + sale_gamma: 44 + service_level: 0.95 + SKU426: + constraint: null + cost: 234 + init_stock: 64 + max_stock: 1000 + price: 453 + sale_gamma: 16 + service_level: 0.95 + SKU427: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 162 + init_stock: 252 + max_stock: 1000 + price: 225 + sale_gamma: 84 + service_level: 0.95 + SKU428: + constraint: G(stock_constraint) + cost: 423 + init_stock: 310 + max_stock: 1000 + price: 765 + sale_gamma: 62 + service_level: 0.95 + SKU429: + constraint: G(low_profit -> low_stock_constraint) + cost: 322 + init_stock: 297 + max_stock: 1000 + price: 634 + sale_gamma: 99 + service_level: 0.95 + SKU43: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 428 + init_stock: 432 + max_stock: 1000 + price: 791 + sale_gamma: 54 + service_level: 0.95 + SKU430: + constraint: G(stock_constraint) + cost: 289 + init_stock: 135 + max_stock: 1000 + price: 534 + sale_gamma: 45 + service_level: 0.95 + SKU431: + constraint: null + cost: 29 + init_stock: 91 + max_stock: 1000 + price: 44 + sale_gamma: 13 + service_level: 0.95 + SKU432: + constraint: G(low_profit -> low_stock_constraint) + cost: 349 + init_stock: 115 + max_stock: 1000 + price: 415 + sale_gamma: 23 + service_level: 0.95 + SKU433: + constraint: G(low_profit -> low_stock_constraint) + cost: 380 + init_stock: 156 + max_stock: 1000 + price: 634 + sale_gamma: 26 + service_level: 0.95 + SKU434: + constraint: null + cost: 373 + init_stock: 328 + max_stock: 1000 + price: 704 + sale_gamma: 41 + service_level: 0.95 + SKU435: + constraint: null + cost: 46 + init_stock: 200 + max_stock: 1000 + price: 71 + sale_gamma: 50 + service_level: 0.95 + SKU436: + constraint: null + cost: 383 + init_stock: 190 + max_stock: 1000 + price: 731 + sale_gamma: 38 + service_level: 0.95 + SKU437: + constraint: null + cost: 76 + init_stock: 656 + max_stock: 1000 + price: 85 + sale_gamma: 82 + service_level: 0.95 + SKU438: + constraint: G(stock_constraint) + cost: 433 + init_stock: 108 + max_stock: 1000 + price: 796 + sale_gamma: 27 + service_level: 0.95 + SKU439: + constraint: null + cost: 40 + init_stock: 285 + max_stock: 1000 + price: 66 + sale_gamma: 57 + service_level: 0.95 + SKU44: + constraint: null + cost: 255 + init_stock: 76 + max_stock: 1000 + price: 507 + sale_gamma: 19 + service_level: 0.95 + SKU440: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 124 + init_stock: 282 + max_stock: 1000 + price: 189 + sale_gamma: 47 + service_level: 0.95 + SKU441: + constraint: null + cost: 228 + init_stock: 324 + max_stock: 1000 + price: 323 + sale_gamma: 54 + service_level: 0.95 + SKU442: + constraint: null + cost: 19 + init_stock: 164 + max_stock: 1000 + price: 29 + sale_gamma: 41 + service_level: 0.95 + SKU443: + constraint: null + cost: 446 + init_stock: 310 + max_stock: 1000 + price: 825 + sale_gamma: 62 + service_level: 0.95 + SKU444: + constraint: G(low_profit -> low_stock_constraint) + cost: 212 + init_stock: 162 + max_stock: 1000 + price: 248 + sale_gamma: 54 + service_level: 0.95 + SKU445: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 396 + init_stock: 100 + max_stock: 1000 + price: 728 + sale_gamma: 20 + service_level: 0.95 + SKU446: + constraint: null + cost: 126 + init_stock: 384 + max_stock: 1000 + price: 146 + sale_gamma: 64 + service_level: 0.95 + SKU447: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 20 + init_stock: 280 + max_stock: 1000 + price: 23 + sale_gamma: 40 + service_level: 0.95 + SKU448: + constraint: null + cost: 365 + init_stock: 528 + max_stock: 1000 + price: 584 + sale_gamma: 66 + service_level: 0.95 + SKU449: + constraint: null + cost: 328 + init_stock: 24 + max_stock: 1000 + price: 570 + sale_gamma: 8 + service_level: 0.95 + SKU45: + constraint: G(low_profit -> low_stock_constraint) + cost: 158 + init_stock: 196 + max_stock: 1000 + price: 238 + sale_gamma: 28 + service_level: 0.95 + SKU450: + constraint: G(stock_constraint) + cost: 61 + init_stock: 24 + max_stock: 1000 + price: 91 + sale_gamma: 6 + service_level: 0.95 + SKU451: + constraint: G(low_profit -> low_stock_constraint) + cost: 385 + init_stock: 108 + max_stock: 1000 + price: 712 + sale_gamma: 36 + service_level: 0.95 + SKU452: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 450 + init_stock: 292 + max_stock: 1000 + price: 724 + sale_gamma: 73 + service_level: 0.95 + SKU453: + constraint: null + cost: 131 + init_stock: 243 + max_stock: 1000 + price: 146 + sale_gamma: 81 + service_level: 0.95 + SKU454: + constraint: G(low_profit -> low_stock_constraint) + cost: 404 + init_stock: 413 + max_stock: 1000 + price: 731 + sale_gamma: 59 + service_level: 0.95 + SKU455: + constraint: G(low_profit -> low_stock_constraint) + cost: 334 + init_stock: 296 + max_stock: 1000 + price: 400 + sale_gamma: 74 + service_level: 0.95 + SKU456: + constraint: G(low_profit -> low_stock_constraint) + cost: 126 + init_stock: 328 + max_stock: 1000 + price: 161 + sale_gamma: 41 + service_level: 0.95 + SKU457: + constraint: G(low_profit -> low_stock_constraint) + cost: 235 + init_stock: 336 + max_stock: 1000 + price: 350 + sale_gamma: 56 + service_level: 0.95 + SKU458: + constraint: null + cost: 282 + init_stock: 94 + max_stock: 1000 + price: 493 + sale_gamma: 47 + service_level: 0.95 + SKU459: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 180 + init_stock: 132 + max_stock: 1000 + price: 225 + sale_gamma: 22 + service_level: 0.95 + SKU46: + constraint: G(low_profit -> low_stock_constraint) + cost: 341 + init_stock: 100 + max_stock: 1000 + price: 569 + sale_gamma: 20 + service_level: 0.95 + SKU460: + constraint: G(low_profit -> low_stock_constraint) + cost: 271 + init_stock: 25 + max_stock: 1000 + price: 376 + sale_gamma: 5 + service_level: 0.95 + SKU461: + constraint: null + cost: 131 + init_stock: 135 + max_stock: 1000 + price: 233 + sale_gamma: 27 + service_level: 0.95 + SKU462: + constraint: G(low_profit -> low_stock_constraint) + cost: 98 + init_stock: 27 + max_stock: 1000 + price: 120 + sale_gamma: 9 + service_level: 0.95 + SKU463: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 117 + init_stock: 161 + max_stock: 1000 + price: 132 + sale_gamma: 23 + service_level: 0.95 + SKU464: + constraint: G(stock_constraint) + cost: 295 + init_stock: 144 + max_stock: 1000 + price: 566 + sale_gamma: 24 + service_level: 0.95 + SKU465: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 286 + init_stock: 462 + max_stock: 1000 + price: 540 + sale_gamma: 77 + service_level: 0.95 + SKU466: + constraint: null + cost: 278 + init_stock: 212 + max_stock: 1000 + price: 533 + sale_gamma: 53 + service_level: 0.95 + SKU467: + constraint: null + cost: 294 + init_stock: 48 + max_stock: 1000 + price: 432 + sale_gamma: 8 + service_level: 0.95 + SKU468: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 40 + init_stock: 70 + max_stock: 1000 + price: 50 + sale_gamma: 10 + service_level: 0.95 + SKU469: + constraint: null + cost: 481 + init_stock: 300 + max_stock: 1000 + price: 615 + sale_gamma: 100 + service_level: 0.95 + SKU47: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 460 + init_stock: 77 + max_stock: 1000 + price: 533 + sale_gamma: 11 + service_level: 0.95 + SKU470: + constraint: G(low_profit -> low_stock_constraint) + cost: 470 + init_stock: 183 + max_stock: 1000 + price: 761 + sale_gamma: 61 + service_level: 0.95 + SKU471: + constraint: null + cost: 98 + init_stock: 186 + max_stock: 1000 + price: 147 + sale_gamma: 31 + service_level: 0.95 + SKU472: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 22 + init_stock: 474 + max_stock: 1000 + price: 25 + sale_gamma: 79 + service_level: 0.95 + SKU473: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 414 + init_stock: 574 + max_stock: 1000 + price: 691 + sale_gamma: 82 + service_level: 0.95 + SKU474: + constraint: G(low_profit -> low_stock_constraint) + cost: 95 + init_stock: 205 + max_stock: 1000 + price: 132 + sale_gamma: 41 + service_level: 0.95 + SKU475: + constraint: G(low_profit -> low_stock_constraint) + cost: 390 + init_stock: 144 + max_stock: 1000 + price: 429 + sale_gamma: 36 + service_level: 0.95 + SKU476: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 468 + init_stock: 602 + max_stock: 1000 + price: 739 + sale_gamma: 86 + service_level: 0.95 + SKU477: + constraint: null + cost: 91 + init_stock: 160 + max_stock: 1000 + price: 144 + sale_gamma: 40 + service_level: 0.95 + SKU478: + constraint: G(stock_constraint) + cost: 242 + init_stock: 408 + max_stock: 1000 + price: 341 + sale_gamma: 51 + service_level: 0.95 + SKU479: + constraint: G(stock_constraint) + cost: 158 + init_stock: 250 + max_stock: 1000 + price: 301 + sale_gamma: 50 + service_level: 0.95 + SKU48: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 274 + init_stock: 96 + max_stock: 1000 + price: 402 + sale_gamma: 12 + service_level: 0.95 + SKU480: + constraint: null + cost: 424 + init_stock: 136 + max_stock: 1000 + price: 746 + sale_gamma: 68 + service_level: 0.95 + SKU481: + constraint: null + cost: 199 + init_stock: 294 + max_stock: 1000 + price: 256 + sale_gamma: 49 + service_level: 0.95 + SKU482: + constraint: null + cost: 217 + init_stock: 130 + max_stock: 1000 + price: 277 + sale_gamma: 26 + service_level: 0.95 + SKU483: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 120 + init_stock: 36 + max_stock: 1000 + price: 234 + sale_gamma: 12 + service_level: 0.95 + SKU484: + constraint: G(low_profit -> low_stock_constraint) + cost: 244 + init_stock: 560 + max_stock: 1000 + price: 314 + sale_gamma: 80 + service_level: 0.95 + SKU485: + constraint: G(stock_constraint) + cost: 266 + init_stock: 399 + max_stock: 1000 + price: 321 + sale_gamma: 57 + service_level: 0.95 + SKU486: + constraint: null + cost: 243 + init_stock: 36 + max_stock: 1000 + price: 478 + sale_gamma: 12 + service_level: 0.95 + SKU487: + constraint: null + cost: 275 + init_stock: 350 + max_stock: 1000 + price: 456 + sale_gamma: 70 + service_level: 0.95 + SKU488: + constraint: null + cost: 423 + init_stock: 26 + max_stock: 1000 + price: 681 + sale_gamma: 13 + service_level: 0.95 + SKU489: + constraint: G(stock_constraint) + cost: 241 + init_stock: 190 + max_stock: 1000 + price: 284 + sale_gamma: 38 + service_level: 0.95 + SKU49: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 184 + init_stock: 144 + max_stock: 1000 + price: 241 + sale_gamma: 72 + service_level: 0.95 + SKU490: + constraint: null + cost: 285 + init_stock: 96 + max_stock: 1000 + price: 547 + sale_gamma: 16 + service_level: 0.95 + SKU491: + constraint: null + cost: 334 + init_stock: 546 + max_stock: 1000 + price: 594 + sale_gamma: 78 + service_level: 0.95 + SKU492: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 371 + init_stock: 408 + max_stock: 1000 + price: 638 + sale_gamma: 68 + service_level: 0.95 + SKU493: + constraint: G(stock_constraint) + cost: 402 + init_stock: 380 + max_stock: 1000 + price: 619 + sale_gamma: 95 + service_level: 0.95 + SKU494: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 84 + init_stock: 348 + max_stock: 1000 + price: 153 + sale_gamma: 87 + service_level: 0.95 + SKU495: + constraint: null + cost: 347 + init_stock: 259 + max_stock: 1000 + price: 572 + sale_gamma: 37 + service_level: 0.95 + SKU496: + constraint: null + cost: 177 + init_stock: 159 + max_stock: 1000 + price: 196 + sale_gamma: 53 + service_level: 0.95 + SKU497: + constraint: null + cost: 260 + init_stock: 217 + max_stock: 1000 + price: 473 + sale_gamma: 31 + service_level: 0.95 + SKU498: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 447 + init_stock: 282 + max_stock: 1000 + price: 867 + sale_gamma: 94 + service_level: 0.95 + SKU499: + constraint: null + cost: 425 + init_stock: 468 + max_stock: 1000 + price: 522 + sale_gamma: 78 + service_level: 0.95 + SKU5: + constraint: null + cost: 478 + init_stock: 444 + max_stock: 1000 + price: 688 + sale_gamma: 74 + service_level: 0.95 + SKU50: + constraint: null + cost: 427 + init_stock: 36 + max_stock: 1000 + price: 768 + sale_gamma: 18 + service_level: 0.95 + SKU500: + constraint: G(stock_constraint) + cost: 413 + init_stock: 50 + max_stock: 1000 + price: 536 + sale_gamma: 10 + service_level: 0.95 + SKU501: + constraint: null + cost: 74 + init_stock: 312 + max_stock: 1000 + price: 147 + sale_gamma: 78 + service_level: 0.95 + SKU502: + constraint: null + cost: 411 + init_stock: 18 + max_stock: 1000 + price: 505 + sale_gamma: 9 + service_level: 0.95 + SKU503: + constraint: G(low_profit -> low_stock_constraint) + cost: 459 + init_stock: 176 + max_stock: 1000 + price: 885 + sale_gamma: 44 + service_level: 0.95 + SKU504: + constraint: null + cost: 325 + init_stock: 200 + max_stock: 1000 + price: 484 + sale_gamma: 40 + service_level: 0.95 + SKU505: + constraint: null + cost: 324 + init_stock: 432 + max_stock: 1000 + price: 573 + sale_gamma: 72 + service_level: 0.95 + SKU506: + constraint: G(low_profit -> low_stock_constraint) + cost: 477 + init_stock: 40 + max_stock: 1000 + price: 896 + sale_gamma: 8 + service_level: 0.95 + SKU507: + constraint: G(low_profit -> low_stock_constraint) + cost: 36 + init_stock: 142 + max_stock: 1000 + price: 65 + sale_gamma: 71 + service_level: 0.95 + SKU508: + constraint: null + cost: 390 + init_stock: 342 + max_stock: 1000 + price: 546 + sale_gamma: 57 + service_level: 0.95 + SKU509: + constraint: null + cost: 464 + init_stock: 355 + max_stock: 1000 + price: 928 + sale_gamma: 71 + service_level: 0.95 + SKU51: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 434 + init_stock: 196 + max_stock: 1000 + price: 598 + sale_gamma: 49 + service_level: 0.95 + SKU510: + constraint: G(low_profit -> low_stock_constraint) + cost: 233 + init_stock: 220 + max_stock: 1000 + price: 267 + sale_gamma: 55 + service_level: 0.95 + SKU511: + constraint: G(stock_constraint) + cost: 317 + init_stock: 162 + max_stock: 1000 + price: 535 + sale_gamma: 27 + service_level: 0.95 + SKU512: + constraint: null + cost: 405 + init_stock: 360 + max_stock: 1000 + price: 619 + sale_gamma: 72 + service_level: 0.95 + SKU513: + constraint: null + cost: 33 + init_stock: 196 + max_stock: 1000 + price: 50 + sale_gamma: 49 + service_level: 0.95 + SKU514: + constraint: null + cost: 441 + init_stock: 144 + max_stock: 1000 + price: 657 + sale_gamma: 24 + service_level: 0.95 + SKU515: + constraint: null + cost: 456 + init_stock: 140 + max_stock: 1000 + price: 579 + sale_gamma: 35 + service_level: 0.95 + SKU516: + constraint: G(stock_constraint) + cost: 432 + init_stock: 96 + max_stock: 1000 + price: 842 + sale_gamma: 12 + service_level: 0.95 + SKU517: + constraint: null + cost: 223 + init_stock: 316 + max_stock: 1000 + price: 399 + sale_gamma: 79 + service_level: 0.95 + SKU518: + constraint: G(low_profit -> low_stock_constraint) + cost: 155 + init_stock: 616 + max_stock: 1000 + price: 230 + sale_gamma: 77 + service_level: 0.95 + SKU519: + constraint: null + cost: 276 + init_stock: 329 + max_stock: 1000 + price: 322 + sale_gamma: 47 + service_level: 0.95 + SKU52: + constraint: null + cost: 340 + init_stock: 594 + max_stock: 1000 + price: 428 + sale_gamma: 99 + service_level: 0.95 + SKU520: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 18 + init_stock: 145 + max_stock: 1000 + price: 26 + sale_gamma: 29 + service_level: 0.95 + SKU521: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 356 + init_stock: 455 + max_stock: 1000 + price: 647 + sale_gamma: 91 + service_level: 0.95 + SKU522: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 389 + init_stock: 210 + max_stock: 1000 + price: 778 + sale_gamma: 30 + service_level: 0.95 + SKU523: + constraint: G(stock_constraint) + cost: 359 + init_stock: 100 + max_stock: 1000 + price: 437 + sale_gamma: 25 + service_level: 0.95 + SKU524: + constraint: G(stock_constraint) + cost: 213 + init_stock: 117 + max_stock: 1000 + price: 387 + sale_gamma: 39 + service_level: 0.95 + SKU525: + constraint: null + cost: 131 + init_stock: 66 + max_stock: 1000 + price: 224 + sale_gamma: 11 + service_level: 0.95 + SKU526: + constraint: null + cost: 237 + init_stock: 72 + max_stock: 1000 + price: 310 + sale_gamma: 36 + service_level: 0.95 + SKU527: + constraint: G(low_profit -> low_stock_constraint) + cost: 161 + init_stock: 72 + max_stock: 1000 + price: 262 + sale_gamma: 12 + service_level: 0.95 + SKU528: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 384 + init_stock: 343 + max_stock: 1000 + price: 552 + sale_gamma: 49 + service_level: 0.95 + SKU529: + constraint: null + cost: 321 + init_stock: 52 + max_stock: 1000 + price: 455 + sale_gamma: 13 + service_level: 0.95 + SKU53: + constraint: G(low_profit -> low_stock_constraint) + cost: 20 + init_stock: 294 + max_stock: 1000 + price: 36 + sale_gamma: 42 + service_level: 0.95 + SKU530: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 299 + init_stock: 480 + max_stock: 1000 + price: 550 + sale_gamma: 96 + service_level: 0.95 + SKU531: + constraint: null + cost: 335 + init_stock: 300 + max_stock: 1000 + price: 452 + sale_gamma: 60 + service_level: 0.95 + SKU532: + constraint: null + cost: 155 + init_stock: 528 + max_stock: 1000 + price: 181 + sale_gamma: 88 + service_level: 0.95 + SKU533: + constraint: null + cost: 444 + init_stock: 205 + max_stock: 1000 + price: 621 + sale_gamma: 41 + service_level: 0.95 + SKU534: + constraint: G(stock_constraint) + cost: 146 + init_stock: 485 + max_stock: 1000 + price: 289 + sale_gamma: 97 + service_level: 0.95 + SKU535: + constraint: null + cost: 384 + init_stock: 210 + max_stock: 1000 + price: 733 + sale_gamma: 35 + service_level: 0.95 + SKU536: + constraint: null + cost: 86 + init_stock: 300 + max_stock: 1000 + price: 137 + sale_gamma: 100 + service_level: 0.95 + SKU537: + constraint: null + cost: 173 + init_stock: 282 + max_stock: 1000 + price: 223 + sale_gamma: 47 + service_level: 0.95 + SKU538: + constraint: null + cost: 40 + init_stock: 329 + max_stock: 1000 + price: 53 + sale_gamma: 47 + service_level: 0.95 + SKU539: + constraint: null + cost: 390 + init_stock: 40 + max_stock: 1000 + price: 729 + sale_gamma: 10 + service_level: 0.95 + SKU54: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 162 + init_stock: 462 + max_stock: 1000 + price: 278 + sale_gamma: 77 + service_level: 0.95 + SKU540: + constraint: G(stock_constraint) + cost: 290 + init_stock: 240 + max_stock: 1000 + price: 568 + sale_gamma: 48 + service_level: 0.95 + SKU541: + constraint: null + cost: 192 + init_stock: 288 + max_stock: 1000 + price: 384 + sale_gamma: 36 + service_level: 0.95 + SKU542: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 96 + init_stock: 184 + max_stock: 1000 + price: 114 + sale_gamma: 46 + service_level: 0.95 + SKU543: + constraint: null + cost: 197 + init_stock: 280 + max_stock: 1000 + price: 222 + sale_gamma: 40 + service_level: 0.95 + SKU544: + constraint: null + cost: 408 + init_stock: 448 + max_stock: 1000 + price: 501 + sale_gamma: 64 + service_level: 0.95 + SKU545: + constraint: null + cost: 431 + init_stock: 195 + max_stock: 1000 + price: 853 + sale_gamma: 65 + service_level: 0.95 + SKU546: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 88 + init_stock: 85 + max_stock: 1000 + price: 168 + sale_gamma: 17 + service_level: 0.95 + SKU547: + constraint: null + cost: 454 + init_stock: 384 + max_stock: 1000 + price: 744 + sale_gamma: 96 + service_level: 0.95 + SKU548: + constraint: null + cost: 415 + init_stock: 637 + max_stock: 1000 + price: 489 + sale_gamma: 91 + service_level: 0.95 + SKU549: + constraint: null + cost: 307 + init_stock: 290 + max_stock: 1000 + price: 435 + sale_gamma: 58 + service_level: 0.95 + SKU55: + constraint: G(low_profit -> low_stock_constraint) + cost: 106 + init_stock: 147 + max_stock: 1000 + price: 163 + sale_gamma: 21 + service_level: 0.95 + SKU550: + constraint: null + cost: 446 + init_stock: 76 + max_stock: 1000 + price: 611 + sale_gamma: 19 + service_level: 0.95 + SKU551: + constraint: null + cost: 393 + init_stock: 420 + max_stock: 1000 + price: 577 + sale_gamma: 60 + service_level: 0.95 + SKU552: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 114 + init_stock: 504 + max_stock: 1000 + price: 224 + sale_gamma: 84 + service_level: 0.95 + SKU553: + constraint: null + cost: 71 + init_stock: 245 + max_stock: 1000 + price: 112 + sale_gamma: 35 + service_level: 0.95 + SKU554: + constraint: null + cost: 314 + init_stock: 39 + max_stock: 1000 + price: 411 + sale_gamma: 13 + service_level: 0.95 + SKU555: + constraint: G(stock_constraint) + cost: 492 + init_stock: 68 + max_stock: 1000 + price: 585 + sale_gamma: 17 + service_level: 0.95 + SKU556: + constraint: null + cost: 96 + init_stock: 198 + max_stock: 1000 + price: 187 + sale_gamma: 66 + service_level: 0.95 + SKU557: + constraint: null + cost: 303 + init_stock: 576 + max_stock: 1000 + price: 515 + sale_gamma: 72 + service_level: 0.95 + SKU558: + constraint: null + cost: 430 + init_stock: 126 + max_stock: 1000 + price: 748 + sale_gamma: 21 + service_level: 0.95 + SKU559: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 222 + init_stock: 354 + max_stock: 1000 + price: 357 + sale_gamma: 59 + service_level: 0.95 + SKU56: + constraint: null + cost: 435 + init_stock: 92 + max_stock: 1000 + price: 539 + sale_gamma: 23 + service_level: 0.95 + SKU560: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 146 + init_stock: 534 + max_stock: 1000 + price: 290 + sale_gamma: 89 + service_level: 0.95 + SKU561: + constraint: null + cost: 158 + init_stock: 287 + max_stock: 1000 + price: 243 + sale_gamma: 41 + service_level: 0.95 + SKU562: + constraint: G(low_profit -> low_stock_constraint) + cost: 51 + init_stock: 370 + max_stock: 1000 + price: 96 + sale_gamma: 74 + service_level: 0.95 + SKU563: + constraint: G(low_profit -> low_stock_constraint) + cost: 418 + init_stock: 75 + max_stock: 1000 + price: 727 + sale_gamma: 25 + service_level: 0.95 + SKU564: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 442 + init_stock: 120 + max_stock: 1000 + price: 800 + sale_gamma: 24 + service_level: 0.95 + SKU565: + constraint: G(stock_constraint) + cost: 310 + init_stock: 348 + max_stock: 1000 + price: 582 + sale_gamma: 87 + service_level: 0.95 + SKU566: + constraint: G(low_profit -> low_stock_constraint) + cost: 34 + init_stock: 30 + max_stock: 1000 + price: 46 + sale_gamma: 6 + service_level: 0.95 + SKU567: + constraint: null + cost: 120 + init_stock: 172 + max_stock: 1000 + price: 195 + sale_gamma: 43 + service_level: 0.95 + SKU568: + constraint: null + cost: 97 + init_stock: 32 + max_stock: 1000 + price: 109 + sale_gamma: 8 + service_level: 0.95 + SKU569: + constraint: null + cost: 285 + init_stock: 376 + max_stock: 1000 + price: 413 + sale_gamma: 94 + service_level: 0.95 + SKU57: + constraint: null + cost: 400 + init_stock: 210 + max_stock: 1000 + price: 688 + sale_gamma: 30 + service_level: 0.95 + SKU570: + constraint: null + cost: 161 + init_stock: 301 + max_stock: 1000 + price: 209 + sale_gamma: 43 + service_level: 0.95 + SKU571: + constraint: null + cost: 50 + init_stock: 410 + max_stock: 1000 + price: 91 + sale_gamma: 82 + service_level: 0.95 + SKU572: + constraint: null + cost: 489 + init_stock: 144 + max_stock: 1000 + price: 655 + sale_gamma: 48 + service_level: 0.95 + SKU573: + constraint: null + cost: 274 + init_stock: 259 + max_stock: 1000 + price: 476 + sale_gamma: 37 + service_level: 0.95 + SKU574: + constraint: null + cost: 357 + init_stock: 224 + max_stock: 1000 + price: 392 + sale_gamma: 32 + service_level: 0.95 + SKU575: + constraint: null + cost: 265 + init_stock: 588 + max_stock: 1000 + price: 511 + sale_gamma: 98 + service_level: 0.95 + SKU576: + constraint: null + cost: 137 + init_stock: 335 + max_stock: 1000 + price: 165 + sale_gamma: 67 + service_level: 0.95 + SKU577: + constraint: null + cost: 420 + init_stock: 320 + max_stock: 1000 + price: 793 + sale_gamma: 64 + service_level: 0.95 + SKU578: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 74 + init_stock: 120 + max_stock: 1000 + price: 115 + sale_gamma: 15 + service_level: 0.95 + SKU579: + constraint: G(low_profit -> low_stock_constraint) + cost: 310 + init_stock: 280 + max_stock: 1000 + price: 589 + sale_gamma: 56 + service_level: 0.95 + SKU58: + constraint: G(low_profit -> low_stock_constraint) + cost: 217 + init_stock: 486 + max_stock: 1000 + price: 318 + sale_gamma: 81 + service_level: 0.95 + SKU580: + constraint: null + cost: 153 + init_stock: 594 + max_stock: 1000 + price: 241 + sale_gamma: 99 + service_level: 0.95 + SKU581: + constraint: null + cost: 494 + init_stock: 306 + max_stock: 1000 + price: 696 + sale_gamma: 51 + service_level: 0.95 + SKU582: + constraint: null + cost: 476 + init_stock: 52 + max_stock: 1000 + price: 932 + sale_gamma: 13 + service_level: 0.95 + SKU583: + constraint: G(low_profit -> low_stock_constraint) + cost: 105 + init_stock: 55 + max_stock: 1000 + price: 132 + sale_gamma: 11 + service_level: 0.95 + SKU584: + constraint: null + cost: 365 + init_stock: 235 + max_stock: 1000 + price: 591 + sale_gamma: 47 + service_level: 0.95 + SKU585: + constraint: G(stock_constraint) + cost: 184 + init_stock: 216 + max_stock: 1000 + price: 331 + sale_gamma: 54 + service_level: 0.95 + SKU586: + constraint: null + cost: 471 + init_stock: 470 + max_stock: 1000 + price: 819 + sale_gamma: 94 + service_level: 0.95 + SKU587: + constraint: G(low_profit -> low_stock_constraint) + cost: 197 + init_stock: 160 + max_stock: 1000 + price: 364 + sale_gamma: 80 + service_level: 0.95 + SKU588: + constraint: null + cost: 15 + init_stock: 189 + max_stock: 1000 + price: 22 + sale_gamma: 27 + service_level: 0.95 + SKU589: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 185 + init_stock: 125 + max_stock: 1000 + price: 325 + sale_gamma: 25 + service_level: 0.95 + SKU59: + constraint: null + cost: 39 + init_stock: 96 + max_stock: 1000 + price: 47 + sale_gamma: 48 + service_level: 0.95 + SKU590: + constraint: null + cost: 348 + init_stock: 310 + max_stock: 1000 + price: 480 + sale_gamma: 62 + service_level: 0.95 + SKU591: + constraint: null + cost: 163 + init_stock: 148 + max_stock: 1000 + price: 237 + sale_gamma: 37 + service_level: 0.95 + SKU592: + constraint: G(low_profit -> low_stock_constraint) + cost: 305 + init_stock: 552 + max_stock: 1000 + price: 497 + sale_gamma: 92 + service_level: 0.95 + SKU593: + constraint: G(low_profit -> low_stock_constraint) + cost: 479 + init_stock: 204 + max_stock: 1000 + price: 871 + sale_gamma: 51 + service_level: 0.95 + SKU594: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 478 + init_stock: 204 + max_stock: 1000 + price: 855 + sale_gamma: 51 + service_level: 0.95 + SKU595: + constraint: G(stock_constraint) + cost: 35 + init_stock: 110 + max_stock: 1000 + price: 57 + sale_gamma: 55 + service_level: 0.95 + SKU596: + constraint: null + cost: 449 + init_stock: 189 + max_stock: 1000 + price: 884 + sale_gamma: 27 + service_level: 0.95 + SKU597: + constraint: G(stock_constraint) + cost: 21 + init_stock: 384 + max_stock: 1000 + price: 31 + sale_gamma: 96 + service_level: 0.95 + SKU598: + constraint: G(low_profit -> low_stock_constraint) + cost: 49 + init_stock: 195 + max_stock: 1000 + price: 67 + sale_gamma: 39 + service_level: 0.95 + SKU599: + constraint: null + cost: 201 + init_stock: 380 + max_stock: 1000 + price: 391 + sale_gamma: 76 + service_level: 0.95 + SKU6: + constraint: null + cost: 472 + init_stock: 328 + max_stock: 1000 + price: 741 + sale_gamma: 41 + service_level: 0.95 + SKU60: + constraint: G(stock_constraint) + cost: 24 + init_stock: 255 + max_stock: 1000 + price: 33 + sale_gamma: 85 + service_level: 0.95 + SKU600: + constraint: G(low_profit -> low_stock_constraint) + cost: 321 + init_stock: 165 + max_stock: 1000 + price: 404 + sale_gamma: 55 + service_level: 0.95 + SKU601: + constraint: G(low_profit -> low_stock_constraint) + cost: 185 + init_stock: 60 + max_stock: 1000 + price: 301 + sale_gamma: 10 + service_level: 0.95 + SKU602: + constraint: G(low_profit -> low_stock_constraint) + cost: 180 + init_stock: 564 + max_stock: 1000 + price: 237 + sale_gamma: 94 + service_level: 0.95 + SKU603: + constraint: null + cost: 328 + init_stock: 665 + max_stock: 1000 + price: 367 + sale_gamma: 95 + service_level: 0.95 + SKU604: + constraint: null + cost: 143 + init_stock: 576 + max_stock: 1000 + price: 194 + sale_gamma: 96 + service_level: 0.95 + SKU605: + constraint: null + cost: 337 + init_stock: 44 + max_stock: 1000 + price: 461 + sale_gamma: 22 + service_level: 0.95 + SKU606: + constraint: G(low_profit -> low_stock_constraint) + cost: 166 + init_stock: 150 + max_stock: 1000 + price: 252 + sale_gamma: 25 + service_level: 0.95 + SKU607: + constraint: null + cost: 286 + init_stock: 405 + max_stock: 1000 + price: 363 + sale_gamma: 81 + service_level: 0.95 + SKU608: + constraint: G(stock_constraint) + cost: 18 + init_stock: 123 + max_stock: 1000 + price: 36 + sale_gamma: 41 + service_level: 0.95 + SKU609: + constraint: null + cost: 330 + init_stock: 390 + max_stock: 1000 + price: 435 + sale_gamma: 78 + service_level: 0.95 + SKU61: + constraint: null + cost: 422 + init_stock: 60 + max_stock: 1000 + price: 738 + sale_gamma: 10 + service_level: 0.95 + SKU610: + constraint: null + cost: 489 + init_stock: 266 + max_stock: 1000 + price: 885 + sale_gamma: 38 + service_level: 0.95 + SKU611: + constraint: null + cost: 379 + init_stock: 140 + max_stock: 1000 + price: 640 + sale_gamma: 35 + service_level: 0.95 + SKU612: + constraint: G(stock_constraint) + cost: 28 + init_stock: 114 + max_stock: 1000 + price: 44 + sale_gamma: 38 + service_level: 0.95 + SKU613: + constraint: null + cost: 303 + init_stock: 147 + max_stock: 1000 + price: 333 + sale_gamma: 49 + service_level: 0.95 + SKU614: + constraint: null + cost: 264 + init_stock: 148 + max_stock: 1000 + price: 512 + sale_gamma: 37 + service_level: 0.95 + SKU615: + constraint: null + cost: 337 + init_stock: 246 + max_stock: 1000 + price: 647 + sale_gamma: 41 + service_level: 0.95 + SKU616: + constraint: null + cost: 309 + init_stock: 366 + max_stock: 1000 + price: 423 + sale_gamma: 61 + service_level: 0.95 + SKU617: + constraint: null + cost: 237 + init_stock: 208 + max_stock: 1000 + price: 471 + sale_gamma: 52 + service_level: 0.95 + SKU618: + constraint: null + cost: 143 + init_stock: 287 + max_stock: 1000 + price: 278 + sale_gamma: 41 + service_level: 0.95 + SKU619: + constraint: null + cost: 60 + init_stock: 216 + max_stock: 1000 + price: 115 + sale_gamma: 36 + service_level: 0.95 + SKU62: + constraint: null + cost: 92 + init_stock: 287 + max_stock: 1000 + price: 124 + sale_gamma: 41 + service_level: 0.95 + SKU620: + constraint: null + cost: 478 + init_stock: 147 + max_stock: 1000 + price: 807 + sale_gamma: 49 + service_level: 0.95 + SKU621: + constraint: null + cost: 216 + init_stock: 150 + max_stock: 1000 + price: 347 + sale_gamma: 30 + service_level: 0.95 + SKU622: + constraint: null + cost: 270 + init_stock: 120 + max_stock: 1000 + price: 513 + sale_gamma: 24 + service_level: 0.95 + SKU623: + constraint: null + cost: 448 + init_stock: 190 + max_stock: 1000 + price: 698 + sale_gamma: 38 + service_level: 0.95 + SKU624: + constraint: G(stock_constraint) + cost: 429 + init_stock: 220 + max_stock: 1000 + price: 600 + sale_gamma: 55 + service_level: 0.95 + SKU625: + constraint: null + cost: 236 + init_stock: 468 + max_stock: 1000 + price: 448 + sale_gamma: 78 + service_level: 0.95 + SKU626: + constraint: null + cost: 135 + init_stock: 128 + max_stock: 1000 + price: 170 + sale_gamma: 32 + service_level: 0.95 + SKU627: + constraint: null + cost: 78 + init_stock: 312 + max_stock: 1000 + price: 102 + sale_gamma: 39 + service_level: 0.95 + SKU628: + constraint: null + cost: 114 + init_stock: 528 + max_stock: 1000 + price: 176 + sale_gamma: 88 + service_level: 0.95 + SKU629: + constraint: G(low_profit -> low_stock_constraint) + cost: 390 + init_stock: 324 + max_stock: 1000 + price: 557 + sale_gamma: 54 + service_level: 0.95 + SKU63: + constraint: null + cost: 341 + init_stock: 138 + max_stock: 1000 + price: 579 + sale_gamma: 69 + service_level: 0.95 + SKU630: + constraint: null + cost: 500 + init_stock: 168 + max_stock: 1000 + price: 750 + sale_gamma: 84 + service_level: 0.95 + SKU631: + constraint: null + cost: 386 + init_stock: 511 + max_stock: 1000 + price: 683 + sale_gamma: 73 + service_level: 0.95 + SKU632: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 72 + init_stock: 238 + max_stock: 1000 + price: 142 + sale_gamma: 34 + service_level: 0.95 + SKU633: + constraint: null + cost: 306 + init_stock: 290 + max_stock: 1000 + price: 529 + sale_gamma: 58 + service_level: 0.95 + SKU634: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 136 + init_stock: 504 + max_stock: 1000 + price: 229 + sale_gamma: 84 + service_level: 0.95 + SKU635: + constraint: null + cost: 463 + init_stock: 270 + max_stock: 1000 + price: 800 + sale_gamma: 45 + service_level: 0.95 + SKU636: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 243 + init_stock: 285 + max_stock: 1000 + price: 345 + sale_gamma: 57 + service_level: 0.95 + SKU637: + constraint: null + cost: 498 + init_stock: 150 + max_stock: 1000 + price: 552 + sale_gamma: 30 + service_level: 0.95 + SKU638: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 93 + init_stock: 264 + max_stock: 1000 + price: 119 + sale_gamma: 66 + service_level: 0.95 + SKU639: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 476 + init_stock: 40 + max_stock: 1000 + price: 580 + sale_gamma: 10 + service_level: 0.95 + SKU64: + constraint: G(stock_constraint) + cost: 151 + init_stock: 480 + max_stock: 1000 + price: 283 + sale_gamma: 96 + service_level: 0.95 + SKU640: + constraint: null + cost: 350 + init_stock: 280 + max_stock: 1000 + price: 567 + sale_gamma: 56 + service_level: 0.95 + SKU641: + constraint: null + cost: 434 + init_stock: 216 + max_stock: 1000 + price: 729 + sale_gamma: 36 + service_level: 0.95 + SKU642: + constraint: null + cost: 349 + init_stock: 576 + max_stock: 1000 + price: 401 + sale_gamma: 72 + service_level: 0.95 + SKU643: + constraint: null + cost: 17 + init_stock: 54 + max_stock: 1000 + price: 22 + sale_gamma: 27 + service_level: 0.95 + SKU644: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 27 + init_stock: 186 + max_stock: 1000 + price: 46 + sale_gamma: 93 + service_level: 0.95 + SKU645: + constraint: G(low_profit -> low_stock_constraint) + cost: 143 + init_stock: 45 + max_stock: 1000 + price: 174 + sale_gamma: 9 + service_level: 0.95 + SKU646: + constraint: null + cost: 320 + init_stock: 435 + max_stock: 1000 + price: 636 + sale_gamma: 87 + service_level: 0.95 + SKU647: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 208 + max_stock: 1000 + price: 500 + sale_gamma: 26 + service_level: 0.95 + SKU648: + constraint: null + cost: 251 + init_stock: 240 + max_stock: 1000 + price: 293 + sale_gamma: 48 + service_level: 0.95 + SKU649: + constraint: null + cost: 171 + init_stock: 430 + max_stock: 1000 + price: 188 + sale_gamma: 86 + service_level: 0.95 + SKU65: + constraint: null + cost: 48 + init_stock: 400 + max_stock: 1000 + price: 90 + sale_gamma: 80 + service_level: 0.95 + SKU650: + constraint: null + cost: 406 + init_stock: 282 + max_stock: 1000 + price: 633 + sale_gamma: 47 + service_level: 0.95 + SKU651: + constraint: null + cost: 209 + init_stock: 426 + max_stock: 1000 + price: 395 + sale_gamma: 71 + service_level: 0.95 + SKU652: + constraint: G(low_profit -> low_stock_constraint) + cost: 167 + init_stock: 544 + max_stock: 1000 + price: 198 + sale_gamma: 68 + service_level: 0.95 + SKU653: + constraint: null + cost: 122 + init_stock: 48 + max_stock: 1000 + price: 180 + sale_gamma: 6 + service_level: 0.95 + SKU654: + constraint: null + cost: 344 + init_stock: 230 + max_stock: 1000 + price: 674 + sale_gamma: 46 + service_level: 0.95 + SKU655: + constraint: null + cost: 382 + init_stock: 445 + max_stock: 1000 + price: 569 + sale_gamma: 89 + service_level: 0.95 + SKU656: + constraint: null + cost: 229 + init_stock: 665 + max_stock: 1000 + price: 387 + sale_gamma: 95 + service_level: 0.95 + SKU657: + constraint: null + cost: 474 + init_stock: 462 + max_stock: 1000 + price: 838 + sale_gamma: 77 + service_level: 0.95 + SKU658: + constraint: null + cost: 307 + init_stock: 215 + max_stock: 1000 + price: 432 + sale_gamma: 43 + service_level: 0.95 + SKU659: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 392 + init_stock: 125 + max_stock: 1000 + price: 709 + sale_gamma: 25 + service_level: 0.95 + SKU66: + constraint: G(low_profit -> low_stock_constraint) + cost: 492 + init_stock: 150 + max_stock: 1000 + price: 905 + sale_gamma: 50 + service_level: 0.95 + SKU660: + constraint: G(stock_constraint) + cost: 206 + init_stock: 48 + max_stock: 1000 + price: 364 + sale_gamma: 12 + service_level: 0.95 + SKU661: + constraint: G(low_profit -> low_stock_constraint) + cost: 100 + init_stock: 600 + max_stock: 1000 + price: 135 + sale_gamma: 100 + service_level: 0.95 + SKU662: + constraint: G(low_profit -> low_stock_constraint) + cost: 283 + init_stock: 12 + max_stock: 1000 + price: 489 + sale_gamma: 6 + service_level: 0.95 + SKU663: + constraint: null + cost: 416 + init_stock: 220 + max_stock: 1000 + price: 777 + sale_gamma: 44 + service_level: 0.95 + SKU664: + constraint: null + cost: 93 + init_stock: 210 + max_stock: 1000 + price: 135 + sale_gamma: 70 + service_level: 0.95 + SKU665: + constraint: null + cost: 415 + init_stock: 594 + max_stock: 1000 + price: 705 + sale_gamma: 99 + service_level: 0.95 + SKU666: + constraint: null + cost: 378 + init_stock: 20 + max_stock: 1000 + price: 597 + sale_gamma: 5 + service_level: 0.95 + SKU667: + constraint: null + cost: 114 + init_stock: 390 + max_stock: 1000 + price: 159 + sale_gamma: 78 + service_level: 0.95 + SKU668: + constraint: null + cost: 115 + init_stock: 48 + max_stock: 1000 + price: 161 + sale_gamma: 16 + service_level: 0.95 + SKU669: + constraint: null + cost: 414 + init_stock: 140 + max_stock: 1000 + price: 654 + sale_gamma: 20 + service_level: 0.95 + SKU67: + constraint: G(stock_constraint) + cost: 16 + init_stock: 248 + max_stock: 1000 + price: 20 + sale_gamma: 62 + service_level: 0.95 + SKU670: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 171 + init_stock: 462 + max_stock: 1000 + price: 340 + sale_gamma: 66 + service_level: 0.95 + SKU671: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 163 + init_stock: 234 + max_stock: 1000 + price: 252 + sale_gamma: 39 + service_level: 0.95 + SKU672: + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 33 + max_stock: 1000 + price: 128 + sale_gamma: 11 + service_level: 0.95 + SKU673: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 437 + init_stock: 592 + max_stock: 1000 + price: 804 + sale_gamma: 74 + service_level: 0.95 + SKU674: + constraint: null + cost: 50 + init_stock: 133 + max_stock: 1000 + price: 85 + sale_gamma: 19 + service_level: 0.95 + SKU675: + constraint: null + cost: 479 + init_stock: 100 + max_stock: 1000 + price: 555 + sale_gamma: 20 + service_level: 0.95 + SKU676: + constraint: G(low_profit -> low_stock_constraint) + cost: 460 + init_stock: 196 + max_stock: 1000 + price: 542 + sale_gamma: 49 + service_level: 0.95 + SKU677: + constraint: G(stock_constraint) + cost: 392 + init_stock: 470 + max_stock: 1000 + price: 666 + sale_gamma: 94 + service_level: 0.95 + SKU678: + constraint: null + cost: 143 + init_stock: 268 + max_stock: 1000 + price: 195 + sale_gamma: 67 + service_level: 0.95 + SKU679: + constraint: G(stock_constraint) + cost: 131 + init_stock: 492 + max_stock: 1000 + price: 248 + sale_gamma: 82 + service_level: 0.95 + SKU68: + constraint: G(stock_constraint) + cost: 108 + init_stock: 322 + max_stock: 1000 + price: 212 + sale_gamma: 46 + service_level: 0.95 + SKU680: + constraint: null + cost: 436 + init_stock: 48 + max_stock: 1000 + price: 566 + sale_gamma: 24 + service_level: 0.95 + SKU681: + constraint: null + cost: 338 + init_stock: 65 + max_stock: 1000 + price: 557 + sale_gamma: 13 + service_level: 0.95 + SKU682: + constraint: null + cost: 146 + init_stock: 80 + max_stock: 1000 + price: 251 + sale_gamma: 16 + service_level: 0.95 + SKU683: + constraint: null + cost: 486 + init_stock: 144 + max_stock: 1000 + price: 903 + sale_gamma: 36 + service_level: 0.95 + SKU684: + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 288 + max_stock: 1000 + price: 466 + sale_gamma: 72 + service_level: 0.95 + SKU685: + constraint: null + cost: 69 + init_stock: 462 + max_stock: 1000 + price: 89 + sale_gamma: 77 + service_level: 0.95 + SKU686: + constraint: null + cost: 56 + init_stock: 135 + max_stock: 1000 + price: 63 + sale_gamma: 27 + service_level: 0.95 + SKU687: + constraint: null + cost: 308 + init_stock: 144 + max_stock: 1000 + price: 502 + sale_gamma: 48 + service_level: 0.95 + SKU688: + constraint: null + cost: 200 + init_stock: 159 + max_stock: 1000 + price: 256 + sale_gamma: 53 + service_level: 0.95 + SKU689: + constraint: G(stock_constraint) + cost: 36 + init_stock: 80 + max_stock: 1000 + price: 42 + sale_gamma: 16 + service_level: 0.95 + SKU69: + constraint: G(low_profit -> low_stock_constraint) + cost: 329 + init_stock: 168 + max_stock: 1000 + price: 628 + sale_gamma: 28 + service_level: 0.95 + SKU690: + constraint: G(low_profit -> low_stock_constraint) + cost: 283 + init_stock: 21 + max_stock: 1000 + price: 455 + sale_gamma: 7 + service_level: 0.95 + SKU691: + constraint: null + cost: 422 + init_stock: 294 + max_stock: 1000 + price: 776 + sale_gamma: 98 + service_level: 0.95 + SKU692: + constraint: null + cost: 457 + init_stock: 325 + max_stock: 1000 + price: 539 + sale_gamma: 65 + service_level: 0.95 + SKU693: + constraint: null + cost: 351 + init_stock: 240 + max_stock: 1000 + price: 414 + sale_gamma: 48 + service_level: 0.95 + SKU694: + constraint: G(stock_constraint) + cost: 441 + init_stock: 256 + max_stock: 1000 + price: 630 + sale_gamma: 32 + service_level: 0.95 + SKU695: + constraint: G(stock_constraint) + cost: 344 + init_stock: 490 + max_stock: 1000 + price: 550 + sale_gamma: 98 + service_level: 0.95 + SKU696: + constraint: null + cost: 378 + init_stock: 175 + max_stock: 1000 + price: 597 + sale_gamma: 35 + service_level: 0.95 + SKU697: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 483 + init_stock: 552 + max_stock: 1000 + price: 859 + sale_gamma: 92 + service_level: 0.95 + SKU698: + constraint: null + cost: 53 + init_stock: 350 + max_stock: 1000 + price: 91 + sale_gamma: 50 + service_level: 0.95 + SKU699: + constraint: G(stock_constraint) + cost: 265 + init_stock: 192 + max_stock: 1000 + price: 291 + sale_gamma: 64 + service_level: 0.95 + SKU7: + constraint: null + cost: 389 + init_stock: 36 + max_stock: 1000 + price: 634 + sale_gamma: 12 + service_level: 0.95 + SKU70: + constraint: null + cost: 140 + init_stock: 152 + max_stock: 1000 + price: 186 + sale_gamma: 38 + service_level: 0.95 + SKU700: + constraint: G(stock_constraint) + cost: 122 + init_stock: 120 + max_stock: 1000 + price: 165 + sale_gamma: 20 + service_level: 0.95 + SKU701: + constraint: null + cost: 394 + init_stock: 114 + max_stock: 1000 + price: 693 + sale_gamma: 38 + service_level: 0.95 + SKU702: + constraint: G(stock_constraint) + cost: 78 + init_stock: 108 + max_stock: 1000 + price: 146 + sale_gamma: 27 + service_level: 0.95 + SKU703: + constraint: null + cost: 472 + init_stock: 355 + max_stock: 1000 + price: 892 + sale_gamma: 71 + service_level: 0.95 + SKU704: + constraint: G(stock_constraint) + cost: 384 + init_stock: 132 + max_stock: 1000 + price: 480 + sale_gamma: 44 + service_level: 0.95 + SKU705: + constraint: null + cost: 486 + init_stock: 224 + max_stock: 1000 + price: 937 + sale_gamma: 56 + service_level: 0.95 + SKU706: + constraint: null + cost: 222 + init_stock: 185 + max_stock: 1000 + price: 330 + sale_gamma: 37 + service_level: 0.95 + SKU707: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 195 + init_stock: 185 + max_stock: 1000 + price: 339 + sale_gamma: 37 + service_level: 0.95 + SKU708: + constraint: null + cost: 131 + init_stock: 396 + max_stock: 1000 + price: 155 + sale_gamma: 99 + service_level: 0.95 + SKU709: + constraint: null + cost: 137 + init_stock: 210 + max_stock: 1000 + price: 221 + sale_gamma: 35 + service_level: 0.95 + SKU71: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 381 + init_stock: 148 + max_stock: 1000 + price: 464 + sale_gamma: 37 + service_level: 0.95 + SKU710: + constraint: null + cost: 186 + init_stock: 56 + max_stock: 1000 + price: 228 + sale_gamma: 14 + service_level: 0.95 + SKU711: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 190 + init_stock: 366 + max_stock: 1000 + price: 300 + sale_gamma: 61 + service_level: 0.95 + SKU712: + constraint: null + cost: 384 + init_stock: 252 + max_stock: 1000 + price: 426 + sale_gamma: 36 + service_level: 0.95 + SKU713: + constraint: G(low_profit -> low_stock_constraint) + cost: 229 + init_stock: 276 + max_stock: 1000 + price: 368 + sale_gamma: 69 + service_level: 0.95 + SKU714: + constraint: G(low_profit -> low_stock_constraint) + cost: 364 + init_stock: 360 + max_stock: 1000 + price: 469 + sale_gamma: 90 + service_level: 0.95 + SKU715: + constraint: G(stock_constraint) + cost: 235 + init_stock: 63 + max_stock: 1000 + price: 430 + sale_gamma: 9 + service_level: 0.95 + SKU716: + constraint: null + cost: 108 + init_stock: 623 + max_stock: 1000 + price: 139 + sale_gamma: 89 + service_level: 0.95 + SKU717: + constraint: G(low_profit -> low_stock_constraint) + cost: 12 + init_stock: 48 + max_stock: 1000 + price: 20 + sale_gamma: 12 + service_level: 0.95 + SKU718: + constraint: null + cost: 397 + init_stock: 132 + max_stock: 1000 + price: 460 + sale_gamma: 22 + service_level: 0.95 + SKU719: + constraint: G(stock_constraint) + cost: 21 + init_stock: 148 + max_stock: 1000 + price: 34 + sale_gamma: 37 + service_level: 0.95 + SKU72: + constraint: null + cost: 228 + init_stock: 388 + max_stock: 1000 + price: 319 + sale_gamma: 97 + service_level: 0.95 + SKU720: + constraint: G(stock_constraint) + cost: 120 + init_stock: 288 + max_stock: 1000 + price: 211 + sale_gamma: 48 + service_level: 0.95 + SKU721: + constraint: G(low_profit -> low_stock_constraint) + cost: 265 + init_stock: 332 + max_stock: 1000 + price: 431 + sale_gamma: 83 + service_level: 0.95 + SKU722: + constraint: G(low_profit -> low_stock_constraint) + cost: 353 + init_stock: 232 + max_stock: 1000 + price: 511 + sale_gamma: 58 + service_level: 0.95 + SKU723: + constraint: null + cost: 144 + init_stock: 595 + max_stock: 1000 + price: 239 + sale_gamma: 85 + service_level: 0.95 + SKU724: + constraint: G(stock_constraint) + cost: 120 + init_stock: 142 + max_stock: 1000 + price: 187 + sale_gamma: 71 + service_level: 0.95 + SKU725: + constraint: G(stock_constraint) + cost: 42 + init_stock: 356 + max_stock: 1000 + price: 49 + sale_gamma: 89 + service_level: 0.95 + SKU726: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 132 + init_stock: 696 + max_stock: 1000 + price: 183 + sale_gamma: 87 + service_level: 0.95 + SKU727: + constraint: null + cost: 103 + init_stock: 105 + max_stock: 1000 + price: 145 + sale_gamma: 35 + service_level: 0.95 + SKU728: + constraint: G(low_profit -> low_stock_constraint) + cost: 29 + init_stock: 204 + max_stock: 1000 + price: 56 + sale_gamma: 34 + service_level: 0.95 + SKU729: + constraint: null + cost: 251 + init_stock: 679 + max_stock: 1000 + price: 353 + sale_gamma: 97 + service_level: 0.95 + SKU73: + constraint: G(stock_constraint) + cost: 334 + init_stock: 365 + max_stock: 1000 + price: 634 + sale_gamma: 73 + service_level: 0.95 + SKU730: + constraint: null + cost: 71 + init_stock: 512 + max_stock: 1000 + price: 139 + sale_gamma: 64 + service_level: 0.95 + SKU731: + constraint: null + cost: 480 + init_stock: 276 + max_stock: 1000 + price: 604 + sale_gamma: 92 + service_level: 0.95 + SKU732: + constraint: G(low_profit -> low_stock_constraint) + cost: 231 + init_stock: 402 + max_stock: 1000 + price: 438 + sale_gamma: 67 + service_level: 0.95 + SKU733: + constraint: null + cost: 245 + init_stock: 196 + max_stock: 1000 + price: 450 + sale_gamma: 28 + service_level: 0.95 + SKU734: + constraint: G(low_profit -> low_stock_constraint) + cost: 117 + init_stock: 116 + max_stock: 1000 + price: 187 + sale_gamma: 58 + service_level: 0.95 + SKU735: + constraint: G(low_profit -> low_stock_constraint) + cost: 197 + init_stock: 182 + max_stock: 1000 + price: 279 + sale_gamma: 26 + service_level: 0.95 + SKU736: + constraint: G(stock_constraint) + cost: 221 + init_stock: 496 + max_stock: 1000 + price: 282 + sale_gamma: 62 + service_level: 0.95 + SKU737: + constraint: null + cost: 260 + init_stock: 595 + max_stock: 1000 + price: 488 + sale_gamma: 85 + service_level: 0.95 + SKU738: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 184 + max_stock: 1000 + price: 557 + sale_gamma: 46 + service_level: 0.95 + SKU739: + constraint: null + cost: 451 + init_stock: 266 + max_stock: 1000 + price: 699 + sale_gamma: 38 + service_level: 0.95 + SKU74: + constraint: null + cost: 363 + init_stock: 80 + max_stock: 1000 + price: 511 + sale_gamma: 10 + service_level: 0.95 + SKU740: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 250 + init_stock: 10 + max_stock: 1000 + price: 500 + sale_gamma: 5 + service_level: 0.95 + SKU741: + constraint: null + cost: 139 + init_stock: 68 + max_stock: 1000 + price: 266 + sale_gamma: 17 + service_level: 0.95 + SKU742: + constraint: null + cost: 174 + init_stock: 445 + max_stock: 1000 + price: 194 + sale_gamma: 89 + service_level: 0.95 + SKU743: + constraint: null + cost: 79 + init_stock: 574 + max_stock: 1000 + price: 106 + sale_gamma: 82 + service_level: 0.95 + SKU744: + constraint: null + cost: 304 + init_stock: 312 + max_stock: 1000 + price: 367 + sale_gamma: 39 + service_level: 0.95 + SKU745: + constraint: null + cost: 105 + init_stock: 465 + max_stock: 1000 + price: 191 + sale_gamma: 93 + service_level: 0.95 + SKU746: + constraint: null + cost: 397 + init_stock: 282 + max_stock: 1000 + price: 643 + sale_gamma: 94 + service_level: 0.95 + SKU747: + constraint: null + cost: 426 + init_stock: 72 + max_stock: 1000 + price: 498 + sale_gamma: 36 + service_level: 0.95 + SKU748: + constraint: null + cost: 169 + init_stock: 235 + max_stock: 1000 + price: 305 + sale_gamma: 47 + service_level: 0.95 + SKU749: + constraint: null + cost: 433 + init_stock: 168 + max_stock: 1000 + price: 697 + sale_gamma: 42 + service_level: 0.95 + SKU75: + constraint: G(low_profit -> low_stock_constraint) + cost: 179 + init_stock: 325 + max_stock: 1000 + price: 277 + sale_gamma: 65 + service_level: 0.95 + SKU750: + constraint: null + cost: 373 + init_stock: 408 + max_stock: 1000 + price: 555 + sale_gamma: 68 + service_level: 0.95 + SKU751: + constraint: G(low_profit -> low_stock_constraint) + cost: 196 + init_stock: 238 + max_stock: 1000 + price: 270 + sale_gamma: 34 + service_level: 0.95 + SKU752: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 67 + init_stock: 20 + max_stock: 1000 + price: 123 + sale_gamma: 10 + service_level: 0.95 + SKU753: + constraint: null + cost: 162 + init_stock: 122 + max_stock: 1000 + price: 319 + sale_gamma: 61 + service_level: 0.95 + SKU754: + constraint: null + cost: 206 + init_stock: 256 + max_stock: 1000 + price: 278 + sale_gamma: 64 + service_level: 0.95 + SKU755: + constraint: null + cost: 125 + init_stock: 86 + max_stock: 1000 + price: 223 + sale_gamma: 43 + service_level: 0.95 + SKU756: + constraint: G(stock_constraint) + cost: 215 + init_stock: 246 + max_stock: 1000 + price: 290 + sale_gamma: 82 + service_level: 0.95 + SKU757: + constraint: null + cost: 263 + init_stock: 176 + max_stock: 1000 + price: 341 + sale_gamma: 22 + service_level: 0.95 + SKU758: + constraint: null + cost: 70 + init_stock: 609 + max_stock: 1000 + price: 127 + sale_gamma: 87 + service_level: 0.95 + SKU759: + constraint: null + cost: 310 + init_stock: 395 + max_stock: 1000 + price: 616 + sale_gamma: 79 + service_level: 0.95 + SKU76: + constraint: null + cost: 464 + init_stock: 336 + max_stock: 1000 + price: 709 + sale_gamma: 84 + service_level: 0.95 + SKU760: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 110 + max_stock: 1000 + price: 949 + sale_gamma: 22 + service_level: 0.95 + SKU761: + constraint: G(stock_constraint) + cost: 288 + init_stock: 93 + max_stock: 1000 + price: 414 + sale_gamma: 31 + service_level: 0.95 + SKU762: + constraint: G(stock_constraint) + cost: 374 + init_stock: 310 + max_stock: 1000 + price: 650 + sale_gamma: 62 + service_level: 0.95 + SKU763: + constraint: null + cost: 121 + init_stock: 30 + max_stock: 1000 + price: 174 + sale_gamma: 6 + service_level: 0.95 + SKU764: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 494 + init_stock: 190 + max_stock: 1000 + price: 637 + sale_gamma: 38 + service_level: 0.95 + SKU765: + constraint: G(stock_constraint) + cost: 459 + init_stock: 356 + max_stock: 1000 + price: 523 + sale_gamma: 89 + service_level: 0.95 + SKU766: + constraint: null + cost: 249 + init_stock: 216 + max_stock: 1000 + price: 433 + sale_gamma: 54 + service_level: 0.95 + SKU767: + constraint: G(low_profit -> low_stock_constraint) + cost: 245 + init_stock: 528 + max_stock: 1000 + price: 360 + sale_gamma: 88 + service_level: 0.95 + SKU768: + constraint: null + cost: 429 + init_stock: 425 + max_stock: 1000 + price: 600 + sale_gamma: 85 + service_level: 0.95 + SKU769: + constraint: G(low_profit -> low_stock_constraint) + cost: 324 + init_stock: 192 + max_stock: 1000 + price: 521 + sale_gamma: 48 + service_level: 0.95 + SKU77: + constraint: G(low_profit -> low_stock_constraint) + cost: 281 + init_stock: 168 + max_stock: 1000 + price: 323 + sale_gamma: 28 + service_level: 0.95 + SKU770: + constraint: G(stock_constraint) + cost: 362 + init_stock: 152 + max_stock: 1000 + price: 477 + sale_gamma: 76 + service_level: 0.95 + SKU771: + constraint: G(low_profit -> low_stock_constraint) + cost: 233 + init_stock: 135 + max_stock: 1000 + price: 379 + sale_gamma: 27 + service_level: 0.95 + SKU772: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 152 + init_stock: 64 + max_stock: 1000 + price: 297 + sale_gamma: 16 + service_level: 0.95 + SKU773: + constraint: null + cost: 337 + init_stock: 768 + max_stock: 1000 + price: 539 + sale_gamma: 96 + service_level: 0.95 + SKU774: + constraint: null + cost: 232 + init_stock: 45 + max_stock: 1000 + price: 255 + sale_gamma: 9 + service_level: 0.95 + SKU775: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 393 + init_stock: 96 + max_stock: 1000 + price: 695 + sale_gamma: 12 + service_level: 0.95 + SKU776: + constraint: null + cost: 342 + init_stock: 336 + max_stock: 1000 + price: 680 + sale_gamma: 48 + service_level: 0.95 + SKU777: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 354 + max_stock: 1000 + price: 466 + sale_gamma: 59 + service_level: 0.95 + SKU778: + constraint: G(low_profit -> low_stock_constraint) + cost: 327 + init_stock: 370 + max_stock: 1000 + price: 614 + sale_gamma: 74 + service_level: 0.95 + SKU779: + constraint: G(stock_constraint) + cost: 109 + init_stock: 352 + max_stock: 1000 + price: 156 + sale_gamma: 44 + service_level: 0.95 + SKU78: + constraint: null + cost: 486 + init_stock: 148 + max_stock: 1000 + price: 544 + sale_gamma: 37 + service_level: 0.95 + SKU780: + constraint: null + cost: 153 + init_stock: 50 + max_stock: 1000 + price: 286 + sale_gamma: 25 + service_level: 0.95 + SKU781: + constraint: null + cost: 126 + init_stock: 21 + max_stock: 1000 + price: 197 + sale_gamma: 7 + service_level: 0.95 + SKU782: + constraint: null + cost: 258 + init_stock: 368 + max_stock: 1000 + price: 479 + sale_gamma: 92 + service_level: 0.95 + SKU783: + constraint: null + cost: 214 + init_stock: 140 + max_stock: 1000 + price: 314 + sale_gamma: 70 + service_level: 0.95 + SKU784: + constraint: null + cost: 426 + init_stock: 245 + max_stock: 1000 + price: 677 + sale_gamma: 35 + service_level: 0.95 + SKU785: + constraint: null + cost: 486 + init_stock: 145 + max_stock: 1000 + price: 588 + sale_gamma: 29 + service_level: 0.95 + SKU786: + constraint: null + cost: 109 + init_stock: 104 + max_stock: 1000 + price: 150 + sale_gamma: 52 + service_level: 0.95 + SKU787: + constraint: null + cost: 74 + init_stock: 84 + max_stock: 1000 + price: 91 + sale_gamma: 21 + service_level: 0.95 + SKU788: + constraint: G(stock_constraint) + cost: 25 + init_stock: 252 + max_stock: 1000 + price: 45 + sale_gamma: 42 + service_level: 0.95 + SKU789: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 265 + init_stock: 273 + max_stock: 1000 + price: 344 + sale_gamma: 39 + service_level: 0.95 + SKU79: + constraint: null + cost: 486 + init_stock: 210 + max_stock: 1000 + price: 636 + sale_gamma: 42 + service_level: 0.95 + SKU790: + constraint: null + cost: 222 + init_stock: 112 + max_stock: 1000 + price: 270 + sale_gamma: 28 + service_level: 0.95 + SKU791: + constraint: null + cost: 495 + init_stock: 270 + max_stock: 1000 + price: 792 + sale_gamma: 54 + service_level: 0.95 + SKU792: + constraint: null + cost: 410 + init_stock: 128 + max_stock: 1000 + price: 451 + sale_gamma: 32 + service_level: 0.95 + SKU793: + constraint: null + cost: 442 + init_stock: 480 + max_stock: 1000 + price: 746 + sale_gamma: 80 + service_level: 0.95 + SKU794: + constraint: G(stock_constraint) + cost: 222 + init_stock: 48 + max_stock: 1000 + price: 330 + sale_gamma: 6 + service_level: 0.95 + SKU795: + constraint: G(low_profit -> low_stock_constraint) + cost: 279 + init_stock: 232 + max_stock: 1000 + price: 368 + sale_gamma: 58 + service_level: 0.95 + SKU796: + constraint: null + cost: 122 + init_stock: 130 + max_stock: 1000 + price: 218 + sale_gamma: 26 + service_level: 0.95 + SKU797: + constraint: null + cost: 107 + init_stock: 300 + max_stock: 1000 + price: 195 + sale_gamma: 100 + service_level: 0.95 + SKU798: + constraint: G(low_profit -> low_stock_constraint) + cost: 383 + init_stock: 250 + max_stock: 1000 + price: 467 + sale_gamma: 50 + service_level: 0.95 + SKU799: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 119 + init_stock: 280 + max_stock: 1000 + price: 215 + sale_gamma: 35 + service_level: 0.95 + SKU8: + constraint: null + cost: 423 + init_stock: 510 + max_stock: 1000 + price: 791 + sale_gamma: 85 + service_level: 0.95 + SKU80: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 324 + init_stock: 180 + max_stock: 1000 + price: 417 + sale_gamma: 36 + service_level: 0.95 + SKU800: + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 356 + max_stock: 1000 + price: 353 + sale_gamma: 89 + service_level: 0.95 + SKU801: + constraint: G(stock_constraint) + cost: 65 + init_stock: 672 + max_stock: 1000 + price: 91 + sale_gamma: 84 + service_level: 0.95 + SKU802: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 133 + init_stock: 376 + max_stock: 1000 + price: 155 + sale_gamma: 94 + service_level: 0.95 + SKU803: + constraint: G(stock_constraint) + cost: 209 + init_stock: 175 + max_stock: 1000 + price: 234 + sale_gamma: 35 + service_level: 0.95 + SKU804: + constraint: null + cost: 273 + init_stock: 425 + max_stock: 1000 + price: 526 + sale_gamma: 85 + service_level: 0.95 + SKU805: + constraint: G(low_profit -> low_stock_constraint) + cost: 304 + init_stock: 152 + max_stock: 1000 + price: 349 + sale_gamma: 38 + service_level: 0.95 + SKU806: + constraint: G(low_profit -> low_stock_constraint) + cost: 232 + init_stock: 54 + max_stock: 1000 + price: 269 + sale_gamma: 9 + service_level: 0.95 + SKU807: + constraint: G(low_profit -> low_stock_constraint) + cost: 169 + init_stock: 696 + max_stock: 1000 + price: 216 + sale_gamma: 87 + service_level: 0.95 + SKU808: + constraint: G(stock_constraint) + cost: 350 + init_stock: 45 + max_stock: 1000 + price: 612 + sale_gamma: 9 + service_level: 0.95 + SKU809: + constraint: null + cost: 85 + init_stock: 388 + max_stock: 1000 + price: 161 + sale_gamma: 97 + service_level: 0.95 + SKU81: + constraint: G(low_profit -> low_stock_constraint) + cost: 319 + init_stock: 240 + max_stock: 1000 + price: 504 + sale_gamma: 60 + service_level: 0.95 + SKU810: + constraint: null + cost: 51 + init_stock: 255 + max_stock: 1000 + price: 68 + sale_gamma: 51 + service_level: 0.95 + SKU811: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 53 + init_stock: 324 + max_stock: 1000 + price: 62 + sale_gamma: 54 + service_level: 0.95 + SKU812: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 141 + init_stock: 228 + max_stock: 1000 + price: 266 + sale_gamma: 57 + service_level: 0.95 + SKU813: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 364 + init_stock: 156 + max_stock: 1000 + price: 425 + sale_gamma: 39 + service_level: 0.95 + SKU814: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 79 + init_stock: 768 + max_stock: 1000 + price: 118 + sale_gamma: 96 + service_level: 0.95 + SKU815: + constraint: G(low_profit -> low_stock_constraint) + cost: 178 + init_stock: 64 + max_stock: 1000 + price: 329 + sale_gamma: 16 + service_level: 0.95 + SKU816: + constraint: null + cost: 352 + init_stock: 330 + max_stock: 1000 + price: 587 + sale_gamma: 66 + service_level: 0.95 + SKU817: + constraint: G(low_profit -> low_stock_constraint) + cost: 119 + init_stock: 34 + max_stock: 1000 + price: 209 + sale_gamma: 17 + service_level: 0.95 + SKU818: + constraint: G(low_profit -> low_stock_constraint) + cost: 110 + init_stock: 65 + max_stock: 1000 + price: 189 + sale_gamma: 13 + service_level: 0.95 + SKU819: + constraint: null + cost: 24 + init_stock: 40 + max_stock: 1000 + price: 41 + sale_gamma: 5 + service_level: 0.95 + SKU82: + constraint: null + cost: 413 + init_stock: 560 + max_stock: 1000 + price: 776 + sale_gamma: 70 + service_level: 0.95 + SKU820: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 426 + init_stock: 260 + max_stock: 1000 + price: 736 + sale_gamma: 52 + service_level: 0.95 + SKU821: + constraint: null + cost: 229 + init_stock: 184 + max_stock: 1000 + price: 311 + sale_gamma: 92 + service_level: 0.95 + SKU822: + constraint: G(low_profit -> low_stock_constraint) + cost: 282 + init_stock: 24 + max_stock: 1000 + price: 470 + sale_gamma: 12 + service_level: 0.95 + SKU823: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 221 + init_stock: 364 + max_stock: 1000 + price: 419 + sale_gamma: 91 + service_level: 0.95 + SKU824: + constraint: null + cost: 318 + init_stock: 288 + max_stock: 1000 + price: 616 + sale_gamma: 36 + service_level: 0.95 + SKU825: + constraint: G(low_profit -> low_stock_constraint) + cost: 287 + init_stock: 558 + max_stock: 1000 + price: 375 + sale_gamma: 93 + service_level: 0.95 + SKU826: + constraint: G(low_profit -> low_stock_constraint) + cost: 278 + init_stock: 240 + max_stock: 1000 + price: 380 + sale_gamma: 48 + service_level: 0.95 + SKU827: + constraint: G(stock_constraint) + cost: 312 + init_stock: 378 + max_stock: 1000 + price: 514 + sale_gamma: 63 + service_level: 0.95 + SKU828: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 140 + init_stock: 558 + max_stock: 1000 + price: 261 + sale_gamma: 93 + service_level: 0.95 + SKU829: + constraint: null + cost: 415 + init_stock: 88 + max_stock: 1000 + price: 655 + sale_gamma: 11 + service_level: 0.95 + SKU83: + constraint: null + cost: 327 + init_stock: 462 + max_stock: 1000 + price: 474 + sale_gamma: 77 + service_level: 0.95 + SKU830: + constraint: null + cost: 119 + init_stock: 138 + max_stock: 1000 + price: 205 + sale_gamma: 23 + service_level: 0.95 + SKU831: + constraint: null + cost: 376 + init_stock: 420 + max_stock: 1000 + price: 624 + sale_gamma: 70 + service_level: 0.95 + SKU832: + constraint: null + cost: 289 + init_stock: 322 + max_stock: 1000 + price: 488 + sale_gamma: 46 + service_level: 0.95 + SKU833: + constraint: null + cost: 183 + init_stock: 64 + max_stock: 1000 + price: 325 + sale_gamma: 16 + service_level: 0.95 + SKU834: + constraint: G(stock_constraint) + cost: 398 + init_stock: 264 + max_stock: 1000 + price: 565 + sale_gamma: 66 + service_level: 0.95 + SKU835: + constraint: G(stock_constraint) + cost: 407 + init_stock: 370 + max_stock: 1000 + price: 582 + sale_gamma: 74 + service_level: 0.95 + SKU836: + constraint: G(low_profit -> low_stock_constraint) + cost: 301 + init_stock: 123 + max_stock: 1000 + price: 454 + sale_gamma: 41 + service_level: 0.95 + SKU837: + constraint: G(low_profit -> low_stock_constraint) + cost: 213 + init_stock: 130 + max_stock: 1000 + price: 313 + sale_gamma: 26 + service_level: 0.95 + SKU838: + constraint: null + cost: 23 + init_stock: 252 + max_stock: 1000 + price: 25 + sale_gamma: 42 + service_level: 0.95 + SKU839: + constraint: null + cost: 354 + init_stock: 106 + max_stock: 1000 + price: 679 + sale_gamma: 53 + service_level: 0.95 + SKU84: + constraint: null + cost: 217 + init_stock: 192 + max_stock: 1000 + price: 327 + sale_gamma: 32 + service_level: 0.95 + SKU840: + constraint: G(low_profit -> low_stock_constraint) + cost: 19 + init_stock: 348 + max_stock: 1000 + price: 23 + sale_gamma: 87 + service_level: 0.95 + SKU841: + constraint: G(low_profit -> low_stock_constraint) + cost: 362 + init_stock: 470 + max_stock: 1000 + price: 629 + sale_gamma: 94 + service_level: 0.95 + SKU842: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 73 + init_stock: 273 + max_stock: 1000 + price: 100 + sale_gamma: 91 + service_level: 0.95 + SKU843: + constraint: G(low_profit -> low_stock_constraint) + cost: 169 + init_stock: 215 + max_stock: 1000 + price: 212 + sale_gamma: 43 + service_level: 0.95 + SKU844: + constraint: G(low_profit -> low_stock_constraint) + cost: 325 + init_stock: 144 + max_stock: 1000 + price: 585 + sale_gamma: 36 + service_level: 0.95 + SKU845: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 163 + init_stock: 120 + max_stock: 1000 + price: 237 + sale_gamma: 15 + service_level: 0.95 + SKU846: + constraint: null + cost: 184 + init_stock: 686 + max_stock: 1000 + price: 298 + sale_gamma: 98 + service_level: 0.95 + SKU847: + constraint: null + cost: 441 + init_stock: 136 + max_stock: 1000 + price: 811 + sale_gamma: 34 + service_level: 0.95 + SKU848: + constraint: null + cost: 450 + init_stock: 200 + max_stock: 1000 + price: 751 + sale_gamma: 25 + service_level: 0.95 + SKU849: + constraint: null + cost: 479 + init_stock: 245 + max_stock: 1000 + price: 526 + sale_gamma: 35 + service_level: 0.95 + SKU85: + constraint: null + cost: 49 + init_stock: 152 + max_stock: 1000 + price: 73 + sale_gamma: 76 + service_level: 0.95 + SKU850: + constraint: null + cost: 105 + init_stock: 292 + max_stock: 1000 + price: 180 + sale_gamma: 73 + service_level: 0.95 + SKU851: + constraint: G(stock_constraint) + cost: 366 + init_stock: 539 + max_stock: 1000 + price: 721 + sale_gamma: 77 + service_level: 0.95 + SKU852: + constraint: G(low_profit -> low_stock_constraint) + cost: 392 + init_stock: 312 + max_stock: 1000 + price: 439 + sale_gamma: 78 + service_level: 0.95 + SKU853: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 60 + max_stock: 1000 + price: 181 + sale_gamma: 10 + service_level: 0.95 + SKU854: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 110 + init_stock: 325 + max_stock: 1000 + price: 139 + sale_gamma: 65 + service_level: 0.95 + SKU855: + constraint: null + cost: 254 + init_stock: 287 + max_stock: 1000 + price: 505 + sale_gamma: 41 + service_level: 0.95 + SKU856: + constraint: null + cost: 435 + init_stock: 145 + max_stock: 1000 + price: 600 + sale_gamma: 29 + service_level: 0.95 + SKU857: + constraint: G(low_profit -> low_stock_constraint) + cost: 101 + init_stock: 192 + max_stock: 1000 + price: 163 + sale_gamma: 24 + service_level: 0.95 + SKU858: + constraint: null + cost: 487 + init_stock: 168 + max_stock: 1000 + price: 891 + sale_gamma: 28 + service_level: 0.95 + SKU859: + constraint: G(stock_constraint) + cost: 496 + init_stock: 558 + max_stock: 1000 + price: 585 + sale_gamma: 93 + service_level: 0.95 + SKU86: + constraint: null + cost: 59 + init_stock: 225 + max_stock: 1000 + price: 89 + sale_gamma: 75 + service_level: 0.95 + SKU860: + constraint: null + cost: 350 + init_stock: 600 + max_stock: 1000 + price: 465 + sale_gamma: 100 + service_level: 0.95 + SKU861: + constraint: null + cost: 295 + init_stock: 175 + max_stock: 1000 + price: 345 + sale_gamma: 25 + service_level: 0.95 + SKU862: + constraint: G(low_profit -> low_stock_constraint) + cost: 57 + init_stock: 219 + max_stock: 1000 + price: 74 + sale_gamma: 73 + service_level: 0.95 + SKU863: + constraint: null + cost: 311 + init_stock: 456 + max_stock: 1000 + price: 351 + sale_gamma: 57 + service_level: 0.95 + SKU864: + constraint: null + cost: 96 + init_stock: 192 + max_stock: 1000 + price: 144 + sale_gamma: 64 + service_level: 0.95 + SKU865: + constraint: null + cost: 107 + init_stock: 348 + max_stock: 1000 + price: 166 + sale_gamma: 58 + service_level: 0.95 + SKU866: + constraint: null + cost: 397 + init_stock: 147 + max_stock: 1000 + price: 766 + sale_gamma: 21 + service_level: 0.95 + SKU867: + constraint: null + cost: 478 + init_stock: 512 + max_stock: 1000 + price: 702 + sale_gamma: 64 + service_level: 0.95 + SKU868: + constraint: null + cost: 343 + init_stock: 63 + max_stock: 1000 + price: 415 + sale_gamma: 21 + service_level: 0.95 + SKU869: + constraint: null + cost: 168 + init_stock: 305 + max_stock: 1000 + price: 213 + sale_gamma: 61 + service_level: 0.95 + SKU87: + constraint: G(low_profit -> low_stock_constraint) + cost: 93 + init_stock: 213 + max_stock: 1000 + price: 159 + sale_gamma: 71 + service_level: 0.95 + SKU870: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 257 + init_stock: 441 + max_stock: 1000 + price: 431 + sale_gamma: 63 + service_level: 0.95 + SKU871: + constraint: G(stock_constraint) + cost: 68 + init_stock: 120 + max_stock: 1000 + price: 125 + sale_gamma: 20 + service_level: 0.95 + SKU872: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 475 + init_stock: 284 + max_stock: 1000 + price: 836 + sale_gamma: 71 + service_level: 0.95 + SKU873: + constraint: null + cost: 388 + init_stock: 260 + max_stock: 1000 + price: 671 + sale_gamma: 65 + service_level: 0.95 + SKU874: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 267 + init_stock: 30 + max_stock: 1000 + price: 448 + sale_gamma: 6 + service_level: 0.95 + SKU875: + constraint: null + cost: 332 + init_stock: 140 + max_stock: 1000 + price: 488 + sale_gamma: 35 + service_level: 0.95 + SKU876: + constraint: null + cost: 392 + init_stock: 200 + max_stock: 1000 + price: 490 + sale_gamma: 40 + service_level: 0.95 + SKU877: + constraint: G(low_profit -> low_stock_constraint) + cost: 374 + init_stock: 455 + max_stock: 1000 + price: 680 + sale_gamma: 91 + service_level: 0.95 + SKU878: + constraint: G(stock_constraint) + cost: 297 + init_stock: 145 + max_stock: 1000 + price: 415 + sale_gamma: 29 + service_level: 0.95 + SKU879: + constraint: G(low_profit -> low_stock_constraint) + cost: 497 + init_stock: 312 + max_stock: 1000 + price: 939 + sale_gamma: 78 + service_level: 0.95 + SKU88: + constraint: null + cost: 19 + init_stock: 126 + max_stock: 1000 + price: 22 + sale_gamma: 42 + service_level: 0.95 + SKU880: + constraint: null + cost: 199 + init_stock: 141 + max_stock: 1000 + price: 390 + sale_gamma: 47 + service_level: 0.95 + SKU881: + constraint: null + cost: 328 + init_stock: 240 + max_stock: 1000 + price: 485 + sale_gamma: 60 + service_level: 0.95 + SKU882: + constraint: null + cost: 307 + init_stock: 360 + max_stock: 1000 + price: 340 + sale_gamma: 90 + service_level: 0.95 + SKU883: + constraint: null + cost: 16 + init_stock: 196 + max_stock: 1000 + price: 23 + sale_gamma: 28 + service_level: 0.95 + SKU884: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 329 + init_stock: 335 + max_stock: 1000 + price: 585 + sale_gamma: 67 + service_level: 0.95 + SKU885: + constraint: null + cost: 176 + init_stock: 355 + max_stock: 1000 + price: 262 + sale_gamma: 71 + service_level: 0.95 + SKU886: + constraint: null + cost: 48 + init_stock: 623 + max_stock: 1000 + price: 94 + sale_gamma: 89 + service_level: 0.95 + SKU887: + constraint: null + cost: 241 + init_stock: 90 + max_stock: 1000 + price: 477 + sale_gamma: 15 + service_level: 0.95 + SKU888: + constraint: null + cost: 114 + init_stock: 424 + max_stock: 1000 + price: 169 + sale_gamma: 53 + service_level: 0.95 + SKU889: + constraint: G(low_profit -> low_stock_constraint) + cost: 261 + init_stock: 380 + max_stock: 1000 + price: 441 + sale_gamma: 95 + service_level: 0.95 + SKU89: + constraint: G(stock_constraint) + cost: 134 + init_stock: 567 + max_stock: 1000 + price: 230 + sale_gamma: 81 + service_level: 0.95 + SKU890: + constraint: null + cost: 322 + init_stock: 497 + max_stock: 1000 + price: 367 + sale_gamma: 71 + service_level: 0.95 + SKU891: + constraint: G(stock_constraint) + cost: 347 + init_stock: 114 + max_stock: 1000 + price: 489 + sale_gamma: 57 + service_level: 0.95 + SKU892: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 474 + init_stock: 336 + max_stock: 1000 + price: 938 + sale_gamma: 84 + service_level: 0.95 + SKU893: + constraint: null + cost: 433 + init_stock: 196 + max_stock: 1000 + price: 701 + sale_gamma: 98 + service_level: 0.95 + SKU894: + constraint: null + cost: 399 + init_stock: 104 + max_stock: 1000 + price: 746 + sale_gamma: 26 + service_level: 0.95 + SKU895: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 335 + init_stock: 240 + max_stock: 1000 + price: 629 + sale_gamma: 48 + service_level: 0.95 + SKU896: + constraint: G(stock_constraint) + cost: 92 + init_stock: 385 + max_stock: 1000 + price: 111 + sale_gamma: 55 + service_level: 0.95 + SKU897: + constraint: null + cost: 404 + init_stock: 88 + max_stock: 1000 + price: 779 + sale_gamma: 22 + service_level: 0.95 + SKU898: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 72 + init_stock: 490 + max_stock: 1000 + price: 90 + sale_gamma: 98 + service_level: 0.95 + SKU899: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 91 + init_stock: 623 + max_stock: 1000 + price: 139 + sale_gamma: 89 + service_level: 0.95 + SKU9: + constraint: null + cost: 231 + init_stock: 84 + max_stock: 1000 + price: 462 + sale_gamma: 21 + service_level: 0.95 + SKU90: + constraint: G(low_profit -> low_stock_constraint) + cost: 38 + init_stock: 60 + max_stock: 1000 + price: 50 + sale_gamma: 30 + service_level: 0.95 + SKU900: + constraint: null + cost: 12 + init_stock: 354 + max_stock: 1000 + price: 21 + sale_gamma: 59 + service_level: 0.95 + SKU901: + constraint: G(low_profit -> low_stock_constraint) + cost: 307 + init_stock: 312 + max_stock: 1000 + price: 469 + sale_gamma: 78 + service_level: 0.95 + SKU902: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 354 + init_stock: 112 + max_stock: 1000 + price: 407 + sale_gamma: 56 + service_level: 0.95 + SKU903: + constraint: null + cost: 14 + init_stock: 372 + max_stock: 1000 + price: 19 + sale_gamma: 93 + service_level: 0.95 + SKU904: + constraint: G(stock_constraint) + cost: 300 + init_stock: 194 + max_stock: 1000 + price: 501 + sale_gamma: 97 + service_level: 0.95 + SKU905: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 134 + init_stock: 306 + max_stock: 1000 + price: 221 + sale_gamma: 51 + service_level: 0.95 + SKU906: + constraint: G(stock_constraint) + cost: 424 + init_stock: 528 + max_stock: 1000 + price: 784 + sale_gamma: 88 + service_level: 0.95 + SKU907: + constraint: null + cost: 360 + init_stock: 224 + max_stock: 1000 + price: 486 + sale_gamma: 32 + service_level: 0.95 + SKU908: + constraint: null + cost: 361 + init_stock: 156 + max_stock: 1000 + price: 501 + sale_gamma: 39 + service_level: 0.95 + SKU909: + constraint: null + cost: 65 + init_stock: 483 + max_stock: 1000 + price: 124 + sale_gamma: 69 + service_level: 0.95 + SKU91: + constraint: G(low_profit -> low_stock_constraint) + cost: 223 + init_stock: 172 + max_stock: 1000 + price: 269 + sale_gamma: 43 + service_level: 0.95 + SKU910: + constraint: null + cost: 161 + init_stock: 60 + max_stock: 1000 + price: 189 + sale_gamma: 10 + service_level: 0.95 + SKU911: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 186 + init_stock: 250 + max_stock: 1000 + price: 295 + sale_gamma: 50 + service_level: 0.95 + SKU912: + constraint: null + cost: 235 + init_stock: 216 + max_stock: 1000 + price: 408 + sale_gamma: 54 + service_level: 0.95 + SKU913: + constraint: G(stock_constraint) + cost: 122 + init_stock: 476 + max_stock: 1000 + price: 224 + sale_gamma: 68 + service_level: 0.95 + SKU914: + constraint: null + cost: 436 + init_stock: 48 + max_stock: 1000 + price: 597 + sale_gamma: 16 + service_level: 0.95 + SKU915: + constraint: null + cost: 233 + init_stock: 115 + max_stock: 1000 + price: 424 + sale_gamma: 23 + service_level: 0.95 + SKU916: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 26 + init_stock: 245 + max_stock: 1000 + price: 42 + sale_gamma: 49 + service_level: 0.95 + SKU917: + constraint: null + cost: 141 + init_stock: 490 + max_stock: 1000 + price: 250 + sale_gamma: 98 + service_level: 0.95 + SKU918: + constraint: null + cost: 113 + init_stock: 623 + max_stock: 1000 + price: 192 + sale_gamma: 89 + service_level: 0.95 + SKU919: + constraint: null + cost: 68 + init_stock: 696 + max_stock: 1000 + price: 111 + sale_gamma: 87 + service_level: 0.95 + SKU92: + constraint: G(stock_constraint) + cost: 325 + init_stock: 152 + max_stock: 1000 + price: 640 + sale_gamma: 38 + service_level: 0.95 + SKU920: + constraint: null + cost: 428 + init_stock: 42 + max_stock: 1000 + price: 637 + sale_gamma: 21 + service_level: 0.95 + SKU921: + constraint: G(stock_constraint) + cost: 491 + init_stock: 456 + max_stock: 1000 + price: 893 + sale_gamma: 76 + service_level: 0.95 + SKU922: + constraint: null + cost: 10 + init_stock: 204 + max_stock: 1000 + price: 18 + sale_gamma: 51 + service_level: 0.95 + SKU923: + constraint: null + cost: 80 + init_stock: 20 + max_stock: 1000 + price: 88 + sale_gamma: 10 + service_level: 0.95 + SKU924: + constraint: null + cost: 164 + init_stock: 456 + max_stock: 1000 + price: 203 + sale_gamma: 76 + service_level: 0.95 + SKU925: + constraint: null + cost: 221 + init_stock: 366 + max_stock: 1000 + price: 313 + sale_gamma: 61 + service_level: 0.95 + SKU926: + constraint: G(stock_constraint) + cost: 300 + init_stock: 14 + max_stock: 1000 + price: 390 + sale_gamma: 7 + service_level: 0.95 + SKU927: + constraint: null + cost: 128 + init_stock: 189 + max_stock: 1000 + price: 168 + sale_gamma: 63 + service_level: 0.95 + SKU928: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 126 + init_stock: 24 + max_stock: 1000 + price: 209 + sale_gamma: 8 + service_level: 0.95 + SKU929: + constraint: null + cost: 377 + init_stock: 30 + max_stock: 1000 + price: 591 + sale_gamma: 15 + service_level: 0.95 + SKU93: + constraint: null + cost: 441 + init_stock: 63 + max_stock: 1000 + price: 789 + sale_gamma: 9 + service_level: 0.95 + SKU930: + constraint: G(low_profit -> low_stock_constraint) + cost: 270 + init_stock: 396 + max_stock: 1000 + price: 437 + sale_gamma: 99 + service_level: 0.95 + SKU931: + constraint: null + cost: 412 + init_stock: 520 + max_stock: 1000 + price: 482 + sale_gamma: 65 + service_level: 0.95 + SKU932: + constraint: null + cost: 118 + init_stock: 384 + max_stock: 1000 + price: 215 + sale_gamma: 48 + service_level: 0.95 + SKU933: + constraint: G(stock_constraint) + cost: 54 + init_stock: 364 + max_stock: 1000 + price: 85 + sale_gamma: 91 + service_level: 0.95 + SKU934: + constraint: G(low_profit -> low_stock_constraint) + cost: 347 + init_stock: 234 + max_stock: 1000 + price: 385 + sale_gamma: 39 + service_level: 0.95 + SKU935: + constraint: null + cost: 219 + init_stock: 85 + max_stock: 1000 + price: 348 + sale_gamma: 17 + service_level: 0.95 + SKU936: + constraint: null + cost: 283 + init_stock: 385 + max_stock: 1000 + price: 387 + sale_gamma: 77 + service_level: 0.95 + SKU937: + constraint: G(stock_constraint) + cost: 457 + init_stock: 292 + max_stock: 1000 + price: 589 + sale_gamma: 73 + service_level: 0.95 + SKU938: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 450 + init_stock: 360 + max_stock: 1000 + price: 661 + sale_gamma: 90 + service_level: 0.95 + SKU939: + constraint: G(stock_constraint) + cost: 67 + init_stock: 54 + max_stock: 1000 + price: 116 + sale_gamma: 27 + service_level: 0.95 + SKU94: + constraint: G(stock_constraint) + cost: 279 + init_stock: 34 + max_stock: 1000 + price: 491 + sale_gamma: 17 + service_level: 0.95 + SKU940: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 65 + init_stock: 145 + max_stock: 1000 + price: 110 + sale_gamma: 29 + service_level: 0.95 + SKU941: + constraint: null + cost: 213 + init_stock: 25 + max_stock: 1000 + price: 368 + sale_gamma: 5 + service_level: 0.95 + SKU942: + constraint: null + cost: 151 + init_stock: 135 + max_stock: 1000 + price: 212 + sale_gamma: 27 + service_level: 0.95 + SKU943: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 85 + init_stock: 250 + max_stock: 1000 + price: 161 + sale_gamma: 50 + service_level: 0.95 + SKU944: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 154 + init_stock: 42 + max_stock: 1000 + price: 178 + sale_gamma: 6 + service_level: 0.95 + SKU945: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 267 + init_stock: 60 + max_stock: 1000 + price: 472 + sale_gamma: 15 + service_level: 0.95 + SKU946: + constraint: null + cost: 397 + init_stock: 72 + max_stock: 1000 + price: 504 + sale_gamma: 9 + service_level: 0.95 + SKU947: + constraint: null + cost: 275 + init_stock: 205 + max_stock: 1000 + price: 330 + sale_gamma: 41 + service_level: 0.95 + SKU948: + constraint: null + cost: 472 + init_stock: 616 + max_stock: 1000 + price: 892 + sale_gamma: 88 + service_level: 0.95 + SKU949: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 275 + init_stock: 294 + max_stock: 1000 + price: 335 + sale_gamma: 98 + service_level: 0.95 + SKU95: + constraint: null + cost: 183 + init_stock: 102 + max_stock: 1000 + price: 243 + sale_gamma: 51 + service_level: 0.95 + SKU950: + constraint: null + cost: 374 + init_stock: 125 + max_stock: 1000 + price: 710 + sale_gamma: 25 + service_level: 0.95 + SKU951: + constraint: null + cost: 272 + init_stock: 256 + max_stock: 1000 + price: 386 + sale_gamma: 32 + service_level: 0.95 + SKU952: + constraint: G(low_profit -> low_stock_constraint) + cost: 194 + init_stock: 315 + max_stock: 1000 + price: 219 + sale_gamma: 63 + service_level: 0.95 + SKU953: + constraint: null + cost: 428 + init_stock: 60 + max_stock: 1000 + price: 577 + sale_gamma: 10 + service_level: 0.95 + SKU954: + constraint: null + cost: 272 + init_stock: 48 + max_stock: 1000 + price: 489 + sale_gamma: 16 + service_level: 0.95 + SKU955: + constraint: G(stock_constraint) + cost: 84 + init_stock: 552 + max_stock: 1000 + price: 141 + sale_gamma: 92 + service_level: 0.95 + SKU956: + constraint: G(stock_constraint) + cost: 363 + init_stock: 175 + max_stock: 1000 + price: 671 + sale_gamma: 35 + service_level: 0.95 + SKU957: + constraint: null + cost: 221 + init_stock: 360 + max_stock: 1000 + price: 375 + sale_gamma: 90 + service_level: 0.95 + SKU958: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 335 + init_stock: 180 + max_stock: 1000 + price: 629 + sale_gamma: 36 + service_level: 0.95 + SKU959: + constraint: null + cost: 427 + init_stock: 497 + max_stock: 1000 + price: 781 + sale_gamma: 71 + service_level: 0.95 + SKU96: + constraint: null + cost: 320 + init_stock: 66 + max_stock: 1000 + price: 422 + sale_gamma: 11 + service_level: 0.95 + SKU960: + constraint: null + cost: 187 + init_stock: 462 + max_stock: 1000 + price: 216 + sale_gamma: 77 + service_level: 0.95 + SKU961: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 90 + init_stock: 198 + max_stock: 1000 + price: 150 + sale_gamma: 66 + service_level: 0.95 + SKU962: + constraint: null + cost: 70 + init_stock: 490 + max_stock: 1000 + price: 89 + sale_gamma: 98 + service_level: 0.95 + SKU963: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 184 + init_stock: 222 + max_stock: 1000 + price: 224 + sale_gamma: 37 + service_level: 0.95 + SKU964: + constraint: null + cost: 180 + init_stock: 40 + max_stock: 1000 + price: 261 + sale_gamma: 10 + service_level: 0.95 + SKU965: + constraint: G(low_profit -> low_stock_constraint) + cost: 441 + init_stock: 96 + max_stock: 1000 + price: 529 + sale_gamma: 16 + service_level: 0.95 + SKU966: + constraint: null + cost: 214 + init_stock: 160 + max_stock: 1000 + price: 400 + sale_gamma: 32 + service_level: 0.95 + SKU967: + constraint: null + cost: 71 + init_stock: 434 + max_stock: 1000 + price: 117 + sale_gamma: 62 + service_level: 0.95 + SKU968: + constraint: null + cost: 142 + init_stock: 10 + max_stock: 1000 + price: 238 + sale_gamma: 5 + service_level: 0.95 + SKU969: + constraint: null + cost: 435 + init_stock: 174 + max_stock: 1000 + price: 769 + sale_gamma: 58 + service_level: 0.95 + SKU97: + constraint: null + cost: 15 + init_stock: 189 + max_stock: 1000 + price: 23 + sale_gamma: 27 + service_level: 0.95 + SKU970: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 392 + init_stock: 460 + max_stock: 1000 + price: 658 + sale_gamma: 92 + service_level: 0.95 + SKU971: + constraint: null + cost: 408 + init_stock: 39 + max_stock: 1000 + price: 554 + sale_gamma: 13 + service_level: 0.95 + SKU972: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 22 + init_stock: 140 + max_stock: 1000 + price: 33 + sale_gamma: 20 + service_level: 0.95 + SKU973: + constraint: G(low_profit -> low_stock_constraint) + cost: 476 + init_stock: 445 + max_stock: 1000 + price: 923 + sale_gamma: 89 + service_level: 0.95 + SKU974: + constraint: null + cost: 356 + init_stock: 300 + max_stock: 1000 + price: 637 + sale_gamma: 50 + service_level: 0.95 + SKU975: + constraint: null + cost: 299 + init_stock: 136 + max_stock: 1000 + price: 598 + sale_gamma: 17 + service_level: 0.95 + SKU976: + constraint: null + cost: 369 + init_stock: 175 + max_stock: 1000 + price: 490 + sale_gamma: 35 + service_level: 0.95 + SKU977: + constraint: G(low_profit -> low_stock_constraint) + cost: 142 + init_stock: 159 + max_stock: 1000 + price: 213 + sale_gamma: 53 + service_level: 0.95 + SKU978: + constraint: null + cost: 242 + init_stock: 196 + max_stock: 1000 + price: 481 + sale_gamma: 98 + service_level: 0.95 + SKU979: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 219 + init_stock: 198 + max_stock: 1000 + price: 400 + sale_gamma: 99 + service_level: 0.95 + SKU98: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 167 + init_stock: 234 + max_stock: 1000 + price: 317 + sale_gamma: 78 + service_level: 0.95 + SKU980: + constraint: null + cost: 363 + init_stock: 410 + max_stock: 1000 + price: 700 + sale_gamma: 82 + service_level: 0.95 + SKU981: + constraint: null + cost: 416 + init_stock: 504 + max_stock: 1000 + price: 470 + sale_gamma: 84 + service_level: 0.95 + SKU982: + constraint: null + cost: 163 + init_stock: 92 + max_stock: 1000 + price: 208 + sale_gamma: 23 + service_level: 0.95 + SKU983: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 151 + init_stock: 300 + max_stock: 1000 + price: 211 + sale_gamma: 60 + service_level: 0.95 + SKU984: + constraint: G(stock_constraint) + cost: 123 + init_stock: 420 + max_stock: 1000 + price: 152 + sale_gamma: 84 + service_level: 0.95 + SKU985: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 114 + init_stock: 246 + max_stock: 1000 + price: 175 + sale_gamma: 41 + service_level: 0.95 + SKU986: + constraint: null + cost: 107 + init_stock: 608 + max_stock: 1000 + price: 142 + sale_gamma: 76 + service_level: 0.95 + SKU987: + constraint: null + cost: 56 + init_stock: 558 + max_stock: 1000 + price: 61 + sale_gamma: 93 + service_level: 0.95 + SKU988: + constraint: null + cost: 66 + init_stock: 234 + max_stock: 1000 + price: 118 + sale_gamma: 39 + service_level: 0.95 + SKU989: + constraint: G(low_profit -> low_stock_constraint) + cost: 472 + init_stock: 469 + max_stock: 1000 + price: 759 + sale_gamma: 67 + service_level: 0.95 + SKU99: + constraint: null + cost: 31 + init_stock: 267 + max_stock: 1000 + price: 41 + sale_gamma: 89 + service_level: 0.95 + SKU990: + constraint: G(low_profit -> low_stock_constraint) + cost: 14 + init_stock: 188 + max_stock: 1000 + price: 23 + sale_gamma: 47 + service_level: 0.95 + SKU991: + constraint: null + cost: 450 + init_stock: 294 + max_stock: 1000 + price: 864 + sale_gamma: 49 + service_level: 0.95 + SKU992: + constraint: G(stock_constraint) + cost: 18 + init_stock: 366 + max_stock: 1000 + price: 21 + sale_gamma: 61 + service_level: 0.95 + SKU993: + constraint: null + cost: 350 + init_stock: 282 + max_stock: 1000 + price: 475 + sale_gamma: 47 + service_level: 0.95 + SKU994: + constraint: null + cost: 45 + init_stock: 637 + max_stock: 1000 + price: 73 + sale_gamma: 91 + service_level: 0.95 + SKU995: + constraint: null + cost: 406 + init_stock: 344 + max_stock: 1000 + price: 690 + sale_gamma: 86 + service_level: 0.95 + SKU996: + constraint: G(stock_constraint) + cost: 472 + init_stock: 192 + max_stock: 1000 + price: 769 + sale_gamma: 48 + service_level: 0.95 + SKU997: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 81 + max_stock: 1000 + price: 518 + sale_gamma: 27 + service_level: 0.95 + SKU998: + constraint: null + cost: 239 + init_stock: 90 + max_stock: 1000 + price: 442 + sale_gamma: 15 + service_level: 0.95 + SKU999: + constraint: null + cost: 435 + init_stock: 400 + max_stock: 1000 + price: 843 + sale_gamma: 100 + service_level: 0.95 + grid: + facilities: + STORE0: + - 14 + - 17 + SUPPLIER0: + - 9 + - 0 + WAREHOUSE0: + - 8 + - 18 + size: + - 20 + - 20 + skus: + - id: 0 + name: SKU0 + - id: 1 + name: SKU1 + - id: 2 + name: SKU2 + - id: 3 + name: SKU3 + - id: 4 + name: SKU4 + - id: 5 + name: SKU5 + - id: 6 + name: SKU6 + - id: 7 + name: SKU7 + - id: 8 + name: SKU8 + - id: 9 + name: SKU9 + - id: 10 + name: SKU10 + - id: 11 + name: SKU11 + - id: 12 + name: SKU12 + - id: 13 + name: SKU13 + - id: 14 + name: SKU14 + - id: 15 + name: SKU15 + - id: 16 + name: SKU16 + - id: 17 + name: SKU17 + - id: 18 + name: SKU18 + - id: 19 + name: SKU19 + - id: 20 + name: SKU20 + - id: 21 + name: SKU21 + - id: 22 + name: SKU22 + - id: 23 + name: SKU23 + - id: 24 + name: SKU24 + - id: 25 + name: SKU25 + - id: 26 + name: SKU26 + - id: 27 + name: SKU27 + - id: 28 + name: SKU28 + - id: 29 + name: SKU29 + - id: 30 + name: SKU30 + - id: 31 + name: SKU31 + - id: 32 + name: SKU32 + - id: 33 + name: SKU33 + - id: 34 + name: SKU34 + - id: 35 + name: SKU35 + - id: 36 + name: SKU36 + - id: 37 + name: SKU37 + - id: 38 + name: SKU38 + - id: 39 + name: SKU39 + - id: 40 + name: SKU40 + - id: 41 + name: SKU41 + - id: 42 + name: SKU42 + - id: 43 + name: SKU43 + - id: 44 + name: SKU44 + - id: 45 + name: SKU45 + - id: 46 + name: SKU46 + - id: 47 + name: SKU47 + - id: 48 + name: SKU48 + - id: 49 + name: SKU49 + - id: 50 + name: SKU50 + - id: 51 + name: SKU51 + - id: 52 + name: SKU52 + - id: 53 + name: SKU53 + - id: 54 + name: SKU54 + - id: 55 + name: SKU55 + - id: 56 + name: SKU56 + - id: 57 + name: SKU57 + - id: 58 + name: SKU58 + - id: 59 + name: SKU59 + - id: 60 + name: SKU60 + - id: 61 + name: SKU61 + - id: 62 + name: SKU62 + - id: 63 + name: SKU63 + - id: 64 + name: SKU64 + - id: 65 + name: SKU65 + - id: 66 + name: SKU66 + - id: 67 + name: SKU67 + - id: 68 + name: SKU68 + - id: 69 + name: SKU69 + - id: 70 + name: SKU70 + - id: 71 + name: SKU71 + - id: 72 + name: SKU72 + - id: 73 + name: SKU73 + - id: 74 + name: SKU74 + - id: 75 + name: SKU75 + - id: 76 + name: SKU76 + - id: 77 + name: SKU77 + - id: 78 + name: SKU78 + - id: 79 + name: SKU79 + - id: 80 + name: SKU80 + - id: 81 + name: SKU81 + - id: 82 + name: SKU82 + - id: 83 + name: SKU83 + - id: 84 + name: SKU84 + - id: 85 + name: SKU85 + - id: 86 + name: SKU86 + - id: 87 + name: SKU87 + - id: 88 + name: SKU88 + - id: 89 + name: SKU89 + - id: 90 + name: SKU90 + - id: 91 + name: SKU91 + - id: 92 + name: SKU92 + - id: 93 + name: SKU93 + - id: 94 + name: SKU94 + - id: 95 + name: SKU95 + - id: 96 + name: SKU96 + - id: 97 + name: SKU97 + - id: 98 + name: SKU98 + - id: 99 + name: SKU99 + - id: 100 + name: SKU100 + - id: 101 + name: SKU101 + - id: 102 + name: SKU102 + - id: 103 + name: SKU103 + - id: 104 + name: SKU104 + - id: 105 + name: SKU105 + - id: 106 + name: SKU106 + - id: 107 + name: SKU107 + - id: 108 + name: SKU108 + - id: 109 + name: SKU109 + - id: 110 + name: SKU110 + - id: 111 + name: SKU111 + - id: 112 + name: SKU112 + - id: 113 + name: SKU113 + - id: 114 + name: SKU114 + - id: 115 + name: SKU115 + - id: 116 + name: SKU116 + - id: 117 + name: SKU117 + - id: 118 + name: SKU118 + - id: 119 + name: SKU119 + - id: 120 + name: SKU120 + - id: 121 + name: SKU121 + - id: 122 + name: SKU122 + - id: 123 + name: SKU123 + - id: 124 + name: SKU124 + - id: 125 + name: SKU125 + - id: 126 + name: SKU126 + - id: 127 + name: SKU127 + - id: 128 + name: SKU128 + - id: 129 + name: SKU129 + - id: 130 + name: SKU130 + - id: 131 + name: SKU131 + - id: 132 + name: SKU132 + - id: 133 + name: SKU133 + - id: 134 + name: SKU134 + - id: 135 + name: SKU135 + - id: 136 + name: SKU136 + - id: 137 + name: SKU137 + - id: 138 + name: SKU138 + - id: 139 + name: SKU139 + - id: 140 + name: SKU140 + - id: 141 + name: SKU141 + - id: 142 + name: SKU142 + - id: 143 + name: SKU143 + - id: 144 + name: SKU144 + - id: 145 + name: SKU145 + - id: 146 + name: SKU146 + - id: 147 + name: SKU147 + - id: 148 + name: SKU148 + - id: 149 + name: SKU149 + - id: 150 + name: SKU150 + - id: 151 + name: SKU151 + - id: 152 + name: SKU152 + - id: 153 + name: SKU153 + - id: 154 + name: SKU154 + - id: 155 + name: SKU155 + - id: 156 + name: SKU156 + - id: 157 + name: SKU157 + - id: 158 + name: SKU158 + - id: 159 + name: SKU159 + - id: 160 + name: SKU160 + - id: 161 + name: SKU161 + - id: 162 + name: SKU162 + - id: 163 + name: SKU163 + - id: 164 + name: SKU164 + - id: 165 + name: SKU165 + - id: 166 + name: SKU166 + - id: 167 + name: SKU167 + - id: 168 + name: SKU168 + - id: 169 + name: SKU169 + - id: 170 + name: SKU170 + - id: 171 + name: SKU171 + - id: 172 + name: SKU172 + - id: 173 + name: SKU173 + - id: 174 + name: SKU174 + - id: 175 + name: SKU175 + - id: 176 + name: SKU176 + - id: 177 + name: SKU177 + - id: 178 + name: SKU178 + - id: 179 + name: SKU179 + - id: 180 + name: SKU180 + - id: 181 + name: SKU181 + - id: 182 + name: SKU182 + - id: 183 + name: SKU183 + - id: 184 + name: SKU184 + - id: 185 + name: SKU185 + - id: 186 + name: SKU186 + - id: 187 + name: SKU187 + - id: 188 + name: SKU188 + - id: 189 + name: SKU189 + - id: 190 + name: SKU190 + - id: 191 + name: SKU191 + - id: 192 + name: SKU192 + - id: 193 + name: SKU193 + - id: 194 + name: SKU194 + - id: 195 + name: SKU195 + - id: 196 + name: SKU196 + - id: 197 + name: SKU197 + - id: 198 + name: SKU198 + - id: 199 + name: SKU199 + - id: 200 + name: SKU200 + - id: 201 + name: SKU201 + - id: 202 + name: SKU202 + - id: 203 + name: SKU203 + - id: 204 + name: SKU204 + - id: 205 + name: SKU205 + - id: 206 + name: SKU206 + - id: 207 + name: SKU207 + - id: 208 + name: SKU208 + - id: 209 + name: SKU209 + - id: 210 + name: SKU210 + - id: 211 + name: SKU211 + - id: 212 + name: SKU212 + - id: 213 + name: SKU213 + - id: 214 + name: SKU214 + - id: 215 + name: SKU215 + - id: 216 + name: SKU216 + - id: 217 + name: SKU217 + - id: 218 + name: SKU218 + - id: 219 + name: SKU219 + - id: 220 + name: SKU220 + - id: 221 + name: SKU221 + - id: 222 + name: SKU222 + - id: 223 + name: SKU223 + - id: 224 + name: SKU224 + - id: 225 + name: SKU225 + - id: 226 + name: SKU226 + - id: 227 + name: SKU227 + - id: 228 + name: SKU228 + - id: 229 + name: SKU229 + - id: 230 + name: SKU230 + - id: 231 + name: SKU231 + - id: 232 + name: SKU232 + - id: 233 + name: SKU233 + - id: 234 + name: SKU234 + - id: 235 + name: SKU235 + - id: 236 + name: SKU236 + - id: 237 + name: SKU237 + - id: 238 + name: SKU238 + - id: 239 + name: SKU239 + - id: 240 + name: SKU240 + - id: 241 + name: SKU241 + - id: 242 + name: SKU242 + - id: 243 + name: SKU243 + - id: 244 + name: SKU244 + - id: 245 + name: SKU245 + - id: 246 + name: SKU246 + - id: 247 + name: SKU247 + - id: 248 + name: SKU248 + - id: 249 + name: SKU249 + - id: 250 + name: SKU250 + - id: 251 + name: SKU251 + - id: 252 + name: SKU252 + - id: 253 + name: SKU253 + - id: 254 + name: SKU254 + - id: 255 + name: SKU255 + - id: 256 + name: SKU256 + - id: 257 + name: SKU257 + - id: 258 + name: SKU258 + - id: 259 + name: SKU259 + - id: 260 + name: SKU260 + - id: 261 + name: SKU261 + - id: 262 + name: SKU262 + - id: 263 + name: SKU263 + - id: 264 + name: SKU264 + - id: 265 + name: SKU265 + - id: 266 + name: SKU266 + - id: 267 + name: SKU267 + - id: 268 + name: SKU268 + - id: 269 + name: SKU269 + - id: 270 + name: SKU270 + - id: 271 + name: SKU271 + - id: 272 + name: SKU272 + - id: 273 + name: SKU273 + - id: 274 + name: SKU274 + - id: 275 + name: SKU275 + - id: 276 + name: SKU276 + - id: 277 + name: SKU277 + - id: 278 + name: SKU278 + - id: 279 + name: SKU279 + - id: 280 + name: SKU280 + - id: 281 + name: SKU281 + - id: 282 + name: SKU282 + - id: 283 + name: SKU283 + - id: 284 + name: SKU284 + - id: 285 + name: SKU285 + - id: 286 + name: SKU286 + - id: 287 + name: SKU287 + - id: 288 + name: SKU288 + - id: 289 + name: SKU289 + - id: 290 + name: SKU290 + - id: 291 + name: SKU291 + - id: 292 + name: SKU292 + - id: 293 + name: SKU293 + - id: 294 + name: SKU294 + - id: 295 + name: SKU295 + - id: 296 + name: SKU296 + - id: 297 + name: SKU297 + - id: 298 + name: SKU298 + - id: 299 + name: SKU299 + - id: 300 + name: SKU300 + - id: 301 + name: SKU301 + - id: 302 + name: SKU302 + - id: 303 + name: SKU303 + - id: 304 + name: SKU304 + - id: 305 + name: SKU305 + - id: 306 + name: SKU306 + - id: 307 + name: SKU307 + - id: 308 + name: SKU308 + - id: 309 + name: SKU309 + - id: 310 + name: SKU310 + - id: 311 + name: SKU311 + - id: 312 + name: SKU312 + - id: 313 + name: SKU313 + - id: 314 + name: SKU314 + - id: 315 + name: SKU315 + - id: 316 + name: SKU316 + - id: 317 + name: SKU317 + - id: 318 + name: SKU318 + - id: 319 + name: SKU319 + - id: 320 + name: SKU320 + - id: 321 + name: SKU321 + - id: 322 + name: SKU322 + - id: 323 + name: SKU323 + - id: 324 + name: SKU324 + - id: 325 + name: SKU325 + - id: 326 + name: SKU326 + - id: 327 + name: SKU327 + - id: 328 + name: SKU328 + - id: 329 + name: SKU329 + - id: 330 + name: SKU330 + - id: 331 + name: SKU331 + - id: 332 + name: SKU332 + - id: 333 + name: SKU333 + - id: 334 + name: SKU334 + - id: 335 + name: SKU335 + - id: 336 + name: SKU336 + - id: 337 + name: SKU337 + - id: 338 + name: SKU338 + - id: 339 + name: SKU339 + - id: 340 + name: SKU340 + - id: 341 + name: SKU341 + - id: 342 + name: SKU342 + - id: 343 + name: SKU343 + - id: 344 + name: SKU344 + - id: 345 + name: SKU345 + - id: 346 + name: SKU346 + - id: 347 + name: SKU347 + - id: 348 + name: SKU348 + - id: 349 + name: SKU349 + - id: 350 + name: SKU350 + - id: 351 + name: SKU351 + - id: 352 + name: SKU352 + - id: 353 + name: SKU353 + - id: 354 + name: SKU354 + - id: 355 + name: SKU355 + - id: 356 + name: SKU356 + - id: 357 + name: SKU357 + - id: 358 + name: SKU358 + - id: 359 + name: SKU359 + - id: 360 + name: SKU360 + - id: 361 + name: SKU361 + - id: 362 + name: SKU362 + - id: 363 + name: SKU363 + - id: 364 + name: SKU364 + - id: 365 + name: SKU365 + - id: 366 + name: SKU366 + - id: 367 + name: SKU367 + - id: 368 + name: SKU368 + - id: 369 + name: SKU369 + - id: 370 + name: SKU370 + - id: 371 + name: SKU371 + - id: 372 + name: SKU372 + - id: 373 + name: SKU373 + - id: 374 + name: SKU374 + - id: 375 + name: SKU375 + - id: 376 + name: SKU376 + - id: 377 + name: SKU377 + - id: 378 + name: SKU378 + - id: 379 + name: SKU379 + - id: 380 + name: SKU380 + - id: 381 + name: SKU381 + - id: 382 + name: SKU382 + - id: 383 + name: SKU383 + - id: 384 + name: SKU384 + - id: 385 + name: SKU385 + - id: 386 + name: SKU386 + - id: 387 + name: SKU387 + - id: 388 + name: SKU388 + - id: 389 + name: SKU389 + - id: 390 + name: SKU390 + - id: 391 + name: SKU391 + - id: 392 + name: SKU392 + - id: 393 + name: SKU393 + - id: 394 + name: SKU394 + - id: 395 + name: SKU395 + - id: 396 + name: SKU396 + - id: 397 + name: SKU397 + - id: 398 + name: SKU398 + - id: 399 + name: SKU399 + - id: 400 + name: SKU400 + - id: 401 + name: SKU401 + - id: 402 + name: SKU402 + - id: 403 + name: SKU403 + - id: 404 + name: SKU404 + - id: 405 + name: SKU405 + - id: 406 + name: SKU406 + - id: 407 + name: SKU407 + - id: 408 + name: SKU408 + - id: 409 + name: SKU409 + - id: 410 + name: SKU410 + - id: 411 + name: SKU411 + - id: 412 + name: SKU412 + - id: 413 + name: SKU413 + - id: 414 + name: SKU414 + - id: 415 + name: SKU415 + - id: 416 + name: SKU416 + - id: 417 + name: SKU417 + - id: 418 + name: SKU418 + - id: 419 + name: SKU419 + - id: 420 + name: SKU420 + - id: 421 + name: SKU421 + - id: 422 + name: SKU422 + - id: 423 + name: SKU423 + - id: 424 + name: SKU424 + - id: 425 + name: SKU425 + - id: 426 + name: SKU426 + - id: 427 + name: SKU427 + - id: 428 + name: SKU428 + - id: 429 + name: SKU429 + - id: 430 + name: SKU430 + - id: 431 + name: SKU431 + - id: 432 + name: SKU432 + - id: 433 + name: SKU433 + - id: 434 + name: SKU434 + - id: 435 + name: SKU435 + - id: 436 + name: SKU436 + - id: 437 + name: SKU437 + - id: 438 + name: SKU438 + - id: 439 + name: SKU439 + - id: 440 + name: SKU440 + - id: 441 + name: SKU441 + - id: 442 + name: SKU442 + - id: 443 + name: SKU443 + - id: 444 + name: SKU444 + - id: 445 + name: SKU445 + - id: 446 + name: SKU446 + - id: 447 + name: SKU447 + - id: 448 + name: SKU448 + - id: 449 + name: SKU449 + - id: 450 + name: SKU450 + - id: 451 + name: SKU451 + - id: 452 + name: SKU452 + - id: 453 + name: SKU453 + - id: 454 + name: SKU454 + - id: 455 + name: SKU455 + - id: 456 + name: SKU456 + - id: 457 + name: SKU457 + - id: 458 + name: SKU458 + - id: 459 + name: SKU459 + - id: 460 + name: SKU460 + - id: 461 + name: SKU461 + - id: 462 + name: SKU462 + - id: 463 + name: SKU463 + - id: 464 + name: SKU464 + - id: 465 + name: SKU465 + - id: 466 + name: SKU466 + - id: 467 + name: SKU467 + - id: 468 + name: SKU468 + - id: 469 + name: SKU469 + - id: 470 + name: SKU470 + - id: 471 + name: SKU471 + - id: 472 + name: SKU472 + - id: 473 + name: SKU473 + - id: 474 + name: SKU474 + - id: 475 + name: SKU475 + - id: 476 + name: SKU476 + - id: 477 + name: SKU477 + - id: 478 + name: SKU478 + - id: 479 + name: SKU479 + - id: 480 + name: SKU480 + - id: 481 + name: SKU481 + - id: 482 + name: SKU482 + - id: 483 + name: SKU483 + - id: 484 + name: SKU484 + - id: 485 + name: SKU485 + - id: 486 + name: SKU486 + - id: 487 + name: SKU487 + - id: 488 + name: SKU488 + - id: 489 + name: SKU489 + - id: 490 + name: SKU490 + - id: 491 + name: SKU491 + - id: 492 + name: SKU492 + - id: 493 + name: SKU493 + - id: 494 + name: SKU494 + - id: 495 + name: SKU495 + - id: 496 + name: SKU496 + - id: 497 + name: SKU497 + - id: 498 + name: SKU498 + - id: 499 + name: SKU499 + - id: 500 + name: SKU500 + - id: 501 + name: SKU501 + - id: 502 + name: SKU502 + - id: 503 + name: SKU503 + - id: 504 + name: SKU504 + - id: 505 + name: SKU505 + - id: 506 + name: SKU506 + - id: 507 + name: SKU507 + - id: 508 + name: SKU508 + - id: 509 + name: SKU509 + - id: 510 + name: SKU510 + - id: 511 + name: SKU511 + - id: 512 + name: SKU512 + - id: 513 + name: SKU513 + - id: 514 + name: SKU514 + - id: 515 + name: SKU515 + - id: 516 + name: SKU516 + - id: 517 + name: SKU517 + - id: 518 + name: SKU518 + - id: 519 + name: SKU519 + - id: 520 + name: SKU520 + - id: 521 + name: SKU521 + - id: 522 + name: SKU522 + - id: 523 + name: SKU523 + - id: 524 + name: SKU524 + - id: 525 + name: SKU525 + - id: 526 + name: SKU526 + - id: 527 + name: SKU527 + - id: 528 + name: SKU528 + - id: 529 + name: SKU529 + - id: 530 + name: SKU530 + - id: 531 + name: SKU531 + - id: 532 + name: SKU532 + - id: 533 + name: SKU533 + - id: 534 + name: SKU534 + - id: 535 + name: SKU535 + - id: 536 + name: SKU536 + - id: 537 + name: SKU537 + - id: 538 + name: SKU538 + - id: 539 + name: SKU539 + - id: 540 + name: SKU540 + - id: 541 + name: SKU541 + - id: 542 + name: SKU542 + - id: 543 + name: SKU543 + - id: 544 + name: SKU544 + - id: 545 + name: SKU545 + - id: 546 + name: SKU546 + - id: 547 + name: SKU547 + - id: 548 + name: SKU548 + - id: 549 + name: SKU549 + - id: 550 + name: SKU550 + - id: 551 + name: SKU551 + - id: 552 + name: SKU552 + - id: 553 + name: SKU553 + - id: 554 + name: SKU554 + - id: 555 + name: SKU555 + - id: 556 + name: SKU556 + - id: 557 + name: SKU557 + - id: 558 + name: SKU558 + - id: 559 + name: SKU559 + - id: 560 + name: SKU560 + - id: 561 + name: SKU561 + - id: 562 + name: SKU562 + - id: 563 + name: SKU563 + - id: 564 + name: SKU564 + - id: 565 + name: SKU565 + - id: 566 + name: SKU566 + - id: 567 + name: SKU567 + - id: 568 + name: SKU568 + - id: 569 + name: SKU569 + - id: 570 + name: SKU570 + - id: 571 + name: SKU571 + - id: 572 + name: SKU572 + - id: 573 + name: SKU573 + - id: 574 + name: SKU574 + - id: 575 + name: SKU575 + - id: 576 + name: SKU576 + - id: 577 + name: SKU577 + - id: 578 + name: SKU578 + - id: 579 + name: SKU579 + - id: 580 + name: SKU580 + - id: 581 + name: SKU581 + - id: 582 + name: SKU582 + - id: 583 + name: SKU583 + - id: 584 + name: SKU584 + - id: 585 + name: SKU585 + - id: 586 + name: SKU586 + - id: 587 + name: SKU587 + - id: 588 + name: SKU588 + - id: 589 + name: SKU589 + - id: 590 + name: SKU590 + - id: 591 + name: SKU591 + - id: 592 + name: SKU592 + - id: 593 + name: SKU593 + - id: 594 + name: SKU594 + - id: 595 + name: SKU595 + - id: 596 + name: SKU596 + - id: 597 + name: SKU597 + - id: 598 + name: SKU598 + - id: 599 + name: SKU599 + - id: 600 + name: SKU600 + - id: 601 + name: SKU601 + - id: 602 + name: SKU602 + - id: 603 + name: SKU603 + - id: 604 + name: SKU604 + - id: 605 + name: SKU605 + - id: 606 + name: SKU606 + - id: 607 + name: SKU607 + - id: 608 + name: SKU608 + - id: 609 + name: SKU609 + - id: 610 + name: SKU610 + - id: 611 + name: SKU611 + - id: 612 + name: SKU612 + - id: 613 + name: SKU613 + - id: 614 + name: SKU614 + - id: 615 + name: SKU615 + - id: 616 + name: SKU616 + - id: 617 + name: SKU617 + - id: 618 + name: SKU618 + - id: 619 + name: SKU619 + - id: 620 + name: SKU620 + - id: 621 + name: SKU621 + - id: 622 + name: SKU622 + - id: 623 + name: SKU623 + - id: 624 + name: SKU624 + - id: 625 + name: SKU625 + - id: 626 + name: SKU626 + - id: 627 + name: SKU627 + - id: 628 + name: SKU628 + - id: 629 + name: SKU629 + - id: 630 + name: SKU630 + - id: 631 + name: SKU631 + - id: 632 + name: SKU632 + - id: 633 + name: SKU633 + - id: 634 + name: SKU634 + - id: 635 + name: SKU635 + - id: 636 + name: SKU636 + - id: 637 + name: SKU637 + - id: 638 + name: SKU638 + - id: 639 + name: SKU639 + - id: 640 + name: SKU640 + - id: 641 + name: SKU641 + - id: 642 + name: SKU642 + - id: 643 + name: SKU643 + - id: 644 + name: SKU644 + - id: 645 + name: SKU645 + - id: 646 + name: SKU646 + - id: 647 + name: SKU647 + - id: 648 + name: SKU648 + - id: 649 + name: SKU649 + - id: 650 + name: SKU650 + - id: 651 + name: SKU651 + - id: 652 + name: SKU652 + - id: 653 + name: SKU653 + - id: 654 + name: SKU654 + - id: 655 + name: SKU655 + - id: 656 + name: SKU656 + - id: 657 + name: SKU657 + - id: 658 + name: SKU658 + - id: 659 + name: SKU659 + - id: 660 + name: SKU660 + - id: 661 + name: SKU661 + - id: 662 + name: SKU662 + - id: 663 + name: SKU663 + - id: 664 + name: SKU664 + - id: 665 + name: SKU665 + - id: 666 + name: SKU666 + - id: 667 + name: SKU667 + - id: 668 + name: SKU668 + - id: 669 + name: SKU669 + - id: 670 + name: SKU670 + - id: 671 + name: SKU671 + - id: 672 + name: SKU672 + - id: 673 + name: SKU673 + - id: 674 + name: SKU674 + - id: 675 + name: SKU675 + - id: 676 + name: SKU676 + - id: 677 + name: SKU677 + - id: 678 + name: SKU678 + - id: 679 + name: SKU679 + - id: 680 + name: SKU680 + - id: 681 + name: SKU681 + - id: 682 + name: SKU682 + - id: 683 + name: SKU683 + - id: 684 + name: SKU684 + - id: 685 + name: SKU685 + - id: 686 + name: SKU686 + - id: 687 + name: SKU687 + - id: 688 + name: SKU688 + - id: 689 + name: SKU689 + - id: 690 + name: SKU690 + - id: 691 + name: SKU691 + - id: 692 + name: SKU692 + - id: 693 + name: SKU693 + - id: 694 + name: SKU694 + - id: 695 + name: SKU695 + - id: 696 + name: SKU696 + - id: 697 + name: SKU697 + - id: 698 + name: SKU698 + - id: 699 + name: SKU699 + - id: 700 + name: SKU700 + - id: 701 + name: SKU701 + - id: 702 + name: SKU702 + - id: 703 + name: SKU703 + - id: 704 + name: SKU704 + - id: 705 + name: SKU705 + - id: 706 + name: SKU706 + - id: 707 + name: SKU707 + - id: 708 + name: SKU708 + - id: 709 + name: SKU709 + - id: 710 + name: SKU710 + - id: 711 + name: SKU711 + - id: 712 + name: SKU712 + - id: 713 + name: SKU713 + - id: 714 + name: SKU714 + - id: 715 + name: SKU715 + - id: 716 + name: SKU716 + - id: 717 + name: SKU717 + - id: 718 + name: SKU718 + - id: 719 + name: SKU719 + - id: 720 + name: SKU720 + - id: 721 + name: SKU721 + - id: 722 + name: SKU722 + - id: 723 + name: SKU723 + - id: 724 + name: SKU724 + - id: 725 + name: SKU725 + - id: 726 + name: SKU726 + - id: 727 + name: SKU727 + - id: 728 + name: SKU728 + - id: 729 + name: SKU729 + - id: 730 + name: SKU730 + - id: 731 + name: SKU731 + - id: 732 + name: SKU732 + - id: 733 + name: SKU733 + - id: 734 + name: SKU734 + - id: 735 + name: SKU735 + - id: 736 + name: SKU736 + - id: 737 + name: SKU737 + - id: 738 + name: SKU738 + - id: 739 + name: SKU739 + - id: 740 + name: SKU740 + - id: 741 + name: SKU741 + - id: 742 + name: SKU742 + - id: 743 + name: SKU743 + - id: 744 + name: SKU744 + - id: 745 + name: SKU745 + - id: 746 + name: SKU746 + - id: 747 + name: SKU747 + - id: 748 + name: SKU748 + - id: 749 + name: SKU749 + - id: 750 + name: SKU750 + - id: 751 + name: SKU751 + - id: 752 + name: SKU752 + - id: 753 + name: SKU753 + - id: 754 + name: SKU754 + - id: 755 + name: SKU755 + - id: 756 + name: SKU756 + - id: 757 + name: SKU757 + - id: 758 + name: SKU758 + - id: 759 + name: SKU759 + - id: 760 + name: SKU760 + - id: 761 + name: SKU761 + - id: 762 + name: SKU762 + - id: 763 + name: SKU763 + - id: 764 + name: SKU764 + - id: 765 + name: SKU765 + - id: 766 + name: SKU766 + - id: 767 + name: SKU767 + - id: 768 + name: SKU768 + - id: 769 + name: SKU769 + - id: 770 + name: SKU770 + - id: 771 + name: SKU771 + - id: 772 + name: SKU772 + - id: 773 + name: SKU773 + - id: 774 + name: SKU774 + - id: 775 + name: SKU775 + - id: 776 + name: SKU776 + - id: 777 + name: SKU777 + - id: 778 + name: SKU778 + - id: 779 + name: SKU779 + - id: 780 + name: SKU780 + - id: 781 + name: SKU781 + - id: 782 + name: SKU782 + - id: 783 + name: SKU783 + - id: 784 + name: SKU784 + - id: 785 + name: SKU785 + - id: 786 + name: SKU786 + - id: 787 + name: SKU787 + - id: 788 + name: SKU788 + - id: 789 + name: SKU789 + - id: 790 + name: SKU790 + - id: 791 + name: SKU791 + - id: 792 + name: SKU792 + - id: 793 + name: SKU793 + - id: 794 + name: SKU794 + - id: 795 + name: SKU795 + - id: 796 + name: SKU796 + - id: 797 + name: SKU797 + - id: 798 + name: SKU798 + - id: 799 + name: SKU799 + - id: 800 + name: SKU800 + - id: 801 + name: SKU801 + - id: 802 + name: SKU802 + - id: 803 + name: SKU803 + - id: 804 + name: SKU804 + - id: 805 + name: SKU805 + - id: 806 + name: SKU806 + - id: 807 + name: SKU807 + - id: 808 + name: SKU808 + - id: 809 + name: SKU809 + - id: 810 + name: SKU810 + - id: 811 + name: SKU811 + - id: 812 + name: SKU812 + - id: 813 + name: SKU813 + - id: 814 + name: SKU814 + - id: 815 + name: SKU815 + - id: 816 + name: SKU816 + - id: 817 + name: SKU817 + - id: 818 + name: SKU818 + - id: 819 + name: SKU819 + - id: 820 + name: SKU820 + - id: 821 + name: SKU821 + - id: 822 + name: SKU822 + - id: 823 + name: SKU823 + - id: 824 + name: SKU824 + - id: 825 + name: SKU825 + - id: 826 + name: SKU826 + - id: 827 + name: SKU827 + - id: 828 + name: SKU828 + - id: 829 + name: SKU829 + - id: 830 + name: SKU830 + - id: 831 + name: SKU831 + - id: 832 + name: SKU832 + - id: 833 + name: SKU833 + - id: 834 + name: SKU834 + - id: 835 + name: SKU835 + - id: 836 + name: SKU836 + - id: 837 + name: SKU837 + - id: 838 + name: SKU838 + - id: 839 + name: SKU839 + - id: 840 + name: SKU840 + - id: 841 + name: SKU841 + - id: 842 + name: SKU842 + - id: 843 + name: SKU843 + - id: 844 + name: SKU844 + - id: 845 + name: SKU845 + - id: 846 + name: SKU846 + - id: 847 + name: SKU847 + - id: 848 + name: SKU848 + - id: 849 + name: SKU849 + - id: 850 + name: SKU850 + - id: 851 + name: SKU851 + - id: 852 + name: SKU852 + - id: 853 + name: SKU853 + - id: 854 + name: SKU854 + - id: 855 + name: SKU855 + - id: 856 + name: SKU856 + - id: 857 + name: SKU857 + - id: 858 + name: SKU858 + - id: 859 + name: SKU859 + - id: 860 + name: SKU860 + - id: 861 + name: SKU861 + - id: 862 + name: SKU862 + - id: 863 + name: SKU863 + - id: 864 + name: SKU864 + - id: 865 + name: SKU865 + - id: 866 + name: SKU866 + - id: 867 + name: SKU867 + - id: 868 + name: SKU868 + - id: 869 + name: SKU869 + - id: 870 + name: SKU870 + - id: 871 + name: SKU871 + - id: 872 + name: SKU872 + - id: 873 + name: SKU873 + - id: 874 + name: SKU874 + - id: 875 + name: SKU875 + - id: 876 + name: SKU876 + - id: 877 + name: SKU877 + - id: 878 + name: SKU878 + - id: 879 + name: SKU879 + - id: 880 + name: SKU880 + - id: 881 + name: SKU881 + - id: 882 + name: SKU882 + - id: 883 + name: SKU883 + - id: 884 + name: SKU884 + - id: 885 + name: SKU885 + - id: 886 + name: SKU886 + - id: 887 + name: SKU887 + - id: 888 + name: SKU888 + - id: 889 + name: SKU889 + - id: 890 + name: SKU890 + - id: 891 + name: SKU891 + - id: 892 + name: SKU892 + - id: 893 + name: SKU893 + - id: 894 + name: SKU894 + - id: 895 + name: SKU895 + - id: 896 + name: SKU896 + - id: 897 + name: SKU897 + - id: 898 + name: SKU898 + - id: 899 + name: SKU899 + - id: 900 + name: SKU900 + - id: 901 + name: SKU901 + - id: 902 + name: SKU902 + - id: 903 + name: SKU903 + - id: 904 + name: SKU904 + - id: 905 + name: SKU905 + - id: 906 + name: SKU906 + - id: 907 + name: SKU907 + - id: 908 + name: SKU908 + - id: 909 + name: SKU909 + - id: 910 + name: SKU910 + - id: 911 + name: SKU911 + - id: 912 + name: SKU912 + - id: 913 + name: SKU913 + - id: 914 + name: SKU914 + - id: 915 + name: SKU915 + - id: 916 + name: SKU916 + - id: 917 + name: SKU917 + - id: 918 + name: SKU918 + - id: 919 + name: SKU919 + - id: 920 + name: SKU920 + - id: 921 + name: SKU921 + - id: 922 + name: SKU922 + - id: 923 + name: SKU923 + - id: 924 + name: SKU924 + - id: 925 + name: SKU925 + - id: 926 + name: SKU926 + - id: 927 + name: SKU927 + - id: 928 + name: SKU928 + - id: 929 + name: SKU929 + - id: 930 + name: SKU930 + - id: 931 + name: SKU931 + - id: 932 + name: SKU932 + - id: 933 + name: SKU933 + - id: 934 + name: SKU934 + - id: 935 + name: SKU935 + - id: 936 + name: SKU936 + - id: 937 + name: SKU937 + - id: 938 + name: SKU938 + - id: 939 + name: SKU939 + - id: 940 + name: SKU940 + - id: 941 + name: SKU941 + - id: 942 + name: SKU942 + - id: 943 + name: SKU943 + - id: 944 + name: SKU944 + - id: 945 + name: SKU945 + - id: 946 + name: SKU946 + - id: 947 + name: SKU947 + - id: 948 + name: SKU948 + - id: 949 + name: SKU949 + - id: 950 + name: SKU950 + - id: 951 + name: SKU951 + - id: 952 + name: SKU952 + - id: 953 + name: SKU953 + - id: 954 + name: SKU954 + - id: 955 + name: SKU955 + - id: 956 + name: SKU956 + - id: 957 + name: SKU957 + - id: 958 + name: SKU958 + - id: 959 + name: SKU959 + - id: 960 + name: SKU960 + - id: 961 + name: SKU961 + - id: 962 + name: SKU962 + - id: 963 + name: SKU963 + - id: 964 + name: SKU964 + - id: 965 + name: SKU965 + - id: 966 + name: SKU966 + - id: 967 + name: SKU967 + - id: 968 + name: SKU968 + - id: 969 + name: SKU969 + - id: 970 + name: SKU970 + - id: 971 + name: SKU971 + - id: 972 + name: SKU972 + - id: 973 + name: SKU973 + - id: 974 + name: SKU974 + - id: 975 + name: SKU975 + - id: 976 + name: SKU976 + - id: 977 + name: SKU977 + - id: 978 + name: SKU978 + - id: 979 + name: SKU979 + - id: 980 + name: SKU980 + - id: 981 + name: SKU981 + - id: 982 + name: SKU982 + - id: 983 + name: SKU983 + - id: 984 + name: SKU984 + - id: 985 + name: SKU985 + - id: 986 + name: SKU986 + - id: 987 + name: SKU987 + - id: 988 + name: SKU988 + - id: 989 + name: SKU989 + - id: 990 + name: SKU990 + - id: 991 + name: SKU991 + - id: 992 + name: SKU992 + - id: 993 + name: SKU993 + - id: 994 + name: SKU994 + - id: 995 + name: SKU995 + - id: 996 + name: SKU996 + - id: 997 + name: SKU997 + - id: 998 + name: SKU998 + - id: 999 + name: SKU999 + topology: + STORE0: + SKU0: + - WAREHOUSE0 + SKU1: + - WAREHOUSE0 + SKU10: + - WAREHOUSE0 + SKU100: + - WAREHOUSE0 + SKU101: + - WAREHOUSE0 + SKU102: + - WAREHOUSE0 + SKU103: + - WAREHOUSE0 + SKU104: + - WAREHOUSE0 + SKU105: + - WAREHOUSE0 + SKU106: + - WAREHOUSE0 + SKU107: + - WAREHOUSE0 + SKU108: + - WAREHOUSE0 + SKU109: + - WAREHOUSE0 + SKU11: + - WAREHOUSE0 + SKU110: + - WAREHOUSE0 + SKU111: + - WAREHOUSE0 + SKU112: + - WAREHOUSE0 + SKU113: + - WAREHOUSE0 + SKU114: + - WAREHOUSE0 + SKU115: + - WAREHOUSE0 + SKU116: + - WAREHOUSE0 + SKU117: + - WAREHOUSE0 + SKU118: + - WAREHOUSE0 + SKU119: + - WAREHOUSE0 + SKU12: + - WAREHOUSE0 + SKU120: + - WAREHOUSE0 + SKU121: + - WAREHOUSE0 + SKU122: + - WAREHOUSE0 + SKU123: + - WAREHOUSE0 + SKU124: + - WAREHOUSE0 + SKU125: + - WAREHOUSE0 + SKU126: + - WAREHOUSE0 + SKU127: + - WAREHOUSE0 + SKU128: + - WAREHOUSE0 + SKU129: + - WAREHOUSE0 + SKU13: + - WAREHOUSE0 + SKU130: + - WAREHOUSE0 + SKU131: + - WAREHOUSE0 + SKU132: + - WAREHOUSE0 + SKU133: + - WAREHOUSE0 + SKU134: + - WAREHOUSE0 + SKU135: + - WAREHOUSE0 + SKU136: + - WAREHOUSE0 + SKU137: + - WAREHOUSE0 + SKU138: + - WAREHOUSE0 + SKU139: + - WAREHOUSE0 + SKU14: + - WAREHOUSE0 + SKU140: + - WAREHOUSE0 + SKU141: + - WAREHOUSE0 + SKU142: + - WAREHOUSE0 + SKU143: + - WAREHOUSE0 + SKU144: + - WAREHOUSE0 + SKU145: + - WAREHOUSE0 + SKU146: + - WAREHOUSE0 + SKU147: + - WAREHOUSE0 + SKU148: + - WAREHOUSE0 + SKU149: + - WAREHOUSE0 + SKU15: + - WAREHOUSE0 + SKU150: + - WAREHOUSE0 + SKU151: + - WAREHOUSE0 + SKU152: + - WAREHOUSE0 + SKU153: + - WAREHOUSE0 + SKU154: + - WAREHOUSE0 + SKU155: + - WAREHOUSE0 + SKU156: + - WAREHOUSE0 + SKU157: + - WAREHOUSE0 + SKU158: + - WAREHOUSE0 + SKU159: + - WAREHOUSE0 + SKU16: + - WAREHOUSE0 + SKU160: + - WAREHOUSE0 + SKU161: + - WAREHOUSE0 + SKU162: + - WAREHOUSE0 + SKU163: + - WAREHOUSE0 + SKU164: + - WAREHOUSE0 + SKU165: + - WAREHOUSE0 + SKU166: + - WAREHOUSE0 + SKU167: + - WAREHOUSE0 + SKU168: + - WAREHOUSE0 + SKU169: + - WAREHOUSE0 + SKU17: + - WAREHOUSE0 + SKU170: + - WAREHOUSE0 + SKU171: + - WAREHOUSE0 + SKU172: + - WAREHOUSE0 + SKU173: + - WAREHOUSE0 + SKU174: + - WAREHOUSE0 + SKU175: + - WAREHOUSE0 + SKU176: + - WAREHOUSE0 + SKU177: + - WAREHOUSE0 + SKU178: + - WAREHOUSE0 + SKU179: + - WAREHOUSE0 + SKU18: + - WAREHOUSE0 + SKU180: + - WAREHOUSE0 + SKU181: + - WAREHOUSE0 + SKU182: + - WAREHOUSE0 + SKU183: + - WAREHOUSE0 + SKU184: + - WAREHOUSE0 + SKU185: + - WAREHOUSE0 + SKU186: + - WAREHOUSE0 + SKU187: + - WAREHOUSE0 + SKU188: + - WAREHOUSE0 + SKU189: + - WAREHOUSE0 + SKU19: + - WAREHOUSE0 + SKU190: + - WAREHOUSE0 + SKU191: + - WAREHOUSE0 + SKU192: + - WAREHOUSE0 + SKU193: + - WAREHOUSE0 + SKU194: + - WAREHOUSE0 + SKU195: + - WAREHOUSE0 + SKU196: + - WAREHOUSE0 + SKU197: + - WAREHOUSE0 + SKU198: + - WAREHOUSE0 + SKU199: + - WAREHOUSE0 + SKU2: + - WAREHOUSE0 + SKU20: + - WAREHOUSE0 + SKU200: + - WAREHOUSE0 + SKU201: + - WAREHOUSE0 + SKU202: + - WAREHOUSE0 + SKU203: + - WAREHOUSE0 + SKU204: + - WAREHOUSE0 + SKU205: + - WAREHOUSE0 + SKU206: + - WAREHOUSE0 + SKU207: + - WAREHOUSE0 + SKU208: + - WAREHOUSE0 + SKU209: + - WAREHOUSE0 + SKU21: + - WAREHOUSE0 + SKU210: + - WAREHOUSE0 + SKU211: + - WAREHOUSE0 + SKU212: + - WAREHOUSE0 + SKU213: + - WAREHOUSE0 + SKU214: + - WAREHOUSE0 + SKU215: + - WAREHOUSE0 + SKU216: + - WAREHOUSE0 + SKU217: + - WAREHOUSE0 + SKU218: + - WAREHOUSE0 + SKU219: + - WAREHOUSE0 + SKU22: + - WAREHOUSE0 + SKU220: + - WAREHOUSE0 + SKU221: + - WAREHOUSE0 + SKU222: + - WAREHOUSE0 + SKU223: + - WAREHOUSE0 + SKU224: + - WAREHOUSE0 + SKU225: + - WAREHOUSE0 + SKU226: + - WAREHOUSE0 + SKU227: + - WAREHOUSE0 + SKU228: + - WAREHOUSE0 + SKU229: + - WAREHOUSE0 + SKU23: + - WAREHOUSE0 + SKU230: + - WAREHOUSE0 + SKU231: + - WAREHOUSE0 + SKU232: + - WAREHOUSE0 + SKU233: + - WAREHOUSE0 + SKU234: + - WAREHOUSE0 + SKU235: + - WAREHOUSE0 + SKU236: + - WAREHOUSE0 + SKU237: + - WAREHOUSE0 + SKU238: + - WAREHOUSE0 + SKU239: + - WAREHOUSE0 + SKU24: + - WAREHOUSE0 + SKU240: + - WAREHOUSE0 + SKU241: + - WAREHOUSE0 + SKU242: + - WAREHOUSE0 + SKU243: + - WAREHOUSE0 + SKU244: + - WAREHOUSE0 + SKU245: + - WAREHOUSE0 + SKU246: + - WAREHOUSE0 + SKU247: + - WAREHOUSE0 + SKU248: + - WAREHOUSE0 + SKU249: + - WAREHOUSE0 + SKU25: + - WAREHOUSE0 + SKU250: + - WAREHOUSE0 + SKU251: + - WAREHOUSE0 + SKU252: + - WAREHOUSE0 + SKU253: + - WAREHOUSE0 + SKU254: + - WAREHOUSE0 + SKU255: + - WAREHOUSE0 + SKU256: + - WAREHOUSE0 + SKU257: + - WAREHOUSE0 + SKU258: + - WAREHOUSE0 + SKU259: + - WAREHOUSE0 + SKU26: + - WAREHOUSE0 + SKU260: + - WAREHOUSE0 + SKU261: + - WAREHOUSE0 + SKU262: + - WAREHOUSE0 + SKU263: + - WAREHOUSE0 + SKU264: + - WAREHOUSE0 + SKU265: + - WAREHOUSE0 + SKU266: + - WAREHOUSE0 + SKU267: + - WAREHOUSE0 + SKU268: + - WAREHOUSE0 + SKU269: + - WAREHOUSE0 + SKU27: + - WAREHOUSE0 + SKU270: + - WAREHOUSE0 + SKU271: + - WAREHOUSE0 + SKU272: + - WAREHOUSE0 + SKU273: + - WAREHOUSE0 + SKU274: + - WAREHOUSE0 + SKU275: + - WAREHOUSE0 + SKU276: + - WAREHOUSE0 + SKU277: + - WAREHOUSE0 + SKU278: + - WAREHOUSE0 + SKU279: + - WAREHOUSE0 + SKU28: + - WAREHOUSE0 + SKU280: + - WAREHOUSE0 + SKU281: + - WAREHOUSE0 + SKU282: + - WAREHOUSE0 + SKU283: + - WAREHOUSE0 + SKU284: + - WAREHOUSE0 + SKU285: + - WAREHOUSE0 + SKU286: + - WAREHOUSE0 + SKU287: + - WAREHOUSE0 + SKU288: + - WAREHOUSE0 + SKU289: + - WAREHOUSE0 + SKU29: + - WAREHOUSE0 + SKU290: + - WAREHOUSE0 + SKU291: + - WAREHOUSE0 + SKU292: + - WAREHOUSE0 + SKU293: + - WAREHOUSE0 + SKU294: + - WAREHOUSE0 + SKU295: + - WAREHOUSE0 + SKU296: + - WAREHOUSE0 + SKU297: + - WAREHOUSE0 + SKU298: + - WAREHOUSE0 + SKU299: + - WAREHOUSE0 + SKU3: + - WAREHOUSE0 + SKU30: + - WAREHOUSE0 + SKU300: + - WAREHOUSE0 + SKU301: + - WAREHOUSE0 + SKU302: + - WAREHOUSE0 + SKU303: + - WAREHOUSE0 + SKU304: + - WAREHOUSE0 + SKU305: + - WAREHOUSE0 + SKU306: + - WAREHOUSE0 + SKU307: + - WAREHOUSE0 + SKU308: + - WAREHOUSE0 + SKU309: + - WAREHOUSE0 + SKU31: + - WAREHOUSE0 + SKU310: + - WAREHOUSE0 + SKU311: + - WAREHOUSE0 + SKU312: + - WAREHOUSE0 + SKU313: + - WAREHOUSE0 + SKU314: + - WAREHOUSE0 + SKU315: + - WAREHOUSE0 + SKU316: + - WAREHOUSE0 + SKU317: + - WAREHOUSE0 + SKU318: + - WAREHOUSE0 + SKU319: + - WAREHOUSE0 + SKU32: + - WAREHOUSE0 + SKU320: + - WAREHOUSE0 + SKU321: + - WAREHOUSE0 + SKU322: + - WAREHOUSE0 + SKU323: + - WAREHOUSE0 + SKU324: + - WAREHOUSE0 + SKU325: + - WAREHOUSE0 + SKU326: + - WAREHOUSE0 + SKU327: + - WAREHOUSE0 + SKU328: + - WAREHOUSE0 + SKU329: + - WAREHOUSE0 + SKU33: + - WAREHOUSE0 + SKU330: + - WAREHOUSE0 + SKU331: + - WAREHOUSE0 + SKU332: + - WAREHOUSE0 + SKU333: + - WAREHOUSE0 + SKU334: + - WAREHOUSE0 + SKU335: + - WAREHOUSE0 + SKU336: + - WAREHOUSE0 + SKU337: + - WAREHOUSE0 + SKU338: + - WAREHOUSE0 + SKU339: + - WAREHOUSE0 + SKU34: + - WAREHOUSE0 + SKU340: + - WAREHOUSE0 + SKU341: + - WAREHOUSE0 + SKU342: + - WAREHOUSE0 + SKU343: + - WAREHOUSE0 + SKU344: + - WAREHOUSE0 + SKU345: + - WAREHOUSE0 + SKU346: + - WAREHOUSE0 + SKU347: + - WAREHOUSE0 + SKU348: + - WAREHOUSE0 + SKU349: + - WAREHOUSE0 + SKU35: + - WAREHOUSE0 + SKU350: + - WAREHOUSE0 + SKU351: + - WAREHOUSE0 + SKU352: + - WAREHOUSE0 + SKU353: + - WAREHOUSE0 + SKU354: + - WAREHOUSE0 + SKU355: + - WAREHOUSE0 + SKU356: + - WAREHOUSE0 + SKU357: + - WAREHOUSE0 + SKU358: + - WAREHOUSE0 + SKU359: + - WAREHOUSE0 + SKU36: + - WAREHOUSE0 + SKU360: + - WAREHOUSE0 + SKU361: + - WAREHOUSE0 + SKU362: + - WAREHOUSE0 + SKU363: + - WAREHOUSE0 + SKU364: + - WAREHOUSE0 + SKU365: + - WAREHOUSE0 + SKU366: + - WAREHOUSE0 + SKU367: + - WAREHOUSE0 + SKU368: + - WAREHOUSE0 + SKU369: + - WAREHOUSE0 + SKU37: + - WAREHOUSE0 + SKU370: + - WAREHOUSE0 + SKU371: + - WAREHOUSE0 + SKU372: + - WAREHOUSE0 + SKU373: + - WAREHOUSE0 + SKU374: + - WAREHOUSE0 + SKU375: + - WAREHOUSE0 + SKU376: + - WAREHOUSE0 + SKU377: + - WAREHOUSE0 + SKU378: + - WAREHOUSE0 + SKU379: + - WAREHOUSE0 + SKU38: + - WAREHOUSE0 + SKU380: + - WAREHOUSE0 + SKU381: + - WAREHOUSE0 + SKU382: + - WAREHOUSE0 + SKU383: + - WAREHOUSE0 + SKU384: + - WAREHOUSE0 + SKU385: + - WAREHOUSE0 + SKU386: + - WAREHOUSE0 + SKU387: + - WAREHOUSE0 + SKU388: + - WAREHOUSE0 + SKU389: + - WAREHOUSE0 + SKU39: + - WAREHOUSE0 + SKU390: + - WAREHOUSE0 + SKU391: + - WAREHOUSE0 + SKU392: + - WAREHOUSE0 + SKU393: + - WAREHOUSE0 + SKU394: + - WAREHOUSE0 + SKU395: + - WAREHOUSE0 + SKU396: + - WAREHOUSE0 + SKU397: + - WAREHOUSE0 + SKU398: + - WAREHOUSE0 + SKU399: + - WAREHOUSE0 + SKU4: + - WAREHOUSE0 + SKU40: + - WAREHOUSE0 + SKU400: + - WAREHOUSE0 + SKU401: + - WAREHOUSE0 + SKU402: + - WAREHOUSE0 + SKU403: + - WAREHOUSE0 + SKU404: + - WAREHOUSE0 + SKU405: + - WAREHOUSE0 + SKU406: + - WAREHOUSE0 + SKU407: + - WAREHOUSE0 + SKU408: + - WAREHOUSE0 + SKU409: + - WAREHOUSE0 + SKU41: + - WAREHOUSE0 + SKU410: + - WAREHOUSE0 + SKU411: + - WAREHOUSE0 + SKU412: + - WAREHOUSE0 + SKU413: + - WAREHOUSE0 + SKU414: + - WAREHOUSE0 + SKU415: + - WAREHOUSE0 + SKU416: + - WAREHOUSE0 + SKU417: + - WAREHOUSE0 + SKU418: + - WAREHOUSE0 + SKU419: + - WAREHOUSE0 + SKU42: + - WAREHOUSE0 + SKU420: + - WAREHOUSE0 + SKU421: + - WAREHOUSE0 + SKU422: + - WAREHOUSE0 + SKU423: + - WAREHOUSE0 + SKU424: + - WAREHOUSE0 + SKU425: + - WAREHOUSE0 + SKU426: + - WAREHOUSE0 + SKU427: + - WAREHOUSE0 + SKU428: + - WAREHOUSE0 + SKU429: + - WAREHOUSE0 + SKU43: + - WAREHOUSE0 + SKU430: + - WAREHOUSE0 + SKU431: + - WAREHOUSE0 + SKU432: + - WAREHOUSE0 + SKU433: + - WAREHOUSE0 + SKU434: + - WAREHOUSE0 + SKU435: + - WAREHOUSE0 + SKU436: + - WAREHOUSE0 + SKU437: + - WAREHOUSE0 + SKU438: + - WAREHOUSE0 + SKU439: + - WAREHOUSE0 + SKU44: + - WAREHOUSE0 + SKU440: + - WAREHOUSE0 + SKU441: + - WAREHOUSE0 + SKU442: + - WAREHOUSE0 + SKU443: + - WAREHOUSE0 + SKU444: + - WAREHOUSE0 + SKU445: + - WAREHOUSE0 + SKU446: + - WAREHOUSE0 + SKU447: + - WAREHOUSE0 + SKU448: + - WAREHOUSE0 + SKU449: + - WAREHOUSE0 + SKU45: + - WAREHOUSE0 + SKU450: + - WAREHOUSE0 + SKU451: + - WAREHOUSE0 + SKU452: + - WAREHOUSE0 + SKU453: + - WAREHOUSE0 + SKU454: + - WAREHOUSE0 + SKU455: + - WAREHOUSE0 + SKU456: + - WAREHOUSE0 + SKU457: + - WAREHOUSE0 + SKU458: + - WAREHOUSE0 + SKU459: + - WAREHOUSE0 + SKU46: + - WAREHOUSE0 + SKU460: + - WAREHOUSE0 + SKU461: + - WAREHOUSE0 + SKU462: + - WAREHOUSE0 + SKU463: + - WAREHOUSE0 + SKU464: + - WAREHOUSE0 + SKU465: + - WAREHOUSE0 + SKU466: + - WAREHOUSE0 + SKU467: + - WAREHOUSE0 + SKU468: + - WAREHOUSE0 + SKU469: + - WAREHOUSE0 + SKU47: + - WAREHOUSE0 + SKU470: + - WAREHOUSE0 + SKU471: + - WAREHOUSE0 + SKU472: + - WAREHOUSE0 + SKU473: + - WAREHOUSE0 + SKU474: + - WAREHOUSE0 + SKU475: + - WAREHOUSE0 + SKU476: + - WAREHOUSE0 + SKU477: + - WAREHOUSE0 + SKU478: + - WAREHOUSE0 + SKU479: + - WAREHOUSE0 + SKU48: + - WAREHOUSE0 + SKU480: + - WAREHOUSE0 + SKU481: + - WAREHOUSE0 + SKU482: + - WAREHOUSE0 + SKU483: + - WAREHOUSE0 + SKU484: + - WAREHOUSE0 + SKU485: + - WAREHOUSE0 + SKU486: + - WAREHOUSE0 + SKU487: + - WAREHOUSE0 + SKU488: + - WAREHOUSE0 + SKU489: + - WAREHOUSE0 + SKU49: + - WAREHOUSE0 + SKU490: + - WAREHOUSE0 + SKU491: + - WAREHOUSE0 + SKU492: + - WAREHOUSE0 + SKU493: + - WAREHOUSE0 + SKU494: + - WAREHOUSE0 + SKU495: + - WAREHOUSE0 + SKU496: + - WAREHOUSE0 + SKU497: + - WAREHOUSE0 + SKU498: + - WAREHOUSE0 + SKU499: + - WAREHOUSE0 + SKU5: + - WAREHOUSE0 + SKU50: + - WAREHOUSE0 + SKU500: + - WAREHOUSE0 + SKU501: + - WAREHOUSE0 + SKU502: + - WAREHOUSE0 + SKU503: + - WAREHOUSE0 + SKU504: + - WAREHOUSE0 + SKU505: + - WAREHOUSE0 + SKU506: + - WAREHOUSE0 + SKU507: + - WAREHOUSE0 + SKU508: + - WAREHOUSE0 + SKU509: + - WAREHOUSE0 + SKU51: + - WAREHOUSE0 + SKU510: + - WAREHOUSE0 + SKU511: + - WAREHOUSE0 + SKU512: + - WAREHOUSE0 + SKU513: + - WAREHOUSE0 + SKU514: + - WAREHOUSE0 + SKU515: + - WAREHOUSE0 + SKU516: + - WAREHOUSE0 + SKU517: + - WAREHOUSE0 + SKU518: + - WAREHOUSE0 + SKU519: + - WAREHOUSE0 + SKU52: + - WAREHOUSE0 + SKU520: + - WAREHOUSE0 + SKU521: + - WAREHOUSE0 + SKU522: + - WAREHOUSE0 + SKU523: + - WAREHOUSE0 + SKU524: + - WAREHOUSE0 + SKU525: + - WAREHOUSE0 + SKU526: + - WAREHOUSE0 + SKU527: + - WAREHOUSE0 + SKU528: + - WAREHOUSE0 + SKU529: + - WAREHOUSE0 + SKU53: + - WAREHOUSE0 + SKU530: + - WAREHOUSE0 + SKU531: + - WAREHOUSE0 + SKU532: + - WAREHOUSE0 + SKU533: + - WAREHOUSE0 + SKU534: + - WAREHOUSE0 + SKU535: + - WAREHOUSE0 + SKU536: + - WAREHOUSE0 + SKU537: + - WAREHOUSE0 + SKU538: + - WAREHOUSE0 + SKU539: + - WAREHOUSE0 + SKU54: + - WAREHOUSE0 + SKU540: + - WAREHOUSE0 + SKU541: + - WAREHOUSE0 + SKU542: + - WAREHOUSE0 + SKU543: + - WAREHOUSE0 + SKU544: + - WAREHOUSE0 + SKU545: + - WAREHOUSE0 + SKU546: + - WAREHOUSE0 + SKU547: + - WAREHOUSE0 + SKU548: + - WAREHOUSE0 + SKU549: + - WAREHOUSE0 + SKU55: + - WAREHOUSE0 + SKU550: + - WAREHOUSE0 + SKU551: + - WAREHOUSE0 + SKU552: + - WAREHOUSE0 + SKU553: + - WAREHOUSE0 + SKU554: + - WAREHOUSE0 + SKU555: + - WAREHOUSE0 + SKU556: + - WAREHOUSE0 + SKU557: + - WAREHOUSE0 + SKU558: + - WAREHOUSE0 + SKU559: + - WAREHOUSE0 + SKU56: + - WAREHOUSE0 + SKU560: + - WAREHOUSE0 + SKU561: + - WAREHOUSE0 + SKU562: + - WAREHOUSE0 + SKU563: + - WAREHOUSE0 + SKU564: + - WAREHOUSE0 + SKU565: + - WAREHOUSE0 + SKU566: + - WAREHOUSE0 + SKU567: + - WAREHOUSE0 + SKU568: + - WAREHOUSE0 + SKU569: + - WAREHOUSE0 + SKU57: + - WAREHOUSE0 + SKU570: + - WAREHOUSE0 + SKU571: + - WAREHOUSE0 + SKU572: + - WAREHOUSE0 + SKU573: + - WAREHOUSE0 + SKU574: + - WAREHOUSE0 + SKU575: + - WAREHOUSE0 + SKU576: + - WAREHOUSE0 + SKU577: + - WAREHOUSE0 + SKU578: + - WAREHOUSE0 + SKU579: + - WAREHOUSE0 + SKU58: + - WAREHOUSE0 + SKU580: + - WAREHOUSE0 + SKU581: + - WAREHOUSE0 + SKU582: + - WAREHOUSE0 + SKU583: + - WAREHOUSE0 + SKU584: + - WAREHOUSE0 + SKU585: + - WAREHOUSE0 + SKU586: + - WAREHOUSE0 + SKU587: + - WAREHOUSE0 + SKU588: + - WAREHOUSE0 + SKU589: + - WAREHOUSE0 + SKU59: + - WAREHOUSE0 + SKU590: + - WAREHOUSE0 + SKU591: + - WAREHOUSE0 + SKU592: + - WAREHOUSE0 + SKU593: + - WAREHOUSE0 + SKU594: + - WAREHOUSE0 + SKU595: + - WAREHOUSE0 + SKU596: + - WAREHOUSE0 + SKU597: + - WAREHOUSE0 + SKU598: + - WAREHOUSE0 + SKU599: + - WAREHOUSE0 + SKU6: + - WAREHOUSE0 + SKU60: + - WAREHOUSE0 + SKU600: + - WAREHOUSE0 + SKU601: + - WAREHOUSE0 + SKU602: + - WAREHOUSE0 + SKU603: + - WAREHOUSE0 + SKU604: + - WAREHOUSE0 + SKU605: + - WAREHOUSE0 + SKU606: + - WAREHOUSE0 + SKU607: + - WAREHOUSE0 + SKU608: + - WAREHOUSE0 + SKU609: + - WAREHOUSE0 + SKU61: + - WAREHOUSE0 + SKU610: + - WAREHOUSE0 + SKU611: + - WAREHOUSE0 + SKU612: + - WAREHOUSE0 + SKU613: + - WAREHOUSE0 + SKU614: + - WAREHOUSE0 + SKU615: + - WAREHOUSE0 + SKU616: + - WAREHOUSE0 + SKU617: + - WAREHOUSE0 + SKU618: + - WAREHOUSE0 + SKU619: + - WAREHOUSE0 + SKU62: + - WAREHOUSE0 + SKU620: + - WAREHOUSE0 + SKU621: + - WAREHOUSE0 + SKU622: + - WAREHOUSE0 + SKU623: + - WAREHOUSE0 + SKU624: + - WAREHOUSE0 + SKU625: + - WAREHOUSE0 + SKU626: + - WAREHOUSE0 + SKU627: + - WAREHOUSE0 + SKU628: + - WAREHOUSE0 + SKU629: + - WAREHOUSE0 + SKU63: + - WAREHOUSE0 + SKU630: + - WAREHOUSE0 + SKU631: + - WAREHOUSE0 + SKU632: + - WAREHOUSE0 + SKU633: + - WAREHOUSE0 + SKU634: + - WAREHOUSE0 + SKU635: + - WAREHOUSE0 + SKU636: + - WAREHOUSE0 + SKU637: + - WAREHOUSE0 + SKU638: + - WAREHOUSE0 + SKU639: + - WAREHOUSE0 + SKU64: + - WAREHOUSE0 + SKU640: + - WAREHOUSE0 + SKU641: + - WAREHOUSE0 + SKU642: + - WAREHOUSE0 + SKU643: + - WAREHOUSE0 + SKU644: + - WAREHOUSE0 + SKU645: + - WAREHOUSE0 + SKU646: + - WAREHOUSE0 + SKU647: + - WAREHOUSE0 + SKU648: + - WAREHOUSE0 + SKU649: + - WAREHOUSE0 + SKU65: + - WAREHOUSE0 + SKU650: + - WAREHOUSE0 + SKU651: + - WAREHOUSE0 + SKU652: + - WAREHOUSE0 + SKU653: + - WAREHOUSE0 + SKU654: + - WAREHOUSE0 + SKU655: + - WAREHOUSE0 + SKU656: + - WAREHOUSE0 + SKU657: + - WAREHOUSE0 + SKU658: + - WAREHOUSE0 + SKU659: + - WAREHOUSE0 + SKU66: + - WAREHOUSE0 + SKU660: + - WAREHOUSE0 + SKU661: + - WAREHOUSE0 + SKU662: + - WAREHOUSE0 + SKU663: + - WAREHOUSE0 + SKU664: + - WAREHOUSE0 + SKU665: + - WAREHOUSE0 + SKU666: + - WAREHOUSE0 + SKU667: + - WAREHOUSE0 + SKU668: + - WAREHOUSE0 + SKU669: + - WAREHOUSE0 + SKU67: + - WAREHOUSE0 + SKU670: + - WAREHOUSE0 + SKU671: + - WAREHOUSE0 + SKU672: + - WAREHOUSE0 + SKU673: + - WAREHOUSE0 + SKU674: + - WAREHOUSE0 + SKU675: + - WAREHOUSE0 + SKU676: + - WAREHOUSE0 + SKU677: + - WAREHOUSE0 + SKU678: + - WAREHOUSE0 + SKU679: + - WAREHOUSE0 + SKU68: + - WAREHOUSE0 + SKU680: + - WAREHOUSE0 + SKU681: + - WAREHOUSE0 + SKU682: + - WAREHOUSE0 + SKU683: + - WAREHOUSE0 + SKU684: + - WAREHOUSE0 + SKU685: + - WAREHOUSE0 + SKU686: + - WAREHOUSE0 + SKU687: + - WAREHOUSE0 + SKU688: + - WAREHOUSE0 + SKU689: + - WAREHOUSE0 + SKU69: + - WAREHOUSE0 + SKU690: + - WAREHOUSE0 + SKU691: + - WAREHOUSE0 + SKU692: + - WAREHOUSE0 + SKU693: + - WAREHOUSE0 + SKU694: + - WAREHOUSE0 + SKU695: + - WAREHOUSE0 + SKU696: + - WAREHOUSE0 + SKU697: + - WAREHOUSE0 + SKU698: + - WAREHOUSE0 + SKU699: + - WAREHOUSE0 + SKU7: + - WAREHOUSE0 + SKU70: + - WAREHOUSE0 + SKU700: + - WAREHOUSE0 + SKU701: + - WAREHOUSE0 + SKU702: + - WAREHOUSE0 + SKU703: + - WAREHOUSE0 + SKU704: + - WAREHOUSE0 + SKU705: + - WAREHOUSE0 + SKU706: + - WAREHOUSE0 + SKU707: + - WAREHOUSE0 + SKU708: + - WAREHOUSE0 + SKU709: + - WAREHOUSE0 + SKU71: + - WAREHOUSE0 + SKU710: + - WAREHOUSE0 + SKU711: + - WAREHOUSE0 + SKU712: + - WAREHOUSE0 + SKU713: + - WAREHOUSE0 + SKU714: + - WAREHOUSE0 + SKU715: + - WAREHOUSE0 + SKU716: + - WAREHOUSE0 + SKU717: + - WAREHOUSE0 + SKU718: + - WAREHOUSE0 + SKU719: + - WAREHOUSE0 + SKU72: + - WAREHOUSE0 + SKU720: + - WAREHOUSE0 + SKU721: + - WAREHOUSE0 + SKU722: + - WAREHOUSE0 + SKU723: + - WAREHOUSE0 + SKU724: + - WAREHOUSE0 + SKU725: + - WAREHOUSE0 + SKU726: + - WAREHOUSE0 + SKU727: + - WAREHOUSE0 + SKU728: + - WAREHOUSE0 + SKU729: + - WAREHOUSE0 + SKU73: + - WAREHOUSE0 + SKU730: + - WAREHOUSE0 + SKU731: + - WAREHOUSE0 + SKU732: + - WAREHOUSE0 + SKU733: + - WAREHOUSE0 + SKU734: + - WAREHOUSE0 + SKU735: + - WAREHOUSE0 + SKU736: + - WAREHOUSE0 + SKU737: + - WAREHOUSE0 + SKU738: + - WAREHOUSE0 + SKU739: + - WAREHOUSE0 + SKU74: + - WAREHOUSE0 + SKU740: + - WAREHOUSE0 + SKU741: + - WAREHOUSE0 + SKU742: + - WAREHOUSE0 + SKU743: + - WAREHOUSE0 + SKU744: + - WAREHOUSE0 + SKU745: + - WAREHOUSE0 + SKU746: + - WAREHOUSE0 + SKU747: + - WAREHOUSE0 + SKU748: + - WAREHOUSE0 + SKU749: + - WAREHOUSE0 + SKU75: + - WAREHOUSE0 + SKU750: + - WAREHOUSE0 + SKU751: + - WAREHOUSE0 + SKU752: + - WAREHOUSE0 + SKU753: + - WAREHOUSE0 + SKU754: + - WAREHOUSE0 + SKU755: + - WAREHOUSE0 + SKU756: + - WAREHOUSE0 + SKU757: + - WAREHOUSE0 + SKU758: + - WAREHOUSE0 + SKU759: + - WAREHOUSE0 + SKU76: + - WAREHOUSE0 + SKU760: + - WAREHOUSE0 + SKU761: + - WAREHOUSE0 + SKU762: + - WAREHOUSE0 + SKU763: + - WAREHOUSE0 + SKU764: + - WAREHOUSE0 + SKU765: + - WAREHOUSE0 + SKU766: + - WAREHOUSE0 + SKU767: + - WAREHOUSE0 + SKU768: + - WAREHOUSE0 + SKU769: + - WAREHOUSE0 + SKU77: + - WAREHOUSE0 + SKU770: + - WAREHOUSE0 + SKU771: + - WAREHOUSE0 + SKU772: + - WAREHOUSE0 + SKU773: + - WAREHOUSE0 + SKU774: + - WAREHOUSE0 + SKU775: + - WAREHOUSE0 + SKU776: + - WAREHOUSE0 + SKU777: + - WAREHOUSE0 + SKU778: + - WAREHOUSE0 + SKU779: + - WAREHOUSE0 + SKU78: + - WAREHOUSE0 + SKU780: + - WAREHOUSE0 + SKU781: + - WAREHOUSE0 + SKU782: + - WAREHOUSE0 + SKU783: + - WAREHOUSE0 + SKU784: + - WAREHOUSE0 + SKU785: + - WAREHOUSE0 + SKU786: + - WAREHOUSE0 + SKU787: + - WAREHOUSE0 + SKU788: + - WAREHOUSE0 + SKU789: + - WAREHOUSE0 + SKU79: + - WAREHOUSE0 + SKU790: + - WAREHOUSE0 + SKU791: + - WAREHOUSE0 + SKU792: + - WAREHOUSE0 + SKU793: + - WAREHOUSE0 + SKU794: + - WAREHOUSE0 + SKU795: + - WAREHOUSE0 + SKU796: + - WAREHOUSE0 + SKU797: + - WAREHOUSE0 + SKU798: + - WAREHOUSE0 + SKU799: + - WAREHOUSE0 + SKU8: + - WAREHOUSE0 + SKU80: + - WAREHOUSE0 + SKU800: + - WAREHOUSE0 + SKU801: + - WAREHOUSE0 + SKU802: + - WAREHOUSE0 + SKU803: + - WAREHOUSE0 + SKU804: + - WAREHOUSE0 + SKU805: + - WAREHOUSE0 + SKU806: + - WAREHOUSE0 + SKU807: + - WAREHOUSE0 + SKU808: + - WAREHOUSE0 + SKU809: + - WAREHOUSE0 + SKU81: + - WAREHOUSE0 + SKU810: + - WAREHOUSE0 + SKU811: + - WAREHOUSE0 + SKU812: + - WAREHOUSE0 + SKU813: + - WAREHOUSE0 + SKU814: + - WAREHOUSE0 + SKU815: + - WAREHOUSE0 + SKU816: + - WAREHOUSE0 + SKU817: + - WAREHOUSE0 + SKU818: + - WAREHOUSE0 + SKU819: + - WAREHOUSE0 + SKU82: + - WAREHOUSE0 + SKU820: + - WAREHOUSE0 + SKU821: + - WAREHOUSE0 + SKU822: + - WAREHOUSE0 + SKU823: + - WAREHOUSE0 + SKU824: + - WAREHOUSE0 + SKU825: + - WAREHOUSE0 + SKU826: + - WAREHOUSE0 + SKU827: + - WAREHOUSE0 + SKU828: + - WAREHOUSE0 + SKU829: + - WAREHOUSE0 + SKU83: + - WAREHOUSE0 + SKU830: + - WAREHOUSE0 + SKU831: + - WAREHOUSE0 + SKU832: + - WAREHOUSE0 + SKU833: + - WAREHOUSE0 + SKU834: + - WAREHOUSE0 + SKU835: + - WAREHOUSE0 + SKU836: + - WAREHOUSE0 + SKU837: + - WAREHOUSE0 + SKU838: + - WAREHOUSE0 + SKU839: + - WAREHOUSE0 + SKU84: + - WAREHOUSE0 + SKU840: + - WAREHOUSE0 + SKU841: + - WAREHOUSE0 + SKU842: + - WAREHOUSE0 + SKU843: + - WAREHOUSE0 + SKU844: + - WAREHOUSE0 + SKU845: + - WAREHOUSE0 + SKU846: + - WAREHOUSE0 + SKU847: + - WAREHOUSE0 + SKU848: + - WAREHOUSE0 + SKU849: + - WAREHOUSE0 + SKU85: + - WAREHOUSE0 + SKU850: + - WAREHOUSE0 + SKU851: + - WAREHOUSE0 + SKU852: + - WAREHOUSE0 + SKU853: + - WAREHOUSE0 + SKU854: + - WAREHOUSE0 + SKU855: + - WAREHOUSE0 + SKU856: + - WAREHOUSE0 + SKU857: + - WAREHOUSE0 + SKU858: + - WAREHOUSE0 + SKU859: + - WAREHOUSE0 + SKU86: + - WAREHOUSE0 + SKU860: + - WAREHOUSE0 + SKU861: + - WAREHOUSE0 + SKU862: + - WAREHOUSE0 + SKU863: + - WAREHOUSE0 + SKU864: + - WAREHOUSE0 + SKU865: + - WAREHOUSE0 + SKU866: + - WAREHOUSE0 + SKU867: + - WAREHOUSE0 + SKU868: + - WAREHOUSE0 + SKU869: + - WAREHOUSE0 + SKU87: + - WAREHOUSE0 + SKU870: + - WAREHOUSE0 + SKU871: + - WAREHOUSE0 + SKU872: + - WAREHOUSE0 + SKU873: + - WAREHOUSE0 + SKU874: + - WAREHOUSE0 + SKU875: + - WAREHOUSE0 + SKU876: + - WAREHOUSE0 + SKU877: + - WAREHOUSE0 + SKU878: + - WAREHOUSE0 + SKU879: + - WAREHOUSE0 + SKU88: + - WAREHOUSE0 + SKU880: + - WAREHOUSE0 + SKU881: + - WAREHOUSE0 + SKU882: + - WAREHOUSE0 + SKU883: + - WAREHOUSE0 + SKU884: + - WAREHOUSE0 + SKU885: + - WAREHOUSE0 + SKU886: + - WAREHOUSE0 + SKU887: + - WAREHOUSE0 + SKU888: + - WAREHOUSE0 + SKU889: + - WAREHOUSE0 + SKU89: + - WAREHOUSE0 + SKU890: + - WAREHOUSE0 + SKU891: + - WAREHOUSE0 + SKU892: + - WAREHOUSE0 + SKU893: + - WAREHOUSE0 + SKU894: + - WAREHOUSE0 + SKU895: + - WAREHOUSE0 + SKU896: + - WAREHOUSE0 + SKU897: + - WAREHOUSE0 + SKU898: + - WAREHOUSE0 + SKU899: + - WAREHOUSE0 + SKU9: + - WAREHOUSE0 + SKU90: + - WAREHOUSE0 + SKU900: + - WAREHOUSE0 + SKU901: + - WAREHOUSE0 + SKU902: + - WAREHOUSE0 + SKU903: + - WAREHOUSE0 + SKU904: + - WAREHOUSE0 + SKU905: + - WAREHOUSE0 + SKU906: + - WAREHOUSE0 + SKU907: + - WAREHOUSE0 + SKU908: + - WAREHOUSE0 + SKU909: + - WAREHOUSE0 + SKU91: + - WAREHOUSE0 + SKU910: + - WAREHOUSE0 + SKU911: + - WAREHOUSE0 + SKU912: + - WAREHOUSE0 + SKU913: + - WAREHOUSE0 + SKU914: + - WAREHOUSE0 + SKU915: + - WAREHOUSE0 + SKU916: + - WAREHOUSE0 + SKU917: + - WAREHOUSE0 + SKU918: + - WAREHOUSE0 + SKU919: + - WAREHOUSE0 + SKU92: + - WAREHOUSE0 + SKU920: + - WAREHOUSE0 + SKU921: + - WAREHOUSE0 + SKU922: + - WAREHOUSE0 + SKU923: + - WAREHOUSE0 + SKU924: + - WAREHOUSE0 + SKU925: + - WAREHOUSE0 + SKU926: + - WAREHOUSE0 + SKU927: + - WAREHOUSE0 + SKU928: + - WAREHOUSE0 + SKU929: + - WAREHOUSE0 + SKU93: + - WAREHOUSE0 + SKU930: + - WAREHOUSE0 + SKU931: + - WAREHOUSE0 + SKU932: + - WAREHOUSE0 + SKU933: + - WAREHOUSE0 + SKU934: + - WAREHOUSE0 + SKU935: + - WAREHOUSE0 + SKU936: + - WAREHOUSE0 + SKU937: + - WAREHOUSE0 + SKU938: + - WAREHOUSE0 + SKU939: + - WAREHOUSE0 + SKU94: + - WAREHOUSE0 + SKU940: + - WAREHOUSE0 + SKU941: + - WAREHOUSE0 + SKU942: + - WAREHOUSE0 + SKU943: + - WAREHOUSE0 + SKU944: + - WAREHOUSE0 + SKU945: + - WAREHOUSE0 + SKU946: + - WAREHOUSE0 + SKU947: + - WAREHOUSE0 + SKU948: + - WAREHOUSE0 + SKU949: + - WAREHOUSE0 + SKU95: + - WAREHOUSE0 + SKU950: + - WAREHOUSE0 + SKU951: + - WAREHOUSE0 + SKU952: + - WAREHOUSE0 + SKU953: + - WAREHOUSE0 + SKU954: + - WAREHOUSE0 + SKU955: + - WAREHOUSE0 + SKU956: + - WAREHOUSE0 + SKU957: + - WAREHOUSE0 + SKU958: + - WAREHOUSE0 + SKU959: + - WAREHOUSE0 + SKU96: + - WAREHOUSE0 + SKU960: + - WAREHOUSE0 + SKU961: + - WAREHOUSE0 + SKU962: + - WAREHOUSE0 + SKU963: + - WAREHOUSE0 + SKU964: + - WAREHOUSE0 + SKU965: + - WAREHOUSE0 + SKU966: + - WAREHOUSE0 + SKU967: + - WAREHOUSE0 + SKU968: + - WAREHOUSE0 + SKU969: + - WAREHOUSE0 + SKU97: + - WAREHOUSE0 + SKU970: + - WAREHOUSE0 + SKU971: + - WAREHOUSE0 + SKU972: + - WAREHOUSE0 + SKU973: + - WAREHOUSE0 + SKU974: + - WAREHOUSE0 + SKU975: + - WAREHOUSE0 + SKU976: + - WAREHOUSE0 + SKU977: + - WAREHOUSE0 + SKU978: + - WAREHOUSE0 + SKU979: + - WAREHOUSE0 + SKU98: + - WAREHOUSE0 + SKU980: + - WAREHOUSE0 + SKU981: + - WAREHOUSE0 + SKU982: + - WAREHOUSE0 + SKU983: + - WAREHOUSE0 + SKU984: + - WAREHOUSE0 + SKU985: + - WAREHOUSE0 + SKU986: + - WAREHOUSE0 + SKU987: + - WAREHOUSE0 + SKU988: + - WAREHOUSE0 + SKU989: + - WAREHOUSE0 + SKU99: + - WAREHOUSE0 + SKU990: + - WAREHOUSE0 + SKU991: + - WAREHOUSE0 + SKU992: + - WAREHOUSE0 + SKU993: + - WAREHOUSE0 + SKU994: + - WAREHOUSE0 + SKU995: + - WAREHOUSE0 + SKU996: + - WAREHOUSE0 + SKU997: + - WAREHOUSE0 + SKU998: + - WAREHOUSE0 + SKU999: + - WAREHOUSE0 + WAREHOUSE0: + SKU0: + - SUPPLIER0 + SKU1: + - SUPPLIER0 + SKU10: + - SUPPLIER0 + SKU100: + - SUPPLIER0 + SKU101: + - SUPPLIER0 + SKU102: + - SUPPLIER0 + SKU103: + - SUPPLIER0 + SKU104: + - SUPPLIER0 + SKU105: + - SUPPLIER0 + SKU106: + - SUPPLIER0 + SKU107: + - SUPPLIER0 + SKU108: + - SUPPLIER0 + SKU109: + - SUPPLIER0 + SKU11: + - SUPPLIER0 + SKU110: + - SUPPLIER0 + SKU111: + - SUPPLIER0 + SKU112: + - SUPPLIER0 + SKU113: + - SUPPLIER0 + SKU114: + - SUPPLIER0 + SKU115: + - SUPPLIER0 + SKU116: + - SUPPLIER0 + SKU117: + - SUPPLIER0 + SKU118: + - SUPPLIER0 + SKU119: + - SUPPLIER0 + SKU12: + - SUPPLIER0 + SKU120: + - SUPPLIER0 + SKU121: + - SUPPLIER0 + SKU122: + - SUPPLIER0 + SKU123: + - SUPPLIER0 + SKU124: + - SUPPLIER0 + SKU125: + - SUPPLIER0 + SKU126: + - SUPPLIER0 + SKU127: + - SUPPLIER0 + SKU128: + - SUPPLIER0 + SKU129: + - SUPPLIER0 + SKU13: + - SUPPLIER0 + SKU130: + - SUPPLIER0 + SKU131: + - SUPPLIER0 + SKU132: + - SUPPLIER0 + SKU133: + - SUPPLIER0 + SKU134: + - SUPPLIER0 + SKU135: + - SUPPLIER0 + SKU136: + - SUPPLIER0 + SKU137: + - SUPPLIER0 + SKU138: + - SUPPLIER0 + SKU139: + - SUPPLIER0 + SKU14: + - SUPPLIER0 + SKU140: + - SUPPLIER0 + SKU141: + - SUPPLIER0 + SKU142: + - SUPPLIER0 + SKU143: + - SUPPLIER0 + SKU144: + - SUPPLIER0 + SKU145: + - SUPPLIER0 + SKU146: + - SUPPLIER0 + SKU147: + - SUPPLIER0 + SKU148: + - SUPPLIER0 + SKU149: + - SUPPLIER0 + SKU15: + - SUPPLIER0 + SKU150: + - SUPPLIER0 + SKU151: + - SUPPLIER0 + SKU152: + - SUPPLIER0 + SKU153: + - SUPPLIER0 + SKU154: + - SUPPLIER0 + SKU155: + - SUPPLIER0 + SKU156: + - SUPPLIER0 + SKU157: + - SUPPLIER0 + SKU158: + - SUPPLIER0 + SKU159: + - SUPPLIER0 + SKU16: + - SUPPLIER0 + SKU160: + - SUPPLIER0 + SKU161: + - SUPPLIER0 + SKU162: + - SUPPLIER0 + SKU163: + - SUPPLIER0 + SKU164: + - SUPPLIER0 + SKU165: + - SUPPLIER0 + SKU166: + - SUPPLIER0 + SKU167: + - SUPPLIER0 + SKU168: + - SUPPLIER0 + SKU169: + - SUPPLIER0 + SKU17: + - SUPPLIER0 + SKU170: + - SUPPLIER0 + SKU171: + - SUPPLIER0 + SKU172: + - SUPPLIER0 + SKU173: + - SUPPLIER0 + SKU174: + - SUPPLIER0 + SKU175: + - SUPPLIER0 + SKU176: + - SUPPLIER0 + SKU177: + - SUPPLIER0 + SKU178: + - SUPPLIER0 + SKU179: + - SUPPLIER0 + SKU18: + - SUPPLIER0 + SKU180: + - SUPPLIER0 + SKU181: + - SUPPLIER0 + SKU182: + - SUPPLIER0 + SKU183: + - SUPPLIER0 + SKU184: + - SUPPLIER0 + SKU185: + - SUPPLIER0 + SKU186: + - SUPPLIER0 + SKU187: + - SUPPLIER0 + SKU188: + - SUPPLIER0 + SKU189: + - SUPPLIER0 + SKU19: + - SUPPLIER0 + SKU190: + - SUPPLIER0 + SKU191: + - SUPPLIER0 + SKU192: + - SUPPLIER0 + SKU193: + - SUPPLIER0 + SKU194: + - SUPPLIER0 + SKU195: + - SUPPLIER0 + SKU196: + - SUPPLIER0 + SKU197: + - SUPPLIER0 + SKU198: + - SUPPLIER0 + SKU199: + - SUPPLIER0 + SKU2: + - SUPPLIER0 + SKU20: + - SUPPLIER0 + SKU200: + - SUPPLIER0 + SKU201: + - SUPPLIER0 + SKU202: + - SUPPLIER0 + SKU203: + - SUPPLIER0 + SKU204: + - SUPPLIER0 + SKU205: + - SUPPLIER0 + SKU206: + - SUPPLIER0 + SKU207: + - SUPPLIER0 + SKU208: + - SUPPLIER0 + SKU209: + - SUPPLIER0 + SKU21: + - SUPPLIER0 + SKU210: + - SUPPLIER0 + SKU211: + - SUPPLIER0 + SKU212: + - SUPPLIER0 + SKU213: + - SUPPLIER0 + SKU214: + - SUPPLIER0 + SKU215: + - SUPPLIER0 + SKU216: + - SUPPLIER0 + SKU217: + - SUPPLIER0 + SKU218: + - SUPPLIER0 + SKU219: + - SUPPLIER0 + SKU22: + - SUPPLIER0 + SKU220: + - SUPPLIER0 + SKU221: + - SUPPLIER0 + SKU222: + - SUPPLIER0 + SKU223: + - SUPPLIER0 + SKU224: + - SUPPLIER0 + SKU225: + - SUPPLIER0 + SKU226: + - SUPPLIER0 + SKU227: + - SUPPLIER0 + SKU228: + - SUPPLIER0 + SKU229: + - SUPPLIER0 + SKU23: + - SUPPLIER0 + SKU230: + - SUPPLIER0 + SKU231: + - SUPPLIER0 + SKU232: + - SUPPLIER0 + SKU233: + - SUPPLIER0 + SKU234: + - SUPPLIER0 + SKU235: + - SUPPLIER0 + SKU236: + - SUPPLIER0 + SKU237: + - SUPPLIER0 + SKU238: + - SUPPLIER0 + SKU239: + - SUPPLIER0 + SKU24: + - SUPPLIER0 + SKU240: + - SUPPLIER0 + SKU241: + - SUPPLIER0 + SKU242: + - SUPPLIER0 + SKU243: + - SUPPLIER0 + SKU244: + - SUPPLIER0 + SKU245: + - SUPPLIER0 + SKU246: + - SUPPLIER0 + SKU247: + - SUPPLIER0 + SKU248: + - SUPPLIER0 + SKU249: + - SUPPLIER0 + SKU25: + - SUPPLIER0 + SKU250: + - SUPPLIER0 + SKU251: + - SUPPLIER0 + SKU252: + - SUPPLIER0 + SKU253: + - SUPPLIER0 + SKU254: + - SUPPLIER0 + SKU255: + - SUPPLIER0 + SKU256: + - SUPPLIER0 + SKU257: + - SUPPLIER0 + SKU258: + - SUPPLIER0 + SKU259: + - SUPPLIER0 + SKU26: + - SUPPLIER0 + SKU260: + - SUPPLIER0 + SKU261: + - SUPPLIER0 + SKU262: + - SUPPLIER0 + SKU263: + - SUPPLIER0 + SKU264: + - SUPPLIER0 + SKU265: + - SUPPLIER0 + SKU266: + - SUPPLIER0 + SKU267: + - SUPPLIER0 + SKU268: + - SUPPLIER0 + SKU269: + - SUPPLIER0 + SKU27: + - SUPPLIER0 + SKU270: + - SUPPLIER0 + SKU271: + - SUPPLIER0 + SKU272: + - SUPPLIER0 + SKU273: + - SUPPLIER0 + SKU274: + - SUPPLIER0 + SKU275: + - SUPPLIER0 + SKU276: + - SUPPLIER0 + SKU277: + - SUPPLIER0 + SKU278: + - SUPPLIER0 + SKU279: + - SUPPLIER0 + SKU28: + - SUPPLIER0 + SKU280: + - SUPPLIER0 + SKU281: + - SUPPLIER0 + SKU282: + - SUPPLIER0 + SKU283: + - SUPPLIER0 + SKU284: + - SUPPLIER0 + SKU285: + - SUPPLIER0 + SKU286: + - SUPPLIER0 + SKU287: + - SUPPLIER0 + SKU288: + - SUPPLIER0 + SKU289: + - SUPPLIER0 + SKU29: + - SUPPLIER0 + SKU290: + - SUPPLIER0 + SKU291: + - SUPPLIER0 + SKU292: + - SUPPLIER0 + SKU293: + - SUPPLIER0 + SKU294: + - SUPPLIER0 + SKU295: + - SUPPLIER0 + SKU296: + - SUPPLIER0 + SKU297: + - SUPPLIER0 + SKU298: + - SUPPLIER0 + SKU299: + - SUPPLIER0 + SKU3: + - SUPPLIER0 + SKU30: + - SUPPLIER0 + SKU300: + - SUPPLIER0 + SKU301: + - SUPPLIER0 + SKU302: + - SUPPLIER0 + SKU303: + - SUPPLIER0 + SKU304: + - SUPPLIER0 + SKU305: + - SUPPLIER0 + SKU306: + - SUPPLIER0 + SKU307: + - SUPPLIER0 + SKU308: + - SUPPLIER0 + SKU309: + - SUPPLIER0 + SKU31: + - SUPPLIER0 + SKU310: + - SUPPLIER0 + SKU311: + - SUPPLIER0 + SKU312: + - SUPPLIER0 + SKU313: + - SUPPLIER0 + SKU314: + - SUPPLIER0 + SKU315: + - SUPPLIER0 + SKU316: + - SUPPLIER0 + SKU317: + - SUPPLIER0 + SKU318: + - SUPPLIER0 + SKU319: + - SUPPLIER0 + SKU32: + - SUPPLIER0 + SKU320: + - SUPPLIER0 + SKU321: + - SUPPLIER0 + SKU322: + - SUPPLIER0 + SKU323: + - SUPPLIER0 + SKU324: + - SUPPLIER0 + SKU325: + - SUPPLIER0 + SKU326: + - SUPPLIER0 + SKU327: + - SUPPLIER0 + SKU328: + - SUPPLIER0 + SKU329: + - SUPPLIER0 + SKU33: + - SUPPLIER0 + SKU330: + - SUPPLIER0 + SKU331: + - SUPPLIER0 + SKU332: + - SUPPLIER0 + SKU333: + - SUPPLIER0 + SKU334: + - SUPPLIER0 + SKU335: + - SUPPLIER0 + SKU336: + - SUPPLIER0 + SKU337: + - SUPPLIER0 + SKU338: + - SUPPLIER0 + SKU339: + - SUPPLIER0 + SKU34: + - SUPPLIER0 + SKU340: + - SUPPLIER0 + SKU341: + - SUPPLIER0 + SKU342: + - SUPPLIER0 + SKU343: + - SUPPLIER0 + SKU344: + - SUPPLIER0 + SKU345: + - SUPPLIER0 + SKU346: + - SUPPLIER0 + SKU347: + - SUPPLIER0 + SKU348: + - SUPPLIER0 + SKU349: + - SUPPLIER0 + SKU35: + - SUPPLIER0 + SKU350: + - SUPPLIER0 + SKU351: + - SUPPLIER0 + SKU352: + - SUPPLIER0 + SKU353: + - SUPPLIER0 + SKU354: + - SUPPLIER0 + SKU355: + - SUPPLIER0 + SKU356: + - SUPPLIER0 + SKU357: + - SUPPLIER0 + SKU358: + - SUPPLIER0 + SKU359: + - SUPPLIER0 + SKU36: + - SUPPLIER0 + SKU360: + - SUPPLIER0 + SKU361: + - SUPPLIER0 + SKU362: + - SUPPLIER0 + SKU363: + - SUPPLIER0 + SKU364: + - SUPPLIER0 + SKU365: + - SUPPLIER0 + SKU366: + - SUPPLIER0 + SKU367: + - SUPPLIER0 + SKU368: + - SUPPLIER0 + SKU369: + - SUPPLIER0 + SKU37: + - SUPPLIER0 + SKU370: + - SUPPLIER0 + SKU371: + - SUPPLIER0 + SKU372: + - SUPPLIER0 + SKU373: + - SUPPLIER0 + SKU374: + - SUPPLIER0 + SKU375: + - SUPPLIER0 + SKU376: + - SUPPLIER0 + SKU377: + - SUPPLIER0 + SKU378: + - SUPPLIER0 + SKU379: + - SUPPLIER0 + SKU38: + - SUPPLIER0 + SKU380: + - SUPPLIER0 + SKU381: + - SUPPLIER0 + SKU382: + - SUPPLIER0 + SKU383: + - SUPPLIER0 + SKU384: + - SUPPLIER0 + SKU385: + - SUPPLIER0 + SKU386: + - SUPPLIER0 + SKU387: + - SUPPLIER0 + SKU388: + - SUPPLIER0 + SKU389: + - SUPPLIER0 + SKU39: + - SUPPLIER0 + SKU390: + - SUPPLIER0 + SKU391: + - SUPPLIER0 + SKU392: + - SUPPLIER0 + SKU393: + - SUPPLIER0 + SKU394: + - SUPPLIER0 + SKU395: + - SUPPLIER0 + SKU396: + - SUPPLIER0 + SKU397: + - SUPPLIER0 + SKU398: + - SUPPLIER0 + SKU399: + - SUPPLIER0 + SKU4: + - SUPPLIER0 + SKU40: + - SUPPLIER0 + SKU400: + - SUPPLIER0 + SKU401: + - SUPPLIER0 + SKU402: + - SUPPLIER0 + SKU403: + - SUPPLIER0 + SKU404: + - SUPPLIER0 + SKU405: + - SUPPLIER0 + SKU406: + - SUPPLIER0 + SKU407: + - SUPPLIER0 + SKU408: + - SUPPLIER0 + SKU409: + - SUPPLIER0 + SKU41: + - SUPPLIER0 + SKU410: + - SUPPLIER0 + SKU411: + - SUPPLIER0 + SKU412: + - SUPPLIER0 + SKU413: + - SUPPLIER0 + SKU414: + - SUPPLIER0 + SKU415: + - SUPPLIER0 + SKU416: + - SUPPLIER0 + SKU417: + - SUPPLIER0 + SKU418: + - SUPPLIER0 + SKU419: + - SUPPLIER0 + SKU42: + - SUPPLIER0 + SKU420: + - SUPPLIER0 + SKU421: + - SUPPLIER0 + SKU422: + - SUPPLIER0 + SKU423: + - SUPPLIER0 + SKU424: + - SUPPLIER0 + SKU425: + - SUPPLIER0 + SKU426: + - SUPPLIER0 + SKU427: + - SUPPLIER0 + SKU428: + - SUPPLIER0 + SKU429: + - SUPPLIER0 + SKU43: + - SUPPLIER0 + SKU430: + - SUPPLIER0 + SKU431: + - SUPPLIER0 + SKU432: + - SUPPLIER0 + SKU433: + - SUPPLIER0 + SKU434: + - SUPPLIER0 + SKU435: + - SUPPLIER0 + SKU436: + - SUPPLIER0 + SKU437: + - SUPPLIER0 + SKU438: + - SUPPLIER0 + SKU439: + - SUPPLIER0 + SKU44: + - SUPPLIER0 + SKU440: + - SUPPLIER0 + SKU441: + - SUPPLIER0 + SKU442: + - SUPPLIER0 + SKU443: + - SUPPLIER0 + SKU444: + - SUPPLIER0 + SKU445: + - SUPPLIER0 + SKU446: + - SUPPLIER0 + SKU447: + - SUPPLIER0 + SKU448: + - SUPPLIER0 + SKU449: + - SUPPLIER0 + SKU45: + - SUPPLIER0 + SKU450: + - SUPPLIER0 + SKU451: + - SUPPLIER0 + SKU452: + - SUPPLIER0 + SKU453: + - SUPPLIER0 + SKU454: + - SUPPLIER0 + SKU455: + - SUPPLIER0 + SKU456: + - SUPPLIER0 + SKU457: + - SUPPLIER0 + SKU458: + - SUPPLIER0 + SKU459: + - SUPPLIER0 + SKU46: + - SUPPLIER0 + SKU460: + - SUPPLIER0 + SKU461: + - SUPPLIER0 + SKU462: + - SUPPLIER0 + SKU463: + - SUPPLIER0 + SKU464: + - SUPPLIER0 + SKU465: + - SUPPLIER0 + SKU466: + - SUPPLIER0 + SKU467: + - SUPPLIER0 + SKU468: + - SUPPLIER0 + SKU469: + - SUPPLIER0 + SKU47: + - SUPPLIER0 + SKU470: + - SUPPLIER0 + SKU471: + - SUPPLIER0 + SKU472: + - SUPPLIER0 + SKU473: + - SUPPLIER0 + SKU474: + - SUPPLIER0 + SKU475: + - SUPPLIER0 + SKU476: + - SUPPLIER0 + SKU477: + - SUPPLIER0 + SKU478: + - SUPPLIER0 + SKU479: + - SUPPLIER0 + SKU48: + - SUPPLIER0 + SKU480: + - SUPPLIER0 + SKU481: + - SUPPLIER0 + SKU482: + - SUPPLIER0 + SKU483: + - SUPPLIER0 + SKU484: + - SUPPLIER0 + SKU485: + - SUPPLIER0 + SKU486: + - SUPPLIER0 + SKU487: + - SUPPLIER0 + SKU488: + - SUPPLIER0 + SKU489: + - SUPPLIER0 + SKU49: + - SUPPLIER0 + SKU490: + - SUPPLIER0 + SKU491: + - SUPPLIER0 + SKU492: + - SUPPLIER0 + SKU493: + - SUPPLIER0 + SKU494: + - SUPPLIER0 + SKU495: + - SUPPLIER0 + SKU496: + - SUPPLIER0 + SKU497: + - SUPPLIER0 + SKU498: + - SUPPLIER0 + SKU499: + - SUPPLIER0 + SKU5: + - SUPPLIER0 + SKU50: + - SUPPLIER0 + SKU500: + - SUPPLIER0 + SKU501: + - SUPPLIER0 + SKU502: + - SUPPLIER0 + SKU503: + - SUPPLIER0 + SKU504: + - SUPPLIER0 + SKU505: + - SUPPLIER0 + SKU506: + - SUPPLIER0 + SKU507: + - SUPPLIER0 + SKU508: + - SUPPLIER0 + SKU509: + - SUPPLIER0 + SKU51: + - SUPPLIER0 + SKU510: + - SUPPLIER0 + SKU511: + - SUPPLIER0 + SKU512: + - SUPPLIER0 + SKU513: + - SUPPLIER0 + SKU514: + - SUPPLIER0 + SKU515: + - SUPPLIER0 + SKU516: + - SUPPLIER0 + SKU517: + - SUPPLIER0 + SKU518: + - SUPPLIER0 + SKU519: + - SUPPLIER0 + SKU52: + - SUPPLIER0 + SKU520: + - SUPPLIER0 + SKU521: + - SUPPLIER0 + SKU522: + - SUPPLIER0 + SKU523: + - SUPPLIER0 + SKU524: + - SUPPLIER0 + SKU525: + - SUPPLIER0 + SKU526: + - SUPPLIER0 + SKU527: + - SUPPLIER0 + SKU528: + - SUPPLIER0 + SKU529: + - SUPPLIER0 + SKU53: + - SUPPLIER0 + SKU530: + - SUPPLIER0 + SKU531: + - SUPPLIER0 + SKU532: + - SUPPLIER0 + SKU533: + - SUPPLIER0 + SKU534: + - SUPPLIER0 + SKU535: + - SUPPLIER0 + SKU536: + - SUPPLIER0 + SKU537: + - SUPPLIER0 + SKU538: + - SUPPLIER0 + SKU539: + - SUPPLIER0 + SKU54: + - SUPPLIER0 + SKU540: + - SUPPLIER0 + SKU541: + - SUPPLIER0 + SKU542: + - SUPPLIER0 + SKU543: + - SUPPLIER0 + SKU544: + - SUPPLIER0 + SKU545: + - SUPPLIER0 + SKU546: + - SUPPLIER0 + SKU547: + - SUPPLIER0 + SKU548: + - SUPPLIER0 + SKU549: + - SUPPLIER0 + SKU55: + - SUPPLIER0 + SKU550: + - SUPPLIER0 + SKU551: + - SUPPLIER0 + SKU552: + - SUPPLIER0 + SKU553: + - SUPPLIER0 + SKU554: + - SUPPLIER0 + SKU555: + - SUPPLIER0 + SKU556: + - SUPPLIER0 + SKU557: + - SUPPLIER0 + SKU558: + - SUPPLIER0 + SKU559: + - SUPPLIER0 + SKU56: + - SUPPLIER0 + SKU560: + - SUPPLIER0 + SKU561: + - SUPPLIER0 + SKU562: + - SUPPLIER0 + SKU563: + - SUPPLIER0 + SKU564: + - SUPPLIER0 + SKU565: + - SUPPLIER0 + SKU566: + - SUPPLIER0 + SKU567: + - SUPPLIER0 + SKU568: + - SUPPLIER0 + SKU569: + - SUPPLIER0 + SKU57: + - SUPPLIER0 + SKU570: + - SUPPLIER0 + SKU571: + - SUPPLIER0 + SKU572: + - SUPPLIER0 + SKU573: + - SUPPLIER0 + SKU574: + - SUPPLIER0 + SKU575: + - SUPPLIER0 + SKU576: + - SUPPLIER0 + SKU577: + - SUPPLIER0 + SKU578: + - SUPPLIER0 + SKU579: + - SUPPLIER0 + SKU58: + - SUPPLIER0 + SKU580: + - SUPPLIER0 + SKU581: + - SUPPLIER0 + SKU582: + - SUPPLIER0 + SKU583: + - SUPPLIER0 + SKU584: + - SUPPLIER0 + SKU585: + - SUPPLIER0 + SKU586: + - SUPPLIER0 + SKU587: + - SUPPLIER0 + SKU588: + - SUPPLIER0 + SKU589: + - SUPPLIER0 + SKU59: + - SUPPLIER0 + SKU590: + - SUPPLIER0 + SKU591: + - SUPPLIER0 + SKU592: + - SUPPLIER0 + SKU593: + - SUPPLIER0 + SKU594: + - SUPPLIER0 + SKU595: + - SUPPLIER0 + SKU596: + - SUPPLIER0 + SKU597: + - SUPPLIER0 + SKU598: + - SUPPLIER0 + SKU599: + - SUPPLIER0 + SKU6: + - SUPPLIER0 + SKU60: + - SUPPLIER0 + SKU600: + - SUPPLIER0 + SKU601: + - SUPPLIER0 + SKU602: + - SUPPLIER0 + SKU603: + - SUPPLIER0 + SKU604: + - SUPPLIER0 + SKU605: + - SUPPLIER0 + SKU606: + - SUPPLIER0 + SKU607: + - SUPPLIER0 + SKU608: + - SUPPLIER0 + SKU609: + - SUPPLIER0 + SKU61: + - SUPPLIER0 + SKU610: + - SUPPLIER0 + SKU611: + - SUPPLIER0 + SKU612: + - SUPPLIER0 + SKU613: + - SUPPLIER0 + SKU614: + - SUPPLIER0 + SKU615: + - SUPPLIER0 + SKU616: + - SUPPLIER0 + SKU617: + - SUPPLIER0 + SKU618: + - SUPPLIER0 + SKU619: + - SUPPLIER0 + SKU62: + - SUPPLIER0 + SKU620: + - SUPPLIER0 + SKU621: + - SUPPLIER0 + SKU622: + - SUPPLIER0 + SKU623: + - SUPPLIER0 + SKU624: + - SUPPLIER0 + SKU625: + - SUPPLIER0 + SKU626: + - SUPPLIER0 + SKU627: + - SUPPLIER0 + SKU628: + - SUPPLIER0 + SKU629: + - SUPPLIER0 + SKU63: + - SUPPLIER0 + SKU630: + - SUPPLIER0 + SKU631: + - SUPPLIER0 + SKU632: + - SUPPLIER0 + SKU633: + - SUPPLIER0 + SKU634: + - SUPPLIER0 + SKU635: + - SUPPLIER0 + SKU636: + - SUPPLIER0 + SKU637: + - SUPPLIER0 + SKU638: + - SUPPLIER0 + SKU639: + - SUPPLIER0 + SKU64: + - SUPPLIER0 + SKU640: + - SUPPLIER0 + SKU641: + - SUPPLIER0 + SKU642: + - SUPPLIER0 + SKU643: + - SUPPLIER0 + SKU644: + - SUPPLIER0 + SKU645: + - SUPPLIER0 + SKU646: + - SUPPLIER0 + SKU647: + - SUPPLIER0 + SKU648: + - SUPPLIER0 + SKU649: + - SUPPLIER0 + SKU65: + - SUPPLIER0 + SKU650: + - SUPPLIER0 + SKU651: + - SUPPLIER0 + SKU652: + - SUPPLIER0 + SKU653: + - SUPPLIER0 + SKU654: + - SUPPLIER0 + SKU655: + - SUPPLIER0 + SKU656: + - SUPPLIER0 + SKU657: + - SUPPLIER0 + SKU658: + - SUPPLIER0 + SKU659: + - SUPPLIER0 + SKU66: + - SUPPLIER0 + SKU660: + - SUPPLIER0 + SKU661: + - SUPPLIER0 + SKU662: + - SUPPLIER0 + SKU663: + - SUPPLIER0 + SKU664: + - SUPPLIER0 + SKU665: + - SUPPLIER0 + SKU666: + - SUPPLIER0 + SKU667: + - SUPPLIER0 + SKU668: + - SUPPLIER0 + SKU669: + - SUPPLIER0 + SKU67: + - SUPPLIER0 + SKU670: + - SUPPLIER0 + SKU671: + - SUPPLIER0 + SKU672: + - SUPPLIER0 + SKU673: + - SUPPLIER0 + SKU674: + - SUPPLIER0 + SKU675: + - SUPPLIER0 + SKU676: + - SUPPLIER0 + SKU677: + - SUPPLIER0 + SKU678: + - SUPPLIER0 + SKU679: + - SUPPLIER0 + SKU68: + - SUPPLIER0 + SKU680: + - SUPPLIER0 + SKU681: + - SUPPLIER0 + SKU682: + - SUPPLIER0 + SKU683: + - SUPPLIER0 + SKU684: + - SUPPLIER0 + SKU685: + - SUPPLIER0 + SKU686: + - SUPPLIER0 + SKU687: + - SUPPLIER0 + SKU688: + - SUPPLIER0 + SKU689: + - SUPPLIER0 + SKU69: + - SUPPLIER0 + SKU690: + - SUPPLIER0 + SKU691: + - SUPPLIER0 + SKU692: + - SUPPLIER0 + SKU693: + - SUPPLIER0 + SKU694: + - SUPPLIER0 + SKU695: + - SUPPLIER0 + SKU696: + - SUPPLIER0 + SKU697: + - SUPPLIER0 + SKU698: + - SUPPLIER0 + SKU699: + - SUPPLIER0 + SKU7: + - SUPPLIER0 + SKU70: + - SUPPLIER0 + SKU700: + - SUPPLIER0 + SKU701: + - SUPPLIER0 + SKU702: + - SUPPLIER0 + SKU703: + - SUPPLIER0 + SKU704: + - SUPPLIER0 + SKU705: + - SUPPLIER0 + SKU706: + - SUPPLIER0 + SKU707: + - SUPPLIER0 + SKU708: + - SUPPLIER0 + SKU709: + - SUPPLIER0 + SKU71: + - SUPPLIER0 + SKU710: + - SUPPLIER0 + SKU711: + - SUPPLIER0 + SKU712: + - SUPPLIER0 + SKU713: + - SUPPLIER0 + SKU714: + - SUPPLIER0 + SKU715: + - SUPPLIER0 + SKU716: + - SUPPLIER0 + SKU717: + - SUPPLIER0 + SKU718: + - SUPPLIER0 + SKU719: + - SUPPLIER0 + SKU72: + - SUPPLIER0 + SKU720: + - SUPPLIER0 + SKU721: + - SUPPLIER0 + SKU722: + - SUPPLIER0 + SKU723: + - SUPPLIER0 + SKU724: + - SUPPLIER0 + SKU725: + - SUPPLIER0 + SKU726: + - SUPPLIER0 + SKU727: + - SUPPLIER0 + SKU728: + - SUPPLIER0 + SKU729: + - SUPPLIER0 + SKU73: + - SUPPLIER0 + SKU730: + - SUPPLIER0 + SKU731: + - SUPPLIER0 + SKU732: + - SUPPLIER0 + SKU733: + - SUPPLIER0 + SKU734: + - SUPPLIER0 + SKU735: + - SUPPLIER0 + SKU736: + - SUPPLIER0 + SKU737: + - SUPPLIER0 + SKU738: + - SUPPLIER0 + SKU739: + - SUPPLIER0 + SKU74: + - SUPPLIER0 + SKU740: + - SUPPLIER0 + SKU741: + - SUPPLIER0 + SKU742: + - SUPPLIER0 + SKU743: + - SUPPLIER0 + SKU744: + - SUPPLIER0 + SKU745: + - SUPPLIER0 + SKU746: + - SUPPLIER0 + SKU747: + - SUPPLIER0 + SKU748: + - SUPPLIER0 + SKU749: + - SUPPLIER0 + SKU75: + - SUPPLIER0 + SKU750: + - SUPPLIER0 + SKU751: + - SUPPLIER0 + SKU752: + - SUPPLIER0 + SKU753: + - SUPPLIER0 + SKU754: + - SUPPLIER0 + SKU755: + - SUPPLIER0 + SKU756: + - SUPPLIER0 + SKU757: + - SUPPLIER0 + SKU758: + - SUPPLIER0 + SKU759: + - SUPPLIER0 + SKU76: + - SUPPLIER0 + SKU760: + - SUPPLIER0 + SKU761: + - SUPPLIER0 + SKU762: + - SUPPLIER0 + SKU763: + - SUPPLIER0 + SKU764: + - SUPPLIER0 + SKU765: + - SUPPLIER0 + SKU766: + - SUPPLIER0 + SKU767: + - SUPPLIER0 + SKU768: + - SUPPLIER0 + SKU769: + - SUPPLIER0 + SKU77: + - SUPPLIER0 + SKU770: + - SUPPLIER0 + SKU771: + - SUPPLIER0 + SKU772: + - SUPPLIER0 + SKU773: + - SUPPLIER0 + SKU774: + - SUPPLIER0 + SKU775: + - SUPPLIER0 + SKU776: + - SUPPLIER0 + SKU777: + - SUPPLIER0 + SKU778: + - SUPPLIER0 + SKU779: + - SUPPLIER0 + SKU78: + - SUPPLIER0 + SKU780: + - SUPPLIER0 + SKU781: + - SUPPLIER0 + SKU782: + - SUPPLIER0 + SKU783: + - SUPPLIER0 + SKU784: + - SUPPLIER0 + SKU785: + - SUPPLIER0 + SKU786: + - SUPPLIER0 + SKU787: + - SUPPLIER0 + SKU788: + - SUPPLIER0 + SKU789: + - SUPPLIER0 + SKU79: + - SUPPLIER0 + SKU790: + - SUPPLIER0 + SKU791: + - SUPPLIER0 + SKU792: + - SUPPLIER0 + SKU793: + - SUPPLIER0 + SKU794: + - SUPPLIER0 + SKU795: + - SUPPLIER0 + SKU796: + - SUPPLIER0 + SKU797: + - SUPPLIER0 + SKU798: + - SUPPLIER0 + SKU799: + - SUPPLIER0 + SKU8: + - SUPPLIER0 + SKU80: + - SUPPLIER0 + SKU800: + - SUPPLIER0 + SKU801: + - SUPPLIER0 + SKU802: + - SUPPLIER0 + SKU803: + - SUPPLIER0 + SKU804: + - SUPPLIER0 + SKU805: + - SUPPLIER0 + SKU806: + - SUPPLIER0 + SKU807: + - SUPPLIER0 + SKU808: + - SUPPLIER0 + SKU809: + - SUPPLIER0 + SKU81: + - SUPPLIER0 + SKU810: + - SUPPLIER0 + SKU811: + - SUPPLIER0 + SKU812: + - SUPPLIER0 + SKU813: + - SUPPLIER0 + SKU814: + - SUPPLIER0 + SKU815: + - SUPPLIER0 + SKU816: + - SUPPLIER0 + SKU817: + - SUPPLIER0 + SKU818: + - SUPPLIER0 + SKU819: + - SUPPLIER0 + SKU82: + - SUPPLIER0 + SKU820: + - SUPPLIER0 + SKU821: + - SUPPLIER0 + SKU822: + - SUPPLIER0 + SKU823: + - SUPPLIER0 + SKU824: + - SUPPLIER0 + SKU825: + - SUPPLIER0 + SKU826: + - SUPPLIER0 + SKU827: + - SUPPLIER0 + SKU828: + - SUPPLIER0 + SKU829: + - SUPPLIER0 + SKU83: + - SUPPLIER0 + SKU830: + - SUPPLIER0 + SKU831: + - SUPPLIER0 + SKU832: + - SUPPLIER0 + SKU833: + - SUPPLIER0 + SKU834: + - SUPPLIER0 + SKU835: + - SUPPLIER0 + SKU836: + - SUPPLIER0 + SKU837: + - SUPPLIER0 + SKU838: + - SUPPLIER0 + SKU839: + - SUPPLIER0 + SKU84: + - SUPPLIER0 + SKU840: + - SUPPLIER0 + SKU841: + - SUPPLIER0 + SKU842: + - SUPPLIER0 + SKU843: + - SUPPLIER0 + SKU844: + - SUPPLIER0 + SKU845: + - SUPPLIER0 + SKU846: + - SUPPLIER0 + SKU847: + - SUPPLIER0 + SKU848: + - SUPPLIER0 + SKU849: + - SUPPLIER0 + SKU85: + - SUPPLIER0 + SKU850: + - SUPPLIER0 + SKU851: + - SUPPLIER0 + SKU852: + - SUPPLIER0 + SKU853: + - SUPPLIER0 + SKU854: + - SUPPLIER0 + SKU855: + - SUPPLIER0 + SKU856: + - SUPPLIER0 + SKU857: + - SUPPLIER0 + SKU858: + - SUPPLIER0 + SKU859: + - SUPPLIER0 + SKU86: + - SUPPLIER0 + SKU860: + - SUPPLIER0 + SKU861: + - SUPPLIER0 + SKU862: + - SUPPLIER0 + SKU863: + - SUPPLIER0 + SKU864: + - SUPPLIER0 + SKU865: + - SUPPLIER0 + SKU866: + - SUPPLIER0 + SKU867: + - SUPPLIER0 + SKU868: + - SUPPLIER0 + SKU869: + - SUPPLIER0 + SKU87: + - SUPPLIER0 + SKU870: + - SUPPLIER0 + SKU871: + - SUPPLIER0 + SKU872: + - SUPPLIER0 + SKU873: + - SUPPLIER0 + SKU874: + - SUPPLIER0 + SKU875: + - SUPPLIER0 + SKU876: + - SUPPLIER0 + SKU877: + - SUPPLIER0 + SKU878: + - SUPPLIER0 + SKU879: + - SUPPLIER0 + SKU88: + - SUPPLIER0 + SKU880: + - SUPPLIER0 + SKU881: + - SUPPLIER0 + SKU882: + - SUPPLIER0 + SKU883: + - SUPPLIER0 + SKU884: + - SUPPLIER0 + SKU885: + - SUPPLIER0 + SKU886: + - SUPPLIER0 + SKU887: + - SUPPLIER0 + SKU888: + - SUPPLIER0 + SKU889: + - SUPPLIER0 + SKU89: + - SUPPLIER0 + SKU890: + - SUPPLIER0 + SKU891: + - SUPPLIER0 + SKU892: + - SUPPLIER0 + SKU893: + - SUPPLIER0 + SKU894: + - SUPPLIER0 + SKU895: + - SUPPLIER0 + SKU896: + - SUPPLIER0 + SKU897: + - SUPPLIER0 + SKU898: + - SUPPLIER0 + SKU899: + - SUPPLIER0 + SKU9: + - SUPPLIER0 + SKU90: + - SUPPLIER0 + SKU900: + - SUPPLIER0 + SKU901: + - SUPPLIER0 + SKU902: + - SUPPLIER0 + SKU903: + - SUPPLIER0 + SKU904: + - SUPPLIER0 + SKU905: + - SUPPLIER0 + SKU906: + - SUPPLIER0 + SKU907: + - SUPPLIER0 + SKU908: + - SUPPLIER0 + SKU909: + - SUPPLIER0 + SKU91: + - SUPPLIER0 + SKU910: + - SUPPLIER0 + SKU911: + - SUPPLIER0 + SKU912: + - SUPPLIER0 + SKU913: + - SUPPLIER0 + SKU914: + - SUPPLIER0 + SKU915: + - SUPPLIER0 + SKU916: + - SUPPLIER0 + SKU917: + - SUPPLIER0 + SKU918: + - SUPPLIER0 + SKU919: + - SUPPLIER0 + SKU92: + - SUPPLIER0 + SKU920: + - SUPPLIER0 + SKU921: + - SUPPLIER0 + SKU922: + - SUPPLIER0 + SKU923: + - SUPPLIER0 + SKU924: + - SUPPLIER0 + SKU925: + - SUPPLIER0 + SKU926: + - SUPPLIER0 + SKU927: + - SUPPLIER0 + SKU928: + - SUPPLIER0 + SKU929: + - SUPPLIER0 + SKU93: + - SUPPLIER0 + SKU930: + - SUPPLIER0 + SKU931: + - SUPPLIER0 + SKU932: + - SUPPLIER0 + SKU933: + - SUPPLIER0 + SKU934: + - SUPPLIER0 + SKU935: + - SUPPLIER0 + SKU936: + - SUPPLIER0 + SKU937: + - SUPPLIER0 + SKU938: + - SUPPLIER0 + SKU939: + - SUPPLIER0 + SKU94: + - SUPPLIER0 + SKU940: + - SUPPLIER0 + SKU941: + - SUPPLIER0 + SKU942: + - SUPPLIER0 + SKU943: + - SUPPLIER0 + SKU944: + - SUPPLIER0 + SKU945: + - SUPPLIER0 + SKU946: + - SUPPLIER0 + SKU947: + - SUPPLIER0 + SKU948: + - SUPPLIER0 + SKU949: + - SUPPLIER0 + SKU95: + - SUPPLIER0 + SKU950: + - SUPPLIER0 + SKU951: + - SUPPLIER0 + SKU952: + - SUPPLIER0 + SKU953: + - SUPPLIER0 + SKU954: + - SUPPLIER0 + SKU955: + - SUPPLIER0 + SKU956: + - SUPPLIER0 + SKU957: + - SUPPLIER0 + SKU958: + - SUPPLIER0 + SKU959: + - SUPPLIER0 + SKU96: + - SUPPLIER0 + SKU960: + - SUPPLIER0 + SKU961: + - SUPPLIER0 + SKU962: + - SUPPLIER0 + SKU963: + - SUPPLIER0 + SKU964: + - SUPPLIER0 + SKU965: + - SUPPLIER0 + SKU966: + - SUPPLIER0 + SKU967: + - SUPPLIER0 + SKU968: + - SUPPLIER0 + SKU969: + - SUPPLIER0 + SKU97: + - SUPPLIER0 + SKU970: + - SUPPLIER0 + SKU971: + - SUPPLIER0 + SKU972: + - SUPPLIER0 + SKU973: + - SUPPLIER0 + SKU974: + - SUPPLIER0 + SKU975: + - SUPPLIER0 + SKU976: + - SUPPLIER0 + SKU977: + - SUPPLIER0 + SKU978: + - SUPPLIER0 + SKU979: + - SUPPLIER0 + SKU98: + - SUPPLIER0 + SKU980: + - SUPPLIER0 + SKU981: + - SUPPLIER0 + SKU982: + - SUPPLIER0 + SKU983: + - SUPPLIER0 + SKU984: + - SUPPLIER0 + SKU985: + - SUPPLIER0 + SKU986: + - SUPPLIER0 + SKU987: + - SUPPLIER0 + SKU988: + - SUPPLIER0 + SKU989: + - SUPPLIER0 + SKU99: + - SUPPLIER0 + SKU990: + - SUPPLIER0 + SKU991: + - SUPPLIER0 + SKU992: + - SUPPLIER0 + SKU993: + - SUPPLIER0 + SKU994: + - SUPPLIER0 + SKU995: + - SUPPLIER0 + SKU996: + - SUPPLIER0 + SKU997: + - SUPPLIER0 + SKU998: + - SUPPLIER0 + SKU999: + - SUPPLIER0 diff --git a/maro/simulator/scenarios/supply_chain/units/balancesheet.py b/maro/simulator/scenarios/supply_chain/units/balancesheet.py deleted file mode 100644 index c01358e13..000000000 --- a/maro/simulator/scenarios/supply_chain/units/balancesheet.py +++ /dev/null @@ -1,23 +0,0 @@ - -class BalanceSheet: - def __init__(self, profit: int = 0, loss: int = 0): - self.profit = profit - self.loss = loss - - def total(self) -> int: - return self.profit + self.loss - - def __add__(self, other): - return BalanceSheet(self.profit + other.profit, self.loss + other.loss) - - def __sub__(self, other): - return BalanceSheet(self.profit - other.profit, self.loss - other.loss) - - def __repr__(self): - return f"{round(self.profit+self.loss, 0)} ({round(self.profit, 0)} {round(self.loss, 0)})" - - def __radd__(self, other): - if other == 0: - return self - else: - return self.__add__(other) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index ec0a4e037..626ce3cc5 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -20,13 +20,9 @@ def __init__(self): # States in python side. self.received = 0 self.purchased = 0 - self.order_cost = 0 self.sources = [] self.pending_order_daily = None - - # NOTE: this value is not set - self.reward_discount = 0.0 - self.price = 0 + self.order_product_cost = 0 def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): """Called after order product is received. @@ -63,11 +59,9 @@ def initialize(self): sku = self.facility.skus[self.product_id] - self.price = sku.price - - self.order_cost = self.facility.get_config("order_cost") + order_cost = self.facility.get_config("order_cost") - self.data_model.initialize() + self.data_model.initialize(sku.price, order_cost) if self.facility.upstreams is not None: # Construct sources from facility's upstreams. @@ -114,7 +108,7 @@ def step(self, tick: int): source_facility = self.world.get_facility_by_id(self.action.source_id) - order_product_cost = source_facility.distribution.place_order(order) + self.order_product_cost = source_facility.distribution.place_order(order) self.purchased = self.action.quantity @@ -123,9 +117,8 @@ def step(self, tick: int): if order.vlt < len(self.pending_order_daily): self.pending_order_daily[order.vlt-1] += order.quantity - order_profit = self.price * order.quantity - self.step_balance_sheet.loss = -self.order_cost - order_product_cost - self.step_reward = -self.order_cost - order_product_cost + self.reward_discount * order_profit + if self.order_product_cost > 0: + self.data_model.order_product_cost = self.order_product_cost def flush_states(self): if self.received > 0: @@ -152,9 +145,8 @@ def post_step(self, tick: int): self.data_model.purchased = 0 self.purchased = 0 - if self.order_cost > 0: + if self.order_product_cost > 0: self.data_model.order_product_cost = 0 - self.order_cost = 0 def reset(self): super(ConsumerUnit, self).reset() @@ -166,6 +158,9 @@ def reset(self): def set_action(self, action: object): super(ConsumerUnit, self).set_action(action) + if action.product_id > 0 and action.quantity > 0: + self.data_model.order_quantity = action.quantity + def get_in_transit_quantity(self): quantity = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 65e969e08..83f7a0cea 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -59,7 +59,6 @@ def place_order(self, order: Order) -> int: order_total_price = sku.price * order.quantity - # TODO: states related, enable it later if needed. self.check_in_order[order.product_id] += order.quantity return order_total_price @@ -87,9 +86,7 @@ def step(self, tick: int): # Push vehicle. vehicle.step(tick) - self.transportation_cost[vehicle.product_id] += abs(vehicle.step_reward) - - self.step_balance_sheet += vehicle.step_balance_sheet + self.transportation_cost[vehicle.product_id] += abs(vehicle.cost) # update order's delay penalty per tick. for order in self.order_queue: diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 4460674bc..942972aa0 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -25,8 +25,6 @@ class ManufactureUnit(SkuUnit): # How many we procedure per current step. manufacture_number = 0 - product_unit_cost = 0 - def __init__(self): super().__init__() @@ -34,9 +32,9 @@ def initialize(self): super(ManufactureUnit, self).initialize() facility_sku_info = self.facility.skus[self.product_id] - self.product_unit_cost = facility_sku_info.product_unit_cost + product_unit_cost = facility_sku_info.product_unit_cost - self.data_model.initialize() + self.data_model.initialize(product_unit_cost) global_sku_info = self.world.get_sku_by_id(self.product_id) @@ -89,11 +87,6 @@ def step(self, tick: int): else: self.manufacture_number = 0 - cost = self.manufacture_number * self.product_unit_cost - - self.step_balance_sheet.loss = -cost - self.step_reward = -cost - def flush_states(self): if self.manufacture_number > 0: self.data_model.manufacturing_number = self.manufacture_number diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index e8345f236..456e9fc07 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. import numpy as np -from .balancesheet import BalanceSheet from .consumer import ConsumerUnit from .manufacture import ManufactureUnit from .seller import SellerUnit @@ -27,67 +26,18 @@ class ProductUnit(SkuUnit): distribution: DistributionUnit = None - total_step_balance: BalanceSheet = None - def __init__(self): super().__init__() - self.total_step_balance = BalanceSheet() def initialize(self): super(ProductUnit, self).initialize() + self.data_model.initialize(self.get_selling_price()) + def step(self, tick: int): for unit in self.children: unit.step(tick) - self.deposit() - - def deposit(self): - balance_sheets = [] - rewards = [] - - for unit in self.children: - balance_sheets.append(unit.step_balance_sheet) - rewards.append(unit.step_reward) - - storage_reward = 0 - if self.storage is not None: - storage_reward = - self.storage.product_number[self.storage.product_index_mapping[self.product_id]] * self.storage.unit_storage_cost - - storage_balance = BalanceSheet(0, storage_reward) - - balance_sheets.append(storage_balance) - rewards.append(storage_reward) - - distribution_balance = BalanceSheet() - - if self.distribution is not None: - check_order = self.distribution.check_in_order[self.product_id] - transport_cost = self.distribution.transportation_cost[self.product_id] - delay_order_penalty = self.distribution.delay_order_penalty[self.product_id] - distribution_cost = -(transport_cost + delay_order_penalty) - - self.distribution.transportation_cost[self.product_id] = 0 - self.distribution.delay_order_penalty[self.product_id] = 0 - - distribution_balance.profit = check_order * self.get_selling_price() - distribution_balance.loss = distribution_cost - - balance_sheets.append(distribution_balance) - rewards.append(distribution_balance.total()) - - if self.product_id in self.facility.downstreams: - for facility in self.facility.downstreams[self.product_id]: - downstream_product = facility.products[self.product_id] - balance_sheets.append(downstream_product.step_balance_sheet) - rewards.append(downstream_product.step_reward) - - self.step_balance_sheet = sum(balance_sheets) - - self.step_reward = sum(rewards) - - self.total_step_balance += self.step_balance_sheet - def flush_states(self): for unit in self.children: unit.flush_states() diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index 83521d030..f69ae07c5 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -27,7 +27,6 @@ def __init__(self): self.price = 0 self.sale_hist = [] - self.backlog_ratio = 0 def market_demand(self, tick: int) -> int: """Generate market demand for current tick. @@ -45,11 +44,11 @@ def initialize(self): sku = self.facility.skus[self.product_id] - self.price = sku.price self.gamma = sku.sale_gamma - self.backlog_ratio = sku.backlog_ratio self.durations = self.world.durations + self.data_model.initialize(sku.price, sku.backlog_ratio) + # Generate demand distribution of this episode. for _ in range(self.durations): self.demand_distribution.append(int(np.random.gamma(self.gamma))) @@ -70,10 +69,6 @@ def step(self, tick: int): self.sale_hist.append(demand) self.sale_hist = self.sale_hist[1:] - self.step_balance_sheet.profit = sold_qty * self.price - self.step_balance_sheet.loss = -demand * self.price * self.backlog_ratio - self.step_reward = self.step_balance_sheet.total() - def flush_states(self): if self.sold > 0: self.data_model.sold = self.sold diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index b95a669ab..f172356c5 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -21,7 +21,6 @@ def __init__(self): self.product_index_mapping: Dict[int, int] = {} self.capacity = 0 self.remaining_space = 0 - self.unit_storage_cost = 0 def try_add_products(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: """Try to add products into storage. @@ -109,14 +108,10 @@ def get_product_number(self, product_id: int) -> int: return self.product_number[product_index] - def step(self, tick: int): - self.step_balance_sheet.loss = -(self.capacity - self.remaining_space) * self.unit_storage_cost - def initialize(self): super(StorageUnit, self).initialize() self.capacity = self.config.get("capacity", 100) - self.unit_storage_cost = self.config.get("unit_storage_cost", 1) for sku in self.facility.skus.values(): self.product_list.append(sku.id) diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py index d8cc53500..521ad9072 100644 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .balancesheet import BalanceSheet class UnitBase: """Base of all unit used to contain related logic. @@ -51,12 +50,9 @@ class UnitBase: # Current unit configurations. config: dict = None - step_balance_sheet: BalanceSheet = None - step_reward: float = None def __init__(self): - self.step_balance_sheet = BalanceSheet() - self.step_reward = 0 + pass def parse_configs(self, config: dict): """Parse configurations from config. diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index cdccdb1e6..9493e609b 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -39,7 +39,7 @@ def __init__(self): self.velocity = 0 self.quantity = 0 self.patient = 0 - + self.cost = 0 self.unit_transport_cost = 0 def schedule(self, destination: object, product_id: int, quantity: int, vlt: int): @@ -119,11 +119,11 @@ def initialize(self): super(VehicleUnit, self).initialize() patient = self.config.get("patient", 100) + self.unit_transport_cost = self.config.get("unit_transport_cost", 1) - self.data_model.initialize(patient=patient) + self.data_model.initialize(patient=patient, unit_transport_cost=self.unit_transport_cost) self.max_patient = patient - self.unit_transport_cost = self.config.get("unit_transport_cost", 1) def step(self, tick: int): # If we have not arrive at destination yet. @@ -170,8 +170,7 @@ def step(self, tick: int): self._reset_internal_states() self._reset_data_model() - self.step_balance_sheet.loss = -self.payload * self.unit_transport_cost - self.step_reward = -self.payload * self.unit_transport_cost + self.cost = self.payload * self.unit_transport_cost def flush_states(self): pass diff --git a/scripts/random_config.py b/scripts/random_config.py index f34de68e5..80a9313fc 100644 --- a/scripts/random_config.py +++ b/scripts/random_config.py @@ -26,8 +26,11 @@ class: "ProductUnit" is_template: true config: + agent_type: 4 consumer: class: "ConsumerUnit" +config: + agent_type: 1 """ # Definition of supplier. @@ -42,10 +45,13 @@ class: "ProductUnit" is_template: true config: + agent_type: 3 consumer: class: "ConsumerUnit" manufacture: class: "ManufactureUnit" +config: + agent_type: 0 """ # Definition of retailer. @@ -55,13 +61,18 @@ storage: class: "StorageUnit" products: - class: "ProductUnit" + class: "StoreProductUnit" is_template: true config: + agent_type: 5 consumer: class: "ConsumerUnit" seller: class: "SellerUnit" + config: + sale_hist_len: 4 +config: + agent_type: 2 """ # Template to generate a supplier facility. @@ -157,7 +168,21 @@ def construct_formula(constraint): } # Save the vehicle definition in the config, so later distribution will reference to it. - config = {"normal_vehicle": vehicle_conf, "facility_definitions": {}} + config = { + "normal_vehicle": vehicle_conf, + "facility_definitions": {}, + "settings": { + 'global_reward_weight_producer': 0.50, + 'global_reward_weight_consumer': 0.50, + "initial_balance": 100000, + "constraint_state_hist_len": max_constraint_states, + "total_echelons": 3, + "replenishment_discount": 0.9, + "reward_normalization": 1e7, + "constraint_violate_reward": -1e7, + "pending_order_len": 5 + } + } # Add the facility definitions. config["facility_definitions"]["SupplierFacility"] = safe_load(supplier_def) @@ -165,16 +190,7 @@ def construct_formula(constraint): config["facility_definitions"]["RetailerFacility"] = safe_load(retailer_def) # Generate settings first. - world_conf = {"settings": { - 'global_reward_weight_producer': 0.50, - 'global_reward_weight_consumer': 0.50, - "initial_balance": 100000, - "constraint_state_hist_len": max_constraint_states, - "total_echelons": 3, - "replenishment_discount": 0.9, - "reward_normalization": 1e7, - "constraint_violate_reward": -1e7, - }} + world_conf = {} sku_names = [f'SKU{i}' for i in range(sku_num)] From f4e65c3cf466c357f519ed81d5fc330d684ef4fd Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 1 Apr 2021 20:11:17 +0800 Subject: [PATCH 133/482] move max price, max_sources_per_facility into summary --- examples/hello_world/supply_chain/env_wrapper.py | 5 +++-- maro/simulator/scenarios/supply_chain/business_engine.py | 3 --- maro/simulator/scenarios/supply_chain/world.py | 4 +++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/hello_world/supply_chain/env_wrapper.py b/examples/hello_world/supply_chain/env_wrapper.py index 5ea567018..0e90bbf56 100644 --- a/examples/hello_world/supply_chain/env_wrapper.py +++ b/examples/hello_world/supply_chain/env_wrapper.py @@ -71,6 +71,8 @@ def __init__(self, env: Env): self._agent_list = env.agent_idx_list self._sku_number = len(self._summary["skus"]) + 1 + self._max_price = self._summary["max_price"] + self._max_sources_per_facility = self._summary["max_sources_per_facility"] # state for each tick self._cur_metrics = env.metrics @@ -79,7 +81,6 @@ def __init__(self, env: Env): # TODO: this is fixed after env setup self._cur_facility_storage_product_mapping = {} - self._max_sources_per_facility = self._cur_metrics["max_sources_per_facility"] # facility -> { # data_model_index:int, @@ -466,7 +467,7 @@ def _add_consumer_features(self, state, agent_info): state['is_below_rop'] = 1 def _add_price_features(self, state, agent_info): - state['max_price'] = self._cur_metrics["max_price"] + state['max_price'] = self._max_price state['sku_price'] = 0 state['sku_cost'] = 0 diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 1abe4a0cb..3c9ee8db3 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -144,9 +144,6 @@ def _dispatch_action(self): def get_metrics(self): return { - # TODO: move fields that will not change to summary - "max_price": self.world.max_price, - "max_sources_per_facility": self.world.max_sources_per_facility, "products": { product.id: { "sale_mean": product.get_sale_mean(), diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 1fe86c8f3..6a3e1658b 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -408,7 +408,9 @@ def get_node_mapping(self): "agent_types": [k for k in self.agent_type_dict.keys()], "unit_mapping": id2index_mapping, "skus": {sku.id: sku for sku in self._sku_collection.values()}, - "facilities": facility_info_dict + "facilities": facility_info_dict, + "max_price": self.max_price, + "max_sources_per_facility": self.max_sources_per_facility, } def _register_data_model(self, alias: str) -> int: From 237f009c2ff8aaaff99c7819ddeee4f8f51061df Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 1 Apr 2021 20:22:47 +0800 Subject: [PATCH 134/482] cache ppf value --- .../hello_world/supply_chain/env_wrapper.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/hello_world/supply_chain/env_wrapper.py b/examples/hello_world/supply_chain/env_wrapper.py index 0e90bbf56..c3b4a2c8f 100644 --- a/examples/hello_world/supply_chain/env_wrapper.py +++ b/examples/hello_world/supply_chain/env_wrapper.py @@ -81,6 +81,8 @@ def __init__(self, env: Env): # TODO: this is fixed after env setup self._cur_facility_storage_product_mapping = {} + self._service_index_ppf_cache = {} + # facility -> { # data_model_index:int, @@ -460,8 +462,13 @@ def _add_consumer_features(self, state, agent_info): if (state['inventory_estimated'] <= 0): state['is_out_of_stock'] = 1 + service_index = state['service_level'] + + if service_index not in self._service_index_ppf_cache: + self._service_index_ppf_cache[service_index] = st.norm.ppf(service_index) + state['inventory_rop'] = (state['max_vlt']*state['sale_mean'] - + np.sqrt(state['max_vlt'])*state['sale_std']*st.norm.ppf(state['service_level'])) + + np.sqrt(state['max_vlt'])*state['sale_std']*self._service_index_ppf_cache[service_index]) if state['inventory_estimated'] < state['inventory_rop']: state['is_below_rop'] = 1 @@ -734,7 +741,7 @@ def calc(self): start_tick = 0 durations = 100 - env = Env(scenario="supply_chain", topology="sample1", + env = Env(scenario="supply_chain", topology="random2", start_tick=start_tick, durations=durations) ss = SCEnvWrapper(env) @@ -751,8 +758,15 @@ def calc(self): # print("time cost: ", end_time - start_time) + times = 10000 start_time = time() - ss.get_state(None) + + for i in range(times): + env.step(None) + + # for i in range(times): + # ss.get_state(None) + # ss.get_reward(None) end_time = time() From d3dd077bbbc7d528b7485d8d472f03ab184519b7 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Apr 2021 05:09:48 +0000 Subject: [PATCH 135/482] revised supply chain example based on new state and reward shaping --- examples/supply_chain/dqn/agent.py | 20 +- examples/supply_chain/dqn/config.yml | 2 +- .../dqn/single_thread_launcher.py | 3 +- examples/supply_chain/env_wrapper.py | 527 ++++++++++++++++-- 4 files changed, 485 insertions(+), 67 deletions(-) diff --git a/examples/supply_chain/dqn/agent.py b/examples/supply_chain/dqn/agent.py index f0638c10a..76008f102 100644 --- a/examples/supply_chain/dqn/agent.py +++ b/examples/supply_chain/dqn/agent.py @@ -5,9 +5,9 @@ # model input and output dimensions -MANUFACTURER_IN_DIM = 6 -MANUFACTURER_OUT_DIM = 10 -CONSUMER_IN_DIM = 8 +PRODUCER_IN_DIM = 32 +PRODUCER_OUT_DIM = 10 +CONSUMER_IN_DIM = 32 CONSUMER_OUT_DIM = 100 @@ -19,13 +19,13 @@ def get_dqn_agent(in_dim, out_dim, config): return DQN(q_model, DQNConfig(**config["hyper_params"])) -def get_sc_agents(agent_ids, config): - manufacturer_agents = { - id_: get_dqn_agent(MANUFACTURER_IN_DIM, MANUFACTURER_OUT_DIM, config) - for type_, id_ in agent_ids if type_ == "manufacture" +def get_sc_agents(agent_info_list, config): + producer_agents = { + f"producer.{info.id}": get_dqn_agent(PRODUCER_IN_DIM, PRODUCER_OUT_DIM, config) + for info in agent_info_list } consumer_agents = { - id_: get_dqn_agent(CONSUMER_IN_DIM, CONSUMER_OUT_DIM, config) - for type_, id_ in agent_ids if type_ == "consumer" + f"consumer.{info.id}": get_dqn_agent(CONSUMER_IN_DIM, CONSUMER_OUT_DIM, config) + for info in agent_info_list } - return MultiAgentWrapper({**manufacturer_agents, **consumer_agents}) + return MultiAgentWrapper({**producer_agents, **consumer_agents}) diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index 4612224d8..7573b2d2e 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -26,7 +26,7 @@ training: env: scenario: supply_chain topology: sample1 - durations: 10000 + durations: 100 max_episode: 1 min_experiences_to_train: 10 train_iter: 10 diff --git a/examples/supply_chain/dqn/single_thread_launcher.py b/examples/supply_chain/dqn/single_thread_launcher.py index 17fc174b3..bd52c45a5 100644 --- a/examples/supply_chain/dqn/single_thread_launcher.py +++ b/examples/supply_chain/dqn/single_thread_launcher.py @@ -11,9 +11,8 @@ from maro.simulator import Env from maro.utils import set_seeds -from examples.supply_chain.env_wrapper import SCEnvWrapper - from examples.supply_chain.dqn.agent import get_sc_agents +from examples.supply_chain.env_wrapper import SCEnvWrapper # Single-threaded launcher diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 1db12fc70..543bd82a8 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -1,84 +1,503 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import random -from collections import defaultdict +from collections import defaultdict, namedtuple import numpy as np +import scipy.stats as st from maro.rl import AbsEnvWrapper +from maro.simulator import Env from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction +def stock_constraint(f_state): + return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt']+7)*f_state['sale_mean']) + + +def is_replenish_constraint(f_state): + return (f_state['consumption_hist'][-1] > 0) + + +def low_profit(f_state): + return ((f_state['sku_price']-f_state['sku_cost']) * f_state['sale_mean'] <= 1000) + + +def low_stock_constraint(f_state): + return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt']+3)*f_state['sale_mean']) + + +def out_of_stock(f_state): + return (0 < f_state['inventory_in_stock']) + + +atoms = { + 'stock_constraint': stock_constraint, + 'is_replenish_constraint': is_replenish_constraint, + 'low_profit': low_profit, + 'low_stock_constraint': low_stock_constraint, + 'out_of_stock': out_of_stock +} + + +class UnitBaseInfo: + id: int = None + node_index: int = None + config: dict = None + summary: dict = None + + def __init__(self, unit_summary): + self.id = unit_summary["id"] + self.node_index = unit_summary["node_index"] + self.config = unit_summary.get("config", {}) + self.summary = unit_summary + + def __getitem__(self, key, default=None): + if key in self.summary: + return self.summary[key] + + return default + + class SCEnvWrapper(AbsEnvWrapper): - manufacturer_features = [ - "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", - "product_unit_cost", "storage_data_model_index" - ] + def __init__(self, env: Env): + super().__init__(env) + self.storage_ss = env.snapshot_list["storage"] - consumer_features = [ - "id", "facility_id", "product_id", "order_cost", "total_purchased", "total_received", "source_id", - "quantity", "vlt", "purchased", "received", "order_product_cost" - ] + self._summary = env.summary['node_mapping'] + self._configs = env.configs + self._agent_types = self._summary["agent_types"] + self._units_mapping = self._summary["unit_mapping"] + self._agent_list = env.agent_idx_list - storage_features = ["id", "facility_id", "capacity", "remaining_space", "unit_storage_cost"] + self._sku_number = len(self._summary["skus"]) + 1 - # Only keep non-ID features in states - manufacturer_state_indexes = [2, 3, 6] - consumer_state_indexes = [3, 4, 5, 7, 8, 9, 10, 11] + # state for each tick + self._cur_metrics = env.metrics + self._cur_facility_storage_products = None - def get_state(self, event): - self.state_info = {} - # manufacturer state shaping - manufacturer_snapshots = self.env.snapshot_list["manufacture"] - storage_snapshots = self.env.snapshot_list["storage"] - manufacturer_features = manufacturer_snapshots[self.env.frame_index::SCEnvWrapper.manufacturer_features] - manufacturer_features = manufacturer_features.flatten().reshape(len(manufacturer_snapshots), -1).astype(np.int32) - - # combine manufacture state and the corresponding storage state - state = { - feature[0]: np.concatenate([ - feature[SCEnvWrapper.manufacturer_state_indexes], - storage_snapshots[self.env.frame_index:feature[-1]:SCEnvWrapper.storage_features[2:]].flatten() - ]).astype(np.float32) - for feature in manufacturer_features - } + # TODO: this is fixed after env setup + self._cur_facility_storage_product_mapping = {} - # consumer state shaping - consumer_snapshots = self.env.snapshot_list["consumer"] - consumer_features = consumer_snapshots[self.env.frame_index::SCEnvWrapper.consumer_features] - consumer_features = consumer_features.flatten().reshape(len(consumer_snapshots), -1).astype(np.int32) - state.update({ - feature[0]: np.asarray(feature[SCEnvWrapper.consumer_state_indexes], dtype=np.float32) - for feature in consumer_features - }) - self.state_info = {feature[0]: {"product_id": feature[2]} for feature in consumer_features} + self._max_sources_per_facility = self._cur_metrics["max_sources_per_facility"] - return state + # facility -> { + # data_model_index:int, + # storage:UnitBaseInfo, + # distribution: UnitBaseInfo, + # product_id: { + # consumer: UnitBaseInfo, + # seller: UnitBaseInfo, + # manufacture: UnitBaseInfo + # } + # } + self.facility_levels = {} + + # unit id -> (facility id) + self.unit_2_facility_dict = {} + + for facility_id, facility in self._summary["facilities"].items(): + self.facility_levels[facility_id] = { + "node_index": facility["node_index"], + "config": facility['configs'], + "upstreams": facility["upstreams"], + "skus": facility["skus"] + } + + units = facility["units"] + + storage = units["storage"] + if storage is not None: + self.facility_levels[facility_id]["storage"] = UnitBaseInfo(storage) + + self.unit_2_facility_dict[storage["id"]] = facility_id + + distribution = units["distribution"] + + if distribution is not None: + self.facility_levels[facility_id]["distribution"] = UnitBaseInfo(distribution) + self.unit_2_facility_dict[distribution["id"]] = facility_id + + products = units["products"] + + if products: + for product_id, product in products.items(): + product_info = { + "skuproduct": UnitBaseInfo(product) + } + + self.unit_2_facility_dict[product["id"]] = facility_id + + seller = product['seller'] + + if seller is not None: + product_info["seller"] = UnitBaseInfo(seller) + self.unit_2_facility_dict[seller["id"]] = facility_id + + consumer = product["consumer"] + + if consumer is not None: + product_info["consumer"] = UnitBaseInfo(consumer) + self.unit_2_facility_dict[consumer["id"]] = facility_id + + manufacture = product["manufacture"] + + if manufacture is not None: + product_info["manufacture"] = UnitBaseInfo(manufacture) + self.unit_2_facility_dict[manufacture["id"]] = facility_id + + self.facility_levels[facility_id][product_id] = product_info + + def get_state(self, event): + return self._get_state() def get_action(self, action_by_agent): # cache the sources for each consumer if not yet cached - if not hasattr(self, "consumer_source"): - self.consumer_source = {} - for id_, (type_, idx) in self.env.summary["node_mapping"]["unit_mapping"].items(): - if type_ == "consumer": - sources = self.env.snapshot_list["consumer"][self.env.frame_index:idx:"sources"] - if sources: - sources = sources.flatten().astype(np.int) - self.consumer_source[id_] = sources + if not hasattr(self, "consumer2source"): + self.consumer2source, self.consumer2product = {}, {} + for facility in self.env.summary["node_mapping"]["facilities"].values(): + products = facility["units"]["products"] + for product_id, product in products.items(): + consumer = product["consumer"] + if consumer is not None: + consumer_id = ".".join(["consumer", str(consumer["id"])]) + self.consumer2source[consumer_id] = consumer["sources"] + self.consumer2product[consumer_id] = product_id env_action = {} for agent_id, action in action_by_agent.items(): # consumer action - if agent_id in self.state_info: - source_id = random.choice(self.consumer_source[agent_id]) if agent_id in self.consumer_source else 0 - product_id = self.state_info[agent_id]["product_id"] - env_action[agent_id] = ConsumerAction(agent_id, product_id, source_id, action, 1) + if agent_id.startswith("consumer"): + sources = self.consumer2source.get(agent_id, []) + if sources: + source_id = sources[0] + product_id = self.consumer2product.get(agent_id, 0) + env_action[agent_id] = ConsumerAction(agent_id, product_id, source_id, action, 1) # manufacturer action - else: + elif agent_id.startswith("producer"): env_action[agent_id] = ManufactureAction(agent_id, action) return env_action def get_reward(self, tick=None): - return np.float32(np.random.rand()) + step_rewards = self._cur_metrics["step_rewards"] + step_balance_sheet = self._cur_metrics["step_balance_sheet"] + + wc = self.env.configs.settings["global_reward_weight_consumer"] + + parent_facility_balance = {} + + for f_id, sheet in step_balance_sheet.items(): + if f_id in self.unit_2_facility_dict: + # it is a product unit + parent_facility_balance[f_id] = step_balance_sheet[self.unit_2_facility_dict[f_id]] + else: + parent_facility_balance[f_id] = sheet + + consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id] + (1 - wc) * reward for f_id, reward in step_balance_sheet.items() } + + return { + **{f"producer.{f_id}": np.float32(reward) for f_id, reward in step_balance_sheet.items()}, + **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} + } + + def _get_state(self): + self._cur_metrics = self.env.metrics + state = {} + for agent_info in self._agent_list: + storage_index = self.facility_levels[agent_info.facility_id]['storage'].node_index + + storage_product_list = self.storage_ss[self.env.tick:storage_index:"product_list"].flatten().astype(np.int) + storage_product_number = self.storage_ss[self.env.tick:storage_index:"product_number"].flatten().astype(np.int) + + self._cur_facility_storage_products = {pid: pnum for pid, pnum in zip(storage_product_list, storage_product_number)} + + self._cur_facility_storage_product_mapping = {product_id: i for i, product_id in enumerate(storage_product_list)} + + f_state = self._state(agent_info) + + self._add_global_features(f_state) + + state[f"consumer.{agent_info.id}"] = f_state + state[f"producer.{agent_info.id}"] = f_state + + return self._serialize_state(state) + + def _state(self, agent_info): + state = {} + + self._add_facility_features(state, agent_info) + self._add_storage_features(state, agent_info) + self._add_bom_features(state, agent_info) + self._add_distributor_features(state, agent_info) + self._add_sale_features(state, agent_info) + self._add_vlt_features(state, agent_info) + self._add_consumer_features(state, agent_info) + self._add_price_features(state, agent_info) + + return state + + def _add_global_features(self, state): + state['global_time'] = self.env.tick + + def _add_facility_features(self, state, agent_info): + state['facility_type'] = [ + 1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] + + # NOTE: We cannot provide facility instance + state["facility"] = None + + state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] + + # NOTE: we have no constraint now + state['constraint_idx'] = [0] + + for atom_name in atoms.keys(): + state[atom_name] = list(np.ones(self._configs.settings['constraint_state_hist_len'])) + + # NOTE: named as facility_id but actually sku id + # NOTE: as our sku id start from 1, so we need expend one-hot length + state['facility_id'] = [0] * self._sku_number + + facility = self.facility_levels[agent_info.facility_id] + + if agent_info.is_facility: + # truely facility + state['facility_info'] = facility['config'] + state['sku_info'] = {} + + metrics = self._cur_metrics["facilities"][agent_info.facility_id] + + state['is_positive_balance'] = 1 if metrics["total_balance_sheet"].total() > 0 else 0 + else: + # a product unit + # 3rd slot is the facility id of this unit + state['facility_info'] = facility['config'] + state['sku_info'] = agent_info.sku + + metrics = self._cur_metrics["products"][agent_info.id] + state['is_positive_balance'] = 1 if metrics["total_balance_sheet"].total() > 0 else 0 + + # NOTE: ignore constraint here + + state['facility_id'][agent_info.sku.id] = 1 + + # NOTE: ignore atom here + + # NOTE: ignore this as we do not implement it now + state['echelon_level'] = 0 + + def _add_storage_features(self, state, agent_info): + facility = self.facility_levels[agent_info.facility_id] + + state['storage_levels'] = [0] * self._sku_number + + state['storage_capacity'] = facility['storage'].config["capacity"] + + state['storage_utilization'] = 0 + + for product_id, product_number in self._cur_facility_storage_products.items(): + state['storage_levels'][product_id] = product_number + state['storage_utilization'] += product_number + + def _add_bom_features(self, state, agent_info): + state['bom_inputs'] = [0] * self._sku_number + state['bom_outputs'] = [0] * self._sku_number + + if not agent_info.is_facility: + state['bom_inputs'][agent_info.sku.id] = 1 + state['bom_outputs'][agent_info.sku.id] = 1 + + def _add_vlt_features(self, state, agent_info): + sku_list = self._summary["skus"] + facility = self.facility_levels[agent_info.facility_id] + + current_source_list = [] + + # only for product unit + if agent_info.sku is not None: + current_source_list = facility["upstreams"].get(agent_info.sku.id, []) + + state['vlt'] = [0] * (self._max_sources_per_facility * self._sku_number) + state['max_vlt'] = 0 + + if not agent_info.is_facility: + # only for sku product + product_info = facility[agent_info.sku.id] + + if "consumer" in product_info and len(current_source_list) > 0: + state['max_vlt'] = product_info["skuproduct"]["max_vlt"] + + for i, source in enumerate(current_source_list): + for j, sku in enumerate(sku_list.values()): + # NOTE: different with original code, our config can make sure that source has product we need + + if sku.id == agent_info.sku.id: + state['vlt'][i * len(sku_list) + j + 1] = facility["skus"][sku.id].vlt + + def _add_sale_features(self, state, agent_info): + state['sale_mean'] = 1.0 + state['sale_std'] = 1.0 + state['sale_gamma'] = 1.0 + state['service_level'] = 0.95 + state['total_backlog_demand'] = 0 + + settings = self.env.configs.settings + + hist_len = settings['sale_hist_len'] + consumption_hist_len = settings['consumption_hist_len'] + + state['sale_hist'] = [0] * hist_len + state['backlog_demand_hist'] = [0] * hist_len + state['consumption_hist'] = [0] * consumption_hist_len + state['pending_order'] = [0] * settings['pending_order_len'] + + if agent_info.is_facility: + return + + product_metrics = self._cur_metrics["products"][agent_info.id] + + # for product unit only + state['service_level'] = agent_info.sku.service_level + state['sale_mean'] = product_metrics["sale_mean"] + state['sale_gamma'] = state['sale_mean'] + state['sale_std'] = product_metrics["sale_std"] + + facility = self.facility_levels[agent_info.facility_id] + product_info = facility[agent_info.sku.id] + + if "consumer" in product_info: + # TODO: implement later + consumer_index = product_info["consumer"].node_index + + consumption_hist = self.env.snapshot_list["consumer"][[self.env.tick - i for i in range(consumption_hist_len)]:consumer_index:"latest_consumptions"] + consumption_hist = consumption_hist.flatten() + + state['consumption_hist'] = list(consumption_hist) + state['pending_order'] = list(product_metrics["pending_order_daily"]) + + if "seller" in product_info: + seller_index = product_info["seller"].node_index + seller_ss = self.env.snapshot_list["seller"] + + single_states = seller_ss[self.env.tick:seller_index:("total_demand")].flatten().astype(np.int) + hist_states = seller_ss[[self.env.tick - i for i in range(hist_len)]:seller_index:("sold", "demand")].flatten().reshape(2, -1).astype(np.int) + + state['total_backlog_demand'] = single_states[0] + state['sale_hist'] = list(hist_states[0]) + state['backlog_demand_hist'] = list(hist_states[1]) + state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma + + def _add_distributor_features(self, state, agent_info): + state['distributor_in_transit_orders'] = 0 + state['distributor_in_transit_orders_qty'] = 0 + + facility = self.facility_levels[agent_info.facility_id] + + distribution = facility.get("distribution", None) + + if distribution is not None: + dist_states = self.env.snapshot_list["distribution"][self.env.tick:distribution.id:("remaining_order_quantity", "remaining_order_number")] + dist_states = dist_states.flatten().astype(np.int) + + state['distributor_in_transit_orders'] = dist_states[1] + state['distributor_in_transit_orders_qty'] = dist_states[0] + + def _add_consumer_features(self, state, agent_info): + state['consumer_source_export_mask'] = [0] * (self._max_sources_per_facility * self._sku_number) + state['consumer_source_inventory'] = [0] * self._sku_number + state['consumer_in_transit_orders'] = [0] * self._sku_number + + state['inventory_in_stock'] = 0 + state['inventory_in_transit'] = 0 + state['inventory_in_distribution'] = 0 + state['inventory_estimated'] = 0 + state['inventory_rop'] = 0 + state['is_over_stock'] = 0 + state['is_out_of_stock'] = 0 + state['is_below_rop'] = 0 + + if agent_info.is_facility: + return + + facility = self.facility_levels[agent_info.facility_id] + product_info = facility[agent_info.sku.id] + + if "consumer" not in product_info: + return + + source_list = facility["upstreams"].get(agent_info.sku.id, []) + + if len(source_list) == 0: + return + + sku_list = self._summary["skus"] + + for i, source in enumerate(source_list): + for j, sku in enumerate(sku_list.values()): + if sku.id == agent_info.sku.id: + state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = self.facility_levels[source]["skus"][sku.id].vlt + + in_transit_orders = self._cur_metrics['facilities'][agent_info.facility_id]["in_transit_orders"] + + for i, sku in enumerate(sku_list.values()): + state['consumer_in_transit_orders'][sku.id] += in_transit_orders[sku.id] + + state['inventory_in_stock'] = self._cur_facility_storage_products[agent_info.sku.id] + state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] + + pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] + + if pending_order is not None: + state['inventory_in_distribution'] = pending_order[agent_info.sku.id] + + state['inventory_estimated'] = (state['inventory_in_stock'] + + state['inventory_in_transit'] + - state['inventory_in_distribution']) + if (state['inventory_estimated'] >= 0.5*state['storage_capacity']): + state['is_over_stock'] = 1 + + if (state['inventory_estimated'] <= 0): + state['is_out_of_stock'] = 1 + + state['inventory_rop'] = (state['max_vlt']*state['sale_mean'] + + np.sqrt(state['max_vlt'])*state['sale_std']*st.norm.ppf(state['service_level'])) + + if state['inventory_estimated'] < state['inventory_rop']: + state['is_below_rop'] = 1 + + def _add_price_features(self, state, agent_info): + state['max_price'] = self._cur_metrics["max_price"] + state['sku_price'] = 0 + state['sku_cost'] = 0 + + if not agent_info.is_facility: + state['sku_price'] = agent_info.sku.price + state['sku_cost'] = agent_info.sku.cost + + def _serialize_state(self, state): + result = defaultdict(list) + keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', + 'constraint_idx', 'is_accepted', 'consumption_hist']), + ('storage_capacity', ['storage_utilization']), + ('sale_gamma', ['sale_std', + 'sale_hist', + 'pending_order', + 'inventory_in_stock', + 'inventory_in_transit', + 'inventory_estimated', + 'inventory_rop']), + ('max_price', ['sku_price', 'sku_cost'])] + + for agent_id, agent_raw_state in state.items(): + for norm, fields in keys_in_state: + for field in fields: + vals = agent_raw_state[field] + if not isinstance(vals, list): + vals = [vals] + if norm is not None: + vals = [max(0.0, min(100.0, x/(agent_raw_state[norm]+0.01))) for x in vals] + result[agent_id].extend(vals) + result[agent_id] = np.asarray(result[agent_id], dtype=np.float32) + + return result From 42d1cc8040699b4506450e6351a773a6a69bc7f1 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Apr 2021 05:17:26 +0000 Subject: [PATCH 136/482] bug fixes --- examples/supply_chain/dqn/config.yml | 2 +- maro/rl/agent/__init__.py | 4 ++-- maro/rl/training/env_wrapper.py | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index 7573b2d2e..dbfd48c18 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -27,7 +27,7 @@ training: scenario: supply_chain topology: sample1 durations: 100 - max_episode: 1 + max_episode: 10 min_experiences_to_train: 10 train_iter: 10 batch_size: 128 diff --git a/maro/rl/agent/__init__.py b/maro/rl/agent/__init__.py index 137f0a66b..d79d894bc 100644 --- a/maro/rl/agent/__init__.py +++ b/maro/rl/agent/__init__.py @@ -3,16 +3,16 @@ from .abs_agent import AbsAgent from .ac import ActorCritic, ActorCriticConfig -from .agent_wrapper import MultiAgentWrapper from .ddpg import DDPG, DDPGConfig from .dqn import DQN, DQNConfig +from .multi_agent_wrapper import MultiAgentWrapper from .pg import PolicyGradient __all__ = [ "AbsAgent", "ActorCritic", "ActorCriticConfig", - "MultiAgentWrapper", "DDPG", "DDPGConfig", "DQN", "DQNConfig", + "MultiAgentWrapper", "PolicyGradient" ] diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index 3b8b9c185..f74ec9013 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -109,14 +109,15 @@ def step(self, action_by_agent: dict): if not done and self.env.tick - tick < self.reward_eval_delay: self._pending_reward_idx += i break - reward = self.get_reward(tick=tick) + reward_dict = self.get_reward(tick=tick) for agent_id in action: if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): - self.replay[agent_id]["R"].append(reward) + self.replay[agent_id]["R"].append(reward_dict[agent_id]) if done: self._pending_reward_idx = len(self._event_ticks) - 1 else: + reward_dict = self.get_reward() for agent_id, action in action_by_agent.items(): if isinstance(action, tuple): self.replay[agent_id]["A"].append(action[0]) @@ -124,7 +125,7 @@ def step(self, action_by_agent: dict): else: self.replay[agent_id]["A"].append(action) if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): - self.replay[agent_id]["R"].append(self.get_reward()) + self.replay[agent_id]["R"].append(reward_dict[agent_id]) self._pending_reward_idx += 1 if not done: From 4184e52328e27267d23894070bcfcd324263a8da Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 2 Apr 2021 16:51:22 +0800 Subject: [PATCH 137/482] fix unused branch --- maro/simulator/scenarios/supply_chain/units/consumer.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 626ce3cc5..f6659da39 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -45,12 +45,8 @@ def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): product_id (int): What product in the order. qty_delta (int): Number of product to update (sum). """ - if qty_delta > 0: - # New order for product. - self.open_orders[source_id][product_id] += qty_delta - else: - # An order is completed, update the remaining number. - self.open_orders[source_id][product_id] += qty_delta + # New order for product. + self.open_orders[source_id][product_id] += qty_delta def initialize(self): super(ConsumerUnit, self).initialize() From 94afafdeab4b3794e0781a57b1c7a2e9f6d469bd Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 2 Apr 2021 17:53:56 +0800 Subject: [PATCH 138/482] use a more flexible object instead original sku info --- .../hello_world/supply_chain/env_wrapper.py | 39 +++++------------- examples/hello_world/supply_chain/hello.py | 4 +- .../supply_chain/facilities/facility.py | 18 ++++++++- .../supply_chain/facilities/retailer.py | 35 ---------------- .../supply_chain/facilities/supplier.py | 40 +------------------ .../supply_chain/facilities/warehouse.py | 30 -------------- .../topologies/sample1/config.yml | 17 ++++++-- .../scenarios/supply_chain/units/storage.py | 1 + .../simulator/scenarios/supply_chain/world.py | 11 ++--- 9 files changed, 51 insertions(+), 144 deletions(-) diff --git a/examples/hello_world/supply_chain/env_wrapper.py b/examples/hello_world/supply_chain/env_wrapper.py index c3b4a2c8f..7b37072f4 100644 --- a/examples/hello_world/supply_chain/env_wrapper.py +++ b/examples/hello_world/supply_chain/env_wrapper.py @@ -176,14 +176,14 @@ def get_reward(self, tick=None): parent_facility_balance = {} - for f_id, sheet in step_balance_sheet.items(): + for f_id, sheet in self.cur_balance_sheet_reward.items(): if f_id in self.unit_2_facility_dict: # it is a product unit - parent_facility_balance[f_id] = step_balance_sheet[self.unit_2_facility_dict[f_id]] + parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] else: parent_facility_balance[f_id] = sheet - consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items() } + consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items() } rewards_by_agent = { "consumer": {}, @@ -625,7 +625,7 @@ def calc(self): # product distribution profit = check order * price product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] - # result we need + # result we need product_step_reward = np.zeros((len(self.products,))) product_balance_sheet_profit = np.zeros((len(self.products,))) product_balance_sheet_loss = np.zeros((len(self.products, ))) @@ -686,7 +686,7 @@ def calc(self): # storage storage_states = self.storage_ss[tick::self.storage_features].flatten().reshape(-1, len(self.storage_features)) - # loss = (capacity-remaining space) * cost + # loss = (capacity-remaining space) * cost storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) # vehicles @@ -736,39 +736,22 @@ def calc(self): return result + if __name__ == "__main__": from time import time start_tick = 0 durations = 100 - env = Env(scenario="supply_chain", topology="random2", + env = Env(scenario="supply_chain", topology="sample1", start_tick=start_tick, durations=durations) ss = SCEnvWrapper(env) env.step(None) - # bbs = BalanceSheetCalculator(env) - - # start_time = time() - # ss = bbs.calc() - # end_time = time() - - # print(ss) - - # print("time cost: ", end_time - start_time) - - times = 10000 - start_time = time() - - for i in range(times): - env.step(None) - - # for i in range(times): - # ss.get_state(None) + states = ss.get_state(None) - # ss.get_reward(None) - end_time = time() + rewards = ss.get_reward(None) - print("time cost: ", end_time - start_time) - # print(env.metrics) + print(states) + print(rewards) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index e79262aac..ba357a333 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -10,12 +10,14 @@ def main(): start_tick = 0 durations = 100 - env = Env(scenario="supply_chain", topology="sample1", start_tick=start_tick, durations=durations) + env = Env(scenario="supply_chain", topology="sample1", start_tick=start_tick, durations=durations, max_snapshots=100) total_episodes = 10 matrics = None is_done = False + print(env.agent_idx_list) + for ep in range(total_episodes): print("Current episode:", ep) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index f0a2b7268..dc06759d7 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -3,6 +3,10 @@ from collections import defaultdict from abc import ABC +from typing import List, Dict + +from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo + class FacilityBase(ABC): """Base of all facilities.""" @@ -17,7 +21,7 @@ class FacilityBase(ABC): world = None # Skus in this facility. - skus: dict = None + skus: Dict[int, SkuInfo] = None # Product units for each sku in this facility. # Key is sku(product) id, value is the instance of product unit. @@ -32,6 +36,7 @@ class FacilityBase(ABC): # Upstream facilities. # Key is sku id, value is the list of product unit from upstream. upstreams: dict = None + downstreams: dict = None # Configuration of this facility. @@ -42,19 +47,28 @@ class FacilityBase(ABC): children: list = None + skus: dict = None + def __init__(self): self.upstreams = {} self.downstreams = {} self.children = [] + self.skus = {} + def parse_skus(self, configs: dict): """Parse sku information from config. Args: configs (dict): Configuration of skus belongs to this facility. """ - pass + for sku_name, sku_config in configs.items(): + global_sku = self.world.get_sku_by_name(sku_name) + facility_sku = SkuInfo(sku_config) + facility_sku.id = global_sku.id + + self.skus[global_sku.id] = facility_sku def parse_configs(self, configs: dict): """Parse configuration of this facility. diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index c236c9010..cbef45388 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -13,22 +13,6 @@ class RetailerFacility(FacilityBase): """Retail facility used to generate order from upstream, and sell products by demand.""" - SkuInfo = namedtuple( - "SkuInfo", - ( - "name", - "id", - "price", - "cost", - "init_stock", - "sale_gamma", - "order_cost", - "backlog_ratio", - "vlt", - "service_level" - ) - ) - # Product unit list of this facility. products: List[ProductUnit] @@ -37,22 +21,3 @@ class RetailerFacility(FacilityBase): def __init__(self): super().__init__() - self.skus = {} - - def parse_skus(self, configs: dict): - for sku_name, sku_config in configs.items(): - sku = self.world.get_sku_by_name(sku_name) - sku_info = RetailerFacility.SkuInfo( - sku_name, - sku.id, - sku_config.get("price", 0), - sku_config.get("cost", 0), - sku_config["init_stock"], - sku_config.get("sale_gamma", 0), - sku_config.get("order_cost", 0), - sku_config.get("backlog_ratio", 0), - sku_config.get("vlt", 1), - sku_config.get("service_level", 0.9) - ) - - self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index f9c58d0b0..ab1d165a2 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -13,23 +13,6 @@ class SupplierFacility(FacilityBase): """Supplier facilities used to produce products with material products.""" - SkuInfo = namedtuple( - "SkuInfo", - ( - "name", - "id", - "init_stock", - "type", - "cost", - "price", - "delay_order_penalty", - "product_unit_cost", - "order_cost", - "vlt", - "service_level" - ) - ) - # Storage unit of this facility. storage: StorageUnit @@ -40,25 +23,4 @@ class SupplierFacility(FacilityBase): products: List[ProductUnit] def __init__(self): - super().__init__() - - self.skus = {} - - def parse_skus(self, configs: dict): - for sku_name, sku_config in configs.items(): - sku = self.world.get_sku_by_name(sku_name) - sku_info = SupplierFacility.SkuInfo( - sku_name, - sku.id, - sku_config["init_stock"], - sku_config["type"], - sku_config.get("cost", 0), - sku_config.get("price", 0), - sku_config.get("delay_order_penalty", 0), - sku_config.get("product_unit_cost", 1), - sku_config.get("order_cost", 0), - sku_config.get("vlt", 1), - sku_config.get("service_level", 0.9) - ) - - self.skus[sku.id] = sku_info + super(SupplierFacility, self).__init__() diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index eaac2f669..9a56a1a85 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -13,16 +13,6 @@ class WarehouseFacility(FacilityBase): """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" - SkuInfo = namedtuple("SkuInfo", ("name", - "init_stock", - "id", - "price", - "delay_order_penalty", - "order_cost", - "vlt", - "service_level", - "cost")) - # Storage unit for this facility, must be a sub class of StorageUnit. storage: StorageUnit = None @@ -34,23 +24,3 @@ class WarehouseFacility(FacilityBase): def __init__(self): super().__init__() - - self.skus = {} - - def parse_skus(self, configs: dict): - for sku_name, sku_config in configs.items(): - sku = self.world.get_sku_by_name(sku_name) - - sku_info = WarehouseFacility.SkuInfo( - sku_name, - sku_config["init_stock"], - sku.id, - sku_config.get("price", 0), - sku_config.get("delay_order_penalty", 0), - sku_config.get("order_cost", 0), - sku_config.get("vlt", 1), - sku_config.get("service_level", 0.9), - sku_config.get("cost", 0) - ) - - self.skus[sku.id] = sku_info diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml index e346f6b9e..43667be78 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml @@ -139,8 +139,9 @@ world: init_stock: 100 product_unit_cost: 1 type: "production" # production means this is the output production of this facility - cost": 10 - price": 10 + cost: 10 + price: 10 + vlt: 1 # configuration of child units. children: @@ -164,11 +165,13 @@ world: type: "production" cost: 10 price: 100 + vlt: 1 sku3: init_stock: 100 type: "material" cost: 10 price: 100 + vlt: 1 children: storage: *small_storage @@ -184,12 +187,15 @@ world: sku1: init_stock: 1000 price: 100 + vlt: 1 sku2: init_stock: 1000 price: 100 + vlt: 1 sku3: init_stock: 1000 price: 100 + vlt: 1 children: storage: *huge_storage @@ -207,18 +213,21 @@ world: init_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 # optional + vlt: 1 sku3: price: 200 cost: 10 init_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 + vlt: 1 sku2: price: 100 cost: 10 init_stock: 100 sale_gamma: 100 backlog_ratio: 0.1 + vlt: 1 children: storage: *midium_storage @@ -265,7 +274,7 @@ world: - [10, 11] - [10, 12] - [11, 12] - + settings: global_reward_weight_producer: 0.50 global_reward_weight_consumer: 0.50 @@ -282,4 +291,4 @@ settings: constraint_violate_reward: -1e6 gamma: 0.99 tail_timesteps: 7 - heading_timesteps: 7 \ No newline at end of file + heading_timesteps: 7 diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index f172356c5..5973364cd 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -121,6 +121,7 @@ def initialize(self): for index, product_id in enumerate(self.product_list): product_number = self.product_number[index] + self.data_model.product_list.append(product_id) self.data_model.product_number.append(product_number) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 6a3e1658b..ea5da0359 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -8,13 +8,13 @@ import networkx as nx from maro.backends.frame import FrameBase +from .easy_config import EasyConfig, SkuInfo from .facilities import FacilityBase from .frame_builder import build_frame from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef from .units import UnitBase, ProductUnit -SkuInfo = namedtuple("SkuInfo", ("name", "id", "bom", "output_units_per_lot")) AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id")) @@ -62,18 +62,18 @@ def __init__(self): self.max_sources_per_facility = 0 self.max_price = 0 - def get_sku_by_name(self, name: str) -> SkuInfo: + def get_sku_by_name(self, name: str) -> EasyConfig: """Get sku information by name. Args: name (str): Sku name to query. Returns: - SkuInfo: General information for sku. + EasyConfig: General information for sku, used as a dict, but support use key as property. """ return self._sku_collection.get(name, None) - def get_sku_by_id(self, sku_id: int) -> SkuInfo: + def get_sku_by_id(self, sku_id: int) -> EasyConfig: """Get sku information by sku id. Args: @@ -146,7 +146,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio # Grab sku information for this world. for sku_conf in world_config["skus"]: - sku = SkuInfo(sku_conf["name"], sku_conf["id"], {}, sku_conf.get("output_units_per_lot", 1)) + sku = SkuInfo(sku_conf) self._sku_id2name_mapping[sku.id] = sku.name self._sku_collection[sku.name] = sku @@ -154,6 +154,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio # Collect bom info. for sku_conf in world_config["skus"]: sku = self._sku_collection[sku_conf["name"]] + sku.bom = {} bom = sku_conf.get("bom", {}) From 8d892038636b445db1f805cb8e79f63931c78f20 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 2 Apr 2021 17:56:58 +0800 Subject: [PATCH 139/482] more commens --- .gitignore | 1 + .../scenarios/supply_chain/easy_config.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 maro/simulator/scenarios/supply_chain/easy_config.py diff --git a/.gitignore b/.gitignore index 8f614f0c9..9b443cf3e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ maro_venv/ pyvenv.cfg htmlcov/ .coverage +*supply_chain_*/ \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/easy_config.py b/maro/simulator/scenarios/supply_chain/easy_config.py new file mode 100644 index 000000000..83e5f7551 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/easy_config.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +class EasyConfig(dict): + """A wrapper base on dictionary, give it ability to access key as property.""" + + # Default value for not exist keys. + default_values: dict = {} + + def __getattr__(self, key): + if key in self: + return self[key] + + if key in self.default_values: + return self.default_values[key] + + return None + + def __setattr__(self, key, value): + self[key] = value + + +class SkuInfo(EasyConfig): + """Sku information wrapper, with default property value.""" + default_values = { + "price": 0, + "vlt": 1, + "product_unit_cost": 0, + "backlog_ratio": 0, + "service_level": 0.90, + "cost": 0 + } From 5f805e8aace28e00b261921c2b97ac935f610655 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 2 Apr 2021 18:00:54 +0800 Subject: [PATCH 140/482] remove unused code --- .../supply_chain/facilities/facility.py | 14 +++++++------- .../supply_chain/facilities/retailer.py | 15 +-------------- .../supply_chain/facilities/supplier.py | 18 +----------------- .../supply_chain/facilities/warehouse.py | 18 +----------------- 4 files changed, 10 insertions(+), 55 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index dc06759d7..a5ec3749a 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -5,6 +5,8 @@ from abc import ABC from typing import List, Dict +from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit + from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo @@ -25,19 +27,19 @@ class FacilityBase(ABC): # Product units for each sku in this facility. # Key is sku(product) id, value is the instance of product unit. - products: dict = None + products: Dict[int, ProductUnit] = None # Storage unit in this facility. - storage = None + storage: StorageUnit = None # Distribution unit in this facility. - distribution = None + distribution: DistributionUnit = None # Upstream facilities. # Key is sku id, value is the list of product unit from upstream. - upstreams: dict = None + upstreams: Dict[int, List[ProductUnit]] = None - downstreams: dict = None + downstreams: Dict[int, List[ProductUnit]] = None # Configuration of this facility. configs: dict = None @@ -47,8 +49,6 @@ class FacilityBase(ABC): children: list = None - skus: dict = None - def __init__(self): self.upstreams = {} self.downstreams = {} diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py index cbef45388..6a08fc64d 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -2,22 +2,9 @@ # Licensed under the MIT license. -from collections import namedtuple -from typing import List - -from maro.simulator.scenarios.supply_chain.units import ProductUnit, StorageUnit - from .facility import FacilityBase class RetailerFacility(FacilityBase): """Retail facility used to generate order from upstream, and sell products by demand.""" - - # Product unit list of this facility. - products: List[ProductUnit] - - # Storage unit of this facility. - storage: StorageUnit - - def __init__(self): - super().__init__() + pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py index ab1d165a2..7ddface02 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -2,25 +2,9 @@ # Licensed under the MIT license. -from collections import namedtuple -from typing import List - -from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit - from .facility import FacilityBase class SupplierFacility(FacilityBase): """Supplier facilities used to produce products with material products.""" - - # Storage unit of this facility. - storage: StorageUnit - - # Distribution unit of this facility. - distribution: DistributionUnit - - # Product unit list of this facility. - products: List[ProductUnit] - - def __init__(self): - super(SupplierFacility, self).__init__() + pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py index 9a56a1a85..97725d171 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -2,25 +2,9 @@ # Licensed under the MIT license. -from collections import namedtuple -from typing import List - -from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit - from .facility import FacilityBase class WarehouseFacility(FacilityBase): """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" - - # Storage unit for this facility, must be a sub class of StorageUnit. - storage: StorageUnit = None - - # Distribution unit for this facility. - distribution: DistributionUnit = None - - # Product unit list for this facility. - products: List[ProductUnit] = None - - def __init__(self): - super().__init__() + pass From 75902930b6fa25e09758766fd5ad1524c543580c Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 2 Apr 2021 18:28:40 +0800 Subject: [PATCH 141/482] go through for 1st time --- .../supply_chain/facilities/facility.py | 33 ++++++++++----- .../scenarios/supply_chain/units/__init__.py | 2 +- .../scenarios/supply_chain/units/consumer.py | 25 ++++++------ .../supply_chain/units/distribution.py | 40 +++++++++++++------ .../supply_chain/units/manufacture.py | 16 ++------ .../scenarios/supply_chain/units/product.py | 29 +++++++++----- .../scenarios/supply_chain/units/seller.py | 7 ++-- .../scenarios/supply_chain/units/vehicle.py | 5 +-- 8 files changed, 90 insertions(+), 67 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index a5ec3749a..d1815512f 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -1,13 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import defaultdict from abc import ABC +from collections import defaultdict from typing import List, Dict -from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit - from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo +from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit class FacilityBase(ABC): @@ -39,22 +38,25 @@ class FacilityBase(ABC): # Key is sku id, value is the list of product unit from upstream. upstreams: Dict[int, List[ProductUnit]] = None + # Down stream facilities, value same as upstreams. downstreams: Dict[int, List[ProductUnit]] = None # Configuration of this facility. configs: dict = None + # Name of data model, from configuration. data_model_name: str = None + + # Index of the data model node. data_model_index: int = 0 + # Children of this facility (valid units). children: list = None def __init__(self): self.upstreams = {} self.downstreams = {} - self.children = [] - self.skus = {} def parse_skus(self, configs: dict): @@ -78,16 +80,23 @@ def parse_configs(self, configs: dict): """ self.configs = configs - def get_config(self, key: str, default: object = None): + def get_config(self, key: str, default: object = None) -> object: + """Get specified configuration of facility. + + Args: + key (str): Key of the configuration. + default (object): Default value if key not exist, default is None. + + Returns: + object: value in configuration. + """ return default if self.configs is None else self.configs.get(key, default) def initialize(self): """Initialize this facility after frame is ready.""" - has_storage = self.storage is not None - has_distribution = self.distribution is not None - self.data_model.initialize() + # Put valid units into the children, used to simplify following usage. if self.storage is not None: self.children.append(self.storage) @@ -149,6 +158,8 @@ def get_node_info(self) -> dict: }, "configs": self.configs, "skus": self.skus, - "upstreams": { product_id: [f.id for f in source_list] for product_id, source_list in self.upstreams.items()}, - "downstreams": { product_id: [f.id for f in source_list] for product_id, source_list in self.downstreams.items() } + "upstreams": {product_id: [f.id for f in source_list] for product_id, source_list in + self.upstreams.items()}, + "downstreams": {product_id: [f.id for f in source_list] for product_id, source_list in + self.downstreams.items()} } diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index 644bb72c1..86d9717fe 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -9,6 +9,6 @@ from .seller import SellerUnit from .skuunit import SkuUnit from .storage import StorageUnit +from .storeproduct import StoreProductUnit from .unitbase import UnitBase from .vehicle import VehicleUnit -from .storeproduct import StoreProductUnit \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index f6659da39..5ea7450ee 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -4,7 +4,9 @@ import warnings from collections import Counter, defaultdict + from scipy.ndimage.interpolation import shift + from .order import Order from .skuunit import SkuUnit @@ -108,13 +110,8 @@ def step(self, tick: int): self.purchased = self.action.quantity - self.data_model.latest_consumptions = 1.0 - if order.vlt < len(self.pending_order_daily): - self.pending_order_daily[order.vlt-1] += order.quantity - - if self.order_product_cost > 0: - self.data_model.order_product_cost = self.order_product_cost + self.pending_order_daily[order.vlt - 1] += order.quantity def flush_states(self): if self.received > 0: @@ -124,12 +121,22 @@ def flush_states(self): if self.purchased > 0: self.data_model.purchased = self.purchased self.data_model.total_purchased += self.purchased + self.data_model.latest_consumptions = 1.0 + + if self.order_product_cost > 0: + self.data_model.order_product_cost = self.order_product_cost + + if self.action is not None and self.action.quantity > 0: + self.data_model.order_quantity = self.action def post_step(self, tick: int): # Clear the action states per step. if self.action is not None: self.data_model.latest_consumptions = 0 + if self.action.quantity > 0: + self.data_model.order_quantity = 0 + # This will set action to None. super(ConsumerUnit, self).post_step(tick) @@ -151,12 +158,6 @@ def reset(self): self.open_orders.clear() - def set_action(self, action: object): - super(ConsumerUnit, self).set_action(action) - - if action.product_id > 0 and action.quantity > 0: - self.data_model.order_quantity = action.quantity - def get_in_transit_quantity(self): quantity = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 83f7a0cea..0f3b4ff93 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -3,10 +3,11 @@ from collections import defaultdict, deque, Counter -from typing import Dict +from typing import Dict, List from .order import Order from .unitbase import UnitBase +from .vehicle import VehicleUnit class DistributionUnit(UnitBase): @@ -14,8 +15,8 @@ class DistributionUnit(UnitBase): One distribution can accept all kind of sku order. """ - # Transport unit list of this distribution unit. - vehicles = None + # Vehicle unit list of this distribution unit. + vehicles: List[VehicleUnit] = None def __init__(self): super().__init__() @@ -25,6 +26,10 @@ def __init__(self): self.delay_order_penalty = Counter() self.check_in_order = Counter() + self.base_delay_order_penalty = 0 + + self._is_order_changed = False + def get_pending_order(self) -> Dict[int, int]: """Get orders that states is pending. @@ -55,6 +60,8 @@ def place_order(self, order: Order) -> int: sku = self.facility.skus[order.product_id] if sku is not None: + self._is_order_changed = True + self.order_queue.append(order) order_total_price = sku.price * order.quantity @@ -68,9 +75,11 @@ def place_order(self, order: Order) -> int: def initialize(self): super(DistributionUnit, self).initialize() + self.base_delay_order_penalty = self.facility.get_config("delay_order_penalty", 0) + def step(self, tick: int): for vehicle in self.vehicles: - # If we have vehicle not on the way and there is any pending order + # If we have vehicle not on the way and there is any pending order. if len(self.order_queue) > 0 and vehicle.quantity == 0: order = self.order_queue.popleft() @@ -83,32 +92,37 @@ def step(self, tick: int): order.vlt ) + self._is_order_changed = True + # Push vehicle. vehicle.step(tick) self.transportation_cost[vehicle.product_id] += abs(vehicle.cost) - # update order's delay penalty per tick. + # Update order's delay penalty per tick. for order in self.order_queue: - self.delay_order_penalty[order.product_id] += self.facility.get_config("delay_order_penalty") + self.delay_order_penalty[order.product_id] += self.base_delay_order_penalty def flush_states(self): + super(DistributionUnit, self).flush_states() + for vehicle in self.vehicles: vehicle.flush_states() - # TODO: optimize it later, only update if there is any changes - self.data_model.remaining_order_quantity = sum(order.quantity for order in self.order_queue) - self.data_model.remaining_order_number = len(self.order_queue) + if self._is_order_changed: + self._is_order_changed = False + + self.data_model.remaining_order_quantity = sum(order.quantity for order in self.order_queue) + self.data_model.remaining_order_number = len(self.order_queue) def reset(self): super(DistributionUnit, self).reset() self.order_queue.clear() + self.transportation_cost.clear() + self.check_in_order.clear() + self.delay_order_penalty.clear() # Reset vehicles. for vehicle in self.vehicles: vehicle.reset() - - self.transportation_cost.clear() - self.check_in_order.clear() - self.delay_order_penalty.clear() diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 942972aa0..228828922 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -2,8 +2,6 @@ # Licensed under the MIT license. -from maro.simulator.scenarios.supply_chain.actions import ManufactureAction - from .skuunit import SkuUnit @@ -14,19 +12,16 @@ class ManufactureUnit(SkuUnit): """ # Source material sku and related number per produce cycle. - bom = None + bom: dict = None # How many production unit each produce cycle. - output_units_per_lot = None + output_units_per_lot: int = None # How many unit we will consume each produce cycle. - input_units_per_lot = 0 + input_units_per_lot: int = 0 # How many we procedure per current step. - manufacture_number = 0 - - def __init__(self): - super().__init__() + manufacture_number: int = 0 def initialize(self): super(ManufactureUnit, self).initialize() @@ -98,6 +93,3 @@ def post_step(self, tick: int): # NOTE: call super at last, since it will clear the action. super(ManufactureUnit, self).post_step(tick) - - def set_action(self, action: ManufactureAction): - super(ManufactureUnit, self).set_action(action) diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index 456e9fc07..a8542a82a 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -2,12 +2,14 @@ # Licensed under the MIT license. import numpy as np + from .consumer import ConsumerUnit +from .distribution import DistributionUnit from .manufacture import ManufactureUnit from .seller import SellerUnit from .skuunit import SkuUnit from .storage import StorageUnit -from .distribution import DistributionUnit + class ProductUnit(SkuUnit): """Unit that used to group units of one special sku, usually contains consumer, seller and manufacture.""" @@ -26,13 +28,12 @@ class ProductUnit(SkuUnit): distribution: DistributionUnit = None - def __init__(self): - super().__init__() - def initialize(self): super(ProductUnit, self).initialize() - self.data_model.initialize(self.get_selling_price()) + facility_sku = self.facility.skus[self.product_id] + + self.data_model.initialize(facility_sku.price) def step(self, tick: int): for unit in self.children: @@ -68,6 +69,7 @@ def get_unit_info(self) -> dict: "manufacture": self.manufacture.get_unit_info() if self.manufacture is not None else None } + # TODO: add following field into states. def get_latest_sale(self): sale = 0 downstreams = self.facility.downstreams.get(self.product_id, []) @@ -114,25 +116,29 @@ def _get_max_vlt(self): source_vlt = source_facility.skus[self.product_id].vlt - vlt = max(vlt, source_vlt) + vlt = max(vlt, source_vlt) return vlt @staticmethod - def generate(facility, config: dict, unit_def): + def generate(facility, config: dict, unit_def: object): """Generate product unit by sku information. Args: facility (FacilityBase): Facility this product belongs to. config (dict): Config of children unit. + unit_def (object): Definition of the unit (from config). + + Returns: + dict: Dictionary of product unit, key is the product id, value if ProductUnit. """ - instance_list = {} + products_dict = {} if facility.skus is not None and len(facility.skus) > 0: world = facility.world for sku_id, sku in facility.skus.items(): - sku_type = getattr(sku, "type", None) + sku_type = sku.type product_unit: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility, unit_def) product_unit.product_id = sku_id @@ -149,6 +155,7 @@ def generate(facility, config: dict, unit_def): if sku_type != "production" and child_name == "manufacture": continue + # We produce the product, so we do not need to purchase it. if sku_type == "production" and child_name == "consumer": continue @@ -162,6 +169,6 @@ def generate(facility, config: dict, unit_def): product_unit.children.append(child_unit) - instance_list[sku_id] = product_unit + products_dict[sku_id] = product_unit - return instance_list + return products_dict diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index f69ae07c5..23475aac3 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -92,11 +92,10 @@ def post_step(self, tick: int): def reset(self): super(SellerUnit, self).reset() - # TODO: regenerate the demand distribution? - # self.demand_distribution.clear() + self.demand_distribution.clear() - # for _ in range(self.durations): - # self.demand_distribution.append(np.random.gamma(self.gamma)) + for _ in range(self.durations): + self.demand_distribution.append(np.random.gamma(self.gamma)) def sale_mean(self): return np.mean(self.sale_hist) diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index 9493e609b..92ff867ed 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. -from .product import ProductUnit from .unitbase import UnitBase @@ -15,7 +14,7 @@ def __init__(self): self.max_patient: int = None # Current products' destination. - self.destination: ProductUnit = None + self.destination = None # Path to destination. self.path: list = None @@ -30,7 +29,7 @@ def __init__(self): self.payload = 0 # Which product unit current product related to. - self.product: ProductUnit = None + self.product = None # Current location in the path. self.location = 0 From 190c0af4b3f950ee12cbf83dbafe4578bca29e30 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 2 Apr 2021 18:49:59 +0800 Subject: [PATCH 142/482] refine storage logic --- examples/hello_world/supply_chain/hello.py | 2 +- .../supply_chain/datamodels/storage.py | 20 ++++- .../scenarios/supply_chain/units/storage.py | 85 +++++++++---------- 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index ba357a333..73e5945fe 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -16,7 +16,7 @@ def main(): matrics = None is_done = False - print(env.agent_idx_list) + # print(env.agent_idx_list) for ep in range(total_episodes): print("Current episode:", ep) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py index 07bf57e2b..4e9aa71f1 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/storage.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/storage.py @@ -23,10 +23,18 @@ def __init__(self): self._capacity = 0 self._remaining_space = None - - def initialize(self, capacity: int = 0, remaining_space: int = None): + self._product_list = None + self._product_number = None + + def initialize(self, + capacity: int = 0, + remaining_space: int = None, + product_list: list = None, + product_number: list = None): self._capacity = capacity self._remaining_space = remaining_space + self._product_list = product_list + self._product_number = product_number self.reset() @@ -39,3 +47,11 @@ def reset(self): self.remaining_space = self._remaining_space else: self.remaining_space = self._capacity + + if self._product_list is not None: + for id in self._product_list: + self.product_list.append(id) + + if self._product_number is not None: + for n in self._product_number: + self.product_number.append(n) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 5973364cd..2e3ff5190 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -14,13 +14,17 @@ def __init__(self): super().__init__() # We use these variables to hold changes at python side, flash to frame before taking snapshot. - self.product_number = [] - self.product_list = [] + # self.product_number = [] + # self.product_list = [] # Used to map from product id to slot index. - self.product_index_mapping: Dict[int, int] = {} + # self.product_index_mapping: Dict[int, int] = {} self.capacity = 0 self.remaining_space = 0 + self.product_level = {} + + # Which product's number has changed. + self._changed_product_cache = {} def try_add_products(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: """Try to add products into storage. @@ -40,10 +44,11 @@ def try_add_products(self, product_quantities: Dict[int, int], all_or_nothing=Tr for product_id, quantity in product_quantities.items(): unload_quantity = min(self.remaining_space, quantity) - product_index = self.product_index_mapping[product_id] - self.product_number[product_index] += unload_quantity + self.product_level[product_id] += unload_quantity unloaded_quantities[product_id] = unload_quantity + self._changed_product_cache[product_id] = True + self.remaining_space -= unload_quantity return unloaded_quantities @@ -59,17 +64,13 @@ def try_take_products(self, product_quantities: Dict[int, int]) -> bool: """ # Check if we can take all kinds of products? for product_id, quantity in product_quantities.items(): - product_index = self.product_index_mapping[product_id] - - if self.product_number[product_index] < quantity: + if self.product_level[product_id] < quantity: return False - # TODO: refactoring for dup code # Take from storage. for product_id, quantity in product_quantities.items(): - product_index = self.product_index_mapping[product_id] - - self.product_number[product_index] -= quantity + self.product_level[product_id] -= quantity + self._changed_product_cache[product_id] = True self.remaining_space += quantity @@ -85,11 +86,11 @@ def take_available(self, product_id: int, quantity: int) -> int: Returns: int: Actual quantity taken. """ - product_index = self.product_index_mapping[product_id] - available = self.product_number[product_index] + available = self.product_level[product_id] actual = min(available, quantity) - self.product_number[product_index] -= actual + self.product_level[product_id] -= actual + self._changed_product_cache[product_id] = True self.remaining_space += actual @@ -104,57 +105,47 @@ def get_product_number(self, product_id: int) -> int: Returns: int: Available number of product. """ - product_index = self.product_index_mapping[product_id] - - return self.product_number[product_index] + return self.product_level[product_id] def initialize(self): super(StorageUnit, self).initialize() self.capacity = self.config.get("capacity", 100) - - for sku in self.facility.skus.values(): - self.product_list.append(sku.id) - self.product_number.append(sku.init_stock) - self.remaining_space = self.capacity - for index, product_id in enumerate(self.product_list): - product_number = self.product_number[index] - - self.data_model.product_list.append(product_id) - self.data_model.product_number.append(product_number) - - self.product_index_mapping[product_id] = index + for sku in self.facility.skus.values(): + self.product_level[sku.id] = sku.init_stock + self._changed_product_cache[sku.id] = False - self.remaining_space -= product_number + self.remaining_space -= sku.init_stock self.data_model.initialize( capacity=self.capacity, - remaining_space=self.remaining_space + remaining_space=self.remaining_space, + product_list=[id for id in self.product_level.keys()], + product_number=[n for n in self.product_level.values()] ) def flush_states(self): # Write the changes to frame. - for i, number in enumerate(self.product_number): - self.data_model.product_number[i] = self.product_number[i] + i = 0 + has_changes = False + for product_id, product_number in self.product_level.items(): + if self._changed_product_cache[product_id]: + has_changes = True + self._changed_product_cache[product_id] = False + + self.data_model.product_number[i] = product_number + i += 1 - self.data_model.remaining_space = self.remaining_space + if has_changes: + self.data_model.remaining_space = self.remaining_space def reset(self): super(StorageUnit, self).reset() - self.product_number.clear() - - for sku in self.facility.skus.values(): - self.product_number.append(sku.init_stock) - self.remaining_space = self.capacity - for index, product_id in enumerate(self.product_list): - product_number = self.product_number[index] - - self.data_model.product_list.append(product_id) - self.data_model.product_number.append(product_number) - - self.remaining_space -= product_number + for sku in self.facility.skus.values(): + self.product_level[sku.id] = sku.init_stock + self.remaining_space -= sku.init_stock From a546344e0f1d49ffc05490bd80dda6bd72d1472e Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 2 Apr 2021 18:53:11 +0800 Subject: [PATCH 143/482] format --- maro/simulator/scenarios/supply_chain/units/unitbase.py | 1 - maro/simulator/scenarios/supply_chain/units/vehicle.py | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py index 521ad9072..6563a5aaa 100644 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -50,7 +50,6 @@ class UnitBase: # Current unit configurations. config: dict = None - def __init__(self): pass diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index 92ff867ed..39cb2864a 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -88,8 +88,8 @@ def try_load(self, quantity: int) -> bool: self.data_model.payload = quantity return True - else: - return False + + return False def try_unload(self): """Try unload products into destination's storage.""" @@ -171,9 +171,6 @@ def step(self, tick: int): self.cost = self.payload * self.unit_transport_cost - def flush_states(self): - pass - def reset(self): super(VehicleUnit, self).reset() From 6a37e91d984b3d6b583df7b50f99d86b62fa97f5 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Fri, 2 Apr 2021 18:58:50 +0800 Subject: [PATCH 144/482] format --- .../scenarios/supply_chain/business_engine.py | 1 + maro/simulator/scenarios/supply_chain/world.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 3c9ee8db3..ad4b1cdff 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -23,6 +23,7 @@ def __init__(self, **kwargs): self._product_units = [] + # Prepare product unit for later using. for unit in self.world.units.values(): if type(unit) == ProductUnit: self._product_units.append(unit) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index ea5da0359..e0eb797cf 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -6,10 +6,9 @@ from typing import List, Tuple, Union import networkx as nx - from maro.backends.frame import FrameBase -from .easy_config import EasyConfig, SkuInfo +from .easy_config import EasyConfig, SkuInfo from .facilities import FacilityBase from .frame_builder import build_frame from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef @@ -238,7 +237,6 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio for unit in self.units.values(): unit.initialize() - # TODO: replace tcod with other lib. # Construct the map grid. grid_config = world_config["grid"] @@ -259,9 +257,9 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio facility.y = pos[1] pos = tuple(pos) - # Neighbors to facility will have hight cost. + # Neighbors to facility will have high cost. for npos in ((pos[0] - 1, pos[1]), (pos[0] + 1, pos[1]), (pos[0], pos[1] - 1), (pos[0], pos[1] + 1)): - if npos[0] >= 0 and npos[0] < grid_width and npos[1] >= 0 and npos[1] < grid_height: + if 0 <= npos[0] < grid_width and 0 <= npos[1] < grid_height: edge_weights[(npos, pos)] = 4 nx.set_edge_attributes(self._graph, edge_weights, "cost") @@ -281,12 +279,14 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio if type(unit) == ProductUnit: agent_type = unit.config["agent_type"] - # unit or failicty id, agent type, is facility, sku info, facility id - self.agent_list.append(AgentInfo(unit.id, agent_type, False, unit.facility.skus[unit.product_id], unit.facility.id)) + # unit or facility id, agent type, is facility, sku info, facility id + self.agent_list.append( + AgentInfo(unit.id, agent_type, False, unit.facility.skus[unit.product_id], unit.facility.id)) self.agent_type_dict[agent_type] = True - def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase, unit_def: UnitDef): + def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase, + unit_def: UnitDef): unit = unit_type() unit.id = self._gen_id() From 5d259bd5b287b120a649fb78deebbd9470876f34 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Apr 2021 14:41:47 +0000 Subject: [PATCH 145/482] added missing file --- maro/rl/agent/multi_agent_wrapper.py | 102 +++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 maro/rl/agent/multi_agent_wrapper.py diff --git a/maro/rl/agent/multi_agent_wrapper.py b/maro/rl/agent/multi_agent_wrapper.py new file mode 100644 index 000000000..72eb87602 --- /dev/null +++ b/maro/rl/agent/multi_agent_wrapper.py @@ -0,0 +1,102 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +from typing import List, Union + +from .abs_agent import AbsAgent + + +class MultiAgentWrapper: + """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. + + Args: + agent_dict (Union[AbsAgent, dict]): A single agent or a homogeneous set of agents that have the same + method signatures. + """ + def __init__(self, agent_dict: Union[AbsAgent, dict]): + if isinstance(agent_dict, AbsAgent): + agent_dict = {"AGENT": agent_dict} + self.agent_dict = agent_dict + self._names = list(self.agent_dict.keys()) + + def __getitem__(self, agent_id): + if len(self.agent_dict) == 1: + return self.agent_dict["AGENT"] + else: + return self.agent_dict[agent_id] + + def __len__(self): + return len(self.agent_dict) + + @property + def names(self): + return self._names + + def choose_action(self, state_by_agent: dict): + return {agent_id: self.agent_dict[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} + + def set_exploration_params(self, params): + # Per-agent exploration parameters + if isinstance(params, dict) and params.keys() <= self.agent_dict.keys(): + for agent_id, params in params.items(): + self.agent_dict[agent_id].set_exploration_params(**params) + # Shared exploration parameters for all agents + else: + for agent in self.agent_dict.values(): + agent.set_exploration_params(**params) + + def store_experiences(self, experiences: dict): + """Store experiences in the agents' experience memory. + + The top-level keys of ``experiences`` will be treated as agent IDs. + """ + for agent_id, exp in experiences.items(): + self.agent_dict[agent_id].store_experiences(exp) + + def learn(self, agent_ids=None): + if agent_ids is None: + for agent in self.agent_dict.values(): + agent.learn() + elif not isinstance(agent_ids, list): + self.agent_dict[agent_ids].learn() + else: + for agent_id in agent_ids: + self.agent_dict[agent_id].learn() + + def load_model(self, model_dict: dict): + """Load models from memory for each agent.""" + for agent_id, model in model_dict.items(): + self.agent_dict[agent_id].load_model(model) + + def dump_model(self, agent_ids=None): + """Get agents' underlying models. + + This is usually used in distributed mode where models need to be broadcast to remote roll-out actors. + """ + if agent_ids is None: + return {agent_id: agent.dump_model() for agent_id, agent in self.agent_dict.items()} + elif not isinstance(agent_ids, list): + return self.agent_dict[agent_ids].dump_model() + else: + return {agent_id: self.agent_dict[agent_id].dump_model() for agent_id in self.agent_dict} + + def load_model_from_file(self, dir_path): + """Load models from disk for each agent.""" + for agent_id, agent in self.agent_dict.items(): + agent.load_model_from_file(os.path.join(dir_path, agent_id)) + + def dump_model_to_file(self, dir_path: str, agent_ids=None): + """Dump agents' models to disk. + + Each agent will use its own name to create a separate file under ``dir_path`` for dumping. + """ + os.makedirs(dir_path, exist_ok=True) + if agent_ids is None: + for agent_id, agent in self.agent_dict.items(): + agent.dump_model_to_file(os.path.join(dir_path, agent_id)) + elif not isinstance(agent_ids, list): + self.agent_dict[agent_ids].dump_model_to_file(os.path.join(dir_path, agent_ids)) + else: + for agent_id in agent_ids: + self.agent_dict[agent_id].dump_model_to_file(os.path.join(dir_path, agent_id)) From 8fa33644e7a7a7654e686ddc3c237b51b17d1170 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Apr 2021 23:13:43 +0800 Subject: [PATCH 146/482] bug fix --- maro/rl/training/env_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index f74ec9013..4bb1e76d6 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -87,7 +87,7 @@ def step(self, action_by_agent: dict): if len(env_action) == 1: env_action = list(env_action.values())[0] t1 = time.time() - _, event, done = self.env.step(None) + _, event, done = self.env.step(env_action) t2 = time.time() self._tot_raw_step_time += t2 - t1 From bfc1ce17023ab1b81e199ece61bad2470dcf94e6 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Apr 2021 16:09:55 +0000 Subject: [PATCH 147/482] learner logic revamp in progress --- examples/cim/ac/config.yml | 9 +- examples/cim/ac/main.py | 10 +- examples/cim/env_wrapper.py | 13 +- .../hello_world/supply_chain/env_wrapper.py | 7 +- examples/supply_chain/dqn/config.yml | 15 +- .../dqn/single_thread_launcher.py | 8 +- maro/rl/__init__.py | 4 +- maro/rl/agent/abs_agent.py | 35 ++++- maro/rl/agent/ac.py | 52 ++++--- maro/rl/agent/dqn.py | 101 ++++++++----- maro/rl/distributed/actor_proxy.py | 18 ++- maro/rl/distributed/learner.py | 39 +++-- maro/rl/storage/abs_store.py | 14 -- maro/rl/storage/sampler.py | 58 ++++++++ maro/rl/storage/simple_store.py | 75 ++-------- maro/rl/training/__init__.py | 4 +- maro/rl/training/env_wrapper.py | 110 ++++++-------- maro/rl/training/learner.py | 140 ++++++------------ maro/rl/training/test.py | 17 +++ 19 files changed, 382 insertions(+), 347 deletions(-) create mode 100644 maro/rl/storage/sampler.py create mode 100644 maro/rl/training/test.py diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index 25c62cce0..afe688e4a 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -57,6 +57,8 @@ agent: lr: 0.001 hyper_params: reward_discount: .0 + experience_memory_size: -1 + experience_memory_overwrite_type: "rolling" critic_loss_cls: smooth_l1 train_iters: 10 actor_loss_coefficient: 0.1 @@ -66,4 +68,9 @@ training: scenario: cim topology: toy.4p_ssdd_l0.0 durations: 1120 - max_episode: 50 \ No newline at end of file + max_episode: 50 + learning_interval: + 0: 200 + 1: 200 + 2: 400 + 3: 200 diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index 383f19195..a6c7e83cb 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -8,8 +8,8 @@ import numpy as np from maro.rl import ( - Actor, ActorCritic, ActorCriticConfig, FullyConnectedBlock, MultiAgentWrapper, SimpleMultiHeadModel, - OnPolicyLearner, OptimOption + Actor, ActorCritic, ActorCriticConfig, FullyConnectedBlock, MultiAgentWrapper, Learner, Scheduler, + SimpleMultiHeadModel, OptimOption ) from maro.simulator import Env from maro.utils import set_seeds @@ -49,5 +49,9 @@ def get_ac_agent(): set_seeds(1024) # for reproducibility env = Env(**config["training"]["env"]) agent = MultiAgentWrapper({name: get_ac_agent() for name in env.agent_idx_list}) - learner = OnPolicyLearner(CIMEnvWrapper(env, **config["shaping"]), agent, config["training"]["max_episode"]) + scheduler = Scheduler(config["training"]["max_episode"]) + learner = Learner( + CIMEnvWrapper(env, **config["shaping"]), agent, scheduler, + learning_interval=config["training"]["learning_interval"] + ) learner.run() diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index 838c33cb3..14d5c8768 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -63,7 +63,7 @@ def get_action(self, action_by_agent): return {port: Action(vessel, port, actual_action, action_type)} - def get_reward(self, tick=None): + def get_reward(self, tick=None, target_agents=None): """Delayed reward evaluation.""" if tick is None: tick = self.env.tick @@ -78,7 +78,10 @@ def get_reward(self, tick=None): for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay) ] - return np.float32( - self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - - self.shortage_factor * np.dot(future_shortage, decay_list) - ) + return { + target_agents[0]: + np.float32( + self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - + self.shortage_factor * np.dot(future_shortage, decay_list) + ) + } diff --git a/examples/hello_world/supply_chain/env_wrapper.py b/examples/hello_world/supply_chain/env_wrapper.py index af72d762a..fa1ab4d10 100644 --- a/examples/hello_world/supply_chain/env_wrapper.py +++ b/examples/hello_world/supply_chain/env_wrapper.py @@ -519,11 +519,14 @@ def _serialize_state(self, state): start_tick=start_tick, durations=durations) ss = SCEnvWrapper(env) - env.step(None) states = ss.get_state(None) rewards = ss.get_reward(None) - print(states) + for id_, state in states["consumer"].items(): + print(id_, state.shape) + + for id_, state in states["producer"].items(): + print(id_, state.shape) print(rewards) diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index dbfd48c18..43f9b66f1 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -18,6 +18,11 @@ agent: lr: 0.001 hyper_params: reward_discount: .0 + experience_memory_size: 50000 + experience_memory_overwrite_type: random + min_experiences: 10 + train_iters: 10 + batch_size: 128 loss_cls: smooth_l1 target_update_freq: 5 tau: 0.1 @@ -26,15 +31,9 @@ training: env: scenario: supply_chain topology: sample1 - durations: 100 + durations: 200 max_episode: 10 - min_experiences_to_train: 10 - train_iter: 10 - batch_size: 128 - replay_memory: - size: 50000 - overwrite_type: random - # prioritized_sampling_by_loss: true + learning_interval: 45 exploration: parameter_names: - epsilon diff --git a/examples/supply_chain/dqn/single_thread_launcher.py b/examples/supply_chain/dqn/single_thread_launcher.py index bd52c45a5..ab422c4f0 100644 --- a/examples/supply_chain/dqn/single_thread_launcher.py +++ b/examples/supply_chain/dqn/single_thread_launcher.py @@ -7,7 +7,7 @@ import numpy as np -from maro.rl import LinearParameterScheduler, OffPolicyLearner +from maro.rl import Learner, LinearParameterScheduler from maro.simulator import Env from maro.utils import set_seeds @@ -24,9 +24,7 @@ env = Env(**config["training"]["env"]) agent = get_sc_agents(env.agent_idx_list, config["agent"]) scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) - learner = OffPolicyLearner( - SCEnvWrapper(env), agent, scheduler, - replay_memory_size=config["training"]["replay_memory"]["size"], - replay_memory_overwrite_type=config["training"]["replay_memory"]["overwrite_type"] + learner = Learner( + SCEnvWrapper(env), agent, scheduler, learning_interval=config["training"][""] ) learner.run() diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 3fc568907..8b820ee01 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -11,7 +11,7 @@ from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler from maro.rl.storage import AbsStore, SimpleStore -from maro.rl.training import AbsEnvWrapper, AbsLearner, OffPolicyLearner, OnPolicyLearner +from maro.rl.training import AbsEnvWrapper, Learner from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward, select_by_actions @@ -25,7 +25,7 @@ "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", "AbsStore", "SimpleStore", - "AbsEnvWrapper", "AbsLearner", "OffPolicyLearner", "OnPolicyLearner", + "AbsEnvWrapper", "Learner", "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward", "select_by_actions" ] diff --git a/maro/rl/agent/abs_agent.py b/maro/rl/agent/abs_agent.py index 377f88a7e..c7ec57771 100644 --- a/maro/rl/agent/abs_agent.py +++ b/maro/rl/agent/abs_agent.py @@ -6,6 +6,26 @@ import torch from maro.rl.model import AbsCoreModel +from maro.rl.storage import SimpleStore + + +class AgentConfig: + """Configuration for the DQN algorithm. + + Args: + reward_discount (float): Reward decay as defined in standard RL terminology. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + + """ + __slots__ = ["reward_discount", "experience_memory_size", "experience_memory_overwrite_type"] + + def __init__(self, reward_discount: float, experience_memory_size: int, experience_memory_overwrite_type: str): + self.reward_discount = reward_discount + self.experience_memory_size = experience_memory_size + self.experience_memory_overwrite_type = experience_memory_overwrite_type class AbsAgent(ABC): @@ -21,9 +41,14 @@ class AbsAgent(ABC): model (AbsCoreModel): Task model or container of task models required by the algorithm. config: Settings for the algorithm. """ - def __init__(self, model: AbsCoreModel, config): + def __init__(self, model: AbsCoreModel, config: AgentConfig): self.model = model self.config = config + self.experience_memory = SimpleStore( + ["S", "A", "R", "S_"], + capacity=self.config.experience_memory_size, + overwrite_type=self.config.experience_memory_overwrite_type + ) self.device = None def to_device(self, device): @@ -46,8 +71,14 @@ def choose_action(self, state): def set_exploration_params(self, **params): pass + def store_experiences(self, experiences: dict): + """Pull experiences from the replay memory stored by an environment wrapper.""" + if set(experiences) != {"S", "A", "R", "S_"}: + raise ValueError("The keys of experiences must be {'S', 'A', 'R', 'S_'}") + self.experience_memory.put(experiences) + @abstractmethod - def learn(self, *args, **kwargs): + def learn(self): """Algorithm-specific training logic. The parameters are data to train the underlying model on. Algorithm-specific loss and optimization diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py index 8460a7764..175ee9f90 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/agent/ac.py @@ -12,14 +12,18 @@ from maro.rl.utils import get_log_prob, get_torch_loss_cls from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent +from .abs_agent import AbsAgent, AgentConfig -class ActorCriticConfig: +class ActorCriticConfig(AgentConfig): """Configuration for the Actor-Critic algorithm. Args: reward_discount (float): Reward decay as defined in standard RL terminology. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". train_iters (int): Number of gradient descent steps per call to ``train``. @@ -29,22 +33,27 @@ class ActorCriticConfig: in which case the actor loss is calculated using the usual policy gradient theorem. """ __slots__ = [ - "reward_discount", "critic_loss_func", "train_iters", "actor_loss_coefficient", "k", "lam", "clip_ratio" + "critic_loss_func", "train_iters", "actor_loss_coefficient", "k", "lam", "clip_ratio", + "flush_experience_memory_after_training" ] def __init__( self, reward_discount: float, + experience_memory_size: int, + experience_memory_overwrite_type: str, train_iters: int, critic_loss_cls="mse", actor_loss_coefficient: float = 1.0, - clip_ratio: float = None + clip_ratio: float = None, + flush_experience_memory_after_training: bool = True ): - self.reward_discount = reward_discount + super().__init__(reward_discount, experience_memory_size, experience_memory_overwrite_type) self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.train_iters = train_iters self.actor_loss_coefficient = actor_loss_coefficient self.clip_ratio = clip_ratio + self.flush_experience_memory_after_training = flush_experience_memory_after_training class ActorCritic(AbsAgent): @@ -84,19 +93,26 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: action, log_p = action.cpu().numpy(), log_p.cpu().numpy() return (action[0], log_p[0]) if is_single else (action, log_p) - def learn( - self, - states: np.ndarray, - actions: np.ndarray, - log_p: np.ndarray, - rewards: np.ndarray, - next_states: np.ndarray - ): - states = torch.from_numpy(states).to(self.device) - actions = torch.from_numpy(actions).to(self.device) - log_p = torch.from_numpy(log_p).to(self.device) - rewards = torch.from_numpy(rewards).to(self.device) - next_states = torch.from_numpy(next_states).to(self.device) + def learn(self): + print(len(self.experience_memory)) + if len(self.experience_memory) == 0: + return + + batch = self.experience_memory.get() + states = torch.from_numpy(np.asarray(batch["S"])) + actions = torch.from_numpy(np.asarray([act[0] for act in batch["A"]])) + log_p = torch.from_numpy(np.asarray([act[1] for act in batch["A"]])) + rewards = torch.from_numpy(np.asarray(batch["R"])) + next_states = torch.from_numpy(np.asarray(batch["S_"])) + if self.config.flush_experience_memory_after_training: + self.experience_memory.clear() + + if self.device: + states = states.to(self.device) + actions = actions.to(self.device) + log_p = log_p.to(self.device) + rewards = rewards.to(self.device) + next_states = next_states.to(self.device) state_values = self.model(states, task_name="critic").detach().squeeze() next_state_values = self.model(next_states, task_name="critic").detach().squeeze() diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index b3446a13c..13b4fc0ea 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -7,17 +7,30 @@ import torch from maro.rl.model import SimpleMultiHeadModel +from maro.rl.storage import SimpleStore from maro.rl.utils import get_max, get_td_errors, get_torch_loss_cls, select_by_actions from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent +from .abs_agent import AbsAgent, AgentConfig -class DQNConfig: +class DQNConfig(AgentConfig): """Configuration for the DQN algorithm. Args: reward_discount (float): Reward decay as defined in standard RL terminology. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + target_update_freq (int): Number of training rounds between target model updates. + min_experiences (int): Minimum number of experiences required for training. If the number of experiences in the + replay memory is below this number, training will not be triggered. + train_iters (int): Number of batches to train the model on in each call to ``learn``. + batch_size (int): Experience minibatch size. + sampler_cls: A string indicating the sampler class or a custom sampler class that provides the ``sample`` interface. + Defaults to "uniform". + sampler_params (dict): Parameters for the sampler class. Defaults to None. epsilon (float): Exploration rate for epsilon-greedy exploration. Defaults to None. tau (float): Soft update coefficient, i.e., target_model = tau * eval_model + (1 - tau) * target_model. double (bool): If True, the next Q values will be computed according to the double DQN algorithm, @@ -27,24 +40,37 @@ class DQNConfig: case it is assumed that the regular Q-value model is used. loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - target_update_freq (int): Number of training rounds between target model updates. + """ __slots__ = [ - "reward_discount", "target_update_freq", "epsilon", "tau", "double", "advantage_type", "loss_func" + "target_update_freq", "min_experiences", "train_iters", "batch_size", "sampler_cls", "sampler_params", + "epsilon", "tau", "double", "advantage_type", "loss_func" ] def __init__( self, reward_discount: float, + experience_memory_size: int, + experience_memory_overwrite_type: str, target_update_freq: int, + min_experiences: int, + train_iters: int, + batch_size: int, + sampler_cls="uniform", + sampler_params=None, epsilon: float = .0, tau: float = 0.1, double: bool = True, advantage_type: str = None, loss_cls="mse" ): - self.reward_discount = reward_discount + super().__init__(reward_discount, experience_memory_size, experience_memory_overwrite_type) self.target_update_freq = target_update_freq + self.min_experiences = min_experiences + self.train_iters = train_iters + self.batch_size = batch_size + self.sampler_cls = sampler_cls + self.sampler_params = sampler_params self.epsilon = epsilon self.tau = tau self.double = double @@ -69,6 +95,7 @@ def __init__(self, model: SimpleMultiHeadModel, config: DQNConfig): f"got {model.task_names}" ) super().__init__(model, config) + self._sampler = self.config.sampler_cls(self.experience_memory, **self.config.sampler_params) self._training_counter = 0 self._target_model = model.copy() if model.trainable else None @@ -96,35 +123,41 @@ def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: for act in greedy_action ]) - def learn(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): - states = torch.from_numpy(states) - actions = torch.from_numpy(actions) - rewards = torch.from_numpy(rewards) - next_states = torch.from_numpy(next_states) - - if self.device: - states = states.to(self.device) - actions = actions.to(self.device) - rewards = rewards.to(self.device) - next_states = next_states.to(self.device) - - q_all = self._get_q_values(states) - q = select_by_actions(q_all, actions) - next_q_all_target = self._get_q_values(next_states, is_eval=False, training=False) - if self.config.double: - print("double DQN") - next_q_all_eval = self._get_q_values(next_states, training=False) - next_q = select_by_actions(next_q_all_target, next_q_all_eval.max(dim=1)[1]) # (N,) - else: - next_q, _ = get_max(next_q_all_target) # (N,) - - loss = get_td_errors(q, next_q, rewards, self.config.reward_discount, loss_func=self.config.loss_func) - self.model.step(loss.mean()) - self._training_counter += 1 - if self._training_counter % self.config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.config.tau) - - return loss.detach().numpy() + def learn(self): + if len(self.experience_memory) < self.config.min_experiences: + return + + for _ in range(self.config.train_iters): + # sample from the replay memory + indexes, batch = self._sampler.sample(self.config.batch_size) + states = torch.from_numpy(np.asarray(batch["S"])) + actions = torch.from_numpy(np.asarray(batch["A"])) + rewards = torch.from_numpy(np.asarray(batch["R"])) + next_states = torch.from_numpy(np.asarray(batch["S_"])) + + if self.device: + states = states.to(self.device) + actions = actions.to(self.device) + rewards = rewards.to(self.device) + next_states = next_states.to(self.device) + + q_all = self._get_q_values(states) + q = select_by_actions(q_all, actions) + next_q_all_target = self._get_q_values(next_states, is_eval=False, training=False) + if self.config.double: + next_q_all_eval = self._get_q_values(next_states, training=False) + next_q = select_by_actions(next_q_all_target, next_q_all_eval.max(dim=1)[1]) # (N,) + else: + next_q, _ = get_max(next_q_all_target) # (N,) + + loss = get_td_errors(q, next_q, rewards, self.config.reward_discount, loss_func=self.config.loss_func) + self.model.step(loss.mean()) + self._training_counter += 1 + if self._training_counter % self.config.target_update_freq == 0: + self._target_model.soft_update(self.model, self.config.tau) + + # update auxillary info for the next round of sampling + self._sampler.update(indexes, loss.detach().numpy()) def set_exploration_params(self, epsilon): self.config.epsilon = epsilon diff --git a/maro/rl/distributed/actor_proxy.py b/maro/rl/distributed/actor_proxy.py index 4f75ed540..a44a8104a 100644 --- a/maro/rl/distributed/actor_proxy.py +++ b/maro/rl/distributed/actor_proxy.py @@ -30,8 +30,8 @@ def __init__( group_name: str, proxy_options: dict = None, update_trigger: str = None, - replay_memory_size: int = -1, - replay_memory_overwrite_type: str = None + experience_memory_size: int = -1, + experience_memory_overwrite_type: str = None ): peers = {"actor": num_actors} if proxy_options is None: @@ -44,9 +44,10 @@ def __init__( self._registry_table.register_event_handler( f"actor:{MsgTag.ROLLOUT_DONE.value}:{update_trigger}", self._on_rollout_finish ) - self.replay_memory = defaultdict( - lambda: SimpleStore(capacity=replay_memory_size, overwrite_type=replay_memory_overwrite_type) + self.experience_memory = defaultdict( + lambda: SimpleStore(capacity=experience_memory_size, overwrite_type=experience_memory_overwrite_type) ) + self.rollout_results = {} self.logger = InternalLogger(self._proxy.name) def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): @@ -67,6 +68,7 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non self._proxy.iscatter(MsgTag.ROLLOUT, SessionType.TASK, [(actor, body) for actor in self._actors]) self.logger.info(f"Sent roll-out requests to {self._actors} for ep-{index}") + def wait_for_actor_results(self): # Receive roll-out results from remote actors for msg in self._proxy.receive(): if msg.body[MsgKey.ROLLOUT_INDEX] != index: @@ -86,16 +88,16 @@ def roll_out(self, index: int, training: bool = True, model_by_agent: dict = Non # print(f"received exp from actor {msg.source} ") # print({agent_id: {k: len(v) for k, v in exp.items()} for agent_id, exp in msg.body[MsgKey.REPLAY].items()}) for agent_id, exp in msg.body[MsgKey.REPLAY].items(): - self.replay_memory[agent_id].put(exp) - # print({agent_id: len(pool) for agent_id, pool in self.replay_memory.items()}) + self.experience_memory[agent_id].put(exp) + # print({agent_id: len(pool) for agent_id, pool in self.experience_memory.items()}) - return env_metrics + rollout_results[index] = env_metrics def _on_rollout_finish(self, messages: List[Message]): metrics = {msg.source: msg.body[MsgKey.METRICS] for msg in messages} for msg in messages: for agent_id, replay in msg.body[MsgKey.REPLAY].items(): - self.replay_memory[agent_id].put(replay) + self.experience_memory[agent_id].put(replay) return metrics def terminate(self): diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 35e5b451f..a27c1a1af 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -22,16 +22,36 @@ class AbsDistLearner(ABC): data for learning purposes. agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. """ - def __init__(self, actor_proxy: ActorProxy, agent: Union[AbsAgent, MultiAgentWrapper]): + def __init__( + self, + actor_proxy: ActorProxy, + agent: Union[AbsAgent, MultiAgentWrapper], + scheduler: Scheduler, + + ): super().__init__() self.actor_proxy = actor_proxy self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent self.logger = InternalLogger("LEARNER") - @abstractmethod def run(self): """Main learning loop is implemented here.""" - return NotImplementedError + for exploration_params in self.scheduler: + rollout_index = self.scheduler.iter + env_metrics = self.actor_proxy.roll_out( + rollout_index, model_by_agent=self.agent.dump_model(), exploration_params=exploration_params + ) + self.logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") + + for _ in range(self.train_iter): + batch_by_agent, idx_by_agent = self.get_batch() + for agent_id, batch in batch_by_agent.items(): + self.agent[agent_id].learn(*batch) + + self.logger.info("Agent learning finished") + + # Signal remote actors to quit + self.actor_proxy.terminate() class OnPolicyDistLearner(AbsDistLearner): @@ -85,16 +105,3 @@ def run(self): # Signal remote actors to quit self.actor_proxy.terminate() - - def get_batch(self): - idx, batch = {}, {} - for agent_id, mem in self.actor_proxy.replay_memory.items(): - if len(mem) < self.min_experiences_to_train: - continue - indexes, sample = mem.sample(self.batch_size) - batch[agent_id] = ( - asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) - ) - idx[agent_id] = indexes - - return batch, idx diff --git a/maro/rl/storage/abs_store.py b/maro/rl/storage/abs_store.py index 021f002cd..4e09e5b82 100644 --- a/maro/rl/storage/abs_store.py +++ b/maro/rl/storage/abs_store.py @@ -55,17 +55,3 @@ def filter(self, filters: Sequence[Callable]): Filtered indexes and corresponding objects. """ pass - - @abstractmethod - def sample(self, size: int, weights: Sequence = None, replace: bool = True): - """Obtain a random sample from the experience pool. - - Args: - size (int): Sample size. - weights (Sequence): A sequence of sampling weights. If None, uniform sampling is performed. - Defaults to None. - replace (bool): If True, sampling is performed with replacement. Defaults to True. - Returns: - A random sample from the experience pool. - """ - pass diff --git a/maro/rl/storage/sampler.py b/maro/rl/storage/sampler.py new file mode 100644 index 000000000..b1252a163 --- /dev/null +++ b/maro/rl/storage/sampler.py @@ -0,0 +1,58 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod + +import numpy as np + +from .abs_store import AbsStore + + +class AbsSampler(ABC): + def __init__(self, data: AbsStore): + self.data = data + + @abstractmethod + def sample(self, size: int): + raise NotImplementedError + + @abstractmethod + def update(self): + """Update statistics used for sampling.""" + pass + + +class UniformSampler(AbsSampler): + def __init__(self, data, replace: bool = True): + super().__init__(data) + self.replace = replace + + def sample(self, size: int): + """ + Obtain a random sample from the experience pool. + + Args: + size (int): Sample sizes for each round of sampling in the chain. If this is a single integer, it is + used as the sample size for all samplers in the chain. + weights (Union[list, np.ndarray]): Sampling weights. + replace (bool): If True, sampling is performed with replacement. Defaults to True. + Returns: + Sampled indexes and the corresponding objects, + e.g., [1, 2, 3], ['a', 'b', 'c']. + """ + indexes = np.random.choice(len(self.data), size=size, replace=self.replace) + return indexes, self.data.get(indexes=indexes) + + def update(self): + pass + + +class PrioritizedSampler(AbsSampler): + def __init__(self, data): + super().__init__(data) + + def sample(self, size: int): + pass + + def update(self): + pass diff --git a/maro/rl/storage/simple_store.py b/maro/rl/storage/simple_store.py index bb3db0682..4e6b58dc9 100644 --- a/maro/rl/storage/simple_store.py +++ b/maro/rl/storage/simple_store.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import defaultdict from enum import Enum from typing import Callable, Dict, List, Tuple, Union @@ -23,6 +22,7 @@ class SimpleStore(AbsStore): and limited storage are supported. Args: + keys (list): Keys to identify the stored lists of objects. capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. overwrite_type (str): If storage capacity is bounded, this specifies how existing entries are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: @@ -30,13 +30,14 @@ class SimpleStore(AbsStore): - "random", where overwrite occurs randomly among filled positions. Alternatively, the user may also specify overwrite positions (see ``put``). """ - def __init__(self, capacity: int = -1, overwrite_type: str = None): + def __init__(self, keys: list, capacity: int = -1, overwrite_type: str = None): super().__init__() if overwrite_type not in {"rolling", "random"}: raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") + self.keys = keys self._capacity = capacity self._overwrite_type = overwrite_type - self.data = defaultdict(list) if capacity == -1 else defaultdict(lambda: [None] * capacity) + self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self.keys} self._size = 0 def __len__(self): @@ -59,7 +60,9 @@ def overwrite_type(self): """An string indicating the overwrite behavior when the store capacity is exceeded.""" return self._overwrite_type - def get(self, indexes: [int]) -> dict: + def get(self, indexes: [int] = None) -> dict: + if indexes is None: + return self.data return {k: [self.data[k][i] for i in indexes] for k in self.data} def put(self, contents: Dict[str, List], overwrite_indexes: list = None) -> List[int]: @@ -76,7 +79,7 @@ def put(self, contents: Dict[str, List], overwrite_indexes: list = None) -> List The indexes where the newly added entries reside in the store. """ if len(self.data) > 0: - expected_keys, actual_keys = list(self.data.keys()), list(contents.keys()) + expected_keys, actual_keys = set(self.data.keys()), set(contents.keys()) if expected_keys != actual_keys: raise StoreMisalignment(f"expected keys {expected_keys}, got {actual_keys}") self.validate(contents) @@ -112,66 +115,9 @@ def update(self, indexes: list, contents: Dict[str, List]): return indexes - def apply_multi_filters(self, filters: List[Callable]): - """Multi-filter method. - - The input to one filter is the output from its predecessor in the sequence. - - Args: - filters (List[Callable]): Filter list, each item is a lambda function, - e.g., [lambda d: d['a'] == 1 and d['b'] == 1]. - Returns: - Filtered indexes and corresponding objects. - """ - indexes = range(self._size) - for f in filters: - indexes = [i for i in indexes if f(self[i])] - - return indexes, self.get(indexes) - - def apply_multi_samplers(self, samplers: list, replace: bool = True) -> Tuple: - """Multi-samplers method. - - This implements chained sampling where the input to one sampler is the output from its predecessor in - the sequence. - - Args: - samplers (list): A sequence of weight functions for computing the sampling weights of the items - in the store, - e.g., [lambda d: d['a'], lambda d: d['b']]. - replace (bool): If True, sampling will be performed with replacement. - Returns: - Sampled indexes and corresponding objects. - """ - indexes = range(self._size) - for weight_fn, sample_size in samplers: - weights = np.asarray([weight_fn(self[i]) for i in indexes]) - indexes = np.random.choice(indexes, size=sample_size, replace=replace, p=weights / np.sum(weights)) - - return indexes, self.get(indexes) - - def sample(self, size, weights: Union[list, np.ndarray] = None, replace: bool = True): - """ - Obtain a random sample from the experience pool. - - Args: - size (int): Sample sizes for each round of sampling in the chain. If this is a single integer, it is - used as the sample size for all samplers in the chain. - weights (Union[list, np.ndarray]): Sampling weights. - replace (bool): If True, sampling is performed with replacement. Defaults to True. - Returns: - Sampled indexes and the corresponding objects, - e.g., [1, 2, 3], ['a', 'b', 'c']. - """ - if weights is not None: - weights = np.asarray(weights) - weights = weights / np.sum(weights) - indexes = np.random.choice(self._size, size=size, replace=replace, p=weights) - return indexes, self.get(indexes) - def clear(self): """Empty the store.""" - self.data = defaultdict(list) if self._capacity == -1 else defaultdict(lambda: [None] * self._capacity) + self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self.keys} self._size = 0 def dumps(self): @@ -182,9 +128,6 @@ def get_by_key(self, key): """Get the contents of the store corresponding to ``key``.""" return self.data[key] - def insert(self, key: str, default_val=None): - self.data[key] = [default_val for _ in range(self._size)] - def _get_update_indexes(self, added_size: int, overwrite_indexes=None): if added_size > self._capacity: raise ValueError("size of added items should not exceed the store capacity.") diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 382eb87ec..3ecff3bc9 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -2,6 +2,6 @@ # Licensed under the MIT license. from .env_wrapper import AbsEnvWrapper -from .learner import AbsLearner, OffPolicyLearner, OnPolicyLearner +from .learner import Learner -__all__ = ["AbsEnvWrapper", "AbsLearner", "OffPolicyLearner", "OnPolicyLearner"] +__all__ = ["AbsEnvWrapper", "Learner"] diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index f74ec9013..d6efddc3b 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -3,7 +3,8 @@ import time from abc import ABC, abstractmethod -from collections import defaultdict +from collections import defaultdict, deque +from typing import List, Union from maro.simulator import Env @@ -22,20 +23,21 @@ class AbsEnvWrapper(ABC): """ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): self.env = env - self.step_index = None + self._step_index = None self.replay = defaultdict(lambda: defaultdict(list)) self.state_info = None # context for converting model output to actions that can be executed by the env self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay - self._pending_reward_idx = 0 - self._event_ticks = [] # for delayed reward evaluation - self._action_history = [] # for delayed reward evaluation + self._acting_agents = deque() # list of (tick, acting_agent_list) for delayed reward evaluation self._tot_raw_step_time = 0 self._tot_step_time = 0 + @property + def step_index(self): + return self._step_index + def start(self, rollout_index: int = None): - self.step_index = 0 - self._pending_reward_idx = 0 + self._step_index = 0 _, event, _ = self.env.step(None) state_by_agent = self.get_state(event) if self.save_replay: @@ -48,12 +50,16 @@ def start(self, rollout_index: int = None): return state_by_agent - @property - def replay_memory(self): - return { - agent_id: {k: vals[:len(replay["R"])] for k, vals in replay.items()} - for agent_id, replay in self.replay.items() - } + def get_experiences(self, copy: bool = False): + experience = defaultdict(dict) + for agent_id, replay in self.replay.items(): + num_complete = min(len(replay["R"]), len(replay["S_"])) + for k, vals in replay.items(): + experience[agent_id][k] = vals[:num_complete] + if not copy: + del vals[:num_complete] + + return experience @property def metrics(self): @@ -68,65 +74,44 @@ def get_action(self, action) -> dict: pass @abstractmethod - def get_reward(self, tick: int = None) -> float: + def get_reward(self, tick: int = None, target_agents: list = None) -> dict: """User-defined reward evaluation. Args: - tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed reward - evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. - + tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed + reward evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. + targets_agents (list): If given, rewards will be given only to these agents. Defaults to None. """ pass def step(self, action_by_agent: dict): t0 = time.time() - self.step_index += 1 - self._event_ticks.append(self.env.tick) + self._step_index += 1 env_action = self.get_action(action_by_agent) - self._action_history.append(env_action) + self._acting_agents.append((self.env.tick, list(env_action.keys()))) if len(env_action) == 1: env_action = list(env_action.values())[0] t1 = time.time() - _, event, done = self.env.step(None) + _, event, done = self.env.step(env_action) t2 = time.time() self._tot_raw_step_time += t2 - t1 if self.save_replay: - if self.reward_eval_delay: - for agent_id, action in action_by_agent.items(): - if isinstance(action, tuple): - self.replay[agent_id]["A"].append(action[0]) - self.replay[agent_id]["LOGP"].append(action[1]) - else: - self.replay[agent_id]["A"].append(action) - """ - If roll-out is complete, evaluate rewards for all remaining events except the last. - Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ - for i, (tick, action) in enumerate( - zip(self._event_ticks[self._pending_reward_idx:], self._action_history[self._pending_reward_idx:]) - ): - if not done and self.env.tick - tick < self.reward_eval_delay: - self._pending_reward_idx += i - break - reward_dict = self.get_reward(tick=tick) - for agent_id in action: - if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): - self.replay[agent_id]["R"].append(reward_dict[agent_id]) - - if done: - self._pending_reward_idx = len(self._event_ticks) - 1 - else: - reward_dict = self.get_reward() - for agent_id, action in action_by_agent.items(): - if isinstance(action, tuple): - self.replay[agent_id]["A"].append(action[0]) - self.replay[agent_id]["LOGP"].append(action[1]) - else: - self.replay[agent_id]["A"].append(action) - if len(self.replay[agent_id]["R"]) < len(self.replay[agent_id]["S_"]): - self.replay[agent_id]["R"].append(reward_dict[agent_id]) - self._pending_reward_idx += 1 + for agent_id, action in action_by_agent.items(): + self.replay[agent_id]["A"].append(action) + """ + If roll-out is complete, evaluate rewards for all remaining events except the last. + Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. + """ + while ( + self._acting_agents and + (done or self.env.tick - self._acting_agents[0][0] >= self.reward_eval_delay) + ): + reward = self.get_reward(tick=self._acting_agents[0][0], target_agents=self._acting_agents[0][1]) + # assign rewards to the relevant agents + for agent_id in self._acting_agents[0][1]: + self.replay[agent_id]["R"].append(reward[agent_id]) + self._acting_agents.popleft() if not done: state_by_agent = self.get_state(event) @@ -150,16 +135,5 @@ def step(self, action_by_agent: dict): def reset(self): self.env.reset() self.state_info = None - self._event_ticks.clear() - self._action_history.clear() + self._acting_agents.clear() self.replay = defaultdict(lambda: defaultdict(list)) - - def flush(self): - for replay in self.replay.values(): - num_complete = len(replay["R"]) - for vals in replay.values(): - del vals[:num_complete] - - del self._event_ticks[:self._pending_reward_idx] - del self._action_history[:self._pending_reward_idx] - self._pending_reward_idx = 0 diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index f793f4031..706715a53 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -2,21 +2,17 @@ # Licensed under the MIT license. import time -from abc import ABC, abstractmethod from collections import defaultdict -from typing import Callable, Union - -from numpy import asarray +from typing import Dict, Union from maro.rl.agent import AbsAgent, MultiAgentWrapper from maro.rl.scheduling import Scheduler -from maro.rl.storage import SimpleStore from maro.utils import InternalLogger from .env_wrapper import AbsEnvWrapper -class AbsLearner(ABC): +class Learner(object): """Learner class for distributed training. Args: @@ -24,100 +20,58 @@ class AbsLearner(ABC): processing logic and stores transitions during roll-outs in a replay memory. agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. """ - def __init__(self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper]): - super().__init__() - self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent - self.logger = InternalLogger("LEARNER") - - def roll_out(self, index: int, training: bool = True): - t0 = time.time() - self.env.reset() - if not training: - self.env.save_replay = False # no need to record the trajectory if roll-out is not for training - - state = self.env.start(rollout_index=index) # get initial state - while state: - action = self.agent.choose_action(state) - state = self.env.step(action) - t1 = time.time() - print(f"roll-out time: {t1 - t0}") - - @abstractmethod - def run(self): - """Main learning loop is implemented here.""" - return NotImplementedError - - -class OnPolicyLearner(AbsLearner): - def __init__(self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], max_episode: int): - super().__init__(env, agent) - self.max_episode = max_episode - - def run(self): - for ep in range(self.max_episode): - self.roll_out(ep) - self.logger.info(f"ep-{ep}: {self.env.metrics}") - for agent_id, replay in self.env.replay_memory.items(): - self.agent[agent_id].learn( - asarray(replay["S"]), - asarray(replay["A"]), - asarray(replay["LOGP"]), - asarray(replay["R"]), - asarray(replay["S_"]) - ) - - self.logger.info("Agent learning finished") - - -class OffPolicyLearner(AbsLearner): def __init__( self, env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, - *, - replay_memory_size: int, - replay_memory_overwrite_type: str, - train_iter: int = 1, - min_experiences_to_train: int = 0, - batch_size: int = 128 + learning_interval: Union[int, Dict[str, int]] = -1, + end_of_episode_learning: bool = True ): - super().__init__(env, agent) + super().__init__() + self.env = env + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent self.scheduler = scheduler - self.train_iter = train_iter - self.min_experiences_to_train = min_experiences_to_train - self.batch_size = batch_size - self.replay_memory = defaultdict( - lambda: SimpleStore(capacity=replay_memory_size, overwrite_type=replay_memory_overwrite_type) - ) + self.online = learning_interval != -1 + if isinstance(learning_interval, int): + assert learning_interval == -1 or learning_interval > 0, \ + f"learning_interval must be -1 or a positive integer" + self.learning_interval = {agent_id: learning_interval for agent_id in self.agent.names} + else: + self.learning_interval = learning_interval + self.end_of_episode_learning = end_of_episode_learning + self.logger = InternalLogger("LEARNER") def run(self): for exploration_params in self.scheduler: - rollout_index = self.scheduler.iter - self.roll_out(rollout_index) - self.logger.info(f"ep-{rollout_index}: {self.env.metrics} ({exploration_params})") - # Add the latest transitions to the replay memory - for agent_id, mem in self.env.replay_memory.items(): - self.replay_memory[agent_id].put(mem) - - # Training - for _ in range(self.train_iter): - batch_by_agent, idx_by_agent = self.get_batch() - for agent_id, batch in batch_by_agent.items(): - self.agent[agent_id].learn(*batch) - - self.logger.info("Agent learning finished") - - def get_batch(self): - idx, batch = {}, {} - for agent_id, mem in self.replay_memory.items(): - if len(mem) < self.min_experiences_to_train: - continue - indexes, sample = mem.sample(self.batch_size) - batch[agent_id] = ( - asarray(sample["S"]), asarray(sample["A"]), asarray(sample["R"]), asarray(sample["S_"]) - ) - idx[agent_id] = indexes - - return batch, idx + # t0 = time.time() + self.env.reset() + if exploration_params: + self.agent.set_exploration_params(exploration_params) + + pending_learning_agents = defaultdict(list) + for agent_id, interval in self.learning_interval.items(): + pending_learning_agents[interval].append(agent_id) + + state = self.env.start(rollout_index=self.scheduler.iter) # get initial state + while state: + action = self.agent.choose_action(state) + state = self.env.step(action) + if self.online and self.env.step_index in pending_learning_agents: + self.agent.store_experiences(self.env.get_experiences()) + for agent_id in pending_learning_agents[self.env.step_index]: + self.agent.learn(agent_id) + next_learning_time = self.env.step_index + self.learning_interval[agent_id] + if next_learning_time > self.env.step_index: + pending_learning_agents[next_learning_time].append(agent_id) + del pending_learning_agents[self.env.step_index] + print(f"step = {self.env.step_index}, next up: {pending_learning_agents}") + + if self.end_of_episode_learning: + self.agent.store_experiences(self.env.get_experiences()) + self.agent.learn() + + self.logger.info(f"ep-{self.scheduler.iter}: {self.env.metrics} ({exploration_params})") + + # t1 = time.time() + # print(f"roll-out time: {t1 - t0}") diff --git a/maro/rl/training/test.py b/maro/rl/training/test.py new file mode 100644 index 000000000..7335f369b --- /dev/null +++ b/maro/rl/training/test.py @@ -0,0 +1,17 @@ +import random +import time +from functools import reduce + +pool = range(100) +n = 100000 + +l = [random.choices(pool, k=10) for _ in range(n)] + +t0 = time.time() +for vals in l: + reduce(lambda x, y: x + y, vals) +t1 = time.time() +[reduce(lambda x, y: x + y, vals) for vals in l] +t2 = time.time() + +print(t1 - t0, t2 - t1) From 1acfd48149449fde25d08e03319db4063ecf6177 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 4 Apr 2021 16:08:07 +0000 Subject: [PATCH 148/482] added online learning logic --- examples/cim/ac/config.yml | 7 +- examples/cim/ac/main.py | 2 +- examples/cim/dqn/config.yml | 20 +-- examples/cim/dqn/main.py | 19 +-- .../supply_chain/dqn/distributed_launcher.py | 2 +- maro/rl/__init__.py | 18 +- maro/rl/agent/abs_agent.py | 50 +++++- maro/rl/agent/ac.py | 36 ++-- maro/rl/agent/ddpg.py | 21 ++- maro/rl/agent/dqn.py | 36 ++-- maro/rl/agent/multi_agent_wrapper.py | 36 ++-- maro/rl/agent/pg.py | 11 +- maro/rl/distributed/__init__.py | 5 +- maro/rl/distributed/actor.py | 91 ++++------ maro/rl/distributed/actor_proxy.py | 108 ------------ maro/rl/distributed/learner.py | 156 +++++++++--------- maro/rl/distributed/message_enums.py | 9 +- maro/rl/distributed/trainer.py | 2 +- maro/rl/storage/__init__.py | 3 +- maro/rl/storage/sampler.py | 11 -- maro/rl/training/env_wrapper.py | 45 ++--- maro/rl/training/learner.py | 42 ++--- maro/rl/utils/__init__.py | 2 + maro/rl/utils/sampler_cls_index.py | 16 ++ 24 files changed, 321 insertions(+), 427 deletions(-) delete mode 100644 maro/rl/distributed/actor_proxy.py create mode 100644 maro/rl/utils/sampler_cls_index.py diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index afe688e4a..097938cf9 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -62,6 +62,7 @@ agent: critic_loss_cls: smooth_l1 train_iters: 10 actor_loss_coefficient: 0.1 + min_new_experiences_to_learn: 1 # clip_ratio: 0.8 # for PPO training: env: @@ -69,8 +70,4 @@ training: topology: toy.4p_ssdd_l0.0 durations: 1120 max_episode: 50 - learning_interval: - 0: 200 - 1: 200 - 2: 400 - 3: 200 + agent_update_interval: 200 \ No newline at end of file diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index a6c7e83cb..8c51e0bf9 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -52,6 +52,6 @@ def get_ac_agent(): scheduler = Scheduler(config["training"]["max_episode"]) learner = Learner( CIMEnvWrapper(env, **config["shaping"]), agent, scheduler, - learning_interval=config["training"]["learning_interval"] + agent_update_interval=config["training"]["agent_update_interval"] ) learner.run() diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index a48f16016..c8569d5a7 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -44,23 +44,22 @@ agent: lr: 0.05 hyper_params: reward_discount: .0 + experience_memory_size: -1 + experience_memory_overwrite_type: "rolling" + min_new_experiences_to_learn: 16 + min_experiences: 1024 + train_iters: 10 + batch_size: 128 loss_cls: smooth_l1 target_update_freq: 5 tau: 0.1 double: false training: - env: + env: scenario: cim topology: toy.4p_ssdd_l0.0 durations: 1120 max_episode: 100 - min_experiences_to_train: 1024 - train_iter: 10 - batch_size: 128 - replay_memory: - size: -1 - overwrite_type: random - # prioritized_sampling_by_loss: true exploration: parameter_names: - epsilon @@ -73,5 +72,6 @@ distributed: num_actors: 2 redis_host: localhost redis_port: 6379 - learner_update_trigger: 2 - replay_sync_interval: 100 + min_actor_finishes: 2 + agent_update_interval: 100 + ignore_stale_experiences: False \ No newline at end of file diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 3e4850b6c..05840db87 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -8,8 +8,8 @@ from os.path import dirname, join, realpath from maro.rl import ( - Actor, ActorProxy, DQN, DQNConfig, FullyConnectedBlock, MultiAgentWrapper, OffPolicyDistLearner, - OptimOption, SimpleMultiHeadModel, TwoPhaseLinearParameterScheduler + Actor, DQN, DQNConfig, DistLearner, FullyConnectedBlock, MultiAgentWrapper, OptimOption, SimpleMultiHeadModel, + TwoPhaseLinearParameterScheduler ) from maro.simulator import Env from maro.utils import set_seeds @@ -48,18 +48,10 @@ def get_dqn_agent(): def cim_dqn_learner(): agent = MultiAgentWrapper({name: get_dqn_agent() for name in Env(**config["training"]["env"]).agent_idx_list}) scheduler = TwoPhaseLinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) - actor_proxy = ActorProxy( - NUM_ACTORS, GROUP, + learner = DistLearner( + agent, scheduler, NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, - update_trigger=config["distributed"]["learner_update_trigger"], - replay_memory_size=config["training"]["replay_memory"]["size"], - replay_memory_overwrite_type=config["training"]["replay_memory"]["overwrite_type"] - ) - learner = OffPolicyDistLearner( - actor_proxy, agent, scheduler, - min_experiences_to_train=config["training"]["min_experiences_to_train"], - train_iter=config["training"]["train_iter"], - batch_size=config["training"]["batch_size"] + agent_update_interval=config["distributed"]["agent_update_interval"] ) learner.run() @@ -69,7 +61,6 @@ def cim_dqn_actor(): agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) actor = Actor( CIMEnvWrapper(env, **config["shaping"]), agent, GROUP, - replay_sync_interval=config["distributed"]["replay_sync_interval"], proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)} ) actor.run() diff --git a/examples/supply_chain/dqn/distributed_launcher.py b/examples/supply_chain/dqn/distributed_launcher.py index fa09b6b10..4f0123dd6 100644 --- a/examples/supply_chain/dqn/distributed_launcher.py +++ b/examples/supply_chain/dqn/distributed_launcher.py @@ -8,7 +8,7 @@ from os.path import dirname, join, realpath from maro.rl import ( - Actor, ActorProxy, DQN, DQNConfig, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, + Actor, DQN, DQNConfig, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, OffPolicyDistLearner, OptimOption, SimpleMultiHeadModel ) from maro.simulator import Env diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 8b820ee01..1251bd33f 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -4,28 +4,30 @@ from maro.rl.agent import ( DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient ) -from maro.rl.distributed import AbsDistLearner, Actor, ActorProxy, OffPolicyDistLearner, OnPolicyDistLearner +from maro.rl.distributed import Actor, DistLearner from maro.rl.exploration import ( AbsExplorer, EpsilonGreedyExplorer, GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer ) from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler -from maro.rl.storage import AbsStore, SimpleStore +from maro.rl.storage import AbsSampler, AbsStore, SimpleStore, UniformSampler from maro.rl.training import AbsEnvWrapper, Learner from maro.rl.utils import ( - get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_torch_activation_cls, get_torch_loss_cls, - get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward, select_by_actions + get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_sampler_cls, get_torch_activation_cls, + get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward, + select_by_actions ) __all__ = [ "AbsAgent", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", "PolicyGradient", - "AbsDistLearner", "Actor", "ActorProxy", "OffPolicyDistLearner", "OnPolicyDistLearner", + "Actor", "DistLearner", "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", - "AbsStore", "SimpleStore", + "AbsSampler", "AbsStore", "SimpleStore", "UniformSampler", "AbsEnvWrapper", "Learner", - "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_torch_activation_cls", "get_torch_loss_cls", - "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward", "select_by_actions" + "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_sampler_cls", + "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", + "get_truncated_cumulative_reward", "select_by_actions" ] diff --git a/maro/rl/agent/abs_agent.py b/maro/rl/agent/abs_agent.py index c7ec57771..d60fa33b8 100644 --- a/maro/rl/agent/abs_agent.py +++ b/maro/rl/agent/abs_agent.py @@ -18,14 +18,28 @@ class AgentConfig: unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". - + min_new_experiences_to_learn (int): Minimum number of new experiences required to trigger learning. + flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + to ``step``. """ - __slots__ = ["reward_discount", "experience_memory_size", "experience_memory_overwrite_type"] - - def __init__(self, reward_discount: float, experience_memory_size: int, experience_memory_overwrite_type: str): + __slots__ = [ + "reward_discount", "experience_memory_size", "experience_memory_overwrite_type", + "min_new_experiences_to_learn", "flush_experience_memory_after_step" + ] + + def __init__( + self, + reward_discount: float, + experience_memory_size: int, + experience_memory_overwrite_type: str, + min_new_experiences_to_learn: int, + flush_experience_memory_after_step: bool + ): self.reward_discount = reward_discount self.experience_memory_size = experience_memory_size self.experience_memory_overwrite_type = experience_memory_overwrite_type + self.min_new_experiences_to_learn = min_new_experiences_to_learn + self.flush_experience_memory_after_step = flush_experience_memory_after_step class AbsAgent(ABC): @@ -49,7 +63,16 @@ def __init__(self, model: AbsCoreModel, config: AgentConfig): capacity=self.config.experience_memory_size, overwrite_type=self.config.experience_memory_overwrite_type ) - self.device = None + self.device = torch.device('cpu') + self._version_index = 0 + + @property + def version(self): + return self._version_index + + @version.setter + def version(self, version_index): + self._version_index = version_index def to_device(self, device): self.device = device @@ -71,14 +94,23 @@ def choose_action(self, state): def set_exploration_params(self, **params): pass - def store_experiences(self, experiences: dict): - """Pull experiences from the replay memory stored by an environment wrapper.""" - if set(experiences) != {"S", "A", "R", "S_"}: + def update(self, experiences: dict) -> bool: + """Store experinces in the experience memory and train the model if necessary.""" + if set(experiences.keys()) != {"S", "A", "R", "S_"}: raise ValueError("The keys of experiences must be {'S', 'A', 'R', 'S_'}") self.experience_memory.put(experiences) + n = len(experiences["S"]) + print(f"got {n} new exp") + if len(experiences["S"]) >= self.config.min_new_experiences_to_learn: + self.step() + self._version_index += 1 + if self.config.flush_experience_memory_after_step: + self.experience_memory.clear() + return True + return False @abstractmethod - def learn(self): + def step(self): """Algorithm-specific training logic. The parameters are data to train the underlying model on. Algorithm-specific loss and optimization diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py index 175ee9f90..96b49dfff 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/agent/ac.py @@ -24,6 +24,7 @@ class ActorCriticConfig(AgentConfig): unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". + min_new_experiences_to_learn (int): Minimum number of new experiences required to trigger learning. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". train_iters (int): Number of gradient descent steps per call to ``train``. @@ -31,6 +32,8 @@ class ActorCriticConfig(AgentConfig): loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. + flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + to ``step``. Defaults to True. """ __slots__ = [ "critic_loss_func", "train_iters", "actor_loss_coefficient", "k", "lam", "clip_ratio", @@ -42,18 +45,21 @@ def __init__( reward_discount: float, experience_memory_size: int, experience_memory_overwrite_type: str, + min_new_experiences_to_learn: int, train_iters: int, critic_loss_cls="mse", actor_loss_coefficient: float = 1.0, clip_ratio: float = None, - flush_experience_memory_after_training: bool = True + flush_experience_memory_after_step: bool = True ): - super().__init__(reward_discount, experience_memory_size, experience_memory_overwrite_type) + super().__init__( + reward_discount, experience_memory_size, experience_memory_overwrite_type, min_new_experiences_to_learn, + flush_experience_memory_after_step + ) self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.train_iters = train_iters self.actor_loss_coefficient = actor_loss_coefficient self.clip_ratio = clip_ratio - self.flush_experience_memory_after_training = flush_experience_memory_after_training class ActorCritic(AbsAgent): @@ -93,32 +99,22 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: action, log_p = action.cpu().numpy(), log_p.cpu().numpy() return (action[0], log_p[0]) if is_single else (action, log_p) - def learn(self): - print(len(self.experience_memory)) + def step(self): if len(self.experience_memory) == 0: return batch = self.experience_memory.get() - states = torch.from_numpy(np.asarray(batch["S"])) - actions = torch.from_numpy(np.asarray([act[0] for act in batch["A"]])) - log_p = torch.from_numpy(np.asarray([act[1] for act in batch["A"]])) - rewards = torch.from_numpy(np.asarray(batch["R"])) - next_states = torch.from_numpy(np.asarray(batch["S_"])) - if self.config.flush_experience_memory_after_training: - self.experience_memory.clear() - - if self.device: - states = states.to(self.device) - actions = actions.to(self.device) - log_p = log_p.to(self.device) - rewards = rewards.to(self.device) - next_states = next_states.to(self.device) + states = torch.from_numpy(np.asarray(batch["S"])).to(self.device) + actions = torch.from_numpy(np.asarray([act[0] for act in batch["A"]])).to(self.device) + log_p = torch.from_numpy(np.asarray([act[1] for act in batch["A"]])).to(self.device) + rewards = torch.from_numpy(np.asarray(batch["R"])).to(self.device) + next_states = torch.from_numpy(np.asarray(batch["S_"])).to(self.device) state_values = self.model(states, task_name="critic").detach().squeeze() next_state_values = self.model(next_states, task_name="critic").detach().squeeze() return_est = rewards + self.config.reward_discount * next_state_values advantages = return_est - state_values - + print("training") for i in range(self.config.train_iters): # actor loss log_p_new = get_log_prob(self.model(states, task_name="actor"), actions) diff --git a/maro/rl/agent/ddpg.py b/maro/rl/agent/ddpg.py index 2059d0da0..8a1cd7f21 100644 --- a/maro/rl/agent/ddpg.py +++ b/maro/rl/agent/ddpg.py @@ -19,6 +19,11 @@ class DDPGConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + min_new_experiences_to_learn (int): Minimum number of new experiences required to trigger learning. target_update_freq (int): Number of training rounds between policy target model updates. q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". @@ -26,18 +31,26 @@ class DDPGConfig: loss = q_value_loss + ``policy_loss_coefficient`` * policy_loss. Defaults to 1.0. tau (float): Soft update coefficient, e.g., target_model = tau * eval_model + (1-tau) * target_model. Defaults to 1.0. + flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + to ``step``. Defaults to False. """ - __slots__ = ["reward_discount", "q_value_loss_func", "target_update_freq", "policy_loss_coefficient", "tau"] + __slots__ = ["q_value_loss_func", "target_update_freq", "policy_loss_coefficient", "tau"] def __init__( self, reward_discount: float, + experience_memory_size: int, + experience_memory_overwrite_type: str, + min_new_experiences_to_learn: int, target_update_freq: int, q_value_loss_cls="mse", policy_loss_coefficient: float = 1.0, tau: float = 1.0, ): - self.reward_discount = reward_discount + super().__init__( + reward_discount, experience_memory_size, experience_memory_overwrite_type, min_new_experiences_to_learn, + flush_experience_memory_after_step + ) self.target_update_freq = target_update_freq self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() self.policy_loss_coefficient = policy_loss_coefficient @@ -80,7 +93,7 @@ def choose_action(self, state) -> Union[float, np.ndarray]: return action[0] if is_single else action - def learn(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): + def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): states = torch.from_numpy(states).to(self.device) actual_actions = torch.from_numpy(actions).to(self.device) rewards = torch.from_numpy(rewards).to(self.device) @@ -98,7 +111,7 @@ def learn(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, ne q_value_loss = self.config.q_value_loss_func(current_q_values, target_q_values) actions_from_model = self.model(states, task_name="policy") policy_loss = -self.model(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() - self.model.learn(q_value_loss + self.config.policy_loss_coefficient * policy_loss) + self.model.step(q_value_loss + self.config.policy_loss_coefficient * policy_loss) self._train_cnt += 1 if self._train_cnt % self.config.target_update_freq == 0: self._target_model.soft_update(self.model, self.config.tau) diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index 13b4fc0ea..823d8cf31 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -8,7 +8,7 @@ from maro.rl.model import SimpleMultiHeadModel from maro.rl.storage import SimpleStore -from maro.rl.utils import get_max, get_td_errors, get_torch_loss_cls, select_by_actions +from maro.rl.utils import get_max, get_sampler_cls, get_td_errors, get_torch_loss_cls, select_by_actions from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask from .abs_agent import AbsAgent, AgentConfig @@ -23,6 +23,7 @@ class DQNConfig(AgentConfig): unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". + min_new_experiences_to_learn (int): Minimum number of new experiences required to trigger learning. target_update_freq (int): Number of training rounds between target model updates. min_experiences (int): Minimum number of experiences required for training. If the number of experiences in the replay memory is below this number, training will not be triggered. @@ -40,7 +41,8 @@ class DQNConfig(AgentConfig): case it is assumed that the regular Q-value model is used. loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - + flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + to ``step``. Defaults to False. """ __slots__ = [ "target_update_freq", "min_experiences", "train_iters", "batch_size", "sampler_cls", "sampler_params", @@ -52,6 +54,7 @@ def __init__( reward_discount: float, experience_memory_size: int, experience_memory_overwrite_type: str, + min_new_experiences_to_learn: int, target_update_freq: int, min_experiences: int, train_iters: int, @@ -62,15 +65,19 @@ def __init__( tau: float = 0.1, double: bool = True, advantage_type: str = None, - loss_cls="mse" + loss_cls="mse", + flush_experience_memory_after_step: bool = False ): - super().__init__(reward_discount, experience_memory_size, experience_memory_overwrite_type) + super().__init__( + reward_discount, experience_memory_size, experience_memory_overwrite_type, min_new_experiences_to_learn, + flush_experience_memory_after_step + ) self.target_update_freq = target_update_freq self.min_experiences = min_experiences self.train_iters = train_iters self.batch_size = batch_size - self.sampler_cls = sampler_cls - self.sampler_params = sampler_params + self.sampler_cls = get_sampler_cls(sampler_cls) + self.sampler_params = sampler_params if sampler_params else {} self.epsilon = epsilon self.tau = tau self.double = double @@ -123,23 +130,18 @@ def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: for act in greedy_action ]) - def learn(self): + def step(self): + print(len(self.experience_memory)) if len(self.experience_memory) < self.config.min_experiences: return for _ in range(self.config.train_iters): # sample from the replay memory indexes, batch = self._sampler.sample(self.config.batch_size) - states = torch.from_numpy(np.asarray(batch["S"])) - actions = torch.from_numpy(np.asarray(batch["A"])) - rewards = torch.from_numpy(np.asarray(batch["R"])) - next_states = torch.from_numpy(np.asarray(batch["S_"])) - - if self.device: - states = states.to(self.device) - actions = actions.to(self.device) - rewards = rewards.to(self.device) - next_states = next_states.to(self.device) + states = torch.from_numpy(np.asarray(batch["S"])).to(self.device) + actions = torch.from_numpy(np.asarray(batch["A"])).to(self.device) + rewards = torch.from_numpy(np.asarray(batch["R"])).to(self.device) + next_states = torch.from_numpy(np.asarray(batch["S_"])).to(self.device) q_all = self._get_q_values(states) q = select_by_actions(q_all, actions) diff --git a/maro/rl/agent/multi_agent_wrapper.py b/maro/rl/agent/multi_agent_wrapper.py index 72eb87602..49b0f087b 100644 --- a/maro/rl/agent/multi_agent_wrapper.py +++ b/maro/rl/agent/multi_agent_wrapper.py @@ -18,7 +18,7 @@ def __init__(self, agent_dict: Union[AbsAgent, dict]): if isinstance(agent_dict, AbsAgent): agent_dict = {"AGENT": agent_dict} self.agent_dict = agent_dict - self._names = list(self.agent_dict.keys()) + self._names = set(self.agent_dict.keys()) def __getitem__(self, agent_id): if len(self.agent_dict) == 1: @@ -46,23 +46,23 @@ def set_exploration_params(self, params): for agent in self.agent_dict.values(): agent.set_exploration_params(**params) - def store_experiences(self, experiences: dict): + def update(self, experiences: dict) -> set: """Store experiences in the agents' experience memory. The top-level keys of ``experiences`` will be treated as agent IDs. """ - for agent_id, exp in experiences.items(): - self.agent_dict[agent_id].store_experiences(exp) + print("updating agents") + return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].update(exp)} - def learn(self, agent_ids=None): + def step(self, agent_ids=None): if agent_ids is None: for agent in self.agent_dict.values(): - agent.learn() - elif not isinstance(agent_ids, list): - self.agent_dict[agent_ids].learn() + agent.step() + elif not isinstance(agent_ids, set): + self.agent_dict[agent_ids].step() else: for agent_id in agent_ids: - self.agent_dict[agent_id].learn() + self.agent_dict[agent_id].step() def load_model(self, model_dict: dict): """Load models from memory for each agent.""" @@ -76,10 +76,10 @@ def dump_model(self, agent_ids=None): """ if agent_ids is None: return {agent_id: agent.dump_model() for agent_id, agent in self.agent_dict.items()} - elif not isinstance(agent_ids, list): + elif not isinstance(agent_ids, set): return self.agent_dict[agent_ids].dump_model() else: - return {agent_id: self.agent_dict[agent_id].dump_model() for agent_id in self.agent_dict} + return {agent_id: self.agent_dict[agent_id].dump_model() for agent_id in agent_ids} def load_model_from_file(self, dir_path): """Load models from disk for each agent.""" @@ -95,8 +95,20 @@ def dump_model_to_file(self, dir_path: str, agent_ids=None): if agent_ids is None: for agent_id, agent in self.agent_dict.items(): agent.dump_model_to_file(os.path.join(dir_path, agent_id)) - elif not isinstance(agent_ids, list): + elif not isinstance(agent_ids, set): self.agent_dict[agent_ids].dump_model_to_file(os.path.join(dir_path, agent_ids)) else: for agent_id in agent_ids: self.agent_dict[agent_id].dump_model_to_file(os.path.join(dir_path, agent_id)) + + def get_versions(self, agent_ids=None): + if agent_ids is None: + return {aent_id: agent.version for agent_id, agent in self.agent_dict.items()} + elif not isinstance(agent_ids, set): + return self.agent_dict[agent_ids].version + else: + return {agent_id: self.agent_dict[agent_id].version for agent_id in agent_ids} + + def set_versions(self, version_index_by_agent: dict): + for agent_id, version_index in version_index_by_agent.items(): + self.agent_dict[agent_id].version = version_index diff --git a/maro/rl/agent/pg.py b/maro/rl/agent/pg.py index b58acbe09..2079c0383 100644 --- a/maro/rl/agent/pg.py +++ b/maro/rl/agent/pg.py @@ -10,21 +10,14 @@ from maro.rl.model import SimpleMultiHeadModel from maro.rl.utils import get_truncated_cumulative_reward -from .abs_agent import AbsAgent +from .abs_agent import AbsAgent, AgentConfig class PolicyGradient(AbsAgent): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - - Args: - model (SimpleMultiHeadModel): Model that computes action distributions. - reward_discount (float): Reward decay as defined in standard RL terminology. """ - def __init__(self, model: SimpleMultiHeadModel, reward_discount: float): - super().__init__(model, reward_discount) - def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Use the actor (policy) model to generate stochastic actions. @@ -45,7 +38,7 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: action, log_p = action.cpu().numpy(), log_p.cpu().numpy() return (action[0], log_p[0]) if is_single else (action, log_p) - def learn(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): + def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): states = torch.from_numpy(states).to(self.device) actions = torch.from_numpy(actions).to(self.device) returns = get_truncated_cumulative_reward(rewards, self.config) diff --git a/maro/rl/distributed/__init__.py b/maro/rl/distributed/__init__.py index 1abf4e3e7..76a304121 100644 --- a/maro/rl/distributed/__init__.py +++ b/maro/rl/distributed/__init__.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. from .actor import Actor -from .actor_proxy import ActorProxy -from .learner import AbsDistLearner, OffPolicyDistLearner, OnPolicyDistLearner +from .learner import DistLearner -__all__ = ["AbsDistLearner", "Actor", "ActorProxy", "OffPolicyDistLearner", "OnPolicyDistLearner"] +__all__ = ["Actor", "DistLearner"] diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index b1b6dbb30..6e3a59137 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -22,7 +22,6 @@ class Actor(object): assigned to the learner (and decision clients, if any). proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. - replay_sync_interval (int): Number of roll-out steps between replay syncing calls. """ def __init__( self, @@ -30,75 +29,49 @@ def __init__( agent: Union[AbsAgent, MultiAgentWrapper], group: str, proxy_options: dict = None, - replay_sync_interval: int = None, - send_results: bool = True + pull_experiences_with_copy: bool = False ): - if replay_sync_interval == 0: - raise ValueError("replay_sync_interval must be a positive integer or None") self.env = env self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent + self._pull_experiences_with_copy = pull_experiences_with_copy if proxy_options is None: proxy_options = {} - self._proxy = Proxy(group, "actor", {"actor_proxy": 1}, **proxy_options) - self.replay_sync_interval = replay_sync_interval - self.send_results = send_results + self._proxy = Proxy(group, "actor", {"learner": 1}, **proxy_options) self._logger = InternalLogger(self._proxy.name) - def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): - self.env.reset() - if not training: - self.env.save_replay = False # no need to record the trajectory if roll-out is not for training - - # Load models and exploration parameters - if model_by_agent: - self.agent.load_model(model_by_agent) - if exploration_params: - self.agent.set_exploration_params(exploration_params) - - state = self.env.start(rollout_index=index) # get initial state - while state: - action = self.agent.choose_action(state) - state = self.env.step(action) - if self.replay_sync_interval is not None and (self.env.step_index + 1) % self.replay_sync_interval == 0: - self._proxy.isend( - Message( - MsgTag.REPLAY_SYNC, self._proxy.name, self._proxy.peers["actor_proxy"][0], - body={MsgKey.ROLLOUT_INDEX: index, MsgKey.REPLAY: self.env.replay_memory} - ) - ) - self.env.flush() - - return self.env.metrics - def run(self): for msg in self._proxy.receive(): if msg.tag == MsgTag.EXIT: self._logger.info("Exiting...") break - elif msg.tag == MsgTag.ROLLOUT: - self.on_rollout_request(msg) - self.post_rollout(msg.body[MsgKey.ROLLOUT_INDEX]) + if msg.tag == MsgTag.ROLLOUT: + rollout_index, segment_index = msg.body[MsgKey.ROLLOUT_INDEX], msg.body[MsgKey.SEGMENT_INDEX] + if self.env.state is None: + self.env.reset() + # Load exploration parameters + if MsgKey.EXPLORATION_PARAMS in msg.body: + self.agent.set_exploration_params(msg.body[MsgKey.EXPLORATION_PARAMS]) + self.env.start(rollout_index=rollout_index) # get initial state - def on_rollout_request(self, msg: Message): - ep = msg.body[MsgKey.ROLLOUT_INDEX] - self._logger.info(f"Rolling out ({ep})...") - self.roll_out( - ep, - training=msg.body[MsgKey.TRAINING], - model_by_agent=msg.body[MsgKey.MODEL], - exploration_params=msg.body[MsgKey.EXPLORATION_PARAMS] - ) - self._logger.info(f"Roll-out {ep} finished") + step_index = self.env.step_index + self.agent.load_model(msg.body[MsgKey.MODEL]) + for _ in range(msg.body[MsgKey.NUM_STEPS]): + action = self.agent.choose_action(self.env.state) + self.env.step(action) + if not self.env.state: + break - def post_rollout(self, index: int): - if not self.send_results: - return - - body = {MsgKey.ROLLOUT_INDEX: index, MsgKey.METRICS: self.env.metrics} - if self.env.save_replay: - body[MsgKey.REPLAY] = self.env.replay_memory - - actor_proxy_addr = self._proxy.peers["actor_proxy"][0] - self._proxy.isend( - Message(MsgTag.ROLLOUT_DONE, self._proxy.name, actor_proxy_addr, body=body) - ) + self._logger.info( + f"Roll-out finished for ep {rollout_index}, segment {segment_index}" + f"(steps {step_index} - {self.env.step_index})" + ) + self._proxy.reply( + msg, + tag=MsgTag.ROLLOUT_DONE, + body={ + MsgKey.END_OF_EPISODE: not self.env.state, + MsgKey.ROLLOUT_INDEX: rollout_index, + MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.EXPERIENCES: self.env.pull_experiences(copy=self._pull_experiences_with_copy) + } + ) diff --git a/maro/rl/distributed/actor_proxy.py b/maro/rl/distributed/actor_proxy.py deleted file mode 100644 index a44a8104a..000000000 --- a/maro/rl/distributed/actor_proxy.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from typing import Callable, List, Union - -from maro.communication import Message, Proxy, RegisterTable, SessionType -from maro.rl.agent import AbsAgent, MultiAgentWrapper -from maro.rl.storage import SimpleStore -from maro.utils import InternalLogger - -from .message_enums import MsgTag, MsgKey - - -class ActorProxy(object): - """Actor proxy that manages a set of remote actors. - - Args: - num_actors (int): Expected number of actors in the group identified by ``group_name``. - group_name (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the actors (and roll-out clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. - update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger - learner updates, i.e., model training. - """ - def __init__( - self, - num_actors: int, - group_name: str, - proxy_options: dict = None, - update_trigger: str = None, - experience_memory_size: int = -1, - experience_memory_overwrite_type: str = None - ): - peers = {"actor": num_actors} - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group_name, "actor_proxy", peers, **proxy_options) - self._actors = self._proxy.peers["actor"] # remote actor ID's - self._registry_table = RegisterTable(self._proxy.peers) - if update_trigger is None: - update_trigger = len(self._actors) - self._registry_table.register_event_handler( - f"actor:{MsgTag.ROLLOUT_DONE.value}:{update_trigger}", self._on_rollout_finish - ) - self.experience_memory = defaultdict( - lambda: SimpleStore(capacity=experience_memory_size, overwrite_type=experience_memory_overwrite_type) - ) - self.rollout_results = {} - self.logger = InternalLogger(self._proxy.name) - - def roll_out(self, index: int, training: bool = True, model_by_agent: dict = None, exploration_params=None): - """Collect roll-out data from remote actors. - - Args: - index (int): Index of roll-out requests. - training (bool): If true, the roll-out request is for training purposes. - model_by_agent (dict): Models to be broadcast to remote actors for inference. Defaults to None. - exploration_params: Exploration parameters to be used by the remote roll-out actors. Defaults to None. - """ - body = { - MsgKey.ROLLOUT_INDEX: index, - MsgKey.TRAINING: training, - MsgKey.MODEL: model_by_agent, - MsgKey.EXPLORATION_PARAMS: exploration_params - } - self._proxy.iscatter(MsgTag.ROLLOUT, SessionType.TASK, [(actor, body) for actor in self._actors]) - self.logger.info(f"Sent roll-out requests to {self._actors} for ep-{index}") - - def wait_for_actor_results(self): - # Receive roll-out results from remote actors - for msg in self._proxy.receive(): - if msg.body[MsgKey.ROLLOUT_INDEX] != index: - self.logger.info( - f"Ignore a message of type {msg.tag} with ep {msg.body[MsgKey.ROLLOUT_INDEX]} " - f"(expected {index} or greater)" - ) - continue - if msg.tag == MsgTag.ROLLOUT_DONE: - # If enough update messages have been received, call update() and break out of the loop to start - # the next episode. - result = self._registry_table.push(msg) - if result: - env_metrics = result - break - elif msg.tag == MsgTag.REPLAY_SYNC: - # print(f"received exp from actor {msg.source} ") - # print({agent_id: {k: len(v) for k, v in exp.items()} for agent_id, exp in msg.body[MsgKey.REPLAY].items()}) - for agent_id, exp in msg.body[MsgKey.REPLAY].items(): - self.experience_memory[agent_id].put(exp) - # print({agent_id: len(pool) for agent_id, pool in self.experience_memory.items()}) - - rollout_results[index] = env_metrics - - def _on_rollout_finish(self, messages: List[Message]): - metrics = {msg.source: msg.body[MsgKey.METRICS] for msg in messages} - for msg in messages: - for agent_id, replay in msg.body[MsgKey.REPLAY].items(): - self.experience_memory[agent_id].put(replay) - return metrics - - def terminate(self): - """Tell the remote actors to exit.""" - self._proxy.ibroadcast( - component_type="actor", tag=MsgTag.EXIT, session_type=SessionType.NOTIFICATION - ) - self.logger.info("Exiting...") diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index a27c1a1af..6a3fff74b 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -1,107 +1,101 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from abc import ABC, abstractmethod -from typing import Callable, Union - -from numpy import asarray +from collections import defaultdict +from typing import Union +from maro.communication import Message, Proxy, SessionType from maro.rl.agent import AbsAgent, MultiAgentWrapper from maro.rl.scheduling import Scheduler -from maro.rl.storage import SimpleStore from maro.utils import InternalLogger -from .actor_proxy import ActorProxy +from .message_enums import MsgTag, MsgKey -class AbsDistLearner(ABC): +class DistLearner(object): """Learner class for distributed training. Args: - actor_proxy (ActorProxy): ``ActorProxy`` instance that manages a set of remote actors to collect roll-out - data for learning purposes. agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. + scheduler (Scheduler): . + num_actors (int): Expected number of actors in the group identified by ``group_name``. + group_name (str): Identifier of the group to which the actor belongs. It must be the same group name + assigned to the actors (and roll-out clients, if any). + proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to None. + update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger + learner updates, i.e., model training. """ def __init__( self, - actor_proxy: ActorProxy, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, - + num_actors: int, + group_name: str, + proxy_options: dict = None, + agent_update_interval: int = -1, + min_actor_finishes: str = None, + ignore_stale_experiences: bool = True ): super().__init__() - self.actor_proxy = actor_proxy self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent - self.logger = InternalLogger("LEARNER") - - def run(self): - """Main learning loop is implemented here.""" - for exploration_params in self.scheduler: - rollout_index = self.scheduler.iter - env_metrics = self.actor_proxy.roll_out( - rollout_index, model_by_agent=self.agent.dump_model(), exploration_params=exploration_params - ) - self.logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") - - for _ in range(self.train_iter): - batch_by_agent, idx_by_agent = self.get_batch() - for agent_id, batch in batch_by_agent.items(): - self.agent[agent_id].learn(*batch) - - self.logger.info("Agent learning finished") - - # Signal remote actors to quit - self.actor_proxy.terminate() - - -class OnPolicyDistLearner(AbsDistLearner): - def __init__(self, actor_proxy: ActorProxy, agent: Union[AbsAgent, MultiAgentWrapper], max_episode: int): - super().__init__(actor_proxy, agent) - self.max_episode = max_episode - - def run(self): - for ep in range(self.max_episode): - env_metrics = self.actor_proxy.roll_out(ep, model_by_agent=self.agent.dump_model()) - self.logger.info(f"ep-{ep}: {env_metrics}") - for agent_id, replay in self.actor_proxy.replay_memory.items(): - self.agent[agent_id].learn(replay["S"], replay["A"], replay["LOGP"], replay["R"], replay["S_"]) - - self.logger.info("Agent learning finished") - - # Signal remote actors to quit - self.actor_proxy.terminate() - - -class OffPolicyDistLearner(AbsDistLearner): - def __init__( - self, - actor_proxy: ActorProxy, - agent: Union[AbsAgent, MultiAgentWrapper], - scheduler: Scheduler, - train_iter: int = 1, - min_experiences_to_train: int = 0, - batch_size: int = 128 - ): - super().__init__(actor_proxy, agent) self.scheduler = scheduler - self.train_iter = train_iter - self.min_experiences_to_train = min_experiences_to_train - self.batch_size = batch_size + peers = {"actor": num_actors} + if proxy_options is None: + proxy_options = {} + self._proxy = Proxy(group_name, "learner", peers, **proxy_options) + self.actors = self._proxy.peers["actor"] # remote actor ID's + if min_actor_finishes is None: + self.min_actor_finishes = len(self.actors) + self.agent_update_interval = agent_update_interval + self.ignore_stale_experiences = ignore_stale_experiences + self._logger = InternalLogger("LEARNER") def run(self): + """Main learning loop""" for exploration_params in self.scheduler: - rollout_index = self.scheduler.iter - env_metrics = self.actor_proxy.roll_out( - rollout_index, model_by_agent=self.agent.dump_model(), exploration_params=exploration_params - ) - self.logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") - - for _ in range(self.train_iter): - batch_by_agent, idx_by_agent = self.get_batch() - for agent_id, batch in batch_by_agent.items(): - self.agent[agent_id].learn(*batch) - - self.logger.info("Agent learning finished") - - # Signal remote actors to quit - self.actor_proxy.terminate() + updated_agents = self.agent.names + rollout_index, segment_index, num_episode_finishes = self.scheduler.iter, 0, 0 + while num_episode_finishes < self.min_actor_finishes: + msg_body = { + MsgKey.ROLLOUT_INDEX: rollout_index, + MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.NUM_STEPS: self.agent_update_interval, + MsgKey.MODEL: self.agent.dump_model(agent_ids=updated_agents) + } + if segment_index == 0 and exploration_params: + msg_body[MsgKey.EXPLORATION_PARAMS] = exploration_params + self._proxy.ibroadcast("actor", MsgTag.ROLLOUT, SessionType.TASK, body=msg_body) + self._logger.info(f"Sent roll-out requests for ep-{rollout_index}, segment-{segment_index}") + + # Receive roll-out results from remote actors + updated_agents, num_segment_finishes = set(), 0 + for msg in self._proxy.receive(): + if msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: + self._logger.info( + f"Ignore a message of type {msg.tag} with ep {msg.body[MsgKey.ROLLOUT_INDEX]} " + f"(expected {index})" + ) + continue + + # if msg.tag == MsgTag.EXPERIENCE: + # print(f"received exp from actor {msg.source} ") + # print({agent_id: {k: len(v) for k, v in exp.items()} for agent_id, exp in msg.body[MsgKey.EXPERIENCE].items()}) + # If enough update messages have been received, call update() and break out of the loop to start + # the next episode. + if msg.body[MsgKey.SEGMENT_INDEX] == segment_index or not self.ignore_stale_experiences: + updated_agents.update(self.agent.update(msg.body[MsgKey.EXPERIENCES])) + self._logger.info(f"Learning finished for agent {updated_agents}") + if msg.body[MsgKey.END_OF_EPISODE]: + num_episode_finishes += 1 + num_segment_finishes += 1 + if num_segment_finishes == self.min_actor_finishes: + break + + segment_index += 1 + # self._logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") + + def terminate(self): + """Tell the remote actors to exit.""" + self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) + self._logger.info("Exiting...") diff --git a/maro/rl/distributed/message_enums.py b/maro/rl/distributed/message_enums.py index 61c375c7f..7aca877b2 100644 --- a/maro/rl/distributed/message_enums.py +++ b/maro/rl/distributed/message_enums.py @@ -3,9 +3,10 @@ class MsgTag(Enum): ROLLOUT = "rollout" + AGENT_UPDATE = "agent_update" CHOOSE_ACTION = "choose_action" ACTION = "action" - REPLAY_SYNC = "replay_sync" + EXPERIENCE_SYNC = "experience_sync" TRAIN = "train" ABORT_ROLLOUT = "abort_rollout" ROLLOUT_DONE = "rollout_done" @@ -18,8 +19,12 @@ class MsgKey(Enum): ROLLOUT_INDEX = "rollout_index" TIME_STEP = "time_step" METRICS = "metrics" - REPLAY = "replay" + EXPERIENCES = "experiences" STATE = "state" TRAINING = "training" MODEL = "model" + VERSION = "version" EXPLORATION_PARAMS = "exploration_params" + NUM_STEPS = "num_steps" + SEGMENT_INDEX = "segment_index" + END_OF_EPISODE = "end_of_episode" diff --git a/maro/rl/distributed/trainer.py b/maro/rl/distributed/trainer.py index c66d20757..82777e1a7 100755 --- a/maro/rl/distributed/trainer.py +++ b/maro/rl/distributed/trainer.py @@ -19,7 +19,7 @@ def trainer(id_: str): def train(sock, msg): client, _, request = msg request = pickle.loads(request) - info = request["agent"].learn(*request["args"], **request["kwargs"]) + info = request["agent"].step(*request["args"], **request["kwargs"]) request.update({"model": request["agent"].dump_model(), "info": info}) del request["agent"] del request["args"] diff --git a/maro/rl/storage/__init__.py b/maro/rl/storage/__init__.py index c916fcf76..8b8173d2b 100644 --- a/maro/rl/storage/__init__.py +++ b/maro/rl/storage/__init__.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from .abs_store import AbsStore +from .sampler import AbsSampler, UniformSampler from .simple_store import SimpleStore -__all__ = ["AbsStore", "SimpleStore"] +__all__ = ["AbsSampler", "AbsStore", "SimpleStore", "UniformSampler"] diff --git a/maro/rl/storage/sampler.py b/maro/rl/storage/sampler.py index b1252a163..0818f903c 100644 --- a/maro/rl/storage/sampler.py +++ b/maro/rl/storage/sampler.py @@ -45,14 +45,3 @@ def sample(self, size: int): def update(self): pass - - -class PrioritizedSampler(AbsSampler): - def __init__(self, data): - super().__init__(data) - - def sample(self, size: int): - pass - - def update(self): - pass diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index d6efddc3b..c9873a0ef 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -28,9 +28,10 @@ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = self.state_info = None # context for converting model output to actions that can be executed by the env self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay + self._state = None # the latest extracted state is kept here self._acting_agents = deque() # list of (tick, acting_agent_list) for delayed reward evaluation - self._tot_raw_step_time = 0 - self._tot_step_time = 0 + # self._tot_raw_step_time = 0 + # self._tot_step_time = 0 @property def step_index(self): @@ -39,18 +40,16 @@ def step_index(self): def start(self, rollout_index: int = None): self._step_index = 0 _, event, _ = self.env.step(None) - state_by_agent = self.get_state(event) + self._state = self.get_state(event) if self.save_replay: - for agent_id, state in state_by_agent.items(): + for agent_id, state in self._state.items(): replay = self.replay[agent_id] if replay["S"]: replay["S_"].append(state) replay["S"].append(state) assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 - return state_by_agent - - def get_experiences(self, copy: bool = False): + def pull_experiences(self, copy: bool = False): experience = defaultdict(dict) for agent_id, replay in self.replay.items(): num_complete = min(len(replay["R"]), len(replay["S_"])) @@ -65,6 +64,10 @@ def get_experiences(self, copy: bool = False): def metrics(self): return self.env.metrics + @property + def state(self): + return self._state + @abstractmethod def get_state(self, event) -> dict: pass @@ -85,16 +88,16 @@ def get_reward(self, tick: int = None, target_agents: list = None) -> dict: pass def step(self, action_by_agent: dict): - t0 = time.time() + # t0 = time.time() self._step_index += 1 env_action = self.get_action(action_by_agent) self._acting_agents.append((self.env.tick, list(env_action.keys()))) if len(env_action) == 1: env_action = list(env_action.values())[0] - t1 = time.time() + # t1 = time.time() _, event, done = self.env.step(env_action) - t2 = time.time() - self._tot_raw_step_time += t2 - t1 + # t2 = time.time() + # self._tot_raw_step_time += t2 - t1 if self.save_replay: for agent_id, action in action_by_agent.items(): @@ -114,26 +117,28 @@ def step(self, action_by_agent: dict): self._acting_agents.popleft() if not done: - state_by_agent = self.get_state(event) + self._state = self.get_state(event) if self.save_replay: - for agent_id, state in state_by_agent.items(): + for agent_id, state in self._state.items(): replay = self.replay[agent_id] if replay["S"]: replay["S_"].append(state) replay["S"].append(state) assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 - t3 = time.time() - self._tot_step_time += t3 - t0 - return state_by_agent + # t3 = time.time() + # self._tot_step_time += t3 - t0 + else: + self._state = None - print(f"total raw step time: {self._tot_raw_step_time}") - print(f"total step time: {self._tot_step_time}") - self._tot_raw_step_time = 0 - self._tot_step_time = 0 + # print(f"total raw step time: {self._tot_raw_step_time}") + # print(f"total step time: {self._tot_step_time}") + # self._tot_raw_step_time = 0 + # self._tot_step_time = 0 def reset(self): self.env.reset() self.state_info = None + self._state = None self._acting_agents.clear() self.replay = defaultdict(lambda: defaultdict(list)) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 706715a53..94756413d 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -25,21 +25,15 @@ def __init__( env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, - learning_interval: Union[int, Dict[str, int]] = -1, - end_of_episode_learning: bool = True + agent_update_interval: int = None ): super().__init__() + if agent_update_interval == 0: + raise ValueError("agent_update_interval must be a positive integer or None.") self.env = env self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent self.scheduler = scheduler - self.online = learning_interval != -1 - if isinstance(learning_interval, int): - assert learning_interval == -1 or learning_interval > 0, \ - f"learning_interval must be -1 or a positive integer" - self.learning_interval = {agent_id: learning_interval for agent_id in self.agent.names} - else: - self.learning_interval = learning_interval - self.end_of_episode_learning = end_of_episode_learning + self.agent_update_interval = agent_update_interval self.logger = InternalLogger("LEARNER") def run(self): @@ -49,28 +43,14 @@ def run(self): if exploration_params: self.agent.set_exploration_params(exploration_params) - pending_learning_agents = defaultdict(list) - for agent_id, interval in self.learning_interval.items(): - pending_learning_agents[interval].append(agent_id) - - state = self.env.start(rollout_index=self.scheduler.iter) # get initial state - while state: - action = self.agent.choose_action(state) - state = self.env.step(action) - if self.online and self.env.step_index in pending_learning_agents: - self.agent.store_experiences(self.env.get_experiences()) - for agent_id in pending_learning_agents[self.env.step_index]: - self.agent.learn(agent_id) - next_learning_time = self.env.step_index + self.learning_interval[agent_id] - if next_learning_time > self.env.step_index: - pending_learning_agents[next_learning_time].append(agent_id) - del pending_learning_agents[self.env.step_index] - print(f"step = {self.env.step_index}, next up: {pending_learning_agents}") - - if self.end_of_episode_learning: - self.agent.store_experiences(self.env.get_experiences()) - self.agent.learn() + self.env.start(rollout_index=self.scheduler.iter) # get initial state + while self.env.state: + action = self.agent.choose_action(self.env.state) + self.env.step(action) + if self.agent_update_interval is not None and self.env.step_index % self.agent_update_interval == 0: + self.agent.update(self.env.pull_experiences()) + self.agent.update(self.env.pull_experiences()) self.logger.info(f"ep-{self.scheduler.iter}: {self.env.metrics} ({exploration_params})") # t1 = time.time() diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 9dcb9994f..69a92edb2 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .sampler_cls_index import get_sampler_cls from .torch_cls_index import ( get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls ) @@ -8,6 +9,7 @@ from .value_utils import get_log_prob, get_max, get_td_errors, select_by_actions __all__ = [ + "get_sampler_cls", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", "get_log_prob", "get_max", "get_td_errors", "select_by_actions" diff --git a/maro/rl/utils/sampler_cls_index.py b/maro/rl/utils/sampler_cls_index.py new file mode 100644 index 000000000..1910fe951 --- /dev/null +++ b/maro/rl/utils/sampler_cls_index.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.rl.storage import UniformSampler + +SAMPLER = { + "uniform": UniformSampler, +} + +def get_sampler_cls(sampler_type): + if isinstance(sampler_type, str): + if sampler_type not in SAMPLER: + raise KeyError(f"A string sampler_type must be one of {list(SAMPLER.keys())}.") + return SAMPLER[sampler_type] + + return sampler_type From 6f21941aee5aed2bc2651a6fc9f45d2199680939 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 5 Apr 2021 08:58:35 +0800 Subject: [PATCH 149/482] make supply chain trainning work --- examples/supply_chain/env_wrapper.py | 285 ++++++++++++++++-- maro/rl/utils/value_utils.py | 4 + .../scenarios/supply_chain/business_engine.py | 2 +- .../supply_chain/facilities/facility.py | 3 + .../simulator/scenarios/supply_chain/world.py | 15 +- tests/supply_chain/test_supply_chain.py | 32 +- 6 files changed, 300 insertions(+), 41 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 543bd82a8..ad42c7b5e 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -1,13 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from maro.simulator import Env from collections import defaultdict, namedtuple - -import numpy as np import scipy.stats as st - +import numpy as np from maro.rl import AbsEnvWrapper -from maro.simulator import Env from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction @@ -62,6 +60,8 @@ def __getitem__(self, key, default=None): class SCEnvWrapper(AbsEnvWrapper): def __init__(self, env: Env): super().__init__(env) + self.balance_cal = BalanceSheetCalculator(env) + self.cur_balance_sheet_reward = None self.storage_ss = env.snapshot_list["storage"] self._summary = env.summary['node_mapping'] @@ -71,6 +71,8 @@ def __init__(self, env: Env): self._agent_list = env.agent_idx_list self._sku_number = len(self._summary["skus"]) + 1 + self._max_price = self._summary["max_price"] + self._max_sources_per_facility = self._summary["max_sources_per_facility"] # state for each tick self._cur_metrics = env.metrics @@ -79,7 +81,8 @@ def __init__(self, env: Env): # TODO: this is fixed after env setup self._cur_facility_storage_product_mapping = {} - self._max_sources_per_facility = self._cur_metrics["max_sources_per_facility"] + self._service_index_ppf_cache = {} + # facility -> { # data_model_index:int, @@ -144,11 +147,14 @@ def __init__(self, env: Env): if manufacture is not None: product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"]] = facility_id + self.unit_2_facility_dict[manufacture["id"] + ] = facility_id self.facility_levels[facility_id][product_id] = product_info def get_state(self, event): + self.cur_balance_sheet_reward = self.balance_cal.calc() + return self._get_state() def get_action(self, action_by_agent): @@ -175,42 +181,42 @@ def get_action(self, action_by_agent): env_action[agent_id] = ConsumerAction(agent_id, product_id, source_id, action, 1) # manufacturer action elif agent_id.startswith("producer"): + agent_id = int(agent_id.split(".")[1]) env_action[agent_id] = ManufactureAction(agent_id, action) return env_action def get_reward(self, tick=None): - step_rewards = self._cur_metrics["step_rewards"] - step_balance_sheet = self._cur_metrics["step_balance_sheet"] - wc = self.env.configs.settings["global_reward_weight_consumer"] parent_facility_balance = {} - for f_id, sheet in step_balance_sheet.items(): + for f_id, sheet in self.cur_balance_sheet_reward.items(): if f_id in self.unit_2_facility_dict: # it is a product unit - parent_facility_balance[f_id] = step_balance_sheet[self.unit_2_facility_dict[f_id]] + parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] else: parent_facility_balance[f_id] = sheet - consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id] + (1 - wc) * reward for f_id, reward in step_balance_sheet.items() } + consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items() } return { - **{f"producer.{f_id}": np.float32(reward) for f_id, reward in step_balance_sheet.items()}, + **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} } def _get_state(self): self._cur_metrics = self.env.metrics + state = {} + for agent_info in self._agent_list: storage_index = self.facility_levels[agent_info.facility_id]['storage'].node_index storage_product_list = self.storage_ss[self.env.tick:storage_index:"product_list"].flatten().astype(np.int) storage_product_number = self.storage_ss[self.env.tick:storage_index:"product_number"].flatten().astype(np.int) - self._cur_facility_storage_products = {pid: pnum for pid, pnum in zip(storage_product_list, storage_product_number)} + self._cur_facility_storage_products = { pid:pnum for pid, pnum in zip(storage_product_list, storage_product_number)} self._cur_facility_storage_product_mapping = {product_id: i for i, product_id in enumerate(storage_product_list)} @@ -268,7 +274,7 @@ def _add_facility_features(self, state, agent_info): metrics = self._cur_metrics["facilities"][agent_info.facility_id] - state['is_positive_balance'] = 1 if metrics["total_balance_sheet"].total() > 0 else 0 + state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 else: # a product unit # 3rd slot is the facility id of this unit @@ -276,7 +282,7 @@ def _add_facility_features(self, state, agent_info): state['sku_info'] = agent_info.sku metrics = self._cur_metrics["products"][agent_info.id] - state['is_positive_balance'] = 1 if metrics["total_balance_sheet"].total() > 0 else 0 + state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 # NOTE: ignore constraint here @@ -460,14 +466,19 @@ def _add_consumer_features(self, state, agent_info): if (state['inventory_estimated'] <= 0): state['is_out_of_stock'] = 1 + service_index = state['service_level'] + + if service_index not in self._service_index_ppf_cache: + self._service_index_ppf_cache[service_index] = st.norm.ppf(service_index) + state['inventory_rop'] = (state['max_vlt']*state['sale_mean'] - + np.sqrt(state['max_vlt'])*state['sale_std']*st.norm.ppf(state['service_level'])) + + np.sqrt(state['max_vlt'])*state['sale_std']*self._service_index_ppf_cache[service_index]) if state['inventory_estimated'] < state['inventory_rop']: state['is_below_rop'] = 1 def _add_price_features(self, state, agent_info): - state['max_price'] = self._cur_metrics["max_price"] + state['max_price'] = self._max_price state['sku_price'] = 0 state['sku_cost'] = 0 @@ -477,6 +488,7 @@ def _add_price_features(self, state, agent_info): def _serialize_state(self, state): result = defaultdict(list) + keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'constraint_idx', 'is_accepted', 'consumption_hist']), ('storage_capacity', ['storage_utilization']), @@ -501,3 +513,240 @@ def _serialize_state(self, state): result[agent_id] = np.asarray(result[agent_id], dtype=np.float32) return result + +class BalanceSheetCalculator: + consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") + seller_features = ("id", "sold", "demand", "price", "backlog_ratio") + manufacture_features = ("id", "manufacturing_number", "product_unit_cost") + product_features = ("id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") + storage_features = ("capacity", "remaining_space") + vehicle_features= ("id", "payload", "unit_transport_cost") + + def __init__(self, env: Env): + self.env = env + self.consumer_ss = env.snapshot_list["consumer"] + self.seller_ss = env.snapshot_list["seller"] + self.manufacture_ss = env.snapshot_list["manufacture"] + self.storage_ss = env.snapshot_list["storage"] + self.distribution_ss = env.snapshot_list["distribution"] + self.vehicle_ss = env.snapshot_list["vehicle"] + self.product_ss = env.snapshot_list["product"] + self.products = [] + self.product_id2index_dict = {} + self.facility_levels = [] + + self.facilities = env.summary["node_mapping"]["facilities"] + + for facility_id, facility in self.facilities.items(): + pid_list = [] + distribution = facility["units"]["distribution"] + + for product_id, product in facility["units"]["products"].items(): + pid_list.append(product["id"]) + consumer = product["consumer"] + seller = product["seller"] + manufacture = product["manufacture"] + + self.product_id2index_dict[product["id"]] = len(self.products) + + self.products.append(( + product["id"], + product_id, + facility["units"]["storage"]["node_index"], + facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution["node_index"] if distribution is not None else None, + facility["downstreams"], + None if consumer is None else (consumer["id"], consumer["node_index"]), + None if seller is None else (seller["id"], seller["node_index"]), + None if manufacture is None else (manufacture["id"], manufacture["node_index"]), + )) + + self.facility_levels.append(( + facility_id, + pid_list, + facility["units"]["storage"]["node_index"], + facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution["node_index"] if distribution is not None else None, + [v["node_index"] for v in distribution["children"]] if distribution is not None else [] + )) + + self.total_balance_sheet = defaultdict(int) + + def calc(self): + tick = self.env.tick + # consumer + consumer_bs_states = self.consumer_ss[tick::self.consumer_features].flatten().reshape(-1, len(self.consumer_features)) + + # quantity * price + consumer_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] + + # balance_sheet_profit = 0 + # order_cost + order_product_cost + consumer_step_balance_sheet_loss = -1 * (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) + + # not sure about this. + reward_discount = 0 + + # consumer step reward: balance sheet los + profile * discount + consumer_step_reward = consumer_step_balance_sheet_loss + consumer_profit * reward_discount + + # seller + seller_bs_states = self.seller_ss[tick::self.seller_features].flatten().reshape(-1, len(self.seller_features)) + + # profit = sold * price + seller_balance_sheet_profit = seller_bs_states[:, 1] * seller_bs_states[:, 3] + + # loss = demand * price * backlog_ratio + seller_balance_sheet_loss = -1 * seller_bs_states[:, 2] * seller_bs_states[:, 3] * seller_bs_states[:, 4] + + # step reward = loss + profit + seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit + + # manufacture + man_bs_states = self.manufacture_ss[tick::self.manufacture_features].flatten().reshape(-1, len(self.manufacture_features)) + + # loss = manufacture number * cost + man_balance_sheet_profit_loss = -1 * man_bs_states[:, 1] * man_bs_states[:, 2] + + # step reward = loss + man_step_reward = man_balance_sheet_profit_loss + + # product + product_bs_states = self.product_ss[tick::self.product_features].flatten().reshape(-1, len(self.product_features)) + + # product distribution loss = check order + delay order penalty + product_distribution_balance_sheet_loss= -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) + + # product distribution profit = check order * price + product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] + + # result we need + product_step_reward = np.zeros((len(self.products,))) + product_balance_sheet_profit = np.zeros((len(self.products,))) + product_balance_sheet_loss = np.zeros((len(self.products, ))) + + # create product number mapping for storages + storages_product_map = {} + for storage_index in range(len(self.storage_ss)): + product_list = self.storage_ss[tick:storage_index:"product_list"].flatten().astype(np.int) + product_number = self.storage_ss[tick:storage_index:"product_number"].flatten().astype(np.int) + + storages_product_map[storage_index] = {pid:pnum for pid, pnum in zip(product_list, product_number)} + + # product balance sheet and reward + # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss + # profit = same as above + # reward = same as above + for i, product in enumerate(self.products): + id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = product + + if consumer: + product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer[1]] + product_step_reward[i] += consumer_step_reward[consumer[1]] + + if seller: + product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller[1]] + product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller[1]] + product_step_reward[i] += seller_step_reward[seller[1]] + + if manufacture: + product_balance_sheet_loss[i] += man_balance_sheet_profit_loss[manufacture[1]] + product_step_reward[i] += man_step_reward[manufacture[1]] + + storage_reward = -1 * storages_product_map[storage_index][product_id] * unit_storage_cost + + product_step_reward[i] += storage_reward + + product_balance_sheet_loss[i] += storage_reward + + if distribution_index is not None: + product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[distribution_index] + product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[distribution_index] + + product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + product_distribution_balance_sheet_profit[distribution_index] + + if downstreams and len(downstreams) > 0: + if product_id in downstreams: + for dfacility in downstreams[product_id]: + dproducts = self.facilities[dfacility]["units"]["products"] + + did = dproducts[product_id]["id"] + + product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] + product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] + product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] + + product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss + + # storage + storage_states = self.storage_ss[tick::self.storage_features].flatten().reshape(-1, len(self.storage_features)) + + # loss = (capacity-remaining space) * cost + storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) + + # vehicles + vehicle_states = self.vehicle_ss[tick::self.vehicle_features].flatten().reshape(-1, len(self.vehicle_features)) + + # loss = cost * payload + vehicle_balance_sheet_loss = -1 * vehicle_states[:, 1] * vehicle_states[:, 2] + vehicle_step_reward = vehicle_balance_sheet_loss + + facility_balance_sheet_loss = np.zeros((len(self.facility_levels),)) + facility_balance_sheet_profit = np.zeros((len(self.facility_levels),)) + facility_step_reward = np.zeros((len(self.facility_levels),)) + + # for facilities + for i, facility in enumerate(self.facility_levels): + id, pid_list, storage_index, unit_storage_cost, distribution_index, vehicle_indices = facility + + # storage balance sheet + # profit=0 + facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * unit_storage_cost + + # distribution balance sheet + if distribution_index is not None: + for vindex in vehicle_indices: + facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vindex] + # distribution unit do not provide reward + + # sku product unit balance sheet + for pid in pid_list: + facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] + facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] + facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] + + result = {} + + for id, bs, rw in zip([item[0] for item in self.products], product_balance_sheet, product_step_reward): + result[id] = (bs, rw) + + self.total_balance_sheet[id] += bs + + facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit + + for id, bs, rw in zip([item[0] for item in self.facility_levels], facility_balance_sheet, facility_step_reward): + result[id] = (bs, rw) + + self.total_balance_sheet[id] += bs + + return result + + +if __name__ == "__main__": + from time import time + + start_tick = 0 + durations = 100 + env = Env(scenario="supply_chain", topology="sample1", + start_tick=start_tick, durations=durations) + + ss = SCEnvWrapper(env) + + env.step(None) + + states = ss.get_state(None) + + rewards = ss.get_reward(None) + + print(states) + print(rewards) diff --git a/maro/rl/utils/value_utils.py b/maro/rl/utils/value_utils.py index 99de02b6e..b8dc5fd41 100644 --- a/maro/rl/utils/value_utils.py +++ b/maro/rl/utils/value_utils.py @@ -9,6 +9,10 @@ def select_by_actions(q_values: torch.Tensor, actions: torch.Tensor): if len(actions.shape) == 1: actions = actions.unsqueeze(1) # (N, 1) + + if actions.dtype != torch.int64: + actions = actions.to(torch.int64) + return q_values.gather(1, actions).squeeze(1) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index ad4b1cdff..b77137913 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -136,7 +136,7 @@ def _dispatch_action(self): if self._action_cache is not None: # NOTE: we assume that the action is dictionary that key is the unit(agent) id, value is the real action. for unit_id, action_obj in self._action_cache.items(): - entity = self.world.get_unit(unit_id) + entity = self.world.get_entity(unit_id) if entity is not None and issubclass(type(entity), UnitBase): entity.set_action(action_obj) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index d1815512f..e7dfa285e 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -140,6 +140,9 @@ def get_in_transit_orders(self): return in_transit_orders + def set_action(self, action: object): + pass + def get_node_info(self) -> dict: products_info = {} diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index e0eb797cf..7f280d7be 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -30,7 +30,7 @@ def __init__(self): # Durations of current simulation. self.durations = 0 - # All the units in the world. + # All the entities in the world. self.units = {} # All the facilities in this world. @@ -105,16 +105,19 @@ def get_facility_by_name(self, name: str): """ return self.facilities[self._facility_name2id_mapping[name]] - def get_unit(self, unit_id: int) -> UnitBase: - """Get an unit by id. + def get_entity(self, id: int) -> Union[FacilityBase, UnitBase]: + """Get an entity(Unit or Facility) by id. Args: - unit_id (int): Id to query. + id (int): Id to query. Returns: - UnitBase: Unit instance. + Union[FacilityBase, UnitBase]: Unit or facility instance. """ - return self.units[unit_id] + if id in self.units: + return self.units[id] + else: + return self.facilities[id] def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: """Find path to specified cell. diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 052b722e9..b5a34c666 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -374,7 +374,7 @@ def test_storage_take_available(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_unit(storage_unit_id) + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) @@ -445,7 +445,7 @@ def test_storage_try_add_products(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_unit(storage_unit_id) + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) @@ -520,7 +520,7 @@ def test_storage_try_take_products(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_unit(storage_unit_id) + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) @@ -573,7 +573,7 @@ def test_storage_get_product_number(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_unit(storage_unit_id) + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) @@ -626,7 +626,7 @@ def test_consumer_init_state(self): # try to find sku3 consumer sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env._business_engine.world.get_unit(sku3_consumer_unit_id) + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] if facility_defail["name"] == "Supplier_SKU3": @@ -727,7 +727,7 @@ def test_consumer_action(self): if facility_defail["name"] == "Supplier_SKU1": sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env._business_engine.world.get_unit(sku3_consumer_unit_id) + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] if facility_defail["name"] == "Supplier_SKU3": @@ -841,7 +841,7 @@ def test_consumer_on_order_reception(self): if facility_defail["name"] == "Supplier_SKU1": sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env._business_engine.world.get_unit(sku3_consumer_unit_id) + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] if facility_defail["name"] == "Supplier_SKU3": @@ -895,7 +895,7 @@ def test_vehicle_unit_state(self): for id, info in env.summary["node_mapping"]["unit_mapping"].items(): if info[0] == "vehicle": vehicle_unit_id = id - vehicle_unit = env._business_engine.world.get_unit(id) + vehicle_unit = env._business_engine.world.get_entity(id) vehicle_unit_data_model_index = vehicle_unit.data_model_index break @@ -979,7 +979,7 @@ def test_vehicle_unit_schedule(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_unit(v["id"]) + vehicle_unit = env._business_engine.world.get_entity(v["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1111,7 +1111,7 @@ def test_vehicle_unit_no_patient(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_unit(v["id"]) + vehicle_unit = env._business_engine.world.get_entity(v["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1207,7 +1207,7 @@ def test_vehicle_unit_cannot_unload_at_destination(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_unit(v["id"]) + vehicle_unit = env._business_engine.world.get_entity(v["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1269,7 +1269,7 @@ def test_distribution_unit_initial_state(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": - dist_unit = env._business_engine.world.get_unit(info["units"]["distribution"]["id"]) + dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1308,7 +1308,7 @@ def test_distribution_unit_dispatch_order(self): for id, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": - dist_unit = env._business_engine.world.get_unit(info["units"]["distribution"]["id"]) + dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) if info["name"] == "Warehouse_001": dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1385,7 +1385,7 @@ def test_seller_unit_initial_states(self): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_unit(pdetail["seller"]["id"]) + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) if info["name"] == "Warehouse_001": source_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1433,7 +1433,7 @@ def test_seller_unit_demand_states(self): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_unit(pdetail["seller"]["id"]) + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) if info["name"] == "Warehouse_001": source_facility = env._business_engine.world.get_facility_by_id(info["id"]) @@ -1499,7 +1499,7 @@ def test_seller_unit_customized(self): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_unit(pdetail["seller"]["id"]) + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) if info["name"] == "Warehouse_001": source_facility = env._business_engine.world.get_facility_by_id(info["id"]) From 9d55fd64d1d1b572cce6f67e4080839e77e8d085 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 5 Apr 2021 10:25:57 +0800 Subject: [PATCH 150/482] fix unit tests --- .../scenarios/cim/ports_order_export.py | 2 +- .../supply_chain/datamodels/distribution.py | 3 - .../supply_chain/datamodels/vehicle.py | 8 +- .../scenarios/supply_chain/units/consumer.py | 2 +- .../supply_chain/units/distribution.py | 4 +- .../scenarios/supply_chain/units/vehicle.py | 18 +- tests/data/supply_chain/case_01/config.yml | 22 +- tests/data/supply_chain/case_02/config.yml | 31 ++- tests/data/supply_chain/case_03/config.yml | 32 ++- tests/supply_chain/test_supply_chain.py | 247 ++++-------------- 10 files changed, 136 insertions(+), 233 deletions(-) diff --git a/maro/simulator/scenarios/cim/ports_order_export.py b/maro/simulator/scenarios/cim/ports_order_export.py index d69dac10e..baf66b106 100644 --- a/maro/simulator/scenarios/cim/ports_order_export.py +++ b/maro/simulator/scenarios/cim/ports_order_export.py @@ -37,6 +37,6 @@ def dump(self, folder: str): for order in self._orders: writer.writerow( - [order.tick, order.src_port_idx, order.dest_port_idx, order.quantity]) + [order.tick, order.src_port_idx, order.dest_port_idx, order.requested_quantity]) self._orders.clear() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py index 5acc6c109..ee28e4c7f 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py @@ -11,7 +11,6 @@ @node("distribution") class DistributionDataModel(DataModelBase): """Distribution data model for distribution unit.""" - delay_order_penalty = NodeAttribute(AttributeType.UInt, 1, is_list=True) remaining_order_quantity = NodeAttribute(AttributeType.UInt) remaining_order_number = NodeAttribute(AttributeType.UInt) @@ -19,7 +18,5 @@ class DistributionDataModel(DataModelBase): def __init__(self): super(DistributionDataModel, self).__init__() - self._unit_price = 0 - def reset(self): super(DistributionDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py index dac99a9b3..34cb1df64 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py @@ -13,19 +13,14 @@ class VehicleDataModel(DataModelBase): # Number of product. payload = NodeAttribute(AttributeType.UInt) - # Patient to wait for products ready. - patient = NodeAttribute(AttributeType.UInt) - unit_transport_cost = NodeAttribute(AttributeType.Float) def __init__(self): super(VehicleDataModel, self).__init__() - self._patient = 0 self._unit_transport_cost = 1 - def initialize(self, patient: int = 100, unit_transport_cost: int = 1): - self._patient = patient + def initialize(self, unit_transport_cost: int = 1): self._unit_transport_cost = unit_transport_cost self.reset() @@ -33,5 +28,4 @@ def initialize(self, patient: int = 100, unit_transport_cost: int = 1): def reset(self): super(VehicleDataModel, self).reset() - self.patient = self._patient self.unit_transport_cost = self._unit_transport_cost diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 5ea7450ee..d6fb88045 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -127,7 +127,7 @@ def flush_states(self): self.data_model.order_product_cost = self.order_product_cost if self.action is not None and self.action.quantity > 0: - self.data_model.order_quantity = self.action + self.data_model.order_quantity = self.action.quantity def post_step(self, tick: int): # Clear the action states per step. diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 0f3b4ff93..0bfd11379 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -80,7 +80,7 @@ def initialize(self): def step(self, tick: int): for vehicle in self.vehicles: # If we have vehicle not on the way and there is any pending order. - if len(self.order_queue) > 0 and vehicle.quantity == 0: + if len(self.order_queue) > 0 and vehicle.requested_quantity == 0: order = self.order_queue.popleft() # Schedule a job for available vehicle. @@ -105,7 +105,7 @@ def step(self, tick: int): def flush_states(self): super(DistributionUnit, self).flush_states() - + for vehicle in self.vehicles: vehicle.flush_states() diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index 39cb2864a..d1751896b 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -36,7 +36,7 @@ def __init__(self): # Velocity. self.velocity = 0 - self.quantity = 0 + self.requested_quantity = 0 self.patient = 0 self.cost = 0 self.unit_transport_cost = 0 @@ -52,7 +52,7 @@ def schedule(self, destination: object, product_id: int, quantity: int, vlt: int """ self.product_id = product_id self.destination = destination - self.quantity = quantity + self.requested_quantity = quantity # Find the path from current entity to target. self.path = self.world.find_path( @@ -110,6 +110,7 @@ def try_unload(self): ) self.payload -= unloaded_units + self.data_model.payload = self.payload def is_enroute(self): return self.destination is not None @@ -120,7 +121,7 @@ def initialize(self): patient = self.config.get("patient", 100) self.unit_transport_cost = self.config.get("unit_transport_cost", 1) - self.data_model.initialize(patient=patient, unit_transport_cost=self.unit_transport_cost) + self.data_model.initialize(unit_transport_cost=self.unit_transport_cost) self.max_patient = patient @@ -131,7 +132,7 @@ def step(self, tick: int): if self.location == 0 and self.payload == 0: # then try to load by requested. - if self.try_load(self.quantity): + if self.try_load(self.requested_quantity): # NOTE: here we return to simulate loading return else: @@ -142,7 +143,7 @@ def step(self, tick: int): self.destination.products[self.product_id].consumer.update_open_orders( self.facility.id, self.product_id, - -self.quantity + -self.requested_quantity ) self._reset_internal_states() @@ -160,11 +161,11 @@ def step(self, tick: int): else: # Avoid update under idle state. if self.location > 0: - # try to unload + # Try to unload./////////////////////////////////////////////////////////////////// if self.payload > 0: self.try_unload() - # back to source if we unload all + # Back to source if we unload all. if self.payload == 0: self._reset_internal_states() self._reset_data_model() @@ -184,11 +185,10 @@ def _reset_internal_states(self): self.product_id = 0 self.steps = 0 self.location = 0 - self.quantity = 0 + self.requested_quantity = 0 self.velocity = 0 self.patient = self.max_patient def _reset_data_model(self): # Reset data model. self.data_model.payload = 0 - self.data_model.patient = self.max_patient diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml index 30578d93a..4dee0fab6 100644 --- a/tests/data/supply_chain/case_01/config.yml +++ b/tests/data/supply_chain/case_01/config.yml @@ -24,13 +24,15 @@ facility_definitions: class: "ProductUnit" is_template: true config: + agent_type: 3 consumer: class: "ConsumerUnit" manufacture: class: "ManufactureUnit" distribution: class: "DistributionUnit" - + config: + agent_type: 0 small_storage: &small_storage config: capacity: 100 @@ -176,3 +178,21 @@ world: - [10, 11] - [10, 12] - [11, 12] + +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e7 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 diff --git a/tests/data/supply_chain/case_02/config.yml b/tests/data/supply_chain/case_02/config.yml index a56ecea70..e2f3ac2cf 100644 --- a/tests/data/supply_chain/case_02/config.yml +++ b/tests/data/supply_chain/case_02/config.yml @@ -24,13 +24,15 @@ facility_definitions: class: "ProductUnit" is_template: true config: + agent_type: 3 consumer: class: "ConsumerUnit" manufacture: class: "ManufactureUnit" distribution: class: "DistributionUnit" - + config: + agent_type: 0 WarehouseFacility: &warehouse_facility class: "WarehouseFacility" children: @@ -42,9 +44,11 @@ facility_definitions: class: "ProductUnit" is_template: true config: + agent_type: 4 consumer: class: "ConsumerUnit" - + config: + agent_type: 1 RetailerFacility: &retailer_facility class: "RetailerFacility" children: @@ -54,11 +58,15 @@ facility_definitions: class: "ProductUnit" is_template: true config: + agent_type: 5 consumer: class: "ConsumerUnit" seller: class: "SellerUnit" - + config: + sale_hist_len: 4 + config: + agent_type: 2 small_storage: &small_storage config: capacity: 100 @@ -172,3 +180,20 @@ world: - [ 10, 12 ] - [ 11, 12 ] +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e7 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 diff --git a/tests/data/supply_chain/case_03/config.yml b/tests/data/supply_chain/case_03/config.yml index c5bda9556..18f828692 100644 --- a/tests/data/supply_chain/case_03/config.yml +++ b/tests/data/supply_chain/case_03/config.yml @@ -36,13 +36,15 @@ facility_definitions: class: "ProductUnit" is_template: true config: + agent_type: 3 consumer: class: "ConsumerUnit" manufacture: class: "ManufactureUnit" distribution: class: "DistributionUnit" - + config: + agent_type: 0 WarehouseFacility: &warehouse_facility class: "WarehouseFacility" children: @@ -54,9 +56,11 @@ facility_definitions: class: "ProductUnit" is_template: true config: + agent_type: 4 consumer: class: "ConsumerUnit" - + config: + agent_type: 1 RetailerFacility: &retailer_facility class: "RetailerFacility" children: @@ -66,11 +70,15 @@ facility_definitions: class: "ProductUnit" is_template: true config: + agent_type: 5 consumer: class: "ConsumerUnit" seller: class: "SimpleSellerUnit" - + config: + sale_hist_len: 4 + config: + agent_type: 2 small_storage: &small_storage config: capacity: 100 @@ -183,3 +191,21 @@ world: - [ 10, 11 ] - [ 10, 12 ] - [ 11, 12 ] + +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e7 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index b5a34c666..bec62fb88 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -57,12 +57,12 @@ def test_manufacture_meet_storage_limitation(self): env = build_env("case_01", 100) storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space", "unit_storage_cost") + storage_features = ("id", "facility_id", "capacity", "remaining_space") manufacture_nodes = env.snapshot_list["manufacture"] manufacture_number = len(manufacture_nodes) manufacture_features = ( - "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", + "id", "facility_id", "manufacturing_number", "product_id", "product_unit_cost" ) @@ -77,13 +77,13 @@ def test_manufacture_meet_storage_limitation(self): # try to find which one is sku3 manufacture unit. for index, state in enumerate(states): # Id of sku3 is 3. - if state[4] == SKU3_ID: + if state[3] == SKU3_ID: sku3_data_model_index = index sku3_manufacture_id = state[0] - sku3_storage_id = state[5] + sku3_facility_id = state[1] # try to find sku3's storage from env.summary - sku3_storage_index = env.summary["node_mapping"]["unit_mapping"][sku3_storage_id][1] + sku3_storage_index = env.summary["node_mapping"]["facilities"][sku3_facility_id]["units"]["storage"]["node_index"] storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) @@ -163,12 +163,12 @@ def test_manufacture_meet_source_lack(self): env = build_env("case_01", 100) storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space", "unit_storage_cost") + storage_features = ("id", "facility_id", "capacity", "remaining_space") manufacture_nodes = env.snapshot_list["manufacture"] manufacture_number = len(manufacture_nodes) manufacture_features = ( - "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", + "id", "facility_id", "manufacturing_number", "product_id", "product_unit_cost" ) @@ -183,13 +183,13 @@ def test_manufacture_meet_source_lack(self): # try to find which one is sku3 manufacture unit. for index, state in enumerate(states): # Id of sku4 is 4. - if state[4] == SKU4_ID: + if state[3] == SKU4_ID: sku4_data_model_index = index sku4_manufacture_id = state[0] - sku4_storage_id = state[5] + sku4_facility_id = state[1] # try to find sku4's storage from env.summary - sku4_storage_index = env.summary["node_mapping"]["unit_mapping"][sku4_storage_id][1] + sku4_storage_index = env.summary["node_mapping"]["facilities"][sku4_facility_id]["units"]["storage"]["node_index"] # the storage should be same as initialized (50 + 0). storage_states = storage_nodes[env.frame_index:sku4_storage_index:storage_features].flatten().astype(np.int) @@ -207,14 +207,11 @@ def test_manufacture_meet_source_lack(self): # manufacturing_number should be 0 self.assertEqual(0, manufature_states[2]) - # production rate should be 0 - self.assertEqual(0, manufature_states[3]) - # output product id should be same as configured. - self.assertEqual(4, manufature_states[4]) + self.assertEqual(4, manufature_states[3]) # product unit cost should be same as configured. - self.assertEqual(4, manufature_states[6]) + self.assertEqual(4, manufature_states[4]) product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) @@ -239,14 +236,11 @@ def test_manufacture_meet_source_lack(self): # manufacturing_number should be 0 self.assertEqual(0, manufature_states[2]) - # production rate should be 10 - self.assertEqual(10, manufature_states[3]) - # output product id should be same as configured. - self.assertEqual(SKU4_ID, manufature_states[4]) + self.assertEqual(SKU4_ID, manufature_states[3]) # product unit cost should be same as configured. - self.assertEqual(4, manufature_states[6]) + self.assertEqual(4, manufature_states[4]) product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) @@ -262,12 +256,12 @@ def test_manufacture_meet_avg_storage_limitation(self): env = build_env("case_01", 100) storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space", "unit_storage_cost") + storage_features = ("id", "facility_id", "capacity", "remaining_space") manufacture_nodes = env.snapshot_list["manufacture"] manufacture_number = len(manufacture_nodes) manufacture_features = ( - "id", "facility_id", "manufacturing_number", "production_rate", "product_id", "storage_id", + "id", "facility_id", "manufacturing_number", "product_id", "product_unit_cost" ) @@ -281,12 +275,12 @@ def test_manufacture_meet_avg_storage_limitation(self): # try to find which one is sku3 manufacture unit. for index, state in enumerate(states): # Id of sku1 is 1. - if state[4] == SKU1_ID: + if state[3] == SKU1_ID: sku1_data_model_index = index sku1_manufacture_id = state[0] - sku1_storage_id = state[5] + sku1_facility_id = state[1] - sku1_storage_index = env.summary["node_mapping"]["unit_mapping"][sku1_storage_id][1] + sku1_storage_index = env.summary["node_mapping"]["facilities"][sku1_facility_id]["units"]["storage"]["node_index"] ############################### TICK: 1 ###################################### @@ -300,9 +294,6 @@ def test_manufacture_meet_avg_storage_limitation(self): # we can produce 4 sku1, as it will meet storage avg limitation per sku self.assertEqual(4, manufacture_states[2]) - # but the production rate is same as action - self.assertEqual(10, manufacture_states[3]) - # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) @@ -330,9 +321,6 @@ def test_manufacture_meet_avg_storage_limitation(self): # but manufacture number is 0 self.assertEqual(0, manufacture_states[2]) - # but the production rate is same as action - self.assertEqual(20, manufacture_states[3]) - # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) @@ -396,7 +384,7 @@ def test_storage_take_available(self): # check if internal state correct for product_id, num in products_taken.items(): - remaining_num = storage_unit.product_number[storage_unit.product_index_mapping[product_id]] + remaining_num = storage_unit.product_level[product_id] self.assertEqual(init_product_dict[product_id] - num, remaining_num) @@ -475,7 +463,7 @@ def test_storage_try_add_products(self): # each product number should be same as before for product_id, product_number in init_product_dict.items(): self.assertEqual(product_number, - storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + storage_unit.product_level[product_id]) # if we set all_or_nothing=False, then part of the product will be added to storage, and cause remaining space being 0 result = storage_unit.try_add_products(products_to_put, all_or_nothing=False) @@ -506,7 +494,7 @@ def test_storage_try_add_products(self): for product_id, product_number in init_product_dict.items(): self.assertEqual(product_number, - storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + storage_unit.product_level[product_id]) def test_storage_try_take_products(self): env = build_env("case_01", 100) @@ -541,8 +529,7 @@ def test_storage_try_take_products(self): self.assertEqual(init_remaining_space, storage_unit.remaining_space) for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + self.assertEqual(product_number, storage_unit.product_level[product_id]) # try to get all products for product_id, product_number in product_to_take.items(): @@ -580,7 +567,7 @@ def test_storage_get_product_number(self): # number in object should be same with states for product_id, product_number in init_product_dict.items(): self.assertEqual(product_number, - storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + storage_unit.product_level[product_id]) # should not change even after reset env.reset() @@ -591,7 +578,7 @@ def test_storage_get_product_number(self): # number in object should be same with states for product_id, product_number in init_product_dict.items(): self.assertEqual(product_number, - storage_unit.product_number[storage_unit.product_index_mapping[product_id]]) + storage_unit.product_level[product_id]) """ @@ -637,7 +624,7 @@ def test_consumer_init_state(self): # check initial state self.assertEqual(0, sku3_consumer_unit.received) self.assertEqual(0, sku3_consumer_unit.purchased) - self.assertEqual(0, sku3_consumer_unit.order_cost) + self.assertEqual(0, sku3_consumer_unit.order_product_cost) self.assertEqual(SKU3_ID, sku3_consumer_unit.product_id) # check data model state @@ -650,9 +637,7 @@ def test_consumer_init_state(self): self.assertEqual(SKU3_ID, sku3_consumer_unit.data_model.product_id) self.assertEqual(sku3_consumer_unit_id, sku3_consumer_unit.data_model.id) self.assertEqual(sku3_product_unit_id, sku3_consumer_unit.data_model.product_unit_id) - self.assertEqual(0, sku3_consumer_unit.data_model.source_id) - self.assertEqual(0, sku3_consumer_unit.data_model.quantity) - self.assertEqual(0, sku3_consumer_unit.data_model.vlt) + self.assertEqual(0, sku3_consumer_unit.data_model.order_quantity) self.assertEqual(0, sku3_consumer_unit.data_model.purchased) self.assertEqual(0, sku3_consumer_unit.data_model.received) self.assertEqual(0, sku3_consumer_unit.data_model.order_product_cost) @@ -674,9 +659,7 @@ def test_consumer_init_state(self): "order_cost", "total_purchased", "total_received", - "source_id", - "quantity", - "vlt", + "order_quantity", "purchased", "received", "order_product_cost" @@ -692,12 +675,6 @@ def test_consumer_init_state(self): self.assertEqual(sku3_consumer_unit_id, states[0]) self.assertEqual(SKU3_ID, states[2]) - cur_sources = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"sources"].flatten().astype(np.int) - - # only one source according to configuration - self.assertEqual(1, len(cur_sources)) - self.assertEqual(sku3_supplier_faiclity_id, cur_sources[0]) - env.reset() env.step(None) @@ -709,12 +686,6 @@ def test_consumer_init_state(self): self.assertEqual(sku3_consumer_unit_id, states[0]) self.assertEqual(SKU3_ID, states[2]) - cur_sources = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"sources"].flatten().astype(np.int) - - # only one source according to configuration - self.assertEqual(1, len(cur_sources)) - self.assertEqual(sku3_supplier_faiclity_id, cur_sources[0]) - def test_consumer_action(self): env = build_env("case_01", 100) @@ -752,9 +723,7 @@ def test_consumer_action(self): "total_purchased", "total_received", "product_id", - "source_id", - "quantity", - "vlt", + "order_quantity", "purchased", "received", "order_product_cost" @@ -766,20 +735,11 @@ def test_consumer_action(self): # Nothing happened at tick 0, at the action will be recorded self.assertEqual(action_with_zero.product_id, states[6]) - self.assertEqual(action_with_zero.source_id, states[7]) self.assertEqual(action_with_zero.quantity, states[8]) - self.assertEqual(action_with_zero.vlt, states[9]) - self.assertTrue((states[[4, 5, 10, 11, 12]] == 0).all()) self.assertEqual(sku3_consumer_unit_id, states[0]) self.assertEqual(SKU3_ID, states[2]) - cur_sources = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"sources"].flatten().astype(np.int) - - # only one source according to configuration - self.assertEqual(1, len(cur_sources)) - self.assertEqual(sku3_supplier_faiclity_id, cur_sources[0]) - # NOTE: we cannot set_action directly here, as post_step will clear the action before starting next tick env.step({action.id: action}) @@ -790,9 +750,7 @@ def test_consumer_action(self): # action field should be recorded self.assertEqual(action.product_id, states[6]) - self.assertEqual(action.source_id, states[7]) self.assertEqual(action.quantity, states[8]) - self.assertEqual(action.vlt, states[9]) # total purchased should be same as purchased at this tick. self.assertEqual(action.quantity, states[4]) @@ -801,10 +759,10 @@ def test_consumer_action(self): self.assertEqual(0, states[5]) # purchased same as quantity - self.assertEqual(action.quantity, states[10]) + self.assertEqual(action.quantity, states[8]) # no receives - self.assertEqual(0, states[11]) + self.assertEqual(0, states[9]) # same action for next step, so total_XXX will be changed to double env.step({action.id: action}) @@ -813,9 +771,7 @@ def test_consumer_action(self): # action field should be recorded self.assertEqual(action.product_id, states[6]) - self.assertEqual(action.source_id, states[7]) self.assertEqual(action.quantity, states[8]) - self.assertEqual(action.vlt, states[9]) # total purchased should be same as purchased at this tick. self.assertEqual(action.quantity * 2, states[4]) @@ -824,10 +780,10 @@ def test_consumer_action(self): self.assertEqual(0, states[5]) # purchased same as quantity - self.assertEqual(action.quantity, states[10]) + self.assertEqual(action.quantity, states[8]) # no receives - self.assertEqual(0, states[11]) + self.assertEqual(0, states[9]) def test_consumer_on_order_reception(self): env = build_env("case_01", 100) @@ -902,9 +858,8 @@ def test_vehicle_unit_state(self): # check initial state according to configuration file self.assertEqual(10, vehicle_unit.max_patient) - self.assertEqual(10, vehicle_unit.data_model.patient) - self.assertEqual(0, vehicle_unit.quantity) + self.assertEqual(0, vehicle_unit.requested_quantity) # not destination at first self.assertIsNone(vehicle_unit.destination) # no path @@ -923,14 +878,8 @@ def test_vehicle_unit_state(self): self.assertEqual(0, vehicle_unit.velocity) # state in frame - self.assertEqual(0, vehicle_unit.data_model.source) - self.assertEqual(0, vehicle_unit.data_model.destination) self.assertEqual(0, vehicle_unit.data_model.payload) - self.assertEqual(0, vehicle_unit.data_model.product_id) - self.assertEqual(0, vehicle_unit.data_model.requested_quantity) - self.assertEqual(0, vehicle_unit.data_model.steps) self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - self.assertListEqual([-1, -1], vehicle_unit.data_model.position[:]) # reset to check again env.step(None) @@ -938,7 +887,6 @@ def test_vehicle_unit_state(self): # check initial state according to configuration file self.assertEqual(10, vehicle_unit.max_patient) - self.assertEqual(10, vehicle_unit.data_model.patient) # not destination at first self.assertIsNone(vehicle_unit.destination) @@ -957,17 +905,11 @@ def test_vehicle_unit_state(self): # self.assertEqual(0, vehicle_unit.velocity) # - self.assertEqual(0, vehicle_unit.quantity) + self.assertEqual(0, vehicle_unit.requested_quantity) # state in frame - self.assertEqual(0, vehicle_unit.data_model.source) - self.assertEqual(0, vehicle_unit.data_model.destination) self.assertEqual(0, vehicle_unit.data_model.payload) - self.assertEqual(0, vehicle_unit.data_model.product_id) - self.assertEqual(0, vehicle_unit.data_model.requested_quantity) - self.assertEqual(0, vehicle_unit.data_model.steps) self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - self.assertListEqual([-1, -1], vehicle_unit.data_model.position[:]) def test_vehicle_unit_schedule(self): env = build_env("case_02", 100) @@ -999,7 +941,7 @@ def test_vehicle_unit_schedule(self): # check internal states self.assertEqual(dest_facility, vehicle_unit.destination) self.assertEqual(SKU3_ID, vehicle_unit.product_id) - self.assertEqual(20, vehicle_unit.quantity) + self.assertEqual(20, vehicle_unit.requested_quantity) self.assertEqual(2, vehicle_unit.velocity) # 6/2 self.assertEqual(3, vehicle_unit.steps) @@ -1007,67 +949,28 @@ def test_vehicle_unit_schedule(self): features = ( "id", "facility_id", - "source", - "destination", "payload", - "product_id", - "requested_quantity", - "steps", "unit_transport_cost" ) states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) # source id - self.assertEqual(vehicle_unit.facility.id, states[2]) - # destination - self.assertEqual(dest_facility.id, states[3]) + self.assertEqual(vehicle_unit.facility.id, states[1]) # payload should be 20, as we already env.step - self.assertEqual(20, states[4]) - # product id - self.assertEqual(SKU3_ID, states[5]) - # quantity - self.assertEqual(20, states[6]) - # steps - self.assertEqual(3, states[7]) + self.assertEqual(20, states[2]) # push the vehicle on the way env.step(None) states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - # source id - self.assertEqual(vehicle_unit.facility.id, states[2]) - # destination - self.assertEqual(dest_facility.id, states[3]) # payload - self.assertEqual(20, states[4]) - # product id - self.assertEqual(SKU3_ID, states[5]) - # quantity - self.assertEqual(20, states[6]) - # steps, one step forward (vlt=2) - self.assertEqual(2, states[7]) + self.assertEqual(20, states[2]) env.step(None) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # steps, one step forward (vlt=2) - self.assertEqual(1, states[7]) - env.step(None) - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # steps, one step forward (vlt=2) - self.assertEqual(0, states[7]) - - pos = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:"position"].flatten().astype(np.int) - - # the position should be (0, 0) (warehouse) - self.assertListEqual([0, 0], list(pos)) - # next step vehicle will try to unload the products env.step(None) @@ -1083,21 +986,12 @@ def test_vehicle_unit_schedule(self): self.assertIsNone(vehicle_unit.product) self.assertEqual(0, vehicle_unit.location) self.assertEqual(0, vehicle_unit.velocity) - self.assertEqual(0, vehicle_unit.quantity) + self.assertEqual(0, vehicle_unit.requested_quantity) # check states - self.assertEqual(0, states[2]) - self.assertEqual(0, states[3]) - self.assertEqual(0, states[4]) - self.assertEqual(0, states[5]) - self.assertEqual(0, states[6]) - self.assertEqual(0, states[7]) self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - pos = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:"position"].flatten().astype(np.int) - self.assertListEqual([-1, -1], list(pos)) - def test_vehicle_unit_no_patient(self): """ NOTE: with patient is tried in above case after schedule the job @@ -1122,11 +1016,10 @@ def test_vehicle_unit_no_patient(self): # push env to next step env.step(None) - self.assertEqual(100, vehicle_unit.quantity) + self.assertEqual(100, vehicle_unit.requested_quantity) # the patient will -1 as no enough product so load self.assertEqual(10 - 1, vehicle_unit.patient) - self.assertEqual(10 - 1, vehicle_unit.data_model.patient) # no payload self.assertEqual(0, vehicle_unit.payload) @@ -1137,27 +1030,15 @@ def test_vehicle_unit_no_patient(self): env.step(None) self.assertEqual(10 - 1 - (i + 1), vehicle_unit.patient) - self.assertEqual(10 - 1 - (i + 1), vehicle_unit.data_model.patient) vehicle_nodes = env.snapshot_list["vehicle"] features = ( "id", "facility_id", - "source", - "destination", "payload", - "product_id", - "requested_quantity", - "steps", "unit_transport_cost" ) - states = vehicle_nodes[:vehicle_unit.data_model_index:"patient"].flatten().astype(np.int) - - # check the patient history - self.assertEqual(10, len(states)) - self.assertListEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], list(states)) - states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) # no payload from start to now @@ -1178,21 +1059,13 @@ def test_vehicle_unit_no_patient(self): self.assertIsNone(vehicle_unit.product) self.assertEqual(0, vehicle_unit.location) self.assertEqual(0, vehicle_unit.velocity) - self.assertEqual(0, vehicle_unit.quantity) + self.assertEqual(0, vehicle_unit.requested_quantity) # check states self.assertEqual(0, states[2]) - self.assertEqual(0, states[3]) - self.assertEqual(0, states[4]) - self.assertEqual(0, states[5]) - self.assertEqual(0, states[6]) - self.assertEqual(0, states[7]) self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - pos = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:"position"].flatten().astype(np.int) - self.assertListEqual([-1, -1], list(pos)) - def test_vehicle_unit_cannot_unload_at_destination(self): """ NOTE: If vehicle cannot unload at destination, it will keep waiting, until success to unload. @@ -1240,17 +1113,6 @@ def test_vehicle_unit_cannot_unload_at_destination(self): payload_states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) self.assertListEqual([80] * 4 + [10] * 96, list(payload_states)) - # other states should not be reset as it not finish it task - quantity_states = vehicle_nodes[:vehicle_unit.data_model_index:"requested_quantity"].flatten().astype(np.int) - self.assertListEqual([80] * 100, list(quantity_states)) - - # same situation as payload - steps_states = vehicle_nodes[:vehicle_unit.data_model_index:"steps"].flatten().astype(np.int) - self.assertListEqual([3, 2, 1] + [0] * 97, list(steps_states)) - - destination_states = vehicle_nodes[:vehicle_unit.data_model_index:"destination"].flatten().astype(np.int) - self.assertListEqual([dest_facility.id] * 100, list(destination_states)) - """ Distribution unit test: @@ -1275,29 +1137,11 @@ def test_distribution_unit_initial_state(self): dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) self.assertEqual(0, len(dist_unit.order_queue)) - self.assertEqual(1, len(dist_unit.product_index_mapping)) - self.assertDictEqual({3: 0}, dist_unit.product_index_mapping) - self.assertEqual(1, len(dist_unit.product_list)) - self.assertListEqual([3], dist_unit.product_list) - - # from configuration - self.assertEqual(1, dist_unit.data_model.unit_price) - self.assertListEqual([3], list(dist_unit.data_model.product_list[:])) - self.assertListEqual([0], list(dist_unit.data_model.delay_order_penalty)) # reset env.reset() self.assertEqual(0, len(dist_unit.order_queue)) - self.assertEqual(1, len(dist_unit.product_index_mapping)) - self.assertDictEqual({3: 0}, dist_unit.product_index_mapping) - self.assertEqual(1, len(dist_unit.product_list)) - self.assertListEqual([3], dist_unit.product_list) - - # from configuration - self.assertEqual(1, dist_unit.data_model.unit_price) - self.assertListEqual([3], list(dist_unit.data_model.product_list[:])) - self.assertListEqual([0], list(dist_unit.data_model.delay_order_penalty)) def test_distribution_unit_dispatch_order(self): env = build_env("case_02", 100) @@ -1332,7 +1176,7 @@ def test_distribution_unit_dispatch_order(self): env.step(None) self.assertEqual(dest_facility, first_vehicle.destination) - self.assertEqual(10, first_vehicle.quantity) + self.assertEqual(10, first_vehicle.requested_quantity) self.assertEqual(2, first_vehicle.velocity) self.assertEqual(SKU3_ID, first_vehicle.product_id) @@ -1360,13 +1204,10 @@ def test_distribution_unit_dispatch_order(self): second_vehicle = dist_unit.vehicles[1] self.assertEqual(dest_facility, second_vehicle.destination) - self.assertEqual(10, second_vehicle.quantity) + self.assertEqual(10, second_vehicle.requested_quantity) self.assertEqual(2, second_vehicle.velocity) self.assertEqual(SKU3_ID, second_vehicle.product_id) - # from configuration - self.assertEqual(20, dist_unit.data_model.delay_order_penalty[0]) - """ Seller unit test: . initial state From 707ca65513f0c4bd8af4a73ed6d77c2164d7d562 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 5 Apr 2021 10:59:24 +0800 Subject: [PATCH 151/482] bug fix, remove unused code --- maro/simulator/scenarios/cim/ports_order_export.py | 2 +- .../scenarios/supply_chain/datamodels/skumodel.py | 4 ++-- .../scenarios/supply_chain/facilities/facility.py | 5 +++++ .../simulator/scenarios/supply_chain/units/consumer.py | 5 ----- maro/simulator/scenarios/supply_chain/units/product.py | 10 ++++++---- .../scenarios/supply_chain/units/storeproduct.py | 6 ------ maro/simulator/scenarios/supply_chain/world.py | 5 ++--- 7 files changed, 16 insertions(+), 21 deletions(-) diff --git a/maro/simulator/scenarios/cim/ports_order_export.py b/maro/simulator/scenarios/cim/ports_order_export.py index baf66b106..d69dac10e 100644 --- a/maro/simulator/scenarios/cim/ports_order_export.py +++ b/maro/simulator/scenarios/cim/ports_order_export.py @@ -37,6 +37,6 @@ def dump(self, folder: str): for order in self._orders: writer.writerow( - [order.tick, order.src_port_idx, order.dest_port_idx, order.requested_quantity]) + [order.tick, order.src_port_idx, order.dest_port_idx, order.quantity]) self._orders.clear() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py b/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py index 1029c1d94..17a165f26 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py @@ -16,8 +16,8 @@ class SkuDataModel(DataModelBase): # Parent unit id. product_unit_id = NodeAttribute(AttributeType.UInt) - def __int__(self): - super(SkuDataModel, self).__int__() + def __init__(self): + super(SkuDataModel, self).__init__() self._product_id = 0 self._product_unit_id = 0 diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index e7dfa285e..e0a27b7c4 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -50,6 +50,8 @@ class FacilityBase(ABC): # Index of the data model node. data_model_index: int = 0 + data_model: object = None + # Children of this facility (valid units). children: list = None @@ -131,6 +133,9 @@ def reset(self): for unit in self.children: unit.reset() + if self.data_model is not None: + self.data_model.reset() + def get_in_transit_orders(self): in_transit_orders = defaultdict(int) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index d6fb88045..1b9a48a3c 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -86,11 +86,6 @@ def initialize(self): if sku.id in source_facility.skus: self.sources.append(source_facility.id) - if len(self.sources) == 0: - warnings.warn( - f"No sources for consumer: {self.id}, sku: {self.product_id} in facility: {self.facility.name}.") - return - def step(self, tick: int): self._update_pending_order() diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index a8542a82a..f74b473fe 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -26,10 +26,11 @@ class ProductUnit(SkuUnit): # Storage of this facility, always a reference of facility.storage. storage: StorageUnit = None + # Reference to facility's distribution unit. distribution: DistributionUnit = None def initialize(self): - super(ProductUnit, self).initialize() + super().initialize() facility_sku = self.facility.skus[self.product_id] @@ -44,13 +45,13 @@ def flush_states(self): unit.flush_states() def post_step(self, tick: int): - super(ProductUnit, self).post_step(tick) + super().post_step(tick) for unit in self.children: unit.post_step(tick) def reset(self): - super(ProductUnit, self).reset() + super().reset() for unit in self.children: unit.reset() @@ -140,7 +141,8 @@ def generate(facility, config: dict, unit_def: object): for sku_id, sku in facility.skus.items(): sku_type = sku.type - product_unit: ProductUnit = world.build_unit_by_type(ProductUnit, facility, facility, unit_def) + product_unit: ProductUnit = world.build_unit_by_type(unit_def, facility, facility) + # product_unit: ProductUnit = world.build_unit(facility, facility, unit_def) product_unit.product_id = sku_id product_unit.children = [] product_unit.parse_configs(config) diff --git a/maro/simulator/scenarios/supply_chain/units/storeproduct.py b/maro/simulator/scenarios/supply_chain/units/storeproduct.py index 6ee1ef8ad..7ce9a5396 100644 --- a/maro/simulator/scenarios/supply_chain/units/storeproduct.py +++ b/maro/simulator/scenarios/supply_chain/units/storeproduct.py @@ -3,12 +3,6 @@ class StoreProductUnit(ProductUnit): - def get_latest_sale(self): - sale_hist = self.seller.config.get("sale_hist", 0) - - # TODO: why demand not sold? - return 0 if sale_hist == 0 else self.seller.demand - def get_sale_mean(self): return self.seller.sale_mean() diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 7f280d7be..a76381699 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -288,9 +288,8 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio self.agent_type_dict[agent_type] = True - def build_unit_by_type(self, unit_type: type, parent: Union[FacilityBase, UnitBase], facility: FacilityBase, - unit_def: UnitDef): - unit = unit_type() + def build_unit_by_type(self, unit_def: UnitDef, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): + unit = unit_def.class_type() unit.id = self._gen_id() unit.parent = parent From 5e0a3dcdbd2ba2eb856a47a4180975a976ac99eb Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 5 Apr 2021 11:21:19 +0000 Subject: [PATCH 152/482] refined agent interface and renamed some configs --- examples/cim/ac/config.yml | 13 ++-- examples/cim/ac/main.py | 11 +-- examples/cim/dqn/config.yml | 19 ++--- examples/cim/dqn/main.py | 10 ++- examples/supply_chain/dqn/agent.py | 2 +- examples/supply_chain/dqn/config.yml | 19 +++-- .../dqn/single_thread_launcher.py | 3 +- examples/supply_chain/env_wrapper.py | 5 +- maro/rl/__init__.py | 5 +- maro/rl/agent/__init__.py | 4 +- maro/rl/agent/abs_agent.py | 70 +++++++---------- maro/rl/agent/ac.py | 63 ++++++++-------- maro/rl/agent/ddpg.py | 64 ++++++++++------ maro/rl/agent/dqn.py | 75 ++++++++++--------- maro/rl/agent/multi_agent_wrapper.py | 7 +- maro/rl/agent/pg.py | 48 +++++++++++- maro/rl/distributed/actor.py | 7 +- maro/rl/distributed/learner.py | 12 ++- maro/rl/storage/sampler.py | 2 +- maro/rl/training/learner.py | 4 +- .../simulator/scenarios/supply_chain/world.py | 1 + 21 files changed, 256 insertions(+), 188 deletions(-) diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index 097938cf9..d48669ec5 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -55,19 +55,22 @@ agent: optim_cls: rmsprop optim_params: lr: 0.001 - hyper_params: + algorithm: reward_discount: .0 - experience_memory_size: -1 - experience_memory_overwrite_type: "rolling" critic_loss_cls: smooth_l1 train_iters: 10 actor_loss_coefficient: 0.1 - min_new_experiences_to_learn: 1 # clip_ratio: 0.8 # for PPO + experience_memory: + experience_memory_size: -1 + experience_memory_overwrite_type: "rolling" + flush_experience_memory_after_step: bool, + min_new_experiences_to_trigger_learning: 1 + min_experience_memory_size: 1 training: env: scenario: cim topology: toy.4p_ssdd_l0.0 durations: 1120 max_episode: 50 - agent_update_interval: 200 \ No newline at end of file + agent_update_interval: -1 \ No newline at end of file diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index 8c51e0bf9..a909da809 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -32,16 +32,17 @@ def get_ac_agent(): - actor_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **config["agent"]["model"]["actor"]) - critic_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **config["agent"]["model"]["critic"]) + cfg = config["agent"] + actor_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["actor"]) + critic_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **cfg["model"]["critic"]) ac_model = SimpleMultiHeadModel( {"actor": actor_net, "critic": critic_net}, optim_option={ - "actor": OptimOption(**config["agent"]["optimization"]["actor"]), - "critic": OptimOption(**config["agent"]["optimization"]["critic"]) + "actor": OptimOption(**cfg["optimization"]["actor"]), + "critic": OptimOption(**cfg["optimization"]["critic"]) } ) - return ActorCritic(ac_model, ActorCriticConfig(**config["agent"]["hyper_params"])) + return ActorCritic(ac_model, ActorCriticConfig(**cfg["algorithm"]), **cfg["experience_memory"]) # Single-threaded launcher diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index c8569d5a7..5e308519b 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -42,24 +42,27 @@ agent: optim_cls: rmsprop optim_params: lr: 0.05 - hyper_params: + algorithm: reward_discount: .0 - experience_memory_size: -1 - experience_memory_overwrite_type: "rolling" - min_new_experiences_to_learn: 16 - min_experiences: 1024 train_iters: 10 batch_size: 128 loss_cls: smooth_l1 target_update_freq: 5 - tau: 0.1 + soft_update_coefficient: 0.1 double: false + experience_memory: + experience_memory_size: -1 + experience_memory_overwrite_type: "rolling" + flush_experience_memory_after_step: false + min_new_experiences_to_trigger_learning: 16 + min_experience_memory_size: 1024 training: env: scenario: cim topology: toy.4p_ssdd_l0.0 durations: 1120 max_episode: 100 + agent_update_interval: -1 exploration: parameter_names: - epsilon @@ -72,6 +75,4 @@ distributed: num_actors: 2 redis_host: localhost redis_port: 6379 - min_actor_finishes: 2 - agent_update_interval: 100 - ignore_stale_experiences: False \ No newline at end of file + min_actor_finishes: 2 \ No newline at end of file diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 05840db87..ae45d1f3d 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -38,11 +38,12 @@ def get_dqn_agent(): + cfg = config["agent"] q_model = SimpleMultiHeadModel( - FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **config["agent"]["model"]), - optim_option=OptimOption(**config["agent"]["optimization"]) + FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]), + optim_option=OptimOption(**cfg["optimization"]) ) - return DQN(q_model, DQNConfig(**config["agent"]["hyper_params"])) + return DQN(q_model, DQNConfig(**cfg["algorithm"]), **cfg["experience_memory"]) def cim_dqn_learner(): @@ -51,7 +52,8 @@ def cim_dqn_learner(): learner = DistLearner( agent, scheduler, NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, - agent_update_interval=config["distributed"]["agent_update_interval"] + agent_update_interval=config["training"]["agent_update_interval"], + ignore_stale_experiences=False ) learner.run() diff --git a/examples/supply_chain/dqn/agent.py b/examples/supply_chain/dqn/agent.py index 76008f102..a61adf3e8 100644 --- a/examples/supply_chain/dqn/agent.py +++ b/examples/supply_chain/dqn/agent.py @@ -16,7 +16,7 @@ def get_dqn_agent(in_dim, out_dim, config): FullyConnectedBlock(input_dim=in_dim, output_dim=out_dim, **config["model"]), optim_option=OptimOption(**config["optimization"]) ) - return DQN(q_model, DQNConfig(**config["hyper_params"])) + return DQN(q_model, DQNConfig(**config["algorithm"]), **config["experience_memory"]) def get_sc_agents(agent_info_list, config): diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index 43f9b66f1..d0e874f00 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -16,24 +16,27 @@ agent: optim_cls: rmsprop optim_params: lr: 0.001 - hyper_params: - reward_discount: .0 - experience_memory_size: 50000 - experience_memory_overwrite_type: random - min_experiences: 10 + algorithm: + reward_discount: .9 train_iters: 10 batch_size: 128 - loss_cls: smooth_l1 + loss_cls: mse target_update_freq: 5 - tau: 0.1 + soft_update_coefficient: 0.1 double: false + experience_memory: + experience_memory_size: 50000 + experience_memory_overwrite_type: random + flush_experience_memory_after_step: false + min_new_experiences_to_trigger_learning: 16 + min_experience_memory_size: 10 training: env: scenario: supply_chain topology: sample1 durations: 200 max_episode: 10 - learning_interval: 45 + agent_update_interval: 45 exploration: parameter_names: - epsilon diff --git a/examples/supply_chain/dqn/single_thread_launcher.py b/examples/supply_chain/dqn/single_thread_launcher.py index ab422c4f0..31bbec855 100644 --- a/examples/supply_chain/dqn/single_thread_launcher.py +++ b/examples/supply_chain/dqn/single_thread_launcher.py @@ -25,6 +25,7 @@ agent = get_sc_agents(env.agent_idx_list, config["agent"]) scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) learner = Learner( - SCEnvWrapper(env), agent, scheduler, learning_interval=config["training"][""] + SCEnvWrapper(env), agent, scheduler, + agent_update_interval=config["training"]["agent_update_interval"] ) learner.run() diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 543bd82a8..658d65a2e 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -175,6 +175,7 @@ def get_action(self, action_by_agent): env_action[agent_id] = ConsumerAction(agent_id, product_id, source_id, action, 1) # manufacturer action elif agent_id.startswith("producer"): + agent_id = int(agent_id.split(".")[1]) env_action[agent_id] = ManufactureAction(agent_id, action) return env_action @@ -214,7 +215,7 @@ def _get_state(self): self._cur_facility_storage_product_mapping = {product_id: i for i, product_id in enumerate(storage_product_list)} - f_state = self._state(agent_info) + f_state = self._state_from_info(agent_info) self._add_global_features(f_state) @@ -223,7 +224,7 @@ def _get_state(self): return self._serialize_state(state) - def _state(self, agent_info): + def _state_from_info(self, agent_info): state = {} self._add_facility_features(state, agent_info) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 1251bd33f..d3c561d91 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -2,7 +2,8 @@ # Licensed under the MIT license. from maro.rl.agent import ( - DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient + DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient, + PolicyGradientConfig ) from maro.rl.distributed import Actor, DistLearner from maro.rl.exploration import ( @@ -20,7 +21,7 @@ __all__ = [ "AbsAgent", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", - "PolicyGradient", + "PolicyGradient", "PolicyGradientConfig", "Actor", "DistLearner", "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", diff --git a/maro/rl/agent/__init__.py b/maro/rl/agent/__init__.py index d79d894bc..ee082cd64 100644 --- a/maro/rl/agent/__init__.py +++ b/maro/rl/agent/__init__.py @@ -6,7 +6,7 @@ from .ddpg import DDPG, DDPGConfig from .dqn import DQN, DQNConfig from .multi_agent_wrapper import MultiAgentWrapper -from .pg import PolicyGradient +from .pg import PolicyGradient, PolicyGradientConfig __all__ = [ "AbsAgent", @@ -14,5 +14,5 @@ "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", - "PolicyGradient" + "PolicyGradient", "PolicyGradientConfig" ] diff --git a/maro/rl/agent/abs_agent.py b/maro/rl/agent/abs_agent.py index d60fa33b8..20b8c4730 100644 --- a/maro/rl/agent/abs_agent.py +++ b/maro/rl/agent/abs_agent.py @@ -6,63 +6,50 @@ import torch from maro.rl.model import AbsCoreModel -from maro.rl.storage import SimpleStore +from maro.rl.storage import SimpleStore -class AgentConfig: - """Configuration for the DQN algorithm. +class AbsAgent(ABC): + """Abstract RL agent class. + + It's a sandbox for the RL algorithm. Scenario-specific details will be excluded. + We focus on the abstraction algorithm development here. Environment observation and decision events will + be converted to a uniform format before calling in. The output will be converted to an environment + executable format before return back to the environment. Its key responsibility is optimizing policy based + on interaction with the environment. Args: - reward_discount (float): Reward decay as defined in standard RL terminology. + model (AbsCoreModel): Task model or container of task models required by the algorithm. + config: Settings for the algorithm. experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". - min_new_experiences_to_learn (int): Minimum number of new experiences required to trigger learning. flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call to ``step``. + min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. + Defaults to 1. + min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + Defaults to 1. """ - __slots__ = [ - "reward_discount", "experience_memory_size", "experience_memory_overwrite_type", - "min_new_experiences_to_learn", "flush_experience_memory_after_step" - ] - def __init__( self, - reward_discount: float, + model: AbsCoreModel, + config, experience_memory_size: int, experience_memory_overwrite_type: str, - min_new_experiences_to_learn: int, - flush_experience_memory_after_step: bool + flush_experience_memory_after_step: bool, + min_new_experiences_to_trigger_learning: int = 1, + min_experience_memory_size: int = 1 ): - self.reward_discount = reward_discount - self.experience_memory_size = experience_memory_size - self.experience_memory_overwrite_type = experience_memory_overwrite_type - self.min_new_experiences_to_learn = min_new_experiences_to_learn - self.flush_experience_memory_after_step = flush_experience_memory_after_step - - -class AbsAgent(ABC): - """Abstract RL agent class. - - It's a sandbox for the RL algorithm. Scenario-specific details will be excluded. - We focus on the abstraction algorithm development here. Environment observation and decision events will - be converted to a uniform format before calling in. The output will be converted to an environment - executable format before return back to the environment. Its key responsibility is optimizing policy based - on interaction with the environment. - - Args: - model (AbsCoreModel): Task model or container of task models required by the algorithm. - config: Settings for the algorithm. - """ - def __init__(self, model: AbsCoreModel, config: AgentConfig): self.model = model self.config = config self.experience_memory = SimpleStore( - ["S", "A", "R", "S_"], - capacity=self.config.experience_memory_size, - overwrite_type=self.config.experience_memory_overwrite_type + ["S", "A", "R", "S_"], capacity=experience_memory_size, overwrite_type=experience_memory_overwrite_type ) + self.flush_experience_memory_after_step = flush_experience_memory_after_step + self.min_new_experiences_to_trigger_learning = min_new_experiences_to_trigger_learning + self.min_experience_memory_size = min_experience_memory_size self.device = torch.device('cpu') self._version_index = 0 @@ -99,12 +86,13 @@ def update(self, experiences: dict) -> bool: if set(experiences.keys()) != {"S", "A", "R", "S_"}: raise ValueError("The keys of experiences must be {'S', 'A', 'R', 'S_'}") self.experience_memory.put(experiences) - n = len(experiences["S"]) - print(f"got {n} new exp") - if len(experiences["S"]) >= self.config.min_new_experiences_to_learn: + if ( + len(experiences["S"]) >= self.min_new_experiences_to_trigger_learning and + len(self.experience_memory) >= self.min_experience_memory_size + ): self.step() self._version_index += 1 - if self.config.flush_experience_memory_after_step: + if self.flush_experience_memory_after_step: self.experience_memory.clear() return True return False diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py index 96b49dfff..21ec1296f 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/agent/ac.py @@ -12,19 +12,14 @@ from maro.rl.utils import get_log_prob, get_torch_loss_cls from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent, AgentConfig +from .abs_agent import AbsAgent -class ActorCriticConfig(AgentConfig): +class ActorCriticConfig: """Configuration for the Actor-Critic algorithm. Args: reward_discount (float): Reward decay as defined in standard RL terminology. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - min_new_experiences_to_learn (int): Minimum number of new experiences required to trigger learning. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". train_iters (int): Number of gradient descent steps per call to ``train``. @@ -32,30 +27,18 @@ class ActorCriticConfig(AgentConfig): loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. - flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call - to ``step``. Defaults to True. """ - __slots__ = [ - "critic_loss_func", "train_iters", "actor_loss_coefficient", "k", "lam", "clip_ratio", - "flush_experience_memory_after_training" - ] + __slots__ = ["reward_discount", "critic_loss_func", "train_iters", "actor_loss_coefficient", "clip_ratio"] def __init__( self, reward_discount: float, - experience_memory_size: int, - experience_memory_overwrite_type: str, - min_new_experiences_to_learn: int, train_iters: int, critic_loss_cls="mse", actor_loss_coefficient: float = 1.0, - clip_ratio: float = None, - flush_experience_memory_after_step: bool = True + clip_ratio: float = None ): - super().__init__( - reward_discount, experience_memory_size, experience_memory_overwrite_type, min_new_experiences_to_learn, - flush_experience_memory_after_step - ) + self.reward_discount = reward_discount self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.train_iters = train_iters self.actor_loss_coefficient = actor_loss_coefficient @@ -66,18 +49,42 @@ class ActorCritic(AbsAgent): """Actor Critic algorithm with separate policy and value models. References: - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: model (SimpleMultiHeadModel): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. config: Configuration for the AC algorithm. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + to ``step``. Defaults to True. + min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. + Defaults to 1. + min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + Defaults to 1. """ - def __init__(self, model: SimpleMultiHeadModel, config: ActorCriticConfig): + def __init__( + self, + model: SimpleMultiHeadModel, + config: ActorCriticConfig, + experience_memory_size: int, + experience_memory_overwrite_type: str, + flush_experience_memory_after_step: bool = True, + min_new_experiences_to_trigger_learning: int = 1, + min_experience_memory_size: int = 1 + ): if model.task_names is None or set(model.task_names) != {"actor", "critic"}: raise UnrecognizedTask(f"Expected model task names 'actor' and 'critic', but got {model.task_names}") - super().__init__(model, config) + super().__init__( + model, config, experience_memory_size, experience_memory_overwrite_type, + flush_experience_memory_after_step, + min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, + min_experience_memory_size=min_experience_memory_size + ) def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Use the actor (policy) model to generate stochastic actions. @@ -100,9 +107,6 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: return (action[0], log_p[0]) if is_single else (action, log_p) def step(self): - if len(self.experience_memory) == 0: - return - batch = self.experience_memory.get() states = torch.from_numpy(np.asarray(batch["S"])).to(self.device) actions = torch.from_numpy(np.asarray([act[0] for act in batch["A"]])).to(self.device) @@ -114,7 +118,6 @@ def step(self): next_state_values = self.model(next_states, task_name="critic").detach().squeeze() return_est = rewards + self.config.reward_discount * next_state_values advantages = return_est - state_values - print("training") for i in range(self.config.train_iters): # actor loss log_p_new = get_log_prob(self.model(states, task_name="actor"), actions) diff --git a/maro/rl/agent/ddpg.py b/maro/rl/agent/ddpg.py index 8a1cd7f21..48ad14dae 100644 --- a/maro/rl/agent/ddpg.py +++ b/maro/rl/agent/ddpg.py @@ -19,60 +19,76 @@ class DDPGConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - min_new_experiences_to_learn (int): Minimum number of new experiences required to trigger learning. target_update_freq (int): Number of training rounds between policy target model updates. q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., loss = q_value_loss + ``policy_loss_coefficient`` * policy_loss. Defaults to 1.0. - tau (float): Soft update coefficient, e.g., target_model = tau * eval_model + (1-tau) * target_model. + soft_update_coefficient (float): Soft update coefficient, e.g., + target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. Defaults to 1.0. - flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call - to ``step``. Defaults to False. """ - __slots__ = ["q_value_loss_func", "target_update_freq", "policy_loss_coefficient", "tau"] + __slots__ = [ + "reward_discount", "q_value_loss_func", "target_update_freq", "policy_loss_coefficient", + "soft_update_coefficient" + ] def __init__( self, reward_discount: float, - experience_memory_size: int, - experience_memory_overwrite_type: str, - min_new_experiences_to_learn: int, target_update_freq: int, q_value_loss_cls="mse", policy_loss_coefficient: float = 1.0, - tau: float = 1.0, + soft_update_coefficient: float = 1.0, ): - super().__init__( - reward_discount, experience_memory_size, experience_memory_overwrite_type, min_new_experiences_to_learn, - flush_experience_memory_after_step - ) + self.reward_discount = reward_discount self.target_update_freq = target_update_freq self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() self.policy_loss_coefficient = policy_loss_coefficient - self.tau = tau + self.soft_update_coefficient = soft_update_coefficient class DDPG(AbsAgent): """The Deep Deterministic Policy Gradient (DDPG) algorithm. References: - https://arxiv.org/pdf/1509.02971.pdf - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg + https://arxiv.org/pdf/1509.02971.pdf + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg Args: model (SimpleMultiHeadModel): DDPG policy and q-value models. - config: Configuration for DDPG algorithm. + config (DDPGConfig): Configuration for DDPG algorithm. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + to ``step``. Defaults to False. + min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. + Defaults to 1. + min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + Defaults to 1. explorer (NoiseExplorer): An NoiseExplorer instance for generating exploratory actions. Defaults to None. """ - def __init__(self, model: SimpleMultiHeadModel, config: DDPGConfig, explorer: NoiseExplorer = None): + def __init__( + self, + model: SimpleMultiHeadModel, + config: DDPGConfig, + experience_memory_size: int, + experience_memory_overwrite_type: str, + flush_experience_memory_after_step: bool = False, + min_new_experiences_to_trigger_learning: int = 1, + min_experience_memory_size: int = 1, + explorer: NoiseExplorer = None + ): if model.task_names is None or set(model.task_names) != {"policy", "q_value"}: raise UnrecognizedTask(f"Expected model task names 'policy' and 'q_value', but got {model.task_names}") - super().__init__(model, config) + super().__init__( + model, config, experience_memory_size, experience_memory_overwrite_type, + flush_experience_memory_after_step, + min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, + min_experience_memory_size=min_experience_memory_size + ) self._explorer = explorer self._target_model = model.copy() if model.trainable else None self._train_cnt = 0 @@ -114,4 +130,4 @@ def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, nex self.model.step(q_value_loss + self.config.policy_loss_coefficient * policy_loss) self._train_cnt += 1 if self._train_cnt % self.config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.config.tau) + self._target_model.soft_update(self.model, self.config.soft_update_coefficient) diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index 823d8cf31..306667856 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -11,29 +11,24 @@ from maro.rl.utils import get_max, get_sampler_cls, get_td_errors, get_torch_loss_cls, select_by_actions from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent, AgentConfig +from .abs_agent import AbsAgent -class DQNConfig(AgentConfig): +class DQNConfig: """Configuration for the DQN algorithm. Args: reward_discount (float): Reward decay as defined in standard RL terminology. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - min_new_experiences_to_learn (int): Minimum number of new experiences required to trigger learning. target_update_freq (int): Number of training rounds between target model updates. - min_experiences (int): Minimum number of experiences required for training. If the number of experiences in the - replay memory is below this number, training will not be triggered. train_iters (int): Number of batches to train the model on in each call to ``learn``. batch_size (int): Experience minibatch size. sampler_cls: A string indicating the sampler class or a custom sampler class that provides the ``sample`` interface. Defaults to "uniform". sampler_params (dict): Parameters for the sampler class. Defaults to None. epsilon (float): Exploration rate for epsilon-greedy exploration. Defaults to None. - tau (float): Soft update coefficient, i.e., target_model = tau * eval_model + (1 - tau) * target_model. + soft_update_coefficient (float): Soft update coefficient, e.g., + target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. + Defaults to 1.0. double (bool): If True, the next Q values will be computed according to the double DQN algorithm, i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. @@ -41,45 +36,34 @@ class DQNConfig(AgentConfig): case it is assumed that the regular Q-value model is used. loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call - to ``step``. Defaults to False. """ __slots__ = [ - "target_update_freq", "min_experiences", "train_iters", "batch_size", "sampler_cls", "sampler_params", - "epsilon", "tau", "double", "advantage_type", "loss_func" + "reward_discount", "target_update_freq", "train_iters", "batch_size", "sampler_cls", "sampler_params", + "epsilon", "soft_update_coefficient", "double", "advantage_type", "loss_func" ] def __init__( self, reward_discount: float, - experience_memory_size: int, - experience_memory_overwrite_type: str, - min_new_experiences_to_learn: int, target_update_freq: int, - min_experiences: int, train_iters: int, batch_size: int, sampler_cls="uniform", sampler_params=None, epsilon: float = .0, - tau: float = 0.1, + soft_update_coefficient: float = 0.1, double: bool = True, advantage_type: str = None, - loss_cls="mse", - flush_experience_memory_after_step: bool = False + loss_cls="mse" ): - super().__init__( - reward_discount, experience_memory_size, experience_memory_overwrite_type, min_new_experiences_to_learn, - flush_experience_memory_after_step - ) + self.reward_discount = reward_discount self.target_update_freq = target_update_freq - self.min_experiences = min_experiences self.train_iters = train_iters self.batch_size = batch_size self.sampler_cls = get_sampler_cls(sampler_cls) self.sampler_params = sampler_params if sampler_params else {} self.epsilon = epsilon - self.tau = tau + self.soft_update_coefficient = soft_update_coefficient self.double = double self.advantage_type = advantage_type self.loss_func = get_torch_loss_cls(loss_cls)() @@ -92,16 +76,40 @@ class DQN(AbsAgent): Args: model (SimpleMultiHeadModel): Q-value model. - config: Configuration for DQN algorithm. + config (DQNConfig): Configuration for DQN algorithm. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + to ``step``. Defaults to False. + min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. + Defaults to 1. + min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + Defaults to 1. """ - def __init__(self, model: SimpleMultiHeadModel, config: DQNConfig): + def __init__( + self, + model: SimpleMultiHeadModel, + config: DQNConfig, + experience_memory_size: int, + experience_memory_overwrite_type: str, + flush_experience_memory_after_step: bool = False, + min_new_experiences_to_trigger_learning: int = 1, + min_experience_memory_size: int = 1 + ): if (config.advantage_type is not None and (model.task_names is None or set(model.task_names) != {"state_value", "advantage"})): raise UnrecognizedTask( f"Expected model task names 'state_value' and 'advantage' since dueling DQN is used, " f"got {model.task_names}" ) - super().__init__(model, config) + super().__init__( + model, config, experience_memory_size, experience_memory_overwrite_type, + flush_experience_memory_after_step, + min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, + min_experience_memory_size=min_experience_memory_size + ) self._sampler = self.config.sampler_cls(self.experience_memory, **self.config.sampler_params) self._training_counter = 0 self._target_model = model.copy() if model.trainable else None @@ -131,10 +139,7 @@ def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: ]) def step(self): - print(len(self.experience_memory)) - if len(self.experience_memory) < self.config.min_experiences: - return - + print(f"experience memory size: {len(self.experience_memory)}") for _ in range(self.config.train_iters): # sample from the replay memory indexes, batch = self._sampler.sample(self.config.batch_size) @@ -156,7 +161,7 @@ def step(self): self.model.step(loss.mean()) self._training_counter += 1 if self._training_counter % self.config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.config.tau) + self._target_model.soft_update(self.model, self.config.soft_update_coefficient) # update auxillary info for the next round of sampling self._sampler.update(indexes, loss.detach().numpy()) diff --git a/maro/rl/agent/multi_agent_wrapper.py b/maro/rl/agent/multi_agent_wrapper.py index 49b0f087b..15cb784c8 100644 --- a/maro/rl/agent/multi_agent_wrapper.py +++ b/maro/rl/agent/multi_agent_wrapper.py @@ -51,7 +51,6 @@ def update(self, experiences: dict) -> set: The top-level keys of ``experiences`` will be treated as agent IDs. """ - print("updating agents") return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].update(exp)} def step(self, agent_ids=None): @@ -65,7 +64,7 @@ def step(self, agent_ids=None): self.agent_dict[agent_id].step() def load_model(self, model_dict: dict): - """Load models from memory for each agent.""" + """Load models from memory.""" for agent_id, model in model_dict.items(): self.agent_dict[agent_id].load_model(model) @@ -82,7 +81,7 @@ def dump_model(self, agent_ids=None): return {agent_id: self.agent_dict[agent_id].dump_model() for agent_id in agent_ids} def load_model_from_file(self, dir_path): - """Load models from disk for each agent.""" + """Load models from disk.""" for agent_id, agent in self.agent_dict.items(): agent.load_model_from_file(os.path.join(dir_path, agent_id)) @@ -103,7 +102,7 @@ def dump_model_to_file(self, dir_path: str, agent_ids=None): def get_versions(self, agent_ids=None): if agent_ids is None: - return {aent_id: agent.version for agent_id, agent in self.agent_dict.items()} + return {agent_id: agent.version for agent_id, agent in self.agent_dict.items()} elif not isinstance(agent_ids, set): return self.agent_dict[agent_ids].version else: diff --git a/maro/rl/agent/pg.py b/maro/rl/agent/pg.py index 2079c0383..05a68864f 100644 --- a/maro/rl/agent/pg.py +++ b/maro/rl/agent/pg.py @@ -10,14 +10,58 @@ from maro.rl.model import SimpleMultiHeadModel from maro.rl.utils import get_truncated_cumulative_reward -from .abs_agent import AbsAgent, AgentConfig +from .abs_agent import AbsAgent + + +class PolicyGradientConfig: + """Configuration for the Policy Gradient algorithm. + + Args: + reward_discount (float): Reward decay as defined in standard RL terminology. + """ + __slots__ = ["reward_discount"] + + def __init__(self, reward_discount: float): + self.reward_discount = reward_discount class PolicyGradient(AbsAgent): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + + Args: + model (SimpleMultiHeadModel): Multi-task model that computes action distributions and state values. + It may or may not have a shared bottom stack. + config (PolicyGradientConfig): Configuration for the PG algorithm. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + to ``step``. Defaults to True. + min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. + Defaults to 1. + min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + Defaults to 1. """ + def __init__( + self, + model: SimpleMultiHeadModel, + config: PolicyGradientConfig, + experience_memory_size: int, + experience_memory_overwrite_type: str, + flush_experience_memory_after_step: bool = True, + min_new_experiences_to_trigger_learning: int = 1, + min_experience_memory_size: int = 1 + ): + super().__init__( + model, config, experience_memory_size, experience_memory_overwrite_type, + flush_experience_memory_after_step, + min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, + min_experience_memory_size=min_experience_memory_size + ) + def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Use the actor (policy) model to generate stochastic actions. @@ -41,7 +85,7 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): states = torch.from_numpy(states).to(self.device) actions = torch.from_numpy(actions).to(self.device) - returns = get_truncated_cumulative_reward(rewards, self.config) + returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) returns = torch.from_numpy(returns).to(self.device) action_distributions = self.model(states) action_prob = action_distributions.gather(1, actions.unsqueeze(1)).squeeze() # (N, 1) diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 6e3a59137..3f298b0f4 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -55,11 +55,11 @@ def run(self): step_index = self.env.step_index self.agent.load_model(msg.body[MsgKey.MODEL]) - for _ in range(msg.body[MsgKey.NUM_STEPS]): + num_steps = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] + while self.env.state and num_steps > 0: action = self.agent.choose_action(self.env.state) self.env.step(action) - if not self.env.state: - break + num_steps -= 1 self._logger.info( f"Roll-out finished for ep {rollout_index}, segment {segment_index}" @@ -72,6 +72,7 @@ def run(self): MsgKey.END_OF_EPISODE: not self.env.state, MsgKey.ROLLOUT_INDEX: rollout_index, MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.METRICS: self.env.metrics, MsgKey.EXPERIENCES: self.env.pull_experiences(copy=self._pull_experiences_with_copy) } ) diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 6a3fff74b..3d8e81900 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -77,12 +77,11 @@ def run(self): f"(expected {index})" ) continue - - # if msg.tag == MsgTag.EXPERIENCE: - # print(f"received exp from actor {msg.source} ") - # print({agent_id: {k: len(v) for k, v in exp.items()} for agent_id, exp in msg.body[MsgKey.EXPERIENCE].items()}) - # If enough update messages have been received, call update() and break out of the loop to start - # the next episode. + + env_metrics = msg.body[MsgKey.METRICS] + self._logger.info( + f"ep-{rollout_index}, segment-{segment_index}: {env_metrics} ({exploration_params})" + ) if msg.body[MsgKey.SEGMENT_INDEX] == segment_index or not self.ignore_stale_experiences: updated_agents.update(self.agent.update(msg.body[MsgKey.EXPERIENCES])) self._logger.info(f"Learning finished for agent {updated_agents}") @@ -93,7 +92,6 @@ def run(self): break segment_index += 1 - # self._logger.info(f"ep-{rollout_index}: {env_metrics} ({exploration_params})") def terminate(self): """Tell the remote actors to exit.""" diff --git a/maro/rl/storage/sampler.py b/maro/rl/storage/sampler.py index 0818f903c..19135ee7d 100644 --- a/maro/rl/storage/sampler.py +++ b/maro/rl/storage/sampler.py @@ -43,5 +43,5 @@ def sample(self, size: int): indexes = np.random.choice(len(self.data), size=size, replace=self.replace) return indexes, self.data.get(indexes=indexes) - def update(self): + def update(self, indexes, values): pass diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 94756413d..8df8f43e1 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -25,7 +25,7 @@ def __init__( env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, - agent_update_interval: int = None + agent_update_interval: int = -1 ): super().__init__() if agent_update_interval == 0: @@ -47,7 +47,7 @@ def run(self): while self.env.state: action = self.agent.choose_action(self.env.state) self.env.step(action) - if self.agent_update_interval is not None and self.env.step_index % self.agent_update_interval == 0: + if self.agent_update_interval != -1 and self.env.step_index % self.agent_update_interval == 0: self.agent.update(self.env.pull_experiences()) self.agent.update(self.env.pull_experiences()) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 1fe86c8f3..fdac9a216 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -115,6 +115,7 @@ def get_unit(self, unit_id: int) -> UnitBase: Returns: UnitBase: Unit instance. """ + print(list(self.units.keys())) return self.units[unit_id] def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: From fa5e28762eb23cf3461090299dad223123dbc4aa Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 14:25:29 +0800 Subject: [PATCH 153/482] more settings --- scripts/random_config.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/random_config.py b/scripts/random_config.py index 80a9313fc..1bcce4e52 100644 --- a/scripts/random_config.py +++ b/scripts/random_config.py @@ -172,15 +172,22 @@ def construct_formula(constraint): "normal_vehicle": vehicle_conf, "facility_definitions": {}, "settings": { - 'global_reward_weight_producer': 0.50, - 'global_reward_weight_consumer': 0.50, + "global_reward_weight_producer": 0.50, + "global_reward_weight_consumer": 0.50, + "downsampling_rate": 1, + "episod_duration": 21, "initial_balance": 100000, + "consumption_hist_len": 4, + "sale_hist_len": 4, + "pending_order_len": 4, "constraint_state_hist_len": max_constraint_states, "total_echelons": 3, "replenishment_discount": 0.9, "reward_normalization": 1e7, - "constraint_violate_reward": -1e7, - "pending_order_len": 5 + "constraint_violate_reward": -1e6, + "gamma": 0.99, + "tail_timesteps": 7, + "heading_timesteps": 7, } } @@ -244,7 +251,8 @@ def construct_formula(constraint): # Why this configuration, as manufacture is controlled by action? "production_rate": int(sku_gamma[sku_name] * 50), # For this script, all sku is a production that produced by suppliers, no bom. - "type": "production" + "type": "production", + "product_unit_cost": 1, } facility["skus"] = sku_list From 18c8e7407e4b0ac2774a08b06d48a75b8fda68d3 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 15:13:30 +0800 Subject: [PATCH 154/482] cache the metrics --- .../scenarios/supply_chain/business_engine.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index b77137913..cb72274c3 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -35,6 +35,8 @@ def __init__(self, **kwargs): # Used to cache the action from outside, then dispatch to units at the beginning of step. self._action_cache = None + self._metrics_cache = None + @property def frame(self): return self._frame @@ -48,6 +50,9 @@ def configs(self) -> SupplyChainConfiguration: return self.world.configs def step(self, tick: int): + # Clear the metrics cache. + self._metrics_cache = None + # NOTE: we have to dispatch the action here. self._dispatch_action() self._step_by_facility(tick) @@ -144,19 +149,22 @@ def _dispatch_action(self): self._action_cache = None def get_metrics(self): - return { - "products": { - product.id: { - "sale_mean": product.get_sale_mean(), - "sale_std": product.get_sale_std(), - "selling_price": product.get_selling_price(), - "pending_order_daily": None if product.consumer is None else product.consumer.pending_order_daily - } for product in self._product_units - }, - "facilities": { - facility.id: { - "in_transit_orders": facility.get_in_transit_orders(), - "pending_order": None if facility.distribution is None else facility.distribution.get_pending_order() - } for facility in self.world.facilities.values() + if self._metrics_cache is None: + self._metrics_cache = { + "products": { + product.id: { + "sale_mean": product.get_sale_mean(), + "sale_std": product.get_sale_std(), + "selling_price": product.get_selling_price(), + "pending_order_daily": None if product.consumer is None else product.consumer.pending_order_daily + } for product in self._product_units + }, + "facilities": { + facility.id: { + "in_transit_orders": facility.get_in_transit_orders(), + "pending_order": None if facility.distribution is None else facility.distribution.get_pending_order() + } for facility in self.world.facilities.values() + } } - } + + return self._metrics_cache From eda8c17d0dbf7c79705d59590ad8a6d5288ee173 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 7 Apr 2021 08:32:57 +0000 Subject: [PATCH 155/482] all kinds of profiling logs --- examples/cim/dqn/config.yml | 6 +- examples/cim/dqn/main.py | 13 +- examples/supply_chain/dqn/config.yml | 11 +- .../supply_chain/dqn/distributed_launcher.py | 28 +- .../dqn/single_thread_launcher.py | 2 +- examples/supply_chain/env_wrapper.py | 9 +- maro/communication/proxy.py | 2 +- maro/rl/__init__.py | 4 +- maro/rl/agent/abs_agent.py | 2 +- maro/rl/agent/dqn.py | 1 - maro/rl/agent/multi_agent_wrapper.py | 4 +- maro/rl/distributed/__init__.py | 3 +- maro/rl/distributed/actor.py | 8 +- maro/rl/distributed/actor_manager.py | 109 + maro/rl/distributed/learner.py | 94 +- maro/rl/distributed/message_enums.py | 3 +- maro/rl/training/env_wrapper.py | 7 +- maro/rl/training/learner.py | 36 +- .../topologies/{random2 => random}/config.yml | 97267 ++++++++-------- requirements.dev.txt | 1 + 20 files changed, 49356 insertions(+), 48254 deletions(-) create mode 100644 maro/rl/distributed/actor_manager.py rename maro/simulator/scenarios/supply_chain/topologies/{random2 => random}/config.yml (86%) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 5e308519b..fc4e87f62 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -62,7 +62,7 @@ training: topology: toy.4p_ssdd_l0.0 durations: 1120 max_episode: 100 - agent_update_interval: -1 + agent_update_interval: 200 exploration: parameter_names: - epsilon @@ -72,7 +72,7 @@ training: end: 0.0 distributed: group: cim-dqn - num_actors: 2 + num_actors: 3 redis_host: localhost redis_port: 6379 - min_actor_finishes: 2 \ No newline at end of file + required_actor_finishes: 2 \ No newline at end of file diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index ae45d1f3d..22937b618 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -8,8 +8,8 @@ from os.path import dirname, join, realpath from maro.rl import ( - Actor, DQN, DQNConfig, DistLearner, FullyConnectedBlock, MultiAgentWrapper, OptimOption, SimpleMultiHeadModel, - TwoPhaseLinearParameterScheduler + Actor, ActorManager, DQN, DQNConfig, DistLearner, FullyConnectedBlock, MultiAgentWrapper, OptimOption, + SimpleMultiHeadModel, TwoPhaseLinearParameterScheduler ) from maro.simulator import Env from maro.utils import set_seeds @@ -49,11 +49,14 @@ def get_dqn_agent(): def cim_dqn_learner(): agent = MultiAgentWrapper({name: get_dqn_agent() for name in Env(**config["training"]["env"]).agent_idx_list}) scheduler = TwoPhaseLinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) + actor_manager = ActorManager( + NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False} + ) learner = DistLearner( - agent, scheduler, NUM_ACTORS, GROUP, - proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, + agent, scheduler, actor_manager, agent_update_interval=config["training"]["agent_update_interval"], - ignore_stale_experiences=False + required_actor_finishes=config["distributed"]["required_actor_finishes"], + discard_stale_experiences=False ) learner.run() diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index d0e874f00..0a0a9e37d 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -34,9 +34,9 @@ training: env: scenario: supply_chain topology: sample1 - durations: 200 - max_episode: 10 - agent_update_interval: 45 + durations: 100 + max_episode: 3 + agent_update_interval: 10 exploration: parameter_names: - epsilon @@ -45,7 +45,6 @@ training: distributed: group: sc-dqn num_actors: 2 - redis_host: localhost + redis_host: maro-redis redis_port: 6379 - learner_update_trigger: 2 - replay_sync_interval: 30 + required_actor_finishes: 2 diff --git a/examples/supply_chain/dqn/distributed_launcher.py b/examples/supply_chain/dqn/distributed_launcher.py index 4f0123dd6..c2d09f306 100644 --- a/examples/supply_chain/dqn/distributed_launcher.py +++ b/examples/supply_chain/dqn/distributed_launcher.py @@ -8,8 +8,8 @@ from os.path import dirname, join, realpath from maro.rl import ( - Actor, DQN, DQNConfig, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, - OffPolicyDistLearner, OptimOption, SimpleMultiHeadModel + Actor, ActorManager, DQN, DQNConfig, DistLearner, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, + OptimOption, SimpleMultiHeadModel ) from maro.simulator import Env from maro.utils import set_seeds @@ -32,18 +32,14 @@ def sc_dqn_learner(): agent = get_sc_agents(Env(**config["training"]["env"]).agent_idx_list, config["agent"]) scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) - actor_proxy = ActorProxy( - NUM_ACTORS, GROUP, - proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, - update_trigger=config["distributed"]["learner_update_trigger"], - replay_memory_size=config["training"]["replay_memory"]["size"], - replay_memory_overwrite_type=config["training"]["replay_memory"]["overwrite_type"] + actor_manager = ActorManager( + NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False} ) - learner = OffPolicyDistLearner( - actor_proxy, agent, scheduler, - min_experiences_to_train=config["training"]["min_experiences_to_train"], - train_iter=config["training"]["train_iter"], - batch_size=config["training"]["batch_size"] + learner = DistLearner( + agent, scheduler, actor_manager, + agent_update_interval=config["training"]["agent_update_interval"], + required_actor_finishes=config["distributed"]["required_actor_finishes"], + discard_stale_experiences=False ) learner.run() @@ -51,11 +47,7 @@ def sc_dqn_learner(): def sc_dqn_actor(): env = Env(**config["training"]["env"]) agent = get_sc_agents(env.agent_idx_list, config["agent"]) - actor = Actor( - SCEnvWrapper(env), agent, GROUP, - replay_sync_interval=config["distributed"]["replay_sync_interval"], - proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)} - ) + actor = Actor(SCEnvWrapper(env), agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}) actor.run() diff --git a/examples/supply_chain/dqn/single_thread_launcher.py b/examples/supply_chain/dqn/single_thread_launcher.py index 31bbec855..8ad86a742 100644 --- a/examples/supply_chain/dqn/single_thread_launcher.py +++ b/examples/supply_chain/dqn/single_thread_launcher.py @@ -23,7 +23,7 @@ set_seeds(1024) # for reproducibility env = Env(**config["training"]["env"]) agent = get_sc_agents(env.agent_idx_list, config["agent"]) - scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) + scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) learner = Learner( SCEnvWrapper(env), agent, scheduler, agent_update_interval=config["training"]["agent_update_interval"] diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 270761a2d..47daf766e 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -147,14 +147,12 @@ def __init__(self, env: Env): if manufacture is not None: product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"] - ] = facility_id + self.unit_2_facility_dict[manufacture["id"]] = facility_id self.facility_levels[facility_id][product_id] = product_info def get_state(self, event): self.cur_balance_sheet_reward = self.balance_cal.calc() - return self._get_state() def get_action(self, action_by_agent): @@ -178,6 +176,7 @@ def get_action(self, action_by_agent): if sources: source_id = sources[0] product_id = self.consumer2product.get(agent_id, 0) + agent_id = int(agent_id.split(".")[1]) env_action[agent_id] = ConsumerAction(agent_id, product_id, source_id, action, 1) # manufacturer action elif agent_id.startswith("producer"): @@ -186,11 +185,9 @@ def get_action(self, action_by_agent): return env_action - def get_reward(self, tick=None): + def get_reward(self, tick=None, target_agents=None): wc = self.env.configs.settings["global_reward_weight_consumer"] - parent_facility_balance = {} - for f_id, sheet in self.cur_balance_sheet_reward.items(): if f_id in self.unit_2_facility_dict: # it is a product unit diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 3dc2e5a9b..110b8a14b 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -99,7 +99,7 @@ def __init__( self._max_retries = max_retries self._retry_interval_base_value = retry_interval_base_value self._log_enable = log_enable - self._logger = InternalLogger(component_name=self._name) if self._log_enable else DummyLogger() + self._logger = InternalLogger(component_name=self._name + "_proxy") if self._log_enable else DummyLogger() # TODO:In multiprocess with spawn start method, the driver must be initiated before the Redis. # Otherwise it will cause Error 9: Bad File Descriptor in proxy.__del__(). Root cause not found. diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index d3c561d91..d3069a4d8 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -5,7 +5,7 @@ DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient, PolicyGradientConfig ) -from maro.rl.distributed import Actor, DistLearner +from maro.rl.distributed import Actor, ActorManager, DistLearner from maro.rl.exploration import ( AbsExplorer, EpsilonGreedyExplorer, GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer ) @@ -22,7 +22,7 @@ __all__ = [ "AbsAgent", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", "PolicyGradient", "PolicyGradientConfig", - "Actor", "DistLearner", + "Actor", "ActorManager", "DistLearner", "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", diff --git a/maro/rl/agent/abs_agent.py b/maro/rl/agent/abs_agent.py index 20b8c4730..d06762886 100644 --- a/maro/rl/agent/abs_agent.py +++ b/maro/rl/agent/abs_agent.py @@ -81,7 +81,7 @@ def choose_action(self, state): def set_exploration_params(self, **params): pass - def update(self, experiences: dict) -> bool: + def learn(self, experiences: dict) -> bool: """Store experinces in the experience memory and train the model if necessary.""" if set(experiences.keys()) != {"S", "A", "R", "S_"}: raise ValueError("The keys of experiences must be {'S', 'A', 'R', 'S_'}") diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index 306667856..32a50dc4c 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -139,7 +139,6 @@ def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: ]) def step(self): - print(f"experience memory size: {len(self.experience_memory)}") for _ in range(self.config.train_iters): # sample from the replay memory indexes, batch = self._sampler.sample(self.config.batch_size) diff --git a/maro/rl/agent/multi_agent_wrapper.py b/maro/rl/agent/multi_agent_wrapper.py index 15cb784c8..7551cc1da 100644 --- a/maro/rl/agent/multi_agent_wrapper.py +++ b/maro/rl/agent/multi_agent_wrapper.py @@ -46,12 +46,12 @@ def set_exploration_params(self, params): for agent in self.agent_dict.values(): agent.set_exploration_params(**params) - def update(self, experiences: dict) -> set: + def learn(self, experiences: dict) -> set: """Store experiences in the agents' experience memory. The top-level keys of ``experiences`` will be treated as agent IDs. """ - return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].update(exp)} + return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].learn(exp)} def step(self, agent_ids=None): if agent_ids is None: diff --git a/maro/rl/distributed/__init__.py b/maro/rl/distributed/__init__.py index 76a304121..a1513dd4a 100644 --- a/maro/rl/distributed/__init__.py +++ b/maro/rl/distributed/__init__.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from .actor import Actor +from .actor_manager import ActorManager from .learner import DistLearner -__all__ = ["Actor", "DistLearner"] +__all__ = ["Actor", "ActorManager", "DistLearner"] diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 3f298b0f4..39be73fc8 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -36,7 +36,7 @@ def __init__( self._pull_experiences_with_copy = pull_experiences_with_copy if proxy_options is None: proxy_options = {} - self._proxy = Proxy(group, "actor", {"learner": 1}, **proxy_options) + self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) self._logger = InternalLogger(self._proxy.name) def run(self): @@ -65,14 +65,16 @@ def run(self): f"Roll-out finished for ep {rollout_index}, segment {segment_index}" f"(steps {step_index} - {self.env.step_index})" ) + experiences, num_exp = self.env.pull_experiences(copy=self._pull_experiences_with_copy) self._proxy.reply( msg, tag=MsgTag.ROLLOUT_DONE, body={ - MsgKey.END_OF_EPISODE: not self.env.state, + MsgKey.ENV_END: not self.env.state, MsgKey.ROLLOUT_INDEX: rollout_index, MsgKey.SEGMENT_INDEX: segment_index, MsgKey.METRICS: self.env.metrics, - MsgKey.EXPERIENCES: self.env.pull_experiences(copy=self._pull_experiences_with_copy) + MsgKey.EXPERIENCES: experiences, + MsgKey.NUM_EXPERIENCES: num_exp } ) diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py new file mode 100644 index 000000000..6bd385f23 --- /dev/null +++ b/maro/rl/distributed/actor_manager.py @@ -0,0 +1,109 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict +from typing import Union + +from maro.communication import Message, Proxy, SessionType +from maro.utils import InternalLogger + +from .message_enums import MsgTag, MsgKey + + +class ActorManager(object): + """Learner class for distributed training. + + Args: + agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. + scheduler (Scheduler): . + num_actors (int): Expected number of actors in the group identified by ``group_name``. + group_name (str): Identifier of the group to which the actor belongs. It must be the same group name + assigned to the actors (and roll-out clients, if any). + proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to None. + update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger + learner updates, i.e., model training. + """ + def __init__( + self, + num_actors: int, + group_name: str, + proxy_options: dict = None, + log_env_metrics: bool = False + ): + super().__init__() + peers = {"actor": num_actors} + if proxy_options is None: + proxy_options = {} + self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_options) + self._actors = self._proxy.peers["actor"] # remote actor ID's + self.total_experiences_collected = 0 + self.total_env_steps = 0 + self._log_env_metrics = log_env_metrics + self._logger = InternalLogger("ACTOR_MANAGER") + + def collect( + self, + rollout_index: int, + segment_index: int, + num_steps: int, + models: dict = None, + exploration_params=None, + required_actor_finishes: int = None, + discard_stale_experiences: bool = True + ): + """Collect experiences from actors.""" + if required_actor_finishes is None: + required_actor_finishes = len(self._actors) + + msg_body = { + MsgKey.ROLLOUT_INDEX: rollout_index, + MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.NUM_STEPS: num_steps, + MsgKey.MODEL: models + } + + if exploration_params: + msg_body[MsgKey.EXPLORATION_PARAMS] = exploration_params + + if self._log_env_metrics: + self._logger.info(f"EPISODE-{rollout_index}, SEGMENT-{segment_index}: ") + self._logger.info(f"exploration_params: {exploration_params}") + + self._proxy.ibroadcast("actor", MsgTag.ROLLOUT, SessionType.TASK, body=msg_body) + self._logger.info(f"Sent roll-out requests for ep-{rollout_index}, segment-{segment_index}") + + # Receive roll-out results from remote actors + num_finishes = 0 + for msg in self._proxy.receive(): + if msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: + self._logger.info( + f"Ignore a message of type {msg.tag} with ep {msg.body[MsgKey.ROLLOUT_INDEX]} " + f"(expected {rollout_index})" + ) + continue + + # log roll-out summary + if self._log_env_metrics: + env_metrics = msg.body[MsgKey.METRICS] + self._logger.info(f"env_metrics: {env_metrics}") + + if msg.body[MsgKey.SEGMENT_INDEX] != segment_index: + if not discard_stale_experiences: + experiences = msg.body[MsgKey.EXPERIENCES] + self.total_experiences_collected += msg.body[MsgKey.NUM_EXPERIENCES] + self.total_env_steps += num_steps + yield msg.body[MsgKey.EXPERIENCES], msg.body[MsgKey.ENV_END] + else: + self.total_experiences_collected += msg.body[MsgKey.NUM_EXPERIENCES] + self.total_env_steps += num_steps + yield msg.body[MsgKey.EXPERIENCES], msg.body[MsgKey.ENV_END] + num_finishes += 1 + + if num_finishes == required_actor_finishes: + break + + def exit(self): + """Tell the remote actors to exit.""" + self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) + self._logger.info("Exiting...") diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 3d8e81900..2f3279ca5 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import time from collections import defaultdict from typing import Union @@ -9,6 +10,7 @@ from maro.rl.scheduling import Scheduler from maro.utils import InternalLogger +from .actor_manager import ActorManager from .message_enums import MsgTag, MsgKey @@ -17,83 +19,51 @@ class DistLearner(object): Args: agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. - scheduler (Scheduler): . - num_actors (int): Expected number of actors in the group identified by ``group_name``. - group_name (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the actors (and roll-out clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. - update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger - learner updates, i.e., model training. + scheduler (Scheduler): A ``Scheduler`` instance for generating exploration parameters. """ def __init__( self, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, - num_actors: int, - group_name: str, - proxy_options: dict = None, + actor_manager: ActorManager, agent_update_interval: int = -1, - min_actor_finishes: str = None, - ignore_stale_experiences: bool = True + required_actor_finishes: str = None, + discard_stale_experiences: bool = True ): super().__init__() self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent self.scheduler = scheduler - peers = {"actor": num_actors} - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group_name, "learner", peers, **proxy_options) - self.actors = self._proxy.peers["actor"] # remote actor ID's - if min_actor_finishes is None: - self.min_actor_finishes = len(self.actors) + self.actor_manager = actor_manager self.agent_update_interval = agent_update_interval - self.ignore_stale_experiences = ignore_stale_experiences + self.required_actor_finishes = required_actor_finishes + self.discard_stale_experiences = discard_stale_experiences + self._total_learning_time = 0 self._logger = InternalLogger("LEARNER") def run(self): - """Main learning loop""" + """Main learning loop.""" + t0 = time.time() for exploration_params in self.scheduler: - updated_agents = self.agent.names - rollout_index, segment_index, num_episode_finishes = self.scheduler.iter, 0, 0 - while num_episode_finishes < self.min_actor_finishes: - msg_body = { - MsgKey.ROLLOUT_INDEX: rollout_index, - MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.NUM_STEPS: self.agent_update_interval, - MsgKey.MODEL: self.agent.dump_model(agent_ids=updated_agents) - } - if segment_index == 0 and exploration_params: - msg_body[MsgKey.EXPLORATION_PARAMS] = exploration_params - self._proxy.ibroadcast("actor", MsgTag.ROLLOUT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent roll-out requests for ep-{rollout_index}, segment-{segment_index}") - - # Receive roll-out results from remote actors - updated_agents, num_segment_finishes = set(), 0 - for msg in self._proxy.receive(): - if msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: - self._logger.info( - f"Ignore a message of type {msg.tag} with ep {msg.body[MsgKey.ROLLOUT_INDEX]} " - f"(expected {index})" - ) - continue - - env_metrics = msg.body[MsgKey.METRICS] - self._logger.info( - f"ep-{rollout_index}, segment-{segment_index}: {env_metrics} ({exploration_params})" - ) - if msg.body[MsgKey.SEGMENT_INDEX] == segment_index or not self.ignore_stale_experiences: - updated_agents.update(self.agent.update(msg.body[MsgKey.EXPERIENCES])) - self._logger.info(f"Learning finished for agent {updated_agents}") - if msg.body[MsgKey.END_OF_EPISODE]: - num_episode_finishes += 1 - num_segment_finishes += 1 - if num_segment_finishes == self.min_actor_finishes: - break + updated_agents, num_actor_finishes, segment_index = self.agent.names, 0, 0 + while num_actor_finishes < self.required_actor_finishes: + for exp, done in self.actor_manager.collect( + self.scheduler.iter, + segment_index, + self.agent_update_interval, + models=self.agent.dump_model(agent_ids=updated_agents), + exploration_params=exploration_params if segment_index == 0 else None, + required_actor_finishes=self.required_actor_finishes, + discard_stale_experiences=self.discard_stale_experiences + ): + tl0 = time.time() + updated_agents = self.agent.learn(exp) + num_actor_finishes += done + self._total_learning_time += time.time() - tl0 + self._logger.info(f"total running time: {time.time() - t0}") + self._logger.info(f"total learning time: {self._total_learning_time}") + self._logger.info(f"total env steps: {self.actor_manager.total_env_steps}") + self._logger.info(f"total experiences collected: {self.actor_manager.total_experiences_collected}") segment_index += 1 - def terminate(self): - """Tell the remote actors to exit.""" - self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) - self._logger.info("Exiting...") + self.actor_manager.exit() diff --git a/maro/rl/distributed/message_enums.py b/maro/rl/distributed/message_enums.py index 7aca877b2..08d84cb49 100644 --- a/maro/rl/distributed/message_enums.py +++ b/maro/rl/distributed/message_enums.py @@ -20,6 +20,7 @@ class MsgKey(Enum): TIME_STEP = "time_step" METRICS = "metrics" EXPERIENCES = "experiences" + NUM_EXPERIENCES = "num_experiences" STATE = "state" TRAINING = "training" MODEL = "model" @@ -27,4 +28,4 @@ class MsgKey(Enum): EXPLORATION_PARAMS = "exploration_params" NUM_STEPS = "num_steps" SEGMENT_INDEX = "segment_index" - END_OF_EPISODE = "end_of_episode" + ENV_END = "env_end" diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index c9873a0ef..3ea2340d2 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -50,15 +50,16 @@ def start(self, rollout_index: int = None): assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 def pull_experiences(self, copy: bool = False): - experience = defaultdict(dict) + experience, num_experiences = defaultdict(dict), 0 for agent_id, replay in self.replay.items(): num_complete = min(len(replay["R"]), len(replay["S_"])) + num_experiences += num_complete for k, vals in replay.items(): experience[agent_id][k] = vals[:num_complete] if not copy: del vals[:num_complete] - return experience + return experience, num_experiences @property def metrics(self): @@ -91,7 +92,7 @@ def step(self, action_by_agent: dict): # t0 = time.time() self._step_index += 1 env_action = self.get_action(action_by_agent) - self._acting_agents.append((self.env.tick, list(env_action.keys()))) + self._acting_agents.append((self.env.tick, list(action_by_agent.keys()))) if len(env_action) == 1: env_action = list(env_action.values())[0] # t1 = time.time() diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 8df8f43e1..93dda8ec9 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -25,7 +25,8 @@ def __init__( env: AbsEnvWrapper, agent: Union[AbsAgent, MultiAgentWrapper], scheduler: Scheduler, - agent_update_interval: int = -1 + agent_update_interval: int = -1, + log_env_metrics: bool = False ): super().__init__() if agent_update_interval == 0: @@ -34,11 +35,15 @@ def __init__( self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent self.scheduler = scheduler self.agent_update_interval = agent_update_interval - self.logger = InternalLogger("LEARNER") + self.total_env_steps = 0 + self.total_experiences_collected = 0 + self.total_learning_time = 0 + self._log_env_metrics = log_env_metrics + self._logger = InternalLogger("LEARNER") def run(self): + t0 = time.time() for exploration_params in self.scheduler: - # t0 = time.time() self.env.reset() if exploration_params: self.agent.set_exploration_params(exploration_params) @@ -48,10 +53,25 @@ def run(self): action = self.agent.choose_action(self.env.state) self.env.step(action) if self.agent_update_interval != -1 and self.env.step_index % self.agent_update_interval == 0: - self.agent.update(self.env.pull_experiences()) + exp, num_exp = self.env.pull_experiences() + tl0 = time.time() + self.agent.learn(exp) + self.total_learning_time += time.time() - tl0 + self.total_env_steps += self.agent_update_interval + self.total_experiences_collected += num_exp + self._logger.info(f"total running time: {time.time() - t0}") + self._logger.info(f"total learning time: {self.total_learning_time}") + self._logger.info(f"total env steps: {self.total_env_steps}") + self._logger.info(f"total experiences collected: {self.total_experiences_collected}") - self.agent.update(self.env.pull_experiences()) - self.logger.info(f"ep-{self.scheduler.iter}: {self.env.metrics} ({exploration_params})") + exp, num_exp = self.env.pull_experiences() + tl0 = time.time() + self.agent.learn(exp) + self.total_learning_time += time.time() - tl0 + if self._log_env_metrics: + self._logger.info(f"ep-{self.scheduler.iter}: {self.env.metrics} ({exploration_params})") - # t1 = time.time() - # print(f"roll-out time: {t1 - t0}") + self._logger.info(f"total running time: {time.time() - t0}") + self._logger.info(f"total learning time: {self.total_learning_time}") + self._logger.info(f"total env steps: {self.total_env_steps}") + self._logger.info(f"total experiences collected: {self.total_experiences_collected}") diff --git a/maro/simulator/scenarios/supply_chain/topologies/random2/config.yml b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml similarity index 86% rename from maro/simulator/scenarios/supply_chain/topologies/random2/config.yml rename to maro/simulator/scenarios/supply_chain/topologies/random/config.yml index a2d1f1506..32f21232f 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/random2/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml @@ -1,48130 +1,49137 @@ -facility_definitions: - RetailerFacility: - children: - products: - class: StoreProductUnit - config: - agent_type: 5 - consumer: - class: ConsumerUnit - seller: - class: SellerUnit - config: - sale_hist_len: 4 - is_template: true - storage: - class: StorageUnit - class: RetailerFacility - config: - agent_type: 2 - SupplierFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: 3 - consumer: - class: ConsumerUnit - manufacture: - class: ManufactureUnit - is_template: true - storage: - class: StorageUnit - class: SupplierFacility - config: - agent_type: 0 - WarehouseFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: 4 - consumer: - class: ConsumerUnit - is_template: true - storage: - class: StorageUnit - class: WarehouseFacility - config: - agent_type: 1 -normal_vehicle: &id001 - class: VehicleUnit - config: - patient: 100 - unit_transport_cost: 1 -settings: - constraint_state_hist_len: 4 - constraint_violate_reward: -10000000.0 - global_reward_weight_consumer: 0.5 - global_reward_weight_producer: 0.5 - initial_balance: 100000 - pending_order_len: 5 - replenishment_discount: 0.9 - reward_normalization: 10000000.0 - total_echelons: 3 -world: - facilities: - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5139100 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 200 - definition_ref: SupplierFacility - name: SUPPLIER0 - skus: - SKU0: - cost: 364 - init_stock: 4250 - price: 405 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU1: - cost: 37 - init_stock: 2100 - price: 42 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU10: - cost: 112 - init_stock: 4550 - price: 125 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU100: - cost: 304 - init_stock: 4600 - price: 338 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU101: - cost: 392 - init_stock: 3450 - price: 436 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU102: - cost: 81 - init_stock: 3350 - price: 90 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU103: - cost: 144 - init_stock: 4050 - price: 161 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU104: - cost: 423 - init_stock: 2000 - price: 470 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU105: - cost: 192 - init_stock: 2700 - price: 214 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU106: - cost: 33 - init_stock: 2550 - price: 37 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU107: - cost: 57 - init_stock: 3050 - price: 64 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU108: - cost: 426 - init_stock: 1500 - price: 474 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU109: - cost: 358 - init_stock: 2550 - price: 398 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU11: - cost: 95 - init_stock: 4400 - price: 106 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU110: - cost: 63 - init_stock: 1250 - price: 71 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU111: - cost: 164 - init_stock: 2500 - price: 183 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU112: - cost: 70 - init_stock: 550 - price: 78 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU113: - cost: 260 - init_stock: 2650 - price: 289 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU114: - cost: 299 - init_stock: 3250 - price: 333 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU115: - cost: 393 - init_stock: 1250 - price: 437 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU116: - cost: 230 - init_stock: 3400 - price: 256 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU117: - cost: 110 - init_stock: 1100 - price: 123 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU118: - cost: 162 - init_stock: 2450 - price: 181 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU119: - cost: 52 - init_stock: 250 - price: 58 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU12: - cost: 283 - init_stock: 3800 - price: 315 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU120: - cost: 27 - init_stock: 600 - price: 31 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU121: - cost: 229 - init_stock: 3350 - price: 255 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU122: - cost: 199 - init_stock: 1850 - price: 222 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU123: - cost: 364 - init_stock: 3100 - price: 405 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU124: - cost: 152 - init_stock: 900 - price: 169 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU125: - cost: 309 - init_stock: 1750 - price: 344 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU126: - cost: 325 - init_stock: 4700 - price: 362 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU127: - cost: 134 - init_stock: 1550 - price: 149 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU128: - cost: 193 - init_stock: 4350 - price: 215 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU129: - cost: 74 - init_stock: 2650 - price: 83 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU13: - cost: 426 - init_stock: 2250 - price: 474 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU130: - cost: 384 - init_stock: 4450 - price: 427 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU131: - cost: 52 - init_stock: 2450 - price: 58 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU132: - cost: 273 - init_stock: 750 - price: 304 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU133: - cost: 144 - init_stock: 1450 - price: 161 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU134: - cost: 343 - init_stock: 3700 - price: 382 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU135: - cost: 113 - init_stock: 4400 - price: 126 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU136: - cost: 387 - init_stock: 350 - price: 431 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU137: - cost: 243 - init_stock: 1100 - price: 270 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU138: - cost: 9 - init_stock: 3150 - price: 10 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU139: - cost: 233 - init_stock: 4100 - price: 259 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU14: - cost: 163 - init_stock: 4550 - price: 182 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU140: - cost: 330 - init_stock: 2750 - price: 367 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU141: - cost: 292 - init_stock: 500 - price: 325 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU142: - cost: 395 - init_stock: 4050 - price: 439 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU143: - cost: 234 - init_stock: 3450 - price: 260 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU144: - cost: 411 - init_stock: 300 - price: 457 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU145: - cost: 432 - init_stock: 3750 - price: 480 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU146: - cost: 218 - init_stock: 4800 - price: 243 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU147: - cost: 300 - init_stock: 3700 - price: 334 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU148: - cost: 128 - init_stock: 2000 - price: 143 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU149: - cost: 226 - init_stock: 2550 - price: 252 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU15: - cost: 288 - init_stock: 1800 - price: 320 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU150: - cost: 74 - init_stock: 3100 - price: 83 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU151: - cost: 52 - init_stock: 1250 - price: 58 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU152: - cost: 228 - init_stock: 1750 - price: 254 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU153: - cost: 44 - init_stock: 3600 - price: 49 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU154: - cost: 351 - init_stock: 500 - price: 391 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU155: - cost: 32 - init_stock: 1150 - price: 36 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU156: - cost: 126 - init_stock: 1850 - price: 140 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU157: - cost: 336 - init_stock: 1950 - price: 374 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU158: - cost: 163 - init_stock: 1350 - price: 182 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU159: - cost: 385 - init_stock: 2000 - price: 428 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU16: - cost: 249 - init_stock: 3550 - price: 277 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU160: - cost: 116 - init_stock: 3900 - price: 129 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU161: - cost: 28 - init_stock: 4800 - price: 32 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU162: - cost: 61 - init_stock: 3050 - price: 68 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU163: - cost: 116 - init_stock: 2400 - price: 129 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU164: - cost: 172 - init_stock: 3400 - price: 192 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU165: - cost: 284 - init_stock: 3100 - price: 316 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU166: - cost: 408 - init_stock: 350 - price: 454 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU167: - cost: 97 - init_stock: 3550 - price: 108 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU168: - cost: 387 - init_stock: 1500 - price: 431 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU169: - cost: 385 - init_stock: 3600 - price: 428 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU17: - cost: 353 - init_stock: 2000 - price: 393 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU170: - cost: 96 - init_stock: 700 - price: 107 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU171: - cost: 12 - init_stock: 4750 - price: 14 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU172: - cost: 261 - init_stock: 4300 - price: 291 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU173: - cost: 171 - init_stock: 750 - price: 190 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU174: - cost: 387 - init_stock: 1300 - price: 431 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU175: - cost: 319 - init_stock: 4350 - price: 355 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU176: - cost: 274 - init_stock: 2450 - price: 305 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU177: - cost: 261 - init_stock: 1150 - price: 291 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU178: - cost: 375 - init_stock: 1000 - price: 417 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU179: - cost: 16 - init_stock: 3050 - price: 18 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU18: - cost: 200 - init_stock: 2600 - price: 223 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU180: - cost: 335 - init_stock: 1850 - price: 373 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU181: - cost: 437 - init_stock: 2900 - price: 486 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU182: - cost: 346 - init_stock: 1700 - price: 385 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU183: - cost: 175 - init_stock: 4000 - price: 195 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU184: - cost: 214 - init_stock: 250 - price: 238 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU185: - cost: 45 - init_stock: 2250 - price: 50 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU186: - cost: 128 - init_stock: 4700 - price: 143 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU187: - cost: 256 - init_stock: 1900 - price: 285 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU188: - cost: 162 - init_stock: 1650 - price: 180 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU189: - cost: 264 - init_stock: 900 - price: 294 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU19: - cost: 270 - init_stock: 1400 - price: 301 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU190: - cost: 352 - init_stock: 1850 - price: 392 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU191: - cost: 111 - init_stock: 850 - price: 124 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU192: - cost: 106 - init_stock: 1850 - price: 118 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU193: - cost: 151 - init_stock: 4100 - price: 168 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU194: - cost: 297 - init_stock: 1800 - price: 331 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU195: - cost: 72 - init_stock: 1650 - price: 81 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU196: - cost: 84 - init_stock: 600 - price: 94 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU197: - cost: 148 - init_stock: 2100 - price: 165 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU198: - cost: 408 - init_stock: 3900 - price: 454 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU199: - cost: 163 - init_stock: 1850 - price: 182 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU2: - cost: 54 - init_stock: 2850 - price: 60 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU20: - cost: 111 - init_stock: 4200 - price: 124 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU200: - cost: 319 - init_stock: 2950 - price: 355 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU201: - cost: 236 - init_stock: 2300 - price: 263 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU202: - cost: 284 - init_stock: 1050 - price: 316 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU203: - cost: 130 - init_stock: 300 - price: 145 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU204: - cost: 25 - init_stock: 1500 - price: 28 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU205: - cost: 171 - init_stock: 4750 - price: 190 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU206: - cost: 299 - init_stock: 3100 - price: 333 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU207: - cost: 289 - init_stock: 3450 - price: 322 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU208: - cost: 258 - init_stock: 800 - price: 287 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU209: - cost: 250 - init_stock: 850 - price: 278 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU21: - cost: 367 - init_stock: 900 - price: 408 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU210: - cost: 289 - init_stock: 2050 - price: 322 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU211: - cost: 410 - init_stock: 2600 - price: 456 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU212: - cost: 223 - init_stock: 850 - price: 248 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU213: - cost: 258 - init_stock: 250 - price: 287 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU214: - cost: 80 - init_stock: 3150 - price: 89 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU215: - cost: 159 - init_stock: 4800 - price: 177 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU216: - cost: 208 - init_stock: 4450 - price: 232 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU217: - cost: 28 - init_stock: 4250 - price: 32 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU218: - cost: 46 - init_stock: 900 - price: 52 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU219: - cost: 447 - init_stock: 2300 - price: 497 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU22: - cost: 447 - init_stock: 1500 - price: 497 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU220: - cost: 392 - init_stock: 2200 - price: 436 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU221: - cost: 52 - init_stock: 3700 - price: 58 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU222: - cost: 156 - init_stock: 700 - price: 174 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU223: - cost: 126 - init_stock: 2300 - price: 140 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU224: - cost: 221 - init_stock: 1350 - price: 246 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU225: - cost: 15 - init_stock: 2650 - price: 17 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU226: - cost: 72 - init_stock: 2800 - price: 81 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU227: - cost: 427 - init_stock: 1500 - price: 475 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU228: - cost: 266 - init_stock: 3250 - price: 296 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU229: - cost: 418 - init_stock: 2450 - price: 465 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU23: - cost: 270 - init_stock: 3400 - price: 301 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU230: - cost: 434 - init_stock: 1750 - price: 483 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU231: - cost: 155 - init_stock: 650 - price: 173 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU232: - cost: 283 - init_stock: 1100 - price: 315 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU233: - cost: 51 - init_stock: 250 - price: 57 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU234: - cost: 283 - init_stock: 3450 - price: 315 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU235: - cost: 82 - init_stock: 400 - price: 92 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU236: - cost: 112 - init_stock: 900 - price: 125 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU237: - cost: 180 - init_stock: 1050 - price: 201 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU238: - cost: 94 - init_stock: 1900 - price: 105 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU239: - cost: 59 - init_stock: 4300 - price: 66 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU24: - cost: 89 - init_stock: 3500 - price: 99 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU240: - cost: 235 - init_stock: 3050 - price: 262 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU241: - cost: 324 - init_stock: 3200 - price: 361 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU242: - cost: 198 - init_stock: 4400 - price: 221 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU243: - cost: 126 - init_stock: 3100 - price: 141 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU244: - cost: 173 - init_stock: 2550 - price: 193 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU245: - cost: 148 - init_stock: 2750 - price: 165 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU246: - cost: 426 - init_stock: 800 - price: 474 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU247: - cost: 59 - init_stock: 4500 - price: 66 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU248: - cost: 122 - init_stock: 650 - price: 136 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU249: - cost: 74 - init_stock: 3000 - price: 83 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU25: - cost: 9 - init_stock: 2750 - price: 11 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU250: - cost: 279 - init_stock: 4250 - price: 310 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU251: - cost: 222 - init_stock: 2200 - price: 247 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU252: - cost: 172 - init_stock: 2400 - price: 192 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU253: - cost: 243 - init_stock: 3000 - price: 270 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU254: - cost: 148 - init_stock: 4950 - price: 165 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU255: - cost: 44 - init_stock: 2700 - price: 49 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU256: - cost: 282 - init_stock: 4300 - price: 314 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU257: - cost: 421 - init_stock: 1200 - price: 468 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU258: - cost: 135 - init_stock: 500 - price: 151 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU259: - cost: 350 - init_stock: 950 - price: 389 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU26: - cost: 313 - init_stock: 1850 - price: 348 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU260: - cost: 81 - init_stock: 4250 - price: 90 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU261: - cost: 109 - init_stock: 3750 - price: 122 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU262: - cost: 13 - init_stock: 900 - price: 15 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU263: - cost: 315 - init_stock: 1950 - price: 350 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU264: - cost: 9 - init_stock: 1550 - price: 10 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU265: - cost: 72 - init_stock: 950 - price: 81 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU266: - cost: 248 - init_stock: 4750 - price: 276 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU267: - cost: 110 - init_stock: 2100 - price: 123 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU268: - cost: 57 - init_stock: 1700 - price: 64 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU269: - cost: 391 - init_stock: 500 - price: 435 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU27: - cost: 176 - init_stock: 2850 - price: 196 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU270: - cost: 61 - init_stock: 2900 - price: 68 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU271: - cost: 12 - init_stock: 400 - price: 14 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU272: - cost: 224 - init_stock: 4150 - price: 249 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU273: - cost: 35 - init_stock: 1000 - price: 39 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU274: - cost: 392 - init_stock: 450 - price: 436 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU275: - cost: 55 - init_stock: 4650 - price: 62 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU276: - cost: 252 - init_stock: 3750 - price: 281 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU277: - cost: 397 - init_stock: 4550 - price: 442 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU278: - cost: 261 - init_stock: 4200 - price: 290 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU279: - cost: 157 - init_stock: 2950 - price: 175 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU28: - cost: 54 - init_stock: 1800 - price: 61 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU280: - cost: 400 - init_stock: 4100 - price: 445 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU281: - cost: 44 - init_stock: 350 - price: 49 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU282: - cost: 449 - init_stock: 1550 - price: 499 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU283: - cost: 279 - init_stock: 2100 - price: 310 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU284: - cost: 342 - init_stock: 2100 - price: 381 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU285: - cost: 340 - init_stock: 1750 - price: 378 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU286: - cost: 329 - init_stock: 4450 - price: 366 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU287: - cost: 258 - init_stock: 3350 - price: 287 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU288: - cost: 21 - init_stock: 600 - price: 24 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU289: - cost: 395 - init_stock: 4950 - price: 439 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU29: - cost: 441 - init_stock: 3600 - price: 490 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU290: - cost: 9 - init_stock: 3850 - price: 11 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU291: - cost: 233 - init_stock: 900 - price: 259 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU292: - cost: 350 - init_stock: 5000 - price: 389 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU293: - cost: 221 - init_stock: 2800 - price: 246 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU294: - cost: 68 - init_stock: 650 - price: 76 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU295: - cost: 19 - init_stock: 4300 - price: 22 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU296: - cost: 350 - init_stock: 4750 - price: 389 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU297: - cost: 45 - init_stock: 3200 - price: 51 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU298: - cost: 242 - init_stock: 4450 - price: 269 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU299: - cost: 12 - init_stock: 4450 - price: 14 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU3: - cost: 423 - init_stock: 4400 - price: 470 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU30: - cost: 196 - init_stock: 2250 - price: 218 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU300: - cost: 84 - init_stock: 1100 - price: 94 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU301: - cost: 375 - init_stock: 1600 - price: 417 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU302: - cost: 144 - init_stock: 4600 - price: 161 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU303: - cost: 193 - init_stock: 3950 - price: 215 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU304: - cost: 130 - init_stock: 2900 - price: 145 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU305: - cost: 396 - init_stock: 400 - price: 441 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU306: - cost: 255 - init_stock: 3450 - price: 284 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU307: - cost: 370 - init_stock: 2800 - price: 412 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU308: - cost: 368 - init_stock: 4000 - price: 409 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU309: - cost: 234 - init_stock: 2550 - price: 260 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU31: - cost: 126 - init_stock: 4400 - price: 140 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU310: - cost: 330 - init_stock: 1400 - price: 367 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU311: - cost: 128 - init_stock: 4550 - price: 143 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU312: - cost: 237 - init_stock: 900 - price: 264 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU313: - cost: 170 - init_stock: 4750 - price: 189 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU314: - cost: 286 - init_stock: 750 - price: 318 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU315: - cost: 27 - init_stock: 2850 - price: 31 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU316: - cost: 193 - init_stock: 4300 - price: 215 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU317: - cost: 64 - init_stock: 1550 - price: 72 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU318: - cost: 436 - init_stock: 4500 - price: 485 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU319: - cost: 196 - init_stock: 1200 - price: 218 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU32: - cost: 342 - init_stock: 1600 - price: 380 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU320: - cost: 400 - init_stock: 5000 - price: 445 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU321: - cost: 65 - init_stock: 4400 - price: 73 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU322: - cost: 108 - init_stock: 3850 - price: 120 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU323: - cost: 216 - init_stock: 4750 - price: 241 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU324: - cost: 242 - init_stock: 5000 - price: 269 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU325: - cost: 265 - init_stock: 1200 - price: 295 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU326: - cost: 20 - init_stock: 4150 - price: 23 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU327: - cost: 300 - init_stock: 3300 - price: 334 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU328: - cost: 96 - init_stock: 550 - price: 107 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU329: - cost: 234 - init_stock: 3000 - price: 260 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU33: - cost: 72 - init_stock: 4200 - price: 81 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU330: - cost: 327 - init_stock: 2200 - price: 364 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU331: - cost: 391 - init_stock: 1400 - price: 435 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU332: - cost: 204 - init_stock: 4150 - price: 227 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU333: - cost: 241 - init_stock: 3150 - price: 268 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU334: - cost: 223 - init_stock: 2150 - price: 248 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU335: - cost: 306 - init_stock: 4700 - price: 341 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU336: - cost: 440 - init_stock: 2500 - price: 489 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU337: - cost: 374 - init_stock: 4900 - price: 416 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU338: - cost: 188 - init_stock: 3700 - price: 209 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU339: - cost: 170 - init_stock: 3100 - price: 189 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU34: - cost: 236 - init_stock: 2350 - price: 263 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU340: - cost: 337 - init_stock: 950 - price: 375 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU341: - cost: 149 - init_stock: 2800 - price: 166 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU342: - cost: 159 - init_stock: 2050 - price: 177 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU343: - cost: 430 - init_stock: 4200 - price: 478 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU344: - cost: 441 - init_stock: 4050 - price: 491 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU345: - cost: 244 - init_stock: 1650 - price: 272 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU346: - cost: 180 - init_stock: 3550 - price: 200 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU347: - cost: 162 - init_stock: 1550 - price: 180 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU348: - cost: 266 - init_stock: 1700 - price: 296 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU349: - cost: 72 - init_stock: 4550 - price: 81 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU35: - cost: 216 - init_stock: 2950 - price: 240 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU350: - cost: 95 - init_stock: 600 - price: 106 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU351: - cost: 91 - init_stock: 3700 - price: 102 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU352: - cost: 246 - init_stock: 800 - price: 274 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU353: - cost: 132 - init_stock: 3400 - price: 147 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU354: - cost: 369 - init_stock: 3900 - price: 410 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU355: - cost: 375 - init_stock: 2800 - price: 417 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU356: - cost: 172 - init_stock: 1750 - price: 192 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU357: - cost: 72 - init_stock: 5000 - price: 80 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU358: - cost: 117 - init_stock: 1600 - price: 130 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU359: - cost: 294 - init_stock: 1600 - price: 327 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU36: - cost: 120 - init_stock: 4600 - price: 134 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU360: - cost: 410 - init_stock: 2750 - price: 456 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU361: - cost: 164 - init_stock: 3350 - price: 183 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU362: - cost: 316 - init_stock: 2050 - price: 352 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU363: - cost: 28 - init_stock: 250 - price: 32 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU364: - cost: 297 - init_stock: 3250 - price: 331 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU365: - cost: 51 - init_stock: 4100 - price: 57 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU366: - cost: 165 - init_stock: 1200 - price: 184 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU367: - cost: 346 - init_stock: 4000 - price: 385 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU368: - cost: 351 - init_stock: 1550 - price: 391 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU369: - cost: 414 - init_stock: 1600 - price: 460 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU37: - cost: 255 - init_stock: 3450 - price: 284 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU370: - cost: 92 - init_stock: 1400 - price: 103 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU371: - cost: 162 - init_stock: 4200 - price: 181 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU372: - cost: 439 - init_stock: 1000 - price: 488 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU373: - cost: 139 - init_stock: 1350 - price: 155 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU374: - cost: 11 - init_stock: 4750 - price: 13 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU375: - cost: 389 - init_stock: 3100 - price: 433 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU376: - cost: 27 - init_stock: 4050 - price: 30 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU377: - cost: 90 - init_stock: 1800 - price: 100 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU378: - cost: 440 - init_stock: 450 - price: 489 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU379: - cost: 415 - init_stock: 2050 - price: 462 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU38: - cost: 293 - init_stock: 4050 - price: 326 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU380: - cost: 413 - init_stock: 650 - price: 459 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU381: - cost: 248 - init_stock: 3800 - price: 276 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU382: - cost: 424 - init_stock: 850 - price: 472 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU383: - cost: 224 - init_stock: 1650 - price: 249 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU384: - cost: 33 - init_stock: 1200 - price: 37 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU385: - cost: 247 - init_stock: 4000 - price: 275 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU386: - cost: 321 - init_stock: 2550 - price: 357 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU387: - cost: 423 - init_stock: 1850 - price: 471 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU388: - cost: 237 - init_stock: 3350 - price: 264 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU389: - cost: 11 - init_stock: 2450 - price: 13 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU39: - cost: 342 - init_stock: 1100 - price: 381 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU390: - cost: 131 - init_stock: 250 - price: 146 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU391: - cost: 328 - init_stock: 4600 - price: 365 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU392: - cost: 440 - init_stock: 2300 - price: 489 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU393: - cost: 419 - init_stock: 550 - price: 466 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU394: - cost: 167 - init_stock: 850 - price: 186 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU395: - cost: 291 - init_stock: 1500 - price: 324 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU396: - cost: 352 - init_stock: 3400 - price: 392 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU397: - cost: 231 - init_stock: 1150 - price: 257 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU398: - cost: 409 - init_stock: 3550 - price: 455 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU399: - cost: 13 - init_stock: 3900 - price: 15 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU4: - cost: 260 - init_stock: 4650 - price: 289 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU40: - cost: 194 - init_stock: 400 - price: 216 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU400: - cost: 319 - init_stock: 4750 - price: 355 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU401: - cost: 135 - init_stock: 400 - price: 150 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU402: - cost: 229 - init_stock: 4600 - price: 255 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU403: - cost: 299 - init_stock: 4150 - price: 333 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU404: - cost: 84 - init_stock: 1800 - price: 94 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU405: - cost: 217 - init_stock: 2900 - price: 242 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU406: - cost: 361 - init_stock: 2750 - price: 402 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU407: - cost: 311 - init_stock: 1950 - price: 346 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU408: - cost: 246 - init_stock: 4100 - price: 274 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU409: - cost: 241 - init_stock: 4700 - price: 268 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU41: - cost: 280 - init_stock: 3400 - price: 312 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU410: - cost: 386 - init_stock: 2400 - price: 429 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU411: - cost: 9 - init_stock: 1500 - price: 11 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU412: - cost: 81 - init_stock: 2950 - price: 91 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU413: - cost: 398 - init_stock: 550 - price: 443 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU414: - cost: 60 - init_stock: 3850 - price: 67 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU415: - cost: 312 - init_stock: 500 - price: 347 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU416: - cost: 26 - init_stock: 2550 - price: 29 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU417: - cost: 439 - init_stock: 3400 - price: 488 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU418: - cost: 388 - init_stock: 4850 - price: 432 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU419: - cost: 34 - init_stock: 650 - price: 38 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU42: - cost: 247 - init_stock: 4150 - price: 275 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU420: - cost: 284 - init_stock: 350 - price: 316 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU421: - cost: 260 - init_stock: 4000 - price: 289 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU422: - cost: 74 - init_stock: 1700 - price: 83 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU423: - cost: 156 - init_stock: 600 - price: 174 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU424: - cost: 445 - init_stock: 2050 - price: 495 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU425: - cost: 127 - init_stock: 2200 - price: 142 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU426: - cost: 210 - init_stock: 800 - price: 234 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU427: - cost: 145 - init_stock: 4200 - price: 162 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU428: - cost: 380 - init_stock: 3100 - price: 423 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU429: - cost: 289 - init_stock: 4950 - price: 322 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU43: - cost: 385 - init_stock: 2700 - price: 428 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU430: - cost: 260 - init_stock: 2250 - price: 289 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU431: - cost: 26 - init_stock: 650 - price: 29 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU432: - cost: 314 - init_stock: 1150 - price: 349 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU433: - cost: 342 - init_stock: 1300 - price: 380 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU434: - cost: 335 - init_stock: 2050 - price: 373 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU435: - cost: 41 - init_stock: 2500 - price: 46 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU436: - cost: 344 - init_stock: 1900 - price: 383 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU437: - cost: 68 - init_stock: 4100 - price: 76 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU438: - cost: 389 - init_stock: 1350 - price: 433 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU439: - cost: 36 - init_stock: 2850 - price: 40 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU44: - cost: 229 - init_stock: 950 - price: 255 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU440: - cost: 111 - init_stock: 2350 - price: 124 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU441: - cost: 205 - init_stock: 2700 - price: 228 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU442: - cost: 17 - init_stock: 2050 - price: 19 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU443: - cost: 401 - init_stock: 3100 - price: 446 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU444: - cost: 190 - init_stock: 2700 - price: 212 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU445: - cost: 356 - init_stock: 1000 - price: 396 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU446: - cost: 113 - init_stock: 3200 - price: 126 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU447: - cost: 18 - init_stock: 2000 - price: 20 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU448: - cost: 328 - init_stock: 3300 - price: 365 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU449: - cost: 295 - init_stock: 400 - price: 328 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU45: - cost: 142 - init_stock: 1400 - price: 158 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU450: - cost: 54 - init_stock: 300 - price: 61 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU451: - cost: 346 - init_stock: 1800 - price: 385 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU452: - cost: 405 - init_stock: 3650 - price: 450 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU453: - cost: 117 - init_stock: 4050 - price: 131 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU454: - cost: 363 - init_stock: 2950 - price: 404 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU455: - cost: 300 - init_stock: 3700 - price: 334 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU456: - cost: 113 - init_stock: 2050 - price: 126 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU457: - cost: 211 - init_stock: 2800 - price: 235 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU458: - cost: 253 - init_stock: 2350 - price: 282 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU459: - cost: 162 - init_stock: 1100 - price: 180 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU46: - cost: 306 - init_stock: 1000 - price: 341 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU460: - cost: 243 - init_stock: 250 - price: 271 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU461: - cost: 117 - init_stock: 1350 - price: 131 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU462: - cost: 88 - init_stock: 450 - price: 98 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU463: - cost: 105 - init_stock: 1150 - price: 117 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU464: - cost: 265 - init_stock: 1200 - price: 295 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU465: - cost: 257 - init_stock: 3850 - price: 286 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU466: - cost: 250 - init_stock: 2650 - price: 278 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU467: - cost: 264 - init_stock: 400 - price: 294 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU468: - cost: 36 - init_stock: 500 - price: 40 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU469: - cost: 432 - init_stock: 5000 - price: 481 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU47: - cost: 414 - init_stock: 550 - price: 460 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU470: - cost: 423 - init_stock: 3050 - price: 470 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU471: - cost: 88 - init_stock: 1550 - price: 98 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU472: - cost: 19 - init_stock: 3950 - price: 22 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU473: - cost: 372 - init_stock: 4100 - price: 414 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU474: - cost: 85 - init_stock: 2050 - price: 95 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU475: - cost: 351 - init_stock: 1800 - price: 390 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU476: - cost: 421 - init_stock: 4300 - price: 468 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU477: - cost: 81 - init_stock: 2000 - price: 91 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU478: - cost: 217 - init_stock: 2550 - price: 242 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU479: - cost: 142 - init_stock: 2500 - price: 158 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU48: - cost: 246 - init_stock: 600 - price: 274 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU480: - cost: 381 - init_stock: 3400 - price: 424 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU481: - cost: 179 - init_stock: 2450 - price: 199 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU482: - cost: 195 - init_stock: 1300 - price: 217 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU483: - cost: 108 - init_stock: 600 - price: 120 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU484: - cost: 219 - init_stock: 4000 - price: 244 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU485: - cost: 239 - init_stock: 2850 - price: 266 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU486: - cost: 218 - init_stock: 600 - price: 243 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU487: - cost: 247 - init_stock: 3500 - price: 275 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU488: - cost: 380 - init_stock: 650 - price: 423 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU489: - cost: 216 - init_stock: 1900 - price: 241 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU49: - cost: 165 - init_stock: 3600 - price: 184 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU490: - cost: 256 - init_stock: 800 - price: 285 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU491: - cost: 300 - init_stock: 3900 - price: 334 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU492: - cost: 333 - init_stock: 3400 - price: 371 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU493: - cost: 361 - init_stock: 4750 - price: 402 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU494: - cost: 75 - init_stock: 4350 - price: 84 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU495: - cost: 312 - init_stock: 1850 - price: 347 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU496: - cost: 159 - init_stock: 2650 - price: 177 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU497: - cost: 234 - init_stock: 1550 - price: 260 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU498: - cost: 402 - init_stock: 4700 - price: 447 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU499: - cost: 382 - init_stock: 3900 - price: 425 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU5: - cost: 430 - init_stock: 3700 - price: 478 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU50: - cost: 384 - init_stock: 900 - price: 427 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU500: - cost: 371 - init_stock: 500 - price: 413 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU501: - cost: 66 - init_stock: 3900 - price: 74 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU502: - cost: 369 - init_stock: 450 - price: 411 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU503: - cost: 413 - init_stock: 2200 - price: 459 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU504: - cost: 292 - init_stock: 2000 - price: 325 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU505: - cost: 291 - init_stock: 3600 - price: 324 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU506: - cost: 429 - init_stock: 400 - price: 477 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU507: - cost: 32 - init_stock: 3550 - price: 36 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU508: - cost: 351 - init_stock: 2850 - price: 390 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU509: - cost: 417 - init_stock: 3550 - price: 464 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU51: - cost: 390 - init_stock: 2450 - price: 434 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU510: - cost: 209 - init_stock: 2750 - price: 233 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU511: - cost: 285 - init_stock: 1350 - price: 317 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU512: - cost: 364 - init_stock: 3600 - price: 405 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU513: - cost: 29 - init_stock: 2450 - price: 33 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU514: - cost: 396 - init_stock: 1200 - price: 441 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU515: - cost: 410 - init_stock: 1750 - price: 456 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU516: - cost: 388 - init_stock: 600 - price: 432 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU517: - cost: 200 - init_stock: 3950 - price: 223 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU518: - cost: 139 - init_stock: 3850 - price: 155 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU519: - cost: 248 - init_stock: 2350 - price: 276 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU52: - cost: 306 - init_stock: 4950 - price: 340 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU520: - cost: 16 - init_stock: 1450 - price: 18 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU521: - cost: 320 - init_stock: 4550 - price: 356 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU522: - cost: 350 - init_stock: 1500 - price: 389 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU523: - cost: 323 - init_stock: 1250 - price: 359 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU524: - cost: 191 - init_stock: 1950 - price: 213 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU525: - cost: 117 - init_stock: 550 - price: 131 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU526: - cost: 213 - init_stock: 1800 - price: 237 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU527: - cost: 144 - init_stock: 600 - price: 161 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU528: - cost: 345 - init_stock: 2450 - price: 384 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU529: - cost: 288 - init_stock: 650 - price: 321 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU53: - cost: 18 - init_stock: 2100 - price: 20 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU530: - cost: 269 - init_stock: 4800 - price: 299 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU531: - cost: 301 - init_stock: 3000 - price: 335 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU532: - cost: 139 - init_stock: 4400 - price: 155 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU533: - cost: 399 - init_stock: 2050 - price: 444 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU534: - cost: 131 - init_stock: 4850 - price: 146 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU535: - cost: 345 - init_stock: 1750 - price: 384 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU536: - cost: 77 - init_stock: 5000 - price: 86 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU537: - cost: 155 - init_stock: 2350 - price: 173 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU538: - cost: 36 - init_stock: 2350 - price: 40 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU539: - cost: 351 - init_stock: 500 - price: 390 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU54: - cost: 145 - init_stock: 3850 - price: 162 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU540: - cost: 261 - init_stock: 2400 - price: 290 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU541: - cost: 172 - init_stock: 1800 - price: 192 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU542: - cost: 86 - init_stock: 2300 - price: 96 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU543: - cost: 177 - init_stock: 2000 - price: 197 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU544: - cost: 367 - init_stock: 3200 - price: 408 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU545: - cost: 387 - init_stock: 3250 - price: 431 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU546: - cost: 79 - init_stock: 850 - price: 88 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU547: - cost: 408 - init_stock: 4800 - price: 454 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU548: - cost: 373 - init_stock: 4550 - price: 415 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU549: - cost: 276 - init_stock: 2900 - price: 307 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU55: - cost: 95 - init_stock: 1050 - price: 106 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU550: - cost: 401 - init_stock: 950 - price: 446 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU551: - cost: 353 - init_stock: 3000 - price: 393 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU552: - cost: 102 - init_stock: 4200 - price: 114 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU553: - cost: 63 - init_stock: 1750 - price: 71 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU554: - cost: 282 - init_stock: 650 - price: 314 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU555: - cost: 442 - init_stock: 850 - price: 492 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU556: - cost: 86 - init_stock: 3300 - price: 96 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU557: - cost: 272 - init_stock: 3600 - price: 303 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU558: - cost: 387 - init_stock: 1050 - price: 430 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU559: - cost: 199 - init_stock: 2950 - price: 222 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU56: - cost: 391 - init_stock: 1150 - price: 435 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU560: - cost: 131 - init_stock: 4450 - price: 146 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU561: - cost: 142 - init_stock: 2050 - price: 158 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU562: - cost: 45 - init_stock: 3700 - price: 51 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU563: - cost: 376 - init_stock: 1250 - price: 418 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU564: - cost: 397 - init_stock: 1200 - price: 442 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU565: - cost: 279 - init_stock: 4350 - price: 310 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU566: - cost: 30 - init_stock: 300 - price: 34 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU567: - cost: 108 - init_stock: 2150 - price: 120 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU568: - cost: 87 - init_stock: 400 - price: 97 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU569: - cost: 256 - init_stock: 4700 - price: 285 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU57: - cost: 360 - init_stock: 1500 - price: 400 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU570: - cost: 144 - init_stock: 2150 - price: 161 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU571: - cost: 45 - init_stock: 4100 - price: 50 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU572: - cost: 440 - init_stock: 2400 - price: 489 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU573: - cost: 246 - init_stock: 1850 - price: 274 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU574: - cost: 321 - init_stock: 1600 - price: 357 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU575: - cost: 238 - init_stock: 4900 - price: 265 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU576: - cost: 123 - init_stock: 3350 - price: 137 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU577: - cost: 378 - init_stock: 3200 - price: 420 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU578: - cost: 66 - init_stock: 750 - price: 74 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU579: - cost: 279 - init_stock: 2800 - price: 310 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU58: - cost: 195 - init_stock: 4050 - price: 217 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU580: - cost: 137 - init_stock: 4950 - price: 153 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU581: - cost: 444 - init_stock: 2550 - price: 494 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU582: - cost: 428 - init_stock: 650 - price: 476 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU583: - cost: 94 - init_stock: 550 - price: 105 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU584: - cost: 328 - init_stock: 2350 - price: 365 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU585: - cost: 165 - init_stock: 2700 - price: 184 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU586: - cost: 423 - init_stock: 4700 - price: 471 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU587: - cost: 177 - init_stock: 4000 - price: 197 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU588: - cost: 13 - init_stock: 1350 - price: 15 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU589: - cost: 166 - init_stock: 1250 - price: 185 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU59: - cost: 35 - init_stock: 2400 - price: 39 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU590: - cost: 313 - init_stock: 3100 - price: 348 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU591: - cost: 146 - init_stock: 1850 - price: 163 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU592: - cost: 274 - init_stock: 4600 - price: 305 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU593: - cost: 431 - init_stock: 2550 - price: 479 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU594: - cost: 430 - init_stock: 2550 - price: 478 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU595: - cost: 31 - init_stock: 2750 - price: 35 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU596: - cost: 404 - init_stock: 1350 - price: 449 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU597: - cost: 18 - init_stock: 4800 - price: 21 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU598: - cost: 44 - init_stock: 1950 - price: 49 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU599: - cost: 180 - init_stock: 3800 - price: 201 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU6: - cost: 424 - init_stock: 2050 - price: 472 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU60: - cost: 21 - init_stock: 4250 - price: 24 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU600: - cost: 288 - init_stock: 2750 - price: 321 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU601: - cost: 166 - init_stock: 500 - price: 185 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU602: - cost: 162 - init_stock: 4700 - price: 180 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU603: - cost: 295 - init_stock: 4750 - price: 328 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU604: - cost: 128 - init_stock: 4800 - price: 143 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU605: - cost: 303 - init_stock: 1100 - price: 337 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU606: - cost: 149 - init_stock: 1250 - price: 166 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU607: - cost: 257 - init_stock: 4050 - price: 286 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU608: - cost: 16 - init_stock: 2050 - price: 18 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU609: - cost: 297 - init_stock: 3900 - price: 330 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU61: - cost: 379 - init_stock: 500 - price: 422 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU610: - cost: 440 - init_stock: 1900 - price: 489 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU611: - cost: 341 - init_stock: 1750 - price: 379 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU612: - cost: 25 - init_stock: 1900 - price: 28 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU613: - cost: 272 - init_stock: 2450 - price: 303 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU614: - cost: 237 - init_stock: 1850 - price: 264 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU615: - cost: 303 - init_stock: 2050 - price: 337 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU616: - cost: 278 - init_stock: 3050 - price: 309 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU617: - cost: 213 - init_stock: 2600 - price: 237 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU618: - cost: 128 - init_stock: 2050 - price: 143 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU619: - cost: 54 - init_stock: 1800 - price: 60 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU62: - cost: 82 - init_stock: 2050 - price: 92 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU620: - cost: 430 - init_stock: 2450 - price: 478 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU621: - cost: 194 - init_stock: 1500 - price: 216 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU622: - cost: 243 - init_stock: 1200 - price: 270 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU623: - cost: 403 - init_stock: 1900 - price: 448 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU624: - cost: 386 - init_stock: 2750 - price: 429 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU625: - cost: 212 - init_stock: 3900 - price: 236 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU626: - cost: 121 - init_stock: 1600 - price: 135 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU627: - cost: 70 - init_stock: 1950 - price: 78 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU628: - cost: 102 - init_stock: 4400 - price: 114 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU629: - cost: 351 - init_stock: 2700 - price: 390 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU63: - cost: 306 - init_stock: 3450 - price: 341 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU630: - cost: 450 - init_stock: 4200 - price: 500 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU631: - cost: 347 - init_stock: 3650 - price: 386 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU632: - cost: 64 - init_stock: 1700 - price: 72 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU633: - cost: 275 - init_stock: 2900 - price: 306 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU634: - cost: 122 - init_stock: 4200 - price: 136 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU635: - cost: 416 - init_stock: 2250 - price: 463 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU636: - cost: 218 - init_stock: 2850 - price: 243 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU637: - cost: 448 - init_stock: 1500 - price: 498 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU638: - cost: 83 - init_stock: 3300 - price: 93 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU639: - cost: 428 - init_stock: 500 - price: 476 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU64: - cost: 135 - init_stock: 4800 - price: 151 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU640: - cost: 315 - init_stock: 2800 - price: 350 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU641: - cost: 390 - init_stock: 1800 - price: 434 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU642: - cost: 314 - init_stock: 3600 - price: 349 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU643: - cost: 15 - init_stock: 1350 - price: 17 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU644: - cost: 24 - init_stock: 4650 - price: 27 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU645: - cost: 128 - init_stock: 450 - price: 143 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU646: - cost: 288 - init_stock: 4350 - price: 320 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU647: - cost: 266 - init_stock: 1300 - price: 296 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU648: - cost: 225 - init_stock: 2400 - price: 251 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU649: - cost: 153 - init_stock: 4300 - price: 171 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU65: - cost: 43 - init_stock: 4000 - price: 48 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU650: - cost: 365 - init_stock: 2350 - price: 406 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU651: - cost: 188 - init_stock: 3550 - price: 209 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU652: - cost: 150 - init_stock: 3400 - price: 167 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU653: - cost: 109 - init_stock: 300 - price: 122 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU654: - cost: 309 - init_stock: 2300 - price: 344 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU655: - cost: 343 - init_stock: 4450 - price: 382 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU656: - cost: 206 - init_stock: 4750 - price: 229 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU657: - cost: 426 - init_stock: 3850 - price: 474 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU658: - cost: 276 - init_stock: 2150 - price: 307 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU659: - cost: 352 - init_stock: 1250 - price: 392 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU66: - cost: 442 - init_stock: 2500 - price: 492 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU660: - cost: 185 - init_stock: 600 - price: 206 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU661: - cost: 90 - init_stock: 5000 - price: 100 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU662: - cost: 254 - init_stock: 300 - price: 283 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU663: - cost: 374 - init_stock: 2200 - price: 416 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU664: - cost: 83 - init_stock: 3500 - price: 93 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU665: - cost: 373 - init_stock: 4950 - price: 415 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU666: - cost: 340 - init_stock: 250 - price: 378 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU667: - cost: 102 - init_stock: 3900 - price: 114 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU668: - cost: 103 - init_stock: 800 - price: 115 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU669: - cost: 372 - init_stock: 1000 - price: 414 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU67: - cost: 14 - init_stock: 3100 - price: 16 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU670: - cost: 153 - init_stock: 3300 - price: 171 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU671: - cost: 146 - init_stock: 1950 - price: 163 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU672: - cost: 60 - init_stock: 550 - price: 67 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU673: - cost: 393 - init_stock: 3700 - price: 437 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU674: - cost: 45 - init_stock: 950 - price: 50 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU675: - cost: 431 - init_stock: 1000 - price: 479 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU676: - cost: 414 - init_stock: 2450 - price: 460 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU677: - cost: 352 - init_stock: 4700 - price: 392 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU678: - cost: 128 - init_stock: 3350 - price: 143 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU679: - cost: 117 - init_stock: 4100 - price: 131 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU68: - cost: 97 - init_stock: 2300 - price: 108 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU680: - cost: 392 - init_stock: 1200 - price: 436 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU681: - cost: 304 - init_stock: 650 - price: 338 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU682: - cost: 131 - init_stock: 800 - price: 146 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU683: - cost: 437 - init_stock: 1800 - price: 486 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU684: - cost: 333 - init_stock: 3600 - price: 370 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU685: - cost: 62 - init_stock: 3850 - price: 69 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU686: - cost: 50 - init_stock: 1350 - price: 56 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU687: - cost: 277 - init_stock: 2400 - price: 308 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU688: - cost: 180 - init_stock: 2650 - price: 200 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU689: - cost: 32 - init_stock: 800 - price: 36 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU69: - cost: 296 - init_stock: 1400 - price: 329 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU690: - cost: 254 - init_stock: 350 - price: 283 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU691: - cost: 379 - init_stock: 4900 - price: 422 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU692: - cost: 411 - init_stock: 3250 - price: 457 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU693: - cost: 315 - init_stock: 2400 - price: 351 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU694: - cost: 396 - init_stock: 1600 - price: 441 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU695: - cost: 309 - init_stock: 4900 - price: 344 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU696: - cost: 340 - init_stock: 1750 - price: 378 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU697: - cost: 434 - init_stock: 4600 - price: 483 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU698: - cost: 47 - init_stock: 2500 - price: 53 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU699: - cost: 238 - init_stock: 3200 - price: 265 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU7: - cost: 350 - init_stock: 600 - price: 389 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU70: - cost: 126 - init_stock: 1900 - price: 140 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU700: - cost: 109 - init_stock: 1000 - price: 122 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU701: - cost: 354 - init_stock: 1900 - price: 394 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU702: - cost: 70 - init_stock: 1350 - price: 78 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU703: - cost: 424 - init_stock: 3550 - price: 472 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU704: - cost: 345 - init_stock: 2200 - price: 384 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU705: - cost: 437 - init_stock: 2800 - price: 486 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU706: - cost: 199 - init_stock: 1850 - price: 222 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU707: - cost: 175 - init_stock: 1850 - price: 195 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU708: - cost: 117 - init_stock: 4950 - price: 131 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU709: - cost: 123 - init_stock: 1750 - price: 137 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU71: - cost: 342 - init_stock: 1850 - price: 381 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU710: - cost: 167 - init_stock: 700 - price: 186 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU711: - cost: 171 - init_stock: 3050 - price: 190 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU712: - cost: 345 - init_stock: 1800 - price: 384 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU713: - cost: 206 - init_stock: 3450 - price: 229 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU714: - cost: 327 - init_stock: 4500 - price: 364 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU715: - cost: 211 - init_stock: 450 - price: 235 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU716: - cost: 97 - init_stock: 4450 - price: 108 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU717: - cost: 10 - init_stock: 600 - price: 12 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU718: - cost: 357 - init_stock: 1100 - price: 397 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU719: - cost: 18 - init_stock: 1850 - price: 21 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU72: - cost: 205 - init_stock: 4850 - price: 228 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU720: - cost: 108 - init_stock: 2400 - price: 120 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU721: - cost: 238 - init_stock: 4150 - price: 265 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU722: - cost: 317 - init_stock: 2900 - price: 353 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU723: - cost: 129 - init_stock: 4250 - price: 144 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU724: - cost: 108 - init_stock: 3550 - price: 120 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU725: - cost: 37 - init_stock: 4450 - price: 42 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU726: - cost: 118 - init_stock: 4350 - price: 132 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU727: - cost: 92 - init_stock: 1750 - price: 103 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU728: - cost: 26 - init_stock: 1700 - price: 29 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU729: - cost: 225 - init_stock: 4850 - price: 251 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU73: - cost: 300 - init_stock: 3650 - price: 334 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU730: - cost: 63 - init_stock: 3200 - price: 71 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU731: - cost: 432 - init_stock: 4600 - price: 480 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU732: - cost: 207 - init_stock: 3350 - price: 231 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU733: - cost: 220 - init_stock: 1400 - price: 245 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU734: - cost: 105 - init_stock: 2900 - price: 117 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU735: - cost: 177 - init_stock: 1300 - price: 197 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU736: - cost: 198 - init_stock: 3100 - price: 221 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU737: - cost: 234 - init_stock: 4250 - price: 260 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU738: - cost: 440 - init_stock: 2300 - price: 489 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU739: - cost: 405 - init_stock: 1900 - price: 451 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU74: - cost: 326 - init_stock: 500 - price: 363 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU740: - cost: 225 - init_stock: 250 - price: 250 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU741: - cost: 125 - init_stock: 850 - price: 139 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU742: - cost: 156 - init_stock: 4450 - price: 174 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU743: - cost: 71 - init_stock: 4100 - price: 79 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU744: - cost: 273 - init_stock: 1950 - price: 304 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU745: - cost: 94 - init_stock: 4650 - price: 105 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU746: - cost: 357 - init_stock: 4700 - price: 397 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU747: - cost: 383 - init_stock: 1800 - price: 426 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU748: - cost: 152 - init_stock: 2350 - price: 169 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU749: - cost: 389 - init_stock: 2100 - price: 433 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU75: - cost: 161 - init_stock: 3250 - price: 179 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU750: - cost: 335 - init_stock: 3400 - price: 373 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU751: - cost: 176 - init_stock: 1700 - price: 196 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU752: - cost: 60 - init_stock: 500 - price: 67 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU753: - cost: 145 - init_stock: 3050 - price: 162 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU754: - cost: 185 - init_stock: 3200 - price: 206 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU755: - cost: 112 - init_stock: 2150 - price: 125 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU756: - cost: 193 - init_stock: 4100 - price: 215 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU757: - cost: 236 - init_stock: 1100 - price: 263 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU758: - cost: 63 - init_stock: 4350 - price: 70 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU759: - cost: 279 - init_stock: 3950 - price: 310 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU76: - cost: 417 - init_stock: 4200 - price: 464 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU760: - cost: 438 - init_stock: 1100 - price: 487 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU761: - cost: 259 - init_stock: 1550 - price: 288 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU762: - cost: 336 - init_stock: 3100 - price: 374 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU763: - cost: 108 - init_stock: 300 - price: 121 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU764: - cost: 444 - init_stock: 1900 - price: 494 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU765: - cost: 413 - init_stock: 4450 - price: 459 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU766: - cost: 224 - init_stock: 2700 - price: 249 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU767: - cost: 220 - init_stock: 4400 - price: 245 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU768: - cost: 386 - init_stock: 4250 - price: 429 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU769: - cost: 291 - init_stock: 2400 - price: 324 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU77: - cost: 252 - init_stock: 1400 - price: 281 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU770: - cost: 325 - init_stock: 3800 - price: 362 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU771: - cost: 209 - init_stock: 1350 - price: 233 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU772: - cost: 136 - init_stock: 800 - price: 152 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU773: - cost: 303 - init_stock: 4800 - price: 337 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU774: - cost: 208 - init_stock: 450 - price: 232 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU775: - cost: 353 - init_stock: 600 - price: 393 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU776: - cost: 307 - init_stock: 2400 - price: 342 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU777: - cost: 235 - init_stock: 2950 - price: 262 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU778: - cost: 294 - init_stock: 3700 - price: 327 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU779: - cost: 98 - init_stock: 2200 - price: 109 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU78: - cost: 437 - init_stock: 1850 - price: 486 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU780: - cost: 137 - init_stock: 1250 - price: 153 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU781: - cost: 113 - init_stock: 350 - price: 126 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU782: - cost: 232 - init_stock: 4600 - price: 258 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU783: - cost: 192 - init_stock: 3500 - price: 214 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU784: - cost: 383 - init_stock: 1750 - price: 426 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU785: - cost: 437 - init_stock: 1450 - price: 486 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU786: - cost: 98 - init_stock: 2600 - price: 109 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU787: - cost: 66 - init_stock: 1050 - price: 74 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU788: - cost: 22 - init_stock: 2100 - price: 25 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU789: - cost: 238 - init_stock: 1950 - price: 265 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU79: - cost: 437 - init_stock: 2100 - price: 486 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU790: - cost: 199 - init_stock: 1400 - price: 222 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU791: - cost: 445 - init_stock: 2700 - price: 495 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU792: - cost: 369 - init_stock: 1600 - price: 410 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU793: - cost: 397 - init_stock: 4000 - price: 442 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU794: - cost: 199 - init_stock: 300 - price: 222 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU795: - cost: 251 - init_stock: 2900 - price: 279 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU796: - cost: 109 - init_stock: 1300 - price: 122 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU797: - cost: 96 - init_stock: 5000 - price: 107 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU798: - cost: 344 - init_stock: 2500 - price: 383 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU799: - cost: 107 - init_stock: 1750 - price: 119 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU8: - cost: 380 - init_stock: 4250 - price: 423 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU80: - cost: 291 - init_stock: 1800 - price: 324 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU800: - cost: 284 - init_stock: 4450 - price: 316 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU801: - cost: 58 - init_stock: 4200 - price: 65 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU802: - cost: 119 - init_stock: 4700 - price: 133 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU803: - cost: 188 - init_stock: 1750 - price: 209 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU804: - cost: 245 - init_stock: 4250 - price: 273 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU805: - cost: 273 - init_stock: 1900 - price: 304 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU806: - cost: 208 - init_stock: 450 - price: 232 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU807: - cost: 152 - init_stock: 4350 - price: 169 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU808: - cost: 315 - init_stock: 450 - price: 350 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU809: - cost: 76 - init_stock: 4850 - price: 85 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU81: - cost: 287 - init_stock: 3000 - price: 319 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU810: - cost: 45 - init_stock: 2550 - price: 51 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU811: - cost: 47 - init_stock: 2700 - price: 53 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU812: - cost: 126 - init_stock: 2850 - price: 141 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU813: - cost: 327 - init_stock: 1950 - price: 364 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU814: - cost: 71 - init_stock: 4800 - price: 79 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU815: - cost: 160 - init_stock: 800 - price: 178 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU816: - cost: 316 - init_stock: 3300 - price: 352 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU817: - cost: 107 - init_stock: 850 - price: 119 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU818: - cost: 99 - init_stock: 650 - price: 110 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU819: - cost: 21 - init_stock: 250 - price: 24 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU82: - cost: 371 - init_stock: 3500 - price: 413 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU820: - cost: 383 - init_stock: 2600 - price: 426 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU821: - cost: 206 - init_stock: 4600 - price: 229 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU822: - cost: 253 - init_stock: 600 - price: 282 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU823: - cost: 198 - init_stock: 4550 - price: 221 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU824: - cost: 286 - init_stock: 1800 - price: 318 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU825: - cost: 258 - init_stock: 4650 - price: 287 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU826: - cost: 250 - init_stock: 2400 - price: 278 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU827: - cost: 280 - init_stock: 3150 - price: 312 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU828: - cost: 126 - init_stock: 4650 - price: 140 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU829: - cost: 373 - init_stock: 550 - price: 415 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU83: - cost: 294 - init_stock: 3850 - price: 327 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU830: - cost: 107 - init_stock: 1150 - price: 119 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU831: - cost: 338 - init_stock: 3500 - price: 376 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU832: - cost: 260 - init_stock: 2300 - price: 289 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU833: - cost: 164 - init_stock: 800 - price: 183 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU834: - cost: 358 - init_stock: 3300 - price: 398 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU835: - cost: 366 - init_stock: 3700 - price: 407 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU836: - cost: 270 - init_stock: 2050 - price: 301 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU837: - cost: 191 - init_stock: 1300 - price: 213 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU838: - cost: 20 - init_stock: 2100 - price: 23 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU839: - cost: 318 - init_stock: 2650 - price: 354 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU84: - cost: 195 - init_stock: 1600 - price: 217 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU840: - cost: 17 - init_stock: 4350 - price: 19 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU841: - cost: 325 - init_stock: 4700 - price: 362 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU842: - cost: 65 - init_stock: 4550 - price: 73 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU843: - cost: 152 - init_stock: 2150 - price: 169 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU844: - cost: 292 - init_stock: 1800 - price: 325 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU845: - cost: 146 - init_stock: 750 - price: 163 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU846: - cost: 165 - init_stock: 4900 - price: 184 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU847: - cost: 396 - init_stock: 1700 - price: 441 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU848: - cost: 405 - init_stock: 1250 - price: 450 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU849: - cost: 431 - init_stock: 1750 - price: 479 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU85: - cost: 44 - init_stock: 3800 - price: 49 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU850: - cost: 94 - init_stock: 3650 - price: 105 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU851: - cost: 329 - init_stock: 3850 - price: 366 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU852: - cost: 352 - init_stock: 3900 - price: 392 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU853: - cost: 84 - init_stock: 500 - price: 94 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU854: - cost: 99 - init_stock: 3250 - price: 110 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU855: - cost: 228 - init_stock: 2050 - price: 254 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU856: - cost: 391 - init_stock: 1450 - price: 435 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU857: - cost: 90 - init_stock: 1200 - price: 101 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU858: - cost: 438 - init_stock: 1400 - price: 487 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU859: - cost: 446 - init_stock: 4650 - price: 496 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU86: - cost: 53 - init_stock: 3750 - price: 59 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU860: - cost: 315 - init_stock: 5000 - price: 350 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU861: - cost: 265 - init_stock: 1250 - price: 295 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU862: - cost: 51 - init_stock: 3650 - price: 57 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU863: - cost: 279 - init_stock: 2850 - price: 311 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU864: - cost: 86 - init_stock: 3200 - price: 96 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU865: - cost: 96 - init_stock: 2900 - price: 107 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU866: - cost: 357 - init_stock: 1050 - price: 397 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU867: - cost: 430 - init_stock: 3200 - price: 478 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU868: - cost: 308 - init_stock: 1050 - price: 343 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU869: - cost: 151 - init_stock: 3050 - price: 168 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU87: - cost: 83 - init_stock: 3550 - price: 93 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU870: - cost: 231 - init_stock: 3150 - price: 257 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU871: - cost: 61 - init_stock: 1000 - price: 68 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU872: - cost: 427 - init_stock: 3550 - price: 475 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU873: - cost: 349 - init_stock: 3250 - price: 388 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU874: - cost: 240 - init_stock: 300 - price: 267 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU875: - cost: 298 - init_stock: 1750 - price: 332 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU876: - cost: 352 - init_stock: 2000 - price: 392 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU877: - cost: 336 - init_stock: 4550 - price: 374 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU878: - cost: 267 - init_stock: 1450 - price: 297 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU879: - cost: 447 - init_stock: 3900 - price: 497 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU88: - cost: 17 - init_stock: 2100 - price: 19 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU880: - cost: 179 - init_stock: 2350 - price: 199 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU881: - cost: 295 - init_stock: 3000 - price: 328 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU882: - cost: 276 - init_stock: 4500 - price: 307 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU883: - cost: 14 - init_stock: 1400 - price: 16 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU884: - cost: 296 - init_stock: 3350 - price: 329 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU885: - cost: 158 - init_stock: 3550 - price: 176 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU886: - cost: 43 - init_stock: 4450 - price: 48 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU887: - cost: 216 - init_stock: 750 - price: 241 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU888: - cost: 102 - init_stock: 2650 - price: 114 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU889: - cost: 234 - init_stock: 4750 - price: 261 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU89: - cost: 120 - init_stock: 4050 - price: 134 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU890: - cost: 289 - init_stock: 3550 - price: 322 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU891: - cost: 312 - init_stock: 2850 - price: 347 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU892: - cost: 426 - init_stock: 4200 - price: 474 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU893: - cost: 389 - init_stock: 4900 - price: 433 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU894: - cost: 359 - init_stock: 1300 - price: 399 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU895: - cost: 301 - init_stock: 2400 - price: 335 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU896: - cost: 82 - init_stock: 2750 - price: 92 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU897: - cost: 363 - init_stock: 1100 - price: 404 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU898: - cost: 64 - init_stock: 4900 - price: 72 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU899: - cost: 81 - init_stock: 4450 - price: 91 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU9: - cost: 207 - init_stock: 1050 - price: 231 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU90: - cost: 34 - init_stock: 1500 - price: 38 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU900: - cost: 10 - init_stock: 2950 - price: 12 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU901: - cost: 276 - init_stock: 3900 - price: 307 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU902: - cost: 318 - init_stock: 2800 - price: 354 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU903: - cost: 12 - init_stock: 4650 - price: 14 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU904: - cost: 270 - init_stock: 4850 - price: 300 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU905: - cost: 120 - init_stock: 2550 - price: 134 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU906: - cost: 381 - init_stock: 4400 - price: 424 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU907: - cost: 324 - init_stock: 1600 - price: 360 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU908: - cost: 324 - init_stock: 1950 - price: 361 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU909: - cost: 58 - init_stock: 3450 - price: 65 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU91: - cost: 200 - init_stock: 2150 - price: 223 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU910: - cost: 144 - init_stock: 500 - price: 161 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU911: - cost: 167 - init_stock: 2500 - price: 186 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU912: - cost: 211 - init_stock: 2700 - price: 235 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU913: - cost: 109 - init_stock: 3400 - price: 122 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU914: - cost: 392 - init_stock: 800 - price: 436 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU915: - cost: 209 - init_stock: 1150 - price: 233 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU916: - cost: 23 - init_stock: 2450 - price: 26 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU917: - cost: 126 - init_stock: 4900 - price: 141 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU918: - cost: 101 - init_stock: 4450 - price: 113 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU919: - cost: 61 - init_stock: 4350 - price: 68 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU92: - cost: 292 - init_stock: 1900 - price: 325 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU920: - cost: 385 - init_stock: 1050 - price: 428 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU921: - cost: 441 - init_stock: 3800 - price: 491 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU922: - cost: 9 - init_stock: 2550 - price: 10 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU923: - cost: 72 - init_stock: 500 - price: 80 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU924: - cost: 147 - init_stock: 3800 - price: 164 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU925: - cost: 198 - init_stock: 3050 - price: 221 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU926: - cost: 270 - init_stock: 350 - price: 300 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU927: - cost: 115 - init_stock: 3150 - price: 128 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU928: - cost: 113 - init_stock: 400 - price: 126 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU929: - cost: 339 - init_stock: 750 - price: 377 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU93: - cost: 396 - init_stock: 450 - price: 441 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU930: - cost: 243 - init_stock: 4950 - price: 270 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU931: - cost: 370 - init_stock: 3250 - price: 412 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU932: - cost: 106 - init_stock: 2400 - price: 118 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU933: - cost: 48 - init_stock: 4550 - price: 54 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU934: - cost: 312 - init_stock: 1950 - price: 347 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU935: - cost: 197 - init_stock: 850 - price: 219 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU936: - cost: 254 - init_stock: 3850 - price: 283 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU937: - cost: 411 - init_stock: 3650 - price: 457 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU938: - cost: 405 - init_stock: 4500 - price: 450 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU939: - cost: 60 - init_stock: 1350 - price: 67 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU94: - cost: 251 - init_stock: 850 - price: 279 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU940: - cost: 58 - init_stock: 1450 - price: 65 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU941: - cost: 191 - init_stock: 250 - price: 213 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU942: - cost: 135 - init_stock: 1350 - price: 151 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU943: - cost: 76 - init_stock: 2500 - price: 85 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU944: - cost: 138 - init_stock: 300 - price: 154 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU945: - cost: 240 - init_stock: 750 - price: 267 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU946: - cost: 357 - init_stock: 450 - price: 397 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU947: - cost: 247 - init_stock: 2050 - price: 275 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU948: - cost: 424 - init_stock: 4400 - price: 472 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU949: - cost: 247 - init_stock: 4900 - price: 275 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU95: - cost: 164 - init_stock: 2550 - price: 183 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU950: - cost: 336 - init_stock: 1250 - price: 374 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU951: - cost: 244 - init_stock: 1600 - price: 272 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU952: - cost: 174 - init_stock: 3150 - price: 194 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU953: - cost: 385 - init_stock: 500 - price: 428 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU954: - cost: 244 - init_stock: 800 - price: 272 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU955: - cost: 75 - init_stock: 4600 - price: 84 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU956: - cost: 326 - init_stock: 1750 - price: 363 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU957: - cost: 198 - init_stock: 4500 - price: 221 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU958: - cost: 301 - init_stock: 1800 - price: 335 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU959: - cost: 384 - init_stock: 3550 - price: 427 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU96: - cost: 288 - init_stock: 550 - price: 320 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU960: - cost: 168 - init_stock: 3850 - price: 187 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU961: - cost: 81 - init_stock: 3300 - price: 90 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU962: - cost: 63 - init_stock: 4900 - price: 70 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU963: - cost: 165 - init_stock: 1850 - price: 184 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU964: - cost: 162 - init_stock: 500 - price: 180 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU965: - cost: 396 - init_stock: 800 - price: 441 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU966: - cost: 192 - init_stock: 1600 - price: 214 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU967: - cost: 63 - init_stock: 3100 - price: 71 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU968: - cost: 127 - init_stock: 250 - price: 142 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU969: - cost: 391 - init_stock: 2900 - price: 435 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU97: - cost: 13 - init_stock: 1350 - price: 15 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU970: - cost: 352 - init_stock: 4600 - price: 392 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU971: - cost: 367 - init_stock: 650 - price: 408 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU972: - cost: 19 - init_stock: 1000 - price: 22 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU973: - cost: 428 - init_stock: 4450 - price: 476 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU974: - cost: 320 - init_stock: 2500 - price: 356 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU975: - cost: 269 - init_stock: 850 - price: 299 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU976: - cost: 332 - init_stock: 1750 - price: 369 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU977: - cost: 127 - init_stock: 2650 - price: 142 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU978: - cost: 217 - init_stock: 4900 - price: 242 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU979: - cost: 197 - init_stock: 4950 - price: 219 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU98: - cost: 150 - init_stock: 3900 - price: 167 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU980: - cost: 326 - init_stock: 4100 - price: 363 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU981: - cost: 374 - init_stock: 4200 - price: 416 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU982: - cost: 146 - init_stock: 1150 - price: 163 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU983: - cost: 135 - init_stock: 3000 - price: 151 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU984: - cost: 110 - init_stock: 4200 - price: 123 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU985: - cost: 102 - init_stock: 2050 - price: 114 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU986: - cost: 96 - init_stock: 3800 - price: 107 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU987: - cost: 50 - init_stock: 4650 - price: 56 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU988: - cost: 59 - init_stock: 1950 - price: 66 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU989: - cost: 424 - init_stock: 3350 - price: 472 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU99: - cost: 27 - init_stock: 4450 - price: 31 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU990: - cost: 12 - init_stock: 2350 - price: 14 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU991: - cost: 405 - init_stock: 2450 - price: 450 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU992: - cost: 16 - init_stock: 3050 - price: 18 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU993: - cost: 315 - init_stock: 2350 - price: 350 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU994: - cost: 40 - init_stock: 4550 - price: 45 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU995: - cost: 365 - init_stock: 4300 - price: 406 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU996: - cost: 424 - init_stock: 2400 - price: 472 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU997: - cost: 313 - init_stock: 1350 - price: 348 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU998: - cost: 215 - init_stock: 750 - price: 239 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU999: - cost: 391 - init_stock: 5000 - price: 435 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5139100 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 500 - definition_ref: WarehouseFacility - name: WAREHOUSE0 - skus: - SKU0: - cost: 405 - init_stock: 1700 - price: 405 - service_level: 0.96 - vlt: 2 - SKU1: - cost: 42 - init_stock: 840 - price: 42 - service_level: 0.96 - vlt: 1 - SKU10: - cost: 125 - init_stock: 1820 - price: 125 - service_level: 0.96 - vlt: 1 - SKU100: - cost: 338 - init_stock: 1840 - price: 338 - service_level: 0.96 - vlt: 3 - SKU101: - cost: 436 - init_stock: 1380 - price: 436 - service_level: 0.96 - vlt: 1 - SKU102: - cost: 90 - init_stock: 1340 - price: 90 - service_level: 0.96 - vlt: 2 - SKU103: - cost: 161 - init_stock: 1620 - price: 161 - service_level: 0.96 - vlt: 2 - SKU104: - cost: 470 - init_stock: 800 - price: 470 - service_level: 0.96 - vlt: 2 - SKU105: - cost: 214 - init_stock: 1080 - price: 214 - service_level: 0.96 - vlt: 2 - SKU106: - cost: 37 - init_stock: 1020 - price: 37 - service_level: 0.96 - vlt: 1 - SKU107: - cost: 64 - init_stock: 1220 - price: 64 - service_level: 0.96 - vlt: 1 - SKU108: - cost: 474 - init_stock: 600 - price: 474 - service_level: 0.96 - vlt: 3 - SKU109: - cost: 398 - init_stock: 1020 - price: 398 - service_level: 0.96 - vlt: 1 - SKU11: - cost: 106 - init_stock: 1760 - price: 106 - service_level: 0.96 - vlt: 3 - SKU110: - cost: 71 - init_stock: 500 - price: 71 - service_level: 0.96 - vlt: 3 - SKU111: - cost: 183 - init_stock: 1000 - price: 183 - service_level: 0.96 - vlt: 3 - SKU112: - cost: 78 - init_stock: 220 - price: 78 - service_level: 0.96 - vlt: 1 - SKU113: - cost: 289 - init_stock: 1060 - price: 289 - service_level: 0.96 - vlt: 2 - SKU114: - cost: 333 - init_stock: 1300 - price: 333 - service_level: 0.96 - vlt: 3 - SKU115: - cost: 437 - init_stock: 500 - price: 437 - service_level: 0.96 - vlt: 2 - SKU116: - cost: 256 - init_stock: 1360 - price: 256 - service_level: 0.96 - vlt: 1 - SKU117: - cost: 123 - init_stock: 440 - price: 123 - service_level: 0.96 - vlt: 2 - SKU118: - cost: 181 - init_stock: 980 - price: 181 - service_level: 0.96 - vlt: 3 - SKU119: - cost: 58 - init_stock: 100 - price: 58 - service_level: 0.96 - vlt: 2 - SKU12: - cost: 315 - init_stock: 1520 - price: 315 - service_level: 0.96 - vlt: 3 - SKU120: - cost: 31 - init_stock: 240 - price: 31 - service_level: 0.96 - vlt: 3 - SKU121: - cost: 255 - init_stock: 1340 - price: 255 - service_level: 0.96 - vlt: 2 - SKU122: - cost: 222 - init_stock: 740 - price: 222 - service_level: 0.96 - vlt: 1 - SKU123: - cost: 405 - init_stock: 1240 - price: 405 - service_level: 0.96 - vlt: 2 - SKU124: - cost: 169 - init_stock: 360 - price: 169 - service_level: 0.96 - vlt: 1 - SKU125: - cost: 344 - init_stock: 700 - price: 344 - service_level: 0.96 - vlt: 3 - SKU126: - cost: 362 - init_stock: 1880 - price: 362 - service_level: 0.96 - vlt: 1 - SKU127: - cost: 149 - init_stock: 620 - price: 149 - service_level: 0.96 - vlt: 3 - SKU128: - cost: 215 - init_stock: 1740 - price: 215 - service_level: 0.96 - vlt: 2 - SKU129: - cost: 83 - init_stock: 1060 - price: 83 - service_level: 0.96 - vlt: 2 - SKU13: - cost: 474 - init_stock: 900 - price: 474 - service_level: 0.96 - vlt: 2 - SKU130: - cost: 427 - init_stock: 1780 - price: 427 - service_level: 0.96 - vlt: 1 - SKU131: - cost: 58 - init_stock: 980 - price: 58 - service_level: 0.96 - vlt: 1 - SKU132: - cost: 304 - init_stock: 300 - price: 304 - service_level: 0.96 - vlt: 2 - SKU133: - cost: 161 - init_stock: 580 - price: 161 - service_level: 0.96 - vlt: 2 - SKU134: - cost: 382 - init_stock: 1480 - price: 382 - service_level: 0.96 - vlt: 3 - SKU135: - cost: 126 - init_stock: 1760 - price: 126 - service_level: 0.96 - vlt: 3 - SKU136: - cost: 431 - init_stock: 140 - price: 431 - service_level: 0.96 - vlt: 2 - SKU137: - cost: 270 - init_stock: 440 - price: 270 - service_level: 0.96 - vlt: 3 - SKU138: - cost: 10 - init_stock: 1260 - price: 10 - service_level: 0.96 - vlt: 2 - SKU139: - cost: 259 - init_stock: 1640 - price: 259 - service_level: 0.96 - vlt: 3 - SKU14: - cost: 182 - init_stock: 1820 - price: 182 - service_level: 0.96 - vlt: 3 - SKU140: - cost: 367 - init_stock: 1100 - price: 367 - service_level: 0.96 - vlt: 2 - SKU141: - cost: 325 - init_stock: 200 - price: 325 - service_level: 0.96 - vlt: 3 - SKU142: - cost: 439 - init_stock: 1620 - price: 439 - service_level: 0.96 - vlt: 2 - SKU143: - cost: 260 - init_stock: 1380 - price: 260 - service_level: 0.96 - vlt: 2 - SKU144: - cost: 457 - init_stock: 120 - price: 457 - service_level: 0.96 - vlt: 3 - SKU145: - cost: 480 - init_stock: 1500 - price: 480 - service_level: 0.96 - vlt: 1 - SKU146: - cost: 243 - init_stock: 1920 - price: 243 - service_level: 0.96 - vlt: 1 - SKU147: - cost: 334 - init_stock: 1480 - price: 334 - service_level: 0.96 - vlt: 3 - SKU148: - cost: 143 - init_stock: 800 - price: 143 - service_level: 0.96 - vlt: 1 - SKU149: - cost: 252 - init_stock: 1020 - price: 252 - service_level: 0.96 - vlt: 2 - SKU15: - cost: 320 - init_stock: 720 - price: 320 - service_level: 0.96 - vlt: 2 - SKU150: - cost: 83 - init_stock: 1240 - price: 83 - service_level: 0.96 - vlt: 3 - SKU151: - cost: 58 - init_stock: 500 - price: 58 - service_level: 0.96 - vlt: 3 - SKU152: - cost: 254 - init_stock: 700 - price: 254 - service_level: 0.96 - vlt: 1 - SKU153: - cost: 49 - init_stock: 1440 - price: 49 - service_level: 0.96 - vlt: 2 - SKU154: - cost: 391 - init_stock: 200 - price: 391 - service_level: 0.96 - vlt: 2 - SKU155: - cost: 36 - init_stock: 460 - price: 36 - service_level: 0.96 - vlt: 3 - SKU156: - cost: 140 - init_stock: 740 - price: 140 - service_level: 0.96 - vlt: 3 - SKU157: - cost: 374 - init_stock: 780 - price: 374 - service_level: 0.96 - vlt: 1 - SKU158: - cost: 182 - init_stock: 540 - price: 182 - service_level: 0.96 - vlt: 1 - SKU159: - cost: 428 - init_stock: 800 - price: 428 - service_level: 0.96 - vlt: 2 - SKU16: - cost: 277 - init_stock: 1420 - price: 277 - service_level: 0.96 - vlt: 2 - SKU160: - cost: 129 - init_stock: 1560 - price: 129 - service_level: 0.96 - vlt: 2 - SKU161: - cost: 32 - init_stock: 1920 - price: 32 - service_level: 0.96 - vlt: 2 - SKU162: - cost: 68 - init_stock: 1220 - price: 68 - service_level: 0.96 - vlt: 2 - SKU163: - cost: 129 - init_stock: 960 - price: 129 - service_level: 0.96 - vlt: 2 - SKU164: - cost: 192 - init_stock: 1360 - price: 192 - service_level: 0.96 - vlt: 1 - SKU165: - cost: 316 - init_stock: 1240 - price: 316 - service_level: 0.96 - vlt: 1 - SKU166: - cost: 454 - init_stock: 140 - price: 454 - service_level: 0.96 - vlt: 1 - SKU167: - cost: 108 - init_stock: 1420 - price: 108 - service_level: 0.96 - vlt: 2 - SKU168: - cost: 431 - init_stock: 600 - price: 431 - service_level: 0.96 - vlt: 2 - SKU169: - cost: 428 - init_stock: 1440 - price: 428 - service_level: 0.96 - vlt: 3 - SKU17: - cost: 393 - init_stock: 800 - price: 393 - service_level: 0.96 - vlt: 1 - SKU170: - cost: 107 - init_stock: 280 - price: 107 - service_level: 0.96 - vlt: 3 - SKU171: - cost: 14 - init_stock: 1900 - price: 14 - service_level: 0.96 - vlt: 3 - SKU172: - cost: 291 - init_stock: 1720 - price: 291 - service_level: 0.96 - vlt: 1 - SKU173: - cost: 190 - init_stock: 300 - price: 190 - service_level: 0.96 - vlt: 2 - SKU174: - cost: 431 - init_stock: 520 - price: 431 - service_level: 0.96 - vlt: 3 - SKU175: - cost: 355 - init_stock: 1740 - price: 355 - service_level: 0.96 - vlt: 1 - SKU176: - cost: 305 - init_stock: 980 - price: 305 - service_level: 0.96 - vlt: 3 - SKU177: - cost: 291 - init_stock: 460 - price: 291 - service_level: 0.96 - vlt: 2 - SKU178: - cost: 417 - init_stock: 400 - price: 417 - service_level: 0.96 - vlt: 2 - SKU179: - cost: 18 - init_stock: 1220 - price: 18 - service_level: 0.96 - vlt: 2 - SKU18: - cost: 223 - init_stock: 1040 - price: 223 - service_level: 0.96 - vlt: 1 - SKU180: - cost: 373 - init_stock: 740 - price: 373 - service_level: 0.96 - vlt: 2 - SKU181: - cost: 486 - init_stock: 1160 - price: 486 - service_level: 0.96 - vlt: 3 - SKU182: - cost: 385 - init_stock: 680 - price: 385 - service_level: 0.96 - vlt: 3 - SKU183: - cost: 195 - init_stock: 1600 - price: 195 - service_level: 0.96 - vlt: 2 - SKU184: - cost: 238 - init_stock: 100 - price: 238 - service_level: 0.96 - vlt: 3 - SKU185: - cost: 50 - init_stock: 900 - price: 50 - service_level: 0.96 - vlt: 2 - SKU186: - cost: 143 - init_stock: 1880 - price: 143 - service_level: 0.96 - vlt: 3 - SKU187: - cost: 285 - init_stock: 760 - price: 285 - service_level: 0.96 - vlt: 3 - SKU188: - cost: 180 - init_stock: 660 - price: 180 - service_level: 0.96 - vlt: 2 - SKU189: - cost: 294 - init_stock: 360 - price: 294 - service_level: 0.96 - vlt: 2 - SKU19: - cost: 301 - init_stock: 560 - price: 301 - service_level: 0.96 - vlt: 2 - SKU190: - cost: 392 - init_stock: 740 - price: 392 - service_level: 0.96 - vlt: 1 - SKU191: - cost: 124 - init_stock: 340 - price: 124 - service_level: 0.96 - vlt: 2 - SKU192: - cost: 118 - init_stock: 740 - price: 118 - service_level: 0.96 - vlt: 3 - SKU193: - cost: 168 - init_stock: 1640 - price: 168 - service_level: 0.96 - vlt: 1 - SKU194: - cost: 331 - init_stock: 720 - price: 331 - service_level: 0.96 - vlt: 2 - SKU195: - cost: 81 - init_stock: 660 - price: 81 - service_level: 0.96 - vlt: 3 - SKU196: - cost: 94 - init_stock: 240 - price: 94 - service_level: 0.96 - vlt: 3 - SKU197: - cost: 165 - init_stock: 840 - price: 165 - service_level: 0.96 - vlt: 2 - SKU198: - cost: 454 - init_stock: 1560 - price: 454 - service_level: 0.96 - vlt: 3 - SKU199: - cost: 182 - init_stock: 740 - price: 182 - service_level: 0.96 - vlt: 3 - SKU2: - cost: 60 - init_stock: 1140 - price: 60 - service_level: 0.96 - vlt: 1 - SKU20: - cost: 124 - init_stock: 1680 - price: 124 - service_level: 0.96 - vlt: 1 - SKU200: - cost: 355 - init_stock: 1180 - price: 355 - service_level: 0.96 - vlt: 3 - SKU201: - cost: 263 - init_stock: 920 - price: 263 - service_level: 0.96 - vlt: 2 - SKU202: - cost: 316 - init_stock: 420 - price: 316 - service_level: 0.96 - vlt: 1 - SKU203: - cost: 145 - init_stock: 120 - price: 145 - service_level: 0.96 - vlt: 2 - SKU204: - cost: 28 - init_stock: 600 - price: 28 - service_level: 0.96 - vlt: 3 - SKU205: - cost: 190 - init_stock: 1900 - price: 190 - service_level: 0.96 - vlt: 1 - SKU206: - cost: 333 - init_stock: 1240 - price: 333 - service_level: 0.96 - vlt: 3 - SKU207: - cost: 322 - init_stock: 1380 - price: 322 - service_level: 0.96 - vlt: 2 - SKU208: - cost: 287 - init_stock: 320 - price: 287 - service_level: 0.96 - vlt: 2 - SKU209: - cost: 278 - init_stock: 340 - price: 278 - service_level: 0.96 - vlt: 2 - SKU21: - cost: 408 - init_stock: 360 - price: 408 - service_level: 0.96 - vlt: 2 - SKU210: - cost: 322 - init_stock: 820 - price: 322 - service_level: 0.96 - vlt: 3 - SKU211: - cost: 456 - init_stock: 1040 - price: 456 - service_level: 0.96 - vlt: 1 - SKU212: - cost: 248 - init_stock: 340 - price: 248 - service_level: 0.96 - vlt: 3 - SKU213: - cost: 287 - init_stock: 100 - price: 287 - service_level: 0.96 - vlt: 3 - SKU214: - cost: 89 - init_stock: 1260 - price: 89 - service_level: 0.96 - vlt: 2 - SKU215: - cost: 177 - init_stock: 1920 - price: 177 - service_level: 0.96 - vlt: 3 - SKU216: - cost: 232 - init_stock: 1780 - price: 232 - service_level: 0.96 - vlt: 3 - SKU217: - cost: 32 - init_stock: 1700 - price: 32 - service_level: 0.96 - vlt: 2 - SKU218: - cost: 52 - init_stock: 360 - price: 52 - service_level: 0.96 - vlt: 2 - SKU219: - cost: 497 - init_stock: 920 - price: 497 - service_level: 0.96 - vlt: 1 - SKU22: - cost: 497 - init_stock: 600 - price: 497 - service_level: 0.96 - vlt: 2 - SKU220: - cost: 436 - init_stock: 880 - price: 436 - service_level: 0.96 - vlt: 2 - SKU221: - cost: 58 - init_stock: 1480 - price: 58 - service_level: 0.96 - vlt: 3 - SKU222: - cost: 174 - init_stock: 280 - price: 174 - service_level: 0.96 - vlt: 3 - SKU223: - cost: 140 - init_stock: 920 - price: 140 - service_level: 0.96 - vlt: 3 - SKU224: - cost: 246 - init_stock: 540 - price: 246 - service_level: 0.96 - vlt: 2 - SKU225: - cost: 17 - init_stock: 1060 - price: 17 - service_level: 0.96 - vlt: 1 - SKU226: - cost: 81 - init_stock: 1120 - price: 81 - service_level: 0.96 - vlt: 3 - SKU227: - cost: 475 - init_stock: 600 - price: 475 - service_level: 0.96 - vlt: 1 - SKU228: - cost: 296 - init_stock: 1300 - price: 296 - service_level: 0.96 - vlt: 3 - SKU229: - cost: 465 - init_stock: 980 - price: 465 - service_level: 0.96 - vlt: 2 - SKU23: - cost: 301 - init_stock: 1360 - price: 301 - service_level: 0.96 - vlt: 1 - SKU230: - cost: 483 - init_stock: 700 - price: 483 - service_level: 0.96 - vlt: 2 - SKU231: - cost: 173 - init_stock: 260 - price: 173 - service_level: 0.96 - vlt: 3 - SKU232: - cost: 315 - init_stock: 440 - price: 315 - service_level: 0.96 - vlt: 2 - SKU233: - cost: 57 - init_stock: 100 - price: 57 - service_level: 0.96 - vlt: 1 - SKU234: - cost: 315 - init_stock: 1380 - price: 315 - service_level: 0.96 - vlt: 2 - SKU235: - cost: 92 - init_stock: 160 - price: 92 - service_level: 0.96 - vlt: 2 - SKU236: - cost: 125 - init_stock: 360 - price: 125 - service_level: 0.96 - vlt: 1 - SKU237: - cost: 201 - init_stock: 420 - price: 201 - service_level: 0.96 - vlt: 1 - SKU238: - cost: 105 - init_stock: 760 - price: 105 - service_level: 0.96 - vlt: 2 - SKU239: - cost: 66 - init_stock: 1720 - price: 66 - service_level: 0.96 - vlt: 1 - SKU24: - cost: 99 - init_stock: 1400 - price: 99 - service_level: 0.96 - vlt: 3 - SKU240: - cost: 262 - init_stock: 1220 - price: 262 - service_level: 0.96 - vlt: 1 - SKU241: - cost: 361 - init_stock: 1280 - price: 361 - service_level: 0.96 - vlt: 3 - SKU242: - cost: 221 - init_stock: 1760 - price: 221 - service_level: 0.96 - vlt: 1 - SKU243: - cost: 141 - init_stock: 1240 - price: 141 - service_level: 0.96 - vlt: 2 - SKU244: - cost: 193 - init_stock: 1020 - price: 193 - service_level: 0.96 - vlt: 2 - SKU245: - cost: 165 - init_stock: 1100 - price: 165 - service_level: 0.96 - vlt: 2 - SKU246: - cost: 474 - init_stock: 320 - price: 474 - service_level: 0.96 - vlt: 1 - SKU247: - cost: 66 - init_stock: 1800 - price: 66 - service_level: 0.96 - vlt: 1 - SKU248: - cost: 136 - init_stock: 260 - price: 136 - service_level: 0.96 - vlt: 3 - SKU249: - cost: 83 - init_stock: 1200 - price: 83 - service_level: 0.96 - vlt: 1 - SKU25: - cost: 11 - init_stock: 1100 - price: 11 - service_level: 0.96 - vlt: 2 - SKU250: - cost: 310 - init_stock: 1700 - price: 310 - service_level: 0.96 - vlt: 2 - SKU251: - cost: 247 - init_stock: 880 - price: 247 - service_level: 0.96 - vlt: 3 - SKU252: - cost: 192 - init_stock: 960 - price: 192 - service_level: 0.96 - vlt: 2 - SKU253: - cost: 270 - init_stock: 1200 - price: 270 - service_level: 0.96 - vlt: 3 - SKU254: - cost: 165 - init_stock: 1980 - price: 165 - service_level: 0.96 - vlt: 3 - SKU255: - cost: 49 - init_stock: 1080 - price: 49 - service_level: 0.96 - vlt: 3 - SKU256: - cost: 314 - init_stock: 1720 - price: 314 - service_level: 0.96 - vlt: 2 - SKU257: - cost: 468 - init_stock: 480 - price: 468 - service_level: 0.96 - vlt: 3 - SKU258: - cost: 151 - init_stock: 200 - price: 151 - service_level: 0.96 - vlt: 2 - SKU259: - cost: 389 - init_stock: 380 - price: 389 - service_level: 0.96 - vlt: 3 - SKU26: - cost: 348 - init_stock: 740 - price: 348 - service_level: 0.96 - vlt: 3 - SKU260: - cost: 90 - init_stock: 1700 - price: 90 - service_level: 0.96 - vlt: 1 - SKU261: - cost: 122 - init_stock: 1500 - price: 122 - service_level: 0.96 - vlt: 3 - SKU262: - cost: 15 - init_stock: 360 - price: 15 - service_level: 0.96 - vlt: 2 - SKU263: - cost: 350 - init_stock: 780 - price: 350 - service_level: 0.96 - vlt: 1 - SKU264: - cost: 10 - init_stock: 620 - price: 10 - service_level: 0.96 - vlt: 3 - SKU265: - cost: 81 - init_stock: 380 - price: 81 - service_level: 0.96 - vlt: 3 - SKU266: - cost: 276 - init_stock: 1900 - price: 276 - service_level: 0.96 - vlt: 1 - SKU267: - cost: 123 - init_stock: 840 - price: 123 - service_level: 0.96 - vlt: 1 - SKU268: - cost: 64 - init_stock: 680 - price: 64 - service_level: 0.96 - vlt: 3 - SKU269: - cost: 435 - init_stock: 200 - price: 435 - service_level: 0.96 - vlt: 3 - SKU27: - cost: 196 - init_stock: 1140 - price: 196 - service_level: 0.96 - vlt: 1 - SKU270: - cost: 68 - init_stock: 1160 - price: 68 - service_level: 0.96 - vlt: 3 - SKU271: - cost: 14 - init_stock: 160 - price: 14 - service_level: 0.96 - vlt: 1 - SKU272: - cost: 249 - init_stock: 1660 - price: 249 - service_level: 0.96 - vlt: 1 - SKU273: - cost: 39 - init_stock: 400 - price: 39 - service_level: 0.96 - vlt: 3 - SKU274: - cost: 436 - init_stock: 180 - price: 436 - service_level: 0.96 - vlt: 2 - SKU275: - cost: 62 - init_stock: 1860 - price: 62 - service_level: 0.96 - vlt: 1 - SKU276: - cost: 281 - init_stock: 1500 - price: 281 - service_level: 0.96 - vlt: 1 - SKU277: - cost: 442 - init_stock: 1820 - price: 442 - service_level: 0.96 - vlt: 2 - SKU278: - cost: 290 - init_stock: 1680 - price: 290 - service_level: 0.96 - vlt: 2 - SKU279: - cost: 175 - init_stock: 1180 - price: 175 - service_level: 0.96 - vlt: 2 - SKU28: - cost: 61 - init_stock: 720 - price: 61 - service_level: 0.96 - vlt: 1 - SKU280: - cost: 445 - init_stock: 1640 - price: 445 - service_level: 0.96 - vlt: 3 - SKU281: - cost: 49 - init_stock: 140 - price: 49 - service_level: 0.96 - vlt: 3 - SKU282: - cost: 499 - init_stock: 620 - price: 499 - service_level: 0.96 - vlt: 3 - SKU283: - cost: 310 - init_stock: 840 - price: 310 - service_level: 0.96 - vlt: 1 - SKU284: - cost: 381 - init_stock: 840 - price: 381 - service_level: 0.96 - vlt: 1 - SKU285: - cost: 378 - init_stock: 700 - price: 378 - service_level: 0.96 - vlt: 2 - SKU286: - cost: 366 - init_stock: 1780 - price: 366 - service_level: 0.96 - vlt: 1 - SKU287: - cost: 287 - init_stock: 1340 - price: 287 - service_level: 0.96 - vlt: 1 - SKU288: - cost: 24 - init_stock: 240 - price: 24 - service_level: 0.96 - vlt: 1 - SKU289: - cost: 439 - init_stock: 1980 - price: 439 - service_level: 0.96 - vlt: 2 - SKU29: - cost: 490 - init_stock: 1440 - price: 490 - service_level: 0.96 - vlt: 2 - SKU290: - cost: 11 - init_stock: 1540 - price: 11 - service_level: 0.96 - vlt: 3 - SKU291: - cost: 259 - init_stock: 360 - price: 259 - service_level: 0.96 - vlt: 1 - SKU292: - cost: 389 - init_stock: 2000 - price: 389 - service_level: 0.96 - vlt: 3 - SKU293: - cost: 246 - init_stock: 1120 - price: 246 - service_level: 0.96 - vlt: 1 - SKU294: - cost: 76 - init_stock: 260 - price: 76 - service_level: 0.96 - vlt: 3 - SKU295: - cost: 22 - init_stock: 1720 - price: 22 - service_level: 0.96 - vlt: 3 - SKU296: - cost: 389 - init_stock: 1900 - price: 389 - service_level: 0.96 - vlt: 2 - SKU297: - cost: 51 - init_stock: 1280 - price: 51 - service_level: 0.96 - vlt: 3 - SKU298: - cost: 269 - init_stock: 1780 - price: 269 - service_level: 0.96 - vlt: 2 - SKU299: - cost: 14 - init_stock: 1780 - price: 14 - service_level: 0.96 - vlt: 1 - SKU3: - cost: 470 - init_stock: 1760 - price: 470 - service_level: 0.96 - vlt: 1 - SKU30: - cost: 218 - init_stock: 900 - price: 218 - service_level: 0.96 - vlt: 1 - SKU300: - cost: 94 - init_stock: 440 - price: 94 - service_level: 0.96 - vlt: 3 - SKU301: - cost: 417 - init_stock: 640 - price: 417 - service_level: 0.96 - vlt: 2 - SKU302: - cost: 161 - init_stock: 1840 - price: 161 - service_level: 0.96 - vlt: 1 - SKU303: - cost: 215 - init_stock: 1580 - price: 215 - service_level: 0.96 - vlt: 2 - SKU304: - cost: 145 - init_stock: 1160 - price: 145 - service_level: 0.96 - vlt: 3 - SKU305: - cost: 441 - init_stock: 160 - price: 441 - service_level: 0.96 - vlt: 2 - SKU306: - cost: 284 - init_stock: 1380 - price: 284 - service_level: 0.96 - vlt: 1 - SKU307: - cost: 412 - init_stock: 1120 - price: 412 - service_level: 0.96 - vlt: 2 - SKU308: - cost: 409 - init_stock: 1600 - price: 409 - service_level: 0.96 - vlt: 2 - SKU309: - cost: 260 - init_stock: 1020 - price: 260 - service_level: 0.96 - vlt: 2 - SKU31: - cost: 140 - init_stock: 1760 - price: 140 - service_level: 0.96 - vlt: 3 - SKU310: - cost: 367 - init_stock: 560 - price: 367 - service_level: 0.96 - vlt: 2 - SKU311: - cost: 143 - init_stock: 1820 - price: 143 - service_level: 0.96 - vlt: 3 - SKU312: - cost: 264 - init_stock: 360 - price: 264 - service_level: 0.96 - vlt: 3 - SKU313: - cost: 189 - init_stock: 1900 - price: 189 - service_level: 0.96 - vlt: 3 - SKU314: - cost: 318 - init_stock: 300 - price: 318 - service_level: 0.96 - vlt: 3 - SKU315: - cost: 31 - init_stock: 1140 - price: 31 - service_level: 0.96 - vlt: 3 - SKU316: - cost: 215 - init_stock: 1720 - price: 215 - service_level: 0.96 - vlt: 1 - SKU317: - cost: 72 - init_stock: 620 - price: 72 - service_level: 0.96 - vlt: 2 - SKU318: - cost: 485 - init_stock: 1800 - price: 485 - service_level: 0.96 - vlt: 1 - SKU319: - cost: 218 - init_stock: 480 - price: 218 - service_level: 0.96 - vlt: 1 - SKU32: - cost: 380 - init_stock: 640 - price: 380 - service_level: 0.96 - vlt: 2 - SKU320: - cost: 445 - init_stock: 2000 - price: 445 - service_level: 0.96 - vlt: 3 - SKU321: - cost: 73 - init_stock: 1760 - price: 73 - service_level: 0.96 - vlt: 1 - SKU322: - cost: 120 - init_stock: 1540 - price: 120 - service_level: 0.96 - vlt: 2 - SKU323: - cost: 241 - init_stock: 1900 - price: 241 - service_level: 0.96 - vlt: 1 - SKU324: - cost: 269 - init_stock: 2000 - price: 269 - service_level: 0.96 - vlt: 1 - SKU325: - cost: 295 - init_stock: 480 - price: 295 - service_level: 0.96 - vlt: 3 - SKU326: - cost: 23 - init_stock: 1660 - price: 23 - service_level: 0.96 - vlt: 3 - SKU327: - cost: 334 - init_stock: 1320 - price: 334 - service_level: 0.96 - vlt: 1 - SKU328: - cost: 107 - init_stock: 220 - price: 107 - service_level: 0.96 - vlt: 2 - SKU329: - cost: 260 - init_stock: 1200 - price: 260 - service_level: 0.96 - vlt: 3 - SKU33: - cost: 81 - init_stock: 1680 - price: 81 - service_level: 0.96 - vlt: 3 - SKU330: - cost: 364 - init_stock: 880 - price: 364 - service_level: 0.96 - vlt: 2 - SKU331: - cost: 435 - init_stock: 560 - price: 435 - service_level: 0.96 - vlt: 3 - SKU332: - cost: 227 - init_stock: 1660 - price: 227 - service_level: 0.96 - vlt: 3 - SKU333: - cost: 268 - init_stock: 1260 - price: 268 - service_level: 0.96 - vlt: 3 - SKU334: - cost: 248 - init_stock: 860 - price: 248 - service_level: 0.96 - vlt: 1 - SKU335: - cost: 341 - init_stock: 1880 - price: 341 - service_level: 0.96 - vlt: 2 - SKU336: - cost: 489 - init_stock: 1000 - price: 489 - service_level: 0.96 - vlt: 2 - SKU337: - cost: 416 - init_stock: 1960 - price: 416 - service_level: 0.96 - vlt: 2 - SKU338: - cost: 209 - init_stock: 1480 - price: 209 - service_level: 0.96 - vlt: 3 - SKU339: - cost: 189 - init_stock: 1240 - price: 189 - service_level: 0.96 - vlt: 2 - SKU34: - cost: 263 - init_stock: 940 - price: 263 - service_level: 0.96 - vlt: 2 - SKU340: - cost: 375 - init_stock: 380 - price: 375 - service_level: 0.96 - vlt: 2 - SKU341: - cost: 166 - init_stock: 1120 - price: 166 - service_level: 0.96 - vlt: 3 - SKU342: - cost: 177 - init_stock: 820 - price: 177 - service_level: 0.96 - vlt: 1 - SKU343: - cost: 478 - init_stock: 1680 - price: 478 - service_level: 0.96 - vlt: 2 - SKU344: - cost: 491 - init_stock: 1620 - price: 491 - service_level: 0.96 - vlt: 3 - SKU345: - cost: 272 - init_stock: 660 - price: 272 - service_level: 0.96 - vlt: 3 - SKU346: - cost: 200 - init_stock: 1420 - price: 200 - service_level: 0.96 - vlt: 2 - SKU347: - cost: 180 - init_stock: 620 - price: 180 - service_level: 0.96 - vlt: 2 - SKU348: - cost: 296 - init_stock: 680 - price: 296 - service_level: 0.96 - vlt: 1 - SKU349: - cost: 81 - init_stock: 1820 - price: 81 - service_level: 0.96 - vlt: 2 - SKU35: - cost: 240 - init_stock: 1180 - price: 240 - service_level: 0.96 - vlt: 1 - SKU350: - cost: 106 - init_stock: 240 - price: 106 - service_level: 0.96 - vlt: 2 - SKU351: - cost: 102 - init_stock: 1480 - price: 102 - service_level: 0.96 - vlt: 3 - SKU352: - cost: 274 - init_stock: 320 - price: 274 - service_level: 0.96 - vlt: 2 - SKU353: - cost: 147 - init_stock: 1360 - price: 147 - service_level: 0.96 - vlt: 1 - SKU354: - cost: 410 - init_stock: 1560 - price: 410 - service_level: 0.96 - vlt: 3 - SKU355: - cost: 417 - init_stock: 1120 - price: 417 - service_level: 0.96 - vlt: 3 - SKU356: - cost: 192 - init_stock: 700 - price: 192 - service_level: 0.96 - vlt: 2 - SKU357: - cost: 80 - init_stock: 2000 - price: 80 - service_level: 0.96 - vlt: 1 - SKU358: - cost: 130 - init_stock: 640 - price: 130 - service_level: 0.96 - vlt: 1 - SKU359: - cost: 327 - init_stock: 640 - price: 327 - service_level: 0.96 - vlt: 2 - SKU36: - cost: 134 - init_stock: 1840 - price: 134 - service_level: 0.96 - vlt: 2 - SKU360: - cost: 456 - init_stock: 1100 - price: 456 - service_level: 0.96 - vlt: 2 - SKU361: - cost: 183 - init_stock: 1340 - price: 183 - service_level: 0.96 - vlt: 3 - SKU362: - cost: 352 - init_stock: 820 - price: 352 - service_level: 0.96 - vlt: 3 - SKU363: - cost: 32 - init_stock: 100 - price: 32 - service_level: 0.96 - vlt: 3 - SKU364: - cost: 331 - init_stock: 1300 - price: 331 - service_level: 0.96 - vlt: 1 - SKU365: - cost: 57 - init_stock: 1640 - price: 57 - service_level: 0.96 - vlt: 3 - SKU366: - cost: 184 - init_stock: 480 - price: 184 - service_level: 0.96 - vlt: 3 - SKU367: - cost: 385 - init_stock: 1600 - price: 385 - service_level: 0.96 - vlt: 1 - SKU368: - cost: 391 - init_stock: 620 - price: 391 - service_level: 0.96 - vlt: 3 - SKU369: - cost: 460 - init_stock: 640 - price: 460 - service_level: 0.96 - vlt: 3 - SKU37: - cost: 284 - init_stock: 1380 - price: 284 - service_level: 0.96 - vlt: 2 - SKU370: - cost: 103 - init_stock: 560 - price: 103 - service_level: 0.96 - vlt: 2 - SKU371: - cost: 181 - init_stock: 1680 - price: 181 - service_level: 0.96 - vlt: 3 - SKU372: - cost: 488 - init_stock: 400 - price: 488 - service_level: 0.96 - vlt: 3 - SKU373: - cost: 155 - init_stock: 540 - price: 155 - service_level: 0.96 - vlt: 2 - SKU374: - cost: 13 - init_stock: 1900 - price: 13 - service_level: 0.96 - vlt: 3 - SKU375: - cost: 433 - init_stock: 1240 - price: 433 - service_level: 0.96 - vlt: 3 - SKU376: - cost: 30 - init_stock: 1620 - price: 30 - service_level: 0.96 - vlt: 2 - SKU377: - cost: 100 - init_stock: 720 - price: 100 - service_level: 0.96 - vlt: 1 - SKU378: - cost: 489 - init_stock: 180 - price: 489 - service_level: 0.96 - vlt: 2 - SKU379: - cost: 462 - init_stock: 820 - price: 462 - service_level: 0.96 - vlt: 3 - SKU38: - cost: 326 - init_stock: 1620 - price: 326 - service_level: 0.96 - vlt: 3 - SKU380: - cost: 459 - init_stock: 260 - price: 459 - service_level: 0.96 - vlt: 3 - SKU381: - cost: 276 - init_stock: 1520 - price: 276 - service_level: 0.96 - vlt: 1 - SKU382: - cost: 472 - init_stock: 340 - price: 472 - service_level: 0.96 - vlt: 2 - SKU383: - cost: 249 - init_stock: 660 - price: 249 - service_level: 0.96 - vlt: 2 - SKU384: - cost: 37 - init_stock: 480 - price: 37 - service_level: 0.96 - vlt: 3 - SKU385: - cost: 275 - init_stock: 1600 - price: 275 - service_level: 0.96 - vlt: 3 - SKU386: - cost: 357 - init_stock: 1020 - price: 357 - service_level: 0.96 - vlt: 2 - SKU387: - cost: 471 - init_stock: 740 - price: 471 - service_level: 0.96 - vlt: 3 - SKU388: - cost: 264 - init_stock: 1340 - price: 264 - service_level: 0.96 - vlt: 2 - SKU389: - cost: 13 - init_stock: 980 - price: 13 - service_level: 0.96 - vlt: 2 - SKU39: - cost: 381 - init_stock: 440 - price: 381 - service_level: 0.96 - vlt: 1 - SKU390: - cost: 146 - init_stock: 100 - price: 146 - service_level: 0.96 - vlt: 1 - SKU391: - cost: 365 - init_stock: 1840 - price: 365 - service_level: 0.96 - vlt: 1 - SKU392: - cost: 489 - init_stock: 920 - price: 489 - service_level: 0.96 - vlt: 3 - SKU393: - cost: 466 - init_stock: 220 - price: 466 - service_level: 0.96 - vlt: 3 - SKU394: - cost: 186 - init_stock: 340 - price: 186 - service_level: 0.96 - vlt: 2 - SKU395: - cost: 324 - init_stock: 600 - price: 324 - service_level: 0.96 - vlt: 1 - SKU396: - cost: 392 - init_stock: 1360 - price: 392 - service_level: 0.96 - vlt: 3 - SKU397: - cost: 257 - init_stock: 460 - price: 257 - service_level: 0.96 - vlt: 1 - SKU398: - cost: 455 - init_stock: 1420 - price: 455 - service_level: 0.96 - vlt: 1 - SKU399: - cost: 15 - init_stock: 1560 - price: 15 - service_level: 0.96 - vlt: 3 - SKU4: - cost: 289 - init_stock: 1860 - price: 289 - service_level: 0.96 - vlt: 1 - SKU40: - cost: 216 - init_stock: 160 - price: 216 - service_level: 0.96 - vlt: 2 - SKU400: - cost: 355 - init_stock: 1900 - price: 355 - service_level: 0.96 - vlt: 2 - SKU401: - cost: 150 - init_stock: 160 - price: 150 - service_level: 0.96 - vlt: 3 - SKU402: - cost: 255 - init_stock: 1840 - price: 255 - service_level: 0.96 - vlt: 2 - SKU403: - cost: 333 - init_stock: 1660 - price: 333 - service_level: 0.96 - vlt: 2 - SKU404: - cost: 94 - init_stock: 720 - price: 94 - service_level: 0.96 - vlt: 3 - SKU405: - cost: 242 - init_stock: 1160 - price: 242 - service_level: 0.96 - vlt: 1 - SKU406: - cost: 402 - init_stock: 1100 - price: 402 - service_level: 0.96 - vlt: 3 - SKU407: - cost: 346 - init_stock: 780 - price: 346 - service_level: 0.96 - vlt: 3 - SKU408: - cost: 274 - init_stock: 1640 - price: 274 - service_level: 0.96 - vlt: 2 - SKU409: - cost: 268 - init_stock: 1880 - price: 268 - service_level: 0.96 - vlt: 2 - SKU41: - cost: 312 - init_stock: 1360 - price: 312 - service_level: 0.96 - vlt: 3 - SKU410: - cost: 429 - init_stock: 960 - price: 429 - service_level: 0.96 - vlt: 2 - SKU411: - cost: 11 - init_stock: 600 - price: 11 - service_level: 0.96 - vlt: 1 - SKU412: - cost: 91 - init_stock: 1180 - price: 91 - service_level: 0.96 - vlt: 3 - SKU413: - cost: 443 - init_stock: 220 - price: 443 - service_level: 0.96 - vlt: 3 - SKU414: - cost: 67 - init_stock: 1540 - price: 67 - service_level: 0.96 - vlt: 3 - SKU415: - cost: 347 - init_stock: 200 - price: 347 - service_level: 0.96 - vlt: 2 - SKU416: - cost: 29 - init_stock: 1020 - price: 29 - service_level: 0.96 - vlt: 3 - SKU417: - cost: 488 - init_stock: 1360 - price: 488 - service_level: 0.96 - vlt: 1 - SKU418: - cost: 432 - init_stock: 1940 - price: 432 - service_level: 0.96 - vlt: 3 - SKU419: - cost: 38 - init_stock: 260 - price: 38 - service_level: 0.96 - vlt: 1 - SKU42: - cost: 275 - init_stock: 1660 - price: 275 - service_level: 0.96 - vlt: 3 - SKU420: - cost: 316 - init_stock: 140 - price: 316 - service_level: 0.96 - vlt: 2 - SKU421: - cost: 289 - init_stock: 1600 - price: 289 - service_level: 0.96 - vlt: 3 - SKU422: - cost: 83 - init_stock: 680 - price: 83 - service_level: 0.96 - vlt: 2 - SKU423: - cost: 174 - init_stock: 240 - price: 174 - service_level: 0.96 - vlt: 2 - SKU424: - cost: 495 - init_stock: 820 - price: 495 - service_level: 0.96 - vlt: 1 - SKU425: - cost: 142 - init_stock: 880 - price: 142 - service_level: 0.96 - vlt: 1 - SKU426: - cost: 234 - init_stock: 320 - price: 234 - service_level: 0.96 - vlt: 3 - SKU427: - cost: 162 - init_stock: 1680 - price: 162 - service_level: 0.96 - vlt: 2 - SKU428: - cost: 423 - init_stock: 1240 - price: 423 - service_level: 0.96 - vlt: 1 - SKU429: - cost: 322 - init_stock: 1980 - price: 322 - service_level: 0.96 - vlt: 2 - SKU43: - cost: 428 - init_stock: 1080 - price: 428 - service_level: 0.96 - vlt: 3 - SKU430: - cost: 289 - init_stock: 900 - price: 289 - service_level: 0.96 - vlt: 1 - SKU431: - cost: 29 - init_stock: 260 - price: 29 - service_level: 0.96 - vlt: 3 - SKU432: - cost: 349 - init_stock: 460 - price: 349 - service_level: 0.96 - vlt: 3 - SKU433: - cost: 380 - init_stock: 520 - price: 380 - service_level: 0.96 - vlt: 3 - SKU434: - cost: 373 - init_stock: 820 - price: 373 - service_level: 0.96 - vlt: 3 - SKU435: - cost: 46 - init_stock: 1000 - price: 46 - service_level: 0.96 - vlt: 3 - SKU436: - cost: 383 - init_stock: 760 - price: 383 - service_level: 0.96 - vlt: 2 - SKU437: - cost: 76 - init_stock: 1640 - price: 76 - service_level: 0.96 - vlt: 3 - SKU438: - cost: 433 - init_stock: 540 - price: 433 - service_level: 0.96 - vlt: 3 - SKU439: - cost: 40 - init_stock: 1140 - price: 40 - service_level: 0.96 - vlt: 1 - SKU44: - cost: 255 - init_stock: 380 - price: 255 - service_level: 0.96 - vlt: 1 - SKU440: - cost: 124 - init_stock: 940 - price: 124 - service_level: 0.96 - vlt: 3 - SKU441: - cost: 228 - init_stock: 1080 - price: 228 - service_level: 0.96 - vlt: 2 - SKU442: - cost: 19 - init_stock: 820 - price: 19 - service_level: 0.96 - vlt: 3 - SKU443: - cost: 446 - init_stock: 1240 - price: 446 - service_level: 0.96 - vlt: 1 - SKU444: - cost: 212 - init_stock: 1080 - price: 212 - service_level: 0.96 - vlt: 2 - SKU445: - cost: 396 - init_stock: 400 - price: 396 - service_level: 0.96 - vlt: 2 - SKU446: - cost: 126 - init_stock: 1280 - price: 126 - service_level: 0.96 - vlt: 3 - SKU447: - cost: 20 - init_stock: 800 - price: 20 - service_level: 0.96 - vlt: 2 - SKU448: - cost: 365 - init_stock: 1320 - price: 365 - service_level: 0.96 - vlt: 3 - SKU449: - cost: 328 - init_stock: 160 - price: 328 - service_level: 0.96 - vlt: 1 - SKU45: - cost: 158 - init_stock: 560 - price: 158 - service_level: 0.96 - vlt: 3 - SKU450: - cost: 61 - init_stock: 120 - price: 61 - service_level: 0.96 - vlt: 1 - SKU451: - cost: 385 - init_stock: 720 - price: 385 - service_level: 0.96 - vlt: 2 - SKU452: - cost: 450 - init_stock: 1460 - price: 450 - service_level: 0.96 - vlt: 3 - SKU453: - cost: 131 - init_stock: 1620 - price: 131 - service_level: 0.96 - vlt: 1 - SKU454: - cost: 404 - init_stock: 1180 - price: 404 - service_level: 0.96 - vlt: 2 - SKU455: - cost: 334 - init_stock: 1480 - price: 334 - service_level: 0.96 - vlt: 1 - SKU456: - cost: 126 - init_stock: 820 - price: 126 - service_level: 0.96 - vlt: 3 - SKU457: - cost: 235 - init_stock: 1120 - price: 235 - service_level: 0.96 - vlt: 1 - SKU458: - cost: 282 - init_stock: 940 - price: 282 - service_level: 0.96 - vlt: 1 - SKU459: - cost: 180 - init_stock: 440 - price: 180 - service_level: 0.96 - vlt: 3 - SKU46: - cost: 341 - init_stock: 400 - price: 341 - service_level: 0.96 - vlt: 1 - SKU460: - cost: 271 - init_stock: 100 - price: 271 - service_level: 0.96 - vlt: 3 - SKU461: - cost: 131 - init_stock: 540 - price: 131 - service_level: 0.96 - vlt: 1 - SKU462: - cost: 98 - init_stock: 180 - price: 98 - service_level: 0.96 - vlt: 2 - SKU463: - cost: 117 - init_stock: 460 - price: 117 - service_level: 0.96 - vlt: 3 - SKU464: - cost: 295 - init_stock: 480 - price: 295 - service_level: 0.96 - vlt: 1 - SKU465: - cost: 286 - init_stock: 1540 - price: 286 - service_level: 0.96 - vlt: 2 - SKU466: - cost: 278 - init_stock: 1060 - price: 278 - service_level: 0.96 - vlt: 2 - SKU467: - cost: 294 - init_stock: 160 - price: 294 - service_level: 0.96 - vlt: 3 - SKU468: - cost: 40 - init_stock: 200 - price: 40 - service_level: 0.96 - vlt: 2 - SKU469: - cost: 481 - init_stock: 2000 - price: 481 - service_level: 0.96 - vlt: 2 - SKU47: - cost: 460 - init_stock: 220 - price: 460 - service_level: 0.96 - vlt: 2 - SKU470: - cost: 470 - init_stock: 1220 - price: 470 - service_level: 0.96 - vlt: 2 - SKU471: - cost: 98 - init_stock: 620 - price: 98 - service_level: 0.96 - vlt: 3 - SKU472: - cost: 22 - init_stock: 1580 - price: 22 - service_level: 0.96 - vlt: 3 - SKU473: - cost: 414 - init_stock: 1640 - price: 414 - service_level: 0.96 - vlt: 3 - SKU474: - cost: 95 - init_stock: 820 - price: 95 - service_level: 0.96 - vlt: 2 - SKU475: - cost: 390 - init_stock: 720 - price: 390 - service_level: 0.96 - vlt: 2 - SKU476: - cost: 468 - init_stock: 1720 - price: 468 - service_level: 0.96 - vlt: 2 - SKU477: - cost: 91 - init_stock: 800 - price: 91 - service_level: 0.96 - vlt: 1 - SKU478: - cost: 242 - init_stock: 1020 - price: 242 - service_level: 0.96 - vlt: 3 - SKU479: - cost: 158 - init_stock: 1000 - price: 158 - service_level: 0.96 - vlt: 2 - SKU48: - cost: 274 - init_stock: 240 - price: 274 - service_level: 0.96 - vlt: 3 - SKU480: - cost: 424 - init_stock: 1360 - price: 424 - service_level: 0.96 - vlt: 1 - SKU481: - cost: 199 - init_stock: 980 - price: 199 - service_level: 0.96 - vlt: 2 - SKU482: - cost: 217 - init_stock: 520 - price: 217 - service_level: 0.96 - vlt: 1 - SKU483: - cost: 120 - init_stock: 240 - price: 120 - service_level: 0.96 - vlt: 1 - SKU484: - cost: 244 - init_stock: 1600 - price: 244 - service_level: 0.96 - vlt: 2 - SKU485: - cost: 266 - init_stock: 1140 - price: 266 - service_level: 0.96 - vlt: 3 - SKU486: - cost: 243 - init_stock: 240 - price: 243 - service_level: 0.96 - vlt: 1 - SKU487: - cost: 275 - init_stock: 1400 - price: 275 - service_level: 0.96 - vlt: 1 - SKU488: - cost: 423 - init_stock: 260 - price: 423 - service_level: 0.96 - vlt: 1 - SKU489: - cost: 241 - init_stock: 760 - price: 241 - service_level: 0.96 - vlt: 3 - SKU49: - cost: 184 - init_stock: 1440 - price: 184 - service_level: 0.96 - vlt: 1 - SKU490: - cost: 285 - init_stock: 320 - price: 285 - service_level: 0.96 - vlt: 3 - SKU491: - cost: 334 - init_stock: 1560 - price: 334 - service_level: 0.96 - vlt: 3 - SKU492: - cost: 371 - init_stock: 1360 - price: 371 - service_level: 0.96 - vlt: 2 - SKU493: - cost: 402 - init_stock: 1900 - price: 402 - service_level: 0.96 - vlt: 3 - SKU494: - cost: 84 - init_stock: 1740 - price: 84 - service_level: 0.96 - vlt: 2 - SKU495: - cost: 347 - init_stock: 740 - price: 347 - service_level: 0.96 - vlt: 2 - SKU496: - cost: 177 - init_stock: 1060 - price: 177 - service_level: 0.96 - vlt: 1 - SKU497: - cost: 260 - init_stock: 620 - price: 260 - service_level: 0.96 - vlt: 3 - SKU498: - cost: 447 - init_stock: 1880 - price: 447 - service_level: 0.96 - vlt: 1 - SKU499: - cost: 425 - init_stock: 1560 - price: 425 - service_level: 0.96 - vlt: 1 - SKU5: - cost: 478 - init_stock: 1480 - price: 478 - service_level: 0.96 - vlt: 2 - SKU50: - cost: 427 - init_stock: 360 - price: 427 - service_level: 0.96 - vlt: 1 - SKU500: - cost: 413 - init_stock: 200 - price: 413 - service_level: 0.96 - vlt: 3 - SKU501: - cost: 74 - init_stock: 1560 - price: 74 - service_level: 0.96 - vlt: 3 - SKU502: - cost: 411 - init_stock: 180 - price: 411 - service_level: 0.96 - vlt: 1 - SKU503: - cost: 459 - init_stock: 880 - price: 459 - service_level: 0.96 - vlt: 2 - SKU504: - cost: 325 - init_stock: 800 - price: 325 - service_level: 0.96 - vlt: 3 - SKU505: - cost: 324 - init_stock: 1440 - price: 324 - service_level: 0.96 - vlt: 1 - SKU506: - cost: 477 - init_stock: 160 - price: 477 - service_level: 0.96 - vlt: 3 - SKU507: - cost: 36 - init_stock: 1420 - price: 36 - service_level: 0.96 - vlt: 1 - SKU508: - cost: 390 - init_stock: 1140 - price: 390 - service_level: 0.96 - vlt: 2 - SKU509: - cost: 464 - init_stock: 1420 - price: 464 - service_level: 0.96 - vlt: 1 - SKU51: - cost: 434 - init_stock: 980 - price: 434 - service_level: 0.96 - vlt: 3 - SKU510: - cost: 233 - init_stock: 1100 - price: 233 - service_level: 0.96 - vlt: 3 - SKU511: - cost: 317 - init_stock: 540 - price: 317 - service_level: 0.96 - vlt: 2 - SKU512: - cost: 405 - init_stock: 1440 - price: 405 - service_level: 0.96 - vlt: 1 - SKU513: - cost: 33 - init_stock: 980 - price: 33 - service_level: 0.96 - vlt: 3 - SKU514: - cost: 441 - init_stock: 480 - price: 441 - service_level: 0.96 - vlt: 3 - SKU515: - cost: 456 - init_stock: 700 - price: 456 - service_level: 0.96 - vlt: 1 - SKU516: - cost: 432 - init_stock: 240 - price: 432 - service_level: 0.96 - vlt: 3 - SKU517: - cost: 223 - init_stock: 1580 - price: 223 - service_level: 0.96 - vlt: 2 - SKU518: - cost: 155 - init_stock: 1540 - price: 155 - service_level: 0.96 - vlt: 3 - SKU519: - cost: 276 - init_stock: 940 - price: 276 - service_level: 0.96 - vlt: 2 - SKU52: - cost: 340 - init_stock: 1980 - price: 340 - service_level: 0.96 - vlt: 3 - SKU520: - cost: 18 - init_stock: 580 - price: 18 - service_level: 0.96 - vlt: 1 - SKU521: - cost: 356 - init_stock: 1820 - price: 356 - service_level: 0.96 - vlt: 2 - SKU522: - cost: 389 - init_stock: 600 - price: 389 - service_level: 0.96 - vlt: 2 - SKU523: - cost: 359 - init_stock: 500 - price: 359 - service_level: 0.96 - vlt: 3 - SKU524: - cost: 213 - init_stock: 780 - price: 213 - service_level: 0.96 - vlt: 1 - SKU525: - cost: 131 - init_stock: 220 - price: 131 - service_level: 0.96 - vlt: 1 - SKU526: - cost: 237 - init_stock: 720 - price: 237 - service_level: 0.96 - vlt: 1 - SKU527: - cost: 161 - init_stock: 240 - price: 161 - service_level: 0.96 - vlt: 3 - SKU528: - cost: 384 - init_stock: 980 - price: 384 - service_level: 0.96 - vlt: 2 - SKU529: - cost: 321 - init_stock: 260 - price: 321 - service_level: 0.96 - vlt: 1 - SKU53: - cost: 20 - init_stock: 840 - price: 20 - service_level: 0.96 - vlt: 2 - SKU530: - cost: 299 - init_stock: 1920 - price: 299 - service_level: 0.96 - vlt: 1 - SKU531: - cost: 335 - init_stock: 1200 - price: 335 - service_level: 0.96 - vlt: 3 - SKU532: - cost: 155 - init_stock: 1760 - price: 155 - service_level: 0.96 - vlt: 1 - SKU533: - cost: 444 - init_stock: 820 - price: 444 - service_level: 0.96 - vlt: 3 - SKU534: - cost: 146 - init_stock: 1940 - price: 146 - service_level: 0.96 - vlt: 1 - SKU535: - cost: 384 - init_stock: 700 - price: 384 - service_level: 0.96 - vlt: 2 - SKU536: - cost: 86 - init_stock: 2000 - price: 86 - service_level: 0.96 - vlt: 1 - SKU537: - cost: 173 - init_stock: 940 - price: 173 - service_level: 0.96 - vlt: 1 - SKU538: - cost: 40 - init_stock: 940 - price: 40 - service_level: 0.96 - vlt: 3 - SKU539: - cost: 390 - init_stock: 200 - price: 390 - service_level: 0.96 - vlt: 2 - SKU54: - cost: 162 - init_stock: 1540 - price: 162 - service_level: 0.96 - vlt: 2 - SKU540: - cost: 290 - init_stock: 960 - price: 290 - service_level: 0.96 - vlt: 1 - SKU541: - cost: 192 - init_stock: 720 - price: 192 - service_level: 0.96 - vlt: 3 - SKU542: - cost: 96 - init_stock: 920 - price: 96 - service_level: 0.96 - vlt: 3 - SKU543: - cost: 197 - init_stock: 800 - price: 197 - service_level: 0.96 - vlt: 3 - SKU544: - cost: 408 - init_stock: 1280 - price: 408 - service_level: 0.96 - vlt: 2 - SKU545: - cost: 431 - init_stock: 1300 - price: 431 - service_level: 0.96 - vlt: 1 - SKU546: - cost: 88 - init_stock: 340 - price: 88 - service_level: 0.96 - vlt: 1 - SKU547: - cost: 454 - init_stock: 1920 - price: 454 - service_level: 0.96 - vlt: 3 - SKU548: - cost: 415 - init_stock: 1820 - price: 415 - service_level: 0.96 - vlt: 2 - SKU549: - cost: 307 - init_stock: 1160 - price: 307 - service_level: 0.96 - vlt: 2 - SKU55: - cost: 106 - init_stock: 420 - price: 106 - service_level: 0.96 - vlt: 3 - SKU550: - cost: 446 - init_stock: 380 - price: 446 - service_level: 0.96 - vlt: 3 - SKU551: - cost: 393 - init_stock: 1200 - price: 393 - service_level: 0.96 - vlt: 2 - SKU552: - cost: 114 - init_stock: 1680 - price: 114 - service_level: 0.96 - vlt: 3 - SKU553: - cost: 71 - init_stock: 700 - price: 71 - service_level: 0.96 - vlt: 3 - SKU554: - cost: 314 - init_stock: 260 - price: 314 - service_level: 0.96 - vlt: 1 - SKU555: - cost: 492 - init_stock: 340 - price: 492 - service_level: 0.96 - vlt: 3 - SKU556: - cost: 96 - init_stock: 1320 - price: 96 - service_level: 0.96 - vlt: 2 - SKU557: - cost: 303 - init_stock: 1440 - price: 303 - service_level: 0.96 - vlt: 3 - SKU558: - cost: 430 - init_stock: 420 - price: 430 - service_level: 0.96 - vlt: 1 - SKU559: - cost: 222 - init_stock: 1180 - price: 222 - service_level: 0.96 - vlt: 1 - SKU56: - cost: 435 - init_stock: 460 - price: 435 - service_level: 0.96 - vlt: 3 - SKU560: - cost: 146 - init_stock: 1780 - price: 146 - service_level: 0.96 - vlt: 2 - SKU561: - cost: 158 - init_stock: 820 - price: 158 - service_level: 0.96 - vlt: 2 - SKU562: - cost: 51 - init_stock: 1480 - price: 51 - service_level: 0.96 - vlt: 1 - SKU563: - cost: 418 - init_stock: 500 - price: 418 - service_level: 0.96 - vlt: 2 - SKU564: - cost: 442 - init_stock: 480 - price: 442 - service_level: 0.96 - vlt: 2 - SKU565: - cost: 310 - init_stock: 1740 - price: 310 - service_level: 0.96 - vlt: 2 - SKU566: - cost: 34 - init_stock: 120 - price: 34 - service_level: 0.96 - vlt: 3 - SKU567: - cost: 120 - init_stock: 860 - price: 120 - service_level: 0.96 - vlt: 2 - SKU568: - cost: 97 - init_stock: 160 - price: 97 - service_level: 0.96 - vlt: 1 - SKU569: - cost: 285 - init_stock: 1880 - price: 285 - service_level: 0.96 - vlt: 1 - SKU57: - cost: 400 - init_stock: 600 - price: 400 - service_level: 0.96 - vlt: 3 - SKU570: - cost: 161 - init_stock: 860 - price: 161 - service_level: 0.96 - vlt: 3 - SKU571: - cost: 50 - init_stock: 1640 - price: 50 - service_level: 0.96 - vlt: 3 - SKU572: - cost: 489 - init_stock: 960 - price: 489 - service_level: 0.96 - vlt: 2 - SKU573: - cost: 274 - init_stock: 740 - price: 274 - service_level: 0.96 - vlt: 2 - SKU574: - cost: 357 - init_stock: 640 - price: 357 - service_level: 0.96 - vlt: 2 - SKU575: - cost: 265 - init_stock: 1960 - price: 265 - service_level: 0.96 - vlt: 2 - SKU576: - cost: 137 - init_stock: 1340 - price: 137 - service_level: 0.96 - vlt: 2 - SKU577: - cost: 420 - init_stock: 1280 - price: 420 - service_level: 0.96 - vlt: 2 - SKU578: - cost: 74 - init_stock: 300 - price: 74 - service_level: 0.96 - vlt: 3 - SKU579: - cost: 310 - init_stock: 1120 - price: 310 - service_level: 0.96 - vlt: 2 - SKU58: - cost: 217 - init_stock: 1620 - price: 217 - service_level: 0.96 - vlt: 1 - SKU580: - cost: 153 - init_stock: 1980 - price: 153 - service_level: 0.96 - vlt: 1 - SKU581: - cost: 494 - init_stock: 1020 - price: 494 - service_level: 0.96 - vlt: 2 - SKU582: - cost: 476 - init_stock: 260 - price: 476 - service_level: 0.96 - vlt: 2 - SKU583: - cost: 105 - init_stock: 220 - price: 105 - service_level: 0.96 - vlt: 1 - SKU584: - cost: 365 - init_stock: 940 - price: 365 - service_level: 0.96 - vlt: 2 - SKU585: - cost: 184 - init_stock: 1080 - price: 184 - service_level: 0.96 - vlt: 2 - SKU586: - cost: 471 - init_stock: 1880 - price: 471 - service_level: 0.96 - vlt: 1 - SKU587: - cost: 197 - init_stock: 1600 - price: 197 - service_level: 0.96 - vlt: 1 - SKU588: - cost: 15 - init_stock: 540 - price: 15 - service_level: 0.96 - vlt: 3 - SKU589: - cost: 185 - init_stock: 500 - price: 185 - service_level: 0.96 - vlt: 3 - SKU59: - cost: 39 - init_stock: 960 - price: 39 - service_level: 0.96 - vlt: 1 - SKU590: - cost: 348 - init_stock: 1240 - price: 348 - service_level: 0.96 - vlt: 2 - SKU591: - cost: 163 - init_stock: 740 - price: 163 - service_level: 0.96 - vlt: 2 - SKU592: - cost: 305 - init_stock: 1840 - price: 305 - service_level: 0.96 - vlt: 1 - SKU593: - cost: 479 - init_stock: 1020 - price: 479 - service_level: 0.96 - vlt: 2 - SKU594: - cost: 478 - init_stock: 1020 - price: 478 - service_level: 0.96 - vlt: 2 - SKU595: - cost: 35 - init_stock: 1100 - price: 35 - service_level: 0.96 - vlt: 1 - SKU596: - cost: 449 - init_stock: 540 - price: 449 - service_level: 0.96 - vlt: 2 - SKU597: - cost: 21 - init_stock: 1920 - price: 21 - service_level: 0.96 - vlt: 3 - SKU598: - cost: 49 - init_stock: 780 - price: 49 - service_level: 0.96 - vlt: 1 - SKU599: - cost: 201 - init_stock: 1520 - price: 201 - service_level: 0.96 - vlt: 3 - SKU6: - cost: 472 - init_stock: 820 - price: 472 - service_level: 0.96 - vlt: 3 - SKU60: - cost: 24 - init_stock: 1700 - price: 24 - service_level: 0.96 - vlt: 2 - SKU600: - cost: 321 - init_stock: 1100 - price: 321 - service_level: 0.96 - vlt: 2 - SKU601: - cost: 185 - init_stock: 200 - price: 185 - service_level: 0.96 - vlt: 1 - SKU602: - cost: 180 - init_stock: 1880 - price: 180 - service_level: 0.96 - vlt: 2 - SKU603: - cost: 328 - init_stock: 1900 - price: 328 - service_level: 0.96 - vlt: 3 - SKU604: - cost: 143 - init_stock: 1920 - price: 143 - service_level: 0.96 - vlt: 2 - SKU605: - cost: 337 - init_stock: 440 - price: 337 - service_level: 0.96 - vlt: 1 - SKU606: - cost: 166 - init_stock: 500 - price: 166 - service_level: 0.96 - vlt: 1 - SKU607: - cost: 286 - init_stock: 1620 - price: 286 - service_level: 0.96 - vlt: 3 - SKU608: - cost: 18 - init_stock: 820 - price: 18 - service_level: 0.96 - vlt: 1 - SKU609: - cost: 330 - init_stock: 1560 - price: 330 - service_level: 0.96 - vlt: 3 - SKU61: - cost: 422 - init_stock: 200 - price: 422 - service_level: 0.96 - vlt: 2 - SKU610: - cost: 489 - init_stock: 760 - price: 489 - service_level: 0.96 - vlt: 2 - SKU611: - cost: 379 - init_stock: 700 - price: 379 - service_level: 0.96 - vlt: 1 - SKU612: - cost: 28 - init_stock: 760 - price: 28 - service_level: 0.96 - vlt: 2 - SKU613: - cost: 303 - init_stock: 980 - price: 303 - service_level: 0.96 - vlt: 2 - SKU614: - cost: 264 - init_stock: 740 - price: 264 - service_level: 0.96 - vlt: 1 - SKU615: - cost: 337 - init_stock: 820 - price: 337 - service_level: 0.96 - vlt: 2 - SKU616: - cost: 309 - init_stock: 1220 - price: 309 - service_level: 0.96 - vlt: 1 - SKU617: - cost: 237 - init_stock: 1040 - price: 237 - service_level: 0.96 - vlt: 2 - SKU618: - cost: 143 - init_stock: 820 - price: 143 - service_level: 0.96 - vlt: 3 - SKU619: - cost: 60 - init_stock: 720 - price: 60 - service_level: 0.96 - vlt: 1 - SKU62: - cost: 92 - init_stock: 820 - price: 92 - service_level: 0.96 - vlt: 2 - SKU620: - cost: 478 - init_stock: 980 - price: 478 - service_level: 0.96 - vlt: 2 - SKU621: - cost: 216 - init_stock: 600 - price: 216 - service_level: 0.96 - vlt: 1 - SKU622: - cost: 270 - init_stock: 480 - price: 270 - service_level: 0.96 - vlt: 3 - SKU623: - cost: 448 - init_stock: 760 - price: 448 - service_level: 0.96 - vlt: 3 - SKU624: - cost: 429 - init_stock: 1100 - price: 429 - service_level: 0.96 - vlt: 1 - SKU625: - cost: 236 - init_stock: 1560 - price: 236 - service_level: 0.96 - vlt: 1 - SKU626: - cost: 135 - init_stock: 640 - price: 135 - service_level: 0.96 - vlt: 1 - SKU627: - cost: 78 - init_stock: 780 - price: 78 - service_level: 0.96 - vlt: 3 - SKU628: - cost: 114 - init_stock: 1760 - price: 114 - service_level: 0.96 - vlt: 3 - SKU629: - cost: 390 - init_stock: 1080 - price: 390 - service_level: 0.96 - vlt: 1 - SKU63: - cost: 341 - init_stock: 1380 - price: 341 - service_level: 0.96 - vlt: 1 - SKU630: - cost: 500 - init_stock: 1680 - price: 500 - service_level: 0.96 - vlt: 1 - SKU631: - cost: 386 - init_stock: 1460 - price: 386 - service_level: 0.96 - vlt: 2 - SKU632: - cost: 72 - init_stock: 680 - price: 72 - service_level: 0.96 - vlt: 3 - SKU633: - cost: 306 - init_stock: 1160 - price: 306 - service_level: 0.96 - vlt: 1 - SKU634: - cost: 136 - init_stock: 1680 - price: 136 - service_level: 0.96 - vlt: 3 - SKU635: - cost: 463 - init_stock: 900 - price: 463 - service_level: 0.96 - vlt: 1 - SKU636: - cost: 243 - init_stock: 1140 - price: 243 - service_level: 0.96 - vlt: 3 - SKU637: - cost: 498 - init_stock: 600 - price: 498 - service_level: 0.96 - vlt: 2 - SKU638: - cost: 93 - init_stock: 1320 - price: 93 - service_level: 0.96 - vlt: 3 - SKU639: - cost: 476 - init_stock: 200 - price: 476 - service_level: 0.96 - vlt: 2 - SKU64: - cost: 151 - init_stock: 1920 - price: 151 - service_level: 0.96 - vlt: 2 - SKU640: - cost: 350 - init_stock: 1120 - price: 350 - service_level: 0.96 - vlt: 3 - SKU641: - cost: 434 - init_stock: 720 - price: 434 - service_level: 0.96 - vlt: 2 - SKU642: - cost: 349 - init_stock: 1440 - price: 349 - service_level: 0.96 - vlt: 3 - SKU643: - cost: 17 - init_stock: 540 - price: 17 - service_level: 0.96 - vlt: 1 - SKU644: - cost: 27 - init_stock: 1860 - price: 27 - service_level: 0.96 - vlt: 1 - SKU645: - cost: 143 - init_stock: 180 - price: 143 - service_level: 0.96 - vlt: 3 - SKU646: - cost: 320 - init_stock: 1740 - price: 320 - service_level: 0.96 - vlt: 2 - SKU647: - cost: 296 - init_stock: 520 - price: 296 - service_level: 0.96 - vlt: 3 - SKU648: - cost: 251 - init_stock: 960 - price: 251 - service_level: 0.96 - vlt: 1 - SKU649: - cost: 171 - init_stock: 1720 - price: 171 - service_level: 0.96 - vlt: 2 - SKU65: - cost: 48 - init_stock: 1600 - price: 48 - service_level: 0.96 - vlt: 1 - SKU650: - cost: 406 - init_stock: 940 - price: 406 - service_level: 0.96 - vlt: 1 - SKU651: - cost: 209 - init_stock: 1420 - price: 209 - service_level: 0.96 - vlt: 3 - SKU652: - cost: 167 - init_stock: 1360 - price: 167 - service_level: 0.96 - vlt: 3 - SKU653: - cost: 122 - init_stock: 120 - price: 122 - service_level: 0.96 - vlt: 3 - SKU654: - cost: 344 - init_stock: 920 - price: 344 - service_level: 0.96 - vlt: 2 - SKU655: - cost: 382 - init_stock: 1780 - price: 382 - service_level: 0.96 - vlt: 3 - SKU656: - cost: 229 - init_stock: 1900 - price: 229 - service_level: 0.96 - vlt: 2 - SKU657: - cost: 474 - init_stock: 1540 - price: 474 - service_level: 0.96 - vlt: 3 - SKU658: - cost: 307 - init_stock: 860 - price: 307 - service_level: 0.96 - vlt: 3 - SKU659: - cost: 392 - init_stock: 500 - price: 392 - service_level: 0.96 - vlt: 3 - SKU66: - cost: 492 - init_stock: 1000 - price: 492 - service_level: 0.96 - vlt: 1 - SKU660: - cost: 206 - init_stock: 240 - price: 206 - service_level: 0.96 - vlt: 3 - SKU661: - cost: 100 - init_stock: 2000 - price: 100 - service_level: 0.96 - vlt: 1 - SKU662: - cost: 283 - init_stock: 120 - price: 283 - service_level: 0.96 - vlt: 1 - SKU663: - cost: 416 - init_stock: 880 - price: 416 - service_level: 0.96 - vlt: 3 - SKU664: - cost: 93 - init_stock: 1400 - price: 93 - service_level: 0.96 - vlt: 2 - SKU665: - cost: 415 - init_stock: 1980 - price: 415 - service_level: 0.96 - vlt: 3 - SKU666: - cost: 378 - init_stock: 100 - price: 378 - service_level: 0.96 - vlt: 3 - SKU667: - cost: 114 - init_stock: 1560 - price: 114 - service_level: 0.96 - vlt: 1 - SKU668: - cost: 115 - init_stock: 320 - price: 115 - service_level: 0.96 - vlt: 1 - SKU669: - cost: 414 - init_stock: 400 - price: 414 - service_level: 0.96 - vlt: 3 - SKU67: - cost: 16 - init_stock: 1240 - price: 16 - service_level: 0.96 - vlt: 3 - SKU670: - cost: 171 - init_stock: 1320 - price: 171 - service_level: 0.96 - vlt: 2 - SKU671: - cost: 163 - init_stock: 780 - price: 163 - service_level: 0.96 - vlt: 3 - SKU672: - cost: 67 - init_stock: 220 - price: 67 - service_level: 0.96 - vlt: 1 - SKU673: - cost: 437 - init_stock: 1480 - price: 437 - service_level: 0.96 - vlt: 3 - SKU674: - cost: 50 - init_stock: 380 - price: 50 - service_level: 0.96 - vlt: 2 - SKU675: - cost: 479 - init_stock: 400 - price: 479 - service_level: 0.96 - vlt: 1 - SKU676: - cost: 460 - init_stock: 980 - price: 460 - service_level: 0.96 - vlt: 2 - SKU677: - cost: 392 - init_stock: 1880 - price: 392 - service_level: 0.96 - vlt: 2 - SKU678: - cost: 143 - init_stock: 1340 - price: 143 - service_level: 0.96 - vlt: 2 - SKU679: - cost: 131 - init_stock: 1640 - price: 131 - service_level: 0.96 - vlt: 3 - SKU68: - cost: 108 - init_stock: 920 - price: 108 - service_level: 0.96 - vlt: 2 - SKU680: - cost: 436 - init_stock: 480 - price: 436 - service_level: 0.96 - vlt: 1 - SKU681: - cost: 338 - init_stock: 260 - price: 338 - service_level: 0.96 - vlt: 3 - SKU682: - cost: 146 - init_stock: 320 - price: 146 - service_level: 0.96 - vlt: 2 - SKU683: - cost: 486 - init_stock: 720 - price: 486 - service_level: 0.96 - vlt: 2 - SKU684: - cost: 370 - init_stock: 1440 - price: 370 - service_level: 0.96 - vlt: 3 - SKU685: - cost: 69 - init_stock: 1540 - price: 69 - service_level: 0.96 - vlt: 1 - SKU686: - cost: 56 - init_stock: 540 - price: 56 - service_level: 0.96 - vlt: 1 - SKU687: - cost: 308 - init_stock: 960 - price: 308 - service_level: 0.96 - vlt: 1 - SKU688: - cost: 200 - init_stock: 1060 - price: 200 - service_level: 0.96 - vlt: 2 - SKU689: - cost: 36 - init_stock: 320 - price: 36 - service_level: 0.96 - vlt: 2 - SKU69: - cost: 329 - init_stock: 560 - price: 329 - service_level: 0.96 - vlt: 1 - SKU690: - cost: 283 - init_stock: 140 - price: 283 - service_level: 0.96 - vlt: 2 - SKU691: - cost: 422 - init_stock: 1960 - price: 422 - service_level: 0.96 - vlt: 1 - SKU692: - cost: 457 - init_stock: 1300 - price: 457 - service_level: 0.96 - vlt: 3 - SKU693: - cost: 351 - init_stock: 960 - price: 351 - service_level: 0.96 - vlt: 1 - SKU694: - cost: 441 - init_stock: 640 - price: 441 - service_level: 0.96 - vlt: 3 - SKU695: - cost: 344 - init_stock: 1960 - price: 344 - service_level: 0.96 - vlt: 2 - SKU696: - cost: 378 - init_stock: 700 - price: 378 - service_level: 0.96 - vlt: 1 - SKU697: - cost: 483 - init_stock: 1840 - price: 483 - service_level: 0.96 - vlt: 1 - SKU698: - cost: 53 - init_stock: 1000 - price: 53 - service_level: 0.96 - vlt: 3 - SKU699: - cost: 265 - init_stock: 1280 - price: 265 - service_level: 0.96 - vlt: 2 - SKU7: - cost: 389 - init_stock: 240 - price: 389 - service_level: 0.96 - vlt: 2 - SKU70: - cost: 140 - init_stock: 760 - price: 140 - service_level: 0.96 - vlt: 2 - SKU700: - cost: 122 - init_stock: 400 - price: 122 - service_level: 0.96 - vlt: 3 - SKU701: - cost: 394 - init_stock: 760 - price: 394 - service_level: 0.96 - vlt: 2 - SKU702: - cost: 78 - init_stock: 540 - price: 78 - service_level: 0.96 - vlt: 1 - SKU703: - cost: 472 - init_stock: 1420 - price: 472 - service_level: 0.96 - vlt: 3 - SKU704: - cost: 384 - init_stock: 880 - price: 384 - service_level: 0.96 - vlt: 2 - SKU705: - cost: 486 - init_stock: 1120 - price: 486 - service_level: 0.96 - vlt: 2 - SKU706: - cost: 222 - init_stock: 740 - price: 222 - service_level: 0.96 - vlt: 1 - SKU707: - cost: 195 - init_stock: 740 - price: 195 - service_level: 0.96 - vlt: 2 - SKU708: - cost: 131 - init_stock: 1980 - price: 131 - service_level: 0.96 - vlt: 1 - SKU709: - cost: 137 - init_stock: 700 - price: 137 - service_level: 0.96 - vlt: 3 - SKU71: - cost: 381 - init_stock: 740 - price: 381 - service_level: 0.96 - vlt: 2 - SKU710: - cost: 186 - init_stock: 280 - price: 186 - service_level: 0.96 - vlt: 3 - SKU711: - cost: 190 - init_stock: 1220 - price: 190 - service_level: 0.96 - vlt: 2 - SKU712: - cost: 384 - init_stock: 720 - price: 384 - service_level: 0.96 - vlt: 2 - SKU713: - cost: 229 - init_stock: 1380 - price: 229 - service_level: 0.96 - vlt: 3 - SKU714: - cost: 364 - init_stock: 1800 - price: 364 - service_level: 0.96 - vlt: 1 - SKU715: - cost: 235 - init_stock: 180 - price: 235 - service_level: 0.96 - vlt: 2 - SKU716: - cost: 108 - init_stock: 1780 - price: 108 - service_level: 0.96 - vlt: 3 - SKU717: - cost: 12 - init_stock: 240 - price: 12 - service_level: 0.96 - vlt: 3 - SKU718: - cost: 397 - init_stock: 440 - price: 397 - service_level: 0.96 - vlt: 3 - SKU719: - cost: 21 - init_stock: 740 - price: 21 - service_level: 0.96 - vlt: 1 - SKU72: - cost: 228 - init_stock: 1940 - price: 228 - service_level: 0.96 - vlt: 1 - SKU720: - cost: 120 - init_stock: 960 - price: 120 - service_level: 0.96 - vlt: 2 - SKU721: - cost: 265 - init_stock: 1660 - price: 265 - service_level: 0.96 - vlt: 1 - SKU722: - cost: 353 - init_stock: 1160 - price: 353 - service_level: 0.96 - vlt: 2 - SKU723: - cost: 144 - init_stock: 1700 - price: 144 - service_level: 0.96 - vlt: 2 - SKU724: - cost: 120 - init_stock: 1420 - price: 120 - service_level: 0.96 - vlt: 1 - SKU725: - cost: 42 - init_stock: 1780 - price: 42 - service_level: 0.96 - vlt: 3 - SKU726: - cost: 132 - init_stock: 1740 - price: 132 - service_level: 0.96 - vlt: 3 - SKU727: - cost: 103 - init_stock: 700 - price: 103 - service_level: 0.96 - vlt: 2 - SKU728: - cost: 29 - init_stock: 680 - price: 29 - service_level: 0.96 - vlt: 3 - SKU729: - cost: 251 - init_stock: 1940 - price: 251 - service_level: 0.96 - vlt: 2 - SKU73: - cost: 334 - init_stock: 1460 - price: 334 - service_level: 0.96 - vlt: 3 - SKU730: - cost: 71 - init_stock: 1280 - price: 71 - service_level: 0.96 - vlt: 3 - SKU731: - cost: 480 - init_stock: 1840 - price: 480 - service_level: 0.96 - vlt: 1 - SKU732: - cost: 231 - init_stock: 1340 - price: 231 - service_level: 0.96 - vlt: 3 - SKU733: - cost: 245 - init_stock: 560 - price: 245 - service_level: 0.96 - vlt: 2 - SKU734: - cost: 117 - init_stock: 1160 - price: 117 - service_level: 0.96 - vlt: 1 - SKU735: - cost: 197 - init_stock: 520 - price: 197 - service_level: 0.96 - vlt: 2 - SKU736: - cost: 221 - init_stock: 1240 - price: 221 - service_level: 0.96 - vlt: 3 - SKU737: - cost: 260 - init_stock: 1700 - price: 260 - service_level: 0.96 - vlt: 2 - SKU738: - cost: 489 - init_stock: 920 - price: 489 - service_level: 0.96 - vlt: 3 - SKU739: - cost: 451 - init_stock: 760 - price: 451 - service_level: 0.96 - vlt: 3 - SKU74: - cost: 363 - init_stock: 200 - price: 363 - service_level: 0.96 - vlt: 3 - SKU740: - cost: 250 - init_stock: 100 - price: 250 - service_level: 0.96 - vlt: 1 - SKU741: - cost: 139 - init_stock: 340 - price: 139 - service_level: 0.96 - vlt: 3 - SKU742: - cost: 174 - init_stock: 1780 - price: 174 - service_level: 0.96 - vlt: 3 - SKU743: - cost: 79 - init_stock: 1640 - price: 79 - service_level: 0.96 - vlt: 3 - SKU744: - cost: 304 - init_stock: 780 - price: 304 - service_level: 0.96 - vlt: 3 - SKU745: - cost: 105 - init_stock: 1860 - price: 105 - service_level: 0.96 - vlt: 2 - SKU746: - cost: 397 - init_stock: 1880 - price: 397 - service_level: 0.96 - vlt: 1 - SKU747: - cost: 426 - init_stock: 720 - price: 426 - service_level: 0.96 - vlt: 1 - SKU748: - cost: 169 - init_stock: 940 - price: 169 - service_level: 0.96 - vlt: 3 - SKU749: - cost: 433 - init_stock: 840 - price: 433 - service_level: 0.96 - vlt: 2 - SKU75: - cost: 179 - init_stock: 1300 - price: 179 - service_level: 0.96 - vlt: 1 - SKU750: - cost: 373 - init_stock: 1360 - price: 373 - service_level: 0.96 - vlt: 2 - SKU751: - cost: 196 - init_stock: 680 - price: 196 - service_level: 0.96 - vlt: 2 - SKU752: - cost: 67 - init_stock: 200 - price: 67 - service_level: 0.96 - vlt: 1 - SKU753: - cost: 162 - init_stock: 1220 - price: 162 - service_level: 0.96 - vlt: 1 - SKU754: - cost: 206 - init_stock: 1280 - price: 206 - service_level: 0.96 - vlt: 1 - SKU755: - cost: 125 - init_stock: 860 - price: 125 - service_level: 0.96 - vlt: 1 - SKU756: - cost: 215 - init_stock: 1640 - price: 215 - service_level: 0.96 - vlt: 1 - SKU757: - cost: 263 - init_stock: 440 - price: 263 - service_level: 0.96 - vlt: 3 - SKU758: - cost: 70 - init_stock: 1740 - price: 70 - service_level: 0.96 - vlt: 3 - SKU759: - cost: 310 - init_stock: 1580 - price: 310 - service_level: 0.96 - vlt: 3 - SKU76: - cost: 464 - init_stock: 1680 - price: 464 - service_level: 0.96 - vlt: 3 - SKU760: - cost: 487 - init_stock: 440 - price: 487 - service_level: 0.96 - vlt: 2 - SKU761: - cost: 288 - init_stock: 620 - price: 288 - service_level: 0.96 - vlt: 2 - SKU762: - cost: 374 - init_stock: 1240 - price: 374 - service_level: 0.96 - vlt: 1 - SKU763: - cost: 121 - init_stock: 120 - price: 121 - service_level: 0.96 - vlt: 1 - SKU764: - cost: 494 - init_stock: 760 - price: 494 - service_level: 0.96 - vlt: 1 - SKU765: - cost: 459 - init_stock: 1780 - price: 459 - service_level: 0.96 - vlt: 1 - SKU766: - cost: 249 - init_stock: 1080 - price: 249 - service_level: 0.96 - vlt: 1 - SKU767: - cost: 245 - init_stock: 1760 - price: 245 - service_level: 0.96 - vlt: 1 - SKU768: - cost: 429 - init_stock: 1700 - price: 429 - service_level: 0.96 - vlt: 3 - SKU769: - cost: 324 - init_stock: 960 - price: 324 - service_level: 0.96 - vlt: 2 - SKU77: - cost: 281 - init_stock: 560 - price: 281 - service_level: 0.96 - vlt: 3 - SKU770: - cost: 362 - init_stock: 1520 - price: 362 - service_level: 0.96 - vlt: 1 - SKU771: - cost: 233 - init_stock: 540 - price: 233 - service_level: 0.96 - vlt: 1 - SKU772: - cost: 152 - init_stock: 320 - price: 152 - service_level: 0.96 - vlt: 1 - SKU773: - cost: 337 - init_stock: 1920 - price: 337 - service_level: 0.96 - vlt: 3 - SKU774: - cost: 232 - init_stock: 180 - price: 232 - service_level: 0.96 - vlt: 3 - SKU775: - cost: 393 - init_stock: 240 - price: 393 - service_level: 0.96 - vlt: 3 - SKU776: - cost: 342 - init_stock: 960 - price: 342 - service_level: 0.96 - vlt: 3 - SKU777: - cost: 262 - init_stock: 1180 - price: 262 - service_level: 0.96 - vlt: 2 - SKU778: - cost: 327 - init_stock: 1480 - price: 327 - service_level: 0.96 - vlt: 2 - SKU779: - cost: 109 - init_stock: 880 - price: 109 - service_level: 0.96 - vlt: 3 - SKU78: - cost: 486 - init_stock: 740 - price: 486 - service_level: 0.96 - vlt: 3 - SKU780: - cost: 153 - init_stock: 500 - price: 153 - service_level: 0.96 - vlt: 1 - SKU781: - cost: 126 - init_stock: 140 - price: 126 - service_level: 0.96 - vlt: 1 - SKU782: - cost: 258 - init_stock: 1840 - price: 258 - service_level: 0.96 - vlt: 2 - SKU783: - cost: 214 - init_stock: 1400 - price: 214 - service_level: 0.96 - vlt: 1 - SKU784: - cost: 426 - init_stock: 700 - price: 426 - service_level: 0.96 - vlt: 2 - SKU785: - cost: 486 - init_stock: 580 - price: 486 - service_level: 0.96 - vlt: 3 - SKU786: - cost: 109 - init_stock: 1040 - price: 109 - service_level: 0.96 - vlt: 1 - SKU787: - cost: 74 - init_stock: 420 - price: 74 - service_level: 0.96 - vlt: 1 - SKU788: - cost: 25 - init_stock: 840 - price: 25 - service_level: 0.96 - vlt: 2 - SKU789: - cost: 265 - init_stock: 780 - price: 265 - service_level: 0.96 - vlt: 2 - SKU79: - cost: 486 - init_stock: 840 - price: 486 - service_level: 0.96 - vlt: 3 - SKU790: - cost: 222 - init_stock: 560 - price: 222 - service_level: 0.96 - vlt: 1 - SKU791: - cost: 495 - init_stock: 1080 - price: 495 - service_level: 0.96 - vlt: 3 - SKU792: - cost: 410 - init_stock: 640 - price: 410 - service_level: 0.96 - vlt: 3 - SKU793: - cost: 442 - init_stock: 1600 - price: 442 - service_level: 0.96 - vlt: 1 - SKU794: - cost: 222 - init_stock: 120 - price: 222 - service_level: 0.96 - vlt: 3 - SKU795: - cost: 279 - init_stock: 1160 - price: 279 - service_level: 0.96 - vlt: 2 - SKU796: - cost: 122 - init_stock: 520 - price: 122 - service_level: 0.96 - vlt: 2 - SKU797: - cost: 107 - init_stock: 2000 - price: 107 - service_level: 0.96 - vlt: 1 - SKU798: - cost: 383 - init_stock: 1000 - price: 383 - service_level: 0.96 - vlt: 3 - SKU799: - cost: 119 - init_stock: 700 - price: 119 - service_level: 0.96 - vlt: 3 - SKU8: - cost: 423 - init_stock: 1700 - price: 423 - service_level: 0.96 - vlt: 2 - SKU80: - cost: 324 - init_stock: 720 - price: 324 - service_level: 0.96 - vlt: 3 - SKU800: - cost: 316 - init_stock: 1780 - price: 316 - service_level: 0.96 - vlt: 3 - SKU801: - cost: 65 - init_stock: 1680 - price: 65 - service_level: 0.96 - vlt: 3 - SKU802: - cost: 133 - init_stock: 1880 - price: 133 - service_level: 0.96 - vlt: 2 - SKU803: - cost: 209 - init_stock: 700 - price: 209 - service_level: 0.96 - vlt: 3 - SKU804: - cost: 273 - init_stock: 1700 - price: 273 - service_level: 0.96 - vlt: 3 - SKU805: - cost: 304 - init_stock: 760 - price: 304 - service_level: 0.96 - vlt: 2 - SKU806: - cost: 232 - init_stock: 180 - price: 232 - service_level: 0.96 - vlt: 2 - SKU807: - cost: 169 - init_stock: 1740 - price: 169 - service_level: 0.96 - vlt: 3 - SKU808: - cost: 350 - init_stock: 180 - price: 350 - service_level: 0.96 - vlt: 2 - SKU809: - cost: 85 - init_stock: 1940 - price: 85 - service_level: 0.96 - vlt: 1 - SKU81: - cost: 319 - init_stock: 1200 - price: 319 - service_level: 0.96 - vlt: 3 - SKU810: - cost: 51 - init_stock: 1020 - price: 51 - service_level: 0.96 - vlt: 3 - SKU811: - cost: 53 - init_stock: 1080 - price: 53 - service_level: 0.96 - vlt: 1 - SKU812: - cost: 141 - init_stock: 1140 - price: 141 - service_level: 0.96 - vlt: 1 - SKU813: - cost: 364 - init_stock: 780 - price: 364 - service_level: 0.96 - vlt: 2 - SKU814: - cost: 79 - init_stock: 1920 - price: 79 - service_level: 0.96 - vlt: 3 - SKU815: - cost: 178 - init_stock: 320 - price: 178 - service_level: 0.96 - vlt: 2 - SKU816: - cost: 352 - init_stock: 1320 - price: 352 - service_level: 0.96 - vlt: 2 - SKU817: - cost: 119 - init_stock: 340 - price: 119 - service_level: 0.96 - vlt: 1 - SKU818: - cost: 110 - init_stock: 260 - price: 110 - service_level: 0.96 - vlt: 3 - SKU819: - cost: 24 - init_stock: 100 - price: 24 - service_level: 0.96 - vlt: 3 - SKU82: - cost: 413 - init_stock: 1400 - price: 413 - service_level: 0.96 - vlt: 3 - SKU820: - cost: 426 - init_stock: 1040 - price: 426 - service_level: 0.96 - vlt: 1 - SKU821: - cost: 229 - init_stock: 1840 - price: 229 - service_level: 0.96 - vlt: 1 - SKU822: - cost: 282 - init_stock: 240 - price: 282 - service_level: 0.96 - vlt: 1 - SKU823: - cost: 221 - init_stock: 1820 - price: 221 - service_level: 0.96 - vlt: 2 - SKU824: - cost: 318 - init_stock: 720 - price: 318 - service_level: 0.96 - vlt: 3 - SKU825: - cost: 287 - init_stock: 1860 - price: 287 - service_level: 0.96 - vlt: 1 - SKU826: - cost: 278 - init_stock: 960 - price: 278 - service_level: 0.96 - vlt: 2 - SKU827: - cost: 312 - init_stock: 1260 - price: 312 - service_level: 0.96 - vlt: 2 - SKU828: - cost: 140 - init_stock: 1860 - price: 140 - service_level: 0.96 - vlt: 3 - SKU829: - cost: 415 - init_stock: 220 - price: 415 - service_level: 0.96 - vlt: 3 - SKU83: - cost: 327 - init_stock: 1540 - price: 327 - service_level: 0.96 - vlt: 3 - SKU830: - cost: 119 - init_stock: 460 - price: 119 - service_level: 0.96 - vlt: 3 - SKU831: - cost: 376 - init_stock: 1400 - price: 376 - service_level: 0.96 - vlt: 3 - SKU832: - cost: 289 - init_stock: 920 - price: 289 - service_level: 0.96 - vlt: 2 - SKU833: - cost: 183 - init_stock: 320 - price: 183 - service_level: 0.96 - vlt: 2 - SKU834: - cost: 398 - init_stock: 1320 - price: 398 - service_level: 0.96 - vlt: 2 - SKU835: - cost: 407 - init_stock: 1480 - price: 407 - service_level: 0.96 - vlt: 1 - SKU836: - cost: 301 - init_stock: 820 - price: 301 - service_level: 0.96 - vlt: 2 - SKU837: - cost: 213 - init_stock: 520 - price: 213 - service_level: 0.96 - vlt: 3 - SKU838: - cost: 23 - init_stock: 840 - price: 23 - service_level: 0.96 - vlt: 2 - SKU839: - cost: 354 - init_stock: 1060 - price: 354 - service_level: 0.96 - vlt: 1 - SKU84: - cost: 217 - init_stock: 640 - price: 217 - service_level: 0.96 - vlt: 1 - SKU840: - cost: 19 - init_stock: 1740 - price: 19 - service_level: 0.96 - vlt: 2 - SKU841: - cost: 362 - init_stock: 1880 - price: 362 - service_level: 0.96 - vlt: 2 - SKU842: - cost: 73 - init_stock: 1820 - price: 73 - service_level: 0.96 - vlt: 2 - SKU843: - cost: 169 - init_stock: 860 - price: 169 - service_level: 0.96 - vlt: 1 - SKU844: - cost: 325 - init_stock: 720 - price: 325 - service_level: 0.96 - vlt: 2 - SKU845: - cost: 163 - init_stock: 300 - price: 163 - service_level: 0.96 - vlt: 3 - SKU846: - cost: 184 - init_stock: 1960 - price: 184 - service_level: 0.96 - vlt: 2 - SKU847: - cost: 441 - init_stock: 680 - price: 441 - service_level: 0.96 - vlt: 1 - SKU848: - cost: 450 - init_stock: 500 - price: 450 - service_level: 0.96 - vlt: 3 - SKU849: - cost: 479 - init_stock: 700 - price: 479 - service_level: 0.96 - vlt: 3 - SKU85: - cost: 49 - init_stock: 1520 - price: 49 - service_level: 0.96 - vlt: 1 - SKU850: - cost: 105 - init_stock: 1460 - price: 105 - service_level: 0.96 - vlt: 3 - SKU851: - cost: 366 - init_stock: 1540 - price: 366 - service_level: 0.96 - vlt: 3 - SKU852: - cost: 392 - init_stock: 1560 - price: 392 - service_level: 0.96 - vlt: 3 - SKU853: - cost: 94 - init_stock: 200 - price: 94 - service_level: 0.96 - vlt: 2 - SKU854: - cost: 110 - init_stock: 1300 - price: 110 - service_level: 0.96 - vlt: 2 - SKU855: - cost: 254 - init_stock: 820 - price: 254 - service_level: 0.96 - vlt: 2 - SKU856: - cost: 435 - init_stock: 580 - price: 435 - service_level: 0.96 - vlt: 2 - SKU857: - cost: 101 - init_stock: 480 - price: 101 - service_level: 0.96 - vlt: 3 - SKU858: - cost: 487 - init_stock: 560 - price: 487 - service_level: 0.96 - vlt: 3 - SKU859: - cost: 496 - init_stock: 1860 - price: 496 - service_level: 0.96 - vlt: 1 - SKU86: - cost: 59 - init_stock: 1500 - price: 59 - service_level: 0.96 - vlt: 1 - SKU860: - cost: 350 - init_stock: 2000 - price: 350 - service_level: 0.96 - vlt: 2 - SKU861: - cost: 295 - init_stock: 500 - price: 295 - service_level: 0.96 - vlt: 2 - SKU862: - cost: 57 - init_stock: 1460 - price: 57 - service_level: 0.96 - vlt: 2 - SKU863: - cost: 311 - init_stock: 1140 - price: 311 - service_level: 0.96 - vlt: 3 - SKU864: - cost: 96 - init_stock: 1280 - price: 96 - service_level: 0.96 - vlt: 2 - SKU865: - cost: 107 - init_stock: 1160 - price: 107 - service_level: 0.96 - vlt: 2 - SKU866: - cost: 397 - init_stock: 420 - price: 397 - service_level: 0.96 - vlt: 3 - SKU867: - cost: 478 - init_stock: 1280 - price: 478 - service_level: 0.96 - vlt: 3 - SKU868: - cost: 343 - init_stock: 420 - price: 343 - service_level: 0.96 - vlt: 2 - SKU869: - cost: 168 - init_stock: 1220 - price: 168 - service_level: 0.96 - vlt: 3 - SKU87: - cost: 93 - init_stock: 1420 - price: 93 - service_level: 0.96 - vlt: 1 - SKU870: - cost: 257 - init_stock: 1260 - price: 257 - service_level: 0.96 - vlt: 3 - SKU871: - cost: 68 - init_stock: 400 - price: 68 - service_level: 0.96 - vlt: 3 - SKU872: - cost: 475 - init_stock: 1420 - price: 475 - service_level: 0.96 - vlt: 1 - SKU873: - cost: 388 - init_stock: 1300 - price: 388 - service_level: 0.96 - vlt: 3 - SKU874: - cost: 267 - init_stock: 120 - price: 267 - service_level: 0.96 - vlt: 2 - SKU875: - cost: 332 - init_stock: 700 - price: 332 - service_level: 0.96 - vlt: 2 - SKU876: - cost: 392 - init_stock: 800 - price: 392 - service_level: 0.96 - vlt: 3 - SKU877: - cost: 374 - init_stock: 1820 - price: 374 - service_level: 0.96 - vlt: 2 - SKU878: - cost: 297 - init_stock: 580 - price: 297 - service_level: 0.96 - vlt: 3 - SKU879: - cost: 497 - init_stock: 1560 - price: 497 - service_level: 0.96 - vlt: 3 - SKU88: - cost: 19 - init_stock: 840 - price: 19 - service_level: 0.96 - vlt: 2 - SKU880: - cost: 199 - init_stock: 940 - price: 199 - service_level: 0.96 - vlt: 1 - SKU881: - cost: 328 - init_stock: 1200 - price: 328 - service_level: 0.96 - vlt: 1 - SKU882: - cost: 307 - init_stock: 1800 - price: 307 - service_level: 0.96 - vlt: 2 - SKU883: - cost: 16 - init_stock: 560 - price: 16 - service_level: 0.96 - vlt: 3 - SKU884: - cost: 329 - init_stock: 1340 - price: 329 - service_level: 0.96 - vlt: 2 - SKU885: - cost: 176 - init_stock: 1420 - price: 176 - service_level: 0.96 - vlt: 2 - SKU886: - cost: 48 - init_stock: 1780 - price: 48 - service_level: 0.96 - vlt: 2 - SKU887: - cost: 241 - init_stock: 300 - price: 241 - service_level: 0.96 - vlt: 1 - SKU888: - cost: 114 - init_stock: 1060 - price: 114 - service_level: 0.96 - vlt: 3 - SKU889: - cost: 261 - init_stock: 1900 - price: 261 - service_level: 0.96 - vlt: 1 - SKU89: - cost: 134 - init_stock: 1620 - price: 134 - service_level: 0.96 - vlt: 2 - SKU890: - cost: 322 - init_stock: 1420 - price: 322 - service_level: 0.96 - vlt: 2 - SKU891: - cost: 347 - init_stock: 1140 - price: 347 - service_level: 0.96 - vlt: 1 - SKU892: - cost: 474 - init_stock: 1680 - price: 474 - service_level: 0.96 - vlt: 1 - SKU893: - cost: 433 - init_stock: 1960 - price: 433 - service_level: 0.96 - vlt: 1 - SKU894: - cost: 399 - init_stock: 520 - price: 399 - service_level: 0.96 - vlt: 2 - SKU895: - cost: 335 - init_stock: 960 - price: 335 - service_level: 0.96 - vlt: 3 - SKU896: - cost: 92 - init_stock: 1100 - price: 92 - service_level: 0.96 - vlt: 3 - SKU897: - cost: 404 - init_stock: 440 - price: 404 - service_level: 0.96 - vlt: 2 - SKU898: - cost: 72 - init_stock: 1960 - price: 72 - service_level: 0.96 - vlt: 3 - SKU899: - cost: 91 - init_stock: 1780 - price: 91 - service_level: 0.96 - vlt: 2 - SKU9: - cost: 231 - init_stock: 420 - price: 231 - service_level: 0.96 - vlt: 2 - SKU90: - cost: 38 - init_stock: 600 - price: 38 - service_level: 0.96 - vlt: 1 - SKU900: - cost: 12 - init_stock: 1180 - price: 12 - service_level: 0.96 - vlt: 2 - SKU901: - cost: 307 - init_stock: 1560 - price: 307 - service_level: 0.96 - vlt: 3 - SKU902: - cost: 354 - init_stock: 1120 - price: 354 - service_level: 0.96 - vlt: 1 - SKU903: - cost: 14 - init_stock: 1860 - price: 14 - service_level: 0.96 - vlt: 3 - SKU904: - cost: 300 - init_stock: 1940 - price: 300 - service_level: 0.96 - vlt: 1 - SKU905: - cost: 134 - init_stock: 1020 - price: 134 - service_level: 0.96 - vlt: 2 - SKU906: - cost: 424 - init_stock: 1760 - price: 424 - service_level: 0.96 - vlt: 3 - SKU907: - cost: 360 - init_stock: 640 - price: 360 - service_level: 0.96 - vlt: 2 - SKU908: - cost: 361 - init_stock: 780 - price: 361 - service_level: 0.96 - vlt: 2 - SKU909: - cost: 65 - init_stock: 1380 - price: 65 - service_level: 0.96 - vlt: 3 - SKU91: - cost: 223 - init_stock: 860 - price: 223 - service_level: 0.96 - vlt: 1 - SKU910: - cost: 161 - init_stock: 200 - price: 161 - service_level: 0.96 - vlt: 3 - SKU911: - cost: 186 - init_stock: 1000 - price: 186 - service_level: 0.96 - vlt: 3 - SKU912: - cost: 235 - init_stock: 1080 - price: 235 - service_level: 0.96 - vlt: 1 - SKU913: - cost: 122 - init_stock: 1360 - price: 122 - service_level: 0.96 - vlt: 2 - SKU914: - cost: 436 - init_stock: 320 - price: 436 - service_level: 0.96 - vlt: 2 - SKU915: - cost: 233 - init_stock: 460 - price: 233 - service_level: 0.96 - vlt: 2 - SKU916: - cost: 26 - init_stock: 980 - price: 26 - service_level: 0.96 - vlt: 3 - SKU917: - cost: 141 - init_stock: 1960 - price: 141 - service_level: 0.96 - vlt: 3 - SKU918: - cost: 113 - init_stock: 1780 - price: 113 - service_level: 0.96 - vlt: 3 - SKU919: - cost: 68 - init_stock: 1740 - price: 68 - service_level: 0.96 - vlt: 3 - SKU92: - cost: 325 - init_stock: 760 - price: 325 - service_level: 0.96 - vlt: 1 - SKU920: - cost: 428 - init_stock: 420 - price: 428 - service_level: 0.96 - vlt: 1 - SKU921: - cost: 491 - init_stock: 1520 - price: 491 - service_level: 0.96 - vlt: 2 - SKU922: - cost: 10 - init_stock: 1020 - price: 10 - service_level: 0.96 - vlt: 1 - SKU923: - cost: 80 - init_stock: 200 - price: 80 - service_level: 0.96 - vlt: 1 - SKU924: - cost: 164 - init_stock: 1520 - price: 164 - service_level: 0.96 - vlt: 3 - SKU925: - cost: 221 - init_stock: 1220 - price: 221 - service_level: 0.96 - vlt: 3 - SKU926: - cost: 300 - init_stock: 140 - price: 300 - service_level: 0.96 - vlt: 1 - SKU927: - cost: 128 - init_stock: 1260 - price: 128 - service_level: 0.96 - vlt: 2 - SKU928: - cost: 126 - init_stock: 160 - price: 126 - service_level: 0.96 - vlt: 1 - SKU929: - cost: 377 - init_stock: 300 - price: 377 - service_level: 0.96 - vlt: 1 - SKU93: - cost: 441 - init_stock: 180 - price: 441 - service_level: 0.96 - vlt: 2 - SKU930: - cost: 270 - init_stock: 1980 - price: 270 - service_level: 0.96 - vlt: 1 - SKU931: - cost: 412 - init_stock: 1300 - price: 412 - service_level: 0.96 - vlt: 3 - SKU932: - cost: 118 - init_stock: 960 - price: 118 - service_level: 0.96 - vlt: 3 - SKU933: - cost: 54 - init_stock: 1820 - price: 54 - service_level: 0.96 - vlt: 1 - SKU934: - cost: 347 - init_stock: 780 - price: 347 - service_level: 0.96 - vlt: 3 - SKU935: - cost: 219 - init_stock: 340 - price: 219 - service_level: 0.96 - vlt: 1 - SKU936: - cost: 283 - init_stock: 1540 - price: 283 - service_level: 0.96 - vlt: 1 - SKU937: - cost: 457 - init_stock: 1460 - price: 457 - service_level: 0.96 - vlt: 1 - SKU938: - cost: 450 - init_stock: 1800 - price: 450 - service_level: 0.96 - vlt: 2 - SKU939: - cost: 67 - init_stock: 540 - price: 67 - service_level: 0.96 - vlt: 1 - SKU94: - cost: 279 - init_stock: 340 - price: 279 - service_level: 0.96 - vlt: 1 - SKU940: - cost: 65 - init_stock: 580 - price: 65 - service_level: 0.96 - vlt: 1 - SKU941: - cost: 213 - init_stock: 100 - price: 213 - service_level: 0.96 - vlt: 3 - SKU942: - cost: 151 - init_stock: 540 - price: 151 - service_level: 0.96 - vlt: 3 - SKU943: - cost: 85 - init_stock: 1000 - price: 85 - service_level: 0.96 - vlt: 3 - SKU944: - cost: 154 - init_stock: 120 - price: 154 - service_level: 0.96 - vlt: 3 - SKU945: - cost: 267 - init_stock: 300 - price: 267 - service_level: 0.96 - vlt: 3 - SKU946: - cost: 397 - init_stock: 180 - price: 397 - service_level: 0.96 - vlt: 3 - SKU947: - cost: 275 - init_stock: 820 - price: 275 - service_level: 0.96 - vlt: 2 - SKU948: - cost: 472 - init_stock: 1760 - price: 472 - service_level: 0.96 - vlt: 3 - SKU949: - cost: 275 - init_stock: 1960 - price: 275 - service_level: 0.96 - vlt: 1 - SKU95: - cost: 183 - init_stock: 1020 - price: 183 - service_level: 0.96 - vlt: 1 - SKU950: - cost: 374 - init_stock: 500 - price: 374 - service_level: 0.96 - vlt: 3 - SKU951: - cost: 272 - init_stock: 640 - price: 272 - service_level: 0.96 - vlt: 3 - SKU952: - cost: 194 - init_stock: 1260 - price: 194 - service_level: 0.96 - vlt: 2 - SKU953: - cost: 428 - init_stock: 200 - price: 428 - service_level: 0.96 - vlt: 2 - SKU954: - cost: 272 - init_stock: 320 - price: 272 - service_level: 0.96 - vlt: 1 - SKU955: - cost: 84 - init_stock: 1840 - price: 84 - service_level: 0.96 - vlt: 1 - SKU956: - cost: 363 - init_stock: 700 - price: 363 - service_level: 0.96 - vlt: 2 - SKU957: - cost: 221 - init_stock: 1800 - price: 221 - service_level: 0.96 - vlt: 1 - SKU958: - cost: 335 - init_stock: 720 - price: 335 - service_level: 0.96 - vlt: 1 - SKU959: - cost: 427 - init_stock: 1420 - price: 427 - service_level: 0.96 - vlt: 3 - SKU96: - cost: 320 - init_stock: 220 - price: 320 - service_level: 0.96 - vlt: 1 - SKU960: - cost: 187 - init_stock: 1540 - price: 187 - service_level: 0.96 - vlt: 1 - SKU961: - cost: 90 - init_stock: 1320 - price: 90 - service_level: 0.96 - vlt: 1 - SKU962: - cost: 70 - init_stock: 1960 - price: 70 - service_level: 0.96 - vlt: 1 - SKU963: - cost: 184 - init_stock: 740 - price: 184 - service_level: 0.96 - vlt: 3 - SKU964: - cost: 180 - init_stock: 200 - price: 180 - service_level: 0.96 - vlt: 2 - SKU965: - cost: 441 - init_stock: 320 - price: 441 - service_level: 0.96 - vlt: 3 - SKU966: - cost: 214 - init_stock: 640 - price: 214 - service_level: 0.96 - vlt: 2 - SKU967: - cost: 71 - init_stock: 1240 - price: 71 - service_level: 0.96 - vlt: 3 - SKU968: - cost: 142 - init_stock: 100 - price: 142 - service_level: 0.96 - vlt: 1 - SKU969: - cost: 435 - init_stock: 1160 - price: 435 - service_level: 0.96 - vlt: 2 - SKU97: - cost: 15 - init_stock: 540 - price: 15 - service_level: 0.96 - vlt: 2 - SKU970: - cost: 392 - init_stock: 1840 - price: 392 - service_level: 0.96 - vlt: 1 - SKU971: - cost: 408 - init_stock: 260 - price: 408 - service_level: 0.96 - vlt: 2 - SKU972: - cost: 22 - init_stock: 400 - price: 22 - service_level: 0.96 - vlt: 3 - SKU973: - cost: 476 - init_stock: 1780 - price: 476 - service_level: 0.96 - vlt: 3 - SKU974: - cost: 356 - init_stock: 1000 - price: 356 - service_level: 0.96 - vlt: 2 - SKU975: - cost: 299 - init_stock: 340 - price: 299 - service_level: 0.96 - vlt: 3 - SKU976: - cost: 369 - init_stock: 700 - price: 369 - service_level: 0.96 - vlt: 1 - SKU977: - cost: 142 - init_stock: 1060 - price: 142 - service_level: 0.96 - vlt: 1 - SKU978: - cost: 242 - init_stock: 1960 - price: 242 - service_level: 0.96 - vlt: 1 - SKU979: - cost: 219 - init_stock: 1980 - price: 219 - service_level: 0.96 - vlt: 1 - SKU98: - cost: 167 - init_stock: 1560 - price: 167 - service_level: 0.96 - vlt: 2 - SKU980: - cost: 363 - init_stock: 1640 - price: 363 - service_level: 0.96 - vlt: 1 - SKU981: - cost: 416 - init_stock: 1680 - price: 416 - service_level: 0.96 - vlt: 3 - SKU982: - cost: 163 - init_stock: 460 - price: 163 - service_level: 0.96 - vlt: 3 - SKU983: - cost: 151 - init_stock: 1200 - price: 151 - service_level: 0.96 - vlt: 2 - SKU984: - cost: 123 - init_stock: 1680 - price: 123 - service_level: 0.96 - vlt: 3 - SKU985: - cost: 114 - init_stock: 820 - price: 114 - service_level: 0.96 - vlt: 1 - SKU986: - cost: 107 - init_stock: 1520 - price: 107 - service_level: 0.96 - vlt: 3 - SKU987: - cost: 56 - init_stock: 1860 - price: 56 - service_level: 0.96 - vlt: 3 - SKU988: - cost: 66 - init_stock: 780 - price: 66 - service_level: 0.96 - vlt: 3 - SKU989: - cost: 472 - init_stock: 1340 - price: 472 - service_level: 0.96 - vlt: 2 - SKU99: - cost: 31 - init_stock: 1780 - price: 31 - service_level: 0.96 - vlt: 2 - SKU990: - cost: 14 - init_stock: 940 - price: 14 - service_level: 0.96 - vlt: 3 - SKU991: - cost: 450 - init_stock: 980 - price: 450 - service_level: 0.96 - vlt: 2 - SKU992: - cost: 18 - init_stock: 1220 - price: 18 - service_level: 0.96 - vlt: 3 - SKU993: - cost: 350 - init_stock: 940 - price: 350 - service_level: 0.96 - vlt: 1 - SKU994: - cost: 45 - init_stock: 1820 - price: 45 - service_level: 0.96 - vlt: 2 - SKU995: - cost: 406 - init_stock: 1720 - price: 406 - service_level: 0.96 - vlt: 1 - SKU996: - cost: 472 - init_stock: 960 - price: 472 - service_level: 0.96 - vlt: 3 - SKU997: - cost: 348 - init_stock: 540 - price: 348 - service_level: 0.96 - vlt: 2 - SKU998: - cost: 239 - init_stock: 300 - price: 239 - service_level: 0.96 - vlt: 1 - SKU999: - cost: 435 - init_stock: 2000 - price: 435 - service_level: 0.96 - vlt: 2 - - children: - storage: - config: - capacity: 1027820 - unit_storage_cost: 1 - config: - order_cost: 500 - definition_ref: RetailerFacility - name: STORE0 - skus: - SKU0: - constraint: G(stock_constraint) - cost: 405 - init_stock: 340 - max_stock: 1000 - price: 498 - sale_gamma: 85 - service_level: 0.95 - SKU1: - constraint: null - cost: 42 - init_stock: 126 - max_stock: 1000 - price: 49 - sale_gamma: 42 - service_level: 0.95 - SKU10: - constraint: null - cost: 125 - init_stock: 273 - max_stock: 1000 - price: 191 - sale_gamma: 91 - service_level: 0.95 - SKU100: - constraint: G(low_profit -> low_stock_constraint) - cost: 338 - init_stock: 552 - max_stock: 1000 - price: 469 - sale_gamma: 92 - service_level: 0.95 - SKU101: - constraint: G(low_profit -> low_stock_constraint) - cost: 436 - init_stock: 276 - max_stock: 1000 - price: 536 - sale_gamma: 69 - service_level: 0.95 - SKU102: - constraint: null - cost: 90 - init_stock: 335 - max_stock: 1000 - price: 119 - sale_gamma: 67 - service_level: 0.95 - SKU103: - constraint: null - cost: 161 - init_stock: 324 - max_stock: 1000 - price: 251 - sale_gamma: 81 - service_level: 0.95 - SKU104: - constraint: G(stock_constraint) - cost: 470 - init_stock: 240 - max_stock: 1000 - price: 531 - sale_gamma: 40 - service_level: 0.95 - SKU105: - constraint: G(stock_constraint) - cost: 214 - init_stock: 162 - max_stock: 1000 - price: 316 - sale_gamma: 54 - service_level: 0.95 - SKU106: - constraint: null - cost: 37 - init_stock: 255 - max_stock: 1000 - price: 49 - sale_gamma: 51 - service_level: 0.95 - SKU107: - constraint: null - cost: 64 - init_stock: 305 - max_stock: 1000 - price: 81 - sale_gamma: 61 - service_level: 0.95 - SKU108: - constraint: G(low_profit -> low_stock_constraint) - cost: 474 - init_stock: 180 - max_stock: 1000 - price: 639 - sale_gamma: 30 - service_level: 0.95 - SKU109: - constraint: null - cost: 398 - init_stock: 255 - max_stock: 1000 - price: 537 - sale_gamma: 51 - service_level: 0.95 - SKU11: - constraint: null - cost: 106 - init_stock: 440 - max_stock: 1000 - price: 184 - sale_gamma: 88 - service_level: 0.95 - SKU110: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 71 - init_stock: 200 - max_stock: 1000 - price: 79 - sale_gamma: 25 - service_level: 0.95 - SKU111: - constraint: null - cost: 183 - init_stock: 250 - max_stock: 1000 - price: 223 - sale_gamma: 50 - service_level: 0.95 - SKU112: - constraint: null - cost: 78 - init_stock: 33 - max_stock: 1000 - price: 114 - sale_gamma: 11 - service_level: 0.95 - SKU113: - constraint: null - cost: 289 - init_stock: 318 - max_stock: 1000 - price: 338 - sale_gamma: 53 - service_level: 0.95 - SKU114: - constraint: null - cost: 333 - init_stock: 325 - max_stock: 1000 - price: 509 - sale_gamma: 65 - service_level: 0.95 - SKU115: - constraint: G(low_profit -> low_stock_constraint) - cost: 437 - init_stock: 75 - max_stock: 1000 - price: 777 - sale_gamma: 25 - service_level: 0.95 - SKU116: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 408 - max_stock: 1000 - price: 353 - sale_gamma: 68 - service_level: 0.95 - SKU117: - constraint: G(stock_constraint) - cost: 123 - init_stock: 132 - max_stock: 1000 - price: 233 - sale_gamma: 22 - service_level: 0.95 - SKU118: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 181 - init_stock: 196 - max_stock: 1000 - price: 289 - sale_gamma: 49 - service_level: 0.95 - SKU119: - constraint: null - cost: 58 - init_stock: 20 - max_stock: 1000 - price: 70 - sale_gamma: 5 - service_level: 0.95 - SKU12: - constraint: null - cost: 315 - init_stock: 304 - max_stock: 1000 - price: 441 - sale_gamma: 76 - service_level: 0.95 - SKU120: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 96 - max_stock: 1000 - price: 43 - sale_gamma: 12 - service_level: 0.95 - SKU121: - constraint: G(stock_constraint) - cost: 255 - init_stock: 469 - max_stock: 1000 - price: 433 - sale_gamma: 67 - service_level: 0.95 - SKU122: - constraint: null - cost: 222 - init_stock: 148 - max_stock: 1000 - price: 326 - sale_gamma: 37 - service_level: 0.95 - SKU123: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 405 - init_stock: 372 - max_stock: 1000 - price: 797 - sale_gamma: 62 - service_level: 0.95 - SKU124: - constraint: null - cost: 169 - init_stock: 72 - max_stock: 1000 - price: 327 - sale_gamma: 18 - service_level: 0.95 - SKU125: - constraint: G(low_profit -> low_stock_constraint) - cost: 344 - init_stock: 140 - max_stock: 1000 - price: 646 - sale_gamma: 35 - service_level: 0.95 - SKU126: - constraint: G(stock_constraint) - cost: 362 - init_stock: 188 - max_stock: 1000 - price: 658 - sale_gamma: 94 - service_level: 0.95 - SKU127: - constraint: G(stock_constraint) - cost: 149 - init_stock: 124 - max_stock: 1000 - price: 289 - sale_gamma: 31 - service_level: 0.95 - SKU128: - constraint: null - cost: 215 - init_stock: 522 - max_stock: 1000 - price: 242 - sale_gamma: 87 - service_level: 0.95 - SKU129: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 83 - init_stock: 371 - max_stock: 1000 - price: 93 - sale_gamma: 53 - service_level: 0.95 - SKU13: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 474 - init_stock: 135 - max_stock: 1000 - price: 810 - sale_gamma: 45 - service_level: 0.95 - SKU130: - constraint: null - cost: 427 - init_stock: 445 - max_stock: 1000 - price: 623 - sale_gamma: 89 - service_level: 0.95 - SKU131: - constraint: null - cost: 58 - init_stock: 294 - max_stock: 1000 - price: 104 - sale_gamma: 49 - service_level: 0.95 - SKU132: - constraint: G(low_profit -> low_stock_constraint) - cost: 304 - init_stock: 90 - max_stock: 1000 - price: 376 - sale_gamma: 15 - service_level: 0.95 - SKU133: - constraint: null - cost: 161 - init_stock: 87 - max_stock: 1000 - price: 252 - sale_gamma: 29 - service_level: 0.95 - SKU134: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 382 - init_stock: 370 - max_stock: 1000 - price: 446 - sale_gamma: 74 - service_level: 0.95 - SKU135: - constraint: null - cost: 126 - init_stock: 616 - max_stock: 1000 - price: 209 - sale_gamma: 88 - service_level: 0.95 - SKU136: - constraint: null - cost: 431 - init_stock: 49 - max_stock: 1000 - price: 659 - sale_gamma: 7 - service_level: 0.95 - SKU137: - constraint: null - cost: 270 - init_stock: 110 - max_stock: 1000 - price: 448 - sale_gamma: 22 - service_level: 0.95 - SKU138: - constraint: G(low_profit -> low_stock_constraint) - cost: 10 - init_stock: 252 - max_stock: 1000 - price: 11 - sale_gamma: 63 - service_level: 0.95 - SKU139: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 259 - init_stock: 574 - max_stock: 1000 - price: 292 - sale_gamma: 82 - service_level: 0.95 - SKU14: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 728 - max_stock: 1000 - price: 307 - sale_gamma: 91 - service_level: 0.95 - SKU140: - constraint: null - cost: 367 - init_stock: 330 - max_stock: 1000 - price: 554 - sale_gamma: 55 - service_level: 0.95 - SKU141: - constraint: null - cost: 325 - init_stock: 50 - max_stock: 1000 - price: 500 - sale_gamma: 10 - service_level: 0.95 - SKU142: - constraint: null - cost: 439 - init_stock: 567 - max_stock: 1000 - price: 583 - sale_gamma: 81 - service_level: 0.95 - SKU143: - constraint: G(stock_constraint) - cost: 260 - init_stock: 207 - max_stock: 1000 - price: 364 - sale_gamma: 69 - service_level: 0.95 - SKU144: - constraint: null - cost: 457 - init_stock: 30 - max_stock: 1000 - price: 525 - sale_gamma: 6 - service_level: 0.95 - SKU145: - constraint: G(stock_constraint) - cost: 480 - init_stock: 450 - max_stock: 1000 - price: 686 - sale_gamma: 75 - service_level: 0.95 - SKU146: - constraint: null - cost: 243 - init_stock: 576 - max_stock: 1000 - price: 396 - sale_gamma: 96 - service_level: 0.95 - SKU147: - constraint: null - cost: 334 - init_stock: 518 - max_stock: 1000 - price: 534 - sale_gamma: 74 - service_level: 0.95 - SKU148: - constraint: null - cost: 143 - init_stock: 200 - max_stock: 1000 - price: 250 - sale_gamma: 40 - service_level: 0.95 - SKU149: - constraint: G(low_profit -> low_stock_constraint) - cost: 252 - init_stock: 204 - max_stock: 1000 - price: 423 - sale_gamma: 51 - service_level: 0.95 - SKU15: - constraint: G(stock_constraint) - cost: 320 - init_stock: 144 - max_stock: 1000 - price: 470 - sale_gamma: 36 - service_level: 0.95 - SKU150: - constraint: G(stock_constraint) - cost: 83 - init_stock: 310 - max_stock: 1000 - price: 106 - sale_gamma: 62 - service_level: 0.95 - SKU151: - constraint: null - cost: 58 - init_stock: 175 - max_stock: 1000 - price: 64 - sale_gamma: 25 - service_level: 0.95 - SKU152: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 254 - init_stock: 210 - max_stock: 1000 - price: 373 - sale_gamma: 35 - service_level: 0.95 - SKU153: - constraint: null - cost: 49 - init_stock: 216 - max_stock: 1000 - price: 63 - sale_gamma: 72 - service_level: 0.95 - SKU154: - constraint: null - cost: 391 - init_stock: 70 - max_stock: 1000 - price: 680 - sale_gamma: 10 - service_level: 0.95 - SKU155: - constraint: G(stock_constraint) - cost: 36 - init_stock: 138 - max_stock: 1000 - price: 68 - sale_gamma: 23 - service_level: 0.95 - SKU156: - constraint: G(stock_constraint) - cost: 140 - init_stock: 296 - max_stock: 1000 - price: 219 - sale_gamma: 37 - service_level: 0.95 - SKU157: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 374 - init_stock: 195 - max_stock: 1000 - price: 489 - sale_gamma: 39 - service_level: 0.95 - SKU158: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 135 - max_stock: 1000 - price: 274 - sale_gamma: 27 - service_level: 0.95 - SKU159: - constraint: G(stock_constraint) - cost: 428 - init_stock: 160 - max_stock: 1000 - price: 701 - sale_gamma: 40 - service_level: 0.95 - SKU16: - constraint: null - cost: 277 - init_stock: 497 - max_stock: 1000 - price: 357 - sale_gamma: 71 - service_level: 0.95 - SKU160: - constraint: null - cost: 129 - init_stock: 312 - max_stock: 1000 - price: 247 - sale_gamma: 78 - service_level: 0.95 - SKU161: - constraint: G(stock_constraint) - cost: 32 - init_stock: 480 - max_stock: 1000 - price: 58 - sale_gamma: 96 - service_level: 0.95 - SKU162: - constraint: null - cost: 68 - init_stock: 366 - max_stock: 1000 - price: 99 - sale_gamma: 61 - service_level: 0.95 - SKU163: - constraint: G(low_profit -> low_stock_constraint) - cost: 129 - init_stock: 240 - max_stock: 1000 - price: 167 - sale_gamma: 48 - service_level: 0.95 - SKU164: - constraint: null - cost: 192 - init_stock: 136 - max_stock: 1000 - price: 320 - sale_gamma: 68 - service_level: 0.95 - SKU165: - constraint: G(stock_constraint) - cost: 316 - init_stock: 124 - max_stock: 1000 - price: 527 - sale_gamma: 62 - service_level: 0.95 - SKU166: - constraint: G(stock_constraint) - cost: 454 - init_stock: 14 - max_stock: 1000 - price: 867 - sale_gamma: 7 - service_level: 0.95 - SKU167: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 108 - init_stock: 497 - max_stock: 1000 - price: 170 - sale_gamma: 71 - service_level: 0.95 - SKU168: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 431 - init_stock: 210 - max_stock: 1000 - price: 642 - sale_gamma: 30 - service_level: 0.95 - SKU169: - constraint: G(low_profit -> low_stock_constraint) - cost: 428 - init_stock: 432 - max_stock: 1000 - price: 706 - sale_gamma: 72 - service_level: 0.95 - SKU17: - constraint: G(low_profit -> low_stock_constraint) - cost: 393 - init_stock: 160 - max_stock: 1000 - price: 471 - sale_gamma: 40 - service_level: 0.95 - SKU170: - constraint: null - cost: 107 - init_stock: 98 - max_stock: 1000 - price: 176 - sale_gamma: 14 - service_level: 0.95 - SKU171: - constraint: null - cost: 14 - init_stock: 665 - max_stock: 1000 - price: 21 - sale_gamma: 95 - service_level: 0.95 - SKU172: - constraint: null - cost: 291 - init_stock: 344 - max_stock: 1000 - price: 538 - sale_gamma: 86 - service_level: 0.95 - SKU173: - constraint: G(low_profit -> low_stock_constraint) - cost: 190 - init_stock: 75 - max_stock: 1000 - price: 286 - sale_gamma: 15 - service_level: 0.95 - SKU174: - constraint: G(stock_constraint) - cost: 431 - init_stock: 104 - max_stock: 1000 - price: 612 - sale_gamma: 26 - service_level: 0.95 - SKU175: - constraint: G(stock_constraint) - cost: 355 - init_stock: 348 - max_stock: 1000 - price: 578 - sale_gamma: 87 - service_level: 0.95 - SKU176: - constraint: null - cost: 305 - init_stock: 196 - max_stock: 1000 - price: 524 - sale_gamma: 49 - service_level: 0.95 - SKU177: - constraint: G(low_profit -> low_stock_constraint) - cost: 291 - init_stock: 115 - max_stock: 1000 - price: 477 - sale_gamma: 23 - service_level: 0.95 - SKU178: - constraint: null - cost: 417 - init_stock: 120 - max_stock: 1000 - price: 704 - sale_gamma: 20 - service_level: 0.95 - SKU179: - constraint: G(low_profit -> low_stock_constraint) - cost: 18 - init_stock: 427 - max_stock: 1000 - price: 28 - sale_gamma: 61 - service_level: 0.95 - SKU18: - constraint: null - cost: 223 - init_stock: 312 - max_stock: 1000 - price: 399 - sale_gamma: 52 - service_level: 0.95 - SKU180: - constraint: G(stock_constraint) - cost: 373 - init_stock: 222 - max_stock: 1000 - price: 712 - sale_gamma: 37 - service_level: 0.95 - SKU181: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 486 - init_stock: 464 - max_stock: 1000 - price: 865 - sale_gamma: 58 - service_level: 0.95 - SKU182: - constraint: null - cost: 385 - init_stock: 238 - max_stock: 1000 - price: 735 - sale_gamma: 34 - service_level: 0.95 - SKU183: - constraint: G(low_profit -> low_stock_constraint) - cost: 195 - init_stock: 560 - max_stock: 1000 - price: 234 - sale_gamma: 80 - service_level: 0.95 - SKU184: - constraint: null - cost: 238 - init_stock: 20 - max_stock: 1000 - price: 402 - sale_gamma: 5 - service_level: 0.95 - SKU185: - constraint: null - cost: 50 - init_stock: 135 - max_stock: 1000 - price: 89 - sale_gamma: 45 - service_level: 0.95 - SKU186: - constraint: null - cost: 143 - init_stock: 376 - max_stock: 1000 - price: 264 - sale_gamma: 94 - service_level: 0.95 - SKU187: - constraint: G(stock_constraint) - cost: 285 - init_stock: 228 - max_stock: 1000 - price: 495 - sale_gamma: 38 - service_level: 0.95 - SKU188: - constraint: null - cost: 180 - init_stock: 165 - max_stock: 1000 - price: 198 - sale_gamma: 33 - service_level: 0.95 - SKU189: - constraint: null - cost: 294 - init_stock: 126 - max_stock: 1000 - price: 438 - sale_gamma: 18 - service_level: 0.95 - SKU19: - constraint: null - cost: 301 - init_stock: 168 - max_stock: 1000 - price: 352 - sale_gamma: 28 - service_level: 0.95 - SKU190: - constraint: null - cost: 392 - init_stock: 222 - max_stock: 1000 - price: 756 - sale_gamma: 37 - service_level: 0.95 - SKU191: - constraint: null - cost: 124 - init_stock: 68 - max_stock: 1000 - price: 193 - sale_gamma: 17 - service_level: 0.95 - SKU192: - constraint: null - cost: 118 - init_stock: 185 - max_stock: 1000 - price: 177 - sale_gamma: 37 - service_level: 0.95 - SKU193: - constraint: G(stock_constraint) - cost: 168 - init_stock: 410 - max_stock: 1000 - price: 304 - sale_gamma: 82 - service_level: 0.95 - SKU194: - constraint: G(stock_constraint) - cost: 331 - init_stock: 216 - max_stock: 1000 - price: 562 - sale_gamma: 36 - service_level: 0.95 - SKU195: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 81 - init_stock: 132 - max_stock: 1000 - price: 155 - sale_gamma: 33 - service_level: 0.95 - SKU196: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 94 - init_stock: 72 - max_stock: 1000 - price: 112 - sale_gamma: 12 - service_level: 0.95 - SKU197: - constraint: null - cost: 165 - init_stock: 210 - max_stock: 1000 - price: 206 - sale_gamma: 42 - service_level: 0.95 - SKU198: - constraint: G(stock_constraint) - cost: 454 - init_stock: 546 - max_stock: 1000 - price: 699 - sale_gamma: 78 - service_level: 0.95 - SKU199: - constraint: null - cost: 182 - init_stock: 222 - max_stock: 1000 - price: 307 - sale_gamma: 37 - service_level: 0.95 - SKU2: - constraint: G(low_profit -> low_stock_constraint) - cost: 60 - init_stock: 228 - max_stock: 1000 - price: 99 - sale_gamma: 57 - service_level: 0.95 - SKU20: - constraint: G(stock_constraint) - cost: 124 - init_stock: 252 - max_stock: 1000 - price: 243 - sale_gamma: 84 - service_level: 0.95 - SKU200: - constraint: G(low_profit -> low_stock_constraint) - cost: 355 - init_stock: 354 - max_stock: 1000 - price: 507 - sale_gamma: 59 - service_level: 0.95 - SKU201: - constraint: null - cost: 263 - init_stock: 322 - max_stock: 1000 - price: 373 - sale_gamma: 46 - service_level: 0.95 - SKU202: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 316 - init_stock: 126 - max_stock: 1000 - price: 423 - sale_gamma: 21 - service_level: 0.95 - SKU203: - constraint: null - cost: 145 - init_stock: 18 - max_stock: 1000 - price: 221 - sale_gamma: 6 - service_level: 0.95 - SKU204: - constraint: null - cost: 28 - init_stock: 240 - max_stock: 1000 - price: 48 - sale_gamma: 30 - service_level: 0.95 - SKU205: - constraint: null - cost: 190 - init_stock: 475 - max_stock: 1000 - price: 271 - sale_gamma: 95 - service_level: 0.95 - SKU206: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 496 - max_stock: 1000 - price: 439 - sale_gamma: 62 - service_level: 0.95 - SKU207: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 322 - init_stock: 276 - max_stock: 1000 - price: 421 - sale_gamma: 69 - service_level: 0.95 - SKU208: - constraint: null - cost: 287 - init_stock: 64 - max_stock: 1000 - price: 384 - sale_gamma: 16 - service_level: 0.95 - SKU209: - constraint: G(low_profit -> low_stock_constraint) - cost: 278 - init_stock: 68 - max_stock: 1000 - price: 556 - sale_gamma: 17 - service_level: 0.95 - SKU21: - constraint: G(stock_constraint) - cost: 408 - init_stock: 54 - max_stock: 1000 - price: 644 - sale_gamma: 18 - service_level: 0.95 - SKU210: - constraint: null - cost: 322 - init_stock: 164 - max_stock: 1000 - price: 540 - sale_gamma: 41 - service_level: 0.95 - SKU211: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 456 - init_stock: 260 - max_stock: 1000 - price: 579 - sale_gamma: 52 - service_level: 0.95 - SKU212: - constraint: null - cost: 248 - init_stock: 68 - max_stock: 1000 - price: 394 - sale_gamma: 17 - service_level: 0.95 - SKU213: - constraint: null - cost: 287 - init_stock: 40 - max_stock: 1000 - price: 450 - sale_gamma: 5 - service_level: 0.95 - SKU214: - constraint: G(low_profit -> low_stock_constraint) - cost: 89 - init_stock: 252 - max_stock: 1000 - price: 127 - sale_gamma: 63 - service_level: 0.95 - SKU215: - constraint: null - cost: 177 - init_stock: 576 - max_stock: 1000 - price: 341 - sale_gamma: 96 - service_level: 0.95 - SKU216: - constraint: G(low_profit -> low_stock_constraint) - cost: 232 - init_stock: 623 - max_stock: 1000 - price: 424 - sale_gamma: 89 - service_level: 0.95 - SKU217: - constraint: null - cost: 32 - init_stock: 510 - max_stock: 1000 - price: 52 - sale_gamma: 85 - service_level: 0.95 - SKU218: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 126 - max_stock: 1000 - price: 57 - sale_gamma: 18 - service_level: 0.95 - SKU219: - constraint: G(low_profit -> low_stock_constraint) - cost: 497 - init_stock: 230 - max_stock: 1000 - price: 919 - sale_gamma: 46 - service_level: 0.95 - SKU22: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 497 - init_stock: 180 - max_stock: 1000 - price: 954 - sale_gamma: 30 - service_level: 0.95 - SKU220: - constraint: null - cost: 436 - init_stock: 264 - max_stock: 1000 - price: 745 - sale_gamma: 44 - service_level: 0.95 - SKU221: - constraint: G(low_profit -> low_stock_constraint) - cost: 58 - init_stock: 518 - max_stock: 1000 - price: 107 - sale_gamma: 74 - service_level: 0.95 - SKU222: - constraint: G(low_profit -> low_stock_constraint) - cost: 174 - init_stock: 56 - max_stock: 1000 - price: 220 - sale_gamma: 14 - service_level: 0.95 - SKU223: - constraint: null - cost: 140 - init_stock: 322 - max_stock: 1000 - price: 169 - sale_gamma: 46 - service_level: 0.95 - SKU224: - constraint: null - cost: 246 - init_stock: 162 - max_stock: 1000 - price: 455 - sale_gamma: 27 - service_level: 0.95 - SKU225: - constraint: G(stock_constraint) - cost: 17 - init_stock: 318 - max_stock: 1000 - price: 31 - sale_gamma: 53 - service_level: 0.95 - SKU226: - constraint: null - cost: 81 - init_stock: 224 - max_stock: 1000 - price: 119 - sale_gamma: 56 - service_level: 0.95 - SKU227: - constraint: null - cost: 475 - init_stock: 180 - max_stock: 1000 - price: 650 - sale_gamma: 30 - service_level: 0.95 - SKU228: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 390 - max_stock: 1000 - price: 438 - sale_gamma: 65 - service_level: 0.95 - SKU229: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 465 - init_stock: 245 - max_stock: 1000 - price: 799 - sale_gamma: 49 - service_level: 0.95 - SKU23: - constraint: null - cost: 301 - init_stock: 204 - max_stock: 1000 - price: 553 - sale_gamma: 68 - service_level: 0.95 - SKU230: - constraint: null - cost: 483 - init_stock: 140 - max_stock: 1000 - price: 632 - sale_gamma: 35 - service_level: 0.95 - SKU231: - constraint: null - cost: 173 - init_stock: 65 - max_stock: 1000 - price: 257 - sale_gamma: 13 - service_level: 0.95 - SKU232: - constraint: G(low_profit -> low_stock_constraint) - cost: 315 - init_stock: 154 - max_stock: 1000 - price: 368 - sale_gamma: 22 - service_level: 0.95 - SKU233: - constraint: G(stock_constraint) - cost: 57 - init_stock: 15 - max_stock: 1000 - price: 94 - sale_gamma: 5 - service_level: 0.95 - SKU234: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 315 - init_stock: 276 - max_stock: 1000 - price: 349 - sale_gamma: 69 - service_level: 0.95 - SKU235: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 92 - init_stock: 48 - max_stock: 1000 - price: 115 - sale_gamma: 8 - service_level: 0.95 - SKU236: - constraint: G(low_profit -> low_stock_constraint) - cost: 125 - init_stock: 54 - max_stock: 1000 - price: 227 - sale_gamma: 18 - service_level: 0.95 - SKU237: - constraint: null - cost: 201 - init_stock: 63 - max_stock: 1000 - price: 335 - sale_gamma: 21 - service_level: 0.95 - SKU238: - constraint: null - cost: 105 - init_stock: 114 - max_stock: 1000 - price: 155 - sale_gamma: 38 - service_level: 0.95 - SKU239: - constraint: null - cost: 66 - init_stock: 258 - max_stock: 1000 - price: 83 - sale_gamma: 86 - service_level: 0.95 - SKU24: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 99 - init_stock: 420 - max_stock: 1000 - price: 188 - sale_gamma: 70 - service_level: 0.95 - SKU240: - constraint: null - cost: 262 - init_stock: 122 - max_stock: 1000 - price: 301 - sale_gamma: 61 - service_level: 0.95 - SKU241: - constraint: G(low_profit -> low_stock_constraint) - cost: 361 - init_stock: 448 - max_stock: 1000 - price: 534 - sale_gamma: 64 - service_level: 0.95 - SKU242: - constraint: G(low_profit -> low_stock_constraint) - cost: 221 - init_stock: 352 - max_stock: 1000 - price: 322 - sale_gamma: 88 - service_level: 0.95 - SKU243: - constraint: null - cost: 141 - init_stock: 372 - max_stock: 1000 - price: 222 - sale_gamma: 62 - service_level: 0.95 - SKU244: - constraint: G(stock_constraint) - cost: 193 - init_stock: 306 - max_stock: 1000 - price: 264 - sale_gamma: 51 - service_level: 0.95 - SKU245: - constraint: null - cost: 165 - init_stock: 165 - max_stock: 1000 - price: 287 - sale_gamma: 55 - service_level: 0.95 - SKU246: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 474 - init_stock: 48 - max_stock: 1000 - price: 734 - sale_gamma: 16 - service_level: 0.95 - SKU247: - constraint: G(stock_constraint) - cost: 66 - init_stock: 180 - max_stock: 1000 - price: 114 - sale_gamma: 90 - service_level: 0.95 - SKU248: - constraint: null - cost: 136 - init_stock: 52 - max_stock: 1000 - price: 221 - sale_gamma: 13 - service_level: 0.95 - SKU249: - constraint: null - cost: 83 - init_stock: 180 - max_stock: 1000 - price: 165 - sale_gamma: 60 - service_level: 0.95 - SKU25: - constraint: G(stock_constraint) - cost: 11 - init_stock: 165 - max_stock: 1000 - price: 21 - sale_gamma: 55 - service_level: 0.95 - SKU250: - constraint: null - cost: 310 - init_stock: 595 - max_stock: 1000 - price: 480 - sale_gamma: 85 - service_level: 0.95 - SKU251: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 247 - init_stock: 176 - max_stock: 1000 - price: 489 - sale_gamma: 44 - service_level: 0.95 - SKU252: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 192 - init_stock: 288 - max_stock: 1000 - price: 336 - sale_gamma: 48 - service_level: 0.95 - SKU253: - constraint: null - cost: 270 - init_stock: 240 - max_stock: 1000 - price: 475 - sale_gamma: 60 - service_level: 0.95 - SKU254: - constraint: G(stock_constraint) - cost: 165 - init_stock: 396 - max_stock: 1000 - price: 290 - sale_gamma: 99 - service_level: 0.95 - SKU255: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 49 - init_stock: 324 - max_stock: 1000 - price: 67 - sale_gamma: 54 - service_level: 0.95 - SKU256: - constraint: null - cost: 314 - init_stock: 516 - max_stock: 1000 - price: 439 - sale_gamma: 86 - service_level: 0.95 - SKU257: - constraint: null - cost: 468 - init_stock: 168 - max_stock: 1000 - price: 631 - sale_gamma: 24 - service_level: 0.95 - SKU258: - constraint: G(low_profit -> low_stock_constraint) - cost: 151 - init_stock: 70 - max_stock: 1000 - price: 292 - sale_gamma: 10 - service_level: 0.95 - SKU259: - constraint: null - cost: 389 - init_stock: 95 - max_stock: 1000 - price: 626 - sale_gamma: 19 - service_level: 0.95 - SKU26: - constraint: null - cost: 348 - init_stock: 222 - max_stock: 1000 - price: 685 - sale_gamma: 37 - service_level: 0.95 - SKU260: - constraint: G(stock_constraint) - cost: 90 - init_stock: 170 - max_stock: 1000 - price: 125 - sale_gamma: 85 - service_level: 0.95 - SKU261: - constraint: null - cost: 122 - init_stock: 375 - max_stock: 1000 - price: 207 - sale_gamma: 75 - service_level: 0.95 - SKU262: - constraint: null - cost: 15 - init_stock: 108 - max_stock: 1000 - price: 18 - sale_gamma: 18 - service_level: 0.95 - SKU263: - constraint: G(stock_constraint) - cost: 350 - init_stock: 234 - max_stock: 1000 - price: 539 - sale_gamma: 39 - service_level: 0.95 - SKU264: - constraint: null - cost: 10 - init_stock: 186 - max_stock: 1000 - price: 11 - sale_gamma: 31 - service_level: 0.95 - SKU265: - constraint: null - cost: 81 - init_stock: 95 - max_stock: 1000 - price: 135 - sale_gamma: 19 - service_level: 0.95 - SKU266: - constraint: null - cost: 276 - init_stock: 475 - max_stock: 1000 - price: 414 - sale_gamma: 95 - service_level: 0.95 - SKU267: - constraint: null - cost: 123 - init_stock: 168 - max_stock: 1000 - price: 199 - sale_gamma: 42 - service_level: 0.95 - SKU268: - constraint: null - cost: 64 - init_stock: 272 - max_stock: 1000 - price: 90 - sale_gamma: 34 - service_level: 0.95 - SKU269: - constraint: null - cost: 435 - init_stock: 40 - max_stock: 1000 - price: 574 - sale_gamma: 10 - service_level: 0.95 - SKU27: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 196 - init_stock: 285 - max_stock: 1000 - price: 248 - sale_gamma: 57 - service_level: 0.95 - SKU270: - constraint: G(stock_constraint) - cost: 68 - init_stock: 290 - max_stock: 1000 - price: 121 - sale_gamma: 58 - service_level: 0.95 - SKU271: - constraint: G(stock_constraint) - cost: 14 - init_stock: 48 - max_stock: 1000 - price: 22 - sale_gamma: 8 - service_level: 0.95 - SKU272: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 249 - init_stock: 415 - max_stock: 1000 - price: 478 - sale_gamma: 83 - service_level: 0.95 - SKU273: - constraint: null - cost: 39 - init_stock: 140 - max_stock: 1000 - price: 78 - sale_gamma: 20 - service_level: 0.95 - SKU274: - constraint: null - cost: 436 - init_stock: 63 - max_stock: 1000 - price: 592 - sale_gamma: 9 - service_level: 0.95 - SKU275: - constraint: G(stock_constraint) - cost: 62 - init_stock: 372 - max_stock: 1000 - price: 75 - sale_gamma: 93 - service_level: 0.95 - SKU276: - constraint: G(stock_constraint) - cost: 281 - init_stock: 225 - max_stock: 1000 - price: 435 - sale_gamma: 75 - service_level: 0.95 - SKU277: - constraint: G(low_profit -> low_stock_constraint) - cost: 442 - init_stock: 546 - max_stock: 1000 - price: 693 - sale_gamma: 91 - service_level: 0.95 - SKU278: - constraint: null - cost: 290 - init_stock: 420 - max_stock: 1000 - price: 490 - sale_gamma: 84 - service_level: 0.95 - SKU279: - constraint: G(low_profit -> low_stock_constraint) - cost: 175 - init_stock: 177 - max_stock: 1000 - price: 252 - sale_gamma: 59 - service_level: 0.95 - SKU28: - constraint: null - cost: 61 - init_stock: 72 - max_stock: 1000 - price: 119 - sale_gamma: 36 - service_level: 0.95 - SKU280: - constraint: null - cost: 445 - init_stock: 410 - max_stock: 1000 - price: 565 - sale_gamma: 82 - service_level: 0.95 - SKU281: - constraint: null - cost: 49 - init_stock: 35 - max_stock: 1000 - price: 60 - sale_gamma: 7 - service_level: 0.95 - SKU282: - constraint: G(stock_constraint) - cost: 499 - init_stock: 155 - max_stock: 1000 - price: 898 - sale_gamma: 31 - service_level: 0.95 - SKU283: - constraint: null - cost: 310 - init_stock: 126 - max_stock: 1000 - price: 607 - sale_gamma: 42 - service_level: 0.95 - SKU284: - constraint: G(low_profit -> low_stock_constraint) - cost: 381 - init_stock: 252 - max_stock: 1000 - price: 701 - sale_gamma: 42 - service_level: 0.95 - SKU285: - constraint: G(stock_constraint) - cost: 378 - init_stock: 105 - max_stock: 1000 - price: 627 - sale_gamma: 35 - service_level: 0.95 - SKU286: - constraint: G(low_profit -> low_stock_constraint) - cost: 366 - init_stock: 534 - max_stock: 1000 - price: 706 - sale_gamma: 89 - service_level: 0.95 - SKU287: - constraint: G(stock_constraint) - cost: 287 - init_stock: 402 - max_stock: 1000 - price: 378 - sale_gamma: 67 - service_level: 0.95 - SKU288: - constraint: null - cost: 24 - init_stock: 72 - max_stock: 1000 - price: 43 - sale_gamma: 12 - service_level: 0.95 - SKU289: - constraint: null - cost: 439 - init_stock: 297 - max_stock: 1000 - price: 698 - sale_gamma: 99 - service_level: 0.95 - SKU29: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 490 - init_stock: 216 - max_stock: 1000 - price: 862 - sale_gamma: 72 - service_level: 0.95 - SKU290: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 11 - init_stock: 385 - max_stock: 1000 - price: 13 - sale_gamma: 77 - service_level: 0.95 - SKU291: - constraint: G(stock_constraint) - cost: 259 - init_stock: 90 - max_stock: 1000 - price: 321 - sale_gamma: 18 - service_level: 0.95 - SKU292: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 389 - init_stock: 800 - max_stock: 1000 - price: 427 - sale_gamma: 100 - service_level: 0.95 - SKU293: - constraint: null - cost: 246 - init_stock: 336 - max_stock: 1000 - price: 314 - sale_gamma: 56 - service_level: 0.95 - SKU294: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 76 - init_stock: 91 - max_stock: 1000 - price: 133 - sale_gamma: 13 - service_level: 0.95 - SKU295: - constraint: G(low_profit -> low_stock_constraint) - cost: 22 - init_stock: 688 - max_stock: 1000 - price: 35 - sale_gamma: 86 - service_level: 0.95 - SKU296: - constraint: null - cost: 389 - init_stock: 570 - max_stock: 1000 - price: 540 - sale_gamma: 95 - service_level: 0.95 - SKU297: - constraint: null - cost: 51 - init_stock: 448 - max_stock: 1000 - price: 82 - sale_gamma: 64 - service_level: 0.95 - SKU298: - constraint: G(low_profit -> low_stock_constraint) - cost: 269 - init_stock: 267 - max_stock: 1000 - price: 301 - sale_gamma: 89 - service_level: 0.95 - SKU299: - constraint: null - cost: 14 - init_stock: 356 - max_stock: 1000 - price: 16 - sale_gamma: 89 - service_level: 0.95 - SKU3: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 470 - init_stock: 176 - max_stock: 1000 - price: 893 - sale_gamma: 88 - service_level: 0.95 - SKU30: - constraint: G(low_profit -> low_stock_constraint) - cost: 218 - init_stock: 90 - max_stock: 1000 - price: 420 - sale_gamma: 45 - service_level: 0.95 - SKU300: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 88 - max_stock: 1000 - price: 169 - sale_gamma: 22 - service_level: 0.95 - SKU301: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 417 - init_stock: 160 - max_stock: 1000 - price: 467 - sale_gamma: 32 - service_level: 0.95 - SKU302: - constraint: G(stock_constraint) - cost: 161 - init_stock: 276 - max_stock: 1000 - price: 310 - sale_gamma: 92 - service_level: 0.95 - SKU303: - constraint: G(stock_constraint) - cost: 215 - init_stock: 316 - max_stock: 1000 - price: 320 - sale_gamma: 79 - service_level: 0.95 - SKU304: - constraint: null - cost: 145 - init_stock: 348 - max_stock: 1000 - price: 288 - sale_gamma: 58 - service_level: 0.95 - SKU305: - constraint: null - cost: 441 - init_stock: 32 - max_stock: 1000 - price: 498 - sale_gamma: 8 - service_level: 0.95 - SKU306: - constraint: G(low_profit -> low_stock_constraint) - cost: 284 - init_stock: 138 - max_stock: 1000 - price: 357 - sale_gamma: 69 - service_level: 0.95 - SKU307: - constraint: G(stock_constraint) - cost: 412 - init_stock: 280 - max_stock: 1000 - price: 811 - sale_gamma: 56 - service_level: 0.95 - SKU308: - constraint: null - cost: 409 - init_stock: 480 - max_stock: 1000 - price: 609 - sale_gamma: 80 - service_level: 0.95 - SKU309: - constraint: G(low_profit -> low_stock_constraint) - cost: 260 - init_stock: 153 - max_stock: 1000 - price: 403 - sale_gamma: 51 - service_level: 0.95 - SKU31: - constraint: null - cost: 140 - init_stock: 704 - max_stock: 1000 - price: 232 - sale_gamma: 88 - service_level: 0.95 - SKU310: - constraint: null - cost: 367 - init_stock: 196 - max_stock: 1000 - price: 480 - sale_gamma: 28 - service_level: 0.95 - SKU311: - constraint: null - cost: 143 - init_stock: 455 - max_stock: 1000 - price: 208 - sale_gamma: 91 - service_level: 0.95 - SKU312: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 264 - init_stock: 144 - max_stock: 1000 - price: 306 - sale_gamma: 18 - service_level: 0.95 - SKU313: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 475 - max_stock: 1000 - price: 272 - sale_gamma: 95 - service_level: 0.95 - SKU314: - constraint: null - cost: 318 - init_stock: 60 - max_stock: 1000 - price: 597 - sale_gamma: 15 - service_level: 0.95 - SKU315: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 285 - max_stock: 1000 - price: 39 - sale_gamma: 57 - service_level: 0.95 - SKU316: - constraint: null - cost: 215 - init_stock: 258 - max_stock: 1000 - price: 367 - sale_gamma: 86 - service_level: 0.95 - SKU317: - constraint: null - cost: 72 - init_stock: 93 - max_stock: 1000 - price: 138 - sale_gamma: 31 - service_level: 0.95 - SKU318: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 485 - init_stock: 540 - max_stock: 1000 - price: 562 - sale_gamma: 90 - service_level: 0.95 - SKU319: - constraint: null - cost: 218 - init_stock: 48 - max_stock: 1000 - price: 420 - sale_gamma: 24 - service_level: 0.95 - SKU32: - constraint: null - cost: 380 - init_stock: 160 - max_stock: 1000 - price: 478 - sale_gamma: 32 - service_level: 0.95 - SKU320: - constraint: G(stock_constraint) - cost: 445 - init_stock: 700 - max_stock: 1000 - price: 685 - sale_gamma: 100 - service_level: 0.95 - SKU321: - constraint: G(stock_constraint) - cost: 73 - init_stock: 352 - max_stock: 1000 - price: 115 - sale_gamma: 88 - service_level: 0.95 - SKU322: - constraint: G(low_profit -> low_stock_constraint) - cost: 120 - init_stock: 385 - max_stock: 1000 - price: 207 - sale_gamma: 77 - service_level: 0.95 - SKU323: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 241 - init_stock: 570 - max_stock: 1000 - price: 409 - sale_gamma: 95 - service_level: 0.95 - SKU324: - constraint: G(low_profit -> low_stock_constraint) - cost: 269 - init_stock: 500 - max_stock: 1000 - price: 435 - sale_gamma: 100 - service_level: 0.95 - SKU325: - constraint: null - cost: 295 - init_stock: 144 - max_stock: 1000 - price: 421 - sale_gamma: 24 - service_level: 0.95 - SKU326: - constraint: G(stock_constraint) - cost: 23 - init_stock: 332 - max_stock: 1000 - price: 28 - sale_gamma: 83 - service_level: 0.95 - SKU327: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 334 - init_stock: 264 - max_stock: 1000 - price: 367 - sale_gamma: 66 - service_level: 0.95 - SKU328: - constraint: null - cost: 107 - init_stock: 77 - max_stock: 1000 - price: 191 - sale_gamma: 11 - service_level: 0.95 - SKU329: - constraint: G(stock_constraint) - cost: 260 - init_stock: 420 - max_stock: 1000 - price: 520 - sale_gamma: 60 - service_level: 0.95 - SKU33: - constraint: G(stock_constraint) - cost: 81 - init_stock: 672 - max_stock: 1000 - price: 127 - sale_gamma: 84 - service_level: 0.95 - SKU330: - constraint: G(stock_constraint) - cost: 364 - init_stock: 264 - max_stock: 1000 - price: 498 - sale_gamma: 44 - service_level: 0.95 - SKU331: - constraint: G(low_profit -> low_stock_constraint) - cost: 435 - init_stock: 168 - max_stock: 1000 - price: 600 - sale_gamma: 28 - service_level: 0.95 - SKU332: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 227 - init_stock: 664 - max_stock: 1000 - price: 283 - sale_gamma: 83 - service_level: 0.95 - SKU333: - constraint: null - cost: 268 - init_stock: 252 - max_stock: 1000 - price: 391 - sale_gamma: 63 - service_level: 0.95 - SKU334: - constraint: G(low_profit -> low_stock_constraint) - cost: 248 - init_stock: 172 - max_stock: 1000 - price: 327 - sale_gamma: 43 - service_level: 0.95 - SKU335: - constraint: G(low_profit -> low_stock_constraint) - cost: 341 - init_stock: 376 - max_stock: 1000 - price: 675 - sale_gamma: 94 - service_level: 0.95 - SKU336: - constraint: G(low_profit -> low_stock_constraint) - cost: 489 - init_stock: 200 - max_stock: 1000 - price: 738 - sale_gamma: 50 - service_level: 0.95 - SKU337: - constraint: G(stock_constraint) - cost: 416 - init_stock: 294 - max_stock: 1000 - price: 736 - sale_gamma: 98 - service_level: 0.95 - SKU338: - constraint: null - cost: 209 - init_stock: 518 - max_stock: 1000 - price: 330 - sale_gamma: 74 - service_level: 0.95 - SKU339: - constraint: null - cost: 189 - init_stock: 186 - max_stock: 1000 - price: 336 - sale_gamma: 62 - service_level: 0.95 - SKU34: - constraint: G(low_profit -> low_stock_constraint) - cost: 263 - init_stock: 188 - max_stock: 1000 - price: 526 - sale_gamma: 47 - service_level: 0.95 - SKU340: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 375 - init_stock: 133 - max_stock: 1000 - price: 442 - sale_gamma: 19 - service_level: 0.95 - SKU341: - constraint: G(low_profit -> low_stock_constraint) - cost: 166 - init_stock: 336 - max_stock: 1000 - price: 325 - sale_gamma: 56 - service_level: 0.95 - SKU342: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 177 - init_stock: 82 - max_stock: 1000 - price: 242 - sale_gamma: 41 - service_level: 0.95 - SKU343: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 478 - init_stock: 336 - max_stock: 1000 - price: 616 - sale_gamma: 84 - service_level: 0.95 - SKU344: - constraint: null - cost: 491 - init_stock: 324 - max_stock: 1000 - price: 903 - sale_gamma: 81 - service_level: 0.95 - SKU345: - constraint: null - cost: 272 - init_stock: 165 - max_stock: 1000 - price: 470 - sale_gamma: 33 - service_level: 0.95 - SKU346: - constraint: null - cost: 200 - init_stock: 355 - max_stock: 1000 - price: 366 - sale_gamma: 71 - service_level: 0.95 - SKU347: - constraint: null - cost: 180 - init_stock: 217 - max_stock: 1000 - price: 221 - sale_gamma: 31 - service_level: 0.95 - SKU348: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 102 - max_stock: 1000 - price: 559 - sale_gamma: 34 - service_level: 0.95 - SKU349: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 81 - init_stock: 273 - max_stock: 1000 - price: 151 - sale_gamma: 91 - service_level: 0.95 - SKU35: - constraint: G(stock_constraint) - cost: 240 - init_stock: 118 - max_stock: 1000 - price: 350 - sale_gamma: 59 - service_level: 0.95 - SKU350: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 106 - init_stock: 36 - max_stock: 1000 - price: 136 - sale_gamma: 12 - service_level: 0.95 - SKU351: - constraint: G(stock_constraint) - cost: 102 - init_stock: 444 - max_stock: 1000 - price: 192 - sale_gamma: 74 - service_level: 0.95 - SKU352: - constraint: null - cost: 274 - init_stock: 112 - max_stock: 1000 - price: 337 - sale_gamma: 16 - service_level: 0.95 - SKU353: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 147 - init_stock: 408 - max_stock: 1000 - price: 238 - sale_gamma: 68 - service_level: 0.95 - SKU354: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 410 - init_stock: 390 - max_stock: 1000 - price: 508 - sale_gamma: 78 - service_level: 0.95 - SKU355: - constraint: null - cost: 417 - init_stock: 336 - max_stock: 1000 - price: 575 - sale_gamma: 56 - service_level: 0.95 - SKU356: - constraint: null - cost: 192 - init_stock: 175 - max_stock: 1000 - price: 211 - sale_gamma: 35 - service_level: 0.95 - SKU357: - constraint: G(stock_constraint) - cost: 80 - init_stock: 400 - max_stock: 1000 - price: 122 - sale_gamma: 100 - service_level: 0.95 - SKU358: - constraint: G(stock_constraint) - cost: 130 - init_stock: 160 - max_stock: 1000 - price: 153 - sale_gamma: 32 - service_level: 0.95 - SKU359: - constraint: null - cost: 327 - init_stock: 128 - max_stock: 1000 - price: 578 - sale_gamma: 32 - service_level: 0.95 - SKU36: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 134 - init_stock: 644 - max_stock: 1000 - price: 222 - sale_gamma: 92 - service_level: 0.95 - SKU360: - constraint: G(low_profit -> low_stock_constraint) - cost: 456 - init_stock: 165 - max_stock: 1000 - price: 693 - sale_gamma: 55 - service_level: 0.95 - SKU361: - constraint: G(stock_constraint) - cost: 183 - init_stock: 536 - max_stock: 1000 - price: 336 - sale_gamma: 67 - service_level: 0.95 - SKU362: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 352 - init_stock: 164 - max_stock: 1000 - price: 520 - sale_gamma: 41 - service_level: 0.95 - SKU363: - constraint: null - cost: 32 - init_stock: 20 - max_stock: 1000 - price: 62 - sale_gamma: 5 - service_level: 0.95 - SKU364: - constraint: null - cost: 331 - init_stock: 260 - max_stock: 1000 - price: 483 - sale_gamma: 65 - service_level: 0.95 - SKU365: - constraint: G(low_profit -> low_stock_constraint) - cost: 57 - init_stock: 492 - max_stock: 1000 - price: 100 - sale_gamma: 82 - service_level: 0.95 - SKU366: - constraint: null - cost: 184 - init_stock: 168 - max_stock: 1000 - price: 211 - sale_gamma: 24 - service_level: 0.95 - SKU367: - constraint: G(stock_constraint) - cost: 385 - init_stock: 400 - max_stock: 1000 - price: 469 - sale_gamma: 80 - service_level: 0.95 - SKU368: - constraint: G(low_profit -> low_stock_constraint) - cost: 391 - init_stock: 217 - max_stock: 1000 - price: 609 - sale_gamma: 31 - service_level: 0.95 - SKU369: - constraint: null - cost: 460 - init_stock: 256 - max_stock: 1000 - price: 662 - sale_gamma: 32 - service_level: 0.95 - SKU37: - constraint: G(low_profit -> low_stock_constraint) - cost: 284 - init_stock: 207 - max_stock: 1000 - price: 423 - sale_gamma: 69 - service_level: 0.95 - SKU370: - constraint: null - cost: 103 - init_stock: 168 - max_stock: 1000 - price: 180 - sale_gamma: 28 - service_level: 0.95 - SKU371: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 588 - max_stock: 1000 - price: 280 - sale_gamma: 84 - service_level: 0.95 - SKU372: - constraint: G(stock_constraint) - cost: 488 - init_stock: 160 - max_stock: 1000 - price: 707 - sale_gamma: 20 - service_level: 0.95 - SKU373: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 155 - init_stock: 189 - max_stock: 1000 - price: 175 - sale_gamma: 27 - service_level: 0.95 - SKU374: - constraint: G(low_profit -> low_stock_constraint) - cost: 13 - init_stock: 760 - max_stock: 1000 - price: 19 - sale_gamma: 95 - service_level: 0.95 - SKU375: - constraint: G(stock_constraint) - cost: 433 - init_stock: 434 - max_stock: 1000 - price: 588 - sale_gamma: 62 - service_level: 0.95 - SKU376: - constraint: null - cost: 30 - init_stock: 324 - max_stock: 1000 - price: 54 - sale_gamma: 81 - service_level: 0.95 - SKU377: - constraint: null - cost: 100 - init_stock: 72 - max_stock: 1000 - price: 159 - sale_gamma: 36 - service_level: 0.95 - SKU378: - constraint: null - cost: 489 - init_stock: 45 - max_stock: 1000 - price: 699 - sale_gamma: 9 - service_level: 0.95 - SKU379: - constraint: G(stock_constraint) - cost: 462 - init_stock: 328 - max_stock: 1000 - price: 808 - sale_gamma: 41 - service_level: 0.95 - SKU38: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 326 - init_stock: 486 - max_stock: 1000 - price: 394 - sale_gamma: 81 - service_level: 0.95 - SKU380: - constraint: G(stock_constraint) - cost: 459 - init_stock: 78 - max_stock: 1000 - price: 573 - sale_gamma: 13 - service_level: 0.95 - SKU381: - constraint: null - cost: 276 - init_stock: 304 - max_stock: 1000 - price: 521 - sale_gamma: 76 - service_level: 0.95 - SKU382: - constraint: null - cost: 472 - init_stock: 85 - max_stock: 1000 - price: 674 - sale_gamma: 17 - service_level: 0.95 - SKU383: - constraint: null - cost: 249 - init_stock: 99 - max_stock: 1000 - price: 410 - sale_gamma: 33 - service_level: 0.95 - SKU384: - constraint: G(stock_constraint) - cost: 37 - init_stock: 192 - max_stock: 1000 - price: 66 - sale_gamma: 24 - service_level: 0.95 - SKU385: - constraint: null - cost: 275 - init_stock: 400 - max_stock: 1000 - price: 368 - sale_gamma: 80 - service_level: 0.95 - SKU386: - constraint: null - cost: 357 - init_stock: 357 - max_stock: 1000 - price: 639 - sale_gamma: 51 - service_level: 0.95 - SKU387: - constraint: G(low_profit -> low_stock_constraint) - cost: 471 - init_stock: 185 - max_stock: 1000 - price: 744 - sale_gamma: 37 - service_level: 0.95 - SKU388: - constraint: null - cost: 264 - init_stock: 469 - max_stock: 1000 - price: 485 - sale_gamma: 67 - service_level: 0.95 - SKU389: - constraint: G(stock_constraint) - cost: 13 - init_stock: 343 - max_stock: 1000 - price: 18 - sale_gamma: 49 - service_level: 0.95 - SKU39: - constraint: G(stock_constraint) - cost: 381 - init_stock: 44 - max_stock: 1000 - price: 605 - sale_gamma: 22 - service_level: 0.95 - SKU390: - constraint: null - cost: 146 - init_stock: 10 - max_stock: 1000 - price: 188 - sale_gamma: 5 - service_level: 0.95 - SKU391: - constraint: null - cost: 365 - init_stock: 276 - max_stock: 1000 - price: 657 - sale_gamma: 92 - service_level: 0.95 - SKU392: - constraint: null - cost: 489 - init_stock: 368 - max_stock: 1000 - price: 855 - sale_gamma: 46 - service_level: 0.95 - SKU393: - constraint: null - cost: 466 - init_stock: 88 - max_stock: 1000 - price: 573 - sale_gamma: 11 - service_level: 0.95 - SKU394: - constraint: null - cost: 186 - init_stock: 51 - max_stock: 1000 - price: 303 - sale_gamma: 17 - service_level: 0.95 - SKU395: - constraint: G(low_profit -> low_stock_constraint) - cost: 324 - init_stock: 60 - max_stock: 1000 - price: 563 - sale_gamma: 30 - service_level: 0.95 - SKU396: - constraint: null - cost: 392 - init_stock: 544 - max_stock: 1000 - price: 678 - sale_gamma: 68 - service_level: 0.95 - SKU397: - constraint: null - cost: 257 - init_stock: 69 - max_stock: 1000 - price: 493 - sale_gamma: 23 - service_level: 0.95 - SKU398: - constraint: G(stock_constraint) - cost: 455 - init_stock: 426 - max_stock: 1000 - price: 609 - sale_gamma: 71 - service_level: 0.95 - SKU399: - constraint: null - cost: 15 - init_stock: 546 - max_stock: 1000 - price: 30 - sale_gamma: 78 - service_level: 0.95 - SKU4: - constraint: null - cost: 289 - init_stock: 186 - max_stock: 1000 - price: 372 - sale_gamma: 93 - service_level: 0.95 - SKU40: - constraint: null - cost: 216 - init_stock: 32 - max_stock: 1000 - price: 319 - sale_gamma: 8 - service_level: 0.95 - SKU400: - constraint: null - cost: 355 - init_stock: 570 - max_stock: 1000 - price: 553 - sale_gamma: 95 - service_level: 0.95 - SKU401: - constraint: null - cost: 150 - init_stock: 64 - max_stock: 1000 - price: 282 - sale_gamma: 8 - service_level: 0.95 - SKU402: - constraint: null - cost: 255 - init_stock: 552 - max_stock: 1000 - price: 339 - sale_gamma: 92 - service_level: 0.95 - SKU403: - constraint: null - cost: 333 - init_stock: 332 - max_stock: 1000 - price: 479 - sale_gamma: 83 - service_level: 0.95 - SKU404: - constraint: G(stock_constraint) - cost: 94 - init_stock: 216 - max_stock: 1000 - price: 172 - sale_gamma: 36 - service_level: 0.95 - SKU405: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 242 - init_stock: 348 - max_stock: 1000 - price: 396 - sale_gamma: 58 - service_level: 0.95 - SKU406: - constraint: G(stock_constraint) - cost: 402 - init_stock: 220 - max_stock: 1000 - price: 695 - sale_gamma: 55 - service_level: 0.95 - SKU407: - constraint: G(low_profit -> low_stock_constraint) - cost: 346 - init_stock: 273 - max_stock: 1000 - price: 494 - sale_gamma: 39 - service_level: 0.95 - SKU408: - constraint: null - cost: 274 - init_stock: 574 - max_stock: 1000 - price: 405 - sale_gamma: 82 - service_level: 0.95 - SKU409: - constraint: G(stock_constraint) - cost: 268 - init_stock: 376 - max_stock: 1000 - price: 530 - sale_gamma: 94 - service_level: 0.95 - SKU41: - constraint: null - cost: 312 - init_stock: 544 - max_stock: 1000 - price: 343 - sale_gamma: 68 - service_level: 0.95 - SKU410: - constraint: G(low_profit -> low_stock_constraint) - cost: 429 - init_stock: 192 - max_stock: 1000 - price: 673 - sale_gamma: 48 - service_level: 0.95 - SKU411: - constraint: G(stock_constraint) - cost: 11 - init_stock: 120 - max_stock: 1000 - price: 20 - sale_gamma: 30 - service_level: 0.95 - SKU412: - constraint: null - cost: 91 - init_stock: 236 - max_stock: 1000 - price: 181 - sale_gamma: 59 - service_level: 0.95 - SKU413: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 443 - init_stock: 66 - max_stock: 1000 - price: 815 - sale_gamma: 11 - service_level: 0.95 - SKU414: - constraint: null - cost: 67 - init_stock: 385 - max_stock: 1000 - price: 112 - sale_gamma: 77 - service_level: 0.95 - SKU415: - constraint: null - cost: 347 - init_stock: 50 - max_stock: 1000 - price: 548 - sale_gamma: 10 - service_level: 0.95 - SKU416: - constraint: null - cost: 29 - init_stock: 204 - max_stock: 1000 - price: 51 - sale_gamma: 51 - service_level: 0.95 - SKU417: - constraint: null - cost: 488 - init_stock: 204 - max_stock: 1000 - price: 800 - sale_gamma: 68 - service_level: 0.95 - SKU418: - constraint: null - cost: 432 - init_stock: 776 - max_stock: 1000 - price: 781 - sale_gamma: 97 - service_level: 0.95 - SKU419: - constraint: G(low_profit -> low_stock_constraint) - cost: 38 - init_stock: 39 - max_stock: 1000 - price: 65 - sale_gamma: 13 - service_level: 0.95 - SKU42: - constraint: null - cost: 275 - init_stock: 581 - max_stock: 1000 - price: 418 - sale_gamma: 83 - service_level: 0.95 - SKU420: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 21 - max_stock: 1000 - price: 401 - sale_gamma: 7 - service_level: 0.95 - SKU421: - constraint: null - cost: 289 - init_stock: 640 - max_stock: 1000 - price: 442 - sale_gamma: 80 - service_level: 0.95 - SKU422: - constraint: null - cost: 83 - init_stock: 102 - max_stock: 1000 - price: 101 - sale_gamma: 34 - service_level: 0.95 - SKU423: - constraint: null - cost: 174 - init_stock: 84 - max_stock: 1000 - price: 234 - sale_gamma: 12 - service_level: 0.95 - SKU424: - constraint: G(low_profit -> low_stock_constraint) - cost: 495 - init_stock: 82 - max_stock: 1000 - price: 935 - sale_gamma: 41 - service_level: 0.95 - SKU425: - constraint: null - cost: 142 - init_stock: 264 - max_stock: 1000 - price: 230 - sale_gamma: 44 - service_level: 0.95 - SKU426: - constraint: null - cost: 234 - init_stock: 64 - max_stock: 1000 - price: 453 - sale_gamma: 16 - service_level: 0.95 - SKU427: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 162 - init_stock: 252 - max_stock: 1000 - price: 225 - sale_gamma: 84 - service_level: 0.95 - SKU428: - constraint: G(stock_constraint) - cost: 423 - init_stock: 310 - max_stock: 1000 - price: 765 - sale_gamma: 62 - service_level: 0.95 - SKU429: - constraint: G(low_profit -> low_stock_constraint) - cost: 322 - init_stock: 297 - max_stock: 1000 - price: 634 - sale_gamma: 99 - service_level: 0.95 - SKU43: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 428 - init_stock: 432 - max_stock: 1000 - price: 791 - sale_gamma: 54 - service_level: 0.95 - SKU430: - constraint: G(stock_constraint) - cost: 289 - init_stock: 135 - max_stock: 1000 - price: 534 - sale_gamma: 45 - service_level: 0.95 - SKU431: - constraint: null - cost: 29 - init_stock: 91 - max_stock: 1000 - price: 44 - sale_gamma: 13 - service_level: 0.95 - SKU432: - constraint: G(low_profit -> low_stock_constraint) - cost: 349 - init_stock: 115 - max_stock: 1000 - price: 415 - sale_gamma: 23 - service_level: 0.95 - SKU433: - constraint: G(low_profit -> low_stock_constraint) - cost: 380 - init_stock: 156 - max_stock: 1000 - price: 634 - sale_gamma: 26 - service_level: 0.95 - SKU434: - constraint: null - cost: 373 - init_stock: 328 - max_stock: 1000 - price: 704 - sale_gamma: 41 - service_level: 0.95 - SKU435: - constraint: null - cost: 46 - init_stock: 200 - max_stock: 1000 - price: 71 - sale_gamma: 50 - service_level: 0.95 - SKU436: - constraint: null - cost: 383 - init_stock: 190 - max_stock: 1000 - price: 731 - sale_gamma: 38 - service_level: 0.95 - SKU437: - constraint: null - cost: 76 - init_stock: 656 - max_stock: 1000 - price: 85 - sale_gamma: 82 - service_level: 0.95 - SKU438: - constraint: G(stock_constraint) - cost: 433 - init_stock: 108 - max_stock: 1000 - price: 796 - sale_gamma: 27 - service_level: 0.95 - SKU439: - constraint: null - cost: 40 - init_stock: 285 - max_stock: 1000 - price: 66 - sale_gamma: 57 - service_level: 0.95 - SKU44: - constraint: null - cost: 255 - init_stock: 76 - max_stock: 1000 - price: 507 - sale_gamma: 19 - service_level: 0.95 - SKU440: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 124 - init_stock: 282 - max_stock: 1000 - price: 189 - sale_gamma: 47 - service_level: 0.95 - SKU441: - constraint: null - cost: 228 - init_stock: 324 - max_stock: 1000 - price: 323 - sale_gamma: 54 - service_level: 0.95 - SKU442: - constraint: null - cost: 19 - init_stock: 164 - max_stock: 1000 - price: 29 - sale_gamma: 41 - service_level: 0.95 - SKU443: - constraint: null - cost: 446 - init_stock: 310 - max_stock: 1000 - price: 825 - sale_gamma: 62 - service_level: 0.95 - SKU444: - constraint: G(low_profit -> low_stock_constraint) - cost: 212 - init_stock: 162 - max_stock: 1000 - price: 248 - sale_gamma: 54 - service_level: 0.95 - SKU445: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 396 - init_stock: 100 - max_stock: 1000 - price: 728 - sale_gamma: 20 - service_level: 0.95 - SKU446: - constraint: null - cost: 126 - init_stock: 384 - max_stock: 1000 - price: 146 - sale_gamma: 64 - service_level: 0.95 - SKU447: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 20 - init_stock: 280 - max_stock: 1000 - price: 23 - sale_gamma: 40 - service_level: 0.95 - SKU448: - constraint: null - cost: 365 - init_stock: 528 - max_stock: 1000 - price: 584 - sale_gamma: 66 - service_level: 0.95 - SKU449: - constraint: null - cost: 328 - init_stock: 24 - max_stock: 1000 - price: 570 - sale_gamma: 8 - service_level: 0.95 - SKU45: - constraint: G(low_profit -> low_stock_constraint) - cost: 158 - init_stock: 196 - max_stock: 1000 - price: 238 - sale_gamma: 28 - service_level: 0.95 - SKU450: - constraint: G(stock_constraint) - cost: 61 - init_stock: 24 - max_stock: 1000 - price: 91 - sale_gamma: 6 - service_level: 0.95 - SKU451: - constraint: G(low_profit -> low_stock_constraint) - cost: 385 - init_stock: 108 - max_stock: 1000 - price: 712 - sale_gamma: 36 - service_level: 0.95 - SKU452: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 450 - init_stock: 292 - max_stock: 1000 - price: 724 - sale_gamma: 73 - service_level: 0.95 - SKU453: - constraint: null - cost: 131 - init_stock: 243 - max_stock: 1000 - price: 146 - sale_gamma: 81 - service_level: 0.95 - SKU454: - constraint: G(low_profit -> low_stock_constraint) - cost: 404 - init_stock: 413 - max_stock: 1000 - price: 731 - sale_gamma: 59 - service_level: 0.95 - SKU455: - constraint: G(low_profit -> low_stock_constraint) - cost: 334 - init_stock: 296 - max_stock: 1000 - price: 400 - sale_gamma: 74 - service_level: 0.95 - SKU456: - constraint: G(low_profit -> low_stock_constraint) - cost: 126 - init_stock: 328 - max_stock: 1000 - price: 161 - sale_gamma: 41 - service_level: 0.95 - SKU457: - constraint: G(low_profit -> low_stock_constraint) - cost: 235 - init_stock: 336 - max_stock: 1000 - price: 350 - sale_gamma: 56 - service_level: 0.95 - SKU458: - constraint: null - cost: 282 - init_stock: 94 - max_stock: 1000 - price: 493 - sale_gamma: 47 - service_level: 0.95 - SKU459: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 180 - init_stock: 132 - max_stock: 1000 - price: 225 - sale_gamma: 22 - service_level: 0.95 - SKU46: - constraint: G(low_profit -> low_stock_constraint) - cost: 341 - init_stock: 100 - max_stock: 1000 - price: 569 - sale_gamma: 20 - service_level: 0.95 - SKU460: - constraint: G(low_profit -> low_stock_constraint) - cost: 271 - init_stock: 25 - max_stock: 1000 - price: 376 - sale_gamma: 5 - service_level: 0.95 - SKU461: - constraint: null - cost: 131 - init_stock: 135 - max_stock: 1000 - price: 233 - sale_gamma: 27 - service_level: 0.95 - SKU462: - constraint: G(low_profit -> low_stock_constraint) - cost: 98 - init_stock: 27 - max_stock: 1000 - price: 120 - sale_gamma: 9 - service_level: 0.95 - SKU463: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 117 - init_stock: 161 - max_stock: 1000 - price: 132 - sale_gamma: 23 - service_level: 0.95 - SKU464: - constraint: G(stock_constraint) - cost: 295 - init_stock: 144 - max_stock: 1000 - price: 566 - sale_gamma: 24 - service_level: 0.95 - SKU465: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 286 - init_stock: 462 - max_stock: 1000 - price: 540 - sale_gamma: 77 - service_level: 0.95 - SKU466: - constraint: null - cost: 278 - init_stock: 212 - max_stock: 1000 - price: 533 - sale_gamma: 53 - service_level: 0.95 - SKU467: - constraint: null - cost: 294 - init_stock: 48 - max_stock: 1000 - price: 432 - sale_gamma: 8 - service_level: 0.95 - SKU468: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 40 - init_stock: 70 - max_stock: 1000 - price: 50 - sale_gamma: 10 - service_level: 0.95 - SKU469: - constraint: null - cost: 481 - init_stock: 300 - max_stock: 1000 - price: 615 - sale_gamma: 100 - service_level: 0.95 - SKU47: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 460 - init_stock: 77 - max_stock: 1000 - price: 533 - sale_gamma: 11 - service_level: 0.95 - SKU470: - constraint: G(low_profit -> low_stock_constraint) - cost: 470 - init_stock: 183 - max_stock: 1000 - price: 761 - sale_gamma: 61 - service_level: 0.95 - SKU471: - constraint: null - cost: 98 - init_stock: 186 - max_stock: 1000 - price: 147 - sale_gamma: 31 - service_level: 0.95 - SKU472: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 22 - init_stock: 474 - max_stock: 1000 - price: 25 - sale_gamma: 79 - service_level: 0.95 - SKU473: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 414 - init_stock: 574 - max_stock: 1000 - price: 691 - sale_gamma: 82 - service_level: 0.95 - SKU474: - constraint: G(low_profit -> low_stock_constraint) - cost: 95 - init_stock: 205 - max_stock: 1000 - price: 132 - sale_gamma: 41 - service_level: 0.95 - SKU475: - constraint: G(low_profit -> low_stock_constraint) - cost: 390 - init_stock: 144 - max_stock: 1000 - price: 429 - sale_gamma: 36 - service_level: 0.95 - SKU476: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 468 - init_stock: 602 - max_stock: 1000 - price: 739 - sale_gamma: 86 - service_level: 0.95 - SKU477: - constraint: null - cost: 91 - init_stock: 160 - max_stock: 1000 - price: 144 - sale_gamma: 40 - service_level: 0.95 - SKU478: - constraint: G(stock_constraint) - cost: 242 - init_stock: 408 - max_stock: 1000 - price: 341 - sale_gamma: 51 - service_level: 0.95 - SKU479: - constraint: G(stock_constraint) - cost: 158 - init_stock: 250 - max_stock: 1000 - price: 301 - sale_gamma: 50 - service_level: 0.95 - SKU48: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 274 - init_stock: 96 - max_stock: 1000 - price: 402 - sale_gamma: 12 - service_level: 0.95 - SKU480: - constraint: null - cost: 424 - init_stock: 136 - max_stock: 1000 - price: 746 - sale_gamma: 68 - service_level: 0.95 - SKU481: - constraint: null - cost: 199 - init_stock: 294 - max_stock: 1000 - price: 256 - sale_gamma: 49 - service_level: 0.95 - SKU482: - constraint: null - cost: 217 - init_stock: 130 - max_stock: 1000 - price: 277 - sale_gamma: 26 - service_level: 0.95 - SKU483: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 120 - init_stock: 36 - max_stock: 1000 - price: 234 - sale_gamma: 12 - service_level: 0.95 - SKU484: - constraint: G(low_profit -> low_stock_constraint) - cost: 244 - init_stock: 560 - max_stock: 1000 - price: 314 - sale_gamma: 80 - service_level: 0.95 - SKU485: - constraint: G(stock_constraint) - cost: 266 - init_stock: 399 - max_stock: 1000 - price: 321 - sale_gamma: 57 - service_level: 0.95 - SKU486: - constraint: null - cost: 243 - init_stock: 36 - max_stock: 1000 - price: 478 - sale_gamma: 12 - service_level: 0.95 - SKU487: - constraint: null - cost: 275 - init_stock: 350 - max_stock: 1000 - price: 456 - sale_gamma: 70 - service_level: 0.95 - SKU488: - constraint: null - cost: 423 - init_stock: 26 - max_stock: 1000 - price: 681 - sale_gamma: 13 - service_level: 0.95 - SKU489: - constraint: G(stock_constraint) - cost: 241 - init_stock: 190 - max_stock: 1000 - price: 284 - sale_gamma: 38 - service_level: 0.95 - SKU49: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 184 - init_stock: 144 - max_stock: 1000 - price: 241 - sale_gamma: 72 - service_level: 0.95 - SKU490: - constraint: null - cost: 285 - init_stock: 96 - max_stock: 1000 - price: 547 - sale_gamma: 16 - service_level: 0.95 - SKU491: - constraint: null - cost: 334 - init_stock: 546 - max_stock: 1000 - price: 594 - sale_gamma: 78 - service_level: 0.95 - SKU492: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 371 - init_stock: 408 - max_stock: 1000 - price: 638 - sale_gamma: 68 - service_level: 0.95 - SKU493: - constraint: G(stock_constraint) - cost: 402 - init_stock: 380 - max_stock: 1000 - price: 619 - sale_gamma: 95 - service_level: 0.95 - SKU494: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 84 - init_stock: 348 - max_stock: 1000 - price: 153 - sale_gamma: 87 - service_level: 0.95 - SKU495: - constraint: null - cost: 347 - init_stock: 259 - max_stock: 1000 - price: 572 - sale_gamma: 37 - service_level: 0.95 - SKU496: - constraint: null - cost: 177 - init_stock: 159 - max_stock: 1000 - price: 196 - sale_gamma: 53 - service_level: 0.95 - SKU497: - constraint: null - cost: 260 - init_stock: 217 - max_stock: 1000 - price: 473 - sale_gamma: 31 - service_level: 0.95 - SKU498: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 447 - init_stock: 282 - max_stock: 1000 - price: 867 - sale_gamma: 94 - service_level: 0.95 - SKU499: - constraint: null - cost: 425 - init_stock: 468 - max_stock: 1000 - price: 522 - sale_gamma: 78 - service_level: 0.95 - SKU5: - constraint: null - cost: 478 - init_stock: 444 - max_stock: 1000 - price: 688 - sale_gamma: 74 - service_level: 0.95 - SKU50: - constraint: null - cost: 427 - init_stock: 36 - max_stock: 1000 - price: 768 - sale_gamma: 18 - service_level: 0.95 - SKU500: - constraint: G(stock_constraint) - cost: 413 - init_stock: 50 - max_stock: 1000 - price: 536 - sale_gamma: 10 - service_level: 0.95 - SKU501: - constraint: null - cost: 74 - init_stock: 312 - max_stock: 1000 - price: 147 - sale_gamma: 78 - service_level: 0.95 - SKU502: - constraint: null - cost: 411 - init_stock: 18 - max_stock: 1000 - price: 505 - sale_gamma: 9 - service_level: 0.95 - SKU503: - constraint: G(low_profit -> low_stock_constraint) - cost: 459 - init_stock: 176 - max_stock: 1000 - price: 885 - sale_gamma: 44 - service_level: 0.95 - SKU504: - constraint: null - cost: 325 - init_stock: 200 - max_stock: 1000 - price: 484 - sale_gamma: 40 - service_level: 0.95 - SKU505: - constraint: null - cost: 324 - init_stock: 432 - max_stock: 1000 - price: 573 - sale_gamma: 72 - service_level: 0.95 - SKU506: - constraint: G(low_profit -> low_stock_constraint) - cost: 477 - init_stock: 40 - max_stock: 1000 - price: 896 - sale_gamma: 8 - service_level: 0.95 - SKU507: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 142 - max_stock: 1000 - price: 65 - sale_gamma: 71 - service_level: 0.95 - SKU508: - constraint: null - cost: 390 - init_stock: 342 - max_stock: 1000 - price: 546 - sale_gamma: 57 - service_level: 0.95 - SKU509: - constraint: null - cost: 464 - init_stock: 355 - max_stock: 1000 - price: 928 - sale_gamma: 71 - service_level: 0.95 - SKU51: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 434 - init_stock: 196 - max_stock: 1000 - price: 598 - sale_gamma: 49 - service_level: 0.95 - SKU510: - constraint: G(low_profit -> low_stock_constraint) - cost: 233 - init_stock: 220 - max_stock: 1000 - price: 267 - sale_gamma: 55 - service_level: 0.95 - SKU511: - constraint: G(stock_constraint) - cost: 317 - init_stock: 162 - max_stock: 1000 - price: 535 - sale_gamma: 27 - service_level: 0.95 - SKU512: - constraint: null - cost: 405 - init_stock: 360 - max_stock: 1000 - price: 619 - sale_gamma: 72 - service_level: 0.95 - SKU513: - constraint: null - cost: 33 - init_stock: 196 - max_stock: 1000 - price: 50 - sale_gamma: 49 - service_level: 0.95 - SKU514: - constraint: null - cost: 441 - init_stock: 144 - max_stock: 1000 - price: 657 - sale_gamma: 24 - service_level: 0.95 - SKU515: - constraint: null - cost: 456 - init_stock: 140 - max_stock: 1000 - price: 579 - sale_gamma: 35 - service_level: 0.95 - SKU516: - constraint: G(stock_constraint) - cost: 432 - init_stock: 96 - max_stock: 1000 - price: 842 - sale_gamma: 12 - service_level: 0.95 - SKU517: - constraint: null - cost: 223 - init_stock: 316 - max_stock: 1000 - price: 399 - sale_gamma: 79 - service_level: 0.95 - SKU518: - constraint: G(low_profit -> low_stock_constraint) - cost: 155 - init_stock: 616 - max_stock: 1000 - price: 230 - sale_gamma: 77 - service_level: 0.95 - SKU519: - constraint: null - cost: 276 - init_stock: 329 - max_stock: 1000 - price: 322 - sale_gamma: 47 - service_level: 0.95 - SKU52: - constraint: null - cost: 340 - init_stock: 594 - max_stock: 1000 - price: 428 - sale_gamma: 99 - service_level: 0.95 - SKU520: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 18 - init_stock: 145 - max_stock: 1000 - price: 26 - sale_gamma: 29 - service_level: 0.95 - SKU521: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 356 - init_stock: 455 - max_stock: 1000 - price: 647 - sale_gamma: 91 - service_level: 0.95 - SKU522: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 389 - init_stock: 210 - max_stock: 1000 - price: 778 - sale_gamma: 30 - service_level: 0.95 - SKU523: - constraint: G(stock_constraint) - cost: 359 - init_stock: 100 - max_stock: 1000 - price: 437 - sale_gamma: 25 - service_level: 0.95 - SKU524: - constraint: G(stock_constraint) - cost: 213 - init_stock: 117 - max_stock: 1000 - price: 387 - sale_gamma: 39 - service_level: 0.95 - SKU525: - constraint: null - cost: 131 - init_stock: 66 - max_stock: 1000 - price: 224 - sale_gamma: 11 - service_level: 0.95 - SKU526: - constraint: null - cost: 237 - init_stock: 72 - max_stock: 1000 - price: 310 - sale_gamma: 36 - service_level: 0.95 - SKU527: - constraint: G(low_profit -> low_stock_constraint) - cost: 161 - init_stock: 72 - max_stock: 1000 - price: 262 - sale_gamma: 12 - service_level: 0.95 - SKU528: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 384 - init_stock: 343 - max_stock: 1000 - price: 552 - sale_gamma: 49 - service_level: 0.95 - SKU529: - constraint: null - cost: 321 - init_stock: 52 - max_stock: 1000 - price: 455 - sale_gamma: 13 - service_level: 0.95 - SKU53: - constraint: G(low_profit -> low_stock_constraint) - cost: 20 - init_stock: 294 - max_stock: 1000 - price: 36 - sale_gamma: 42 - service_level: 0.95 - SKU530: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 299 - init_stock: 480 - max_stock: 1000 - price: 550 - sale_gamma: 96 - service_level: 0.95 - SKU531: - constraint: null - cost: 335 - init_stock: 300 - max_stock: 1000 - price: 452 - sale_gamma: 60 - service_level: 0.95 - SKU532: - constraint: null - cost: 155 - init_stock: 528 - max_stock: 1000 - price: 181 - sale_gamma: 88 - service_level: 0.95 - SKU533: - constraint: null - cost: 444 - init_stock: 205 - max_stock: 1000 - price: 621 - sale_gamma: 41 - service_level: 0.95 - SKU534: - constraint: G(stock_constraint) - cost: 146 - init_stock: 485 - max_stock: 1000 - price: 289 - sale_gamma: 97 - service_level: 0.95 - SKU535: - constraint: null - cost: 384 - init_stock: 210 - max_stock: 1000 - price: 733 - sale_gamma: 35 - service_level: 0.95 - SKU536: - constraint: null - cost: 86 - init_stock: 300 - max_stock: 1000 - price: 137 - sale_gamma: 100 - service_level: 0.95 - SKU537: - constraint: null - cost: 173 - init_stock: 282 - max_stock: 1000 - price: 223 - sale_gamma: 47 - service_level: 0.95 - SKU538: - constraint: null - cost: 40 - init_stock: 329 - max_stock: 1000 - price: 53 - sale_gamma: 47 - service_level: 0.95 - SKU539: - constraint: null - cost: 390 - init_stock: 40 - max_stock: 1000 - price: 729 - sale_gamma: 10 - service_level: 0.95 - SKU54: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 162 - init_stock: 462 - max_stock: 1000 - price: 278 - sale_gamma: 77 - service_level: 0.95 - SKU540: - constraint: G(stock_constraint) - cost: 290 - init_stock: 240 - max_stock: 1000 - price: 568 - sale_gamma: 48 - service_level: 0.95 - SKU541: - constraint: null - cost: 192 - init_stock: 288 - max_stock: 1000 - price: 384 - sale_gamma: 36 - service_level: 0.95 - SKU542: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 96 - init_stock: 184 - max_stock: 1000 - price: 114 - sale_gamma: 46 - service_level: 0.95 - SKU543: - constraint: null - cost: 197 - init_stock: 280 - max_stock: 1000 - price: 222 - sale_gamma: 40 - service_level: 0.95 - SKU544: - constraint: null - cost: 408 - init_stock: 448 - max_stock: 1000 - price: 501 - sale_gamma: 64 - service_level: 0.95 - SKU545: - constraint: null - cost: 431 - init_stock: 195 - max_stock: 1000 - price: 853 - sale_gamma: 65 - service_level: 0.95 - SKU546: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 88 - init_stock: 85 - max_stock: 1000 - price: 168 - sale_gamma: 17 - service_level: 0.95 - SKU547: - constraint: null - cost: 454 - init_stock: 384 - max_stock: 1000 - price: 744 - sale_gamma: 96 - service_level: 0.95 - SKU548: - constraint: null - cost: 415 - init_stock: 637 - max_stock: 1000 - price: 489 - sale_gamma: 91 - service_level: 0.95 - SKU549: - constraint: null - cost: 307 - init_stock: 290 - max_stock: 1000 - price: 435 - sale_gamma: 58 - service_level: 0.95 - SKU55: - constraint: G(low_profit -> low_stock_constraint) - cost: 106 - init_stock: 147 - max_stock: 1000 - price: 163 - sale_gamma: 21 - service_level: 0.95 - SKU550: - constraint: null - cost: 446 - init_stock: 76 - max_stock: 1000 - price: 611 - sale_gamma: 19 - service_level: 0.95 - SKU551: - constraint: null - cost: 393 - init_stock: 420 - max_stock: 1000 - price: 577 - sale_gamma: 60 - service_level: 0.95 - SKU552: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 114 - init_stock: 504 - max_stock: 1000 - price: 224 - sale_gamma: 84 - service_level: 0.95 - SKU553: - constraint: null - cost: 71 - init_stock: 245 - max_stock: 1000 - price: 112 - sale_gamma: 35 - service_level: 0.95 - SKU554: - constraint: null - cost: 314 - init_stock: 39 - max_stock: 1000 - price: 411 - sale_gamma: 13 - service_level: 0.95 - SKU555: - constraint: G(stock_constraint) - cost: 492 - init_stock: 68 - max_stock: 1000 - price: 585 - sale_gamma: 17 - service_level: 0.95 - SKU556: - constraint: null - cost: 96 - init_stock: 198 - max_stock: 1000 - price: 187 - sale_gamma: 66 - service_level: 0.95 - SKU557: - constraint: null - cost: 303 - init_stock: 576 - max_stock: 1000 - price: 515 - sale_gamma: 72 - service_level: 0.95 - SKU558: - constraint: null - cost: 430 - init_stock: 126 - max_stock: 1000 - price: 748 - sale_gamma: 21 - service_level: 0.95 - SKU559: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 222 - init_stock: 354 - max_stock: 1000 - price: 357 - sale_gamma: 59 - service_level: 0.95 - SKU56: - constraint: null - cost: 435 - init_stock: 92 - max_stock: 1000 - price: 539 - sale_gamma: 23 - service_level: 0.95 - SKU560: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 146 - init_stock: 534 - max_stock: 1000 - price: 290 - sale_gamma: 89 - service_level: 0.95 - SKU561: - constraint: null - cost: 158 - init_stock: 287 - max_stock: 1000 - price: 243 - sale_gamma: 41 - service_level: 0.95 - SKU562: - constraint: G(low_profit -> low_stock_constraint) - cost: 51 - init_stock: 370 - max_stock: 1000 - price: 96 - sale_gamma: 74 - service_level: 0.95 - SKU563: - constraint: G(low_profit -> low_stock_constraint) - cost: 418 - init_stock: 75 - max_stock: 1000 - price: 727 - sale_gamma: 25 - service_level: 0.95 - SKU564: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 442 - init_stock: 120 - max_stock: 1000 - price: 800 - sale_gamma: 24 - service_level: 0.95 - SKU565: - constraint: G(stock_constraint) - cost: 310 - init_stock: 348 - max_stock: 1000 - price: 582 - sale_gamma: 87 - service_level: 0.95 - SKU566: - constraint: G(low_profit -> low_stock_constraint) - cost: 34 - init_stock: 30 - max_stock: 1000 - price: 46 - sale_gamma: 6 - service_level: 0.95 - SKU567: - constraint: null - cost: 120 - init_stock: 172 - max_stock: 1000 - price: 195 - sale_gamma: 43 - service_level: 0.95 - SKU568: - constraint: null - cost: 97 - init_stock: 32 - max_stock: 1000 - price: 109 - sale_gamma: 8 - service_level: 0.95 - SKU569: - constraint: null - cost: 285 - init_stock: 376 - max_stock: 1000 - price: 413 - sale_gamma: 94 - service_level: 0.95 - SKU57: - constraint: null - cost: 400 - init_stock: 210 - max_stock: 1000 - price: 688 - sale_gamma: 30 - service_level: 0.95 - SKU570: - constraint: null - cost: 161 - init_stock: 301 - max_stock: 1000 - price: 209 - sale_gamma: 43 - service_level: 0.95 - SKU571: - constraint: null - cost: 50 - init_stock: 410 - max_stock: 1000 - price: 91 - sale_gamma: 82 - service_level: 0.95 - SKU572: - constraint: null - cost: 489 - init_stock: 144 - max_stock: 1000 - price: 655 - sale_gamma: 48 - service_level: 0.95 - SKU573: - constraint: null - cost: 274 - init_stock: 259 - max_stock: 1000 - price: 476 - sale_gamma: 37 - service_level: 0.95 - SKU574: - constraint: null - cost: 357 - init_stock: 224 - max_stock: 1000 - price: 392 - sale_gamma: 32 - service_level: 0.95 - SKU575: - constraint: null - cost: 265 - init_stock: 588 - max_stock: 1000 - price: 511 - sale_gamma: 98 - service_level: 0.95 - SKU576: - constraint: null - cost: 137 - init_stock: 335 - max_stock: 1000 - price: 165 - sale_gamma: 67 - service_level: 0.95 - SKU577: - constraint: null - cost: 420 - init_stock: 320 - max_stock: 1000 - price: 793 - sale_gamma: 64 - service_level: 0.95 - SKU578: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 74 - init_stock: 120 - max_stock: 1000 - price: 115 - sale_gamma: 15 - service_level: 0.95 - SKU579: - constraint: G(low_profit -> low_stock_constraint) - cost: 310 - init_stock: 280 - max_stock: 1000 - price: 589 - sale_gamma: 56 - service_level: 0.95 - SKU58: - constraint: G(low_profit -> low_stock_constraint) - cost: 217 - init_stock: 486 - max_stock: 1000 - price: 318 - sale_gamma: 81 - service_level: 0.95 - SKU580: - constraint: null - cost: 153 - init_stock: 594 - max_stock: 1000 - price: 241 - sale_gamma: 99 - service_level: 0.95 - SKU581: - constraint: null - cost: 494 - init_stock: 306 - max_stock: 1000 - price: 696 - sale_gamma: 51 - service_level: 0.95 - SKU582: - constraint: null - cost: 476 - init_stock: 52 - max_stock: 1000 - price: 932 - sale_gamma: 13 - service_level: 0.95 - SKU583: - constraint: G(low_profit -> low_stock_constraint) - cost: 105 - init_stock: 55 - max_stock: 1000 - price: 132 - sale_gamma: 11 - service_level: 0.95 - SKU584: - constraint: null - cost: 365 - init_stock: 235 - max_stock: 1000 - price: 591 - sale_gamma: 47 - service_level: 0.95 - SKU585: - constraint: G(stock_constraint) - cost: 184 - init_stock: 216 - max_stock: 1000 - price: 331 - sale_gamma: 54 - service_level: 0.95 - SKU586: - constraint: null - cost: 471 - init_stock: 470 - max_stock: 1000 - price: 819 - sale_gamma: 94 - service_level: 0.95 - SKU587: - constraint: G(low_profit -> low_stock_constraint) - cost: 197 - init_stock: 160 - max_stock: 1000 - price: 364 - sale_gamma: 80 - service_level: 0.95 - SKU588: - constraint: null - cost: 15 - init_stock: 189 - max_stock: 1000 - price: 22 - sale_gamma: 27 - service_level: 0.95 - SKU589: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 185 - init_stock: 125 - max_stock: 1000 - price: 325 - sale_gamma: 25 - service_level: 0.95 - SKU59: - constraint: null - cost: 39 - init_stock: 96 - max_stock: 1000 - price: 47 - sale_gamma: 48 - service_level: 0.95 - SKU590: - constraint: null - cost: 348 - init_stock: 310 - max_stock: 1000 - price: 480 - sale_gamma: 62 - service_level: 0.95 - SKU591: - constraint: null - cost: 163 - init_stock: 148 - max_stock: 1000 - price: 237 - sale_gamma: 37 - service_level: 0.95 - SKU592: - constraint: G(low_profit -> low_stock_constraint) - cost: 305 - init_stock: 552 - max_stock: 1000 - price: 497 - sale_gamma: 92 - service_level: 0.95 - SKU593: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 204 - max_stock: 1000 - price: 871 - sale_gamma: 51 - service_level: 0.95 - SKU594: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 478 - init_stock: 204 - max_stock: 1000 - price: 855 - sale_gamma: 51 - service_level: 0.95 - SKU595: - constraint: G(stock_constraint) - cost: 35 - init_stock: 110 - max_stock: 1000 - price: 57 - sale_gamma: 55 - service_level: 0.95 - SKU596: - constraint: null - cost: 449 - init_stock: 189 - max_stock: 1000 - price: 884 - sale_gamma: 27 - service_level: 0.95 - SKU597: - constraint: G(stock_constraint) - cost: 21 - init_stock: 384 - max_stock: 1000 - price: 31 - sale_gamma: 96 - service_level: 0.95 - SKU598: - constraint: G(low_profit -> low_stock_constraint) - cost: 49 - init_stock: 195 - max_stock: 1000 - price: 67 - sale_gamma: 39 - service_level: 0.95 - SKU599: - constraint: null - cost: 201 - init_stock: 380 - max_stock: 1000 - price: 391 - sale_gamma: 76 - service_level: 0.95 - SKU6: - constraint: null - cost: 472 - init_stock: 328 - max_stock: 1000 - price: 741 - sale_gamma: 41 - service_level: 0.95 - SKU60: - constraint: G(stock_constraint) - cost: 24 - init_stock: 255 - max_stock: 1000 - price: 33 - sale_gamma: 85 - service_level: 0.95 - SKU600: - constraint: G(low_profit -> low_stock_constraint) - cost: 321 - init_stock: 165 - max_stock: 1000 - price: 404 - sale_gamma: 55 - service_level: 0.95 - SKU601: - constraint: G(low_profit -> low_stock_constraint) - cost: 185 - init_stock: 60 - max_stock: 1000 - price: 301 - sale_gamma: 10 - service_level: 0.95 - SKU602: - constraint: G(low_profit -> low_stock_constraint) - cost: 180 - init_stock: 564 - max_stock: 1000 - price: 237 - sale_gamma: 94 - service_level: 0.95 - SKU603: - constraint: null - cost: 328 - init_stock: 665 - max_stock: 1000 - price: 367 - sale_gamma: 95 - service_level: 0.95 - SKU604: - constraint: null - cost: 143 - init_stock: 576 - max_stock: 1000 - price: 194 - sale_gamma: 96 - service_level: 0.95 - SKU605: - constraint: null - cost: 337 - init_stock: 44 - max_stock: 1000 - price: 461 - sale_gamma: 22 - service_level: 0.95 - SKU606: - constraint: G(low_profit -> low_stock_constraint) - cost: 166 - init_stock: 150 - max_stock: 1000 - price: 252 - sale_gamma: 25 - service_level: 0.95 - SKU607: - constraint: null - cost: 286 - init_stock: 405 - max_stock: 1000 - price: 363 - sale_gamma: 81 - service_level: 0.95 - SKU608: - constraint: G(stock_constraint) - cost: 18 - init_stock: 123 - max_stock: 1000 - price: 36 - sale_gamma: 41 - service_level: 0.95 - SKU609: - constraint: null - cost: 330 - init_stock: 390 - max_stock: 1000 - price: 435 - sale_gamma: 78 - service_level: 0.95 - SKU61: - constraint: null - cost: 422 - init_stock: 60 - max_stock: 1000 - price: 738 - sale_gamma: 10 - service_level: 0.95 - SKU610: - constraint: null - cost: 489 - init_stock: 266 - max_stock: 1000 - price: 885 - sale_gamma: 38 - service_level: 0.95 - SKU611: - constraint: null - cost: 379 - init_stock: 140 - max_stock: 1000 - price: 640 - sale_gamma: 35 - service_level: 0.95 - SKU612: - constraint: G(stock_constraint) - cost: 28 - init_stock: 114 - max_stock: 1000 - price: 44 - sale_gamma: 38 - service_level: 0.95 - SKU613: - constraint: null - cost: 303 - init_stock: 147 - max_stock: 1000 - price: 333 - sale_gamma: 49 - service_level: 0.95 - SKU614: - constraint: null - cost: 264 - init_stock: 148 - max_stock: 1000 - price: 512 - sale_gamma: 37 - service_level: 0.95 - SKU615: - constraint: null - cost: 337 - init_stock: 246 - max_stock: 1000 - price: 647 - sale_gamma: 41 - service_level: 0.95 - SKU616: - constraint: null - cost: 309 - init_stock: 366 - max_stock: 1000 - price: 423 - sale_gamma: 61 - service_level: 0.95 - SKU617: - constraint: null - cost: 237 - init_stock: 208 - max_stock: 1000 - price: 471 - sale_gamma: 52 - service_level: 0.95 - SKU618: - constraint: null - cost: 143 - init_stock: 287 - max_stock: 1000 - price: 278 - sale_gamma: 41 - service_level: 0.95 - SKU619: - constraint: null - cost: 60 - init_stock: 216 - max_stock: 1000 - price: 115 - sale_gamma: 36 - service_level: 0.95 - SKU62: - constraint: null - cost: 92 - init_stock: 287 - max_stock: 1000 - price: 124 - sale_gamma: 41 - service_level: 0.95 - SKU620: - constraint: null - cost: 478 - init_stock: 147 - max_stock: 1000 - price: 807 - sale_gamma: 49 - service_level: 0.95 - SKU621: - constraint: null - cost: 216 - init_stock: 150 - max_stock: 1000 - price: 347 - sale_gamma: 30 - service_level: 0.95 - SKU622: - constraint: null - cost: 270 - init_stock: 120 - max_stock: 1000 - price: 513 - sale_gamma: 24 - service_level: 0.95 - SKU623: - constraint: null - cost: 448 - init_stock: 190 - max_stock: 1000 - price: 698 - sale_gamma: 38 - service_level: 0.95 - SKU624: - constraint: G(stock_constraint) - cost: 429 - init_stock: 220 - max_stock: 1000 - price: 600 - sale_gamma: 55 - service_level: 0.95 - SKU625: - constraint: null - cost: 236 - init_stock: 468 - max_stock: 1000 - price: 448 - sale_gamma: 78 - service_level: 0.95 - SKU626: - constraint: null - cost: 135 - init_stock: 128 - max_stock: 1000 - price: 170 - sale_gamma: 32 - service_level: 0.95 - SKU627: - constraint: null - cost: 78 - init_stock: 312 - max_stock: 1000 - price: 102 - sale_gamma: 39 - service_level: 0.95 - SKU628: - constraint: null - cost: 114 - init_stock: 528 - max_stock: 1000 - price: 176 - sale_gamma: 88 - service_level: 0.95 - SKU629: - constraint: G(low_profit -> low_stock_constraint) - cost: 390 - init_stock: 324 - max_stock: 1000 - price: 557 - sale_gamma: 54 - service_level: 0.95 - SKU63: - constraint: null - cost: 341 - init_stock: 138 - max_stock: 1000 - price: 579 - sale_gamma: 69 - service_level: 0.95 - SKU630: - constraint: null - cost: 500 - init_stock: 168 - max_stock: 1000 - price: 750 - sale_gamma: 84 - service_level: 0.95 - SKU631: - constraint: null - cost: 386 - init_stock: 511 - max_stock: 1000 - price: 683 - sale_gamma: 73 - service_level: 0.95 - SKU632: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 72 - init_stock: 238 - max_stock: 1000 - price: 142 - sale_gamma: 34 - service_level: 0.95 - SKU633: - constraint: null - cost: 306 - init_stock: 290 - max_stock: 1000 - price: 529 - sale_gamma: 58 - service_level: 0.95 - SKU634: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 136 - init_stock: 504 - max_stock: 1000 - price: 229 - sale_gamma: 84 - service_level: 0.95 - SKU635: - constraint: null - cost: 463 - init_stock: 270 - max_stock: 1000 - price: 800 - sale_gamma: 45 - service_level: 0.95 - SKU636: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 243 - init_stock: 285 - max_stock: 1000 - price: 345 - sale_gamma: 57 - service_level: 0.95 - SKU637: - constraint: null - cost: 498 - init_stock: 150 - max_stock: 1000 - price: 552 - sale_gamma: 30 - service_level: 0.95 - SKU638: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 93 - init_stock: 264 - max_stock: 1000 - price: 119 - sale_gamma: 66 - service_level: 0.95 - SKU639: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 476 - init_stock: 40 - max_stock: 1000 - price: 580 - sale_gamma: 10 - service_level: 0.95 - SKU64: - constraint: G(stock_constraint) - cost: 151 - init_stock: 480 - max_stock: 1000 - price: 283 - sale_gamma: 96 - service_level: 0.95 - SKU640: - constraint: null - cost: 350 - init_stock: 280 - max_stock: 1000 - price: 567 - sale_gamma: 56 - service_level: 0.95 - SKU641: - constraint: null - cost: 434 - init_stock: 216 - max_stock: 1000 - price: 729 - sale_gamma: 36 - service_level: 0.95 - SKU642: - constraint: null - cost: 349 - init_stock: 576 - max_stock: 1000 - price: 401 - sale_gamma: 72 - service_level: 0.95 - SKU643: - constraint: null - cost: 17 - init_stock: 54 - max_stock: 1000 - price: 22 - sale_gamma: 27 - service_level: 0.95 - SKU644: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 27 - init_stock: 186 - max_stock: 1000 - price: 46 - sale_gamma: 93 - service_level: 0.95 - SKU645: - constraint: G(low_profit -> low_stock_constraint) - cost: 143 - init_stock: 45 - max_stock: 1000 - price: 174 - sale_gamma: 9 - service_level: 0.95 - SKU646: - constraint: null - cost: 320 - init_stock: 435 - max_stock: 1000 - price: 636 - sale_gamma: 87 - service_level: 0.95 - SKU647: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 208 - max_stock: 1000 - price: 500 - sale_gamma: 26 - service_level: 0.95 - SKU648: - constraint: null - cost: 251 - init_stock: 240 - max_stock: 1000 - price: 293 - sale_gamma: 48 - service_level: 0.95 - SKU649: - constraint: null - cost: 171 - init_stock: 430 - max_stock: 1000 - price: 188 - sale_gamma: 86 - service_level: 0.95 - SKU65: - constraint: null - cost: 48 - init_stock: 400 - max_stock: 1000 - price: 90 - sale_gamma: 80 - service_level: 0.95 - SKU650: - constraint: null - cost: 406 - init_stock: 282 - max_stock: 1000 - price: 633 - sale_gamma: 47 - service_level: 0.95 - SKU651: - constraint: null - cost: 209 - init_stock: 426 - max_stock: 1000 - price: 395 - sale_gamma: 71 - service_level: 0.95 - SKU652: - constraint: G(low_profit -> low_stock_constraint) - cost: 167 - init_stock: 544 - max_stock: 1000 - price: 198 - sale_gamma: 68 - service_level: 0.95 - SKU653: - constraint: null - cost: 122 - init_stock: 48 - max_stock: 1000 - price: 180 - sale_gamma: 6 - service_level: 0.95 - SKU654: - constraint: null - cost: 344 - init_stock: 230 - max_stock: 1000 - price: 674 - sale_gamma: 46 - service_level: 0.95 - SKU655: - constraint: null - cost: 382 - init_stock: 445 - max_stock: 1000 - price: 569 - sale_gamma: 89 - service_level: 0.95 - SKU656: - constraint: null - cost: 229 - init_stock: 665 - max_stock: 1000 - price: 387 - sale_gamma: 95 - service_level: 0.95 - SKU657: - constraint: null - cost: 474 - init_stock: 462 - max_stock: 1000 - price: 838 - sale_gamma: 77 - service_level: 0.95 - SKU658: - constraint: null - cost: 307 - init_stock: 215 - max_stock: 1000 - price: 432 - sale_gamma: 43 - service_level: 0.95 - SKU659: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 392 - init_stock: 125 - max_stock: 1000 - price: 709 - sale_gamma: 25 - service_level: 0.95 - SKU66: - constraint: G(low_profit -> low_stock_constraint) - cost: 492 - init_stock: 150 - max_stock: 1000 - price: 905 - sale_gamma: 50 - service_level: 0.95 - SKU660: - constraint: G(stock_constraint) - cost: 206 - init_stock: 48 - max_stock: 1000 - price: 364 - sale_gamma: 12 - service_level: 0.95 - SKU661: - constraint: G(low_profit -> low_stock_constraint) - cost: 100 - init_stock: 600 - max_stock: 1000 - price: 135 - sale_gamma: 100 - service_level: 0.95 - SKU662: - constraint: G(low_profit -> low_stock_constraint) - cost: 283 - init_stock: 12 - max_stock: 1000 - price: 489 - sale_gamma: 6 - service_level: 0.95 - SKU663: - constraint: null - cost: 416 - init_stock: 220 - max_stock: 1000 - price: 777 - sale_gamma: 44 - service_level: 0.95 - SKU664: - constraint: null - cost: 93 - init_stock: 210 - max_stock: 1000 - price: 135 - sale_gamma: 70 - service_level: 0.95 - SKU665: - constraint: null - cost: 415 - init_stock: 594 - max_stock: 1000 - price: 705 - sale_gamma: 99 - service_level: 0.95 - SKU666: - constraint: null - cost: 378 - init_stock: 20 - max_stock: 1000 - price: 597 - sale_gamma: 5 - service_level: 0.95 - SKU667: - constraint: null - cost: 114 - init_stock: 390 - max_stock: 1000 - price: 159 - sale_gamma: 78 - service_level: 0.95 - SKU668: - constraint: null - cost: 115 - init_stock: 48 - max_stock: 1000 - price: 161 - sale_gamma: 16 - service_level: 0.95 - SKU669: - constraint: null - cost: 414 - init_stock: 140 - max_stock: 1000 - price: 654 - sale_gamma: 20 - service_level: 0.95 - SKU67: - constraint: G(stock_constraint) - cost: 16 - init_stock: 248 - max_stock: 1000 - price: 20 - sale_gamma: 62 - service_level: 0.95 - SKU670: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 171 - init_stock: 462 - max_stock: 1000 - price: 340 - sale_gamma: 66 - service_level: 0.95 - SKU671: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 163 - init_stock: 234 - max_stock: 1000 - price: 252 - sale_gamma: 39 - service_level: 0.95 - SKU672: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 33 - max_stock: 1000 - price: 128 - sale_gamma: 11 - service_level: 0.95 - SKU673: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 437 - init_stock: 592 - max_stock: 1000 - price: 804 - sale_gamma: 74 - service_level: 0.95 - SKU674: - constraint: null - cost: 50 - init_stock: 133 - max_stock: 1000 - price: 85 - sale_gamma: 19 - service_level: 0.95 - SKU675: - constraint: null - cost: 479 - init_stock: 100 - max_stock: 1000 - price: 555 - sale_gamma: 20 - service_level: 0.95 - SKU676: - constraint: G(low_profit -> low_stock_constraint) - cost: 460 - init_stock: 196 - max_stock: 1000 - price: 542 - sale_gamma: 49 - service_level: 0.95 - SKU677: - constraint: G(stock_constraint) - cost: 392 - init_stock: 470 - max_stock: 1000 - price: 666 - sale_gamma: 94 - service_level: 0.95 - SKU678: - constraint: null - cost: 143 - init_stock: 268 - max_stock: 1000 - price: 195 - sale_gamma: 67 - service_level: 0.95 - SKU679: - constraint: G(stock_constraint) - cost: 131 - init_stock: 492 - max_stock: 1000 - price: 248 - sale_gamma: 82 - service_level: 0.95 - SKU68: - constraint: G(stock_constraint) - cost: 108 - init_stock: 322 - max_stock: 1000 - price: 212 - sale_gamma: 46 - service_level: 0.95 - SKU680: - constraint: null - cost: 436 - init_stock: 48 - max_stock: 1000 - price: 566 - sale_gamma: 24 - service_level: 0.95 - SKU681: - constraint: null - cost: 338 - init_stock: 65 - max_stock: 1000 - price: 557 - sale_gamma: 13 - service_level: 0.95 - SKU682: - constraint: null - cost: 146 - init_stock: 80 - max_stock: 1000 - price: 251 - sale_gamma: 16 - service_level: 0.95 - SKU683: - constraint: null - cost: 486 - init_stock: 144 - max_stock: 1000 - price: 903 - sale_gamma: 36 - service_level: 0.95 - SKU684: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 288 - max_stock: 1000 - price: 466 - sale_gamma: 72 - service_level: 0.95 - SKU685: - constraint: null - cost: 69 - init_stock: 462 - max_stock: 1000 - price: 89 - sale_gamma: 77 - service_level: 0.95 - SKU686: - constraint: null - cost: 56 - init_stock: 135 - max_stock: 1000 - price: 63 - sale_gamma: 27 - service_level: 0.95 - SKU687: - constraint: null - cost: 308 - init_stock: 144 - max_stock: 1000 - price: 502 - sale_gamma: 48 - service_level: 0.95 - SKU688: - constraint: null - cost: 200 - init_stock: 159 - max_stock: 1000 - price: 256 - sale_gamma: 53 - service_level: 0.95 - SKU689: - constraint: G(stock_constraint) - cost: 36 - init_stock: 80 - max_stock: 1000 - price: 42 - sale_gamma: 16 - service_level: 0.95 - SKU69: - constraint: G(low_profit -> low_stock_constraint) - cost: 329 - init_stock: 168 - max_stock: 1000 - price: 628 - sale_gamma: 28 - service_level: 0.95 - SKU690: - constraint: G(low_profit -> low_stock_constraint) - cost: 283 - init_stock: 21 - max_stock: 1000 - price: 455 - sale_gamma: 7 - service_level: 0.95 - SKU691: - constraint: null - cost: 422 - init_stock: 294 - max_stock: 1000 - price: 776 - sale_gamma: 98 - service_level: 0.95 - SKU692: - constraint: null - cost: 457 - init_stock: 325 - max_stock: 1000 - price: 539 - sale_gamma: 65 - service_level: 0.95 - SKU693: - constraint: null - cost: 351 - init_stock: 240 - max_stock: 1000 - price: 414 - sale_gamma: 48 - service_level: 0.95 - SKU694: - constraint: G(stock_constraint) - cost: 441 - init_stock: 256 - max_stock: 1000 - price: 630 - sale_gamma: 32 - service_level: 0.95 - SKU695: - constraint: G(stock_constraint) - cost: 344 - init_stock: 490 - max_stock: 1000 - price: 550 - sale_gamma: 98 - service_level: 0.95 - SKU696: - constraint: null - cost: 378 - init_stock: 175 - max_stock: 1000 - price: 597 - sale_gamma: 35 - service_level: 0.95 - SKU697: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 483 - init_stock: 552 - max_stock: 1000 - price: 859 - sale_gamma: 92 - service_level: 0.95 - SKU698: - constraint: null - cost: 53 - init_stock: 350 - max_stock: 1000 - price: 91 - sale_gamma: 50 - service_level: 0.95 - SKU699: - constraint: G(stock_constraint) - cost: 265 - init_stock: 192 - max_stock: 1000 - price: 291 - sale_gamma: 64 - service_level: 0.95 - SKU7: - constraint: null - cost: 389 - init_stock: 36 - max_stock: 1000 - price: 634 - sale_gamma: 12 - service_level: 0.95 - SKU70: - constraint: null - cost: 140 - init_stock: 152 - max_stock: 1000 - price: 186 - sale_gamma: 38 - service_level: 0.95 - SKU700: - constraint: G(stock_constraint) - cost: 122 - init_stock: 120 - max_stock: 1000 - price: 165 - sale_gamma: 20 - service_level: 0.95 - SKU701: - constraint: null - cost: 394 - init_stock: 114 - max_stock: 1000 - price: 693 - sale_gamma: 38 - service_level: 0.95 - SKU702: - constraint: G(stock_constraint) - cost: 78 - init_stock: 108 - max_stock: 1000 - price: 146 - sale_gamma: 27 - service_level: 0.95 - SKU703: - constraint: null - cost: 472 - init_stock: 355 - max_stock: 1000 - price: 892 - sale_gamma: 71 - service_level: 0.95 - SKU704: - constraint: G(stock_constraint) - cost: 384 - init_stock: 132 - max_stock: 1000 - price: 480 - sale_gamma: 44 - service_level: 0.95 - SKU705: - constraint: null - cost: 486 - init_stock: 224 - max_stock: 1000 - price: 937 - sale_gamma: 56 - service_level: 0.95 - SKU706: - constraint: null - cost: 222 - init_stock: 185 - max_stock: 1000 - price: 330 - sale_gamma: 37 - service_level: 0.95 - SKU707: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 195 - init_stock: 185 - max_stock: 1000 - price: 339 - sale_gamma: 37 - service_level: 0.95 - SKU708: - constraint: null - cost: 131 - init_stock: 396 - max_stock: 1000 - price: 155 - sale_gamma: 99 - service_level: 0.95 - SKU709: - constraint: null - cost: 137 - init_stock: 210 - max_stock: 1000 - price: 221 - sale_gamma: 35 - service_level: 0.95 - SKU71: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 381 - init_stock: 148 - max_stock: 1000 - price: 464 - sale_gamma: 37 - service_level: 0.95 - SKU710: - constraint: null - cost: 186 - init_stock: 56 - max_stock: 1000 - price: 228 - sale_gamma: 14 - service_level: 0.95 - SKU711: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 190 - init_stock: 366 - max_stock: 1000 - price: 300 - sale_gamma: 61 - service_level: 0.95 - SKU712: - constraint: null - cost: 384 - init_stock: 252 - max_stock: 1000 - price: 426 - sale_gamma: 36 - service_level: 0.95 - SKU713: - constraint: G(low_profit -> low_stock_constraint) - cost: 229 - init_stock: 276 - max_stock: 1000 - price: 368 - sale_gamma: 69 - service_level: 0.95 - SKU714: - constraint: G(low_profit -> low_stock_constraint) - cost: 364 - init_stock: 360 - max_stock: 1000 - price: 469 - sale_gamma: 90 - service_level: 0.95 - SKU715: - constraint: G(stock_constraint) - cost: 235 - init_stock: 63 - max_stock: 1000 - price: 430 - sale_gamma: 9 - service_level: 0.95 - SKU716: - constraint: null - cost: 108 - init_stock: 623 - max_stock: 1000 - price: 139 - sale_gamma: 89 - service_level: 0.95 - SKU717: - constraint: G(low_profit -> low_stock_constraint) - cost: 12 - init_stock: 48 - max_stock: 1000 - price: 20 - sale_gamma: 12 - service_level: 0.95 - SKU718: - constraint: null - cost: 397 - init_stock: 132 - max_stock: 1000 - price: 460 - sale_gamma: 22 - service_level: 0.95 - SKU719: - constraint: G(stock_constraint) - cost: 21 - init_stock: 148 - max_stock: 1000 - price: 34 - sale_gamma: 37 - service_level: 0.95 - SKU72: - constraint: null - cost: 228 - init_stock: 388 - max_stock: 1000 - price: 319 - sale_gamma: 97 - service_level: 0.95 - SKU720: - constraint: G(stock_constraint) - cost: 120 - init_stock: 288 - max_stock: 1000 - price: 211 - sale_gamma: 48 - service_level: 0.95 - SKU721: - constraint: G(low_profit -> low_stock_constraint) - cost: 265 - init_stock: 332 - max_stock: 1000 - price: 431 - sale_gamma: 83 - service_level: 0.95 - SKU722: - constraint: G(low_profit -> low_stock_constraint) - cost: 353 - init_stock: 232 - max_stock: 1000 - price: 511 - sale_gamma: 58 - service_level: 0.95 - SKU723: - constraint: null - cost: 144 - init_stock: 595 - max_stock: 1000 - price: 239 - sale_gamma: 85 - service_level: 0.95 - SKU724: - constraint: G(stock_constraint) - cost: 120 - init_stock: 142 - max_stock: 1000 - price: 187 - sale_gamma: 71 - service_level: 0.95 - SKU725: - constraint: G(stock_constraint) - cost: 42 - init_stock: 356 - max_stock: 1000 - price: 49 - sale_gamma: 89 - service_level: 0.95 - SKU726: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 132 - init_stock: 696 - max_stock: 1000 - price: 183 - sale_gamma: 87 - service_level: 0.95 - SKU727: - constraint: null - cost: 103 - init_stock: 105 - max_stock: 1000 - price: 145 - sale_gamma: 35 - service_level: 0.95 - SKU728: - constraint: G(low_profit -> low_stock_constraint) - cost: 29 - init_stock: 204 - max_stock: 1000 - price: 56 - sale_gamma: 34 - service_level: 0.95 - SKU729: - constraint: null - cost: 251 - init_stock: 679 - max_stock: 1000 - price: 353 - sale_gamma: 97 - service_level: 0.95 - SKU73: - constraint: G(stock_constraint) - cost: 334 - init_stock: 365 - max_stock: 1000 - price: 634 - sale_gamma: 73 - service_level: 0.95 - SKU730: - constraint: null - cost: 71 - init_stock: 512 - max_stock: 1000 - price: 139 - sale_gamma: 64 - service_level: 0.95 - SKU731: - constraint: null - cost: 480 - init_stock: 276 - max_stock: 1000 - price: 604 - sale_gamma: 92 - service_level: 0.95 - SKU732: - constraint: G(low_profit -> low_stock_constraint) - cost: 231 - init_stock: 402 - max_stock: 1000 - price: 438 - sale_gamma: 67 - service_level: 0.95 - SKU733: - constraint: null - cost: 245 - init_stock: 196 - max_stock: 1000 - price: 450 - sale_gamma: 28 - service_level: 0.95 - SKU734: - constraint: G(low_profit -> low_stock_constraint) - cost: 117 - init_stock: 116 - max_stock: 1000 - price: 187 - sale_gamma: 58 - service_level: 0.95 - SKU735: - constraint: G(low_profit -> low_stock_constraint) - cost: 197 - init_stock: 182 - max_stock: 1000 - price: 279 - sale_gamma: 26 - service_level: 0.95 - SKU736: - constraint: G(stock_constraint) - cost: 221 - init_stock: 496 - max_stock: 1000 - price: 282 - sale_gamma: 62 - service_level: 0.95 - SKU737: - constraint: null - cost: 260 - init_stock: 595 - max_stock: 1000 - price: 488 - sale_gamma: 85 - service_level: 0.95 - SKU738: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 184 - max_stock: 1000 - price: 557 - sale_gamma: 46 - service_level: 0.95 - SKU739: - constraint: null - cost: 451 - init_stock: 266 - max_stock: 1000 - price: 699 - sale_gamma: 38 - service_level: 0.95 - SKU74: - constraint: null - cost: 363 - init_stock: 80 - max_stock: 1000 - price: 511 - sale_gamma: 10 - service_level: 0.95 - SKU740: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 250 - init_stock: 10 - max_stock: 1000 - price: 500 - sale_gamma: 5 - service_level: 0.95 - SKU741: - constraint: null - cost: 139 - init_stock: 68 - max_stock: 1000 - price: 266 - sale_gamma: 17 - service_level: 0.95 - SKU742: - constraint: null - cost: 174 - init_stock: 445 - max_stock: 1000 - price: 194 - sale_gamma: 89 - service_level: 0.95 - SKU743: - constraint: null - cost: 79 - init_stock: 574 - max_stock: 1000 - price: 106 - sale_gamma: 82 - service_level: 0.95 - SKU744: - constraint: null - cost: 304 - init_stock: 312 - max_stock: 1000 - price: 367 - sale_gamma: 39 - service_level: 0.95 - SKU745: - constraint: null - cost: 105 - init_stock: 465 - max_stock: 1000 - price: 191 - sale_gamma: 93 - service_level: 0.95 - SKU746: - constraint: null - cost: 397 - init_stock: 282 - max_stock: 1000 - price: 643 - sale_gamma: 94 - service_level: 0.95 - SKU747: - constraint: null - cost: 426 - init_stock: 72 - max_stock: 1000 - price: 498 - sale_gamma: 36 - service_level: 0.95 - SKU748: - constraint: null - cost: 169 - init_stock: 235 - max_stock: 1000 - price: 305 - sale_gamma: 47 - service_level: 0.95 - SKU749: - constraint: null - cost: 433 - init_stock: 168 - max_stock: 1000 - price: 697 - sale_gamma: 42 - service_level: 0.95 - SKU75: - constraint: G(low_profit -> low_stock_constraint) - cost: 179 - init_stock: 325 - max_stock: 1000 - price: 277 - sale_gamma: 65 - service_level: 0.95 - SKU750: - constraint: null - cost: 373 - init_stock: 408 - max_stock: 1000 - price: 555 - sale_gamma: 68 - service_level: 0.95 - SKU751: - constraint: G(low_profit -> low_stock_constraint) - cost: 196 - init_stock: 238 - max_stock: 1000 - price: 270 - sale_gamma: 34 - service_level: 0.95 - SKU752: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 67 - init_stock: 20 - max_stock: 1000 - price: 123 - sale_gamma: 10 - service_level: 0.95 - SKU753: - constraint: null - cost: 162 - init_stock: 122 - max_stock: 1000 - price: 319 - sale_gamma: 61 - service_level: 0.95 - SKU754: - constraint: null - cost: 206 - init_stock: 256 - max_stock: 1000 - price: 278 - sale_gamma: 64 - service_level: 0.95 - SKU755: - constraint: null - cost: 125 - init_stock: 86 - max_stock: 1000 - price: 223 - sale_gamma: 43 - service_level: 0.95 - SKU756: - constraint: G(stock_constraint) - cost: 215 - init_stock: 246 - max_stock: 1000 - price: 290 - sale_gamma: 82 - service_level: 0.95 - SKU757: - constraint: null - cost: 263 - init_stock: 176 - max_stock: 1000 - price: 341 - sale_gamma: 22 - service_level: 0.95 - SKU758: - constraint: null - cost: 70 - init_stock: 609 - max_stock: 1000 - price: 127 - sale_gamma: 87 - service_level: 0.95 - SKU759: - constraint: null - cost: 310 - init_stock: 395 - max_stock: 1000 - price: 616 - sale_gamma: 79 - service_level: 0.95 - SKU76: - constraint: null - cost: 464 - init_stock: 336 - max_stock: 1000 - price: 709 - sale_gamma: 84 - service_level: 0.95 - SKU760: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 110 - max_stock: 1000 - price: 949 - sale_gamma: 22 - service_level: 0.95 - SKU761: - constraint: G(stock_constraint) - cost: 288 - init_stock: 93 - max_stock: 1000 - price: 414 - sale_gamma: 31 - service_level: 0.95 - SKU762: - constraint: G(stock_constraint) - cost: 374 - init_stock: 310 - max_stock: 1000 - price: 650 - sale_gamma: 62 - service_level: 0.95 - SKU763: - constraint: null - cost: 121 - init_stock: 30 - max_stock: 1000 - price: 174 - sale_gamma: 6 - service_level: 0.95 - SKU764: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 494 - init_stock: 190 - max_stock: 1000 - price: 637 - sale_gamma: 38 - service_level: 0.95 - SKU765: - constraint: G(stock_constraint) - cost: 459 - init_stock: 356 - max_stock: 1000 - price: 523 - sale_gamma: 89 - service_level: 0.95 - SKU766: - constraint: null - cost: 249 - init_stock: 216 - max_stock: 1000 - price: 433 - sale_gamma: 54 - service_level: 0.95 - SKU767: - constraint: G(low_profit -> low_stock_constraint) - cost: 245 - init_stock: 528 - max_stock: 1000 - price: 360 - sale_gamma: 88 - service_level: 0.95 - SKU768: - constraint: null - cost: 429 - init_stock: 425 - max_stock: 1000 - price: 600 - sale_gamma: 85 - service_level: 0.95 - SKU769: - constraint: G(low_profit -> low_stock_constraint) - cost: 324 - init_stock: 192 - max_stock: 1000 - price: 521 - sale_gamma: 48 - service_level: 0.95 - SKU77: - constraint: G(low_profit -> low_stock_constraint) - cost: 281 - init_stock: 168 - max_stock: 1000 - price: 323 - sale_gamma: 28 - service_level: 0.95 - SKU770: - constraint: G(stock_constraint) - cost: 362 - init_stock: 152 - max_stock: 1000 - price: 477 - sale_gamma: 76 - service_level: 0.95 - SKU771: - constraint: G(low_profit -> low_stock_constraint) - cost: 233 - init_stock: 135 - max_stock: 1000 - price: 379 - sale_gamma: 27 - service_level: 0.95 - SKU772: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 152 - init_stock: 64 - max_stock: 1000 - price: 297 - sale_gamma: 16 - service_level: 0.95 - SKU773: - constraint: null - cost: 337 - init_stock: 768 - max_stock: 1000 - price: 539 - sale_gamma: 96 - service_level: 0.95 - SKU774: - constraint: null - cost: 232 - init_stock: 45 - max_stock: 1000 - price: 255 - sale_gamma: 9 - service_level: 0.95 - SKU775: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 393 - init_stock: 96 - max_stock: 1000 - price: 695 - sale_gamma: 12 - service_level: 0.95 - SKU776: - constraint: null - cost: 342 - init_stock: 336 - max_stock: 1000 - price: 680 - sale_gamma: 48 - service_level: 0.95 - SKU777: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 354 - max_stock: 1000 - price: 466 - sale_gamma: 59 - service_level: 0.95 - SKU778: - constraint: G(low_profit -> low_stock_constraint) - cost: 327 - init_stock: 370 - max_stock: 1000 - price: 614 - sale_gamma: 74 - service_level: 0.95 - SKU779: - constraint: G(stock_constraint) - cost: 109 - init_stock: 352 - max_stock: 1000 - price: 156 - sale_gamma: 44 - service_level: 0.95 - SKU78: - constraint: null - cost: 486 - init_stock: 148 - max_stock: 1000 - price: 544 - sale_gamma: 37 - service_level: 0.95 - SKU780: - constraint: null - cost: 153 - init_stock: 50 - max_stock: 1000 - price: 286 - sale_gamma: 25 - service_level: 0.95 - SKU781: - constraint: null - cost: 126 - init_stock: 21 - max_stock: 1000 - price: 197 - sale_gamma: 7 - service_level: 0.95 - SKU782: - constraint: null - cost: 258 - init_stock: 368 - max_stock: 1000 - price: 479 - sale_gamma: 92 - service_level: 0.95 - SKU783: - constraint: null - cost: 214 - init_stock: 140 - max_stock: 1000 - price: 314 - sale_gamma: 70 - service_level: 0.95 - SKU784: - constraint: null - cost: 426 - init_stock: 245 - max_stock: 1000 - price: 677 - sale_gamma: 35 - service_level: 0.95 - SKU785: - constraint: null - cost: 486 - init_stock: 145 - max_stock: 1000 - price: 588 - sale_gamma: 29 - service_level: 0.95 - SKU786: - constraint: null - cost: 109 - init_stock: 104 - max_stock: 1000 - price: 150 - sale_gamma: 52 - service_level: 0.95 - SKU787: - constraint: null - cost: 74 - init_stock: 84 - max_stock: 1000 - price: 91 - sale_gamma: 21 - service_level: 0.95 - SKU788: - constraint: G(stock_constraint) - cost: 25 - init_stock: 252 - max_stock: 1000 - price: 45 - sale_gamma: 42 - service_level: 0.95 - SKU789: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 265 - init_stock: 273 - max_stock: 1000 - price: 344 - sale_gamma: 39 - service_level: 0.95 - SKU79: - constraint: null - cost: 486 - init_stock: 210 - max_stock: 1000 - price: 636 - sale_gamma: 42 - service_level: 0.95 - SKU790: - constraint: null - cost: 222 - init_stock: 112 - max_stock: 1000 - price: 270 - sale_gamma: 28 - service_level: 0.95 - SKU791: - constraint: null - cost: 495 - init_stock: 270 - max_stock: 1000 - price: 792 - sale_gamma: 54 - service_level: 0.95 - SKU792: - constraint: null - cost: 410 - init_stock: 128 - max_stock: 1000 - price: 451 - sale_gamma: 32 - service_level: 0.95 - SKU793: - constraint: null - cost: 442 - init_stock: 480 - max_stock: 1000 - price: 746 - sale_gamma: 80 - service_level: 0.95 - SKU794: - constraint: G(stock_constraint) - cost: 222 - init_stock: 48 - max_stock: 1000 - price: 330 - sale_gamma: 6 - service_level: 0.95 - SKU795: - constraint: G(low_profit -> low_stock_constraint) - cost: 279 - init_stock: 232 - max_stock: 1000 - price: 368 - sale_gamma: 58 - service_level: 0.95 - SKU796: - constraint: null - cost: 122 - init_stock: 130 - max_stock: 1000 - price: 218 - sale_gamma: 26 - service_level: 0.95 - SKU797: - constraint: null - cost: 107 - init_stock: 300 - max_stock: 1000 - price: 195 - sale_gamma: 100 - service_level: 0.95 - SKU798: - constraint: G(low_profit -> low_stock_constraint) - cost: 383 - init_stock: 250 - max_stock: 1000 - price: 467 - sale_gamma: 50 - service_level: 0.95 - SKU799: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 119 - init_stock: 280 - max_stock: 1000 - price: 215 - sale_gamma: 35 - service_level: 0.95 - SKU8: - constraint: null - cost: 423 - init_stock: 510 - max_stock: 1000 - price: 791 - sale_gamma: 85 - service_level: 0.95 - SKU80: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 324 - init_stock: 180 - max_stock: 1000 - price: 417 - sale_gamma: 36 - service_level: 0.95 - SKU800: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 356 - max_stock: 1000 - price: 353 - sale_gamma: 89 - service_level: 0.95 - SKU801: - constraint: G(stock_constraint) - cost: 65 - init_stock: 672 - max_stock: 1000 - price: 91 - sale_gamma: 84 - service_level: 0.95 - SKU802: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 133 - init_stock: 376 - max_stock: 1000 - price: 155 - sale_gamma: 94 - service_level: 0.95 - SKU803: - constraint: G(stock_constraint) - cost: 209 - init_stock: 175 - max_stock: 1000 - price: 234 - sale_gamma: 35 - service_level: 0.95 - SKU804: - constraint: null - cost: 273 - init_stock: 425 - max_stock: 1000 - price: 526 - sale_gamma: 85 - service_level: 0.95 - SKU805: - constraint: G(low_profit -> low_stock_constraint) - cost: 304 - init_stock: 152 - max_stock: 1000 - price: 349 - sale_gamma: 38 - service_level: 0.95 - SKU806: - constraint: G(low_profit -> low_stock_constraint) - cost: 232 - init_stock: 54 - max_stock: 1000 - price: 269 - sale_gamma: 9 - service_level: 0.95 - SKU807: - constraint: G(low_profit -> low_stock_constraint) - cost: 169 - init_stock: 696 - max_stock: 1000 - price: 216 - sale_gamma: 87 - service_level: 0.95 - SKU808: - constraint: G(stock_constraint) - cost: 350 - init_stock: 45 - max_stock: 1000 - price: 612 - sale_gamma: 9 - service_level: 0.95 - SKU809: - constraint: null - cost: 85 - init_stock: 388 - max_stock: 1000 - price: 161 - sale_gamma: 97 - service_level: 0.95 - SKU81: - constraint: G(low_profit -> low_stock_constraint) - cost: 319 - init_stock: 240 - max_stock: 1000 - price: 504 - sale_gamma: 60 - service_level: 0.95 - SKU810: - constraint: null - cost: 51 - init_stock: 255 - max_stock: 1000 - price: 68 - sale_gamma: 51 - service_level: 0.95 - SKU811: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 53 - init_stock: 324 - max_stock: 1000 - price: 62 - sale_gamma: 54 - service_level: 0.95 - SKU812: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 141 - init_stock: 228 - max_stock: 1000 - price: 266 - sale_gamma: 57 - service_level: 0.95 - SKU813: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 364 - init_stock: 156 - max_stock: 1000 - price: 425 - sale_gamma: 39 - service_level: 0.95 - SKU814: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 79 - init_stock: 768 - max_stock: 1000 - price: 118 - sale_gamma: 96 - service_level: 0.95 - SKU815: - constraint: G(low_profit -> low_stock_constraint) - cost: 178 - init_stock: 64 - max_stock: 1000 - price: 329 - sale_gamma: 16 - service_level: 0.95 - SKU816: - constraint: null - cost: 352 - init_stock: 330 - max_stock: 1000 - price: 587 - sale_gamma: 66 - service_level: 0.95 - SKU817: - constraint: G(low_profit -> low_stock_constraint) - cost: 119 - init_stock: 34 - max_stock: 1000 - price: 209 - sale_gamma: 17 - service_level: 0.95 - SKU818: - constraint: G(low_profit -> low_stock_constraint) - cost: 110 - init_stock: 65 - max_stock: 1000 - price: 189 - sale_gamma: 13 - service_level: 0.95 - SKU819: - constraint: null - cost: 24 - init_stock: 40 - max_stock: 1000 - price: 41 - sale_gamma: 5 - service_level: 0.95 - SKU82: - constraint: null - cost: 413 - init_stock: 560 - max_stock: 1000 - price: 776 - sale_gamma: 70 - service_level: 0.95 - SKU820: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 426 - init_stock: 260 - max_stock: 1000 - price: 736 - sale_gamma: 52 - service_level: 0.95 - SKU821: - constraint: null - cost: 229 - init_stock: 184 - max_stock: 1000 - price: 311 - sale_gamma: 92 - service_level: 0.95 - SKU822: - constraint: G(low_profit -> low_stock_constraint) - cost: 282 - init_stock: 24 - max_stock: 1000 - price: 470 - sale_gamma: 12 - service_level: 0.95 - SKU823: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 221 - init_stock: 364 - max_stock: 1000 - price: 419 - sale_gamma: 91 - service_level: 0.95 - SKU824: - constraint: null - cost: 318 - init_stock: 288 - max_stock: 1000 - price: 616 - sale_gamma: 36 - service_level: 0.95 - SKU825: - constraint: G(low_profit -> low_stock_constraint) - cost: 287 - init_stock: 558 - max_stock: 1000 - price: 375 - sale_gamma: 93 - service_level: 0.95 - SKU826: - constraint: G(low_profit -> low_stock_constraint) - cost: 278 - init_stock: 240 - max_stock: 1000 - price: 380 - sale_gamma: 48 - service_level: 0.95 - SKU827: - constraint: G(stock_constraint) - cost: 312 - init_stock: 378 - max_stock: 1000 - price: 514 - sale_gamma: 63 - service_level: 0.95 - SKU828: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 140 - init_stock: 558 - max_stock: 1000 - price: 261 - sale_gamma: 93 - service_level: 0.95 - SKU829: - constraint: null - cost: 415 - init_stock: 88 - max_stock: 1000 - price: 655 - sale_gamma: 11 - service_level: 0.95 - SKU83: - constraint: null - cost: 327 - init_stock: 462 - max_stock: 1000 - price: 474 - sale_gamma: 77 - service_level: 0.95 - SKU830: - constraint: null - cost: 119 - init_stock: 138 - max_stock: 1000 - price: 205 - sale_gamma: 23 - service_level: 0.95 - SKU831: - constraint: null - cost: 376 - init_stock: 420 - max_stock: 1000 - price: 624 - sale_gamma: 70 - service_level: 0.95 - SKU832: - constraint: null - cost: 289 - init_stock: 322 - max_stock: 1000 - price: 488 - sale_gamma: 46 - service_level: 0.95 - SKU833: - constraint: null - cost: 183 - init_stock: 64 - max_stock: 1000 - price: 325 - sale_gamma: 16 - service_level: 0.95 - SKU834: - constraint: G(stock_constraint) - cost: 398 - init_stock: 264 - max_stock: 1000 - price: 565 - sale_gamma: 66 - service_level: 0.95 - SKU835: - constraint: G(stock_constraint) - cost: 407 - init_stock: 370 - max_stock: 1000 - price: 582 - sale_gamma: 74 - service_level: 0.95 - SKU836: - constraint: G(low_profit -> low_stock_constraint) - cost: 301 - init_stock: 123 - max_stock: 1000 - price: 454 - sale_gamma: 41 - service_level: 0.95 - SKU837: - constraint: G(low_profit -> low_stock_constraint) - cost: 213 - init_stock: 130 - max_stock: 1000 - price: 313 - sale_gamma: 26 - service_level: 0.95 - SKU838: - constraint: null - cost: 23 - init_stock: 252 - max_stock: 1000 - price: 25 - sale_gamma: 42 - service_level: 0.95 - SKU839: - constraint: null - cost: 354 - init_stock: 106 - max_stock: 1000 - price: 679 - sale_gamma: 53 - service_level: 0.95 - SKU84: - constraint: null - cost: 217 - init_stock: 192 - max_stock: 1000 - price: 327 - sale_gamma: 32 - service_level: 0.95 - SKU840: - constraint: G(low_profit -> low_stock_constraint) - cost: 19 - init_stock: 348 - max_stock: 1000 - price: 23 - sale_gamma: 87 - service_level: 0.95 - SKU841: - constraint: G(low_profit -> low_stock_constraint) - cost: 362 - init_stock: 470 - max_stock: 1000 - price: 629 - sale_gamma: 94 - service_level: 0.95 - SKU842: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 73 - init_stock: 273 - max_stock: 1000 - price: 100 - sale_gamma: 91 - service_level: 0.95 - SKU843: - constraint: G(low_profit -> low_stock_constraint) - cost: 169 - init_stock: 215 - max_stock: 1000 - price: 212 - sale_gamma: 43 - service_level: 0.95 - SKU844: - constraint: G(low_profit -> low_stock_constraint) - cost: 325 - init_stock: 144 - max_stock: 1000 - price: 585 - sale_gamma: 36 - service_level: 0.95 - SKU845: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 163 - init_stock: 120 - max_stock: 1000 - price: 237 - sale_gamma: 15 - service_level: 0.95 - SKU846: - constraint: null - cost: 184 - init_stock: 686 - max_stock: 1000 - price: 298 - sale_gamma: 98 - service_level: 0.95 - SKU847: - constraint: null - cost: 441 - init_stock: 136 - max_stock: 1000 - price: 811 - sale_gamma: 34 - service_level: 0.95 - SKU848: - constraint: null - cost: 450 - init_stock: 200 - max_stock: 1000 - price: 751 - sale_gamma: 25 - service_level: 0.95 - SKU849: - constraint: null - cost: 479 - init_stock: 245 - max_stock: 1000 - price: 526 - sale_gamma: 35 - service_level: 0.95 - SKU85: - constraint: null - cost: 49 - init_stock: 152 - max_stock: 1000 - price: 73 - sale_gamma: 76 - service_level: 0.95 - SKU850: - constraint: null - cost: 105 - init_stock: 292 - max_stock: 1000 - price: 180 - sale_gamma: 73 - service_level: 0.95 - SKU851: - constraint: G(stock_constraint) - cost: 366 - init_stock: 539 - max_stock: 1000 - price: 721 - sale_gamma: 77 - service_level: 0.95 - SKU852: - constraint: G(low_profit -> low_stock_constraint) - cost: 392 - init_stock: 312 - max_stock: 1000 - price: 439 - sale_gamma: 78 - service_level: 0.95 - SKU853: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 60 - max_stock: 1000 - price: 181 - sale_gamma: 10 - service_level: 0.95 - SKU854: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 110 - init_stock: 325 - max_stock: 1000 - price: 139 - sale_gamma: 65 - service_level: 0.95 - SKU855: - constraint: null - cost: 254 - init_stock: 287 - max_stock: 1000 - price: 505 - sale_gamma: 41 - service_level: 0.95 - SKU856: - constraint: null - cost: 435 - init_stock: 145 - max_stock: 1000 - price: 600 - sale_gamma: 29 - service_level: 0.95 - SKU857: - constraint: G(low_profit -> low_stock_constraint) - cost: 101 - init_stock: 192 - max_stock: 1000 - price: 163 - sale_gamma: 24 - service_level: 0.95 - SKU858: - constraint: null - cost: 487 - init_stock: 168 - max_stock: 1000 - price: 891 - sale_gamma: 28 - service_level: 0.95 - SKU859: - constraint: G(stock_constraint) - cost: 496 - init_stock: 558 - max_stock: 1000 - price: 585 - sale_gamma: 93 - service_level: 0.95 - SKU86: - constraint: null - cost: 59 - init_stock: 225 - max_stock: 1000 - price: 89 - sale_gamma: 75 - service_level: 0.95 - SKU860: - constraint: null - cost: 350 - init_stock: 600 - max_stock: 1000 - price: 465 - sale_gamma: 100 - service_level: 0.95 - SKU861: - constraint: null - cost: 295 - init_stock: 175 - max_stock: 1000 - price: 345 - sale_gamma: 25 - service_level: 0.95 - SKU862: - constraint: G(low_profit -> low_stock_constraint) - cost: 57 - init_stock: 219 - max_stock: 1000 - price: 74 - sale_gamma: 73 - service_level: 0.95 - SKU863: - constraint: null - cost: 311 - init_stock: 456 - max_stock: 1000 - price: 351 - sale_gamma: 57 - service_level: 0.95 - SKU864: - constraint: null - cost: 96 - init_stock: 192 - max_stock: 1000 - price: 144 - sale_gamma: 64 - service_level: 0.95 - SKU865: - constraint: null - cost: 107 - init_stock: 348 - max_stock: 1000 - price: 166 - sale_gamma: 58 - service_level: 0.95 - SKU866: - constraint: null - cost: 397 - init_stock: 147 - max_stock: 1000 - price: 766 - sale_gamma: 21 - service_level: 0.95 - SKU867: - constraint: null - cost: 478 - init_stock: 512 - max_stock: 1000 - price: 702 - sale_gamma: 64 - service_level: 0.95 - SKU868: - constraint: null - cost: 343 - init_stock: 63 - max_stock: 1000 - price: 415 - sale_gamma: 21 - service_level: 0.95 - SKU869: - constraint: null - cost: 168 - init_stock: 305 - max_stock: 1000 - price: 213 - sale_gamma: 61 - service_level: 0.95 - SKU87: - constraint: G(low_profit -> low_stock_constraint) - cost: 93 - init_stock: 213 - max_stock: 1000 - price: 159 - sale_gamma: 71 - service_level: 0.95 - SKU870: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 257 - init_stock: 441 - max_stock: 1000 - price: 431 - sale_gamma: 63 - service_level: 0.95 - SKU871: - constraint: G(stock_constraint) - cost: 68 - init_stock: 120 - max_stock: 1000 - price: 125 - sale_gamma: 20 - service_level: 0.95 - SKU872: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 475 - init_stock: 284 - max_stock: 1000 - price: 836 - sale_gamma: 71 - service_level: 0.95 - SKU873: - constraint: null - cost: 388 - init_stock: 260 - max_stock: 1000 - price: 671 - sale_gamma: 65 - service_level: 0.95 - SKU874: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 267 - init_stock: 30 - max_stock: 1000 - price: 448 - sale_gamma: 6 - service_level: 0.95 - SKU875: - constraint: null - cost: 332 - init_stock: 140 - max_stock: 1000 - price: 488 - sale_gamma: 35 - service_level: 0.95 - SKU876: - constraint: null - cost: 392 - init_stock: 200 - max_stock: 1000 - price: 490 - sale_gamma: 40 - service_level: 0.95 - SKU877: - constraint: G(low_profit -> low_stock_constraint) - cost: 374 - init_stock: 455 - max_stock: 1000 - price: 680 - sale_gamma: 91 - service_level: 0.95 - SKU878: - constraint: G(stock_constraint) - cost: 297 - init_stock: 145 - max_stock: 1000 - price: 415 - sale_gamma: 29 - service_level: 0.95 - SKU879: - constraint: G(low_profit -> low_stock_constraint) - cost: 497 - init_stock: 312 - max_stock: 1000 - price: 939 - sale_gamma: 78 - service_level: 0.95 - SKU88: - constraint: null - cost: 19 - init_stock: 126 - max_stock: 1000 - price: 22 - sale_gamma: 42 - service_level: 0.95 - SKU880: - constraint: null - cost: 199 - init_stock: 141 - max_stock: 1000 - price: 390 - sale_gamma: 47 - service_level: 0.95 - SKU881: - constraint: null - cost: 328 - init_stock: 240 - max_stock: 1000 - price: 485 - sale_gamma: 60 - service_level: 0.95 - SKU882: - constraint: null - cost: 307 - init_stock: 360 - max_stock: 1000 - price: 340 - sale_gamma: 90 - service_level: 0.95 - SKU883: - constraint: null - cost: 16 - init_stock: 196 - max_stock: 1000 - price: 23 - sale_gamma: 28 - service_level: 0.95 - SKU884: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 329 - init_stock: 335 - max_stock: 1000 - price: 585 - sale_gamma: 67 - service_level: 0.95 - SKU885: - constraint: null - cost: 176 - init_stock: 355 - max_stock: 1000 - price: 262 - sale_gamma: 71 - service_level: 0.95 - SKU886: - constraint: null - cost: 48 - init_stock: 623 - max_stock: 1000 - price: 94 - sale_gamma: 89 - service_level: 0.95 - SKU887: - constraint: null - cost: 241 - init_stock: 90 - max_stock: 1000 - price: 477 - sale_gamma: 15 - service_level: 0.95 - SKU888: - constraint: null - cost: 114 - init_stock: 424 - max_stock: 1000 - price: 169 - sale_gamma: 53 - service_level: 0.95 - SKU889: - constraint: G(low_profit -> low_stock_constraint) - cost: 261 - init_stock: 380 - max_stock: 1000 - price: 441 - sale_gamma: 95 - service_level: 0.95 - SKU89: - constraint: G(stock_constraint) - cost: 134 - init_stock: 567 - max_stock: 1000 - price: 230 - sale_gamma: 81 - service_level: 0.95 - SKU890: - constraint: null - cost: 322 - init_stock: 497 - max_stock: 1000 - price: 367 - sale_gamma: 71 - service_level: 0.95 - SKU891: - constraint: G(stock_constraint) - cost: 347 - init_stock: 114 - max_stock: 1000 - price: 489 - sale_gamma: 57 - service_level: 0.95 - SKU892: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 474 - init_stock: 336 - max_stock: 1000 - price: 938 - sale_gamma: 84 - service_level: 0.95 - SKU893: - constraint: null - cost: 433 - init_stock: 196 - max_stock: 1000 - price: 701 - sale_gamma: 98 - service_level: 0.95 - SKU894: - constraint: null - cost: 399 - init_stock: 104 - max_stock: 1000 - price: 746 - sale_gamma: 26 - service_level: 0.95 - SKU895: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 335 - init_stock: 240 - max_stock: 1000 - price: 629 - sale_gamma: 48 - service_level: 0.95 - SKU896: - constraint: G(stock_constraint) - cost: 92 - init_stock: 385 - max_stock: 1000 - price: 111 - sale_gamma: 55 - service_level: 0.95 - SKU897: - constraint: null - cost: 404 - init_stock: 88 - max_stock: 1000 - price: 779 - sale_gamma: 22 - service_level: 0.95 - SKU898: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 72 - init_stock: 490 - max_stock: 1000 - price: 90 - sale_gamma: 98 - service_level: 0.95 - SKU899: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 91 - init_stock: 623 - max_stock: 1000 - price: 139 - sale_gamma: 89 - service_level: 0.95 - SKU9: - constraint: null - cost: 231 - init_stock: 84 - max_stock: 1000 - price: 462 - sale_gamma: 21 - service_level: 0.95 - SKU90: - constraint: G(low_profit -> low_stock_constraint) - cost: 38 - init_stock: 60 - max_stock: 1000 - price: 50 - sale_gamma: 30 - service_level: 0.95 - SKU900: - constraint: null - cost: 12 - init_stock: 354 - max_stock: 1000 - price: 21 - sale_gamma: 59 - service_level: 0.95 - SKU901: - constraint: G(low_profit -> low_stock_constraint) - cost: 307 - init_stock: 312 - max_stock: 1000 - price: 469 - sale_gamma: 78 - service_level: 0.95 - SKU902: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 354 - init_stock: 112 - max_stock: 1000 - price: 407 - sale_gamma: 56 - service_level: 0.95 - SKU903: - constraint: null - cost: 14 - init_stock: 372 - max_stock: 1000 - price: 19 - sale_gamma: 93 - service_level: 0.95 - SKU904: - constraint: G(stock_constraint) - cost: 300 - init_stock: 194 - max_stock: 1000 - price: 501 - sale_gamma: 97 - service_level: 0.95 - SKU905: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 134 - init_stock: 306 - max_stock: 1000 - price: 221 - sale_gamma: 51 - service_level: 0.95 - SKU906: - constraint: G(stock_constraint) - cost: 424 - init_stock: 528 - max_stock: 1000 - price: 784 - sale_gamma: 88 - service_level: 0.95 - SKU907: - constraint: null - cost: 360 - init_stock: 224 - max_stock: 1000 - price: 486 - sale_gamma: 32 - service_level: 0.95 - SKU908: - constraint: null - cost: 361 - init_stock: 156 - max_stock: 1000 - price: 501 - sale_gamma: 39 - service_level: 0.95 - SKU909: - constraint: null - cost: 65 - init_stock: 483 - max_stock: 1000 - price: 124 - sale_gamma: 69 - service_level: 0.95 - SKU91: - constraint: G(low_profit -> low_stock_constraint) - cost: 223 - init_stock: 172 - max_stock: 1000 - price: 269 - sale_gamma: 43 - service_level: 0.95 - SKU910: - constraint: null - cost: 161 - init_stock: 60 - max_stock: 1000 - price: 189 - sale_gamma: 10 - service_level: 0.95 - SKU911: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 186 - init_stock: 250 - max_stock: 1000 - price: 295 - sale_gamma: 50 - service_level: 0.95 - SKU912: - constraint: null - cost: 235 - init_stock: 216 - max_stock: 1000 - price: 408 - sale_gamma: 54 - service_level: 0.95 - SKU913: - constraint: G(stock_constraint) - cost: 122 - init_stock: 476 - max_stock: 1000 - price: 224 - sale_gamma: 68 - service_level: 0.95 - SKU914: - constraint: null - cost: 436 - init_stock: 48 - max_stock: 1000 - price: 597 - sale_gamma: 16 - service_level: 0.95 - SKU915: - constraint: null - cost: 233 - init_stock: 115 - max_stock: 1000 - price: 424 - sale_gamma: 23 - service_level: 0.95 - SKU916: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 26 - init_stock: 245 - max_stock: 1000 - price: 42 - sale_gamma: 49 - service_level: 0.95 - SKU917: - constraint: null - cost: 141 - init_stock: 490 - max_stock: 1000 - price: 250 - sale_gamma: 98 - service_level: 0.95 - SKU918: - constraint: null - cost: 113 - init_stock: 623 - max_stock: 1000 - price: 192 - sale_gamma: 89 - service_level: 0.95 - SKU919: - constraint: null - cost: 68 - init_stock: 696 - max_stock: 1000 - price: 111 - sale_gamma: 87 - service_level: 0.95 - SKU92: - constraint: G(stock_constraint) - cost: 325 - init_stock: 152 - max_stock: 1000 - price: 640 - sale_gamma: 38 - service_level: 0.95 - SKU920: - constraint: null - cost: 428 - init_stock: 42 - max_stock: 1000 - price: 637 - sale_gamma: 21 - service_level: 0.95 - SKU921: - constraint: G(stock_constraint) - cost: 491 - init_stock: 456 - max_stock: 1000 - price: 893 - sale_gamma: 76 - service_level: 0.95 - SKU922: - constraint: null - cost: 10 - init_stock: 204 - max_stock: 1000 - price: 18 - sale_gamma: 51 - service_level: 0.95 - SKU923: - constraint: null - cost: 80 - init_stock: 20 - max_stock: 1000 - price: 88 - sale_gamma: 10 - service_level: 0.95 - SKU924: - constraint: null - cost: 164 - init_stock: 456 - max_stock: 1000 - price: 203 - sale_gamma: 76 - service_level: 0.95 - SKU925: - constraint: null - cost: 221 - init_stock: 366 - max_stock: 1000 - price: 313 - sale_gamma: 61 - service_level: 0.95 - SKU926: - constraint: G(stock_constraint) - cost: 300 - init_stock: 14 - max_stock: 1000 - price: 390 - sale_gamma: 7 - service_level: 0.95 - SKU927: - constraint: null - cost: 128 - init_stock: 189 - max_stock: 1000 - price: 168 - sale_gamma: 63 - service_level: 0.95 - SKU928: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 126 - init_stock: 24 - max_stock: 1000 - price: 209 - sale_gamma: 8 - service_level: 0.95 - SKU929: - constraint: null - cost: 377 - init_stock: 30 - max_stock: 1000 - price: 591 - sale_gamma: 15 - service_level: 0.95 - SKU93: - constraint: null - cost: 441 - init_stock: 63 - max_stock: 1000 - price: 789 - sale_gamma: 9 - service_level: 0.95 - SKU930: - constraint: G(low_profit -> low_stock_constraint) - cost: 270 - init_stock: 396 - max_stock: 1000 - price: 437 - sale_gamma: 99 - service_level: 0.95 - SKU931: - constraint: null - cost: 412 - init_stock: 520 - max_stock: 1000 - price: 482 - sale_gamma: 65 - service_level: 0.95 - SKU932: - constraint: null - cost: 118 - init_stock: 384 - max_stock: 1000 - price: 215 - sale_gamma: 48 - service_level: 0.95 - SKU933: - constraint: G(stock_constraint) - cost: 54 - init_stock: 364 - max_stock: 1000 - price: 85 - sale_gamma: 91 - service_level: 0.95 - SKU934: - constraint: G(low_profit -> low_stock_constraint) - cost: 347 - init_stock: 234 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU935: - constraint: null - cost: 219 - init_stock: 85 - max_stock: 1000 - price: 348 - sale_gamma: 17 - service_level: 0.95 - SKU936: - constraint: null - cost: 283 - init_stock: 385 - max_stock: 1000 - price: 387 - sale_gamma: 77 - service_level: 0.95 - SKU937: - constraint: G(stock_constraint) - cost: 457 - init_stock: 292 - max_stock: 1000 - price: 589 - sale_gamma: 73 - service_level: 0.95 - SKU938: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 450 - init_stock: 360 - max_stock: 1000 - price: 661 - sale_gamma: 90 - service_level: 0.95 - SKU939: - constraint: G(stock_constraint) - cost: 67 - init_stock: 54 - max_stock: 1000 - price: 116 - sale_gamma: 27 - service_level: 0.95 - SKU94: - constraint: G(stock_constraint) - cost: 279 - init_stock: 34 - max_stock: 1000 - price: 491 - sale_gamma: 17 - service_level: 0.95 - SKU940: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 65 - init_stock: 145 - max_stock: 1000 - price: 110 - sale_gamma: 29 - service_level: 0.95 - SKU941: - constraint: null - cost: 213 - init_stock: 25 - max_stock: 1000 - price: 368 - sale_gamma: 5 - service_level: 0.95 - SKU942: - constraint: null - cost: 151 - init_stock: 135 - max_stock: 1000 - price: 212 - sale_gamma: 27 - service_level: 0.95 - SKU943: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 85 - init_stock: 250 - max_stock: 1000 - price: 161 - sale_gamma: 50 - service_level: 0.95 - SKU944: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 154 - init_stock: 42 - max_stock: 1000 - price: 178 - sale_gamma: 6 - service_level: 0.95 - SKU945: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 267 - init_stock: 60 - max_stock: 1000 - price: 472 - sale_gamma: 15 - service_level: 0.95 - SKU946: - constraint: null - cost: 397 - init_stock: 72 - max_stock: 1000 - price: 504 - sale_gamma: 9 - service_level: 0.95 - SKU947: - constraint: null - cost: 275 - init_stock: 205 - max_stock: 1000 - price: 330 - sale_gamma: 41 - service_level: 0.95 - SKU948: - constraint: null - cost: 472 - init_stock: 616 - max_stock: 1000 - price: 892 - sale_gamma: 88 - service_level: 0.95 - SKU949: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 275 - init_stock: 294 - max_stock: 1000 - price: 335 - sale_gamma: 98 - service_level: 0.95 - SKU95: - constraint: null - cost: 183 - init_stock: 102 - max_stock: 1000 - price: 243 - sale_gamma: 51 - service_level: 0.95 - SKU950: - constraint: null - cost: 374 - init_stock: 125 - max_stock: 1000 - price: 710 - sale_gamma: 25 - service_level: 0.95 - SKU951: - constraint: null - cost: 272 - init_stock: 256 - max_stock: 1000 - price: 386 - sale_gamma: 32 - service_level: 0.95 - SKU952: - constraint: G(low_profit -> low_stock_constraint) - cost: 194 - init_stock: 315 - max_stock: 1000 - price: 219 - sale_gamma: 63 - service_level: 0.95 - SKU953: - constraint: null - cost: 428 - init_stock: 60 - max_stock: 1000 - price: 577 - sale_gamma: 10 - service_level: 0.95 - SKU954: - constraint: null - cost: 272 - init_stock: 48 - max_stock: 1000 - price: 489 - sale_gamma: 16 - service_level: 0.95 - SKU955: - constraint: G(stock_constraint) - cost: 84 - init_stock: 552 - max_stock: 1000 - price: 141 - sale_gamma: 92 - service_level: 0.95 - SKU956: - constraint: G(stock_constraint) - cost: 363 - init_stock: 175 - max_stock: 1000 - price: 671 - sale_gamma: 35 - service_level: 0.95 - SKU957: - constraint: null - cost: 221 - init_stock: 360 - max_stock: 1000 - price: 375 - sale_gamma: 90 - service_level: 0.95 - SKU958: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 335 - init_stock: 180 - max_stock: 1000 - price: 629 - sale_gamma: 36 - service_level: 0.95 - SKU959: - constraint: null - cost: 427 - init_stock: 497 - max_stock: 1000 - price: 781 - sale_gamma: 71 - service_level: 0.95 - SKU96: - constraint: null - cost: 320 - init_stock: 66 - max_stock: 1000 - price: 422 - sale_gamma: 11 - service_level: 0.95 - SKU960: - constraint: null - cost: 187 - init_stock: 462 - max_stock: 1000 - price: 216 - sale_gamma: 77 - service_level: 0.95 - SKU961: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 90 - init_stock: 198 - max_stock: 1000 - price: 150 - sale_gamma: 66 - service_level: 0.95 - SKU962: - constraint: null - cost: 70 - init_stock: 490 - max_stock: 1000 - price: 89 - sale_gamma: 98 - service_level: 0.95 - SKU963: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 184 - init_stock: 222 - max_stock: 1000 - price: 224 - sale_gamma: 37 - service_level: 0.95 - SKU964: - constraint: null - cost: 180 - init_stock: 40 - max_stock: 1000 - price: 261 - sale_gamma: 10 - service_level: 0.95 - SKU965: - constraint: G(low_profit -> low_stock_constraint) - cost: 441 - init_stock: 96 - max_stock: 1000 - price: 529 - sale_gamma: 16 - service_level: 0.95 - SKU966: - constraint: null - cost: 214 - init_stock: 160 - max_stock: 1000 - price: 400 - sale_gamma: 32 - service_level: 0.95 - SKU967: - constraint: null - cost: 71 - init_stock: 434 - max_stock: 1000 - price: 117 - sale_gamma: 62 - service_level: 0.95 - SKU968: - constraint: null - cost: 142 - init_stock: 10 - max_stock: 1000 - price: 238 - sale_gamma: 5 - service_level: 0.95 - SKU969: - constraint: null - cost: 435 - init_stock: 174 - max_stock: 1000 - price: 769 - sale_gamma: 58 - service_level: 0.95 - SKU97: - constraint: null - cost: 15 - init_stock: 189 - max_stock: 1000 - price: 23 - sale_gamma: 27 - service_level: 0.95 - SKU970: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 392 - init_stock: 460 - max_stock: 1000 - price: 658 - sale_gamma: 92 - service_level: 0.95 - SKU971: - constraint: null - cost: 408 - init_stock: 39 - max_stock: 1000 - price: 554 - sale_gamma: 13 - service_level: 0.95 - SKU972: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 22 - init_stock: 140 - max_stock: 1000 - price: 33 - sale_gamma: 20 - service_level: 0.95 - SKU973: - constraint: G(low_profit -> low_stock_constraint) - cost: 476 - init_stock: 445 - max_stock: 1000 - price: 923 - sale_gamma: 89 - service_level: 0.95 - SKU974: - constraint: null - cost: 356 - init_stock: 300 - max_stock: 1000 - price: 637 - sale_gamma: 50 - service_level: 0.95 - SKU975: - constraint: null - cost: 299 - init_stock: 136 - max_stock: 1000 - price: 598 - sale_gamma: 17 - service_level: 0.95 - SKU976: - constraint: null - cost: 369 - init_stock: 175 - max_stock: 1000 - price: 490 - sale_gamma: 35 - service_level: 0.95 - SKU977: - constraint: G(low_profit -> low_stock_constraint) - cost: 142 - init_stock: 159 - max_stock: 1000 - price: 213 - sale_gamma: 53 - service_level: 0.95 - SKU978: - constraint: null - cost: 242 - init_stock: 196 - max_stock: 1000 - price: 481 - sale_gamma: 98 - service_level: 0.95 - SKU979: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 219 - init_stock: 198 - max_stock: 1000 - price: 400 - sale_gamma: 99 - service_level: 0.95 - SKU98: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 167 - init_stock: 234 - max_stock: 1000 - price: 317 - sale_gamma: 78 - service_level: 0.95 - SKU980: - constraint: null - cost: 363 - init_stock: 410 - max_stock: 1000 - price: 700 - sale_gamma: 82 - service_level: 0.95 - SKU981: - constraint: null - cost: 416 - init_stock: 504 - max_stock: 1000 - price: 470 - sale_gamma: 84 - service_level: 0.95 - SKU982: - constraint: null - cost: 163 - init_stock: 92 - max_stock: 1000 - price: 208 - sale_gamma: 23 - service_level: 0.95 - SKU983: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 151 - init_stock: 300 - max_stock: 1000 - price: 211 - sale_gamma: 60 - service_level: 0.95 - SKU984: - constraint: G(stock_constraint) - cost: 123 - init_stock: 420 - max_stock: 1000 - price: 152 - sale_gamma: 84 - service_level: 0.95 - SKU985: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 114 - init_stock: 246 - max_stock: 1000 - price: 175 - sale_gamma: 41 - service_level: 0.95 - SKU986: - constraint: null - cost: 107 - init_stock: 608 - max_stock: 1000 - price: 142 - sale_gamma: 76 - service_level: 0.95 - SKU987: - constraint: null - cost: 56 - init_stock: 558 - max_stock: 1000 - price: 61 - sale_gamma: 93 - service_level: 0.95 - SKU988: - constraint: null - cost: 66 - init_stock: 234 - max_stock: 1000 - price: 118 - sale_gamma: 39 - service_level: 0.95 - SKU989: - constraint: G(low_profit -> low_stock_constraint) - cost: 472 - init_stock: 469 - max_stock: 1000 - price: 759 - sale_gamma: 67 - service_level: 0.95 - SKU99: - constraint: null - cost: 31 - init_stock: 267 - max_stock: 1000 - price: 41 - sale_gamma: 89 - service_level: 0.95 - SKU990: - constraint: G(low_profit -> low_stock_constraint) - cost: 14 - init_stock: 188 - max_stock: 1000 - price: 23 - sale_gamma: 47 - service_level: 0.95 - SKU991: - constraint: null - cost: 450 - init_stock: 294 - max_stock: 1000 - price: 864 - sale_gamma: 49 - service_level: 0.95 - SKU992: - constraint: G(stock_constraint) - cost: 18 - init_stock: 366 - max_stock: 1000 - price: 21 - sale_gamma: 61 - service_level: 0.95 - SKU993: - constraint: null - cost: 350 - init_stock: 282 - max_stock: 1000 - price: 475 - sale_gamma: 47 - service_level: 0.95 - SKU994: - constraint: null - cost: 45 - init_stock: 637 - max_stock: 1000 - price: 73 - sale_gamma: 91 - service_level: 0.95 - SKU995: - constraint: null - cost: 406 - init_stock: 344 - max_stock: 1000 - price: 690 - sale_gamma: 86 - service_level: 0.95 - SKU996: - constraint: G(stock_constraint) - cost: 472 - init_stock: 192 - max_stock: 1000 - price: 769 - sale_gamma: 48 - service_level: 0.95 - SKU997: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 81 - max_stock: 1000 - price: 518 - sale_gamma: 27 - service_level: 0.95 - SKU998: - constraint: null - cost: 239 - init_stock: 90 - max_stock: 1000 - price: 442 - sale_gamma: 15 - service_level: 0.95 - SKU999: - constraint: null - cost: 435 - init_stock: 400 - max_stock: 1000 - price: 843 - sale_gamma: 100 - service_level: 0.95 - grid: - facilities: - STORE0: - - 14 - - 17 - SUPPLIER0: - - 9 - - 0 - WAREHOUSE0: - - 8 - - 18 - size: - - 20 - - 20 - skus: - - id: 0 - name: SKU0 - - id: 1 - name: SKU1 - - id: 2 - name: SKU2 - - id: 3 - name: SKU3 - - id: 4 - name: SKU4 - - id: 5 - name: SKU5 - - id: 6 - name: SKU6 - - id: 7 - name: SKU7 - - id: 8 - name: SKU8 - - id: 9 - name: SKU9 - - id: 10 - name: SKU10 - - id: 11 - name: SKU11 - - id: 12 - name: SKU12 - - id: 13 - name: SKU13 - - id: 14 - name: SKU14 - - id: 15 - name: SKU15 - - id: 16 - name: SKU16 - - id: 17 - name: SKU17 - - id: 18 - name: SKU18 - - id: 19 - name: SKU19 - - id: 20 - name: SKU20 - - id: 21 - name: SKU21 - - id: 22 - name: SKU22 - - id: 23 - name: SKU23 - - id: 24 - name: SKU24 - - id: 25 - name: SKU25 - - id: 26 - name: SKU26 - - id: 27 - name: SKU27 - - id: 28 - name: SKU28 - - id: 29 - name: SKU29 - - id: 30 - name: SKU30 - - id: 31 - name: SKU31 - - id: 32 - name: SKU32 - - id: 33 - name: SKU33 - - id: 34 - name: SKU34 - - id: 35 - name: SKU35 - - id: 36 - name: SKU36 - - id: 37 - name: SKU37 - - id: 38 - name: SKU38 - - id: 39 - name: SKU39 - - id: 40 - name: SKU40 - - id: 41 - name: SKU41 - - id: 42 - name: SKU42 - - id: 43 - name: SKU43 - - id: 44 - name: SKU44 - - id: 45 - name: SKU45 - - id: 46 - name: SKU46 - - id: 47 - name: SKU47 - - id: 48 - name: SKU48 - - id: 49 - name: SKU49 - - id: 50 - name: SKU50 - - id: 51 - name: SKU51 - - id: 52 - name: SKU52 - - id: 53 - name: SKU53 - - id: 54 - name: SKU54 - - id: 55 - name: SKU55 - - id: 56 - name: SKU56 - - id: 57 - name: SKU57 - - id: 58 - name: SKU58 - - id: 59 - name: SKU59 - - id: 60 - name: SKU60 - - id: 61 - name: SKU61 - - id: 62 - name: SKU62 - - id: 63 - name: SKU63 - - id: 64 - name: SKU64 - - id: 65 - name: SKU65 - - id: 66 - name: SKU66 - - id: 67 - name: SKU67 - - id: 68 - name: SKU68 - - id: 69 - name: SKU69 - - id: 70 - name: SKU70 - - id: 71 - name: SKU71 - - id: 72 - name: SKU72 - - id: 73 - name: SKU73 - - id: 74 - name: SKU74 - - id: 75 - name: SKU75 - - id: 76 - name: SKU76 - - id: 77 - name: SKU77 - - id: 78 - name: SKU78 - - id: 79 - name: SKU79 - - id: 80 - name: SKU80 - - id: 81 - name: SKU81 - - id: 82 - name: SKU82 - - id: 83 - name: SKU83 - - id: 84 - name: SKU84 - - id: 85 - name: SKU85 - - id: 86 - name: SKU86 - - id: 87 - name: SKU87 - - id: 88 - name: SKU88 - - id: 89 - name: SKU89 - - id: 90 - name: SKU90 - - id: 91 - name: SKU91 - - id: 92 - name: SKU92 - - id: 93 - name: SKU93 - - id: 94 - name: SKU94 - - id: 95 - name: SKU95 - - id: 96 - name: SKU96 - - id: 97 - name: SKU97 - - id: 98 - name: SKU98 - - id: 99 - name: SKU99 - - id: 100 - name: SKU100 - - id: 101 - name: SKU101 - - id: 102 - name: SKU102 - - id: 103 - name: SKU103 - - id: 104 - name: SKU104 - - id: 105 - name: SKU105 - - id: 106 - name: SKU106 - - id: 107 - name: SKU107 - - id: 108 - name: SKU108 - - id: 109 - name: SKU109 - - id: 110 - name: SKU110 - - id: 111 - name: SKU111 - - id: 112 - name: SKU112 - - id: 113 - name: SKU113 - - id: 114 - name: SKU114 - - id: 115 - name: SKU115 - - id: 116 - name: SKU116 - - id: 117 - name: SKU117 - - id: 118 - name: SKU118 - - id: 119 - name: SKU119 - - id: 120 - name: SKU120 - - id: 121 - name: SKU121 - - id: 122 - name: SKU122 - - id: 123 - name: SKU123 - - id: 124 - name: SKU124 - - id: 125 - name: SKU125 - - id: 126 - name: SKU126 - - id: 127 - name: SKU127 - - id: 128 - name: SKU128 - - id: 129 - name: SKU129 - - id: 130 - name: SKU130 - - id: 131 - name: SKU131 - - id: 132 - name: SKU132 - - id: 133 - name: SKU133 - - id: 134 - name: SKU134 - - id: 135 - name: SKU135 - - id: 136 - name: SKU136 - - id: 137 - name: SKU137 - - id: 138 - name: SKU138 - - id: 139 - name: SKU139 - - id: 140 - name: SKU140 - - id: 141 - name: SKU141 - - id: 142 - name: SKU142 - - id: 143 - name: SKU143 - - id: 144 - name: SKU144 - - id: 145 - name: SKU145 - - id: 146 - name: SKU146 - - id: 147 - name: SKU147 - - id: 148 - name: SKU148 - - id: 149 - name: SKU149 - - id: 150 - name: SKU150 - - id: 151 - name: SKU151 - - id: 152 - name: SKU152 - - id: 153 - name: SKU153 - - id: 154 - name: SKU154 - - id: 155 - name: SKU155 - - id: 156 - name: SKU156 - - id: 157 - name: SKU157 - - id: 158 - name: SKU158 - - id: 159 - name: SKU159 - - id: 160 - name: SKU160 - - id: 161 - name: SKU161 - - id: 162 - name: SKU162 - - id: 163 - name: SKU163 - - id: 164 - name: SKU164 - - id: 165 - name: SKU165 - - id: 166 - name: SKU166 - - id: 167 - name: SKU167 - - id: 168 - name: SKU168 - - id: 169 - name: SKU169 - - id: 170 - name: SKU170 - - id: 171 - name: SKU171 - - id: 172 - name: SKU172 - - id: 173 - name: SKU173 - - id: 174 - name: SKU174 - - id: 175 - name: SKU175 - - id: 176 - name: SKU176 - - id: 177 - name: SKU177 - - id: 178 - name: SKU178 - - id: 179 - name: SKU179 - - id: 180 - name: SKU180 - - id: 181 - name: SKU181 - - id: 182 - name: SKU182 - - id: 183 - name: SKU183 - - id: 184 - name: SKU184 - - id: 185 - name: SKU185 - - id: 186 - name: SKU186 - - id: 187 - name: SKU187 - - id: 188 - name: SKU188 - - id: 189 - name: SKU189 - - id: 190 - name: SKU190 - - id: 191 - name: SKU191 - - id: 192 - name: SKU192 - - id: 193 - name: SKU193 - - id: 194 - name: SKU194 - - id: 195 - name: SKU195 - - id: 196 - name: SKU196 - - id: 197 - name: SKU197 - - id: 198 - name: SKU198 - - id: 199 - name: SKU199 - - id: 200 - name: SKU200 - - id: 201 - name: SKU201 - - id: 202 - name: SKU202 - - id: 203 - name: SKU203 - - id: 204 - name: SKU204 - - id: 205 - name: SKU205 - - id: 206 - name: SKU206 - - id: 207 - name: SKU207 - - id: 208 - name: SKU208 - - id: 209 - name: SKU209 - - id: 210 - name: SKU210 - - id: 211 - name: SKU211 - - id: 212 - name: SKU212 - - id: 213 - name: SKU213 - - id: 214 - name: SKU214 - - id: 215 - name: SKU215 - - id: 216 - name: SKU216 - - id: 217 - name: SKU217 - - id: 218 - name: SKU218 - - id: 219 - name: SKU219 - - id: 220 - name: SKU220 - - id: 221 - name: SKU221 - - id: 222 - name: SKU222 - - id: 223 - name: SKU223 - - id: 224 - name: SKU224 - - id: 225 - name: SKU225 - - id: 226 - name: SKU226 - - id: 227 - name: SKU227 - - id: 228 - name: SKU228 - - id: 229 - name: SKU229 - - id: 230 - name: SKU230 - - id: 231 - name: SKU231 - - id: 232 - name: SKU232 - - id: 233 - name: SKU233 - - id: 234 - name: SKU234 - - id: 235 - name: SKU235 - - id: 236 - name: SKU236 - - id: 237 - name: SKU237 - - id: 238 - name: SKU238 - - id: 239 - name: SKU239 - - id: 240 - name: SKU240 - - id: 241 - name: SKU241 - - id: 242 - name: SKU242 - - id: 243 - name: SKU243 - - id: 244 - name: SKU244 - - id: 245 - name: SKU245 - - id: 246 - name: SKU246 - - id: 247 - name: SKU247 - - id: 248 - name: SKU248 - - id: 249 - name: SKU249 - - id: 250 - name: SKU250 - - id: 251 - name: SKU251 - - id: 252 - name: SKU252 - - id: 253 - name: SKU253 - - id: 254 - name: SKU254 - - id: 255 - name: SKU255 - - id: 256 - name: SKU256 - - id: 257 - name: SKU257 - - id: 258 - name: SKU258 - - id: 259 - name: SKU259 - - id: 260 - name: SKU260 - - id: 261 - name: SKU261 - - id: 262 - name: SKU262 - - id: 263 - name: SKU263 - - id: 264 - name: SKU264 - - id: 265 - name: SKU265 - - id: 266 - name: SKU266 - - id: 267 - name: SKU267 - - id: 268 - name: SKU268 - - id: 269 - name: SKU269 - - id: 270 - name: SKU270 - - id: 271 - name: SKU271 - - id: 272 - name: SKU272 - - id: 273 - name: SKU273 - - id: 274 - name: SKU274 - - id: 275 - name: SKU275 - - id: 276 - name: SKU276 - - id: 277 - name: SKU277 - - id: 278 - name: SKU278 - - id: 279 - name: SKU279 - - id: 280 - name: SKU280 - - id: 281 - name: SKU281 - - id: 282 - name: SKU282 - - id: 283 - name: SKU283 - - id: 284 - name: SKU284 - - id: 285 - name: SKU285 - - id: 286 - name: SKU286 - - id: 287 - name: SKU287 - - id: 288 - name: SKU288 - - id: 289 - name: SKU289 - - id: 290 - name: SKU290 - - id: 291 - name: SKU291 - - id: 292 - name: SKU292 - - id: 293 - name: SKU293 - - id: 294 - name: SKU294 - - id: 295 - name: SKU295 - - id: 296 - name: SKU296 - - id: 297 - name: SKU297 - - id: 298 - name: SKU298 - - id: 299 - name: SKU299 - - id: 300 - name: SKU300 - - id: 301 - name: SKU301 - - id: 302 - name: SKU302 - - id: 303 - name: SKU303 - - id: 304 - name: SKU304 - - id: 305 - name: SKU305 - - id: 306 - name: SKU306 - - id: 307 - name: SKU307 - - id: 308 - name: SKU308 - - id: 309 - name: SKU309 - - id: 310 - name: SKU310 - - id: 311 - name: SKU311 - - id: 312 - name: SKU312 - - id: 313 - name: SKU313 - - id: 314 - name: SKU314 - - id: 315 - name: SKU315 - - id: 316 - name: SKU316 - - id: 317 - name: SKU317 - - id: 318 - name: SKU318 - - id: 319 - name: SKU319 - - id: 320 - name: SKU320 - - id: 321 - name: SKU321 - - id: 322 - name: SKU322 - - id: 323 - name: SKU323 - - id: 324 - name: SKU324 - - id: 325 - name: SKU325 - - id: 326 - name: SKU326 - - id: 327 - name: SKU327 - - id: 328 - name: SKU328 - - id: 329 - name: SKU329 - - id: 330 - name: SKU330 - - id: 331 - name: SKU331 - - id: 332 - name: SKU332 - - id: 333 - name: SKU333 - - id: 334 - name: SKU334 - - id: 335 - name: SKU335 - - id: 336 - name: SKU336 - - id: 337 - name: SKU337 - - id: 338 - name: SKU338 - - id: 339 - name: SKU339 - - id: 340 - name: SKU340 - - id: 341 - name: SKU341 - - id: 342 - name: SKU342 - - id: 343 - name: SKU343 - - id: 344 - name: SKU344 - - id: 345 - name: SKU345 - - id: 346 - name: SKU346 - - id: 347 - name: SKU347 - - id: 348 - name: SKU348 - - id: 349 - name: SKU349 - - id: 350 - name: SKU350 - - id: 351 - name: SKU351 - - id: 352 - name: SKU352 - - id: 353 - name: SKU353 - - id: 354 - name: SKU354 - - id: 355 - name: SKU355 - - id: 356 - name: SKU356 - - id: 357 - name: SKU357 - - id: 358 - name: SKU358 - - id: 359 - name: SKU359 - - id: 360 - name: SKU360 - - id: 361 - name: SKU361 - - id: 362 - name: SKU362 - - id: 363 - name: SKU363 - - id: 364 - name: SKU364 - - id: 365 - name: SKU365 - - id: 366 - name: SKU366 - - id: 367 - name: SKU367 - - id: 368 - name: SKU368 - - id: 369 - name: SKU369 - - id: 370 - name: SKU370 - - id: 371 - name: SKU371 - - id: 372 - name: SKU372 - - id: 373 - name: SKU373 - - id: 374 - name: SKU374 - - id: 375 - name: SKU375 - - id: 376 - name: SKU376 - - id: 377 - name: SKU377 - - id: 378 - name: SKU378 - - id: 379 - name: SKU379 - - id: 380 - name: SKU380 - - id: 381 - name: SKU381 - - id: 382 - name: SKU382 - - id: 383 - name: SKU383 - - id: 384 - name: SKU384 - - id: 385 - name: SKU385 - - id: 386 - name: SKU386 - - id: 387 - name: SKU387 - - id: 388 - name: SKU388 - - id: 389 - name: SKU389 - - id: 390 - name: SKU390 - - id: 391 - name: SKU391 - - id: 392 - name: SKU392 - - id: 393 - name: SKU393 - - id: 394 - name: SKU394 - - id: 395 - name: SKU395 - - id: 396 - name: SKU396 - - id: 397 - name: SKU397 - - id: 398 - name: SKU398 - - id: 399 - name: SKU399 - - id: 400 - name: SKU400 - - id: 401 - name: SKU401 - - id: 402 - name: SKU402 - - id: 403 - name: SKU403 - - id: 404 - name: SKU404 - - id: 405 - name: SKU405 - - id: 406 - name: SKU406 - - id: 407 - name: SKU407 - - id: 408 - name: SKU408 - - id: 409 - name: SKU409 - - id: 410 - name: SKU410 - - id: 411 - name: SKU411 - - id: 412 - name: SKU412 - - id: 413 - name: SKU413 - - id: 414 - name: SKU414 - - id: 415 - name: SKU415 - - id: 416 - name: SKU416 - - id: 417 - name: SKU417 - - id: 418 - name: SKU418 - - id: 419 - name: SKU419 - - id: 420 - name: SKU420 - - id: 421 - name: SKU421 - - id: 422 - name: SKU422 - - id: 423 - name: SKU423 - - id: 424 - name: SKU424 - - id: 425 - name: SKU425 - - id: 426 - name: SKU426 - - id: 427 - name: SKU427 - - id: 428 - name: SKU428 - - id: 429 - name: SKU429 - - id: 430 - name: SKU430 - - id: 431 - name: SKU431 - - id: 432 - name: SKU432 - - id: 433 - name: SKU433 - - id: 434 - name: SKU434 - - id: 435 - name: SKU435 - - id: 436 - name: SKU436 - - id: 437 - name: SKU437 - - id: 438 - name: SKU438 - - id: 439 - name: SKU439 - - id: 440 - name: SKU440 - - id: 441 - name: SKU441 - - id: 442 - name: SKU442 - - id: 443 - name: SKU443 - - id: 444 - name: SKU444 - - id: 445 - name: SKU445 - - id: 446 - name: SKU446 - - id: 447 - name: SKU447 - - id: 448 - name: SKU448 - - id: 449 - name: SKU449 - - id: 450 - name: SKU450 - - id: 451 - name: SKU451 - - id: 452 - name: SKU452 - - id: 453 - name: SKU453 - - id: 454 - name: SKU454 - - id: 455 - name: SKU455 - - id: 456 - name: SKU456 - - id: 457 - name: SKU457 - - id: 458 - name: SKU458 - - id: 459 - name: SKU459 - - id: 460 - name: SKU460 - - id: 461 - name: SKU461 - - id: 462 - name: SKU462 - - id: 463 - name: SKU463 - - id: 464 - name: SKU464 - - id: 465 - name: SKU465 - - id: 466 - name: SKU466 - - id: 467 - name: SKU467 - - id: 468 - name: SKU468 - - id: 469 - name: SKU469 - - id: 470 - name: SKU470 - - id: 471 - name: SKU471 - - id: 472 - name: SKU472 - - id: 473 - name: SKU473 - - id: 474 - name: SKU474 - - id: 475 - name: SKU475 - - id: 476 - name: SKU476 - - id: 477 - name: SKU477 - - id: 478 - name: SKU478 - - id: 479 - name: SKU479 - - id: 480 - name: SKU480 - - id: 481 - name: SKU481 - - id: 482 - name: SKU482 - - id: 483 - name: SKU483 - - id: 484 - name: SKU484 - - id: 485 - name: SKU485 - - id: 486 - name: SKU486 - - id: 487 - name: SKU487 - - id: 488 - name: SKU488 - - id: 489 - name: SKU489 - - id: 490 - name: SKU490 - - id: 491 - name: SKU491 - - id: 492 - name: SKU492 - - id: 493 - name: SKU493 - - id: 494 - name: SKU494 - - id: 495 - name: SKU495 - - id: 496 - name: SKU496 - - id: 497 - name: SKU497 - - id: 498 - name: SKU498 - - id: 499 - name: SKU499 - - id: 500 - name: SKU500 - - id: 501 - name: SKU501 - - id: 502 - name: SKU502 - - id: 503 - name: SKU503 - - id: 504 - name: SKU504 - - id: 505 - name: SKU505 - - id: 506 - name: SKU506 - - id: 507 - name: SKU507 - - id: 508 - name: SKU508 - - id: 509 - name: SKU509 - - id: 510 - name: SKU510 - - id: 511 - name: SKU511 - - id: 512 - name: SKU512 - - id: 513 - name: SKU513 - - id: 514 - name: SKU514 - - id: 515 - name: SKU515 - - id: 516 - name: SKU516 - - id: 517 - name: SKU517 - - id: 518 - name: SKU518 - - id: 519 - name: SKU519 - - id: 520 - name: SKU520 - - id: 521 - name: SKU521 - - id: 522 - name: SKU522 - - id: 523 - name: SKU523 - - id: 524 - name: SKU524 - - id: 525 - name: SKU525 - - id: 526 - name: SKU526 - - id: 527 - name: SKU527 - - id: 528 - name: SKU528 - - id: 529 - name: SKU529 - - id: 530 - name: SKU530 - - id: 531 - name: SKU531 - - id: 532 - name: SKU532 - - id: 533 - name: SKU533 - - id: 534 - name: SKU534 - - id: 535 - name: SKU535 - - id: 536 - name: SKU536 - - id: 537 - name: SKU537 - - id: 538 - name: SKU538 - - id: 539 - name: SKU539 - - id: 540 - name: SKU540 - - id: 541 - name: SKU541 - - id: 542 - name: SKU542 - - id: 543 - name: SKU543 - - id: 544 - name: SKU544 - - id: 545 - name: SKU545 - - id: 546 - name: SKU546 - - id: 547 - name: SKU547 - - id: 548 - name: SKU548 - - id: 549 - name: SKU549 - - id: 550 - name: SKU550 - - id: 551 - name: SKU551 - - id: 552 - name: SKU552 - - id: 553 - name: SKU553 - - id: 554 - name: SKU554 - - id: 555 - name: SKU555 - - id: 556 - name: SKU556 - - id: 557 - name: SKU557 - - id: 558 - name: SKU558 - - id: 559 - name: SKU559 - - id: 560 - name: SKU560 - - id: 561 - name: SKU561 - - id: 562 - name: SKU562 - - id: 563 - name: SKU563 - - id: 564 - name: SKU564 - - id: 565 - name: SKU565 - - id: 566 - name: SKU566 - - id: 567 - name: SKU567 - - id: 568 - name: SKU568 - - id: 569 - name: SKU569 - - id: 570 - name: SKU570 - - id: 571 - name: SKU571 - - id: 572 - name: SKU572 - - id: 573 - name: SKU573 - - id: 574 - name: SKU574 - - id: 575 - name: SKU575 - - id: 576 - name: SKU576 - - id: 577 - name: SKU577 - - id: 578 - name: SKU578 - - id: 579 - name: SKU579 - - id: 580 - name: SKU580 - - id: 581 - name: SKU581 - - id: 582 - name: SKU582 - - id: 583 - name: SKU583 - - id: 584 - name: SKU584 - - id: 585 - name: SKU585 - - id: 586 - name: SKU586 - - id: 587 - name: SKU587 - - id: 588 - name: SKU588 - - id: 589 - name: SKU589 - - id: 590 - name: SKU590 - - id: 591 - name: SKU591 - - id: 592 - name: SKU592 - - id: 593 - name: SKU593 - - id: 594 - name: SKU594 - - id: 595 - name: SKU595 - - id: 596 - name: SKU596 - - id: 597 - name: SKU597 - - id: 598 - name: SKU598 - - id: 599 - name: SKU599 - - id: 600 - name: SKU600 - - id: 601 - name: SKU601 - - id: 602 - name: SKU602 - - id: 603 - name: SKU603 - - id: 604 - name: SKU604 - - id: 605 - name: SKU605 - - id: 606 - name: SKU606 - - id: 607 - name: SKU607 - - id: 608 - name: SKU608 - - id: 609 - name: SKU609 - - id: 610 - name: SKU610 - - id: 611 - name: SKU611 - - id: 612 - name: SKU612 - - id: 613 - name: SKU613 - - id: 614 - name: SKU614 - - id: 615 - name: SKU615 - - id: 616 - name: SKU616 - - id: 617 - name: SKU617 - - id: 618 - name: SKU618 - - id: 619 - name: SKU619 - - id: 620 - name: SKU620 - - id: 621 - name: SKU621 - - id: 622 - name: SKU622 - - id: 623 - name: SKU623 - - id: 624 - name: SKU624 - - id: 625 - name: SKU625 - - id: 626 - name: SKU626 - - id: 627 - name: SKU627 - - id: 628 - name: SKU628 - - id: 629 - name: SKU629 - - id: 630 - name: SKU630 - - id: 631 - name: SKU631 - - id: 632 - name: SKU632 - - id: 633 - name: SKU633 - - id: 634 - name: SKU634 - - id: 635 - name: SKU635 - - id: 636 - name: SKU636 - - id: 637 - name: SKU637 - - id: 638 - name: SKU638 - - id: 639 - name: SKU639 - - id: 640 - name: SKU640 - - id: 641 - name: SKU641 - - id: 642 - name: SKU642 - - id: 643 - name: SKU643 - - id: 644 - name: SKU644 - - id: 645 - name: SKU645 - - id: 646 - name: SKU646 - - id: 647 - name: SKU647 - - id: 648 - name: SKU648 - - id: 649 - name: SKU649 - - id: 650 - name: SKU650 - - id: 651 - name: SKU651 - - id: 652 - name: SKU652 - - id: 653 - name: SKU653 - - id: 654 - name: SKU654 - - id: 655 - name: SKU655 - - id: 656 - name: SKU656 - - id: 657 - name: SKU657 - - id: 658 - name: SKU658 - - id: 659 - name: SKU659 - - id: 660 - name: SKU660 - - id: 661 - name: SKU661 - - id: 662 - name: SKU662 - - id: 663 - name: SKU663 - - id: 664 - name: SKU664 - - id: 665 - name: SKU665 - - id: 666 - name: SKU666 - - id: 667 - name: SKU667 - - id: 668 - name: SKU668 - - id: 669 - name: SKU669 - - id: 670 - name: SKU670 - - id: 671 - name: SKU671 - - id: 672 - name: SKU672 - - id: 673 - name: SKU673 - - id: 674 - name: SKU674 - - id: 675 - name: SKU675 - - id: 676 - name: SKU676 - - id: 677 - name: SKU677 - - id: 678 - name: SKU678 - - id: 679 - name: SKU679 - - id: 680 - name: SKU680 - - id: 681 - name: SKU681 - - id: 682 - name: SKU682 - - id: 683 - name: SKU683 - - id: 684 - name: SKU684 - - id: 685 - name: SKU685 - - id: 686 - name: SKU686 - - id: 687 - name: SKU687 - - id: 688 - name: SKU688 - - id: 689 - name: SKU689 - - id: 690 - name: SKU690 - - id: 691 - name: SKU691 - - id: 692 - name: SKU692 - - id: 693 - name: SKU693 - - id: 694 - name: SKU694 - - id: 695 - name: SKU695 - - id: 696 - name: SKU696 - - id: 697 - name: SKU697 - - id: 698 - name: SKU698 - - id: 699 - name: SKU699 - - id: 700 - name: SKU700 - - id: 701 - name: SKU701 - - id: 702 - name: SKU702 - - id: 703 - name: SKU703 - - id: 704 - name: SKU704 - - id: 705 - name: SKU705 - - id: 706 - name: SKU706 - - id: 707 - name: SKU707 - - id: 708 - name: SKU708 - - id: 709 - name: SKU709 - - id: 710 - name: SKU710 - - id: 711 - name: SKU711 - - id: 712 - name: SKU712 - - id: 713 - name: SKU713 - - id: 714 - name: SKU714 - - id: 715 - name: SKU715 - - id: 716 - name: SKU716 - - id: 717 - name: SKU717 - - id: 718 - name: SKU718 - - id: 719 - name: SKU719 - - id: 720 - name: SKU720 - - id: 721 - name: SKU721 - - id: 722 - name: SKU722 - - id: 723 - name: SKU723 - - id: 724 - name: SKU724 - - id: 725 - name: SKU725 - - id: 726 - name: SKU726 - - id: 727 - name: SKU727 - - id: 728 - name: SKU728 - - id: 729 - name: SKU729 - - id: 730 - name: SKU730 - - id: 731 - name: SKU731 - - id: 732 - name: SKU732 - - id: 733 - name: SKU733 - - id: 734 - name: SKU734 - - id: 735 - name: SKU735 - - id: 736 - name: SKU736 - - id: 737 - name: SKU737 - - id: 738 - name: SKU738 - - id: 739 - name: SKU739 - - id: 740 - name: SKU740 - - id: 741 - name: SKU741 - - id: 742 - name: SKU742 - - id: 743 - name: SKU743 - - id: 744 - name: SKU744 - - id: 745 - name: SKU745 - - id: 746 - name: SKU746 - - id: 747 - name: SKU747 - - id: 748 - name: SKU748 - - id: 749 - name: SKU749 - - id: 750 - name: SKU750 - - id: 751 - name: SKU751 - - id: 752 - name: SKU752 - - id: 753 - name: SKU753 - - id: 754 - name: SKU754 - - id: 755 - name: SKU755 - - id: 756 - name: SKU756 - - id: 757 - name: SKU757 - - id: 758 - name: SKU758 - - id: 759 - name: SKU759 - - id: 760 - name: SKU760 - - id: 761 - name: SKU761 - - id: 762 - name: SKU762 - - id: 763 - name: SKU763 - - id: 764 - name: SKU764 - - id: 765 - name: SKU765 - - id: 766 - name: SKU766 - - id: 767 - name: SKU767 - - id: 768 - name: SKU768 - - id: 769 - name: SKU769 - - id: 770 - name: SKU770 - - id: 771 - name: SKU771 - - id: 772 - name: SKU772 - - id: 773 - name: SKU773 - - id: 774 - name: SKU774 - - id: 775 - name: SKU775 - - id: 776 - name: SKU776 - - id: 777 - name: SKU777 - - id: 778 - name: SKU778 - - id: 779 - name: SKU779 - - id: 780 - name: SKU780 - - id: 781 - name: SKU781 - - id: 782 - name: SKU782 - - id: 783 - name: SKU783 - - id: 784 - name: SKU784 - - id: 785 - name: SKU785 - - id: 786 - name: SKU786 - - id: 787 - name: SKU787 - - id: 788 - name: SKU788 - - id: 789 - name: SKU789 - - id: 790 - name: SKU790 - - id: 791 - name: SKU791 - - id: 792 - name: SKU792 - - id: 793 - name: SKU793 - - id: 794 - name: SKU794 - - id: 795 - name: SKU795 - - id: 796 - name: SKU796 - - id: 797 - name: SKU797 - - id: 798 - name: SKU798 - - id: 799 - name: SKU799 - - id: 800 - name: SKU800 - - id: 801 - name: SKU801 - - id: 802 - name: SKU802 - - id: 803 - name: SKU803 - - id: 804 - name: SKU804 - - id: 805 - name: SKU805 - - id: 806 - name: SKU806 - - id: 807 - name: SKU807 - - id: 808 - name: SKU808 - - id: 809 - name: SKU809 - - id: 810 - name: SKU810 - - id: 811 - name: SKU811 - - id: 812 - name: SKU812 - - id: 813 - name: SKU813 - - id: 814 - name: SKU814 - - id: 815 - name: SKU815 - - id: 816 - name: SKU816 - - id: 817 - name: SKU817 - - id: 818 - name: SKU818 - - id: 819 - name: SKU819 - - id: 820 - name: SKU820 - - id: 821 - name: SKU821 - - id: 822 - name: SKU822 - - id: 823 - name: SKU823 - - id: 824 - name: SKU824 - - id: 825 - name: SKU825 - - id: 826 - name: SKU826 - - id: 827 - name: SKU827 - - id: 828 - name: SKU828 - - id: 829 - name: SKU829 - - id: 830 - name: SKU830 - - id: 831 - name: SKU831 - - id: 832 - name: SKU832 - - id: 833 - name: SKU833 - - id: 834 - name: SKU834 - - id: 835 - name: SKU835 - - id: 836 - name: SKU836 - - id: 837 - name: SKU837 - - id: 838 - name: SKU838 - - id: 839 - name: SKU839 - - id: 840 - name: SKU840 - - id: 841 - name: SKU841 - - id: 842 - name: SKU842 - - id: 843 - name: SKU843 - - id: 844 - name: SKU844 - - id: 845 - name: SKU845 - - id: 846 - name: SKU846 - - id: 847 - name: SKU847 - - id: 848 - name: SKU848 - - id: 849 - name: SKU849 - - id: 850 - name: SKU850 - - id: 851 - name: SKU851 - - id: 852 - name: SKU852 - - id: 853 - name: SKU853 - - id: 854 - name: SKU854 - - id: 855 - name: SKU855 - - id: 856 - name: SKU856 - - id: 857 - name: SKU857 - - id: 858 - name: SKU858 - - id: 859 - name: SKU859 - - id: 860 - name: SKU860 - - id: 861 - name: SKU861 - - id: 862 - name: SKU862 - - id: 863 - name: SKU863 - - id: 864 - name: SKU864 - - id: 865 - name: SKU865 - - id: 866 - name: SKU866 - - id: 867 - name: SKU867 - - id: 868 - name: SKU868 - - id: 869 - name: SKU869 - - id: 870 - name: SKU870 - - id: 871 - name: SKU871 - - id: 872 - name: SKU872 - - id: 873 - name: SKU873 - - id: 874 - name: SKU874 - - id: 875 - name: SKU875 - - id: 876 - name: SKU876 - - id: 877 - name: SKU877 - - id: 878 - name: SKU878 - - id: 879 - name: SKU879 - - id: 880 - name: SKU880 - - id: 881 - name: SKU881 - - id: 882 - name: SKU882 - - id: 883 - name: SKU883 - - id: 884 - name: SKU884 - - id: 885 - name: SKU885 - - id: 886 - name: SKU886 - - id: 887 - name: SKU887 - - id: 888 - name: SKU888 - - id: 889 - name: SKU889 - - id: 890 - name: SKU890 - - id: 891 - name: SKU891 - - id: 892 - name: SKU892 - - id: 893 - name: SKU893 - - id: 894 - name: SKU894 - - id: 895 - name: SKU895 - - id: 896 - name: SKU896 - - id: 897 - name: SKU897 - - id: 898 - name: SKU898 - - id: 899 - name: SKU899 - - id: 900 - name: SKU900 - - id: 901 - name: SKU901 - - id: 902 - name: SKU902 - - id: 903 - name: SKU903 - - id: 904 - name: SKU904 - - id: 905 - name: SKU905 - - id: 906 - name: SKU906 - - id: 907 - name: SKU907 - - id: 908 - name: SKU908 - - id: 909 - name: SKU909 - - id: 910 - name: SKU910 - - id: 911 - name: SKU911 - - id: 912 - name: SKU912 - - id: 913 - name: SKU913 - - id: 914 - name: SKU914 - - id: 915 - name: SKU915 - - id: 916 - name: SKU916 - - id: 917 - name: SKU917 - - id: 918 - name: SKU918 - - id: 919 - name: SKU919 - - id: 920 - name: SKU920 - - id: 921 - name: SKU921 - - id: 922 - name: SKU922 - - id: 923 - name: SKU923 - - id: 924 - name: SKU924 - - id: 925 - name: SKU925 - - id: 926 - name: SKU926 - - id: 927 - name: SKU927 - - id: 928 - name: SKU928 - - id: 929 - name: SKU929 - - id: 930 - name: SKU930 - - id: 931 - name: SKU931 - - id: 932 - name: SKU932 - - id: 933 - name: SKU933 - - id: 934 - name: SKU934 - - id: 935 - name: SKU935 - - id: 936 - name: SKU936 - - id: 937 - name: SKU937 - - id: 938 - name: SKU938 - - id: 939 - name: SKU939 - - id: 940 - name: SKU940 - - id: 941 - name: SKU941 - - id: 942 - name: SKU942 - - id: 943 - name: SKU943 - - id: 944 - name: SKU944 - - id: 945 - name: SKU945 - - id: 946 - name: SKU946 - - id: 947 - name: SKU947 - - id: 948 - name: SKU948 - - id: 949 - name: SKU949 - - id: 950 - name: SKU950 - - id: 951 - name: SKU951 - - id: 952 - name: SKU952 - - id: 953 - name: SKU953 - - id: 954 - name: SKU954 - - id: 955 - name: SKU955 - - id: 956 - name: SKU956 - - id: 957 - name: SKU957 - - id: 958 - name: SKU958 - - id: 959 - name: SKU959 - - id: 960 - name: SKU960 - - id: 961 - name: SKU961 - - id: 962 - name: SKU962 - - id: 963 - name: SKU963 - - id: 964 - name: SKU964 - - id: 965 - name: SKU965 - - id: 966 - name: SKU966 - - id: 967 - name: SKU967 - - id: 968 - name: SKU968 - - id: 969 - name: SKU969 - - id: 970 - name: SKU970 - - id: 971 - name: SKU971 - - id: 972 - name: SKU972 - - id: 973 - name: SKU973 - - id: 974 - name: SKU974 - - id: 975 - name: SKU975 - - id: 976 - name: SKU976 - - id: 977 - name: SKU977 - - id: 978 - name: SKU978 - - id: 979 - name: SKU979 - - id: 980 - name: SKU980 - - id: 981 - name: SKU981 - - id: 982 - name: SKU982 - - id: 983 - name: SKU983 - - id: 984 - name: SKU984 - - id: 985 - name: SKU985 - - id: 986 - name: SKU986 - - id: 987 - name: SKU987 - - id: 988 - name: SKU988 - - id: 989 - name: SKU989 - - id: 990 - name: SKU990 - - id: 991 - name: SKU991 - - id: 992 - name: SKU992 - - id: 993 - name: SKU993 - - id: 994 - name: SKU994 - - id: 995 - name: SKU995 - - id: 996 - name: SKU996 - - id: 997 - name: SKU997 - - id: 998 - name: SKU998 - - id: 999 - name: SKU999 - topology: - STORE0: - SKU0: - - WAREHOUSE0 - SKU1: - - WAREHOUSE0 - SKU10: - - WAREHOUSE0 - SKU100: - - WAREHOUSE0 - SKU101: - - WAREHOUSE0 - SKU102: - - WAREHOUSE0 - SKU103: - - WAREHOUSE0 - SKU104: - - WAREHOUSE0 - SKU105: - - WAREHOUSE0 - SKU106: - - WAREHOUSE0 - SKU107: - - WAREHOUSE0 - SKU108: - - WAREHOUSE0 - SKU109: - - WAREHOUSE0 - SKU11: - - WAREHOUSE0 - SKU110: - - WAREHOUSE0 - SKU111: - - WAREHOUSE0 - SKU112: - - WAREHOUSE0 - SKU113: - - WAREHOUSE0 - SKU114: - - WAREHOUSE0 - SKU115: - - WAREHOUSE0 - SKU116: - - WAREHOUSE0 - SKU117: - - WAREHOUSE0 - SKU118: - - WAREHOUSE0 - SKU119: - - WAREHOUSE0 - SKU12: - - WAREHOUSE0 - SKU120: - - WAREHOUSE0 - SKU121: - - WAREHOUSE0 - SKU122: - - WAREHOUSE0 - SKU123: - - WAREHOUSE0 - SKU124: - - WAREHOUSE0 - SKU125: - - WAREHOUSE0 - SKU126: - - WAREHOUSE0 - SKU127: - - WAREHOUSE0 - SKU128: - - WAREHOUSE0 - SKU129: - - WAREHOUSE0 - SKU13: - - WAREHOUSE0 - SKU130: - - WAREHOUSE0 - SKU131: - - WAREHOUSE0 - SKU132: - - WAREHOUSE0 - SKU133: - - WAREHOUSE0 - SKU134: - - WAREHOUSE0 - SKU135: - - WAREHOUSE0 - SKU136: - - WAREHOUSE0 - SKU137: - - WAREHOUSE0 - SKU138: - - WAREHOUSE0 - SKU139: - - WAREHOUSE0 - SKU14: - - WAREHOUSE0 - SKU140: - - WAREHOUSE0 - SKU141: - - WAREHOUSE0 - SKU142: - - WAREHOUSE0 - SKU143: - - WAREHOUSE0 - SKU144: - - WAREHOUSE0 - SKU145: - - WAREHOUSE0 - SKU146: - - WAREHOUSE0 - SKU147: - - WAREHOUSE0 - SKU148: - - WAREHOUSE0 - SKU149: - - WAREHOUSE0 - SKU15: - - WAREHOUSE0 - SKU150: - - WAREHOUSE0 - SKU151: - - WAREHOUSE0 - SKU152: - - WAREHOUSE0 - SKU153: - - WAREHOUSE0 - SKU154: - - WAREHOUSE0 - SKU155: - - WAREHOUSE0 - SKU156: - - WAREHOUSE0 - SKU157: - - WAREHOUSE0 - SKU158: - - WAREHOUSE0 - SKU159: - - WAREHOUSE0 - SKU16: - - WAREHOUSE0 - SKU160: - - WAREHOUSE0 - SKU161: - - WAREHOUSE0 - SKU162: - - WAREHOUSE0 - SKU163: - - WAREHOUSE0 - SKU164: - - WAREHOUSE0 - SKU165: - - WAREHOUSE0 - SKU166: - - WAREHOUSE0 - SKU167: - - WAREHOUSE0 - SKU168: - - WAREHOUSE0 - SKU169: - - WAREHOUSE0 - SKU17: - - WAREHOUSE0 - SKU170: - - WAREHOUSE0 - SKU171: - - WAREHOUSE0 - SKU172: - - WAREHOUSE0 - SKU173: - - WAREHOUSE0 - SKU174: - - WAREHOUSE0 - SKU175: - - WAREHOUSE0 - SKU176: - - WAREHOUSE0 - SKU177: - - WAREHOUSE0 - SKU178: - - WAREHOUSE0 - SKU179: - - WAREHOUSE0 - SKU18: - - WAREHOUSE0 - SKU180: - - WAREHOUSE0 - SKU181: - - WAREHOUSE0 - SKU182: - - WAREHOUSE0 - SKU183: - - WAREHOUSE0 - SKU184: - - WAREHOUSE0 - SKU185: - - WAREHOUSE0 - SKU186: - - WAREHOUSE0 - SKU187: - - WAREHOUSE0 - SKU188: - - WAREHOUSE0 - SKU189: - - WAREHOUSE0 - SKU19: - - WAREHOUSE0 - SKU190: - - WAREHOUSE0 - SKU191: - - WAREHOUSE0 - SKU192: - - WAREHOUSE0 - SKU193: - - WAREHOUSE0 - SKU194: - - WAREHOUSE0 - SKU195: - - WAREHOUSE0 - SKU196: - - WAREHOUSE0 - SKU197: - - WAREHOUSE0 - SKU198: - - WAREHOUSE0 - SKU199: - - WAREHOUSE0 - SKU2: - - WAREHOUSE0 - SKU20: - - WAREHOUSE0 - SKU200: - - WAREHOUSE0 - SKU201: - - WAREHOUSE0 - SKU202: - - WAREHOUSE0 - SKU203: - - WAREHOUSE0 - SKU204: - - WAREHOUSE0 - SKU205: - - WAREHOUSE0 - SKU206: - - WAREHOUSE0 - SKU207: - - WAREHOUSE0 - SKU208: - - WAREHOUSE0 - SKU209: - - WAREHOUSE0 - SKU21: - - WAREHOUSE0 - SKU210: - - WAREHOUSE0 - SKU211: - - WAREHOUSE0 - SKU212: - - WAREHOUSE0 - SKU213: - - WAREHOUSE0 - SKU214: - - WAREHOUSE0 - SKU215: - - WAREHOUSE0 - SKU216: - - WAREHOUSE0 - SKU217: - - WAREHOUSE0 - SKU218: - - WAREHOUSE0 - SKU219: - - WAREHOUSE0 - SKU22: - - WAREHOUSE0 - SKU220: - - WAREHOUSE0 - SKU221: - - WAREHOUSE0 - SKU222: - - WAREHOUSE0 - SKU223: - - WAREHOUSE0 - SKU224: - - WAREHOUSE0 - SKU225: - - WAREHOUSE0 - SKU226: - - WAREHOUSE0 - SKU227: - - WAREHOUSE0 - SKU228: - - WAREHOUSE0 - SKU229: - - WAREHOUSE0 - SKU23: - - WAREHOUSE0 - SKU230: - - WAREHOUSE0 - SKU231: - - WAREHOUSE0 - SKU232: - - WAREHOUSE0 - SKU233: - - WAREHOUSE0 - SKU234: - - WAREHOUSE0 - SKU235: - - WAREHOUSE0 - SKU236: - - WAREHOUSE0 - SKU237: - - WAREHOUSE0 - SKU238: - - WAREHOUSE0 - SKU239: - - WAREHOUSE0 - SKU24: - - WAREHOUSE0 - SKU240: - - WAREHOUSE0 - SKU241: - - WAREHOUSE0 - SKU242: - - WAREHOUSE0 - SKU243: - - WAREHOUSE0 - SKU244: - - WAREHOUSE0 - SKU245: - - WAREHOUSE0 - SKU246: - - WAREHOUSE0 - SKU247: - - WAREHOUSE0 - SKU248: - - WAREHOUSE0 - SKU249: - - WAREHOUSE0 - SKU25: - - WAREHOUSE0 - SKU250: - - WAREHOUSE0 - SKU251: - - WAREHOUSE0 - SKU252: - - WAREHOUSE0 - SKU253: - - WAREHOUSE0 - SKU254: - - WAREHOUSE0 - SKU255: - - WAREHOUSE0 - SKU256: - - WAREHOUSE0 - SKU257: - - WAREHOUSE0 - SKU258: - - WAREHOUSE0 - SKU259: - - WAREHOUSE0 - SKU26: - - WAREHOUSE0 - SKU260: - - WAREHOUSE0 - SKU261: - - WAREHOUSE0 - SKU262: - - WAREHOUSE0 - SKU263: - - WAREHOUSE0 - SKU264: - - WAREHOUSE0 - SKU265: - - WAREHOUSE0 - SKU266: - - WAREHOUSE0 - SKU267: - - WAREHOUSE0 - SKU268: - - WAREHOUSE0 - SKU269: - - WAREHOUSE0 - SKU27: - - WAREHOUSE0 - SKU270: - - WAREHOUSE0 - SKU271: - - WAREHOUSE0 - SKU272: - - WAREHOUSE0 - SKU273: - - WAREHOUSE0 - SKU274: - - WAREHOUSE0 - SKU275: - - WAREHOUSE0 - SKU276: - - WAREHOUSE0 - SKU277: - - WAREHOUSE0 - SKU278: - - WAREHOUSE0 - SKU279: - - WAREHOUSE0 - SKU28: - - WAREHOUSE0 - SKU280: - - WAREHOUSE0 - SKU281: - - WAREHOUSE0 - SKU282: - - WAREHOUSE0 - SKU283: - - WAREHOUSE0 - SKU284: - - WAREHOUSE0 - SKU285: - - WAREHOUSE0 - SKU286: - - WAREHOUSE0 - SKU287: - - WAREHOUSE0 - SKU288: - - WAREHOUSE0 - SKU289: - - WAREHOUSE0 - SKU29: - - WAREHOUSE0 - SKU290: - - WAREHOUSE0 - SKU291: - - WAREHOUSE0 - SKU292: - - WAREHOUSE0 - SKU293: - - WAREHOUSE0 - SKU294: - - WAREHOUSE0 - SKU295: - - WAREHOUSE0 - SKU296: - - WAREHOUSE0 - SKU297: - - WAREHOUSE0 - SKU298: - - WAREHOUSE0 - SKU299: - - WAREHOUSE0 - SKU3: - - WAREHOUSE0 - SKU30: - - WAREHOUSE0 - SKU300: - - WAREHOUSE0 - SKU301: - - WAREHOUSE0 - SKU302: - - WAREHOUSE0 - SKU303: - - WAREHOUSE0 - SKU304: - - WAREHOUSE0 - SKU305: - - WAREHOUSE0 - SKU306: - - WAREHOUSE0 - SKU307: - - WAREHOUSE0 - SKU308: - - WAREHOUSE0 - SKU309: - - WAREHOUSE0 - SKU31: - - WAREHOUSE0 - SKU310: - - WAREHOUSE0 - SKU311: - - WAREHOUSE0 - SKU312: - - WAREHOUSE0 - SKU313: - - WAREHOUSE0 - SKU314: - - WAREHOUSE0 - SKU315: - - WAREHOUSE0 - SKU316: - - WAREHOUSE0 - SKU317: - - WAREHOUSE0 - SKU318: - - WAREHOUSE0 - SKU319: - - WAREHOUSE0 - SKU32: - - WAREHOUSE0 - SKU320: - - WAREHOUSE0 - SKU321: - - WAREHOUSE0 - SKU322: - - WAREHOUSE0 - SKU323: - - WAREHOUSE0 - SKU324: - - WAREHOUSE0 - SKU325: - - WAREHOUSE0 - SKU326: - - WAREHOUSE0 - SKU327: - - WAREHOUSE0 - SKU328: - - WAREHOUSE0 - SKU329: - - WAREHOUSE0 - SKU33: - - WAREHOUSE0 - SKU330: - - WAREHOUSE0 - SKU331: - - WAREHOUSE0 - SKU332: - - WAREHOUSE0 - SKU333: - - WAREHOUSE0 - SKU334: - - WAREHOUSE0 - SKU335: - - WAREHOUSE0 - SKU336: - - WAREHOUSE0 - SKU337: - - WAREHOUSE0 - SKU338: - - WAREHOUSE0 - SKU339: - - WAREHOUSE0 - SKU34: - - WAREHOUSE0 - SKU340: - - WAREHOUSE0 - SKU341: - - WAREHOUSE0 - SKU342: - - WAREHOUSE0 - SKU343: - - WAREHOUSE0 - SKU344: - - WAREHOUSE0 - SKU345: - - WAREHOUSE0 - SKU346: - - WAREHOUSE0 - SKU347: - - WAREHOUSE0 - SKU348: - - WAREHOUSE0 - SKU349: - - WAREHOUSE0 - SKU35: - - WAREHOUSE0 - SKU350: - - WAREHOUSE0 - SKU351: - - WAREHOUSE0 - SKU352: - - WAREHOUSE0 - SKU353: - - WAREHOUSE0 - SKU354: - - WAREHOUSE0 - SKU355: - - WAREHOUSE0 - SKU356: - - WAREHOUSE0 - SKU357: - - WAREHOUSE0 - SKU358: - - WAREHOUSE0 - SKU359: - - WAREHOUSE0 - SKU36: - - WAREHOUSE0 - SKU360: - - WAREHOUSE0 - SKU361: - - WAREHOUSE0 - SKU362: - - WAREHOUSE0 - SKU363: - - WAREHOUSE0 - SKU364: - - WAREHOUSE0 - SKU365: - - WAREHOUSE0 - SKU366: - - WAREHOUSE0 - SKU367: - - WAREHOUSE0 - SKU368: - - WAREHOUSE0 - SKU369: - - WAREHOUSE0 - SKU37: - - WAREHOUSE0 - SKU370: - - WAREHOUSE0 - SKU371: - - WAREHOUSE0 - SKU372: - - WAREHOUSE0 - SKU373: - - WAREHOUSE0 - SKU374: - - WAREHOUSE0 - SKU375: - - WAREHOUSE0 - SKU376: - - WAREHOUSE0 - SKU377: - - WAREHOUSE0 - SKU378: - - WAREHOUSE0 - SKU379: - - WAREHOUSE0 - SKU38: - - WAREHOUSE0 - SKU380: - - WAREHOUSE0 - SKU381: - - WAREHOUSE0 - SKU382: - - WAREHOUSE0 - SKU383: - - WAREHOUSE0 - SKU384: - - WAREHOUSE0 - SKU385: - - WAREHOUSE0 - SKU386: - - WAREHOUSE0 - SKU387: - - WAREHOUSE0 - SKU388: - - WAREHOUSE0 - SKU389: - - WAREHOUSE0 - SKU39: - - WAREHOUSE0 - SKU390: - - WAREHOUSE0 - SKU391: - - WAREHOUSE0 - SKU392: - - WAREHOUSE0 - SKU393: - - WAREHOUSE0 - SKU394: - - WAREHOUSE0 - SKU395: - - WAREHOUSE0 - SKU396: - - WAREHOUSE0 - SKU397: - - WAREHOUSE0 - SKU398: - - WAREHOUSE0 - SKU399: - - WAREHOUSE0 - SKU4: - - WAREHOUSE0 - SKU40: - - WAREHOUSE0 - SKU400: - - WAREHOUSE0 - SKU401: - - WAREHOUSE0 - SKU402: - - WAREHOUSE0 - SKU403: - - WAREHOUSE0 - SKU404: - - WAREHOUSE0 - SKU405: - - WAREHOUSE0 - SKU406: - - WAREHOUSE0 - SKU407: - - WAREHOUSE0 - SKU408: - - WAREHOUSE0 - SKU409: - - WAREHOUSE0 - SKU41: - - WAREHOUSE0 - SKU410: - - WAREHOUSE0 - SKU411: - - WAREHOUSE0 - SKU412: - - WAREHOUSE0 - SKU413: - - WAREHOUSE0 - SKU414: - - WAREHOUSE0 - SKU415: - - WAREHOUSE0 - SKU416: - - WAREHOUSE0 - SKU417: - - WAREHOUSE0 - SKU418: - - WAREHOUSE0 - SKU419: - - WAREHOUSE0 - SKU42: - - WAREHOUSE0 - SKU420: - - WAREHOUSE0 - SKU421: - - WAREHOUSE0 - SKU422: - - WAREHOUSE0 - SKU423: - - WAREHOUSE0 - SKU424: - - WAREHOUSE0 - SKU425: - - WAREHOUSE0 - SKU426: - - WAREHOUSE0 - SKU427: - - WAREHOUSE0 - SKU428: - - WAREHOUSE0 - SKU429: - - WAREHOUSE0 - SKU43: - - WAREHOUSE0 - SKU430: - - WAREHOUSE0 - SKU431: - - WAREHOUSE0 - SKU432: - - WAREHOUSE0 - SKU433: - - WAREHOUSE0 - SKU434: - - WAREHOUSE0 - SKU435: - - WAREHOUSE0 - SKU436: - - WAREHOUSE0 - SKU437: - - WAREHOUSE0 - SKU438: - - WAREHOUSE0 - SKU439: - - WAREHOUSE0 - SKU44: - - WAREHOUSE0 - SKU440: - - WAREHOUSE0 - SKU441: - - WAREHOUSE0 - SKU442: - - WAREHOUSE0 - SKU443: - - WAREHOUSE0 - SKU444: - - WAREHOUSE0 - SKU445: - - WAREHOUSE0 - SKU446: - - WAREHOUSE0 - SKU447: - - WAREHOUSE0 - SKU448: - - WAREHOUSE0 - SKU449: - - WAREHOUSE0 - SKU45: - - WAREHOUSE0 - SKU450: - - WAREHOUSE0 - SKU451: - - WAREHOUSE0 - SKU452: - - WAREHOUSE0 - SKU453: - - WAREHOUSE0 - SKU454: - - WAREHOUSE0 - SKU455: - - WAREHOUSE0 - SKU456: - - WAREHOUSE0 - SKU457: - - WAREHOUSE0 - SKU458: - - WAREHOUSE0 - SKU459: - - WAREHOUSE0 - SKU46: - - WAREHOUSE0 - SKU460: - - WAREHOUSE0 - SKU461: - - WAREHOUSE0 - SKU462: - - WAREHOUSE0 - SKU463: - - WAREHOUSE0 - SKU464: - - WAREHOUSE0 - SKU465: - - WAREHOUSE0 - SKU466: - - WAREHOUSE0 - SKU467: - - WAREHOUSE0 - SKU468: - - WAREHOUSE0 - SKU469: - - WAREHOUSE0 - SKU47: - - WAREHOUSE0 - SKU470: - - WAREHOUSE0 - SKU471: - - WAREHOUSE0 - SKU472: - - WAREHOUSE0 - SKU473: - - WAREHOUSE0 - SKU474: - - WAREHOUSE0 - SKU475: - - WAREHOUSE0 - SKU476: - - WAREHOUSE0 - SKU477: - - WAREHOUSE0 - SKU478: - - WAREHOUSE0 - SKU479: - - WAREHOUSE0 - SKU48: - - WAREHOUSE0 - SKU480: - - WAREHOUSE0 - SKU481: - - WAREHOUSE0 - SKU482: - - WAREHOUSE0 - SKU483: - - WAREHOUSE0 - SKU484: - - WAREHOUSE0 - SKU485: - - WAREHOUSE0 - SKU486: - - WAREHOUSE0 - SKU487: - - WAREHOUSE0 - SKU488: - - WAREHOUSE0 - SKU489: - - WAREHOUSE0 - SKU49: - - WAREHOUSE0 - SKU490: - - WAREHOUSE0 - SKU491: - - WAREHOUSE0 - SKU492: - - WAREHOUSE0 - SKU493: - - WAREHOUSE0 - SKU494: - - WAREHOUSE0 - SKU495: - - WAREHOUSE0 - SKU496: - - WAREHOUSE0 - SKU497: - - WAREHOUSE0 - SKU498: - - WAREHOUSE0 - SKU499: - - WAREHOUSE0 - SKU5: - - WAREHOUSE0 - SKU50: - - WAREHOUSE0 - SKU500: - - WAREHOUSE0 - SKU501: - - WAREHOUSE0 - SKU502: - - WAREHOUSE0 - SKU503: - - WAREHOUSE0 - SKU504: - - WAREHOUSE0 - SKU505: - - WAREHOUSE0 - SKU506: - - WAREHOUSE0 - SKU507: - - WAREHOUSE0 - SKU508: - - WAREHOUSE0 - SKU509: - - WAREHOUSE0 - SKU51: - - WAREHOUSE0 - SKU510: - - WAREHOUSE0 - SKU511: - - WAREHOUSE0 - SKU512: - - WAREHOUSE0 - SKU513: - - WAREHOUSE0 - SKU514: - - WAREHOUSE0 - SKU515: - - WAREHOUSE0 - SKU516: - - WAREHOUSE0 - SKU517: - - WAREHOUSE0 - SKU518: - - WAREHOUSE0 - SKU519: - - WAREHOUSE0 - SKU52: - - WAREHOUSE0 - SKU520: - - WAREHOUSE0 - SKU521: - - WAREHOUSE0 - SKU522: - - WAREHOUSE0 - SKU523: - - WAREHOUSE0 - SKU524: - - WAREHOUSE0 - SKU525: - - WAREHOUSE0 - SKU526: - - WAREHOUSE0 - SKU527: - - WAREHOUSE0 - SKU528: - - WAREHOUSE0 - SKU529: - - WAREHOUSE0 - SKU53: - - WAREHOUSE0 - SKU530: - - WAREHOUSE0 - SKU531: - - WAREHOUSE0 - SKU532: - - WAREHOUSE0 - SKU533: - - WAREHOUSE0 - SKU534: - - WAREHOUSE0 - SKU535: - - WAREHOUSE0 - SKU536: - - WAREHOUSE0 - SKU537: - - WAREHOUSE0 - SKU538: - - WAREHOUSE0 - SKU539: - - WAREHOUSE0 - SKU54: - - WAREHOUSE0 - SKU540: - - WAREHOUSE0 - SKU541: - - WAREHOUSE0 - SKU542: - - WAREHOUSE0 - SKU543: - - WAREHOUSE0 - SKU544: - - WAREHOUSE0 - SKU545: - - WAREHOUSE0 - SKU546: - - WAREHOUSE0 - SKU547: - - WAREHOUSE0 - SKU548: - - WAREHOUSE0 - SKU549: - - WAREHOUSE0 - SKU55: - - WAREHOUSE0 - SKU550: - - WAREHOUSE0 - SKU551: - - WAREHOUSE0 - SKU552: - - WAREHOUSE0 - SKU553: - - WAREHOUSE0 - SKU554: - - WAREHOUSE0 - SKU555: - - WAREHOUSE0 - SKU556: - - WAREHOUSE0 - SKU557: - - WAREHOUSE0 - SKU558: - - WAREHOUSE0 - SKU559: - - WAREHOUSE0 - SKU56: - - WAREHOUSE0 - SKU560: - - WAREHOUSE0 - SKU561: - - WAREHOUSE0 - SKU562: - - WAREHOUSE0 - SKU563: - - WAREHOUSE0 - SKU564: - - WAREHOUSE0 - SKU565: - - WAREHOUSE0 - SKU566: - - WAREHOUSE0 - SKU567: - - WAREHOUSE0 - SKU568: - - WAREHOUSE0 - SKU569: - - WAREHOUSE0 - SKU57: - - WAREHOUSE0 - SKU570: - - WAREHOUSE0 - SKU571: - - WAREHOUSE0 - SKU572: - - WAREHOUSE0 - SKU573: - - WAREHOUSE0 - SKU574: - - WAREHOUSE0 - SKU575: - - WAREHOUSE0 - SKU576: - - WAREHOUSE0 - SKU577: - - WAREHOUSE0 - SKU578: - - WAREHOUSE0 - SKU579: - - WAREHOUSE0 - SKU58: - - WAREHOUSE0 - SKU580: - - WAREHOUSE0 - SKU581: - - WAREHOUSE0 - SKU582: - - WAREHOUSE0 - SKU583: - - WAREHOUSE0 - SKU584: - - WAREHOUSE0 - SKU585: - - WAREHOUSE0 - SKU586: - - WAREHOUSE0 - SKU587: - - WAREHOUSE0 - SKU588: - - WAREHOUSE0 - SKU589: - - WAREHOUSE0 - SKU59: - - WAREHOUSE0 - SKU590: - - WAREHOUSE0 - SKU591: - - WAREHOUSE0 - SKU592: - - WAREHOUSE0 - SKU593: - - WAREHOUSE0 - SKU594: - - WAREHOUSE0 - SKU595: - - WAREHOUSE0 - SKU596: - - WAREHOUSE0 - SKU597: - - WAREHOUSE0 - SKU598: - - WAREHOUSE0 - SKU599: - - WAREHOUSE0 - SKU6: - - WAREHOUSE0 - SKU60: - - WAREHOUSE0 - SKU600: - - WAREHOUSE0 - SKU601: - - WAREHOUSE0 - SKU602: - - WAREHOUSE0 - SKU603: - - WAREHOUSE0 - SKU604: - - WAREHOUSE0 - SKU605: - - WAREHOUSE0 - SKU606: - - WAREHOUSE0 - SKU607: - - WAREHOUSE0 - SKU608: - - WAREHOUSE0 - SKU609: - - WAREHOUSE0 - SKU61: - - WAREHOUSE0 - SKU610: - - WAREHOUSE0 - SKU611: - - WAREHOUSE0 - SKU612: - - WAREHOUSE0 - SKU613: - - WAREHOUSE0 - SKU614: - - WAREHOUSE0 - SKU615: - - WAREHOUSE0 - SKU616: - - WAREHOUSE0 - SKU617: - - WAREHOUSE0 - SKU618: - - WAREHOUSE0 - SKU619: - - WAREHOUSE0 - SKU62: - - WAREHOUSE0 - SKU620: - - WAREHOUSE0 - SKU621: - - WAREHOUSE0 - SKU622: - - WAREHOUSE0 - SKU623: - - WAREHOUSE0 - SKU624: - - WAREHOUSE0 - SKU625: - - WAREHOUSE0 - SKU626: - - WAREHOUSE0 - SKU627: - - WAREHOUSE0 - SKU628: - - WAREHOUSE0 - SKU629: - - WAREHOUSE0 - SKU63: - - WAREHOUSE0 - SKU630: - - WAREHOUSE0 - SKU631: - - WAREHOUSE0 - SKU632: - - WAREHOUSE0 - SKU633: - - WAREHOUSE0 - SKU634: - - WAREHOUSE0 - SKU635: - - WAREHOUSE0 - SKU636: - - WAREHOUSE0 - SKU637: - - WAREHOUSE0 - SKU638: - - WAREHOUSE0 - SKU639: - - WAREHOUSE0 - SKU64: - - WAREHOUSE0 - SKU640: - - WAREHOUSE0 - SKU641: - - WAREHOUSE0 - SKU642: - - WAREHOUSE0 - SKU643: - - WAREHOUSE0 - SKU644: - - WAREHOUSE0 - SKU645: - - WAREHOUSE0 - SKU646: - - WAREHOUSE0 - SKU647: - - WAREHOUSE0 - SKU648: - - WAREHOUSE0 - SKU649: - - WAREHOUSE0 - SKU65: - - WAREHOUSE0 - SKU650: - - WAREHOUSE0 - SKU651: - - WAREHOUSE0 - SKU652: - - WAREHOUSE0 - SKU653: - - WAREHOUSE0 - SKU654: - - WAREHOUSE0 - SKU655: - - WAREHOUSE0 - SKU656: - - WAREHOUSE0 - SKU657: - - WAREHOUSE0 - SKU658: - - WAREHOUSE0 - SKU659: - - WAREHOUSE0 - SKU66: - - WAREHOUSE0 - SKU660: - - WAREHOUSE0 - SKU661: - - WAREHOUSE0 - SKU662: - - WAREHOUSE0 - SKU663: - - WAREHOUSE0 - SKU664: - - WAREHOUSE0 - SKU665: - - WAREHOUSE0 - SKU666: - - WAREHOUSE0 - SKU667: - - WAREHOUSE0 - SKU668: - - WAREHOUSE0 - SKU669: - - WAREHOUSE0 - SKU67: - - WAREHOUSE0 - SKU670: - - WAREHOUSE0 - SKU671: - - WAREHOUSE0 - SKU672: - - WAREHOUSE0 - SKU673: - - WAREHOUSE0 - SKU674: - - WAREHOUSE0 - SKU675: - - WAREHOUSE0 - SKU676: - - WAREHOUSE0 - SKU677: - - WAREHOUSE0 - SKU678: - - WAREHOUSE0 - SKU679: - - WAREHOUSE0 - SKU68: - - WAREHOUSE0 - SKU680: - - WAREHOUSE0 - SKU681: - - WAREHOUSE0 - SKU682: - - WAREHOUSE0 - SKU683: - - WAREHOUSE0 - SKU684: - - WAREHOUSE0 - SKU685: - - WAREHOUSE0 - SKU686: - - WAREHOUSE0 - SKU687: - - WAREHOUSE0 - SKU688: - - WAREHOUSE0 - SKU689: - - WAREHOUSE0 - SKU69: - - WAREHOUSE0 - SKU690: - - WAREHOUSE0 - SKU691: - - WAREHOUSE0 - SKU692: - - WAREHOUSE0 - SKU693: - - WAREHOUSE0 - SKU694: - - WAREHOUSE0 - SKU695: - - WAREHOUSE0 - SKU696: - - WAREHOUSE0 - SKU697: - - WAREHOUSE0 - SKU698: - - WAREHOUSE0 - SKU699: - - WAREHOUSE0 - SKU7: - - WAREHOUSE0 - SKU70: - - WAREHOUSE0 - SKU700: - - WAREHOUSE0 - SKU701: - - WAREHOUSE0 - SKU702: - - WAREHOUSE0 - SKU703: - - WAREHOUSE0 - SKU704: - - WAREHOUSE0 - SKU705: - - WAREHOUSE0 - SKU706: - - WAREHOUSE0 - SKU707: - - WAREHOUSE0 - SKU708: - - WAREHOUSE0 - SKU709: - - WAREHOUSE0 - SKU71: - - WAREHOUSE0 - SKU710: - - WAREHOUSE0 - SKU711: - - WAREHOUSE0 - SKU712: - - WAREHOUSE0 - SKU713: - - WAREHOUSE0 - SKU714: - - WAREHOUSE0 - SKU715: - - WAREHOUSE0 - SKU716: - - WAREHOUSE0 - SKU717: - - WAREHOUSE0 - SKU718: - - WAREHOUSE0 - SKU719: - - WAREHOUSE0 - SKU72: - - WAREHOUSE0 - SKU720: - - WAREHOUSE0 - SKU721: - - WAREHOUSE0 - SKU722: - - WAREHOUSE0 - SKU723: - - WAREHOUSE0 - SKU724: - - WAREHOUSE0 - SKU725: - - WAREHOUSE0 - SKU726: - - WAREHOUSE0 - SKU727: - - WAREHOUSE0 - SKU728: - - WAREHOUSE0 - SKU729: - - WAREHOUSE0 - SKU73: - - WAREHOUSE0 - SKU730: - - WAREHOUSE0 - SKU731: - - WAREHOUSE0 - SKU732: - - WAREHOUSE0 - SKU733: - - WAREHOUSE0 - SKU734: - - WAREHOUSE0 - SKU735: - - WAREHOUSE0 - SKU736: - - WAREHOUSE0 - SKU737: - - WAREHOUSE0 - SKU738: - - WAREHOUSE0 - SKU739: - - WAREHOUSE0 - SKU74: - - WAREHOUSE0 - SKU740: - - WAREHOUSE0 - SKU741: - - WAREHOUSE0 - SKU742: - - WAREHOUSE0 - SKU743: - - WAREHOUSE0 - SKU744: - - WAREHOUSE0 - SKU745: - - WAREHOUSE0 - SKU746: - - WAREHOUSE0 - SKU747: - - WAREHOUSE0 - SKU748: - - WAREHOUSE0 - SKU749: - - WAREHOUSE0 - SKU75: - - WAREHOUSE0 - SKU750: - - WAREHOUSE0 - SKU751: - - WAREHOUSE0 - SKU752: - - WAREHOUSE0 - SKU753: - - WAREHOUSE0 - SKU754: - - WAREHOUSE0 - SKU755: - - WAREHOUSE0 - SKU756: - - WAREHOUSE0 - SKU757: - - WAREHOUSE0 - SKU758: - - WAREHOUSE0 - SKU759: - - WAREHOUSE0 - SKU76: - - WAREHOUSE0 - SKU760: - - WAREHOUSE0 - SKU761: - - WAREHOUSE0 - SKU762: - - WAREHOUSE0 - SKU763: - - WAREHOUSE0 - SKU764: - - WAREHOUSE0 - SKU765: - - WAREHOUSE0 - SKU766: - - WAREHOUSE0 - SKU767: - - WAREHOUSE0 - SKU768: - - WAREHOUSE0 - SKU769: - - WAREHOUSE0 - SKU77: - - WAREHOUSE0 - SKU770: - - WAREHOUSE0 - SKU771: - - WAREHOUSE0 - SKU772: - - WAREHOUSE0 - SKU773: - - WAREHOUSE0 - SKU774: - - WAREHOUSE0 - SKU775: - - WAREHOUSE0 - SKU776: - - WAREHOUSE0 - SKU777: - - WAREHOUSE0 - SKU778: - - WAREHOUSE0 - SKU779: - - WAREHOUSE0 - SKU78: - - WAREHOUSE0 - SKU780: - - WAREHOUSE0 - SKU781: - - WAREHOUSE0 - SKU782: - - WAREHOUSE0 - SKU783: - - WAREHOUSE0 - SKU784: - - WAREHOUSE0 - SKU785: - - WAREHOUSE0 - SKU786: - - WAREHOUSE0 - SKU787: - - WAREHOUSE0 - SKU788: - - WAREHOUSE0 - SKU789: - - WAREHOUSE0 - SKU79: - - WAREHOUSE0 - SKU790: - - WAREHOUSE0 - SKU791: - - WAREHOUSE0 - SKU792: - - WAREHOUSE0 - SKU793: - - WAREHOUSE0 - SKU794: - - WAREHOUSE0 - SKU795: - - WAREHOUSE0 - SKU796: - - WAREHOUSE0 - SKU797: - - WAREHOUSE0 - SKU798: - - WAREHOUSE0 - SKU799: - - WAREHOUSE0 - SKU8: - - WAREHOUSE0 - SKU80: - - WAREHOUSE0 - SKU800: - - WAREHOUSE0 - SKU801: - - WAREHOUSE0 - SKU802: - - WAREHOUSE0 - SKU803: - - WAREHOUSE0 - SKU804: - - WAREHOUSE0 - SKU805: - - WAREHOUSE0 - SKU806: - - WAREHOUSE0 - SKU807: - - WAREHOUSE0 - SKU808: - - WAREHOUSE0 - SKU809: - - WAREHOUSE0 - SKU81: - - WAREHOUSE0 - SKU810: - - WAREHOUSE0 - SKU811: - - WAREHOUSE0 - SKU812: - - WAREHOUSE0 - SKU813: - - WAREHOUSE0 - SKU814: - - WAREHOUSE0 - SKU815: - - WAREHOUSE0 - SKU816: - - WAREHOUSE0 - SKU817: - - WAREHOUSE0 - SKU818: - - WAREHOUSE0 - SKU819: - - WAREHOUSE0 - SKU82: - - WAREHOUSE0 - SKU820: - - WAREHOUSE0 - SKU821: - - WAREHOUSE0 - SKU822: - - WAREHOUSE0 - SKU823: - - WAREHOUSE0 - SKU824: - - WAREHOUSE0 - SKU825: - - WAREHOUSE0 - SKU826: - - WAREHOUSE0 - SKU827: - - WAREHOUSE0 - SKU828: - - WAREHOUSE0 - SKU829: - - WAREHOUSE0 - SKU83: - - WAREHOUSE0 - SKU830: - - WAREHOUSE0 - SKU831: - - WAREHOUSE0 - SKU832: - - WAREHOUSE0 - SKU833: - - WAREHOUSE0 - SKU834: - - WAREHOUSE0 - SKU835: - - WAREHOUSE0 - SKU836: - - WAREHOUSE0 - SKU837: - - WAREHOUSE0 - SKU838: - - WAREHOUSE0 - SKU839: - - WAREHOUSE0 - SKU84: - - WAREHOUSE0 - SKU840: - - WAREHOUSE0 - SKU841: - - WAREHOUSE0 - SKU842: - - WAREHOUSE0 - SKU843: - - WAREHOUSE0 - SKU844: - - WAREHOUSE0 - SKU845: - - WAREHOUSE0 - SKU846: - - WAREHOUSE0 - SKU847: - - WAREHOUSE0 - SKU848: - - WAREHOUSE0 - SKU849: - - WAREHOUSE0 - SKU85: - - WAREHOUSE0 - SKU850: - - WAREHOUSE0 - SKU851: - - WAREHOUSE0 - SKU852: - - WAREHOUSE0 - SKU853: - - WAREHOUSE0 - SKU854: - - WAREHOUSE0 - SKU855: - - WAREHOUSE0 - SKU856: - - WAREHOUSE0 - SKU857: - - WAREHOUSE0 - SKU858: - - WAREHOUSE0 - SKU859: - - WAREHOUSE0 - SKU86: - - WAREHOUSE0 - SKU860: - - WAREHOUSE0 - SKU861: - - WAREHOUSE0 - SKU862: - - WAREHOUSE0 - SKU863: - - WAREHOUSE0 - SKU864: - - WAREHOUSE0 - SKU865: - - WAREHOUSE0 - SKU866: - - WAREHOUSE0 - SKU867: - - WAREHOUSE0 - SKU868: - - WAREHOUSE0 - SKU869: - - WAREHOUSE0 - SKU87: - - WAREHOUSE0 - SKU870: - - WAREHOUSE0 - SKU871: - - WAREHOUSE0 - SKU872: - - WAREHOUSE0 - SKU873: - - WAREHOUSE0 - SKU874: - - WAREHOUSE0 - SKU875: - - WAREHOUSE0 - SKU876: - - WAREHOUSE0 - SKU877: - - WAREHOUSE0 - SKU878: - - WAREHOUSE0 - SKU879: - - WAREHOUSE0 - SKU88: - - WAREHOUSE0 - SKU880: - - WAREHOUSE0 - SKU881: - - WAREHOUSE0 - SKU882: - - WAREHOUSE0 - SKU883: - - WAREHOUSE0 - SKU884: - - WAREHOUSE0 - SKU885: - - WAREHOUSE0 - SKU886: - - WAREHOUSE0 - SKU887: - - WAREHOUSE0 - SKU888: - - WAREHOUSE0 - SKU889: - - WAREHOUSE0 - SKU89: - - WAREHOUSE0 - SKU890: - - WAREHOUSE0 - SKU891: - - WAREHOUSE0 - SKU892: - - WAREHOUSE0 - SKU893: - - WAREHOUSE0 - SKU894: - - WAREHOUSE0 - SKU895: - - WAREHOUSE0 - SKU896: - - WAREHOUSE0 - SKU897: - - WAREHOUSE0 - SKU898: - - WAREHOUSE0 - SKU899: - - WAREHOUSE0 - SKU9: - - WAREHOUSE0 - SKU90: - - WAREHOUSE0 - SKU900: - - WAREHOUSE0 - SKU901: - - WAREHOUSE0 - SKU902: - - WAREHOUSE0 - SKU903: - - WAREHOUSE0 - SKU904: - - WAREHOUSE0 - SKU905: - - WAREHOUSE0 - SKU906: - - WAREHOUSE0 - SKU907: - - WAREHOUSE0 - SKU908: - - WAREHOUSE0 - SKU909: - - WAREHOUSE0 - SKU91: - - WAREHOUSE0 - SKU910: - - WAREHOUSE0 - SKU911: - - WAREHOUSE0 - SKU912: - - WAREHOUSE0 - SKU913: - - WAREHOUSE0 - SKU914: - - WAREHOUSE0 - SKU915: - - WAREHOUSE0 - SKU916: - - WAREHOUSE0 - SKU917: - - WAREHOUSE0 - SKU918: - - WAREHOUSE0 - SKU919: - - WAREHOUSE0 - SKU92: - - WAREHOUSE0 - SKU920: - - WAREHOUSE0 - SKU921: - - WAREHOUSE0 - SKU922: - - WAREHOUSE0 - SKU923: - - WAREHOUSE0 - SKU924: - - WAREHOUSE0 - SKU925: - - WAREHOUSE0 - SKU926: - - WAREHOUSE0 - SKU927: - - WAREHOUSE0 - SKU928: - - WAREHOUSE0 - SKU929: - - WAREHOUSE0 - SKU93: - - WAREHOUSE0 - SKU930: - - WAREHOUSE0 - SKU931: - - WAREHOUSE0 - SKU932: - - WAREHOUSE0 - SKU933: - - WAREHOUSE0 - SKU934: - - WAREHOUSE0 - SKU935: - - WAREHOUSE0 - SKU936: - - WAREHOUSE0 - SKU937: - - WAREHOUSE0 - SKU938: - - WAREHOUSE0 - SKU939: - - WAREHOUSE0 - SKU94: - - WAREHOUSE0 - SKU940: - - WAREHOUSE0 - SKU941: - - WAREHOUSE0 - SKU942: - - WAREHOUSE0 - SKU943: - - WAREHOUSE0 - SKU944: - - WAREHOUSE0 - SKU945: - - WAREHOUSE0 - SKU946: - - WAREHOUSE0 - SKU947: - - WAREHOUSE0 - SKU948: - - WAREHOUSE0 - SKU949: - - WAREHOUSE0 - SKU95: - - WAREHOUSE0 - SKU950: - - WAREHOUSE0 - SKU951: - - WAREHOUSE0 - SKU952: - - WAREHOUSE0 - SKU953: - - WAREHOUSE0 - SKU954: - - WAREHOUSE0 - SKU955: - - WAREHOUSE0 - SKU956: - - WAREHOUSE0 - SKU957: - - WAREHOUSE0 - SKU958: - - WAREHOUSE0 - SKU959: - - WAREHOUSE0 - SKU96: - - WAREHOUSE0 - SKU960: - - WAREHOUSE0 - SKU961: - - WAREHOUSE0 - SKU962: - - WAREHOUSE0 - SKU963: - - WAREHOUSE0 - SKU964: - - WAREHOUSE0 - SKU965: - - WAREHOUSE0 - SKU966: - - WAREHOUSE0 - SKU967: - - WAREHOUSE0 - SKU968: - - WAREHOUSE0 - SKU969: - - WAREHOUSE0 - SKU97: - - WAREHOUSE0 - SKU970: - - WAREHOUSE0 - SKU971: - - WAREHOUSE0 - SKU972: - - WAREHOUSE0 - SKU973: - - WAREHOUSE0 - SKU974: - - WAREHOUSE0 - SKU975: - - WAREHOUSE0 - SKU976: - - WAREHOUSE0 - SKU977: - - WAREHOUSE0 - SKU978: - - WAREHOUSE0 - SKU979: - - WAREHOUSE0 - SKU98: - - WAREHOUSE0 - SKU980: - - WAREHOUSE0 - SKU981: - - WAREHOUSE0 - SKU982: - - WAREHOUSE0 - SKU983: - - WAREHOUSE0 - SKU984: - - WAREHOUSE0 - SKU985: - - WAREHOUSE0 - SKU986: - - WAREHOUSE0 - SKU987: - - WAREHOUSE0 - SKU988: - - WAREHOUSE0 - SKU989: - - WAREHOUSE0 - SKU99: - - WAREHOUSE0 - SKU990: - - WAREHOUSE0 - SKU991: - - WAREHOUSE0 - SKU992: - - WAREHOUSE0 - SKU993: - - WAREHOUSE0 - SKU994: - - WAREHOUSE0 - SKU995: - - WAREHOUSE0 - SKU996: - - WAREHOUSE0 - SKU997: - - WAREHOUSE0 - SKU998: - - WAREHOUSE0 - SKU999: - - WAREHOUSE0 - WAREHOUSE0: - SKU0: - - SUPPLIER0 - SKU1: - - SUPPLIER0 - SKU10: - - SUPPLIER0 - SKU100: - - SUPPLIER0 - SKU101: - - SUPPLIER0 - SKU102: - - SUPPLIER0 - SKU103: - - SUPPLIER0 - SKU104: - - SUPPLIER0 - SKU105: - - SUPPLIER0 - SKU106: - - SUPPLIER0 - SKU107: - - SUPPLIER0 - SKU108: - - SUPPLIER0 - SKU109: - - SUPPLIER0 - SKU11: - - SUPPLIER0 - SKU110: - - SUPPLIER0 - SKU111: - - SUPPLIER0 - SKU112: - - SUPPLIER0 - SKU113: - - SUPPLIER0 - SKU114: - - SUPPLIER0 - SKU115: - - SUPPLIER0 - SKU116: - - SUPPLIER0 - SKU117: - - SUPPLIER0 - SKU118: - - SUPPLIER0 - SKU119: - - SUPPLIER0 - SKU12: - - SUPPLIER0 - SKU120: - - SUPPLIER0 - SKU121: - - SUPPLIER0 - SKU122: - - SUPPLIER0 - SKU123: - - SUPPLIER0 - SKU124: - - SUPPLIER0 - SKU125: - - SUPPLIER0 - SKU126: - - SUPPLIER0 - SKU127: - - SUPPLIER0 - SKU128: - - SUPPLIER0 - SKU129: - - SUPPLIER0 - SKU13: - - SUPPLIER0 - SKU130: - - SUPPLIER0 - SKU131: - - SUPPLIER0 - SKU132: - - SUPPLIER0 - SKU133: - - SUPPLIER0 - SKU134: - - SUPPLIER0 - SKU135: - - SUPPLIER0 - SKU136: - - SUPPLIER0 - SKU137: - - SUPPLIER0 - SKU138: - - SUPPLIER0 - SKU139: - - SUPPLIER0 - SKU14: - - SUPPLIER0 - SKU140: - - SUPPLIER0 - SKU141: - - SUPPLIER0 - SKU142: - - SUPPLIER0 - SKU143: - - SUPPLIER0 - SKU144: - - SUPPLIER0 - SKU145: - - SUPPLIER0 - SKU146: - - SUPPLIER0 - SKU147: - - SUPPLIER0 - SKU148: - - SUPPLIER0 - SKU149: - - SUPPLIER0 - SKU15: - - SUPPLIER0 - SKU150: - - SUPPLIER0 - SKU151: - - SUPPLIER0 - SKU152: - - SUPPLIER0 - SKU153: - - SUPPLIER0 - SKU154: - - SUPPLIER0 - SKU155: - - SUPPLIER0 - SKU156: - - SUPPLIER0 - SKU157: - - SUPPLIER0 - SKU158: - - SUPPLIER0 - SKU159: - - SUPPLIER0 - SKU16: - - SUPPLIER0 - SKU160: - - SUPPLIER0 - SKU161: - - SUPPLIER0 - SKU162: - - SUPPLIER0 - SKU163: - - SUPPLIER0 - SKU164: - - SUPPLIER0 - SKU165: - - SUPPLIER0 - SKU166: - - SUPPLIER0 - SKU167: - - SUPPLIER0 - SKU168: - - SUPPLIER0 - SKU169: - - SUPPLIER0 - SKU17: - - SUPPLIER0 - SKU170: - - SUPPLIER0 - SKU171: - - SUPPLIER0 - SKU172: - - SUPPLIER0 - SKU173: - - SUPPLIER0 - SKU174: - - SUPPLIER0 - SKU175: - - SUPPLIER0 - SKU176: - - SUPPLIER0 - SKU177: - - SUPPLIER0 - SKU178: - - SUPPLIER0 - SKU179: - - SUPPLIER0 - SKU18: - - SUPPLIER0 - SKU180: - - SUPPLIER0 - SKU181: - - SUPPLIER0 - SKU182: - - SUPPLIER0 - SKU183: - - SUPPLIER0 - SKU184: - - SUPPLIER0 - SKU185: - - SUPPLIER0 - SKU186: - - SUPPLIER0 - SKU187: - - SUPPLIER0 - SKU188: - - SUPPLIER0 - SKU189: - - SUPPLIER0 - SKU19: - - SUPPLIER0 - SKU190: - - SUPPLIER0 - SKU191: - - SUPPLIER0 - SKU192: - - SUPPLIER0 - SKU193: - - SUPPLIER0 - SKU194: - - SUPPLIER0 - SKU195: - - SUPPLIER0 - SKU196: - - SUPPLIER0 - SKU197: - - SUPPLIER0 - SKU198: - - SUPPLIER0 - SKU199: - - SUPPLIER0 - SKU2: - - SUPPLIER0 - SKU20: - - SUPPLIER0 - SKU200: - - SUPPLIER0 - SKU201: - - SUPPLIER0 - SKU202: - - SUPPLIER0 - SKU203: - - SUPPLIER0 - SKU204: - - SUPPLIER0 - SKU205: - - SUPPLIER0 - SKU206: - - SUPPLIER0 - SKU207: - - SUPPLIER0 - SKU208: - - SUPPLIER0 - SKU209: - - SUPPLIER0 - SKU21: - - SUPPLIER0 - SKU210: - - SUPPLIER0 - SKU211: - - SUPPLIER0 - SKU212: - - SUPPLIER0 - SKU213: - - SUPPLIER0 - SKU214: - - SUPPLIER0 - SKU215: - - SUPPLIER0 - SKU216: - - SUPPLIER0 - SKU217: - - SUPPLIER0 - SKU218: - - SUPPLIER0 - SKU219: - - SUPPLIER0 - SKU22: - - SUPPLIER0 - SKU220: - - SUPPLIER0 - SKU221: - - SUPPLIER0 - SKU222: - - SUPPLIER0 - SKU223: - - SUPPLIER0 - SKU224: - - SUPPLIER0 - SKU225: - - SUPPLIER0 - SKU226: - - SUPPLIER0 - SKU227: - - SUPPLIER0 - SKU228: - - SUPPLIER0 - SKU229: - - SUPPLIER0 - SKU23: - - SUPPLIER0 - SKU230: - - SUPPLIER0 - SKU231: - - SUPPLIER0 - SKU232: - - SUPPLIER0 - SKU233: - - SUPPLIER0 - SKU234: - - SUPPLIER0 - SKU235: - - SUPPLIER0 - SKU236: - - SUPPLIER0 - SKU237: - - SUPPLIER0 - SKU238: - - SUPPLIER0 - SKU239: - - SUPPLIER0 - SKU24: - - SUPPLIER0 - SKU240: - - SUPPLIER0 - SKU241: - - SUPPLIER0 - SKU242: - - SUPPLIER0 - SKU243: - - SUPPLIER0 - SKU244: - - SUPPLIER0 - SKU245: - - SUPPLIER0 - SKU246: - - SUPPLIER0 - SKU247: - - SUPPLIER0 - SKU248: - - SUPPLIER0 - SKU249: - - SUPPLIER0 - SKU25: - - SUPPLIER0 - SKU250: - - SUPPLIER0 - SKU251: - - SUPPLIER0 - SKU252: - - SUPPLIER0 - SKU253: - - SUPPLIER0 - SKU254: - - SUPPLIER0 - SKU255: - - SUPPLIER0 - SKU256: - - SUPPLIER0 - SKU257: - - SUPPLIER0 - SKU258: - - SUPPLIER0 - SKU259: - - SUPPLIER0 - SKU26: - - SUPPLIER0 - SKU260: - - SUPPLIER0 - SKU261: - - SUPPLIER0 - SKU262: - - SUPPLIER0 - SKU263: - - SUPPLIER0 - SKU264: - - SUPPLIER0 - SKU265: - - SUPPLIER0 - SKU266: - - SUPPLIER0 - SKU267: - - SUPPLIER0 - SKU268: - - SUPPLIER0 - SKU269: - - SUPPLIER0 - SKU27: - - SUPPLIER0 - SKU270: - - SUPPLIER0 - SKU271: - - SUPPLIER0 - SKU272: - - SUPPLIER0 - SKU273: - - SUPPLIER0 - SKU274: - - SUPPLIER0 - SKU275: - - SUPPLIER0 - SKU276: - - SUPPLIER0 - SKU277: - - SUPPLIER0 - SKU278: - - SUPPLIER0 - SKU279: - - SUPPLIER0 - SKU28: - - SUPPLIER0 - SKU280: - - SUPPLIER0 - SKU281: - - SUPPLIER0 - SKU282: - - SUPPLIER0 - SKU283: - - SUPPLIER0 - SKU284: - - SUPPLIER0 - SKU285: - - SUPPLIER0 - SKU286: - - SUPPLIER0 - SKU287: - - SUPPLIER0 - SKU288: - - SUPPLIER0 - SKU289: - - SUPPLIER0 - SKU29: - - SUPPLIER0 - SKU290: - - SUPPLIER0 - SKU291: - - SUPPLIER0 - SKU292: - - SUPPLIER0 - SKU293: - - SUPPLIER0 - SKU294: - - SUPPLIER0 - SKU295: - - SUPPLIER0 - SKU296: - - SUPPLIER0 - SKU297: - - SUPPLIER0 - SKU298: - - SUPPLIER0 - SKU299: - - SUPPLIER0 - SKU3: - - SUPPLIER0 - SKU30: - - SUPPLIER0 - SKU300: - - SUPPLIER0 - SKU301: - - SUPPLIER0 - SKU302: - - SUPPLIER0 - SKU303: - - SUPPLIER0 - SKU304: - - SUPPLIER0 - SKU305: - - SUPPLIER0 - SKU306: - - SUPPLIER0 - SKU307: - - SUPPLIER0 - SKU308: - - SUPPLIER0 - SKU309: - - SUPPLIER0 - SKU31: - - SUPPLIER0 - SKU310: - - SUPPLIER0 - SKU311: - - SUPPLIER0 - SKU312: - - SUPPLIER0 - SKU313: - - SUPPLIER0 - SKU314: - - SUPPLIER0 - SKU315: - - SUPPLIER0 - SKU316: - - SUPPLIER0 - SKU317: - - SUPPLIER0 - SKU318: - - SUPPLIER0 - SKU319: - - SUPPLIER0 - SKU32: - - SUPPLIER0 - SKU320: - - SUPPLIER0 - SKU321: - - SUPPLIER0 - SKU322: - - SUPPLIER0 - SKU323: - - SUPPLIER0 - SKU324: - - SUPPLIER0 - SKU325: - - SUPPLIER0 - SKU326: - - SUPPLIER0 - SKU327: - - SUPPLIER0 - SKU328: - - SUPPLIER0 - SKU329: - - SUPPLIER0 - SKU33: - - SUPPLIER0 - SKU330: - - SUPPLIER0 - SKU331: - - SUPPLIER0 - SKU332: - - SUPPLIER0 - SKU333: - - SUPPLIER0 - SKU334: - - SUPPLIER0 - SKU335: - - SUPPLIER0 - SKU336: - - SUPPLIER0 - SKU337: - - SUPPLIER0 - SKU338: - - SUPPLIER0 - SKU339: - - SUPPLIER0 - SKU34: - - SUPPLIER0 - SKU340: - - SUPPLIER0 - SKU341: - - SUPPLIER0 - SKU342: - - SUPPLIER0 - SKU343: - - SUPPLIER0 - SKU344: - - SUPPLIER0 - SKU345: - - SUPPLIER0 - SKU346: - - SUPPLIER0 - SKU347: - - SUPPLIER0 - SKU348: - - SUPPLIER0 - SKU349: - - SUPPLIER0 - SKU35: - - SUPPLIER0 - SKU350: - - SUPPLIER0 - SKU351: - - SUPPLIER0 - SKU352: - - SUPPLIER0 - SKU353: - - SUPPLIER0 - SKU354: - - SUPPLIER0 - SKU355: - - SUPPLIER0 - SKU356: - - SUPPLIER0 - SKU357: - - SUPPLIER0 - SKU358: - - SUPPLIER0 - SKU359: - - SUPPLIER0 - SKU36: - - SUPPLIER0 - SKU360: - - SUPPLIER0 - SKU361: - - SUPPLIER0 - SKU362: - - SUPPLIER0 - SKU363: - - SUPPLIER0 - SKU364: - - SUPPLIER0 - SKU365: - - SUPPLIER0 - SKU366: - - SUPPLIER0 - SKU367: - - SUPPLIER0 - SKU368: - - SUPPLIER0 - SKU369: - - SUPPLIER0 - SKU37: - - SUPPLIER0 - SKU370: - - SUPPLIER0 - SKU371: - - SUPPLIER0 - SKU372: - - SUPPLIER0 - SKU373: - - SUPPLIER0 - SKU374: - - SUPPLIER0 - SKU375: - - SUPPLIER0 - SKU376: - - SUPPLIER0 - SKU377: - - SUPPLIER0 - SKU378: - - SUPPLIER0 - SKU379: - - SUPPLIER0 - SKU38: - - SUPPLIER0 - SKU380: - - SUPPLIER0 - SKU381: - - SUPPLIER0 - SKU382: - - SUPPLIER0 - SKU383: - - SUPPLIER0 - SKU384: - - SUPPLIER0 - SKU385: - - SUPPLIER0 - SKU386: - - SUPPLIER0 - SKU387: - - SUPPLIER0 - SKU388: - - SUPPLIER0 - SKU389: - - SUPPLIER0 - SKU39: - - SUPPLIER0 - SKU390: - - SUPPLIER0 - SKU391: - - SUPPLIER0 - SKU392: - - SUPPLIER0 - SKU393: - - SUPPLIER0 - SKU394: - - SUPPLIER0 - SKU395: - - SUPPLIER0 - SKU396: - - SUPPLIER0 - SKU397: - - SUPPLIER0 - SKU398: - - SUPPLIER0 - SKU399: - - SUPPLIER0 - SKU4: - - SUPPLIER0 - SKU40: - - SUPPLIER0 - SKU400: - - SUPPLIER0 - SKU401: - - SUPPLIER0 - SKU402: - - SUPPLIER0 - SKU403: - - SUPPLIER0 - SKU404: - - SUPPLIER0 - SKU405: - - SUPPLIER0 - SKU406: - - SUPPLIER0 - SKU407: - - SUPPLIER0 - SKU408: - - SUPPLIER0 - SKU409: - - SUPPLIER0 - SKU41: - - SUPPLIER0 - SKU410: - - SUPPLIER0 - SKU411: - - SUPPLIER0 - SKU412: - - SUPPLIER0 - SKU413: - - SUPPLIER0 - SKU414: - - SUPPLIER0 - SKU415: - - SUPPLIER0 - SKU416: - - SUPPLIER0 - SKU417: - - SUPPLIER0 - SKU418: - - SUPPLIER0 - SKU419: - - SUPPLIER0 - SKU42: - - SUPPLIER0 - SKU420: - - SUPPLIER0 - SKU421: - - SUPPLIER0 - SKU422: - - SUPPLIER0 - SKU423: - - SUPPLIER0 - SKU424: - - SUPPLIER0 - SKU425: - - SUPPLIER0 - SKU426: - - SUPPLIER0 - SKU427: - - SUPPLIER0 - SKU428: - - SUPPLIER0 - SKU429: - - SUPPLIER0 - SKU43: - - SUPPLIER0 - SKU430: - - SUPPLIER0 - SKU431: - - SUPPLIER0 - SKU432: - - SUPPLIER0 - SKU433: - - SUPPLIER0 - SKU434: - - SUPPLIER0 - SKU435: - - SUPPLIER0 - SKU436: - - SUPPLIER0 - SKU437: - - SUPPLIER0 - SKU438: - - SUPPLIER0 - SKU439: - - SUPPLIER0 - SKU44: - - SUPPLIER0 - SKU440: - - SUPPLIER0 - SKU441: - - SUPPLIER0 - SKU442: - - SUPPLIER0 - SKU443: - - SUPPLIER0 - SKU444: - - SUPPLIER0 - SKU445: - - SUPPLIER0 - SKU446: - - SUPPLIER0 - SKU447: - - SUPPLIER0 - SKU448: - - SUPPLIER0 - SKU449: - - SUPPLIER0 - SKU45: - - SUPPLIER0 - SKU450: - - SUPPLIER0 - SKU451: - - SUPPLIER0 - SKU452: - - SUPPLIER0 - SKU453: - - SUPPLIER0 - SKU454: - - SUPPLIER0 - SKU455: - - SUPPLIER0 - SKU456: - - SUPPLIER0 - SKU457: - - SUPPLIER0 - SKU458: - - SUPPLIER0 - SKU459: - - SUPPLIER0 - SKU46: - - SUPPLIER0 - SKU460: - - SUPPLIER0 - SKU461: - - SUPPLIER0 - SKU462: - - SUPPLIER0 - SKU463: - - SUPPLIER0 - SKU464: - - SUPPLIER0 - SKU465: - - SUPPLIER0 - SKU466: - - SUPPLIER0 - SKU467: - - SUPPLIER0 - SKU468: - - SUPPLIER0 - SKU469: - - SUPPLIER0 - SKU47: - - SUPPLIER0 - SKU470: - - SUPPLIER0 - SKU471: - - SUPPLIER0 - SKU472: - - SUPPLIER0 - SKU473: - - SUPPLIER0 - SKU474: - - SUPPLIER0 - SKU475: - - SUPPLIER0 - SKU476: - - SUPPLIER0 - SKU477: - - SUPPLIER0 - SKU478: - - SUPPLIER0 - SKU479: - - SUPPLIER0 - SKU48: - - SUPPLIER0 - SKU480: - - SUPPLIER0 - SKU481: - - SUPPLIER0 - SKU482: - - SUPPLIER0 - SKU483: - - SUPPLIER0 - SKU484: - - SUPPLIER0 - SKU485: - - SUPPLIER0 - SKU486: - - SUPPLIER0 - SKU487: - - SUPPLIER0 - SKU488: - - SUPPLIER0 - SKU489: - - SUPPLIER0 - SKU49: - - SUPPLIER0 - SKU490: - - SUPPLIER0 - SKU491: - - SUPPLIER0 - SKU492: - - SUPPLIER0 - SKU493: - - SUPPLIER0 - SKU494: - - SUPPLIER0 - SKU495: - - SUPPLIER0 - SKU496: - - SUPPLIER0 - SKU497: - - SUPPLIER0 - SKU498: - - SUPPLIER0 - SKU499: - - SUPPLIER0 - SKU5: - - SUPPLIER0 - SKU50: - - SUPPLIER0 - SKU500: - - SUPPLIER0 - SKU501: - - SUPPLIER0 - SKU502: - - SUPPLIER0 - SKU503: - - SUPPLIER0 - SKU504: - - SUPPLIER0 - SKU505: - - SUPPLIER0 - SKU506: - - SUPPLIER0 - SKU507: - - SUPPLIER0 - SKU508: - - SUPPLIER0 - SKU509: - - SUPPLIER0 - SKU51: - - SUPPLIER0 - SKU510: - - SUPPLIER0 - SKU511: - - SUPPLIER0 - SKU512: - - SUPPLIER0 - SKU513: - - SUPPLIER0 - SKU514: - - SUPPLIER0 - SKU515: - - SUPPLIER0 - SKU516: - - SUPPLIER0 - SKU517: - - SUPPLIER0 - SKU518: - - SUPPLIER0 - SKU519: - - SUPPLIER0 - SKU52: - - SUPPLIER0 - SKU520: - - SUPPLIER0 - SKU521: - - SUPPLIER0 - SKU522: - - SUPPLIER0 - SKU523: - - SUPPLIER0 - SKU524: - - SUPPLIER0 - SKU525: - - SUPPLIER0 - SKU526: - - SUPPLIER0 - SKU527: - - SUPPLIER0 - SKU528: - - SUPPLIER0 - SKU529: - - SUPPLIER0 - SKU53: - - SUPPLIER0 - SKU530: - - SUPPLIER0 - SKU531: - - SUPPLIER0 - SKU532: - - SUPPLIER0 - SKU533: - - SUPPLIER0 - SKU534: - - SUPPLIER0 - SKU535: - - SUPPLIER0 - SKU536: - - SUPPLIER0 - SKU537: - - SUPPLIER0 - SKU538: - - SUPPLIER0 - SKU539: - - SUPPLIER0 - SKU54: - - SUPPLIER0 - SKU540: - - SUPPLIER0 - SKU541: - - SUPPLIER0 - SKU542: - - SUPPLIER0 - SKU543: - - SUPPLIER0 - SKU544: - - SUPPLIER0 - SKU545: - - SUPPLIER0 - SKU546: - - SUPPLIER0 - SKU547: - - SUPPLIER0 - SKU548: - - SUPPLIER0 - SKU549: - - SUPPLIER0 - SKU55: - - SUPPLIER0 - SKU550: - - SUPPLIER0 - SKU551: - - SUPPLIER0 - SKU552: - - SUPPLIER0 - SKU553: - - SUPPLIER0 - SKU554: - - SUPPLIER0 - SKU555: - - SUPPLIER0 - SKU556: - - SUPPLIER0 - SKU557: - - SUPPLIER0 - SKU558: - - SUPPLIER0 - SKU559: - - SUPPLIER0 - SKU56: - - SUPPLIER0 - SKU560: - - SUPPLIER0 - SKU561: - - SUPPLIER0 - SKU562: - - SUPPLIER0 - SKU563: - - SUPPLIER0 - SKU564: - - SUPPLIER0 - SKU565: - - SUPPLIER0 - SKU566: - - SUPPLIER0 - SKU567: - - SUPPLIER0 - SKU568: - - SUPPLIER0 - SKU569: - - SUPPLIER0 - SKU57: - - SUPPLIER0 - SKU570: - - SUPPLIER0 - SKU571: - - SUPPLIER0 - SKU572: - - SUPPLIER0 - SKU573: - - SUPPLIER0 - SKU574: - - SUPPLIER0 - SKU575: - - SUPPLIER0 - SKU576: - - SUPPLIER0 - SKU577: - - SUPPLIER0 - SKU578: - - SUPPLIER0 - SKU579: - - SUPPLIER0 - SKU58: - - SUPPLIER0 - SKU580: - - SUPPLIER0 - SKU581: - - SUPPLIER0 - SKU582: - - SUPPLIER0 - SKU583: - - SUPPLIER0 - SKU584: - - SUPPLIER0 - SKU585: - - SUPPLIER0 - SKU586: - - SUPPLIER0 - SKU587: - - SUPPLIER0 - SKU588: - - SUPPLIER0 - SKU589: - - SUPPLIER0 - SKU59: - - SUPPLIER0 - SKU590: - - SUPPLIER0 - SKU591: - - SUPPLIER0 - SKU592: - - SUPPLIER0 - SKU593: - - SUPPLIER0 - SKU594: - - SUPPLIER0 - SKU595: - - SUPPLIER0 - SKU596: - - SUPPLIER0 - SKU597: - - SUPPLIER0 - SKU598: - - SUPPLIER0 - SKU599: - - SUPPLIER0 - SKU6: - - SUPPLIER0 - SKU60: - - SUPPLIER0 - SKU600: - - SUPPLIER0 - SKU601: - - SUPPLIER0 - SKU602: - - SUPPLIER0 - SKU603: - - SUPPLIER0 - SKU604: - - SUPPLIER0 - SKU605: - - SUPPLIER0 - SKU606: - - SUPPLIER0 - SKU607: - - SUPPLIER0 - SKU608: - - SUPPLIER0 - SKU609: - - SUPPLIER0 - SKU61: - - SUPPLIER0 - SKU610: - - SUPPLIER0 - SKU611: - - SUPPLIER0 - SKU612: - - SUPPLIER0 - SKU613: - - SUPPLIER0 - SKU614: - - SUPPLIER0 - SKU615: - - SUPPLIER0 - SKU616: - - SUPPLIER0 - SKU617: - - SUPPLIER0 - SKU618: - - SUPPLIER0 - SKU619: - - SUPPLIER0 - SKU62: - - SUPPLIER0 - SKU620: - - SUPPLIER0 - SKU621: - - SUPPLIER0 - SKU622: - - SUPPLIER0 - SKU623: - - SUPPLIER0 - SKU624: - - SUPPLIER0 - SKU625: - - SUPPLIER0 - SKU626: - - SUPPLIER0 - SKU627: - - SUPPLIER0 - SKU628: - - SUPPLIER0 - SKU629: - - SUPPLIER0 - SKU63: - - SUPPLIER0 - SKU630: - - SUPPLIER0 - SKU631: - - SUPPLIER0 - SKU632: - - SUPPLIER0 - SKU633: - - SUPPLIER0 - SKU634: - - SUPPLIER0 - SKU635: - - SUPPLIER0 - SKU636: - - SUPPLIER0 - SKU637: - - SUPPLIER0 - SKU638: - - SUPPLIER0 - SKU639: - - SUPPLIER0 - SKU64: - - SUPPLIER0 - SKU640: - - SUPPLIER0 - SKU641: - - SUPPLIER0 - SKU642: - - SUPPLIER0 - SKU643: - - SUPPLIER0 - SKU644: - - SUPPLIER0 - SKU645: - - SUPPLIER0 - SKU646: - - SUPPLIER0 - SKU647: - - SUPPLIER0 - SKU648: - - SUPPLIER0 - SKU649: - - SUPPLIER0 - SKU65: - - SUPPLIER0 - SKU650: - - SUPPLIER0 - SKU651: - - SUPPLIER0 - SKU652: - - SUPPLIER0 - SKU653: - - SUPPLIER0 - SKU654: - - SUPPLIER0 - SKU655: - - SUPPLIER0 - SKU656: - - SUPPLIER0 - SKU657: - - SUPPLIER0 - SKU658: - - SUPPLIER0 - SKU659: - - SUPPLIER0 - SKU66: - - SUPPLIER0 - SKU660: - - SUPPLIER0 - SKU661: - - SUPPLIER0 - SKU662: - - SUPPLIER0 - SKU663: - - SUPPLIER0 - SKU664: - - SUPPLIER0 - SKU665: - - SUPPLIER0 - SKU666: - - SUPPLIER0 - SKU667: - - SUPPLIER0 - SKU668: - - SUPPLIER0 - SKU669: - - SUPPLIER0 - SKU67: - - SUPPLIER0 - SKU670: - - SUPPLIER0 - SKU671: - - SUPPLIER0 - SKU672: - - SUPPLIER0 - SKU673: - - SUPPLIER0 - SKU674: - - SUPPLIER0 - SKU675: - - SUPPLIER0 - SKU676: - - SUPPLIER0 - SKU677: - - SUPPLIER0 - SKU678: - - SUPPLIER0 - SKU679: - - SUPPLIER0 - SKU68: - - SUPPLIER0 - SKU680: - - SUPPLIER0 - SKU681: - - SUPPLIER0 - SKU682: - - SUPPLIER0 - SKU683: - - SUPPLIER0 - SKU684: - - SUPPLIER0 - SKU685: - - SUPPLIER0 - SKU686: - - SUPPLIER0 - SKU687: - - SUPPLIER0 - SKU688: - - SUPPLIER0 - SKU689: - - SUPPLIER0 - SKU69: - - SUPPLIER0 - SKU690: - - SUPPLIER0 - SKU691: - - SUPPLIER0 - SKU692: - - SUPPLIER0 - SKU693: - - SUPPLIER0 - SKU694: - - SUPPLIER0 - SKU695: - - SUPPLIER0 - SKU696: - - SUPPLIER0 - SKU697: - - SUPPLIER0 - SKU698: - - SUPPLIER0 - SKU699: - - SUPPLIER0 - SKU7: - - SUPPLIER0 - SKU70: - - SUPPLIER0 - SKU700: - - SUPPLIER0 - SKU701: - - SUPPLIER0 - SKU702: - - SUPPLIER0 - SKU703: - - SUPPLIER0 - SKU704: - - SUPPLIER0 - SKU705: - - SUPPLIER0 - SKU706: - - SUPPLIER0 - SKU707: - - SUPPLIER0 - SKU708: - - SUPPLIER0 - SKU709: - - SUPPLIER0 - SKU71: - - SUPPLIER0 - SKU710: - - SUPPLIER0 - SKU711: - - SUPPLIER0 - SKU712: - - SUPPLIER0 - SKU713: - - SUPPLIER0 - SKU714: - - SUPPLIER0 - SKU715: - - SUPPLIER0 - SKU716: - - SUPPLIER0 - SKU717: - - SUPPLIER0 - SKU718: - - SUPPLIER0 - SKU719: - - SUPPLIER0 - SKU72: - - SUPPLIER0 - SKU720: - - SUPPLIER0 - SKU721: - - SUPPLIER0 - SKU722: - - SUPPLIER0 - SKU723: - - SUPPLIER0 - SKU724: - - SUPPLIER0 - SKU725: - - SUPPLIER0 - SKU726: - - SUPPLIER0 - SKU727: - - SUPPLIER0 - SKU728: - - SUPPLIER0 - SKU729: - - SUPPLIER0 - SKU73: - - SUPPLIER0 - SKU730: - - SUPPLIER0 - SKU731: - - SUPPLIER0 - SKU732: - - SUPPLIER0 - SKU733: - - SUPPLIER0 - SKU734: - - SUPPLIER0 - SKU735: - - SUPPLIER0 - SKU736: - - SUPPLIER0 - SKU737: - - SUPPLIER0 - SKU738: - - SUPPLIER0 - SKU739: - - SUPPLIER0 - SKU74: - - SUPPLIER0 - SKU740: - - SUPPLIER0 - SKU741: - - SUPPLIER0 - SKU742: - - SUPPLIER0 - SKU743: - - SUPPLIER0 - SKU744: - - SUPPLIER0 - SKU745: - - SUPPLIER0 - SKU746: - - SUPPLIER0 - SKU747: - - SUPPLIER0 - SKU748: - - SUPPLIER0 - SKU749: - - SUPPLIER0 - SKU75: - - SUPPLIER0 - SKU750: - - SUPPLIER0 - SKU751: - - SUPPLIER0 - SKU752: - - SUPPLIER0 - SKU753: - - SUPPLIER0 - SKU754: - - SUPPLIER0 - SKU755: - - SUPPLIER0 - SKU756: - - SUPPLIER0 - SKU757: - - SUPPLIER0 - SKU758: - - SUPPLIER0 - SKU759: - - SUPPLIER0 - SKU76: - - SUPPLIER0 - SKU760: - - SUPPLIER0 - SKU761: - - SUPPLIER0 - SKU762: - - SUPPLIER0 - SKU763: - - SUPPLIER0 - SKU764: - - SUPPLIER0 - SKU765: - - SUPPLIER0 - SKU766: - - SUPPLIER0 - SKU767: - - SUPPLIER0 - SKU768: - - SUPPLIER0 - SKU769: - - SUPPLIER0 - SKU77: - - SUPPLIER0 - SKU770: - - SUPPLIER0 - SKU771: - - SUPPLIER0 - SKU772: - - SUPPLIER0 - SKU773: - - SUPPLIER0 - SKU774: - - SUPPLIER0 - SKU775: - - SUPPLIER0 - SKU776: - - SUPPLIER0 - SKU777: - - SUPPLIER0 - SKU778: - - SUPPLIER0 - SKU779: - - SUPPLIER0 - SKU78: - - SUPPLIER0 - SKU780: - - SUPPLIER0 - SKU781: - - SUPPLIER0 - SKU782: - - SUPPLIER0 - SKU783: - - SUPPLIER0 - SKU784: - - SUPPLIER0 - SKU785: - - SUPPLIER0 - SKU786: - - SUPPLIER0 - SKU787: - - SUPPLIER0 - SKU788: - - SUPPLIER0 - SKU789: - - SUPPLIER0 - SKU79: - - SUPPLIER0 - SKU790: - - SUPPLIER0 - SKU791: - - SUPPLIER0 - SKU792: - - SUPPLIER0 - SKU793: - - SUPPLIER0 - SKU794: - - SUPPLIER0 - SKU795: - - SUPPLIER0 - SKU796: - - SUPPLIER0 - SKU797: - - SUPPLIER0 - SKU798: - - SUPPLIER0 - SKU799: - - SUPPLIER0 - SKU8: - - SUPPLIER0 - SKU80: - - SUPPLIER0 - SKU800: - - SUPPLIER0 - SKU801: - - SUPPLIER0 - SKU802: - - SUPPLIER0 - SKU803: - - SUPPLIER0 - SKU804: - - SUPPLIER0 - SKU805: - - SUPPLIER0 - SKU806: - - SUPPLIER0 - SKU807: - - SUPPLIER0 - SKU808: - - SUPPLIER0 - SKU809: - - SUPPLIER0 - SKU81: - - SUPPLIER0 - SKU810: - - SUPPLIER0 - SKU811: - - SUPPLIER0 - SKU812: - - SUPPLIER0 - SKU813: - - SUPPLIER0 - SKU814: - - SUPPLIER0 - SKU815: - - SUPPLIER0 - SKU816: - - SUPPLIER0 - SKU817: - - SUPPLIER0 - SKU818: - - SUPPLIER0 - SKU819: - - SUPPLIER0 - SKU82: - - SUPPLIER0 - SKU820: - - SUPPLIER0 - SKU821: - - SUPPLIER0 - SKU822: - - SUPPLIER0 - SKU823: - - SUPPLIER0 - SKU824: - - SUPPLIER0 - SKU825: - - SUPPLIER0 - SKU826: - - SUPPLIER0 - SKU827: - - SUPPLIER0 - SKU828: - - SUPPLIER0 - SKU829: - - SUPPLIER0 - SKU83: - - SUPPLIER0 - SKU830: - - SUPPLIER0 - SKU831: - - SUPPLIER0 - SKU832: - - SUPPLIER0 - SKU833: - - SUPPLIER0 - SKU834: - - SUPPLIER0 - SKU835: - - SUPPLIER0 - SKU836: - - SUPPLIER0 - SKU837: - - SUPPLIER0 - SKU838: - - SUPPLIER0 - SKU839: - - SUPPLIER0 - SKU84: - - SUPPLIER0 - SKU840: - - SUPPLIER0 - SKU841: - - SUPPLIER0 - SKU842: - - SUPPLIER0 - SKU843: - - SUPPLIER0 - SKU844: - - SUPPLIER0 - SKU845: - - SUPPLIER0 - SKU846: - - SUPPLIER0 - SKU847: - - SUPPLIER0 - SKU848: - - SUPPLIER0 - SKU849: - - SUPPLIER0 - SKU85: - - SUPPLIER0 - SKU850: - - SUPPLIER0 - SKU851: - - SUPPLIER0 - SKU852: - - SUPPLIER0 - SKU853: - - SUPPLIER0 - SKU854: - - SUPPLIER0 - SKU855: - - SUPPLIER0 - SKU856: - - SUPPLIER0 - SKU857: - - SUPPLIER0 - SKU858: - - SUPPLIER0 - SKU859: - - SUPPLIER0 - SKU86: - - SUPPLIER0 - SKU860: - - SUPPLIER0 - SKU861: - - SUPPLIER0 - SKU862: - - SUPPLIER0 - SKU863: - - SUPPLIER0 - SKU864: - - SUPPLIER0 - SKU865: - - SUPPLIER0 - SKU866: - - SUPPLIER0 - SKU867: - - SUPPLIER0 - SKU868: - - SUPPLIER0 - SKU869: - - SUPPLIER0 - SKU87: - - SUPPLIER0 - SKU870: - - SUPPLIER0 - SKU871: - - SUPPLIER0 - SKU872: - - SUPPLIER0 - SKU873: - - SUPPLIER0 - SKU874: - - SUPPLIER0 - SKU875: - - SUPPLIER0 - SKU876: - - SUPPLIER0 - SKU877: - - SUPPLIER0 - SKU878: - - SUPPLIER0 - SKU879: - - SUPPLIER0 - SKU88: - - SUPPLIER0 - SKU880: - - SUPPLIER0 - SKU881: - - SUPPLIER0 - SKU882: - - SUPPLIER0 - SKU883: - - SUPPLIER0 - SKU884: - - SUPPLIER0 - SKU885: - - SUPPLIER0 - SKU886: - - SUPPLIER0 - SKU887: - - SUPPLIER0 - SKU888: - - SUPPLIER0 - SKU889: - - SUPPLIER0 - SKU89: - - SUPPLIER0 - SKU890: - - SUPPLIER0 - SKU891: - - SUPPLIER0 - SKU892: - - SUPPLIER0 - SKU893: - - SUPPLIER0 - SKU894: - - SUPPLIER0 - SKU895: - - SUPPLIER0 - SKU896: - - SUPPLIER0 - SKU897: - - SUPPLIER0 - SKU898: - - SUPPLIER0 - SKU899: - - SUPPLIER0 - SKU9: - - SUPPLIER0 - SKU90: - - SUPPLIER0 - SKU900: - - SUPPLIER0 - SKU901: - - SUPPLIER0 - SKU902: - - SUPPLIER0 - SKU903: - - SUPPLIER0 - SKU904: - - SUPPLIER0 - SKU905: - - SUPPLIER0 - SKU906: - - SUPPLIER0 - SKU907: - - SUPPLIER0 - SKU908: - - SUPPLIER0 - SKU909: - - SUPPLIER0 - SKU91: - - SUPPLIER0 - SKU910: - - SUPPLIER0 - SKU911: - - SUPPLIER0 - SKU912: - - SUPPLIER0 - SKU913: - - SUPPLIER0 - SKU914: - - SUPPLIER0 - SKU915: - - SUPPLIER0 - SKU916: - - SUPPLIER0 - SKU917: - - SUPPLIER0 - SKU918: - - SUPPLIER0 - SKU919: - - SUPPLIER0 - SKU92: - - SUPPLIER0 - SKU920: - - SUPPLIER0 - SKU921: - - SUPPLIER0 - SKU922: - - SUPPLIER0 - SKU923: - - SUPPLIER0 - SKU924: - - SUPPLIER0 - SKU925: - - SUPPLIER0 - SKU926: - - SUPPLIER0 - SKU927: - - SUPPLIER0 - SKU928: - - SUPPLIER0 - SKU929: - - SUPPLIER0 - SKU93: - - SUPPLIER0 - SKU930: - - SUPPLIER0 - SKU931: - - SUPPLIER0 - SKU932: - - SUPPLIER0 - SKU933: - - SUPPLIER0 - SKU934: - - SUPPLIER0 - SKU935: - - SUPPLIER0 - SKU936: - - SUPPLIER0 - SKU937: - - SUPPLIER0 - SKU938: - - SUPPLIER0 - SKU939: - - SUPPLIER0 - SKU94: - - SUPPLIER0 - SKU940: - - SUPPLIER0 - SKU941: - - SUPPLIER0 - SKU942: - - SUPPLIER0 - SKU943: - - SUPPLIER0 - SKU944: - - SUPPLIER0 - SKU945: - - SUPPLIER0 - SKU946: - - SUPPLIER0 - SKU947: - - SUPPLIER0 - SKU948: - - SUPPLIER0 - SKU949: - - SUPPLIER0 - SKU95: - - SUPPLIER0 - SKU950: - - SUPPLIER0 - SKU951: - - SUPPLIER0 - SKU952: - - SUPPLIER0 - SKU953: - - SUPPLIER0 - SKU954: - - SUPPLIER0 - SKU955: - - SUPPLIER0 - SKU956: - - SUPPLIER0 - SKU957: - - SUPPLIER0 - SKU958: - - SUPPLIER0 - SKU959: - - SUPPLIER0 - SKU96: - - SUPPLIER0 - SKU960: - - SUPPLIER0 - SKU961: - - SUPPLIER0 - SKU962: - - SUPPLIER0 - SKU963: - - SUPPLIER0 - SKU964: - - SUPPLIER0 - SKU965: - - SUPPLIER0 - SKU966: - - SUPPLIER0 - SKU967: - - SUPPLIER0 - SKU968: - - SUPPLIER0 - SKU969: - - SUPPLIER0 - SKU97: - - SUPPLIER0 - SKU970: - - SUPPLIER0 - SKU971: - - SUPPLIER0 - SKU972: - - SUPPLIER0 - SKU973: - - SUPPLIER0 - SKU974: - - SUPPLIER0 - SKU975: - - SUPPLIER0 - SKU976: - - SUPPLIER0 - SKU977: - - SUPPLIER0 - SKU978: - - SUPPLIER0 - SKU979: - - SUPPLIER0 - SKU98: - - SUPPLIER0 - SKU980: - - SUPPLIER0 - SKU981: - - SUPPLIER0 - SKU982: - - SUPPLIER0 - SKU983: - - SUPPLIER0 - SKU984: - - SUPPLIER0 - SKU985: - - SUPPLIER0 - SKU986: - - SUPPLIER0 - SKU987: - - SUPPLIER0 - SKU988: - - SUPPLIER0 - SKU989: - - SUPPLIER0 - SKU99: - - SUPPLIER0 - SKU990: - - SUPPLIER0 - SKU991: - - SUPPLIER0 - SKU992: - - SUPPLIER0 - SKU993: - - SUPPLIER0 - SKU994: - - SUPPLIER0 - SKU995: - - SUPPLIER0 - SKU996: - - SUPPLIER0 - SKU997: - - SUPPLIER0 - SKU998: - - SUPPLIER0 - SKU999: - - SUPPLIER0 +facility_definitions: + RetailerFacility: + children: + products: + class: StoreProductUnit + config: + agent_type: 5 + consumer: + class: ConsumerUnit + seller: + class: SellerUnit + config: + sale_hist_len: 4 + is_template: true + storage: + class: StorageUnit + class: RetailerFacility + config: + agent_type: 2 + SupplierFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: 3 + consumer: + class: ConsumerUnit + manufacture: + class: ManufactureUnit + is_template: true + storage: + class: StorageUnit + class: SupplierFacility + config: + agent_type: 0 + WarehouseFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: 4 + consumer: + class: ConsumerUnit + is_template: true + storage: + class: StorageUnit + class: WarehouseFacility + config: + agent_type: 1 +normal_vehicle: &id001 + class: VehicleUnit + config: + patient: 100 + unit_transport_cost: 1 +settings: + constraint_state_hist_len: 4 + constraint_violate_reward: -1000000.0 + consumption_hist_len: 4 + downsampling_rate: 1 + episod_duration: 21 + gamma: 0.99 + global_reward_weight_consumer: 0.5 + global_reward_weight_producer: 0.5 + heading_timesteps: 7 + initial_balance: 100000 + pending_order_len: 4 + replenishment_discount: 0.9 + reward_normalization: 10000000.0 + sale_hist_len: 4 + tail_timesteps: 7 + total_echelons: 3 +world: + facilities: + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5316100 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 200 + definition_ref: SupplierFacility + name: SUPPLIER0 + skus: + SKU0: + cost: 138 + init_stock: 4450 + price: 154 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU1: + cost: 109 + init_stock: 4300 + price: 122 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU10: + cost: 142 + init_stock: 4600 + price: 158 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU100: + cost: 403 + init_stock: 1850 + price: 448 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU101: + cost: 388 + init_stock: 3100 + price: 432 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU102: + cost: 29 + init_stock: 4150 + price: 33 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU103: + cost: 152 + init_stock: 3950 + price: 169 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU104: + cost: 227 + init_stock: 1450 + price: 253 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU105: + cost: 274 + init_stock: 2950 + price: 305 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU106: + cost: 154 + init_stock: 1450 + price: 172 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU107: + cost: 290 + init_stock: 4050 + price: 323 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU108: + cost: 254 + init_stock: 4600 + price: 283 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU109: + cost: 273 + init_stock: 3850 + price: 304 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU11: + cost: 183 + init_stock: 2900 + price: 204 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU110: + cost: 336 + init_stock: 4650 + price: 374 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU111: + cost: 193 + init_stock: 3900 + price: 215 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU112: + cost: 375 + init_stock: 400 + price: 417 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU113: + cost: 57 + init_stock: 3450 + price: 64 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU114: + cost: 56 + init_stock: 2700 + price: 63 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU115: + cost: 419 + init_stock: 4350 + price: 466 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU116: + cost: 103 + init_stock: 4600 + price: 115 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU117: + cost: 68 + init_stock: 3350 + price: 76 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU118: + cost: 262 + init_stock: 3450 + price: 292 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU119: + cost: 299 + init_stock: 1950 + price: 333 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU12: + cost: 386 + init_stock: 3700 + price: 429 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU120: + cost: 132 + init_stock: 2400 + price: 147 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU121: + cost: 152 + init_stock: 2950 + price: 169 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU122: + cost: 63 + init_stock: 3600 + price: 71 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU123: + cost: 29 + init_stock: 2950 + price: 33 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU124: + cost: 430 + init_stock: 2900 + price: 478 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU125: + cost: 279 + init_stock: 1800 + price: 311 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU126: + cost: 447 + init_stock: 3800 + price: 497 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU127: + cost: 56 + init_stock: 4600 + price: 63 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU128: + cost: 180 + init_stock: 2100 + price: 200 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU129: + cost: 418 + init_stock: 1450 + price: 465 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU13: + cost: 412 + init_stock: 1450 + price: 458 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU130: + cost: 201 + init_stock: 2700 + price: 224 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU131: + cost: 441 + init_stock: 3000 + price: 491 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU132: + cost: 83 + init_stock: 3050 + price: 93 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU133: + cost: 242 + init_stock: 650 + price: 269 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU134: + cost: 327 + init_stock: 1000 + price: 364 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU135: + cost: 315 + init_stock: 4900 + price: 351 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU136: + cost: 386 + init_stock: 2000 + price: 429 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU137: + cost: 173 + init_stock: 4600 + price: 193 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU138: + cost: 101 + init_stock: 1400 + price: 113 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU139: + cost: 9 + init_stock: 4750 + price: 10 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU14: + cost: 49 + init_stock: 650 + price: 55 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU140: + cost: 315 + init_stock: 4800 + price: 350 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU141: + cost: 63 + init_stock: 3550 + price: 70 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU142: + cost: 446 + init_stock: 3650 + price: 496 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU143: + cost: 29 + init_stock: 4150 + price: 33 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU144: + cost: 247 + init_stock: 3750 + price: 275 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU145: + cost: 45 + init_stock: 4850 + price: 51 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU146: + cost: 126 + init_stock: 1800 + price: 140 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU147: + cost: 344 + init_stock: 2450 + price: 383 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU148: + cost: 250 + init_stock: 3400 + price: 278 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU149: + cost: 361 + init_stock: 2600 + price: 402 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU15: + cost: 378 + init_stock: 3550 + price: 421 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU150: + cost: 72 + init_stock: 4150 + price: 81 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU151: + cost: 366 + init_stock: 3250 + price: 407 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU152: + cost: 294 + init_stock: 3200 + price: 327 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU153: + cost: 183 + init_stock: 4850 + price: 204 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU154: + cost: 79 + init_stock: 1300 + price: 88 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU155: + cost: 385 + init_stock: 1300 + price: 428 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU156: + cost: 437 + init_stock: 400 + price: 486 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU157: + cost: 31 + init_stock: 3300 + price: 35 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU158: + cost: 167 + init_stock: 2850 + price: 186 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU159: + cost: 181 + init_stock: 4600 + price: 202 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU16: + cost: 188 + init_stock: 1200 + price: 209 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU160: + cost: 389 + init_stock: 2350 + price: 433 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU161: + cost: 225 + init_stock: 2200 + price: 250 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU162: + cost: 284 + init_stock: 4000 + price: 316 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU163: + cost: 102 + init_stock: 700 + price: 114 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU164: + cost: 273 + init_stock: 3300 + price: 304 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU165: + cost: 175 + init_stock: 2650 + price: 195 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU166: + cost: 283 + init_stock: 2550 + price: 315 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU167: + cost: 423 + init_stock: 4700 + price: 470 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU168: + cost: 428 + init_stock: 950 + price: 476 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU169: + cost: 120 + init_stock: 950 + price: 134 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU17: + cost: 360 + init_stock: 1900 + price: 401 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU170: + cost: 260 + init_stock: 1900 + price: 289 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU171: + cost: 432 + init_stock: 4100 + price: 481 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU172: + cost: 390 + init_stock: 1550 + price: 434 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU173: + cost: 262 + init_stock: 4700 + price: 292 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU174: + cost: 426 + init_stock: 700 + price: 474 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU175: + cost: 428 + init_stock: 1050 + price: 476 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU176: + cost: 269 + init_stock: 4000 + price: 299 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU177: + cost: 382 + init_stock: 1050 + price: 425 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU178: + cost: 377 + init_stock: 3500 + price: 419 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU179: + cost: 212 + init_stock: 3250 + price: 236 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU18: + cost: 414 + init_stock: 4050 + price: 460 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU180: + cost: 89 + init_stock: 800 + price: 99 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU181: + cost: 109 + init_stock: 1350 + price: 122 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU182: + cost: 398 + init_stock: 1150 + price: 443 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU183: + cost: 64 + init_stock: 2650 + price: 72 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU184: + cost: 172 + init_stock: 3850 + price: 192 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU185: + cost: 117 + init_stock: 1400 + price: 130 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU186: + cost: 44 + init_stock: 1950 + price: 49 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU187: + cost: 442 + init_stock: 2350 + price: 492 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU188: + cost: 45 + init_stock: 4500 + price: 50 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU189: + cost: 130 + init_stock: 1650 + price: 145 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU19: + cost: 405 + init_stock: 300 + price: 451 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU190: + cost: 64 + init_stock: 3950 + price: 72 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU191: + cost: 256 + init_stock: 2000 + price: 285 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU192: + cost: 164 + init_stock: 4750 + price: 183 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU193: + cost: 310 + init_stock: 3250 + price: 345 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU194: + cost: 395 + init_stock: 3700 + price: 439 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU195: + cost: 184 + init_stock: 1600 + price: 205 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU196: + cost: 187 + init_stock: 1100 + price: 208 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU197: + cost: 428 + init_stock: 250 + price: 476 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU198: + cost: 242 + init_stock: 3850 + price: 269 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU199: + cost: 154 + init_stock: 2750 + price: 172 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU2: + cost: 183 + init_stock: 1250 + price: 204 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU20: + cost: 393 + init_stock: 2050 + price: 437 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU200: + cost: 45 + init_stock: 4700 + price: 51 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU201: + cost: 438 + init_stock: 3700 + price: 487 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU202: + cost: 365 + init_stock: 700 + price: 406 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU203: + cost: 299 + init_stock: 1450 + price: 333 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU204: + cost: 57 + init_stock: 3900 + price: 64 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU205: + cost: 403 + init_stock: 4550 + price: 448 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU206: + cost: 58 + init_stock: 4850 + price: 65 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU207: + cost: 393 + init_stock: 2900 + price: 437 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU208: + cost: 34 + init_stock: 1250 + price: 38 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU209: + cost: 294 + init_stock: 3250 + price: 327 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU21: + cost: 15 + init_stock: 2750 + price: 17 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU210: + cost: 409 + init_stock: 2500 + price: 455 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU211: + cost: 153 + init_stock: 750 + price: 170 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU212: + cost: 55 + init_stock: 3450 + price: 62 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU213: + cost: 236 + init_stock: 3500 + price: 263 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU214: + cost: 390 + init_stock: 350 + price: 434 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU215: + cost: 254 + init_stock: 1200 + price: 283 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU216: + cost: 351 + init_stock: 3900 + price: 391 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU217: + cost: 151 + init_stock: 4900 + price: 168 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU218: + cost: 436 + init_stock: 900 + price: 485 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU219: + cost: 180 + init_stock: 4300 + price: 201 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU22: + cost: 19 + init_stock: 1500 + price: 22 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU220: + cost: 354 + init_stock: 650 + price: 394 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU221: + cost: 97 + init_stock: 4600 + price: 108 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU222: + cost: 413 + init_stock: 2800 + price: 459 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU223: + cost: 378 + init_stock: 3650 + price: 420 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU224: + cost: 275 + init_stock: 3500 + price: 306 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU225: + cost: 106 + init_stock: 4600 + price: 118 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU226: + cost: 382 + init_stock: 2200 + price: 425 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU227: + cost: 171 + init_stock: 3250 + price: 191 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU228: + cost: 144 + init_stock: 1600 + price: 160 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU229: + cost: 250 + init_stock: 3650 + price: 278 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU23: + cost: 111 + init_stock: 700 + price: 124 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU230: + cost: 147 + init_stock: 950 + price: 164 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU231: + cost: 288 + init_stock: 4700 + price: 321 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU232: + cost: 391 + init_stock: 2050 + price: 435 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU233: + cost: 197 + init_stock: 1850 + price: 219 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU234: + cost: 129 + init_stock: 850 + price: 144 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU235: + cost: 356 + init_stock: 1100 + price: 396 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU236: + cost: 344 + init_stock: 4950 + price: 383 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU237: + cost: 97 + init_stock: 2550 + price: 108 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU238: + cost: 44 + init_stock: 2850 + price: 49 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU239: + cost: 199 + init_stock: 4500 + price: 222 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU24: + cost: 331 + init_stock: 2350 + price: 368 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU240: + cost: 348 + init_stock: 4150 + price: 387 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU241: + cost: 271 + init_stock: 4450 + price: 302 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU242: + cost: 55 + init_stock: 3950 + price: 62 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU243: + cost: 10 + init_stock: 2500 + price: 12 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU244: + cost: 266 + init_stock: 3250 + price: 296 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU245: + cost: 35 + init_stock: 1950 + price: 39 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU246: + cost: 236 + init_stock: 2250 + price: 263 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU247: + cost: 201 + init_stock: 1300 + price: 224 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU248: + cost: 369 + init_stock: 4200 + price: 410 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU249: + cost: 57 + init_stock: 4850 + price: 64 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU25: + cost: 180 + init_stock: 2300 + price: 201 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU250: + cost: 233 + init_stock: 2250 + price: 259 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU251: + cost: 162 + init_stock: 1150 + price: 181 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU252: + cost: 297 + init_stock: 3250 + price: 330 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU253: + cost: 283 + init_stock: 4600 + price: 315 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU254: + cost: 366 + init_stock: 2450 + price: 407 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU255: + cost: 324 + init_stock: 3700 + price: 361 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU256: + cost: 60 + init_stock: 2350 + price: 67 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU257: + cost: 45 + init_stock: 3000 + price: 50 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU258: + cost: 418 + init_stock: 400 + price: 465 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU259: + cost: 311 + init_stock: 2000 + price: 346 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU26: + cost: 377 + init_stock: 3250 + price: 419 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU260: + cost: 336 + init_stock: 2100 + price: 374 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU261: + cost: 66 + init_stock: 1350 + price: 74 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU262: + cost: 410 + init_stock: 3450 + price: 456 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU263: + cost: 248 + init_stock: 2700 + price: 276 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU264: + cost: 80 + init_stock: 3000 + price: 89 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU265: + cost: 133 + init_stock: 3950 + price: 148 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU266: + cost: 258 + init_stock: 650 + price: 287 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU267: + cost: 27 + init_stock: 4800 + price: 31 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU268: + cost: 306 + init_stock: 1100 + price: 340 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU269: + cost: 431 + init_stock: 2600 + price: 479 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU27: + cost: 242 + init_stock: 4450 + price: 269 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU270: + cost: 209 + init_stock: 2000 + price: 233 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU271: + cost: 81 + init_stock: 600 + price: 90 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU272: + cost: 439 + init_stock: 1500 + price: 488 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU273: + cost: 449 + init_stock: 1000 + price: 499 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU274: + cost: 390 + init_stock: 4650 + price: 434 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU275: + cost: 411 + init_stock: 4800 + price: 457 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU276: + cost: 161 + init_stock: 1650 + price: 179 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU277: + cost: 272 + init_stock: 3100 + price: 303 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU278: + cost: 270 + init_stock: 2400 + price: 301 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU279: + cost: 15 + init_stock: 4100 + price: 17 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU28: + cost: 359 + init_stock: 300 + price: 399 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU280: + cost: 214 + init_stock: 4150 + price: 238 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU281: + cost: 11 + init_stock: 3450 + price: 13 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU282: + cost: 363 + init_stock: 4200 + price: 404 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU283: + cost: 298 + init_stock: 1600 + price: 332 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU284: + cost: 231 + init_stock: 1950 + price: 257 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU285: + cost: 47 + init_stock: 300 + price: 53 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU286: + cost: 226 + init_stock: 650 + price: 252 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU287: + cost: 144 + init_stock: 4700 + price: 161 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU288: + cost: 429 + init_stock: 800 + price: 477 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU289: + cost: 406 + init_stock: 4750 + price: 452 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU29: + cost: 144 + init_stock: 3700 + price: 160 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU290: + cost: 443 + init_stock: 1400 + price: 493 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU291: + cost: 162 + init_stock: 950 + price: 180 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU292: + cost: 209 + init_stock: 4750 + price: 233 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU293: + cost: 159 + init_stock: 3300 + price: 177 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU294: + cost: 320 + init_stock: 1350 + price: 356 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU295: + cost: 378 + init_stock: 4100 + price: 421 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU296: + cost: 372 + init_stock: 2350 + price: 414 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU297: + cost: 293 + init_stock: 4500 + price: 326 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU298: + cost: 109 + init_stock: 2300 + price: 122 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU299: + cost: 134 + init_stock: 3400 + price: 149 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU3: + cost: 144 + init_stock: 250 + price: 161 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU30: + cost: 355 + init_stock: 3050 + price: 395 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU300: + cost: 225 + init_stock: 1950 + price: 250 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU301: + cost: 251 + init_stock: 250 + price: 279 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU302: + cost: 288 + init_stock: 3700 + price: 321 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU303: + cost: 181 + init_stock: 2750 + price: 202 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU304: + cost: 353 + init_stock: 3400 + price: 393 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU305: + cost: 101 + init_stock: 3350 + price: 113 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU306: + cost: 387 + init_stock: 2150 + price: 430 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU307: + cost: 279 + init_stock: 4050 + price: 311 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU308: + cost: 380 + init_stock: 2900 + price: 423 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU309: + cost: 403 + init_stock: 3950 + price: 448 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU31: + cost: 410 + init_stock: 3800 + price: 456 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU310: + cost: 220 + init_stock: 3150 + price: 245 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU311: + cost: 406 + init_stock: 3500 + price: 452 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU312: + cost: 100 + init_stock: 3650 + price: 112 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU313: + cost: 276 + init_stock: 500 + price: 307 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU314: + cost: 36 + init_stock: 3800 + price: 40 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU315: + cost: 78 + init_stock: 1100 + price: 87 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU316: + cost: 330 + init_stock: 800 + price: 367 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU317: + cost: 55 + init_stock: 650 + price: 62 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU318: + cost: 137 + init_stock: 2100 + price: 153 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU319: + cost: 306 + init_stock: 2300 + price: 340 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU32: + cost: 200 + init_stock: 4450 + price: 223 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU320: + cost: 169 + init_stock: 4200 + price: 188 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU321: + cost: 378 + init_stock: 2350 + price: 421 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU322: + cost: 415 + init_stock: 3800 + price: 462 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU323: + cost: 104 + init_stock: 3950 + price: 116 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU324: + cost: 356 + init_stock: 1000 + price: 396 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU325: + cost: 366 + init_stock: 3000 + price: 407 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU326: + cost: 360 + init_stock: 600 + price: 400 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU327: + cost: 390 + init_stock: 5000 + price: 434 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU328: + cost: 161 + init_stock: 1050 + price: 179 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU329: + cost: 407 + init_stock: 4050 + price: 453 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU33: + cost: 101 + init_stock: 4450 + price: 113 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU330: + cost: 193 + init_stock: 4600 + price: 215 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU331: + cost: 25 + init_stock: 4200 + price: 28 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU332: + cost: 154 + init_stock: 2750 + price: 172 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU333: + cost: 56 + init_stock: 3350 + price: 63 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU334: + cost: 227 + init_stock: 1950 + price: 253 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU335: + cost: 19 + init_stock: 450 + price: 22 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU336: + cost: 236 + init_stock: 1450 + price: 263 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU337: + cost: 249 + init_stock: 3700 + price: 277 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU338: + cost: 162 + init_stock: 1700 + price: 181 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU339: + cost: 439 + init_stock: 3800 + price: 488 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU34: + cost: 203 + init_stock: 2200 + price: 226 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU340: + cost: 191 + init_stock: 1350 + price: 213 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU341: + cost: 380 + init_stock: 4450 + price: 423 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU342: + cost: 372 + init_stock: 2450 + price: 414 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU343: + cost: 386 + init_stock: 2600 + price: 429 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU344: + cost: 176 + init_stock: 4450 + price: 196 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU345: + cost: 405 + init_stock: 4600 + price: 451 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU346: + cost: 436 + init_stock: 1500 + price: 485 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU347: + cost: 27 + init_stock: 3400 + price: 30 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU348: + cost: 10 + init_stock: 600 + price: 12 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU349: + cost: 151 + init_stock: 2200 + price: 168 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU35: + cost: 162 + init_stock: 2250 + price: 181 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU350: + cost: 342 + init_stock: 1450 + price: 381 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU351: + cost: 299 + init_stock: 4800 + price: 333 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU352: + cost: 328 + init_stock: 950 + price: 365 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU353: + cost: 119 + init_stock: 3250 + price: 133 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU354: + cost: 320 + init_stock: 3100 + price: 356 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU355: + cost: 407 + init_stock: 2800 + price: 453 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU356: + cost: 302 + init_stock: 3450 + price: 336 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU357: + cost: 69 + init_stock: 1650 + price: 77 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU358: + cost: 306 + init_stock: 3350 + price: 340 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU359: + cost: 233 + init_stock: 3200 + price: 259 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU36: + cost: 200 + init_stock: 900 + price: 223 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU360: + cost: 131 + init_stock: 2700 + price: 146 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU361: + cost: 42 + init_stock: 4450 + price: 47 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU362: + cost: 386 + init_stock: 4850 + price: 429 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU363: + cost: 445 + init_stock: 4000 + price: 495 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU364: + cost: 191 + init_stock: 4500 + price: 213 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU365: + cost: 104 + init_stock: 600 + price: 116 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU366: + cost: 309 + init_stock: 4100 + price: 344 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU367: + cost: 177 + init_stock: 750 + price: 197 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU368: + cost: 369 + init_stock: 3000 + price: 410 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU369: + cost: 168 + init_stock: 4500 + price: 187 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU37: + cost: 271 + init_stock: 2250 + price: 302 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU370: + cost: 189 + init_stock: 4200 + price: 210 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU371: + cost: 363 + init_stock: 3450 + price: 404 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU372: + cost: 56 + init_stock: 3950 + price: 63 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU373: + cost: 95 + init_stock: 1200 + price: 106 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU374: + cost: 275 + init_stock: 1800 + price: 306 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU375: + cost: 137 + init_stock: 2550 + price: 153 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU376: + cost: 245 + init_stock: 750 + price: 273 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU377: + cost: 275 + init_stock: 3750 + price: 306 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU378: + cost: 209 + init_stock: 2200 + price: 233 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU379: + cost: 9 + init_stock: 1100 + price: 10 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU38: + cost: 319 + init_stock: 3150 + price: 355 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU380: + cost: 396 + init_stock: 1150 + price: 440 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU381: + cost: 361 + init_stock: 4000 + price: 402 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU382: + cost: 430 + init_stock: 5000 + price: 478 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU383: + cost: 223 + init_stock: 3500 + price: 248 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU384: + cost: 304 + init_stock: 1600 + price: 338 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU385: + cost: 269 + init_stock: 3050 + price: 299 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU386: + cost: 252 + init_stock: 4600 + price: 281 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU387: + cost: 141 + init_stock: 4700 + price: 157 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU388: + cost: 192 + init_stock: 2700 + price: 214 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU389: + cost: 116 + init_stock: 1200 + price: 129 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU39: + cost: 23 + init_stock: 350 + price: 26 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU390: + cost: 339 + init_stock: 4750 + price: 377 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU391: + cost: 162 + init_stock: 3750 + price: 180 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU392: + cost: 95 + init_stock: 3000 + price: 106 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU393: + cost: 115 + init_stock: 2600 + price: 128 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU394: + cost: 372 + init_stock: 3000 + price: 414 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU395: + cost: 374 + init_stock: 3450 + price: 416 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU396: + cost: 383 + init_stock: 2200 + price: 426 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU397: + cost: 243 + init_stock: 750 + price: 271 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU398: + cost: 18 + init_stock: 2950 + price: 21 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU399: + cost: 242 + init_stock: 2450 + price: 269 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU4: + cost: 171 + init_stock: 4250 + price: 190 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU40: + cost: 416 + init_stock: 4400 + price: 463 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU400: + cost: 40 + init_stock: 2650 + price: 45 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU401: + cost: 134 + init_stock: 4100 + price: 149 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU402: + cost: 356 + init_stock: 3950 + price: 396 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU403: + cost: 63 + init_stock: 2900 + price: 70 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU404: + cost: 202 + init_stock: 3950 + price: 225 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU405: + cost: 27 + init_stock: 1100 + price: 31 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU406: + cost: 404 + init_stock: 2150 + price: 449 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU407: + cost: 415 + init_stock: 800 + price: 462 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU408: + cost: 327 + init_stock: 1350 + price: 364 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU409: + cost: 381 + init_stock: 2900 + price: 424 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU41: + cost: 50 + init_stock: 1150 + price: 56 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU410: + cost: 183 + init_stock: 450 + price: 204 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU411: + cost: 177 + init_stock: 300 + price: 197 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU412: + cost: 126 + init_stock: 650 + price: 140 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU413: + cost: 9 + init_stock: 3550 + price: 11 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU414: + cost: 442 + init_stock: 1000 + price: 492 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU415: + cost: 23 + init_stock: 3150 + price: 26 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU416: + cost: 135 + init_stock: 250 + price: 151 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU417: + cost: 275 + init_stock: 2850 + price: 306 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU418: + cost: 138 + init_stock: 2100 + price: 154 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU419: + cost: 172 + init_stock: 1200 + price: 192 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU42: + cost: 95 + init_stock: 2050 + price: 106 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU420: + cost: 71 + init_stock: 3650 + price: 79 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU421: + cost: 259 + init_stock: 3200 + price: 288 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU422: + cost: 72 + init_stock: 3550 + price: 81 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU423: + cost: 43 + init_stock: 3950 + price: 48 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU424: + cost: 309 + init_stock: 1600 + price: 344 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU425: + cost: 139 + init_stock: 1300 + price: 155 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU426: + cost: 103 + init_stock: 4100 + price: 115 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU427: + cost: 118 + init_stock: 250 + price: 132 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU428: + cost: 449 + init_stock: 2900 + price: 499 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU429: + cost: 132 + init_stock: 800 + price: 147 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU43: + cost: 145 + init_stock: 4050 + price: 162 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU430: + cost: 297 + init_stock: 1450 + price: 330 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU431: + cost: 58 + init_stock: 3450 + price: 65 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU432: + cost: 175 + init_stock: 4650 + price: 195 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU433: + cost: 49 + init_stock: 4300 + price: 55 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU434: + cost: 101 + init_stock: 3750 + price: 113 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU435: + cost: 230 + init_stock: 750 + price: 256 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU436: + cost: 54 + init_stock: 4900 + price: 61 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU437: + cost: 330 + init_stock: 2100 + price: 367 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU438: + cost: 356 + init_stock: 3600 + price: 396 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU439: + cost: 89 + init_stock: 3100 + price: 99 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU44: + cost: 216 + init_stock: 2950 + price: 241 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU440: + cost: 409 + init_stock: 2550 + price: 455 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU441: + cost: 195 + init_stock: 3250 + price: 217 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU442: + cost: 414 + init_stock: 2900 + price: 460 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU443: + cost: 337 + init_stock: 2400 + price: 375 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU444: + cost: 39 + init_stock: 4900 + price: 44 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU445: + cost: 53 + init_stock: 3400 + price: 59 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU446: + cost: 164 + init_stock: 1000 + price: 183 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU447: + cost: 395 + init_stock: 2400 + price: 439 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU448: + cost: 449 + init_stock: 1400 + price: 499 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU449: + cost: 299 + init_stock: 3700 + price: 333 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU45: + cost: 120 + init_stock: 2400 + price: 134 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU450: + cost: 266 + init_stock: 1550 + price: 296 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU451: + cost: 290 + init_stock: 850 + price: 323 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU452: + cost: 46 + init_stock: 2000 + price: 52 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU453: + cost: 18 + init_stock: 4050 + price: 21 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU454: + cost: 178 + init_stock: 1150 + price: 198 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU455: + cost: 9 + init_stock: 3200 + price: 11 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU456: + cost: 183 + init_stock: 4150 + price: 204 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU457: + cost: 174 + init_stock: 5000 + price: 194 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU458: + cost: 99 + init_stock: 550 + price: 110 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU459: + cost: 333 + init_stock: 3200 + price: 370 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU46: + cost: 81 + init_stock: 4950 + price: 90 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU460: + cost: 108 + init_stock: 4050 + price: 121 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU461: + cost: 274 + init_stock: 1100 + price: 305 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU462: + cost: 339 + init_stock: 3300 + price: 377 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU463: + cost: 404 + init_stock: 400 + price: 449 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU464: + cost: 419 + init_stock: 1700 + price: 466 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU465: + cost: 77 + init_stock: 3050 + price: 86 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU466: + cost: 370 + init_stock: 4700 + price: 412 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU467: + cost: 92 + init_stock: 3550 + price: 103 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU468: + cost: 407 + init_stock: 4400 + price: 453 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU469: + cost: 379 + init_stock: 4650 + price: 422 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU47: + cost: 45 + init_stock: 1850 + price: 51 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU470: + cost: 255 + init_stock: 1400 + price: 284 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU471: + cost: 438 + init_stock: 1150 + price: 487 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU472: + cost: 418 + init_stock: 4800 + price: 465 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU473: + cost: 243 + init_stock: 1300 + price: 271 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU474: + cost: 61 + init_stock: 1250 + price: 68 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU475: + cost: 288 + init_stock: 3600 + price: 320 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU476: + cost: 213 + init_stock: 2450 + price: 237 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU477: + cost: 409 + init_stock: 3450 + price: 455 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU478: + cost: 36 + init_stock: 3700 + price: 40 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU479: + cost: 265 + init_stock: 2150 + price: 295 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU48: + cost: 355 + init_stock: 4550 + price: 395 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU480: + cost: 84 + init_stock: 3250 + price: 94 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU481: + cost: 367 + init_stock: 1050 + price: 408 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU482: + cost: 178 + init_stock: 4350 + price: 198 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU483: + cost: 76 + init_stock: 4100 + price: 85 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU484: + cost: 414 + init_stock: 2100 + price: 460 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU485: + cost: 277 + init_stock: 1750 + price: 308 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU486: + cost: 75 + init_stock: 3050 + price: 84 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU487: + cost: 378 + init_stock: 3100 + price: 421 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU488: + cost: 97 + init_stock: 1450 + price: 108 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU489: + cost: 131 + init_stock: 1500 + price: 146 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU49: + cost: 288 + init_stock: 2200 + price: 320 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU490: + cost: 38 + init_stock: 950 + price: 43 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU491: + cost: 120 + init_stock: 4450 + price: 134 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU492: + cost: 333 + init_stock: 4300 + price: 370 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU493: + cost: 309 + init_stock: 1000 + price: 344 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU494: + cost: 78 + init_stock: 2450 + price: 87 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU495: + cost: 164 + init_stock: 4250 + price: 183 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU496: + cost: 199 + init_stock: 3200 + price: 222 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU497: + cost: 407 + init_stock: 1800 + price: 453 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU498: + cost: 427 + init_stock: 1250 + price: 475 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU499: + cost: 109 + init_stock: 1650 + price: 122 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU5: + cost: 108 + init_stock: 3800 + price: 121 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU50: + cost: 268 + init_stock: 2850 + price: 298 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU500: + cost: 114 + init_stock: 4350 + price: 127 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU501: + cost: 295 + init_stock: 4150 + price: 328 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU502: + cost: 301 + init_stock: 1600 + price: 335 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU503: + cost: 77 + init_stock: 300 + price: 86 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU504: + cost: 12 + init_stock: 3250 + price: 14 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU505: + cost: 66 + init_stock: 3950 + price: 74 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU506: + cost: 45 + init_stock: 2500 + price: 51 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU507: + cost: 33 + init_stock: 850 + price: 37 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU508: + cost: 400 + init_stock: 4750 + price: 445 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU509: + cost: 282 + init_stock: 700 + price: 314 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU51: + cost: 296 + init_stock: 1650 + price: 329 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU510: + cost: 207 + init_stock: 3100 + price: 230 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU511: + cost: 18 + init_stock: 1700 + price: 21 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU512: + cost: 381 + init_stock: 1650 + price: 424 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU513: + cost: 24 + init_stock: 300 + price: 27 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU514: + cost: 70 + init_stock: 2550 + price: 78 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU515: + cost: 441 + init_stock: 4600 + price: 490 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU516: + cost: 92 + init_stock: 750 + price: 103 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU517: + cost: 178 + init_stock: 2050 + price: 198 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU518: + cost: 442 + init_stock: 750 + price: 492 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU519: + cost: 441 + init_stock: 1950 + price: 490 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU52: + cost: 164 + init_stock: 3150 + price: 183 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU520: + cost: 360 + init_stock: 4300 + price: 400 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU521: + cost: 190 + init_stock: 3100 + price: 212 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU522: + cost: 295 + init_stock: 1950 + price: 328 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU523: + cost: 20 + init_stock: 2500 + price: 23 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU524: + cost: 82 + init_stock: 3750 + price: 92 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU525: + cost: 234 + init_stock: 4200 + price: 261 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU526: + cost: 113 + init_stock: 1850 + price: 126 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU527: + cost: 37 + init_stock: 700 + price: 42 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU528: + cost: 19 + init_stock: 4500 + price: 22 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU529: + cost: 243 + init_stock: 4050 + price: 271 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU53: + cost: 394 + init_stock: 3850 + price: 438 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU530: + cost: 57 + init_stock: 3250 + price: 64 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU531: + cost: 245 + init_stock: 3050 + price: 273 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU532: + cost: 392 + init_stock: 3900 + price: 436 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU533: + cost: 364 + init_stock: 1050 + price: 405 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU534: + cost: 337 + init_stock: 4150 + price: 375 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU535: + cost: 380 + init_stock: 3900 + price: 423 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU536: + cost: 266 + init_stock: 2900 + price: 296 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU537: + cost: 428 + init_stock: 1450 + price: 476 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU538: + cost: 405 + init_stock: 1150 + price: 451 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU539: + cost: 334 + init_stock: 2200 + price: 372 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU54: + cost: 126 + init_stock: 2950 + price: 141 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU540: + cost: 22 + init_stock: 3050 + price: 25 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU541: + cost: 207 + init_stock: 2450 + price: 230 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU542: + cost: 282 + init_stock: 2350 + price: 314 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU543: + cost: 378 + init_stock: 4650 + price: 421 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU544: + cost: 220 + init_stock: 700 + price: 245 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU545: + cost: 291 + init_stock: 1500 + price: 324 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU546: + cost: 219 + init_stock: 500 + price: 244 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU547: + cost: 201 + init_stock: 900 + price: 224 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU548: + cost: 162 + init_stock: 1450 + price: 181 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU549: + cost: 342 + init_stock: 4200 + price: 380 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU55: + cost: 272 + init_stock: 4450 + price: 303 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU550: + cost: 306 + init_stock: 1100 + price: 340 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU551: + cost: 409 + init_stock: 2400 + price: 455 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU552: + cost: 217 + init_stock: 3750 + price: 242 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU553: + cost: 198 + init_stock: 2300 + price: 221 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU554: + cost: 43 + init_stock: 3250 + price: 48 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU555: + cost: 187 + init_stock: 2650 + price: 208 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU556: + cost: 446 + init_stock: 2300 + price: 496 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU557: + cost: 247 + init_stock: 4750 + price: 275 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU558: + cost: 307 + init_stock: 3650 + price: 342 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU559: + cost: 193 + init_stock: 4800 + price: 215 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU56: + cost: 401 + init_stock: 5000 + price: 446 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU560: + cost: 326 + init_stock: 300 + price: 363 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU561: + cost: 142 + init_stock: 1850 + price: 158 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU562: + cost: 234 + init_stock: 3300 + price: 260 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU563: + cost: 418 + init_stock: 1050 + price: 465 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU564: + cost: 320 + init_stock: 550 + price: 356 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU565: + cost: 431 + init_stock: 2400 + price: 479 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU566: + cost: 396 + init_stock: 3450 + price: 441 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU567: + cost: 79 + init_stock: 2900 + price: 88 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU568: + cost: 189 + init_stock: 1350 + price: 210 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU569: + cost: 388 + init_stock: 2100 + price: 432 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU57: + cost: 235 + init_stock: 3500 + price: 262 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU570: + cost: 128 + init_stock: 500 + price: 143 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU571: + cost: 183 + init_stock: 3200 + price: 204 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU572: + cost: 261 + init_stock: 2850 + price: 291 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU573: + cost: 412 + init_stock: 1250 + price: 458 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU574: + cost: 218 + init_stock: 4700 + price: 243 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU575: + cost: 295 + init_stock: 3600 + price: 328 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU576: + cost: 299 + init_stock: 3500 + price: 333 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU577: + cost: 331 + init_stock: 750 + price: 368 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU578: + cost: 73 + init_stock: 3000 + price: 82 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU579: + cost: 334 + init_stock: 950 + price: 372 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU58: + cost: 368 + init_stock: 3350 + price: 409 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU580: + cost: 378 + init_stock: 2700 + price: 421 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU581: + cost: 261 + init_stock: 2850 + price: 290 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU582: + cost: 117 + init_stock: 250 + price: 130 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU583: + cost: 154 + init_stock: 1400 + price: 172 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU584: + cost: 155 + init_stock: 4250 + price: 173 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU585: + cost: 180 + init_stock: 3950 + price: 201 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU586: + cost: 135 + init_stock: 2600 + price: 150 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU587: + cost: 178 + init_stock: 1800 + price: 198 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU588: + cost: 225 + init_stock: 2950 + price: 250 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU589: + cost: 45 + init_stock: 4550 + price: 51 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU59: + cost: 9 + init_stock: 600 + price: 11 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU590: + cost: 442 + init_stock: 900 + price: 492 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU591: + cost: 239 + init_stock: 4500 + price: 266 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU592: + cost: 263 + init_stock: 3950 + price: 293 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU593: + cost: 324 + init_stock: 4950 + price: 361 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU594: + cost: 210 + init_stock: 650 + price: 234 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU595: + cost: 33 + init_stock: 2200 + price: 37 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU596: + cost: 220 + init_stock: 4050 + price: 245 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU597: + cost: 24 + init_stock: 1950 + price: 27 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU598: + cost: 261 + init_stock: 2300 + price: 290 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU599: + cost: 404 + init_stock: 2050 + price: 449 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU6: + cost: 81 + init_stock: 850 + price: 90 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU60: + cost: 430 + init_stock: 4450 + price: 478 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU600: + cost: 369 + init_stock: 1200 + price: 411 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU601: + cost: 326 + init_stock: 2250 + price: 363 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU602: + cost: 316 + init_stock: 2450 + price: 352 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU603: + cost: 32 + init_stock: 2950 + price: 36 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU604: + cost: 297 + init_stock: 3900 + price: 330 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU605: + cost: 378 + init_stock: 4650 + price: 420 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU606: + cost: 381 + init_stock: 3200 + price: 424 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU607: + cost: 129 + init_stock: 550 + price: 144 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU608: + cost: 31 + init_stock: 2200 + price: 35 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU609: + cost: 141 + init_stock: 2000 + price: 157 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU61: + cost: 285 + init_stock: 4150 + price: 317 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU610: + cost: 55 + init_stock: 600 + price: 62 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU611: + cost: 232 + init_stock: 4550 + price: 258 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU612: + cost: 51 + init_stock: 3600 + price: 57 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU613: + cost: 398 + init_stock: 2150 + price: 443 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU614: + cost: 223 + init_stock: 4300 + price: 248 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU615: + cost: 233 + init_stock: 3000 + price: 259 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU616: + cost: 169 + init_stock: 4400 + price: 188 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU617: + cost: 300 + init_stock: 2050 + price: 334 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU618: + cost: 78 + init_stock: 1550 + price: 87 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU619: + cost: 193 + init_stock: 1750 + price: 215 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU62: + cost: 141 + init_stock: 3650 + price: 157 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU620: + cost: 10 + init_stock: 5000 + price: 12 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU621: + cost: 326 + init_stock: 800 + price: 363 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU622: + cost: 136 + init_stock: 3650 + price: 152 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU623: + cost: 236 + init_stock: 500 + price: 263 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU624: + cost: 380 + init_stock: 4800 + price: 423 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU625: + cost: 264 + init_stock: 4900 + price: 294 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU626: + cost: 183 + init_stock: 2900 + price: 204 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU627: + cost: 157 + init_stock: 4400 + price: 175 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU628: + cost: 12 + init_stock: 4100 + price: 14 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU629: + cost: 316 + init_stock: 3750 + price: 352 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU63: + cost: 274 + init_stock: 300 + price: 305 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU630: + cost: 366 + init_stock: 3950 + price: 407 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU631: + cost: 333 + init_stock: 1650 + price: 370 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU632: + cost: 440 + init_stock: 750 + price: 489 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU633: + cost: 268 + init_stock: 3250 + price: 298 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU634: + cost: 46 + init_stock: 3700 + price: 52 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU635: + cost: 27 + init_stock: 4850 + price: 30 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU636: + cost: 34 + init_stock: 2500 + price: 38 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU637: + cost: 426 + init_stock: 2050 + price: 474 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU638: + cost: 240 + init_stock: 2700 + price: 267 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU639: + cost: 419 + init_stock: 1300 + price: 466 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU64: + cost: 165 + init_stock: 2700 + price: 184 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU640: + cost: 212 + init_stock: 900 + price: 236 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU641: + cost: 256 + init_stock: 3050 + price: 285 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU642: + cost: 119 + init_stock: 4200 + price: 133 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU643: + cost: 320 + init_stock: 5000 + price: 356 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU644: + cost: 449 + init_stock: 1750 + price: 499 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU645: + cost: 428 + init_stock: 4700 + price: 476 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU646: + cost: 70 + init_stock: 1900 + price: 78 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU647: + cost: 46 + init_stock: 2950 + price: 52 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU648: + cost: 421 + init_stock: 4150 + price: 468 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU649: + cost: 310 + init_stock: 3500 + price: 345 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU65: + cost: 346 + init_stock: 1350 + price: 385 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU650: + cost: 117 + init_stock: 800 + price: 130 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU651: + cost: 223 + init_stock: 1100 + price: 248 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU652: + cost: 90 + init_stock: 4600 + price: 100 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU653: + cost: 407 + init_stock: 3950 + price: 453 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU654: + cost: 396 + init_stock: 1550 + price: 441 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU655: + cost: 131 + init_stock: 2700 + price: 146 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU656: + cost: 330 + init_stock: 1900 + price: 367 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU657: + cost: 175 + init_stock: 1200 + price: 195 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU658: + cost: 432 + init_stock: 4350 + price: 480 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU659: + cost: 447 + init_stock: 4800 + price: 497 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU66: + cost: 33 + init_stock: 3650 + price: 37 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU660: + cost: 146 + init_stock: 3300 + price: 163 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU661: + cost: 350 + init_stock: 600 + price: 389 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU662: + cost: 445 + init_stock: 4100 + price: 495 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU663: + cost: 414 + init_stock: 3250 + price: 460 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU664: + cost: 357 + init_stock: 600 + price: 397 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU665: + cost: 60 + init_stock: 3550 + price: 67 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU666: + cost: 113 + init_stock: 600 + price: 126 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU667: + cost: 126 + init_stock: 350 + price: 140 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU668: + cost: 99 + init_stock: 4950 + price: 110 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU669: + cost: 108 + init_stock: 3350 + price: 121 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU67: + cost: 48 + init_stock: 4300 + price: 54 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU670: + cost: 445 + init_stock: 250 + price: 495 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU671: + cost: 399 + init_stock: 500 + price: 444 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU672: + cost: 383 + init_stock: 3900 + price: 426 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU673: + cost: 98 + init_stock: 2700 + price: 109 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU674: + cost: 352 + init_stock: 400 + price: 392 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU675: + cost: 121 + init_stock: 1650 + price: 135 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU676: + cost: 360 + init_stock: 4250 + price: 401 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU677: + cost: 404 + init_stock: 2500 + price: 449 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU678: + cost: 187 + init_stock: 3050 + price: 208 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU679: + cost: 320 + init_stock: 4150 + price: 356 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU68: + cost: 56 + init_stock: 3700 + price: 63 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU680: + cost: 258 + init_stock: 1300 + price: 287 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU681: + cost: 126 + init_stock: 3500 + price: 140 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU682: + cost: 146 + init_stock: 4300 + price: 163 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU683: + cost: 323 + init_stock: 3800 + price: 359 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU684: + cost: 33 + init_stock: 1550 + price: 37 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU685: + cost: 95 + init_stock: 1600 + price: 106 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU686: + cost: 46 + init_stock: 2900 + price: 52 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU687: + cost: 76 + init_stock: 2950 + price: 85 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU688: + cost: 133 + init_stock: 1400 + price: 148 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU689: + cost: 299 + init_stock: 1650 + price: 333 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU69: + cost: 58 + init_stock: 3350 + price: 65 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU690: + cost: 334 + init_stock: 1000 + price: 372 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU691: + cost: 247 + init_stock: 1200 + price: 275 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU692: + cost: 260 + init_stock: 4000 + price: 289 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU693: + cost: 232 + init_stock: 3500 + price: 258 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU694: + cost: 424 + init_stock: 900 + price: 472 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU695: + cost: 286 + init_stock: 2400 + price: 318 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU696: + cost: 56 + init_stock: 4600 + price: 63 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU697: + cost: 13 + init_stock: 2700 + price: 15 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU698: + cost: 12 + init_stock: 2550 + price: 14 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU699: + cost: 321 + init_stock: 3650 + price: 357 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU7: + cost: 282 + init_stock: 2150 + price: 314 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU70: + cost: 186 + init_stock: 3850 + price: 207 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU700: + cost: 18 + init_stock: 2700 + price: 21 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU701: + cost: 117 + init_stock: 3950 + price: 130 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU702: + cost: 204 + init_stock: 3450 + price: 227 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU703: + cost: 65 + init_stock: 1750 + price: 73 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU704: + cost: 33 + init_stock: 1300 + price: 37 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU705: + cost: 281 + init_stock: 1050 + price: 313 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU706: + cost: 340 + init_stock: 700 + price: 378 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU707: + cost: 271 + init_stock: 5000 + price: 302 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU708: + cost: 62 + init_stock: 2900 + price: 69 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU709: + cost: 56 + init_stock: 3900 + price: 63 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU71: + cost: 225 + init_stock: 2050 + price: 251 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU710: + cost: 429 + init_stock: 3100 + price: 477 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU711: + cost: 355 + init_stock: 4900 + price: 395 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU712: + cost: 45 + init_stock: 3400 + price: 50 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU713: + cost: 201 + init_stock: 1250 + price: 224 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU714: + cost: 88 + init_stock: 2100 + price: 98 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU715: + cost: 449 + init_stock: 1650 + price: 499 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU716: + cost: 421 + init_stock: 1600 + price: 468 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU717: + cost: 103 + init_stock: 1250 + price: 115 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU718: + cost: 255 + init_stock: 1750 + price: 284 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU719: + cost: 150 + init_stock: 600 + price: 167 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU72: + cost: 69 + init_stock: 3650 + price: 77 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU720: + cost: 343 + init_stock: 250 + price: 382 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU721: + cost: 332 + init_stock: 2700 + price: 369 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU722: + cost: 418 + init_stock: 3500 + price: 465 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU723: + cost: 38 + init_stock: 2400 + price: 43 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU724: + cost: 221 + init_stock: 4600 + price: 246 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU725: + cost: 156 + init_stock: 1550 + price: 174 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU726: + cost: 351 + init_stock: 2650 + price: 391 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU727: + cost: 402 + init_stock: 3050 + price: 447 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU728: + cost: 312 + init_stock: 1500 + price: 347 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU729: + cost: 416 + init_stock: 4150 + price: 463 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU73: + cost: 146 + init_stock: 1650 + price: 163 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU730: + cost: 189 + init_stock: 750 + price: 211 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU731: + cost: 51 + init_stock: 2700 + price: 57 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU732: + cost: 95 + init_stock: 1200 + price: 106 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU733: + cost: 271 + init_stock: 950 + price: 302 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU734: + cost: 347 + init_stock: 700 + price: 386 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU735: + cost: 76 + init_stock: 1950 + price: 85 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU736: + cost: 14 + init_stock: 3350 + price: 16 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU737: + cost: 343 + init_stock: 4600 + price: 382 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU738: + cost: 386 + init_stock: 450 + price: 429 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU739: + cost: 256 + init_stock: 2250 + price: 285 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU74: + cost: 42 + init_stock: 4950 + price: 47 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU740: + cost: 427 + init_stock: 2900 + price: 475 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU741: + cost: 419 + init_stock: 3550 + price: 466 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU742: + cost: 397 + init_stock: 1350 + price: 442 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU743: + cost: 74 + init_stock: 1000 + price: 83 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU744: + cost: 306 + init_stock: 3600 + price: 340 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU745: + cost: 399 + init_stock: 2500 + price: 444 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU746: + cost: 446 + init_stock: 4800 + price: 496 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU747: + cost: 157 + init_stock: 3000 + price: 175 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU748: + cost: 414 + init_stock: 350 + price: 461 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU749: + cost: 432 + init_stock: 850 + price: 481 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU75: + cost: 287 + init_stock: 1600 + price: 319 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU750: + cost: 216 + init_stock: 2250 + price: 240 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU751: + cost: 271 + init_stock: 450 + price: 302 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU752: + cost: 382 + init_stock: 1650 + price: 425 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU753: + cost: 94 + init_stock: 2200 + price: 105 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU754: + cost: 445 + init_stock: 4650 + price: 495 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU755: + cost: 392 + init_stock: 1550 + price: 436 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU756: + cost: 210 + init_stock: 4750 + price: 234 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU757: + cost: 281 + init_stock: 650 + price: 313 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU758: + cost: 287 + init_stock: 1050 + price: 319 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU759: + cost: 281 + init_stock: 2050 + price: 313 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU76: + cost: 217 + init_stock: 3350 + price: 242 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU760: + cost: 322 + init_stock: 1300 + price: 358 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU761: + cost: 438 + init_stock: 800 + price: 487 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU762: + cost: 370 + init_stock: 250 + price: 412 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU763: + cost: 351 + init_stock: 1900 + price: 391 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU764: + cost: 114 + init_stock: 2450 + price: 127 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU765: + cost: 243 + init_stock: 1250 + price: 271 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU766: + cost: 207 + init_stock: 1150 + price: 230 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU767: + cost: 81 + init_stock: 4000 + price: 91 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU768: + cost: 369 + init_stock: 5000 + price: 410 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU769: + cost: 291 + init_stock: 2050 + price: 324 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU77: + cost: 288 + init_stock: 1700 + price: 321 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU770: + cost: 259 + init_stock: 2400 + price: 288 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU771: + cost: 47 + init_stock: 500 + price: 53 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU772: + cost: 184 + init_stock: 2750 + price: 205 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU773: + cost: 62 + init_stock: 1350 + price: 69 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU774: + cost: 450 + init_stock: 4800 + price: 500 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU775: + cost: 329 + init_stock: 2150 + price: 366 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU776: + cost: 417 + init_stock: 400 + price: 464 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU777: + cost: 443 + init_stock: 2700 + price: 493 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU778: + cost: 347 + init_stock: 1250 + price: 386 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU779: + cost: 134 + init_stock: 3100 + price: 149 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU78: + cost: 207 + init_stock: 2250 + price: 231 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU780: + cost: 243 + init_stock: 1750 + price: 271 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU781: + cost: 75 + init_stock: 2900 + price: 84 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU782: + cost: 130 + init_stock: 3500 + price: 145 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU783: + cost: 171 + init_stock: 1650 + price: 191 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU784: + cost: 30 + init_stock: 900 + price: 34 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU785: + cost: 331 + init_stock: 1400 + price: 368 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU786: + cost: 49 + init_stock: 1150 + price: 55 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU787: + cost: 331 + init_stock: 300 + price: 368 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU788: + cost: 231 + init_stock: 500 + price: 257 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU789: + cost: 196 + init_stock: 4250 + price: 218 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU79: + cost: 392 + init_stock: 3650 + price: 436 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU790: + cost: 386 + init_stock: 2650 + price: 429 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU791: + cost: 49 + init_stock: 3650 + price: 55 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU792: + cost: 72 + init_stock: 3500 + price: 80 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU793: + cost: 204 + init_stock: 550 + price: 227 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU794: + cost: 324 + init_stock: 1400 + price: 361 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU795: + cost: 180 + init_stock: 2400 + price: 200 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU796: + cost: 324 + init_stock: 350 + price: 360 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU797: + cost: 33 + init_stock: 4350 + price: 37 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU798: + cost: 111 + init_stock: 4750 + price: 124 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU799: + cost: 83 + init_stock: 900 + price: 93 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU8: + cost: 288 + init_stock: 1200 + price: 320 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU80: + cost: 284 + init_stock: 2100 + price: 316 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU800: + cost: 237 + init_stock: 3300 + price: 264 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU801: + cost: 55 + init_stock: 5000 + price: 62 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU802: + cost: 260 + init_stock: 550 + price: 289 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU803: + cost: 436 + init_stock: 3050 + price: 485 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU804: + cost: 113 + init_stock: 2900 + price: 126 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU805: + cost: 204 + init_stock: 3650 + price: 227 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU806: + cost: 53 + init_stock: 1700 + price: 59 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU807: + cost: 421 + init_stock: 3750 + price: 468 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU808: + cost: 144 + init_stock: 2700 + price: 161 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU809: + cost: 47 + init_stock: 3400 + price: 53 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU81: + cost: 172 + init_stock: 4900 + price: 192 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU810: + cost: 405 + init_stock: 2200 + price: 451 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU811: + cost: 288 + init_stock: 800 + price: 320 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU812: + cost: 263 + init_stock: 750 + price: 293 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU813: + cost: 35 + init_stock: 3950 + price: 39 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU814: + cost: 436 + init_stock: 3200 + price: 485 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU815: + cost: 47 + init_stock: 1800 + price: 53 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU816: + cost: 138 + init_stock: 2800 + price: 154 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU817: + cost: 235 + init_stock: 4750 + price: 262 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU818: + cost: 80 + init_stock: 3800 + price: 89 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU819: + cost: 295 + init_stock: 4500 + price: 328 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU82: + cost: 213 + init_stock: 4750 + price: 237 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU820: + cost: 414 + init_stock: 1500 + price: 461 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU821: + cost: 359 + init_stock: 4150 + price: 399 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU822: + cost: 54 + init_stock: 3600 + price: 60 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU823: + cost: 153 + init_stock: 4050 + price: 171 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU824: + cost: 48 + init_stock: 3050 + price: 54 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU825: + cost: 324 + init_stock: 2100 + price: 361 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU826: + cost: 368 + init_stock: 1300 + price: 409 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU827: + cost: 445 + init_stock: 3900 + price: 495 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU828: + cost: 87 + init_stock: 2250 + price: 97 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU829: + cost: 270 + init_stock: 2500 + price: 301 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU83: + cost: 328 + init_stock: 2950 + price: 365 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU830: + cost: 193 + init_stock: 650 + price: 215 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU831: + cost: 291 + init_stock: 2500 + price: 324 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU832: + cost: 106 + init_stock: 4650 + price: 118 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU833: + cost: 391 + init_stock: 1500 + price: 435 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU834: + cost: 361 + init_stock: 4750 + price: 402 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU835: + cost: 194 + init_stock: 2800 + price: 216 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU836: + cost: 13 + init_stock: 3700 + price: 15 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU837: + cost: 55 + init_stock: 3450 + price: 62 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU838: + cost: 121 + init_stock: 1200 + price: 135 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU839: + cost: 436 + init_stock: 4350 + price: 485 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU84: + cost: 18 + init_stock: 4100 + price: 21 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU840: + cost: 166 + init_stock: 1500 + price: 185 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU841: + cost: 189 + init_stock: 4900 + price: 211 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU842: + cost: 225 + init_stock: 500 + price: 251 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU843: + cost: 107 + init_stock: 3050 + price: 119 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU844: + cost: 107 + init_stock: 4800 + price: 119 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU845: + cost: 440 + init_stock: 3750 + price: 489 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU846: + cost: 450 + init_stock: 500 + price: 500 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU847: + cost: 273 + init_stock: 4600 + price: 304 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU848: + cost: 217 + init_stock: 4500 + price: 242 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU849: + cost: 29 + init_stock: 4800 + price: 33 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU85: + cost: 140 + init_stock: 3100 + price: 156 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU850: + cost: 404 + init_stock: 2550 + price: 449 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU851: + cost: 375 + init_stock: 2300 + price: 417 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU852: + cost: 246 + init_stock: 2600 + price: 274 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU853: + cost: 58 + init_stock: 2700 + price: 65 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU854: + cost: 201 + init_stock: 4600 + price: 224 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU855: + cost: 112 + init_stock: 350 + price: 125 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU856: + cost: 172 + init_stock: 2100 + price: 192 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU857: + cost: 162 + init_stock: 900 + price: 180 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU858: + cost: 353 + init_stock: 3950 + price: 393 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU859: + cost: 233 + init_stock: 3100 + price: 259 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU86: + cost: 289 + init_stock: 300 + price: 322 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU860: + cost: 258 + init_stock: 1500 + price: 287 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU861: + cost: 18 + init_stock: 1400 + price: 20 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU862: + cost: 77 + init_stock: 1150 + price: 86 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU863: + cost: 75 + init_stock: 3950 + price: 84 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU864: + cost: 258 + init_stock: 2150 + price: 287 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU865: + cost: 177 + init_stock: 1200 + price: 197 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU866: + cost: 353 + init_stock: 250 + price: 393 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU867: + cost: 207 + init_stock: 1750 + price: 231 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU868: + cost: 420 + init_stock: 3300 + price: 467 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU869: + cost: 102 + init_stock: 4600 + price: 114 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU87: + cost: 293 + init_stock: 450 + price: 326 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU870: + cost: 39 + init_stock: 550 + price: 44 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU871: + cost: 363 + init_stock: 4200 + price: 404 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU872: + cost: 261 + init_stock: 2700 + price: 291 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU873: + cost: 300 + init_stock: 1050 + price: 334 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU874: + cost: 327 + init_stock: 2400 + price: 364 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU875: + cost: 248 + init_stock: 4350 + price: 276 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU876: + cost: 307 + init_stock: 3500 + price: 342 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU877: + cost: 358 + init_stock: 4050 + price: 398 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU878: + cost: 117 + init_stock: 3400 + price: 131 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU879: + cost: 308 + init_stock: 400 + price: 343 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU88: + cost: 219 + init_stock: 1100 + price: 244 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU880: + cost: 446 + init_stock: 2200 + price: 496 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU881: + cost: 330 + init_stock: 3350 + price: 367 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU882: + cost: 77 + init_stock: 3150 + price: 86 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU883: + cost: 171 + init_stock: 2300 + price: 190 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU884: + cost: 180 + init_stock: 1450 + price: 200 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU885: + cost: 119 + init_stock: 2300 + price: 133 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU886: + cost: 196 + init_stock: 800 + price: 218 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU887: + cost: 193 + init_stock: 4400 + price: 215 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU888: + cost: 395 + init_stock: 3350 + price: 439 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU889: + cost: 94 + init_stock: 3400 + price: 105 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU89: + cost: 200 + init_stock: 350 + price: 223 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU890: + cost: 164 + init_stock: 1800 + price: 183 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU891: + cost: 76 + init_stock: 2200 + price: 85 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU892: + cost: 36 + init_stock: 4200 + price: 41 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU893: + cost: 121 + init_stock: 1100 + price: 135 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU894: + cost: 364 + init_stock: 3500 + price: 405 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU895: + cost: 288 + init_stock: 3100 + price: 320 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU896: + cost: 405 + init_stock: 2700 + price: 451 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU897: + cost: 126 + init_stock: 4800 + price: 141 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU898: + cost: 198 + init_stock: 4450 + price: 220 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU899: + cost: 101 + init_stock: 1900 + price: 113 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU9: + cost: 434 + init_stock: 2100 + price: 483 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU90: + cost: 195 + init_stock: 550 + price: 217 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU900: + cost: 196 + init_stock: 3500 + price: 218 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU901: + cost: 301 + init_stock: 4150 + price: 335 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU902: + cost: 106 + init_stock: 4950 + price: 118 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU903: + cost: 147 + init_stock: 550 + price: 164 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU904: + cost: 285 + init_stock: 1500 + price: 317 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU905: + cost: 415 + init_stock: 1550 + price: 462 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU906: + cost: 224 + init_stock: 2250 + price: 249 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU907: + cost: 185 + init_stock: 3800 + price: 206 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU908: + cost: 377 + init_stock: 3150 + price: 419 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU909: + cost: 372 + init_stock: 3200 + price: 414 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU91: + cost: 228 + init_stock: 4500 + price: 254 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU910: + cost: 333 + init_stock: 2600 + price: 371 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU911: + cost: 378 + init_stock: 3000 + price: 421 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU912: + cost: 147 + init_stock: 750 + price: 164 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU913: + cost: 18 + init_stock: 3850 + price: 20 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU914: + cost: 129 + init_stock: 2300 + price: 144 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU915: + cost: 192 + init_stock: 500 + price: 214 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU916: + cost: 105 + init_stock: 2400 + price: 117 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU917: + cost: 190 + init_stock: 1300 + price: 212 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU918: + cost: 286 + init_stock: 2450 + price: 318 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU919: + cost: 53 + init_stock: 550 + price: 59 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU92: + cost: 306 + init_stock: 3050 + price: 341 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU920: + cost: 144 + init_stock: 1200 + price: 161 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU921: + cost: 440 + init_stock: 4400 + price: 489 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU922: + cost: 31 + init_stock: 4550 + price: 35 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU923: + cost: 163 + init_stock: 400 + price: 182 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU924: + cost: 327 + init_stock: 2300 + price: 364 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU925: + cost: 27 + init_stock: 4750 + price: 31 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU926: + cost: 424 + init_stock: 500 + price: 472 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU927: + cost: 128 + init_stock: 250 + price: 143 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU928: + cost: 292 + init_stock: 1850 + price: 325 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU929: + cost: 266 + init_stock: 3800 + price: 296 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU93: + cost: 324 + init_stock: 4200 + price: 361 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU930: + cost: 231 + init_stock: 4250 + price: 257 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU931: + cost: 140 + init_stock: 1650 + price: 156 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU932: + cost: 124 + init_stock: 4950 + price: 138 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU933: + cost: 255 + init_stock: 1900 + price: 284 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU934: + cost: 191 + init_stock: 2150 + price: 213 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU935: + cost: 305 + init_stock: 1350 + price: 339 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU936: + cost: 153 + init_stock: 450 + price: 170 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU937: + cost: 110 + init_stock: 750 + price: 123 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU938: + cost: 269 + init_stock: 4550 + price: 299 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU939: + cost: 240 + init_stock: 1250 + price: 267 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU94: + cost: 401 + init_stock: 2450 + price: 446 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU940: + cost: 427 + init_stock: 2750 + price: 475 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU941: + cost: 181 + init_stock: 1200 + price: 202 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU942: + cost: 367 + init_stock: 3600 + price: 408 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU943: + cost: 170 + init_stock: 400 + price: 189 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU944: + cost: 444 + init_stock: 1700 + price: 494 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU945: + cost: 213 + init_stock: 850 + price: 237 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU946: + cost: 310 + init_stock: 4900 + price: 345 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU947: + cost: 445 + init_stock: 2700 + price: 495 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU948: + cost: 282 + init_stock: 3900 + price: 314 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU949: + cost: 407 + init_stock: 950 + price: 453 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU95: + cost: 22 + init_stock: 4850 + price: 25 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU950: + cost: 257 + init_stock: 3450 + price: 286 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU951: + cost: 150 + init_stock: 3500 + price: 167 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU952: + cost: 443 + init_stock: 2050 + price: 493 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU953: + cost: 424 + init_stock: 3950 + price: 472 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU954: + cost: 258 + init_stock: 700 + price: 287 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU955: + cost: 84 + init_stock: 750 + price: 94 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU956: + cost: 141 + init_stock: 1850 + price: 157 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU957: + cost: 273 + init_stock: 3150 + price: 304 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU958: + cost: 313 + init_stock: 2750 + price: 348 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU959: + cost: 397 + init_stock: 3950 + price: 442 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU96: + cost: 425 + init_stock: 2450 + price: 473 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU960: + cost: 24 + init_stock: 3800 + price: 27 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU961: + cost: 253 + init_stock: 3200 + price: 282 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU962: + cost: 351 + init_stock: 550 + price: 391 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU963: + cost: 222 + init_stock: 900 + price: 247 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU964: + cost: 445 + init_stock: 600 + price: 495 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU965: + cost: 385 + init_stock: 3700 + price: 428 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU966: + cost: 324 + init_stock: 2050 + price: 360 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU967: + cost: 366 + init_stock: 4050 + price: 407 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU968: + cost: 62 + init_stock: 3150 + price: 69 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU969: + cost: 117 + init_stock: 1600 + price: 131 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU97: + cost: 388 + init_stock: 2450 + price: 432 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU970: + cost: 338 + init_stock: 4800 + price: 376 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU971: + cost: 39 + init_stock: 700 + price: 44 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU972: + cost: 70 + init_stock: 400 + price: 78 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU973: + cost: 366 + init_stock: 2100 + price: 407 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU974: + cost: 390 + init_stock: 1350 + price: 434 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU975: + cost: 88 + init_stock: 4450 + price: 98 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU976: + cost: 236 + init_stock: 3050 + price: 263 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU977: + cost: 74 + init_stock: 1150 + price: 83 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU978: + cost: 81 + init_stock: 3850 + price: 90 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU979: + cost: 186 + init_stock: 650 + price: 207 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU98: + cost: 38 + init_stock: 4150 + price: 43 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU980: + cost: 204 + init_stock: 3000 + price: 227 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU981: + cost: 250 + init_stock: 850 + price: 278 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU982: + cost: 112 + init_stock: 500 + price: 125 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU983: + cost: 387 + init_stock: 3400 + price: 431 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU984: + cost: 53 + init_stock: 1400 + price: 59 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU985: + cost: 171 + init_stock: 3150 + price: 191 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU986: + cost: 212 + init_stock: 1900 + price: 236 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU987: + cost: 299 + init_stock: 4950 + price: 333 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU988: + cost: 366 + init_stock: 3850 + price: 407 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU989: + cost: 47 + init_stock: 4100 + price: 53 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU99: + cost: 58 + init_stock: 3950 + price: 65 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU990: + cost: 331 + init_stock: 4700 + price: 368 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU991: + cost: 313 + init_stock: 4600 + price: 348 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU992: + cost: 53 + init_stock: 3400 + price: 59 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU993: + cost: 448 + init_stock: 1150 + price: 498 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU994: + cost: 394 + init_stock: 1700 + price: 438 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU995: + cost: 262 + init_stock: 2450 + price: 292 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU996: + cost: 65 + init_stock: 2450 + price: 73 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU997: + cost: 288 + init_stock: 3550 + price: 321 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU998: + cost: 333 + init_stock: 750 + price: 371 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU999: + cost: 187 + init_stock: 4450 + price: 208 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5316100 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 500 + definition_ref: WarehouseFacility + name: WAREHOUSE0 + skus: + SKU0: + cost: 154 + init_stock: 1780 + price: 154 + service_level: 0.96 + vlt: 2 + SKU1: + cost: 122 + init_stock: 1720 + price: 122 + service_level: 0.96 + vlt: 1 + SKU10: + cost: 158 + init_stock: 1840 + price: 158 + service_level: 0.96 + vlt: 3 + SKU100: + cost: 448 + init_stock: 740 + price: 448 + service_level: 0.96 + vlt: 3 + SKU101: + cost: 432 + init_stock: 1240 + price: 432 + service_level: 0.96 + vlt: 1 + SKU102: + cost: 33 + init_stock: 1660 + price: 33 + service_level: 0.96 + vlt: 2 + SKU103: + cost: 169 + init_stock: 1580 + price: 169 + service_level: 0.96 + vlt: 3 + SKU104: + cost: 253 + init_stock: 580 + price: 253 + service_level: 0.96 + vlt: 3 + SKU105: + cost: 305 + init_stock: 1180 + price: 305 + service_level: 0.96 + vlt: 3 + SKU106: + cost: 172 + init_stock: 580 + price: 172 + service_level: 0.96 + vlt: 3 + SKU107: + cost: 323 + init_stock: 1620 + price: 323 + service_level: 0.96 + vlt: 1 + SKU108: + cost: 283 + init_stock: 1840 + price: 283 + service_level: 0.96 + vlt: 2 + SKU109: + cost: 304 + init_stock: 1540 + price: 304 + service_level: 0.96 + vlt: 2 + SKU11: + cost: 204 + init_stock: 1160 + price: 204 + service_level: 0.96 + vlt: 3 + SKU110: + cost: 374 + init_stock: 1860 + price: 374 + service_level: 0.96 + vlt: 3 + SKU111: + cost: 215 + init_stock: 1560 + price: 215 + service_level: 0.96 + vlt: 3 + SKU112: + cost: 417 + init_stock: 160 + price: 417 + service_level: 0.96 + vlt: 1 + SKU113: + cost: 64 + init_stock: 1380 + price: 64 + service_level: 0.96 + vlt: 1 + SKU114: + cost: 63 + init_stock: 1080 + price: 63 + service_level: 0.96 + vlt: 2 + SKU115: + cost: 466 + init_stock: 1740 + price: 466 + service_level: 0.96 + vlt: 2 + SKU116: + cost: 115 + init_stock: 1840 + price: 115 + service_level: 0.96 + vlt: 1 + SKU117: + cost: 76 + init_stock: 1340 + price: 76 + service_level: 0.96 + vlt: 1 + SKU118: + cost: 292 + init_stock: 1380 + price: 292 + service_level: 0.96 + vlt: 3 + SKU119: + cost: 333 + init_stock: 780 + price: 333 + service_level: 0.96 + vlt: 1 + SKU12: + cost: 429 + init_stock: 1480 + price: 429 + service_level: 0.96 + vlt: 1 + SKU120: + cost: 147 + init_stock: 960 + price: 147 + service_level: 0.96 + vlt: 3 + SKU121: + cost: 169 + init_stock: 1180 + price: 169 + service_level: 0.96 + vlt: 2 + SKU122: + cost: 71 + init_stock: 1440 + price: 71 + service_level: 0.96 + vlt: 2 + SKU123: + cost: 33 + init_stock: 1180 + price: 33 + service_level: 0.96 + vlt: 2 + SKU124: + cost: 478 + init_stock: 1160 + price: 478 + service_level: 0.96 + vlt: 2 + SKU125: + cost: 311 + init_stock: 720 + price: 311 + service_level: 0.96 + vlt: 2 + SKU126: + cost: 497 + init_stock: 1520 + price: 497 + service_level: 0.96 + vlt: 2 + SKU127: + cost: 63 + init_stock: 1840 + price: 63 + service_level: 0.96 + vlt: 1 + SKU128: + cost: 200 + init_stock: 840 + price: 200 + service_level: 0.96 + vlt: 3 + SKU129: + cost: 465 + init_stock: 580 + price: 465 + service_level: 0.96 + vlt: 2 + SKU13: + cost: 458 + init_stock: 580 + price: 458 + service_level: 0.96 + vlt: 3 + SKU130: + cost: 224 + init_stock: 1080 + price: 224 + service_level: 0.96 + vlt: 2 + SKU131: + cost: 491 + init_stock: 1200 + price: 491 + service_level: 0.96 + vlt: 1 + SKU132: + cost: 93 + init_stock: 1220 + price: 93 + service_level: 0.96 + vlt: 2 + SKU133: + cost: 269 + init_stock: 260 + price: 269 + service_level: 0.96 + vlt: 1 + SKU134: + cost: 364 + init_stock: 400 + price: 364 + service_level: 0.96 + vlt: 2 + SKU135: + cost: 351 + init_stock: 1960 + price: 351 + service_level: 0.96 + vlt: 3 + SKU136: + cost: 429 + init_stock: 800 + price: 429 + service_level: 0.96 + vlt: 1 + SKU137: + cost: 193 + init_stock: 1840 + price: 193 + service_level: 0.96 + vlt: 3 + SKU138: + cost: 113 + init_stock: 560 + price: 113 + service_level: 0.96 + vlt: 2 + SKU139: + cost: 10 + init_stock: 1900 + price: 10 + service_level: 0.96 + vlt: 3 + SKU14: + cost: 55 + init_stock: 260 + price: 55 + service_level: 0.96 + vlt: 3 + SKU140: + cost: 350 + init_stock: 1920 + price: 350 + service_level: 0.96 + vlt: 1 + SKU141: + cost: 70 + init_stock: 1420 + price: 70 + service_level: 0.96 + vlt: 3 + SKU142: + cost: 496 + init_stock: 1460 + price: 496 + service_level: 0.96 + vlt: 3 + SKU143: + cost: 33 + init_stock: 1660 + price: 33 + service_level: 0.96 + vlt: 1 + SKU144: + cost: 275 + init_stock: 1500 + price: 275 + service_level: 0.96 + vlt: 1 + SKU145: + cost: 51 + init_stock: 1940 + price: 51 + service_level: 0.96 + vlt: 3 + SKU146: + cost: 140 + init_stock: 720 + price: 140 + service_level: 0.96 + vlt: 3 + SKU147: + cost: 383 + init_stock: 980 + price: 383 + service_level: 0.96 + vlt: 1 + SKU148: + cost: 278 + init_stock: 1360 + price: 278 + service_level: 0.96 + vlt: 1 + SKU149: + cost: 402 + init_stock: 1040 + price: 402 + service_level: 0.96 + vlt: 3 + SKU15: + cost: 421 + init_stock: 1420 + price: 421 + service_level: 0.96 + vlt: 2 + SKU150: + cost: 81 + init_stock: 1660 + price: 81 + service_level: 0.96 + vlt: 1 + SKU151: + cost: 407 + init_stock: 1300 + price: 407 + service_level: 0.96 + vlt: 2 + SKU152: + cost: 327 + init_stock: 1280 + price: 327 + service_level: 0.96 + vlt: 3 + SKU153: + cost: 204 + init_stock: 1940 + price: 204 + service_level: 0.96 + vlt: 1 + SKU154: + cost: 88 + init_stock: 520 + price: 88 + service_level: 0.96 + vlt: 2 + SKU155: + cost: 428 + init_stock: 520 + price: 428 + service_level: 0.96 + vlt: 2 + SKU156: + cost: 486 + init_stock: 160 + price: 486 + service_level: 0.96 + vlt: 3 + SKU157: + cost: 35 + init_stock: 1320 + price: 35 + service_level: 0.96 + vlt: 2 + SKU158: + cost: 186 + init_stock: 1140 + price: 186 + service_level: 0.96 + vlt: 2 + SKU159: + cost: 202 + init_stock: 1840 + price: 202 + service_level: 0.96 + vlt: 1 + SKU16: + cost: 209 + init_stock: 480 + price: 209 + service_level: 0.96 + vlt: 3 + SKU160: + cost: 433 + init_stock: 940 + price: 433 + service_level: 0.96 + vlt: 1 + SKU161: + cost: 250 + init_stock: 880 + price: 250 + service_level: 0.96 + vlt: 1 + SKU162: + cost: 316 + init_stock: 1600 + price: 316 + service_level: 0.96 + vlt: 3 + SKU163: + cost: 114 + init_stock: 280 + price: 114 + service_level: 0.96 + vlt: 1 + SKU164: + cost: 304 + init_stock: 1320 + price: 304 + service_level: 0.96 + vlt: 2 + SKU165: + cost: 195 + init_stock: 1060 + price: 195 + service_level: 0.96 + vlt: 2 + SKU166: + cost: 315 + init_stock: 1020 + price: 315 + service_level: 0.96 + vlt: 1 + SKU167: + cost: 470 + init_stock: 1880 + price: 470 + service_level: 0.96 + vlt: 1 + SKU168: + cost: 476 + init_stock: 380 + price: 476 + service_level: 0.96 + vlt: 1 + SKU169: + cost: 134 + init_stock: 380 + price: 134 + service_level: 0.96 + vlt: 1 + SKU17: + cost: 401 + init_stock: 760 + price: 401 + service_level: 0.96 + vlt: 1 + SKU170: + cost: 289 + init_stock: 760 + price: 289 + service_level: 0.96 + vlt: 1 + SKU171: + cost: 481 + init_stock: 1640 + price: 481 + service_level: 0.96 + vlt: 1 + SKU172: + cost: 434 + init_stock: 620 + price: 434 + service_level: 0.96 + vlt: 2 + SKU173: + cost: 292 + init_stock: 1880 + price: 292 + service_level: 0.96 + vlt: 2 + SKU174: + cost: 474 + init_stock: 280 + price: 474 + service_level: 0.96 + vlt: 2 + SKU175: + cost: 476 + init_stock: 420 + price: 476 + service_level: 0.96 + vlt: 2 + SKU176: + cost: 299 + init_stock: 1600 + price: 299 + service_level: 0.96 + vlt: 2 + SKU177: + cost: 425 + init_stock: 420 + price: 425 + service_level: 0.96 + vlt: 1 + SKU178: + cost: 419 + init_stock: 1400 + price: 419 + service_level: 0.96 + vlt: 3 + SKU179: + cost: 236 + init_stock: 1300 + price: 236 + service_level: 0.96 + vlt: 3 + SKU18: + cost: 460 + init_stock: 1620 + price: 460 + service_level: 0.96 + vlt: 2 + SKU180: + cost: 99 + init_stock: 320 + price: 99 + service_level: 0.96 + vlt: 1 + SKU181: + cost: 122 + init_stock: 540 + price: 122 + service_level: 0.96 + vlt: 2 + SKU182: + cost: 443 + init_stock: 460 + price: 443 + service_level: 0.96 + vlt: 2 + SKU183: + cost: 72 + init_stock: 1060 + price: 72 + service_level: 0.96 + vlt: 1 + SKU184: + cost: 192 + init_stock: 1540 + price: 192 + service_level: 0.96 + vlt: 3 + SKU185: + cost: 130 + init_stock: 560 + price: 130 + service_level: 0.96 + vlt: 2 + SKU186: + cost: 49 + init_stock: 780 + price: 49 + service_level: 0.96 + vlt: 3 + SKU187: + cost: 492 + init_stock: 940 + price: 492 + service_level: 0.96 + vlt: 3 + SKU188: + cost: 50 + init_stock: 1800 + price: 50 + service_level: 0.96 + vlt: 3 + SKU189: + cost: 145 + init_stock: 660 + price: 145 + service_level: 0.96 + vlt: 2 + SKU19: + cost: 451 + init_stock: 120 + price: 451 + service_level: 0.96 + vlt: 1 + SKU190: + cost: 72 + init_stock: 1580 + price: 72 + service_level: 0.96 + vlt: 2 + SKU191: + cost: 285 + init_stock: 800 + price: 285 + service_level: 0.96 + vlt: 3 + SKU192: + cost: 183 + init_stock: 1900 + price: 183 + service_level: 0.96 + vlt: 1 + SKU193: + cost: 345 + init_stock: 1300 + price: 345 + service_level: 0.96 + vlt: 2 + SKU194: + cost: 439 + init_stock: 1480 + price: 439 + service_level: 0.96 + vlt: 2 + SKU195: + cost: 205 + init_stock: 640 + price: 205 + service_level: 0.96 + vlt: 3 + SKU196: + cost: 208 + init_stock: 440 + price: 208 + service_level: 0.96 + vlt: 2 + SKU197: + cost: 476 + init_stock: 100 + price: 476 + service_level: 0.96 + vlt: 1 + SKU198: + cost: 269 + init_stock: 1540 + price: 269 + service_level: 0.96 + vlt: 1 + SKU199: + cost: 172 + init_stock: 1100 + price: 172 + service_level: 0.96 + vlt: 3 + SKU2: + cost: 204 + init_stock: 500 + price: 204 + service_level: 0.96 + vlt: 3 + SKU20: + cost: 437 + init_stock: 820 + price: 437 + service_level: 0.96 + vlt: 1 + SKU200: + cost: 51 + init_stock: 1880 + price: 51 + service_level: 0.96 + vlt: 3 + SKU201: + cost: 487 + init_stock: 1480 + price: 487 + service_level: 0.96 + vlt: 2 + SKU202: + cost: 406 + init_stock: 280 + price: 406 + service_level: 0.96 + vlt: 1 + SKU203: + cost: 333 + init_stock: 580 + price: 333 + service_level: 0.96 + vlt: 1 + SKU204: + cost: 64 + init_stock: 1560 + price: 64 + service_level: 0.96 + vlt: 3 + SKU205: + cost: 448 + init_stock: 1820 + price: 448 + service_level: 0.96 + vlt: 1 + SKU206: + cost: 65 + init_stock: 1940 + price: 65 + service_level: 0.96 + vlt: 3 + SKU207: + cost: 437 + init_stock: 1160 + price: 437 + service_level: 0.96 + vlt: 2 + SKU208: + cost: 38 + init_stock: 500 + price: 38 + service_level: 0.96 + vlt: 3 + SKU209: + cost: 327 + init_stock: 1300 + price: 327 + service_level: 0.96 + vlt: 3 + SKU21: + cost: 17 + init_stock: 1100 + price: 17 + service_level: 0.96 + vlt: 3 + SKU210: + cost: 455 + init_stock: 1000 + price: 455 + service_level: 0.96 + vlt: 2 + SKU211: + cost: 170 + init_stock: 300 + price: 170 + service_level: 0.96 + vlt: 2 + SKU212: + cost: 62 + init_stock: 1380 + price: 62 + service_level: 0.96 + vlt: 3 + SKU213: + cost: 263 + init_stock: 1400 + price: 263 + service_level: 0.96 + vlt: 1 + SKU214: + cost: 434 + init_stock: 140 + price: 434 + service_level: 0.96 + vlt: 2 + SKU215: + cost: 283 + init_stock: 480 + price: 283 + service_level: 0.96 + vlt: 1 + SKU216: + cost: 391 + init_stock: 1560 + price: 391 + service_level: 0.96 + vlt: 2 + SKU217: + cost: 168 + init_stock: 1960 + price: 168 + service_level: 0.96 + vlt: 1 + SKU218: + cost: 485 + init_stock: 360 + price: 485 + service_level: 0.96 + vlt: 1 + SKU219: + cost: 201 + init_stock: 1720 + price: 201 + service_level: 0.96 + vlt: 1 + SKU22: + cost: 22 + init_stock: 600 + price: 22 + service_level: 0.96 + vlt: 2 + SKU220: + cost: 394 + init_stock: 260 + price: 394 + service_level: 0.96 + vlt: 2 + SKU221: + cost: 108 + init_stock: 1840 + price: 108 + service_level: 0.96 + vlt: 2 + SKU222: + cost: 459 + init_stock: 1120 + price: 459 + service_level: 0.96 + vlt: 2 + SKU223: + cost: 420 + init_stock: 1460 + price: 420 + service_level: 0.96 + vlt: 1 + SKU224: + cost: 306 + init_stock: 1400 + price: 306 + service_level: 0.96 + vlt: 3 + SKU225: + cost: 118 + init_stock: 1840 + price: 118 + service_level: 0.96 + vlt: 2 + SKU226: + cost: 425 + init_stock: 880 + price: 425 + service_level: 0.96 + vlt: 3 + SKU227: + cost: 191 + init_stock: 1300 + price: 191 + service_level: 0.96 + vlt: 3 + SKU228: + cost: 160 + init_stock: 640 + price: 160 + service_level: 0.96 + vlt: 2 + SKU229: + cost: 278 + init_stock: 1460 + price: 278 + service_level: 0.96 + vlt: 3 + SKU23: + cost: 124 + init_stock: 280 + price: 124 + service_level: 0.96 + vlt: 3 + SKU230: + cost: 164 + init_stock: 380 + price: 164 + service_level: 0.96 + vlt: 2 + SKU231: + cost: 321 + init_stock: 1880 + price: 321 + service_level: 0.96 + vlt: 3 + SKU232: + cost: 435 + init_stock: 820 + price: 435 + service_level: 0.96 + vlt: 1 + SKU233: + cost: 219 + init_stock: 740 + price: 219 + service_level: 0.96 + vlt: 1 + SKU234: + cost: 144 + init_stock: 340 + price: 144 + service_level: 0.96 + vlt: 2 + SKU235: + cost: 396 + init_stock: 440 + price: 396 + service_level: 0.96 + vlt: 3 + SKU236: + cost: 383 + init_stock: 1980 + price: 383 + service_level: 0.96 + vlt: 3 + SKU237: + cost: 108 + init_stock: 1020 + price: 108 + service_level: 0.96 + vlt: 3 + SKU238: + cost: 49 + init_stock: 1140 + price: 49 + service_level: 0.96 + vlt: 2 + SKU239: + cost: 222 + init_stock: 1800 + price: 222 + service_level: 0.96 + vlt: 2 + SKU24: + cost: 368 + init_stock: 940 + price: 368 + service_level: 0.96 + vlt: 2 + SKU240: + cost: 387 + init_stock: 1660 + price: 387 + service_level: 0.96 + vlt: 3 + SKU241: + cost: 302 + init_stock: 1780 + price: 302 + service_level: 0.96 + vlt: 1 + SKU242: + cost: 62 + init_stock: 1580 + price: 62 + service_level: 0.96 + vlt: 3 + SKU243: + cost: 12 + init_stock: 1000 + price: 12 + service_level: 0.96 + vlt: 2 + SKU244: + cost: 296 + init_stock: 1300 + price: 296 + service_level: 0.96 + vlt: 1 + SKU245: + cost: 39 + init_stock: 780 + price: 39 + service_level: 0.96 + vlt: 3 + SKU246: + cost: 263 + init_stock: 900 + price: 263 + service_level: 0.96 + vlt: 3 + SKU247: + cost: 224 + init_stock: 520 + price: 224 + service_level: 0.96 + vlt: 2 + SKU248: + cost: 410 + init_stock: 1680 + price: 410 + service_level: 0.96 + vlt: 1 + SKU249: + cost: 64 + init_stock: 1940 + price: 64 + service_level: 0.96 + vlt: 1 + SKU25: + cost: 201 + init_stock: 920 + price: 201 + service_level: 0.96 + vlt: 2 + SKU250: + cost: 259 + init_stock: 900 + price: 259 + service_level: 0.96 + vlt: 2 + SKU251: + cost: 181 + init_stock: 460 + price: 181 + service_level: 0.96 + vlt: 2 + SKU252: + cost: 330 + init_stock: 1300 + price: 330 + service_level: 0.96 + vlt: 1 + SKU253: + cost: 315 + init_stock: 1840 + price: 315 + service_level: 0.96 + vlt: 3 + SKU254: + cost: 407 + init_stock: 980 + price: 407 + service_level: 0.96 + vlt: 2 + SKU255: + cost: 361 + init_stock: 1480 + price: 361 + service_level: 0.96 + vlt: 1 + SKU256: + cost: 67 + init_stock: 940 + price: 67 + service_level: 0.96 + vlt: 3 + SKU257: + cost: 50 + init_stock: 1200 + price: 50 + service_level: 0.96 + vlt: 1 + SKU258: + cost: 465 + init_stock: 160 + price: 465 + service_level: 0.96 + vlt: 1 + SKU259: + cost: 346 + init_stock: 800 + price: 346 + service_level: 0.96 + vlt: 1 + SKU26: + cost: 419 + init_stock: 1300 + price: 419 + service_level: 0.96 + vlt: 3 + SKU260: + cost: 374 + init_stock: 840 + price: 374 + service_level: 0.96 + vlt: 2 + SKU261: + cost: 74 + init_stock: 540 + price: 74 + service_level: 0.96 + vlt: 2 + SKU262: + cost: 456 + init_stock: 1380 + price: 456 + service_level: 0.96 + vlt: 2 + SKU263: + cost: 276 + init_stock: 1080 + price: 276 + service_level: 0.96 + vlt: 3 + SKU264: + cost: 89 + init_stock: 1200 + price: 89 + service_level: 0.96 + vlt: 1 + SKU265: + cost: 148 + init_stock: 1580 + price: 148 + service_level: 0.96 + vlt: 2 + SKU266: + cost: 287 + init_stock: 260 + price: 287 + service_level: 0.96 + vlt: 2 + SKU267: + cost: 31 + init_stock: 1920 + price: 31 + service_level: 0.96 + vlt: 1 + SKU268: + cost: 340 + init_stock: 440 + price: 340 + service_level: 0.96 + vlt: 2 + SKU269: + cost: 479 + init_stock: 1040 + price: 479 + service_level: 0.96 + vlt: 3 + SKU27: + cost: 269 + init_stock: 1780 + price: 269 + service_level: 0.96 + vlt: 3 + SKU270: + cost: 233 + init_stock: 800 + price: 233 + service_level: 0.96 + vlt: 2 + SKU271: + cost: 90 + init_stock: 240 + price: 90 + service_level: 0.96 + vlt: 1 + SKU272: + cost: 488 + init_stock: 600 + price: 488 + service_level: 0.96 + vlt: 3 + SKU273: + cost: 499 + init_stock: 400 + price: 499 + service_level: 0.96 + vlt: 3 + SKU274: + cost: 434 + init_stock: 1860 + price: 434 + service_level: 0.96 + vlt: 2 + SKU275: + cost: 457 + init_stock: 1920 + price: 457 + service_level: 0.96 + vlt: 1 + SKU276: + cost: 179 + init_stock: 660 + price: 179 + service_level: 0.96 + vlt: 2 + SKU277: + cost: 303 + init_stock: 1240 + price: 303 + service_level: 0.96 + vlt: 2 + SKU278: + cost: 301 + init_stock: 960 + price: 301 + service_level: 0.96 + vlt: 3 + SKU279: + cost: 17 + init_stock: 1640 + price: 17 + service_level: 0.96 + vlt: 2 + SKU28: + cost: 399 + init_stock: 120 + price: 399 + service_level: 0.96 + vlt: 1 + SKU280: + cost: 238 + init_stock: 1660 + price: 238 + service_level: 0.96 + vlt: 3 + SKU281: + cost: 13 + init_stock: 1380 + price: 13 + service_level: 0.96 + vlt: 1 + SKU282: + cost: 404 + init_stock: 1680 + price: 404 + service_level: 0.96 + vlt: 1 + SKU283: + cost: 332 + init_stock: 640 + price: 332 + service_level: 0.96 + vlt: 3 + SKU284: + cost: 257 + init_stock: 780 + price: 257 + service_level: 0.96 + vlt: 3 + SKU285: + cost: 53 + init_stock: 120 + price: 53 + service_level: 0.96 + vlt: 2 + SKU286: + cost: 252 + init_stock: 260 + price: 252 + service_level: 0.96 + vlt: 2 + SKU287: + cost: 161 + init_stock: 1880 + price: 161 + service_level: 0.96 + vlt: 3 + SKU288: + cost: 477 + init_stock: 320 + price: 477 + service_level: 0.96 + vlt: 2 + SKU289: + cost: 452 + init_stock: 1900 + price: 452 + service_level: 0.96 + vlt: 2 + SKU29: + cost: 160 + init_stock: 1480 + price: 160 + service_level: 0.96 + vlt: 1 + SKU290: + cost: 493 + init_stock: 560 + price: 493 + service_level: 0.96 + vlt: 2 + SKU291: + cost: 180 + init_stock: 380 + price: 180 + service_level: 0.96 + vlt: 2 + SKU292: + cost: 233 + init_stock: 1900 + price: 233 + service_level: 0.96 + vlt: 2 + SKU293: + cost: 177 + init_stock: 1320 + price: 177 + service_level: 0.96 + vlt: 1 + SKU294: + cost: 356 + init_stock: 540 + price: 356 + service_level: 0.96 + vlt: 2 + SKU295: + cost: 421 + init_stock: 1640 + price: 421 + service_level: 0.96 + vlt: 2 + SKU296: + cost: 414 + init_stock: 940 + price: 414 + service_level: 0.96 + vlt: 3 + SKU297: + cost: 326 + init_stock: 1800 + price: 326 + service_level: 0.96 + vlt: 3 + SKU298: + cost: 122 + init_stock: 920 + price: 122 + service_level: 0.96 + vlt: 3 + SKU299: + cost: 149 + init_stock: 1360 + price: 149 + service_level: 0.96 + vlt: 2 + SKU3: + cost: 161 + init_stock: 100 + price: 161 + service_level: 0.96 + vlt: 1 + SKU30: + cost: 395 + init_stock: 1220 + price: 395 + service_level: 0.96 + vlt: 3 + SKU300: + cost: 250 + init_stock: 780 + price: 250 + service_level: 0.96 + vlt: 1 + SKU301: + cost: 279 + init_stock: 100 + price: 279 + service_level: 0.96 + vlt: 2 + SKU302: + cost: 321 + init_stock: 1480 + price: 321 + service_level: 0.96 + vlt: 2 + SKU303: + cost: 202 + init_stock: 1100 + price: 202 + service_level: 0.96 + vlt: 3 + SKU304: + cost: 393 + init_stock: 1360 + price: 393 + service_level: 0.96 + vlt: 2 + SKU305: + cost: 113 + init_stock: 1340 + price: 113 + service_level: 0.96 + vlt: 1 + SKU306: + cost: 430 + init_stock: 860 + price: 430 + service_level: 0.96 + vlt: 2 + SKU307: + cost: 311 + init_stock: 1620 + price: 311 + service_level: 0.96 + vlt: 3 + SKU308: + cost: 423 + init_stock: 1160 + price: 423 + service_level: 0.96 + vlt: 1 + SKU309: + cost: 448 + init_stock: 1580 + price: 448 + service_level: 0.96 + vlt: 2 + SKU31: + cost: 456 + init_stock: 1520 + price: 456 + service_level: 0.96 + vlt: 2 + SKU310: + cost: 245 + init_stock: 1260 + price: 245 + service_level: 0.96 + vlt: 1 + SKU311: + cost: 452 + init_stock: 1400 + price: 452 + service_level: 0.96 + vlt: 1 + SKU312: + cost: 112 + init_stock: 1460 + price: 112 + service_level: 0.96 + vlt: 3 + SKU313: + cost: 307 + init_stock: 200 + price: 307 + service_level: 0.96 + vlt: 2 + SKU314: + cost: 40 + init_stock: 1520 + price: 40 + service_level: 0.96 + vlt: 2 + SKU315: + cost: 87 + init_stock: 440 + price: 87 + service_level: 0.96 + vlt: 1 + SKU316: + cost: 367 + init_stock: 320 + price: 367 + service_level: 0.96 + vlt: 3 + SKU317: + cost: 62 + init_stock: 260 + price: 62 + service_level: 0.96 + vlt: 1 + SKU318: + cost: 153 + init_stock: 840 + price: 153 + service_level: 0.96 + vlt: 2 + SKU319: + cost: 340 + init_stock: 920 + price: 340 + service_level: 0.96 + vlt: 2 + SKU32: + cost: 223 + init_stock: 1780 + price: 223 + service_level: 0.96 + vlt: 1 + SKU320: + cost: 188 + init_stock: 1680 + price: 188 + service_level: 0.96 + vlt: 1 + SKU321: + cost: 421 + init_stock: 940 + price: 421 + service_level: 0.96 + vlt: 1 + SKU322: + cost: 462 + init_stock: 1520 + price: 462 + service_level: 0.96 + vlt: 1 + SKU323: + cost: 116 + init_stock: 1580 + price: 116 + service_level: 0.96 + vlt: 2 + SKU324: + cost: 396 + init_stock: 400 + price: 396 + service_level: 0.96 + vlt: 3 + SKU325: + cost: 407 + init_stock: 1200 + price: 407 + service_level: 0.96 + vlt: 1 + SKU326: + cost: 400 + init_stock: 240 + price: 400 + service_level: 0.96 + vlt: 3 + SKU327: + cost: 434 + init_stock: 2000 + price: 434 + service_level: 0.96 + vlt: 2 + SKU328: + cost: 179 + init_stock: 420 + price: 179 + service_level: 0.96 + vlt: 1 + SKU329: + cost: 453 + init_stock: 1620 + price: 453 + service_level: 0.96 + vlt: 1 + SKU33: + cost: 113 + init_stock: 1780 + price: 113 + service_level: 0.96 + vlt: 3 + SKU330: + cost: 215 + init_stock: 1840 + price: 215 + service_level: 0.96 + vlt: 1 + SKU331: + cost: 28 + init_stock: 1680 + price: 28 + service_level: 0.96 + vlt: 3 + SKU332: + cost: 172 + init_stock: 1100 + price: 172 + service_level: 0.96 + vlt: 2 + SKU333: + cost: 63 + init_stock: 1340 + price: 63 + service_level: 0.96 + vlt: 1 + SKU334: + cost: 253 + init_stock: 780 + price: 253 + service_level: 0.96 + vlt: 3 + SKU335: + cost: 22 + init_stock: 180 + price: 22 + service_level: 0.96 + vlt: 1 + SKU336: + cost: 263 + init_stock: 580 + price: 263 + service_level: 0.96 + vlt: 3 + SKU337: + cost: 277 + init_stock: 1480 + price: 277 + service_level: 0.96 + vlt: 3 + SKU338: + cost: 181 + init_stock: 680 + price: 181 + service_level: 0.96 + vlt: 3 + SKU339: + cost: 488 + init_stock: 1520 + price: 488 + service_level: 0.96 + vlt: 1 + SKU34: + cost: 226 + init_stock: 880 + price: 226 + service_level: 0.96 + vlt: 2 + SKU340: + cost: 213 + init_stock: 540 + price: 213 + service_level: 0.96 + vlt: 3 + SKU341: + cost: 423 + init_stock: 1780 + price: 423 + service_level: 0.96 + vlt: 1 + SKU342: + cost: 414 + init_stock: 980 + price: 414 + service_level: 0.96 + vlt: 1 + SKU343: + cost: 429 + init_stock: 1040 + price: 429 + service_level: 0.96 + vlt: 1 + SKU344: + cost: 196 + init_stock: 1780 + price: 196 + service_level: 0.96 + vlt: 1 + SKU345: + cost: 451 + init_stock: 1840 + price: 451 + service_level: 0.96 + vlt: 1 + SKU346: + cost: 485 + init_stock: 600 + price: 485 + service_level: 0.96 + vlt: 3 + SKU347: + cost: 30 + init_stock: 1360 + price: 30 + service_level: 0.96 + vlt: 1 + SKU348: + cost: 12 + init_stock: 240 + price: 12 + service_level: 0.96 + vlt: 2 + SKU349: + cost: 168 + init_stock: 880 + price: 168 + service_level: 0.96 + vlt: 2 + SKU35: + cost: 181 + init_stock: 900 + price: 181 + service_level: 0.96 + vlt: 1 + SKU350: + cost: 381 + init_stock: 580 + price: 381 + service_level: 0.96 + vlt: 2 + SKU351: + cost: 333 + init_stock: 1920 + price: 333 + service_level: 0.96 + vlt: 2 + SKU352: + cost: 365 + init_stock: 380 + price: 365 + service_level: 0.96 + vlt: 2 + SKU353: + cost: 133 + init_stock: 1300 + price: 133 + service_level: 0.96 + vlt: 3 + SKU354: + cost: 356 + init_stock: 1240 + price: 356 + service_level: 0.96 + vlt: 3 + SKU355: + cost: 453 + init_stock: 1120 + price: 453 + service_level: 0.96 + vlt: 1 + SKU356: + cost: 336 + init_stock: 1380 + price: 336 + service_level: 0.96 + vlt: 2 + SKU357: + cost: 77 + init_stock: 660 + price: 77 + service_level: 0.96 + vlt: 1 + SKU358: + cost: 340 + init_stock: 1340 + price: 340 + service_level: 0.96 + vlt: 3 + SKU359: + cost: 259 + init_stock: 1280 + price: 259 + service_level: 0.96 + vlt: 2 + SKU36: + cost: 223 + init_stock: 360 + price: 223 + service_level: 0.96 + vlt: 1 + SKU360: + cost: 146 + init_stock: 1080 + price: 146 + service_level: 0.96 + vlt: 3 + SKU361: + cost: 47 + init_stock: 1780 + price: 47 + service_level: 0.96 + vlt: 2 + SKU362: + cost: 429 + init_stock: 1940 + price: 429 + service_level: 0.96 + vlt: 2 + SKU363: + cost: 495 + init_stock: 1600 + price: 495 + service_level: 0.96 + vlt: 3 + SKU364: + cost: 213 + init_stock: 1800 + price: 213 + service_level: 0.96 + vlt: 3 + SKU365: + cost: 116 + init_stock: 240 + price: 116 + service_level: 0.96 + vlt: 1 + SKU366: + cost: 344 + init_stock: 1640 + price: 344 + service_level: 0.96 + vlt: 1 + SKU367: + cost: 197 + init_stock: 300 + price: 197 + service_level: 0.96 + vlt: 1 + SKU368: + cost: 410 + init_stock: 1200 + price: 410 + service_level: 0.96 + vlt: 2 + SKU369: + cost: 187 + init_stock: 1800 + price: 187 + service_level: 0.96 + vlt: 1 + SKU37: + cost: 302 + init_stock: 900 + price: 302 + service_level: 0.96 + vlt: 1 + SKU370: + cost: 210 + init_stock: 1680 + price: 210 + service_level: 0.96 + vlt: 3 + SKU371: + cost: 404 + init_stock: 1380 + price: 404 + service_level: 0.96 + vlt: 3 + SKU372: + cost: 63 + init_stock: 1580 + price: 63 + service_level: 0.96 + vlt: 3 + SKU373: + cost: 106 + init_stock: 480 + price: 106 + service_level: 0.96 + vlt: 1 + SKU374: + cost: 306 + init_stock: 720 + price: 306 + service_level: 0.96 + vlt: 3 + SKU375: + cost: 153 + init_stock: 1020 + price: 153 + service_level: 0.96 + vlt: 3 + SKU376: + cost: 273 + init_stock: 300 + price: 273 + service_level: 0.96 + vlt: 3 + SKU377: + cost: 306 + init_stock: 1500 + price: 306 + service_level: 0.96 + vlt: 3 + SKU378: + cost: 233 + init_stock: 880 + price: 233 + service_level: 0.96 + vlt: 3 + SKU379: + cost: 10 + init_stock: 440 + price: 10 + service_level: 0.96 + vlt: 3 + SKU38: + cost: 355 + init_stock: 1260 + price: 355 + service_level: 0.96 + vlt: 1 + SKU380: + cost: 440 + init_stock: 460 + price: 440 + service_level: 0.96 + vlt: 2 + SKU381: + cost: 402 + init_stock: 1600 + price: 402 + service_level: 0.96 + vlt: 1 + SKU382: + cost: 478 + init_stock: 2000 + price: 478 + service_level: 0.96 + vlt: 1 + SKU383: + cost: 248 + init_stock: 1400 + price: 248 + service_level: 0.96 + vlt: 3 + SKU384: + cost: 338 + init_stock: 640 + price: 338 + service_level: 0.96 + vlt: 3 + SKU385: + cost: 299 + init_stock: 1220 + price: 299 + service_level: 0.96 + vlt: 2 + SKU386: + cost: 281 + init_stock: 1840 + price: 281 + service_level: 0.96 + vlt: 3 + SKU387: + cost: 157 + init_stock: 1880 + price: 157 + service_level: 0.96 + vlt: 1 + SKU388: + cost: 214 + init_stock: 1080 + price: 214 + service_level: 0.96 + vlt: 2 + SKU389: + cost: 129 + init_stock: 480 + price: 129 + service_level: 0.96 + vlt: 2 + SKU39: + cost: 26 + init_stock: 140 + price: 26 + service_level: 0.96 + vlt: 3 + SKU390: + cost: 377 + init_stock: 1900 + price: 377 + service_level: 0.96 + vlt: 3 + SKU391: + cost: 180 + init_stock: 1500 + price: 180 + service_level: 0.96 + vlt: 1 + SKU392: + cost: 106 + init_stock: 1200 + price: 106 + service_level: 0.96 + vlt: 2 + SKU393: + cost: 128 + init_stock: 1040 + price: 128 + service_level: 0.96 + vlt: 3 + SKU394: + cost: 414 + init_stock: 1200 + price: 414 + service_level: 0.96 + vlt: 3 + SKU395: + cost: 416 + init_stock: 1380 + price: 416 + service_level: 0.96 + vlt: 2 + SKU396: + cost: 426 + init_stock: 880 + price: 426 + service_level: 0.96 + vlt: 1 + SKU397: + cost: 271 + init_stock: 300 + price: 271 + service_level: 0.96 + vlt: 3 + SKU398: + cost: 21 + init_stock: 1180 + price: 21 + service_level: 0.96 + vlt: 1 + SKU399: + cost: 269 + init_stock: 980 + price: 269 + service_level: 0.96 + vlt: 1 + SKU4: + cost: 190 + init_stock: 1700 + price: 190 + service_level: 0.96 + vlt: 3 + SKU40: + cost: 463 + init_stock: 1760 + price: 463 + service_level: 0.96 + vlt: 1 + SKU400: + cost: 45 + init_stock: 1060 + price: 45 + service_level: 0.96 + vlt: 1 + SKU401: + cost: 149 + init_stock: 1640 + price: 149 + service_level: 0.96 + vlt: 1 + SKU402: + cost: 396 + init_stock: 1580 + price: 396 + service_level: 0.96 + vlt: 1 + SKU403: + cost: 70 + init_stock: 1160 + price: 70 + service_level: 0.96 + vlt: 1 + SKU404: + cost: 225 + init_stock: 1580 + price: 225 + service_level: 0.96 + vlt: 2 + SKU405: + cost: 31 + init_stock: 440 + price: 31 + service_level: 0.96 + vlt: 3 + SKU406: + cost: 449 + init_stock: 860 + price: 449 + service_level: 0.96 + vlt: 1 + SKU407: + cost: 462 + init_stock: 320 + price: 462 + service_level: 0.96 + vlt: 2 + SKU408: + cost: 364 + init_stock: 540 + price: 364 + service_level: 0.96 + vlt: 1 + SKU409: + cost: 424 + init_stock: 1160 + price: 424 + service_level: 0.96 + vlt: 1 + SKU41: + cost: 56 + init_stock: 460 + price: 56 + service_level: 0.96 + vlt: 1 + SKU410: + cost: 204 + init_stock: 180 + price: 204 + service_level: 0.96 + vlt: 2 + SKU411: + cost: 197 + init_stock: 120 + price: 197 + service_level: 0.96 + vlt: 3 + SKU412: + cost: 140 + init_stock: 260 + price: 140 + service_level: 0.96 + vlt: 1 + SKU413: + cost: 11 + init_stock: 1420 + price: 11 + service_level: 0.96 + vlt: 2 + SKU414: + cost: 492 + init_stock: 400 + price: 492 + service_level: 0.96 + vlt: 3 + SKU415: + cost: 26 + init_stock: 1260 + price: 26 + service_level: 0.96 + vlt: 1 + SKU416: + cost: 151 + init_stock: 100 + price: 151 + service_level: 0.96 + vlt: 2 + SKU417: + cost: 306 + init_stock: 1140 + price: 306 + service_level: 0.96 + vlt: 3 + SKU418: + cost: 154 + init_stock: 840 + price: 154 + service_level: 0.96 + vlt: 2 + SKU419: + cost: 192 + init_stock: 480 + price: 192 + service_level: 0.96 + vlt: 3 + SKU42: + cost: 106 + init_stock: 820 + price: 106 + service_level: 0.96 + vlt: 3 + SKU420: + cost: 79 + init_stock: 1460 + price: 79 + service_level: 0.96 + vlt: 3 + SKU421: + cost: 288 + init_stock: 1280 + price: 288 + service_level: 0.96 + vlt: 1 + SKU422: + cost: 81 + init_stock: 1420 + price: 81 + service_level: 0.96 + vlt: 3 + SKU423: + cost: 48 + init_stock: 1580 + price: 48 + service_level: 0.96 + vlt: 2 + SKU424: + cost: 344 + init_stock: 640 + price: 344 + service_level: 0.96 + vlt: 3 + SKU425: + cost: 155 + init_stock: 520 + price: 155 + service_level: 0.96 + vlt: 2 + SKU426: + cost: 115 + init_stock: 1640 + price: 115 + service_level: 0.96 + vlt: 2 + SKU427: + cost: 132 + init_stock: 100 + price: 132 + service_level: 0.96 + vlt: 1 + SKU428: + cost: 499 + init_stock: 1160 + price: 499 + service_level: 0.96 + vlt: 2 + SKU429: + cost: 147 + init_stock: 320 + price: 147 + service_level: 0.96 + vlt: 3 + SKU43: + cost: 162 + init_stock: 1620 + price: 162 + service_level: 0.96 + vlt: 3 + SKU430: + cost: 330 + init_stock: 580 + price: 330 + service_level: 0.96 + vlt: 2 + SKU431: + cost: 65 + init_stock: 1380 + price: 65 + service_level: 0.96 + vlt: 1 + SKU432: + cost: 195 + init_stock: 1860 + price: 195 + service_level: 0.96 + vlt: 3 + SKU433: + cost: 55 + init_stock: 1720 + price: 55 + service_level: 0.96 + vlt: 2 + SKU434: + cost: 113 + init_stock: 1500 + price: 113 + service_level: 0.96 + vlt: 2 + SKU435: + cost: 256 + init_stock: 300 + price: 256 + service_level: 0.96 + vlt: 2 + SKU436: + cost: 61 + init_stock: 1960 + price: 61 + service_level: 0.96 + vlt: 3 + SKU437: + cost: 367 + init_stock: 840 + price: 367 + service_level: 0.96 + vlt: 2 + SKU438: + cost: 396 + init_stock: 1440 + price: 396 + service_level: 0.96 + vlt: 2 + SKU439: + cost: 99 + init_stock: 1240 + price: 99 + service_level: 0.96 + vlt: 2 + SKU44: + cost: 241 + init_stock: 1180 + price: 241 + service_level: 0.96 + vlt: 3 + SKU440: + cost: 455 + init_stock: 1020 + price: 455 + service_level: 0.96 + vlt: 1 + SKU441: + cost: 217 + init_stock: 1300 + price: 217 + service_level: 0.96 + vlt: 3 + SKU442: + cost: 460 + init_stock: 1160 + price: 460 + service_level: 0.96 + vlt: 1 + SKU443: + cost: 375 + init_stock: 960 + price: 375 + service_level: 0.96 + vlt: 1 + SKU444: + cost: 44 + init_stock: 1960 + price: 44 + service_level: 0.96 + vlt: 3 + SKU445: + cost: 59 + init_stock: 1360 + price: 59 + service_level: 0.96 + vlt: 2 + SKU446: + cost: 183 + init_stock: 400 + price: 183 + service_level: 0.96 + vlt: 3 + SKU447: + cost: 439 + init_stock: 960 + price: 439 + service_level: 0.96 + vlt: 3 + SKU448: + cost: 499 + init_stock: 560 + price: 499 + service_level: 0.96 + vlt: 2 + SKU449: + cost: 333 + init_stock: 1480 + price: 333 + service_level: 0.96 + vlt: 3 + SKU45: + cost: 134 + init_stock: 960 + price: 134 + service_level: 0.96 + vlt: 1 + SKU450: + cost: 296 + init_stock: 620 + price: 296 + service_level: 0.96 + vlt: 2 + SKU451: + cost: 323 + init_stock: 340 + price: 323 + service_level: 0.96 + vlt: 1 + SKU452: + cost: 52 + init_stock: 800 + price: 52 + service_level: 0.96 + vlt: 2 + SKU453: + cost: 21 + init_stock: 1620 + price: 21 + service_level: 0.96 + vlt: 2 + SKU454: + cost: 198 + init_stock: 460 + price: 198 + service_level: 0.96 + vlt: 3 + SKU455: + cost: 11 + init_stock: 1280 + price: 11 + service_level: 0.96 + vlt: 2 + SKU456: + cost: 204 + init_stock: 1660 + price: 204 + service_level: 0.96 + vlt: 1 + SKU457: + cost: 194 + init_stock: 2000 + price: 194 + service_level: 0.96 + vlt: 2 + SKU458: + cost: 110 + init_stock: 220 + price: 110 + service_level: 0.96 + vlt: 3 + SKU459: + cost: 370 + init_stock: 1280 + price: 370 + service_level: 0.96 + vlt: 1 + SKU46: + cost: 90 + init_stock: 1980 + price: 90 + service_level: 0.96 + vlt: 3 + SKU460: + cost: 121 + init_stock: 1620 + price: 121 + service_level: 0.96 + vlt: 1 + SKU461: + cost: 305 + init_stock: 440 + price: 305 + service_level: 0.96 + vlt: 2 + SKU462: + cost: 377 + init_stock: 1320 + price: 377 + service_level: 0.96 + vlt: 1 + SKU463: + cost: 449 + init_stock: 160 + price: 449 + service_level: 0.96 + vlt: 3 + SKU464: + cost: 466 + init_stock: 680 + price: 466 + service_level: 0.96 + vlt: 2 + SKU465: + cost: 86 + init_stock: 1220 + price: 86 + service_level: 0.96 + vlt: 1 + SKU466: + cost: 412 + init_stock: 1880 + price: 412 + service_level: 0.96 + vlt: 1 + SKU467: + cost: 103 + init_stock: 1420 + price: 103 + service_level: 0.96 + vlt: 1 + SKU468: + cost: 453 + init_stock: 1760 + price: 453 + service_level: 0.96 + vlt: 2 + SKU469: + cost: 422 + init_stock: 1860 + price: 422 + service_level: 0.96 + vlt: 1 + SKU47: + cost: 51 + init_stock: 740 + price: 51 + service_level: 0.96 + vlt: 2 + SKU470: + cost: 284 + init_stock: 560 + price: 284 + service_level: 0.96 + vlt: 3 + SKU471: + cost: 487 + init_stock: 460 + price: 487 + service_level: 0.96 + vlt: 1 + SKU472: + cost: 465 + init_stock: 1920 + price: 465 + service_level: 0.96 + vlt: 1 + SKU473: + cost: 271 + init_stock: 520 + price: 271 + service_level: 0.96 + vlt: 2 + SKU474: + cost: 68 + init_stock: 500 + price: 68 + service_level: 0.96 + vlt: 2 + SKU475: + cost: 320 + init_stock: 1440 + price: 320 + service_level: 0.96 + vlt: 1 + SKU476: + cost: 237 + init_stock: 980 + price: 237 + service_level: 0.96 + vlt: 1 + SKU477: + cost: 455 + init_stock: 1380 + price: 455 + service_level: 0.96 + vlt: 2 + SKU478: + cost: 40 + init_stock: 1480 + price: 40 + service_level: 0.96 + vlt: 2 + SKU479: + cost: 295 + init_stock: 860 + price: 295 + service_level: 0.96 + vlt: 2 + SKU48: + cost: 395 + init_stock: 1820 + price: 395 + service_level: 0.96 + vlt: 3 + SKU480: + cost: 94 + init_stock: 1300 + price: 94 + service_level: 0.96 + vlt: 1 + SKU481: + cost: 408 + init_stock: 420 + price: 408 + service_level: 0.96 + vlt: 1 + SKU482: + cost: 198 + init_stock: 1740 + price: 198 + service_level: 0.96 + vlt: 1 + SKU483: + cost: 85 + init_stock: 1640 + price: 85 + service_level: 0.96 + vlt: 2 + SKU484: + cost: 460 + init_stock: 840 + price: 460 + service_level: 0.96 + vlt: 3 + SKU485: + cost: 308 + init_stock: 700 + price: 308 + service_level: 0.96 + vlt: 3 + SKU486: + cost: 84 + init_stock: 1220 + price: 84 + service_level: 0.96 + vlt: 2 + SKU487: + cost: 421 + init_stock: 1240 + price: 421 + service_level: 0.96 + vlt: 2 + SKU488: + cost: 108 + init_stock: 580 + price: 108 + service_level: 0.96 + vlt: 2 + SKU489: + cost: 146 + init_stock: 600 + price: 146 + service_level: 0.96 + vlt: 2 + SKU49: + cost: 320 + init_stock: 880 + price: 320 + service_level: 0.96 + vlt: 2 + SKU490: + cost: 43 + init_stock: 380 + price: 43 + service_level: 0.96 + vlt: 1 + SKU491: + cost: 134 + init_stock: 1780 + price: 134 + service_level: 0.96 + vlt: 2 + SKU492: + cost: 370 + init_stock: 1720 + price: 370 + service_level: 0.96 + vlt: 2 + SKU493: + cost: 344 + init_stock: 400 + price: 344 + service_level: 0.96 + vlt: 2 + SKU494: + cost: 87 + init_stock: 980 + price: 87 + service_level: 0.96 + vlt: 2 + SKU495: + cost: 183 + init_stock: 1700 + price: 183 + service_level: 0.96 + vlt: 3 + SKU496: + cost: 222 + init_stock: 1280 + price: 222 + service_level: 0.96 + vlt: 3 + SKU497: + cost: 453 + init_stock: 720 + price: 453 + service_level: 0.96 + vlt: 2 + SKU498: + cost: 475 + init_stock: 500 + price: 475 + service_level: 0.96 + vlt: 3 + SKU499: + cost: 122 + init_stock: 660 + price: 122 + service_level: 0.96 + vlt: 1 + SKU5: + cost: 121 + init_stock: 1520 + price: 121 + service_level: 0.96 + vlt: 2 + SKU50: + cost: 298 + init_stock: 1140 + price: 298 + service_level: 0.96 + vlt: 2 + SKU500: + cost: 127 + init_stock: 1740 + price: 127 + service_level: 0.96 + vlt: 2 + SKU501: + cost: 328 + init_stock: 1660 + price: 328 + service_level: 0.96 + vlt: 3 + SKU502: + cost: 335 + init_stock: 640 + price: 335 + service_level: 0.96 + vlt: 3 + SKU503: + cost: 86 + init_stock: 120 + price: 86 + service_level: 0.96 + vlt: 1 + SKU504: + cost: 14 + init_stock: 1300 + price: 14 + service_level: 0.96 + vlt: 1 + SKU505: + cost: 74 + init_stock: 1580 + price: 74 + service_level: 0.96 + vlt: 3 + SKU506: + cost: 51 + init_stock: 1000 + price: 51 + service_level: 0.96 + vlt: 2 + SKU507: + cost: 37 + init_stock: 340 + price: 37 + service_level: 0.96 + vlt: 1 + SKU508: + cost: 445 + init_stock: 1900 + price: 445 + service_level: 0.96 + vlt: 2 + SKU509: + cost: 314 + init_stock: 280 + price: 314 + service_level: 0.96 + vlt: 2 + SKU51: + cost: 329 + init_stock: 660 + price: 329 + service_level: 0.96 + vlt: 2 + SKU510: + cost: 230 + init_stock: 1240 + price: 230 + service_level: 0.96 + vlt: 3 + SKU511: + cost: 21 + init_stock: 680 + price: 21 + service_level: 0.96 + vlt: 1 + SKU512: + cost: 424 + init_stock: 660 + price: 424 + service_level: 0.96 + vlt: 1 + SKU513: + cost: 27 + init_stock: 120 + price: 27 + service_level: 0.96 + vlt: 1 + SKU514: + cost: 78 + init_stock: 1020 + price: 78 + service_level: 0.96 + vlt: 3 + SKU515: + cost: 490 + init_stock: 1840 + price: 490 + service_level: 0.96 + vlt: 3 + SKU516: + cost: 103 + init_stock: 300 + price: 103 + service_level: 0.96 + vlt: 1 + SKU517: + cost: 198 + init_stock: 820 + price: 198 + service_level: 0.96 + vlt: 2 + SKU518: + cost: 492 + init_stock: 300 + price: 492 + service_level: 0.96 + vlt: 2 + SKU519: + cost: 490 + init_stock: 780 + price: 490 + service_level: 0.96 + vlt: 1 + SKU52: + cost: 183 + init_stock: 1260 + price: 183 + service_level: 0.96 + vlt: 1 + SKU520: + cost: 400 + init_stock: 1720 + price: 400 + service_level: 0.96 + vlt: 3 + SKU521: + cost: 212 + init_stock: 1240 + price: 212 + service_level: 0.96 + vlt: 3 + SKU522: + cost: 328 + init_stock: 780 + price: 328 + service_level: 0.96 + vlt: 3 + SKU523: + cost: 23 + init_stock: 1000 + price: 23 + service_level: 0.96 + vlt: 2 + SKU524: + cost: 92 + init_stock: 1500 + price: 92 + service_level: 0.96 + vlt: 1 + SKU525: + cost: 261 + init_stock: 1680 + price: 261 + service_level: 0.96 + vlt: 3 + SKU526: + cost: 126 + init_stock: 740 + price: 126 + service_level: 0.96 + vlt: 3 + SKU527: + cost: 42 + init_stock: 280 + price: 42 + service_level: 0.96 + vlt: 3 + SKU528: + cost: 22 + init_stock: 1800 + price: 22 + service_level: 0.96 + vlt: 2 + SKU529: + cost: 271 + init_stock: 1620 + price: 271 + service_level: 0.96 + vlt: 1 + SKU53: + cost: 438 + init_stock: 1540 + price: 438 + service_level: 0.96 + vlt: 2 + SKU530: + cost: 64 + init_stock: 1300 + price: 64 + service_level: 0.96 + vlt: 3 + SKU531: + cost: 273 + init_stock: 1220 + price: 273 + service_level: 0.96 + vlt: 2 + SKU532: + cost: 436 + init_stock: 1560 + price: 436 + service_level: 0.96 + vlt: 2 + SKU533: + cost: 405 + init_stock: 420 + price: 405 + service_level: 0.96 + vlt: 3 + SKU534: + cost: 375 + init_stock: 1660 + price: 375 + service_level: 0.96 + vlt: 3 + SKU535: + cost: 423 + init_stock: 1560 + price: 423 + service_level: 0.96 + vlt: 2 + SKU536: + cost: 296 + init_stock: 1160 + price: 296 + service_level: 0.96 + vlt: 1 + SKU537: + cost: 476 + init_stock: 580 + price: 476 + service_level: 0.96 + vlt: 2 + SKU538: + cost: 451 + init_stock: 460 + price: 451 + service_level: 0.96 + vlt: 2 + SKU539: + cost: 372 + init_stock: 880 + price: 372 + service_level: 0.96 + vlt: 1 + SKU54: + cost: 141 + init_stock: 1180 + price: 141 + service_level: 0.96 + vlt: 1 + SKU540: + cost: 25 + init_stock: 1220 + price: 25 + service_level: 0.96 + vlt: 2 + SKU541: + cost: 230 + init_stock: 980 + price: 230 + service_level: 0.96 + vlt: 1 + SKU542: + cost: 314 + init_stock: 940 + price: 314 + service_level: 0.96 + vlt: 1 + SKU543: + cost: 421 + init_stock: 1860 + price: 421 + service_level: 0.96 + vlt: 3 + SKU544: + cost: 245 + init_stock: 280 + price: 245 + service_level: 0.96 + vlt: 2 + SKU545: + cost: 324 + init_stock: 600 + price: 324 + service_level: 0.96 + vlt: 2 + SKU546: + cost: 244 + init_stock: 200 + price: 244 + service_level: 0.96 + vlt: 2 + SKU547: + cost: 224 + init_stock: 360 + price: 224 + service_level: 0.96 + vlt: 2 + SKU548: + cost: 181 + init_stock: 580 + price: 181 + service_level: 0.96 + vlt: 3 + SKU549: + cost: 380 + init_stock: 1680 + price: 380 + service_level: 0.96 + vlt: 3 + SKU55: + cost: 303 + init_stock: 1780 + price: 303 + service_level: 0.96 + vlt: 1 + SKU550: + cost: 340 + init_stock: 440 + price: 340 + service_level: 0.96 + vlt: 3 + SKU551: + cost: 455 + init_stock: 960 + price: 455 + service_level: 0.96 + vlt: 2 + SKU552: + cost: 242 + init_stock: 1500 + price: 242 + service_level: 0.96 + vlt: 3 + SKU553: + cost: 221 + init_stock: 920 + price: 221 + service_level: 0.96 + vlt: 3 + SKU554: + cost: 48 + init_stock: 1300 + price: 48 + service_level: 0.96 + vlt: 1 + SKU555: + cost: 208 + init_stock: 1060 + price: 208 + service_level: 0.96 + vlt: 3 + SKU556: + cost: 496 + init_stock: 920 + price: 496 + service_level: 0.96 + vlt: 3 + SKU557: + cost: 275 + init_stock: 1900 + price: 275 + service_level: 0.96 + vlt: 2 + SKU558: + cost: 342 + init_stock: 1460 + price: 342 + service_level: 0.96 + vlt: 2 + SKU559: + cost: 215 + init_stock: 1920 + price: 215 + service_level: 0.96 + vlt: 3 + SKU56: + cost: 446 + init_stock: 2000 + price: 446 + service_level: 0.96 + vlt: 2 + SKU560: + cost: 363 + init_stock: 120 + price: 363 + service_level: 0.96 + vlt: 3 + SKU561: + cost: 158 + init_stock: 740 + price: 158 + service_level: 0.96 + vlt: 3 + SKU562: + cost: 260 + init_stock: 1320 + price: 260 + service_level: 0.96 + vlt: 2 + SKU563: + cost: 465 + init_stock: 420 + price: 465 + service_level: 0.96 + vlt: 3 + SKU564: + cost: 356 + init_stock: 220 + price: 356 + service_level: 0.96 + vlt: 1 + SKU565: + cost: 479 + init_stock: 960 + price: 479 + service_level: 0.96 + vlt: 1 + SKU566: + cost: 441 + init_stock: 1380 + price: 441 + service_level: 0.96 + vlt: 3 + SKU567: + cost: 88 + init_stock: 1160 + price: 88 + service_level: 0.96 + vlt: 3 + SKU568: + cost: 210 + init_stock: 540 + price: 210 + service_level: 0.96 + vlt: 3 + SKU569: + cost: 432 + init_stock: 840 + price: 432 + service_level: 0.96 + vlt: 3 + SKU57: + cost: 262 + init_stock: 1400 + price: 262 + service_level: 0.96 + vlt: 3 + SKU570: + cost: 143 + init_stock: 200 + price: 143 + service_level: 0.96 + vlt: 3 + SKU571: + cost: 204 + init_stock: 1280 + price: 204 + service_level: 0.96 + vlt: 1 + SKU572: + cost: 291 + init_stock: 1140 + price: 291 + service_level: 0.96 + vlt: 2 + SKU573: + cost: 458 + init_stock: 500 + price: 458 + service_level: 0.96 + vlt: 1 + SKU574: + cost: 243 + init_stock: 1880 + price: 243 + service_level: 0.96 + vlt: 2 + SKU575: + cost: 328 + init_stock: 1440 + price: 328 + service_level: 0.96 + vlt: 1 + SKU576: + cost: 333 + init_stock: 1400 + price: 333 + service_level: 0.96 + vlt: 3 + SKU577: + cost: 368 + init_stock: 300 + price: 368 + service_level: 0.96 + vlt: 2 + SKU578: + cost: 82 + init_stock: 1200 + price: 82 + service_level: 0.96 + vlt: 2 + SKU579: + cost: 372 + init_stock: 380 + price: 372 + service_level: 0.96 + vlt: 2 + SKU58: + cost: 409 + init_stock: 1340 + price: 409 + service_level: 0.96 + vlt: 3 + SKU580: + cost: 421 + init_stock: 1080 + price: 421 + service_level: 0.96 + vlt: 3 + SKU581: + cost: 290 + init_stock: 1140 + price: 290 + service_level: 0.96 + vlt: 3 + SKU582: + cost: 130 + init_stock: 100 + price: 130 + service_level: 0.96 + vlt: 3 + SKU583: + cost: 172 + init_stock: 560 + price: 172 + service_level: 0.96 + vlt: 3 + SKU584: + cost: 173 + init_stock: 1700 + price: 173 + service_level: 0.96 + vlt: 2 + SKU585: + cost: 201 + init_stock: 1580 + price: 201 + service_level: 0.96 + vlt: 3 + SKU586: + cost: 150 + init_stock: 1040 + price: 150 + service_level: 0.96 + vlt: 1 + SKU587: + cost: 198 + init_stock: 720 + price: 198 + service_level: 0.96 + vlt: 2 + SKU588: + cost: 250 + init_stock: 1180 + price: 250 + service_level: 0.96 + vlt: 1 + SKU589: + cost: 51 + init_stock: 1820 + price: 51 + service_level: 0.96 + vlt: 1 + SKU59: + cost: 11 + init_stock: 240 + price: 11 + service_level: 0.96 + vlt: 2 + SKU590: + cost: 492 + init_stock: 360 + price: 492 + service_level: 0.96 + vlt: 2 + SKU591: + cost: 266 + init_stock: 1800 + price: 266 + service_level: 0.96 + vlt: 3 + SKU592: + cost: 293 + init_stock: 1580 + price: 293 + service_level: 0.96 + vlt: 3 + SKU593: + cost: 361 + init_stock: 1980 + price: 361 + service_level: 0.96 + vlt: 3 + SKU594: + cost: 234 + init_stock: 260 + price: 234 + service_level: 0.96 + vlt: 3 + SKU595: + cost: 37 + init_stock: 880 + price: 37 + service_level: 0.96 + vlt: 2 + SKU596: + cost: 245 + init_stock: 1620 + price: 245 + service_level: 0.96 + vlt: 3 + SKU597: + cost: 27 + init_stock: 780 + price: 27 + service_level: 0.96 + vlt: 3 + SKU598: + cost: 290 + init_stock: 920 + price: 290 + service_level: 0.96 + vlt: 1 + SKU599: + cost: 449 + init_stock: 820 + price: 449 + service_level: 0.96 + vlt: 2 + SKU6: + cost: 90 + init_stock: 340 + price: 90 + service_level: 0.96 + vlt: 3 + SKU60: + cost: 478 + init_stock: 1780 + price: 478 + service_level: 0.96 + vlt: 2 + SKU600: + cost: 411 + init_stock: 480 + price: 411 + service_level: 0.96 + vlt: 2 + SKU601: + cost: 363 + init_stock: 900 + price: 363 + service_level: 0.96 + vlt: 1 + SKU602: + cost: 352 + init_stock: 980 + price: 352 + service_level: 0.96 + vlt: 3 + SKU603: + cost: 36 + init_stock: 1180 + price: 36 + service_level: 0.96 + vlt: 3 + SKU604: + cost: 330 + init_stock: 1560 + price: 330 + service_level: 0.96 + vlt: 3 + SKU605: + cost: 420 + init_stock: 1860 + price: 420 + service_level: 0.96 + vlt: 3 + SKU606: + cost: 424 + init_stock: 1280 + price: 424 + service_level: 0.96 + vlt: 1 + SKU607: + cost: 144 + init_stock: 220 + price: 144 + service_level: 0.96 + vlt: 2 + SKU608: + cost: 35 + init_stock: 880 + price: 35 + service_level: 0.96 + vlt: 2 + SKU609: + cost: 157 + init_stock: 800 + price: 157 + service_level: 0.96 + vlt: 2 + SKU61: + cost: 317 + init_stock: 1660 + price: 317 + service_level: 0.96 + vlt: 2 + SKU610: + cost: 62 + init_stock: 240 + price: 62 + service_level: 0.96 + vlt: 3 + SKU611: + cost: 258 + init_stock: 1820 + price: 258 + service_level: 0.96 + vlt: 1 + SKU612: + cost: 57 + init_stock: 1440 + price: 57 + service_level: 0.96 + vlt: 3 + SKU613: + cost: 443 + init_stock: 860 + price: 443 + service_level: 0.96 + vlt: 2 + SKU614: + cost: 248 + init_stock: 1720 + price: 248 + service_level: 0.96 + vlt: 2 + SKU615: + cost: 259 + init_stock: 1200 + price: 259 + service_level: 0.96 + vlt: 1 + SKU616: + cost: 188 + init_stock: 1760 + price: 188 + service_level: 0.96 + vlt: 2 + SKU617: + cost: 334 + init_stock: 820 + price: 334 + service_level: 0.96 + vlt: 1 + SKU618: + cost: 87 + init_stock: 620 + price: 87 + service_level: 0.96 + vlt: 3 + SKU619: + cost: 215 + init_stock: 700 + price: 215 + service_level: 0.96 + vlt: 2 + SKU62: + cost: 157 + init_stock: 1460 + price: 157 + service_level: 0.96 + vlt: 1 + SKU620: + cost: 12 + init_stock: 2000 + price: 12 + service_level: 0.96 + vlt: 2 + SKU621: + cost: 363 + init_stock: 320 + price: 363 + service_level: 0.96 + vlt: 2 + SKU622: + cost: 152 + init_stock: 1460 + price: 152 + service_level: 0.96 + vlt: 1 + SKU623: + cost: 263 + init_stock: 200 + price: 263 + service_level: 0.96 + vlt: 2 + SKU624: + cost: 423 + init_stock: 1920 + price: 423 + service_level: 0.96 + vlt: 2 + SKU625: + cost: 294 + init_stock: 1960 + price: 294 + service_level: 0.96 + vlt: 1 + SKU626: + cost: 204 + init_stock: 1160 + price: 204 + service_level: 0.96 + vlt: 1 + SKU627: + cost: 175 + init_stock: 1760 + price: 175 + service_level: 0.96 + vlt: 1 + SKU628: + cost: 14 + init_stock: 1640 + price: 14 + service_level: 0.96 + vlt: 1 + SKU629: + cost: 352 + init_stock: 1500 + price: 352 + service_level: 0.96 + vlt: 1 + SKU63: + cost: 305 + init_stock: 120 + price: 305 + service_level: 0.96 + vlt: 2 + SKU630: + cost: 407 + init_stock: 1580 + price: 407 + service_level: 0.96 + vlt: 1 + SKU631: + cost: 370 + init_stock: 660 + price: 370 + service_level: 0.96 + vlt: 2 + SKU632: + cost: 489 + init_stock: 300 + price: 489 + service_level: 0.96 + vlt: 3 + SKU633: + cost: 298 + init_stock: 1300 + price: 298 + service_level: 0.96 + vlt: 1 + SKU634: + cost: 52 + init_stock: 1480 + price: 52 + service_level: 0.96 + vlt: 3 + SKU635: + cost: 30 + init_stock: 1940 + price: 30 + service_level: 0.96 + vlt: 1 + SKU636: + cost: 38 + init_stock: 1000 + price: 38 + service_level: 0.96 + vlt: 2 + SKU637: + cost: 474 + init_stock: 820 + price: 474 + service_level: 0.96 + vlt: 2 + SKU638: + cost: 267 + init_stock: 1080 + price: 267 + service_level: 0.96 + vlt: 3 + SKU639: + cost: 466 + init_stock: 520 + price: 466 + service_level: 0.96 + vlt: 2 + SKU64: + cost: 184 + init_stock: 1080 + price: 184 + service_level: 0.96 + vlt: 1 + SKU640: + cost: 236 + init_stock: 360 + price: 236 + service_level: 0.96 + vlt: 2 + SKU641: + cost: 285 + init_stock: 1220 + price: 285 + service_level: 0.96 + vlt: 2 + SKU642: + cost: 133 + init_stock: 1680 + price: 133 + service_level: 0.96 + vlt: 3 + SKU643: + cost: 356 + init_stock: 2000 + price: 356 + service_level: 0.96 + vlt: 3 + SKU644: + cost: 499 + init_stock: 700 + price: 499 + service_level: 0.96 + vlt: 1 + SKU645: + cost: 476 + init_stock: 1880 + price: 476 + service_level: 0.96 + vlt: 3 + SKU646: + cost: 78 + init_stock: 760 + price: 78 + service_level: 0.96 + vlt: 1 + SKU647: + cost: 52 + init_stock: 1180 + price: 52 + service_level: 0.96 + vlt: 1 + SKU648: + cost: 468 + init_stock: 1660 + price: 468 + service_level: 0.96 + vlt: 2 + SKU649: + cost: 345 + init_stock: 1400 + price: 345 + service_level: 0.96 + vlt: 2 + SKU65: + cost: 385 + init_stock: 540 + price: 385 + service_level: 0.96 + vlt: 2 + SKU650: + cost: 130 + init_stock: 320 + price: 130 + service_level: 0.96 + vlt: 2 + SKU651: + cost: 248 + init_stock: 440 + price: 248 + service_level: 0.96 + vlt: 3 + SKU652: + cost: 100 + init_stock: 1840 + price: 100 + service_level: 0.96 + vlt: 1 + SKU653: + cost: 453 + init_stock: 1580 + price: 453 + service_level: 0.96 + vlt: 1 + SKU654: + cost: 441 + init_stock: 620 + price: 441 + service_level: 0.96 + vlt: 3 + SKU655: + cost: 146 + init_stock: 1080 + price: 146 + service_level: 0.96 + vlt: 1 + SKU656: + cost: 367 + init_stock: 760 + price: 367 + service_level: 0.96 + vlt: 3 + SKU657: + cost: 195 + init_stock: 480 + price: 195 + service_level: 0.96 + vlt: 1 + SKU658: + cost: 480 + init_stock: 1740 + price: 480 + service_level: 0.96 + vlt: 2 + SKU659: + cost: 497 + init_stock: 1920 + price: 497 + service_level: 0.96 + vlt: 3 + SKU66: + cost: 37 + init_stock: 1460 + price: 37 + service_level: 0.96 + vlt: 2 + SKU660: + cost: 163 + init_stock: 1320 + price: 163 + service_level: 0.96 + vlt: 3 + SKU661: + cost: 389 + init_stock: 240 + price: 389 + service_level: 0.96 + vlt: 2 + SKU662: + cost: 495 + init_stock: 1640 + price: 495 + service_level: 0.96 + vlt: 2 + SKU663: + cost: 460 + init_stock: 1300 + price: 460 + service_level: 0.96 + vlt: 3 + SKU664: + cost: 397 + init_stock: 240 + price: 397 + service_level: 0.96 + vlt: 3 + SKU665: + cost: 67 + init_stock: 1420 + price: 67 + service_level: 0.96 + vlt: 1 + SKU666: + cost: 126 + init_stock: 240 + price: 126 + service_level: 0.96 + vlt: 1 + SKU667: + cost: 140 + init_stock: 140 + price: 140 + service_level: 0.96 + vlt: 3 + SKU668: + cost: 110 + init_stock: 1980 + price: 110 + service_level: 0.96 + vlt: 1 + SKU669: + cost: 121 + init_stock: 1340 + price: 121 + service_level: 0.96 + vlt: 1 + SKU67: + cost: 54 + init_stock: 1720 + price: 54 + service_level: 0.96 + vlt: 3 + SKU670: + cost: 495 + init_stock: 100 + price: 495 + service_level: 0.96 + vlt: 1 + SKU671: + cost: 444 + init_stock: 200 + price: 444 + service_level: 0.96 + vlt: 3 + SKU672: + cost: 426 + init_stock: 1560 + price: 426 + service_level: 0.96 + vlt: 3 + SKU673: + cost: 109 + init_stock: 1080 + price: 109 + service_level: 0.96 + vlt: 1 + SKU674: + cost: 392 + init_stock: 160 + price: 392 + service_level: 0.96 + vlt: 3 + SKU675: + cost: 135 + init_stock: 660 + price: 135 + service_level: 0.96 + vlt: 3 + SKU676: + cost: 401 + init_stock: 1700 + price: 401 + service_level: 0.96 + vlt: 2 + SKU677: + cost: 449 + init_stock: 1000 + price: 449 + service_level: 0.96 + vlt: 2 + SKU678: + cost: 208 + init_stock: 1220 + price: 208 + service_level: 0.96 + vlt: 3 + SKU679: + cost: 356 + init_stock: 1660 + price: 356 + service_level: 0.96 + vlt: 1 + SKU68: + cost: 63 + init_stock: 1480 + price: 63 + service_level: 0.96 + vlt: 2 + SKU680: + cost: 287 + init_stock: 520 + price: 287 + service_level: 0.96 + vlt: 3 + SKU681: + cost: 140 + init_stock: 1400 + price: 140 + service_level: 0.96 + vlt: 2 + SKU682: + cost: 163 + init_stock: 1720 + price: 163 + service_level: 0.96 + vlt: 2 + SKU683: + cost: 359 + init_stock: 1520 + price: 359 + service_level: 0.96 + vlt: 1 + SKU684: + cost: 37 + init_stock: 620 + price: 37 + service_level: 0.96 + vlt: 1 + SKU685: + cost: 106 + init_stock: 640 + price: 106 + service_level: 0.96 + vlt: 3 + SKU686: + cost: 52 + init_stock: 1160 + price: 52 + service_level: 0.96 + vlt: 1 + SKU687: + cost: 85 + init_stock: 1180 + price: 85 + service_level: 0.96 + vlt: 3 + SKU688: + cost: 148 + init_stock: 560 + price: 148 + service_level: 0.96 + vlt: 1 + SKU689: + cost: 333 + init_stock: 660 + price: 333 + service_level: 0.96 + vlt: 3 + SKU69: + cost: 65 + init_stock: 1340 + price: 65 + service_level: 0.96 + vlt: 3 + SKU690: + cost: 372 + init_stock: 400 + price: 372 + service_level: 0.96 + vlt: 1 + SKU691: + cost: 275 + init_stock: 480 + price: 275 + service_level: 0.96 + vlt: 1 + SKU692: + cost: 289 + init_stock: 1600 + price: 289 + service_level: 0.96 + vlt: 1 + SKU693: + cost: 258 + init_stock: 1400 + price: 258 + service_level: 0.96 + vlt: 2 + SKU694: + cost: 472 + init_stock: 360 + price: 472 + service_level: 0.96 + vlt: 2 + SKU695: + cost: 318 + init_stock: 960 + price: 318 + service_level: 0.96 + vlt: 1 + SKU696: + cost: 63 + init_stock: 1840 + price: 63 + service_level: 0.96 + vlt: 1 + SKU697: + cost: 15 + init_stock: 1080 + price: 15 + service_level: 0.96 + vlt: 1 + SKU698: + cost: 14 + init_stock: 1020 + price: 14 + service_level: 0.96 + vlt: 3 + SKU699: + cost: 357 + init_stock: 1460 + price: 357 + service_level: 0.96 + vlt: 3 + SKU7: + cost: 314 + init_stock: 860 + price: 314 + service_level: 0.96 + vlt: 1 + SKU70: + cost: 207 + init_stock: 1540 + price: 207 + service_level: 0.96 + vlt: 3 + SKU700: + cost: 21 + init_stock: 1080 + price: 21 + service_level: 0.96 + vlt: 1 + SKU701: + cost: 130 + init_stock: 1580 + price: 130 + service_level: 0.96 + vlt: 1 + SKU702: + cost: 227 + init_stock: 1380 + price: 227 + service_level: 0.96 + vlt: 2 + SKU703: + cost: 73 + init_stock: 700 + price: 73 + service_level: 0.96 + vlt: 3 + SKU704: + cost: 37 + init_stock: 520 + price: 37 + service_level: 0.96 + vlt: 1 + SKU705: + cost: 313 + init_stock: 420 + price: 313 + service_level: 0.96 + vlt: 1 + SKU706: + cost: 378 + init_stock: 280 + price: 378 + service_level: 0.96 + vlt: 1 + SKU707: + cost: 302 + init_stock: 2000 + price: 302 + service_level: 0.96 + vlt: 3 + SKU708: + cost: 69 + init_stock: 1160 + price: 69 + service_level: 0.96 + vlt: 3 + SKU709: + cost: 63 + init_stock: 1560 + price: 63 + service_level: 0.96 + vlt: 1 + SKU71: + cost: 251 + init_stock: 820 + price: 251 + service_level: 0.96 + vlt: 1 + SKU710: + cost: 477 + init_stock: 1240 + price: 477 + service_level: 0.96 + vlt: 1 + SKU711: + cost: 395 + init_stock: 1960 + price: 395 + service_level: 0.96 + vlt: 1 + SKU712: + cost: 50 + init_stock: 1360 + price: 50 + service_level: 0.96 + vlt: 2 + SKU713: + cost: 224 + init_stock: 500 + price: 224 + service_level: 0.96 + vlt: 1 + SKU714: + cost: 98 + init_stock: 840 + price: 98 + service_level: 0.96 + vlt: 1 + SKU715: + cost: 499 + init_stock: 660 + price: 499 + service_level: 0.96 + vlt: 1 + SKU716: + cost: 468 + init_stock: 640 + price: 468 + service_level: 0.96 + vlt: 1 + SKU717: + cost: 115 + init_stock: 500 + price: 115 + service_level: 0.96 + vlt: 2 + SKU718: + cost: 284 + init_stock: 700 + price: 284 + service_level: 0.96 + vlt: 1 + SKU719: + cost: 167 + init_stock: 240 + price: 167 + service_level: 0.96 + vlt: 1 + SKU72: + cost: 77 + init_stock: 1460 + price: 77 + service_level: 0.96 + vlt: 2 + SKU720: + cost: 382 + init_stock: 100 + price: 382 + service_level: 0.96 + vlt: 3 + SKU721: + cost: 369 + init_stock: 1080 + price: 369 + service_level: 0.96 + vlt: 3 + SKU722: + cost: 465 + init_stock: 1400 + price: 465 + service_level: 0.96 + vlt: 2 + SKU723: + cost: 43 + init_stock: 960 + price: 43 + service_level: 0.96 + vlt: 1 + SKU724: + cost: 246 + init_stock: 1840 + price: 246 + service_level: 0.96 + vlt: 1 + SKU725: + cost: 174 + init_stock: 620 + price: 174 + service_level: 0.96 + vlt: 1 + SKU726: + cost: 391 + init_stock: 1060 + price: 391 + service_level: 0.96 + vlt: 2 + SKU727: + cost: 447 + init_stock: 1220 + price: 447 + service_level: 0.96 + vlt: 2 + SKU728: + cost: 347 + init_stock: 600 + price: 347 + service_level: 0.96 + vlt: 2 + SKU729: + cost: 463 + init_stock: 1660 + price: 463 + service_level: 0.96 + vlt: 1 + SKU73: + cost: 163 + init_stock: 660 + price: 163 + service_level: 0.96 + vlt: 2 + SKU730: + cost: 211 + init_stock: 300 + price: 211 + service_level: 0.96 + vlt: 2 + SKU731: + cost: 57 + init_stock: 1080 + price: 57 + service_level: 0.96 + vlt: 3 + SKU732: + cost: 106 + init_stock: 480 + price: 106 + service_level: 0.96 + vlt: 2 + SKU733: + cost: 302 + init_stock: 380 + price: 302 + service_level: 0.96 + vlt: 3 + SKU734: + cost: 386 + init_stock: 280 + price: 386 + service_level: 0.96 + vlt: 3 + SKU735: + cost: 85 + init_stock: 780 + price: 85 + service_level: 0.96 + vlt: 3 + SKU736: + cost: 16 + init_stock: 1340 + price: 16 + service_level: 0.96 + vlt: 3 + SKU737: + cost: 382 + init_stock: 1840 + price: 382 + service_level: 0.96 + vlt: 3 + SKU738: + cost: 429 + init_stock: 180 + price: 429 + service_level: 0.96 + vlt: 2 + SKU739: + cost: 285 + init_stock: 900 + price: 285 + service_level: 0.96 + vlt: 1 + SKU74: + cost: 47 + init_stock: 1980 + price: 47 + service_level: 0.96 + vlt: 3 + SKU740: + cost: 475 + init_stock: 1160 + price: 475 + service_level: 0.96 + vlt: 3 + SKU741: + cost: 466 + init_stock: 1420 + price: 466 + service_level: 0.96 + vlt: 1 + SKU742: + cost: 442 + init_stock: 540 + price: 442 + service_level: 0.96 + vlt: 3 + SKU743: + cost: 83 + init_stock: 400 + price: 83 + service_level: 0.96 + vlt: 2 + SKU744: + cost: 340 + init_stock: 1440 + price: 340 + service_level: 0.96 + vlt: 3 + SKU745: + cost: 444 + init_stock: 1000 + price: 444 + service_level: 0.96 + vlt: 2 + SKU746: + cost: 496 + init_stock: 1920 + price: 496 + service_level: 0.96 + vlt: 1 + SKU747: + cost: 175 + init_stock: 1200 + price: 175 + service_level: 0.96 + vlt: 2 + SKU748: + cost: 461 + init_stock: 140 + price: 461 + service_level: 0.96 + vlt: 3 + SKU749: + cost: 481 + init_stock: 340 + price: 481 + service_level: 0.96 + vlt: 2 + SKU75: + cost: 319 + init_stock: 640 + price: 319 + service_level: 0.96 + vlt: 2 + SKU750: + cost: 240 + init_stock: 900 + price: 240 + service_level: 0.96 + vlt: 3 + SKU751: + cost: 302 + init_stock: 180 + price: 302 + service_level: 0.96 + vlt: 3 + SKU752: + cost: 425 + init_stock: 660 + price: 425 + service_level: 0.96 + vlt: 2 + SKU753: + cost: 105 + init_stock: 880 + price: 105 + service_level: 0.96 + vlt: 3 + SKU754: + cost: 495 + init_stock: 1860 + price: 495 + service_level: 0.96 + vlt: 1 + SKU755: + cost: 436 + init_stock: 620 + price: 436 + service_level: 0.96 + vlt: 3 + SKU756: + cost: 234 + init_stock: 1900 + price: 234 + service_level: 0.96 + vlt: 3 + SKU757: + cost: 313 + init_stock: 260 + price: 313 + service_level: 0.96 + vlt: 3 + SKU758: + cost: 319 + init_stock: 420 + price: 319 + service_level: 0.96 + vlt: 3 + SKU759: + cost: 313 + init_stock: 820 + price: 313 + service_level: 0.96 + vlt: 2 + SKU76: + cost: 242 + init_stock: 1340 + price: 242 + service_level: 0.96 + vlt: 2 + SKU760: + cost: 358 + init_stock: 520 + price: 358 + service_level: 0.96 + vlt: 1 + SKU761: + cost: 487 + init_stock: 320 + price: 487 + service_level: 0.96 + vlt: 3 + SKU762: + cost: 412 + init_stock: 100 + price: 412 + service_level: 0.96 + vlt: 1 + SKU763: + cost: 391 + init_stock: 760 + price: 391 + service_level: 0.96 + vlt: 1 + SKU764: + cost: 127 + init_stock: 980 + price: 127 + service_level: 0.96 + vlt: 1 + SKU765: + cost: 271 + init_stock: 500 + price: 271 + service_level: 0.96 + vlt: 3 + SKU766: + cost: 230 + init_stock: 460 + price: 230 + service_level: 0.96 + vlt: 1 + SKU767: + cost: 91 + init_stock: 1600 + price: 91 + service_level: 0.96 + vlt: 1 + SKU768: + cost: 410 + init_stock: 2000 + price: 410 + service_level: 0.96 + vlt: 3 + SKU769: + cost: 324 + init_stock: 820 + price: 324 + service_level: 0.96 + vlt: 2 + SKU77: + cost: 321 + init_stock: 680 + price: 321 + service_level: 0.96 + vlt: 1 + SKU770: + cost: 288 + init_stock: 960 + price: 288 + service_level: 0.96 + vlt: 2 + SKU771: + cost: 53 + init_stock: 200 + price: 53 + service_level: 0.96 + vlt: 2 + SKU772: + cost: 205 + init_stock: 1100 + price: 205 + service_level: 0.96 + vlt: 2 + SKU773: + cost: 69 + init_stock: 540 + price: 69 + service_level: 0.96 + vlt: 1 + SKU774: + cost: 500 + init_stock: 1920 + price: 500 + service_level: 0.96 + vlt: 2 + SKU775: + cost: 366 + init_stock: 860 + price: 366 + service_level: 0.96 + vlt: 2 + SKU776: + cost: 464 + init_stock: 160 + price: 464 + service_level: 0.96 + vlt: 2 + SKU777: + cost: 493 + init_stock: 1080 + price: 493 + service_level: 0.96 + vlt: 2 + SKU778: + cost: 386 + init_stock: 500 + price: 386 + service_level: 0.96 + vlt: 2 + SKU779: + cost: 149 + init_stock: 1240 + price: 149 + service_level: 0.96 + vlt: 2 + SKU78: + cost: 231 + init_stock: 900 + price: 231 + service_level: 0.96 + vlt: 3 + SKU780: + cost: 271 + init_stock: 700 + price: 271 + service_level: 0.96 + vlt: 3 + SKU781: + cost: 84 + init_stock: 1160 + price: 84 + service_level: 0.96 + vlt: 2 + SKU782: + cost: 145 + init_stock: 1400 + price: 145 + service_level: 0.96 + vlt: 2 + SKU783: + cost: 191 + init_stock: 660 + price: 191 + service_level: 0.96 + vlt: 1 + SKU784: + cost: 34 + init_stock: 360 + price: 34 + service_level: 0.96 + vlt: 1 + SKU785: + cost: 368 + init_stock: 560 + price: 368 + service_level: 0.96 + vlt: 3 + SKU786: + cost: 55 + init_stock: 460 + price: 55 + service_level: 0.96 + vlt: 2 + SKU787: + cost: 368 + init_stock: 120 + price: 368 + service_level: 0.96 + vlt: 3 + SKU788: + cost: 257 + init_stock: 200 + price: 257 + service_level: 0.96 + vlt: 2 + SKU789: + cost: 218 + init_stock: 1700 + price: 218 + service_level: 0.96 + vlt: 1 + SKU79: + cost: 436 + init_stock: 1460 + price: 436 + service_level: 0.96 + vlt: 2 + SKU790: + cost: 429 + init_stock: 1060 + price: 429 + service_level: 0.96 + vlt: 3 + SKU791: + cost: 55 + init_stock: 1460 + price: 55 + service_level: 0.96 + vlt: 1 + SKU792: + cost: 80 + init_stock: 1400 + price: 80 + service_level: 0.96 + vlt: 3 + SKU793: + cost: 227 + init_stock: 220 + price: 227 + service_level: 0.96 + vlt: 1 + SKU794: + cost: 361 + init_stock: 560 + price: 361 + service_level: 0.96 + vlt: 3 + SKU795: + cost: 200 + init_stock: 960 + price: 200 + service_level: 0.96 + vlt: 1 + SKU796: + cost: 360 + init_stock: 140 + price: 360 + service_level: 0.96 + vlt: 3 + SKU797: + cost: 37 + init_stock: 1740 + price: 37 + service_level: 0.96 + vlt: 1 + SKU798: + cost: 124 + init_stock: 1900 + price: 124 + service_level: 0.96 + vlt: 2 + SKU799: + cost: 93 + init_stock: 360 + price: 93 + service_level: 0.96 + vlt: 1 + SKU8: + cost: 320 + init_stock: 480 + price: 320 + service_level: 0.96 + vlt: 1 + SKU80: + cost: 316 + init_stock: 840 + price: 316 + service_level: 0.96 + vlt: 2 + SKU800: + cost: 264 + init_stock: 1320 + price: 264 + service_level: 0.96 + vlt: 2 + SKU801: + cost: 62 + init_stock: 2000 + price: 62 + service_level: 0.96 + vlt: 3 + SKU802: + cost: 289 + init_stock: 220 + price: 289 + service_level: 0.96 + vlt: 1 + SKU803: + cost: 485 + init_stock: 1220 + price: 485 + service_level: 0.96 + vlt: 3 + SKU804: + cost: 126 + init_stock: 1160 + price: 126 + service_level: 0.96 + vlt: 3 + SKU805: + cost: 227 + init_stock: 1460 + price: 227 + service_level: 0.96 + vlt: 1 + SKU806: + cost: 59 + init_stock: 680 + price: 59 + service_level: 0.96 + vlt: 1 + SKU807: + cost: 468 + init_stock: 1500 + price: 468 + service_level: 0.96 + vlt: 3 + SKU808: + cost: 161 + init_stock: 1080 + price: 161 + service_level: 0.96 + vlt: 3 + SKU809: + cost: 53 + init_stock: 1360 + price: 53 + service_level: 0.96 + vlt: 1 + SKU81: + cost: 192 + init_stock: 1960 + price: 192 + service_level: 0.96 + vlt: 1 + SKU810: + cost: 451 + init_stock: 880 + price: 451 + service_level: 0.96 + vlt: 1 + SKU811: + cost: 320 + init_stock: 320 + price: 320 + service_level: 0.96 + vlt: 1 + SKU812: + cost: 293 + init_stock: 300 + price: 293 + service_level: 0.96 + vlt: 3 + SKU813: + cost: 39 + init_stock: 1580 + price: 39 + service_level: 0.96 + vlt: 2 + SKU814: + cost: 485 + init_stock: 1280 + price: 485 + service_level: 0.96 + vlt: 1 + SKU815: + cost: 53 + init_stock: 720 + price: 53 + service_level: 0.96 + vlt: 3 + SKU816: + cost: 154 + init_stock: 1120 + price: 154 + service_level: 0.96 + vlt: 1 + SKU817: + cost: 262 + init_stock: 1900 + price: 262 + service_level: 0.96 + vlt: 1 + SKU818: + cost: 89 + init_stock: 1520 + price: 89 + service_level: 0.96 + vlt: 3 + SKU819: + cost: 328 + init_stock: 1800 + price: 328 + service_level: 0.96 + vlt: 2 + SKU82: + cost: 237 + init_stock: 1900 + price: 237 + service_level: 0.96 + vlt: 2 + SKU820: + cost: 461 + init_stock: 600 + price: 461 + service_level: 0.96 + vlt: 2 + SKU821: + cost: 399 + init_stock: 1660 + price: 399 + service_level: 0.96 + vlt: 1 + SKU822: + cost: 60 + init_stock: 1440 + price: 60 + service_level: 0.96 + vlt: 1 + SKU823: + cost: 171 + init_stock: 1620 + price: 171 + service_level: 0.96 + vlt: 2 + SKU824: + cost: 54 + init_stock: 1220 + price: 54 + service_level: 0.96 + vlt: 2 + SKU825: + cost: 361 + init_stock: 840 + price: 361 + service_level: 0.96 + vlt: 1 + SKU826: + cost: 409 + init_stock: 520 + price: 409 + service_level: 0.96 + vlt: 1 + SKU827: + cost: 495 + init_stock: 1560 + price: 495 + service_level: 0.96 + vlt: 1 + SKU828: + cost: 97 + init_stock: 900 + price: 97 + service_level: 0.96 + vlt: 1 + SKU829: + cost: 301 + init_stock: 1000 + price: 301 + service_level: 0.96 + vlt: 2 + SKU83: + cost: 365 + init_stock: 1180 + price: 365 + service_level: 0.96 + vlt: 3 + SKU830: + cost: 215 + init_stock: 260 + price: 215 + service_level: 0.96 + vlt: 2 + SKU831: + cost: 324 + init_stock: 1000 + price: 324 + service_level: 0.96 + vlt: 3 + SKU832: + cost: 118 + init_stock: 1860 + price: 118 + service_level: 0.96 + vlt: 2 + SKU833: + cost: 435 + init_stock: 600 + price: 435 + service_level: 0.96 + vlt: 1 + SKU834: + cost: 402 + init_stock: 1900 + price: 402 + service_level: 0.96 + vlt: 1 + SKU835: + cost: 216 + init_stock: 1120 + price: 216 + service_level: 0.96 + vlt: 2 + SKU836: + cost: 15 + init_stock: 1480 + price: 15 + service_level: 0.96 + vlt: 3 + SKU837: + cost: 62 + init_stock: 1380 + price: 62 + service_level: 0.96 + vlt: 3 + SKU838: + cost: 135 + init_stock: 480 + price: 135 + service_level: 0.96 + vlt: 2 + SKU839: + cost: 485 + init_stock: 1740 + price: 485 + service_level: 0.96 + vlt: 3 + SKU84: + cost: 21 + init_stock: 1640 + price: 21 + service_level: 0.96 + vlt: 1 + SKU840: + cost: 185 + init_stock: 600 + price: 185 + service_level: 0.96 + vlt: 3 + SKU841: + cost: 211 + init_stock: 1960 + price: 211 + service_level: 0.96 + vlt: 1 + SKU842: + cost: 251 + init_stock: 200 + price: 251 + service_level: 0.96 + vlt: 3 + SKU843: + cost: 119 + init_stock: 1220 + price: 119 + service_level: 0.96 + vlt: 2 + SKU844: + cost: 119 + init_stock: 1920 + price: 119 + service_level: 0.96 + vlt: 1 + SKU845: + cost: 489 + init_stock: 1500 + price: 489 + service_level: 0.96 + vlt: 3 + SKU846: + cost: 500 + init_stock: 200 + price: 500 + service_level: 0.96 + vlt: 2 + SKU847: + cost: 304 + init_stock: 1840 + price: 304 + service_level: 0.96 + vlt: 3 + SKU848: + cost: 242 + init_stock: 1800 + price: 242 + service_level: 0.96 + vlt: 1 + SKU849: + cost: 33 + init_stock: 1920 + price: 33 + service_level: 0.96 + vlt: 3 + SKU85: + cost: 156 + init_stock: 1240 + price: 156 + service_level: 0.96 + vlt: 1 + SKU850: + cost: 449 + init_stock: 1020 + price: 449 + service_level: 0.96 + vlt: 1 + SKU851: + cost: 417 + init_stock: 920 + price: 417 + service_level: 0.96 + vlt: 1 + SKU852: + cost: 274 + init_stock: 1040 + price: 274 + service_level: 0.96 + vlt: 3 + SKU853: + cost: 65 + init_stock: 1080 + price: 65 + service_level: 0.96 + vlt: 3 + SKU854: + cost: 224 + init_stock: 1840 + price: 224 + service_level: 0.96 + vlt: 1 + SKU855: + cost: 125 + init_stock: 140 + price: 125 + service_level: 0.96 + vlt: 2 + SKU856: + cost: 192 + init_stock: 840 + price: 192 + service_level: 0.96 + vlt: 1 + SKU857: + cost: 180 + init_stock: 360 + price: 180 + service_level: 0.96 + vlt: 3 + SKU858: + cost: 393 + init_stock: 1580 + price: 393 + service_level: 0.96 + vlt: 3 + SKU859: + cost: 259 + init_stock: 1240 + price: 259 + service_level: 0.96 + vlt: 2 + SKU86: + cost: 322 + init_stock: 120 + price: 322 + service_level: 0.96 + vlt: 1 + SKU860: + cost: 287 + init_stock: 600 + price: 287 + service_level: 0.96 + vlt: 2 + SKU861: + cost: 20 + init_stock: 560 + price: 20 + service_level: 0.96 + vlt: 1 + SKU862: + cost: 86 + init_stock: 460 + price: 86 + service_level: 0.96 + vlt: 1 + SKU863: + cost: 84 + init_stock: 1580 + price: 84 + service_level: 0.96 + vlt: 2 + SKU864: + cost: 287 + init_stock: 860 + price: 287 + service_level: 0.96 + vlt: 1 + SKU865: + cost: 197 + init_stock: 480 + price: 197 + service_level: 0.96 + vlt: 2 + SKU866: + cost: 393 + init_stock: 100 + price: 393 + service_level: 0.96 + vlt: 3 + SKU867: + cost: 231 + init_stock: 700 + price: 231 + service_level: 0.96 + vlt: 1 + SKU868: + cost: 467 + init_stock: 1320 + price: 467 + service_level: 0.96 + vlt: 1 + SKU869: + cost: 114 + init_stock: 1840 + price: 114 + service_level: 0.96 + vlt: 3 + SKU87: + cost: 326 + init_stock: 180 + price: 326 + service_level: 0.96 + vlt: 1 + SKU870: + cost: 44 + init_stock: 220 + price: 44 + service_level: 0.96 + vlt: 1 + SKU871: + cost: 404 + init_stock: 1680 + price: 404 + service_level: 0.96 + vlt: 2 + SKU872: + cost: 291 + init_stock: 1080 + price: 291 + service_level: 0.96 + vlt: 2 + SKU873: + cost: 334 + init_stock: 420 + price: 334 + service_level: 0.96 + vlt: 2 + SKU874: + cost: 364 + init_stock: 960 + price: 364 + service_level: 0.96 + vlt: 3 + SKU875: + cost: 276 + init_stock: 1740 + price: 276 + service_level: 0.96 + vlt: 3 + SKU876: + cost: 342 + init_stock: 1400 + price: 342 + service_level: 0.96 + vlt: 1 + SKU877: + cost: 398 + init_stock: 1620 + price: 398 + service_level: 0.96 + vlt: 3 + SKU878: + cost: 131 + init_stock: 1360 + price: 131 + service_level: 0.96 + vlt: 1 + SKU879: + cost: 343 + init_stock: 160 + price: 343 + service_level: 0.96 + vlt: 1 + SKU88: + cost: 244 + init_stock: 440 + price: 244 + service_level: 0.96 + vlt: 3 + SKU880: + cost: 496 + init_stock: 880 + price: 496 + service_level: 0.96 + vlt: 2 + SKU881: + cost: 367 + init_stock: 1340 + price: 367 + service_level: 0.96 + vlt: 3 + SKU882: + cost: 86 + init_stock: 1260 + price: 86 + service_level: 0.96 + vlt: 1 + SKU883: + cost: 190 + init_stock: 920 + price: 190 + service_level: 0.96 + vlt: 3 + SKU884: + cost: 200 + init_stock: 580 + price: 200 + service_level: 0.96 + vlt: 2 + SKU885: + cost: 133 + init_stock: 920 + price: 133 + service_level: 0.96 + vlt: 3 + SKU886: + cost: 218 + init_stock: 320 + price: 218 + service_level: 0.96 + vlt: 1 + SKU887: + cost: 215 + init_stock: 1760 + price: 215 + service_level: 0.96 + vlt: 1 + SKU888: + cost: 439 + init_stock: 1340 + price: 439 + service_level: 0.96 + vlt: 2 + SKU889: + cost: 105 + init_stock: 1360 + price: 105 + service_level: 0.96 + vlt: 3 + SKU89: + cost: 223 + init_stock: 140 + price: 223 + service_level: 0.96 + vlt: 2 + SKU890: + cost: 183 + init_stock: 720 + price: 183 + service_level: 0.96 + vlt: 1 + SKU891: + cost: 85 + init_stock: 880 + price: 85 + service_level: 0.96 + vlt: 2 + SKU892: + cost: 41 + init_stock: 1680 + price: 41 + service_level: 0.96 + vlt: 1 + SKU893: + cost: 135 + init_stock: 440 + price: 135 + service_level: 0.96 + vlt: 3 + SKU894: + cost: 405 + init_stock: 1400 + price: 405 + service_level: 0.96 + vlt: 3 + SKU895: + cost: 320 + init_stock: 1240 + price: 320 + service_level: 0.96 + vlt: 1 + SKU896: + cost: 451 + init_stock: 1080 + price: 451 + service_level: 0.96 + vlt: 2 + SKU897: + cost: 141 + init_stock: 1920 + price: 141 + service_level: 0.96 + vlt: 2 + SKU898: + cost: 220 + init_stock: 1780 + price: 220 + service_level: 0.96 + vlt: 3 + SKU899: + cost: 113 + init_stock: 760 + price: 113 + service_level: 0.96 + vlt: 1 + SKU9: + cost: 483 + init_stock: 840 + price: 483 + service_level: 0.96 + vlt: 3 + SKU90: + cost: 217 + init_stock: 220 + price: 217 + service_level: 0.96 + vlt: 1 + SKU900: + cost: 218 + init_stock: 1400 + price: 218 + service_level: 0.96 + vlt: 3 + SKU901: + cost: 335 + init_stock: 1660 + price: 335 + service_level: 0.96 + vlt: 2 + SKU902: + cost: 118 + init_stock: 1980 + price: 118 + service_level: 0.96 + vlt: 3 + SKU903: + cost: 164 + init_stock: 220 + price: 164 + service_level: 0.96 + vlt: 2 + SKU904: + cost: 317 + init_stock: 600 + price: 317 + service_level: 0.96 + vlt: 3 + SKU905: + cost: 462 + init_stock: 620 + price: 462 + service_level: 0.96 + vlt: 1 + SKU906: + cost: 249 + init_stock: 900 + price: 249 + service_level: 0.96 + vlt: 2 + SKU907: + cost: 206 + init_stock: 1520 + price: 206 + service_level: 0.96 + vlt: 2 + SKU908: + cost: 419 + init_stock: 1260 + price: 419 + service_level: 0.96 + vlt: 3 + SKU909: + cost: 414 + init_stock: 1280 + price: 414 + service_level: 0.96 + vlt: 1 + SKU91: + cost: 254 + init_stock: 1800 + price: 254 + service_level: 0.96 + vlt: 2 + SKU910: + cost: 371 + init_stock: 1040 + price: 371 + service_level: 0.96 + vlt: 3 + SKU911: + cost: 421 + init_stock: 1200 + price: 421 + service_level: 0.96 + vlt: 2 + SKU912: + cost: 164 + init_stock: 300 + price: 164 + service_level: 0.96 + vlt: 3 + SKU913: + cost: 20 + init_stock: 1540 + price: 20 + service_level: 0.96 + vlt: 2 + SKU914: + cost: 144 + init_stock: 920 + price: 144 + service_level: 0.96 + vlt: 1 + SKU915: + cost: 214 + init_stock: 200 + price: 214 + service_level: 0.96 + vlt: 2 + SKU916: + cost: 117 + init_stock: 960 + price: 117 + service_level: 0.96 + vlt: 3 + SKU917: + cost: 212 + init_stock: 520 + price: 212 + service_level: 0.96 + vlt: 3 + SKU918: + cost: 318 + init_stock: 980 + price: 318 + service_level: 0.96 + vlt: 3 + SKU919: + cost: 59 + init_stock: 220 + price: 59 + service_level: 0.96 + vlt: 3 + SKU92: + cost: 341 + init_stock: 1220 + price: 341 + service_level: 0.96 + vlt: 1 + SKU920: + cost: 161 + init_stock: 480 + price: 161 + service_level: 0.96 + vlt: 2 + SKU921: + cost: 489 + init_stock: 1760 + price: 489 + service_level: 0.96 + vlt: 1 + SKU922: + cost: 35 + init_stock: 1820 + price: 35 + service_level: 0.96 + vlt: 2 + SKU923: + cost: 182 + init_stock: 160 + price: 182 + service_level: 0.96 + vlt: 3 + SKU924: + cost: 364 + init_stock: 920 + price: 364 + service_level: 0.96 + vlt: 3 + SKU925: + cost: 31 + init_stock: 1900 + price: 31 + service_level: 0.96 + vlt: 2 + SKU926: + cost: 472 + init_stock: 200 + price: 472 + service_level: 0.96 + vlt: 3 + SKU927: + cost: 143 + init_stock: 100 + price: 143 + service_level: 0.96 + vlt: 3 + SKU928: + cost: 325 + init_stock: 740 + price: 325 + service_level: 0.96 + vlt: 3 + SKU929: + cost: 296 + init_stock: 1520 + price: 296 + service_level: 0.96 + vlt: 1 + SKU93: + cost: 361 + init_stock: 1680 + price: 361 + service_level: 0.96 + vlt: 1 + SKU930: + cost: 257 + init_stock: 1700 + price: 257 + service_level: 0.96 + vlt: 1 + SKU931: + cost: 156 + init_stock: 660 + price: 156 + service_level: 0.96 + vlt: 1 + SKU932: + cost: 138 + init_stock: 1980 + price: 138 + service_level: 0.96 + vlt: 2 + SKU933: + cost: 284 + init_stock: 760 + price: 284 + service_level: 0.96 + vlt: 1 + SKU934: + cost: 213 + init_stock: 860 + price: 213 + service_level: 0.96 + vlt: 1 + SKU935: + cost: 339 + init_stock: 540 + price: 339 + service_level: 0.96 + vlt: 1 + SKU936: + cost: 170 + init_stock: 180 + price: 170 + service_level: 0.96 + vlt: 3 + SKU937: + cost: 123 + init_stock: 300 + price: 123 + service_level: 0.96 + vlt: 2 + SKU938: + cost: 299 + init_stock: 1820 + price: 299 + service_level: 0.96 + vlt: 3 + SKU939: + cost: 267 + init_stock: 500 + price: 267 + service_level: 0.96 + vlt: 3 + SKU94: + cost: 446 + init_stock: 980 + price: 446 + service_level: 0.96 + vlt: 1 + SKU940: + cost: 475 + init_stock: 1100 + price: 475 + service_level: 0.96 + vlt: 3 + SKU941: + cost: 202 + init_stock: 480 + price: 202 + service_level: 0.96 + vlt: 3 + SKU942: + cost: 408 + init_stock: 1440 + price: 408 + service_level: 0.96 + vlt: 2 + SKU943: + cost: 189 + init_stock: 160 + price: 189 + service_level: 0.96 + vlt: 3 + SKU944: + cost: 494 + init_stock: 680 + price: 494 + service_level: 0.96 + vlt: 1 + SKU945: + cost: 237 + init_stock: 340 + price: 237 + service_level: 0.96 + vlt: 1 + SKU946: + cost: 345 + init_stock: 1960 + price: 345 + service_level: 0.96 + vlt: 2 + SKU947: + cost: 495 + init_stock: 1080 + price: 495 + service_level: 0.96 + vlt: 3 + SKU948: + cost: 314 + init_stock: 1560 + price: 314 + service_level: 0.96 + vlt: 3 + SKU949: + cost: 453 + init_stock: 380 + price: 453 + service_level: 0.96 + vlt: 1 + SKU95: + cost: 25 + init_stock: 1940 + price: 25 + service_level: 0.96 + vlt: 2 + SKU950: + cost: 286 + init_stock: 1380 + price: 286 + service_level: 0.96 + vlt: 3 + SKU951: + cost: 167 + init_stock: 1400 + price: 167 + service_level: 0.96 + vlt: 2 + SKU952: + cost: 493 + init_stock: 820 + price: 493 + service_level: 0.96 + vlt: 1 + SKU953: + cost: 472 + init_stock: 1580 + price: 472 + service_level: 0.96 + vlt: 1 + SKU954: + cost: 287 + init_stock: 280 + price: 287 + service_level: 0.96 + vlt: 3 + SKU955: + cost: 94 + init_stock: 300 + price: 94 + service_level: 0.96 + vlt: 1 + SKU956: + cost: 157 + init_stock: 740 + price: 157 + service_level: 0.96 + vlt: 1 + SKU957: + cost: 304 + init_stock: 1260 + price: 304 + service_level: 0.96 + vlt: 2 + SKU958: + cost: 348 + init_stock: 1100 + price: 348 + service_level: 0.96 + vlt: 2 + SKU959: + cost: 442 + init_stock: 1580 + price: 442 + service_level: 0.96 + vlt: 3 + SKU96: + cost: 473 + init_stock: 980 + price: 473 + service_level: 0.96 + vlt: 1 + SKU960: + cost: 27 + init_stock: 1520 + price: 27 + service_level: 0.96 + vlt: 2 + SKU961: + cost: 282 + init_stock: 1280 + price: 282 + service_level: 0.96 + vlt: 2 + SKU962: + cost: 391 + init_stock: 220 + price: 391 + service_level: 0.96 + vlt: 3 + SKU963: + cost: 247 + init_stock: 360 + price: 247 + service_level: 0.96 + vlt: 1 + SKU964: + cost: 495 + init_stock: 240 + price: 495 + service_level: 0.96 + vlt: 3 + SKU965: + cost: 428 + init_stock: 1480 + price: 428 + service_level: 0.96 + vlt: 3 + SKU966: + cost: 360 + init_stock: 820 + price: 360 + service_level: 0.96 + vlt: 1 + SKU967: + cost: 407 + init_stock: 1620 + price: 407 + service_level: 0.96 + vlt: 1 + SKU968: + cost: 69 + init_stock: 1260 + price: 69 + service_level: 0.96 + vlt: 1 + SKU969: + cost: 131 + init_stock: 640 + price: 131 + service_level: 0.96 + vlt: 3 + SKU97: + cost: 432 + init_stock: 980 + price: 432 + service_level: 0.96 + vlt: 2 + SKU970: + cost: 376 + init_stock: 1920 + price: 376 + service_level: 0.96 + vlt: 2 + SKU971: + cost: 44 + init_stock: 280 + price: 44 + service_level: 0.96 + vlt: 1 + SKU972: + cost: 78 + init_stock: 160 + price: 78 + service_level: 0.96 + vlt: 2 + SKU973: + cost: 407 + init_stock: 840 + price: 407 + service_level: 0.96 + vlt: 2 + SKU974: + cost: 434 + init_stock: 540 + price: 434 + service_level: 0.96 + vlt: 2 + SKU975: + cost: 98 + init_stock: 1780 + price: 98 + service_level: 0.96 + vlt: 2 + SKU976: + cost: 263 + init_stock: 1220 + price: 263 + service_level: 0.96 + vlt: 2 + SKU977: + cost: 83 + init_stock: 460 + price: 83 + service_level: 0.96 + vlt: 3 + SKU978: + cost: 90 + init_stock: 1540 + price: 90 + service_level: 0.96 + vlt: 3 + SKU979: + cost: 207 + init_stock: 260 + price: 207 + service_level: 0.96 + vlt: 3 + SKU98: + cost: 43 + init_stock: 1660 + price: 43 + service_level: 0.96 + vlt: 1 + SKU980: + cost: 227 + init_stock: 1200 + price: 227 + service_level: 0.96 + vlt: 2 + SKU981: + cost: 278 + init_stock: 340 + price: 278 + service_level: 0.96 + vlt: 2 + SKU982: + cost: 125 + init_stock: 200 + price: 125 + service_level: 0.96 + vlt: 3 + SKU983: + cost: 431 + init_stock: 1360 + price: 431 + service_level: 0.96 + vlt: 3 + SKU984: + cost: 59 + init_stock: 560 + price: 59 + service_level: 0.96 + vlt: 2 + SKU985: + cost: 191 + init_stock: 1260 + price: 191 + service_level: 0.96 + vlt: 1 + SKU986: + cost: 236 + init_stock: 760 + price: 236 + service_level: 0.96 + vlt: 2 + SKU987: + cost: 333 + init_stock: 1980 + price: 333 + service_level: 0.96 + vlt: 3 + SKU988: + cost: 407 + init_stock: 1540 + price: 407 + service_level: 0.96 + vlt: 1 + SKU989: + cost: 53 + init_stock: 1640 + price: 53 + service_level: 0.96 + vlt: 3 + SKU99: + cost: 65 + init_stock: 1580 + price: 65 + service_level: 0.96 + vlt: 1 + SKU990: + cost: 368 + init_stock: 1880 + price: 368 + service_level: 0.96 + vlt: 2 + SKU991: + cost: 348 + init_stock: 1840 + price: 348 + service_level: 0.96 + vlt: 3 + SKU992: + cost: 59 + init_stock: 1360 + price: 59 + service_level: 0.96 + vlt: 3 + SKU993: + cost: 498 + init_stock: 460 + price: 498 + service_level: 0.96 + vlt: 2 + SKU994: + cost: 438 + init_stock: 680 + price: 438 + service_level: 0.96 + vlt: 2 + SKU995: + cost: 292 + init_stock: 980 + price: 292 + service_level: 0.96 + vlt: 1 + SKU996: + cost: 73 + init_stock: 980 + price: 73 + service_level: 0.96 + vlt: 3 + SKU997: + cost: 321 + init_stock: 1420 + price: 321 + service_level: 0.96 + vlt: 3 + SKU998: + cost: 371 + init_stock: 300 + price: 371 + service_level: 0.96 + vlt: 1 + SKU999: + cost: 208 + init_stock: 1780 + price: 208 + service_level: 0.96 + vlt: 3 + - children: + storage: + config: + capacity: 1063220 + unit_storage_cost: 1 + config: + order_cost: 500 + definition_ref: RetailerFacility + name: STORE0 + skus: + SKU0: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 154 + init_stock: 267 + max_stock: 1000 + price: 195 + sale_gamma: 89 + service_level: 0.95 + SKU1: + constraint: null + cost: 122 + init_stock: 172 + max_stock: 1000 + price: 168 + sale_gamma: 86 + service_level: 0.95 + SKU10: + constraint: G(stock_constraint) + cost: 158 + init_stock: 460 + max_stock: 1000 + price: 254 + sale_gamma: 92 + service_level: 0.95 + SKU100: + constraint: G(stock_constraint) + cost: 448 + init_stock: 185 + max_stock: 1000 + price: 663 + sale_gamma: 37 + service_level: 0.95 + SKU101: + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 248 + max_stock: 1000 + price: 807 + sale_gamma: 62 + service_level: 0.95 + SKU102: + constraint: null + cost: 33 + init_stock: 581 + max_stock: 1000 + price: 38 + sale_gamma: 83 + service_level: 0.95 + SKU103: + constraint: null + cost: 169 + init_stock: 395 + max_stock: 1000 + price: 273 + sale_gamma: 79 + service_level: 0.95 + SKU104: + constraint: null + cost: 253 + init_stock: 116 + max_stock: 1000 + price: 384 + sale_gamma: 29 + service_level: 0.95 + SKU105: + constraint: null + cost: 305 + init_stock: 472 + max_stock: 1000 + price: 506 + sale_gamma: 59 + service_level: 0.95 + SKU106: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 172 + init_stock: 174 + max_stock: 1000 + price: 211 + sale_gamma: 29 + service_level: 0.95 + SKU107: + constraint: null + cost: 323 + init_stock: 243 + max_stock: 1000 + price: 555 + sale_gamma: 81 + service_level: 0.95 + SKU108: + constraint: null + cost: 283 + init_stock: 644 + max_stock: 1000 + price: 316 + sale_gamma: 92 + service_level: 0.95 + SKU109: + constraint: null + cost: 304 + init_stock: 462 + max_stock: 1000 + price: 550 + sale_gamma: 77 + service_level: 0.95 + SKU11: + constraint: null + cost: 204 + init_stock: 348 + max_stock: 1000 + price: 359 + sale_gamma: 58 + service_level: 0.95 + SKU110: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 374 + init_stock: 651 + max_stock: 1000 + price: 628 + sale_gamma: 93 + service_level: 0.95 + SKU111: + constraint: G(stock_constraint) + cost: 215 + init_stock: 312 + max_stock: 1000 + price: 245 + sale_gamma: 78 + service_level: 0.95 + SKU112: + constraint: null + cost: 417 + init_stock: 48 + max_stock: 1000 + price: 792 + sale_gamma: 8 + service_level: 0.95 + SKU113: + constraint: G(stock_constraint) + cost: 64 + init_stock: 207 + max_stock: 1000 + price: 101 + sale_gamma: 69 + service_level: 0.95 + SKU114: + constraint: G(stock_constraint) + cost: 63 + init_stock: 270 + max_stock: 1000 + price: 72 + sale_gamma: 54 + service_level: 0.95 + SKU115: + constraint: G(stock_constraint) + cost: 466 + init_stock: 609 + max_stock: 1000 + price: 880 + sale_gamma: 87 + service_level: 0.95 + SKU116: + constraint: null + cost: 115 + init_stock: 184 + max_stock: 1000 + price: 175 + sale_gamma: 92 + service_level: 0.95 + SKU117: + constraint: null + cost: 76 + init_stock: 201 + max_stock: 1000 + price: 97 + sale_gamma: 67 + service_level: 0.95 + SKU118: + constraint: null + cost: 292 + init_stock: 414 + max_stock: 1000 + price: 338 + sale_gamma: 69 + service_level: 0.95 + SKU119: + constraint: null + cost: 333 + init_stock: 78 + max_stock: 1000 + price: 656 + sale_gamma: 39 + service_level: 0.95 + SKU12: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 429 + init_stock: 296 + max_stock: 1000 + price: 660 + sale_gamma: 74 + service_level: 0.95 + SKU120: + constraint: null + cost: 147 + init_stock: 384 + max_stock: 1000 + price: 224 + sale_gamma: 48 + service_level: 0.95 + SKU121: + constraint: null + cost: 169 + init_stock: 354 + max_stock: 1000 + price: 290 + sale_gamma: 59 + service_level: 0.95 + SKU122: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 71 + init_stock: 360 + max_stock: 1000 + price: 127 + sale_gamma: 72 + service_level: 0.95 + SKU123: + constraint: null + cost: 33 + init_stock: 354 + max_stock: 1000 + price: 50 + sale_gamma: 59 + service_level: 0.95 + SKU124: + constraint: G(stock_constraint) + cost: 478 + init_stock: 348 + max_stock: 1000 + price: 884 + sale_gamma: 58 + service_level: 0.95 + SKU125: + constraint: G(stock_constraint) + cost: 311 + init_stock: 180 + max_stock: 1000 + price: 469 + sale_gamma: 36 + service_level: 0.95 + SKU126: + constraint: null + cost: 497 + init_stock: 456 + max_stock: 1000 + price: 979 + sale_gamma: 76 + service_level: 0.95 + SKU127: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 63 + init_stock: 460 + max_stock: 1000 + price: 105 + sale_gamma: 92 + service_level: 0.95 + SKU128: + constraint: null + cost: 200 + init_stock: 294 + max_stock: 1000 + price: 348 + sale_gamma: 42 + service_level: 0.95 + SKU129: + constraint: null + cost: 465 + init_stock: 203 + max_stock: 1000 + price: 902 + sale_gamma: 29 + service_level: 0.95 + SKU13: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 458 + init_stock: 232 + max_stock: 1000 + price: 540 + sale_gamma: 29 + service_level: 0.95 + SKU130: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 224 + init_stock: 324 + max_stock: 1000 + price: 425 + sale_gamma: 54 + service_level: 0.95 + SKU131: + constraint: null + cost: 491 + init_stock: 240 + max_stock: 1000 + price: 746 + sale_gamma: 60 + service_level: 0.95 + SKU132: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 93 + init_stock: 366 + max_stock: 1000 + price: 169 + sale_gamma: 61 + service_level: 0.95 + SKU133: + constraint: null + cost: 269 + init_stock: 39 + max_stock: 1000 + price: 422 + sale_gamma: 13 + service_level: 0.95 + SKU134: + constraint: null + cost: 364 + init_stock: 80 + max_stock: 1000 + price: 680 + sale_gamma: 20 + service_level: 0.95 + SKU135: + constraint: null + cost: 351 + init_stock: 490 + max_stock: 1000 + price: 582 + sale_gamma: 98 + service_level: 0.95 + SKU136: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 429 + init_stock: 240 + max_stock: 1000 + price: 617 + sale_gamma: 40 + service_level: 0.95 + SKU137: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 193 + init_stock: 552 + max_stock: 1000 + price: 301 + sale_gamma: 92 + service_level: 0.95 + SKU138: + constraint: G(low_profit -> low_stock_constraint) + cost: 113 + init_stock: 140 + max_stock: 1000 + price: 187 + sale_gamma: 28 + service_level: 0.95 + SKU139: + constraint: null + cost: 10 + init_stock: 475 + max_stock: 1000 + price: 14 + sale_gamma: 95 + service_level: 0.95 + SKU14: + constraint: G(stock_constraint) + cost: 55 + init_stock: 65 + max_stock: 1000 + price: 67 + sale_gamma: 13 + service_level: 0.95 + SKU140: + constraint: null + cost: 350 + init_stock: 288 + max_stock: 1000 + price: 399 + sale_gamma: 96 + service_level: 0.95 + SKU141: + constraint: G(low_profit -> low_stock_constraint) + cost: 70 + init_stock: 426 + max_stock: 1000 + price: 133 + sale_gamma: 71 + service_level: 0.95 + SKU142: + constraint: null + cost: 496 + init_stock: 438 + max_stock: 1000 + price: 863 + sale_gamma: 73 + service_level: 0.95 + SKU143: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 33 + init_stock: 332 + max_stock: 1000 + price: 55 + sale_gamma: 83 + service_level: 0.95 + SKU144: + constraint: null + cost: 275 + init_stock: 450 + max_stock: 1000 + price: 354 + sale_gamma: 75 + service_level: 0.95 + SKU145: + constraint: G(stock_constraint) + cost: 51 + init_stock: 679 + max_stock: 1000 + price: 87 + sale_gamma: 97 + service_level: 0.95 + SKU146: + constraint: G(stock_constraint) + cost: 140 + init_stock: 252 + max_stock: 1000 + price: 275 + sale_gamma: 36 + service_level: 0.95 + SKU147: + constraint: null + cost: 383 + init_stock: 98 + max_stock: 1000 + price: 547 + sale_gamma: 49 + service_level: 0.95 + SKU148: + constraint: G(stock_constraint) + cost: 278 + init_stock: 408 + max_stock: 1000 + price: 553 + sale_gamma: 68 + service_level: 0.95 + SKU149: + constraint: null + cost: 402 + init_stock: 260 + max_stock: 1000 + price: 554 + sale_gamma: 52 + service_level: 0.95 + SKU15: + constraint: G(stock_constraint) + cost: 421 + init_stock: 284 + max_stock: 1000 + price: 783 + sale_gamma: 71 + service_level: 0.95 + SKU150: + constraint: G(low_profit -> low_stock_constraint) + cost: 81 + init_stock: 332 + max_stock: 1000 + price: 105 + sale_gamma: 83 + service_level: 0.95 + SKU151: + constraint: null + cost: 407 + init_stock: 455 + max_stock: 1000 + price: 724 + sale_gamma: 65 + service_level: 0.95 + SKU152: + constraint: null + cost: 327 + init_stock: 320 + max_stock: 1000 + price: 640 + sale_gamma: 64 + service_level: 0.95 + SKU153: + constraint: null + cost: 204 + init_stock: 582 + max_stock: 1000 + price: 236 + sale_gamma: 97 + service_level: 0.95 + SKU154: + constraint: G(stock_constraint) + cost: 88 + init_stock: 156 + max_stock: 1000 + price: 123 + sale_gamma: 26 + service_level: 0.95 + SKU155: + constraint: null + cost: 428 + init_stock: 130 + max_stock: 1000 + price: 509 + sale_gamma: 26 + service_level: 0.95 + SKU156: + constraint: G(low_profit -> low_stock_constraint) + cost: 486 + init_stock: 32 + max_stock: 1000 + price: 913 + sale_gamma: 8 + service_level: 0.95 + SKU157: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 35 + init_stock: 264 + max_stock: 1000 + price: 40 + sale_gamma: 66 + service_level: 0.95 + SKU158: + constraint: G(stock_constraint) + cost: 186 + init_stock: 285 + max_stock: 1000 + price: 264 + sale_gamma: 57 + service_level: 0.95 + SKU159: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 202 + init_stock: 368 + max_stock: 1000 + price: 321 + sale_gamma: 92 + service_level: 0.95 + SKU16: + constraint: null + cost: 209 + init_stock: 144 + max_stock: 1000 + price: 252 + sale_gamma: 24 + service_level: 0.95 + SKU160: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 433 + init_stock: 141 + max_stock: 1000 + price: 853 + sale_gamma: 47 + service_level: 0.95 + SKU161: + constraint: null + cost: 250 + init_stock: 176 + max_stock: 1000 + price: 477 + sale_gamma: 44 + service_level: 0.95 + SKU162: + constraint: null + cost: 316 + init_stock: 640 + max_stock: 1000 + price: 467 + sale_gamma: 80 + service_level: 0.95 + SKU163: + constraint: null + cost: 114 + init_stock: 70 + max_stock: 1000 + price: 224 + sale_gamma: 14 + service_level: 0.95 + SKU164: + constraint: G(low_profit -> low_stock_constraint) + cost: 304 + init_stock: 264 + max_stock: 1000 + price: 370 + sale_gamma: 66 + service_level: 0.95 + SKU165: + constraint: null + cost: 195 + init_stock: 212 + max_stock: 1000 + price: 300 + sale_gamma: 53 + service_level: 0.95 + SKU166: + constraint: null + cost: 315 + init_stock: 255 + max_stock: 1000 + price: 620 + sale_gamma: 51 + service_level: 0.95 + SKU167: + constraint: G(low_profit -> low_stock_constraint) + cost: 470 + init_stock: 470 + max_stock: 1000 + price: 714 + sale_gamma: 94 + service_level: 0.95 + SKU168: + constraint: G(stock_constraint) + cost: 476 + init_stock: 95 + max_stock: 1000 + price: 923 + sale_gamma: 19 + service_level: 0.95 + SKU169: + constraint: G(stock_constraint) + cost: 134 + init_stock: 95 + max_stock: 1000 + price: 176 + sale_gamma: 19 + service_level: 0.95 + SKU17: + constraint: null + cost: 401 + init_stock: 152 + max_stock: 1000 + price: 701 + sale_gamma: 38 + service_level: 0.95 + SKU170: + constraint: null + cost: 289 + init_stock: 114 + max_stock: 1000 + price: 482 + sale_gamma: 38 + service_level: 0.95 + SKU171: + constraint: null + cost: 481 + init_stock: 492 + max_stock: 1000 + price: 630 + sale_gamma: 82 + service_level: 0.95 + SKU172: + constraint: G(low_profit -> low_stock_constraint) + cost: 434 + init_stock: 124 + max_stock: 1000 + price: 525 + sale_gamma: 31 + service_level: 0.95 + SKU173: + constraint: null + cost: 292 + init_stock: 470 + max_stock: 1000 + price: 449 + sale_gamma: 94 + service_level: 0.95 + SKU174: + constraint: null + cost: 474 + init_stock: 56 + max_stock: 1000 + price: 848 + sale_gamma: 14 + service_level: 0.95 + SKU175: + constraint: G(stock_constraint) + cost: 476 + init_stock: 63 + max_stock: 1000 + price: 633 + sale_gamma: 21 + service_level: 0.95 + SKU176: + constraint: null + cost: 299 + init_stock: 320 + max_stock: 1000 + price: 499 + sale_gamma: 80 + service_level: 0.95 + SKU177: + constraint: G(stock_constraint) + cost: 425 + init_stock: 63 + max_stock: 1000 + price: 731 + sale_gamma: 21 + service_level: 0.95 + SKU178: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 419 + init_stock: 420 + max_stock: 1000 + price: 733 + sale_gamma: 70 + service_level: 0.95 + SKU179: + constraint: G(stock_constraint) + cost: 236 + init_stock: 390 + max_stock: 1000 + price: 413 + sale_gamma: 65 + service_level: 0.95 + SKU18: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 460 + init_stock: 243 + max_stock: 1000 + price: 542 + sale_gamma: 81 + service_level: 0.95 + SKU180: + constraint: null + cost: 99 + init_stock: 32 + max_stock: 1000 + price: 167 + sale_gamma: 16 + service_level: 0.95 + SKU181: + constraint: null + cost: 122 + init_stock: 108 + max_stock: 1000 + price: 158 + sale_gamma: 27 + service_level: 0.95 + SKU182: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 443 + init_stock: 69 + max_stock: 1000 + price: 637 + sale_gamma: 23 + service_level: 0.95 + SKU183: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 72 + init_stock: 106 + max_stock: 1000 + price: 108 + sale_gamma: 53 + service_level: 0.95 + SKU184: + constraint: null + cost: 192 + init_stock: 462 + max_stock: 1000 + price: 345 + sale_gamma: 77 + service_level: 0.95 + SKU185: + constraint: G(low_profit -> low_stock_constraint) + cost: 130 + init_stock: 84 + max_stock: 1000 + price: 166 + sale_gamma: 28 + service_level: 0.95 + SKU186: + constraint: G(low_profit -> low_stock_constraint) + cost: 49 + init_stock: 195 + max_stock: 1000 + price: 59 + sale_gamma: 39 + service_level: 0.95 + SKU187: + constraint: G(stock_constraint) + cost: 492 + init_stock: 188 + max_stock: 1000 + price: 757 + sale_gamma: 47 + service_level: 0.95 + SKU188: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 50 + init_stock: 450 + max_stock: 1000 + price: 90 + sale_gamma: 90 + service_level: 0.95 + SKU189: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 145 + init_stock: 99 + max_stock: 1000 + price: 163 + sale_gamma: 33 + service_level: 0.95 + SKU19: + constraint: G(low_profit -> low_stock_constraint) + cost: 451 + init_stock: 18 + max_stock: 1000 + price: 775 + sale_gamma: 6 + service_level: 0.95 + SKU190: + constraint: G(low_profit -> low_stock_constraint) + cost: 72 + init_stock: 474 + max_stock: 1000 + price: 108 + sale_gamma: 79 + service_level: 0.95 + SKU191: + constraint: null + cost: 285 + init_stock: 280 + max_stock: 1000 + price: 473 + sale_gamma: 40 + service_level: 0.95 + SKU192: + constraint: null + cost: 183 + init_stock: 190 + max_stock: 1000 + price: 296 + sale_gamma: 95 + service_level: 0.95 + SKU193: + constraint: null + cost: 345 + init_stock: 260 + max_stock: 1000 + price: 424 + sale_gamma: 65 + service_level: 0.95 + SKU194: + constraint: G(stock_constraint) + cost: 439 + init_stock: 222 + max_stock: 1000 + price: 777 + sale_gamma: 74 + service_level: 0.95 + SKU195: + constraint: G(stock_constraint) + cost: 205 + init_stock: 128 + max_stock: 1000 + price: 373 + sale_gamma: 32 + service_level: 0.95 + SKU196: + constraint: null + cost: 208 + init_stock: 88 + max_stock: 1000 + price: 278 + sale_gamma: 22 + service_level: 0.95 + SKU197: + constraint: null + cost: 476 + init_stock: 30 + max_stock: 1000 + price: 952 + sale_gamma: 5 + service_level: 0.95 + SKU198: + constraint: null + cost: 269 + init_stock: 308 + max_stock: 1000 + price: 508 + sale_gamma: 77 + service_level: 0.95 + SKU199: + constraint: G(stock_constraint) + cost: 172 + init_stock: 330 + max_stock: 1000 + price: 247 + sale_gamma: 55 + service_level: 0.95 + SKU2: + constraint: G(stock_constraint) + cost: 204 + init_stock: 175 + max_stock: 1000 + price: 405 + sale_gamma: 25 + service_level: 0.95 + SKU20: + constraint: null + cost: 437 + init_stock: 164 + max_stock: 1000 + price: 865 + sale_gamma: 41 + service_level: 0.95 + SKU200: + constraint: G(low_profit -> low_stock_constraint) + cost: 51 + init_stock: 376 + max_stock: 1000 + price: 63 + sale_gamma: 94 + service_level: 0.95 + SKU201: + constraint: G(stock_constraint) + cost: 487 + init_stock: 444 + max_stock: 1000 + price: 637 + sale_gamma: 74 + service_level: 0.95 + SKU202: + constraint: G(low_profit -> low_stock_constraint) + cost: 406 + init_stock: 42 + max_stock: 1000 + price: 657 + sale_gamma: 14 + service_level: 0.95 + SKU203: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 116 + max_stock: 1000 + price: 566 + sale_gamma: 29 + service_level: 0.95 + SKU204: + constraint: null + cost: 64 + init_stock: 624 + max_stock: 1000 + price: 122 + sale_gamma: 78 + service_level: 0.95 + SKU205: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 448 + init_stock: 455 + max_stock: 1000 + price: 595 + sale_gamma: 91 + service_level: 0.95 + SKU206: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 65 + init_stock: 485 + max_stock: 1000 + price: 107 + sale_gamma: 97 + service_level: 0.95 + SKU207: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 437 + init_stock: 290 + max_stock: 1000 + price: 725 + sale_gamma: 58 + service_level: 0.95 + SKU208: + constraint: G(stock_constraint) + cost: 38 + init_stock: 100 + max_stock: 1000 + price: 63 + sale_gamma: 25 + service_level: 0.95 + SKU209: + constraint: null + cost: 327 + init_stock: 260 + max_stock: 1000 + price: 637 + sale_gamma: 65 + service_level: 0.95 + SKU21: + constraint: null + cost: 17 + init_stock: 275 + max_stock: 1000 + price: 26 + sale_gamma: 55 + service_level: 0.95 + SKU210: + constraint: null + cost: 455 + init_stock: 150 + max_stock: 1000 + price: 800 + sale_gamma: 50 + service_level: 0.95 + SKU211: + constraint: G(stock_constraint) + cost: 170 + init_stock: 105 + max_stock: 1000 + price: 326 + sale_gamma: 15 + service_level: 0.95 + SKU212: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 62 + init_stock: 276 + max_stock: 1000 + price: 83 + sale_gamma: 69 + service_level: 0.95 + SKU213: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 263 + init_stock: 140 + max_stock: 1000 + price: 410 + sale_gamma: 70 + service_level: 0.95 + SKU214: + constraint: null + cost: 434 + init_stock: 42 + max_stock: 1000 + price: 555 + sale_gamma: 7 + service_level: 0.95 + SKU215: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 283 + init_stock: 144 + max_stock: 1000 + price: 546 + sale_gamma: 24 + service_level: 0.95 + SKU216: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 391 + init_stock: 390 + max_stock: 1000 + price: 574 + sale_gamma: 78 + service_level: 0.95 + SKU217: + constraint: G(low_profit -> low_stock_constraint) + cost: 168 + init_stock: 196 + max_stock: 1000 + price: 324 + sale_gamma: 98 + service_level: 0.95 + SKU218: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 485 + init_stock: 54 + max_stock: 1000 + price: 635 + sale_gamma: 18 + service_level: 0.95 + SKU219: + constraint: G(low_profit -> low_stock_constraint) + cost: 201 + init_stock: 516 + max_stock: 1000 + price: 355 + sale_gamma: 86 + service_level: 0.95 + SKU22: + constraint: G(stock_constraint) + cost: 22 + init_stock: 90 + max_stock: 1000 + price: 33 + sale_gamma: 30 + service_level: 0.95 + SKU220: + constraint: null + cost: 394 + init_stock: 78 + max_stock: 1000 + price: 673 + sale_gamma: 13 + service_level: 0.95 + SKU221: + constraint: G(stock_constraint) + cost: 108 + init_stock: 644 + max_stock: 1000 + price: 156 + sale_gamma: 92 + service_level: 0.95 + SKU222: + constraint: null + cost: 459 + init_stock: 168 + max_stock: 1000 + price: 514 + sale_gamma: 56 + service_level: 0.95 + SKU223: + constraint: null + cost: 420 + init_stock: 365 + max_stock: 1000 + price: 747 + sale_gamma: 73 + service_level: 0.95 + SKU224: + constraint: null + cost: 306 + init_stock: 420 + max_stock: 1000 + price: 388 + sale_gamma: 70 + service_level: 0.95 + SKU225: + constraint: null + cost: 118 + init_stock: 368 + max_stock: 1000 + price: 172 + sale_gamma: 92 + service_level: 0.95 + SKU226: + constraint: G(stock_constraint) + cost: 425 + init_stock: 176 + max_stock: 1000 + price: 565 + sale_gamma: 44 + service_level: 0.95 + SKU227: + constraint: G(low_profit -> low_stock_constraint) + cost: 191 + init_stock: 325 + max_stock: 1000 + price: 259 + sale_gamma: 65 + service_level: 0.95 + SKU228: + constraint: G(stock_constraint) + cost: 160 + init_stock: 192 + max_stock: 1000 + price: 227 + sale_gamma: 32 + service_level: 0.95 + SKU229: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 278 + init_stock: 584 + max_stock: 1000 + price: 430 + sale_gamma: 73 + service_level: 0.95 + SKU23: + constraint: null + cost: 124 + init_stock: 70 + max_stock: 1000 + price: 235 + sale_gamma: 14 + service_level: 0.95 + SKU230: + constraint: null + cost: 164 + init_stock: 76 + max_stock: 1000 + price: 216 + sale_gamma: 19 + service_level: 0.95 + SKU231: + constraint: null + cost: 321 + init_stock: 564 + max_stock: 1000 + price: 510 + sale_gamma: 94 + service_level: 0.95 + SKU232: + constraint: null + cost: 435 + init_stock: 82 + max_stock: 1000 + price: 526 + sale_gamma: 41 + service_level: 0.95 + SKU233: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 219 + init_stock: 148 + max_stock: 1000 + price: 326 + sale_gamma: 37 + service_level: 0.95 + SKU234: + constraint: G(low_profit -> low_stock_constraint) + cost: 144 + init_stock: 119 + max_stock: 1000 + price: 230 + sale_gamma: 17 + service_level: 0.95 + SKU235: + constraint: G(stock_constraint) + cost: 396 + init_stock: 132 + max_stock: 1000 + price: 669 + sale_gamma: 22 + service_level: 0.95 + SKU236: + constraint: G(stock_constraint) + cost: 383 + init_stock: 396 + max_stock: 1000 + price: 501 + sale_gamma: 99 + service_level: 0.95 + SKU237: + constraint: null + cost: 108 + init_stock: 306 + max_stock: 1000 + price: 128 + sale_gamma: 51 + service_level: 0.95 + SKU238: + constraint: null + cost: 49 + init_stock: 228 + max_stock: 1000 + price: 84 + sale_gamma: 57 + service_level: 0.95 + SKU239: + constraint: G(stock_constraint) + cost: 222 + init_stock: 270 + max_stock: 1000 + price: 381 + sale_gamma: 90 + service_level: 0.95 + SKU24: + constraint: G(stock_constraint) + cost: 368 + init_stock: 235 + max_stock: 1000 + price: 426 + sale_gamma: 47 + service_level: 0.95 + SKU240: + constraint: null + cost: 387 + init_stock: 498 + max_stock: 1000 + price: 534 + sale_gamma: 83 + service_level: 0.95 + SKU241: + constraint: null + cost: 302 + init_stock: 267 + max_stock: 1000 + price: 356 + sale_gamma: 89 + service_level: 0.95 + SKU242: + constraint: G(stock_constraint) + cost: 62 + init_stock: 395 + max_stock: 1000 + price: 101 + sale_gamma: 79 + service_level: 0.95 + SKU243: + constraint: G(stock_constraint) + cost: 12 + init_stock: 350 + max_stock: 1000 + price: 16 + sale_gamma: 50 + service_level: 0.95 + SKU244: + constraint: null + cost: 296 + init_stock: 390 + max_stock: 1000 + price: 372 + sale_gamma: 65 + service_level: 0.95 + SKU245: + constraint: G(low_profit -> low_stock_constraint) + cost: 39 + init_stock: 195 + max_stock: 1000 + price: 70 + sale_gamma: 39 + service_level: 0.95 + SKU246: + constraint: null + cost: 263 + init_stock: 180 + max_stock: 1000 + price: 297 + sale_gamma: 45 + service_level: 0.95 + SKU247: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 224 + init_stock: 130 + max_stock: 1000 + price: 277 + sale_gamma: 26 + service_level: 0.95 + SKU248: + constraint: null + cost: 410 + init_stock: 252 + max_stock: 1000 + price: 553 + sale_gamma: 84 + service_level: 0.95 + SKU249: + constraint: null + cost: 64 + init_stock: 485 + max_stock: 1000 + price: 74 + sale_gamma: 97 + service_level: 0.95 + SKU25: + constraint: G(stock_constraint) + cost: 201 + init_stock: 138 + max_stock: 1000 + price: 267 + sale_gamma: 46 + service_level: 0.95 + SKU250: + constraint: null + cost: 259 + init_stock: 225 + max_stock: 1000 + price: 466 + sale_gamma: 45 + service_level: 0.95 + SKU251: + constraint: null + cost: 181 + init_stock: 69 + max_stock: 1000 + price: 208 + sale_gamma: 23 + service_level: 0.95 + SKU252: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 330 + init_stock: 390 + max_stock: 1000 + price: 468 + sale_gamma: 65 + service_level: 0.95 + SKU253: + constraint: null + cost: 315 + init_stock: 736 + max_stock: 1000 + price: 526 + sale_gamma: 92 + service_level: 0.95 + SKU254: + constraint: null + cost: 407 + init_stock: 343 + max_stock: 1000 + price: 740 + sale_gamma: 49 + service_level: 0.95 + SKU255: + constraint: null + cost: 361 + init_stock: 444 + max_stock: 1000 + price: 689 + sale_gamma: 74 + service_level: 0.95 + SKU256: + constraint: null + cost: 67 + init_stock: 235 + max_stock: 1000 + price: 83 + sale_gamma: 47 + service_level: 0.95 + SKU257: + constraint: G(stock_constraint) + cost: 50 + init_stock: 120 + max_stock: 1000 + price: 62 + sale_gamma: 60 + service_level: 0.95 + SKU258: + constraint: G(stock_constraint) + cost: 465 + init_stock: 16 + max_stock: 1000 + price: 692 + sale_gamma: 8 + service_level: 0.95 + SKU259: + constraint: null + cost: 346 + init_stock: 160 + max_stock: 1000 + price: 570 + sale_gamma: 40 + service_level: 0.95 + SKU26: + constraint: null + cost: 419 + init_stock: 325 + max_stock: 1000 + price: 733 + sale_gamma: 65 + service_level: 0.95 + SKU260: + constraint: null + cost: 374 + init_stock: 126 + max_stock: 1000 + price: 527 + sale_gamma: 42 + service_level: 0.95 + SKU261: + constraint: null + cost: 74 + init_stock: 189 + max_stock: 1000 + price: 139 + sale_gamma: 27 + service_level: 0.95 + SKU262: + constraint: G(low_profit -> low_stock_constraint) + cost: 456 + init_stock: 345 + max_stock: 1000 + price: 606 + sale_gamma: 69 + service_level: 0.95 + SKU263: + constraint: null + cost: 276 + init_stock: 432 + max_stock: 1000 + price: 347 + sale_gamma: 54 + service_level: 0.95 + SKU264: + constraint: null + cost: 89 + init_stock: 360 + max_stock: 1000 + price: 126 + sale_gamma: 60 + service_level: 0.95 + SKU265: + constraint: null + cost: 148 + init_stock: 395 + max_stock: 1000 + price: 281 + sale_gamma: 79 + service_level: 0.95 + SKU266: + constraint: null + cost: 287 + init_stock: 91 + max_stock: 1000 + price: 373 + sale_gamma: 13 + service_level: 0.95 + SKU267: + constraint: null + cost: 31 + init_stock: 288 + max_stock: 1000 + price: 39 + sale_gamma: 96 + service_level: 0.95 + SKU268: + constraint: null + cost: 340 + init_stock: 66 + max_stock: 1000 + price: 530 + sale_gamma: 22 + service_level: 0.95 + SKU269: + constraint: G(stock_constraint) + cost: 479 + init_stock: 416 + max_stock: 1000 + price: 819 + sale_gamma: 52 + service_level: 0.95 + SKU27: + constraint: null + cost: 269 + init_stock: 623 + max_stock: 1000 + price: 508 + sale_gamma: 89 + service_level: 0.95 + SKU270: + constraint: null + cost: 233 + init_stock: 280 + max_stock: 1000 + price: 410 + sale_gamma: 40 + service_level: 0.95 + SKU271: + constraint: G(low_profit -> low_stock_constraint) + cost: 90 + init_stock: 60 + max_stock: 1000 + price: 131 + sale_gamma: 12 + service_level: 0.95 + SKU272: + constraint: G(low_profit -> low_stock_constraint) + cost: 488 + init_stock: 240 + max_stock: 1000 + price: 858 + sale_gamma: 30 + service_level: 0.95 + SKU273: + constraint: G(low_profit -> low_stock_constraint) + cost: 499 + init_stock: 140 + max_stock: 1000 + price: 683 + sale_gamma: 20 + service_level: 0.95 + SKU274: + constraint: G(low_profit -> low_stock_constraint) + cost: 434 + init_stock: 372 + max_stock: 1000 + price: 512 + sale_gamma: 93 + service_level: 0.95 + SKU275: + constraint: G(stock_constraint) + cost: 457 + init_stock: 192 + max_stock: 1000 + price: 676 + sale_gamma: 96 + service_level: 0.95 + SKU276: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 179 + init_stock: 198 + max_stock: 1000 + price: 261 + sale_gamma: 33 + service_level: 0.95 + SKU277: + constraint: null + cost: 303 + init_stock: 310 + max_stock: 1000 + price: 518 + sale_gamma: 62 + service_level: 0.95 + SKU278: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 301 + init_stock: 192 + max_stock: 1000 + price: 358 + sale_gamma: 48 + service_level: 0.95 + SKU279: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 17 + init_stock: 328 + max_stock: 1000 + price: 22 + sale_gamma: 82 + service_level: 0.95 + SKU28: + constraint: null + cost: 399 + init_stock: 36 + max_stock: 1000 + price: 694 + sale_gamma: 6 + service_level: 0.95 + SKU280: + constraint: null + cost: 238 + init_stock: 664 + max_stock: 1000 + price: 278 + sale_gamma: 83 + service_level: 0.95 + SKU281: + constraint: null + cost: 13 + init_stock: 276 + max_stock: 1000 + price: 23 + sale_gamma: 69 + service_level: 0.95 + SKU282: + constraint: null + cost: 404 + init_stock: 336 + max_stock: 1000 + price: 755 + sale_gamma: 84 + service_level: 0.95 + SKU283: + constraint: G(stock_constraint) + cost: 332 + init_stock: 192 + max_stock: 1000 + price: 491 + sale_gamma: 32 + service_level: 0.95 + SKU284: + constraint: G(low_profit -> low_stock_constraint) + cost: 257 + init_stock: 195 + max_stock: 1000 + price: 436 + sale_gamma: 39 + service_level: 0.95 + SKU285: + constraint: null + cost: 53 + init_stock: 30 + max_stock: 1000 + price: 79 + sale_gamma: 6 + service_level: 0.95 + SKU286: + constraint: null + cost: 252 + init_stock: 52 + max_stock: 1000 + price: 493 + sale_gamma: 13 + service_level: 0.95 + SKU287: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 161 + init_stock: 658 + max_stock: 1000 + price: 180 + sale_gamma: 94 + service_level: 0.95 + SKU288: + constraint: null + cost: 477 + init_stock: 80 + max_stock: 1000 + price: 701 + sale_gamma: 16 + service_level: 0.95 + SKU289: + constraint: null + cost: 452 + init_stock: 475 + max_stock: 1000 + price: 858 + sale_gamma: 95 + service_level: 0.95 + SKU29: + constraint: null + cost: 160 + init_stock: 148 + max_stock: 1000 + price: 228 + sale_gamma: 74 + service_level: 0.95 + SKU290: + constraint: null + cost: 493 + init_stock: 84 + max_stock: 1000 + price: 719 + sale_gamma: 28 + service_level: 0.95 + SKU291: + constraint: null + cost: 180 + init_stock: 57 + max_stock: 1000 + price: 331 + sale_gamma: 19 + service_level: 0.95 + SKU292: + constraint: null + cost: 233 + init_stock: 380 + max_stock: 1000 + price: 447 + sale_gamma: 95 + service_level: 0.95 + SKU293: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 177 + init_stock: 132 + max_stock: 1000 + price: 329 + sale_gamma: 66 + service_level: 0.95 + SKU294: + constraint: null + cost: 356 + init_stock: 135 + max_stock: 1000 + price: 427 + sale_gamma: 27 + service_level: 0.95 + SKU295: + constraint: G(low_profit -> low_stock_constraint) + cost: 421 + init_stock: 574 + max_stock: 1000 + price: 572 + sale_gamma: 82 + service_level: 0.95 + SKU296: + constraint: G(stock_constraint) + cost: 414 + init_stock: 282 + max_stock: 1000 + price: 480 + sale_gamma: 47 + service_level: 0.95 + SKU297: + constraint: null + cost: 326 + init_stock: 360 + max_stock: 1000 + price: 495 + sale_gamma: 90 + service_level: 0.95 + SKU298: + constraint: G(low_profit -> low_stock_constraint) + cost: 122 + init_stock: 276 + max_stock: 1000 + price: 157 + sale_gamma: 46 + service_level: 0.95 + SKU299: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 149 + init_stock: 340 + max_stock: 1000 + price: 207 + sale_gamma: 68 + service_level: 0.95 + SKU3: + constraint: null + cost: 161 + init_stock: 30 + max_stock: 1000 + price: 305 + sale_gamma: 5 + service_level: 0.95 + SKU30: + constraint: G(stock_constraint) + cost: 395 + init_stock: 366 + max_stock: 1000 + price: 533 + sale_gamma: 61 + service_level: 0.95 + SKU300: + constraint: null + cost: 250 + init_stock: 117 + max_stock: 1000 + price: 315 + sale_gamma: 39 + service_level: 0.95 + SKU301: + constraint: G(stock_constraint) + cost: 279 + init_stock: 35 + max_stock: 1000 + price: 410 + sale_gamma: 5 + service_level: 0.95 + SKU302: + constraint: G(low_profit -> low_stock_constraint) + cost: 321 + init_stock: 222 + max_stock: 1000 + price: 609 + sale_gamma: 74 + service_level: 0.95 + SKU303: + constraint: G(stock_constraint) + cost: 202 + init_stock: 275 + max_stock: 1000 + price: 343 + sale_gamma: 55 + service_level: 0.95 + SKU304: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 393 + init_stock: 476 + max_stock: 1000 + price: 758 + sale_gamma: 68 + service_level: 0.95 + SKU305: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 113 + init_stock: 335 + max_stock: 1000 + price: 195 + sale_gamma: 67 + service_level: 0.95 + SKU306: + constraint: null + cost: 430 + init_stock: 172 + max_stock: 1000 + price: 477 + sale_gamma: 43 + service_level: 0.95 + SKU307: + constraint: null + cost: 311 + init_stock: 324 + max_stock: 1000 + price: 482 + sale_gamma: 81 + service_level: 0.95 + SKU308: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 423 + init_stock: 348 + max_stock: 1000 + price: 727 + sale_gamma: 58 + service_level: 0.95 + SKU309: + constraint: G(low_profit -> low_stock_constraint) + cost: 448 + init_stock: 237 + max_stock: 1000 + price: 779 + sale_gamma: 79 + service_level: 0.95 + SKU31: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 456 + init_stock: 228 + max_stock: 1000 + price: 775 + sale_gamma: 76 + service_level: 0.95 + SKU310: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 245 + init_stock: 378 + max_stock: 1000 + price: 365 + sale_gamma: 63 + service_level: 0.95 + SKU311: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 452 + init_stock: 420 + max_stock: 1000 + price: 872 + sale_gamma: 70 + service_level: 0.95 + SKU312: + constraint: null + cost: 112 + init_stock: 584 + max_stock: 1000 + price: 162 + sale_gamma: 73 + service_level: 0.95 + SKU313: + constraint: null + cost: 307 + init_stock: 30 + max_stock: 1000 + price: 583 + sale_gamma: 10 + service_level: 0.95 + SKU314: + constraint: null + cost: 40 + init_stock: 532 + max_stock: 1000 + price: 53 + sale_gamma: 76 + service_level: 0.95 + SKU315: + constraint: G(low_profit -> low_stock_constraint) + cost: 87 + init_stock: 88 + max_stock: 1000 + price: 155 + sale_gamma: 22 + service_level: 0.95 + SKU316: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 367 + init_stock: 128 + max_stock: 1000 + price: 638 + sale_gamma: 16 + service_level: 0.95 + SKU317: + constraint: G(stock_constraint) + cost: 62 + init_stock: 65 + max_stock: 1000 + price: 71 + sale_gamma: 13 + service_level: 0.95 + SKU318: + constraint: null + cost: 153 + init_stock: 126 + max_stock: 1000 + price: 257 + sale_gamma: 42 + service_level: 0.95 + SKU319: + constraint: null + cost: 340 + init_stock: 184 + max_stock: 1000 + price: 469 + sale_gamma: 46 + service_level: 0.95 + SKU32: + constraint: G(low_profit -> low_stock_constraint) + cost: 223 + init_stock: 356 + max_stock: 1000 + price: 437 + sale_gamma: 89 + service_level: 0.95 + SKU320: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 188 + init_stock: 252 + max_stock: 1000 + price: 347 + sale_gamma: 84 + service_level: 0.95 + SKU321: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 421 + init_stock: 282 + max_stock: 1000 + price: 808 + sale_gamma: 47 + service_level: 0.95 + SKU322: + constraint: null + cost: 462 + init_stock: 380 + max_stock: 1000 + price: 863 + sale_gamma: 76 + service_level: 0.95 + SKU323: + constraint: G(stock_constraint) + cost: 116 + init_stock: 395 + max_stock: 1000 + price: 143 + sale_gamma: 79 + service_level: 0.95 + SKU324: + constraint: null + cost: 396 + init_stock: 100 + max_stock: 1000 + price: 597 + sale_gamma: 20 + service_level: 0.95 + SKU325: + constraint: null + cost: 407 + init_stock: 180 + max_stock: 1000 + price: 797 + sale_gamma: 60 + service_level: 0.95 + SKU326: + constraint: null + cost: 400 + init_stock: 96 + max_stock: 1000 + price: 516 + sale_gamma: 12 + service_level: 0.95 + SKU327: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 434 + init_stock: 600 + max_stock: 1000 + price: 607 + sale_gamma: 100 + service_level: 0.95 + SKU328: + constraint: null + cost: 179 + init_stock: 42 + max_stock: 1000 + price: 257 + sale_gamma: 21 + service_level: 0.95 + SKU329: + constraint: null + cost: 453 + init_stock: 486 + max_stock: 1000 + price: 530 + sale_gamma: 81 + service_level: 0.95 + SKU33: + constraint: null + cost: 113 + init_stock: 356 + max_stock: 1000 + price: 124 + sale_gamma: 89 + service_level: 0.95 + SKU330: + constraint: null + cost: 215 + init_stock: 552 + max_stock: 1000 + price: 341 + sale_gamma: 92 + service_level: 0.95 + SKU331: + constraint: G(stock_constraint) + cost: 28 + init_stock: 672 + max_stock: 1000 + price: 44 + sale_gamma: 84 + service_level: 0.95 + SKU332: + constraint: null + cost: 172 + init_stock: 220 + max_stock: 1000 + price: 338 + sale_gamma: 55 + service_level: 0.95 + SKU333: + constraint: G(stock_constraint) + cost: 63 + init_stock: 201 + max_stock: 1000 + price: 112 + sale_gamma: 67 + service_level: 0.95 + SKU334: + constraint: G(low_profit -> low_stock_constraint) + cost: 253 + init_stock: 273 + max_stock: 1000 + price: 366 + sale_gamma: 39 + service_level: 0.95 + SKU335: + constraint: G(low_profit -> low_stock_constraint) + cost: 22 + init_stock: 45 + max_stock: 1000 + price: 30 + sale_gamma: 9 + service_level: 0.95 + SKU336: + constraint: null + cost: 263 + init_stock: 232 + max_stock: 1000 + price: 357 + sale_gamma: 29 + service_level: 0.95 + SKU337: + constraint: G(low_profit -> low_stock_constraint) + cost: 277 + init_stock: 444 + max_stock: 1000 + price: 321 + sale_gamma: 74 + service_level: 0.95 + SKU338: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 181 + init_stock: 272 + max_stock: 1000 + price: 242 + sale_gamma: 34 + service_level: 0.95 + SKU339: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 488 + init_stock: 380 + max_stock: 1000 + price: 780 + sale_gamma: 76 + service_level: 0.95 + SKU34: + constraint: G(low_profit -> low_stock_constraint) + cost: 226 + init_stock: 308 + max_stock: 1000 + price: 442 + sale_gamma: 44 + service_level: 0.95 + SKU340: + constraint: null + cost: 213 + init_stock: 135 + max_stock: 1000 + price: 340 + sale_gamma: 27 + service_level: 0.95 + SKU341: + constraint: null + cost: 423 + init_stock: 178 + max_stock: 1000 + price: 626 + sale_gamma: 89 + service_level: 0.95 + SKU342: + constraint: null + cost: 414 + init_stock: 294 + max_stock: 1000 + price: 625 + sale_gamma: 49 + service_level: 0.95 + SKU343: + constraint: G(low_profit -> low_stock_constraint) + cost: 429 + init_stock: 208 + max_stock: 1000 + price: 712 + sale_gamma: 52 + service_level: 0.95 + SKU344: + constraint: G(stock_constraint) + cost: 196 + init_stock: 356 + max_stock: 1000 + price: 282 + sale_gamma: 89 + service_level: 0.95 + SKU345: + constraint: G(low_profit -> low_stock_constraint) + cost: 451 + init_stock: 460 + max_stock: 1000 + price: 581 + sale_gamma: 92 + service_level: 0.95 + SKU346: + constraint: null + cost: 485 + init_stock: 120 + max_stock: 1000 + price: 756 + sale_gamma: 30 + service_level: 0.95 + SKU347: + constraint: G(stock_constraint) + cost: 30 + init_stock: 136 + max_stock: 1000 + price: 35 + sale_gamma: 68 + service_level: 0.95 + SKU348: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 12 + init_stock: 72 + max_stock: 1000 + price: 13 + sale_gamma: 12 + service_level: 0.95 + SKU349: + constraint: null + cost: 168 + init_stock: 176 + max_stock: 1000 + price: 275 + sale_gamma: 44 + service_level: 0.95 + SKU35: + constraint: null + cost: 181 + init_stock: 180 + max_stock: 1000 + price: 260 + sale_gamma: 45 + service_level: 0.95 + SKU350: + constraint: null + cost: 381 + init_stock: 87 + max_stock: 1000 + price: 491 + sale_gamma: 29 + service_level: 0.95 + SKU351: + constraint: null + cost: 333 + init_stock: 480 + max_stock: 1000 + price: 556 + sale_gamma: 96 + service_level: 0.95 + SKU352: + constraint: null + cost: 365 + init_stock: 57 + max_stock: 1000 + price: 518 + sale_gamma: 19 + service_level: 0.95 + SKU353: + constraint: G(stock_constraint) + cost: 133 + init_stock: 520 + max_stock: 1000 + price: 210 + sale_gamma: 65 + service_level: 0.95 + SKU354: + constraint: G(low_profit -> low_stock_constraint) + cost: 356 + init_stock: 310 + max_stock: 1000 + price: 534 + sale_gamma: 62 + service_level: 0.95 + SKU355: + constraint: null + cost: 453 + init_stock: 280 + max_stock: 1000 + price: 901 + sale_gamma: 56 + service_level: 0.95 + SKU356: + constraint: G(low_profit -> low_stock_constraint) + cost: 336 + init_stock: 414 + max_stock: 1000 + price: 588 + sale_gamma: 69 + service_level: 0.95 + SKU357: + constraint: null + cost: 77 + init_stock: 165 + max_stock: 1000 + price: 147 + sale_gamma: 33 + service_level: 0.95 + SKU358: + constraint: null + cost: 340 + init_stock: 268 + max_stock: 1000 + price: 408 + sale_gamma: 67 + service_level: 0.95 + SKU359: + constraint: null + cost: 259 + init_stock: 320 + max_stock: 1000 + price: 295 + sale_gamma: 64 + service_level: 0.95 + SKU36: + constraint: G(stock_constraint) + cost: 223 + init_stock: 90 + max_stock: 1000 + price: 256 + sale_gamma: 18 + service_level: 0.95 + SKU360: + constraint: null + cost: 146 + init_stock: 270 + max_stock: 1000 + price: 258 + sale_gamma: 54 + service_level: 0.95 + SKU361: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 47 + init_stock: 445 + max_stock: 1000 + price: 89 + sale_gamma: 89 + service_level: 0.95 + SKU362: + constraint: null + cost: 429 + init_stock: 679 + max_stock: 1000 + price: 849 + sale_gamma: 97 + service_level: 0.95 + SKU363: + constraint: null + cost: 495 + init_stock: 640 + max_stock: 1000 + price: 891 + sale_gamma: 80 + service_level: 0.95 + SKU364: + constraint: null + cost: 213 + init_stock: 720 + max_stock: 1000 + price: 315 + sale_gamma: 90 + service_level: 0.95 + SKU365: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 116 + init_stock: 36 + max_stock: 1000 + price: 194 + sale_gamma: 12 + service_level: 0.95 + SKU366: + constraint: null + cost: 344 + init_stock: 246 + max_stock: 1000 + price: 519 + sale_gamma: 82 + service_level: 0.95 + SKU367: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 197 + init_stock: 45 + max_stock: 1000 + price: 279 + sale_gamma: 15 + service_level: 0.95 + SKU368: + constraint: null + cost: 410 + init_stock: 360 + max_stock: 1000 + price: 520 + sale_gamma: 60 + service_level: 0.95 + SKU369: + constraint: null + cost: 187 + init_stock: 450 + max_stock: 1000 + price: 370 + sale_gamma: 90 + service_level: 0.95 + SKU37: + constraint: G(stock_constraint) + cost: 302 + init_stock: 270 + max_stock: 1000 + price: 540 + sale_gamma: 45 + service_level: 0.95 + SKU370: + constraint: G(low_profit -> low_stock_constraint) + cost: 210 + init_stock: 588 + max_stock: 1000 + price: 308 + sale_gamma: 84 + service_level: 0.95 + SKU371: + constraint: null + cost: 404 + init_stock: 345 + max_stock: 1000 + price: 719 + sale_gamma: 69 + service_level: 0.95 + SKU372: + constraint: null + cost: 63 + init_stock: 553 + max_stock: 1000 + price: 120 + sale_gamma: 79 + service_level: 0.95 + SKU373: + constraint: G(low_profit -> low_stock_constraint) + cost: 106 + init_stock: 48 + max_stock: 1000 + price: 125 + sale_gamma: 24 + service_level: 0.95 + SKU374: + constraint: null + cost: 306 + init_stock: 144 + max_stock: 1000 + price: 345 + sale_gamma: 36 + service_level: 0.95 + SKU375: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 153 + init_stock: 255 + max_stock: 1000 + price: 247 + sale_gamma: 51 + service_level: 0.95 + SKU376: + constraint: null + cost: 273 + init_stock: 120 + max_stock: 1000 + price: 428 + sale_gamma: 15 + service_level: 0.95 + SKU377: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 306 + init_stock: 300 + max_stock: 1000 + price: 419 + sale_gamma: 75 + service_level: 0.95 + SKU378: + constraint: G(low_profit -> low_stock_constraint) + cost: 233 + init_stock: 220 + max_stock: 1000 + price: 400 + sale_gamma: 44 + service_level: 0.95 + SKU379: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 10 + init_stock: 176 + max_stock: 1000 + price: 16 + sale_gamma: 22 + service_level: 0.95 + SKU38: + constraint: null + cost: 355 + init_stock: 126 + max_stock: 1000 + price: 628 + sale_gamma: 63 + service_level: 0.95 + SKU380: + constraint: G(stock_constraint) + cost: 440 + init_stock: 69 + max_stock: 1000 + price: 858 + sale_gamma: 23 + service_level: 0.95 + SKU381: + constraint: null + cost: 402 + init_stock: 240 + max_stock: 1000 + price: 727 + sale_gamma: 80 + service_level: 0.95 + SKU382: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 478 + init_stock: 500 + max_stock: 1000 + price: 932 + sale_gamma: 100 + service_level: 0.95 + SKU383: + constraint: null + cost: 248 + init_stock: 350 + max_stock: 1000 + price: 342 + sale_gamma: 70 + service_level: 0.95 + SKU384: + constraint: null + cost: 338 + init_stock: 128 + max_stock: 1000 + price: 486 + sale_gamma: 32 + service_level: 0.95 + SKU385: + constraint: G(low_profit -> low_stock_constraint) + cost: 299 + init_stock: 427 + max_stock: 1000 + price: 424 + sale_gamma: 61 + service_level: 0.95 + SKU386: + constraint: G(stock_constraint) + cost: 281 + init_stock: 552 + max_stock: 1000 + price: 368 + sale_gamma: 92 + service_level: 0.95 + SKU387: + constraint: G(low_profit -> low_stock_constraint) + cost: 157 + init_stock: 470 + max_stock: 1000 + price: 240 + sale_gamma: 94 + service_level: 0.95 + SKU388: + constraint: null + cost: 214 + init_stock: 378 + max_stock: 1000 + price: 406 + sale_gamma: 54 + service_level: 0.95 + SKU389: + constraint: null + cost: 129 + init_stock: 168 + max_stock: 1000 + price: 221 + sale_gamma: 24 + service_level: 0.95 + SKU39: + constraint: G(stock_constraint) + cost: 26 + init_stock: 49 + max_stock: 1000 + price: 39 + sale_gamma: 7 + service_level: 0.95 + SKU390: + constraint: null + cost: 377 + init_stock: 475 + max_stock: 1000 + price: 674 + sale_gamma: 95 + service_level: 0.95 + SKU391: + constraint: null + cost: 180 + init_stock: 150 + max_stock: 1000 + price: 352 + sale_gamma: 75 + service_level: 0.95 + SKU392: + constraint: null + cost: 106 + init_stock: 420 + max_stock: 1000 + price: 151 + sale_gamma: 60 + service_level: 0.95 + SKU393: + constraint: null + cost: 128 + init_stock: 260 + max_stock: 1000 + price: 211 + sale_gamma: 52 + service_level: 0.95 + SKU394: + constraint: null + cost: 414 + init_stock: 240 + max_stock: 1000 + price: 600 + sale_gamma: 60 + service_level: 0.95 + SKU395: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 416 + init_stock: 345 + max_stock: 1000 + price: 782 + sale_gamma: 69 + service_level: 0.95 + SKU396: + constraint: G(low_profit -> low_stock_constraint) + cost: 426 + init_stock: 132 + max_stock: 1000 + price: 579 + sale_gamma: 44 + service_level: 0.95 + SKU397: + constraint: G(stock_constraint) + cost: 271 + init_stock: 60 + max_stock: 1000 + price: 417 + sale_gamma: 15 + service_level: 0.95 + SKU398: + constraint: null + cost: 21 + init_stock: 177 + max_stock: 1000 + price: 35 + sale_gamma: 59 + service_level: 0.95 + SKU399: + constraint: G(low_profit -> low_stock_constraint) + cost: 269 + init_stock: 196 + max_stock: 1000 + price: 381 + sale_gamma: 49 + service_level: 0.95 + SKU4: + constraint: null + cost: 190 + init_stock: 510 + max_stock: 1000 + price: 231 + sale_gamma: 85 + service_level: 0.95 + SKU40: + constraint: null + cost: 463 + init_stock: 352 + max_stock: 1000 + price: 759 + sale_gamma: 88 + service_level: 0.95 + SKU400: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 45 + init_stock: 106 + max_stock: 1000 + price: 62 + sale_gamma: 53 + service_level: 0.95 + SKU401: + constraint: G(stock_constraint) + cost: 149 + init_stock: 328 + max_stock: 1000 + price: 177 + sale_gamma: 82 + service_level: 0.95 + SKU402: + constraint: null + cost: 396 + init_stock: 158 + max_stock: 1000 + price: 506 + sale_gamma: 79 + service_level: 0.95 + SKU403: + constraint: G(low_profit -> low_stock_constraint) + cost: 70 + init_stock: 116 + max_stock: 1000 + price: 133 + sale_gamma: 58 + service_level: 0.95 + SKU404: + constraint: null + cost: 225 + init_stock: 395 + max_stock: 1000 + price: 342 + sale_gamma: 79 + service_level: 0.95 + SKU405: + constraint: null + cost: 31 + init_stock: 132 + max_stock: 1000 + price: 57 + sale_gamma: 22 + service_level: 0.95 + SKU406: + constraint: null + cost: 449 + init_stock: 86 + max_stock: 1000 + price: 727 + sale_gamma: 43 + service_level: 0.95 + SKU407: + constraint: null + cost: 462 + init_stock: 96 + max_stock: 1000 + price: 609 + sale_gamma: 16 + service_level: 0.95 + SKU408: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 364 + init_stock: 162 + max_stock: 1000 + price: 484 + sale_gamma: 27 + service_level: 0.95 + SKU409: + constraint: null + cost: 424 + init_stock: 174 + max_stock: 1000 + price: 644 + sale_gamma: 58 + service_level: 0.95 + SKU41: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 56 + init_stock: 138 + max_stock: 1000 + price: 88 + sale_gamma: 23 + service_level: 0.95 + SKU410: + constraint: null + cost: 204 + init_stock: 27 + max_stock: 1000 + price: 326 + sale_gamma: 9 + service_level: 0.95 + SKU411: + constraint: null + cost: 197 + init_stock: 42 + max_stock: 1000 + price: 269 + sale_gamma: 6 + service_level: 0.95 + SKU412: + constraint: G(stock_constraint) + cost: 140 + init_stock: 65 + max_stock: 1000 + price: 245 + sale_gamma: 13 + service_level: 0.95 + SKU413: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 11 + init_stock: 284 + max_stock: 1000 + price: 21 + sale_gamma: 71 + service_level: 0.95 + SKU414: + constraint: null + cost: 492 + init_stock: 80 + max_stock: 1000 + price: 546 + sale_gamma: 20 + service_level: 0.95 + SKU415: + constraint: G(stock_constraint) + cost: 26 + init_stock: 126 + max_stock: 1000 + price: 41 + sale_gamma: 63 + service_level: 0.95 + SKU416: + constraint: G(low_profit -> low_stock_constraint) + cost: 151 + init_stock: 30 + max_stock: 1000 + price: 279 + sale_gamma: 5 + service_level: 0.95 + SKU417: + constraint: G(low_profit -> low_stock_constraint) + cost: 306 + init_stock: 228 + max_stock: 1000 + price: 406 + sale_gamma: 57 + service_level: 0.95 + SKU418: + constraint: G(stock_constraint) + cost: 154 + init_stock: 210 + max_stock: 1000 + price: 281 + sale_gamma: 42 + service_level: 0.95 + SKU419: + constraint: G(low_profit -> low_stock_constraint) + cost: 192 + init_stock: 96 + max_stock: 1000 + price: 253 + sale_gamma: 24 + service_level: 0.95 + SKU42: + constraint: null + cost: 106 + init_stock: 246 + max_stock: 1000 + price: 119 + sale_gamma: 41 + service_level: 0.95 + SKU420: + constraint: null + cost: 79 + init_stock: 511 + max_stock: 1000 + price: 96 + sale_gamma: 73 + service_level: 0.95 + SKU421: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 288 + init_stock: 128 + max_stock: 1000 + price: 391 + sale_gamma: 64 + service_level: 0.95 + SKU422: + constraint: null + cost: 81 + init_stock: 497 + max_stock: 1000 + price: 106 + sale_gamma: 71 + service_level: 0.95 + SKU423: + constraint: G(low_profit -> low_stock_constraint) + cost: 48 + init_stock: 316 + max_stock: 1000 + price: 63 + sale_gamma: 79 + service_level: 0.95 + SKU424: + constraint: null + cost: 344 + init_stock: 192 + max_stock: 1000 + price: 550 + sale_gamma: 32 + service_level: 0.95 + SKU425: + constraint: G(stock_constraint) + cost: 155 + init_stock: 182 + max_stock: 1000 + price: 192 + sale_gamma: 26 + service_level: 0.95 + SKU426: + constraint: null + cost: 115 + init_stock: 410 + max_stock: 1000 + price: 181 + sale_gamma: 82 + service_level: 0.95 + SKU427: + constraint: null + cost: 132 + init_stock: 10 + max_stock: 1000 + price: 182 + sale_gamma: 5 + service_level: 0.95 + SKU428: + constraint: G(stock_constraint) + cost: 499 + init_stock: 174 + max_stock: 1000 + price: 963 + sale_gamma: 58 + service_level: 0.95 + SKU429: + constraint: null + cost: 147 + init_stock: 64 + max_stock: 1000 + price: 252 + sale_gamma: 16 + service_level: 0.95 + SKU43: + constraint: null + cost: 162 + init_stock: 648 + max_stock: 1000 + price: 260 + sale_gamma: 81 + service_level: 0.95 + SKU430: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 330 + init_stock: 145 + max_stock: 1000 + price: 508 + sale_gamma: 29 + service_level: 0.95 + SKU431: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 65 + init_stock: 207 + max_stock: 1000 + price: 79 + sale_gamma: 69 + service_level: 0.95 + SKU432: + constraint: null + cost: 195 + init_stock: 372 + max_stock: 1000 + price: 323 + sale_gamma: 93 + service_level: 0.95 + SKU433: + constraint: G(low_profit -> low_stock_constraint) + cost: 55 + init_stock: 516 + max_stock: 1000 + price: 73 + sale_gamma: 86 + service_level: 0.95 + SKU434: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 113 + init_stock: 525 + max_stock: 1000 + price: 198 + sale_gamma: 75 + service_level: 0.95 + SKU435: + constraint: null + cost: 256 + init_stock: 75 + max_stock: 1000 + price: 337 + sale_gamma: 15 + service_level: 0.95 + SKU436: + constraint: null + cost: 61 + init_stock: 784 + max_stock: 1000 + price: 70 + sale_gamma: 98 + service_level: 0.95 + SKU437: + constraint: G(stock_constraint) + cost: 367 + init_stock: 294 + max_stock: 1000 + price: 451 + sale_gamma: 42 + service_level: 0.95 + SKU438: + constraint: G(stock_constraint) + cost: 396 + init_stock: 504 + max_stock: 1000 + price: 570 + sale_gamma: 72 + service_level: 0.95 + SKU439: + constraint: null + cost: 99 + init_stock: 434 + max_stock: 1000 + price: 187 + sale_gamma: 62 + service_level: 0.95 + SKU44: + constraint: G(low_profit -> low_stock_constraint) + cost: 241 + init_stock: 236 + max_stock: 1000 + price: 443 + sale_gamma: 59 + service_level: 0.95 + SKU440: + constraint: null + cost: 455 + init_stock: 102 + max_stock: 1000 + price: 500 + sale_gamma: 51 + service_level: 0.95 + SKU441: + constraint: null + cost: 217 + init_stock: 260 + max_stock: 1000 + price: 292 + sale_gamma: 65 + service_level: 0.95 + SKU442: + constraint: null + cost: 460 + init_stock: 116 + max_stock: 1000 + price: 782 + sale_gamma: 58 + service_level: 0.95 + SKU443: + constraint: G(stock_constraint) + cost: 375 + init_stock: 144 + max_stock: 1000 + price: 506 + sale_gamma: 48 + service_level: 0.95 + SKU444: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 44 + init_stock: 490 + max_stock: 1000 + price: 80 + sale_gamma: 98 + service_level: 0.95 + SKU445: + constraint: G(stock_constraint) + cost: 59 + init_stock: 340 + max_stock: 1000 + price: 109 + sale_gamma: 68 + service_level: 0.95 + SKU446: + constraint: null + cost: 183 + init_stock: 140 + max_stock: 1000 + price: 223 + sale_gamma: 20 + service_level: 0.95 + SKU447: + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 192 + max_stock: 1000 + price: 737 + sale_gamma: 48 + service_level: 0.95 + SKU448: + constraint: null + cost: 499 + init_stock: 140 + max_stock: 1000 + price: 813 + sale_gamma: 28 + service_level: 0.95 + SKU449: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 592 + max_stock: 1000 + price: 619 + sale_gamma: 74 + service_level: 0.95 + SKU45: + constraint: null + cost: 134 + init_stock: 288 + max_stock: 1000 + price: 247 + sale_gamma: 48 + service_level: 0.95 + SKU450: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 217 + max_stock: 1000 + price: 520 + sale_gamma: 31 + service_level: 0.95 + SKU451: + constraint: null + cost: 323 + init_stock: 34 + max_stock: 1000 + price: 516 + sale_gamma: 17 + service_level: 0.95 + SKU452: + constraint: null + cost: 52 + init_stock: 280 + max_stock: 1000 + price: 97 + sale_gamma: 40 + service_level: 0.95 + SKU453: + constraint: G(low_profit -> low_stock_constraint) + cost: 21 + init_stock: 486 + max_stock: 1000 + price: 30 + sale_gamma: 81 + service_level: 0.95 + SKU454: + constraint: null + cost: 198 + init_stock: 161 + max_stock: 1000 + price: 285 + sale_gamma: 23 + service_level: 0.95 + SKU455: + constraint: null + cost: 11 + init_stock: 256 + max_stock: 1000 + price: 21 + sale_gamma: 64 + service_level: 0.95 + SKU456: + constraint: G(low_profit -> low_stock_constraint) + cost: 204 + init_stock: 498 + max_stock: 1000 + price: 267 + sale_gamma: 83 + service_level: 0.95 + SKU457: + constraint: null + cost: 194 + init_stock: 700 + max_stock: 1000 + price: 322 + sale_gamma: 100 + service_level: 0.95 + SKU458: + constraint: G(stock_constraint) + cost: 110 + init_stock: 44 + max_stock: 1000 + price: 180 + sale_gamma: 11 + service_level: 0.95 + SKU459: + constraint: null + cost: 370 + init_stock: 256 + max_stock: 1000 + price: 573 + sale_gamma: 64 + service_level: 0.95 + SKU46: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 90 + init_stock: 396 + max_stock: 1000 + price: 99 + sale_gamma: 99 + service_level: 0.95 + SKU460: + constraint: null + cost: 121 + init_stock: 243 + max_stock: 1000 + price: 196 + sale_gamma: 81 + service_level: 0.95 + SKU461: + constraint: null + cost: 305 + init_stock: 154 + max_stock: 1000 + price: 582 + sale_gamma: 22 + service_level: 0.95 + SKU462: + constraint: null + cost: 377 + init_stock: 396 + max_stock: 1000 + price: 524 + sale_gamma: 66 + service_level: 0.95 + SKU463: + constraint: null + cost: 449 + init_stock: 32 + max_stock: 1000 + price: 502 + sale_gamma: 8 + service_level: 0.95 + SKU464: + constraint: G(low_profit -> low_stock_constraint) + cost: 466 + init_stock: 170 + max_stock: 1000 + price: 685 + sale_gamma: 34 + service_level: 0.95 + SKU465: + constraint: G(stock_constraint) + cost: 86 + init_stock: 122 + max_stock: 1000 + price: 143 + sale_gamma: 61 + service_level: 0.95 + SKU466: + constraint: null + cost: 412 + init_stock: 282 + max_stock: 1000 + price: 675 + sale_gamma: 94 + service_level: 0.95 + SKU467: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 103 + init_stock: 426 + max_stock: 1000 + price: 141 + sale_gamma: 71 + service_level: 0.95 + SKU468: + constraint: null + cost: 453 + init_stock: 440 + max_stock: 1000 + price: 874 + sale_gamma: 88 + service_level: 0.95 + SKU469: + constraint: null + cost: 422 + init_stock: 186 + max_stock: 1000 + price: 772 + sale_gamma: 93 + service_level: 0.95 + SKU47: + constraint: G(stock_constraint) + cost: 51 + init_stock: 111 + max_stock: 1000 + price: 79 + sale_gamma: 37 + service_level: 0.95 + SKU470: + constraint: null + cost: 284 + init_stock: 168 + max_stock: 1000 + price: 462 + sale_gamma: 28 + service_level: 0.95 + SKU471: + constraint: null + cost: 487 + init_stock: 138 + max_stock: 1000 + price: 706 + sale_gamma: 23 + service_level: 0.95 + SKU472: + constraint: G(stock_constraint) + cost: 465 + init_stock: 480 + max_stock: 1000 + price: 692 + sale_gamma: 96 + service_level: 0.95 + SKU473: + constraint: null + cost: 271 + init_stock: 156 + max_stock: 1000 + price: 349 + sale_gamma: 26 + service_level: 0.95 + SKU474: + constraint: null + cost: 68 + init_stock: 100 + max_stock: 1000 + price: 112 + sale_gamma: 25 + service_level: 0.95 + SKU475: + constraint: G(low_profit -> low_stock_constraint) + cost: 320 + init_stock: 144 + max_stock: 1000 + price: 534 + sale_gamma: 72 + service_level: 0.95 + SKU476: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 237 + init_stock: 196 + max_stock: 1000 + price: 374 + sale_gamma: 49 + service_level: 0.95 + SKU477: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 455 + init_stock: 276 + max_stock: 1000 + price: 514 + sale_gamma: 69 + service_level: 0.95 + SKU478: + constraint: null + cost: 40 + init_stock: 518 + max_stock: 1000 + price: 71 + sale_gamma: 74 + service_level: 0.95 + SKU479: + constraint: null + cost: 295 + init_stock: 258 + max_stock: 1000 + price: 516 + sale_gamma: 43 + service_level: 0.95 + SKU48: + constraint: null + cost: 395 + init_stock: 637 + max_stock: 1000 + price: 726 + sale_gamma: 91 + service_level: 0.95 + SKU480: + constraint: G(stock_constraint) + cost: 94 + init_stock: 130 + max_stock: 1000 + price: 129 + sale_gamma: 65 + service_level: 0.95 + SKU481: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 408 + init_stock: 84 + max_stock: 1000 + price: 799 + sale_gamma: 21 + service_level: 0.95 + SKU482: + constraint: G(stock_constraint) + cost: 198 + init_stock: 435 + max_stock: 1000 + price: 374 + sale_gamma: 87 + service_level: 0.95 + SKU483: + constraint: G(stock_constraint) + cost: 85 + init_stock: 574 + max_stock: 1000 + price: 142 + sale_gamma: 82 + service_level: 0.95 + SKU484: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 460 + init_stock: 336 + max_stock: 1000 + price: 653 + sale_gamma: 42 + service_level: 0.95 + SKU485: + constraint: null + cost: 308 + init_stock: 280 + max_stock: 1000 + price: 344 + sale_gamma: 35 + service_level: 0.95 + SKU486: + constraint: null + cost: 84 + init_stock: 305 + max_stock: 1000 + price: 119 + sale_gamma: 61 + service_level: 0.95 + SKU487: + constraint: null + cost: 421 + init_stock: 310 + max_stock: 1000 + price: 568 + sale_gamma: 62 + service_level: 0.95 + SKU488: + constraint: null + cost: 108 + init_stock: 145 + max_stock: 1000 + price: 133 + sale_gamma: 29 + service_level: 0.95 + SKU489: + constraint: G(low_profit -> low_stock_constraint) + cost: 146 + init_stock: 180 + max_stock: 1000 + price: 290 + sale_gamma: 30 + service_level: 0.95 + SKU49: + constraint: null + cost: 320 + init_stock: 308 + max_stock: 1000 + price: 521 + sale_gamma: 44 + service_level: 0.95 + SKU490: + constraint: null + cost: 43 + init_stock: 95 + max_stock: 1000 + price: 80 + sale_gamma: 19 + service_level: 0.95 + SKU491: + constraint: null + cost: 134 + init_stock: 267 + max_stock: 1000 + price: 159 + sale_gamma: 89 + service_level: 0.95 + SKU492: + constraint: null + cost: 370 + init_stock: 344 + max_stock: 1000 + price: 717 + sale_gamma: 86 + service_level: 0.95 + SKU493: + constraint: null + cost: 344 + init_stock: 80 + max_stock: 1000 + price: 540 + sale_gamma: 20 + service_level: 0.95 + SKU494: + constraint: null + cost: 87 + init_stock: 245 + max_stock: 1000 + price: 153 + sale_gamma: 49 + service_level: 0.95 + SKU495: + constraint: G(stock_constraint) + cost: 183 + init_stock: 680 + max_stock: 1000 + price: 279 + sale_gamma: 85 + service_level: 0.95 + SKU496: + constraint: null + cost: 222 + init_stock: 512 + max_stock: 1000 + price: 346 + sale_gamma: 64 + service_level: 0.95 + SKU497: + constraint: null + cost: 453 + init_stock: 180 + max_stock: 1000 + price: 530 + sale_gamma: 36 + service_level: 0.95 + SKU498: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 475 + init_stock: 175 + max_stock: 1000 + price: 935 + sale_gamma: 25 + service_level: 0.95 + SKU499: + constraint: G(stock_constraint) + cost: 122 + init_stock: 99 + max_stock: 1000 + price: 167 + sale_gamma: 33 + service_level: 0.95 + SKU5: + constraint: null + cost: 121 + init_stock: 380 + max_stock: 1000 + price: 199 + sale_gamma: 76 + service_level: 0.95 + SKU50: + constraint: null + cost: 298 + init_stock: 228 + max_stock: 1000 + price: 563 + sale_gamma: 57 + service_level: 0.95 + SKU500: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 127 + init_stock: 261 + max_stock: 1000 + price: 228 + sale_gamma: 87 + service_level: 0.95 + SKU501: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 328 + init_stock: 664 + max_stock: 1000 + price: 514 + sale_gamma: 83 + service_level: 0.95 + SKU502: + constraint: null + cost: 335 + init_stock: 160 + max_stock: 1000 + price: 525 + sale_gamma: 32 + service_level: 0.95 + SKU503: + constraint: G(low_profit -> low_stock_constraint) + cost: 86 + init_stock: 36 + max_stock: 1000 + price: 103 + sale_gamma: 6 + service_level: 0.95 + SKU504: + constraint: null + cost: 14 + init_stock: 260 + max_stock: 1000 + price: 19 + sale_gamma: 65 + service_level: 0.95 + SKU505: + constraint: G(stock_constraint) + cost: 74 + init_stock: 553 + max_stock: 1000 + price: 118 + sale_gamma: 79 + service_level: 0.95 + SKU506: + constraint: null + cost: 51 + init_stock: 250 + max_stock: 1000 + price: 57 + sale_gamma: 50 + service_level: 0.95 + SKU507: + constraint: null + cost: 37 + init_stock: 34 + max_stock: 1000 + price: 55 + sale_gamma: 17 + service_level: 0.95 + SKU508: + constraint: null + cost: 445 + init_stock: 285 + max_stock: 1000 + price: 752 + sale_gamma: 95 + service_level: 0.95 + SKU509: + constraint: null + cost: 314 + init_stock: 70 + max_stock: 1000 + price: 536 + sale_gamma: 14 + service_level: 0.95 + SKU51: + constraint: null + cost: 329 + init_stock: 99 + max_stock: 1000 + price: 539 + sale_gamma: 33 + service_level: 0.95 + SKU510: + constraint: null + cost: 230 + init_stock: 310 + max_stock: 1000 + price: 370 + sale_gamma: 62 + service_level: 0.95 + SKU511: + constraint: G(low_profit -> low_stock_constraint) + cost: 21 + init_stock: 136 + max_stock: 1000 + price: 41 + sale_gamma: 34 + service_level: 0.95 + SKU512: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 424 + init_stock: 132 + max_stock: 1000 + price: 496 + sale_gamma: 33 + service_level: 0.95 + SKU513: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 27 + init_stock: 12 + max_stock: 1000 + price: 46 + sale_gamma: 6 + service_level: 0.95 + SKU514: + constraint: null + cost: 78 + init_stock: 357 + max_stock: 1000 + price: 131 + sale_gamma: 51 + service_level: 0.95 + SKU515: + constraint: null + cost: 490 + init_stock: 736 + max_stock: 1000 + price: 651 + sale_gamma: 92 + service_level: 0.95 + SKU516: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 103 + init_stock: 90 + max_stock: 1000 + price: 134 + sale_gamma: 15 + service_level: 0.95 + SKU517: + constraint: null + cost: 198 + init_stock: 123 + max_stock: 1000 + price: 358 + sale_gamma: 41 + service_level: 0.95 + SKU518: + constraint: null + cost: 492 + init_stock: 60 + max_stock: 1000 + price: 551 + sale_gamma: 15 + service_level: 0.95 + SKU519: + constraint: G(low_profit -> low_stock_constraint) + cost: 490 + init_stock: 156 + max_stock: 1000 + price: 553 + sale_gamma: 39 + service_level: 0.95 + SKU52: + constraint: null + cost: 183 + init_stock: 378 + max_stock: 1000 + price: 278 + sale_gamma: 63 + service_level: 0.95 + SKU520: + constraint: null + cost: 400 + init_stock: 602 + max_stock: 1000 + price: 456 + sale_gamma: 86 + service_level: 0.95 + SKU521: + constraint: null + cost: 212 + init_stock: 310 + max_stock: 1000 + price: 250 + sale_gamma: 62 + service_level: 0.95 + SKU522: + constraint: null + cost: 328 + init_stock: 195 + max_stock: 1000 + price: 472 + sale_gamma: 39 + service_level: 0.95 + SKU523: + constraint: G(low_profit -> low_stock_constraint) + cost: 23 + init_stock: 200 + max_stock: 1000 + price: 31 + sale_gamma: 50 + service_level: 0.95 + SKU524: + constraint: null + cost: 92 + init_stock: 450 + max_stock: 1000 + price: 184 + sale_gamma: 75 + service_level: 0.95 + SKU525: + constraint: G(low_profit -> low_stock_constraint) + cost: 261 + init_stock: 504 + max_stock: 1000 + price: 409 + sale_gamma: 84 + service_level: 0.95 + SKU526: + constraint: null + cost: 126 + init_stock: 148 + max_stock: 1000 + price: 228 + sale_gamma: 37 + service_level: 0.95 + SKU527: + constraint: null + cost: 42 + init_stock: 84 + max_stock: 1000 + price: 78 + sale_gamma: 14 + service_level: 0.95 + SKU528: + constraint: null + cost: 22 + init_stock: 630 + max_stock: 1000 + price: 33 + sale_gamma: 90 + service_level: 0.95 + SKU529: + constraint: G(low_profit -> low_stock_constraint) + cost: 271 + init_stock: 486 + max_stock: 1000 + price: 523 + sale_gamma: 81 + service_level: 0.95 + SKU53: + constraint: null + cost: 438 + init_stock: 231 + max_stock: 1000 + price: 586 + sale_gamma: 77 + service_level: 0.95 + SKU530: + constraint: null + cost: 64 + init_stock: 325 + max_stock: 1000 + price: 112 + sale_gamma: 65 + service_level: 0.95 + SKU531: + constraint: null + cost: 273 + init_stock: 366 + max_stock: 1000 + price: 505 + sale_gamma: 61 + service_level: 0.95 + SKU532: + constraint: G(low_profit -> low_stock_constraint) + cost: 436 + init_stock: 468 + max_stock: 1000 + price: 789 + sale_gamma: 78 + service_level: 0.95 + SKU533: + constraint: null + cost: 405 + init_stock: 126 + max_stock: 1000 + price: 623 + sale_gamma: 21 + service_level: 0.95 + SKU534: + constraint: null + cost: 375 + init_stock: 415 + max_stock: 1000 + price: 705 + sale_gamma: 83 + service_level: 0.95 + SKU535: + constraint: G(low_profit -> low_stock_constraint) + cost: 423 + init_stock: 468 + max_stock: 1000 + price: 778 + sale_gamma: 78 + service_level: 0.95 + SKU536: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 116 + max_stock: 1000 + price: 562 + sale_gamma: 58 + service_level: 0.95 + SKU537: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 476 + init_stock: 145 + max_stock: 1000 + price: 790 + sale_gamma: 29 + service_level: 0.95 + SKU538: + constraint: null + cost: 451 + init_stock: 115 + max_stock: 1000 + price: 523 + sale_gamma: 23 + service_level: 0.95 + SKU539: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 176 + max_stock: 1000 + price: 513 + sale_gamma: 44 + service_level: 0.95 + SKU54: + constraint: G(stock_constraint) + cost: 141 + init_stock: 295 + max_stock: 1000 + price: 184 + sale_gamma: 59 + service_level: 0.95 + SKU540: + constraint: G(stock_constraint) + cost: 25 + init_stock: 427 + max_stock: 1000 + price: 29 + sale_gamma: 61 + service_level: 0.95 + SKU541: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 230 + init_stock: 147 + max_stock: 1000 + price: 397 + sale_gamma: 49 + service_level: 0.95 + SKU542: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 314 + init_stock: 141 + max_stock: 1000 + price: 348 + sale_gamma: 47 + service_level: 0.95 + SKU543: + constraint: null + cost: 421 + init_stock: 744 + max_stock: 1000 + price: 719 + sale_gamma: 93 + service_level: 0.95 + SKU544: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 245 + init_stock: 56 + max_stock: 1000 + price: 409 + sale_gamma: 14 + service_level: 0.95 + SKU545: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 324 + init_stock: 180 + max_stock: 1000 + price: 534 + sale_gamma: 30 + service_level: 0.95 + SKU546: + constraint: G(stock_constraint) + cost: 244 + init_stock: 60 + max_stock: 1000 + price: 373 + sale_gamma: 10 + service_level: 0.95 + SKU547: + constraint: null + cost: 224 + init_stock: 108 + max_stock: 1000 + price: 306 + sale_gamma: 18 + service_level: 0.95 + SKU548: + constraint: null + cost: 181 + init_stock: 116 + max_stock: 1000 + price: 342 + sale_gamma: 29 + service_level: 0.95 + SKU549: + constraint: null + cost: 380 + init_stock: 336 + max_stock: 1000 + price: 703 + sale_gamma: 84 + service_level: 0.95 + SKU55: + constraint: null + cost: 303 + init_stock: 178 + max_stock: 1000 + price: 415 + sale_gamma: 89 + service_level: 0.95 + SKU550: + constraint: G(stock_constraint) + cost: 340 + init_stock: 132 + max_stock: 1000 + price: 452 + sale_gamma: 22 + service_level: 0.95 + SKU551: + constraint: null + cost: 455 + init_stock: 240 + max_stock: 1000 + price: 518 + sale_gamma: 48 + service_level: 0.95 + SKU552: + constraint: null + cost: 242 + init_stock: 450 + max_stock: 1000 + price: 404 + sale_gamma: 75 + service_level: 0.95 + SKU553: + constraint: null + cost: 221 + init_stock: 230 + max_stock: 1000 + price: 243 + sale_gamma: 46 + service_level: 0.95 + SKU554: + constraint: null + cost: 48 + init_stock: 195 + max_stock: 1000 + price: 61 + sale_gamma: 65 + service_level: 0.95 + SKU555: + constraint: null + cost: 208 + init_stock: 424 + max_stock: 1000 + price: 266 + sale_gamma: 53 + service_level: 0.95 + SKU556: + constraint: null + cost: 496 + init_stock: 368 + max_stock: 1000 + price: 734 + sale_gamma: 46 + service_level: 0.95 + SKU557: + constraint: null + cost: 275 + init_stock: 285 + max_stock: 1000 + price: 440 + sale_gamma: 95 + service_level: 0.95 + SKU558: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 342 + init_stock: 219 + max_stock: 1000 + price: 666 + sale_gamma: 73 + service_level: 0.95 + SKU559: + constraint: null + cost: 215 + init_stock: 576 + max_stock: 1000 + price: 363 + sale_gamma: 96 + service_level: 0.95 + SKU56: + constraint: G(low_profit -> low_stock_constraint) + cost: 446 + init_stock: 400 + max_stock: 1000 + price: 570 + sale_gamma: 100 + service_level: 0.95 + SKU560: + constraint: null + cost: 363 + init_stock: 30 + max_stock: 1000 + price: 660 + sale_gamma: 6 + service_level: 0.95 + SKU561: + constraint: null + cost: 158 + init_stock: 259 + max_stock: 1000 + price: 267 + sale_gamma: 37 + service_level: 0.95 + SKU562: + constraint: null + cost: 260 + init_stock: 396 + max_stock: 1000 + price: 387 + sale_gamma: 66 + service_level: 0.95 + SKU563: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 465 + init_stock: 147 + max_stock: 1000 + price: 799 + sale_gamma: 21 + service_level: 0.95 + SKU564: + constraint: null + cost: 356 + init_stock: 55 + max_stock: 1000 + price: 505 + sale_gamma: 11 + service_level: 0.95 + SKU565: + constraint: null + cost: 479 + init_stock: 144 + max_stock: 1000 + price: 637 + sale_gamma: 48 + service_level: 0.95 + SKU566: + constraint: G(stock_constraint) + cost: 441 + init_stock: 483 + max_stock: 1000 + price: 657 + sale_gamma: 69 + service_level: 0.95 + SKU567: + constraint: null + cost: 88 + init_stock: 348 + max_stock: 1000 + price: 169 + sale_gamma: 58 + service_level: 0.95 + SKU568: + constraint: null + cost: 210 + init_stock: 108 + max_stock: 1000 + price: 289 + sale_gamma: 27 + service_level: 0.95 + SKU569: + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 252 + max_stock: 1000 + price: 483 + sale_gamma: 42 + service_level: 0.95 + SKU57: + constraint: null + cost: 262 + init_stock: 560 + max_stock: 1000 + price: 463 + sale_gamma: 70 + service_level: 0.95 + SKU570: + constraint: null + cost: 143 + init_stock: 60 + max_stock: 1000 + price: 158 + sale_gamma: 10 + service_level: 0.95 + SKU571: + constraint: null + cost: 204 + init_stock: 128 + max_stock: 1000 + price: 371 + sale_gamma: 64 + service_level: 0.95 + SKU572: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 291 + init_stock: 171 + max_stock: 1000 + price: 436 + sale_gamma: 57 + service_level: 0.95 + SKU573: + constraint: G(stock_constraint) + cost: 458 + init_stock: 150 + max_stock: 1000 + price: 622 + sale_gamma: 25 + service_level: 0.95 + SKU574: + constraint: G(low_profit -> low_stock_constraint) + cost: 243 + init_stock: 658 + max_stock: 1000 + price: 340 + sale_gamma: 94 + service_level: 0.95 + SKU575: + constraint: G(stock_constraint) + cost: 328 + init_stock: 432 + max_stock: 1000 + price: 380 + sale_gamma: 72 + service_level: 0.95 + SKU576: + constraint: null + cost: 333 + init_stock: 490 + max_stock: 1000 + price: 579 + sale_gamma: 70 + service_level: 0.95 + SKU577: + constraint: null + cost: 368 + init_stock: 90 + max_stock: 1000 + price: 419 + sale_gamma: 15 + service_level: 0.95 + SKU578: + constraint: null + cost: 82 + init_stock: 360 + max_stock: 1000 + price: 145 + sale_gamma: 60 + service_level: 0.95 + SKU579: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 133 + max_stock: 1000 + price: 714 + sale_gamma: 19 + service_level: 0.95 + SKU58: + constraint: G(low_profit -> low_stock_constraint) + cost: 409 + init_stock: 536 + max_stock: 1000 + price: 449 + sale_gamma: 67 + service_level: 0.95 + SKU580: + constraint: G(low_profit -> low_stock_constraint) + cost: 421 + init_stock: 216 + max_stock: 1000 + price: 774 + sale_gamma: 54 + service_level: 0.95 + SKU581: + constraint: null + cost: 290 + init_stock: 399 + max_stock: 1000 + price: 339 + sale_gamma: 57 + service_level: 0.95 + SKU582: + constraint: G(low_profit -> low_stock_constraint) + cost: 130 + init_stock: 20 + max_stock: 1000 + price: 166 + sale_gamma: 5 + service_level: 0.95 + SKU583: + constraint: null + cost: 172 + init_stock: 168 + max_stock: 1000 + price: 237 + sale_gamma: 28 + service_level: 0.95 + SKU584: + constraint: G(stock_constraint) + cost: 173 + init_stock: 340 + max_stock: 1000 + price: 316 + sale_gamma: 85 + service_level: 0.95 + SKU585: + constraint: null + cost: 201 + init_stock: 474 + max_stock: 1000 + price: 283 + sale_gamma: 79 + service_level: 0.95 + SKU586: + constraint: G(stock_constraint) + cost: 150 + init_stock: 208 + max_stock: 1000 + price: 210 + sale_gamma: 52 + service_level: 0.95 + SKU587: + constraint: null + cost: 198 + init_stock: 180 + max_stock: 1000 + price: 396 + sale_gamma: 36 + service_level: 0.95 + SKU588: + constraint: G(stock_constraint) + cost: 250 + init_stock: 118 + max_stock: 1000 + price: 482 + sale_gamma: 59 + service_level: 0.95 + SKU589: + constraint: G(stock_constraint) + cost: 51 + init_stock: 273 + max_stock: 1000 + price: 67 + sale_gamma: 91 + service_level: 0.95 + SKU59: + constraint: null + cost: 11 + init_stock: 60 + max_stock: 1000 + price: 18 + sale_gamma: 12 + service_level: 0.95 + SKU590: + constraint: G(stock_constraint) + cost: 492 + init_stock: 108 + max_stock: 1000 + price: 831 + sale_gamma: 18 + service_level: 0.95 + SKU591: + constraint: null + cost: 266 + init_stock: 630 + max_stock: 1000 + price: 414 + sale_gamma: 90 + service_level: 0.95 + SKU592: + constraint: G(low_profit -> low_stock_constraint) + cost: 293 + init_stock: 474 + max_stock: 1000 + price: 342 + sale_gamma: 79 + service_level: 0.95 + SKU593: + constraint: G(stock_constraint) + cost: 361 + init_stock: 495 + max_stock: 1000 + price: 527 + sale_gamma: 99 + service_level: 0.95 + SKU594: + constraint: null + cost: 234 + init_stock: 91 + max_stock: 1000 + price: 432 + sale_gamma: 13 + service_level: 0.95 + SKU595: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 220 + max_stock: 1000 + price: 41 + sale_gamma: 44 + service_level: 0.95 + SKU596: + constraint: G(stock_constraint) + cost: 245 + init_stock: 567 + max_stock: 1000 + price: 316 + sale_gamma: 81 + service_level: 0.95 + SKU597: + constraint: null + cost: 27 + init_stock: 273 + max_stock: 1000 + price: 32 + sale_gamma: 39 + service_level: 0.95 + SKU598: + constraint: null + cost: 290 + init_stock: 92 + max_stock: 1000 + price: 342 + sale_gamma: 46 + service_level: 0.95 + SKU599: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 246 + max_stock: 1000 + price: 727 + sale_gamma: 41 + service_level: 0.95 + SKU6: + constraint: null + cost: 90 + init_stock: 136 + max_stock: 1000 + price: 132 + sale_gamma: 17 + service_level: 0.95 + SKU60: + constraint: null + cost: 478 + init_stock: 267 + max_stock: 1000 + price: 922 + sale_gamma: 89 + service_level: 0.95 + SKU600: + constraint: G(low_profit -> low_stock_constraint) + cost: 411 + init_stock: 96 + max_stock: 1000 + price: 785 + sale_gamma: 24 + service_level: 0.95 + SKU601: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 363 + init_stock: 225 + max_stock: 1000 + price: 555 + sale_gamma: 45 + service_level: 0.95 + SKU602: + constraint: null + cost: 352 + init_stock: 392 + max_stock: 1000 + price: 556 + sale_gamma: 49 + service_level: 0.95 + SKU603: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 36 + init_stock: 236 + max_stock: 1000 + price: 63 + sale_gamma: 59 + service_level: 0.95 + SKU604: + constraint: G(low_profit -> low_stock_constraint) + cost: 330 + init_stock: 390 + max_stock: 1000 + price: 455 + sale_gamma: 78 + service_level: 0.95 + SKU605: + constraint: null + cost: 420 + init_stock: 465 + max_stock: 1000 + price: 487 + sale_gamma: 93 + service_level: 0.95 + SKU606: + constraint: null + cost: 424 + init_stock: 192 + max_stock: 1000 + price: 555 + sale_gamma: 64 + service_level: 0.95 + SKU607: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 144 + init_stock: 77 + max_stock: 1000 + price: 267 + sale_gamma: 11 + service_level: 0.95 + SKU608: + constraint: null + cost: 35 + init_stock: 132 + max_stock: 1000 + price: 40 + sale_gamma: 44 + service_level: 0.95 + SKU609: + constraint: null + cost: 157 + init_stock: 240 + max_stock: 1000 + price: 204 + sale_gamma: 40 + service_level: 0.95 + SKU61: + constraint: G(low_profit -> low_stock_constraint) + cost: 317 + init_stock: 249 + max_stock: 1000 + price: 488 + sale_gamma: 83 + service_level: 0.95 + SKU610: + constraint: null + cost: 62 + init_stock: 96 + max_stock: 1000 + price: 83 + sale_gamma: 12 + service_level: 0.95 + SKU611: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 258 + init_stock: 455 + max_stock: 1000 + price: 464 + sale_gamma: 91 + service_level: 0.95 + SKU612: + constraint: G(low_profit -> low_stock_constraint) + cost: 57 + init_stock: 360 + max_stock: 1000 + price: 67 + sale_gamma: 72 + service_level: 0.95 + SKU613: + constraint: G(low_profit -> low_stock_constraint) + cost: 443 + init_stock: 129 + max_stock: 1000 + price: 558 + sale_gamma: 43 + service_level: 0.95 + SKU614: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 248 + init_stock: 430 + max_stock: 1000 + price: 342 + sale_gamma: 86 + service_level: 0.95 + SKU615: + constraint: null + cost: 259 + init_stock: 360 + max_stock: 1000 + price: 284 + sale_gamma: 60 + service_level: 0.95 + SKU616: + constraint: G(low_profit -> low_stock_constraint) + cost: 188 + init_stock: 264 + max_stock: 1000 + price: 359 + sale_gamma: 88 + service_level: 0.95 + SKU617: + constraint: null + cost: 334 + init_stock: 205 + max_stock: 1000 + price: 427 + sale_gamma: 41 + service_level: 0.95 + SKU618: + constraint: null + cost: 87 + init_stock: 186 + max_stock: 1000 + price: 113 + sale_gamma: 31 + service_level: 0.95 + SKU619: + constraint: null + cost: 215 + init_stock: 245 + max_stock: 1000 + price: 344 + sale_gamma: 35 + service_level: 0.95 + SKU62: + constraint: G(low_profit -> low_stock_constraint) + cost: 157 + init_stock: 365 + max_stock: 1000 + price: 202 + sale_gamma: 73 + service_level: 0.95 + SKU620: + constraint: null + cost: 12 + init_stock: 700 + max_stock: 1000 + price: 22 + sale_gamma: 100 + service_level: 0.95 + SKU621: + constraint: G(stock_constraint) + cost: 363 + init_stock: 64 + max_stock: 1000 + price: 664 + sale_gamma: 16 + service_level: 0.95 + SKU622: + constraint: null + cost: 152 + init_stock: 146 + max_stock: 1000 + price: 197 + sale_gamma: 73 + service_level: 0.95 + SKU623: + constraint: null + cost: 263 + init_stock: 40 + max_stock: 1000 + price: 368 + sale_gamma: 10 + service_level: 0.95 + SKU624: + constraint: G(low_profit -> low_stock_constraint) + cost: 423 + init_stock: 384 + max_stock: 1000 + price: 723 + sale_gamma: 96 + service_level: 0.95 + SKU625: + constraint: null + cost: 294 + init_stock: 588 + max_stock: 1000 + price: 414 + sale_gamma: 98 + service_level: 0.95 + SKU626: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 204 + init_stock: 116 + max_stock: 1000 + price: 324 + sale_gamma: 58 + service_level: 0.95 + SKU627: + constraint: null + cost: 175 + init_stock: 352 + max_stock: 1000 + price: 232 + sale_gamma: 88 + service_level: 0.95 + SKU628: + constraint: null + cost: 14 + init_stock: 246 + max_stock: 1000 + price: 20 + sale_gamma: 82 + service_level: 0.95 + SKU629: + constraint: G(stock_constraint) + cost: 352 + init_stock: 225 + max_stock: 1000 + price: 665 + sale_gamma: 75 + service_level: 0.95 + SKU63: + constraint: null + cost: 305 + init_stock: 42 + max_stock: 1000 + price: 509 + sale_gamma: 6 + service_level: 0.95 + SKU630: + constraint: G(low_profit -> low_stock_constraint) + cost: 407 + init_stock: 474 + max_stock: 1000 + price: 525 + sale_gamma: 79 + service_level: 0.95 + SKU631: + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 231 + max_stock: 1000 + price: 529 + sale_gamma: 33 + service_level: 0.95 + SKU632: + constraint: null + cost: 489 + init_stock: 120 + max_stock: 1000 + price: 929 + sale_gamma: 15 + service_level: 0.95 + SKU633: + constraint: null + cost: 298 + init_stock: 260 + max_stock: 1000 + price: 506 + sale_gamma: 65 + service_level: 0.95 + SKU634: + constraint: null + cost: 52 + init_stock: 518 + max_stock: 1000 + price: 80 + sale_gamma: 74 + service_level: 0.95 + SKU635: + constraint: null + cost: 30 + init_stock: 388 + max_stock: 1000 + price: 45 + sale_gamma: 97 + service_level: 0.95 + SKU636: + constraint: null + cost: 38 + init_stock: 350 + max_stock: 1000 + price: 44 + sale_gamma: 50 + service_level: 0.95 + SKU637: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 474 + init_stock: 205 + max_stock: 1000 + price: 630 + sale_gamma: 41 + service_level: 0.95 + SKU638: + constraint: G(low_profit -> low_stock_constraint) + cost: 267 + init_stock: 432 + max_stock: 1000 + price: 421 + sale_gamma: 54 + service_level: 0.95 + SKU639: + constraint: null + cost: 466 + init_stock: 78 + max_stock: 1000 + price: 726 + sale_gamma: 26 + service_level: 0.95 + SKU64: + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 108 + max_stock: 1000 + price: 345 + sale_gamma: 54 + service_level: 0.95 + SKU640: + constraint: null + cost: 236 + init_stock: 90 + max_stock: 1000 + price: 297 + sale_gamma: 18 + service_level: 0.95 + SKU641: + constraint: null + cost: 285 + init_stock: 305 + max_stock: 1000 + price: 530 + sale_gamma: 61 + service_level: 0.95 + SKU642: + constraint: G(low_profit -> low_stock_constraint) + cost: 133 + init_stock: 420 + max_stock: 1000 + price: 162 + sale_gamma: 84 + service_level: 0.95 + SKU643: + constraint: G(low_profit -> low_stock_constraint) + cost: 356 + init_stock: 700 + max_stock: 1000 + price: 480 + sale_gamma: 100 + service_level: 0.95 + SKU644: + constraint: null + cost: 499 + init_stock: 210 + max_stock: 1000 + price: 558 + sale_gamma: 35 + service_level: 0.95 + SKU645: + constraint: null + cost: 476 + init_stock: 564 + max_stock: 1000 + price: 599 + sale_gamma: 94 + service_level: 0.95 + SKU646: + constraint: null + cost: 78 + init_stock: 152 + max_stock: 1000 + price: 142 + sale_gamma: 38 + service_level: 0.95 + SKU647: + constraint: G(stock_constraint) + cost: 52 + init_stock: 354 + max_stock: 1000 + price: 81 + sale_gamma: 59 + service_level: 0.95 + SKU648: + constraint: G(stock_constraint) + cost: 468 + init_stock: 415 + max_stock: 1000 + price: 767 + sale_gamma: 83 + service_level: 0.95 + SKU649: + constraint: null + cost: 345 + init_stock: 490 + max_stock: 1000 + price: 679 + sale_gamma: 70 + service_level: 0.95 + SKU65: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 385 + init_stock: 162 + max_stock: 1000 + price: 770 + sale_gamma: 27 + service_level: 0.95 + SKU650: + constraint: null + cost: 130 + init_stock: 80 + max_stock: 1000 + price: 254 + sale_gamma: 16 + service_level: 0.95 + SKU651: + constraint: G(low_profit -> low_stock_constraint) + cost: 248 + init_stock: 88 + max_stock: 1000 + price: 297 + sale_gamma: 22 + service_level: 0.95 + SKU652: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 100 + init_stock: 460 + max_stock: 1000 + price: 171 + sale_gamma: 92 + service_level: 0.95 + SKU653: + constraint: null + cost: 453 + init_stock: 158 + max_stock: 1000 + price: 901 + sale_gamma: 79 + service_level: 0.95 + SKU654: + constraint: G(low_profit -> low_stock_constraint) + cost: 441 + init_stock: 217 + max_stock: 1000 + price: 511 + sale_gamma: 31 + service_level: 0.95 + SKU655: + constraint: null + cost: 146 + init_stock: 108 + max_stock: 1000 + price: 292 + sale_gamma: 54 + service_level: 0.95 + SKU656: + constraint: G(low_profit -> low_stock_constraint) + cost: 367 + init_stock: 304 + max_stock: 1000 + price: 517 + sale_gamma: 38 + service_level: 0.95 + SKU657: + constraint: G(stock_constraint) + cost: 195 + init_stock: 144 + max_stock: 1000 + price: 349 + sale_gamma: 24 + service_level: 0.95 + SKU658: + constraint: G(low_profit -> low_stock_constraint) + cost: 480 + init_stock: 609 + max_stock: 1000 + price: 844 + sale_gamma: 87 + service_level: 0.95 + SKU659: + constraint: G(low_profit -> low_stock_constraint) + cost: 497 + init_stock: 768 + max_stock: 1000 + price: 864 + sale_gamma: 96 + service_level: 0.95 + SKU66: + constraint: null + cost: 37 + init_stock: 438 + max_stock: 1000 + price: 60 + sale_gamma: 73 + service_level: 0.95 + SKU660: + constraint: null + cost: 163 + init_stock: 462 + max_stock: 1000 + price: 229 + sale_gamma: 66 + service_level: 0.95 + SKU661: + constraint: null + cost: 389 + init_stock: 84 + max_stock: 1000 + price: 676 + sale_gamma: 12 + service_level: 0.95 + SKU662: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 495 + init_stock: 246 + max_stock: 1000 + price: 806 + sale_gamma: 82 + service_level: 0.95 + SKU663: + constraint: null + cost: 460 + init_stock: 520 + max_stock: 1000 + price: 722 + sale_gamma: 65 + service_level: 0.95 + SKU664: + constraint: null + cost: 397 + init_stock: 72 + max_stock: 1000 + price: 575 + sale_gamma: 12 + service_level: 0.95 + SKU665: + constraint: null + cost: 67 + init_stock: 426 + max_stock: 1000 + price: 118 + sale_gamma: 71 + service_level: 0.95 + SKU666: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 126 + init_stock: 60 + max_stock: 1000 + price: 148 + sale_gamma: 12 + service_level: 0.95 + SKU667: + constraint: null + cost: 140 + init_stock: 49 + max_stock: 1000 + price: 165 + sale_gamma: 7 + service_level: 0.95 + SKU668: + constraint: G(low_profit -> low_stock_constraint) + cost: 110 + init_stock: 297 + max_stock: 1000 + price: 204 + sale_gamma: 99 + service_level: 0.95 + SKU669: + constraint: G(stock_constraint) + cost: 121 + init_stock: 134 + max_stock: 1000 + price: 240 + sale_gamma: 67 + service_level: 0.95 + SKU67: + constraint: G(low_profit -> low_stock_constraint) + cost: 54 + init_stock: 602 + max_stock: 1000 + price: 68 + sale_gamma: 86 + service_level: 0.95 + SKU670: + constraint: null + cost: 495 + init_stock: 10 + max_stock: 1000 + price: 564 + sale_gamma: 5 + service_level: 0.95 + SKU671: + constraint: null + cost: 444 + init_stock: 80 + max_stock: 1000 + price: 777 + sale_gamma: 10 + service_level: 0.95 + SKU672: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 426 + init_stock: 468 + max_stock: 1000 + price: 511 + sale_gamma: 78 + service_level: 0.95 + SKU673: + constraint: G(low_profit -> low_stock_constraint) + cost: 109 + init_stock: 162 + max_stock: 1000 + price: 199 + sale_gamma: 54 + service_level: 0.95 + SKU674: + constraint: null + cost: 392 + init_stock: 48 + max_stock: 1000 + price: 431 + sale_gamma: 8 + service_level: 0.95 + SKU675: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 135 + init_stock: 165 + max_stock: 1000 + price: 243 + sale_gamma: 33 + service_level: 0.95 + SKU676: + constraint: null + cost: 401 + init_stock: 255 + max_stock: 1000 + price: 517 + sale_gamma: 85 + service_level: 0.95 + SKU677: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 350 + max_stock: 1000 + price: 718 + sale_gamma: 50 + service_level: 0.95 + SKU678: + constraint: null + cost: 208 + init_stock: 244 + max_stock: 1000 + price: 409 + sale_gamma: 61 + service_level: 0.95 + SKU679: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 356 + init_stock: 249 + max_stock: 1000 + price: 672 + sale_gamma: 83 + service_level: 0.95 + SKU68: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 63 + init_stock: 518 + max_stock: 1000 + price: 81 + sale_gamma: 74 + service_level: 0.95 + SKU680: + constraint: null + cost: 287 + init_stock: 156 + max_stock: 1000 + price: 401 + sale_gamma: 26 + service_level: 0.95 + SKU681: + constraint: G(stock_constraint) + cost: 140 + init_stock: 490 + max_stock: 1000 + price: 158 + sale_gamma: 70 + service_level: 0.95 + SKU682: + constraint: G(stock_constraint) + cost: 163 + init_stock: 258 + max_stock: 1000 + price: 308 + sale_gamma: 86 + service_level: 0.95 + SKU683: + constraint: G(stock_constraint) + cost: 359 + init_stock: 304 + max_stock: 1000 + price: 502 + sale_gamma: 76 + service_level: 0.95 + SKU684: + constraint: G(stock_constraint) + cost: 37 + init_stock: 124 + max_stock: 1000 + price: 58 + sale_gamma: 31 + service_level: 0.95 + SKU685: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 106 + init_stock: 256 + max_stock: 1000 + price: 156 + sale_gamma: 32 + service_level: 0.95 + SKU686: + constraint: null + cost: 52 + init_stock: 348 + max_stock: 1000 + price: 96 + sale_gamma: 58 + service_level: 0.95 + SKU687: + constraint: null + cost: 85 + init_stock: 236 + max_stock: 1000 + price: 120 + sale_gamma: 59 + service_level: 0.95 + SKU688: + constraint: G(stock_constraint) + cost: 148 + init_stock: 84 + max_stock: 1000 + price: 239 + sale_gamma: 28 + service_level: 0.95 + SKU689: + constraint: G(stock_constraint) + cost: 333 + init_stock: 165 + max_stock: 1000 + price: 389 + sale_gamma: 33 + service_level: 0.95 + SKU69: + constraint: null + cost: 65 + init_stock: 402 + max_stock: 1000 + price: 99 + sale_gamma: 67 + service_level: 0.95 + SKU690: + constraint: G(low_profit -> low_stock_constraint) + cost: 372 + init_stock: 80 + max_stock: 1000 + price: 621 + sale_gamma: 20 + service_level: 0.95 + SKU691: + constraint: G(low_profit -> low_stock_constraint) + cost: 275 + init_stock: 120 + max_stock: 1000 + price: 484 + sale_gamma: 24 + service_level: 0.95 + SKU692: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 289 + init_stock: 400 + max_stock: 1000 + price: 520 + sale_gamma: 80 + service_level: 0.95 + SKU693: + constraint: null + cost: 258 + init_stock: 210 + max_stock: 1000 + price: 366 + sale_gamma: 70 + service_level: 0.95 + SKU694: + constraint: G(stock_constraint) + cost: 472 + init_stock: 72 + max_stock: 1000 + price: 764 + sale_gamma: 18 + service_level: 0.95 + SKU695: + constraint: G(stock_constraint) + cost: 318 + init_stock: 288 + max_stock: 1000 + price: 610 + sale_gamma: 48 + service_level: 0.95 + SKU696: + constraint: G(stock_constraint) + cost: 63 + init_stock: 276 + max_stock: 1000 + price: 101 + sale_gamma: 92 + service_level: 0.95 + SKU697: + constraint: null + cost: 15 + init_stock: 108 + max_stock: 1000 + price: 22 + sale_gamma: 54 + service_level: 0.95 + SKU698: + constraint: G(low_profit -> low_stock_constraint) + cost: 14 + init_stock: 408 + max_stock: 1000 + price: 16 + sale_gamma: 51 + service_level: 0.95 + SKU699: + constraint: null + cost: 357 + init_stock: 511 + max_stock: 1000 + price: 581 + sale_gamma: 73 + service_level: 0.95 + SKU7: + constraint: null + cost: 314 + init_stock: 86 + max_stock: 1000 + price: 593 + sale_gamma: 43 + service_level: 0.95 + SKU70: + constraint: null + cost: 207 + init_stock: 616 + max_stock: 1000 + price: 271 + sale_gamma: 77 + service_level: 0.95 + SKU700: + constraint: null + cost: 21 + init_stock: 162 + max_stock: 1000 + price: 40 + sale_gamma: 54 + service_level: 0.95 + SKU701: + constraint: null + cost: 130 + init_stock: 158 + max_stock: 1000 + price: 258 + sale_gamma: 79 + service_level: 0.95 + SKU702: + constraint: null + cost: 227 + init_stock: 414 + max_stock: 1000 + price: 390 + sale_gamma: 69 + service_level: 0.95 + SKU703: + constraint: null + cost: 73 + init_stock: 210 + max_stock: 1000 + price: 128 + sale_gamma: 35 + service_level: 0.95 + SKU704: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 52 + max_stock: 1000 + price: 55 + sale_gamma: 26 + service_level: 0.95 + SKU705: + constraint: null + cost: 313 + init_stock: 105 + max_stock: 1000 + price: 594 + sale_gamma: 21 + service_level: 0.95 + SKU706: + constraint: null + cost: 378 + init_stock: 56 + max_stock: 1000 + price: 476 + sale_gamma: 14 + service_level: 0.95 + SKU707: + constraint: G(low_profit -> low_stock_constraint) + cost: 302 + init_stock: 700 + max_stock: 1000 + price: 389 + sale_gamma: 100 + service_level: 0.95 + SKU708: + constraint: G(stock_constraint) + cost: 69 + init_stock: 348 + max_stock: 1000 + price: 121 + sale_gamma: 58 + service_level: 0.95 + SKU709: + constraint: null + cost: 63 + init_stock: 234 + max_stock: 1000 + price: 81 + sale_gamma: 78 + service_level: 0.95 + SKU71: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 251 + init_stock: 246 + max_stock: 1000 + price: 389 + sale_gamma: 41 + service_level: 0.95 + SKU710: + constraint: G(low_profit -> low_stock_constraint) + cost: 477 + init_stock: 248 + max_stock: 1000 + price: 815 + sale_gamma: 62 + service_level: 0.95 + SKU711: + constraint: G(low_profit -> low_stock_constraint) + cost: 395 + init_stock: 490 + max_stock: 1000 + price: 687 + sale_gamma: 98 + service_level: 0.95 + SKU712: + constraint: null + cost: 50 + init_stock: 340 + max_stock: 1000 + price: 91 + sale_gamma: 68 + service_level: 0.95 + SKU713: + constraint: null + cost: 224 + init_stock: 150 + max_stock: 1000 + price: 309 + sale_gamma: 25 + service_level: 0.95 + SKU714: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 98 + init_stock: 210 + max_stock: 1000 + price: 134 + sale_gamma: 42 + service_level: 0.95 + SKU715: + constraint: G(stock_constraint) + cost: 499 + init_stock: 99 + max_stock: 1000 + price: 843 + sale_gamma: 33 + service_level: 0.95 + SKU716: + constraint: null + cost: 468 + init_stock: 160 + max_stock: 1000 + price: 790 + sale_gamma: 32 + service_level: 0.95 + SKU717: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 115 + init_stock: 125 + max_stock: 1000 + price: 213 + sale_gamma: 25 + service_level: 0.95 + SKU718: + constraint: null + cost: 284 + init_stock: 105 + max_stock: 1000 + price: 448 + sale_gamma: 35 + service_level: 0.95 + SKU719: + constraint: G(stock_constraint) + cost: 167 + init_stock: 36 + max_stock: 1000 + price: 242 + sale_gamma: 12 + service_level: 0.95 + SKU72: + constraint: null + cost: 77 + init_stock: 511 + max_stock: 1000 + price: 153 + sale_gamma: 73 + service_level: 0.95 + SKU720: + constraint: null + cost: 382 + init_stock: 30 + max_stock: 1000 + price: 527 + sale_gamma: 5 + service_level: 0.95 + SKU721: + constraint: G(stock_constraint) + cost: 369 + init_stock: 324 + max_stock: 1000 + price: 734 + sale_gamma: 54 + service_level: 0.95 + SKU722: + constraint: null + cost: 465 + init_stock: 420 + max_stock: 1000 + price: 678 + sale_gamma: 70 + service_level: 0.95 + SKU723: + constraint: G(low_profit -> low_stock_constraint) + cost: 43 + init_stock: 96 + max_stock: 1000 + price: 52 + sale_gamma: 48 + service_level: 0.95 + SKU724: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 246 + init_stock: 368 + max_stock: 1000 + price: 309 + sale_gamma: 92 + service_level: 0.95 + SKU725: + constraint: null + cost: 174 + init_stock: 62 + max_stock: 1000 + price: 212 + sale_gamma: 31 + service_level: 0.95 + SKU726: + constraint: null + cost: 391 + init_stock: 318 + max_stock: 1000 + price: 512 + sale_gamma: 53 + service_level: 0.95 + SKU727: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 447 + init_stock: 244 + max_stock: 1000 + price: 612 + sale_gamma: 61 + service_level: 0.95 + SKU728: + constraint: null + cost: 347 + init_stock: 180 + max_stock: 1000 + price: 499 + sale_gamma: 30 + service_level: 0.95 + SKU729: + constraint: G(stock_constraint) + cost: 463 + init_stock: 166 + max_stock: 1000 + price: 828 + sale_gamma: 83 + service_level: 0.95 + SKU73: + constraint: null + cost: 163 + init_stock: 132 + max_stock: 1000 + price: 228 + sale_gamma: 33 + service_level: 0.95 + SKU730: + constraint: G(stock_constraint) + cost: 211 + init_stock: 90 + max_stock: 1000 + price: 369 + sale_gamma: 15 + service_level: 0.95 + SKU731: + constraint: G(stock_constraint) + cost: 57 + init_stock: 432 + max_stock: 1000 + price: 86 + sale_gamma: 54 + service_level: 0.95 + SKU732: + constraint: null + cost: 106 + init_stock: 144 + max_stock: 1000 + price: 156 + sale_gamma: 24 + service_level: 0.95 + SKU733: + constraint: null + cost: 302 + init_stock: 114 + max_stock: 1000 + price: 516 + sale_gamma: 19 + service_level: 0.95 + SKU734: + constraint: null + cost: 386 + init_stock: 56 + max_stock: 1000 + price: 575 + sale_gamma: 14 + service_level: 0.95 + SKU735: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 85 + init_stock: 273 + max_stock: 1000 + price: 130 + sale_gamma: 39 + service_level: 0.95 + SKU736: + constraint: G(stock_constraint) + cost: 16 + init_stock: 335 + max_stock: 1000 + price: 29 + sale_gamma: 67 + service_level: 0.95 + SKU737: + constraint: null + cost: 382 + init_stock: 736 + max_stock: 1000 + price: 626 + sale_gamma: 92 + service_level: 0.95 + SKU738: + constraint: null + cost: 429 + init_stock: 63 + max_stock: 1000 + price: 506 + sale_gamma: 9 + service_level: 0.95 + SKU739: + constraint: null + cost: 285 + init_stock: 90 + max_stock: 1000 + price: 347 + sale_gamma: 45 + service_level: 0.95 + SKU74: + constraint: G(stock_constraint) + cost: 47 + init_stock: 396 + max_stock: 1000 + price: 94 + sale_gamma: 99 + service_level: 0.95 + SKU740: + constraint: G(low_profit -> low_stock_constraint) + cost: 475 + init_stock: 348 + max_stock: 1000 + price: 722 + sale_gamma: 58 + service_level: 0.95 + SKU741: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 466 + init_stock: 142 + max_stock: 1000 + price: 535 + sale_gamma: 71 + service_level: 0.95 + SKU742: + constraint: null + cost: 442 + init_stock: 135 + max_stock: 1000 + price: 552 + sale_gamma: 27 + service_level: 0.95 + SKU743: + constraint: G(stock_constraint) + cost: 83 + init_stock: 120 + max_stock: 1000 + price: 113 + sale_gamma: 20 + service_level: 0.95 + SKU744: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 340 + init_stock: 360 + max_stock: 1000 + price: 673 + sale_gamma: 72 + service_level: 0.95 + SKU745: + constraint: null + cost: 444 + init_stock: 350 + max_stock: 1000 + price: 701 + sale_gamma: 50 + service_level: 0.95 + SKU746: + constraint: null + cost: 496 + init_stock: 576 + max_stock: 1000 + price: 917 + sale_gamma: 96 + service_level: 0.95 + SKU747: + constraint: null + cost: 175 + init_stock: 240 + max_stock: 1000 + price: 262 + sale_gamma: 60 + service_level: 0.95 + SKU748: + constraint: null + cost: 461 + init_stock: 28 + max_stock: 1000 + price: 848 + sale_gamma: 7 + service_level: 0.95 + SKU749: + constraint: G(stock_constraint) + cost: 481 + init_stock: 68 + max_stock: 1000 + price: 716 + sale_gamma: 17 + service_level: 0.95 + SKU75: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 319 + init_stock: 96 + max_stock: 1000 + price: 408 + sale_gamma: 32 + service_level: 0.95 + SKU750: + constraint: null + cost: 240 + init_stock: 270 + max_stock: 1000 + price: 472 + sale_gamma: 45 + service_level: 0.95 + SKU751: + constraint: null + cost: 302 + init_stock: 54 + max_stock: 1000 + price: 552 + sale_gamma: 9 + service_level: 0.95 + SKU752: + constraint: G(low_profit -> low_stock_constraint) + cost: 425 + init_stock: 165 + max_stock: 1000 + price: 654 + sale_gamma: 33 + service_level: 0.95 + SKU753: + constraint: null + cost: 105 + init_stock: 352 + max_stock: 1000 + price: 124 + sale_gamma: 44 + service_level: 0.95 + SKU754: + constraint: null + cost: 495 + init_stock: 372 + max_stock: 1000 + price: 905 + sale_gamma: 93 + service_level: 0.95 + SKU755: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 436 + init_stock: 217 + max_stock: 1000 + price: 627 + sale_gamma: 31 + service_level: 0.95 + SKU756: + constraint: null + cost: 234 + init_stock: 665 + max_stock: 1000 + price: 297 + sale_gamma: 95 + service_level: 0.95 + SKU757: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 313 + init_stock: 78 + max_stock: 1000 + price: 438 + sale_gamma: 13 + service_level: 0.95 + SKU758: + constraint: null + cost: 319 + init_stock: 126 + max_stock: 1000 + price: 392 + sale_gamma: 21 + service_level: 0.95 + SKU759: + constraint: null + cost: 313 + init_stock: 287 + max_stock: 1000 + price: 394 + sale_gamma: 41 + service_level: 0.95 + SKU76: + constraint: G(stock_constraint) + cost: 242 + init_stock: 268 + max_stock: 1000 + price: 358 + sale_gamma: 67 + service_level: 0.95 + SKU760: + constraint: null + cost: 358 + init_stock: 104 + max_stock: 1000 + price: 544 + sale_gamma: 26 + service_level: 0.95 + SKU761: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 112 + max_stock: 1000 + price: 866 + sale_gamma: 16 + service_level: 0.95 + SKU762: + constraint: null + cost: 412 + init_stock: 30 + max_stock: 1000 + price: 815 + sale_gamma: 5 + service_level: 0.95 + SKU763: + constraint: G(low_profit -> low_stock_constraint) + cost: 391 + init_stock: 152 + max_stock: 1000 + price: 645 + sale_gamma: 38 + service_level: 0.95 + SKU764: + constraint: null + cost: 127 + init_stock: 196 + max_stock: 1000 + price: 226 + sale_gamma: 49 + service_level: 0.95 + SKU765: + constraint: null + cost: 271 + init_stock: 200 + max_stock: 1000 + price: 433 + sale_gamma: 25 + service_level: 0.95 + SKU766: + constraint: null + cost: 230 + init_stock: 115 + max_stock: 1000 + price: 439 + sale_gamma: 23 + service_level: 0.95 + SKU767: + constraint: G(low_profit -> low_stock_constraint) + cost: 91 + init_stock: 400 + max_stock: 1000 + price: 152 + sale_gamma: 80 + service_level: 0.95 + SKU768: + constraint: G(stock_constraint) + cost: 410 + init_stock: 700 + max_stock: 1000 + price: 811 + sale_gamma: 100 + service_level: 0.95 + SKU769: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 324 + init_stock: 246 + max_stock: 1000 + price: 502 + sale_gamma: 41 + service_level: 0.95 + SKU77: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 321 + init_stock: 170 + max_stock: 1000 + price: 616 + sale_gamma: 34 + service_level: 0.95 + SKU770: + constraint: null + cost: 288 + init_stock: 336 + max_stock: 1000 + price: 498 + sale_gamma: 48 + service_level: 0.95 + SKU771: + constraint: G(low_profit -> low_stock_constraint) + cost: 53 + init_stock: 60 + max_stock: 1000 + price: 90 + sale_gamma: 10 + service_level: 0.95 + SKU772: + constraint: null + cost: 205 + init_stock: 165 + max_stock: 1000 + price: 383 + sale_gamma: 55 + service_level: 0.95 + SKU773: + constraint: null + cost: 69 + init_stock: 108 + max_stock: 1000 + price: 128 + sale_gamma: 27 + service_level: 0.95 + SKU774: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 500 + init_stock: 480 + max_stock: 1000 + price: 585 + sale_gamma: 96 + service_level: 0.95 + SKU775: + constraint: null + cost: 366 + init_stock: 301 + max_stock: 1000 + price: 600 + sale_gamma: 43 + service_level: 0.95 + SKU776: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 464 + init_stock: 40 + max_stock: 1000 + price: 807 + sale_gamma: 8 + service_level: 0.95 + SKU777: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 493 + init_stock: 324 + max_stock: 1000 + price: 709 + sale_gamma: 54 + service_level: 0.95 + SKU778: + constraint: null + cost: 386 + init_stock: 125 + max_stock: 1000 + price: 706 + sale_gamma: 25 + service_level: 0.95 + SKU779: + constraint: null + cost: 149 + init_stock: 310 + max_stock: 1000 + price: 251 + sale_gamma: 62 + service_level: 0.95 + SKU78: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 231 + init_stock: 180 + max_stock: 1000 + price: 277 + sale_gamma: 45 + service_level: 0.95 + SKU780: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 271 + init_stock: 210 + max_stock: 1000 + price: 341 + sale_gamma: 35 + service_level: 0.95 + SKU781: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 84 + init_stock: 174 + max_stock: 1000 + price: 119 + sale_gamma: 58 + service_level: 0.95 + SKU782: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 145 + init_stock: 350 + max_stock: 1000 + price: 227 + sale_gamma: 70 + service_level: 0.95 + SKU783: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 191 + init_stock: 165 + max_stock: 1000 + price: 351 + sale_gamma: 33 + service_level: 0.95 + SKU784: + constraint: G(low_profit -> low_stock_constraint) + cost: 34 + init_stock: 108 + max_stock: 1000 + price: 51 + sale_gamma: 18 + service_level: 0.95 + SKU785: + constraint: G(stock_constraint) + cost: 368 + init_stock: 140 + max_stock: 1000 + price: 644 + sale_gamma: 28 + service_level: 0.95 + SKU786: + constraint: G(stock_constraint) + cost: 55 + init_stock: 92 + max_stock: 1000 + price: 76 + sale_gamma: 23 + service_level: 0.95 + SKU787: + constraint: null + cost: 368 + init_stock: 36 + max_stock: 1000 + price: 415 + sale_gamma: 6 + service_level: 0.95 + SKU788: + constraint: null + cost: 257 + init_stock: 70 + max_stock: 1000 + price: 514 + sale_gamma: 10 + service_level: 0.95 + SKU789: + constraint: null + cost: 218 + init_stock: 340 + max_stock: 1000 + price: 401 + sale_gamma: 85 + service_level: 0.95 + SKU79: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 436 + init_stock: 511 + max_stock: 1000 + price: 776 + sale_gamma: 73 + service_level: 0.95 + SKU790: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 429 + init_stock: 212 + max_stock: 1000 + price: 557 + sale_gamma: 53 + service_level: 0.95 + SKU791: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 55 + init_stock: 365 + max_stock: 1000 + price: 98 + sale_gamma: 73 + service_level: 0.95 + SKU792: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 80 + init_stock: 560 + max_stock: 1000 + price: 138 + sale_gamma: 70 + service_level: 0.95 + SKU793: + constraint: G(low_profit -> low_stock_constraint) + cost: 227 + init_stock: 66 + max_stock: 1000 + price: 399 + sale_gamma: 11 + service_level: 0.95 + SKU794: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 361 + init_stock: 196 + max_stock: 1000 + price: 573 + sale_gamma: 28 + service_level: 0.95 + SKU795: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 200 + init_stock: 192 + max_stock: 1000 + price: 390 + sale_gamma: 48 + service_level: 0.95 + SKU796: + constraint: null + cost: 360 + init_stock: 28 + max_stock: 1000 + price: 468 + sale_gamma: 7 + service_level: 0.95 + SKU797: + constraint: null + cost: 37 + init_stock: 174 + max_stock: 1000 + price: 56 + sale_gamma: 87 + service_level: 0.95 + SKU798: + constraint: null + cost: 124 + init_stock: 380 + max_stock: 1000 + price: 226 + sale_gamma: 95 + service_level: 0.95 + SKU799: + constraint: G(low_profit -> low_stock_constraint) + cost: 93 + init_stock: 108 + max_stock: 1000 + price: 144 + sale_gamma: 18 + service_level: 0.95 + SKU8: + constraint: null + cost: 320 + init_stock: 96 + max_stock: 1000 + price: 396 + sale_gamma: 24 + service_level: 0.95 + SKU80: + constraint: G(stock_constraint) + cost: 316 + init_stock: 126 + max_stock: 1000 + price: 376 + sale_gamma: 42 + service_level: 0.95 + SKU800: + constraint: G(stock_constraint) + cost: 264 + init_stock: 330 + max_stock: 1000 + price: 496 + sale_gamma: 66 + service_level: 0.95 + SKU801: + constraint: null + cost: 62 + init_stock: 400 + max_stock: 1000 + price: 107 + sale_gamma: 100 + service_level: 0.95 + SKU802: + constraint: null + cost: 289 + init_stock: 33 + max_stock: 1000 + price: 361 + sale_gamma: 11 + service_level: 0.95 + SKU803: + constraint: null + cost: 485 + init_stock: 244 + max_stock: 1000 + price: 669 + sale_gamma: 61 + service_level: 0.95 + SKU804: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 126 + init_stock: 406 + max_stock: 1000 + price: 143 + sale_gamma: 58 + service_level: 0.95 + SKU805: + constraint: null + cost: 227 + init_stock: 365 + max_stock: 1000 + price: 274 + sale_gamma: 73 + service_level: 0.95 + SKU806: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 59 + init_stock: 68 + max_stock: 1000 + price: 86 + sale_gamma: 34 + service_level: 0.95 + SKU807: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 468 + init_stock: 375 + max_stock: 1000 + price: 936 + sale_gamma: 75 + service_level: 0.95 + SKU808: + constraint: G(low_profit -> low_stock_constraint) + cost: 161 + init_stock: 432 + max_stock: 1000 + price: 301 + sale_gamma: 54 + service_level: 0.95 + SKU809: + constraint: null + cost: 53 + init_stock: 272 + max_stock: 1000 + price: 58 + sale_gamma: 68 + service_level: 0.95 + SKU81: + constraint: G(low_profit -> low_stock_constraint) + cost: 192 + init_stock: 196 + max_stock: 1000 + price: 243 + sale_gamma: 98 + service_level: 0.95 + SKU810: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 451 + init_stock: 264 + max_stock: 1000 + price: 559 + sale_gamma: 44 + service_level: 0.95 + SKU811: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 320 + init_stock: 64 + max_stock: 1000 + price: 419 + sale_gamma: 16 + service_level: 0.95 + SKU812: + constraint: null + cost: 293 + init_stock: 105 + max_stock: 1000 + price: 536 + sale_gamma: 15 + service_level: 0.95 + SKU813: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 39 + init_stock: 474 + max_stock: 1000 + price: 66 + sale_gamma: 79 + service_level: 0.95 + SKU814: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 485 + init_stock: 320 + max_stock: 1000 + price: 843 + sale_gamma: 64 + service_level: 0.95 + SKU815: + constraint: G(low_profit -> low_stock_constraint) + cost: 53 + init_stock: 144 + max_stock: 1000 + price: 102 + sale_gamma: 36 + service_level: 0.95 + SKU816: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 154 + init_stock: 112 + max_stock: 1000 + price: 231 + sale_gamma: 56 + service_level: 0.95 + SKU817: + constraint: G(stock_constraint) + cost: 262 + init_stock: 190 + max_stock: 1000 + price: 372 + sale_gamma: 95 + service_level: 0.95 + SKU818: + constraint: G(stock_constraint) + cost: 89 + init_stock: 456 + max_stock: 1000 + price: 164 + sale_gamma: 76 + service_level: 0.95 + SKU819: + constraint: G(low_profit -> low_stock_constraint) + cost: 328 + init_stock: 270 + max_stock: 1000 + price: 400 + sale_gamma: 90 + service_level: 0.95 + SKU82: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 237 + init_stock: 665 + max_stock: 1000 + price: 324 + sale_gamma: 95 + service_level: 0.95 + SKU820: + constraint: null + cost: 461 + init_stock: 210 + max_stock: 1000 + price: 645 + sale_gamma: 30 + service_level: 0.95 + SKU821: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 399 + init_stock: 166 + max_stock: 1000 + price: 570 + sale_gamma: 83 + service_level: 0.95 + SKU822: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 60 + init_stock: 360 + max_stock: 1000 + price: 85 + sale_gamma: 72 + service_level: 0.95 + SKU823: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 171 + init_stock: 405 + max_stock: 1000 + price: 198 + sale_gamma: 81 + service_level: 0.95 + SKU824: + constraint: G(stock_constraint) + cost: 54 + init_stock: 183 + max_stock: 1000 + price: 77 + sale_gamma: 61 + service_level: 0.95 + SKU825: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 361 + init_stock: 252 + max_stock: 1000 + price: 516 + sale_gamma: 42 + service_level: 0.95 + SKU826: + constraint: null + cost: 409 + init_stock: 104 + max_stock: 1000 + price: 548 + sale_gamma: 26 + service_level: 0.95 + SKU827: + constraint: null + cost: 495 + init_stock: 390 + max_stock: 1000 + price: 861 + sale_gamma: 78 + service_level: 0.95 + SKU828: + constraint: G(low_profit -> low_stock_constraint) + cost: 97 + init_stock: 270 + max_stock: 1000 + price: 130 + sale_gamma: 45 + service_level: 0.95 + SKU829: + constraint: null + cost: 301 + init_stock: 250 + max_stock: 1000 + price: 376 + sale_gamma: 50 + service_level: 0.95 + SKU83: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 365 + init_stock: 472 + max_stock: 1000 + price: 719 + sale_gamma: 59 + service_level: 0.95 + SKU830: + constraint: null + cost: 215 + init_stock: 52 + max_stock: 1000 + price: 408 + sale_gamma: 13 + service_level: 0.95 + SKU831: + constraint: null + cost: 324 + init_stock: 350 + max_stock: 1000 + price: 392 + sale_gamma: 50 + service_level: 0.95 + SKU832: + constraint: null + cost: 118 + init_stock: 558 + max_stock: 1000 + price: 200 + sale_gamma: 93 + service_level: 0.95 + SKU833: + constraint: null + cost: 435 + init_stock: 60 + max_stock: 1000 + price: 639 + sale_gamma: 30 + service_level: 0.95 + SKU834: + constraint: null + cost: 402 + init_stock: 380 + max_stock: 1000 + price: 623 + sale_gamma: 95 + service_level: 0.95 + SKU835: + constraint: G(low_profit -> low_stock_constraint) + cost: 216 + init_stock: 280 + max_stock: 1000 + price: 241 + sale_gamma: 56 + service_level: 0.95 + SKU836: + constraint: null + cost: 15 + init_stock: 444 + max_stock: 1000 + price: 19 + sale_gamma: 74 + service_level: 0.95 + SKU837: + constraint: null + cost: 62 + init_stock: 276 + max_stock: 1000 + price: 113 + sale_gamma: 69 + service_level: 0.95 + SKU838: + constraint: null + cost: 135 + init_stock: 144 + max_stock: 1000 + price: 183 + sale_gamma: 24 + service_level: 0.95 + SKU839: + constraint: null + cost: 485 + init_stock: 522 + max_stock: 1000 + price: 606 + sale_gamma: 87 + service_level: 0.95 + SKU84: + constraint: null + cost: 21 + init_stock: 492 + max_stock: 1000 + price: 32 + sale_gamma: 82 + service_level: 0.95 + SKU840: + constraint: G(stock_constraint) + cost: 185 + init_stock: 120 + max_stock: 1000 + price: 349 + sale_gamma: 30 + service_level: 0.95 + SKU841: + constraint: G(stock_constraint) + cost: 211 + init_stock: 588 + max_stock: 1000 + price: 276 + sale_gamma: 98 + service_level: 0.95 + SKU842: + constraint: null + cost: 251 + init_stock: 70 + max_stock: 1000 + price: 401 + sale_gamma: 10 + service_level: 0.95 + SKU843: + constraint: null + cost: 119 + init_stock: 183 + max_stock: 1000 + price: 217 + sale_gamma: 61 + service_level: 0.95 + SKU844: + constraint: null + cost: 119 + init_stock: 576 + max_stock: 1000 + price: 143 + sale_gamma: 96 + service_level: 0.95 + SKU845: + constraint: G(stock_constraint) + cost: 489 + init_stock: 375 + max_stock: 1000 + price: 572 + sale_gamma: 75 + service_level: 0.95 + SKU846: + constraint: null + cost: 500 + init_stock: 70 + max_stock: 1000 + price: 940 + sale_gamma: 10 + service_level: 0.95 + SKU847: + constraint: null + cost: 304 + init_stock: 736 + max_stock: 1000 + price: 361 + sale_gamma: 92 + service_level: 0.95 + SKU848: + constraint: null + cost: 242 + init_stock: 270 + max_stock: 1000 + price: 285 + sale_gamma: 90 + service_level: 0.95 + SKU849: + constraint: null + cost: 33 + init_stock: 576 + max_stock: 1000 + price: 61 + sale_gamma: 96 + service_level: 0.95 + SKU85: + constraint: G(stock_constraint) + cost: 156 + init_stock: 124 + max_stock: 1000 + price: 307 + sale_gamma: 62 + service_level: 0.95 + SKU850: + constraint: null + cost: 449 + init_stock: 306 + max_stock: 1000 + price: 642 + sale_gamma: 51 + service_level: 0.95 + SKU851: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 417 + init_stock: 92 + max_stock: 1000 + price: 533 + sale_gamma: 46 + service_level: 0.95 + SKU852: + constraint: G(low_profit -> low_stock_constraint) + cost: 274 + init_stock: 260 + max_stock: 1000 + price: 358 + sale_gamma: 52 + service_level: 0.95 + SKU853: + constraint: null + cost: 65 + init_stock: 270 + max_stock: 1000 + price: 120 + sale_gamma: 54 + service_level: 0.95 + SKU854: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 224 + init_stock: 552 + max_stock: 1000 + price: 396 + sale_gamma: 92 + service_level: 0.95 + SKU855: + constraint: G(stock_constraint) + cost: 125 + init_stock: 21 + max_stock: 1000 + price: 158 + sale_gamma: 7 + service_level: 0.95 + SKU856: + constraint: null + cost: 192 + init_stock: 168 + max_stock: 1000 + price: 280 + sale_gamma: 42 + service_level: 0.95 + SKU857: + constraint: G(stock_constraint) + cost: 180 + init_stock: 108 + max_stock: 1000 + price: 289 + sale_gamma: 18 + service_level: 0.95 + SKU858: + constraint: G(low_profit -> low_stock_constraint) + cost: 393 + init_stock: 553 + max_stock: 1000 + price: 581 + sale_gamma: 79 + service_level: 0.95 + SKU859: + constraint: null + cost: 259 + init_stock: 434 + max_stock: 1000 + price: 440 + sale_gamma: 62 + service_level: 0.95 + SKU86: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 322 + init_stock: 18 + max_stock: 1000 + price: 598 + sale_gamma: 6 + service_level: 0.95 + SKU860: + constraint: null + cost: 287 + init_stock: 90 + max_stock: 1000 + price: 556 + sale_gamma: 30 + service_level: 0.95 + SKU861: + constraint: null + cost: 20 + init_stock: 56 + max_stock: 1000 + price: 38 + sale_gamma: 28 + service_level: 0.95 + SKU862: + constraint: null + cost: 86 + init_stock: 92 + max_stock: 1000 + price: 102 + sale_gamma: 23 + service_level: 0.95 + SKU863: + constraint: null + cost: 84 + init_stock: 553 + max_stock: 1000 + price: 145 + sale_gamma: 79 + service_level: 0.95 + SKU864: + constraint: null + cost: 287 + init_stock: 258 + max_stock: 1000 + price: 464 + sale_gamma: 43 + service_level: 0.95 + SKU865: + constraint: G(low_profit -> low_stock_constraint) + cost: 197 + init_stock: 96 + max_stock: 1000 + price: 344 + sale_gamma: 24 + service_level: 0.95 + SKU866: + constraint: G(stock_constraint) + cost: 393 + init_stock: 25 + max_stock: 1000 + price: 746 + sale_gamma: 5 + service_level: 0.95 + SKU867: + constraint: G(stock_constraint) + cost: 231 + init_stock: 175 + max_stock: 1000 + price: 374 + sale_gamma: 35 + service_level: 0.95 + SKU868: + constraint: null + cost: 467 + init_stock: 396 + max_stock: 1000 + price: 826 + sale_gamma: 66 + service_level: 0.95 + SKU869: + constraint: null + cost: 114 + init_stock: 368 + max_stock: 1000 + price: 204 + sale_gamma: 92 + service_level: 0.95 + SKU87: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 326 + init_stock: 45 + max_stock: 1000 + price: 361 + sale_gamma: 9 + service_level: 0.95 + SKU870: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 44 + init_stock: 66 + max_stock: 1000 + price: 69 + sale_gamma: 11 + service_level: 0.95 + SKU871: + constraint: null + cost: 404 + init_stock: 252 + max_stock: 1000 + price: 767 + sale_gamma: 84 + service_level: 0.95 + SKU872: + constraint: null + cost: 291 + init_stock: 270 + max_stock: 1000 + price: 369 + sale_gamma: 54 + service_level: 0.95 + SKU873: + constraint: G(stock_constraint) + cost: 334 + init_stock: 84 + max_stock: 1000 + price: 407 + sale_gamma: 21 + service_level: 0.95 + SKU874: + constraint: null + cost: 364 + init_stock: 336 + max_stock: 1000 + price: 469 + sale_gamma: 48 + service_level: 0.95 + SKU875: + constraint: G(low_profit -> low_stock_constraint) + cost: 276 + init_stock: 609 + max_stock: 1000 + price: 353 + sale_gamma: 87 + service_level: 0.95 + SKU876: + constraint: null + cost: 342 + init_stock: 420 + max_stock: 1000 + price: 608 + sale_gamma: 70 + service_level: 0.95 + SKU877: + constraint: null + cost: 398 + init_stock: 486 + max_stock: 1000 + price: 509 + sale_gamma: 81 + service_level: 0.95 + SKU878: + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 272 + max_stock: 1000 + price: 148 + sale_gamma: 68 + service_level: 0.95 + SKU879: + constraint: null + cost: 343 + init_stock: 48 + max_stock: 1000 + price: 511 + sale_gamma: 8 + service_level: 0.95 + SKU88: + constraint: G(low_profit -> low_stock_constraint) + cost: 244 + init_stock: 176 + max_stock: 1000 + price: 436 + sale_gamma: 22 + service_level: 0.95 + SKU880: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 496 + init_stock: 132 + max_stock: 1000 + price: 922 + sale_gamma: 44 + service_level: 0.95 + SKU881: + constraint: null + cost: 367 + init_stock: 536 + max_stock: 1000 + price: 598 + sale_gamma: 67 + service_level: 0.95 + SKU882: + constraint: null + cost: 86 + init_stock: 189 + max_stock: 1000 + price: 141 + sale_gamma: 63 + service_level: 0.95 + SKU883: + constraint: G(stock_constraint) + cost: 190 + init_stock: 230 + max_stock: 1000 + price: 302 + sale_gamma: 46 + service_level: 0.95 + SKU884: + constraint: null + cost: 200 + init_stock: 116 + max_stock: 1000 + price: 366 + sale_gamma: 29 + service_level: 0.95 + SKU885: + constraint: null + cost: 133 + init_stock: 184 + max_stock: 1000 + price: 251 + sale_gamma: 46 + service_level: 0.95 + SKU886: + constraint: null + cost: 218 + init_stock: 96 + max_stock: 1000 + price: 370 + sale_gamma: 16 + service_level: 0.95 + SKU887: + constraint: G(low_profit -> low_stock_constraint) + cost: 215 + init_stock: 352 + max_stock: 1000 + price: 331 + sale_gamma: 88 + service_level: 0.95 + SKU888: + constraint: null + cost: 439 + init_stock: 201 + max_stock: 1000 + price: 566 + sale_gamma: 67 + service_level: 0.95 + SKU889: + constraint: null + cost: 105 + init_stock: 476 + max_stock: 1000 + price: 157 + sale_gamma: 68 + service_level: 0.95 + SKU89: + constraint: G(low_profit -> low_stock_constraint) + cost: 223 + init_stock: 21 + max_stock: 1000 + price: 352 + sale_gamma: 7 + service_level: 0.95 + SKU890: + constraint: null + cost: 183 + init_stock: 216 + max_stock: 1000 + price: 201 + sale_gamma: 36 + service_level: 0.95 + SKU891: + constraint: null + cost: 85 + init_stock: 132 + max_stock: 1000 + price: 110 + sale_gamma: 44 + service_level: 0.95 + SKU892: + constraint: null + cost: 41 + init_stock: 252 + max_stock: 1000 + price: 76 + sale_gamma: 84 + service_level: 0.95 + SKU893: + constraint: null + cost: 135 + init_stock: 154 + max_stock: 1000 + price: 238 + sale_gamma: 22 + service_level: 0.95 + SKU894: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 405 + init_stock: 420 + max_stock: 1000 + price: 579 + sale_gamma: 70 + service_level: 0.95 + SKU895: + constraint: null + cost: 320 + init_stock: 186 + max_stock: 1000 + price: 480 + sale_gamma: 62 + service_level: 0.95 + SKU896: + constraint: G(stock_constraint) + cost: 451 + init_stock: 162 + max_stock: 1000 + price: 726 + sale_gamma: 54 + service_level: 0.95 + SKU897: + constraint: G(stock_constraint) + cost: 141 + init_stock: 480 + max_stock: 1000 + price: 211 + sale_gamma: 96 + service_level: 0.95 + SKU898: + constraint: null + cost: 220 + init_stock: 712 + max_stock: 1000 + price: 426 + sale_gamma: 89 + service_level: 0.95 + SKU899: + constraint: G(low_profit -> low_stock_constraint) + cost: 113 + init_stock: 228 + max_stock: 1000 + price: 166 + sale_gamma: 38 + service_level: 0.95 + SKU9: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 483 + init_stock: 168 + max_stock: 1000 + price: 811 + sale_gamma: 42 + service_level: 0.95 + SKU90: + constraint: G(low_profit -> low_stock_constraint) + cost: 217 + init_stock: 66 + max_stock: 1000 + price: 371 + sale_gamma: 11 + service_level: 0.95 + SKU900: + constraint: null + cost: 218 + init_stock: 280 + max_stock: 1000 + price: 427 + sale_gamma: 70 + service_level: 0.95 + SKU901: + constraint: G(low_profit -> low_stock_constraint) + cost: 335 + init_stock: 332 + max_stock: 1000 + price: 592 + sale_gamma: 83 + service_level: 0.95 + SKU902: + constraint: null + cost: 118 + init_stock: 495 + max_stock: 1000 + price: 193 + sale_gamma: 99 + service_level: 0.95 + SKU903: + constraint: G(stock_constraint) + cost: 164 + init_stock: 77 + max_stock: 1000 + price: 296 + sale_gamma: 11 + service_level: 0.95 + SKU904: + constraint: null + cost: 317 + init_stock: 180 + max_stock: 1000 + price: 595 + sale_gamma: 30 + service_level: 0.95 + SKU905: + constraint: null + cost: 462 + init_stock: 155 + max_stock: 1000 + price: 831 + sale_gamma: 31 + service_level: 0.95 + SKU906: + constraint: null + cost: 249 + init_stock: 180 + max_stock: 1000 + price: 313 + sale_gamma: 45 + service_level: 0.95 + SKU907: + constraint: G(low_profit -> low_stock_constraint) + cost: 206 + init_stock: 532 + max_stock: 1000 + price: 230 + sale_gamma: 76 + service_level: 0.95 + SKU908: + constraint: null + cost: 419 + init_stock: 315 + max_stock: 1000 + price: 599 + sale_gamma: 63 + service_level: 0.95 + SKU909: + constraint: G(low_profit -> low_stock_constraint) + cost: 414 + init_stock: 128 + max_stock: 1000 + price: 811 + sale_gamma: 64 + service_level: 0.95 + SKU91: + constraint: null + cost: 254 + init_stock: 630 + max_stock: 1000 + price: 355 + sale_gamma: 90 + service_level: 0.95 + SKU910: + constraint: G(stock_constraint) + cost: 371 + init_stock: 312 + max_stock: 1000 + price: 552 + sale_gamma: 52 + service_level: 0.95 + SKU911: + constraint: null + cost: 421 + init_stock: 420 + max_stock: 1000 + price: 509 + sale_gamma: 60 + service_level: 0.95 + SKU912: + constraint: null + cost: 164 + init_stock: 105 + max_stock: 1000 + price: 295 + sale_gamma: 15 + service_level: 0.95 + SKU913: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 20 + init_stock: 385 + max_stock: 1000 + price: 36 + sale_gamma: 77 + service_level: 0.95 + SKU914: + constraint: null + cost: 144 + init_stock: 138 + max_stock: 1000 + price: 211 + sale_gamma: 46 + service_level: 0.95 + SKU915: + constraint: null + cost: 214 + init_stock: 40 + max_stock: 1000 + price: 250 + sale_gamma: 10 + service_level: 0.95 + SKU916: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 117 + init_stock: 336 + max_stock: 1000 + price: 162 + sale_gamma: 48 + service_level: 0.95 + SKU917: + constraint: null + cost: 212 + init_stock: 130 + max_stock: 1000 + price: 383 + sale_gamma: 26 + service_level: 0.95 + SKU918: + constraint: null + cost: 318 + init_stock: 392 + max_stock: 1000 + price: 632 + sale_gamma: 49 + service_level: 0.95 + SKU919: + constraint: null + cost: 59 + init_stock: 55 + max_stock: 1000 + price: 69 + sale_gamma: 11 + service_level: 0.95 + SKU92: + constraint: null + cost: 341 + init_stock: 244 + max_stock: 1000 + price: 671 + sale_gamma: 61 + service_level: 0.95 + SKU920: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 161 + init_stock: 120 + max_stock: 1000 + price: 318 + sale_gamma: 24 + service_level: 0.95 + SKU921: + constraint: G(low_profit -> low_stock_constraint) + cost: 489 + init_stock: 176 + max_stock: 1000 + price: 591 + sale_gamma: 88 + service_level: 0.95 + SKU922: + constraint: G(low_profit -> low_stock_constraint) + cost: 35 + init_stock: 364 + max_stock: 1000 + price: 53 + sale_gamma: 91 + service_level: 0.95 + SKU923: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 182 + init_stock: 40 + max_stock: 1000 + price: 364 + sale_gamma: 8 + service_level: 0.95 + SKU924: + constraint: null + cost: 364 + init_stock: 184 + max_stock: 1000 + price: 447 + sale_gamma: 46 + service_level: 0.95 + SKU925: + constraint: null + cost: 31 + init_stock: 475 + max_stock: 1000 + price: 37 + sale_gamma: 95 + service_level: 0.95 + SKU926: + constraint: null + cost: 472 + init_stock: 60 + max_stock: 1000 + price: 623 + sale_gamma: 10 + service_level: 0.95 + SKU927: + constraint: null + cost: 143 + init_stock: 40 + max_stock: 1000 + price: 258 + sale_gamma: 5 + service_level: 0.95 + SKU928: + constraint: null + cost: 325 + init_stock: 185 + max_stock: 1000 + price: 454 + sale_gamma: 37 + service_level: 0.95 + SKU929: + constraint: G(stock_constraint) + cost: 296 + init_stock: 304 + max_stock: 1000 + price: 512 + sale_gamma: 76 + service_level: 0.95 + SKU93: + constraint: null + cost: 361 + init_stock: 504 + max_stock: 1000 + price: 555 + sale_gamma: 84 + service_level: 0.95 + SKU930: + constraint: G(stock_constraint) + cost: 257 + init_stock: 425 + max_stock: 1000 + price: 449 + sale_gamma: 85 + service_level: 0.95 + SKU931: + constraint: null + cost: 156 + init_stock: 132 + max_stock: 1000 + price: 196 + sale_gamma: 33 + service_level: 0.95 + SKU932: + constraint: G(low_profit -> low_stock_constraint) + cost: 138 + init_stock: 396 + max_stock: 1000 + price: 160 + sale_gamma: 99 + service_level: 0.95 + SKU933: + constraint: G(low_profit -> low_stock_constraint) + cost: 284 + init_stock: 228 + max_stock: 1000 + price: 460 + sale_gamma: 38 + service_level: 0.95 + SKU934: + constraint: null + cost: 213 + init_stock: 86 + max_stock: 1000 + price: 255 + sale_gamma: 43 + service_level: 0.95 + SKU935: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 339 + init_stock: 54 + max_stock: 1000 + price: 396 + sale_gamma: 27 + service_level: 0.95 + SKU936: + constraint: null + cost: 170 + init_stock: 36 + max_stock: 1000 + price: 261 + sale_gamma: 9 + service_level: 0.95 + SKU937: + constraint: null + cost: 123 + init_stock: 90 + max_stock: 1000 + price: 202 + sale_gamma: 15 + service_level: 0.95 + SKU938: + constraint: null + cost: 299 + init_stock: 364 + max_stock: 1000 + price: 442 + sale_gamma: 91 + service_level: 0.95 + SKU939: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 267 + init_stock: 100 + max_stock: 1000 + price: 469 + sale_gamma: 25 + service_level: 0.95 + SKU94: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 446 + init_stock: 245 + max_stock: 1000 + price: 789 + sale_gamma: 49 + service_level: 0.95 + SKU940: + constraint: G(stock_constraint) + cost: 475 + init_stock: 220 + max_stock: 1000 + price: 783 + sale_gamma: 55 + service_level: 0.95 + SKU941: + constraint: null + cost: 202 + init_stock: 192 + max_stock: 1000 + price: 363 + sale_gamma: 24 + service_level: 0.95 + SKU942: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 408 + init_stock: 216 + max_stock: 1000 + price: 505 + sale_gamma: 72 + service_level: 0.95 + SKU943: + constraint: null + cost: 189 + init_stock: 64 + max_stock: 1000 + price: 238 + sale_gamma: 8 + service_level: 0.95 + SKU944: + constraint: null + cost: 494 + init_stock: 170 + max_stock: 1000 + price: 602 + sale_gamma: 34 + service_level: 0.95 + SKU945: + constraint: null + cost: 237 + init_stock: 51 + max_stock: 1000 + price: 455 + sale_gamma: 17 + service_level: 0.95 + SKU946: + constraint: null + cost: 345 + init_stock: 294 + max_stock: 1000 + price: 610 + sale_gamma: 98 + service_level: 0.95 + SKU947: + constraint: G(stock_constraint) + cost: 495 + init_stock: 324 + max_stock: 1000 + price: 975 + sale_gamma: 54 + service_level: 0.95 + SKU948: + constraint: null + cost: 314 + init_stock: 546 + max_stock: 1000 + price: 433 + sale_gamma: 78 + service_level: 0.95 + SKU949: + constraint: G(stock_constraint) + cost: 453 + init_stock: 38 + max_stock: 1000 + price: 602 + sale_gamma: 19 + service_level: 0.95 + SKU95: + constraint: null + cost: 25 + init_stock: 679 + max_stock: 1000 + price: 34 + sale_gamma: 97 + service_level: 0.95 + SKU950: + constraint: null + cost: 286 + init_stock: 345 + max_stock: 1000 + price: 551 + sale_gamma: 69 + service_level: 0.95 + SKU951: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 167 + init_stock: 280 + max_stock: 1000 + price: 225 + sale_gamma: 70 + service_level: 0.95 + SKU952: + constraint: null + cost: 493 + init_stock: 205 + max_stock: 1000 + price: 818 + sale_gamma: 41 + service_level: 0.95 + SKU953: + constraint: null + cost: 472 + init_stock: 316 + max_stock: 1000 + price: 920 + sale_gamma: 79 + service_level: 0.95 + SKU954: + constraint: null + cost: 287 + init_stock: 70 + max_stock: 1000 + price: 401 + sale_gamma: 14 + service_level: 0.95 + SKU955: + constraint: null + cost: 94 + init_stock: 90 + max_stock: 1000 + price: 139 + sale_gamma: 15 + service_level: 0.95 + SKU956: + constraint: null + cost: 157 + init_stock: 74 + max_stock: 1000 + price: 229 + sale_gamma: 37 + service_level: 0.95 + SKU957: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 304 + init_stock: 441 + max_stock: 1000 + price: 577 + sale_gamma: 63 + service_level: 0.95 + SKU958: + constraint: G(stock_constraint) + cost: 348 + init_stock: 275 + max_stock: 1000 + price: 619 + sale_gamma: 55 + service_level: 0.95 + SKU959: + constraint: G(low_profit -> low_stock_constraint) + cost: 442 + init_stock: 474 + max_stock: 1000 + price: 667 + sale_gamma: 79 + service_level: 0.95 + SKU96: + constraint: null + cost: 473 + init_stock: 98 + max_stock: 1000 + price: 884 + sale_gamma: 49 + service_level: 0.95 + SKU960: + constraint: G(low_profit -> low_stock_constraint) + cost: 27 + init_stock: 456 + max_stock: 1000 + price: 44 + sale_gamma: 76 + service_level: 0.95 + SKU961: + constraint: null + cost: 282 + init_stock: 384 + max_stock: 1000 + price: 349 + sale_gamma: 64 + service_level: 0.95 + SKU962: + constraint: G(low_profit -> low_stock_constraint) + cost: 391 + init_stock: 44 + max_stock: 1000 + price: 590 + sale_gamma: 11 + service_level: 0.95 + SKU963: + constraint: G(low_profit -> low_stock_constraint) + cost: 247 + init_stock: 54 + max_stock: 1000 + price: 444 + sale_gamma: 18 + service_level: 0.95 + SKU964: + constraint: G(low_profit -> low_stock_constraint) + cost: 495 + init_stock: 72 + max_stock: 1000 + price: 836 + sale_gamma: 12 + service_level: 0.95 + SKU965: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 428 + init_stock: 296 + max_stock: 1000 + price: 577 + sale_gamma: 74 + service_level: 0.95 + SKU966: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 360 + init_stock: 82 + max_stock: 1000 + price: 482 + sale_gamma: 41 + service_level: 0.95 + SKU967: + constraint: null + cost: 407 + init_stock: 162 + max_stock: 1000 + price: 712 + sale_gamma: 81 + service_level: 0.95 + SKU968: + constraint: G(low_profit -> low_stock_constraint) + cost: 69 + init_stock: 378 + max_stock: 1000 + price: 122 + sale_gamma: 63 + service_level: 0.95 + SKU969: + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 256 + max_stock: 1000 + price: 226 + sale_gamma: 32 + service_level: 0.95 + SKU97: + constraint: null + cost: 432 + init_stock: 245 + max_stock: 1000 + price: 691 + sale_gamma: 49 + service_level: 0.95 + SKU970: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 376 + init_stock: 288 + max_stock: 1000 + price: 503 + sale_gamma: 96 + service_level: 0.95 + SKU971: + constraint: null + cost: 44 + init_stock: 84 + max_stock: 1000 + price: 63 + sale_gamma: 14 + service_level: 0.95 + SKU972: + constraint: G(stock_constraint) + cost: 78 + init_stock: 56 + max_stock: 1000 + price: 117 + sale_gamma: 8 + service_level: 0.95 + SKU973: + constraint: null + cost: 407 + init_stock: 168 + max_stock: 1000 + price: 525 + sale_gamma: 42 + service_level: 0.95 + SKU974: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 434 + init_stock: 162 + max_stock: 1000 + price: 551 + sale_gamma: 27 + service_level: 0.95 + SKU975: + constraint: G(stock_constraint) + cost: 98 + init_stock: 356 + max_stock: 1000 + price: 135 + sale_gamma: 89 + service_level: 0.95 + SKU976: + constraint: null + cost: 263 + init_stock: 183 + max_stock: 1000 + price: 481 + sale_gamma: 61 + service_level: 0.95 + SKU977: + constraint: null + cost: 83 + init_stock: 184 + max_stock: 1000 + price: 107 + sale_gamma: 23 + service_level: 0.95 + SKU978: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 90 + init_stock: 616 + max_stock: 1000 + price: 99 + sale_gamma: 77 + service_level: 0.95 + SKU979: + constraint: G(low_profit -> low_stock_constraint) + cost: 207 + init_stock: 52 + max_stock: 1000 + price: 289 + sale_gamma: 13 + service_level: 0.95 + SKU98: + constraint: G(stock_constraint) + cost: 43 + init_stock: 249 + max_stock: 1000 + price: 49 + sale_gamma: 83 + service_level: 0.95 + SKU980: + constraint: null + cost: 227 + init_stock: 180 + max_stock: 1000 + price: 404 + sale_gamma: 60 + service_level: 0.95 + SKU981: + constraint: G(low_profit -> low_stock_constraint) + cost: 278 + init_stock: 68 + max_stock: 1000 + price: 436 + sale_gamma: 17 + service_level: 0.95 + SKU982: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 125 + init_stock: 40 + max_stock: 1000 + price: 193 + sale_gamma: 10 + service_level: 0.95 + SKU983: + constraint: G(stock_constraint) + cost: 431 + init_stock: 544 + max_stock: 1000 + price: 504 + sale_gamma: 68 + service_level: 0.95 + SKU984: + constraint: null + cost: 59 + init_stock: 140 + max_stock: 1000 + price: 92 + sale_gamma: 28 + service_level: 0.95 + SKU985: + constraint: null + cost: 191 + init_stock: 126 + max_stock: 1000 + price: 334 + sale_gamma: 63 + service_level: 0.95 + SKU986: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 236 + init_stock: 266 + max_stock: 1000 + price: 363 + sale_gamma: 38 + service_level: 0.95 + SKU987: + constraint: null + cost: 333 + init_stock: 792 + max_stock: 1000 + price: 496 + sale_gamma: 99 + service_level: 0.95 + SKU988: + constraint: null + cost: 407 + init_stock: 231 + max_stock: 1000 + price: 577 + sale_gamma: 77 + service_level: 0.95 + SKU989: + constraint: null + cost: 53 + init_stock: 574 + max_stock: 1000 + price: 93 + sale_gamma: 82 + service_level: 0.95 + SKU99: + constraint: null + cost: 65 + init_stock: 158 + max_stock: 1000 + price: 125 + sale_gamma: 79 + service_level: 0.95 + SKU990: + constraint: G(stock_constraint) + cost: 368 + init_stock: 376 + max_stock: 1000 + price: 566 + sale_gamma: 94 + service_level: 0.95 + SKU991: + constraint: null + cost: 348 + init_stock: 644 + max_stock: 1000 + price: 438 + sale_gamma: 92 + service_level: 0.95 + SKU992: + constraint: null + cost: 59 + init_stock: 408 + max_stock: 1000 + price: 117 + sale_gamma: 68 + service_level: 0.95 + SKU993: + constraint: G(low_profit -> low_stock_constraint) + cost: 498 + init_stock: 161 + max_stock: 1000 + price: 756 + sale_gamma: 23 + service_level: 0.95 + SKU994: + constraint: null + cost: 438 + init_stock: 136 + max_stock: 1000 + price: 823 + sale_gamma: 34 + service_level: 0.95 + SKU995: + constraint: G(stock_constraint) + cost: 292 + init_stock: 294 + max_stock: 1000 + price: 543 + sale_gamma: 49 + service_level: 0.95 + SKU996: + constraint: null + cost: 73 + init_stock: 245 + max_stock: 1000 + price: 102 + sale_gamma: 49 + service_level: 0.95 + SKU997: + constraint: G(stock_constraint) + cost: 321 + init_stock: 355 + max_stock: 1000 + price: 503 + sale_gamma: 71 + service_level: 0.95 + SKU998: + constraint: null + cost: 371 + init_stock: 90 + max_stock: 1000 + price: 708 + sale_gamma: 15 + service_level: 0.95 + SKU999: + constraint: null + cost: 208 + init_stock: 712 + max_stock: 1000 + price: 403 + sale_gamma: 89 + service_level: 0.95 + grid: + facilities: + STORE0: + - 13 + - 0 + SUPPLIER0: + - 7 + - 18 + WAREHOUSE0: + - 16 + - 14 + size: + - 20 + - 20 + skus: + - id: 0 + name: SKU0 + - id: 1 + name: SKU1 + - id: 2 + name: SKU2 + - id: 3 + name: SKU3 + - id: 4 + name: SKU4 + - id: 5 + name: SKU5 + - id: 6 + name: SKU6 + - id: 7 + name: SKU7 + - id: 8 + name: SKU8 + - id: 9 + name: SKU9 + - id: 10 + name: SKU10 + - id: 11 + name: SKU11 + - id: 12 + name: SKU12 + - id: 13 + name: SKU13 + - id: 14 + name: SKU14 + - id: 15 + name: SKU15 + - id: 16 + name: SKU16 + - id: 17 + name: SKU17 + - id: 18 + name: SKU18 + - id: 19 + name: SKU19 + - id: 20 + name: SKU20 + - id: 21 + name: SKU21 + - id: 22 + name: SKU22 + - id: 23 + name: SKU23 + - id: 24 + name: SKU24 + - id: 25 + name: SKU25 + - id: 26 + name: SKU26 + - id: 27 + name: SKU27 + - id: 28 + name: SKU28 + - id: 29 + name: SKU29 + - id: 30 + name: SKU30 + - id: 31 + name: SKU31 + - id: 32 + name: SKU32 + - id: 33 + name: SKU33 + - id: 34 + name: SKU34 + - id: 35 + name: SKU35 + - id: 36 + name: SKU36 + - id: 37 + name: SKU37 + - id: 38 + name: SKU38 + - id: 39 + name: SKU39 + - id: 40 + name: SKU40 + - id: 41 + name: SKU41 + - id: 42 + name: SKU42 + - id: 43 + name: SKU43 + - id: 44 + name: SKU44 + - id: 45 + name: SKU45 + - id: 46 + name: SKU46 + - id: 47 + name: SKU47 + - id: 48 + name: SKU48 + - id: 49 + name: SKU49 + - id: 50 + name: SKU50 + - id: 51 + name: SKU51 + - id: 52 + name: SKU52 + - id: 53 + name: SKU53 + - id: 54 + name: SKU54 + - id: 55 + name: SKU55 + - id: 56 + name: SKU56 + - id: 57 + name: SKU57 + - id: 58 + name: SKU58 + - id: 59 + name: SKU59 + - id: 60 + name: SKU60 + - id: 61 + name: SKU61 + - id: 62 + name: SKU62 + - id: 63 + name: SKU63 + - id: 64 + name: SKU64 + - id: 65 + name: SKU65 + - id: 66 + name: SKU66 + - id: 67 + name: SKU67 + - id: 68 + name: SKU68 + - id: 69 + name: SKU69 + - id: 70 + name: SKU70 + - id: 71 + name: SKU71 + - id: 72 + name: SKU72 + - id: 73 + name: SKU73 + - id: 74 + name: SKU74 + - id: 75 + name: SKU75 + - id: 76 + name: SKU76 + - id: 77 + name: SKU77 + - id: 78 + name: SKU78 + - id: 79 + name: SKU79 + - id: 80 + name: SKU80 + - id: 81 + name: SKU81 + - id: 82 + name: SKU82 + - id: 83 + name: SKU83 + - id: 84 + name: SKU84 + - id: 85 + name: SKU85 + - id: 86 + name: SKU86 + - id: 87 + name: SKU87 + - id: 88 + name: SKU88 + - id: 89 + name: SKU89 + - id: 90 + name: SKU90 + - id: 91 + name: SKU91 + - id: 92 + name: SKU92 + - id: 93 + name: SKU93 + - id: 94 + name: SKU94 + - id: 95 + name: SKU95 + - id: 96 + name: SKU96 + - id: 97 + name: SKU97 + - id: 98 + name: SKU98 + - id: 99 + name: SKU99 + - id: 100 + name: SKU100 + - id: 101 + name: SKU101 + - id: 102 + name: SKU102 + - id: 103 + name: SKU103 + - id: 104 + name: SKU104 + - id: 105 + name: SKU105 + - id: 106 + name: SKU106 + - id: 107 + name: SKU107 + - id: 108 + name: SKU108 + - id: 109 + name: SKU109 + - id: 110 + name: SKU110 + - id: 111 + name: SKU111 + - id: 112 + name: SKU112 + - id: 113 + name: SKU113 + - id: 114 + name: SKU114 + - id: 115 + name: SKU115 + - id: 116 + name: SKU116 + - id: 117 + name: SKU117 + - id: 118 + name: SKU118 + - id: 119 + name: SKU119 + - id: 120 + name: SKU120 + - id: 121 + name: SKU121 + - id: 122 + name: SKU122 + - id: 123 + name: SKU123 + - id: 124 + name: SKU124 + - id: 125 + name: SKU125 + - id: 126 + name: SKU126 + - id: 127 + name: SKU127 + - id: 128 + name: SKU128 + - id: 129 + name: SKU129 + - id: 130 + name: SKU130 + - id: 131 + name: SKU131 + - id: 132 + name: SKU132 + - id: 133 + name: SKU133 + - id: 134 + name: SKU134 + - id: 135 + name: SKU135 + - id: 136 + name: SKU136 + - id: 137 + name: SKU137 + - id: 138 + name: SKU138 + - id: 139 + name: SKU139 + - id: 140 + name: SKU140 + - id: 141 + name: SKU141 + - id: 142 + name: SKU142 + - id: 143 + name: SKU143 + - id: 144 + name: SKU144 + - id: 145 + name: SKU145 + - id: 146 + name: SKU146 + - id: 147 + name: SKU147 + - id: 148 + name: SKU148 + - id: 149 + name: SKU149 + - id: 150 + name: SKU150 + - id: 151 + name: SKU151 + - id: 152 + name: SKU152 + - id: 153 + name: SKU153 + - id: 154 + name: SKU154 + - id: 155 + name: SKU155 + - id: 156 + name: SKU156 + - id: 157 + name: SKU157 + - id: 158 + name: SKU158 + - id: 159 + name: SKU159 + - id: 160 + name: SKU160 + - id: 161 + name: SKU161 + - id: 162 + name: SKU162 + - id: 163 + name: SKU163 + - id: 164 + name: SKU164 + - id: 165 + name: SKU165 + - id: 166 + name: SKU166 + - id: 167 + name: SKU167 + - id: 168 + name: SKU168 + - id: 169 + name: SKU169 + - id: 170 + name: SKU170 + - id: 171 + name: SKU171 + - id: 172 + name: SKU172 + - id: 173 + name: SKU173 + - id: 174 + name: SKU174 + - id: 175 + name: SKU175 + - id: 176 + name: SKU176 + - id: 177 + name: SKU177 + - id: 178 + name: SKU178 + - id: 179 + name: SKU179 + - id: 180 + name: SKU180 + - id: 181 + name: SKU181 + - id: 182 + name: SKU182 + - id: 183 + name: SKU183 + - id: 184 + name: SKU184 + - id: 185 + name: SKU185 + - id: 186 + name: SKU186 + - id: 187 + name: SKU187 + - id: 188 + name: SKU188 + - id: 189 + name: SKU189 + - id: 190 + name: SKU190 + - id: 191 + name: SKU191 + - id: 192 + name: SKU192 + - id: 193 + name: SKU193 + - id: 194 + name: SKU194 + - id: 195 + name: SKU195 + - id: 196 + name: SKU196 + - id: 197 + name: SKU197 + - id: 198 + name: SKU198 + - id: 199 + name: SKU199 + - id: 200 + name: SKU200 + - id: 201 + name: SKU201 + - id: 202 + name: SKU202 + - id: 203 + name: SKU203 + - id: 204 + name: SKU204 + - id: 205 + name: SKU205 + - id: 206 + name: SKU206 + - id: 207 + name: SKU207 + - id: 208 + name: SKU208 + - id: 209 + name: SKU209 + - id: 210 + name: SKU210 + - id: 211 + name: SKU211 + - id: 212 + name: SKU212 + - id: 213 + name: SKU213 + - id: 214 + name: SKU214 + - id: 215 + name: SKU215 + - id: 216 + name: SKU216 + - id: 217 + name: SKU217 + - id: 218 + name: SKU218 + - id: 219 + name: SKU219 + - id: 220 + name: SKU220 + - id: 221 + name: SKU221 + - id: 222 + name: SKU222 + - id: 223 + name: SKU223 + - id: 224 + name: SKU224 + - id: 225 + name: SKU225 + - id: 226 + name: SKU226 + - id: 227 + name: SKU227 + - id: 228 + name: SKU228 + - id: 229 + name: SKU229 + - id: 230 + name: SKU230 + - id: 231 + name: SKU231 + - id: 232 + name: SKU232 + - id: 233 + name: SKU233 + - id: 234 + name: SKU234 + - id: 235 + name: SKU235 + - id: 236 + name: SKU236 + - id: 237 + name: SKU237 + - id: 238 + name: SKU238 + - id: 239 + name: SKU239 + - id: 240 + name: SKU240 + - id: 241 + name: SKU241 + - id: 242 + name: SKU242 + - id: 243 + name: SKU243 + - id: 244 + name: SKU244 + - id: 245 + name: SKU245 + - id: 246 + name: SKU246 + - id: 247 + name: SKU247 + - id: 248 + name: SKU248 + - id: 249 + name: SKU249 + - id: 250 + name: SKU250 + - id: 251 + name: SKU251 + - id: 252 + name: SKU252 + - id: 253 + name: SKU253 + - id: 254 + name: SKU254 + - id: 255 + name: SKU255 + - id: 256 + name: SKU256 + - id: 257 + name: SKU257 + - id: 258 + name: SKU258 + - id: 259 + name: SKU259 + - id: 260 + name: SKU260 + - id: 261 + name: SKU261 + - id: 262 + name: SKU262 + - id: 263 + name: SKU263 + - id: 264 + name: SKU264 + - id: 265 + name: SKU265 + - id: 266 + name: SKU266 + - id: 267 + name: SKU267 + - id: 268 + name: SKU268 + - id: 269 + name: SKU269 + - id: 270 + name: SKU270 + - id: 271 + name: SKU271 + - id: 272 + name: SKU272 + - id: 273 + name: SKU273 + - id: 274 + name: SKU274 + - id: 275 + name: SKU275 + - id: 276 + name: SKU276 + - id: 277 + name: SKU277 + - id: 278 + name: SKU278 + - id: 279 + name: SKU279 + - id: 280 + name: SKU280 + - id: 281 + name: SKU281 + - id: 282 + name: SKU282 + - id: 283 + name: SKU283 + - id: 284 + name: SKU284 + - id: 285 + name: SKU285 + - id: 286 + name: SKU286 + - id: 287 + name: SKU287 + - id: 288 + name: SKU288 + - id: 289 + name: SKU289 + - id: 290 + name: SKU290 + - id: 291 + name: SKU291 + - id: 292 + name: SKU292 + - id: 293 + name: SKU293 + - id: 294 + name: SKU294 + - id: 295 + name: SKU295 + - id: 296 + name: SKU296 + - id: 297 + name: SKU297 + - id: 298 + name: SKU298 + - id: 299 + name: SKU299 + - id: 300 + name: SKU300 + - id: 301 + name: SKU301 + - id: 302 + name: SKU302 + - id: 303 + name: SKU303 + - id: 304 + name: SKU304 + - id: 305 + name: SKU305 + - id: 306 + name: SKU306 + - id: 307 + name: SKU307 + - id: 308 + name: SKU308 + - id: 309 + name: SKU309 + - id: 310 + name: SKU310 + - id: 311 + name: SKU311 + - id: 312 + name: SKU312 + - id: 313 + name: SKU313 + - id: 314 + name: SKU314 + - id: 315 + name: SKU315 + - id: 316 + name: SKU316 + - id: 317 + name: SKU317 + - id: 318 + name: SKU318 + - id: 319 + name: SKU319 + - id: 320 + name: SKU320 + - id: 321 + name: SKU321 + - id: 322 + name: SKU322 + - id: 323 + name: SKU323 + - id: 324 + name: SKU324 + - id: 325 + name: SKU325 + - id: 326 + name: SKU326 + - id: 327 + name: SKU327 + - id: 328 + name: SKU328 + - id: 329 + name: SKU329 + - id: 330 + name: SKU330 + - id: 331 + name: SKU331 + - id: 332 + name: SKU332 + - id: 333 + name: SKU333 + - id: 334 + name: SKU334 + - id: 335 + name: SKU335 + - id: 336 + name: SKU336 + - id: 337 + name: SKU337 + - id: 338 + name: SKU338 + - id: 339 + name: SKU339 + - id: 340 + name: SKU340 + - id: 341 + name: SKU341 + - id: 342 + name: SKU342 + - id: 343 + name: SKU343 + - id: 344 + name: SKU344 + - id: 345 + name: SKU345 + - id: 346 + name: SKU346 + - id: 347 + name: SKU347 + - id: 348 + name: SKU348 + - id: 349 + name: SKU349 + - id: 350 + name: SKU350 + - id: 351 + name: SKU351 + - id: 352 + name: SKU352 + - id: 353 + name: SKU353 + - id: 354 + name: SKU354 + - id: 355 + name: SKU355 + - id: 356 + name: SKU356 + - id: 357 + name: SKU357 + - id: 358 + name: SKU358 + - id: 359 + name: SKU359 + - id: 360 + name: SKU360 + - id: 361 + name: SKU361 + - id: 362 + name: SKU362 + - id: 363 + name: SKU363 + - id: 364 + name: SKU364 + - id: 365 + name: SKU365 + - id: 366 + name: SKU366 + - id: 367 + name: SKU367 + - id: 368 + name: SKU368 + - id: 369 + name: SKU369 + - id: 370 + name: SKU370 + - id: 371 + name: SKU371 + - id: 372 + name: SKU372 + - id: 373 + name: SKU373 + - id: 374 + name: SKU374 + - id: 375 + name: SKU375 + - id: 376 + name: SKU376 + - id: 377 + name: SKU377 + - id: 378 + name: SKU378 + - id: 379 + name: SKU379 + - id: 380 + name: SKU380 + - id: 381 + name: SKU381 + - id: 382 + name: SKU382 + - id: 383 + name: SKU383 + - id: 384 + name: SKU384 + - id: 385 + name: SKU385 + - id: 386 + name: SKU386 + - id: 387 + name: SKU387 + - id: 388 + name: SKU388 + - id: 389 + name: SKU389 + - id: 390 + name: SKU390 + - id: 391 + name: SKU391 + - id: 392 + name: SKU392 + - id: 393 + name: SKU393 + - id: 394 + name: SKU394 + - id: 395 + name: SKU395 + - id: 396 + name: SKU396 + - id: 397 + name: SKU397 + - id: 398 + name: SKU398 + - id: 399 + name: SKU399 + - id: 400 + name: SKU400 + - id: 401 + name: SKU401 + - id: 402 + name: SKU402 + - id: 403 + name: SKU403 + - id: 404 + name: SKU404 + - id: 405 + name: SKU405 + - id: 406 + name: SKU406 + - id: 407 + name: SKU407 + - id: 408 + name: SKU408 + - id: 409 + name: SKU409 + - id: 410 + name: SKU410 + - id: 411 + name: SKU411 + - id: 412 + name: SKU412 + - id: 413 + name: SKU413 + - id: 414 + name: SKU414 + - id: 415 + name: SKU415 + - id: 416 + name: SKU416 + - id: 417 + name: SKU417 + - id: 418 + name: SKU418 + - id: 419 + name: SKU419 + - id: 420 + name: SKU420 + - id: 421 + name: SKU421 + - id: 422 + name: SKU422 + - id: 423 + name: SKU423 + - id: 424 + name: SKU424 + - id: 425 + name: SKU425 + - id: 426 + name: SKU426 + - id: 427 + name: SKU427 + - id: 428 + name: SKU428 + - id: 429 + name: SKU429 + - id: 430 + name: SKU430 + - id: 431 + name: SKU431 + - id: 432 + name: SKU432 + - id: 433 + name: SKU433 + - id: 434 + name: SKU434 + - id: 435 + name: SKU435 + - id: 436 + name: SKU436 + - id: 437 + name: SKU437 + - id: 438 + name: SKU438 + - id: 439 + name: SKU439 + - id: 440 + name: SKU440 + - id: 441 + name: SKU441 + - id: 442 + name: SKU442 + - id: 443 + name: SKU443 + - id: 444 + name: SKU444 + - id: 445 + name: SKU445 + - id: 446 + name: SKU446 + - id: 447 + name: SKU447 + - id: 448 + name: SKU448 + - id: 449 + name: SKU449 + - id: 450 + name: SKU450 + - id: 451 + name: SKU451 + - id: 452 + name: SKU452 + - id: 453 + name: SKU453 + - id: 454 + name: SKU454 + - id: 455 + name: SKU455 + - id: 456 + name: SKU456 + - id: 457 + name: SKU457 + - id: 458 + name: SKU458 + - id: 459 + name: SKU459 + - id: 460 + name: SKU460 + - id: 461 + name: SKU461 + - id: 462 + name: SKU462 + - id: 463 + name: SKU463 + - id: 464 + name: SKU464 + - id: 465 + name: SKU465 + - id: 466 + name: SKU466 + - id: 467 + name: SKU467 + - id: 468 + name: SKU468 + - id: 469 + name: SKU469 + - id: 470 + name: SKU470 + - id: 471 + name: SKU471 + - id: 472 + name: SKU472 + - id: 473 + name: SKU473 + - id: 474 + name: SKU474 + - id: 475 + name: SKU475 + - id: 476 + name: SKU476 + - id: 477 + name: SKU477 + - id: 478 + name: SKU478 + - id: 479 + name: SKU479 + - id: 480 + name: SKU480 + - id: 481 + name: SKU481 + - id: 482 + name: SKU482 + - id: 483 + name: SKU483 + - id: 484 + name: SKU484 + - id: 485 + name: SKU485 + - id: 486 + name: SKU486 + - id: 487 + name: SKU487 + - id: 488 + name: SKU488 + - id: 489 + name: SKU489 + - id: 490 + name: SKU490 + - id: 491 + name: SKU491 + - id: 492 + name: SKU492 + - id: 493 + name: SKU493 + - id: 494 + name: SKU494 + - id: 495 + name: SKU495 + - id: 496 + name: SKU496 + - id: 497 + name: SKU497 + - id: 498 + name: SKU498 + - id: 499 + name: SKU499 + - id: 500 + name: SKU500 + - id: 501 + name: SKU501 + - id: 502 + name: SKU502 + - id: 503 + name: SKU503 + - id: 504 + name: SKU504 + - id: 505 + name: SKU505 + - id: 506 + name: SKU506 + - id: 507 + name: SKU507 + - id: 508 + name: SKU508 + - id: 509 + name: SKU509 + - id: 510 + name: SKU510 + - id: 511 + name: SKU511 + - id: 512 + name: SKU512 + - id: 513 + name: SKU513 + - id: 514 + name: SKU514 + - id: 515 + name: SKU515 + - id: 516 + name: SKU516 + - id: 517 + name: SKU517 + - id: 518 + name: SKU518 + - id: 519 + name: SKU519 + - id: 520 + name: SKU520 + - id: 521 + name: SKU521 + - id: 522 + name: SKU522 + - id: 523 + name: SKU523 + - id: 524 + name: SKU524 + - id: 525 + name: SKU525 + - id: 526 + name: SKU526 + - id: 527 + name: SKU527 + - id: 528 + name: SKU528 + - id: 529 + name: SKU529 + - id: 530 + name: SKU530 + - id: 531 + name: SKU531 + - id: 532 + name: SKU532 + - id: 533 + name: SKU533 + - id: 534 + name: SKU534 + - id: 535 + name: SKU535 + - id: 536 + name: SKU536 + - id: 537 + name: SKU537 + - id: 538 + name: SKU538 + - id: 539 + name: SKU539 + - id: 540 + name: SKU540 + - id: 541 + name: SKU541 + - id: 542 + name: SKU542 + - id: 543 + name: SKU543 + - id: 544 + name: SKU544 + - id: 545 + name: SKU545 + - id: 546 + name: SKU546 + - id: 547 + name: SKU547 + - id: 548 + name: SKU548 + - id: 549 + name: SKU549 + - id: 550 + name: SKU550 + - id: 551 + name: SKU551 + - id: 552 + name: SKU552 + - id: 553 + name: SKU553 + - id: 554 + name: SKU554 + - id: 555 + name: SKU555 + - id: 556 + name: SKU556 + - id: 557 + name: SKU557 + - id: 558 + name: SKU558 + - id: 559 + name: SKU559 + - id: 560 + name: SKU560 + - id: 561 + name: SKU561 + - id: 562 + name: SKU562 + - id: 563 + name: SKU563 + - id: 564 + name: SKU564 + - id: 565 + name: SKU565 + - id: 566 + name: SKU566 + - id: 567 + name: SKU567 + - id: 568 + name: SKU568 + - id: 569 + name: SKU569 + - id: 570 + name: SKU570 + - id: 571 + name: SKU571 + - id: 572 + name: SKU572 + - id: 573 + name: SKU573 + - id: 574 + name: SKU574 + - id: 575 + name: SKU575 + - id: 576 + name: SKU576 + - id: 577 + name: SKU577 + - id: 578 + name: SKU578 + - id: 579 + name: SKU579 + - id: 580 + name: SKU580 + - id: 581 + name: SKU581 + - id: 582 + name: SKU582 + - id: 583 + name: SKU583 + - id: 584 + name: SKU584 + - id: 585 + name: SKU585 + - id: 586 + name: SKU586 + - id: 587 + name: SKU587 + - id: 588 + name: SKU588 + - id: 589 + name: SKU589 + - id: 590 + name: SKU590 + - id: 591 + name: SKU591 + - id: 592 + name: SKU592 + - id: 593 + name: SKU593 + - id: 594 + name: SKU594 + - id: 595 + name: SKU595 + - id: 596 + name: SKU596 + - id: 597 + name: SKU597 + - id: 598 + name: SKU598 + - id: 599 + name: SKU599 + - id: 600 + name: SKU600 + - id: 601 + name: SKU601 + - id: 602 + name: SKU602 + - id: 603 + name: SKU603 + - id: 604 + name: SKU604 + - id: 605 + name: SKU605 + - id: 606 + name: SKU606 + - id: 607 + name: SKU607 + - id: 608 + name: SKU608 + - id: 609 + name: SKU609 + - id: 610 + name: SKU610 + - id: 611 + name: SKU611 + - id: 612 + name: SKU612 + - id: 613 + name: SKU613 + - id: 614 + name: SKU614 + - id: 615 + name: SKU615 + - id: 616 + name: SKU616 + - id: 617 + name: SKU617 + - id: 618 + name: SKU618 + - id: 619 + name: SKU619 + - id: 620 + name: SKU620 + - id: 621 + name: SKU621 + - id: 622 + name: SKU622 + - id: 623 + name: SKU623 + - id: 624 + name: SKU624 + - id: 625 + name: SKU625 + - id: 626 + name: SKU626 + - id: 627 + name: SKU627 + - id: 628 + name: SKU628 + - id: 629 + name: SKU629 + - id: 630 + name: SKU630 + - id: 631 + name: SKU631 + - id: 632 + name: SKU632 + - id: 633 + name: SKU633 + - id: 634 + name: SKU634 + - id: 635 + name: SKU635 + - id: 636 + name: SKU636 + - id: 637 + name: SKU637 + - id: 638 + name: SKU638 + - id: 639 + name: SKU639 + - id: 640 + name: SKU640 + - id: 641 + name: SKU641 + - id: 642 + name: SKU642 + - id: 643 + name: SKU643 + - id: 644 + name: SKU644 + - id: 645 + name: SKU645 + - id: 646 + name: SKU646 + - id: 647 + name: SKU647 + - id: 648 + name: SKU648 + - id: 649 + name: SKU649 + - id: 650 + name: SKU650 + - id: 651 + name: SKU651 + - id: 652 + name: SKU652 + - id: 653 + name: SKU653 + - id: 654 + name: SKU654 + - id: 655 + name: SKU655 + - id: 656 + name: SKU656 + - id: 657 + name: SKU657 + - id: 658 + name: SKU658 + - id: 659 + name: SKU659 + - id: 660 + name: SKU660 + - id: 661 + name: SKU661 + - id: 662 + name: SKU662 + - id: 663 + name: SKU663 + - id: 664 + name: SKU664 + - id: 665 + name: SKU665 + - id: 666 + name: SKU666 + - id: 667 + name: SKU667 + - id: 668 + name: SKU668 + - id: 669 + name: SKU669 + - id: 670 + name: SKU670 + - id: 671 + name: SKU671 + - id: 672 + name: SKU672 + - id: 673 + name: SKU673 + - id: 674 + name: SKU674 + - id: 675 + name: SKU675 + - id: 676 + name: SKU676 + - id: 677 + name: SKU677 + - id: 678 + name: SKU678 + - id: 679 + name: SKU679 + - id: 680 + name: SKU680 + - id: 681 + name: SKU681 + - id: 682 + name: SKU682 + - id: 683 + name: SKU683 + - id: 684 + name: SKU684 + - id: 685 + name: SKU685 + - id: 686 + name: SKU686 + - id: 687 + name: SKU687 + - id: 688 + name: SKU688 + - id: 689 + name: SKU689 + - id: 690 + name: SKU690 + - id: 691 + name: SKU691 + - id: 692 + name: SKU692 + - id: 693 + name: SKU693 + - id: 694 + name: SKU694 + - id: 695 + name: SKU695 + - id: 696 + name: SKU696 + - id: 697 + name: SKU697 + - id: 698 + name: SKU698 + - id: 699 + name: SKU699 + - id: 700 + name: SKU700 + - id: 701 + name: SKU701 + - id: 702 + name: SKU702 + - id: 703 + name: SKU703 + - id: 704 + name: SKU704 + - id: 705 + name: SKU705 + - id: 706 + name: SKU706 + - id: 707 + name: SKU707 + - id: 708 + name: SKU708 + - id: 709 + name: SKU709 + - id: 710 + name: SKU710 + - id: 711 + name: SKU711 + - id: 712 + name: SKU712 + - id: 713 + name: SKU713 + - id: 714 + name: SKU714 + - id: 715 + name: SKU715 + - id: 716 + name: SKU716 + - id: 717 + name: SKU717 + - id: 718 + name: SKU718 + - id: 719 + name: SKU719 + - id: 720 + name: SKU720 + - id: 721 + name: SKU721 + - id: 722 + name: SKU722 + - id: 723 + name: SKU723 + - id: 724 + name: SKU724 + - id: 725 + name: SKU725 + - id: 726 + name: SKU726 + - id: 727 + name: SKU727 + - id: 728 + name: SKU728 + - id: 729 + name: SKU729 + - id: 730 + name: SKU730 + - id: 731 + name: SKU731 + - id: 732 + name: SKU732 + - id: 733 + name: SKU733 + - id: 734 + name: SKU734 + - id: 735 + name: SKU735 + - id: 736 + name: SKU736 + - id: 737 + name: SKU737 + - id: 738 + name: SKU738 + - id: 739 + name: SKU739 + - id: 740 + name: SKU740 + - id: 741 + name: SKU741 + - id: 742 + name: SKU742 + - id: 743 + name: SKU743 + - id: 744 + name: SKU744 + - id: 745 + name: SKU745 + - id: 746 + name: SKU746 + - id: 747 + name: SKU747 + - id: 748 + name: SKU748 + - id: 749 + name: SKU749 + - id: 750 + name: SKU750 + - id: 751 + name: SKU751 + - id: 752 + name: SKU752 + - id: 753 + name: SKU753 + - id: 754 + name: SKU754 + - id: 755 + name: SKU755 + - id: 756 + name: SKU756 + - id: 757 + name: SKU757 + - id: 758 + name: SKU758 + - id: 759 + name: SKU759 + - id: 760 + name: SKU760 + - id: 761 + name: SKU761 + - id: 762 + name: SKU762 + - id: 763 + name: SKU763 + - id: 764 + name: SKU764 + - id: 765 + name: SKU765 + - id: 766 + name: SKU766 + - id: 767 + name: SKU767 + - id: 768 + name: SKU768 + - id: 769 + name: SKU769 + - id: 770 + name: SKU770 + - id: 771 + name: SKU771 + - id: 772 + name: SKU772 + - id: 773 + name: SKU773 + - id: 774 + name: SKU774 + - id: 775 + name: SKU775 + - id: 776 + name: SKU776 + - id: 777 + name: SKU777 + - id: 778 + name: SKU778 + - id: 779 + name: SKU779 + - id: 780 + name: SKU780 + - id: 781 + name: SKU781 + - id: 782 + name: SKU782 + - id: 783 + name: SKU783 + - id: 784 + name: SKU784 + - id: 785 + name: SKU785 + - id: 786 + name: SKU786 + - id: 787 + name: SKU787 + - id: 788 + name: SKU788 + - id: 789 + name: SKU789 + - id: 790 + name: SKU790 + - id: 791 + name: SKU791 + - id: 792 + name: SKU792 + - id: 793 + name: SKU793 + - id: 794 + name: SKU794 + - id: 795 + name: SKU795 + - id: 796 + name: SKU796 + - id: 797 + name: SKU797 + - id: 798 + name: SKU798 + - id: 799 + name: SKU799 + - id: 800 + name: SKU800 + - id: 801 + name: SKU801 + - id: 802 + name: SKU802 + - id: 803 + name: SKU803 + - id: 804 + name: SKU804 + - id: 805 + name: SKU805 + - id: 806 + name: SKU806 + - id: 807 + name: SKU807 + - id: 808 + name: SKU808 + - id: 809 + name: SKU809 + - id: 810 + name: SKU810 + - id: 811 + name: SKU811 + - id: 812 + name: SKU812 + - id: 813 + name: SKU813 + - id: 814 + name: SKU814 + - id: 815 + name: SKU815 + - id: 816 + name: SKU816 + - id: 817 + name: SKU817 + - id: 818 + name: SKU818 + - id: 819 + name: SKU819 + - id: 820 + name: SKU820 + - id: 821 + name: SKU821 + - id: 822 + name: SKU822 + - id: 823 + name: SKU823 + - id: 824 + name: SKU824 + - id: 825 + name: SKU825 + - id: 826 + name: SKU826 + - id: 827 + name: SKU827 + - id: 828 + name: SKU828 + - id: 829 + name: SKU829 + - id: 830 + name: SKU830 + - id: 831 + name: SKU831 + - id: 832 + name: SKU832 + - id: 833 + name: SKU833 + - id: 834 + name: SKU834 + - id: 835 + name: SKU835 + - id: 836 + name: SKU836 + - id: 837 + name: SKU837 + - id: 838 + name: SKU838 + - id: 839 + name: SKU839 + - id: 840 + name: SKU840 + - id: 841 + name: SKU841 + - id: 842 + name: SKU842 + - id: 843 + name: SKU843 + - id: 844 + name: SKU844 + - id: 845 + name: SKU845 + - id: 846 + name: SKU846 + - id: 847 + name: SKU847 + - id: 848 + name: SKU848 + - id: 849 + name: SKU849 + - id: 850 + name: SKU850 + - id: 851 + name: SKU851 + - id: 852 + name: SKU852 + - id: 853 + name: SKU853 + - id: 854 + name: SKU854 + - id: 855 + name: SKU855 + - id: 856 + name: SKU856 + - id: 857 + name: SKU857 + - id: 858 + name: SKU858 + - id: 859 + name: SKU859 + - id: 860 + name: SKU860 + - id: 861 + name: SKU861 + - id: 862 + name: SKU862 + - id: 863 + name: SKU863 + - id: 864 + name: SKU864 + - id: 865 + name: SKU865 + - id: 866 + name: SKU866 + - id: 867 + name: SKU867 + - id: 868 + name: SKU868 + - id: 869 + name: SKU869 + - id: 870 + name: SKU870 + - id: 871 + name: SKU871 + - id: 872 + name: SKU872 + - id: 873 + name: SKU873 + - id: 874 + name: SKU874 + - id: 875 + name: SKU875 + - id: 876 + name: SKU876 + - id: 877 + name: SKU877 + - id: 878 + name: SKU878 + - id: 879 + name: SKU879 + - id: 880 + name: SKU880 + - id: 881 + name: SKU881 + - id: 882 + name: SKU882 + - id: 883 + name: SKU883 + - id: 884 + name: SKU884 + - id: 885 + name: SKU885 + - id: 886 + name: SKU886 + - id: 887 + name: SKU887 + - id: 888 + name: SKU888 + - id: 889 + name: SKU889 + - id: 890 + name: SKU890 + - id: 891 + name: SKU891 + - id: 892 + name: SKU892 + - id: 893 + name: SKU893 + - id: 894 + name: SKU894 + - id: 895 + name: SKU895 + - id: 896 + name: SKU896 + - id: 897 + name: SKU897 + - id: 898 + name: SKU898 + - id: 899 + name: SKU899 + - id: 900 + name: SKU900 + - id: 901 + name: SKU901 + - id: 902 + name: SKU902 + - id: 903 + name: SKU903 + - id: 904 + name: SKU904 + - id: 905 + name: SKU905 + - id: 906 + name: SKU906 + - id: 907 + name: SKU907 + - id: 908 + name: SKU908 + - id: 909 + name: SKU909 + - id: 910 + name: SKU910 + - id: 911 + name: SKU911 + - id: 912 + name: SKU912 + - id: 913 + name: SKU913 + - id: 914 + name: SKU914 + - id: 915 + name: SKU915 + - id: 916 + name: SKU916 + - id: 917 + name: SKU917 + - id: 918 + name: SKU918 + - id: 919 + name: SKU919 + - id: 920 + name: SKU920 + - id: 921 + name: SKU921 + - id: 922 + name: SKU922 + - id: 923 + name: SKU923 + - id: 924 + name: SKU924 + - id: 925 + name: SKU925 + - id: 926 + name: SKU926 + - id: 927 + name: SKU927 + - id: 928 + name: SKU928 + - id: 929 + name: SKU929 + - id: 930 + name: SKU930 + - id: 931 + name: SKU931 + - id: 932 + name: SKU932 + - id: 933 + name: SKU933 + - id: 934 + name: SKU934 + - id: 935 + name: SKU935 + - id: 936 + name: SKU936 + - id: 937 + name: SKU937 + - id: 938 + name: SKU938 + - id: 939 + name: SKU939 + - id: 940 + name: SKU940 + - id: 941 + name: SKU941 + - id: 942 + name: SKU942 + - id: 943 + name: SKU943 + - id: 944 + name: SKU944 + - id: 945 + name: SKU945 + - id: 946 + name: SKU946 + - id: 947 + name: SKU947 + - id: 948 + name: SKU948 + - id: 949 + name: SKU949 + - id: 950 + name: SKU950 + - id: 951 + name: SKU951 + - id: 952 + name: SKU952 + - id: 953 + name: SKU953 + - id: 954 + name: SKU954 + - id: 955 + name: SKU955 + - id: 956 + name: SKU956 + - id: 957 + name: SKU957 + - id: 958 + name: SKU958 + - id: 959 + name: SKU959 + - id: 960 + name: SKU960 + - id: 961 + name: SKU961 + - id: 962 + name: SKU962 + - id: 963 + name: SKU963 + - id: 964 + name: SKU964 + - id: 965 + name: SKU965 + - id: 966 + name: SKU966 + - id: 967 + name: SKU967 + - id: 968 + name: SKU968 + - id: 969 + name: SKU969 + - id: 970 + name: SKU970 + - id: 971 + name: SKU971 + - id: 972 + name: SKU972 + - id: 973 + name: SKU973 + - id: 974 + name: SKU974 + - id: 975 + name: SKU975 + - id: 976 + name: SKU976 + - id: 977 + name: SKU977 + - id: 978 + name: SKU978 + - id: 979 + name: SKU979 + - id: 980 + name: SKU980 + - id: 981 + name: SKU981 + - id: 982 + name: SKU982 + - id: 983 + name: SKU983 + - id: 984 + name: SKU984 + - id: 985 + name: SKU985 + - id: 986 + name: SKU986 + - id: 987 + name: SKU987 + - id: 988 + name: SKU988 + - id: 989 + name: SKU989 + - id: 990 + name: SKU990 + - id: 991 + name: SKU991 + - id: 992 + name: SKU992 + - id: 993 + name: SKU993 + - id: 994 + name: SKU994 + - id: 995 + name: SKU995 + - id: 996 + name: SKU996 + - id: 997 + name: SKU997 + - id: 998 + name: SKU998 + - id: 999 + name: SKU999 + topology: + STORE0: + SKU0: + - WAREHOUSE0 + SKU1: + - WAREHOUSE0 + SKU10: + - WAREHOUSE0 + SKU100: + - WAREHOUSE0 + SKU101: + - WAREHOUSE0 + SKU102: + - WAREHOUSE0 + SKU103: + - WAREHOUSE0 + SKU104: + - WAREHOUSE0 + SKU105: + - WAREHOUSE0 + SKU106: + - WAREHOUSE0 + SKU107: + - WAREHOUSE0 + SKU108: + - WAREHOUSE0 + SKU109: + - WAREHOUSE0 + SKU11: + - WAREHOUSE0 + SKU110: + - WAREHOUSE0 + SKU111: + - WAREHOUSE0 + SKU112: + - WAREHOUSE0 + SKU113: + - WAREHOUSE0 + SKU114: + - WAREHOUSE0 + SKU115: + - WAREHOUSE0 + SKU116: + - WAREHOUSE0 + SKU117: + - WAREHOUSE0 + SKU118: + - WAREHOUSE0 + SKU119: + - WAREHOUSE0 + SKU12: + - WAREHOUSE0 + SKU120: + - WAREHOUSE0 + SKU121: + - WAREHOUSE0 + SKU122: + - WAREHOUSE0 + SKU123: + - WAREHOUSE0 + SKU124: + - WAREHOUSE0 + SKU125: + - WAREHOUSE0 + SKU126: + - WAREHOUSE0 + SKU127: + - WAREHOUSE0 + SKU128: + - WAREHOUSE0 + SKU129: + - WAREHOUSE0 + SKU13: + - WAREHOUSE0 + SKU130: + - WAREHOUSE0 + SKU131: + - WAREHOUSE0 + SKU132: + - WAREHOUSE0 + SKU133: + - WAREHOUSE0 + SKU134: + - WAREHOUSE0 + SKU135: + - WAREHOUSE0 + SKU136: + - WAREHOUSE0 + SKU137: + - WAREHOUSE0 + SKU138: + - WAREHOUSE0 + SKU139: + - WAREHOUSE0 + SKU14: + - WAREHOUSE0 + SKU140: + - WAREHOUSE0 + SKU141: + - WAREHOUSE0 + SKU142: + - WAREHOUSE0 + SKU143: + - WAREHOUSE0 + SKU144: + - WAREHOUSE0 + SKU145: + - WAREHOUSE0 + SKU146: + - WAREHOUSE0 + SKU147: + - WAREHOUSE0 + SKU148: + - WAREHOUSE0 + SKU149: + - WAREHOUSE0 + SKU15: + - WAREHOUSE0 + SKU150: + - WAREHOUSE0 + SKU151: + - WAREHOUSE0 + SKU152: + - WAREHOUSE0 + SKU153: + - WAREHOUSE0 + SKU154: + - WAREHOUSE0 + SKU155: + - WAREHOUSE0 + SKU156: + - WAREHOUSE0 + SKU157: + - WAREHOUSE0 + SKU158: + - WAREHOUSE0 + SKU159: + - WAREHOUSE0 + SKU16: + - WAREHOUSE0 + SKU160: + - WAREHOUSE0 + SKU161: + - WAREHOUSE0 + SKU162: + - WAREHOUSE0 + SKU163: + - WAREHOUSE0 + SKU164: + - WAREHOUSE0 + SKU165: + - WAREHOUSE0 + SKU166: + - WAREHOUSE0 + SKU167: + - WAREHOUSE0 + SKU168: + - WAREHOUSE0 + SKU169: + - WAREHOUSE0 + SKU17: + - WAREHOUSE0 + SKU170: + - WAREHOUSE0 + SKU171: + - WAREHOUSE0 + SKU172: + - WAREHOUSE0 + SKU173: + - WAREHOUSE0 + SKU174: + - WAREHOUSE0 + SKU175: + - WAREHOUSE0 + SKU176: + - WAREHOUSE0 + SKU177: + - WAREHOUSE0 + SKU178: + - WAREHOUSE0 + SKU179: + - WAREHOUSE0 + SKU18: + - WAREHOUSE0 + SKU180: + - WAREHOUSE0 + SKU181: + - WAREHOUSE0 + SKU182: + - WAREHOUSE0 + SKU183: + - WAREHOUSE0 + SKU184: + - WAREHOUSE0 + SKU185: + - WAREHOUSE0 + SKU186: + - WAREHOUSE0 + SKU187: + - WAREHOUSE0 + SKU188: + - WAREHOUSE0 + SKU189: + - WAREHOUSE0 + SKU19: + - WAREHOUSE0 + SKU190: + - WAREHOUSE0 + SKU191: + - WAREHOUSE0 + SKU192: + - WAREHOUSE0 + SKU193: + - WAREHOUSE0 + SKU194: + - WAREHOUSE0 + SKU195: + - WAREHOUSE0 + SKU196: + - WAREHOUSE0 + SKU197: + - WAREHOUSE0 + SKU198: + - WAREHOUSE0 + SKU199: + - WAREHOUSE0 + SKU2: + - WAREHOUSE0 + SKU20: + - WAREHOUSE0 + SKU200: + - WAREHOUSE0 + SKU201: + - WAREHOUSE0 + SKU202: + - WAREHOUSE0 + SKU203: + - WAREHOUSE0 + SKU204: + - WAREHOUSE0 + SKU205: + - WAREHOUSE0 + SKU206: + - WAREHOUSE0 + SKU207: + - WAREHOUSE0 + SKU208: + - WAREHOUSE0 + SKU209: + - WAREHOUSE0 + SKU21: + - WAREHOUSE0 + SKU210: + - WAREHOUSE0 + SKU211: + - WAREHOUSE0 + SKU212: + - WAREHOUSE0 + SKU213: + - WAREHOUSE0 + SKU214: + - WAREHOUSE0 + SKU215: + - WAREHOUSE0 + SKU216: + - WAREHOUSE0 + SKU217: + - WAREHOUSE0 + SKU218: + - WAREHOUSE0 + SKU219: + - WAREHOUSE0 + SKU22: + - WAREHOUSE0 + SKU220: + - WAREHOUSE0 + SKU221: + - WAREHOUSE0 + SKU222: + - WAREHOUSE0 + SKU223: + - WAREHOUSE0 + SKU224: + - WAREHOUSE0 + SKU225: + - WAREHOUSE0 + SKU226: + - WAREHOUSE0 + SKU227: + - WAREHOUSE0 + SKU228: + - WAREHOUSE0 + SKU229: + - WAREHOUSE0 + SKU23: + - WAREHOUSE0 + SKU230: + - WAREHOUSE0 + SKU231: + - WAREHOUSE0 + SKU232: + - WAREHOUSE0 + SKU233: + - WAREHOUSE0 + SKU234: + - WAREHOUSE0 + SKU235: + - WAREHOUSE0 + SKU236: + - WAREHOUSE0 + SKU237: + - WAREHOUSE0 + SKU238: + - WAREHOUSE0 + SKU239: + - WAREHOUSE0 + SKU24: + - WAREHOUSE0 + SKU240: + - WAREHOUSE0 + SKU241: + - WAREHOUSE0 + SKU242: + - WAREHOUSE0 + SKU243: + - WAREHOUSE0 + SKU244: + - WAREHOUSE0 + SKU245: + - WAREHOUSE0 + SKU246: + - WAREHOUSE0 + SKU247: + - WAREHOUSE0 + SKU248: + - WAREHOUSE0 + SKU249: + - WAREHOUSE0 + SKU25: + - WAREHOUSE0 + SKU250: + - WAREHOUSE0 + SKU251: + - WAREHOUSE0 + SKU252: + - WAREHOUSE0 + SKU253: + - WAREHOUSE0 + SKU254: + - WAREHOUSE0 + SKU255: + - WAREHOUSE0 + SKU256: + - WAREHOUSE0 + SKU257: + - WAREHOUSE0 + SKU258: + - WAREHOUSE0 + SKU259: + - WAREHOUSE0 + SKU26: + - WAREHOUSE0 + SKU260: + - WAREHOUSE0 + SKU261: + - WAREHOUSE0 + SKU262: + - WAREHOUSE0 + SKU263: + - WAREHOUSE0 + SKU264: + - WAREHOUSE0 + SKU265: + - WAREHOUSE0 + SKU266: + - WAREHOUSE0 + SKU267: + - WAREHOUSE0 + SKU268: + - WAREHOUSE0 + SKU269: + - WAREHOUSE0 + SKU27: + - WAREHOUSE0 + SKU270: + - WAREHOUSE0 + SKU271: + - WAREHOUSE0 + SKU272: + - WAREHOUSE0 + SKU273: + - WAREHOUSE0 + SKU274: + - WAREHOUSE0 + SKU275: + - WAREHOUSE0 + SKU276: + - WAREHOUSE0 + SKU277: + - WAREHOUSE0 + SKU278: + - WAREHOUSE0 + SKU279: + - WAREHOUSE0 + SKU28: + - WAREHOUSE0 + SKU280: + - WAREHOUSE0 + SKU281: + - WAREHOUSE0 + SKU282: + - WAREHOUSE0 + SKU283: + - WAREHOUSE0 + SKU284: + - WAREHOUSE0 + SKU285: + - WAREHOUSE0 + SKU286: + - WAREHOUSE0 + SKU287: + - WAREHOUSE0 + SKU288: + - WAREHOUSE0 + SKU289: + - WAREHOUSE0 + SKU29: + - WAREHOUSE0 + SKU290: + - WAREHOUSE0 + SKU291: + - WAREHOUSE0 + SKU292: + - WAREHOUSE0 + SKU293: + - WAREHOUSE0 + SKU294: + - WAREHOUSE0 + SKU295: + - WAREHOUSE0 + SKU296: + - WAREHOUSE0 + SKU297: + - WAREHOUSE0 + SKU298: + - WAREHOUSE0 + SKU299: + - WAREHOUSE0 + SKU3: + - WAREHOUSE0 + SKU30: + - WAREHOUSE0 + SKU300: + - WAREHOUSE0 + SKU301: + - WAREHOUSE0 + SKU302: + - WAREHOUSE0 + SKU303: + - WAREHOUSE0 + SKU304: + - WAREHOUSE0 + SKU305: + - WAREHOUSE0 + SKU306: + - WAREHOUSE0 + SKU307: + - WAREHOUSE0 + SKU308: + - WAREHOUSE0 + SKU309: + - WAREHOUSE0 + SKU31: + - WAREHOUSE0 + SKU310: + - WAREHOUSE0 + SKU311: + - WAREHOUSE0 + SKU312: + - WAREHOUSE0 + SKU313: + - WAREHOUSE0 + SKU314: + - WAREHOUSE0 + SKU315: + - WAREHOUSE0 + SKU316: + - WAREHOUSE0 + SKU317: + - WAREHOUSE0 + SKU318: + - WAREHOUSE0 + SKU319: + - WAREHOUSE0 + SKU32: + - WAREHOUSE0 + SKU320: + - WAREHOUSE0 + SKU321: + - WAREHOUSE0 + SKU322: + - WAREHOUSE0 + SKU323: + - WAREHOUSE0 + SKU324: + - WAREHOUSE0 + SKU325: + - WAREHOUSE0 + SKU326: + - WAREHOUSE0 + SKU327: + - WAREHOUSE0 + SKU328: + - WAREHOUSE0 + SKU329: + - WAREHOUSE0 + SKU33: + - WAREHOUSE0 + SKU330: + - WAREHOUSE0 + SKU331: + - WAREHOUSE0 + SKU332: + - WAREHOUSE0 + SKU333: + - WAREHOUSE0 + SKU334: + - WAREHOUSE0 + SKU335: + - WAREHOUSE0 + SKU336: + - WAREHOUSE0 + SKU337: + - WAREHOUSE0 + SKU338: + - WAREHOUSE0 + SKU339: + - WAREHOUSE0 + SKU34: + - WAREHOUSE0 + SKU340: + - WAREHOUSE0 + SKU341: + - WAREHOUSE0 + SKU342: + - WAREHOUSE0 + SKU343: + - WAREHOUSE0 + SKU344: + - WAREHOUSE0 + SKU345: + - WAREHOUSE0 + SKU346: + - WAREHOUSE0 + SKU347: + - WAREHOUSE0 + SKU348: + - WAREHOUSE0 + SKU349: + - WAREHOUSE0 + SKU35: + - WAREHOUSE0 + SKU350: + - WAREHOUSE0 + SKU351: + - WAREHOUSE0 + SKU352: + - WAREHOUSE0 + SKU353: + - WAREHOUSE0 + SKU354: + - WAREHOUSE0 + SKU355: + - WAREHOUSE0 + SKU356: + - WAREHOUSE0 + SKU357: + - WAREHOUSE0 + SKU358: + - WAREHOUSE0 + SKU359: + - WAREHOUSE0 + SKU36: + - WAREHOUSE0 + SKU360: + - WAREHOUSE0 + SKU361: + - WAREHOUSE0 + SKU362: + - WAREHOUSE0 + SKU363: + - WAREHOUSE0 + SKU364: + - WAREHOUSE0 + SKU365: + - WAREHOUSE0 + SKU366: + - WAREHOUSE0 + SKU367: + - WAREHOUSE0 + SKU368: + - WAREHOUSE0 + SKU369: + - WAREHOUSE0 + SKU37: + - WAREHOUSE0 + SKU370: + - WAREHOUSE0 + SKU371: + - WAREHOUSE0 + SKU372: + - WAREHOUSE0 + SKU373: + - WAREHOUSE0 + SKU374: + - WAREHOUSE0 + SKU375: + - WAREHOUSE0 + SKU376: + - WAREHOUSE0 + SKU377: + - WAREHOUSE0 + SKU378: + - WAREHOUSE0 + SKU379: + - WAREHOUSE0 + SKU38: + - WAREHOUSE0 + SKU380: + - WAREHOUSE0 + SKU381: + - WAREHOUSE0 + SKU382: + - WAREHOUSE0 + SKU383: + - WAREHOUSE0 + SKU384: + - WAREHOUSE0 + SKU385: + - WAREHOUSE0 + SKU386: + - WAREHOUSE0 + SKU387: + - WAREHOUSE0 + SKU388: + - WAREHOUSE0 + SKU389: + - WAREHOUSE0 + SKU39: + - WAREHOUSE0 + SKU390: + - WAREHOUSE0 + SKU391: + - WAREHOUSE0 + SKU392: + - WAREHOUSE0 + SKU393: + - WAREHOUSE0 + SKU394: + - WAREHOUSE0 + SKU395: + - WAREHOUSE0 + SKU396: + - WAREHOUSE0 + SKU397: + - WAREHOUSE0 + SKU398: + - WAREHOUSE0 + SKU399: + - WAREHOUSE0 + SKU4: + - WAREHOUSE0 + SKU40: + - WAREHOUSE0 + SKU400: + - WAREHOUSE0 + SKU401: + - WAREHOUSE0 + SKU402: + - WAREHOUSE0 + SKU403: + - WAREHOUSE0 + SKU404: + - WAREHOUSE0 + SKU405: + - WAREHOUSE0 + SKU406: + - WAREHOUSE0 + SKU407: + - WAREHOUSE0 + SKU408: + - WAREHOUSE0 + SKU409: + - WAREHOUSE0 + SKU41: + - WAREHOUSE0 + SKU410: + - WAREHOUSE0 + SKU411: + - WAREHOUSE0 + SKU412: + - WAREHOUSE0 + SKU413: + - WAREHOUSE0 + SKU414: + - WAREHOUSE0 + SKU415: + - WAREHOUSE0 + SKU416: + - WAREHOUSE0 + SKU417: + - WAREHOUSE0 + SKU418: + - WAREHOUSE0 + SKU419: + - WAREHOUSE0 + SKU42: + - WAREHOUSE0 + SKU420: + - WAREHOUSE0 + SKU421: + - WAREHOUSE0 + SKU422: + - WAREHOUSE0 + SKU423: + - WAREHOUSE0 + SKU424: + - WAREHOUSE0 + SKU425: + - WAREHOUSE0 + SKU426: + - WAREHOUSE0 + SKU427: + - WAREHOUSE0 + SKU428: + - WAREHOUSE0 + SKU429: + - WAREHOUSE0 + SKU43: + - WAREHOUSE0 + SKU430: + - WAREHOUSE0 + SKU431: + - WAREHOUSE0 + SKU432: + - WAREHOUSE0 + SKU433: + - WAREHOUSE0 + SKU434: + - WAREHOUSE0 + SKU435: + - WAREHOUSE0 + SKU436: + - WAREHOUSE0 + SKU437: + - WAREHOUSE0 + SKU438: + - WAREHOUSE0 + SKU439: + - WAREHOUSE0 + SKU44: + - WAREHOUSE0 + SKU440: + - WAREHOUSE0 + SKU441: + - WAREHOUSE0 + SKU442: + - WAREHOUSE0 + SKU443: + - WAREHOUSE0 + SKU444: + - WAREHOUSE0 + SKU445: + - WAREHOUSE0 + SKU446: + - WAREHOUSE0 + SKU447: + - WAREHOUSE0 + SKU448: + - WAREHOUSE0 + SKU449: + - WAREHOUSE0 + SKU45: + - WAREHOUSE0 + SKU450: + - WAREHOUSE0 + SKU451: + - WAREHOUSE0 + SKU452: + - WAREHOUSE0 + SKU453: + - WAREHOUSE0 + SKU454: + - WAREHOUSE0 + SKU455: + - WAREHOUSE0 + SKU456: + - WAREHOUSE0 + SKU457: + - WAREHOUSE0 + SKU458: + - WAREHOUSE0 + SKU459: + - WAREHOUSE0 + SKU46: + - WAREHOUSE0 + SKU460: + - WAREHOUSE0 + SKU461: + - WAREHOUSE0 + SKU462: + - WAREHOUSE0 + SKU463: + - WAREHOUSE0 + SKU464: + - WAREHOUSE0 + SKU465: + - WAREHOUSE0 + SKU466: + - WAREHOUSE0 + SKU467: + - WAREHOUSE0 + SKU468: + - WAREHOUSE0 + SKU469: + - WAREHOUSE0 + SKU47: + - WAREHOUSE0 + SKU470: + - WAREHOUSE0 + SKU471: + - WAREHOUSE0 + SKU472: + - WAREHOUSE0 + SKU473: + - WAREHOUSE0 + SKU474: + - WAREHOUSE0 + SKU475: + - WAREHOUSE0 + SKU476: + - WAREHOUSE0 + SKU477: + - WAREHOUSE0 + SKU478: + - WAREHOUSE0 + SKU479: + - WAREHOUSE0 + SKU48: + - WAREHOUSE0 + SKU480: + - WAREHOUSE0 + SKU481: + - WAREHOUSE0 + SKU482: + - WAREHOUSE0 + SKU483: + - WAREHOUSE0 + SKU484: + - WAREHOUSE0 + SKU485: + - WAREHOUSE0 + SKU486: + - WAREHOUSE0 + SKU487: + - WAREHOUSE0 + SKU488: + - WAREHOUSE0 + SKU489: + - WAREHOUSE0 + SKU49: + - WAREHOUSE0 + SKU490: + - WAREHOUSE0 + SKU491: + - WAREHOUSE0 + SKU492: + - WAREHOUSE0 + SKU493: + - WAREHOUSE0 + SKU494: + - WAREHOUSE0 + SKU495: + - WAREHOUSE0 + SKU496: + - WAREHOUSE0 + SKU497: + - WAREHOUSE0 + SKU498: + - WAREHOUSE0 + SKU499: + - WAREHOUSE0 + SKU5: + - WAREHOUSE0 + SKU50: + - WAREHOUSE0 + SKU500: + - WAREHOUSE0 + SKU501: + - WAREHOUSE0 + SKU502: + - WAREHOUSE0 + SKU503: + - WAREHOUSE0 + SKU504: + - WAREHOUSE0 + SKU505: + - WAREHOUSE0 + SKU506: + - WAREHOUSE0 + SKU507: + - WAREHOUSE0 + SKU508: + - WAREHOUSE0 + SKU509: + - WAREHOUSE0 + SKU51: + - WAREHOUSE0 + SKU510: + - WAREHOUSE0 + SKU511: + - WAREHOUSE0 + SKU512: + - WAREHOUSE0 + SKU513: + - WAREHOUSE0 + SKU514: + - WAREHOUSE0 + SKU515: + - WAREHOUSE0 + SKU516: + - WAREHOUSE0 + SKU517: + - WAREHOUSE0 + SKU518: + - WAREHOUSE0 + SKU519: + - WAREHOUSE0 + SKU52: + - WAREHOUSE0 + SKU520: + - WAREHOUSE0 + SKU521: + - WAREHOUSE0 + SKU522: + - WAREHOUSE0 + SKU523: + - WAREHOUSE0 + SKU524: + - WAREHOUSE0 + SKU525: + - WAREHOUSE0 + SKU526: + - WAREHOUSE0 + SKU527: + - WAREHOUSE0 + SKU528: + - WAREHOUSE0 + SKU529: + - WAREHOUSE0 + SKU53: + - WAREHOUSE0 + SKU530: + - WAREHOUSE0 + SKU531: + - WAREHOUSE0 + SKU532: + - WAREHOUSE0 + SKU533: + - WAREHOUSE0 + SKU534: + - WAREHOUSE0 + SKU535: + - WAREHOUSE0 + SKU536: + - WAREHOUSE0 + SKU537: + - WAREHOUSE0 + SKU538: + - WAREHOUSE0 + SKU539: + - WAREHOUSE0 + SKU54: + - WAREHOUSE0 + SKU540: + - WAREHOUSE0 + SKU541: + - WAREHOUSE0 + SKU542: + - WAREHOUSE0 + SKU543: + - WAREHOUSE0 + SKU544: + - WAREHOUSE0 + SKU545: + - WAREHOUSE0 + SKU546: + - WAREHOUSE0 + SKU547: + - WAREHOUSE0 + SKU548: + - WAREHOUSE0 + SKU549: + - WAREHOUSE0 + SKU55: + - WAREHOUSE0 + SKU550: + - WAREHOUSE0 + SKU551: + - WAREHOUSE0 + SKU552: + - WAREHOUSE0 + SKU553: + - WAREHOUSE0 + SKU554: + - WAREHOUSE0 + SKU555: + - WAREHOUSE0 + SKU556: + - WAREHOUSE0 + SKU557: + - WAREHOUSE0 + SKU558: + - WAREHOUSE0 + SKU559: + - WAREHOUSE0 + SKU56: + - WAREHOUSE0 + SKU560: + - WAREHOUSE0 + SKU561: + - WAREHOUSE0 + SKU562: + - WAREHOUSE0 + SKU563: + - WAREHOUSE0 + SKU564: + - WAREHOUSE0 + SKU565: + - WAREHOUSE0 + SKU566: + - WAREHOUSE0 + SKU567: + - WAREHOUSE0 + SKU568: + - WAREHOUSE0 + SKU569: + - WAREHOUSE0 + SKU57: + - WAREHOUSE0 + SKU570: + - WAREHOUSE0 + SKU571: + - WAREHOUSE0 + SKU572: + - WAREHOUSE0 + SKU573: + - WAREHOUSE0 + SKU574: + - WAREHOUSE0 + SKU575: + - WAREHOUSE0 + SKU576: + - WAREHOUSE0 + SKU577: + - WAREHOUSE0 + SKU578: + - WAREHOUSE0 + SKU579: + - WAREHOUSE0 + SKU58: + - WAREHOUSE0 + SKU580: + - WAREHOUSE0 + SKU581: + - WAREHOUSE0 + SKU582: + - WAREHOUSE0 + SKU583: + - WAREHOUSE0 + SKU584: + - WAREHOUSE0 + SKU585: + - WAREHOUSE0 + SKU586: + - WAREHOUSE0 + SKU587: + - WAREHOUSE0 + SKU588: + - WAREHOUSE0 + SKU589: + - WAREHOUSE0 + SKU59: + - WAREHOUSE0 + SKU590: + - WAREHOUSE0 + SKU591: + - WAREHOUSE0 + SKU592: + - WAREHOUSE0 + SKU593: + - WAREHOUSE0 + SKU594: + - WAREHOUSE0 + SKU595: + - WAREHOUSE0 + SKU596: + - WAREHOUSE0 + SKU597: + - WAREHOUSE0 + SKU598: + - WAREHOUSE0 + SKU599: + - WAREHOUSE0 + SKU6: + - WAREHOUSE0 + SKU60: + - WAREHOUSE0 + SKU600: + - WAREHOUSE0 + SKU601: + - WAREHOUSE0 + SKU602: + - WAREHOUSE0 + SKU603: + - WAREHOUSE0 + SKU604: + - WAREHOUSE0 + SKU605: + - WAREHOUSE0 + SKU606: + - WAREHOUSE0 + SKU607: + - WAREHOUSE0 + SKU608: + - WAREHOUSE0 + SKU609: + - WAREHOUSE0 + SKU61: + - WAREHOUSE0 + SKU610: + - WAREHOUSE0 + SKU611: + - WAREHOUSE0 + SKU612: + - WAREHOUSE0 + SKU613: + - WAREHOUSE0 + SKU614: + - WAREHOUSE0 + SKU615: + - WAREHOUSE0 + SKU616: + - WAREHOUSE0 + SKU617: + - WAREHOUSE0 + SKU618: + - WAREHOUSE0 + SKU619: + - WAREHOUSE0 + SKU62: + - WAREHOUSE0 + SKU620: + - WAREHOUSE0 + SKU621: + - WAREHOUSE0 + SKU622: + - WAREHOUSE0 + SKU623: + - WAREHOUSE0 + SKU624: + - WAREHOUSE0 + SKU625: + - WAREHOUSE0 + SKU626: + - WAREHOUSE0 + SKU627: + - WAREHOUSE0 + SKU628: + - WAREHOUSE0 + SKU629: + - WAREHOUSE0 + SKU63: + - WAREHOUSE0 + SKU630: + - WAREHOUSE0 + SKU631: + - WAREHOUSE0 + SKU632: + - WAREHOUSE0 + SKU633: + - WAREHOUSE0 + SKU634: + - WAREHOUSE0 + SKU635: + - WAREHOUSE0 + SKU636: + - WAREHOUSE0 + SKU637: + - WAREHOUSE0 + SKU638: + - WAREHOUSE0 + SKU639: + - WAREHOUSE0 + SKU64: + - WAREHOUSE0 + SKU640: + - WAREHOUSE0 + SKU641: + - WAREHOUSE0 + SKU642: + - WAREHOUSE0 + SKU643: + - WAREHOUSE0 + SKU644: + - WAREHOUSE0 + SKU645: + - WAREHOUSE0 + SKU646: + - WAREHOUSE0 + SKU647: + - WAREHOUSE0 + SKU648: + - WAREHOUSE0 + SKU649: + - WAREHOUSE0 + SKU65: + - WAREHOUSE0 + SKU650: + - WAREHOUSE0 + SKU651: + - WAREHOUSE0 + SKU652: + - WAREHOUSE0 + SKU653: + - WAREHOUSE0 + SKU654: + - WAREHOUSE0 + SKU655: + - WAREHOUSE0 + SKU656: + - WAREHOUSE0 + SKU657: + - WAREHOUSE0 + SKU658: + - WAREHOUSE0 + SKU659: + - WAREHOUSE0 + SKU66: + - WAREHOUSE0 + SKU660: + - WAREHOUSE0 + SKU661: + - WAREHOUSE0 + SKU662: + - WAREHOUSE0 + SKU663: + - WAREHOUSE0 + SKU664: + - WAREHOUSE0 + SKU665: + - WAREHOUSE0 + SKU666: + - WAREHOUSE0 + SKU667: + - WAREHOUSE0 + SKU668: + - WAREHOUSE0 + SKU669: + - WAREHOUSE0 + SKU67: + - WAREHOUSE0 + SKU670: + - WAREHOUSE0 + SKU671: + - WAREHOUSE0 + SKU672: + - WAREHOUSE0 + SKU673: + - WAREHOUSE0 + SKU674: + - WAREHOUSE0 + SKU675: + - WAREHOUSE0 + SKU676: + - WAREHOUSE0 + SKU677: + - WAREHOUSE0 + SKU678: + - WAREHOUSE0 + SKU679: + - WAREHOUSE0 + SKU68: + - WAREHOUSE0 + SKU680: + - WAREHOUSE0 + SKU681: + - WAREHOUSE0 + SKU682: + - WAREHOUSE0 + SKU683: + - WAREHOUSE0 + SKU684: + - WAREHOUSE0 + SKU685: + - WAREHOUSE0 + SKU686: + - WAREHOUSE0 + SKU687: + - WAREHOUSE0 + SKU688: + - WAREHOUSE0 + SKU689: + - WAREHOUSE0 + SKU69: + - WAREHOUSE0 + SKU690: + - WAREHOUSE0 + SKU691: + - WAREHOUSE0 + SKU692: + - WAREHOUSE0 + SKU693: + - WAREHOUSE0 + SKU694: + - WAREHOUSE0 + SKU695: + - WAREHOUSE0 + SKU696: + - WAREHOUSE0 + SKU697: + - WAREHOUSE0 + SKU698: + - WAREHOUSE0 + SKU699: + - WAREHOUSE0 + SKU7: + - WAREHOUSE0 + SKU70: + - WAREHOUSE0 + SKU700: + - WAREHOUSE0 + SKU701: + - WAREHOUSE0 + SKU702: + - WAREHOUSE0 + SKU703: + - WAREHOUSE0 + SKU704: + - WAREHOUSE0 + SKU705: + - WAREHOUSE0 + SKU706: + - WAREHOUSE0 + SKU707: + - WAREHOUSE0 + SKU708: + - WAREHOUSE0 + SKU709: + - WAREHOUSE0 + SKU71: + - WAREHOUSE0 + SKU710: + - WAREHOUSE0 + SKU711: + - WAREHOUSE0 + SKU712: + - WAREHOUSE0 + SKU713: + - WAREHOUSE0 + SKU714: + - WAREHOUSE0 + SKU715: + - WAREHOUSE0 + SKU716: + - WAREHOUSE0 + SKU717: + - WAREHOUSE0 + SKU718: + - WAREHOUSE0 + SKU719: + - WAREHOUSE0 + SKU72: + - WAREHOUSE0 + SKU720: + - WAREHOUSE0 + SKU721: + - WAREHOUSE0 + SKU722: + - WAREHOUSE0 + SKU723: + - WAREHOUSE0 + SKU724: + - WAREHOUSE0 + SKU725: + - WAREHOUSE0 + SKU726: + - WAREHOUSE0 + SKU727: + - WAREHOUSE0 + SKU728: + - WAREHOUSE0 + SKU729: + - WAREHOUSE0 + SKU73: + - WAREHOUSE0 + SKU730: + - WAREHOUSE0 + SKU731: + - WAREHOUSE0 + SKU732: + - WAREHOUSE0 + SKU733: + - WAREHOUSE0 + SKU734: + - WAREHOUSE0 + SKU735: + - WAREHOUSE0 + SKU736: + - WAREHOUSE0 + SKU737: + - WAREHOUSE0 + SKU738: + - WAREHOUSE0 + SKU739: + - WAREHOUSE0 + SKU74: + - WAREHOUSE0 + SKU740: + - WAREHOUSE0 + SKU741: + - WAREHOUSE0 + SKU742: + - WAREHOUSE0 + SKU743: + - WAREHOUSE0 + SKU744: + - WAREHOUSE0 + SKU745: + - WAREHOUSE0 + SKU746: + - WAREHOUSE0 + SKU747: + - WAREHOUSE0 + SKU748: + - WAREHOUSE0 + SKU749: + - WAREHOUSE0 + SKU75: + - WAREHOUSE0 + SKU750: + - WAREHOUSE0 + SKU751: + - WAREHOUSE0 + SKU752: + - WAREHOUSE0 + SKU753: + - WAREHOUSE0 + SKU754: + - WAREHOUSE0 + SKU755: + - WAREHOUSE0 + SKU756: + - WAREHOUSE0 + SKU757: + - WAREHOUSE0 + SKU758: + - WAREHOUSE0 + SKU759: + - WAREHOUSE0 + SKU76: + - WAREHOUSE0 + SKU760: + - WAREHOUSE0 + SKU761: + - WAREHOUSE0 + SKU762: + - WAREHOUSE0 + SKU763: + - WAREHOUSE0 + SKU764: + - WAREHOUSE0 + SKU765: + - WAREHOUSE0 + SKU766: + - WAREHOUSE0 + SKU767: + - WAREHOUSE0 + SKU768: + - WAREHOUSE0 + SKU769: + - WAREHOUSE0 + SKU77: + - WAREHOUSE0 + SKU770: + - WAREHOUSE0 + SKU771: + - WAREHOUSE0 + SKU772: + - WAREHOUSE0 + SKU773: + - WAREHOUSE0 + SKU774: + - WAREHOUSE0 + SKU775: + - WAREHOUSE0 + SKU776: + - WAREHOUSE0 + SKU777: + - WAREHOUSE0 + SKU778: + - WAREHOUSE0 + SKU779: + - WAREHOUSE0 + SKU78: + - WAREHOUSE0 + SKU780: + - WAREHOUSE0 + SKU781: + - WAREHOUSE0 + SKU782: + - WAREHOUSE0 + SKU783: + - WAREHOUSE0 + SKU784: + - WAREHOUSE0 + SKU785: + - WAREHOUSE0 + SKU786: + - WAREHOUSE0 + SKU787: + - WAREHOUSE0 + SKU788: + - WAREHOUSE0 + SKU789: + - WAREHOUSE0 + SKU79: + - WAREHOUSE0 + SKU790: + - WAREHOUSE0 + SKU791: + - WAREHOUSE0 + SKU792: + - WAREHOUSE0 + SKU793: + - WAREHOUSE0 + SKU794: + - WAREHOUSE0 + SKU795: + - WAREHOUSE0 + SKU796: + - WAREHOUSE0 + SKU797: + - WAREHOUSE0 + SKU798: + - WAREHOUSE0 + SKU799: + - WAREHOUSE0 + SKU8: + - WAREHOUSE0 + SKU80: + - WAREHOUSE0 + SKU800: + - WAREHOUSE0 + SKU801: + - WAREHOUSE0 + SKU802: + - WAREHOUSE0 + SKU803: + - WAREHOUSE0 + SKU804: + - WAREHOUSE0 + SKU805: + - WAREHOUSE0 + SKU806: + - WAREHOUSE0 + SKU807: + - WAREHOUSE0 + SKU808: + - WAREHOUSE0 + SKU809: + - WAREHOUSE0 + SKU81: + - WAREHOUSE0 + SKU810: + - WAREHOUSE0 + SKU811: + - WAREHOUSE0 + SKU812: + - WAREHOUSE0 + SKU813: + - WAREHOUSE0 + SKU814: + - WAREHOUSE0 + SKU815: + - WAREHOUSE0 + SKU816: + - WAREHOUSE0 + SKU817: + - WAREHOUSE0 + SKU818: + - WAREHOUSE0 + SKU819: + - WAREHOUSE0 + SKU82: + - WAREHOUSE0 + SKU820: + - WAREHOUSE0 + SKU821: + - WAREHOUSE0 + SKU822: + - WAREHOUSE0 + SKU823: + - WAREHOUSE0 + SKU824: + - WAREHOUSE0 + SKU825: + - WAREHOUSE0 + SKU826: + - WAREHOUSE0 + SKU827: + - WAREHOUSE0 + SKU828: + - WAREHOUSE0 + SKU829: + - WAREHOUSE0 + SKU83: + - WAREHOUSE0 + SKU830: + - WAREHOUSE0 + SKU831: + - WAREHOUSE0 + SKU832: + - WAREHOUSE0 + SKU833: + - WAREHOUSE0 + SKU834: + - WAREHOUSE0 + SKU835: + - WAREHOUSE0 + SKU836: + - WAREHOUSE0 + SKU837: + - WAREHOUSE0 + SKU838: + - WAREHOUSE0 + SKU839: + - WAREHOUSE0 + SKU84: + - WAREHOUSE0 + SKU840: + - WAREHOUSE0 + SKU841: + - WAREHOUSE0 + SKU842: + - WAREHOUSE0 + SKU843: + - WAREHOUSE0 + SKU844: + - WAREHOUSE0 + SKU845: + - WAREHOUSE0 + SKU846: + - WAREHOUSE0 + SKU847: + - WAREHOUSE0 + SKU848: + - WAREHOUSE0 + SKU849: + - WAREHOUSE0 + SKU85: + - WAREHOUSE0 + SKU850: + - WAREHOUSE0 + SKU851: + - WAREHOUSE0 + SKU852: + - WAREHOUSE0 + SKU853: + - WAREHOUSE0 + SKU854: + - WAREHOUSE0 + SKU855: + - WAREHOUSE0 + SKU856: + - WAREHOUSE0 + SKU857: + - WAREHOUSE0 + SKU858: + - WAREHOUSE0 + SKU859: + - WAREHOUSE0 + SKU86: + - WAREHOUSE0 + SKU860: + - WAREHOUSE0 + SKU861: + - WAREHOUSE0 + SKU862: + - WAREHOUSE0 + SKU863: + - WAREHOUSE0 + SKU864: + - WAREHOUSE0 + SKU865: + - WAREHOUSE0 + SKU866: + - WAREHOUSE0 + SKU867: + - WAREHOUSE0 + SKU868: + - WAREHOUSE0 + SKU869: + - WAREHOUSE0 + SKU87: + - WAREHOUSE0 + SKU870: + - WAREHOUSE0 + SKU871: + - WAREHOUSE0 + SKU872: + - WAREHOUSE0 + SKU873: + - WAREHOUSE0 + SKU874: + - WAREHOUSE0 + SKU875: + - WAREHOUSE0 + SKU876: + - WAREHOUSE0 + SKU877: + - WAREHOUSE0 + SKU878: + - WAREHOUSE0 + SKU879: + - WAREHOUSE0 + SKU88: + - WAREHOUSE0 + SKU880: + - WAREHOUSE0 + SKU881: + - WAREHOUSE0 + SKU882: + - WAREHOUSE0 + SKU883: + - WAREHOUSE0 + SKU884: + - WAREHOUSE0 + SKU885: + - WAREHOUSE0 + SKU886: + - WAREHOUSE0 + SKU887: + - WAREHOUSE0 + SKU888: + - WAREHOUSE0 + SKU889: + - WAREHOUSE0 + SKU89: + - WAREHOUSE0 + SKU890: + - WAREHOUSE0 + SKU891: + - WAREHOUSE0 + SKU892: + - WAREHOUSE0 + SKU893: + - WAREHOUSE0 + SKU894: + - WAREHOUSE0 + SKU895: + - WAREHOUSE0 + SKU896: + - WAREHOUSE0 + SKU897: + - WAREHOUSE0 + SKU898: + - WAREHOUSE0 + SKU899: + - WAREHOUSE0 + SKU9: + - WAREHOUSE0 + SKU90: + - WAREHOUSE0 + SKU900: + - WAREHOUSE0 + SKU901: + - WAREHOUSE0 + SKU902: + - WAREHOUSE0 + SKU903: + - WAREHOUSE0 + SKU904: + - WAREHOUSE0 + SKU905: + - WAREHOUSE0 + SKU906: + - WAREHOUSE0 + SKU907: + - WAREHOUSE0 + SKU908: + - WAREHOUSE0 + SKU909: + - WAREHOUSE0 + SKU91: + - WAREHOUSE0 + SKU910: + - WAREHOUSE0 + SKU911: + - WAREHOUSE0 + SKU912: + - WAREHOUSE0 + SKU913: + - WAREHOUSE0 + SKU914: + - WAREHOUSE0 + SKU915: + - WAREHOUSE0 + SKU916: + - WAREHOUSE0 + SKU917: + - WAREHOUSE0 + SKU918: + - WAREHOUSE0 + SKU919: + - WAREHOUSE0 + SKU92: + - WAREHOUSE0 + SKU920: + - WAREHOUSE0 + SKU921: + - WAREHOUSE0 + SKU922: + - WAREHOUSE0 + SKU923: + - WAREHOUSE0 + SKU924: + - WAREHOUSE0 + SKU925: + - WAREHOUSE0 + SKU926: + - WAREHOUSE0 + SKU927: + - WAREHOUSE0 + SKU928: + - WAREHOUSE0 + SKU929: + - WAREHOUSE0 + SKU93: + - WAREHOUSE0 + SKU930: + - WAREHOUSE0 + SKU931: + - WAREHOUSE0 + SKU932: + - WAREHOUSE0 + SKU933: + - WAREHOUSE0 + SKU934: + - WAREHOUSE0 + SKU935: + - WAREHOUSE0 + SKU936: + - WAREHOUSE0 + SKU937: + - WAREHOUSE0 + SKU938: + - WAREHOUSE0 + SKU939: + - WAREHOUSE0 + SKU94: + - WAREHOUSE0 + SKU940: + - WAREHOUSE0 + SKU941: + - WAREHOUSE0 + SKU942: + - WAREHOUSE0 + SKU943: + - WAREHOUSE0 + SKU944: + - WAREHOUSE0 + SKU945: + - WAREHOUSE0 + SKU946: + - WAREHOUSE0 + SKU947: + - WAREHOUSE0 + SKU948: + - WAREHOUSE0 + SKU949: + - WAREHOUSE0 + SKU95: + - WAREHOUSE0 + SKU950: + - WAREHOUSE0 + SKU951: + - WAREHOUSE0 + SKU952: + - WAREHOUSE0 + SKU953: + - WAREHOUSE0 + SKU954: + - WAREHOUSE0 + SKU955: + - WAREHOUSE0 + SKU956: + - WAREHOUSE0 + SKU957: + - WAREHOUSE0 + SKU958: + - WAREHOUSE0 + SKU959: + - WAREHOUSE0 + SKU96: + - WAREHOUSE0 + SKU960: + - WAREHOUSE0 + SKU961: + - WAREHOUSE0 + SKU962: + - WAREHOUSE0 + SKU963: + - WAREHOUSE0 + SKU964: + - WAREHOUSE0 + SKU965: + - WAREHOUSE0 + SKU966: + - WAREHOUSE0 + SKU967: + - WAREHOUSE0 + SKU968: + - WAREHOUSE0 + SKU969: + - WAREHOUSE0 + SKU97: + - WAREHOUSE0 + SKU970: + - WAREHOUSE0 + SKU971: + - WAREHOUSE0 + SKU972: + - WAREHOUSE0 + SKU973: + - WAREHOUSE0 + SKU974: + - WAREHOUSE0 + SKU975: + - WAREHOUSE0 + SKU976: + - WAREHOUSE0 + SKU977: + - WAREHOUSE0 + SKU978: + - WAREHOUSE0 + SKU979: + - WAREHOUSE0 + SKU98: + - WAREHOUSE0 + SKU980: + - WAREHOUSE0 + SKU981: + - WAREHOUSE0 + SKU982: + - WAREHOUSE0 + SKU983: + - WAREHOUSE0 + SKU984: + - WAREHOUSE0 + SKU985: + - WAREHOUSE0 + SKU986: + - WAREHOUSE0 + SKU987: + - WAREHOUSE0 + SKU988: + - WAREHOUSE0 + SKU989: + - WAREHOUSE0 + SKU99: + - WAREHOUSE0 + SKU990: + - WAREHOUSE0 + SKU991: + - WAREHOUSE0 + SKU992: + - WAREHOUSE0 + SKU993: + - WAREHOUSE0 + SKU994: + - WAREHOUSE0 + SKU995: + - WAREHOUSE0 + SKU996: + - WAREHOUSE0 + SKU997: + - WAREHOUSE0 + SKU998: + - WAREHOUSE0 + SKU999: + - WAREHOUSE0 + WAREHOUSE0: + SKU0: + - SUPPLIER0 + SKU1: + - SUPPLIER0 + SKU10: + - SUPPLIER0 + SKU100: + - SUPPLIER0 + SKU101: + - SUPPLIER0 + SKU102: + - SUPPLIER0 + SKU103: + - SUPPLIER0 + SKU104: + - SUPPLIER0 + SKU105: + - SUPPLIER0 + SKU106: + - SUPPLIER0 + SKU107: + - SUPPLIER0 + SKU108: + - SUPPLIER0 + SKU109: + - SUPPLIER0 + SKU11: + - SUPPLIER0 + SKU110: + - SUPPLIER0 + SKU111: + - SUPPLIER0 + SKU112: + - SUPPLIER0 + SKU113: + - SUPPLIER0 + SKU114: + - SUPPLIER0 + SKU115: + - SUPPLIER0 + SKU116: + - SUPPLIER0 + SKU117: + - SUPPLIER0 + SKU118: + - SUPPLIER0 + SKU119: + - SUPPLIER0 + SKU12: + - SUPPLIER0 + SKU120: + - SUPPLIER0 + SKU121: + - SUPPLIER0 + SKU122: + - SUPPLIER0 + SKU123: + - SUPPLIER0 + SKU124: + - SUPPLIER0 + SKU125: + - SUPPLIER0 + SKU126: + - SUPPLIER0 + SKU127: + - SUPPLIER0 + SKU128: + - SUPPLIER0 + SKU129: + - SUPPLIER0 + SKU13: + - SUPPLIER0 + SKU130: + - SUPPLIER0 + SKU131: + - SUPPLIER0 + SKU132: + - SUPPLIER0 + SKU133: + - SUPPLIER0 + SKU134: + - SUPPLIER0 + SKU135: + - SUPPLIER0 + SKU136: + - SUPPLIER0 + SKU137: + - SUPPLIER0 + SKU138: + - SUPPLIER0 + SKU139: + - SUPPLIER0 + SKU14: + - SUPPLIER0 + SKU140: + - SUPPLIER0 + SKU141: + - SUPPLIER0 + SKU142: + - SUPPLIER0 + SKU143: + - SUPPLIER0 + SKU144: + - SUPPLIER0 + SKU145: + - SUPPLIER0 + SKU146: + - SUPPLIER0 + SKU147: + - SUPPLIER0 + SKU148: + - SUPPLIER0 + SKU149: + - SUPPLIER0 + SKU15: + - SUPPLIER0 + SKU150: + - SUPPLIER0 + SKU151: + - SUPPLIER0 + SKU152: + - SUPPLIER0 + SKU153: + - SUPPLIER0 + SKU154: + - SUPPLIER0 + SKU155: + - SUPPLIER0 + SKU156: + - SUPPLIER0 + SKU157: + - SUPPLIER0 + SKU158: + - SUPPLIER0 + SKU159: + - SUPPLIER0 + SKU16: + - SUPPLIER0 + SKU160: + - SUPPLIER0 + SKU161: + - SUPPLIER0 + SKU162: + - SUPPLIER0 + SKU163: + - SUPPLIER0 + SKU164: + - SUPPLIER0 + SKU165: + - SUPPLIER0 + SKU166: + - SUPPLIER0 + SKU167: + - SUPPLIER0 + SKU168: + - SUPPLIER0 + SKU169: + - SUPPLIER0 + SKU17: + - SUPPLIER0 + SKU170: + - SUPPLIER0 + SKU171: + - SUPPLIER0 + SKU172: + - SUPPLIER0 + SKU173: + - SUPPLIER0 + SKU174: + - SUPPLIER0 + SKU175: + - SUPPLIER0 + SKU176: + - SUPPLIER0 + SKU177: + - SUPPLIER0 + SKU178: + - SUPPLIER0 + SKU179: + - SUPPLIER0 + SKU18: + - SUPPLIER0 + SKU180: + - SUPPLIER0 + SKU181: + - SUPPLIER0 + SKU182: + - SUPPLIER0 + SKU183: + - SUPPLIER0 + SKU184: + - SUPPLIER0 + SKU185: + - SUPPLIER0 + SKU186: + - SUPPLIER0 + SKU187: + - SUPPLIER0 + SKU188: + - SUPPLIER0 + SKU189: + - SUPPLIER0 + SKU19: + - SUPPLIER0 + SKU190: + - SUPPLIER0 + SKU191: + - SUPPLIER0 + SKU192: + - SUPPLIER0 + SKU193: + - SUPPLIER0 + SKU194: + - SUPPLIER0 + SKU195: + - SUPPLIER0 + SKU196: + - SUPPLIER0 + SKU197: + - SUPPLIER0 + SKU198: + - SUPPLIER0 + SKU199: + - SUPPLIER0 + SKU2: + - SUPPLIER0 + SKU20: + - SUPPLIER0 + SKU200: + - SUPPLIER0 + SKU201: + - SUPPLIER0 + SKU202: + - SUPPLIER0 + SKU203: + - SUPPLIER0 + SKU204: + - SUPPLIER0 + SKU205: + - SUPPLIER0 + SKU206: + - SUPPLIER0 + SKU207: + - SUPPLIER0 + SKU208: + - SUPPLIER0 + SKU209: + - SUPPLIER0 + SKU21: + - SUPPLIER0 + SKU210: + - SUPPLIER0 + SKU211: + - SUPPLIER0 + SKU212: + - SUPPLIER0 + SKU213: + - SUPPLIER0 + SKU214: + - SUPPLIER0 + SKU215: + - SUPPLIER0 + SKU216: + - SUPPLIER0 + SKU217: + - SUPPLIER0 + SKU218: + - SUPPLIER0 + SKU219: + - SUPPLIER0 + SKU22: + - SUPPLIER0 + SKU220: + - SUPPLIER0 + SKU221: + - SUPPLIER0 + SKU222: + - SUPPLIER0 + SKU223: + - SUPPLIER0 + SKU224: + - SUPPLIER0 + SKU225: + - SUPPLIER0 + SKU226: + - SUPPLIER0 + SKU227: + - SUPPLIER0 + SKU228: + - SUPPLIER0 + SKU229: + - SUPPLIER0 + SKU23: + - SUPPLIER0 + SKU230: + - SUPPLIER0 + SKU231: + - SUPPLIER0 + SKU232: + - SUPPLIER0 + SKU233: + - SUPPLIER0 + SKU234: + - SUPPLIER0 + SKU235: + - SUPPLIER0 + SKU236: + - SUPPLIER0 + SKU237: + - SUPPLIER0 + SKU238: + - SUPPLIER0 + SKU239: + - SUPPLIER0 + SKU24: + - SUPPLIER0 + SKU240: + - SUPPLIER0 + SKU241: + - SUPPLIER0 + SKU242: + - SUPPLIER0 + SKU243: + - SUPPLIER0 + SKU244: + - SUPPLIER0 + SKU245: + - SUPPLIER0 + SKU246: + - SUPPLIER0 + SKU247: + - SUPPLIER0 + SKU248: + - SUPPLIER0 + SKU249: + - SUPPLIER0 + SKU25: + - SUPPLIER0 + SKU250: + - SUPPLIER0 + SKU251: + - SUPPLIER0 + SKU252: + - SUPPLIER0 + SKU253: + - SUPPLIER0 + SKU254: + - SUPPLIER0 + SKU255: + - SUPPLIER0 + SKU256: + - SUPPLIER0 + SKU257: + - SUPPLIER0 + SKU258: + - SUPPLIER0 + SKU259: + - SUPPLIER0 + SKU26: + - SUPPLIER0 + SKU260: + - SUPPLIER0 + SKU261: + - SUPPLIER0 + SKU262: + - SUPPLIER0 + SKU263: + - SUPPLIER0 + SKU264: + - SUPPLIER0 + SKU265: + - SUPPLIER0 + SKU266: + - SUPPLIER0 + SKU267: + - SUPPLIER0 + SKU268: + - SUPPLIER0 + SKU269: + - SUPPLIER0 + SKU27: + - SUPPLIER0 + SKU270: + - SUPPLIER0 + SKU271: + - SUPPLIER0 + SKU272: + - SUPPLIER0 + SKU273: + - SUPPLIER0 + SKU274: + - SUPPLIER0 + SKU275: + - SUPPLIER0 + SKU276: + - SUPPLIER0 + SKU277: + - SUPPLIER0 + SKU278: + - SUPPLIER0 + SKU279: + - SUPPLIER0 + SKU28: + - SUPPLIER0 + SKU280: + - SUPPLIER0 + SKU281: + - SUPPLIER0 + SKU282: + - SUPPLIER0 + SKU283: + - SUPPLIER0 + SKU284: + - SUPPLIER0 + SKU285: + - SUPPLIER0 + SKU286: + - SUPPLIER0 + SKU287: + - SUPPLIER0 + SKU288: + - SUPPLIER0 + SKU289: + - SUPPLIER0 + SKU29: + - SUPPLIER0 + SKU290: + - SUPPLIER0 + SKU291: + - SUPPLIER0 + SKU292: + - SUPPLIER0 + SKU293: + - SUPPLIER0 + SKU294: + - SUPPLIER0 + SKU295: + - SUPPLIER0 + SKU296: + - SUPPLIER0 + SKU297: + - SUPPLIER0 + SKU298: + - SUPPLIER0 + SKU299: + - SUPPLIER0 + SKU3: + - SUPPLIER0 + SKU30: + - SUPPLIER0 + SKU300: + - SUPPLIER0 + SKU301: + - SUPPLIER0 + SKU302: + - SUPPLIER0 + SKU303: + - SUPPLIER0 + SKU304: + - SUPPLIER0 + SKU305: + - SUPPLIER0 + SKU306: + - SUPPLIER0 + SKU307: + - SUPPLIER0 + SKU308: + - SUPPLIER0 + SKU309: + - SUPPLIER0 + SKU31: + - SUPPLIER0 + SKU310: + - SUPPLIER0 + SKU311: + - SUPPLIER0 + SKU312: + - SUPPLIER0 + SKU313: + - SUPPLIER0 + SKU314: + - SUPPLIER0 + SKU315: + - SUPPLIER0 + SKU316: + - SUPPLIER0 + SKU317: + - SUPPLIER0 + SKU318: + - SUPPLIER0 + SKU319: + - SUPPLIER0 + SKU32: + - SUPPLIER0 + SKU320: + - SUPPLIER0 + SKU321: + - SUPPLIER0 + SKU322: + - SUPPLIER0 + SKU323: + - SUPPLIER0 + SKU324: + - SUPPLIER0 + SKU325: + - SUPPLIER0 + SKU326: + - SUPPLIER0 + SKU327: + - SUPPLIER0 + SKU328: + - SUPPLIER0 + SKU329: + - SUPPLIER0 + SKU33: + - SUPPLIER0 + SKU330: + - SUPPLIER0 + SKU331: + - SUPPLIER0 + SKU332: + - SUPPLIER0 + SKU333: + - SUPPLIER0 + SKU334: + - SUPPLIER0 + SKU335: + - SUPPLIER0 + SKU336: + - SUPPLIER0 + SKU337: + - SUPPLIER0 + SKU338: + - SUPPLIER0 + SKU339: + - SUPPLIER0 + SKU34: + - SUPPLIER0 + SKU340: + - SUPPLIER0 + SKU341: + - SUPPLIER0 + SKU342: + - SUPPLIER0 + SKU343: + - SUPPLIER0 + SKU344: + - SUPPLIER0 + SKU345: + - SUPPLIER0 + SKU346: + - SUPPLIER0 + SKU347: + - SUPPLIER0 + SKU348: + - SUPPLIER0 + SKU349: + - SUPPLIER0 + SKU35: + - SUPPLIER0 + SKU350: + - SUPPLIER0 + SKU351: + - SUPPLIER0 + SKU352: + - SUPPLIER0 + SKU353: + - SUPPLIER0 + SKU354: + - SUPPLIER0 + SKU355: + - SUPPLIER0 + SKU356: + - SUPPLIER0 + SKU357: + - SUPPLIER0 + SKU358: + - SUPPLIER0 + SKU359: + - SUPPLIER0 + SKU36: + - SUPPLIER0 + SKU360: + - SUPPLIER0 + SKU361: + - SUPPLIER0 + SKU362: + - SUPPLIER0 + SKU363: + - SUPPLIER0 + SKU364: + - SUPPLIER0 + SKU365: + - SUPPLIER0 + SKU366: + - SUPPLIER0 + SKU367: + - SUPPLIER0 + SKU368: + - SUPPLIER0 + SKU369: + - SUPPLIER0 + SKU37: + - SUPPLIER0 + SKU370: + - SUPPLIER0 + SKU371: + - SUPPLIER0 + SKU372: + - SUPPLIER0 + SKU373: + - SUPPLIER0 + SKU374: + - SUPPLIER0 + SKU375: + - SUPPLIER0 + SKU376: + - SUPPLIER0 + SKU377: + - SUPPLIER0 + SKU378: + - SUPPLIER0 + SKU379: + - SUPPLIER0 + SKU38: + - SUPPLIER0 + SKU380: + - SUPPLIER0 + SKU381: + - SUPPLIER0 + SKU382: + - SUPPLIER0 + SKU383: + - SUPPLIER0 + SKU384: + - SUPPLIER0 + SKU385: + - SUPPLIER0 + SKU386: + - SUPPLIER0 + SKU387: + - SUPPLIER0 + SKU388: + - SUPPLIER0 + SKU389: + - SUPPLIER0 + SKU39: + - SUPPLIER0 + SKU390: + - SUPPLIER0 + SKU391: + - SUPPLIER0 + SKU392: + - SUPPLIER0 + SKU393: + - SUPPLIER0 + SKU394: + - SUPPLIER0 + SKU395: + - SUPPLIER0 + SKU396: + - SUPPLIER0 + SKU397: + - SUPPLIER0 + SKU398: + - SUPPLIER0 + SKU399: + - SUPPLIER0 + SKU4: + - SUPPLIER0 + SKU40: + - SUPPLIER0 + SKU400: + - SUPPLIER0 + SKU401: + - SUPPLIER0 + SKU402: + - SUPPLIER0 + SKU403: + - SUPPLIER0 + SKU404: + - SUPPLIER0 + SKU405: + - SUPPLIER0 + SKU406: + - SUPPLIER0 + SKU407: + - SUPPLIER0 + SKU408: + - SUPPLIER0 + SKU409: + - SUPPLIER0 + SKU41: + - SUPPLIER0 + SKU410: + - SUPPLIER0 + SKU411: + - SUPPLIER0 + SKU412: + - SUPPLIER0 + SKU413: + - SUPPLIER0 + SKU414: + - SUPPLIER0 + SKU415: + - SUPPLIER0 + SKU416: + - SUPPLIER0 + SKU417: + - SUPPLIER0 + SKU418: + - SUPPLIER0 + SKU419: + - SUPPLIER0 + SKU42: + - SUPPLIER0 + SKU420: + - SUPPLIER0 + SKU421: + - SUPPLIER0 + SKU422: + - SUPPLIER0 + SKU423: + - SUPPLIER0 + SKU424: + - SUPPLIER0 + SKU425: + - SUPPLIER0 + SKU426: + - SUPPLIER0 + SKU427: + - SUPPLIER0 + SKU428: + - SUPPLIER0 + SKU429: + - SUPPLIER0 + SKU43: + - SUPPLIER0 + SKU430: + - SUPPLIER0 + SKU431: + - SUPPLIER0 + SKU432: + - SUPPLIER0 + SKU433: + - SUPPLIER0 + SKU434: + - SUPPLIER0 + SKU435: + - SUPPLIER0 + SKU436: + - SUPPLIER0 + SKU437: + - SUPPLIER0 + SKU438: + - SUPPLIER0 + SKU439: + - SUPPLIER0 + SKU44: + - SUPPLIER0 + SKU440: + - SUPPLIER0 + SKU441: + - SUPPLIER0 + SKU442: + - SUPPLIER0 + SKU443: + - SUPPLIER0 + SKU444: + - SUPPLIER0 + SKU445: + - SUPPLIER0 + SKU446: + - SUPPLIER0 + SKU447: + - SUPPLIER0 + SKU448: + - SUPPLIER0 + SKU449: + - SUPPLIER0 + SKU45: + - SUPPLIER0 + SKU450: + - SUPPLIER0 + SKU451: + - SUPPLIER0 + SKU452: + - SUPPLIER0 + SKU453: + - SUPPLIER0 + SKU454: + - SUPPLIER0 + SKU455: + - SUPPLIER0 + SKU456: + - SUPPLIER0 + SKU457: + - SUPPLIER0 + SKU458: + - SUPPLIER0 + SKU459: + - SUPPLIER0 + SKU46: + - SUPPLIER0 + SKU460: + - SUPPLIER0 + SKU461: + - SUPPLIER0 + SKU462: + - SUPPLIER0 + SKU463: + - SUPPLIER0 + SKU464: + - SUPPLIER0 + SKU465: + - SUPPLIER0 + SKU466: + - SUPPLIER0 + SKU467: + - SUPPLIER0 + SKU468: + - SUPPLIER0 + SKU469: + - SUPPLIER0 + SKU47: + - SUPPLIER0 + SKU470: + - SUPPLIER0 + SKU471: + - SUPPLIER0 + SKU472: + - SUPPLIER0 + SKU473: + - SUPPLIER0 + SKU474: + - SUPPLIER0 + SKU475: + - SUPPLIER0 + SKU476: + - SUPPLIER0 + SKU477: + - SUPPLIER0 + SKU478: + - SUPPLIER0 + SKU479: + - SUPPLIER0 + SKU48: + - SUPPLIER0 + SKU480: + - SUPPLIER0 + SKU481: + - SUPPLIER0 + SKU482: + - SUPPLIER0 + SKU483: + - SUPPLIER0 + SKU484: + - SUPPLIER0 + SKU485: + - SUPPLIER0 + SKU486: + - SUPPLIER0 + SKU487: + - SUPPLIER0 + SKU488: + - SUPPLIER0 + SKU489: + - SUPPLIER0 + SKU49: + - SUPPLIER0 + SKU490: + - SUPPLIER0 + SKU491: + - SUPPLIER0 + SKU492: + - SUPPLIER0 + SKU493: + - SUPPLIER0 + SKU494: + - SUPPLIER0 + SKU495: + - SUPPLIER0 + SKU496: + - SUPPLIER0 + SKU497: + - SUPPLIER0 + SKU498: + - SUPPLIER0 + SKU499: + - SUPPLIER0 + SKU5: + - SUPPLIER0 + SKU50: + - SUPPLIER0 + SKU500: + - SUPPLIER0 + SKU501: + - SUPPLIER0 + SKU502: + - SUPPLIER0 + SKU503: + - SUPPLIER0 + SKU504: + - SUPPLIER0 + SKU505: + - SUPPLIER0 + SKU506: + - SUPPLIER0 + SKU507: + - SUPPLIER0 + SKU508: + - SUPPLIER0 + SKU509: + - SUPPLIER0 + SKU51: + - SUPPLIER0 + SKU510: + - SUPPLIER0 + SKU511: + - SUPPLIER0 + SKU512: + - SUPPLIER0 + SKU513: + - SUPPLIER0 + SKU514: + - SUPPLIER0 + SKU515: + - SUPPLIER0 + SKU516: + - SUPPLIER0 + SKU517: + - SUPPLIER0 + SKU518: + - SUPPLIER0 + SKU519: + - SUPPLIER0 + SKU52: + - SUPPLIER0 + SKU520: + - SUPPLIER0 + SKU521: + - SUPPLIER0 + SKU522: + - SUPPLIER0 + SKU523: + - SUPPLIER0 + SKU524: + - SUPPLIER0 + SKU525: + - SUPPLIER0 + SKU526: + - SUPPLIER0 + SKU527: + - SUPPLIER0 + SKU528: + - SUPPLIER0 + SKU529: + - SUPPLIER0 + SKU53: + - SUPPLIER0 + SKU530: + - SUPPLIER0 + SKU531: + - SUPPLIER0 + SKU532: + - SUPPLIER0 + SKU533: + - SUPPLIER0 + SKU534: + - SUPPLIER0 + SKU535: + - SUPPLIER0 + SKU536: + - SUPPLIER0 + SKU537: + - SUPPLIER0 + SKU538: + - SUPPLIER0 + SKU539: + - SUPPLIER0 + SKU54: + - SUPPLIER0 + SKU540: + - SUPPLIER0 + SKU541: + - SUPPLIER0 + SKU542: + - SUPPLIER0 + SKU543: + - SUPPLIER0 + SKU544: + - SUPPLIER0 + SKU545: + - SUPPLIER0 + SKU546: + - SUPPLIER0 + SKU547: + - SUPPLIER0 + SKU548: + - SUPPLIER0 + SKU549: + - SUPPLIER0 + SKU55: + - SUPPLIER0 + SKU550: + - SUPPLIER0 + SKU551: + - SUPPLIER0 + SKU552: + - SUPPLIER0 + SKU553: + - SUPPLIER0 + SKU554: + - SUPPLIER0 + SKU555: + - SUPPLIER0 + SKU556: + - SUPPLIER0 + SKU557: + - SUPPLIER0 + SKU558: + - SUPPLIER0 + SKU559: + - SUPPLIER0 + SKU56: + - SUPPLIER0 + SKU560: + - SUPPLIER0 + SKU561: + - SUPPLIER0 + SKU562: + - SUPPLIER0 + SKU563: + - SUPPLIER0 + SKU564: + - SUPPLIER0 + SKU565: + - SUPPLIER0 + SKU566: + - SUPPLIER0 + SKU567: + - SUPPLIER0 + SKU568: + - SUPPLIER0 + SKU569: + - SUPPLIER0 + SKU57: + - SUPPLIER0 + SKU570: + - SUPPLIER0 + SKU571: + - SUPPLIER0 + SKU572: + - SUPPLIER0 + SKU573: + - SUPPLIER0 + SKU574: + - SUPPLIER0 + SKU575: + - SUPPLIER0 + SKU576: + - SUPPLIER0 + SKU577: + - SUPPLIER0 + SKU578: + - SUPPLIER0 + SKU579: + - SUPPLIER0 + SKU58: + - SUPPLIER0 + SKU580: + - SUPPLIER0 + SKU581: + - SUPPLIER0 + SKU582: + - SUPPLIER0 + SKU583: + - SUPPLIER0 + SKU584: + - SUPPLIER0 + SKU585: + - SUPPLIER0 + SKU586: + - SUPPLIER0 + SKU587: + - SUPPLIER0 + SKU588: + - SUPPLIER0 + SKU589: + - SUPPLIER0 + SKU59: + - SUPPLIER0 + SKU590: + - SUPPLIER0 + SKU591: + - SUPPLIER0 + SKU592: + - SUPPLIER0 + SKU593: + - SUPPLIER0 + SKU594: + - SUPPLIER0 + SKU595: + - SUPPLIER0 + SKU596: + - SUPPLIER0 + SKU597: + - SUPPLIER0 + SKU598: + - SUPPLIER0 + SKU599: + - SUPPLIER0 + SKU6: + - SUPPLIER0 + SKU60: + - SUPPLIER0 + SKU600: + - SUPPLIER0 + SKU601: + - SUPPLIER0 + SKU602: + - SUPPLIER0 + SKU603: + - SUPPLIER0 + SKU604: + - SUPPLIER0 + SKU605: + - SUPPLIER0 + SKU606: + - SUPPLIER0 + SKU607: + - SUPPLIER0 + SKU608: + - SUPPLIER0 + SKU609: + - SUPPLIER0 + SKU61: + - SUPPLIER0 + SKU610: + - SUPPLIER0 + SKU611: + - SUPPLIER0 + SKU612: + - SUPPLIER0 + SKU613: + - SUPPLIER0 + SKU614: + - SUPPLIER0 + SKU615: + - SUPPLIER0 + SKU616: + - SUPPLIER0 + SKU617: + - SUPPLIER0 + SKU618: + - SUPPLIER0 + SKU619: + - SUPPLIER0 + SKU62: + - SUPPLIER0 + SKU620: + - SUPPLIER0 + SKU621: + - SUPPLIER0 + SKU622: + - SUPPLIER0 + SKU623: + - SUPPLIER0 + SKU624: + - SUPPLIER0 + SKU625: + - SUPPLIER0 + SKU626: + - SUPPLIER0 + SKU627: + - SUPPLIER0 + SKU628: + - SUPPLIER0 + SKU629: + - SUPPLIER0 + SKU63: + - SUPPLIER0 + SKU630: + - SUPPLIER0 + SKU631: + - SUPPLIER0 + SKU632: + - SUPPLIER0 + SKU633: + - SUPPLIER0 + SKU634: + - SUPPLIER0 + SKU635: + - SUPPLIER0 + SKU636: + - SUPPLIER0 + SKU637: + - SUPPLIER0 + SKU638: + - SUPPLIER0 + SKU639: + - SUPPLIER0 + SKU64: + - SUPPLIER0 + SKU640: + - SUPPLIER0 + SKU641: + - SUPPLIER0 + SKU642: + - SUPPLIER0 + SKU643: + - SUPPLIER0 + SKU644: + - SUPPLIER0 + SKU645: + - SUPPLIER0 + SKU646: + - SUPPLIER0 + SKU647: + - SUPPLIER0 + SKU648: + - SUPPLIER0 + SKU649: + - SUPPLIER0 + SKU65: + - SUPPLIER0 + SKU650: + - SUPPLIER0 + SKU651: + - SUPPLIER0 + SKU652: + - SUPPLIER0 + SKU653: + - SUPPLIER0 + SKU654: + - SUPPLIER0 + SKU655: + - SUPPLIER0 + SKU656: + - SUPPLIER0 + SKU657: + - SUPPLIER0 + SKU658: + - SUPPLIER0 + SKU659: + - SUPPLIER0 + SKU66: + - SUPPLIER0 + SKU660: + - SUPPLIER0 + SKU661: + - SUPPLIER0 + SKU662: + - SUPPLIER0 + SKU663: + - SUPPLIER0 + SKU664: + - SUPPLIER0 + SKU665: + - SUPPLIER0 + SKU666: + - SUPPLIER0 + SKU667: + - SUPPLIER0 + SKU668: + - SUPPLIER0 + SKU669: + - SUPPLIER0 + SKU67: + - SUPPLIER0 + SKU670: + - SUPPLIER0 + SKU671: + - SUPPLIER0 + SKU672: + - SUPPLIER0 + SKU673: + - SUPPLIER0 + SKU674: + - SUPPLIER0 + SKU675: + - SUPPLIER0 + SKU676: + - SUPPLIER0 + SKU677: + - SUPPLIER0 + SKU678: + - SUPPLIER0 + SKU679: + - SUPPLIER0 + SKU68: + - SUPPLIER0 + SKU680: + - SUPPLIER0 + SKU681: + - SUPPLIER0 + SKU682: + - SUPPLIER0 + SKU683: + - SUPPLIER0 + SKU684: + - SUPPLIER0 + SKU685: + - SUPPLIER0 + SKU686: + - SUPPLIER0 + SKU687: + - SUPPLIER0 + SKU688: + - SUPPLIER0 + SKU689: + - SUPPLIER0 + SKU69: + - SUPPLIER0 + SKU690: + - SUPPLIER0 + SKU691: + - SUPPLIER0 + SKU692: + - SUPPLIER0 + SKU693: + - SUPPLIER0 + SKU694: + - SUPPLIER0 + SKU695: + - SUPPLIER0 + SKU696: + - SUPPLIER0 + SKU697: + - SUPPLIER0 + SKU698: + - SUPPLIER0 + SKU699: + - SUPPLIER0 + SKU7: + - SUPPLIER0 + SKU70: + - SUPPLIER0 + SKU700: + - SUPPLIER0 + SKU701: + - SUPPLIER0 + SKU702: + - SUPPLIER0 + SKU703: + - SUPPLIER0 + SKU704: + - SUPPLIER0 + SKU705: + - SUPPLIER0 + SKU706: + - SUPPLIER0 + SKU707: + - SUPPLIER0 + SKU708: + - SUPPLIER0 + SKU709: + - SUPPLIER0 + SKU71: + - SUPPLIER0 + SKU710: + - SUPPLIER0 + SKU711: + - SUPPLIER0 + SKU712: + - SUPPLIER0 + SKU713: + - SUPPLIER0 + SKU714: + - SUPPLIER0 + SKU715: + - SUPPLIER0 + SKU716: + - SUPPLIER0 + SKU717: + - SUPPLIER0 + SKU718: + - SUPPLIER0 + SKU719: + - SUPPLIER0 + SKU72: + - SUPPLIER0 + SKU720: + - SUPPLIER0 + SKU721: + - SUPPLIER0 + SKU722: + - SUPPLIER0 + SKU723: + - SUPPLIER0 + SKU724: + - SUPPLIER0 + SKU725: + - SUPPLIER0 + SKU726: + - SUPPLIER0 + SKU727: + - SUPPLIER0 + SKU728: + - SUPPLIER0 + SKU729: + - SUPPLIER0 + SKU73: + - SUPPLIER0 + SKU730: + - SUPPLIER0 + SKU731: + - SUPPLIER0 + SKU732: + - SUPPLIER0 + SKU733: + - SUPPLIER0 + SKU734: + - SUPPLIER0 + SKU735: + - SUPPLIER0 + SKU736: + - SUPPLIER0 + SKU737: + - SUPPLIER0 + SKU738: + - SUPPLIER0 + SKU739: + - SUPPLIER0 + SKU74: + - SUPPLIER0 + SKU740: + - SUPPLIER0 + SKU741: + - SUPPLIER0 + SKU742: + - SUPPLIER0 + SKU743: + - SUPPLIER0 + SKU744: + - SUPPLIER0 + SKU745: + - SUPPLIER0 + SKU746: + - SUPPLIER0 + SKU747: + - SUPPLIER0 + SKU748: + - SUPPLIER0 + SKU749: + - SUPPLIER0 + SKU75: + - SUPPLIER0 + SKU750: + - SUPPLIER0 + SKU751: + - SUPPLIER0 + SKU752: + - SUPPLIER0 + SKU753: + - SUPPLIER0 + SKU754: + - SUPPLIER0 + SKU755: + - SUPPLIER0 + SKU756: + - SUPPLIER0 + SKU757: + - SUPPLIER0 + SKU758: + - SUPPLIER0 + SKU759: + - SUPPLIER0 + SKU76: + - SUPPLIER0 + SKU760: + - SUPPLIER0 + SKU761: + - SUPPLIER0 + SKU762: + - SUPPLIER0 + SKU763: + - SUPPLIER0 + SKU764: + - SUPPLIER0 + SKU765: + - SUPPLIER0 + SKU766: + - SUPPLIER0 + SKU767: + - SUPPLIER0 + SKU768: + - SUPPLIER0 + SKU769: + - SUPPLIER0 + SKU77: + - SUPPLIER0 + SKU770: + - SUPPLIER0 + SKU771: + - SUPPLIER0 + SKU772: + - SUPPLIER0 + SKU773: + - SUPPLIER0 + SKU774: + - SUPPLIER0 + SKU775: + - SUPPLIER0 + SKU776: + - SUPPLIER0 + SKU777: + - SUPPLIER0 + SKU778: + - SUPPLIER0 + SKU779: + - SUPPLIER0 + SKU78: + - SUPPLIER0 + SKU780: + - SUPPLIER0 + SKU781: + - SUPPLIER0 + SKU782: + - SUPPLIER0 + SKU783: + - SUPPLIER0 + SKU784: + - SUPPLIER0 + SKU785: + - SUPPLIER0 + SKU786: + - SUPPLIER0 + SKU787: + - SUPPLIER0 + SKU788: + - SUPPLIER0 + SKU789: + - SUPPLIER0 + SKU79: + - SUPPLIER0 + SKU790: + - SUPPLIER0 + SKU791: + - SUPPLIER0 + SKU792: + - SUPPLIER0 + SKU793: + - SUPPLIER0 + SKU794: + - SUPPLIER0 + SKU795: + - SUPPLIER0 + SKU796: + - SUPPLIER0 + SKU797: + - SUPPLIER0 + SKU798: + - SUPPLIER0 + SKU799: + - SUPPLIER0 + SKU8: + - SUPPLIER0 + SKU80: + - SUPPLIER0 + SKU800: + - SUPPLIER0 + SKU801: + - SUPPLIER0 + SKU802: + - SUPPLIER0 + SKU803: + - SUPPLIER0 + SKU804: + - SUPPLIER0 + SKU805: + - SUPPLIER0 + SKU806: + - SUPPLIER0 + SKU807: + - SUPPLIER0 + SKU808: + - SUPPLIER0 + SKU809: + - SUPPLIER0 + SKU81: + - SUPPLIER0 + SKU810: + - SUPPLIER0 + SKU811: + - SUPPLIER0 + SKU812: + - SUPPLIER0 + SKU813: + - SUPPLIER0 + SKU814: + - SUPPLIER0 + SKU815: + - SUPPLIER0 + SKU816: + - SUPPLIER0 + SKU817: + - SUPPLIER0 + SKU818: + - SUPPLIER0 + SKU819: + - SUPPLIER0 + SKU82: + - SUPPLIER0 + SKU820: + - SUPPLIER0 + SKU821: + - SUPPLIER0 + SKU822: + - SUPPLIER0 + SKU823: + - SUPPLIER0 + SKU824: + - SUPPLIER0 + SKU825: + - SUPPLIER0 + SKU826: + - SUPPLIER0 + SKU827: + - SUPPLIER0 + SKU828: + - SUPPLIER0 + SKU829: + - SUPPLIER0 + SKU83: + - SUPPLIER0 + SKU830: + - SUPPLIER0 + SKU831: + - SUPPLIER0 + SKU832: + - SUPPLIER0 + SKU833: + - SUPPLIER0 + SKU834: + - SUPPLIER0 + SKU835: + - SUPPLIER0 + SKU836: + - SUPPLIER0 + SKU837: + - SUPPLIER0 + SKU838: + - SUPPLIER0 + SKU839: + - SUPPLIER0 + SKU84: + - SUPPLIER0 + SKU840: + - SUPPLIER0 + SKU841: + - SUPPLIER0 + SKU842: + - SUPPLIER0 + SKU843: + - SUPPLIER0 + SKU844: + - SUPPLIER0 + SKU845: + - SUPPLIER0 + SKU846: + - SUPPLIER0 + SKU847: + - SUPPLIER0 + SKU848: + - SUPPLIER0 + SKU849: + - SUPPLIER0 + SKU85: + - SUPPLIER0 + SKU850: + - SUPPLIER0 + SKU851: + - SUPPLIER0 + SKU852: + - SUPPLIER0 + SKU853: + - SUPPLIER0 + SKU854: + - SUPPLIER0 + SKU855: + - SUPPLIER0 + SKU856: + - SUPPLIER0 + SKU857: + - SUPPLIER0 + SKU858: + - SUPPLIER0 + SKU859: + - SUPPLIER0 + SKU86: + - SUPPLIER0 + SKU860: + - SUPPLIER0 + SKU861: + - SUPPLIER0 + SKU862: + - SUPPLIER0 + SKU863: + - SUPPLIER0 + SKU864: + - SUPPLIER0 + SKU865: + - SUPPLIER0 + SKU866: + - SUPPLIER0 + SKU867: + - SUPPLIER0 + SKU868: + - SUPPLIER0 + SKU869: + - SUPPLIER0 + SKU87: + - SUPPLIER0 + SKU870: + - SUPPLIER0 + SKU871: + - SUPPLIER0 + SKU872: + - SUPPLIER0 + SKU873: + - SUPPLIER0 + SKU874: + - SUPPLIER0 + SKU875: + - SUPPLIER0 + SKU876: + - SUPPLIER0 + SKU877: + - SUPPLIER0 + SKU878: + - SUPPLIER0 + SKU879: + - SUPPLIER0 + SKU88: + - SUPPLIER0 + SKU880: + - SUPPLIER0 + SKU881: + - SUPPLIER0 + SKU882: + - SUPPLIER0 + SKU883: + - SUPPLIER0 + SKU884: + - SUPPLIER0 + SKU885: + - SUPPLIER0 + SKU886: + - SUPPLIER0 + SKU887: + - SUPPLIER0 + SKU888: + - SUPPLIER0 + SKU889: + - SUPPLIER0 + SKU89: + - SUPPLIER0 + SKU890: + - SUPPLIER0 + SKU891: + - SUPPLIER0 + SKU892: + - SUPPLIER0 + SKU893: + - SUPPLIER0 + SKU894: + - SUPPLIER0 + SKU895: + - SUPPLIER0 + SKU896: + - SUPPLIER0 + SKU897: + - SUPPLIER0 + SKU898: + - SUPPLIER0 + SKU899: + - SUPPLIER0 + SKU9: + - SUPPLIER0 + SKU90: + - SUPPLIER0 + SKU900: + - SUPPLIER0 + SKU901: + - SUPPLIER0 + SKU902: + - SUPPLIER0 + SKU903: + - SUPPLIER0 + SKU904: + - SUPPLIER0 + SKU905: + - SUPPLIER0 + SKU906: + - SUPPLIER0 + SKU907: + - SUPPLIER0 + SKU908: + - SUPPLIER0 + SKU909: + - SUPPLIER0 + SKU91: + - SUPPLIER0 + SKU910: + - SUPPLIER0 + SKU911: + - SUPPLIER0 + SKU912: + - SUPPLIER0 + SKU913: + - SUPPLIER0 + SKU914: + - SUPPLIER0 + SKU915: + - SUPPLIER0 + SKU916: + - SUPPLIER0 + SKU917: + - SUPPLIER0 + SKU918: + - SUPPLIER0 + SKU919: + - SUPPLIER0 + SKU92: + - SUPPLIER0 + SKU920: + - SUPPLIER0 + SKU921: + - SUPPLIER0 + SKU922: + - SUPPLIER0 + SKU923: + - SUPPLIER0 + SKU924: + - SUPPLIER0 + SKU925: + - SUPPLIER0 + SKU926: + - SUPPLIER0 + SKU927: + - SUPPLIER0 + SKU928: + - SUPPLIER0 + SKU929: + - SUPPLIER0 + SKU93: + - SUPPLIER0 + SKU930: + - SUPPLIER0 + SKU931: + - SUPPLIER0 + SKU932: + - SUPPLIER0 + SKU933: + - SUPPLIER0 + SKU934: + - SUPPLIER0 + SKU935: + - SUPPLIER0 + SKU936: + - SUPPLIER0 + SKU937: + - SUPPLIER0 + SKU938: + - SUPPLIER0 + SKU939: + - SUPPLIER0 + SKU94: + - SUPPLIER0 + SKU940: + - SUPPLIER0 + SKU941: + - SUPPLIER0 + SKU942: + - SUPPLIER0 + SKU943: + - SUPPLIER0 + SKU944: + - SUPPLIER0 + SKU945: + - SUPPLIER0 + SKU946: + - SUPPLIER0 + SKU947: + - SUPPLIER0 + SKU948: + - SUPPLIER0 + SKU949: + - SUPPLIER0 + SKU95: + - SUPPLIER0 + SKU950: + - SUPPLIER0 + SKU951: + - SUPPLIER0 + SKU952: + - SUPPLIER0 + SKU953: + - SUPPLIER0 + SKU954: + - SUPPLIER0 + SKU955: + - SUPPLIER0 + SKU956: + - SUPPLIER0 + SKU957: + - SUPPLIER0 + SKU958: + - SUPPLIER0 + SKU959: + - SUPPLIER0 + SKU96: + - SUPPLIER0 + SKU960: + - SUPPLIER0 + SKU961: + - SUPPLIER0 + SKU962: + - SUPPLIER0 + SKU963: + - SUPPLIER0 + SKU964: + - SUPPLIER0 + SKU965: + - SUPPLIER0 + SKU966: + - SUPPLIER0 + SKU967: + - SUPPLIER0 + SKU968: + - SUPPLIER0 + SKU969: + - SUPPLIER0 + SKU97: + - SUPPLIER0 + SKU970: + - SUPPLIER0 + SKU971: + - SUPPLIER0 + SKU972: + - SUPPLIER0 + SKU973: + - SUPPLIER0 + SKU974: + - SUPPLIER0 + SKU975: + - SUPPLIER0 + SKU976: + - SUPPLIER0 + SKU977: + - SUPPLIER0 + SKU978: + - SUPPLIER0 + SKU979: + - SUPPLIER0 + SKU98: + - SUPPLIER0 + SKU980: + - SUPPLIER0 + SKU981: + - SUPPLIER0 + SKU982: + - SUPPLIER0 + SKU983: + - SUPPLIER0 + SKU984: + - SUPPLIER0 + SKU985: + - SUPPLIER0 + SKU986: + - SUPPLIER0 + SKU987: + - SUPPLIER0 + SKU988: + - SUPPLIER0 + SKU989: + - SUPPLIER0 + SKU99: + - SUPPLIER0 + SKU990: + - SUPPLIER0 + SKU991: + - SUPPLIER0 + SKU992: + - SUPPLIER0 + SKU993: + - SUPPLIER0 + SKU994: + - SUPPLIER0 + SKU995: + - SUPPLIER0 + SKU996: + - SUPPLIER0 + SKU997: + - SUPPLIER0 + SKU998: + - SUPPLIER0 + SKU999: + - SUPPLIER0 diff --git a/requirements.dev.txt b/requirements.dev.txt index 8bd0a5b19..cddc53a49 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -20,6 +20,7 @@ torchsummary==1.5.1 wrapt==1.11.2 zmq==0.0.0 numpy==1.19.1 +scipy==1.6.0 tabulate==0.8.5 networkx==2.4 palettable==3.3.0 From 99806c38bc3e939f1ff68128947caf278c7448d4 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 19:01:21 +0800 Subject: [PATCH 156/482] env wrapper speed up --- examples/supply_chain/env_wrapper.py | 532 +- .../supply_chain/topologies/random/config.yml | 54472 +++++----------- .../scenarios/supply_chain/units/storage.py | 7 + 3 files changed, 17617 insertions(+), 37394 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 47daf766e..591a85f9d 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -1,6 +1,3 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - from maro.simulator import Env from collections import defaultdict, namedtuple import scipy.stats as st @@ -57,6 +54,7 @@ def __getitem__(self, key, default=None): return default + class SCEnvWrapper(AbsEnvWrapper): def __init__(self, env: Env): super().__init__(env) @@ -80,6 +78,7 @@ def __init__(self, env: Env): # TODO: this is fixed after env setup self._cur_facility_storage_product_mapping = {} + self._cur_product_numbers = [] self._service_index_ppf_cache = {} @@ -99,61 +98,72 @@ def __init__(self, env: Env): # unit id -> (facility id) self.unit_2_facility_dict = {} - for facility_id, facility in self._summary["facilities"].items(): - self.facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility['configs'], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } + # our raw state + self._states = {} - units = facility["units"] + # facility_id -> storage index + self._facility2storage_index_dict = {} - storage = units["storage"] - if storage is not None: - self.facility_levels[facility_id]["storage"] = UnitBaseInfo(storage) + # facility_id -> product id -> number + self._storage_product_numbers = {} - self.unit_2_facility_dict[storage["id"]] = facility_id + # facility id -> product_id -> index + self._storage_product_indices = {} - distribution = units["distribution"] + # built internal helpers. + self._build_internal_helpers() - if distribution is not None: - self.facility_levels[facility_id]["distribution"] = UnitBaseInfo(distribution) - self.unit_2_facility_dict[distribution["id"]] = facility_id + def get_state(self, event): + self.cur_balance_sheet_reward = self.balance_cal.calc() + self._cur_metrics = self.env.metrics - products = units["products"] + final_state = {} - if products: - for product_id, product in products.items(): - product_info = { - "skuproduct": UnitBaseInfo(product) - } + # calculate storage info first, then use it later to speed up. + for facility_id, storage_index in self._facility2storage_index_dict.items(): + product_numbers = self.storage_ss[self.env.tick:storage_index:"product_number"].flatten().astype(np.int) - self.unit_2_facility_dict[product["id"]] = facility_id + for pid, index in self._storage_product_indices[facility_id].items(): + self._storage_product_numbers[facility_id][pid] = product_numbers[index] - seller = product['seller'] + for agent_info in self._agent_list: + state = self._states[agent_info.id] - if seller is not None: - product_info["seller"] = UnitBaseInfo(seller) - self.unit_2_facility_dict[seller["id"]] = facility_id + storage_index = self._facility2storage_index_dict[agent_info.facility_id] - consumer = product["consumer"] + self._update_facility_features(state, agent_info) + self._update_storage_features(state, agent_info) + # bom do not need to update + # self._add_bom_features(state, agent_info) + self._update_distribution_features(state, agent_info) + self._update_sale_features(state, agent_info) + # vlt do not need to update + # self._update_vlt_features(state, agent_info) + self._update_consumer_features(state, agent_info) + # self._add_price_features(state, agent_info) + # self._add_global_features(state) - if consumer is not None: - product_info["consumer"] = UnitBaseInfo(consumer) - self.unit_2_facility_dict[consumer["id"]] = facility_id + state[f"consumer.{agent_info.id}"] = state + state[f"producer.{agent_info.id}"] = state - manufacture = product["manufacture"] + return self._serialize_state(final_state) - if manufacture is not None: - product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"]] = facility_id + def get_reward(self, tick=None, target_agents=None): + wc = self.env.configs.settings["global_reward_weight_consumer"] + parent_facility_balance = {} + for f_id, sheet in self.cur_balance_sheet_reward.items(): + if f_id in self.unit_2_facility_dict: + # it is a product unit + parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] + else: + parent_facility_balance[f_id] = sheet - self.facility_levels[facility_id][product_id] = product_info + consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items() } - def get_state(self, event): - self.cur_balance_sheet_reward = self.balance_cal.calc() - return self._get_state() + return { + **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, + **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} + } def get_action(self, action_by_agent): # cache the sources for each consumer if not yet cached @@ -185,183 +195,27 @@ def get_action(self, action_by_agent): return env_action - def get_reward(self, tick=None, target_agents=None): - wc = self.env.configs.settings["global_reward_weight_consumer"] - parent_facility_balance = {} - for f_id, sheet in self.cur_balance_sheet_reward.items(): - if f_id in self.unit_2_facility_dict: - # it is a product unit - parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] - else: - parent_facility_balance[f_id] = sheet - - consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items() } - - return { - **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, - **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} - } - - def _get_state(self): - self._cur_metrics = self.env.metrics - - state = {} - - for agent_info in self._agent_list: - storage_index = self.facility_levels[agent_info.facility_id]['storage'].node_index - - storage_product_list = self.storage_ss[self.env.tick:storage_index:"product_list"].flatten().astype(np.int) - storage_product_number = self.storage_ss[self.env.tick:storage_index:"product_number"].flatten().astype(np.int) - - self._cur_facility_storage_products = { pid:pnum for pid, pnum in zip(storage_product_list, storage_product_number)} - - self._cur_facility_storage_product_mapping = {product_id: i for i, product_id in enumerate(storage_product_list)} - - f_state = self._state_from_info(agent_info) - - self._add_global_features(f_state) - - state[f"consumer.{agent_info.id}"] = f_state - state[f"producer.{agent_info.id}"] = f_state - - return self._serialize_state(state) - - def _state_from_info(self, agent_info): - state = {} - - self._add_facility_features(state, agent_info) - self._add_storage_features(state, agent_info) - self._add_bom_features(state, agent_info) - self._add_distributor_features(state, agent_info) - self._add_sale_features(state, agent_info) - self._add_vlt_features(state, agent_info) - self._add_consumer_features(state, agent_info) - self._add_price_features(state, agent_info) - - return state - - def _add_global_features(self, state): - state['global_time'] = self.env.tick + def _update_facility_features(self, state, agent_info): + state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - def _add_facility_features(self, state, agent_info): - state['facility_type'] = [ - 1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] - - # NOTE: We cannot provide facility instance - state["facility"] = None - - state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] - - # NOTE: we have no constraint now - state['constraint_idx'] = [0] - - for atom_name in atoms.keys(): - state[atom_name] = list(np.ones(self._configs.settings['constraint_state_hist_len'])) - - # NOTE: named as facility_id but actually sku id - # NOTE: as our sku id start from 1, so we need expend one-hot length - state['facility_id'] = [0] * self._sku_number - - facility = self.facility_levels[agent_info.facility_id] - - if agent_info.is_facility: - # truely facility - state['facility_info'] = facility['config'] - state['sku_info'] = {} - - metrics = self._cur_metrics["facilities"][agent_info.facility_id] - - state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - else: - # a product unit - # 3rd slot is the facility id of this unit - state['facility_info'] = facility['config'] - state['sku_info'] = agent_info.sku - - metrics = self._cur_metrics["products"][agent_info.id] - state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - - # NOTE: ignore constraint here - - state['facility_id'][agent_info.sku.id] = 1 - - # NOTE: ignore atom here - - # NOTE: ignore this as we do not implement it now - state['echelon_level'] = 0 - - def _add_storage_features(self, state, agent_info): - facility = self.facility_levels[agent_info.facility_id] - - state['storage_levels'] = [0] * self._sku_number - - state['storage_capacity'] = facility['storage'].config["capacity"] - - state['storage_utilization'] = 0 - - for product_id, product_number in self._cur_facility_storage_products.items(): - state['storage_levels'][product_id] = product_number + def _update_storage_features(self, state, agent_info): + facility_id = agent_info.facility_id + for pid, index in self._storage_product_indices[facility_id].items(): + product_number = self._storage_product_numbers[facility_id][pid] # product_number = self._cur_product_numbers[index] + state['storage_levels'][pid] = product_number state['storage_utilization'] += product_number - def _add_bom_features(self, state, agent_info): - state['bom_inputs'] = [0] * self._sku_number - state['bom_outputs'] = [0] * self._sku_number - - if not agent_info.is_facility: - state['bom_inputs'][agent_info.sku.id] = 1 - state['bom_outputs'][agent_info.sku.id] = 1 - - def _add_vlt_features(self, state, agent_info): - sku_list = self._summary["skus"] - facility = self.facility_levels[agent_info.facility_id] - - current_source_list = [] - - # only for product unit - if agent_info.sku is not None: - current_source_list = facility["upstreams"].get(agent_info.sku.id, []) - - state['vlt'] = [0] * (self._max_sources_per_facility * self._sku_number) - state['max_vlt'] = 0 - - if not agent_info.is_facility: - # only for sku product - product_info = facility[agent_info.sku.id] - - if "consumer" in product_info and len(current_source_list) > 0: - state['max_vlt'] = product_info["skuproduct"]["max_vlt"] - - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - # NOTE: different with original code, our config can make sure that source has product we need - - if sku.id == agent_info.sku.id: - state['vlt'][i * len(sku_list) + j + 1] = facility["skus"][sku.id].vlt - - def _add_sale_features(self, state, agent_info): - state['sale_mean'] = 1.0 - state['sale_std'] = 1.0 - state['sale_gamma'] = 1.0 - state['service_level'] = 0.95 - state['total_backlog_demand'] = 0 + def _update_sale_features(self, state, agent_info): + if agent_info.is_facility: + return settings = self.env.configs.settings hist_len = settings['sale_hist_len'] consumption_hist_len = settings['consumption_hist_len'] - - state['sale_hist'] = [0] * hist_len - state['backlog_demand_hist'] = [0] * hist_len - state['consumption_hist'] = [0] * consumption_hist_len - state['pending_order'] = [0] * settings['pending_order_len'] - - if agent_info.is_facility: - return - product_metrics = self._cur_metrics["products"][agent_info.id] # for product unit only - state['service_level'] = agent_info.sku.service_level state['sale_mean'] = product_metrics["sale_mean"] state['sale_gamma'] = state['sale_mean'] state['sale_std'] = product_metrics["sale_std"] @@ -389,37 +243,19 @@ def _add_sale_features(self, state, agent_info): state['total_backlog_demand'] = single_states[0] state['sale_hist'] = list(hist_states[0]) state['backlog_demand_hist'] = list(hist_states[1]) - state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma - - def _add_distributor_features(self, state, agent_info): - state['distributor_in_transit_orders'] = 0 - state['distributor_in_transit_orders_qty'] = 0 + def _update_distribution_features(self, state, agent_info): facility = self.facility_levels[agent_info.facility_id] - distribution = facility.get("distribution", None) if distribution is not None: - dist_states = self.env.snapshot_list["distribution"][self.env.tick:distribution.id:("remaining_order_quantity", "remaining_order_number")] + dist_states = self.env.snapshot_list["distribution"][self.env.tick:distribution.node_index:("remaining_order_quantity", "remaining_order_number")] dist_states = dist_states.flatten().astype(np.int) state['distributor_in_transit_orders'] = dist_states[1] state['distributor_in_transit_orders_qty'] = dist_states[0] - def _add_consumer_features(self, state, agent_info): - state['consumer_source_export_mask'] = [0] * (self._max_sources_per_facility * self._sku_number) - state['consumer_source_inventory'] = [0] * self._sku_number - state['consumer_in_transit_orders'] = [0] * self._sku_number - - state['inventory_in_stock'] = 0 - state['inventory_in_transit'] = 0 - state['inventory_in_distribution'] = 0 - state['inventory_estimated'] = 0 - state['inventory_rop'] = 0 - state['is_over_stock'] = 0 - state['is_out_of_stock'] = 0 - state['is_below_rop'] = 0 - + def _update_consumer_features(self, state, agent_info): if agent_info.is_facility: return @@ -429,24 +265,17 @@ def _add_consumer_features(self, state, agent_info): if "consumer" not in product_info: return - source_list = facility["upstreams"].get(agent_info.sku.id, []) - - if len(source_list) == 0: - return - sku_list = self._summary["skus"] - for i, source in enumerate(source_list): - for j, sku in enumerate(sku_list.values()): - if sku.id == agent_info.sku.id: - state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = self.facility_levels[source]["skus"][sku.id].vlt - in_transit_orders = self._cur_metrics['facilities'][agent_info.facility_id]["in_transit_orders"] - for i, sku in enumerate(sku_list.values()): - state['consumer_in_transit_orders'][sku.id] += in_transit_orders[sku.id] + # for i, sku in enumerate(sku_list.values()): + # state['consumer_in_transit_orders'][sku.id] += in_transit_orders[sku.id] + for sku_id, number in in_transit_orders.items(): + state['consumer_in_transit_orders'][sku_id] += number - state['inventory_in_stock'] = self._cur_facility_storage_products[agent_info.sku.id] + product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] @@ -474,15 +303,6 @@ def _add_consumer_features(self, state, agent_info): if state['inventory_estimated'] < state['inventory_rop']: state['is_below_rop'] = 1 - def _add_price_features(self, state, agent_info): - state['max_price'] = self._max_price - state['sku_price'] = 0 - state['sku_cost'] = 0 - - if not agent_info.is_facility: - state['sku_price'] = agent_info.sku.price - state['sku_cost'] = agent_info.sku.cost - def _serialize_state(self, state): result = defaultdict(list) @@ -511,6 +331,200 @@ def _serialize_state(self, state): return result + def _build_internal_helpers(self): + # facility levels + for facility_id, facility in self._summary["facilities"].items(): + self.facility_levels[facility_id] = { + "node_index": facility["node_index"], + "config": facility['configs'], + "upstreams": facility["upstreams"], + "skus": facility["skus"] + } + + units = facility["units"] + + storage = units["storage"] + if storage is not None: + self.facility_levels[facility_id]["storage"] = UnitBaseInfo(storage) + + self.unit_2_facility_dict[storage["id"]] = facility_id + + self._facility2storage_index_dict[facility_id] = storage["node_index"] + + self._storage_product_numbers[facility_id] = {} + self._storage_product_indices[facility_id] = {} + + for i, pid in enumerate(storage["product_list"]): + self._storage_product_indices[facility_id][pid] = i + self._storage_product_numbers[facility_id][pid] = 0 + + distribution = units["distribution"] + + if distribution is not None: + self.facility_levels[facility_id]["distribution"] = UnitBaseInfo(distribution) + self.unit_2_facility_dict[distribution["id"]] = facility_id + + products = units["products"] + + if products: + for product_id, product in products.items(): + product_info = { + "skuproduct": UnitBaseInfo(product) + } + + self.unit_2_facility_dict[product["id"]] = facility_id + + seller = product['seller'] + + if seller is not None: + product_info["seller"] = UnitBaseInfo(seller) + self.unit_2_facility_dict[seller["id"]] = facility_id + + consumer = product["consumer"] + + if consumer is not None: + product_info["consumer"] = UnitBaseInfo(consumer) + self.unit_2_facility_dict[consumer["id"]] = facility_id + + manufacture = product["manufacture"] + + if manufacture is not None: + product_info["manufacture"] = UnitBaseInfo(manufacture) + self.unit_2_facility_dict[manufacture["id"]] = facility_id + + self.facility_levels[facility_id][product_id] = product_info + + # create initial state structure + self._build_init_state() + + def _build_init_state(self): + # we will build the final state with default and const values, + # then update dynamic part per step + for agent_info in self._agent_list: + state = {} + + facility = self.facility_levels[agent_info.facility_id] + + # global features + state["global_time"] = 0 + + # facility features + state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] + state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] + state['constraint_idx'] = [0] + state['facility_id'] = [0] * self._sku_number + state['sku_info'] = {} if agent_info.sku is not None else agent_info.sku + state['echelon_level'] = 0 + + state['facility_info'] = facility['config'] + state["is_positive_balance"] = 0 + + if not agent_info.is_facility: + state['facility_id'][agent_info.sku.id] = 1 + + state[f"consumer.{agent_info.id}"] = state + state[f"producer.{agent_info.id}"] = state + + for atom_name in atoms.keys(): + state[atom_name] = list(np.ones(self._configs.settings['constraint_state_hist_len'])) + + # storage features + state['storage_levels'] = [0] * self._sku_number + state['storage_capacity'] = facility['storage'].config["capacity"] + state['storage_utilization'] = 0 + + # bom features + state['bom_inputs'] = [0] * self._sku_number + state['bom_outputs'] = [0] * self._sku_number + + if not agent_info.is_facility: + state['bom_inputs'][agent_info.sku.id] = 1 + state['bom_outputs'][agent_info.sku.id] = 1 + + # vlt features + sku_list = self._summary["skus"] + current_source_list = [] + + if agent_info.sku is not None: + current_source_list = facility["upstreams"].get(agent_info.sku.id, []) + + state['vlt'] = [0] * (self._max_sources_per_facility * self._sku_number) + state['max_vlt'] = 0 + + if not agent_info.is_facility: + # only for sku product + product_info = facility[agent_info.sku.id] + + if "consumer" in product_info and len(current_source_list) > 0: + state['max_vlt'] = product_info["skuproduct"]["max_vlt"] + + for i, source in enumerate(current_source_list): + for j, sku in enumerate(sku_list.values()): + # NOTE: different with original code, our config can make sure that source has product we need + + if sku.id == agent_info.sku.id: + state['vlt'][i * len(sku_list) + j + 1] = facility["skus"][sku.id].vlt + + # sale features + settings = self.env.configs.settings + hist_len = settings['sale_hist_len'] + consumption_hist_len = settings['consumption_hist_len'] + + state['sale_mean'] = 1.0 + state['sale_std'] = 1.0 + state['sale_gamma'] = 1.0 + state['service_level'] = 0.95 + state['total_backlog_demand'] = 0 + + state['sale_hist'] = [0] * hist_len + state['backlog_demand_hist'] = [0] * hist_len + state['consumption_hist'] = [0] * consumption_hist_len + state['pending_order'] = [0] * settings['pending_order_len'] + + if not agent_info.is_facility: + state['service_level'] = agent_info.sku.service_level + + product_info = facility[agent_info.sku.id] + + if "seller" in product_info: + state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma + + # distribution features + state['distributor_in_transit_orders'] = 0 + state['distributor_in_transit_orders_qty'] = 0 + + # consumer features + state['consumer_source_export_mask'] = [0] * (self._max_sources_per_facility * self._sku_number) + state['consumer_source_inventory'] = [0] * self._sku_number + state['consumer_in_transit_orders'] = [0] * self._sku_number + + state['inventory_in_stock'] = 0 + state['inventory_in_transit'] = 0 + state['inventory_in_distribution'] = 0 + state['inventory_estimated'] = 0 + state['inventory_rop'] = 0 + state['is_over_stock'] = 0 + state['is_out_of_stock'] = 0 + state['is_below_rop'] = 0 + + if len(current_source_list) > 0: + for i, source in enumerate(current_source_list): + for j, sku in enumerate(sku_list.values()): + if sku.id == agent_info.sku.id: + state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = self.facility_levels[source]["skus"][sku.id].vlt + + # price features + state['max_price'] = self._max_price + state['sku_price'] = 0 + state['sku_cost'] = 0 + + if not agent_info.is_facility: + state['sku_price'] = agent_info.sku.price + state['sku_cost'] = agent_info.sku.cost + + self._states[agent_info.id] = state + + class BalanceSheetCalculator: consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") seller_features = ("id", "sold", "demand", "price", "backlog_ratio") @@ -732,18 +746,20 @@ def calc(self): if __name__ == "__main__": from time import time - start_tick = 0 - durations = 100 - env = Env(scenario="supply_chain", topology="sample1", - start_tick=start_tick, durations=durations) + env = Env( + scenario="supply_chain", + topology="random", + durations=100, + max_snapshots=10) ss = SCEnvWrapper(env) env.step(None) + start_time = time() + states = ss.get_state(None) - rewards = ss.get_reward(None) + end_time = time() - print(states) - print(rewards) + print("time cost:", end_time - start_time) \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/topologies/random/config.yml b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml index 32f21232f..bd875d1ad 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/random/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml @@ -179,28822 +179,9022 @@ world: - *id001 - *id001 - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5316100 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 200 - definition_ref: SupplierFacility - name: SUPPLIER0 - skus: - SKU0: - cost: 138 - init_stock: 4450 - price: 154 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU1: - cost: 109 - init_stock: 4300 - price: 122 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU10: - cost: 142 - init_stock: 4600 - price: 158 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU100: - cost: 403 - init_stock: 1850 - price: 448 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU101: - cost: 388 - init_stock: 3100 - price: 432 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU102: - cost: 29 - init_stock: 4150 - price: 33 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU103: - cost: 152 - init_stock: 3950 - price: 169 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU104: - cost: 227 - init_stock: 1450 - price: 253 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU105: - cost: 274 - init_stock: 2950 - price: 305 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU106: - cost: 154 - init_stock: 1450 - price: 172 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU107: - cost: 290 - init_stock: 4050 - price: 323 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU108: - cost: 254 - init_stock: 4600 - price: 283 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU109: - cost: 273 - init_stock: 3850 - price: 304 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU11: - cost: 183 - init_stock: 2900 - price: 204 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU110: - cost: 336 - init_stock: 4650 - price: 374 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU111: - cost: 193 - init_stock: 3900 - price: 215 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU112: - cost: 375 - init_stock: 400 - price: 417 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU113: - cost: 57 - init_stock: 3450 - price: 64 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU114: - cost: 56 - init_stock: 2700 - price: 63 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU115: - cost: 419 - init_stock: 4350 - price: 466 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU116: - cost: 103 - init_stock: 4600 - price: 115 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU117: - cost: 68 - init_stock: 3350 - price: 76 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU118: - cost: 262 - init_stock: 3450 - price: 292 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU119: - cost: 299 - init_stock: 1950 - price: 333 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU12: - cost: 386 - init_stock: 3700 - price: 429 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU120: - cost: 132 - init_stock: 2400 - price: 147 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU121: - cost: 152 - init_stock: 2950 - price: 169 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU122: - cost: 63 - init_stock: 3600 - price: 71 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU123: - cost: 29 - init_stock: 2950 - price: 33 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU124: - cost: 430 - init_stock: 2900 - price: 478 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU125: - cost: 279 - init_stock: 1800 - price: 311 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU126: - cost: 447 - init_stock: 3800 - price: 497 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU127: - cost: 56 - init_stock: 4600 - price: 63 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU128: - cost: 180 - init_stock: 2100 - price: 200 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU129: - cost: 418 - init_stock: 1450 - price: 465 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU13: - cost: 412 - init_stock: 1450 - price: 458 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU130: - cost: 201 - init_stock: 2700 - price: 224 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU131: - cost: 441 - init_stock: 3000 - price: 491 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU132: - cost: 83 - init_stock: 3050 - price: 93 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU133: - cost: 242 - init_stock: 650 - price: 269 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU134: - cost: 327 - init_stock: 1000 - price: 364 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU135: - cost: 315 - init_stock: 4900 - price: 351 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU136: - cost: 386 - init_stock: 2000 - price: 429 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU137: - cost: 173 - init_stock: 4600 - price: 193 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU138: - cost: 101 - init_stock: 1400 - price: 113 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU139: - cost: 9 - init_stock: 4750 - price: 10 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU14: - cost: 49 - init_stock: 650 - price: 55 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU140: - cost: 315 - init_stock: 4800 - price: 350 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU141: - cost: 63 - init_stock: 3550 - price: 70 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU142: - cost: 446 - init_stock: 3650 - price: 496 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU143: - cost: 29 - init_stock: 4150 - price: 33 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU144: - cost: 247 - init_stock: 3750 - price: 275 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU145: - cost: 45 - init_stock: 4850 - price: 51 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU146: - cost: 126 - init_stock: 1800 - price: 140 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU147: - cost: 344 - init_stock: 2450 - price: 383 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU148: - cost: 250 - init_stock: 3400 - price: 278 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU149: - cost: 361 - init_stock: 2600 - price: 402 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU15: - cost: 378 - init_stock: 3550 - price: 421 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU150: - cost: 72 - init_stock: 4150 - price: 81 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU151: - cost: 366 - init_stock: 3250 - price: 407 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU152: - cost: 294 - init_stock: 3200 - price: 327 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU153: - cost: 183 - init_stock: 4850 - price: 204 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU154: - cost: 79 - init_stock: 1300 - price: 88 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU155: - cost: 385 - init_stock: 1300 - price: 428 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU156: - cost: 437 - init_stock: 400 - price: 486 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU157: - cost: 31 - init_stock: 3300 - price: 35 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU158: - cost: 167 - init_stock: 2850 - price: 186 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU159: - cost: 181 - init_stock: 4600 - price: 202 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU16: - cost: 188 - init_stock: 1200 - price: 209 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU160: - cost: 389 - init_stock: 2350 - price: 433 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU161: - cost: 225 - init_stock: 2200 - price: 250 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU162: - cost: 284 - init_stock: 4000 - price: 316 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU163: - cost: 102 - init_stock: 700 - price: 114 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU164: - cost: 273 - init_stock: 3300 - price: 304 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU165: - cost: 175 - init_stock: 2650 - price: 195 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU166: - cost: 283 - init_stock: 2550 - price: 315 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU167: - cost: 423 - init_stock: 4700 - price: 470 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU168: - cost: 428 - init_stock: 950 - price: 476 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU169: - cost: 120 - init_stock: 950 - price: 134 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU17: - cost: 360 - init_stock: 1900 - price: 401 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU170: - cost: 260 - init_stock: 1900 - price: 289 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU171: - cost: 432 - init_stock: 4100 - price: 481 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU172: - cost: 390 - init_stock: 1550 - price: 434 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU173: - cost: 262 - init_stock: 4700 - price: 292 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU174: - cost: 426 - init_stock: 700 - price: 474 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU175: - cost: 428 - init_stock: 1050 - price: 476 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU176: - cost: 269 - init_stock: 4000 - price: 299 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU177: - cost: 382 - init_stock: 1050 - price: 425 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU178: - cost: 377 - init_stock: 3500 - price: 419 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU179: - cost: 212 - init_stock: 3250 - price: 236 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU18: - cost: 414 - init_stock: 4050 - price: 460 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU180: - cost: 89 - init_stock: 800 - price: 99 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU181: - cost: 109 - init_stock: 1350 - price: 122 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU182: - cost: 398 - init_stock: 1150 - price: 443 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU183: - cost: 64 - init_stock: 2650 - price: 72 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU184: - cost: 172 - init_stock: 3850 - price: 192 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU185: - cost: 117 - init_stock: 1400 - price: 130 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU186: - cost: 44 - init_stock: 1950 - price: 49 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU187: - cost: 442 - init_stock: 2350 - price: 492 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU188: - cost: 45 - init_stock: 4500 - price: 50 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU189: - cost: 130 - init_stock: 1650 - price: 145 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU19: - cost: 405 - init_stock: 300 - price: 451 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU190: - cost: 64 - init_stock: 3950 - price: 72 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU191: - cost: 256 - init_stock: 2000 - price: 285 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU192: - cost: 164 - init_stock: 4750 - price: 183 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU193: - cost: 310 - init_stock: 3250 - price: 345 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU194: - cost: 395 - init_stock: 3700 - price: 439 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU195: - cost: 184 - init_stock: 1600 - price: 205 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU196: - cost: 187 - init_stock: 1100 - price: 208 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU197: - cost: 428 - init_stock: 250 - price: 476 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU198: - cost: 242 - init_stock: 3850 - price: 269 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU199: - cost: 154 - init_stock: 2750 - price: 172 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU2: - cost: 183 - init_stock: 1250 - price: 204 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU20: - cost: 393 - init_stock: 2050 - price: 437 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU200: - cost: 45 - init_stock: 4700 - price: 51 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU201: - cost: 438 - init_stock: 3700 - price: 487 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU202: - cost: 365 - init_stock: 700 - price: 406 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU203: - cost: 299 - init_stock: 1450 - price: 333 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU204: - cost: 57 - init_stock: 3900 - price: 64 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU205: - cost: 403 - init_stock: 4550 - price: 448 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU206: - cost: 58 - init_stock: 4850 - price: 65 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU207: - cost: 393 - init_stock: 2900 - price: 437 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU208: - cost: 34 - init_stock: 1250 - price: 38 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU209: - cost: 294 - init_stock: 3250 - price: 327 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU21: - cost: 15 - init_stock: 2750 - price: 17 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU210: - cost: 409 - init_stock: 2500 - price: 455 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU211: - cost: 153 - init_stock: 750 - price: 170 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU212: - cost: 55 - init_stock: 3450 - price: 62 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU213: - cost: 236 - init_stock: 3500 - price: 263 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU214: - cost: 390 - init_stock: 350 - price: 434 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU215: - cost: 254 - init_stock: 1200 - price: 283 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU216: - cost: 351 - init_stock: 3900 - price: 391 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU217: - cost: 151 - init_stock: 4900 - price: 168 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU218: - cost: 436 - init_stock: 900 - price: 485 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU219: - cost: 180 - init_stock: 4300 - price: 201 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU22: - cost: 19 - init_stock: 1500 - price: 22 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU220: - cost: 354 - init_stock: 650 - price: 394 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU221: - cost: 97 - init_stock: 4600 - price: 108 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU222: - cost: 413 - init_stock: 2800 - price: 459 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU223: - cost: 378 - init_stock: 3650 - price: 420 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU224: - cost: 275 - init_stock: 3500 - price: 306 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU225: - cost: 106 - init_stock: 4600 - price: 118 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU226: - cost: 382 - init_stock: 2200 - price: 425 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU227: - cost: 171 - init_stock: 3250 - price: 191 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU228: - cost: 144 - init_stock: 1600 - price: 160 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU229: - cost: 250 - init_stock: 3650 - price: 278 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU23: - cost: 111 - init_stock: 700 - price: 124 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU230: - cost: 147 - init_stock: 950 - price: 164 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU231: - cost: 288 - init_stock: 4700 - price: 321 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU232: - cost: 391 - init_stock: 2050 - price: 435 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU233: - cost: 197 - init_stock: 1850 - price: 219 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU234: - cost: 129 - init_stock: 850 - price: 144 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU235: - cost: 356 - init_stock: 1100 - price: 396 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU236: - cost: 344 - init_stock: 4950 - price: 383 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU237: - cost: 97 - init_stock: 2550 - price: 108 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU238: - cost: 44 - init_stock: 2850 - price: 49 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU239: - cost: 199 - init_stock: 4500 - price: 222 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU24: - cost: 331 - init_stock: 2350 - price: 368 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU240: - cost: 348 - init_stock: 4150 - price: 387 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU241: - cost: 271 - init_stock: 4450 - price: 302 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU242: - cost: 55 - init_stock: 3950 - price: 62 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU243: - cost: 10 - init_stock: 2500 - price: 12 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU244: - cost: 266 - init_stock: 3250 - price: 296 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU245: - cost: 35 - init_stock: 1950 - price: 39 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU246: - cost: 236 - init_stock: 2250 - price: 263 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU247: - cost: 201 - init_stock: 1300 - price: 224 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU248: - cost: 369 - init_stock: 4200 - price: 410 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU249: - cost: 57 - init_stock: 4850 - price: 64 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU25: - cost: 180 - init_stock: 2300 - price: 201 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU250: - cost: 233 - init_stock: 2250 - price: 259 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU251: - cost: 162 - init_stock: 1150 - price: 181 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU252: - cost: 297 - init_stock: 3250 - price: 330 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU253: - cost: 283 - init_stock: 4600 - price: 315 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU254: - cost: 366 - init_stock: 2450 - price: 407 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU255: - cost: 324 - init_stock: 3700 - price: 361 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU256: - cost: 60 - init_stock: 2350 - price: 67 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU257: - cost: 45 - init_stock: 3000 - price: 50 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU258: - cost: 418 - init_stock: 400 - price: 465 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU259: - cost: 311 - init_stock: 2000 - price: 346 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU26: - cost: 377 - init_stock: 3250 - price: 419 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU260: - cost: 336 - init_stock: 2100 - price: 374 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU261: - cost: 66 - init_stock: 1350 - price: 74 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU262: - cost: 410 - init_stock: 3450 - price: 456 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU263: - cost: 248 - init_stock: 2700 - price: 276 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU264: - cost: 80 - init_stock: 3000 - price: 89 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU265: - cost: 133 - init_stock: 3950 - price: 148 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU266: - cost: 258 - init_stock: 650 - price: 287 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU267: - cost: 27 - init_stock: 4800 - price: 31 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU268: - cost: 306 - init_stock: 1100 - price: 340 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU269: - cost: 431 - init_stock: 2600 - price: 479 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU27: - cost: 242 - init_stock: 4450 - price: 269 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU270: - cost: 209 - init_stock: 2000 - price: 233 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU271: - cost: 81 - init_stock: 600 - price: 90 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU272: - cost: 439 - init_stock: 1500 - price: 488 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU273: - cost: 449 - init_stock: 1000 - price: 499 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU274: - cost: 390 - init_stock: 4650 - price: 434 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU275: - cost: 411 - init_stock: 4800 - price: 457 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU276: - cost: 161 - init_stock: 1650 - price: 179 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU277: - cost: 272 - init_stock: 3100 - price: 303 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU278: - cost: 270 - init_stock: 2400 - price: 301 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU279: - cost: 15 - init_stock: 4100 - price: 17 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU28: - cost: 359 - init_stock: 300 - price: 399 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU280: - cost: 214 - init_stock: 4150 - price: 238 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU281: - cost: 11 - init_stock: 3450 - price: 13 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU282: - cost: 363 - init_stock: 4200 - price: 404 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU283: - cost: 298 - init_stock: 1600 - price: 332 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU284: - cost: 231 - init_stock: 1950 - price: 257 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU285: - cost: 47 - init_stock: 300 - price: 53 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU286: - cost: 226 - init_stock: 650 - price: 252 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU287: - cost: 144 - init_stock: 4700 - price: 161 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU288: - cost: 429 - init_stock: 800 - price: 477 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU289: - cost: 406 - init_stock: 4750 - price: 452 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU29: - cost: 144 - init_stock: 3700 - price: 160 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU290: - cost: 443 - init_stock: 1400 - price: 493 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU291: - cost: 162 - init_stock: 950 - price: 180 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU292: - cost: 209 - init_stock: 4750 - price: 233 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU293: - cost: 159 - init_stock: 3300 - price: 177 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU294: - cost: 320 - init_stock: 1350 - price: 356 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU295: - cost: 378 - init_stock: 4100 - price: 421 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU296: - cost: 372 - init_stock: 2350 - price: 414 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU297: - cost: 293 - init_stock: 4500 - price: 326 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU298: - cost: 109 - init_stock: 2300 - price: 122 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU299: - cost: 134 - init_stock: 3400 - price: 149 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU3: - cost: 144 - init_stock: 250 - price: 161 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU30: - cost: 355 - init_stock: 3050 - price: 395 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU300: - cost: 225 - init_stock: 1950 - price: 250 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU301: - cost: 251 - init_stock: 250 - price: 279 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU302: - cost: 288 - init_stock: 3700 - price: 321 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU303: - cost: 181 - init_stock: 2750 - price: 202 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU304: - cost: 353 - init_stock: 3400 - price: 393 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU305: - cost: 101 - init_stock: 3350 - price: 113 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU306: - cost: 387 - init_stock: 2150 - price: 430 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU307: - cost: 279 - init_stock: 4050 - price: 311 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU308: - cost: 380 - init_stock: 2900 - price: 423 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU309: - cost: 403 - init_stock: 3950 - price: 448 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU31: - cost: 410 - init_stock: 3800 - price: 456 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU310: - cost: 220 - init_stock: 3150 - price: 245 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU311: - cost: 406 - init_stock: 3500 - price: 452 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU312: - cost: 100 - init_stock: 3650 - price: 112 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU313: - cost: 276 - init_stock: 500 - price: 307 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU314: - cost: 36 - init_stock: 3800 - price: 40 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU315: - cost: 78 - init_stock: 1100 - price: 87 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU316: - cost: 330 - init_stock: 800 - price: 367 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU317: - cost: 55 - init_stock: 650 - price: 62 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU318: - cost: 137 - init_stock: 2100 - price: 153 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU319: - cost: 306 - init_stock: 2300 - price: 340 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU32: - cost: 200 - init_stock: 4450 - price: 223 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU320: - cost: 169 - init_stock: 4200 - price: 188 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU321: - cost: 378 - init_stock: 2350 - price: 421 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU322: - cost: 415 - init_stock: 3800 - price: 462 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU323: - cost: 104 - init_stock: 3950 - price: 116 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU324: - cost: 356 - init_stock: 1000 - price: 396 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU325: - cost: 366 - init_stock: 3000 - price: 407 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU326: - cost: 360 - init_stock: 600 - price: 400 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU327: - cost: 390 - init_stock: 5000 - price: 434 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU328: - cost: 161 - init_stock: 1050 - price: 179 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU329: - cost: 407 - init_stock: 4050 - price: 453 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU33: - cost: 101 - init_stock: 4450 - price: 113 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU330: - cost: 193 - init_stock: 4600 - price: 215 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU331: - cost: 25 - init_stock: 4200 - price: 28 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU332: - cost: 154 - init_stock: 2750 - price: 172 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU333: - cost: 56 - init_stock: 3350 - price: 63 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU334: - cost: 227 - init_stock: 1950 - price: 253 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU335: - cost: 19 - init_stock: 450 - price: 22 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU336: - cost: 236 - init_stock: 1450 - price: 263 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU337: - cost: 249 - init_stock: 3700 - price: 277 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU338: - cost: 162 - init_stock: 1700 - price: 181 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU339: - cost: 439 - init_stock: 3800 - price: 488 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU34: - cost: 203 - init_stock: 2200 - price: 226 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU340: - cost: 191 - init_stock: 1350 - price: 213 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU341: - cost: 380 - init_stock: 4450 - price: 423 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU342: - cost: 372 - init_stock: 2450 - price: 414 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU343: - cost: 386 - init_stock: 2600 - price: 429 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU344: - cost: 176 - init_stock: 4450 - price: 196 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU345: - cost: 405 - init_stock: 4600 - price: 451 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU346: - cost: 436 - init_stock: 1500 - price: 485 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU347: - cost: 27 - init_stock: 3400 - price: 30 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU348: - cost: 10 - init_stock: 600 - price: 12 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU349: - cost: 151 - init_stock: 2200 - price: 168 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU35: - cost: 162 - init_stock: 2250 - price: 181 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU350: - cost: 342 - init_stock: 1450 - price: 381 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU351: - cost: 299 - init_stock: 4800 - price: 333 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU352: - cost: 328 - init_stock: 950 - price: 365 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU353: - cost: 119 - init_stock: 3250 - price: 133 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU354: - cost: 320 - init_stock: 3100 - price: 356 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU355: - cost: 407 - init_stock: 2800 - price: 453 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU356: - cost: 302 - init_stock: 3450 - price: 336 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU357: - cost: 69 - init_stock: 1650 - price: 77 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU358: - cost: 306 - init_stock: 3350 - price: 340 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU359: - cost: 233 - init_stock: 3200 - price: 259 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU36: - cost: 200 - init_stock: 900 - price: 223 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU360: - cost: 131 - init_stock: 2700 - price: 146 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU361: - cost: 42 - init_stock: 4450 - price: 47 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU362: - cost: 386 - init_stock: 4850 - price: 429 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU363: - cost: 445 - init_stock: 4000 - price: 495 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU364: - cost: 191 - init_stock: 4500 - price: 213 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU365: - cost: 104 - init_stock: 600 - price: 116 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU366: - cost: 309 - init_stock: 4100 - price: 344 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU367: - cost: 177 - init_stock: 750 - price: 197 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU368: - cost: 369 - init_stock: 3000 - price: 410 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU369: - cost: 168 - init_stock: 4500 - price: 187 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU37: - cost: 271 - init_stock: 2250 - price: 302 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU370: - cost: 189 - init_stock: 4200 - price: 210 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU371: - cost: 363 - init_stock: 3450 - price: 404 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU372: - cost: 56 - init_stock: 3950 - price: 63 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU373: - cost: 95 - init_stock: 1200 - price: 106 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU374: - cost: 275 - init_stock: 1800 - price: 306 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU375: - cost: 137 - init_stock: 2550 - price: 153 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU376: - cost: 245 - init_stock: 750 - price: 273 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU377: - cost: 275 - init_stock: 3750 - price: 306 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU378: - cost: 209 - init_stock: 2200 - price: 233 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU379: - cost: 9 - init_stock: 1100 - price: 10 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU38: - cost: 319 - init_stock: 3150 - price: 355 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU380: - cost: 396 - init_stock: 1150 - price: 440 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU381: - cost: 361 - init_stock: 4000 - price: 402 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU382: - cost: 430 - init_stock: 5000 - price: 478 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU383: - cost: 223 - init_stock: 3500 - price: 248 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU384: - cost: 304 - init_stock: 1600 - price: 338 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU385: - cost: 269 - init_stock: 3050 - price: 299 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU386: - cost: 252 - init_stock: 4600 - price: 281 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU387: - cost: 141 - init_stock: 4700 - price: 157 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU388: - cost: 192 - init_stock: 2700 - price: 214 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU389: - cost: 116 - init_stock: 1200 - price: 129 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU39: - cost: 23 - init_stock: 350 - price: 26 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU390: - cost: 339 - init_stock: 4750 - price: 377 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU391: - cost: 162 - init_stock: 3750 - price: 180 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU392: - cost: 95 - init_stock: 3000 - price: 106 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU393: - cost: 115 - init_stock: 2600 - price: 128 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU394: - cost: 372 - init_stock: 3000 - price: 414 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU395: - cost: 374 - init_stock: 3450 - price: 416 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU396: - cost: 383 - init_stock: 2200 - price: 426 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU397: - cost: 243 - init_stock: 750 - price: 271 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU398: - cost: 18 - init_stock: 2950 - price: 21 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU399: - cost: 242 - init_stock: 2450 - price: 269 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU4: - cost: 171 - init_stock: 4250 - price: 190 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU40: - cost: 416 - init_stock: 4400 - price: 463 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU400: - cost: 40 - init_stock: 2650 - price: 45 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU401: - cost: 134 - init_stock: 4100 - price: 149 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU402: - cost: 356 - init_stock: 3950 - price: 396 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU403: - cost: 63 - init_stock: 2900 - price: 70 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU404: - cost: 202 - init_stock: 3950 - price: 225 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU405: - cost: 27 - init_stock: 1100 - price: 31 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU406: - cost: 404 - init_stock: 2150 - price: 449 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU407: - cost: 415 - init_stock: 800 - price: 462 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU408: - cost: 327 - init_stock: 1350 - price: 364 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU409: - cost: 381 - init_stock: 2900 - price: 424 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU41: - cost: 50 - init_stock: 1150 - price: 56 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU410: - cost: 183 - init_stock: 450 - price: 204 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU411: - cost: 177 - init_stock: 300 - price: 197 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU412: - cost: 126 - init_stock: 650 - price: 140 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU413: - cost: 9 - init_stock: 3550 - price: 11 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU414: - cost: 442 - init_stock: 1000 - price: 492 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU415: - cost: 23 - init_stock: 3150 - price: 26 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU416: - cost: 135 - init_stock: 250 - price: 151 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU417: - cost: 275 - init_stock: 2850 - price: 306 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU418: - cost: 138 - init_stock: 2100 - price: 154 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU419: - cost: 172 - init_stock: 1200 - price: 192 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU42: - cost: 95 - init_stock: 2050 - price: 106 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU420: - cost: 71 - init_stock: 3650 - price: 79 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU421: - cost: 259 - init_stock: 3200 - price: 288 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU422: - cost: 72 - init_stock: 3550 - price: 81 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU423: - cost: 43 - init_stock: 3950 - price: 48 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU424: - cost: 309 - init_stock: 1600 - price: 344 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU425: - cost: 139 - init_stock: 1300 - price: 155 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU426: - cost: 103 - init_stock: 4100 - price: 115 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU427: - cost: 118 - init_stock: 250 - price: 132 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU428: - cost: 449 - init_stock: 2900 - price: 499 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU429: - cost: 132 - init_stock: 800 - price: 147 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU43: - cost: 145 - init_stock: 4050 - price: 162 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU430: - cost: 297 - init_stock: 1450 - price: 330 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU431: - cost: 58 - init_stock: 3450 - price: 65 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU432: - cost: 175 - init_stock: 4650 - price: 195 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU433: - cost: 49 - init_stock: 4300 - price: 55 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU434: - cost: 101 - init_stock: 3750 - price: 113 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU435: - cost: 230 - init_stock: 750 - price: 256 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU436: - cost: 54 - init_stock: 4900 - price: 61 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU437: - cost: 330 - init_stock: 2100 - price: 367 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU438: - cost: 356 - init_stock: 3600 - price: 396 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU439: - cost: 89 - init_stock: 3100 - price: 99 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU44: - cost: 216 - init_stock: 2950 - price: 241 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU440: - cost: 409 - init_stock: 2550 - price: 455 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU441: - cost: 195 - init_stock: 3250 - price: 217 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU442: - cost: 414 - init_stock: 2900 - price: 460 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU443: - cost: 337 - init_stock: 2400 - price: 375 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU444: - cost: 39 - init_stock: 4900 - price: 44 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU445: - cost: 53 - init_stock: 3400 - price: 59 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU446: - cost: 164 - init_stock: 1000 - price: 183 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU447: - cost: 395 - init_stock: 2400 - price: 439 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU448: - cost: 449 - init_stock: 1400 - price: 499 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU449: - cost: 299 - init_stock: 3700 - price: 333 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU45: - cost: 120 - init_stock: 2400 - price: 134 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU450: - cost: 266 - init_stock: 1550 - price: 296 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU451: - cost: 290 - init_stock: 850 - price: 323 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU452: - cost: 46 - init_stock: 2000 - price: 52 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU453: - cost: 18 - init_stock: 4050 - price: 21 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU454: - cost: 178 - init_stock: 1150 - price: 198 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU455: - cost: 9 - init_stock: 3200 - price: 11 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU456: - cost: 183 - init_stock: 4150 - price: 204 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU457: - cost: 174 - init_stock: 5000 - price: 194 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU458: - cost: 99 - init_stock: 550 - price: 110 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU459: - cost: 333 - init_stock: 3200 - price: 370 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU46: - cost: 81 - init_stock: 4950 - price: 90 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU460: - cost: 108 - init_stock: 4050 - price: 121 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU461: - cost: 274 - init_stock: 1100 - price: 305 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU462: - cost: 339 - init_stock: 3300 - price: 377 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU463: - cost: 404 - init_stock: 400 - price: 449 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU464: - cost: 419 - init_stock: 1700 - price: 466 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU465: - cost: 77 - init_stock: 3050 - price: 86 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU466: - cost: 370 - init_stock: 4700 - price: 412 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU467: - cost: 92 - init_stock: 3550 - price: 103 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU468: - cost: 407 - init_stock: 4400 - price: 453 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU469: - cost: 379 - init_stock: 4650 - price: 422 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU47: - cost: 45 - init_stock: 1850 - price: 51 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU470: - cost: 255 - init_stock: 1400 - price: 284 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU471: - cost: 438 - init_stock: 1150 - price: 487 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU472: - cost: 418 - init_stock: 4800 - price: 465 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU473: - cost: 243 - init_stock: 1300 - price: 271 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU474: - cost: 61 - init_stock: 1250 - price: 68 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU475: - cost: 288 - init_stock: 3600 - price: 320 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU476: - cost: 213 - init_stock: 2450 - price: 237 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU477: - cost: 409 - init_stock: 3450 - price: 455 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU478: - cost: 36 - init_stock: 3700 - price: 40 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU479: - cost: 265 - init_stock: 2150 - price: 295 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU48: - cost: 355 - init_stock: 4550 - price: 395 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU480: - cost: 84 - init_stock: 3250 - price: 94 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU481: - cost: 367 - init_stock: 1050 - price: 408 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU482: - cost: 178 - init_stock: 4350 - price: 198 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU483: - cost: 76 - init_stock: 4100 - price: 85 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU484: - cost: 414 - init_stock: 2100 - price: 460 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU485: - cost: 277 - init_stock: 1750 - price: 308 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU486: - cost: 75 - init_stock: 3050 - price: 84 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU487: - cost: 378 - init_stock: 3100 - price: 421 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU488: - cost: 97 - init_stock: 1450 - price: 108 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU489: - cost: 131 - init_stock: 1500 - price: 146 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU49: - cost: 288 - init_stock: 2200 - price: 320 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU490: - cost: 38 - init_stock: 950 - price: 43 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU491: - cost: 120 - init_stock: 4450 - price: 134 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU492: - cost: 333 - init_stock: 4300 - price: 370 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU493: - cost: 309 - init_stock: 1000 - price: 344 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU494: - cost: 78 - init_stock: 2450 - price: 87 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU495: - cost: 164 - init_stock: 4250 - price: 183 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU496: - cost: 199 - init_stock: 3200 - price: 222 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU497: - cost: 407 - init_stock: 1800 - price: 453 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU498: - cost: 427 - init_stock: 1250 - price: 475 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU499: - cost: 109 - init_stock: 1650 - price: 122 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU5: - cost: 108 - init_stock: 3800 - price: 121 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU50: - cost: 268 - init_stock: 2850 - price: 298 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU500: - cost: 114 - init_stock: 4350 - price: 127 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU501: - cost: 295 - init_stock: 4150 - price: 328 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU502: - cost: 301 - init_stock: 1600 - price: 335 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU503: - cost: 77 - init_stock: 300 - price: 86 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU504: - cost: 12 - init_stock: 3250 - price: 14 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU505: - cost: 66 - init_stock: 3950 - price: 74 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU506: - cost: 45 - init_stock: 2500 - price: 51 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU507: - cost: 33 - init_stock: 850 - price: 37 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU508: - cost: 400 - init_stock: 4750 - price: 445 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU509: - cost: 282 - init_stock: 700 - price: 314 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU51: - cost: 296 - init_stock: 1650 - price: 329 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU510: - cost: 207 - init_stock: 3100 - price: 230 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU511: - cost: 18 - init_stock: 1700 - price: 21 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU512: - cost: 381 - init_stock: 1650 - price: 424 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU513: - cost: 24 - init_stock: 300 - price: 27 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU514: - cost: 70 - init_stock: 2550 - price: 78 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU515: - cost: 441 - init_stock: 4600 - price: 490 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU516: - cost: 92 - init_stock: 750 - price: 103 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU517: - cost: 178 - init_stock: 2050 - price: 198 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU518: - cost: 442 - init_stock: 750 - price: 492 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU519: - cost: 441 - init_stock: 1950 - price: 490 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU52: - cost: 164 - init_stock: 3150 - price: 183 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU520: - cost: 360 - init_stock: 4300 - price: 400 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU521: - cost: 190 - init_stock: 3100 - price: 212 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU522: - cost: 295 - init_stock: 1950 - price: 328 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU523: - cost: 20 - init_stock: 2500 - price: 23 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU524: - cost: 82 - init_stock: 3750 - price: 92 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU525: - cost: 234 - init_stock: 4200 - price: 261 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU526: - cost: 113 - init_stock: 1850 - price: 126 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU527: - cost: 37 - init_stock: 700 - price: 42 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU528: - cost: 19 - init_stock: 4500 - price: 22 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU529: - cost: 243 - init_stock: 4050 - price: 271 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU53: - cost: 394 - init_stock: 3850 - price: 438 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU530: - cost: 57 - init_stock: 3250 - price: 64 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU531: - cost: 245 - init_stock: 3050 - price: 273 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU532: - cost: 392 - init_stock: 3900 - price: 436 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU533: - cost: 364 - init_stock: 1050 - price: 405 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU534: - cost: 337 - init_stock: 4150 - price: 375 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU535: - cost: 380 - init_stock: 3900 - price: 423 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU536: - cost: 266 - init_stock: 2900 - price: 296 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU537: - cost: 428 - init_stock: 1450 - price: 476 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU538: - cost: 405 - init_stock: 1150 - price: 451 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU539: - cost: 334 - init_stock: 2200 - price: 372 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU54: - cost: 126 - init_stock: 2950 - price: 141 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU540: - cost: 22 - init_stock: 3050 - price: 25 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU541: - cost: 207 - init_stock: 2450 - price: 230 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU542: - cost: 282 - init_stock: 2350 - price: 314 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU543: - cost: 378 - init_stock: 4650 - price: 421 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU544: - cost: 220 - init_stock: 700 - price: 245 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU545: - cost: 291 - init_stock: 1500 - price: 324 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU546: - cost: 219 - init_stock: 500 - price: 244 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU547: - cost: 201 - init_stock: 900 - price: 224 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU548: - cost: 162 - init_stock: 1450 - price: 181 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU549: - cost: 342 - init_stock: 4200 - price: 380 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU55: - cost: 272 - init_stock: 4450 - price: 303 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU550: - cost: 306 - init_stock: 1100 - price: 340 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU551: - cost: 409 - init_stock: 2400 - price: 455 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU552: - cost: 217 - init_stock: 3750 - price: 242 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU553: - cost: 198 - init_stock: 2300 - price: 221 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU554: - cost: 43 - init_stock: 3250 - price: 48 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU555: - cost: 187 - init_stock: 2650 - price: 208 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU556: - cost: 446 - init_stock: 2300 - price: 496 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU557: - cost: 247 - init_stock: 4750 - price: 275 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU558: - cost: 307 - init_stock: 3650 - price: 342 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU559: - cost: 193 - init_stock: 4800 - price: 215 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU56: - cost: 401 - init_stock: 5000 - price: 446 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU560: - cost: 326 - init_stock: 300 - price: 363 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU561: - cost: 142 - init_stock: 1850 - price: 158 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU562: - cost: 234 - init_stock: 3300 - price: 260 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU563: - cost: 418 - init_stock: 1050 - price: 465 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU564: - cost: 320 - init_stock: 550 - price: 356 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU565: - cost: 431 - init_stock: 2400 - price: 479 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU566: - cost: 396 - init_stock: 3450 - price: 441 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU567: - cost: 79 - init_stock: 2900 - price: 88 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU568: - cost: 189 - init_stock: 1350 - price: 210 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU569: - cost: 388 - init_stock: 2100 - price: 432 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU57: - cost: 235 - init_stock: 3500 - price: 262 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU570: - cost: 128 - init_stock: 500 - price: 143 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU571: - cost: 183 - init_stock: 3200 - price: 204 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU572: - cost: 261 - init_stock: 2850 - price: 291 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU573: - cost: 412 - init_stock: 1250 - price: 458 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU574: - cost: 218 - init_stock: 4700 - price: 243 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU575: - cost: 295 - init_stock: 3600 - price: 328 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU576: - cost: 299 - init_stock: 3500 - price: 333 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU577: - cost: 331 - init_stock: 750 - price: 368 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU578: - cost: 73 - init_stock: 3000 - price: 82 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU579: - cost: 334 - init_stock: 950 - price: 372 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU58: - cost: 368 - init_stock: 3350 - price: 409 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU580: - cost: 378 - init_stock: 2700 - price: 421 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU581: - cost: 261 - init_stock: 2850 - price: 290 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU582: - cost: 117 - init_stock: 250 - price: 130 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU583: - cost: 154 - init_stock: 1400 - price: 172 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU584: - cost: 155 - init_stock: 4250 - price: 173 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU585: - cost: 180 - init_stock: 3950 - price: 201 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU586: - cost: 135 - init_stock: 2600 - price: 150 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU587: - cost: 178 - init_stock: 1800 - price: 198 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU588: - cost: 225 - init_stock: 2950 - price: 250 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU589: - cost: 45 - init_stock: 4550 - price: 51 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU59: - cost: 9 - init_stock: 600 - price: 11 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU590: - cost: 442 - init_stock: 900 - price: 492 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU591: - cost: 239 - init_stock: 4500 - price: 266 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU592: - cost: 263 - init_stock: 3950 - price: 293 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU593: - cost: 324 - init_stock: 4950 - price: 361 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU594: - cost: 210 - init_stock: 650 - price: 234 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU595: - cost: 33 - init_stock: 2200 - price: 37 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU596: - cost: 220 - init_stock: 4050 - price: 245 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU597: - cost: 24 - init_stock: 1950 - price: 27 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU598: - cost: 261 - init_stock: 2300 - price: 290 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU599: - cost: 404 - init_stock: 2050 - price: 449 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU6: - cost: 81 - init_stock: 850 - price: 90 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU60: - cost: 430 - init_stock: 4450 - price: 478 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU600: - cost: 369 - init_stock: 1200 - price: 411 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU601: - cost: 326 - init_stock: 2250 - price: 363 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU602: - cost: 316 - init_stock: 2450 - price: 352 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU603: - cost: 32 - init_stock: 2950 - price: 36 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU604: - cost: 297 - init_stock: 3900 - price: 330 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU605: - cost: 378 - init_stock: 4650 - price: 420 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU606: - cost: 381 - init_stock: 3200 - price: 424 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU607: - cost: 129 - init_stock: 550 - price: 144 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU608: - cost: 31 - init_stock: 2200 - price: 35 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU609: - cost: 141 - init_stock: 2000 - price: 157 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU61: - cost: 285 - init_stock: 4150 - price: 317 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU610: - cost: 55 - init_stock: 600 - price: 62 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU611: - cost: 232 - init_stock: 4550 - price: 258 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU612: - cost: 51 - init_stock: 3600 - price: 57 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU613: - cost: 398 - init_stock: 2150 - price: 443 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU614: - cost: 223 - init_stock: 4300 - price: 248 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU615: - cost: 233 - init_stock: 3000 - price: 259 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU616: - cost: 169 - init_stock: 4400 - price: 188 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU617: - cost: 300 - init_stock: 2050 - price: 334 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU618: - cost: 78 - init_stock: 1550 - price: 87 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU619: - cost: 193 - init_stock: 1750 - price: 215 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU62: - cost: 141 - init_stock: 3650 - price: 157 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU620: - cost: 10 - init_stock: 5000 - price: 12 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU621: - cost: 326 - init_stock: 800 - price: 363 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU622: - cost: 136 - init_stock: 3650 - price: 152 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU623: - cost: 236 - init_stock: 500 - price: 263 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU624: - cost: 380 - init_stock: 4800 - price: 423 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU625: - cost: 264 - init_stock: 4900 - price: 294 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU626: - cost: 183 - init_stock: 2900 - price: 204 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU627: - cost: 157 - init_stock: 4400 - price: 175 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU628: - cost: 12 - init_stock: 4100 - price: 14 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU629: - cost: 316 - init_stock: 3750 - price: 352 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU63: - cost: 274 - init_stock: 300 - price: 305 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU630: - cost: 366 - init_stock: 3950 - price: 407 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU631: - cost: 333 - init_stock: 1650 - price: 370 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU632: - cost: 440 - init_stock: 750 - price: 489 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU633: - cost: 268 - init_stock: 3250 - price: 298 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU634: - cost: 46 - init_stock: 3700 - price: 52 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU635: - cost: 27 - init_stock: 4850 - price: 30 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU636: - cost: 34 - init_stock: 2500 - price: 38 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU637: - cost: 426 - init_stock: 2050 - price: 474 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU638: - cost: 240 - init_stock: 2700 - price: 267 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU639: - cost: 419 - init_stock: 1300 - price: 466 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU64: - cost: 165 - init_stock: 2700 - price: 184 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU640: - cost: 212 - init_stock: 900 - price: 236 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU641: - cost: 256 - init_stock: 3050 - price: 285 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU642: - cost: 119 - init_stock: 4200 - price: 133 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU643: - cost: 320 - init_stock: 5000 - price: 356 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU644: - cost: 449 - init_stock: 1750 - price: 499 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU645: - cost: 428 - init_stock: 4700 - price: 476 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU646: - cost: 70 - init_stock: 1900 - price: 78 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU647: - cost: 46 - init_stock: 2950 - price: 52 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU648: - cost: 421 - init_stock: 4150 - price: 468 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU649: - cost: 310 - init_stock: 3500 - price: 345 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU65: - cost: 346 - init_stock: 1350 - price: 385 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU650: - cost: 117 - init_stock: 800 - price: 130 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU651: - cost: 223 - init_stock: 1100 - price: 248 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU652: - cost: 90 - init_stock: 4600 - price: 100 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU653: - cost: 407 - init_stock: 3950 - price: 453 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU654: - cost: 396 - init_stock: 1550 - price: 441 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU655: - cost: 131 - init_stock: 2700 - price: 146 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU656: - cost: 330 - init_stock: 1900 - price: 367 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU657: - cost: 175 - init_stock: 1200 - price: 195 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU658: - cost: 432 - init_stock: 4350 - price: 480 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU659: - cost: 447 - init_stock: 4800 - price: 497 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU66: - cost: 33 - init_stock: 3650 - price: 37 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU660: - cost: 146 - init_stock: 3300 - price: 163 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU661: - cost: 350 - init_stock: 600 - price: 389 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU662: - cost: 445 - init_stock: 4100 - price: 495 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU663: - cost: 414 - init_stock: 3250 - price: 460 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU664: - cost: 357 - init_stock: 600 - price: 397 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU665: - cost: 60 - init_stock: 3550 - price: 67 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU666: - cost: 113 - init_stock: 600 - price: 126 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU667: - cost: 126 - init_stock: 350 - price: 140 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU668: - cost: 99 - init_stock: 4950 - price: 110 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU669: - cost: 108 - init_stock: 3350 - price: 121 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU67: - cost: 48 - init_stock: 4300 - price: 54 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU670: - cost: 445 - init_stock: 250 - price: 495 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU671: - cost: 399 - init_stock: 500 - price: 444 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU672: - cost: 383 - init_stock: 3900 - price: 426 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU673: - cost: 98 - init_stock: 2700 - price: 109 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU674: - cost: 352 - init_stock: 400 - price: 392 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU675: - cost: 121 - init_stock: 1650 - price: 135 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU676: - cost: 360 - init_stock: 4250 - price: 401 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU677: - cost: 404 - init_stock: 2500 - price: 449 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU678: - cost: 187 - init_stock: 3050 - price: 208 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU679: - cost: 320 - init_stock: 4150 - price: 356 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU68: - cost: 56 - init_stock: 3700 - price: 63 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU680: - cost: 258 - init_stock: 1300 - price: 287 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU681: - cost: 126 - init_stock: 3500 - price: 140 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU682: - cost: 146 - init_stock: 4300 - price: 163 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU683: - cost: 323 - init_stock: 3800 - price: 359 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU684: - cost: 33 - init_stock: 1550 - price: 37 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU685: - cost: 95 - init_stock: 1600 - price: 106 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU686: - cost: 46 - init_stock: 2900 - price: 52 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU687: - cost: 76 - init_stock: 2950 - price: 85 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU688: - cost: 133 - init_stock: 1400 - price: 148 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU689: - cost: 299 - init_stock: 1650 - price: 333 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU69: - cost: 58 - init_stock: 3350 - price: 65 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU690: - cost: 334 - init_stock: 1000 - price: 372 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU691: - cost: 247 - init_stock: 1200 - price: 275 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU692: - cost: 260 - init_stock: 4000 - price: 289 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU693: - cost: 232 - init_stock: 3500 - price: 258 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU694: - cost: 424 - init_stock: 900 - price: 472 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU695: - cost: 286 - init_stock: 2400 - price: 318 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU696: - cost: 56 - init_stock: 4600 - price: 63 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU697: - cost: 13 - init_stock: 2700 - price: 15 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU698: - cost: 12 - init_stock: 2550 - price: 14 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU699: - cost: 321 - init_stock: 3650 - price: 357 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU7: - cost: 282 - init_stock: 2150 - price: 314 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU70: - cost: 186 - init_stock: 3850 - price: 207 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU700: - cost: 18 - init_stock: 2700 - price: 21 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU701: - cost: 117 - init_stock: 3950 - price: 130 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU702: - cost: 204 - init_stock: 3450 - price: 227 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU703: - cost: 65 - init_stock: 1750 - price: 73 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU704: - cost: 33 - init_stock: 1300 - price: 37 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU705: - cost: 281 - init_stock: 1050 - price: 313 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU706: - cost: 340 - init_stock: 700 - price: 378 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU707: - cost: 271 - init_stock: 5000 - price: 302 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU708: - cost: 62 - init_stock: 2900 - price: 69 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU709: - cost: 56 - init_stock: 3900 - price: 63 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU71: - cost: 225 - init_stock: 2050 - price: 251 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU710: - cost: 429 - init_stock: 3100 - price: 477 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU711: - cost: 355 - init_stock: 4900 - price: 395 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU712: - cost: 45 - init_stock: 3400 - price: 50 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU713: - cost: 201 - init_stock: 1250 - price: 224 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU714: - cost: 88 - init_stock: 2100 - price: 98 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU715: - cost: 449 - init_stock: 1650 - price: 499 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU716: - cost: 421 - init_stock: 1600 - price: 468 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU717: - cost: 103 - init_stock: 1250 - price: 115 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU718: - cost: 255 - init_stock: 1750 - price: 284 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU719: - cost: 150 - init_stock: 600 - price: 167 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU72: - cost: 69 - init_stock: 3650 - price: 77 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU720: - cost: 343 - init_stock: 250 - price: 382 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU721: - cost: 332 - init_stock: 2700 - price: 369 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU722: - cost: 418 - init_stock: 3500 - price: 465 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU723: - cost: 38 - init_stock: 2400 - price: 43 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU724: - cost: 221 - init_stock: 4600 - price: 246 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU725: - cost: 156 - init_stock: 1550 - price: 174 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU726: - cost: 351 - init_stock: 2650 - price: 391 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU727: - cost: 402 - init_stock: 3050 - price: 447 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU728: - cost: 312 - init_stock: 1500 - price: 347 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU729: - cost: 416 - init_stock: 4150 - price: 463 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU73: - cost: 146 - init_stock: 1650 - price: 163 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU730: - cost: 189 - init_stock: 750 - price: 211 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU731: - cost: 51 - init_stock: 2700 - price: 57 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU732: - cost: 95 - init_stock: 1200 - price: 106 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU733: - cost: 271 - init_stock: 950 - price: 302 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU734: - cost: 347 - init_stock: 700 - price: 386 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU735: - cost: 76 - init_stock: 1950 - price: 85 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU736: - cost: 14 - init_stock: 3350 - price: 16 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU737: - cost: 343 - init_stock: 4600 - price: 382 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU738: - cost: 386 - init_stock: 450 - price: 429 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU739: - cost: 256 - init_stock: 2250 - price: 285 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU74: - cost: 42 - init_stock: 4950 - price: 47 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU740: - cost: 427 - init_stock: 2900 - price: 475 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU741: - cost: 419 - init_stock: 3550 - price: 466 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU742: - cost: 397 - init_stock: 1350 - price: 442 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU743: - cost: 74 - init_stock: 1000 - price: 83 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU744: - cost: 306 - init_stock: 3600 - price: 340 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU745: - cost: 399 - init_stock: 2500 - price: 444 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU746: - cost: 446 - init_stock: 4800 - price: 496 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU747: - cost: 157 - init_stock: 3000 - price: 175 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU748: - cost: 414 - init_stock: 350 - price: 461 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU749: - cost: 432 - init_stock: 850 - price: 481 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU75: - cost: 287 - init_stock: 1600 - price: 319 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU750: - cost: 216 - init_stock: 2250 - price: 240 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU751: - cost: 271 - init_stock: 450 - price: 302 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU752: - cost: 382 - init_stock: 1650 - price: 425 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU753: - cost: 94 - init_stock: 2200 - price: 105 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU754: - cost: 445 - init_stock: 4650 - price: 495 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU755: - cost: 392 - init_stock: 1550 - price: 436 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU756: - cost: 210 - init_stock: 4750 - price: 234 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU757: - cost: 281 - init_stock: 650 - price: 313 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU758: - cost: 287 - init_stock: 1050 - price: 319 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU759: - cost: 281 - init_stock: 2050 - price: 313 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU76: - cost: 217 - init_stock: 3350 - price: 242 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU760: - cost: 322 - init_stock: 1300 - price: 358 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU761: - cost: 438 - init_stock: 800 - price: 487 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU762: - cost: 370 - init_stock: 250 - price: 412 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU763: - cost: 351 - init_stock: 1900 - price: 391 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU764: - cost: 114 - init_stock: 2450 - price: 127 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU765: - cost: 243 - init_stock: 1250 - price: 271 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU766: - cost: 207 - init_stock: 1150 - price: 230 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU767: - cost: 81 - init_stock: 4000 - price: 91 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU768: - cost: 369 - init_stock: 5000 - price: 410 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU769: - cost: 291 - init_stock: 2050 - price: 324 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU77: - cost: 288 - init_stock: 1700 - price: 321 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU770: - cost: 259 - init_stock: 2400 - price: 288 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU771: - cost: 47 - init_stock: 500 - price: 53 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU772: - cost: 184 - init_stock: 2750 - price: 205 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU773: - cost: 62 - init_stock: 1350 - price: 69 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU774: - cost: 450 - init_stock: 4800 - price: 500 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU775: - cost: 329 - init_stock: 2150 - price: 366 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU776: - cost: 417 - init_stock: 400 - price: 464 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU777: - cost: 443 - init_stock: 2700 - price: 493 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU778: - cost: 347 - init_stock: 1250 - price: 386 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU779: - cost: 134 - init_stock: 3100 - price: 149 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU78: - cost: 207 - init_stock: 2250 - price: 231 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU780: - cost: 243 - init_stock: 1750 - price: 271 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU781: - cost: 75 - init_stock: 2900 - price: 84 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU782: - cost: 130 - init_stock: 3500 - price: 145 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU783: - cost: 171 - init_stock: 1650 - price: 191 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU784: - cost: 30 - init_stock: 900 - price: 34 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU785: - cost: 331 - init_stock: 1400 - price: 368 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU786: - cost: 49 - init_stock: 1150 - price: 55 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU787: - cost: 331 - init_stock: 300 - price: 368 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU788: - cost: 231 - init_stock: 500 - price: 257 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU789: - cost: 196 - init_stock: 4250 - price: 218 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU79: - cost: 392 - init_stock: 3650 - price: 436 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU790: - cost: 386 - init_stock: 2650 - price: 429 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU791: - cost: 49 - init_stock: 3650 - price: 55 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU792: - cost: 72 - init_stock: 3500 - price: 80 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU793: - cost: 204 - init_stock: 550 - price: 227 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU794: - cost: 324 - init_stock: 1400 - price: 361 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU795: - cost: 180 - init_stock: 2400 - price: 200 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU796: - cost: 324 - init_stock: 350 - price: 360 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU797: - cost: 33 - init_stock: 4350 - price: 37 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU798: - cost: 111 - init_stock: 4750 - price: 124 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU799: - cost: 83 - init_stock: 900 - price: 93 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU8: - cost: 288 - init_stock: 1200 - price: 320 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU80: - cost: 284 - init_stock: 2100 - price: 316 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU800: - cost: 237 - init_stock: 3300 - price: 264 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU801: - cost: 55 - init_stock: 5000 - price: 62 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU802: - cost: 260 - init_stock: 550 - price: 289 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU803: - cost: 436 - init_stock: 3050 - price: 485 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU804: - cost: 113 - init_stock: 2900 - price: 126 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU805: - cost: 204 - init_stock: 3650 - price: 227 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU806: - cost: 53 - init_stock: 1700 - price: 59 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU807: - cost: 421 - init_stock: 3750 - price: 468 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU808: - cost: 144 - init_stock: 2700 - price: 161 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU809: - cost: 47 - init_stock: 3400 - price: 53 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU81: - cost: 172 - init_stock: 4900 - price: 192 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU810: - cost: 405 - init_stock: 2200 - price: 451 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU811: - cost: 288 - init_stock: 800 - price: 320 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU812: - cost: 263 - init_stock: 750 - price: 293 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU813: - cost: 35 - init_stock: 3950 - price: 39 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU814: - cost: 436 - init_stock: 3200 - price: 485 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU815: - cost: 47 - init_stock: 1800 - price: 53 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU816: - cost: 138 - init_stock: 2800 - price: 154 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU817: - cost: 235 - init_stock: 4750 - price: 262 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU818: - cost: 80 - init_stock: 3800 - price: 89 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU819: - cost: 295 - init_stock: 4500 - price: 328 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU82: - cost: 213 - init_stock: 4750 - price: 237 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU820: - cost: 414 - init_stock: 1500 - price: 461 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU821: - cost: 359 - init_stock: 4150 - price: 399 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU822: - cost: 54 - init_stock: 3600 - price: 60 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU823: - cost: 153 - init_stock: 4050 - price: 171 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU824: - cost: 48 - init_stock: 3050 - price: 54 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU825: - cost: 324 - init_stock: 2100 - price: 361 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU826: - cost: 368 - init_stock: 1300 - price: 409 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU827: - cost: 445 - init_stock: 3900 - price: 495 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU828: - cost: 87 - init_stock: 2250 - price: 97 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU829: - cost: 270 - init_stock: 2500 - price: 301 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU83: - cost: 328 - init_stock: 2950 - price: 365 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU830: - cost: 193 - init_stock: 650 - price: 215 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU831: - cost: 291 - init_stock: 2500 - price: 324 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU832: - cost: 106 - init_stock: 4650 - price: 118 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU833: - cost: 391 - init_stock: 1500 - price: 435 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU834: - cost: 361 - init_stock: 4750 - price: 402 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU835: - cost: 194 - init_stock: 2800 - price: 216 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU836: - cost: 13 - init_stock: 3700 - price: 15 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU837: - cost: 55 - init_stock: 3450 - price: 62 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU838: - cost: 121 - init_stock: 1200 - price: 135 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU839: - cost: 436 - init_stock: 4350 - price: 485 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU84: - cost: 18 - init_stock: 4100 - price: 21 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU840: - cost: 166 - init_stock: 1500 - price: 185 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU841: - cost: 189 - init_stock: 4900 - price: 211 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU842: - cost: 225 - init_stock: 500 - price: 251 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU843: - cost: 107 - init_stock: 3050 - price: 119 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU844: - cost: 107 - init_stock: 4800 - price: 119 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU845: - cost: 440 - init_stock: 3750 - price: 489 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU846: - cost: 450 - init_stock: 500 - price: 500 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU847: - cost: 273 - init_stock: 4600 - price: 304 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU848: - cost: 217 - init_stock: 4500 - price: 242 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU849: - cost: 29 - init_stock: 4800 - price: 33 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU85: - cost: 140 - init_stock: 3100 - price: 156 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU850: - cost: 404 - init_stock: 2550 - price: 449 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU851: - cost: 375 - init_stock: 2300 - price: 417 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU852: - cost: 246 - init_stock: 2600 - price: 274 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU853: - cost: 58 - init_stock: 2700 - price: 65 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU854: - cost: 201 - init_stock: 4600 - price: 224 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU855: - cost: 112 - init_stock: 350 - price: 125 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU856: - cost: 172 - init_stock: 2100 - price: 192 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU857: - cost: 162 - init_stock: 900 - price: 180 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU858: - cost: 353 - init_stock: 3950 - price: 393 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU859: - cost: 233 - init_stock: 3100 - price: 259 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU86: - cost: 289 - init_stock: 300 - price: 322 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU860: - cost: 258 - init_stock: 1500 - price: 287 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU861: - cost: 18 - init_stock: 1400 - price: 20 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU862: - cost: 77 - init_stock: 1150 - price: 86 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU863: - cost: 75 - init_stock: 3950 - price: 84 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU864: - cost: 258 - init_stock: 2150 - price: 287 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU865: - cost: 177 - init_stock: 1200 - price: 197 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU866: - cost: 353 - init_stock: 250 - price: 393 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU867: - cost: 207 - init_stock: 1750 - price: 231 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU868: - cost: 420 - init_stock: 3300 - price: 467 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU869: - cost: 102 - init_stock: 4600 - price: 114 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU87: - cost: 293 - init_stock: 450 - price: 326 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU870: - cost: 39 - init_stock: 550 - price: 44 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU871: - cost: 363 - init_stock: 4200 - price: 404 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU872: - cost: 261 - init_stock: 2700 - price: 291 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU873: - cost: 300 - init_stock: 1050 - price: 334 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU874: - cost: 327 - init_stock: 2400 - price: 364 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU875: - cost: 248 - init_stock: 4350 - price: 276 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU876: - cost: 307 - init_stock: 3500 - price: 342 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU877: - cost: 358 - init_stock: 4050 - price: 398 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU878: - cost: 117 - init_stock: 3400 - price: 131 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU879: - cost: 308 - init_stock: 400 - price: 343 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU88: - cost: 219 - init_stock: 1100 - price: 244 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU880: - cost: 446 - init_stock: 2200 - price: 496 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU881: - cost: 330 - init_stock: 3350 - price: 367 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU882: - cost: 77 - init_stock: 3150 - price: 86 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU883: - cost: 171 - init_stock: 2300 - price: 190 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU884: - cost: 180 - init_stock: 1450 - price: 200 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU885: - cost: 119 - init_stock: 2300 - price: 133 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU886: - cost: 196 - init_stock: 800 - price: 218 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU887: - cost: 193 - init_stock: 4400 - price: 215 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU888: - cost: 395 - init_stock: 3350 - price: 439 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU889: - cost: 94 - init_stock: 3400 - price: 105 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU89: - cost: 200 - init_stock: 350 - price: 223 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU890: - cost: 164 - init_stock: 1800 - price: 183 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU891: - cost: 76 - init_stock: 2200 - price: 85 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU892: - cost: 36 - init_stock: 4200 - price: 41 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU893: - cost: 121 - init_stock: 1100 - price: 135 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU894: - cost: 364 - init_stock: 3500 - price: 405 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU895: - cost: 288 - init_stock: 3100 - price: 320 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU896: - cost: 405 - init_stock: 2700 - price: 451 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU897: - cost: 126 - init_stock: 4800 - price: 141 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU898: - cost: 198 - init_stock: 4450 - price: 220 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU899: - cost: 101 - init_stock: 1900 - price: 113 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU9: - cost: 434 - init_stock: 2100 - price: 483 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU90: - cost: 195 - init_stock: 550 - price: 217 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU900: - cost: 196 - init_stock: 3500 - price: 218 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU901: - cost: 301 - init_stock: 4150 - price: 335 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU902: - cost: 106 - init_stock: 4950 - price: 118 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU903: - cost: 147 - init_stock: 550 - price: 164 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU904: - cost: 285 - init_stock: 1500 - price: 317 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU905: - cost: 415 - init_stock: 1550 - price: 462 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU906: - cost: 224 - init_stock: 2250 - price: 249 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU907: - cost: 185 - init_stock: 3800 - price: 206 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU908: - cost: 377 - init_stock: 3150 - price: 419 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU909: - cost: 372 - init_stock: 3200 - price: 414 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU91: - cost: 228 - init_stock: 4500 - price: 254 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU910: - cost: 333 - init_stock: 2600 - price: 371 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU911: - cost: 378 - init_stock: 3000 - price: 421 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU912: - cost: 147 - init_stock: 750 - price: 164 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU913: - cost: 18 - init_stock: 3850 - price: 20 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU914: - cost: 129 - init_stock: 2300 - price: 144 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU915: - cost: 192 - init_stock: 500 - price: 214 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU916: - cost: 105 - init_stock: 2400 - price: 117 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU917: - cost: 190 - init_stock: 1300 - price: 212 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU918: - cost: 286 - init_stock: 2450 - price: 318 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU919: - cost: 53 - init_stock: 550 - price: 59 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU92: - cost: 306 - init_stock: 3050 - price: 341 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU920: - cost: 144 - init_stock: 1200 - price: 161 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU921: - cost: 440 - init_stock: 4400 - price: 489 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU922: - cost: 31 - init_stock: 4550 - price: 35 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU923: - cost: 163 - init_stock: 400 - price: 182 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU924: - cost: 327 - init_stock: 2300 - price: 364 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU925: - cost: 27 - init_stock: 4750 - price: 31 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU926: - cost: 424 - init_stock: 500 - price: 472 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU927: - cost: 128 - init_stock: 250 - price: 143 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU928: - cost: 292 - init_stock: 1850 - price: 325 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU929: - cost: 266 - init_stock: 3800 - price: 296 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU93: - cost: 324 - init_stock: 4200 - price: 361 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU930: - cost: 231 - init_stock: 4250 - price: 257 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU931: - cost: 140 - init_stock: 1650 - price: 156 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU932: - cost: 124 - init_stock: 4950 - price: 138 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU933: - cost: 255 - init_stock: 1900 - price: 284 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU934: - cost: 191 - init_stock: 2150 - price: 213 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU935: - cost: 305 - init_stock: 1350 - price: 339 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU936: - cost: 153 - init_stock: 450 - price: 170 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU937: - cost: 110 - init_stock: 750 - price: 123 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU938: - cost: 269 - init_stock: 4550 - price: 299 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU939: - cost: 240 - init_stock: 1250 - price: 267 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU94: - cost: 401 - init_stock: 2450 - price: 446 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU940: - cost: 427 - init_stock: 2750 - price: 475 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU941: - cost: 181 - init_stock: 1200 - price: 202 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU942: - cost: 367 - init_stock: 3600 - price: 408 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU943: - cost: 170 - init_stock: 400 - price: 189 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU944: - cost: 444 - init_stock: 1700 - price: 494 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU945: - cost: 213 - init_stock: 850 - price: 237 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU946: - cost: 310 - init_stock: 4900 - price: 345 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU947: - cost: 445 - init_stock: 2700 - price: 495 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU948: - cost: 282 - init_stock: 3900 - price: 314 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU949: - cost: 407 - init_stock: 950 - price: 453 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU95: - cost: 22 - init_stock: 4850 - price: 25 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU950: - cost: 257 - init_stock: 3450 - price: 286 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU951: - cost: 150 - init_stock: 3500 - price: 167 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU952: - cost: 443 - init_stock: 2050 - price: 493 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU953: - cost: 424 - init_stock: 3950 - price: 472 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU954: - cost: 258 - init_stock: 700 - price: 287 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU955: - cost: 84 - init_stock: 750 - price: 94 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU956: - cost: 141 - init_stock: 1850 - price: 157 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU957: - cost: 273 - init_stock: 3150 - price: 304 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU958: - cost: 313 - init_stock: 2750 - price: 348 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU959: - cost: 397 - init_stock: 3950 - price: 442 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU96: - cost: 425 - init_stock: 2450 - price: 473 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU960: - cost: 24 - init_stock: 3800 - price: 27 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU961: - cost: 253 - init_stock: 3200 - price: 282 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU962: - cost: 351 - init_stock: 550 - price: 391 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU963: - cost: 222 - init_stock: 900 - price: 247 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU964: - cost: 445 - init_stock: 600 - price: 495 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU965: - cost: 385 - init_stock: 3700 - price: 428 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU966: - cost: 324 - init_stock: 2050 - price: 360 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU967: - cost: 366 - init_stock: 4050 - price: 407 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU968: - cost: 62 - init_stock: 3150 - price: 69 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU969: - cost: 117 - init_stock: 1600 - price: 131 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU97: - cost: 388 - init_stock: 2450 - price: 432 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU970: - cost: 338 - init_stock: 4800 - price: 376 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU971: - cost: 39 - init_stock: 700 - price: 44 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU972: - cost: 70 - init_stock: 400 - price: 78 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU973: - cost: 366 - init_stock: 2100 - price: 407 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU974: - cost: 390 - init_stock: 1350 - price: 434 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU975: - cost: 88 - init_stock: 4450 - price: 98 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU976: - cost: 236 - init_stock: 3050 - price: 263 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU977: - cost: 74 - init_stock: 1150 - price: 83 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU978: - cost: 81 - init_stock: 3850 - price: 90 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU979: - cost: 186 - init_stock: 650 - price: 207 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU98: - cost: 38 - init_stock: 4150 - price: 43 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU980: - cost: 204 - init_stock: 3000 - price: 227 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU981: - cost: 250 - init_stock: 850 - price: 278 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU982: - cost: 112 - init_stock: 500 - price: 125 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU983: - cost: 387 - init_stock: 3400 - price: 431 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU984: - cost: 53 - init_stock: 1400 - price: 59 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU985: - cost: 171 - init_stock: 3150 - price: 191 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU986: - cost: 212 - init_stock: 1900 - price: 236 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU987: - cost: 299 - init_stock: 4950 - price: 333 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU988: - cost: 366 - init_stock: 3850 - price: 407 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU989: - cost: 47 - init_stock: 4100 - price: 53 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU99: - cost: 58 - init_stock: 3950 - price: 65 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU990: - cost: 331 - init_stock: 4700 - price: 368 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU991: - cost: 313 - init_stock: 4600 - price: 348 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU992: - cost: 53 - init_stock: 3400 - price: 59 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU993: - cost: 448 - init_stock: 1150 - price: 498 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU994: - cost: 394 - init_stock: 1700 - price: 438 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU995: - cost: 262 - init_stock: 2450 - price: 292 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU996: - cost: 65 - init_stock: 2450 - price: 73 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU997: - cost: 288 - init_stock: 3550 - price: 321 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU998: - cost: 333 - init_stock: 750 - price: 371 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU999: - cost: 187 - init_stock: 4450 - price: 208 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5324500 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 200 + definition_ref: SupplierFacility + name: SUPPLIER0 + skus: + SKU0: + cost: 289 + init_stock: 3150 + price: 322 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU1: + cost: 255 + init_stock: 1100 + price: 284 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU10: + cost: 61 + init_stock: 1250 + price: 68 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU100: + cost: 14 + init_stock: 3250 + price: 16 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU101: + cost: 75 + init_stock: 3550 + price: 84 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU102: + cost: 295 + init_stock: 4650 + price: 328 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU103: + cost: 300 + init_stock: 4750 + price: 334 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU104: + cost: 44 + init_stock: 3250 + price: 49 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU105: + cost: 99 + init_stock: 2850 + price: 110 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU106: + cost: 225 + init_stock: 3650 + price: 251 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU107: + cost: 380 + init_stock: 4350 + price: 423 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU108: + cost: 412 + init_stock: 4600 + price: 458 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU109: + cost: 79 + init_stock: 4100 + price: 88 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU11: + cost: 360 + init_stock: 2550 + price: 400 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU110: + cost: 59 + init_stock: 700 + price: 66 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU111: + cost: 234 + init_stock: 3050 + price: 260 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU112: + cost: 54 + init_stock: 4750 + price: 61 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU113: + cost: 313 + init_stock: 1550 + price: 348 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU114: + cost: 350 + init_stock: 1350 + price: 389 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU115: + cost: 257 + init_stock: 4300 + price: 286 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU116: + cost: 446 + init_stock: 3600 + price: 496 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU117: + cost: 288 + init_stock: 4600 + price: 320 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU118: + cost: 164 + init_stock: 1650 + price: 183 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU119: + cost: 188 + init_stock: 1600 + price: 209 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU12: + cost: 100 + init_stock: 4200 + price: 112 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU120: + cost: 108 + init_stock: 4900 + price: 121 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU121: + cost: 36 + init_stock: 4250 + price: 40 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU122: + cost: 393 + init_stock: 350 + price: 437 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU123: + cost: 209 + init_stock: 950 + price: 233 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU124: + cost: 163 + init_stock: 1800 + price: 182 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU125: + cost: 14 + init_stock: 4600 + price: 16 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU126: + cost: 32 + init_stock: 1950 + price: 36 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU127: + cost: 195 + init_stock: 1550 + price: 217 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU128: + cost: 148 + init_stock: 950 + price: 165 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU129: + cost: 128 + init_stock: 2500 + price: 143 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU13: + cost: 285 + init_stock: 2850 + price: 317 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU130: + cost: 313 + init_stock: 4900 + price: 348 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU131: + cost: 57 + init_stock: 2250 + price: 64 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU132: + cost: 384 + init_stock: 1050 + price: 427 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU133: + cost: 201 + init_stock: 1450 + price: 224 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU134: + cost: 302 + init_stock: 3850 + price: 336 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU135: + cost: 137 + init_stock: 5000 + price: 153 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU136: + cost: 179 + init_stock: 3550 + price: 199 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU137: + cost: 83 + init_stock: 3700 + price: 93 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU138: + cost: 205 + init_stock: 1800 + price: 228 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU139: + cost: 186 + init_stock: 1200 + price: 207 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU14: + cost: 241 + init_stock: 3100 + price: 268 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU140: + cost: 234 + init_stock: 1700 + price: 261 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU141: + cost: 171 + init_stock: 2050 + price: 190 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU142: + cost: 288 + init_stock: 1900 + price: 320 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU143: + cost: 286 + init_stock: 1300 + price: 318 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU144: + cost: 360 + init_stock: 600 + price: 400 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU145: + cost: 359 + init_stock: 4100 + price: 399 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU146: + cost: 159 + init_stock: 2400 + price: 177 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU147: + cost: 424 + init_stock: 2800 + price: 472 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU148: + cost: 281 + init_stock: 3850 + price: 313 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU149: + cost: 321 + init_stock: 3850 + price: 357 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU15: + cost: 46 + init_stock: 250 + price: 52 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU150: + cost: 95 + init_stock: 3500 + price: 106 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU151: + cost: 200 + init_stock: 3650 + price: 223 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU152: + cost: 9 + init_stock: 2550 + price: 10 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU153: + cost: 396 + init_stock: 3100 + price: 441 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU154: + cost: 69 + init_stock: 4250 + price: 77 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU155: + cost: 379 + init_stock: 2650 + price: 422 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU156: + cost: 9 + init_stock: 600 + price: 10 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU157: + cost: 369 + init_stock: 3750 + price: 410 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU158: + cost: 130 + init_stock: 4050 + price: 145 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU159: + cost: 173 + init_stock: 1250 + price: 193 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU16: + cost: 157 + init_stock: 2900 + price: 175 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU160: + cost: 413 + init_stock: 4250 + price: 459 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU161: + cost: 215 + init_stock: 2300 + price: 239 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU162: + cost: 142 + init_stock: 250 + price: 158 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU163: + cost: 437 + init_stock: 1950 + price: 486 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU164: + cost: 446 + init_stock: 5000 + price: 496 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU165: + cost: 246 + init_stock: 1650 + price: 274 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU166: + cost: 71 + init_stock: 4450 + price: 79 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU167: + cost: 398 + init_stock: 650 + price: 443 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU168: + cost: 321 + init_stock: 4350 + price: 357 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU169: + cost: 332 + init_stock: 4900 + price: 369 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU17: + cost: 311 + init_stock: 450 + price: 346 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU170: + cost: 61 + init_stock: 2750 + price: 68 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU171: + cost: 358 + init_stock: 3800 + price: 398 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU172: + cost: 180 + init_stock: 3550 + price: 200 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU173: + cost: 157 + init_stock: 4800 + price: 175 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU174: + cost: 261 + init_stock: 3800 + price: 291 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU175: + cost: 75 + init_stock: 3750 + price: 84 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU176: + cost: 366 + init_stock: 3300 + price: 407 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU177: + cost: 231 + init_stock: 1550 + price: 257 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU178: + cost: 380 + init_stock: 250 + price: 423 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU179: + cost: 447 + init_stock: 4150 + price: 497 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU18: + cost: 232 + init_stock: 4050 + price: 258 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU180: + cost: 195 + init_stock: 2750 + price: 217 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU181: + cost: 128 + init_stock: 3000 + price: 143 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU182: + cost: 393 + init_stock: 4950 + price: 437 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU183: + cost: 130 + init_stock: 400 + price: 145 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU184: + cost: 65 + init_stock: 1200 + price: 73 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU185: + cost: 9 + init_stock: 4500 + price: 10 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU186: + cost: 323 + init_stock: 1100 + price: 359 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU187: + cost: 159 + init_stock: 1500 + price: 177 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU188: + cost: 351 + init_stock: 4350 + price: 391 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU189: + cost: 322 + init_stock: 1750 + price: 358 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU19: + cost: 429 + init_stock: 4550 + price: 477 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU190: + cost: 101 + init_stock: 850 + price: 113 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU191: + cost: 425 + init_stock: 2700 + price: 473 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU192: + cost: 373 + init_stock: 3050 + price: 415 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU193: + cost: 186 + init_stock: 1500 + price: 207 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU194: + cost: 388 + init_stock: 250 + price: 432 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU195: + cost: 196 + init_stock: 1550 + price: 218 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU196: + cost: 44 + init_stock: 3400 + price: 49 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU197: + cost: 272 + init_stock: 2850 + price: 303 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU198: + cost: 152 + init_stock: 2700 + price: 169 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU199: + cost: 404 + init_stock: 1150 + price: 449 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU2: + cost: 297 + init_stock: 3500 + price: 331 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU20: + cost: 301 + init_stock: 1250 + price: 335 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU200: + cost: 58 + init_stock: 1250 + price: 65 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU201: + cost: 93 + init_stock: 2950 + price: 104 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU202: + cost: 127 + init_stock: 3650 + price: 142 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU203: + cost: 396 + init_stock: 4100 + price: 440 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU204: + cost: 440 + init_stock: 2350 + price: 489 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU205: + cost: 117 + init_stock: 5000 + price: 130 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU206: + cost: 301 + init_stock: 550 + price: 335 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU207: + cost: 126 + init_stock: 4000 + price: 140 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU208: + cost: 441 + init_stock: 3850 + price: 491 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU209: + cost: 161 + init_stock: 1000 + price: 179 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU21: + cost: 110 + init_stock: 5000 + price: 123 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU210: + cost: 363 + init_stock: 3450 + price: 404 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU211: + cost: 156 + init_stock: 4550 + price: 174 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU212: + cost: 364 + init_stock: 3950 + price: 405 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU213: + cost: 108 + init_stock: 3200 + price: 121 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU214: + cost: 90 + init_stock: 500 + price: 101 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU215: + cost: 377 + init_stock: 2350 + price: 419 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU216: + cost: 297 + init_stock: 1150 + price: 330 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU217: + cost: 255 + init_stock: 3250 + price: 284 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU218: + cost: 184 + init_stock: 2950 + price: 205 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU219: + cost: 82 + init_stock: 2300 + price: 92 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU22: + cost: 443 + init_stock: 3300 + price: 493 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU220: + cost: 348 + init_stock: 4350 + price: 387 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU221: + cost: 35 + init_stock: 3900 + price: 39 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU222: + cost: 103 + init_stock: 1800 + price: 115 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU223: + cost: 176 + init_stock: 600 + price: 196 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU224: + cost: 348 + init_stock: 250 + price: 387 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU225: + cost: 147 + init_stock: 1450 + price: 164 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU226: + cost: 185 + init_stock: 650 + price: 206 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU227: + cost: 431 + init_stock: 1200 + price: 479 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU228: + cost: 12 + init_stock: 4500 + price: 14 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU229: + cost: 424 + init_stock: 2200 + price: 472 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU23: + cost: 348 + init_stock: 2100 + price: 387 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU230: + cost: 216 + init_stock: 1150 + price: 241 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU231: + cost: 43 + init_stock: 4050 + price: 48 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU232: + cost: 201 + init_stock: 4800 + price: 224 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU233: + cost: 324 + init_stock: 3750 + price: 360 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU234: + cost: 258 + init_stock: 250 + price: 287 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU235: + cost: 21 + init_stock: 2850 + price: 24 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU236: + cost: 139 + init_stock: 2750 + price: 155 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU237: + cost: 389 + init_stock: 2250 + price: 433 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU238: + cost: 57 + init_stock: 3300 + price: 64 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU239: + cost: 92 + init_stock: 4900 + price: 103 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU24: + cost: 87 + init_stock: 2350 + price: 97 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU240: + cost: 335 + init_stock: 2350 + price: 373 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU241: + cost: 395 + init_stock: 3550 + price: 439 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU242: + cost: 15 + init_stock: 2200 + price: 17 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU243: + cost: 316 + init_stock: 700 + price: 352 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU244: + cost: 156 + init_stock: 4100 + price: 174 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU245: + cost: 363 + init_stock: 3300 + price: 404 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU246: + cost: 270 + init_stock: 1500 + price: 300 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU247: + cost: 347 + init_stock: 1750 + price: 386 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU248: + cost: 319 + init_stock: 1450 + price: 355 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU249: + cost: 320 + init_stock: 1250 + price: 356 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU25: + cost: 144 + init_stock: 250 + price: 161 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU250: + cost: 114 + init_stock: 2700 + price: 127 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU251: + cost: 309 + init_stock: 3050 + price: 344 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU252: + cost: 162 + init_stock: 4150 + price: 181 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU253: + cost: 403 + init_stock: 800 + price: 448 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU254: + cost: 435 + init_stock: 2300 + price: 484 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU255: + cost: 261 + init_stock: 3350 + price: 290 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU256: + cost: 81 + init_stock: 3600 + price: 91 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU257: + cost: 313 + init_stock: 2850 + price: 348 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU258: + cost: 440 + init_stock: 2150 + price: 489 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU259: + cost: 299 + init_stock: 3450 + price: 333 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU26: + cost: 206 + init_stock: 3150 + price: 229 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU260: + cost: 438 + init_stock: 2600 + price: 487 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU261: + cost: 331 + init_stock: 1100 + price: 368 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU262: + cost: 298 + init_stock: 3900 + price: 332 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU263: + cost: 170 + init_stock: 3700 + price: 189 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU264: + cost: 324 + init_stock: 3950 + price: 361 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU265: + cost: 257 + init_stock: 2950 + price: 286 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU266: + cost: 115 + init_stock: 2350 + price: 128 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU267: + cost: 69 + init_stock: 4000 + price: 77 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU268: + cost: 198 + init_stock: 4450 + price: 221 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU269: + cost: 113 + init_stock: 2200 + price: 126 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU27: + cost: 333 + init_stock: 2800 + price: 370 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU270: + cost: 163 + init_stock: 3700 + price: 182 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU271: + cost: 207 + init_stock: 900 + price: 230 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU272: + cost: 329 + init_stock: 850 + price: 366 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU273: + cost: 378 + init_stock: 900 + price: 421 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU274: + cost: 26 + init_stock: 3850 + price: 29 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU275: + cost: 45 + init_stock: 2400 + price: 50 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU276: + cost: 146 + init_stock: 2700 + price: 163 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU277: + cost: 404 + init_stock: 2050 + price: 449 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU278: + cost: 94 + init_stock: 600 + price: 105 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU279: + cost: 45 + init_stock: 1950 + price: 51 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU28: + cost: 187 + init_stock: 2350 + price: 208 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU280: + cost: 265 + init_stock: 1650 + price: 295 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU281: + cost: 355 + init_stock: 3750 + price: 395 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU282: + cost: 56 + init_stock: 2300 + price: 63 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU283: + cost: 352 + init_stock: 4600 + price: 392 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU284: + cost: 309 + init_stock: 3350 + price: 344 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU285: + cost: 119 + init_stock: 4550 + price: 133 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU286: + cost: 303 + init_stock: 1950 + price: 337 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU287: + cost: 337 + init_stock: 2800 + price: 375 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU288: + cost: 162 + init_stock: 1900 + price: 181 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU289: + cost: 60 + init_stock: 1550 + price: 67 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU29: + cost: 220 + init_stock: 2900 + price: 245 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU290: + cost: 394 + init_stock: 3350 + price: 438 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU291: + cost: 84 + init_stock: 3050 + price: 94 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU292: + cost: 167 + init_stock: 250 + price: 186 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU293: + cost: 145 + init_stock: 2750 + price: 162 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU294: + cost: 368 + init_stock: 450 + price: 409 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU295: + cost: 391 + init_stock: 4650 + price: 435 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU296: + cost: 333 + init_stock: 4600 + price: 370 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU297: + cost: 268 + init_stock: 1900 + price: 298 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU298: + cost: 257 + init_stock: 1750 + price: 286 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU299: + cost: 330 + init_stock: 2550 + price: 367 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU3: + cost: 364 + init_stock: 600 + price: 405 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU30: + cost: 323 + init_stock: 3450 + price: 359 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU300: + cost: 338 + init_stock: 2900 + price: 376 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU301: + cost: 389 + init_stock: 4150 + price: 433 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU302: + cost: 165 + init_stock: 550 + price: 184 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU303: + cost: 221 + init_stock: 4700 + price: 246 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU304: + cost: 377 + init_stock: 1150 + price: 419 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU305: + cost: 445 + init_stock: 5000 + price: 495 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU306: + cost: 431 + init_stock: 2100 + price: 479 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU307: + cost: 189 + init_stock: 3900 + price: 210 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU308: + cost: 187 + init_stock: 250 + price: 208 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU309: + cost: 90 + init_stock: 4600 + price: 101 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU31: + cost: 62 + init_stock: 2800 + price: 69 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU310: + cost: 210 + init_stock: 2200 + price: 234 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU311: + cost: 275 + init_stock: 4000 + price: 306 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU312: + cost: 261 + init_stock: 1250 + price: 291 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU313: + cost: 291 + init_stock: 1900 + price: 324 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU314: + cost: 363 + init_stock: 1450 + price: 404 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU315: + cost: 423 + init_stock: 4200 + price: 471 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU316: + cost: 181 + init_stock: 900 + price: 202 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU317: + cost: 194 + init_stock: 1200 + price: 216 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU318: + cost: 250 + init_stock: 4250 + price: 278 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU319: + cost: 231 + init_stock: 2650 + price: 257 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU32: + cost: 302 + init_stock: 1100 + price: 336 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU320: + cost: 176 + init_stock: 1950 + price: 196 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU321: + cost: 60 + init_stock: 800 + price: 67 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU322: + cost: 216 + init_stock: 5000 + price: 240 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU323: + cost: 59 + init_stock: 1950 + price: 66 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU324: + cost: 397 + init_stock: 4650 + price: 442 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU325: + cost: 407 + init_stock: 3450 + price: 453 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU326: + cost: 310 + init_stock: 1200 + price: 345 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU327: + cost: 411 + init_stock: 700 + price: 457 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU328: + cost: 351 + init_stock: 2250 + price: 390 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU329: + cost: 60 + init_stock: 2100 + price: 67 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU33: + cost: 374 + init_stock: 4600 + price: 416 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU330: + cost: 275 + init_stock: 1950 + price: 306 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU331: + cost: 124 + init_stock: 2050 + price: 138 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU332: + cost: 351 + init_stock: 4800 + price: 390 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU333: + cost: 436 + init_stock: 2650 + price: 485 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU334: + cost: 164 + init_stock: 2850 + price: 183 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU335: + cost: 72 + init_stock: 4050 + price: 80 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU336: + cost: 266 + init_stock: 1400 + price: 296 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU337: + cost: 220 + init_stock: 1450 + price: 245 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU338: + cost: 78 + init_stock: 4050 + price: 87 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU339: + cost: 238 + init_stock: 3150 + price: 265 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU34: + cost: 85 + init_stock: 650 + price: 95 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU340: + cost: 432 + init_stock: 4350 + price: 480 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU341: + cost: 415 + init_stock: 3500 + price: 462 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU342: + cost: 129 + init_stock: 450 + price: 144 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU343: + cost: 279 + init_stock: 750 + price: 310 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU344: + cost: 321 + init_stock: 4350 + price: 357 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU345: + cost: 397 + init_stock: 4450 + price: 442 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU346: + cost: 315 + init_stock: 2100 + price: 350 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU347: + cost: 447 + init_stock: 4100 + price: 497 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU348: + cost: 132 + init_stock: 1000 + price: 147 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU349: + cost: 60 + init_stock: 3350 + price: 67 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU35: + cost: 240 + init_stock: 4300 + price: 267 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU350: + cost: 251 + init_stock: 4600 + price: 279 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU351: + cost: 139 + init_stock: 3350 + price: 155 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU352: + cost: 63 + init_stock: 900 + price: 71 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU353: + cost: 227 + init_stock: 4650 + price: 253 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU354: + cost: 356 + init_stock: 3950 + price: 396 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU355: + cost: 56 + init_stock: 500 + price: 63 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU356: + cost: 313 + init_stock: 1450 + price: 348 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU357: + cost: 449 + init_stock: 4600 + price: 499 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU358: + cost: 242 + init_stock: 3450 + price: 269 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU359: + cost: 73 + init_stock: 3750 + price: 82 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU36: + cost: 94 + init_stock: 500 + price: 105 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU360: + cost: 435 + init_stock: 1250 + price: 484 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU361: + cost: 146 + init_stock: 4500 + price: 163 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU362: + cost: 417 + init_stock: 4350 + price: 464 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU363: + cost: 46 + init_stock: 3900 + price: 52 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU364: + cost: 348 + init_stock: 1650 + price: 387 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU365: + cost: 142 + init_stock: 4150 + price: 158 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU366: + cost: 399 + init_stock: 4300 + price: 444 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU367: + cost: 244 + init_stock: 2300 + price: 272 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU368: + cost: 424 + init_stock: 1650 + price: 472 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU369: + cost: 86 + init_stock: 3750 + price: 96 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU37: + cost: 59 + init_stock: 2950 + price: 66 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU370: + cost: 100 + init_stock: 750 + price: 112 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU371: + cost: 295 + init_stock: 250 + price: 328 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU372: + cost: 60 + init_stock: 1600 + price: 67 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU373: + cost: 52 + init_stock: 3350 + price: 58 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU374: + cost: 34 + init_stock: 2700 + price: 38 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU375: + cost: 254 + init_stock: 3600 + price: 283 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU376: + cost: 140 + init_stock: 4100 + price: 156 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU377: + cost: 70 + init_stock: 3350 + price: 78 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU378: + cost: 381 + init_stock: 1750 + price: 424 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU379: + cost: 9 + init_stock: 2450 + price: 11 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU38: + cost: 309 + init_stock: 2150 + price: 344 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU380: + cost: 353 + init_stock: 2650 + price: 393 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU381: + cost: 428 + init_stock: 300 + price: 476 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU382: + cost: 112 + init_stock: 3550 + price: 125 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU383: + cost: 273 + init_stock: 4600 + price: 304 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU384: + cost: 288 + init_stock: 450 + price: 320 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU385: + cost: 108 + init_stock: 650 + price: 120 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU386: + cost: 265 + init_stock: 1550 + price: 295 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU387: + cost: 100 + init_stock: 4850 + price: 112 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU388: + cost: 364 + init_stock: 2200 + price: 405 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU389: + cost: 338 + init_stock: 3500 + price: 376 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU39: + cost: 22 + init_stock: 2550 + price: 25 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU390: + cost: 129 + init_stock: 1950 + price: 144 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU391: + cost: 209 + init_stock: 3350 + price: 233 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU392: + cost: 146 + init_stock: 3700 + price: 163 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU393: + cost: 438 + init_stock: 3350 + price: 487 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU394: + cost: 138 + init_stock: 2650 + price: 154 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU395: + cost: 439 + init_stock: 1650 + price: 488 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU396: + cost: 299 + init_stock: 3050 + price: 333 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU397: + cost: 414 + init_stock: 2550 + price: 460 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU398: + cost: 210 + init_stock: 2900 + price: 234 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU399: + cost: 338 + init_stock: 1850 + price: 376 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU4: + cost: 395 + init_stock: 350 + price: 439 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU40: + cost: 441 + init_stock: 4950 + price: 490 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU400: + cost: 400 + init_stock: 4200 + price: 445 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU401: + cost: 369 + init_stock: 3900 + price: 410 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU402: + cost: 77 + init_stock: 350 + price: 86 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU403: + cost: 80 + init_stock: 4950 + price: 89 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU404: + cost: 258 + init_stock: 3050 + price: 287 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU405: + cost: 414 + init_stock: 950 + price: 460 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU406: + cost: 294 + init_stock: 5000 + price: 327 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU407: + cost: 23 + init_stock: 2300 + price: 26 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU408: + cost: 399 + init_stock: 400 + price: 444 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU409: + cost: 231 + init_stock: 4550 + price: 257 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU41: + cost: 126 + init_stock: 3950 + price: 141 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU410: + cost: 63 + init_stock: 800 + price: 70 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU411: + cost: 189 + init_stock: 4750 + price: 210 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU412: + cost: 257 + init_stock: 3100 + price: 286 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU413: + cost: 362 + init_stock: 4150 + price: 403 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU414: + cost: 148 + init_stock: 4350 + price: 165 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU415: + cost: 261 + init_stock: 1150 + price: 291 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU416: + cost: 205 + init_stock: 450 + price: 228 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU417: + cost: 398 + init_stock: 3600 + price: 443 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU418: + cost: 412 + init_stock: 650 + price: 458 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU419: + cost: 272 + init_stock: 4450 + price: 303 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU42: + cost: 415 + init_stock: 1300 + price: 462 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU420: + cost: 241 + init_stock: 2100 + price: 268 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU421: + cost: 192 + init_stock: 2300 + price: 214 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU422: + cost: 46 + init_stock: 2700 + price: 52 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU423: + cost: 169 + init_stock: 3300 + price: 188 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU424: + cost: 170 + init_stock: 550 + price: 189 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU425: + cost: 145 + init_stock: 600 + price: 162 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU426: + cost: 112 + init_stock: 4900 + price: 125 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU427: + cost: 371 + init_stock: 4700 + price: 413 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU428: + cost: 351 + init_stock: 3150 + price: 391 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU429: + cost: 35 + init_stock: 2050 + price: 39 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU43: + cost: 195 + init_stock: 3400 + price: 217 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU430: + cost: 194 + init_stock: 3650 + price: 216 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU431: + cost: 295 + init_stock: 1050 + price: 328 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU432: + cost: 22 + init_stock: 2300 + price: 25 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU433: + cost: 313 + init_stock: 2250 + price: 348 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU434: + cost: 33 + init_stock: 1500 + price: 37 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU435: + cost: 244 + init_stock: 1500 + price: 272 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU436: + cost: 64 + init_stock: 4050 + price: 72 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU437: + cost: 103 + init_stock: 700 + price: 115 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU438: + cost: 128 + init_stock: 500 + price: 143 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU439: + cost: 230 + init_stock: 1900 + price: 256 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU44: + cost: 324 + init_stock: 2400 + price: 360 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU440: + cost: 223 + init_stock: 4100 + price: 248 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU441: + cost: 227 + init_stock: 2800 + price: 253 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU442: + cost: 423 + init_stock: 4400 + price: 470 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU443: + cost: 196 + init_stock: 3650 + price: 218 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU444: + cost: 140 + init_stock: 300 + price: 156 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU445: + cost: 234 + init_stock: 4300 + price: 260 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU446: + cost: 447 + init_stock: 2450 + price: 497 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU447: + cost: 176 + init_stock: 250 + price: 196 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU448: + cost: 436 + init_stock: 3550 + price: 485 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU449: + cost: 253 + init_stock: 1900 + price: 282 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU45: + cost: 227 + init_stock: 500 + price: 253 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU450: + cost: 52 + init_stock: 4900 + price: 58 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU451: + cost: 421 + init_stock: 3450 + price: 468 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU452: + cost: 56 + init_stock: 3950 + price: 63 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU453: + cost: 33 + init_stock: 1750 + price: 37 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU454: + cost: 444 + init_stock: 4250 + price: 494 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU455: + cost: 122 + init_stock: 1300 + price: 136 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU456: + cost: 304 + init_stock: 1250 + price: 338 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU457: + cost: 152 + init_stock: 3200 + price: 169 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU458: + cost: 362 + init_stock: 350 + price: 403 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU459: + cost: 382 + init_stock: 1700 + price: 425 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU46: + cost: 269 + init_stock: 2500 + price: 299 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU460: + cost: 364 + init_stock: 3650 + price: 405 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU461: + cost: 225 + init_stock: 2400 + price: 251 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU462: + cost: 403 + init_stock: 450 + price: 448 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU463: + cost: 168 + init_stock: 4100 + price: 187 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU464: + cost: 251 + init_stock: 1700 + price: 279 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU465: + cost: 132 + init_stock: 5000 + price: 147 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU466: + cost: 87 + init_stock: 2100 + price: 97 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU467: + cost: 127 + init_stock: 1800 + price: 142 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU468: + cost: 49 + init_stock: 2500 + price: 55 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU469: + cost: 160 + init_stock: 750 + price: 178 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU47: + cost: 108 + init_stock: 1950 + price: 121 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU470: + cost: 335 + init_stock: 1600 + price: 373 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU471: + cost: 283 + init_stock: 3550 + price: 315 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU472: + cost: 378 + init_stock: 2950 + price: 421 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU473: + cost: 175 + init_stock: 1050 + price: 195 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU474: + cost: 403 + init_stock: 4300 + price: 448 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU475: + cost: 30 + init_stock: 4700 + price: 34 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU476: + cost: 225 + init_stock: 4500 + price: 251 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU477: + cost: 260 + init_stock: 2350 + price: 289 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU478: + cost: 431 + init_stock: 4400 + price: 479 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU479: + cost: 329 + init_stock: 1900 + price: 366 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU48: + cost: 306 + init_stock: 2900 + price: 340 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU480: + cost: 16 + init_stock: 1500 + price: 18 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU481: + cost: 297 + init_stock: 4400 + price: 330 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU482: + cost: 84 + init_stock: 4350 + price: 94 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU483: + cost: 268 + init_stock: 1700 + price: 298 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU484: + cost: 75 + init_stock: 3650 + price: 84 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU485: + cost: 286 + init_stock: 3350 + price: 318 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU486: + cost: 109 + init_stock: 2600 + price: 122 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU487: + cost: 249 + init_stock: 3300 + price: 277 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU488: + cost: 32 + init_stock: 1350 + price: 36 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU489: + cost: 53 + init_stock: 1550 + price: 59 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU49: + cost: 236 + init_stock: 4750 + price: 263 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU490: + cost: 144 + init_stock: 4050 + price: 161 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU491: + cost: 399 + init_stock: 3750 + price: 444 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU492: + cost: 47 + init_stock: 4000 + price: 53 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU493: + cost: 414 + init_stock: 1350 + price: 461 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU494: + cost: 130 + init_stock: 4700 + price: 145 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU495: + cost: 134 + init_stock: 3100 + price: 149 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU496: + cost: 307 + init_stock: 2950 + price: 342 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU497: + cost: 29 + init_stock: 450 + price: 33 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU498: + cost: 274 + init_stock: 3250 + price: 305 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU499: + cost: 276 + init_stock: 1450 + price: 307 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU5: + cost: 419 + init_stock: 3550 + price: 466 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU50: + cost: 253 + init_stock: 1850 + price: 282 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU500: + cost: 187 + init_stock: 2100 + price: 208 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU501: + cost: 174 + init_stock: 3800 + price: 194 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU502: + cost: 62 + init_stock: 3250 + price: 69 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU503: + cost: 187 + init_stock: 2850 + price: 208 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU504: + cost: 225 + init_stock: 1500 + price: 251 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU505: + cost: 383 + init_stock: 2950 + price: 426 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU506: + cost: 134 + init_stock: 4650 + price: 149 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU507: + cost: 168 + init_stock: 750 + price: 187 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU508: + cost: 244 + init_stock: 4800 + price: 272 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU509: + cost: 267 + init_stock: 4050 + price: 297 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU51: + cost: 265 + init_stock: 3350 + price: 295 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU510: + cost: 84 + init_stock: 450 + price: 94 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU511: + cost: 324 + init_stock: 3750 + price: 361 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU512: + cost: 420 + init_stock: 1100 + price: 467 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU513: + cost: 308 + init_stock: 350 + price: 343 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU514: + cost: 167 + init_stock: 3150 + price: 186 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU515: + cost: 130 + init_stock: 2700 + price: 145 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU516: + cost: 57 + init_stock: 1900 + price: 64 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU517: + cost: 105 + init_stock: 450 + price: 117 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU518: + cost: 23 + init_stock: 1050 + price: 26 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU519: + cost: 209 + init_stock: 2500 + price: 233 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU52: + cost: 19 + init_stock: 3350 + price: 22 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU520: + cost: 188 + init_stock: 1100 + price: 209 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU521: + cost: 198 + init_stock: 2500 + price: 220 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU522: + cost: 140 + init_stock: 4350 + price: 156 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU523: + cost: 58 + init_stock: 5000 + price: 65 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU524: + cost: 264 + init_stock: 4700 + price: 294 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU525: + cost: 388 + init_stock: 2100 + price: 432 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU526: + cost: 377 + init_stock: 1200 + price: 419 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU527: + cost: 117 + init_stock: 3500 + price: 131 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU528: + cost: 284 + init_stock: 1050 + price: 316 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU529: + cost: 268 + init_stock: 1550 + price: 298 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU53: + cost: 135 + init_stock: 3500 + price: 151 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU530: + cost: 117 + init_stock: 2250 + price: 131 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU531: + cost: 92 + init_stock: 3000 + price: 103 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU532: + cost: 315 + init_stock: 3850 + price: 351 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU533: + cost: 266 + init_stock: 2100 + price: 296 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU534: + cost: 382 + init_stock: 2050 + price: 425 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU535: + cost: 273 + init_stock: 4300 + price: 304 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU536: + cost: 322 + init_stock: 2600 + price: 358 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU537: + cost: 288 + init_stock: 450 + price: 321 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU538: + cost: 411 + init_stock: 2500 + price: 457 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU539: + cost: 148 + init_stock: 3900 + price: 165 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU54: + cost: 142 + init_stock: 2300 + price: 158 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU540: + cost: 349 + init_stock: 2100 + price: 388 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU541: + cost: 117 + init_stock: 1450 + price: 131 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU542: + cost: 34 + init_stock: 1300 + price: 38 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU543: + cost: 387 + init_stock: 4250 + price: 430 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU544: + cost: 311 + init_stock: 4850 + price: 346 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU545: + cost: 157 + init_stock: 750 + price: 175 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU546: + cost: 441 + init_stock: 4800 + price: 491 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU547: + cost: 144 + init_stock: 2200 + price: 161 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU548: + cost: 99 + init_stock: 300 + price: 111 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU549: + cost: 348 + init_stock: 3950 + price: 387 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU55: + cost: 433 + init_stock: 3250 + price: 482 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU550: + cost: 233 + init_stock: 4700 + price: 259 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU551: + cost: 41 + init_stock: 1550 + price: 46 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU552: + cost: 171 + init_stock: 4500 + price: 191 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU553: + cost: 187 + init_stock: 1500 + price: 208 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU554: + cost: 306 + init_stock: 2050 + price: 340 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU555: + cost: 440 + init_stock: 4200 + price: 489 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU556: + cost: 99 + init_stock: 1500 + price: 110 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU557: + cost: 300 + init_stock: 3650 + price: 334 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU558: + cost: 359 + init_stock: 850 + price: 399 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU559: + cost: 281 + init_stock: 2700 + price: 313 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU56: + cost: 339 + init_stock: 3700 + price: 377 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU560: + cost: 254 + init_stock: 2050 + price: 283 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU561: + cost: 181 + init_stock: 1950 + price: 202 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU562: + cost: 312 + init_stock: 4450 + price: 347 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU563: + cost: 360 + init_stock: 250 + price: 401 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU564: + cost: 98 + init_stock: 450 + price: 109 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU565: + cost: 51 + init_stock: 3750 + price: 57 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU566: + cost: 117 + init_stock: 550 + price: 131 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU567: + cost: 324 + init_stock: 450 + price: 361 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU568: + cost: 67 + init_stock: 3900 + price: 75 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU569: + cost: 425 + init_stock: 2400 + price: 473 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU57: + cost: 323 + init_stock: 2500 + price: 359 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU570: + cost: 349 + init_stock: 4150 + price: 388 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU571: + cost: 151 + init_stock: 1950 + price: 168 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU572: + cost: 23 + init_stock: 2250 + price: 26 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU573: + cost: 221 + init_stock: 2050 + price: 246 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU574: + cost: 84 + init_stock: 2500 + price: 94 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU575: + cost: 213 + init_stock: 3950 + price: 237 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU576: + cost: 238 + init_stock: 3900 + price: 265 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU577: + cost: 16 + init_stock: 4350 + price: 18 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU578: + cost: 90 + init_stock: 3550 + price: 100 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU579: + cost: 373 + init_stock: 450 + price: 415 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU58: + cost: 325 + init_stock: 2400 + price: 362 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU580: + cost: 182 + init_stock: 250 + price: 203 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU581: + cost: 136 + init_stock: 4300 + price: 152 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU582: + cost: 323 + init_stock: 4800 + price: 359 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU583: + cost: 77 + init_stock: 4350 + price: 86 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU584: + cost: 29 + init_stock: 2250 + price: 33 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU585: + cost: 27 + init_stock: 1000 + price: 31 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU586: + cost: 54 + init_stock: 2200 + price: 61 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU587: + cost: 261 + init_stock: 3900 + price: 290 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU588: + cost: 428 + init_stock: 2200 + price: 476 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU589: + cost: 219 + init_stock: 1700 + price: 244 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU59: + cost: 130 + init_stock: 2550 + price: 145 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU590: + cost: 63 + init_stock: 1550 + price: 70 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU591: + cost: 185 + init_stock: 4200 + price: 206 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU592: + cost: 196 + init_stock: 2300 + price: 218 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU593: + cost: 111 + init_stock: 2200 + price: 124 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU594: + cost: 214 + init_stock: 2500 + price: 238 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU595: + cost: 65 + init_stock: 2150 + price: 73 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU596: + cost: 388 + init_stock: 350 + price: 432 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU597: + cost: 226 + init_stock: 4350 + price: 252 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU598: + cost: 313 + init_stock: 700 + price: 348 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU599: + cost: 48 + init_stock: 3400 + price: 54 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU6: + cost: 109 + init_stock: 4400 + price: 122 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU60: + cost: 180 + init_stock: 1950 + price: 200 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU600: + cost: 347 + init_stock: 3250 + price: 386 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU601: + cost: 124 + init_stock: 1950 + price: 138 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU602: + cost: 165 + init_stock: 4900 + price: 184 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU603: + cost: 250 + init_stock: 2100 + price: 278 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU604: + cost: 243 + init_stock: 2500 + price: 270 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU605: + cost: 259 + init_stock: 1200 + price: 288 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU606: + cost: 102 + init_stock: 550 + price: 114 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU607: + cost: 187 + init_stock: 3950 + price: 208 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU608: + cost: 35 + init_stock: 3650 + price: 39 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU609: + cost: 91 + init_stock: 950 + price: 102 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU61: + cost: 414 + init_stock: 3750 + price: 461 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU610: + cost: 404 + init_stock: 3400 + price: 449 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU611: + cost: 275 + init_stock: 3850 + price: 306 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU612: + cost: 351 + init_stock: 4400 + price: 391 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU613: + cost: 156 + init_stock: 350 + price: 174 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU614: + cost: 155 + init_stock: 750 + price: 173 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU615: + cost: 297 + init_stock: 4550 + price: 330 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU616: + cost: 208 + init_stock: 3650 + price: 232 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU617: + cost: 182 + init_stock: 3000 + price: 203 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU618: + cost: 69 + init_stock: 1150 + price: 77 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU619: + cost: 170 + init_stock: 4300 + price: 189 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU62: + cost: 111 + init_stock: 450 + price: 124 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU620: + cost: 207 + init_stock: 1950 + price: 231 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU621: + cost: 179 + init_stock: 3600 + price: 199 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU622: + cost: 227 + init_stock: 3250 + price: 253 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU623: + cost: 301 + init_stock: 2750 + price: 335 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU624: + cost: 433 + init_stock: 2700 + price: 482 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU625: + cost: 41 + init_stock: 4450 + price: 46 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU626: + cost: 405 + init_stock: 4450 + price: 451 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU627: + cost: 198 + init_stock: 4100 + price: 220 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU628: + cost: 111 + init_stock: 1300 + price: 124 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU629: + cost: 317 + init_stock: 4600 + price: 353 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU63: + cost: 61 + init_stock: 700 + price: 68 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU630: + cost: 109 + init_stock: 850 + price: 122 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU631: + cost: 266 + init_stock: 2450 + price: 296 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU632: + cost: 76 + init_stock: 4700 + price: 85 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU633: + cost: 165 + init_stock: 3350 + price: 184 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU634: + cost: 15 + init_stock: 3650 + price: 17 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU635: + cost: 306 + init_stock: 4650 + price: 341 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU636: + cost: 346 + init_stock: 1850 + price: 385 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU637: + cost: 74 + init_stock: 3050 + price: 83 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU638: + cost: 337 + init_stock: 400 + price: 375 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU639: + cost: 294 + init_stock: 2000 + price: 327 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU64: + cost: 32 + init_stock: 950 + price: 36 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU640: + cost: 247 + init_stock: 4550 + price: 275 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU641: + cost: 328 + init_stock: 4050 + price: 365 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU642: + cost: 372 + init_stock: 1250 + price: 414 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU643: + cost: 322 + init_stock: 2300 + price: 358 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU644: + cost: 41 + init_stock: 4100 + price: 46 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU645: + cost: 420 + init_stock: 4950 + price: 467 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU646: + cost: 170 + init_stock: 650 + price: 189 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU647: + cost: 272 + init_stock: 3850 + price: 303 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU648: + cost: 203 + init_stock: 2750 + price: 226 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU649: + cost: 178 + init_stock: 1250 + price: 198 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU65: + cost: 13 + init_stock: 4700 + price: 15 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU650: + cost: 315 + init_stock: 2800 + price: 351 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU651: + cost: 342 + init_stock: 4800 + price: 380 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU652: + cost: 110 + init_stock: 2000 + price: 123 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU653: + cost: 431 + init_stock: 4800 + price: 479 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU654: + cost: 59 + init_stock: 400 + price: 66 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU655: + cost: 299 + init_stock: 2100 + price: 333 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU656: + cost: 47 + init_stock: 3600 + price: 53 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU657: + cost: 65 + init_stock: 4050 + price: 73 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU658: + cost: 63 + init_stock: 1800 + price: 70 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU659: + cost: 18 + init_stock: 2800 + price: 21 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU66: + cost: 128 + init_stock: 4500 + price: 143 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU660: + cost: 438 + init_stock: 4150 + price: 487 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU661: + cost: 309 + init_stock: 3700 + price: 344 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU662: + cost: 334 + init_stock: 1900 + price: 372 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU663: + cost: 376 + init_stock: 2000 + price: 418 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU664: + cost: 315 + init_stock: 4200 + price: 351 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU665: + cost: 443 + init_stock: 2150 + price: 493 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU666: + cost: 306 + init_stock: 4400 + price: 341 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU667: + cost: 292 + init_stock: 650 + price: 325 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU668: + cost: 257 + init_stock: 3000 + price: 286 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU669: + cost: 66 + init_stock: 800 + price: 74 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU67: + cost: 222 + init_stock: 2650 + price: 247 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU670: + cost: 260 + init_stock: 4300 + price: 289 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU671: + cost: 240 + init_stock: 4150 + price: 267 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU672: + cost: 332 + init_stock: 650 + price: 369 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU673: + cost: 289 + init_stock: 2050 + price: 322 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU674: + cost: 200 + init_stock: 1100 + price: 223 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU675: + cost: 394 + init_stock: 1650 + price: 438 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU676: + cost: 219 + init_stock: 3050 + price: 244 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU677: + cost: 382 + init_stock: 2200 + price: 425 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU678: + cost: 151 + init_stock: 1800 + price: 168 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU679: + cost: 355 + init_stock: 2200 + price: 395 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU68: + cost: 152 + init_stock: 700 + price: 169 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU680: + cost: 234 + init_stock: 1500 + price: 260 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU681: + cost: 417 + init_stock: 4850 + price: 464 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU682: + cost: 333 + init_stock: 2650 + price: 370 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU683: + cost: 294 + init_stock: 3350 + price: 327 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU684: + cost: 319 + init_stock: 3950 + price: 355 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU685: + cost: 379 + init_stock: 2250 + price: 422 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU686: + cost: 56 + init_stock: 3150 + price: 63 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU687: + cost: 30 + init_stock: 3800 + price: 34 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU688: + cost: 435 + init_stock: 3850 + price: 484 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU689: + cost: 449 + init_stock: 750 + price: 499 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU69: + cost: 158 + init_stock: 3350 + price: 176 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU690: + cost: 393 + init_stock: 4150 + price: 437 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU691: + cost: 15 + init_stock: 3950 + price: 17 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU692: + cost: 202 + init_stock: 3250 + price: 225 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU693: + cost: 17 + init_stock: 3050 + price: 19 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU694: + cost: 187 + init_stock: 3750 + price: 208 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU695: + cost: 171 + init_stock: 350 + price: 190 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU696: + cost: 313 + init_stock: 2900 + price: 348 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU697: + cost: 409 + init_stock: 4000 + price: 455 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU698: + cost: 178 + init_stock: 3050 + price: 198 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU699: + cost: 27 + init_stock: 1000 + price: 31 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU7: + cost: 90 + init_stock: 4800 + price: 101 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU70: + cost: 167 + init_stock: 1750 + price: 186 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU700: + cost: 66 + init_stock: 450 + price: 74 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU701: + cost: 359 + init_stock: 3850 + price: 399 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU702: + cost: 73 + init_stock: 1700 + price: 82 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU703: + cost: 326 + init_stock: 1900 + price: 363 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU704: + cost: 345 + init_stock: 4350 + price: 384 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU705: + cost: 115 + init_stock: 3150 + price: 128 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU706: + cost: 163 + init_stock: 4550 + price: 182 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU707: + cost: 16 + init_stock: 3450 + price: 18 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU708: + cost: 160 + init_stock: 1400 + price: 178 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU709: + cost: 91 + init_stock: 2650 + price: 102 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU71: + cost: 283 + init_stock: 2250 + price: 315 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU710: + cost: 226 + init_stock: 950 + price: 252 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU711: + cost: 252 + init_stock: 3450 + price: 281 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU712: + cost: 208 + init_stock: 550 + price: 232 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU713: + cost: 256 + init_stock: 2150 + price: 285 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU714: + cost: 71 + init_stock: 2050 + price: 79 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU715: + cost: 210 + init_stock: 850 + price: 234 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU716: + cost: 63 + init_stock: 3750 + price: 70 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU717: + cost: 310 + init_stock: 400 + price: 345 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU718: + cost: 321 + init_stock: 2900 + price: 357 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU719: + cost: 306 + init_stock: 3250 + price: 340 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU72: + cost: 412 + init_stock: 4250 + price: 458 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU720: + cost: 292 + init_stock: 2100 + price: 325 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU721: + cost: 65 + init_stock: 550 + price: 73 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU722: + cost: 352 + init_stock: 4850 + price: 392 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU723: + cost: 286 + init_stock: 4450 + price: 318 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU724: + cost: 360 + init_stock: 1650 + price: 400 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU725: + cost: 157 + init_stock: 1850 + price: 175 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU726: + cost: 412 + init_stock: 4450 + price: 458 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU727: + cost: 376 + init_stock: 2850 + price: 418 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU728: + cost: 427 + init_stock: 1850 + price: 475 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU729: + cost: 291 + init_stock: 1800 + price: 324 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU73: + cost: 190 + init_stock: 2700 + price: 212 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU730: + cost: 14 + init_stock: 650 + price: 16 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU731: + cost: 79 + init_stock: 4100 + price: 88 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU732: + cost: 36 + init_stock: 3100 + price: 41 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU733: + cost: 283 + init_stock: 1300 + price: 315 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU734: + cost: 33 + init_stock: 2550 + price: 37 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU735: + cost: 239 + init_stock: 4950 + price: 266 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU736: + cost: 331 + init_stock: 1250 + price: 368 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU737: + cost: 427 + init_stock: 1550 + price: 475 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU738: + cost: 166 + init_stock: 2400 + price: 185 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU739: + cost: 427 + init_stock: 3700 + price: 475 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU74: + cost: 143 + init_stock: 2100 + price: 159 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU740: + cost: 351 + init_stock: 3200 + price: 390 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU741: + cost: 81 + init_stock: 2800 + price: 91 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU742: + cost: 169 + init_stock: 1100 + price: 188 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU743: + cost: 195 + init_stock: 2150 + price: 217 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU744: + cost: 341 + init_stock: 2900 + price: 379 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU745: + cost: 284 + init_stock: 4600 + price: 316 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU746: + cost: 393 + init_stock: 2000 + price: 437 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU747: + cost: 335 + init_stock: 3950 + price: 373 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU748: + cost: 247 + init_stock: 3300 + price: 275 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU749: + cost: 354 + init_stock: 2650 + price: 394 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU75: + cost: 117 + init_stock: 950 + price: 131 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU750: + cost: 230 + init_stock: 3100 + price: 256 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU751: + cost: 332 + init_stock: 3250 + price: 369 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU752: + cost: 233 + init_stock: 3900 + price: 259 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU753: + cost: 69 + init_stock: 3250 + price: 77 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU754: + cost: 348 + init_stock: 650 + price: 387 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU755: + cost: 318 + init_stock: 4000 + price: 354 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU756: + cost: 221 + init_stock: 4500 + price: 246 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU757: + cost: 36 + init_stock: 2850 + price: 40 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU758: + cost: 216 + init_stock: 2000 + price: 241 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU759: + cost: 391 + init_stock: 1900 + price: 435 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU76: + cost: 132 + init_stock: 3200 + price: 147 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU760: + cost: 158 + init_stock: 2550 + price: 176 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU761: + cost: 201 + init_stock: 2750 + price: 224 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU762: + cost: 237 + init_stock: 2550 + price: 264 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU763: + cost: 346 + init_stock: 3950 + price: 385 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU764: + cost: 314 + init_stock: 1700 + price: 349 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU765: + cost: 310 + init_stock: 650 + price: 345 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU766: + cost: 430 + init_stock: 1000 + price: 478 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU767: + cost: 85 + init_stock: 3800 + price: 95 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU768: + cost: 162 + init_stock: 4600 + price: 181 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU769: + cost: 21 + init_stock: 1450 + price: 24 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU77: + cost: 368 + init_stock: 3600 + price: 409 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU770: + cost: 135 + init_stock: 3100 + price: 150 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU771: + cost: 90 + init_stock: 350 + price: 101 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU772: + cost: 230 + init_stock: 300 + price: 256 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU773: + cost: 75 + init_stock: 4600 + price: 84 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU774: + cost: 402 + init_stock: 2950 + price: 447 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU775: + cost: 157 + init_stock: 4300 + price: 175 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU776: + cost: 92 + init_stock: 4250 + price: 103 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU777: + cost: 262 + init_stock: 2850 + price: 292 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU778: + cost: 182 + init_stock: 400 + price: 203 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU779: + cost: 105 + init_stock: 2150 + price: 117 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU78: + cost: 210 + init_stock: 650 + price: 234 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU780: + cost: 62 + init_stock: 4100 + price: 69 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU781: + cost: 334 + init_stock: 1800 + price: 372 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU782: + cost: 24 + init_stock: 500 + price: 27 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU783: + cost: 78 + init_stock: 4050 + price: 87 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU784: + cost: 278 + init_stock: 2600 + price: 309 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU785: + cost: 171 + init_stock: 3500 + price: 191 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU786: + cost: 81 + init_stock: 1100 + price: 91 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU787: + cost: 324 + init_stock: 3050 + price: 360 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU788: + cost: 315 + init_stock: 1400 + price: 351 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU789: + cost: 137 + init_stock: 2900 + price: 153 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU79: + cost: 220 + init_stock: 550 + price: 245 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU790: + cost: 375 + init_stock: 3300 + price: 417 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU791: + cost: 120 + init_stock: 1400 + price: 134 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU792: + cost: 281 + init_stock: 1050 + price: 313 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU793: + cost: 175 + init_stock: 3800 + price: 195 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU794: + cost: 292 + init_stock: 4000 + price: 325 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU795: + cost: 248 + init_stock: 2400 + price: 276 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU796: + cost: 402 + init_stock: 2450 + price: 447 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU797: + cost: 90 + init_stock: 1950 + price: 100 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU798: + cost: 383 + init_stock: 1500 + price: 426 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU799: + cost: 56 + init_stock: 350 + price: 63 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU8: + cost: 112 + init_stock: 3150 + price: 125 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU80: + cost: 146 + init_stock: 3500 + price: 163 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU800: + cost: 268 + init_stock: 4700 + price: 298 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU801: + cost: 350 + init_stock: 1800 + price: 389 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU802: + cost: 155 + init_stock: 3100 + price: 173 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU803: + cost: 244 + init_stock: 950 + price: 272 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU804: + cost: 351 + init_stock: 2350 + price: 390 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU805: + cost: 54 + init_stock: 1650 + price: 61 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU806: + cost: 142 + init_stock: 2600 + price: 158 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU807: + cost: 407 + init_stock: 1300 + price: 453 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU808: + cost: 224 + init_stock: 4550 + price: 249 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU809: + cost: 49 + init_stock: 3250 + price: 55 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU81: + cost: 163 + init_stock: 3300 + price: 182 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU810: + cost: 248 + init_stock: 300 + price: 276 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU811: + cost: 228 + init_stock: 1850 + price: 254 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU812: + cost: 226 + init_stock: 4000 + price: 252 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU813: + cost: 227 + init_stock: 350 + price: 253 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU814: + cost: 436 + init_stock: 4850 + price: 485 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU815: + cost: 351 + init_stock: 1200 + price: 390 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU816: + cost: 136 + init_stock: 4550 + price: 152 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU817: + cost: 204 + init_stock: 250 + price: 227 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU818: + cost: 318 + init_stock: 2150 + price: 354 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU819: + cost: 271 + init_stock: 1350 + price: 302 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU82: + cost: 261 + init_stock: 1400 + price: 290 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU820: + cost: 237 + init_stock: 4100 + price: 264 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU821: + cost: 89 + init_stock: 3450 + price: 99 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU822: + cost: 122 + init_stock: 3500 + price: 136 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU823: + cost: 67 + init_stock: 1400 + price: 75 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU824: + cost: 153 + init_stock: 3500 + price: 170 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU825: + cost: 192 + init_stock: 750 + price: 214 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU826: + cost: 347 + init_stock: 2750 + price: 386 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU827: + cost: 133 + init_stock: 3200 + price: 148 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU828: + cost: 360 + init_stock: 3450 + price: 400 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU829: + cost: 54 + init_stock: 3700 + price: 61 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU83: + cost: 266 + init_stock: 850 + price: 296 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU830: + cost: 150 + init_stock: 1200 + price: 167 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU831: + cost: 235 + init_stock: 1450 + price: 262 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU832: + cost: 29 + init_stock: 4100 + price: 33 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU833: + cost: 360 + init_stock: 4900 + price: 400 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU834: + cost: 379 + init_stock: 250 + price: 422 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU835: + cost: 396 + init_stock: 2600 + price: 440 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU836: + cost: 290 + init_stock: 4800 + price: 323 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU837: + cost: 335 + init_stock: 1300 + price: 373 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU838: + cost: 410 + init_stock: 3850 + price: 456 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU839: + cost: 425 + init_stock: 3000 + price: 473 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU84: + cost: 286 + init_stock: 2100 + price: 318 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU840: + cost: 239 + init_stock: 2500 + price: 266 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU841: + cost: 256 + init_stock: 4250 + price: 285 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU842: + cost: 36 + init_stock: 4200 + price: 41 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU843: + cost: 324 + init_stock: 3600 + price: 360 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU844: + cost: 45 + init_stock: 3950 + price: 51 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU845: + cost: 259 + init_stock: 1100 + price: 288 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU846: + cost: 436 + init_stock: 1950 + price: 485 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU847: + cost: 349 + init_stock: 4550 + price: 388 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU848: + cost: 275 + init_stock: 2300 + price: 306 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU849: + cost: 288 + init_stock: 2000 + price: 320 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU85: + cost: 397 + init_stock: 5000 + price: 442 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU850: + cost: 164 + init_stock: 2850 + price: 183 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU851: + cost: 47 + init_stock: 3200 + price: 53 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU852: + cost: 275 + init_stock: 2700 + price: 306 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU853: + cost: 347 + init_stock: 2800 + price: 386 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU854: + cost: 190 + init_stock: 3250 + price: 212 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU855: + cost: 68 + init_stock: 3600 + price: 76 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU856: + cost: 342 + init_stock: 1600 + price: 380 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU857: + cost: 415 + init_stock: 5000 + price: 462 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU858: + cost: 72 + init_stock: 1200 + price: 80 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU859: + cost: 193 + init_stock: 1400 + price: 215 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU86: + cost: 347 + init_stock: 4250 + price: 386 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU860: + cost: 281 + init_stock: 750 + price: 313 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU861: + cost: 429 + init_stock: 650 + price: 477 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU862: + cost: 216 + init_stock: 800 + price: 240 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU863: + cost: 423 + init_stock: 4650 + price: 470 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU864: + cost: 182 + init_stock: 950 + price: 203 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU865: + cost: 129 + init_stock: 950 + price: 144 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU866: + cost: 154 + init_stock: 4400 + price: 172 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU867: + cost: 449 + init_stock: 5000 + price: 499 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU868: + cost: 36 + init_stock: 2500 + price: 41 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU869: + cost: 87 + init_stock: 4850 + price: 97 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU87: + cost: 331 + init_stock: 3450 + price: 368 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU870: + cost: 252 + init_stock: 3350 + price: 281 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU871: + cost: 90 + init_stock: 4950 + price: 101 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU872: + cost: 119 + init_stock: 1600 + price: 133 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU873: + cost: 380 + init_stock: 1550 + price: 423 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU874: + cost: 422 + init_stock: 3000 + price: 469 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU875: + cost: 45 + init_stock: 1150 + price: 51 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU876: + cost: 272 + init_stock: 3050 + price: 303 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU877: + cost: 36 + init_stock: 1750 + price: 40 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU878: + cost: 24 + init_stock: 3400 + price: 27 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU879: + cost: 135 + init_stock: 5000 + price: 150 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU88: + cost: 423 + init_stock: 600 + price: 471 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU880: + cost: 389 + init_stock: 1550 + price: 433 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU881: + cost: 387 + init_stock: 3500 + price: 431 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU882: + cost: 174 + init_stock: 800 + price: 194 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU883: + cost: 255 + init_stock: 1900 + price: 284 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU884: + cost: 125 + init_stock: 1950 + price: 139 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU885: + cost: 44 + init_stock: 1350 + price: 49 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU886: + cost: 334 + init_stock: 3650 + price: 372 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU887: + cost: 239 + init_stock: 4350 + price: 266 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU888: + cost: 128 + init_stock: 450 + price: 143 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU889: + cost: 90 + init_stock: 1050 + price: 101 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU89: + cost: 124 + init_stock: 4650 + price: 138 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU890: + cost: 144 + init_stock: 5000 + price: 161 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU891: + cost: 320 + init_stock: 1100 + price: 356 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU892: + cost: 281 + init_stock: 4600 + price: 313 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU893: + cost: 206 + init_stock: 4950 + price: 229 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU894: + cost: 116 + init_stock: 3700 + price: 129 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU895: + cost: 207 + init_stock: 650 + price: 230 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU896: + cost: 260 + init_stock: 4000 + price: 289 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU897: + cost: 353 + init_stock: 3600 + price: 393 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU898: + cost: 429 + init_stock: 550 + price: 477 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU899: + cost: 209 + init_stock: 2400 + price: 233 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU9: + cost: 149 + init_stock: 4800 + price: 166 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU90: + cost: 408 + init_stock: 2550 + price: 454 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU900: + cost: 142 + init_stock: 2750 + price: 158 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU901: + cost: 193 + init_stock: 1450 + price: 215 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU902: + cost: 112 + init_stock: 4000 + price: 125 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU903: + cost: 321 + init_stock: 1900 + price: 357 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU904: + cost: 446 + init_stock: 2550 + price: 496 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU905: + cost: 224 + init_stock: 1550 + price: 249 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU906: + cost: 149 + init_stock: 2600 + price: 166 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU907: + cost: 19 + init_stock: 4150 + price: 22 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU908: + cost: 367 + init_stock: 3900 + price: 408 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU909: + cost: 433 + init_stock: 4800 + price: 482 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU91: + cost: 272 + init_stock: 800 + price: 303 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU910: + cost: 203 + init_stock: 1650 + price: 226 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU911: + cost: 414 + init_stock: 3500 + price: 461 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU912: + cost: 212 + init_stock: 1350 + price: 236 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU913: + cost: 289 + init_stock: 2300 + price: 322 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU914: + cost: 244 + init_stock: 3100 + price: 272 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU915: + cost: 303 + init_stock: 2800 + price: 337 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU916: + cost: 303 + init_stock: 4450 + price: 337 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU917: + cost: 232 + init_stock: 1500 + price: 258 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU918: + cost: 133 + init_stock: 2750 + price: 148 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU919: + cost: 353 + init_stock: 1250 + price: 393 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU92: + cost: 235 + init_stock: 3150 + price: 262 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU920: + cost: 321 + init_stock: 4300 + price: 357 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU921: + cost: 204 + init_stock: 3300 + price: 227 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU922: + cost: 100 + init_stock: 3350 + price: 112 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU923: + cost: 446 + init_stock: 2900 + price: 496 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU924: + cost: 284 + init_stock: 4250 + price: 316 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU925: + cost: 324 + init_stock: 750 + price: 360 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU926: + cost: 324 + init_stock: 3350 + price: 360 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU927: + cost: 234 + init_stock: 1050 + price: 260 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU928: + cost: 441 + init_stock: 4150 + price: 491 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU929: + cost: 323 + init_stock: 5000 + price: 359 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU93: + cost: 363 + init_stock: 3350 + price: 404 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU930: + cost: 178 + init_stock: 1400 + price: 198 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU931: + cost: 63 + init_stock: 700 + price: 71 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU932: + cost: 146 + init_stock: 3300 + price: 163 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU933: + cost: 101 + init_stock: 3900 + price: 113 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU934: + cost: 197 + init_stock: 3350 + price: 219 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU935: + cost: 327 + init_stock: 4700 + price: 364 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU936: + cost: 21 + init_stock: 250 + price: 24 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU937: + cost: 121 + init_stock: 850 + price: 135 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU938: + cost: 388 + init_stock: 1050 + price: 432 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU939: + cost: 155 + init_stock: 2950 + price: 173 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU94: + cost: 165 + init_stock: 2350 + price: 184 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU940: + cost: 12 + init_stock: 4650 + price: 14 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU941: + cost: 72 + init_stock: 2850 + price: 80 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU942: + cost: 181 + init_stock: 650 + price: 202 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU943: + cost: 124 + init_stock: 2450 + price: 138 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU944: + cost: 176 + init_stock: 2200 + price: 196 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU945: + cost: 126 + init_stock: 850 + price: 141 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU946: + cost: 292 + init_stock: 2450 + price: 325 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU947: + cost: 304 + init_stock: 4550 + price: 338 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU948: + cost: 382 + init_stock: 1400 + price: 425 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU949: + cost: 278 + init_stock: 250 + price: 309 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU95: + cost: 122 + init_stock: 1050 + price: 136 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU950: + cost: 91 + init_stock: 2700 + price: 102 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU951: + cost: 67 + init_stock: 900 + price: 75 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU952: + cost: 140 + init_stock: 550 + price: 156 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU953: + cost: 124 + init_stock: 1750 + price: 138 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU954: + cost: 266 + init_stock: 4300 + price: 296 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU955: + cost: 49 + init_stock: 3150 + price: 55 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU956: + cost: 253 + init_stock: 4250 + price: 282 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU957: + cost: 274 + init_stock: 2550 + price: 305 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU958: + cost: 332 + init_stock: 3300 + price: 369 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU959: + cost: 72 + init_stock: 4100 + price: 81 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU96: + cost: 158 + init_stock: 2200 + price: 176 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU960: + cost: 132 + init_stock: 300 + price: 147 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU961: + cost: 237 + init_stock: 2200 + price: 264 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU962: + cost: 318 + init_stock: 2200 + price: 354 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU963: + cost: 314 + init_stock: 1050 + price: 349 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU964: + cost: 219 + init_stock: 3650 + price: 244 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU965: + cost: 111 + init_stock: 2550 + price: 124 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU966: + cost: 271 + init_stock: 2200 + price: 302 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU967: + cost: 60 + init_stock: 2250 + price: 67 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU968: + cost: 252 + init_stock: 2450 + price: 281 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU969: + cost: 224 + init_stock: 300 + price: 249 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU97: + cost: 25 + init_stock: 1500 + price: 28 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU970: + cost: 219 + init_stock: 250 + price: 244 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU971: + cost: 331 + init_stock: 3650 + price: 368 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU972: + cost: 188 + init_stock: 3450 + price: 209 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU973: + cost: 243 + init_stock: 550 + price: 271 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU974: + cost: 153 + init_stock: 1600 + price: 170 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU975: + cost: 178 + init_stock: 1100 + price: 198 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU976: + cost: 160 + init_stock: 750 + price: 178 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU977: + cost: 210 + init_stock: 2700 + price: 234 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU978: + cost: 423 + init_stock: 3200 + price: 470 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU979: + cost: 398 + init_stock: 4800 + price: 443 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU98: + cost: 398 + init_stock: 1750 + price: 443 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU980: + cost: 35 + init_stock: 3400 + price: 39 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU981: + cost: 433 + init_stock: 3600 + price: 482 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU982: + cost: 191 + init_stock: 2600 + price: 213 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU983: + cost: 404 + init_stock: 4600 + price: 449 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU984: + cost: 208 + init_stock: 4550 + price: 232 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU985: + cost: 261 + init_stock: 2550 + price: 290 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU986: + cost: 247 + init_stock: 850 + price: 275 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU987: + cost: 390 + init_stock: 1700 + price: 434 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU988: + cost: 91 + init_stock: 1150 + price: 102 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU989: + cost: 435 + init_stock: 4650 + price: 484 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU99: + cost: 324 + init_stock: 2250 + price: 361 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU990: + cost: 97 + init_stock: 2850 + price: 108 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU991: + cost: 368 + init_stock: 950 + price: 409 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU992: + cost: 390 + init_stock: 4650 + price: 434 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU993: + cost: 130 + init_stock: 4300 + price: 145 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU994: + cost: 423 + init_stock: 2500 + price: 470 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU995: + cost: 216 + init_stock: 3800 + price: 241 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU996: + cost: 234 + init_stock: 3650 + price: 260 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU997: + cost: 360 + init_stock: 1700 + price: 400 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU998: + cost: 402 + init_stock: 2750 + price: 447 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU999: + cost: 71 + init_stock: 1150 + price: 79 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + - children: + distribution: + children: + vehicles: - *id001 - *id001 - *id001 @@ -29099,7 +9299,7 @@ world: unit_price: 1 storage: config: - capacity: 5316100 + capacity: 5324500 unit_storage_cost: 1 config: delay_order_penalty: 1000 @@ -29108,6009 +9308,6009 @@ world: name: WAREHOUSE0 skus: SKU0: - cost: 154 - init_stock: 1780 - price: 154 + cost: 322 + init_stock: 1260 + price: 322 service_level: 0.96 - vlt: 2 + vlt: 1 SKU1: - cost: 122 - init_stock: 1720 - price: 122 + cost: 284 + init_stock: 440 + price: 284 service_level: 0.96 - vlt: 1 + vlt: 3 SKU10: - cost: 158 - init_stock: 1840 - price: 158 + cost: 68 + init_stock: 500 + price: 68 service_level: 0.96 vlt: 3 SKU100: - cost: 448 - init_stock: 740 - price: 448 + cost: 16 + init_stock: 1300 + price: 16 service_level: 0.96 vlt: 3 SKU101: - cost: 432 - init_stock: 1240 - price: 432 + cost: 84 + init_stock: 1420 + price: 84 service_level: 0.96 - vlt: 1 + vlt: 3 SKU102: - cost: 33 - init_stock: 1660 - price: 33 + cost: 328 + init_stock: 1860 + price: 328 service_level: 0.96 vlt: 2 SKU103: - cost: 169 - init_stock: 1580 - price: 169 + cost: 334 + init_stock: 1900 + price: 334 service_level: 0.96 - vlt: 3 + vlt: 1 SKU104: - cost: 253 - init_stock: 580 - price: 253 + cost: 49 + init_stock: 1300 + price: 49 service_level: 0.96 vlt: 3 SKU105: - cost: 305 - init_stock: 1180 - price: 305 + cost: 110 + init_stock: 1140 + price: 110 service_level: 0.96 - vlt: 3 + vlt: 2 SKU106: - cost: 172 - init_stock: 580 - price: 172 + cost: 251 + init_stock: 1460 + price: 251 service_level: 0.96 vlt: 3 SKU107: - cost: 323 - init_stock: 1620 - price: 323 + cost: 423 + init_stock: 1740 + price: 423 service_level: 0.96 - vlt: 1 + vlt: 3 SKU108: - cost: 283 + cost: 458 init_stock: 1840 - price: 283 + price: 458 service_level: 0.96 - vlt: 2 + vlt: 3 SKU109: - cost: 304 - init_stock: 1540 - price: 304 + cost: 88 + init_stock: 1640 + price: 88 service_level: 0.96 vlt: 2 SKU11: - cost: 204 - init_stock: 1160 - price: 204 + cost: 400 + init_stock: 1020 + price: 400 service_level: 0.96 - vlt: 3 + vlt: 2 SKU110: - cost: 374 - init_stock: 1860 - price: 374 + cost: 66 + init_stock: 280 + price: 66 service_level: 0.96 vlt: 3 SKU111: - cost: 215 - init_stock: 1560 - price: 215 + cost: 260 + init_stock: 1220 + price: 260 service_level: 0.96 - vlt: 3 + vlt: 1 SKU112: - cost: 417 - init_stock: 160 - price: 417 + cost: 61 + init_stock: 1900 + price: 61 service_level: 0.96 - vlt: 1 + vlt: 2 SKU113: - cost: 64 - init_stock: 1380 - price: 64 + cost: 348 + init_stock: 620 + price: 348 service_level: 0.96 - vlt: 1 + vlt: 3 SKU114: - cost: 63 - init_stock: 1080 - price: 63 + cost: 389 + init_stock: 540 + price: 389 service_level: 0.96 - vlt: 2 + vlt: 1 SKU115: - cost: 466 - init_stock: 1740 - price: 466 + cost: 286 + init_stock: 1720 + price: 286 service_level: 0.96 vlt: 2 SKU116: - cost: 115 - init_stock: 1840 - price: 115 + cost: 496 + init_stock: 1440 + price: 496 service_level: 0.96 - vlt: 1 + vlt: 3 SKU117: - cost: 76 - init_stock: 1340 - price: 76 + cost: 320 + init_stock: 1840 + price: 320 service_level: 0.96 - vlt: 1 + vlt: 3 SKU118: - cost: 292 - init_stock: 1380 - price: 292 + cost: 183 + init_stock: 660 + price: 183 service_level: 0.96 - vlt: 3 + vlt: 1 SKU119: - cost: 333 - init_stock: 780 - price: 333 + cost: 209 + init_stock: 640 + price: 209 service_level: 0.96 - vlt: 1 + vlt: 3 SKU12: - cost: 429 - init_stock: 1480 - price: 429 + cost: 112 + init_stock: 1680 + price: 112 service_level: 0.96 vlt: 1 SKU120: - cost: 147 - init_stock: 960 - price: 147 + cost: 121 + init_stock: 1960 + price: 121 service_level: 0.96 - vlt: 3 + vlt: 1 SKU121: - cost: 169 - init_stock: 1180 - price: 169 + cost: 40 + init_stock: 1700 + price: 40 service_level: 0.96 - vlt: 2 + vlt: 1 SKU122: - cost: 71 - init_stock: 1440 - price: 71 + cost: 437 + init_stock: 140 + price: 437 service_level: 0.96 - vlt: 2 + vlt: 3 SKU123: - cost: 33 - init_stock: 1180 - price: 33 + cost: 233 + init_stock: 380 + price: 233 service_level: 0.96 - vlt: 2 + vlt: 3 SKU124: - cost: 478 - init_stock: 1160 - price: 478 + cost: 182 + init_stock: 720 + price: 182 service_level: 0.96 vlt: 2 SKU125: - cost: 311 - init_stock: 720 - price: 311 + cost: 16 + init_stock: 1840 + price: 16 service_level: 0.96 vlt: 2 SKU126: - cost: 497 - init_stock: 1520 - price: 497 + cost: 36 + init_stock: 780 + price: 36 service_level: 0.96 - vlt: 2 + vlt: 3 SKU127: - cost: 63 - init_stock: 1840 - price: 63 + cost: 217 + init_stock: 620 + price: 217 service_level: 0.96 - vlt: 1 + vlt: 2 SKU128: - cost: 200 - init_stock: 840 - price: 200 + cost: 165 + init_stock: 380 + price: 165 service_level: 0.96 - vlt: 3 + vlt: 1 SKU129: - cost: 465 - init_stock: 580 - price: 465 + cost: 143 + init_stock: 1000 + price: 143 service_level: 0.96 vlt: 2 SKU13: - cost: 458 - init_stock: 580 - price: 458 + cost: 317 + init_stock: 1140 + price: 317 service_level: 0.96 vlt: 3 SKU130: - cost: 224 - init_stock: 1080 - price: 224 + cost: 348 + init_stock: 1960 + price: 348 service_level: 0.96 - vlt: 2 + vlt: 3 SKU131: - cost: 491 - init_stock: 1200 - price: 491 + cost: 64 + init_stock: 900 + price: 64 service_level: 0.96 vlt: 1 SKU132: - cost: 93 - init_stock: 1220 - price: 93 + cost: 427 + init_stock: 420 + price: 427 service_level: 0.96 vlt: 2 SKU133: - cost: 269 - init_stock: 260 - price: 269 - service_level: 0.96 - vlt: 1 - SKU134: - cost: 364 - init_stock: 400 - price: 364 + cost: 224 + init_stock: 580 + price: 224 service_level: 0.96 vlt: 2 - SKU135: - cost: 351 - init_stock: 1960 - price: 351 + SKU134: + cost: 336 + init_stock: 1540 + price: 336 service_level: 0.96 vlt: 3 - SKU136: - cost: 429 - init_stock: 800 - price: 429 + SKU135: + cost: 153 + init_stock: 2000 + price: 153 service_level: 0.96 vlt: 1 - SKU137: - cost: 193 - init_stock: 1840 - price: 193 + SKU136: + cost: 199 + init_stock: 1420 + price: 199 service_level: 0.96 vlt: 3 - SKU138: - cost: 113 - init_stock: 560 - price: 113 + SKU137: + cost: 93 + init_stock: 1480 + price: 93 service_level: 0.96 vlt: 2 + SKU138: + cost: 228 + init_stock: 720 + price: 228 + service_level: 0.96 + vlt: 1 SKU139: - cost: 10 - init_stock: 1900 - price: 10 + cost: 207 + init_stock: 480 + price: 207 service_level: 0.96 - vlt: 3 + vlt: 1 SKU14: - cost: 55 - init_stock: 260 - price: 55 + cost: 268 + init_stock: 1240 + price: 268 service_level: 0.96 - vlt: 3 + vlt: 1 SKU140: - cost: 350 - init_stock: 1920 - price: 350 + cost: 261 + init_stock: 680 + price: 261 service_level: 0.96 - vlt: 1 + vlt: 3 SKU141: - cost: 70 - init_stock: 1420 - price: 70 + cost: 190 + init_stock: 820 + price: 190 service_level: 0.96 - vlt: 3 + vlt: 1 SKU142: - cost: 496 - init_stock: 1460 - price: 496 + cost: 320 + init_stock: 760 + price: 320 service_level: 0.96 vlt: 3 SKU143: - cost: 33 - init_stock: 1660 - price: 33 + cost: 318 + init_stock: 520 + price: 318 service_level: 0.96 - vlt: 1 + vlt: 3 SKU144: - cost: 275 - init_stock: 1500 - price: 275 + cost: 400 + init_stock: 240 + price: 400 service_level: 0.96 vlt: 1 SKU145: - cost: 51 - init_stock: 1940 - price: 51 + cost: 399 + init_stock: 1640 + price: 399 service_level: 0.96 - vlt: 3 + vlt: 1 SKU146: - cost: 140 - init_stock: 720 - price: 140 + cost: 177 + init_stock: 960 + price: 177 service_level: 0.96 vlt: 3 SKU147: - cost: 383 - init_stock: 980 - price: 383 + cost: 472 + init_stock: 1120 + price: 472 service_level: 0.96 - vlt: 1 + vlt: 3 SKU148: - cost: 278 - init_stock: 1360 - price: 278 + cost: 313 + init_stock: 1540 + price: 313 service_level: 0.96 - vlt: 1 + vlt: 3 SKU149: - cost: 402 - init_stock: 1040 - price: 402 + cost: 357 + init_stock: 1540 + price: 357 service_level: 0.96 vlt: 3 SKU15: - cost: 421 - init_stock: 1420 - price: 421 + cost: 52 + init_stock: 100 + price: 52 service_level: 0.96 vlt: 2 SKU150: - cost: 81 - init_stock: 1660 - price: 81 + cost: 106 + init_stock: 1400 + price: 106 service_level: 0.96 - vlt: 1 + vlt: 2 SKU151: - cost: 407 - init_stock: 1300 - price: 407 + cost: 223 + init_stock: 1460 + price: 223 service_level: 0.96 - vlt: 2 + vlt: 1 SKU152: - cost: 327 - init_stock: 1280 - price: 327 + cost: 10 + init_stock: 1020 + price: 10 service_level: 0.96 vlt: 3 SKU153: - cost: 204 - init_stock: 1940 - price: 204 + cost: 441 + init_stock: 1240 + price: 441 service_level: 0.96 vlt: 1 SKU154: - cost: 88 - init_stock: 520 - price: 88 + cost: 77 + init_stock: 1700 + price: 77 service_level: 0.96 - vlt: 2 + vlt: 3 SKU155: - cost: 428 - init_stock: 520 - price: 428 + cost: 422 + init_stock: 1060 + price: 422 service_level: 0.96 - vlt: 2 + vlt: 1 SKU156: - cost: 486 - init_stock: 160 - price: 486 + cost: 10 + init_stock: 240 + price: 10 service_level: 0.96 - vlt: 3 + vlt: 1 SKU157: - cost: 35 - init_stock: 1320 - price: 35 + cost: 410 + init_stock: 1500 + price: 410 service_level: 0.96 vlt: 2 SKU158: - cost: 186 - init_stock: 1140 - price: 186 + cost: 145 + init_stock: 1620 + price: 145 service_level: 0.96 - vlt: 2 + vlt: 3 SKU159: - cost: 202 - init_stock: 1840 - price: 202 + cost: 193 + init_stock: 500 + price: 193 service_level: 0.96 - vlt: 1 + vlt: 2 SKU16: - cost: 209 - init_stock: 480 - price: 209 + cost: 175 + init_stock: 1160 + price: 175 service_level: 0.96 vlt: 3 SKU160: - cost: 433 - init_stock: 940 - price: 433 + cost: 459 + init_stock: 1700 + price: 459 service_level: 0.96 vlt: 1 SKU161: - cost: 250 - init_stock: 880 - price: 250 + cost: 239 + init_stock: 920 + price: 239 service_level: 0.96 - vlt: 1 + vlt: 2 SKU162: - cost: 316 - init_stock: 1600 - price: 316 + cost: 158 + init_stock: 100 + price: 158 service_level: 0.96 - vlt: 3 + vlt: 2 SKU163: - cost: 114 - init_stock: 280 - price: 114 + cost: 486 + init_stock: 780 + price: 486 service_level: 0.96 - vlt: 1 + vlt: 2 SKU164: - cost: 304 - init_stock: 1320 - price: 304 + cost: 496 + init_stock: 2000 + price: 496 service_level: 0.96 - vlt: 2 + vlt: 1 SKU165: - cost: 195 - init_stock: 1060 - price: 195 + cost: 274 + init_stock: 660 + price: 274 service_level: 0.96 - vlt: 2 + vlt: 3 SKU166: - cost: 315 - init_stock: 1020 - price: 315 + cost: 79 + init_stock: 1780 + price: 79 service_level: 0.96 vlt: 1 SKU167: - cost: 470 - init_stock: 1880 - price: 470 + cost: 443 + init_stock: 260 + price: 443 service_level: 0.96 vlt: 1 SKU168: - cost: 476 - init_stock: 380 - price: 476 + cost: 357 + init_stock: 1740 + price: 357 service_level: 0.96 - vlt: 1 + vlt: 2 SKU169: - cost: 134 - init_stock: 380 - price: 134 + cost: 369 + init_stock: 1960 + price: 369 service_level: 0.96 - vlt: 1 + vlt: 3 SKU17: - cost: 401 - init_stock: 760 - price: 401 + cost: 346 + init_stock: 180 + price: 346 service_level: 0.96 vlt: 1 SKU170: - cost: 289 - init_stock: 760 - price: 289 + cost: 68 + init_stock: 1100 + price: 68 service_level: 0.96 - vlt: 1 + vlt: 2 SKU171: - cost: 481 - init_stock: 1640 - price: 481 + cost: 398 + init_stock: 1520 + price: 398 service_level: 0.96 vlt: 1 SKU172: - cost: 434 - init_stock: 620 - price: 434 + cost: 200 + init_stock: 1420 + price: 200 service_level: 0.96 vlt: 2 SKU173: - cost: 292 - init_stock: 1880 - price: 292 + cost: 175 + init_stock: 1920 + price: 175 service_level: 0.96 - vlt: 2 + vlt: 3 SKU174: - cost: 474 - init_stock: 280 - price: 474 + cost: 291 + init_stock: 1520 + price: 291 service_level: 0.96 vlt: 2 SKU175: - cost: 476 - init_stock: 420 - price: 476 + cost: 84 + init_stock: 1500 + price: 84 service_level: 0.96 vlt: 2 SKU176: - cost: 299 - init_stock: 1600 - price: 299 + cost: 407 + init_stock: 1320 + price: 407 service_level: 0.96 - vlt: 2 + vlt: 1 SKU177: - cost: 425 - init_stock: 420 - price: 425 + cost: 257 + init_stock: 620 + price: 257 service_level: 0.96 vlt: 1 SKU178: - cost: 419 - init_stock: 1400 - price: 419 + cost: 423 + init_stock: 100 + price: 423 service_level: 0.96 - vlt: 3 + vlt: 2 SKU179: - cost: 236 - init_stock: 1300 - price: 236 + cost: 497 + init_stock: 1660 + price: 497 service_level: 0.96 - vlt: 3 + vlt: 1 SKU18: - cost: 460 + cost: 258 init_stock: 1620 - price: 460 + price: 258 service_level: 0.96 - vlt: 2 + vlt: 1 SKU180: - cost: 99 - init_stock: 320 - price: 99 + cost: 217 + init_stock: 1100 + price: 217 service_level: 0.96 - vlt: 1 + vlt: 2 SKU181: - cost: 122 - init_stock: 540 - price: 122 + cost: 143 + init_stock: 1200 + price: 143 service_level: 0.96 - vlt: 2 + vlt: 1 SKU182: - cost: 443 - init_stock: 460 - price: 443 + cost: 437 + init_stock: 1980 + price: 437 service_level: 0.96 - vlt: 2 + vlt: 3 SKU183: - cost: 72 - init_stock: 1060 - price: 72 + cost: 145 + init_stock: 160 + price: 145 service_level: 0.96 - vlt: 1 + vlt: 3 SKU184: - cost: 192 - init_stock: 1540 - price: 192 + cost: 73 + init_stock: 480 + price: 73 service_level: 0.96 - vlt: 3 + vlt: 2 SKU185: - cost: 130 - init_stock: 560 - price: 130 + cost: 10 + init_stock: 1800 + price: 10 service_level: 0.96 vlt: 2 SKU186: - cost: 49 - init_stock: 780 - price: 49 + cost: 359 + init_stock: 440 + price: 359 service_level: 0.96 - vlt: 3 + vlt: 1 SKU187: - cost: 492 - init_stock: 940 - price: 492 + cost: 177 + init_stock: 600 + price: 177 service_level: 0.96 vlt: 3 SKU188: - cost: 50 - init_stock: 1800 - price: 50 + cost: 391 + init_stock: 1740 + price: 391 service_level: 0.96 vlt: 3 SKU189: - cost: 145 - init_stock: 660 - price: 145 + cost: 358 + init_stock: 700 + price: 358 service_level: 0.96 vlt: 2 SKU19: - cost: 451 - init_stock: 120 - price: 451 + cost: 477 + init_stock: 1820 + price: 477 service_level: 0.96 - vlt: 1 + vlt: 3 SKU190: - cost: 72 - init_stock: 1580 - price: 72 + cost: 113 + init_stock: 340 + price: 113 service_level: 0.96 - vlt: 2 + vlt: 3 SKU191: - cost: 285 - init_stock: 800 - price: 285 + cost: 473 + init_stock: 1080 + price: 473 service_level: 0.96 - vlt: 3 + vlt: 2 SKU192: - cost: 183 - init_stock: 1900 - price: 183 + cost: 415 + init_stock: 1220 + price: 415 service_level: 0.96 - vlt: 1 + vlt: 2 SKU193: - cost: 345 - init_stock: 1300 - price: 345 + cost: 207 + init_stock: 600 + price: 207 service_level: 0.96 vlt: 2 SKU194: - cost: 439 - init_stock: 1480 - price: 439 + cost: 432 + init_stock: 100 + price: 432 service_level: 0.96 vlt: 2 SKU195: - cost: 205 - init_stock: 640 - price: 205 + cost: 218 + init_stock: 620 + price: 218 service_level: 0.96 - vlt: 3 + vlt: 2 SKU196: - cost: 208 - init_stock: 440 - price: 208 + cost: 49 + init_stock: 1360 + price: 49 service_level: 0.96 - vlt: 2 + vlt: 3 SKU197: - cost: 476 - init_stock: 100 - price: 476 + cost: 303 + init_stock: 1140 + price: 303 service_level: 0.96 vlt: 1 SKU198: - cost: 269 - init_stock: 1540 - price: 269 + cost: 169 + init_stock: 1080 + price: 169 service_level: 0.96 - vlt: 1 + vlt: 2 SKU199: - cost: 172 - init_stock: 1100 - price: 172 + cost: 449 + init_stock: 460 + price: 449 service_level: 0.96 - vlt: 3 + vlt: 1 SKU2: - cost: 204 - init_stock: 500 - price: 204 + cost: 331 + init_stock: 1400 + price: 331 service_level: 0.96 vlt: 3 SKU20: - cost: 437 - init_stock: 820 - price: 437 + cost: 335 + init_stock: 500 + price: 335 service_level: 0.96 - vlt: 1 + vlt: 3 SKU200: - cost: 51 - init_stock: 1880 - price: 51 + cost: 65 + init_stock: 500 + price: 65 service_level: 0.96 - vlt: 3 + vlt: 1 SKU201: - cost: 487 - init_stock: 1480 - price: 487 + cost: 104 + init_stock: 1180 + price: 104 service_level: 0.96 - vlt: 2 + vlt: 1 SKU202: - cost: 406 - init_stock: 280 - price: 406 + cost: 142 + init_stock: 1460 + price: 142 service_level: 0.96 vlt: 1 SKU203: - cost: 333 - init_stock: 580 - price: 333 + cost: 440 + init_stock: 1640 + price: 440 service_level: 0.96 - vlt: 1 + vlt: 2 SKU204: - cost: 64 - init_stock: 1560 - price: 64 + cost: 489 + init_stock: 940 + price: 489 service_level: 0.96 - vlt: 3 + vlt: 2 SKU205: - cost: 448 - init_stock: 1820 - price: 448 + cost: 130 + init_stock: 2000 + price: 130 service_level: 0.96 - vlt: 1 + vlt: 3 SKU206: - cost: 65 - init_stock: 1940 - price: 65 + cost: 335 + init_stock: 220 + price: 335 service_level: 0.96 - vlt: 3 + vlt: 2 SKU207: - cost: 437 - init_stock: 1160 - price: 437 + cost: 140 + init_stock: 1600 + price: 140 service_level: 0.96 - vlt: 2 + vlt: 1 SKU208: - cost: 38 - init_stock: 500 - price: 38 + cost: 491 + init_stock: 1540 + price: 491 service_level: 0.96 - vlt: 3 + vlt: 1 SKU209: - cost: 327 - init_stock: 1300 - price: 327 + cost: 179 + init_stock: 400 + price: 179 service_level: 0.96 vlt: 3 SKU21: - cost: 17 - init_stock: 1100 - price: 17 + cost: 123 + init_stock: 2000 + price: 123 service_level: 0.96 - vlt: 3 + vlt: 2 SKU210: - cost: 455 - init_stock: 1000 - price: 455 + cost: 404 + init_stock: 1380 + price: 404 service_level: 0.96 - vlt: 2 + vlt: 3 SKU211: - cost: 170 - init_stock: 300 - price: 170 + cost: 174 + init_stock: 1820 + price: 174 service_level: 0.96 vlt: 2 SKU212: - cost: 62 - init_stock: 1380 - price: 62 + cost: 405 + init_stock: 1580 + price: 405 service_level: 0.96 vlt: 3 SKU213: - cost: 263 - init_stock: 1400 - price: 263 + cost: 121 + init_stock: 1280 + price: 121 service_level: 0.96 - vlt: 1 + vlt: 2 SKU214: - cost: 434 - init_stock: 140 - price: 434 + cost: 101 + init_stock: 200 + price: 101 service_level: 0.96 vlt: 2 SKU215: - cost: 283 - init_stock: 480 - price: 283 + cost: 419 + init_stock: 940 + price: 419 service_level: 0.96 vlt: 1 SKU216: - cost: 391 - init_stock: 1560 - price: 391 + cost: 330 + init_stock: 460 + price: 330 service_level: 0.96 - vlt: 2 + vlt: 1 SKU217: - cost: 168 - init_stock: 1960 - price: 168 + cost: 284 + init_stock: 1300 + price: 284 service_level: 0.96 - vlt: 1 + vlt: 2 SKU218: - cost: 485 - init_stock: 360 - price: 485 + cost: 205 + init_stock: 1180 + price: 205 service_level: 0.96 vlt: 1 SKU219: - cost: 201 - init_stock: 1720 - price: 201 + cost: 92 + init_stock: 920 + price: 92 service_level: 0.96 - vlt: 1 + vlt: 3 SKU22: - cost: 22 - init_stock: 600 - price: 22 + cost: 493 + init_stock: 1320 + price: 493 service_level: 0.96 - vlt: 2 + vlt: 1 SKU220: - cost: 394 - init_stock: 260 - price: 394 + cost: 387 + init_stock: 1740 + price: 387 service_level: 0.96 vlt: 2 SKU221: - cost: 108 - init_stock: 1840 - price: 108 + cost: 39 + init_stock: 1560 + price: 39 service_level: 0.96 vlt: 2 SKU222: - cost: 459 - init_stock: 1120 - price: 459 + cost: 115 + init_stock: 720 + price: 115 service_level: 0.96 vlt: 2 SKU223: - cost: 420 - init_stock: 1460 - price: 420 + cost: 196 + init_stock: 240 + price: 196 service_level: 0.96 - vlt: 1 + vlt: 2 SKU224: - cost: 306 - init_stock: 1400 - price: 306 + cost: 387 + init_stock: 100 + price: 387 service_level: 0.96 - vlt: 3 + vlt: 2 SKU225: - cost: 118 - init_stock: 1840 - price: 118 + cost: 164 + init_stock: 580 + price: 164 service_level: 0.96 - vlt: 2 + vlt: 1 SKU226: - cost: 425 - init_stock: 880 - price: 425 + cost: 206 + init_stock: 260 + price: 206 service_level: 0.96 - vlt: 3 + vlt: 2 SKU227: - cost: 191 - init_stock: 1300 - price: 191 + cost: 479 + init_stock: 480 + price: 479 service_level: 0.96 vlt: 3 SKU228: - cost: 160 - init_stock: 640 - price: 160 + cost: 14 + init_stock: 1800 + price: 14 service_level: 0.96 - vlt: 2 + vlt: 1 SKU229: - cost: 278 - init_stock: 1460 - price: 278 + cost: 472 + init_stock: 880 + price: 472 service_level: 0.96 - vlt: 3 + vlt: 1 SKU23: - cost: 124 - init_stock: 280 - price: 124 + cost: 387 + init_stock: 840 + price: 387 service_level: 0.96 - vlt: 3 + vlt: 1 SKU230: - cost: 164 - init_stock: 380 - price: 164 + cost: 241 + init_stock: 460 + price: 241 service_level: 0.96 - vlt: 2 + vlt: 3 SKU231: - cost: 321 - init_stock: 1880 - price: 321 + cost: 48 + init_stock: 1620 + price: 48 service_level: 0.96 vlt: 3 SKU232: - cost: 435 - init_stock: 820 - price: 435 + cost: 224 + init_stock: 1920 + price: 224 service_level: 0.96 vlt: 1 SKU233: - cost: 219 - init_stock: 740 - price: 219 + cost: 360 + init_stock: 1500 + price: 360 service_level: 0.96 - vlt: 1 + vlt: 2 SKU234: - cost: 144 - init_stock: 340 - price: 144 + cost: 287 + init_stock: 100 + price: 287 service_level: 0.96 - vlt: 2 + vlt: 1 SKU235: - cost: 396 - init_stock: 440 - price: 396 + cost: 24 + init_stock: 1140 + price: 24 service_level: 0.96 vlt: 3 SKU236: - cost: 383 - init_stock: 1980 - price: 383 + cost: 155 + init_stock: 1100 + price: 155 service_level: 0.96 - vlt: 3 + vlt: 2 SKU237: - cost: 108 - init_stock: 1020 - price: 108 + cost: 433 + init_stock: 900 + price: 433 service_level: 0.96 vlt: 3 SKU238: - cost: 49 - init_stock: 1140 - price: 49 + cost: 64 + init_stock: 1320 + price: 64 service_level: 0.96 - vlt: 2 + vlt: 3 SKU239: - cost: 222 - init_stock: 1800 - price: 222 + cost: 103 + init_stock: 1960 + price: 103 service_level: 0.96 - vlt: 2 + vlt: 1 SKU24: - cost: 368 + cost: 97 init_stock: 940 - price: 368 + price: 97 service_level: 0.96 vlt: 2 SKU240: - cost: 387 - init_stock: 1660 - price: 387 + cost: 373 + init_stock: 940 + price: 373 service_level: 0.96 - vlt: 3 + vlt: 2 SKU241: - cost: 302 - init_stock: 1780 - price: 302 + cost: 439 + init_stock: 1420 + price: 439 service_level: 0.96 - vlt: 1 + vlt: 2 SKU242: - cost: 62 - init_stock: 1580 - price: 62 + cost: 17 + init_stock: 880 + price: 17 service_level: 0.96 - vlt: 3 + vlt: 1 SKU243: - cost: 12 - init_stock: 1000 - price: 12 + cost: 352 + init_stock: 280 + price: 352 service_level: 0.96 - vlt: 2 + vlt: 1 SKU244: - cost: 296 - init_stock: 1300 - price: 296 + cost: 174 + init_stock: 1640 + price: 174 service_level: 0.96 - vlt: 1 + vlt: 2 SKU245: - cost: 39 - init_stock: 780 - price: 39 + cost: 404 + init_stock: 1320 + price: 404 service_level: 0.96 - vlt: 3 + vlt: 2 SKU246: - cost: 263 - init_stock: 900 - price: 263 + cost: 300 + init_stock: 600 + price: 300 service_level: 0.96 - vlt: 3 + vlt: 2 SKU247: - cost: 224 - init_stock: 520 - price: 224 + cost: 386 + init_stock: 700 + price: 386 service_level: 0.96 vlt: 2 SKU248: - cost: 410 - init_stock: 1680 - price: 410 + cost: 355 + init_stock: 580 + price: 355 service_level: 0.96 - vlt: 1 + vlt: 3 SKU249: - cost: 64 - init_stock: 1940 - price: 64 + cost: 356 + init_stock: 500 + price: 356 service_level: 0.96 vlt: 1 SKU25: - cost: 201 - init_stock: 920 - price: 201 + cost: 161 + init_stock: 100 + price: 161 service_level: 0.96 vlt: 2 SKU250: - cost: 259 - init_stock: 900 - price: 259 + cost: 127 + init_stock: 1080 + price: 127 service_level: 0.96 - vlt: 2 + vlt: 3 SKU251: - cost: 181 - init_stock: 460 - price: 181 + cost: 344 + init_stock: 1220 + price: 344 service_level: 0.96 - vlt: 2 + vlt: 1 SKU252: - cost: 330 - init_stock: 1300 - price: 330 + cost: 181 + init_stock: 1660 + price: 181 service_level: 0.96 vlt: 1 SKU253: - cost: 315 - init_stock: 1840 - price: 315 + cost: 448 + init_stock: 320 + price: 448 service_level: 0.96 - vlt: 3 + vlt: 1 SKU254: - cost: 407 - init_stock: 980 - price: 407 + cost: 484 + init_stock: 920 + price: 484 service_level: 0.96 - vlt: 2 + vlt: 3 SKU255: - cost: 361 - init_stock: 1480 - price: 361 + cost: 290 + init_stock: 1340 + price: 290 service_level: 0.96 - vlt: 1 + vlt: 2 SKU256: - cost: 67 - init_stock: 940 - price: 67 + cost: 91 + init_stock: 1440 + price: 91 service_level: 0.96 vlt: 3 SKU257: - cost: 50 - init_stock: 1200 - price: 50 + cost: 348 + init_stock: 1140 + price: 348 service_level: 0.96 vlt: 1 SKU258: - cost: 465 - init_stock: 160 - price: 465 + cost: 489 + init_stock: 860 + price: 489 service_level: 0.96 vlt: 1 SKU259: - cost: 346 - init_stock: 800 - price: 346 + cost: 333 + init_stock: 1380 + price: 333 service_level: 0.96 vlt: 1 SKU26: - cost: 419 - init_stock: 1300 - price: 419 + cost: 229 + init_stock: 1260 + price: 229 service_level: 0.96 - vlt: 3 + vlt: 1 SKU260: - cost: 374 - init_stock: 840 - price: 374 + cost: 487 + init_stock: 1040 + price: 487 service_level: 0.96 - vlt: 2 + vlt: 3 SKU261: - cost: 74 - init_stock: 540 - price: 74 + cost: 368 + init_stock: 440 + price: 368 service_level: 0.96 - vlt: 2 + vlt: 1 SKU262: - cost: 456 - init_stock: 1380 - price: 456 + cost: 332 + init_stock: 1560 + price: 332 service_level: 0.96 - vlt: 2 + vlt: 3 SKU263: - cost: 276 - init_stock: 1080 - price: 276 + cost: 189 + init_stock: 1480 + price: 189 service_level: 0.96 vlt: 3 SKU264: - cost: 89 - init_stock: 1200 - price: 89 + cost: 361 + init_stock: 1580 + price: 361 service_level: 0.96 vlt: 1 SKU265: - cost: 148 - init_stock: 1580 - price: 148 + cost: 286 + init_stock: 1180 + price: 286 service_level: 0.96 - vlt: 2 + vlt: 3 SKU266: - cost: 287 - init_stock: 260 - price: 287 + cost: 128 + init_stock: 940 + price: 128 service_level: 0.96 vlt: 2 SKU267: - cost: 31 - init_stock: 1920 - price: 31 + cost: 77 + init_stock: 1600 + price: 77 service_level: 0.96 vlt: 1 SKU268: - cost: 340 - init_stock: 440 - price: 340 + cost: 221 + init_stock: 1780 + price: 221 service_level: 0.96 vlt: 2 SKU269: - cost: 479 - init_stock: 1040 - price: 479 + cost: 126 + init_stock: 880 + price: 126 service_level: 0.96 - vlt: 3 + vlt: 2 SKU27: - cost: 269 - init_stock: 1780 - price: 269 + cost: 370 + init_stock: 1120 + price: 370 service_level: 0.96 vlt: 3 SKU270: - cost: 233 - init_stock: 800 - price: 233 + cost: 182 + init_stock: 1480 + price: 182 service_level: 0.96 - vlt: 2 + vlt: 3 SKU271: - cost: 90 - init_stock: 240 - price: 90 + cost: 230 + init_stock: 360 + price: 230 service_level: 0.96 vlt: 1 SKU272: - cost: 488 - init_stock: 600 - price: 488 + cost: 366 + init_stock: 340 + price: 366 service_level: 0.96 - vlt: 3 + vlt: 2 SKU273: - cost: 499 - init_stock: 400 - price: 499 + cost: 421 + init_stock: 360 + price: 421 service_level: 0.96 - vlt: 3 + vlt: 2 SKU274: - cost: 434 - init_stock: 1860 - price: 434 + cost: 29 + init_stock: 1540 + price: 29 service_level: 0.96 vlt: 2 SKU275: - cost: 457 - init_stock: 1920 - price: 457 + cost: 50 + init_stock: 960 + price: 50 service_level: 0.96 vlt: 1 SKU276: - cost: 179 - init_stock: 660 - price: 179 + cost: 163 + init_stock: 1080 + price: 163 service_level: 0.96 - vlt: 2 + vlt: 3 SKU277: - cost: 303 - init_stock: 1240 - price: 303 + cost: 449 + init_stock: 820 + price: 449 service_level: 0.96 - vlt: 2 + vlt: 1 SKU278: - cost: 301 - init_stock: 960 - price: 301 + cost: 105 + init_stock: 240 + price: 105 service_level: 0.96 vlt: 3 SKU279: - cost: 17 - init_stock: 1640 - price: 17 + cost: 51 + init_stock: 780 + price: 51 service_level: 0.96 vlt: 2 SKU28: - cost: 399 - init_stock: 120 - price: 399 + cost: 208 + init_stock: 940 + price: 208 service_level: 0.96 - vlt: 1 + vlt: 2 SKU280: - cost: 238 - init_stock: 1660 - price: 238 + cost: 295 + init_stock: 660 + price: 295 service_level: 0.96 - vlt: 3 + vlt: 2 SKU281: - cost: 13 - init_stock: 1380 - price: 13 + cost: 395 + init_stock: 1500 + price: 395 service_level: 0.96 vlt: 1 SKU282: - cost: 404 - init_stock: 1680 - price: 404 + cost: 63 + init_stock: 920 + price: 63 service_level: 0.96 - vlt: 1 + vlt: 2 SKU283: - cost: 332 - init_stock: 640 - price: 332 + cost: 392 + init_stock: 1840 + price: 392 service_level: 0.96 vlt: 3 SKU284: - cost: 257 - init_stock: 780 - price: 257 + cost: 344 + init_stock: 1340 + price: 344 service_level: 0.96 vlt: 3 SKU285: - cost: 53 - init_stock: 120 - price: 53 + cost: 133 + init_stock: 1820 + price: 133 service_level: 0.96 vlt: 2 SKU286: - cost: 252 - init_stock: 260 - price: 252 + cost: 337 + init_stock: 780 + price: 337 service_level: 0.96 - vlt: 2 + vlt: 1 SKU287: - cost: 161 - init_stock: 1880 - price: 161 + cost: 375 + init_stock: 1120 + price: 375 service_level: 0.96 vlt: 3 SKU288: - cost: 477 - init_stock: 320 - price: 477 + cost: 181 + init_stock: 760 + price: 181 service_level: 0.96 - vlt: 2 + vlt: 1 SKU289: - cost: 452 - init_stock: 1900 - price: 452 + cost: 67 + init_stock: 620 + price: 67 service_level: 0.96 - vlt: 2 + vlt: 1 SKU29: - cost: 160 - init_stock: 1480 - price: 160 + cost: 245 + init_stock: 1160 + price: 245 service_level: 0.96 vlt: 1 SKU290: - cost: 493 - init_stock: 560 - price: 493 + cost: 438 + init_stock: 1340 + price: 438 service_level: 0.96 - vlt: 2 + vlt: 1 SKU291: - cost: 180 - init_stock: 380 - price: 180 + cost: 94 + init_stock: 1220 + price: 94 service_level: 0.96 - vlt: 2 + vlt: 1 SKU292: - cost: 233 - init_stock: 1900 - price: 233 + cost: 186 + init_stock: 100 + price: 186 service_level: 0.96 - vlt: 2 + vlt: 1 SKU293: - cost: 177 - init_stock: 1320 - price: 177 + cost: 162 + init_stock: 1100 + price: 162 service_level: 0.96 - vlt: 1 + vlt: 2 SKU294: - cost: 356 - init_stock: 540 - price: 356 + cost: 409 + init_stock: 180 + price: 409 service_level: 0.96 vlt: 2 SKU295: - cost: 421 - init_stock: 1640 - price: 421 + cost: 435 + init_stock: 1860 + price: 435 service_level: 0.96 - vlt: 2 + vlt: 3 SKU296: - cost: 414 - init_stock: 940 - price: 414 + cost: 370 + init_stock: 1840 + price: 370 service_level: 0.96 vlt: 3 SKU297: - cost: 326 - init_stock: 1800 - price: 326 + cost: 298 + init_stock: 760 + price: 298 service_level: 0.96 - vlt: 3 + vlt: 1 SKU298: - cost: 122 - init_stock: 920 - price: 122 + cost: 286 + init_stock: 700 + price: 286 service_level: 0.96 - vlt: 3 + vlt: 2 SKU299: - cost: 149 - init_stock: 1360 - price: 149 + cost: 367 + init_stock: 1020 + price: 367 service_level: 0.96 - vlt: 2 - SKU3: - cost: 161 - init_stock: 100 - price: 161 + vlt: 3 + SKU3: + cost: 405 + init_stock: 240 + price: 405 service_level: 0.96 - vlt: 1 + vlt: 3 SKU30: - cost: 395 - init_stock: 1220 - price: 395 + cost: 359 + init_stock: 1380 + price: 359 service_level: 0.96 - vlt: 3 + vlt: 2 SKU300: - cost: 250 - init_stock: 780 - price: 250 + cost: 376 + init_stock: 1160 + price: 376 service_level: 0.96 vlt: 1 SKU301: - cost: 279 - init_stock: 100 - price: 279 + cost: 433 + init_stock: 1660 + price: 433 service_level: 0.96 vlt: 2 SKU302: - cost: 321 - init_stock: 1480 - price: 321 + cost: 184 + init_stock: 220 + price: 184 service_level: 0.96 vlt: 2 SKU303: - cost: 202 - init_stock: 1100 - price: 202 + cost: 246 + init_stock: 1880 + price: 246 service_level: 0.96 - vlt: 3 + vlt: 1 SKU304: - cost: 393 - init_stock: 1360 - price: 393 + cost: 419 + init_stock: 460 + price: 419 service_level: 0.96 - vlt: 2 + vlt: 3 SKU305: - cost: 113 - init_stock: 1340 - price: 113 + cost: 495 + init_stock: 2000 + price: 495 service_level: 0.96 vlt: 1 SKU306: - cost: 430 - init_stock: 860 - price: 430 + cost: 479 + init_stock: 840 + price: 479 service_level: 0.96 vlt: 2 SKU307: - cost: 311 - init_stock: 1620 - price: 311 + cost: 210 + init_stock: 1560 + price: 210 service_level: 0.96 - vlt: 3 + vlt: 1 SKU308: - cost: 423 - init_stock: 1160 - price: 423 + cost: 208 + init_stock: 100 + price: 208 service_level: 0.96 - vlt: 1 + vlt: 2 SKU309: - cost: 448 - init_stock: 1580 - price: 448 + cost: 101 + init_stock: 1840 + price: 101 service_level: 0.96 vlt: 2 SKU31: - cost: 456 - init_stock: 1520 - price: 456 + cost: 69 + init_stock: 1120 + price: 69 service_level: 0.96 - vlt: 2 + vlt: 3 SKU310: - cost: 245 - init_stock: 1260 - price: 245 + cost: 234 + init_stock: 880 + price: 234 service_level: 0.96 - vlt: 1 + vlt: 3 SKU311: - cost: 452 - init_stock: 1400 - price: 452 + cost: 306 + init_stock: 1600 + price: 306 service_level: 0.96 - vlt: 1 + vlt: 3 SKU312: - cost: 112 - init_stock: 1460 - price: 112 + cost: 291 + init_stock: 500 + price: 291 service_level: 0.96 - vlt: 3 + vlt: 1 SKU313: - cost: 307 - init_stock: 200 - price: 307 + cost: 324 + init_stock: 760 + price: 324 service_level: 0.96 - vlt: 2 + vlt: 1 SKU314: - cost: 40 - init_stock: 1520 - price: 40 + cost: 404 + init_stock: 580 + price: 404 service_level: 0.96 - vlt: 2 + vlt: 1 SKU315: - cost: 87 - init_stock: 440 - price: 87 + cost: 471 + init_stock: 1680 + price: 471 service_level: 0.96 - vlt: 1 + vlt: 2 SKU316: - cost: 367 - init_stock: 320 - price: 367 + cost: 202 + init_stock: 360 + price: 202 service_level: 0.96 - vlt: 3 + vlt: 1 SKU317: - cost: 62 - init_stock: 260 - price: 62 + cost: 216 + init_stock: 480 + price: 216 service_level: 0.96 - vlt: 1 + vlt: 2 SKU318: - cost: 153 - init_stock: 840 - price: 153 + cost: 278 + init_stock: 1700 + price: 278 service_level: 0.96 - vlt: 2 + vlt: 1 SKU319: - cost: 340 - init_stock: 920 - price: 340 + cost: 257 + init_stock: 1060 + price: 257 service_level: 0.96 - vlt: 2 + vlt: 3 SKU32: - cost: 223 - init_stock: 1780 - price: 223 + cost: 336 + init_stock: 440 + price: 336 service_level: 0.96 vlt: 1 SKU320: - cost: 188 - init_stock: 1680 - price: 188 + cost: 196 + init_stock: 780 + price: 196 service_level: 0.96 - vlt: 1 + vlt: 3 SKU321: - cost: 421 - init_stock: 940 - price: 421 + cost: 67 + init_stock: 320 + price: 67 service_level: 0.96 - vlt: 1 + vlt: 3 SKU322: - cost: 462 - init_stock: 1520 - price: 462 + cost: 240 + init_stock: 2000 + price: 240 service_level: 0.96 vlt: 1 SKU323: - cost: 116 - init_stock: 1580 - price: 116 + cost: 66 + init_stock: 780 + price: 66 service_level: 0.96 vlt: 2 SKU324: - cost: 396 - init_stock: 400 - price: 396 + cost: 442 + init_stock: 1860 + price: 442 service_level: 0.96 vlt: 3 SKU325: - cost: 407 - init_stock: 1200 - price: 407 + cost: 453 + init_stock: 1380 + price: 453 service_level: 0.96 - vlt: 1 + vlt: 2 SKU326: - cost: 400 - init_stock: 240 - price: 400 + cost: 345 + init_stock: 480 + price: 345 service_level: 0.96 - vlt: 3 + vlt: 2 SKU327: - cost: 434 - init_stock: 2000 - price: 434 + cost: 457 + init_stock: 280 + price: 457 service_level: 0.96 vlt: 2 SKU328: - cost: 179 - init_stock: 420 - price: 179 + cost: 390 + init_stock: 900 + price: 390 service_level: 0.96 vlt: 1 SKU329: - cost: 453 - init_stock: 1620 - price: 453 + cost: 67 + init_stock: 840 + price: 67 service_level: 0.96 vlt: 1 SKU33: - cost: 113 - init_stock: 1780 - price: 113 + cost: 416 + init_stock: 1840 + price: 416 service_level: 0.96 - vlt: 3 + vlt: 2 SKU330: - cost: 215 - init_stock: 1840 - price: 215 + cost: 306 + init_stock: 780 + price: 306 service_level: 0.96 - vlt: 1 + vlt: 2 SKU331: - cost: 28 - init_stock: 1680 - price: 28 + cost: 138 + init_stock: 820 + price: 138 service_level: 0.96 vlt: 3 SKU332: - cost: 172 - init_stock: 1100 - price: 172 + cost: 390 + init_stock: 1920 + price: 390 service_level: 0.96 vlt: 2 SKU333: - cost: 63 - init_stock: 1340 - price: 63 + cost: 485 + init_stock: 1060 + price: 485 service_level: 0.96 - vlt: 1 + vlt: 2 SKU334: - cost: 253 - init_stock: 780 - price: 253 + cost: 183 + init_stock: 1140 + price: 183 service_level: 0.96 - vlt: 3 + vlt: 1 SKU335: - cost: 22 - init_stock: 180 - price: 22 + cost: 80 + init_stock: 1620 + price: 80 service_level: 0.96 vlt: 1 SKU336: - cost: 263 - init_stock: 580 - price: 263 + cost: 296 + init_stock: 560 + price: 296 service_level: 0.96 - vlt: 3 + vlt: 1 SKU337: - cost: 277 - init_stock: 1480 - price: 277 + cost: 245 + init_stock: 580 + price: 245 service_level: 0.96 - vlt: 3 + vlt: 2 SKU338: - cost: 181 - init_stock: 680 - price: 181 + cost: 87 + init_stock: 1620 + price: 87 service_level: 0.96 vlt: 3 SKU339: - cost: 488 - init_stock: 1520 - price: 488 + cost: 265 + init_stock: 1260 + price: 265 service_level: 0.96 - vlt: 1 + vlt: 2 SKU34: - cost: 226 - init_stock: 880 - price: 226 + cost: 95 + init_stock: 260 + price: 95 service_level: 0.96 - vlt: 2 + vlt: 3 SKU340: - cost: 213 - init_stock: 540 - price: 213 + cost: 480 + init_stock: 1740 + price: 480 service_level: 0.96 - vlt: 3 + vlt: 2 SKU341: - cost: 423 - init_stock: 1780 - price: 423 + cost: 462 + init_stock: 1400 + price: 462 service_level: 0.96 vlt: 1 SKU342: - cost: 414 - init_stock: 980 - price: 414 + cost: 144 + init_stock: 180 + price: 144 service_level: 0.96 - vlt: 1 + vlt: 3 SKU343: - cost: 429 - init_stock: 1040 - price: 429 + cost: 310 + init_stock: 300 + price: 310 service_level: 0.96 - vlt: 1 + vlt: 2 SKU344: - cost: 196 - init_stock: 1780 - price: 196 + cost: 357 + init_stock: 1740 + price: 357 service_level: 0.96 - vlt: 1 + vlt: 2 SKU345: - cost: 451 - init_stock: 1840 - price: 451 + cost: 442 + init_stock: 1780 + price: 442 service_level: 0.96 - vlt: 1 + vlt: 2 SKU346: - cost: 485 - init_stock: 600 - price: 485 + cost: 350 + init_stock: 840 + price: 350 service_level: 0.96 vlt: 3 SKU347: - cost: 30 - init_stock: 1360 - price: 30 + cost: 497 + init_stock: 1640 + price: 497 service_level: 0.96 vlt: 1 SKU348: - cost: 12 - init_stock: 240 - price: 12 + cost: 147 + init_stock: 400 + price: 147 service_level: 0.96 - vlt: 2 + vlt: 1 SKU349: - cost: 168 - init_stock: 880 - price: 168 + cost: 67 + init_stock: 1340 + price: 67 service_level: 0.96 - vlt: 2 + vlt: 3 SKU35: - cost: 181 - init_stock: 900 - price: 181 + cost: 267 + init_stock: 1720 + price: 267 service_level: 0.96 - vlt: 1 + vlt: 3 SKU350: - cost: 381 - init_stock: 580 - price: 381 + cost: 279 + init_stock: 1840 + price: 279 service_level: 0.96 - vlt: 2 + vlt: 3 SKU351: - cost: 333 - init_stock: 1920 - price: 333 + cost: 155 + init_stock: 1340 + price: 155 service_level: 0.96 - vlt: 2 + vlt: 1 SKU352: - cost: 365 - init_stock: 380 - price: 365 + cost: 71 + init_stock: 360 + price: 71 service_level: 0.96 vlt: 2 SKU353: - cost: 133 - init_stock: 1300 - price: 133 + cost: 253 + init_stock: 1860 + price: 253 service_level: 0.96 - vlt: 3 + vlt: 2 SKU354: - cost: 356 - init_stock: 1240 - price: 356 + cost: 396 + init_stock: 1580 + price: 396 service_level: 0.96 vlt: 3 SKU355: - cost: 453 - init_stock: 1120 - price: 453 + cost: 63 + init_stock: 200 + price: 63 service_level: 0.96 vlt: 1 SKU356: - cost: 336 - init_stock: 1380 - price: 336 + cost: 348 + init_stock: 580 + price: 348 service_level: 0.96 - vlt: 2 + vlt: 1 SKU357: - cost: 77 - init_stock: 660 - price: 77 + cost: 499 + init_stock: 1840 + price: 499 service_level: 0.96 - vlt: 1 + vlt: 3 SKU358: - cost: 340 - init_stock: 1340 - price: 340 + cost: 269 + init_stock: 1380 + price: 269 service_level: 0.96 - vlt: 3 + vlt: 2 SKU359: - cost: 259 - init_stock: 1280 - price: 259 + cost: 82 + init_stock: 1500 + price: 82 service_level: 0.96 - vlt: 2 + vlt: 3 SKU36: - cost: 223 - init_stock: 360 - price: 223 + cost: 105 + init_stock: 200 + price: 105 service_level: 0.96 - vlt: 1 + vlt: 2 SKU360: - cost: 146 - init_stock: 1080 - price: 146 + cost: 484 + init_stock: 500 + price: 484 service_level: 0.96 - vlt: 3 + vlt: 1 SKU361: - cost: 47 - init_stock: 1780 - price: 47 + cost: 163 + init_stock: 1800 + price: 163 service_level: 0.96 - vlt: 2 + vlt: 1 SKU362: - cost: 429 - init_stock: 1940 - price: 429 + cost: 464 + init_stock: 1740 + price: 464 service_level: 0.96 vlt: 2 SKU363: - cost: 495 - init_stock: 1600 - price: 495 + cost: 52 + init_stock: 1560 + price: 52 service_level: 0.96 - vlt: 3 + vlt: 2 SKU364: - cost: 213 - init_stock: 1800 - price: 213 + cost: 387 + init_stock: 660 + price: 387 service_level: 0.96 - vlt: 3 + vlt: 1 SKU365: - cost: 116 - init_stock: 240 - price: 116 + cost: 158 + init_stock: 1660 + price: 158 service_level: 0.96 - vlt: 1 + vlt: 3 SKU366: - cost: 344 - init_stock: 1640 - price: 344 + cost: 444 + init_stock: 1720 + price: 444 service_level: 0.96 - vlt: 1 + vlt: 3 SKU367: - cost: 197 - init_stock: 300 - price: 197 + cost: 272 + init_stock: 920 + price: 272 service_level: 0.96 - vlt: 1 + vlt: 3 SKU368: - cost: 410 - init_stock: 1200 - price: 410 + cost: 472 + init_stock: 660 + price: 472 service_level: 0.96 - vlt: 2 + vlt: 1 SKU369: - cost: 187 - init_stock: 1800 - price: 187 + cost: 96 + init_stock: 1500 + price: 96 service_level: 0.96 - vlt: 1 + vlt: 2 SKU37: - cost: 302 - init_stock: 900 - price: 302 + cost: 66 + init_stock: 1180 + price: 66 service_level: 0.96 - vlt: 1 + vlt: 2 SKU370: - cost: 210 - init_stock: 1680 - price: 210 + cost: 112 + init_stock: 300 + price: 112 service_level: 0.96 - vlt: 3 + vlt: 2 SKU371: - cost: 404 - init_stock: 1380 - price: 404 + cost: 328 + init_stock: 100 + price: 328 service_level: 0.96 vlt: 3 SKU372: - cost: 63 - init_stock: 1580 - price: 63 + cost: 67 + init_stock: 640 + price: 67 service_level: 0.96 - vlt: 3 + vlt: 1 SKU373: - cost: 106 - init_stock: 480 - price: 106 + cost: 58 + init_stock: 1340 + price: 58 service_level: 0.96 - vlt: 1 + vlt: 3 SKU374: - cost: 306 - init_stock: 720 - price: 306 + cost: 38 + init_stock: 1080 + price: 38 service_level: 0.96 - vlt: 3 + vlt: 2 SKU375: - cost: 153 - init_stock: 1020 - price: 153 + cost: 283 + init_stock: 1440 + price: 283 service_level: 0.96 - vlt: 3 + vlt: 1 SKU376: - cost: 273 - init_stock: 300 - price: 273 + cost: 156 + init_stock: 1640 + price: 156 service_level: 0.96 - vlt: 3 + vlt: 1 SKU377: - cost: 306 - init_stock: 1500 - price: 306 + cost: 78 + init_stock: 1340 + price: 78 service_level: 0.96 vlt: 3 SKU378: - cost: 233 - init_stock: 880 - price: 233 + cost: 424 + init_stock: 700 + price: 424 service_level: 0.96 - vlt: 3 + vlt: 1 SKU379: - cost: 10 - init_stock: 440 - price: 10 + cost: 11 + init_stock: 980 + price: 11 service_level: 0.96 - vlt: 3 + vlt: 1 SKU38: - cost: 355 - init_stock: 1260 - price: 355 + cost: 344 + init_stock: 860 + price: 344 service_level: 0.96 - vlt: 1 + vlt: 2 SKU380: - cost: 440 - init_stock: 460 - price: 440 + cost: 393 + init_stock: 1060 + price: 393 service_level: 0.96 - vlt: 2 + vlt: 1 SKU381: - cost: 402 - init_stock: 1600 - price: 402 + cost: 476 + init_stock: 120 + price: 476 service_level: 0.96 vlt: 1 SKU382: - cost: 478 - init_stock: 2000 - price: 478 + cost: 125 + init_stock: 1420 + price: 125 service_level: 0.96 - vlt: 1 + vlt: 2 SKU383: - cost: 248 - init_stock: 1400 - price: 248 + cost: 304 + init_stock: 1840 + price: 304 service_level: 0.96 vlt: 3 SKU384: - cost: 338 - init_stock: 640 - price: 338 + cost: 320 + init_stock: 180 + price: 320 service_level: 0.96 - vlt: 3 + vlt: 2 SKU385: - cost: 299 - init_stock: 1220 - price: 299 + cost: 120 + init_stock: 260 + price: 120 service_level: 0.96 vlt: 2 SKU386: - cost: 281 - init_stock: 1840 - price: 281 + cost: 295 + init_stock: 620 + price: 295 service_level: 0.96 - vlt: 3 + vlt: 1 SKU387: - cost: 157 - init_stock: 1880 - price: 157 + cost: 112 + init_stock: 1940 + price: 112 service_level: 0.96 - vlt: 1 + vlt: 3 SKU388: - cost: 214 - init_stock: 1080 - price: 214 + cost: 405 + init_stock: 880 + price: 405 service_level: 0.96 vlt: 2 SKU389: - cost: 129 - init_stock: 480 - price: 129 + cost: 376 + init_stock: 1400 + price: 376 service_level: 0.96 vlt: 2 SKU39: - cost: 26 - init_stock: 140 - price: 26 + cost: 25 + init_stock: 1020 + price: 25 service_level: 0.96 vlt: 3 SKU390: - cost: 377 - init_stock: 1900 - price: 377 + cost: 144 + init_stock: 780 + price: 144 service_level: 0.96 - vlt: 3 + vlt: 2 SKU391: - cost: 180 - init_stock: 1500 - price: 180 + cost: 233 + init_stock: 1340 + price: 233 service_level: 0.96 - vlt: 1 + vlt: 2 SKU392: - cost: 106 - init_stock: 1200 - price: 106 + cost: 163 + init_stock: 1480 + price: 163 service_level: 0.96 vlt: 2 SKU393: - cost: 128 - init_stock: 1040 - price: 128 + cost: 487 + init_stock: 1340 + price: 487 service_level: 0.96 - vlt: 3 + vlt: 1 SKU394: - cost: 414 - init_stock: 1200 - price: 414 - service_level: 0.96 - vlt: 3 - SKU395: - cost: 416 - init_stock: 1380 - price: 416 + cost: 154 + init_stock: 1060 + price: 154 service_level: 0.96 vlt: 2 + SKU395: + cost: 488 + init_stock: 660 + price: 488 + service_level: 0.96 + vlt: 3 SKU396: - cost: 426 - init_stock: 880 - price: 426 + cost: 333 + init_stock: 1220 + price: 333 service_level: 0.96 - vlt: 1 + vlt: 3 SKU397: - cost: 271 - init_stock: 300 - price: 271 + cost: 460 + init_stock: 1020 + price: 460 service_level: 0.96 - vlt: 3 + vlt: 1 SKU398: - cost: 21 - init_stock: 1180 - price: 21 + cost: 234 + init_stock: 1160 + price: 234 service_level: 0.96 - vlt: 1 + vlt: 2 SKU399: - cost: 269 - init_stock: 980 - price: 269 + cost: 376 + init_stock: 740 + price: 376 service_level: 0.96 - vlt: 1 + vlt: 2 SKU4: - cost: 190 - init_stock: 1700 - price: 190 + cost: 439 + init_stock: 140 + price: 439 service_level: 0.96 - vlt: 3 + vlt: 2 SKU40: - cost: 463 - init_stock: 1760 - price: 463 + cost: 490 + init_stock: 1980 + price: 490 service_level: 0.96 - vlt: 1 + vlt: 3 SKU400: - cost: 45 - init_stock: 1060 - price: 45 + cost: 445 + init_stock: 1680 + price: 445 service_level: 0.96 - vlt: 1 + vlt: 2 SKU401: - cost: 149 - init_stock: 1640 - price: 149 + cost: 410 + init_stock: 1560 + price: 410 service_level: 0.96 vlt: 1 SKU402: - cost: 396 - init_stock: 1580 - price: 396 + cost: 86 + init_stock: 140 + price: 86 service_level: 0.96 - vlt: 1 + vlt: 3 SKU403: - cost: 70 - init_stock: 1160 - price: 70 + cost: 89 + init_stock: 1980 + price: 89 service_level: 0.96 - vlt: 1 + vlt: 3 SKU404: - cost: 225 - init_stock: 1580 - price: 225 + cost: 287 + init_stock: 1220 + price: 287 service_level: 0.96 - vlt: 2 + vlt: 1 SKU405: - cost: 31 - init_stock: 440 - price: 31 + cost: 460 + init_stock: 380 + price: 460 service_level: 0.96 - vlt: 3 + vlt: 1 SKU406: - cost: 449 - init_stock: 860 - price: 449 + cost: 327 + init_stock: 2000 + price: 327 service_level: 0.96 vlt: 1 SKU407: - cost: 462 - init_stock: 320 - price: 462 + cost: 26 + init_stock: 920 + price: 26 service_level: 0.96 vlt: 2 SKU408: - cost: 364 - init_stock: 540 - price: 364 + cost: 444 + init_stock: 160 + price: 444 service_level: 0.96 - vlt: 1 + vlt: 2 SKU409: - cost: 424 - init_stock: 1160 - price: 424 + cost: 257 + init_stock: 1820 + price: 257 service_level: 0.96 - vlt: 1 + vlt: 2 SKU41: - cost: 56 - init_stock: 460 - price: 56 + cost: 141 + init_stock: 1580 + price: 141 service_level: 0.96 - vlt: 1 + vlt: 2 SKU410: - cost: 204 - init_stock: 180 - price: 204 + cost: 70 + init_stock: 320 + price: 70 service_level: 0.96 - vlt: 2 + vlt: 1 SKU411: - cost: 197 - init_stock: 120 - price: 197 + cost: 210 + init_stock: 1900 + price: 210 service_level: 0.96 vlt: 3 SKU412: - cost: 140 - init_stock: 260 - price: 140 + cost: 286 + init_stock: 1240 + price: 286 service_level: 0.96 - vlt: 1 + vlt: 2 SKU413: - cost: 11 - init_stock: 1420 - price: 11 + cost: 403 + init_stock: 1660 + price: 403 service_level: 0.96 - vlt: 2 + vlt: 3 SKU414: - cost: 492 - init_stock: 400 - price: 492 + cost: 165 + init_stock: 1740 + price: 165 service_level: 0.96 - vlt: 3 + vlt: 1 SKU415: - cost: 26 - init_stock: 1260 - price: 26 + cost: 291 + init_stock: 460 + price: 291 service_level: 0.96 - vlt: 1 + vlt: 3 SKU416: - cost: 151 - init_stock: 100 - price: 151 + cost: 228 + init_stock: 180 + price: 228 service_level: 0.96 - vlt: 2 + vlt: 3 SKU417: - cost: 306 - init_stock: 1140 - price: 306 + cost: 443 + init_stock: 1440 + price: 443 service_level: 0.96 - vlt: 3 + vlt: 1 SKU418: - cost: 154 - init_stock: 840 - price: 154 + cost: 458 + init_stock: 260 + price: 458 service_level: 0.96 - vlt: 2 + vlt: 3 SKU419: - cost: 192 - init_stock: 480 - price: 192 + cost: 303 + init_stock: 1780 + price: 303 service_level: 0.96 vlt: 3 SKU42: - cost: 106 - init_stock: 820 - price: 106 + cost: 462 + init_stock: 520 + price: 462 service_level: 0.96 vlt: 3 SKU420: - cost: 79 - init_stock: 1460 - price: 79 + cost: 268 + init_stock: 840 + price: 268 service_level: 0.96 - vlt: 3 + vlt: 1 SKU421: - cost: 288 - init_stock: 1280 - price: 288 + cost: 214 + init_stock: 920 + price: 214 service_level: 0.96 - vlt: 1 + vlt: 2 SKU422: - cost: 81 - init_stock: 1420 - price: 81 + cost: 52 + init_stock: 1080 + price: 52 service_level: 0.96 vlt: 3 SKU423: - cost: 48 - init_stock: 1580 - price: 48 + cost: 188 + init_stock: 1320 + price: 188 service_level: 0.96 - vlt: 2 + vlt: 1 SKU424: - cost: 344 - init_stock: 640 - price: 344 - service_level: 0.96 - vlt: 3 - SKU425: - cost: 155 - init_stock: 520 - price: 155 + cost: 189 + init_stock: 220 + price: 189 service_level: 0.96 vlt: 2 + SKU425: + cost: 162 + init_stock: 240 + price: 162 + service_level: 0.96 + vlt: 3 SKU426: - cost: 115 - init_stock: 1640 - price: 115 + cost: 125 + init_stock: 1960 + price: 125 service_level: 0.96 - vlt: 2 + vlt: 3 SKU427: - cost: 132 - init_stock: 100 - price: 132 + cost: 413 + init_stock: 1880 + price: 413 service_level: 0.96 vlt: 1 SKU428: - cost: 499 - init_stock: 1160 - price: 499 + cost: 391 + init_stock: 1260 + price: 391 service_level: 0.96 - vlt: 2 + vlt: 3 SKU429: - cost: 147 - init_stock: 320 - price: 147 + cost: 39 + init_stock: 820 + price: 39 service_level: 0.96 - vlt: 3 + vlt: 1 SKU43: - cost: 162 - init_stock: 1620 - price: 162 + cost: 217 + init_stock: 1360 + price: 217 service_level: 0.96 - vlt: 3 + vlt: 1 SKU430: - cost: 330 - init_stock: 580 - price: 330 + cost: 216 + init_stock: 1460 + price: 216 service_level: 0.96 vlt: 2 SKU431: - cost: 65 - init_stock: 1380 - price: 65 + cost: 328 + init_stock: 420 + price: 328 service_level: 0.96 vlt: 1 SKU432: - cost: 195 - init_stock: 1860 - price: 195 + cost: 25 + init_stock: 920 + price: 25 service_level: 0.96 vlt: 3 SKU433: - cost: 55 - init_stock: 1720 - price: 55 + cost: 348 + init_stock: 900 + price: 348 service_level: 0.96 vlt: 2 SKU434: - cost: 113 - init_stock: 1500 - price: 113 + cost: 37 + init_stock: 600 + price: 37 service_level: 0.96 - vlt: 2 + vlt: 1 SKU435: - cost: 256 - init_stock: 300 - price: 256 + cost: 272 + init_stock: 600 + price: 272 service_level: 0.96 - vlt: 2 + vlt: 3 SKU436: - cost: 61 - init_stock: 1960 - price: 61 + cost: 72 + init_stock: 1620 + price: 72 service_level: 0.96 - vlt: 3 + vlt: 1 SKU437: - cost: 367 - init_stock: 840 - price: 367 + cost: 115 + init_stock: 280 + price: 115 service_level: 0.96 vlt: 2 SKU438: - cost: 396 - init_stock: 1440 - price: 396 + cost: 143 + init_stock: 200 + price: 143 service_level: 0.96 - vlt: 2 + vlt: 3 SKU439: - cost: 99 - init_stock: 1240 - price: 99 + cost: 256 + init_stock: 760 + price: 256 service_level: 0.96 - vlt: 2 + vlt: 1 SKU44: - cost: 241 - init_stock: 1180 - price: 241 + cost: 360 + init_stock: 960 + price: 360 service_level: 0.96 - vlt: 3 + vlt: 1 SKU440: - cost: 455 - init_stock: 1020 - price: 455 + cost: 248 + init_stock: 1640 + price: 248 service_level: 0.96 - vlt: 1 + vlt: 2 SKU441: - cost: 217 - init_stock: 1300 - price: 217 + cost: 253 + init_stock: 1120 + price: 253 service_level: 0.96 - vlt: 3 + vlt: 2 SKU442: - cost: 460 - init_stock: 1160 - price: 460 + cost: 470 + init_stock: 1760 + price: 470 service_level: 0.96 - vlt: 1 + vlt: 2 SKU443: - cost: 375 - init_stock: 960 - price: 375 + cost: 218 + init_stock: 1460 + price: 218 service_level: 0.96 vlt: 1 SKU444: - cost: 44 - init_stock: 1960 - price: 44 + cost: 156 + init_stock: 120 + price: 156 service_level: 0.96 - vlt: 3 + vlt: 2 SKU445: - cost: 59 - init_stock: 1360 - price: 59 + cost: 260 + init_stock: 1720 + price: 260 service_level: 0.96 vlt: 2 SKU446: - cost: 183 - init_stock: 400 - price: 183 + cost: 497 + init_stock: 980 + price: 497 service_level: 0.96 - vlt: 3 + vlt: 1 SKU447: - cost: 439 - init_stock: 960 - price: 439 + cost: 196 + init_stock: 100 + price: 196 service_level: 0.96 - vlt: 3 + vlt: 1 SKU448: - cost: 499 - init_stock: 560 - price: 499 + cost: 485 + init_stock: 1420 + price: 485 service_level: 0.96 - vlt: 2 + vlt: 3 SKU449: - cost: 333 - init_stock: 1480 - price: 333 + cost: 282 + init_stock: 760 + price: 282 service_level: 0.96 - vlt: 3 + vlt: 2 SKU45: - cost: 134 - init_stock: 960 - price: 134 + cost: 253 + init_stock: 200 + price: 253 service_level: 0.96 vlt: 1 SKU450: - cost: 296 - init_stock: 620 - price: 296 + cost: 58 + init_stock: 1960 + price: 58 service_level: 0.96 vlt: 2 SKU451: - cost: 323 - init_stock: 340 - price: 323 + cost: 468 + init_stock: 1380 + price: 468 service_level: 0.96 - vlt: 1 + vlt: 3 SKU452: - cost: 52 - init_stock: 800 - price: 52 + cost: 63 + init_stock: 1580 + price: 63 service_level: 0.96 - vlt: 2 + vlt: 1 SKU453: - cost: 21 - init_stock: 1620 - price: 21 + cost: 37 + init_stock: 700 + price: 37 service_level: 0.96 - vlt: 2 + vlt: 3 SKU454: - cost: 198 - init_stock: 460 - price: 198 + cost: 494 + init_stock: 1700 + price: 494 service_level: 0.96 - vlt: 3 + vlt: 1 SKU455: - cost: 11 - init_stock: 1280 - price: 11 + cost: 136 + init_stock: 520 + price: 136 service_level: 0.96 vlt: 2 SKU456: - cost: 204 - init_stock: 1660 - price: 204 + cost: 338 + init_stock: 500 + price: 338 service_level: 0.96 - vlt: 1 - SKU457: - cost: 194 - init_stock: 2000 - price: 194 + vlt: 2 + SKU457: + cost: 169 + init_stock: 1280 + price: 169 service_level: 0.96 vlt: 2 SKU458: - cost: 110 - init_stock: 220 - price: 110 + cost: 403 + init_stock: 140 + price: 403 service_level: 0.96 vlt: 3 SKU459: - cost: 370 - init_stock: 1280 - price: 370 + cost: 425 + init_stock: 680 + price: 425 service_level: 0.96 - vlt: 1 + vlt: 3 SKU46: - cost: 90 - init_stock: 1980 - price: 90 + cost: 299 + init_stock: 1000 + price: 299 service_level: 0.96 vlt: 3 SKU460: - cost: 121 - init_stock: 1620 - price: 121 + cost: 405 + init_stock: 1460 + price: 405 service_level: 0.96 - vlt: 1 + vlt: 2 SKU461: - cost: 305 - init_stock: 440 - price: 305 + cost: 251 + init_stock: 960 + price: 251 service_level: 0.96 - vlt: 2 + vlt: 1 SKU462: - cost: 377 - init_stock: 1320 - price: 377 + cost: 448 + init_stock: 180 + price: 448 service_level: 0.96 vlt: 1 SKU463: - cost: 449 - init_stock: 160 - price: 449 + cost: 187 + init_stock: 1640 + price: 187 service_level: 0.96 vlt: 3 SKU464: - cost: 466 + cost: 279 init_stock: 680 - price: 466 + price: 279 service_level: 0.96 - vlt: 2 + vlt: 3 SKU465: - cost: 86 - init_stock: 1220 - price: 86 + cost: 147 + init_stock: 2000 + price: 147 service_level: 0.96 - vlt: 1 + vlt: 2 SKU466: - cost: 412 - init_stock: 1880 - price: 412 + cost: 97 + init_stock: 840 + price: 97 service_level: 0.96 vlt: 1 SKU467: - cost: 103 - init_stock: 1420 - price: 103 + cost: 142 + init_stock: 720 + price: 142 service_level: 0.96 - vlt: 1 + vlt: 2 SKU468: - cost: 453 - init_stock: 1760 - price: 453 + cost: 55 + init_stock: 1000 + price: 55 service_level: 0.96 vlt: 2 SKU469: - cost: 422 - init_stock: 1860 - price: 422 + cost: 178 + init_stock: 300 + price: 178 service_level: 0.96 - vlt: 1 + vlt: 3 SKU47: - cost: 51 - init_stock: 740 - price: 51 + cost: 121 + init_stock: 780 + price: 121 service_level: 0.96 - vlt: 2 + vlt: 1 SKU470: - cost: 284 - init_stock: 560 - price: 284 + cost: 373 + init_stock: 640 + price: 373 service_level: 0.96 - vlt: 3 + vlt: 2 SKU471: - cost: 487 - init_stock: 460 - price: 487 + cost: 315 + init_stock: 1420 + price: 315 service_level: 0.96 - vlt: 1 + vlt: 3 SKU472: - cost: 465 - init_stock: 1920 - price: 465 + cost: 421 + init_stock: 1180 + price: 421 service_level: 0.96 - vlt: 1 + vlt: 2 SKU473: - cost: 271 - init_stock: 520 - price: 271 + cost: 195 + init_stock: 420 + price: 195 service_level: 0.96 - vlt: 2 + vlt: 1 SKU474: - cost: 68 - init_stock: 500 - price: 68 + cost: 448 + init_stock: 1720 + price: 448 service_level: 0.96 vlt: 2 SKU475: - cost: 320 - init_stock: 1440 - price: 320 + cost: 34 + init_stock: 1880 + price: 34 service_level: 0.96 vlt: 1 SKU476: - cost: 237 - init_stock: 980 - price: 237 + cost: 251 + init_stock: 1800 + price: 251 service_level: 0.96 vlt: 1 SKU477: - cost: 455 - init_stock: 1380 - price: 455 + cost: 289 + init_stock: 940 + price: 289 service_level: 0.96 vlt: 2 SKU478: - cost: 40 - init_stock: 1480 - price: 40 + cost: 479 + init_stock: 1760 + price: 479 service_level: 0.96 vlt: 2 SKU479: - cost: 295 - init_stock: 860 - price: 295 + cost: 366 + init_stock: 760 + price: 366 service_level: 0.96 - vlt: 2 + vlt: 1 SKU48: - cost: 395 - init_stock: 1820 - price: 395 + cost: 340 + init_stock: 1160 + price: 340 service_level: 0.96 - vlt: 3 + vlt: 2 SKU480: - cost: 94 - init_stock: 1300 - price: 94 + cost: 18 + init_stock: 600 + price: 18 service_level: 0.96 - vlt: 1 + vlt: 2 SKU481: - cost: 408 - init_stock: 420 - price: 408 + cost: 330 + init_stock: 1760 + price: 330 service_level: 0.96 vlt: 1 SKU482: - cost: 198 + cost: 94 init_stock: 1740 - price: 198 + price: 94 service_level: 0.96 - vlt: 1 + vlt: 3 SKU483: - cost: 85 - init_stock: 1640 - price: 85 + cost: 298 + init_stock: 680 + price: 298 service_level: 0.96 vlt: 2 SKU484: - cost: 460 - init_stock: 840 - price: 460 + cost: 84 + init_stock: 1460 + price: 84 service_level: 0.96 vlt: 3 SKU485: - cost: 308 - init_stock: 700 - price: 308 + cost: 318 + init_stock: 1340 + price: 318 service_level: 0.96 vlt: 3 SKU486: - cost: 84 - init_stock: 1220 - price: 84 + cost: 122 + init_stock: 1040 + price: 122 service_level: 0.96 - vlt: 2 + vlt: 1 SKU487: - cost: 421 - init_stock: 1240 - price: 421 + cost: 277 + init_stock: 1320 + price: 277 service_level: 0.96 - vlt: 2 + vlt: 3 SKU488: - cost: 108 - init_stock: 580 - price: 108 + cost: 36 + init_stock: 540 + price: 36 service_level: 0.96 - vlt: 2 + vlt: 3 SKU489: - cost: 146 - init_stock: 600 - price: 146 + cost: 59 + init_stock: 620 + price: 59 service_level: 0.96 - vlt: 2 + vlt: 3 SKU49: - cost: 320 - init_stock: 880 - price: 320 + cost: 263 + init_stock: 1900 + price: 263 + service_level: 0.96 + vlt: 3 + SKU490: + cost: 161 + init_stock: 1620 + price: 161 service_level: 0.96 vlt: 2 - SKU490: - cost: 43 - init_stock: 380 - price: 43 - service_level: 0.96 - vlt: 1 SKU491: - cost: 134 - init_stock: 1780 - price: 134 + cost: 444 + init_stock: 1500 + price: 444 service_level: 0.96 vlt: 2 SKU492: - cost: 370 - init_stock: 1720 - price: 370 + cost: 53 + init_stock: 1600 + price: 53 service_level: 0.96 vlt: 2 SKU493: - cost: 344 - init_stock: 400 - price: 344 + cost: 461 + init_stock: 540 + price: 461 service_level: 0.96 - vlt: 2 + vlt: 1 SKU494: - cost: 87 - init_stock: 980 - price: 87 + cost: 145 + init_stock: 1880 + price: 145 service_level: 0.96 - vlt: 2 + vlt: 1 SKU495: - cost: 183 - init_stock: 1700 - price: 183 + cost: 149 + init_stock: 1240 + price: 149 service_level: 0.96 vlt: 3 SKU496: - cost: 222 - init_stock: 1280 - price: 222 + cost: 342 + init_stock: 1180 + price: 342 service_level: 0.96 - vlt: 3 + vlt: 1 SKU497: - cost: 453 - init_stock: 720 - price: 453 + cost: 33 + init_stock: 180 + price: 33 service_level: 0.96 - vlt: 2 + vlt: 3 SKU498: - cost: 475 - init_stock: 500 - price: 475 + cost: 305 + init_stock: 1300 + price: 305 service_level: 0.96 vlt: 3 SKU499: - cost: 122 - init_stock: 660 - price: 122 + cost: 307 + init_stock: 580 + price: 307 service_level: 0.96 - vlt: 1 + vlt: 2 SKU5: - cost: 121 - init_stock: 1520 - price: 121 + cost: 466 + init_stock: 1420 + price: 466 service_level: 0.96 vlt: 2 SKU50: - cost: 298 - init_stock: 1140 - price: 298 + cost: 282 + init_stock: 740 + price: 282 service_level: 0.96 - vlt: 2 + vlt: 3 SKU500: - cost: 127 - init_stock: 1740 - price: 127 + cost: 208 + init_stock: 840 + price: 208 service_level: 0.96 - vlt: 2 + vlt: 3 SKU501: - cost: 328 - init_stock: 1660 - price: 328 + cost: 194 + init_stock: 1520 + price: 194 service_level: 0.96 - vlt: 3 + vlt: 1 SKU502: - cost: 335 - init_stock: 640 - price: 335 + cost: 69 + init_stock: 1300 + price: 69 service_level: 0.96 - vlt: 3 + vlt: 2 SKU503: - cost: 86 - init_stock: 120 - price: 86 + cost: 208 + init_stock: 1140 + price: 208 service_level: 0.96 vlt: 1 SKU504: - cost: 14 - init_stock: 1300 - price: 14 + cost: 251 + init_stock: 600 + price: 251 service_level: 0.96 - vlt: 1 + vlt: 2 SKU505: - cost: 74 - init_stock: 1580 - price: 74 + cost: 426 + init_stock: 1180 + price: 426 service_level: 0.96 - vlt: 3 + vlt: 1 SKU506: - cost: 51 - init_stock: 1000 - price: 51 + cost: 149 + init_stock: 1860 + price: 149 service_level: 0.96 - vlt: 2 + vlt: 3 SKU507: - cost: 37 - init_stock: 340 - price: 37 + cost: 187 + init_stock: 300 + price: 187 service_level: 0.96 vlt: 1 SKU508: - cost: 445 - init_stock: 1900 - price: 445 + cost: 272 + init_stock: 1920 + price: 272 service_level: 0.96 - vlt: 2 + vlt: 1 SKU509: - cost: 314 - init_stock: 280 - price: 314 + cost: 297 + init_stock: 1620 + price: 297 service_level: 0.96 - vlt: 2 + vlt: 1 SKU51: - cost: 329 - init_stock: 660 - price: 329 + cost: 295 + init_stock: 1340 + price: 295 service_level: 0.96 - vlt: 2 + vlt: 3 SKU510: - cost: 230 - init_stock: 1240 - price: 230 + cost: 94 + init_stock: 180 + price: 94 service_level: 0.96 - vlt: 3 + vlt: 1 SKU511: - cost: 21 - init_stock: 680 - price: 21 + cost: 361 + init_stock: 1500 + price: 361 service_level: 0.96 vlt: 1 SKU512: - cost: 424 - init_stock: 660 - price: 424 + cost: 467 + init_stock: 440 + price: 467 service_level: 0.96 - vlt: 1 + vlt: 2 SKU513: - cost: 27 - init_stock: 120 - price: 27 + cost: 343 + init_stock: 140 + price: 343 service_level: 0.96 - vlt: 1 + vlt: 3 SKU514: - cost: 78 - init_stock: 1020 - price: 78 + cost: 186 + init_stock: 1260 + price: 186 service_level: 0.96 vlt: 3 SKU515: - cost: 490 - init_stock: 1840 - price: 490 + cost: 145 + init_stock: 1080 + price: 145 service_level: 0.96 vlt: 3 SKU516: - cost: 103 - init_stock: 300 - price: 103 + cost: 64 + init_stock: 760 + price: 64 service_level: 0.96 vlt: 1 SKU517: - cost: 198 - init_stock: 820 - price: 198 + cost: 117 + init_stock: 180 + price: 117 service_level: 0.96 - vlt: 2 + vlt: 3 SKU518: - cost: 492 - init_stock: 300 - price: 492 + cost: 26 + init_stock: 420 + price: 26 service_level: 0.96 - vlt: 2 + vlt: 3 SKU519: - cost: 490 - init_stock: 780 - price: 490 + cost: 233 + init_stock: 1000 + price: 233 service_level: 0.96 - vlt: 1 + vlt: 2 SKU52: - cost: 183 - init_stock: 1260 - price: 183 + cost: 22 + init_stock: 1340 + price: 22 service_level: 0.96 vlt: 1 SKU520: - cost: 400 - init_stock: 1720 - price: 400 + cost: 209 + init_stock: 440 + price: 209 service_level: 0.96 - vlt: 3 - SKU521: - cost: 212 - init_stock: 1240 - price: 212 + vlt: 1 + SKU521: + cost: 220 + init_stock: 1000 + price: 220 service_level: 0.96 vlt: 3 SKU522: - cost: 328 - init_stock: 780 - price: 328 + cost: 156 + init_stock: 1740 + price: 156 service_level: 0.96 - vlt: 3 + vlt: 1 SKU523: - cost: 23 - init_stock: 1000 - price: 23 + cost: 65 + init_stock: 2000 + price: 65 service_level: 0.96 - vlt: 2 + vlt: 3 SKU524: - cost: 92 - init_stock: 1500 - price: 92 + cost: 294 + init_stock: 1880 + price: 294 service_level: 0.96 vlt: 1 SKU525: - cost: 261 - init_stock: 1680 - price: 261 + cost: 432 + init_stock: 840 + price: 432 service_level: 0.96 - vlt: 3 + vlt: 2 SKU526: - cost: 126 - init_stock: 740 - price: 126 + cost: 419 + init_stock: 480 + price: 419 service_level: 0.96 vlt: 3 SKU527: - cost: 42 - init_stock: 280 - price: 42 + cost: 131 + init_stock: 1400 + price: 131 service_level: 0.96 - vlt: 3 + vlt: 1 SKU528: - cost: 22 - init_stock: 1800 - price: 22 + cost: 316 + init_stock: 420 + price: 316 service_level: 0.96 - vlt: 2 + vlt: 3 SKU529: - cost: 271 - init_stock: 1620 - price: 271 + cost: 298 + init_stock: 620 + price: 298 service_level: 0.96 - vlt: 1 + vlt: 3 SKU53: - cost: 438 - init_stock: 1540 - price: 438 + cost: 151 + init_stock: 1400 + price: 151 service_level: 0.96 vlt: 2 SKU530: - cost: 64 - init_stock: 1300 - price: 64 + cost: 131 + init_stock: 900 + price: 131 service_level: 0.96 - vlt: 3 + vlt: 1 SKU531: - cost: 273 - init_stock: 1220 - price: 273 + cost: 103 + init_stock: 1200 + price: 103 service_level: 0.96 - vlt: 2 + vlt: 3 SKU532: - cost: 436 - init_stock: 1560 - price: 436 + cost: 351 + init_stock: 1540 + price: 351 service_level: 0.96 - vlt: 2 + vlt: 3 SKU533: - cost: 405 - init_stock: 420 - price: 405 + cost: 296 + init_stock: 840 + price: 296 service_level: 0.96 vlt: 3 SKU534: - cost: 375 - init_stock: 1660 - price: 375 + cost: 425 + init_stock: 820 + price: 425 service_level: 0.96 - vlt: 3 + vlt: 1 SKU535: - cost: 423 - init_stock: 1560 - price: 423 + cost: 304 + init_stock: 1720 + price: 304 service_level: 0.96 vlt: 2 SKU536: - cost: 296 - init_stock: 1160 - price: 296 + cost: 358 + init_stock: 1040 + price: 358 service_level: 0.96 - vlt: 1 + vlt: 2 SKU537: - cost: 476 - init_stock: 580 - price: 476 + cost: 321 + init_stock: 180 + price: 321 service_level: 0.96 - vlt: 2 + vlt: 3 SKU538: - cost: 451 - init_stock: 460 - price: 451 + cost: 457 + init_stock: 1000 + price: 457 service_level: 0.96 - vlt: 2 + vlt: 1 SKU539: - cost: 372 - init_stock: 880 - price: 372 + cost: 165 + init_stock: 1560 + price: 165 service_level: 0.96 - vlt: 1 + vlt: 2 SKU54: - cost: 141 - init_stock: 1180 - price: 141 + cost: 158 + init_stock: 920 + price: 158 service_level: 0.96 vlt: 1 SKU540: - cost: 25 - init_stock: 1220 - price: 25 + cost: 388 + init_stock: 840 + price: 388 service_level: 0.96 - vlt: 2 + vlt: 3 SKU541: - cost: 230 - init_stock: 980 - price: 230 + cost: 131 + init_stock: 580 + price: 131 service_level: 0.96 - vlt: 1 + vlt: 2 SKU542: - cost: 314 - init_stock: 940 - price: 314 + cost: 38 + init_stock: 520 + price: 38 service_level: 0.96 - vlt: 1 + vlt: 3 SKU543: - cost: 421 - init_stock: 1860 - price: 421 + cost: 430 + init_stock: 1700 + price: 430 service_level: 0.96 - vlt: 3 + vlt: 2 SKU544: - cost: 245 - init_stock: 280 - price: 245 + cost: 346 + init_stock: 1940 + price: 346 service_level: 0.96 - vlt: 2 + vlt: 1 SKU545: - cost: 324 - init_stock: 600 - price: 324 + cost: 175 + init_stock: 300 + price: 175 service_level: 0.96 - vlt: 2 + vlt: 3 SKU546: - cost: 244 - init_stock: 200 - price: 244 + cost: 491 + init_stock: 1920 + price: 491 service_level: 0.96 - vlt: 2 + vlt: 3 SKU547: - cost: 224 - init_stock: 360 - price: 224 + cost: 161 + init_stock: 880 + price: 161 service_level: 0.96 vlt: 2 SKU548: - cost: 181 - init_stock: 580 - price: 181 + cost: 111 + init_stock: 120 + price: 111 service_level: 0.96 - vlt: 3 + vlt: 1 SKU549: - cost: 380 - init_stock: 1680 - price: 380 + cost: 387 + init_stock: 1580 + price: 387 service_level: 0.96 - vlt: 3 + vlt: 1 SKU55: - cost: 303 - init_stock: 1780 - price: 303 + cost: 482 + init_stock: 1300 + price: 482 service_level: 0.96 - vlt: 1 + vlt: 3 SKU550: - cost: 340 - init_stock: 440 - price: 340 + cost: 259 + init_stock: 1880 + price: 259 service_level: 0.96 - vlt: 3 + vlt: 1 SKU551: - cost: 455 - init_stock: 960 - price: 455 + cost: 46 + init_stock: 620 + price: 46 service_level: 0.96 vlt: 2 SKU552: - cost: 242 - init_stock: 1500 - price: 242 + cost: 191 + init_stock: 1800 + price: 191 service_level: 0.96 vlt: 3 SKU553: - cost: 221 - init_stock: 920 - price: 221 + cost: 208 + init_stock: 600 + price: 208 service_level: 0.96 - vlt: 3 + vlt: 1 SKU554: - cost: 48 - init_stock: 1300 - price: 48 + cost: 340 + init_stock: 820 + price: 340 service_level: 0.96 vlt: 1 SKU555: - cost: 208 - init_stock: 1060 - price: 208 + cost: 489 + init_stock: 1680 + price: 489 service_level: 0.96 - vlt: 3 + vlt: 1 SKU556: - cost: 496 - init_stock: 920 - price: 496 + cost: 110 + init_stock: 600 + price: 110 service_level: 0.96 - vlt: 3 + vlt: 2 SKU557: - cost: 275 - init_stock: 1900 - price: 275 + cost: 334 + init_stock: 1460 + price: 334 service_level: 0.96 vlt: 2 SKU558: - cost: 342 - init_stock: 1460 - price: 342 + cost: 399 + init_stock: 340 + price: 399 service_level: 0.96 - vlt: 2 + vlt: 1 SKU559: - cost: 215 - init_stock: 1920 - price: 215 + cost: 313 + init_stock: 1080 + price: 313 service_level: 0.96 - vlt: 3 + vlt: 1 SKU56: - cost: 446 - init_stock: 2000 - price: 446 + cost: 377 + init_stock: 1480 + price: 377 service_level: 0.96 - vlt: 2 + vlt: 1 SKU560: - cost: 363 - init_stock: 120 - price: 363 + cost: 283 + init_stock: 820 + price: 283 service_level: 0.96 - vlt: 3 + vlt: 1 SKU561: - cost: 158 - init_stock: 740 - price: 158 + cost: 202 + init_stock: 780 + price: 202 service_level: 0.96 vlt: 3 SKU562: - cost: 260 - init_stock: 1320 - price: 260 + cost: 347 + init_stock: 1780 + price: 347 service_level: 0.96 vlt: 2 SKU563: - cost: 465 - init_stock: 420 - price: 465 + cost: 401 + init_stock: 100 + price: 401 service_level: 0.96 vlt: 3 SKU564: - cost: 356 - init_stock: 220 - price: 356 + cost: 109 + init_stock: 180 + price: 109 service_level: 0.96 - vlt: 1 + vlt: 2 SKU565: - cost: 479 - init_stock: 960 - price: 479 + cost: 57 + init_stock: 1500 + price: 57 service_level: 0.96 - vlt: 1 + vlt: 3 SKU566: - cost: 441 - init_stock: 1380 - price: 441 + cost: 131 + init_stock: 220 + price: 131 service_level: 0.96 - vlt: 3 + vlt: 2 SKU567: - cost: 88 - init_stock: 1160 - price: 88 + cost: 361 + init_stock: 180 + price: 361 service_level: 0.96 - vlt: 3 + vlt: 1 SKU568: - cost: 210 - init_stock: 540 - price: 210 + cost: 75 + init_stock: 1560 + price: 75 service_level: 0.96 - vlt: 3 + vlt: 2 SKU569: - cost: 432 - init_stock: 840 - price: 432 + cost: 473 + init_stock: 960 + price: 473 service_level: 0.96 - vlt: 3 + vlt: 2 SKU57: - cost: 262 - init_stock: 1400 - price: 262 + cost: 359 + init_stock: 1000 + price: 359 service_level: 0.96 - vlt: 3 + vlt: 2 SKU570: - cost: 143 - init_stock: 200 - price: 143 + cost: 388 + init_stock: 1660 + price: 388 service_level: 0.96 - vlt: 3 + vlt: 2 SKU571: - cost: 204 - init_stock: 1280 - price: 204 + cost: 168 + init_stock: 780 + price: 168 service_level: 0.96 vlt: 1 SKU572: - cost: 291 - init_stock: 1140 - price: 291 + cost: 26 + init_stock: 900 + price: 26 service_level: 0.96 - vlt: 2 + vlt: 3 SKU573: - cost: 458 - init_stock: 500 - price: 458 + cost: 246 + init_stock: 820 + price: 246 service_level: 0.96 - vlt: 1 + vlt: 2 SKU574: - cost: 243 - init_stock: 1880 - price: 243 + cost: 94 + init_stock: 1000 + price: 94 service_level: 0.96 vlt: 2 SKU575: - cost: 328 - init_stock: 1440 - price: 328 + cost: 237 + init_stock: 1580 + price: 237 service_level: 0.96 vlt: 1 SKU576: - cost: 333 - init_stock: 1400 - price: 333 + cost: 265 + init_stock: 1560 + price: 265 service_level: 0.96 vlt: 3 SKU577: - cost: 368 - init_stock: 300 - price: 368 + cost: 18 + init_stock: 1740 + price: 18 service_level: 0.96 - vlt: 2 + vlt: 3 SKU578: - cost: 82 - init_stock: 1200 - price: 82 + cost: 100 + init_stock: 1420 + price: 100 service_level: 0.96 vlt: 2 SKU579: - cost: 372 - init_stock: 380 - price: 372 + cost: 415 + init_stock: 180 + price: 415 service_level: 0.96 - vlt: 2 + vlt: 1 SKU58: - cost: 409 - init_stock: 1340 - price: 409 + cost: 362 + init_stock: 960 + price: 362 service_level: 0.96 - vlt: 3 + vlt: 2 SKU580: - cost: 421 - init_stock: 1080 - price: 421 + cost: 203 + init_stock: 100 + price: 203 service_level: 0.96 - vlt: 3 + vlt: 1 SKU581: - cost: 290 - init_stock: 1140 - price: 290 + cost: 152 + init_stock: 1720 + price: 152 service_level: 0.96 - vlt: 3 + vlt: 2 SKU582: - cost: 130 - init_stock: 100 - price: 130 + cost: 359 + init_stock: 1920 + price: 359 service_level: 0.96 - vlt: 3 + vlt: 2 SKU583: - cost: 172 - init_stock: 560 - price: 172 + cost: 86 + init_stock: 1740 + price: 86 service_level: 0.96 - vlt: 3 + vlt: 1 SKU584: - cost: 173 - init_stock: 1700 - price: 173 + cost: 33 + init_stock: 900 + price: 33 + service_level: 0.96 + vlt: 2 + SKU585: + cost: 31 + init_stock: 400 + price: 31 service_level: 0.96 vlt: 2 - SKU585: - cost: 201 - init_stock: 1580 - price: 201 - service_level: 0.96 - vlt: 3 SKU586: - cost: 150 - init_stock: 1040 - price: 150 + cost: 61 + init_stock: 880 + price: 61 service_level: 0.96 - vlt: 1 + vlt: 2 SKU587: - cost: 198 - init_stock: 720 - price: 198 + cost: 290 + init_stock: 1560 + price: 290 service_level: 0.96 vlt: 2 SKU588: - cost: 250 - init_stock: 1180 - price: 250 + cost: 476 + init_stock: 880 + price: 476 service_level: 0.96 - vlt: 1 + vlt: 2 SKU589: - cost: 51 - init_stock: 1820 - price: 51 + cost: 244 + init_stock: 680 + price: 244 service_level: 0.96 - vlt: 1 + vlt: 3 SKU59: - cost: 11 - init_stock: 240 - price: 11 + cost: 145 + init_stock: 1020 + price: 145 service_level: 0.96 - vlt: 2 + vlt: 1 SKU590: - cost: 492 - init_stock: 360 - price: 492 + cost: 70 + init_stock: 620 + price: 70 service_level: 0.96 vlt: 2 SKU591: - cost: 266 - init_stock: 1800 - price: 266 + cost: 206 + init_stock: 1680 + price: 206 service_level: 0.96 - vlt: 3 + vlt: 2 SKU592: - cost: 293 - init_stock: 1580 - price: 293 + cost: 218 + init_stock: 920 + price: 218 service_level: 0.96 - vlt: 3 + vlt: 1 SKU593: - cost: 361 - init_stock: 1980 - price: 361 + cost: 124 + init_stock: 880 + price: 124 service_level: 0.96 - vlt: 3 + vlt: 1 SKU594: - cost: 234 - init_stock: 260 - price: 234 + cost: 238 + init_stock: 1000 + price: 238 service_level: 0.96 - vlt: 3 + vlt: 1 SKU595: - cost: 37 - init_stock: 880 - price: 37 + cost: 73 + init_stock: 860 + price: 73 service_level: 0.96 - vlt: 2 + vlt: 3 SKU596: - cost: 245 - init_stock: 1620 - price: 245 + cost: 432 + init_stock: 140 + price: 432 service_level: 0.96 - vlt: 3 + vlt: 2 SKU597: - cost: 27 - init_stock: 780 - price: 27 + cost: 252 + init_stock: 1740 + price: 252 service_level: 0.96 - vlt: 3 + vlt: 2 SKU598: - cost: 290 - init_stock: 920 - price: 290 + cost: 348 + init_stock: 280 + price: 348 service_level: 0.96 vlt: 1 SKU599: - cost: 449 - init_stock: 820 - price: 449 + cost: 54 + init_stock: 1360 + price: 54 service_level: 0.96 vlt: 2 SKU6: - cost: 90 - init_stock: 340 - price: 90 + cost: 122 + init_stock: 1760 + price: 122 service_level: 0.96 - vlt: 3 + vlt: 2 SKU60: - cost: 478 - init_stock: 1780 - price: 478 + cost: 200 + init_stock: 780 + price: 200 service_level: 0.96 - vlt: 2 + vlt: 1 SKU600: - cost: 411 - init_stock: 480 - price: 411 + cost: 386 + init_stock: 1300 + price: 386 service_level: 0.96 - vlt: 2 + vlt: 1 SKU601: - cost: 363 - init_stock: 900 - price: 363 + cost: 138 + init_stock: 780 + price: 138 service_level: 0.96 - vlt: 1 + vlt: 3 SKU602: - cost: 352 - init_stock: 980 - price: 352 + cost: 184 + init_stock: 1960 + price: 184 service_level: 0.96 - vlt: 3 + vlt: 1 SKU603: - cost: 36 - init_stock: 1180 - price: 36 + cost: 278 + init_stock: 840 + price: 278 service_level: 0.96 vlt: 3 SKU604: - cost: 330 - init_stock: 1560 - price: 330 + cost: 270 + init_stock: 1000 + price: 270 service_level: 0.96 vlt: 3 SKU605: - cost: 420 - init_stock: 1860 - price: 420 + cost: 288 + init_stock: 480 + price: 288 service_level: 0.96 - vlt: 3 + vlt: 2 SKU606: - cost: 424 - init_stock: 1280 - price: 424 + cost: 114 + init_stock: 220 + price: 114 service_level: 0.96 - vlt: 1 + vlt: 3 SKU607: - cost: 144 - init_stock: 220 - price: 144 + cost: 208 + init_stock: 1580 + price: 208 service_level: 0.96 vlt: 2 SKU608: - cost: 35 - init_stock: 880 - price: 35 + cost: 39 + init_stock: 1460 + price: 39 service_level: 0.96 - vlt: 2 + vlt: 1 SKU609: - cost: 157 - init_stock: 800 - price: 157 + cost: 102 + init_stock: 380 + price: 102 service_level: 0.96 vlt: 2 SKU61: - cost: 317 - init_stock: 1660 - price: 317 + cost: 461 + init_stock: 1500 + price: 461 service_level: 0.96 - vlt: 2 + vlt: 1 SKU610: - cost: 62 - init_stock: 240 - price: 62 + cost: 449 + init_stock: 1360 + price: 449 service_level: 0.96 - vlt: 3 + vlt: 1 SKU611: - cost: 258 - init_stock: 1820 - price: 258 + cost: 306 + init_stock: 1540 + price: 306 service_level: 0.96 - vlt: 1 + vlt: 3 SKU612: - cost: 57 - init_stock: 1440 - price: 57 + cost: 391 + init_stock: 1760 + price: 391 service_level: 0.96 - vlt: 3 + vlt: 2 SKU613: - cost: 443 - init_stock: 860 - price: 443 + cost: 174 + init_stock: 140 + price: 174 service_level: 0.96 - vlt: 2 + vlt: 3 SKU614: - cost: 248 - init_stock: 1720 - price: 248 + cost: 173 + init_stock: 300 + price: 173 service_level: 0.96 - vlt: 2 + vlt: 1 SKU615: - cost: 259 - init_stock: 1200 - price: 259 + cost: 330 + init_stock: 1820 + price: 330 service_level: 0.96 - vlt: 1 + vlt: 2 SKU616: - cost: 188 - init_stock: 1760 - price: 188 + cost: 232 + init_stock: 1460 + price: 232 service_level: 0.96 - vlt: 2 + vlt: 1 SKU617: - cost: 334 - init_stock: 820 - price: 334 + cost: 203 + init_stock: 1200 + price: 203 service_level: 0.96 - vlt: 1 + vlt: 3 SKU618: - cost: 87 - init_stock: 620 - price: 87 + cost: 77 + init_stock: 460 + price: 77 service_level: 0.96 - vlt: 3 + vlt: 1 SKU619: - cost: 215 - init_stock: 700 - price: 215 + cost: 189 + init_stock: 1720 + price: 189 service_level: 0.96 vlt: 2 SKU62: - cost: 157 - init_stock: 1460 - price: 157 + cost: 124 + init_stock: 180 + price: 124 service_level: 0.96 - vlt: 1 + vlt: 2 SKU620: - cost: 12 - init_stock: 2000 - price: 12 + cost: 231 + init_stock: 780 + price: 231 service_level: 0.96 - vlt: 2 + vlt: 3 SKU621: - cost: 363 - init_stock: 320 - price: 363 + cost: 199 + init_stock: 1440 + price: 199 service_level: 0.96 vlt: 2 SKU622: - cost: 152 - init_stock: 1460 - price: 152 + cost: 253 + init_stock: 1300 + price: 253 service_level: 0.96 - vlt: 1 + vlt: 3 SKU623: - cost: 263 - init_stock: 200 - price: 263 + cost: 335 + init_stock: 1100 + price: 335 service_level: 0.96 - vlt: 2 + vlt: 1 SKU624: - cost: 423 - init_stock: 1920 - price: 423 + cost: 482 + init_stock: 1080 + price: 482 service_level: 0.96 vlt: 2 SKU625: - cost: 294 - init_stock: 1960 - price: 294 + cost: 46 + init_stock: 1780 + price: 46 service_level: 0.96 vlt: 1 SKU626: - cost: 204 - init_stock: 1160 - price: 204 + cost: 451 + init_stock: 1780 + price: 451 service_level: 0.96 - vlt: 1 + vlt: 3 SKU627: - cost: 175 - init_stock: 1760 - price: 175 + cost: 220 + init_stock: 1640 + price: 220 service_level: 0.96 - vlt: 1 + vlt: 3 SKU628: - cost: 14 - init_stock: 1640 - price: 14 + cost: 124 + init_stock: 520 + price: 124 service_level: 0.96 - vlt: 1 + vlt: 2 SKU629: - cost: 352 - init_stock: 1500 - price: 352 + cost: 353 + init_stock: 1840 + price: 353 service_level: 0.96 vlt: 1 SKU63: - cost: 305 - init_stock: 120 - price: 305 + cost: 68 + init_stock: 280 + price: 68 service_level: 0.96 - vlt: 2 + vlt: 3 SKU630: - cost: 407 - init_stock: 1580 - price: 407 + cost: 122 + init_stock: 340 + price: 122 service_level: 0.96 vlt: 1 SKU631: - cost: 370 - init_stock: 660 - price: 370 + cost: 296 + init_stock: 980 + price: 296 service_level: 0.96 vlt: 2 SKU632: - cost: 489 - init_stock: 300 - price: 489 + cost: 85 + init_stock: 1880 + price: 85 service_level: 0.96 - vlt: 3 + vlt: 2 SKU633: - cost: 298 - init_stock: 1300 - price: 298 + cost: 184 + init_stock: 1340 + price: 184 service_level: 0.96 - vlt: 1 + vlt: 2 SKU634: - cost: 52 - init_stock: 1480 - price: 52 + cost: 17 + init_stock: 1460 + price: 17 service_level: 0.96 - vlt: 3 + vlt: 1 SKU635: - cost: 30 - init_stock: 1940 - price: 30 + cost: 341 + init_stock: 1860 + price: 341 service_level: 0.96 vlt: 1 SKU636: - cost: 38 - init_stock: 1000 - price: 38 + cost: 385 + init_stock: 740 + price: 385 service_level: 0.96 - vlt: 2 + vlt: 1 SKU637: - cost: 474 - init_stock: 820 - price: 474 + cost: 83 + init_stock: 1220 + price: 83 service_level: 0.96 - vlt: 2 + vlt: 3 SKU638: - cost: 267 - init_stock: 1080 - price: 267 + cost: 375 + init_stock: 160 + price: 375 service_level: 0.96 - vlt: 3 + vlt: 1 SKU639: - cost: 466 - init_stock: 520 - price: 466 + cost: 327 + init_stock: 800 + price: 327 service_level: 0.96 vlt: 2 SKU64: - cost: 184 - init_stock: 1080 - price: 184 + cost: 36 + init_stock: 380 + price: 36 service_level: 0.96 - vlt: 1 + vlt: 3 SKU640: - cost: 236 - init_stock: 360 - price: 236 + cost: 275 + init_stock: 1820 + price: 275 service_level: 0.96 - vlt: 2 + vlt: 1 SKU641: - cost: 285 - init_stock: 1220 - price: 285 + cost: 365 + init_stock: 1620 + price: 365 service_level: 0.96 - vlt: 2 + vlt: 1 SKU642: - cost: 133 - init_stock: 1680 - price: 133 + cost: 414 + init_stock: 500 + price: 414 service_level: 0.96 - vlt: 3 + vlt: 2 SKU643: - cost: 356 - init_stock: 2000 - price: 356 + cost: 358 + init_stock: 920 + price: 358 service_level: 0.96 - vlt: 3 + vlt: 2 SKU644: - cost: 499 - init_stock: 700 - price: 499 + cost: 46 + init_stock: 1640 + price: 46 service_level: 0.96 - vlt: 1 + vlt: 2 SKU645: - cost: 476 - init_stock: 1880 - price: 476 + cost: 467 + init_stock: 1980 + price: 467 service_level: 0.96 vlt: 3 SKU646: - cost: 78 - init_stock: 760 - price: 78 + cost: 189 + init_stock: 260 + price: 189 service_level: 0.96 - vlt: 1 + vlt: 3 SKU647: - cost: 52 - init_stock: 1180 - price: 52 + cost: 303 + init_stock: 1540 + price: 303 service_level: 0.96 - vlt: 1 + vlt: 3 SKU648: - cost: 468 - init_stock: 1660 - price: 468 + cost: 226 + init_stock: 1100 + price: 226 service_level: 0.96 - vlt: 2 + vlt: 1 SKU649: - cost: 345 - init_stock: 1400 - price: 345 + cost: 198 + init_stock: 500 + price: 198 service_level: 0.96 - vlt: 2 + vlt: 3 SKU65: - cost: 385 - init_stock: 540 - price: 385 + cost: 15 + init_stock: 1880 + price: 15 service_level: 0.96 vlt: 2 SKU650: - cost: 130 - init_stock: 320 - price: 130 + cost: 351 + init_stock: 1120 + price: 351 service_level: 0.96 vlt: 2 SKU651: - cost: 248 - init_stock: 440 - price: 248 + cost: 380 + init_stock: 1920 + price: 380 service_level: 0.96 vlt: 3 SKU652: - cost: 100 - init_stock: 1840 - price: 100 + cost: 123 + init_stock: 800 + price: 123 service_level: 0.96 - vlt: 1 + vlt: 2 SKU653: - cost: 453 - init_stock: 1580 - price: 453 + cost: 479 + init_stock: 1920 + price: 479 service_level: 0.96 - vlt: 1 + vlt: 2 SKU654: - cost: 441 - init_stock: 620 - price: 441 + cost: 66 + init_stock: 160 + price: 66 service_level: 0.96 vlt: 3 SKU655: - cost: 146 - init_stock: 1080 - price: 146 + cost: 333 + init_stock: 840 + price: 333 service_level: 0.96 vlt: 1 SKU656: - cost: 367 - init_stock: 760 - price: 367 + cost: 53 + init_stock: 1440 + price: 53 service_level: 0.96 - vlt: 3 + vlt: 2 SKU657: - cost: 195 - init_stock: 480 - price: 195 + cost: 73 + init_stock: 1620 + price: 73 service_level: 0.96 - vlt: 1 + vlt: 2 SKU658: - cost: 480 - init_stock: 1740 - price: 480 + cost: 70 + init_stock: 720 + price: 70 service_level: 0.96 - vlt: 2 + vlt: 3 SKU659: - cost: 497 - init_stock: 1920 - price: 497 + cost: 21 + init_stock: 1120 + price: 21 service_level: 0.96 - vlt: 3 + vlt: 1 SKU66: - cost: 37 - init_stock: 1460 - price: 37 + cost: 143 + init_stock: 1800 + price: 143 service_level: 0.96 - vlt: 2 + vlt: 1 SKU660: - cost: 163 - init_stock: 1320 - price: 163 + cost: 487 + init_stock: 1660 + price: 487 service_level: 0.96 - vlt: 3 + vlt: 1 SKU661: - cost: 389 - init_stock: 240 - price: 389 + cost: 344 + init_stock: 1480 + price: 344 service_level: 0.96 vlt: 2 SKU662: - cost: 495 - init_stock: 1640 - price: 495 + cost: 372 + init_stock: 760 + price: 372 service_level: 0.96 - vlt: 2 + vlt: 3 SKU663: - cost: 460 - init_stock: 1300 - price: 460 + cost: 418 + init_stock: 800 + price: 418 service_level: 0.96 vlt: 3 SKU664: - cost: 397 - init_stock: 240 - price: 397 + cost: 351 + init_stock: 1680 + price: 351 service_level: 0.96 - vlt: 3 + vlt: 1 SKU665: - cost: 67 - init_stock: 1420 - price: 67 + cost: 493 + init_stock: 860 + price: 493 service_level: 0.96 - vlt: 1 + vlt: 3 SKU666: - cost: 126 - init_stock: 240 - price: 126 + cost: 341 + init_stock: 1760 + price: 341 service_level: 0.96 vlt: 1 SKU667: - cost: 140 - init_stock: 140 - price: 140 + cost: 325 + init_stock: 260 + price: 325 service_level: 0.96 vlt: 3 SKU668: - cost: 110 - init_stock: 1980 - price: 110 + cost: 286 + init_stock: 1200 + price: 286 service_level: 0.96 - vlt: 1 + vlt: 3 SKU669: - cost: 121 - init_stock: 1340 - price: 121 + cost: 74 + init_stock: 320 + price: 74 service_level: 0.96 vlt: 1 SKU67: - cost: 54 - init_stock: 1720 - price: 54 + cost: 247 + init_stock: 1060 + price: 247 service_level: 0.96 - vlt: 3 + vlt: 1 SKU670: - cost: 495 - init_stock: 100 - price: 495 + cost: 289 + init_stock: 1720 + price: 289 service_level: 0.96 vlt: 1 SKU671: - cost: 444 - init_stock: 200 - price: 444 + cost: 267 + init_stock: 1660 + price: 267 service_level: 0.96 vlt: 3 SKU672: - cost: 426 - init_stock: 1560 - price: 426 + cost: 369 + init_stock: 260 + price: 369 service_level: 0.96 - vlt: 3 + vlt: 2 SKU673: - cost: 109 - init_stock: 1080 - price: 109 + cost: 322 + init_stock: 820 + price: 322 service_level: 0.96 - vlt: 1 + vlt: 2 SKU674: - cost: 392 - init_stock: 160 - price: 392 + cost: 223 + init_stock: 440 + price: 223 service_level: 0.96 - vlt: 3 + vlt: 1 SKU675: - cost: 135 + cost: 438 init_stock: 660 - price: 135 + price: 438 service_level: 0.96 - vlt: 3 + vlt: 1 SKU676: - cost: 401 - init_stock: 1700 - price: 401 + cost: 244 + init_stock: 1220 + price: 244 service_level: 0.96 - vlt: 2 + vlt: 1 SKU677: - cost: 449 - init_stock: 1000 - price: 449 + cost: 425 + init_stock: 880 + price: 425 service_level: 0.96 vlt: 2 SKU678: - cost: 208 - init_stock: 1220 - price: 208 + cost: 168 + init_stock: 720 + price: 168 service_level: 0.96 - vlt: 3 + vlt: 1 SKU679: - cost: 356 - init_stock: 1660 - price: 356 + cost: 395 + init_stock: 880 + price: 395 service_level: 0.96 - vlt: 1 + vlt: 2 SKU68: - cost: 63 - init_stock: 1480 - price: 63 + cost: 169 + init_stock: 280 + price: 169 service_level: 0.96 vlt: 2 SKU680: - cost: 287 - init_stock: 520 - price: 287 + cost: 260 + init_stock: 600 + price: 260 service_level: 0.96 - vlt: 3 + vlt: 1 SKU681: - cost: 140 - init_stock: 1400 - price: 140 + cost: 464 + init_stock: 1940 + price: 464 service_level: 0.96 - vlt: 2 + vlt: 3 SKU682: - cost: 163 - init_stock: 1720 - price: 163 + cost: 370 + init_stock: 1060 + price: 370 service_level: 0.96 vlt: 2 SKU683: - cost: 359 - init_stock: 1520 - price: 359 + cost: 327 + init_stock: 1340 + price: 327 service_level: 0.96 - vlt: 1 + vlt: 3 SKU684: - cost: 37 - init_stock: 620 - price: 37 + cost: 355 + init_stock: 1580 + price: 355 service_level: 0.96 vlt: 1 SKU685: - cost: 106 - init_stock: 640 - price: 106 + cost: 422 + init_stock: 900 + price: 422 service_level: 0.96 - vlt: 3 + vlt: 2 SKU686: - cost: 52 - init_stock: 1160 - price: 52 + cost: 63 + init_stock: 1260 + price: 63 service_level: 0.96 vlt: 1 SKU687: - cost: 85 - init_stock: 1180 - price: 85 + cost: 34 + init_stock: 1520 + price: 34 service_level: 0.96 - vlt: 3 + vlt: 2 SKU688: - cost: 148 - init_stock: 560 - price: 148 + cost: 484 + init_stock: 1540 + price: 484 service_level: 0.96 - vlt: 1 + vlt: 3 SKU689: - cost: 333 - init_stock: 660 - price: 333 + cost: 499 + init_stock: 300 + price: 499 service_level: 0.96 - vlt: 3 + vlt: 1 SKU69: - cost: 65 + cost: 176 init_stock: 1340 - price: 65 + price: 176 service_level: 0.96 - vlt: 3 + vlt: 1 SKU690: - cost: 372 - init_stock: 400 - price: 372 + cost: 437 + init_stock: 1660 + price: 437 service_level: 0.96 - vlt: 1 + vlt: 2 SKU691: - cost: 275 - init_stock: 480 - price: 275 + cost: 17 + init_stock: 1580 + price: 17 service_level: 0.96 - vlt: 1 + vlt: 3 SKU692: - cost: 289 - init_stock: 1600 - price: 289 + cost: 225 + init_stock: 1300 + price: 225 service_level: 0.96 vlt: 1 SKU693: - cost: 258 - init_stock: 1400 - price: 258 + cost: 19 + init_stock: 1220 + price: 19 service_level: 0.96 vlt: 2 SKU694: - cost: 472 - init_stock: 360 - price: 472 + cost: 208 + init_stock: 1500 + price: 208 service_level: 0.96 - vlt: 2 + vlt: 3 SKU695: - cost: 318 - init_stock: 960 - price: 318 + cost: 190 + init_stock: 140 + price: 190 service_level: 0.96 - vlt: 1 + vlt: 2 SKU696: - cost: 63 - init_stock: 1840 - price: 63 + cost: 348 + init_stock: 1160 + price: 348 service_level: 0.96 vlt: 1 SKU697: - cost: 15 - init_stock: 1080 - price: 15 + cost: 455 + init_stock: 1600 + price: 455 service_level: 0.96 vlt: 1 SKU698: - cost: 14 - init_stock: 1020 - price: 14 + cost: 198 + init_stock: 1220 + price: 198 service_level: 0.96 vlt: 3 SKU699: - cost: 357 - init_stock: 1460 - price: 357 + cost: 31 + init_stock: 400 + price: 31 service_level: 0.96 vlt: 3 SKU7: - cost: 314 - init_stock: 860 - price: 314 + cost: 101 + init_stock: 1920 + price: 101 service_level: 0.96 - vlt: 1 + vlt: 2 SKU70: - cost: 207 - init_stock: 1540 - price: 207 + cost: 186 + init_stock: 700 + price: 186 service_level: 0.96 - vlt: 3 + vlt: 1 SKU700: - cost: 21 - init_stock: 1080 - price: 21 + cost: 74 + init_stock: 180 + price: 74 service_level: 0.96 - vlt: 1 + vlt: 2 SKU701: - cost: 130 - init_stock: 1580 - price: 130 + cost: 399 + init_stock: 1540 + price: 399 service_level: 0.96 vlt: 1 SKU702: - cost: 227 - init_stock: 1380 - price: 227 + cost: 82 + init_stock: 680 + price: 82 service_level: 0.96 - vlt: 2 + vlt: 1 SKU703: - cost: 73 - init_stock: 700 - price: 73 + cost: 363 + init_stock: 760 + price: 363 service_level: 0.96 - vlt: 3 + vlt: 1 SKU704: - cost: 37 - init_stock: 520 - price: 37 + cost: 384 + init_stock: 1740 + price: 384 service_level: 0.96 - vlt: 1 + vlt: 2 SKU705: - cost: 313 - init_stock: 420 - price: 313 + cost: 128 + init_stock: 1260 + price: 128 service_level: 0.96 - vlt: 1 + vlt: 2 SKU706: - cost: 378 - init_stock: 280 - price: 378 + cost: 182 + init_stock: 1820 + price: 182 service_level: 0.96 - vlt: 1 + vlt: 2 SKU707: - cost: 302 - init_stock: 2000 - price: 302 + cost: 18 + init_stock: 1380 + price: 18 service_level: 0.96 - vlt: 3 + vlt: 1 SKU708: - cost: 69 - init_stock: 1160 - price: 69 + cost: 178 + init_stock: 560 + price: 178 service_level: 0.96 vlt: 3 SKU709: - cost: 63 - init_stock: 1560 - price: 63 + cost: 102 + init_stock: 1060 + price: 102 service_level: 0.96 - vlt: 1 + vlt: 3 SKU71: - cost: 251 - init_stock: 820 - price: 251 + cost: 315 + init_stock: 900 + price: 315 service_level: 0.96 - vlt: 1 + vlt: 2 SKU710: - cost: 477 - init_stock: 1240 - price: 477 + cost: 252 + init_stock: 380 + price: 252 service_level: 0.96 - vlt: 1 - SKU711: - cost: 395 - init_stock: 1960 - price: 395 + vlt: 2 + SKU711: + cost: 281 + init_stock: 1380 + price: 281 service_level: 0.96 vlt: 1 SKU712: - cost: 50 - init_stock: 1360 - price: 50 + cost: 232 + init_stock: 220 + price: 232 service_level: 0.96 vlt: 2 SKU713: - cost: 224 - init_stock: 500 - price: 224 + cost: 285 + init_stock: 860 + price: 285 service_level: 0.96 - vlt: 1 + vlt: 2 SKU714: - cost: 98 - init_stock: 840 - price: 98 + cost: 79 + init_stock: 820 + price: 79 service_level: 0.96 - vlt: 1 + vlt: 3 SKU715: - cost: 499 - init_stock: 660 - price: 499 + cost: 234 + init_stock: 340 + price: 234 service_level: 0.96 vlt: 1 SKU716: - cost: 468 - init_stock: 640 - price: 468 + cost: 70 + init_stock: 1500 + price: 70 service_level: 0.96 - vlt: 1 + vlt: 2 SKU717: - cost: 115 - init_stock: 500 - price: 115 + cost: 345 + init_stock: 160 + price: 345 service_level: 0.96 vlt: 2 SKU718: - cost: 284 - init_stock: 700 - price: 284 + cost: 357 + init_stock: 1160 + price: 357 service_level: 0.96 - vlt: 1 + vlt: 2 SKU719: - cost: 167 - init_stock: 240 - price: 167 + cost: 340 + init_stock: 1300 + price: 340 service_level: 0.96 - vlt: 1 + vlt: 3 SKU72: - cost: 77 - init_stock: 1460 - price: 77 + cost: 458 + init_stock: 1700 + price: 458 service_level: 0.96 vlt: 2 SKU720: - cost: 382 - init_stock: 100 - price: 382 + cost: 325 + init_stock: 840 + price: 325 service_level: 0.96 vlt: 3 SKU721: - cost: 369 - init_stock: 1080 - price: 369 + cost: 73 + init_stock: 220 + price: 73 service_level: 0.96 - vlt: 3 + vlt: 2 SKU722: - cost: 465 - init_stock: 1400 - price: 465 + cost: 392 + init_stock: 1940 + price: 392 service_level: 0.96 - vlt: 2 + vlt: 1 SKU723: - cost: 43 - init_stock: 960 - price: 43 + cost: 318 + init_stock: 1780 + price: 318 service_level: 0.96 - vlt: 1 + vlt: 3 SKU724: - cost: 246 - init_stock: 1840 - price: 246 + cost: 400 + init_stock: 660 + price: 400 service_level: 0.96 vlt: 1 SKU725: - cost: 174 - init_stock: 620 - price: 174 + cost: 175 + init_stock: 740 + price: 175 service_level: 0.96 vlt: 1 SKU726: - cost: 391 - init_stock: 1060 - price: 391 + cost: 458 + init_stock: 1780 + price: 458 service_level: 0.96 - vlt: 2 + vlt: 3 SKU727: - cost: 447 - init_stock: 1220 - price: 447 + cost: 418 + init_stock: 1140 + price: 418 service_level: 0.96 - vlt: 2 + vlt: 1 SKU728: - cost: 347 - init_stock: 600 - price: 347 + cost: 475 + init_stock: 740 + price: 475 service_level: 0.96 - vlt: 2 + vlt: 3 SKU729: - cost: 463 - init_stock: 1660 - price: 463 + cost: 324 + init_stock: 720 + price: 324 service_level: 0.96 - vlt: 1 + vlt: 3 SKU73: - cost: 163 - init_stock: 660 - price: 163 + cost: 212 + init_stock: 1080 + price: 212 service_level: 0.96 vlt: 2 SKU730: - cost: 211 - init_stock: 300 - price: 211 + cost: 16 + init_stock: 260 + price: 16 service_level: 0.96 vlt: 2 SKU731: - cost: 57 - init_stock: 1080 - price: 57 + cost: 88 + init_stock: 1640 + price: 88 service_level: 0.96 - vlt: 3 + vlt: 2 SKU732: - cost: 106 - init_stock: 480 - price: 106 + cost: 41 + init_stock: 1240 + price: 41 service_level: 0.96 - vlt: 2 + vlt: 3 SKU733: - cost: 302 - init_stock: 380 - price: 302 + cost: 315 + init_stock: 520 + price: 315 service_level: 0.96 vlt: 3 SKU734: - cost: 386 - init_stock: 280 - price: 386 + cost: 37 + init_stock: 1020 + price: 37 service_level: 0.96 vlt: 3 SKU735: - cost: 85 - init_stock: 780 - price: 85 + cost: 266 + init_stock: 1980 + price: 266 service_level: 0.96 vlt: 3 SKU736: - cost: 16 - init_stock: 1340 - price: 16 + cost: 368 + init_stock: 500 + price: 368 service_level: 0.96 - vlt: 3 + vlt: 2 SKU737: - cost: 382 - init_stock: 1840 - price: 382 + cost: 475 + init_stock: 620 + price: 475 service_level: 0.96 - vlt: 3 + vlt: 2 SKU738: - cost: 429 - init_stock: 180 - price: 429 + cost: 185 + init_stock: 960 + price: 185 service_level: 0.96 - vlt: 2 + vlt: 3 SKU739: - cost: 285 - init_stock: 900 - price: 285 + cost: 475 + init_stock: 1480 + price: 475 service_level: 0.96 - vlt: 1 + vlt: 2 SKU74: - cost: 47 - init_stock: 1980 - price: 47 + cost: 159 + init_stock: 840 + price: 159 service_level: 0.96 - vlt: 3 + vlt: 1 SKU740: - cost: 475 - init_stock: 1160 - price: 475 + cost: 390 + init_stock: 1280 + price: 390 service_level: 0.96 - vlt: 3 + vlt: 1 SKU741: - cost: 466 - init_stock: 1420 - price: 466 + cost: 91 + init_stock: 1120 + price: 91 service_level: 0.96 vlt: 1 SKU742: - cost: 442 - init_stock: 540 - price: 442 + cost: 188 + init_stock: 440 + price: 188 service_level: 0.96 - vlt: 3 + vlt: 1 SKU743: - cost: 83 - init_stock: 400 - price: 83 - service_level: 0.96 - vlt: 2 - SKU744: - cost: 340 - init_stock: 1440 - price: 340 + cost: 217 + init_stock: 860 + price: 217 service_level: 0.96 vlt: 3 + SKU744: + cost: 379 + init_stock: 1160 + price: 379 + service_level: 0.96 + vlt: 1 SKU745: - cost: 444 - init_stock: 1000 - price: 444 + cost: 316 + init_stock: 1840 + price: 316 service_level: 0.96 vlt: 2 SKU746: - cost: 496 - init_stock: 1920 - price: 496 + cost: 437 + init_stock: 800 + price: 437 service_level: 0.96 - vlt: 1 + vlt: 2 SKU747: - cost: 175 - init_stock: 1200 - price: 175 + cost: 373 + init_stock: 1580 + price: 373 service_level: 0.96 vlt: 2 SKU748: - cost: 461 - init_stock: 140 - price: 461 + cost: 275 + init_stock: 1320 + price: 275 service_level: 0.96 - vlt: 3 + vlt: 2 SKU749: - cost: 481 - init_stock: 340 - price: 481 + cost: 394 + init_stock: 1060 + price: 394 service_level: 0.96 - vlt: 2 + vlt: 1 SKU75: - cost: 319 - init_stock: 640 - price: 319 + cost: 131 + init_stock: 380 + price: 131 service_level: 0.96 - vlt: 2 + vlt: 1 SKU750: - cost: 240 - init_stock: 900 - price: 240 + cost: 256 + init_stock: 1240 + price: 256 service_level: 0.96 vlt: 3 SKU751: - cost: 302 - init_stock: 180 - price: 302 + cost: 369 + init_stock: 1300 + price: 369 service_level: 0.96 vlt: 3 SKU752: - cost: 425 - init_stock: 660 - price: 425 + cost: 259 + init_stock: 1560 + price: 259 service_level: 0.96 - vlt: 2 + vlt: 3 SKU753: - cost: 105 - init_stock: 880 - price: 105 + cost: 77 + init_stock: 1300 + price: 77 service_level: 0.96 vlt: 3 SKU754: - cost: 495 - init_stock: 1860 - price: 495 + cost: 387 + init_stock: 260 + price: 387 service_level: 0.96 - vlt: 1 + vlt: 2 SKU755: - cost: 436 - init_stock: 620 - price: 436 + cost: 354 + init_stock: 1600 + price: 354 service_level: 0.96 vlt: 3 SKU756: - cost: 234 - init_stock: 1900 - price: 234 + cost: 246 + init_stock: 1800 + price: 246 service_level: 0.96 - vlt: 3 + vlt: 1 SKU757: - cost: 313 - init_stock: 260 - price: 313 + cost: 40 + init_stock: 1140 + price: 40 service_level: 0.96 vlt: 3 SKU758: - cost: 319 - init_stock: 420 - price: 319 + cost: 241 + init_stock: 800 + price: 241 service_level: 0.96 - vlt: 3 + vlt: 2 SKU759: - cost: 313 - init_stock: 820 - price: 313 + cost: 435 + init_stock: 760 + price: 435 service_level: 0.96 - vlt: 2 + vlt: 1 SKU76: - cost: 242 - init_stock: 1340 - price: 242 + cost: 147 + init_stock: 1280 + price: 147 service_level: 0.96 vlt: 2 SKU760: - cost: 358 - init_stock: 520 - price: 358 + cost: 176 + init_stock: 1020 + price: 176 service_level: 0.96 - vlt: 1 + vlt: 3 SKU761: - cost: 487 - init_stock: 320 - price: 487 + cost: 224 + init_stock: 1100 + price: 224 service_level: 0.96 - vlt: 3 + vlt: 1 SKU762: - cost: 412 - init_stock: 100 - price: 412 + cost: 264 + init_stock: 1020 + price: 264 service_level: 0.96 vlt: 1 SKU763: - cost: 391 - init_stock: 760 - price: 391 + cost: 385 + init_stock: 1580 + price: 385 service_level: 0.96 - vlt: 1 + vlt: 2 SKU764: - cost: 127 - init_stock: 980 - price: 127 + cost: 349 + init_stock: 680 + price: 349 service_level: 0.96 vlt: 1 SKU765: - cost: 271 - init_stock: 500 - price: 271 + cost: 345 + init_stock: 260 + price: 345 service_level: 0.96 - vlt: 3 + vlt: 1 SKU766: - cost: 230 - init_stock: 460 - price: 230 + cost: 478 + init_stock: 400 + price: 478 service_level: 0.96 - vlt: 1 + vlt: 2 SKU767: - cost: 91 - init_stock: 1600 - price: 91 + cost: 95 + init_stock: 1520 + price: 95 service_level: 0.96 vlt: 1 SKU768: - cost: 410 - init_stock: 2000 - price: 410 + cost: 181 + init_stock: 1840 + price: 181 service_level: 0.96 - vlt: 3 + vlt: 2 SKU769: - cost: 324 - init_stock: 820 - price: 324 + cost: 24 + init_stock: 580 + price: 24 service_level: 0.96 vlt: 2 SKU77: - cost: 321 - init_stock: 680 - price: 321 + cost: 409 + init_stock: 1440 + price: 409 service_level: 0.96 vlt: 1 SKU770: - cost: 288 - init_stock: 960 - price: 288 + cost: 150 + init_stock: 1240 + price: 150 service_level: 0.96 vlt: 2 SKU771: - cost: 53 - init_stock: 200 - price: 53 + cost: 101 + init_stock: 140 + price: 101 service_level: 0.96 - vlt: 2 + vlt: 1 SKU772: - cost: 205 - init_stock: 1100 - price: 205 + cost: 256 + init_stock: 120 + price: 256 service_level: 0.96 - vlt: 2 + vlt: 3 SKU773: - cost: 69 - init_stock: 540 - price: 69 + cost: 84 + init_stock: 1840 + price: 84 service_level: 0.96 vlt: 1 SKU774: - cost: 500 - init_stock: 1920 - price: 500 + cost: 447 + init_stock: 1180 + price: 447 service_level: 0.96 - vlt: 2 + vlt: 1 SKU775: - cost: 366 - init_stock: 860 - price: 366 + cost: 175 + init_stock: 1720 + price: 175 service_level: 0.96 - vlt: 2 + vlt: 3 SKU776: - cost: 464 - init_stock: 160 - price: 464 + cost: 103 + init_stock: 1700 + price: 103 service_level: 0.96 vlt: 2 SKU777: - cost: 493 - init_stock: 1080 - price: 493 + cost: 292 + init_stock: 1140 + price: 292 service_level: 0.96 vlt: 2 SKU778: - cost: 386 - init_stock: 500 - price: 386 + cost: 203 + init_stock: 160 + price: 203 service_level: 0.96 - vlt: 2 + vlt: 3 SKU779: - cost: 149 - init_stock: 1240 - price: 149 + cost: 117 + init_stock: 860 + price: 117 service_level: 0.96 - vlt: 2 + vlt: 1 SKU78: - cost: 231 - init_stock: 900 - price: 231 + cost: 234 + init_stock: 260 + price: 234 service_level: 0.96 vlt: 3 SKU780: - cost: 271 - init_stock: 700 - price: 271 + cost: 69 + init_stock: 1640 + price: 69 service_level: 0.96 vlt: 3 SKU781: - cost: 84 - init_stock: 1160 - price: 84 + cost: 372 + init_stock: 720 + price: 372 service_level: 0.96 - vlt: 2 + vlt: 3 SKU782: - cost: 145 - init_stock: 1400 - price: 145 + cost: 27 + init_stock: 200 + price: 27 service_level: 0.96 - vlt: 2 + vlt: 3 SKU783: - cost: 191 - init_stock: 660 - price: 191 + cost: 87 + init_stock: 1620 + price: 87 service_level: 0.96 - vlt: 1 + vlt: 2 SKU784: - cost: 34 - init_stock: 360 - price: 34 + cost: 309 + init_stock: 1040 + price: 309 service_level: 0.96 - vlt: 1 + vlt: 3 SKU785: - cost: 368 - init_stock: 560 - price: 368 + cost: 191 + init_stock: 1400 + price: 191 service_level: 0.96 - vlt: 3 + vlt: 2 SKU786: - cost: 55 - init_stock: 460 - price: 55 + cost: 91 + init_stock: 440 + price: 91 service_level: 0.96 - vlt: 2 + vlt: 3 SKU787: - cost: 368 - init_stock: 120 - price: 368 + cost: 360 + init_stock: 1220 + price: 360 service_level: 0.96 - vlt: 3 + vlt: 1 SKU788: - cost: 257 - init_stock: 200 - price: 257 + cost: 351 + init_stock: 560 + price: 351 service_level: 0.96 - vlt: 2 + vlt: 1 SKU789: - cost: 218 - init_stock: 1700 - price: 218 + cost: 153 + init_stock: 1160 + price: 153 service_level: 0.96 - vlt: 1 + vlt: 2 SKU79: - cost: 436 - init_stock: 1460 - price: 436 + cost: 245 + init_stock: 220 + price: 245 service_level: 0.96 - vlt: 2 + vlt: 1 SKU790: - cost: 429 - init_stock: 1060 - price: 429 + cost: 417 + init_stock: 1320 + price: 417 service_level: 0.96 - vlt: 3 + vlt: 2 SKU791: - cost: 55 - init_stock: 1460 - price: 55 + cost: 134 + init_stock: 560 + price: 134 service_level: 0.96 vlt: 1 SKU792: - cost: 80 - init_stock: 1400 - price: 80 + cost: 313 + init_stock: 420 + price: 313 service_level: 0.96 - vlt: 3 + vlt: 2 SKU793: - cost: 227 - init_stock: 220 - price: 227 + cost: 195 + init_stock: 1520 + price: 195 service_level: 0.96 - vlt: 1 + vlt: 2 SKU794: - cost: 361 - init_stock: 560 - price: 361 + cost: 325 + init_stock: 1600 + price: 325 service_level: 0.96 vlt: 3 SKU795: - cost: 200 + cost: 276 init_stock: 960 - price: 200 + price: 276 service_level: 0.96 - vlt: 1 + vlt: 3 SKU796: - cost: 360 - init_stock: 140 - price: 360 + cost: 447 + init_stock: 980 + price: 447 service_level: 0.96 - vlt: 3 + vlt: 1 SKU797: - cost: 37 - init_stock: 1740 - price: 37 + cost: 100 + init_stock: 780 + price: 100 service_level: 0.96 - vlt: 1 + vlt: 3 SKU798: - cost: 124 - init_stock: 1900 - price: 124 + cost: 426 + init_stock: 600 + price: 426 service_level: 0.96 vlt: 2 SKU799: - cost: 93 - init_stock: 360 - price: 93 + cost: 63 + init_stock: 140 + price: 63 service_level: 0.96 - vlt: 1 + vlt: 2 SKU8: - cost: 320 - init_stock: 480 - price: 320 + cost: 125 + init_stock: 1260 + price: 125 service_level: 0.96 - vlt: 1 + vlt: 3 SKU80: - cost: 316 - init_stock: 840 - price: 316 + cost: 163 + init_stock: 1400 + price: 163 service_level: 0.96 - vlt: 2 + vlt: 1 SKU800: - cost: 264 - init_stock: 1320 - price: 264 + cost: 298 + init_stock: 1880 + price: 298 service_level: 0.96 vlt: 2 SKU801: - cost: 62 - init_stock: 2000 - price: 62 + cost: 389 + init_stock: 720 + price: 389 service_level: 0.96 - vlt: 3 + vlt: 2 SKU802: - cost: 289 - init_stock: 220 - price: 289 + cost: 173 + init_stock: 1240 + price: 173 service_level: 0.96 - vlt: 1 + vlt: 2 SKU803: - cost: 485 - init_stock: 1220 - price: 485 + cost: 272 + init_stock: 380 + price: 272 service_level: 0.96 vlt: 3 SKU804: - cost: 126 - init_stock: 1160 - price: 126 + cost: 390 + init_stock: 940 + price: 390 service_level: 0.96 vlt: 3 SKU805: - cost: 227 - init_stock: 1460 - price: 227 + cost: 61 + init_stock: 660 + price: 61 service_level: 0.96 - vlt: 1 + vlt: 3 SKU806: - cost: 59 - init_stock: 680 - price: 59 + cost: 158 + init_stock: 1040 + price: 158 service_level: 0.96 vlt: 1 SKU807: - cost: 468 - init_stock: 1500 - price: 468 + cost: 453 + init_stock: 520 + price: 453 service_level: 0.96 vlt: 3 SKU808: - cost: 161 - init_stock: 1080 - price: 161 + cost: 249 + init_stock: 1820 + price: 249 service_level: 0.96 - vlt: 3 + vlt: 1 SKU809: - cost: 53 - init_stock: 1360 - price: 53 + cost: 55 + init_stock: 1300 + price: 55 service_level: 0.96 vlt: 1 SKU81: - cost: 192 - init_stock: 1960 - price: 192 + cost: 182 + init_stock: 1320 + price: 182 service_level: 0.96 - vlt: 1 + vlt: 3 SKU810: - cost: 451 - init_stock: 880 - price: 451 + cost: 276 + init_stock: 120 + price: 276 service_level: 0.96 - vlt: 1 + vlt: 2 SKU811: - cost: 320 - init_stock: 320 - price: 320 + cost: 254 + init_stock: 740 + price: 254 service_level: 0.96 - vlt: 1 + vlt: 3 SKU812: - cost: 293 - init_stock: 300 - price: 293 + cost: 252 + init_stock: 1600 + price: 252 service_level: 0.96 - vlt: 3 + vlt: 1 SKU813: - cost: 39 - init_stock: 1580 - price: 39 + cost: 253 + init_stock: 140 + price: 253 service_level: 0.96 - vlt: 2 + vlt: 3 SKU814: cost: 485 - init_stock: 1280 + init_stock: 1940 price: 485 service_level: 0.96 vlt: 1 SKU815: - cost: 53 - init_stock: 720 - price: 53 + cost: 390 + init_stock: 480 + price: 390 service_level: 0.96 - vlt: 3 + vlt: 2 SKU816: - cost: 154 - init_stock: 1120 - price: 154 + cost: 152 + init_stock: 1820 + price: 152 service_level: 0.96 - vlt: 1 + vlt: 3 SKU817: - cost: 262 - init_stock: 1900 - price: 262 + cost: 227 + init_stock: 100 + price: 227 service_level: 0.96 - vlt: 1 + vlt: 2 SKU818: - cost: 89 - init_stock: 1520 - price: 89 + cost: 354 + init_stock: 860 + price: 354 service_level: 0.96 - vlt: 3 + vlt: 2 SKU819: - cost: 328 - init_stock: 1800 - price: 328 + cost: 302 + init_stock: 540 + price: 302 service_level: 0.96 - vlt: 2 + vlt: 1 SKU82: - cost: 237 - init_stock: 1900 - price: 237 + cost: 290 + init_stock: 560 + price: 290 service_level: 0.96 - vlt: 2 + vlt: 1 SKU820: - cost: 461 - init_stock: 600 - price: 461 + cost: 264 + init_stock: 1640 + price: 264 service_level: 0.96 - vlt: 2 + vlt: 3 SKU821: - cost: 399 - init_stock: 1660 - price: 399 + cost: 99 + init_stock: 1380 + price: 99 service_level: 0.96 vlt: 1 SKU822: - cost: 60 - init_stock: 1440 - price: 60 + cost: 136 + init_stock: 1400 + price: 136 service_level: 0.96 - vlt: 1 + vlt: 3 SKU823: - cost: 171 - init_stock: 1620 - price: 171 + cost: 75 + init_stock: 560 + price: 75 service_level: 0.96 vlt: 2 SKU824: - cost: 54 - init_stock: 1220 - price: 54 + cost: 170 + init_stock: 1400 + price: 170 service_level: 0.96 - vlt: 2 + vlt: 1 SKU825: - cost: 361 - init_stock: 840 - price: 361 + cost: 214 + init_stock: 300 + price: 214 service_level: 0.96 vlt: 1 SKU826: - cost: 409 - init_stock: 520 - price: 409 + cost: 386 + init_stock: 1100 + price: 386 service_level: 0.96 - vlt: 1 + vlt: 2 SKU827: - cost: 495 - init_stock: 1560 - price: 495 + cost: 148 + init_stock: 1280 + price: 148 service_level: 0.96 - vlt: 1 + vlt: 2 SKU828: - cost: 97 - init_stock: 900 - price: 97 + cost: 400 + init_stock: 1380 + price: 400 service_level: 0.96 vlt: 1 SKU829: - cost: 301 - init_stock: 1000 - price: 301 + cost: 61 + init_stock: 1480 + price: 61 service_level: 0.96 - vlt: 2 + vlt: 1 SKU83: - cost: 365 - init_stock: 1180 - price: 365 + cost: 296 + init_stock: 340 + price: 296 service_level: 0.96 - vlt: 3 + vlt: 2 SKU830: - cost: 215 - init_stock: 260 - price: 215 + cost: 167 + init_stock: 480 + price: 167 service_level: 0.96 - vlt: 2 + vlt: 3 SKU831: - cost: 324 - init_stock: 1000 - price: 324 + cost: 262 + init_stock: 580 + price: 262 service_level: 0.96 - vlt: 3 + vlt: 1 SKU832: - cost: 118 - init_stock: 1860 - price: 118 + cost: 33 + init_stock: 1640 + price: 33 service_level: 0.96 - vlt: 2 + vlt: 1 SKU833: - cost: 435 - init_stock: 600 - price: 435 + cost: 400 + init_stock: 1960 + price: 400 service_level: 0.96 - vlt: 1 + vlt: 3 SKU834: - cost: 402 - init_stock: 1900 - price: 402 + cost: 422 + init_stock: 100 + price: 422 service_level: 0.96 vlt: 1 SKU835: - cost: 216 - init_stock: 1120 - price: 216 + cost: 440 + init_stock: 1040 + price: 440 service_level: 0.96 vlt: 2 SKU836: - cost: 15 - init_stock: 1480 - price: 15 + cost: 323 + init_stock: 1920 + price: 323 service_level: 0.96 - vlt: 3 + vlt: 1 SKU837: - cost: 62 - init_stock: 1380 - price: 62 + cost: 373 + init_stock: 520 + price: 373 service_level: 0.96 vlt: 3 SKU838: - cost: 135 - init_stock: 480 - price: 135 + cost: 456 + init_stock: 1540 + price: 456 service_level: 0.96 - vlt: 2 + vlt: 1 SKU839: - cost: 485 - init_stock: 1740 - price: 485 - service_level: 0.96 - vlt: 3 - SKU84: - cost: 21 - init_stock: 1640 - price: 21 + cost: 473 + init_stock: 1200 + price: 473 service_level: 0.96 vlt: 1 - SKU840: - cost: 185 - init_stock: 600 - price: 185 + SKU84: + cost: 318 + init_stock: 840 + price: 318 service_level: 0.96 vlt: 3 - SKU841: - cost: 211 - init_stock: 1960 - price: 211 + SKU840: + cost: 266 + init_stock: 1000 + price: 266 service_level: 0.96 vlt: 1 - SKU842: - cost: 251 - init_stock: 200 - price: 251 + SKU841: + cost: 285 + init_stock: 1700 + price: 285 service_level: 0.96 vlt: 3 + SKU842: + cost: 41 + init_stock: 1680 + price: 41 + service_level: 0.96 + vlt: 1 SKU843: - cost: 119 - init_stock: 1220 - price: 119 + cost: 360 + init_stock: 1440 + price: 360 service_level: 0.96 vlt: 2 SKU844: - cost: 119 - init_stock: 1920 - price: 119 + cost: 51 + init_stock: 1580 + price: 51 service_level: 0.96 - vlt: 1 + vlt: 3 SKU845: - cost: 489 - init_stock: 1500 - price: 489 + cost: 288 + init_stock: 440 + price: 288 service_level: 0.96 vlt: 3 SKU846: - cost: 500 - init_stock: 200 - price: 500 + cost: 485 + init_stock: 780 + price: 485 service_level: 0.96 - vlt: 2 + vlt: 1 SKU847: - cost: 304 - init_stock: 1840 - price: 304 + cost: 388 + init_stock: 1820 + price: 388 service_level: 0.96 - vlt: 3 + vlt: 1 SKU848: - cost: 242 - init_stock: 1800 - price: 242 + cost: 306 + init_stock: 920 + price: 306 service_level: 0.96 - vlt: 1 + vlt: 2 SKU849: - cost: 33 - init_stock: 1920 - price: 33 + cost: 320 + init_stock: 800 + price: 320 service_level: 0.96 - vlt: 3 + vlt: 2 SKU85: - cost: 156 - init_stock: 1240 - price: 156 + cost: 442 + init_stock: 2000 + price: 442 service_level: 0.96 - vlt: 1 + vlt: 3 SKU850: - cost: 449 - init_stock: 1020 - price: 449 + cost: 183 + init_stock: 1140 + price: 183 service_level: 0.96 - vlt: 1 + vlt: 2 SKU851: - cost: 417 - init_stock: 920 - price: 417 + cost: 53 + init_stock: 1280 + price: 53 service_level: 0.96 vlt: 1 SKU852: - cost: 274 - init_stock: 1040 - price: 274 + cost: 306 + init_stock: 1080 + price: 306 service_level: 0.96 - vlt: 3 + vlt: 2 SKU853: - cost: 65 - init_stock: 1080 - price: 65 + cost: 386 + init_stock: 1120 + price: 386 service_level: 0.96 vlt: 3 SKU854: - cost: 224 - init_stock: 1840 - price: 224 + cost: 212 + init_stock: 1300 + price: 212 service_level: 0.96 - vlt: 1 + vlt: 3 SKU855: - cost: 125 - init_stock: 140 - price: 125 + cost: 76 + init_stock: 1440 + price: 76 service_level: 0.96 - vlt: 2 + vlt: 3 SKU856: - cost: 192 - init_stock: 840 - price: 192 + cost: 380 + init_stock: 640 + price: 380 service_level: 0.96 - vlt: 1 + vlt: 2 SKU857: - cost: 180 - init_stock: 360 - price: 180 + cost: 462 + init_stock: 2000 + price: 462 service_level: 0.96 - vlt: 3 + vlt: 1 SKU858: - cost: 393 - init_stock: 1580 - price: 393 + cost: 80 + init_stock: 480 + price: 80 service_level: 0.96 vlt: 3 SKU859: - cost: 259 - init_stock: 1240 - price: 259 + cost: 215 + init_stock: 560 + price: 215 service_level: 0.96 - vlt: 2 + vlt: 3 SKU86: - cost: 322 - init_stock: 120 - price: 322 + cost: 386 + init_stock: 1700 + price: 386 service_level: 0.96 - vlt: 1 + vlt: 2 SKU860: - cost: 287 - init_stock: 600 - price: 287 + cost: 313 + init_stock: 300 + price: 313 service_level: 0.96 vlt: 2 SKU861: - cost: 20 - init_stock: 560 - price: 20 + cost: 477 + init_stock: 260 + price: 477 service_level: 0.96 - vlt: 1 + vlt: 2 SKU862: - cost: 86 - init_stock: 460 - price: 86 + cost: 240 + init_stock: 320 + price: 240 service_level: 0.96 - vlt: 1 + vlt: 2 SKU863: - cost: 84 - init_stock: 1580 - price: 84 + cost: 470 + init_stock: 1860 + price: 470 service_level: 0.96 vlt: 2 SKU864: - cost: 287 - init_stock: 860 - price: 287 + cost: 203 + init_stock: 380 + price: 203 service_level: 0.96 vlt: 1 SKU865: - cost: 197 - init_stock: 480 - price: 197 + cost: 144 + init_stock: 380 + price: 144 service_level: 0.96 - vlt: 2 + vlt: 3 SKU866: - cost: 393 - init_stock: 100 - price: 393 + cost: 172 + init_stock: 1760 + price: 172 service_level: 0.96 - vlt: 3 + vlt: 1 SKU867: - cost: 231 - init_stock: 700 - price: 231 + cost: 499 + init_stock: 2000 + price: 499 service_level: 0.96 - vlt: 1 + vlt: 2 SKU868: - cost: 467 - init_stock: 1320 - price: 467 + cost: 41 + init_stock: 1000 + price: 41 service_level: 0.96 - vlt: 1 + vlt: 2 SKU869: - cost: 114 - init_stock: 1840 - price: 114 + cost: 97 + init_stock: 1940 + price: 97 service_level: 0.96 - vlt: 3 + vlt: 2 SKU87: - cost: 326 - init_stock: 180 - price: 326 + cost: 368 + init_stock: 1380 + price: 368 service_level: 0.96 - vlt: 1 + vlt: 2 SKU870: - cost: 44 - init_stock: 220 - price: 44 + cost: 281 + init_stock: 1340 + price: 281 service_level: 0.96 vlt: 1 SKU871: - cost: 404 - init_stock: 1680 - price: 404 + cost: 101 + init_stock: 1980 + price: 101 service_level: 0.96 - vlt: 2 + vlt: 1 SKU872: - cost: 291 - init_stock: 1080 - price: 291 + cost: 133 + init_stock: 640 + price: 133 service_level: 0.96 - vlt: 2 + vlt: 3 SKU873: - cost: 334 - init_stock: 420 - price: 334 + cost: 423 + init_stock: 620 + price: 423 service_level: 0.96 vlt: 2 SKU874: - cost: 364 - init_stock: 960 - price: 364 + cost: 469 + init_stock: 1200 + price: 469 service_level: 0.96 - vlt: 3 + vlt: 1 SKU875: - cost: 276 - init_stock: 1740 - price: 276 + cost: 51 + init_stock: 460 + price: 51 service_level: 0.96 - vlt: 3 + vlt: 2 SKU876: - cost: 342 - init_stock: 1400 - price: 342 + cost: 303 + init_stock: 1220 + price: 303 service_level: 0.96 - vlt: 1 + vlt: 3 SKU877: - cost: 398 - init_stock: 1620 - price: 398 + cost: 40 + init_stock: 700 + price: 40 service_level: 0.96 vlt: 3 SKU878: - cost: 131 + cost: 27 init_stock: 1360 - price: 131 + price: 27 service_level: 0.96 - vlt: 1 + vlt: 3 SKU879: - cost: 343 - init_stock: 160 - price: 343 + cost: 150 + init_stock: 2000 + price: 150 service_level: 0.96 vlt: 1 SKU88: - cost: 244 - init_stock: 440 - price: 244 + cost: 471 + init_stock: 240 + price: 471 service_level: 0.96 - vlt: 3 + vlt: 1 SKU880: - cost: 496 - init_stock: 880 - price: 496 + cost: 433 + init_stock: 620 + price: 433 service_level: 0.96 - vlt: 2 + vlt: 3 SKU881: - cost: 367 - init_stock: 1340 - price: 367 + cost: 431 + init_stock: 1400 + price: 431 service_level: 0.96 vlt: 3 SKU882: - cost: 86 - init_stock: 1260 - price: 86 + cost: 194 + init_stock: 320 + price: 194 service_level: 0.96 vlt: 1 SKU883: - cost: 190 - init_stock: 920 - price: 190 + cost: 284 + init_stock: 760 + price: 284 service_level: 0.96 - vlt: 3 + vlt: 1 SKU884: - cost: 200 - init_stock: 580 - price: 200 + cost: 139 + init_stock: 780 + price: 139 service_level: 0.96 vlt: 2 SKU885: - cost: 133 - init_stock: 920 - price: 133 + cost: 49 + init_stock: 540 + price: 49 service_level: 0.96 vlt: 3 SKU886: - cost: 218 - init_stock: 320 - price: 218 + cost: 372 + init_stock: 1460 + price: 372 service_level: 0.96 - vlt: 1 + vlt: 3 SKU887: - cost: 215 - init_stock: 1760 - price: 215 + cost: 266 + init_stock: 1740 + price: 266 service_level: 0.96 vlt: 1 SKU888: - cost: 439 - init_stock: 1340 - price: 439 + cost: 143 + init_stock: 180 + price: 143 service_level: 0.96 vlt: 2 SKU889: - cost: 105 - init_stock: 1360 - price: 105 + cost: 101 + init_stock: 420 + price: 101 service_level: 0.96 - vlt: 3 + vlt: 2 SKU89: - cost: 223 - init_stock: 140 - price: 223 + cost: 138 + init_stock: 1860 + price: 138 service_level: 0.96 - vlt: 2 + vlt: 3 SKU890: - cost: 183 - init_stock: 720 - price: 183 + cost: 161 + init_stock: 2000 + price: 161 service_level: 0.96 - vlt: 1 + vlt: 3 SKU891: - cost: 85 - init_stock: 880 - price: 85 + cost: 356 + init_stock: 440 + price: 356 service_level: 0.96 - vlt: 2 + vlt: 3 SKU892: - cost: 41 - init_stock: 1680 - price: 41 + cost: 313 + init_stock: 1840 + price: 313 service_level: 0.96 - vlt: 1 + vlt: 2 SKU893: - cost: 135 - init_stock: 440 - price: 135 + cost: 229 + init_stock: 1980 + price: 229 service_level: 0.96 - vlt: 3 + vlt: 1 SKU894: - cost: 405 - init_stock: 1400 - price: 405 + cost: 129 + init_stock: 1480 + price: 129 service_level: 0.96 - vlt: 3 + vlt: 1 SKU895: - cost: 320 - init_stock: 1240 - price: 320 + cost: 230 + init_stock: 260 + price: 230 service_level: 0.96 - vlt: 1 + vlt: 2 SKU896: - cost: 451 - init_stock: 1080 - price: 451 + cost: 289 + init_stock: 1600 + price: 289 service_level: 0.96 - vlt: 2 + vlt: 3 SKU897: - cost: 141 - init_stock: 1920 - price: 141 + cost: 393 + init_stock: 1440 + price: 393 service_level: 0.96 - vlt: 2 + vlt: 3 SKU898: - cost: 220 - init_stock: 1780 - price: 220 + cost: 477 + init_stock: 220 + price: 477 service_level: 0.96 - vlt: 3 + vlt: 2 SKU899: - cost: 113 - init_stock: 760 - price: 113 + cost: 233 + init_stock: 960 + price: 233 service_level: 0.96 - vlt: 1 + vlt: 2 SKU9: - cost: 483 - init_stock: 840 - price: 483 + cost: 166 + init_stock: 1920 + price: 166 service_level: 0.96 - vlt: 3 + vlt: 2 SKU90: - cost: 217 - init_stock: 220 - price: 217 + cost: 454 + init_stock: 1020 + price: 454 service_level: 0.96 - vlt: 1 + vlt: 2 SKU900: - cost: 218 - init_stock: 1400 - price: 218 + cost: 158 + init_stock: 1100 + price: 158 service_level: 0.96 - vlt: 3 + vlt: 1 SKU901: - cost: 335 - init_stock: 1660 - price: 335 + cost: 215 + init_stock: 580 + price: 215 service_level: 0.96 - vlt: 2 + vlt: 3 SKU902: - cost: 118 - init_stock: 1980 - price: 118 + cost: 125 + init_stock: 1600 + price: 125 service_level: 0.96 - vlt: 3 + vlt: 2 SKU903: - cost: 164 - init_stock: 220 - price: 164 + cost: 357 + init_stock: 760 + price: 357 service_level: 0.96 vlt: 2 SKU904: - cost: 317 - init_stock: 600 - price: 317 + cost: 496 + init_stock: 1020 + price: 496 service_level: 0.96 - vlt: 3 + vlt: 1 SKU905: - cost: 462 + cost: 249 init_stock: 620 - price: 462 + price: 249 service_level: 0.96 - vlt: 1 + vlt: 3 SKU906: - cost: 249 - init_stock: 900 - price: 249 + cost: 166 + init_stock: 1040 + price: 166 service_level: 0.96 vlt: 2 SKU907: - cost: 206 - init_stock: 1520 - price: 206 + cost: 22 + init_stock: 1660 + price: 22 service_level: 0.96 - vlt: 2 + vlt: 3 SKU908: - cost: 419 - init_stock: 1260 - price: 419 + cost: 408 + init_stock: 1560 + price: 408 service_level: 0.96 - vlt: 3 + vlt: 2 SKU909: - cost: 414 - init_stock: 1280 - price: 414 + cost: 482 + init_stock: 1920 + price: 482 service_level: 0.96 vlt: 1 SKU91: - cost: 254 - init_stock: 1800 - price: 254 + cost: 303 + init_stock: 320 + price: 303 service_level: 0.96 vlt: 2 SKU910: - cost: 371 - init_stock: 1040 - price: 371 + cost: 226 + init_stock: 660 + price: 226 service_level: 0.96 - vlt: 3 + vlt: 2 SKU911: - cost: 421 - init_stock: 1200 - price: 421 + cost: 461 + init_stock: 1400 + price: 461 service_level: 0.96 vlt: 2 SKU912: - cost: 164 - init_stock: 300 - price: 164 + cost: 236 + init_stock: 540 + price: 236 service_level: 0.96 vlt: 3 SKU913: - cost: 20 - init_stock: 1540 - price: 20 + cost: 322 + init_stock: 920 + price: 322 service_level: 0.96 vlt: 2 SKU914: - cost: 144 - init_stock: 920 - price: 144 + cost: 272 + init_stock: 1240 + price: 272 service_level: 0.96 - vlt: 1 + vlt: 3 SKU915: - cost: 214 - init_stock: 200 - price: 214 + cost: 337 + init_stock: 1120 + price: 337 service_level: 0.96 - vlt: 2 + vlt: 3 SKU916: - cost: 117 - init_stock: 960 - price: 117 + cost: 337 + init_stock: 1780 + price: 337 service_level: 0.96 vlt: 3 SKU917: - cost: 212 - init_stock: 520 - price: 212 + cost: 258 + init_stock: 600 + price: 258 service_level: 0.96 vlt: 3 SKU918: - cost: 318 - init_stock: 980 - price: 318 + cost: 148 + init_stock: 1100 + price: 148 service_level: 0.96 - vlt: 3 + vlt: 1 SKU919: - cost: 59 - init_stock: 220 - price: 59 + cost: 393 + init_stock: 500 + price: 393 service_level: 0.96 vlt: 3 SKU92: - cost: 341 - init_stock: 1220 - price: 341 + cost: 262 + init_stock: 1260 + price: 262 service_level: 0.96 - vlt: 1 + vlt: 2 SKU920: - cost: 161 - init_stock: 480 - price: 161 + cost: 357 + init_stock: 1720 + price: 357 service_level: 0.96 - vlt: 2 + vlt: 3 SKU921: - cost: 489 - init_stock: 1760 - price: 489 + cost: 227 + init_stock: 1320 + price: 227 service_level: 0.96 - vlt: 1 + vlt: 2 SKU922: - cost: 35 - init_stock: 1820 - price: 35 + cost: 112 + init_stock: 1340 + price: 112 service_level: 0.96 vlt: 2 SKU923: - cost: 182 - init_stock: 160 - price: 182 + cost: 496 + init_stock: 1160 + price: 496 service_level: 0.96 - vlt: 3 + vlt: 2 SKU924: - cost: 364 - init_stock: 920 - price: 364 + cost: 316 + init_stock: 1700 + price: 316 service_level: 0.96 vlt: 3 SKU925: - cost: 31 - init_stock: 1900 - price: 31 + cost: 360 + init_stock: 300 + price: 360 service_level: 0.96 - vlt: 2 + vlt: 1 SKU926: - cost: 472 - init_stock: 200 - price: 472 + cost: 360 + init_stock: 1340 + price: 360 service_level: 0.96 - vlt: 3 + vlt: 2 SKU927: - cost: 143 - init_stock: 100 - price: 143 + cost: 260 + init_stock: 420 + price: 260 service_level: 0.96 vlt: 3 SKU928: - cost: 325 - init_stock: 740 - price: 325 + cost: 491 + init_stock: 1660 + price: 491 service_level: 0.96 - vlt: 3 + vlt: 1 SKU929: - cost: 296 - init_stock: 1520 - price: 296 + cost: 359 + init_stock: 2000 + price: 359 service_level: 0.96 - vlt: 1 + vlt: 3 SKU93: - cost: 361 - init_stock: 1680 - price: 361 + cost: 404 + init_stock: 1340 + price: 404 service_level: 0.96 - vlt: 1 + vlt: 2 SKU930: - cost: 257 - init_stock: 1700 - price: 257 + cost: 198 + init_stock: 560 + price: 198 service_level: 0.96 - vlt: 1 + vlt: 2 SKU931: - cost: 156 - init_stock: 660 - price: 156 + cost: 71 + init_stock: 280 + price: 71 service_level: 0.96 - vlt: 1 + vlt: 3 SKU932: - cost: 138 - init_stock: 1980 - price: 138 + cost: 163 + init_stock: 1320 + price: 163 service_level: 0.96 vlt: 2 SKU933: - cost: 284 - init_stock: 760 - price: 284 + cost: 113 + init_stock: 1560 + price: 113 service_level: 0.96 vlt: 1 SKU934: - cost: 213 - init_stock: 860 - price: 213 + cost: 219 + init_stock: 1340 + price: 219 service_level: 0.96 - vlt: 1 + vlt: 3 SKU935: - cost: 339 - init_stock: 540 - price: 339 + cost: 364 + init_stock: 1880 + price: 364 service_level: 0.96 vlt: 1 SKU936: - cost: 170 - init_stock: 180 - price: 170 + cost: 24 + init_stock: 100 + price: 24 service_level: 0.96 vlt: 3 SKU937: - cost: 123 - init_stock: 300 - price: 123 + cost: 135 + init_stock: 340 + price: 135 service_level: 0.96 - vlt: 2 + vlt: 1 SKU938: - cost: 299 - init_stock: 1820 - price: 299 + cost: 432 + init_stock: 420 + price: 432 service_level: 0.96 - vlt: 3 + vlt: 2 SKU939: - cost: 267 - init_stock: 500 - price: 267 + cost: 173 + init_stock: 1180 + price: 173 service_level: 0.96 vlt: 3 SKU94: - cost: 446 - init_stock: 980 - price: 446 + cost: 184 + init_stock: 940 + price: 184 service_level: 0.96 - vlt: 1 + vlt: 3 SKU940: - cost: 475 - init_stock: 1100 - price: 475 + cost: 14 + init_stock: 1860 + price: 14 service_level: 0.96 - vlt: 3 + vlt: 1 SKU941: - cost: 202 - init_stock: 480 - price: 202 + cost: 80 + init_stock: 1140 + price: 80 service_level: 0.96 vlt: 3 SKU942: - cost: 408 - init_stock: 1440 - price: 408 + cost: 202 + init_stock: 260 + price: 202 service_level: 0.96 - vlt: 2 + vlt: 3 SKU943: - cost: 189 - init_stock: 160 - price: 189 + cost: 138 + init_stock: 980 + price: 138 service_level: 0.96 - vlt: 3 + vlt: 1 SKU944: - cost: 494 - init_stock: 680 - price: 494 + cost: 196 + init_stock: 880 + price: 196 service_level: 0.96 vlt: 1 SKU945: - cost: 237 + cost: 141 init_stock: 340 - price: 237 + price: 141 service_level: 0.96 - vlt: 1 + vlt: 2 SKU946: - cost: 345 - init_stock: 1960 - price: 345 + cost: 325 + init_stock: 980 + price: 325 service_level: 0.96 - vlt: 2 + vlt: 1 SKU947: - cost: 495 - init_stock: 1080 - price: 495 + cost: 338 + init_stock: 1820 + price: 338 service_level: 0.96 vlt: 3 SKU948: - cost: 314 - init_stock: 1560 - price: 314 + cost: 425 + init_stock: 560 + price: 425 service_level: 0.96 vlt: 3 SKU949: - cost: 453 - init_stock: 380 - price: 453 + cost: 309 + init_stock: 100 + price: 309 service_level: 0.96 - vlt: 1 + vlt: 2 SKU95: - cost: 25 - init_stock: 1940 - price: 25 + cost: 136 + init_stock: 420 + price: 136 service_level: 0.96 - vlt: 2 + vlt: 3 SKU950: - cost: 286 - init_stock: 1380 - price: 286 + cost: 102 + init_stock: 1080 + price: 102 service_level: 0.96 - vlt: 3 + vlt: 2 SKU951: - cost: 167 - init_stock: 1400 - price: 167 + cost: 75 + init_stock: 360 + price: 75 service_level: 0.96 vlt: 2 SKU952: - cost: 493 - init_stock: 820 - price: 493 + cost: 156 + init_stock: 220 + price: 156 service_level: 0.96 - vlt: 1 + vlt: 3 SKU953: - cost: 472 - init_stock: 1580 - price: 472 + cost: 138 + init_stock: 700 + price: 138 service_level: 0.96 - vlt: 1 + vlt: 3 SKU954: - cost: 287 - init_stock: 280 - price: 287 + cost: 296 + init_stock: 1720 + price: 296 service_level: 0.96 - vlt: 3 + vlt: 1 SKU955: - cost: 94 - init_stock: 300 - price: 94 + cost: 55 + init_stock: 1260 + price: 55 service_level: 0.96 vlt: 1 SKU956: - cost: 157 - init_stock: 740 - price: 157 + cost: 282 + init_stock: 1700 + price: 282 service_level: 0.96 vlt: 1 SKU957: - cost: 304 - init_stock: 1260 - price: 304 + cost: 305 + init_stock: 1020 + price: 305 service_level: 0.96 vlt: 2 SKU958: - cost: 348 - init_stock: 1100 - price: 348 + cost: 369 + init_stock: 1320 + price: 369 service_level: 0.96 vlt: 2 SKU959: - cost: 442 - init_stock: 1580 - price: 442 + cost: 81 + init_stock: 1640 + price: 81 service_level: 0.96 - vlt: 3 + vlt: 1 SKU96: - cost: 473 - init_stock: 980 - price: 473 + cost: 176 + init_stock: 880 + price: 176 service_level: 0.96 - vlt: 1 + vlt: 3 SKU960: - cost: 27 - init_stock: 1520 - price: 27 + cost: 147 + init_stock: 120 + price: 147 service_level: 0.96 - vlt: 2 + vlt: 3 SKU961: - cost: 282 - init_stock: 1280 - price: 282 + cost: 264 + init_stock: 880 + price: 264 service_level: 0.96 - vlt: 2 + vlt: 1 SKU962: - cost: 391 - init_stock: 220 - price: 391 + cost: 354 + init_stock: 880 + price: 354 service_level: 0.96 - vlt: 3 + vlt: 1 SKU963: - cost: 247 - init_stock: 360 - price: 247 + cost: 349 + init_stock: 420 + price: 349 service_level: 0.96 vlt: 1 SKU964: - cost: 495 - init_stock: 240 - price: 495 + cost: 244 + init_stock: 1460 + price: 244 service_level: 0.96 - vlt: 3 + vlt: 1 SKU965: - cost: 428 - init_stock: 1480 - price: 428 + cost: 124 + init_stock: 1020 + price: 124 service_level: 0.96 - vlt: 3 + vlt: 1 SKU966: - cost: 360 - init_stock: 820 - price: 360 + cost: 302 + init_stock: 880 + price: 302 service_level: 0.96 - vlt: 1 + vlt: 3 SKU967: - cost: 407 - init_stock: 1620 - price: 407 + cost: 67 + init_stock: 900 + price: 67 service_level: 0.96 - vlt: 1 + vlt: 3 SKU968: - cost: 69 - init_stock: 1260 - price: 69 + cost: 281 + init_stock: 980 + price: 281 service_level: 0.96 - vlt: 1 + vlt: 2 SKU969: - cost: 131 - init_stock: 640 - price: 131 + cost: 249 + init_stock: 120 + price: 249 service_level: 0.96 - vlt: 3 + vlt: 1 SKU97: - cost: 432 - init_stock: 980 - price: 432 + cost: 28 + init_stock: 600 + price: 28 service_level: 0.96 - vlt: 2 + vlt: 3 SKU970: - cost: 376 - init_stock: 1920 - price: 376 + cost: 244 + init_stock: 100 + price: 244 service_level: 0.96 vlt: 2 SKU971: - cost: 44 - init_stock: 280 - price: 44 + cost: 368 + init_stock: 1460 + price: 368 service_level: 0.96 vlt: 1 SKU972: - cost: 78 - init_stock: 160 - price: 78 + cost: 209 + init_stock: 1380 + price: 209 service_level: 0.96 - vlt: 2 + vlt: 1 SKU973: - cost: 407 - init_stock: 840 - price: 407 + cost: 271 + init_stock: 220 + price: 271 service_level: 0.96 vlt: 2 SKU974: - cost: 434 - init_stock: 540 - price: 434 + cost: 170 + init_stock: 640 + price: 170 service_level: 0.96 - vlt: 2 + vlt: 1 SKU975: - cost: 98 - init_stock: 1780 - price: 98 + cost: 198 + init_stock: 440 + price: 198 service_level: 0.96 - vlt: 2 + vlt: 3 SKU976: - cost: 263 - init_stock: 1220 - price: 263 + cost: 178 + init_stock: 300 + price: 178 service_level: 0.96 vlt: 2 SKU977: - cost: 83 - init_stock: 460 - price: 83 + cost: 234 + init_stock: 1080 + price: 234 service_level: 0.96 vlt: 3 SKU978: - cost: 90 - init_stock: 1540 - price: 90 + cost: 470 + init_stock: 1280 + price: 470 service_level: 0.96 vlt: 3 SKU979: - cost: 207 - init_stock: 260 - price: 207 + cost: 443 + init_stock: 1920 + price: 443 service_level: 0.96 - vlt: 3 + vlt: 2 SKU98: - cost: 43 - init_stock: 1660 - price: 43 + cost: 443 + init_stock: 700 + price: 443 service_level: 0.96 vlt: 1 SKU980: - cost: 227 - init_stock: 1200 - price: 227 + cost: 39 + init_stock: 1360 + price: 39 service_level: 0.96 - vlt: 2 + vlt: 3 SKU981: - cost: 278 - init_stock: 340 - price: 278 + cost: 482 + init_stock: 1440 + price: 482 service_level: 0.96 vlt: 2 SKU982: - cost: 125 - init_stock: 200 - price: 125 + cost: 213 + init_stock: 1040 + price: 213 service_level: 0.96 vlt: 3 SKU983: - cost: 431 - init_stock: 1360 - price: 431 + cost: 449 + init_stock: 1840 + price: 449 service_level: 0.96 - vlt: 3 + vlt: 1 SKU984: - cost: 59 - init_stock: 560 - price: 59 + cost: 232 + init_stock: 1820 + price: 232 service_level: 0.96 - vlt: 2 + vlt: 3 SKU985: - cost: 191 - init_stock: 1260 - price: 191 + cost: 290 + init_stock: 1020 + price: 290 service_level: 0.96 vlt: 1 SKU986: - cost: 236 - init_stock: 760 - price: 236 + cost: 275 + init_stock: 340 + price: 275 service_level: 0.96 - vlt: 2 + vlt: 3 SKU987: - cost: 333 - init_stock: 1980 - price: 333 + cost: 434 + init_stock: 680 + price: 434 service_level: 0.96 - vlt: 3 + vlt: 1 SKU988: - cost: 407 - init_stock: 1540 - price: 407 + cost: 102 + init_stock: 460 + price: 102 service_level: 0.96 vlt: 1 SKU989: - cost: 53 - init_stock: 1640 - price: 53 + cost: 484 + init_stock: 1860 + price: 484 service_level: 0.96 - vlt: 3 + vlt: 1 SKU99: - cost: 65 - init_stock: 1580 - price: 65 + cost: 361 + init_stock: 900 + price: 361 service_level: 0.96 - vlt: 1 + vlt: 3 SKU990: - cost: 368 - init_stock: 1880 - price: 368 + cost: 108 + init_stock: 1140 + price: 108 service_level: 0.96 - vlt: 2 + vlt: 3 SKU991: - cost: 348 - init_stock: 1840 - price: 348 + cost: 409 + init_stock: 380 + price: 409 service_level: 0.96 vlt: 3 SKU992: - cost: 59 - init_stock: 1360 - price: 59 + cost: 434 + init_stock: 1860 + price: 434 service_level: 0.96 vlt: 3 SKU993: - cost: 498 - init_stock: 460 - price: 498 + cost: 145 + init_stock: 1720 + price: 145 service_level: 0.96 vlt: 2 SKU994: - cost: 438 - init_stock: 680 - price: 438 + cost: 470 + init_stock: 1000 + price: 470 service_level: 0.96 - vlt: 2 + vlt: 3 SKU995: - cost: 292 - init_stock: 980 - price: 292 + cost: 241 + init_stock: 1520 + price: 241 service_level: 0.96 - vlt: 1 + vlt: 2 SKU996: - cost: 73 - init_stock: 980 - price: 73 + cost: 260 + init_stock: 1460 + price: 260 service_level: 0.96 vlt: 3 SKU997: - cost: 321 - init_stock: 1420 - price: 321 + cost: 400 + init_stock: 680 + price: 400 service_level: 0.96 - vlt: 3 + vlt: 1 SKU998: - cost: 371 - init_stock: 300 - price: 371 + cost: 447 + init_stock: 1100 + price: 447 service_level: 0.96 - vlt: 1 + vlt: 2 SKU999: - cost: 208 - init_stock: 1780 - price: 208 + cost: 79 + init_stock: 460 + price: 79 service_level: 0.96 - vlt: 3 + vlt: 2 - children: storage: config: - capacity: 1063220 + capacity: 1064900 unit_storage_cost: 1 config: order_cost: 500 @@ -35118,8016 +15318,8016 @@ world: name: STORE0 skus: SKU0: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 154 - init_stock: 267 + constraint: null + cost: 322 + init_stock: 126 max_stock: 1000 - price: 195 - sale_gamma: 89 + price: 631 + sale_gamma: 63 service_level: 0.95 SKU1: - constraint: null - cost: 122 - init_stock: 172 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 284 + init_stock: 176 max_stock: 1000 - price: 168 - sale_gamma: 86 + price: 448 + sale_gamma: 22 service_level: 0.95 SKU10: - constraint: G(stock_constraint) - cost: 158 - init_stock: 460 + constraint: null + cost: 68 + init_stock: 150 max_stock: 1000 - price: 254 - sale_gamma: 92 - service_level: 0.95 - SKU100: - constraint: G(stock_constraint) - cost: 448 - init_stock: 185 + price: 116 + sale_gamma: 25 + service_level: 0.95 + SKU100: + constraint: G(low_profit -> low_stock_constraint) + cost: 16 + init_stock: 390 max_stock: 1000 - price: 663 - sale_gamma: 37 + price: 23 + sale_gamma: 65 service_level: 0.95 SKU101: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 248 + constraint: null + cost: 84 + init_stock: 497 max_stock: 1000 - price: 807 - sale_gamma: 62 + price: 149 + sale_gamma: 71 service_level: 0.95 SKU102: constraint: null - cost: 33 - init_stock: 581 + cost: 328 + init_stock: 558 max_stock: 1000 - price: 38 - sale_gamma: 83 + price: 505 + sale_gamma: 93 service_level: 0.95 SKU103: constraint: null - cost: 169 - init_stock: 395 + cost: 334 + init_stock: 285 max_stock: 1000 - price: 273 - sale_gamma: 79 + price: 601 + sale_gamma: 95 service_level: 0.95 SKU104: constraint: null - cost: 253 - init_stock: 116 + cost: 49 + init_stock: 325 max_stock: 1000 - price: 384 - sale_gamma: 29 + price: 57 + sale_gamma: 65 service_level: 0.95 SKU105: - constraint: null - cost: 305 - init_stock: 472 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 110 + init_stock: 285 max_stock: 1000 - price: 506 - sale_gamma: 59 + price: 171 + sale_gamma: 57 service_level: 0.95 SKU106: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 172 - init_stock: 174 + constraint: G(low_profit -> low_stock_constraint) + cost: 251 + init_stock: 584 max_stock: 1000 - price: 211 - sale_gamma: 29 + price: 454 + sale_gamma: 73 service_level: 0.95 SKU107: - constraint: null - cost: 323 - init_stock: 243 + constraint: G(stock_constraint) + cost: 423 + init_stock: 348 max_stock: 1000 - price: 555 - sale_gamma: 81 + price: 706 + sale_gamma: 87 service_level: 0.95 SKU108: - constraint: null - cost: 283 - init_stock: 644 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 458 + init_stock: 368 max_stock: 1000 - price: 316 + price: 801 sale_gamma: 92 service_level: 0.95 SKU109: - constraint: null - cost: 304 - init_stock: 462 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 88 + init_stock: 328 max_stock: 1000 - price: 550 - sale_gamma: 77 + price: 98 + sale_gamma: 82 service_level: 0.95 SKU11: constraint: null - cost: 204 - init_stock: 348 + cost: 400 + init_stock: 306 max_stock: 1000 - price: 359 - sale_gamma: 58 + price: 680 + sale_gamma: 51 service_level: 0.95 SKU110: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 374 - init_stock: 651 + cost: 66 + init_stock: 112 max_stock: 1000 - price: 628 - sale_gamma: 93 + price: 100 + sale_gamma: 14 service_level: 0.95 SKU111: constraint: G(stock_constraint) - cost: 215 - init_stock: 312 + cost: 260 + init_stock: 183 max_stock: 1000 - price: 245 - sale_gamma: 78 + price: 364 + sale_gamma: 61 service_level: 0.95 SKU112: - constraint: null - cost: 417 - init_stock: 48 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 61 + init_stock: 475 max_stock: 1000 - price: 792 - sale_gamma: 8 + price: 119 + sale_gamma: 95 service_level: 0.95 SKU113: - constraint: G(stock_constraint) - cost: 64 - init_stock: 207 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 155 max_stock: 1000 - price: 101 - sale_gamma: 69 + price: 678 + sale_gamma: 31 service_level: 0.95 SKU114: - constraint: G(stock_constraint) - cost: 63 - init_stock: 270 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 389 + init_stock: 162 max_stock: 1000 - price: 72 - sale_gamma: 54 + price: 564 + sale_gamma: 27 service_level: 0.95 SKU115: - constraint: G(stock_constraint) - cost: 466 - init_stock: 609 + constraint: G(low_profit -> low_stock_constraint) + cost: 286 + init_stock: 258 max_stock: 1000 - price: 880 - sale_gamma: 87 + price: 557 + sale_gamma: 86 service_level: 0.95 SKU116: - constraint: null - cost: 115 - init_stock: 184 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 496 + init_stock: 288 max_stock: 1000 - price: 175 - sale_gamma: 92 + price: 768 + sale_gamma: 72 service_level: 0.95 SKU117: constraint: null - cost: 76 - init_stock: 201 + cost: 320 + init_stock: 644 max_stock: 1000 - price: 97 - sale_gamma: 67 + price: 374 + sale_gamma: 92 service_level: 0.95 SKU118: constraint: null - cost: 292 - init_stock: 414 + cost: 183 + init_stock: 66 max_stock: 1000 - price: 338 - sale_gamma: 69 + price: 208 + sale_gamma: 33 service_level: 0.95 SKU119: constraint: null - cost: 333 - init_stock: 78 + cost: 209 + init_stock: 256 max_stock: 1000 - price: 656 - sale_gamma: 39 + price: 317 + sale_gamma: 32 service_level: 0.95 SKU12: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 429 - init_stock: 296 + constraint: null + cost: 112 + init_stock: 336 max_stock: 1000 - price: 660 - sale_gamma: 74 + price: 210 + sale_gamma: 84 service_level: 0.95 SKU120: - constraint: null - cost: 147 - init_stock: 384 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 121 + init_stock: 392 max_stock: 1000 - price: 224 - sale_gamma: 48 + price: 151 + sale_gamma: 98 service_level: 0.95 SKU121: constraint: null - cost: 169 - init_stock: 354 + cost: 40 + init_stock: 510 max_stock: 1000 - price: 290 - sale_gamma: 59 + price: 54 + sale_gamma: 85 service_level: 0.95 SKU122: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 71 - init_stock: 360 + constraint: G(stock_constraint) + cost: 437 + init_stock: 35 max_stock: 1000 - price: 127 - sale_gamma: 72 + price: 589 + sale_gamma: 7 service_level: 0.95 SKU123: - constraint: null - cost: 33 - init_stock: 354 + constraint: G(stock_constraint) + cost: 233 + init_stock: 114 max_stock: 1000 - price: 50 - sale_gamma: 59 + price: 326 + sale_gamma: 19 service_level: 0.95 SKU124: - constraint: G(stock_constraint) - cost: 478 - init_stock: 348 + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 108 max_stock: 1000 - price: 884 - sale_gamma: 58 + price: 298 + sale_gamma: 36 service_level: 0.95 SKU125: - constraint: G(stock_constraint) - cost: 311 - init_stock: 180 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 16 + init_stock: 368 max_stock: 1000 - price: 469 - sale_gamma: 36 + price: 32 + sale_gamma: 92 service_level: 0.95 SKU126: constraint: null - cost: 497 - init_stock: 456 + cost: 36 + init_stock: 156 max_stock: 1000 - price: 979 - sale_gamma: 76 + price: 40 + sale_gamma: 39 service_level: 0.95 SKU127: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 63 - init_stock: 460 + constraint: G(stock_constraint) + cost: 217 + init_stock: 155 max_stock: 1000 - price: 105 - sale_gamma: 92 + price: 366 + sale_gamma: 31 service_level: 0.95 SKU128: constraint: null - cost: 200 - init_stock: 294 + cost: 165 + init_stock: 95 max_stock: 1000 - price: 348 - sale_gamma: 42 + price: 283 + sale_gamma: 19 service_level: 0.95 SKU129: - constraint: null - cost: 465 - init_stock: 203 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 300 max_stock: 1000 - price: 902 - sale_gamma: 29 + price: 203 + sale_gamma: 50 service_level: 0.95 SKU13: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 458 - init_stock: 232 + constraint: null + cost: 317 + init_stock: 456 max_stock: 1000 - price: 540 - sale_gamma: 29 + price: 573 + sale_gamma: 57 service_level: 0.95 SKU130: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 224 - init_stock: 324 + constraint: null + cost: 348 + init_stock: 784 max_stock: 1000 - price: 425 - sale_gamma: 54 + price: 396 + sale_gamma: 98 service_level: 0.95 SKU131: - constraint: null - cost: 491 - init_stock: 240 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 64 + init_stock: 270 max_stock: 1000 - price: 746 - sale_gamma: 60 + price: 110 + sale_gamma: 45 service_level: 0.95 SKU132: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 93 - init_stock: 366 + constraint: null + cost: 427 + init_stock: 105 max_stock: 1000 - price: 169 - sale_gamma: 61 + price: 678 + sale_gamma: 21 service_level: 0.95 SKU133: constraint: null - cost: 269 - init_stock: 39 + cost: 224 + init_stock: 145 max_stock: 1000 - price: 422 - sale_gamma: 13 + price: 448 + sale_gamma: 29 service_level: 0.95 SKU134: - constraint: null - cost: 364 - init_stock: 80 + constraint: G(stock_constraint) + cost: 336 + init_stock: 539 max_stock: 1000 - price: 680 - sale_gamma: 20 + price: 470 + sale_gamma: 77 service_level: 0.95 SKU135: constraint: null - cost: 351 - init_stock: 490 + cost: 153 + init_stock: 500 max_stock: 1000 - price: 582 - sale_gamma: 98 + price: 243 + sale_gamma: 100 service_level: 0.95 SKU136: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 429 - init_stock: 240 + constraint: G(low_profit -> low_stock_constraint) + cost: 199 + init_stock: 497 max_stock: 1000 - price: 617 - sale_gamma: 40 + price: 370 + sale_gamma: 71 service_level: 0.95 SKU137: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 193 - init_stock: 552 + constraint: G(stock_constraint) + cost: 93 + init_stock: 444 max_stock: 1000 - price: 301 - sale_gamma: 92 + price: 165 + sale_gamma: 74 service_level: 0.95 SKU138: - constraint: G(low_profit -> low_stock_constraint) - cost: 113 - init_stock: 140 + constraint: G(stock_constraint) + cost: 228 + init_stock: 180 max_stock: 1000 - price: 187 - sale_gamma: 28 + price: 253 + sale_gamma: 36 service_level: 0.95 SKU139: - constraint: null - cost: 10 - init_stock: 475 + constraint: G(stock_constraint) + cost: 207 + init_stock: 144 max_stock: 1000 - price: 14 - sale_gamma: 95 + price: 368 + sale_gamma: 24 service_level: 0.95 SKU14: - constraint: G(stock_constraint) - cost: 55 - init_stock: 65 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 268 + init_stock: 372 max_stock: 1000 - price: 67 - sale_gamma: 13 + price: 305 + sale_gamma: 62 service_level: 0.95 SKU140: constraint: null - cost: 350 - init_stock: 288 + cost: 261 + init_stock: 238 max_stock: 1000 - price: 399 - sale_gamma: 96 + price: 357 + sale_gamma: 34 service_level: 0.95 SKU141: - constraint: G(low_profit -> low_stock_constraint) - cost: 70 - init_stock: 426 + constraint: null + cost: 190 + init_stock: 164 max_stock: 1000 - price: 133 - sale_gamma: 71 + price: 370 + sale_gamma: 41 service_level: 0.95 SKU142: - constraint: null - cost: 496 - init_stock: 438 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 320 + init_stock: 152 max_stock: 1000 - price: 863 - sale_gamma: 73 + price: 428 + sale_gamma: 38 service_level: 0.95 SKU143: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 33 - init_stock: 332 + constraint: G(stock_constraint) + cost: 318 + init_stock: 182 max_stock: 1000 - price: 55 - sale_gamma: 83 + price: 566 + sale_gamma: 26 service_level: 0.95 SKU144: constraint: null - cost: 275 - init_stock: 450 + cost: 400 + init_stock: 24 max_stock: 1000 - price: 354 - sale_gamma: 75 + price: 716 + sale_gamma: 12 service_level: 0.95 SKU145: - constraint: G(stock_constraint) - cost: 51 - init_stock: 679 + constraint: null + cost: 399 + init_stock: 328 max_stock: 1000 - price: 87 - sale_gamma: 97 + price: 774 + sale_gamma: 82 service_level: 0.95 SKU146: - constraint: G(stock_constraint) - cost: 140 - init_stock: 252 + constraint: null + cost: 177 + init_stock: 192 max_stock: 1000 - price: 275 - sale_gamma: 36 + price: 194 + sale_gamma: 48 service_level: 0.95 SKU147: - constraint: null - cost: 383 - init_stock: 98 + constraint: G(low_profit -> low_stock_constraint) + cost: 472 + init_stock: 392 max_stock: 1000 - price: 547 - sale_gamma: 49 + price: 840 + sale_gamma: 56 service_level: 0.95 SKU148: constraint: G(stock_constraint) - cost: 278 - init_stock: 408 + cost: 313 + init_stock: 308 max_stock: 1000 - price: 553 - sale_gamma: 68 + price: 566 + sale_gamma: 77 service_level: 0.95 SKU149: - constraint: null - cost: 402 - init_stock: 260 + constraint: G(low_profit -> low_stock_constraint) + cost: 357 + init_stock: 539 max_stock: 1000 - price: 554 - sale_gamma: 52 + price: 549 + sale_gamma: 77 service_level: 0.95 SKU15: - constraint: G(stock_constraint) - cost: 421 - init_stock: 284 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 25 max_stock: 1000 - price: 783 - sale_gamma: 71 + price: 58 + sale_gamma: 5 service_level: 0.95 SKU150: - constraint: G(low_profit -> low_stock_constraint) - cost: 81 - init_stock: 332 + constraint: null + cost: 106 + init_stock: 210 max_stock: 1000 - price: 105 - sale_gamma: 83 + price: 159 + sale_gamma: 70 service_level: 0.95 SKU151: - constraint: null - cost: 407 - init_stock: 455 + constraint: G(low_profit -> low_stock_constraint) + cost: 223 + init_stock: 146 max_stock: 1000 - price: 724 - sale_gamma: 65 + price: 370 + sale_gamma: 73 service_level: 0.95 SKU152: constraint: null - cost: 327 - init_stock: 320 + cost: 10 + init_stock: 408 max_stock: 1000 - price: 640 - sale_gamma: 64 + price: 14 + sale_gamma: 51 service_level: 0.95 SKU153: - constraint: null - cost: 204 - init_stock: 582 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 441 + init_stock: 372 max_stock: 1000 - price: 236 - sale_gamma: 97 + price: 538 + sale_gamma: 62 service_level: 0.95 SKU154: - constraint: G(stock_constraint) - cost: 88 - init_stock: 156 + constraint: null + cost: 77 + init_stock: 680 max_stock: 1000 - price: 123 - sale_gamma: 26 + price: 135 + sale_gamma: 85 service_level: 0.95 SKU155: - constraint: null - cost: 428 - init_stock: 130 + constraint: G(low_profit -> low_stock_constraint) + cost: 422 + init_stock: 159 max_stock: 1000 - price: 509 - sale_gamma: 26 + price: 641 + sale_gamma: 53 service_level: 0.95 SKU156: - constraint: G(low_profit -> low_stock_constraint) - cost: 486 - init_stock: 32 + constraint: null + cost: 10 + init_stock: 36 max_stock: 1000 - price: 913 - sale_gamma: 8 + price: 16 + sale_gamma: 12 service_level: 0.95 SKU157: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 35 - init_stock: 264 + constraint: null + cost: 410 + init_stock: 525 max_stock: 1000 - price: 40 - sale_gamma: 66 + price: 594 + sale_gamma: 75 service_level: 0.95 SKU158: - constraint: G(stock_constraint) - cost: 186 - init_stock: 285 + constraint: null + cost: 145 + init_stock: 486 max_stock: 1000 - price: 264 - sale_gamma: 57 + price: 275 + sale_gamma: 81 service_level: 0.95 SKU159: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 202 - init_stock: 368 + constraint: G(stock_constraint) + cost: 193 + init_stock: 100 max_stock: 1000 - price: 321 - sale_gamma: 92 + price: 368 + sale_gamma: 25 service_level: 0.95 SKU16: constraint: null - cost: 209 - init_stock: 144 + cost: 175 + init_stock: 464 max_stock: 1000 - price: 252 - sale_gamma: 24 + price: 344 + sale_gamma: 58 service_level: 0.95 SKU160: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 433 - init_stock: 141 + constraint: G(low_profit -> low_stock_constraint) + cost: 459 + init_stock: 510 max_stock: 1000 - price: 853 - sale_gamma: 47 + price: 876 + sale_gamma: 85 service_level: 0.95 SKU161: - constraint: null - cost: 250 - init_stock: 176 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 239 + init_stock: 230 max_stock: 1000 - price: 477 - sale_gamma: 44 + price: 478 + sale_gamma: 46 service_level: 0.95 SKU162: constraint: null - cost: 316 - init_stock: 640 + cost: 158 + init_stock: 30 max_stock: 1000 - price: 467 - sale_gamma: 80 + price: 290 + sale_gamma: 5 service_level: 0.95 SKU163: - constraint: null - cost: 114 - init_stock: 70 + constraint: G(stock_constraint) + cost: 486 + init_stock: 273 max_stock: 1000 - price: 224 - sale_gamma: 14 + price: 704 + sale_gamma: 39 service_level: 0.95 SKU164: - constraint: G(low_profit -> low_stock_constraint) - cost: 304 - init_stock: 264 + constraint: null + cost: 496 + init_stock: 500 max_stock: 1000 - price: 370 - sale_gamma: 66 + price: 615 + sale_gamma: 100 service_level: 0.95 SKU165: - constraint: null - cost: 195 - init_stock: 212 + constraint: G(stock_constraint) + cost: 274 + init_stock: 231 max_stock: 1000 - price: 300 - sale_gamma: 53 + price: 548 + sale_gamma: 33 service_level: 0.95 SKU166: constraint: null - cost: 315 - init_stock: 255 + cost: 79 + init_stock: 178 max_stock: 1000 - price: 620 - sale_gamma: 51 + price: 137 + sale_gamma: 89 service_level: 0.95 SKU167: - constraint: G(low_profit -> low_stock_constraint) - cost: 470 - init_stock: 470 + constraint: G(stock_constraint) + cost: 443 + init_stock: 52 max_stock: 1000 - price: 714 - sale_gamma: 94 + price: 854 + sale_gamma: 13 service_level: 0.95 SKU168: - constraint: G(stock_constraint) - cost: 476 - init_stock: 95 + constraint: null + cost: 357 + init_stock: 522 max_stock: 1000 - price: 923 - sale_gamma: 19 + price: 546 + sale_gamma: 87 service_level: 0.95 SKU169: - constraint: G(stock_constraint) - cost: 134 - init_stock: 95 + constraint: null + cost: 369 + init_stock: 686 max_stock: 1000 - price: 176 - sale_gamma: 19 + price: 505 + sale_gamma: 98 service_level: 0.95 SKU17: constraint: null - cost: 401 - init_stock: 152 + cost: 346 + init_stock: 18 max_stock: 1000 - price: 701 - sale_gamma: 38 + price: 553 + sale_gamma: 9 service_level: 0.95 SKU170: constraint: null - cost: 289 - init_stock: 114 + cost: 68 + init_stock: 275 max_stock: 1000 - price: 482 - sale_gamma: 38 + price: 113 + sale_gamma: 55 service_level: 0.95 SKU171: - constraint: null - cost: 481 - init_stock: 492 + constraint: G(low_profit -> low_stock_constraint) + cost: 398 + init_stock: 380 max_stock: 1000 - price: 630 - sale_gamma: 82 + price: 704 + sale_gamma: 76 service_level: 0.95 SKU172: - constraint: G(low_profit -> low_stock_constraint) - cost: 434 - init_stock: 124 + constraint: null + cost: 200 + init_stock: 426 max_stock: 1000 - price: 525 - sale_gamma: 31 + price: 222 + sale_gamma: 71 service_level: 0.95 SKU173: - constraint: null - cost: 292 - init_stock: 470 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 175 + init_stock: 480 max_stock: 1000 - price: 449 - sale_gamma: 94 + price: 264 + sale_gamma: 96 service_level: 0.95 SKU174: constraint: null - cost: 474 - init_stock: 56 + cost: 291 + init_stock: 380 max_stock: 1000 - price: 848 - sale_gamma: 14 + price: 582 + sale_gamma: 76 service_level: 0.95 SKU175: - constraint: G(stock_constraint) - cost: 476 - init_stock: 63 + constraint: null + cost: 84 + init_stock: 225 max_stock: 1000 - price: 633 - sale_gamma: 21 + price: 140 + sale_gamma: 75 service_level: 0.95 SKU176: - constraint: null - cost: 299 - init_stock: 320 + constraint: G(stock_constraint) + cost: 407 + init_stock: 330 max_stock: 1000 - price: 499 - sale_gamma: 80 + price: 610 + sale_gamma: 66 service_level: 0.95 SKU177: - constraint: G(stock_constraint) - cost: 425 - init_stock: 63 + constraint: null + cost: 257 + init_stock: 155 max_stock: 1000 - price: 731 - sale_gamma: 21 + price: 346 + sale_gamma: 31 service_level: 0.95 SKU178: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 419 - init_stock: 420 + constraint: null + cost: 423 + init_stock: 20 max_stock: 1000 - price: 733 - sale_gamma: 70 + price: 499 + sale_gamma: 5 service_level: 0.95 SKU179: - constraint: G(stock_constraint) - cost: 236 - init_stock: 390 + constraint: null + cost: 497 + init_stock: 415 max_stock: 1000 - price: 413 - sale_gamma: 65 + price: 690 + sale_gamma: 83 service_level: 0.95 SKU18: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 460 - init_stock: 243 + constraint: null + cost: 258 + init_stock: 405 max_stock: 1000 - price: 542 + price: 387 sale_gamma: 81 service_level: 0.95 SKU180: constraint: null - cost: 99 - init_stock: 32 + cost: 217 + init_stock: 165 max_stock: 1000 - price: 167 - sale_gamma: 16 + price: 297 + sale_gamma: 55 service_level: 0.95 SKU181: - constraint: null - cost: 122 - init_stock: 108 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 300 max_stock: 1000 - price: 158 - sale_gamma: 27 + price: 214 + sale_gamma: 60 service_level: 0.95 SKU182: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 443 - init_stock: 69 + constraint: null + cost: 437 + init_stock: 693 max_stock: 1000 - price: 637 - sale_gamma: 23 + price: 764 + sale_gamma: 99 service_level: 0.95 SKU183: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 72 - init_stock: 106 + constraint: null + cost: 145 + init_stock: 56 max_stock: 1000 - price: 108 - sale_gamma: 53 + price: 178 + sale_gamma: 8 service_level: 0.95 SKU184: constraint: null - cost: 192 - init_stock: 462 + cost: 73 + init_stock: 72 max_stock: 1000 - price: 345 - sale_gamma: 77 + price: 100 + sale_gamma: 24 service_level: 0.95 SKU185: - constraint: G(low_profit -> low_stock_constraint) - cost: 130 - init_stock: 84 + constraint: null + cost: 10 + init_stock: 360 max_stock: 1000 - price: 166 - sale_gamma: 28 + price: 14 + sale_gamma: 90 service_level: 0.95 SKU186: - constraint: G(low_profit -> low_stock_constraint) - cost: 49 - init_stock: 195 + constraint: null + cost: 359 + init_stock: 66 max_stock: 1000 - price: 59 - sale_gamma: 39 + price: 649 + sale_gamma: 22 service_level: 0.95 SKU187: - constraint: G(stock_constraint) - cost: 492 - init_stock: 188 + constraint: G(low_profit -> low_stock_constraint) + cost: 177 + init_stock: 120 max_stock: 1000 - price: 757 - sale_gamma: 47 + price: 249 + sale_gamma: 30 service_level: 0.95 SKU188: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 50 - init_stock: 450 + constraint: null + cost: 391 + init_stock: 348 max_stock: 1000 - price: 90 - sale_gamma: 90 + price: 586 + sale_gamma: 87 service_level: 0.95 SKU189: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 145 - init_stock: 99 + constraint: G(low_profit -> low_stock_constraint) + cost: 358 + init_stock: 210 max_stock: 1000 - price: 163 - sale_gamma: 33 + price: 400 + sale_gamma: 35 service_level: 0.95 SKU19: - constraint: G(low_profit -> low_stock_constraint) - cost: 451 - init_stock: 18 + constraint: G(stock_constraint) + cost: 477 + init_stock: 364 max_stock: 1000 - price: 775 - sale_gamma: 6 + price: 791 + sale_gamma: 91 service_level: 0.95 SKU190: - constraint: G(low_profit -> low_stock_constraint) - cost: 72 - init_stock: 474 + constraint: null + cost: 113 + init_stock: 102 max_stock: 1000 - price: 108 - sale_gamma: 79 + price: 153 + sale_gamma: 17 service_level: 0.95 SKU191: - constraint: null - cost: 285 - init_stock: 280 + constraint: G(stock_constraint) + cost: 473 + init_stock: 324 max_stock: 1000 - price: 473 - sale_gamma: 40 + price: 638 + sale_gamma: 54 service_level: 0.95 SKU192: constraint: null - cost: 183 - init_stock: 190 + cost: 415 + init_stock: 244 max_stock: 1000 - price: 296 - sale_gamma: 95 + price: 539 + sale_gamma: 61 service_level: 0.95 SKU193: - constraint: null - cost: 345 - init_stock: 260 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 207 + init_stock: 120 max_stock: 1000 - price: 424 - sale_gamma: 65 + price: 353 + sale_gamma: 30 service_level: 0.95 SKU194: - constraint: G(stock_constraint) - cost: 439 - init_stock: 222 + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 30 max_stock: 1000 - price: 777 - sale_gamma: 74 + price: 803 + sale_gamma: 5 service_level: 0.95 SKU195: - constraint: G(stock_constraint) - cost: 205 - init_stock: 128 + constraint: G(low_profit -> low_stock_constraint) + cost: 218 + init_stock: 93 max_stock: 1000 - price: 373 - sale_gamma: 32 + price: 248 + sale_gamma: 31 service_level: 0.95 SKU196: constraint: null - cost: 208 - init_stock: 88 + cost: 49 + init_stock: 544 max_stock: 1000 - price: 278 - sale_gamma: 22 + price: 62 + sale_gamma: 68 service_level: 0.95 SKU197: - constraint: null - cost: 476 - init_stock: 30 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 303 + init_stock: 285 max_stock: 1000 - price: 952 - sale_gamma: 5 + price: 509 + sale_gamma: 57 service_level: 0.95 SKU198: - constraint: null - cost: 269 - init_stock: 308 + constraint: G(low_profit -> low_stock_constraint) + cost: 169 + init_stock: 378 max_stock: 1000 - price: 508 - sale_gamma: 77 + price: 307 + sale_gamma: 54 service_level: 0.95 SKU199: - constraint: G(stock_constraint) - cost: 172 - init_stock: 330 + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 138 max_stock: 1000 - price: 247 - sale_gamma: 55 + price: 794 + sale_gamma: 23 service_level: 0.95 SKU2: - constraint: G(stock_constraint) - cost: 204 - init_stock: 175 + constraint: null + cost: 331 + init_stock: 350 max_stock: 1000 - price: 405 - sale_gamma: 25 + price: 499 + sale_gamma: 70 service_level: 0.95 SKU20: - constraint: null - cost: 437 - init_stock: 164 + constraint: G(stock_constraint) + cost: 335 + init_stock: 200 max_stock: 1000 - price: 865 - sale_gamma: 41 + price: 649 + sale_gamma: 25 service_level: 0.95 SKU200: - constraint: G(low_profit -> low_stock_constraint) - cost: 51 - init_stock: 376 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 65 + init_stock: 150 max_stock: 1000 - price: 63 - sale_gamma: 94 + price: 85 + sale_gamma: 25 service_level: 0.95 SKU201: constraint: G(stock_constraint) - cost: 487 - init_stock: 444 + cost: 104 + init_stock: 354 max_stock: 1000 - price: 637 - sale_gamma: 74 + price: 161 + sale_gamma: 59 service_level: 0.95 SKU202: - constraint: G(low_profit -> low_stock_constraint) - cost: 406 - init_stock: 42 + constraint: null + cost: 142 + init_stock: 365 max_stock: 1000 - price: 657 - sale_gamma: 14 + price: 222 + sale_gamma: 73 service_level: 0.95 SKU203: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 116 + constraint: null + cost: 440 + init_stock: 328 max_stock: 1000 - price: 566 - sale_gamma: 29 + price: 677 + sale_gamma: 82 service_level: 0.95 SKU204: constraint: null - cost: 64 - init_stock: 624 + cost: 489 + init_stock: 188 max_stock: 1000 - price: 122 - sale_gamma: 78 + price: 831 + sale_gamma: 47 service_level: 0.95 SKU205: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 448 - init_stock: 455 + cost: 130 + init_stock: 500 max_stock: 1000 - price: 595 - sale_gamma: 91 + price: 175 + sale_gamma: 100 service_level: 0.95 SKU206: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 65 - init_stock: 485 + constraint: null + cost: 335 + init_stock: 55 max_stock: 1000 - price: 107 - sale_gamma: 97 + price: 552 + sale_gamma: 11 service_level: 0.95 SKU207: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 437 - init_stock: 290 + constraint: G(low_profit -> low_stock_constraint) + cost: 140 + init_stock: 240 max_stock: 1000 - price: 725 - sale_gamma: 58 + price: 170 + sale_gamma: 80 service_level: 0.95 SKU208: - constraint: G(stock_constraint) - cost: 38 - init_stock: 100 + constraint: null + cost: 491 + init_stock: 308 max_stock: 1000 - price: 63 - sale_gamma: 25 + price: 927 + sale_gamma: 77 service_level: 0.95 SKU209: - constraint: null - cost: 327 - init_stock: 260 + constraint: G(stock_constraint) + cost: 179 + init_stock: 120 max_stock: 1000 - price: 637 - sale_gamma: 65 + price: 322 + sale_gamma: 20 service_level: 0.95 SKU21: constraint: null - cost: 17 - init_stock: 275 + cost: 123 + init_stock: 400 max_stock: 1000 - price: 26 - sale_gamma: 55 + price: 225 + sale_gamma: 100 service_level: 0.95 SKU210: constraint: null - cost: 455 - init_stock: 150 + cost: 404 + init_stock: 345 max_stock: 1000 - price: 800 - sale_gamma: 50 + price: 468 + sale_gamma: 69 service_level: 0.95 SKU211: - constraint: G(stock_constraint) - cost: 170 - init_stock: 105 + constraint: null + cost: 174 + init_stock: 364 max_stock: 1000 - price: 326 - sale_gamma: 15 + price: 226 + sale_gamma: 91 service_level: 0.95 SKU212: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 62 - init_stock: 276 + constraint: null + cost: 405 + init_stock: 632 max_stock: 1000 - price: 83 - sale_gamma: 69 + price: 534 + sale_gamma: 79 service_level: 0.95 SKU213: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 263 - init_stock: 140 + constraint: null + cost: 121 + init_stock: 192 max_stock: 1000 - price: 410 - sale_gamma: 70 + price: 229 + sale_gamma: 64 service_level: 0.95 SKU214: - constraint: null - cost: 434 - init_stock: 42 + constraint: G(stock_constraint) + cost: 101 + init_stock: 60 max_stock: 1000 - price: 555 - sale_gamma: 7 + price: 144 + sale_gamma: 10 service_level: 0.95 SKU215: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 283 - init_stock: 144 + constraint: null + cost: 419 + init_stock: 94 max_stock: 1000 - price: 546 - sale_gamma: 24 + price: 469 + sale_gamma: 47 service_level: 0.95 SKU216: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 391 - init_stock: 390 + cost: 330 + init_stock: 92 max_stock: 1000 - price: 574 - sale_gamma: 78 + price: 432 + sale_gamma: 23 service_level: 0.95 SKU217: - constraint: G(low_profit -> low_stock_constraint) - cost: 168 - init_stock: 196 + constraint: null + cost: 284 + init_stock: 455 max_stock: 1000 - price: 324 - sale_gamma: 98 + price: 312 + sale_gamma: 65 service_level: 0.95 SKU218: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 485 - init_stock: 54 + constraint: G(low_profit -> low_stock_constraint) + cost: 205 + init_stock: 354 max_stock: 1000 - price: 635 - sale_gamma: 18 + price: 397 + sale_gamma: 59 service_level: 0.95 SKU219: - constraint: G(low_profit -> low_stock_constraint) - cost: 201 - init_stock: 516 + constraint: G(stock_constraint) + cost: 92 + init_stock: 368 max_stock: 1000 - price: 355 - sale_gamma: 86 + price: 135 + sale_gamma: 46 service_level: 0.95 SKU22: - constraint: G(stock_constraint) - cost: 22 - init_stock: 90 + constraint: null + cost: 493 + init_stock: 264 max_stock: 1000 - price: 33 - sale_gamma: 30 + price: 986 + sale_gamma: 66 service_level: 0.95 SKU220: constraint: null - cost: 394 - init_stock: 78 + cost: 387 + init_stock: 435 max_stock: 1000 - price: 673 - sale_gamma: 13 + price: 572 + sale_gamma: 87 service_level: 0.95 SKU221: - constraint: G(stock_constraint) - cost: 108 - init_stock: 644 + constraint: null + cost: 39 + init_stock: 468 max_stock: 1000 - price: 156 - sale_gamma: 92 + price: 48 + sale_gamma: 78 service_level: 0.95 SKU222: - constraint: null - cost: 459 - init_stock: 168 + constraint: G(low_profit -> low_stock_constraint) + cost: 115 + init_stock: 180 max_stock: 1000 - price: 514 - sale_gamma: 56 + price: 210 + sale_gamma: 36 service_level: 0.95 SKU223: - constraint: null - cost: 420 - init_stock: 365 + constraint: G(low_profit -> low_stock_constraint) + cost: 196 + init_stock: 36 max_stock: 1000 - price: 747 - sale_gamma: 73 + price: 286 + sale_gamma: 12 service_level: 0.95 SKU224: - constraint: null - cost: 306 - init_stock: 420 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 387 + init_stock: 25 max_stock: 1000 - price: 388 - sale_gamma: 70 + price: 661 + sale_gamma: 5 service_level: 0.95 SKU225: constraint: null - cost: 118 - init_stock: 368 + cost: 164 + init_stock: 116 max_stock: 1000 - price: 172 - sale_gamma: 92 + price: 216 + sale_gamma: 29 service_level: 0.95 SKU226: - constraint: G(stock_constraint) - cost: 425 - init_stock: 176 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 206 + init_stock: 65 max_stock: 1000 - price: 565 - sale_gamma: 44 + price: 350 + sale_gamma: 13 service_level: 0.95 SKU227: constraint: G(low_profit -> low_stock_constraint) - cost: 191 - init_stock: 325 + cost: 479 + init_stock: 144 max_stock: 1000 - price: 259 - sale_gamma: 65 + price: 895 + sale_gamma: 24 service_level: 0.95 SKU228: - constraint: G(stock_constraint) - cost: 160 - init_stock: 192 + constraint: null + cost: 14 + init_stock: 180 max_stock: 1000 - price: 227 - sale_gamma: 32 + price: 21 + sale_gamma: 90 service_level: 0.95 SKU229: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 278 - init_stock: 584 + constraint: null + cost: 472 + init_stock: 176 max_stock: 1000 - price: 430 - sale_gamma: 73 + price: 708 + sale_gamma: 44 service_level: 0.95 SKU23: constraint: null - cost: 124 - init_stock: 70 + cost: 387 + init_stock: 210 max_stock: 1000 - price: 235 - sale_gamma: 14 + price: 700 + sale_gamma: 42 service_level: 0.95 SKU230: constraint: null - cost: 164 - init_stock: 76 - max_stock: 1000 - price: 216 - sale_gamma: 19 + cost: 241 + init_stock: 138 + max_stock: 1000 + price: 436 + sale_gamma: 23 service_level: 0.95 SKU231: - constraint: null - cost: 321 - init_stock: 564 + constraint: G(stock_constraint) + cost: 48 + init_stock: 486 max_stock: 1000 - price: 510 - sale_gamma: 94 + price: 96 + sale_gamma: 81 service_level: 0.95 SKU232: - constraint: null - cost: 435 - init_stock: 82 + constraint: G(low_profit -> low_stock_constraint) + cost: 224 + init_stock: 480 max_stock: 1000 - price: 526 - sale_gamma: 41 + price: 338 + sale_gamma: 96 service_level: 0.95 SKU233: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 219 - init_stock: 148 + constraint: G(low_profit -> low_stock_constraint) + cost: 360 + init_stock: 300 max_stock: 1000 - price: 326 - sale_gamma: 37 + price: 428 + sale_gamma: 75 service_level: 0.95 SKU234: - constraint: G(low_profit -> low_stock_constraint) - cost: 144 - init_stock: 119 + constraint: null + cost: 287 + init_stock: 25 max_stock: 1000 - price: 230 - sale_gamma: 17 + price: 387 + sale_gamma: 5 service_level: 0.95 SKU235: - constraint: G(stock_constraint) - cost: 396 - init_stock: 132 + constraint: G(low_profit -> low_stock_constraint) + cost: 24 + init_stock: 228 max_stock: 1000 - price: 669 - sale_gamma: 22 + price: 32 + sale_gamma: 57 service_level: 0.95 SKU236: - constraint: G(stock_constraint) - cost: 383 - init_stock: 396 + constraint: G(low_profit -> low_stock_constraint) + cost: 155 + init_stock: 165 max_stock: 1000 - price: 501 - sale_gamma: 99 + price: 289 + sale_gamma: 55 service_level: 0.95 SKU237: constraint: null - cost: 108 - init_stock: 306 + cost: 433 + init_stock: 270 max_stock: 1000 - price: 128 - sale_gamma: 51 + price: 779 + sale_gamma: 45 service_level: 0.95 SKU238: - constraint: null - cost: 49 - init_stock: 228 + constraint: G(stock_constraint) + cost: 64 + init_stock: 264 max_stock: 1000 - price: 84 - sale_gamma: 57 + price: 112 + sale_gamma: 66 service_level: 0.95 SKU239: - constraint: G(stock_constraint) - cost: 222 - init_stock: 270 + constraint: null + cost: 103 + init_stock: 490 max_stock: 1000 - price: 381 - sale_gamma: 90 + price: 139 + sale_gamma: 98 service_level: 0.95 SKU24: - constraint: G(stock_constraint) - cost: 368 - init_stock: 235 + constraint: null + cost: 97 + init_stock: 329 max_stock: 1000 - price: 426 + price: 114 sale_gamma: 47 service_level: 0.95 SKU240: constraint: null - cost: 387 - init_stock: 498 + cost: 373 + init_stock: 188 max_stock: 1000 - price: 534 - sale_gamma: 83 + price: 738 + sale_gamma: 47 service_level: 0.95 SKU241: - constraint: null - cost: 302 - init_stock: 267 + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 213 max_stock: 1000 - price: 356 - sale_gamma: 89 + price: 860 + sale_gamma: 71 service_level: 0.95 SKU242: - constraint: G(stock_constraint) - cost: 62 - init_stock: 395 + constraint: null + cost: 17 + init_stock: 88 max_stock: 1000 - price: 101 - sale_gamma: 79 + price: 29 + sale_gamma: 44 service_level: 0.95 SKU243: - constraint: G(stock_constraint) - cost: 12 - init_stock: 350 + constraint: null + cost: 352 + init_stock: 84 max_stock: 1000 - price: 16 - sale_gamma: 50 + price: 394 + sale_gamma: 14 service_level: 0.95 SKU244: constraint: null - cost: 296 - init_stock: 390 + cost: 174 + init_stock: 410 max_stock: 1000 - price: 372 - sale_gamma: 65 + price: 226 + sale_gamma: 82 service_level: 0.95 SKU245: constraint: G(low_profit -> low_stock_constraint) - cost: 39 - init_stock: 195 + cost: 404 + init_stock: 198 max_stock: 1000 - price: 70 - sale_gamma: 39 + price: 525 + sale_gamma: 66 service_level: 0.95 SKU246: - constraint: null - cost: 263 - init_stock: 180 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 300 + init_stock: 210 max_stock: 1000 - price: 297 - sale_gamma: 45 + price: 474 + sale_gamma: 30 service_level: 0.95 SKU247: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 224 - init_stock: 130 + constraint: null + cost: 386 + init_stock: 210 max_stock: 1000 - price: 277 - sale_gamma: 26 + price: 497 + sale_gamma: 35 service_level: 0.95 SKU248: - constraint: null - cost: 410 - init_stock: 252 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 355 + init_stock: 174 max_stock: 1000 - price: 553 - sale_gamma: 84 + price: 539 + sale_gamma: 29 service_level: 0.95 SKU249: constraint: null - cost: 64 - init_stock: 485 + cost: 356 + init_stock: 100 max_stock: 1000 - price: 74 - sale_gamma: 97 + price: 544 + sale_gamma: 25 service_level: 0.95 SKU25: constraint: G(stock_constraint) - cost: 201 - init_stock: 138 + cost: 161 + init_stock: 35 max_stock: 1000 - price: 267 - sale_gamma: 46 + price: 249 + sale_gamma: 5 service_level: 0.95 SKU250: constraint: null - cost: 259 - init_stock: 225 + cost: 127 + init_stock: 324 max_stock: 1000 - price: 466 - sale_gamma: 45 + price: 186 + sale_gamma: 54 service_level: 0.95 SKU251: - constraint: null - cost: 181 - init_stock: 69 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 344 + init_stock: 244 max_stock: 1000 - price: 208 - sale_gamma: 23 + price: 543 + sale_gamma: 61 service_level: 0.95 SKU252: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 330 - init_stock: 390 + constraint: null + cost: 181 + init_stock: 415 max_stock: 1000 - price: 468 - sale_gamma: 65 + price: 334 + sale_gamma: 83 service_level: 0.95 SKU253: - constraint: null - cost: 315 - init_stock: 736 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 448 + init_stock: 32 max_stock: 1000 - price: 526 - sale_gamma: 92 + price: 864 + sale_gamma: 16 service_level: 0.95 SKU254: constraint: null - cost: 407 - init_stock: 343 + cost: 484 + init_stock: 184 max_stock: 1000 - price: 740 - sale_gamma: 49 + price: 696 + sale_gamma: 46 service_level: 0.95 SKU255: constraint: null - cost: 361 - init_stock: 444 + cost: 290 + init_stock: 335 max_stock: 1000 - price: 689 - sale_gamma: 74 + price: 568 + sale_gamma: 67 service_level: 0.95 SKU256: constraint: null - cost: 67 - init_stock: 235 + cost: 91 + init_stock: 360 max_stock: 1000 - price: 83 - sale_gamma: 47 + price: 167 + sale_gamma: 72 service_level: 0.95 SKU257: - constraint: G(stock_constraint) - cost: 50 - init_stock: 120 + constraint: null + cost: 348 + init_stock: 171 max_stock: 1000 - price: 62 - sale_gamma: 60 + price: 497 + sale_gamma: 57 service_level: 0.95 SKU258: - constraint: G(stock_constraint) - cost: 465 - init_stock: 16 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 258 max_stock: 1000 - price: 692 - sale_gamma: 8 + price: 689 + sale_gamma: 43 service_level: 0.95 SKU259: - constraint: null - cost: 346 - init_stock: 160 + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 207 max_stock: 1000 - price: 570 - sale_gamma: 40 + price: 559 + sale_gamma: 69 service_level: 0.95 SKU26: constraint: null - cost: 419 - init_stock: 325 + cost: 229 + init_stock: 126 max_stock: 1000 - price: 733 - sale_gamma: 65 + price: 357 + sale_gamma: 63 service_level: 0.95 SKU260: - constraint: null - cost: 374 - init_stock: 126 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 364 max_stock: 1000 - price: 527 - sale_gamma: 42 + price: 866 + sale_gamma: 52 service_level: 0.95 SKU261: constraint: null - cost: 74 - init_stock: 189 + cost: 368 + init_stock: 66 max_stock: 1000 - price: 139 - sale_gamma: 27 + price: 688 + sale_gamma: 22 service_level: 0.95 SKU262: - constraint: G(low_profit -> low_stock_constraint) - cost: 456 - init_stock: 345 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 332 + init_stock: 390 max_stock: 1000 - price: 606 - sale_gamma: 69 + price: 385 + sale_gamma: 78 service_level: 0.95 SKU263: - constraint: null - cost: 276 - init_stock: 432 + constraint: G(stock_constraint) + cost: 189 + init_stock: 444 max_stock: 1000 - price: 347 - sale_gamma: 54 + price: 372 + sale_gamma: 74 service_level: 0.95 SKU264: constraint: null - cost: 89 - init_stock: 360 + cost: 361 + init_stock: 158 max_stock: 1000 - price: 126 - sale_gamma: 60 + price: 566 + sale_gamma: 79 service_level: 0.95 SKU265: constraint: null - cost: 148 - init_stock: 395 + cost: 286 + init_stock: 236 max_stock: 1000 - price: 281 - sale_gamma: 79 + price: 434 + sale_gamma: 59 service_level: 0.95 SKU266: constraint: null - cost: 287 - init_stock: 91 + cost: 128 + init_stock: 282 max_stock: 1000 - price: 373 - sale_gamma: 13 + price: 172 + sale_gamma: 47 service_level: 0.95 SKU267: - constraint: null - cost: 31 - init_stock: 288 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 77 + init_stock: 320 max_stock: 1000 - price: 39 - sale_gamma: 96 + price: 130 + sale_gamma: 80 service_level: 0.95 SKU268: constraint: null - cost: 340 - init_stock: 66 + cost: 221 + init_stock: 356 max_stock: 1000 - price: 530 - sale_gamma: 22 + price: 362 + sale_gamma: 89 service_level: 0.95 SKU269: - constraint: G(stock_constraint) - cost: 479 - init_stock: 416 + constraint: G(low_profit -> low_stock_constraint) + cost: 126 + init_stock: 132 max_stock: 1000 - price: 819 - sale_gamma: 52 + price: 175 + sale_gamma: 44 service_level: 0.95 SKU27: - constraint: null - cost: 269 - init_stock: 623 + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 448 max_stock: 1000 - price: 508 - sale_gamma: 89 + price: 728 + sale_gamma: 56 service_level: 0.95 SKU270: constraint: null - cost: 233 - init_stock: 280 + cost: 182 + init_stock: 370 max_stock: 1000 - price: 410 - sale_gamma: 40 + price: 263 + sale_gamma: 74 service_level: 0.95 SKU271: - constraint: G(low_profit -> low_stock_constraint) - cost: 90 - init_stock: 60 + constraint: G(stock_constraint) + cost: 230 + init_stock: 54 max_stock: 1000 - price: 131 - sale_gamma: 12 + price: 266 + sale_gamma: 18 service_level: 0.95 SKU272: - constraint: G(low_profit -> low_stock_constraint) - cost: 488 - init_stock: 240 + constraint: null + cost: 366 + init_stock: 51 max_stock: 1000 - price: 858 - sale_gamma: 30 + price: 625 + sale_gamma: 17 service_level: 0.95 SKU273: - constraint: G(low_profit -> low_stock_constraint) - cost: 499 - init_stock: 140 + constraint: null + cost: 421 + init_stock: 72 max_stock: 1000 - price: 683 - sale_gamma: 20 + price: 614 + sale_gamma: 18 service_level: 0.95 SKU274: - constraint: G(low_profit -> low_stock_constraint) - cost: 434 - init_stock: 372 + constraint: null + cost: 29 + init_stock: 308 max_stock: 1000 - price: 512 - sale_gamma: 93 + price: 42 + sale_gamma: 77 service_level: 0.95 SKU275: - constraint: G(stock_constraint) - cost: 457 - init_stock: 192 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 50 + init_stock: 144 max_stock: 1000 - price: 676 - sale_gamma: 96 + price: 76 + sale_gamma: 48 service_level: 0.95 SKU276: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 179 - init_stock: 198 + constraint: G(stock_constraint) + cost: 163 + init_stock: 378 max_stock: 1000 - price: 261 - sale_gamma: 33 + price: 299 + sale_gamma: 54 service_level: 0.95 SKU277: - constraint: null - cost: 303 - init_stock: 310 + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 82 max_stock: 1000 - price: 518 - sale_gamma: 62 + price: 507 + sale_gamma: 41 service_level: 0.95 SKU278: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 301 - init_stock: 192 + constraint: null + cost: 105 + init_stock: 84 max_stock: 1000 - price: 358 - sale_gamma: 48 + price: 140 + sale_gamma: 12 service_level: 0.95 SKU279: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 17 - init_stock: 328 + constraint: G(stock_constraint) + cost: 51 + init_stock: 273 max_stock: 1000 - price: 22 - sale_gamma: 82 + price: 61 + sale_gamma: 39 service_level: 0.95 SKU28: - constraint: null - cost: 399 - init_stock: 36 + constraint: G(stock_constraint) + cost: 208 + init_stock: 235 max_stock: 1000 - price: 694 - sale_gamma: 6 + price: 295 + sale_gamma: 47 service_level: 0.95 SKU280: - constraint: null - cost: 238 - init_stock: 664 + constraint: G(low_profit -> low_stock_constraint) + cost: 295 + init_stock: 198 max_stock: 1000 - price: 278 - sale_gamma: 83 + price: 436 + sale_gamma: 33 service_level: 0.95 SKU281: constraint: null - cost: 13 - init_stock: 276 + cost: 395 + init_stock: 375 max_stock: 1000 - price: 23 - sale_gamma: 69 + price: 592 + sale_gamma: 75 service_level: 0.95 SKU282: constraint: null - cost: 404 - init_stock: 336 + cost: 63 + init_stock: 184 max_stock: 1000 - price: 755 - sale_gamma: 84 + price: 112 + sale_gamma: 46 service_level: 0.95 SKU283: - constraint: G(stock_constraint) - cost: 332 - init_stock: 192 + constraint: null + cost: 392 + init_stock: 736 max_stock: 1000 - price: 491 - sale_gamma: 32 - service_level: 0.95 - SKU284: - constraint: G(low_profit -> low_stock_constraint) - cost: 257 - init_stock: 195 + price: 490 + sale_gamma: 92 + service_level: 0.95 + SKU284: + constraint: G(stock_constraint) + cost: 344 + init_stock: 268 max_stock: 1000 - price: 436 - sale_gamma: 39 + price: 643 + sale_gamma: 67 service_level: 0.95 SKU285: - constraint: null - cost: 53 - init_stock: 30 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 133 + init_stock: 546 max_stock: 1000 - price: 79 - sale_gamma: 6 + price: 148 + sale_gamma: 91 service_level: 0.95 SKU286: constraint: null - cost: 252 - init_stock: 52 + cost: 337 + init_stock: 156 max_stock: 1000 - price: 493 - sale_gamma: 13 + price: 626 + sale_gamma: 39 service_level: 0.95 SKU287: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 161 - init_stock: 658 + constraint: null + cost: 375 + init_stock: 224 max_stock: 1000 - price: 180 - sale_gamma: 94 + price: 660 + sale_gamma: 56 service_level: 0.95 SKU288: - constraint: null - cost: 477 - init_stock: 80 + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 190 max_stock: 1000 - price: 701 - sale_gamma: 16 + price: 352 + sale_gamma: 38 service_level: 0.95 SKU289: - constraint: null - cost: 452 - init_stock: 475 + constraint: G(stock_constraint) + cost: 67 + init_stock: 62 max_stock: 1000 - price: 858 - sale_gamma: 95 + price: 91 + sale_gamma: 31 service_level: 0.95 SKU29: constraint: null - cost: 160 - init_stock: 148 + cost: 245 + init_stock: 174 max_stock: 1000 - price: 228 - sale_gamma: 74 + price: 475 + sale_gamma: 58 service_level: 0.95 SKU290: constraint: null - cost: 493 - init_stock: 84 + cost: 438 + init_stock: 268 max_stock: 1000 - price: 719 - sale_gamma: 28 + price: 779 + sale_gamma: 67 service_level: 0.95 SKU291: - constraint: null - cost: 180 - init_stock: 57 + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 122 max_stock: 1000 - price: 331 - sale_gamma: 19 + price: 126 + sale_gamma: 61 service_level: 0.95 SKU292: constraint: null - cost: 233 - init_stock: 380 + cost: 186 + init_stock: 15 max_stock: 1000 - price: 447 - sale_gamma: 95 + price: 344 + sale_gamma: 5 service_level: 0.95 SKU293: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 177 - init_stock: 132 + constraint: G(stock_constraint) + cost: 162 + init_stock: 385 max_stock: 1000 - price: 329 - sale_gamma: 66 + price: 288 + sale_gamma: 55 service_level: 0.95 SKU294: constraint: null - cost: 356 - init_stock: 135 + cost: 409 + init_stock: 45 max_stock: 1000 - price: 427 - sale_gamma: 27 + price: 605 + sale_gamma: 9 service_level: 0.95 SKU295: - constraint: G(low_profit -> low_stock_constraint) - cost: 421 - init_stock: 574 + constraint: null + cost: 435 + init_stock: 651 max_stock: 1000 - price: 572 - sale_gamma: 82 + price: 674 + sale_gamma: 93 service_level: 0.95 SKU296: - constraint: G(stock_constraint) - cost: 414 - init_stock: 282 + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 552 max_stock: 1000 - price: 480 - sale_gamma: 47 + price: 580 + sale_gamma: 92 service_level: 0.95 SKU297: constraint: null - cost: 326 - init_stock: 360 + cost: 298 + init_stock: 228 max_stock: 1000 - price: 495 - sale_gamma: 90 + price: 333 + sale_gamma: 38 service_level: 0.95 SKU298: - constraint: G(low_profit -> low_stock_constraint) - cost: 122 - init_stock: 276 + constraint: null + cost: 286 + init_stock: 140 max_stock: 1000 - price: 157 - sale_gamma: 46 + price: 500 + sale_gamma: 35 service_level: 0.95 SKU299: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 149 - init_stock: 340 + constraint: G(stock_constraint) + cost: 367 + init_stock: 255 max_stock: 1000 - price: 207 - sale_gamma: 68 + price: 451 + sale_gamma: 51 service_level: 0.95 SKU3: - constraint: null - cost: 161 - init_stock: 30 + constraint: G(stock_constraint) + cost: 405 + init_stock: 48 max_stock: 1000 - price: 305 - sale_gamma: 5 + price: 733 + sale_gamma: 12 service_level: 0.95 SKU30: - constraint: G(stock_constraint) - cost: 395 - init_stock: 366 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 359 + init_stock: 276 max_stock: 1000 - price: 533 - sale_gamma: 61 + price: 581 + sale_gamma: 69 service_level: 0.95 SKU300: - constraint: null - cost: 250 - init_stock: 117 + constraint: G(low_profit -> low_stock_constraint) + cost: 376 + init_stock: 290 max_stock: 1000 - price: 315 - sale_gamma: 39 + price: 428 + sale_gamma: 58 service_level: 0.95 SKU301: - constraint: G(stock_constraint) - cost: 279 - init_stock: 35 + constraint: null + cost: 433 + init_stock: 581 max_stock: 1000 - price: 410 - sale_gamma: 5 + price: 857 + sale_gamma: 83 service_level: 0.95 SKU302: - constraint: G(low_profit -> low_stock_constraint) - cost: 321 - init_stock: 222 + constraint: null + cost: 184 + init_stock: 44 max_stock: 1000 - price: 609 - sale_gamma: 74 + price: 322 + sale_gamma: 11 service_level: 0.95 SKU303: - constraint: G(stock_constraint) - cost: 202 - init_stock: 275 + constraint: null + cost: 246 + init_stock: 188 max_stock: 1000 - price: 343 - sale_gamma: 55 + price: 487 + sale_gamma: 94 service_level: 0.95 SKU304: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 393 - init_stock: 476 + constraint: G(low_profit -> low_stock_constraint) + cost: 419 + init_stock: 138 max_stock: 1000 - price: 758 - sale_gamma: 68 + price: 632 + sale_gamma: 23 service_level: 0.95 SKU305: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 113 - init_stock: 335 + constraint: null + cost: 495 + init_stock: 500 max_stock: 1000 - price: 195 - sale_gamma: 67 + price: 772 + sale_gamma: 100 service_level: 0.95 SKU306: constraint: null - cost: 430 - init_stock: 172 + cost: 479 + init_stock: 126 max_stock: 1000 - price: 477 - sale_gamma: 43 + price: 852 + sale_gamma: 42 service_level: 0.95 SKU307: - constraint: null - cost: 311 - init_stock: 324 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 210 + init_stock: 156 max_stock: 1000 - price: 482 - sale_gamma: 81 + price: 371 + sale_gamma: 78 service_level: 0.95 SKU308: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 423 - init_stock: 348 + constraint: null + cost: 208 + init_stock: 25 max_stock: 1000 - price: 727 - sale_gamma: 58 + price: 262 + sale_gamma: 5 service_level: 0.95 - SKU309: - constraint: G(low_profit -> low_stock_constraint) - cost: 448 - init_stock: 237 + SKU309: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 101 + init_stock: 460 max_stock: 1000 - price: 779 - sale_gamma: 79 + price: 156 + sale_gamma: 92 service_level: 0.95 SKU31: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 456 - init_stock: 228 + constraint: null + cost: 69 + init_stock: 280 max_stock: 1000 - price: 775 - sale_gamma: 76 + price: 120 + sale_gamma: 56 service_level: 0.95 SKU310: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 245 - init_stock: 378 + constraint: null + cost: 234 + init_stock: 352 max_stock: 1000 - price: 365 - sale_gamma: 63 + price: 294 + sale_gamma: 44 service_level: 0.95 SKU311: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 452 - init_stock: 420 + constraint: null + cost: 306 + init_stock: 400 max_stock: 1000 - price: 872 - sale_gamma: 70 + price: 437 + sale_gamma: 80 service_level: 0.95 SKU312: - constraint: null - cost: 112 - init_stock: 584 + constraint: G(stock_constraint) + cost: 291 + init_stock: 75 max_stock: 1000 - price: 162 - sale_gamma: 73 + price: 392 + sale_gamma: 25 service_level: 0.95 SKU313: - constraint: null - cost: 307 - init_stock: 30 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 324 + init_stock: 190 max_stock: 1000 - price: 583 - sale_gamma: 10 + price: 492 + sale_gamma: 38 service_level: 0.95 SKU314: - constraint: null - cost: 40 - init_stock: 532 + constraint: G(stock_constraint) + cost: 404 + init_stock: 116 max_stock: 1000 - price: 53 - sale_gamma: 76 + price: 456 + sale_gamma: 29 service_level: 0.95 SKU315: - constraint: G(low_profit -> low_stock_constraint) - cost: 87 - init_stock: 88 + constraint: G(stock_constraint) + cost: 471 + init_stock: 504 max_stock: 1000 - price: 155 - sale_gamma: 22 + price: 885 + sale_gamma: 84 service_level: 0.95 SKU316: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 367 - init_stock: 128 + constraint: null + cost: 202 + init_stock: 90 max_stock: 1000 - price: 638 - sale_gamma: 16 + price: 282 + sale_gamma: 18 service_level: 0.95 SKU317: - constraint: G(stock_constraint) - cost: 62 - init_stock: 65 + constraint: null + cost: 216 + init_stock: 168 max_stock: 1000 - price: 71 - sale_gamma: 13 + price: 352 + sale_gamma: 24 service_level: 0.95 SKU318: constraint: null - cost: 153 - init_stock: 126 + cost: 278 + init_stock: 255 max_stock: 1000 - price: 257 - sale_gamma: 42 + price: 350 + sale_gamma: 85 service_level: 0.95 SKU319: constraint: null - cost: 340 - init_stock: 184 + cost: 257 + init_stock: 371 max_stock: 1000 - price: 469 - sale_gamma: 46 + price: 454 + sale_gamma: 53 service_level: 0.95 SKU32: - constraint: G(low_profit -> low_stock_constraint) - cost: 223 - init_stock: 356 + constraint: null + cost: 336 + init_stock: 110 max_stock: 1000 - price: 437 - sale_gamma: 89 + price: 581 + sale_gamma: 22 service_level: 0.95 SKU320: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 188 - init_stock: 252 + constraint: null + cost: 196 + init_stock: 156 max_stock: 1000 - price: 347 - sale_gamma: 84 + price: 258 + sale_gamma: 39 service_level: 0.95 SKU321: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 421 - init_stock: 282 + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 96 max_stock: 1000 - price: 808 - sale_gamma: 47 + price: 105 + sale_gamma: 16 service_level: 0.95 SKU322: constraint: null - cost: 462 - init_stock: 380 + cost: 240 + init_stock: 600 max_stock: 1000 - price: 863 - sale_gamma: 76 + price: 453 + sale_gamma: 100 service_level: 0.95 SKU323: constraint: G(stock_constraint) - cost: 116 - init_stock: 395 + cost: 66 + init_stock: 195 max_stock: 1000 - price: 143 - sale_gamma: 79 + price: 74 + sale_gamma: 39 service_level: 0.95 SKU324: - constraint: null - cost: 396 - init_stock: 100 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 442 + init_stock: 465 max_stock: 1000 - price: 597 - sale_gamma: 20 + price: 804 + sale_gamma: 93 service_level: 0.95 SKU325: constraint: null - cost: 407 - init_stock: 180 + cost: 453 + init_stock: 345 max_stock: 1000 - price: 797 - sale_gamma: 60 + price: 579 + sale_gamma: 69 service_level: 0.95 SKU326: constraint: null - cost: 400 - init_stock: 96 + cost: 345 + init_stock: 72 max_stock: 1000 - price: 516 - sale_gamma: 12 + price: 538 + sale_gamma: 24 service_level: 0.95 SKU327: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 434 - init_stock: 600 + constraint: G(low_profit -> low_stock_constraint) + cost: 457 + init_stock: 84 max_stock: 1000 - price: 607 - sale_gamma: 100 + price: 749 + sale_gamma: 14 service_level: 0.95 SKU328: constraint: null - cost: 179 - init_stock: 42 + cost: 390 + init_stock: 90 max_stock: 1000 - price: 257 - sale_gamma: 21 + price: 487 + sale_gamma: 45 service_level: 0.95 SKU329: constraint: null - cost: 453 - init_stock: 486 + cost: 67 + init_stock: 84 max_stock: 1000 - price: 530 - sale_gamma: 81 + price: 126 + sale_gamma: 42 service_level: 0.95 SKU33: - constraint: null - cost: 113 - init_stock: 356 + constraint: G(low_profit -> low_stock_constraint) + cost: 416 + init_stock: 552 max_stock: 1000 - price: 124 - sale_gamma: 89 + price: 465 + sale_gamma: 92 service_level: 0.95 SKU330: constraint: null - cost: 215 - init_stock: 552 + cost: 306 + init_stock: 273 max_stock: 1000 - price: 341 - sale_gamma: 92 + price: 385 + sale_gamma: 39 service_level: 0.95 SKU331: - constraint: G(stock_constraint) - cost: 28 - init_stock: 672 + constraint: G(low_profit -> low_stock_constraint) + cost: 138 + init_stock: 164 max_stock: 1000 - price: 44 - sale_gamma: 84 + price: 180 + sale_gamma: 41 service_level: 0.95 SKU332: constraint: null - cost: 172 - init_stock: 220 + cost: 390 + init_stock: 288 max_stock: 1000 - price: 338 - sale_gamma: 55 + price: 670 + sale_gamma: 96 service_level: 0.95 SKU333: constraint: G(stock_constraint) - cost: 63 - init_stock: 201 + cost: 485 + init_stock: 318 max_stock: 1000 - price: 112 - sale_gamma: 67 + price: 800 + sale_gamma: 53 service_level: 0.95 SKU334: - constraint: G(low_profit -> low_stock_constraint) - cost: 253 - init_stock: 273 + constraint: null + cost: 183 + init_stock: 114 max_stock: 1000 - price: 366 - sale_gamma: 39 + price: 245 + sale_gamma: 57 service_level: 0.95 - SKU335: - constraint: G(low_profit -> low_stock_constraint) - cost: 22 - init_stock: 45 + SKU335: + constraint: null + cost: 80 + init_stock: 243 max_stock: 1000 - price: 30 - sale_gamma: 9 + price: 141 + sale_gamma: 81 service_level: 0.95 SKU336: - constraint: null - cost: 263 - init_stock: 232 + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 56 max_stock: 1000 - price: 357 - sale_gamma: 29 + price: 565 + sale_gamma: 28 service_level: 0.95 SKU337: - constraint: G(low_profit -> low_stock_constraint) - cost: 277 - init_stock: 444 + constraint: null + cost: 245 + init_stock: 174 max_stock: 1000 - price: 321 - sale_gamma: 74 + price: 394 + sale_gamma: 29 service_level: 0.95 SKU338: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 181 - init_stock: 272 + cost: 87 + init_stock: 324 max_stock: 1000 - price: 242 - sale_gamma: 34 + price: 150 + sale_gamma: 81 service_level: 0.95 SKU339: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 488 - init_stock: 380 + constraint: G(stock_constraint) + cost: 265 + init_stock: 441 max_stock: 1000 - price: 780 - sale_gamma: 76 + price: 368 + sale_gamma: 63 service_level: 0.95 SKU34: - constraint: G(low_profit -> low_stock_constraint) - cost: 226 - init_stock: 308 + constraint: null + cost: 95 + init_stock: 91 max_stock: 1000 - price: 442 - sale_gamma: 44 + price: 135 + sale_gamma: 13 service_level: 0.95 SKU340: constraint: null - cost: 213 - init_stock: 135 + cost: 480 + init_stock: 435 max_stock: 1000 - price: 340 - sale_gamma: 27 + price: 633 + sale_gamma: 87 service_level: 0.95 SKU341: - constraint: null - cost: 423 - init_stock: 178 + constraint: G(low_profit -> low_stock_constraint) + cost: 462 + init_stock: 280 max_stock: 1000 - price: 626 - sale_gamma: 89 + price: 924 + sale_gamma: 70 service_level: 0.95 SKU342: constraint: null - cost: 414 - init_stock: 294 + cost: 144 + init_stock: 72 max_stock: 1000 - price: 625 - sale_gamma: 49 + price: 216 + sale_gamma: 9 service_level: 0.95 SKU343: - constraint: G(low_profit -> low_stock_constraint) - cost: 429 - init_stock: 208 + constraint: null + cost: 310 + init_stock: 90 max_stock: 1000 - price: 712 - sale_gamma: 52 + price: 589 + sale_gamma: 15 service_level: 0.95 SKU344: - constraint: G(stock_constraint) - cost: 196 - init_stock: 356 + constraint: null + cost: 357 + init_stock: 348 max_stock: 1000 - price: 282 - sale_gamma: 89 + price: 442 + sale_gamma: 87 service_level: 0.95 SKU345: - constraint: G(low_profit -> low_stock_constraint) - cost: 451 - init_stock: 460 + constraint: null + cost: 442 + init_stock: 267 max_stock: 1000 - price: 581 - sale_gamma: 92 + price: 857 + sale_gamma: 89 service_level: 0.95 SKU346: - constraint: null - cost: 485 - init_stock: 120 + constraint: G(stock_constraint) + cost: 350 + init_stock: 336 max_stock: 1000 - price: 756 - sale_gamma: 30 + price: 549 + sale_gamma: 42 service_level: 0.95 SKU347: - constraint: G(stock_constraint) - cost: 30 - init_stock: 136 + constraint: G(low_profit -> low_stock_constraint) + cost: 497 + init_stock: 328 max_stock: 1000 - price: 35 - sale_gamma: 68 + price: 810 + sale_gamma: 82 service_level: 0.95 SKU348: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 12 - init_stock: 72 + constraint: G(stock_constraint) + cost: 147 + init_stock: 100 max_stock: 1000 - price: 13 - sale_gamma: 12 + price: 277 + sale_gamma: 20 service_level: 0.95 SKU349: constraint: null - cost: 168 - init_stock: 176 + cost: 67 + init_stock: 335 max_stock: 1000 - price: 275 - sale_gamma: 44 + price: 116 + sale_gamma: 67 service_level: 0.95 SKU35: constraint: null - cost: 181 - init_stock: 180 + cost: 267 + init_stock: 430 max_stock: 1000 - price: 260 - sale_gamma: 45 + price: 373 + sale_gamma: 86 service_level: 0.95 SKU350: - constraint: null - cost: 381 - init_stock: 87 + constraint: G(stock_constraint) + cost: 279 + init_stock: 552 max_stock: 1000 - price: 491 - sale_gamma: 29 + price: 460 + sale_gamma: 92 service_level: 0.95 SKU351: constraint: null - cost: 333 - init_stock: 480 + cost: 155 + init_stock: 335 max_stock: 1000 - price: 556 - sale_gamma: 96 + price: 294 + sale_gamma: 67 service_level: 0.95 SKU352: constraint: null - cost: 365 - init_stock: 57 + cost: 71 + init_stock: 54 max_stock: 1000 - price: 518 - sale_gamma: 19 + price: 100 + sale_gamma: 18 service_level: 0.95 SKU353: constraint: G(stock_constraint) - cost: 133 - init_stock: 520 + cost: 253 + init_stock: 465 max_stock: 1000 - price: 210 - sale_gamma: 65 + price: 437 + sale_gamma: 93 service_level: 0.95 SKU354: constraint: G(low_profit -> low_stock_constraint) - cost: 356 - init_stock: 310 + cost: 396 + init_stock: 395 max_stock: 1000 - price: 534 - sale_gamma: 62 + price: 566 + sale_gamma: 79 service_level: 0.95 SKU355: - constraint: null - cost: 453 - init_stock: 280 + constraint: G(stock_constraint) + cost: 63 + init_stock: 30 max_stock: 1000 - price: 901 - sale_gamma: 56 + price: 74 + sale_gamma: 10 service_level: 0.95 SKU356: - constraint: G(low_profit -> low_stock_constraint) - cost: 336 - init_stock: 414 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 87 max_stock: 1000 - price: 588 - sale_gamma: 69 + price: 441 + sale_gamma: 29 service_level: 0.95 SKU357: - constraint: null - cost: 77 - init_stock: 165 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 499 + init_stock: 460 max_stock: 1000 - price: 147 - sale_gamma: 33 + price: 868 + sale_gamma: 92 service_level: 0.95 SKU358: constraint: null - cost: 340 - init_stock: 268 + cost: 269 + init_stock: 414 max_stock: 1000 price: 408 - sale_gamma: 67 + sale_gamma: 69 service_level: 0.95 SKU359: - constraint: null - cost: 259 - init_stock: 320 + constraint: G(low_profit -> low_stock_constraint) + cost: 82 + init_stock: 600 max_stock: 1000 - price: 295 - sale_gamma: 64 + price: 110 + sale_gamma: 75 service_level: 0.95 SKU36: - constraint: G(stock_constraint) - cost: 223 - init_stock: 90 + constraint: null + cost: 105 + init_stock: 30 max_stock: 1000 - price: 256 - sale_gamma: 18 + price: 165 + sale_gamma: 10 service_level: 0.95 SKU360: - constraint: null - cost: 146 - init_stock: 270 + constraint: G(low_profit -> low_stock_constraint) + cost: 484 + init_stock: 75 max_stock: 1000 - price: 258 - sale_gamma: 54 + price: 682 + sale_gamma: 25 service_level: 0.95 SKU361: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 47 - init_stock: 445 + constraint: null + cost: 163 + init_stock: 360 max_stock: 1000 - price: 89 - sale_gamma: 89 + price: 288 + sale_gamma: 90 service_level: 0.95 SKU362: constraint: null - cost: 429 - init_stock: 679 + cost: 464 + init_stock: 435 max_stock: 1000 - price: 849 - sale_gamma: 97 + price: 867 + sale_gamma: 87 service_level: 0.95 SKU363: - constraint: null - cost: 495 - init_stock: 640 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 390 max_stock: 1000 - price: 891 - sale_gamma: 80 + price: 93 + sale_gamma: 78 service_level: 0.95 SKU364: - constraint: null - cost: 213 - init_stock: 720 + constraint: G(stock_constraint) + cost: 387 + init_stock: 66 max_stock: 1000 - price: 315 - sale_gamma: 90 + price: 472 + sale_gamma: 33 service_level: 0.95 SKU365: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 116 - init_stock: 36 + constraint: null + cost: 158 + init_stock: 581 max_stock: 1000 - price: 194 - sale_gamma: 12 + price: 241 + sale_gamma: 83 service_level: 0.95 SKU366: constraint: null - cost: 344 - init_stock: 246 + cost: 444 + init_stock: 344 max_stock: 1000 - price: 519 - sale_gamma: 82 + price: 639 + sale_gamma: 86 service_level: 0.95 SKU367: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 197 - init_stock: 45 + constraint: G(low_profit -> low_stock_constraint) + cost: 272 + init_stock: 322 max_stock: 1000 - price: 279 - sale_gamma: 15 + price: 304 + sale_gamma: 46 service_level: 0.95 SKU368: - constraint: null - cost: 410 - init_stock: 360 + constraint: G(stock_constraint) + cost: 472 + init_stock: 198 max_stock: 1000 - price: 520 - sale_gamma: 60 + price: 613 + sale_gamma: 33 service_level: 0.95 SKU369: constraint: null - cost: 187 - init_stock: 450 + cost: 96 + init_stock: 375 max_stock: 1000 - price: 370 - sale_gamma: 90 + price: 155 + sale_gamma: 75 service_level: 0.95 SKU37: - constraint: G(stock_constraint) - cost: 302 - init_stock: 270 + constraint: G(low_profit -> low_stock_constraint) + cost: 66 + init_stock: 177 max_stock: 1000 - price: 540 - sale_gamma: 45 + price: 110 + sale_gamma: 59 service_level: 0.95 SKU370: - constraint: G(low_profit -> low_stock_constraint) - cost: 210 - init_stock: 588 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 112 + init_stock: 90 max_stock: 1000 - price: 308 - sale_gamma: 84 + price: 141 + sale_gamma: 15 service_level: 0.95 SKU371: constraint: null - cost: 404 - init_stock: 345 + cost: 328 + init_stock: 25 max_stock: 1000 - price: 719 - sale_gamma: 69 + price: 511 + sale_gamma: 5 service_level: 0.95 SKU372: - constraint: null - cost: 63 - init_stock: 553 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 67 + init_stock: 192 max_stock: 1000 - price: 120 - sale_gamma: 79 + price: 134 + sale_gamma: 32 service_level: 0.95 SKU373: - constraint: G(low_profit -> low_stock_constraint) - cost: 106 - init_stock: 48 + constraint: null + cost: 58 + init_stock: 268 max_stock: 1000 - price: 125 - sale_gamma: 24 + price: 91 + sale_gamma: 67 service_level: 0.95 SKU374: - constraint: null - cost: 306 - init_stock: 144 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 38 + init_stock: 216 max_stock: 1000 - price: 345 - sale_gamma: 36 + price: 73 + sale_gamma: 54 service_level: 0.95 SKU375: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 153 - init_stock: 255 + constraint: G(low_profit -> low_stock_constraint) + cost: 283 + init_stock: 432 max_stock: 1000 - price: 247 - sale_gamma: 51 + price: 416 + sale_gamma: 72 service_level: 0.95 SKU376: constraint: null - cost: 273 - init_stock: 120 + cost: 156 + init_stock: 164 max_stock: 1000 - price: 428 - sale_gamma: 15 + price: 291 + sale_gamma: 82 service_level: 0.95 SKU377: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 306 - init_stock: 300 + cost: 78 + init_stock: 536 max_stock: 1000 - price: 419 - sale_gamma: 75 + price: 134 + sale_gamma: 67 service_level: 0.95 SKU378: constraint: G(low_profit -> low_stock_constraint) - cost: 233 - init_stock: 220 + cost: 424 + init_stock: 175 max_stock: 1000 - price: 400 - sale_gamma: 44 + price: 546 + sale_gamma: 35 service_level: 0.95 SKU379: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 10 - init_stock: 176 + constraint: null + cost: 11 + init_stock: 196 max_stock: 1000 - price: 16 - sale_gamma: 22 + price: 20 + sale_gamma: 49 service_level: 0.95 SKU38: constraint: null - cost: 355 - init_stock: 126 + cost: 344 + init_stock: 258 max_stock: 1000 - price: 628 - sale_gamma: 63 + price: 567 + sale_gamma: 43 service_level: 0.95 SKU380: - constraint: G(stock_constraint) - cost: 440 - init_stock: 69 + constraint: null + cost: 393 + init_stock: 212 max_stock: 1000 - price: 858 - sale_gamma: 23 + price: 605 + sale_gamma: 53 service_level: 0.95 SKU381: constraint: null - cost: 402 - init_stock: 240 + cost: 476 + init_stock: 18 max_stock: 1000 - price: 727 - sale_gamma: 80 + price: 609 + sale_gamma: 6 service_level: 0.95 SKU382: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 478 - init_stock: 500 + constraint: null + cost: 125 + init_stock: 426 max_stock: 1000 - price: 932 - sale_gamma: 100 + price: 177 + sale_gamma: 71 service_level: 0.95 SKU383: constraint: null - cost: 248 - init_stock: 350 + cost: 304 + init_stock: 368 max_stock: 1000 - price: 342 - sale_gamma: 70 + price: 425 + sale_gamma: 92 service_level: 0.95 SKU384: constraint: null - cost: 338 - init_stock: 128 + cost: 320 + init_stock: 54 max_stock: 1000 - price: 486 - sale_gamma: 32 + price: 460 + sale_gamma: 9 service_level: 0.95 SKU385: - constraint: G(low_profit -> low_stock_constraint) - cost: 299 - init_stock: 427 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 120 + init_stock: 91 max_stock: 1000 - price: 424 - sale_gamma: 61 + price: 184 + sale_gamma: 13 service_level: 0.95 SKU386: constraint: G(stock_constraint) - cost: 281 - init_stock: 552 + cost: 295 + init_stock: 124 max_stock: 1000 - price: 368 - sale_gamma: 92 + price: 536 + sale_gamma: 31 service_level: 0.95 SKU387: - constraint: G(low_profit -> low_stock_constraint) - cost: 157 - init_stock: 470 + constraint: null + cost: 112 + init_stock: 582 max_stock: 1000 - price: 240 - sale_gamma: 94 + price: 154 + sale_gamma: 97 service_level: 0.95 SKU388: - constraint: null - cost: 214 - init_stock: 378 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 405 + init_stock: 264 max_stock: 1000 - price: 406 - sale_gamma: 54 + price: 635 + sale_gamma: 44 service_level: 0.95 SKU389: - constraint: null - cost: 129 - init_stock: 168 + constraint: G(stock_constraint) + cost: 376 + init_stock: 490 max_stock: 1000 - price: 221 - sale_gamma: 24 + price: 699 + sale_gamma: 70 service_level: 0.95 SKU39: - constraint: G(stock_constraint) - cost: 26 - init_stock: 49 + constraint: G(low_profit -> low_stock_constraint) + cost: 25 + init_stock: 204 max_stock: 1000 - price: 39 - sale_gamma: 7 + price: 45 + sale_gamma: 51 service_level: 0.95 SKU390: constraint: null - cost: 377 - init_stock: 475 + cost: 144 + init_stock: 156 max_stock: 1000 - price: 674 - sale_gamma: 95 + price: 223 + sale_gamma: 39 service_level: 0.95 SKU391: - constraint: null - cost: 180 - init_stock: 150 + constraint: G(stock_constraint) + cost: 233 + init_stock: 402 max_stock: 1000 - price: 352 - sale_gamma: 75 + price: 403 + sale_gamma: 67 service_level: 0.95 SKU392: constraint: null - cost: 106 - init_stock: 420 + cost: 163 + init_stock: 370 max_stock: 1000 - price: 151 - sale_gamma: 60 + price: 205 + sale_gamma: 74 service_level: 0.95 SKU393: - constraint: null - cost: 128 - init_stock: 260 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 402 max_stock: 1000 - price: 211 - sale_gamma: 52 + price: 701 + sale_gamma: 67 service_level: 0.95 SKU394: constraint: null - cost: 414 - init_stock: 240 + cost: 154 + init_stock: 318 max_stock: 1000 - price: 600 - sale_gamma: 60 + price: 252 + sale_gamma: 53 service_level: 0.95 SKU395: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 416 - init_stock: 345 + constraint: null + cost: 488 + init_stock: 198 max_stock: 1000 - price: 782 - sale_gamma: 69 + price: 854 + sale_gamma: 33 service_level: 0.95 SKU396: - constraint: G(low_profit -> low_stock_constraint) - cost: 426 - init_stock: 132 + constraint: null + cost: 333 + init_stock: 427 max_stock: 1000 - price: 579 - sale_gamma: 44 + price: 469 + sale_gamma: 61 service_level: 0.95 SKU397: - constraint: G(stock_constraint) - cost: 271 - init_stock: 60 + constraint: null + cost: 460 + init_stock: 153 max_stock: 1000 - price: 417 - sale_gamma: 15 + price: 791 + sale_gamma: 51 service_level: 0.95 SKU398: - constraint: null - cost: 21 - init_stock: 177 + constraint: G(stock_constraint) + cost: 234 + init_stock: 174 max_stock: 1000 - price: 35 - sale_gamma: 59 + price: 414 + sale_gamma: 58 service_level: 0.95 SKU399: - constraint: G(low_profit -> low_stock_constraint) - cost: 269 - init_stock: 196 + constraint: G(stock_constraint) + cost: 376 + init_stock: 148 max_stock: 1000 - price: 381 - sale_gamma: 49 + price: 612 + sale_gamma: 37 service_level: 0.95 SKU4: - constraint: null - cost: 190 - init_stock: 510 + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 21 max_stock: 1000 - price: 231 - sale_gamma: 85 + price: 798 + sale_gamma: 7 service_level: 0.95 SKU40: constraint: null - cost: 463 - init_stock: 352 + cost: 490 + init_stock: 594 max_stock: 1000 - price: 759 - sale_gamma: 88 + price: 563 + sale_gamma: 99 service_level: 0.95 SKU400: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 45 - init_stock: 106 + constraint: G(low_profit -> low_stock_constraint) + cost: 445 + init_stock: 420 max_stock: 1000 - price: 62 - sale_gamma: 53 + price: 729 + sale_gamma: 84 service_level: 0.95 SKU401: - constraint: G(stock_constraint) - cost: 149 - init_stock: 328 + constraint: G(low_profit -> low_stock_constraint) + cost: 410 + init_stock: 312 max_stock: 1000 - price: 177 - sale_gamma: 82 + price: 774 + sale_gamma: 78 service_level: 0.95 SKU402: - constraint: null - cost: 396 - init_stock: 158 + constraint: G(low_profit -> low_stock_constraint) + cost: 86 + init_stock: 35 max_stock: 1000 - price: 506 - sale_gamma: 79 + price: 153 + sale_gamma: 7 service_level: 0.95 SKU403: - constraint: G(low_profit -> low_stock_constraint) - cost: 70 - init_stock: 116 + constraint: null + cost: 89 + init_stock: 594 max_stock: 1000 price: 133 - sale_gamma: 58 + sale_gamma: 99 service_level: 0.95 SKU404: constraint: null - cost: 225 - init_stock: 395 + cost: 287 + init_stock: 305 max_stock: 1000 - price: 342 - sale_gamma: 79 + price: 522 + sale_gamma: 61 service_level: 0.95 SKU405: - constraint: null - cost: 31 - init_stock: 132 + constraint: G(stock_constraint) + cost: 460 + init_stock: 114 max_stock: 1000 - price: 57 - sale_gamma: 22 + price: 584 + sale_gamma: 19 service_level: 0.95 SKU406: constraint: null - cost: 449 - init_stock: 86 + cost: 327 + init_stock: 400 max_stock: 1000 - price: 727 - sale_gamma: 43 + price: 405 + sale_gamma: 100 service_level: 0.95 SKU407: constraint: null - cost: 462 - init_stock: 96 + cost: 26 + init_stock: 184 max_stock: 1000 - price: 609 - sale_gamma: 16 + price: 41 + sale_gamma: 46 service_level: 0.95 SKU408: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 364 - init_stock: 162 + constraint: null + cost: 444 + init_stock: 48 max_stock: 1000 - price: 484 - sale_gamma: 27 + price: 754 + sale_gamma: 8 service_level: 0.95 SKU409: - constraint: null - cost: 424 - init_stock: 174 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 257 + init_stock: 273 max_stock: 1000 - price: 644 - sale_gamma: 58 + price: 449 + sale_gamma: 91 service_level: 0.95 SKU41: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 56 - init_stock: 138 + cost: 141 + init_stock: 553 max_stock: 1000 - price: 88 - sale_gamma: 23 + price: 162 + sale_gamma: 79 service_level: 0.95 SKU410: - constraint: null - cost: 204 - init_stock: 27 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 70 + init_stock: 32 max_stock: 1000 - price: 326 - sale_gamma: 9 + price: 88 + sale_gamma: 16 service_level: 0.95 SKU411: - constraint: null - cost: 197 - init_stock: 42 + constraint: G(stock_constraint) + cost: 210 + init_stock: 380 max_stock: 1000 - price: 269 - sale_gamma: 6 + price: 405 + sale_gamma: 95 service_level: 0.95 SKU412: - constraint: G(stock_constraint) - cost: 140 - init_stock: 65 + constraint: null + cost: 286 + init_stock: 248 max_stock: 1000 - price: 245 - sale_gamma: 13 + price: 414 + sale_gamma: 62 service_level: 0.95 SKU413: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 11 - init_stock: 284 + constraint: null + cost: 403 + init_stock: 581 max_stock: 1000 - price: 21 - sale_gamma: 71 + price: 801 + sale_gamma: 83 service_level: 0.95 SKU414: - constraint: null - cost: 492 - init_stock: 80 + constraint: G(stock_constraint) + cost: 165 + init_stock: 435 max_stock: 1000 - price: 546 - sale_gamma: 20 + price: 229 + sale_gamma: 87 service_level: 0.95 SKU415: constraint: G(stock_constraint) - cost: 26 - init_stock: 126 + cost: 291 + init_stock: 184 max_stock: 1000 - price: 41 - sale_gamma: 63 + price: 372 + sale_gamma: 23 service_level: 0.95 SKU416: - constraint: G(low_profit -> low_stock_constraint) - cost: 151 - init_stock: 30 + constraint: null + cost: 228 + init_stock: 36 max_stock: 1000 - price: 279 - sale_gamma: 5 + price: 373 + sale_gamma: 9 service_level: 0.95 SKU417: constraint: G(low_profit -> low_stock_constraint) - cost: 306 - init_stock: 228 + cost: 443 + init_stock: 288 max_stock: 1000 - price: 406 - sale_gamma: 57 + price: 872 + sale_gamma: 72 service_level: 0.95 SKU418: - constraint: G(stock_constraint) - cost: 154 - init_stock: 210 + constraint: null + cost: 458 + init_stock: 52 max_stock: 1000 - price: 281 - sale_gamma: 42 + price: 838 + sale_gamma: 13 service_level: 0.95 SKU419: - constraint: G(low_profit -> low_stock_constraint) - cost: 192 - init_stock: 96 + constraint: null + cost: 303 + init_stock: 712 max_stock: 1000 - price: 253 - sale_gamma: 24 + price: 448 + sale_gamma: 89 service_level: 0.95 SKU42: constraint: null - cost: 106 - init_stock: 246 + cost: 462 + init_stock: 156 max_stock: 1000 - price: 119 - sale_gamma: 41 + price: 688 + sale_gamma: 26 service_level: 0.95 SKU420: constraint: null - cost: 79 - init_stock: 511 + cost: 268 + init_stock: 84 max_stock: 1000 - price: 96 - sale_gamma: 73 + price: 506 + sale_gamma: 42 service_level: 0.95 SKU421: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 288 - init_stock: 128 + constraint: G(stock_constraint) + cost: 214 + init_stock: 138 max_stock: 1000 - price: 391 - sale_gamma: 64 + price: 314 + sale_gamma: 46 service_level: 0.95 SKU422: - constraint: null - cost: 81 - init_stock: 497 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 432 max_stock: 1000 - price: 106 - sale_gamma: 71 + price: 97 + sale_gamma: 54 service_level: 0.95 SKU423: constraint: G(low_profit -> low_stock_constraint) - cost: 48 - init_stock: 316 + cost: 188 + init_stock: 396 max_stock: 1000 - price: 63 - sale_gamma: 79 + price: 265 + sale_gamma: 66 service_level: 0.95 SKU424: - constraint: null - cost: 344 - init_stock: 192 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 66 max_stock: 1000 - price: 550 - sale_gamma: 32 + price: 317 + sale_gamma: 11 service_level: 0.95 SKU425: constraint: G(stock_constraint) - cost: 155 - init_stock: 182 + cost: 162 + init_stock: 72 max_stock: 1000 - price: 192 - sale_gamma: 26 + price: 277 + sale_gamma: 12 service_level: 0.95 SKU426: - constraint: null - cost: 115 - init_stock: 410 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 125 + init_stock: 588 max_stock: 1000 - price: 181 - sale_gamma: 82 + price: 246 + sale_gamma: 98 service_level: 0.95 SKU427: - constraint: null - cost: 132 - init_stock: 10 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 413 + init_stock: 282 max_stock: 1000 - price: 182 - sale_gamma: 5 + price: 710 + sale_gamma: 94 service_level: 0.95 SKU428: constraint: G(stock_constraint) - cost: 499 - init_stock: 174 + cost: 391 + init_stock: 378 max_stock: 1000 - price: 963 - sale_gamma: 58 + price: 629 + sale_gamma: 63 service_level: 0.95 SKU429: - constraint: null - cost: 147 - init_stock: 64 + constraint: G(stock_constraint) + cost: 39 + init_stock: 246 max_stock: 1000 - price: 252 - sale_gamma: 16 + price: 73 + sale_gamma: 41 service_level: 0.95 SKU43: - constraint: null - cost: 162 - init_stock: 648 + constraint: G(stock_constraint) + cost: 217 + init_stock: 272 max_stock: 1000 - price: 260 - sale_gamma: 81 + price: 353 + sale_gamma: 68 service_level: 0.95 SKU430: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 330 - init_stock: 145 + constraint: null + cost: 216 + init_stock: 511 max_stock: 1000 - price: 508 - sale_gamma: 29 + price: 313 + sale_gamma: 73 service_level: 0.95 SKU431: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 65 - init_stock: 207 + cost: 328 + init_stock: 126 max_stock: 1000 - price: 79 - sale_gamma: 69 + price: 646 + sale_gamma: 21 service_level: 0.95 SKU432: - constraint: null - cost: 195 - init_stock: 372 + constraint: G(stock_constraint) + cost: 25 + init_stock: 368 max_stock: 1000 - price: 323 - sale_gamma: 93 + price: 35 + sale_gamma: 46 service_level: 0.95 SKU433: - constraint: G(low_profit -> low_stock_constraint) - cost: 55 - init_stock: 516 + constraint: null + cost: 348 + init_stock: 270 max_stock: 1000 - price: 73 - sale_gamma: 86 + price: 396 + sale_gamma: 45 service_level: 0.95 SKU434: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 113 - init_stock: 525 + cost: 37 + init_stock: 90 max_stock: 1000 - price: 198 - sale_gamma: 75 + price: 40 + sale_gamma: 30 service_level: 0.95 SKU435: - constraint: null - cost: 256 - init_stock: 75 + constraint: G(stock_constraint) + cost: 272 + init_stock: 210 max_stock: 1000 - price: 337 - sale_gamma: 15 + price: 320 + sale_gamma: 30 service_level: 0.95 SKU436: constraint: null - cost: 61 - init_stock: 784 + cost: 72 + init_stock: 405 max_stock: 1000 - price: 70 - sale_gamma: 98 + price: 105 + sale_gamma: 81 service_level: 0.95 SKU437: constraint: G(stock_constraint) - cost: 367 - init_stock: 294 + cost: 115 + init_stock: 84 max_stock: 1000 - price: 451 - sale_gamma: 42 + price: 223 + sale_gamma: 14 service_level: 0.95 SKU438: - constraint: G(stock_constraint) - cost: 396 - init_stock: 504 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 40 max_stock: 1000 - price: 570 - sale_gamma: 72 + price: 190 + sale_gamma: 10 service_level: 0.95 SKU439: - constraint: null - cost: 99 - init_stock: 434 + constraint: G(stock_constraint) + cost: 256 + init_stock: 190 max_stock: 1000 - price: 187 - sale_gamma: 62 + price: 304 + sale_gamma: 38 service_level: 0.95 SKU44: - constraint: G(low_profit -> low_stock_constraint) - cost: 241 - init_stock: 236 + constraint: null + cost: 360 + init_stock: 240 max_stock: 1000 - price: 443 - sale_gamma: 59 + price: 669 + sale_gamma: 48 service_level: 0.95 SKU440: - constraint: null - cost: 455 - init_stock: 102 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 248 + init_stock: 246 max_stock: 1000 - price: 500 - sale_gamma: 51 + price: 357 + sale_gamma: 82 service_level: 0.95 SKU441: constraint: null - cost: 217 - init_stock: 260 + cost: 253 + init_stock: 224 max_stock: 1000 - price: 292 - sale_gamma: 65 + price: 374 + sale_gamma: 56 service_level: 0.95 SKU442: constraint: null - cost: 460 - init_stock: 116 + cost: 470 + init_stock: 352 max_stock: 1000 - price: 782 - sale_gamma: 58 + price: 629 + sale_gamma: 88 service_level: 0.95 SKU443: - constraint: G(stock_constraint) - cost: 375 - init_stock: 144 + constraint: null + cost: 218 + init_stock: 365 max_stock: 1000 - price: 506 - sale_gamma: 48 + price: 361 + sale_gamma: 73 service_level: 0.95 SKU444: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 44 - init_stock: 490 + constraint: null + cost: 156 + init_stock: 18 max_stock: 1000 - price: 80 - sale_gamma: 98 + price: 182 + sale_gamma: 6 service_level: 0.95 SKU445: - constraint: G(stock_constraint) - cost: 59 - init_stock: 340 + constraint: null + cost: 260 + init_stock: 602 max_stock: 1000 - price: 109 - sale_gamma: 68 + price: 296 + sale_gamma: 86 service_level: 0.95 SKU446: constraint: null - cost: 183 - init_stock: 140 + cost: 497 + init_stock: 147 max_stock: 1000 - price: 223 - sale_gamma: 20 + price: 690 + sale_gamma: 49 service_level: 0.95 SKU447: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 192 + constraint: null + cost: 196 + init_stock: 30 max_stock: 1000 - price: 737 - sale_gamma: 48 + price: 280 + sale_gamma: 5 service_level: 0.95 SKU448: constraint: null - cost: 499 - init_stock: 140 + cost: 485 + init_stock: 497 max_stock: 1000 - price: 813 - sale_gamma: 28 + price: 742 + sale_gamma: 71 service_level: 0.95 SKU449: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 592 + constraint: null + cost: 282 + init_stock: 114 max_stock: 1000 - price: 619 - sale_gamma: 74 + price: 360 + sale_gamma: 38 service_level: 0.95 SKU45: - constraint: null - cost: 134 - init_stock: 288 + constraint: G(stock_constraint) + cost: 253 + init_stock: 30 max_stock: 1000 - price: 247 - sale_gamma: 48 + price: 351 + sale_gamma: 10 service_level: 0.95 SKU450: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 217 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 58 + init_stock: 294 max_stock: 1000 - price: 520 - sale_gamma: 31 + price: 77 + sale_gamma: 98 service_level: 0.95 SKU451: constraint: null - cost: 323 - init_stock: 34 + cost: 468 + init_stock: 483 max_stock: 1000 - price: 516 - sale_gamma: 17 + price: 851 + sale_gamma: 69 service_level: 0.95 SKU452: - constraint: null - cost: 52 - init_stock: 280 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 63 + init_stock: 395 max_stock: 1000 price: 97 - sale_gamma: 40 + sale_gamma: 79 service_level: 0.95 SKU453: - constraint: G(low_profit -> low_stock_constraint) - cost: 21 - init_stock: 486 + constraint: null + cost: 37 + init_stock: 140 max_stock: 1000 - price: 30 - sale_gamma: 81 + price: 65 + sale_gamma: 35 service_level: 0.95 SKU454: - constraint: null - cost: 198 - init_stock: 161 + constraint: G(low_profit -> low_stock_constraint) + cost: 494 + init_stock: 340 max_stock: 1000 - price: 285 - sale_gamma: 23 + price: 899 + sale_gamma: 85 service_level: 0.95 SKU455: - constraint: null - cost: 11 - init_stock: 256 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 136 + init_stock: 182 max_stock: 1000 - price: 21 - sale_gamma: 64 + price: 195 + sale_gamma: 26 service_level: 0.95 SKU456: - constraint: G(low_profit -> low_stock_constraint) - cost: 204 - init_stock: 498 + constraint: G(stock_constraint) + cost: 338 + init_stock: 75 max_stock: 1000 - price: 267 - sale_gamma: 83 + price: 659 + sale_gamma: 25 service_level: 0.95 SKU457: constraint: null - cost: 194 - init_stock: 700 + cost: 169 + init_stock: 256 max_stock: 1000 - price: 322 - sale_gamma: 100 + price: 190 + sale_gamma: 64 service_level: 0.95 SKU458: - constraint: G(stock_constraint) - cost: 110 - init_stock: 44 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 403 + init_stock: 28 max_stock: 1000 - price: 180 - sale_gamma: 11 + price: 685 + sale_gamma: 7 service_level: 0.95 SKU459: constraint: null - cost: 370 - init_stock: 256 + cost: 425 + init_stock: 272 max_stock: 1000 - price: 573 - sale_gamma: 64 + price: 731 + sale_gamma: 34 service_level: 0.95 SKU46: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 90 - init_stock: 396 + constraint: G(low_profit -> low_stock_constraint) + cost: 299 + init_stock: 200 max_stock: 1000 - price: 99 - sale_gamma: 99 + price: 409 + sale_gamma: 50 service_level: 0.95 SKU460: - constraint: null - cost: 121 - init_stock: 243 + constraint: G(low_profit -> low_stock_constraint) + cost: 405 + init_stock: 292 max_stock: 1000 - price: 196 - sale_gamma: 81 + price: 558 + sale_gamma: 73 service_level: 0.95 SKU461: - constraint: null - cost: 305 - init_stock: 154 + constraint: G(stock_constraint) + cost: 251 + init_stock: 240 max_stock: 1000 - price: 582 - sale_gamma: 22 + price: 326 + sale_gamma: 48 service_level: 0.95 SKU462: - constraint: null - cost: 377 - init_stock: 396 + constraint: G(low_profit -> low_stock_constraint) + cost: 448 + init_stock: 36 max_stock: 1000 - price: 524 - sale_gamma: 66 + price: 757 + sale_gamma: 9 service_level: 0.95 SKU463: constraint: null - cost: 449 - init_stock: 32 + cost: 187 + init_stock: 574 max_stock: 1000 - price: 502 - sale_gamma: 8 + price: 213 + sale_gamma: 82 service_level: 0.95 SKU464: - constraint: G(low_profit -> low_stock_constraint) - cost: 466 - init_stock: 170 + constraint: null + cost: 279 + init_stock: 272 max_stock: 1000 - price: 685 + price: 438 sale_gamma: 34 service_level: 0.95 SKU465: - constraint: G(stock_constraint) - cost: 86 - init_stock: 122 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 147 + init_stock: 400 max_stock: 1000 - price: 143 - sale_gamma: 61 + price: 288 + sale_gamma: 100 service_level: 0.95 SKU466: - constraint: null - cost: 412 - init_stock: 282 + constraint: G(stock_constraint) + cost: 97 + init_stock: 126 max_stock: 1000 - price: 675 - sale_gamma: 94 + price: 167 + sale_gamma: 42 service_level: 0.95 SKU467: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 103 - init_stock: 426 + constraint: G(stock_constraint) + cost: 142 + init_stock: 252 max_stock: 1000 - price: 141 - sale_gamma: 71 + price: 230 + sale_gamma: 36 service_level: 0.95 SKU468: - constraint: null - cost: 453 - init_stock: 440 + constraint: G(stock_constraint) + cost: 55 + init_stock: 250 max_stock: 1000 - price: 874 - sale_gamma: 88 + price: 104 + sale_gamma: 50 service_level: 0.95 SKU469: - constraint: null - cost: 422 - init_stock: 186 + constraint: G(stock_constraint) + cost: 178 + init_stock: 105 max_stock: 1000 - price: 772 - sale_gamma: 93 + price: 331 + sale_gamma: 15 service_level: 0.95 SKU47: constraint: G(stock_constraint) - cost: 51 - init_stock: 111 + cost: 121 + init_stock: 117 max_stock: 1000 - price: 79 - sale_gamma: 37 + price: 240 + sale_gamma: 39 service_level: 0.95 SKU470: - constraint: null - cost: 284 - init_stock: 168 + constraint: G(low_profit -> low_stock_constraint) + cost: 373 + init_stock: 128 max_stock: 1000 - price: 462 - sale_gamma: 28 + price: 548 + sale_gamma: 32 service_level: 0.95 SKU471: - constraint: null - cost: 487 - init_stock: 138 + constraint: G(low_profit -> low_stock_constraint) + cost: 315 + init_stock: 568 max_stock: 1000 - price: 706 - sale_gamma: 23 + price: 387 + sale_gamma: 71 service_level: 0.95 SKU472: - constraint: G(stock_constraint) - cost: 465 - init_stock: 480 + constraint: null + cost: 421 + init_stock: 177 max_stock: 1000 - price: 692 - sale_gamma: 96 + price: 627 + sale_gamma: 59 service_level: 0.95 SKU473: - constraint: null - cost: 271 - init_stock: 156 + constraint: G(low_profit -> low_stock_constraint) + cost: 195 + init_stock: 105 max_stock: 1000 - price: 349 - sale_gamma: 26 + price: 323 + sale_gamma: 21 service_level: 0.95 SKU474: constraint: null - cost: 68 - init_stock: 100 + cost: 448 + init_stock: 602 max_stock: 1000 - price: 112 - sale_gamma: 25 + price: 775 + sale_gamma: 86 service_level: 0.95 SKU475: - constraint: G(low_profit -> low_stock_constraint) - cost: 320 - init_stock: 144 + constraint: null + cost: 34 + init_stock: 282 max_stock: 1000 - price: 534 - sale_gamma: 72 + price: 62 + sale_gamma: 94 service_level: 0.95 SKU476: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 237 - init_stock: 196 + constraint: null + cost: 251 + init_stock: 180 max_stock: 1000 - price: 374 - sale_gamma: 49 + price: 361 + sale_gamma: 90 service_level: 0.95 SKU477: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 455 - init_stock: 276 + constraint: null + cost: 289 + init_stock: 329 max_stock: 1000 - price: 514 - sale_gamma: 69 + price: 338 + sale_gamma: 47 service_level: 0.95 SKU478: constraint: null - cost: 40 - init_stock: 518 + cost: 479 + init_stock: 352 max_stock: 1000 - price: 71 - sale_gamma: 74 + price: 723 + sale_gamma: 88 service_level: 0.95 SKU479: - constraint: null - cost: 295 - init_stock: 258 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 366 + init_stock: 152 max_stock: 1000 - price: 516 - sale_gamma: 43 + price: 552 + sale_gamma: 38 service_level: 0.95 SKU48: constraint: null - cost: 395 - init_stock: 637 + cost: 340 + init_stock: 232 max_stock: 1000 - price: 726 - sale_gamma: 91 + price: 387 + sale_gamma: 58 service_level: 0.95 SKU480: constraint: G(stock_constraint) - cost: 94 - init_stock: 130 + cost: 18 + init_stock: 180 max_stock: 1000 - price: 129 - sale_gamma: 65 + price: 21 + sale_gamma: 30 service_level: 0.95 SKU481: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 408 - init_stock: 84 + constraint: null + cost: 330 + init_stock: 352 max_stock: 1000 - price: 799 - sale_gamma: 21 + price: 422 + sale_gamma: 88 service_level: 0.95 SKU482: constraint: G(stock_constraint) - cost: 198 - init_stock: 435 + cost: 94 + init_stock: 696 max_stock: 1000 - price: 374 + price: 108 sale_gamma: 87 service_level: 0.95 SKU483: - constraint: G(stock_constraint) - cost: 85 - init_stock: 574 + constraint: null + cost: 298 + init_stock: 170 max_stock: 1000 - price: 142 - sale_gamma: 82 + price: 533 + sale_gamma: 34 service_level: 0.95 SKU484: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 460 - init_stock: 336 + constraint: null + cost: 84 + init_stock: 365 max_stock: 1000 - price: 653 - sale_gamma: 42 + price: 117 + sale_gamma: 73 service_level: 0.95 SKU485: constraint: null - cost: 308 - init_stock: 280 + cost: 318 + init_stock: 536 max_stock: 1000 - price: 344 - sale_gamma: 35 + price: 543 + sale_gamma: 67 service_level: 0.95 SKU486: - constraint: null - cost: 84 - init_stock: 305 + constraint: G(low_profit -> low_stock_constraint) + cost: 122 + init_stock: 208 max_stock: 1000 - price: 119 - sale_gamma: 61 + price: 161 + sale_gamma: 52 service_level: 0.95 SKU487: constraint: null - cost: 421 - init_stock: 310 + cost: 277 + init_stock: 264 max_stock: 1000 - price: 568 - sale_gamma: 62 + price: 362 + sale_gamma: 66 service_level: 0.95 SKU488: - constraint: null - cost: 108 - init_stock: 145 + constraint: G(low_profit -> low_stock_constraint) + cost: 36 + init_stock: 189 max_stock: 1000 - price: 133 - sale_gamma: 29 + price: 42 + sale_gamma: 27 service_level: 0.95 SKU489: - constraint: G(low_profit -> low_stock_constraint) - cost: 146 - init_stock: 180 + constraint: null + cost: 59 + init_stock: 124 max_stock: 1000 - price: 290 - sale_gamma: 30 + price: 97 + sale_gamma: 31 service_level: 0.95 SKU49: constraint: null - cost: 320 - init_stock: 308 + cost: 263 + init_stock: 760 max_stock: 1000 - price: 521 - sale_gamma: 44 + price: 515 + sale_gamma: 95 service_level: 0.95 SKU490: constraint: null - cost: 43 - init_stock: 95 + cost: 161 + init_stock: 486 max_stock: 1000 - price: 80 - sale_gamma: 19 + price: 264 + sale_gamma: 81 service_level: 0.95 SKU491: constraint: null - cost: 134 - init_stock: 267 + cost: 444 + init_stock: 450 max_stock: 1000 - price: 159 - sale_gamma: 89 + price: 834 + sale_gamma: 75 service_level: 0.95 SKU492: constraint: null - cost: 370 - init_stock: 344 + cost: 53 + init_stock: 240 max_stock: 1000 - price: 717 - sale_gamma: 86 + price: 63 + sale_gamma: 80 service_level: 0.95 SKU493: constraint: null - cost: 344 - init_stock: 80 + cost: 461 + init_stock: 81 max_stock: 1000 - price: 540 - sale_gamma: 20 + price: 567 + sale_gamma: 27 service_level: 0.95 SKU494: constraint: null - cost: 87 - init_stock: 245 + cost: 145 + init_stock: 282 max_stock: 1000 - price: 153 - sale_gamma: 49 + price: 242 + sale_gamma: 94 service_level: 0.95 SKU495: constraint: G(stock_constraint) - cost: 183 - init_stock: 680 + cost: 149 + init_stock: 372 max_stock: 1000 - price: 279 - sale_gamma: 85 + price: 217 + sale_gamma: 62 service_level: 0.95 SKU496: - constraint: null - cost: 222 - init_stock: 512 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 342 + init_stock: 295 max_stock: 1000 - price: 346 - sale_gamma: 64 + price: 492 + sale_gamma: 59 service_level: 0.95 SKU497: - constraint: null - cost: 453 - init_stock: 180 + constraint: G(low_profit -> low_stock_constraint) + cost: 33 + init_stock: 45 max_stock: 1000 - price: 530 - sale_gamma: 36 + price: 42 + sale_gamma: 9 service_level: 0.95 SKU498: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 475 - init_stock: 175 + constraint: null + cost: 305 + init_stock: 325 max_stock: 1000 - price: 935 - sale_gamma: 25 + price: 439 + sale_gamma: 65 service_level: 0.95 SKU499: constraint: G(stock_constraint) - cost: 122 - init_stock: 99 + cost: 307 + init_stock: 174 max_stock: 1000 - price: 167 - sale_gamma: 33 + price: 472 + sale_gamma: 29 service_level: 0.95 SKU5: - constraint: null - cost: 121 - init_stock: 380 + constraint: G(stock_constraint) + cost: 466 + init_stock: 426 max_stock: 1000 - price: 199 - sale_gamma: 76 + price: 852 + sale_gamma: 71 service_level: 0.95 SKU50: constraint: null - cost: 298 - init_stock: 228 + cost: 282 + init_stock: 185 max_stock: 1000 - price: 563 - sale_gamma: 57 + price: 516 + sale_gamma: 37 service_level: 0.95 SKU500: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 127 - init_stock: 261 + constraint: null + cost: 208 + init_stock: 336 max_stock: 1000 - price: 228 - sale_gamma: 87 + price: 255 + sale_gamma: 42 service_level: 0.95 SKU501: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 328 - init_stock: 664 + constraint: null + cost: 194 + init_stock: 152 max_stock: 1000 - price: 514 - sale_gamma: 83 + price: 265 + sale_gamma: 76 service_level: 0.95 SKU502: constraint: null - cost: 335 - init_stock: 160 + cost: 69 + init_stock: 390 max_stock: 1000 - price: 525 - sale_gamma: 32 + price: 101 + sale_gamma: 65 service_level: 0.95 SKU503: - constraint: G(low_profit -> low_stock_constraint) - cost: 86 - init_stock: 36 + constraint: G(stock_constraint) + cost: 208 + init_stock: 228 max_stock: 1000 - price: 103 - sale_gamma: 6 + price: 280 + sale_gamma: 57 service_level: 0.95 SKU504: constraint: null - cost: 14 - init_stock: 260 + cost: 251 + init_stock: 210 max_stock: 1000 - price: 19 - sale_gamma: 65 + price: 426 + sale_gamma: 30 service_level: 0.95 SKU505: - constraint: G(stock_constraint) - cost: 74 - init_stock: 553 + constraint: null + cost: 426 + init_stock: 295 max_stock: 1000 - price: 118 - sale_gamma: 79 + price: 515 + sale_gamma: 59 service_level: 0.95 SKU506: constraint: null - cost: 51 - init_stock: 250 + cost: 149 + init_stock: 465 max_stock: 1000 - price: 57 - sale_gamma: 50 + price: 220 + sale_gamma: 93 service_level: 0.95 SKU507: - constraint: null - cost: 37 - init_stock: 34 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 187 + init_stock: 60 max_stock: 1000 - price: 55 - sale_gamma: 17 + price: 293 + sale_gamma: 15 service_level: 0.95 SKU508: - constraint: null - cost: 445 - init_stock: 285 + constraint: G(low_profit -> low_stock_constraint) + cost: 272 + init_stock: 576 max_stock: 1000 - price: 752 - sale_gamma: 95 + price: 432 + sale_gamma: 96 service_level: 0.95 SKU509: - constraint: null - cost: 314 - init_stock: 70 + constraint: G(low_profit -> low_stock_constraint) + cost: 297 + init_stock: 486 max_stock: 1000 - price: 536 - sale_gamma: 14 + price: 397 + sale_gamma: 81 service_level: 0.95 SKU51: - constraint: null - cost: 329 - init_stock: 99 + constraint: G(low_profit -> low_stock_constraint) + cost: 295 + init_stock: 469 max_stock: 1000 - price: 539 - sale_gamma: 33 + price: 477 + sale_gamma: 67 service_level: 0.95 SKU510: - constraint: null - cost: 230 - init_stock: 310 + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 36 max_stock: 1000 - price: 370 - sale_gamma: 62 + price: 156 + sale_gamma: 9 service_level: 0.95 SKU511: - constraint: G(low_profit -> low_stock_constraint) - cost: 21 - init_stock: 136 + constraint: G(stock_constraint) + cost: 361 + init_stock: 450 max_stock: 1000 - price: 41 - sale_gamma: 34 + price: 480 + sale_gamma: 75 service_level: 0.95 SKU512: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 424 + constraint: null + cost: 467 init_stock: 132 max_stock: 1000 - price: 496 - sale_gamma: 33 + price: 854 + sale_gamma: 22 service_level: 0.95 SKU513: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 27 - init_stock: 12 + cost: 343 + init_stock: 42 max_stock: 1000 - price: 46 - sale_gamma: 6 + price: 631 + sale_gamma: 7 service_level: 0.95 SKU514: constraint: null - cost: 78 - init_stock: 357 + cost: 186 + init_stock: 504 max_stock: 1000 - price: 131 - sale_gamma: 51 + price: 332 + sale_gamma: 63 service_level: 0.95 SKU515: constraint: null - cost: 490 - init_stock: 736 + cost: 145 + init_stock: 216 max_stock: 1000 - price: 651 - sale_gamma: 92 + price: 227 + sale_gamma: 54 service_level: 0.95 SKU516: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 103 - init_stock: 90 + constraint: G(stock_constraint) + cost: 64 + init_stock: 114 max_stock: 1000 - price: 134 - sale_gamma: 15 + price: 96 + sale_gamma: 38 service_level: 0.95 SKU517: constraint: null - cost: 198 - init_stock: 123 + cost: 117 + init_stock: 45 max_stock: 1000 - price: 358 - sale_gamma: 41 + price: 168 + sale_gamma: 9 service_level: 0.95 SKU518: - constraint: null - cost: 492 - init_stock: 60 + constraint: G(stock_constraint) + cost: 26 + init_stock: 147 max_stock: 1000 - price: 551 - sale_gamma: 15 + price: 30 + sale_gamma: 21 service_level: 0.95 SKU519: - constraint: G(low_profit -> low_stock_constraint) - cost: 490 - init_stock: 156 + constraint: null + cost: 233 + init_stock: 250 max_stock: 1000 - price: 553 - sale_gamma: 39 + price: 410 + sale_gamma: 50 service_level: 0.95 SKU52: constraint: null - cost: 183 - init_stock: 378 + cost: 22 + init_stock: 402 max_stock: 1000 - price: 278 - sale_gamma: 63 - service_level: 0.95 - SKU520: - constraint: null - cost: 400 - init_stock: 602 + price: 28 + sale_gamma: 67 + service_level: 0.95 + SKU520: + constraint: G(low_profit -> low_stock_constraint) + cost: 209 + init_stock: 44 max_stock: 1000 - price: 456 - sale_gamma: 86 + price: 248 + sale_gamma: 22 service_level: 0.95 SKU521: - constraint: null - cost: 212 - init_stock: 310 + constraint: G(low_profit -> low_stock_constraint) + cost: 220 + init_stock: 350 max_stock: 1000 - price: 250 - sale_gamma: 62 + price: 330 + sale_gamma: 50 service_level: 0.95 SKU522: constraint: null - cost: 328 - init_stock: 195 + cost: 156 + init_stock: 522 max_stock: 1000 - price: 472 - sale_gamma: 39 + price: 277 + sale_gamma: 87 service_level: 0.95 SKU523: - constraint: G(low_profit -> low_stock_constraint) - cost: 23 - init_stock: 200 + constraint: null + cost: 65 + init_stock: 500 max_stock: 1000 - price: 31 - sale_gamma: 50 + price: 107 + sale_gamma: 100 service_level: 0.95 SKU524: constraint: null - cost: 92 - init_stock: 450 + cost: 294 + init_stock: 188 max_stock: 1000 - price: 184 - sale_gamma: 75 + price: 464 + sale_gamma: 94 service_level: 0.95 SKU525: - constraint: G(low_profit -> low_stock_constraint) - cost: 261 - init_stock: 504 + constraint: G(stock_constraint) + cost: 432 + init_stock: 126 max_stock: 1000 - price: 409 - sale_gamma: 84 + price: 751 + sale_gamma: 42 service_level: 0.95 SKU526: constraint: null - cost: 126 - init_stock: 148 + cost: 419 + init_stock: 168 max_stock: 1000 - price: 228 - sale_gamma: 37 + price: 716 + sale_gamma: 24 service_level: 0.95 SKU527: constraint: null - cost: 42 - init_stock: 84 + cost: 131 + init_stock: 350 max_stock: 1000 - price: 78 - sale_gamma: 14 + price: 262 + sale_gamma: 70 service_level: 0.95 SKU528: - constraint: null - cost: 22 - init_stock: 630 + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 84 max_stock: 1000 - price: 33 - sale_gamma: 90 + price: 581 + sale_gamma: 21 service_level: 0.95 SKU529: constraint: G(low_profit -> low_stock_constraint) - cost: 271 - init_stock: 486 + cost: 298 + init_stock: 248 max_stock: 1000 - price: 523 - sale_gamma: 81 + price: 375 + sale_gamma: 31 service_level: 0.95 SKU53: - constraint: null - cost: 438 - init_stock: 231 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 151 + init_stock: 490 max_stock: 1000 - price: 586 - sale_gamma: 77 + price: 191 + sale_gamma: 70 service_level: 0.95 SKU530: - constraint: null - cost: 64 - init_stock: 325 + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 135 max_stock: 1000 - price: 112 - sale_gamma: 65 + price: 205 + sale_gamma: 45 service_level: 0.95 SKU531: constraint: null - cost: 273 - init_stock: 366 + cost: 103 + init_stock: 300 max_stock: 1000 - price: 505 - sale_gamma: 61 + price: 153 + sale_gamma: 60 service_level: 0.95 SKU532: - constraint: G(low_profit -> low_stock_constraint) - cost: 436 - init_stock: 468 + constraint: null + cost: 351 + init_stock: 539 max_stock: 1000 - price: 789 - sale_gamma: 78 + price: 431 + sale_gamma: 77 service_level: 0.95 SKU533: - constraint: null - cost: 405 - init_stock: 126 + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 168 max_stock: 1000 - price: 623 - sale_gamma: 21 + price: 340 + sale_gamma: 42 service_level: 0.95 SKU534: constraint: null - cost: 375 - init_stock: 415 + cost: 425 + init_stock: 82 max_stock: 1000 - price: 705 - sale_gamma: 83 + price: 548 + sale_gamma: 41 service_level: 0.95 SKU535: - constraint: G(low_profit -> low_stock_constraint) - cost: 423 - init_stock: 468 + constraint: null + cost: 304 + init_stock: 430 max_stock: 1000 - price: 778 - sale_gamma: 78 + price: 465 + sale_gamma: 86 service_level: 0.95 SKU536: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 116 + constraint: null + cost: 358 + init_stock: 208 max_stock: 1000 - price: 562 - sale_gamma: 58 + price: 415 + sale_gamma: 52 service_level: 0.95 SKU537: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 476 - init_stock: 145 + cost: 321 + init_stock: 45 max_stock: 1000 - price: 790 - sale_gamma: 29 + price: 455 + sale_gamma: 9 service_level: 0.95 SKU538: constraint: null - cost: 451 - init_stock: 115 + cost: 457 + init_stock: 200 max_stock: 1000 - price: 523 - sale_gamma: 23 + price: 868 + sale_gamma: 50 service_level: 0.95 SKU539: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 176 + constraint: G(stock_constraint) + cost: 165 + init_stock: 234 max_stock: 1000 - price: 513 - sale_gamma: 44 + price: 229 + sale_gamma: 78 service_level: 0.95 SKU54: - constraint: G(stock_constraint) - cost: 141 - init_stock: 295 + constraint: G(low_profit -> low_stock_constraint) + cost: 158 + init_stock: 138 max_stock: 1000 - price: 184 - sale_gamma: 59 + price: 241 + sale_gamma: 46 service_level: 0.95 SKU540: - constraint: G(stock_constraint) - cost: 25 - init_stock: 427 + constraint: null + cost: 388 + init_stock: 294 max_stock: 1000 - price: 29 - sale_gamma: 61 + price: 496 + sale_gamma: 42 service_level: 0.95 SKU541: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 230 - init_stock: 147 + constraint: null + cost: 131 + init_stock: 203 max_stock: 1000 - price: 397 - sale_gamma: 49 + price: 213 + sale_gamma: 29 service_level: 0.95 SKU542: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 314 - init_stock: 141 + cost: 38 + init_stock: 130 max_stock: 1000 - price: 348 - sale_gamma: 47 + price: 42 + sale_gamma: 26 service_level: 0.95 SKU543: - constraint: null - cost: 421 - init_stock: 744 + constraint: G(stock_constraint) + cost: 430 + init_stock: 510 max_stock: 1000 - price: 719 - sale_gamma: 93 + price: 718 + sale_gamma: 85 service_level: 0.95 SKU544: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 245 - init_stock: 56 + constraint: G(stock_constraint) + cost: 346 + init_stock: 194 max_stock: 1000 - price: 409 - sale_gamma: 14 + price: 474 + sale_gamma: 97 service_level: 0.95 SKU545: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 324 - init_stock: 180 + cost: 175 + init_stock: 75 max_stock: 1000 - price: 534 - sale_gamma: 30 + price: 323 + sale_gamma: 15 service_level: 0.95 SKU546: - constraint: G(stock_constraint) - cost: 244 - init_stock: 60 + constraint: null + cost: 491 + init_stock: 576 max_stock: 1000 - price: 373 - sale_gamma: 10 + price: 765 + sale_gamma: 96 service_level: 0.95 SKU547: - constraint: null - cost: 224 - init_stock: 108 + constraint: G(stock_constraint) + cost: 161 + init_stock: 264 max_stock: 1000 - price: 306 - sale_gamma: 18 + price: 251 + sale_gamma: 44 service_level: 0.95 SKU548: - constraint: null - cost: 181 - init_stock: 116 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 111 + init_stock: 36 max_stock: 1000 - price: 342 - sale_gamma: 29 + price: 217 + sale_gamma: 6 service_level: 0.95 SKU549: - constraint: null - cost: 380 - init_stock: 336 + constraint: G(stock_constraint) + cost: 387 + init_stock: 316 max_stock: 1000 - price: 703 - sale_gamma: 84 + price: 510 + sale_gamma: 79 service_level: 0.95 SKU55: - constraint: null - cost: 303 - init_stock: 178 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 482 + init_stock: 455 max_stock: 1000 - price: 415 - sale_gamma: 89 + price: 896 + sale_gamma: 65 service_level: 0.95 SKU550: - constraint: G(stock_constraint) - cost: 340 - init_stock: 132 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 259 + init_stock: 282 max_stock: 1000 - price: 452 - sale_gamma: 22 + price: 502 + sale_gamma: 94 service_level: 0.95 SKU551: constraint: null - cost: 455 - init_stock: 240 + cost: 46 + init_stock: 186 max_stock: 1000 - price: 518 - sale_gamma: 48 + price: 60 + sale_gamma: 31 service_level: 0.95 SKU552: constraint: null - cost: 242 + cost: 191 init_stock: 450 max_stock: 1000 - price: 404 - sale_gamma: 75 + price: 315 + sale_gamma: 90 service_level: 0.95 SKU553: - constraint: null - cost: 221 - init_stock: 230 + constraint: G(low_profit -> low_stock_constraint) + cost: 208 + init_stock: 60 max_stock: 1000 - price: 243 - sale_gamma: 46 + price: 251 + sale_gamma: 30 service_level: 0.95 SKU554: constraint: null - cost: 48 - init_stock: 195 + cost: 340 + init_stock: 82 max_stock: 1000 - price: 61 - sale_gamma: 65 + price: 387 + sale_gamma: 41 service_level: 0.95 SKU555: - constraint: null - cost: 208 - init_stock: 424 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 252 max_stock: 1000 - price: 266 - sale_gamma: 53 + price: 684 + sale_gamma: 84 service_level: 0.95 SKU556: - constraint: null - cost: 496 - init_stock: 368 + constraint: G(stock_constraint) + cost: 110 + init_stock: 90 max_stock: 1000 - price: 734 - sale_gamma: 46 + price: 213 + sale_gamma: 30 service_level: 0.95 SKU557: - constraint: null - cost: 275 - init_stock: 285 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 334 + init_stock: 365 max_stock: 1000 - price: 440 - sale_gamma: 95 + price: 661 + sale_gamma: 73 service_level: 0.95 SKU558: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 342 - init_stock: 219 + constraint: null + cost: 399 + init_stock: 85 max_stock: 1000 - price: 666 - sale_gamma: 73 + price: 490 + sale_gamma: 17 service_level: 0.95 SKU559: constraint: null - cost: 215 - init_stock: 576 + cost: 313 + init_stock: 270 max_stock: 1000 - price: 363 - sale_gamma: 96 + price: 591 + sale_gamma: 54 service_level: 0.95 SKU56: - constraint: G(low_profit -> low_stock_constraint) - cost: 446 - init_stock: 400 + constraint: null + cost: 377 + init_stock: 222 max_stock: 1000 - price: 570 - sale_gamma: 100 + price: 671 + sale_gamma: 74 service_level: 0.95 SKU560: - constraint: null - cost: 363 - init_stock: 30 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 283 + init_stock: 246 max_stock: 1000 - price: 660 - sale_gamma: 6 + price: 458 + sale_gamma: 41 service_level: 0.95 SKU561: constraint: null - cost: 158 - init_stock: 259 + cost: 202 + init_stock: 312 max_stock: 1000 - price: 267 - sale_gamma: 37 + price: 385 + sale_gamma: 39 service_level: 0.95 SKU562: constraint: null - cost: 260 - init_stock: 396 + cost: 347 + init_stock: 356 max_stock: 1000 - price: 387 - sale_gamma: 66 + price: 666 + sale_gamma: 89 service_level: 0.95 SKU563: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 465 - init_stock: 147 + cost: 401 + init_stock: 30 max_stock: 1000 - price: 799 - sale_gamma: 21 + price: 509 + sale_gamma: 5 service_level: 0.95 SKU564: constraint: null - cost: 356 - init_stock: 55 + cost: 109 + init_stock: 63 max_stock: 1000 - price: 505 - sale_gamma: 11 + price: 183 + sale_gamma: 9 service_level: 0.95 SKU565: constraint: null - cost: 479 - init_stock: 144 + cost: 57 + init_stock: 525 max_stock: 1000 - price: 637 - sale_gamma: 48 + price: 90 + sale_gamma: 75 service_level: 0.95 SKU566: - constraint: G(stock_constraint) - cost: 441 - init_stock: 483 + constraint: null + cost: 131 + init_stock: 44 max_stock: 1000 - price: 657 - sale_gamma: 69 + price: 165 + sale_gamma: 11 service_level: 0.95 SKU567: - constraint: null - cost: 88 - init_stock: 348 + constraint: G(stock_constraint) + cost: 361 + init_stock: 27 max_stock: 1000 - price: 169 - sale_gamma: 58 + price: 465 + sale_gamma: 9 service_level: 0.95 SKU568: constraint: null - cost: 210 - init_stock: 108 + cost: 75 + init_stock: 234 max_stock: 1000 - price: 289 - sale_gamma: 27 + price: 102 + sale_gamma: 78 service_level: 0.95 SKU569: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 252 + constraint: null + cost: 473 + init_stock: 192 max_stock: 1000 - price: 483 - sale_gamma: 42 + price: 591 + sale_gamma: 48 service_level: 0.95 SKU57: - constraint: null - cost: 262 - init_stock: 560 + constraint: G(stock_constraint) + cost: 359 + init_stock: 350 max_stock: 1000 - price: 463 - sale_gamma: 70 + price: 682 + sale_gamma: 50 service_level: 0.95 SKU570: constraint: null - cost: 143 - init_stock: 60 + cost: 388 + init_stock: 581 max_stock: 1000 - price: 158 - sale_gamma: 10 + price: 686 + sale_gamma: 83 service_level: 0.95 SKU571: - constraint: null - cost: 204 - init_stock: 128 + constraint: G(stock_constraint) + cost: 168 + init_stock: 78 max_stock: 1000 - price: 371 - sale_gamma: 64 + price: 208 + sale_gamma: 39 service_level: 0.95 SKU572: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 291 - init_stock: 171 + constraint: null + cost: 26 + init_stock: 225 max_stock: 1000 - price: 436 - sale_gamma: 57 + price: 41 + sale_gamma: 45 service_level: 0.95 SKU573: - constraint: G(stock_constraint) - cost: 458 - init_stock: 150 + constraint: null + cost: 246 + init_stock: 164 max_stock: 1000 - price: 622 - sale_gamma: 25 + price: 391 + sale_gamma: 41 service_level: 0.95 SKU574: constraint: G(low_profit -> low_stock_constraint) - cost: 243 - init_stock: 658 + cost: 94 + init_stock: 150 max_stock: 1000 - price: 340 - sale_gamma: 94 + price: 166 + sale_gamma: 50 service_level: 0.95 SKU575: - constraint: G(stock_constraint) - cost: 328 - init_stock: 432 + constraint: null + cost: 237 + init_stock: 237 max_stock: 1000 - price: 380 - sale_gamma: 72 + price: 438 + sale_gamma: 79 service_level: 0.95 SKU576: - constraint: null - cost: 333 - init_stock: 490 + constraint: G(stock_constraint) + cost: 265 + init_stock: 468 max_stock: 1000 - price: 579 - sale_gamma: 70 + price: 416 + sale_gamma: 78 service_level: 0.95 SKU577: - constraint: null - cost: 368 - init_stock: 90 + constraint: G(low_profit -> low_stock_constraint) + cost: 18 + init_stock: 348 max_stock: 1000 - price: 419 - sale_gamma: 15 + price: 24 + sale_gamma: 87 service_level: 0.95 SKU578: constraint: null - cost: 82 - init_stock: 360 + cost: 100 + init_stock: 284 max_stock: 1000 - price: 145 - sale_gamma: 60 + price: 148 + sale_gamma: 71 service_level: 0.95 SKU579: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 133 + constraint: null + cost: 415 + init_stock: 27 max_stock: 1000 - price: 714 - sale_gamma: 19 + price: 771 + sale_gamma: 9 service_level: 0.95 SKU58: - constraint: G(low_profit -> low_stock_constraint) - cost: 409 - init_stock: 536 + constraint: null + cost: 362 + init_stock: 240 max_stock: 1000 - price: 449 - sale_gamma: 67 + price: 521 + sale_gamma: 48 service_level: 0.95 SKU580: - constraint: G(low_profit -> low_stock_constraint) - cost: 421 - init_stock: 216 + constraint: null + cost: 203 + init_stock: 15 max_stock: 1000 - price: 774 - sale_gamma: 54 + price: 261 + sale_gamma: 5 service_level: 0.95 SKU581: constraint: null - cost: 290 - init_stock: 399 + cost: 152 + init_stock: 516 max_stock: 1000 - price: 339 - sale_gamma: 57 + price: 185 + sale_gamma: 86 service_level: 0.95 SKU582: - constraint: G(low_profit -> low_stock_constraint) - cost: 130 - init_stock: 20 + constraint: G(stock_constraint) + cost: 359 + init_stock: 480 max_stock: 1000 - price: 166 - sale_gamma: 5 + price: 567 + sale_gamma: 96 service_level: 0.95 SKU583: - constraint: null - cost: 172 - init_stock: 168 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 86 + init_stock: 261 max_stock: 1000 - price: 237 - sale_gamma: 28 + price: 98 + sale_gamma: 87 service_level: 0.95 SKU584: - constraint: G(stock_constraint) - cost: 173 - init_stock: 340 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 33 + init_stock: 270 max_stock: 1000 - price: 316 - sale_gamma: 85 + price: 36 + sale_gamma: 45 service_level: 0.95 SKU585: - constraint: null - cost: 201 - init_stock: 474 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 100 max_stock: 1000 - price: 283 - sale_gamma: 79 + price: 53 + sale_gamma: 20 service_level: 0.95 SKU586: constraint: G(stock_constraint) - cost: 150 - init_stock: 208 + cost: 61 + init_stock: 264 max_stock: 1000 - price: 210 - sale_gamma: 52 + price: 94 + sale_gamma: 44 service_level: 0.95 SKU587: constraint: null - cost: 198 - init_stock: 180 + cost: 290 + init_stock: 468 max_stock: 1000 - price: 396 - sale_gamma: 36 + price: 423 + sale_gamma: 78 service_level: 0.95 SKU588: constraint: G(stock_constraint) - cost: 250 - init_stock: 118 + cost: 476 + init_stock: 176 max_stock: 1000 - price: 482 - sale_gamma: 59 + price: 885 + sale_gamma: 44 service_level: 0.95 SKU589: - constraint: G(stock_constraint) - cost: 51 - init_stock: 273 + constraint: null + cost: 244 + init_stock: 170 max_stock: 1000 - price: 67 - sale_gamma: 91 + price: 380 + sale_gamma: 34 service_level: 0.95 SKU59: - constraint: null - cost: 11 - init_stock: 60 + constraint: G(low_profit -> low_stock_constraint) + cost: 145 + init_stock: 255 max_stock: 1000 - price: 18 - sale_gamma: 12 + price: 275 + sale_gamma: 51 service_level: 0.95 SKU590: - constraint: G(stock_constraint) - cost: 492 - init_stock: 108 + constraint: null + cost: 70 + init_stock: 93 max_stock: 1000 - price: 831 - sale_gamma: 18 + price: 103 + sale_gamma: 31 service_level: 0.95 SKU591: - constraint: null - cost: 266 - init_stock: 630 + constraint: G(stock_constraint) + cost: 206 + init_stock: 504 max_stock: 1000 - price: 414 - sale_gamma: 90 + price: 298 + sale_gamma: 84 service_level: 0.95 SKU592: - constraint: G(low_profit -> low_stock_constraint) - cost: 293 - init_stock: 474 + constraint: null + cost: 218 + init_stock: 276 max_stock: 1000 - price: 342 - sale_gamma: 79 + price: 418 + sale_gamma: 46 service_level: 0.95 SKU593: - constraint: G(stock_constraint) - cost: 361 - init_stock: 495 + constraint: null + cost: 124 + init_stock: 88 max_stock: 1000 - price: 527 - sale_gamma: 99 + price: 221 + sale_gamma: 44 service_level: 0.95 SKU594: constraint: null - cost: 234 - init_stock: 91 + cost: 238 + init_stock: 150 max_stock: 1000 - price: 432 - sale_gamma: 13 + price: 459 + sale_gamma: 50 service_level: 0.95 SKU595: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 220 + constraint: null + cost: 73 + init_stock: 172 max_stock: 1000 - price: 41 - sale_gamma: 44 + price: 107 + sale_gamma: 43 service_level: 0.95 SKU596: - constraint: G(stock_constraint) - cost: 245 - init_stock: 567 + constraint: null + cost: 432 + init_stock: 35 max_stock: 1000 - price: 316 - sale_gamma: 81 + price: 540 + sale_gamma: 7 service_level: 0.95 SKU597: - constraint: null - cost: 27 - init_stock: 273 + constraint: G(stock_constraint) + cost: 252 + init_stock: 435 max_stock: 1000 - price: 32 - sale_gamma: 39 + price: 451 + sale_gamma: 87 service_level: 0.95 SKU598: constraint: null - cost: 290 - init_stock: 92 + cost: 348 + init_stock: 28 max_stock: 1000 - price: 342 - sale_gamma: 46 + price: 612 + sale_gamma: 14 service_level: 0.95 SKU599: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 246 + constraint: null + cost: 54 + init_stock: 204 max_stock: 1000 - price: 727 - sale_gamma: 41 + price: 66 + sale_gamma: 68 service_level: 0.95 SKU6: constraint: null - cost: 90 - init_stock: 136 - max_stock: 1000 - price: 132 - sale_gamma: 17 + cost: 122 + init_stock: 616 + max_stock: 1000 + price: 195 + sale_gamma: 88 service_level: 0.95 SKU60: - constraint: null - cost: 478 - init_stock: 267 + constraint: G(stock_constraint) + cost: 200 + init_stock: 234 max_stock: 1000 - price: 922 - sale_gamma: 89 + price: 346 + sale_gamma: 39 service_level: 0.95 SKU600: - constraint: G(low_profit -> low_stock_constraint) - cost: 411 - init_stock: 96 + constraint: null + cost: 386 + init_stock: 325 max_stock: 1000 - price: 785 - sale_gamma: 24 + price: 505 + sale_gamma: 65 service_level: 0.95 SKU601: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 363 - init_stock: 225 + constraint: null + cost: 138 + init_stock: 273 max_stock: 1000 - price: 555 - sale_gamma: 45 + price: 198 + sale_gamma: 39 service_level: 0.95 SKU602: - constraint: null - cost: 352 - init_stock: 392 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 184 + init_stock: 588 max_stock: 1000 - price: 556 - sale_gamma: 49 + price: 318 + sale_gamma: 98 service_level: 0.95 SKU603: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 36 - init_stock: 236 + constraint: G(low_profit -> low_stock_constraint) + cost: 278 + init_stock: 252 max_stock: 1000 - price: 63 - sale_gamma: 59 + price: 550 + sale_gamma: 42 service_level: 0.95 SKU604: - constraint: G(low_profit -> low_stock_constraint) - cost: 330 - init_stock: 390 + constraint: G(stock_constraint) + cost: 270 + init_stock: 400 max_stock: 1000 - price: 455 - sale_gamma: 78 + price: 378 + sale_gamma: 50 service_level: 0.95 SKU605: constraint: null - cost: 420 - init_stock: 465 + cost: 288 + init_stock: 120 max_stock: 1000 - price: 487 - sale_gamma: 93 + price: 443 + sale_gamma: 24 service_level: 0.95 SKU606: - constraint: null - cost: 424 - init_stock: 192 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 114 + init_stock: 66 max_stock: 1000 - price: 555 - sale_gamma: 64 + price: 161 + sale_gamma: 11 service_level: 0.95 SKU607: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 144 - init_stock: 77 + constraint: null + cost: 208 + init_stock: 395 max_stock: 1000 - price: 267 - sale_gamma: 11 + price: 359 + sale_gamma: 79 service_level: 0.95 SKU608: constraint: null - cost: 35 - init_stock: 132 + cost: 39 + init_stock: 365 max_stock: 1000 - price: 40 - sale_gamma: 44 + price: 49 + sale_gamma: 73 service_level: 0.95 SKU609: constraint: null - cost: 157 - init_stock: 240 + cost: 102 + init_stock: 114 max_stock: 1000 - price: 204 - sale_gamma: 40 + price: 171 + sale_gamma: 19 service_level: 0.95 SKU61: - constraint: G(low_profit -> low_stock_constraint) - cost: 317 - init_stock: 249 + constraint: null + cost: 461 + init_stock: 150 max_stock: 1000 - price: 488 - sale_gamma: 83 + price: 645 + sale_gamma: 75 service_level: 0.95 SKU610: constraint: null - cost: 62 - init_stock: 96 + cost: 449 + init_stock: 204 max_stock: 1000 - price: 83 - sale_gamma: 12 + price: 749 + sale_gamma: 68 service_level: 0.95 SKU611: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 258 - init_stock: 455 + constraint: G(low_profit -> low_stock_constraint) + cost: 306 + init_stock: 539 max_stock: 1000 - price: 464 - sale_gamma: 91 + price: 489 + sale_gamma: 77 service_level: 0.95 SKU612: - constraint: G(low_profit -> low_stock_constraint) - cost: 57 - init_stock: 360 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 391 + init_stock: 616 max_stock: 1000 - price: 67 - sale_gamma: 72 + price: 613 + sale_gamma: 88 service_level: 0.95 SKU613: - constraint: G(low_profit -> low_stock_constraint) - cost: 443 - init_stock: 129 + constraint: G(stock_constraint) + cost: 174 + init_stock: 56 max_stock: 1000 - price: 558 - sale_gamma: 43 + price: 254 + sale_gamma: 7 service_level: 0.95 SKU614: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 248 - init_stock: 430 + cost: 173 + init_stock: 45 max_stock: 1000 - price: 342 - sale_gamma: 86 + price: 318 + sale_gamma: 15 service_level: 0.95 SKU615: - constraint: null - cost: 259 - init_stock: 360 + constraint: G(stock_constraint) + cost: 330 + init_stock: 364 max_stock: 1000 - price: 284 - sale_gamma: 60 + price: 432 + sale_gamma: 91 service_level: 0.95 SKU616: constraint: G(low_profit -> low_stock_constraint) - cost: 188 - init_stock: 264 + cost: 232 + init_stock: 219 max_stock: 1000 - price: 359 - sale_gamma: 88 + price: 382 + sale_gamma: 73 service_level: 0.95 SKU617: constraint: null - cost: 334 - init_stock: 205 + cost: 203 + init_stock: 420 max_stock: 1000 - price: 427 - sale_gamma: 41 + price: 227 + sale_gamma: 60 service_level: 0.95 SKU618: - constraint: null - cost: 87 - init_stock: 186 + constraint: G(stock_constraint) + cost: 77 + init_stock: 138 max_stock: 1000 - price: 113 - sale_gamma: 31 + price: 97 + sale_gamma: 23 service_level: 0.95 SKU619: - constraint: null - cost: 215 - init_stock: 245 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 344 max_stock: 1000 - price: 344 - sale_gamma: 35 + price: 359 + sale_gamma: 86 service_level: 0.95 SKU62: - constraint: G(low_profit -> low_stock_constraint) - cost: 157 - init_stock: 365 + constraint: null + cost: 124 + init_stock: 36 max_stock: 1000 - price: 202 - sale_gamma: 73 + price: 168 + sale_gamma: 9 service_level: 0.95 SKU620: constraint: null - cost: 12 - init_stock: 700 + cost: 231 + init_stock: 156 max_stock: 1000 - price: 22 - sale_gamma: 100 + price: 267 + sale_gamma: 39 service_level: 0.95 SKU621: - constraint: G(stock_constraint) - cost: 363 - init_stock: 64 + constraint: null + cost: 199 + init_stock: 216 max_stock: 1000 - price: 664 - sale_gamma: 16 + price: 332 + sale_gamma: 72 service_level: 0.95 SKU622: constraint: null - cost: 152 - init_stock: 146 + cost: 253 + init_stock: 455 max_stock: 1000 - price: 197 - sale_gamma: 73 + price: 468 + sale_gamma: 65 service_level: 0.95 SKU623: constraint: null - cost: 263 - init_stock: 40 + cost: 335 + init_stock: 220 max_stock: 1000 price: 368 - sale_gamma: 10 + sale_gamma: 55 service_level: 0.95 SKU624: - constraint: G(low_profit -> low_stock_constraint) - cost: 423 - init_stock: 384 + constraint: null + cost: 482 + init_stock: 270 max_stock: 1000 - price: 723 - sale_gamma: 96 + price: 824 + sale_gamma: 54 service_level: 0.95 SKU625: constraint: null - cost: 294 - init_stock: 588 + cost: 46 + init_stock: 445 max_stock: 1000 - price: 414 - sale_gamma: 98 + price: 60 + sale_gamma: 89 service_level: 0.95 SKU626: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 204 - init_stock: 116 + constraint: null + cost: 451 + init_stock: 534 max_stock: 1000 - price: 324 - sale_gamma: 58 + price: 694 + sale_gamma: 89 service_level: 0.95 SKU627: constraint: null - cost: 175 - init_stock: 352 + cost: 220 + init_stock: 656 max_stock: 1000 - price: 232 - sale_gamma: 88 + price: 264 + sale_gamma: 82 service_level: 0.95 SKU628: constraint: null - cost: 14 - init_stock: 246 + cost: 124 + init_stock: 156 max_stock: 1000 - price: 20 - sale_gamma: 82 + price: 229 + sale_gamma: 26 service_level: 0.95 SKU629: constraint: G(stock_constraint) - cost: 352 - init_stock: 225 + cost: 353 + init_stock: 460 max_stock: 1000 - price: 665 - sale_gamma: 75 + price: 515 + sale_gamma: 92 service_level: 0.95 SKU63: constraint: null - cost: 305 - init_stock: 42 + cost: 68 + init_stock: 70 max_stock: 1000 - price: 509 - sale_gamma: 6 + price: 86 + sale_gamma: 14 service_level: 0.95 SKU630: - constraint: G(low_profit -> low_stock_constraint) - cost: 407 - init_stock: 474 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 122 + init_stock: 34 max_stock: 1000 - price: 525 - sale_gamma: 79 + price: 137 + sale_gamma: 17 service_level: 0.95 SKU631: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 231 + constraint: null + cost: 296 + init_stock: 196 max_stock: 1000 - price: 529 - sale_gamma: 33 + price: 399 + sale_gamma: 49 service_level: 0.95 SKU632: constraint: null - cost: 489 - init_stock: 120 + cost: 85 + init_stock: 470 max_stock: 1000 - price: 929 - sale_gamma: 15 + price: 141 + sale_gamma: 94 service_level: 0.95 SKU633: - constraint: null - cost: 298 - init_stock: 260 + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 402 max_stock: 1000 - price: 506 - sale_gamma: 65 + price: 228 + sale_gamma: 67 service_level: 0.95 SKU634: - constraint: null - cost: 52 - init_stock: 518 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 17 + init_stock: 438 max_stock: 1000 - price: 80 - sale_gamma: 74 + price: 30 + sale_gamma: 73 service_level: 0.95 SKU635: constraint: null - cost: 30 - init_stock: 388 + cost: 341 + init_stock: 372 max_stock: 1000 - price: 45 - sale_gamma: 97 + price: 654 + sale_gamma: 93 service_level: 0.95 SKU636: - constraint: null - cost: 38 - init_stock: 350 + constraint: G(stock_constraint) + cost: 385 + init_stock: 111 max_stock: 1000 - price: 44 - sale_gamma: 50 + price: 739 + sale_gamma: 37 service_level: 0.95 SKU637: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 474 - init_stock: 205 + constraint: G(low_profit -> low_stock_constraint) + cost: 83 + init_stock: 305 max_stock: 1000 - price: 630 - sale_gamma: 41 + price: 118 + sale_gamma: 61 service_level: 0.95 SKU638: - constraint: G(low_profit -> low_stock_constraint) - cost: 267 - init_stock: 432 + constraint: G(stock_constraint) + cost: 375 + init_stock: 16 max_stock: 1000 - price: 421 - sale_gamma: 54 + price: 536 + sale_gamma: 8 service_level: 0.95 SKU639: constraint: null - cost: 466 - init_stock: 78 + cost: 327 + init_stock: 160 max_stock: 1000 - price: 726 - sale_gamma: 26 + price: 487 + sale_gamma: 40 service_level: 0.95 SKU64: constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 108 + cost: 36 + init_stock: 133 max_stock: 1000 - price: 345 - sale_gamma: 54 + price: 42 + sale_gamma: 19 service_level: 0.95 SKU640: - constraint: null - cost: 236 - init_stock: 90 + constraint: G(low_profit -> low_stock_constraint) + cost: 275 + init_stock: 546 max_stock: 1000 - price: 297 - sale_gamma: 18 + price: 382 + sale_gamma: 91 service_level: 0.95 SKU641: constraint: null - cost: 285 - init_stock: 305 + cost: 365 + init_stock: 486 max_stock: 1000 - price: 530 - sale_gamma: 61 + price: 445 + sale_gamma: 81 service_level: 0.95 SKU642: - constraint: G(low_profit -> low_stock_constraint) - cost: 133 - init_stock: 420 + constraint: null + cost: 414 + init_stock: 150 max_stock: 1000 - price: 162 - sale_gamma: 84 + price: 554 + sale_gamma: 25 service_level: 0.95 SKU643: - constraint: G(low_profit -> low_stock_constraint) - cost: 356 - init_stock: 700 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 358 + init_stock: 322 max_stock: 1000 - price: 480 - sale_gamma: 100 + price: 461 + sale_gamma: 46 service_level: 0.95 SKU644: - constraint: null - cost: 499 - init_stock: 210 + constraint: G(low_profit -> low_stock_constraint) + cost: 46 + init_stock: 410 max_stock: 1000 - price: 558 - sale_gamma: 35 + price: 61 + sale_gamma: 82 service_level: 0.95 SKU645: constraint: null - cost: 476 - init_stock: 564 + cost: 467 + init_stock: 594 max_stock: 1000 - price: 599 - sale_gamma: 94 + price: 742 + sale_gamma: 99 service_level: 0.95 SKU646: - constraint: null - cost: 78 - init_stock: 152 + constraint: G(low_profit -> low_stock_constraint) + cost: 189 + init_stock: 91 max_stock: 1000 - price: 142 - sale_gamma: 38 + price: 268 + sale_gamma: 13 service_level: 0.95 SKU647: - constraint: G(stock_constraint) - cost: 52 - init_stock: 354 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 303 + init_stock: 462 max_stock: 1000 - price: 81 - sale_gamma: 59 + price: 369 + sale_gamma: 77 service_level: 0.95 SKU648: constraint: G(stock_constraint) - cost: 468 - init_stock: 415 + cost: 226 + init_stock: 110 max_stock: 1000 - price: 767 - sale_gamma: 83 + price: 336 + sale_gamma: 55 service_level: 0.95 SKU649: constraint: null - cost: 345 - init_stock: 490 + cost: 198 + init_stock: 175 max_stock: 1000 - price: 679 - sale_gamma: 70 + price: 229 + sale_gamma: 25 service_level: 0.95 SKU65: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 385 - init_stock: 162 + constraint: null + cost: 15 + init_stock: 658 max_stock: 1000 - price: 770 - sale_gamma: 27 + price: 25 + sale_gamma: 94 service_level: 0.95 SKU650: constraint: null - cost: 130 - init_stock: 80 + cost: 351 + init_stock: 224 max_stock: 1000 - price: 254 - sale_gamma: 16 + price: 498 + sale_gamma: 56 service_level: 0.95 SKU651: - constraint: G(low_profit -> low_stock_constraint) - cost: 248 - init_stock: 88 + constraint: null + cost: 380 + init_stock: 672 max_stock: 1000 - price: 297 - sale_gamma: 22 + price: 596 + sale_gamma: 96 service_level: 0.95 SKU652: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 100 - init_stock: 460 + constraint: null + cost: 123 + init_stock: 280 max_stock: 1000 - price: 171 - sale_gamma: 92 + price: 168 + sale_gamma: 40 service_level: 0.95 SKU653: - constraint: null - cost: 453 - init_stock: 158 + constraint: G(low_profit -> low_stock_constraint) + cost: 479 + init_stock: 384 max_stock: 1000 - price: 901 - sale_gamma: 79 + price: 661 + sale_gamma: 96 service_level: 0.95 SKU654: - constraint: G(low_profit -> low_stock_constraint) - cost: 441 - init_stock: 217 + constraint: null + cost: 66 + init_stock: 32 max_stock: 1000 - price: 511 - sale_gamma: 31 + price: 110 + sale_gamma: 8 service_level: 0.95 SKU655: - constraint: null - cost: 146 - init_stock: 108 + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 126 max_stock: 1000 - price: 292 - sale_gamma: 54 + price: 479 + sale_gamma: 42 service_level: 0.95 SKU656: - constraint: G(low_profit -> low_stock_constraint) - cost: 367 - init_stock: 304 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 53 + init_stock: 504 max_stock: 1000 - price: 517 - sale_gamma: 38 + price: 81 + sale_gamma: 72 service_level: 0.95 SKU657: - constraint: G(stock_constraint) - cost: 195 - init_stock: 144 + constraint: G(low_profit -> low_stock_constraint) + cost: 73 + init_stock: 567 max_stock: 1000 - price: 349 - sale_gamma: 24 + price: 110 + sale_gamma: 81 service_level: 0.95 SKU658: - constraint: G(low_profit -> low_stock_constraint) - cost: 480 - init_stock: 609 + constraint: null + cost: 70 + init_stock: 252 max_stock: 1000 - price: 844 - sale_gamma: 87 + price: 107 + sale_gamma: 36 service_level: 0.95 SKU659: - constraint: G(low_profit -> low_stock_constraint) - cost: 497 - init_stock: 768 + constraint: null + cost: 21 + init_stock: 336 max_stock: 1000 - price: 864 - sale_gamma: 96 + price: 27 + sale_gamma: 56 service_level: 0.95 SKU66: constraint: null - cost: 37 - init_stock: 438 + cost: 143 + init_stock: 360 max_stock: 1000 - price: 60 - sale_gamma: 73 + price: 230 + sale_gamma: 90 service_level: 0.95 SKU660: constraint: null - cost: 163 - init_stock: 462 + cost: 487 + init_stock: 415 max_stock: 1000 - price: 229 - sale_gamma: 66 + price: 560 + sale_gamma: 83 service_level: 0.95 SKU661: constraint: null - cost: 389 - init_stock: 84 + cost: 344 + init_stock: 296 max_stock: 1000 - price: 676 - sale_gamma: 12 + price: 581 + sale_gamma: 74 service_level: 0.95 SKU662: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 495 - init_stock: 246 + cost: 372 + init_stock: 266 max_stock: 1000 - price: 806 - sale_gamma: 82 + price: 628 + sale_gamma: 38 service_level: 0.95 SKU663: - constraint: null - cost: 460 - init_stock: 520 + constraint: G(stock_constraint) + cost: 418 + init_stock: 320 max_stock: 1000 - price: 722 - sale_gamma: 65 + price: 518 + sale_gamma: 40 service_level: 0.95 SKU664: - constraint: null - cost: 397 - init_stock: 72 + constraint: G(stock_constraint) + cost: 351 + init_stock: 420 max_stock: 1000 - price: 575 - sale_gamma: 12 + price: 494 + sale_gamma: 84 service_level: 0.95 SKU665: constraint: null - cost: 67 - init_stock: 426 + cost: 493 + init_stock: 344 max_stock: 1000 - price: 118 - sale_gamma: 71 + price: 734 + sale_gamma: 43 service_level: 0.95 SKU666: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 126 - init_stock: 60 + constraint: G(stock_constraint) + cost: 341 + init_stock: 176 max_stock: 1000 - price: 148 - sale_gamma: 12 + price: 572 + sale_gamma: 88 service_level: 0.95 SKU667: constraint: null - cost: 140 - init_stock: 49 + cost: 325 + init_stock: 52 max_stock: 1000 - price: 165 - sale_gamma: 7 + price: 562 + sale_gamma: 13 service_level: 0.95 SKU668: constraint: G(low_profit -> low_stock_constraint) - cost: 110 - init_stock: 297 + cost: 286 + init_stock: 300 max_stock: 1000 - price: 204 - sale_gamma: 99 + price: 463 + sale_gamma: 60 service_level: 0.95 SKU669: - constraint: G(stock_constraint) - cost: 121 - init_stock: 134 + constraint: null + cost: 74 + init_stock: 96 max_stock: 1000 - price: 240 - sale_gamma: 67 + price: 83 + sale_gamma: 16 service_level: 0.95 SKU67: constraint: G(low_profit -> low_stock_constraint) - cost: 54 - init_stock: 602 + cost: 247 + init_stock: 159 max_stock: 1000 - price: 68 - sale_gamma: 86 + price: 454 + sale_gamma: 53 service_level: 0.95 SKU670: - constraint: null - cost: 495 - init_stock: 10 + constraint: G(stock_constraint) + cost: 289 + init_stock: 258 max_stock: 1000 - price: 564 - sale_gamma: 5 + price: 430 + sale_gamma: 86 service_level: 0.95 SKU671: - constraint: null - cost: 444 - init_stock: 80 + constraint: G(stock_constraint) + cost: 267 + init_stock: 332 max_stock: 1000 - price: 777 - sale_gamma: 10 + price: 296 + sale_gamma: 83 service_level: 0.95 SKU672: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 426 - init_stock: 468 + constraint: null + cost: 369 + init_stock: 78 max_stock: 1000 - price: 511 - sale_gamma: 78 + price: 420 + sale_gamma: 13 service_level: 0.95 SKU673: constraint: G(low_profit -> low_stock_constraint) - cost: 109 - init_stock: 162 + cost: 322 + init_stock: 123 max_stock: 1000 - price: 199 - sale_gamma: 54 + price: 418 + sale_gamma: 41 service_level: 0.95 SKU674: constraint: null - cost: 392 - init_stock: 48 + cost: 223 + init_stock: 66 max_stock: 1000 - price: 431 - sale_gamma: 8 + price: 263 + sale_gamma: 22 service_level: 0.95 SKU675: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 135 - init_stock: 165 + cost: 438 + init_stock: 99 max_stock: 1000 - price: 243 + price: 529 sale_gamma: 33 service_level: 0.95 SKU676: constraint: null - cost: 401 - init_stock: 255 + cost: 244 + init_stock: 366 max_stock: 1000 - price: 517 - sale_gamma: 85 + price: 275 + sale_gamma: 61 service_level: 0.95 SKU677: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 350 + constraint: null + cost: 425 + init_stock: 176 max_stock: 1000 - price: 718 - sale_gamma: 50 + price: 658 + sale_gamma: 44 service_level: 0.95 SKU678: constraint: null - cost: 208 - init_stock: 244 + cost: 168 + init_stock: 216 max_stock: 1000 - price: 409 - sale_gamma: 61 + price: 199 + sale_gamma: 36 service_level: 0.95 SKU679: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 356 - init_stock: 249 + constraint: null + cost: 395 + init_stock: 132 max_stock: 1000 - price: 672 - sale_gamma: 83 + price: 734 + sale_gamma: 44 service_level: 0.95 SKU68: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 63 - init_stock: 518 + constraint: G(stock_constraint) + cost: 169 + init_stock: 56 max_stock: 1000 - price: 81 - sale_gamma: 74 + price: 239 + sale_gamma: 14 service_level: 0.95 SKU680: - constraint: null - cost: 287 - init_stock: 156 - max_stock: 1000 - price: 401 - sale_gamma: 26 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 260 + init_stock: 180 + max_stock: 1000 + price: 327 + sale_gamma: 30 service_level: 0.95 SKU681: - constraint: G(stock_constraint) - cost: 140 - init_stock: 490 + constraint: G(low_profit -> low_stock_constraint) + cost: 464 + init_stock: 776 max_stock: 1000 - price: 158 - sale_gamma: 70 + price: 510 + sale_gamma: 97 service_level: 0.95 SKU682: - constraint: G(stock_constraint) - cost: 163 - init_stock: 258 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 370 + init_stock: 318 max_stock: 1000 - price: 308 - sale_gamma: 86 + price: 407 + sale_gamma: 53 service_level: 0.95 SKU683: - constraint: G(stock_constraint) - cost: 359 - init_stock: 304 + constraint: G(low_profit -> low_stock_constraint) + cost: 327 + init_stock: 536 max_stock: 1000 - price: 502 - sale_gamma: 76 + price: 608 + sale_gamma: 67 service_level: 0.95 SKU684: constraint: G(stock_constraint) - cost: 37 - init_stock: 124 + cost: 355 + init_stock: 158 max_stock: 1000 - price: 58 - sale_gamma: 31 + price: 401 + sale_gamma: 79 service_level: 0.95 SKU685: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 106 - init_stock: 256 + constraint: null + cost: 422 + init_stock: 270 max_stock: 1000 - price: 156 - sale_gamma: 32 + price: 493 + sale_gamma: 45 service_level: 0.95 SKU686: - constraint: null - cost: 52 - init_stock: 348 + constraint: G(low_profit -> low_stock_constraint) + cost: 63 + init_stock: 378 max_stock: 1000 - price: 96 - sale_gamma: 58 + price: 122 + sale_gamma: 63 service_level: 0.95 SKU687: - constraint: null - cost: 85 - init_stock: 236 + constraint: G(low_profit -> low_stock_constraint) + cost: 34 + init_stock: 456 max_stock: 1000 - price: 120 - sale_gamma: 59 + price: 43 + sale_gamma: 76 service_level: 0.95 SKU688: - constraint: G(stock_constraint) - cost: 148 - init_stock: 84 + constraint: G(low_profit -> low_stock_constraint) + cost: 484 + init_stock: 462 max_stock: 1000 - price: 239 - sale_gamma: 28 + price: 759 + sale_gamma: 77 service_level: 0.95 SKU689: constraint: G(stock_constraint) - cost: 333 - init_stock: 165 + cost: 499 + init_stock: 75 max_stock: 1000 - price: 389 - sale_gamma: 33 + price: 808 + sale_gamma: 15 service_level: 0.95 SKU69: - constraint: null - cost: 65 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 176 init_stock: 402 max_stock: 1000 - price: 99 + price: 197 sale_gamma: 67 service_level: 0.95 SKU690: - constraint: G(low_profit -> low_stock_constraint) - cost: 372 - init_stock: 80 + constraint: null + cost: 437 + init_stock: 415 max_stock: 1000 - price: 621 - sale_gamma: 20 + price: 498 + sale_gamma: 83 service_level: 0.95 SKU691: - constraint: G(low_profit -> low_stock_constraint) - cost: 275 - init_stock: 120 + constraint: null + cost: 17 + init_stock: 632 max_stock: 1000 - price: 484 - sale_gamma: 24 + price: 18 + sale_gamma: 79 service_level: 0.95 SKU692: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 289 - init_stock: 400 + constraint: G(stock_constraint) + cost: 225 + init_stock: 195 max_stock: 1000 - price: 520 - sale_gamma: 80 + price: 258 + sale_gamma: 65 service_level: 0.95 SKU693: - constraint: null - cost: 258 - init_stock: 210 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 19 + init_stock: 244 max_stock: 1000 - price: 366 - sale_gamma: 70 + price: 21 + sale_gamma: 61 service_level: 0.95 SKU694: - constraint: G(stock_constraint) - cost: 472 - init_stock: 72 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 208 + init_stock: 300 max_stock: 1000 - price: 764 - sale_gamma: 18 + price: 386 + sale_gamma: 75 service_level: 0.95 SKU695: - constraint: G(stock_constraint) - cost: 318 - init_stock: 288 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 190 + init_stock: 49 max_stock: 1000 - price: 610 - sale_gamma: 48 + price: 361 + sale_gamma: 7 service_level: 0.95 SKU696: - constraint: G(stock_constraint) - cost: 63 - init_stock: 276 + constraint: G(low_profit -> low_stock_constraint) + cost: 348 + init_stock: 290 max_stock: 1000 - price: 101 - sale_gamma: 92 + price: 643 + sale_gamma: 58 service_level: 0.95 SKU697: - constraint: null - cost: 15 - init_stock: 108 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 455 + init_stock: 320 max_stock: 1000 - price: 22 - sale_gamma: 54 + price: 641 + sale_gamma: 80 service_level: 0.95 SKU698: - constraint: G(low_profit -> low_stock_constraint) - cost: 14 - init_stock: 408 + constraint: null + cost: 198 + init_stock: 488 max_stock: 1000 - price: 16 - sale_gamma: 51 + price: 316 + sale_gamma: 61 service_level: 0.95 SKU699: - constraint: null - cost: 357 - init_stock: 511 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 100 max_stock: 1000 - price: 581 - sale_gamma: 73 + price: 58 + sale_gamma: 20 service_level: 0.95 SKU7: constraint: null - cost: 314 - init_stock: 86 + cost: 101 + init_stock: 480 max_stock: 1000 - price: 593 - sale_gamma: 43 + price: 131 + sale_gamma: 96 service_level: 0.95 SKU70: - constraint: null - cost: 207 - init_stock: 616 + constraint: G(stock_constraint) + cost: 186 + init_stock: 140 max_stock: 1000 - price: 271 - sale_gamma: 77 + price: 288 + sale_gamma: 35 service_level: 0.95 SKU700: - constraint: null - cost: 21 - init_stock: 162 + constraint: G(low_profit -> low_stock_constraint) + cost: 74 + init_stock: 45 max_stock: 1000 - price: 40 - sale_gamma: 54 + price: 130 + sale_gamma: 9 service_level: 0.95 SKU701: - constraint: null - cost: 130 - init_stock: 158 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 399 + init_stock: 462 max_stock: 1000 - price: 258 - sale_gamma: 79 + price: 770 + sale_gamma: 77 service_level: 0.95 SKU702: - constraint: null - cost: 227 - init_stock: 414 + constraint: G(stock_constraint) + cost: 82 + init_stock: 204 max_stock: 1000 - price: 390 - sale_gamma: 69 + price: 163 + sale_gamma: 34 service_level: 0.95 SKU703: - constraint: null - cost: 73 - init_stock: 210 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 363 + init_stock: 76 max_stock: 1000 - price: 128 - sale_gamma: 35 + price: 602 + sale_gamma: 38 service_level: 0.95 SKU704: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 52 + constraint: G(stock_constraint) + cost: 384 + init_stock: 261 max_stock: 1000 - price: 55 - sale_gamma: 26 + price: 518 + sale_gamma: 87 service_level: 0.95 SKU705: constraint: null - cost: 313 - init_stock: 105 + cost: 128 + init_stock: 378 max_stock: 1000 - price: 594 - sale_gamma: 21 + price: 236 + sale_gamma: 63 service_level: 0.95 SKU706: constraint: null - cost: 378 - init_stock: 56 + cost: 182 + init_stock: 455 max_stock: 1000 - price: 476 - sale_gamma: 14 + price: 242 + sale_gamma: 91 service_level: 0.95 SKU707: - constraint: G(low_profit -> low_stock_constraint) - cost: 302 - init_stock: 700 + constraint: null + cost: 18 + init_stock: 276 max_stock: 1000 - price: 389 - sale_gamma: 100 + price: 35 + sale_gamma: 69 service_level: 0.95 SKU708: - constraint: G(stock_constraint) - cost: 69 - init_stock: 348 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 178 + init_stock: 112 max_stock: 1000 - price: 121 - sale_gamma: 58 + price: 334 + sale_gamma: 28 service_level: 0.95 SKU709: - constraint: null - cost: 63 - init_stock: 234 + constraint: G(stock_constraint) + cost: 102 + init_stock: 212 max_stock: 1000 - price: 81 - sale_gamma: 78 + price: 173 + sale_gamma: 53 service_level: 0.95 SKU71: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 251 - init_stock: 246 + constraint: null + cost: 315 + init_stock: 225 max_stock: 1000 - price: 389 - sale_gamma: 41 + price: 589 + sale_gamma: 45 service_level: 0.95 SKU710: - constraint: G(low_profit -> low_stock_constraint) - cost: 477 - init_stock: 248 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 252 + init_stock: 95 max_stock: 1000 - price: 815 - sale_gamma: 62 + price: 337 + sale_gamma: 19 service_level: 0.95 SKU711: - constraint: G(low_profit -> low_stock_constraint) - cost: 395 - init_stock: 490 + constraint: null + cost: 281 + init_stock: 414 max_stock: 1000 - price: 687 - sale_gamma: 98 + price: 320 + sale_gamma: 69 service_level: 0.95 SKU712: constraint: null - cost: 50 - init_stock: 340 + cost: 232 + init_stock: 77 max_stock: 1000 - price: 91 - sale_gamma: 68 + price: 438 + sale_gamma: 11 service_level: 0.95 SKU713: constraint: null - cost: 224 - init_stock: 150 + cost: 285 + init_stock: 129 max_stock: 1000 - price: 309 - sale_gamma: 25 + price: 413 + sale_gamma: 43 service_level: 0.95 SKU714: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 98 - init_stock: 210 + constraint: null + cost: 79 + init_stock: 287 max_stock: 1000 - price: 134 - sale_gamma: 42 + price: 106 + sale_gamma: 41 service_level: 0.95 SKU715: constraint: G(stock_constraint) - cost: 499 - init_stock: 99 + cost: 234 + init_stock: 34 max_stock: 1000 - price: 843 - sale_gamma: 33 + price: 271 + sale_gamma: 17 service_level: 0.95 SKU716: constraint: null - cost: 468 - init_stock: 160 + cost: 70 + init_stock: 450 max_stock: 1000 - price: 790 - sale_gamma: 32 + price: 123 + sale_gamma: 75 service_level: 0.95 SKU717: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 115 - init_stock: 125 + cost: 345 + init_stock: 56 max_stock: 1000 - price: 213 - sale_gamma: 25 + price: 382 + sale_gamma: 8 service_level: 0.95 SKU718: constraint: null - cost: 284 - init_stock: 105 + cost: 357 + init_stock: 174 max_stock: 1000 - price: 448 - sale_gamma: 35 + price: 692 + sale_gamma: 58 service_level: 0.95 SKU719: - constraint: G(stock_constraint) - cost: 167 - init_stock: 36 + constraint: null + cost: 340 + init_stock: 455 max_stock: 1000 - price: 242 - sale_gamma: 12 + price: 384 + sale_gamma: 65 service_level: 0.95 SKU72: constraint: null - cost: 77 - init_stock: 511 + cost: 458 + init_stock: 595 max_stock: 1000 - price: 153 - sale_gamma: 73 + price: 870 + sale_gamma: 85 service_level: 0.95 SKU720: constraint: null - cost: 382 - init_stock: 30 + cost: 325 + init_stock: 294 max_stock: 1000 - price: 527 - sale_gamma: 5 + price: 503 + sale_gamma: 42 service_level: 0.95 SKU721: - constraint: G(stock_constraint) - cost: 369 - init_stock: 324 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 73 + init_stock: 44 max_stock: 1000 - price: 734 - sale_gamma: 54 + price: 143 + sale_gamma: 11 service_level: 0.95 SKU722: - constraint: null - cost: 465 - init_stock: 420 + constraint: G(stock_constraint) + cost: 392 + init_stock: 194 max_stock: 1000 - price: 678 - sale_gamma: 70 + price: 572 + sale_gamma: 97 service_level: 0.95 SKU723: - constraint: G(low_profit -> low_stock_constraint) - cost: 43 - init_stock: 96 + constraint: null + cost: 318 + init_stock: 356 max_stock: 1000 - price: 52 - sale_gamma: 48 + price: 442 + sale_gamma: 89 service_level: 0.95 SKU724: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 246 - init_stock: 368 + constraint: G(low_profit -> low_stock_constraint) + cost: 400 + init_stock: 198 max_stock: 1000 - price: 309 - sale_gamma: 92 + price: 520 + sale_gamma: 33 service_level: 0.95 SKU725: constraint: null - cost: 174 - init_stock: 62 + cost: 175 + init_stock: 148 max_stock: 1000 - price: 212 - sale_gamma: 31 + price: 309 + sale_gamma: 37 service_level: 0.95 SKU726: - constraint: null - cost: 391 - init_stock: 318 + constraint: G(low_profit -> low_stock_constraint) + cost: 458 + init_stock: 356 max_stock: 1000 - price: 512 - sale_gamma: 53 + price: 567 + sale_gamma: 89 service_level: 0.95 SKU727: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 447 - init_stock: 244 + constraint: null + cost: 418 + init_stock: 285 max_stock: 1000 - price: 612 - sale_gamma: 61 + price: 526 + sale_gamma: 57 service_level: 0.95 SKU728: constraint: null - cost: 347 - init_stock: 180 + cost: 475 + init_stock: 185 max_stock: 1000 - price: 499 - sale_gamma: 30 + price: 864 + sale_gamma: 37 service_level: 0.95 SKU729: - constraint: G(stock_constraint) - cost: 463 - init_stock: 166 + constraint: null + cost: 324 + init_stock: 252 max_stock: 1000 - price: 828 - sale_gamma: 83 + price: 476 + sale_gamma: 36 service_level: 0.95 SKU73: constraint: null - cost: 163 - init_stock: 132 + cost: 212 + init_stock: 216 max_stock: 1000 - price: 228 - sale_gamma: 33 + price: 248 + sale_gamma: 54 service_level: 0.95 SKU730: - constraint: G(stock_constraint) - cost: 211 - init_stock: 90 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 16 + init_stock: 65 max_stock: 1000 - price: 369 - sale_gamma: 15 + price: 19 + sale_gamma: 13 service_level: 0.95 SKU731: constraint: G(stock_constraint) - cost: 57 - init_stock: 432 + cost: 88 + init_stock: 410 max_stock: 1000 - price: 86 - sale_gamma: 54 + price: 155 + sale_gamma: 82 service_level: 0.95 SKU732: constraint: null - cost: 106 - init_stock: 144 + cost: 41 + init_stock: 434 max_stock: 1000 - price: 156 - sale_gamma: 24 + price: 73 + sale_gamma: 62 service_level: 0.95 SKU733: - constraint: null - cost: 302 - init_stock: 114 + constraint: G(stock_constraint) + cost: 315 + init_stock: 104 max_stock: 1000 - price: 516 - sale_gamma: 19 + price: 541 + sale_gamma: 26 service_level: 0.95 SKU734: - constraint: null - cost: 386 - init_stock: 56 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 255 max_stock: 1000 - price: 575 - sale_gamma: 14 + price: 71 + sale_gamma: 51 service_level: 0.95 SKU735: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 85 - init_stock: 273 + constraint: null + cost: 266 + init_stock: 594 max_stock: 1000 - price: 130 - sale_gamma: 39 + price: 409 + sale_gamma: 99 service_level: 0.95 SKU736: constraint: G(stock_constraint) - cost: 16 - init_stock: 335 + cost: 368 + init_stock: 75 max_stock: 1000 - price: 29 - sale_gamma: 67 + price: 632 + sale_gamma: 25 service_level: 0.95 SKU737: constraint: null - cost: 382 - init_stock: 736 + cost: 475 + init_stock: 93 max_stock: 1000 - price: 626 - sale_gamma: 92 + price: 655 + sale_gamma: 31 service_level: 0.95 SKU738: constraint: null - cost: 429 - init_stock: 63 + cost: 185 + init_stock: 192 max_stock: 1000 - price: 506 - sale_gamma: 9 + price: 246 + sale_gamma: 48 service_level: 0.95 SKU739: - constraint: null - cost: 285 - init_stock: 90 + constraint: G(low_profit -> low_stock_constraint) + cost: 475 + init_stock: 296 max_stock: 1000 - price: 347 - sale_gamma: 45 + price: 612 + sale_gamma: 74 service_level: 0.95 SKU74: - constraint: G(stock_constraint) - cost: 47 - init_stock: 396 + constraint: null + cost: 159 + init_stock: 168 max_stock: 1000 - price: 94 - sale_gamma: 99 + price: 189 + sale_gamma: 42 service_level: 0.95 SKU740: - constraint: G(low_profit -> low_stock_constraint) - cost: 475 - init_stock: 348 + constraint: null + cost: 390 + init_stock: 256 max_stock: 1000 - price: 722 - sale_gamma: 58 + price: 549 + sale_gamma: 64 service_level: 0.95 SKU741: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 466 - init_stock: 142 + constraint: G(stock_constraint) + cost: 91 + init_stock: 280 max_stock: 1000 - price: 535 - sale_gamma: 71 + price: 112 + sale_gamma: 56 service_level: 0.95 SKU742: - constraint: null - cost: 442 - init_stock: 135 + constraint: G(stock_constraint) + cost: 188 + init_stock: 110 max_stock: 1000 - price: 552 - sale_gamma: 27 + price: 374 + sale_gamma: 22 service_level: 0.95 SKU743: - constraint: G(stock_constraint) - cost: 83 - init_stock: 120 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 217 + init_stock: 301 max_stock: 1000 - price: 113 - sale_gamma: 20 + price: 431 + sale_gamma: 43 service_level: 0.95 SKU744: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 340 - init_stock: 360 + constraint: G(low_profit -> low_stock_constraint) + cost: 379 + init_stock: 290 max_stock: 1000 - price: 673 - sale_gamma: 72 + price: 579 + sale_gamma: 58 service_level: 0.95 SKU745: - constraint: null - cost: 444 - init_stock: 350 + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 368 max_stock: 1000 - price: 701 - sale_gamma: 50 + price: 376 + sale_gamma: 92 service_level: 0.95 SKU746: constraint: null - cost: 496 - init_stock: 576 + cost: 437 + init_stock: 160 max_stock: 1000 - price: 917 - sale_gamma: 96 + price: 624 + sale_gamma: 40 service_level: 0.95 SKU747: constraint: null - cost: 175 - init_stock: 240 + cost: 373 + init_stock: 474 max_stock: 1000 - price: 262 - sale_gamma: 60 + price: 589 + sale_gamma: 79 service_level: 0.95 SKU748: constraint: null - cost: 461 - init_stock: 28 + cost: 275 + init_stock: 330 max_stock: 1000 - price: 848 - sale_gamma: 7 + price: 464 + sale_gamma: 66 service_level: 0.95 SKU749: - constraint: G(stock_constraint) - cost: 481 - init_stock: 68 + constraint: null + cost: 394 + init_stock: 106 max_stock: 1000 - price: 716 - sale_gamma: 17 + price: 705 + sale_gamma: 53 service_level: 0.95 SKU75: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 319 - init_stock: 96 + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 76 max_stock: 1000 - price: 408 - sale_gamma: 32 + price: 217 + sale_gamma: 19 service_level: 0.95 SKU750: - constraint: null - cost: 240 - init_stock: 270 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 310 max_stock: 1000 - price: 472 - sale_gamma: 45 + price: 463 + sale_gamma: 62 service_level: 0.95 SKU751: constraint: null - cost: 302 - init_stock: 54 + cost: 369 + init_stock: 325 max_stock: 1000 - price: 552 - sale_gamma: 9 + price: 428 + sale_gamma: 65 service_level: 0.95 SKU752: - constraint: G(low_profit -> low_stock_constraint) - cost: 425 - init_stock: 165 + constraint: null + cost: 259 + init_stock: 546 max_stock: 1000 - price: 654 - sale_gamma: 33 + price: 435 + sale_gamma: 78 service_level: 0.95 SKU753: - constraint: null - cost: 105 - init_stock: 352 + constraint: G(stock_constraint) + cost: 77 + init_stock: 325 max_stock: 1000 - price: 124 - sale_gamma: 44 + price: 128 + sale_gamma: 65 service_level: 0.95 SKU754: - constraint: null - cost: 495 - init_stock: 372 + constraint: G(stock_constraint) + cost: 387 + init_stock: 91 max_stock: 1000 - price: 905 - sale_gamma: 93 + price: 545 + sale_gamma: 13 service_level: 0.95 SKU755: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 436 - init_stock: 217 + constraint: null + cost: 354 + init_stock: 560 max_stock: 1000 - price: 627 - sale_gamma: 31 + price: 523 + sale_gamma: 80 service_level: 0.95 SKU756: - constraint: null - cost: 234 - init_stock: 665 + constraint: G(stock_constraint) + cost: 246 + init_stock: 360 max_stock: 1000 - price: 297 - sale_gamma: 95 + price: 361 + sale_gamma: 90 service_level: 0.95 SKU757: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 313 - init_stock: 78 + constraint: null + cost: 40 + init_stock: 399 max_stock: 1000 - price: 438 - sale_gamma: 13 + price: 76 + sale_gamma: 57 service_level: 0.95 SKU758: constraint: null - cost: 319 - init_stock: 126 + cost: 241 + init_stock: 160 max_stock: 1000 - price: 392 - sale_gamma: 21 + price: 457 + sale_gamma: 40 service_level: 0.95 SKU759: - constraint: null - cost: 313 - init_stock: 287 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 435 + init_stock: 76 max_stock: 1000 - price: 394 - sale_gamma: 41 + price: 648 + sale_gamma: 38 service_level: 0.95 SKU76: - constraint: G(stock_constraint) - cost: 242 - init_stock: 268 + constraint: null + cost: 147 + init_stock: 256 max_stock: 1000 - price: 358 - sale_gamma: 67 + price: 191 + sale_gamma: 64 service_level: 0.95 SKU760: constraint: null - cost: 358 - init_stock: 104 + cost: 176 + init_stock: 255 max_stock: 1000 - price: 544 - sale_gamma: 26 + price: 279 + sale_gamma: 51 service_level: 0.95 SKU761: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 112 + constraint: G(stock_constraint) + cost: 224 + init_stock: 275 max_stock: 1000 - price: 866 - sale_gamma: 16 + price: 250 + sale_gamma: 55 service_level: 0.95 SKU762: - constraint: null - cost: 412 - init_stock: 30 + constraint: G(stock_constraint) + cost: 264 + init_stock: 153 max_stock: 1000 - price: 815 - sale_gamma: 5 + price: 351 + sale_gamma: 51 service_level: 0.95 SKU763: - constraint: G(low_profit -> low_stock_constraint) - cost: 391 - init_stock: 152 + constraint: null + cost: 385 + init_stock: 316 max_stock: 1000 - price: 645 - sale_gamma: 38 + price: 677 + sale_gamma: 79 service_level: 0.95 SKU764: constraint: null - cost: 127 - init_stock: 196 + cost: 349 + init_stock: 68 max_stock: 1000 - price: 226 - sale_gamma: 49 + price: 596 + sale_gamma: 34 service_level: 0.95 SKU765: constraint: null - cost: 271 - init_stock: 200 + cost: 345 + init_stock: 52 max_stock: 1000 - price: 433 - sale_gamma: 25 + price: 472 + sale_gamma: 13 service_level: 0.95 SKU766: - constraint: null - cost: 230 - init_stock: 115 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 478 + init_stock: 140 max_stock: 1000 - price: 439 - sale_gamma: 23 + price: 855 + sale_gamma: 20 service_level: 0.95 SKU767: - constraint: G(low_profit -> low_stock_constraint) - cost: 91 - init_stock: 400 + constraint: null + cost: 95 + init_stock: 456 max_stock: 1000 - price: 152 - sale_gamma: 80 + price: 160 + sale_gamma: 76 service_level: 0.95 SKU768: - constraint: G(stock_constraint) - cost: 410 - init_stock: 700 + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 644 max_stock: 1000 - price: 811 - sale_gamma: 100 + price: 244 + sale_gamma: 92 service_level: 0.95 SKU769: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 324 - init_stock: 246 + constraint: null + cost: 24 + init_stock: 87 max_stock: 1000 - price: 502 - sale_gamma: 41 + price: 43 + sale_gamma: 29 service_level: 0.95 SKU77: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 321 - init_stock: 170 + constraint: null + cost: 409 + init_stock: 144 max_stock: 1000 - price: 616 - sale_gamma: 34 + price: 687 + sale_gamma: 72 service_level: 0.95 SKU770: constraint: null - cost: 288 - init_stock: 336 + cost: 150 + init_stock: 186 max_stock: 1000 - price: 498 - sale_gamma: 48 + price: 166 + sale_gamma: 62 service_level: 0.95 SKU771: - constraint: G(low_profit -> low_stock_constraint) - cost: 53 - init_stock: 60 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 101 + init_stock: 35 max_stock: 1000 - price: 90 - sale_gamma: 10 + price: 182 + sale_gamma: 7 service_level: 0.95 SKU772: - constraint: null - cost: 205 - init_stock: 165 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 36 max_stock: 1000 - price: 383 - sale_gamma: 55 + price: 281 + sale_gamma: 6 service_level: 0.95 SKU773: constraint: null - cost: 69 - init_stock: 108 + cost: 84 + init_stock: 368 max_stock: 1000 - price: 128 - sale_gamma: 27 + price: 117 + sale_gamma: 92 service_level: 0.95 SKU774: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 500 - init_stock: 480 + constraint: null + cost: 447 + init_stock: 118 max_stock: 1000 - price: 585 - sale_gamma: 96 + price: 746 + sale_gamma: 59 service_level: 0.95 SKU775: constraint: null - cost: 366 - init_stock: 301 + cost: 175 + init_stock: 688 max_stock: 1000 - price: 600 - sale_gamma: 43 + price: 192 + sale_gamma: 86 service_level: 0.95 SKU776: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 464 - init_stock: 40 + cost: 103 + init_stock: 425 max_stock: 1000 - price: 807 - sale_gamma: 8 + price: 172 + sale_gamma: 85 service_level: 0.95 SKU777: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 493 - init_stock: 324 + constraint: null + cost: 292 + init_stock: 171 max_stock: 1000 - price: 709 - sale_gamma: 54 + price: 347 + sale_gamma: 57 service_level: 0.95 SKU778: constraint: null - cost: 386 - init_stock: 125 + cost: 203 + init_stock: 40 max_stock: 1000 - price: 706 - sale_gamma: 25 + price: 288 + sale_gamma: 8 service_level: 0.95 SKU779: - constraint: null - cost: 149 - init_stock: 310 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 117 + init_stock: 215 max_stock: 1000 - price: 251 - sale_gamma: 62 + price: 186 + sale_gamma: 43 service_level: 0.95 SKU78: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 231 - init_stock: 180 + constraint: null + cost: 234 + init_stock: 91 max_stock: 1000 - price: 277 - sale_gamma: 45 + price: 400 + sale_gamma: 13 service_level: 0.95 SKU780: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 271 - init_stock: 210 + constraint: G(low_profit -> low_stock_constraint) + cost: 69 + init_stock: 410 max_stock: 1000 - price: 341 - sale_gamma: 35 + price: 102 + sale_gamma: 82 service_level: 0.95 SKU781: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 84 - init_stock: 174 + constraint: null + cost: 372 + init_stock: 144 max_stock: 1000 - price: 119 - sale_gamma: 58 + price: 528 + sale_gamma: 36 service_level: 0.95 SKU782: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 145 - init_stock: 350 + constraint: G(low_profit -> low_stock_constraint) + cost: 27 + init_stock: 70 max_stock: 1000 - price: 227 - sale_gamma: 70 + price: 35 + sale_gamma: 10 service_level: 0.95 SKU783: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 191 - init_stock: 165 + constraint: null + cost: 87 + init_stock: 324 max_stock: 1000 - price: 351 - sale_gamma: 33 + price: 139 + sale_gamma: 81 service_level: 0.95 SKU784: - constraint: G(low_profit -> low_stock_constraint) - cost: 34 - init_stock: 108 + constraint: null + cost: 309 + init_stock: 312 max_stock: 1000 - price: 51 - sale_gamma: 18 + price: 577 + sale_gamma: 52 service_level: 0.95 SKU785: - constraint: G(stock_constraint) - cost: 368 - init_stock: 140 + constraint: null + cost: 191 + init_stock: 210 max_stock: 1000 - price: 644 - sale_gamma: 28 + price: 255 + sale_gamma: 70 service_level: 0.95 SKU786: - constraint: G(stock_constraint) - cost: 55 - init_stock: 92 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 91 + init_stock: 132 max_stock: 1000 - price: 76 - sale_gamma: 23 + price: 102 + sale_gamma: 22 service_level: 0.95 SKU787: constraint: null - cost: 368 - init_stock: 36 + cost: 360 + init_stock: 366 max_stock: 1000 - price: 415 - sale_gamma: 6 + price: 658 + sale_gamma: 61 service_level: 0.95 SKU788: - constraint: null - cost: 257 - init_stock: 70 + constraint: G(stock_constraint) + cost: 351 + init_stock: 112 max_stock: 1000 - price: 514 - sale_gamma: 10 + price: 417 + sale_gamma: 28 service_level: 0.95 SKU789: - constraint: null - cost: 218 - init_stock: 340 + constraint: G(stock_constraint) + cost: 153 + init_stock: 232 max_stock: 1000 - price: 401 - sale_gamma: 85 + price: 298 + sale_gamma: 58 service_level: 0.95 SKU79: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 436 - init_stock: 511 + cost: 245 + init_stock: 66 max_stock: 1000 - price: 776 - sale_gamma: 73 + price: 276 + sale_gamma: 11 service_level: 0.95 SKU790: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 429 - init_stock: 212 + constraint: null + cost: 417 + init_stock: 330 max_stock: 1000 - price: 557 - sale_gamma: 53 + price: 704 + sale_gamma: 66 service_level: 0.95 SKU791: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 55 - init_stock: 365 + constraint: G(stock_constraint) + cost: 134 + init_stock: 56 max_stock: 1000 - price: 98 - sale_gamma: 73 + price: 155 + sale_gamma: 28 service_level: 0.95 SKU792: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 80 - init_stock: 560 + constraint: G(low_profit -> low_stock_constraint) + cost: 313 + init_stock: 84 max_stock: 1000 - price: 138 - sale_gamma: 70 + price: 494 + sale_gamma: 21 service_level: 0.95 SKU793: - constraint: G(low_profit -> low_stock_constraint) - cost: 227 - init_stock: 66 + constraint: null + cost: 195 + init_stock: 532 max_stock: 1000 - price: 399 - sale_gamma: 11 + price: 282 + sale_gamma: 76 service_level: 0.95 SKU794: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 361 - init_stock: 196 + constraint: null + cost: 325 + init_stock: 400 max_stock: 1000 - price: 573 - sale_gamma: 28 + price: 454 + sale_gamma: 80 service_level: 0.95 SKU795: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 200 - init_stock: 192 + constraint: null + cost: 276 + init_stock: 288 max_stock: 1000 - price: 390 + price: 549 sale_gamma: 48 service_level: 0.95 SKU796: constraint: null - cost: 360 - init_stock: 28 + cost: 447 + init_stock: 294 max_stock: 1000 - price: 468 - sale_gamma: 7 + price: 885 + sale_gamma: 49 service_level: 0.95 SKU797: - constraint: null - cost: 37 - init_stock: 174 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 100 + init_stock: 234 max_stock: 1000 - price: 56 - sale_gamma: 87 + price: 119 + sale_gamma: 39 service_level: 0.95 SKU798: constraint: null - cost: 124 - init_stock: 380 + cost: 426 + init_stock: 180 max_stock: 1000 - price: 226 - sale_gamma: 95 + price: 630 + sale_gamma: 30 service_level: 0.95 SKU799: - constraint: G(low_profit -> low_stock_constraint) - cost: 93 - init_stock: 108 + constraint: null + cost: 63 + init_stock: 21 max_stock: 1000 - price: 144 - sale_gamma: 18 + price: 78 + sale_gamma: 7 service_level: 0.95 SKU8: - constraint: null - cost: 320 - init_stock: 96 + constraint: G(stock_constraint) + cost: 125 + init_stock: 252 max_stock: 1000 - price: 396 - sale_gamma: 24 + price: 196 + sale_gamma: 63 service_level: 0.95 SKU80: constraint: G(stock_constraint) - cost: 316 - init_stock: 126 + cost: 163 + init_stock: 420 max_stock: 1000 - price: 376 - sale_gamma: 42 + price: 270 + sale_gamma: 70 service_level: 0.95 SKU800: - constraint: G(stock_constraint) - cost: 264 - init_stock: 330 + constraint: G(low_profit -> low_stock_constraint) + cost: 298 + init_stock: 470 max_stock: 1000 - price: 496 - sale_gamma: 66 + price: 455 + sale_gamma: 94 service_level: 0.95 SKU801: - constraint: null - cost: 62 - init_stock: 400 + constraint: G(low_profit -> low_stock_constraint) + cost: 389 + init_stock: 216 max_stock: 1000 - price: 107 - sale_gamma: 100 + price: 672 + sale_gamma: 36 service_level: 0.95 SKU802: - constraint: null - cost: 289 - init_stock: 33 + constraint: G(stock_constraint) + cost: 173 + init_stock: 310 max_stock: 1000 - price: 361 - sale_gamma: 11 + price: 281 + sale_gamma: 62 service_level: 0.95 SKU803: constraint: null - cost: 485 - init_stock: 244 + cost: 272 + init_stock: 95 max_stock: 1000 - price: 669 - sale_gamma: 61 + price: 544 + sale_gamma: 19 service_level: 0.95 SKU804: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 126 - init_stock: 406 + constraint: null + cost: 390 + init_stock: 188 max_stock: 1000 - price: 143 - sale_gamma: 58 + price: 491 + sale_gamma: 47 service_level: 0.95 SKU805: constraint: null - cost: 227 - init_stock: 365 + cost: 61 + init_stock: 132 max_stock: 1000 - price: 274 - sale_gamma: 73 + price: 118 + sale_gamma: 33 service_level: 0.95 SKU806: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 59 - init_stock: 68 + constraint: null + cost: 158 + init_stock: 156 max_stock: 1000 - price: 86 - sale_gamma: 34 + price: 186 + sale_gamma: 52 service_level: 0.95 SKU807: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 468 - init_stock: 375 + constraint: null + cost: 453 + init_stock: 182 max_stock: 1000 - price: 936 - sale_gamma: 75 + price: 656 + sale_gamma: 26 service_level: 0.95 SKU808: - constraint: G(low_profit -> low_stock_constraint) - cost: 161 - init_stock: 432 + constraint: G(stock_constraint) + cost: 249 + init_stock: 182 max_stock: 1000 - price: 301 - sale_gamma: 54 + price: 435 + sale_gamma: 91 service_level: 0.95 SKU809: constraint: null - cost: 53 - init_stock: 272 + cost: 55 + init_stock: 390 max_stock: 1000 - price: 58 - sale_gamma: 68 + price: 69 + sale_gamma: 65 service_level: 0.95 SKU81: constraint: G(low_profit -> low_stock_constraint) - cost: 192 - init_stock: 196 + cost: 182 + init_stock: 528 max_stock: 1000 - price: 243 - sale_gamma: 98 + price: 223 + sale_gamma: 66 service_level: 0.95 SKU810: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 451 - init_stock: 264 + cost: 276 + init_stock: 42 max_stock: 1000 - price: 559 - sale_gamma: 44 + price: 322 + sale_gamma: 6 service_level: 0.95 SKU811: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 320 - init_stock: 64 + cost: 254 + init_stock: 148 max_stock: 1000 - price: 419 - sale_gamma: 16 + price: 322 + sale_gamma: 37 service_level: 0.95 SKU812: constraint: null - cost: 293 - init_stock: 105 + cost: 252 + init_stock: 320 max_stock: 1000 - price: 536 - sale_gamma: 15 + price: 337 + sale_gamma: 80 service_level: 0.95 SKU813: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 39 - init_stock: 474 + constraint: G(stock_constraint) + cost: 253 + init_stock: 49 max_stock: 1000 - price: 66 - sale_gamma: 79 + price: 333 + sale_gamma: 7 service_level: 0.95 SKU814: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + constraint: null cost: 485 - init_stock: 320 + init_stock: 194 max_stock: 1000 - price: 843 - sale_gamma: 64 + price: 921 + sale_gamma: 97 service_level: 0.95 SKU815: - constraint: G(low_profit -> low_stock_constraint) - cost: 53 - init_stock: 144 + constraint: null + cost: 390 + init_stock: 168 max_stock: 1000 - price: 102 - sale_gamma: 36 + price: 429 + sale_gamma: 24 service_level: 0.95 SKU816: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 154 - init_stock: 112 + constraint: null + cost: 152 + init_stock: 455 max_stock: 1000 - price: 231 - sale_gamma: 56 + price: 174 + sale_gamma: 91 service_level: 0.95 SKU817: - constraint: G(stock_constraint) - cost: 262 - init_stock: 190 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 227 + init_stock: 25 max_stock: 1000 - price: 372 - sale_gamma: 95 + price: 401 + sale_gamma: 5 service_level: 0.95 SKU818: - constraint: G(stock_constraint) - cost: 89 - init_stock: 456 + constraint: null + cost: 354 + init_stock: 215 max_stock: 1000 - price: 164 - sale_gamma: 76 + price: 534 + sale_gamma: 43 service_level: 0.95 SKU819: - constraint: G(low_profit -> low_stock_constraint) - cost: 328 - init_stock: 270 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 302 + init_stock: 135 max_stock: 1000 - price: 400 - sale_gamma: 90 + price: 480 + sale_gamma: 27 service_level: 0.95 SKU82: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 237 - init_stock: 665 + constraint: null + cost: 290 + init_stock: 140 max_stock: 1000 - price: 324 - sale_gamma: 95 + price: 469 + sale_gamma: 28 service_level: 0.95 SKU820: constraint: null - cost: 461 - init_stock: 210 + cost: 264 + init_stock: 410 max_stock: 1000 - price: 645 - sale_gamma: 30 + price: 464 + sale_gamma: 82 service_level: 0.95 SKU821: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 399 - init_stock: 166 + constraint: G(stock_constraint) + cost: 99 + init_stock: 207 max_stock: 1000 - price: 570 - sale_gamma: 83 + price: 151 + sale_gamma: 69 service_level: 0.95 SKU822: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 60 - init_stock: 360 + constraint: null + cost: 136 + init_stock: 490 max_stock: 1000 - price: 85 - sale_gamma: 72 + price: 266 + sale_gamma: 70 service_level: 0.95 SKU823: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 171 - init_stock: 405 + constraint: null + cost: 75 + init_stock: 84 max_stock: 1000 - price: 198 - sale_gamma: 81 + price: 116 + sale_gamma: 28 service_level: 0.95 SKU824: - constraint: G(stock_constraint) - cost: 54 - init_stock: 183 + constraint: G(low_profit -> low_stock_constraint) + cost: 170 + init_stock: 420 max_stock: 1000 - price: 77 - sale_gamma: 61 + price: 200 + sale_gamma: 70 service_level: 0.95 SKU825: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 361 - init_stock: 252 + constraint: null + cost: 214 + init_stock: 45 max_stock: 1000 - price: 516 - sale_gamma: 42 + price: 359 + sale_gamma: 15 service_level: 0.95 SKU826: - constraint: null - cost: 409 - init_stock: 104 + constraint: G(stock_constraint) + cost: 386 + init_stock: 330 max_stock: 1000 - price: 548 - sale_gamma: 26 + price: 428 + sale_gamma: 55 service_level: 0.95 SKU827: - constraint: null - cost: 495 - init_stock: 390 + constraint: G(stock_constraint) + cost: 148 + init_stock: 448 max_stock: 1000 - price: 861 - sale_gamma: 78 + price: 208 + sale_gamma: 64 service_level: 0.95 SKU828: - constraint: G(low_profit -> low_stock_constraint) - cost: 97 - init_stock: 270 + constraint: null + cost: 400 + init_stock: 276 max_stock: 1000 - price: 130 - sale_gamma: 45 + price: 627 + sale_gamma: 69 service_level: 0.95 SKU829: constraint: null - cost: 301 - init_stock: 250 + cost: 61 + init_stock: 370 max_stock: 1000 - price: 376 - sale_gamma: 50 + price: 83 + sale_gamma: 74 service_level: 0.95 SKU83: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 365 - init_stock: 472 + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 68 max_stock: 1000 - price: 719 - sale_gamma: 59 + price: 535 + sale_gamma: 17 service_level: 0.95 SKU830: - constraint: null - cost: 215 - init_stock: 52 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 167 + init_stock: 144 max_stock: 1000 - price: 408 - sale_gamma: 13 + price: 252 + sale_gamma: 24 service_level: 0.95 SKU831: - constraint: null - cost: 324 - init_stock: 350 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 116 max_stock: 1000 - price: 392 - sale_gamma: 50 + price: 353 + sale_gamma: 29 service_level: 0.95 SKU832: constraint: null - cost: 118 - init_stock: 558 + cost: 33 + init_stock: 164 max_stock: 1000 - price: 200 - sale_gamma: 93 + price: 48 + sale_gamma: 82 service_level: 0.95 SKU833: - constraint: null - cost: 435 - init_stock: 60 + constraint: G(low_profit -> low_stock_constraint) + cost: 400 + init_stock: 392 max_stock: 1000 - price: 639 - sale_gamma: 30 + price: 756 + sale_gamma: 98 service_level: 0.95 SKU834: - constraint: null - cost: 402 - init_stock: 380 + constraint: G(low_profit -> low_stock_constraint) + cost: 422 + init_stock: 10 max_stock: 1000 - price: 623 - sale_gamma: 95 + price: 662 + sale_gamma: 5 service_level: 0.95 SKU835: - constraint: G(low_profit -> low_stock_constraint) - cost: 216 - init_stock: 280 + constraint: null + cost: 440 + init_stock: 156 max_stock: 1000 - price: 241 - sale_gamma: 56 + price: 532 + sale_gamma: 52 service_level: 0.95 SKU836: - constraint: null - cost: 15 - init_stock: 444 - max_stock: 1000 - price: 19 - sale_gamma: 74 + constraint: G(stock_constraint) + cost: 323 + init_stock: 192 + max_stock: 1000 + price: 552 + sale_gamma: 96 service_level: 0.95 SKU837: constraint: null - cost: 62 - init_stock: 276 + cost: 373 + init_stock: 156 max_stock: 1000 - price: 113 - sale_gamma: 69 + price: 607 + sale_gamma: 26 service_level: 0.95 SKU838: constraint: null - cost: 135 - init_stock: 144 + cost: 456 + init_stock: 308 max_stock: 1000 - price: 183 - sale_gamma: 24 + price: 711 + sale_gamma: 77 service_level: 0.95 SKU839: constraint: null - cost: 485 - init_stock: 522 + cost: 473 + init_stock: 360 max_stock: 1000 - price: 606 - sale_gamma: 87 + price: 695 + sale_gamma: 60 service_level: 0.95 SKU84: - constraint: null - cost: 21 - init_stock: 492 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 318 + init_stock: 294 max_stock: 1000 - price: 32 - sale_gamma: 82 + price: 594 + sale_gamma: 42 service_level: 0.95 SKU840: - constraint: G(stock_constraint) - cost: 185 - init_stock: 120 + constraint: G(low_profit -> low_stock_constraint) + cost: 266 + init_stock: 150 max_stock: 1000 - price: 349 - sale_gamma: 30 + price: 436 + sale_gamma: 50 service_level: 0.95 SKU841: constraint: G(stock_constraint) - cost: 211 - init_stock: 588 + cost: 285 + init_stock: 595 max_stock: 1000 - price: 276 - sale_gamma: 98 + price: 538 + sale_gamma: 85 service_level: 0.95 SKU842: - constraint: null - cost: 251 - init_stock: 70 + constraint: G(low_profit -> low_stock_constraint) + cost: 41 + init_stock: 252 max_stock: 1000 - price: 401 - sale_gamma: 10 + price: 70 + sale_gamma: 84 service_level: 0.95 SKU843: constraint: null - cost: 119 - init_stock: 183 + cost: 360 + init_stock: 432 max_stock: 1000 - price: 217 - sale_gamma: 61 + price: 694 + sale_gamma: 72 service_level: 0.95 SKU844: - constraint: null - cost: 119 - init_stock: 576 + constraint: G(low_profit -> low_stock_constraint) + cost: 51 + init_stock: 474 max_stock: 1000 - price: 143 - sale_gamma: 96 + price: 63 + sale_gamma: 79 service_level: 0.95 SKU845: - constraint: G(stock_constraint) - cost: 489 - init_stock: 375 + constraint: G(low_profit -> low_stock_constraint) + cost: 288 + init_stock: 176 max_stock: 1000 - price: 572 - sale_gamma: 75 + price: 558 + sale_gamma: 22 service_level: 0.95 SKU846: constraint: null - cost: 500 - init_stock: 70 + cost: 485 + init_stock: 234 max_stock: 1000 price: 940 - sale_gamma: 10 + sale_gamma: 39 service_level: 0.95 SKU847: - constraint: null - cost: 304 - init_stock: 736 + constraint: G(low_profit -> low_stock_constraint) + cost: 388 + init_stock: 546 max_stock: 1000 - price: 361 - sale_gamma: 92 + price: 519 + sale_gamma: 91 service_level: 0.95 SKU848: constraint: null - cost: 242 - init_stock: 270 + cost: 306 + init_stock: 184 max_stock: 1000 - price: 285 - sale_gamma: 90 + price: 358 + sale_gamma: 46 service_level: 0.95 SKU849: - constraint: null - cost: 33 - init_stock: 576 + constraint: G(low_profit -> low_stock_constraint) + cost: 320 + init_stock: 160 max_stock: 1000 - price: 61 - sale_gamma: 96 + price: 425 + sale_gamma: 40 service_level: 0.95 SKU85: - constraint: G(stock_constraint) - cost: 156 - init_stock: 124 + constraint: G(low_profit -> low_stock_constraint) + cost: 442 + init_stock: 400 max_stock: 1000 - price: 307 - sale_gamma: 62 + price: 583 + sale_gamma: 100 service_level: 0.95 SKU850: - constraint: null - cost: 449 - init_stock: 306 + constraint: G(stock_constraint) + cost: 183 + init_stock: 342 max_stock: 1000 - price: 642 - sale_gamma: 51 + price: 206 + sale_gamma: 57 service_level: 0.95 SKU851: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 417 - init_stock: 92 + constraint: null + cost: 53 + init_stock: 384 max_stock: 1000 - price: 533 - sale_gamma: 46 + price: 96 + sale_gamma: 64 service_level: 0.95 SKU852: - constraint: G(low_profit -> low_stock_constraint) - cost: 274 - init_stock: 260 + constraint: null + cost: 306 + init_stock: 162 max_stock: 1000 - price: 358 - sale_gamma: 52 + price: 483 + sale_gamma: 54 service_level: 0.95 SKU853: - constraint: null - cost: 65 - init_stock: 270 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 386 + init_stock: 280 max_stock: 1000 - price: 120 - sale_gamma: 54 + price: 443 + sale_gamma: 56 service_level: 0.95 SKU854: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 224 - init_stock: 552 + cost: 212 + init_stock: 390 max_stock: 1000 - price: 396 - sale_gamma: 92 + price: 290 + sale_gamma: 65 service_level: 0.95 SKU855: - constraint: G(stock_constraint) - cost: 125 - init_stock: 21 + constraint: null + cost: 76 + init_stock: 432 max_stock: 1000 - price: 158 - sale_gamma: 7 + price: 114 + sale_gamma: 72 service_level: 0.95 SKU856: - constraint: null - cost: 192 - init_stock: 168 + constraint: G(low_profit -> low_stock_constraint) + cost: 380 + init_stock: 224 max_stock: 1000 - price: 280 - sale_gamma: 42 + price: 627 + sale_gamma: 32 service_level: 0.95 SKU857: constraint: G(stock_constraint) - cost: 180 - init_stock: 108 + cost: 462 + init_stock: 500 max_stock: 1000 - price: 289 - sale_gamma: 18 + price: 646 + sale_gamma: 100 service_level: 0.95 SKU858: - constraint: G(low_profit -> low_stock_constraint) - cost: 393 - init_stock: 553 + constraint: null + cost: 80 + init_stock: 120 max_stock: 1000 - price: 581 - sale_gamma: 79 + price: 158 + sale_gamma: 24 service_level: 0.95 SKU859: - constraint: null - cost: 259 - init_stock: 434 + constraint: G(low_profit -> low_stock_constraint) + cost: 215 + init_stock: 224 max_stock: 1000 - price: 440 - sale_gamma: 62 + price: 298 + sale_gamma: 28 service_level: 0.95 SKU86: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 322 - init_stock: 18 + constraint: null + cost: 386 + init_stock: 595 max_stock: 1000 - price: 598 - sale_gamma: 6 + price: 447 + sale_gamma: 85 service_level: 0.95 SKU860: - constraint: null - cost: 287 - init_stock: 90 + constraint: G(low_profit -> low_stock_constraint) + cost: 313 + init_stock: 105 max_stock: 1000 - price: 556 - sale_gamma: 30 + price: 435 + sale_gamma: 15 service_level: 0.95 SKU861: constraint: null - cost: 20 - init_stock: 56 + cost: 477 + init_stock: 52 max_stock: 1000 - price: 38 - sale_gamma: 28 + price: 944 + sale_gamma: 13 service_level: 0.95 SKU862: constraint: null - cost: 86 - init_stock: 92 + cost: 240 + init_stock: 64 max_stock: 1000 - price: 102 - sale_gamma: 23 + price: 412 + sale_gamma: 16 service_level: 0.95 SKU863: constraint: null - cost: 84 - init_stock: 553 + cost: 470 + init_stock: 372 max_stock: 1000 - price: 145 - sale_gamma: 79 + price: 911 + sale_gamma: 93 service_level: 0.95 SKU864: constraint: null - cost: 287 - init_stock: 258 + cost: 203 + init_stock: 114 max_stock: 1000 - price: 464 - sale_gamma: 43 + price: 341 + sale_gamma: 19 service_level: 0.95 SKU865: constraint: G(low_profit -> low_stock_constraint) - cost: 197 - init_stock: 96 + cost: 144 + init_stock: 152 max_stock: 1000 - price: 344 - sale_gamma: 24 + price: 253 + sale_gamma: 19 service_level: 0.95 SKU866: - constraint: G(stock_constraint) - cost: 393 - init_stock: 25 + constraint: null + cost: 172 + init_stock: 264 max_stock: 1000 - price: 746 - sale_gamma: 5 + price: 201 + sale_gamma: 88 service_level: 0.95 SKU867: - constraint: G(stock_constraint) - cost: 231 - init_stock: 175 + constraint: G(low_profit -> low_stock_constraint) + cost: 499 + init_stock: 500 max_stock: 1000 - price: 374 - sale_gamma: 35 + price: 698 + sale_gamma: 100 service_level: 0.95 SKU868: - constraint: null - cost: 467 - init_stock: 396 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 41 + init_stock: 150 max_stock: 1000 - price: 826 - sale_gamma: 66 + price: 56 + sale_gamma: 50 service_level: 0.95 SKU869: constraint: null - cost: 114 - init_stock: 368 + cost: 97 + init_stock: 388 max_stock: 1000 - price: 204 - sale_gamma: 92 + price: 111 + sale_gamma: 97 service_level: 0.95 SKU87: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 326 - init_stock: 45 + constraint: null + cost: 368 + init_stock: 207 max_stock: 1000 - price: 361 - sale_gamma: 9 + price: 563 + sale_gamma: 69 service_level: 0.95 SKU870: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 44 - init_stock: 66 + cost: 281 + init_stock: 335 max_stock: 1000 - price: 69 - sale_gamma: 11 + price: 446 + sale_gamma: 67 service_level: 0.95 SKU871: constraint: null - cost: 404 - init_stock: 252 + cost: 101 + init_stock: 396 max_stock: 1000 - price: 767 - sale_gamma: 84 + price: 112 + sale_gamma: 99 service_level: 0.95 SKU872: constraint: null - cost: 291 - init_stock: 270 + cost: 133 + init_stock: 160 max_stock: 1000 - price: 369 - sale_gamma: 54 + price: 195 + sale_gamma: 32 service_level: 0.95 SKU873: - constraint: G(stock_constraint) - cost: 334 - init_stock: 84 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 423 + init_stock: 155 max_stock: 1000 - price: 407 - sale_gamma: 21 + price: 829 + sale_gamma: 31 service_level: 0.95 SKU874: - constraint: null - cost: 364 - init_stock: 336 + constraint: G(low_profit -> low_stock_constraint) + cost: 469 + init_stock: 360 max_stock: 1000 - price: 469 - sale_gamma: 48 + price: 689 + sale_gamma: 60 service_level: 0.95 SKU875: - constraint: G(low_profit -> low_stock_constraint) - cost: 276 - init_stock: 609 + constraint: G(stock_constraint) + cost: 51 + init_stock: 138 max_stock: 1000 - price: 353 - sale_gamma: 87 + price: 57 + sale_gamma: 23 service_level: 0.95 SKU876: constraint: null - cost: 342 - init_stock: 420 + cost: 303 + init_stock: 427 max_stock: 1000 - price: 608 - sale_gamma: 70 + price: 427 + sale_gamma: 61 service_level: 0.95 SKU877: constraint: null - cost: 398 - init_stock: 486 + cost: 40 + init_stock: 245 max_stock: 1000 - price: 509 - sale_gamma: 81 + price: 70 + sale_gamma: 35 service_level: 0.95 SKU878: constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 272 + cost: 27 + init_stock: 476 max_stock: 1000 - price: 148 + price: 32 sale_gamma: 68 service_level: 0.95 SKU879: constraint: null - cost: 343 - init_stock: 48 + cost: 150 + init_stock: 300 max_stock: 1000 - price: 511 - sale_gamma: 8 + price: 198 + sale_gamma: 100 service_level: 0.95 SKU88: constraint: G(low_profit -> low_stock_constraint) - cost: 244 - init_stock: 176 + cost: 471 + init_stock: 36 max_stock: 1000 - price: 436 - sale_gamma: 22 + price: 824 + sale_gamma: 12 service_level: 0.95 SKU880: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 132 + constraint: null + cost: 433 + init_stock: 155 max_stock: 1000 - price: 922 - sale_gamma: 44 + price: 532 + sale_gamma: 31 service_level: 0.95 SKU881: constraint: null - cost: 367 - init_stock: 536 + cost: 431 + init_stock: 420 max_stock: 1000 - price: 598 - sale_gamma: 67 + price: 560 + sale_gamma: 70 service_level: 0.95 SKU882: - constraint: null - cost: 86 - init_stock: 189 + constraint: G(stock_constraint) + cost: 194 + init_stock: 80 max_stock: 1000 - price: 141 - sale_gamma: 63 + price: 314 + sale_gamma: 16 service_level: 0.95 SKU883: - constraint: G(stock_constraint) - cost: 190 - init_stock: 230 + constraint: null + cost: 284 + init_stock: 152 max_stock: 1000 - price: 302 - sale_gamma: 46 + price: 479 + sale_gamma: 38 service_level: 0.95 SKU884: - constraint: null - cost: 200 - init_stock: 116 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 139 + init_stock: 195 max_stock: 1000 - price: 366 - sale_gamma: 29 + price: 222 + sale_gamma: 39 service_level: 0.95 SKU885: - constraint: null - cost: 133 - init_stock: 184 + constraint: G(low_profit -> low_stock_constraint) + cost: 49 + init_stock: 189 max_stock: 1000 - price: 251 - sale_gamma: 46 + price: 58 + sale_gamma: 27 service_level: 0.95 SKU886: - constraint: null - cost: 218 - init_stock: 96 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 584 max_stock: 1000 - price: 370 - sale_gamma: 16 + price: 602 + sale_gamma: 73 service_level: 0.95 SKU887: - constraint: G(low_profit -> low_stock_constraint) - cost: 215 - init_stock: 352 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 266 + init_stock: 174 max_stock: 1000 - price: 331 - sale_gamma: 88 + price: 494 + sale_gamma: 87 service_level: 0.95 SKU888: - constraint: null - cost: 439 - init_stock: 201 + constraint: G(low_profit -> low_stock_constraint) + cost: 143 + init_stock: 45 max_stock: 1000 - price: 566 - sale_gamma: 67 + price: 220 + sale_gamma: 9 service_level: 0.95 SKU889: constraint: null - cost: 105 - init_stock: 476 + cost: 101 + init_stock: 147 max_stock: 1000 - price: 157 - sale_gamma: 68 + price: 199 + sale_gamma: 21 service_level: 0.95 SKU89: - constraint: G(low_profit -> low_stock_constraint) - cost: 223 - init_stock: 21 + constraint: null + cost: 138 + init_stock: 744 max_stock: 1000 - price: 352 - sale_gamma: 7 + price: 269 + sale_gamma: 93 service_level: 0.95 SKU890: - constraint: null - cost: 183 - init_stock: 216 + constraint: G(low_profit -> low_stock_constraint) + cost: 161 + init_stock: 600 max_stock: 1000 - price: 201 - sale_gamma: 36 + price: 247 + sale_gamma: 100 service_level: 0.95 SKU891: constraint: null - cost: 85 - init_stock: 132 + cost: 356 + init_stock: 176 max_stock: 1000 - price: 110 - sale_gamma: 44 + price: 615 + sale_gamma: 22 service_level: 0.95 SKU892: constraint: null - cost: 41 - init_stock: 252 + cost: 313 + init_stock: 552 max_stock: 1000 - price: 76 - sale_gamma: 84 + price: 516 + sale_gamma: 92 service_level: 0.95 SKU893: constraint: null - cost: 135 - init_stock: 154 + cost: 229 + init_stock: 594 max_stock: 1000 - price: 238 - sale_gamma: 22 + price: 302 + sale_gamma: 99 service_level: 0.95 SKU894: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 405 - init_stock: 420 + constraint: null + cost: 129 + init_stock: 222 max_stock: 1000 - price: 579 - sale_gamma: 70 + price: 176 + sale_gamma: 74 service_level: 0.95 SKU895: constraint: null - cost: 320 - init_stock: 186 + cost: 230 + init_stock: 39 max_stock: 1000 - price: 480 - sale_gamma: 62 + price: 301 + sale_gamma: 13 service_level: 0.95 SKU896: - constraint: G(stock_constraint) - cost: 451 - init_stock: 162 + constraint: null + cost: 289 + init_stock: 400 max_stock: 1000 - price: 726 - sale_gamma: 54 + price: 465 + sale_gamma: 80 service_level: 0.95 SKU897: - constraint: G(stock_constraint) - cost: 141 - init_stock: 480 + constraint: null + cost: 393 + init_stock: 432 max_stock: 1000 - price: 211 - sale_gamma: 96 + price: 640 + sale_gamma: 72 service_level: 0.95 SKU898: - constraint: null - cost: 220 - init_stock: 712 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 477 + init_stock: 44 max_stock: 1000 - price: 426 - sale_gamma: 89 + price: 615 + sale_gamma: 11 service_level: 0.95 SKU899: - constraint: G(low_profit -> low_stock_constraint) - cost: 113 - init_stock: 228 + constraint: null + cost: 233 + init_stock: 240 max_stock: 1000 - price: 166 - sale_gamma: 38 + price: 309 + sale_gamma: 48 service_level: 0.95 SKU9: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 483 - init_stock: 168 + constraint: null + cost: 166 + init_stock: 384 max_stock: 1000 - price: 811 - sale_gamma: 42 + price: 247 + sale_gamma: 96 service_level: 0.95 SKU90: - constraint: G(low_profit -> low_stock_constraint) - cost: 217 - init_stock: 66 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 454 + init_stock: 204 max_stock: 1000 - price: 371 - sale_gamma: 11 + price: 726 + sale_gamma: 51 service_level: 0.95 SKU900: constraint: null - cost: 218 - init_stock: 280 + cost: 158 + init_stock: 165 max_stock: 1000 - price: 427 - sale_gamma: 70 + price: 263 + sale_gamma: 55 service_level: 0.95 SKU901: - constraint: G(low_profit -> low_stock_constraint) - cost: 335 - init_stock: 332 + constraint: G(stock_constraint) + cost: 215 + init_stock: 203 max_stock: 1000 - price: 592 - sale_gamma: 83 + price: 320 + sale_gamma: 29 service_level: 0.95 SKU902: - constraint: null - cost: 118 - init_stock: 495 + constraint: G(low_profit -> low_stock_constraint) + cost: 125 + init_stock: 560 max_stock: 1000 - price: 193 - sale_gamma: 99 + price: 205 + sale_gamma: 80 service_level: 0.95 SKU903: - constraint: G(stock_constraint) - cost: 164 - init_stock: 77 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 357 + init_stock: 266 max_stock: 1000 - price: 296 - sale_gamma: 11 + price: 517 + sale_gamma: 38 service_level: 0.95 SKU904: - constraint: null - cost: 317 - init_stock: 180 + constraint: G(low_profit -> low_stock_constraint) + cost: 496 + init_stock: 255 max_stock: 1000 - price: 595 - sale_gamma: 30 + price: 605 + sale_gamma: 51 service_level: 0.95 SKU905: constraint: null - cost: 462 - init_stock: 155 + cost: 249 + init_stock: 217 max_stock: 1000 - price: 831 + price: 383 sale_gamma: 31 service_level: 0.95 SKU906: - constraint: null - cost: 249 - init_stock: 180 + constraint: G(low_profit -> low_stock_constraint) + cost: 166 + init_stock: 156 max_stock: 1000 - price: 313 - sale_gamma: 45 + price: 273 + sale_gamma: 52 service_level: 0.95 SKU907: - constraint: G(low_profit -> low_stock_constraint) - cost: 206 - init_stock: 532 + constraint: null + cost: 22 + init_stock: 498 max_stock: 1000 - price: 230 - sale_gamma: 76 + price: 33 + sale_gamma: 83 service_level: 0.95 SKU908: constraint: null - cost: 419 - init_stock: 315 + cost: 408 + init_stock: 546 max_stock: 1000 - price: 599 - sale_gamma: 63 + price: 595 + sale_gamma: 78 service_level: 0.95 SKU909: - constraint: G(low_profit -> low_stock_constraint) - cost: 414 - init_stock: 128 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 482 + init_stock: 480 max_stock: 1000 - price: 811 - sale_gamma: 64 + price: 703 + sale_gamma: 96 service_level: 0.95 SKU91: - constraint: null - cost: 254 - init_stock: 630 + constraint: G(low_profit -> low_stock_constraint) + cost: 303 + init_stock: 48 max_stock: 1000 - price: 355 - sale_gamma: 90 + price: 463 + sale_gamma: 16 service_level: 0.95 SKU910: - constraint: G(stock_constraint) - cost: 371 - init_stock: 312 + constraint: null + cost: 226 + init_stock: 132 max_stock: 1000 - price: 552 - sale_gamma: 52 + price: 350 + sale_gamma: 33 service_level: 0.95 SKU911: constraint: null - cost: 421 - init_stock: 420 + cost: 461 + init_stock: 210 max_stock: 1000 - price: 509 - sale_gamma: 60 + price: 686 + sale_gamma: 70 service_level: 0.95 SKU912: constraint: null - cost: 164 - init_stock: 105 + cost: 236 + init_stock: 135 max_stock: 1000 - price: 295 - sale_gamma: 15 + price: 328 + sale_gamma: 27 service_level: 0.95 SKU913: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 20 - init_stock: 385 + constraint: null + cost: 322 + init_stock: 184 max_stock: 1000 - price: 36 - sale_gamma: 77 + price: 518 + sale_gamma: 46 service_level: 0.95 SKU914: constraint: null - cost: 144 - init_stock: 138 + cost: 272 + init_stock: 434 max_stock: 1000 - price: 211 - sale_gamma: 46 + price: 446 + sale_gamma: 62 service_level: 0.95 SKU915: - constraint: null - cost: 214 - init_stock: 40 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 337 + init_stock: 280 max_stock: 1000 - price: 250 - sale_gamma: 10 + price: 545 + sale_gamma: 56 service_level: 0.95 SKU916: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 117 - init_stock: 336 + constraint: null + cost: 337 + init_stock: 534 max_stock: 1000 - price: 162 - sale_gamma: 48 + price: 647 + sale_gamma: 89 service_level: 0.95 SKU917: - constraint: null - cost: 212 - init_stock: 130 + constraint: G(low_profit -> low_stock_constraint) + cost: 258 + init_stock: 120 max_stock: 1000 - price: 383 - sale_gamma: 26 + price: 366 + sale_gamma: 30 service_level: 0.95 SKU918: - constraint: null - cost: 318 - init_stock: 392 + constraint: G(stock_constraint) + cost: 148 + init_stock: 330 max_stock: 1000 - price: 632 - sale_gamma: 49 + price: 224 + sale_gamma: 55 service_level: 0.95 SKU919: - constraint: null - cost: 59 - init_stock: 55 + constraint: G(stock_constraint) + cost: 393 + init_stock: 125 max_stock: 1000 - price: 69 - sale_gamma: 11 + price: 711 + sale_gamma: 25 service_level: 0.95 SKU92: - constraint: null - cost: 341 - init_stock: 244 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 252 max_stock: 1000 - price: 671 - sale_gamma: 61 + price: 466 + sale_gamma: 63 service_level: 0.95 SKU920: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 161 - init_stock: 120 + constraint: null + cost: 357 + init_stock: 344 max_stock: 1000 - price: 318 - sale_gamma: 24 + price: 696 + sale_gamma: 86 service_level: 0.95 SKU921: constraint: G(low_profit -> low_stock_constraint) - cost: 489 - init_stock: 176 + cost: 227 + init_stock: 198 max_stock: 1000 - price: 591 - sale_gamma: 88 + price: 419 + sale_gamma: 66 service_level: 0.95 SKU922: - constraint: G(low_profit -> low_stock_constraint) - cost: 35 - init_stock: 364 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 112 + init_stock: 201 max_stock: 1000 - price: 53 - sale_gamma: 91 + price: 123 + sale_gamma: 67 service_level: 0.95 SKU923: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 182 - init_stock: 40 + cost: 496 + init_stock: 348 max_stock: 1000 - price: 364 - sale_gamma: 8 + price: 828 + sale_gamma: 58 service_level: 0.95 SKU924: constraint: null - cost: 364 - init_stock: 184 + cost: 316 + init_stock: 425 max_stock: 1000 - price: 447 - sale_gamma: 46 + price: 423 + sale_gamma: 85 service_level: 0.95 SKU925: constraint: null - cost: 31 - init_stock: 475 + cost: 360 + init_stock: 90 max_stock: 1000 - price: 37 - sale_gamma: 95 + price: 716 + sale_gamma: 15 service_level: 0.95 SKU926: - constraint: null - cost: 472 - init_stock: 60 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 360 + init_stock: 469 max_stock: 1000 - price: 623 - sale_gamma: 10 + price: 475 + sale_gamma: 67 service_level: 0.95 SKU927: - constraint: null - cost: 143 - init_stock: 40 + constraint: G(stock_constraint) + cost: 260 + init_stock: 147 max_stock: 1000 - price: 258 - sale_gamma: 5 + price: 439 + sale_gamma: 21 service_level: 0.95 SKU928: - constraint: null - cost: 325 - init_stock: 185 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 491 + init_stock: 415 max_stock: 1000 - price: 454 - sale_gamma: 37 + price: 869 + sale_gamma: 83 service_level: 0.95 SKU929: - constraint: G(stock_constraint) - cost: 296 - init_stock: 304 + constraint: null + cost: 359 + init_stock: 800 max_stock: 1000 - price: 512 - sale_gamma: 76 + price: 470 + sale_gamma: 100 service_level: 0.95 SKU93: constraint: null - cost: 361 - init_stock: 504 + cost: 404 + init_stock: 268 max_stock: 1000 - price: 555 - sale_gamma: 84 + price: 557 + sale_gamma: 67 service_level: 0.95 SKU930: - constraint: G(stock_constraint) - cost: 257 - init_stock: 425 + constraint: G(low_profit -> low_stock_constraint) + cost: 198 + init_stock: 196 max_stock: 1000 - price: 449 - sale_gamma: 85 + price: 247 + sale_gamma: 28 service_level: 0.95 SKU931: constraint: null - cost: 156 - init_stock: 132 + cost: 71 + init_stock: 56 max_stock: 1000 - price: 196 - sale_gamma: 33 + price: 101 + sale_gamma: 14 service_level: 0.95 SKU932: - constraint: G(low_profit -> low_stock_constraint) - cost: 138 - init_stock: 396 + constraint: null + cost: 163 + init_stock: 264 max_stock: 1000 - price: 160 - sale_gamma: 99 + price: 283 + sale_gamma: 66 service_level: 0.95 SKU933: - constraint: G(low_profit -> low_stock_constraint) - cost: 284 - init_stock: 228 + constraint: null + cost: 113 + init_stock: 156 max_stock: 1000 - price: 460 - sale_gamma: 38 + price: 179 + sale_gamma: 78 service_level: 0.95 SKU934: - constraint: null - cost: 213 - init_stock: 86 + constraint: G(stock_constraint) + cost: 219 + init_stock: 469 max_stock: 1000 - price: 255 - sale_gamma: 43 + price: 346 + sale_gamma: 67 service_level: 0.95 SKU935: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 339 - init_stock: 54 + constraint: null + cost: 364 + init_stock: 376 max_stock: 1000 - price: 396 - sale_gamma: 27 + price: 527 + sale_gamma: 94 service_level: 0.95 SKU936: - constraint: null - cost: 170 - init_stock: 36 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 24 + init_stock: 20 max_stock: 1000 - price: 261 - sale_gamma: 9 + price: 41 + sale_gamma: 5 service_level: 0.95 SKU937: - constraint: null - cost: 123 - init_stock: 90 + constraint: G(stock_constraint) + cost: 135 + init_stock: 85 max_stock: 1000 - price: 202 - sale_gamma: 15 + price: 216 + sale_gamma: 17 service_level: 0.95 SKU938: - constraint: null - cost: 299 - init_stock: 364 + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 63 max_stock: 1000 - price: 442 - sale_gamma: 91 + price: 704 + sale_gamma: 21 service_level: 0.95 SKU939: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 267 - init_stock: 100 + cost: 173 + init_stock: 413 max_stock: 1000 - price: 469 - sale_gamma: 25 + price: 219 + sale_gamma: 59 service_level: 0.95 SKU94: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 446 - init_stock: 245 + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 235 max_stock: 1000 - price: 789 - sale_gamma: 49 + price: 261 + sale_gamma: 47 service_level: 0.95 SKU940: - constraint: G(stock_constraint) - cost: 475 - init_stock: 220 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 14 + init_stock: 279 max_stock: 1000 - price: 783 - sale_gamma: 55 + price: 24 + sale_gamma: 93 service_level: 0.95 SKU941: - constraint: null - cost: 202 - init_stock: 192 + constraint: G(stock_constraint) + cost: 80 + init_stock: 456 max_stock: 1000 - price: 363 - sale_gamma: 24 + price: 132 + sale_gamma: 57 service_level: 0.95 SKU942: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 408 - init_stock: 216 + cost: 202 + init_stock: 65 max_stock: 1000 - price: 505 - sale_gamma: 72 + price: 303 + sale_gamma: 13 service_level: 0.95 SKU943: constraint: null - cost: 189 - init_stock: 64 + cost: 138 + init_stock: 245 max_stock: 1000 - price: 238 - sale_gamma: 8 + price: 182 + sale_gamma: 49 service_level: 0.95 SKU944: constraint: null - cost: 494 - init_stock: 170 + cost: 196 + init_stock: 132 max_stock: 1000 - price: 602 - sale_gamma: 34 + price: 356 + sale_gamma: 44 service_level: 0.95 SKU945: constraint: null - cost: 237 - init_stock: 51 + cost: 141 + init_stock: 85 max_stock: 1000 - price: 455 + price: 176 sale_gamma: 17 service_level: 0.95 SKU946: constraint: null - cost: 345 + cost: 325 init_stock: 294 max_stock: 1000 - price: 610 - sale_gamma: 98 + price: 555 + sale_gamma: 49 service_level: 0.95 SKU947: - constraint: G(stock_constraint) - cost: 495 - init_stock: 324 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 338 + init_stock: 637 max_stock: 1000 - price: 975 - sale_gamma: 54 + price: 547 + sale_gamma: 91 service_level: 0.95 SKU948: constraint: null - cost: 314 - init_stock: 546 + cost: 425 + init_stock: 112 max_stock: 1000 - price: 433 - sale_gamma: 78 + price: 476 + sale_gamma: 28 service_level: 0.95 SKU949: constraint: G(stock_constraint) - cost: 453 - init_stock: 38 + cost: 309 + init_stock: 15 max_stock: 1000 - price: 602 - sale_gamma: 19 + price: 522 + sale_gamma: 5 service_level: 0.95 SKU95: constraint: null - cost: 25 - init_stock: 679 + cost: 136 + init_stock: 84 max_stock: 1000 - price: 34 - sale_gamma: 97 + price: 214 + sale_gamma: 21 service_level: 0.95 SKU950: - constraint: null - cost: 286 - init_stock: 345 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 102 + init_stock: 324 max_stock: 1000 - price: 551 - sale_gamma: 69 + price: 128 + sale_gamma: 54 service_level: 0.95 SKU951: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 167 - init_stock: 280 + constraint: G(low_profit -> low_stock_constraint) + cost: 75 + init_stock: 90 max_stock: 1000 - price: 225 - sale_gamma: 70 + price: 117 + sale_gamma: 18 service_level: 0.95 SKU952: - constraint: null - cost: 493 - init_stock: 205 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 156 + init_stock: 66 max_stock: 1000 - price: 818 - sale_gamma: 41 + price: 276 + sale_gamma: 11 service_level: 0.95 SKU953: constraint: null - cost: 472 - init_stock: 316 + cost: 138 + init_stock: 245 max_stock: 1000 - price: 920 - sale_gamma: 79 + price: 230 + sale_gamma: 35 service_level: 0.95 SKU954: constraint: null - cost: 287 - init_stock: 70 + cost: 296 + init_stock: 430 max_stock: 1000 - price: 401 - sale_gamma: 14 + price: 444 + sale_gamma: 86 service_level: 0.95 SKU955: - constraint: null - cost: 94 - init_stock: 90 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 55 + init_stock: 189 max_stock: 1000 - price: 139 - sale_gamma: 15 + price: 85 + sale_gamma: 63 service_level: 0.95 SKU956: constraint: null - cost: 157 - init_stock: 74 + cost: 282 + init_stock: 425 max_stock: 1000 - price: 229 - sale_gamma: 37 + price: 397 + sale_gamma: 85 service_level: 0.95 SKU957: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 304 - init_stock: 441 + constraint: null + cost: 305 + init_stock: 306 max_stock: 1000 - price: 577 - sale_gamma: 63 + price: 466 + sale_gamma: 51 service_level: 0.95 SKU958: - constraint: G(stock_constraint) - cost: 348 - init_stock: 275 + constraint: null + cost: 369 + init_stock: 462 max_stock: 1000 - price: 619 - sale_gamma: 55 + price: 704 + sale_gamma: 66 service_level: 0.95 SKU959: - constraint: G(low_profit -> low_stock_constraint) - cost: 442 - init_stock: 474 + constraint: null + cost: 81 + init_stock: 164 max_stock: 1000 - price: 667 - sale_gamma: 79 + price: 127 + sale_gamma: 82 service_level: 0.95 SKU96: - constraint: null - cost: 473 - init_stock: 98 + constraint: G(low_profit -> low_stock_constraint) + cost: 176 + init_stock: 264 max_stock: 1000 - price: 884 - sale_gamma: 49 + price: 329 + sale_gamma: 44 service_level: 0.95 SKU960: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 456 + constraint: null + cost: 147 + init_stock: 42 max_stock: 1000 - price: 44 - sale_gamma: 76 + price: 235 + sale_gamma: 6 service_level: 0.95 SKU961: constraint: null - cost: 282 - init_stock: 384 + cost: 264 + init_stock: 176 max_stock: 1000 - price: 349 - sale_gamma: 64 + price: 290 + sale_gamma: 44 service_level: 0.95 SKU962: - constraint: G(low_profit -> low_stock_constraint) - cost: 391 - init_stock: 44 + constraint: null + cost: 354 + init_stock: 176 max_stock: 1000 - price: 590 - sale_gamma: 11 + price: 392 + sale_gamma: 44 service_level: 0.95 SKU963: - constraint: G(low_profit -> low_stock_constraint) - cost: 247 - init_stock: 54 + constraint: null + cost: 349 + init_stock: 63 max_stock: 1000 - price: 444 - sale_gamma: 18 + price: 523 + sale_gamma: 21 service_level: 0.95 SKU964: - constraint: G(low_profit -> low_stock_constraint) - cost: 495 - init_stock: 72 + constraint: null + cost: 244 + init_stock: 219 max_stock: 1000 - price: 836 - sale_gamma: 12 + price: 480 + sale_gamma: 73 service_level: 0.95 SKU965: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 428 - init_stock: 296 + constraint: G(stock_constraint) + cost: 124 + init_stock: 255 max_stock: 1000 - price: 577 - sale_gamma: 74 + price: 199 + sale_gamma: 51 service_level: 0.95 SKU966: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 360 - init_stock: 82 + constraint: G(stock_constraint) + cost: 302 + init_stock: 308 max_stock: 1000 - price: 482 - sale_gamma: 41 + price: 474 + sale_gamma: 44 service_level: 0.95 SKU967: - constraint: null - cost: 407 - init_stock: 162 + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 270 max_stock: 1000 - price: 712 - sale_gamma: 81 + price: 111 + sale_gamma: 45 service_level: 0.95 SKU968: - constraint: G(low_profit -> low_stock_constraint) - cost: 69 - init_stock: 378 + constraint: G(stock_constraint) + cost: 281 + init_stock: 294 max_stock: 1000 - price: 122 - sale_gamma: 63 + price: 528 + sale_gamma: 49 service_level: 0.95 SKU969: constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 256 + cost: 249 + init_stock: 24 max_stock: 1000 - price: 226 - sale_gamma: 32 + price: 328 + sale_gamma: 6 service_level: 0.95 SKU97: - constraint: null - cost: 432 - init_stock: 245 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 28 + init_stock: 180 max_stock: 1000 - price: 691 - sale_gamma: 49 + price: 43 + sale_gamma: 30 service_level: 0.95 SKU970: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 376 - init_stock: 288 + constraint: null + cost: 244 + init_stock: 30 max_stock: 1000 - price: 503 - sale_gamma: 96 + price: 373 + sale_gamma: 5 service_level: 0.95 SKU971: constraint: null - cost: 44 - init_stock: 84 + cost: 368 + init_stock: 219 max_stock: 1000 - price: 63 - sale_gamma: 14 + price: 426 + sale_gamma: 73 service_level: 0.95 SKU972: constraint: G(stock_constraint) - cost: 78 - init_stock: 56 + cost: 209 + init_stock: 345 max_stock: 1000 - price: 117 - sale_gamma: 8 + price: 307 + sale_gamma: 69 service_level: 0.95 SKU973: constraint: null - cost: 407 - init_stock: 168 + cost: 271 + init_stock: 33 max_stock: 1000 - price: 525 - sale_gamma: 42 + price: 447 + sale_gamma: 11 service_level: 0.95 SKU974: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 434 - init_stock: 162 + constraint: null + cost: 170 + init_stock: 192 max_stock: 1000 - price: 551 - sale_gamma: 27 + price: 285 + sale_gamma: 32 service_level: 0.95 SKU975: constraint: G(stock_constraint) - cost: 98 - init_stock: 356 + cost: 198 + init_stock: 88 max_stock: 1000 - price: 135 - sale_gamma: 89 + price: 334 + sale_gamma: 22 service_level: 0.95 SKU976: - constraint: null - cost: 263 - init_stock: 183 + constraint: G(stock_constraint) + cost: 178 + init_stock: 75 max_stock: 1000 - price: 481 - sale_gamma: 61 + price: 323 + sale_gamma: 15 service_level: 0.95 SKU977: - constraint: null - cost: 83 - init_stock: 184 + constraint: G(stock_constraint) + cost: 234 + init_stock: 324 max_stock: 1000 - price: 107 - sale_gamma: 23 + price: 269 + sale_gamma: 54 service_level: 0.95 SKU978: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 90 - init_stock: 616 + constraint: G(stock_constraint) + cost: 470 + init_stock: 320 max_stock: 1000 - price: 99 - sale_gamma: 77 + price: 921 + sale_gamma: 64 service_level: 0.95 SKU979: - constraint: G(low_profit -> low_stock_constraint) - cost: 207 - init_stock: 52 + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 443 + init_stock: 480 max_stock: 1000 - price: 289 - sale_gamma: 13 + price: 753 + sale_gamma: 96 service_level: 0.95 SKU98: constraint: G(stock_constraint) - cost: 43 - init_stock: 249 + cost: 443 + init_stock: 70 max_stock: 1000 - price: 49 - sale_gamma: 83 + price: 527 + sale_gamma: 35 service_level: 0.95 SKU980: constraint: null - cost: 227 - init_stock: 180 + cost: 39 + init_stock: 340 max_stock: 1000 - price: 404 - sale_gamma: 60 + price: 60 + sale_gamma: 68 service_level: 0.95 SKU981: - constraint: G(low_profit -> low_stock_constraint) - cost: 278 - init_stock: 68 + constraint: null + cost: 482 + init_stock: 360 max_stock: 1000 - price: 436 - sale_gamma: 17 + price: 607 + sale_gamma: 72 service_level: 0.95 SKU982: constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 125 - init_stock: 40 + cost: 213 + init_stock: 208 max_stock: 1000 - price: 193 - sale_gamma: 10 + price: 374 + sale_gamma: 52 service_level: 0.95 SKU983: - constraint: G(stock_constraint) - cost: 431 - init_stock: 544 + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 276 max_stock: 1000 - price: 504 - sale_gamma: 68 + price: 776 + sale_gamma: 92 service_level: 0.95 SKU984: - constraint: null - cost: 59 - init_stock: 140 + constraint: G(stock_constraint) + cost: 232 + init_stock: 637 max_stock: 1000 - price: 92 - sale_gamma: 28 + price: 327 + sale_gamma: 91 service_level: 0.95 SKU985: constraint: null - cost: 191 - init_stock: 126 + cost: 290 + init_stock: 153 max_stock: 1000 - price: 334 - sale_gamma: 63 + price: 420 + sale_gamma: 51 service_level: 0.95 SKU986: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 236 - init_stock: 266 + constraint: null + cost: 275 + init_stock: 136 max_stock: 1000 - price: 363 - sale_gamma: 38 + price: 313 + sale_gamma: 17 service_level: 0.95 SKU987: constraint: null - cost: 333 - init_stock: 792 + cost: 434 + init_stock: 204 max_stock: 1000 - price: 496 - sale_gamma: 99 + price: 516 + sale_gamma: 34 service_level: 0.95 SKU988: constraint: null - cost: 407 - init_stock: 231 + cost: 102 + init_stock: 69 max_stock: 1000 - price: 577 - sale_gamma: 77 + price: 166 + sale_gamma: 23 service_level: 0.95 SKU989: constraint: null - cost: 53 - init_stock: 574 + cost: 484 + init_stock: 558 max_stock: 1000 - price: 93 - sale_gamma: 82 + price: 653 + sale_gamma: 93 service_level: 0.95 SKU99: constraint: null - cost: 65 - init_stock: 158 + cost: 361 + init_stock: 225 max_stock: 1000 - price: 125 - sale_gamma: 79 + price: 581 + sale_gamma: 45 service_level: 0.95 SKU990: - constraint: G(stock_constraint) - cost: 368 - init_stock: 376 + constraint: G(low_profit -> low_stock_constraint) + cost: 108 + init_stock: 228 max_stock: 1000 - price: 566 - sale_gamma: 94 + price: 174 + sale_gamma: 57 service_level: 0.95 SKU991: constraint: null - cost: 348 - init_stock: 644 + cost: 409 + init_stock: 152 max_stock: 1000 - price: 438 - sale_gamma: 92 + price: 576 + sale_gamma: 19 service_level: 0.95 SKU992: constraint: null - cost: 59 - init_stock: 408 + cost: 434 + init_stock: 651 max_stock: 1000 - price: 117 - sale_gamma: 68 + price: 685 + sale_gamma: 93 service_level: 0.95 SKU993: constraint: G(low_profit -> low_stock_constraint) - cost: 498 - init_stock: 161 + cost: 145 + init_stock: 602 max_stock: 1000 - price: 756 - sale_gamma: 23 + price: 201 + sale_gamma: 86 service_level: 0.95 SKU994: - constraint: null - cost: 438 - init_stock: 136 + constraint: G(low_profit -> low_stock_constraint) + cost: 470 + init_stock: 300 max_stock: 1000 - price: 823 - sale_gamma: 34 + price: 916 + sale_gamma: 50 service_level: 0.95 SKU995: - constraint: G(stock_constraint) - cost: 292 - init_stock: 294 + constraint: null + cost: 241 + init_stock: 532 max_stock: 1000 - price: 543 - sale_gamma: 49 + price: 310 + sale_gamma: 76 service_level: 0.95 SKU996: - constraint: null - cost: 73 - init_stock: 245 + constraint: G(stock_constraint) + cost: 260 + init_stock: 438 max_stock: 1000 - price: 102 - sale_gamma: 49 + price: 416 + sale_gamma: 73 service_level: 0.95 SKU997: constraint: G(stock_constraint) - cost: 321 - init_stock: 355 + cost: 400 + init_stock: 102 max_stock: 1000 - price: 503 - sale_gamma: 71 + price: 727 + sale_gamma: 34 service_level: 0.95 SKU998: - constraint: null - cost: 371 - init_stock: 90 + constraint: G(low_profit -> low_stock_constraint) + cost: 447 + init_stock: 165 max_stock: 1000 - price: 708 - sale_gamma: 15 + price: 688 + sale_gamma: 55 service_level: 0.95 SKU999: constraint: null - cost: 208 - init_stock: 712 + cost: 79 + init_stock: 161 max_stock: 1000 - price: 403 - sale_gamma: 89 + price: 86 + sale_gamma: 23 service_level: 0.95 grid: facilities: STORE0: - - 13 - - 0 + - 9 + - 8 SUPPLIER0: - - 7 - - 18 + - 14 + - 16 WAREHOUSE0: - 16 - - 14 + - 18 size: - 20 - 20 diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index 2e3ff5190..c136bc5ee 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -149,3 +149,10 @@ def reset(self): for sku in self.facility.skus.values(): self.product_level[sku.id] = sku.init_stock self.remaining_space -= sku.init_stock + + def get_unit_info(self): + info = super().get_unit_info() + + info["product_list"] = [i for i in self.product_level.keys()] + + return info From e56a50f15de0c0c960919cfdafa3333dc2d9de27 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 19:35:17 +0800 Subject: [PATCH 157/482] bug fix --- examples/supply_chain/env_wrapper.py | 81 ++++++++++++++++------------ 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 591a85f9d..10a777d13 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -7,7 +7,7 @@ def stock_constraint(f_state): - return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt']+7)*f_state['sale_mean']) + return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean']) def is_replenish_constraint(f_state): @@ -15,11 +15,11 @@ def is_replenish_constraint(f_state): def low_profit(f_state): - return ((f_state['sku_price']-f_state['sku_cost']) * f_state['sale_mean'] <= 1000) + return ((f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000) def low_stock_constraint(f_state): - return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt']+3)*f_state['sale_mean']) + return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean']) def out_of_stock(f_state): @@ -54,7 +54,6 @@ def __getitem__(self, key, default=None): return default - class SCEnvWrapper(AbsEnvWrapper): def __init__(self, env: Env): super().__init__(env) @@ -82,7 +81,6 @@ def __init__(self, env: Env): self._service_index_ppf_cache = {} - # facility -> { # data_model_index:int, # storage:UnitBaseInfo, @@ -158,7 +156,8 @@ def get_reward(self, tick=None, target_agents=None): else: parent_facility_balance[f_id] = sheet - consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items() } + consumer_reward_by_facility = {f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in + self.cur_balance_sheet_reward.items()} return { **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, @@ -200,8 +199,10 @@ def _update_facility_features(self, state, agent_info): def _update_storage_features(self, state, agent_info): facility_id = agent_info.facility_id + state['storage_utilization'] = 0 + for pid, index in self._storage_product_indices[facility_id].items(): - product_number = self._storage_product_numbers[facility_id][pid] # product_number = self._cur_product_numbers[index] + product_number = self._storage_product_numbers[facility_id][pid] state['storage_levels'][pid] = product_number state['storage_utilization'] += product_number @@ -224,10 +225,10 @@ def _update_sale_features(self, state, agent_info): product_info = facility[agent_info.sku.id] if "consumer" in product_info: - # TODO: implement later consumer_index = product_info["consumer"].node_index - consumption_hist = self.env.snapshot_list["consumer"][[self.env.tick - i for i in range(consumption_hist_len)]:consumer_index:"latest_consumptions"] + consumption_hist = self.env.snapshot_list["consumer"][[self.env.tick - i for i in range( + consumption_hist_len)]:consumer_index:"latest_consumptions"] consumption_hist = consumption_hist.flatten() state['consumption_hist'] = list(consumption_hist) @@ -237,8 +238,9 @@ def _update_sale_features(self, state, agent_info): seller_index = product_info["seller"].node_index seller_ss = self.env.snapshot_list["seller"] - single_states = seller_ss[self.env.tick:seller_index:("total_demand")].flatten().astype(np.int) - hist_states = seller_ss[[self.env.tick - i for i in range(hist_len)]:seller_index:("sold", "demand")].flatten().reshape(2, -1).astype(np.int) + single_states = seller_ss[self.env.tick:seller_index:"total_demand"].flatten().astype(np.int) + hist_states = seller_ss[[self.env.tick - i for i in range(hist_len)]:seller_index:( + "sold", "demand")].flatten().reshape(2, -1).astype(np.int) state['total_backlog_demand'] = single_states[0] state['sale_hist'] = list(hist_states[0]) @@ -249,7 +251,8 @@ def _update_distribution_features(self, state, agent_info): distribution = facility.get("distribution", None) if distribution is not None: - dist_states = self.env.snapshot_list["distribution"][self.env.tick:distribution.node_index:("remaining_order_quantity", "remaining_order_number")] + dist_states = self.env.snapshot_list["distribution"][ + self.env.tick:distribution.node_index:("remaining_order_quantity", "remaining_order_number")] dist_states = dist_states.flatten().astype(np.int) state['distributor_in_transit_orders'] = dist_states[1] @@ -269,10 +272,11 @@ def _update_consumer_features(self, state, agent_info): in_transit_orders = self._cur_metrics['facilities'][agent_info.facility_id]["in_transit_orders"] - # for i, sku in enumerate(sku_list.values()): - # state['consumer_in_transit_orders'][sku.id] += in_transit_orders[sku.id] + for i in range(len(state['consumer_in_transit_orders'])): + state['consumer_in_transit_orders'][i] = 0 + for sku_id, number in in_transit_orders.items(): - state['consumer_in_transit_orders'][sku_id] += number + state['consumer_in_transit_orders'][sku_id] = number product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] @@ -284,12 +288,12 @@ def _update_consumer_features(self, state, agent_info): state['inventory_in_distribution'] = pending_order[agent_info.sku.id] state['inventory_estimated'] = (state['inventory_in_stock'] - + state['inventory_in_transit'] - - state['inventory_in_distribution']) - if (state['inventory_estimated'] >= 0.5*state['storage_capacity']): + + state['inventory_in_transit'] + - state['inventory_in_distribution']) + if state['inventory_estimated'] >= 0.5 * state['storage_capacity']: state['is_over_stock'] = 1 - if (state['inventory_estimated'] <= 0): + if state['inventory_estimated'] <= 0: state['is_out_of_stock'] = 1 service_index = state['service_level'] @@ -297,8 +301,9 @@ def _update_consumer_features(self, state, agent_info): if service_index not in self._service_index_ppf_cache: self._service_index_ppf_cache[service_index] = st.norm.ppf(service_index) - state['inventory_rop'] = (state['max_vlt']*state['sale_mean'] - + np.sqrt(state['max_vlt'])*state['sale_std']*self._service_index_ppf_cache[service_index]) + state['inventory_rop'] = (state['max_vlt'] * state['sale_mean'] + + np.sqrt(state['max_vlt']) * state['sale_std'] * self._service_index_ppf_cache[ + service_index]) if state['inventory_estimated'] < state['inventory_rop']: state['is_below_rop'] = 1 @@ -325,7 +330,7 @@ def _serialize_state(self, state): if not isinstance(vals, list): vals = [vals] if norm is not None: - vals = [max(0.0, min(100.0, x/(agent_raw_state[norm]+0.01))) for x in vals] + vals = [max(0.0, min(100.0, x / (agent_raw_state[norm] + 0.01))) for x in vals] result[agent_id].extend(vals) result[agent_id] = np.asarray(result[agent_id], dtype=np.float32) @@ -511,7 +516,8 @@ def _build_init_state(self): for i, source in enumerate(current_source_list): for j, sku in enumerate(sku_list.values()): if sku.id == agent_info.sku.id: - state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = self.facility_levels[source]["skus"][sku.id].vlt + state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = \ + self.facility_levels[source]["skus"][sku.id].vlt # price features state['max_price'] = self._max_price @@ -529,9 +535,10 @@ class BalanceSheetCalculator: consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") seller_features = ("id", "sold", "demand", "price", "backlog_ratio") manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - product_features = ("id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") + product_features = ( + "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") storage_features = ("capacity", "remaining_space") - vehicle_features= ("id", "payload", "unit_transport_cost") + vehicle_features = ("id", "payload", "unit_transport_cost") def __init__(self, env: Env): self.env = env @@ -586,7 +593,8 @@ def __init__(self, env: Env): def calc(self): tick = self.env.tick # consumer - consumer_bs_states = self.consumer_ss[tick::self.consumer_features].flatten().reshape(-1, len(self.consumer_features)) + consumer_bs_states = self.consumer_ss[tick::self.consumer_features].flatten().reshape(-1, len( + self.consumer_features)) # quantity * price consumer_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] @@ -614,7 +622,8 @@ def calc(self): seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit # manufacture - man_bs_states = self.manufacture_ss[tick::self.manufacture_features].flatten().reshape(-1, len(self.manufacture_features)) + man_bs_states = self.manufacture_ss[tick::self.manufacture_features].flatten().reshape(-1, len( + self.manufacture_features)) # loss = manufacture number * cost man_balance_sheet_profit_loss = -1 * man_bs_states[:, 1] * man_bs_states[:, 2] @@ -623,17 +632,18 @@ def calc(self): man_step_reward = man_balance_sheet_profit_loss # product - product_bs_states = self.product_ss[tick::self.product_features].flatten().reshape(-1, len(self.product_features)) + product_bs_states = self.product_ss[tick::self.product_features].flatten().reshape(-1, + len(self.product_features)) # product distribution loss = check order + delay order penalty - product_distribution_balance_sheet_loss= -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) + product_distribution_balance_sheet_loss = -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) # product distribution profit = check order * price product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] # result we need - product_step_reward = np.zeros((len(self.products,))) - product_balance_sheet_profit = np.zeros((len(self.products,))) + product_step_reward = np.zeros((len(self.products, ))) + product_balance_sheet_profit = np.zeros((len(self.products, ))) product_balance_sheet_loss = np.zeros((len(self.products, ))) # create product number mapping for storages @@ -642,7 +652,7 @@ def calc(self): product_list = self.storage_ss[tick:storage_index:"product_list"].flatten().astype(np.int) product_number = self.storage_ss[tick:storage_index:"product_number"].flatten().astype(np.int) - storages_product_map[storage_index] = {pid:pnum for pid, pnum in zip(product_list, product_number)} + storages_product_map[storage_index] = {pid: pnum for pid, pnum in zip(product_list, product_number)} # product balance sheet and reward # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss @@ -674,7 +684,8 @@ def calc(self): product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[distribution_index] product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[distribution_index] - product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + product_distribution_balance_sheet_profit[distribution_index] + product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + \ + product_distribution_balance_sheet_profit[distribution_index] if downstreams and len(downstreams) > 0: if product_id in downstreams: @@ -747,7 +758,7 @@ def calc(self): from time import time env = Env( - scenario="supply_chain", + scenario="supply_chain", topology="random", durations=100, max_snapshots=10) @@ -762,4 +773,4 @@ def calc(self): end_time = time() - print("time cost:", end_time - start_time) \ No newline at end of file + print("time cost:", end_time - start_time) From 8a7a9d0ea1680cd250ca648a8d6ca40dd7b22577 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 19:52:42 +0800 Subject: [PATCH 158/482] speed up storage features --- examples/supply_chain/env_wrapper.py | 45 +++++++++++++++------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 10a777d13..ee2d12e08 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -7,23 +7,23 @@ def stock_constraint(f_state): - return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean']) + return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean'] def is_replenish_constraint(f_state): - return (f_state['consumption_hist'][-1] > 0) + return f_state['consumption_hist'][-1] > 0 def low_profit(f_state): - return ((f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000) + return (f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000 def low_stock_constraint(f_state): - return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean']) + return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean'] def out_of_stock(f_state): - return (0 < f_state['inventory_in_stock']) + return 0 < f_state['inventory_in_stock'] atoms = { @@ -73,12 +73,8 @@ def __init__(self, env: Env): # state for each tick self._cur_metrics = env.metrics - self._cur_facility_storage_products = None - - # TODO: this is fixed after env setup - self._cur_facility_storage_product_mapping = {} - self._cur_product_numbers = [] + # cache for ppf value. self._service_index_ppf_cache = {} # facility -> { @@ -99,15 +95,18 @@ def __init__(self, env: Env): # our raw state self._states = {} - # facility_id -> storage index + # facility id -> storage index self._facility2storage_index_dict = {} - # facility_id -> product id -> number + # facility id -> product id -> number self._storage_product_numbers = {} # facility id -> product_id -> index self._storage_product_indices = {} + # facility id -> storage product utilization + self._facility_product_utilization = {} + # built internal helpers. self._build_internal_helpers() @@ -115,6 +114,10 @@ def get_state(self, event): self.cur_balance_sheet_reward = self.balance_cal.calc() self._cur_metrics = self.env.metrics + # reset for each step + for facility_id in self._facility_product_utilization: + self._facility_product_utilization[facility_id] = 0 + final_state = {} # calculate storage info first, then use it later to speed up. @@ -122,7 +125,10 @@ def get_state(self, event): product_numbers = self.storage_ss[self.env.tick:storage_index:"product_number"].flatten().astype(np.int) for pid, index in self._storage_product_indices[facility_id].items(): - self._storage_product_numbers[facility_id][pid] = product_numbers[index] + product_number = product_numbers[index] + + self._storage_product_numbers[facility_id][pid] = product_number + self._facility_product_utilization[facility_id] += product_number for agent_info in self._agent_list: state = self._states[agent_info.id] @@ -201,16 +207,14 @@ def _update_storage_features(self, state, agent_info): facility_id = agent_info.facility_id state['storage_utilization'] = 0 - for pid, index in self._storage_product_indices[facility_id].items(): - product_number = self._storage_product_numbers[facility_id][pid] - state['storage_levels'][pid] = product_number - state['storage_utilization'] += product_number + state['storage_levels'] = self._storage_product_numbers[facility_id] + state['storage_utilization'] = self._facility_product_utilization[facility_id] def _update_sale_features(self, state, agent_info): if agent_info.is_facility: return - settings = self.env.configs.settings + settings: dict = self.env.configs.settings hist_len = settings['sale_hist_len'] consumption_hist_len = settings['consumption_hist_len'] @@ -268,8 +272,6 @@ def _update_consumer_features(self, state, agent_info): if "consumer" not in product_info: return - sku_list = self._summary["skus"] - in_transit_orders = self._cur_metrics['facilities'][agent_info.facility_id]["in_transit_orders"] for i in range(len(state['consumer_in_transit_orders'])): @@ -356,8 +358,9 @@ def _build_internal_helpers(self): self._facility2storage_index_dict[facility_id] = storage["node_index"] - self._storage_product_numbers[facility_id] = {} + self._storage_product_numbers[facility_id] = [0] * self._sku_number self._storage_product_indices[facility_id] = {} + self._facility_product_utilization[facility_id] = 0 for i, pid in enumerate(storage["product_list"]): self._storage_product_indices[facility_id][pid] = i From 0514e4b11d403ee47100e3a294bc40d3ef57ae60 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 19:55:03 +0800 Subject: [PATCH 159/482] update global features --- examples/supply_chain/env_wrapper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index ee2d12e08..5f5ac5d63 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -145,7 +145,7 @@ def get_state(self, event): # self._update_vlt_features(state, agent_info) self._update_consumer_features(state, agent_info) # self._add_price_features(state, agent_info) - # self._add_global_features(state) + self._update_global_features(state) state[f"consumer.{agent_info.id}"] = state state[f"producer.{agent_info.id}"] = state @@ -310,6 +310,9 @@ def _update_consumer_features(self, state, agent_info): if state['inventory_estimated'] < state['inventory_rop']: state['is_below_rop'] = 1 + def _update_global_features(self, state): + state["global_time"] = self.env.tick + def _serialize_state(self, state): result = defaultdict(list) From 286d3477f87187ff29890a4edde923d85ccbd3ae Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 20:20:14 +0800 Subject: [PATCH 160/482] speed up for distribution and seller features --- examples/supply_chain/env_wrapper.py | 56 +++++++++++++++++----------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 5f5ac5d63..868624711 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -54,12 +54,19 @@ def __getitem__(self, key, default=None): return default +distribution_features = ("remaining_order_quantity", "remaining_order_number") +seller_features = ("total_demand", "sold", "demand") + + class SCEnvWrapper(AbsEnvWrapper): def __init__(self, env: Env): super().__init__(env) self.balance_cal = BalanceSheetCalculator(env) self.cur_balance_sheet_reward = None self.storage_ss = env.snapshot_list["storage"] + self.distribution_ss = env.snapshot_list["distribution"] + self.consumer_ss = env.snapshot_list["consumer"] + self.seller_ss = env.snapshot_list["seller"] self._summary = env.summary['node_mapping'] self._configs = env.configs @@ -107,13 +114,33 @@ def __init__(self, env: Env): # facility id -> storage product utilization self._facility_product_utilization = {} + # current distribution states + self._cur_distribution_states = None + + # current consumer states + self._cur_consumer_states = None + + # current seller states + self._cur_seller_states = None + # built internal helpers. self._build_internal_helpers() def get_state(self, event): + cur_tick = self.env.tick + settings: dict = self.env.configs.settings + consumption_hist_len = settings['consumption_hist_len'] + hist_len = settings['sale_hist_len'] + consumption_ticks = [cur_tick - i for i in range(consumption_hist_len)] + hist_ticks = [cur_tick - i for i in range(hist_len)] + self.cur_balance_sheet_reward = self.balance_cal.calc() self._cur_metrics = self.env.metrics + self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features].flatten().reshape(-1, 2).astype(np.int) + self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"].flatten().reshape(-1, len(self.consumer_ss)) + self._cur_seller_states = self.seller_ss[hist_ticks::seller_features].astype(np.int) + # reset for each step for facility_id in self._facility_product_utilization: self._facility_product_utilization[facility_id] = 0 @@ -122,7 +149,7 @@ def get_state(self, event): # calculate storage info first, then use it later to speed up. for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[self.env.tick:storage_index:"product_number"].flatten().astype(np.int) + product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"].flatten().astype(np.int) for pid, index in self._storage_product_indices[facility_id].items(): product_number = product_numbers[index] @@ -214,10 +241,6 @@ def _update_sale_features(self, state, agent_info): if agent_info.is_facility: return - settings: dict = self.env.configs.settings - - hist_len = settings['sale_hist_len'] - consumption_hist_len = settings['consumption_hist_len'] product_metrics = self._cur_metrics["products"][agent_info.id] # for product unit only @@ -231,34 +254,25 @@ def _update_sale_features(self, state, agent_info): if "consumer" in product_info: consumer_index = product_info["consumer"].node_index - consumption_hist = self.env.snapshot_list["consumer"][[self.env.tick - i for i in range( - consumption_hist_len)]:consumer_index:"latest_consumptions"] - consumption_hist = consumption_hist.flatten() - - state['consumption_hist'] = list(consumption_hist) + state['consumption_hist'] = list(self._cur_consumer_states[:, consumer_index]) state['pending_order'] = list(product_metrics["pending_order_daily"]) if "seller" in product_info: seller_index = product_info["seller"].node_index - seller_ss = self.env.snapshot_list["seller"] - single_states = seller_ss[self.env.tick:seller_index:"total_demand"].flatten().astype(np.int) - hist_states = seller_ss[[self.env.tick - i for i in range(hist_len)]:seller_index:( - "sold", "demand")].flatten().reshape(2, -1).astype(np.int) + seller_states = self._cur_seller_states[:, seller_index, :] - state['total_backlog_demand'] = single_states[0] - state['sale_hist'] = list(hist_states[0]) - state['backlog_demand_hist'] = list(hist_states[1]) + # for total demand, we need latest one. + state['total_backlog_demand'] = seller_states[:, 0][-1] + state['sale_hist'] = list(seller_states[:, 1]) + state['backlog_demand_hist'] = list(seller_states[:, 2]) def _update_distribution_features(self, state, agent_info): facility = self.facility_levels[agent_info.facility_id] distribution = facility.get("distribution", None) if distribution is not None: - dist_states = self.env.snapshot_list["distribution"][ - self.env.tick:distribution.node_index:("remaining_order_quantity", "remaining_order_number")] - dist_states = dist_states.flatten().astype(np.int) - + dist_states = self._cur_distribution_states[distribution.node_index] state['distributor_in_transit_orders'] = dist_states[1] state['distributor_in_transit_orders_qty'] = dist_states[0] From 772bddbf759115fe3048b54c340df72ccabad922 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 20:25:20 +0800 Subject: [PATCH 161/482] speed up for consumer in transit orders --- examples/supply_chain/env_wrapper.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 868624711..d76e222e4 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -114,6 +114,9 @@ def __init__(self, env: Env): # facility id -> storage product utilization self._facility_product_utilization = {} + # facility id -> in_transit_orders + self._facility_in_transit_orders = {} + # current distribution states self._cur_distribution_states = None @@ -145,6 +148,13 @@ def get_state(self, event): for facility_id in self._facility_product_utilization: self._facility_product_utilization[facility_id] = 0 + in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] + + self._facility_in_transit_orders[facility_id] = [0] * self._sku_number + + for sku_id, number in in_transit_orders.items(): + self._facility_in_transit_orders[facility_id][sku_id] = number + final_state = {} # calculate storage info first, then use it later to speed up. @@ -286,13 +296,7 @@ def _update_consumer_features(self, state, agent_info): if "consumer" not in product_info: return - in_transit_orders = self._cur_metrics['facilities'][agent_info.facility_id]["in_transit_orders"] - - for i in range(len(state['consumer_in_transit_orders'])): - state['consumer_in_transit_orders'][i] = 0 - - for sku_id, number in in_transit_orders.items(): - state['consumer_in_transit_orders'][sku_id] = number + state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] From f56b34fa3a15bb2100f6d3b5028feaf05d0b3040 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 20:30:03 +0800 Subject: [PATCH 162/482] fix hello world bug --- examples/hello_world/supply_chain/hello.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 73e5945fe..9edcc2e1f 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -13,20 +13,19 @@ def main(): env = Env(scenario="supply_chain", topology="sample1", start_tick=start_tick, durations=durations, max_snapshots=100) total_episodes = 10 - matrics = None is_done = False - # print(env.agent_idx_list) - for ep in range(total_episodes): print("Current episode:", ep) while not is_done: # This scenario require a dictionary of actions, which the key is the unit id, value if the action. - matrics, _, is_done = env.step(None) + _, _, is_done = env.step(None) env.reset() + is_done = False + if __name__ == "__main__": main() From 6435f434b975fd4389491f139304d6bf2f4969d9 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 21:05:39 +0800 Subject: [PATCH 163/482] add missing state --- examples/supply_chain/env_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index d76e222e4..143713a58 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -184,8 +184,8 @@ def get_state(self, event): # self._add_price_features(state, agent_info) self._update_global_features(state) - state[f"consumer.{agent_info.id}"] = state - state[f"producer.{agent_info.id}"] = state + final_state[f"consumer.{agent_info.id}"] = state + final_state[f"producer.{agent_info.id}"] = state return self._serialize_state(final_state) From 8553befad89682a374d5d4bbe28e71446faba4bc Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 21:58:18 +0800 Subject: [PATCH 164/482] bug fix --- .../scenarios/supply_chain/business_engine.py | 2 +- .../scenarios/supply_chain/units/product.py | 1 - maro/simulator/scenarios/supply_chain/world.py | 13 +------------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index cb72274c3..702a45424 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -25,7 +25,7 @@ def __init__(self, **kwargs): # Prepare product unit for later using. for unit in self.world.units.values(): - if type(unit) == ProductUnit: + if issubclass(type(unit), ProductUnit): self._product_units.append(unit) self._frame = self.world.frame diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index f74b473fe..1ef598b39 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -142,7 +142,6 @@ def generate(facility, config: dict, unit_def: object): sku_type = sku.type product_unit: ProductUnit = world.build_unit_by_type(unit_def, facility, facility) - # product_unit: ProductUnit = world.build_unit(facility, facility, unit_def) product_unit.product_id = sku_id product_unit.children = [] product_unit.parse_configs(config) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index a76381699..2dd2c47ed 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -279,7 +279,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio self.max_price = max(self.max_price, sku.price) for unit in self.units.values(): - if type(unit) == ProductUnit: + if issubclass(type(unit), ProductUnit): agent_type = unit.config["agent_type"] # unit or facility id, agent type, is facility, sku info, facility id @@ -375,17 +375,6 @@ def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBas # If this is template unit, then will use the class' static method 'generate' to generate sub-units. children = unit_def.class_type.generate(facility, config.get("config"), unit_def) - for child in children.values(): - child.id = self._gen_id() - child.world = self - child.facility = facility - child.parent = parent - - # Pass the config if there is any. - child.parse_configs(config.get("config", {})) - - self.units[child.id] = child - return children def get_node_mapping(self): From 346af166ddaadecd2ab6c4bcb845108df9d61b06 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 7 Apr 2021 22:18:32 +0800 Subject: [PATCH 165/482] speed up for state serialize --- examples/supply_chain/env_wrapper.py | 41 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 143713a58..5db66ce40 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -144,8 +144,9 @@ def get_state(self, event): self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"].flatten().reshape(-1, len(self.consumer_ss)) self._cur_seller_states = self.seller_ss[hist_ticks::seller_features].astype(np.int) - # reset for each step + # facility level states for facility_id in self._facility_product_utilization: + # reset for each step self._facility_product_utilization[facility_id] = 0 in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] @@ -184,10 +185,12 @@ def get_state(self, event): # self._add_price_features(state, agent_info) self._update_global_features(state) - final_state[f"consumer.{agent_info.id}"] = state - final_state[f"producer.{agent_info.id}"] = state + np_state = self._serialize_state(state) - return self._serialize_state(final_state) + final_state[f"consumer.{agent_info.id}"] = np_state + final_state[f"producer.{agent_info.id}"] = np_state + + return final_state def get_reward(self, tick=None, target_agents=None): wc = self.env.configs.settings["global_reward_weight_consumer"] @@ -255,6 +258,7 @@ def _update_sale_features(self, state, agent_info): # for product unit only state['sale_mean'] = product_metrics["sale_mean"] + # TODO: why gamma sale as mean? state['sale_gamma'] = state['sale_mean'] state['sale_std'] = product_metrics["sale_std"] @@ -321,9 +325,10 @@ def _update_consumer_features(self, state, agent_info): if service_index not in self._service_index_ppf_cache: self._service_index_ppf_cache[service_index] = st.norm.ppf(service_index) + ppf = self._service_index_ppf_cache[service_index] + state['inventory_rop'] = (state['max_vlt'] * state['sale_mean'] - + np.sqrt(state['max_vlt']) * state['sale_std'] * self._service_index_ppf_cache[ - service_index]) + + np.sqrt(state['max_vlt']) * state['sale_std'] * ppf) if state['inventory_estimated'] < state['inventory_rop']: state['is_below_rop'] = 1 @@ -332,7 +337,7 @@ def _update_global_features(self, state): state["global_time"] = self.env.tick def _serialize_state(self, state): - result = defaultdict(list) + result = [] keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'constraint_idx', 'is_accepted', 'consumption_hist']), @@ -346,18 +351,16 @@ def _serialize_state(self, state): 'inventory_rop']), ('max_price', ['sku_price', 'sku_cost'])] - for agent_id, agent_raw_state in state.items(): - for norm, fields in keys_in_state: - for field in fields: - vals = agent_raw_state[field] - if not isinstance(vals, list): - vals = [vals] - if norm is not None: - vals = [max(0.0, min(100.0, x / (agent_raw_state[norm] + 0.01))) for x in vals] - result[agent_id].extend(vals) - result[agent_id] = np.asarray(result[agent_id], dtype=np.float32) + for norm, fields in keys_in_state: + for field in fields: + vals = state[field] + if not isinstance(vals, list): + vals = [vals] + if norm is not None: + vals = [max(0.0, min(100.0, x / (state[norm] + 0.01))) for x in vals] + result.extend(vals) - return result + return np.asarray(result, dtype=np.float32) def _build_internal_helpers(self): # facility levels @@ -780,6 +783,7 @@ def calc(self): if __name__ == "__main__": from time import time + import cProfile env = Env( scenario="supply_chain", @@ -793,6 +797,7 @@ def calc(self): start_time = time() + # cProfile.run("ss.get_state(None)", sort="cumtime") states = ss.get_state(None) end_time = time() From 9123c7b51a9c79d2002e3dc27175858ef271a28b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 8 Apr 2021 14:18:01 +0800 Subject: [PATCH 166/482] correct bug in both side --- .../hello_world/supply_chain/env_wrapper.py | 47 ++++++++----------- examples/supply_chain/env_wrapper.py | 20 ++++---- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/examples/hello_world/supply_chain/env_wrapper.py b/examples/hello_world/supply_chain/env_wrapper.py index f5762273c..cdb3e30be 100644 --- a/examples/hello_world/supply_chain/env_wrapper.py +++ b/examples/hello_world/supply_chain/env_wrapper.py @@ -201,10 +201,7 @@ def get_reward(self, tick=None): def _get_state(self): self._cur_metrics = self.env.metrics - state = { - "consumer": {}, - "producer": {} - } + state = {} for agent_info in self._agent_list: storage_index = self.facility_levels[agent_info.facility_id]['storage'].node_index @@ -220,8 +217,8 @@ def _get_state(self): self._add_global_features(f_state) - state['consumer'][agent_info.id] = f_state - state['producer'][agent_info.id] = f_state + state[f"consumer.{agent_info.id}"] = f_state + state[f"producer.{agent_info.id}"] = f_state return self._serialize_state(state) @@ -372,7 +369,7 @@ def _add_sale_features(self, state, agent_info): # TODO: implement later consumer_index = product_info["consumer"].node_index - consumption_hist = self.env.snapshot_list["consumer"][[self.env.tick - i for i in range(consumption_hist_len)]:consumer_index:"latest_consumptions"] + consumption_hist = self.env.snapshot_list["consumer"][[self.env.tick - i for i in range(consumption_hist_len-1, -1, -1)]:consumer_index:"latest_consumptions"] consumption_hist = consumption_hist.flatten() state['consumption_hist'] = list(consumption_hist) @@ -383,11 +380,11 @@ def _add_sale_features(self, state, agent_info): seller_ss = self.env.snapshot_list["seller"] single_states = seller_ss[self.env.tick:seller_index:("total_demand")].flatten().astype(np.int) - hist_states = seller_ss[[self.env.tick - i for i in range(hist_len)]:seller_index:("sold", "demand")].flatten().reshape(2, -1).astype(np.int) + hist_states = seller_ss[[self.env.tick - i for i in range(hist_len-1, -1, -1)]:seller_index:("sold", "demand")].flatten().reshape(-1, 2).astype(np.int) state['total_backlog_demand'] = single_states[0] - state['sale_hist'] = list(hist_states[0]) - state['backlog_demand_hist'] = list(hist_states[1]) + state['sale_hist'] = list(hist_states[:, 0]) + state['backlog_demand_hist'] = list(hist_states[:, 1]) state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma def _add_distributor_features(self, state, agent_info): @@ -483,10 +480,7 @@ def _add_price_features(self, state, agent_info): state['sku_cost'] = agent_info.sku.cost def _serialize_state(self, state): - result = { - "consumer": {}, - "producer": {} - } + result = {} keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'constraint_idx', 'is_accepted', 'consumption_hist']), @@ -500,22 +494,21 @@ def _serialize_state(self, state): 'inventory_rop']), ('max_price', ['sku_price', 'sku_cost'])] - for _type, agents_dict in state.items(): - for agent_id, agent_raw_state in agents_dict.items(): - result[_type][agent_id] = [] + for agent_id, agent_raw_state in state.items(): + result[agent_id] = [] - for norm, fields in keys_in_state: - for field in fields: - vals = agent_raw_state[field] + for norm, fields in keys_in_state: + for field in fields: + vals = agent_raw_state[field] - if not isinstance(vals, list): - vals = [vals] - if norm is not None: - vals = [ - max(0.0, min(100.0, x/(agent_raw_state[norm]+0.01))) for x in vals] + if not isinstance(vals, list): + vals = [vals] + if norm is not None: + vals = [ + max(0.0, min(100.0, x/(agent_raw_state[norm]+0.01))) for x in vals] - result[_type][agent_id].extend(vals) - result[_type][agent_id] = np.array(result[_type][agent_id]) + result[agent_id].extend(vals) + result[agent_id] = np.array(result[agent_id]) return result diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 5db66ce40..ede9a8667 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -134,8 +134,8 @@ def get_state(self, event): settings: dict = self.env.configs.settings consumption_hist_len = settings['consumption_hist_len'] hist_len = settings['sale_hist_len'] - consumption_ticks = [cur_tick - i for i in range(consumption_hist_len)] - hist_ticks = [cur_tick - i for i in range(hist_len)] + consumption_ticks = [cur_tick - i for i in range(consumption_hist_len-1, -1, -1)] + hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] self.cur_balance_sheet_reward = self.balance_cal.calc() self._cur_metrics = self.env.metrics @@ -258,13 +258,15 @@ def _update_sale_features(self, state, agent_info): # for product unit only state['sale_mean'] = product_metrics["sale_mean"] - # TODO: why gamma sale as mean? - state['sale_gamma'] = state['sale_mean'] state['sale_std'] = product_metrics["sale_std"] facility = self.facility_levels[agent_info.facility_id] product_info = facility[agent_info.sku.id] + if "seller" not in product_info: + # TODO: why gamma sale as mean? + state['sale_gamma'] = state['sale_mean'] + if "consumer" in product_info: consumer_index = product_info["consumer"].node_index @@ -277,8 +279,8 @@ def _update_sale_features(self, state, agent_info): seller_states = self._cur_seller_states[:, seller_index, :] # for total demand, we need latest one. - state['total_backlog_demand'] = seller_states[:, 0][-1] - state['sale_hist'] = list(seller_states[:, 1]) + state['total_backlog_demand'] = seller_states[:, 0][-1][0] + state['sale_hist'] = list(seller_states[:, 1].flatten()) state['backlog_demand_hist'] = list(seller_states[:, 2]) def _update_distribution_features(self, state, agent_info): @@ -441,11 +443,12 @@ def _build_init_state(self): state["global_time"] = 0 # facility features + state["facility"] = None state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] state['constraint_idx'] = [0] state['facility_id'] = [0] * self._sku_number - state['sku_info'] = {} if agent_info.sku is not None else agent_info.sku + state['sku_info'] = {} if agent_info.is_facility else agent_info.sku state['echelon_level'] = 0 state['facility_info'] = facility['config'] @@ -454,9 +457,6 @@ def _build_init_state(self): if not agent_info.is_facility: state['facility_id'][agent_info.sku.id] = 1 - state[f"consumer.{agent_info.id}"] = state - state[f"producer.{agent_info.id}"] = state - for atom_name in atoms.keys(): state[atom_name] = list(np.ones(self._configs.settings['constraint_state_hist_len'])) From d3aeb29dd863227f549615c434d9aa718cae5e3f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 12 Apr 2021 14:04:12 +0800 Subject: [PATCH 167/482] add scipy dependency, resove collections warnning --- maro/simulator/core.py | 2 +- setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/maro/simulator/core.py b/maro/simulator/core.py index b5016f49f..82ffd51b3 100644 --- a/maro/simulator/core.py +++ b/maro/simulator/core.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import Iterable +from collections.abc import Iterable from importlib import import_module from inspect import getmembers, isclass from typing import List diff --git a/setup.py b/setup.py index e242d1a53..6da5bc4b4 100644 --- a/setup.py +++ b/setup.py @@ -141,7 +141,8 @@ "kubernetes==12.0.1", "prompt_toolkit==2.0.10", "stringcase==1.2.0", - "networkx==2.4" + "networkx==2.4", + "scipy==1.5.2" ], entry_points={ "console_scripts": [ From a0bfddcf61f9e8f265f9c65bad42cd6cc5d8a591 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 12 Apr 2021 14:23:12 +0800 Subject: [PATCH 168/482] add dependency --- tests/requirements.test.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/requirements.test.txt b/tests/requirements.test.txt index 002db13ae..8a42f0291 100644 --- a/tests/requirements.test.txt +++ b/tests/requirements.test.txt @@ -20,4 +20,5 @@ paramiko==2.7.2 pytz==2019.3 aria2p==0.9.1 kubernetes -networkx==2.4 \ No newline at end of file +networkx==2.4 +scipy==1.5.2 \ No newline at end of file From 1488de8ca8cc92a4998bed76c09569f5bbad5764 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 12 Apr 2021 08:28:46 +0000 Subject: [PATCH 169/482] 1. changed to external logger in distributed learner and actor; 2. added tmp docker scripts --- aks/actor.yml | 28 +++++++++ aks/learner.yml | 27 ++++++++ aks/redis.yml | 38 ++++++++++++ docker-compose.yml | 27 ++++++++ docker_files/dev.df | 37 +++++++++++ examples/cim/ac/main.py | 3 +- examples/cim/dqn/config.yml | 2 +- examples/supply_chain/dqn/config.yml | 6 +- .../scripts/docker_compose_yml_generator.py | 61 +++++++++++++++++++ maro/rl/distributed/actor.py | 19 +++--- maro/rl/distributed/actor_manager.py | 12 ++-- maro/rl/distributed/learner.py | 8 ++- requirements.dev.txt | 5 +- 13 files changed, 248 insertions(+), 25 deletions(-) create mode 100644 aks/actor.yml create mode 100644 aks/learner.yml create mode 100644 aks/redis.yml create mode 100644 docker-compose.yml create mode 100644 docker_files/dev.df create mode 100644 examples/supply_chain/scripts/docker_compose_yml_generator.py diff --git a/aks/actor.yml b/aks/actor.yml new file mode 100644 index 000000000..01b90b67c --- /dev/null +++ b/aks/actor.yml @@ -0,0 +1,28 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: actor + labels: + role: actor +spec: + completions: 5 + parallelism: 5 + # backoffLimit: 2 + template: + metadata: + name: actor + spec: + containers: + - name: actor + image: maroacr.azurecr.cn/maro-dev:latest + command: ["python3", "/mnt/examples/cim/dqn/main.py", "-w", "2"] + volumeMounts: + - name: maro-test + mountPath: /mnt + restartPolicy: Never + volumes: + - name: maro-test + azureFile: + secretName: azure-secret + shareName: marosf + readOnly: false \ No newline at end of file diff --git a/aks/learner.yml b/aks/learner.yml new file mode 100644 index 000000000..9256f97b9 --- /dev/null +++ b/aks/learner.yml @@ -0,0 +1,27 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: learner + labels: + role: learner +spec: + completions: 1 + # backoffLimit: 2 + template: + metadata: + name: learner + spec: + containers: + - name: learner + image: maroacr.azurecr.cn/maro-dev:latest + command: ["python3", "/mnt/examples/cim/dqn/main.py", "-w", "1"] + volumeMounts: + - name: maro-test + mountPath: /mnt + restartPolicy: Never + volumes: + - name: maro-test + azureFile: + secretName: azure-secret + shareName: marosf + readOnly: false \ No newline at end of file diff --git a/aks/redis.yml b/aks/redis.yml new file mode 100644 index 000000000..742b97574 --- /dev/null +++ b/aks/redis.yml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 # API version +kind: Deployment +metadata: + name: maro-redis # Unique name for the deployment + labels: + app: maro-redis # Labels to be applied to this deployment +spec: + selector: + matchLabels: # This deployment applies to the Pods matching these labels + app: maro-redis + replicas: 1 # Run a single pod in the deployment + template: # Template for the pods that will be created by this deployment + metadata: + labels: # Labels to be applied to the Pods in this deployment + app: maro-redis + spec: # Spec for the container which will be run inside the Pod. + containers: + - name: maro-redis + image: redis + resources: + requests: + cpu: 100m + memory: 100Mi + ports: + - containerPort: 6379 + +--- +apiVersion: v1 +kind: Service +metadata: + name: maro-redis +spec: + type: ClusterIP + selector: + app: maro-redis + ports: + - port: 6379 + targetPort: 6379 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..8bbc22b56 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.9" +services: + redis: + image: redis:6 + container_name: maro-redis + learner: + build: + context: . + dockerfile: docker_files/dev.df + image: maro-dev + container_name: learner + volumes: + - /home/data_disk/yaqiu/maro/examples:/maro/examples + command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "1"] + actor_1: + image: maro-dev + container_name: actor_1 + volumes: + - /home/data_disk/yaqiu/maro/examples:/maro/examples + command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "2"] + actor_2: + image: maro-dev + container_name: actor_2 + volumes: + - /home/data_disk/yaqiu/maro/examples:/maro/examples + command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "2"] + \ No newline at end of file diff --git a/docker_files/dev.df b/docker_files/dev.df new file mode 100644 index 000000000..7e1676898 --- /dev/null +++ b/docker_files/dev.df @@ -0,0 +1,37 @@ +FROM ubuntu:18.04 + +WORKDIR /maro + +# Install Apt packages +RUN apt-get update --fix-missing +RUN apt-get install -y apt-utils +RUN apt-get install -y sudo +RUN apt-get install -y gcc +RUN apt-get install -y libcurl4 libcurl4-openssl-dev libssl-dev curl +RUN apt-get install -y libzmq3-dev +RUN apt-get install -y python3-pip +RUN apt-get install -y python3-dev libpython3.6-dev python-numpy +RUN rm -rf /var/lib/apt/lists/* + +# Install Python packages +RUN pip3 install --upgrade pip +RUN pip install --no-cache-dir Cython==0.29.14 +RUN pip install --no-cache-dir pyaml==20.4.0 +RUN pip install --no-cache-dir pyzmq==19.0.2 +RUN pip install --no-cache-dir numpy==1.19.1 +RUN pip install --no-cache-dir torch==1.6.0 +RUN pip install --no-cache-dir scipy +RUN pip install --no-cache-dir redis + +COPY maro /maro/maro/ +COPY scripts /maro/scripts/ +COPY setup.py /maro/ +RUN bash /maro/scripts/install_maro.sh +RUN pip cache purge + +RUN rm -r /maro/scripts +RUN rm /maro/setup.py + +ENV PYTHONPATH=/maro + +CMD ["/bin/bash"] diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index a909da809..a41e2b549 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -53,6 +53,7 @@ def get_ac_agent(): scheduler = Scheduler(config["training"]["max_episode"]) learner = Learner( CIMEnvWrapper(env, **config["shaping"]), agent, scheduler, - agent_update_interval=config["training"]["agent_update_interval"] + agent_update_interval=config["training"]["agent_update_interval"], + log_env_metrics=True ) learner.run() diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index fc4e87f62..6bd3a87d0 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -61,7 +61,7 @@ training: scenario: cim topology: toy.4p_ssdd_l0.0 durations: 1120 - max_episode: 100 + max_episode: 5 agent_update_interval: 200 exploration: parameter_names: diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index 0a0a9e37d..bd34754a7 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -34,9 +34,9 @@ training: env: scenario: supply_chain topology: sample1 - durations: 100 - max_episode: 3 - agent_update_interval: 10 + durations: 200 + max_episode: 5 + agent_update_interval: 45 exploration: parameter_names: - epsilon diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py new file mode 100644 index 000000000..07c50bb2f --- /dev/null +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -0,0 +1,61 @@ +# version: "3.9" +# services: +# redis: +# image: redis:6 +# container_name: maro-redis +# learner: +# build: +# context: . +# dockerfile: docker_files/dev.df +# image: maro-dev +# container_name: learner +# volumes: +# - /home/data_disk/yaqiu/maro/examples:/maro/examples +# command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "1"] +# actor_1: +# image: maro-dev +# container_name: actor_1 +# volumes: +# - /home/data_disk/yaqiu/maro/examples:/maro/examples +# command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "2"] +# actor_2: +# image: maro-dev +# container_name: actor_2 +# volumes: +# - /home/data_disk/yaqiu/maro/examples:/maro/examples +# command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "2"] + +import defaultdict +import yaml +from os.path import dirname, join, realpath + +path = realpath(__file__) +config_path = join(dirname(dirname(path)), "dqn", "config.yml") + + +with open(config_path, "r") as fp: + config = yaml.safe_load(fp) + num_actors = config["distributed"]["num_actors"] + +docker_compose_yaml = { + "version": "3.9", + "services": { + "redis": {"image": "redis:6", "container_name": "maro-redis"}, + "learner": { + "build": {"context": ".", "dockerfile": "docker_files/dev.df"}, + "image": "maro-dev", + "container_name": "learner", + "volumes": ["/home/data_disk/yaqiu/maro/examples:/maro/examples"] + } + } +} + + +build: +# context: . +# dockerfile: docker_files/dev.df +# image: maro-dev +# container_name: learner +# volumes: +# - /home/data_disk/yaqiu/maro/examples:/maro/examples +# command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "1"] \ No newline at end of file diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 39be73fc8..839da373f 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -1,12 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from os import getcwd from typing import Union from maro.communication import Message, Proxy from maro.rl.agent import AbsAgent, MultiAgentWrapper from maro.rl.training import AbsEnvWrapper -from maro.utils import InternalLogger +from maro.utils import Logger from .message_enums import MsgKey, MsgTag @@ -29,7 +30,8 @@ def __init__( agent: Union[AbsAgent, MultiAgentWrapper], group: str, proxy_options: dict = None, - pull_experiences_with_copy: bool = False + pull_experiences_with_copy: bool = False, + log_dir: str = getcwd() ): self.env = env self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent @@ -37,7 +39,7 @@ def __init__( if proxy_options is None: proxy_options = {} self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) - self._logger = InternalLogger(self._proxy.name) + self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): for msg in self._proxy.receive(): @@ -53,17 +55,17 @@ def run(self): self.agent.set_exploration_params(msg.body[MsgKey.EXPLORATION_PARAMS]) self.env.start(rollout_index=rollout_index) # get initial state - step_index = self.env.step_index + starting_step_index = self.env.step_index self.agent.load_model(msg.body[MsgKey.MODEL]) - num_steps = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] - while self.env.state and num_steps > 0: + steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] + while self.env.state and steps_to_go > 0: action = self.agent.choose_action(self.env.state) self.env.step(action) - num_steps -= 1 + steps_to_go -= 1 self._logger.info( f"Roll-out finished for ep {rollout_index}, segment {segment_index}" - f"(steps {step_index} - {self.env.step_index})" + f"(steps {starting_step_index} - {self.env.step_index})" ) experiences, num_exp = self.env.pull_experiences(copy=self._pull_experiences_with_copy) self._proxy.reply( @@ -75,6 +77,7 @@ def run(self): MsgKey.SEGMENT_INDEX: segment_index, MsgKey.METRICS: self.env.metrics, MsgKey.EXPERIENCES: experiences, + MsgKey.NUM_STEPS: self.env.step_index - starting_step_index, MsgKey.NUM_EXPERIENCES: num_exp } ) diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py index 6bd385f23..50f85d069 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/distributed/actor_manager.py @@ -2,10 +2,11 @@ # Licensed under the MIT license. from collections import defaultdict +from os import getcwd from typing import Union from maro.communication import Message, Proxy, SessionType -from maro.utils import InternalLogger +from maro.utils import Logger from .message_enums import MsgTag, MsgKey @@ -29,7 +30,8 @@ def __init__( num_actors: int, group_name: str, proxy_options: dict = None, - log_env_metrics: bool = False + log_env_metrics: bool = False, + log_dir: str = getcwd() ): super().__init__() peers = {"actor": num_actors} @@ -40,7 +42,7 @@ def __init__( self.total_experiences_collected = 0 self.total_env_steps = 0 self._log_env_metrics = log_env_metrics - self._logger = InternalLogger("ACTOR_MANAGER") + self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) def collect( self, @@ -92,11 +94,11 @@ def collect( if not discard_stale_experiences: experiences = msg.body[MsgKey.EXPERIENCES] self.total_experiences_collected += msg.body[MsgKey.NUM_EXPERIENCES] - self.total_env_steps += num_steps + self.total_env_steps += msg.body[MsgKey.NUM_STEPS] yield msg.body[MsgKey.EXPERIENCES], msg.body[MsgKey.ENV_END] else: self.total_experiences_collected += msg.body[MsgKey.NUM_EXPERIENCES] - self.total_env_steps += num_steps + self.total_env_steps += msg.body[MsgKey.NUM_STEPS] yield msg.body[MsgKey.EXPERIENCES], msg.body[MsgKey.ENV_END] num_finishes += 1 diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 2f3279ca5..c10b8ee7e 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -3,12 +3,13 @@ import time from collections import defaultdict +from os import getcwd from typing import Union from maro.communication import Message, Proxy, SessionType from maro.rl.agent import AbsAgent, MultiAgentWrapper from maro.rl.scheduling import Scheduler -from maro.utils import InternalLogger +from maro.utils import Logger from .actor_manager import ActorManager from .message_enums import MsgTag, MsgKey @@ -28,7 +29,8 @@ def __init__( actor_manager: ActorManager, agent_update_interval: int = -1, required_actor_finishes: str = None, - discard_stale_experiences: bool = True + discard_stale_experiences: bool = True, + log_dir: str = getcwd() ): super().__init__() self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent @@ -38,7 +40,7 @@ def __init__( self.required_actor_finishes = required_actor_finishes self.discard_stale_experiences = discard_stale_experiences self._total_learning_time = 0 - self._logger = InternalLogger("LEARNER") + self._logger = Logger("LEARNER", dump_folder=log_dir) def run(self): """Main learning loop.""" diff --git a/requirements.dev.txt b/requirements.dev.txt index cddc53a49..3a549d000 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -20,13 +20,10 @@ torchsummary==1.5.1 wrapt==1.11.2 zmq==0.0.0 numpy==1.19.1 -scipy==1.6.0 +scipy tabulate==0.8.5 networkx==2.4 -palettable==3.3.0 urllib3==1.25.8 -geopy==2.0.0 -pandas==0.25.3 redis==3.5.3 requests==2.25.1 holidays==0.10.3 From 4c31881929f92b59574d95bacb2d214e838c7632 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 12 Apr 2021 17:08:31 +0800 Subject: [PATCH 170/482] added convenience scripts for sc --- .../supply_chain/dqn/distributed_launcher.py | 8 ++- .../scripts/docker_compose_yml_generator.py | 56 ++++++------------- examples/supply_chain/scripts/kill.sh | 4 ++ examples/supply_chain/scripts/run.sh | 5 ++ 4 files changed, 33 insertions(+), 40 deletions(-) create mode 100644 examples/supply_chain/scripts/kill.sh create mode 100644 examples/supply_chain/scripts/run.sh diff --git a/examples/supply_chain/dqn/distributed_launcher.py b/examples/supply_chain/dqn/distributed_launcher.py index c2d09f306..e0abb771e 100644 --- a/examples/supply_chain/dqn/distributed_launcher.py +++ b/examples/supply_chain/dqn/distributed_launcher.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import argparse +import sys import yaml from multiprocessing import Process from os import getenv @@ -14,8 +15,11 @@ from maro.simulator import Env from maro.utils import set_seeds -from examples.supply_chain.env_wrapper import SCEnvWrapper -from examples.supply_chain.dqn.agent import get_sc_agents +sc_code_dir = dirname(dirname(__file__)) +sys.path.insert(0, sc_code_dir) +sys.path.insert(0, join(sc_code_dir, "dqn")) +from env_wrapper import SCEnvWrapper +from agent import get_sc_agents DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py index 07c50bb2f..d79820541 100644 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -1,37 +1,12 @@ -# version: "3.9" -# services: -# redis: -# image: redis:6 -# container_name: maro-redis -# learner: -# build: -# context: . -# dockerfile: docker_files/dev.df -# image: maro-dev -# container_name: learner -# volumes: -# - /home/data_disk/yaqiu/maro/examples:/maro/examples -# command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "1"] -# actor_1: -# image: maro-dev -# container_name: actor_1 -# volumes: -# - /home/data_disk/yaqiu/maro/examples:/maro/examples -# command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "2"] -# actor_2: -# image: maro-dev -# container_name: actor_2 -# volumes: -# - /home/data_disk/yaqiu/maro/examples:/maro/examples -# command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "2"] - import defaultdict import yaml +from os import makedirs from os.path import dirname, join, realpath path = realpath(__file__) -config_path = join(dirname(dirname(path)), "dqn", "config.yml") - +script_dir = dirname(path) +sc_code_dir = dirname(script_dir) +config_path = join(sc_code_dir, "dqn", "config.yml") with open(config_path, "r") as fp: config = yaml.safe_load(fp) @@ -45,17 +20,22 @@ "build": {"context": ".", "dockerfile": "docker_files/dev.df"}, "image": "maro-dev", "container_name": "learner", - "volumes": ["/home/data_disk/yaqiu/maro/examples:/maro/examples"] + "volumes": [f"{sc_code_dir}:/maro/supply_chain"], + "command": ["python3", "/maro/supply_chain/dqn/distributed_launcher.py", "-w", "1"] } } } +actor_template = docker_compose_yaml["services"]["learner"].copy() +del actor_template["build"] +actor_template["command"][-1] = "2" + +for i in range(num_actors): + actor_id = f"actor_{i}" + actor_template["container_name"] = actor_id + docker_compose_yaml["services"][actor_id] = actor_template -build: -# context: . -# dockerfile: docker_files/dev.df -# image: maro-dev -# container_name: learner -# volumes: -# - /home/data_disk/yaqiu/maro/examples:/maro/examples -# command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "1"] \ No newline at end of file +docker_compose_yaml_dir = join(sc_code_dir, "docker_compose_yamls") +makedirs(docker_compose_yaml_dir, exist_ok=True) +with open(join(docker_compose_yaml_dir, "docker-compose.yml"), "w") as fp: + yaml.safe_dump(docker_compose_yaml, fp) diff --git a/examples/supply_chain/scripts/kill.sh b/examples/supply_chain/scripts/kill.sh new file mode 100644 index 000000000..b087a2925 --- /dev/null +++ b/examples/supply_chain/scripts/kill.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# script to kill a previously launcher supply chain training job. +docker-compose -f ../docker_compose_yamls/docker-compose.yml down diff --git a/examples/supply_chain/scripts/run.sh b/examples/supply_chain/scripts/run.sh new file mode 100644 index 000000000..529a1b5d8 --- /dev/null +++ b/examples/supply_chain/scripts/run.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# script to run the supply chain scenario in single-host multi-container mode. +python3 docker_compose_yml_generator.py +docker-compose -f ../docker_compose_yamls/docker-compose.yml up From 306febe15eca811336dda25f017efd416b4154c4 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 13 Apr 2021 11:34:06 +0800 Subject: [PATCH 171/482] add dim function for env wrapper --- examples/supply_chain/env_wrapper.py | 188 ++++++++++++++++++--------- 1 file changed, 128 insertions(+), 60 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index ede9a8667..b744fd439 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -34,6 +34,19 @@ def out_of_stock(f_state): 'out_of_stock': out_of_stock } +# State extracted. +keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', + 'constraint_idx', 'is_accepted', 'consumption_hist']), + ('storage_capacity', ['storage_utilization']), + ('sale_gamma', ['sale_std', + 'sale_hist', + 'pending_order', + 'inventory_in_stock', + 'inventory_in_transit', + 'inventory_estimated', + 'inventory_rop']), + ('max_price', ['sku_price', 'sku_cost'])] + class UnitBaseInfo: id: int = None @@ -129,20 +142,42 @@ def __init__(self, env: Env): # built internal helpers. self._build_internal_helpers() + @property + def dim(self): + """Calculate dim per shape.""" + dim = 0 + + first_state = next(iter(self._states.values())) + + for _, state_keys in keys_in_state: + for key in state_keys: + val = first_state[key] + + if type(val) == list: + dim += len(val) + else: + dim += 1 + + return dim + def get_state(self, event): cur_tick = self.env.tick settings: dict = self.env.configs.settings consumption_hist_len = settings['consumption_hist_len'] hist_len = settings['sale_hist_len'] - consumption_ticks = [cur_tick - i for i in range(consumption_hist_len-1, -1, -1)] + consumption_ticks = [cur_tick - + i for i in range(consumption_hist_len-1, -1, -1)] hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] self.cur_balance_sheet_reward = self.balance_cal.calc() self._cur_metrics = self.env.metrics - self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features].flatten().reshape(-1, 2).astype(np.int) - self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"].flatten().reshape(-1, len(self.consumer_ss)) - self._cur_seller_states = self.seller_ss[hist_ticks::seller_features].astype(np.int) + self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features].flatten( + ).reshape(-1, 2).astype(np.int) + self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"].flatten( + ).reshape(-1, len(self.consumer_ss)) + self._cur_seller_states = self.seller_ss[hist_ticks::seller_features].astype( + np.int) # facility level states for facility_id in self._facility_product_utilization: @@ -151,7 +186,8 @@ def get_state(self, event): in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] - self._facility_in_transit_orders[facility_id] = [0] * self._sku_number + self._facility_in_transit_orders[facility_id] = [ + 0] * self._sku_number for sku_id, number in in_transit_orders.items(): self._facility_in_transit_orders[facility_id][sku_id] = number @@ -160,7 +196,8 @@ def get_state(self, event): # calculate storage info first, then use it later to speed up. for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"].flatten().astype(np.int) + product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"].flatten( + ).astype(np.int) for pid, index in self._storage_product_indices[facility_id].items(): product_number = product_numbers[index] @@ -219,7 +256,8 @@ def get_action(self, action_by_agent): for product_id, product in products.items(): consumer = product["consumer"] if consumer is not None: - consumer_id = ".".join(["consumer", str(consumer["id"])]) + consumer_id = ".".join( + ["consumer", str(consumer["id"])]) self.consumer2source[consumer_id] = consumer["sources"] self.consumer2product[consumer_id] = product_id @@ -232,7 +270,8 @@ def get_action(self, action_by_agent): source_id = sources[0] product_id = self.consumer2product.get(agent_id, 0) agent_id = int(agent_id.split(".")[1]) - env_action[agent_id] = ConsumerAction(agent_id, product_id, source_id, action, 1) + env_action[agent_id] = ConsumerAction( + agent_id, product_id, source_id, action, 1) # manufacturer action elif agent_id.startswith("producer"): agent_id = int(agent_id.split(".")[1]) @@ -270,8 +309,10 @@ def _update_sale_features(self, state, agent_info): if "consumer" in product_info: consumer_index = product_info["consumer"].node_index - state['consumption_hist'] = list(self._cur_consumer_states[:, consumer_index]) - state['pending_order'] = list(product_metrics["pending_order_daily"]) + state['consumption_hist'] = list( + self._cur_consumer_states[:, consumer_index]) + state['pending_order'] = list( + product_metrics["pending_order_daily"]) if "seller" in product_info: seller_index = product_info["seller"].node_index @@ -325,7 +366,8 @@ def _update_consumer_features(self, state, agent_info): service_index = state['service_level'] if service_index not in self._service_index_ppf_cache: - self._service_index_ppf_cache[service_index] = st.norm.ppf(service_index) + self._service_index_ppf_cache[service_index] = st.norm.ppf( + service_index) ppf = self._service_index_ppf_cache[service_index] @@ -341,25 +383,14 @@ def _update_global_features(self, state): def _serialize_state(self, state): result = [] - keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', - 'constraint_idx', 'is_accepted', 'consumption_hist']), - ('storage_capacity', ['storage_utilization']), - ('sale_gamma', ['sale_std', - 'sale_hist', - 'pending_order', - 'inventory_in_stock', - 'inventory_in_transit', - 'inventory_estimated', - 'inventory_rop']), - ('max_price', ['sku_price', 'sku_cost'])] - for norm, fields in keys_in_state: for field in fields: vals = state[field] if not isinstance(vals, list): vals = [vals] if norm is not None: - vals = [max(0.0, min(100.0, x / (state[norm] + 0.01))) for x in vals] + vals = [max(0.0, min(100.0, x / (state[norm] + 0.01))) + for x in vals] result.extend(vals) return np.asarray(result, dtype=np.float32) @@ -378,13 +409,15 @@ def _build_internal_helpers(self): storage = units["storage"] if storage is not None: - self.facility_levels[facility_id]["storage"] = UnitBaseInfo(storage) + self.facility_levels[facility_id]["storage"] = UnitBaseInfo( + storage) self.unit_2_facility_dict[storage["id"]] = facility_id self._facility2storage_index_dict[facility_id] = storage["node_index"] - self._storage_product_numbers[facility_id] = [0] * self._sku_number + self._storage_product_numbers[facility_id] = [ + 0] * self._sku_number self._storage_product_indices[facility_id] = {} self._facility_product_utilization[facility_id] = 0 @@ -395,7 +428,8 @@ def _build_internal_helpers(self): distribution = units["distribution"] if distribution is not None: - self.facility_levels[facility_id]["distribution"] = UnitBaseInfo(distribution) + self.facility_levels[facility_id]["distribution"] = UnitBaseInfo( + distribution) self.unit_2_facility_dict[distribution["id"]] = facility_id products = units["products"] @@ -424,7 +458,8 @@ def _build_internal_helpers(self): if manufacture is not None: product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"]] = facility_id + self.unit_2_facility_dict[manufacture["id"] + ] = facility_id self.facility_levels[facility_id][product_id] = product_info @@ -444,8 +479,10 @@ def _build_init_state(self): # facility features state["facility"] = None - state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] - state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] + state["facility_type"] = [ + 1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] + state["is_accepted"] = [0] * \ + self._configs.settings["constraint_state_hist_len"] state['constraint_idx'] = [0] state['facility_id'] = [0] * self._sku_number state['sku_info'] = {} if agent_info.is_facility else agent_info.sku @@ -458,7 +495,8 @@ def _build_init_state(self): state['facility_id'][agent_info.sku.id] = 1 for atom_name in atoms.keys(): - state[atom_name] = list(np.ones(self._configs.settings['constraint_state_hist_len'])) + state[atom_name] = list( + np.ones(self._configs.settings['constraint_state_hist_len'])) # storage features state['storage_levels'] = [0] * self._sku_number @@ -478,9 +516,11 @@ def _build_init_state(self): current_source_list = [] if agent_info.sku is not None: - current_source_list = facility["upstreams"].get(agent_info.sku.id, []) + current_source_list = facility["upstreams"].get( + agent_info.sku.id, []) - state['vlt'] = [0] * (self._max_sources_per_facility * self._sku_number) + state['vlt'] = [0] * \ + (self._max_sources_per_facility * self._sku_number) state['max_vlt'] = 0 if not agent_info.is_facility: @@ -495,7 +535,8 @@ def _build_init_state(self): # NOTE: different with original code, our config can make sure that source has product we need if sku.id == agent_info.sku.id: - state['vlt'][i * len(sku_list) + j + 1] = facility["skus"][sku.id].vlt + state['vlt'][i * len(sku_list) + j + + 1] = facility["skus"][sku.id].vlt # sale features settings = self.env.configs.settings @@ -526,7 +567,8 @@ def _build_init_state(self): state['distributor_in_transit_orders_qty'] = 0 # consumer features - state['consumer_source_export_mask'] = [0] * (self._max_sources_per_facility * self._sku_number) + state['consumer_source_export_mask'] = [0] * \ + (self._max_sources_per_facility * self._sku_number) state['consumer_source_inventory'] = [0] * self._sku_number state['consumer_in_transit_orders'] = [0] * self._sku_number @@ -544,7 +586,7 @@ def _build_init_state(self): for j, sku in enumerate(sku_list.values()): if sku.id == agent_info.sku.id: state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = \ - self.facility_levels[source]["skus"][sku.id].vlt + self.facility_levels[source]["skus"][sku.id].vlt # price features state['max_price'] = self._max_price @@ -559,11 +601,12 @@ def _build_init_state(self): class BalanceSheetCalculator: - consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") + consumer_features = ("id", "order_quantity", "price", + "order_cost", "order_product_cost") seller_features = ("id", "sold", "demand", "price", "backlog_ratio") manufacture_features = ("id", "manufacturing_number", "product_unit_cost") product_features = ( - "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") + "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") storage_features = ("capacity", "remaining_space") vehicle_features = ("id", "payload", "unit_transport_cost") @@ -601,9 +644,12 @@ def __init__(self, env: Env): facility["units"]["storage"]["config"]["unit_storage_cost"], distribution["node_index"] if distribution is not None else None, facility["downstreams"], - None if consumer is None else (consumer["id"], consumer["node_index"]), - None if seller is None else (seller["id"], seller["node_index"]), - None if manufacture is None else (manufacture["id"], manufacture["node_index"]), + None if consumer is None else ( + consumer["id"], consumer["node_index"]), + None if seller is None else ( + seller["id"], seller["node_index"]), + None if manufacture is None else ( + manufacture["id"], manufacture["node_index"]), )) self.facility_levels.append(( @@ -612,7 +658,8 @@ def __init__(self, env: Env): facility["units"]["storage"]["node_index"], facility["units"]["storage"]["config"]["unit_storage_cost"], distribution["node_index"] if distribution is not None else None, - [v["node_index"] for v in distribution["children"]] if distribution is not None else [] + [v["node_index"] for v in distribution["children"] + ] if distribution is not None else [] )) self.total_balance_sheet = defaultdict(int) @@ -628,22 +675,28 @@ def calc(self): # balance_sheet_profit = 0 # order_cost + order_product_cost - consumer_step_balance_sheet_loss = -1 * (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) + consumer_step_balance_sheet_loss = -1 * \ + (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) # not sure about this. reward_discount = 0 # consumer step reward: balance sheet los + profile * discount - consumer_step_reward = consumer_step_balance_sheet_loss + consumer_profit * reward_discount + consumer_step_reward = consumer_step_balance_sheet_loss + \ + consumer_profit * reward_discount # seller - seller_bs_states = self.seller_ss[tick::self.seller_features].flatten().reshape(-1, len(self.seller_features)) + seller_bs_states = self.seller_ss[tick::self.seller_features].flatten( + ).reshape(-1, len(self.seller_features)) # profit = sold * price - seller_balance_sheet_profit = seller_bs_states[:, 1] * seller_bs_states[:, 3] + seller_balance_sheet_profit = seller_bs_states[:, + 1] * seller_bs_states[:, 3] # loss = demand * price * backlog_ratio - seller_balance_sheet_loss = -1 * seller_bs_states[:, 2] * seller_bs_states[:, 3] * seller_bs_states[:, 4] + seller_balance_sheet_loss = -1 * \ + seller_bs_states[:, 2] * \ + seller_bs_states[:, 3] * seller_bs_states[:, 4] # step reward = loss + profit seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit @@ -653,7 +706,8 @@ def calc(self): self.manufacture_features)) # loss = manufacture number * cost - man_balance_sheet_profit_loss = -1 * man_bs_states[:, 1] * man_bs_states[:, 2] + man_balance_sheet_profit_loss = -1 * \ + man_bs_states[:, 1] * man_bs_states[:, 2] # step reward = loss man_step_reward = man_balance_sheet_profit_loss @@ -663,10 +717,12 @@ def calc(self): len(self.product_features)) # product distribution loss = check order + delay order penalty - product_distribution_balance_sheet_loss = -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) + product_distribution_balance_sheet_loss = -1 * \ + (product_bs_states[:, 3] + product_bs_states[:, 4]) # product distribution profit = check order * price - product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] + product_distribution_balance_sheet_profit = product_bs_states[:, + 2] * product_bs_states[:, 1] # result we need product_step_reward = np.zeros((len(self.products, ))) @@ -676,10 +732,13 @@ def calc(self): # create product number mapping for storages storages_product_map = {} for storage_index in range(len(self.storage_ss)): - product_list = self.storage_ss[tick:storage_index:"product_list"].flatten().astype(np.int) - product_number = self.storage_ss[tick:storage_index:"product_number"].flatten().astype(np.int) + product_list = self.storage_ss[tick:storage_index:"product_list"].flatten( + ).astype(np.int) + product_number = self.storage_ss[tick:storage_index:"product_number"].flatten( + ).astype(np.int) - storages_product_map[storage_index] = {pid: pnum for pid, pnum in zip(product_list, product_number)} + storages_product_map[storage_index] = { + pid: pnum for pid, pnum in zip(product_list, product_number)} # product balance sheet and reward # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss @@ -701,7 +760,9 @@ def calc(self): product_balance_sheet_loss[i] += man_balance_sheet_profit_loss[manufacture[1]] product_step_reward[i] += man_step_reward[manufacture[1]] - storage_reward = -1 * storages_product_map[storage_index][product_id] * unit_storage_cost + storage_reward = -1 * \ + storages_product_map[storage_index][product_id] * \ + unit_storage_cost product_step_reward[i] += storage_reward @@ -712,7 +773,7 @@ def calc(self): product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[distribution_index] product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + \ - product_distribution_balance_sheet_profit[distribution_index] + product_distribution_balance_sheet_profit[distribution_index] if downstreams and len(downstreams) > 0: if product_id in downstreams: @@ -728,16 +789,20 @@ def calc(self): product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss # storage - storage_states = self.storage_ss[tick::self.storage_features].flatten().reshape(-1, len(self.storage_features)) + storage_states = self.storage_ss[tick::self.storage_features].flatten( + ).reshape(-1, len(self.storage_features)) # loss = (capacity-remaining space) * cost - storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) + storage_balance_sheet_loss = -1 * \ + (storage_states[:, 0] - storage_states[:, 1]) # vehicles - vehicle_states = self.vehicle_ss[tick::self.vehicle_features].flatten().reshape(-1, len(self.vehicle_features)) + vehicle_states = self.vehicle_ss[tick::self.vehicle_features].flatten( + ).reshape(-1, len(self.vehicle_features)) # loss = cost * payload - vehicle_balance_sheet_loss = -1 * vehicle_states[:, 1] * vehicle_states[:, 2] + vehicle_balance_sheet_loss = -1 * \ + vehicle_states[:, 1] * vehicle_states[:, 2] vehicle_step_reward = vehicle_balance_sheet_loss facility_balance_sheet_loss = np.zeros((len(self.facility_levels),)) @@ -750,7 +815,8 @@ def calc(self): # storage balance sheet # profit=0 - facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * unit_storage_cost + facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * \ + unit_storage_cost # distribution balance sheet if distribution_index is not None: @@ -803,3 +869,5 @@ def calc(self): end_time = time() print("time cost:", end_time - start_time) + + print("dim:", ss.dim) From e277e186ef96a2eedc4f52dec054e1b7574d2c94 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 13 Apr 2021 03:39:31 +0000 Subject: [PATCH 172/482] 1. moved supply chain topology config to examples/suuply_chain; 2. added comments --- examples/supply_chain/dqn/agent.py | 24 +---- examples/supply_chain/dqn/config.yml | 98 ++++++++++++------- .../supply_chain/dqn/distributed_launcher.py | 28 +++++- .../dqn/single_thread_launcher.py | 36 ++++--- .../supply_chain/envs}/random/config.yml | 0 .../supply_chain/envs}/sample1/config.yml | 0 examples/supply_chain/scripts/build.sh | 4 + .../scripts/docker_compose_yml_generator.py | 16 ++- examples/supply_chain/scripts/kill.sh | 2 +- examples/supply_chain/scripts/run.sh | 2 +- maro/rl/training/env_wrapper.py | 4 + 11 files changed, 132 insertions(+), 82 deletions(-) rename {maro/simulator/scenarios/supply_chain/topologies => examples/supply_chain/envs}/random/config.yml (100%) rename {maro/simulator/scenarios/supply_chain/topologies => examples/supply_chain/envs}/sample1/config.yml (100%) create mode 100644 examples/supply_chain/scripts/build.sh diff --git a/examples/supply_chain/dqn/agent.py b/examples/supply_chain/dqn/agent.py index a61adf3e8..1ba4acc53 100644 --- a/examples/supply_chain/dqn/agent.py +++ b/examples/supply_chain/dqn/agent.py @@ -4,28 +4,8 @@ from maro.rl import DQN, DQNConfig, FullyConnectedBlock, MultiAgentWrapper, OptimOption, SimpleMultiHeadModel -# model input and output dimensions -PRODUCER_IN_DIM = 32 -PRODUCER_OUT_DIM = 10 -CONSUMER_IN_DIM = 32 -CONSUMER_OUT_DIM = 100 - - -def get_dqn_agent(in_dim, out_dim, config): +def get_dqn_agent(config): q_model = SimpleMultiHeadModel( - FullyConnectedBlock(input_dim=in_dim, output_dim=out_dim, **config["model"]), - optim_option=OptimOption(**config["optimization"]) + FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"]) ) return DQN(q_model, DQNConfig(**config["algorithm"]), **config["experience_memory"]) - - -def get_sc_agents(agent_info_list, config): - producer_agents = { - f"producer.{info.id}": get_dqn_agent(PRODUCER_IN_DIM, PRODUCER_OUT_DIM, config) - for info in agent_info_list - } - consumer_agents = { - f"consumer.{info.id}": get_dqn_agent(CONSUMER_IN_DIM, CONSUMER_OUT_DIM, config) - for info in agent_info_list - } - return MultiAgentWrapper({**producer_agents, **consumer_agents}) diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index bd34754a7..261adbe61 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -2,41 +2,73 @@ # Licensed under the MIT license. agent: - model: - hidden_dims: - - 16 - - 8 - activation: - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop - optim_params: - lr: 0.001 - algorithm: - reward_discount: .9 - train_iters: 10 - batch_size: 128 - loss_cls: mse - target_update_freq: 5 - soft_update_coefficient: 0.1 - double: false - experience_memory: - experience_memory_size: 50000 - experience_memory_overwrite_type: random - flush_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 16 - min_experience_memory_size: 10 + consumer: + model: + hidden_dims: + - 16 + - 8 + output_dim: 100 + activation: + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop + optim_params: + lr: 0.001 + algorithm: + reward_discount: .9 + train_iters: 10 + batch_size: 128 + loss_cls: mse + target_update_freq: 5 + soft_update_coefficient: 0.1 + double: false + experience_memory: + experience_memory_size: 50000 + experience_memory_overwrite_type: random + flush_experience_memory_after_step: false + min_new_experiences_to_trigger_learning: 1000000 + min_experience_memory_size: 10 + producer: + model: + hidden_dims: + - 16 + - 8 + output_dim: 10 + activation: + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop + optim_params: + lr: 0.001 + algorithm: + reward_discount: .9 + train_iters: 10 + batch_size: 128 + loss_cls: mse + target_update_freq: 5 + soft_update_coefficient: 0.1 + double: false + experience_memory: + experience_memory_size: 50000 + experience_memory_overwrite_type: random + flush_experience_memory_after_step: false + min_new_experiences_to_trigger_learning: 1000000 + min_experience_memory_size: 10 training: env: scenario: supply_chain topology: sample1 - durations: 200 - max_episode: 5 - agent_update_interval: 45 + durations: 250 + max_episode: 1 + agent_update_interval: 50 exploration: parameter_names: - epsilon @@ -44,7 +76,7 @@ training: end: 0.0 distributed: group: sc-dqn - num_actors: 2 + num_actors: 8 redis_host: maro-redis redis_port: 6379 - required_actor_finishes: 2 + required_actor_finishes: 8 diff --git a/examples/supply_chain/dqn/distributed_launcher.py b/examples/supply_chain/dqn/distributed_launcher.py index e0abb771e..e90a30a14 100644 --- a/examples/supply_chain/dqn/distributed_launcher.py +++ b/examples/supply_chain/dqn/distributed_launcher.py @@ -19,13 +19,18 @@ sys.path.insert(0, sc_code_dir) sys.path.insert(0, join(sc_code_dir, "dqn")) from env_wrapper import SCEnvWrapper -from agent import get_sc_agents +from agent import get_dqn_agent +# Read the configuration DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: config = yaml.safe_load(config_file) +# Get the env config path +topology = config["training"]["env"]["topology"] +config["training"]["env"]["topology"] = join(dirname(dirname(realpath(__file__))), "envs", topology) + # for distributed / multi-process training GROUP = getenv("GROUP", default=config["distributed"]["group"]) REDIS_HOST = config["distributed"]["redis_host"] @@ -34,11 +39,21 @@ def sc_dqn_learner(): - agent = get_sc_agents(Env(**config["training"]["env"]).agent_idx_list, config["agent"]) + # create agents + agent_info_list = Env(**config["training"]["env"]).agent_idx_list + producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in agent_info_list} + consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in agent_info_list} + agent = MultiAgentWrapper({**producers, **consumers}) + + # exploration schedule scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) + + # create an actor manager to collect simulation data from multiple actors actor_manager = ActorManager( NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False} ) + + # create a learner to start the training process learner = DistLearner( agent, scheduler, actor_manager, agent_update_interval=config["training"]["agent_update_interval"], @@ -49,9 +64,12 @@ def sc_dqn_learner(): def sc_dqn_actor(): - env = Env(**config["training"]["env"]) - agent = get_sc_agents(env.agent_idx_list, config["agent"]) - actor = Actor(SCEnvWrapper(env), agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}) + env = SCEnvWrapper(Env(**config["training"]["env"])) + agent_info_list = env.agent_idx_list + producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in agent_info_list} + consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in agent_info_list} + agent = MultiAgentWrapper({**producers, **consumers}) + actor = Actor(env, agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}) actor.run() diff --git a/examples/supply_chain/dqn/single_thread_launcher.py b/examples/supply_chain/dqn/single_thread_launcher.py index 8ad86a742..b0a97f614 100644 --- a/examples/supply_chain/dqn/single_thread_launcher.py +++ b/examples/supply_chain/dqn/single_thread_launcher.py @@ -11,21 +11,35 @@ from maro.simulator import Env from maro.utils import set_seeds -from examples.supply_chain.dqn.agent import get_sc_agents -from examples.supply_chain.env_wrapper import SCEnvWrapper +sc_code_dir = dirname(dirname(__file__)) +sys.path.insert(0, sc_code_dir) +sys.path.insert(0, join(sc_code_dir, "dqn")) +from env_wrapper import SCEnvWrapper +from agent import get_dqn_agent # Single-threaded launcher if __name__ == "__main__": - DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") - with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: + defualt = join(dirname(realpath(__file__)), "config.yml") + with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: config = yaml.safe_load(config_file) - set_seeds(1024) # for reproducibility - env = Env(**config["training"]["env"]) - agent = get_sc_agents(env.agent_idx_list, config["agent"]) + + # Get the env config path + topology = config["training"]["env"]["topology"] + config["training"]["env"]["topology"] = join(dirname(dirname(realpath(__file__))), "envs", topology) + + # create an env wrapper and obtain the input dimension for the pro + env = SCEnvWrapper(Env(**config["training"]["env"])) + + # create agents + agent_info_list = env.agent_idx_list + producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in agent_info_list} + consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in agent_info_list} + agent = MultiAgentWrapper({**producers, **consumers}) + + # exploration schedule scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) - learner = Learner( - SCEnvWrapper(env), agent, scheduler, - agent_update_interval=config["training"]["agent_update_interval"] - ) + + # create a learner to start training + learner = Learner(env, agent, scheduler, agent_update_interval=config["training"]["agent_update_interval"]) learner.run() diff --git a/maro/simulator/scenarios/supply_chain/topologies/random/config.yml b/examples/supply_chain/envs/random/config.yml similarity index 100% rename from maro/simulator/scenarios/supply_chain/topologies/random/config.yml rename to examples/supply_chain/envs/random/config.yml diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml b/examples/supply_chain/envs/sample1/config.yml similarity index 100% rename from maro/simulator/scenarios/supply_chain/topologies/sample1/config.yml rename to examples/supply_chain/envs/sample1/config.yml diff --git a/examples/supply_chain/scripts/build.sh b/examples/supply_chain/scripts/build.sh new file mode 100644 index 000000000..76296e2aa --- /dev/null +++ b/examples/supply_chain/scripts/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# script to build the docker image for running the supply chain scenario. +docker build -f ../../../docker_files/dev.df -t maro-dev ../../../ \ No newline at end of file diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py index d79820541..ac8d5928c 100644 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -1,5 +1,5 @@ -import defaultdict import yaml +from copy import deepcopy from os import makedirs from os.path import dirname, join, realpath @@ -11,11 +11,12 @@ with open(config_path, "r") as fp: config = yaml.safe_load(fp) num_actors = config["distributed"]["num_actors"] + redis_host = config["distributed"]["redis_host"] docker_compose_yaml = { "version": "3.9", "services": { - "redis": {"image": "redis:6", "container_name": "maro-redis"}, + "redis": {"image": "redis:6", "container_name": redis_host}, "learner": { "build": {"context": ".", "dockerfile": "docker_files/dev.df"}, "image": "maro-dev", @@ -26,16 +27,13 @@ } } -actor_template = docker_compose_yaml["services"]["learner"].copy() -del actor_template["build"] -actor_template["command"][-1] = "2" - for i in range(num_actors): actor_id = f"actor_{i}" + actor_template = deepcopy(docker_compose_yaml["services"]["learner"]) + del actor_template["build"] + actor_template["command"][-1] = "2" actor_template["container_name"] = actor_id docker_compose_yaml["services"][actor_id] = actor_template -docker_compose_yaml_dir = join(sc_code_dir, "docker_compose_yamls") -makedirs(docker_compose_yaml_dir, exist_ok=True) -with open(join(docker_compose_yaml_dir, "docker-compose.yml"), "w") as fp: +with open(join(sc_code_dir, "docker-compose.yml"), "w") as fp: yaml.safe_dump(docker_compose_yaml, fp) diff --git a/examples/supply_chain/scripts/kill.sh b/examples/supply_chain/scripts/kill.sh index b087a2925..8603a50b5 100644 --- a/examples/supply_chain/scripts/kill.sh +++ b/examples/supply_chain/scripts/kill.sh @@ -1,4 +1,4 @@ #!/bin/bash # script to kill a previously launcher supply chain training job. -docker-compose -f ../docker_compose_yamls/docker-compose.yml down +docker-compose -f ../docker-compose.yml down diff --git a/examples/supply_chain/scripts/run.sh b/examples/supply_chain/scripts/run.sh index 529a1b5d8..073b30513 100644 --- a/examples/supply_chain/scripts/run.sh +++ b/examples/supply_chain/scripts/run.sh @@ -2,4 +2,4 @@ # script to run the supply chain scenario in single-host multi-container mode. python3 docker_compose_yml_generator.py -docker-compose -f ../docker_compose_yamls/docker-compose.yml up +docker-compose -f ../docker-compose.yml up diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index 3ea2340d2..54babde42 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -36,6 +36,10 @@ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = @property def step_index(self): return self._step_index + + @property + def agent_idx_list(self): + return self.env.agent_idx_list def start(self, rollout_index: int = None): self._step_index = 0 From ea4f7b1b1e308f0366aef3e3a3bbf1376680f0af Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 13 Apr 2021 11:45:18 +0800 Subject: [PATCH 173/482] cache the dim value --- examples/supply_chain/env_wrapper.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index b744fd439..74b03d7ae 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -139,26 +139,30 @@ def __init__(self, env: Env): # current seller states self._cur_seller_states = None + # dim for state + self._dim = None + # built internal helpers. self._build_internal_helpers() @property def dim(self): """Calculate dim per shape.""" - dim = 0 + if self._dim is None: + self._dim = 0 - first_state = next(iter(self._states.values())) + first_state = next(iter(self._states.values())) - for _, state_keys in keys_in_state: - for key in state_keys: - val = first_state[key] + for _, state_keys in keys_in_state: + for key in state_keys: + val = first_state[key] - if type(val) == list: - dim += len(val) - else: - dim += 1 + if type(val) == list: + self._dim += len(val) + else: + self._dim += 1 - return dim + return self._dim def get_state(self, event): cur_tick = self.env.tick From fec60fafb0b1055cb6c54126608d543fc129ffd4 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 13 Apr 2021 10:53:59 +0000 Subject: [PATCH 174/482] refined sc scripts and code --- examples/supply_chain/dqn/agent.py | 2 +- examples/supply_chain/dqn/config.yml | 4 ++-- .../supply_chain/dqn/distributed_launcher.py | 22 ++++++++++++------- .../dqn/single_thread_launcher.py | 12 +++++----- examples/supply_chain/scripts/build.sh | 3 ++- .../scripts/docker_compose_yml_generator.py | 6 +++-- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/examples/supply_chain/dqn/agent.py b/examples/supply_chain/dqn/agent.py index 1ba4acc53..4ea09980a 100644 --- a/examples/supply_chain/dqn/agent.py +++ b/examples/supply_chain/dqn/agent.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl import DQN, DQNConfig, FullyConnectedBlock, MultiAgentWrapper, OptimOption, SimpleMultiHeadModel +from maro.rl import DQN, DQNConfig, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel def get_dqn_agent(config): diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index 261adbe61..43a213992 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -65,7 +65,7 @@ agent: training: env: scenario: supply_chain - topology: sample1 + topology: random durations: 250 max_episode: 1 agent_update_interval: 50 @@ -77,6 +77,6 @@ training: distributed: group: sc-dqn num_actors: 8 - redis_host: maro-redis + redis_host: maro-sc redis_port: 6379 required_actor_finishes: 8 diff --git a/examples/supply_chain/dqn/distributed_launcher.py b/examples/supply_chain/dqn/distributed_launcher.py index e90a30a14..a6064d41e 100644 --- a/examples/supply_chain/dqn/distributed_launcher.py +++ b/examples/supply_chain/dqn/distributed_launcher.py @@ -15,7 +15,7 @@ from maro.simulator import Env from maro.utils import set_seeds -sc_code_dir = dirname(dirname(__file__)) +sc_code_dir = dirname(dirname(realpath(__file__))) sys.path.insert(0, sc_code_dir) sys.path.insert(0, join(sc_code_dir, "dqn")) from env_wrapper import SCEnvWrapper @@ -39,10 +39,11 @@ def sc_dqn_learner(): - # create agents - agent_info_list = Env(**config["training"]["env"]).agent_idx_list - producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in agent_info_list} - consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in agent_info_list} + # create agents that house the latest models. + env = SCEnvWrapper(Env(**config["training"]["env"])) + config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim + producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in env.agent_idx_list} + consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} agent = MultiAgentWrapper({**producers, **consumers}) # exploration schedule @@ -64,11 +65,16 @@ def sc_dqn_learner(): def sc_dqn_actor(): + # create an env for roll-out env = SCEnvWrapper(Env(**config["training"]["env"])) - agent_info_list = env.agent_idx_list - producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in agent_info_list} - consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in agent_info_list} + config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim + + # create agents that will interact with the env + producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in env.agent_idx_list} + consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} agent = MultiAgentWrapper({**producers, **consumers}) + + # create an actor that collects simulation data for the learner. actor = Actor(env, agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}) actor.run() diff --git a/examples/supply_chain/dqn/single_thread_launcher.py b/examples/supply_chain/dqn/single_thread_launcher.py index b0a97f614..54d9aaff1 100644 --- a/examples/supply_chain/dqn/single_thread_launcher.py +++ b/examples/supply_chain/dqn/single_thread_launcher.py @@ -1,17 +1,18 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import sys import yaml from os import getenv from os.path import dirname, join, realpath import numpy as np -from maro.rl import Learner, LinearParameterScheduler +from maro.rl import Learner, LinearParameterScheduler, MultiAgentWrapper from maro.simulator import Env from maro.utils import set_seeds -sc_code_dir = dirname(dirname(__file__)) +sc_code_dir = dirname(dirname(realpath(__file__))) sys.path.insert(0, sc_code_dir) sys.path.insert(0, join(sc_code_dir, "dqn")) from env_wrapper import SCEnvWrapper @@ -20,7 +21,7 @@ # Single-threaded launcher if __name__ == "__main__": - defualt = join(dirname(realpath(__file__)), "config.yml") + default_config_path = join(dirname(realpath(__file__)), "config.yml") with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: config = yaml.safe_load(config_file) @@ -28,9 +29,10 @@ topology = config["training"]["env"]["topology"] config["training"]["env"]["topology"] = join(dirname(dirname(realpath(__file__))), "envs", topology) - # create an env wrapper and obtain the input dimension for the pro + # create an env wrapper and obtain the input dimension for the agents env = SCEnvWrapper(Env(**config["training"]["env"])) - + config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim + # create agents agent_info_list = env.agent_idx_list producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in agent_info_list} diff --git a/examples/supply_chain/scripts/build.sh b/examples/supply_chain/scripts/build.sh index 76296e2aa..d86ecb01f 100644 --- a/examples/supply_chain/scripts/build.sh +++ b/examples/supply_chain/scripts/build.sh @@ -1,4 +1,5 @@ #!/bin/bash # script to build the docker image for running the supply chain scenario. -docker build -f ../../../docker_files/dev.df -t maro-dev ../../../ \ No newline at end of file +docker pull redis:6 +docker build -f ../../../docker_files/dev.df -t maro-sc:latest ../../../ diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py index ac8d5928c..f429edafe 100644 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -6,7 +6,9 @@ path = realpath(__file__) script_dir = dirname(path) sc_code_dir = dirname(script_dir) +root_dir = dirname(dirname(sc_code_dir)) config_path = join(sc_code_dir, "dqn", "config.yml") +dockerfile_path = join(root_dir, "docker_files", "dev.df") with open(config_path, "r") as fp: config = yaml.safe_load(fp) @@ -18,8 +20,8 @@ "services": { "redis": {"image": "redis:6", "container_name": redis_host}, "learner": { - "build": {"context": ".", "dockerfile": "docker_files/dev.df"}, - "image": "maro-dev", + "build": {"context": root_dir, "dockerfile": dockerfile_path}, + "image": "maro-sc", "container_name": "learner", "volumes": [f"{sc_code_dir}:/maro/supply_chain"], "command": ["python3", "/maro/supply_chain/dqn/distributed_launcher.py", "-w", "1"] From cc5753eb96aca0b895973c3c20a1e83285c47d07 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 13 Apr 2021 15:11:01 +0000 Subject: [PATCH 175/482] added total reward logging --- examples/cim/dqn/config.yml | 2 +- examples/supply_chain/dqn/config.yml | 14 ++++++------- maro/rl/distributed/actor.py | 26 ++++++++++++------------ maro/rl/distributed/actor_manager.py | 30 +++++++++++++++------------- maro/rl/distributed/learner.py | 6 +++--- maro/rl/distributed/message_enums.py | 2 ++ maro/rl/training/env_wrapper.py | 12 ++++++++--- maro/rl/training/learner.py | 22 +++++++++----------- 8 files changed, 60 insertions(+), 54 deletions(-) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 6bd3a87d0..aef97a626 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -32,7 +32,7 @@ agent: - 256 - 128 - 64 - activation: + activation: leaky_relu softmax: false batch_norm: true skip_connection: false diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index 43a213992..f21d47a10 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -8,7 +8,7 @@ agent: - 16 - 8 output_dim: 100 - activation: + activation: leaky_relu softmax: false batch_norm: true skip_connection: false @@ -30,7 +30,7 @@ agent: experience_memory_size: 50000 experience_memory_overwrite_type: random flush_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 1000000 + min_new_experiences_to_trigger_learning: 10 min_experience_memory_size: 10 producer: model: @@ -38,7 +38,7 @@ agent: - 16 - 8 output_dim: 10 - activation: + activation: leaky_relu softmax: false batch_norm: true skip_connection: false @@ -60,14 +60,14 @@ agent: experience_memory_size: 50000 experience_memory_overwrite_type: random flush_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 1000000 + min_new_experiences_to_trigger_learning: 16000 min_experience_memory_size: 10 training: env: scenario: supply_chain - topology: random + topology: sample1 durations: 250 - max_episode: 1 + max_episode: 4 agent_update_interval: 50 exploration: parameter_names: @@ -77,6 +77,6 @@ training: distributed: group: sc-dqn num_actors: 8 - redis_host: maro-sc + redis_host: localhost redis_port: 6379 required_actor_finishes: 8 diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 839da373f..5aa6f8d4b 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -68,16 +68,16 @@ def run(self): f"(steps {starting_step_index} - {self.env.step_index})" ) experiences, num_exp = self.env.pull_experiences(copy=self._pull_experiences_with_copy) - self._proxy.reply( - msg, - tag=MsgTag.ROLLOUT_DONE, - body={ - MsgKey.ENV_END: not self.env.state, - MsgKey.ROLLOUT_INDEX: rollout_index, - MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.METRICS: self.env.metrics, - MsgKey.EXPERIENCES: experiences, - MsgKey.NUM_STEPS: self.env.step_index - starting_step_index, - MsgKey.NUM_EXPERIENCES: num_exp - } - ) + return_info = { + MsgKey.ENV_END: not self.env.state, + MsgKey.ROLLOUT_INDEX: rollout_index, + MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.EXPERIENCES: experiences, + MsgKey.NUM_STEPS: self.env.step_index - starting_step_index, + MsgKey.NUM_EXPERIENCES: num_exp + } + if msg.body[MsgKey.RETURN_ENV_METRICS]: + return_info[MsgKey.METRICS] = self.env.metrics + if not self.env.state: + return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward + self._proxy.reply(msg, tag=MsgTag.ROLLOUT_DONE, body=return_info) diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py index 50f85d069..7616d2b49 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/distributed/actor_manager.py @@ -41,6 +41,7 @@ def __init__( self._actors = self._proxy.peers["actor"] # remote actor ID's self.total_experiences_collected = 0 self.total_env_steps = 0 + self.total_reward = defaultdict(float) self._log_env_metrics = log_env_metrics self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) @@ -52,7 +53,8 @@ def collect( models: dict = None, exploration_params=None, required_actor_finishes: int = None, - discard_stale_experiences: bool = True + discard_stale_experiences: bool = True, + return_env_metrics: bool = False ): """Collect experiences from actors.""" if required_actor_finishes is None: @@ -62,7 +64,8 @@ def collect( MsgKey.ROLLOUT_INDEX: rollout_index, MsgKey.SEGMENT_INDEX: segment_index, MsgKey.NUM_STEPS: num_steps, - MsgKey.MODEL: models + MsgKey.MODEL: models, + MsgKey.RETURN_ENV_METRICS: return_env_metrics } if exploration_params: @@ -70,7 +73,8 @@ def collect( if self._log_env_metrics: self._logger.info(f"EPISODE-{rollout_index}, SEGMENT-{segment_index}: ") - self._logger.info(f"exploration_params: {exploration_params}") + if exploration_params: + self._logger.info(f"exploration_params: {exploration_params}") self._proxy.ibroadcast("actor", MsgTag.ROLLOUT, SessionType.TASK, body=msg_body) self._logger.info(f"Sent roll-out requests for ep-{rollout_index}, segment-{segment_index}") @@ -90,20 +94,18 @@ def collect( env_metrics = msg.body[MsgKey.METRICS] self._logger.info(f"env_metrics: {env_metrics}") - if msg.body[MsgKey.SEGMENT_INDEX] != segment_index: - if not discard_stale_experiences: - experiences = msg.body[MsgKey.EXPERIENCES] - self.total_experiences_collected += msg.body[MsgKey.NUM_EXPERIENCES] - self.total_env_steps += msg.body[MsgKey.NUM_STEPS] - yield msg.body[MsgKey.EXPERIENCES], msg.body[MsgKey.ENV_END] - else: + if msg.body[MsgKey.SEGMENT_INDEX] == segment_index or not discard_stale_experiences: self.total_experiences_collected += msg.body[MsgKey.NUM_EXPERIENCES] self.total_env_steps += msg.body[MsgKey.NUM_STEPS] - yield msg.body[MsgKey.EXPERIENCES], msg.body[MsgKey.ENV_END] - num_finishes += 1 + is_env_end = msg.body[MsgKey.ENV_END] + if is_env_end: + self._logger.info(f"total rewards: {msg.body[MsgKey.TOTAL_REWARD]}") + yield msg.body[MsgKey.EXPERIENCES], is_env_end - if num_finishes == required_actor_finishes: - break + if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: + num_finishes += 1 + if num_finishes == required_actor_finishes: + break def exit(self): """Tell the remote actors to exit.""" diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index c10b8ee7e..18a292f11 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -61,9 +61,9 @@ def run(self): updated_agents = self.agent.learn(exp) num_actor_finishes += done self._total_learning_time += time.time() - tl0 - self._logger.info(f"total running time: {time.time() - t0}") - self._logger.info(f"total learning time: {self._total_learning_time}") - self._logger.info(f"total env steps: {self.actor_manager.total_env_steps}") + self._logger.debug(f"total running time: {time.time() - t0}") + self._logger.debug(f"total learning time: {self._total_learning_time}") + self._logger.debug(f"total env steps: {self.actor_manager.total_env_steps}") self._logger.info(f"total experiences collected: {self.actor_manager.total_experiences_collected}") segment_index += 1 diff --git a/maro/rl/distributed/message_enums.py b/maro/rl/distributed/message_enums.py index 08d84cb49..cf60aa879 100644 --- a/maro/rl/distributed/message_enums.py +++ b/maro/rl/distributed/message_enums.py @@ -28,4 +28,6 @@ class MsgKey(Enum): EXPLORATION_PARAMS = "exploration_params" NUM_STEPS = "num_steps" SEGMENT_INDEX = "segment_index" + RETURN_ENV_METRICS = "return_env_metrics" + TOTAL_REWARD = "total_reward" ENV_END = "env_end" diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index 54babde42..f0075ec47 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -28,10 +28,9 @@ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = self.state_info = None # context for converting model output to actions that can be executed by the env self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay + self._total_reward = 0 self._state = None # the latest extracted state is kept here self._acting_agents = deque() # list of (tick, acting_agent_list) for delayed reward evaluation - # self._tot_raw_step_time = 0 - # self._tot_step_time = 0 @property def step_index(self): @@ -73,6 +72,10 @@ def metrics(self): def state(self): return self._state + @property + def total_reward(self): + return self._total_reward + @abstractmethod def get_state(self, event) -> dict: pass @@ -118,7 +121,9 @@ def step(self, action_by_agent: dict): reward = self.get_reward(tick=self._acting_agents[0][0], target_agents=self._acting_agents[0][1]) # assign rewards to the relevant agents for agent_id in self._acting_agents[0][1]: - self.replay[agent_id]["R"].append(reward[agent_id]) + rw = reward.get(agent_id, 0) + self.replay[agent_id]["R"].append(rw) + self._total_reward += rw self._acting_agents.popleft() if not done: @@ -144,6 +149,7 @@ def step(self, action_by_agent: dict): def reset(self): self.env.reset() self.state_info = None + self._total_reward = 0 self._state = None self._acting_agents.clear() self.replay = defaultdict(lambda: defaultdict(list)) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 93dda8ec9..92f3b9d92 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -52,26 +52,22 @@ def run(self): while self.env.state: action = self.agent.choose_action(self.env.state) self.env.step(action) - if self.agent_update_interval != -1 and self.env.step_index % self.agent_update_interval == 0: + if ( + not self.env.state or + self.agent_update_interval != -1 and self.env.step_index % self.agent_update_interval == 0 + ): exp, num_exp = self.env.pull_experiences() tl0 = time.time() self.agent.learn(exp) self.total_learning_time += time.time() - tl0 self.total_env_steps += self.agent_update_interval self.total_experiences_collected += num_exp - self._logger.info(f"total running time: {time.time() - t0}") - self._logger.info(f"total learning time: {self.total_learning_time}") - self._logger.info(f"total env steps: {self.total_env_steps}") + self._logger.debug(f"total running time: {time.time() - t0}") + self._logger.debug(f"total learning time: {self.total_learning_time}") + self._logger.debug(f"total env steps: {self.total_env_steps}") self._logger.info(f"total experiences collected: {self.total_experiences_collected}") + if not self.env.state: + self._logger.info(f"total reward: {self.env.total_reward}") - exp, num_exp = self.env.pull_experiences() - tl0 = time.time() - self.agent.learn(exp) - self.total_learning_time += time.time() - tl0 if self._log_env_metrics: self._logger.info(f"ep-{self.scheduler.iter}: {self.env.metrics} ({exploration_params})") - - self._logger.info(f"total running time: {time.time() - t0}") - self._logger.info(f"total learning time: {self.total_learning_time}") - self._logger.info(f"total env steps: {self.total_env_steps}") - self._logger.info(f"total experiences collected: {self.total_experiences_collected}") From 9f4eee0c91411039e55639b8cce6a9d776742566 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 13 Apr 2021 16:16:56 +0000 Subject: [PATCH 176/482] added README.md for supply_chain and added COMPONENT_NAME env variable to docker-compose manifest generator --- examples/supply_chain/dqn/README.md | 5 +++++ examples/supply_chain/dqn/config.yml | 18 +++++++++--------- .../supply_chain/dqn/distributed_launcher.py | 2 +- .../scripts/docker_compose_yml_generator.py | 3 ++- 4 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 examples/supply_chain/dqn/README.md diff --git a/examples/supply_chain/dqn/README.md b/examples/supply_chain/dqn/README.md new file mode 100644 index 000000000..581204211 --- /dev/null +++ b/examples/supply_chain/dqn/README.md @@ -0,0 +1,5 @@ +Running the supply chain scenario consists of 3 simple steps: + +1. To build the docker images required for running the supply chain scenario, go to examples/suuply_chain/scripts and run ```bash build.sh```. This step only needs to be done once, unless changes are made to the source code in maro/maro. +2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the ```config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). +3. After the program terminates, execute ```bash kill.sh``` to clean up the containers launched in Step 2. \ No newline at end of file diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index f21d47a10..de0c90b67 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -25,7 +25,7 @@ agent: loss_cls: mse target_update_freq: 5 soft_update_coefficient: 0.1 - double: false + double: false # double DQN experience_memory: experience_memory_size: 50000 experience_memory_overwrite_type: random @@ -60,15 +60,15 @@ agent: experience_memory_size: 50000 experience_memory_overwrite_type: random flush_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 16000 + min_new_experiences_to_trigger_learning: 100000 min_experience_memory_size: 10 training: env: scenario: supply_chain - topology: sample1 - durations: 250 - max_episode: 4 - agent_update_interval: 50 + topology: random + durations: 125 + max_episode: 1 + agent_update_interval: 25 exploration: parameter_names: - epsilon @@ -76,7 +76,7 @@ training: end: 0.0 distributed: group: sc-dqn - num_actors: 8 - redis_host: localhost + num_actors: 16 + redis_host: maro-sc redis_port: 6379 - required_actor_finishes: 8 + required_actor_finishes: 16 diff --git a/examples/supply_chain/dqn/distributed_launcher.py b/examples/supply_chain/dqn/distributed_launcher.py index a6064d41e..cb82f8b45 100644 --- a/examples/supply_chain/dqn/distributed_launcher.py +++ b/examples/supply_chain/dqn/distributed_launcher.py @@ -39,7 +39,7 @@ def sc_dqn_learner(): - # create agents that house the latest models. + # create agents that update themselves using experiences collected from the actors. env = SCEnvWrapper(Env(**config["training"]["env"])) config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in env.agent_idx_list} diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py index f429edafe..53718ac1d 100644 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -30,11 +30,12 @@ } for i in range(num_actors): - actor_id = f"actor_{i}" + actor_id = f"actor.{i}" actor_template = deepcopy(docker_compose_yaml["services"]["learner"]) del actor_template["build"] actor_template["command"][-1] = "2" actor_template["container_name"] = actor_id + actor_template["environment"] = [f"COMPONENT_NAME={actor_id}"] docker_compose_yaml["services"][actor_id] = actor_template with open(join(sc_code_dir, "docker-compose.yml"), "w") as fp: From 808ef8498d173c654bcf77679f705fece1509d11 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 14 Apr 2021 08:30:53 +0800 Subject: [PATCH 177/482] add state desc --- examples/supply_chain/sc_state_in_maro.md | 312 ++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 examples/supply_chain/sc_state_in_maro.md diff --git a/examples/supply_chain/sc_state_in_maro.md b/examples/supply_chain/sc_state_in_maro.md new file mode 100644 index 000000000..0d512e25d --- /dev/null +++ b/examples/supply_chain/sc_state_in_maro.md @@ -0,0 +1,312 @@ +# SC state in MARO + + +## Env.summary + +MARO通过summary属性对外提供节点相关信息和其他在环境初始化后不会改变的信息。 +在supply chain场景中, summary中包括了以下部分 + +### unit mapping: + +```python +env.summary["node_mapping"]["unit_mapping"] +``` + +unit id 及其对应的data model名字和索引. + +### facilities: + +```python +env.summary["node_mapping"]["facilities"] +``` + +每个facility的层次结构,如sku list, units + +### skus: + +```python +env.summary["node_mapping"]["skus"] +``` + +当前配置中的所有sku + +### max_price: + +```python +env.summary["node_mapping"]["max_price"] +``` + +当前配置中最大的price + +### max_sources_per_facility: + +```python +env.summary["node_mapping"]["max_sources_per_facility"] +``` + +## States + +MARO中有两种对外提供动态状态(state)的方法。 + + +### Metrics: + +起初是为了对外提供reward相关的信息,但是也可以用来作为对外提供状态的接口,用来对外提供不能存放在snapshot list中的数据,比如字典,或者复杂的数据结构。 + +当前实现包含了以下内容: + +#### products: + +product unit相关信息(sale_mean, sale_std, pending_order_daily)。 + +#### facilities: + +facility相关信息(in_transit_orders, pending_order) + + +### Snapshot list: + +snapshot list是MARO中主要的对外提供状态的接口,它提供了所有节点的历史状态(默认为保存所有历史记录,可配置保存最新的N个来节省内存).返回的结果是numpy array,适合做batch操作。 + +snapshot list中的属性都是按照节点组织起来的,每个节点包括多个属性,每个属性可以有多个slot(array like), 同一种节点类型可以有多个instance.节点及其属性的定义可以在maro/simulator/scenarios/supply_chain/datamodels查看。 + +snapshot list的查询是通过slice接口实现的,形式如下: + +```python +env.snapshot_list["node name"][tick(s):node index(s):attribute name(s)] -> np.array + +``` + +该接口返回的是一个4维(tick, node, attribute, slot)的numpy数组(float). + +其中: +1. node name是定义节点是通过node装饰器提供的名字,当前实现包括如下节点: + +consumer, distribution, facility, manufacture, product, seller, storage, vehicle + + +2. tick(s): 可以是一个int, list或者None, 其中None表示查询当前所有历史记录的状态。 + +3. node index(s): 同tick,可以为int, list或者None,None表示查询当前节点类型的所有实例(instance). 使用中需要注意的是,节点(data model)的index和unit的id并不相同,unit的id是在facility和unit之间连续且唯一的,但是节点的index是每种data model类型内部的索引方式。 +所以在实际使用过程中,通常需要得到每个unit和facility对应的index,这部分信息在env.summary中可以得到。 + +4. attribute name(s): 在对应节点上定义过的属性名,可以为一个str,或者List[str] + + +## 示例 + +### 示例1:通过env.summary构建facility&unit的层级信息 + +这个层级信息可以帮我们在之后的操作中快速索引。 +更详细的可以参考examples/supply_chain/env_wrapper.py, _build_internal_helpers方法。 + +```python +# unit info +class UnitBaseInfo: + id: int = None + node_index: int = None + config: dict = None + summary: dict = None + + def __init__(self, unit_summary): + self.id = unit_summary["id"] + self.node_index = unit_summary["node_index"] + self.config = unit_summary.get("config", {}) + self.summary = unit_summary + + def __getitem__(self, key, default=None): + if key in self.summary: + return self.summary[key] + + return default + +# facility id -> { +# data_model_index: int, +# storage: UnitBaseInfo +# distribution: UnitBaseInfo +# product_id: { +# consumer: UnitBaseInfo +# seller: UnitBaseInfo +# manufacture: UnitBaseInfo +# } +#} +facility_levels = {} + +# 默认env.summary包含node_mapping, node_detail 和 event_payload3个部分, +# 这里只需要node——mapping +summary = env.summary["node_mapping"] + +for facility_id, facility in summary["facilities"].items(): + facility_levels[facility_id] = { + "node_index": facility["node_index"], + "config": facility["configs"], + "upstreams": facility["upstreams"], + "skus": facility["skus"] + } + + # facility所属的unit都挂在units下面。 + units = facility["units"] + + facility_levels[facility_id]["storage"] = UnitBaseInfo(units["storage"]) + facility_levels[facility_id]["distribution"] = UnitBaseInfo(units["distribution"]) + + # 所有的product unit + product_units = units["products"] + + if product_units: + for product_id, product in products.items(): + # product unit 本身也包含state + product_info = { + "product": UnitBaseInfo(product) + } + + # 每个product unit可能包括下面3个unit + # 注意,为了简单我们没有检查对应的key时候存在! + product_info["seller"] = UnitBaseInfo(product["seller"]) + product_info["consumer"] = UnitBaseInfo(product["consumer"]) + product_info["manufacture"] = UnitBaseInfo(product["manufacture"]) + + # 这里我们用product_id作为product 的key,可按照需求更改 + facility_levels[product_id] = product_info +``` + +### 示例2:通过env.summary构建unit id到node index的索引表 + +实际上,在示例的遍历过程中,我们就已经可以得到unit及其对应的node index索引表了,如果你不在意层级关系的话,可以通过unit_mapping快速得到这个索引。 + +```python + +# unit_mapping是一个字典,key是unit id, value是(data model name, data model node index, facility id)类型的tuple。 + +summary = env.summary["node_mapping"] + +unitid2index_mapping = {} + +for unit_id, unit_detail in summary["unit_mapping"].items(): + unitid2index_mapping[unit_id] = unit_detail[1] + +``` + +### 示例3:在state shaping过程中查询seller的销售和需求的历史,时间长度为hist_len + +```python + +# 模拟器当前时间 +cur_tick = env.tick + +# 需要查询的历史长度 +hist_len = 4 + +# 历史长度对象当前时间的时间序列 +ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] + +# 查询seller节点的过去4(含当前)个tick的sold和demand值 +# NOTE:因为这两个是都是整数,所以做一次类型转换 +seller_states =env.snapshot_list["seller"][ticks::("sold", "demand")].astype(np.int) + +# 结果应为4为numpy array +# 假设我们有2个seller +""" +[ + [ + [ + [0.0], # sold (slot = 1) + [0.0] # demand (slot = 1) + ], # seller 0 + [...] # seller 1 + ], # tick 0 + [ + [...], + [...] + ], # tick 1 + [ + [...], + [...] + ], # tick 2 + [ + [...], + [...] + ] # tick 3 (latest) +] +""" + +# 这样得到的结果就是所有的seller unit对应的销售和需求历史。 + +# 假设我们当前需要的seller unit的data model index 为 1 的话。 +cur_seller_node_index = 1 + +# 那么当前seller的销售和需求历史分别为: +cur_seller_hist = seller_states[:, cur_seller_node_index, :] + +# 第二个参数为0,是因为sold是我们查询的第一个属性 +sale_hist = cur_seller_hist[:, 0].flatten() +demand_hist = cur_seller_hist[:, 1].flatten() + +``` + +### 示例4:计算unit或facility的balance sheet + +详细的可以参考examples/supply_chain/env_wrapper.py中的BalanceSheetCalculator类。 + +```python + +# 假设我们需要计算seller, consumer, manufacture的balance sheet. +# 实际情况需要用这3个计算出对应的product unit的balance sheet,这里只是作为示例 + +# 计算所需要属性 +consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") +seller_features = ("id", "sold", "demand", "price", "backlog_ratio") +manufacture_features = ("id", "manufacturing_number", "product_unit_cost") + +# 对应的3种data model snapshot list +consumer_ss = env.snapshot_list["consumer"] +seller_ss = env.snapshot_list["seller"] +manufacture_ss = env.snapshot_list["manufacture"] + +# 当前时间 +tick = env.tick + +# 3种unit对应的所有实例的state +# 这里用len(features)做reshape的原因是,当前用到的属性都是slots=1 +# 又因为我们的tick数量为1,这样reshape之后, 每行对应一个unit的实例,每列对应一个属性 +consumer_states = consumer_ss[tick::consumer_features].flatten().reshape(-1, len(consumer_features)) + +seller_states = seller_ss[tick::seller_features].flatten().reshape(-1, len(seller_features)) + +man_states = manufacture_ss[tick::manufacture_features].flatten().reshape(-1, len(manufacture_features)) + +# balance sheet计算,通常balance sheet 包含profit和loss两部分,这里分开保存。 + +# consumer部分 +# profit = quantity * price +consumer_profit = consumer_states[:, 1] * consumer_states[:, 2] + +# loss = -1 * (order_cost + order_product_cost) +consumer_loss = -1 * (consumer_states[:, 3] + consumer_states[:, 4]) + +# discount在代码里似乎没有用到 +reward_discount = 0 + +# consumer step reward +consumer_reward = consumer_loss + consumer_profit * reward_discount + +# seller部分 +# profit = sold * price +seller_profit = seller_states[:, 1] * seller_states[:, 3] + +# loss = -1 * demand * price * backlog_ratio +seller_loss = -1 * seller_states[:, 2] * seller_states[:, 3] * seller_states[:, 4] + +seller_reward = seller_profit + seller_loss + +# manufacture部分 +# profit = 0 +# loss = manufacture_number * cost +man_loss = -1 * man_states[:, 1] * man_states[:, 2] + +man_reward = man_loss + +# 这样我们就用numpy的batch操作完成3种unit的balance sheet和reward计算, +# 后续需要我们按照product/facility对这些结果做聚合, 这需要类似示例1这样的层级结构,具体可参考现有代码。 + +``` \ No newline at end of file From ede10278231e41bf048dff8c00a31e7d229c4953 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 14 Apr 2021 06:25:54 +0000 Subject: [PATCH 178/482] 1. added redis ping retries; 2. added log folder --- examples/supply_chain/dqn/README.md | 2 +- examples/supply_chain/dqn/config.yml | 15 ++++++----- .../supply_chain/dqn/distributed_launcher.py | 7 ++--- maro/communication/proxy.py | 26 +++++++++++++++---- maro/rl/distributed/actor.py | 5 +++- maro/rl/distributed/actor_manager.py | 5 +++- maro/rl/distributed/learner.py | 5 +++- 7 files changed, 46 insertions(+), 19 deletions(-) diff --git a/examples/supply_chain/dqn/README.md b/examples/supply_chain/dqn/README.md index 581204211..6eb3a8aef 100644 --- a/examples/supply_chain/dqn/README.md +++ b/examples/supply_chain/dqn/README.md @@ -2,4 +2,4 @@ Running the supply chain scenario consists of 3 simple steps: 1. To build the docker images required for running the supply chain scenario, go to examples/suuply_chain/scripts and run ```bash build.sh```. This step only needs to be done once, unless changes are made to the source code in maro/maro. 2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the ```config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). -3. After the program terminates, execute ```bash kill.sh``` to clean up the containers launched in Step 2. \ No newline at end of file +3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml index de0c90b67..b846e5b23 100644 --- a/examples/supply_chain/dqn/config.yml +++ b/examples/supply_chain/dqn/config.yml @@ -65,10 +65,10 @@ agent: training: env: scenario: supply_chain - topology: random - durations: 125 - max_episode: 1 - agent_update_interval: 25 + topology: sample1 + durations: 200 + max_episode: 3 + agent_update_interval: 35 exploration: parameter_names: - epsilon @@ -76,7 +76,8 @@ training: end: 0.0 distributed: group: sc-dqn - num_actors: 16 - redis_host: maro-sc + num_actors: 4 + redis_host: redis-sc redis_port: 6379 - required_actor_finishes: 16 + required_actor_finishes: 4 + discard_stale_experiences: True diff --git a/examples/supply_chain/dqn/distributed_launcher.py b/examples/supply_chain/dqn/distributed_launcher.py index cb82f8b45..67b4264fd 100644 --- a/examples/supply_chain/dqn/distributed_launcher.py +++ b/examples/supply_chain/dqn/distributed_launcher.py @@ -3,6 +3,7 @@ import argparse import sys +import time import yaml from multiprocessing import Process from os import getenv @@ -59,17 +60,17 @@ def sc_dqn_learner(): agent, scheduler, actor_manager, agent_update_interval=config["training"]["agent_update_interval"], required_actor_finishes=config["distributed"]["required_actor_finishes"], - discard_stale_experiences=False + discard_stale_experiences=config["distributed"]["discard_stale_experiences"] ) learner.run() def sc_dqn_actor(): - # create an env for roll-out + # create an env wrapper for roll-out and obtain the input dimension for the agents env = SCEnvWrapper(Env(**config["training"]["env"])) config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - # create agents that will interact with the env + # create agents to interact with the env producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in env.agent_idx_list} consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} agent = MultiAgentWrapper({**producers, **consumers}) diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 110b8a14b..524577ec1 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -112,13 +112,29 @@ def __init__( self._logger.error(f"Unsupported driver type {driver_type}, please use DriverType class.") sys.exit(NON_RESTART_EXIT_CODE) - # Initialize the Redis. + # Initialize connection to the redis server. self._redis_connection = redis.Redis(host=redis_address[0], port=redis_address[1], socket_keepalive=True) - try: - self._redis_connection.ping() - except Exception as e: - self._logger.error(f"{self._name} failure to connect to redis server due to {e}") + num_tries = 0 + while num_tries < self._max_retries: + try: + self._redis_connection.ping() + break + except Exception as e: + retry_time = self._retry_interval_base_value * (2 ** retry_number) + self._logger.error( + f"{self._name} failed to connect to the redis server due to {e}. Retrying in {retry_time} seconds." + ) + num_tries += 1 + time.sleep(retry_time) + + if num_tries == self._max_retries: + self._logger.error(f"{self._name} failed to connect to the redis server.") sys.exit(NON_RESTART_EXIT_CODE) + else: + self._logger.info( + f"{self._name} is successfully connected to the redis server " + f"at {redis_address[0]}:{redis_address[1]}." + ) # Record the peer's redis information. self._peers_info_dict = {} diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 5aa6f8d4b..0204402f9 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from os import getcwd +from os import getcwd, makedirs +from os.path import join from typing import Union from maro.communication import Message, Proxy @@ -39,6 +40,8 @@ def __init__( if proxy_options is None: proxy_options = {} self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) + log_dir = join(log_dir, "logs") + makedirs(log_dir, exist_ok=True) self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py index 7616d2b49..a4b534c6a 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/distributed/actor_manager.py @@ -2,7 +2,8 @@ # Licensed under the MIT license. from collections import defaultdict -from os import getcwd +from os import getcwd, makedirs +from os.path import join from typing import Union from maro.communication import Message, Proxy, SessionType @@ -43,6 +44,8 @@ def __init__( self.total_env_steps = 0 self.total_reward = defaultdict(float) self._log_env_metrics = log_env_metrics + log_dir = join(log_dir, "logs") + makedirs(log_dir, exist_ok=True) self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) def collect( diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 18a292f11..422e6db1d 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -3,7 +3,8 @@ import time from collections import defaultdict -from os import getcwd +from os import getcwd, makedirs +from os.path import join from typing import Union from maro.communication import Message, Proxy, SessionType @@ -40,6 +41,8 @@ def __init__( self.required_actor_finishes = required_actor_finishes self.discard_stale_experiences = discard_stale_experiences self._total_learning_time = 0 + log_dir = join(log_dir, "logs") + makedirs(log_dir, exist_ok=True) self._logger = Logger("LEARNER", dump_folder=log_dir) def run(self): From b7b3297f925d37bb1e9e8e8055b0406f59a38866 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 14 Apr 2021 17:09:39 +0800 Subject: [PATCH 179/482] add reward discount --- examples/supply_chain/env_wrapper.py | 9 +++------ maro/simulator/scenarios/supply_chain/actions.py | 2 +- .../scenarios/supply_chain/datamodels/consumer.py | 2 ++ maro/simulator/scenarios/supply_chain/units/consumer.py | 2 ++ 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 74b03d7ae..54a7115e2 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -275,7 +275,7 @@ def get_action(self, action_by_agent): product_id = self.consumer2product.get(agent_id, 0) agent_id = int(agent_id.split(".")[1]) env_action[agent_id] = ConsumerAction( - agent_id, product_id, source_id, action, 1) + agent_id, product_id, source_id, action, 1, 0) # manufacturer action elif agent_id.startswith("producer"): agent_id = int(agent_id.split(".")[1]) @@ -606,7 +606,7 @@ def _build_init_state(self): class BalanceSheetCalculator: consumer_features = ("id", "order_quantity", "price", - "order_cost", "order_product_cost") + "order_cost", "order_product_cost", "reward_discount") seller_features = ("id", "sold", "demand", "price", "backlog_ratio") manufacture_features = ("id", "manufacturing_number", "product_unit_cost") product_features = ( @@ -682,12 +682,9 @@ def calc(self): consumer_step_balance_sheet_loss = -1 * \ (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) - # not sure about this. - reward_discount = 0 - # consumer step reward: balance sheet los + profile * discount consumer_step_reward = consumer_step_balance_sheet_loss + \ - consumer_profit * reward_discount + consumer_profit * consumer_bs_states[:, 5] # seller seller_bs_states = self.seller_ss[tick::self.seller_features].flatten( diff --git a/maro/simulator/scenarios/supply_chain/actions.py b/maro/simulator/scenarios/supply_chain/actions.py index ea46a3f38..b6791a851 100644 --- a/maro/simulator/scenarios/supply_chain/actions.py +++ b/maro/simulator/scenarios/supply_chain/actions.py @@ -4,6 +4,6 @@ from collections import namedtuple -ConsumerAction = namedtuple("ConsumerAction", ("id", "product_id", "source_id", "quantity", "vlt")) +ConsumerAction = namedtuple("ConsumerAction", ("id", "product_id", "source_id", "quantity", "vlt", "reward_discount")) ManufactureAction = namedtuple("ManufactureAction", ("id", "production_rate")) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index 023a6eab5..b8e5f09b2 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -26,6 +26,8 @@ class ConsumerDataModel(SkuDataModel): price = NodeAttribute(AttributeType.Float) order_cost = NodeAttribute(AttributeType.Float) + reward_discount = NodeAttribute(AttributeType.Float) + def __init__(self): super(ConsumerDataModel, self).__init__() diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 1b9a48a3c..e82233d18 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -123,11 +123,13 @@ def flush_states(self): if self.action is not None and self.action.quantity > 0: self.data_model.order_quantity = self.action.quantity + self.data_model.reward_discount = self.action.reward_discount def post_step(self, tick: int): # Clear the action states per step. if self.action is not None: self.data_model.latest_consumptions = 0 + self.data_model.reward_discount = 0 if self.action.quantity > 0: self.data_model.order_quantity = 0 From edb2b847c58ae0eb608e8968c6bb106d7aceaefd Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 14 Apr 2021 17:51:47 +0800 Subject: [PATCH 180/482] align action to original one --- examples/supply_chain/env_wrapper.py | 25 ++++++++++++------- .../simulator/scenarios/supply_chain/world.py | 9 +++++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 54a7115e2..100eba989 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -267,19 +267,28 @@ def get_action(self, action_by_agent): env_action = {} for agent_id, action in action_by_agent.items(): + unit_id = int(agent_id.split(".")[1]) + + sku = self._units_mapping[unit_id][3] + # consumer action if agent_id.startswith("consumer"): sources = self.consumer2source.get(agent_id, []) if sources: source_id = sources[0] product_id = self.consumer2product.get(agent_id, 0) - agent_id = int(agent_id.split(".")[1]) - env_action[agent_id] = ConsumerAction( - agent_id, product_id, source_id, action, 1, 0) + + # if the agent in products, then it is a product unit, then apply the sale mean + if agent_id in self._summary["products"]: + action *= self._summary["products"][unit_id]["sale_mean"] + + reward_discount = 0 + env_action[agent_id] = ConsumerAction(unit_id, product_id, source_id, action, sku.vlt, reward_discount) # manufacturer action elif agent_id.startswith("producer"): - agent_id = int(agent_id.split(".")[1]) - env_action[agent_id] = ManufactureAction(agent_id, action) + action = sku.production_rate + + env_action[agent_id] = ManufactureAction(unit_id, action) return env_action @@ -483,10 +492,8 @@ def _build_init_state(self): # facility features state["facility"] = None - state["facility_type"] = [ - 1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] - state["is_accepted"] = [0] * \ - self._configs.settings["constraint_state_hist_len"] + state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] + state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] state['constraint_idx'] = [0] state['facility_id'] = [0] * self._sku_number state['sku_info'] = {} if agent_info.is_facility else agent_info.sku diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 2dd2c47ed..2dc93064e 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -391,10 +391,15 @@ def get_node_mapping(self): id2index_mapping = {} for unit_id, unit in self.units.items(): + sku = None + + if isinstance(unit, ProductUnit): + sku = unit.facility.skus[unit.product_id] + if unit.data_model is not None: - id2index_mapping[unit_id] = (unit.data_model_name, unit.data_model_index, unit.facility.id) + id2index_mapping[unit_id] = (unit.data_model_name, unit.data_model_index, unit.facility.id, sku) else: - id2index_mapping[unit_id] = (None, None, unit.facility.id) + id2index_mapping[unit_id] = (None, None, unit.facility.id, sku) return { "agent_types": [k for k in self.agent_type_dict.keys()], From 17adca2818888420b9dae3c63f9a16ed99d5e387 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 14 Apr 2021 18:38:35 +0800 Subject: [PATCH 181/482] add a simple manufacture unit to align with current logic --- .../supply_chain/topologies/core.yml | 3 +++ .../scenarios/supply_chain/units/__init__.py | 1 + .../supply_chain/units/simplemanufacture.py | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 maro/simulator/scenarios/supply_chain/units/simplemanufacture.py diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml index 712ede252..6752cca6c 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/core.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/core.yml @@ -50,6 +50,9 @@ units: ManufactureUnit: class: "ManufactureUnit" datamodel: "ManufactureDataModel" + SimpleManufactureUnit: + class: "SimpleManufactureUnit" + datamodel: "ManufactureDataModel" ProductUnit: class: "ProductUnit" datamodel: "ProductDataModel" diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index 86d9717fe..8917fcd64 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -5,6 +5,7 @@ from .consumer import ConsumerUnit from .distribution import DistributionUnit from .manufacture import ManufactureUnit +from .simplemanufacture import SimpleManufactureUnit from .product import ProductUnit from .seller import SellerUnit from .skuunit import SkuUnit diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py new file mode 100644 index 000000000..f9af16eef --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py @@ -0,0 +1,19 @@ + +from .manufacture import ManufactureUnit + + +class SimpleManufactureUnit(ManufactureUnit): + """This simple manufacture unit will ignore source sku, just generate specified number of product.""" + + def step(self, tick: int): + # Try to produce production if we have positive rate. + self.manufacture_number = 0 + + if self.action is not None and self.action.production_rate > 0: + production_rate = self.action.production_rate + + sku_num = len(self.facility.skus) + unit_num_upper_bound = self.facility.storage.capacity // sku_num + current_product_number = self.facility.storage.get_product_number( + self.product_id) + self.manufacture_number = max(0, min(unit_num_upper_bound-current_product_number, production_rate)) From bfe522fcb56dd54ba77debc986ff03db935b232d Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 14 Apr 2021 18:41:26 +0800 Subject: [PATCH 182/482] correct consumer and producer action to align current logic, fix consumer2source bug --- examples/supply_chain/env_wrapper.py | 40 +++++++++++++------ examples/supply_chain/envs/sample1/config.yml | 3 ++ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 100eba989..8008dd4da 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -253,42 +253,56 @@ def get_reward(self, tick=None, target_agents=None): def get_action(self, action_by_agent): # cache the sources for each consumer if not yet cached - if not hasattr(self, "consumer2source"): - self.consumer2source, self.consumer2product = {}, {} + if not hasattr(self, "product2source"): + self.product2source, self.consumer2product = {}, {} for facility in self.env.summary["node_mapping"]["facilities"].values(): products = facility["units"]["products"] for product_id, product in products.items(): consumer = product["consumer"] if consumer is not None: - consumer_id = ".".join( - ["consumer", str(consumer["id"])]) - self.consumer2source[consumer_id] = consumer["sources"] + consumer_id = consumer["id"] + product_unit_id = product["id"] + self.product2source[product_unit_id] = consumer["sources"] self.consumer2product[consumer_id] = product_id env_action = {} for agent_id, action in action_by_agent.items(): unit_id = int(agent_id.split(".")[1]) - sku = self._units_mapping[unit_id][3] + is_facility = unit_id not in self._units_mapping + + # ignore facility to reduce action number + if is_facility: + continue # consumer action if agent_id.startswith("consumer"): - sources = self.consumer2source.get(agent_id, []) + product_id = self.consumer2product.get(unit_id, 0) + sources = self.product2source.get(unit_id, []) + if sources: source_id = sources[0] - product_id = self.consumer2product.get(agent_id, 0) - # if the agent in products, then it is a product unit, then apply the sale mean - if agent_id in self._summary["products"]: - action *= self._summary["products"][unit_id]["sale_mean"] + action_number = int(int(action) * self._cur_metrics["products"][unit_id]["sale_mean"]) + + # ignore 0 quantity to reduce action number + if action_number == 0: + continue + sku = self._units_mapping[unit_id][3] reward_discount = 0 - env_action[agent_id] = ConsumerAction(unit_id, product_id, source_id, action, sku.vlt, reward_discount) + + env_action[unit_id] = ConsumerAction(unit_id, product_id, source_id, action_number, sku.vlt, reward_discount) # manufacturer action elif agent_id.startswith("producer"): + sku = self._units_mapping[unit_id][3] action = sku.production_rate - env_action[agent_id] = ManufactureAction(unit_id, action) + # ignore invalid actions + if action is None or action == 0: + continue + + env_action[unit_id] = ManufactureAction(unit_id, action) return env_action diff --git a/examples/supply_chain/envs/sample1/config.yml b/examples/supply_chain/envs/sample1/config.yml index 43667be78..f34f7ca3c 100644 --- a/examples/supply_chain/envs/sample1/config.yml +++ b/examples/supply_chain/envs/sample1/config.yml @@ -138,6 +138,7 @@ world: sku3: # sku name and attributes needed for this facility init_stock: 100 product_unit_cost: 1 + production_rate: 1 type: "production" # production means this is the output production of this facility cost: 10 price: 10 @@ -162,12 +163,14 @@ world: sku1: init_stock: 100 product_unit_cost: 1 + production_rate: 1 type: "production" cost: 10 price: 100 vlt: 1 sku3: init_stock: 100 + production_rate: 1 type: "material" cost: 10 price: 100 From 492fb0d90cc7acc7daffc3a630a4aa4330ae7498 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 14 Apr 2021 16:15:41 +0000 Subject: [PATCH 183/482] 1. modified dockerfile to make maro/rl mountable; 2. added logging dir; 3. renamed some agent configs for better clarity --- docker_files/dev.df | 5 +- examples/cim/ac/config.yml | 4 +- examples/cim/dqn/config.yml | 4 +- examples/supply_chain/{dqn => }/README.md | 4 +- examples/supply_chain/{dqn => }/__init__.py | 0 examples/supply_chain/{dqn => }/agent.py | 8 +- examples/supply_chain/config.yml | 104 ++++++++++++++++++ .../{dqn => }/distributed_launcher.py | 51 +++++---- examples/supply_chain/dqn/config.yml | 83 -------------- .../scripts/docker_compose_yml_generator.py | 8 +- .../{dqn => }/single_thread_launcher.py | 14 ++- maro/communication/proxy.py | 2 +- maro/rl/agent/abs_agent.py | 16 +-- maro/rl/agent/ac.py | 12 +- maro/rl/agent/ddpg.py | 12 +- maro/rl/agent/dqn.py | 12 +- maro/rl/agent/pg.py | 12 +- maro/rl/distributed/actor.py | 5 +- maro/rl/distributed/actor_manager.py | 5 +- maro/rl/distributed/learner.py | 5 +- 20 files changed, 197 insertions(+), 169 deletions(-) rename examples/supply_chain/{dqn => }/README.md (67%) rename examples/supply_chain/{dqn => }/__init__.py (100%) rename examples/supply_chain/{dqn => }/agent.py (65%) create mode 100644 examples/supply_chain/config.yml rename examples/supply_chain/{dqn => }/distributed_launcher.py (67%) delete mode 100644 examples/supply_chain/dqn/config.yml rename examples/supply_chain/{dqn => }/single_thread_launcher.py (70%) diff --git a/docker_files/dev.df b/docker_files/dev.df index 7e1676898..547473057 100644 --- a/docker_files/dev.df +++ b/docker_files/dev.df @@ -23,14 +23,13 @@ RUN pip install --no-cache-dir torch==1.6.0 RUN pip install --no-cache-dir scipy RUN pip install --no-cache-dir redis -COPY maro /maro/maro/ +COPY maro /maro/maro COPY scripts /maro/scripts/ COPY setup.py /maro/ RUN bash /maro/scripts/install_maro.sh RUN pip cache purge -RUN rm -r /maro/scripts -RUN rm /maro/setup.py +RUN rm -r /maro/maro/rl ENV PYTHONPATH=/maro diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index d48669ec5..45955023c 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -64,9 +64,9 @@ agent: experience_memory: experience_memory_size: -1 experience_memory_overwrite_type: "rolling" - flush_experience_memory_after_step: bool, + empty_experience_memory_after_step: bool, min_new_experiences_to_trigger_learning: 1 - min_experience_memory_size: 1 + min_experiences_to_trigger_learning: 1 training: env: scenario: cim diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index aef97a626..422e92638 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -53,9 +53,9 @@ agent: experience_memory: experience_memory_size: -1 experience_memory_overwrite_type: "rolling" - flush_experience_memory_after_step: false + empty_experience_memory_after_step: false min_new_experiences_to_trigger_learning: 16 - min_experience_memory_size: 1024 + min_experiences_to_trigger_learning: 1024 training: env: scenario: cim diff --git a/examples/supply_chain/dqn/README.md b/examples/supply_chain/README.md similarity index 67% rename from examples/supply_chain/dqn/README.md rename to examples/supply_chain/README.md index 6eb3a8aef..7c0fff4d1 100644 --- a/examples/supply_chain/dqn/README.md +++ b/examples/supply_chain/README.md @@ -1,5 +1,5 @@ Running the supply chain scenario consists of 3 simple steps: 1. To build the docker images required for running the supply chain scenario, go to examples/suuply_chain/scripts and run ```bash build.sh```. This step only needs to be done once, unless changes are made to the source code in maro/maro. -2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the ```config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). -3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file +2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). +3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file diff --git a/examples/supply_chain/dqn/__init__.py b/examples/supply_chain/__init__.py similarity index 100% rename from examples/supply_chain/dqn/__init__.py rename to examples/supply_chain/__init__.py diff --git a/examples/supply_chain/dqn/agent.py b/examples/supply_chain/agent.py similarity index 65% rename from examples/supply_chain/dqn/agent.py rename to examples/supply_chain/agent.py index 4ea09980a..643f085aa 100644 --- a/examples/supply_chain/dqn/agent.py +++ b/examples/supply_chain/agent.py @@ -8,4 +8,10 @@ def get_dqn_agent(config): q_model = SimpleMultiHeadModel( FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"]) ) - return DQN(q_model, DQNConfig(**config["algorithm"]), **config["experience_memory"]) + return DQN(q_model, DQNConfig(**config["algorithm_config"]), **config["experience_memory"]) + + +def get_ppo_agent(config): + pass + +get_agent_func_map = {"dqn": get_dqn_agent} diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml new file mode 100644 index 000000000..fd4ece78c --- /dev/null +++ b/examples/supply_chain/config.yml @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +agent: + consumer: + algorithm: dqn + model: + hidden_dims: + - 16 + - 8 + output_dim: 10 + activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. + optim_params: + lr: 0.001 + algorithm_config: + reward_discount: .9 + train_iters: 10 # number of gradient steps per call to agent.step() + batch_size: 128 # minibatch size for DQN training + loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. + target_update_freq: 5 # DQN target model update + soft_update_coefficient: 0.1 + double: false # whether to enable double DQN + experience_memory: + experience_memory_size: 50000 # experience memory size + # This determines how existing experiences are replaced when adding new experiences to a full experience + # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, + # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. + experience_memory_overwrite_type: random + # If true, the agent's experience memory will be emptied after calling agent.step(). + empty_experience_memory_after_step: false + # The minimum number of new experiences required to trigger learning (agent.step()). Defaults to 1. + min_new_experiences_to_trigger_learning: 10 + # The minimum number of experiences in the experience memory required for learning (agent.step()). Defaults to 1. + min_experiences_to_trigger_learning: 10 + producer: + algorithm: dqn + model: + hidden_dims: + - 16 + - 8 + output_dim: 10 + activation: leaky_relu + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop + optim_params: + lr: 0.001 + algorithm_config: + reward_discount: .9 + train_iters: 10 + batch_size: 128 + loss_cls: mse + target_update_freq: 5 + soft_update_coefficient: 0.1 + double: false # double DQN + experience_memory: + experience_memory_size: 50000 + experience_memory_overwrite_type: random + empty_experience_memory_after_step: false + min_new_experiences_to_trigger_learning: 1 + min_experiences_to_trigger_learning: 1 +training: + env: + scenario: supply_chain + # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder + # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ + topology: sample1 + durations: 200 # number of ticks per episode + max_episode: 3 # number of episodes + # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps + # before returning experiences to the learner. The learner uses these experiences to update the agents' policies + # and sync the updated policies to the actors for the next learning cycle. + agent_update_interval: 60 + exploration: + parameter_names: + - epsilon + start: 0.4 + end: 0.0 +distributed: + # this is used to group all actor / learner processes belonging to the same job for communication purposes. + # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. + group: sc-dqn + num_actors: 6 # number of parallel roll-out actors + # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" + # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make + # sure that a local redis server is running and listening on the port specified by "redis_port". + redis_host: redis-sc + redis_port: 6379 + # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent + # slow actors from dragging down the whole process. + required_actor_finishes: 4 + # If true, experiences from older segments (usually coming from slow actors) will not be used for learning. + discard_stale_experiences: True diff --git a/examples/supply_chain/dqn/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py similarity index 67% rename from examples/supply_chain/dqn/distributed_launcher.py rename to examples/supply_chain/distributed_launcher.py index 67b4264fd..a40e34998 100644 --- a/examples/supply_chain/dqn/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -6,45 +6,50 @@ import time import yaml from multiprocessing import Process -from os import getenv +from os import getenv, makedirs from os.path import dirname, join, realpath from maro.rl import ( - Actor, ActorManager, DQN, DQNConfig, DistLearner, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, + Actor, ActorManager, DistLearner, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, OptimOption, SimpleMultiHeadModel ) from maro.simulator import Env from maro.utils import set_seeds -sc_code_dir = dirname(dirname(realpath(__file__))) +sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) -sys.path.insert(0, join(sc_code_dir, "dqn")) from env_wrapper import SCEnvWrapper -from agent import get_dqn_agent +from agent import get_agent_func_map # Read the configuration -DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") +DEFAULT_CONFIG_PATH = join(sc_code_dir, "config.yml") with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: config = yaml.safe_load(config_file) +get_producer_agent = get_agent_func_map[config["agent"]["producer"]["algorithm"]] +get_consumer_agent = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] + # Get the env config path topology = config["training"]["env"]["topology"] -config["training"]["env"]["topology"] = join(dirname(dirname(realpath(__file__))), "envs", topology) +config["training"]["env"]["topology"] = join(sc_code_dir, "envs", topology) # for distributed / multi-process training GROUP = getenv("GROUP", default=config["distributed"]["group"]) REDIS_HOST = config["distributed"]["redis_host"] REDIS_PORT = config["distributed"]["redis_port"] -NUM_ACTORS = int(getenv("NUMACTORS", default=config["distributed"]["num_actors"])) +NUM_ACTORS = config["distributed"]["num_actors"] + +log_dir = join(sc_code_dir, "logs", GROUP) +makedirs(log_dir, exist_ok=True) -def sc_dqn_learner(): +def sc_learner(): # create agents that update themselves using experiences collected from the actors. env = SCEnvWrapper(Env(**config["training"]["env"])) config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in env.agent_idx_list} - consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} + producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in env.agent_idx_list} + consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} agent = MultiAgentWrapper({**producers, **consumers}) # exploration schedule @@ -52,7 +57,8 @@ def sc_dqn_learner(): # create an actor manager to collect simulation data from multiple actors actor_manager = ActorManager( - NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False} + NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False}, + log_dir=log_dir ) # create a learner to start the training process @@ -60,23 +66,24 @@ def sc_dqn_learner(): agent, scheduler, actor_manager, agent_update_interval=config["training"]["agent_update_interval"], required_actor_finishes=config["distributed"]["required_actor_finishes"], - discard_stale_experiences=config["distributed"]["discard_stale_experiences"] + discard_stale_experiences=config["distributed"]["discard_stale_experiences"], + log_dir=log_dir ) learner.run() -def sc_dqn_actor(): +def sc_actor(): # create an env wrapper for roll-out and obtain the input dimension for the agents env = SCEnvWrapper(Env(**config["training"]["env"])) config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim # create agents to interact with the env - producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in env.agent_idx_list} - consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} + producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in env.agent_idx_list} + consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} agent = MultiAgentWrapper({**producers, **consumers}) - + # create an actor that collects simulation data for the learner. - actor = Actor(env, agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}) + actor = Actor(env, agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, log_dir=log_dir) actor.run() @@ -89,8 +96,8 @@ def sc_dqn_actor(): args = parser.parse_args() if args.whoami == 0: - actor_processes = [Process(target=sc_dqn_actor) for i in range(NUM_ACTORS)] - learner_process = Process(target=sc_dqn_learner) + actor_processes = [Process(target=sc_actor) for i in range(NUM_ACTORS)] + learner_process = Process(target=sc_learner) for i, actor_process in enumerate(actor_processes): set_seeds(i) # this is to ensure that the actors explore differently. @@ -103,6 +110,6 @@ def sc_dqn_actor(): learner_process.join() elif args.whoami == 1: - sc_dqn_learner() + sc_learner() elif args.whoami == 2: - sc_dqn_actor() + sc_actor() diff --git a/examples/supply_chain/dqn/config.yml b/examples/supply_chain/dqn/config.yml deleted file mode 100644 index b846e5b23..000000000 --- a/examples/supply_chain/dqn/config.yml +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -agent: - consumer: - model: - hidden_dims: - - 16 - - 8 - output_dim: 100 - activation: leaky_relu - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop - optim_params: - lr: 0.001 - algorithm: - reward_discount: .9 - train_iters: 10 - batch_size: 128 - loss_cls: mse - target_update_freq: 5 - soft_update_coefficient: 0.1 - double: false # double DQN - experience_memory: - experience_memory_size: 50000 - experience_memory_overwrite_type: random - flush_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 10 - min_experience_memory_size: 10 - producer: - model: - hidden_dims: - - 16 - - 8 - output_dim: 10 - activation: leaky_relu - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop - optim_params: - lr: 0.001 - algorithm: - reward_discount: .9 - train_iters: 10 - batch_size: 128 - loss_cls: mse - target_update_freq: 5 - soft_update_coefficient: 0.1 - double: false - experience_memory: - experience_memory_size: 50000 - experience_memory_overwrite_type: random - flush_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 100000 - min_experience_memory_size: 10 -training: - env: - scenario: supply_chain - topology: sample1 - durations: 200 - max_episode: 3 - agent_update_interval: 35 - exploration: - parameter_names: - - epsilon - start: 0.4 - end: 0.0 -distributed: - group: sc-dqn - num_actors: 4 - redis_host: redis-sc - redis_port: 6379 - required_actor_finishes: 4 - discard_stale_experiences: True diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py index 53718ac1d..39612c0f9 100644 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -3,11 +3,13 @@ from os import makedirs from os.path import dirname, join, realpath + path = realpath(__file__) script_dir = dirname(path) sc_code_dir = dirname(script_dir) root_dir = dirname(dirname(sc_code_dir)) -config_path = join(sc_code_dir, "dqn", "config.yml") +maro_rl_dir = join(root_dir, "maro", "rl") +config_path = join(sc_code_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") with open(config_path, "r") as fp: @@ -23,8 +25,8 @@ "build": {"context": root_dir, "dockerfile": dockerfile_path}, "image": "maro-sc", "container_name": "learner", - "volumes": [f"{sc_code_dir}:/maro/supply_chain"], - "command": ["python3", "/maro/supply_chain/dqn/distributed_launcher.py", "-w", "1"] + "volumes": [f"{sc_code_dir}:/maro/supply_chain", f"{maro_rl_dir}:/maro/maro/rl"], + "command": ["python3", "/maro/supply_chain/distributed_launcher.py", "-w", "1"] } } } diff --git a/examples/supply_chain/dqn/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py similarity index 70% rename from examples/supply_chain/dqn/single_thread_launcher.py rename to examples/supply_chain/single_thread_launcher.py index 54d9aaff1..6f65f1254 100644 --- a/examples/supply_chain/dqn/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -12,11 +12,10 @@ from maro.simulator import Env from maro.utils import set_seeds -sc_code_dir = dirname(dirname(realpath(__file__))) +sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) -sys.path.insert(0, join(sc_code_dir, "dqn")) from env_wrapper import SCEnvWrapper -from agent import get_dqn_agent +from agent import get_agent_func_map # Single-threaded launcher @@ -25,9 +24,12 @@ with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: config = yaml.safe_load(config_file) + get_producer_agent = get_agent_func_map[config["agent"]["producer"]["algorithm"]] + get_consumer_agent = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] + # Get the env config path topology = config["training"]["env"]["topology"] - config["training"]["env"]["topology"] = join(dirname(dirname(realpath(__file__))), "envs", topology) + config["training"]["env"]["topology"] = join(dirname(realpath(__file__)), "envs", topology) # create an env wrapper and obtain the input dimension for the agents env = SCEnvWrapper(Env(**config["training"]["env"])) @@ -35,8 +37,8 @@ # create agents agent_info_list = env.agent_idx_list - producers = {f"producer.{info.id}": get_dqn_agent(config["agent"]["producer"]) for info in agent_info_list} - consumers = {f"consumer.{info.id}": get_dqn_agent(config["agent"]["consumer"]) for info in agent_info_list} + producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in agent_info_list} + consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in agent_info_list} agent = MultiAgentWrapper({**producers, **consumers}) # exploration schedule diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 524577ec1..8f8d59f90 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -120,7 +120,7 @@ def __init__( self._redis_connection.ping() break except Exception as e: - retry_time = self._retry_interval_base_value * (2 ** retry_number) + retry_time = self._retry_interval_base_value * (2 ** num_tries) self._logger.error( f"{self._name} failed to connect to the redis server due to {e}. Retrying in {retry_time} seconds." ) diff --git a/maro/rl/agent/abs_agent.py b/maro/rl/agent/abs_agent.py index d06762886..192ec65b5 100644 --- a/maro/rl/agent/abs_agent.py +++ b/maro/rl/agent/abs_agent.py @@ -25,11 +25,11 @@ class AbsAgent(ABC): unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". - flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call to ``step``. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. Defaults to 1. """ def __init__( @@ -38,18 +38,18 @@ def __init__( config, experience_memory_size: int, experience_memory_overwrite_type: str, - flush_experience_memory_after_step: bool, + empty_experience_memory_after_step: bool, min_new_experiences_to_trigger_learning: int = 1, - min_experience_memory_size: int = 1 + min_experiences_to_trigger_learning: int = 1 ): self.model = model self.config = config self.experience_memory = SimpleStore( ["S", "A", "R", "S_"], capacity=experience_memory_size, overwrite_type=experience_memory_overwrite_type ) - self.flush_experience_memory_after_step = flush_experience_memory_after_step + self.empty_experience_memory_after_step = empty_experience_memory_after_step self.min_new_experiences_to_trigger_learning = min_new_experiences_to_trigger_learning - self.min_experience_memory_size = min_experience_memory_size + self.min_experiences_to_trigger_learning = min_experiences_to_trigger_learning self.device = torch.device('cpu') self._version_index = 0 @@ -88,11 +88,11 @@ def learn(self, experiences: dict) -> bool: self.experience_memory.put(experiences) if ( len(experiences["S"]) >= self.min_new_experiences_to_trigger_learning and - len(self.experience_memory) >= self.min_experience_memory_size + len(self.experience_memory) >= self.min_experiences_to_trigger_learning ): self.step() self._version_index += 1 - if self.flush_experience_memory_after_step: + if self.empty_experience_memory_after_step: self.experience_memory.clear() return True return False diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py index 21ec1296f..5948389fa 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/agent/ac.py @@ -60,11 +60,11 @@ class ActorCritic(AbsAgent): unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". - flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call to ``step``. Defaults to True. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. Defaults to 1. """ def __init__( @@ -73,17 +73,17 @@ def __init__( config: ActorCriticConfig, experience_memory_size: int, experience_memory_overwrite_type: str, - flush_experience_memory_after_step: bool = True, + empty_experience_memory_after_step: bool = True, min_new_experiences_to_trigger_learning: int = 1, - min_experience_memory_size: int = 1 + min_experiences_to_trigger_learning: int = 1 ): if model.task_names is None or set(model.task_names) != {"actor", "critic"}: raise UnrecognizedTask(f"Expected model task names 'actor' and 'critic', but got {model.task_names}") super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, - flush_experience_memory_after_step, + empty_experience_memory_after_step, min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experience_memory_size=min_experience_memory_size + min_experiences_to_trigger_learning=min_experiences_to_trigger_learning ) def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: diff --git a/maro/rl/agent/ddpg.py b/maro/rl/agent/ddpg.py index 48ad14dae..d5677a982 100644 --- a/maro/rl/agent/ddpg.py +++ b/maro/rl/agent/ddpg.py @@ -62,11 +62,11 @@ class DDPG(AbsAgent): unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". - flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call to ``step``. Defaults to False. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. Defaults to 1. explorer (NoiseExplorer): An NoiseExplorer instance for generating exploratory actions. Defaults to None. """ @@ -76,18 +76,18 @@ def __init__( config: DDPGConfig, experience_memory_size: int, experience_memory_overwrite_type: str, - flush_experience_memory_after_step: bool = False, + empty_experience_memory_after_step: bool = False, min_new_experiences_to_trigger_learning: int = 1, - min_experience_memory_size: int = 1, + min_experiences_to_trigger_learning: int = 1, explorer: NoiseExplorer = None ): if model.task_names is None or set(model.task_names) != {"policy", "q_value"}: raise UnrecognizedTask(f"Expected model task names 'policy' and 'q_value', but got {model.task_names}") super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, - flush_experience_memory_after_step, + empty_experience_memory_after_step, min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experience_memory_size=min_experience_memory_size + min_experiences_to_trigger_learning=min_experiences_to_trigger_learning ) self._explorer = explorer self._target_model = model.copy() if model.trainable else None diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index 32a50dc4c..0c145c310 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -81,11 +81,11 @@ class DQN(AbsAgent): unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". - flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call to ``step``. Defaults to False. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. Defaults to 1. """ def __init__( @@ -94,9 +94,9 @@ def __init__( config: DQNConfig, experience_memory_size: int, experience_memory_overwrite_type: str, - flush_experience_memory_after_step: bool = False, + empty_experience_memory_after_step: bool = False, min_new_experiences_to_trigger_learning: int = 1, - min_experience_memory_size: int = 1 + min_experiences_to_trigger_learning: int = 1 ): if (config.advantage_type is not None and (model.task_names is None or set(model.task_names) != {"state_value", "advantage"})): @@ -106,9 +106,9 @@ def __init__( ) super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, - flush_experience_memory_after_step, + empty_experience_memory_after_step, min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experience_memory_size=min_experience_memory_size + min_experiences_to_trigger_learning=min_experiences_to_trigger_learning ) self._sampler = self.config.sampler_cls(self.experience_memory, **self.config.sampler_params) self._training_counter = 0 diff --git a/maro/rl/agent/pg.py b/maro/rl/agent/pg.py index 05a68864f..16ff16080 100644 --- a/maro/rl/agent/pg.py +++ b/maro/rl/agent/pg.py @@ -38,11 +38,11 @@ class PolicyGradient(AbsAgent): unlimited size. experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are to be overwritten after its capacity has been reached. Must be "rolling" or "random". - flush_experience_memory_after_step (bool): If True, the experience memory will be flushed after each call + empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call to ``step``. Defaults to True. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experience_memory_size (int): Minimum number of experiences in the experience memory required for training. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. Defaults to 1. """ def __init__( @@ -51,15 +51,15 @@ def __init__( config: PolicyGradientConfig, experience_memory_size: int, experience_memory_overwrite_type: str, - flush_experience_memory_after_step: bool = True, + empty_experience_memory_after_step: bool = True, min_new_experiences_to_trigger_learning: int = 1, - min_experience_memory_size: int = 1 + min_experiences_to_trigger_learning: int = 1 ): super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, - flush_experience_memory_after_step, + empty_experience_memory_after_step, min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experience_memory_size=min_experience_memory_size + min_experiences_to_trigger_learning=min_experiences_to_trigger_learning ) def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 0204402f9..5aa6f8d4b 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -1,8 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from os import getcwd, makedirs -from os.path import join +from os import getcwd from typing import Union from maro.communication import Message, Proxy @@ -40,8 +39,6 @@ def __init__( if proxy_options is None: proxy_options = {} self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) - log_dir = join(log_dir, "logs") - makedirs(log_dir, exist_ok=True) self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py index a4b534c6a..7616d2b49 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/distributed/actor_manager.py @@ -2,8 +2,7 @@ # Licensed under the MIT license. from collections import defaultdict -from os import getcwd, makedirs -from os.path import join +from os import getcwd from typing import Union from maro.communication import Message, Proxy, SessionType @@ -44,8 +43,6 @@ def __init__( self.total_env_steps = 0 self.total_reward = defaultdict(float) self._log_env_metrics = log_env_metrics - log_dir = join(log_dir, "logs") - makedirs(log_dir, exist_ok=True) self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) def collect( diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 422e6db1d..18a292f11 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -3,8 +3,7 @@ import time from collections import defaultdict -from os import getcwd, makedirs -from os.path import join +from os import getcwd from typing import Union from maro.communication import Message, Proxy, SessionType @@ -41,8 +40,6 @@ def __init__( self.required_actor_finishes = required_actor_finishes self.discard_stale_experiences = discard_stale_experiences self._total_learning_time = 0 - log_dir = join(log_dir, "logs") - makedirs(log_dir, exist_ok=True) self._logger = Logger("LEARNER", dump_folder=log_dir) def run(self): From 42c24ee43ed93b49bff0be16e40db83df6a74261 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 15 Apr 2021 03:02:50 +0000 Subject: [PATCH 184/482] updated README for supply chain --- examples/supply_chain/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/supply_chain/README.md b/examples/supply_chain/README.md index 7c0fff4d1..52d79ea27 100644 --- a/examples/supply_chain/README.md +++ b/examples/supply_chain/README.md @@ -1,5 +1,8 @@ -Running the supply chain scenario consists of 3 simple steps: +# Supply Chain Scenario -1. To build the docker images required for running the supply chain scenario, go to examples/suuply_chain/scripts and run ```bash build.sh```. This step only needs to be done once, unless changes are made to the source code in maro/maro. +This README contains instructions for running the supply chain scenario using the scripts provided under ```examples/supply_chain/scripts```. For details on state, action and reward shaping based on MARO's supply chain business engine, please refer to ```examples/supply_chain/sc_state_in_maro.md```. + +To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: +1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This step only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. 2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). 3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file From 66fb27ea48a34bbc732fa0c63357907c092bb1a7 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 15 Apr 2021 03:08:06 +0000 Subject: [PATCH 185/482] updated README for supply chain --- examples/supply_chain/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/supply_chain/README.md b/examples/supply_chain/README.md index 52d79ea27..11cf21cb9 100644 --- a/examples/supply_chain/README.md +++ b/examples/supply_chain/README.md @@ -4,5 +4,5 @@ This README contains instructions for running the supply chain scenario using th To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: 1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This step only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. -2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). +2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the "distributed" section of ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). 3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file From 0de83ac4b130eae000c7d7b8641abdcc83f417c8 Mon Sep 17 00:00:00 2001 From: Jinyu Wang Date: Thu, 15 Apr 2021 11:51:01 +0800 Subject: [PATCH 186/482] update supply chain example config comments --- examples/supply_chain/config.yml | 12 +- examples/supply_chain/distributed_launcher.py | 4 +- .../supply_chain/single_thread_launcher.py | 4 +- .../{envs => topologies}/random/config.yml | 58674 ++++++++-------- .../{envs => topologies}/sample1/config.yml | 0 5 files changed, 29347 insertions(+), 29347 deletions(-) rename examples/supply_chain/{envs => topologies}/random/config.yml (95%) rename examples/supply_chain/{envs => topologies}/sample1/config.yml (100%) diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index fd4ece78c..525a71e98 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -4,7 +4,7 @@ agent: consumer: algorithm: dqn - model: + model: # If you want to design your model used, edit the get_dqn_agent() codes in examples\supply_chain\agent.py hidden_dims: - 16 - 8 @@ -22,9 +22,9 @@ agent: algorithm_config: reward_discount: .9 train_iters: 10 # number of gradient steps per call to agent.step() - batch_size: 128 # minibatch size for DQN training + batch_size: 128 # mini-batch size for DQN training loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. - target_update_freq: 5 # DQN target model update + target_update_freq: 5 # How many training iteration, to update DQN target model soft_update_coefficient: 0.1 double: false # whether to enable double DQN experience_memory: @@ -37,7 +37,7 @@ agent: empty_experience_memory_after_step: false # The minimum number of new experiences required to trigger learning (agent.step()). Defaults to 1. min_new_experiences_to_trigger_learning: 10 - # The minimum number of experiences in the experience memory required for learning (agent.step()). Defaults to 1. + # The minimum number of experiences in the experience memory required to trigger learning (agent.step()). Defaults to 1. min_experiences_to_trigger_learning: 10 producer: algorithm: dqn @@ -77,7 +77,7 @@ training: # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ topology: sample1 durations: 200 # number of ticks per episode - max_episode: 3 # number of episodes + num_episodes: 3 # number of episodes to simulate # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps # before returning experiences to the learner. The learner uses these experiences to update the agents' policies # and sync the updated policies to the actors for the next learning cycle. @@ -85,7 +85,7 @@ training: exploration: parameter_names: - epsilon - start: 0.4 + start: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. end: 0.0 distributed: # this is used to group all actor / learner processes belonging to the same job for communication purposes. diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index a40e34998..f752f39b9 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -32,7 +32,7 @@ # Get the env config path topology = config["training"]["env"]["topology"] -config["training"]["env"]["topology"] = join(sc_code_dir, "envs", topology) +config["training"]["env"]["topology"] = join(sc_code_dir, "topologies", topology) # for distributed / multi-process training GROUP = getenv("GROUP", default=config["distributed"]["group"]) @@ -53,7 +53,7 @@ def sc_learner(): agent = MultiAgentWrapper({**producers, **consumers}) # exploration schedule - scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) + scheduler = LinearParameterScheduler(config["training"]["num_episodes"], **config["training"]["exploration"]) # create an actor manager to collect simulation data from multiple actors actor_manager = ActorManager( diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index 6f65f1254..1bc18ba13 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -29,7 +29,7 @@ # Get the env config path topology = config["training"]["env"]["topology"] - config["training"]["env"]["topology"] = join(dirname(realpath(__file__)), "envs", topology) + config["training"]["env"]["topology"] = join(dirname(realpath(__file__)), "topologies", topology) # create an env wrapper and obtain the input dimension for the agents env = SCEnvWrapper(Env(**config["training"]["env"])) @@ -42,7 +42,7 @@ agent = MultiAgentWrapper({**producers, **consumers}) # exploration schedule - scheduler = LinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) + scheduler = LinearParameterScheduler(config["training"]["num_episodes"], **config["training"]["exploration"]) # create a learner to start training learner = Learner(env, agent, scheduler, agent_update_interval=config["training"]["agent_update_interval"]) diff --git a/examples/supply_chain/envs/random/config.yml b/examples/supply_chain/topologies/random/config.yml similarity index 95% rename from examples/supply_chain/envs/random/config.yml rename to examples/supply_chain/topologies/random/config.yml index bd875d1ad..9c3e72409 100644 --- a/examples/supply_chain/envs/random/config.yml +++ b/examples/supply_chain/topologies/random/config.yml @@ -1,29337 +1,29337 @@ -facility_definitions: - RetailerFacility: - children: - products: - class: StoreProductUnit - config: - agent_type: 5 - consumer: - class: ConsumerUnit - seller: - class: SellerUnit - config: - sale_hist_len: 4 - is_template: true - storage: - class: StorageUnit - class: RetailerFacility - config: - agent_type: 2 - SupplierFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: 3 - consumer: - class: ConsumerUnit - manufacture: - class: ManufactureUnit - is_template: true - storage: - class: StorageUnit - class: SupplierFacility - config: - agent_type: 0 - WarehouseFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: 4 - consumer: - class: ConsumerUnit - is_template: true - storage: - class: StorageUnit - class: WarehouseFacility - config: - agent_type: 1 -normal_vehicle: &id001 - class: VehicleUnit - config: - patient: 100 - unit_transport_cost: 1 -settings: - constraint_state_hist_len: 4 - constraint_violate_reward: -1000000.0 - consumption_hist_len: 4 - downsampling_rate: 1 - episod_duration: 21 - gamma: 0.99 - global_reward_weight_consumer: 0.5 - global_reward_weight_producer: 0.5 - heading_timesteps: 7 - initial_balance: 100000 - pending_order_len: 4 - replenishment_discount: 0.9 - reward_normalization: 10000000.0 - sale_hist_len: 4 - tail_timesteps: 7 - total_echelons: 3 -world: - facilities: - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 200 - definition_ref: SupplierFacility - name: SUPPLIER0 - skus: - SKU0: - cost: 289 - init_stock: 3150 - price: 322 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU1: - cost: 255 - init_stock: 1100 - price: 284 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU10: - cost: 61 - init_stock: 1250 - price: 68 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU100: - cost: 14 - init_stock: 3250 - price: 16 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU101: - cost: 75 - init_stock: 3550 - price: 84 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU102: - cost: 295 - init_stock: 4650 - price: 328 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU103: - cost: 300 - init_stock: 4750 - price: 334 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU104: - cost: 44 - init_stock: 3250 - price: 49 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU105: - cost: 99 - init_stock: 2850 - price: 110 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU106: - cost: 225 - init_stock: 3650 - price: 251 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU107: - cost: 380 - init_stock: 4350 - price: 423 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU108: - cost: 412 - init_stock: 4600 - price: 458 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU109: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU11: - cost: 360 - init_stock: 2550 - price: 400 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU110: - cost: 59 - init_stock: 700 - price: 66 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU111: - cost: 234 - init_stock: 3050 - price: 260 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU112: - cost: 54 - init_stock: 4750 - price: 61 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU113: - cost: 313 - init_stock: 1550 - price: 348 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU114: - cost: 350 - init_stock: 1350 - price: 389 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU115: - cost: 257 - init_stock: 4300 - price: 286 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU116: - cost: 446 - init_stock: 3600 - price: 496 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU117: - cost: 288 - init_stock: 4600 - price: 320 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU118: - cost: 164 - init_stock: 1650 - price: 183 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU119: - cost: 188 - init_stock: 1600 - price: 209 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU12: - cost: 100 - init_stock: 4200 - price: 112 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU120: - cost: 108 - init_stock: 4900 - price: 121 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU121: - cost: 36 - init_stock: 4250 - price: 40 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU122: - cost: 393 - init_stock: 350 - price: 437 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU123: - cost: 209 - init_stock: 950 - price: 233 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU124: - cost: 163 - init_stock: 1800 - price: 182 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU125: - cost: 14 - init_stock: 4600 - price: 16 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU126: - cost: 32 - init_stock: 1950 - price: 36 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU127: - cost: 195 - init_stock: 1550 - price: 217 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU128: - cost: 148 - init_stock: 950 - price: 165 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU129: - cost: 128 - init_stock: 2500 - price: 143 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU13: - cost: 285 - init_stock: 2850 - price: 317 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU130: - cost: 313 - init_stock: 4900 - price: 348 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU131: - cost: 57 - init_stock: 2250 - price: 64 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU132: - cost: 384 - init_stock: 1050 - price: 427 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU133: - cost: 201 - init_stock: 1450 - price: 224 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU134: - cost: 302 - init_stock: 3850 - price: 336 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU135: - cost: 137 - init_stock: 5000 - price: 153 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU136: - cost: 179 - init_stock: 3550 - price: 199 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU137: - cost: 83 - init_stock: 3700 - price: 93 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU138: - cost: 205 - init_stock: 1800 - price: 228 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU139: - cost: 186 - init_stock: 1200 - price: 207 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU14: - cost: 241 - init_stock: 3100 - price: 268 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU140: - cost: 234 - init_stock: 1700 - price: 261 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU141: - cost: 171 - init_stock: 2050 - price: 190 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU142: - cost: 288 - init_stock: 1900 - price: 320 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU143: - cost: 286 - init_stock: 1300 - price: 318 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU144: - cost: 360 - init_stock: 600 - price: 400 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU145: - cost: 359 - init_stock: 4100 - price: 399 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU146: - cost: 159 - init_stock: 2400 - price: 177 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU147: - cost: 424 - init_stock: 2800 - price: 472 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU148: - cost: 281 - init_stock: 3850 - price: 313 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU149: - cost: 321 - init_stock: 3850 - price: 357 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU15: - cost: 46 - init_stock: 250 - price: 52 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU150: - cost: 95 - init_stock: 3500 - price: 106 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU151: - cost: 200 - init_stock: 3650 - price: 223 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU152: - cost: 9 - init_stock: 2550 - price: 10 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU153: - cost: 396 - init_stock: 3100 - price: 441 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU154: - cost: 69 - init_stock: 4250 - price: 77 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU155: - cost: 379 - init_stock: 2650 - price: 422 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU156: - cost: 9 - init_stock: 600 - price: 10 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU157: - cost: 369 - init_stock: 3750 - price: 410 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU158: - cost: 130 - init_stock: 4050 - price: 145 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU159: - cost: 173 - init_stock: 1250 - price: 193 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU16: - cost: 157 - init_stock: 2900 - price: 175 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU160: - cost: 413 - init_stock: 4250 - price: 459 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU161: - cost: 215 - init_stock: 2300 - price: 239 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU162: - cost: 142 - init_stock: 250 - price: 158 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU163: - cost: 437 - init_stock: 1950 - price: 486 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU164: - cost: 446 - init_stock: 5000 - price: 496 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU165: - cost: 246 - init_stock: 1650 - price: 274 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU166: - cost: 71 - init_stock: 4450 - price: 79 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU167: - cost: 398 - init_stock: 650 - price: 443 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU168: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU169: - cost: 332 - init_stock: 4900 - price: 369 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU17: - cost: 311 - init_stock: 450 - price: 346 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU170: - cost: 61 - init_stock: 2750 - price: 68 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU171: - cost: 358 - init_stock: 3800 - price: 398 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU172: - cost: 180 - init_stock: 3550 - price: 200 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU173: - cost: 157 - init_stock: 4800 - price: 175 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU174: - cost: 261 - init_stock: 3800 - price: 291 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU175: - cost: 75 - init_stock: 3750 - price: 84 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU176: - cost: 366 - init_stock: 3300 - price: 407 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU177: - cost: 231 - init_stock: 1550 - price: 257 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU178: - cost: 380 - init_stock: 250 - price: 423 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU179: - cost: 447 - init_stock: 4150 - price: 497 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU18: - cost: 232 - init_stock: 4050 - price: 258 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU180: - cost: 195 - init_stock: 2750 - price: 217 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU181: - cost: 128 - init_stock: 3000 - price: 143 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU182: - cost: 393 - init_stock: 4950 - price: 437 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU183: - cost: 130 - init_stock: 400 - price: 145 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU184: - cost: 65 - init_stock: 1200 - price: 73 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU185: - cost: 9 - init_stock: 4500 - price: 10 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU186: - cost: 323 - init_stock: 1100 - price: 359 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU187: - cost: 159 - init_stock: 1500 - price: 177 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU188: - cost: 351 - init_stock: 4350 - price: 391 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU189: - cost: 322 - init_stock: 1750 - price: 358 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU19: - cost: 429 - init_stock: 4550 - price: 477 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU190: - cost: 101 - init_stock: 850 - price: 113 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU191: - cost: 425 - init_stock: 2700 - price: 473 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU192: - cost: 373 - init_stock: 3050 - price: 415 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU193: - cost: 186 - init_stock: 1500 - price: 207 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU194: - cost: 388 - init_stock: 250 - price: 432 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU195: - cost: 196 - init_stock: 1550 - price: 218 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU196: - cost: 44 - init_stock: 3400 - price: 49 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU197: - cost: 272 - init_stock: 2850 - price: 303 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU198: - cost: 152 - init_stock: 2700 - price: 169 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU199: - cost: 404 - init_stock: 1150 - price: 449 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU2: - cost: 297 - init_stock: 3500 - price: 331 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU20: - cost: 301 - init_stock: 1250 - price: 335 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU200: - cost: 58 - init_stock: 1250 - price: 65 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU201: - cost: 93 - init_stock: 2950 - price: 104 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU202: - cost: 127 - init_stock: 3650 - price: 142 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU203: - cost: 396 - init_stock: 4100 - price: 440 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU204: - cost: 440 - init_stock: 2350 - price: 489 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU205: - cost: 117 - init_stock: 5000 - price: 130 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU206: - cost: 301 - init_stock: 550 - price: 335 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU207: - cost: 126 - init_stock: 4000 - price: 140 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU208: - cost: 441 - init_stock: 3850 - price: 491 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU209: - cost: 161 - init_stock: 1000 - price: 179 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU21: - cost: 110 - init_stock: 5000 - price: 123 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU210: - cost: 363 - init_stock: 3450 - price: 404 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU211: - cost: 156 - init_stock: 4550 - price: 174 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU212: - cost: 364 - init_stock: 3950 - price: 405 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU213: - cost: 108 - init_stock: 3200 - price: 121 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU214: - cost: 90 - init_stock: 500 - price: 101 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU215: - cost: 377 - init_stock: 2350 - price: 419 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU216: - cost: 297 - init_stock: 1150 - price: 330 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU217: - cost: 255 - init_stock: 3250 - price: 284 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU218: - cost: 184 - init_stock: 2950 - price: 205 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU219: - cost: 82 - init_stock: 2300 - price: 92 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU22: - cost: 443 - init_stock: 3300 - price: 493 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU220: - cost: 348 - init_stock: 4350 - price: 387 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU221: - cost: 35 - init_stock: 3900 - price: 39 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU222: - cost: 103 - init_stock: 1800 - price: 115 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU223: - cost: 176 - init_stock: 600 - price: 196 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU224: - cost: 348 - init_stock: 250 - price: 387 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU225: - cost: 147 - init_stock: 1450 - price: 164 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU226: - cost: 185 - init_stock: 650 - price: 206 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU227: - cost: 431 - init_stock: 1200 - price: 479 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU228: - cost: 12 - init_stock: 4500 - price: 14 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU229: - cost: 424 - init_stock: 2200 - price: 472 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU23: - cost: 348 - init_stock: 2100 - price: 387 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU230: - cost: 216 - init_stock: 1150 - price: 241 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU231: - cost: 43 - init_stock: 4050 - price: 48 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU232: - cost: 201 - init_stock: 4800 - price: 224 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU233: - cost: 324 - init_stock: 3750 - price: 360 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU234: - cost: 258 - init_stock: 250 - price: 287 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU235: - cost: 21 - init_stock: 2850 - price: 24 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU236: - cost: 139 - init_stock: 2750 - price: 155 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU237: - cost: 389 - init_stock: 2250 - price: 433 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU238: - cost: 57 - init_stock: 3300 - price: 64 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU239: - cost: 92 - init_stock: 4900 - price: 103 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU24: - cost: 87 - init_stock: 2350 - price: 97 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU240: - cost: 335 - init_stock: 2350 - price: 373 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU241: - cost: 395 - init_stock: 3550 - price: 439 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU242: - cost: 15 - init_stock: 2200 - price: 17 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU243: - cost: 316 - init_stock: 700 - price: 352 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU244: - cost: 156 - init_stock: 4100 - price: 174 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU245: - cost: 363 - init_stock: 3300 - price: 404 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU246: - cost: 270 - init_stock: 1500 - price: 300 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU247: - cost: 347 - init_stock: 1750 - price: 386 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU248: - cost: 319 - init_stock: 1450 - price: 355 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU249: - cost: 320 - init_stock: 1250 - price: 356 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU25: - cost: 144 - init_stock: 250 - price: 161 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU250: - cost: 114 - init_stock: 2700 - price: 127 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU251: - cost: 309 - init_stock: 3050 - price: 344 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU252: - cost: 162 - init_stock: 4150 - price: 181 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU253: - cost: 403 - init_stock: 800 - price: 448 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU254: - cost: 435 - init_stock: 2300 - price: 484 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU255: - cost: 261 - init_stock: 3350 - price: 290 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU256: - cost: 81 - init_stock: 3600 - price: 91 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU257: - cost: 313 - init_stock: 2850 - price: 348 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU258: - cost: 440 - init_stock: 2150 - price: 489 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU259: - cost: 299 - init_stock: 3450 - price: 333 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU26: - cost: 206 - init_stock: 3150 - price: 229 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU260: - cost: 438 - init_stock: 2600 - price: 487 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU261: - cost: 331 - init_stock: 1100 - price: 368 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU262: - cost: 298 - init_stock: 3900 - price: 332 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU263: - cost: 170 - init_stock: 3700 - price: 189 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU264: - cost: 324 - init_stock: 3950 - price: 361 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU265: - cost: 257 - init_stock: 2950 - price: 286 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU266: - cost: 115 - init_stock: 2350 - price: 128 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU267: - cost: 69 - init_stock: 4000 - price: 77 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU268: - cost: 198 - init_stock: 4450 - price: 221 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU269: - cost: 113 - init_stock: 2200 - price: 126 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU27: - cost: 333 - init_stock: 2800 - price: 370 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU270: - cost: 163 - init_stock: 3700 - price: 182 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU271: - cost: 207 - init_stock: 900 - price: 230 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU272: - cost: 329 - init_stock: 850 - price: 366 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU273: - cost: 378 - init_stock: 900 - price: 421 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU274: - cost: 26 - init_stock: 3850 - price: 29 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU275: - cost: 45 - init_stock: 2400 - price: 50 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU276: - cost: 146 - init_stock: 2700 - price: 163 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU277: - cost: 404 - init_stock: 2050 - price: 449 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU278: - cost: 94 - init_stock: 600 - price: 105 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU279: - cost: 45 - init_stock: 1950 - price: 51 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU28: - cost: 187 - init_stock: 2350 - price: 208 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU280: - cost: 265 - init_stock: 1650 - price: 295 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU281: - cost: 355 - init_stock: 3750 - price: 395 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU282: - cost: 56 - init_stock: 2300 - price: 63 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU283: - cost: 352 - init_stock: 4600 - price: 392 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU284: - cost: 309 - init_stock: 3350 - price: 344 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU285: - cost: 119 - init_stock: 4550 - price: 133 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU286: - cost: 303 - init_stock: 1950 - price: 337 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU287: - cost: 337 - init_stock: 2800 - price: 375 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU288: - cost: 162 - init_stock: 1900 - price: 181 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU289: - cost: 60 - init_stock: 1550 - price: 67 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU29: - cost: 220 - init_stock: 2900 - price: 245 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU290: - cost: 394 - init_stock: 3350 - price: 438 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU291: - cost: 84 - init_stock: 3050 - price: 94 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU292: - cost: 167 - init_stock: 250 - price: 186 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU293: - cost: 145 - init_stock: 2750 - price: 162 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU294: - cost: 368 - init_stock: 450 - price: 409 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU295: - cost: 391 - init_stock: 4650 - price: 435 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU296: - cost: 333 - init_stock: 4600 - price: 370 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU297: - cost: 268 - init_stock: 1900 - price: 298 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU298: - cost: 257 - init_stock: 1750 - price: 286 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU299: - cost: 330 - init_stock: 2550 - price: 367 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU3: - cost: 364 - init_stock: 600 - price: 405 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU30: - cost: 323 - init_stock: 3450 - price: 359 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU300: - cost: 338 - init_stock: 2900 - price: 376 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU301: - cost: 389 - init_stock: 4150 - price: 433 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU302: - cost: 165 - init_stock: 550 - price: 184 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU303: - cost: 221 - init_stock: 4700 - price: 246 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU304: - cost: 377 - init_stock: 1150 - price: 419 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU305: - cost: 445 - init_stock: 5000 - price: 495 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU306: - cost: 431 - init_stock: 2100 - price: 479 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU307: - cost: 189 - init_stock: 3900 - price: 210 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU308: - cost: 187 - init_stock: 250 - price: 208 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU309: - cost: 90 - init_stock: 4600 - price: 101 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU31: - cost: 62 - init_stock: 2800 - price: 69 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU310: - cost: 210 - init_stock: 2200 - price: 234 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU311: - cost: 275 - init_stock: 4000 - price: 306 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU312: - cost: 261 - init_stock: 1250 - price: 291 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU313: - cost: 291 - init_stock: 1900 - price: 324 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU314: - cost: 363 - init_stock: 1450 - price: 404 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU315: - cost: 423 - init_stock: 4200 - price: 471 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU316: - cost: 181 - init_stock: 900 - price: 202 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU317: - cost: 194 - init_stock: 1200 - price: 216 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU318: - cost: 250 - init_stock: 4250 - price: 278 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU319: - cost: 231 - init_stock: 2650 - price: 257 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU32: - cost: 302 - init_stock: 1100 - price: 336 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU320: - cost: 176 - init_stock: 1950 - price: 196 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU321: - cost: 60 - init_stock: 800 - price: 67 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU322: - cost: 216 - init_stock: 5000 - price: 240 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU323: - cost: 59 - init_stock: 1950 - price: 66 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU324: - cost: 397 - init_stock: 4650 - price: 442 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU325: - cost: 407 - init_stock: 3450 - price: 453 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU326: - cost: 310 - init_stock: 1200 - price: 345 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU327: - cost: 411 - init_stock: 700 - price: 457 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU328: - cost: 351 - init_stock: 2250 - price: 390 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU329: - cost: 60 - init_stock: 2100 - price: 67 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU33: - cost: 374 - init_stock: 4600 - price: 416 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU330: - cost: 275 - init_stock: 1950 - price: 306 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU331: - cost: 124 - init_stock: 2050 - price: 138 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU332: - cost: 351 - init_stock: 4800 - price: 390 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU333: - cost: 436 - init_stock: 2650 - price: 485 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU334: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU335: - cost: 72 - init_stock: 4050 - price: 80 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU336: - cost: 266 - init_stock: 1400 - price: 296 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU337: - cost: 220 - init_stock: 1450 - price: 245 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU338: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU339: - cost: 238 - init_stock: 3150 - price: 265 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU34: - cost: 85 - init_stock: 650 - price: 95 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU340: - cost: 432 - init_stock: 4350 - price: 480 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU341: - cost: 415 - init_stock: 3500 - price: 462 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU342: - cost: 129 - init_stock: 450 - price: 144 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU343: - cost: 279 - init_stock: 750 - price: 310 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU344: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU345: - cost: 397 - init_stock: 4450 - price: 442 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU346: - cost: 315 - init_stock: 2100 - price: 350 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU347: - cost: 447 - init_stock: 4100 - price: 497 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU348: - cost: 132 - init_stock: 1000 - price: 147 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU349: - cost: 60 - init_stock: 3350 - price: 67 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU35: - cost: 240 - init_stock: 4300 - price: 267 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU350: - cost: 251 - init_stock: 4600 - price: 279 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU351: - cost: 139 - init_stock: 3350 - price: 155 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU352: - cost: 63 - init_stock: 900 - price: 71 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU353: - cost: 227 - init_stock: 4650 - price: 253 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU354: - cost: 356 - init_stock: 3950 - price: 396 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU355: - cost: 56 - init_stock: 500 - price: 63 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU356: - cost: 313 - init_stock: 1450 - price: 348 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU357: - cost: 449 - init_stock: 4600 - price: 499 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU358: - cost: 242 - init_stock: 3450 - price: 269 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU359: - cost: 73 - init_stock: 3750 - price: 82 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU36: - cost: 94 - init_stock: 500 - price: 105 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU360: - cost: 435 - init_stock: 1250 - price: 484 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU361: - cost: 146 - init_stock: 4500 - price: 163 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU362: - cost: 417 - init_stock: 4350 - price: 464 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU363: - cost: 46 - init_stock: 3900 - price: 52 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU364: - cost: 348 - init_stock: 1650 - price: 387 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU365: - cost: 142 - init_stock: 4150 - price: 158 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU366: - cost: 399 - init_stock: 4300 - price: 444 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU367: - cost: 244 - init_stock: 2300 - price: 272 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU368: - cost: 424 - init_stock: 1650 - price: 472 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU369: - cost: 86 - init_stock: 3750 - price: 96 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU37: - cost: 59 - init_stock: 2950 - price: 66 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU370: - cost: 100 - init_stock: 750 - price: 112 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU371: - cost: 295 - init_stock: 250 - price: 328 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU372: - cost: 60 - init_stock: 1600 - price: 67 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU373: - cost: 52 - init_stock: 3350 - price: 58 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU374: - cost: 34 - init_stock: 2700 - price: 38 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU375: - cost: 254 - init_stock: 3600 - price: 283 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU376: - cost: 140 - init_stock: 4100 - price: 156 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU377: - cost: 70 - init_stock: 3350 - price: 78 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU378: - cost: 381 - init_stock: 1750 - price: 424 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU379: - cost: 9 - init_stock: 2450 - price: 11 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU38: - cost: 309 - init_stock: 2150 - price: 344 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU380: - cost: 353 - init_stock: 2650 - price: 393 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU381: - cost: 428 - init_stock: 300 - price: 476 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU382: - cost: 112 - init_stock: 3550 - price: 125 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU383: - cost: 273 - init_stock: 4600 - price: 304 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU384: - cost: 288 - init_stock: 450 - price: 320 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU385: - cost: 108 - init_stock: 650 - price: 120 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU386: - cost: 265 - init_stock: 1550 - price: 295 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU387: - cost: 100 - init_stock: 4850 - price: 112 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU388: - cost: 364 - init_stock: 2200 - price: 405 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU389: - cost: 338 - init_stock: 3500 - price: 376 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU39: - cost: 22 - init_stock: 2550 - price: 25 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU390: - cost: 129 - init_stock: 1950 - price: 144 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU391: - cost: 209 - init_stock: 3350 - price: 233 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU392: - cost: 146 - init_stock: 3700 - price: 163 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU393: - cost: 438 - init_stock: 3350 - price: 487 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU394: - cost: 138 - init_stock: 2650 - price: 154 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU395: - cost: 439 - init_stock: 1650 - price: 488 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU396: - cost: 299 - init_stock: 3050 - price: 333 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU397: - cost: 414 - init_stock: 2550 - price: 460 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU398: - cost: 210 - init_stock: 2900 - price: 234 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU399: - cost: 338 - init_stock: 1850 - price: 376 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU4: - cost: 395 - init_stock: 350 - price: 439 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU40: - cost: 441 - init_stock: 4950 - price: 490 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU400: - cost: 400 - init_stock: 4200 - price: 445 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU401: - cost: 369 - init_stock: 3900 - price: 410 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU402: - cost: 77 - init_stock: 350 - price: 86 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU403: - cost: 80 - init_stock: 4950 - price: 89 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU404: - cost: 258 - init_stock: 3050 - price: 287 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU405: - cost: 414 - init_stock: 950 - price: 460 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU406: - cost: 294 - init_stock: 5000 - price: 327 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU407: - cost: 23 - init_stock: 2300 - price: 26 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU408: - cost: 399 - init_stock: 400 - price: 444 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU409: - cost: 231 - init_stock: 4550 - price: 257 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU41: - cost: 126 - init_stock: 3950 - price: 141 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU410: - cost: 63 - init_stock: 800 - price: 70 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU411: - cost: 189 - init_stock: 4750 - price: 210 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU412: - cost: 257 - init_stock: 3100 - price: 286 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU413: - cost: 362 - init_stock: 4150 - price: 403 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU414: - cost: 148 - init_stock: 4350 - price: 165 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU415: - cost: 261 - init_stock: 1150 - price: 291 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU416: - cost: 205 - init_stock: 450 - price: 228 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU417: - cost: 398 - init_stock: 3600 - price: 443 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU418: - cost: 412 - init_stock: 650 - price: 458 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU419: - cost: 272 - init_stock: 4450 - price: 303 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU42: - cost: 415 - init_stock: 1300 - price: 462 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU420: - cost: 241 - init_stock: 2100 - price: 268 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU421: - cost: 192 - init_stock: 2300 - price: 214 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU422: - cost: 46 - init_stock: 2700 - price: 52 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU423: - cost: 169 - init_stock: 3300 - price: 188 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU424: - cost: 170 - init_stock: 550 - price: 189 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU425: - cost: 145 - init_stock: 600 - price: 162 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU426: - cost: 112 - init_stock: 4900 - price: 125 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU427: - cost: 371 - init_stock: 4700 - price: 413 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU428: - cost: 351 - init_stock: 3150 - price: 391 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU429: - cost: 35 - init_stock: 2050 - price: 39 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU43: - cost: 195 - init_stock: 3400 - price: 217 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU430: - cost: 194 - init_stock: 3650 - price: 216 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU431: - cost: 295 - init_stock: 1050 - price: 328 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU432: - cost: 22 - init_stock: 2300 - price: 25 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU433: - cost: 313 - init_stock: 2250 - price: 348 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU434: - cost: 33 - init_stock: 1500 - price: 37 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU435: - cost: 244 - init_stock: 1500 - price: 272 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU436: - cost: 64 - init_stock: 4050 - price: 72 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU437: - cost: 103 - init_stock: 700 - price: 115 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU438: - cost: 128 - init_stock: 500 - price: 143 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU439: - cost: 230 - init_stock: 1900 - price: 256 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU44: - cost: 324 - init_stock: 2400 - price: 360 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU440: - cost: 223 - init_stock: 4100 - price: 248 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU441: - cost: 227 - init_stock: 2800 - price: 253 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU442: - cost: 423 - init_stock: 4400 - price: 470 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU443: - cost: 196 - init_stock: 3650 - price: 218 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU444: - cost: 140 - init_stock: 300 - price: 156 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU445: - cost: 234 - init_stock: 4300 - price: 260 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU446: - cost: 447 - init_stock: 2450 - price: 497 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU447: - cost: 176 - init_stock: 250 - price: 196 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU448: - cost: 436 - init_stock: 3550 - price: 485 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU449: - cost: 253 - init_stock: 1900 - price: 282 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU45: - cost: 227 - init_stock: 500 - price: 253 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU450: - cost: 52 - init_stock: 4900 - price: 58 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU451: - cost: 421 - init_stock: 3450 - price: 468 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU452: - cost: 56 - init_stock: 3950 - price: 63 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU453: - cost: 33 - init_stock: 1750 - price: 37 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU454: - cost: 444 - init_stock: 4250 - price: 494 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU455: - cost: 122 - init_stock: 1300 - price: 136 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU456: - cost: 304 - init_stock: 1250 - price: 338 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU457: - cost: 152 - init_stock: 3200 - price: 169 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU458: - cost: 362 - init_stock: 350 - price: 403 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU459: - cost: 382 - init_stock: 1700 - price: 425 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU46: - cost: 269 - init_stock: 2500 - price: 299 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU460: - cost: 364 - init_stock: 3650 - price: 405 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU461: - cost: 225 - init_stock: 2400 - price: 251 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU462: - cost: 403 - init_stock: 450 - price: 448 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU463: - cost: 168 - init_stock: 4100 - price: 187 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU464: - cost: 251 - init_stock: 1700 - price: 279 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU465: - cost: 132 - init_stock: 5000 - price: 147 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU466: - cost: 87 - init_stock: 2100 - price: 97 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU467: - cost: 127 - init_stock: 1800 - price: 142 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU468: - cost: 49 - init_stock: 2500 - price: 55 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU469: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU47: - cost: 108 - init_stock: 1950 - price: 121 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU470: - cost: 335 - init_stock: 1600 - price: 373 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU471: - cost: 283 - init_stock: 3550 - price: 315 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU472: - cost: 378 - init_stock: 2950 - price: 421 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU473: - cost: 175 - init_stock: 1050 - price: 195 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU474: - cost: 403 - init_stock: 4300 - price: 448 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU475: - cost: 30 - init_stock: 4700 - price: 34 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU476: - cost: 225 - init_stock: 4500 - price: 251 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU477: - cost: 260 - init_stock: 2350 - price: 289 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU478: - cost: 431 - init_stock: 4400 - price: 479 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU479: - cost: 329 - init_stock: 1900 - price: 366 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU48: - cost: 306 - init_stock: 2900 - price: 340 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU480: - cost: 16 - init_stock: 1500 - price: 18 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU481: - cost: 297 - init_stock: 4400 - price: 330 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU482: - cost: 84 - init_stock: 4350 - price: 94 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU483: - cost: 268 - init_stock: 1700 - price: 298 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU484: - cost: 75 - init_stock: 3650 - price: 84 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU485: - cost: 286 - init_stock: 3350 - price: 318 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU486: - cost: 109 - init_stock: 2600 - price: 122 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU487: - cost: 249 - init_stock: 3300 - price: 277 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU488: - cost: 32 - init_stock: 1350 - price: 36 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU489: - cost: 53 - init_stock: 1550 - price: 59 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU49: - cost: 236 - init_stock: 4750 - price: 263 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU490: - cost: 144 - init_stock: 4050 - price: 161 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU491: - cost: 399 - init_stock: 3750 - price: 444 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU492: - cost: 47 - init_stock: 4000 - price: 53 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU493: - cost: 414 - init_stock: 1350 - price: 461 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU494: - cost: 130 - init_stock: 4700 - price: 145 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU495: - cost: 134 - init_stock: 3100 - price: 149 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU496: - cost: 307 - init_stock: 2950 - price: 342 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU497: - cost: 29 - init_stock: 450 - price: 33 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU498: - cost: 274 - init_stock: 3250 - price: 305 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU499: - cost: 276 - init_stock: 1450 - price: 307 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU5: - cost: 419 - init_stock: 3550 - price: 466 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU50: - cost: 253 - init_stock: 1850 - price: 282 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU500: - cost: 187 - init_stock: 2100 - price: 208 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU501: - cost: 174 - init_stock: 3800 - price: 194 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU502: - cost: 62 - init_stock: 3250 - price: 69 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU503: - cost: 187 - init_stock: 2850 - price: 208 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU504: - cost: 225 - init_stock: 1500 - price: 251 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU505: - cost: 383 - init_stock: 2950 - price: 426 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU506: - cost: 134 - init_stock: 4650 - price: 149 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU507: - cost: 168 - init_stock: 750 - price: 187 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU508: - cost: 244 - init_stock: 4800 - price: 272 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU509: - cost: 267 - init_stock: 4050 - price: 297 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU51: - cost: 265 - init_stock: 3350 - price: 295 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU510: - cost: 84 - init_stock: 450 - price: 94 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU511: - cost: 324 - init_stock: 3750 - price: 361 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU512: - cost: 420 - init_stock: 1100 - price: 467 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU513: - cost: 308 - init_stock: 350 - price: 343 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU514: - cost: 167 - init_stock: 3150 - price: 186 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU515: - cost: 130 - init_stock: 2700 - price: 145 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU516: - cost: 57 - init_stock: 1900 - price: 64 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU517: - cost: 105 - init_stock: 450 - price: 117 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU518: - cost: 23 - init_stock: 1050 - price: 26 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU519: - cost: 209 - init_stock: 2500 - price: 233 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU52: - cost: 19 - init_stock: 3350 - price: 22 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU520: - cost: 188 - init_stock: 1100 - price: 209 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU521: - cost: 198 - init_stock: 2500 - price: 220 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU522: - cost: 140 - init_stock: 4350 - price: 156 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU523: - cost: 58 - init_stock: 5000 - price: 65 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU524: - cost: 264 - init_stock: 4700 - price: 294 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU525: - cost: 388 - init_stock: 2100 - price: 432 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU526: - cost: 377 - init_stock: 1200 - price: 419 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU527: - cost: 117 - init_stock: 3500 - price: 131 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU528: - cost: 284 - init_stock: 1050 - price: 316 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU529: - cost: 268 - init_stock: 1550 - price: 298 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU53: - cost: 135 - init_stock: 3500 - price: 151 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU530: - cost: 117 - init_stock: 2250 - price: 131 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU531: - cost: 92 - init_stock: 3000 - price: 103 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU532: - cost: 315 - init_stock: 3850 - price: 351 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU533: - cost: 266 - init_stock: 2100 - price: 296 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU534: - cost: 382 - init_stock: 2050 - price: 425 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU535: - cost: 273 - init_stock: 4300 - price: 304 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU536: - cost: 322 - init_stock: 2600 - price: 358 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU537: - cost: 288 - init_stock: 450 - price: 321 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU538: - cost: 411 - init_stock: 2500 - price: 457 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU539: - cost: 148 - init_stock: 3900 - price: 165 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU54: - cost: 142 - init_stock: 2300 - price: 158 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU540: - cost: 349 - init_stock: 2100 - price: 388 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU541: - cost: 117 - init_stock: 1450 - price: 131 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU542: - cost: 34 - init_stock: 1300 - price: 38 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU543: - cost: 387 - init_stock: 4250 - price: 430 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU544: - cost: 311 - init_stock: 4850 - price: 346 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU545: - cost: 157 - init_stock: 750 - price: 175 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU546: - cost: 441 - init_stock: 4800 - price: 491 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU547: - cost: 144 - init_stock: 2200 - price: 161 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU548: - cost: 99 - init_stock: 300 - price: 111 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU549: - cost: 348 - init_stock: 3950 - price: 387 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU55: - cost: 433 - init_stock: 3250 - price: 482 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU550: - cost: 233 - init_stock: 4700 - price: 259 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU551: - cost: 41 - init_stock: 1550 - price: 46 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU552: - cost: 171 - init_stock: 4500 - price: 191 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU553: - cost: 187 - init_stock: 1500 - price: 208 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU554: - cost: 306 - init_stock: 2050 - price: 340 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU555: - cost: 440 - init_stock: 4200 - price: 489 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU556: - cost: 99 - init_stock: 1500 - price: 110 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU557: - cost: 300 - init_stock: 3650 - price: 334 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU558: - cost: 359 - init_stock: 850 - price: 399 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU559: - cost: 281 - init_stock: 2700 - price: 313 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU56: - cost: 339 - init_stock: 3700 - price: 377 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU560: - cost: 254 - init_stock: 2050 - price: 283 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU561: - cost: 181 - init_stock: 1950 - price: 202 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU562: - cost: 312 - init_stock: 4450 - price: 347 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU563: - cost: 360 - init_stock: 250 - price: 401 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU564: - cost: 98 - init_stock: 450 - price: 109 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU565: - cost: 51 - init_stock: 3750 - price: 57 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU566: - cost: 117 - init_stock: 550 - price: 131 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU567: - cost: 324 - init_stock: 450 - price: 361 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU568: - cost: 67 - init_stock: 3900 - price: 75 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU569: - cost: 425 - init_stock: 2400 - price: 473 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU57: - cost: 323 - init_stock: 2500 - price: 359 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU570: - cost: 349 - init_stock: 4150 - price: 388 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU571: - cost: 151 - init_stock: 1950 - price: 168 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU572: - cost: 23 - init_stock: 2250 - price: 26 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU573: - cost: 221 - init_stock: 2050 - price: 246 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU574: - cost: 84 - init_stock: 2500 - price: 94 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU575: - cost: 213 - init_stock: 3950 - price: 237 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU576: - cost: 238 - init_stock: 3900 - price: 265 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU577: - cost: 16 - init_stock: 4350 - price: 18 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU578: - cost: 90 - init_stock: 3550 - price: 100 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU579: - cost: 373 - init_stock: 450 - price: 415 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU58: - cost: 325 - init_stock: 2400 - price: 362 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU580: - cost: 182 - init_stock: 250 - price: 203 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU581: - cost: 136 - init_stock: 4300 - price: 152 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU582: - cost: 323 - init_stock: 4800 - price: 359 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU583: - cost: 77 - init_stock: 4350 - price: 86 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU584: - cost: 29 - init_stock: 2250 - price: 33 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU585: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU586: - cost: 54 - init_stock: 2200 - price: 61 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU587: - cost: 261 - init_stock: 3900 - price: 290 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU588: - cost: 428 - init_stock: 2200 - price: 476 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU589: - cost: 219 - init_stock: 1700 - price: 244 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU59: - cost: 130 - init_stock: 2550 - price: 145 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU590: - cost: 63 - init_stock: 1550 - price: 70 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU591: - cost: 185 - init_stock: 4200 - price: 206 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU592: - cost: 196 - init_stock: 2300 - price: 218 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU593: - cost: 111 - init_stock: 2200 - price: 124 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU594: - cost: 214 - init_stock: 2500 - price: 238 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU595: - cost: 65 - init_stock: 2150 - price: 73 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU596: - cost: 388 - init_stock: 350 - price: 432 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU597: - cost: 226 - init_stock: 4350 - price: 252 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU598: - cost: 313 - init_stock: 700 - price: 348 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU599: - cost: 48 - init_stock: 3400 - price: 54 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU6: - cost: 109 - init_stock: 4400 - price: 122 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU60: - cost: 180 - init_stock: 1950 - price: 200 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU600: - cost: 347 - init_stock: 3250 - price: 386 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU601: - cost: 124 - init_stock: 1950 - price: 138 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU602: - cost: 165 - init_stock: 4900 - price: 184 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU603: - cost: 250 - init_stock: 2100 - price: 278 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU604: - cost: 243 - init_stock: 2500 - price: 270 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU605: - cost: 259 - init_stock: 1200 - price: 288 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU606: - cost: 102 - init_stock: 550 - price: 114 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU607: - cost: 187 - init_stock: 3950 - price: 208 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU608: - cost: 35 - init_stock: 3650 - price: 39 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU609: - cost: 91 - init_stock: 950 - price: 102 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU61: - cost: 414 - init_stock: 3750 - price: 461 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU610: - cost: 404 - init_stock: 3400 - price: 449 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU611: - cost: 275 - init_stock: 3850 - price: 306 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU612: - cost: 351 - init_stock: 4400 - price: 391 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU613: - cost: 156 - init_stock: 350 - price: 174 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU614: - cost: 155 - init_stock: 750 - price: 173 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU615: - cost: 297 - init_stock: 4550 - price: 330 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU616: - cost: 208 - init_stock: 3650 - price: 232 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU617: - cost: 182 - init_stock: 3000 - price: 203 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU618: - cost: 69 - init_stock: 1150 - price: 77 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU619: - cost: 170 - init_stock: 4300 - price: 189 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU62: - cost: 111 - init_stock: 450 - price: 124 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU620: - cost: 207 - init_stock: 1950 - price: 231 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU621: - cost: 179 - init_stock: 3600 - price: 199 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU622: - cost: 227 - init_stock: 3250 - price: 253 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU623: - cost: 301 - init_stock: 2750 - price: 335 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU624: - cost: 433 - init_stock: 2700 - price: 482 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU625: - cost: 41 - init_stock: 4450 - price: 46 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU626: - cost: 405 - init_stock: 4450 - price: 451 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU627: - cost: 198 - init_stock: 4100 - price: 220 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU628: - cost: 111 - init_stock: 1300 - price: 124 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU629: - cost: 317 - init_stock: 4600 - price: 353 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU63: - cost: 61 - init_stock: 700 - price: 68 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU630: - cost: 109 - init_stock: 850 - price: 122 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU631: - cost: 266 - init_stock: 2450 - price: 296 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU632: - cost: 76 - init_stock: 4700 - price: 85 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU633: - cost: 165 - init_stock: 3350 - price: 184 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU634: - cost: 15 - init_stock: 3650 - price: 17 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU635: - cost: 306 - init_stock: 4650 - price: 341 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU636: - cost: 346 - init_stock: 1850 - price: 385 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU637: - cost: 74 - init_stock: 3050 - price: 83 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU638: - cost: 337 - init_stock: 400 - price: 375 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU639: - cost: 294 - init_stock: 2000 - price: 327 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU64: - cost: 32 - init_stock: 950 - price: 36 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU640: - cost: 247 - init_stock: 4550 - price: 275 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU641: - cost: 328 - init_stock: 4050 - price: 365 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU642: - cost: 372 - init_stock: 1250 - price: 414 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU643: - cost: 322 - init_stock: 2300 - price: 358 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU644: - cost: 41 - init_stock: 4100 - price: 46 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU645: - cost: 420 - init_stock: 4950 - price: 467 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU646: - cost: 170 - init_stock: 650 - price: 189 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU647: - cost: 272 - init_stock: 3850 - price: 303 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU648: - cost: 203 - init_stock: 2750 - price: 226 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU649: - cost: 178 - init_stock: 1250 - price: 198 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU65: - cost: 13 - init_stock: 4700 - price: 15 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU650: - cost: 315 - init_stock: 2800 - price: 351 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU651: - cost: 342 - init_stock: 4800 - price: 380 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU652: - cost: 110 - init_stock: 2000 - price: 123 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU653: - cost: 431 - init_stock: 4800 - price: 479 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU654: - cost: 59 - init_stock: 400 - price: 66 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU655: - cost: 299 - init_stock: 2100 - price: 333 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU656: - cost: 47 - init_stock: 3600 - price: 53 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU657: - cost: 65 - init_stock: 4050 - price: 73 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU658: - cost: 63 - init_stock: 1800 - price: 70 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU659: - cost: 18 - init_stock: 2800 - price: 21 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU66: - cost: 128 - init_stock: 4500 - price: 143 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU660: - cost: 438 - init_stock: 4150 - price: 487 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU661: - cost: 309 - init_stock: 3700 - price: 344 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU662: - cost: 334 - init_stock: 1900 - price: 372 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU663: - cost: 376 - init_stock: 2000 - price: 418 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU664: - cost: 315 - init_stock: 4200 - price: 351 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU665: - cost: 443 - init_stock: 2150 - price: 493 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU666: - cost: 306 - init_stock: 4400 - price: 341 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU667: - cost: 292 - init_stock: 650 - price: 325 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU668: - cost: 257 - init_stock: 3000 - price: 286 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU669: - cost: 66 - init_stock: 800 - price: 74 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU67: - cost: 222 - init_stock: 2650 - price: 247 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU670: - cost: 260 - init_stock: 4300 - price: 289 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU671: - cost: 240 - init_stock: 4150 - price: 267 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU672: - cost: 332 - init_stock: 650 - price: 369 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU673: - cost: 289 - init_stock: 2050 - price: 322 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU674: - cost: 200 - init_stock: 1100 - price: 223 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU675: - cost: 394 - init_stock: 1650 - price: 438 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU676: - cost: 219 - init_stock: 3050 - price: 244 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU677: - cost: 382 - init_stock: 2200 - price: 425 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU678: - cost: 151 - init_stock: 1800 - price: 168 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU679: - cost: 355 - init_stock: 2200 - price: 395 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU68: - cost: 152 - init_stock: 700 - price: 169 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU680: - cost: 234 - init_stock: 1500 - price: 260 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU681: - cost: 417 - init_stock: 4850 - price: 464 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU682: - cost: 333 - init_stock: 2650 - price: 370 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU683: - cost: 294 - init_stock: 3350 - price: 327 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU684: - cost: 319 - init_stock: 3950 - price: 355 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU685: - cost: 379 - init_stock: 2250 - price: 422 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU686: - cost: 56 - init_stock: 3150 - price: 63 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU687: - cost: 30 - init_stock: 3800 - price: 34 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU688: - cost: 435 - init_stock: 3850 - price: 484 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU689: - cost: 449 - init_stock: 750 - price: 499 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU69: - cost: 158 - init_stock: 3350 - price: 176 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU690: - cost: 393 - init_stock: 4150 - price: 437 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU691: - cost: 15 - init_stock: 3950 - price: 17 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU692: - cost: 202 - init_stock: 3250 - price: 225 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU693: - cost: 17 - init_stock: 3050 - price: 19 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU694: - cost: 187 - init_stock: 3750 - price: 208 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU695: - cost: 171 - init_stock: 350 - price: 190 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU696: - cost: 313 - init_stock: 2900 - price: 348 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU697: - cost: 409 - init_stock: 4000 - price: 455 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU698: - cost: 178 - init_stock: 3050 - price: 198 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU699: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU7: - cost: 90 - init_stock: 4800 - price: 101 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU70: - cost: 167 - init_stock: 1750 - price: 186 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU700: - cost: 66 - init_stock: 450 - price: 74 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU701: - cost: 359 - init_stock: 3850 - price: 399 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU702: - cost: 73 - init_stock: 1700 - price: 82 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU703: - cost: 326 - init_stock: 1900 - price: 363 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU704: - cost: 345 - init_stock: 4350 - price: 384 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU705: - cost: 115 - init_stock: 3150 - price: 128 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU706: - cost: 163 - init_stock: 4550 - price: 182 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU707: - cost: 16 - init_stock: 3450 - price: 18 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU708: - cost: 160 - init_stock: 1400 - price: 178 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU709: - cost: 91 - init_stock: 2650 - price: 102 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU71: - cost: 283 - init_stock: 2250 - price: 315 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU710: - cost: 226 - init_stock: 950 - price: 252 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU711: - cost: 252 - init_stock: 3450 - price: 281 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU712: - cost: 208 - init_stock: 550 - price: 232 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU713: - cost: 256 - init_stock: 2150 - price: 285 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU714: - cost: 71 - init_stock: 2050 - price: 79 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU715: - cost: 210 - init_stock: 850 - price: 234 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU716: - cost: 63 - init_stock: 3750 - price: 70 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU717: - cost: 310 - init_stock: 400 - price: 345 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU718: - cost: 321 - init_stock: 2900 - price: 357 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU719: - cost: 306 - init_stock: 3250 - price: 340 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU72: - cost: 412 - init_stock: 4250 - price: 458 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU720: - cost: 292 - init_stock: 2100 - price: 325 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU721: - cost: 65 - init_stock: 550 - price: 73 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU722: - cost: 352 - init_stock: 4850 - price: 392 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU723: - cost: 286 - init_stock: 4450 - price: 318 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU724: - cost: 360 - init_stock: 1650 - price: 400 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU725: - cost: 157 - init_stock: 1850 - price: 175 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU726: - cost: 412 - init_stock: 4450 - price: 458 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU727: - cost: 376 - init_stock: 2850 - price: 418 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU728: - cost: 427 - init_stock: 1850 - price: 475 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU729: - cost: 291 - init_stock: 1800 - price: 324 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU73: - cost: 190 - init_stock: 2700 - price: 212 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU730: - cost: 14 - init_stock: 650 - price: 16 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU731: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU732: - cost: 36 - init_stock: 3100 - price: 41 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU733: - cost: 283 - init_stock: 1300 - price: 315 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU734: - cost: 33 - init_stock: 2550 - price: 37 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU735: - cost: 239 - init_stock: 4950 - price: 266 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU736: - cost: 331 - init_stock: 1250 - price: 368 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU737: - cost: 427 - init_stock: 1550 - price: 475 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU738: - cost: 166 - init_stock: 2400 - price: 185 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU739: - cost: 427 - init_stock: 3700 - price: 475 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU74: - cost: 143 - init_stock: 2100 - price: 159 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU740: - cost: 351 - init_stock: 3200 - price: 390 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU741: - cost: 81 - init_stock: 2800 - price: 91 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU742: - cost: 169 - init_stock: 1100 - price: 188 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU743: - cost: 195 - init_stock: 2150 - price: 217 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU744: - cost: 341 - init_stock: 2900 - price: 379 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU745: - cost: 284 - init_stock: 4600 - price: 316 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU746: - cost: 393 - init_stock: 2000 - price: 437 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU747: - cost: 335 - init_stock: 3950 - price: 373 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU748: - cost: 247 - init_stock: 3300 - price: 275 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU749: - cost: 354 - init_stock: 2650 - price: 394 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU75: - cost: 117 - init_stock: 950 - price: 131 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU750: - cost: 230 - init_stock: 3100 - price: 256 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU751: - cost: 332 - init_stock: 3250 - price: 369 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU752: - cost: 233 - init_stock: 3900 - price: 259 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU753: - cost: 69 - init_stock: 3250 - price: 77 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU754: - cost: 348 - init_stock: 650 - price: 387 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU755: - cost: 318 - init_stock: 4000 - price: 354 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU756: - cost: 221 - init_stock: 4500 - price: 246 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU757: - cost: 36 - init_stock: 2850 - price: 40 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU758: - cost: 216 - init_stock: 2000 - price: 241 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU759: - cost: 391 - init_stock: 1900 - price: 435 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU76: - cost: 132 - init_stock: 3200 - price: 147 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU760: - cost: 158 - init_stock: 2550 - price: 176 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU761: - cost: 201 - init_stock: 2750 - price: 224 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU762: - cost: 237 - init_stock: 2550 - price: 264 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU763: - cost: 346 - init_stock: 3950 - price: 385 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU764: - cost: 314 - init_stock: 1700 - price: 349 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU765: - cost: 310 - init_stock: 650 - price: 345 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU766: - cost: 430 - init_stock: 1000 - price: 478 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU767: - cost: 85 - init_stock: 3800 - price: 95 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU768: - cost: 162 - init_stock: 4600 - price: 181 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU769: - cost: 21 - init_stock: 1450 - price: 24 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU77: - cost: 368 - init_stock: 3600 - price: 409 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU770: - cost: 135 - init_stock: 3100 - price: 150 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU771: - cost: 90 - init_stock: 350 - price: 101 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU772: - cost: 230 - init_stock: 300 - price: 256 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU773: - cost: 75 - init_stock: 4600 - price: 84 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU774: - cost: 402 - init_stock: 2950 - price: 447 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU775: - cost: 157 - init_stock: 4300 - price: 175 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU776: - cost: 92 - init_stock: 4250 - price: 103 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU777: - cost: 262 - init_stock: 2850 - price: 292 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU778: - cost: 182 - init_stock: 400 - price: 203 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU779: - cost: 105 - init_stock: 2150 - price: 117 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU78: - cost: 210 - init_stock: 650 - price: 234 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU780: - cost: 62 - init_stock: 4100 - price: 69 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU781: - cost: 334 - init_stock: 1800 - price: 372 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU782: - cost: 24 - init_stock: 500 - price: 27 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU783: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU784: - cost: 278 - init_stock: 2600 - price: 309 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU785: - cost: 171 - init_stock: 3500 - price: 191 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU786: - cost: 81 - init_stock: 1100 - price: 91 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU787: - cost: 324 - init_stock: 3050 - price: 360 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU788: - cost: 315 - init_stock: 1400 - price: 351 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU789: - cost: 137 - init_stock: 2900 - price: 153 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU79: - cost: 220 - init_stock: 550 - price: 245 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU790: - cost: 375 - init_stock: 3300 - price: 417 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU791: - cost: 120 - init_stock: 1400 - price: 134 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU792: - cost: 281 - init_stock: 1050 - price: 313 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU793: - cost: 175 - init_stock: 3800 - price: 195 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU794: - cost: 292 - init_stock: 4000 - price: 325 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU795: - cost: 248 - init_stock: 2400 - price: 276 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU796: - cost: 402 - init_stock: 2450 - price: 447 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU797: - cost: 90 - init_stock: 1950 - price: 100 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU798: - cost: 383 - init_stock: 1500 - price: 426 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU799: - cost: 56 - init_stock: 350 - price: 63 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU8: - cost: 112 - init_stock: 3150 - price: 125 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU80: - cost: 146 - init_stock: 3500 - price: 163 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU800: - cost: 268 - init_stock: 4700 - price: 298 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU801: - cost: 350 - init_stock: 1800 - price: 389 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU802: - cost: 155 - init_stock: 3100 - price: 173 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU803: - cost: 244 - init_stock: 950 - price: 272 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU804: - cost: 351 - init_stock: 2350 - price: 390 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU805: - cost: 54 - init_stock: 1650 - price: 61 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU806: - cost: 142 - init_stock: 2600 - price: 158 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU807: - cost: 407 - init_stock: 1300 - price: 453 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU808: - cost: 224 - init_stock: 4550 - price: 249 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU809: - cost: 49 - init_stock: 3250 - price: 55 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU81: - cost: 163 - init_stock: 3300 - price: 182 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU810: - cost: 248 - init_stock: 300 - price: 276 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU811: - cost: 228 - init_stock: 1850 - price: 254 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU812: - cost: 226 - init_stock: 4000 - price: 252 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU813: - cost: 227 - init_stock: 350 - price: 253 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU814: - cost: 436 - init_stock: 4850 - price: 485 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU815: - cost: 351 - init_stock: 1200 - price: 390 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU816: - cost: 136 - init_stock: 4550 - price: 152 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU817: - cost: 204 - init_stock: 250 - price: 227 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU818: - cost: 318 - init_stock: 2150 - price: 354 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU819: - cost: 271 - init_stock: 1350 - price: 302 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU82: - cost: 261 - init_stock: 1400 - price: 290 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU820: - cost: 237 - init_stock: 4100 - price: 264 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU821: - cost: 89 - init_stock: 3450 - price: 99 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU822: - cost: 122 - init_stock: 3500 - price: 136 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU823: - cost: 67 - init_stock: 1400 - price: 75 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU824: - cost: 153 - init_stock: 3500 - price: 170 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU825: - cost: 192 - init_stock: 750 - price: 214 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU826: - cost: 347 - init_stock: 2750 - price: 386 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU827: - cost: 133 - init_stock: 3200 - price: 148 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU828: - cost: 360 - init_stock: 3450 - price: 400 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU829: - cost: 54 - init_stock: 3700 - price: 61 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU83: - cost: 266 - init_stock: 850 - price: 296 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU830: - cost: 150 - init_stock: 1200 - price: 167 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU831: - cost: 235 - init_stock: 1450 - price: 262 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU832: - cost: 29 - init_stock: 4100 - price: 33 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU833: - cost: 360 - init_stock: 4900 - price: 400 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU834: - cost: 379 - init_stock: 250 - price: 422 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU835: - cost: 396 - init_stock: 2600 - price: 440 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU836: - cost: 290 - init_stock: 4800 - price: 323 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU837: - cost: 335 - init_stock: 1300 - price: 373 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU838: - cost: 410 - init_stock: 3850 - price: 456 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU839: - cost: 425 - init_stock: 3000 - price: 473 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU84: - cost: 286 - init_stock: 2100 - price: 318 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU840: - cost: 239 - init_stock: 2500 - price: 266 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU841: - cost: 256 - init_stock: 4250 - price: 285 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU842: - cost: 36 - init_stock: 4200 - price: 41 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU843: - cost: 324 - init_stock: 3600 - price: 360 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU844: - cost: 45 - init_stock: 3950 - price: 51 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU845: - cost: 259 - init_stock: 1100 - price: 288 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU846: - cost: 436 - init_stock: 1950 - price: 485 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU847: - cost: 349 - init_stock: 4550 - price: 388 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU848: - cost: 275 - init_stock: 2300 - price: 306 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU849: - cost: 288 - init_stock: 2000 - price: 320 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU85: - cost: 397 - init_stock: 5000 - price: 442 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU850: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU851: - cost: 47 - init_stock: 3200 - price: 53 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU852: - cost: 275 - init_stock: 2700 - price: 306 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU853: - cost: 347 - init_stock: 2800 - price: 386 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU854: - cost: 190 - init_stock: 3250 - price: 212 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU855: - cost: 68 - init_stock: 3600 - price: 76 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU856: - cost: 342 - init_stock: 1600 - price: 380 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU857: - cost: 415 - init_stock: 5000 - price: 462 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU858: - cost: 72 - init_stock: 1200 - price: 80 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU859: - cost: 193 - init_stock: 1400 - price: 215 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU86: - cost: 347 - init_stock: 4250 - price: 386 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU860: - cost: 281 - init_stock: 750 - price: 313 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU861: - cost: 429 - init_stock: 650 - price: 477 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU862: - cost: 216 - init_stock: 800 - price: 240 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU863: - cost: 423 - init_stock: 4650 - price: 470 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU864: - cost: 182 - init_stock: 950 - price: 203 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU865: - cost: 129 - init_stock: 950 - price: 144 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU866: - cost: 154 - init_stock: 4400 - price: 172 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU867: - cost: 449 - init_stock: 5000 - price: 499 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU868: - cost: 36 - init_stock: 2500 - price: 41 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU869: - cost: 87 - init_stock: 4850 - price: 97 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU87: - cost: 331 - init_stock: 3450 - price: 368 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU870: - cost: 252 - init_stock: 3350 - price: 281 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU871: - cost: 90 - init_stock: 4950 - price: 101 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU872: - cost: 119 - init_stock: 1600 - price: 133 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU873: - cost: 380 - init_stock: 1550 - price: 423 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU874: - cost: 422 - init_stock: 3000 - price: 469 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU875: - cost: 45 - init_stock: 1150 - price: 51 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU876: - cost: 272 - init_stock: 3050 - price: 303 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU877: - cost: 36 - init_stock: 1750 - price: 40 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU878: - cost: 24 - init_stock: 3400 - price: 27 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU879: - cost: 135 - init_stock: 5000 - price: 150 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU88: - cost: 423 - init_stock: 600 - price: 471 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU880: - cost: 389 - init_stock: 1550 - price: 433 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU881: - cost: 387 - init_stock: 3500 - price: 431 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU882: - cost: 174 - init_stock: 800 - price: 194 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU883: - cost: 255 - init_stock: 1900 - price: 284 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU884: - cost: 125 - init_stock: 1950 - price: 139 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU885: - cost: 44 - init_stock: 1350 - price: 49 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU886: - cost: 334 - init_stock: 3650 - price: 372 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU887: - cost: 239 - init_stock: 4350 - price: 266 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU888: - cost: 128 - init_stock: 450 - price: 143 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU889: - cost: 90 - init_stock: 1050 - price: 101 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU89: - cost: 124 - init_stock: 4650 - price: 138 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU890: - cost: 144 - init_stock: 5000 - price: 161 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU891: - cost: 320 - init_stock: 1100 - price: 356 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU892: - cost: 281 - init_stock: 4600 - price: 313 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU893: - cost: 206 - init_stock: 4950 - price: 229 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU894: - cost: 116 - init_stock: 3700 - price: 129 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU895: - cost: 207 - init_stock: 650 - price: 230 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU896: - cost: 260 - init_stock: 4000 - price: 289 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU897: - cost: 353 - init_stock: 3600 - price: 393 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU898: - cost: 429 - init_stock: 550 - price: 477 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU899: - cost: 209 - init_stock: 2400 - price: 233 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU9: - cost: 149 - init_stock: 4800 - price: 166 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU90: - cost: 408 - init_stock: 2550 - price: 454 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU900: - cost: 142 - init_stock: 2750 - price: 158 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU901: - cost: 193 - init_stock: 1450 - price: 215 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU902: - cost: 112 - init_stock: 4000 - price: 125 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU903: - cost: 321 - init_stock: 1900 - price: 357 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU904: - cost: 446 - init_stock: 2550 - price: 496 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU905: - cost: 224 - init_stock: 1550 - price: 249 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU906: - cost: 149 - init_stock: 2600 - price: 166 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU907: - cost: 19 - init_stock: 4150 - price: 22 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU908: - cost: 367 - init_stock: 3900 - price: 408 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU909: - cost: 433 - init_stock: 4800 - price: 482 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU91: - cost: 272 - init_stock: 800 - price: 303 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU910: - cost: 203 - init_stock: 1650 - price: 226 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU911: - cost: 414 - init_stock: 3500 - price: 461 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU912: - cost: 212 - init_stock: 1350 - price: 236 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU913: - cost: 289 - init_stock: 2300 - price: 322 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU914: - cost: 244 - init_stock: 3100 - price: 272 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU915: - cost: 303 - init_stock: 2800 - price: 337 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU916: - cost: 303 - init_stock: 4450 - price: 337 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU917: - cost: 232 - init_stock: 1500 - price: 258 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU918: - cost: 133 - init_stock: 2750 - price: 148 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU919: - cost: 353 - init_stock: 1250 - price: 393 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU92: - cost: 235 - init_stock: 3150 - price: 262 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU920: - cost: 321 - init_stock: 4300 - price: 357 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU921: - cost: 204 - init_stock: 3300 - price: 227 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU922: - cost: 100 - init_stock: 3350 - price: 112 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU923: - cost: 446 - init_stock: 2900 - price: 496 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU924: - cost: 284 - init_stock: 4250 - price: 316 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU925: - cost: 324 - init_stock: 750 - price: 360 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU926: - cost: 324 - init_stock: 3350 - price: 360 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU927: - cost: 234 - init_stock: 1050 - price: 260 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU928: - cost: 441 - init_stock: 4150 - price: 491 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU929: - cost: 323 - init_stock: 5000 - price: 359 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU93: - cost: 363 - init_stock: 3350 - price: 404 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU930: - cost: 178 - init_stock: 1400 - price: 198 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU931: - cost: 63 - init_stock: 700 - price: 71 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU932: - cost: 146 - init_stock: 3300 - price: 163 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU933: - cost: 101 - init_stock: 3900 - price: 113 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU934: - cost: 197 - init_stock: 3350 - price: 219 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU935: - cost: 327 - init_stock: 4700 - price: 364 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU936: - cost: 21 - init_stock: 250 - price: 24 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU937: - cost: 121 - init_stock: 850 - price: 135 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU938: - cost: 388 - init_stock: 1050 - price: 432 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU939: - cost: 155 - init_stock: 2950 - price: 173 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU94: - cost: 165 - init_stock: 2350 - price: 184 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU940: - cost: 12 - init_stock: 4650 - price: 14 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU941: - cost: 72 - init_stock: 2850 - price: 80 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU942: - cost: 181 - init_stock: 650 - price: 202 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU943: - cost: 124 - init_stock: 2450 - price: 138 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU944: - cost: 176 - init_stock: 2200 - price: 196 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU945: - cost: 126 - init_stock: 850 - price: 141 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU946: - cost: 292 - init_stock: 2450 - price: 325 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU947: - cost: 304 - init_stock: 4550 - price: 338 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU948: - cost: 382 - init_stock: 1400 - price: 425 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU949: - cost: 278 - init_stock: 250 - price: 309 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU95: - cost: 122 - init_stock: 1050 - price: 136 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU950: - cost: 91 - init_stock: 2700 - price: 102 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU951: - cost: 67 - init_stock: 900 - price: 75 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU952: - cost: 140 - init_stock: 550 - price: 156 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU953: - cost: 124 - init_stock: 1750 - price: 138 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU954: - cost: 266 - init_stock: 4300 - price: 296 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU955: - cost: 49 - init_stock: 3150 - price: 55 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU956: - cost: 253 - init_stock: 4250 - price: 282 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU957: - cost: 274 - init_stock: 2550 - price: 305 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU958: - cost: 332 - init_stock: 3300 - price: 369 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU959: - cost: 72 - init_stock: 4100 - price: 81 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU96: - cost: 158 - init_stock: 2200 - price: 176 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU960: - cost: 132 - init_stock: 300 - price: 147 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU961: - cost: 237 - init_stock: 2200 - price: 264 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU962: - cost: 318 - init_stock: 2200 - price: 354 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU963: - cost: 314 - init_stock: 1050 - price: 349 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU964: - cost: 219 - init_stock: 3650 - price: 244 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU965: - cost: 111 - init_stock: 2550 - price: 124 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU966: - cost: 271 - init_stock: 2200 - price: 302 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU967: - cost: 60 - init_stock: 2250 - price: 67 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU968: - cost: 252 - init_stock: 2450 - price: 281 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU969: - cost: 224 - init_stock: 300 - price: 249 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU97: - cost: 25 - init_stock: 1500 - price: 28 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU970: - cost: 219 - init_stock: 250 - price: 244 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU971: - cost: 331 - init_stock: 3650 - price: 368 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU972: - cost: 188 - init_stock: 3450 - price: 209 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU973: - cost: 243 - init_stock: 550 - price: 271 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU974: - cost: 153 - init_stock: 1600 - price: 170 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU975: - cost: 178 - init_stock: 1100 - price: 198 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU976: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU977: - cost: 210 - init_stock: 2700 - price: 234 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU978: - cost: 423 - init_stock: 3200 - price: 470 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU979: - cost: 398 - init_stock: 4800 - price: 443 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU98: - cost: 398 - init_stock: 1750 - price: 443 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU980: - cost: 35 - init_stock: 3400 - price: 39 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU981: - cost: 433 - init_stock: 3600 - price: 482 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU982: - cost: 191 - init_stock: 2600 - price: 213 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU983: - cost: 404 - init_stock: 4600 - price: 449 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU984: - cost: 208 - init_stock: 4550 - price: 232 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU985: - cost: 261 - init_stock: 2550 - price: 290 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU986: - cost: 247 - init_stock: 850 - price: 275 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU987: - cost: 390 - init_stock: 1700 - price: 434 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU988: - cost: 91 - init_stock: 1150 - price: 102 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU989: - cost: 435 - init_stock: 4650 - price: 484 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU99: - cost: 324 - init_stock: 2250 - price: 361 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU990: - cost: 97 - init_stock: 2850 - price: 108 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU991: - cost: 368 - init_stock: 950 - price: 409 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU992: - cost: 390 - init_stock: 4650 - price: 434 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU993: - cost: 130 - init_stock: 4300 - price: 145 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU994: - cost: 423 - init_stock: 2500 - price: 470 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU995: - cost: 216 - init_stock: 3800 - price: 241 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU996: - cost: 234 - init_stock: 3650 - price: 260 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU997: - cost: 360 - init_stock: 1700 - price: 400 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU998: - cost: 402 - init_stock: 2750 - price: 447 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU999: - cost: 71 - init_stock: 1150 - price: 79 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 500 - definition_ref: WarehouseFacility - name: WAREHOUSE0 - skus: - SKU0: - cost: 322 - init_stock: 1260 - price: 322 - service_level: 0.96 - vlt: 1 - SKU1: - cost: 284 - init_stock: 440 - price: 284 - service_level: 0.96 - vlt: 3 - SKU10: - cost: 68 - init_stock: 500 - price: 68 - service_level: 0.96 - vlt: 3 - SKU100: - cost: 16 - init_stock: 1300 - price: 16 - service_level: 0.96 - vlt: 3 - SKU101: - cost: 84 - init_stock: 1420 - price: 84 - service_level: 0.96 - vlt: 3 - SKU102: - cost: 328 - init_stock: 1860 - price: 328 - service_level: 0.96 - vlt: 2 - SKU103: - cost: 334 - init_stock: 1900 - price: 334 - service_level: 0.96 - vlt: 1 - SKU104: - cost: 49 - init_stock: 1300 - price: 49 - service_level: 0.96 - vlt: 3 - SKU105: - cost: 110 - init_stock: 1140 - price: 110 - service_level: 0.96 - vlt: 2 - SKU106: - cost: 251 - init_stock: 1460 - price: 251 - service_level: 0.96 - vlt: 3 - SKU107: - cost: 423 - init_stock: 1740 - price: 423 - service_level: 0.96 - vlt: 3 - SKU108: - cost: 458 - init_stock: 1840 - price: 458 - service_level: 0.96 - vlt: 3 - SKU109: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU11: - cost: 400 - init_stock: 1020 - price: 400 - service_level: 0.96 - vlt: 2 - SKU110: - cost: 66 - init_stock: 280 - price: 66 - service_level: 0.96 - vlt: 3 - SKU111: - cost: 260 - init_stock: 1220 - price: 260 - service_level: 0.96 - vlt: 1 - SKU112: - cost: 61 - init_stock: 1900 - price: 61 - service_level: 0.96 - vlt: 2 - SKU113: - cost: 348 - init_stock: 620 - price: 348 - service_level: 0.96 - vlt: 3 - SKU114: - cost: 389 - init_stock: 540 - price: 389 - service_level: 0.96 - vlt: 1 - SKU115: - cost: 286 - init_stock: 1720 - price: 286 - service_level: 0.96 - vlt: 2 - SKU116: - cost: 496 - init_stock: 1440 - price: 496 - service_level: 0.96 - vlt: 3 - SKU117: - cost: 320 - init_stock: 1840 - price: 320 - service_level: 0.96 - vlt: 3 - SKU118: - cost: 183 - init_stock: 660 - price: 183 - service_level: 0.96 - vlt: 1 - SKU119: - cost: 209 - init_stock: 640 - price: 209 - service_level: 0.96 - vlt: 3 - SKU12: - cost: 112 - init_stock: 1680 - price: 112 - service_level: 0.96 - vlt: 1 - SKU120: - cost: 121 - init_stock: 1960 - price: 121 - service_level: 0.96 - vlt: 1 - SKU121: - cost: 40 - init_stock: 1700 - price: 40 - service_level: 0.96 - vlt: 1 - SKU122: - cost: 437 - init_stock: 140 - price: 437 - service_level: 0.96 - vlt: 3 - SKU123: - cost: 233 - init_stock: 380 - price: 233 - service_level: 0.96 - vlt: 3 - SKU124: - cost: 182 - init_stock: 720 - price: 182 - service_level: 0.96 - vlt: 2 - SKU125: - cost: 16 - init_stock: 1840 - price: 16 - service_level: 0.96 - vlt: 2 - SKU126: - cost: 36 - init_stock: 780 - price: 36 - service_level: 0.96 - vlt: 3 - SKU127: - cost: 217 - init_stock: 620 - price: 217 - service_level: 0.96 - vlt: 2 - SKU128: - cost: 165 - init_stock: 380 - price: 165 - service_level: 0.96 - vlt: 1 - SKU129: - cost: 143 - init_stock: 1000 - price: 143 - service_level: 0.96 - vlt: 2 - SKU13: - cost: 317 - init_stock: 1140 - price: 317 - service_level: 0.96 - vlt: 3 - SKU130: - cost: 348 - init_stock: 1960 - price: 348 - service_level: 0.96 - vlt: 3 - SKU131: - cost: 64 - init_stock: 900 - price: 64 - service_level: 0.96 - vlt: 1 - SKU132: - cost: 427 - init_stock: 420 - price: 427 - service_level: 0.96 - vlt: 2 - SKU133: - cost: 224 - init_stock: 580 - price: 224 - service_level: 0.96 - vlt: 2 - SKU134: - cost: 336 - init_stock: 1540 - price: 336 - service_level: 0.96 - vlt: 3 - SKU135: - cost: 153 - init_stock: 2000 - price: 153 - service_level: 0.96 - vlt: 1 - SKU136: - cost: 199 - init_stock: 1420 - price: 199 - service_level: 0.96 - vlt: 3 - SKU137: - cost: 93 - init_stock: 1480 - price: 93 - service_level: 0.96 - vlt: 2 - SKU138: - cost: 228 - init_stock: 720 - price: 228 - service_level: 0.96 - vlt: 1 - SKU139: - cost: 207 - init_stock: 480 - price: 207 - service_level: 0.96 - vlt: 1 - SKU14: - cost: 268 - init_stock: 1240 - price: 268 - service_level: 0.96 - vlt: 1 - SKU140: - cost: 261 - init_stock: 680 - price: 261 - service_level: 0.96 - vlt: 3 - SKU141: - cost: 190 - init_stock: 820 - price: 190 - service_level: 0.96 - vlt: 1 - SKU142: - cost: 320 - init_stock: 760 - price: 320 - service_level: 0.96 - vlt: 3 - SKU143: - cost: 318 - init_stock: 520 - price: 318 - service_level: 0.96 - vlt: 3 - SKU144: - cost: 400 - init_stock: 240 - price: 400 - service_level: 0.96 - vlt: 1 - SKU145: - cost: 399 - init_stock: 1640 - price: 399 - service_level: 0.96 - vlt: 1 - SKU146: - cost: 177 - init_stock: 960 - price: 177 - service_level: 0.96 - vlt: 3 - SKU147: - cost: 472 - init_stock: 1120 - price: 472 - service_level: 0.96 - vlt: 3 - SKU148: - cost: 313 - init_stock: 1540 - price: 313 - service_level: 0.96 - vlt: 3 - SKU149: - cost: 357 - init_stock: 1540 - price: 357 - service_level: 0.96 - vlt: 3 - SKU15: - cost: 52 - init_stock: 100 - price: 52 - service_level: 0.96 - vlt: 2 - SKU150: - cost: 106 - init_stock: 1400 - price: 106 - service_level: 0.96 - vlt: 2 - SKU151: - cost: 223 - init_stock: 1460 - price: 223 - service_level: 0.96 - vlt: 1 - SKU152: - cost: 10 - init_stock: 1020 - price: 10 - service_level: 0.96 - vlt: 3 - SKU153: - cost: 441 - init_stock: 1240 - price: 441 - service_level: 0.96 - vlt: 1 - SKU154: - cost: 77 - init_stock: 1700 - price: 77 - service_level: 0.96 - vlt: 3 - SKU155: - cost: 422 - init_stock: 1060 - price: 422 - service_level: 0.96 - vlt: 1 - SKU156: - cost: 10 - init_stock: 240 - price: 10 - service_level: 0.96 - vlt: 1 - SKU157: - cost: 410 - init_stock: 1500 - price: 410 - service_level: 0.96 - vlt: 2 - SKU158: - cost: 145 - init_stock: 1620 - price: 145 - service_level: 0.96 - vlt: 3 - SKU159: - cost: 193 - init_stock: 500 - price: 193 - service_level: 0.96 - vlt: 2 - SKU16: - cost: 175 - init_stock: 1160 - price: 175 - service_level: 0.96 - vlt: 3 - SKU160: - cost: 459 - init_stock: 1700 - price: 459 - service_level: 0.96 - vlt: 1 - SKU161: - cost: 239 - init_stock: 920 - price: 239 - service_level: 0.96 - vlt: 2 - SKU162: - cost: 158 - init_stock: 100 - price: 158 - service_level: 0.96 - vlt: 2 - SKU163: - cost: 486 - init_stock: 780 - price: 486 - service_level: 0.96 - vlt: 2 - SKU164: - cost: 496 - init_stock: 2000 - price: 496 - service_level: 0.96 - vlt: 1 - SKU165: - cost: 274 - init_stock: 660 - price: 274 - service_level: 0.96 - vlt: 3 - SKU166: - cost: 79 - init_stock: 1780 - price: 79 - service_level: 0.96 - vlt: 1 - SKU167: - cost: 443 - init_stock: 260 - price: 443 - service_level: 0.96 - vlt: 1 - SKU168: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU169: - cost: 369 - init_stock: 1960 - price: 369 - service_level: 0.96 - vlt: 3 - SKU17: - cost: 346 - init_stock: 180 - price: 346 - service_level: 0.96 - vlt: 1 - SKU170: - cost: 68 - init_stock: 1100 - price: 68 - service_level: 0.96 - vlt: 2 - SKU171: - cost: 398 - init_stock: 1520 - price: 398 - service_level: 0.96 - vlt: 1 - SKU172: - cost: 200 - init_stock: 1420 - price: 200 - service_level: 0.96 - vlt: 2 - SKU173: - cost: 175 - init_stock: 1920 - price: 175 - service_level: 0.96 - vlt: 3 - SKU174: - cost: 291 - init_stock: 1520 - price: 291 - service_level: 0.96 - vlt: 2 - SKU175: - cost: 84 - init_stock: 1500 - price: 84 - service_level: 0.96 - vlt: 2 - SKU176: - cost: 407 - init_stock: 1320 - price: 407 - service_level: 0.96 - vlt: 1 - SKU177: - cost: 257 - init_stock: 620 - price: 257 - service_level: 0.96 - vlt: 1 - SKU178: - cost: 423 - init_stock: 100 - price: 423 - service_level: 0.96 - vlt: 2 - SKU179: - cost: 497 - init_stock: 1660 - price: 497 - service_level: 0.96 - vlt: 1 - SKU18: - cost: 258 - init_stock: 1620 - price: 258 - service_level: 0.96 - vlt: 1 - SKU180: - cost: 217 - init_stock: 1100 - price: 217 - service_level: 0.96 - vlt: 2 - SKU181: - cost: 143 - init_stock: 1200 - price: 143 - service_level: 0.96 - vlt: 1 - SKU182: - cost: 437 - init_stock: 1980 - price: 437 - service_level: 0.96 - vlt: 3 - SKU183: - cost: 145 - init_stock: 160 - price: 145 - service_level: 0.96 - vlt: 3 - SKU184: - cost: 73 - init_stock: 480 - price: 73 - service_level: 0.96 - vlt: 2 - SKU185: - cost: 10 - init_stock: 1800 - price: 10 - service_level: 0.96 - vlt: 2 - SKU186: - cost: 359 - init_stock: 440 - price: 359 - service_level: 0.96 - vlt: 1 - SKU187: - cost: 177 - init_stock: 600 - price: 177 - service_level: 0.96 - vlt: 3 - SKU188: - cost: 391 - init_stock: 1740 - price: 391 - service_level: 0.96 - vlt: 3 - SKU189: - cost: 358 - init_stock: 700 - price: 358 - service_level: 0.96 - vlt: 2 - SKU19: - cost: 477 - init_stock: 1820 - price: 477 - service_level: 0.96 - vlt: 3 - SKU190: - cost: 113 - init_stock: 340 - price: 113 - service_level: 0.96 - vlt: 3 - SKU191: - cost: 473 - init_stock: 1080 - price: 473 - service_level: 0.96 - vlt: 2 - SKU192: - cost: 415 - init_stock: 1220 - price: 415 - service_level: 0.96 - vlt: 2 - SKU193: - cost: 207 - init_stock: 600 - price: 207 - service_level: 0.96 - vlt: 2 - SKU194: - cost: 432 - init_stock: 100 - price: 432 - service_level: 0.96 - vlt: 2 - SKU195: - cost: 218 - init_stock: 620 - price: 218 - service_level: 0.96 - vlt: 2 - SKU196: - cost: 49 - init_stock: 1360 - price: 49 - service_level: 0.96 - vlt: 3 - SKU197: - cost: 303 - init_stock: 1140 - price: 303 - service_level: 0.96 - vlt: 1 - SKU198: - cost: 169 - init_stock: 1080 - price: 169 - service_level: 0.96 - vlt: 2 - SKU199: - cost: 449 - init_stock: 460 - price: 449 - service_level: 0.96 - vlt: 1 - SKU2: - cost: 331 - init_stock: 1400 - price: 331 - service_level: 0.96 - vlt: 3 - SKU20: - cost: 335 - init_stock: 500 - price: 335 - service_level: 0.96 - vlt: 3 - SKU200: - cost: 65 - init_stock: 500 - price: 65 - service_level: 0.96 - vlt: 1 - SKU201: - cost: 104 - init_stock: 1180 - price: 104 - service_level: 0.96 - vlt: 1 - SKU202: - cost: 142 - init_stock: 1460 - price: 142 - service_level: 0.96 - vlt: 1 - SKU203: - cost: 440 - init_stock: 1640 - price: 440 - service_level: 0.96 - vlt: 2 - SKU204: - cost: 489 - init_stock: 940 - price: 489 - service_level: 0.96 - vlt: 2 - SKU205: - cost: 130 - init_stock: 2000 - price: 130 - service_level: 0.96 - vlt: 3 - SKU206: - cost: 335 - init_stock: 220 - price: 335 - service_level: 0.96 - vlt: 2 - SKU207: - cost: 140 - init_stock: 1600 - price: 140 - service_level: 0.96 - vlt: 1 - SKU208: - cost: 491 - init_stock: 1540 - price: 491 - service_level: 0.96 - vlt: 1 - SKU209: - cost: 179 - init_stock: 400 - price: 179 - service_level: 0.96 - vlt: 3 - SKU21: - cost: 123 - init_stock: 2000 - price: 123 - service_level: 0.96 - vlt: 2 - SKU210: - cost: 404 - init_stock: 1380 - price: 404 - service_level: 0.96 - vlt: 3 - SKU211: - cost: 174 - init_stock: 1820 - price: 174 - service_level: 0.96 - vlt: 2 - SKU212: - cost: 405 - init_stock: 1580 - price: 405 - service_level: 0.96 - vlt: 3 - SKU213: - cost: 121 - init_stock: 1280 - price: 121 - service_level: 0.96 - vlt: 2 - SKU214: - cost: 101 - init_stock: 200 - price: 101 - service_level: 0.96 - vlt: 2 - SKU215: - cost: 419 - init_stock: 940 - price: 419 - service_level: 0.96 - vlt: 1 - SKU216: - cost: 330 - init_stock: 460 - price: 330 - service_level: 0.96 - vlt: 1 - SKU217: - cost: 284 - init_stock: 1300 - price: 284 - service_level: 0.96 - vlt: 2 - SKU218: - cost: 205 - init_stock: 1180 - price: 205 - service_level: 0.96 - vlt: 1 - SKU219: - cost: 92 - init_stock: 920 - price: 92 - service_level: 0.96 - vlt: 3 - SKU22: - cost: 493 - init_stock: 1320 - price: 493 - service_level: 0.96 - vlt: 1 - SKU220: - cost: 387 - init_stock: 1740 - price: 387 - service_level: 0.96 - vlt: 2 - SKU221: - cost: 39 - init_stock: 1560 - price: 39 - service_level: 0.96 - vlt: 2 - SKU222: - cost: 115 - init_stock: 720 - price: 115 - service_level: 0.96 - vlt: 2 - SKU223: - cost: 196 - init_stock: 240 - price: 196 - service_level: 0.96 - vlt: 2 - SKU224: - cost: 387 - init_stock: 100 - price: 387 - service_level: 0.96 - vlt: 2 - SKU225: - cost: 164 - init_stock: 580 - price: 164 - service_level: 0.96 - vlt: 1 - SKU226: - cost: 206 - init_stock: 260 - price: 206 - service_level: 0.96 - vlt: 2 - SKU227: - cost: 479 - init_stock: 480 - price: 479 - service_level: 0.96 - vlt: 3 - SKU228: - cost: 14 - init_stock: 1800 - price: 14 - service_level: 0.96 - vlt: 1 - SKU229: - cost: 472 - init_stock: 880 - price: 472 - service_level: 0.96 - vlt: 1 - SKU23: - cost: 387 - init_stock: 840 - price: 387 - service_level: 0.96 - vlt: 1 - SKU230: - cost: 241 - init_stock: 460 - price: 241 - service_level: 0.96 - vlt: 3 - SKU231: - cost: 48 - init_stock: 1620 - price: 48 - service_level: 0.96 - vlt: 3 - SKU232: - cost: 224 - init_stock: 1920 - price: 224 - service_level: 0.96 - vlt: 1 - SKU233: - cost: 360 - init_stock: 1500 - price: 360 - service_level: 0.96 - vlt: 2 - SKU234: - cost: 287 - init_stock: 100 - price: 287 - service_level: 0.96 - vlt: 1 - SKU235: - cost: 24 - init_stock: 1140 - price: 24 - service_level: 0.96 - vlt: 3 - SKU236: - cost: 155 - init_stock: 1100 - price: 155 - service_level: 0.96 - vlt: 2 - SKU237: - cost: 433 - init_stock: 900 - price: 433 - service_level: 0.96 - vlt: 3 - SKU238: - cost: 64 - init_stock: 1320 - price: 64 - service_level: 0.96 - vlt: 3 - SKU239: - cost: 103 - init_stock: 1960 - price: 103 - service_level: 0.96 - vlt: 1 - SKU24: - cost: 97 - init_stock: 940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU240: - cost: 373 - init_stock: 940 - price: 373 - service_level: 0.96 - vlt: 2 - SKU241: - cost: 439 - init_stock: 1420 - price: 439 - service_level: 0.96 - vlt: 2 - SKU242: - cost: 17 - init_stock: 880 - price: 17 - service_level: 0.96 - vlt: 1 - SKU243: - cost: 352 - init_stock: 280 - price: 352 - service_level: 0.96 - vlt: 1 - SKU244: - cost: 174 - init_stock: 1640 - price: 174 - service_level: 0.96 - vlt: 2 - SKU245: - cost: 404 - init_stock: 1320 - price: 404 - service_level: 0.96 - vlt: 2 - SKU246: - cost: 300 - init_stock: 600 - price: 300 - service_level: 0.96 - vlt: 2 - SKU247: - cost: 386 - init_stock: 700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU248: - cost: 355 - init_stock: 580 - price: 355 - service_level: 0.96 - vlt: 3 - SKU249: - cost: 356 - init_stock: 500 - price: 356 - service_level: 0.96 - vlt: 1 - SKU25: - cost: 161 - init_stock: 100 - price: 161 - service_level: 0.96 - vlt: 2 - SKU250: - cost: 127 - init_stock: 1080 - price: 127 - service_level: 0.96 - vlt: 3 - SKU251: - cost: 344 - init_stock: 1220 - price: 344 - service_level: 0.96 - vlt: 1 - SKU252: - cost: 181 - init_stock: 1660 - price: 181 - service_level: 0.96 - vlt: 1 - SKU253: - cost: 448 - init_stock: 320 - price: 448 - service_level: 0.96 - vlt: 1 - SKU254: - cost: 484 - init_stock: 920 - price: 484 - service_level: 0.96 - vlt: 3 - SKU255: - cost: 290 - init_stock: 1340 - price: 290 - service_level: 0.96 - vlt: 2 - SKU256: - cost: 91 - init_stock: 1440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU257: - cost: 348 - init_stock: 1140 - price: 348 - service_level: 0.96 - vlt: 1 - SKU258: - cost: 489 - init_stock: 860 - price: 489 - service_level: 0.96 - vlt: 1 - SKU259: - cost: 333 - init_stock: 1380 - price: 333 - service_level: 0.96 - vlt: 1 - SKU26: - cost: 229 - init_stock: 1260 - price: 229 - service_level: 0.96 - vlt: 1 - SKU260: - cost: 487 - init_stock: 1040 - price: 487 - service_level: 0.96 - vlt: 3 - SKU261: - cost: 368 - init_stock: 440 - price: 368 - service_level: 0.96 - vlt: 1 - SKU262: - cost: 332 - init_stock: 1560 - price: 332 - service_level: 0.96 - vlt: 3 - SKU263: - cost: 189 - init_stock: 1480 - price: 189 - service_level: 0.96 - vlt: 3 - SKU264: - cost: 361 - init_stock: 1580 - price: 361 - service_level: 0.96 - vlt: 1 - SKU265: - cost: 286 - init_stock: 1180 - price: 286 - service_level: 0.96 - vlt: 3 - SKU266: - cost: 128 - init_stock: 940 - price: 128 - service_level: 0.96 - vlt: 2 - SKU267: - cost: 77 - init_stock: 1600 - price: 77 - service_level: 0.96 - vlt: 1 - SKU268: - cost: 221 - init_stock: 1780 - price: 221 - service_level: 0.96 - vlt: 2 - SKU269: - cost: 126 - init_stock: 880 - price: 126 - service_level: 0.96 - vlt: 2 - SKU27: - cost: 370 - init_stock: 1120 - price: 370 - service_level: 0.96 - vlt: 3 - SKU270: - cost: 182 - init_stock: 1480 - price: 182 - service_level: 0.96 - vlt: 3 - SKU271: - cost: 230 - init_stock: 360 - price: 230 - service_level: 0.96 - vlt: 1 - SKU272: - cost: 366 - init_stock: 340 - price: 366 - service_level: 0.96 - vlt: 2 - SKU273: - cost: 421 - init_stock: 360 - price: 421 - service_level: 0.96 - vlt: 2 - SKU274: - cost: 29 - init_stock: 1540 - price: 29 - service_level: 0.96 - vlt: 2 - SKU275: - cost: 50 - init_stock: 960 - price: 50 - service_level: 0.96 - vlt: 1 - SKU276: - cost: 163 - init_stock: 1080 - price: 163 - service_level: 0.96 - vlt: 3 - SKU277: - cost: 449 - init_stock: 820 - price: 449 - service_level: 0.96 - vlt: 1 - SKU278: - cost: 105 - init_stock: 240 - price: 105 - service_level: 0.96 - vlt: 3 - SKU279: - cost: 51 - init_stock: 780 - price: 51 - service_level: 0.96 - vlt: 2 - SKU28: - cost: 208 - init_stock: 940 - price: 208 - service_level: 0.96 - vlt: 2 - SKU280: - cost: 295 - init_stock: 660 - price: 295 - service_level: 0.96 - vlt: 2 - SKU281: - cost: 395 - init_stock: 1500 - price: 395 - service_level: 0.96 - vlt: 1 - SKU282: - cost: 63 - init_stock: 920 - price: 63 - service_level: 0.96 - vlt: 2 - SKU283: - cost: 392 - init_stock: 1840 - price: 392 - service_level: 0.96 - vlt: 3 - SKU284: - cost: 344 - init_stock: 1340 - price: 344 - service_level: 0.96 - vlt: 3 - SKU285: - cost: 133 - init_stock: 1820 - price: 133 - service_level: 0.96 - vlt: 2 - SKU286: - cost: 337 - init_stock: 780 - price: 337 - service_level: 0.96 - vlt: 1 - SKU287: - cost: 375 - init_stock: 1120 - price: 375 - service_level: 0.96 - vlt: 3 - SKU288: - cost: 181 - init_stock: 760 - price: 181 - service_level: 0.96 - vlt: 1 - SKU289: - cost: 67 - init_stock: 620 - price: 67 - service_level: 0.96 - vlt: 1 - SKU29: - cost: 245 - init_stock: 1160 - price: 245 - service_level: 0.96 - vlt: 1 - SKU290: - cost: 438 - init_stock: 1340 - price: 438 - service_level: 0.96 - vlt: 1 - SKU291: - cost: 94 - init_stock: 1220 - price: 94 - service_level: 0.96 - vlt: 1 - SKU292: - cost: 186 - init_stock: 100 - price: 186 - service_level: 0.96 - vlt: 1 - SKU293: - cost: 162 - init_stock: 1100 - price: 162 - service_level: 0.96 - vlt: 2 - SKU294: - cost: 409 - init_stock: 180 - price: 409 - service_level: 0.96 - vlt: 2 - SKU295: - cost: 435 - init_stock: 1860 - price: 435 - service_level: 0.96 - vlt: 3 - SKU296: - cost: 370 - init_stock: 1840 - price: 370 - service_level: 0.96 - vlt: 3 - SKU297: - cost: 298 - init_stock: 760 - price: 298 - service_level: 0.96 - vlt: 1 - SKU298: - cost: 286 - init_stock: 700 - price: 286 - service_level: 0.96 - vlt: 2 - SKU299: - cost: 367 - init_stock: 1020 - price: 367 - service_level: 0.96 - vlt: 3 - SKU3: - cost: 405 - init_stock: 240 - price: 405 - service_level: 0.96 - vlt: 3 - SKU30: - cost: 359 - init_stock: 1380 - price: 359 - service_level: 0.96 - vlt: 2 - SKU300: - cost: 376 - init_stock: 1160 - price: 376 - service_level: 0.96 - vlt: 1 - SKU301: - cost: 433 - init_stock: 1660 - price: 433 - service_level: 0.96 - vlt: 2 - SKU302: - cost: 184 - init_stock: 220 - price: 184 - service_level: 0.96 - vlt: 2 - SKU303: - cost: 246 - init_stock: 1880 - price: 246 - service_level: 0.96 - vlt: 1 - SKU304: - cost: 419 - init_stock: 460 - price: 419 - service_level: 0.96 - vlt: 3 - SKU305: - cost: 495 - init_stock: 2000 - price: 495 - service_level: 0.96 - vlt: 1 - SKU306: - cost: 479 - init_stock: 840 - price: 479 - service_level: 0.96 - vlt: 2 - SKU307: - cost: 210 - init_stock: 1560 - price: 210 - service_level: 0.96 - vlt: 1 - SKU308: - cost: 208 - init_stock: 100 - price: 208 - service_level: 0.96 - vlt: 2 - SKU309: - cost: 101 - init_stock: 1840 - price: 101 - service_level: 0.96 - vlt: 2 - SKU31: - cost: 69 - init_stock: 1120 - price: 69 - service_level: 0.96 - vlt: 3 - SKU310: - cost: 234 - init_stock: 880 - price: 234 - service_level: 0.96 - vlt: 3 - SKU311: - cost: 306 - init_stock: 1600 - price: 306 - service_level: 0.96 - vlt: 3 - SKU312: - cost: 291 - init_stock: 500 - price: 291 - service_level: 0.96 - vlt: 1 - SKU313: - cost: 324 - init_stock: 760 - price: 324 - service_level: 0.96 - vlt: 1 - SKU314: - cost: 404 - init_stock: 580 - price: 404 - service_level: 0.96 - vlt: 1 - SKU315: - cost: 471 - init_stock: 1680 - price: 471 - service_level: 0.96 - vlt: 2 - SKU316: - cost: 202 - init_stock: 360 - price: 202 - service_level: 0.96 - vlt: 1 - SKU317: - cost: 216 - init_stock: 480 - price: 216 - service_level: 0.96 - vlt: 2 - SKU318: - cost: 278 - init_stock: 1700 - price: 278 - service_level: 0.96 - vlt: 1 - SKU319: - cost: 257 - init_stock: 1060 - price: 257 - service_level: 0.96 - vlt: 3 - SKU32: - cost: 336 - init_stock: 440 - price: 336 - service_level: 0.96 - vlt: 1 - SKU320: - cost: 196 - init_stock: 780 - price: 196 - service_level: 0.96 - vlt: 3 - SKU321: - cost: 67 - init_stock: 320 - price: 67 - service_level: 0.96 - vlt: 3 - SKU322: - cost: 240 - init_stock: 2000 - price: 240 - service_level: 0.96 - vlt: 1 - SKU323: - cost: 66 - init_stock: 780 - price: 66 - service_level: 0.96 - vlt: 2 - SKU324: - cost: 442 - init_stock: 1860 - price: 442 - service_level: 0.96 - vlt: 3 - SKU325: - cost: 453 - init_stock: 1380 - price: 453 - service_level: 0.96 - vlt: 2 - SKU326: - cost: 345 - init_stock: 480 - price: 345 - service_level: 0.96 - vlt: 2 - SKU327: - cost: 457 - init_stock: 280 - price: 457 - service_level: 0.96 - vlt: 2 - SKU328: - cost: 390 - init_stock: 900 - price: 390 - service_level: 0.96 - vlt: 1 - SKU329: - cost: 67 - init_stock: 840 - price: 67 - service_level: 0.96 - vlt: 1 - SKU33: - cost: 416 - init_stock: 1840 - price: 416 - service_level: 0.96 - vlt: 2 - SKU330: - cost: 306 - init_stock: 780 - price: 306 - service_level: 0.96 - vlt: 2 - SKU331: - cost: 138 - init_stock: 820 - price: 138 - service_level: 0.96 - vlt: 3 - SKU332: - cost: 390 - init_stock: 1920 - price: 390 - service_level: 0.96 - vlt: 2 - SKU333: - cost: 485 - init_stock: 1060 - price: 485 - service_level: 0.96 - vlt: 2 - SKU334: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 1 - SKU335: - cost: 80 - init_stock: 1620 - price: 80 - service_level: 0.96 - vlt: 1 - SKU336: - cost: 296 - init_stock: 560 - price: 296 - service_level: 0.96 - vlt: 1 - SKU337: - cost: 245 - init_stock: 580 - price: 245 - service_level: 0.96 - vlt: 2 - SKU338: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 3 - SKU339: - cost: 265 - init_stock: 1260 - price: 265 - service_level: 0.96 - vlt: 2 - SKU34: - cost: 95 - init_stock: 260 - price: 95 - service_level: 0.96 - vlt: 3 - SKU340: - cost: 480 - init_stock: 1740 - price: 480 - service_level: 0.96 - vlt: 2 - SKU341: - cost: 462 - init_stock: 1400 - price: 462 - service_level: 0.96 - vlt: 1 - SKU342: - cost: 144 - init_stock: 180 - price: 144 - service_level: 0.96 - vlt: 3 - SKU343: - cost: 310 - init_stock: 300 - price: 310 - service_level: 0.96 - vlt: 2 - SKU344: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU345: - cost: 442 - init_stock: 1780 - price: 442 - service_level: 0.96 - vlt: 2 - SKU346: - cost: 350 - init_stock: 840 - price: 350 - service_level: 0.96 - vlt: 3 - SKU347: - cost: 497 - init_stock: 1640 - price: 497 - service_level: 0.96 - vlt: 1 - SKU348: - cost: 147 - init_stock: 400 - price: 147 - service_level: 0.96 - vlt: 1 - SKU349: - cost: 67 - init_stock: 1340 - price: 67 - service_level: 0.96 - vlt: 3 - SKU35: - cost: 267 - init_stock: 1720 - price: 267 - service_level: 0.96 - vlt: 3 - SKU350: - cost: 279 - init_stock: 1840 - price: 279 - service_level: 0.96 - vlt: 3 - SKU351: - cost: 155 - init_stock: 1340 - price: 155 - service_level: 0.96 - vlt: 1 - SKU352: - cost: 71 - init_stock: 360 - price: 71 - service_level: 0.96 - vlt: 2 - SKU353: - cost: 253 - init_stock: 1860 - price: 253 - service_level: 0.96 - vlt: 2 - SKU354: - cost: 396 - init_stock: 1580 - price: 396 - service_level: 0.96 - vlt: 3 - SKU355: - cost: 63 - init_stock: 200 - price: 63 - service_level: 0.96 - vlt: 1 - SKU356: - cost: 348 - init_stock: 580 - price: 348 - service_level: 0.96 - vlt: 1 - SKU357: - cost: 499 - init_stock: 1840 - price: 499 - service_level: 0.96 - vlt: 3 - SKU358: - cost: 269 - init_stock: 1380 - price: 269 - service_level: 0.96 - vlt: 2 - SKU359: - cost: 82 - init_stock: 1500 - price: 82 - service_level: 0.96 - vlt: 3 - SKU36: - cost: 105 - init_stock: 200 - price: 105 - service_level: 0.96 - vlt: 2 - SKU360: - cost: 484 - init_stock: 500 - price: 484 - service_level: 0.96 - vlt: 1 - SKU361: - cost: 163 - init_stock: 1800 - price: 163 - service_level: 0.96 - vlt: 1 - SKU362: - cost: 464 - init_stock: 1740 - price: 464 - service_level: 0.96 - vlt: 2 - SKU363: - cost: 52 - init_stock: 1560 - price: 52 - service_level: 0.96 - vlt: 2 - SKU364: - cost: 387 - init_stock: 660 - price: 387 - service_level: 0.96 - vlt: 1 - SKU365: - cost: 158 - init_stock: 1660 - price: 158 - service_level: 0.96 - vlt: 3 - SKU366: - cost: 444 - init_stock: 1720 - price: 444 - service_level: 0.96 - vlt: 3 - SKU367: - cost: 272 - init_stock: 920 - price: 272 - service_level: 0.96 - vlt: 3 - SKU368: - cost: 472 - init_stock: 660 - price: 472 - service_level: 0.96 - vlt: 1 - SKU369: - cost: 96 - init_stock: 1500 - price: 96 - service_level: 0.96 - vlt: 2 - SKU37: - cost: 66 - init_stock: 1180 - price: 66 - service_level: 0.96 - vlt: 2 - SKU370: - cost: 112 - init_stock: 300 - price: 112 - service_level: 0.96 - vlt: 2 - SKU371: - cost: 328 - init_stock: 100 - price: 328 - service_level: 0.96 - vlt: 3 - SKU372: - cost: 67 - init_stock: 640 - price: 67 - service_level: 0.96 - vlt: 1 - SKU373: - cost: 58 - init_stock: 1340 - price: 58 - service_level: 0.96 - vlt: 3 - SKU374: - cost: 38 - init_stock: 1080 - price: 38 - service_level: 0.96 - vlt: 2 - SKU375: - cost: 283 - init_stock: 1440 - price: 283 - service_level: 0.96 - vlt: 1 - SKU376: - cost: 156 - init_stock: 1640 - price: 156 - service_level: 0.96 - vlt: 1 - SKU377: - cost: 78 - init_stock: 1340 - price: 78 - service_level: 0.96 - vlt: 3 - SKU378: - cost: 424 - init_stock: 700 - price: 424 - service_level: 0.96 - vlt: 1 - SKU379: - cost: 11 - init_stock: 980 - price: 11 - service_level: 0.96 - vlt: 1 - SKU38: - cost: 344 - init_stock: 860 - price: 344 - service_level: 0.96 - vlt: 2 - SKU380: - cost: 393 - init_stock: 1060 - price: 393 - service_level: 0.96 - vlt: 1 - SKU381: - cost: 476 - init_stock: 120 - price: 476 - service_level: 0.96 - vlt: 1 - SKU382: - cost: 125 - init_stock: 1420 - price: 125 - service_level: 0.96 - vlt: 2 - SKU383: - cost: 304 - init_stock: 1840 - price: 304 - service_level: 0.96 - vlt: 3 - SKU384: - cost: 320 - init_stock: 180 - price: 320 - service_level: 0.96 - vlt: 2 - SKU385: - cost: 120 - init_stock: 260 - price: 120 - service_level: 0.96 - vlt: 2 - SKU386: - cost: 295 - init_stock: 620 - price: 295 - service_level: 0.96 - vlt: 1 - SKU387: - cost: 112 - init_stock: 1940 - price: 112 - service_level: 0.96 - vlt: 3 - SKU388: - cost: 405 - init_stock: 880 - price: 405 - service_level: 0.96 - vlt: 2 - SKU389: - cost: 376 - init_stock: 1400 - price: 376 - service_level: 0.96 - vlt: 2 - SKU39: - cost: 25 - init_stock: 1020 - price: 25 - service_level: 0.96 - vlt: 3 - SKU390: - cost: 144 - init_stock: 780 - price: 144 - service_level: 0.96 - vlt: 2 - SKU391: - cost: 233 - init_stock: 1340 - price: 233 - service_level: 0.96 - vlt: 2 - SKU392: - cost: 163 - init_stock: 1480 - price: 163 - service_level: 0.96 - vlt: 2 - SKU393: - cost: 487 - init_stock: 1340 - price: 487 - service_level: 0.96 - vlt: 1 - SKU394: - cost: 154 - init_stock: 1060 - price: 154 - service_level: 0.96 - vlt: 2 - SKU395: - cost: 488 - init_stock: 660 - price: 488 - service_level: 0.96 - vlt: 3 - SKU396: - cost: 333 - init_stock: 1220 - price: 333 - service_level: 0.96 - vlt: 3 - SKU397: - cost: 460 - init_stock: 1020 - price: 460 - service_level: 0.96 - vlt: 1 - SKU398: - cost: 234 - init_stock: 1160 - price: 234 - service_level: 0.96 - vlt: 2 - SKU399: - cost: 376 - init_stock: 740 - price: 376 - service_level: 0.96 - vlt: 2 - SKU4: - cost: 439 - init_stock: 140 - price: 439 - service_level: 0.96 - vlt: 2 - SKU40: - cost: 490 - init_stock: 1980 - price: 490 - service_level: 0.96 - vlt: 3 - SKU400: - cost: 445 - init_stock: 1680 - price: 445 - service_level: 0.96 - vlt: 2 - SKU401: - cost: 410 - init_stock: 1560 - price: 410 - service_level: 0.96 - vlt: 1 - SKU402: - cost: 86 - init_stock: 140 - price: 86 - service_level: 0.96 - vlt: 3 - SKU403: - cost: 89 - init_stock: 1980 - price: 89 - service_level: 0.96 - vlt: 3 - SKU404: - cost: 287 - init_stock: 1220 - price: 287 - service_level: 0.96 - vlt: 1 - SKU405: - cost: 460 - init_stock: 380 - price: 460 - service_level: 0.96 - vlt: 1 - SKU406: - cost: 327 - init_stock: 2000 - price: 327 - service_level: 0.96 - vlt: 1 - SKU407: - cost: 26 - init_stock: 920 - price: 26 - service_level: 0.96 - vlt: 2 - SKU408: - cost: 444 - init_stock: 160 - price: 444 - service_level: 0.96 - vlt: 2 - SKU409: - cost: 257 - init_stock: 1820 - price: 257 - service_level: 0.96 - vlt: 2 - SKU41: - cost: 141 - init_stock: 1580 - price: 141 - service_level: 0.96 - vlt: 2 - SKU410: - cost: 70 - init_stock: 320 - price: 70 - service_level: 0.96 - vlt: 1 - SKU411: - cost: 210 - init_stock: 1900 - price: 210 - service_level: 0.96 - vlt: 3 - SKU412: - cost: 286 - init_stock: 1240 - price: 286 - service_level: 0.96 - vlt: 2 - SKU413: - cost: 403 - init_stock: 1660 - price: 403 - service_level: 0.96 - vlt: 3 - SKU414: - cost: 165 - init_stock: 1740 - price: 165 - service_level: 0.96 - vlt: 1 - SKU415: - cost: 291 - init_stock: 460 - price: 291 - service_level: 0.96 - vlt: 3 - SKU416: - cost: 228 - init_stock: 180 - price: 228 - service_level: 0.96 - vlt: 3 - SKU417: - cost: 443 - init_stock: 1440 - price: 443 - service_level: 0.96 - vlt: 1 - SKU418: - cost: 458 - init_stock: 260 - price: 458 - service_level: 0.96 - vlt: 3 - SKU419: - cost: 303 - init_stock: 1780 - price: 303 - service_level: 0.96 - vlt: 3 - SKU42: - cost: 462 - init_stock: 520 - price: 462 - service_level: 0.96 - vlt: 3 - SKU420: - cost: 268 - init_stock: 840 - price: 268 - service_level: 0.96 - vlt: 1 - SKU421: - cost: 214 - init_stock: 920 - price: 214 - service_level: 0.96 - vlt: 2 - SKU422: - cost: 52 - init_stock: 1080 - price: 52 - service_level: 0.96 - vlt: 3 - SKU423: - cost: 188 - init_stock: 1320 - price: 188 - service_level: 0.96 - vlt: 1 - SKU424: - cost: 189 - init_stock: 220 - price: 189 - service_level: 0.96 - vlt: 2 - SKU425: - cost: 162 - init_stock: 240 - price: 162 - service_level: 0.96 - vlt: 3 - SKU426: - cost: 125 - init_stock: 1960 - price: 125 - service_level: 0.96 - vlt: 3 - SKU427: - cost: 413 - init_stock: 1880 - price: 413 - service_level: 0.96 - vlt: 1 - SKU428: - cost: 391 - init_stock: 1260 - price: 391 - service_level: 0.96 - vlt: 3 - SKU429: - cost: 39 - init_stock: 820 - price: 39 - service_level: 0.96 - vlt: 1 - SKU43: - cost: 217 - init_stock: 1360 - price: 217 - service_level: 0.96 - vlt: 1 - SKU430: - cost: 216 - init_stock: 1460 - price: 216 - service_level: 0.96 - vlt: 2 - SKU431: - cost: 328 - init_stock: 420 - price: 328 - service_level: 0.96 - vlt: 1 - SKU432: - cost: 25 - init_stock: 920 - price: 25 - service_level: 0.96 - vlt: 3 - SKU433: - cost: 348 - init_stock: 900 - price: 348 - service_level: 0.96 - vlt: 2 - SKU434: - cost: 37 - init_stock: 600 - price: 37 - service_level: 0.96 - vlt: 1 - SKU435: - cost: 272 - init_stock: 600 - price: 272 - service_level: 0.96 - vlt: 3 - SKU436: - cost: 72 - init_stock: 1620 - price: 72 - service_level: 0.96 - vlt: 1 - SKU437: - cost: 115 - init_stock: 280 - price: 115 - service_level: 0.96 - vlt: 2 - SKU438: - cost: 143 - init_stock: 200 - price: 143 - service_level: 0.96 - vlt: 3 - SKU439: - cost: 256 - init_stock: 760 - price: 256 - service_level: 0.96 - vlt: 1 - SKU44: - cost: 360 - init_stock: 960 - price: 360 - service_level: 0.96 - vlt: 1 - SKU440: - cost: 248 - init_stock: 1640 - price: 248 - service_level: 0.96 - vlt: 2 - SKU441: - cost: 253 - init_stock: 1120 - price: 253 - service_level: 0.96 - vlt: 2 - SKU442: - cost: 470 - init_stock: 1760 - price: 470 - service_level: 0.96 - vlt: 2 - SKU443: - cost: 218 - init_stock: 1460 - price: 218 - service_level: 0.96 - vlt: 1 - SKU444: - cost: 156 - init_stock: 120 - price: 156 - service_level: 0.96 - vlt: 2 - SKU445: - cost: 260 - init_stock: 1720 - price: 260 - service_level: 0.96 - vlt: 2 - SKU446: - cost: 497 - init_stock: 980 - price: 497 - service_level: 0.96 - vlt: 1 - SKU447: - cost: 196 - init_stock: 100 - price: 196 - service_level: 0.96 - vlt: 1 - SKU448: - cost: 485 - init_stock: 1420 - price: 485 - service_level: 0.96 - vlt: 3 - SKU449: - cost: 282 - init_stock: 760 - price: 282 - service_level: 0.96 - vlt: 2 - SKU45: - cost: 253 - init_stock: 200 - price: 253 - service_level: 0.96 - vlt: 1 - SKU450: - cost: 58 - init_stock: 1960 - price: 58 - service_level: 0.96 - vlt: 2 - SKU451: - cost: 468 - init_stock: 1380 - price: 468 - service_level: 0.96 - vlt: 3 - SKU452: - cost: 63 - init_stock: 1580 - price: 63 - service_level: 0.96 - vlt: 1 - SKU453: - cost: 37 - init_stock: 700 - price: 37 - service_level: 0.96 - vlt: 3 - SKU454: - cost: 494 - init_stock: 1700 - price: 494 - service_level: 0.96 - vlt: 1 - SKU455: - cost: 136 - init_stock: 520 - price: 136 - service_level: 0.96 - vlt: 2 - SKU456: - cost: 338 - init_stock: 500 - price: 338 - service_level: 0.96 - vlt: 2 - SKU457: - cost: 169 - init_stock: 1280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU458: - cost: 403 - init_stock: 140 - price: 403 - service_level: 0.96 - vlt: 3 - SKU459: - cost: 425 - init_stock: 680 - price: 425 - service_level: 0.96 - vlt: 3 - SKU46: - cost: 299 - init_stock: 1000 - price: 299 - service_level: 0.96 - vlt: 3 - SKU460: - cost: 405 - init_stock: 1460 - price: 405 - service_level: 0.96 - vlt: 2 - SKU461: - cost: 251 - init_stock: 960 - price: 251 - service_level: 0.96 - vlt: 1 - SKU462: - cost: 448 - init_stock: 180 - price: 448 - service_level: 0.96 - vlt: 1 - SKU463: - cost: 187 - init_stock: 1640 - price: 187 - service_level: 0.96 - vlt: 3 - SKU464: - cost: 279 - init_stock: 680 - price: 279 - service_level: 0.96 - vlt: 3 - SKU465: - cost: 147 - init_stock: 2000 - price: 147 - service_level: 0.96 - vlt: 2 - SKU466: - cost: 97 - init_stock: 840 - price: 97 - service_level: 0.96 - vlt: 1 - SKU467: - cost: 142 - init_stock: 720 - price: 142 - service_level: 0.96 - vlt: 2 - SKU468: - cost: 55 - init_stock: 1000 - price: 55 - service_level: 0.96 - vlt: 2 - SKU469: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 3 - SKU47: - cost: 121 - init_stock: 780 - price: 121 - service_level: 0.96 - vlt: 1 - SKU470: - cost: 373 - init_stock: 640 - price: 373 - service_level: 0.96 - vlt: 2 - SKU471: - cost: 315 - init_stock: 1420 - price: 315 - service_level: 0.96 - vlt: 3 - SKU472: - cost: 421 - init_stock: 1180 - price: 421 - service_level: 0.96 - vlt: 2 - SKU473: - cost: 195 - init_stock: 420 - price: 195 - service_level: 0.96 - vlt: 1 - SKU474: - cost: 448 - init_stock: 1720 - price: 448 - service_level: 0.96 - vlt: 2 - SKU475: - cost: 34 - init_stock: 1880 - price: 34 - service_level: 0.96 - vlt: 1 - SKU476: - cost: 251 - init_stock: 1800 - price: 251 - service_level: 0.96 - vlt: 1 - SKU477: - cost: 289 - init_stock: 940 - price: 289 - service_level: 0.96 - vlt: 2 - SKU478: - cost: 479 - init_stock: 1760 - price: 479 - service_level: 0.96 - vlt: 2 - SKU479: - cost: 366 - init_stock: 760 - price: 366 - service_level: 0.96 - vlt: 1 - SKU48: - cost: 340 - init_stock: 1160 - price: 340 - service_level: 0.96 - vlt: 2 - SKU480: - cost: 18 - init_stock: 600 - price: 18 - service_level: 0.96 - vlt: 2 - SKU481: - cost: 330 - init_stock: 1760 - price: 330 - service_level: 0.96 - vlt: 1 - SKU482: - cost: 94 - init_stock: 1740 - price: 94 - service_level: 0.96 - vlt: 3 - SKU483: - cost: 298 - init_stock: 680 - price: 298 - service_level: 0.96 - vlt: 2 - SKU484: - cost: 84 - init_stock: 1460 - price: 84 - service_level: 0.96 - vlt: 3 - SKU485: - cost: 318 - init_stock: 1340 - price: 318 - service_level: 0.96 - vlt: 3 - SKU486: - cost: 122 - init_stock: 1040 - price: 122 - service_level: 0.96 - vlt: 1 - SKU487: - cost: 277 - init_stock: 1320 - price: 277 - service_level: 0.96 - vlt: 3 - SKU488: - cost: 36 - init_stock: 540 - price: 36 - service_level: 0.96 - vlt: 3 - SKU489: - cost: 59 - init_stock: 620 - price: 59 - service_level: 0.96 - vlt: 3 - SKU49: - cost: 263 - init_stock: 1900 - price: 263 - service_level: 0.96 - vlt: 3 - SKU490: - cost: 161 - init_stock: 1620 - price: 161 - service_level: 0.96 - vlt: 2 - SKU491: - cost: 444 - init_stock: 1500 - price: 444 - service_level: 0.96 - vlt: 2 - SKU492: - cost: 53 - init_stock: 1600 - price: 53 - service_level: 0.96 - vlt: 2 - SKU493: - cost: 461 - init_stock: 540 - price: 461 - service_level: 0.96 - vlt: 1 - SKU494: - cost: 145 - init_stock: 1880 - price: 145 - service_level: 0.96 - vlt: 1 - SKU495: - cost: 149 - init_stock: 1240 - price: 149 - service_level: 0.96 - vlt: 3 - SKU496: - cost: 342 - init_stock: 1180 - price: 342 - service_level: 0.96 - vlt: 1 - SKU497: - cost: 33 - init_stock: 180 - price: 33 - service_level: 0.96 - vlt: 3 - SKU498: - cost: 305 - init_stock: 1300 - price: 305 - service_level: 0.96 - vlt: 3 - SKU499: - cost: 307 - init_stock: 580 - price: 307 - service_level: 0.96 - vlt: 2 - SKU5: - cost: 466 - init_stock: 1420 - price: 466 - service_level: 0.96 - vlt: 2 - SKU50: - cost: 282 - init_stock: 740 - price: 282 - service_level: 0.96 - vlt: 3 - SKU500: - cost: 208 - init_stock: 840 - price: 208 - service_level: 0.96 - vlt: 3 - SKU501: - cost: 194 - init_stock: 1520 - price: 194 - service_level: 0.96 - vlt: 1 - SKU502: - cost: 69 - init_stock: 1300 - price: 69 - service_level: 0.96 - vlt: 2 - SKU503: - cost: 208 - init_stock: 1140 - price: 208 - service_level: 0.96 - vlt: 1 - SKU504: - cost: 251 - init_stock: 600 - price: 251 - service_level: 0.96 - vlt: 2 - SKU505: - cost: 426 - init_stock: 1180 - price: 426 - service_level: 0.96 - vlt: 1 - SKU506: - cost: 149 - init_stock: 1860 - price: 149 - service_level: 0.96 - vlt: 3 - SKU507: - cost: 187 - init_stock: 300 - price: 187 - service_level: 0.96 - vlt: 1 - SKU508: - cost: 272 - init_stock: 1920 - price: 272 - service_level: 0.96 - vlt: 1 - SKU509: - cost: 297 - init_stock: 1620 - price: 297 - service_level: 0.96 - vlt: 1 - SKU51: - cost: 295 - init_stock: 1340 - price: 295 - service_level: 0.96 - vlt: 3 - SKU510: - cost: 94 - init_stock: 180 - price: 94 - service_level: 0.96 - vlt: 1 - SKU511: - cost: 361 - init_stock: 1500 - price: 361 - service_level: 0.96 - vlt: 1 - SKU512: - cost: 467 - init_stock: 440 - price: 467 - service_level: 0.96 - vlt: 2 - SKU513: - cost: 343 - init_stock: 140 - price: 343 - service_level: 0.96 - vlt: 3 - SKU514: - cost: 186 - init_stock: 1260 - price: 186 - service_level: 0.96 - vlt: 3 - SKU515: - cost: 145 - init_stock: 1080 - price: 145 - service_level: 0.96 - vlt: 3 - SKU516: - cost: 64 - init_stock: 760 - price: 64 - service_level: 0.96 - vlt: 1 - SKU517: - cost: 117 - init_stock: 180 - price: 117 - service_level: 0.96 - vlt: 3 - SKU518: - cost: 26 - init_stock: 420 - price: 26 - service_level: 0.96 - vlt: 3 - SKU519: - cost: 233 - init_stock: 1000 - price: 233 - service_level: 0.96 - vlt: 2 - SKU52: - cost: 22 - init_stock: 1340 - price: 22 - service_level: 0.96 - vlt: 1 - SKU520: - cost: 209 - init_stock: 440 - price: 209 - service_level: 0.96 - vlt: 1 - SKU521: - cost: 220 - init_stock: 1000 - price: 220 - service_level: 0.96 - vlt: 3 - SKU522: - cost: 156 - init_stock: 1740 - price: 156 - service_level: 0.96 - vlt: 1 - SKU523: - cost: 65 - init_stock: 2000 - price: 65 - service_level: 0.96 - vlt: 3 - SKU524: - cost: 294 - init_stock: 1880 - price: 294 - service_level: 0.96 - vlt: 1 - SKU525: - cost: 432 - init_stock: 840 - price: 432 - service_level: 0.96 - vlt: 2 - SKU526: - cost: 419 - init_stock: 480 - price: 419 - service_level: 0.96 - vlt: 3 - SKU527: - cost: 131 - init_stock: 1400 - price: 131 - service_level: 0.96 - vlt: 1 - SKU528: - cost: 316 - init_stock: 420 - price: 316 - service_level: 0.96 - vlt: 3 - SKU529: - cost: 298 - init_stock: 620 - price: 298 - service_level: 0.96 - vlt: 3 - SKU53: - cost: 151 - init_stock: 1400 - price: 151 - service_level: 0.96 - vlt: 2 - SKU530: - cost: 131 - init_stock: 900 - price: 131 - service_level: 0.96 - vlt: 1 - SKU531: - cost: 103 - init_stock: 1200 - price: 103 - service_level: 0.96 - vlt: 3 - SKU532: - cost: 351 - init_stock: 1540 - price: 351 - service_level: 0.96 - vlt: 3 - SKU533: - cost: 296 - init_stock: 840 - price: 296 - service_level: 0.96 - vlt: 3 - SKU534: - cost: 425 - init_stock: 820 - price: 425 - service_level: 0.96 - vlt: 1 - SKU535: - cost: 304 - init_stock: 1720 - price: 304 - service_level: 0.96 - vlt: 2 - SKU536: - cost: 358 - init_stock: 1040 - price: 358 - service_level: 0.96 - vlt: 2 - SKU537: - cost: 321 - init_stock: 180 - price: 321 - service_level: 0.96 - vlt: 3 - SKU538: - cost: 457 - init_stock: 1000 - price: 457 - service_level: 0.96 - vlt: 1 - SKU539: - cost: 165 - init_stock: 1560 - price: 165 - service_level: 0.96 - vlt: 2 - SKU54: - cost: 158 - init_stock: 920 - price: 158 - service_level: 0.96 - vlt: 1 - SKU540: - cost: 388 - init_stock: 840 - price: 388 - service_level: 0.96 - vlt: 3 - SKU541: - cost: 131 - init_stock: 580 - price: 131 - service_level: 0.96 - vlt: 2 - SKU542: - cost: 38 - init_stock: 520 - price: 38 - service_level: 0.96 - vlt: 3 - SKU543: - cost: 430 - init_stock: 1700 - price: 430 - service_level: 0.96 - vlt: 2 - SKU544: - cost: 346 - init_stock: 1940 - price: 346 - service_level: 0.96 - vlt: 1 - SKU545: - cost: 175 - init_stock: 300 - price: 175 - service_level: 0.96 - vlt: 3 - SKU546: - cost: 491 - init_stock: 1920 - price: 491 - service_level: 0.96 - vlt: 3 - SKU547: - cost: 161 - init_stock: 880 - price: 161 - service_level: 0.96 - vlt: 2 - SKU548: - cost: 111 - init_stock: 120 - price: 111 - service_level: 0.96 - vlt: 1 - SKU549: - cost: 387 - init_stock: 1580 - price: 387 - service_level: 0.96 - vlt: 1 - SKU55: - cost: 482 - init_stock: 1300 - price: 482 - service_level: 0.96 - vlt: 3 - SKU550: - cost: 259 - init_stock: 1880 - price: 259 - service_level: 0.96 - vlt: 1 - SKU551: - cost: 46 - init_stock: 620 - price: 46 - service_level: 0.96 - vlt: 2 - SKU552: - cost: 191 - init_stock: 1800 - price: 191 - service_level: 0.96 - vlt: 3 - SKU553: - cost: 208 - init_stock: 600 - price: 208 - service_level: 0.96 - vlt: 1 - SKU554: - cost: 340 - init_stock: 820 - price: 340 - service_level: 0.96 - vlt: 1 - SKU555: - cost: 489 - init_stock: 1680 - price: 489 - service_level: 0.96 - vlt: 1 - SKU556: - cost: 110 - init_stock: 600 - price: 110 - service_level: 0.96 - vlt: 2 - SKU557: - cost: 334 - init_stock: 1460 - price: 334 - service_level: 0.96 - vlt: 2 - SKU558: - cost: 399 - init_stock: 340 - price: 399 - service_level: 0.96 - vlt: 1 - SKU559: - cost: 313 - init_stock: 1080 - price: 313 - service_level: 0.96 - vlt: 1 - SKU56: - cost: 377 - init_stock: 1480 - price: 377 - service_level: 0.96 - vlt: 1 - SKU560: - cost: 283 - init_stock: 820 - price: 283 - service_level: 0.96 - vlt: 1 - SKU561: - cost: 202 - init_stock: 780 - price: 202 - service_level: 0.96 - vlt: 3 - SKU562: - cost: 347 - init_stock: 1780 - price: 347 - service_level: 0.96 - vlt: 2 - SKU563: - cost: 401 - init_stock: 100 - price: 401 - service_level: 0.96 - vlt: 3 - SKU564: - cost: 109 - init_stock: 180 - price: 109 - service_level: 0.96 - vlt: 2 - SKU565: - cost: 57 - init_stock: 1500 - price: 57 - service_level: 0.96 - vlt: 3 - SKU566: - cost: 131 - init_stock: 220 - price: 131 - service_level: 0.96 - vlt: 2 - SKU567: - cost: 361 - init_stock: 180 - price: 361 - service_level: 0.96 - vlt: 1 - SKU568: - cost: 75 - init_stock: 1560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU569: - cost: 473 - init_stock: 960 - price: 473 - service_level: 0.96 - vlt: 2 - SKU57: - cost: 359 - init_stock: 1000 - price: 359 - service_level: 0.96 - vlt: 2 - SKU570: - cost: 388 - init_stock: 1660 - price: 388 - service_level: 0.96 - vlt: 2 - SKU571: - cost: 168 - init_stock: 780 - price: 168 - service_level: 0.96 - vlt: 1 - SKU572: - cost: 26 - init_stock: 900 - price: 26 - service_level: 0.96 - vlt: 3 - SKU573: - cost: 246 - init_stock: 820 - price: 246 - service_level: 0.96 - vlt: 2 - SKU574: - cost: 94 - init_stock: 1000 - price: 94 - service_level: 0.96 - vlt: 2 - SKU575: - cost: 237 - init_stock: 1580 - price: 237 - service_level: 0.96 - vlt: 1 - SKU576: - cost: 265 - init_stock: 1560 - price: 265 - service_level: 0.96 - vlt: 3 - SKU577: - cost: 18 - init_stock: 1740 - price: 18 - service_level: 0.96 - vlt: 3 - SKU578: - cost: 100 - init_stock: 1420 - price: 100 - service_level: 0.96 - vlt: 2 - SKU579: - cost: 415 - init_stock: 180 - price: 415 - service_level: 0.96 - vlt: 1 - SKU58: - cost: 362 - init_stock: 960 - price: 362 - service_level: 0.96 - vlt: 2 - SKU580: - cost: 203 - init_stock: 100 - price: 203 - service_level: 0.96 - vlt: 1 - SKU581: - cost: 152 - init_stock: 1720 - price: 152 - service_level: 0.96 - vlt: 2 - SKU582: - cost: 359 - init_stock: 1920 - price: 359 - service_level: 0.96 - vlt: 2 - SKU583: - cost: 86 - init_stock: 1740 - price: 86 - service_level: 0.96 - vlt: 1 - SKU584: - cost: 33 - init_stock: 900 - price: 33 - service_level: 0.96 - vlt: 2 - SKU585: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 2 - SKU586: - cost: 61 - init_stock: 880 - price: 61 - service_level: 0.96 - vlt: 2 - SKU587: - cost: 290 - init_stock: 1560 - price: 290 - service_level: 0.96 - vlt: 2 - SKU588: - cost: 476 - init_stock: 880 - price: 476 - service_level: 0.96 - vlt: 2 - SKU589: - cost: 244 - init_stock: 680 - price: 244 - service_level: 0.96 - vlt: 3 - SKU59: - cost: 145 - init_stock: 1020 - price: 145 - service_level: 0.96 - vlt: 1 - SKU590: - cost: 70 - init_stock: 620 - price: 70 - service_level: 0.96 - vlt: 2 - SKU591: - cost: 206 - init_stock: 1680 - price: 206 - service_level: 0.96 - vlt: 2 - SKU592: - cost: 218 - init_stock: 920 - price: 218 - service_level: 0.96 - vlt: 1 - SKU593: - cost: 124 - init_stock: 880 - price: 124 - service_level: 0.96 - vlt: 1 - SKU594: - cost: 238 - init_stock: 1000 - price: 238 - service_level: 0.96 - vlt: 1 - SKU595: - cost: 73 - init_stock: 860 - price: 73 - service_level: 0.96 - vlt: 3 - SKU596: - cost: 432 - init_stock: 140 - price: 432 - service_level: 0.96 - vlt: 2 - SKU597: - cost: 252 - init_stock: 1740 - price: 252 - service_level: 0.96 - vlt: 2 - SKU598: - cost: 348 - init_stock: 280 - price: 348 - service_level: 0.96 - vlt: 1 - SKU599: - cost: 54 - init_stock: 1360 - price: 54 - service_level: 0.96 - vlt: 2 - SKU6: - cost: 122 - init_stock: 1760 - price: 122 - service_level: 0.96 - vlt: 2 - SKU60: - cost: 200 - init_stock: 780 - price: 200 - service_level: 0.96 - vlt: 1 - SKU600: - cost: 386 - init_stock: 1300 - price: 386 - service_level: 0.96 - vlt: 1 - SKU601: - cost: 138 - init_stock: 780 - price: 138 - service_level: 0.96 - vlt: 3 - SKU602: - cost: 184 - init_stock: 1960 - price: 184 - service_level: 0.96 - vlt: 1 - SKU603: - cost: 278 - init_stock: 840 - price: 278 - service_level: 0.96 - vlt: 3 - SKU604: - cost: 270 - init_stock: 1000 - price: 270 - service_level: 0.96 - vlt: 3 - SKU605: - cost: 288 - init_stock: 480 - price: 288 - service_level: 0.96 - vlt: 2 - SKU606: - cost: 114 - init_stock: 220 - price: 114 - service_level: 0.96 - vlt: 3 - SKU607: - cost: 208 - init_stock: 1580 - price: 208 - service_level: 0.96 - vlt: 2 - SKU608: - cost: 39 - init_stock: 1460 - price: 39 - service_level: 0.96 - vlt: 1 - SKU609: - cost: 102 - init_stock: 380 - price: 102 - service_level: 0.96 - vlt: 2 - SKU61: - cost: 461 - init_stock: 1500 - price: 461 - service_level: 0.96 - vlt: 1 - SKU610: - cost: 449 - init_stock: 1360 - price: 449 - service_level: 0.96 - vlt: 1 - SKU611: - cost: 306 - init_stock: 1540 - price: 306 - service_level: 0.96 - vlt: 3 - SKU612: - cost: 391 - init_stock: 1760 - price: 391 - service_level: 0.96 - vlt: 2 - SKU613: - cost: 174 - init_stock: 140 - price: 174 - service_level: 0.96 - vlt: 3 - SKU614: - cost: 173 - init_stock: 300 - price: 173 - service_level: 0.96 - vlt: 1 - SKU615: - cost: 330 - init_stock: 1820 - price: 330 - service_level: 0.96 - vlt: 2 - SKU616: - cost: 232 - init_stock: 1460 - price: 232 - service_level: 0.96 - vlt: 1 - SKU617: - cost: 203 - init_stock: 1200 - price: 203 - service_level: 0.96 - vlt: 3 - SKU618: - cost: 77 - init_stock: 460 - price: 77 - service_level: 0.96 - vlt: 1 - SKU619: - cost: 189 - init_stock: 1720 - price: 189 - service_level: 0.96 - vlt: 2 - SKU62: - cost: 124 - init_stock: 180 - price: 124 - service_level: 0.96 - vlt: 2 - SKU620: - cost: 231 - init_stock: 780 - price: 231 - service_level: 0.96 - vlt: 3 - SKU621: - cost: 199 - init_stock: 1440 - price: 199 - service_level: 0.96 - vlt: 2 - SKU622: - cost: 253 - init_stock: 1300 - price: 253 - service_level: 0.96 - vlt: 3 - SKU623: - cost: 335 - init_stock: 1100 - price: 335 - service_level: 0.96 - vlt: 1 - SKU624: - cost: 482 - init_stock: 1080 - price: 482 - service_level: 0.96 - vlt: 2 - SKU625: - cost: 46 - init_stock: 1780 - price: 46 - service_level: 0.96 - vlt: 1 - SKU626: - cost: 451 - init_stock: 1780 - price: 451 - service_level: 0.96 - vlt: 3 - SKU627: - cost: 220 - init_stock: 1640 - price: 220 - service_level: 0.96 - vlt: 3 - SKU628: - cost: 124 - init_stock: 520 - price: 124 - service_level: 0.96 - vlt: 2 - SKU629: - cost: 353 - init_stock: 1840 - price: 353 - service_level: 0.96 - vlt: 1 - SKU63: - cost: 68 - init_stock: 280 - price: 68 - service_level: 0.96 - vlt: 3 - SKU630: - cost: 122 - init_stock: 340 - price: 122 - service_level: 0.96 - vlt: 1 - SKU631: - cost: 296 - init_stock: 980 - price: 296 - service_level: 0.96 - vlt: 2 - SKU632: - cost: 85 - init_stock: 1880 - price: 85 - service_level: 0.96 - vlt: 2 - SKU633: - cost: 184 - init_stock: 1340 - price: 184 - service_level: 0.96 - vlt: 2 - SKU634: - cost: 17 - init_stock: 1460 - price: 17 - service_level: 0.96 - vlt: 1 - SKU635: - cost: 341 - init_stock: 1860 - price: 341 - service_level: 0.96 - vlt: 1 - SKU636: - cost: 385 - init_stock: 740 - price: 385 - service_level: 0.96 - vlt: 1 - SKU637: - cost: 83 - init_stock: 1220 - price: 83 - service_level: 0.96 - vlt: 3 - SKU638: - cost: 375 - init_stock: 160 - price: 375 - service_level: 0.96 - vlt: 1 - SKU639: - cost: 327 - init_stock: 800 - price: 327 - service_level: 0.96 - vlt: 2 - SKU64: - cost: 36 - init_stock: 380 - price: 36 - service_level: 0.96 - vlt: 3 - SKU640: - cost: 275 - init_stock: 1820 - price: 275 - service_level: 0.96 - vlt: 1 - SKU641: - cost: 365 - init_stock: 1620 - price: 365 - service_level: 0.96 - vlt: 1 - SKU642: - cost: 414 - init_stock: 500 - price: 414 - service_level: 0.96 - vlt: 2 - SKU643: - cost: 358 - init_stock: 920 - price: 358 - service_level: 0.96 - vlt: 2 - SKU644: - cost: 46 - init_stock: 1640 - price: 46 - service_level: 0.96 - vlt: 2 - SKU645: - cost: 467 - init_stock: 1980 - price: 467 - service_level: 0.96 - vlt: 3 - SKU646: - cost: 189 - init_stock: 260 - price: 189 - service_level: 0.96 - vlt: 3 - SKU647: - cost: 303 - init_stock: 1540 - price: 303 - service_level: 0.96 - vlt: 3 - SKU648: - cost: 226 - init_stock: 1100 - price: 226 - service_level: 0.96 - vlt: 1 - SKU649: - cost: 198 - init_stock: 500 - price: 198 - service_level: 0.96 - vlt: 3 - SKU65: - cost: 15 - init_stock: 1880 - price: 15 - service_level: 0.96 - vlt: 2 - SKU650: - cost: 351 - init_stock: 1120 - price: 351 - service_level: 0.96 - vlt: 2 - SKU651: - cost: 380 - init_stock: 1920 - price: 380 - service_level: 0.96 - vlt: 3 - SKU652: - cost: 123 - init_stock: 800 - price: 123 - service_level: 0.96 - vlt: 2 - SKU653: - cost: 479 - init_stock: 1920 - price: 479 - service_level: 0.96 - vlt: 2 - SKU654: - cost: 66 - init_stock: 160 - price: 66 - service_level: 0.96 - vlt: 3 - SKU655: - cost: 333 - init_stock: 840 - price: 333 - service_level: 0.96 - vlt: 1 - SKU656: - cost: 53 - init_stock: 1440 - price: 53 - service_level: 0.96 - vlt: 2 - SKU657: - cost: 73 - init_stock: 1620 - price: 73 - service_level: 0.96 - vlt: 2 - SKU658: - cost: 70 - init_stock: 720 - price: 70 - service_level: 0.96 - vlt: 3 - SKU659: - cost: 21 - init_stock: 1120 - price: 21 - service_level: 0.96 - vlt: 1 - SKU66: - cost: 143 - init_stock: 1800 - price: 143 - service_level: 0.96 - vlt: 1 - SKU660: - cost: 487 - init_stock: 1660 - price: 487 - service_level: 0.96 - vlt: 1 - SKU661: - cost: 344 - init_stock: 1480 - price: 344 - service_level: 0.96 - vlt: 2 - SKU662: - cost: 372 - init_stock: 760 - price: 372 - service_level: 0.96 - vlt: 3 - SKU663: - cost: 418 - init_stock: 800 - price: 418 - service_level: 0.96 - vlt: 3 - SKU664: - cost: 351 - init_stock: 1680 - price: 351 - service_level: 0.96 - vlt: 1 - SKU665: - cost: 493 - init_stock: 860 - price: 493 - service_level: 0.96 - vlt: 3 - SKU666: - cost: 341 - init_stock: 1760 - price: 341 - service_level: 0.96 - vlt: 1 - SKU667: - cost: 325 - init_stock: 260 - price: 325 - service_level: 0.96 - vlt: 3 - SKU668: - cost: 286 - init_stock: 1200 - price: 286 - service_level: 0.96 - vlt: 3 - SKU669: - cost: 74 - init_stock: 320 - price: 74 - service_level: 0.96 - vlt: 1 - SKU67: - cost: 247 - init_stock: 1060 - price: 247 - service_level: 0.96 - vlt: 1 - SKU670: - cost: 289 - init_stock: 1720 - price: 289 - service_level: 0.96 - vlt: 1 - SKU671: - cost: 267 - init_stock: 1660 - price: 267 - service_level: 0.96 - vlt: 3 - SKU672: - cost: 369 - init_stock: 260 - price: 369 - service_level: 0.96 - vlt: 2 - SKU673: - cost: 322 - init_stock: 820 - price: 322 - service_level: 0.96 - vlt: 2 - SKU674: - cost: 223 - init_stock: 440 - price: 223 - service_level: 0.96 - vlt: 1 - SKU675: - cost: 438 - init_stock: 660 - price: 438 - service_level: 0.96 - vlt: 1 - SKU676: - cost: 244 - init_stock: 1220 - price: 244 - service_level: 0.96 - vlt: 1 - SKU677: - cost: 425 - init_stock: 880 - price: 425 - service_level: 0.96 - vlt: 2 - SKU678: - cost: 168 - init_stock: 720 - price: 168 - service_level: 0.96 - vlt: 1 - SKU679: - cost: 395 - init_stock: 880 - price: 395 - service_level: 0.96 - vlt: 2 - SKU68: - cost: 169 - init_stock: 280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU680: - cost: 260 - init_stock: 600 - price: 260 - service_level: 0.96 - vlt: 1 - SKU681: - cost: 464 - init_stock: 1940 - price: 464 - service_level: 0.96 - vlt: 3 - SKU682: - cost: 370 - init_stock: 1060 - price: 370 - service_level: 0.96 - vlt: 2 - SKU683: - cost: 327 - init_stock: 1340 - price: 327 - service_level: 0.96 - vlt: 3 - SKU684: - cost: 355 - init_stock: 1580 - price: 355 - service_level: 0.96 - vlt: 1 - SKU685: - cost: 422 - init_stock: 900 - price: 422 - service_level: 0.96 - vlt: 2 - SKU686: - cost: 63 - init_stock: 1260 - price: 63 - service_level: 0.96 - vlt: 1 - SKU687: - cost: 34 - init_stock: 1520 - price: 34 - service_level: 0.96 - vlt: 2 - SKU688: - cost: 484 - init_stock: 1540 - price: 484 - service_level: 0.96 - vlt: 3 - SKU689: - cost: 499 - init_stock: 300 - price: 499 - service_level: 0.96 - vlt: 1 - SKU69: - cost: 176 - init_stock: 1340 - price: 176 - service_level: 0.96 - vlt: 1 - SKU690: - cost: 437 - init_stock: 1660 - price: 437 - service_level: 0.96 - vlt: 2 - SKU691: - cost: 17 - init_stock: 1580 - price: 17 - service_level: 0.96 - vlt: 3 - SKU692: - cost: 225 - init_stock: 1300 - price: 225 - service_level: 0.96 - vlt: 1 - SKU693: - cost: 19 - init_stock: 1220 - price: 19 - service_level: 0.96 - vlt: 2 - SKU694: - cost: 208 - init_stock: 1500 - price: 208 - service_level: 0.96 - vlt: 3 - SKU695: - cost: 190 - init_stock: 140 - price: 190 - service_level: 0.96 - vlt: 2 - SKU696: - cost: 348 - init_stock: 1160 - price: 348 - service_level: 0.96 - vlt: 1 - SKU697: - cost: 455 - init_stock: 1600 - price: 455 - service_level: 0.96 - vlt: 1 - SKU698: - cost: 198 - init_stock: 1220 - price: 198 - service_level: 0.96 - vlt: 3 - SKU699: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 3 - SKU7: - cost: 101 - init_stock: 1920 - price: 101 - service_level: 0.96 - vlt: 2 - SKU70: - cost: 186 - init_stock: 700 - price: 186 - service_level: 0.96 - vlt: 1 - SKU700: - cost: 74 - init_stock: 180 - price: 74 - service_level: 0.96 - vlt: 2 - SKU701: - cost: 399 - init_stock: 1540 - price: 399 - service_level: 0.96 - vlt: 1 - SKU702: - cost: 82 - init_stock: 680 - price: 82 - service_level: 0.96 - vlt: 1 - SKU703: - cost: 363 - init_stock: 760 - price: 363 - service_level: 0.96 - vlt: 1 - SKU704: - cost: 384 - init_stock: 1740 - price: 384 - service_level: 0.96 - vlt: 2 - SKU705: - cost: 128 - init_stock: 1260 - price: 128 - service_level: 0.96 - vlt: 2 - SKU706: - cost: 182 - init_stock: 1820 - price: 182 - service_level: 0.96 - vlt: 2 - SKU707: - cost: 18 - init_stock: 1380 - price: 18 - service_level: 0.96 - vlt: 1 - SKU708: - cost: 178 - init_stock: 560 - price: 178 - service_level: 0.96 - vlt: 3 - SKU709: - cost: 102 - init_stock: 1060 - price: 102 - service_level: 0.96 - vlt: 3 - SKU71: - cost: 315 - init_stock: 900 - price: 315 - service_level: 0.96 - vlt: 2 - SKU710: - cost: 252 - init_stock: 380 - price: 252 - service_level: 0.96 - vlt: 2 - SKU711: - cost: 281 - init_stock: 1380 - price: 281 - service_level: 0.96 - vlt: 1 - SKU712: - cost: 232 - init_stock: 220 - price: 232 - service_level: 0.96 - vlt: 2 - SKU713: - cost: 285 - init_stock: 860 - price: 285 - service_level: 0.96 - vlt: 2 - SKU714: - cost: 79 - init_stock: 820 - price: 79 - service_level: 0.96 - vlt: 3 - SKU715: - cost: 234 - init_stock: 340 - price: 234 - service_level: 0.96 - vlt: 1 - SKU716: - cost: 70 - init_stock: 1500 - price: 70 - service_level: 0.96 - vlt: 2 - SKU717: - cost: 345 - init_stock: 160 - price: 345 - service_level: 0.96 - vlt: 2 - SKU718: - cost: 357 - init_stock: 1160 - price: 357 - service_level: 0.96 - vlt: 2 - SKU719: - cost: 340 - init_stock: 1300 - price: 340 - service_level: 0.96 - vlt: 3 - SKU72: - cost: 458 - init_stock: 1700 - price: 458 - service_level: 0.96 - vlt: 2 - SKU720: - cost: 325 - init_stock: 840 - price: 325 - service_level: 0.96 - vlt: 3 - SKU721: - cost: 73 - init_stock: 220 - price: 73 - service_level: 0.96 - vlt: 2 - SKU722: - cost: 392 - init_stock: 1940 - price: 392 - service_level: 0.96 - vlt: 1 - SKU723: - cost: 318 - init_stock: 1780 - price: 318 - service_level: 0.96 - vlt: 3 - SKU724: - cost: 400 - init_stock: 660 - price: 400 - service_level: 0.96 - vlt: 1 - SKU725: - cost: 175 - init_stock: 740 - price: 175 - service_level: 0.96 - vlt: 1 - SKU726: - cost: 458 - init_stock: 1780 - price: 458 - service_level: 0.96 - vlt: 3 - SKU727: - cost: 418 - init_stock: 1140 - price: 418 - service_level: 0.96 - vlt: 1 - SKU728: - cost: 475 - init_stock: 740 - price: 475 - service_level: 0.96 - vlt: 3 - SKU729: - cost: 324 - init_stock: 720 - price: 324 - service_level: 0.96 - vlt: 3 - SKU73: - cost: 212 - init_stock: 1080 - price: 212 - service_level: 0.96 - vlt: 2 - SKU730: - cost: 16 - init_stock: 260 - price: 16 - service_level: 0.96 - vlt: 2 - SKU731: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU732: - cost: 41 - init_stock: 1240 - price: 41 - service_level: 0.96 - vlt: 3 - SKU733: - cost: 315 - init_stock: 520 - price: 315 - service_level: 0.96 - vlt: 3 - SKU734: - cost: 37 - init_stock: 1020 - price: 37 - service_level: 0.96 - vlt: 3 - SKU735: - cost: 266 - init_stock: 1980 - price: 266 - service_level: 0.96 - vlt: 3 - SKU736: - cost: 368 - init_stock: 500 - price: 368 - service_level: 0.96 - vlt: 2 - SKU737: - cost: 475 - init_stock: 620 - price: 475 - service_level: 0.96 - vlt: 2 - SKU738: - cost: 185 - init_stock: 960 - price: 185 - service_level: 0.96 - vlt: 3 - SKU739: - cost: 475 - init_stock: 1480 - price: 475 - service_level: 0.96 - vlt: 2 - SKU74: - cost: 159 - init_stock: 840 - price: 159 - service_level: 0.96 - vlt: 1 - SKU740: - cost: 390 - init_stock: 1280 - price: 390 - service_level: 0.96 - vlt: 1 - SKU741: - cost: 91 - init_stock: 1120 - price: 91 - service_level: 0.96 - vlt: 1 - SKU742: - cost: 188 - init_stock: 440 - price: 188 - service_level: 0.96 - vlt: 1 - SKU743: - cost: 217 - init_stock: 860 - price: 217 - service_level: 0.96 - vlt: 3 - SKU744: - cost: 379 - init_stock: 1160 - price: 379 - service_level: 0.96 - vlt: 1 - SKU745: - cost: 316 - init_stock: 1840 - price: 316 - service_level: 0.96 - vlt: 2 - SKU746: - cost: 437 - init_stock: 800 - price: 437 - service_level: 0.96 - vlt: 2 - SKU747: - cost: 373 - init_stock: 1580 - price: 373 - service_level: 0.96 - vlt: 2 - SKU748: - cost: 275 - init_stock: 1320 - price: 275 - service_level: 0.96 - vlt: 2 - SKU749: - cost: 394 - init_stock: 1060 - price: 394 - service_level: 0.96 - vlt: 1 - SKU75: - cost: 131 - init_stock: 380 - price: 131 - service_level: 0.96 - vlt: 1 - SKU750: - cost: 256 - init_stock: 1240 - price: 256 - service_level: 0.96 - vlt: 3 - SKU751: - cost: 369 - init_stock: 1300 - price: 369 - service_level: 0.96 - vlt: 3 - SKU752: - cost: 259 - init_stock: 1560 - price: 259 - service_level: 0.96 - vlt: 3 - SKU753: - cost: 77 - init_stock: 1300 - price: 77 - service_level: 0.96 - vlt: 3 - SKU754: - cost: 387 - init_stock: 260 - price: 387 - service_level: 0.96 - vlt: 2 - SKU755: - cost: 354 - init_stock: 1600 - price: 354 - service_level: 0.96 - vlt: 3 - SKU756: - cost: 246 - init_stock: 1800 - price: 246 - service_level: 0.96 - vlt: 1 - SKU757: - cost: 40 - init_stock: 1140 - price: 40 - service_level: 0.96 - vlt: 3 - SKU758: - cost: 241 - init_stock: 800 - price: 241 - service_level: 0.96 - vlt: 2 - SKU759: - cost: 435 - init_stock: 760 - price: 435 - service_level: 0.96 - vlt: 1 - SKU76: - cost: 147 - init_stock: 1280 - price: 147 - service_level: 0.96 - vlt: 2 - SKU760: - cost: 176 - init_stock: 1020 - price: 176 - service_level: 0.96 - vlt: 3 - SKU761: - cost: 224 - init_stock: 1100 - price: 224 - service_level: 0.96 - vlt: 1 - SKU762: - cost: 264 - init_stock: 1020 - price: 264 - service_level: 0.96 - vlt: 1 - SKU763: - cost: 385 - init_stock: 1580 - price: 385 - service_level: 0.96 - vlt: 2 - SKU764: - cost: 349 - init_stock: 680 - price: 349 - service_level: 0.96 - vlt: 1 - SKU765: - cost: 345 - init_stock: 260 - price: 345 - service_level: 0.96 - vlt: 1 - SKU766: - cost: 478 - init_stock: 400 - price: 478 - service_level: 0.96 - vlt: 2 - SKU767: - cost: 95 - init_stock: 1520 - price: 95 - service_level: 0.96 - vlt: 1 - SKU768: - cost: 181 - init_stock: 1840 - price: 181 - service_level: 0.96 - vlt: 2 - SKU769: - cost: 24 - init_stock: 580 - price: 24 - service_level: 0.96 - vlt: 2 - SKU77: - cost: 409 - init_stock: 1440 - price: 409 - service_level: 0.96 - vlt: 1 - SKU770: - cost: 150 - init_stock: 1240 - price: 150 - service_level: 0.96 - vlt: 2 - SKU771: - cost: 101 - init_stock: 140 - price: 101 - service_level: 0.96 - vlt: 1 - SKU772: - cost: 256 - init_stock: 120 - price: 256 - service_level: 0.96 - vlt: 3 - SKU773: - cost: 84 - init_stock: 1840 - price: 84 - service_level: 0.96 - vlt: 1 - SKU774: - cost: 447 - init_stock: 1180 - price: 447 - service_level: 0.96 - vlt: 1 - SKU775: - cost: 175 - init_stock: 1720 - price: 175 - service_level: 0.96 - vlt: 3 - SKU776: - cost: 103 - init_stock: 1700 - price: 103 - service_level: 0.96 - vlt: 2 - SKU777: - cost: 292 - init_stock: 1140 - price: 292 - service_level: 0.96 - vlt: 2 - SKU778: - cost: 203 - init_stock: 160 - price: 203 - service_level: 0.96 - vlt: 3 - SKU779: - cost: 117 - init_stock: 860 - price: 117 - service_level: 0.96 - vlt: 1 - SKU78: - cost: 234 - init_stock: 260 - price: 234 - service_level: 0.96 - vlt: 3 - SKU780: - cost: 69 - init_stock: 1640 - price: 69 - service_level: 0.96 - vlt: 3 - SKU781: - cost: 372 - init_stock: 720 - price: 372 - service_level: 0.96 - vlt: 3 - SKU782: - cost: 27 - init_stock: 200 - price: 27 - service_level: 0.96 - vlt: 3 - SKU783: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 2 - SKU784: - cost: 309 - init_stock: 1040 - price: 309 - service_level: 0.96 - vlt: 3 - SKU785: - cost: 191 - init_stock: 1400 - price: 191 - service_level: 0.96 - vlt: 2 - SKU786: - cost: 91 - init_stock: 440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU787: - cost: 360 - init_stock: 1220 - price: 360 - service_level: 0.96 - vlt: 1 - SKU788: - cost: 351 - init_stock: 560 - price: 351 - service_level: 0.96 - vlt: 1 - SKU789: - cost: 153 - init_stock: 1160 - price: 153 - service_level: 0.96 - vlt: 2 - SKU79: - cost: 245 - init_stock: 220 - price: 245 - service_level: 0.96 - vlt: 1 - SKU790: - cost: 417 - init_stock: 1320 - price: 417 - service_level: 0.96 - vlt: 2 - SKU791: - cost: 134 - init_stock: 560 - price: 134 - service_level: 0.96 - vlt: 1 - SKU792: - cost: 313 - init_stock: 420 - price: 313 - service_level: 0.96 - vlt: 2 - SKU793: - cost: 195 - init_stock: 1520 - price: 195 - service_level: 0.96 - vlt: 2 - SKU794: - cost: 325 - init_stock: 1600 - price: 325 - service_level: 0.96 - vlt: 3 - SKU795: - cost: 276 - init_stock: 960 - price: 276 - service_level: 0.96 - vlt: 3 - SKU796: - cost: 447 - init_stock: 980 - price: 447 - service_level: 0.96 - vlt: 1 - SKU797: - cost: 100 - init_stock: 780 - price: 100 - service_level: 0.96 - vlt: 3 - SKU798: - cost: 426 - init_stock: 600 - price: 426 - service_level: 0.96 - vlt: 2 - SKU799: - cost: 63 - init_stock: 140 - price: 63 - service_level: 0.96 - vlt: 2 - SKU8: - cost: 125 - init_stock: 1260 - price: 125 - service_level: 0.96 - vlt: 3 - SKU80: - cost: 163 - init_stock: 1400 - price: 163 - service_level: 0.96 - vlt: 1 - SKU800: - cost: 298 - init_stock: 1880 - price: 298 - service_level: 0.96 - vlt: 2 - SKU801: - cost: 389 - init_stock: 720 - price: 389 - service_level: 0.96 - vlt: 2 - SKU802: - cost: 173 - init_stock: 1240 - price: 173 - service_level: 0.96 - vlt: 2 - SKU803: - cost: 272 - init_stock: 380 - price: 272 - service_level: 0.96 - vlt: 3 - SKU804: - cost: 390 - init_stock: 940 - price: 390 - service_level: 0.96 - vlt: 3 - SKU805: - cost: 61 - init_stock: 660 - price: 61 - service_level: 0.96 - vlt: 3 - SKU806: - cost: 158 - init_stock: 1040 - price: 158 - service_level: 0.96 - vlt: 1 - SKU807: - cost: 453 - init_stock: 520 - price: 453 - service_level: 0.96 - vlt: 3 - SKU808: - cost: 249 - init_stock: 1820 - price: 249 - service_level: 0.96 - vlt: 1 - SKU809: - cost: 55 - init_stock: 1300 - price: 55 - service_level: 0.96 - vlt: 1 - SKU81: - cost: 182 - init_stock: 1320 - price: 182 - service_level: 0.96 - vlt: 3 - SKU810: - cost: 276 - init_stock: 120 - price: 276 - service_level: 0.96 - vlt: 2 - SKU811: - cost: 254 - init_stock: 740 - price: 254 - service_level: 0.96 - vlt: 3 - SKU812: - cost: 252 - init_stock: 1600 - price: 252 - service_level: 0.96 - vlt: 1 - SKU813: - cost: 253 - init_stock: 140 - price: 253 - service_level: 0.96 - vlt: 3 - SKU814: - cost: 485 - init_stock: 1940 - price: 485 - service_level: 0.96 - vlt: 1 - SKU815: - cost: 390 - init_stock: 480 - price: 390 - service_level: 0.96 - vlt: 2 - SKU816: - cost: 152 - init_stock: 1820 - price: 152 - service_level: 0.96 - vlt: 3 - SKU817: - cost: 227 - init_stock: 100 - price: 227 - service_level: 0.96 - vlt: 2 - SKU818: - cost: 354 - init_stock: 860 - price: 354 - service_level: 0.96 - vlt: 2 - SKU819: - cost: 302 - init_stock: 540 - price: 302 - service_level: 0.96 - vlt: 1 - SKU82: - cost: 290 - init_stock: 560 - price: 290 - service_level: 0.96 - vlt: 1 - SKU820: - cost: 264 - init_stock: 1640 - price: 264 - service_level: 0.96 - vlt: 3 - SKU821: - cost: 99 - init_stock: 1380 - price: 99 - service_level: 0.96 - vlt: 1 - SKU822: - cost: 136 - init_stock: 1400 - price: 136 - service_level: 0.96 - vlt: 3 - SKU823: - cost: 75 - init_stock: 560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU824: - cost: 170 - init_stock: 1400 - price: 170 - service_level: 0.96 - vlt: 1 - SKU825: - cost: 214 - init_stock: 300 - price: 214 - service_level: 0.96 - vlt: 1 - SKU826: - cost: 386 - init_stock: 1100 - price: 386 - service_level: 0.96 - vlt: 2 - SKU827: - cost: 148 - init_stock: 1280 - price: 148 - service_level: 0.96 - vlt: 2 - SKU828: - cost: 400 - init_stock: 1380 - price: 400 - service_level: 0.96 - vlt: 1 - SKU829: - cost: 61 - init_stock: 1480 - price: 61 - service_level: 0.96 - vlt: 1 - SKU83: - cost: 296 - init_stock: 340 - price: 296 - service_level: 0.96 - vlt: 2 - SKU830: - cost: 167 - init_stock: 480 - price: 167 - service_level: 0.96 - vlt: 3 - SKU831: - cost: 262 - init_stock: 580 - price: 262 - service_level: 0.96 - vlt: 1 - SKU832: - cost: 33 - init_stock: 1640 - price: 33 - service_level: 0.96 - vlt: 1 - SKU833: - cost: 400 - init_stock: 1960 - price: 400 - service_level: 0.96 - vlt: 3 - SKU834: - cost: 422 - init_stock: 100 - price: 422 - service_level: 0.96 - vlt: 1 - SKU835: - cost: 440 - init_stock: 1040 - price: 440 - service_level: 0.96 - vlt: 2 - SKU836: - cost: 323 - init_stock: 1920 - price: 323 - service_level: 0.96 - vlt: 1 - SKU837: - cost: 373 - init_stock: 520 - price: 373 - service_level: 0.96 - vlt: 3 - SKU838: - cost: 456 - init_stock: 1540 - price: 456 - service_level: 0.96 - vlt: 1 - SKU839: - cost: 473 - init_stock: 1200 - price: 473 - service_level: 0.96 - vlt: 1 - SKU84: - cost: 318 - init_stock: 840 - price: 318 - service_level: 0.96 - vlt: 3 - SKU840: - cost: 266 - init_stock: 1000 - price: 266 - service_level: 0.96 - vlt: 1 - SKU841: - cost: 285 - init_stock: 1700 - price: 285 - service_level: 0.96 - vlt: 3 - SKU842: - cost: 41 - init_stock: 1680 - price: 41 - service_level: 0.96 - vlt: 1 - SKU843: - cost: 360 - init_stock: 1440 - price: 360 - service_level: 0.96 - vlt: 2 - SKU844: - cost: 51 - init_stock: 1580 - price: 51 - service_level: 0.96 - vlt: 3 - SKU845: - cost: 288 - init_stock: 440 - price: 288 - service_level: 0.96 - vlt: 3 - SKU846: - cost: 485 - init_stock: 780 - price: 485 - service_level: 0.96 - vlt: 1 - SKU847: - cost: 388 - init_stock: 1820 - price: 388 - service_level: 0.96 - vlt: 1 - SKU848: - cost: 306 - init_stock: 920 - price: 306 - service_level: 0.96 - vlt: 2 - SKU849: - cost: 320 - init_stock: 800 - price: 320 - service_level: 0.96 - vlt: 2 - SKU85: - cost: 442 - init_stock: 2000 - price: 442 - service_level: 0.96 - vlt: 3 - SKU850: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 2 - SKU851: - cost: 53 - init_stock: 1280 - price: 53 - service_level: 0.96 - vlt: 1 - SKU852: - cost: 306 - init_stock: 1080 - price: 306 - service_level: 0.96 - vlt: 2 - SKU853: - cost: 386 - init_stock: 1120 - price: 386 - service_level: 0.96 - vlt: 3 - SKU854: - cost: 212 - init_stock: 1300 - price: 212 - service_level: 0.96 - vlt: 3 - SKU855: - cost: 76 - init_stock: 1440 - price: 76 - service_level: 0.96 - vlt: 3 - SKU856: - cost: 380 - init_stock: 640 - price: 380 - service_level: 0.96 - vlt: 2 - SKU857: - cost: 462 - init_stock: 2000 - price: 462 - service_level: 0.96 - vlt: 1 - SKU858: - cost: 80 - init_stock: 480 - price: 80 - service_level: 0.96 - vlt: 3 - SKU859: - cost: 215 - init_stock: 560 - price: 215 - service_level: 0.96 - vlt: 3 - SKU86: - cost: 386 - init_stock: 1700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU860: - cost: 313 - init_stock: 300 - price: 313 - service_level: 0.96 - vlt: 2 - SKU861: - cost: 477 - init_stock: 260 - price: 477 - service_level: 0.96 - vlt: 2 - SKU862: - cost: 240 - init_stock: 320 - price: 240 - service_level: 0.96 - vlt: 2 - SKU863: - cost: 470 - init_stock: 1860 - price: 470 - service_level: 0.96 - vlt: 2 - SKU864: - cost: 203 - init_stock: 380 - price: 203 - service_level: 0.96 - vlt: 1 - SKU865: - cost: 144 - init_stock: 380 - price: 144 - service_level: 0.96 - vlt: 3 - SKU866: - cost: 172 - init_stock: 1760 - price: 172 - service_level: 0.96 - vlt: 1 - SKU867: - cost: 499 - init_stock: 2000 - price: 499 - service_level: 0.96 - vlt: 2 - SKU868: - cost: 41 - init_stock: 1000 - price: 41 - service_level: 0.96 - vlt: 2 - SKU869: - cost: 97 - init_stock: 1940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU87: - cost: 368 - init_stock: 1380 - price: 368 - service_level: 0.96 - vlt: 2 - SKU870: - cost: 281 - init_stock: 1340 - price: 281 - service_level: 0.96 - vlt: 1 - SKU871: - cost: 101 - init_stock: 1980 - price: 101 - service_level: 0.96 - vlt: 1 - SKU872: - cost: 133 - init_stock: 640 - price: 133 - service_level: 0.96 - vlt: 3 - SKU873: - cost: 423 - init_stock: 620 - price: 423 - service_level: 0.96 - vlt: 2 - SKU874: - cost: 469 - init_stock: 1200 - price: 469 - service_level: 0.96 - vlt: 1 - SKU875: - cost: 51 - init_stock: 460 - price: 51 - service_level: 0.96 - vlt: 2 - SKU876: - cost: 303 - init_stock: 1220 - price: 303 - service_level: 0.96 - vlt: 3 - SKU877: - cost: 40 - init_stock: 700 - price: 40 - service_level: 0.96 - vlt: 3 - SKU878: - cost: 27 - init_stock: 1360 - price: 27 - service_level: 0.96 - vlt: 3 - SKU879: - cost: 150 - init_stock: 2000 - price: 150 - service_level: 0.96 - vlt: 1 - SKU88: - cost: 471 - init_stock: 240 - price: 471 - service_level: 0.96 - vlt: 1 - SKU880: - cost: 433 - init_stock: 620 - price: 433 - service_level: 0.96 - vlt: 3 - SKU881: - cost: 431 - init_stock: 1400 - price: 431 - service_level: 0.96 - vlt: 3 - SKU882: - cost: 194 - init_stock: 320 - price: 194 - service_level: 0.96 - vlt: 1 - SKU883: - cost: 284 - init_stock: 760 - price: 284 - service_level: 0.96 - vlt: 1 - SKU884: - cost: 139 - init_stock: 780 - price: 139 - service_level: 0.96 - vlt: 2 - SKU885: - cost: 49 - init_stock: 540 - price: 49 - service_level: 0.96 - vlt: 3 - SKU886: - cost: 372 - init_stock: 1460 - price: 372 - service_level: 0.96 - vlt: 3 - SKU887: - cost: 266 - init_stock: 1740 - price: 266 - service_level: 0.96 - vlt: 1 - SKU888: - cost: 143 - init_stock: 180 - price: 143 - service_level: 0.96 - vlt: 2 - SKU889: - cost: 101 - init_stock: 420 - price: 101 - service_level: 0.96 - vlt: 2 - SKU89: - cost: 138 - init_stock: 1860 - price: 138 - service_level: 0.96 - vlt: 3 - SKU890: - cost: 161 - init_stock: 2000 - price: 161 - service_level: 0.96 - vlt: 3 - SKU891: - cost: 356 - init_stock: 440 - price: 356 - service_level: 0.96 - vlt: 3 - SKU892: - cost: 313 - init_stock: 1840 - price: 313 - service_level: 0.96 - vlt: 2 - SKU893: - cost: 229 - init_stock: 1980 - price: 229 - service_level: 0.96 - vlt: 1 - SKU894: - cost: 129 - init_stock: 1480 - price: 129 - service_level: 0.96 - vlt: 1 - SKU895: - cost: 230 - init_stock: 260 - price: 230 - service_level: 0.96 - vlt: 2 - SKU896: - cost: 289 - init_stock: 1600 - price: 289 - service_level: 0.96 - vlt: 3 - SKU897: - cost: 393 - init_stock: 1440 - price: 393 - service_level: 0.96 - vlt: 3 - SKU898: - cost: 477 - init_stock: 220 - price: 477 - service_level: 0.96 - vlt: 2 - SKU899: - cost: 233 - init_stock: 960 - price: 233 - service_level: 0.96 - vlt: 2 - SKU9: - cost: 166 - init_stock: 1920 - price: 166 - service_level: 0.96 - vlt: 2 - SKU90: - cost: 454 - init_stock: 1020 - price: 454 - service_level: 0.96 - vlt: 2 - SKU900: - cost: 158 - init_stock: 1100 - price: 158 - service_level: 0.96 - vlt: 1 - SKU901: - cost: 215 - init_stock: 580 - price: 215 - service_level: 0.96 - vlt: 3 - SKU902: - cost: 125 - init_stock: 1600 - price: 125 - service_level: 0.96 - vlt: 2 - SKU903: - cost: 357 - init_stock: 760 - price: 357 - service_level: 0.96 - vlt: 2 - SKU904: - cost: 496 - init_stock: 1020 - price: 496 - service_level: 0.96 - vlt: 1 - SKU905: - cost: 249 - init_stock: 620 - price: 249 - service_level: 0.96 - vlt: 3 - SKU906: - cost: 166 - init_stock: 1040 - price: 166 - service_level: 0.96 - vlt: 2 - SKU907: - cost: 22 - init_stock: 1660 - price: 22 - service_level: 0.96 - vlt: 3 - SKU908: - cost: 408 - init_stock: 1560 - price: 408 - service_level: 0.96 - vlt: 2 - SKU909: - cost: 482 - init_stock: 1920 - price: 482 - service_level: 0.96 - vlt: 1 - SKU91: - cost: 303 - init_stock: 320 - price: 303 - service_level: 0.96 - vlt: 2 - SKU910: - cost: 226 - init_stock: 660 - price: 226 - service_level: 0.96 - vlt: 2 - SKU911: - cost: 461 - init_stock: 1400 - price: 461 - service_level: 0.96 - vlt: 2 - SKU912: - cost: 236 - init_stock: 540 - price: 236 - service_level: 0.96 - vlt: 3 - SKU913: - cost: 322 - init_stock: 920 - price: 322 - service_level: 0.96 - vlt: 2 - SKU914: - cost: 272 - init_stock: 1240 - price: 272 - service_level: 0.96 - vlt: 3 - SKU915: - cost: 337 - init_stock: 1120 - price: 337 - service_level: 0.96 - vlt: 3 - SKU916: - cost: 337 - init_stock: 1780 - price: 337 - service_level: 0.96 - vlt: 3 - SKU917: - cost: 258 - init_stock: 600 - price: 258 - service_level: 0.96 - vlt: 3 - SKU918: - cost: 148 - init_stock: 1100 - price: 148 - service_level: 0.96 - vlt: 1 - SKU919: - cost: 393 - init_stock: 500 - price: 393 - service_level: 0.96 - vlt: 3 - SKU92: - cost: 262 - init_stock: 1260 - price: 262 - service_level: 0.96 - vlt: 2 - SKU920: - cost: 357 - init_stock: 1720 - price: 357 - service_level: 0.96 - vlt: 3 - SKU921: - cost: 227 - init_stock: 1320 - price: 227 - service_level: 0.96 - vlt: 2 - SKU922: - cost: 112 - init_stock: 1340 - price: 112 - service_level: 0.96 - vlt: 2 - SKU923: - cost: 496 - init_stock: 1160 - price: 496 - service_level: 0.96 - vlt: 2 - SKU924: - cost: 316 - init_stock: 1700 - price: 316 - service_level: 0.96 - vlt: 3 - SKU925: - cost: 360 - init_stock: 300 - price: 360 - service_level: 0.96 - vlt: 1 - SKU926: - cost: 360 - init_stock: 1340 - price: 360 - service_level: 0.96 - vlt: 2 - SKU927: - cost: 260 - init_stock: 420 - price: 260 - service_level: 0.96 - vlt: 3 - SKU928: - cost: 491 - init_stock: 1660 - price: 491 - service_level: 0.96 - vlt: 1 - SKU929: - cost: 359 - init_stock: 2000 - price: 359 - service_level: 0.96 - vlt: 3 - SKU93: - cost: 404 - init_stock: 1340 - price: 404 - service_level: 0.96 - vlt: 2 - SKU930: - cost: 198 - init_stock: 560 - price: 198 - service_level: 0.96 - vlt: 2 - SKU931: - cost: 71 - init_stock: 280 - price: 71 - service_level: 0.96 - vlt: 3 - SKU932: - cost: 163 - init_stock: 1320 - price: 163 - service_level: 0.96 - vlt: 2 - SKU933: - cost: 113 - init_stock: 1560 - price: 113 - service_level: 0.96 - vlt: 1 - SKU934: - cost: 219 - init_stock: 1340 - price: 219 - service_level: 0.96 - vlt: 3 - SKU935: - cost: 364 - init_stock: 1880 - price: 364 - service_level: 0.96 - vlt: 1 - SKU936: - cost: 24 - init_stock: 100 - price: 24 - service_level: 0.96 - vlt: 3 - SKU937: - cost: 135 - init_stock: 340 - price: 135 - service_level: 0.96 - vlt: 1 - SKU938: - cost: 432 - init_stock: 420 - price: 432 - service_level: 0.96 - vlt: 2 - SKU939: - cost: 173 - init_stock: 1180 - price: 173 - service_level: 0.96 - vlt: 3 - SKU94: - cost: 184 - init_stock: 940 - price: 184 - service_level: 0.96 - vlt: 3 - SKU940: - cost: 14 - init_stock: 1860 - price: 14 - service_level: 0.96 - vlt: 1 - SKU941: - cost: 80 - init_stock: 1140 - price: 80 - service_level: 0.96 - vlt: 3 - SKU942: - cost: 202 - init_stock: 260 - price: 202 - service_level: 0.96 - vlt: 3 - SKU943: - cost: 138 - init_stock: 980 - price: 138 - service_level: 0.96 - vlt: 1 - SKU944: - cost: 196 - init_stock: 880 - price: 196 - service_level: 0.96 - vlt: 1 - SKU945: - cost: 141 - init_stock: 340 - price: 141 - service_level: 0.96 - vlt: 2 - SKU946: - cost: 325 - init_stock: 980 - price: 325 - service_level: 0.96 - vlt: 1 - SKU947: - cost: 338 - init_stock: 1820 - price: 338 - service_level: 0.96 - vlt: 3 - SKU948: - cost: 425 - init_stock: 560 - price: 425 - service_level: 0.96 - vlt: 3 - SKU949: - cost: 309 - init_stock: 100 - price: 309 - service_level: 0.96 - vlt: 2 - SKU95: - cost: 136 - init_stock: 420 - price: 136 - service_level: 0.96 - vlt: 3 - SKU950: - cost: 102 - init_stock: 1080 - price: 102 - service_level: 0.96 - vlt: 2 - SKU951: - cost: 75 - init_stock: 360 - price: 75 - service_level: 0.96 - vlt: 2 - SKU952: - cost: 156 - init_stock: 220 - price: 156 - service_level: 0.96 - vlt: 3 - SKU953: - cost: 138 - init_stock: 700 - price: 138 - service_level: 0.96 - vlt: 3 - SKU954: - cost: 296 - init_stock: 1720 - price: 296 - service_level: 0.96 - vlt: 1 - SKU955: - cost: 55 - init_stock: 1260 - price: 55 - service_level: 0.96 - vlt: 1 - SKU956: - cost: 282 - init_stock: 1700 - price: 282 - service_level: 0.96 - vlt: 1 - SKU957: - cost: 305 - init_stock: 1020 - price: 305 - service_level: 0.96 - vlt: 2 - SKU958: - cost: 369 - init_stock: 1320 - price: 369 - service_level: 0.96 - vlt: 2 - SKU959: - cost: 81 - init_stock: 1640 - price: 81 - service_level: 0.96 - vlt: 1 - SKU96: - cost: 176 - init_stock: 880 - price: 176 - service_level: 0.96 - vlt: 3 - SKU960: - cost: 147 - init_stock: 120 - price: 147 - service_level: 0.96 - vlt: 3 - SKU961: - cost: 264 - init_stock: 880 - price: 264 - service_level: 0.96 - vlt: 1 - SKU962: - cost: 354 - init_stock: 880 - price: 354 - service_level: 0.96 - vlt: 1 - SKU963: - cost: 349 - init_stock: 420 - price: 349 - service_level: 0.96 - vlt: 1 - SKU964: - cost: 244 - init_stock: 1460 - price: 244 - service_level: 0.96 - vlt: 1 - SKU965: - cost: 124 - init_stock: 1020 - price: 124 - service_level: 0.96 - vlt: 1 - SKU966: - cost: 302 - init_stock: 880 - price: 302 - service_level: 0.96 - vlt: 3 - SKU967: - cost: 67 - init_stock: 900 - price: 67 - service_level: 0.96 - vlt: 3 - SKU968: - cost: 281 - init_stock: 980 - price: 281 - service_level: 0.96 - vlt: 2 - SKU969: - cost: 249 - init_stock: 120 - price: 249 - service_level: 0.96 - vlt: 1 - SKU97: - cost: 28 - init_stock: 600 - price: 28 - service_level: 0.96 - vlt: 3 - SKU970: - cost: 244 - init_stock: 100 - price: 244 - service_level: 0.96 - vlt: 2 - SKU971: - cost: 368 - init_stock: 1460 - price: 368 - service_level: 0.96 - vlt: 1 - SKU972: - cost: 209 - init_stock: 1380 - price: 209 - service_level: 0.96 - vlt: 1 - SKU973: - cost: 271 - init_stock: 220 - price: 271 - service_level: 0.96 - vlt: 2 - SKU974: - cost: 170 - init_stock: 640 - price: 170 - service_level: 0.96 - vlt: 1 - SKU975: - cost: 198 - init_stock: 440 - price: 198 - service_level: 0.96 - vlt: 3 - SKU976: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 2 - SKU977: - cost: 234 - init_stock: 1080 - price: 234 - service_level: 0.96 - vlt: 3 - SKU978: - cost: 470 - init_stock: 1280 - price: 470 - service_level: 0.96 - vlt: 3 - SKU979: - cost: 443 - init_stock: 1920 - price: 443 - service_level: 0.96 - vlt: 2 - SKU98: - cost: 443 - init_stock: 700 - price: 443 - service_level: 0.96 - vlt: 1 - SKU980: - cost: 39 - init_stock: 1360 - price: 39 - service_level: 0.96 - vlt: 3 - SKU981: - cost: 482 - init_stock: 1440 - price: 482 - service_level: 0.96 - vlt: 2 - SKU982: - cost: 213 - init_stock: 1040 - price: 213 - service_level: 0.96 - vlt: 3 - SKU983: - cost: 449 - init_stock: 1840 - price: 449 - service_level: 0.96 - vlt: 1 - SKU984: - cost: 232 - init_stock: 1820 - price: 232 - service_level: 0.96 - vlt: 3 - SKU985: - cost: 290 - init_stock: 1020 - price: 290 - service_level: 0.96 - vlt: 1 - SKU986: - cost: 275 - init_stock: 340 - price: 275 - service_level: 0.96 - vlt: 3 - SKU987: - cost: 434 - init_stock: 680 - price: 434 - service_level: 0.96 - vlt: 1 - SKU988: - cost: 102 - init_stock: 460 - price: 102 - service_level: 0.96 - vlt: 1 - SKU989: - cost: 484 - init_stock: 1860 - price: 484 - service_level: 0.96 - vlt: 1 - SKU99: - cost: 361 - init_stock: 900 - price: 361 - service_level: 0.96 - vlt: 3 - SKU990: - cost: 108 - init_stock: 1140 - price: 108 - service_level: 0.96 - vlt: 3 - SKU991: - cost: 409 - init_stock: 380 - price: 409 - service_level: 0.96 - vlt: 3 - SKU992: - cost: 434 - init_stock: 1860 - price: 434 - service_level: 0.96 - vlt: 3 - SKU993: - cost: 145 - init_stock: 1720 - price: 145 - service_level: 0.96 - vlt: 2 - SKU994: - cost: 470 - init_stock: 1000 - price: 470 - service_level: 0.96 - vlt: 3 - SKU995: - cost: 241 - init_stock: 1520 - price: 241 - service_level: 0.96 - vlt: 2 - SKU996: - cost: 260 - init_stock: 1460 - price: 260 - service_level: 0.96 - vlt: 3 - SKU997: - cost: 400 - init_stock: 680 - price: 400 - service_level: 0.96 - vlt: 1 - SKU998: - cost: 447 - init_stock: 1100 - price: 447 - service_level: 0.96 - vlt: 2 - SKU999: - cost: 79 - init_stock: 460 - price: 79 - service_level: 0.96 - vlt: 2 - - children: - storage: - config: - capacity: 1064900 - unit_storage_cost: 1 - config: - order_cost: 500 - definition_ref: RetailerFacility - name: STORE0 - skus: - SKU0: - constraint: null - cost: 322 - init_stock: 126 - max_stock: 1000 - price: 631 - sale_gamma: 63 - service_level: 0.95 - SKU1: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 284 - init_stock: 176 - max_stock: 1000 - price: 448 - sale_gamma: 22 - service_level: 0.95 - SKU10: - constraint: null - cost: 68 - init_stock: 150 - max_stock: 1000 - price: 116 - sale_gamma: 25 - service_level: 0.95 - SKU100: - constraint: G(low_profit -> low_stock_constraint) - cost: 16 - init_stock: 390 - max_stock: 1000 - price: 23 - sale_gamma: 65 - service_level: 0.95 - SKU101: - constraint: null - cost: 84 - init_stock: 497 - max_stock: 1000 - price: 149 - sale_gamma: 71 - service_level: 0.95 - SKU102: - constraint: null - cost: 328 - init_stock: 558 - max_stock: 1000 - price: 505 - sale_gamma: 93 - service_level: 0.95 - SKU103: - constraint: null - cost: 334 - init_stock: 285 - max_stock: 1000 - price: 601 - sale_gamma: 95 - service_level: 0.95 - SKU104: - constraint: null - cost: 49 - init_stock: 325 - max_stock: 1000 - price: 57 - sale_gamma: 65 - service_level: 0.95 - SKU105: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 110 - init_stock: 285 - max_stock: 1000 - price: 171 - sale_gamma: 57 - service_level: 0.95 - SKU106: - constraint: G(low_profit -> low_stock_constraint) - cost: 251 - init_stock: 584 - max_stock: 1000 - price: 454 - sale_gamma: 73 - service_level: 0.95 - SKU107: - constraint: G(stock_constraint) - cost: 423 - init_stock: 348 - max_stock: 1000 - price: 706 - sale_gamma: 87 - service_level: 0.95 - SKU108: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 458 - init_stock: 368 - max_stock: 1000 - price: 801 - sale_gamma: 92 - service_level: 0.95 - SKU109: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 88 - init_stock: 328 - max_stock: 1000 - price: 98 - sale_gamma: 82 - service_level: 0.95 - SKU11: - constraint: null - cost: 400 - init_stock: 306 - max_stock: 1000 - price: 680 - sale_gamma: 51 - service_level: 0.95 - SKU110: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 66 - init_stock: 112 - max_stock: 1000 - price: 100 - sale_gamma: 14 - service_level: 0.95 - SKU111: - constraint: G(stock_constraint) - cost: 260 - init_stock: 183 - max_stock: 1000 - price: 364 - sale_gamma: 61 - service_level: 0.95 - SKU112: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 61 - init_stock: 475 - max_stock: 1000 - price: 119 - sale_gamma: 95 - service_level: 0.95 - SKU113: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 155 - max_stock: 1000 - price: 678 - sale_gamma: 31 - service_level: 0.95 - SKU114: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 389 - init_stock: 162 - max_stock: 1000 - price: 564 - sale_gamma: 27 - service_level: 0.95 - SKU115: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 258 - max_stock: 1000 - price: 557 - sale_gamma: 86 - service_level: 0.95 - SKU116: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 288 - max_stock: 1000 - price: 768 - sale_gamma: 72 - service_level: 0.95 - SKU117: - constraint: null - cost: 320 - init_stock: 644 - max_stock: 1000 - price: 374 - sale_gamma: 92 - service_level: 0.95 - SKU118: - constraint: null - cost: 183 - init_stock: 66 - max_stock: 1000 - price: 208 - sale_gamma: 33 - service_level: 0.95 - SKU119: - constraint: null - cost: 209 - init_stock: 256 - max_stock: 1000 - price: 317 - sale_gamma: 32 - service_level: 0.95 - SKU12: - constraint: null - cost: 112 - init_stock: 336 - max_stock: 1000 - price: 210 - sale_gamma: 84 - service_level: 0.95 - SKU120: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 121 - init_stock: 392 - max_stock: 1000 - price: 151 - sale_gamma: 98 - service_level: 0.95 - SKU121: - constraint: null - cost: 40 - init_stock: 510 - max_stock: 1000 - price: 54 - sale_gamma: 85 - service_level: 0.95 - SKU122: - constraint: G(stock_constraint) - cost: 437 - init_stock: 35 - max_stock: 1000 - price: 589 - sale_gamma: 7 - service_level: 0.95 - SKU123: - constraint: G(stock_constraint) - cost: 233 - init_stock: 114 - max_stock: 1000 - price: 326 - sale_gamma: 19 - service_level: 0.95 - SKU124: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 108 - max_stock: 1000 - price: 298 - sale_gamma: 36 - service_level: 0.95 - SKU125: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 368 - max_stock: 1000 - price: 32 - sale_gamma: 92 - service_level: 0.95 - SKU126: - constraint: null - cost: 36 - init_stock: 156 - max_stock: 1000 - price: 40 - sale_gamma: 39 - service_level: 0.95 - SKU127: - constraint: G(stock_constraint) - cost: 217 - init_stock: 155 - max_stock: 1000 - price: 366 - sale_gamma: 31 - service_level: 0.95 - SKU128: - constraint: null - cost: 165 - init_stock: 95 - max_stock: 1000 - price: 283 - sale_gamma: 19 - service_level: 0.95 - SKU129: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 203 - sale_gamma: 50 - service_level: 0.95 - SKU13: - constraint: null - cost: 317 - init_stock: 456 - max_stock: 1000 - price: 573 - sale_gamma: 57 - service_level: 0.95 - SKU130: - constraint: null - cost: 348 - init_stock: 784 - max_stock: 1000 - price: 396 - sale_gamma: 98 - service_level: 0.95 - SKU131: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 64 - init_stock: 270 - max_stock: 1000 - price: 110 - sale_gamma: 45 - service_level: 0.95 - SKU132: - constraint: null - cost: 427 - init_stock: 105 - max_stock: 1000 - price: 678 - sale_gamma: 21 - service_level: 0.95 - SKU133: - constraint: null - cost: 224 - init_stock: 145 - max_stock: 1000 - price: 448 - sale_gamma: 29 - service_level: 0.95 - SKU134: - constraint: G(stock_constraint) - cost: 336 - init_stock: 539 - max_stock: 1000 - price: 470 - sale_gamma: 77 - service_level: 0.95 - SKU135: - constraint: null - cost: 153 - init_stock: 500 - max_stock: 1000 - price: 243 - sale_gamma: 100 - service_level: 0.95 - SKU136: - constraint: G(low_profit -> low_stock_constraint) - cost: 199 - init_stock: 497 - max_stock: 1000 - price: 370 - sale_gamma: 71 - service_level: 0.95 - SKU137: - constraint: G(stock_constraint) - cost: 93 - init_stock: 444 - max_stock: 1000 - price: 165 - sale_gamma: 74 - service_level: 0.95 - SKU138: - constraint: G(stock_constraint) - cost: 228 - init_stock: 180 - max_stock: 1000 - price: 253 - sale_gamma: 36 - service_level: 0.95 - SKU139: - constraint: G(stock_constraint) - cost: 207 - init_stock: 144 - max_stock: 1000 - price: 368 - sale_gamma: 24 - service_level: 0.95 - SKU14: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 268 - init_stock: 372 - max_stock: 1000 - price: 305 - sale_gamma: 62 - service_level: 0.95 - SKU140: - constraint: null - cost: 261 - init_stock: 238 - max_stock: 1000 - price: 357 - sale_gamma: 34 - service_level: 0.95 - SKU141: - constraint: null - cost: 190 - init_stock: 164 - max_stock: 1000 - price: 370 - sale_gamma: 41 - service_level: 0.95 - SKU142: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 320 - init_stock: 152 - max_stock: 1000 - price: 428 - sale_gamma: 38 - service_level: 0.95 - SKU143: - constraint: G(stock_constraint) - cost: 318 - init_stock: 182 - max_stock: 1000 - price: 566 - sale_gamma: 26 - service_level: 0.95 - SKU144: - constraint: null - cost: 400 - init_stock: 24 - max_stock: 1000 - price: 716 - sale_gamma: 12 - service_level: 0.95 - SKU145: - constraint: null - cost: 399 - init_stock: 328 - max_stock: 1000 - price: 774 - sale_gamma: 82 - service_level: 0.95 - SKU146: - constraint: null - cost: 177 - init_stock: 192 - max_stock: 1000 - price: 194 - sale_gamma: 48 - service_level: 0.95 - SKU147: - constraint: G(low_profit -> low_stock_constraint) - cost: 472 - init_stock: 392 - max_stock: 1000 - price: 840 - sale_gamma: 56 - service_level: 0.95 - SKU148: - constraint: G(stock_constraint) - cost: 313 - init_stock: 308 - max_stock: 1000 - price: 566 - sale_gamma: 77 - service_level: 0.95 - SKU149: - constraint: G(low_profit -> low_stock_constraint) - cost: 357 - init_stock: 539 - max_stock: 1000 - price: 549 - sale_gamma: 77 - service_level: 0.95 - SKU15: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 25 - max_stock: 1000 - price: 58 - sale_gamma: 5 - service_level: 0.95 - SKU150: - constraint: null - cost: 106 - init_stock: 210 - max_stock: 1000 - price: 159 - sale_gamma: 70 - service_level: 0.95 - SKU151: - constraint: G(low_profit -> low_stock_constraint) - cost: 223 - init_stock: 146 - max_stock: 1000 - price: 370 - sale_gamma: 73 - service_level: 0.95 - SKU152: - constraint: null - cost: 10 - init_stock: 408 - max_stock: 1000 - price: 14 - sale_gamma: 51 - service_level: 0.95 - SKU153: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 441 - init_stock: 372 - max_stock: 1000 - price: 538 - sale_gamma: 62 - service_level: 0.95 - SKU154: - constraint: null - cost: 77 - init_stock: 680 - max_stock: 1000 - price: 135 - sale_gamma: 85 - service_level: 0.95 - SKU155: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 159 - max_stock: 1000 - price: 641 - sale_gamma: 53 - service_level: 0.95 - SKU156: - constraint: null - cost: 10 - init_stock: 36 - max_stock: 1000 - price: 16 - sale_gamma: 12 - service_level: 0.95 - SKU157: - constraint: null - cost: 410 - init_stock: 525 - max_stock: 1000 - price: 594 - sale_gamma: 75 - service_level: 0.95 - SKU158: - constraint: null - cost: 145 - init_stock: 486 - max_stock: 1000 - price: 275 - sale_gamma: 81 - service_level: 0.95 - SKU159: - constraint: G(stock_constraint) - cost: 193 - init_stock: 100 - max_stock: 1000 - price: 368 - sale_gamma: 25 - service_level: 0.95 - SKU16: - constraint: null - cost: 175 - init_stock: 464 - max_stock: 1000 - price: 344 - sale_gamma: 58 - service_level: 0.95 - SKU160: - constraint: G(low_profit -> low_stock_constraint) - cost: 459 - init_stock: 510 - max_stock: 1000 - price: 876 - sale_gamma: 85 - service_level: 0.95 - SKU161: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 239 - init_stock: 230 - max_stock: 1000 - price: 478 - sale_gamma: 46 - service_level: 0.95 - SKU162: - constraint: null - cost: 158 - init_stock: 30 - max_stock: 1000 - price: 290 - sale_gamma: 5 - service_level: 0.95 - SKU163: - constraint: G(stock_constraint) - cost: 486 - init_stock: 273 - max_stock: 1000 - price: 704 - sale_gamma: 39 - service_level: 0.95 - SKU164: - constraint: null - cost: 496 - init_stock: 500 - max_stock: 1000 - price: 615 - sale_gamma: 100 - service_level: 0.95 - SKU165: - constraint: G(stock_constraint) - cost: 274 - init_stock: 231 - max_stock: 1000 - price: 548 - sale_gamma: 33 - service_level: 0.95 - SKU166: - constraint: null - cost: 79 - init_stock: 178 - max_stock: 1000 - price: 137 - sale_gamma: 89 - service_level: 0.95 - SKU167: - constraint: G(stock_constraint) - cost: 443 - init_stock: 52 - max_stock: 1000 - price: 854 - sale_gamma: 13 - service_level: 0.95 - SKU168: - constraint: null - cost: 357 - init_stock: 522 - max_stock: 1000 - price: 546 - sale_gamma: 87 - service_level: 0.95 - SKU169: - constraint: null - cost: 369 - init_stock: 686 - max_stock: 1000 - price: 505 - sale_gamma: 98 - service_level: 0.95 - SKU17: - constraint: null - cost: 346 - init_stock: 18 - max_stock: 1000 - price: 553 - sale_gamma: 9 - service_level: 0.95 - SKU170: - constraint: null - cost: 68 - init_stock: 275 - max_stock: 1000 - price: 113 - sale_gamma: 55 - service_level: 0.95 - SKU171: - constraint: G(low_profit -> low_stock_constraint) - cost: 398 - init_stock: 380 - max_stock: 1000 - price: 704 - sale_gamma: 76 - service_level: 0.95 - SKU172: - constraint: null - cost: 200 - init_stock: 426 - max_stock: 1000 - price: 222 - sale_gamma: 71 - service_level: 0.95 - SKU173: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 480 - max_stock: 1000 - price: 264 - sale_gamma: 96 - service_level: 0.95 - SKU174: - constraint: null - cost: 291 - init_stock: 380 - max_stock: 1000 - price: 582 - sale_gamma: 76 - service_level: 0.95 - SKU175: - constraint: null - cost: 84 - init_stock: 225 - max_stock: 1000 - price: 140 - sale_gamma: 75 - service_level: 0.95 - SKU176: - constraint: G(stock_constraint) - cost: 407 - init_stock: 330 - max_stock: 1000 - price: 610 - sale_gamma: 66 - service_level: 0.95 - SKU177: - constraint: null - cost: 257 - init_stock: 155 - max_stock: 1000 - price: 346 - sale_gamma: 31 - service_level: 0.95 - SKU178: - constraint: null - cost: 423 - init_stock: 20 - max_stock: 1000 - price: 499 - sale_gamma: 5 - service_level: 0.95 - SKU179: - constraint: null - cost: 497 - init_stock: 415 - max_stock: 1000 - price: 690 - sale_gamma: 83 - service_level: 0.95 - SKU18: - constraint: null - cost: 258 - init_stock: 405 - max_stock: 1000 - price: 387 - sale_gamma: 81 - service_level: 0.95 - SKU180: - constraint: null - cost: 217 - init_stock: 165 - max_stock: 1000 - price: 297 - sale_gamma: 55 - service_level: 0.95 - SKU181: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 214 - sale_gamma: 60 - service_level: 0.95 - SKU182: - constraint: null - cost: 437 - init_stock: 693 - max_stock: 1000 - price: 764 - sale_gamma: 99 - service_level: 0.95 - SKU183: - constraint: null - cost: 145 - init_stock: 56 - max_stock: 1000 - price: 178 - sale_gamma: 8 - service_level: 0.95 - SKU184: - constraint: null - cost: 73 - init_stock: 72 - max_stock: 1000 - price: 100 - sale_gamma: 24 - service_level: 0.95 - SKU185: - constraint: null - cost: 10 - init_stock: 360 - max_stock: 1000 - price: 14 - sale_gamma: 90 - service_level: 0.95 - SKU186: - constraint: null - cost: 359 - init_stock: 66 - max_stock: 1000 - price: 649 - sale_gamma: 22 - service_level: 0.95 - SKU187: - constraint: G(low_profit -> low_stock_constraint) - cost: 177 - init_stock: 120 - max_stock: 1000 - price: 249 - sale_gamma: 30 - service_level: 0.95 - SKU188: - constraint: null - cost: 391 - init_stock: 348 - max_stock: 1000 - price: 586 - sale_gamma: 87 - service_level: 0.95 - SKU189: - constraint: G(low_profit -> low_stock_constraint) - cost: 358 - init_stock: 210 - max_stock: 1000 - price: 400 - sale_gamma: 35 - service_level: 0.95 - SKU19: - constraint: G(stock_constraint) - cost: 477 - init_stock: 364 - max_stock: 1000 - price: 791 - sale_gamma: 91 - service_level: 0.95 - SKU190: - constraint: null - cost: 113 - init_stock: 102 - max_stock: 1000 - price: 153 - sale_gamma: 17 - service_level: 0.95 - SKU191: - constraint: G(stock_constraint) - cost: 473 - init_stock: 324 - max_stock: 1000 - price: 638 - sale_gamma: 54 - service_level: 0.95 - SKU192: - constraint: null - cost: 415 - init_stock: 244 - max_stock: 1000 - price: 539 - sale_gamma: 61 - service_level: 0.95 - SKU193: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 207 - init_stock: 120 - max_stock: 1000 - price: 353 - sale_gamma: 30 - service_level: 0.95 - SKU194: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 30 - max_stock: 1000 - price: 803 - sale_gamma: 5 - service_level: 0.95 - SKU195: - constraint: G(low_profit -> low_stock_constraint) - cost: 218 - init_stock: 93 - max_stock: 1000 - price: 248 - sale_gamma: 31 - service_level: 0.95 - SKU196: - constraint: null - cost: 49 - init_stock: 544 - max_stock: 1000 - price: 62 - sale_gamma: 68 - service_level: 0.95 - SKU197: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 285 - max_stock: 1000 - price: 509 - sale_gamma: 57 - service_level: 0.95 - SKU198: - constraint: G(low_profit -> low_stock_constraint) - cost: 169 - init_stock: 378 - max_stock: 1000 - price: 307 - sale_gamma: 54 - service_level: 0.95 - SKU199: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 138 - max_stock: 1000 - price: 794 - sale_gamma: 23 - service_level: 0.95 - SKU2: - constraint: null - cost: 331 - init_stock: 350 - max_stock: 1000 - price: 499 - sale_gamma: 70 - service_level: 0.95 - SKU20: - constraint: G(stock_constraint) - cost: 335 - init_stock: 200 - max_stock: 1000 - price: 649 - sale_gamma: 25 - service_level: 0.95 - SKU200: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 65 - init_stock: 150 - max_stock: 1000 - price: 85 - sale_gamma: 25 - service_level: 0.95 - SKU201: - constraint: G(stock_constraint) - cost: 104 - init_stock: 354 - max_stock: 1000 - price: 161 - sale_gamma: 59 - service_level: 0.95 - SKU202: - constraint: null - cost: 142 - init_stock: 365 - max_stock: 1000 - price: 222 - sale_gamma: 73 - service_level: 0.95 - SKU203: - constraint: null - cost: 440 - init_stock: 328 - max_stock: 1000 - price: 677 - sale_gamma: 82 - service_level: 0.95 - SKU204: - constraint: null - cost: 489 - init_stock: 188 - max_stock: 1000 - price: 831 - sale_gamma: 47 - service_level: 0.95 - SKU205: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 130 - init_stock: 500 - max_stock: 1000 - price: 175 - sale_gamma: 100 - service_level: 0.95 - SKU206: - constraint: null - cost: 335 - init_stock: 55 - max_stock: 1000 - price: 552 - sale_gamma: 11 - service_level: 0.95 - SKU207: - constraint: G(low_profit -> low_stock_constraint) - cost: 140 - init_stock: 240 - max_stock: 1000 - price: 170 - sale_gamma: 80 - service_level: 0.95 - SKU208: - constraint: null - cost: 491 - init_stock: 308 - max_stock: 1000 - price: 927 - sale_gamma: 77 - service_level: 0.95 - SKU209: - constraint: G(stock_constraint) - cost: 179 - init_stock: 120 - max_stock: 1000 - price: 322 - sale_gamma: 20 - service_level: 0.95 - SKU21: - constraint: null - cost: 123 - init_stock: 400 - max_stock: 1000 - price: 225 - sale_gamma: 100 - service_level: 0.95 - SKU210: - constraint: null - cost: 404 - init_stock: 345 - max_stock: 1000 - price: 468 - sale_gamma: 69 - service_level: 0.95 - SKU211: - constraint: null - cost: 174 - init_stock: 364 - max_stock: 1000 - price: 226 - sale_gamma: 91 - service_level: 0.95 - SKU212: - constraint: null - cost: 405 - init_stock: 632 - max_stock: 1000 - price: 534 - sale_gamma: 79 - service_level: 0.95 - SKU213: - constraint: null - cost: 121 - init_stock: 192 - max_stock: 1000 - price: 229 - sale_gamma: 64 - service_level: 0.95 - SKU214: - constraint: G(stock_constraint) - cost: 101 - init_stock: 60 - max_stock: 1000 - price: 144 - sale_gamma: 10 - service_level: 0.95 - SKU215: - constraint: null - cost: 419 - init_stock: 94 - max_stock: 1000 - price: 469 - sale_gamma: 47 - service_level: 0.95 - SKU216: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 330 - init_stock: 92 - max_stock: 1000 - price: 432 - sale_gamma: 23 - service_level: 0.95 - SKU217: - constraint: null - cost: 284 - init_stock: 455 - max_stock: 1000 - price: 312 - sale_gamma: 65 - service_level: 0.95 - SKU218: - constraint: G(low_profit -> low_stock_constraint) - cost: 205 - init_stock: 354 - max_stock: 1000 - price: 397 - sale_gamma: 59 - service_level: 0.95 - SKU219: - constraint: G(stock_constraint) - cost: 92 - init_stock: 368 - max_stock: 1000 - price: 135 - sale_gamma: 46 - service_level: 0.95 - SKU22: - constraint: null - cost: 493 - init_stock: 264 - max_stock: 1000 - price: 986 - sale_gamma: 66 - service_level: 0.95 - SKU220: - constraint: null - cost: 387 - init_stock: 435 - max_stock: 1000 - price: 572 - sale_gamma: 87 - service_level: 0.95 - SKU221: - constraint: null - cost: 39 - init_stock: 468 - max_stock: 1000 - price: 48 - sale_gamma: 78 - service_level: 0.95 - SKU222: - constraint: G(low_profit -> low_stock_constraint) - cost: 115 - init_stock: 180 - max_stock: 1000 - price: 210 - sale_gamma: 36 - service_level: 0.95 - SKU223: - constraint: G(low_profit -> low_stock_constraint) - cost: 196 - init_stock: 36 - max_stock: 1000 - price: 286 - sale_gamma: 12 - service_level: 0.95 - SKU224: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 387 - init_stock: 25 - max_stock: 1000 - price: 661 - sale_gamma: 5 - service_level: 0.95 - SKU225: - constraint: null - cost: 164 - init_stock: 116 - max_stock: 1000 - price: 216 - sale_gamma: 29 - service_level: 0.95 - SKU226: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 206 - init_stock: 65 - max_stock: 1000 - price: 350 - sale_gamma: 13 - service_level: 0.95 - SKU227: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 144 - max_stock: 1000 - price: 895 - sale_gamma: 24 - service_level: 0.95 - SKU228: - constraint: null - cost: 14 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 90 - service_level: 0.95 - SKU229: - constraint: null - cost: 472 - init_stock: 176 - max_stock: 1000 - price: 708 - sale_gamma: 44 - service_level: 0.95 - SKU23: - constraint: null - cost: 387 - init_stock: 210 - max_stock: 1000 - price: 700 - sale_gamma: 42 - service_level: 0.95 - SKU230: - constraint: null - cost: 241 - init_stock: 138 - max_stock: 1000 - price: 436 - sale_gamma: 23 - service_level: 0.95 - SKU231: - constraint: G(stock_constraint) - cost: 48 - init_stock: 486 - max_stock: 1000 - price: 96 - sale_gamma: 81 - service_level: 0.95 - SKU232: - constraint: G(low_profit -> low_stock_constraint) - cost: 224 - init_stock: 480 - max_stock: 1000 - price: 338 - sale_gamma: 96 - service_level: 0.95 - SKU233: - constraint: G(low_profit -> low_stock_constraint) - cost: 360 - init_stock: 300 - max_stock: 1000 - price: 428 - sale_gamma: 75 - service_level: 0.95 - SKU234: - constraint: null - cost: 287 - init_stock: 25 - max_stock: 1000 - price: 387 - sale_gamma: 5 - service_level: 0.95 - SKU235: - constraint: G(low_profit -> low_stock_constraint) - cost: 24 - init_stock: 228 - max_stock: 1000 - price: 32 - sale_gamma: 57 - service_level: 0.95 - SKU236: - constraint: G(low_profit -> low_stock_constraint) - cost: 155 - init_stock: 165 - max_stock: 1000 - price: 289 - sale_gamma: 55 - service_level: 0.95 - SKU237: - constraint: null - cost: 433 - init_stock: 270 - max_stock: 1000 - price: 779 - sale_gamma: 45 - service_level: 0.95 - SKU238: - constraint: G(stock_constraint) - cost: 64 - init_stock: 264 - max_stock: 1000 - price: 112 - sale_gamma: 66 - service_level: 0.95 - SKU239: - constraint: null - cost: 103 - init_stock: 490 - max_stock: 1000 - price: 139 - sale_gamma: 98 - service_level: 0.95 - SKU24: - constraint: null - cost: 97 - init_stock: 329 - max_stock: 1000 - price: 114 - sale_gamma: 47 - service_level: 0.95 - SKU240: - constraint: null - cost: 373 - init_stock: 188 - max_stock: 1000 - price: 738 - sale_gamma: 47 - service_level: 0.95 - SKU241: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 213 - max_stock: 1000 - price: 860 - sale_gamma: 71 - service_level: 0.95 - SKU242: - constraint: null - cost: 17 - init_stock: 88 - max_stock: 1000 - price: 29 - sale_gamma: 44 - service_level: 0.95 - SKU243: - constraint: null - cost: 352 - init_stock: 84 - max_stock: 1000 - price: 394 - sale_gamma: 14 - service_level: 0.95 - SKU244: - constraint: null - cost: 174 - init_stock: 410 - max_stock: 1000 - price: 226 - sale_gamma: 82 - service_level: 0.95 - SKU245: - constraint: G(low_profit -> low_stock_constraint) - cost: 404 - init_stock: 198 - max_stock: 1000 - price: 525 - sale_gamma: 66 - service_level: 0.95 - SKU246: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 300 - init_stock: 210 - max_stock: 1000 - price: 474 - sale_gamma: 30 - service_level: 0.95 - SKU247: - constraint: null - cost: 386 - init_stock: 210 - max_stock: 1000 - price: 497 - sale_gamma: 35 - service_level: 0.95 - SKU248: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 355 - init_stock: 174 - max_stock: 1000 - price: 539 - sale_gamma: 29 - service_level: 0.95 - SKU249: - constraint: null - cost: 356 - init_stock: 100 - max_stock: 1000 - price: 544 - sale_gamma: 25 - service_level: 0.95 - SKU25: - constraint: G(stock_constraint) - cost: 161 - init_stock: 35 - max_stock: 1000 - price: 249 - sale_gamma: 5 - service_level: 0.95 - SKU250: - constraint: null - cost: 127 - init_stock: 324 - max_stock: 1000 - price: 186 - sale_gamma: 54 - service_level: 0.95 - SKU251: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 344 - init_stock: 244 - max_stock: 1000 - price: 543 - sale_gamma: 61 - service_level: 0.95 - SKU252: - constraint: null - cost: 181 - init_stock: 415 - max_stock: 1000 - price: 334 - sale_gamma: 83 - service_level: 0.95 - SKU253: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 448 - init_stock: 32 - max_stock: 1000 - price: 864 - sale_gamma: 16 - service_level: 0.95 - SKU254: - constraint: null - cost: 484 - init_stock: 184 - max_stock: 1000 - price: 696 - sale_gamma: 46 - service_level: 0.95 - SKU255: - constraint: null - cost: 290 - init_stock: 335 - max_stock: 1000 - price: 568 - sale_gamma: 67 - service_level: 0.95 - SKU256: - constraint: null - cost: 91 - init_stock: 360 - max_stock: 1000 - price: 167 - sale_gamma: 72 - service_level: 0.95 - SKU257: - constraint: null - cost: 348 - init_stock: 171 - max_stock: 1000 - price: 497 - sale_gamma: 57 - service_level: 0.95 - SKU258: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 258 - max_stock: 1000 - price: 689 - sale_gamma: 43 - service_level: 0.95 - SKU259: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 207 - max_stock: 1000 - price: 559 - sale_gamma: 69 - service_level: 0.95 - SKU26: - constraint: null - cost: 229 - init_stock: 126 - max_stock: 1000 - price: 357 - sale_gamma: 63 - service_level: 0.95 - SKU260: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 364 - max_stock: 1000 - price: 866 - sale_gamma: 52 - service_level: 0.95 - SKU261: - constraint: null - cost: 368 - init_stock: 66 - max_stock: 1000 - price: 688 - sale_gamma: 22 - service_level: 0.95 - SKU262: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 332 - init_stock: 390 - max_stock: 1000 - price: 385 - sale_gamma: 78 - service_level: 0.95 - SKU263: - constraint: G(stock_constraint) - cost: 189 - init_stock: 444 - max_stock: 1000 - price: 372 - sale_gamma: 74 - service_level: 0.95 - SKU264: - constraint: null - cost: 361 - init_stock: 158 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU265: - constraint: null - cost: 286 - init_stock: 236 - max_stock: 1000 - price: 434 - sale_gamma: 59 - service_level: 0.95 - SKU266: - constraint: null - cost: 128 - init_stock: 282 - max_stock: 1000 - price: 172 - sale_gamma: 47 - service_level: 0.95 - SKU267: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 77 - init_stock: 320 - max_stock: 1000 - price: 130 - sale_gamma: 80 - service_level: 0.95 - SKU268: - constraint: null - cost: 221 - init_stock: 356 - max_stock: 1000 - price: 362 - sale_gamma: 89 - service_level: 0.95 - SKU269: - constraint: G(low_profit -> low_stock_constraint) - cost: 126 - init_stock: 132 - max_stock: 1000 - price: 175 - sale_gamma: 44 - service_level: 0.95 - SKU27: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 448 - max_stock: 1000 - price: 728 - sale_gamma: 56 - service_level: 0.95 - SKU270: - constraint: null - cost: 182 - init_stock: 370 - max_stock: 1000 - price: 263 - sale_gamma: 74 - service_level: 0.95 - SKU271: - constraint: G(stock_constraint) - cost: 230 - init_stock: 54 - max_stock: 1000 - price: 266 - sale_gamma: 18 - service_level: 0.95 - SKU272: - constraint: null - cost: 366 - init_stock: 51 - max_stock: 1000 - price: 625 - sale_gamma: 17 - service_level: 0.95 - SKU273: - constraint: null - cost: 421 - init_stock: 72 - max_stock: 1000 - price: 614 - sale_gamma: 18 - service_level: 0.95 - SKU274: - constraint: null - cost: 29 - init_stock: 308 - max_stock: 1000 - price: 42 - sale_gamma: 77 - service_level: 0.95 - SKU275: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 50 - init_stock: 144 - max_stock: 1000 - price: 76 - sale_gamma: 48 - service_level: 0.95 - SKU276: - constraint: G(stock_constraint) - cost: 163 - init_stock: 378 - max_stock: 1000 - price: 299 - sale_gamma: 54 - service_level: 0.95 - SKU277: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 82 - max_stock: 1000 - price: 507 - sale_gamma: 41 - service_level: 0.95 - SKU278: - constraint: null - cost: 105 - init_stock: 84 - max_stock: 1000 - price: 140 - sale_gamma: 12 - service_level: 0.95 - SKU279: - constraint: G(stock_constraint) - cost: 51 - init_stock: 273 - max_stock: 1000 - price: 61 - sale_gamma: 39 - service_level: 0.95 - SKU28: - constraint: G(stock_constraint) - cost: 208 - init_stock: 235 - max_stock: 1000 - price: 295 - sale_gamma: 47 - service_level: 0.95 - SKU280: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 198 - max_stock: 1000 - price: 436 - sale_gamma: 33 - service_level: 0.95 - SKU281: - constraint: null - cost: 395 - init_stock: 375 - max_stock: 1000 - price: 592 - sale_gamma: 75 - service_level: 0.95 - SKU282: - constraint: null - cost: 63 - init_stock: 184 - max_stock: 1000 - price: 112 - sale_gamma: 46 - service_level: 0.95 - SKU283: - constraint: null - cost: 392 - init_stock: 736 - max_stock: 1000 - price: 490 - sale_gamma: 92 - service_level: 0.95 - SKU284: - constraint: G(stock_constraint) - cost: 344 - init_stock: 268 - max_stock: 1000 - price: 643 - sale_gamma: 67 - service_level: 0.95 - SKU285: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 133 - init_stock: 546 - max_stock: 1000 - price: 148 - sale_gamma: 91 - service_level: 0.95 - SKU286: - constraint: null - cost: 337 - init_stock: 156 - max_stock: 1000 - price: 626 - sale_gamma: 39 - service_level: 0.95 - SKU287: - constraint: null - cost: 375 - init_stock: 224 - max_stock: 1000 - price: 660 - sale_gamma: 56 - service_level: 0.95 - SKU288: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 190 - max_stock: 1000 - price: 352 - sale_gamma: 38 - service_level: 0.95 - SKU289: - constraint: G(stock_constraint) - cost: 67 - init_stock: 62 - max_stock: 1000 - price: 91 - sale_gamma: 31 - service_level: 0.95 - SKU29: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 475 - sale_gamma: 58 - service_level: 0.95 - SKU290: - constraint: null - cost: 438 - init_stock: 268 - max_stock: 1000 - price: 779 - sale_gamma: 67 - service_level: 0.95 - SKU291: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 122 - max_stock: 1000 - price: 126 - sale_gamma: 61 - service_level: 0.95 - SKU292: - constraint: null - cost: 186 - init_stock: 15 - max_stock: 1000 - price: 344 - sale_gamma: 5 - service_level: 0.95 - SKU293: - constraint: G(stock_constraint) - cost: 162 - init_stock: 385 - max_stock: 1000 - price: 288 - sale_gamma: 55 - service_level: 0.95 - SKU294: - constraint: null - cost: 409 - init_stock: 45 - max_stock: 1000 - price: 605 - sale_gamma: 9 - service_level: 0.95 - SKU295: - constraint: null - cost: 435 - init_stock: 651 - max_stock: 1000 - price: 674 - sale_gamma: 93 - service_level: 0.95 - SKU296: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 552 - max_stock: 1000 - price: 580 - sale_gamma: 92 - service_level: 0.95 - SKU297: - constraint: null - cost: 298 - init_stock: 228 - max_stock: 1000 - price: 333 - sale_gamma: 38 - service_level: 0.95 - SKU298: - constraint: null - cost: 286 - init_stock: 140 - max_stock: 1000 - price: 500 - sale_gamma: 35 - service_level: 0.95 - SKU299: - constraint: G(stock_constraint) - cost: 367 - init_stock: 255 - max_stock: 1000 - price: 451 - sale_gamma: 51 - service_level: 0.95 - SKU3: - constraint: G(stock_constraint) - cost: 405 - init_stock: 48 - max_stock: 1000 - price: 733 - sale_gamma: 12 - service_level: 0.95 - SKU30: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 359 - init_stock: 276 - max_stock: 1000 - price: 581 - sale_gamma: 69 - service_level: 0.95 - SKU300: - constraint: G(low_profit -> low_stock_constraint) - cost: 376 - init_stock: 290 - max_stock: 1000 - price: 428 - sale_gamma: 58 - service_level: 0.95 - SKU301: - constraint: null - cost: 433 - init_stock: 581 - max_stock: 1000 - price: 857 - sale_gamma: 83 - service_level: 0.95 - SKU302: - constraint: null - cost: 184 - init_stock: 44 - max_stock: 1000 - price: 322 - sale_gamma: 11 - service_level: 0.95 - SKU303: - constraint: null - cost: 246 - init_stock: 188 - max_stock: 1000 - price: 487 - sale_gamma: 94 - service_level: 0.95 - SKU304: - constraint: G(low_profit -> low_stock_constraint) - cost: 419 - init_stock: 138 - max_stock: 1000 - price: 632 - sale_gamma: 23 - service_level: 0.95 - SKU305: - constraint: null - cost: 495 - init_stock: 500 - max_stock: 1000 - price: 772 - sale_gamma: 100 - service_level: 0.95 - SKU306: - constraint: null - cost: 479 - init_stock: 126 - max_stock: 1000 - price: 852 - sale_gamma: 42 - service_level: 0.95 - SKU307: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 210 - init_stock: 156 - max_stock: 1000 - price: 371 - sale_gamma: 78 - service_level: 0.95 - SKU308: - constraint: null - cost: 208 - init_stock: 25 - max_stock: 1000 - price: 262 - sale_gamma: 5 - service_level: 0.95 - SKU309: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 460 - max_stock: 1000 - price: 156 - sale_gamma: 92 - service_level: 0.95 - SKU31: - constraint: null - cost: 69 - init_stock: 280 - max_stock: 1000 - price: 120 - sale_gamma: 56 - service_level: 0.95 - SKU310: - constraint: null - cost: 234 - init_stock: 352 - max_stock: 1000 - price: 294 - sale_gamma: 44 - service_level: 0.95 - SKU311: - constraint: null - cost: 306 - init_stock: 400 - max_stock: 1000 - price: 437 - sale_gamma: 80 - service_level: 0.95 - SKU312: - constraint: G(stock_constraint) - cost: 291 - init_stock: 75 - max_stock: 1000 - price: 392 - sale_gamma: 25 - service_level: 0.95 - SKU313: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 324 - init_stock: 190 - max_stock: 1000 - price: 492 - sale_gamma: 38 - service_level: 0.95 - SKU314: - constraint: G(stock_constraint) - cost: 404 - init_stock: 116 - max_stock: 1000 - price: 456 - sale_gamma: 29 - service_level: 0.95 - SKU315: - constraint: G(stock_constraint) - cost: 471 - init_stock: 504 - max_stock: 1000 - price: 885 - sale_gamma: 84 - service_level: 0.95 - SKU316: - constraint: null - cost: 202 - init_stock: 90 - max_stock: 1000 - price: 282 - sale_gamma: 18 - service_level: 0.95 - SKU317: - constraint: null - cost: 216 - init_stock: 168 - max_stock: 1000 - price: 352 - sale_gamma: 24 - service_level: 0.95 - SKU318: - constraint: null - cost: 278 - init_stock: 255 - max_stock: 1000 - price: 350 - sale_gamma: 85 - service_level: 0.95 - SKU319: - constraint: null - cost: 257 - init_stock: 371 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU32: - constraint: null - cost: 336 - init_stock: 110 - max_stock: 1000 - price: 581 - sale_gamma: 22 - service_level: 0.95 - SKU320: - constraint: null - cost: 196 - init_stock: 156 - max_stock: 1000 - price: 258 - sale_gamma: 39 - service_level: 0.95 - SKU321: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 96 - max_stock: 1000 - price: 105 - sale_gamma: 16 - service_level: 0.95 - SKU322: - constraint: null - cost: 240 - init_stock: 600 - max_stock: 1000 - price: 453 - sale_gamma: 100 - service_level: 0.95 - SKU323: - constraint: G(stock_constraint) - cost: 66 - init_stock: 195 - max_stock: 1000 - price: 74 - sale_gamma: 39 - service_level: 0.95 - SKU324: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 442 - init_stock: 465 - max_stock: 1000 - price: 804 - sale_gamma: 93 - service_level: 0.95 - SKU325: - constraint: null - cost: 453 - init_stock: 345 - max_stock: 1000 - price: 579 - sale_gamma: 69 - service_level: 0.95 - SKU326: - constraint: null - cost: 345 - init_stock: 72 - max_stock: 1000 - price: 538 - sale_gamma: 24 - service_level: 0.95 - SKU327: - constraint: G(low_profit -> low_stock_constraint) - cost: 457 - init_stock: 84 - max_stock: 1000 - price: 749 - sale_gamma: 14 - service_level: 0.95 - SKU328: - constraint: null - cost: 390 - init_stock: 90 - max_stock: 1000 - price: 487 - sale_gamma: 45 - service_level: 0.95 - SKU329: - constraint: null - cost: 67 - init_stock: 84 - max_stock: 1000 - price: 126 - sale_gamma: 42 - service_level: 0.95 - SKU33: - constraint: G(low_profit -> low_stock_constraint) - cost: 416 - init_stock: 552 - max_stock: 1000 - price: 465 - sale_gamma: 92 - service_level: 0.95 - SKU330: - constraint: null - cost: 306 - init_stock: 273 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU331: - constraint: G(low_profit -> low_stock_constraint) - cost: 138 - init_stock: 164 - max_stock: 1000 - price: 180 - sale_gamma: 41 - service_level: 0.95 - SKU332: - constraint: null - cost: 390 - init_stock: 288 - max_stock: 1000 - price: 670 - sale_gamma: 96 - service_level: 0.95 - SKU333: - constraint: G(stock_constraint) - cost: 485 - init_stock: 318 - max_stock: 1000 - price: 800 - sale_gamma: 53 - service_level: 0.95 - SKU334: - constraint: null - cost: 183 - init_stock: 114 - max_stock: 1000 - price: 245 - sale_gamma: 57 - service_level: 0.95 - SKU335: - constraint: null - cost: 80 - init_stock: 243 - max_stock: 1000 - price: 141 - sale_gamma: 81 - service_level: 0.95 - SKU336: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 56 - max_stock: 1000 - price: 565 - sale_gamma: 28 - service_level: 0.95 - SKU337: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 394 - sale_gamma: 29 - service_level: 0.95 - SKU338: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 150 - sale_gamma: 81 - service_level: 0.95 - SKU339: - constraint: G(stock_constraint) - cost: 265 - init_stock: 441 - max_stock: 1000 - price: 368 - sale_gamma: 63 - service_level: 0.95 - SKU34: - constraint: null - cost: 95 - init_stock: 91 - max_stock: 1000 - price: 135 - sale_gamma: 13 - service_level: 0.95 - SKU340: - constraint: null - cost: 480 - init_stock: 435 - max_stock: 1000 - price: 633 - sale_gamma: 87 - service_level: 0.95 - SKU341: - constraint: G(low_profit -> low_stock_constraint) - cost: 462 - init_stock: 280 - max_stock: 1000 - price: 924 - sale_gamma: 70 - service_level: 0.95 - SKU342: - constraint: null - cost: 144 - init_stock: 72 - max_stock: 1000 - price: 216 - sale_gamma: 9 - service_level: 0.95 - SKU343: - constraint: null - cost: 310 - init_stock: 90 - max_stock: 1000 - price: 589 - sale_gamma: 15 - service_level: 0.95 - SKU344: - constraint: null - cost: 357 - init_stock: 348 - max_stock: 1000 - price: 442 - sale_gamma: 87 - service_level: 0.95 - SKU345: - constraint: null - cost: 442 - init_stock: 267 - max_stock: 1000 - price: 857 - sale_gamma: 89 - service_level: 0.95 - SKU346: - constraint: G(stock_constraint) - cost: 350 - init_stock: 336 - max_stock: 1000 - price: 549 - sale_gamma: 42 - service_level: 0.95 - SKU347: - constraint: G(low_profit -> low_stock_constraint) - cost: 497 - init_stock: 328 - max_stock: 1000 - price: 810 - sale_gamma: 82 - service_level: 0.95 - SKU348: - constraint: G(stock_constraint) - cost: 147 - init_stock: 100 - max_stock: 1000 - price: 277 - sale_gamma: 20 - service_level: 0.95 - SKU349: - constraint: null - cost: 67 - init_stock: 335 - max_stock: 1000 - price: 116 - sale_gamma: 67 - service_level: 0.95 - SKU35: - constraint: null - cost: 267 - init_stock: 430 - max_stock: 1000 - price: 373 - sale_gamma: 86 - service_level: 0.95 - SKU350: - constraint: G(stock_constraint) - cost: 279 - init_stock: 552 - max_stock: 1000 - price: 460 - sale_gamma: 92 - service_level: 0.95 - SKU351: - constraint: null - cost: 155 - init_stock: 335 - max_stock: 1000 - price: 294 - sale_gamma: 67 - service_level: 0.95 - SKU352: - constraint: null - cost: 71 - init_stock: 54 - max_stock: 1000 - price: 100 - sale_gamma: 18 - service_level: 0.95 - SKU353: - constraint: G(stock_constraint) - cost: 253 - init_stock: 465 - max_stock: 1000 - price: 437 - sale_gamma: 93 - service_level: 0.95 - SKU354: - constraint: G(low_profit -> low_stock_constraint) - cost: 396 - init_stock: 395 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU355: - constraint: G(stock_constraint) - cost: 63 - init_stock: 30 - max_stock: 1000 - price: 74 - sale_gamma: 10 - service_level: 0.95 - SKU356: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 87 - max_stock: 1000 - price: 441 - sale_gamma: 29 - service_level: 0.95 - SKU357: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 499 - init_stock: 460 - max_stock: 1000 - price: 868 - sale_gamma: 92 - service_level: 0.95 - SKU358: - constraint: null - cost: 269 - init_stock: 414 - max_stock: 1000 - price: 408 - sale_gamma: 69 - service_level: 0.95 - SKU359: - constraint: G(low_profit -> low_stock_constraint) - cost: 82 - init_stock: 600 - max_stock: 1000 - price: 110 - sale_gamma: 75 - service_level: 0.95 - SKU36: - constraint: null - cost: 105 - init_stock: 30 - max_stock: 1000 - price: 165 - sale_gamma: 10 - service_level: 0.95 - SKU360: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 75 - max_stock: 1000 - price: 682 - sale_gamma: 25 - service_level: 0.95 - SKU361: - constraint: null - cost: 163 - init_stock: 360 - max_stock: 1000 - price: 288 - sale_gamma: 90 - service_level: 0.95 - SKU362: - constraint: null - cost: 464 - init_stock: 435 - max_stock: 1000 - price: 867 - sale_gamma: 87 - service_level: 0.95 - SKU363: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 390 - max_stock: 1000 - price: 93 - sale_gamma: 78 - service_level: 0.95 - SKU364: - constraint: G(stock_constraint) - cost: 387 - init_stock: 66 - max_stock: 1000 - price: 472 - sale_gamma: 33 - service_level: 0.95 - SKU365: - constraint: null - cost: 158 - init_stock: 581 - max_stock: 1000 - price: 241 - sale_gamma: 83 - service_level: 0.95 - SKU366: - constraint: null - cost: 444 - init_stock: 344 - max_stock: 1000 - price: 639 - sale_gamma: 86 - service_level: 0.95 - SKU367: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 322 - max_stock: 1000 - price: 304 - sale_gamma: 46 - service_level: 0.95 - SKU368: - constraint: G(stock_constraint) - cost: 472 - init_stock: 198 - max_stock: 1000 - price: 613 - sale_gamma: 33 - service_level: 0.95 - SKU369: - constraint: null - cost: 96 - init_stock: 375 - max_stock: 1000 - price: 155 - sale_gamma: 75 - service_level: 0.95 - SKU37: - constraint: G(low_profit -> low_stock_constraint) - cost: 66 - init_stock: 177 - max_stock: 1000 - price: 110 - sale_gamma: 59 - service_level: 0.95 - SKU370: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 90 - max_stock: 1000 - price: 141 - sale_gamma: 15 - service_level: 0.95 - SKU371: - constraint: null - cost: 328 - init_stock: 25 - max_stock: 1000 - price: 511 - sale_gamma: 5 - service_level: 0.95 - SKU372: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 67 - init_stock: 192 - max_stock: 1000 - price: 134 - sale_gamma: 32 - service_level: 0.95 - SKU373: - constraint: null - cost: 58 - init_stock: 268 - max_stock: 1000 - price: 91 - sale_gamma: 67 - service_level: 0.95 - SKU374: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 216 - max_stock: 1000 - price: 73 - sale_gamma: 54 - service_level: 0.95 - SKU375: - constraint: G(low_profit -> low_stock_constraint) - cost: 283 - init_stock: 432 - max_stock: 1000 - price: 416 - sale_gamma: 72 - service_level: 0.95 - SKU376: - constraint: null - cost: 156 - init_stock: 164 - max_stock: 1000 - price: 291 - sale_gamma: 82 - service_level: 0.95 - SKU377: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 78 - init_stock: 536 - max_stock: 1000 - price: 134 - sale_gamma: 67 - service_level: 0.95 - SKU378: - constraint: G(low_profit -> low_stock_constraint) - cost: 424 - init_stock: 175 - max_stock: 1000 - price: 546 - sale_gamma: 35 - service_level: 0.95 - SKU379: - constraint: null - cost: 11 - init_stock: 196 - max_stock: 1000 - price: 20 - sale_gamma: 49 - service_level: 0.95 - SKU38: - constraint: null - cost: 344 - init_stock: 258 - max_stock: 1000 - price: 567 - sale_gamma: 43 - service_level: 0.95 - SKU380: - constraint: null - cost: 393 - init_stock: 212 - max_stock: 1000 - price: 605 - sale_gamma: 53 - service_level: 0.95 - SKU381: - constraint: null - cost: 476 - init_stock: 18 - max_stock: 1000 - price: 609 - sale_gamma: 6 - service_level: 0.95 - SKU382: - constraint: null - cost: 125 - init_stock: 426 - max_stock: 1000 - price: 177 - sale_gamma: 71 - service_level: 0.95 - SKU383: - constraint: null - cost: 304 - init_stock: 368 - max_stock: 1000 - price: 425 - sale_gamma: 92 - service_level: 0.95 - SKU384: - constraint: null - cost: 320 - init_stock: 54 - max_stock: 1000 - price: 460 - sale_gamma: 9 - service_level: 0.95 - SKU385: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 120 - init_stock: 91 - max_stock: 1000 - price: 184 - sale_gamma: 13 - service_level: 0.95 - SKU386: - constraint: G(stock_constraint) - cost: 295 - init_stock: 124 - max_stock: 1000 - price: 536 - sale_gamma: 31 - service_level: 0.95 - SKU387: - constraint: null - cost: 112 - init_stock: 582 - max_stock: 1000 - price: 154 - sale_gamma: 97 - service_level: 0.95 - SKU388: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 405 - init_stock: 264 - max_stock: 1000 - price: 635 - sale_gamma: 44 - service_level: 0.95 - SKU389: - constraint: G(stock_constraint) - cost: 376 - init_stock: 490 - max_stock: 1000 - price: 699 - sale_gamma: 70 - service_level: 0.95 - SKU39: - constraint: G(low_profit -> low_stock_constraint) - cost: 25 - init_stock: 204 - max_stock: 1000 - price: 45 - sale_gamma: 51 - service_level: 0.95 - SKU390: - constraint: null - cost: 144 - init_stock: 156 - max_stock: 1000 - price: 223 - sale_gamma: 39 - service_level: 0.95 - SKU391: - constraint: G(stock_constraint) - cost: 233 - init_stock: 402 - max_stock: 1000 - price: 403 - sale_gamma: 67 - service_level: 0.95 - SKU392: - constraint: null - cost: 163 - init_stock: 370 - max_stock: 1000 - price: 205 - sale_gamma: 74 - service_level: 0.95 - SKU393: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 402 - max_stock: 1000 - price: 701 - sale_gamma: 67 - service_level: 0.95 - SKU394: - constraint: null - cost: 154 - init_stock: 318 - max_stock: 1000 - price: 252 - sale_gamma: 53 - service_level: 0.95 - SKU395: - constraint: null - cost: 488 - init_stock: 198 - max_stock: 1000 - price: 854 - sale_gamma: 33 - service_level: 0.95 - SKU396: - constraint: null - cost: 333 - init_stock: 427 - max_stock: 1000 - price: 469 - sale_gamma: 61 - service_level: 0.95 - SKU397: - constraint: null - cost: 460 - init_stock: 153 - max_stock: 1000 - price: 791 - sale_gamma: 51 - service_level: 0.95 - SKU398: - constraint: G(stock_constraint) - cost: 234 - init_stock: 174 - max_stock: 1000 - price: 414 - sale_gamma: 58 - service_level: 0.95 - SKU399: - constraint: G(stock_constraint) - cost: 376 - init_stock: 148 - max_stock: 1000 - price: 612 - sale_gamma: 37 - service_level: 0.95 - SKU4: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 21 - max_stock: 1000 - price: 798 - sale_gamma: 7 - service_level: 0.95 - SKU40: - constraint: null - cost: 490 - init_stock: 594 - max_stock: 1000 - price: 563 - sale_gamma: 99 - service_level: 0.95 - SKU400: - constraint: G(low_profit -> low_stock_constraint) - cost: 445 - init_stock: 420 - max_stock: 1000 - price: 729 - sale_gamma: 84 - service_level: 0.95 - SKU401: - constraint: G(low_profit -> low_stock_constraint) - cost: 410 - init_stock: 312 - max_stock: 1000 - price: 774 - sale_gamma: 78 - service_level: 0.95 - SKU402: - constraint: G(low_profit -> low_stock_constraint) - cost: 86 - init_stock: 35 - max_stock: 1000 - price: 153 - sale_gamma: 7 - service_level: 0.95 - SKU403: - constraint: null - cost: 89 - init_stock: 594 - max_stock: 1000 - price: 133 - sale_gamma: 99 - service_level: 0.95 - SKU404: - constraint: null - cost: 287 - init_stock: 305 - max_stock: 1000 - price: 522 - sale_gamma: 61 - service_level: 0.95 - SKU405: - constraint: G(stock_constraint) - cost: 460 - init_stock: 114 - max_stock: 1000 - price: 584 - sale_gamma: 19 - service_level: 0.95 - SKU406: - constraint: null - cost: 327 - init_stock: 400 - max_stock: 1000 - price: 405 - sale_gamma: 100 - service_level: 0.95 - SKU407: - constraint: null - cost: 26 - init_stock: 184 - max_stock: 1000 - price: 41 - sale_gamma: 46 - service_level: 0.95 - SKU408: - constraint: null - cost: 444 - init_stock: 48 - max_stock: 1000 - price: 754 - sale_gamma: 8 - service_level: 0.95 - SKU409: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 257 - init_stock: 273 - max_stock: 1000 - price: 449 - sale_gamma: 91 - service_level: 0.95 - SKU41: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 141 - init_stock: 553 - max_stock: 1000 - price: 162 - sale_gamma: 79 - service_level: 0.95 - SKU410: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 70 - init_stock: 32 - max_stock: 1000 - price: 88 - sale_gamma: 16 - service_level: 0.95 - SKU411: - constraint: G(stock_constraint) - cost: 210 - init_stock: 380 - max_stock: 1000 - price: 405 - sale_gamma: 95 - service_level: 0.95 - SKU412: - constraint: null - cost: 286 - init_stock: 248 - max_stock: 1000 - price: 414 - sale_gamma: 62 - service_level: 0.95 - SKU413: - constraint: null - cost: 403 - init_stock: 581 - max_stock: 1000 - price: 801 - sale_gamma: 83 - service_level: 0.95 - SKU414: - constraint: G(stock_constraint) - cost: 165 - init_stock: 435 - max_stock: 1000 - price: 229 - sale_gamma: 87 - service_level: 0.95 - SKU415: - constraint: G(stock_constraint) - cost: 291 - init_stock: 184 - max_stock: 1000 - price: 372 - sale_gamma: 23 - service_level: 0.95 - SKU416: - constraint: null - cost: 228 - init_stock: 36 - max_stock: 1000 - price: 373 - sale_gamma: 9 - service_level: 0.95 - SKU417: - constraint: G(low_profit -> low_stock_constraint) - cost: 443 - init_stock: 288 - max_stock: 1000 - price: 872 - sale_gamma: 72 - service_level: 0.95 - SKU418: - constraint: null - cost: 458 - init_stock: 52 - max_stock: 1000 - price: 838 - sale_gamma: 13 - service_level: 0.95 - SKU419: - constraint: null - cost: 303 - init_stock: 712 - max_stock: 1000 - price: 448 - sale_gamma: 89 - service_level: 0.95 - SKU42: - constraint: null - cost: 462 - init_stock: 156 - max_stock: 1000 - price: 688 - sale_gamma: 26 - service_level: 0.95 - SKU420: - constraint: null - cost: 268 - init_stock: 84 - max_stock: 1000 - price: 506 - sale_gamma: 42 - service_level: 0.95 - SKU421: - constraint: G(stock_constraint) - cost: 214 - init_stock: 138 - max_stock: 1000 - price: 314 - sale_gamma: 46 - service_level: 0.95 - SKU422: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 432 - max_stock: 1000 - price: 97 - sale_gamma: 54 - service_level: 0.95 - SKU423: - constraint: G(low_profit -> low_stock_constraint) - cost: 188 - init_stock: 396 - max_stock: 1000 - price: 265 - sale_gamma: 66 - service_level: 0.95 - SKU424: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 66 - max_stock: 1000 - price: 317 - sale_gamma: 11 - service_level: 0.95 - SKU425: - constraint: G(stock_constraint) - cost: 162 - init_stock: 72 - max_stock: 1000 - price: 277 - sale_gamma: 12 - service_level: 0.95 - SKU426: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 125 - init_stock: 588 - max_stock: 1000 - price: 246 - sale_gamma: 98 - service_level: 0.95 - SKU427: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 413 - init_stock: 282 - max_stock: 1000 - price: 710 - sale_gamma: 94 - service_level: 0.95 - SKU428: - constraint: G(stock_constraint) - cost: 391 - init_stock: 378 - max_stock: 1000 - price: 629 - sale_gamma: 63 - service_level: 0.95 - SKU429: - constraint: G(stock_constraint) - cost: 39 - init_stock: 246 - max_stock: 1000 - price: 73 - sale_gamma: 41 - service_level: 0.95 - SKU43: - constraint: G(stock_constraint) - cost: 217 - init_stock: 272 - max_stock: 1000 - price: 353 - sale_gamma: 68 - service_level: 0.95 - SKU430: - constraint: null - cost: 216 - init_stock: 511 - max_stock: 1000 - price: 313 - sale_gamma: 73 - service_level: 0.95 - SKU431: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 328 - init_stock: 126 - max_stock: 1000 - price: 646 - sale_gamma: 21 - service_level: 0.95 - SKU432: - constraint: G(stock_constraint) - cost: 25 - init_stock: 368 - max_stock: 1000 - price: 35 - sale_gamma: 46 - service_level: 0.95 - SKU433: - constraint: null - cost: 348 - init_stock: 270 - max_stock: 1000 - price: 396 - sale_gamma: 45 - service_level: 0.95 - SKU434: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 90 - max_stock: 1000 - price: 40 - sale_gamma: 30 - service_level: 0.95 - SKU435: - constraint: G(stock_constraint) - cost: 272 - init_stock: 210 - max_stock: 1000 - price: 320 - sale_gamma: 30 - service_level: 0.95 - SKU436: - constraint: null - cost: 72 - init_stock: 405 - max_stock: 1000 - price: 105 - sale_gamma: 81 - service_level: 0.95 - SKU437: - constraint: G(stock_constraint) - cost: 115 - init_stock: 84 - max_stock: 1000 - price: 223 - sale_gamma: 14 - service_level: 0.95 - SKU438: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 40 - max_stock: 1000 - price: 190 - sale_gamma: 10 - service_level: 0.95 - SKU439: - constraint: G(stock_constraint) - cost: 256 - init_stock: 190 - max_stock: 1000 - price: 304 - sale_gamma: 38 - service_level: 0.95 - SKU44: - constraint: null - cost: 360 - init_stock: 240 - max_stock: 1000 - price: 669 - sale_gamma: 48 - service_level: 0.95 - SKU440: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 248 - init_stock: 246 - max_stock: 1000 - price: 357 - sale_gamma: 82 - service_level: 0.95 - SKU441: - constraint: null - cost: 253 - init_stock: 224 - max_stock: 1000 - price: 374 - sale_gamma: 56 - service_level: 0.95 - SKU442: - constraint: null - cost: 470 - init_stock: 352 - max_stock: 1000 - price: 629 - sale_gamma: 88 - service_level: 0.95 - SKU443: - constraint: null - cost: 218 - init_stock: 365 - max_stock: 1000 - price: 361 - sale_gamma: 73 - service_level: 0.95 - SKU444: - constraint: null - cost: 156 - init_stock: 18 - max_stock: 1000 - price: 182 - sale_gamma: 6 - service_level: 0.95 - SKU445: - constraint: null - cost: 260 - init_stock: 602 - max_stock: 1000 - price: 296 - sale_gamma: 86 - service_level: 0.95 - SKU446: - constraint: null - cost: 497 - init_stock: 147 - max_stock: 1000 - price: 690 - sale_gamma: 49 - service_level: 0.95 - SKU447: - constraint: null - cost: 196 - init_stock: 30 - max_stock: 1000 - price: 280 - sale_gamma: 5 - service_level: 0.95 - SKU448: - constraint: null - cost: 485 - init_stock: 497 - max_stock: 1000 - price: 742 - sale_gamma: 71 - service_level: 0.95 - SKU449: - constraint: null - cost: 282 - init_stock: 114 - max_stock: 1000 - price: 360 - sale_gamma: 38 - service_level: 0.95 - SKU45: - constraint: G(stock_constraint) - cost: 253 - init_stock: 30 - max_stock: 1000 - price: 351 - sale_gamma: 10 - service_level: 0.95 - SKU450: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 58 - init_stock: 294 - max_stock: 1000 - price: 77 - sale_gamma: 98 - service_level: 0.95 - SKU451: - constraint: null - cost: 468 - init_stock: 483 - max_stock: 1000 - price: 851 - sale_gamma: 69 - service_level: 0.95 - SKU452: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 63 - init_stock: 395 - max_stock: 1000 - price: 97 - sale_gamma: 79 - service_level: 0.95 - SKU453: - constraint: null - cost: 37 - init_stock: 140 - max_stock: 1000 - price: 65 - sale_gamma: 35 - service_level: 0.95 - SKU454: - constraint: G(low_profit -> low_stock_constraint) - cost: 494 - init_stock: 340 - max_stock: 1000 - price: 899 - sale_gamma: 85 - service_level: 0.95 - SKU455: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 136 - init_stock: 182 - max_stock: 1000 - price: 195 - sale_gamma: 26 - service_level: 0.95 - SKU456: - constraint: G(stock_constraint) - cost: 338 - init_stock: 75 - max_stock: 1000 - price: 659 - sale_gamma: 25 - service_level: 0.95 - SKU457: - constraint: null - cost: 169 - init_stock: 256 - max_stock: 1000 - price: 190 - sale_gamma: 64 - service_level: 0.95 - SKU458: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 403 - init_stock: 28 - max_stock: 1000 - price: 685 - sale_gamma: 7 - service_level: 0.95 - SKU459: - constraint: null - cost: 425 - init_stock: 272 - max_stock: 1000 - price: 731 - sale_gamma: 34 - service_level: 0.95 - SKU46: - constraint: G(low_profit -> low_stock_constraint) - cost: 299 - init_stock: 200 - max_stock: 1000 - price: 409 - sale_gamma: 50 - service_level: 0.95 - SKU460: - constraint: G(low_profit -> low_stock_constraint) - cost: 405 - init_stock: 292 - max_stock: 1000 - price: 558 - sale_gamma: 73 - service_level: 0.95 - SKU461: - constraint: G(stock_constraint) - cost: 251 - init_stock: 240 - max_stock: 1000 - price: 326 - sale_gamma: 48 - service_level: 0.95 - SKU462: - constraint: G(low_profit -> low_stock_constraint) - cost: 448 - init_stock: 36 - max_stock: 1000 - price: 757 - sale_gamma: 9 - service_level: 0.95 - SKU463: - constraint: null - cost: 187 - init_stock: 574 - max_stock: 1000 - price: 213 - sale_gamma: 82 - service_level: 0.95 - SKU464: - constraint: null - cost: 279 - init_stock: 272 - max_stock: 1000 - price: 438 - sale_gamma: 34 - service_level: 0.95 - SKU465: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 147 - init_stock: 400 - max_stock: 1000 - price: 288 - sale_gamma: 100 - service_level: 0.95 - SKU466: - constraint: G(stock_constraint) - cost: 97 - init_stock: 126 - max_stock: 1000 - price: 167 - sale_gamma: 42 - service_level: 0.95 - SKU467: - constraint: G(stock_constraint) - cost: 142 - init_stock: 252 - max_stock: 1000 - price: 230 - sale_gamma: 36 - service_level: 0.95 - SKU468: - constraint: G(stock_constraint) - cost: 55 - init_stock: 250 - max_stock: 1000 - price: 104 - sale_gamma: 50 - service_level: 0.95 - SKU469: - constraint: G(stock_constraint) - cost: 178 - init_stock: 105 - max_stock: 1000 - price: 331 - sale_gamma: 15 - service_level: 0.95 - SKU47: - constraint: G(stock_constraint) - cost: 121 - init_stock: 117 - max_stock: 1000 - price: 240 - sale_gamma: 39 - service_level: 0.95 - SKU470: - constraint: G(low_profit -> low_stock_constraint) - cost: 373 - init_stock: 128 - max_stock: 1000 - price: 548 - sale_gamma: 32 - service_level: 0.95 - SKU471: - constraint: G(low_profit -> low_stock_constraint) - cost: 315 - init_stock: 568 - max_stock: 1000 - price: 387 - sale_gamma: 71 - service_level: 0.95 - SKU472: - constraint: null - cost: 421 - init_stock: 177 - max_stock: 1000 - price: 627 - sale_gamma: 59 - service_level: 0.95 - SKU473: - constraint: G(low_profit -> low_stock_constraint) - cost: 195 - init_stock: 105 - max_stock: 1000 - price: 323 - sale_gamma: 21 - service_level: 0.95 - SKU474: - constraint: null - cost: 448 - init_stock: 602 - max_stock: 1000 - price: 775 - sale_gamma: 86 - service_level: 0.95 - SKU475: - constraint: null - cost: 34 - init_stock: 282 - max_stock: 1000 - price: 62 - sale_gamma: 94 - service_level: 0.95 - SKU476: - constraint: null - cost: 251 - init_stock: 180 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU477: - constraint: null - cost: 289 - init_stock: 329 - max_stock: 1000 - price: 338 - sale_gamma: 47 - service_level: 0.95 - SKU478: - constraint: null - cost: 479 - init_stock: 352 - max_stock: 1000 - price: 723 - sale_gamma: 88 - service_level: 0.95 - SKU479: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 366 - init_stock: 152 - max_stock: 1000 - price: 552 - sale_gamma: 38 - service_level: 0.95 - SKU48: - constraint: null - cost: 340 - init_stock: 232 - max_stock: 1000 - price: 387 - sale_gamma: 58 - service_level: 0.95 - SKU480: - constraint: G(stock_constraint) - cost: 18 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 30 - service_level: 0.95 - SKU481: - constraint: null - cost: 330 - init_stock: 352 - max_stock: 1000 - price: 422 - sale_gamma: 88 - service_level: 0.95 - SKU482: - constraint: G(stock_constraint) - cost: 94 - init_stock: 696 - max_stock: 1000 - price: 108 - sale_gamma: 87 - service_level: 0.95 - SKU483: - constraint: null - cost: 298 - init_stock: 170 - max_stock: 1000 - price: 533 - sale_gamma: 34 - service_level: 0.95 - SKU484: - constraint: null - cost: 84 - init_stock: 365 - max_stock: 1000 - price: 117 - sale_gamma: 73 - service_level: 0.95 - SKU485: - constraint: null - cost: 318 - init_stock: 536 - max_stock: 1000 - price: 543 - sale_gamma: 67 - service_level: 0.95 - SKU486: - constraint: G(low_profit -> low_stock_constraint) - cost: 122 - init_stock: 208 - max_stock: 1000 - price: 161 - sale_gamma: 52 - service_level: 0.95 - SKU487: - constraint: null - cost: 277 - init_stock: 264 - max_stock: 1000 - price: 362 - sale_gamma: 66 - service_level: 0.95 - SKU488: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 189 - max_stock: 1000 - price: 42 - sale_gamma: 27 - service_level: 0.95 - SKU489: - constraint: null - cost: 59 - init_stock: 124 - max_stock: 1000 - price: 97 - sale_gamma: 31 - service_level: 0.95 - SKU49: - constraint: null - cost: 263 - init_stock: 760 - max_stock: 1000 - price: 515 - sale_gamma: 95 - service_level: 0.95 - SKU490: - constraint: null - cost: 161 - init_stock: 486 - max_stock: 1000 - price: 264 - sale_gamma: 81 - service_level: 0.95 - SKU491: - constraint: null - cost: 444 - init_stock: 450 - max_stock: 1000 - price: 834 - sale_gamma: 75 - service_level: 0.95 - SKU492: - constraint: null - cost: 53 - init_stock: 240 - max_stock: 1000 - price: 63 - sale_gamma: 80 - service_level: 0.95 - SKU493: - constraint: null - cost: 461 - init_stock: 81 - max_stock: 1000 - price: 567 - sale_gamma: 27 - service_level: 0.95 - SKU494: - constraint: null - cost: 145 - init_stock: 282 - max_stock: 1000 - price: 242 - sale_gamma: 94 - service_level: 0.95 - SKU495: - constraint: G(stock_constraint) - cost: 149 - init_stock: 372 - max_stock: 1000 - price: 217 - sale_gamma: 62 - service_level: 0.95 - SKU496: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 342 - init_stock: 295 - max_stock: 1000 - price: 492 - sale_gamma: 59 - service_level: 0.95 - SKU497: - constraint: G(low_profit -> low_stock_constraint) - cost: 33 - init_stock: 45 - max_stock: 1000 - price: 42 - sale_gamma: 9 - service_level: 0.95 - SKU498: - constraint: null - cost: 305 - init_stock: 325 - max_stock: 1000 - price: 439 - sale_gamma: 65 - service_level: 0.95 - SKU499: - constraint: G(stock_constraint) - cost: 307 - init_stock: 174 - max_stock: 1000 - price: 472 - sale_gamma: 29 - service_level: 0.95 - SKU5: - constraint: G(stock_constraint) - cost: 466 - init_stock: 426 - max_stock: 1000 - price: 852 - sale_gamma: 71 - service_level: 0.95 - SKU50: - constraint: null - cost: 282 - init_stock: 185 - max_stock: 1000 - price: 516 - sale_gamma: 37 - service_level: 0.95 - SKU500: - constraint: null - cost: 208 - init_stock: 336 - max_stock: 1000 - price: 255 - sale_gamma: 42 - service_level: 0.95 - SKU501: - constraint: null - cost: 194 - init_stock: 152 - max_stock: 1000 - price: 265 - sale_gamma: 76 - service_level: 0.95 - SKU502: - constraint: null - cost: 69 - init_stock: 390 - max_stock: 1000 - price: 101 - sale_gamma: 65 - service_level: 0.95 - SKU503: - constraint: G(stock_constraint) - cost: 208 - init_stock: 228 - max_stock: 1000 - price: 280 - sale_gamma: 57 - service_level: 0.95 - SKU504: - constraint: null - cost: 251 - init_stock: 210 - max_stock: 1000 - price: 426 - sale_gamma: 30 - service_level: 0.95 - SKU505: - constraint: null - cost: 426 - init_stock: 295 - max_stock: 1000 - price: 515 - sale_gamma: 59 - service_level: 0.95 - SKU506: - constraint: null - cost: 149 - init_stock: 465 - max_stock: 1000 - price: 220 - sale_gamma: 93 - service_level: 0.95 - SKU507: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 187 - init_stock: 60 - max_stock: 1000 - price: 293 - sale_gamma: 15 - service_level: 0.95 - SKU508: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 576 - max_stock: 1000 - price: 432 - sale_gamma: 96 - service_level: 0.95 - SKU509: - constraint: G(low_profit -> low_stock_constraint) - cost: 297 - init_stock: 486 - max_stock: 1000 - price: 397 - sale_gamma: 81 - service_level: 0.95 - SKU51: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 469 - max_stock: 1000 - price: 477 - sale_gamma: 67 - service_level: 0.95 - SKU510: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 36 - max_stock: 1000 - price: 156 - sale_gamma: 9 - service_level: 0.95 - SKU511: - constraint: G(stock_constraint) - cost: 361 - init_stock: 450 - max_stock: 1000 - price: 480 - sale_gamma: 75 - service_level: 0.95 - SKU512: - constraint: null - cost: 467 - init_stock: 132 - max_stock: 1000 - price: 854 - sale_gamma: 22 - service_level: 0.95 - SKU513: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 343 - init_stock: 42 - max_stock: 1000 - price: 631 - sale_gamma: 7 - service_level: 0.95 - SKU514: - constraint: null - cost: 186 - init_stock: 504 - max_stock: 1000 - price: 332 - sale_gamma: 63 - service_level: 0.95 - SKU515: - constraint: null - cost: 145 - init_stock: 216 - max_stock: 1000 - price: 227 - sale_gamma: 54 - service_level: 0.95 - SKU516: - constraint: G(stock_constraint) - cost: 64 - init_stock: 114 - max_stock: 1000 - price: 96 - sale_gamma: 38 - service_level: 0.95 - SKU517: - constraint: null - cost: 117 - init_stock: 45 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU518: - constraint: G(stock_constraint) - cost: 26 - init_stock: 147 - max_stock: 1000 - price: 30 - sale_gamma: 21 - service_level: 0.95 - SKU519: - constraint: null - cost: 233 - init_stock: 250 - max_stock: 1000 - price: 410 - sale_gamma: 50 - service_level: 0.95 - SKU52: - constraint: null - cost: 22 - init_stock: 402 - max_stock: 1000 - price: 28 - sale_gamma: 67 - service_level: 0.95 - SKU520: - constraint: G(low_profit -> low_stock_constraint) - cost: 209 - init_stock: 44 - max_stock: 1000 - price: 248 - sale_gamma: 22 - service_level: 0.95 - SKU521: - constraint: G(low_profit -> low_stock_constraint) - cost: 220 - init_stock: 350 - max_stock: 1000 - price: 330 - sale_gamma: 50 - service_level: 0.95 - SKU522: - constraint: null - cost: 156 - init_stock: 522 - max_stock: 1000 - price: 277 - sale_gamma: 87 - service_level: 0.95 - SKU523: - constraint: null - cost: 65 - init_stock: 500 - max_stock: 1000 - price: 107 - sale_gamma: 100 - service_level: 0.95 - SKU524: - constraint: null - cost: 294 - init_stock: 188 - max_stock: 1000 - price: 464 - sale_gamma: 94 - service_level: 0.95 - SKU525: - constraint: G(stock_constraint) - cost: 432 - init_stock: 126 - max_stock: 1000 - price: 751 - sale_gamma: 42 - service_level: 0.95 - SKU526: - constraint: null - cost: 419 - init_stock: 168 - max_stock: 1000 - price: 716 - sale_gamma: 24 - service_level: 0.95 - SKU527: - constraint: null - cost: 131 - init_stock: 350 - max_stock: 1000 - price: 262 - sale_gamma: 70 - service_level: 0.95 - SKU528: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 84 - max_stock: 1000 - price: 581 - sale_gamma: 21 - service_level: 0.95 - SKU529: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 248 - max_stock: 1000 - price: 375 - sale_gamma: 31 - service_level: 0.95 - SKU53: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 151 - init_stock: 490 - max_stock: 1000 - price: 191 - sale_gamma: 70 - service_level: 0.95 - SKU530: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 135 - max_stock: 1000 - price: 205 - sale_gamma: 45 - service_level: 0.95 - SKU531: - constraint: null - cost: 103 - init_stock: 300 - max_stock: 1000 - price: 153 - sale_gamma: 60 - service_level: 0.95 - SKU532: - constraint: null - cost: 351 - init_stock: 539 - max_stock: 1000 - price: 431 - sale_gamma: 77 - service_level: 0.95 - SKU533: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 168 - max_stock: 1000 - price: 340 - sale_gamma: 42 - service_level: 0.95 - SKU534: - constraint: null - cost: 425 - init_stock: 82 - max_stock: 1000 - price: 548 - sale_gamma: 41 - service_level: 0.95 - SKU535: - constraint: null - cost: 304 - init_stock: 430 - max_stock: 1000 - price: 465 - sale_gamma: 86 - service_level: 0.95 - SKU536: - constraint: null - cost: 358 - init_stock: 208 - max_stock: 1000 - price: 415 - sale_gamma: 52 - service_level: 0.95 - SKU537: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 321 - init_stock: 45 - max_stock: 1000 - price: 455 - sale_gamma: 9 - service_level: 0.95 - SKU538: - constraint: null - cost: 457 - init_stock: 200 - max_stock: 1000 - price: 868 - sale_gamma: 50 - service_level: 0.95 - SKU539: - constraint: G(stock_constraint) - cost: 165 - init_stock: 234 - max_stock: 1000 - price: 229 - sale_gamma: 78 - service_level: 0.95 - SKU54: - constraint: G(low_profit -> low_stock_constraint) - cost: 158 - init_stock: 138 - max_stock: 1000 - price: 241 - sale_gamma: 46 - service_level: 0.95 - SKU540: - constraint: null - cost: 388 - init_stock: 294 - max_stock: 1000 - price: 496 - sale_gamma: 42 - service_level: 0.95 - SKU541: - constraint: null - cost: 131 - init_stock: 203 - max_stock: 1000 - price: 213 - sale_gamma: 29 - service_level: 0.95 - SKU542: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 130 - max_stock: 1000 - price: 42 - sale_gamma: 26 - service_level: 0.95 - SKU543: - constraint: G(stock_constraint) - cost: 430 - init_stock: 510 - max_stock: 1000 - price: 718 - sale_gamma: 85 - service_level: 0.95 - SKU544: - constraint: G(stock_constraint) - cost: 346 - init_stock: 194 - max_stock: 1000 - price: 474 - sale_gamma: 97 - service_level: 0.95 - SKU545: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU546: - constraint: null - cost: 491 - init_stock: 576 - max_stock: 1000 - price: 765 - sale_gamma: 96 - service_level: 0.95 - SKU547: - constraint: G(stock_constraint) - cost: 161 - init_stock: 264 - max_stock: 1000 - price: 251 - sale_gamma: 44 - service_level: 0.95 - SKU548: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 111 - init_stock: 36 - max_stock: 1000 - price: 217 - sale_gamma: 6 - service_level: 0.95 - SKU549: - constraint: G(stock_constraint) - cost: 387 - init_stock: 316 - max_stock: 1000 - price: 510 - sale_gamma: 79 - service_level: 0.95 - SKU55: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 455 - max_stock: 1000 - price: 896 - sale_gamma: 65 - service_level: 0.95 - SKU550: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 259 - init_stock: 282 - max_stock: 1000 - price: 502 - sale_gamma: 94 - service_level: 0.95 - SKU551: - constraint: null - cost: 46 - init_stock: 186 - max_stock: 1000 - price: 60 - sale_gamma: 31 - service_level: 0.95 - SKU552: - constraint: null - cost: 191 - init_stock: 450 - max_stock: 1000 - price: 315 - sale_gamma: 90 - service_level: 0.95 - SKU553: - constraint: G(low_profit -> low_stock_constraint) - cost: 208 - init_stock: 60 - max_stock: 1000 - price: 251 - sale_gamma: 30 - service_level: 0.95 - SKU554: - constraint: null - cost: 340 - init_stock: 82 - max_stock: 1000 - price: 387 - sale_gamma: 41 - service_level: 0.95 - SKU555: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 252 - max_stock: 1000 - price: 684 - sale_gamma: 84 - service_level: 0.95 - SKU556: - constraint: G(stock_constraint) - cost: 110 - init_stock: 90 - max_stock: 1000 - price: 213 - sale_gamma: 30 - service_level: 0.95 - SKU557: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 334 - init_stock: 365 - max_stock: 1000 - price: 661 - sale_gamma: 73 - service_level: 0.95 - SKU558: - constraint: null - cost: 399 - init_stock: 85 - max_stock: 1000 - price: 490 - sale_gamma: 17 - service_level: 0.95 - SKU559: - constraint: null - cost: 313 - init_stock: 270 - max_stock: 1000 - price: 591 - sale_gamma: 54 - service_level: 0.95 - SKU56: - constraint: null - cost: 377 - init_stock: 222 - max_stock: 1000 - price: 671 - sale_gamma: 74 - service_level: 0.95 - SKU560: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 283 - init_stock: 246 - max_stock: 1000 - price: 458 - sale_gamma: 41 - service_level: 0.95 - SKU561: - constraint: null - cost: 202 - init_stock: 312 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU562: - constraint: null - cost: 347 - init_stock: 356 - max_stock: 1000 - price: 666 - sale_gamma: 89 - service_level: 0.95 - SKU563: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 401 - init_stock: 30 - max_stock: 1000 - price: 509 - sale_gamma: 5 - service_level: 0.95 - SKU564: - constraint: null - cost: 109 - init_stock: 63 - max_stock: 1000 - price: 183 - sale_gamma: 9 - service_level: 0.95 - SKU565: - constraint: null - cost: 57 - init_stock: 525 - max_stock: 1000 - price: 90 - sale_gamma: 75 - service_level: 0.95 - SKU566: - constraint: null - cost: 131 - init_stock: 44 - max_stock: 1000 - price: 165 - sale_gamma: 11 - service_level: 0.95 - SKU567: - constraint: G(stock_constraint) - cost: 361 - init_stock: 27 - max_stock: 1000 - price: 465 - sale_gamma: 9 - service_level: 0.95 - SKU568: - constraint: null - cost: 75 - init_stock: 234 - max_stock: 1000 - price: 102 - sale_gamma: 78 - service_level: 0.95 - SKU569: - constraint: null - cost: 473 - init_stock: 192 - max_stock: 1000 - price: 591 - sale_gamma: 48 - service_level: 0.95 - SKU57: - constraint: G(stock_constraint) - cost: 359 - init_stock: 350 - max_stock: 1000 - price: 682 - sale_gamma: 50 - service_level: 0.95 - SKU570: - constraint: null - cost: 388 - init_stock: 581 - max_stock: 1000 - price: 686 - sale_gamma: 83 - service_level: 0.95 - SKU571: - constraint: G(stock_constraint) - cost: 168 - init_stock: 78 - max_stock: 1000 - price: 208 - sale_gamma: 39 - service_level: 0.95 - SKU572: - constraint: null - cost: 26 - init_stock: 225 - max_stock: 1000 - price: 41 - sale_gamma: 45 - service_level: 0.95 - SKU573: - constraint: null - cost: 246 - init_stock: 164 - max_stock: 1000 - price: 391 - sale_gamma: 41 - service_level: 0.95 - SKU574: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 150 - max_stock: 1000 - price: 166 - sale_gamma: 50 - service_level: 0.95 - SKU575: - constraint: null - cost: 237 - init_stock: 237 - max_stock: 1000 - price: 438 - sale_gamma: 79 - service_level: 0.95 - SKU576: - constraint: G(stock_constraint) - cost: 265 - init_stock: 468 - max_stock: 1000 - price: 416 - sale_gamma: 78 - service_level: 0.95 - SKU577: - constraint: G(low_profit -> low_stock_constraint) - cost: 18 - init_stock: 348 - max_stock: 1000 - price: 24 - sale_gamma: 87 - service_level: 0.95 - SKU578: - constraint: null - cost: 100 - init_stock: 284 - max_stock: 1000 - price: 148 - sale_gamma: 71 - service_level: 0.95 - SKU579: - constraint: null - cost: 415 - init_stock: 27 - max_stock: 1000 - price: 771 - sale_gamma: 9 - service_level: 0.95 - SKU58: - constraint: null - cost: 362 - init_stock: 240 - max_stock: 1000 - price: 521 - sale_gamma: 48 - service_level: 0.95 - SKU580: - constraint: null - cost: 203 - init_stock: 15 - max_stock: 1000 - price: 261 - sale_gamma: 5 - service_level: 0.95 - SKU581: - constraint: null - cost: 152 - init_stock: 516 - max_stock: 1000 - price: 185 - sale_gamma: 86 - service_level: 0.95 - SKU582: - constraint: G(stock_constraint) - cost: 359 - init_stock: 480 - max_stock: 1000 - price: 567 - sale_gamma: 96 - service_level: 0.95 - SKU583: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 86 - init_stock: 261 - max_stock: 1000 - price: 98 - sale_gamma: 87 - service_level: 0.95 - SKU584: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 33 - init_stock: 270 - max_stock: 1000 - price: 36 - sale_gamma: 45 - service_level: 0.95 - SKU585: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 53 - sale_gamma: 20 - service_level: 0.95 - SKU586: - constraint: G(stock_constraint) - cost: 61 - init_stock: 264 - max_stock: 1000 - price: 94 - sale_gamma: 44 - service_level: 0.95 - SKU587: - constraint: null - cost: 290 - init_stock: 468 - max_stock: 1000 - price: 423 - sale_gamma: 78 - service_level: 0.95 - SKU588: - constraint: G(stock_constraint) - cost: 476 - init_stock: 176 - max_stock: 1000 - price: 885 - sale_gamma: 44 - service_level: 0.95 - SKU589: - constraint: null - cost: 244 - init_stock: 170 - max_stock: 1000 - price: 380 - sale_gamma: 34 - service_level: 0.95 - SKU59: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 255 - max_stock: 1000 - price: 275 - sale_gamma: 51 - service_level: 0.95 - SKU590: - constraint: null - cost: 70 - init_stock: 93 - max_stock: 1000 - price: 103 - sale_gamma: 31 - service_level: 0.95 - SKU591: - constraint: G(stock_constraint) - cost: 206 - init_stock: 504 - max_stock: 1000 - price: 298 - sale_gamma: 84 - service_level: 0.95 - SKU592: - constraint: null - cost: 218 - init_stock: 276 - max_stock: 1000 - price: 418 - sale_gamma: 46 - service_level: 0.95 - SKU593: - constraint: null - cost: 124 - init_stock: 88 - max_stock: 1000 - price: 221 - sale_gamma: 44 - service_level: 0.95 - SKU594: - constraint: null - cost: 238 - init_stock: 150 - max_stock: 1000 - price: 459 - sale_gamma: 50 - service_level: 0.95 - SKU595: - constraint: null - cost: 73 - init_stock: 172 - max_stock: 1000 - price: 107 - sale_gamma: 43 - service_level: 0.95 - SKU596: - constraint: null - cost: 432 - init_stock: 35 - max_stock: 1000 - price: 540 - sale_gamma: 7 - service_level: 0.95 - SKU597: - constraint: G(stock_constraint) - cost: 252 - init_stock: 435 - max_stock: 1000 - price: 451 - sale_gamma: 87 - service_level: 0.95 - SKU598: - constraint: null - cost: 348 - init_stock: 28 - max_stock: 1000 - price: 612 - sale_gamma: 14 - service_level: 0.95 - SKU599: - constraint: null - cost: 54 - init_stock: 204 - max_stock: 1000 - price: 66 - sale_gamma: 68 - service_level: 0.95 - SKU6: - constraint: null - cost: 122 - init_stock: 616 - max_stock: 1000 - price: 195 - sale_gamma: 88 - service_level: 0.95 - SKU60: - constraint: G(stock_constraint) - cost: 200 - init_stock: 234 - max_stock: 1000 - price: 346 - sale_gamma: 39 - service_level: 0.95 - SKU600: - constraint: null - cost: 386 - init_stock: 325 - max_stock: 1000 - price: 505 - sale_gamma: 65 - service_level: 0.95 - SKU601: - constraint: null - cost: 138 - init_stock: 273 - max_stock: 1000 - price: 198 - sale_gamma: 39 - service_level: 0.95 - SKU602: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 184 - init_stock: 588 - max_stock: 1000 - price: 318 - sale_gamma: 98 - service_level: 0.95 - SKU603: - constraint: G(low_profit -> low_stock_constraint) - cost: 278 - init_stock: 252 - max_stock: 1000 - price: 550 - sale_gamma: 42 - service_level: 0.95 - SKU604: - constraint: G(stock_constraint) - cost: 270 - init_stock: 400 - max_stock: 1000 - price: 378 - sale_gamma: 50 - service_level: 0.95 - SKU605: - constraint: null - cost: 288 - init_stock: 120 - max_stock: 1000 - price: 443 - sale_gamma: 24 - service_level: 0.95 - SKU606: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 114 - init_stock: 66 - max_stock: 1000 - price: 161 - sale_gamma: 11 - service_level: 0.95 - SKU607: - constraint: null - cost: 208 - init_stock: 395 - max_stock: 1000 - price: 359 - sale_gamma: 79 - service_level: 0.95 - SKU608: - constraint: null - cost: 39 - init_stock: 365 - max_stock: 1000 - price: 49 - sale_gamma: 73 - service_level: 0.95 - SKU609: - constraint: null - cost: 102 - init_stock: 114 - max_stock: 1000 - price: 171 - sale_gamma: 19 - service_level: 0.95 - SKU61: - constraint: null - cost: 461 - init_stock: 150 - max_stock: 1000 - price: 645 - sale_gamma: 75 - service_level: 0.95 - SKU610: - constraint: null - cost: 449 - init_stock: 204 - max_stock: 1000 - price: 749 - sale_gamma: 68 - service_level: 0.95 - SKU611: - constraint: G(low_profit -> low_stock_constraint) - cost: 306 - init_stock: 539 - max_stock: 1000 - price: 489 - sale_gamma: 77 - service_level: 0.95 - SKU612: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 391 - init_stock: 616 - max_stock: 1000 - price: 613 - sale_gamma: 88 - service_level: 0.95 - SKU613: - constraint: G(stock_constraint) - cost: 174 - init_stock: 56 - max_stock: 1000 - price: 254 - sale_gamma: 7 - service_level: 0.95 - SKU614: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 45 - max_stock: 1000 - price: 318 - sale_gamma: 15 - service_level: 0.95 - SKU615: - constraint: G(stock_constraint) - cost: 330 - init_stock: 364 - max_stock: 1000 - price: 432 - sale_gamma: 91 - service_level: 0.95 - SKU616: - constraint: G(low_profit -> low_stock_constraint) - cost: 232 - init_stock: 219 - max_stock: 1000 - price: 382 - sale_gamma: 73 - service_level: 0.95 - SKU617: - constraint: null - cost: 203 - init_stock: 420 - max_stock: 1000 - price: 227 - sale_gamma: 60 - service_level: 0.95 - SKU618: - constraint: G(stock_constraint) - cost: 77 - init_stock: 138 - max_stock: 1000 - price: 97 - sale_gamma: 23 - service_level: 0.95 - SKU619: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 344 - max_stock: 1000 - price: 359 - sale_gamma: 86 - service_level: 0.95 - SKU62: - constraint: null - cost: 124 - init_stock: 36 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU620: - constraint: null - cost: 231 - init_stock: 156 - max_stock: 1000 - price: 267 - sale_gamma: 39 - service_level: 0.95 - SKU621: - constraint: null - cost: 199 - init_stock: 216 - max_stock: 1000 - price: 332 - sale_gamma: 72 - service_level: 0.95 - SKU622: - constraint: null - cost: 253 - init_stock: 455 - max_stock: 1000 - price: 468 - sale_gamma: 65 - service_level: 0.95 - SKU623: - constraint: null - cost: 335 - init_stock: 220 - max_stock: 1000 - price: 368 - sale_gamma: 55 - service_level: 0.95 - SKU624: - constraint: null - cost: 482 - init_stock: 270 - max_stock: 1000 - price: 824 - sale_gamma: 54 - service_level: 0.95 - SKU625: - constraint: null - cost: 46 - init_stock: 445 - max_stock: 1000 - price: 60 - sale_gamma: 89 - service_level: 0.95 - SKU626: - constraint: null - cost: 451 - init_stock: 534 - max_stock: 1000 - price: 694 - sale_gamma: 89 - service_level: 0.95 - SKU627: - constraint: null - cost: 220 - init_stock: 656 - max_stock: 1000 - price: 264 - sale_gamma: 82 - service_level: 0.95 - SKU628: - constraint: null - cost: 124 - init_stock: 156 - max_stock: 1000 - price: 229 - sale_gamma: 26 - service_level: 0.95 - SKU629: - constraint: G(stock_constraint) - cost: 353 - init_stock: 460 - max_stock: 1000 - price: 515 - sale_gamma: 92 - service_level: 0.95 - SKU63: - constraint: null - cost: 68 - init_stock: 70 - max_stock: 1000 - price: 86 - sale_gamma: 14 - service_level: 0.95 - SKU630: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 122 - init_stock: 34 - max_stock: 1000 - price: 137 - sale_gamma: 17 - service_level: 0.95 - SKU631: - constraint: null - cost: 296 - init_stock: 196 - max_stock: 1000 - price: 399 - sale_gamma: 49 - service_level: 0.95 - SKU632: - constraint: null - cost: 85 - init_stock: 470 - max_stock: 1000 - price: 141 - sale_gamma: 94 - service_level: 0.95 - SKU633: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 402 - max_stock: 1000 - price: 228 - sale_gamma: 67 - service_level: 0.95 - SKU634: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 17 - init_stock: 438 - max_stock: 1000 - price: 30 - sale_gamma: 73 - service_level: 0.95 - SKU635: - constraint: null - cost: 341 - init_stock: 372 - max_stock: 1000 - price: 654 - sale_gamma: 93 - service_level: 0.95 - SKU636: - constraint: G(stock_constraint) - cost: 385 - init_stock: 111 - max_stock: 1000 - price: 739 - sale_gamma: 37 - service_level: 0.95 - SKU637: - constraint: G(low_profit -> low_stock_constraint) - cost: 83 - init_stock: 305 - max_stock: 1000 - price: 118 - sale_gamma: 61 - service_level: 0.95 - SKU638: - constraint: G(stock_constraint) - cost: 375 - init_stock: 16 - max_stock: 1000 - price: 536 - sale_gamma: 8 - service_level: 0.95 - SKU639: - constraint: null - cost: 327 - init_stock: 160 - max_stock: 1000 - price: 487 - sale_gamma: 40 - service_level: 0.95 - SKU64: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 133 - max_stock: 1000 - price: 42 - sale_gamma: 19 - service_level: 0.95 - SKU640: - constraint: G(low_profit -> low_stock_constraint) - cost: 275 - init_stock: 546 - max_stock: 1000 - price: 382 - sale_gamma: 91 - service_level: 0.95 - SKU641: - constraint: null - cost: 365 - init_stock: 486 - max_stock: 1000 - price: 445 - sale_gamma: 81 - service_level: 0.95 - SKU642: - constraint: null - cost: 414 - init_stock: 150 - max_stock: 1000 - price: 554 - sale_gamma: 25 - service_level: 0.95 - SKU643: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 358 - init_stock: 322 - max_stock: 1000 - price: 461 - sale_gamma: 46 - service_level: 0.95 - SKU644: - constraint: G(low_profit -> low_stock_constraint) - cost: 46 - init_stock: 410 - max_stock: 1000 - price: 61 - sale_gamma: 82 - service_level: 0.95 - SKU645: - constraint: null - cost: 467 - init_stock: 594 - max_stock: 1000 - price: 742 - sale_gamma: 99 - service_level: 0.95 - SKU646: - constraint: G(low_profit -> low_stock_constraint) - cost: 189 - init_stock: 91 - max_stock: 1000 - price: 268 - sale_gamma: 13 - service_level: 0.95 - SKU647: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 462 - max_stock: 1000 - price: 369 - sale_gamma: 77 - service_level: 0.95 - SKU648: - constraint: G(stock_constraint) - cost: 226 - init_stock: 110 - max_stock: 1000 - price: 336 - sale_gamma: 55 - service_level: 0.95 - SKU649: - constraint: null - cost: 198 - init_stock: 175 - max_stock: 1000 - price: 229 - sale_gamma: 25 - service_level: 0.95 - SKU65: - constraint: null - cost: 15 - init_stock: 658 - max_stock: 1000 - price: 25 - sale_gamma: 94 - service_level: 0.95 - SKU650: - constraint: null - cost: 351 - init_stock: 224 - max_stock: 1000 - price: 498 - sale_gamma: 56 - service_level: 0.95 - SKU651: - constraint: null - cost: 380 - init_stock: 672 - max_stock: 1000 - price: 596 - sale_gamma: 96 - service_level: 0.95 - SKU652: - constraint: null - cost: 123 - init_stock: 280 - max_stock: 1000 - price: 168 - sale_gamma: 40 - service_level: 0.95 - SKU653: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 384 - max_stock: 1000 - price: 661 - sale_gamma: 96 - service_level: 0.95 - SKU654: - constraint: null - cost: 66 - init_stock: 32 - max_stock: 1000 - price: 110 - sale_gamma: 8 - service_level: 0.95 - SKU655: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 126 - max_stock: 1000 - price: 479 - sale_gamma: 42 - service_level: 0.95 - SKU656: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 53 - init_stock: 504 - max_stock: 1000 - price: 81 - sale_gamma: 72 - service_level: 0.95 - SKU657: - constraint: G(low_profit -> low_stock_constraint) - cost: 73 - init_stock: 567 - max_stock: 1000 - price: 110 - sale_gamma: 81 - service_level: 0.95 - SKU658: - constraint: null - cost: 70 - init_stock: 252 - max_stock: 1000 - price: 107 - sale_gamma: 36 - service_level: 0.95 - SKU659: - constraint: null - cost: 21 - init_stock: 336 - max_stock: 1000 - price: 27 - sale_gamma: 56 - service_level: 0.95 - SKU66: - constraint: null - cost: 143 - init_stock: 360 - max_stock: 1000 - price: 230 - sale_gamma: 90 - service_level: 0.95 - SKU660: - constraint: null - cost: 487 - init_stock: 415 - max_stock: 1000 - price: 560 - sale_gamma: 83 - service_level: 0.95 - SKU661: - constraint: null - cost: 344 - init_stock: 296 - max_stock: 1000 - price: 581 - sale_gamma: 74 - service_level: 0.95 - SKU662: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 266 - max_stock: 1000 - price: 628 - sale_gamma: 38 - service_level: 0.95 - SKU663: - constraint: G(stock_constraint) - cost: 418 - init_stock: 320 - max_stock: 1000 - price: 518 - sale_gamma: 40 - service_level: 0.95 - SKU664: - constraint: G(stock_constraint) - cost: 351 - init_stock: 420 - max_stock: 1000 - price: 494 - sale_gamma: 84 - service_level: 0.95 - SKU665: - constraint: null - cost: 493 - init_stock: 344 - max_stock: 1000 - price: 734 - sale_gamma: 43 - service_level: 0.95 - SKU666: - constraint: G(stock_constraint) - cost: 341 - init_stock: 176 - max_stock: 1000 - price: 572 - sale_gamma: 88 - service_level: 0.95 - SKU667: - constraint: null - cost: 325 - init_stock: 52 - max_stock: 1000 - price: 562 - sale_gamma: 13 - service_level: 0.95 - SKU668: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 300 - max_stock: 1000 - price: 463 - sale_gamma: 60 - service_level: 0.95 - SKU669: - constraint: null - cost: 74 - init_stock: 96 - max_stock: 1000 - price: 83 - sale_gamma: 16 - service_level: 0.95 - SKU67: - constraint: G(low_profit -> low_stock_constraint) - cost: 247 - init_stock: 159 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU670: - constraint: G(stock_constraint) - cost: 289 - init_stock: 258 - max_stock: 1000 - price: 430 - sale_gamma: 86 - service_level: 0.95 - SKU671: - constraint: G(stock_constraint) - cost: 267 - init_stock: 332 - max_stock: 1000 - price: 296 - sale_gamma: 83 - service_level: 0.95 - SKU672: - constraint: null - cost: 369 - init_stock: 78 - max_stock: 1000 - price: 420 - sale_gamma: 13 - service_level: 0.95 - SKU673: - constraint: G(low_profit -> low_stock_constraint) - cost: 322 - init_stock: 123 - max_stock: 1000 - price: 418 - sale_gamma: 41 - service_level: 0.95 - SKU674: - constraint: null - cost: 223 - init_stock: 66 - max_stock: 1000 - price: 263 - sale_gamma: 22 - service_level: 0.95 - SKU675: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 438 - init_stock: 99 - max_stock: 1000 - price: 529 - sale_gamma: 33 - service_level: 0.95 - SKU676: - constraint: null - cost: 244 - init_stock: 366 - max_stock: 1000 - price: 275 - sale_gamma: 61 - service_level: 0.95 - SKU677: - constraint: null - cost: 425 - init_stock: 176 - max_stock: 1000 - price: 658 - sale_gamma: 44 - service_level: 0.95 - SKU678: - constraint: null - cost: 168 - init_stock: 216 - max_stock: 1000 - price: 199 - sale_gamma: 36 - service_level: 0.95 - SKU679: - constraint: null - cost: 395 - init_stock: 132 - max_stock: 1000 - price: 734 - sale_gamma: 44 - service_level: 0.95 - SKU68: - constraint: G(stock_constraint) - cost: 169 - init_stock: 56 - max_stock: 1000 - price: 239 - sale_gamma: 14 - service_level: 0.95 - SKU680: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 260 - init_stock: 180 - max_stock: 1000 - price: 327 - sale_gamma: 30 - service_level: 0.95 - SKU681: - constraint: G(low_profit -> low_stock_constraint) - cost: 464 - init_stock: 776 - max_stock: 1000 - price: 510 - sale_gamma: 97 - service_level: 0.95 - SKU682: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 370 - init_stock: 318 - max_stock: 1000 - price: 407 - sale_gamma: 53 - service_level: 0.95 - SKU683: - constraint: G(low_profit -> low_stock_constraint) - cost: 327 - init_stock: 536 - max_stock: 1000 - price: 608 - sale_gamma: 67 - service_level: 0.95 - SKU684: - constraint: G(stock_constraint) - cost: 355 - init_stock: 158 - max_stock: 1000 - price: 401 - sale_gamma: 79 - service_level: 0.95 - SKU685: - constraint: null - cost: 422 - init_stock: 270 - max_stock: 1000 - price: 493 - sale_gamma: 45 - service_level: 0.95 - SKU686: - constraint: G(low_profit -> low_stock_constraint) - cost: 63 - init_stock: 378 - max_stock: 1000 - price: 122 - sale_gamma: 63 - service_level: 0.95 - SKU687: - constraint: G(low_profit -> low_stock_constraint) - cost: 34 - init_stock: 456 - max_stock: 1000 - price: 43 - sale_gamma: 76 - service_level: 0.95 - SKU688: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 462 - max_stock: 1000 - price: 759 - sale_gamma: 77 - service_level: 0.95 - SKU689: - constraint: G(stock_constraint) - cost: 499 - init_stock: 75 - max_stock: 1000 - price: 808 - sale_gamma: 15 - service_level: 0.95 - SKU69: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 176 - init_stock: 402 - max_stock: 1000 - price: 197 - sale_gamma: 67 - service_level: 0.95 - SKU690: - constraint: null - cost: 437 - init_stock: 415 - max_stock: 1000 - price: 498 - sale_gamma: 83 - service_level: 0.95 - SKU691: - constraint: null - cost: 17 - init_stock: 632 - max_stock: 1000 - price: 18 - sale_gamma: 79 - service_level: 0.95 - SKU692: - constraint: G(stock_constraint) - cost: 225 - init_stock: 195 - max_stock: 1000 - price: 258 - sale_gamma: 65 - service_level: 0.95 - SKU693: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 19 - init_stock: 244 - max_stock: 1000 - price: 21 - sale_gamma: 61 - service_level: 0.95 - SKU694: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 208 - init_stock: 300 - max_stock: 1000 - price: 386 - sale_gamma: 75 - service_level: 0.95 - SKU695: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 190 - init_stock: 49 - max_stock: 1000 - price: 361 - sale_gamma: 7 - service_level: 0.95 - SKU696: - constraint: G(low_profit -> low_stock_constraint) - cost: 348 - init_stock: 290 - max_stock: 1000 - price: 643 - sale_gamma: 58 - service_level: 0.95 - SKU697: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 455 - init_stock: 320 - max_stock: 1000 - price: 641 - sale_gamma: 80 - service_level: 0.95 - SKU698: - constraint: null - cost: 198 - init_stock: 488 - max_stock: 1000 - price: 316 - sale_gamma: 61 - service_level: 0.95 - SKU699: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 58 - sale_gamma: 20 - service_level: 0.95 - SKU7: - constraint: null - cost: 101 - init_stock: 480 - max_stock: 1000 - price: 131 - sale_gamma: 96 - service_level: 0.95 - SKU70: - constraint: G(stock_constraint) - cost: 186 - init_stock: 140 - max_stock: 1000 - price: 288 - sale_gamma: 35 - service_level: 0.95 - SKU700: - constraint: G(low_profit -> low_stock_constraint) - cost: 74 - init_stock: 45 - max_stock: 1000 - price: 130 - sale_gamma: 9 - service_level: 0.95 - SKU701: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 399 - init_stock: 462 - max_stock: 1000 - price: 770 - sale_gamma: 77 - service_level: 0.95 - SKU702: - constraint: G(stock_constraint) - cost: 82 - init_stock: 204 - max_stock: 1000 - price: 163 - sale_gamma: 34 - service_level: 0.95 - SKU703: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 363 - init_stock: 76 - max_stock: 1000 - price: 602 - sale_gamma: 38 - service_level: 0.95 - SKU704: - constraint: G(stock_constraint) - cost: 384 - init_stock: 261 - max_stock: 1000 - price: 518 - sale_gamma: 87 - service_level: 0.95 - SKU705: - constraint: null - cost: 128 - init_stock: 378 - max_stock: 1000 - price: 236 - sale_gamma: 63 - service_level: 0.95 - SKU706: - constraint: null - cost: 182 - init_stock: 455 - max_stock: 1000 - price: 242 - sale_gamma: 91 - service_level: 0.95 - SKU707: - constraint: null - cost: 18 - init_stock: 276 - max_stock: 1000 - price: 35 - sale_gamma: 69 - service_level: 0.95 - SKU708: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 178 - init_stock: 112 - max_stock: 1000 - price: 334 - sale_gamma: 28 - service_level: 0.95 - SKU709: - constraint: G(stock_constraint) - cost: 102 - init_stock: 212 - max_stock: 1000 - price: 173 - sale_gamma: 53 - service_level: 0.95 - SKU71: - constraint: null - cost: 315 - init_stock: 225 - max_stock: 1000 - price: 589 - sale_gamma: 45 - service_level: 0.95 - SKU710: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 252 - init_stock: 95 - max_stock: 1000 - price: 337 - sale_gamma: 19 - service_level: 0.95 - SKU711: - constraint: null - cost: 281 - init_stock: 414 - max_stock: 1000 - price: 320 - sale_gamma: 69 - service_level: 0.95 - SKU712: - constraint: null - cost: 232 - init_stock: 77 - max_stock: 1000 - price: 438 - sale_gamma: 11 - service_level: 0.95 - SKU713: - constraint: null - cost: 285 - init_stock: 129 - max_stock: 1000 - price: 413 - sale_gamma: 43 - service_level: 0.95 - SKU714: - constraint: null - cost: 79 - init_stock: 287 - max_stock: 1000 - price: 106 - sale_gamma: 41 - service_level: 0.95 - SKU715: - constraint: G(stock_constraint) - cost: 234 - init_stock: 34 - max_stock: 1000 - price: 271 - sale_gamma: 17 - service_level: 0.95 - SKU716: - constraint: null - cost: 70 - init_stock: 450 - max_stock: 1000 - price: 123 - sale_gamma: 75 - service_level: 0.95 - SKU717: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 345 - init_stock: 56 - max_stock: 1000 - price: 382 - sale_gamma: 8 - service_level: 0.95 - SKU718: - constraint: null - cost: 357 - init_stock: 174 - max_stock: 1000 - price: 692 - sale_gamma: 58 - service_level: 0.95 - SKU719: - constraint: null - cost: 340 - init_stock: 455 - max_stock: 1000 - price: 384 - sale_gamma: 65 - service_level: 0.95 - SKU72: - constraint: null - cost: 458 - init_stock: 595 - max_stock: 1000 - price: 870 - sale_gamma: 85 - service_level: 0.95 - SKU720: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 503 - sale_gamma: 42 - service_level: 0.95 - SKU721: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 73 - init_stock: 44 - max_stock: 1000 - price: 143 - sale_gamma: 11 - service_level: 0.95 - SKU722: - constraint: G(stock_constraint) - cost: 392 - init_stock: 194 - max_stock: 1000 - price: 572 - sale_gamma: 97 - service_level: 0.95 - SKU723: - constraint: null - cost: 318 - init_stock: 356 - max_stock: 1000 - price: 442 - sale_gamma: 89 - service_level: 0.95 - SKU724: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 198 - max_stock: 1000 - price: 520 - sale_gamma: 33 - service_level: 0.95 - SKU725: - constraint: null - cost: 175 - init_stock: 148 - max_stock: 1000 - price: 309 - sale_gamma: 37 - service_level: 0.95 - SKU726: - constraint: G(low_profit -> low_stock_constraint) - cost: 458 - init_stock: 356 - max_stock: 1000 - price: 567 - sale_gamma: 89 - service_level: 0.95 - SKU727: - constraint: null - cost: 418 - init_stock: 285 - max_stock: 1000 - price: 526 - sale_gamma: 57 - service_level: 0.95 - SKU728: - constraint: null - cost: 475 - init_stock: 185 - max_stock: 1000 - price: 864 - sale_gamma: 37 - service_level: 0.95 - SKU729: - constraint: null - cost: 324 - init_stock: 252 - max_stock: 1000 - price: 476 - sale_gamma: 36 - service_level: 0.95 - SKU73: - constraint: null - cost: 212 - init_stock: 216 - max_stock: 1000 - price: 248 - sale_gamma: 54 - service_level: 0.95 - SKU730: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 65 - max_stock: 1000 - price: 19 - sale_gamma: 13 - service_level: 0.95 - SKU731: - constraint: G(stock_constraint) - cost: 88 - init_stock: 410 - max_stock: 1000 - price: 155 - sale_gamma: 82 - service_level: 0.95 - SKU732: - constraint: null - cost: 41 - init_stock: 434 - max_stock: 1000 - price: 73 - sale_gamma: 62 - service_level: 0.95 - SKU733: - constraint: G(stock_constraint) - cost: 315 - init_stock: 104 - max_stock: 1000 - price: 541 - sale_gamma: 26 - service_level: 0.95 - SKU734: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 255 - max_stock: 1000 - price: 71 - sale_gamma: 51 - service_level: 0.95 - SKU735: - constraint: null - cost: 266 - init_stock: 594 - max_stock: 1000 - price: 409 - sale_gamma: 99 - service_level: 0.95 - SKU736: - constraint: G(stock_constraint) - cost: 368 - init_stock: 75 - max_stock: 1000 - price: 632 - sale_gamma: 25 - service_level: 0.95 - SKU737: - constraint: null - cost: 475 - init_stock: 93 - max_stock: 1000 - price: 655 - sale_gamma: 31 - service_level: 0.95 - SKU738: - constraint: null - cost: 185 - init_stock: 192 - max_stock: 1000 - price: 246 - sale_gamma: 48 - service_level: 0.95 - SKU739: - constraint: G(low_profit -> low_stock_constraint) - cost: 475 - init_stock: 296 - max_stock: 1000 - price: 612 - sale_gamma: 74 - service_level: 0.95 - SKU74: - constraint: null - cost: 159 - init_stock: 168 - max_stock: 1000 - price: 189 - sale_gamma: 42 - service_level: 0.95 - SKU740: - constraint: null - cost: 390 - init_stock: 256 - max_stock: 1000 - price: 549 - sale_gamma: 64 - service_level: 0.95 - SKU741: - constraint: G(stock_constraint) - cost: 91 - init_stock: 280 - max_stock: 1000 - price: 112 - sale_gamma: 56 - service_level: 0.95 - SKU742: - constraint: G(stock_constraint) - cost: 188 - init_stock: 110 - max_stock: 1000 - price: 374 - sale_gamma: 22 - service_level: 0.95 - SKU743: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 217 - init_stock: 301 - max_stock: 1000 - price: 431 - sale_gamma: 43 - service_level: 0.95 - SKU744: - constraint: G(low_profit -> low_stock_constraint) - cost: 379 - init_stock: 290 - max_stock: 1000 - price: 579 - sale_gamma: 58 - service_level: 0.95 - SKU745: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 368 - max_stock: 1000 - price: 376 - sale_gamma: 92 - service_level: 0.95 - SKU746: - constraint: null - cost: 437 - init_stock: 160 - max_stock: 1000 - price: 624 - sale_gamma: 40 - service_level: 0.95 - SKU747: - constraint: null - cost: 373 - init_stock: 474 - max_stock: 1000 - price: 589 - sale_gamma: 79 - service_level: 0.95 - SKU748: - constraint: null - cost: 275 - init_stock: 330 - max_stock: 1000 - price: 464 - sale_gamma: 66 - service_level: 0.95 - SKU749: - constraint: null - cost: 394 - init_stock: 106 - max_stock: 1000 - price: 705 - sale_gamma: 53 - service_level: 0.95 - SKU75: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 76 - max_stock: 1000 - price: 217 - sale_gamma: 19 - service_level: 0.95 - SKU750: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 310 - max_stock: 1000 - price: 463 - sale_gamma: 62 - service_level: 0.95 - SKU751: - constraint: null - cost: 369 - init_stock: 325 - max_stock: 1000 - price: 428 - sale_gamma: 65 - service_level: 0.95 - SKU752: - constraint: null - cost: 259 - init_stock: 546 - max_stock: 1000 - price: 435 - sale_gamma: 78 - service_level: 0.95 - SKU753: - constraint: G(stock_constraint) - cost: 77 - init_stock: 325 - max_stock: 1000 - price: 128 - sale_gamma: 65 - service_level: 0.95 - SKU754: - constraint: G(stock_constraint) - cost: 387 - init_stock: 91 - max_stock: 1000 - price: 545 - sale_gamma: 13 - service_level: 0.95 - SKU755: - constraint: null - cost: 354 - init_stock: 560 - max_stock: 1000 - price: 523 - sale_gamma: 80 - service_level: 0.95 - SKU756: - constraint: G(stock_constraint) - cost: 246 - init_stock: 360 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU757: - constraint: null - cost: 40 - init_stock: 399 - max_stock: 1000 - price: 76 - sale_gamma: 57 - service_level: 0.95 - SKU758: - constraint: null - cost: 241 - init_stock: 160 - max_stock: 1000 - price: 457 - sale_gamma: 40 - service_level: 0.95 - SKU759: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 435 - init_stock: 76 - max_stock: 1000 - price: 648 - sale_gamma: 38 - service_level: 0.95 - SKU76: - constraint: null - cost: 147 - init_stock: 256 - max_stock: 1000 - price: 191 - sale_gamma: 64 - service_level: 0.95 - SKU760: - constraint: null - cost: 176 - init_stock: 255 - max_stock: 1000 - price: 279 - sale_gamma: 51 - service_level: 0.95 - SKU761: - constraint: G(stock_constraint) - cost: 224 - init_stock: 275 - max_stock: 1000 - price: 250 - sale_gamma: 55 - service_level: 0.95 - SKU762: - constraint: G(stock_constraint) - cost: 264 - init_stock: 153 - max_stock: 1000 - price: 351 - sale_gamma: 51 - service_level: 0.95 - SKU763: - constraint: null - cost: 385 - init_stock: 316 - max_stock: 1000 - price: 677 - sale_gamma: 79 - service_level: 0.95 - SKU764: - constraint: null - cost: 349 - init_stock: 68 - max_stock: 1000 - price: 596 - sale_gamma: 34 - service_level: 0.95 - SKU765: - constraint: null - cost: 345 - init_stock: 52 - max_stock: 1000 - price: 472 - sale_gamma: 13 - service_level: 0.95 - SKU766: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 478 - init_stock: 140 - max_stock: 1000 - price: 855 - sale_gamma: 20 - service_level: 0.95 - SKU767: - constraint: null - cost: 95 - init_stock: 456 - max_stock: 1000 - price: 160 - sale_gamma: 76 - service_level: 0.95 - SKU768: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 644 - max_stock: 1000 - price: 244 - sale_gamma: 92 - service_level: 0.95 - SKU769: - constraint: null - cost: 24 - init_stock: 87 - max_stock: 1000 - price: 43 - sale_gamma: 29 - service_level: 0.95 - SKU77: - constraint: null - cost: 409 - init_stock: 144 - max_stock: 1000 - price: 687 - sale_gamma: 72 - service_level: 0.95 - SKU770: - constraint: null - cost: 150 - init_stock: 186 - max_stock: 1000 - price: 166 - sale_gamma: 62 - service_level: 0.95 - SKU771: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 35 - max_stock: 1000 - price: 182 - sale_gamma: 7 - service_level: 0.95 - SKU772: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 36 - max_stock: 1000 - price: 281 - sale_gamma: 6 - service_level: 0.95 - SKU773: - constraint: null - cost: 84 - init_stock: 368 - max_stock: 1000 - price: 117 - sale_gamma: 92 - service_level: 0.95 - SKU774: - constraint: null - cost: 447 - init_stock: 118 - max_stock: 1000 - price: 746 - sale_gamma: 59 - service_level: 0.95 - SKU775: - constraint: null - cost: 175 - init_stock: 688 - max_stock: 1000 - price: 192 - sale_gamma: 86 - service_level: 0.95 - SKU776: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 103 - init_stock: 425 - max_stock: 1000 - price: 172 - sale_gamma: 85 - service_level: 0.95 - SKU777: - constraint: null - cost: 292 - init_stock: 171 - max_stock: 1000 - price: 347 - sale_gamma: 57 - service_level: 0.95 - SKU778: - constraint: null - cost: 203 - init_stock: 40 - max_stock: 1000 - price: 288 - sale_gamma: 8 - service_level: 0.95 - SKU779: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 117 - init_stock: 215 - max_stock: 1000 - price: 186 - sale_gamma: 43 - service_level: 0.95 - SKU78: - constraint: null - cost: 234 - init_stock: 91 - max_stock: 1000 - price: 400 - sale_gamma: 13 - service_level: 0.95 - SKU780: - constraint: G(low_profit -> low_stock_constraint) - cost: 69 - init_stock: 410 - max_stock: 1000 - price: 102 - sale_gamma: 82 - service_level: 0.95 - SKU781: - constraint: null - cost: 372 - init_stock: 144 - max_stock: 1000 - price: 528 - sale_gamma: 36 - service_level: 0.95 - SKU782: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 70 - max_stock: 1000 - price: 35 - sale_gamma: 10 - service_level: 0.95 - SKU783: - constraint: null - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 139 - sale_gamma: 81 - service_level: 0.95 - SKU784: - constraint: null - cost: 309 - init_stock: 312 - max_stock: 1000 - price: 577 - sale_gamma: 52 - service_level: 0.95 - SKU785: - constraint: null - cost: 191 - init_stock: 210 - max_stock: 1000 - price: 255 - sale_gamma: 70 - service_level: 0.95 - SKU786: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 91 - init_stock: 132 - max_stock: 1000 - price: 102 - sale_gamma: 22 - service_level: 0.95 - SKU787: - constraint: null - cost: 360 - init_stock: 366 - max_stock: 1000 - price: 658 - sale_gamma: 61 - service_level: 0.95 - SKU788: - constraint: G(stock_constraint) - cost: 351 - init_stock: 112 - max_stock: 1000 - price: 417 - sale_gamma: 28 - service_level: 0.95 - SKU789: - constraint: G(stock_constraint) - cost: 153 - init_stock: 232 - max_stock: 1000 - price: 298 - sale_gamma: 58 - service_level: 0.95 - SKU79: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 245 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU790: - constraint: null - cost: 417 - init_stock: 330 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU791: - constraint: G(stock_constraint) - cost: 134 - init_stock: 56 - max_stock: 1000 - price: 155 - sale_gamma: 28 - service_level: 0.95 - SKU792: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 84 - max_stock: 1000 - price: 494 - sale_gamma: 21 - service_level: 0.95 - SKU793: - constraint: null - cost: 195 - init_stock: 532 - max_stock: 1000 - price: 282 - sale_gamma: 76 - service_level: 0.95 - SKU794: - constraint: null - cost: 325 - init_stock: 400 - max_stock: 1000 - price: 454 - sale_gamma: 80 - service_level: 0.95 - SKU795: - constraint: null - cost: 276 - init_stock: 288 - max_stock: 1000 - price: 549 - sale_gamma: 48 - service_level: 0.95 - SKU796: - constraint: null - cost: 447 - init_stock: 294 - max_stock: 1000 - price: 885 - sale_gamma: 49 - service_level: 0.95 - SKU797: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 100 - init_stock: 234 - max_stock: 1000 - price: 119 - sale_gamma: 39 - service_level: 0.95 - SKU798: - constraint: null - cost: 426 - init_stock: 180 - max_stock: 1000 - price: 630 - sale_gamma: 30 - service_level: 0.95 - SKU799: - constraint: null - cost: 63 - init_stock: 21 - max_stock: 1000 - price: 78 - sale_gamma: 7 - service_level: 0.95 - SKU8: - constraint: G(stock_constraint) - cost: 125 - init_stock: 252 - max_stock: 1000 - price: 196 - sale_gamma: 63 - service_level: 0.95 - SKU80: - constraint: G(stock_constraint) - cost: 163 - init_stock: 420 - max_stock: 1000 - price: 270 - sale_gamma: 70 - service_level: 0.95 - SKU800: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 470 - max_stock: 1000 - price: 455 - sale_gamma: 94 - service_level: 0.95 - SKU801: - constraint: G(low_profit -> low_stock_constraint) - cost: 389 - init_stock: 216 - max_stock: 1000 - price: 672 - sale_gamma: 36 - service_level: 0.95 - SKU802: - constraint: G(stock_constraint) - cost: 173 - init_stock: 310 - max_stock: 1000 - price: 281 - sale_gamma: 62 - service_level: 0.95 - SKU803: - constraint: null - cost: 272 - init_stock: 95 - max_stock: 1000 - price: 544 - sale_gamma: 19 - service_level: 0.95 - SKU804: - constraint: null - cost: 390 - init_stock: 188 - max_stock: 1000 - price: 491 - sale_gamma: 47 - service_level: 0.95 - SKU805: - constraint: null - cost: 61 - init_stock: 132 - max_stock: 1000 - price: 118 - sale_gamma: 33 - service_level: 0.95 - SKU806: - constraint: null - cost: 158 - init_stock: 156 - max_stock: 1000 - price: 186 - sale_gamma: 52 - service_level: 0.95 - SKU807: - constraint: null - cost: 453 - init_stock: 182 - max_stock: 1000 - price: 656 - sale_gamma: 26 - service_level: 0.95 - SKU808: - constraint: G(stock_constraint) - cost: 249 - init_stock: 182 - max_stock: 1000 - price: 435 - sale_gamma: 91 - service_level: 0.95 - SKU809: - constraint: null - cost: 55 - init_stock: 390 - max_stock: 1000 - price: 69 - sale_gamma: 65 - service_level: 0.95 - SKU81: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 528 - max_stock: 1000 - price: 223 - sale_gamma: 66 - service_level: 0.95 - SKU810: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 276 - init_stock: 42 - max_stock: 1000 - price: 322 - sale_gamma: 6 - service_level: 0.95 - SKU811: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 254 - init_stock: 148 - max_stock: 1000 - price: 322 - sale_gamma: 37 - service_level: 0.95 - SKU812: - constraint: null - cost: 252 - init_stock: 320 - max_stock: 1000 - price: 337 - sale_gamma: 80 - service_level: 0.95 - SKU813: - constraint: G(stock_constraint) - cost: 253 - init_stock: 49 - max_stock: 1000 - price: 333 - sale_gamma: 7 - service_level: 0.95 - SKU814: - constraint: null - cost: 485 - init_stock: 194 - max_stock: 1000 - price: 921 - sale_gamma: 97 - service_level: 0.95 - SKU815: - constraint: null - cost: 390 - init_stock: 168 - max_stock: 1000 - price: 429 - sale_gamma: 24 - service_level: 0.95 - SKU816: - constraint: null - cost: 152 - init_stock: 455 - max_stock: 1000 - price: 174 - sale_gamma: 91 - service_level: 0.95 - SKU817: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 227 - init_stock: 25 - max_stock: 1000 - price: 401 - sale_gamma: 5 - service_level: 0.95 - SKU818: - constraint: null - cost: 354 - init_stock: 215 - max_stock: 1000 - price: 534 - sale_gamma: 43 - service_level: 0.95 - SKU819: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 302 - init_stock: 135 - max_stock: 1000 - price: 480 - sale_gamma: 27 - service_level: 0.95 - SKU82: - constraint: null - cost: 290 - init_stock: 140 - max_stock: 1000 - price: 469 - sale_gamma: 28 - service_level: 0.95 - SKU820: - constraint: null - cost: 264 - init_stock: 410 - max_stock: 1000 - price: 464 - sale_gamma: 82 - service_level: 0.95 - SKU821: - constraint: G(stock_constraint) - cost: 99 - init_stock: 207 - max_stock: 1000 - price: 151 - sale_gamma: 69 - service_level: 0.95 - SKU822: - constraint: null - cost: 136 - init_stock: 490 - max_stock: 1000 - price: 266 - sale_gamma: 70 - service_level: 0.95 - SKU823: - constraint: null - cost: 75 - init_stock: 84 - max_stock: 1000 - price: 116 - sale_gamma: 28 - service_level: 0.95 - SKU824: - constraint: G(low_profit -> low_stock_constraint) - cost: 170 - init_stock: 420 - max_stock: 1000 - price: 200 - sale_gamma: 70 - service_level: 0.95 - SKU825: - constraint: null - cost: 214 - init_stock: 45 - max_stock: 1000 - price: 359 - sale_gamma: 15 - service_level: 0.95 - SKU826: - constraint: G(stock_constraint) - cost: 386 - init_stock: 330 - max_stock: 1000 - price: 428 - sale_gamma: 55 - service_level: 0.95 - SKU827: - constraint: G(stock_constraint) - cost: 148 - init_stock: 448 - max_stock: 1000 - price: 208 - sale_gamma: 64 - service_level: 0.95 - SKU828: - constraint: null - cost: 400 - init_stock: 276 - max_stock: 1000 - price: 627 - sale_gamma: 69 - service_level: 0.95 - SKU829: - constraint: null - cost: 61 - init_stock: 370 - max_stock: 1000 - price: 83 - sale_gamma: 74 - service_level: 0.95 - SKU83: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 68 - max_stock: 1000 - price: 535 - sale_gamma: 17 - service_level: 0.95 - SKU830: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 167 - init_stock: 144 - max_stock: 1000 - price: 252 - sale_gamma: 24 - service_level: 0.95 - SKU831: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 116 - max_stock: 1000 - price: 353 - sale_gamma: 29 - service_level: 0.95 - SKU832: - constraint: null - cost: 33 - init_stock: 164 - max_stock: 1000 - price: 48 - sale_gamma: 82 - service_level: 0.95 - SKU833: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 392 - max_stock: 1000 - price: 756 - sale_gamma: 98 - service_level: 0.95 - SKU834: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 10 - max_stock: 1000 - price: 662 - sale_gamma: 5 - service_level: 0.95 - SKU835: - constraint: null - cost: 440 - init_stock: 156 - max_stock: 1000 - price: 532 - sale_gamma: 52 - service_level: 0.95 - SKU836: - constraint: G(stock_constraint) - cost: 323 - init_stock: 192 - max_stock: 1000 - price: 552 - sale_gamma: 96 - service_level: 0.95 - SKU837: - constraint: null - cost: 373 - init_stock: 156 - max_stock: 1000 - price: 607 - sale_gamma: 26 - service_level: 0.95 - SKU838: - constraint: null - cost: 456 - init_stock: 308 - max_stock: 1000 - price: 711 - sale_gamma: 77 - service_level: 0.95 - SKU839: - constraint: null - cost: 473 - init_stock: 360 - max_stock: 1000 - price: 695 - sale_gamma: 60 - service_level: 0.95 - SKU84: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 318 - init_stock: 294 - max_stock: 1000 - price: 594 - sale_gamma: 42 - service_level: 0.95 - SKU840: - constraint: G(low_profit -> low_stock_constraint) - cost: 266 - init_stock: 150 - max_stock: 1000 - price: 436 - sale_gamma: 50 - service_level: 0.95 - SKU841: - constraint: G(stock_constraint) - cost: 285 - init_stock: 595 - max_stock: 1000 - price: 538 - sale_gamma: 85 - service_level: 0.95 - SKU842: - constraint: G(low_profit -> low_stock_constraint) - cost: 41 - init_stock: 252 - max_stock: 1000 - price: 70 - sale_gamma: 84 - service_level: 0.95 - SKU843: - constraint: null - cost: 360 - init_stock: 432 - max_stock: 1000 - price: 694 - sale_gamma: 72 - service_level: 0.95 - SKU844: - constraint: G(low_profit -> low_stock_constraint) - cost: 51 - init_stock: 474 - max_stock: 1000 - price: 63 - sale_gamma: 79 - service_level: 0.95 - SKU845: - constraint: G(low_profit -> low_stock_constraint) - cost: 288 - init_stock: 176 - max_stock: 1000 - price: 558 - sale_gamma: 22 - service_level: 0.95 - SKU846: - constraint: null - cost: 485 - init_stock: 234 - max_stock: 1000 - price: 940 - sale_gamma: 39 - service_level: 0.95 - SKU847: - constraint: G(low_profit -> low_stock_constraint) - cost: 388 - init_stock: 546 - max_stock: 1000 - price: 519 - sale_gamma: 91 - service_level: 0.95 - SKU848: - constraint: null - cost: 306 - init_stock: 184 - max_stock: 1000 - price: 358 - sale_gamma: 46 - service_level: 0.95 - SKU849: - constraint: G(low_profit -> low_stock_constraint) - cost: 320 - init_stock: 160 - max_stock: 1000 - price: 425 - sale_gamma: 40 - service_level: 0.95 - SKU85: - constraint: G(low_profit -> low_stock_constraint) - cost: 442 - init_stock: 400 - max_stock: 1000 - price: 583 - sale_gamma: 100 - service_level: 0.95 - SKU850: - constraint: G(stock_constraint) - cost: 183 - init_stock: 342 - max_stock: 1000 - price: 206 - sale_gamma: 57 - service_level: 0.95 - SKU851: - constraint: null - cost: 53 - init_stock: 384 - max_stock: 1000 - price: 96 - sale_gamma: 64 - service_level: 0.95 - SKU852: - constraint: null - cost: 306 - init_stock: 162 - max_stock: 1000 - price: 483 - sale_gamma: 54 - service_level: 0.95 - SKU853: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 386 - init_stock: 280 - max_stock: 1000 - price: 443 - sale_gamma: 56 - service_level: 0.95 - SKU854: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 212 - init_stock: 390 - max_stock: 1000 - price: 290 - sale_gamma: 65 - service_level: 0.95 - SKU855: - constraint: null - cost: 76 - init_stock: 432 - max_stock: 1000 - price: 114 - sale_gamma: 72 - service_level: 0.95 - SKU856: - constraint: G(low_profit -> low_stock_constraint) - cost: 380 - init_stock: 224 - max_stock: 1000 - price: 627 - sale_gamma: 32 - service_level: 0.95 - SKU857: - constraint: G(stock_constraint) - cost: 462 - init_stock: 500 - max_stock: 1000 - price: 646 - sale_gamma: 100 - service_level: 0.95 - SKU858: - constraint: null - cost: 80 - init_stock: 120 - max_stock: 1000 - price: 158 - sale_gamma: 24 - service_level: 0.95 - SKU859: - constraint: G(low_profit -> low_stock_constraint) - cost: 215 - init_stock: 224 - max_stock: 1000 - price: 298 - sale_gamma: 28 - service_level: 0.95 - SKU86: - constraint: null - cost: 386 - init_stock: 595 - max_stock: 1000 - price: 447 - sale_gamma: 85 - service_level: 0.95 - SKU860: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 105 - max_stock: 1000 - price: 435 - sale_gamma: 15 - service_level: 0.95 - SKU861: - constraint: null - cost: 477 - init_stock: 52 - max_stock: 1000 - price: 944 - sale_gamma: 13 - service_level: 0.95 - SKU862: - constraint: null - cost: 240 - init_stock: 64 - max_stock: 1000 - price: 412 - sale_gamma: 16 - service_level: 0.95 - SKU863: - constraint: null - cost: 470 - init_stock: 372 - max_stock: 1000 - price: 911 - sale_gamma: 93 - service_level: 0.95 - SKU864: - constraint: null - cost: 203 - init_stock: 114 - max_stock: 1000 - price: 341 - sale_gamma: 19 - service_level: 0.95 - SKU865: - constraint: G(low_profit -> low_stock_constraint) - cost: 144 - init_stock: 152 - max_stock: 1000 - price: 253 - sale_gamma: 19 - service_level: 0.95 - SKU866: - constraint: null - cost: 172 - init_stock: 264 - max_stock: 1000 - price: 201 - sale_gamma: 88 - service_level: 0.95 - SKU867: - constraint: G(low_profit -> low_stock_constraint) - cost: 499 - init_stock: 500 - max_stock: 1000 - price: 698 - sale_gamma: 100 - service_level: 0.95 - SKU868: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 41 - init_stock: 150 - max_stock: 1000 - price: 56 - sale_gamma: 50 - service_level: 0.95 - SKU869: - constraint: null - cost: 97 - init_stock: 388 - max_stock: 1000 - price: 111 - sale_gamma: 97 - service_level: 0.95 - SKU87: - constraint: null - cost: 368 - init_stock: 207 - max_stock: 1000 - price: 563 - sale_gamma: 69 - service_level: 0.95 - SKU870: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 281 - init_stock: 335 - max_stock: 1000 - price: 446 - sale_gamma: 67 - service_level: 0.95 - SKU871: - constraint: null - cost: 101 - init_stock: 396 - max_stock: 1000 - price: 112 - sale_gamma: 99 - service_level: 0.95 - SKU872: - constraint: null - cost: 133 - init_stock: 160 - max_stock: 1000 - price: 195 - sale_gamma: 32 - service_level: 0.95 - SKU873: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 423 - init_stock: 155 - max_stock: 1000 - price: 829 - sale_gamma: 31 - service_level: 0.95 - SKU874: - constraint: G(low_profit -> low_stock_constraint) - cost: 469 - init_stock: 360 - max_stock: 1000 - price: 689 - sale_gamma: 60 - service_level: 0.95 - SKU875: - constraint: G(stock_constraint) - cost: 51 - init_stock: 138 - max_stock: 1000 - price: 57 - sale_gamma: 23 - service_level: 0.95 - SKU876: - constraint: null - cost: 303 - init_stock: 427 - max_stock: 1000 - price: 427 - sale_gamma: 61 - service_level: 0.95 - SKU877: - constraint: null - cost: 40 - init_stock: 245 - max_stock: 1000 - price: 70 - sale_gamma: 35 - service_level: 0.95 - SKU878: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 476 - max_stock: 1000 - price: 32 - sale_gamma: 68 - service_level: 0.95 - SKU879: - constraint: null - cost: 150 - init_stock: 300 - max_stock: 1000 - price: 198 - sale_gamma: 100 - service_level: 0.95 - SKU88: - constraint: G(low_profit -> low_stock_constraint) - cost: 471 - init_stock: 36 - max_stock: 1000 - price: 824 - sale_gamma: 12 - service_level: 0.95 - SKU880: - constraint: null - cost: 433 - init_stock: 155 - max_stock: 1000 - price: 532 - sale_gamma: 31 - service_level: 0.95 - SKU881: - constraint: null - cost: 431 - init_stock: 420 - max_stock: 1000 - price: 560 - sale_gamma: 70 - service_level: 0.95 - SKU882: - constraint: G(stock_constraint) - cost: 194 - init_stock: 80 - max_stock: 1000 - price: 314 - sale_gamma: 16 - service_level: 0.95 - SKU883: - constraint: null - cost: 284 - init_stock: 152 - max_stock: 1000 - price: 479 - sale_gamma: 38 - service_level: 0.95 - SKU884: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 139 - init_stock: 195 - max_stock: 1000 - price: 222 - sale_gamma: 39 - service_level: 0.95 - SKU885: - constraint: G(low_profit -> low_stock_constraint) - cost: 49 - init_stock: 189 - max_stock: 1000 - price: 58 - sale_gamma: 27 - service_level: 0.95 - SKU886: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 584 - max_stock: 1000 - price: 602 - sale_gamma: 73 - service_level: 0.95 - SKU887: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 266 - init_stock: 174 - max_stock: 1000 - price: 494 - sale_gamma: 87 - service_level: 0.95 - SKU888: - constraint: G(low_profit -> low_stock_constraint) - cost: 143 - init_stock: 45 - max_stock: 1000 - price: 220 - sale_gamma: 9 - service_level: 0.95 - SKU889: - constraint: null - cost: 101 - init_stock: 147 - max_stock: 1000 - price: 199 - sale_gamma: 21 - service_level: 0.95 - SKU89: - constraint: null - cost: 138 - init_stock: 744 - max_stock: 1000 - price: 269 - sale_gamma: 93 - service_level: 0.95 - SKU890: - constraint: G(low_profit -> low_stock_constraint) - cost: 161 - init_stock: 600 - max_stock: 1000 - price: 247 - sale_gamma: 100 - service_level: 0.95 - SKU891: - constraint: null - cost: 356 - init_stock: 176 - max_stock: 1000 - price: 615 - sale_gamma: 22 - service_level: 0.95 - SKU892: - constraint: null - cost: 313 - init_stock: 552 - max_stock: 1000 - price: 516 - sale_gamma: 92 - service_level: 0.95 - SKU893: - constraint: null - cost: 229 - init_stock: 594 - max_stock: 1000 - price: 302 - sale_gamma: 99 - service_level: 0.95 - SKU894: - constraint: null - cost: 129 - init_stock: 222 - max_stock: 1000 - price: 176 - sale_gamma: 74 - service_level: 0.95 - SKU895: - constraint: null - cost: 230 - init_stock: 39 - max_stock: 1000 - price: 301 - sale_gamma: 13 - service_level: 0.95 - SKU896: - constraint: null - cost: 289 - init_stock: 400 - max_stock: 1000 - price: 465 - sale_gamma: 80 - service_level: 0.95 - SKU897: - constraint: null - cost: 393 - init_stock: 432 - max_stock: 1000 - price: 640 - sale_gamma: 72 - service_level: 0.95 - SKU898: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 477 - init_stock: 44 - max_stock: 1000 - price: 615 - sale_gamma: 11 - service_level: 0.95 - SKU899: - constraint: null - cost: 233 - init_stock: 240 - max_stock: 1000 - price: 309 - sale_gamma: 48 - service_level: 0.95 - SKU9: - constraint: null - cost: 166 - init_stock: 384 - max_stock: 1000 - price: 247 - sale_gamma: 96 - service_level: 0.95 - SKU90: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 454 - init_stock: 204 - max_stock: 1000 - price: 726 - sale_gamma: 51 - service_level: 0.95 - SKU900: - constraint: null - cost: 158 - init_stock: 165 - max_stock: 1000 - price: 263 - sale_gamma: 55 - service_level: 0.95 - SKU901: - constraint: G(stock_constraint) - cost: 215 - init_stock: 203 - max_stock: 1000 - price: 320 - sale_gamma: 29 - service_level: 0.95 - SKU902: - constraint: G(low_profit -> low_stock_constraint) - cost: 125 - init_stock: 560 - max_stock: 1000 - price: 205 - sale_gamma: 80 - service_level: 0.95 - SKU903: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 357 - init_stock: 266 - max_stock: 1000 - price: 517 - sale_gamma: 38 - service_level: 0.95 - SKU904: - constraint: G(low_profit -> low_stock_constraint) - cost: 496 - init_stock: 255 - max_stock: 1000 - price: 605 - sale_gamma: 51 - service_level: 0.95 - SKU905: - constraint: null - cost: 249 - init_stock: 217 - max_stock: 1000 - price: 383 - sale_gamma: 31 - service_level: 0.95 - SKU906: - constraint: G(low_profit -> low_stock_constraint) - cost: 166 - init_stock: 156 - max_stock: 1000 - price: 273 - sale_gamma: 52 - service_level: 0.95 - SKU907: - constraint: null - cost: 22 - init_stock: 498 - max_stock: 1000 - price: 33 - sale_gamma: 83 - service_level: 0.95 - SKU908: - constraint: null - cost: 408 - init_stock: 546 - max_stock: 1000 - price: 595 - sale_gamma: 78 - service_level: 0.95 - SKU909: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 480 - max_stock: 1000 - price: 703 - sale_gamma: 96 - service_level: 0.95 - SKU91: - constraint: G(low_profit -> low_stock_constraint) - cost: 303 - init_stock: 48 - max_stock: 1000 - price: 463 - sale_gamma: 16 - service_level: 0.95 - SKU910: - constraint: null - cost: 226 - init_stock: 132 - max_stock: 1000 - price: 350 - sale_gamma: 33 - service_level: 0.95 - SKU911: - constraint: null - cost: 461 - init_stock: 210 - max_stock: 1000 - price: 686 - sale_gamma: 70 - service_level: 0.95 - SKU912: - constraint: null - cost: 236 - init_stock: 135 - max_stock: 1000 - price: 328 - sale_gamma: 27 - service_level: 0.95 - SKU913: - constraint: null - cost: 322 - init_stock: 184 - max_stock: 1000 - price: 518 - sale_gamma: 46 - service_level: 0.95 - SKU914: - constraint: null - cost: 272 - init_stock: 434 - max_stock: 1000 - price: 446 - sale_gamma: 62 - service_level: 0.95 - SKU915: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 337 - init_stock: 280 - max_stock: 1000 - price: 545 - sale_gamma: 56 - service_level: 0.95 - SKU916: - constraint: null - cost: 337 - init_stock: 534 - max_stock: 1000 - price: 647 - sale_gamma: 89 - service_level: 0.95 - SKU917: - constraint: G(low_profit -> low_stock_constraint) - cost: 258 - init_stock: 120 - max_stock: 1000 - price: 366 - sale_gamma: 30 - service_level: 0.95 - SKU918: - constraint: G(stock_constraint) - cost: 148 - init_stock: 330 - max_stock: 1000 - price: 224 - sale_gamma: 55 - service_level: 0.95 - SKU919: - constraint: G(stock_constraint) - cost: 393 - init_stock: 125 - max_stock: 1000 - price: 711 - sale_gamma: 25 - service_level: 0.95 - SKU92: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 252 - max_stock: 1000 - price: 466 - sale_gamma: 63 - service_level: 0.95 - SKU920: - constraint: null - cost: 357 - init_stock: 344 - max_stock: 1000 - price: 696 - sale_gamma: 86 - service_level: 0.95 - SKU921: - constraint: G(low_profit -> low_stock_constraint) - cost: 227 - init_stock: 198 - max_stock: 1000 - price: 419 - sale_gamma: 66 - service_level: 0.95 - SKU922: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 201 - max_stock: 1000 - price: 123 - sale_gamma: 67 - service_level: 0.95 - SKU923: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 348 - max_stock: 1000 - price: 828 - sale_gamma: 58 - service_level: 0.95 - SKU924: - constraint: null - cost: 316 - init_stock: 425 - max_stock: 1000 - price: 423 - sale_gamma: 85 - service_level: 0.95 - SKU925: - constraint: null - cost: 360 - init_stock: 90 - max_stock: 1000 - price: 716 - sale_gamma: 15 - service_level: 0.95 - SKU926: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 360 - init_stock: 469 - max_stock: 1000 - price: 475 - sale_gamma: 67 - service_level: 0.95 - SKU927: - constraint: G(stock_constraint) - cost: 260 - init_stock: 147 - max_stock: 1000 - price: 439 - sale_gamma: 21 - service_level: 0.95 - SKU928: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 491 - init_stock: 415 - max_stock: 1000 - price: 869 - sale_gamma: 83 - service_level: 0.95 - SKU929: - constraint: null - cost: 359 - init_stock: 800 - max_stock: 1000 - price: 470 - sale_gamma: 100 - service_level: 0.95 - SKU93: - constraint: null - cost: 404 - init_stock: 268 - max_stock: 1000 - price: 557 - sale_gamma: 67 - service_level: 0.95 - SKU930: - constraint: G(low_profit -> low_stock_constraint) - cost: 198 - init_stock: 196 - max_stock: 1000 - price: 247 - sale_gamma: 28 - service_level: 0.95 - SKU931: - constraint: null - cost: 71 - init_stock: 56 - max_stock: 1000 - price: 101 - sale_gamma: 14 - service_level: 0.95 - SKU932: - constraint: null - cost: 163 - init_stock: 264 - max_stock: 1000 - price: 283 - sale_gamma: 66 - service_level: 0.95 - SKU933: - constraint: null - cost: 113 - init_stock: 156 - max_stock: 1000 - price: 179 - sale_gamma: 78 - service_level: 0.95 - SKU934: - constraint: G(stock_constraint) - cost: 219 - init_stock: 469 - max_stock: 1000 - price: 346 - sale_gamma: 67 - service_level: 0.95 - SKU935: - constraint: null - cost: 364 - init_stock: 376 - max_stock: 1000 - price: 527 - sale_gamma: 94 - service_level: 0.95 - SKU936: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 24 - init_stock: 20 - max_stock: 1000 - price: 41 - sale_gamma: 5 - service_level: 0.95 - SKU937: - constraint: G(stock_constraint) - cost: 135 - init_stock: 85 - max_stock: 1000 - price: 216 - sale_gamma: 17 - service_level: 0.95 - SKU938: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 63 - max_stock: 1000 - price: 704 - sale_gamma: 21 - service_level: 0.95 - SKU939: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 413 - max_stock: 1000 - price: 219 - sale_gamma: 59 - service_level: 0.95 - SKU94: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 235 - max_stock: 1000 - price: 261 - sale_gamma: 47 - service_level: 0.95 - SKU940: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 14 - init_stock: 279 - max_stock: 1000 - price: 24 - sale_gamma: 93 - service_level: 0.95 - SKU941: - constraint: G(stock_constraint) - cost: 80 - init_stock: 456 - max_stock: 1000 - price: 132 - sale_gamma: 57 - service_level: 0.95 - SKU942: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 202 - init_stock: 65 - max_stock: 1000 - price: 303 - sale_gamma: 13 - service_level: 0.95 - SKU943: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 182 - sale_gamma: 49 - service_level: 0.95 - SKU944: - constraint: null - cost: 196 - init_stock: 132 - max_stock: 1000 - price: 356 - sale_gamma: 44 - service_level: 0.95 - SKU945: - constraint: null - cost: 141 - init_stock: 85 - max_stock: 1000 - price: 176 - sale_gamma: 17 - service_level: 0.95 - SKU946: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 555 - sale_gamma: 49 - service_level: 0.95 - SKU947: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 338 - init_stock: 637 - max_stock: 1000 - price: 547 - sale_gamma: 91 - service_level: 0.95 - SKU948: - constraint: null - cost: 425 - init_stock: 112 - max_stock: 1000 - price: 476 - sale_gamma: 28 - service_level: 0.95 - SKU949: - constraint: G(stock_constraint) - cost: 309 - init_stock: 15 - max_stock: 1000 - price: 522 - sale_gamma: 5 - service_level: 0.95 - SKU95: - constraint: null - cost: 136 - init_stock: 84 - max_stock: 1000 - price: 214 - sale_gamma: 21 - service_level: 0.95 - SKU950: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 102 - init_stock: 324 - max_stock: 1000 - price: 128 - sale_gamma: 54 - service_level: 0.95 - SKU951: - constraint: G(low_profit -> low_stock_constraint) - cost: 75 - init_stock: 90 - max_stock: 1000 - price: 117 - sale_gamma: 18 - service_level: 0.95 - SKU952: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 156 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU953: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 230 - sale_gamma: 35 - service_level: 0.95 - SKU954: - constraint: null - cost: 296 - init_stock: 430 - max_stock: 1000 - price: 444 - sale_gamma: 86 - service_level: 0.95 - SKU955: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 55 - init_stock: 189 - max_stock: 1000 - price: 85 - sale_gamma: 63 - service_level: 0.95 - SKU956: - constraint: null - cost: 282 - init_stock: 425 - max_stock: 1000 - price: 397 - sale_gamma: 85 - service_level: 0.95 - SKU957: - constraint: null - cost: 305 - init_stock: 306 - max_stock: 1000 - price: 466 - sale_gamma: 51 - service_level: 0.95 - SKU958: - constraint: null - cost: 369 - init_stock: 462 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU959: - constraint: null - cost: 81 - init_stock: 164 - max_stock: 1000 - price: 127 - sale_gamma: 82 - service_level: 0.95 - SKU96: - constraint: G(low_profit -> low_stock_constraint) - cost: 176 - init_stock: 264 - max_stock: 1000 - price: 329 - sale_gamma: 44 - service_level: 0.95 - SKU960: - constraint: null - cost: 147 - init_stock: 42 - max_stock: 1000 - price: 235 - sale_gamma: 6 - service_level: 0.95 - SKU961: - constraint: null - cost: 264 - init_stock: 176 - max_stock: 1000 - price: 290 - sale_gamma: 44 - service_level: 0.95 - SKU962: - constraint: null - cost: 354 - init_stock: 176 - max_stock: 1000 - price: 392 - sale_gamma: 44 - service_level: 0.95 - SKU963: - constraint: null - cost: 349 - init_stock: 63 - max_stock: 1000 - price: 523 - sale_gamma: 21 - service_level: 0.95 - SKU964: - constraint: null - cost: 244 - init_stock: 219 - max_stock: 1000 - price: 480 - sale_gamma: 73 - service_level: 0.95 - SKU965: - constraint: G(stock_constraint) - cost: 124 - init_stock: 255 - max_stock: 1000 - price: 199 - sale_gamma: 51 - service_level: 0.95 - SKU966: - constraint: G(stock_constraint) - cost: 302 - init_stock: 308 - max_stock: 1000 - price: 474 - sale_gamma: 44 - service_level: 0.95 - SKU967: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 270 - max_stock: 1000 - price: 111 - sale_gamma: 45 - service_level: 0.95 - SKU968: - constraint: G(stock_constraint) - cost: 281 - init_stock: 294 - max_stock: 1000 - price: 528 - sale_gamma: 49 - service_level: 0.95 - SKU969: - constraint: G(low_profit -> low_stock_constraint) - cost: 249 - init_stock: 24 - max_stock: 1000 - price: 328 - sale_gamma: 6 - service_level: 0.95 - SKU97: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 28 - init_stock: 180 - max_stock: 1000 - price: 43 - sale_gamma: 30 - service_level: 0.95 - SKU970: - constraint: null - cost: 244 - init_stock: 30 - max_stock: 1000 - price: 373 - sale_gamma: 5 - service_level: 0.95 - SKU971: - constraint: null - cost: 368 - init_stock: 219 - max_stock: 1000 - price: 426 - sale_gamma: 73 - service_level: 0.95 - SKU972: - constraint: G(stock_constraint) - cost: 209 - init_stock: 345 - max_stock: 1000 - price: 307 - sale_gamma: 69 - service_level: 0.95 - SKU973: - constraint: null - cost: 271 - init_stock: 33 - max_stock: 1000 - price: 447 - sale_gamma: 11 - service_level: 0.95 - SKU974: - constraint: null - cost: 170 - init_stock: 192 - max_stock: 1000 - price: 285 - sale_gamma: 32 - service_level: 0.95 - SKU975: - constraint: G(stock_constraint) - cost: 198 - init_stock: 88 - max_stock: 1000 - price: 334 - sale_gamma: 22 - service_level: 0.95 - SKU976: - constraint: G(stock_constraint) - cost: 178 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU977: - constraint: G(stock_constraint) - cost: 234 - init_stock: 324 - max_stock: 1000 - price: 269 - sale_gamma: 54 - service_level: 0.95 - SKU978: - constraint: G(stock_constraint) - cost: 470 - init_stock: 320 - max_stock: 1000 - price: 921 - sale_gamma: 64 - service_level: 0.95 - SKU979: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 443 - init_stock: 480 - max_stock: 1000 - price: 753 - sale_gamma: 96 - service_level: 0.95 - SKU98: - constraint: G(stock_constraint) - cost: 443 - init_stock: 70 - max_stock: 1000 - price: 527 - sale_gamma: 35 - service_level: 0.95 - SKU980: - constraint: null - cost: 39 - init_stock: 340 - max_stock: 1000 - price: 60 - sale_gamma: 68 - service_level: 0.95 - SKU981: - constraint: null - cost: 482 - init_stock: 360 - max_stock: 1000 - price: 607 - sale_gamma: 72 - service_level: 0.95 - SKU982: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 213 - init_stock: 208 - max_stock: 1000 - price: 374 - sale_gamma: 52 - service_level: 0.95 - SKU983: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 276 - max_stock: 1000 - price: 776 - sale_gamma: 92 - service_level: 0.95 - SKU984: - constraint: G(stock_constraint) - cost: 232 - init_stock: 637 - max_stock: 1000 - price: 327 - sale_gamma: 91 - service_level: 0.95 - SKU985: - constraint: null - cost: 290 - init_stock: 153 - max_stock: 1000 - price: 420 - sale_gamma: 51 - service_level: 0.95 - SKU986: - constraint: null - cost: 275 - init_stock: 136 - max_stock: 1000 - price: 313 - sale_gamma: 17 - service_level: 0.95 - SKU987: - constraint: null - cost: 434 - init_stock: 204 - max_stock: 1000 - price: 516 - sale_gamma: 34 - service_level: 0.95 - SKU988: - constraint: null - cost: 102 - init_stock: 69 - max_stock: 1000 - price: 166 - sale_gamma: 23 - service_level: 0.95 - SKU989: - constraint: null - cost: 484 - init_stock: 558 - max_stock: 1000 - price: 653 - sale_gamma: 93 - service_level: 0.95 - SKU99: - constraint: null - cost: 361 - init_stock: 225 - max_stock: 1000 - price: 581 - sale_gamma: 45 - service_level: 0.95 - SKU990: - constraint: G(low_profit -> low_stock_constraint) - cost: 108 - init_stock: 228 - max_stock: 1000 - price: 174 - sale_gamma: 57 - service_level: 0.95 - SKU991: - constraint: null - cost: 409 - init_stock: 152 - max_stock: 1000 - price: 576 - sale_gamma: 19 - service_level: 0.95 - SKU992: - constraint: null - cost: 434 - init_stock: 651 - max_stock: 1000 - price: 685 - sale_gamma: 93 - service_level: 0.95 - SKU993: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 602 - max_stock: 1000 - price: 201 - sale_gamma: 86 - service_level: 0.95 - SKU994: - constraint: G(low_profit -> low_stock_constraint) - cost: 470 - init_stock: 300 - max_stock: 1000 - price: 916 - sale_gamma: 50 - service_level: 0.95 - SKU995: - constraint: null - cost: 241 - init_stock: 532 - max_stock: 1000 - price: 310 - sale_gamma: 76 - service_level: 0.95 - SKU996: - constraint: G(stock_constraint) - cost: 260 - init_stock: 438 - max_stock: 1000 - price: 416 - sale_gamma: 73 - service_level: 0.95 - SKU997: - constraint: G(stock_constraint) - cost: 400 - init_stock: 102 - max_stock: 1000 - price: 727 - sale_gamma: 34 - service_level: 0.95 - SKU998: - constraint: G(low_profit -> low_stock_constraint) - cost: 447 - init_stock: 165 - max_stock: 1000 - price: 688 - sale_gamma: 55 - service_level: 0.95 - SKU999: - constraint: null - cost: 79 - init_stock: 161 - max_stock: 1000 - price: 86 - sale_gamma: 23 - service_level: 0.95 - grid: - facilities: - STORE0: - - 9 - - 8 - SUPPLIER0: - - 14 - - 16 - WAREHOUSE0: - - 16 - - 18 - size: - - 20 - - 20 - skus: - - id: 0 - name: SKU0 - - id: 1 - name: SKU1 - - id: 2 - name: SKU2 - - id: 3 - name: SKU3 - - id: 4 - name: SKU4 - - id: 5 - name: SKU5 - - id: 6 - name: SKU6 - - id: 7 - name: SKU7 - - id: 8 - name: SKU8 - - id: 9 - name: SKU9 - - id: 10 - name: SKU10 - - id: 11 - name: SKU11 - - id: 12 - name: SKU12 - - id: 13 - name: SKU13 - - id: 14 - name: SKU14 - - id: 15 - name: SKU15 - - id: 16 - name: SKU16 - - id: 17 - name: SKU17 - - id: 18 - name: SKU18 - - id: 19 - name: SKU19 - - id: 20 - name: SKU20 - - id: 21 - name: SKU21 - - id: 22 - name: SKU22 - - id: 23 - name: SKU23 - - id: 24 - name: SKU24 - - id: 25 - name: SKU25 - - id: 26 - name: SKU26 - - id: 27 - name: SKU27 - - id: 28 - name: SKU28 - - id: 29 - name: SKU29 - - id: 30 - name: SKU30 - - id: 31 - name: SKU31 - - id: 32 - name: SKU32 - - id: 33 - name: SKU33 - - id: 34 - name: SKU34 - - id: 35 - name: SKU35 - - id: 36 - name: SKU36 - - id: 37 - name: SKU37 - - id: 38 - name: SKU38 - - id: 39 - name: SKU39 - - id: 40 - name: SKU40 - - id: 41 - name: SKU41 - - id: 42 - name: SKU42 - - id: 43 - name: SKU43 - - id: 44 - name: SKU44 - - id: 45 - name: SKU45 - - id: 46 - name: SKU46 - - id: 47 - name: SKU47 - - id: 48 - name: SKU48 - - id: 49 - name: SKU49 - - id: 50 - name: SKU50 - - id: 51 - name: SKU51 - - id: 52 - name: SKU52 - - id: 53 - name: SKU53 - - id: 54 - name: SKU54 - - id: 55 - name: SKU55 - - id: 56 - name: SKU56 - - id: 57 - name: SKU57 - - id: 58 - name: SKU58 - - id: 59 - name: SKU59 - - id: 60 - name: SKU60 - - id: 61 - name: SKU61 - - id: 62 - name: SKU62 - - id: 63 - name: SKU63 - - id: 64 - name: SKU64 - - id: 65 - name: SKU65 - - id: 66 - name: SKU66 - - id: 67 - name: SKU67 - - id: 68 - name: SKU68 - - id: 69 - name: SKU69 - - id: 70 - name: SKU70 - - id: 71 - name: SKU71 - - id: 72 - name: SKU72 - - id: 73 - name: SKU73 - - id: 74 - name: SKU74 - - id: 75 - name: SKU75 - - id: 76 - name: SKU76 - - id: 77 - name: SKU77 - - id: 78 - name: SKU78 - - id: 79 - name: SKU79 - - id: 80 - name: SKU80 - - id: 81 - name: SKU81 - - id: 82 - name: SKU82 - - id: 83 - name: SKU83 - - id: 84 - name: SKU84 - - id: 85 - name: SKU85 - - id: 86 - name: SKU86 - - id: 87 - name: SKU87 - - id: 88 - name: SKU88 - - id: 89 - name: SKU89 - - id: 90 - name: SKU90 - - id: 91 - name: SKU91 - - id: 92 - name: SKU92 - - id: 93 - name: SKU93 - - id: 94 - name: SKU94 - - id: 95 - name: SKU95 - - id: 96 - name: SKU96 - - id: 97 - name: SKU97 - - id: 98 - name: SKU98 - - id: 99 - name: SKU99 - - id: 100 - name: SKU100 - - id: 101 - name: SKU101 - - id: 102 - name: SKU102 - - id: 103 - name: SKU103 - - id: 104 - name: SKU104 - - id: 105 - name: SKU105 - - id: 106 - name: SKU106 - - id: 107 - name: SKU107 - - id: 108 - name: SKU108 - - id: 109 - name: SKU109 - - id: 110 - name: SKU110 - - id: 111 - name: SKU111 - - id: 112 - name: SKU112 - - id: 113 - name: SKU113 - - id: 114 - name: SKU114 - - id: 115 - name: SKU115 - - id: 116 - name: SKU116 - - id: 117 - name: SKU117 - - id: 118 - name: SKU118 - - id: 119 - name: SKU119 - - id: 120 - name: SKU120 - - id: 121 - name: SKU121 - - id: 122 - name: SKU122 - - id: 123 - name: SKU123 - - id: 124 - name: SKU124 - - id: 125 - name: SKU125 - - id: 126 - name: SKU126 - - id: 127 - name: SKU127 - - id: 128 - name: SKU128 - - id: 129 - name: SKU129 - - id: 130 - name: SKU130 - - id: 131 - name: SKU131 - - id: 132 - name: SKU132 - - id: 133 - name: SKU133 - - id: 134 - name: SKU134 - - id: 135 - name: SKU135 - - id: 136 - name: SKU136 - - id: 137 - name: SKU137 - - id: 138 - name: SKU138 - - id: 139 - name: SKU139 - - id: 140 - name: SKU140 - - id: 141 - name: SKU141 - - id: 142 - name: SKU142 - - id: 143 - name: SKU143 - - id: 144 - name: SKU144 - - id: 145 - name: SKU145 - - id: 146 - name: SKU146 - - id: 147 - name: SKU147 - - id: 148 - name: SKU148 - - id: 149 - name: SKU149 - - id: 150 - name: SKU150 - - id: 151 - name: SKU151 - - id: 152 - name: SKU152 - - id: 153 - name: SKU153 - - id: 154 - name: SKU154 - - id: 155 - name: SKU155 - - id: 156 - name: SKU156 - - id: 157 - name: SKU157 - - id: 158 - name: SKU158 - - id: 159 - name: SKU159 - - id: 160 - name: SKU160 - - id: 161 - name: SKU161 - - id: 162 - name: SKU162 - - id: 163 - name: SKU163 - - id: 164 - name: SKU164 - - id: 165 - name: SKU165 - - id: 166 - name: SKU166 - - id: 167 - name: SKU167 - - id: 168 - name: SKU168 - - id: 169 - name: SKU169 - - id: 170 - name: SKU170 - - id: 171 - name: SKU171 - - id: 172 - name: SKU172 - - id: 173 - name: SKU173 - - id: 174 - name: SKU174 - - id: 175 - name: SKU175 - - id: 176 - name: SKU176 - - id: 177 - name: SKU177 - - id: 178 - name: SKU178 - - id: 179 - name: SKU179 - - id: 180 - name: SKU180 - - id: 181 - name: SKU181 - - id: 182 - name: SKU182 - - id: 183 - name: SKU183 - - id: 184 - name: SKU184 - - id: 185 - name: SKU185 - - id: 186 - name: SKU186 - - id: 187 - name: SKU187 - - id: 188 - name: SKU188 - - id: 189 - name: SKU189 - - id: 190 - name: SKU190 - - id: 191 - name: SKU191 - - id: 192 - name: SKU192 - - id: 193 - name: SKU193 - - id: 194 - name: SKU194 - - id: 195 - name: SKU195 - - id: 196 - name: SKU196 - - id: 197 - name: SKU197 - - id: 198 - name: SKU198 - - id: 199 - name: SKU199 - - id: 200 - name: SKU200 - - id: 201 - name: SKU201 - - id: 202 - name: SKU202 - - id: 203 - name: SKU203 - - id: 204 - name: SKU204 - - id: 205 - name: SKU205 - - id: 206 - name: SKU206 - - id: 207 - name: SKU207 - - id: 208 - name: SKU208 - - id: 209 - name: SKU209 - - id: 210 - name: SKU210 - - id: 211 - name: SKU211 - - id: 212 - name: SKU212 - - id: 213 - name: SKU213 - - id: 214 - name: SKU214 - - id: 215 - name: SKU215 - - id: 216 - name: SKU216 - - id: 217 - name: SKU217 - - id: 218 - name: SKU218 - - id: 219 - name: SKU219 - - id: 220 - name: SKU220 - - id: 221 - name: SKU221 - - id: 222 - name: SKU222 - - id: 223 - name: SKU223 - - id: 224 - name: SKU224 - - id: 225 - name: SKU225 - - id: 226 - name: SKU226 - - id: 227 - name: SKU227 - - id: 228 - name: SKU228 - - id: 229 - name: SKU229 - - id: 230 - name: SKU230 - - id: 231 - name: SKU231 - - id: 232 - name: SKU232 - - id: 233 - name: SKU233 - - id: 234 - name: SKU234 - - id: 235 - name: SKU235 - - id: 236 - name: SKU236 - - id: 237 - name: SKU237 - - id: 238 - name: SKU238 - - id: 239 - name: SKU239 - - id: 240 - name: SKU240 - - id: 241 - name: SKU241 - - id: 242 - name: SKU242 - - id: 243 - name: SKU243 - - id: 244 - name: SKU244 - - id: 245 - name: SKU245 - - id: 246 - name: SKU246 - - id: 247 - name: SKU247 - - id: 248 - name: SKU248 - - id: 249 - name: SKU249 - - id: 250 - name: SKU250 - - id: 251 - name: SKU251 - - id: 252 - name: SKU252 - - id: 253 - name: SKU253 - - id: 254 - name: SKU254 - - id: 255 - name: SKU255 - - id: 256 - name: SKU256 - - id: 257 - name: SKU257 - - id: 258 - name: SKU258 - - id: 259 - name: SKU259 - - id: 260 - name: SKU260 - - id: 261 - name: SKU261 - - id: 262 - name: SKU262 - - id: 263 - name: SKU263 - - id: 264 - name: SKU264 - - id: 265 - name: SKU265 - - id: 266 - name: SKU266 - - id: 267 - name: SKU267 - - id: 268 - name: SKU268 - - id: 269 - name: SKU269 - - id: 270 - name: SKU270 - - id: 271 - name: SKU271 - - id: 272 - name: SKU272 - - id: 273 - name: SKU273 - - id: 274 - name: SKU274 - - id: 275 - name: SKU275 - - id: 276 - name: SKU276 - - id: 277 - name: SKU277 - - id: 278 - name: SKU278 - - id: 279 - name: SKU279 - - id: 280 - name: SKU280 - - id: 281 - name: SKU281 - - id: 282 - name: SKU282 - - id: 283 - name: SKU283 - - id: 284 - name: SKU284 - - id: 285 - name: SKU285 - - id: 286 - name: SKU286 - - id: 287 - name: SKU287 - - id: 288 - name: SKU288 - - id: 289 - name: SKU289 - - id: 290 - name: SKU290 - - id: 291 - name: SKU291 - - id: 292 - name: SKU292 - - id: 293 - name: SKU293 - - id: 294 - name: SKU294 - - id: 295 - name: SKU295 - - id: 296 - name: SKU296 - - id: 297 - name: SKU297 - - id: 298 - name: SKU298 - - id: 299 - name: SKU299 - - id: 300 - name: SKU300 - - id: 301 - name: SKU301 - - id: 302 - name: SKU302 - - id: 303 - name: SKU303 - - id: 304 - name: SKU304 - - id: 305 - name: SKU305 - - id: 306 - name: SKU306 - - id: 307 - name: SKU307 - - id: 308 - name: SKU308 - - id: 309 - name: SKU309 - - id: 310 - name: SKU310 - - id: 311 - name: SKU311 - - id: 312 - name: SKU312 - - id: 313 - name: SKU313 - - id: 314 - name: SKU314 - - id: 315 - name: SKU315 - - id: 316 - name: SKU316 - - id: 317 - name: SKU317 - - id: 318 - name: SKU318 - - id: 319 - name: SKU319 - - id: 320 - name: SKU320 - - id: 321 - name: SKU321 - - id: 322 - name: SKU322 - - id: 323 - name: SKU323 - - id: 324 - name: SKU324 - - id: 325 - name: SKU325 - - id: 326 - name: SKU326 - - id: 327 - name: SKU327 - - id: 328 - name: SKU328 - - id: 329 - name: SKU329 - - id: 330 - name: SKU330 - - id: 331 - name: SKU331 - - id: 332 - name: SKU332 - - id: 333 - name: SKU333 - - id: 334 - name: SKU334 - - id: 335 - name: SKU335 - - id: 336 - name: SKU336 - - id: 337 - name: SKU337 - - id: 338 - name: SKU338 - - id: 339 - name: SKU339 - - id: 340 - name: SKU340 - - id: 341 - name: SKU341 - - id: 342 - name: SKU342 - - id: 343 - name: SKU343 - - id: 344 - name: SKU344 - - id: 345 - name: SKU345 - - id: 346 - name: SKU346 - - id: 347 - name: SKU347 - - id: 348 - name: SKU348 - - id: 349 - name: SKU349 - - id: 350 - name: SKU350 - - id: 351 - name: SKU351 - - id: 352 - name: SKU352 - - id: 353 - name: SKU353 - - id: 354 - name: SKU354 - - id: 355 - name: SKU355 - - id: 356 - name: SKU356 - - id: 357 - name: SKU357 - - id: 358 - name: SKU358 - - id: 359 - name: SKU359 - - id: 360 - name: SKU360 - - id: 361 - name: SKU361 - - id: 362 - name: SKU362 - - id: 363 - name: SKU363 - - id: 364 - name: SKU364 - - id: 365 - name: SKU365 - - id: 366 - name: SKU366 - - id: 367 - name: SKU367 - - id: 368 - name: SKU368 - - id: 369 - name: SKU369 - - id: 370 - name: SKU370 - - id: 371 - name: SKU371 - - id: 372 - name: SKU372 - - id: 373 - name: SKU373 - - id: 374 - name: SKU374 - - id: 375 - name: SKU375 - - id: 376 - name: SKU376 - - id: 377 - name: SKU377 - - id: 378 - name: SKU378 - - id: 379 - name: SKU379 - - id: 380 - name: SKU380 - - id: 381 - name: SKU381 - - id: 382 - name: SKU382 - - id: 383 - name: SKU383 - - id: 384 - name: SKU384 - - id: 385 - name: SKU385 - - id: 386 - name: SKU386 - - id: 387 - name: SKU387 - - id: 388 - name: SKU388 - - id: 389 - name: SKU389 - - id: 390 - name: SKU390 - - id: 391 - name: SKU391 - - id: 392 - name: SKU392 - - id: 393 - name: SKU393 - - id: 394 - name: SKU394 - - id: 395 - name: SKU395 - - id: 396 - name: SKU396 - - id: 397 - name: SKU397 - - id: 398 - name: SKU398 - - id: 399 - name: SKU399 - - id: 400 - name: SKU400 - - id: 401 - name: SKU401 - - id: 402 - name: SKU402 - - id: 403 - name: SKU403 - - id: 404 - name: SKU404 - - id: 405 - name: SKU405 - - id: 406 - name: SKU406 - - id: 407 - name: SKU407 - - id: 408 - name: SKU408 - - id: 409 - name: SKU409 - - id: 410 - name: SKU410 - - id: 411 - name: SKU411 - - id: 412 - name: SKU412 - - id: 413 - name: SKU413 - - id: 414 - name: SKU414 - - id: 415 - name: SKU415 - - id: 416 - name: SKU416 - - id: 417 - name: SKU417 - - id: 418 - name: SKU418 - - id: 419 - name: SKU419 - - id: 420 - name: SKU420 - - id: 421 - name: SKU421 - - id: 422 - name: SKU422 - - id: 423 - name: SKU423 - - id: 424 - name: SKU424 - - id: 425 - name: SKU425 - - id: 426 - name: SKU426 - - id: 427 - name: SKU427 - - id: 428 - name: SKU428 - - id: 429 - name: SKU429 - - id: 430 - name: SKU430 - - id: 431 - name: SKU431 - - id: 432 - name: SKU432 - - id: 433 - name: SKU433 - - id: 434 - name: SKU434 - - id: 435 - name: SKU435 - - id: 436 - name: SKU436 - - id: 437 - name: SKU437 - - id: 438 - name: SKU438 - - id: 439 - name: SKU439 - - id: 440 - name: SKU440 - - id: 441 - name: SKU441 - - id: 442 - name: SKU442 - - id: 443 - name: SKU443 - - id: 444 - name: SKU444 - - id: 445 - name: SKU445 - - id: 446 - name: SKU446 - - id: 447 - name: SKU447 - - id: 448 - name: SKU448 - - id: 449 - name: SKU449 - - id: 450 - name: SKU450 - - id: 451 - name: SKU451 - - id: 452 - name: SKU452 - - id: 453 - name: SKU453 - - id: 454 - name: SKU454 - - id: 455 - name: SKU455 - - id: 456 - name: SKU456 - - id: 457 - name: SKU457 - - id: 458 - name: SKU458 - - id: 459 - name: SKU459 - - id: 460 - name: SKU460 - - id: 461 - name: SKU461 - - id: 462 - name: SKU462 - - id: 463 - name: SKU463 - - id: 464 - name: SKU464 - - id: 465 - name: SKU465 - - id: 466 - name: SKU466 - - id: 467 - name: SKU467 - - id: 468 - name: SKU468 - - id: 469 - name: SKU469 - - id: 470 - name: SKU470 - - id: 471 - name: SKU471 - - id: 472 - name: SKU472 - - id: 473 - name: SKU473 - - id: 474 - name: SKU474 - - id: 475 - name: SKU475 - - id: 476 - name: SKU476 - - id: 477 - name: SKU477 - - id: 478 - name: SKU478 - - id: 479 - name: SKU479 - - id: 480 - name: SKU480 - - id: 481 - name: SKU481 - - id: 482 - name: SKU482 - - id: 483 - name: SKU483 - - id: 484 - name: SKU484 - - id: 485 - name: SKU485 - - id: 486 - name: SKU486 - - id: 487 - name: SKU487 - - id: 488 - name: SKU488 - - id: 489 - name: SKU489 - - id: 490 - name: SKU490 - - id: 491 - name: SKU491 - - id: 492 - name: SKU492 - - id: 493 - name: SKU493 - - id: 494 - name: SKU494 - - id: 495 - name: SKU495 - - id: 496 - name: SKU496 - - id: 497 - name: SKU497 - - id: 498 - name: SKU498 - - id: 499 - name: SKU499 - - id: 500 - name: SKU500 - - id: 501 - name: SKU501 - - id: 502 - name: SKU502 - - id: 503 - name: SKU503 - - id: 504 - name: SKU504 - - id: 505 - name: SKU505 - - id: 506 - name: SKU506 - - id: 507 - name: SKU507 - - id: 508 - name: SKU508 - - id: 509 - name: SKU509 - - id: 510 - name: SKU510 - - id: 511 - name: SKU511 - - id: 512 - name: SKU512 - - id: 513 - name: SKU513 - - id: 514 - name: SKU514 - - id: 515 - name: SKU515 - - id: 516 - name: SKU516 - - id: 517 - name: SKU517 - - id: 518 - name: SKU518 - - id: 519 - name: SKU519 - - id: 520 - name: SKU520 - - id: 521 - name: SKU521 - - id: 522 - name: SKU522 - - id: 523 - name: SKU523 - - id: 524 - name: SKU524 - - id: 525 - name: SKU525 - - id: 526 - name: SKU526 - - id: 527 - name: SKU527 - - id: 528 - name: SKU528 - - id: 529 - name: SKU529 - - id: 530 - name: SKU530 - - id: 531 - name: SKU531 - - id: 532 - name: SKU532 - - id: 533 - name: SKU533 - - id: 534 - name: SKU534 - - id: 535 - name: SKU535 - - id: 536 - name: SKU536 - - id: 537 - name: SKU537 - - id: 538 - name: SKU538 - - id: 539 - name: SKU539 - - id: 540 - name: SKU540 - - id: 541 - name: SKU541 - - id: 542 - name: SKU542 - - id: 543 - name: SKU543 - - id: 544 - name: SKU544 - - id: 545 - name: SKU545 - - id: 546 - name: SKU546 - - id: 547 - name: SKU547 - - id: 548 - name: SKU548 - - id: 549 - name: SKU549 - - id: 550 - name: SKU550 - - id: 551 - name: SKU551 - - id: 552 - name: SKU552 - - id: 553 - name: SKU553 - - id: 554 - name: SKU554 - - id: 555 - name: SKU555 - - id: 556 - name: SKU556 - - id: 557 - name: SKU557 - - id: 558 - name: SKU558 - - id: 559 - name: SKU559 - - id: 560 - name: SKU560 - - id: 561 - name: SKU561 - - id: 562 - name: SKU562 - - id: 563 - name: SKU563 - - id: 564 - name: SKU564 - - id: 565 - name: SKU565 - - id: 566 - name: SKU566 - - id: 567 - name: SKU567 - - id: 568 - name: SKU568 - - id: 569 - name: SKU569 - - id: 570 - name: SKU570 - - id: 571 - name: SKU571 - - id: 572 - name: SKU572 - - id: 573 - name: SKU573 - - id: 574 - name: SKU574 - - id: 575 - name: SKU575 - - id: 576 - name: SKU576 - - id: 577 - name: SKU577 - - id: 578 - name: SKU578 - - id: 579 - name: SKU579 - - id: 580 - name: SKU580 - - id: 581 - name: SKU581 - - id: 582 - name: SKU582 - - id: 583 - name: SKU583 - - id: 584 - name: SKU584 - - id: 585 - name: SKU585 - - id: 586 - name: SKU586 - - id: 587 - name: SKU587 - - id: 588 - name: SKU588 - - id: 589 - name: SKU589 - - id: 590 - name: SKU590 - - id: 591 - name: SKU591 - - id: 592 - name: SKU592 - - id: 593 - name: SKU593 - - id: 594 - name: SKU594 - - id: 595 - name: SKU595 - - id: 596 - name: SKU596 - - id: 597 - name: SKU597 - - id: 598 - name: SKU598 - - id: 599 - name: SKU599 - - id: 600 - name: SKU600 - - id: 601 - name: SKU601 - - id: 602 - name: SKU602 - - id: 603 - name: SKU603 - - id: 604 - name: SKU604 - - id: 605 - name: SKU605 - - id: 606 - name: SKU606 - - id: 607 - name: SKU607 - - id: 608 - name: SKU608 - - id: 609 - name: SKU609 - - id: 610 - name: SKU610 - - id: 611 - name: SKU611 - - id: 612 - name: SKU612 - - id: 613 - name: SKU613 - - id: 614 - name: SKU614 - - id: 615 - name: SKU615 - - id: 616 - name: SKU616 - - id: 617 - name: SKU617 - - id: 618 - name: SKU618 - - id: 619 - name: SKU619 - - id: 620 - name: SKU620 - - id: 621 - name: SKU621 - - id: 622 - name: SKU622 - - id: 623 - name: SKU623 - - id: 624 - name: SKU624 - - id: 625 - name: SKU625 - - id: 626 - name: SKU626 - - id: 627 - name: SKU627 - - id: 628 - name: SKU628 - - id: 629 - name: SKU629 - - id: 630 - name: SKU630 - - id: 631 - name: SKU631 - - id: 632 - name: SKU632 - - id: 633 - name: SKU633 - - id: 634 - name: SKU634 - - id: 635 - name: SKU635 - - id: 636 - name: SKU636 - - id: 637 - name: SKU637 - - id: 638 - name: SKU638 - - id: 639 - name: SKU639 - - id: 640 - name: SKU640 - - id: 641 - name: SKU641 - - id: 642 - name: SKU642 - - id: 643 - name: SKU643 - - id: 644 - name: SKU644 - - id: 645 - name: SKU645 - - id: 646 - name: SKU646 - - id: 647 - name: SKU647 - - id: 648 - name: SKU648 - - id: 649 - name: SKU649 - - id: 650 - name: SKU650 - - id: 651 - name: SKU651 - - id: 652 - name: SKU652 - - id: 653 - name: SKU653 - - id: 654 - name: SKU654 - - id: 655 - name: SKU655 - - id: 656 - name: SKU656 - - id: 657 - name: SKU657 - - id: 658 - name: SKU658 - - id: 659 - name: SKU659 - - id: 660 - name: SKU660 - - id: 661 - name: SKU661 - - id: 662 - name: SKU662 - - id: 663 - name: SKU663 - - id: 664 - name: SKU664 - - id: 665 - name: SKU665 - - id: 666 - name: SKU666 - - id: 667 - name: SKU667 - - id: 668 - name: SKU668 - - id: 669 - name: SKU669 - - id: 670 - name: SKU670 - - id: 671 - name: SKU671 - - id: 672 - name: SKU672 - - id: 673 - name: SKU673 - - id: 674 - name: SKU674 - - id: 675 - name: SKU675 - - id: 676 - name: SKU676 - - id: 677 - name: SKU677 - - id: 678 - name: SKU678 - - id: 679 - name: SKU679 - - id: 680 - name: SKU680 - - id: 681 - name: SKU681 - - id: 682 - name: SKU682 - - id: 683 - name: SKU683 - - id: 684 - name: SKU684 - - id: 685 - name: SKU685 - - id: 686 - name: SKU686 - - id: 687 - name: SKU687 - - id: 688 - name: SKU688 - - id: 689 - name: SKU689 - - id: 690 - name: SKU690 - - id: 691 - name: SKU691 - - id: 692 - name: SKU692 - - id: 693 - name: SKU693 - - id: 694 - name: SKU694 - - id: 695 - name: SKU695 - - id: 696 - name: SKU696 - - id: 697 - name: SKU697 - - id: 698 - name: SKU698 - - id: 699 - name: SKU699 - - id: 700 - name: SKU700 - - id: 701 - name: SKU701 - - id: 702 - name: SKU702 - - id: 703 - name: SKU703 - - id: 704 - name: SKU704 - - id: 705 - name: SKU705 - - id: 706 - name: SKU706 - - id: 707 - name: SKU707 - - id: 708 - name: SKU708 - - id: 709 - name: SKU709 - - id: 710 - name: SKU710 - - id: 711 - name: SKU711 - - id: 712 - name: SKU712 - - id: 713 - name: SKU713 - - id: 714 - name: SKU714 - - id: 715 - name: SKU715 - - id: 716 - name: SKU716 - - id: 717 - name: SKU717 - - id: 718 - name: SKU718 - - id: 719 - name: SKU719 - - id: 720 - name: SKU720 - - id: 721 - name: SKU721 - - id: 722 - name: SKU722 - - id: 723 - name: SKU723 - - id: 724 - name: SKU724 - - id: 725 - name: SKU725 - - id: 726 - name: SKU726 - - id: 727 - name: SKU727 - - id: 728 - name: SKU728 - - id: 729 - name: SKU729 - - id: 730 - name: SKU730 - - id: 731 - name: SKU731 - - id: 732 - name: SKU732 - - id: 733 - name: SKU733 - - id: 734 - name: SKU734 - - id: 735 - name: SKU735 - - id: 736 - name: SKU736 - - id: 737 - name: SKU737 - - id: 738 - name: SKU738 - - id: 739 - name: SKU739 - - id: 740 - name: SKU740 - - id: 741 - name: SKU741 - - id: 742 - name: SKU742 - - id: 743 - name: SKU743 - - id: 744 - name: SKU744 - - id: 745 - name: SKU745 - - id: 746 - name: SKU746 - - id: 747 - name: SKU747 - - id: 748 - name: SKU748 - - id: 749 - name: SKU749 - - id: 750 - name: SKU750 - - id: 751 - name: SKU751 - - id: 752 - name: SKU752 - - id: 753 - name: SKU753 - - id: 754 - name: SKU754 - - id: 755 - name: SKU755 - - id: 756 - name: SKU756 - - id: 757 - name: SKU757 - - id: 758 - name: SKU758 - - id: 759 - name: SKU759 - - id: 760 - name: SKU760 - - id: 761 - name: SKU761 - - id: 762 - name: SKU762 - - id: 763 - name: SKU763 - - id: 764 - name: SKU764 - - id: 765 - name: SKU765 - - id: 766 - name: SKU766 - - id: 767 - name: SKU767 - - id: 768 - name: SKU768 - - id: 769 - name: SKU769 - - id: 770 - name: SKU770 - - id: 771 - name: SKU771 - - id: 772 - name: SKU772 - - id: 773 - name: SKU773 - - id: 774 - name: SKU774 - - id: 775 - name: SKU775 - - id: 776 - name: SKU776 - - id: 777 - name: SKU777 - - id: 778 - name: SKU778 - - id: 779 - name: SKU779 - - id: 780 - name: SKU780 - - id: 781 - name: SKU781 - - id: 782 - name: SKU782 - - id: 783 - name: SKU783 - - id: 784 - name: SKU784 - - id: 785 - name: SKU785 - - id: 786 - name: SKU786 - - id: 787 - name: SKU787 - - id: 788 - name: SKU788 - - id: 789 - name: SKU789 - - id: 790 - name: SKU790 - - id: 791 - name: SKU791 - - id: 792 - name: SKU792 - - id: 793 - name: SKU793 - - id: 794 - name: SKU794 - - id: 795 - name: SKU795 - - id: 796 - name: SKU796 - - id: 797 - name: SKU797 - - id: 798 - name: SKU798 - - id: 799 - name: SKU799 - - id: 800 - name: SKU800 - - id: 801 - name: SKU801 - - id: 802 - name: SKU802 - - id: 803 - name: SKU803 - - id: 804 - name: SKU804 - - id: 805 - name: SKU805 - - id: 806 - name: SKU806 - - id: 807 - name: SKU807 - - id: 808 - name: SKU808 - - id: 809 - name: SKU809 - - id: 810 - name: SKU810 - - id: 811 - name: SKU811 - - id: 812 - name: SKU812 - - id: 813 - name: SKU813 - - id: 814 - name: SKU814 - - id: 815 - name: SKU815 - - id: 816 - name: SKU816 - - id: 817 - name: SKU817 - - id: 818 - name: SKU818 - - id: 819 - name: SKU819 - - id: 820 - name: SKU820 - - id: 821 - name: SKU821 - - id: 822 - name: SKU822 - - id: 823 - name: SKU823 - - id: 824 - name: SKU824 - - id: 825 - name: SKU825 - - id: 826 - name: SKU826 - - id: 827 - name: SKU827 - - id: 828 - name: SKU828 - - id: 829 - name: SKU829 - - id: 830 - name: SKU830 - - id: 831 - name: SKU831 - - id: 832 - name: SKU832 - - id: 833 - name: SKU833 - - id: 834 - name: SKU834 - - id: 835 - name: SKU835 - - id: 836 - name: SKU836 - - id: 837 - name: SKU837 - - id: 838 - name: SKU838 - - id: 839 - name: SKU839 - - id: 840 - name: SKU840 - - id: 841 - name: SKU841 - - id: 842 - name: SKU842 - - id: 843 - name: SKU843 - - id: 844 - name: SKU844 - - id: 845 - name: SKU845 - - id: 846 - name: SKU846 - - id: 847 - name: SKU847 - - id: 848 - name: SKU848 - - id: 849 - name: SKU849 - - id: 850 - name: SKU850 - - id: 851 - name: SKU851 - - id: 852 - name: SKU852 - - id: 853 - name: SKU853 - - id: 854 - name: SKU854 - - id: 855 - name: SKU855 - - id: 856 - name: SKU856 - - id: 857 - name: SKU857 - - id: 858 - name: SKU858 - - id: 859 - name: SKU859 - - id: 860 - name: SKU860 - - id: 861 - name: SKU861 - - id: 862 - name: SKU862 - - id: 863 - name: SKU863 - - id: 864 - name: SKU864 - - id: 865 - name: SKU865 - - id: 866 - name: SKU866 - - id: 867 - name: SKU867 - - id: 868 - name: SKU868 - - id: 869 - name: SKU869 - - id: 870 - name: SKU870 - - id: 871 - name: SKU871 - - id: 872 - name: SKU872 - - id: 873 - name: SKU873 - - id: 874 - name: SKU874 - - id: 875 - name: SKU875 - - id: 876 - name: SKU876 - - id: 877 - name: SKU877 - - id: 878 - name: SKU878 - - id: 879 - name: SKU879 - - id: 880 - name: SKU880 - - id: 881 - name: SKU881 - - id: 882 - name: SKU882 - - id: 883 - name: SKU883 - - id: 884 - name: SKU884 - - id: 885 - name: SKU885 - - id: 886 - name: SKU886 - - id: 887 - name: SKU887 - - id: 888 - name: SKU888 - - id: 889 - name: SKU889 - - id: 890 - name: SKU890 - - id: 891 - name: SKU891 - - id: 892 - name: SKU892 - - id: 893 - name: SKU893 - - id: 894 - name: SKU894 - - id: 895 - name: SKU895 - - id: 896 - name: SKU896 - - id: 897 - name: SKU897 - - id: 898 - name: SKU898 - - id: 899 - name: SKU899 - - id: 900 - name: SKU900 - - id: 901 - name: SKU901 - - id: 902 - name: SKU902 - - id: 903 - name: SKU903 - - id: 904 - name: SKU904 - - id: 905 - name: SKU905 - - id: 906 - name: SKU906 - - id: 907 - name: SKU907 - - id: 908 - name: SKU908 - - id: 909 - name: SKU909 - - id: 910 - name: SKU910 - - id: 911 - name: SKU911 - - id: 912 - name: SKU912 - - id: 913 - name: SKU913 - - id: 914 - name: SKU914 - - id: 915 - name: SKU915 - - id: 916 - name: SKU916 - - id: 917 - name: SKU917 - - id: 918 - name: SKU918 - - id: 919 - name: SKU919 - - id: 920 - name: SKU920 - - id: 921 - name: SKU921 - - id: 922 - name: SKU922 - - id: 923 - name: SKU923 - - id: 924 - name: SKU924 - - id: 925 - name: SKU925 - - id: 926 - name: SKU926 - - id: 927 - name: SKU927 - - id: 928 - name: SKU928 - - id: 929 - name: SKU929 - - id: 930 - name: SKU930 - - id: 931 - name: SKU931 - - id: 932 - name: SKU932 - - id: 933 - name: SKU933 - - id: 934 - name: SKU934 - - id: 935 - name: SKU935 - - id: 936 - name: SKU936 - - id: 937 - name: SKU937 - - id: 938 - name: SKU938 - - id: 939 - name: SKU939 - - id: 940 - name: SKU940 - - id: 941 - name: SKU941 - - id: 942 - name: SKU942 - - id: 943 - name: SKU943 - - id: 944 - name: SKU944 - - id: 945 - name: SKU945 - - id: 946 - name: SKU946 - - id: 947 - name: SKU947 - - id: 948 - name: SKU948 - - id: 949 - name: SKU949 - - id: 950 - name: SKU950 - - id: 951 - name: SKU951 - - id: 952 - name: SKU952 - - id: 953 - name: SKU953 - - id: 954 - name: SKU954 - - id: 955 - name: SKU955 - - id: 956 - name: SKU956 - - id: 957 - name: SKU957 - - id: 958 - name: SKU958 - - id: 959 - name: SKU959 - - id: 960 - name: SKU960 - - id: 961 - name: SKU961 - - id: 962 - name: SKU962 - - id: 963 - name: SKU963 - - id: 964 - name: SKU964 - - id: 965 - name: SKU965 - - id: 966 - name: SKU966 - - id: 967 - name: SKU967 - - id: 968 - name: SKU968 - - id: 969 - name: SKU969 - - id: 970 - name: SKU970 - - id: 971 - name: SKU971 - - id: 972 - name: SKU972 - - id: 973 - name: SKU973 - - id: 974 - name: SKU974 - - id: 975 - name: SKU975 - - id: 976 - name: SKU976 - - id: 977 - name: SKU977 - - id: 978 - name: SKU978 - - id: 979 - name: SKU979 - - id: 980 - name: SKU980 - - id: 981 - name: SKU981 - - id: 982 - name: SKU982 - - id: 983 - name: SKU983 - - id: 984 - name: SKU984 - - id: 985 - name: SKU985 - - id: 986 - name: SKU986 - - id: 987 - name: SKU987 - - id: 988 - name: SKU988 - - id: 989 - name: SKU989 - - id: 990 - name: SKU990 - - id: 991 - name: SKU991 - - id: 992 - name: SKU992 - - id: 993 - name: SKU993 - - id: 994 - name: SKU994 - - id: 995 - name: SKU995 - - id: 996 - name: SKU996 - - id: 997 - name: SKU997 - - id: 998 - name: SKU998 - - id: 999 - name: SKU999 - topology: - STORE0: - SKU0: - - WAREHOUSE0 - SKU1: - - WAREHOUSE0 - SKU10: - - WAREHOUSE0 - SKU100: - - WAREHOUSE0 - SKU101: - - WAREHOUSE0 - SKU102: - - WAREHOUSE0 - SKU103: - - WAREHOUSE0 - SKU104: - - WAREHOUSE0 - SKU105: - - WAREHOUSE0 - SKU106: - - WAREHOUSE0 - SKU107: - - WAREHOUSE0 - SKU108: - - WAREHOUSE0 - SKU109: - - WAREHOUSE0 - SKU11: - - WAREHOUSE0 - SKU110: - - WAREHOUSE0 - SKU111: - - WAREHOUSE0 - SKU112: - - WAREHOUSE0 - SKU113: - - WAREHOUSE0 - SKU114: - - WAREHOUSE0 - SKU115: - - WAREHOUSE0 - SKU116: - - WAREHOUSE0 - SKU117: - - WAREHOUSE0 - SKU118: - - WAREHOUSE0 - SKU119: - - WAREHOUSE0 - SKU12: - - WAREHOUSE0 - SKU120: - - WAREHOUSE0 - SKU121: - - WAREHOUSE0 - SKU122: - - WAREHOUSE0 - SKU123: - - WAREHOUSE0 - SKU124: - - WAREHOUSE0 - SKU125: - - WAREHOUSE0 - SKU126: - - WAREHOUSE0 - SKU127: - - WAREHOUSE0 - SKU128: - - WAREHOUSE0 - SKU129: - - WAREHOUSE0 - SKU13: - - WAREHOUSE0 - SKU130: - - WAREHOUSE0 - SKU131: - - WAREHOUSE0 - SKU132: - - WAREHOUSE0 - SKU133: - - WAREHOUSE0 - SKU134: - - WAREHOUSE0 - SKU135: - - WAREHOUSE0 - SKU136: - - WAREHOUSE0 - SKU137: - - WAREHOUSE0 - SKU138: - - WAREHOUSE0 - SKU139: - - WAREHOUSE0 - SKU14: - - WAREHOUSE0 - SKU140: - - WAREHOUSE0 - SKU141: - - WAREHOUSE0 - SKU142: - - WAREHOUSE0 - SKU143: - - WAREHOUSE0 - SKU144: - - WAREHOUSE0 - SKU145: - - WAREHOUSE0 - SKU146: - - WAREHOUSE0 - SKU147: - - WAREHOUSE0 - SKU148: - - WAREHOUSE0 - SKU149: - - WAREHOUSE0 - SKU15: - - WAREHOUSE0 - SKU150: - - WAREHOUSE0 - SKU151: - - WAREHOUSE0 - SKU152: - - WAREHOUSE0 - SKU153: - - WAREHOUSE0 - SKU154: - - WAREHOUSE0 - SKU155: - - WAREHOUSE0 - SKU156: - - WAREHOUSE0 - SKU157: - - WAREHOUSE0 - SKU158: - - WAREHOUSE0 - SKU159: - - WAREHOUSE0 - SKU16: - - WAREHOUSE0 - SKU160: - - WAREHOUSE0 - SKU161: - - WAREHOUSE0 - SKU162: - - WAREHOUSE0 - SKU163: - - WAREHOUSE0 - SKU164: - - WAREHOUSE0 - SKU165: - - WAREHOUSE0 - SKU166: - - WAREHOUSE0 - SKU167: - - WAREHOUSE0 - SKU168: - - WAREHOUSE0 - SKU169: - - WAREHOUSE0 - SKU17: - - WAREHOUSE0 - SKU170: - - WAREHOUSE0 - SKU171: - - WAREHOUSE0 - SKU172: - - WAREHOUSE0 - SKU173: - - WAREHOUSE0 - SKU174: - - WAREHOUSE0 - SKU175: - - WAREHOUSE0 - SKU176: - - WAREHOUSE0 - SKU177: - - WAREHOUSE0 - SKU178: - - WAREHOUSE0 - SKU179: - - WAREHOUSE0 - SKU18: - - WAREHOUSE0 - SKU180: - - WAREHOUSE0 - SKU181: - - WAREHOUSE0 - SKU182: - - WAREHOUSE0 - SKU183: - - WAREHOUSE0 - SKU184: - - WAREHOUSE0 - SKU185: - - WAREHOUSE0 - SKU186: - - WAREHOUSE0 - SKU187: - - WAREHOUSE0 - SKU188: - - WAREHOUSE0 - SKU189: - - WAREHOUSE0 - SKU19: - - WAREHOUSE0 - SKU190: - - WAREHOUSE0 - SKU191: - - WAREHOUSE0 - SKU192: - - WAREHOUSE0 - SKU193: - - WAREHOUSE0 - SKU194: - - WAREHOUSE0 - SKU195: - - WAREHOUSE0 - SKU196: - - WAREHOUSE0 - SKU197: - - WAREHOUSE0 - SKU198: - - WAREHOUSE0 - SKU199: - - WAREHOUSE0 - SKU2: - - WAREHOUSE0 - SKU20: - - WAREHOUSE0 - SKU200: - - WAREHOUSE0 - SKU201: - - WAREHOUSE0 - SKU202: - - WAREHOUSE0 - SKU203: - - WAREHOUSE0 - SKU204: - - WAREHOUSE0 - SKU205: - - WAREHOUSE0 - SKU206: - - WAREHOUSE0 - SKU207: - - WAREHOUSE0 - SKU208: - - WAREHOUSE0 - SKU209: - - WAREHOUSE0 - SKU21: - - WAREHOUSE0 - SKU210: - - WAREHOUSE0 - SKU211: - - WAREHOUSE0 - SKU212: - - WAREHOUSE0 - SKU213: - - WAREHOUSE0 - SKU214: - - WAREHOUSE0 - SKU215: - - WAREHOUSE0 - SKU216: - - WAREHOUSE0 - SKU217: - - WAREHOUSE0 - SKU218: - - WAREHOUSE0 - SKU219: - - WAREHOUSE0 - SKU22: - - WAREHOUSE0 - SKU220: - - WAREHOUSE0 - SKU221: - - WAREHOUSE0 - SKU222: - - WAREHOUSE0 - SKU223: - - WAREHOUSE0 - SKU224: - - WAREHOUSE0 - SKU225: - - WAREHOUSE0 - SKU226: - - WAREHOUSE0 - SKU227: - - WAREHOUSE0 - SKU228: - - WAREHOUSE0 - SKU229: - - WAREHOUSE0 - SKU23: - - WAREHOUSE0 - SKU230: - - WAREHOUSE0 - SKU231: - - WAREHOUSE0 - SKU232: - - WAREHOUSE0 - SKU233: - - WAREHOUSE0 - SKU234: - - WAREHOUSE0 - SKU235: - - WAREHOUSE0 - SKU236: - - WAREHOUSE0 - SKU237: - - WAREHOUSE0 - SKU238: - - WAREHOUSE0 - SKU239: - - WAREHOUSE0 - SKU24: - - WAREHOUSE0 - SKU240: - - WAREHOUSE0 - SKU241: - - WAREHOUSE0 - SKU242: - - WAREHOUSE0 - SKU243: - - WAREHOUSE0 - SKU244: - - WAREHOUSE0 - SKU245: - - WAREHOUSE0 - SKU246: - - WAREHOUSE0 - SKU247: - - WAREHOUSE0 - SKU248: - - WAREHOUSE0 - SKU249: - - WAREHOUSE0 - SKU25: - - WAREHOUSE0 - SKU250: - - WAREHOUSE0 - SKU251: - - WAREHOUSE0 - SKU252: - - WAREHOUSE0 - SKU253: - - WAREHOUSE0 - SKU254: - - WAREHOUSE0 - SKU255: - - WAREHOUSE0 - SKU256: - - WAREHOUSE0 - SKU257: - - WAREHOUSE0 - SKU258: - - WAREHOUSE0 - SKU259: - - WAREHOUSE0 - SKU26: - - WAREHOUSE0 - SKU260: - - WAREHOUSE0 - SKU261: - - WAREHOUSE0 - SKU262: - - WAREHOUSE0 - SKU263: - - WAREHOUSE0 - SKU264: - - WAREHOUSE0 - SKU265: - - WAREHOUSE0 - SKU266: - - WAREHOUSE0 - SKU267: - - WAREHOUSE0 - SKU268: - - WAREHOUSE0 - SKU269: - - WAREHOUSE0 - SKU27: - - WAREHOUSE0 - SKU270: - - WAREHOUSE0 - SKU271: - - WAREHOUSE0 - SKU272: - - WAREHOUSE0 - SKU273: - - WAREHOUSE0 - SKU274: - - WAREHOUSE0 - SKU275: - - WAREHOUSE0 - SKU276: - - WAREHOUSE0 - SKU277: - - WAREHOUSE0 - SKU278: - - WAREHOUSE0 - SKU279: - - WAREHOUSE0 - SKU28: - - WAREHOUSE0 - SKU280: - - WAREHOUSE0 - SKU281: - - WAREHOUSE0 - SKU282: - - WAREHOUSE0 - SKU283: - - WAREHOUSE0 - SKU284: - - WAREHOUSE0 - SKU285: - - WAREHOUSE0 - SKU286: - - WAREHOUSE0 - SKU287: - - WAREHOUSE0 - SKU288: - - WAREHOUSE0 - SKU289: - - WAREHOUSE0 - SKU29: - - WAREHOUSE0 - SKU290: - - WAREHOUSE0 - SKU291: - - WAREHOUSE0 - SKU292: - - WAREHOUSE0 - SKU293: - - WAREHOUSE0 - SKU294: - - WAREHOUSE0 - SKU295: - - WAREHOUSE0 - SKU296: - - WAREHOUSE0 - SKU297: - - WAREHOUSE0 - SKU298: - - WAREHOUSE0 - SKU299: - - WAREHOUSE0 - SKU3: - - WAREHOUSE0 - SKU30: - - WAREHOUSE0 - SKU300: - - WAREHOUSE0 - SKU301: - - WAREHOUSE0 - SKU302: - - WAREHOUSE0 - SKU303: - - WAREHOUSE0 - SKU304: - - WAREHOUSE0 - SKU305: - - WAREHOUSE0 - SKU306: - - WAREHOUSE0 - SKU307: - - WAREHOUSE0 - SKU308: - - WAREHOUSE0 - SKU309: - - WAREHOUSE0 - SKU31: - - WAREHOUSE0 - SKU310: - - WAREHOUSE0 - SKU311: - - WAREHOUSE0 - SKU312: - - WAREHOUSE0 - SKU313: - - WAREHOUSE0 - SKU314: - - WAREHOUSE0 - SKU315: - - WAREHOUSE0 - SKU316: - - WAREHOUSE0 - SKU317: - - WAREHOUSE0 - SKU318: - - WAREHOUSE0 - SKU319: - - WAREHOUSE0 - SKU32: - - WAREHOUSE0 - SKU320: - - WAREHOUSE0 - SKU321: - - WAREHOUSE0 - SKU322: - - WAREHOUSE0 - SKU323: - - WAREHOUSE0 - SKU324: - - WAREHOUSE0 - SKU325: - - WAREHOUSE0 - SKU326: - - WAREHOUSE0 - SKU327: - - WAREHOUSE0 - SKU328: - - WAREHOUSE0 - SKU329: - - WAREHOUSE0 - SKU33: - - WAREHOUSE0 - SKU330: - - WAREHOUSE0 - SKU331: - - WAREHOUSE0 - SKU332: - - WAREHOUSE0 - SKU333: - - WAREHOUSE0 - SKU334: - - WAREHOUSE0 - SKU335: - - WAREHOUSE0 - SKU336: - - WAREHOUSE0 - SKU337: - - WAREHOUSE0 - SKU338: - - WAREHOUSE0 - SKU339: - - WAREHOUSE0 - SKU34: - - WAREHOUSE0 - SKU340: - - WAREHOUSE0 - SKU341: - - WAREHOUSE0 - SKU342: - - WAREHOUSE0 - SKU343: - - WAREHOUSE0 - SKU344: - - WAREHOUSE0 - SKU345: - - WAREHOUSE0 - SKU346: - - WAREHOUSE0 - SKU347: - - WAREHOUSE0 - SKU348: - - WAREHOUSE0 - SKU349: - - WAREHOUSE0 - SKU35: - - WAREHOUSE0 - SKU350: - - WAREHOUSE0 - SKU351: - - WAREHOUSE0 - SKU352: - - WAREHOUSE0 - SKU353: - - WAREHOUSE0 - SKU354: - - WAREHOUSE0 - SKU355: - - WAREHOUSE0 - SKU356: - - WAREHOUSE0 - SKU357: - - WAREHOUSE0 - SKU358: - - WAREHOUSE0 - SKU359: - - WAREHOUSE0 - SKU36: - - WAREHOUSE0 - SKU360: - - WAREHOUSE0 - SKU361: - - WAREHOUSE0 - SKU362: - - WAREHOUSE0 - SKU363: - - WAREHOUSE0 - SKU364: - - WAREHOUSE0 - SKU365: - - WAREHOUSE0 - SKU366: - - WAREHOUSE0 - SKU367: - - WAREHOUSE0 - SKU368: - - WAREHOUSE0 - SKU369: - - WAREHOUSE0 - SKU37: - - WAREHOUSE0 - SKU370: - - WAREHOUSE0 - SKU371: - - WAREHOUSE0 - SKU372: - - WAREHOUSE0 - SKU373: - - WAREHOUSE0 - SKU374: - - WAREHOUSE0 - SKU375: - - WAREHOUSE0 - SKU376: - - WAREHOUSE0 - SKU377: - - WAREHOUSE0 - SKU378: - - WAREHOUSE0 - SKU379: - - WAREHOUSE0 - SKU38: - - WAREHOUSE0 - SKU380: - - WAREHOUSE0 - SKU381: - - WAREHOUSE0 - SKU382: - - WAREHOUSE0 - SKU383: - - WAREHOUSE0 - SKU384: - - WAREHOUSE0 - SKU385: - - WAREHOUSE0 - SKU386: - - WAREHOUSE0 - SKU387: - - WAREHOUSE0 - SKU388: - - WAREHOUSE0 - SKU389: - - WAREHOUSE0 - SKU39: - - WAREHOUSE0 - SKU390: - - WAREHOUSE0 - SKU391: - - WAREHOUSE0 - SKU392: - - WAREHOUSE0 - SKU393: - - WAREHOUSE0 - SKU394: - - WAREHOUSE0 - SKU395: - - WAREHOUSE0 - SKU396: - - WAREHOUSE0 - SKU397: - - WAREHOUSE0 - SKU398: - - WAREHOUSE0 - SKU399: - - WAREHOUSE0 - SKU4: - - WAREHOUSE0 - SKU40: - - WAREHOUSE0 - SKU400: - - WAREHOUSE0 - SKU401: - - WAREHOUSE0 - SKU402: - - WAREHOUSE0 - SKU403: - - WAREHOUSE0 - SKU404: - - WAREHOUSE0 - SKU405: - - WAREHOUSE0 - SKU406: - - WAREHOUSE0 - SKU407: - - WAREHOUSE0 - SKU408: - - WAREHOUSE0 - SKU409: - - WAREHOUSE0 - SKU41: - - WAREHOUSE0 - SKU410: - - WAREHOUSE0 - SKU411: - - WAREHOUSE0 - SKU412: - - WAREHOUSE0 - SKU413: - - WAREHOUSE0 - SKU414: - - WAREHOUSE0 - SKU415: - - WAREHOUSE0 - SKU416: - - WAREHOUSE0 - SKU417: - - WAREHOUSE0 - SKU418: - - WAREHOUSE0 - SKU419: - - WAREHOUSE0 - SKU42: - - WAREHOUSE0 - SKU420: - - WAREHOUSE0 - SKU421: - - WAREHOUSE0 - SKU422: - - WAREHOUSE0 - SKU423: - - WAREHOUSE0 - SKU424: - - WAREHOUSE0 - SKU425: - - WAREHOUSE0 - SKU426: - - WAREHOUSE0 - SKU427: - - WAREHOUSE0 - SKU428: - - WAREHOUSE0 - SKU429: - - WAREHOUSE0 - SKU43: - - WAREHOUSE0 - SKU430: - - WAREHOUSE0 - SKU431: - - WAREHOUSE0 - SKU432: - - WAREHOUSE0 - SKU433: - - WAREHOUSE0 - SKU434: - - WAREHOUSE0 - SKU435: - - WAREHOUSE0 - SKU436: - - WAREHOUSE0 - SKU437: - - WAREHOUSE0 - SKU438: - - WAREHOUSE0 - SKU439: - - WAREHOUSE0 - SKU44: - - WAREHOUSE0 - SKU440: - - WAREHOUSE0 - SKU441: - - WAREHOUSE0 - SKU442: - - WAREHOUSE0 - SKU443: - - WAREHOUSE0 - SKU444: - - WAREHOUSE0 - SKU445: - - WAREHOUSE0 - SKU446: - - WAREHOUSE0 - SKU447: - - WAREHOUSE0 - SKU448: - - WAREHOUSE0 - SKU449: - - WAREHOUSE0 - SKU45: - - WAREHOUSE0 - SKU450: - - WAREHOUSE0 - SKU451: - - WAREHOUSE0 - SKU452: - - WAREHOUSE0 - SKU453: - - WAREHOUSE0 - SKU454: - - WAREHOUSE0 - SKU455: - - WAREHOUSE0 - SKU456: - - WAREHOUSE0 - SKU457: - - WAREHOUSE0 - SKU458: - - WAREHOUSE0 - SKU459: - - WAREHOUSE0 - SKU46: - - WAREHOUSE0 - SKU460: - - WAREHOUSE0 - SKU461: - - WAREHOUSE0 - SKU462: - - WAREHOUSE0 - SKU463: - - WAREHOUSE0 - SKU464: - - WAREHOUSE0 - SKU465: - - WAREHOUSE0 - SKU466: - - WAREHOUSE0 - SKU467: - - WAREHOUSE0 - SKU468: - - WAREHOUSE0 - SKU469: - - WAREHOUSE0 - SKU47: - - WAREHOUSE0 - SKU470: - - WAREHOUSE0 - SKU471: - - WAREHOUSE0 - SKU472: - - WAREHOUSE0 - SKU473: - - WAREHOUSE0 - SKU474: - - WAREHOUSE0 - SKU475: - - WAREHOUSE0 - SKU476: - - WAREHOUSE0 - SKU477: - - WAREHOUSE0 - SKU478: - - WAREHOUSE0 - SKU479: - - WAREHOUSE0 - SKU48: - - WAREHOUSE0 - SKU480: - - WAREHOUSE0 - SKU481: - - WAREHOUSE0 - SKU482: - - WAREHOUSE0 - SKU483: - - WAREHOUSE0 - SKU484: - - WAREHOUSE0 - SKU485: - - WAREHOUSE0 - SKU486: - - WAREHOUSE0 - SKU487: - - WAREHOUSE0 - SKU488: - - WAREHOUSE0 - SKU489: - - WAREHOUSE0 - SKU49: - - WAREHOUSE0 - SKU490: - - WAREHOUSE0 - SKU491: - - WAREHOUSE0 - SKU492: - - WAREHOUSE0 - SKU493: - - WAREHOUSE0 - SKU494: - - WAREHOUSE0 - SKU495: - - WAREHOUSE0 - SKU496: - - WAREHOUSE0 - SKU497: - - WAREHOUSE0 - SKU498: - - WAREHOUSE0 - SKU499: - - WAREHOUSE0 - SKU5: - - WAREHOUSE0 - SKU50: - - WAREHOUSE0 - SKU500: - - WAREHOUSE0 - SKU501: - - WAREHOUSE0 - SKU502: - - WAREHOUSE0 - SKU503: - - WAREHOUSE0 - SKU504: - - WAREHOUSE0 - SKU505: - - WAREHOUSE0 - SKU506: - - WAREHOUSE0 - SKU507: - - WAREHOUSE0 - SKU508: - - WAREHOUSE0 - SKU509: - - WAREHOUSE0 - SKU51: - - WAREHOUSE0 - SKU510: - - WAREHOUSE0 - SKU511: - - WAREHOUSE0 - SKU512: - - WAREHOUSE0 - SKU513: - - WAREHOUSE0 - SKU514: - - WAREHOUSE0 - SKU515: - - WAREHOUSE0 - SKU516: - - WAREHOUSE0 - SKU517: - - WAREHOUSE0 - SKU518: - - WAREHOUSE0 - SKU519: - - WAREHOUSE0 - SKU52: - - WAREHOUSE0 - SKU520: - - WAREHOUSE0 - SKU521: - - WAREHOUSE0 - SKU522: - - WAREHOUSE0 - SKU523: - - WAREHOUSE0 - SKU524: - - WAREHOUSE0 - SKU525: - - WAREHOUSE0 - SKU526: - - WAREHOUSE0 - SKU527: - - WAREHOUSE0 - SKU528: - - WAREHOUSE0 - SKU529: - - WAREHOUSE0 - SKU53: - - WAREHOUSE0 - SKU530: - - WAREHOUSE0 - SKU531: - - WAREHOUSE0 - SKU532: - - WAREHOUSE0 - SKU533: - - WAREHOUSE0 - SKU534: - - WAREHOUSE0 - SKU535: - - WAREHOUSE0 - SKU536: - - WAREHOUSE0 - SKU537: - - WAREHOUSE0 - SKU538: - - WAREHOUSE0 - SKU539: - - WAREHOUSE0 - SKU54: - - WAREHOUSE0 - SKU540: - - WAREHOUSE0 - SKU541: - - WAREHOUSE0 - SKU542: - - WAREHOUSE0 - SKU543: - - WAREHOUSE0 - SKU544: - - WAREHOUSE0 - SKU545: - - WAREHOUSE0 - SKU546: - - WAREHOUSE0 - SKU547: - - WAREHOUSE0 - SKU548: - - WAREHOUSE0 - SKU549: - - WAREHOUSE0 - SKU55: - - WAREHOUSE0 - SKU550: - - WAREHOUSE0 - SKU551: - - WAREHOUSE0 - SKU552: - - WAREHOUSE0 - SKU553: - - WAREHOUSE0 - SKU554: - - WAREHOUSE0 - SKU555: - - WAREHOUSE0 - SKU556: - - WAREHOUSE0 - SKU557: - - WAREHOUSE0 - SKU558: - - WAREHOUSE0 - SKU559: - - WAREHOUSE0 - SKU56: - - WAREHOUSE0 - SKU560: - - WAREHOUSE0 - SKU561: - - WAREHOUSE0 - SKU562: - - WAREHOUSE0 - SKU563: - - WAREHOUSE0 - SKU564: - - WAREHOUSE0 - SKU565: - - WAREHOUSE0 - SKU566: - - WAREHOUSE0 - SKU567: - - WAREHOUSE0 - SKU568: - - WAREHOUSE0 - SKU569: - - WAREHOUSE0 - SKU57: - - WAREHOUSE0 - SKU570: - - WAREHOUSE0 - SKU571: - - WAREHOUSE0 - SKU572: - - WAREHOUSE0 - SKU573: - - WAREHOUSE0 - SKU574: - - WAREHOUSE0 - SKU575: - - WAREHOUSE0 - SKU576: - - WAREHOUSE0 - SKU577: - - WAREHOUSE0 - SKU578: - - WAREHOUSE0 - SKU579: - - WAREHOUSE0 - SKU58: - - WAREHOUSE0 - SKU580: - - WAREHOUSE0 - SKU581: - - WAREHOUSE0 - SKU582: - - WAREHOUSE0 - SKU583: - - WAREHOUSE0 - SKU584: - - WAREHOUSE0 - SKU585: - - WAREHOUSE0 - SKU586: - - WAREHOUSE0 - SKU587: - - WAREHOUSE0 - SKU588: - - WAREHOUSE0 - SKU589: - - WAREHOUSE0 - SKU59: - - WAREHOUSE0 - SKU590: - - WAREHOUSE0 - SKU591: - - WAREHOUSE0 - SKU592: - - WAREHOUSE0 - SKU593: - - WAREHOUSE0 - SKU594: - - WAREHOUSE0 - SKU595: - - WAREHOUSE0 - SKU596: - - WAREHOUSE0 - SKU597: - - WAREHOUSE0 - SKU598: - - WAREHOUSE0 - SKU599: - - WAREHOUSE0 - SKU6: - - WAREHOUSE0 - SKU60: - - WAREHOUSE0 - SKU600: - - WAREHOUSE0 - SKU601: - - WAREHOUSE0 - SKU602: - - WAREHOUSE0 - SKU603: - - WAREHOUSE0 - SKU604: - - WAREHOUSE0 - SKU605: - - WAREHOUSE0 - SKU606: - - WAREHOUSE0 - SKU607: - - WAREHOUSE0 - SKU608: - - WAREHOUSE0 - SKU609: - - WAREHOUSE0 - SKU61: - - WAREHOUSE0 - SKU610: - - WAREHOUSE0 - SKU611: - - WAREHOUSE0 - SKU612: - - WAREHOUSE0 - SKU613: - - WAREHOUSE0 - SKU614: - - WAREHOUSE0 - SKU615: - - WAREHOUSE0 - SKU616: - - WAREHOUSE0 - SKU617: - - WAREHOUSE0 - SKU618: - - WAREHOUSE0 - SKU619: - - WAREHOUSE0 - SKU62: - - WAREHOUSE0 - SKU620: - - WAREHOUSE0 - SKU621: - - WAREHOUSE0 - SKU622: - - WAREHOUSE0 - SKU623: - - WAREHOUSE0 - SKU624: - - WAREHOUSE0 - SKU625: - - WAREHOUSE0 - SKU626: - - WAREHOUSE0 - SKU627: - - WAREHOUSE0 - SKU628: - - WAREHOUSE0 - SKU629: - - WAREHOUSE0 - SKU63: - - WAREHOUSE0 - SKU630: - - WAREHOUSE0 - SKU631: - - WAREHOUSE0 - SKU632: - - WAREHOUSE0 - SKU633: - - WAREHOUSE0 - SKU634: - - WAREHOUSE0 - SKU635: - - WAREHOUSE0 - SKU636: - - WAREHOUSE0 - SKU637: - - WAREHOUSE0 - SKU638: - - WAREHOUSE0 - SKU639: - - WAREHOUSE0 - SKU64: - - WAREHOUSE0 - SKU640: - - WAREHOUSE0 - SKU641: - - WAREHOUSE0 - SKU642: - - WAREHOUSE0 - SKU643: - - WAREHOUSE0 - SKU644: - - WAREHOUSE0 - SKU645: - - WAREHOUSE0 - SKU646: - - WAREHOUSE0 - SKU647: - - WAREHOUSE0 - SKU648: - - WAREHOUSE0 - SKU649: - - WAREHOUSE0 - SKU65: - - WAREHOUSE0 - SKU650: - - WAREHOUSE0 - SKU651: - - WAREHOUSE0 - SKU652: - - WAREHOUSE0 - SKU653: - - WAREHOUSE0 - SKU654: - - WAREHOUSE0 - SKU655: - - WAREHOUSE0 - SKU656: - - WAREHOUSE0 - SKU657: - - WAREHOUSE0 - SKU658: - - WAREHOUSE0 - SKU659: - - WAREHOUSE0 - SKU66: - - WAREHOUSE0 - SKU660: - - WAREHOUSE0 - SKU661: - - WAREHOUSE0 - SKU662: - - WAREHOUSE0 - SKU663: - - WAREHOUSE0 - SKU664: - - WAREHOUSE0 - SKU665: - - WAREHOUSE0 - SKU666: - - WAREHOUSE0 - SKU667: - - WAREHOUSE0 - SKU668: - - WAREHOUSE0 - SKU669: - - WAREHOUSE0 - SKU67: - - WAREHOUSE0 - SKU670: - - WAREHOUSE0 - SKU671: - - WAREHOUSE0 - SKU672: - - WAREHOUSE0 - SKU673: - - WAREHOUSE0 - SKU674: - - WAREHOUSE0 - SKU675: - - WAREHOUSE0 - SKU676: - - WAREHOUSE0 - SKU677: - - WAREHOUSE0 - SKU678: - - WAREHOUSE0 - SKU679: - - WAREHOUSE0 - SKU68: - - WAREHOUSE0 - SKU680: - - WAREHOUSE0 - SKU681: - - WAREHOUSE0 - SKU682: - - WAREHOUSE0 - SKU683: - - WAREHOUSE0 - SKU684: - - WAREHOUSE0 - SKU685: - - WAREHOUSE0 - SKU686: - - WAREHOUSE0 - SKU687: - - WAREHOUSE0 - SKU688: - - WAREHOUSE0 - SKU689: - - WAREHOUSE0 - SKU69: - - WAREHOUSE0 - SKU690: - - WAREHOUSE0 - SKU691: - - WAREHOUSE0 - SKU692: - - WAREHOUSE0 - SKU693: - - WAREHOUSE0 - SKU694: - - WAREHOUSE0 - SKU695: - - WAREHOUSE0 - SKU696: - - WAREHOUSE0 - SKU697: - - WAREHOUSE0 - SKU698: - - WAREHOUSE0 - SKU699: - - WAREHOUSE0 - SKU7: - - WAREHOUSE0 - SKU70: - - WAREHOUSE0 - SKU700: - - WAREHOUSE0 - SKU701: - - WAREHOUSE0 - SKU702: - - WAREHOUSE0 - SKU703: - - WAREHOUSE0 - SKU704: - - WAREHOUSE0 - SKU705: - - WAREHOUSE0 - SKU706: - - WAREHOUSE0 - SKU707: - - WAREHOUSE0 - SKU708: - - WAREHOUSE0 - SKU709: - - WAREHOUSE0 - SKU71: - - WAREHOUSE0 - SKU710: - - WAREHOUSE0 - SKU711: - - WAREHOUSE0 - SKU712: - - WAREHOUSE0 - SKU713: - - WAREHOUSE0 - SKU714: - - WAREHOUSE0 - SKU715: - - WAREHOUSE0 - SKU716: - - WAREHOUSE0 - SKU717: - - WAREHOUSE0 - SKU718: - - WAREHOUSE0 - SKU719: - - WAREHOUSE0 - SKU72: - - WAREHOUSE0 - SKU720: - - WAREHOUSE0 - SKU721: - - WAREHOUSE0 - SKU722: - - WAREHOUSE0 - SKU723: - - WAREHOUSE0 - SKU724: - - WAREHOUSE0 - SKU725: - - WAREHOUSE0 - SKU726: - - WAREHOUSE0 - SKU727: - - WAREHOUSE0 - SKU728: - - WAREHOUSE0 - SKU729: - - WAREHOUSE0 - SKU73: - - WAREHOUSE0 - SKU730: - - WAREHOUSE0 - SKU731: - - WAREHOUSE0 - SKU732: - - WAREHOUSE0 - SKU733: - - WAREHOUSE0 - SKU734: - - WAREHOUSE0 - SKU735: - - WAREHOUSE0 - SKU736: - - WAREHOUSE0 - SKU737: - - WAREHOUSE0 - SKU738: - - WAREHOUSE0 - SKU739: - - WAREHOUSE0 - SKU74: - - WAREHOUSE0 - SKU740: - - WAREHOUSE0 - SKU741: - - WAREHOUSE0 - SKU742: - - WAREHOUSE0 - SKU743: - - WAREHOUSE0 - SKU744: - - WAREHOUSE0 - SKU745: - - WAREHOUSE0 - SKU746: - - WAREHOUSE0 - SKU747: - - WAREHOUSE0 - SKU748: - - WAREHOUSE0 - SKU749: - - WAREHOUSE0 - SKU75: - - WAREHOUSE0 - SKU750: - - WAREHOUSE0 - SKU751: - - WAREHOUSE0 - SKU752: - - WAREHOUSE0 - SKU753: - - WAREHOUSE0 - SKU754: - - WAREHOUSE0 - SKU755: - - WAREHOUSE0 - SKU756: - - WAREHOUSE0 - SKU757: - - WAREHOUSE0 - SKU758: - - WAREHOUSE0 - SKU759: - - WAREHOUSE0 - SKU76: - - WAREHOUSE0 - SKU760: - - WAREHOUSE0 - SKU761: - - WAREHOUSE0 - SKU762: - - WAREHOUSE0 - SKU763: - - WAREHOUSE0 - SKU764: - - WAREHOUSE0 - SKU765: - - WAREHOUSE0 - SKU766: - - WAREHOUSE0 - SKU767: - - WAREHOUSE0 - SKU768: - - WAREHOUSE0 - SKU769: - - WAREHOUSE0 - SKU77: - - WAREHOUSE0 - SKU770: - - WAREHOUSE0 - SKU771: - - WAREHOUSE0 - SKU772: - - WAREHOUSE0 - SKU773: - - WAREHOUSE0 - SKU774: - - WAREHOUSE0 - SKU775: - - WAREHOUSE0 - SKU776: - - WAREHOUSE0 - SKU777: - - WAREHOUSE0 - SKU778: - - WAREHOUSE0 - SKU779: - - WAREHOUSE0 - SKU78: - - WAREHOUSE0 - SKU780: - - WAREHOUSE0 - SKU781: - - WAREHOUSE0 - SKU782: - - WAREHOUSE0 - SKU783: - - WAREHOUSE0 - SKU784: - - WAREHOUSE0 - SKU785: - - WAREHOUSE0 - SKU786: - - WAREHOUSE0 - SKU787: - - WAREHOUSE0 - SKU788: - - WAREHOUSE0 - SKU789: - - WAREHOUSE0 - SKU79: - - WAREHOUSE0 - SKU790: - - WAREHOUSE0 - SKU791: - - WAREHOUSE0 - SKU792: - - WAREHOUSE0 - SKU793: - - WAREHOUSE0 - SKU794: - - WAREHOUSE0 - SKU795: - - WAREHOUSE0 - SKU796: - - WAREHOUSE0 - SKU797: - - WAREHOUSE0 - SKU798: - - WAREHOUSE0 - SKU799: - - WAREHOUSE0 - SKU8: - - WAREHOUSE0 - SKU80: - - WAREHOUSE0 - SKU800: - - WAREHOUSE0 - SKU801: - - WAREHOUSE0 - SKU802: - - WAREHOUSE0 - SKU803: - - WAREHOUSE0 - SKU804: - - WAREHOUSE0 - SKU805: - - WAREHOUSE0 - SKU806: - - WAREHOUSE0 - SKU807: - - WAREHOUSE0 - SKU808: - - WAREHOUSE0 - SKU809: - - WAREHOUSE0 - SKU81: - - WAREHOUSE0 - SKU810: - - WAREHOUSE0 - SKU811: - - WAREHOUSE0 - SKU812: - - WAREHOUSE0 - SKU813: - - WAREHOUSE0 - SKU814: - - WAREHOUSE0 - SKU815: - - WAREHOUSE0 - SKU816: - - WAREHOUSE0 - SKU817: - - WAREHOUSE0 - SKU818: - - WAREHOUSE0 - SKU819: - - WAREHOUSE0 - SKU82: - - WAREHOUSE0 - SKU820: - - WAREHOUSE0 - SKU821: - - WAREHOUSE0 - SKU822: - - WAREHOUSE0 - SKU823: - - WAREHOUSE0 - SKU824: - - WAREHOUSE0 - SKU825: - - WAREHOUSE0 - SKU826: - - WAREHOUSE0 - SKU827: - - WAREHOUSE0 - SKU828: - - WAREHOUSE0 - SKU829: - - WAREHOUSE0 - SKU83: - - WAREHOUSE0 - SKU830: - - WAREHOUSE0 - SKU831: - - WAREHOUSE0 - SKU832: - - WAREHOUSE0 - SKU833: - - WAREHOUSE0 - SKU834: - - WAREHOUSE0 - SKU835: - - WAREHOUSE0 - SKU836: - - WAREHOUSE0 - SKU837: - - WAREHOUSE0 - SKU838: - - WAREHOUSE0 - SKU839: - - WAREHOUSE0 - SKU84: - - WAREHOUSE0 - SKU840: - - WAREHOUSE0 - SKU841: - - WAREHOUSE0 - SKU842: - - WAREHOUSE0 - SKU843: - - WAREHOUSE0 - SKU844: - - WAREHOUSE0 - SKU845: - - WAREHOUSE0 - SKU846: - - WAREHOUSE0 - SKU847: - - WAREHOUSE0 - SKU848: - - WAREHOUSE0 - SKU849: - - WAREHOUSE0 - SKU85: - - WAREHOUSE0 - SKU850: - - WAREHOUSE0 - SKU851: - - WAREHOUSE0 - SKU852: - - WAREHOUSE0 - SKU853: - - WAREHOUSE0 - SKU854: - - WAREHOUSE0 - SKU855: - - WAREHOUSE0 - SKU856: - - WAREHOUSE0 - SKU857: - - WAREHOUSE0 - SKU858: - - WAREHOUSE0 - SKU859: - - WAREHOUSE0 - SKU86: - - WAREHOUSE0 - SKU860: - - WAREHOUSE0 - SKU861: - - WAREHOUSE0 - SKU862: - - WAREHOUSE0 - SKU863: - - WAREHOUSE0 - SKU864: - - WAREHOUSE0 - SKU865: - - WAREHOUSE0 - SKU866: - - WAREHOUSE0 - SKU867: - - WAREHOUSE0 - SKU868: - - WAREHOUSE0 - SKU869: - - WAREHOUSE0 - SKU87: - - WAREHOUSE0 - SKU870: - - WAREHOUSE0 - SKU871: - - WAREHOUSE0 - SKU872: - - WAREHOUSE0 - SKU873: - - WAREHOUSE0 - SKU874: - - WAREHOUSE0 - SKU875: - - WAREHOUSE0 - SKU876: - - WAREHOUSE0 - SKU877: - - WAREHOUSE0 - SKU878: - - WAREHOUSE0 - SKU879: - - WAREHOUSE0 - SKU88: - - WAREHOUSE0 - SKU880: - - WAREHOUSE0 - SKU881: - - WAREHOUSE0 - SKU882: - - WAREHOUSE0 - SKU883: - - WAREHOUSE0 - SKU884: - - WAREHOUSE0 - SKU885: - - WAREHOUSE0 - SKU886: - - WAREHOUSE0 - SKU887: - - WAREHOUSE0 - SKU888: - - WAREHOUSE0 - SKU889: - - WAREHOUSE0 - SKU89: - - WAREHOUSE0 - SKU890: - - WAREHOUSE0 - SKU891: - - WAREHOUSE0 - SKU892: - - WAREHOUSE0 - SKU893: - - WAREHOUSE0 - SKU894: - - WAREHOUSE0 - SKU895: - - WAREHOUSE0 - SKU896: - - WAREHOUSE0 - SKU897: - - WAREHOUSE0 - SKU898: - - WAREHOUSE0 - SKU899: - - WAREHOUSE0 - SKU9: - - WAREHOUSE0 - SKU90: - - WAREHOUSE0 - SKU900: - - WAREHOUSE0 - SKU901: - - WAREHOUSE0 - SKU902: - - WAREHOUSE0 - SKU903: - - WAREHOUSE0 - SKU904: - - WAREHOUSE0 - SKU905: - - WAREHOUSE0 - SKU906: - - WAREHOUSE0 - SKU907: - - WAREHOUSE0 - SKU908: - - WAREHOUSE0 - SKU909: - - WAREHOUSE0 - SKU91: - - WAREHOUSE0 - SKU910: - - WAREHOUSE0 - SKU911: - - WAREHOUSE0 - SKU912: - - WAREHOUSE0 - SKU913: - - WAREHOUSE0 - SKU914: - - WAREHOUSE0 - SKU915: - - WAREHOUSE0 - SKU916: - - WAREHOUSE0 - SKU917: - - WAREHOUSE0 - SKU918: - - WAREHOUSE0 - SKU919: - - WAREHOUSE0 - SKU92: - - WAREHOUSE0 - SKU920: - - WAREHOUSE0 - SKU921: - - WAREHOUSE0 - SKU922: - - WAREHOUSE0 - SKU923: - - WAREHOUSE0 - SKU924: - - WAREHOUSE0 - SKU925: - - WAREHOUSE0 - SKU926: - - WAREHOUSE0 - SKU927: - - WAREHOUSE0 - SKU928: - - WAREHOUSE0 - SKU929: - - WAREHOUSE0 - SKU93: - - WAREHOUSE0 - SKU930: - - WAREHOUSE0 - SKU931: - - WAREHOUSE0 - SKU932: - - WAREHOUSE0 - SKU933: - - WAREHOUSE0 - SKU934: - - WAREHOUSE0 - SKU935: - - WAREHOUSE0 - SKU936: - - WAREHOUSE0 - SKU937: - - WAREHOUSE0 - SKU938: - - WAREHOUSE0 - SKU939: - - WAREHOUSE0 - SKU94: - - WAREHOUSE0 - SKU940: - - WAREHOUSE0 - SKU941: - - WAREHOUSE0 - SKU942: - - WAREHOUSE0 - SKU943: - - WAREHOUSE0 - SKU944: - - WAREHOUSE0 - SKU945: - - WAREHOUSE0 - SKU946: - - WAREHOUSE0 - SKU947: - - WAREHOUSE0 - SKU948: - - WAREHOUSE0 - SKU949: - - WAREHOUSE0 - SKU95: - - WAREHOUSE0 - SKU950: - - WAREHOUSE0 - SKU951: - - WAREHOUSE0 - SKU952: - - WAREHOUSE0 - SKU953: - - WAREHOUSE0 - SKU954: - - WAREHOUSE0 - SKU955: - - WAREHOUSE0 - SKU956: - - WAREHOUSE0 - SKU957: - - WAREHOUSE0 - SKU958: - - WAREHOUSE0 - SKU959: - - WAREHOUSE0 - SKU96: - - WAREHOUSE0 - SKU960: - - WAREHOUSE0 - SKU961: - - WAREHOUSE0 - SKU962: - - WAREHOUSE0 - SKU963: - - WAREHOUSE0 - SKU964: - - WAREHOUSE0 - SKU965: - - WAREHOUSE0 - SKU966: - - WAREHOUSE0 - SKU967: - - WAREHOUSE0 - SKU968: - - WAREHOUSE0 - SKU969: - - WAREHOUSE0 - SKU97: - - WAREHOUSE0 - SKU970: - - WAREHOUSE0 - SKU971: - - WAREHOUSE0 - SKU972: - - WAREHOUSE0 - SKU973: - - WAREHOUSE0 - SKU974: - - WAREHOUSE0 - SKU975: - - WAREHOUSE0 - SKU976: - - WAREHOUSE0 - SKU977: - - WAREHOUSE0 - SKU978: - - WAREHOUSE0 - SKU979: - - WAREHOUSE0 - SKU98: - - WAREHOUSE0 - SKU980: - - WAREHOUSE0 - SKU981: - - WAREHOUSE0 - SKU982: - - WAREHOUSE0 - SKU983: - - WAREHOUSE0 - SKU984: - - WAREHOUSE0 - SKU985: - - WAREHOUSE0 - SKU986: - - WAREHOUSE0 - SKU987: - - WAREHOUSE0 - SKU988: - - WAREHOUSE0 - SKU989: - - WAREHOUSE0 - SKU99: - - WAREHOUSE0 - SKU990: - - WAREHOUSE0 - SKU991: - - WAREHOUSE0 - SKU992: - - WAREHOUSE0 - SKU993: - - WAREHOUSE0 - SKU994: - - WAREHOUSE0 - SKU995: - - WAREHOUSE0 - SKU996: - - WAREHOUSE0 - SKU997: - - WAREHOUSE0 - SKU998: - - WAREHOUSE0 - SKU999: - - WAREHOUSE0 - WAREHOUSE0: - SKU0: - - SUPPLIER0 - SKU1: - - SUPPLIER0 - SKU10: - - SUPPLIER0 - SKU100: - - SUPPLIER0 - SKU101: - - SUPPLIER0 - SKU102: - - SUPPLIER0 - SKU103: - - SUPPLIER0 - SKU104: - - SUPPLIER0 - SKU105: - - SUPPLIER0 - SKU106: - - SUPPLIER0 - SKU107: - - SUPPLIER0 - SKU108: - - SUPPLIER0 - SKU109: - - SUPPLIER0 - SKU11: - - SUPPLIER0 - SKU110: - - SUPPLIER0 - SKU111: - - SUPPLIER0 - SKU112: - - SUPPLIER0 - SKU113: - - SUPPLIER0 - SKU114: - - SUPPLIER0 - SKU115: - - SUPPLIER0 - SKU116: - - SUPPLIER0 - SKU117: - - SUPPLIER0 - SKU118: - - SUPPLIER0 - SKU119: - - SUPPLIER0 - SKU12: - - SUPPLIER0 - SKU120: - - SUPPLIER0 - SKU121: - - SUPPLIER0 - SKU122: - - SUPPLIER0 - SKU123: - - SUPPLIER0 - SKU124: - - SUPPLIER0 - SKU125: - - SUPPLIER0 - SKU126: - - SUPPLIER0 - SKU127: - - SUPPLIER0 - SKU128: - - SUPPLIER0 - SKU129: - - SUPPLIER0 - SKU13: - - SUPPLIER0 - SKU130: - - SUPPLIER0 - SKU131: - - SUPPLIER0 - SKU132: - - SUPPLIER0 - SKU133: - - SUPPLIER0 - SKU134: - - SUPPLIER0 - SKU135: - - SUPPLIER0 - SKU136: - - SUPPLIER0 - SKU137: - - SUPPLIER0 - SKU138: - - SUPPLIER0 - SKU139: - - SUPPLIER0 - SKU14: - - SUPPLIER0 - SKU140: - - SUPPLIER0 - SKU141: - - SUPPLIER0 - SKU142: - - SUPPLIER0 - SKU143: - - SUPPLIER0 - SKU144: - - SUPPLIER0 - SKU145: - - SUPPLIER0 - SKU146: - - SUPPLIER0 - SKU147: - - SUPPLIER0 - SKU148: - - SUPPLIER0 - SKU149: - - SUPPLIER0 - SKU15: - - SUPPLIER0 - SKU150: - - SUPPLIER0 - SKU151: - - SUPPLIER0 - SKU152: - - SUPPLIER0 - SKU153: - - SUPPLIER0 - SKU154: - - SUPPLIER0 - SKU155: - - SUPPLIER0 - SKU156: - - SUPPLIER0 - SKU157: - - SUPPLIER0 - SKU158: - - SUPPLIER0 - SKU159: - - SUPPLIER0 - SKU16: - - SUPPLIER0 - SKU160: - - SUPPLIER0 - SKU161: - - SUPPLIER0 - SKU162: - - SUPPLIER0 - SKU163: - - SUPPLIER0 - SKU164: - - SUPPLIER0 - SKU165: - - SUPPLIER0 - SKU166: - - SUPPLIER0 - SKU167: - - SUPPLIER0 - SKU168: - - SUPPLIER0 - SKU169: - - SUPPLIER0 - SKU17: - - SUPPLIER0 - SKU170: - - SUPPLIER0 - SKU171: - - SUPPLIER0 - SKU172: - - SUPPLIER0 - SKU173: - - SUPPLIER0 - SKU174: - - SUPPLIER0 - SKU175: - - SUPPLIER0 - SKU176: - - SUPPLIER0 - SKU177: - - SUPPLIER0 - SKU178: - - SUPPLIER0 - SKU179: - - SUPPLIER0 - SKU18: - - SUPPLIER0 - SKU180: - - SUPPLIER0 - SKU181: - - SUPPLIER0 - SKU182: - - SUPPLIER0 - SKU183: - - SUPPLIER0 - SKU184: - - SUPPLIER0 - SKU185: - - SUPPLIER0 - SKU186: - - SUPPLIER0 - SKU187: - - SUPPLIER0 - SKU188: - - SUPPLIER0 - SKU189: - - SUPPLIER0 - SKU19: - - SUPPLIER0 - SKU190: - - SUPPLIER0 - SKU191: - - SUPPLIER0 - SKU192: - - SUPPLIER0 - SKU193: - - SUPPLIER0 - SKU194: - - SUPPLIER0 - SKU195: - - SUPPLIER0 - SKU196: - - SUPPLIER0 - SKU197: - - SUPPLIER0 - SKU198: - - SUPPLIER0 - SKU199: - - SUPPLIER0 - SKU2: - - SUPPLIER0 - SKU20: - - SUPPLIER0 - SKU200: - - SUPPLIER0 - SKU201: - - SUPPLIER0 - SKU202: - - SUPPLIER0 - SKU203: - - SUPPLIER0 - SKU204: - - SUPPLIER0 - SKU205: - - SUPPLIER0 - SKU206: - - SUPPLIER0 - SKU207: - - SUPPLIER0 - SKU208: - - SUPPLIER0 - SKU209: - - SUPPLIER0 - SKU21: - - SUPPLIER0 - SKU210: - - SUPPLIER0 - SKU211: - - SUPPLIER0 - SKU212: - - SUPPLIER0 - SKU213: - - SUPPLIER0 - SKU214: - - SUPPLIER0 - SKU215: - - SUPPLIER0 - SKU216: - - SUPPLIER0 - SKU217: - - SUPPLIER0 - SKU218: - - SUPPLIER0 - SKU219: - - SUPPLIER0 - SKU22: - - SUPPLIER0 - SKU220: - - SUPPLIER0 - SKU221: - - SUPPLIER0 - SKU222: - - SUPPLIER0 - SKU223: - - SUPPLIER0 - SKU224: - - SUPPLIER0 - SKU225: - - SUPPLIER0 - SKU226: - - SUPPLIER0 - SKU227: - - SUPPLIER0 - SKU228: - - SUPPLIER0 - SKU229: - - SUPPLIER0 - SKU23: - - SUPPLIER0 - SKU230: - - SUPPLIER0 - SKU231: - - SUPPLIER0 - SKU232: - - SUPPLIER0 - SKU233: - - SUPPLIER0 - SKU234: - - SUPPLIER0 - SKU235: - - SUPPLIER0 - SKU236: - - SUPPLIER0 - SKU237: - - SUPPLIER0 - SKU238: - - SUPPLIER0 - SKU239: - - SUPPLIER0 - SKU24: - - SUPPLIER0 - SKU240: - - SUPPLIER0 - SKU241: - - SUPPLIER0 - SKU242: - - SUPPLIER0 - SKU243: - - SUPPLIER0 - SKU244: - - SUPPLIER0 - SKU245: - - SUPPLIER0 - SKU246: - - SUPPLIER0 - SKU247: - - SUPPLIER0 - SKU248: - - SUPPLIER0 - SKU249: - - SUPPLIER0 - SKU25: - - SUPPLIER0 - SKU250: - - SUPPLIER0 - SKU251: - - SUPPLIER0 - SKU252: - - SUPPLIER0 - SKU253: - - SUPPLIER0 - SKU254: - - SUPPLIER0 - SKU255: - - SUPPLIER0 - SKU256: - - SUPPLIER0 - SKU257: - - SUPPLIER0 - SKU258: - - SUPPLIER0 - SKU259: - - SUPPLIER0 - SKU26: - - SUPPLIER0 - SKU260: - - SUPPLIER0 - SKU261: - - SUPPLIER0 - SKU262: - - SUPPLIER0 - SKU263: - - SUPPLIER0 - SKU264: - - SUPPLIER0 - SKU265: - - SUPPLIER0 - SKU266: - - SUPPLIER0 - SKU267: - - SUPPLIER0 - SKU268: - - SUPPLIER0 - SKU269: - - SUPPLIER0 - SKU27: - - SUPPLIER0 - SKU270: - - SUPPLIER0 - SKU271: - - SUPPLIER0 - SKU272: - - SUPPLIER0 - SKU273: - - SUPPLIER0 - SKU274: - - SUPPLIER0 - SKU275: - - SUPPLIER0 - SKU276: - - SUPPLIER0 - SKU277: - - SUPPLIER0 - SKU278: - - SUPPLIER0 - SKU279: - - SUPPLIER0 - SKU28: - - SUPPLIER0 - SKU280: - - SUPPLIER0 - SKU281: - - SUPPLIER0 - SKU282: - - SUPPLIER0 - SKU283: - - SUPPLIER0 - SKU284: - - SUPPLIER0 - SKU285: - - SUPPLIER0 - SKU286: - - SUPPLIER0 - SKU287: - - SUPPLIER0 - SKU288: - - SUPPLIER0 - SKU289: - - SUPPLIER0 - SKU29: - - SUPPLIER0 - SKU290: - - SUPPLIER0 - SKU291: - - SUPPLIER0 - SKU292: - - SUPPLIER0 - SKU293: - - SUPPLIER0 - SKU294: - - SUPPLIER0 - SKU295: - - SUPPLIER0 - SKU296: - - SUPPLIER0 - SKU297: - - SUPPLIER0 - SKU298: - - SUPPLIER0 - SKU299: - - SUPPLIER0 - SKU3: - - SUPPLIER0 - SKU30: - - SUPPLIER0 - SKU300: - - SUPPLIER0 - SKU301: - - SUPPLIER0 - SKU302: - - SUPPLIER0 - SKU303: - - SUPPLIER0 - SKU304: - - SUPPLIER0 - SKU305: - - SUPPLIER0 - SKU306: - - SUPPLIER0 - SKU307: - - SUPPLIER0 - SKU308: - - SUPPLIER0 - SKU309: - - SUPPLIER0 - SKU31: - - SUPPLIER0 - SKU310: - - SUPPLIER0 - SKU311: - - SUPPLIER0 - SKU312: - - SUPPLIER0 - SKU313: - - SUPPLIER0 - SKU314: - - SUPPLIER0 - SKU315: - - SUPPLIER0 - SKU316: - - SUPPLIER0 - SKU317: - - SUPPLIER0 - SKU318: - - SUPPLIER0 - SKU319: - - SUPPLIER0 - SKU32: - - SUPPLIER0 - SKU320: - - SUPPLIER0 - SKU321: - - SUPPLIER0 - SKU322: - - SUPPLIER0 - SKU323: - - SUPPLIER0 - SKU324: - - SUPPLIER0 - SKU325: - - SUPPLIER0 - SKU326: - - SUPPLIER0 - SKU327: - - SUPPLIER0 - SKU328: - - SUPPLIER0 - SKU329: - - SUPPLIER0 - SKU33: - - SUPPLIER0 - SKU330: - - SUPPLIER0 - SKU331: - - SUPPLIER0 - SKU332: - - SUPPLIER0 - SKU333: - - SUPPLIER0 - SKU334: - - SUPPLIER0 - SKU335: - - SUPPLIER0 - SKU336: - - SUPPLIER0 - SKU337: - - SUPPLIER0 - SKU338: - - SUPPLIER0 - SKU339: - - SUPPLIER0 - SKU34: - - SUPPLIER0 - SKU340: - - SUPPLIER0 - SKU341: - - SUPPLIER0 - SKU342: - - SUPPLIER0 - SKU343: - - SUPPLIER0 - SKU344: - - SUPPLIER0 - SKU345: - - SUPPLIER0 - SKU346: - - SUPPLIER0 - SKU347: - - SUPPLIER0 - SKU348: - - SUPPLIER0 - SKU349: - - SUPPLIER0 - SKU35: - - SUPPLIER0 - SKU350: - - SUPPLIER0 - SKU351: - - SUPPLIER0 - SKU352: - - SUPPLIER0 - SKU353: - - SUPPLIER0 - SKU354: - - SUPPLIER0 - SKU355: - - SUPPLIER0 - SKU356: - - SUPPLIER0 - SKU357: - - SUPPLIER0 - SKU358: - - SUPPLIER0 - SKU359: - - SUPPLIER0 - SKU36: - - SUPPLIER0 - SKU360: - - SUPPLIER0 - SKU361: - - SUPPLIER0 - SKU362: - - SUPPLIER0 - SKU363: - - SUPPLIER0 - SKU364: - - SUPPLIER0 - SKU365: - - SUPPLIER0 - SKU366: - - SUPPLIER0 - SKU367: - - SUPPLIER0 - SKU368: - - SUPPLIER0 - SKU369: - - SUPPLIER0 - SKU37: - - SUPPLIER0 - SKU370: - - SUPPLIER0 - SKU371: - - SUPPLIER0 - SKU372: - - SUPPLIER0 - SKU373: - - SUPPLIER0 - SKU374: - - SUPPLIER0 - SKU375: - - SUPPLIER0 - SKU376: - - SUPPLIER0 - SKU377: - - SUPPLIER0 - SKU378: - - SUPPLIER0 - SKU379: - - SUPPLIER0 - SKU38: - - SUPPLIER0 - SKU380: - - SUPPLIER0 - SKU381: - - SUPPLIER0 - SKU382: - - SUPPLIER0 - SKU383: - - SUPPLIER0 - SKU384: - - SUPPLIER0 - SKU385: - - SUPPLIER0 - SKU386: - - SUPPLIER0 - SKU387: - - SUPPLIER0 - SKU388: - - SUPPLIER0 - SKU389: - - SUPPLIER0 - SKU39: - - SUPPLIER0 - SKU390: - - SUPPLIER0 - SKU391: - - SUPPLIER0 - SKU392: - - SUPPLIER0 - SKU393: - - SUPPLIER0 - SKU394: - - SUPPLIER0 - SKU395: - - SUPPLIER0 - SKU396: - - SUPPLIER0 - SKU397: - - SUPPLIER0 - SKU398: - - SUPPLIER0 - SKU399: - - SUPPLIER0 - SKU4: - - SUPPLIER0 - SKU40: - - SUPPLIER0 - SKU400: - - SUPPLIER0 - SKU401: - - SUPPLIER0 - SKU402: - - SUPPLIER0 - SKU403: - - SUPPLIER0 - SKU404: - - SUPPLIER0 - SKU405: - - SUPPLIER0 - SKU406: - - SUPPLIER0 - SKU407: - - SUPPLIER0 - SKU408: - - SUPPLIER0 - SKU409: - - SUPPLIER0 - SKU41: - - SUPPLIER0 - SKU410: - - SUPPLIER0 - SKU411: - - SUPPLIER0 - SKU412: - - SUPPLIER0 - SKU413: - - SUPPLIER0 - SKU414: - - SUPPLIER0 - SKU415: - - SUPPLIER0 - SKU416: - - SUPPLIER0 - SKU417: - - SUPPLIER0 - SKU418: - - SUPPLIER0 - SKU419: - - SUPPLIER0 - SKU42: - - SUPPLIER0 - SKU420: - - SUPPLIER0 - SKU421: - - SUPPLIER0 - SKU422: - - SUPPLIER0 - SKU423: - - SUPPLIER0 - SKU424: - - SUPPLIER0 - SKU425: - - SUPPLIER0 - SKU426: - - SUPPLIER0 - SKU427: - - SUPPLIER0 - SKU428: - - SUPPLIER0 - SKU429: - - SUPPLIER0 - SKU43: - - SUPPLIER0 - SKU430: - - SUPPLIER0 - SKU431: - - SUPPLIER0 - SKU432: - - SUPPLIER0 - SKU433: - - SUPPLIER0 - SKU434: - - SUPPLIER0 - SKU435: - - SUPPLIER0 - SKU436: - - SUPPLIER0 - SKU437: - - SUPPLIER0 - SKU438: - - SUPPLIER0 - SKU439: - - SUPPLIER0 - SKU44: - - SUPPLIER0 - SKU440: - - SUPPLIER0 - SKU441: - - SUPPLIER0 - SKU442: - - SUPPLIER0 - SKU443: - - SUPPLIER0 - SKU444: - - SUPPLIER0 - SKU445: - - SUPPLIER0 - SKU446: - - SUPPLIER0 - SKU447: - - SUPPLIER0 - SKU448: - - SUPPLIER0 - SKU449: - - SUPPLIER0 - SKU45: - - SUPPLIER0 - SKU450: - - SUPPLIER0 - SKU451: - - SUPPLIER0 - SKU452: - - SUPPLIER0 - SKU453: - - SUPPLIER0 - SKU454: - - SUPPLIER0 - SKU455: - - SUPPLIER0 - SKU456: - - SUPPLIER0 - SKU457: - - SUPPLIER0 - SKU458: - - SUPPLIER0 - SKU459: - - SUPPLIER0 - SKU46: - - SUPPLIER0 - SKU460: - - SUPPLIER0 - SKU461: - - SUPPLIER0 - SKU462: - - SUPPLIER0 - SKU463: - - SUPPLIER0 - SKU464: - - SUPPLIER0 - SKU465: - - SUPPLIER0 - SKU466: - - SUPPLIER0 - SKU467: - - SUPPLIER0 - SKU468: - - SUPPLIER0 - SKU469: - - SUPPLIER0 - SKU47: - - SUPPLIER0 - SKU470: - - SUPPLIER0 - SKU471: - - SUPPLIER0 - SKU472: - - SUPPLIER0 - SKU473: - - SUPPLIER0 - SKU474: - - SUPPLIER0 - SKU475: - - SUPPLIER0 - SKU476: - - SUPPLIER0 - SKU477: - - SUPPLIER0 - SKU478: - - SUPPLIER0 - SKU479: - - SUPPLIER0 - SKU48: - - SUPPLIER0 - SKU480: - - SUPPLIER0 - SKU481: - - SUPPLIER0 - SKU482: - - SUPPLIER0 - SKU483: - - SUPPLIER0 - SKU484: - - SUPPLIER0 - SKU485: - - SUPPLIER0 - SKU486: - - SUPPLIER0 - SKU487: - - SUPPLIER0 - SKU488: - - SUPPLIER0 - SKU489: - - SUPPLIER0 - SKU49: - - SUPPLIER0 - SKU490: - - SUPPLIER0 - SKU491: - - SUPPLIER0 - SKU492: - - SUPPLIER0 - SKU493: - - SUPPLIER0 - SKU494: - - SUPPLIER0 - SKU495: - - SUPPLIER0 - SKU496: - - SUPPLIER0 - SKU497: - - SUPPLIER0 - SKU498: - - SUPPLIER0 - SKU499: - - SUPPLIER0 - SKU5: - - SUPPLIER0 - SKU50: - - SUPPLIER0 - SKU500: - - SUPPLIER0 - SKU501: - - SUPPLIER0 - SKU502: - - SUPPLIER0 - SKU503: - - SUPPLIER0 - SKU504: - - SUPPLIER0 - SKU505: - - SUPPLIER0 - SKU506: - - SUPPLIER0 - SKU507: - - SUPPLIER0 - SKU508: - - SUPPLIER0 - SKU509: - - SUPPLIER0 - SKU51: - - SUPPLIER0 - SKU510: - - SUPPLIER0 - SKU511: - - SUPPLIER0 - SKU512: - - SUPPLIER0 - SKU513: - - SUPPLIER0 - SKU514: - - SUPPLIER0 - SKU515: - - SUPPLIER0 - SKU516: - - SUPPLIER0 - SKU517: - - SUPPLIER0 - SKU518: - - SUPPLIER0 - SKU519: - - SUPPLIER0 - SKU52: - - SUPPLIER0 - SKU520: - - SUPPLIER0 - SKU521: - - SUPPLIER0 - SKU522: - - SUPPLIER0 - SKU523: - - SUPPLIER0 - SKU524: - - SUPPLIER0 - SKU525: - - SUPPLIER0 - SKU526: - - SUPPLIER0 - SKU527: - - SUPPLIER0 - SKU528: - - SUPPLIER0 - SKU529: - - SUPPLIER0 - SKU53: - - SUPPLIER0 - SKU530: - - SUPPLIER0 - SKU531: - - SUPPLIER0 - SKU532: - - SUPPLIER0 - SKU533: - - SUPPLIER0 - SKU534: - - SUPPLIER0 - SKU535: - - SUPPLIER0 - SKU536: - - SUPPLIER0 - SKU537: - - SUPPLIER0 - SKU538: - - SUPPLIER0 - SKU539: - - SUPPLIER0 - SKU54: - - SUPPLIER0 - SKU540: - - SUPPLIER0 - SKU541: - - SUPPLIER0 - SKU542: - - SUPPLIER0 - SKU543: - - SUPPLIER0 - SKU544: - - SUPPLIER0 - SKU545: - - SUPPLIER0 - SKU546: - - SUPPLIER0 - SKU547: - - SUPPLIER0 - SKU548: - - SUPPLIER0 - SKU549: - - SUPPLIER0 - SKU55: - - SUPPLIER0 - SKU550: - - SUPPLIER0 - SKU551: - - SUPPLIER0 - SKU552: - - SUPPLIER0 - SKU553: - - SUPPLIER0 - SKU554: - - SUPPLIER0 - SKU555: - - SUPPLIER0 - SKU556: - - SUPPLIER0 - SKU557: - - SUPPLIER0 - SKU558: - - SUPPLIER0 - SKU559: - - SUPPLIER0 - SKU56: - - SUPPLIER0 - SKU560: - - SUPPLIER0 - SKU561: - - SUPPLIER0 - SKU562: - - SUPPLIER0 - SKU563: - - SUPPLIER0 - SKU564: - - SUPPLIER0 - SKU565: - - SUPPLIER0 - SKU566: - - SUPPLIER0 - SKU567: - - SUPPLIER0 - SKU568: - - SUPPLIER0 - SKU569: - - SUPPLIER0 - SKU57: - - SUPPLIER0 - SKU570: - - SUPPLIER0 - SKU571: - - SUPPLIER0 - SKU572: - - SUPPLIER0 - SKU573: - - SUPPLIER0 - SKU574: - - SUPPLIER0 - SKU575: - - SUPPLIER0 - SKU576: - - SUPPLIER0 - SKU577: - - SUPPLIER0 - SKU578: - - SUPPLIER0 - SKU579: - - SUPPLIER0 - SKU58: - - SUPPLIER0 - SKU580: - - SUPPLIER0 - SKU581: - - SUPPLIER0 - SKU582: - - SUPPLIER0 - SKU583: - - SUPPLIER0 - SKU584: - - SUPPLIER0 - SKU585: - - SUPPLIER0 - SKU586: - - SUPPLIER0 - SKU587: - - SUPPLIER0 - SKU588: - - SUPPLIER0 - SKU589: - - SUPPLIER0 - SKU59: - - SUPPLIER0 - SKU590: - - SUPPLIER0 - SKU591: - - SUPPLIER0 - SKU592: - - SUPPLIER0 - SKU593: - - SUPPLIER0 - SKU594: - - SUPPLIER0 - SKU595: - - SUPPLIER0 - SKU596: - - SUPPLIER0 - SKU597: - - SUPPLIER0 - SKU598: - - SUPPLIER0 - SKU599: - - SUPPLIER0 - SKU6: - - SUPPLIER0 - SKU60: - - SUPPLIER0 - SKU600: - - SUPPLIER0 - SKU601: - - SUPPLIER0 - SKU602: - - SUPPLIER0 - SKU603: - - SUPPLIER0 - SKU604: - - SUPPLIER0 - SKU605: - - SUPPLIER0 - SKU606: - - SUPPLIER0 - SKU607: - - SUPPLIER0 - SKU608: - - SUPPLIER0 - SKU609: - - SUPPLIER0 - SKU61: - - SUPPLIER0 - SKU610: - - SUPPLIER0 - SKU611: - - SUPPLIER0 - SKU612: - - SUPPLIER0 - SKU613: - - SUPPLIER0 - SKU614: - - SUPPLIER0 - SKU615: - - SUPPLIER0 - SKU616: - - SUPPLIER0 - SKU617: - - SUPPLIER0 - SKU618: - - SUPPLIER0 - SKU619: - - SUPPLIER0 - SKU62: - - SUPPLIER0 - SKU620: - - SUPPLIER0 - SKU621: - - SUPPLIER0 - SKU622: - - SUPPLIER0 - SKU623: - - SUPPLIER0 - SKU624: - - SUPPLIER0 - SKU625: - - SUPPLIER0 - SKU626: - - SUPPLIER0 - SKU627: - - SUPPLIER0 - SKU628: - - SUPPLIER0 - SKU629: - - SUPPLIER0 - SKU63: - - SUPPLIER0 - SKU630: - - SUPPLIER0 - SKU631: - - SUPPLIER0 - SKU632: - - SUPPLIER0 - SKU633: - - SUPPLIER0 - SKU634: - - SUPPLIER0 - SKU635: - - SUPPLIER0 - SKU636: - - SUPPLIER0 - SKU637: - - SUPPLIER0 - SKU638: - - SUPPLIER0 - SKU639: - - SUPPLIER0 - SKU64: - - SUPPLIER0 - SKU640: - - SUPPLIER0 - SKU641: - - SUPPLIER0 - SKU642: - - SUPPLIER0 - SKU643: - - SUPPLIER0 - SKU644: - - SUPPLIER0 - SKU645: - - SUPPLIER0 - SKU646: - - SUPPLIER0 - SKU647: - - SUPPLIER0 - SKU648: - - SUPPLIER0 - SKU649: - - SUPPLIER0 - SKU65: - - SUPPLIER0 - SKU650: - - SUPPLIER0 - SKU651: - - SUPPLIER0 - SKU652: - - SUPPLIER0 - SKU653: - - SUPPLIER0 - SKU654: - - SUPPLIER0 - SKU655: - - SUPPLIER0 - SKU656: - - SUPPLIER0 - SKU657: - - SUPPLIER0 - SKU658: - - SUPPLIER0 - SKU659: - - SUPPLIER0 - SKU66: - - SUPPLIER0 - SKU660: - - SUPPLIER0 - SKU661: - - SUPPLIER0 - SKU662: - - SUPPLIER0 - SKU663: - - SUPPLIER0 - SKU664: - - SUPPLIER0 - SKU665: - - SUPPLIER0 - SKU666: - - SUPPLIER0 - SKU667: - - SUPPLIER0 - SKU668: - - SUPPLIER0 - SKU669: - - SUPPLIER0 - SKU67: - - SUPPLIER0 - SKU670: - - SUPPLIER0 - SKU671: - - SUPPLIER0 - SKU672: - - SUPPLIER0 - SKU673: - - SUPPLIER0 - SKU674: - - SUPPLIER0 - SKU675: - - SUPPLIER0 - SKU676: - - SUPPLIER0 - SKU677: - - SUPPLIER0 - SKU678: - - SUPPLIER0 - SKU679: - - SUPPLIER0 - SKU68: - - SUPPLIER0 - SKU680: - - SUPPLIER0 - SKU681: - - SUPPLIER0 - SKU682: - - SUPPLIER0 - SKU683: - - SUPPLIER0 - SKU684: - - SUPPLIER0 - SKU685: - - SUPPLIER0 - SKU686: - - SUPPLIER0 - SKU687: - - SUPPLIER0 - SKU688: - - SUPPLIER0 - SKU689: - - SUPPLIER0 - SKU69: - - SUPPLIER0 - SKU690: - - SUPPLIER0 - SKU691: - - SUPPLIER0 - SKU692: - - SUPPLIER0 - SKU693: - - SUPPLIER0 - SKU694: - - SUPPLIER0 - SKU695: - - SUPPLIER0 - SKU696: - - SUPPLIER0 - SKU697: - - SUPPLIER0 - SKU698: - - SUPPLIER0 - SKU699: - - SUPPLIER0 - SKU7: - - SUPPLIER0 - SKU70: - - SUPPLIER0 - SKU700: - - SUPPLIER0 - SKU701: - - SUPPLIER0 - SKU702: - - SUPPLIER0 - SKU703: - - SUPPLIER0 - SKU704: - - SUPPLIER0 - SKU705: - - SUPPLIER0 - SKU706: - - SUPPLIER0 - SKU707: - - SUPPLIER0 - SKU708: - - SUPPLIER0 - SKU709: - - SUPPLIER0 - SKU71: - - SUPPLIER0 - SKU710: - - SUPPLIER0 - SKU711: - - SUPPLIER0 - SKU712: - - SUPPLIER0 - SKU713: - - SUPPLIER0 - SKU714: - - SUPPLIER0 - SKU715: - - SUPPLIER0 - SKU716: - - SUPPLIER0 - SKU717: - - SUPPLIER0 - SKU718: - - SUPPLIER0 - SKU719: - - SUPPLIER0 - SKU72: - - SUPPLIER0 - SKU720: - - SUPPLIER0 - SKU721: - - SUPPLIER0 - SKU722: - - SUPPLIER0 - SKU723: - - SUPPLIER0 - SKU724: - - SUPPLIER0 - SKU725: - - SUPPLIER0 - SKU726: - - SUPPLIER0 - SKU727: - - SUPPLIER0 - SKU728: - - SUPPLIER0 - SKU729: - - SUPPLIER0 - SKU73: - - SUPPLIER0 - SKU730: - - SUPPLIER0 - SKU731: - - SUPPLIER0 - SKU732: - - SUPPLIER0 - SKU733: - - SUPPLIER0 - SKU734: - - SUPPLIER0 - SKU735: - - SUPPLIER0 - SKU736: - - SUPPLIER0 - SKU737: - - SUPPLIER0 - SKU738: - - SUPPLIER0 - SKU739: - - SUPPLIER0 - SKU74: - - SUPPLIER0 - SKU740: - - SUPPLIER0 - SKU741: - - SUPPLIER0 - SKU742: - - SUPPLIER0 - SKU743: - - SUPPLIER0 - SKU744: - - SUPPLIER0 - SKU745: - - SUPPLIER0 - SKU746: - - SUPPLIER0 - SKU747: - - SUPPLIER0 - SKU748: - - SUPPLIER0 - SKU749: - - SUPPLIER0 - SKU75: - - SUPPLIER0 - SKU750: - - SUPPLIER0 - SKU751: - - SUPPLIER0 - SKU752: - - SUPPLIER0 - SKU753: - - SUPPLIER0 - SKU754: - - SUPPLIER0 - SKU755: - - SUPPLIER0 - SKU756: - - SUPPLIER0 - SKU757: - - SUPPLIER0 - SKU758: - - SUPPLIER0 - SKU759: - - SUPPLIER0 - SKU76: - - SUPPLIER0 - SKU760: - - SUPPLIER0 - SKU761: - - SUPPLIER0 - SKU762: - - SUPPLIER0 - SKU763: - - SUPPLIER0 - SKU764: - - SUPPLIER0 - SKU765: - - SUPPLIER0 - SKU766: - - SUPPLIER0 - SKU767: - - SUPPLIER0 - SKU768: - - SUPPLIER0 - SKU769: - - SUPPLIER0 - SKU77: - - SUPPLIER0 - SKU770: - - SUPPLIER0 - SKU771: - - SUPPLIER0 - SKU772: - - SUPPLIER0 - SKU773: - - SUPPLIER0 - SKU774: - - SUPPLIER0 - SKU775: - - SUPPLIER0 - SKU776: - - SUPPLIER0 - SKU777: - - SUPPLIER0 - SKU778: - - SUPPLIER0 - SKU779: - - SUPPLIER0 - SKU78: - - SUPPLIER0 - SKU780: - - SUPPLIER0 - SKU781: - - SUPPLIER0 - SKU782: - - SUPPLIER0 - SKU783: - - SUPPLIER0 - SKU784: - - SUPPLIER0 - SKU785: - - SUPPLIER0 - SKU786: - - SUPPLIER0 - SKU787: - - SUPPLIER0 - SKU788: - - SUPPLIER0 - SKU789: - - SUPPLIER0 - SKU79: - - SUPPLIER0 - SKU790: - - SUPPLIER0 - SKU791: - - SUPPLIER0 - SKU792: - - SUPPLIER0 - SKU793: - - SUPPLIER0 - SKU794: - - SUPPLIER0 - SKU795: - - SUPPLIER0 - SKU796: - - SUPPLIER0 - SKU797: - - SUPPLIER0 - SKU798: - - SUPPLIER0 - SKU799: - - SUPPLIER0 - SKU8: - - SUPPLIER0 - SKU80: - - SUPPLIER0 - SKU800: - - SUPPLIER0 - SKU801: - - SUPPLIER0 - SKU802: - - SUPPLIER0 - SKU803: - - SUPPLIER0 - SKU804: - - SUPPLIER0 - SKU805: - - SUPPLIER0 - SKU806: - - SUPPLIER0 - SKU807: - - SUPPLIER0 - SKU808: - - SUPPLIER0 - SKU809: - - SUPPLIER0 - SKU81: - - SUPPLIER0 - SKU810: - - SUPPLIER0 - SKU811: - - SUPPLIER0 - SKU812: - - SUPPLIER0 - SKU813: - - SUPPLIER0 - SKU814: - - SUPPLIER0 - SKU815: - - SUPPLIER0 - SKU816: - - SUPPLIER0 - SKU817: - - SUPPLIER0 - SKU818: - - SUPPLIER0 - SKU819: - - SUPPLIER0 - SKU82: - - SUPPLIER0 - SKU820: - - SUPPLIER0 - SKU821: - - SUPPLIER0 - SKU822: - - SUPPLIER0 - SKU823: - - SUPPLIER0 - SKU824: - - SUPPLIER0 - SKU825: - - SUPPLIER0 - SKU826: - - SUPPLIER0 - SKU827: - - SUPPLIER0 - SKU828: - - SUPPLIER0 - SKU829: - - SUPPLIER0 - SKU83: - - SUPPLIER0 - SKU830: - - SUPPLIER0 - SKU831: - - SUPPLIER0 - SKU832: - - SUPPLIER0 - SKU833: - - SUPPLIER0 - SKU834: - - SUPPLIER0 - SKU835: - - SUPPLIER0 - SKU836: - - SUPPLIER0 - SKU837: - - SUPPLIER0 - SKU838: - - SUPPLIER0 - SKU839: - - SUPPLIER0 - SKU84: - - SUPPLIER0 - SKU840: - - SUPPLIER0 - SKU841: - - SUPPLIER0 - SKU842: - - SUPPLIER0 - SKU843: - - SUPPLIER0 - SKU844: - - SUPPLIER0 - SKU845: - - SUPPLIER0 - SKU846: - - SUPPLIER0 - SKU847: - - SUPPLIER0 - SKU848: - - SUPPLIER0 - SKU849: - - SUPPLIER0 - SKU85: - - SUPPLIER0 - SKU850: - - SUPPLIER0 - SKU851: - - SUPPLIER0 - SKU852: - - SUPPLIER0 - SKU853: - - SUPPLIER0 - SKU854: - - SUPPLIER0 - SKU855: - - SUPPLIER0 - SKU856: - - SUPPLIER0 - SKU857: - - SUPPLIER0 - SKU858: - - SUPPLIER0 - SKU859: - - SUPPLIER0 - SKU86: - - SUPPLIER0 - SKU860: - - SUPPLIER0 - SKU861: - - SUPPLIER0 - SKU862: - - SUPPLIER0 - SKU863: - - SUPPLIER0 - SKU864: - - SUPPLIER0 - SKU865: - - SUPPLIER0 - SKU866: - - SUPPLIER0 - SKU867: - - SUPPLIER0 - SKU868: - - SUPPLIER0 - SKU869: - - SUPPLIER0 - SKU87: - - SUPPLIER0 - SKU870: - - SUPPLIER0 - SKU871: - - SUPPLIER0 - SKU872: - - SUPPLIER0 - SKU873: - - SUPPLIER0 - SKU874: - - SUPPLIER0 - SKU875: - - SUPPLIER0 - SKU876: - - SUPPLIER0 - SKU877: - - SUPPLIER0 - SKU878: - - SUPPLIER0 - SKU879: - - SUPPLIER0 - SKU88: - - SUPPLIER0 - SKU880: - - SUPPLIER0 - SKU881: - - SUPPLIER0 - SKU882: - - SUPPLIER0 - SKU883: - - SUPPLIER0 - SKU884: - - SUPPLIER0 - SKU885: - - SUPPLIER0 - SKU886: - - SUPPLIER0 - SKU887: - - SUPPLIER0 - SKU888: - - SUPPLIER0 - SKU889: - - SUPPLIER0 - SKU89: - - SUPPLIER0 - SKU890: - - SUPPLIER0 - SKU891: - - SUPPLIER0 - SKU892: - - SUPPLIER0 - SKU893: - - SUPPLIER0 - SKU894: - - SUPPLIER0 - SKU895: - - SUPPLIER0 - SKU896: - - SUPPLIER0 - SKU897: - - SUPPLIER0 - SKU898: - - SUPPLIER0 - SKU899: - - SUPPLIER0 - SKU9: - - SUPPLIER0 - SKU90: - - SUPPLIER0 - SKU900: - - SUPPLIER0 - SKU901: - - SUPPLIER0 - SKU902: - - SUPPLIER0 - SKU903: - - SUPPLIER0 - SKU904: - - SUPPLIER0 - SKU905: - - SUPPLIER0 - SKU906: - - SUPPLIER0 - SKU907: - - SUPPLIER0 - SKU908: - - SUPPLIER0 - SKU909: - - SUPPLIER0 - SKU91: - - SUPPLIER0 - SKU910: - - SUPPLIER0 - SKU911: - - SUPPLIER0 - SKU912: - - SUPPLIER0 - SKU913: - - SUPPLIER0 - SKU914: - - SUPPLIER0 - SKU915: - - SUPPLIER0 - SKU916: - - SUPPLIER0 - SKU917: - - SUPPLIER0 - SKU918: - - SUPPLIER0 - SKU919: - - SUPPLIER0 - SKU92: - - SUPPLIER0 - SKU920: - - SUPPLIER0 - SKU921: - - SUPPLIER0 - SKU922: - - SUPPLIER0 - SKU923: - - SUPPLIER0 - SKU924: - - SUPPLIER0 - SKU925: - - SUPPLIER0 - SKU926: - - SUPPLIER0 - SKU927: - - SUPPLIER0 - SKU928: - - SUPPLIER0 - SKU929: - - SUPPLIER0 - SKU93: - - SUPPLIER0 - SKU930: - - SUPPLIER0 - SKU931: - - SUPPLIER0 - SKU932: - - SUPPLIER0 - SKU933: - - SUPPLIER0 - SKU934: - - SUPPLIER0 - SKU935: - - SUPPLIER0 - SKU936: - - SUPPLIER0 - SKU937: - - SUPPLIER0 - SKU938: - - SUPPLIER0 - SKU939: - - SUPPLIER0 - SKU94: - - SUPPLIER0 - SKU940: - - SUPPLIER0 - SKU941: - - SUPPLIER0 - SKU942: - - SUPPLIER0 - SKU943: - - SUPPLIER0 - SKU944: - - SUPPLIER0 - SKU945: - - SUPPLIER0 - SKU946: - - SUPPLIER0 - SKU947: - - SUPPLIER0 - SKU948: - - SUPPLIER0 - SKU949: - - SUPPLIER0 - SKU95: - - SUPPLIER0 - SKU950: - - SUPPLIER0 - SKU951: - - SUPPLIER0 - SKU952: - - SUPPLIER0 - SKU953: - - SUPPLIER0 - SKU954: - - SUPPLIER0 - SKU955: - - SUPPLIER0 - SKU956: - - SUPPLIER0 - SKU957: - - SUPPLIER0 - SKU958: - - SUPPLIER0 - SKU959: - - SUPPLIER0 - SKU96: - - SUPPLIER0 - SKU960: - - SUPPLIER0 - SKU961: - - SUPPLIER0 - SKU962: - - SUPPLIER0 - SKU963: - - SUPPLIER0 - SKU964: - - SUPPLIER0 - SKU965: - - SUPPLIER0 - SKU966: - - SUPPLIER0 - SKU967: - - SUPPLIER0 - SKU968: - - SUPPLIER0 - SKU969: - - SUPPLIER0 - SKU97: - - SUPPLIER0 - SKU970: - - SUPPLIER0 - SKU971: - - SUPPLIER0 - SKU972: - - SUPPLIER0 - SKU973: - - SUPPLIER0 - SKU974: - - SUPPLIER0 - SKU975: - - SUPPLIER0 - SKU976: - - SUPPLIER0 - SKU977: - - SUPPLIER0 - SKU978: - - SUPPLIER0 - SKU979: - - SUPPLIER0 - SKU98: - - SUPPLIER0 - SKU980: - - SUPPLIER0 - SKU981: - - SUPPLIER0 - SKU982: - - SUPPLIER0 - SKU983: - - SUPPLIER0 - SKU984: - - SUPPLIER0 - SKU985: - - SUPPLIER0 - SKU986: - - SUPPLIER0 - SKU987: - - SUPPLIER0 - SKU988: - - SUPPLIER0 - SKU989: - - SUPPLIER0 - SKU99: - - SUPPLIER0 - SKU990: - - SUPPLIER0 - SKU991: - - SUPPLIER0 - SKU992: - - SUPPLIER0 - SKU993: - - SUPPLIER0 - SKU994: - - SUPPLIER0 - SKU995: - - SUPPLIER0 - SKU996: - - SUPPLIER0 - SKU997: - - SUPPLIER0 - SKU998: - - SUPPLIER0 - SKU999: - - SUPPLIER0 +facility_definitions: + RetailerFacility: + children: + products: + class: StoreProductUnit + config: + agent_type: 5 + consumer: + class: ConsumerUnit + seller: + class: SellerUnit + config: + sale_hist_len: 4 + is_template: true + storage: + class: StorageUnit + class: RetailerFacility + config: + agent_type: 2 + SupplierFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: 3 + consumer: + class: ConsumerUnit + manufacture: + class: ManufactureUnit + is_template: true + storage: + class: StorageUnit + class: SupplierFacility + config: + agent_type: 0 + WarehouseFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: 4 + consumer: + class: ConsumerUnit + is_template: true + storage: + class: StorageUnit + class: WarehouseFacility + config: + agent_type: 1 +normal_vehicle: &id001 + class: VehicleUnit + config: + patient: 100 + unit_transport_cost: 1 +settings: + constraint_state_hist_len: 4 + constraint_violate_reward: -1000000.0 + consumption_hist_len: 4 + downsampling_rate: 1 + episod_duration: 21 + gamma: 0.99 + global_reward_weight_consumer: 0.5 + global_reward_weight_producer: 0.5 + heading_timesteps: 7 + initial_balance: 100000 + pending_order_len: 4 + replenishment_discount: 0.9 + reward_normalization: 10000000.0 + sale_hist_len: 4 + tail_timesteps: 7 + total_echelons: 3 +world: + facilities: + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5324500 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 200 + definition_ref: SupplierFacility + name: SUPPLIER0 + skus: + SKU0: + cost: 289 + init_stock: 3150 + price: 322 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU1: + cost: 255 + init_stock: 1100 + price: 284 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU10: + cost: 61 + init_stock: 1250 + price: 68 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU100: + cost: 14 + init_stock: 3250 + price: 16 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU101: + cost: 75 + init_stock: 3550 + price: 84 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU102: + cost: 295 + init_stock: 4650 + price: 328 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU103: + cost: 300 + init_stock: 4750 + price: 334 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU104: + cost: 44 + init_stock: 3250 + price: 49 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU105: + cost: 99 + init_stock: 2850 + price: 110 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU106: + cost: 225 + init_stock: 3650 + price: 251 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU107: + cost: 380 + init_stock: 4350 + price: 423 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU108: + cost: 412 + init_stock: 4600 + price: 458 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU109: + cost: 79 + init_stock: 4100 + price: 88 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU11: + cost: 360 + init_stock: 2550 + price: 400 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU110: + cost: 59 + init_stock: 700 + price: 66 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU111: + cost: 234 + init_stock: 3050 + price: 260 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU112: + cost: 54 + init_stock: 4750 + price: 61 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU113: + cost: 313 + init_stock: 1550 + price: 348 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU114: + cost: 350 + init_stock: 1350 + price: 389 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU115: + cost: 257 + init_stock: 4300 + price: 286 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU116: + cost: 446 + init_stock: 3600 + price: 496 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU117: + cost: 288 + init_stock: 4600 + price: 320 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU118: + cost: 164 + init_stock: 1650 + price: 183 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU119: + cost: 188 + init_stock: 1600 + price: 209 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU12: + cost: 100 + init_stock: 4200 + price: 112 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU120: + cost: 108 + init_stock: 4900 + price: 121 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU121: + cost: 36 + init_stock: 4250 + price: 40 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU122: + cost: 393 + init_stock: 350 + price: 437 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU123: + cost: 209 + init_stock: 950 + price: 233 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU124: + cost: 163 + init_stock: 1800 + price: 182 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU125: + cost: 14 + init_stock: 4600 + price: 16 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU126: + cost: 32 + init_stock: 1950 + price: 36 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU127: + cost: 195 + init_stock: 1550 + price: 217 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU128: + cost: 148 + init_stock: 950 + price: 165 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU129: + cost: 128 + init_stock: 2500 + price: 143 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU13: + cost: 285 + init_stock: 2850 + price: 317 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU130: + cost: 313 + init_stock: 4900 + price: 348 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU131: + cost: 57 + init_stock: 2250 + price: 64 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU132: + cost: 384 + init_stock: 1050 + price: 427 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU133: + cost: 201 + init_stock: 1450 + price: 224 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU134: + cost: 302 + init_stock: 3850 + price: 336 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU135: + cost: 137 + init_stock: 5000 + price: 153 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU136: + cost: 179 + init_stock: 3550 + price: 199 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU137: + cost: 83 + init_stock: 3700 + price: 93 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU138: + cost: 205 + init_stock: 1800 + price: 228 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU139: + cost: 186 + init_stock: 1200 + price: 207 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU14: + cost: 241 + init_stock: 3100 + price: 268 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU140: + cost: 234 + init_stock: 1700 + price: 261 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU141: + cost: 171 + init_stock: 2050 + price: 190 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU142: + cost: 288 + init_stock: 1900 + price: 320 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU143: + cost: 286 + init_stock: 1300 + price: 318 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU144: + cost: 360 + init_stock: 600 + price: 400 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU145: + cost: 359 + init_stock: 4100 + price: 399 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU146: + cost: 159 + init_stock: 2400 + price: 177 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU147: + cost: 424 + init_stock: 2800 + price: 472 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU148: + cost: 281 + init_stock: 3850 + price: 313 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU149: + cost: 321 + init_stock: 3850 + price: 357 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU15: + cost: 46 + init_stock: 250 + price: 52 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU150: + cost: 95 + init_stock: 3500 + price: 106 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU151: + cost: 200 + init_stock: 3650 + price: 223 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU152: + cost: 9 + init_stock: 2550 + price: 10 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU153: + cost: 396 + init_stock: 3100 + price: 441 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU154: + cost: 69 + init_stock: 4250 + price: 77 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU155: + cost: 379 + init_stock: 2650 + price: 422 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU156: + cost: 9 + init_stock: 600 + price: 10 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU157: + cost: 369 + init_stock: 3750 + price: 410 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU158: + cost: 130 + init_stock: 4050 + price: 145 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU159: + cost: 173 + init_stock: 1250 + price: 193 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU16: + cost: 157 + init_stock: 2900 + price: 175 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU160: + cost: 413 + init_stock: 4250 + price: 459 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU161: + cost: 215 + init_stock: 2300 + price: 239 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU162: + cost: 142 + init_stock: 250 + price: 158 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU163: + cost: 437 + init_stock: 1950 + price: 486 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU164: + cost: 446 + init_stock: 5000 + price: 496 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU165: + cost: 246 + init_stock: 1650 + price: 274 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU166: + cost: 71 + init_stock: 4450 + price: 79 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU167: + cost: 398 + init_stock: 650 + price: 443 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU168: + cost: 321 + init_stock: 4350 + price: 357 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU169: + cost: 332 + init_stock: 4900 + price: 369 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU17: + cost: 311 + init_stock: 450 + price: 346 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU170: + cost: 61 + init_stock: 2750 + price: 68 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU171: + cost: 358 + init_stock: 3800 + price: 398 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU172: + cost: 180 + init_stock: 3550 + price: 200 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU173: + cost: 157 + init_stock: 4800 + price: 175 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU174: + cost: 261 + init_stock: 3800 + price: 291 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU175: + cost: 75 + init_stock: 3750 + price: 84 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU176: + cost: 366 + init_stock: 3300 + price: 407 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU177: + cost: 231 + init_stock: 1550 + price: 257 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU178: + cost: 380 + init_stock: 250 + price: 423 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU179: + cost: 447 + init_stock: 4150 + price: 497 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU18: + cost: 232 + init_stock: 4050 + price: 258 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU180: + cost: 195 + init_stock: 2750 + price: 217 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU181: + cost: 128 + init_stock: 3000 + price: 143 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU182: + cost: 393 + init_stock: 4950 + price: 437 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU183: + cost: 130 + init_stock: 400 + price: 145 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU184: + cost: 65 + init_stock: 1200 + price: 73 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU185: + cost: 9 + init_stock: 4500 + price: 10 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU186: + cost: 323 + init_stock: 1100 + price: 359 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU187: + cost: 159 + init_stock: 1500 + price: 177 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU188: + cost: 351 + init_stock: 4350 + price: 391 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU189: + cost: 322 + init_stock: 1750 + price: 358 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU19: + cost: 429 + init_stock: 4550 + price: 477 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU190: + cost: 101 + init_stock: 850 + price: 113 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU191: + cost: 425 + init_stock: 2700 + price: 473 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU192: + cost: 373 + init_stock: 3050 + price: 415 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU193: + cost: 186 + init_stock: 1500 + price: 207 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU194: + cost: 388 + init_stock: 250 + price: 432 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU195: + cost: 196 + init_stock: 1550 + price: 218 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU196: + cost: 44 + init_stock: 3400 + price: 49 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU197: + cost: 272 + init_stock: 2850 + price: 303 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU198: + cost: 152 + init_stock: 2700 + price: 169 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU199: + cost: 404 + init_stock: 1150 + price: 449 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU2: + cost: 297 + init_stock: 3500 + price: 331 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU20: + cost: 301 + init_stock: 1250 + price: 335 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU200: + cost: 58 + init_stock: 1250 + price: 65 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU201: + cost: 93 + init_stock: 2950 + price: 104 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU202: + cost: 127 + init_stock: 3650 + price: 142 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU203: + cost: 396 + init_stock: 4100 + price: 440 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU204: + cost: 440 + init_stock: 2350 + price: 489 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU205: + cost: 117 + init_stock: 5000 + price: 130 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU206: + cost: 301 + init_stock: 550 + price: 335 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU207: + cost: 126 + init_stock: 4000 + price: 140 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU208: + cost: 441 + init_stock: 3850 + price: 491 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU209: + cost: 161 + init_stock: 1000 + price: 179 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU21: + cost: 110 + init_stock: 5000 + price: 123 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU210: + cost: 363 + init_stock: 3450 + price: 404 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU211: + cost: 156 + init_stock: 4550 + price: 174 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU212: + cost: 364 + init_stock: 3950 + price: 405 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU213: + cost: 108 + init_stock: 3200 + price: 121 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU214: + cost: 90 + init_stock: 500 + price: 101 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU215: + cost: 377 + init_stock: 2350 + price: 419 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU216: + cost: 297 + init_stock: 1150 + price: 330 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU217: + cost: 255 + init_stock: 3250 + price: 284 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU218: + cost: 184 + init_stock: 2950 + price: 205 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU219: + cost: 82 + init_stock: 2300 + price: 92 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU22: + cost: 443 + init_stock: 3300 + price: 493 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU220: + cost: 348 + init_stock: 4350 + price: 387 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU221: + cost: 35 + init_stock: 3900 + price: 39 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU222: + cost: 103 + init_stock: 1800 + price: 115 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU223: + cost: 176 + init_stock: 600 + price: 196 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU224: + cost: 348 + init_stock: 250 + price: 387 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU225: + cost: 147 + init_stock: 1450 + price: 164 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU226: + cost: 185 + init_stock: 650 + price: 206 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU227: + cost: 431 + init_stock: 1200 + price: 479 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU228: + cost: 12 + init_stock: 4500 + price: 14 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU229: + cost: 424 + init_stock: 2200 + price: 472 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU23: + cost: 348 + init_stock: 2100 + price: 387 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU230: + cost: 216 + init_stock: 1150 + price: 241 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU231: + cost: 43 + init_stock: 4050 + price: 48 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU232: + cost: 201 + init_stock: 4800 + price: 224 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU233: + cost: 324 + init_stock: 3750 + price: 360 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU234: + cost: 258 + init_stock: 250 + price: 287 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU235: + cost: 21 + init_stock: 2850 + price: 24 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU236: + cost: 139 + init_stock: 2750 + price: 155 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU237: + cost: 389 + init_stock: 2250 + price: 433 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU238: + cost: 57 + init_stock: 3300 + price: 64 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU239: + cost: 92 + init_stock: 4900 + price: 103 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU24: + cost: 87 + init_stock: 2350 + price: 97 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU240: + cost: 335 + init_stock: 2350 + price: 373 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU241: + cost: 395 + init_stock: 3550 + price: 439 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU242: + cost: 15 + init_stock: 2200 + price: 17 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU243: + cost: 316 + init_stock: 700 + price: 352 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU244: + cost: 156 + init_stock: 4100 + price: 174 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU245: + cost: 363 + init_stock: 3300 + price: 404 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU246: + cost: 270 + init_stock: 1500 + price: 300 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU247: + cost: 347 + init_stock: 1750 + price: 386 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU248: + cost: 319 + init_stock: 1450 + price: 355 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU249: + cost: 320 + init_stock: 1250 + price: 356 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU25: + cost: 144 + init_stock: 250 + price: 161 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU250: + cost: 114 + init_stock: 2700 + price: 127 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU251: + cost: 309 + init_stock: 3050 + price: 344 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU252: + cost: 162 + init_stock: 4150 + price: 181 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU253: + cost: 403 + init_stock: 800 + price: 448 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU254: + cost: 435 + init_stock: 2300 + price: 484 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU255: + cost: 261 + init_stock: 3350 + price: 290 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU256: + cost: 81 + init_stock: 3600 + price: 91 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU257: + cost: 313 + init_stock: 2850 + price: 348 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU258: + cost: 440 + init_stock: 2150 + price: 489 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU259: + cost: 299 + init_stock: 3450 + price: 333 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU26: + cost: 206 + init_stock: 3150 + price: 229 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU260: + cost: 438 + init_stock: 2600 + price: 487 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU261: + cost: 331 + init_stock: 1100 + price: 368 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU262: + cost: 298 + init_stock: 3900 + price: 332 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU263: + cost: 170 + init_stock: 3700 + price: 189 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU264: + cost: 324 + init_stock: 3950 + price: 361 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU265: + cost: 257 + init_stock: 2950 + price: 286 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU266: + cost: 115 + init_stock: 2350 + price: 128 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU267: + cost: 69 + init_stock: 4000 + price: 77 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU268: + cost: 198 + init_stock: 4450 + price: 221 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU269: + cost: 113 + init_stock: 2200 + price: 126 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU27: + cost: 333 + init_stock: 2800 + price: 370 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU270: + cost: 163 + init_stock: 3700 + price: 182 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU271: + cost: 207 + init_stock: 900 + price: 230 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU272: + cost: 329 + init_stock: 850 + price: 366 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU273: + cost: 378 + init_stock: 900 + price: 421 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU274: + cost: 26 + init_stock: 3850 + price: 29 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU275: + cost: 45 + init_stock: 2400 + price: 50 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU276: + cost: 146 + init_stock: 2700 + price: 163 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU277: + cost: 404 + init_stock: 2050 + price: 449 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU278: + cost: 94 + init_stock: 600 + price: 105 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU279: + cost: 45 + init_stock: 1950 + price: 51 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU28: + cost: 187 + init_stock: 2350 + price: 208 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU280: + cost: 265 + init_stock: 1650 + price: 295 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU281: + cost: 355 + init_stock: 3750 + price: 395 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU282: + cost: 56 + init_stock: 2300 + price: 63 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU283: + cost: 352 + init_stock: 4600 + price: 392 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU284: + cost: 309 + init_stock: 3350 + price: 344 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU285: + cost: 119 + init_stock: 4550 + price: 133 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU286: + cost: 303 + init_stock: 1950 + price: 337 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU287: + cost: 337 + init_stock: 2800 + price: 375 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU288: + cost: 162 + init_stock: 1900 + price: 181 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU289: + cost: 60 + init_stock: 1550 + price: 67 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU29: + cost: 220 + init_stock: 2900 + price: 245 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU290: + cost: 394 + init_stock: 3350 + price: 438 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU291: + cost: 84 + init_stock: 3050 + price: 94 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU292: + cost: 167 + init_stock: 250 + price: 186 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU293: + cost: 145 + init_stock: 2750 + price: 162 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU294: + cost: 368 + init_stock: 450 + price: 409 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU295: + cost: 391 + init_stock: 4650 + price: 435 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU296: + cost: 333 + init_stock: 4600 + price: 370 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU297: + cost: 268 + init_stock: 1900 + price: 298 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU298: + cost: 257 + init_stock: 1750 + price: 286 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU299: + cost: 330 + init_stock: 2550 + price: 367 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU3: + cost: 364 + init_stock: 600 + price: 405 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU30: + cost: 323 + init_stock: 3450 + price: 359 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU300: + cost: 338 + init_stock: 2900 + price: 376 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU301: + cost: 389 + init_stock: 4150 + price: 433 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU302: + cost: 165 + init_stock: 550 + price: 184 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU303: + cost: 221 + init_stock: 4700 + price: 246 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU304: + cost: 377 + init_stock: 1150 + price: 419 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU305: + cost: 445 + init_stock: 5000 + price: 495 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU306: + cost: 431 + init_stock: 2100 + price: 479 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU307: + cost: 189 + init_stock: 3900 + price: 210 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU308: + cost: 187 + init_stock: 250 + price: 208 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU309: + cost: 90 + init_stock: 4600 + price: 101 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU31: + cost: 62 + init_stock: 2800 + price: 69 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU310: + cost: 210 + init_stock: 2200 + price: 234 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU311: + cost: 275 + init_stock: 4000 + price: 306 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU312: + cost: 261 + init_stock: 1250 + price: 291 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU313: + cost: 291 + init_stock: 1900 + price: 324 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU314: + cost: 363 + init_stock: 1450 + price: 404 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU315: + cost: 423 + init_stock: 4200 + price: 471 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU316: + cost: 181 + init_stock: 900 + price: 202 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU317: + cost: 194 + init_stock: 1200 + price: 216 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU318: + cost: 250 + init_stock: 4250 + price: 278 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU319: + cost: 231 + init_stock: 2650 + price: 257 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU32: + cost: 302 + init_stock: 1100 + price: 336 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU320: + cost: 176 + init_stock: 1950 + price: 196 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU321: + cost: 60 + init_stock: 800 + price: 67 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU322: + cost: 216 + init_stock: 5000 + price: 240 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU323: + cost: 59 + init_stock: 1950 + price: 66 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU324: + cost: 397 + init_stock: 4650 + price: 442 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU325: + cost: 407 + init_stock: 3450 + price: 453 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU326: + cost: 310 + init_stock: 1200 + price: 345 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU327: + cost: 411 + init_stock: 700 + price: 457 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU328: + cost: 351 + init_stock: 2250 + price: 390 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU329: + cost: 60 + init_stock: 2100 + price: 67 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU33: + cost: 374 + init_stock: 4600 + price: 416 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU330: + cost: 275 + init_stock: 1950 + price: 306 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU331: + cost: 124 + init_stock: 2050 + price: 138 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU332: + cost: 351 + init_stock: 4800 + price: 390 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU333: + cost: 436 + init_stock: 2650 + price: 485 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU334: + cost: 164 + init_stock: 2850 + price: 183 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU335: + cost: 72 + init_stock: 4050 + price: 80 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU336: + cost: 266 + init_stock: 1400 + price: 296 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU337: + cost: 220 + init_stock: 1450 + price: 245 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU338: + cost: 78 + init_stock: 4050 + price: 87 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU339: + cost: 238 + init_stock: 3150 + price: 265 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU34: + cost: 85 + init_stock: 650 + price: 95 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU340: + cost: 432 + init_stock: 4350 + price: 480 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU341: + cost: 415 + init_stock: 3500 + price: 462 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU342: + cost: 129 + init_stock: 450 + price: 144 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU343: + cost: 279 + init_stock: 750 + price: 310 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU344: + cost: 321 + init_stock: 4350 + price: 357 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU345: + cost: 397 + init_stock: 4450 + price: 442 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU346: + cost: 315 + init_stock: 2100 + price: 350 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU347: + cost: 447 + init_stock: 4100 + price: 497 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU348: + cost: 132 + init_stock: 1000 + price: 147 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU349: + cost: 60 + init_stock: 3350 + price: 67 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU35: + cost: 240 + init_stock: 4300 + price: 267 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU350: + cost: 251 + init_stock: 4600 + price: 279 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU351: + cost: 139 + init_stock: 3350 + price: 155 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU352: + cost: 63 + init_stock: 900 + price: 71 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU353: + cost: 227 + init_stock: 4650 + price: 253 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU354: + cost: 356 + init_stock: 3950 + price: 396 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU355: + cost: 56 + init_stock: 500 + price: 63 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU356: + cost: 313 + init_stock: 1450 + price: 348 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU357: + cost: 449 + init_stock: 4600 + price: 499 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU358: + cost: 242 + init_stock: 3450 + price: 269 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU359: + cost: 73 + init_stock: 3750 + price: 82 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU36: + cost: 94 + init_stock: 500 + price: 105 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU360: + cost: 435 + init_stock: 1250 + price: 484 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU361: + cost: 146 + init_stock: 4500 + price: 163 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU362: + cost: 417 + init_stock: 4350 + price: 464 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU363: + cost: 46 + init_stock: 3900 + price: 52 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU364: + cost: 348 + init_stock: 1650 + price: 387 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU365: + cost: 142 + init_stock: 4150 + price: 158 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU366: + cost: 399 + init_stock: 4300 + price: 444 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU367: + cost: 244 + init_stock: 2300 + price: 272 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU368: + cost: 424 + init_stock: 1650 + price: 472 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU369: + cost: 86 + init_stock: 3750 + price: 96 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU37: + cost: 59 + init_stock: 2950 + price: 66 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU370: + cost: 100 + init_stock: 750 + price: 112 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU371: + cost: 295 + init_stock: 250 + price: 328 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU372: + cost: 60 + init_stock: 1600 + price: 67 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU373: + cost: 52 + init_stock: 3350 + price: 58 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU374: + cost: 34 + init_stock: 2700 + price: 38 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU375: + cost: 254 + init_stock: 3600 + price: 283 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU376: + cost: 140 + init_stock: 4100 + price: 156 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU377: + cost: 70 + init_stock: 3350 + price: 78 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU378: + cost: 381 + init_stock: 1750 + price: 424 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU379: + cost: 9 + init_stock: 2450 + price: 11 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU38: + cost: 309 + init_stock: 2150 + price: 344 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU380: + cost: 353 + init_stock: 2650 + price: 393 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU381: + cost: 428 + init_stock: 300 + price: 476 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU382: + cost: 112 + init_stock: 3550 + price: 125 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU383: + cost: 273 + init_stock: 4600 + price: 304 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU384: + cost: 288 + init_stock: 450 + price: 320 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU385: + cost: 108 + init_stock: 650 + price: 120 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU386: + cost: 265 + init_stock: 1550 + price: 295 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU387: + cost: 100 + init_stock: 4850 + price: 112 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU388: + cost: 364 + init_stock: 2200 + price: 405 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU389: + cost: 338 + init_stock: 3500 + price: 376 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU39: + cost: 22 + init_stock: 2550 + price: 25 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU390: + cost: 129 + init_stock: 1950 + price: 144 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU391: + cost: 209 + init_stock: 3350 + price: 233 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU392: + cost: 146 + init_stock: 3700 + price: 163 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU393: + cost: 438 + init_stock: 3350 + price: 487 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU394: + cost: 138 + init_stock: 2650 + price: 154 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU395: + cost: 439 + init_stock: 1650 + price: 488 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU396: + cost: 299 + init_stock: 3050 + price: 333 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU397: + cost: 414 + init_stock: 2550 + price: 460 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU398: + cost: 210 + init_stock: 2900 + price: 234 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU399: + cost: 338 + init_stock: 1850 + price: 376 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU4: + cost: 395 + init_stock: 350 + price: 439 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU40: + cost: 441 + init_stock: 4950 + price: 490 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU400: + cost: 400 + init_stock: 4200 + price: 445 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU401: + cost: 369 + init_stock: 3900 + price: 410 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU402: + cost: 77 + init_stock: 350 + price: 86 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU403: + cost: 80 + init_stock: 4950 + price: 89 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU404: + cost: 258 + init_stock: 3050 + price: 287 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU405: + cost: 414 + init_stock: 950 + price: 460 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU406: + cost: 294 + init_stock: 5000 + price: 327 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU407: + cost: 23 + init_stock: 2300 + price: 26 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU408: + cost: 399 + init_stock: 400 + price: 444 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU409: + cost: 231 + init_stock: 4550 + price: 257 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU41: + cost: 126 + init_stock: 3950 + price: 141 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU410: + cost: 63 + init_stock: 800 + price: 70 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU411: + cost: 189 + init_stock: 4750 + price: 210 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU412: + cost: 257 + init_stock: 3100 + price: 286 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU413: + cost: 362 + init_stock: 4150 + price: 403 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU414: + cost: 148 + init_stock: 4350 + price: 165 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU415: + cost: 261 + init_stock: 1150 + price: 291 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU416: + cost: 205 + init_stock: 450 + price: 228 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU417: + cost: 398 + init_stock: 3600 + price: 443 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU418: + cost: 412 + init_stock: 650 + price: 458 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU419: + cost: 272 + init_stock: 4450 + price: 303 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU42: + cost: 415 + init_stock: 1300 + price: 462 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU420: + cost: 241 + init_stock: 2100 + price: 268 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU421: + cost: 192 + init_stock: 2300 + price: 214 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU422: + cost: 46 + init_stock: 2700 + price: 52 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU423: + cost: 169 + init_stock: 3300 + price: 188 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU424: + cost: 170 + init_stock: 550 + price: 189 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU425: + cost: 145 + init_stock: 600 + price: 162 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU426: + cost: 112 + init_stock: 4900 + price: 125 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU427: + cost: 371 + init_stock: 4700 + price: 413 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU428: + cost: 351 + init_stock: 3150 + price: 391 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU429: + cost: 35 + init_stock: 2050 + price: 39 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU43: + cost: 195 + init_stock: 3400 + price: 217 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU430: + cost: 194 + init_stock: 3650 + price: 216 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU431: + cost: 295 + init_stock: 1050 + price: 328 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU432: + cost: 22 + init_stock: 2300 + price: 25 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU433: + cost: 313 + init_stock: 2250 + price: 348 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU434: + cost: 33 + init_stock: 1500 + price: 37 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU435: + cost: 244 + init_stock: 1500 + price: 272 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU436: + cost: 64 + init_stock: 4050 + price: 72 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU437: + cost: 103 + init_stock: 700 + price: 115 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU438: + cost: 128 + init_stock: 500 + price: 143 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU439: + cost: 230 + init_stock: 1900 + price: 256 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU44: + cost: 324 + init_stock: 2400 + price: 360 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU440: + cost: 223 + init_stock: 4100 + price: 248 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU441: + cost: 227 + init_stock: 2800 + price: 253 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU442: + cost: 423 + init_stock: 4400 + price: 470 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU443: + cost: 196 + init_stock: 3650 + price: 218 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU444: + cost: 140 + init_stock: 300 + price: 156 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU445: + cost: 234 + init_stock: 4300 + price: 260 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU446: + cost: 447 + init_stock: 2450 + price: 497 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU447: + cost: 176 + init_stock: 250 + price: 196 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU448: + cost: 436 + init_stock: 3550 + price: 485 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU449: + cost: 253 + init_stock: 1900 + price: 282 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU45: + cost: 227 + init_stock: 500 + price: 253 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU450: + cost: 52 + init_stock: 4900 + price: 58 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU451: + cost: 421 + init_stock: 3450 + price: 468 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU452: + cost: 56 + init_stock: 3950 + price: 63 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU453: + cost: 33 + init_stock: 1750 + price: 37 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU454: + cost: 444 + init_stock: 4250 + price: 494 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU455: + cost: 122 + init_stock: 1300 + price: 136 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU456: + cost: 304 + init_stock: 1250 + price: 338 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU457: + cost: 152 + init_stock: 3200 + price: 169 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU458: + cost: 362 + init_stock: 350 + price: 403 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU459: + cost: 382 + init_stock: 1700 + price: 425 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU46: + cost: 269 + init_stock: 2500 + price: 299 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU460: + cost: 364 + init_stock: 3650 + price: 405 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU461: + cost: 225 + init_stock: 2400 + price: 251 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU462: + cost: 403 + init_stock: 450 + price: 448 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU463: + cost: 168 + init_stock: 4100 + price: 187 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU464: + cost: 251 + init_stock: 1700 + price: 279 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU465: + cost: 132 + init_stock: 5000 + price: 147 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU466: + cost: 87 + init_stock: 2100 + price: 97 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU467: + cost: 127 + init_stock: 1800 + price: 142 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU468: + cost: 49 + init_stock: 2500 + price: 55 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU469: + cost: 160 + init_stock: 750 + price: 178 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU47: + cost: 108 + init_stock: 1950 + price: 121 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU470: + cost: 335 + init_stock: 1600 + price: 373 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU471: + cost: 283 + init_stock: 3550 + price: 315 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU472: + cost: 378 + init_stock: 2950 + price: 421 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU473: + cost: 175 + init_stock: 1050 + price: 195 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU474: + cost: 403 + init_stock: 4300 + price: 448 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU475: + cost: 30 + init_stock: 4700 + price: 34 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU476: + cost: 225 + init_stock: 4500 + price: 251 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU477: + cost: 260 + init_stock: 2350 + price: 289 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU478: + cost: 431 + init_stock: 4400 + price: 479 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU479: + cost: 329 + init_stock: 1900 + price: 366 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU48: + cost: 306 + init_stock: 2900 + price: 340 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU480: + cost: 16 + init_stock: 1500 + price: 18 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU481: + cost: 297 + init_stock: 4400 + price: 330 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU482: + cost: 84 + init_stock: 4350 + price: 94 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU483: + cost: 268 + init_stock: 1700 + price: 298 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU484: + cost: 75 + init_stock: 3650 + price: 84 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU485: + cost: 286 + init_stock: 3350 + price: 318 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU486: + cost: 109 + init_stock: 2600 + price: 122 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU487: + cost: 249 + init_stock: 3300 + price: 277 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU488: + cost: 32 + init_stock: 1350 + price: 36 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU489: + cost: 53 + init_stock: 1550 + price: 59 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU49: + cost: 236 + init_stock: 4750 + price: 263 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU490: + cost: 144 + init_stock: 4050 + price: 161 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU491: + cost: 399 + init_stock: 3750 + price: 444 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU492: + cost: 47 + init_stock: 4000 + price: 53 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU493: + cost: 414 + init_stock: 1350 + price: 461 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU494: + cost: 130 + init_stock: 4700 + price: 145 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU495: + cost: 134 + init_stock: 3100 + price: 149 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU496: + cost: 307 + init_stock: 2950 + price: 342 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU497: + cost: 29 + init_stock: 450 + price: 33 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU498: + cost: 274 + init_stock: 3250 + price: 305 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU499: + cost: 276 + init_stock: 1450 + price: 307 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU5: + cost: 419 + init_stock: 3550 + price: 466 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU50: + cost: 253 + init_stock: 1850 + price: 282 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU500: + cost: 187 + init_stock: 2100 + price: 208 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU501: + cost: 174 + init_stock: 3800 + price: 194 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU502: + cost: 62 + init_stock: 3250 + price: 69 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU503: + cost: 187 + init_stock: 2850 + price: 208 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU504: + cost: 225 + init_stock: 1500 + price: 251 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU505: + cost: 383 + init_stock: 2950 + price: 426 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU506: + cost: 134 + init_stock: 4650 + price: 149 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU507: + cost: 168 + init_stock: 750 + price: 187 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU508: + cost: 244 + init_stock: 4800 + price: 272 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU509: + cost: 267 + init_stock: 4050 + price: 297 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU51: + cost: 265 + init_stock: 3350 + price: 295 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU510: + cost: 84 + init_stock: 450 + price: 94 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU511: + cost: 324 + init_stock: 3750 + price: 361 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU512: + cost: 420 + init_stock: 1100 + price: 467 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU513: + cost: 308 + init_stock: 350 + price: 343 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU514: + cost: 167 + init_stock: 3150 + price: 186 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU515: + cost: 130 + init_stock: 2700 + price: 145 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU516: + cost: 57 + init_stock: 1900 + price: 64 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU517: + cost: 105 + init_stock: 450 + price: 117 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU518: + cost: 23 + init_stock: 1050 + price: 26 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU519: + cost: 209 + init_stock: 2500 + price: 233 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU52: + cost: 19 + init_stock: 3350 + price: 22 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU520: + cost: 188 + init_stock: 1100 + price: 209 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU521: + cost: 198 + init_stock: 2500 + price: 220 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU522: + cost: 140 + init_stock: 4350 + price: 156 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU523: + cost: 58 + init_stock: 5000 + price: 65 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU524: + cost: 264 + init_stock: 4700 + price: 294 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU525: + cost: 388 + init_stock: 2100 + price: 432 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU526: + cost: 377 + init_stock: 1200 + price: 419 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU527: + cost: 117 + init_stock: 3500 + price: 131 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU528: + cost: 284 + init_stock: 1050 + price: 316 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU529: + cost: 268 + init_stock: 1550 + price: 298 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU53: + cost: 135 + init_stock: 3500 + price: 151 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU530: + cost: 117 + init_stock: 2250 + price: 131 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU531: + cost: 92 + init_stock: 3000 + price: 103 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU532: + cost: 315 + init_stock: 3850 + price: 351 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU533: + cost: 266 + init_stock: 2100 + price: 296 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU534: + cost: 382 + init_stock: 2050 + price: 425 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU535: + cost: 273 + init_stock: 4300 + price: 304 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU536: + cost: 322 + init_stock: 2600 + price: 358 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU537: + cost: 288 + init_stock: 450 + price: 321 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU538: + cost: 411 + init_stock: 2500 + price: 457 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU539: + cost: 148 + init_stock: 3900 + price: 165 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU54: + cost: 142 + init_stock: 2300 + price: 158 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU540: + cost: 349 + init_stock: 2100 + price: 388 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU541: + cost: 117 + init_stock: 1450 + price: 131 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU542: + cost: 34 + init_stock: 1300 + price: 38 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU543: + cost: 387 + init_stock: 4250 + price: 430 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU544: + cost: 311 + init_stock: 4850 + price: 346 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU545: + cost: 157 + init_stock: 750 + price: 175 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU546: + cost: 441 + init_stock: 4800 + price: 491 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU547: + cost: 144 + init_stock: 2200 + price: 161 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU548: + cost: 99 + init_stock: 300 + price: 111 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU549: + cost: 348 + init_stock: 3950 + price: 387 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU55: + cost: 433 + init_stock: 3250 + price: 482 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU550: + cost: 233 + init_stock: 4700 + price: 259 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU551: + cost: 41 + init_stock: 1550 + price: 46 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU552: + cost: 171 + init_stock: 4500 + price: 191 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU553: + cost: 187 + init_stock: 1500 + price: 208 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU554: + cost: 306 + init_stock: 2050 + price: 340 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU555: + cost: 440 + init_stock: 4200 + price: 489 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU556: + cost: 99 + init_stock: 1500 + price: 110 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU557: + cost: 300 + init_stock: 3650 + price: 334 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU558: + cost: 359 + init_stock: 850 + price: 399 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU559: + cost: 281 + init_stock: 2700 + price: 313 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU56: + cost: 339 + init_stock: 3700 + price: 377 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU560: + cost: 254 + init_stock: 2050 + price: 283 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU561: + cost: 181 + init_stock: 1950 + price: 202 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU562: + cost: 312 + init_stock: 4450 + price: 347 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU563: + cost: 360 + init_stock: 250 + price: 401 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU564: + cost: 98 + init_stock: 450 + price: 109 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU565: + cost: 51 + init_stock: 3750 + price: 57 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU566: + cost: 117 + init_stock: 550 + price: 131 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU567: + cost: 324 + init_stock: 450 + price: 361 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU568: + cost: 67 + init_stock: 3900 + price: 75 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU569: + cost: 425 + init_stock: 2400 + price: 473 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU57: + cost: 323 + init_stock: 2500 + price: 359 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU570: + cost: 349 + init_stock: 4150 + price: 388 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU571: + cost: 151 + init_stock: 1950 + price: 168 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU572: + cost: 23 + init_stock: 2250 + price: 26 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU573: + cost: 221 + init_stock: 2050 + price: 246 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU574: + cost: 84 + init_stock: 2500 + price: 94 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU575: + cost: 213 + init_stock: 3950 + price: 237 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU576: + cost: 238 + init_stock: 3900 + price: 265 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU577: + cost: 16 + init_stock: 4350 + price: 18 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU578: + cost: 90 + init_stock: 3550 + price: 100 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU579: + cost: 373 + init_stock: 450 + price: 415 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU58: + cost: 325 + init_stock: 2400 + price: 362 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU580: + cost: 182 + init_stock: 250 + price: 203 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU581: + cost: 136 + init_stock: 4300 + price: 152 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU582: + cost: 323 + init_stock: 4800 + price: 359 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU583: + cost: 77 + init_stock: 4350 + price: 86 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU584: + cost: 29 + init_stock: 2250 + price: 33 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU585: + cost: 27 + init_stock: 1000 + price: 31 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU586: + cost: 54 + init_stock: 2200 + price: 61 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU587: + cost: 261 + init_stock: 3900 + price: 290 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU588: + cost: 428 + init_stock: 2200 + price: 476 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU589: + cost: 219 + init_stock: 1700 + price: 244 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU59: + cost: 130 + init_stock: 2550 + price: 145 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU590: + cost: 63 + init_stock: 1550 + price: 70 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU591: + cost: 185 + init_stock: 4200 + price: 206 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU592: + cost: 196 + init_stock: 2300 + price: 218 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU593: + cost: 111 + init_stock: 2200 + price: 124 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU594: + cost: 214 + init_stock: 2500 + price: 238 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU595: + cost: 65 + init_stock: 2150 + price: 73 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU596: + cost: 388 + init_stock: 350 + price: 432 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU597: + cost: 226 + init_stock: 4350 + price: 252 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU598: + cost: 313 + init_stock: 700 + price: 348 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU599: + cost: 48 + init_stock: 3400 + price: 54 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU6: + cost: 109 + init_stock: 4400 + price: 122 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU60: + cost: 180 + init_stock: 1950 + price: 200 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU600: + cost: 347 + init_stock: 3250 + price: 386 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU601: + cost: 124 + init_stock: 1950 + price: 138 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU602: + cost: 165 + init_stock: 4900 + price: 184 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU603: + cost: 250 + init_stock: 2100 + price: 278 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU604: + cost: 243 + init_stock: 2500 + price: 270 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU605: + cost: 259 + init_stock: 1200 + price: 288 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU606: + cost: 102 + init_stock: 550 + price: 114 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU607: + cost: 187 + init_stock: 3950 + price: 208 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU608: + cost: 35 + init_stock: 3650 + price: 39 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU609: + cost: 91 + init_stock: 950 + price: 102 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU61: + cost: 414 + init_stock: 3750 + price: 461 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU610: + cost: 404 + init_stock: 3400 + price: 449 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU611: + cost: 275 + init_stock: 3850 + price: 306 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU612: + cost: 351 + init_stock: 4400 + price: 391 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU613: + cost: 156 + init_stock: 350 + price: 174 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU614: + cost: 155 + init_stock: 750 + price: 173 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU615: + cost: 297 + init_stock: 4550 + price: 330 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU616: + cost: 208 + init_stock: 3650 + price: 232 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU617: + cost: 182 + init_stock: 3000 + price: 203 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU618: + cost: 69 + init_stock: 1150 + price: 77 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU619: + cost: 170 + init_stock: 4300 + price: 189 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU62: + cost: 111 + init_stock: 450 + price: 124 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU620: + cost: 207 + init_stock: 1950 + price: 231 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU621: + cost: 179 + init_stock: 3600 + price: 199 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU622: + cost: 227 + init_stock: 3250 + price: 253 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU623: + cost: 301 + init_stock: 2750 + price: 335 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU624: + cost: 433 + init_stock: 2700 + price: 482 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU625: + cost: 41 + init_stock: 4450 + price: 46 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU626: + cost: 405 + init_stock: 4450 + price: 451 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU627: + cost: 198 + init_stock: 4100 + price: 220 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU628: + cost: 111 + init_stock: 1300 + price: 124 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU629: + cost: 317 + init_stock: 4600 + price: 353 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU63: + cost: 61 + init_stock: 700 + price: 68 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU630: + cost: 109 + init_stock: 850 + price: 122 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU631: + cost: 266 + init_stock: 2450 + price: 296 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU632: + cost: 76 + init_stock: 4700 + price: 85 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU633: + cost: 165 + init_stock: 3350 + price: 184 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU634: + cost: 15 + init_stock: 3650 + price: 17 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU635: + cost: 306 + init_stock: 4650 + price: 341 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU636: + cost: 346 + init_stock: 1850 + price: 385 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU637: + cost: 74 + init_stock: 3050 + price: 83 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU638: + cost: 337 + init_stock: 400 + price: 375 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU639: + cost: 294 + init_stock: 2000 + price: 327 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU64: + cost: 32 + init_stock: 950 + price: 36 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU640: + cost: 247 + init_stock: 4550 + price: 275 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU641: + cost: 328 + init_stock: 4050 + price: 365 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU642: + cost: 372 + init_stock: 1250 + price: 414 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU643: + cost: 322 + init_stock: 2300 + price: 358 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU644: + cost: 41 + init_stock: 4100 + price: 46 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU645: + cost: 420 + init_stock: 4950 + price: 467 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU646: + cost: 170 + init_stock: 650 + price: 189 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU647: + cost: 272 + init_stock: 3850 + price: 303 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU648: + cost: 203 + init_stock: 2750 + price: 226 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU649: + cost: 178 + init_stock: 1250 + price: 198 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU65: + cost: 13 + init_stock: 4700 + price: 15 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU650: + cost: 315 + init_stock: 2800 + price: 351 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU651: + cost: 342 + init_stock: 4800 + price: 380 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU652: + cost: 110 + init_stock: 2000 + price: 123 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU653: + cost: 431 + init_stock: 4800 + price: 479 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU654: + cost: 59 + init_stock: 400 + price: 66 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU655: + cost: 299 + init_stock: 2100 + price: 333 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU656: + cost: 47 + init_stock: 3600 + price: 53 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU657: + cost: 65 + init_stock: 4050 + price: 73 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU658: + cost: 63 + init_stock: 1800 + price: 70 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU659: + cost: 18 + init_stock: 2800 + price: 21 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU66: + cost: 128 + init_stock: 4500 + price: 143 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU660: + cost: 438 + init_stock: 4150 + price: 487 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU661: + cost: 309 + init_stock: 3700 + price: 344 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU662: + cost: 334 + init_stock: 1900 + price: 372 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU663: + cost: 376 + init_stock: 2000 + price: 418 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU664: + cost: 315 + init_stock: 4200 + price: 351 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU665: + cost: 443 + init_stock: 2150 + price: 493 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU666: + cost: 306 + init_stock: 4400 + price: 341 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU667: + cost: 292 + init_stock: 650 + price: 325 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU668: + cost: 257 + init_stock: 3000 + price: 286 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU669: + cost: 66 + init_stock: 800 + price: 74 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU67: + cost: 222 + init_stock: 2650 + price: 247 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU670: + cost: 260 + init_stock: 4300 + price: 289 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU671: + cost: 240 + init_stock: 4150 + price: 267 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU672: + cost: 332 + init_stock: 650 + price: 369 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU673: + cost: 289 + init_stock: 2050 + price: 322 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU674: + cost: 200 + init_stock: 1100 + price: 223 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU675: + cost: 394 + init_stock: 1650 + price: 438 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU676: + cost: 219 + init_stock: 3050 + price: 244 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU677: + cost: 382 + init_stock: 2200 + price: 425 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU678: + cost: 151 + init_stock: 1800 + price: 168 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU679: + cost: 355 + init_stock: 2200 + price: 395 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU68: + cost: 152 + init_stock: 700 + price: 169 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU680: + cost: 234 + init_stock: 1500 + price: 260 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU681: + cost: 417 + init_stock: 4850 + price: 464 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU682: + cost: 333 + init_stock: 2650 + price: 370 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU683: + cost: 294 + init_stock: 3350 + price: 327 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU684: + cost: 319 + init_stock: 3950 + price: 355 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU685: + cost: 379 + init_stock: 2250 + price: 422 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU686: + cost: 56 + init_stock: 3150 + price: 63 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU687: + cost: 30 + init_stock: 3800 + price: 34 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU688: + cost: 435 + init_stock: 3850 + price: 484 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU689: + cost: 449 + init_stock: 750 + price: 499 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU69: + cost: 158 + init_stock: 3350 + price: 176 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU690: + cost: 393 + init_stock: 4150 + price: 437 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU691: + cost: 15 + init_stock: 3950 + price: 17 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU692: + cost: 202 + init_stock: 3250 + price: 225 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU693: + cost: 17 + init_stock: 3050 + price: 19 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU694: + cost: 187 + init_stock: 3750 + price: 208 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU695: + cost: 171 + init_stock: 350 + price: 190 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU696: + cost: 313 + init_stock: 2900 + price: 348 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU697: + cost: 409 + init_stock: 4000 + price: 455 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU698: + cost: 178 + init_stock: 3050 + price: 198 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU699: + cost: 27 + init_stock: 1000 + price: 31 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU7: + cost: 90 + init_stock: 4800 + price: 101 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU70: + cost: 167 + init_stock: 1750 + price: 186 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU700: + cost: 66 + init_stock: 450 + price: 74 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU701: + cost: 359 + init_stock: 3850 + price: 399 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU702: + cost: 73 + init_stock: 1700 + price: 82 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU703: + cost: 326 + init_stock: 1900 + price: 363 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU704: + cost: 345 + init_stock: 4350 + price: 384 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU705: + cost: 115 + init_stock: 3150 + price: 128 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU706: + cost: 163 + init_stock: 4550 + price: 182 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU707: + cost: 16 + init_stock: 3450 + price: 18 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU708: + cost: 160 + init_stock: 1400 + price: 178 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU709: + cost: 91 + init_stock: 2650 + price: 102 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU71: + cost: 283 + init_stock: 2250 + price: 315 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU710: + cost: 226 + init_stock: 950 + price: 252 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU711: + cost: 252 + init_stock: 3450 + price: 281 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU712: + cost: 208 + init_stock: 550 + price: 232 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU713: + cost: 256 + init_stock: 2150 + price: 285 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU714: + cost: 71 + init_stock: 2050 + price: 79 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU715: + cost: 210 + init_stock: 850 + price: 234 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU716: + cost: 63 + init_stock: 3750 + price: 70 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU717: + cost: 310 + init_stock: 400 + price: 345 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU718: + cost: 321 + init_stock: 2900 + price: 357 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU719: + cost: 306 + init_stock: 3250 + price: 340 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU72: + cost: 412 + init_stock: 4250 + price: 458 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU720: + cost: 292 + init_stock: 2100 + price: 325 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU721: + cost: 65 + init_stock: 550 + price: 73 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU722: + cost: 352 + init_stock: 4850 + price: 392 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU723: + cost: 286 + init_stock: 4450 + price: 318 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU724: + cost: 360 + init_stock: 1650 + price: 400 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU725: + cost: 157 + init_stock: 1850 + price: 175 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU726: + cost: 412 + init_stock: 4450 + price: 458 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU727: + cost: 376 + init_stock: 2850 + price: 418 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU728: + cost: 427 + init_stock: 1850 + price: 475 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU729: + cost: 291 + init_stock: 1800 + price: 324 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU73: + cost: 190 + init_stock: 2700 + price: 212 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU730: + cost: 14 + init_stock: 650 + price: 16 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU731: + cost: 79 + init_stock: 4100 + price: 88 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU732: + cost: 36 + init_stock: 3100 + price: 41 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU733: + cost: 283 + init_stock: 1300 + price: 315 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU734: + cost: 33 + init_stock: 2550 + price: 37 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU735: + cost: 239 + init_stock: 4950 + price: 266 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU736: + cost: 331 + init_stock: 1250 + price: 368 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU737: + cost: 427 + init_stock: 1550 + price: 475 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU738: + cost: 166 + init_stock: 2400 + price: 185 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU739: + cost: 427 + init_stock: 3700 + price: 475 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU74: + cost: 143 + init_stock: 2100 + price: 159 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU740: + cost: 351 + init_stock: 3200 + price: 390 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU741: + cost: 81 + init_stock: 2800 + price: 91 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU742: + cost: 169 + init_stock: 1100 + price: 188 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU743: + cost: 195 + init_stock: 2150 + price: 217 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU744: + cost: 341 + init_stock: 2900 + price: 379 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU745: + cost: 284 + init_stock: 4600 + price: 316 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU746: + cost: 393 + init_stock: 2000 + price: 437 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU747: + cost: 335 + init_stock: 3950 + price: 373 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU748: + cost: 247 + init_stock: 3300 + price: 275 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU749: + cost: 354 + init_stock: 2650 + price: 394 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU75: + cost: 117 + init_stock: 950 + price: 131 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU750: + cost: 230 + init_stock: 3100 + price: 256 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU751: + cost: 332 + init_stock: 3250 + price: 369 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU752: + cost: 233 + init_stock: 3900 + price: 259 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU753: + cost: 69 + init_stock: 3250 + price: 77 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU754: + cost: 348 + init_stock: 650 + price: 387 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU755: + cost: 318 + init_stock: 4000 + price: 354 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU756: + cost: 221 + init_stock: 4500 + price: 246 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU757: + cost: 36 + init_stock: 2850 + price: 40 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU758: + cost: 216 + init_stock: 2000 + price: 241 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU759: + cost: 391 + init_stock: 1900 + price: 435 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU76: + cost: 132 + init_stock: 3200 + price: 147 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU760: + cost: 158 + init_stock: 2550 + price: 176 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU761: + cost: 201 + init_stock: 2750 + price: 224 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU762: + cost: 237 + init_stock: 2550 + price: 264 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU763: + cost: 346 + init_stock: 3950 + price: 385 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU764: + cost: 314 + init_stock: 1700 + price: 349 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU765: + cost: 310 + init_stock: 650 + price: 345 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU766: + cost: 430 + init_stock: 1000 + price: 478 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU767: + cost: 85 + init_stock: 3800 + price: 95 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU768: + cost: 162 + init_stock: 4600 + price: 181 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU769: + cost: 21 + init_stock: 1450 + price: 24 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU77: + cost: 368 + init_stock: 3600 + price: 409 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU770: + cost: 135 + init_stock: 3100 + price: 150 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU771: + cost: 90 + init_stock: 350 + price: 101 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU772: + cost: 230 + init_stock: 300 + price: 256 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU773: + cost: 75 + init_stock: 4600 + price: 84 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU774: + cost: 402 + init_stock: 2950 + price: 447 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU775: + cost: 157 + init_stock: 4300 + price: 175 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU776: + cost: 92 + init_stock: 4250 + price: 103 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU777: + cost: 262 + init_stock: 2850 + price: 292 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU778: + cost: 182 + init_stock: 400 + price: 203 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU779: + cost: 105 + init_stock: 2150 + price: 117 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU78: + cost: 210 + init_stock: 650 + price: 234 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU780: + cost: 62 + init_stock: 4100 + price: 69 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU781: + cost: 334 + init_stock: 1800 + price: 372 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU782: + cost: 24 + init_stock: 500 + price: 27 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU783: + cost: 78 + init_stock: 4050 + price: 87 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU784: + cost: 278 + init_stock: 2600 + price: 309 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU785: + cost: 171 + init_stock: 3500 + price: 191 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU786: + cost: 81 + init_stock: 1100 + price: 91 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU787: + cost: 324 + init_stock: 3050 + price: 360 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU788: + cost: 315 + init_stock: 1400 + price: 351 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU789: + cost: 137 + init_stock: 2900 + price: 153 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU79: + cost: 220 + init_stock: 550 + price: 245 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU790: + cost: 375 + init_stock: 3300 + price: 417 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU791: + cost: 120 + init_stock: 1400 + price: 134 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU792: + cost: 281 + init_stock: 1050 + price: 313 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU793: + cost: 175 + init_stock: 3800 + price: 195 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU794: + cost: 292 + init_stock: 4000 + price: 325 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU795: + cost: 248 + init_stock: 2400 + price: 276 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU796: + cost: 402 + init_stock: 2450 + price: 447 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU797: + cost: 90 + init_stock: 1950 + price: 100 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU798: + cost: 383 + init_stock: 1500 + price: 426 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU799: + cost: 56 + init_stock: 350 + price: 63 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU8: + cost: 112 + init_stock: 3150 + price: 125 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU80: + cost: 146 + init_stock: 3500 + price: 163 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU800: + cost: 268 + init_stock: 4700 + price: 298 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU801: + cost: 350 + init_stock: 1800 + price: 389 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU802: + cost: 155 + init_stock: 3100 + price: 173 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU803: + cost: 244 + init_stock: 950 + price: 272 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU804: + cost: 351 + init_stock: 2350 + price: 390 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU805: + cost: 54 + init_stock: 1650 + price: 61 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU806: + cost: 142 + init_stock: 2600 + price: 158 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU807: + cost: 407 + init_stock: 1300 + price: 453 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU808: + cost: 224 + init_stock: 4550 + price: 249 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU809: + cost: 49 + init_stock: 3250 + price: 55 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU81: + cost: 163 + init_stock: 3300 + price: 182 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU810: + cost: 248 + init_stock: 300 + price: 276 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU811: + cost: 228 + init_stock: 1850 + price: 254 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU812: + cost: 226 + init_stock: 4000 + price: 252 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU813: + cost: 227 + init_stock: 350 + price: 253 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU814: + cost: 436 + init_stock: 4850 + price: 485 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU815: + cost: 351 + init_stock: 1200 + price: 390 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU816: + cost: 136 + init_stock: 4550 + price: 152 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU817: + cost: 204 + init_stock: 250 + price: 227 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU818: + cost: 318 + init_stock: 2150 + price: 354 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU819: + cost: 271 + init_stock: 1350 + price: 302 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU82: + cost: 261 + init_stock: 1400 + price: 290 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU820: + cost: 237 + init_stock: 4100 + price: 264 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU821: + cost: 89 + init_stock: 3450 + price: 99 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU822: + cost: 122 + init_stock: 3500 + price: 136 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU823: + cost: 67 + init_stock: 1400 + price: 75 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU824: + cost: 153 + init_stock: 3500 + price: 170 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU825: + cost: 192 + init_stock: 750 + price: 214 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU826: + cost: 347 + init_stock: 2750 + price: 386 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU827: + cost: 133 + init_stock: 3200 + price: 148 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU828: + cost: 360 + init_stock: 3450 + price: 400 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU829: + cost: 54 + init_stock: 3700 + price: 61 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU83: + cost: 266 + init_stock: 850 + price: 296 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU830: + cost: 150 + init_stock: 1200 + price: 167 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU831: + cost: 235 + init_stock: 1450 + price: 262 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU832: + cost: 29 + init_stock: 4100 + price: 33 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU833: + cost: 360 + init_stock: 4900 + price: 400 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU834: + cost: 379 + init_stock: 250 + price: 422 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU835: + cost: 396 + init_stock: 2600 + price: 440 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU836: + cost: 290 + init_stock: 4800 + price: 323 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU837: + cost: 335 + init_stock: 1300 + price: 373 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU838: + cost: 410 + init_stock: 3850 + price: 456 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU839: + cost: 425 + init_stock: 3000 + price: 473 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU84: + cost: 286 + init_stock: 2100 + price: 318 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU840: + cost: 239 + init_stock: 2500 + price: 266 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU841: + cost: 256 + init_stock: 4250 + price: 285 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU842: + cost: 36 + init_stock: 4200 + price: 41 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU843: + cost: 324 + init_stock: 3600 + price: 360 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU844: + cost: 45 + init_stock: 3950 + price: 51 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU845: + cost: 259 + init_stock: 1100 + price: 288 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU846: + cost: 436 + init_stock: 1950 + price: 485 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU847: + cost: 349 + init_stock: 4550 + price: 388 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU848: + cost: 275 + init_stock: 2300 + price: 306 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU849: + cost: 288 + init_stock: 2000 + price: 320 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU85: + cost: 397 + init_stock: 5000 + price: 442 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU850: + cost: 164 + init_stock: 2850 + price: 183 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU851: + cost: 47 + init_stock: 3200 + price: 53 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU852: + cost: 275 + init_stock: 2700 + price: 306 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU853: + cost: 347 + init_stock: 2800 + price: 386 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU854: + cost: 190 + init_stock: 3250 + price: 212 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU855: + cost: 68 + init_stock: 3600 + price: 76 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU856: + cost: 342 + init_stock: 1600 + price: 380 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU857: + cost: 415 + init_stock: 5000 + price: 462 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU858: + cost: 72 + init_stock: 1200 + price: 80 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU859: + cost: 193 + init_stock: 1400 + price: 215 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU86: + cost: 347 + init_stock: 4250 + price: 386 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU860: + cost: 281 + init_stock: 750 + price: 313 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU861: + cost: 429 + init_stock: 650 + price: 477 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU862: + cost: 216 + init_stock: 800 + price: 240 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU863: + cost: 423 + init_stock: 4650 + price: 470 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU864: + cost: 182 + init_stock: 950 + price: 203 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU865: + cost: 129 + init_stock: 950 + price: 144 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU866: + cost: 154 + init_stock: 4400 + price: 172 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU867: + cost: 449 + init_stock: 5000 + price: 499 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU868: + cost: 36 + init_stock: 2500 + price: 41 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU869: + cost: 87 + init_stock: 4850 + price: 97 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU87: + cost: 331 + init_stock: 3450 + price: 368 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU870: + cost: 252 + init_stock: 3350 + price: 281 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU871: + cost: 90 + init_stock: 4950 + price: 101 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU872: + cost: 119 + init_stock: 1600 + price: 133 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU873: + cost: 380 + init_stock: 1550 + price: 423 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU874: + cost: 422 + init_stock: 3000 + price: 469 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU875: + cost: 45 + init_stock: 1150 + price: 51 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU876: + cost: 272 + init_stock: 3050 + price: 303 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU877: + cost: 36 + init_stock: 1750 + price: 40 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU878: + cost: 24 + init_stock: 3400 + price: 27 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU879: + cost: 135 + init_stock: 5000 + price: 150 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU88: + cost: 423 + init_stock: 600 + price: 471 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU880: + cost: 389 + init_stock: 1550 + price: 433 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU881: + cost: 387 + init_stock: 3500 + price: 431 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU882: + cost: 174 + init_stock: 800 + price: 194 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU883: + cost: 255 + init_stock: 1900 + price: 284 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU884: + cost: 125 + init_stock: 1950 + price: 139 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU885: + cost: 44 + init_stock: 1350 + price: 49 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU886: + cost: 334 + init_stock: 3650 + price: 372 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU887: + cost: 239 + init_stock: 4350 + price: 266 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU888: + cost: 128 + init_stock: 450 + price: 143 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU889: + cost: 90 + init_stock: 1050 + price: 101 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU89: + cost: 124 + init_stock: 4650 + price: 138 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU890: + cost: 144 + init_stock: 5000 + price: 161 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU891: + cost: 320 + init_stock: 1100 + price: 356 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU892: + cost: 281 + init_stock: 4600 + price: 313 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU893: + cost: 206 + init_stock: 4950 + price: 229 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU894: + cost: 116 + init_stock: 3700 + price: 129 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU895: + cost: 207 + init_stock: 650 + price: 230 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU896: + cost: 260 + init_stock: 4000 + price: 289 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU897: + cost: 353 + init_stock: 3600 + price: 393 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU898: + cost: 429 + init_stock: 550 + price: 477 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU899: + cost: 209 + init_stock: 2400 + price: 233 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU9: + cost: 149 + init_stock: 4800 + price: 166 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU90: + cost: 408 + init_stock: 2550 + price: 454 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU900: + cost: 142 + init_stock: 2750 + price: 158 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU901: + cost: 193 + init_stock: 1450 + price: 215 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU902: + cost: 112 + init_stock: 4000 + price: 125 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU903: + cost: 321 + init_stock: 1900 + price: 357 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU904: + cost: 446 + init_stock: 2550 + price: 496 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU905: + cost: 224 + init_stock: 1550 + price: 249 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU906: + cost: 149 + init_stock: 2600 + price: 166 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU907: + cost: 19 + init_stock: 4150 + price: 22 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU908: + cost: 367 + init_stock: 3900 + price: 408 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU909: + cost: 433 + init_stock: 4800 + price: 482 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU91: + cost: 272 + init_stock: 800 + price: 303 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU910: + cost: 203 + init_stock: 1650 + price: 226 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU911: + cost: 414 + init_stock: 3500 + price: 461 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU912: + cost: 212 + init_stock: 1350 + price: 236 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU913: + cost: 289 + init_stock: 2300 + price: 322 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU914: + cost: 244 + init_stock: 3100 + price: 272 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU915: + cost: 303 + init_stock: 2800 + price: 337 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU916: + cost: 303 + init_stock: 4450 + price: 337 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU917: + cost: 232 + init_stock: 1500 + price: 258 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU918: + cost: 133 + init_stock: 2750 + price: 148 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU919: + cost: 353 + init_stock: 1250 + price: 393 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU92: + cost: 235 + init_stock: 3150 + price: 262 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU920: + cost: 321 + init_stock: 4300 + price: 357 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU921: + cost: 204 + init_stock: 3300 + price: 227 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU922: + cost: 100 + init_stock: 3350 + price: 112 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU923: + cost: 446 + init_stock: 2900 + price: 496 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU924: + cost: 284 + init_stock: 4250 + price: 316 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU925: + cost: 324 + init_stock: 750 + price: 360 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU926: + cost: 324 + init_stock: 3350 + price: 360 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU927: + cost: 234 + init_stock: 1050 + price: 260 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU928: + cost: 441 + init_stock: 4150 + price: 491 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU929: + cost: 323 + init_stock: 5000 + price: 359 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU93: + cost: 363 + init_stock: 3350 + price: 404 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU930: + cost: 178 + init_stock: 1400 + price: 198 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU931: + cost: 63 + init_stock: 700 + price: 71 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU932: + cost: 146 + init_stock: 3300 + price: 163 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU933: + cost: 101 + init_stock: 3900 + price: 113 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU934: + cost: 197 + init_stock: 3350 + price: 219 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU935: + cost: 327 + init_stock: 4700 + price: 364 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU936: + cost: 21 + init_stock: 250 + price: 24 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU937: + cost: 121 + init_stock: 850 + price: 135 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU938: + cost: 388 + init_stock: 1050 + price: 432 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU939: + cost: 155 + init_stock: 2950 + price: 173 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU94: + cost: 165 + init_stock: 2350 + price: 184 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU940: + cost: 12 + init_stock: 4650 + price: 14 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU941: + cost: 72 + init_stock: 2850 + price: 80 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU942: + cost: 181 + init_stock: 650 + price: 202 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU943: + cost: 124 + init_stock: 2450 + price: 138 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU944: + cost: 176 + init_stock: 2200 + price: 196 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU945: + cost: 126 + init_stock: 850 + price: 141 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU946: + cost: 292 + init_stock: 2450 + price: 325 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU947: + cost: 304 + init_stock: 4550 + price: 338 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU948: + cost: 382 + init_stock: 1400 + price: 425 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU949: + cost: 278 + init_stock: 250 + price: 309 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU95: + cost: 122 + init_stock: 1050 + price: 136 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU950: + cost: 91 + init_stock: 2700 + price: 102 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU951: + cost: 67 + init_stock: 900 + price: 75 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU952: + cost: 140 + init_stock: 550 + price: 156 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU953: + cost: 124 + init_stock: 1750 + price: 138 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU954: + cost: 266 + init_stock: 4300 + price: 296 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU955: + cost: 49 + init_stock: 3150 + price: 55 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU956: + cost: 253 + init_stock: 4250 + price: 282 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU957: + cost: 274 + init_stock: 2550 + price: 305 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU958: + cost: 332 + init_stock: 3300 + price: 369 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU959: + cost: 72 + init_stock: 4100 + price: 81 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU96: + cost: 158 + init_stock: 2200 + price: 176 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU960: + cost: 132 + init_stock: 300 + price: 147 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU961: + cost: 237 + init_stock: 2200 + price: 264 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU962: + cost: 318 + init_stock: 2200 + price: 354 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU963: + cost: 314 + init_stock: 1050 + price: 349 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU964: + cost: 219 + init_stock: 3650 + price: 244 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU965: + cost: 111 + init_stock: 2550 + price: 124 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU966: + cost: 271 + init_stock: 2200 + price: 302 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU967: + cost: 60 + init_stock: 2250 + price: 67 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU968: + cost: 252 + init_stock: 2450 + price: 281 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU969: + cost: 224 + init_stock: 300 + price: 249 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU97: + cost: 25 + init_stock: 1500 + price: 28 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU970: + cost: 219 + init_stock: 250 + price: 244 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU971: + cost: 331 + init_stock: 3650 + price: 368 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU972: + cost: 188 + init_stock: 3450 + price: 209 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU973: + cost: 243 + init_stock: 550 + price: 271 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU974: + cost: 153 + init_stock: 1600 + price: 170 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU975: + cost: 178 + init_stock: 1100 + price: 198 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU976: + cost: 160 + init_stock: 750 + price: 178 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU977: + cost: 210 + init_stock: 2700 + price: 234 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU978: + cost: 423 + init_stock: 3200 + price: 470 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU979: + cost: 398 + init_stock: 4800 + price: 443 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU98: + cost: 398 + init_stock: 1750 + price: 443 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU980: + cost: 35 + init_stock: 3400 + price: 39 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU981: + cost: 433 + init_stock: 3600 + price: 482 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU982: + cost: 191 + init_stock: 2600 + price: 213 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU983: + cost: 404 + init_stock: 4600 + price: 449 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU984: + cost: 208 + init_stock: 4550 + price: 232 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU985: + cost: 261 + init_stock: 2550 + price: 290 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU986: + cost: 247 + init_stock: 850 + price: 275 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU987: + cost: 390 + init_stock: 1700 + price: 434 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU988: + cost: 91 + init_stock: 1150 + price: 102 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU989: + cost: 435 + init_stock: 4650 + price: 484 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU99: + cost: 324 + init_stock: 2250 + price: 361 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU990: + cost: 97 + init_stock: 2850 + price: 108 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU991: + cost: 368 + init_stock: 950 + price: 409 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU992: + cost: 390 + init_stock: 4650 + price: 434 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU993: + cost: 130 + init_stock: 4300 + price: 145 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU994: + cost: 423 + init_stock: 2500 + price: 470 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU995: + cost: 216 + init_stock: 3800 + price: 241 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU996: + cost: 234 + init_stock: 3650 + price: 260 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU997: + cost: 360 + init_stock: 1700 + price: 400 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU998: + cost: 402 + init_stock: 2750 + price: 447 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU999: + cost: 71 + init_stock: 1150 + price: 79 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5324500 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 500 + definition_ref: WarehouseFacility + name: WAREHOUSE0 + skus: + SKU0: + cost: 322 + init_stock: 1260 + price: 322 + service_level: 0.96 + vlt: 1 + SKU1: + cost: 284 + init_stock: 440 + price: 284 + service_level: 0.96 + vlt: 3 + SKU10: + cost: 68 + init_stock: 500 + price: 68 + service_level: 0.96 + vlt: 3 + SKU100: + cost: 16 + init_stock: 1300 + price: 16 + service_level: 0.96 + vlt: 3 + SKU101: + cost: 84 + init_stock: 1420 + price: 84 + service_level: 0.96 + vlt: 3 + SKU102: + cost: 328 + init_stock: 1860 + price: 328 + service_level: 0.96 + vlt: 2 + SKU103: + cost: 334 + init_stock: 1900 + price: 334 + service_level: 0.96 + vlt: 1 + SKU104: + cost: 49 + init_stock: 1300 + price: 49 + service_level: 0.96 + vlt: 3 + SKU105: + cost: 110 + init_stock: 1140 + price: 110 + service_level: 0.96 + vlt: 2 + SKU106: + cost: 251 + init_stock: 1460 + price: 251 + service_level: 0.96 + vlt: 3 + SKU107: + cost: 423 + init_stock: 1740 + price: 423 + service_level: 0.96 + vlt: 3 + SKU108: + cost: 458 + init_stock: 1840 + price: 458 + service_level: 0.96 + vlt: 3 + SKU109: + cost: 88 + init_stock: 1640 + price: 88 + service_level: 0.96 + vlt: 2 + SKU11: + cost: 400 + init_stock: 1020 + price: 400 + service_level: 0.96 + vlt: 2 + SKU110: + cost: 66 + init_stock: 280 + price: 66 + service_level: 0.96 + vlt: 3 + SKU111: + cost: 260 + init_stock: 1220 + price: 260 + service_level: 0.96 + vlt: 1 + SKU112: + cost: 61 + init_stock: 1900 + price: 61 + service_level: 0.96 + vlt: 2 + SKU113: + cost: 348 + init_stock: 620 + price: 348 + service_level: 0.96 + vlt: 3 + SKU114: + cost: 389 + init_stock: 540 + price: 389 + service_level: 0.96 + vlt: 1 + SKU115: + cost: 286 + init_stock: 1720 + price: 286 + service_level: 0.96 + vlt: 2 + SKU116: + cost: 496 + init_stock: 1440 + price: 496 + service_level: 0.96 + vlt: 3 + SKU117: + cost: 320 + init_stock: 1840 + price: 320 + service_level: 0.96 + vlt: 3 + SKU118: + cost: 183 + init_stock: 660 + price: 183 + service_level: 0.96 + vlt: 1 + SKU119: + cost: 209 + init_stock: 640 + price: 209 + service_level: 0.96 + vlt: 3 + SKU12: + cost: 112 + init_stock: 1680 + price: 112 + service_level: 0.96 + vlt: 1 + SKU120: + cost: 121 + init_stock: 1960 + price: 121 + service_level: 0.96 + vlt: 1 + SKU121: + cost: 40 + init_stock: 1700 + price: 40 + service_level: 0.96 + vlt: 1 + SKU122: + cost: 437 + init_stock: 140 + price: 437 + service_level: 0.96 + vlt: 3 + SKU123: + cost: 233 + init_stock: 380 + price: 233 + service_level: 0.96 + vlt: 3 + SKU124: + cost: 182 + init_stock: 720 + price: 182 + service_level: 0.96 + vlt: 2 + SKU125: + cost: 16 + init_stock: 1840 + price: 16 + service_level: 0.96 + vlt: 2 + SKU126: + cost: 36 + init_stock: 780 + price: 36 + service_level: 0.96 + vlt: 3 + SKU127: + cost: 217 + init_stock: 620 + price: 217 + service_level: 0.96 + vlt: 2 + SKU128: + cost: 165 + init_stock: 380 + price: 165 + service_level: 0.96 + vlt: 1 + SKU129: + cost: 143 + init_stock: 1000 + price: 143 + service_level: 0.96 + vlt: 2 + SKU13: + cost: 317 + init_stock: 1140 + price: 317 + service_level: 0.96 + vlt: 3 + SKU130: + cost: 348 + init_stock: 1960 + price: 348 + service_level: 0.96 + vlt: 3 + SKU131: + cost: 64 + init_stock: 900 + price: 64 + service_level: 0.96 + vlt: 1 + SKU132: + cost: 427 + init_stock: 420 + price: 427 + service_level: 0.96 + vlt: 2 + SKU133: + cost: 224 + init_stock: 580 + price: 224 + service_level: 0.96 + vlt: 2 + SKU134: + cost: 336 + init_stock: 1540 + price: 336 + service_level: 0.96 + vlt: 3 + SKU135: + cost: 153 + init_stock: 2000 + price: 153 + service_level: 0.96 + vlt: 1 + SKU136: + cost: 199 + init_stock: 1420 + price: 199 + service_level: 0.96 + vlt: 3 + SKU137: + cost: 93 + init_stock: 1480 + price: 93 + service_level: 0.96 + vlt: 2 + SKU138: + cost: 228 + init_stock: 720 + price: 228 + service_level: 0.96 + vlt: 1 + SKU139: + cost: 207 + init_stock: 480 + price: 207 + service_level: 0.96 + vlt: 1 + SKU14: + cost: 268 + init_stock: 1240 + price: 268 + service_level: 0.96 + vlt: 1 + SKU140: + cost: 261 + init_stock: 680 + price: 261 + service_level: 0.96 + vlt: 3 + SKU141: + cost: 190 + init_stock: 820 + price: 190 + service_level: 0.96 + vlt: 1 + SKU142: + cost: 320 + init_stock: 760 + price: 320 + service_level: 0.96 + vlt: 3 + SKU143: + cost: 318 + init_stock: 520 + price: 318 + service_level: 0.96 + vlt: 3 + SKU144: + cost: 400 + init_stock: 240 + price: 400 + service_level: 0.96 + vlt: 1 + SKU145: + cost: 399 + init_stock: 1640 + price: 399 + service_level: 0.96 + vlt: 1 + SKU146: + cost: 177 + init_stock: 960 + price: 177 + service_level: 0.96 + vlt: 3 + SKU147: + cost: 472 + init_stock: 1120 + price: 472 + service_level: 0.96 + vlt: 3 + SKU148: + cost: 313 + init_stock: 1540 + price: 313 + service_level: 0.96 + vlt: 3 + SKU149: + cost: 357 + init_stock: 1540 + price: 357 + service_level: 0.96 + vlt: 3 + SKU15: + cost: 52 + init_stock: 100 + price: 52 + service_level: 0.96 + vlt: 2 + SKU150: + cost: 106 + init_stock: 1400 + price: 106 + service_level: 0.96 + vlt: 2 + SKU151: + cost: 223 + init_stock: 1460 + price: 223 + service_level: 0.96 + vlt: 1 + SKU152: + cost: 10 + init_stock: 1020 + price: 10 + service_level: 0.96 + vlt: 3 + SKU153: + cost: 441 + init_stock: 1240 + price: 441 + service_level: 0.96 + vlt: 1 + SKU154: + cost: 77 + init_stock: 1700 + price: 77 + service_level: 0.96 + vlt: 3 + SKU155: + cost: 422 + init_stock: 1060 + price: 422 + service_level: 0.96 + vlt: 1 + SKU156: + cost: 10 + init_stock: 240 + price: 10 + service_level: 0.96 + vlt: 1 + SKU157: + cost: 410 + init_stock: 1500 + price: 410 + service_level: 0.96 + vlt: 2 + SKU158: + cost: 145 + init_stock: 1620 + price: 145 + service_level: 0.96 + vlt: 3 + SKU159: + cost: 193 + init_stock: 500 + price: 193 + service_level: 0.96 + vlt: 2 + SKU16: + cost: 175 + init_stock: 1160 + price: 175 + service_level: 0.96 + vlt: 3 + SKU160: + cost: 459 + init_stock: 1700 + price: 459 + service_level: 0.96 + vlt: 1 + SKU161: + cost: 239 + init_stock: 920 + price: 239 + service_level: 0.96 + vlt: 2 + SKU162: + cost: 158 + init_stock: 100 + price: 158 + service_level: 0.96 + vlt: 2 + SKU163: + cost: 486 + init_stock: 780 + price: 486 + service_level: 0.96 + vlt: 2 + SKU164: + cost: 496 + init_stock: 2000 + price: 496 + service_level: 0.96 + vlt: 1 + SKU165: + cost: 274 + init_stock: 660 + price: 274 + service_level: 0.96 + vlt: 3 + SKU166: + cost: 79 + init_stock: 1780 + price: 79 + service_level: 0.96 + vlt: 1 + SKU167: + cost: 443 + init_stock: 260 + price: 443 + service_level: 0.96 + vlt: 1 + SKU168: + cost: 357 + init_stock: 1740 + price: 357 + service_level: 0.96 + vlt: 2 + SKU169: + cost: 369 + init_stock: 1960 + price: 369 + service_level: 0.96 + vlt: 3 + SKU17: + cost: 346 + init_stock: 180 + price: 346 + service_level: 0.96 + vlt: 1 + SKU170: + cost: 68 + init_stock: 1100 + price: 68 + service_level: 0.96 + vlt: 2 + SKU171: + cost: 398 + init_stock: 1520 + price: 398 + service_level: 0.96 + vlt: 1 + SKU172: + cost: 200 + init_stock: 1420 + price: 200 + service_level: 0.96 + vlt: 2 + SKU173: + cost: 175 + init_stock: 1920 + price: 175 + service_level: 0.96 + vlt: 3 + SKU174: + cost: 291 + init_stock: 1520 + price: 291 + service_level: 0.96 + vlt: 2 + SKU175: + cost: 84 + init_stock: 1500 + price: 84 + service_level: 0.96 + vlt: 2 + SKU176: + cost: 407 + init_stock: 1320 + price: 407 + service_level: 0.96 + vlt: 1 + SKU177: + cost: 257 + init_stock: 620 + price: 257 + service_level: 0.96 + vlt: 1 + SKU178: + cost: 423 + init_stock: 100 + price: 423 + service_level: 0.96 + vlt: 2 + SKU179: + cost: 497 + init_stock: 1660 + price: 497 + service_level: 0.96 + vlt: 1 + SKU18: + cost: 258 + init_stock: 1620 + price: 258 + service_level: 0.96 + vlt: 1 + SKU180: + cost: 217 + init_stock: 1100 + price: 217 + service_level: 0.96 + vlt: 2 + SKU181: + cost: 143 + init_stock: 1200 + price: 143 + service_level: 0.96 + vlt: 1 + SKU182: + cost: 437 + init_stock: 1980 + price: 437 + service_level: 0.96 + vlt: 3 + SKU183: + cost: 145 + init_stock: 160 + price: 145 + service_level: 0.96 + vlt: 3 + SKU184: + cost: 73 + init_stock: 480 + price: 73 + service_level: 0.96 + vlt: 2 + SKU185: + cost: 10 + init_stock: 1800 + price: 10 + service_level: 0.96 + vlt: 2 + SKU186: + cost: 359 + init_stock: 440 + price: 359 + service_level: 0.96 + vlt: 1 + SKU187: + cost: 177 + init_stock: 600 + price: 177 + service_level: 0.96 + vlt: 3 + SKU188: + cost: 391 + init_stock: 1740 + price: 391 + service_level: 0.96 + vlt: 3 + SKU189: + cost: 358 + init_stock: 700 + price: 358 + service_level: 0.96 + vlt: 2 + SKU19: + cost: 477 + init_stock: 1820 + price: 477 + service_level: 0.96 + vlt: 3 + SKU190: + cost: 113 + init_stock: 340 + price: 113 + service_level: 0.96 + vlt: 3 + SKU191: + cost: 473 + init_stock: 1080 + price: 473 + service_level: 0.96 + vlt: 2 + SKU192: + cost: 415 + init_stock: 1220 + price: 415 + service_level: 0.96 + vlt: 2 + SKU193: + cost: 207 + init_stock: 600 + price: 207 + service_level: 0.96 + vlt: 2 + SKU194: + cost: 432 + init_stock: 100 + price: 432 + service_level: 0.96 + vlt: 2 + SKU195: + cost: 218 + init_stock: 620 + price: 218 + service_level: 0.96 + vlt: 2 + SKU196: + cost: 49 + init_stock: 1360 + price: 49 + service_level: 0.96 + vlt: 3 + SKU197: + cost: 303 + init_stock: 1140 + price: 303 + service_level: 0.96 + vlt: 1 + SKU198: + cost: 169 + init_stock: 1080 + price: 169 + service_level: 0.96 + vlt: 2 + SKU199: + cost: 449 + init_stock: 460 + price: 449 + service_level: 0.96 + vlt: 1 + SKU2: + cost: 331 + init_stock: 1400 + price: 331 + service_level: 0.96 + vlt: 3 + SKU20: + cost: 335 + init_stock: 500 + price: 335 + service_level: 0.96 + vlt: 3 + SKU200: + cost: 65 + init_stock: 500 + price: 65 + service_level: 0.96 + vlt: 1 + SKU201: + cost: 104 + init_stock: 1180 + price: 104 + service_level: 0.96 + vlt: 1 + SKU202: + cost: 142 + init_stock: 1460 + price: 142 + service_level: 0.96 + vlt: 1 + SKU203: + cost: 440 + init_stock: 1640 + price: 440 + service_level: 0.96 + vlt: 2 + SKU204: + cost: 489 + init_stock: 940 + price: 489 + service_level: 0.96 + vlt: 2 + SKU205: + cost: 130 + init_stock: 2000 + price: 130 + service_level: 0.96 + vlt: 3 + SKU206: + cost: 335 + init_stock: 220 + price: 335 + service_level: 0.96 + vlt: 2 + SKU207: + cost: 140 + init_stock: 1600 + price: 140 + service_level: 0.96 + vlt: 1 + SKU208: + cost: 491 + init_stock: 1540 + price: 491 + service_level: 0.96 + vlt: 1 + SKU209: + cost: 179 + init_stock: 400 + price: 179 + service_level: 0.96 + vlt: 3 + SKU21: + cost: 123 + init_stock: 2000 + price: 123 + service_level: 0.96 + vlt: 2 + SKU210: + cost: 404 + init_stock: 1380 + price: 404 + service_level: 0.96 + vlt: 3 + SKU211: + cost: 174 + init_stock: 1820 + price: 174 + service_level: 0.96 + vlt: 2 + SKU212: + cost: 405 + init_stock: 1580 + price: 405 + service_level: 0.96 + vlt: 3 + SKU213: + cost: 121 + init_stock: 1280 + price: 121 + service_level: 0.96 + vlt: 2 + SKU214: + cost: 101 + init_stock: 200 + price: 101 + service_level: 0.96 + vlt: 2 + SKU215: + cost: 419 + init_stock: 940 + price: 419 + service_level: 0.96 + vlt: 1 + SKU216: + cost: 330 + init_stock: 460 + price: 330 + service_level: 0.96 + vlt: 1 + SKU217: + cost: 284 + init_stock: 1300 + price: 284 + service_level: 0.96 + vlt: 2 + SKU218: + cost: 205 + init_stock: 1180 + price: 205 + service_level: 0.96 + vlt: 1 + SKU219: + cost: 92 + init_stock: 920 + price: 92 + service_level: 0.96 + vlt: 3 + SKU22: + cost: 493 + init_stock: 1320 + price: 493 + service_level: 0.96 + vlt: 1 + SKU220: + cost: 387 + init_stock: 1740 + price: 387 + service_level: 0.96 + vlt: 2 + SKU221: + cost: 39 + init_stock: 1560 + price: 39 + service_level: 0.96 + vlt: 2 + SKU222: + cost: 115 + init_stock: 720 + price: 115 + service_level: 0.96 + vlt: 2 + SKU223: + cost: 196 + init_stock: 240 + price: 196 + service_level: 0.96 + vlt: 2 + SKU224: + cost: 387 + init_stock: 100 + price: 387 + service_level: 0.96 + vlt: 2 + SKU225: + cost: 164 + init_stock: 580 + price: 164 + service_level: 0.96 + vlt: 1 + SKU226: + cost: 206 + init_stock: 260 + price: 206 + service_level: 0.96 + vlt: 2 + SKU227: + cost: 479 + init_stock: 480 + price: 479 + service_level: 0.96 + vlt: 3 + SKU228: + cost: 14 + init_stock: 1800 + price: 14 + service_level: 0.96 + vlt: 1 + SKU229: + cost: 472 + init_stock: 880 + price: 472 + service_level: 0.96 + vlt: 1 + SKU23: + cost: 387 + init_stock: 840 + price: 387 + service_level: 0.96 + vlt: 1 + SKU230: + cost: 241 + init_stock: 460 + price: 241 + service_level: 0.96 + vlt: 3 + SKU231: + cost: 48 + init_stock: 1620 + price: 48 + service_level: 0.96 + vlt: 3 + SKU232: + cost: 224 + init_stock: 1920 + price: 224 + service_level: 0.96 + vlt: 1 + SKU233: + cost: 360 + init_stock: 1500 + price: 360 + service_level: 0.96 + vlt: 2 + SKU234: + cost: 287 + init_stock: 100 + price: 287 + service_level: 0.96 + vlt: 1 + SKU235: + cost: 24 + init_stock: 1140 + price: 24 + service_level: 0.96 + vlt: 3 + SKU236: + cost: 155 + init_stock: 1100 + price: 155 + service_level: 0.96 + vlt: 2 + SKU237: + cost: 433 + init_stock: 900 + price: 433 + service_level: 0.96 + vlt: 3 + SKU238: + cost: 64 + init_stock: 1320 + price: 64 + service_level: 0.96 + vlt: 3 + SKU239: + cost: 103 + init_stock: 1960 + price: 103 + service_level: 0.96 + vlt: 1 + SKU24: + cost: 97 + init_stock: 940 + price: 97 + service_level: 0.96 + vlt: 2 + SKU240: + cost: 373 + init_stock: 940 + price: 373 + service_level: 0.96 + vlt: 2 + SKU241: + cost: 439 + init_stock: 1420 + price: 439 + service_level: 0.96 + vlt: 2 + SKU242: + cost: 17 + init_stock: 880 + price: 17 + service_level: 0.96 + vlt: 1 + SKU243: + cost: 352 + init_stock: 280 + price: 352 + service_level: 0.96 + vlt: 1 + SKU244: + cost: 174 + init_stock: 1640 + price: 174 + service_level: 0.96 + vlt: 2 + SKU245: + cost: 404 + init_stock: 1320 + price: 404 + service_level: 0.96 + vlt: 2 + SKU246: + cost: 300 + init_stock: 600 + price: 300 + service_level: 0.96 + vlt: 2 + SKU247: + cost: 386 + init_stock: 700 + price: 386 + service_level: 0.96 + vlt: 2 + SKU248: + cost: 355 + init_stock: 580 + price: 355 + service_level: 0.96 + vlt: 3 + SKU249: + cost: 356 + init_stock: 500 + price: 356 + service_level: 0.96 + vlt: 1 + SKU25: + cost: 161 + init_stock: 100 + price: 161 + service_level: 0.96 + vlt: 2 + SKU250: + cost: 127 + init_stock: 1080 + price: 127 + service_level: 0.96 + vlt: 3 + SKU251: + cost: 344 + init_stock: 1220 + price: 344 + service_level: 0.96 + vlt: 1 + SKU252: + cost: 181 + init_stock: 1660 + price: 181 + service_level: 0.96 + vlt: 1 + SKU253: + cost: 448 + init_stock: 320 + price: 448 + service_level: 0.96 + vlt: 1 + SKU254: + cost: 484 + init_stock: 920 + price: 484 + service_level: 0.96 + vlt: 3 + SKU255: + cost: 290 + init_stock: 1340 + price: 290 + service_level: 0.96 + vlt: 2 + SKU256: + cost: 91 + init_stock: 1440 + price: 91 + service_level: 0.96 + vlt: 3 + SKU257: + cost: 348 + init_stock: 1140 + price: 348 + service_level: 0.96 + vlt: 1 + SKU258: + cost: 489 + init_stock: 860 + price: 489 + service_level: 0.96 + vlt: 1 + SKU259: + cost: 333 + init_stock: 1380 + price: 333 + service_level: 0.96 + vlt: 1 + SKU26: + cost: 229 + init_stock: 1260 + price: 229 + service_level: 0.96 + vlt: 1 + SKU260: + cost: 487 + init_stock: 1040 + price: 487 + service_level: 0.96 + vlt: 3 + SKU261: + cost: 368 + init_stock: 440 + price: 368 + service_level: 0.96 + vlt: 1 + SKU262: + cost: 332 + init_stock: 1560 + price: 332 + service_level: 0.96 + vlt: 3 + SKU263: + cost: 189 + init_stock: 1480 + price: 189 + service_level: 0.96 + vlt: 3 + SKU264: + cost: 361 + init_stock: 1580 + price: 361 + service_level: 0.96 + vlt: 1 + SKU265: + cost: 286 + init_stock: 1180 + price: 286 + service_level: 0.96 + vlt: 3 + SKU266: + cost: 128 + init_stock: 940 + price: 128 + service_level: 0.96 + vlt: 2 + SKU267: + cost: 77 + init_stock: 1600 + price: 77 + service_level: 0.96 + vlt: 1 + SKU268: + cost: 221 + init_stock: 1780 + price: 221 + service_level: 0.96 + vlt: 2 + SKU269: + cost: 126 + init_stock: 880 + price: 126 + service_level: 0.96 + vlt: 2 + SKU27: + cost: 370 + init_stock: 1120 + price: 370 + service_level: 0.96 + vlt: 3 + SKU270: + cost: 182 + init_stock: 1480 + price: 182 + service_level: 0.96 + vlt: 3 + SKU271: + cost: 230 + init_stock: 360 + price: 230 + service_level: 0.96 + vlt: 1 + SKU272: + cost: 366 + init_stock: 340 + price: 366 + service_level: 0.96 + vlt: 2 + SKU273: + cost: 421 + init_stock: 360 + price: 421 + service_level: 0.96 + vlt: 2 + SKU274: + cost: 29 + init_stock: 1540 + price: 29 + service_level: 0.96 + vlt: 2 + SKU275: + cost: 50 + init_stock: 960 + price: 50 + service_level: 0.96 + vlt: 1 + SKU276: + cost: 163 + init_stock: 1080 + price: 163 + service_level: 0.96 + vlt: 3 + SKU277: + cost: 449 + init_stock: 820 + price: 449 + service_level: 0.96 + vlt: 1 + SKU278: + cost: 105 + init_stock: 240 + price: 105 + service_level: 0.96 + vlt: 3 + SKU279: + cost: 51 + init_stock: 780 + price: 51 + service_level: 0.96 + vlt: 2 + SKU28: + cost: 208 + init_stock: 940 + price: 208 + service_level: 0.96 + vlt: 2 + SKU280: + cost: 295 + init_stock: 660 + price: 295 + service_level: 0.96 + vlt: 2 + SKU281: + cost: 395 + init_stock: 1500 + price: 395 + service_level: 0.96 + vlt: 1 + SKU282: + cost: 63 + init_stock: 920 + price: 63 + service_level: 0.96 + vlt: 2 + SKU283: + cost: 392 + init_stock: 1840 + price: 392 + service_level: 0.96 + vlt: 3 + SKU284: + cost: 344 + init_stock: 1340 + price: 344 + service_level: 0.96 + vlt: 3 + SKU285: + cost: 133 + init_stock: 1820 + price: 133 + service_level: 0.96 + vlt: 2 + SKU286: + cost: 337 + init_stock: 780 + price: 337 + service_level: 0.96 + vlt: 1 + SKU287: + cost: 375 + init_stock: 1120 + price: 375 + service_level: 0.96 + vlt: 3 + SKU288: + cost: 181 + init_stock: 760 + price: 181 + service_level: 0.96 + vlt: 1 + SKU289: + cost: 67 + init_stock: 620 + price: 67 + service_level: 0.96 + vlt: 1 + SKU29: + cost: 245 + init_stock: 1160 + price: 245 + service_level: 0.96 + vlt: 1 + SKU290: + cost: 438 + init_stock: 1340 + price: 438 + service_level: 0.96 + vlt: 1 + SKU291: + cost: 94 + init_stock: 1220 + price: 94 + service_level: 0.96 + vlt: 1 + SKU292: + cost: 186 + init_stock: 100 + price: 186 + service_level: 0.96 + vlt: 1 + SKU293: + cost: 162 + init_stock: 1100 + price: 162 + service_level: 0.96 + vlt: 2 + SKU294: + cost: 409 + init_stock: 180 + price: 409 + service_level: 0.96 + vlt: 2 + SKU295: + cost: 435 + init_stock: 1860 + price: 435 + service_level: 0.96 + vlt: 3 + SKU296: + cost: 370 + init_stock: 1840 + price: 370 + service_level: 0.96 + vlt: 3 + SKU297: + cost: 298 + init_stock: 760 + price: 298 + service_level: 0.96 + vlt: 1 + SKU298: + cost: 286 + init_stock: 700 + price: 286 + service_level: 0.96 + vlt: 2 + SKU299: + cost: 367 + init_stock: 1020 + price: 367 + service_level: 0.96 + vlt: 3 + SKU3: + cost: 405 + init_stock: 240 + price: 405 + service_level: 0.96 + vlt: 3 + SKU30: + cost: 359 + init_stock: 1380 + price: 359 + service_level: 0.96 + vlt: 2 + SKU300: + cost: 376 + init_stock: 1160 + price: 376 + service_level: 0.96 + vlt: 1 + SKU301: + cost: 433 + init_stock: 1660 + price: 433 + service_level: 0.96 + vlt: 2 + SKU302: + cost: 184 + init_stock: 220 + price: 184 + service_level: 0.96 + vlt: 2 + SKU303: + cost: 246 + init_stock: 1880 + price: 246 + service_level: 0.96 + vlt: 1 + SKU304: + cost: 419 + init_stock: 460 + price: 419 + service_level: 0.96 + vlt: 3 + SKU305: + cost: 495 + init_stock: 2000 + price: 495 + service_level: 0.96 + vlt: 1 + SKU306: + cost: 479 + init_stock: 840 + price: 479 + service_level: 0.96 + vlt: 2 + SKU307: + cost: 210 + init_stock: 1560 + price: 210 + service_level: 0.96 + vlt: 1 + SKU308: + cost: 208 + init_stock: 100 + price: 208 + service_level: 0.96 + vlt: 2 + SKU309: + cost: 101 + init_stock: 1840 + price: 101 + service_level: 0.96 + vlt: 2 + SKU31: + cost: 69 + init_stock: 1120 + price: 69 + service_level: 0.96 + vlt: 3 + SKU310: + cost: 234 + init_stock: 880 + price: 234 + service_level: 0.96 + vlt: 3 + SKU311: + cost: 306 + init_stock: 1600 + price: 306 + service_level: 0.96 + vlt: 3 + SKU312: + cost: 291 + init_stock: 500 + price: 291 + service_level: 0.96 + vlt: 1 + SKU313: + cost: 324 + init_stock: 760 + price: 324 + service_level: 0.96 + vlt: 1 + SKU314: + cost: 404 + init_stock: 580 + price: 404 + service_level: 0.96 + vlt: 1 + SKU315: + cost: 471 + init_stock: 1680 + price: 471 + service_level: 0.96 + vlt: 2 + SKU316: + cost: 202 + init_stock: 360 + price: 202 + service_level: 0.96 + vlt: 1 + SKU317: + cost: 216 + init_stock: 480 + price: 216 + service_level: 0.96 + vlt: 2 + SKU318: + cost: 278 + init_stock: 1700 + price: 278 + service_level: 0.96 + vlt: 1 + SKU319: + cost: 257 + init_stock: 1060 + price: 257 + service_level: 0.96 + vlt: 3 + SKU32: + cost: 336 + init_stock: 440 + price: 336 + service_level: 0.96 + vlt: 1 + SKU320: + cost: 196 + init_stock: 780 + price: 196 + service_level: 0.96 + vlt: 3 + SKU321: + cost: 67 + init_stock: 320 + price: 67 + service_level: 0.96 + vlt: 3 + SKU322: + cost: 240 + init_stock: 2000 + price: 240 + service_level: 0.96 + vlt: 1 + SKU323: + cost: 66 + init_stock: 780 + price: 66 + service_level: 0.96 + vlt: 2 + SKU324: + cost: 442 + init_stock: 1860 + price: 442 + service_level: 0.96 + vlt: 3 + SKU325: + cost: 453 + init_stock: 1380 + price: 453 + service_level: 0.96 + vlt: 2 + SKU326: + cost: 345 + init_stock: 480 + price: 345 + service_level: 0.96 + vlt: 2 + SKU327: + cost: 457 + init_stock: 280 + price: 457 + service_level: 0.96 + vlt: 2 + SKU328: + cost: 390 + init_stock: 900 + price: 390 + service_level: 0.96 + vlt: 1 + SKU329: + cost: 67 + init_stock: 840 + price: 67 + service_level: 0.96 + vlt: 1 + SKU33: + cost: 416 + init_stock: 1840 + price: 416 + service_level: 0.96 + vlt: 2 + SKU330: + cost: 306 + init_stock: 780 + price: 306 + service_level: 0.96 + vlt: 2 + SKU331: + cost: 138 + init_stock: 820 + price: 138 + service_level: 0.96 + vlt: 3 + SKU332: + cost: 390 + init_stock: 1920 + price: 390 + service_level: 0.96 + vlt: 2 + SKU333: + cost: 485 + init_stock: 1060 + price: 485 + service_level: 0.96 + vlt: 2 + SKU334: + cost: 183 + init_stock: 1140 + price: 183 + service_level: 0.96 + vlt: 1 + SKU335: + cost: 80 + init_stock: 1620 + price: 80 + service_level: 0.96 + vlt: 1 + SKU336: + cost: 296 + init_stock: 560 + price: 296 + service_level: 0.96 + vlt: 1 + SKU337: + cost: 245 + init_stock: 580 + price: 245 + service_level: 0.96 + vlt: 2 + SKU338: + cost: 87 + init_stock: 1620 + price: 87 + service_level: 0.96 + vlt: 3 + SKU339: + cost: 265 + init_stock: 1260 + price: 265 + service_level: 0.96 + vlt: 2 + SKU34: + cost: 95 + init_stock: 260 + price: 95 + service_level: 0.96 + vlt: 3 + SKU340: + cost: 480 + init_stock: 1740 + price: 480 + service_level: 0.96 + vlt: 2 + SKU341: + cost: 462 + init_stock: 1400 + price: 462 + service_level: 0.96 + vlt: 1 + SKU342: + cost: 144 + init_stock: 180 + price: 144 + service_level: 0.96 + vlt: 3 + SKU343: + cost: 310 + init_stock: 300 + price: 310 + service_level: 0.96 + vlt: 2 + SKU344: + cost: 357 + init_stock: 1740 + price: 357 + service_level: 0.96 + vlt: 2 + SKU345: + cost: 442 + init_stock: 1780 + price: 442 + service_level: 0.96 + vlt: 2 + SKU346: + cost: 350 + init_stock: 840 + price: 350 + service_level: 0.96 + vlt: 3 + SKU347: + cost: 497 + init_stock: 1640 + price: 497 + service_level: 0.96 + vlt: 1 + SKU348: + cost: 147 + init_stock: 400 + price: 147 + service_level: 0.96 + vlt: 1 + SKU349: + cost: 67 + init_stock: 1340 + price: 67 + service_level: 0.96 + vlt: 3 + SKU35: + cost: 267 + init_stock: 1720 + price: 267 + service_level: 0.96 + vlt: 3 + SKU350: + cost: 279 + init_stock: 1840 + price: 279 + service_level: 0.96 + vlt: 3 + SKU351: + cost: 155 + init_stock: 1340 + price: 155 + service_level: 0.96 + vlt: 1 + SKU352: + cost: 71 + init_stock: 360 + price: 71 + service_level: 0.96 + vlt: 2 + SKU353: + cost: 253 + init_stock: 1860 + price: 253 + service_level: 0.96 + vlt: 2 + SKU354: + cost: 396 + init_stock: 1580 + price: 396 + service_level: 0.96 + vlt: 3 + SKU355: + cost: 63 + init_stock: 200 + price: 63 + service_level: 0.96 + vlt: 1 + SKU356: + cost: 348 + init_stock: 580 + price: 348 + service_level: 0.96 + vlt: 1 + SKU357: + cost: 499 + init_stock: 1840 + price: 499 + service_level: 0.96 + vlt: 3 + SKU358: + cost: 269 + init_stock: 1380 + price: 269 + service_level: 0.96 + vlt: 2 + SKU359: + cost: 82 + init_stock: 1500 + price: 82 + service_level: 0.96 + vlt: 3 + SKU36: + cost: 105 + init_stock: 200 + price: 105 + service_level: 0.96 + vlt: 2 + SKU360: + cost: 484 + init_stock: 500 + price: 484 + service_level: 0.96 + vlt: 1 + SKU361: + cost: 163 + init_stock: 1800 + price: 163 + service_level: 0.96 + vlt: 1 + SKU362: + cost: 464 + init_stock: 1740 + price: 464 + service_level: 0.96 + vlt: 2 + SKU363: + cost: 52 + init_stock: 1560 + price: 52 + service_level: 0.96 + vlt: 2 + SKU364: + cost: 387 + init_stock: 660 + price: 387 + service_level: 0.96 + vlt: 1 + SKU365: + cost: 158 + init_stock: 1660 + price: 158 + service_level: 0.96 + vlt: 3 + SKU366: + cost: 444 + init_stock: 1720 + price: 444 + service_level: 0.96 + vlt: 3 + SKU367: + cost: 272 + init_stock: 920 + price: 272 + service_level: 0.96 + vlt: 3 + SKU368: + cost: 472 + init_stock: 660 + price: 472 + service_level: 0.96 + vlt: 1 + SKU369: + cost: 96 + init_stock: 1500 + price: 96 + service_level: 0.96 + vlt: 2 + SKU37: + cost: 66 + init_stock: 1180 + price: 66 + service_level: 0.96 + vlt: 2 + SKU370: + cost: 112 + init_stock: 300 + price: 112 + service_level: 0.96 + vlt: 2 + SKU371: + cost: 328 + init_stock: 100 + price: 328 + service_level: 0.96 + vlt: 3 + SKU372: + cost: 67 + init_stock: 640 + price: 67 + service_level: 0.96 + vlt: 1 + SKU373: + cost: 58 + init_stock: 1340 + price: 58 + service_level: 0.96 + vlt: 3 + SKU374: + cost: 38 + init_stock: 1080 + price: 38 + service_level: 0.96 + vlt: 2 + SKU375: + cost: 283 + init_stock: 1440 + price: 283 + service_level: 0.96 + vlt: 1 + SKU376: + cost: 156 + init_stock: 1640 + price: 156 + service_level: 0.96 + vlt: 1 + SKU377: + cost: 78 + init_stock: 1340 + price: 78 + service_level: 0.96 + vlt: 3 + SKU378: + cost: 424 + init_stock: 700 + price: 424 + service_level: 0.96 + vlt: 1 + SKU379: + cost: 11 + init_stock: 980 + price: 11 + service_level: 0.96 + vlt: 1 + SKU38: + cost: 344 + init_stock: 860 + price: 344 + service_level: 0.96 + vlt: 2 + SKU380: + cost: 393 + init_stock: 1060 + price: 393 + service_level: 0.96 + vlt: 1 + SKU381: + cost: 476 + init_stock: 120 + price: 476 + service_level: 0.96 + vlt: 1 + SKU382: + cost: 125 + init_stock: 1420 + price: 125 + service_level: 0.96 + vlt: 2 + SKU383: + cost: 304 + init_stock: 1840 + price: 304 + service_level: 0.96 + vlt: 3 + SKU384: + cost: 320 + init_stock: 180 + price: 320 + service_level: 0.96 + vlt: 2 + SKU385: + cost: 120 + init_stock: 260 + price: 120 + service_level: 0.96 + vlt: 2 + SKU386: + cost: 295 + init_stock: 620 + price: 295 + service_level: 0.96 + vlt: 1 + SKU387: + cost: 112 + init_stock: 1940 + price: 112 + service_level: 0.96 + vlt: 3 + SKU388: + cost: 405 + init_stock: 880 + price: 405 + service_level: 0.96 + vlt: 2 + SKU389: + cost: 376 + init_stock: 1400 + price: 376 + service_level: 0.96 + vlt: 2 + SKU39: + cost: 25 + init_stock: 1020 + price: 25 + service_level: 0.96 + vlt: 3 + SKU390: + cost: 144 + init_stock: 780 + price: 144 + service_level: 0.96 + vlt: 2 + SKU391: + cost: 233 + init_stock: 1340 + price: 233 + service_level: 0.96 + vlt: 2 + SKU392: + cost: 163 + init_stock: 1480 + price: 163 + service_level: 0.96 + vlt: 2 + SKU393: + cost: 487 + init_stock: 1340 + price: 487 + service_level: 0.96 + vlt: 1 + SKU394: + cost: 154 + init_stock: 1060 + price: 154 + service_level: 0.96 + vlt: 2 + SKU395: + cost: 488 + init_stock: 660 + price: 488 + service_level: 0.96 + vlt: 3 + SKU396: + cost: 333 + init_stock: 1220 + price: 333 + service_level: 0.96 + vlt: 3 + SKU397: + cost: 460 + init_stock: 1020 + price: 460 + service_level: 0.96 + vlt: 1 + SKU398: + cost: 234 + init_stock: 1160 + price: 234 + service_level: 0.96 + vlt: 2 + SKU399: + cost: 376 + init_stock: 740 + price: 376 + service_level: 0.96 + vlt: 2 + SKU4: + cost: 439 + init_stock: 140 + price: 439 + service_level: 0.96 + vlt: 2 + SKU40: + cost: 490 + init_stock: 1980 + price: 490 + service_level: 0.96 + vlt: 3 + SKU400: + cost: 445 + init_stock: 1680 + price: 445 + service_level: 0.96 + vlt: 2 + SKU401: + cost: 410 + init_stock: 1560 + price: 410 + service_level: 0.96 + vlt: 1 + SKU402: + cost: 86 + init_stock: 140 + price: 86 + service_level: 0.96 + vlt: 3 + SKU403: + cost: 89 + init_stock: 1980 + price: 89 + service_level: 0.96 + vlt: 3 + SKU404: + cost: 287 + init_stock: 1220 + price: 287 + service_level: 0.96 + vlt: 1 + SKU405: + cost: 460 + init_stock: 380 + price: 460 + service_level: 0.96 + vlt: 1 + SKU406: + cost: 327 + init_stock: 2000 + price: 327 + service_level: 0.96 + vlt: 1 + SKU407: + cost: 26 + init_stock: 920 + price: 26 + service_level: 0.96 + vlt: 2 + SKU408: + cost: 444 + init_stock: 160 + price: 444 + service_level: 0.96 + vlt: 2 + SKU409: + cost: 257 + init_stock: 1820 + price: 257 + service_level: 0.96 + vlt: 2 + SKU41: + cost: 141 + init_stock: 1580 + price: 141 + service_level: 0.96 + vlt: 2 + SKU410: + cost: 70 + init_stock: 320 + price: 70 + service_level: 0.96 + vlt: 1 + SKU411: + cost: 210 + init_stock: 1900 + price: 210 + service_level: 0.96 + vlt: 3 + SKU412: + cost: 286 + init_stock: 1240 + price: 286 + service_level: 0.96 + vlt: 2 + SKU413: + cost: 403 + init_stock: 1660 + price: 403 + service_level: 0.96 + vlt: 3 + SKU414: + cost: 165 + init_stock: 1740 + price: 165 + service_level: 0.96 + vlt: 1 + SKU415: + cost: 291 + init_stock: 460 + price: 291 + service_level: 0.96 + vlt: 3 + SKU416: + cost: 228 + init_stock: 180 + price: 228 + service_level: 0.96 + vlt: 3 + SKU417: + cost: 443 + init_stock: 1440 + price: 443 + service_level: 0.96 + vlt: 1 + SKU418: + cost: 458 + init_stock: 260 + price: 458 + service_level: 0.96 + vlt: 3 + SKU419: + cost: 303 + init_stock: 1780 + price: 303 + service_level: 0.96 + vlt: 3 + SKU42: + cost: 462 + init_stock: 520 + price: 462 + service_level: 0.96 + vlt: 3 + SKU420: + cost: 268 + init_stock: 840 + price: 268 + service_level: 0.96 + vlt: 1 + SKU421: + cost: 214 + init_stock: 920 + price: 214 + service_level: 0.96 + vlt: 2 + SKU422: + cost: 52 + init_stock: 1080 + price: 52 + service_level: 0.96 + vlt: 3 + SKU423: + cost: 188 + init_stock: 1320 + price: 188 + service_level: 0.96 + vlt: 1 + SKU424: + cost: 189 + init_stock: 220 + price: 189 + service_level: 0.96 + vlt: 2 + SKU425: + cost: 162 + init_stock: 240 + price: 162 + service_level: 0.96 + vlt: 3 + SKU426: + cost: 125 + init_stock: 1960 + price: 125 + service_level: 0.96 + vlt: 3 + SKU427: + cost: 413 + init_stock: 1880 + price: 413 + service_level: 0.96 + vlt: 1 + SKU428: + cost: 391 + init_stock: 1260 + price: 391 + service_level: 0.96 + vlt: 3 + SKU429: + cost: 39 + init_stock: 820 + price: 39 + service_level: 0.96 + vlt: 1 + SKU43: + cost: 217 + init_stock: 1360 + price: 217 + service_level: 0.96 + vlt: 1 + SKU430: + cost: 216 + init_stock: 1460 + price: 216 + service_level: 0.96 + vlt: 2 + SKU431: + cost: 328 + init_stock: 420 + price: 328 + service_level: 0.96 + vlt: 1 + SKU432: + cost: 25 + init_stock: 920 + price: 25 + service_level: 0.96 + vlt: 3 + SKU433: + cost: 348 + init_stock: 900 + price: 348 + service_level: 0.96 + vlt: 2 + SKU434: + cost: 37 + init_stock: 600 + price: 37 + service_level: 0.96 + vlt: 1 + SKU435: + cost: 272 + init_stock: 600 + price: 272 + service_level: 0.96 + vlt: 3 + SKU436: + cost: 72 + init_stock: 1620 + price: 72 + service_level: 0.96 + vlt: 1 + SKU437: + cost: 115 + init_stock: 280 + price: 115 + service_level: 0.96 + vlt: 2 + SKU438: + cost: 143 + init_stock: 200 + price: 143 + service_level: 0.96 + vlt: 3 + SKU439: + cost: 256 + init_stock: 760 + price: 256 + service_level: 0.96 + vlt: 1 + SKU44: + cost: 360 + init_stock: 960 + price: 360 + service_level: 0.96 + vlt: 1 + SKU440: + cost: 248 + init_stock: 1640 + price: 248 + service_level: 0.96 + vlt: 2 + SKU441: + cost: 253 + init_stock: 1120 + price: 253 + service_level: 0.96 + vlt: 2 + SKU442: + cost: 470 + init_stock: 1760 + price: 470 + service_level: 0.96 + vlt: 2 + SKU443: + cost: 218 + init_stock: 1460 + price: 218 + service_level: 0.96 + vlt: 1 + SKU444: + cost: 156 + init_stock: 120 + price: 156 + service_level: 0.96 + vlt: 2 + SKU445: + cost: 260 + init_stock: 1720 + price: 260 + service_level: 0.96 + vlt: 2 + SKU446: + cost: 497 + init_stock: 980 + price: 497 + service_level: 0.96 + vlt: 1 + SKU447: + cost: 196 + init_stock: 100 + price: 196 + service_level: 0.96 + vlt: 1 + SKU448: + cost: 485 + init_stock: 1420 + price: 485 + service_level: 0.96 + vlt: 3 + SKU449: + cost: 282 + init_stock: 760 + price: 282 + service_level: 0.96 + vlt: 2 + SKU45: + cost: 253 + init_stock: 200 + price: 253 + service_level: 0.96 + vlt: 1 + SKU450: + cost: 58 + init_stock: 1960 + price: 58 + service_level: 0.96 + vlt: 2 + SKU451: + cost: 468 + init_stock: 1380 + price: 468 + service_level: 0.96 + vlt: 3 + SKU452: + cost: 63 + init_stock: 1580 + price: 63 + service_level: 0.96 + vlt: 1 + SKU453: + cost: 37 + init_stock: 700 + price: 37 + service_level: 0.96 + vlt: 3 + SKU454: + cost: 494 + init_stock: 1700 + price: 494 + service_level: 0.96 + vlt: 1 + SKU455: + cost: 136 + init_stock: 520 + price: 136 + service_level: 0.96 + vlt: 2 + SKU456: + cost: 338 + init_stock: 500 + price: 338 + service_level: 0.96 + vlt: 2 + SKU457: + cost: 169 + init_stock: 1280 + price: 169 + service_level: 0.96 + vlt: 2 + SKU458: + cost: 403 + init_stock: 140 + price: 403 + service_level: 0.96 + vlt: 3 + SKU459: + cost: 425 + init_stock: 680 + price: 425 + service_level: 0.96 + vlt: 3 + SKU46: + cost: 299 + init_stock: 1000 + price: 299 + service_level: 0.96 + vlt: 3 + SKU460: + cost: 405 + init_stock: 1460 + price: 405 + service_level: 0.96 + vlt: 2 + SKU461: + cost: 251 + init_stock: 960 + price: 251 + service_level: 0.96 + vlt: 1 + SKU462: + cost: 448 + init_stock: 180 + price: 448 + service_level: 0.96 + vlt: 1 + SKU463: + cost: 187 + init_stock: 1640 + price: 187 + service_level: 0.96 + vlt: 3 + SKU464: + cost: 279 + init_stock: 680 + price: 279 + service_level: 0.96 + vlt: 3 + SKU465: + cost: 147 + init_stock: 2000 + price: 147 + service_level: 0.96 + vlt: 2 + SKU466: + cost: 97 + init_stock: 840 + price: 97 + service_level: 0.96 + vlt: 1 + SKU467: + cost: 142 + init_stock: 720 + price: 142 + service_level: 0.96 + vlt: 2 + SKU468: + cost: 55 + init_stock: 1000 + price: 55 + service_level: 0.96 + vlt: 2 + SKU469: + cost: 178 + init_stock: 300 + price: 178 + service_level: 0.96 + vlt: 3 + SKU47: + cost: 121 + init_stock: 780 + price: 121 + service_level: 0.96 + vlt: 1 + SKU470: + cost: 373 + init_stock: 640 + price: 373 + service_level: 0.96 + vlt: 2 + SKU471: + cost: 315 + init_stock: 1420 + price: 315 + service_level: 0.96 + vlt: 3 + SKU472: + cost: 421 + init_stock: 1180 + price: 421 + service_level: 0.96 + vlt: 2 + SKU473: + cost: 195 + init_stock: 420 + price: 195 + service_level: 0.96 + vlt: 1 + SKU474: + cost: 448 + init_stock: 1720 + price: 448 + service_level: 0.96 + vlt: 2 + SKU475: + cost: 34 + init_stock: 1880 + price: 34 + service_level: 0.96 + vlt: 1 + SKU476: + cost: 251 + init_stock: 1800 + price: 251 + service_level: 0.96 + vlt: 1 + SKU477: + cost: 289 + init_stock: 940 + price: 289 + service_level: 0.96 + vlt: 2 + SKU478: + cost: 479 + init_stock: 1760 + price: 479 + service_level: 0.96 + vlt: 2 + SKU479: + cost: 366 + init_stock: 760 + price: 366 + service_level: 0.96 + vlt: 1 + SKU48: + cost: 340 + init_stock: 1160 + price: 340 + service_level: 0.96 + vlt: 2 + SKU480: + cost: 18 + init_stock: 600 + price: 18 + service_level: 0.96 + vlt: 2 + SKU481: + cost: 330 + init_stock: 1760 + price: 330 + service_level: 0.96 + vlt: 1 + SKU482: + cost: 94 + init_stock: 1740 + price: 94 + service_level: 0.96 + vlt: 3 + SKU483: + cost: 298 + init_stock: 680 + price: 298 + service_level: 0.96 + vlt: 2 + SKU484: + cost: 84 + init_stock: 1460 + price: 84 + service_level: 0.96 + vlt: 3 + SKU485: + cost: 318 + init_stock: 1340 + price: 318 + service_level: 0.96 + vlt: 3 + SKU486: + cost: 122 + init_stock: 1040 + price: 122 + service_level: 0.96 + vlt: 1 + SKU487: + cost: 277 + init_stock: 1320 + price: 277 + service_level: 0.96 + vlt: 3 + SKU488: + cost: 36 + init_stock: 540 + price: 36 + service_level: 0.96 + vlt: 3 + SKU489: + cost: 59 + init_stock: 620 + price: 59 + service_level: 0.96 + vlt: 3 + SKU49: + cost: 263 + init_stock: 1900 + price: 263 + service_level: 0.96 + vlt: 3 + SKU490: + cost: 161 + init_stock: 1620 + price: 161 + service_level: 0.96 + vlt: 2 + SKU491: + cost: 444 + init_stock: 1500 + price: 444 + service_level: 0.96 + vlt: 2 + SKU492: + cost: 53 + init_stock: 1600 + price: 53 + service_level: 0.96 + vlt: 2 + SKU493: + cost: 461 + init_stock: 540 + price: 461 + service_level: 0.96 + vlt: 1 + SKU494: + cost: 145 + init_stock: 1880 + price: 145 + service_level: 0.96 + vlt: 1 + SKU495: + cost: 149 + init_stock: 1240 + price: 149 + service_level: 0.96 + vlt: 3 + SKU496: + cost: 342 + init_stock: 1180 + price: 342 + service_level: 0.96 + vlt: 1 + SKU497: + cost: 33 + init_stock: 180 + price: 33 + service_level: 0.96 + vlt: 3 + SKU498: + cost: 305 + init_stock: 1300 + price: 305 + service_level: 0.96 + vlt: 3 + SKU499: + cost: 307 + init_stock: 580 + price: 307 + service_level: 0.96 + vlt: 2 + SKU5: + cost: 466 + init_stock: 1420 + price: 466 + service_level: 0.96 + vlt: 2 + SKU50: + cost: 282 + init_stock: 740 + price: 282 + service_level: 0.96 + vlt: 3 + SKU500: + cost: 208 + init_stock: 840 + price: 208 + service_level: 0.96 + vlt: 3 + SKU501: + cost: 194 + init_stock: 1520 + price: 194 + service_level: 0.96 + vlt: 1 + SKU502: + cost: 69 + init_stock: 1300 + price: 69 + service_level: 0.96 + vlt: 2 + SKU503: + cost: 208 + init_stock: 1140 + price: 208 + service_level: 0.96 + vlt: 1 + SKU504: + cost: 251 + init_stock: 600 + price: 251 + service_level: 0.96 + vlt: 2 + SKU505: + cost: 426 + init_stock: 1180 + price: 426 + service_level: 0.96 + vlt: 1 + SKU506: + cost: 149 + init_stock: 1860 + price: 149 + service_level: 0.96 + vlt: 3 + SKU507: + cost: 187 + init_stock: 300 + price: 187 + service_level: 0.96 + vlt: 1 + SKU508: + cost: 272 + init_stock: 1920 + price: 272 + service_level: 0.96 + vlt: 1 + SKU509: + cost: 297 + init_stock: 1620 + price: 297 + service_level: 0.96 + vlt: 1 + SKU51: + cost: 295 + init_stock: 1340 + price: 295 + service_level: 0.96 + vlt: 3 + SKU510: + cost: 94 + init_stock: 180 + price: 94 + service_level: 0.96 + vlt: 1 + SKU511: + cost: 361 + init_stock: 1500 + price: 361 + service_level: 0.96 + vlt: 1 + SKU512: + cost: 467 + init_stock: 440 + price: 467 + service_level: 0.96 + vlt: 2 + SKU513: + cost: 343 + init_stock: 140 + price: 343 + service_level: 0.96 + vlt: 3 + SKU514: + cost: 186 + init_stock: 1260 + price: 186 + service_level: 0.96 + vlt: 3 + SKU515: + cost: 145 + init_stock: 1080 + price: 145 + service_level: 0.96 + vlt: 3 + SKU516: + cost: 64 + init_stock: 760 + price: 64 + service_level: 0.96 + vlt: 1 + SKU517: + cost: 117 + init_stock: 180 + price: 117 + service_level: 0.96 + vlt: 3 + SKU518: + cost: 26 + init_stock: 420 + price: 26 + service_level: 0.96 + vlt: 3 + SKU519: + cost: 233 + init_stock: 1000 + price: 233 + service_level: 0.96 + vlt: 2 + SKU52: + cost: 22 + init_stock: 1340 + price: 22 + service_level: 0.96 + vlt: 1 + SKU520: + cost: 209 + init_stock: 440 + price: 209 + service_level: 0.96 + vlt: 1 + SKU521: + cost: 220 + init_stock: 1000 + price: 220 + service_level: 0.96 + vlt: 3 + SKU522: + cost: 156 + init_stock: 1740 + price: 156 + service_level: 0.96 + vlt: 1 + SKU523: + cost: 65 + init_stock: 2000 + price: 65 + service_level: 0.96 + vlt: 3 + SKU524: + cost: 294 + init_stock: 1880 + price: 294 + service_level: 0.96 + vlt: 1 + SKU525: + cost: 432 + init_stock: 840 + price: 432 + service_level: 0.96 + vlt: 2 + SKU526: + cost: 419 + init_stock: 480 + price: 419 + service_level: 0.96 + vlt: 3 + SKU527: + cost: 131 + init_stock: 1400 + price: 131 + service_level: 0.96 + vlt: 1 + SKU528: + cost: 316 + init_stock: 420 + price: 316 + service_level: 0.96 + vlt: 3 + SKU529: + cost: 298 + init_stock: 620 + price: 298 + service_level: 0.96 + vlt: 3 + SKU53: + cost: 151 + init_stock: 1400 + price: 151 + service_level: 0.96 + vlt: 2 + SKU530: + cost: 131 + init_stock: 900 + price: 131 + service_level: 0.96 + vlt: 1 + SKU531: + cost: 103 + init_stock: 1200 + price: 103 + service_level: 0.96 + vlt: 3 + SKU532: + cost: 351 + init_stock: 1540 + price: 351 + service_level: 0.96 + vlt: 3 + SKU533: + cost: 296 + init_stock: 840 + price: 296 + service_level: 0.96 + vlt: 3 + SKU534: + cost: 425 + init_stock: 820 + price: 425 + service_level: 0.96 + vlt: 1 + SKU535: + cost: 304 + init_stock: 1720 + price: 304 + service_level: 0.96 + vlt: 2 + SKU536: + cost: 358 + init_stock: 1040 + price: 358 + service_level: 0.96 + vlt: 2 + SKU537: + cost: 321 + init_stock: 180 + price: 321 + service_level: 0.96 + vlt: 3 + SKU538: + cost: 457 + init_stock: 1000 + price: 457 + service_level: 0.96 + vlt: 1 + SKU539: + cost: 165 + init_stock: 1560 + price: 165 + service_level: 0.96 + vlt: 2 + SKU54: + cost: 158 + init_stock: 920 + price: 158 + service_level: 0.96 + vlt: 1 + SKU540: + cost: 388 + init_stock: 840 + price: 388 + service_level: 0.96 + vlt: 3 + SKU541: + cost: 131 + init_stock: 580 + price: 131 + service_level: 0.96 + vlt: 2 + SKU542: + cost: 38 + init_stock: 520 + price: 38 + service_level: 0.96 + vlt: 3 + SKU543: + cost: 430 + init_stock: 1700 + price: 430 + service_level: 0.96 + vlt: 2 + SKU544: + cost: 346 + init_stock: 1940 + price: 346 + service_level: 0.96 + vlt: 1 + SKU545: + cost: 175 + init_stock: 300 + price: 175 + service_level: 0.96 + vlt: 3 + SKU546: + cost: 491 + init_stock: 1920 + price: 491 + service_level: 0.96 + vlt: 3 + SKU547: + cost: 161 + init_stock: 880 + price: 161 + service_level: 0.96 + vlt: 2 + SKU548: + cost: 111 + init_stock: 120 + price: 111 + service_level: 0.96 + vlt: 1 + SKU549: + cost: 387 + init_stock: 1580 + price: 387 + service_level: 0.96 + vlt: 1 + SKU55: + cost: 482 + init_stock: 1300 + price: 482 + service_level: 0.96 + vlt: 3 + SKU550: + cost: 259 + init_stock: 1880 + price: 259 + service_level: 0.96 + vlt: 1 + SKU551: + cost: 46 + init_stock: 620 + price: 46 + service_level: 0.96 + vlt: 2 + SKU552: + cost: 191 + init_stock: 1800 + price: 191 + service_level: 0.96 + vlt: 3 + SKU553: + cost: 208 + init_stock: 600 + price: 208 + service_level: 0.96 + vlt: 1 + SKU554: + cost: 340 + init_stock: 820 + price: 340 + service_level: 0.96 + vlt: 1 + SKU555: + cost: 489 + init_stock: 1680 + price: 489 + service_level: 0.96 + vlt: 1 + SKU556: + cost: 110 + init_stock: 600 + price: 110 + service_level: 0.96 + vlt: 2 + SKU557: + cost: 334 + init_stock: 1460 + price: 334 + service_level: 0.96 + vlt: 2 + SKU558: + cost: 399 + init_stock: 340 + price: 399 + service_level: 0.96 + vlt: 1 + SKU559: + cost: 313 + init_stock: 1080 + price: 313 + service_level: 0.96 + vlt: 1 + SKU56: + cost: 377 + init_stock: 1480 + price: 377 + service_level: 0.96 + vlt: 1 + SKU560: + cost: 283 + init_stock: 820 + price: 283 + service_level: 0.96 + vlt: 1 + SKU561: + cost: 202 + init_stock: 780 + price: 202 + service_level: 0.96 + vlt: 3 + SKU562: + cost: 347 + init_stock: 1780 + price: 347 + service_level: 0.96 + vlt: 2 + SKU563: + cost: 401 + init_stock: 100 + price: 401 + service_level: 0.96 + vlt: 3 + SKU564: + cost: 109 + init_stock: 180 + price: 109 + service_level: 0.96 + vlt: 2 + SKU565: + cost: 57 + init_stock: 1500 + price: 57 + service_level: 0.96 + vlt: 3 + SKU566: + cost: 131 + init_stock: 220 + price: 131 + service_level: 0.96 + vlt: 2 + SKU567: + cost: 361 + init_stock: 180 + price: 361 + service_level: 0.96 + vlt: 1 + SKU568: + cost: 75 + init_stock: 1560 + price: 75 + service_level: 0.96 + vlt: 2 + SKU569: + cost: 473 + init_stock: 960 + price: 473 + service_level: 0.96 + vlt: 2 + SKU57: + cost: 359 + init_stock: 1000 + price: 359 + service_level: 0.96 + vlt: 2 + SKU570: + cost: 388 + init_stock: 1660 + price: 388 + service_level: 0.96 + vlt: 2 + SKU571: + cost: 168 + init_stock: 780 + price: 168 + service_level: 0.96 + vlt: 1 + SKU572: + cost: 26 + init_stock: 900 + price: 26 + service_level: 0.96 + vlt: 3 + SKU573: + cost: 246 + init_stock: 820 + price: 246 + service_level: 0.96 + vlt: 2 + SKU574: + cost: 94 + init_stock: 1000 + price: 94 + service_level: 0.96 + vlt: 2 + SKU575: + cost: 237 + init_stock: 1580 + price: 237 + service_level: 0.96 + vlt: 1 + SKU576: + cost: 265 + init_stock: 1560 + price: 265 + service_level: 0.96 + vlt: 3 + SKU577: + cost: 18 + init_stock: 1740 + price: 18 + service_level: 0.96 + vlt: 3 + SKU578: + cost: 100 + init_stock: 1420 + price: 100 + service_level: 0.96 + vlt: 2 + SKU579: + cost: 415 + init_stock: 180 + price: 415 + service_level: 0.96 + vlt: 1 + SKU58: + cost: 362 + init_stock: 960 + price: 362 + service_level: 0.96 + vlt: 2 + SKU580: + cost: 203 + init_stock: 100 + price: 203 + service_level: 0.96 + vlt: 1 + SKU581: + cost: 152 + init_stock: 1720 + price: 152 + service_level: 0.96 + vlt: 2 + SKU582: + cost: 359 + init_stock: 1920 + price: 359 + service_level: 0.96 + vlt: 2 + SKU583: + cost: 86 + init_stock: 1740 + price: 86 + service_level: 0.96 + vlt: 1 + SKU584: + cost: 33 + init_stock: 900 + price: 33 + service_level: 0.96 + vlt: 2 + SKU585: + cost: 31 + init_stock: 400 + price: 31 + service_level: 0.96 + vlt: 2 + SKU586: + cost: 61 + init_stock: 880 + price: 61 + service_level: 0.96 + vlt: 2 + SKU587: + cost: 290 + init_stock: 1560 + price: 290 + service_level: 0.96 + vlt: 2 + SKU588: + cost: 476 + init_stock: 880 + price: 476 + service_level: 0.96 + vlt: 2 + SKU589: + cost: 244 + init_stock: 680 + price: 244 + service_level: 0.96 + vlt: 3 + SKU59: + cost: 145 + init_stock: 1020 + price: 145 + service_level: 0.96 + vlt: 1 + SKU590: + cost: 70 + init_stock: 620 + price: 70 + service_level: 0.96 + vlt: 2 + SKU591: + cost: 206 + init_stock: 1680 + price: 206 + service_level: 0.96 + vlt: 2 + SKU592: + cost: 218 + init_stock: 920 + price: 218 + service_level: 0.96 + vlt: 1 + SKU593: + cost: 124 + init_stock: 880 + price: 124 + service_level: 0.96 + vlt: 1 + SKU594: + cost: 238 + init_stock: 1000 + price: 238 + service_level: 0.96 + vlt: 1 + SKU595: + cost: 73 + init_stock: 860 + price: 73 + service_level: 0.96 + vlt: 3 + SKU596: + cost: 432 + init_stock: 140 + price: 432 + service_level: 0.96 + vlt: 2 + SKU597: + cost: 252 + init_stock: 1740 + price: 252 + service_level: 0.96 + vlt: 2 + SKU598: + cost: 348 + init_stock: 280 + price: 348 + service_level: 0.96 + vlt: 1 + SKU599: + cost: 54 + init_stock: 1360 + price: 54 + service_level: 0.96 + vlt: 2 + SKU6: + cost: 122 + init_stock: 1760 + price: 122 + service_level: 0.96 + vlt: 2 + SKU60: + cost: 200 + init_stock: 780 + price: 200 + service_level: 0.96 + vlt: 1 + SKU600: + cost: 386 + init_stock: 1300 + price: 386 + service_level: 0.96 + vlt: 1 + SKU601: + cost: 138 + init_stock: 780 + price: 138 + service_level: 0.96 + vlt: 3 + SKU602: + cost: 184 + init_stock: 1960 + price: 184 + service_level: 0.96 + vlt: 1 + SKU603: + cost: 278 + init_stock: 840 + price: 278 + service_level: 0.96 + vlt: 3 + SKU604: + cost: 270 + init_stock: 1000 + price: 270 + service_level: 0.96 + vlt: 3 + SKU605: + cost: 288 + init_stock: 480 + price: 288 + service_level: 0.96 + vlt: 2 + SKU606: + cost: 114 + init_stock: 220 + price: 114 + service_level: 0.96 + vlt: 3 + SKU607: + cost: 208 + init_stock: 1580 + price: 208 + service_level: 0.96 + vlt: 2 + SKU608: + cost: 39 + init_stock: 1460 + price: 39 + service_level: 0.96 + vlt: 1 + SKU609: + cost: 102 + init_stock: 380 + price: 102 + service_level: 0.96 + vlt: 2 + SKU61: + cost: 461 + init_stock: 1500 + price: 461 + service_level: 0.96 + vlt: 1 + SKU610: + cost: 449 + init_stock: 1360 + price: 449 + service_level: 0.96 + vlt: 1 + SKU611: + cost: 306 + init_stock: 1540 + price: 306 + service_level: 0.96 + vlt: 3 + SKU612: + cost: 391 + init_stock: 1760 + price: 391 + service_level: 0.96 + vlt: 2 + SKU613: + cost: 174 + init_stock: 140 + price: 174 + service_level: 0.96 + vlt: 3 + SKU614: + cost: 173 + init_stock: 300 + price: 173 + service_level: 0.96 + vlt: 1 + SKU615: + cost: 330 + init_stock: 1820 + price: 330 + service_level: 0.96 + vlt: 2 + SKU616: + cost: 232 + init_stock: 1460 + price: 232 + service_level: 0.96 + vlt: 1 + SKU617: + cost: 203 + init_stock: 1200 + price: 203 + service_level: 0.96 + vlt: 3 + SKU618: + cost: 77 + init_stock: 460 + price: 77 + service_level: 0.96 + vlt: 1 + SKU619: + cost: 189 + init_stock: 1720 + price: 189 + service_level: 0.96 + vlt: 2 + SKU62: + cost: 124 + init_stock: 180 + price: 124 + service_level: 0.96 + vlt: 2 + SKU620: + cost: 231 + init_stock: 780 + price: 231 + service_level: 0.96 + vlt: 3 + SKU621: + cost: 199 + init_stock: 1440 + price: 199 + service_level: 0.96 + vlt: 2 + SKU622: + cost: 253 + init_stock: 1300 + price: 253 + service_level: 0.96 + vlt: 3 + SKU623: + cost: 335 + init_stock: 1100 + price: 335 + service_level: 0.96 + vlt: 1 + SKU624: + cost: 482 + init_stock: 1080 + price: 482 + service_level: 0.96 + vlt: 2 + SKU625: + cost: 46 + init_stock: 1780 + price: 46 + service_level: 0.96 + vlt: 1 + SKU626: + cost: 451 + init_stock: 1780 + price: 451 + service_level: 0.96 + vlt: 3 + SKU627: + cost: 220 + init_stock: 1640 + price: 220 + service_level: 0.96 + vlt: 3 + SKU628: + cost: 124 + init_stock: 520 + price: 124 + service_level: 0.96 + vlt: 2 + SKU629: + cost: 353 + init_stock: 1840 + price: 353 + service_level: 0.96 + vlt: 1 + SKU63: + cost: 68 + init_stock: 280 + price: 68 + service_level: 0.96 + vlt: 3 + SKU630: + cost: 122 + init_stock: 340 + price: 122 + service_level: 0.96 + vlt: 1 + SKU631: + cost: 296 + init_stock: 980 + price: 296 + service_level: 0.96 + vlt: 2 + SKU632: + cost: 85 + init_stock: 1880 + price: 85 + service_level: 0.96 + vlt: 2 + SKU633: + cost: 184 + init_stock: 1340 + price: 184 + service_level: 0.96 + vlt: 2 + SKU634: + cost: 17 + init_stock: 1460 + price: 17 + service_level: 0.96 + vlt: 1 + SKU635: + cost: 341 + init_stock: 1860 + price: 341 + service_level: 0.96 + vlt: 1 + SKU636: + cost: 385 + init_stock: 740 + price: 385 + service_level: 0.96 + vlt: 1 + SKU637: + cost: 83 + init_stock: 1220 + price: 83 + service_level: 0.96 + vlt: 3 + SKU638: + cost: 375 + init_stock: 160 + price: 375 + service_level: 0.96 + vlt: 1 + SKU639: + cost: 327 + init_stock: 800 + price: 327 + service_level: 0.96 + vlt: 2 + SKU64: + cost: 36 + init_stock: 380 + price: 36 + service_level: 0.96 + vlt: 3 + SKU640: + cost: 275 + init_stock: 1820 + price: 275 + service_level: 0.96 + vlt: 1 + SKU641: + cost: 365 + init_stock: 1620 + price: 365 + service_level: 0.96 + vlt: 1 + SKU642: + cost: 414 + init_stock: 500 + price: 414 + service_level: 0.96 + vlt: 2 + SKU643: + cost: 358 + init_stock: 920 + price: 358 + service_level: 0.96 + vlt: 2 + SKU644: + cost: 46 + init_stock: 1640 + price: 46 + service_level: 0.96 + vlt: 2 + SKU645: + cost: 467 + init_stock: 1980 + price: 467 + service_level: 0.96 + vlt: 3 + SKU646: + cost: 189 + init_stock: 260 + price: 189 + service_level: 0.96 + vlt: 3 + SKU647: + cost: 303 + init_stock: 1540 + price: 303 + service_level: 0.96 + vlt: 3 + SKU648: + cost: 226 + init_stock: 1100 + price: 226 + service_level: 0.96 + vlt: 1 + SKU649: + cost: 198 + init_stock: 500 + price: 198 + service_level: 0.96 + vlt: 3 + SKU65: + cost: 15 + init_stock: 1880 + price: 15 + service_level: 0.96 + vlt: 2 + SKU650: + cost: 351 + init_stock: 1120 + price: 351 + service_level: 0.96 + vlt: 2 + SKU651: + cost: 380 + init_stock: 1920 + price: 380 + service_level: 0.96 + vlt: 3 + SKU652: + cost: 123 + init_stock: 800 + price: 123 + service_level: 0.96 + vlt: 2 + SKU653: + cost: 479 + init_stock: 1920 + price: 479 + service_level: 0.96 + vlt: 2 + SKU654: + cost: 66 + init_stock: 160 + price: 66 + service_level: 0.96 + vlt: 3 + SKU655: + cost: 333 + init_stock: 840 + price: 333 + service_level: 0.96 + vlt: 1 + SKU656: + cost: 53 + init_stock: 1440 + price: 53 + service_level: 0.96 + vlt: 2 + SKU657: + cost: 73 + init_stock: 1620 + price: 73 + service_level: 0.96 + vlt: 2 + SKU658: + cost: 70 + init_stock: 720 + price: 70 + service_level: 0.96 + vlt: 3 + SKU659: + cost: 21 + init_stock: 1120 + price: 21 + service_level: 0.96 + vlt: 1 + SKU66: + cost: 143 + init_stock: 1800 + price: 143 + service_level: 0.96 + vlt: 1 + SKU660: + cost: 487 + init_stock: 1660 + price: 487 + service_level: 0.96 + vlt: 1 + SKU661: + cost: 344 + init_stock: 1480 + price: 344 + service_level: 0.96 + vlt: 2 + SKU662: + cost: 372 + init_stock: 760 + price: 372 + service_level: 0.96 + vlt: 3 + SKU663: + cost: 418 + init_stock: 800 + price: 418 + service_level: 0.96 + vlt: 3 + SKU664: + cost: 351 + init_stock: 1680 + price: 351 + service_level: 0.96 + vlt: 1 + SKU665: + cost: 493 + init_stock: 860 + price: 493 + service_level: 0.96 + vlt: 3 + SKU666: + cost: 341 + init_stock: 1760 + price: 341 + service_level: 0.96 + vlt: 1 + SKU667: + cost: 325 + init_stock: 260 + price: 325 + service_level: 0.96 + vlt: 3 + SKU668: + cost: 286 + init_stock: 1200 + price: 286 + service_level: 0.96 + vlt: 3 + SKU669: + cost: 74 + init_stock: 320 + price: 74 + service_level: 0.96 + vlt: 1 + SKU67: + cost: 247 + init_stock: 1060 + price: 247 + service_level: 0.96 + vlt: 1 + SKU670: + cost: 289 + init_stock: 1720 + price: 289 + service_level: 0.96 + vlt: 1 + SKU671: + cost: 267 + init_stock: 1660 + price: 267 + service_level: 0.96 + vlt: 3 + SKU672: + cost: 369 + init_stock: 260 + price: 369 + service_level: 0.96 + vlt: 2 + SKU673: + cost: 322 + init_stock: 820 + price: 322 + service_level: 0.96 + vlt: 2 + SKU674: + cost: 223 + init_stock: 440 + price: 223 + service_level: 0.96 + vlt: 1 + SKU675: + cost: 438 + init_stock: 660 + price: 438 + service_level: 0.96 + vlt: 1 + SKU676: + cost: 244 + init_stock: 1220 + price: 244 + service_level: 0.96 + vlt: 1 + SKU677: + cost: 425 + init_stock: 880 + price: 425 + service_level: 0.96 + vlt: 2 + SKU678: + cost: 168 + init_stock: 720 + price: 168 + service_level: 0.96 + vlt: 1 + SKU679: + cost: 395 + init_stock: 880 + price: 395 + service_level: 0.96 + vlt: 2 + SKU68: + cost: 169 + init_stock: 280 + price: 169 + service_level: 0.96 + vlt: 2 + SKU680: + cost: 260 + init_stock: 600 + price: 260 + service_level: 0.96 + vlt: 1 + SKU681: + cost: 464 + init_stock: 1940 + price: 464 + service_level: 0.96 + vlt: 3 + SKU682: + cost: 370 + init_stock: 1060 + price: 370 + service_level: 0.96 + vlt: 2 + SKU683: + cost: 327 + init_stock: 1340 + price: 327 + service_level: 0.96 + vlt: 3 + SKU684: + cost: 355 + init_stock: 1580 + price: 355 + service_level: 0.96 + vlt: 1 + SKU685: + cost: 422 + init_stock: 900 + price: 422 + service_level: 0.96 + vlt: 2 + SKU686: + cost: 63 + init_stock: 1260 + price: 63 + service_level: 0.96 + vlt: 1 + SKU687: + cost: 34 + init_stock: 1520 + price: 34 + service_level: 0.96 + vlt: 2 + SKU688: + cost: 484 + init_stock: 1540 + price: 484 + service_level: 0.96 + vlt: 3 + SKU689: + cost: 499 + init_stock: 300 + price: 499 + service_level: 0.96 + vlt: 1 + SKU69: + cost: 176 + init_stock: 1340 + price: 176 + service_level: 0.96 + vlt: 1 + SKU690: + cost: 437 + init_stock: 1660 + price: 437 + service_level: 0.96 + vlt: 2 + SKU691: + cost: 17 + init_stock: 1580 + price: 17 + service_level: 0.96 + vlt: 3 + SKU692: + cost: 225 + init_stock: 1300 + price: 225 + service_level: 0.96 + vlt: 1 + SKU693: + cost: 19 + init_stock: 1220 + price: 19 + service_level: 0.96 + vlt: 2 + SKU694: + cost: 208 + init_stock: 1500 + price: 208 + service_level: 0.96 + vlt: 3 + SKU695: + cost: 190 + init_stock: 140 + price: 190 + service_level: 0.96 + vlt: 2 + SKU696: + cost: 348 + init_stock: 1160 + price: 348 + service_level: 0.96 + vlt: 1 + SKU697: + cost: 455 + init_stock: 1600 + price: 455 + service_level: 0.96 + vlt: 1 + SKU698: + cost: 198 + init_stock: 1220 + price: 198 + service_level: 0.96 + vlt: 3 + SKU699: + cost: 31 + init_stock: 400 + price: 31 + service_level: 0.96 + vlt: 3 + SKU7: + cost: 101 + init_stock: 1920 + price: 101 + service_level: 0.96 + vlt: 2 + SKU70: + cost: 186 + init_stock: 700 + price: 186 + service_level: 0.96 + vlt: 1 + SKU700: + cost: 74 + init_stock: 180 + price: 74 + service_level: 0.96 + vlt: 2 + SKU701: + cost: 399 + init_stock: 1540 + price: 399 + service_level: 0.96 + vlt: 1 + SKU702: + cost: 82 + init_stock: 680 + price: 82 + service_level: 0.96 + vlt: 1 + SKU703: + cost: 363 + init_stock: 760 + price: 363 + service_level: 0.96 + vlt: 1 + SKU704: + cost: 384 + init_stock: 1740 + price: 384 + service_level: 0.96 + vlt: 2 + SKU705: + cost: 128 + init_stock: 1260 + price: 128 + service_level: 0.96 + vlt: 2 + SKU706: + cost: 182 + init_stock: 1820 + price: 182 + service_level: 0.96 + vlt: 2 + SKU707: + cost: 18 + init_stock: 1380 + price: 18 + service_level: 0.96 + vlt: 1 + SKU708: + cost: 178 + init_stock: 560 + price: 178 + service_level: 0.96 + vlt: 3 + SKU709: + cost: 102 + init_stock: 1060 + price: 102 + service_level: 0.96 + vlt: 3 + SKU71: + cost: 315 + init_stock: 900 + price: 315 + service_level: 0.96 + vlt: 2 + SKU710: + cost: 252 + init_stock: 380 + price: 252 + service_level: 0.96 + vlt: 2 + SKU711: + cost: 281 + init_stock: 1380 + price: 281 + service_level: 0.96 + vlt: 1 + SKU712: + cost: 232 + init_stock: 220 + price: 232 + service_level: 0.96 + vlt: 2 + SKU713: + cost: 285 + init_stock: 860 + price: 285 + service_level: 0.96 + vlt: 2 + SKU714: + cost: 79 + init_stock: 820 + price: 79 + service_level: 0.96 + vlt: 3 + SKU715: + cost: 234 + init_stock: 340 + price: 234 + service_level: 0.96 + vlt: 1 + SKU716: + cost: 70 + init_stock: 1500 + price: 70 + service_level: 0.96 + vlt: 2 + SKU717: + cost: 345 + init_stock: 160 + price: 345 + service_level: 0.96 + vlt: 2 + SKU718: + cost: 357 + init_stock: 1160 + price: 357 + service_level: 0.96 + vlt: 2 + SKU719: + cost: 340 + init_stock: 1300 + price: 340 + service_level: 0.96 + vlt: 3 + SKU72: + cost: 458 + init_stock: 1700 + price: 458 + service_level: 0.96 + vlt: 2 + SKU720: + cost: 325 + init_stock: 840 + price: 325 + service_level: 0.96 + vlt: 3 + SKU721: + cost: 73 + init_stock: 220 + price: 73 + service_level: 0.96 + vlt: 2 + SKU722: + cost: 392 + init_stock: 1940 + price: 392 + service_level: 0.96 + vlt: 1 + SKU723: + cost: 318 + init_stock: 1780 + price: 318 + service_level: 0.96 + vlt: 3 + SKU724: + cost: 400 + init_stock: 660 + price: 400 + service_level: 0.96 + vlt: 1 + SKU725: + cost: 175 + init_stock: 740 + price: 175 + service_level: 0.96 + vlt: 1 + SKU726: + cost: 458 + init_stock: 1780 + price: 458 + service_level: 0.96 + vlt: 3 + SKU727: + cost: 418 + init_stock: 1140 + price: 418 + service_level: 0.96 + vlt: 1 + SKU728: + cost: 475 + init_stock: 740 + price: 475 + service_level: 0.96 + vlt: 3 + SKU729: + cost: 324 + init_stock: 720 + price: 324 + service_level: 0.96 + vlt: 3 + SKU73: + cost: 212 + init_stock: 1080 + price: 212 + service_level: 0.96 + vlt: 2 + SKU730: + cost: 16 + init_stock: 260 + price: 16 + service_level: 0.96 + vlt: 2 + SKU731: + cost: 88 + init_stock: 1640 + price: 88 + service_level: 0.96 + vlt: 2 + SKU732: + cost: 41 + init_stock: 1240 + price: 41 + service_level: 0.96 + vlt: 3 + SKU733: + cost: 315 + init_stock: 520 + price: 315 + service_level: 0.96 + vlt: 3 + SKU734: + cost: 37 + init_stock: 1020 + price: 37 + service_level: 0.96 + vlt: 3 + SKU735: + cost: 266 + init_stock: 1980 + price: 266 + service_level: 0.96 + vlt: 3 + SKU736: + cost: 368 + init_stock: 500 + price: 368 + service_level: 0.96 + vlt: 2 + SKU737: + cost: 475 + init_stock: 620 + price: 475 + service_level: 0.96 + vlt: 2 + SKU738: + cost: 185 + init_stock: 960 + price: 185 + service_level: 0.96 + vlt: 3 + SKU739: + cost: 475 + init_stock: 1480 + price: 475 + service_level: 0.96 + vlt: 2 + SKU74: + cost: 159 + init_stock: 840 + price: 159 + service_level: 0.96 + vlt: 1 + SKU740: + cost: 390 + init_stock: 1280 + price: 390 + service_level: 0.96 + vlt: 1 + SKU741: + cost: 91 + init_stock: 1120 + price: 91 + service_level: 0.96 + vlt: 1 + SKU742: + cost: 188 + init_stock: 440 + price: 188 + service_level: 0.96 + vlt: 1 + SKU743: + cost: 217 + init_stock: 860 + price: 217 + service_level: 0.96 + vlt: 3 + SKU744: + cost: 379 + init_stock: 1160 + price: 379 + service_level: 0.96 + vlt: 1 + SKU745: + cost: 316 + init_stock: 1840 + price: 316 + service_level: 0.96 + vlt: 2 + SKU746: + cost: 437 + init_stock: 800 + price: 437 + service_level: 0.96 + vlt: 2 + SKU747: + cost: 373 + init_stock: 1580 + price: 373 + service_level: 0.96 + vlt: 2 + SKU748: + cost: 275 + init_stock: 1320 + price: 275 + service_level: 0.96 + vlt: 2 + SKU749: + cost: 394 + init_stock: 1060 + price: 394 + service_level: 0.96 + vlt: 1 + SKU75: + cost: 131 + init_stock: 380 + price: 131 + service_level: 0.96 + vlt: 1 + SKU750: + cost: 256 + init_stock: 1240 + price: 256 + service_level: 0.96 + vlt: 3 + SKU751: + cost: 369 + init_stock: 1300 + price: 369 + service_level: 0.96 + vlt: 3 + SKU752: + cost: 259 + init_stock: 1560 + price: 259 + service_level: 0.96 + vlt: 3 + SKU753: + cost: 77 + init_stock: 1300 + price: 77 + service_level: 0.96 + vlt: 3 + SKU754: + cost: 387 + init_stock: 260 + price: 387 + service_level: 0.96 + vlt: 2 + SKU755: + cost: 354 + init_stock: 1600 + price: 354 + service_level: 0.96 + vlt: 3 + SKU756: + cost: 246 + init_stock: 1800 + price: 246 + service_level: 0.96 + vlt: 1 + SKU757: + cost: 40 + init_stock: 1140 + price: 40 + service_level: 0.96 + vlt: 3 + SKU758: + cost: 241 + init_stock: 800 + price: 241 + service_level: 0.96 + vlt: 2 + SKU759: + cost: 435 + init_stock: 760 + price: 435 + service_level: 0.96 + vlt: 1 + SKU76: + cost: 147 + init_stock: 1280 + price: 147 + service_level: 0.96 + vlt: 2 + SKU760: + cost: 176 + init_stock: 1020 + price: 176 + service_level: 0.96 + vlt: 3 + SKU761: + cost: 224 + init_stock: 1100 + price: 224 + service_level: 0.96 + vlt: 1 + SKU762: + cost: 264 + init_stock: 1020 + price: 264 + service_level: 0.96 + vlt: 1 + SKU763: + cost: 385 + init_stock: 1580 + price: 385 + service_level: 0.96 + vlt: 2 + SKU764: + cost: 349 + init_stock: 680 + price: 349 + service_level: 0.96 + vlt: 1 + SKU765: + cost: 345 + init_stock: 260 + price: 345 + service_level: 0.96 + vlt: 1 + SKU766: + cost: 478 + init_stock: 400 + price: 478 + service_level: 0.96 + vlt: 2 + SKU767: + cost: 95 + init_stock: 1520 + price: 95 + service_level: 0.96 + vlt: 1 + SKU768: + cost: 181 + init_stock: 1840 + price: 181 + service_level: 0.96 + vlt: 2 + SKU769: + cost: 24 + init_stock: 580 + price: 24 + service_level: 0.96 + vlt: 2 + SKU77: + cost: 409 + init_stock: 1440 + price: 409 + service_level: 0.96 + vlt: 1 + SKU770: + cost: 150 + init_stock: 1240 + price: 150 + service_level: 0.96 + vlt: 2 + SKU771: + cost: 101 + init_stock: 140 + price: 101 + service_level: 0.96 + vlt: 1 + SKU772: + cost: 256 + init_stock: 120 + price: 256 + service_level: 0.96 + vlt: 3 + SKU773: + cost: 84 + init_stock: 1840 + price: 84 + service_level: 0.96 + vlt: 1 + SKU774: + cost: 447 + init_stock: 1180 + price: 447 + service_level: 0.96 + vlt: 1 + SKU775: + cost: 175 + init_stock: 1720 + price: 175 + service_level: 0.96 + vlt: 3 + SKU776: + cost: 103 + init_stock: 1700 + price: 103 + service_level: 0.96 + vlt: 2 + SKU777: + cost: 292 + init_stock: 1140 + price: 292 + service_level: 0.96 + vlt: 2 + SKU778: + cost: 203 + init_stock: 160 + price: 203 + service_level: 0.96 + vlt: 3 + SKU779: + cost: 117 + init_stock: 860 + price: 117 + service_level: 0.96 + vlt: 1 + SKU78: + cost: 234 + init_stock: 260 + price: 234 + service_level: 0.96 + vlt: 3 + SKU780: + cost: 69 + init_stock: 1640 + price: 69 + service_level: 0.96 + vlt: 3 + SKU781: + cost: 372 + init_stock: 720 + price: 372 + service_level: 0.96 + vlt: 3 + SKU782: + cost: 27 + init_stock: 200 + price: 27 + service_level: 0.96 + vlt: 3 + SKU783: + cost: 87 + init_stock: 1620 + price: 87 + service_level: 0.96 + vlt: 2 + SKU784: + cost: 309 + init_stock: 1040 + price: 309 + service_level: 0.96 + vlt: 3 + SKU785: + cost: 191 + init_stock: 1400 + price: 191 + service_level: 0.96 + vlt: 2 + SKU786: + cost: 91 + init_stock: 440 + price: 91 + service_level: 0.96 + vlt: 3 + SKU787: + cost: 360 + init_stock: 1220 + price: 360 + service_level: 0.96 + vlt: 1 + SKU788: + cost: 351 + init_stock: 560 + price: 351 + service_level: 0.96 + vlt: 1 + SKU789: + cost: 153 + init_stock: 1160 + price: 153 + service_level: 0.96 + vlt: 2 + SKU79: + cost: 245 + init_stock: 220 + price: 245 + service_level: 0.96 + vlt: 1 + SKU790: + cost: 417 + init_stock: 1320 + price: 417 + service_level: 0.96 + vlt: 2 + SKU791: + cost: 134 + init_stock: 560 + price: 134 + service_level: 0.96 + vlt: 1 + SKU792: + cost: 313 + init_stock: 420 + price: 313 + service_level: 0.96 + vlt: 2 + SKU793: + cost: 195 + init_stock: 1520 + price: 195 + service_level: 0.96 + vlt: 2 + SKU794: + cost: 325 + init_stock: 1600 + price: 325 + service_level: 0.96 + vlt: 3 + SKU795: + cost: 276 + init_stock: 960 + price: 276 + service_level: 0.96 + vlt: 3 + SKU796: + cost: 447 + init_stock: 980 + price: 447 + service_level: 0.96 + vlt: 1 + SKU797: + cost: 100 + init_stock: 780 + price: 100 + service_level: 0.96 + vlt: 3 + SKU798: + cost: 426 + init_stock: 600 + price: 426 + service_level: 0.96 + vlt: 2 + SKU799: + cost: 63 + init_stock: 140 + price: 63 + service_level: 0.96 + vlt: 2 + SKU8: + cost: 125 + init_stock: 1260 + price: 125 + service_level: 0.96 + vlt: 3 + SKU80: + cost: 163 + init_stock: 1400 + price: 163 + service_level: 0.96 + vlt: 1 + SKU800: + cost: 298 + init_stock: 1880 + price: 298 + service_level: 0.96 + vlt: 2 + SKU801: + cost: 389 + init_stock: 720 + price: 389 + service_level: 0.96 + vlt: 2 + SKU802: + cost: 173 + init_stock: 1240 + price: 173 + service_level: 0.96 + vlt: 2 + SKU803: + cost: 272 + init_stock: 380 + price: 272 + service_level: 0.96 + vlt: 3 + SKU804: + cost: 390 + init_stock: 940 + price: 390 + service_level: 0.96 + vlt: 3 + SKU805: + cost: 61 + init_stock: 660 + price: 61 + service_level: 0.96 + vlt: 3 + SKU806: + cost: 158 + init_stock: 1040 + price: 158 + service_level: 0.96 + vlt: 1 + SKU807: + cost: 453 + init_stock: 520 + price: 453 + service_level: 0.96 + vlt: 3 + SKU808: + cost: 249 + init_stock: 1820 + price: 249 + service_level: 0.96 + vlt: 1 + SKU809: + cost: 55 + init_stock: 1300 + price: 55 + service_level: 0.96 + vlt: 1 + SKU81: + cost: 182 + init_stock: 1320 + price: 182 + service_level: 0.96 + vlt: 3 + SKU810: + cost: 276 + init_stock: 120 + price: 276 + service_level: 0.96 + vlt: 2 + SKU811: + cost: 254 + init_stock: 740 + price: 254 + service_level: 0.96 + vlt: 3 + SKU812: + cost: 252 + init_stock: 1600 + price: 252 + service_level: 0.96 + vlt: 1 + SKU813: + cost: 253 + init_stock: 140 + price: 253 + service_level: 0.96 + vlt: 3 + SKU814: + cost: 485 + init_stock: 1940 + price: 485 + service_level: 0.96 + vlt: 1 + SKU815: + cost: 390 + init_stock: 480 + price: 390 + service_level: 0.96 + vlt: 2 + SKU816: + cost: 152 + init_stock: 1820 + price: 152 + service_level: 0.96 + vlt: 3 + SKU817: + cost: 227 + init_stock: 100 + price: 227 + service_level: 0.96 + vlt: 2 + SKU818: + cost: 354 + init_stock: 860 + price: 354 + service_level: 0.96 + vlt: 2 + SKU819: + cost: 302 + init_stock: 540 + price: 302 + service_level: 0.96 + vlt: 1 + SKU82: + cost: 290 + init_stock: 560 + price: 290 + service_level: 0.96 + vlt: 1 + SKU820: + cost: 264 + init_stock: 1640 + price: 264 + service_level: 0.96 + vlt: 3 + SKU821: + cost: 99 + init_stock: 1380 + price: 99 + service_level: 0.96 + vlt: 1 + SKU822: + cost: 136 + init_stock: 1400 + price: 136 + service_level: 0.96 + vlt: 3 + SKU823: + cost: 75 + init_stock: 560 + price: 75 + service_level: 0.96 + vlt: 2 + SKU824: + cost: 170 + init_stock: 1400 + price: 170 + service_level: 0.96 + vlt: 1 + SKU825: + cost: 214 + init_stock: 300 + price: 214 + service_level: 0.96 + vlt: 1 + SKU826: + cost: 386 + init_stock: 1100 + price: 386 + service_level: 0.96 + vlt: 2 + SKU827: + cost: 148 + init_stock: 1280 + price: 148 + service_level: 0.96 + vlt: 2 + SKU828: + cost: 400 + init_stock: 1380 + price: 400 + service_level: 0.96 + vlt: 1 + SKU829: + cost: 61 + init_stock: 1480 + price: 61 + service_level: 0.96 + vlt: 1 + SKU83: + cost: 296 + init_stock: 340 + price: 296 + service_level: 0.96 + vlt: 2 + SKU830: + cost: 167 + init_stock: 480 + price: 167 + service_level: 0.96 + vlt: 3 + SKU831: + cost: 262 + init_stock: 580 + price: 262 + service_level: 0.96 + vlt: 1 + SKU832: + cost: 33 + init_stock: 1640 + price: 33 + service_level: 0.96 + vlt: 1 + SKU833: + cost: 400 + init_stock: 1960 + price: 400 + service_level: 0.96 + vlt: 3 + SKU834: + cost: 422 + init_stock: 100 + price: 422 + service_level: 0.96 + vlt: 1 + SKU835: + cost: 440 + init_stock: 1040 + price: 440 + service_level: 0.96 + vlt: 2 + SKU836: + cost: 323 + init_stock: 1920 + price: 323 + service_level: 0.96 + vlt: 1 + SKU837: + cost: 373 + init_stock: 520 + price: 373 + service_level: 0.96 + vlt: 3 + SKU838: + cost: 456 + init_stock: 1540 + price: 456 + service_level: 0.96 + vlt: 1 + SKU839: + cost: 473 + init_stock: 1200 + price: 473 + service_level: 0.96 + vlt: 1 + SKU84: + cost: 318 + init_stock: 840 + price: 318 + service_level: 0.96 + vlt: 3 + SKU840: + cost: 266 + init_stock: 1000 + price: 266 + service_level: 0.96 + vlt: 1 + SKU841: + cost: 285 + init_stock: 1700 + price: 285 + service_level: 0.96 + vlt: 3 + SKU842: + cost: 41 + init_stock: 1680 + price: 41 + service_level: 0.96 + vlt: 1 + SKU843: + cost: 360 + init_stock: 1440 + price: 360 + service_level: 0.96 + vlt: 2 + SKU844: + cost: 51 + init_stock: 1580 + price: 51 + service_level: 0.96 + vlt: 3 + SKU845: + cost: 288 + init_stock: 440 + price: 288 + service_level: 0.96 + vlt: 3 + SKU846: + cost: 485 + init_stock: 780 + price: 485 + service_level: 0.96 + vlt: 1 + SKU847: + cost: 388 + init_stock: 1820 + price: 388 + service_level: 0.96 + vlt: 1 + SKU848: + cost: 306 + init_stock: 920 + price: 306 + service_level: 0.96 + vlt: 2 + SKU849: + cost: 320 + init_stock: 800 + price: 320 + service_level: 0.96 + vlt: 2 + SKU85: + cost: 442 + init_stock: 2000 + price: 442 + service_level: 0.96 + vlt: 3 + SKU850: + cost: 183 + init_stock: 1140 + price: 183 + service_level: 0.96 + vlt: 2 + SKU851: + cost: 53 + init_stock: 1280 + price: 53 + service_level: 0.96 + vlt: 1 + SKU852: + cost: 306 + init_stock: 1080 + price: 306 + service_level: 0.96 + vlt: 2 + SKU853: + cost: 386 + init_stock: 1120 + price: 386 + service_level: 0.96 + vlt: 3 + SKU854: + cost: 212 + init_stock: 1300 + price: 212 + service_level: 0.96 + vlt: 3 + SKU855: + cost: 76 + init_stock: 1440 + price: 76 + service_level: 0.96 + vlt: 3 + SKU856: + cost: 380 + init_stock: 640 + price: 380 + service_level: 0.96 + vlt: 2 + SKU857: + cost: 462 + init_stock: 2000 + price: 462 + service_level: 0.96 + vlt: 1 + SKU858: + cost: 80 + init_stock: 480 + price: 80 + service_level: 0.96 + vlt: 3 + SKU859: + cost: 215 + init_stock: 560 + price: 215 + service_level: 0.96 + vlt: 3 + SKU86: + cost: 386 + init_stock: 1700 + price: 386 + service_level: 0.96 + vlt: 2 + SKU860: + cost: 313 + init_stock: 300 + price: 313 + service_level: 0.96 + vlt: 2 + SKU861: + cost: 477 + init_stock: 260 + price: 477 + service_level: 0.96 + vlt: 2 + SKU862: + cost: 240 + init_stock: 320 + price: 240 + service_level: 0.96 + vlt: 2 + SKU863: + cost: 470 + init_stock: 1860 + price: 470 + service_level: 0.96 + vlt: 2 + SKU864: + cost: 203 + init_stock: 380 + price: 203 + service_level: 0.96 + vlt: 1 + SKU865: + cost: 144 + init_stock: 380 + price: 144 + service_level: 0.96 + vlt: 3 + SKU866: + cost: 172 + init_stock: 1760 + price: 172 + service_level: 0.96 + vlt: 1 + SKU867: + cost: 499 + init_stock: 2000 + price: 499 + service_level: 0.96 + vlt: 2 + SKU868: + cost: 41 + init_stock: 1000 + price: 41 + service_level: 0.96 + vlt: 2 + SKU869: + cost: 97 + init_stock: 1940 + price: 97 + service_level: 0.96 + vlt: 2 + SKU87: + cost: 368 + init_stock: 1380 + price: 368 + service_level: 0.96 + vlt: 2 + SKU870: + cost: 281 + init_stock: 1340 + price: 281 + service_level: 0.96 + vlt: 1 + SKU871: + cost: 101 + init_stock: 1980 + price: 101 + service_level: 0.96 + vlt: 1 + SKU872: + cost: 133 + init_stock: 640 + price: 133 + service_level: 0.96 + vlt: 3 + SKU873: + cost: 423 + init_stock: 620 + price: 423 + service_level: 0.96 + vlt: 2 + SKU874: + cost: 469 + init_stock: 1200 + price: 469 + service_level: 0.96 + vlt: 1 + SKU875: + cost: 51 + init_stock: 460 + price: 51 + service_level: 0.96 + vlt: 2 + SKU876: + cost: 303 + init_stock: 1220 + price: 303 + service_level: 0.96 + vlt: 3 + SKU877: + cost: 40 + init_stock: 700 + price: 40 + service_level: 0.96 + vlt: 3 + SKU878: + cost: 27 + init_stock: 1360 + price: 27 + service_level: 0.96 + vlt: 3 + SKU879: + cost: 150 + init_stock: 2000 + price: 150 + service_level: 0.96 + vlt: 1 + SKU88: + cost: 471 + init_stock: 240 + price: 471 + service_level: 0.96 + vlt: 1 + SKU880: + cost: 433 + init_stock: 620 + price: 433 + service_level: 0.96 + vlt: 3 + SKU881: + cost: 431 + init_stock: 1400 + price: 431 + service_level: 0.96 + vlt: 3 + SKU882: + cost: 194 + init_stock: 320 + price: 194 + service_level: 0.96 + vlt: 1 + SKU883: + cost: 284 + init_stock: 760 + price: 284 + service_level: 0.96 + vlt: 1 + SKU884: + cost: 139 + init_stock: 780 + price: 139 + service_level: 0.96 + vlt: 2 + SKU885: + cost: 49 + init_stock: 540 + price: 49 + service_level: 0.96 + vlt: 3 + SKU886: + cost: 372 + init_stock: 1460 + price: 372 + service_level: 0.96 + vlt: 3 + SKU887: + cost: 266 + init_stock: 1740 + price: 266 + service_level: 0.96 + vlt: 1 + SKU888: + cost: 143 + init_stock: 180 + price: 143 + service_level: 0.96 + vlt: 2 + SKU889: + cost: 101 + init_stock: 420 + price: 101 + service_level: 0.96 + vlt: 2 + SKU89: + cost: 138 + init_stock: 1860 + price: 138 + service_level: 0.96 + vlt: 3 + SKU890: + cost: 161 + init_stock: 2000 + price: 161 + service_level: 0.96 + vlt: 3 + SKU891: + cost: 356 + init_stock: 440 + price: 356 + service_level: 0.96 + vlt: 3 + SKU892: + cost: 313 + init_stock: 1840 + price: 313 + service_level: 0.96 + vlt: 2 + SKU893: + cost: 229 + init_stock: 1980 + price: 229 + service_level: 0.96 + vlt: 1 + SKU894: + cost: 129 + init_stock: 1480 + price: 129 + service_level: 0.96 + vlt: 1 + SKU895: + cost: 230 + init_stock: 260 + price: 230 + service_level: 0.96 + vlt: 2 + SKU896: + cost: 289 + init_stock: 1600 + price: 289 + service_level: 0.96 + vlt: 3 + SKU897: + cost: 393 + init_stock: 1440 + price: 393 + service_level: 0.96 + vlt: 3 + SKU898: + cost: 477 + init_stock: 220 + price: 477 + service_level: 0.96 + vlt: 2 + SKU899: + cost: 233 + init_stock: 960 + price: 233 + service_level: 0.96 + vlt: 2 + SKU9: + cost: 166 + init_stock: 1920 + price: 166 + service_level: 0.96 + vlt: 2 + SKU90: + cost: 454 + init_stock: 1020 + price: 454 + service_level: 0.96 + vlt: 2 + SKU900: + cost: 158 + init_stock: 1100 + price: 158 + service_level: 0.96 + vlt: 1 + SKU901: + cost: 215 + init_stock: 580 + price: 215 + service_level: 0.96 + vlt: 3 + SKU902: + cost: 125 + init_stock: 1600 + price: 125 + service_level: 0.96 + vlt: 2 + SKU903: + cost: 357 + init_stock: 760 + price: 357 + service_level: 0.96 + vlt: 2 + SKU904: + cost: 496 + init_stock: 1020 + price: 496 + service_level: 0.96 + vlt: 1 + SKU905: + cost: 249 + init_stock: 620 + price: 249 + service_level: 0.96 + vlt: 3 + SKU906: + cost: 166 + init_stock: 1040 + price: 166 + service_level: 0.96 + vlt: 2 + SKU907: + cost: 22 + init_stock: 1660 + price: 22 + service_level: 0.96 + vlt: 3 + SKU908: + cost: 408 + init_stock: 1560 + price: 408 + service_level: 0.96 + vlt: 2 + SKU909: + cost: 482 + init_stock: 1920 + price: 482 + service_level: 0.96 + vlt: 1 + SKU91: + cost: 303 + init_stock: 320 + price: 303 + service_level: 0.96 + vlt: 2 + SKU910: + cost: 226 + init_stock: 660 + price: 226 + service_level: 0.96 + vlt: 2 + SKU911: + cost: 461 + init_stock: 1400 + price: 461 + service_level: 0.96 + vlt: 2 + SKU912: + cost: 236 + init_stock: 540 + price: 236 + service_level: 0.96 + vlt: 3 + SKU913: + cost: 322 + init_stock: 920 + price: 322 + service_level: 0.96 + vlt: 2 + SKU914: + cost: 272 + init_stock: 1240 + price: 272 + service_level: 0.96 + vlt: 3 + SKU915: + cost: 337 + init_stock: 1120 + price: 337 + service_level: 0.96 + vlt: 3 + SKU916: + cost: 337 + init_stock: 1780 + price: 337 + service_level: 0.96 + vlt: 3 + SKU917: + cost: 258 + init_stock: 600 + price: 258 + service_level: 0.96 + vlt: 3 + SKU918: + cost: 148 + init_stock: 1100 + price: 148 + service_level: 0.96 + vlt: 1 + SKU919: + cost: 393 + init_stock: 500 + price: 393 + service_level: 0.96 + vlt: 3 + SKU92: + cost: 262 + init_stock: 1260 + price: 262 + service_level: 0.96 + vlt: 2 + SKU920: + cost: 357 + init_stock: 1720 + price: 357 + service_level: 0.96 + vlt: 3 + SKU921: + cost: 227 + init_stock: 1320 + price: 227 + service_level: 0.96 + vlt: 2 + SKU922: + cost: 112 + init_stock: 1340 + price: 112 + service_level: 0.96 + vlt: 2 + SKU923: + cost: 496 + init_stock: 1160 + price: 496 + service_level: 0.96 + vlt: 2 + SKU924: + cost: 316 + init_stock: 1700 + price: 316 + service_level: 0.96 + vlt: 3 + SKU925: + cost: 360 + init_stock: 300 + price: 360 + service_level: 0.96 + vlt: 1 + SKU926: + cost: 360 + init_stock: 1340 + price: 360 + service_level: 0.96 + vlt: 2 + SKU927: + cost: 260 + init_stock: 420 + price: 260 + service_level: 0.96 + vlt: 3 + SKU928: + cost: 491 + init_stock: 1660 + price: 491 + service_level: 0.96 + vlt: 1 + SKU929: + cost: 359 + init_stock: 2000 + price: 359 + service_level: 0.96 + vlt: 3 + SKU93: + cost: 404 + init_stock: 1340 + price: 404 + service_level: 0.96 + vlt: 2 + SKU930: + cost: 198 + init_stock: 560 + price: 198 + service_level: 0.96 + vlt: 2 + SKU931: + cost: 71 + init_stock: 280 + price: 71 + service_level: 0.96 + vlt: 3 + SKU932: + cost: 163 + init_stock: 1320 + price: 163 + service_level: 0.96 + vlt: 2 + SKU933: + cost: 113 + init_stock: 1560 + price: 113 + service_level: 0.96 + vlt: 1 + SKU934: + cost: 219 + init_stock: 1340 + price: 219 + service_level: 0.96 + vlt: 3 + SKU935: + cost: 364 + init_stock: 1880 + price: 364 + service_level: 0.96 + vlt: 1 + SKU936: + cost: 24 + init_stock: 100 + price: 24 + service_level: 0.96 + vlt: 3 + SKU937: + cost: 135 + init_stock: 340 + price: 135 + service_level: 0.96 + vlt: 1 + SKU938: + cost: 432 + init_stock: 420 + price: 432 + service_level: 0.96 + vlt: 2 + SKU939: + cost: 173 + init_stock: 1180 + price: 173 + service_level: 0.96 + vlt: 3 + SKU94: + cost: 184 + init_stock: 940 + price: 184 + service_level: 0.96 + vlt: 3 + SKU940: + cost: 14 + init_stock: 1860 + price: 14 + service_level: 0.96 + vlt: 1 + SKU941: + cost: 80 + init_stock: 1140 + price: 80 + service_level: 0.96 + vlt: 3 + SKU942: + cost: 202 + init_stock: 260 + price: 202 + service_level: 0.96 + vlt: 3 + SKU943: + cost: 138 + init_stock: 980 + price: 138 + service_level: 0.96 + vlt: 1 + SKU944: + cost: 196 + init_stock: 880 + price: 196 + service_level: 0.96 + vlt: 1 + SKU945: + cost: 141 + init_stock: 340 + price: 141 + service_level: 0.96 + vlt: 2 + SKU946: + cost: 325 + init_stock: 980 + price: 325 + service_level: 0.96 + vlt: 1 + SKU947: + cost: 338 + init_stock: 1820 + price: 338 + service_level: 0.96 + vlt: 3 + SKU948: + cost: 425 + init_stock: 560 + price: 425 + service_level: 0.96 + vlt: 3 + SKU949: + cost: 309 + init_stock: 100 + price: 309 + service_level: 0.96 + vlt: 2 + SKU95: + cost: 136 + init_stock: 420 + price: 136 + service_level: 0.96 + vlt: 3 + SKU950: + cost: 102 + init_stock: 1080 + price: 102 + service_level: 0.96 + vlt: 2 + SKU951: + cost: 75 + init_stock: 360 + price: 75 + service_level: 0.96 + vlt: 2 + SKU952: + cost: 156 + init_stock: 220 + price: 156 + service_level: 0.96 + vlt: 3 + SKU953: + cost: 138 + init_stock: 700 + price: 138 + service_level: 0.96 + vlt: 3 + SKU954: + cost: 296 + init_stock: 1720 + price: 296 + service_level: 0.96 + vlt: 1 + SKU955: + cost: 55 + init_stock: 1260 + price: 55 + service_level: 0.96 + vlt: 1 + SKU956: + cost: 282 + init_stock: 1700 + price: 282 + service_level: 0.96 + vlt: 1 + SKU957: + cost: 305 + init_stock: 1020 + price: 305 + service_level: 0.96 + vlt: 2 + SKU958: + cost: 369 + init_stock: 1320 + price: 369 + service_level: 0.96 + vlt: 2 + SKU959: + cost: 81 + init_stock: 1640 + price: 81 + service_level: 0.96 + vlt: 1 + SKU96: + cost: 176 + init_stock: 880 + price: 176 + service_level: 0.96 + vlt: 3 + SKU960: + cost: 147 + init_stock: 120 + price: 147 + service_level: 0.96 + vlt: 3 + SKU961: + cost: 264 + init_stock: 880 + price: 264 + service_level: 0.96 + vlt: 1 + SKU962: + cost: 354 + init_stock: 880 + price: 354 + service_level: 0.96 + vlt: 1 + SKU963: + cost: 349 + init_stock: 420 + price: 349 + service_level: 0.96 + vlt: 1 + SKU964: + cost: 244 + init_stock: 1460 + price: 244 + service_level: 0.96 + vlt: 1 + SKU965: + cost: 124 + init_stock: 1020 + price: 124 + service_level: 0.96 + vlt: 1 + SKU966: + cost: 302 + init_stock: 880 + price: 302 + service_level: 0.96 + vlt: 3 + SKU967: + cost: 67 + init_stock: 900 + price: 67 + service_level: 0.96 + vlt: 3 + SKU968: + cost: 281 + init_stock: 980 + price: 281 + service_level: 0.96 + vlt: 2 + SKU969: + cost: 249 + init_stock: 120 + price: 249 + service_level: 0.96 + vlt: 1 + SKU97: + cost: 28 + init_stock: 600 + price: 28 + service_level: 0.96 + vlt: 3 + SKU970: + cost: 244 + init_stock: 100 + price: 244 + service_level: 0.96 + vlt: 2 + SKU971: + cost: 368 + init_stock: 1460 + price: 368 + service_level: 0.96 + vlt: 1 + SKU972: + cost: 209 + init_stock: 1380 + price: 209 + service_level: 0.96 + vlt: 1 + SKU973: + cost: 271 + init_stock: 220 + price: 271 + service_level: 0.96 + vlt: 2 + SKU974: + cost: 170 + init_stock: 640 + price: 170 + service_level: 0.96 + vlt: 1 + SKU975: + cost: 198 + init_stock: 440 + price: 198 + service_level: 0.96 + vlt: 3 + SKU976: + cost: 178 + init_stock: 300 + price: 178 + service_level: 0.96 + vlt: 2 + SKU977: + cost: 234 + init_stock: 1080 + price: 234 + service_level: 0.96 + vlt: 3 + SKU978: + cost: 470 + init_stock: 1280 + price: 470 + service_level: 0.96 + vlt: 3 + SKU979: + cost: 443 + init_stock: 1920 + price: 443 + service_level: 0.96 + vlt: 2 + SKU98: + cost: 443 + init_stock: 700 + price: 443 + service_level: 0.96 + vlt: 1 + SKU980: + cost: 39 + init_stock: 1360 + price: 39 + service_level: 0.96 + vlt: 3 + SKU981: + cost: 482 + init_stock: 1440 + price: 482 + service_level: 0.96 + vlt: 2 + SKU982: + cost: 213 + init_stock: 1040 + price: 213 + service_level: 0.96 + vlt: 3 + SKU983: + cost: 449 + init_stock: 1840 + price: 449 + service_level: 0.96 + vlt: 1 + SKU984: + cost: 232 + init_stock: 1820 + price: 232 + service_level: 0.96 + vlt: 3 + SKU985: + cost: 290 + init_stock: 1020 + price: 290 + service_level: 0.96 + vlt: 1 + SKU986: + cost: 275 + init_stock: 340 + price: 275 + service_level: 0.96 + vlt: 3 + SKU987: + cost: 434 + init_stock: 680 + price: 434 + service_level: 0.96 + vlt: 1 + SKU988: + cost: 102 + init_stock: 460 + price: 102 + service_level: 0.96 + vlt: 1 + SKU989: + cost: 484 + init_stock: 1860 + price: 484 + service_level: 0.96 + vlt: 1 + SKU99: + cost: 361 + init_stock: 900 + price: 361 + service_level: 0.96 + vlt: 3 + SKU990: + cost: 108 + init_stock: 1140 + price: 108 + service_level: 0.96 + vlt: 3 + SKU991: + cost: 409 + init_stock: 380 + price: 409 + service_level: 0.96 + vlt: 3 + SKU992: + cost: 434 + init_stock: 1860 + price: 434 + service_level: 0.96 + vlt: 3 + SKU993: + cost: 145 + init_stock: 1720 + price: 145 + service_level: 0.96 + vlt: 2 + SKU994: + cost: 470 + init_stock: 1000 + price: 470 + service_level: 0.96 + vlt: 3 + SKU995: + cost: 241 + init_stock: 1520 + price: 241 + service_level: 0.96 + vlt: 2 + SKU996: + cost: 260 + init_stock: 1460 + price: 260 + service_level: 0.96 + vlt: 3 + SKU997: + cost: 400 + init_stock: 680 + price: 400 + service_level: 0.96 + vlt: 1 + SKU998: + cost: 447 + init_stock: 1100 + price: 447 + service_level: 0.96 + vlt: 2 + SKU999: + cost: 79 + init_stock: 460 + price: 79 + service_level: 0.96 + vlt: 2 + - children: + storage: + config: + capacity: 1064900 + unit_storage_cost: 1 + config: + order_cost: 500 + definition_ref: RetailerFacility + name: STORE0 + skus: + SKU0: + constraint: null + cost: 322 + init_stock: 126 + max_stock: 1000 + price: 631 + sale_gamma: 63 + service_level: 0.95 + SKU1: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 284 + init_stock: 176 + max_stock: 1000 + price: 448 + sale_gamma: 22 + service_level: 0.95 + SKU10: + constraint: null + cost: 68 + init_stock: 150 + max_stock: 1000 + price: 116 + sale_gamma: 25 + service_level: 0.95 + SKU100: + constraint: G(low_profit -> low_stock_constraint) + cost: 16 + init_stock: 390 + max_stock: 1000 + price: 23 + sale_gamma: 65 + service_level: 0.95 + SKU101: + constraint: null + cost: 84 + init_stock: 497 + max_stock: 1000 + price: 149 + sale_gamma: 71 + service_level: 0.95 + SKU102: + constraint: null + cost: 328 + init_stock: 558 + max_stock: 1000 + price: 505 + sale_gamma: 93 + service_level: 0.95 + SKU103: + constraint: null + cost: 334 + init_stock: 285 + max_stock: 1000 + price: 601 + sale_gamma: 95 + service_level: 0.95 + SKU104: + constraint: null + cost: 49 + init_stock: 325 + max_stock: 1000 + price: 57 + sale_gamma: 65 + service_level: 0.95 + SKU105: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 110 + init_stock: 285 + max_stock: 1000 + price: 171 + sale_gamma: 57 + service_level: 0.95 + SKU106: + constraint: G(low_profit -> low_stock_constraint) + cost: 251 + init_stock: 584 + max_stock: 1000 + price: 454 + sale_gamma: 73 + service_level: 0.95 + SKU107: + constraint: G(stock_constraint) + cost: 423 + init_stock: 348 + max_stock: 1000 + price: 706 + sale_gamma: 87 + service_level: 0.95 + SKU108: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 458 + init_stock: 368 + max_stock: 1000 + price: 801 + sale_gamma: 92 + service_level: 0.95 + SKU109: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 88 + init_stock: 328 + max_stock: 1000 + price: 98 + sale_gamma: 82 + service_level: 0.95 + SKU11: + constraint: null + cost: 400 + init_stock: 306 + max_stock: 1000 + price: 680 + sale_gamma: 51 + service_level: 0.95 + SKU110: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 66 + init_stock: 112 + max_stock: 1000 + price: 100 + sale_gamma: 14 + service_level: 0.95 + SKU111: + constraint: G(stock_constraint) + cost: 260 + init_stock: 183 + max_stock: 1000 + price: 364 + sale_gamma: 61 + service_level: 0.95 + SKU112: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 61 + init_stock: 475 + max_stock: 1000 + price: 119 + sale_gamma: 95 + service_level: 0.95 + SKU113: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 155 + max_stock: 1000 + price: 678 + sale_gamma: 31 + service_level: 0.95 + SKU114: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 389 + init_stock: 162 + max_stock: 1000 + price: 564 + sale_gamma: 27 + service_level: 0.95 + SKU115: + constraint: G(low_profit -> low_stock_constraint) + cost: 286 + init_stock: 258 + max_stock: 1000 + price: 557 + sale_gamma: 86 + service_level: 0.95 + SKU116: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 496 + init_stock: 288 + max_stock: 1000 + price: 768 + sale_gamma: 72 + service_level: 0.95 + SKU117: + constraint: null + cost: 320 + init_stock: 644 + max_stock: 1000 + price: 374 + sale_gamma: 92 + service_level: 0.95 + SKU118: + constraint: null + cost: 183 + init_stock: 66 + max_stock: 1000 + price: 208 + sale_gamma: 33 + service_level: 0.95 + SKU119: + constraint: null + cost: 209 + init_stock: 256 + max_stock: 1000 + price: 317 + sale_gamma: 32 + service_level: 0.95 + SKU12: + constraint: null + cost: 112 + init_stock: 336 + max_stock: 1000 + price: 210 + sale_gamma: 84 + service_level: 0.95 + SKU120: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 121 + init_stock: 392 + max_stock: 1000 + price: 151 + sale_gamma: 98 + service_level: 0.95 + SKU121: + constraint: null + cost: 40 + init_stock: 510 + max_stock: 1000 + price: 54 + sale_gamma: 85 + service_level: 0.95 + SKU122: + constraint: G(stock_constraint) + cost: 437 + init_stock: 35 + max_stock: 1000 + price: 589 + sale_gamma: 7 + service_level: 0.95 + SKU123: + constraint: G(stock_constraint) + cost: 233 + init_stock: 114 + max_stock: 1000 + price: 326 + sale_gamma: 19 + service_level: 0.95 + SKU124: + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 108 + max_stock: 1000 + price: 298 + sale_gamma: 36 + service_level: 0.95 + SKU125: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 16 + init_stock: 368 + max_stock: 1000 + price: 32 + sale_gamma: 92 + service_level: 0.95 + SKU126: + constraint: null + cost: 36 + init_stock: 156 + max_stock: 1000 + price: 40 + sale_gamma: 39 + service_level: 0.95 + SKU127: + constraint: G(stock_constraint) + cost: 217 + init_stock: 155 + max_stock: 1000 + price: 366 + sale_gamma: 31 + service_level: 0.95 + SKU128: + constraint: null + cost: 165 + init_stock: 95 + max_stock: 1000 + price: 283 + sale_gamma: 19 + service_level: 0.95 + SKU129: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 300 + max_stock: 1000 + price: 203 + sale_gamma: 50 + service_level: 0.95 + SKU13: + constraint: null + cost: 317 + init_stock: 456 + max_stock: 1000 + price: 573 + sale_gamma: 57 + service_level: 0.95 + SKU130: + constraint: null + cost: 348 + init_stock: 784 + max_stock: 1000 + price: 396 + sale_gamma: 98 + service_level: 0.95 + SKU131: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 64 + init_stock: 270 + max_stock: 1000 + price: 110 + sale_gamma: 45 + service_level: 0.95 + SKU132: + constraint: null + cost: 427 + init_stock: 105 + max_stock: 1000 + price: 678 + sale_gamma: 21 + service_level: 0.95 + SKU133: + constraint: null + cost: 224 + init_stock: 145 + max_stock: 1000 + price: 448 + sale_gamma: 29 + service_level: 0.95 + SKU134: + constraint: G(stock_constraint) + cost: 336 + init_stock: 539 + max_stock: 1000 + price: 470 + sale_gamma: 77 + service_level: 0.95 + SKU135: + constraint: null + cost: 153 + init_stock: 500 + max_stock: 1000 + price: 243 + sale_gamma: 100 + service_level: 0.95 + SKU136: + constraint: G(low_profit -> low_stock_constraint) + cost: 199 + init_stock: 497 + max_stock: 1000 + price: 370 + sale_gamma: 71 + service_level: 0.95 + SKU137: + constraint: G(stock_constraint) + cost: 93 + init_stock: 444 + max_stock: 1000 + price: 165 + sale_gamma: 74 + service_level: 0.95 + SKU138: + constraint: G(stock_constraint) + cost: 228 + init_stock: 180 + max_stock: 1000 + price: 253 + sale_gamma: 36 + service_level: 0.95 + SKU139: + constraint: G(stock_constraint) + cost: 207 + init_stock: 144 + max_stock: 1000 + price: 368 + sale_gamma: 24 + service_level: 0.95 + SKU14: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 268 + init_stock: 372 + max_stock: 1000 + price: 305 + sale_gamma: 62 + service_level: 0.95 + SKU140: + constraint: null + cost: 261 + init_stock: 238 + max_stock: 1000 + price: 357 + sale_gamma: 34 + service_level: 0.95 + SKU141: + constraint: null + cost: 190 + init_stock: 164 + max_stock: 1000 + price: 370 + sale_gamma: 41 + service_level: 0.95 + SKU142: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 320 + init_stock: 152 + max_stock: 1000 + price: 428 + sale_gamma: 38 + service_level: 0.95 + SKU143: + constraint: G(stock_constraint) + cost: 318 + init_stock: 182 + max_stock: 1000 + price: 566 + sale_gamma: 26 + service_level: 0.95 + SKU144: + constraint: null + cost: 400 + init_stock: 24 + max_stock: 1000 + price: 716 + sale_gamma: 12 + service_level: 0.95 + SKU145: + constraint: null + cost: 399 + init_stock: 328 + max_stock: 1000 + price: 774 + sale_gamma: 82 + service_level: 0.95 + SKU146: + constraint: null + cost: 177 + init_stock: 192 + max_stock: 1000 + price: 194 + sale_gamma: 48 + service_level: 0.95 + SKU147: + constraint: G(low_profit -> low_stock_constraint) + cost: 472 + init_stock: 392 + max_stock: 1000 + price: 840 + sale_gamma: 56 + service_level: 0.95 + SKU148: + constraint: G(stock_constraint) + cost: 313 + init_stock: 308 + max_stock: 1000 + price: 566 + sale_gamma: 77 + service_level: 0.95 + SKU149: + constraint: G(low_profit -> low_stock_constraint) + cost: 357 + init_stock: 539 + max_stock: 1000 + price: 549 + sale_gamma: 77 + service_level: 0.95 + SKU15: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 25 + max_stock: 1000 + price: 58 + sale_gamma: 5 + service_level: 0.95 + SKU150: + constraint: null + cost: 106 + init_stock: 210 + max_stock: 1000 + price: 159 + sale_gamma: 70 + service_level: 0.95 + SKU151: + constraint: G(low_profit -> low_stock_constraint) + cost: 223 + init_stock: 146 + max_stock: 1000 + price: 370 + sale_gamma: 73 + service_level: 0.95 + SKU152: + constraint: null + cost: 10 + init_stock: 408 + max_stock: 1000 + price: 14 + sale_gamma: 51 + service_level: 0.95 + SKU153: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 441 + init_stock: 372 + max_stock: 1000 + price: 538 + sale_gamma: 62 + service_level: 0.95 + SKU154: + constraint: null + cost: 77 + init_stock: 680 + max_stock: 1000 + price: 135 + sale_gamma: 85 + service_level: 0.95 + SKU155: + constraint: G(low_profit -> low_stock_constraint) + cost: 422 + init_stock: 159 + max_stock: 1000 + price: 641 + sale_gamma: 53 + service_level: 0.95 + SKU156: + constraint: null + cost: 10 + init_stock: 36 + max_stock: 1000 + price: 16 + sale_gamma: 12 + service_level: 0.95 + SKU157: + constraint: null + cost: 410 + init_stock: 525 + max_stock: 1000 + price: 594 + sale_gamma: 75 + service_level: 0.95 + SKU158: + constraint: null + cost: 145 + init_stock: 486 + max_stock: 1000 + price: 275 + sale_gamma: 81 + service_level: 0.95 + SKU159: + constraint: G(stock_constraint) + cost: 193 + init_stock: 100 + max_stock: 1000 + price: 368 + sale_gamma: 25 + service_level: 0.95 + SKU16: + constraint: null + cost: 175 + init_stock: 464 + max_stock: 1000 + price: 344 + sale_gamma: 58 + service_level: 0.95 + SKU160: + constraint: G(low_profit -> low_stock_constraint) + cost: 459 + init_stock: 510 + max_stock: 1000 + price: 876 + sale_gamma: 85 + service_level: 0.95 + SKU161: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 239 + init_stock: 230 + max_stock: 1000 + price: 478 + sale_gamma: 46 + service_level: 0.95 + SKU162: + constraint: null + cost: 158 + init_stock: 30 + max_stock: 1000 + price: 290 + sale_gamma: 5 + service_level: 0.95 + SKU163: + constraint: G(stock_constraint) + cost: 486 + init_stock: 273 + max_stock: 1000 + price: 704 + sale_gamma: 39 + service_level: 0.95 + SKU164: + constraint: null + cost: 496 + init_stock: 500 + max_stock: 1000 + price: 615 + sale_gamma: 100 + service_level: 0.95 + SKU165: + constraint: G(stock_constraint) + cost: 274 + init_stock: 231 + max_stock: 1000 + price: 548 + sale_gamma: 33 + service_level: 0.95 + SKU166: + constraint: null + cost: 79 + init_stock: 178 + max_stock: 1000 + price: 137 + sale_gamma: 89 + service_level: 0.95 + SKU167: + constraint: G(stock_constraint) + cost: 443 + init_stock: 52 + max_stock: 1000 + price: 854 + sale_gamma: 13 + service_level: 0.95 + SKU168: + constraint: null + cost: 357 + init_stock: 522 + max_stock: 1000 + price: 546 + sale_gamma: 87 + service_level: 0.95 + SKU169: + constraint: null + cost: 369 + init_stock: 686 + max_stock: 1000 + price: 505 + sale_gamma: 98 + service_level: 0.95 + SKU17: + constraint: null + cost: 346 + init_stock: 18 + max_stock: 1000 + price: 553 + sale_gamma: 9 + service_level: 0.95 + SKU170: + constraint: null + cost: 68 + init_stock: 275 + max_stock: 1000 + price: 113 + sale_gamma: 55 + service_level: 0.95 + SKU171: + constraint: G(low_profit -> low_stock_constraint) + cost: 398 + init_stock: 380 + max_stock: 1000 + price: 704 + sale_gamma: 76 + service_level: 0.95 + SKU172: + constraint: null + cost: 200 + init_stock: 426 + max_stock: 1000 + price: 222 + sale_gamma: 71 + service_level: 0.95 + SKU173: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 175 + init_stock: 480 + max_stock: 1000 + price: 264 + sale_gamma: 96 + service_level: 0.95 + SKU174: + constraint: null + cost: 291 + init_stock: 380 + max_stock: 1000 + price: 582 + sale_gamma: 76 + service_level: 0.95 + SKU175: + constraint: null + cost: 84 + init_stock: 225 + max_stock: 1000 + price: 140 + sale_gamma: 75 + service_level: 0.95 + SKU176: + constraint: G(stock_constraint) + cost: 407 + init_stock: 330 + max_stock: 1000 + price: 610 + sale_gamma: 66 + service_level: 0.95 + SKU177: + constraint: null + cost: 257 + init_stock: 155 + max_stock: 1000 + price: 346 + sale_gamma: 31 + service_level: 0.95 + SKU178: + constraint: null + cost: 423 + init_stock: 20 + max_stock: 1000 + price: 499 + sale_gamma: 5 + service_level: 0.95 + SKU179: + constraint: null + cost: 497 + init_stock: 415 + max_stock: 1000 + price: 690 + sale_gamma: 83 + service_level: 0.95 + SKU18: + constraint: null + cost: 258 + init_stock: 405 + max_stock: 1000 + price: 387 + sale_gamma: 81 + service_level: 0.95 + SKU180: + constraint: null + cost: 217 + init_stock: 165 + max_stock: 1000 + price: 297 + sale_gamma: 55 + service_level: 0.95 + SKU181: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 300 + max_stock: 1000 + price: 214 + sale_gamma: 60 + service_level: 0.95 + SKU182: + constraint: null + cost: 437 + init_stock: 693 + max_stock: 1000 + price: 764 + sale_gamma: 99 + service_level: 0.95 + SKU183: + constraint: null + cost: 145 + init_stock: 56 + max_stock: 1000 + price: 178 + sale_gamma: 8 + service_level: 0.95 + SKU184: + constraint: null + cost: 73 + init_stock: 72 + max_stock: 1000 + price: 100 + sale_gamma: 24 + service_level: 0.95 + SKU185: + constraint: null + cost: 10 + init_stock: 360 + max_stock: 1000 + price: 14 + sale_gamma: 90 + service_level: 0.95 + SKU186: + constraint: null + cost: 359 + init_stock: 66 + max_stock: 1000 + price: 649 + sale_gamma: 22 + service_level: 0.95 + SKU187: + constraint: G(low_profit -> low_stock_constraint) + cost: 177 + init_stock: 120 + max_stock: 1000 + price: 249 + sale_gamma: 30 + service_level: 0.95 + SKU188: + constraint: null + cost: 391 + init_stock: 348 + max_stock: 1000 + price: 586 + sale_gamma: 87 + service_level: 0.95 + SKU189: + constraint: G(low_profit -> low_stock_constraint) + cost: 358 + init_stock: 210 + max_stock: 1000 + price: 400 + sale_gamma: 35 + service_level: 0.95 + SKU19: + constraint: G(stock_constraint) + cost: 477 + init_stock: 364 + max_stock: 1000 + price: 791 + sale_gamma: 91 + service_level: 0.95 + SKU190: + constraint: null + cost: 113 + init_stock: 102 + max_stock: 1000 + price: 153 + sale_gamma: 17 + service_level: 0.95 + SKU191: + constraint: G(stock_constraint) + cost: 473 + init_stock: 324 + max_stock: 1000 + price: 638 + sale_gamma: 54 + service_level: 0.95 + SKU192: + constraint: null + cost: 415 + init_stock: 244 + max_stock: 1000 + price: 539 + sale_gamma: 61 + service_level: 0.95 + SKU193: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 207 + init_stock: 120 + max_stock: 1000 + price: 353 + sale_gamma: 30 + service_level: 0.95 + SKU194: + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 30 + max_stock: 1000 + price: 803 + sale_gamma: 5 + service_level: 0.95 + SKU195: + constraint: G(low_profit -> low_stock_constraint) + cost: 218 + init_stock: 93 + max_stock: 1000 + price: 248 + sale_gamma: 31 + service_level: 0.95 + SKU196: + constraint: null + cost: 49 + init_stock: 544 + max_stock: 1000 + price: 62 + sale_gamma: 68 + service_level: 0.95 + SKU197: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 303 + init_stock: 285 + max_stock: 1000 + price: 509 + sale_gamma: 57 + service_level: 0.95 + SKU198: + constraint: G(low_profit -> low_stock_constraint) + cost: 169 + init_stock: 378 + max_stock: 1000 + price: 307 + sale_gamma: 54 + service_level: 0.95 + SKU199: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 138 + max_stock: 1000 + price: 794 + sale_gamma: 23 + service_level: 0.95 + SKU2: + constraint: null + cost: 331 + init_stock: 350 + max_stock: 1000 + price: 499 + sale_gamma: 70 + service_level: 0.95 + SKU20: + constraint: G(stock_constraint) + cost: 335 + init_stock: 200 + max_stock: 1000 + price: 649 + sale_gamma: 25 + service_level: 0.95 + SKU200: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 65 + init_stock: 150 + max_stock: 1000 + price: 85 + sale_gamma: 25 + service_level: 0.95 + SKU201: + constraint: G(stock_constraint) + cost: 104 + init_stock: 354 + max_stock: 1000 + price: 161 + sale_gamma: 59 + service_level: 0.95 + SKU202: + constraint: null + cost: 142 + init_stock: 365 + max_stock: 1000 + price: 222 + sale_gamma: 73 + service_level: 0.95 + SKU203: + constraint: null + cost: 440 + init_stock: 328 + max_stock: 1000 + price: 677 + sale_gamma: 82 + service_level: 0.95 + SKU204: + constraint: null + cost: 489 + init_stock: 188 + max_stock: 1000 + price: 831 + sale_gamma: 47 + service_level: 0.95 + SKU205: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 130 + init_stock: 500 + max_stock: 1000 + price: 175 + sale_gamma: 100 + service_level: 0.95 + SKU206: + constraint: null + cost: 335 + init_stock: 55 + max_stock: 1000 + price: 552 + sale_gamma: 11 + service_level: 0.95 + SKU207: + constraint: G(low_profit -> low_stock_constraint) + cost: 140 + init_stock: 240 + max_stock: 1000 + price: 170 + sale_gamma: 80 + service_level: 0.95 + SKU208: + constraint: null + cost: 491 + init_stock: 308 + max_stock: 1000 + price: 927 + sale_gamma: 77 + service_level: 0.95 + SKU209: + constraint: G(stock_constraint) + cost: 179 + init_stock: 120 + max_stock: 1000 + price: 322 + sale_gamma: 20 + service_level: 0.95 + SKU21: + constraint: null + cost: 123 + init_stock: 400 + max_stock: 1000 + price: 225 + sale_gamma: 100 + service_level: 0.95 + SKU210: + constraint: null + cost: 404 + init_stock: 345 + max_stock: 1000 + price: 468 + sale_gamma: 69 + service_level: 0.95 + SKU211: + constraint: null + cost: 174 + init_stock: 364 + max_stock: 1000 + price: 226 + sale_gamma: 91 + service_level: 0.95 + SKU212: + constraint: null + cost: 405 + init_stock: 632 + max_stock: 1000 + price: 534 + sale_gamma: 79 + service_level: 0.95 + SKU213: + constraint: null + cost: 121 + init_stock: 192 + max_stock: 1000 + price: 229 + sale_gamma: 64 + service_level: 0.95 + SKU214: + constraint: G(stock_constraint) + cost: 101 + init_stock: 60 + max_stock: 1000 + price: 144 + sale_gamma: 10 + service_level: 0.95 + SKU215: + constraint: null + cost: 419 + init_stock: 94 + max_stock: 1000 + price: 469 + sale_gamma: 47 + service_level: 0.95 + SKU216: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 330 + init_stock: 92 + max_stock: 1000 + price: 432 + sale_gamma: 23 + service_level: 0.95 + SKU217: + constraint: null + cost: 284 + init_stock: 455 + max_stock: 1000 + price: 312 + sale_gamma: 65 + service_level: 0.95 + SKU218: + constraint: G(low_profit -> low_stock_constraint) + cost: 205 + init_stock: 354 + max_stock: 1000 + price: 397 + sale_gamma: 59 + service_level: 0.95 + SKU219: + constraint: G(stock_constraint) + cost: 92 + init_stock: 368 + max_stock: 1000 + price: 135 + sale_gamma: 46 + service_level: 0.95 + SKU22: + constraint: null + cost: 493 + init_stock: 264 + max_stock: 1000 + price: 986 + sale_gamma: 66 + service_level: 0.95 + SKU220: + constraint: null + cost: 387 + init_stock: 435 + max_stock: 1000 + price: 572 + sale_gamma: 87 + service_level: 0.95 + SKU221: + constraint: null + cost: 39 + init_stock: 468 + max_stock: 1000 + price: 48 + sale_gamma: 78 + service_level: 0.95 + SKU222: + constraint: G(low_profit -> low_stock_constraint) + cost: 115 + init_stock: 180 + max_stock: 1000 + price: 210 + sale_gamma: 36 + service_level: 0.95 + SKU223: + constraint: G(low_profit -> low_stock_constraint) + cost: 196 + init_stock: 36 + max_stock: 1000 + price: 286 + sale_gamma: 12 + service_level: 0.95 + SKU224: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 387 + init_stock: 25 + max_stock: 1000 + price: 661 + sale_gamma: 5 + service_level: 0.95 + SKU225: + constraint: null + cost: 164 + init_stock: 116 + max_stock: 1000 + price: 216 + sale_gamma: 29 + service_level: 0.95 + SKU226: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 206 + init_stock: 65 + max_stock: 1000 + price: 350 + sale_gamma: 13 + service_level: 0.95 + SKU227: + constraint: G(low_profit -> low_stock_constraint) + cost: 479 + init_stock: 144 + max_stock: 1000 + price: 895 + sale_gamma: 24 + service_level: 0.95 + SKU228: + constraint: null + cost: 14 + init_stock: 180 + max_stock: 1000 + price: 21 + sale_gamma: 90 + service_level: 0.95 + SKU229: + constraint: null + cost: 472 + init_stock: 176 + max_stock: 1000 + price: 708 + sale_gamma: 44 + service_level: 0.95 + SKU23: + constraint: null + cost: 387 + init_stock: 210 + max_stock: 1000 + price: 700 + sale_gamma: 42 + service_level: 0.95 + SKU230: + constraint: null + cost: 241 + init_stock: 138 + max_stock: 1000 + price: 436 + sale_gamma: 23 + service_level: 0.95 + SKU231: + constraint: G(stock_constraint) + cost: 48 + init_stock: 486 + max_stock: 1000 + price: 96 + sale_gamma: 81 + service_level: 0.95 + SKU232: + constraint: G(low_profit -> low_stock_constraint) + cost: 224 + init_stock: 480 + max_stock: 1000 + price: 338 + sale_gamma: 96 + service_level: 0.95 + SKU233: + constraint: G(low_profit -> low_stock_constraint) + cost: 360 + init_stock: 300 + max_stock: 1000 + price: 428 + sale_gamma: 75 + service_level: 0.95 + SKU234: + constraint: null + cost: 287 + init_stock: 25 + max_stock: 1000 + price: 387 + sale_gamma: 5 + service_level: 0.95 + SKU235: + constraint: G(low_profit -> low_stock_constraint) + cost: 24 + init_stock: 228 + max_stock: 1000 + price: 32 + sale_gamma: 57 + service_level: 0.95 + SKU236: + constraint: G(low_profit -> low_stock_constraint) + cost: 155 + init_stock: 165 + max_stock: 1000 + price: 289 + sale_gamma: 55 + service_level: 0.95 + SKU237: + constraint: null + cost: 433 + init_stock: 270 + max_stock: 1000 + price: 779 + sale_gamma: 45 + service_level: 0.95 + SKU238: + constraint: G(stock_constraint) + cost: 64 + init_stock: 264 + max_stock: 1000 + price: 112 + sale_gamma: 66 + service_level: 0.95 + SKU239: + constraint: null + cost: 103 + init_stock: 490 + max_stock: 1000 + price: 139 + sale_gamma: 98 + service_level: 0.95 + SKU24: + constraint: null + cost: 97 + init_stock: 329 + max_stock: 1000 + price: 114 + sale_gamma: 47 + service_level: 0.95 + SKU240: + constraint: null + cost: 373 + init_stock: 188 + max_stock: 1000 + price: 738 + sale_gamma: 47 + service_level: 0.95 + SKU241: + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 213 + max_stock: 1000 + price: 860 + sale_gamma: 71 + service_level: 0.95 + SKU242: + constraint: null + cost: 17 + init_stock: 88 + max_stock: 1000 + price: 29 + sale_gamma: 44 + service_level: 0.95 + SKU243: + constraint: null + cost: 352 + init_stock: 84 + max_stock: 1000 + price: 394 + sale_gamma: 14 + service_level: 0.95 + SKU244: + constraint: null + cost: 174 + init_stock: 410 + max_stock: 1000 + price: 226 + sale_gamma: 82 + service_level: 0.95 + SKU245: + constraint: G(low_profit -> low_stock_constraint) + cost: 404 + init_stock: 198 + max_stock: 1000 + price: 525 + sale_gamma: 66 + service_level: 0.95 + SKU246: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 300 + init_stock: 210 + max_stock: 1000 + price: 474 + sale_gamma: 30 + service_level: 0.95 + SKU247: + constraint: null + cost: 386 + init_stock: 210 + max_stock: 1000 + price: 497 + sale_gamma: 35 + service_level: 0.95 + SKU248: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 355 + init_stock: 174 + max_stock: 1000 + price: 539 + sale_gamma: 29 + service_level: 0.95 + SKU249: + constraint: null + cost: 356 + init_stock: 100 + max_stock: 1000 + price: 544 + sale_gamma: 25 + service_level: 0.95 + SKU25: + constraint: G(stock_constraint) + cost: 161 + init_stock: 35 + max_stock: 1000 + price: 249 + sale_gamma: 5 + service_level: 0.95 + SKU250: + constraint: null + cost: 127 + init_stock: 324 + max_stock: 1000 + price: 186 + sale_gamma: 54 + service_level: 0.95 + SKU251: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 344 + init_stock: 244 + max_stock: 1000 + price: 543 + sale_gamma: 61 + service_level: 0.95 + SKU252: + constraint: null + cost: 181 + init_stock: 415 + max_stock: 1000 + price: 334 + sale_gamma: 83 + service_level: 0.95 + SKU253: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 448 + init_stock: 32 + max_stock: 1000 + price: 864 + sale_gamma: 16 + service_level: 0.95 + SKU254: + constraint: null + cost: 484 + init_stock: 184 + max_stock: 1000 + price: 696 + sale_gamma: 46 + service_level: 0.95 + SKU255: + constraint: null + cost: 290 + init_stock: 335 + max_stock: 1000 + price: 568 + sale_gamma: 67 + service_level: 0.95 + SKU256: + constraint: null + cost: 91 + init_stock: 360 + max_stock: 1000 + price: 167 + sale_gamma: 72 + service_level: 0.95 + SKU257: + constraint: null + cost: 348 + init_stock: 171 + max_stock: 1000 + price: 497 + sale_gamma: 57 + service_level: 0.95 + SKU258: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 258 + max_stock: 1000 + price: 689 + sale_gamma: 43 + service_level: 0.95 + SKU259: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 207 + max_stock: 1000 + price: 559 + sale_gamma: 69 + service_level: 0.95 + SKU26: + constraint: null + cost: 229 + init_stock: 126 + max_stock: 1000 + price: 357 + sale_gamma: 63 + service_level: 0.95 + SKU260: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 364 + max_stock: 1000 + price: 866 + sale_gamma: 52 + service_level: 0.95 + SKU261: + constraint: null + cost: 368 + init_stock: 66 + max_stock: 1000 + price: 688 + sale_gamma: 22 + service_level: 0.95 + SKU262: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 332 + init_stock: 390 + max_stock: 1000 + price: 385 + sale_gamma: 78 + service_level: 0.95 + SKU263: + constraint: G(stock_constraint) + cost: 189 + init_stock: 444 + max_stock: 1000 + price: 372 + sale_gamma: 74 + service_level: 0.95 + SKU264: + constraint: null + cost: 361 + init_stock: 158 + max_stock: 1000 + price: 566 + sale_gamma: 79 + service_level: 0.95 + SKU265: + constraint: null + cost: 286 + init_stock: 236 + max_stock: 1000 + price: 434 + sale_gamma: 59 + service_level: 0.95 + SKU266: + constraint: null + cost: 128 + init_stock: 282 + max_stock: 1000 + price: 172 + sale_gamma: 47 + service_level: 0.95 + SKU267: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 77 + init_stock: 320 + max_stock: 1000 + price: 130 + sale_gamma: 80 + service_level: 0.95 + SKU268: + constraint: null + cost: 221 + init_stock: 356 + max_stock: 1000 + price: 362 + sale_gamma: 89 + service_level: 0.95 + SKU269: + constraint: G(low_profit -> low_stock_constraint) + cost: 126 + init_stock: 132 + max_stock: 1000 + price: 175 + sale_gamma: 44 + service_level: 0.95 + SKU27: + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 448 + max_stock: 1000 + price: 728 + sale_gamma: 56 + service_level: 0.95 + SKU270: + constraint: null + cost: 182 + init_stock: 370 + max_stock: 1000 + price: 263 + sale_gamma: 74 + service_level: 0.95 + SKU271: + constraint: G(stock_constraint) + cost: 230 + init_stock: 54 + max_stock: 1000 + price: 266 + sale_gamma: 18 + service_level: 0.95 + SKU272: + constraint: null + cost: 366 + init_stock: 51 + max_stock: 1000 + price: 625 + sale_gamma: 17 + service_level: 0.95 + SKU273: + constraint: null + cost: 421 + init_stock: 72 + max_stock: 1000 + price: 614 + sale_gamma: 18 + service_level: 0.95 + SKU274: + constraint: null + cost: 29 + init_stock: 308 + max_stock: 1000 + price: 42 + sale_gamma: 77 + service_level: 0.95 + SKU275: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 50 + init_stock: 144 + max_stock: 1000 + price: 76 + sale_gamma: 48 + service_level: 0.95 + SKU276: + constraint: G(stock_constraint) + cost: 163 + init_stock: 378 + max_stock: 1000 + price: 299 + sale_gamma: 54 + service_level: 0.95 + SKU277: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 82 + max_stock: 1000 + price: 507 + sale_gamma: 41 + service_level: 0.95 + SKU278: + constraint: null + cost: 105 + init_stock: 84 + max_stock: 1000 + price: 140 + sale_gamma: 12 + service_level: 0.95 + SKU279: + constraint: G(stock_constraint) + cost: 51 + init_stock: 273 + max_stock: 1000 + price: 61 + sale_gamma: 39 + service_level: 0.95 + SKU28: + constraint: G(stock_constraint) + cost: 208 + init_stock: 235 + max_stock: 1000 + price: 295 + sale_gamma: 47 + service_level: 0.95 + SKU280: + constraint: G(low_profit -> low_stock_constraint) + cost: 295 + init_stock: 198 + max_stock: 1000 + price: 436 + sale_gamma: 33 + service_level: 0.95 + SKU281: + constraint: null + cost: 395 + init_stock: 375 + max_stock: 1000 + price: 592 + sale_gamma: 75 + service_level: 0.95 + SKU282: + constraint: null + cost: 63 + init_stock: 184 + max_stock: 1000 + price: 112 + sale_gamma: 46 + service_level: 0.95 + SKU283: + constraint: null + cost: 392 + init_stock: 736 + max_stock: 1000 + price: 490 + sale_gamma: 92 + service_level: 0.95 + SKU284: + constraint: G(stock_constraint) + cost: 344 + init_stock: 268 + max_stock: 1000 + price: 643 + sale_gamma: 67 + service_level: 0.95 + SKU285: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 133 + init_stock: 546 + max_stock: 1000 + price: 148 + sale_gamma: 91 + service_level: 0.95 + SKU286: + constraint: null + cost: 337 + init_stock: 156 + max_stock: 1000 + price: 626 + sale_gamma: 39 + service_level: 0.95 + SKU287: + constraint: null + cost: 375 + init_stock: 224 + max_stock: 1000 + price: 660 + sale_gamma: 56 + service_level: 0.95 + SKU288: + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 190 + max_stock: 1000 + price: 352 + sale_gamma: 38 + service_level: 0.95 + SKU289: + constraint: G(stock_constraint) + cost: 67 + init_stock: 62 + max_stock: 1000 + price: 91 + sale_gamma: 31 + service_level: 0.95 + SKU29: + constraint: null + cost: 245 + init_stock: 174 + max_stock: 1000 + price: 475 + sale_gamma: 58 + service_level: 0.95 + SKU290: + constraint: null + cost: 438 + init_stock: 268 + max_stock: 1000 + price: 779 + sale_gamma: 67 + service_level: 0.95 + SKU291: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 122 + max_stock: 1000 + price: 126 + sale_gamma: 61 + service_level: 0.95 + SKU292: + constraint: null + cost: 186 + init_stock: 15 + max_stock: 1000 + price: 344 + sale_gamma: 5 + service_level: 0.95 + SKU293: + constraint: G(stock_constraint) + cost: 162 + init_stock: 385 + max_stock: 1000 + price: 288 + sale_gamma: 55 + service_level: 0.95 + SKU294: + constraint: null + cost: 409 + init_stock: 45 + max_stock: 1000 + price: 605 + sale_gamma: 9 + service_level: 0.95 + SKU295: + constraint: null + cost: 435 + init_stock: 651 + max_stock: 1000 + price: 674 + sale_gamma: 93 + service_level: 0.95 + SKU296: + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 552 + max_stock: 1000 + price: 580 + sale_gamma: 92 + service_level: 0.95 + SKU297: + constraint: null + cost: 298 + init_stock: 228 + max_stock: 1000 + price: 333 + sale_gamma: 38 + service_level: 0.95 + SKU298: + constraint: null + cost: 286 + init_stock: 140 + max_stock: 1000 + price: 500 + sale_gamma: 35 + service_level: 0.95 + SKU299: + constraint: G(stock_constraint) + cost: 367 + init_stock: 255 + max_stock: 1000 + price: 451 + sale_gamma: 51 + service_level: 0.95 + SKU3: + constraint: G(stock_constraint) + cost: 405 + init_stock: 48 + max_stock: 1000 + price: 733 + sale_gamma: 12 + service_level: 0.95 + SKU30: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 359 + init_stock: 276 + max_stock: 1000 + price: 581 + sale_gamma: 69 + service_level: 0.95 + SKU300: + constraint: G(low_profit -> low_stock_constraint) + cost: 376 + init_stock: 290 + max_stock: 1000 + price: 428 + sale_gamma: 58 + service_level: 0.95 + SKU301: + constraint: null + cost: 433 + init_stock: 581 + max_stock: 1000 + price: 857 + sale_gamma: 83 + service_level: 0.95 + SKU302: + constraint: null + cost: 184 + init_stock: 44 + max_stock: 1000 + price: 322 + sale_gamma: 11 + service_level: 0.95 + SKU303: + constraint: null + cost: 246 + init_stock: 188 + max_stock: 1000 + price: 487 + sale_gamma: 94 + service_level: 0.95 + SKU304: + constraint: G(low_profit -> low_stock_constraint) + cost: 419 + init_stock: 138 + max_stock: 1000 + price: 632 + sale_gamma: 23 + service_level: 0.95 + SKU305: + constraint: null + cost: 495 + init_stock: 500 + max_stock: 1000 + price: 772 + sale_gamma: 100 + service_level: 0.95 + SKU306: + constraint: null + cost: 479 + init_stock: 126 + max_stock: 1000 + price: 852 + sale_gamma: 42 + service_level: 0.95 + SKU307: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 210 + init_stock: 156 + max_stock: 1000 + price: 371 + sale_gamma: 78 + service_level: 0.95 + SKU308: + constraint: null + cost: 208 + init_stock: 25 + max_stock: 1000 + price: 262 + sale_gamma: 5 + service_level: 0.95 + SKU309: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 101 + init_stock: 460 + max_stock: 1000 + price: 156 + sale_gamma: 92 + service_level: 0.95 + SKU31: + constraint: null + cost: 69 + init_stock: 280 + max_stock: 1000 + price: 120 + sale_gamma: 56 + service_level: 0.95 + SKU310: + constraint: null + cost: 234 + init_stock: 352 + max_stock: 1000 + price: 294 + sale_gamma: 44 + service_level: 0.95 + SKU311: + constraint: null + cost: 306 + init_stock: 400 + max_stock: 1000 + price: 437 + sale_gamma: 80 + service_level: 0.95 + SKU312: + constraint: G(stock_constraint) + cost: 291 + init_stock: 75 + max_stock: 1000 + price: 392 + sale_gamma: 25 + service_level: 0.95 + SKU313: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 324 + init_stock: 190 + max_stock: 1000 + price: 492 + sale_gamma: 38 + service_level: 0.95 + SKU314: + constraint: G(stock_constraint) + cost: 404 + init_stock: 116 + max_stock: 1000 + price: 456 + sale_gamma: 29 + service_level: 0.95 + SKU315: + constraint: G(stock_constraint) + cost: 471 + init_stock: 504 + max_stock: 1000 + price: 885 + sale_gamma: 84 + service_level: 0.95 + SKU316: + constraint: null + cost: 202 + init_stock: 90 + max_stock: 1000 + price: 282 + sale_gamma: 18 + service_level: 0.95 + SKU317: + constraint: null + cost: 216 + init_stock: 168 + max_stock: 1000 + price: 352 + sale_gamma: 24 + service_level: 0.95 + SKU318: + constraint: null + cost: 278 + init_stock: 255 + max_stock: 1000 + price: 350 + sale_gamma: 85 + service_level: 0.95 + SKU319: + constraint: null + cost: 257 + init_stock: 371 + max_stock: 1000 + price: 454 + sale_gamma: 53 + service_level: 0.95 + SKU32: + constraint: null + cost: 336 + init_stock: 110 + max_stock: 1000 + price: 581 + sale_gamma: 22 + service_level: 0.95 + SKU320: + constraint: null + cost: 196 + init_stock: 156 + max_stock: 1000 + price: 258 + sale_gamma: 39 + service_level: 0.95 + SKU321: + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 96 + max_stock: 1000 + price: 105 + sale_gamma: 16 + service_level: 0.95 + SKU322: + constraint: null + cost: 240 + init_stock: 600 + max_stock: 1000 + price: 453 + sale_gamma: 100 + service_level: 0.95 + SKU323: + constraint: G(stock_constraint) + cost: 66 + init_stock: 195 + max_stock: 1000 + price: 74 + sale_gamma: 39 + service_level: 0.95 + SKU324: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 442 + init_stock: 465 + max_stock: 1000 + price: 804 + sale_gamma: 93 + service_level: 0.95 + SKU325: + constraint: null + cost: 453 + init_stock: 345 + max_stock: 1000 + price: 579 + sale_gamma: 69 + service_level: 0.95 + SKU326: + constraint: null + cost: 345 + init_stock: 72 + max_stock: 1000 + price: 538 + sale_gamma: 24 + service_level: 0.95 + SKU327: + constraint: G(low_profit -> low_stock_constraint) + cost: 457 + init_stock: 84 + max_stock: 1000 + price: 749 + sale_gamma: 14 + service_level: 0.95 + SKU328: + constraint: null + cost: 390 + init_stock: 90 + max_stock: 1000 + price: 487 + sale_gamma: 45 + service_level: 0.95 + SKU329: + constraint: null + cost: 67 + init_stock: 84 + max_stock: 1000 + price: 126 + sale_gamma: 42 + service_level: 0.95 + SKU33: + constraint: G(low_profit -> low_stock_constraint) + cost: 416 + init_stock: 552 + max_stock: 1000 + price: 465 + sale_gamma: 92 + service_level: 0.95 + SKU330: + constraint: null + cost: 306 + init_stock: 273 + max_stock: 1000 + price: 385 + sale_gamma: 39 + service_level: 0.95 + SKU331: + constraint: G(low_profit -> low_stock_constraint) + cost: 138 + init_stock: 164 + max_stock: 1000 + price: 180 + sale_gamma: 41 + service_level: 0.95 + SKU332: + constraint: null + cost: 390 + init_stock: 288 + max_stock: 1000 + price: 670 + sale_gamma: 96 + service_level: 0.95 + SKU333: + constraint: G(stock_constraint) + cost: 485 + init_stock: 318 + max_stock: 1000 + price: 800 + sale_gamma: 53 + service_level: 0.95 + SKU334: + constraint: null + cost: 183 + init_stock: 114 + max_stock: 1000 + price: 245 + sale_gamma: 57 + service_level: 0.95 + SKU335: + constraint: null + cost: 80 + init_stock: 243 + max_stock: 1000 + price: 141 + sale_gamma: 81 + service_level: 0.95 + SKU336: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 56 + max_stock: 1000 + price: 565 + sale_gamma: 28 + service_level: 0.95 + SKU337: + constraint: null + cost: 245 + init_stock: 174 + max_stock: 1000 + price: 394 + sale_gamma: 29 + service_level: 0.95 + SKU338: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 87 + init_stock: 324 + max_stock: 1000 + price: 150 + sale_gamma: 81 + service_level: 0.95 + SKU339: + constraint: G(stock_constraint) + cost: 265 + init_stock: 441 + max_stock: 1000 + price: 368 + sale_gamma: 63 + service_level: 0.95 + SKU34: + constraint: null + cost: 95 + init_stock: 91 + max_stock: 1000 + price: 135 + sale_gamma: 13 + service_level: 0.95 + SKU340: + constraint: null + cost: 480 + init_stock: 435 + max_stock: 1000 + price: 633 + sale_gamma: 87 + service_level: 0.95 + SKU341: + constraint: G(low_profit -> low_stock_constraint) + cost: 462 + init_stock: 280 + max_stock: 1000 + price: 924 + sale_gamma: 70 + service_level: 0.95 + SKU342: + constraint: null + cost: 144 + init_stock: 72 + max_stock: 1000 + price: 216 + sale_gamma: 9 + service_level: 0.95 + SKU343: + constraint: null + cost: 310 + init_stock: 90 + max_stock: 1000 + price: 589 + sale_gamma: 15 + service_level: 0.95 + SKU344: + constraint: null + cost: 357 + init_stock: 348 + max_stock: 1000 + price: 442 + sale_gamma: 87 + service_level: 0.95 + SKU345: + constraint: null + cost: 442 + init_stock: 267 + max_stock: 1000 + price: 857 + sale_gamma: 89 + service_level: 0.95 + SKU346: + constraint: G(stock_constraint) + cost: 350 + init_stock: 336 + max_stock: 1000 + price: 549 + sale_gamma: 42 + service_level: 0.95 + SKU347: + constraint: G(low_profit -> low_stock_constraint) + cost: 497 + init_stock: 328 + max_stock: 1000 + price: 810 + sale_gamma: 82 + service_level: 0.95 + SKU348: + constraint: G(stock_constraint) + cost: 147 + init_stock: 100 + max_stock: 1000 + price: 277 + sale_gamma: 20 + service_level: 0.95 + SKU349: + constraint: null + cost: 67 + init_stock: 335 + max_stock: 1000 + price: 116 + sale_gamma: 67 + service_level: 0.95 + SKU35: + constraint: null + cost: 267 + init_stock: 430 + max_stock: 1000 + price: 373 + sale_gamma: 86 + service_level: 0.95 + SKU350: + constraint: G(stock_constraint) + cost: 279 + init_stock: 552 + max_stock: 1000 + price: 460 + sale_gamma: 92 + service_level: 0.95 + SKU351: + constraint: null + cost: 155 + init_stock: 335 + max_stock: 1000 + price: 294 + sale_gamma: 67 + service_level: 0.95 + SKU352: + constraint: null + cost: 71 + init_stock: 54 + max_stock: 1000 + price: 100 + sale_gamma: 18 + service_level: 0.95 + SKU353: + constraint: G(stock_constraint) + cost: 253 + init_stock: 465 + max_stock: 1000 + price: 437 + sale_gamma: 93 + service_level: 0.95 + SKU354: + constraint: G(low_profit -> low_stock_constraint) + cost: 396 + init_stock: 395 + max_stock: 1000 + price: 566 + sale_gamma: 79 + service_level: 0.95 + SKU355: + constraint: G(stock_constraint) + cost: 63 + init_stock: 30 + max_stock: 1000 + price: 74 + sale_gamma: 10 + service_level: 0.95 + SKU356: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 87 + max_stock: 1000 + price: 441 + sale_gamma: 29 + service_level: 0.95 + SKU357: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 499 + init_stock: 460 + max_stock: 1000 + price: 868 + sale_gamma: 92 + service_level: 0.95 + SKU358: + constraint: null + cost: 269 + init_stock: 414 + max_stock: 1000 + price: 408 + sale_gamma: 69 + service_level: 0.95 + SKU359: + constraint: G(low_profit -> low_stock_constraint) + cost: 82 + init_stock: 600 + max_stock: 1000 + price: 110 + sale_gamma: 75 + service_level: 0.95 + SKU36: + constraint: null + cost: 105 + init_stock: 30 + max_stock: 1000 + price: 165 + sale_gamma: 10 + service_level: 0.95 + SKU360: + constraint: G(low_profit -> low_stock_constraint) + cost: 484 + init_stock: 75 + max_stock: 1000 + price: 682 + sale_gamma: 25 + service_level: 0.95 + SKU361: + constraint: null + cost: 163 + init_stock: 360 + max_stock: 1000 + price: 288 + sale_gamma: 90 + service_level: 0.95 + SKU362: + constraint: null + cost: 464 + init_stock: 435 + max_stock: 1000 + price: 867 + sale_gamma: 87 + service_level: 0.95 + SKU363: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 390 + max_stock: 1000 + price: 93 + sale_gamma: 78 + service_level: 0.95 + SKU364: + constraint: G(stock_constraint) + cost: 387 + init_stock: 66 + max_stock: 1000 + price: 472 + sale_gamma: 33 + service_level: 0.95 + SKU365: + constraint: null + cost: 158 + init_stock: 581 + max_stock: 1000 + price: 241 + sale_gamma: 83 + service_level: 0.95 + SKU366: + constraint: null + cost: 444 + init_stock: 344 + max_stock: 1000 + price: 639 + sale_gamma: 86 + service_level: 0.95 + SKU367: + constraint: G(low_profit -> low_stock_constraint) + cost: 272 + init_stock: 322 + max_stock: 1000 + price: 304 + sale_gamma: 46 + service_level: 0.95 + SKU368: + constraint: G(stock_constraint) + cost: 472 + init_stock: 198 + max_stock: 1000 + price: 613 + sale_gamma: 33 + service_level: 0.95 + SKU369: + constraint: null + cost: 96 + init_stock: 375 + max_stock: 1000 + price: 155 + sale_gamma: 75 + service_level: 0.95 + SKU37: + constraint: G(low_profit -> low_stock_constraint) + cost: 66 + init_stock: 177 + max_stock: 1000 + price: 110 + sale_gamma: 59 + service_level: 0.95 + SKU370: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 112 + init_stock: 90 + max_stock: 1000 + price: 141 + sale_gamma: 15 + service_level: 0.95 + SKU371: + constraint: null + cost: 328 + init_stock: 25 + max_stock: 1000 + price: 511 + sale_gamma: 5 + service_level: 0.95 + SKU372: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 67 + init_stock: 192 + max_stock: 1000 + price: 134 + sale_gamma: 32 + service_level: 0.95 + SKU373: + constraint: null + cost: 58 + init_stock: 268 + max_stock: 1000 + price: 91 + sale_gamma: 67 + service_level: 0.95 + SKU374: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 38 + init_stock: 216 + max_stock: 1000 + price: 73 + sale_gamma: 54 + service_level: 0.95 + SKU375: + constraint: G(low_profit -> low_stock_constraint) + cost: 283 + init_stock: 432 + max_stock: 1000 + price: 416 + sale_gamma: 72 + service_level: 0.95 + SKU376: + constraint: null + cost: 156 + init_stock: 164 + max_stock: 1000 + price: 291 + sale_gamma: 82 + service_level: 0.95 + SKU377: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 78 + init_stock: 536 + max_stock: 1000 + price: 134 + sale_gamma: 67 + service_level: 0.95 + SKU378: + constraint: G(low_profit -> low_stock_constraint) + cost: 424 + init_stock: 175 + max_stock: 1000 + price: 546 + sale_gamma: 35 + service_level: 0.95 + SKU379: + constraint: null + cost: 11 + init_stock: 196 + max_stock: 1000 + price: 20 + sale_gamma: 49 + service_level: 0.95 + SKU38: + constraint: null + cost: 344 + init_stock: 258 + max_stock: 1000 + price: 567 + sale_gamma: 43 + service_level: 0.95 + SKU380: + constraint: null + cost: 393 + init_stock: 212 + max_stock: 1000 + price: 605 + sale_gamma: 53 + service_level: 0.95 + SKU381: + constraint: null + cost: 476 + init_stock: 18 + max_stock: 1000 + price: 609 + sale_gamma: 6 + service_level: 0.95 + SKU382: + constraint: null + cost: 125 + init_stock: 426 + max_stock: 1000 + price: 177 + sale_gamma: 71 + service_level: 0.95 + SKU383: + constraint: null + cost: 304 + init_stock: 368 + max_stock: 1000 + price: 425 + sale_gamma: 92 + service_level: 0.95 + SKU384: + constraint: null + cost: 320 + init_stock: 54 + max_stock: 1000 + price: 460 + sale_gamma: 9 + service_level: 0.95 + SKU385: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 120 + init_stock: 91 + max_stock: 1000 + price: 184 + sale_gamma: 13 + service_level: 0.95 + SKU386: + constraint: G(stock_constraint) + cost: 295 + init_stock: 124 + max_stock: 1000 + price: 536 + sale_gamma: 31 + service_level: 0.95 + SKU387: + constraint: null + cost: 112 + init_stock: 582 + max_stock: 1000 + price: 154 + sale_gamma: 97 + service_level: 0.95 + SKU388: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 405 + init_stock: 264 + max_stock: 1000 + price: 635 + sale_gamma: 44 + service_level: 0.95 + SKU389: + constraint: G(stock_constraint) + cost: 376 + init_stock: 490 + max_stock: 1000 + price: 699 + sale_gamma: 70 + service_level: 0.95 + SKU39: + constraint: G(low_profit -> low_stock_constraint) + cost: 25 + init_stock: 204 + max_stock: 1000 + price: 45 + sale_gamma: 51 + service_level: 0.95 + SKU390: + constraint: null + cost: 144 + init_stock: 156 + max_stock: 1000 + price: 223 + sale_gamma: 39 + service_level: 0.95 + SKU391: + constraint: G(stock_constraint) + cost: 233 + init_stock: 402 + max_stock: 1000 + price: 403 + sale_gamma: 67 + service_level: 0.95 + SKU392: + constraint: null + cost: 163 + init_stock: 370 + max_stock: 1000 + price: 205 + sale_gamma: 74 + service_level: 0.95 + SKU393: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 402 + max_stock: 1000 + price: 701 + sale_gamma: 67 + service_level: 0.95 + SKU394: + constraint: null + cost: 154 + init_stock: 318 + max_stock: 1000 + price: 252 + sale_gamma: 53 + service_level: 0.95 + SKU395: + constraint: null + cost: 488 + init_stock: 198 + max_stock: 1000 + price: 854 + sale_gamma: 33 + service_level: 0.95 + SKU396: + constraint: null + cost: 333 + init_stock: 427 + max_stock: 1000 + price: 469 + sale_gamma: 61 + service_level: 0.95 + SKU397: + constraint: null + cost: 460 + init_stock: 153 + max_stock: 1000 + price: 791 + sale_gamma: 51 + service_level: 0.95 + SKU398: + constraint: G(stock_constraint) + cost: 234 + init_stock: 174 + max_stock: 1000 + price: 414 + sale_gamma: 58 + service_level: 0.95 + SKU399: + constraint: G(stock_constraint) + cost: 376 + init_stock: 148 + max_stock: 1000 + price: 612 + sale_gamma: 37 + service_level: 0.95 + SKU4: + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 21 + max_stock: 1000 + price: 798 + sale_gamma: 7 + service_level: 0.95 + SKU40: + constraint: null + cost: 490 + init_stock: 594 + max_stock: 1000 + price: 563 + sale_gamma: 99 + service_level: 0.95 + SKU400: + constraint: G(low_profit -> low_stock_constraint) + cost: 445 + init_stock: 420 + max_stock: 1000 + price: 729 + sale_gamma: 84 + service_level: 0.95 + SKU401: + constraint: G(low_profit -> low_stock_constraint) + cost: 410 + init_stock: 312 + max_stock: 1000 + price: 774 + sale_gamma: 78 + service_level: 0.95 + SKU402: + constraint: G(low_profit -> low_stock_constraint) + cost: 86 + init_stock: 35 + max_stock: 1000 + price: 153 + sale_gamma: 7 + service_level: 0.95 + SKU403: + constraint: null + cost: 89 + init_stock: 594 + max_stock: 1000 + price: 133 + sale_gamma: 99 + service_level: 0.95 + SKU404: + constraint: null + cost: 287 + init_stock: 305 + max_stock: 1000 + price: 522 + sale_gamma: 61 + service_level: 0.95 + SKU405: + constraint: G(stock_constraint) + cost: 460 + init_stock: 114 + max_stock: 1000 + price: 584 + sale_gamma: 19 + service_level: 0.95 + SKU406: + constraint: null + cost: 327 + init_stock: 400 + max_stock: 1000 + price: 405 + sale_gamma: 100 + service_level: 0.95 + SKU407: + constraint: null + cost: 26 + init_stock: 184 + max_stock: 1000 + price: 41 + sale_gamma: 46 + service_level: 0.95 + SKU408: + constraint: null + cost: 444 + init_stock: 48 + max_stock: 1000 + price: 754 + sale_gamma: 8 + service_level: 0.95 + SKU409: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 257 + init_stock: 273 + max_stock: 1000 + price: 449 + sale_gamma: 91 + service_level: 0.95 + SKU41: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 141 + init_stock: 553 + max_stock: 1000 + price: 162 + sale_gamma: 79 + service_level: 0.95 + SKU410: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 70 + init_stock: 32 + max_stock: 1000 + price: 88 + sale_gamma: 16 + service_level: 0.95 + SKU411: + constraint: G(stock_constraint) + cost: 210 + init_stock: 380 + max_stock: 1000 + price: 405 + sale_gamma: 95 + service_level: 0.95 + SKU412: + constraint: null + cost: 286 + init_stock: 248 + max_stock: 1000 + price: 414 + sale_gamma: 62 + service_level: 0.95 + SKU413: + constraint: null + cost: 403 + init_stock: 581 + max_stock: 1000 + price: 801 + sale_gamma: 83 + service_level: 0.95 + SKU414: + constraint: G(stock_constraint) + cost: 165 + init_stock: 435 + max_stock: 1000 + price: 229 + sale_gamma: 87 + service_level: 0.95 + SKU415: + constraint: G(stock_constraint) + cost: 291 + init_stock: 184 + max_stock: 1000 + price: 372 + sale_gamma: 23 + service_level: 0.95 + SKU416: + constraint: null + cost: 228 + init_stock: 36 + max_stock: 1000 + price: 373 + sale_gamma: 9 + service_level: 0.95 + SKU417: + constraint: G(low_profit -> low_stock_constraint) + cost: 443 + init_stock: 288 + max_stock: 1000 + price: 872 + sale_gamma: 72 + service_level: 0.95 + SKU418: + constraint: null + cost: 458 + init_stock: 52 + max_stock: 1000 + price: 838 + sale_gamma: 13 + service_level: 0.95 + SKU419: + constraint: null + cost: 303 + init_stock: 712 + max_stock: 1000 + price: 448 + sale_gamma: 89 + service_level: 0.95 + SKU42: + constraint: null + cost: 462 + init_stock: 156 + max_stock: 1000 + price: 688 + sale_gamma: 26 + service_level: 0.95 + SKU420: + constraint: null + cost: 268 + init_stock: 84 + max_stock: 1000 + price: 506 + sale_gamma: 42 + service_level: 0.95 + SKU421: + constraint: G(stock_constraint) + cost: 214 + init_stock: 138 + max_stock: 1000 + price: 314 + sale_gamma: 46 + service_level: 0.95 + SKU422: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 432 + max_stock: 1000 + price: 97 + sale_gamma: 54 + service_level: 0.95 + SKU423: + constraint: G(low_profit -> low_stock_constraint) + cost: 188 + init_stock: 396 + max_stock: 1000 + price: 265 + sale_gamma: 66 + service_level: 0.95 + SKU424: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 66 + max_stock: 1000 + price: 317 + sale_gamma: 11 + service_level: 0.95 + SKU425: + constraint: G(stock_constraint) + cost: 162 + init_stock: 72 + max_stock: 1000 + price: 277 + sale_gamma: 12 + service_level: 0.95 + SKU426: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 125 + init_stock: 588 + max_stock: 1000 + price: 246 + sale_gamma: 98 + service_level: 0.95 + SKU427: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 413 + init_stock: 282 + max_stock: 1000 + price: 710 + sale_gamma: 94 + service_level: 0.95 + SKU428: + constraint: G(stock_constraint) + cost: 391 + init_stock: 378 + max_stock: 1000 + price: 629 + sale_gamma: 63 + service_level: 0.95 + SKU429: + constraint: G(stock_constraint) + cost: 39 + init_stock: 246 + max_stock: 1000 + price: 73 + sale_gamma: 41 + service_level: 0.95 + SKU43: + constraint: G(stock_constraint) + cost: 217 + init_stock: 272 + max_stock: 1000 + price: 353 + sale_gamma: 68 + service_level: 0.95 + SKU430: + constraint: null + cost: 216 + init_stock: 511 + max_stock: 1000 + price: 313 + sale_gamma: 73 + service_level: 0.95 + SKU431: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 328 + init_stock: 126 + max_stock: 1000 + price: 646 + sale_gamma: 21 + service_level: 0.95 + SKU432: + constraint: G(stock_constraint) + cost: 25 + init_stock: 368 + max_stock: 1000 + price: 35 + sale_gamma: 46 + service_level: 0.95 + SKU433: + constraint: null + cost: 348 + init_stock: 270 + max_stock: 1000 + price: 396 + sale_gamma: 45 + service_level: 0.95 + SKU434: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 90 + max_stock: 1000 + price: 40 + sale_gamma: 30 + service_level: 0.95 + SKU435: + constraint: G(stock_constraint) + cost: 272 + init_stock: 210 + max_stock: 1000 + price: 320 + sale_gamma: 30 + service_level: 0.95 + SKU436: + constraint: null + cost: 72 + init_stock: 405 + max_stock: 1000 + price: 105 + sale_gamma: 81 + service_level: 0.95 + SKU437: + constraint: G(stock_constraint) + cost: 115 + init_stock: 84 + max_stock: 1000 + price: 223 + sale_gamma: 14 + service_level: 0.95 + SKU438: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 40 + max_stock: 1000 + price: 190 + sale_gamma: 10 + service_level: 0.95 + SKU439: + constraint: G(stock_constraint) + cost: 256 + init_stock: 190 + max_stock: 1000 + price: 304 + sale_gamma: 38 + service_level: 0.95 + SKU44: + constraint: null + cost: 360 + init_stock: 240 + max_stock: 1000 + price: 669 + sale_gamma: 48 + service_level: 0.95 + SKU440: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 248 + init_stock: 246 + max_stock: 1000 + price: 357 + sale_gamma: 82 + service_level: 0.95 + SKU441: + constraint: null + cost: 253 + init_stock: 224 + max_stock: 1000 + price: 374 + sale_gamma: 56 + service_level: 0.95 + SKU442: + constraint: null + cost: 470 + init_stock: 352 + max_stock: 1000 + price: 629 + sale_gamma: 88 + service_level: 0.95 + SKU443: + constraint: null + cost: 218 + init_stock: 365 + max_stock: 1000 + price: 361 + sale_gamma: 73 + service_level: 0.95 + SKU444: + constraint: null + cost: 156 + init_stock: 18 + max_stock: 1000 + price: 182 + sale_gamma: 6 + service_level: 0.95 + SKU445: + constraint: null + cost: 260 + init_stock: 602 + max_stock: 1000 + price: 296 + sale_gamma: 86 + service_level: 0.95 + SKU446: + constraint: null + cost: 497 + init_stock: 147 + max_stock: 1000 + price: 690 + sale_gamma: 49 + service_level: 0.95 + SKU447: + constraint: null + cost: 196 + init_stock: 30 + max_stock: 1000 + price: 280 + sale_gamma: 5 + service_level: 0.95 + SKU448: + constraint: null + cost: 485 + init_stock: 497 + max_stock: 1000 + price: 742 + sale_gamma: 71 + service_level: 0.95 + SKU449: + constraint: null + cost: 282 + init_stock: 114 + max_stock: 1000 + price: 360 + sale_gamma: 38 + service_level: 0.95 + SKU45: + constraint: G(stock_constraint) + cost: 253 + init_stock: 30 + max_stock: 1000 + price: 351 + sale_gamma: 10 + service_level: 0.95 + SKU450: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 58 + init_stock: 294 + max_stock: 1000 + price: 77 + sale_gamma: 98 + service_level: 0.95 + SKU451: + constraint: null + cost: 468 + init_stock: 483 + max_stock: 1000 + price: 851 + sale_gamma: 69 + service_level: 0.95 + SKU452: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 63 + init_stock: 395 + max_stock: 1000 + price: 97 + sale_gamma: 79 + service_level: 0.95 + SKU453: + constraint: null + cost: 37 + init_stock: 140 + max_stock: 1000 + price: 65 + sale_gamma: 35 + service_level: 0.95 + SKU454: + constraint: G(low_profit -> low_stock_constraint) + cost: 494 + init_stock: 340 + max_stock: 1000 + price: 899 + sale_gamma: 85 + service_level: 0.95 + SKU455: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 136 + init_stock: 182 + max_stock: 1000 + price: 195 + sale_gamma: 26 + service_level: 0.95 + SKU456: + constraint: G(stock_constraint) + cost: 338 + init_stock: 75 + max_stock: 1000 + price: 659 + sale_gamma: 25 + service_level: 0.95 + SKU457: + constraint: null + cost: 169 + init_stock: 256 + max_stock: 1000 + price: 190 + sale_gamma: 64 + service_level: 0.95 + SKU458: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 403 + init_stock: 28 + max_stock: 1000 + price: 685 + sale_gamma: 7 + service_level: 0.95 + SKU459: + constraint: null + cost: 425 + init_stock: 272 + max_stock: 1000 + price: 731 + sale_gamma: 34 + service_level: 0.95 + SKU46: + constraint: G(low_profit -> low_stock_constraint) + cost: 299 + init_stock: 200 + max_stock: 1000 + price: 409 + sale_gamma: 50 + service_level: 0.95 + SKU460: + constraint: G(low_profit -> low_stock_constraint) + cost: 405 + init_stock: 292 + max_stock: 1000 + price: 558 + sale_gamma: 73 + service_level: 0.95 + SKU461: + constraint: G(stock_constraint) + cost: 251 + init_stock: 240 + max_stock: 1000 + price: 326 + sale_gamma: 48 + service_level: 0.95 + SKU462: + constraint: G(low_profit -> low_stock_constraint) + cost: 448 + init_stock: 36 + max_stock: 1000 + price: 757 + sale_gamma: 9 + service_level: 0.95 + SKU463: + constraint: null + cost: 187 + init_stock: 574 + max_stock: 1000 + price: 213 + sale_gamma: 82 + service_level: 0.95 + SKU464: + constraint: null + cost: 279 + init_stock: 272 + max_stock: 1000 + price: 438 + sale_gamma: 34 + service_level: 0.95 + SKU465: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 147 + init_stock: 400 + max_stock: 1000 + price: 288 + sale_gamma: 100 + service_level: 0.95 + SKU466: + constraint: G(stock_constraint) + cost: 97 + init_stock: 126 + max_stock: 1000 + price: 167 + sale_gamma: 42 + service_level: 0.95 + SKU467: + constraint: G(stock_constraint) + cost: 142 + init_stock: 252 + max_stock: 1000 + price: 230 + sale_gamma: 36 + service_level: 0.95 + SKU468: + constraint: G(stock_constraint) + cost: 55 + init_stock: 250 + max_stock: 1000 + price: 104 + sale_gamma: 50 + service_level: 0.95 + SKU469: + constraint: G(stock_constraint) + cost: 178 + init_stock: 105 + max_stock: 1000 + price: 331 + sale_gamma: 15 + service_level: 0.95 + SKU47: + constraint: G(stock_constraint) + cost: 121 + init_stock: 117 + max_stock: 1000 + price: 240 + sale_gamma: 39 + service_level: 0.95 + SKU470: + constraint: G(low_profit -> low_stock_constraint) + cost: 373 + init_stock: 128 + max_stock: 1000 + price: 548 + sale_gamma: 32 + service_level: 0.95 + SKU471: + constraint: G(low_profit -> low_stock_constraint) + cost: 315 + init_stock: 568 + max_stock: 1000 + price: 387 + sale_gamma: 71 + service_level: 0.95 + SKU472: + constraint: null + cost: 421 + init_stock: 177 + max_stock: 1000 + price: 627 + sale_gamma: 59 + service_level: 0.95 + SKU473: + constraint: G(low_profit -> low_stock_constraint) + cost: 195 + init_stock: 105 + max_stock: 1000 + price: 323 + sale_gamma: 21 + service_level: 0.95 + SKU474: + constraint: null + cost: 448 + init_stock: 602 + max_stock: 1000 + price: 775 + sale_gamma: 86 + service_level: 0.95 + SKU475: + constraint: null + cost: 34 + init_stock: 282 + max_stock: 1000 + price: 62 + sale_gamma: 94 + service_level: 0.95 + SKU476: + constraint: null + cost: 251 + init_stock: 180 + max_stock: 1000 + price: 361 + sale_gamma: 90 + service_level: 0.95 + SKU477: + constraint: null + cost: 289 + init_stock: 329 + max_stock: 1000 + price: 338 + sale_gamma: 47 + service_level: 0.95 + SKU478: + constraint: null + cost: 479 + init_stock: 352 + max_stock: 1000 + price: 723 + sale_gamma: 88 + service_level: 0.95 + SKU479: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 366 + init_stock: 152 + max_stock: 1000 + price: 552 + sale_gamma: 38 + service_level: 0.95 + SKU48: + constraint: null + cost: 340 + init_stock: 232 + max_stock: 1000 + price: 387 + sale_gamma: 58 + service_level: 0.95 + SKU480: + constraint: G(stock_constraint) + cost: 18 + init_stock: 180 + max_stock: 1000 + price: 21 + sale_gamma: 30 + service_level: 0.95 + SKU481: + constraint: null + cost: 330 + init_stock: 352 + max_stock: 1000 + price: 422 + sale_gamma: 88 + service_level: 0.95 + SKU482: + constraint: G(stock_constraint) + cost: 94 + init_stock: 696 + max_stock: 1000 + price: 108 + sale_gamma: 87 + service_level: 0.95 + SKU483: + constraint: null + cost: 298 + init_stock: 170 + max_stock: 1000 + price: 533 + sale_gamma: 34 + service_level: 0.95 + SKU484: + constraint: null + cost: 84 + init_stock: 365 + max_stock: 1000 + price: 117 + sale_gamma: 73 + service_level: 0.95 + SKU485: + constraint: null + cost: 318 + init_stock: 536 + max_stock: 1000 + price: 543 + sale_gamma: 67 + service_level: 0.95 + SKU486: + constraint: G(low_profit -> low_stock_constraint) + cost: 122 + init_stock: 208 + max_stock: 1000 + price: 161 + sale_gamma: 52 + service_level: 0.95 + SKU487: + constraint: null + cost: 277 + init_stock: 264 + max_stock: 1000 + price: 362 + sale_gamma: 66 + service_level: 0.95 + SKU488: + constraint: G(low_profit -> low_stock_constraint) + cost: 36 + init_stock: 189 + max_stock: 1000 + price: 42 + sale_gamma: 27 + service_level: 0.95 + SKU489: + constraint: null + cost: 59 + init_stock: 124 + max_stock: 1000 + price: 97 + sale_gamma: 31 + service_level: 0.95 + SKU49: + constraint: null + cost: 263 + init_stock: 760 + max_stock: 1000 + price: 515 + sale_gamma: 95 + service_level: 0.95 + SKU490: + constraint: null + cost: 161 + init_stock: 486 + max_stock: 1000 + price: 264 + sale_gamma: 81 + service_level: 0.95 + SKU491: + constraint: null + cost: 444 + init_stock: 450 + max_stock: 1000 + price: 834 + sale_gamma: 75 + service_level: 0.95 + SKU492: + constraint: null + cost: 53 + init_stock: 240 + max_stock: 1000 + price: 63 + sale_gamma: 80 + service_level: 0.95 + SKU493: + constraint: null + cost: 461 + init_stock: 81 + max_stock: 1000 + price: 567 + sale_gamma: 27 + service_level: 0.95 + SKU494: + constraint: null + cost: 145 + init_stock: 282 + max_stock: 1000 + price: 242 + sale_gamma: 94 + service_level: 0.95 + SKU495: + constraint: G(stock_constraint) + cost: 149 + init_stock: 372 + max_stock: 1000 + price: 217 + sale_gamma: 62 + service_level: 0.95 + SKU496: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 342 + init_stock: 295 + max_stock: 1000 + price: 492 + sale_gamma: 59 + service_level: 0.95 + SKU497: + constraint: G(low_profit -> low_stock_constraint) + cost: 33 + init_stock: 45 + max_stock: 1000 + price: 42 + sale_gamma: 9 + service_level: 0.95 + SKU498: + constraint: null + cost: 305 + init_stock: 325 + max_stock: 1000 + price: 439 + sale_gamma: 65 + service_level: 0.95 + SKU499: + constraint: G(stock_constraint) + cost: 307 + init_stock: 174 + max_stock: 1000 + price: 472 + sale_gamma: 29 + service_level: 0.95 + SKU5: + constraint: G(stock_constraint) + cost: 466 + init_stock: 426 + max_stock: 1000 + price: 852 + sale_gamma: 71 + service_level: 0.95 + SKU50: + constraint: null + cost: 282 + init_stock: 185 + max_stock: 1000 + price: 516 + sale_gamma: 37 + service_level: 0.95 + SKU500: + constraint: null + cost: 208 + init_stock: 336 + max_stock: 1000 + price: 255 + sale_gamma: 42 + service_level: 0.95 + SKU501: + constraint: null + cost: 194 + init_stock: 152 + max_stock: 1000 + price: 265 + sale_gamma: 76 + service_level: 0.95 + SKU502: + constraint: null + cost: 69 + init_stock: 390 + max_stock: 1000 + price: 101 + sale_gamma: 65 + service_level: 0.95 + SKU503: + constraint: G(stock_constraint) + cost: 208 + init_stock: 228 + max_stock: 1000 + price: 280 + sale_gamma: 57 + service_level: 0.95 + SKU504: + constraint: null + cost: 251 + init_stock: 210 + max_stock: 1000 + price: 426 + sale_gamma: 30 + service_level: 0.95 + SKU505: + constraint: null + cost: 426 + init_stock: 295 + max_stock: 1000 + price: 515 + sale_gamma: 59 + service_level: 0.95 + SKU506: + constraint: null + cost: 149 + init_stock: 465 + max_stock: 1000 + price: 220 + sale_gamma: 93 + service_level: 0.95 + SKU507: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 187 + init_stock: 60 + max_stock: 1000 + price: 293 + sale_gamma: 15 + service_level: 0.95 + SKU508: + constraint: G(low_profit -> low_stock_constraint) + cost: 272 + init_stock: 576 + max_stock: 1000 + price: 432 + sale_gamma: 96 + service_level: 0.95 + SKU509: + constraint: G(low_profit -> low_stock_constraint) + cost: 297 + init_stock: 486 + max_stock: 1000 + price: 397 + sale_gamma: 81 + service_level: 0.95 + SKU51: + constraint: G(low_profit -> low_stock_constraint) + cost: 295 + init_stock: 469 + max_stock: 1000 + price: 477 + sale_gamma: 67 + service_level: 0.95 + SKU510: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 36 + max_stock: 1000 + price: 156 + sale_gamma: 9 + service_level: 0.95 + SKU511: + constraint: G(stock_constraint) + cost: 361 + init_stock: 450 + max_stock: 1000 + price: 480 + sale_gamma: 75 + service_level: 0.95 + SKU512: + constraint: null + cost: 467 + init_stock: 132 + max_stock: 1000 + price: 854 + sale_gamma: 22 + service_level: 0.95 + SKU513: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 343 + init_stock: 42 + max_stock: 1000 + price: 631 + sale_gamma: 7 + service_level: 0.95 + SKU514: + constraint: null + cost: 186 + init_stock: 504 + max_stock: 1000 + price: 332 + sale_gamma: 63 + service_level: 0.95 + SKU515: + constraint: null + cost: 145 + init_stock: 216 + max_stock: 1000 + price: 227 + sale_gamma: 54 + service_level: 0.95 + SKU516: + constraint: G(stock_constraint) + cost: 64 + init_stock: 114 + max_stock: 1000 + price: 96 + sale_gamma: 38 + service_level: 0.95 + SKU517: + constraint: null + cost: 117 + init_stock: 45 + max_stock: 1000 + price: 168 + sale_gamma: 9 + service_level: 0.95 + SKU518: + constraint: G(stock_constraint) + cost: 26 + init_stock: 147 + max_stock: 1000 + price: 30 + sale_gamma: 21 + service_level: 0.95 + SKU519: + constraint: null + cost: 233 + init_stock: 250 + max_stock: 1000 + price: 410 + sale_gamma: 50 + service_level: 0.95 + SKU52: + constraint: null + cost: 22 + init_stock: 402 + max_stock: 1000 + price: 28 + sale_gamma: 67 + service_level: 0.95 + SKU520: + constraint: G(low_profit -> low_stock_constraint) + cost: 209 + init_stock: 44 + max_stock: 1000 + price: 248 + sale_gamma: 22 + service_level: 0.95 + SKU521: + constraint: G(low_profit -> low_stock_constraint) + cost: 220 + init_stock: 350 + max_stock: 1000 + price: 330 + sale_gamma: 50 + service_level: 0.95 + SKU522: + constraint: null + cost: 156 + init_stock: 522 + max_stock: 1000 + price: 277 + sale_gamma: 87 + service_level: 0.95 + SKU523: + constraint: null + cost: 65 + init_stock: 500 + max_stock: 1000 + price: 107 + sale_gamma: 100 + service_level: 0.95 + SKU524: + constraint: null + cost: 294 + init_stock: 188 + max_stock: 1000 + price: 464 + sale_gamma: 94 + service_level: 0.95 + SKU525: + constraint: G(stock_constraint) + cost: 432 + init_stock: 126 + max_stock: 1000 + price: 751 + sale_gamma: 42 + service_level: 0.95 + SKU526: + constraint: null + cost: 419 + init_stock: 168 + max_stock: 1000 + price: 716 + sale_gamma: 24 + service_level: 0.95 + SKU527: + constraint: null + cost: 131 + init_stock: 350 + max_stock: 1000 + price: 262 + sale_gamma: 70 + service_level: 0.95 + SKU528: + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 84 + max_stock: 1000 + price: 581 + sale_gamma: 21 + service_level: 0.95 + SKU529: + constraint: G(low_profit -> low_stock_constraint) + cost: 298 + init_stock: 248 + max_stock: 1000 + price: 375 + sale_gamma: 31 + service_level: 0.95 + SKU53: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 151 + init_stock: 490 + max_stock: 1000 + price: 191 + sale_gamma: 70 + service_level: 0.95 + SKU530: + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 135 + max_stock: 1000 + price: 205 + sale_gamma: 45 + service_level: 0.95 + SKU531: + constraint: null + cost: 103 + init_stock: 300 + max_stock: 1000 + price: 153 + sale_gamma: 60 + service_level: 0.95 + SKU532: + constraint: null + cost: 351 + init_stock: 539 + max_stock: 1000 + price: 431 + sale_gamma: 77 + service_level: 0.95 + SKU533: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 168 + max_stock: 1000 + price: 340 + sale_gamma: 42 + service_level: 0.95 + SKU534: + constraint: null + cost: 425 + init_stock: 82 + max_stock: 1000 + price: 548 + sale_gamma: 41 + service_level: 0.95 + SKU535: + constraint: null + cost: 304 + init_stock: 430 + max_stock: 1000 + price: 465 + sale_gamma: 86 + service_level: 0.95 + SKU536: + constraint: null + cost: 358 + init_stock: 208 + max_stock: 1000 + price: 415 + sale_gamma: 52 + service_level: 0.95 + SKU537: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 321 + init_stock: 45 + max_stock: 1000 + price: 455 + sale_gamma: 9 + service_level: 0.95 + SKU538: + constraint: null + cost: 457 + init_stock: 200 + max_stock: 1000 + price: 868 + sale_gamma: 50 + service_level: 0.95 + SKU539: + constraint: G(stock_constraint) + cost: 165 + init_stock: 234 + max_stock: 1000 + price: 229 + sale_gamma: 78 + service_level: 0.95 + SKU54: + constraint: G(low_profit -> low_stock_constraint) + cost: 158 + init_stock: 138 + max_stock: 1000 + price: 241 + sale_gamma: 46 + service_level: 0.95 + SKU540: + constraint: null + cost: 388 + init_stock: 294 + max_stock: 1000 + price: 496 + sale_gamma: 42 + service_level: 0.95 + SKU541: + constraint: null + cost: 131 + init_stock: 203 + max_stock: 1000 + price: 213 + sale_gamma: 29 + service_level: 0.95 + SKU542: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 38 + init_stock: 130 + max_stock: 1000 + price: 42 + sale_gamma: 26 + service_level: 0.95 + SKU543: + constraint: G(stock_constraint) + cost: 430 + init_stock: 510 + max_stock: 1000 + price: 718 + sale_gamma: 85 + service_level: 0.95 + SKU544: + constraint: G(stock_constraint) + cost: 346 + init_stock: 194 + max_stock: 1000 + price: 474 + sale_gamma: 97 + service_level: 0.95 + SKU545: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 175 + init_stock: 75 + max_stock: 1000 + price: 323 + sale_gamma: 15 + service_level: 0.95 + SKU546: + constraint: null + cost: 491 + init_stock: 576 + max_stock: 1000 + price: 765 + sale_gamma: 96 + service_level: 0.95 + SKU547: + constraint: G(stock_constraint) + cost: 161 + init_stock: 264 + max_stock: 1000 + price: 251 + sale_gamma: 44 + service_level: 0.95 + SKU548: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 111 + init_stock: 36 + max_stock: 1000 + price: 217 + sale_gamma: 6 + service_level: 0.95 + SKU549: + constraint: G(stock_constraint) + cost: 387 + init_stock: 316 + max_stock: 1000 + price: 510 + sale_gamma: 79 + service_level: 0.95 + SKU55: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 482 + init_stock: 455 + max_stock: 1000 + price: 896 + sale_gamma: 65 + service_level: 0.95 + SKU550: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 259 + init_stock: 282 + max_stock: 1000 + price: 502 + sale_gamma: 94 + service_level: 0.95 + SKU551: + constraint: null + cost: 46 + init_stock: 186 + max_stock: 1000 + price: 60 + sale_gamma: 31 + service_level: 0.95 + SKU552: + constraint: null + cost: 191 + init_stock: 450 + max_stock: 1000 + price: 315 + sale_gamma: 90 + service_level: 0.95 + SKU553: + constraint: G(low_profit -> low_stock_constraint) + cost: 208 + init_stock: 60 + max_stock: 1000 + price: 251 + sale_gamma: 30 + service_level: 0.95 + SKU554: + constraint: null + cost: 340 + init_stock: 82 + max_stock: 1000 + price: 387 + sale_gamma: 41 + service_level: 0.95 + SKU555: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 252 + max_stock: 1000 + price: 684 + sale_gamma: 84 + service_level: 0.95 + SKU556: + constraint: G(stock_constraint) + cost: 110 + init_stock: 90 + max_stock: 1000 + price: 213 + sale_gamma: 30 + service_level: 0.95 + SKU557: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 334 + init_stock: 365 + max_stock: 1000 + price: 661 + sale_gamma: 73 + service_level: 0.95 + SKU558: + constraint: null + cost: 399 + init_stock: 85 + max_stock: 1000 + price: 490 + sale_gamma: 17 + service_level: 0.95 + SKU559: + constraint: null + cost: 313 + init_stock: 270 + max_stock: 1000 + price: 591 + sale_gamma: 54 + service_level: 0.95 + SKU56: + constraint: null + cost: 377 + init_stock: 222 + max_stock: 1000 + price: 671 + sale_gamma: 74 + service_level: 0.95 + SKU560: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 283 + init_stock: 246 + max_stock: 1000 + price: 458 + sale_gamma: 41 + service_level: 0.95 + SKU561: + constraint: null + cost: 202 + init_stock: 312 + max_stock: 1000 + price: 385 + sale_gamma: 39 + service_level: 0.95 + SKU562: + constraint: null + cost: 347 + init_stock: 356 + max_stock: 1000 + price: 666 + sale_gamma: 89 + service_level: 0.95 + SKU563: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 401 + init_stock: 30 + max_stock: 1000 + price: 509 + sale_gamma: 5 + service_level: 0.95 + SKU564: + constraint: null + cost: 109 + init_stock: 63 + max_stock: 1000 + price: 183 + sale_gamma: 9 + service_level: 0.95 + SKU565: + constraint: null + cost: 57 + init_stock: 525 + max_stock: 1000 + price: 90 + sale_gamma: 75 + service_level: 0.95 + SKU566: + constraint: null + cost: 131 + init_stock: 44 + max_stock: 1000 + price: 165 + sale_gamma: 11 + service_level: 0.95 + SKU567: + constraint: G(stock_constraint) + cost: 361 + init_stock: 27 + max_stock: 1000 + price: 465 + sale_gamma: 9 + service_level: 0.95 + SKU568: + constraint: null + cost: 75 + init_stock: 234 + max_stock: 1000 + price: 102 + sale_gamma: 78 + service_level: 0.95 + SKU569: + constraint: null + cost: 473 + init_stock: 192 + max_stock: 1000 + price: 591 + sale_gamma: 48 + service_level: 0.95 + SKU57: + constraint: G(stock_constraint) + cost: 359 + init_stock: 350 + max_stock: 1000 + price: 682 + sale_gamma: 50 + service_level: 0.95 + SKU570: + constraint: null + cost: 388 + init_stock: 581 + max_stock: 1000 + price: 686 + sale_gamma: 83 + service_level: 0.95 + SKU571: + constraint: G(stock_constraint) + cost: 168 + init_stock: 78 + max_stock: 1000 + price: 208 + sale_gamma: 39 + service_level: 0.95 + SKU572: + constraint: null + cost: 26 + init_stock: 225 + max_stock: 1000 + price: 41 + sale_gamma: 45 + service_level: 0.95 + SKU573: + constraint: null + cost: 246 + init_stock: 164 + max_stock: 1000 + price: 391 + sale_gamma: 41 + service_level: 0.95 + SKU574: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 150 + max_stock: 1000 + price: 166 + sale_gamma: 50 + service_level: 0.95 + SKU575: + constraint: null + cost: 237 + init_stock: 237 + max_stock: 1000 + price: 438 + sale_gamma: 79 + service_level: 0.95 + SKU576: + constraint: G(stock_constraint) + cost: 265 + init_stock: 468 + max_stock: 1000 + price: 416 + sale_gamma: 78 + service_level: 0.95 + SKU577: + constraint: G(low_profit -> low_stock_constraint) + cost: 18 + init_stock: 348 + max_stock: 1000 + price: 24 + sale_gamma: 87 + service_level: 0.95 + SKU578: + constraint: null + cost: 100 + init_stock: 284 + max_stock: 1000 + price: 148 + sale_gamma: 71 + service_level: 0.95 + SKU579: + constraint: null + cost: 415 + init_stock: 27 + max_stock: 1000 + price: 771 + sale_gamma: 9 + service_level: 0.95 + SKU58: + constraint: null + cost: 362 + init_stock: 240 + max_stock: 1000 + price: 521 + sale_gamma: 48 + service_level: 0.95 + SKU580: + constraint: null + cost: 203 + init_stock: 15 + max_stock: 1000 + price: 261 + sale_gamma: 5 + service_level: 0.95 + SKU581: + constraint: null + cost: 152 + init_stock: 516 + max_stock: 1000 + price: 185 + sale_gamma: 86 + service_level: 0.95 + SKU582: + constraint: G(stock_constraint) + cost: 359 + init_stock: 480 + max_stock: 1000 + price: 567 + sale_gamma: 96 + service_level: 0.95 + SKU583: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 86 + init_stock: 261 + max_stock: 1000 + price: 98 + sale_gamma: 87 + service_level: 0.95 + SKU584: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 33 + init_stock: 270 + max_stock: 1000 + price: 36 + sale_gamma: 45 + service_level: 0.95 + SKU585: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 100 + max_stock: 1000 + price: 53 + sale_gamma: 20 + service_level: 0.95 + SKU586: + constraint: G(stock_constraint) + cost: 61 + init_stock: 264 + max_stock: 1000 + price: 94 + sale_gamma: 44 + service_level: 0.95 + SKU587: + constraint: null + cost: 290 + init_stock: 468 + max_stock: 1000 + price: 423 + sale_gamma: 78 + service_level: 0.95 + SKU588: + constraint: G(stock_constraint) + cost: 476 + init_stock: 176 + max_stock: 1000 + price: 885 + sale_gamma: 44 + service_level: 0.95 + SKU589: + constraint: null + cost: 244 + init_stock: 170 + max_stock: 1000 + price: 380 + sale_gamma: 34 + service_level: 0.95 + SKU59: + constraint: G(low_profit -> low_stock_constraint) + cost: 145 + init_stock: 255 + max_stock: 1000 + price: 275 + sale_gamma: 51 + service_level: 0.95 + SKU590: + constraint: null + cost: 70 + init_stock: 93 + max_stock: 1000 + price: 103 + sale_gamma: 31 + service_level: 0.95 + SKU591: + constraint: G(stock_constraint) + cost: 206 + init_stock: 504 + max_stock: 1000 + price: 298 + sale_gamma: 84 + service_level: 0.95 + SKU592: + constraint: null + cost: 218 + init_stock: 276 + max_stock: 1000 + price: 418 + sale_gamma: 46 + service_level: 0.95 + SKU593: + constraint: null + cost: 124 + init_stock: 88 + max_stock: 1000 + price: 221 + sale_gamma: 44 + service_level: 0.95 + SKU594: + constraint: null + cost: 238 + init_stock: 150 + max_stock: 1000 + price: 459 + sale_gamma: 50 + service_level: 0.95 + SKU595: + constraint: null + cost: 73 + init_stock: 172 + max_stock: 1000 + price: 107 + sale_gamma: 43 + service_level: 0.95 + SKU596: + constraint: null + cost: 432 + init_stock: 35 + max_stock: 1000 + price: 540 + sale_gamma: 7 + service_level: 0.95 + SKU597: + constraint: G(stock_constraint) + cost: 252 + init_stock: 435 + max_stock: 1000 + price: 451 + sale_gamma: 87 + service_level: 0.95 + SKU598: + constraint: null + cost: 348 + init_stock: 28 + max_stock: 1000 + price: 612 + sale_gamma: 14 + service_level: 0.95 + SKU599: + constraint: null + cost: 54 + init_stock: 204 + max_stock: 1000 + price: 66 + sale_gamma: 68 + service_level: 0.95 + SKU6: + constraint: null + cost: 122 + init_stock: 616 + max_stock: 1000 + price: 195 + sale_gamma: 88 + service_level: 0.95 + SKU60: + constraint: G(stock_constraint) + cost: 200 + init_stock: 234 + max_stock: 1000 + price: 346 + sale_gamma: 39 + service_level: 0.95 + SKU600: + constraint: null + cost: 386 + init_stock: 325 + max_stock: 1000 + price: 505 + sale_gamma: 65 + service_level: 0.95 + SKU601: + constraint: null + cost: 138 + init_stock: 273 + max_stock: 1000 + price: 198 + sale_gamma: 39 + service_level: 0.95 + SKU602: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 184 + init_stock: 588 + max_stock: 1000 + price: 318 + sale_gamma: 98 + service_level: 0.95 + SKU603: + constraint: G(low_profit -> low_stock_constraint) + cost: 278 + init_stock: 252 + max_stock: 1000 + price: 550 + sale_gamma: 42 + service_level: 0.95 + SKU604: + constraint: G(stock_constraint) + cost: 270 + init_stock: 400 + max_stock: 1000 + price: 378 + sale_gamma: 50 + service_level: 0.95 + SKU605: + constraint: null + cost: 288 + init_stock: 120 + max_stock: 1000 + price: 443 + sale_gamma: 24 + service_level: 0.95 + SKU606: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 114 + init_stock: 66 + max_stock: 1000 + price: 161 + sale_gamma: 11 + service_level: 0.95 + SKU607: + constraint: null + cost: 208 + init_stock: 395 + max_stock: 1000 + price: 359 + sale_gamma: 79 + service_level: 0.95 + SKU608: + constraint: null + cost: 39 + init_stock: 365 + max_stock: 1000 + price: 49 + sale_gamma: 73 + service_level: 0.95 + SKU609: + constraint: null + cost: 102 + init_stock: 114 + max_stock: 1000 + price: 171 + sale_gamma: 19 + service_level: 0.95 + SKU61: + constraint: null + cost: 461 + init_stock: 150 + max_stock: 1000 + price: 645 + sale_gamma: 75 + service_level: 0.95 + SKU610: + constraint: null + cost: 449 + init_stock: 204 + max_stock: 1000 + price: 749 + sale_gamma: 68 + service_level: 0.95 + SKU611: + constraint: G(low_profit -> low_stock_constraint) + cost: 306 + init_stock: 539 + max_stock: 1000 + price: 489 + sale_gamma: 77 + service_level: 0.95 + SKU612: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 391 + init_stock: 616 + max_stock: 1000 + price: 613 + sale_gamma: 88 + service_level: 0.95 + SKU613: + constraint: G(stock_constraint) + cost: 174 + init_stock: 56 + max_stock: 1000 + price: 254 + sale_gamma: 7 + service_level: 0.95 + SKU614: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 173 + init_stock: 45 + max_stock: 1000 + price: 318 + sale_gamma: 15 + service_level: 0.95 + SKU615: + constraint: G(stock_constraint) + cost: 330 + init_stock: 364 + max_stock: 1000 + price: 432 + sale_gamma: 91 + service_level: 0.95 + SKU616: + constraint: G(low_profit -> low_stock_constraint) + cost: 232 + init_stock: 219 + max_stock: 1000 + price: 382 + sale_gamma: 73 + service_level: 0.95 + SKU617: + constraint: null + cost: 203 + init_stock: 420 + max_stock: 1000 + price: 227 + sale_gamma: 60 + service_level: 0.95 + SKU618: + constraint: G(stock_constraint) + cost: 77 + init_stock: 138 + max_stock: 1000 + price: 97 + sale_gamma: 23 + service_level: 0.95 + SKU619: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 344 + max_stock: 1000 + price: 359 + sale_gamma: 86 + service_level: 0.95 + SKU62: + constraint: null + cost: 124 + init_stock: 36 + max_stock: 1000 + price: 168 + sale_gamma: 9 + service_level: 0.95 + SKU620: + constraint: null + cost: 231 + init_stock: 156 + max_stock: 1000 + price: 267 + sale_gamma: 39 + service_level: 0.95 + SKU621: + constraint: null + cost: 199 + init_stock: 216 + max_stock: 1000 + price: 332 + sale_gamma: 72 + service_level: 0.95 + SKU622: + constraint: null + cost: 253 + init_stock: 455 + max_stock: 1000 + price: 468 + sale_gamma: 65 + service_level: 0.95 + SKU623: + constraint: null + cost: 335 + init_stock: 220 + max_stock: 1000 + price: 368 + sale_gamma: 55 + service_level: 0.95 + SKU624: + constraint: null + cost: 482 + init_stock: 270 + max_stock: 1000 + price: 824 + sale_gamma: 54 + service_level: 0.95 + SKU625: + constraint: null + cost: 46 + init_stock: 445 + max_stock: 1000 + price: 60 + sale_gamma: 89 + service_level: 0.95 + SKU626: + constraint: null + cost: 451 + init_stock: 534 + max_stock: 1000 + price: 694 + sale_gamma: 89 + service_level: 0.95 + SKU627: + constraint: null + cost: 220 + init_stock: 656 + max_stock: 1000 + price: 264 + sale_gamma: 82 + service_level: 0.95 + SKU628: + constraint: null + cost: 124 + init_stock: 156 + max_stock: 1000 + price: 229 + sale_gamma: 26 + service_level: 0.95 + SKU629: + constraint: G(stock_constraint) + cost: 353 + init_stock: 460 + max_stock: 1000 + price: 515 + sale_gamma: 92 + service_level: 0.95 + SKU63: + constraint: null + cost: 68 + init_stock: 70 + max_stock: 1000 + price: 86 + sale_gamma: 14 + service_level: 0.95 + SKU630: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 122 + init_stock: 34 + max_stock: 1000 + price: 137 + sale_gamma: 17 + service_level: 0.95 + SKU631: + constraint: null + cost: 296 + init_stock: 196 + max_stock: 1000 + price: 399 + sale_gamma: 49 + service_level: 0.95 + SKU632: + constraint: null + cost: 85 + init_stock: 470 + max_stock: 1000 + price: 141 + sale_gamma: 94 + service_level: 0.95 + SKU633: + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 402 + max_stock: 1000 + price: 228 + sale_gamma: 67 + service_level: 0.95 + SKU634: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 17 + init_stock: 438 + max_stock: 1000 + price: 30 + sale_gamma: 73 + service_level: 0.95 + SKU635: + constraint: null + cost: 341 + init_stock: 372 + max_stock: 1000 + price: 654 + sale_gamma: 93 + service_level: 0.95 + SKU636: + constraint: G(stock_constraint) + cost: 385 + init_stock: 111 + max_stock: 1000 + price: 739 + sale_gamma: 37 + service_level: 0.95 + SKU637: + constraint: G(low_profit -> low_stock_constraint) + cost: 83 + init_stock: 305 + max_stock: 1000 + price: 118 + sale_gamma: 61 + service_level: 0.95 + SKU638: + constraint: G(stock_constraint) + cost: 375 + init_stock: 16 + max_stock: 1000 + price: 536 + sale_gamma: 8 + service_level: 0.95 + SKU639: + constraint: null + cost: 327 + init_stock: 160 + max_stock: 1000 + price: 487 + sale_gamma: 40 + service_level: 0.95 + SKU64: + constraint: G(low_profit -> low_stock_constraint) + cost: 36 + init_stock: 133 + max_stock: 1000 + price: 42 + sale_gamma: 19 + service_level: 0.95 + SKU640: + constraint: G(low_profit -> low_stock_constraint) + cost: 275 + init_stock: 546 + max_stock: 1000 + price: 382 + sale_gamma: 91 + service_level: 0.95 + SKU641: + constraint: null + cost: 365 + init_stock: 486 + max_stock: 1000 + price: 445 + sale_gamma: 81 + service_level: 0.95 + SKU642: + constraint: null + cost: 414 + init_stock: 150 + max_stock: 1000 + price: 554 + sale_gamma: 25 + service_level: 0.95 + SKU643: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 358 + init_stock: 322 + max_stock: 1000 + price: 461 + sale_gamma: 46 + service_level: 0.95 + SKU644: + constraint: G(low_profit -> low_stock_constraint) + cost: 46 + init_stock: 410 + max_stock: 1000 + price: 61 + sale_gamma: 82 + service_level: 0.95 + SKU645: + constraint: null + cost: 467 + init_stock: 594 + max_stock: 1000 + price: 742 + sale_gamma: 99 + service_level: 0.95 + SKU646: + constraint: G(low_profit -> low_stock_constraint) + cost: 189 + init_stock: 91 + max_stock: 1000 + price: 268 + sale_gamma: 13 + service_level: 0.95 + SKU647: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 303 + init_stock: 462 + max_stock: 1000 + price: 369 + sale_gamma: 77 + service_level: 0.95 + SKU648: + constraint: G(stock_constraint) + cost: 226 + init_stock: 110 + max_stock: 1000 + price: 336 + sale_gamma: 55 + service_level: 0.95 + SKU649: + constraint: null + cost: 198 + init_stock: 175 + max_stock: 1000 + price: 229 + sale_gamma: 25 + service_level: 0.95 + SKU65: + constraint: null + cost: 15 + init_stock: 658 + max_stock: 1000 + price: 25 + sale_gamma: 94 + service_level: 0.95 + SKU650: + constraint: null + cost: 351 + init_stock: 224 + max_stock: 1000 + price: 498 + sale_gamma: 56 + service_level: 0.95 + SKU651: + constraint: null + cost: 380 + init_stock: 672 + max_stock: 1000 + price: 596 + sale_gamma: 96 + service_level: 0.95 + SKU652: + constraint: null + cost: 123 + init_stock: 280 + max_stock: 1000 + price: 168 + sale_gamma: 40 + service_level: 0.95 + SKU653: + constraint: G(low_profit -> low_stock_constraint) + cost: 479 + init_stock: 384 + max_stock: 1000 + price: 661 + sale_gamma: 96 + service_level: 0.95 + SKU654: + constraint: null + cost: 66 + init_stock: 32 + max_stock: 1000 + price: 110 + sale_gamma: 8 + service_level: 0.95 + SKU655: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 126 + max_stock: 1000 + price: 479 + sale_gamma: 42 + service_level: 0.95 + SKU656: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 53 + init_stock: 504 + max_stock: 1000 + price: 81 + sale_gamma: 72 + service_level: 0.95 + SKU657: + constraint: G(low_profit -> low_stock_constraint) + cost: 73 + init_stock: 567 + max_stock: 1000 + price: 110 + sale_gamma: 81 + service_level: 0.95 + SKU658: + constraint: null + cost: 70 + init_stock: 252 + max_stock: 1000 + price: 107 + sale_gamma: 36 + service_level: 0.95 + SKU659: + constraint: null + cost: 21 + init_stock: 336 + max_stock: 1000 + price: 27 + sale_gamma: 56 + service_level: 0.95 + SKU66: + constraint: null + cost: 143 + init_stock: 360 + max_stock: 1000 + price: 230 + sale_gamma: 90 + service_level: 0.95 + SKU660: + constraint: null + cost: 487 + init_stock: 415 + max_stock: 1000 + price: 560 + sale_gamma: 83 + service_level: 0.95 + SKU661: + constraint: null + cost: 344 + init_stock: 296 + max_stock: 1000 + price: 581 + sale_gamma: 74 + service_level: 0.95 + SKU662: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 266 + max_stock: 1000 + price: 628 + sale_gamma: 38 + service_level: 0.95 + SKU663: + constraint: G(stock_constraint) + cost: 418 + init_stock: 320 + max_stock: 1000 + price: 518 + sale_gamma: 40 + service_level: 0.95 + SKU664: + constraint: G(stock_constraint) + cost: 351 + init_stock: 420 + max_stock: 1000 + price: 494 + sale_gamma: 84 + service_level: 0.95 + SKU665: + constraint: null + cost: 493 + init_stock: 344 + max_stock: 1000 + price: 734 + sale_gamma: 43 + service_level: 0.95 + SKU666: + constraint: G(stock_constraint) + cost: 341 + init_stock: 176 + max_stock: 1000 + price: 572 + sale_gamma: 88 + service_level: 0.95 + SKU667: + constraint: null + cost: 325 + init_stock: 52 + max_stock: 1000 + price: 562 + sale_gamma: 13 + service_level: 0.95 + SKU668: + constraint: G(low_profit -> low_stock_constraint) + cost: 286 + init_stock: 300 + max_stock: 1000 + price: 463 + sale_gamma: 60 + service_level: 0.95 + SKU669: + constraint: null + cost: 74 + init_stock: 96 + max_stock: 1000 + price: 83 + sale_gamma: 16 + service_level: 0.95 + SKU67: + constraint: G(low_profit -> low_stock_constraint) + cost: 247 + init_stock: 159 + max_stock: 1000 + price: 454 + sale_gamma: 53 + service_level: 0.95 + SKU670: + constraint: G(stock_constraint) + cost: 289 + init_stock: 258 + max_stock: 1000 + price: 430 + sale_gamma: 86 + service_level: 0.95 + SKU671: + constraint: G(stock_constraint) + cost: 267 + init_stock: 332 + max_stock: 1000 + price: 296 + sale_gamma: 83 + service_level: 0.95 + SKU672: + constraint: null + cost: 369 + init_stock: 78 + max_stock: 1000 + price: 420 + sale_gamma: 13 + service_level: 0.95 + SKU673: + constraint: G(low_profit -> low_stock_constraint) + cost: 322 + init_stock: 123 + max_stock: 1000 + price: 418 + sale_gamma: 41 + service_level: 0.95 + SKU674: + constraint: null + cost: 223 + init_stock: 66 + max_stock: 1000 + price: 263 + sale_gamma: 22 + service_level: 0.95 + SKU675: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 438 + init_stock: 99 + max_stock: 1000 + price: 529 + sale_gamma: 33 + service_level: 0.95 + SKU676: + constraint: null + cost: 244 + init_stock: 366 + max_stock: 1000 + price: 275 + sale_gamma: 61 + service_level: 0.95 + SKU677: + constraint: null + cost: 425 + init_stock: 176 + max_stock: 1000 + price: 658 + sale_gamma: 44 + service_level: 0.95 + SKU678: + constraint: null + cost: 168 + init_stock: 216 + max_stock: 1000 + price: 199 + sale_gamma: 36 + service_level: 0.95 + SKU679: + constraint: null + cost: 395 + init_stock: 132 + max_stock: 1000 + price: 734 + sale_gamma: 44 + service_level: 0.95 + SKU68: + constraint: G(stock_constraint) + cost: 169 + init_stock: 56 + max_stock: 1000 + price: 239 + sale_gamma: 14 + service_level: 0.95 + SKU680: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 260 + init_stock: 180 + max_stock: 1000 + price: 327 + sale_gamma: 30 + service_level: 0.95 + SKU681: + constraint: G(low_profit -> low_stock_constraint) + cost: 464 + init_stock: 776 + max_stock: 1000 + price: 510 + sale_gamma: 97 + service_level: 0.95 + SKU682: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 370 + init_stock: 318 + max_stock: 1000 + price: 407 + sale_gamma: 53 + service_level: 0.95 + SKU683: + constraint: G(low_profit -> low_stock_constraint) + cost: 327 + init_stock: 536 + max_stock: 1000 + price: 608 + sale_gamma: 67 + service_level: 0.95 + SKU684: + constraint: G(stock_constraint) + cost: 355 + init_stock: 158 + max_stock: 1000 + price: 401 + sale_gamma: 79 + service_level: 0.95 + SKU685: + constraint: null + cost: 422 + init_stock: 270 + max_stock: 1000 + price: 493 + sale_gamma: 45 + service_level: 0.95 + SKU686: + constraint: G(low_profit -> low_stock_constraint) + cost: 63 + init_stock: 378 + max_stock: 1000 + price: 122 + sale_gamma: 63 + service_level: 0.95 + SKU687: + constraint: G(low_profit -> low_stock_constraint) + cost: 34 + init_stock: 456 + max_stock: 1000 + price: 43 + sale_gamma: 76 + service_level: 0.95 + SKU688: + constraint: G(low_profit -> low_stock_constraint) + cost: 484 + init_stock: 462 + max_stock: 1000 + price: 759 + sale_gamma: 77 + service_level: 0.95 + SKU689: + constraint: G(stock_constraint) + cost: 499 + init_stock: 75 + max_stock: 1000 + price: 808 + sale_gamma: 15 + service_level: 0.95 + SKU69: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 176 + init_stock: 402 + max_stock: 1000 + price: 197 + sale_gamma: 67 + service_level: 0.95 + SKU690: + constraint: null + cost: 437 + init_stock: 415 + max_stock: 1000 + price: 498 + sale_gamma: 83 + service_level: 0.95 + SKU691: + constraint: null + cost: 17 + init_stock: 632 + max_stock: 1000 + price: 18 + sale_gamma: 79 + service_level: 0.95 + SKU692: + constraint: G(stock_constraint) + cost: 225 + init_stock: 195 + max_stock: 1000 + price: 258 + sale_gamma: 65 + service_level: 0.95 + SKU693: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 19 + init_stock: 244 + max_stock: 1000 + price: 21 + sale_gamma: 61 + service_level: 0.95 + SKU694: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 208 + init_stock: 300 + max_stock: 1000 + price: 386 + sale_gamma: 75 + service_level: 0.95 + SKU695: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 190 + init_stock: 49 + max_stock: 1000 + price: 361 + sale_gamma: 7 + service_level: 0.95 + SKU696: + constraint: G(low_profit -> low_stock_constraint) + cost: 348 + init_stock: 290 + max_stock: 1000 + price: 643 + sale_gamma: 58 + service_level: 0.95 + SKU697: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 455 + init_stock: 320 + max_stock: 1000 + price: 641 + sale_gamma: 80 + service_level: 0.95 + SKU698: + constraint: null + cost: 198 + init_stock: 488 + max_stock: 1000 + price: 316 + sale_gamma: 61 + service_level: 0.95 + SKU699: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 100 + max_stock: 1000 + price: 58 + sale_gamma: 20 + service_level: 0.95 + SKU7: + constraint: null + cost: 101 + init_stock: 480 + max_stock: 1000 + price: 131 + sale_gamma: 96 + service_level: 0.95 + SKU70: + constraint: G(stock_constraint) + cost: 186 + init_stock: 140 + max_stock: 1000 + price: 288 + sale_gamma: 35 + service_level: 0.95 + SKU700: + constraint: G(low_profit -> low_stock_constraint) + cost: 74 + init_stock: 45 + max_stock: 1000 + price: 130 + sale_gamma: 9 + service_level: 0.95 + SKU701: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 399 + init_stock: 462 + max_stock: 1000 + price: 770 + sale_gamma: 77 + service_level: 0.95 + SKU702: + constraint: G(stock_constraint) + cost: 82 + init_stock: 204 + max_stock: 1000 + price: 163 + sale_gamma: 34 + service_level: 0.95 + SKU703: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 363 + init_stock: 76 + max_stock: 1000 + price: 602 + sale_gamma: 38 + service_level: 0.95 + SKU704: + constraint: G(stock_constraint) + cost: 384 + init_stock: 261 + max_stock: 1000 + price: 518 + sale_gamma: 87 + service_level: 0.95 + SKU705: + constraint: null + cost: 128 + init_stock: 378 + max_stock: 1000 + price: 236 + sale_gamma: 63 + service_level: 0.95 + SKU706: + constraint: null + cost: 182 + init_stock: 455 + max_stock: 1000 + price: 242 + sale_gamma: 91 + service_level: 0.95 + SKU707: + constraint: null + cost: 18 + init_stock: 276 + max_stock: 1000 + price: 35 + sale_gamma: 69 + service_level: 0.95 + SKU708: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 178 + init_stock: 112 + max_stock: 1000 + price: 334 + sale_gamma: 28 + service_level: 0.95 + SKU709: + constraint: G(stock_constraint) + cost: 102 + init_stock: 212 + max_stock: 1000 + price: 173 + sale_gamma: 53 + service_level: 0.95 + SKU71: + constraint: null + cost: 315 + init_stock: 225 + max_stock: 1000 + price: 589 + sale_gamma: 45 + service_level: 0.95 + SKU710: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 252 + init_stock: 95 + max_stock: 1000 + price: 337 + sale_gamma: 19 + service_level: 0.95 + SKU711: + constraint: null + cost: 281 + init_stock: 414 + max_stock: 1000 + price: 320 + sale_gamma: 69 + service_level: 0.95 + SKU712: + constraint: null + cost: 232 + init_stock: 77 + max_stock: 1000 + price: 438 + sale_gamma: 11 + service_level: 0.95 + SKU713: + constraint: null + cost: 285 + init_stock: 129 + max_stock: 1000 + price: 413 + sale_gamma: 43 + service_level: 0.95 + SKU714: + constraint: null + cost: 79 + init_stock: 287 + max_stock: 1000 + price: 106 + sale_gamma: 41 + service_level: 0.95 + SKU715: + constraint: G(stock_constraint) + cost: 234 + init_stock: 34 + max_stock: 1000 + price: 271 + sale_gamma: 17 + service_level: 0.95 + SKU716: + constraint: null + cost: 70 + init_stock: 450 + max_stock: 1000 + price: 123 + sale_gamma: 75 + service_level: 0.95 + SKU717: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 345 + init_stock: 56 + max_stock: 1000 + price: 382 + sale_gamma: 8 + service_level: 0.95 + SKU718: + constraint: null + cost: 357 + init_stock: 174 + max_stock: 1000 + price: 692 + sale_gamma: 58 + service_level: 0.95 + SKU719: + constraint: null + cost: 340 + init_stock: 455 + max_stock: 1000 + price: 384 + sale_gamma: 65 + service_level: 0.95 + SKU72: + constraint: null + cost: 458 + init_stock: 595 + max_stock: 1000 + price: 870 + sale_gamma: 85 + service_level: 0.95 + SKU720: + constraint: null + cost: 325 + init_stock: 294 + max_stock: 1000 + price: 503 + sale_gamma: 42 + service_level: 0.95 + SKU721: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 73 + init_stock: 44 + max_stock: 1000 + price: 143 + sale_gamma: 11 + service_level: 0.95 + SKU722: + constraint: G(stock_constraint) + cost: 392 + init_stock: 194 + max_stock: 1000 + price: 572 + sale_gamma: 97 + service_level: 0.95 + SKU723: + constraint: null + cost: 318 + init_stock: 356 + max_stock: 1000 + price: 442 + sale_gamma: 89 + service_level: 0.95 + SKU724: + constraint: G(low_profit -> low_stock_constraint) + cost: 400 + init_stock: 198 + max_stock: 1000 + price: 520 + sale_gamma: 33 + service_level: 0.95 + SKU725: + constraint: null + cost: 175 + init_stock: 148 + max_stock: 1000 + price: 309 + sale_gamma: 37 + service_level: 0.95 + SKU726: + constraint: G(low_profit -> low_stock_constraint) + cost: 458 + init_stock: 356 + max_stock: 1000 + price: 567 + sale_gamma: 89 + service_level: 0.95 + SKU727: + constraint: null + cost: 418 + init_stock: 285 + max_stock: 1000 + price: 526 + sale_gamma: 57 + service_level: 0.95 + SKU728: + constraint: null + cost: 475 + init_stock: 185 + max_stock: 1000 + price: 864 + sale_gamma: 37 + service_level: 0.95 + SKU729: + constraint: null + cost: 324 + init_stock: 252 + max_stock: 1000 + price: 476 + sale_gamma: 36 + service_level: 0.95 + SKU73: + constraint: null + cost: 212 + init_stock: 216 + max_stock: 1000 + price: 248 + sale_gamma: 54 + service_level: 0.95 + SKU730: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 16 + init_stock: 65 + max_stock: 1000 + price: 19 + sale_gamma: 13 + service_level: 0.95 + SKU731: + constraint: G(stock_constraint) + cost: 88 + init_stock: 410 + max_stock: 1000 + price: 155 + sale_gamma: 82 + service_level: 0.95 + SKU732: + constraint: null + cost: 41 + init_stock: 434 + max_stock: 1000 + price: 73 + sale_gamma: 62 + service_level: 0.95 + SKU733: + constraint: G(stock_constraint) + cost: 315 + init_stock: 104 + max_stock: 1000 + price: 541 + sale_gamma: 26 + service_level: 0.95 + SKU734: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 255 + max_stock: 1000 + price: 71 + sale_gamma: 51 + service_level: 0.95 + SKU735: + constraint: null + cost: 266 + init_stock: 594 + max_stock: 1000 + price: 409 + sale_gamma: 99 + service_level: 0.95 + SKU736: + constraint: G(stock_constraint) + cost: 368 + init_stock: 75 + max_stock: 1000 + price: 632 + sale_gamma: 25 + service_level: 0.95 + SKU737: + constraint: null + cost: 475 + init_stock: 93 + max_stock: 1000 + price: 655 + sale_gamma: 31 + service_level: 0.95 + SKU738: + constraint: null + cost: 185 + init_stock: 192 + max_stock: 1000 + price: 246 + sale_gamma: 48 + service_level: 0.95 + SKU739: + constraint: G(low_profit -> low_stock_constraint) + cost: 475 + init_stock: 296 + max_stock: 1000 + price: 612 + sale_gamma: 74 + service_level: 0.95 + SKU74: + constraint: null + cost: 159 + init_stock: 168 + max_stock: 1000 + price: 189 + sale_gamma: 42 + service_level: 0.95 + SKU740: + constraint: null + cost: 390 + init_stock: 256 + max_stock: 1000 + price: 549 + sale_gamma: 64 + service_level: 0.95 + SKU741: + constraint: G(stock_constraint) + cost: 91 + init_stock: 280 + max_stock: 1000 + price: 112 + sale_gamma: 56 + service_level: 0.95 + SKU742: + constraint: G(stock_constraint) + cost: 188 + init_stock: 110 + max_stock: 1000 + price: 374 + sale_gamma: 22 + service_level: 0.95 + SKU743: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 217 + init_stock: 301 + max_stock: 1000 + price: 431 + sale_gamma: 43 + service_level: 0.95 + SKU744: + constraint: G(low_profit -> low_stock_constraint) + cost: 379 + init_stock: 290 + max_stock: 1000 + price: 579 + sale_gamma: 58 + service_level: 0.95 + SKU745: + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 368 + max_stock: 1000 + price: 376 + sale_gamma: 92 + service_level: 0.95 + SKU746: + constraint: null + cost: 437 + init_stock: 160 + max_stock: 1000 + price: 624 + sale_gamma: 40 + service_level: 0.95 + SKU747: + constraint: null + cost: 373 + init_stock: 474 + max_stock: 1000 + price: 589 + sale_gamma: 79 + service_level: 0.95 + SKU748: + constraint: null + cost: 275 + init_stock: 330 + max_stock: 1000 + price: 464 + sale_gamma: 66 + service_level: 0.95 + SKU749: + constraint: null + cost: 394 + init_stock: 106 + max_stock: 1000 + price: 705 + sale_gamma: 53 + service_level: 0.95 + SKU75: + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 76 + max_stock: 1000 + price: 217 + sale_gamma: 19 + service_level: 0.95 + SKU750: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 310 + max_stock: 1000 + price: 463 + sale_gamma: 62 + service_level: 0.95 + SKU751: + constraint: null + cost: 369 + init_stock: 325 + max_stock: 1000 + price: 428 + sale_gamma: 65 + service_level: 0.95 + SKU752: + constraint: null + cost: 259 + init_stock: 546 + max_stock: 1000 + price: 435 + sale_gamma: 78 + service_level: 0.95 + SKU753: + constraint: G(stock_constraint) + cost: 77 + init_stock: 325 + max_stock: 1000 + price: 128 + sale_gamma: 65 + service_level: 0.95 + SKU754: + constraint: G(stock_constraint) + cost: 387 + init_stock: 91 + max_stock: 1000 + price: 545 + sale_gamma: 13 + service_level: 0.95 + SKU755: + constraint: null + cost: 354 + init_stock: 560 + max_stock: 1000 + price: 523 + sale_gamma: 80 + service_level: 0.95 + SKU756: + constraint: G(stock_constraint) + cost: 246 + init_stock: 360 + max_stock: 1000 + price: 361 + sale_gamma: 90 + service_level: 0.95 + SKU757: + constraint: null + cost: 40 + init_stock: 399 + max_stock: 1000 + price: 76 + sale_gamma: 57 + service_level: 0.95 + SKU758: + constraint: null + cost: 241 + init_stock: 160 + max_stock: 1000 + price: 457 + sale_gamma: 40 + service_level: 0.95 + SKU759: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 435 + init_stock: 76 + max_stock: 1000 + price: 648 + sale_gamma: 38 + service_level: 0.95 + SKU76: + constraint: null + cost: 147 + init_stock: 256 + max_stock: 1000 + price: 191 + sale_gamma: 64 + service_level: 0.95 + SKU760: + constraint: null + cost: 176 + init_stock: 255 + max_stock: 1000 + price: 279 + sale_gamma: 51 + service_level: 0.95 + SKU761: + constraint: G(stock_constraint) + cost: 224 + init_stock: 275 + max_stock: 1000 + price: 250 + sale_gamma: 55 + service_level: 0.95 + SKU762: + constraint: G(stock_constraint) + cost: 264 + init_stock: 153 + max_stock: 1000 + price: 351 + sale_gamma: 51 + service_level: 0.95 + SKU763: + constraint: null + cost: 385 + init_stock: 316 + max_stock: 1000 + price: 677 + sale_gamma: 79 + service_level: 0.95 + SKU764: + constraint: null + cost: 349 + init_stock: 68 + max_stock: 1000 + price: 596 + sale_gamma: 34 + service_level: 0.95 + SKU765: + constraint: null + cost: 345 + init_stock: 52 + max_stock: 1000 + price: 472 + sale_gamma: 13 + service_level: 0.95 + SKU766: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 478 + init_stock: 140 + max_stock: 1000 + price: 855 + sale_gamma: 20 + service_level: 0.95 + SKU767: + constraint: null + cost: 95 + init_stock: 456 + max_stock: 1000 + price: 160 + sale_gamma: 76 + service_level: 0.95 + SKU768: + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 644 + max_stock: 1000 + price: 244 + sale_gamma: 92 + service_level: 0.95 + SKU769: + constraint: null + cost: 24 + init_stock: 87 + max_stock: 1000 + price: 43 + sale_gamma: 29 + service_level: 0.95 + SKU77: + constraint: null + cost: 409 + init_stock: 144 + max_stock: 1000 + price: 687 + sale_gamma: 72 + service_level: 0.95 + SKU770: + constraint: null + cost: 150 + init_stock: 186 + max_stock: 1000 + price: 166 + sale_gamma: 62 + service_level: 0.95 + SKU771: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 101 + init_stock: 35 + max_stock: 1000 + price: 182 + sale_gamma: 7 + service_level: 0.95 + SKU772: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 36 + max_stock: 1000 + price: 281 + sale_gamma: 6 + service_level: 0.95 + SKU773: + constraint: null + cost: 84 + init_stock: 368 + max_stock: 1000 + price: 117 + sale_gamma: 92 + service_level: 0.95 + SKU774: + constraint: null + cost: 447 + init_stock: 118 + max_stock: 1000 + price: 746 + sale_gamma: 59 + service_level: 0.95 + SKU775: + constraint: null + cost: 175 + init_stock: 688 + max_stock: 1000 + price: 192 + sale_gamma: 86 + service_level: 0.95 + SKU776: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 103 + init_stock: 425 + max_stock: 1000 + price: 172 + sale_gamma: 85 + service_level: 0.95 + SKU777: + constraint: null + cost: 292 + init_stock: 171 + max_stock: 1000 + price: 347 + sale_gamma: 57 + service_level: 0.95 + SKU778: + constraint: null + cost: 203 + init_stock: 40 + max_stock: 1000 + price: 288 + sale_gamma: 8 + service_level: 0.95 + SKU779: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 117 + init_stock: 215 + max_stock: 1000 + price: 186 + sale_gamma: 43 + service_level: 0.95 + SKU78: + constraint: null + cost: 234 + init_stock: 91 + max_stock: 1000 + price: 400 + sale_gamma: 13 + service_level: 0.95 + SKU780: + constraint: G(low_profit -> low_stock_constraint) + cost: 69 + init_stock: 410 + max_stock: 1000 + price: 102 + sale_gamma: 82 + service_level: 0.95 + SKU781: + constraint: null + cost: 372 + init_stock: 144 + max_stock: 1000 + price: 528 + sale_gamma: 36 + service_level: 0.95 + SKU782: + constraint: G(low_profit -> low_stock_constraint) + cost: 27 + init_stock: 70 + max_stock: 1000 + price: 35 + sale_gamma: 10 + service_level: 0.95 + SKU783: + constraint: null + cost: 87 + init_stock: 324 + max_stock: 1000 + price: 139 + sale_gamma: 81 + service_level: 0.95 + SKU784: + constraint: null + cost: 309 + init_stock: 312 + max_stock: 1000 + price: 577 + sale_gamma: 52 + service_level: 0.95 + SKU785: + constraint: null + cost: 191 + init_stock: 210 + max_stock: 1000 + price: 255 + sale_gamma: 70 + service_level: 0.95 + SKU786: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 91 + init_stock: 132 + max_stock: 1000 + price: 102 + sale_gamma: 22 + service_level: 0.95 + SKU787: + constraint: null + cost: 360 + init_stock: 366 + max_stock: 1000 + price: 658 + sale_gamma: 61 + service_level: 0.95 + SKU788: + constraint: G(stock_constraint) + cost: 351 + init_stock: 112 + max_stock: 1000 + price: 417 + sale_gamma: 28 + service_level: 0.95 + SKU789: + constraint: G(stock_constraint) + cost: 153 + init_stock: 232 + max_stock: 1000 + price: 298 + sale_gamma: 58 + service_level: 0.95 + SKU79: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 245 + init_stock: 66 + max_stock: 1000 + price: 276 + sale_gamma: 11 + service_level: 0.95 + SKU790: + constraint: null + cost: 417 + init_stock: 330 + max_stock: 1000 + price: 704 + sale_gamma: 66 + service_level: 0.95 + SKU791: + constraint: G(stock_constraint) + cost: 134 + init_stock: 56 + max_stock: 1000 + price: 155 + sale_gamma: 28 + service_level: 0.95 + SKU792: + constraint: G(low_profit -> low_stock_constraint) + cost: 313 + init_stock: 84 + max_stock: 1000 + price: 494 + sale_gamma: 21 + service_level: 0.95 + SKU793: + constraint: null + cost: 195 + init_stock: 532 + max_stock: 1000 + price: 282 + sale_gamma: 76 + service_level: 0.95 + SKU794: + constraint: null + cost: 325 + init_stock: 400 + max_stock: 1000 + price: 454 + sale_gamma: 80 + service_level: 0.95 + SKU795: + constraint: null + cost: 276 + init_stock: 288 + max_stock: 1000 + price: 549 + sale_gamma: 48 + service_level: 0.95 + SKU796: + constraint: null + cost: 447 + init_stock: 294 + max_stock: 1000 + price: 885 + sale_gamma: 49 + service_level: 0.95 + SKU797: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 100 + init_stock: 234 + max_stock: 1000 + price: 119 + sale_gamma: 39 + service_level: 0.95 + SKU798: + constraint: null + cost: 426 + init_stock: 180 + max_stock: 1000 + price: 630 + sale_gamma: 30 + service_level: 0.95 + SKU799: + constraint: null + cost: 63 + init_stock: 21 + max_stock: 1000 + price: 78 + sale_gamma: 7 + service_level: 0.95 + SKU8: + constraint: G(stock_constraint) + cost: 125 + init_stock: 252 + max_stock: 1000 + price: 196 + sale_gamma: 63 + service_level: 0.95 + SKU80: + constraint: G(stock_constraint) + cost: 163 + init_stock: 420 + max_stock: 1000 + price: 270 + sale_gamma: 70 + service_level: 0.95 + SKU800: + constraint: G(low_profit -> low_stock_constraint) + cost: 298 + init_stock: 470 + max_stock: 1000 + price: 455 + sale_gamma: 94 + service_level: 0.95 + SKU801: + constraint: G(low_profit -> low_stock_constraint) + cost: 389 + init_stock: 216 + max_stock: 1000 + price: 672 + sale_gamma: 36 + service_level: 0.95 + SKU802: + constraint: G(stock_constraint) + cost: 173 + init_stock: 310 + max_stock: 1000 + price: 281 + sale_gamma: 62 + service_level: 0.95 + SKU803: + constraint: null + cost: 272 + init_stock: 95 + max_stock: 1000 + price: 544 + sale_gamma: 19 + service_level: 0.95 + SKU804: + constraint: null + cost: 390 + init_stock: 188 + max_stock: 1000 + price: 491 + sale_gamma: 47 + service_level: 0.95 + SKU805: + constraint: null + cost: 61 + init_stock: 132 + max_stock: 1000 + price: 118 + sale_gamma: 33 + service_level: 0.95 + SKU806: + constraint: null + cost: 158 + init_stock: 156 + max_stock: 1000 + price: 186 + sale_gamma: 52 + service_level: 0.95 + SKU807: + constraint: null + cost: 453 + init_stock: 182 + max_stock: 1000 + price: 656 + sale_gamma: 26 + service_level: 0.95 + SKU808: + constraint: G(stock_constraint) + cost: 249 + init_stock: 182 + max_stock: 1000 + price: 435 + sale_gamma: 91 + service_level: 0.95 + SKU809: + constraint: null + cost: 55 + init_stock: 390 + max_stock: 1000 + price: 69 + sale_gamma: 65 + service_level: 0.95 + SKU81: + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 528 + max_stock: 1000 + price: 223 + sale_gamma: 66 + service_level: 0.95 + SKU810: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 276 + init_stock: 42 + max_stock: 1000 + price: 322 + sale_gamma: 6 + service_level: 0.95 + SKU811: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 254 + init_stock: 148 + max_stock: 1000 + price: 322 + sale_gamma: 37 + service_level: 0.95 + SKU812: + constraint: null + cost: 252 + init_stock: 320 + max_stock: 1000 + price: 337 + sale_gamma: 80 + service_level: 0.95 + SKU813: + constraint: G(stock_constraint) + cost: 253 + init_stock: 49 + max_stock: 1000 + price: 333 + sale_gamma: 7 + service_level: 0.95 + SKU814: + constraint: null + cost: 485 + init_stock: 194 + max_stock: 1000 + price: 921 + sale_gamma: 97 + service_level: 0.95 + SKU815: + constraint: null + cost: 390 + init_stock: 168 + max_stock: 1000 + price: 429 + sale_gamma: 24 + service_level: 0.95 + SKU816: + constraint: null + cost: 152 + init_stock: 455 + max_stock: 1000 + price: 174 + sale_gamma: 91 + service_level: 0.95 + SKU817: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 227 + init_stock: 25 + max_stock: 1000 + price: 401 + sale_gamma: 5 + service_level: 0.95 + SKU818: + constraint: null + cost: 354 + init_stock: 215 + max_stock: 1000 + price: 534 + sale_gamma: 43 + service_level: 0.95 + SKU819: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 302 + init_stock: 135 + max_stock: 1000 + price: 480 + sale_gamma: 27 + service_level: 0.95 + SKU82: + constraint: null + cost: 290 + init_stock: 140 + max_stock: 1000 + price: 469 + sale_gamma: 28 + service_level: 0.95 + SKU820: + constraint: null + cost: 264 + init_stock: 410 + max_stock: 1000 + price: 464 + sale_gamma: 82 + service_level: 0.95 + SKU821: + constraint: G(stock_constraint) + cost: 99 + init_stock: 207 + max_stock: 1000 + price: 151 + sale_gamma: 69 + service_level: 0.95 + SKU822: + constraint: null + cost: 136 + init_stock: 490 + max_stock: 1000 + price: 266 + sale_gamma: 70 + service_level: 0.95 + SKU823: + constraint: null + cost: 75 + init_stock: 84 + max_stock: 1000 + price: 116 + sale_gamma: 28 + service_level: 0.95 + SKU824: + constraint: G(low_profit -> low_stock_constraint) + cost: 170 + init_stock: 420 + max_stock: 1000 + price: 200 + sale_gamma: 70 + service_level: 0.95 + SKU825: + constraint: null + cost: 214 + init_stock: 45 + max_stock: 1000 + price: 359 + sale_gamma: 15 + service_level: 0.95 + SKU826: + constraint: G(stock_constraint) + cost: 386 + init_stock: 330 + max_stock: 1000 + price: 428 + sale_gamma: 55 + service_level: 0.95 + SKU827: + constraint: G(stock_constraint) + cost: 148 + init_stock: 448 + max_stock: 1000 + price: 208 + sale_gamma: 64 + service_level: 0.95 + SKU828: + constraint: null + cost: 400 + init_stock: 276 + max_stock: 1000 + price: 627 + sale_gamma: 69 + service_level: 0.95 + SKU829: + constraint: null + cost: 61 + init_stock: 370 + max_stock: 1000 + price: 83 + sale_gamma: 74 + service_level: 0.95 + SKU83: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 68 + max_stock: 1000 + price: 535 + sale_gamma: 17 + service_level: 0.95 + SKU830: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 167 + init_stock: 144 + max_stock: 1000 + price: 252 + sale_gamma: 24 + service_level: 0.95 + SKU831: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 116 + max_stock: 1000 + price: 353 + sale_gamma: 29 + service_level: 0.95 + SKU832: + constraint: null + cost: 33 + init_stock: 164 + max_stock: 1000 + price: 48 + sale_gamma: 82 + service_level: 0.95 + SKU833: + constraint: G(low_profit -> low_stock_constraint) + cost: 400 + init_stock: 392 + max_stock: 1000 + price: 756 + sale_gamma: 98 + service_level: 0.95 + SKU834: + constraint: G(low_profit -> low_stock_constraint) + cost: 422 + init_stock: 10 + max_stock: 1000 + price: 662 + sale_gamma: 5 + service_level: 0.95 + SKU835: + constraint: null + cost: 440 + init_stock: 156 + max_stock: 1000 + price: 532 + sale_gamma: 52 + service_level: 0.95 + SKU836: + constraint: G(stock_constraint) + cost: 323 + init_stock: 192 + max_stock: 1000 + price: 552 + sale_gamma: 96 + service_level: 0.95 + SKU837: + constraint: null + cost: 373 + init_stock: 156 + max_stock: 1000 + price: 607 + sale_gamma: 26 + service_level: 0.95 + SKU838: + constraint: null + cost: 456 + init_stock: 308 + max_stock: 1000 + price: 711 + sale_gamma: 77 + service_level: 0.95 + SKU839: + constraint: null + cost: 473 + init_stock: 360 + max_stock: 1000 + price: 695 + sale_gamma: 60 + service_level: 0.95 + SKU84: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 318 + init_stock: 294 + max_stock: 1000 + price: 594 + sale_gamma: 42 + service_level: 0.95 + SKU840: + constraint: G(low_profit -> low_stock_constraint) + cost: 266 + init_stock: 150 + max_stock: 1000 + price: 436 + sale_gamma: 50 + service_level: 0.95 + SKU841: + constraint: G(stock_constraint) + cost: 285 + init_stock: 595 + max_stock: 1000 + price: 538 + sale_gamma: 85 + service_level: 0.95 + SKU842: + constraint: G(low_profit -> low_stock_constraint) + cost: 41 + init_stock: 252 + max_stock: 1000 + price: 70 + sale_gamma: 84 + service_level: 0.95 + SKU843: + constraint: null + cost: 360 + init_stock: 432 + max_stock: 1000 + price: 694 + sale_gamma: 72 + service_level: 0.95 + SKU844: + constraint: G(low_profit -> low_stock_constraint) + cost: 51 + init_stock: 474 + max_stock: 1000 + price: 63 + sale_gamma: 79 + service_level: 0.95 + SKU845: + constraint: G(low_profit -> low_stock_constraint) + cost: 288 + init_stock: 176 + max_stock: 1000 + price: 558 + sale_gamma: 22 + service_level: 0.95 + SKU846: + constraint: null + cost: 485 + init_stock: 234 + max_stock: 1000 + price: 940 + sale_gamma: 39 + service_level: 0.95 + SKU847: + constraint: G(low_profit -> low_stock_constraint) + cost: 388 + init_stock: 546 + max_stock: 1000 + price: 519 + sale_gamma: 91 + service_level: 0.95 + SKU848: + constraint: null + cost: 306 + init_stock: 184 + max_stock: 1000 + price: 358 + sale_gamma: 46 + service_level: 0.95 + SKU849: + constraint: G(low_profit -> low_stock_constraint) + cost: 320 + init_stock: 160 + max_stock: 1000 + price: 425 + sale_gamma: 40 + service_level: 0.95 + SKU85: + constraint: G(low_profit -> low_stock_constraint) + cost: 442 + init_stock: 400 + max_stock: 1000 + price: 583 + sale_gamma: 100 + service_level: 0.95 + SKU850: + constraint: G(stock_constraint) + cost: 183 + init_stock: 342 + max_stock: 1000 + price: 206 + sale_gamma: 57 + service_level: 0.95 + SKU851: + constraint: null + cost: 53 + init_stock: 384 + max_stock: 1000 + price: 96 + sale_gamma: 64 + service_level: 0.95 + SKU852: + constraint: null + cost: 306 + init_stock: 162 + max_stock: 1000 + price: 483 + sale_gamma: 54 + service_level: 0.95 + SKU853: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 386 + init_stock: 280 + max_stock: 1000 + price: 443 + sale_gamma: 56 + service_level: 0.95 + SKU854: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 212 + init_stock: 390 + max_stock: 1000 + price: 290 + sale_gamma: 65 + service_level: 0.95 + SKU855: + constraint: null + cost: 76 + init_stock: 432 + max_stock: 1000 + price: 114 + sale_gamma: 72 + service_level: 0.95 + SKU856: + constraint: G(low_profit -> low_stock_constraint) + cost: 380 + init_stock: 224 + max_stock: 1000 + price: 627 + sale_gamma: 32 + service_level: 0.95 + SKU857: + constraint: G(stock_constraint) + cost: 462 + init_stock: 500 + max_stock: 1000 + price: 646 + sale_gamma: 100 + service_level: 0.95 + SKU858: + constraint: null + cost: 80 + init_stock: 120 + max_stock: 1000 + price: 158 + sale_gamma: 24 + service_level: 0.95 + SKU859: + constraint: G(low_profit -> low_stock_constraint) + cost: 215 + init_stock: 224 + max_stock: 1000 + price: 298 + sale_gamma: 28 + service_level: 0.95 + SKU86: + constraint: null + cost: 386 + init_stock: 595 + max_stock: 1000 + price: 447 + sale_gamma: 85 + service_level: 0.95 + SKU860: + constraint: G(low_profit -> low_stock_constraint) + cost: 313 + init_stock: 105 + max_stock: 1000 + price: 435 + sale_gamma: 15 + service_level: 0.95 + SKU861: + constraint: null + cost: 477 + init_stock: 52 + max_stock: 1000 + price: 944 + sale_gamma: 13 + service_level: 0.95 + SKU862: + constraint: null + cost: 240 + init_stock: 64 + max_stock: 1000 + price: 412 + sale_gamma: 16 + service_level: 0.95 + SKU863: + constraint: null + cost: 470 + init_stock: 372 + max_stock: 1000 + price: 911 + sale_gamma: 93 + service_level: 0.95 + SKU864: + constraint: null + cost: 203 + init_stock: 114 + max_stock: 1000 + price: 341 + sale_gamma: 19 + service_level: 0.95 + SKU865: + constraint: G(low_profit -> low_stock_constraint) + cost: 144 + init_stock: 152 + max_stock: 1000 + price: 253 + sale_gamma: 19 + service_level: 0.95 + SKU866: + constraint: null + cost: 172 + init_stock: 264 + max_stock: 1000 + price: 201 + sale_gamma: 88 + service_level: 0.95 + SKU867: + constraint: G(low_profit -> low_stock_constraint) + cost: 499 + init_stock: 500 + max_stock: 1000 + price: 698 + sale_gamma: 100 + service_level: 0.95 + SKU868: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 41 + init_stock: 150 + max_stock: 1000 + price: 56 + sale_gamma: 50 + service_level: 0.95 + SKU869: + constraint: null + cost: 97 + init_stock: 388 + max_stock: 1000 + price: 111 + sale_gamma: 97 + service_level: 0.95 + SKU87: + constraint: null + cost: 368 + init_stock: 207 + max_stock: 1000 + price: 563 + sale_gamma: 69 + service_level: 0.95 + SKU870: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 281 + init_stock: 335 + max_stock: 1000 + price: 446 + sale_gamma: 67 + service_level: 0.95 + SKU871: + constraint: null + cost: 101 + init_stock: 396 + max_stock: 1000 + price: 112 + sale_gamma: 99 + service_level: 0.95 + SKU872: + constraint: null + cost: 133 + init_stock: 160 + max_stock: 1000 + price: 195 + sale_gamma: 32 + service_level: 0.95 + SKU873: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 423 + init_stock: 155 + max_stock: 1000 + price: 829 + sale_gamma: 31 + service_level: 0.95 + SKU874: + constraint: G(low_profit -> low_stock_constraint) + cost: 469 + init_stock: 360 + max_stock: 1000 + price: 689 + sale_gamma: 60 + service_level: 0.95 + SKU875: + constraint: G(stock_constraint) + cost: 51 + init_stock: 138 + max_stock: 1000 + price: 57 + sale_gamma: 23 + service_level: 0.95 + SKU876: + constraint: null + cost: 303 + init_stock: 427 + max_stock: 1000 + price: 427 + sale_gamma: 61 + service_level: 0.95 + SKU877: + constraint: null + cost: 40 + init_stock: 245 + max_stock: 1000 + price: 70 + sale_gamma: 35 + service_level: 0.95 + SKU878: + constraint: G(low_profit -> low_stock_constraint) + cost: 27 + init_stock: 476 + max_stock: 1000 + price: 32 + sale_gamma: 68 + service_level: 0.95 + SKU879: + constraint: null + cost: 150 + init_stock: 300 + max_stock: 1000 + price: 198 + sale_gamma: 100 + service_level: 0.95 + SKU88: + constraint: G(low_profit -> low_stock_constraint) + cost: 471 + init_stock: 36 + max_stock: 1000 + price: 824 + sale_gamma: 12 + service_level: 0.95 + SKU880: + constraint: null + cost: 433 + init_stock: 155 + max_stock: 1000 + price: 532 + sale_gamma: 31 + service_level: 0.95 + SKU881: + constraint: null + cost: 431 + init_stock: 420 + max_stock: 1000 + price: 560 + sale_gamma: 70 + service_level: 0.95 + SKU882: + constraint: G(stock_constraint) + cost: 194 + init_stock: 80 + max_stock: 1000 + price: 314 + sale_gamma: 16 + service_level: 0.95 + SKU883: + constraint: null + cost: 284 + init_stock: 152 + max_stock: 1000 + price: 479 + sale_gamma: 38 + service_level: 0.95 + SKU884: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 139 + init_stock: 195 + max_stock: 1000 + price: 222 + sale_gamma: 39 + service_level: 0.95 + SKU885: + constraint: G(low_profit -> low_stock_constraint) + cost: 49 + init_stock: 189 + max_stock: 1000 + price: 58 + sale_gamma: 27 + service_level: 0.95 + SKU886: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 584 + max_stock: 1000 + price: 602 + sale_gamma: 73 + service_level: 0.95 + SKU887: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 266 + init_stock: 174 + max_stock: 1000 + price: 494 + sale_gamma: 87 + service_level: 0.95 + SKU888: + constraint: G(low_profit -> low_stock_constraint) + cost: 143 + init_stock: 45 + max_stock: 1000 + price: 220 + sale_gamma: 9 + service_level: 0.95 + SKU889: + constraint: null + cost: 101 + init_stock: 147 + max_stock: 1000 + price: 199 + sale_gamma: 21 + service_level: 0.95 + SKU89: + constraint: null + cost: 138 + init_stock: 744 + max_stock: 1000 + price: 269 + sale_gamma: 93 + service_level: 0.95 + SKU890: + constraint: G(low_profit -> low_stock_constraint) + cost: 161 + init_stock: 600 + max_stock: 1000 + price: 247 + sale_gamma: 100 + service_level: 0.95 + SKU891: + constraint: null + cost: 356 + init_stock: 176 + max_stock: 1000 + price: 615 + sale_gamma: 22 + service_level: 0.95 + SKU892: + constraint: null + cost: 313 + init_stock: 552 + max_stock: 1000 + price: 516 + sale_gamma: 92 + service_level: 0.95 + SKU893: + constraint: null + cost: 229 + init_stock: 594 + max_stock: 1000 + price: 302 + sale_gamma: 99 + service_level: 0.95 + SKU894: + constraint: null + cost: 129 + init_stock: 222 + max_stock: 1000 + price: 176 + sale_gamma: 74 + service_level: 0.95 + SKU895: + constraint: null + cost: 230 + init_stock: 39 + max_stock: 1000 + price: 301 + sale_gamma: 13 + service_level: 0.95 + SKU896: + constraint: null + cost: 289 + init_stock: 400 + max_stock: 1000 + price: 465 + sale_gamma: 80 + service_level: 0.95 + SKU897: + constraint: null + cost: 393 + init_stock: 432 + max_stock: 1000 + price: 640 + sale_gamma: 72 + service_level: 0.95 + SKU898: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 477 + init_stock: 44 + max_stock: 1000 + price: 615 + sale_gamma: 11 + service_level: 0.95 + SKU899: + constraint: null + cost: 233 + init_stock: 240 + max_stock: 1000 + price: 309 + sale_gamma: 48 + service_level: 0.95 + SKU9: + constraint: null + cost: 166 + init_stock: 384 + max_stock: 1000 + price: 247 + sale_gamma: 96 + service_level: 0.95 + SKU90: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 454 + init_stock: 204 + max_stock: 1000 + price: 726 + sale_gamma: 51 + service_level: 0.95 + SKU900: + constraint: null + cost: 158 + init_stock: 165 + max_stock: 1000 + price: 263 + sale_gamma: 55 + service_level: 0.95 + SKU901: + constraint: G(stock_constraint) + cost: 215 + init_stock: 203 + max_stock: 1000 + price: 320 + sale_gamma: 29 + service_level: 0.95 + SKU902: + constraint: G(low_profit -> low_stock_constraint) + cost: 125 + init_stock: 560 + max_stock: 1000 + price: 205 + sale_gamma: 80 + service_level: 0.95 + SKU903: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 357 + init_stock: 266 + max_stock: 1000 + price: 517 + sale_gamma: 38 + service_level: 0.95 + SKU904: + constraint: G(low_profit -> low_stock_constraint) + cost: 496 + init_stock: 255 + max_stock: 1000 + price: 605 + sale_gamma: 51 + service_level: 0.95 + SKU905: + constraint: null + cost: 249 + init_stock: 217 + max_stock: 1000 + price: 383 + sale_gamma: 31 + service_level: 0.95 + SKU906: + constraint: G(low_profit -> low_stock_constraint) + cost: 166 + init_stock: 156 + max_stock: 1000 + price: 273 + sale_gamma: 52 + service_level: 0.95 + SKU907: + constraint: null + cost: 22 + init_stock: 498 + max_stock: 1000 + price: 33 + sale_gamma: 83 + service_level: 0.95 + SKU908: + constraint: null + cost: 408 + init_stock: 546 + max_stock: 1000 + price: 595 + sale_gamma: 78 + service_level: 0.95 + SKU909: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 482 + init_stock: 480 + max_stock: 1000 + price: 703 + sale_gamma: 96 + service_level: 0.95 + SKU91: + constraint: G(low_profit -> low_stock_constraint) + cost: 303 + init_stock: 48 + max_stock: 1000 + price: 463 + sale_gamma: 16 + service_level: 0.95 + SKU910: + constraint: null + cost: 226 + init_stock: 132 + max_stock: 1000 + price: 350 + sale_gamma: 33 + service_level: 0.95 + SKU911: + constraint: null + cost: 461 + init_stock: 210 + max_stock: 1000 + price: 686 + sale_gamma: 70 + service_level: 0.95 + SKU912: + constraint: null + cost: 236 + init_stock: 135 + max_stock: 1000 + price: 328 + sale_gamma: 27 + service_level: 0.95 + SKU913: + constraint: null + cost: 322 + init_stock: 184 + max_stock: 1000 + price: 518 + sale_gamma: 46 + service_level: 0.95 + SKU914: + constraint: null + cost: 272 + init_stock: 434 + max_stock: 1000 + price: 446 + sale_gamma: 62 + service_level: 0.95 + SKU915: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 337 + init_stock: 280 + max_stock: 1000 + price: 545 + sale_gamma: 56 + service_level: 0.95 + SKU916: + constraint: null + cost: 337 + init_stock: 534 + max_stock: 1000 + price: 647 + sale_gamma: 89 + service_level: 0.95 + SKU917: + constraint: G(low_profit -> low_stock_constraint) + cost: 258 + init_stock: 120 + max_stock: 1000 + price: 366 + sale_gamma: 30 + service_level: 0.95 + SKU918: + constraint: G(stock_constraint) + cost: 148 + init_stock: 330 + max_stock: 1000 + price: 224 + sale_gamma: 55 + service_level: 0.95 + SKU919: + constraint: G(stock_constraint) + cost: 393 + init_stock: 125 + max_stock: 1000 + price: 711 + sale_gamma: 25 + service_level: 0.95 + SKU92: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 252 + max_stock: 1000 + price: 466 + sale_gamma: 63 + service_level: 0.95 + SKU920: + constraint: null + cost: 357 + init_stock: 344 + max_stock: 1000 + price: 696 + sale_gamma: 86 + service_level: 0.95 + SKU921: + constraint: G(low_profit -> low_stock_constraint) + cost: 227 + init_stock: 198 + max_stock: 1000 + price: 419 + sale_gamma: 66 + service_level: 0.95 + SKU922: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 112 + init_stock: 201 + max_stock: 1000 + price: 123 + sale_gamma: 67 + service_level: 0.95 + SKU923: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 496 + init_stock: 348 + max_stock: 1000 + price: 828 + sale_gamma: 58 + service_level: 0.95 + SKU924: + constraint: null + cost: 316 + init_stock: 425 + max_stock: 1000 + price: 423 + sale_gamma: 85 + service_level: 0.95 + SKU925: + constraint: null + cost: 360 + init_stock: 90 + max_stock: 1000 + price: 716 + sale_gamma: 15 + service_level: 0.95 + SKU926: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 360 + init_stock: 469 + max_stock: 1000 + price: 475 + sale_gamma: 67 + service_level: 0.95 + SKU927: + constraint: G(stock_constraint) + cost: 260 + init_stock: 147 + max_stock: 1000 + price: 439 + sale_gamma: 21 + service_level: 0.95 + SKU928: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 491 + init_stock: 415 + max_stock: 1000 + price: 869 + sale_gamma: 83 + service_level: 0.95 + SKU929: + constraint: null + cost: 359 + init_stock: 800 + max_stock: 1000 + price: 470 + sale_gamma: 100 + service_level: 0.95 + SKU93: + constraint: null + cost: 404 + init_stock: 268 + max_stock: 1000 + price: 557 + sale_gamma: 67 + service_level: 0.95 + SKU930: + constraint: G(low_profit -> low_stock_constraint) + cost: 198 + init_stock: 196 + max_stock: 1000 + price: 247 + sale_gamma: 28 + service_level: 0.95 + SKU931: + constraint: null + cost: 71 + init_stock: 56 + max_stock: 1000 + price: 101 + sale_gamma: 14 + service_level: 0.95 + SKU932: + constraint: null + cost: 163 + init_stock: 264 + max_stock: 1000 + price: 283 + sale_gamma: 66 + service_level: 0.95 + SKU933: + constraint: null + cost: 113 + init_stock: 156 + max_stock: 1000 + price: 179 + sale_gamma: 78 + service_level: 0.95 + SKU934: + constraint: G(stock_constraint) + cost: 219 + init_stock: 469 + max_stock: 1000 + price: 346 + sale_gamma: 67 + service_level: 0.95 + SKU935: + constraint: null + cost: 364 + init_stock: 376 + max_stock: 1000 + price: 527 + sale_gamma: 94 + service_level: 0.95 + SKU936: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 24 + init_stock: 20 + max_stock: 1000 + price: 41 + sale_gamma: 5 + service_level: 0.95 + SKU937: + constraint: G(stock_constraint) + cost: 135 + init_stock: 85 + max_stock: 1000 + price: 216 + sale_gamma: 17 + service_level: 0.95 + SKU938: + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 63 + max_stock: 1000 + price: 704 + sale_gamma: 21 + service_level: 0.95 + SKU939: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 173 + init_stock: 413 + max_stock: 1000 + price: 219 + sale_gamma: 59 + service_level: 0.95 + SKU94: + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 235 + max_stock: 1000 + price: 261 + sale_gamma: 47 + service_level: 0.95 + SKU940: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 14 + init_stock: 279 + max_stock: 1000 + price: 24 + sale_gamma: 93 + service_level: 0.95 + SKU941: + constraint: G(stock_constraint) + cost: 80 + init_stock: 456 + max_stock: 1000 + price: 132 + sale_gamma: 57 + service_level: 0.95 + SKU942: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 202 + init_stock: 65 + max_stock: 1000 + price: 303 + sale_gamma: 13 + service_level: 0.95 + SKU943: + constraint: null + cost: 138 + init_stock: 245 + max_stock: 1000 + price: 182 + sale_gamma: 49 + service_level: 0.95 + SKU944: + constraint: null + cost: 196 + init_stock: 132 + max_stock: 1000 + price: 356 + sale_gamma: 44 + service_level: 0.95 + SKU945: + constraint: null + cost: 141 + init_stock: 85 + max_stock: 1000 + price: 176 + sale_gamma: 17 + service_level: 0.95 + SKU946: + constraint: null + cost: 325 + init_stock: 294 + max_stock: 1000 + price: 555 + sale_gamma: 49 + service_level: 0.95 + SKU947: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 338 + init_stock: 637 + max_stock: 1000 + price: 547 + sale_gamma: 91 + service_level: 0.95 + SKU948: + constraint: null + cost: 425 + init_stock: 112 + max_stock: 1000 + price: 476 + sale_gamma: 28 + service_level: 0.95 + SKU949: + constraint: G(stock_constraint) + cost: 309 + init_stock: 15 + max_stock: 1000 + price: 522 + sale_gamma: 5 + service_level: 0.95 + SKU95: + constraint: null + cost: 136 + init_stock: 84 + max_stock: 1000 + price: 214 + sale_gamma: 21 + service_level: 0.95 + SKU950: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 102 + init_stock: 324 + max_stock: 1000 + price: 128 + sale_gamma: 54 + service_level: 0.95 + SKU951: + constraint: G(low_profit -> low_stock_constraint) + cost: 75 + init_stock: 90 + max_stock: 1000 + price: 117 + sale_gamma: 18 + service_level: 0.95 + SKU952: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 156 + init_stock: 66 + max_stock: 1000 + price: 276 + sale_gamma: 11 + service_level: 0.95 + SKU953: + constraint: null + cost: 138 + init_stock: 245 + max_stock: 1000 + price: 230 + sale_gamma: 35 + service_level: 0.95 + SKU954: + constraint: null + cost: 296 + init_stock: 430 + max_stock: 1000 + price: 444 + sale_gamma: 86 + service_level: 0.95 + SKU955: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 55 + init_stock: 189 + max_stock: 1000 + price: 85 + sale_gamma: 63 + service_level: 0.95 + SKU956: + constraint: null + cost: 282 + init_stock: 425 + max_stock: 1000 + price: 397 + sale_gamma: 85 + service_level: 0.95 + SKU957: + constraint: null + cost: 305 + init_stock: 306 + max_stock: 1000 + price: 466 + sale_gamma: 51 + service_level: 0.95 + SKU958: + constraint: null + cost: 369 + init_stock: 462 + max_stock: 1000 + price: 704 + sale_gamma: 66 + service_level: 0.95 + SKU959: + constraint: null + cost: 81 + init_stock: 164 + max_stock: 1000 + price: 127 + sale_gamma: 82 + service_level: 0.95 + SKU96: + constraint: G(low_profit -> low_stock_constraint) + cost: 176 + init_stock: 264 + max_stock: 1000 + price: 329 + sale_gamma: 44 + service_level: 0.95 + SKU960: + constraint: null + cost: 147 + init_stock: 42 + max_stock: 1000 + price: 235 + sale_gamma: 6 + service_level: 0.95 + SKU961: + constraint: null + cost: 264 + init_stock: 176 + max_stock: 1000 + price: 290 + sale_gamma: 44 + service_level: 0.95 + SKU962: + constraint: null + cost: 354 + init_stock: 176 + max_stock: 1000 + price: 392 + sale_gamma: 44 + service_level: 0.95 + SKU963: + constraint: null + cost: 349 + init_stock: 63 + max_stock: 1000 + price: 523 + sale_gamma: 21 + service_level: 0.95 + SKU964: + constraint: null + cost: 244 + init_stock: 219 + max_stock: 1000 + price: 480 + sale_gamma: 73 + service_level: 0.95 + SKU965: + constraint: G(stock_constraint) + cost: 124 + init_stock: 255 + max_stock: 1000 + price: 199 + sale_gamma: 51 + service_level: 0.95 + SKU966: + constraint: G(stock_constraint) + cost: 302 + init_stock: 308 + max_stock: 1000 + price: 474 + sale_gamma: 44 + service_level: 0.95 + SKU967: + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 270 + max_stock: 1000 + price: 111 + sale_gamma: 45 + service_level: 0.95 + SKU968: + constraint: G(stock_constraint) + cost: 281 + init_stock: 294 + max_stock: 1000 + price: 528 + sale_gamma: 49 + service_level: 0.95 + SKU969: + constraint: G(low_profit -> low_stock_constraint) + cost: 249 + init_stock: 24 + max_stock: 1000 + price: 328 + sale_gamma: 6 + service_level: 0.95 + SKU97: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 28 + init_stock: 180 + max_stock: 1000 + price: 43 + sale_gamma: 30 + service_level: 0.95 + SKU970: + constraint: null + cost: 244 + init_stock: 30 + max_stock: 1000 + price: 373 + sale_gamma: 5 + service_level: 0.95 + SKU971: + constraint: null + cost: 368 + init_stock: 219 + max_stock: 1000 + price: 426 + sale_gamma: 73 + service_level: 0.95 + SKU972: + constraint: G(stock_constraint) + cost: 209 + init_stock: 345 + max_stock: 1000 + price: 307 + sale_gamma: 69 + service_level: 0.95 + SKU973: + constraint: null + cost: 271 + init_stock: 33 + max_stock: 1000 + price: 447 + sale_gamma: 11 + service_level: 0.95 + SKU974: + constraint: null + cost: 170 + init_stock: 192 + max_stock: 1000 + price: 285 + sale_gamma: 32 + service_level: 0.95 + SKU975: + constraint: G(stock_constraint) + cost: 198 + init_stock: 88 + max_stock: 1000 + price: 334 + sale_gamma: 22 + service_level: 0.95 + SKU976: + constraint: G(stock_constraint) + cost: 178 + init_stock: 75 + max_stock: 1000 + price: 323 + sale_gamma: 15 + service_level: 0.95 + SKU977: + constraint: G(stock_constraint) + cost: 234 + init_stock: 324 + max_stock: 1000 + price: 269 + sale_gamma: 54 + service_level: 0.95 + SKU978: + constraint: G(stock_constraint) + cost: 470 + init_stock: 320 + max_stock: 1000 + price: 921 + sale_gamma: 64 + service_level: 0.95 + SKU979: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 443 + init_stock: 480 + max_stock: 1000 + price: 753 + sale_gamma: 96 + service_level: 0.95 + SKU98: + constraint: G(stock_constraint) + cost: 443 + init_stock: 70 + max_stock: 1000 + price: 527 + sale_gamma: 35 + service_level: 0.95 + SKU980: + constraint: null + cost: 39 + init_stock: 340 + max_stock: 1000 + price: 60 + sale_gamma: 68 + service_level: 0.95 + SKU981: + constraint: null + cost: 482 + init_stock: 360 + max_stock: 1000 + price: 607 + sale_gamma: 72 + service_level: 0.95 + SKU982: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 213 + init_stock: 208 + max_stock: 1000 + price: 374 + sale_gamma: 52 + service_level: 0.95 + SKU983: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 276 + max_stock: 1000 + price: 776 + sale_gamma: 92 + service_level: 0.95 + SKU984: + constraint: G(stock_constraint) + cost: 232 + init_stock: 637 + max_stock: 1000 + price: 327 + sale_gamma: 91 + service_level: 0.95 + SKU985: + constraint: null + cost: 290 + init_stock: 153 + max_stock: 1000 + price: 420 + sale_gamma: 51 + service_level: 0.95 + SKU986: + constraint: null + cost: 275 + init_stock: 136 + max_stock: 1000 + price: 313 + sale_gamma: 17 + service_level: 0.95 + SKU987: + constraint: null + cost: 434 + init_stock: 204 + max_stock: 1000 + price: 516 + sale_gamma: 34 + service_level: 0.95 + SKU988: + constraint: null + cost: 102 + init_stock: 69 + max_stock: 1000 + price: 166 + sale_gamma: 23 + service_level: 0.95 + SKU989: + constraint: null + cost: 484 + init_stock: 558 + max_stock: 1000 + price: 653 + sale_gamma: 93 + service_level: 0.95 + SKU99: + constraint: null + cost: 361 + init_stock: 225 + max_stock: 1000 + price: 581 + sale_gamma: 45 + service_level: 0.95 + SKU990: + constraint: G(low_profit -> low_stock_constraint) + cost: 108 + init_stock: 228 + max_stock: 1000 + price: 174 + sale_gamma: 57 + service_level: 0.95 + SKU991: + constraint: null + cost: 409 + init_stock: 152 + max_stock: 1000 + price: 576 + sale_gamma: 19 + service_level: 0.95 + SKU992: + constraint: null + cost: 434 + init_stock: 651 + max_stock: 1000 + price: 685 + sale_gamma: 93 + service_level: 0.95 + SKU993: + constraint: G(low_profit -> low_stock_constraint) + cost: 145 + init_stock: 602 + max_stock: 1000 + price: 201 + sale_gamma: 86 + service_level: 0.95 + SKU994: + constraint: G(low_profit -> low_stock_constraint) + cost: 470 + init_stock: 300 + max_stock: 1000 + price: 916 + sale_gamma: 50 + service_level: 0.95 + SKU995: + constraint: null + cost: 241 + init_stock: 532 + max_stock: 1000 + price: 310 + sale_gamma: 76 + service_level: 0.95 + SKU996: + constraint: G(stock_constraint) + cost: 260 + init_stock: 438 + max_stock: 1000 + price: 416 + sale_gamma: 73 + service_level: 0.95 + SKU997: + constraint: G(stock_constraint) + cost: 400 + init_stock: 102 + max_stock: 1000 + price: 727 + sale_gamma: 34 + service_level: 0.95 + SKU998: + constraint: G(low_profit -> low_stock_constraint) + cost: 447 + init_stock: 165 + max_stock: 1000 + price: 688 + sale_gamma: 55 + service_level: 0.95 + SKU999: + constraint: null + cost: 79 + init_stock: 161 + max_stock: 1000 + price: 86 + sale_gamma: 23 + service_level: 0.95 + grid: + facilities: + STORE0: + - 9 + - 8 + SUPPLIER0: + - 14 + - 16 + WAREHOUSE0: + - 16 + - 18 + size: + - 20 + - 20 + skus: + - id: 0 + name: SKU0 + - id: 1 + name: SKU1 + - id: 2 + name: SKU2 + - id: 3 + name: SKU3 + - id: 4 + name: SKU4 + - id: 5 + name: SKU5 + - id: 6 + name: SKU6 + - id: 7 + name: SKU7 + - id: 8 + name: SKU8 + - id: 9 + name: SKU9 + - id: 10 + name: SKU10 + - id: 11 + name: SKU11 + - id: 12 + name: SKU12 + - id: 13 + name: SKU13 + - id: 14 + name: SKU14 + - id: 15 + name: SKU15 + - id: 16 + name: SKU16 + - id: 17 + name: SKU17 + - id: 18 + name: SKU18 + - id: 19 + name: SKU19 + - id: 20 + name: SKU20 + - id: 21 + name: SKU21 + - id: 22 + name: SKU22 + - id: 23 + name: SKU23 + - id: 24 + name: SKU24 + - id: 25 + name: SKU25 + - id: 26 + name: SKU26 + - id: 27 + name: SKU27 + - id: 28 + name: SKU28 + - id: 29 + name: SKU29 + - id: 30 + name: SKU30 + - id: 31 + name: SKU31 + - id: 32 + name: SKU32 + - id: 33 + name: SKU33 + - id: 34 + name: SKU34 + - id: 35 + name: SKU35 + - id: 36 + name: SKU36 + - id: 37 + name: SKU37 + - id: 38 + name: SKU38 + - id: 39 + name: SKU39 + - id: 40 + name: SKU40 + - id: 41 + name: SKU41 + - id: 42 + name: SKU42 + - id: 43 + name: SKU43 + - id: 44 + name: SKU44 + - id: 45 + name: SKU45 + - id: 46 + name: SKU46 + - id: 47 + name: SKU47 + - id: 48 + name: SKU48 + - id: 49 + name: SKU49 + - id: 50 + name: SKU50 + - id: 51 + name: SKU51 + - id: 52 + name: SKU52 + - id: 53 + name: SKU53 + - id: 54 + name: SKU54 + - id: 55 + name: SKU55 + - id: 56 + name: SKU56 + - id: 57 + name: SKU57 + - id: 58 + name: SKU58 + - id: 59 + name: SKU59 + - id: 60 + name: SKU60 + - id: 61 + name: SKU61 + - id: 62 + name: SKU62 + - id: 63 + name: SKU63 + - id: 64 + name: SKU64 + - id: 65 + name: SKU65 + - id: 66 + name: SKU66 + - id: 67 + name: SKU67 + - id: 68 + name: SKU68 + - id: 69 + name: SKU69 + - id: 70 + name: SKU70 + - id: 71 + name: SKU71 + - id: 72 + name: SKU72 + - id: 73 + name: SKU73 + - id: 74 + name: SKU74 + - id: 75 + name: SKU75 + - id: 76 + name: SKU76 + - id: 77 + name: SKU77 + - id: 78 + name: SKU78 + - id: 79 + name: SKU79 + - id: 80 + name: SKU80 + - id: 81 + name: SKU81 + - id: 82 + name: SKU82 + - id: 83 + name: SKU83 + - id: 84 + name: SKU84 + - id: 85 + name: SKU85 + - id: 86 + name: SKU86 + - id: 87 + name: SKU87 + - id: 88 + name: SKU88 + - id: 89 + name: SKU89 + - id: 90 + name: SKU90 + - id: 91 + name: SKU91 + - id: 92 + name: SKU92 + - id: 93 + name: SKU93 + - id: 94 + name: SKU94 + - id: 95 + name: SKU95 + - id: 96 + name: SKU96 + - id: 97 + name: SKU97 + - id: 98 + name: SKU98 + - id: 99 + name: SKU99 + - id: 100 + name: SKU100 + - id: 101 + name: SKU101 + - id: 102 + name: SKU102 + - id: 103 + name: SKU103 + - id: 104 + name: SKU104 + - id: 105 + name: SKU105 + - id: 106 + name: SKU106 + - id: 107 + name: SKU107 + - id: 108 + name: SKU108 + - id: 109 + name: SKU109 + - id: 110 + name: SKU110 + - id: 111 + name: SKU111 + - id: 112 + name: SKU112 + - id: 113 + name: SKU113 + - id: 114 + name: SKU114 + - id: 115 + name: SKU115 + - id: 116 + name: SKU116 + - id: 117 + name: SKU117 + - id: 118 + name: SKU118 + - id: 119 + name: SKU119 + - id: 120 + name: SKU120 + - id: 121 + name: SKU121 + - id: 122 + name: SKU122 + - id: 123 + name: SKU123 + - id: 124 + name: SKU124 + - id: 125 + name: SKU125 + - id: 126 + name: SKU126 + - id: 127 + name: SKU127 + - id: 128 + name: SKU128 + - id: 129 + name: SKU129 + - id: 130 + name: SKU130 + - id: 131 + name: SKU131 + - id: 132 + name: SKU132 + - id: 133 + name: SKU133 + - id: 134 + name: SKU134 + - id: 135 + name: SKU135 + - id: 136 + name: SKU136 + - id: 137 + name: SKU137 + - id: 138 + name: SKU138 + - id: 139 + name: SKU139 + - id: 140 + name: SKU140 + - id: 141 + name: SKU141 + - id: 142 + name: SKU142 + - id: 143 + name: SKU143 + - id: 144 + name: SKU144 + - id: 145 + name: SKU145 + - id: 146 + name: SKU146 + - id: 147 + name: SKU147 + - id: 148 + name: SKU148 + - id: 149 + name: SKU149 + - id: 150 + name: SKU150 + - id: 151 + name: SKU151 + - id: 152 + name: SKU152 + - id: 153 + name: SKU153 + - id: 154 + name: SKU154 + - id: 155 + name: SKU155 + - id: 156 + name: SKU156 + - id: 157 + name: SKU157 + - id: 158 + name: SKU158 + - id: 159 + name: SKU159 + - id: 160 + name: SKU160 + - id: 161 + name: SKU161 + - id: 162 + name: SKU162 + - id: 163 + name: SKU163 + - id: 164 + name: SKU164 + - id: 165 + name: SKU165 + - id: 166 + name: SKU166 + - id: 167 + name: SKU167 + - id: 168 + name: SKU168 + - id: 169 + name: SKU169 + - id: 170 + name: SKU170 + - id: 171 + name: SKU171 + - id: 172 + name: SKU172 + - id: 173 + name: SKU173 + - id: 174 + name: SKU174 + - id: 175 + name: SKU175 + - id: 176 + name: SKU176 + - id: 177 + name: SKU177 + - id: 178 + name: SKU178 + - id: 179 + name: SKU179 + - id: 180 + name: SKU180 + - id: 181 + name: SKU181 + - id: 182 + name: SKU182 + - id: 183 + name: SKU183 + - id: 184 + name: SKU184 + - id: 185 + name: SKU185 + - id: 186 + name: SKU186 + - id: 187 + name: SKU187 + - id: 188 + name: SKU188 + - id: 189 + name: SKU189 + - id: 190 + name: SKU190 + - id: 191 + name: SKU191 + - id: 192 + name: SKU192 + - id: 193 + name: SKU193 + - id: 194 + name: SKU194 + - id: 195 + name: SKU195 + - id: 196 + name: SKU196 + - id: 197 + name: SKU197 + - id: 198 + name: SKU198 + - id: 199 + name: SKU199 + - id: 200 + name: SKU200 + - id: 201 + name: SKU201 + - id: 202 + name: SKU202 + - id: 203 + name: SKU203 + - id: 204 + name: SKU204 + - id: 205 + name: SKU205 + - id: 206 + name: SKU206 + - id: 207 + name: SKU207 + - id: 208 + name: SKU208 + - id: 209 + name: SKU209 + - id: 210 + name: SKU210 + - id: 211 + name: SKU211 + - id: 212 + name: SKU212 + - id: 213 + name: SKU213 + - id: 214 + name: SKU214 + - id: 215 + name: SKU215 + - id: 216 + name: SKU216 + - id: 217 + name: SKU217 + - id: 218 + name: SKU218 + - id: 219 + name: SKU219 + - id: 220 + name: SKU220 + - id: 221 + name: SKU221 + - id: 222 + name: SKU222 + - id: 223 + name: SKU223 + - id: 224 + name: SKU224 + - id: 225 + name: SKU225 + - id: 226 + name: SKU226 + - id: 227 + name: SKU227 + - id: 228 + name: SKU228 + - id: 229 + name: SKU229 + - id: 230 + name: SKU230 + - id: 231 + name: SKU231 + - id: 232 + name: SKU232 + - id: 233 + name: SKU233 + - id: 234 + name: SKU234 + - id: 235 + name: SKU235 + - id: 236 + name: SKU236 + - id: 237 + name: SKU237 + - id: 238 + name: SKU238 + - id: 239 + name: SKU239 + - id: 240 + name: SKU240 + - id: 241 + name: SKU241 + - id: 242 + name: SKU242 + - id: 243 + name: SKU243 + - id: 244 + name: SKU244 + - id: 245 + name: SKU245 + - id: 246 + name: SKU246 + - id: 247 + name: SKU247 + - id: 248 + name: SKU248 + - id: 249 + name: SKU249 + - id: 250 + name: SKU250 + - id: 251 + name: SKU251 + - id: 252 + name: SKU252 + - id: 253 + name: SKU253 + - id: 254 + name: SKU254 + - id: 255 + name: SKU255 + - id: 256 + name: SKU256 + - id: 257 + name: SKU257 + - id: 258 + name: SKU258 + - id: 259 + name: SKU259 + - id: 260 + name: SKU260 + - id: 261 + name: SKU261 + - id: 262 + name: SKU262 + - id: 263 + name: SKU263 + - id: 264 + name: SKU264 + - id: 265 + name: SKU265 + - id: 266 + name: SKU266 + - id: 267 + name: SKU267 + - id: 268 + name: SKU268 + - id: 269 + name: SKU269 + - id: 270 + name: SKU270 + - id: 271 + name: SKU271 + - id: 272 + name: SKU272 + - id: 273 + name: SKU273 + - id: 274 + name: SKU274 + - id: 275 + name: SKU275 + - id: 276 + name: SKU276 + - id: 277 + name: SKU277 + - id: 278 + name: SKU278 + - id: 279 + name: SKU279 + - id: 280 + name: SKU280 + - id: 281 + name: SKU281 + - id: 282 + name: SKU282 + - id: 283 + name: SKU283 + - id: 284 + name: SKU284 + - id: 285 + name: SKU285 + - id: 286 + name: SKU286 + - id: 287 + name: SKU287 + - id: 288 + name: SKU288 + - id: 289 + name: SKU289 + - id: 290 + name: SKU290 + - id: 291 + name: SKU291 + - id: 292 + name: SKU292 + - id: 293 + name: SKU293 + - id: 294 + name: SKU294 + - id: 295 + name: SKU295 + - id: 296 + name: SKU296 + - id: 297 + name: SKU297 + - id: 298 + name: SKU298 + - id: 299 + name: SKU299 + - id: 300 + name: SKU300 + - id: 301 + name: SKU301 + - id: 302 + name: SKU302 + - id: 303 + name: SKU303 + - id: 304 + name: SKU304 + - id: 305 + name: SKU305 + - id: 306 + name: SKU306 + - id: 307 + name: SKU307 + - id: 308 + name: SKU308 + - id: 309 + name: SKU309 + - id: 310 + name: SKU310 + - id: 311 + name: SKU311 + - id: 312 + name: SKU312 + - id: 313 + name: SKU313 + - id: 314 + name: SKU314 + - id: 315 + name: SKU315 + - id: 316 + name: SKU316 + - id: 317 + name: SKU317 + - id: 318 + name: SKU318 + - id: 319 + name: SKU319 + - id: 320 + name: SKU320 + - id: 321 + name: SKU321 + - id: 322 + name: SKU322 + - id: 323 + name: SKU323 + - id: 324 + name: SKU324 + - id: 325 + name: SKU325 + - id: 326 + name: SKU326 + - id: 327 + name: SKU327 + - id: 328 + name: SKU328 + - id: 329 + name: SKU329 + - id: 330 + name: SKU330 + - id: 331 + name: SKU331 + - id: 332 + name: SKU332 + - id: 333 + name: SKU333 + - id: 334 + name: SKU334 + - id: 335 + name: SKU335 + - id: 336 + name: SKU336 + - id: 337 + name: SKU337 + - id: 338 + name: SKU338 + - id: 339 + name: SKU339 + - id: 340 + name: SKU340 + - id: 341 + name: SKU341 + - id: 342 + name: SKU342 + - id: 343 + name: SKU343 + - id: 344 + name: SKU344 + - id: 345 + name: SKU345 + - id: 346 + name: SKU346 + - id: 347 + name: SKU347 + - id: 348 + name: SKU348 + - id: 349 + name: SKU349 + - id: 350 + name: SKU350 + - id: 351 + name: SKU351 + - id: 352 + name: SKU352 + - id: 353 + name: SKU353 + - id: 354 + name: SKU354 + - id: 355 + name: SKU355 + - id: 356 + name: SKU356 + - id: 357 + name: SKU357 + - id: 358 + name: SKU358 + - id: 359 + name: SKU359 + - id: 360 + name: SKU360 + - id: 361 + name: SKU361 + - id: 362 + name: SKU362 + - id: 363 + name: SKU363 + - id: 364 + name: SKU364 + - id: 365 + name: SKU365 + - id: 366 + name: SKU366 + - id: 367 + name: SKU367 + - id: 368 + name: SKU368 + - id: 369 + name: SKU369 + - id: 370 + name: SKU370 + - id: 371 + name: SKU371 + - id: 372 + name: SKU372 + - id: 373 + name: SKU373 + - id: 374 + name: SKU374 + - id: 375 + name: SKU375 + - id: 376 + name: SKU376 + - id: 377 + name: SKU377 + - id: 378 + name: SKU378 + - id: 379 + name: SKU379 + - id: 380 + name: SKU380 + - id: 381 + name: SKU381 + - id: 382 + name: SKU382 + - id: 383 + name: SKU383 + - id: 384 + name: SKU384 + - id: 385 + name: SKU385 + - id: 386 + name: SKU386 + - id: 387 + name: SKU387 + - id: 388 + name: SKU388 + - id: 389 + name: SKU389 + - id: 390 + name: SKU390 + - id: 391 + name: SKU391 + - id: 392 + name: SKU392 + - id: 393 + name: SKU393 + - id: 394 + name: SKU394 + - id: 395 + name: SKU395 + - id: 396 + name: SKU396 + - id: 397 + name: SKU397 + - id: 398 + name: SKU398 + - id: 399 + name: SKU399 + - id: 400 + name: SKU400 + - id: 401 + name: SKU401 + - id: 402 + name: SKU402 + - id: 403 + name: SKU403 + - id: 404 + name: SKU404 + - id: 405 + name: SKU405 + - id: 406 + name: SKU406 + - id: 407 + name: SKU407 + - id: 408 + name: SKU408 + - id: 409 + name: SKU409 + - id: 410 + name: SKU410 + - id: 411 + name: SKU411 + - id: 412 + name: SKU412 + - id: 413 + name: SKU413 + - id: 414 + name: SKU414 + - id: 415 + name: SKU415 + - id: 416 + name: SKU416 + - id: 417 + name: SKU417 + - id: 418 + name: SKU418 + - id: 419 + name: SKU419 + - id: 420 + name: SKU420 + - id: 421 + name: SKU421 + - id: 422 + name: SKU422 + - id: 423 + name: SKU423 + - id: 424 + name: SKU424 + - id: 425 + name: SKU425 + - id: 426 + name: SKU426 + - id: 427 + name: SKU427 + - id: 428 + name: SKU428 + - id: 429 + name: SKU429 + - id: 430 + name: SKU430 + - id: 431 + name: SKU431 + - id: 432 + name: SKU432 + - id: 433 + name: SKU433 + - id: 434 + name: SKU434 + - id: 435 + name: SKU435 + - id: 436 + name: SKU436 + - id: 437 + name: SKU437 + - id: 438 + name: SKU438 + - id: 439 + name: SKU439 + - id: 440 + name: SKU440 + - id: 441 + name: SKU441 + - id: 442 + name: SKU442 + - id: 443 + name: SKU443 + - id: 444 + name: SKU444 + - id: 445 + name: SKU445 + - id: 446 + name: SKU446 + - id: 447 + name: SKU447 + - id: 448 + name: SKU448 + - id: 449 + name: SKU449 + - id: 450 + name: SKU450 + - id: 451 + name: SKU451 + - id: 452 + name: SKU452 + - id: 453 + name: SKU453 + - id: 454 + name: SKU454 + - id: 455 + name: SKU455 + - id: 456 + name: SKU456 + - id: 457 + name: SKU457 + - id: 458 + name: SKU458 + - id: 459 + name: SKU459 + - id: 460 + name: SKU460 + - id: 461 + name: SKU461 + - id: 462 + name: SKU462 + - id: 463 + name: SKU463 + - id: 464 + name: SKU464 + - id: 465 + name: SKU465 + - id: 466 + name: SKU466 + - id: 467 + name: SKU467 + - id: 468 + name: SKU468 + - id: 469 + name: SKU469 + - id: 470 + name: SKU470 + - id: 471 + name: SKU471 + - id: 472 + name: SKU472 + - id: 473 + name: SKU473 + - id: 474 + name: SKU474 + - id: 475 + name: SKU475 + - id: 476 + name: SKU476 + - id: 477 + name: SKU477 + - id: 478 + name: SKU478 + - id: 479 + name: SKU479 + - id: 480 + name: SKU480 + - id: 481 + name: SKU481 + - id: 482 + name: SKU482 + - id: 483 + name: SKU483 + - id: 484 + name: SKU484 + - id: 485 + name: SKU485 + - id: 486 + name: SKU486 + - id: 487 + name: SKU487 + - id: 488 + name: SKU488 + - id: 489 + name: SKU489 + - id: 490 + name: SKU490 + - id: 491 + name: SKU491 + - id: 492 + name: SKU492 + - id: 493 + name: SKU493 + - id: 494 + name: SKU494 + - id: 495 + name: SKU495 + - id: 496 + name: SKU496 + - id: 497 + name: SKU497 + - id: 498 + name: SKU498 + - id: 499 + name: SKU499 + - id: 500 + name: SKU500 + - id: 501 + name: SKU501 + - id: 502 + name: SKU502 + - id: 503 + name: SKU503 + - id: 504 + name: SKU504 + - id: 505 + name: SKU505 + - id: 506 + name: SKU506 + - id: 507 + name: SKU507 + - id: 508 + name: SKU508 + - id: 509 + name: SKU509 + - id: 510 + name: SKU510 + - id: 511 + name: SKU511 + - id: 512 + name: SKU512 + - id: 513 + name: SKU513 + - id: 514 + name: SKU514 + - id: 515 + name: SKU515 + - id: 516 + name: SKU516 + - id: 517 + name: SKU517 + - id: 518 + name: SKU518 + - id: 519 + name: SKU519 + - id: 520 + name: SKU520 + - id: 521 + name: SKU521 + - id: 522 + name: SKU522 + - id: 523 + name: SKU523 + - id: 524 + name: SKU524 + - id: 525 + name: SKU525 + - id: 526 + name: SKU526 + - id: 527 + name: SKU527 + - id: 528 + name: SKU528 + - id: 529 + name: SKU529 + - id: 530 + name: SKU530 + - id: 531 + name: SKU531 + - id: 532 + name: SKU532 + - id: 533 + name: SKU533 + - id: 534 + name: SKU534 + - id: 535 + name: SKU535 + - id: 536 + name: SKU536 + - id: 537 + name: SKU537 + - id: 538 + name: SKU538 + - id: 539 + name: SKU539 + - id: 540 + name: SKU540 + - id: 541 + name: SKU541 + - id: 542 + name: SKU542 + - id: 543 + name: SKU543 + - id: 544 + name: SKU544 + - id: 545 + name: SKU545 + - id: 546 + name: SKU546 + - id: 547 + name: SKU547 + - id: 548 + name: SKU548 + - id: 549 + name: SKU549 + - id: 550 + name: SKU550 + - id: 551 + name: SKU551 + - id: 552 + name: SKU552 + - id: 553 + name: SKU553 + - id: 554 + name: SKU554 + - id: 555 + name: SKU555 + - id: 556 + name: SKU556 + - id: 557 + name: SKU557 + - id: 558 + name: SKU558 + - id: 559 + name: SKU559 + - id: 560 + name: SKU560 + - id: 561 + name: SKU561 + - id: 562 + name: SKU562 + - id: 563 + name: SKU563 + - id: 564 + name: SKU564 + - id: 565 + name: SKU565 + - id: 566 + name: SKU566 + - id: 567 + name: SKU567 + - id: 568 + name: SKU568 + - id: 569 + name: SKU569 + - id: 570 + name: SKU570 + - id: 571 + name: SKU571 + - id: 572 + name: SKU572 + - id: 573 + name: SKU573 + - id: 574 + name: SKU574 + - id: 575 + name: SKU575 + - id: 576 + name: SKU576 + - id: 577 + name: SKU577 + - id: 578 + name: SKU578 + - id: 579 + name: SKU579 + - id: 580 + name: SKU580 + - id: 581 + name: SKU581 + - id: 582 + name: SKU582 + - id: 583 + name: SKU583 + - id: 584 + name: SKU584 + - id: 585 + name: SKU585 + - id: 586 + name: SKU586 + - id: 587 + name: SKU587 + - id: 588 + name: SKU588 + - id: 589 + name: SKU589 + - id: 590 + name: SKU590 + - id: 591 + name: SKU591 + - id: 592 + name: SKU592 + - id: 593 + name: SKU593 + - id: 594 + name: SKU594 + - id: 595 + name: SKU595 + - id: 596 + name: SKU596 + - id: 597 + name: SKU597 + - id: 598 + name: SKU598 + - id: 599 + name: SKU599 + - id: 600 + name: SKU600 + - id: 601 + name: SKU601 + - id: 602 + name: SKU602 + - id: 603 + name: SKU603 + - id: 604 + name: SKU604 + - id: 605 + name: SKU605 + - id: 606 + name: SKU606 + - id: 607 + name: SKU607 + - id: 608 + name: SKU608 + - id: 609 + name: SKU609 + - id: 610 + name: SKU610 + - id: 611 + name: SKU611 + - id: 612 + name: SKU612 + - id: 613 + name: SKU613 + - id: 614 + name: SKU614 + - id: 615 + name: SKU615 + - id: 616 + name: SKU616 + - id: 617 + name: SKU617 + - id: 618 + name: SKU618 + - id: 619 + name: SKU619 + - id: 620 + name: SKU620 + - id: 621 + name: SKU621 + - id: 622 + name: SKU622 + - id: 623 + name: SKU623 + - id: 624 + name: SKU624 + - id: 625 + name: SKU625 + - id: 626 + name: SKU626 + - id: 627 + name: SKU627 + - id: 628 + name: SKU628 + - id: 629 + name: SKU629 + - id: 630 + name: SKU630 + - id: 631 + name: SKU631 + - id: 632 + name: SKU632 + - id: 633 + name: SKU633 + - id: 634 + name: SKU634 + - id: 635 + name: SKU635 + - id: 636 + name: SKU636 + - id: 637 + name: SKU637 + - id: 638 + name: SKU638 + - id: 639 + name: SKU639 + - id: 640 + name: SKU640 + - id: 641 + name: SKU641 + - id: 642 + name: SKU642 + - id: 643 + name: SKU643 + - id: 644 + name: SKU644 + - id: 645 + name: SKU645 + - id: 646 + name: SKU646 + - id: 647 + name: SKU647 + - id: 648 + name: SKU648 + - id: 649 + name: SKU649 + - id: 650 + name: SKU650 + - id: 651 + name: SKU651 + - id: 652 + name: SKU652 + - id: 653 + name: SKU653 + - id: 654 + name: SKU654 + - id: 655 + name: SKU655 + - id: 656 + name: SKU656 + - id: 657 + name: SKU657 + - id: 658 + name: SKU658 + - id: 659 + name: SKU659 + - id: 660 + name: SKU660 + - id: 661 + name: SKU661 + - id: 662 + name: SKU662 + - id: 663 + name: SKU663 + - id: 664 + name: SKU664 + - id: 665 + name: SKU665 + - id: 666 + name: SKU666 + - id: 667 + name: SKU667 + - id: 668 + name: SKU668 + - id: 669 + name: SKU669 + - id: 670 + name: SKU670 + - id: 671 + name: SKU671 + - id: 672 + name: SKU672 + - id: 673 + name: SKU673 + - id: 674 + name: SKU674 + - id: 675 + name: SKU675 + - id: 676 + name: SKU676 + - id: 677 + name: SKU677 + - id: 678 + name: SKU678 + - id: 679 + name: SKU679 + - id: 680 + name: SKU680 + - id: 681 + name: SKU681 + - id: 682 + name: SKU682 + - id: 683 + name: SKU683 + - id: 684 + name: SKU684 + - id: 685 + name: SKU685 + - id: 686 + name: SKU686 + - id: 687 + name: SKU687 + - id: 688 + name: SKU688 + - id: 689 + name: SKU689 + - id: 690 + name: SKU690 + - id: 691 + name: SKU691 + - id: 692 + name: SKU692 + - id: 693 + name: SKU693 + - id: 694 + name: SKU694 + - id: 695 + name: SKU695 + - id: 696 + name: SKU696 + - id: 697 + name: SKU697 + - id: 698 + name: SKU698 + - id: 699 + name: SKU699 + - id: 700 + name: SKU700 + - id: 701 + name: SKU701 + - id: 702 + name: SKU702 + - id: 703 + name: SKU703 + - id: 704 + name: SKU704 + - id: 705 + name: SKU705 + - id: 706 + name: SKU706 + - id: 707 + name: SKU707 + - id: 708 + name: SKU708 + - id: 709 + name: SKU709 + - id: 710 + name: SKU710 + - id: 711 + name: SKU711 + - id: 712 + name: SKU712 + - id: 713 + name: SKU713 + - id: 714 + name: SKU714 + - id: 715 + name: SKU715 + - id: 716 + name: SKU716 + - id: 717 + name: SKU717 + - id: 718 + name: SKU718 + - id: 719 + name: SKU719 + - id: 720 + name: SKU720 + - id: 721 + name: SKU721 + - id: 722 + name: SKU722 + - id: 723 + name: SKU723 + - id: 724 + name: SKU724 + - id: 725 + name: SKU725 + - id: 726 + name: SKU726 + - id: 727 + name: SKU727 + - id: 728 + name: SKU728 + - id: 729 + name: SKU729 + - id: 730 + name: SKU730 + - id: 731 + name: SKU731 + - id: 732 + name: SKU732 + - id: 733 + name: SKU733 + - id: 734 + name: SKU734 + - id: 735 + name: SKU735 + - id: 736 + name: SKU736 + - id: 737 + name: SKU737 + - id: 738 + name: SKU738 + - id: 739 + name: SKU739 + - id: 740 + name: SKU740 + - id: 741 + name: SKU741 + - id: 742 + name: SKU742 + - id: 743 + name: SKU743 + - id: 744 + name: SKU744 + - id: 745 + name: SKU745 + - id: 746 + name: SKU746 + - id: 747 + name: SKU747 + - id: 748 + name: SKU748 + - id: 749 + name: SKU749 + - id: 750 + name: SKU750 + - id: 751 + name: SKU751 + - id: 752 + name: SKU752 + - id: 753 + name: SKU753 + - id: 754 + name: SKU754 + - id: 755 + name: SKU755 + - id: 756 + name: SKU756 + - id: 757 + name: SKU757 + - id: 758 + name: SKU758 + - id: 759 + name: SKU759 + - id: 760 + name: SKU760 + - id: 761 + name: SKU761 + - id: 762 + name: SKU762 + - id: 763 + name: SKU763 + - id: 764 + name: SKU764 + - id: 765 + name: SKU765 + - id: 766 + name: SKU766 + - id: 767 + name: SKU767 + - id: 768 + name: SKU768 + - id: 769 + name: SKU769 + - id: 770 + name: SKU770 + - id: 771 + name: SKU771 + - id: 772 + name: SKU772 + - id: 773 + name: SKU773 + - id: 774 + name: SKU774 + - id: 775 + name: SKU775 + - id: 776 + name: SKU776 + - id: 777 + name: SKU777 + - id: 778 + name: SKU778 + - id: 779 + name: SKU779 + - id: 780 + name: SKU780 + - id: 781 + name: SKU781 + - id: 782 + name: SKU782 + - id: 783 + name: SKU783 + - id: 784 + name: SKU784 + - id: 785 + name: SKU785 + - id: 786 + name: SKU786 + - id: 787 + name: SKU787 + - id: 788 + name: SKU788 + - id: 789 + name: SKU789 + - id: 790 + name: SKU790 + - id: 791 + name: SKU791 + - id: 792 + name: SKU792 + - id: 793 + name: SKU793 + - id: 794 + name: SKU794 + - id: 795 + name: SKU795 + - id: 796 + name: SKU796 + - id: 797 + name: SKU797 + - id: 798 + name: SKU798 + - id: 799 + name: SKU799 + - id: 800 + name: SKU800 + - id: 801 + name: SKU801 + - id: 802 + name: SKU802 + - id: 803 + name: SKU803 + - id: 804 + name: SKU804 + - id: 805 + name: SKU805 + - id: 806 + name: SKU806 + - id: 807 + name: SKU807 + - id: 808 + name: SKU808 + - id: 809 + name: SKU809 + - id: 810 + name: SKU810 + - id: 811 + name: SKU811 + - id: 812 + name: SKU812 + - id: 813 + name: SKU813 + - id: 814 + name: SKU814 + - id: 815 + name: SKU815 + - id: 816 + name: SKU816 + - id: 817 + name: SKU817 + - id: 818 + name: SKU818 + - id: 819 + name: SKU819 + - id: 820 + name: SKU820 + - id: 821 + name: SKU821 + - id: 822 + name: SKU822 + - id: 823 + name: SKU823 + - id: 824 + name: SKU824 + - id: 825 + name: SKU825 + - id: 826 + name: SKU826 + - id: 827 + name: SKU827 + - id: 828 + name: SKU828 + - id: 829 + name: SKU829 + - id: 830 + name: SKU830 + - id: 831 + name: SKU831 + - id: 832 + name: SKU832 + - id: 833 + name: SKU833 + - id: 834 + name: SKU834 + - id: 835 + name: SKU835 + - id: 836 + name: SKU836 + - id: 837 + name: SKU837 + - id: 838 + name: SKU838 + - id: 839 + name: SKU839 + - id: 840 + name: SKU840 + - id: 841 + name: SKU841 + - id: 842 + name: SKU842 + - id: 843 + name: SKU843 + - id: 844 + name: SKU844 + - id: 845 + name: SKU845 + - id: 846 + name: SKU846 + - id: 847 + name: SKU847 + - id: 848 + name: SKU848 + - id: 849 + name: SKU849 + - id: 850 + name: SKU850 + - id: 851 + name: SKU851 + - id: 852 + name: SKU852 + - id: 853 + name: SKU853 + - id: 854 + name: SKU854 + - id: 855 + name: SKU855 + - id: 856 + name: SKU856 + - id: 857 + name: SKU857 + - id: 858 + name: SKU858 + - id: 859 + name: SKU859 + - id: 860 + name: SKU860 + - id: 861 + name: SKU861 + - id: 862 + name: SKU862 + - id: 863 + name: SKU863 + - id: 864 + name: SKU864 + - id: 865 + name: SKU865 + - id: 866 + name: SKU866 + - id: 867 + name: SKU867 + - id: 868 + name: SKU868 + - id: 869 + name: SKU869 + - id: 870 + name: SKU870 + - id: 871 + name: SKU871 + - id: 872 + name: SKU872 + - id: 873 + name: SKU873 + - id: 874 + name: SKU874 + - id: 875 + name: SKU875 + - id: 876 + name: SKU876 + - id: 877 + name: SKU877 + - id: 878 + name: SKU878 + - id: 879 + name: SKU879 + - id: 880 + name: SKU880 + - id: 881 + name: SKU881 + - id: 882 + name: SKU882 + - id: 883 + name: SKU883 + - id: 884 + name: SKU884 + - id: 885 + name: SKU885 + - id: 886 + name: SKU886 + - id: 887 + name: SKU887 + - id: 888 + name: SKU888 + - id: 889 + name: SKU889 + - id: 890 + name: SKU890 + - id: 891 + name: SKU891 + - id: 892 + name: SKU892 + - id: 893 + name: SKU893 + - id: 894 + name: SKU894 + - id: 895 + name: SKU895 + - id: 896 + name: SKU896 + - id: 897 + name: SKU897 + - id: 898 + name: SKU898 + - id: 899 + name: SKU899 + - id: 900 + name: SKU900 + - id: 901 + name: SKU901 + - id: 902 + name: SKU902 + - id: 903 + name: SKU903 + - id: 904 + name: SKU904 + - id: 905 + name: SKU905 + - id: 906 + name: SKU906 + - id: 907 + name: SKU907 + - id: 908 + name: SKU908 + - id: 909 + name: SKU909 + - id: 910 + name: SKU910 + - id: 911 + name: SKU911 + - id: 912 + name: SKU912 + - id: 913 + name: SKU913 + - id: 914 + name: SKU914 + - id: 915 + name: SKU915 + - id: 916 + name: SKU916 + - id: 917 + name: SKU917 + - id: 918 + name: SKU918 + - id: 919 + name: SKU919 + - id: 920 + name: SKU920 + - id: 921 + name: SKU921 + - id: 922 + name: SKU922 + - id: 923 + name: SKU923 + - id: 924 + name: SKU924 + - id: 925 + name: SKU925 + - id: 926 + name: SKU926 + - id: 927 + name: SKU927 + - id: 928 + name: SKU928 + - id: 929 + name: SKU929 + - id: 930 + name: SKU930 + - id: 931 + name: SKU931 + - id: 932 + name: SKU932 + - id: 933 + name: SKU933 + - id: 934 + name: SKU934 + - id: 935 + name: SKU935 + - id: 936 + name: SKU936 + - id: 937 + name: SKU937 + - id: 938 + name: SKU938 + - id: 939 + name: SKU939 + - id: 940 + name: SKU940 + - id: 941 + name: SKU941 + - id: 942 + name: SKU942 + - id: 943 + name: SKU943 + - id: 944 + name: SKU944 + - id: 945 + name: SKU945 + - id: 946 + name: SKU946 + - id: 947 + name: SKU947 + - id: 948 + name: SKU948 + - id: 949 + name: SKU949 + - id: 950 + name: SKU950 + - id: 951 + name: SKU951 + - id: 952 + name: SKU952 + - id: 953 + name: SKU953 + - id: 954 + name: SKU954 + - id: 955 + name: SKU955 + - id: 956 + name: SKU956 + - id: 957 + name: SKU957 + - id: 958 + name: SKU958 + - id: 959 + name: SKU959 + - id: 960 + name: SKU960 + - id: 961 + name: SKU961 + - id: 962 + name: SKU962 + - id: 963 + name: SKU963 + - id: 964 + name: SKU964 + - id: 965 + name: SKU965 + - id: 966 + name: SKU966 + - id: 967 + name: SKU967 + - id: 968 + name: SKU968 + - id: 969 + name: SKU969 + - id: 970 + name: SKU970 + - id: 971 + name: SKU971 + - id: 972 + name: SKU972 + - id: 973 + name: SKU973 + - id: 974 + name: SKU974 + - id: 975 + name: SKU975 + - id: 976 + name: SKU976 + - id: 977 + name: SKU977 + - id: 978 + name: SKU978 + - id: 979 + name: SKU979 + - id: 980 + name: SKU980 + - id: 981 + name: SKU981 + - id: 982 + name: SKU982 + - id: 983 + name: SKU983 + - id: 984 + name: SKU984 + - id: 985 + name: SKU985 + - id: 986 + name: SKU986 + - id: 987 + name: SKU987 + - id: 988 + name: SKU988 + - id: 989 + name: SKU989 + - id: 990 + name: SKU990 + - id: 991 + name: SKU991 + - id: 992 + name: SKU992 + - id: 993 + name: SKU993 + - id: 994 + name: SKU994 + - id: 995 + name: SKU995 + - id: 996 + name: SKU996 + - id: 997 + name: SKU997 + - id: 998 + name: SKU998 + - id: 999 + name: SKU999 + topology: + STORE0: + SKU0: + - WAREHOUSE0 + SKU1: + - WAREHOUSE0 + SKU10: + - WAREHOUSE0 + SKU100: + - WAREHOUSE0 + SKU101: + - WAREHOUSE0 + SKU102: + - WAREHOUSE0 + SKU103: + - WAREHOUSE0 + SKU104: + - WAREHOUSE0 + SKU105: + - WAREHOUSE0 + SKU106: + - WAREHOUSE0 + SKU107: + - WAREHOUSE0 + SKU108: + - WAREHOUSE0 + SKU109: + - WAREHOUSE0 + SKU11: + - WAREHOUSE0 + SKU110: + - WAREHOUSE0 + SKU111: + - WAREHOUSE0 + SKU112: + - WAREHOUSE0 + SKU113: + - WAREHOUSE0 + SKU114: + - WAREHOUSE0 + SKU115: + - WAREHOUSE0 + SKU116: + - WAREHOUSE0 + SKU117: + - WAREHOUSE0 + SKU118: + - WAREHOUSE0 + SKU119: + - WAREHOUSE0 + SKU12: + - WAREHOUSE0 + SKU120: + - WAREHOUSE0 + SKU121: + - WAREHOUSE0 + SKU122: + - WAREHOUSE0 + SKU123: + - WAREHOUSE0 + SKU124: + - WAREHOUSE0 + SKU125: + - WAREHOUSE0 + SKU126: + - WAREHOUSE0 + SKU127: + - WAREHOUSE0 + SKU128: + - WAREHOUSE0 + SKU129: + - WAREHOUSE0 + SKU13: + - WAREHOUSE0 + SKU130: + - WAREHOUSE0 + SKU131: + - WAREHOUSE0 + SKU132: + - WAREHOUSE0 + SKU133: + - WAREHOUSE0 + SKU134: + - WAREHOUSE0 + SKU135: + - WAREHOUSE0 + SKU136: + - WAREHOUSE0 + SKU137: + - WAREHOUSE0 + SKU138: + - WAREHOUSE0 + SKU139: + - WAREHOUSE0 + SKU14: + - WAREHOUSE0 + SKU140: + - WAREHOUSE0 + SKU141: + - WAREHOUSE0 + SKU142: + - WAREHOUSE0 + SKU143: + - WAREHOUSE0 + SKU144: + - WAREHOUSE0 + SKU145: + - WAREHOUSE0 + SKU146: + - WAREHOUSE0 + SKU147: + - WAREHOUSE0 + SKU148: + - WAREHOUSE0 + SKU149: + - WAREHOUSE0 + SKU15: + - WAREHOUSE0 + SKU150: + - WAREHOUSE0 + SKU151: + - WAREHOUSE0 + SKU152: + - WAREHOUSE0 + SKU153: + - WAREHOUSE0 + SKU154: + - WAREHOUSE0 + SKU155: + - WAREHOUSE0 + SKU156: + - WAREHOUSE0 + SKU157: + - WAREHOUSE0 + SKU158: + - WAREHOUSE0 + SKU159: + - WAREHOUSE0 + SKU16: + - WAREHOUSE0 + SKU160: + - WAREHOUSE0 + SKU161: + - WAREHOUSE0 + SKU162: + - WAREHOUSE0 + SKU163: + - WAREHOUSE0 + SKU164: + - WAREHOUSE0 + SKU165: + - WAREHOUSE0 + SKU166: + - WAREHOUSE0 + SKU167: + - WAREHOUSE0 + SKU168: + - WAREHOUSE0 + SKU169: + - WAREHOUSE0 + SKU17: + - WAREHOUSE0 + SKU170: + - WAREHOUSE0 + SKU171: + - WAREHOUSE0 + SKU172: + - WAREHOUSE0 + SKU173: + - WAREHOUSE0 + SKU174: + - WAREHOUSE0 + SKU175: + - WAREHOUSE0 + SKU176: + - WAREHOUSE0 + SKU177: + - WAREHOUSE0 + SKU178: + - WAREHOUSE0 + SKU179: + - WAREHOUSE0 + SKU18: + - WAREHOUSE0 + SKU180: + - WAREHOUSE0 + SKU181: + - WAREHOUSE0 + SKU182: + - WAREHOUSE0 + SKU183: + - WAREHOUSE0 + SKU184: + - WAREHOUSE0 + SKU185: + - WAREHOUSE0 + SKU186: + - WAREHOUSE0 + SKU187: + - WAREHOUSE0 + SKU188: + - WAREHOUSE0 + SKU189: + - WAREHOUSE0 + SKU19: + - WAREHOUSE0 + SKU190: + - WAREHOUSE0 + SKU191: + - WAREHOUSE0 + SKU192: + - WAREHOUSE0 + SKU193: + - WAREHOUSE0 + SKU194: + - WAREHOUSE0 + SKU195: + - WAREHOUSE0 + SKU196: + - WAREHOUSE0 + SKU197: + - WAREHOUSE0 + SKU198: + - WAREHOUSE0 + SKU199: + - WAREHOUSE0 + SKU2: + - WAREHOUSE0 + SKU20: + - WAREHOUSE0 + SKU200: + - WAREHOUSE0 + SKU201: + - WAREHOUSE0 + SKU202: + - WAREHOUSE0 + SKU203: + - WAREHOUSE0 + SKU204: + - WAREHOUSE0 + SKU205: + - WAREHOUSE0 + SKU206: + - WAREHOUSE0 + SKU207: + - WAREHOUSE0 + SKU208: + - WAREHOUSE0 + SKU209: + - WAREHOUSE0 + SKU21: + - WAREHOUSE0 + SKU210: + - WAREHOUSE0 + SKU211: + - WAREHOUSE0 + SKU212: + - WAREHOUSE0 + SKU213: + - WAREHOUSE0 + SKU214: + - WAREHOUSE0 + SKU215: + - WAREHOUSE0 + SKU216: + - WAREHOUSE0 + SKU217: + - WAREHOUSE0 + SKU218: + - WAREHOUSE0 + SKU219: + - WAREHOUSE0 + SKU22: + - WAREHOUSE0 + SKU220: + - WAREHOUSE0 + SKU221: + - WAREHOUSE0 + SKU222: + - WAREHOUSE0 + SKU223: + - WAREHOUSE0 + SKU224: + - WAREHOUSE0 + SKU225: + - WAREHOUSE0 + SKU226: + - WAREHOUSE0 + SKU227: + - WAREHOUSE0 + SKU228: + - WAREHOUSE0 + SKU229: + - WAREHOUSE0 + SKU23: + - WAREHOUSE0 + SKU230: + - WAREHOUSE0 + SKU231: + - WAREHOUSE0 + SKU232: + - WAREHOUSE0 + SKU233: + - WAREHOUSE0 + SKU234: + - WAREHOUSE0 + SKU235: + - WAREHOUSE0 + SKU236: + - WAREHOUSE0 + SKU237: + - WAREHOUSE0 + SKU238: + - WAREHOUSE0 + SKU239: + - WAREHOUSE0 + SKU24: + - WAREHOUSE0 + SKU240: + - WAREHOUSE0 + SKU241: + - WAREHOUSE0 + SKU242: + - WAREHOUSE0 + SKU243: + - WAREHOUSE0 + SKU244: + - WAREHOUSE0 + SKU245: + - WAREHOUSE0 + SKU246: + - WAREHOUSE0 + SKU247: + - WAREHOUSE0 + SKU248: + - WAREHOUSE0 + SKU249: + - WAREHOUSE0 + SKU25: + - WAREHOUSE0 + SKU250: + - WAREHOUSE0 + SKU251: + - WAREHOUSE0 + SKU252: + - WAREHOUSE0 + SKU253: + - WAREHOUSE0 + SKU254: + - WAREHOUSE0 + SKU255: + - WAREHOUSE0 + SKU256: + - WAREHOUSE0 + SKU257: + - WAREHOUSE0 + SKU258: + - WAREHOUSE0 + SKU259: + - WAREHOUSE0 + SKU26: + - WAREHOUSE0 + SKU260: + - WAREHOUSE0 + SKU261: + - WAREHOUSE0 + SKU262: + - WAREHOUSE0 + SKU263: + - WAREHOUSE0 + SKU264: + - WAREHOUSE0 + SKU265: + - WAREHOUSE0 + SKU266: + - WAREHOUSE0 + SKU267: + - WAREHOUSE0 + SKU268: + - WAREHOUSE0 + SKU269: + - WAREHOUSE0 + SKU27: + - WAREHOUSE0 + SKU270: + - WAREHOUSE0 + SKU271: + - WAREHOUSE0 + SKU272: + - WAREHOUSE0 + SKU273: + - WAREHOUSE0 + SKU274: + - WAREHOUSE0 + SKU275: + - WAREHOUSE0 + SKU276: + - WAREHOUSE0 + SKU277: + - WAREHOUSE0 + SKU278: + - WAREHOUSE0 + SKU279: + - WAREHOUSE0 + SKU28: + - WAREHOUSE0 + SKU280: + - WAREHOUSE0 + SKU281: + - WAREHOUSE0 + SKU282: + - WAREHOUSE0 + SKU283: + - WAREHOUSE0 + SKU284: + - WAREHOUSE0 + SKU285: + - WAREHOUSE0 + SKU286: + - WAREHOUSE0 + SKU287: + - WAREHOUSE0 + SKU288: + - WAREHOUSE0 + SKU289: + - WAREHOUSE0 + SKU29: + - WAREHOUSE0 + SKU290: + - WAREHOUSE0 + SKU291: + - WAREHOUSE0 + SKU292: + - WAREHOUSE0 + SKU293: + - WAREHOUSE0 + SKU294: + - WAREHOUSE0 + SKU295: + - WAREHOUSE0 + SKU296: + - WAREHOUSE0 + SKU297: + - WAREHOUSE0 + SKU298: + - WAREHOUSE0 + SKU299: + - WAREHOUSE0 + SKU3: + - WAREHOUSE0 + SKU30: + - WAREHOUSE0 + SKU300: + - WAREHOUSE0 + SKU301: + - WAREHOUSE0 + SKU302: + - WAREHOUSE0 + SKU303: + - WAREHOUSE0 + SKU304: + - WAREHOUSE0 + SKU305: + - WAREHOUSE0 + SKU306: + - WAREHOUSE0 + SKU307: + - WAREHOUSE0 + SKU308: + - WAREHOUSE0 + SKU309: + - WAREHOUSE0 + SKU31: + - WAREHOUSE0 + SKU310: + - WAREHOUSE0 + SKU311: + - WAREHOUSE0 + SKU312: + - WAREHOUSE0 + SKU313: + - WAREHOUSE0 + SKU314: + - WAREHOUSE0 + SKU315: + - WAREHOUSE0 + SKU316: + - WAREHOUSE0 + SKU317: + - WAREHOUSE0 + SKU318: + - WAREHOUSE0 + SKU319: + - WAREHOUSE0 + SKU32: + - WAREHOUSE0 + SKU320: + - WAREHOUSE0 + SKU321: + - WAREHOUSE0 + SKU322: + - WAREHOUSE0 + SKU323: + - WAREHOUSE0 + SKU324: + - WAREHOUSE0 + SKU325: + - WAREHOUSE0 + SKU326: + - WAREHOUSE0 + SKU327: + - WAREHOUSE0 + SKU328: + - WAREHOUSE0 + SKU329: + - WAREHOUSE0 + SKU33: + - WAREHOUSE0 + SKU330: + - WAREHOUSE0 + SKU331: + - WAREHOUSE0 + SKU332: + - WAREHOUSE0 + SKU333: + - WAREHOUSE0 + SKU334: + - WAREHOUSE0 + SKU335: + - WAREHOUSE0 + SKU336: + - WAREHOUSE0 + SKU337: + - WAREHOUSE0 + SKU338: + - WAREHOUSE0 + SKU339: + - WAREHOUSE0 + SKU34: + - WAREHOUSE0 + SKU340: + - WAREHOUSE0 + SKU341: + - WAREHOUSE0 + SKU342: + - WAREHOUSE0 + SKU343: + - WAREHOUSE0 + SKU344: + - WAREHOUSE0 + SKU345: + - WAREHOUSE0 + SKU346: + - WAREHOUSE0 + SKU347: + - WAREHOUSE0 + SKU348: + - WAREHOUSE0 + SKU349: + - WAREHOUSE0 + SKU35: + - WAREHOUSE0 + SKU350: + - WAREHOUSE0 + SKU351: + - WAREHOUSE0 + SKU352: + - WAREHOUSE0 + SKU353: + - WAREHOUSE0 + SKU354: + - WAREHOUSE0 + SKU355: + - WAREHOUSE0 + SKU356: + - WAREHOUSE0 + SKU357: + - WAREHOUSE0 + SKU358: + - WAREHOUSE0 + SKU359: + - WAREHOUSE0 + SKU36: + - WAREHOUSE0 + SKU360: + - WAREHOUSE0 + SKU361: + - WAREHOUSE0 + SKU362: + - WAREHOUSE0 + SKU363: + - WAREHOUSE0 + SKU364: + - WAREHOUSE0 + SKU365: + - WAREHOUSE0 + SKU366: + - WAREHOUSE0 + SKU367: + - WAREHOUSE0 + SKU368: + - WAREHOUSE0 + SKU369: + - WAREHOUSE0 + SKU37: + - WAREHOUSE0 + SKU370: + - WAREHOUSE0 + SKU371: + - WAREHOUSE0 + SKU372: + - WAREHOUSE0 + SKU373: + - WAREHOUSE0 + SKU374: + - WAREHOUSE0 + SKU375: + - WAREHOUSE0 + SKU376: + - WAREHOUSE0 + SKU377: + - WAREHOUSE0 + SKU378: + - WAREHOUSE0 + SKU379: + - WAREHOUSE0 + SKU38: + - WAREHOUSE0 + SKU380: + - WAREHOUSE0 + SKU381: + - WAREHOUSE0 + SKU382: + - WAREHOUSE0 + SKU383: + - WAREHOUSE0 + SKU384: + - WAREHOUSE0 + SKU385: + - WAREHOUSE0 + SKU386: + - WAREHOUSE0 + SKU387: + - WAREHOUSE0 + SKU388: + - WAREHOUSE0 + SKU389: + - WAREHOUSE0 + SKU39: + - WAREHOUSE0 + SKU390: + - WAREHOUSE0 + SKU391: + - WAREHOUSE0 + SKU392: + - WAREHOUSE0 + SKU393: + - WAREHOUSE0 + SKU394: + - WAREHOUSE0 + SKU395: + - WAREHOUSE0 + SKU396: + - WAREHOUSE0 + SKU397: + - WAREHOUSE0 + SKU398: + - WAREHOUSE0 + SKU399: + - WAREHOUSE0 + SKU4: + - WAREHOUSE0 + SKU40: + - WAREHOUSE0 + SKU400: + - WAREHOUSE0 + SKU401: + - WAREHOUSE0 + SKU402: + - WAREHOUSE0 + SKU403: + - WAREHOUSE0 + SKU404: + - WAREHOUSE0 + SKU405: + - WAREHOUSE0 + SKU406: + - WAREHOUSE0 + SKU407: + - WAREHOUSE0 + SKU408: + - WAREHOUSE0 + SKU409: + - WAREHOUSE0 + SKU41: + - WAREHOUSE0 + SKU410: + - WAREHOUSE0 + SKU411: + - WAREHOUSE0 + SKU412: + - WAREHOUSE0 + SKU413: + - WAREHOUSE0 + SKU414: + - WAREHOUSE0 + SKU415: + - WAREHOUSE0 + SKU416: + - WAREHOUSE0 + SKU417: + - WAREHOUSE0 + SKU418: + - WAREHOUSE0 + SKU419: + - WAREHOUSE0 + SKU42: + - WAREHOUSE0 + SKU420: + - WAREHOUSE0 + SKU421: + - WAREHOUSE0 + SKU422: + - WAREHOUSE0 + SKU423: + - WAREHOUSE0 + SKU424: + - WAREHOUSE0 + SKU425: + - WAREHOUSE0 + SKU426: + - WAREHOUSE0 + SKU427: + - WAREHOUSE0 + SKU428: + - WAREHOUSE0 + SKU429: + - WAREHOUSE0 + SKU43: + - WAREHOUSE0 + SKU430: + - WAREHOUSE0 + SKU431: + - WAREHOUSE0 + SKU432: + - WAREHOUSE0 + SKU433: + - WAREHOUSE0 + SKU434: + - WAREHOUSE0 + SKU435: + - WAREHOUSE0 + SKU436: + - WAREHOUSE0 + SKU437: + - WAREHOUSE0 + SKU438: + - WAREHOUSE0 + SKU439: + - WAREHOUSE0 + SKU44: + - WAREHOUSE0 + SKU440: + - WAREHOUSE0 + SKU441: + - WAREHOUSE0 + SKU442: + - WAREHOUSE0 + SKU443: + - WAREHOUSE0 + SKU444: + - WAREHOUSE0 + SKU445: + - WAREHOUSE0 + SKU446: + - WAREHOUSE0 + SKU447: + - WAREHOUSE0 + SKU448: + - WAREHOUSE0 + SKU449: + - WAREHOUSE0 + SKU45: + - WAREHOUSE0 + SKU450: + - WAREHOUSE0 + SKU451: + - WAREHOUSE0 + SKU452: + - WAREHOUSE0 + SKU453: + - WAREHOUSE0 + SKU454: + - WAREHOUSE0 + SKU455: + - WAREHOUSE0 + SKU456: + - WAREHOUSE0 + SKU457: + - WAREHOUSE0 + SKU458: + - WAREHOUSE0 + SKU459: + - WAREHOUSE0 + SKU46: + - WAREHOUSE0 + SKU460: + - WAREHOUSE0 + SKU461: + - WAREHOUSE0 + SKU462: + - WAREHOUSE0 + SKU463: + - WAREHOUSE0 + SKU464: + - WAREHOUSE0 + SKU465: + - WAREHOUSE0 + SKU466: + - WAREHOUSE0 + SKU467: + - WAREHOUSE0 + SKU468: + - WAREHOUSE0 + SKU469: + - WAREHOUSE0 + SKU47: + - WAREHOUSE0 + SKU470: + - WAREHOUSE0 + SKU471: + - WAREHOUSE0 + SKU472: + - WAREHOUSE0 + SKU473: + - WAREHOUSE0 + SKU474: + - WAREHOUSE0 + SKU475: + - WAREHOUSE0 + SKU476: + - WAREHOUSE0 + SKU477: + - WAREHOUSE0 + SKU478: + - WAREHOUSE0 + SKU479: + - WAREHOUSE0 + SKU48: + - WAREHOUSE0 + SKU480: + - WAREHOUSE0 + SKU481: + - WAREHOUSE0 + SKU482: + - WAREHOUSE0 + SKU483: + - WAREHOUSE0 + SKU484: + - WAREHOUSE0 + SKU485: + - WAREHOUSE0 + SKU486: + - WAREHOUSE0 + SKU487: + - WAREHOUSE0 + SKU488: + - WAREHOUSE0 + SKU489: + - WAREHOUSE0 + SKU49: + - WAREHOUSE0 + SKU490: + - WAREHOUSE0 + SKU491: + - WAREHOUSE0 + SKU492: + - WAREHOUSE0 + SKU493: + - WAREHOUSE0 + SKU494: + - WAREHOUSE0 + SKU495: + - WAREHOUSE0 + SKU496: + - WAREHOUSE0 + SKU497: + - WAREHOUSE0 + SKU498: + - WAREHOUSE0 + SKU499: + - WAREHOUSE0 + SKU5: + - WAREHOUSE0 + SKU50: + - WAREHOUSE0 + SKU500: + - WAREHOUSE0 + SKU501: + - WAREHOUSE0 + SKU502: + - WAREHOUSE0 + SKU503: + - WAREHOUSE0 + SKU504: + - WAREHOUSE0 + SKU505: + - WAREHOUSE0 + SKU506: + - WAREHOUSE0 + SKU507: + - WAREHOUSE0 + SKU508: + - WAREHOUSE0 + SKU509: + - WAREHOUSE0 + SKU51: + - WAREHOUSE0 + SKU510: + - WAREHOUSE0 + SKU511: + - WAREHOUSE0 + SKU512: + - WAREHOUSE0 + SKU513: + - WAREHOUSE0 + SKU514: + - WAREHOUSE0 + SKU515: + - WAREHOUSE0 + SKU516: + - WAREHOUSE0 + SKU517: + - WAREHOUSE0 + SKU518: + - WAREHOUSE0 + SKU519: + - WAREHOUSE0 + SKU52: + - WAREHOUSE0 + SKU520: + - WAREHOUSE0 + SKU521: + - WAREHOUSE0 + SKU522: + - WAREHOUSE0 + SKU523: + - WAREHOUSE0 + SKU524: + - WAREHOUSE0 + SKU525: + - WAREHOUSE0 + SKU526: + - WAREHOUSE0 + SKU527: + - WAREHOUSE0 + SKU528: + - WAREHOUSE0 + SKU529: + - WAREHOUSE0 + SKU53: + - WAREHOUSE0 + SKU530: + - WAREHOUSE0 + SKU531: + - WAREHOUSE0 + SKU532: + - WAREHOUSE0 + SKU533: + - WAREHOUSE0 + SKU534: + - WAREHOUSE0 + SKU535: + - WAREHOUSE0 + SKU536: + - WAREHOUSE0 + SKU537: + - WAREHOUSE0 + SKU538: + - WAREHOUSE0 + SKU539: + - WAREHOUSE0 + SKU54: + - WAREHOUSE0 + SKU540: + - WAREHOUSE0 + SKU541: + - WAREHOUSE0 + SKU542: + - WAREHOUSE0 + SKU543: + - WAREHOUSE0 + SKU544: + - WAREHOUSE0 + SKU545: + - WAREHOUSE0 + SKU546: + - WAREHOUSE0 + SKU547: + - WAREHOUSE0 + SKU548: + - WAREHOUSE0 + SKU549: + - WAREHOUSE0 + SKU55: + - WAREHOUSE0 + SKU550: + - WAREHOUSE0 + SKU551: + - WAREHOUSE0 + SKU552: + - WAREHOUSE0 + SKU553: + - WAREHOUSE0 + SKU554: + - WAREHOUSE0 + SKU555: + - WAREHOUSE0 + SKU556: + - WAREHOUSE0 + SKU557: + - WAREHOUSE0 + SKU558: + - WAREHOUSE0 + SKU559: + - WAREHOUSE0 + SKU56: + - WAREHOUSE0 + SKU560: + - WAREHOUSE0 + SKU561: + - WAREHOUSE0 + SKU562: + - WAREHOUSE0 + SKU563: + - WAREHOUSE0 + SKU564: + - WAREHOUSE0 + SKU565: + - WAREHOUSE0 + SKU566: + - WAREHOUSE0 + SKU567: + - WAREHOUSE0 + SKU568: + - WAREHOUSE0 + SKU569: + - WAREHOUSE0 + SKU57: + - WAREHOUSE0 + SKU570: + - WAREHOUSE0 + SKU571: + - WAREHOUSE0 + SKU572: + - WAREHOUSE0 + SKU573: + - WAREHOUSE0 + SKU574: + - WAREHOUSE0 + SKU575: + - WAREHOUSE0 + SKU576: + - WAREHOUSE0 + SKU577: + - WAREHOUSE0 + SKU578: + - WAREHOUSE0 + SKU579: + - WAREHOUSE0 + SKU58: + - WAREHOUSE0 + SKU580: + - WAREHOUSE0 + SKU581: + - WAREHOUSE0 + SKU582: + - WAREHOUSE0 + SKU583: + - WAREHOUSE0 + SKU584: + - WAREHOUSE0 + SKU585: + - WAREHOUSE0 + SKU586: + - WAREHOUSE0 + SKU587: + - WAREHOUSE0 + SKU588: + - WAREHOUSE0 + SKU589: + - WAREHOUSE0 + SKU59: + - WAREHOUSE0 + SKU590: + - WAREHOUSE0 + SKU591: + - WAREHOUSE0 + SKU592: + - WAREHOUSE0 + SKU593: + - WAREHOUSE0 + SKU594: + - WAREHOUSE0 + SKU595: + - WAREHOUSE0 + SKU596: + - WAREHOUSE0 + SKU597: + - WAREHOUSE0 + SKU598: + - WAREHOUSE0 + SKU599: + - WAREHOUSE0 + SKU6: + - WAREHOUSE0 + SKU60: + - WAREHOUSE0 + SKU600: + - WAREHOUSE0 + SKU601: + - WAREHOUSE0 + SKU602: + - WAREHOUSE0 + SKU603: + - WAREHOUSE0 + SKU604: + - WAREHOUSE0 + SKU605: + - WAREHOUSE0 + SKU606: + - WAREHOUSE0 + SKU607: + - WAREHOUSE0 + SKU608: + - WAREHOUSE0 + SKU609: + - WAREHOUSE0 + SKU61: + - WAREHOUSE0 + SKU610: + - WAREHOUSE0 + SKU611: + - WAREHOUSE0 + SKU612: + - WAREHOUSE0 + SKU613: + - WAREHOUSE0 + SKU614: + - WAREHOUSE0 + SKU615: + - WAREHOUSE0 + SKU616: + - WAREHOUSE0 + SKU617: + - WAREHOUSE0 + SKU618: + - WAREHOUSE0 + SKU619: + - WAREHOUSE0 + SKU62: + - WAREHOUSE0 + SKU620: + - WAREHOUSE0 + SKU621: + - WAREHOUSE0 + SKU622: + - WAREHOUSE0 + SKU623: + - WAREHOUSE0 + SKU624: + - WAREHOUSE0 + SKU625: + - WAREHOUSE0 + SKU626: + - WAREHOUSE0 + SKU627: + - WAREHOUSE0 + SKU628: + - WAREHOUSE0 + SKU629: + - WAREHOUSE0 + SKU63: + - WAREHOUSE0 + SKU630: + - WAREHOUSE0 + SKU631: + - WAREHOUSE0 + SKU632: + - WAREHOUSE0 + SKU633: + - WAREHOUSE0 + SKU634: + - WAREHOUSE0 + SKU635: + - WAREHOUSE0 + SKU636: + - WAREHOUSE0 + SKU637: + - WAREHOUSE0 + SKU638: + - WAREHOUSE0 + SKU639: + - WAREHOUSE0 + SKU64: + - WAREHOUSE0 + SKU640: + - WAREHOUSE0 + SKU641: + - WAREHOUSE0 + SKU642: + - WAREHOUSE0 + SKU643: + - WAREHOUSE0 + SKU644: + - WAREHOUSE0 + SKU645: + - WAREHOUSE0 + SKU646: + - WAREHOUSE0 + SKU647: + - WAREHOUSE0 + SKU648: + - WAREHOUSE0 + SKU649: + - WAREHOUSE0 + SKU65: + - WAREHOUSE0 + SKU650: + - WAREHOUSE0 + SKU651: + - WAREHOUSE0 + SKU652: + - WAREHOUSE0 + SKU653: + - WAREHOUSE0 + SKU654: + - WAREHOUSE0 + SKU655: + - WAREHOUSE0 + SKU656: + - WAREHOUSE0 + SKU657: + - WAREHOUSE0 + SKU658: + - WAREHOUSE0 + SKU659: + - WAREHOUSE0 + SKU66: + - WAREHOUSE0 + SKU660: + - WAREHOUSE0 + SKU661: + - WAREHOUSE0 + SKU662: + - WAREHOUSE0 + SKU663: + - WAREHOUSE0 + SKU664: + - WAREHOUSE0 + SKU665: + - WAREHOUSE0 + SKU666: + - WAREHOUSE0 + SKU667: + - WAREHOUSE0 + SKU668: + - WAREHOUSE0 + SKU669: + - WAREHOUSE0 + SKU67: + - WAREHOUSE0 + SKU670: + - WAREHOUSE0 + SKU671: + - WAREHOUSE0 + SKU672: + - WAREHOUSE0 + SKU673: + - WAREHOUSE0 + SKU674: + - WAREHOUSE0 + SKU675: + - WAREHOUSE0 + SKU676: + - WAREHOUSE0 + SKU677: + - WAREHOUSE0 + SKU678: + - WAREHOUSE0 + SKU679: + - WAREHOUSE0 + SKU68: + - WAREHOUSE0 + SKU680: + - WAREHOUSE0 + SKU681: + - WAREHOUSE0 + SKU682: + - WAREHOUSE0 + SKU683: + - WAREHOUSE0 + SKU684: + - WAREHOUSE0 + SKU685: + - WAREHOUSE0 + SKU686: + - WAREHOUSE0 + SKU687: + - WAREHOUSE0 + SKU688: + - WAREHOUSE0 + SKU689: + - WAREHOUSE0 + SKU69: + - WAREHOUSE0 + SKU690: + - WAREHOUSE0 + SKU691: + - WAREHOUSE0 + SKU692: + - WAREHOUSE0 + SKU693: + - WAREHOUSE0 + SKU694: + - WAREHOUSE0 + SKU695: + - WAREHOUSE0 + SKU696: + - WAREHOUSE0 + SKU697: + - WAREHOUSE0 + SKU698: + - WAREHOUSE0 + SKU699: + - WAREHOUSE0 + SKU7: + - WAREHOUSE0 + SKU70: + - WAREHOUSE0 + SKU700: + - WAREHOUSE0 + SKU701: + - WAREHOUSE0 + SKU702: + - WAREHOUSE0 + SKU703: + - WAREHOUSE0 + SKU704: + - WAREHOUSE0 + SKU705: + - WAREHOUSE0 + SKU706: + - WAREHOUSE0 + SKU707: + - WAREHOUSE0 + SKU708: + - WAREHOUSE0 + SKU709: + - WAREHOUSE0 + SKU71: + - WAREHOUSE0 + SKU710: + - WAREHOUSE0 + SKU711: + - WAREHOUSE0 + SKU712: + - WAREHOUSE0 + SKU713: + - WAREHOUSE0 + SKU714: + - WAREHOUSE0 + SKU715: + - WAREHOUSE0 + SKU716: + - WAREHOUSE0 + SKU717: + - WAREHOUSE0 + SKU718: + - WAREHOUSE0 + SKU719: + - WAREHOUSE0 + SKU72: + - WAREHOUSE0 + SKU720: + - WAREHOUSE0 + SKU721: + - WAREHOUSE0 + SKU722: + - WAREHOUSE0 + SKU723: + - WAREHOUSE0 + SKU724: + - WAREHOUSE0 + SKU725: + - WAREHOUSE0 + SKU726: + - WAREHOUSE0 + SKU727: + - WAREHOUSE0 + SKU728: + - WAREHOUSE0 + SKU729: + - WAREHOUSE0 + SKU73: + - WAREHOUSE0 + SKU730: + - WAREHOUSE0 + SKU731: + - WAREHOUSE0 + SKU732: + - WAREHOUSE0 + SKU733: + - WAREHOUSE0 + SKU734: + - WAREHOUSE0 + SKU735: + - WAREHOUSE0 + SKU736: + - WAREHOUSE0 + SKU737: + - WAREHOUSE0 + SKU738: + - WAREHOUSE0 + SKU739: + - WAREHOUSE0 + SKU74: + - WAREHOUSE0 + SKU740: + - WAREHOUSE0 + SKU741: + - WAREHOUSE0 + SKU742: + - WAREHOUSE0 + SKU743: + - WAREHOUSE0 + SKU744: + - WAREHOUSE0 + SKU745: + - WAREHOUSE0 + SKU746: + - WAREHOUSE0 + SKU747: + - WAREHOUSE0 + SKU748: + - WAREHOUSE0 + SKU749: + - WAREHOUSE0 + SKU75: + - WAREHOUSE0 + SKU750: + - WAREHOUSE0 + SKU751: + - WAREHOUSE0 + SKU752: + - WAREHOUSE0 + SKU753: + - WAREHOUSE0 + SKU754: + - WAREHOUSE0 + SKU755: + - WAREHOUSE0 + SKU756: + - WAREHOUSE0 + SKU757: + - WAREHOUSE0 + SKU758: + - WAREHOUSE0 + SKU759: + - WAREHOUSE0 + SKU76: + - WAREHOUSE0 + SKU760: + - WAREHOUSE0 + SKU761: + - WAREHOUSE0 + SKU762: + - WAREHOUSE0 + SKU763: + - WAREHOUSE0 + SKU764: + - WAREHOUSE0 + SKU765: + - WAREHOUSE0 + SKU766: + - WAREHOUSE0 + SKU767: + - WAREHOUSE0 + SKU768: + - WAREHOUSE0 + SKU769: + - WAREHOUSE0 + SKU77: + - WAREHOUSE0 + SKU770: + - WAREHOUSE0 + SKU771: + - WAREHOUSE0 + SKU772: + - WAREHOUSE0 + SKU773: + - WAREHOUSE0 + SKU774: + - WAREHOUSE0 + SKU775: + - WAREHOUSE0 + SKU776: + - WAREHOUSE0 + SKU777: + - WAREHOUSE0 + SKU778: + - WAREHOUSE0 + SKU779: + - WAREHOUSE0 + SKU78: + - WAREHOUSE0 + SKU780: + - WAREHOUSE0 + SKU781: + - WAREHOUSE0 + SKU782: + - WAREHOUSE0 + SKU783: + - WAREHOUSE0 + SKU784: + - WAREHOUSE0 + SKU785: + - WAREHOUSE0 + SKU786: + - WAREHOUSE0 + SKU787: + - WAREHOUSE0 + SKU788: + - WAREHOUSE0 + SKU789: + - WAREHOUSE0 + SKU79: + - WAREHOUSE0 + SKU790: + - WAREHOUSE0 + SKU791: + - WAREHOUSE0 + SKU792: + - WAREHOUSE0 + SKU793: + - WAREHOUSE0 + SKU794: + - WAREHOUSE0 + SKU795: + - WAREHOUSE0 + SKU796: + - WAREHOUSE0 + SKU797: + - WAREHOUSE0 + SKU798: + - WAREHOUSE0 + SKU799: + - WAREHOUSE0 + SKU8: + - WAREHOUSE0 + SKU80: + - WAREHOUSE0 + SKU800: + - WAREHOUSE0 + SKU801: + - WAREHOUSE0 + SKU802: + - WAREHOUSE0 + SKU803: + - WAREHOUSE0 + SKU804: + - WAREHOUSE0 + SKU805: + - WAREHOUSE0 + SKU806: + - WAREHOUSE0 + SKU807: + - WAREHOUSE0 + SKU808: + - WAREHOUSE0 + SKU809: + - WAREHOUSE0 + SKU81: + - WAREHOUSE0 + SKU810: + - WAREHOUSE0 + SKU811: + - WAREHOUSE0 + SKU812: + - WAREHOUSE0 + SKU813: + - WAREHOUSE0 + SKU814: + - WAREHOUSE0 + SKU815: + - WAREHOUSE0 + SKU816: + - WAREHOUSE0 + SKU817: + - WAREHOUSE0 + SKU818: + - WAREHOUSE0 + SKU819: + - WAREHOUSE0 + SKU82: + - WAREHOUSE0 + SKU820: + - WAREHOUSE0 + SKU821: + - WAREHOUSE0 + SKU822: + - WAREHOUSE0 + SKU823: + - WAREHOUSE0 + SKU824: + - WAREHOUSE0 + SKU825: + - WAREHOUSE0 + SKU826: + - WAREHOUSE0 + SKU827: + - WAREHOUSE0 + SKU828: + - WAREHOUSE0 + SKU829: + - WAREHOUSE0 + SKU83: + - WAREHOUSE0 + SKU830: + - WAREHOUSE0 + SKU831: + - WAREHOUSE0 + SKU832: + - WAREHOUSE0 + SKU833: + - WAREHOUSE0 + SKU834: + - WAREHOUSE0 + SKU835: + - WAREHOUSE0 + SKU836: + - WAREHOUSE0 + SKU837: + - WAREHOUSE0 + SKU838: + - WAREHOUSE0 + SKU839: + - WAREHOUSE0 + SKU84: + - WAREHOUSE0 + SKU840: + - WAREHOUSE0 + SKU841: + - WAREHOUSE0 + SKU842: + - WAREHOUSE0 + SKU843: + - WAREHOUSE0 + SKU844: + - WAREHOUSE0 + SKU845: + - WAREHOUSE0 + SKU846: + - WAREHOUSE0 + SKU847: + - WAREHOUSE0 + SKU848: + - WAREHOUSE0 + SKU849: + - WAREHOUSE0 + SKU85: + - WAREHOUSE0 + SKU850: + - WAREHOUSE0 + SKU851: + - WAREHOUSE0 + SKU852: + - WAREHOUSE0 + SKU853: + - WAREHOUSE0 + SKU854: + - WAREHOUSE0 + SKU855: + - WAREHOUSE0 + SKU856: + - WAREHOUSE0 + SKU857: + - WAREHOUSE0 + SKU858: + - WAREHOUSE0 + SKU859: + - WAREHOUSE0 + SKU86: + - WAREHOUSE0 + SKU860: + - WAREHOUSE0 + SKU861: + - WAREHOUSE0 + SKU862: + - WAREHOUSE0 + SKU863: + - WAREHOUSE0 + SKU864: + - WAREHOUSE0 + SKU865: + - WAREHOUSE0 + SKU866: + - WAREHOUSE0 + SKU867: + - WAREHOUSE0 + SKU868: + - WAREHOUSE0 + SKU869: + - WAREHOUSE0 + SKU87: + - WAREHOUSE0 + SKU870: + - WAREHOUSE0 + SKU871: + - WAREHOUSE0 + SKU872: + - WAREHOUSE0 + SKU873: + - WAREHOUSE0 + SKU874: + - WAREHOUSE0 + SKU875: + - WAREHOUSE0 + SKU876: + - WAREHOUSE0 + SKU877: + - WAREHOUSE0 + SKU878: + - WAREHOUSE0 + SKU879: + - WAREHOUSE0 + SKU88: + - WAREHOUSE0 + SKU880: + - WAREHOUSE0 + SKU881: + - WAREHOUSE0 + SKU882: + - WAREHOUSE0 + SKU883: + - WAREHOUSE0 + SKU884: + - WAREHOUSE0 + SKU885: + - WAREHOUSE0 + SKU886: + - WAREHOUSE0 + SKU887: + - WAREHOUSE0 + SKU888: + - WAREHOUSE0 + SKU889: + - WAREHOUSE0 + SKU89: + - WAREHOUSE0 + SKU890: + - WAREHOUSE0 + SKU891: + - WAREHOUSE0 + SKU892: + - WAREHOUSE0 + SKU893: + - WAREHOUSE0 + SKU894: + - WAREHOUSE0 + SKU895: + - WAREHOUSE0 + SKU896: + - WAREHOUSE0 + SKU897: + - WAREHOUSE0 + SKU898: + - WAREHOUSE0 + SKU899: + - WAREHOUSE0 + SKU9: + - WAREHOUSE0 + SKU90: + - WAREHOUSE0 + SKU900: + - WAREHOUSE0 + SKU901: + - WAREHOUSE0 + SKU902: + - WAREHOUSE0 + SKU903: + - WAREHOUSE0 + SKU904: + - WAREHOUSE0 + SKU905: + - WAREHOUSE0 + SKU906: + - WAREHOUSE0 + SKU907: + - WAREHOUSE0 + SKU908: + - WAREHOUSE0 + SKU909: + - WAREHOUSE0 + SKU91: + - WAREHOUSE0 + SKU910: + - WAREHOUSE0 + SKU911: + - WAREHOUSE0 + SKU912: + - WAREHOUSE0 + SKU913: + - WAREHOUSE0 + SKU914: + - WAREHOUSE0 + SKU915: + - WAREHOUSE0 + SKU916: + - WAREHOUSE0 + SKU917: + - WAREHOUSE0 + SKU918: + - WAREHOUSE0 + SKU919: + - WAREHOUSE0 + SKU92: + - WAREHOUSE0 + SKU920: + - WAREHOUSE0 + SKU921: + - WAREHOUSE0 + SKU922: + - WAREHOUSE0 + SKU923: + - WAREHOUSE0 + SKU924: + - WAREHOUSE0 + SKU925: + - WAREHOUSE0 + SKU926: + - WAREHOUSE0 + SKU927: + - WAREHOUSE0 + SKU928: + - WAREHOUSE0 + SKU929: + - WAREHOUSE0 + SKU93: + - WAREHOUSE0 + SKU930: + - WAREHOUSE0 + SKU931: + - WAREHOUSE0 + SKU932: + - WAREHOUSE0 + SKU933: + - WAREHOUSE0 + SKU934: + - WAREHOUSE0 + SKU935: + - WAREHOUSE0 + SKU936: + - WAREHOUSE0 + SKU937: + - WAREHOUSE0 + SKU938: + - WAREHOUSE0 + SKU939: + - WAREHOUSE0 + SKU94: + - WAREHOUSE0 + SKU940: + - WAREHOUSE0 + SKU941: + - WAREHOUSE0 + SKU942: + - WAREHOUSE0 + SKU943: + - WAREHOUSE0 + SKU944: + - WAREHOUSE0 + SKU945: + - WAREHOUSE0 + SKU946: + - WAREHOUSE0 + SKU947: + - WAREHOUSE0 + SKU948: + - WAREHOUSE0 + SKU949: + - WAREHOUSE0 + SKU95: + - WAREHOUSE0 + SKU950: + - WAREHOUSE0 + SKU951: + - WAREHOUSE0 + SKU952: + - WAREHOUSE0 + SKU953: + - WAREHOUSE0 + SKU954: + - WAREHOUSE0 + SKU955: + - WAREHOUSE0 + SKU956: + - WAREHOUSE0 + SKU957: + - WAREHOUSE0 + SKU958: + - WAREHOUSE0 + SKU959: + - WAREHOUSE0 + SKU96: + - WAREHOUSE0 + SKU960: + - WAREHOUSE0 + SKU961: + - WAREHOUSE0 + SKU962: + - WAREHOUSE0 + SKU963: + - WAREHOUSE0 + SKU964: + - WAREHOUSE0 + SKU965: + - WAREHOUSE0 + SKU966: + - WAREHOUSE0 + SKU967: + - WAREHOUSE0 + SKU968: + - WAREHOUSE0 + SKU969: + - WAREHOUSE0 + SKU97: + - WAREHOUSE0 + SKU970: + - WAREHOUSE0 + SKU971: + - WAREHOUSE0 + SKU972: + - WAREHOUSE0 + SKU973: + - WAREHOUSE0 + SKU974: + - WAREHOUSE0 + SKU975: + - WAREHOUSE0 + SKU976: + - WAREHOUSE0 + SKU977: + - WAREHOUSE0 + SKU978: + - WAREHOUSE0 + SKU979: + - WAREHOUSE0 + SKU98: + - WAREHOUSE0 + SKU980: + - WAREHOUSE0 + SKU981: + - WAREHOUSE0 + SKU982: + - WAREHOUSE0 + SKU983: + - WAREHOUSE0 + SKU984: + - WAREHOUSE0 + SKU985: + - WAREHOUSE0 + SKU986: + - WAREHOUSE0 + SKU987: + - WAREHOUSE0 + SKU988: + - WAREHOUSE0 + SKU989: + - WAREHOUSE0 + SKU99: + - WAREHOUSE0 + SKU990: + - WAREHOUSE0 + SKU991: + - WAREHOUSE0 + SKU992: + - WAREHOUSE0 + SKU993: + - WAREHOUSE0 + SKU994: + - WAREHOUSE0 + SKU995: + - WAREHOUSE0 + SKU996: + - WAREHOUSE0 + SKU997: + - WAREHOUSE0 + SKU998: + - WAREHOUSE0 + SKU999: + - WAREHOUSE0 + WAREHOUSE0: + SKU0: + - SUPPLIER0 + SKU1: + - SUPPLIER0 + SKU10: + - SUPPLIER0 + SKU100: + - SUPPLIER0 + SKU101: + - SUPPLIER0 + SKU102: + - SUPPLIER0 + SKU103: + - SUPPLIER0 + SKU104: + - SUPPLIER0 + SKU105: + - SUPPLIER0 + SKU106: + - SUPPLIER0 + SKU107: + - SUPPLIER0 + SKU108: + - SUPPLIER0 + SKU109: + - SUPPLIER0 + SKU11: + - SUPPLIER0 + SKU110: + - SUPPLIER0 + SKU111: + - SUPPLIER0 + SKU112: + - SUPPLIER0 + SKU113: + - SUPPLIER0 + SKU114: + - SUPPLIER0 + SKU115: + - SUPPLIER0 + SKU116: + - SUPPLIER0 + SKU117: + - SUPPLIER0 + SKU118: + - SUPPLIER0 + SKU119: + - SUPPLIER0 + SKU12: + - SUPPLIER0 + SKU120: + - SUPPLIER0 + SKU121: + - SUPPLIER0 + SKU122: + - SUPPLIER0 + SKU123: + - SUPPLIER0 + SKU124: + - SUPPLIER0 + SKU125: + - SUPPLIER0 + SKU126: + - SUPPLIER0 + SKU127: + - SUPPLIER0 + SKU128: + - SUPPLIER0 + SKU129: + - SUPPLIER0 + SKU13: + - SUPPLIER0 + SKU130: + - SUPPLIER0 + SKU131: + - SUPPLIER0 + SKU132: + - SUPPLIER0 + SKU133: + - SUPPLIER0 + SKU134: + - SUPPLIER0 + SKU135: + - SUPPLIER0 + SKU136: + - SUPPLIER0 + SKU137: + - SUPPLIER0 + SKU138: + - SUPPLIER0 + SKU139: + - SUPPLIER0 + SKU14: + - SUPPLIER0 + SKU140: + - SUPPLIER0 + SKU141: + - SUPPLIER0 + SKU142: + - SUPPLIER0 + SKU143: + - SUPPLIER0 + SKU144: + - SUPPLIER0 + SKU145: + - SUPPLIER0 + SKU146: + - SUPPLIER0 + SKU147: + - SUPPLIER0 + SKU148: + - SUPPLIER0 + SKU149: + - SUPPLIER0 + SKU15: + - SUPPLIER0 + SKU150: + - SUPPLIER0 + SKU151: + - SUPPLIER0 + SKU152: + - SUPPLIER0 + SKU153: + - SUPPLIER0 + SKU154: + - SUPPLIER0 + SKU155: + - SUPPLIER0 + SKU156: + - SUPPLIER0 + SKU157: + - SUPPLIER0 + SKU158: + - SUPPLIER0 + SKU159: + - SUPPLIER0 + SKU16: + - SUPPLIER0 + SKU160: + - SUPPLIER0 + SKU161: + - SUPPLIER0 + SKU162: + - SUPPLIER0 + SKU163: + - SUPPLIER0 + SKU164: + - SUPPLIER0 + SKU165: + - SUPPLIER0 + SKU166: + - SUPPLIER0 + SKU167: + - SUPPLIER0 + SKU168: + - SUPPLIER0 + SKU169: + - SUPPLIER0 + SKU17: + - SUPPLIER0 + SKU170: + - SUPPLIER0 + SKU171: + - SUPPLIER0 + SKU172: + - SUPPLIER0 + SKU173: + - SUPPLIER0 + SKU174: + - SUPPLIER0 + SKU175: + - SUPPLIER0 + SKU176: + - SUPPLIER0 + SKU177: + - SUPPLIER0 + SKU178: + - SUPPLIER0 + SKU179: + - SUPPLIER0 + SKU18: + - SUPPLIER0 + SKU180: + - SUPPLIER0 + SKU181: + - SUPPLIER0 + SKU182: + - SUPPLIER0 + SKU183: + - SUPPLIER0 + SKU184: + - SUPPLIER0 + SKU185: + - SUPPLIER0 + SKU186: + - SUPPLIER0 + SKU187: + - SUPPLIER0 + SKU188: + - SUPPLIER0 + SKU189: + - SUPPLIER0 + SKU19: + - SUPPLIER0 + SKU190: + - SUPPLIER0 + SKU191: + - SUPPLIER0 + SKU192: + - SUPPLIER0 + SKU193: + - SUPPLIER0 + SKU194: + - SUPPLIER0 + SKU195: + - SUPPLIER0 + SKU196: + - SUPPLIER0 + SKU197: + - SUPPLIER0 + SKU198: + - SUPPLIER0 + SKU199: + - SUPPLIER0 + SKU2: + - SUPPLIER0 + SKU20: + - SUPPLIER0 + SKU200: + - SUPPLIER0 + SKU201: + - SUPPLIER0 + SKU202: + - SUPPLIER0 + SKU203: + - SUPPLIER0 + SKU204: + - SUPPLIER0 + SKU205: + - SUPPLIER0 + SKU206: + - SUPPLIER0 + SKU207: + - SUPPLIER0 + SKU208: + - SUPPLIER0 + SKU209: + - SUPPLIER0 + SKU21: + - SUPPLIER0 + SKU210: + - SUPPLIER0 + SKU211: + - SUPPLIER0 + SKU212: + - SUPPLIER0 + SKU213: + - SUPPLIER0 + SKU214: + - SUPPLIER0 + SKU215: + - SUPPLIER0 + SKU216: + - SUPPLIER0 + SKU217: + - SUPPLIER0 + SKU218: + - SUPPLIER0 + SKU219: + - SUPPLIER0 + SKU22: + - SUPPLIER0 + SKU220: + - SUPPLIER0 + SKU221: + - SUPPLIER0 + SKU222: + - SUPPLIER0 + SKU223: + - SUPPLIER0 + SKU224: + - SUPPLIER0 + SKU225: + - SUPPLIER0 + SKU226: + - SUPPLIER0 + SKU227: + - SUPPLIER0 + SKU228: + - SUPPLIER0 + SKU229: + - SUPPLIER0 + SKU23: + - SUPPLIER0 + SKU230: + - SUPPLIER0 + SKU231: + - SUPPLIER0 + SKU232: + - SUPPLIER0 + SKU233: + - SUPPLIER0 + SKU234: + - SUPPLIER0 + SKU235: + - SUPPLIER0 + SKU236: + - SUPPLIER0 + SKU237: + - SUPPLIER0 + SKU238: + - SUPPLIER0 + SKU239: + - SUPPLIER0 + SKU24: + - SUPPLIER0 + SKU240: + - SUPPLIER0 + SKU241: + - SUPPLIER0 + SKU242: + - SUPPLIER0 + SKU243: + - SUPPLIER0 + SKU244: + - SUPPLIER0 + SKU245: + - SUPPLIER0 + SKU246: + - SUPPLIER0 + SKU247: + - SUPPLIER0 + SKU248: + - SUPPLIER0 + SKU249: + - SUPPLIER0 + SKU25: + - SUPPLIER0 + SKU250: + - SUPPLIER0 + SKU251: + - SUPPLIER0 + SKU252: + - SUPPLIER0 + SKU253: + - SUPPLIER0 + SKU254: + - SUPPLIER0 + SKU255: + - SUPPLIER0 + SKU256: + - SUPPLIER0 + SKU257: + - SUPPLIER0 + SKU258: + - SUPPLIER0 + SKU259: + - SUPPLIER0 + SKU26: + - SUPPLIER0 + SKU260: + - SUPPLIER0 + SKU261: + - SUPPLIER0 + SKU262: + - SUPPLIER0 + SKU263: + - SUPPLIER0 + SKU264: + - SUPPLIER0 + SKU265: + - SUPPLIER0 + SKU266: + - SUPPLIER0 + SKU267: + - SUPPLIER0 + SKU268: + - SUPPLIER0 + SKU269: + - SUPPLIER0 + SKU27: + - SUPPLIER0 + SKU270: + - SUPPLIER0 + SKU271: + - SUPPLIER0 + SKU272: + - SUPPLIER0 + SKU273: + - SUPPLIER0 + SKU274: + - SUPPLIER0 + SKU275: + - SUPPLIER0 + SKU276: + - SUPPLIER0 + SKU277: + - SUPPLIER0 + SKU278: + - SUPPLIER0 + SKU279: + - SUPPLIER0 + SKU28: + - SUPPLIER0 + SKU280: + - SUPPLIER0 + SKU281: + - SUPPLIER0 + SKU282: + - SUPPLIER0 + SKU283: + - SUPPLIER0 + SKU284: + - SUPPLIER0 + SKU285: + - SUPPLIER0 + SKU286: + - SUPPLIER0 + SKU287: + - SUPPLIER0 + SKU288: + - SUPPLIER0 + SKU289: + - SUPPLIER0 + SKU29: + - SUPPLIER0 + SKU290: + - SUPPLIER0 + SKU291: + - SUPPLIER0 + SKU292: + - SUPPLIER0 + SKU293: + - SUPPLIER0 + SKU294: + - SUPPLIER0 + SKU295: + - SUPPLIER0 + SKU296: + - SUPPLIER0 + SKU297: + - SUPPLIER0 + SKU298: + - SUPPLIER0 + SKU299: + - SUPPLIER0 + SKU3: + - SUPPLIER0 + SKU30: + - SUPPLIER0 + SKU300: + - SUPPLIER0 + SKU301: + - SUPPLIER0 + SKU302: + - SUPPLIER0 + SKU303: + - SUPPLIER0 + SKU304: + - SUPPLIER0 + SKU305: + - SUPPLIER0 + SKU306: + - SUPPLIER0 + SKU307: + - SUPPLIER0 + SKU308: + - SUPPLIER0 + SKU309: + - SUPPLIER0 + SKU31: + - SUPPLIER0 + SKU310: + - SUPPLIER0 + SKU311: + - SUPPLIER0 + SKU312: + - SUPPLIER0 + SKU313: + - SUPPLIER0 + SKU314: + - SUPPLIER0 + SKU315: + - SUPPLIER0 + SKU316: + - SUPPLIER0 + SKU317: + - SUPPLIER0 + SKU318: + - SUPPLIER0 + SKU319: + - SUPPLIER0 + SKU32: + - SUPPLIER0 + SKU320: + - SUPPLIER0 + SKU321: + - SUPPLIER0 + SKU322: + - SUPPLIER0 + SKU323: + - SUPPLIER0 + SKU324: + - SUPPLIER0 + SKU325: + - SUPPLIER0 + SKU326: + - SUPPLIER0 + SKU327: + - SUPPLIER0 + SKU328: + - SUPPLIER0 + SKU329: + - SUPPLIER0 + SKU33: + - SUPPLIER0 + SKU330: + - SUPPLIER0 + SKU331: + - SUPPLIER0 + SKU332: + - SUPPLIER0 + SKU333: + - SUPPLIER0 + SKU334: + - SUPPLIER0 + SKU335: + - SUPPLIER0 + SKU336: + - SUPPLIER0 + SKU337: + - SUPPLIER0 + SKU338: + - SUPPLIER0 + SKU339: + - SUPPLIER0 + SKU34: + - SUPPLIER0 + SKU340: + - SUPPLIER0 + SKU341: + - SUPPLIER0 + SKU342: + - SUPPLIER0 + SKU343: + - SUPPLIER0 + SKU344: + - SUPPLIER0 + SKU345: + - SUPPLIER0 + SKU346: + - SUPPLIER0 + SKU347: + - SUPPLIER0 + SKU348: + - SUPPLIER0 + SKU349: + - SUPPLIER0 + SKU35: + - SUPPLIER0 + SKU350: + - SUPPLIER0 + SKU351: + - SUPPLIER0 + SKU352: + - SUPPLIER0 + SKU353: + - SUPPLIER0 + SKU354: + - SUPPLIER0 + SKU355: + - SUPPLIER0 + SKU356: + - SUPPLIER0 + SKU357: + - SUPPLIER0 + SKU358: + - SUPPLIER0 + SKU359: + - SUPPLIER0 + SKU36: + - SUPPLIER0 + SKU360: + - SUPPLIER0 + SKU361: + - SUPPLIER0 + SKU362: + - SUPPLIER0 + SKU363: + - SUPPLIER0 + SKU364: + - SUPPLIER0 + SKU365: + - SUPPLIER0 + SKU366: + - SUPPLIER0 + SKU367: + - SUPPLIER0 + SKU368: + - SUPPLIER0 + SKU369: + - SUPPLIER0 + SKU37: + - SUPPLIER0 + SKU370: + - SUPPLIER0 + SKU371: + - SUPPLIER0 + SKU372: + - SUPPLIER0 + SKU373: + - SUPPLIER0 + SKU374: + - SUPPLIER0 + SKU375: + - SUPPLIER0 + SKU376: + - SUPPLIER0 + SKU377: + - SUPPLIER0 + SKU378: + - SUPPLIER0 + SKU379: + - SUPPLIER0 + SKU38: + - SUPPLIER0 + SKU380: + - SUPPLIER0 + SKU381: + - SUPPLIER0 + SKU382: + - SUPPLIER0 + SKU383: + - SUPPLIER0 + SKU384: + - SUPPLIER0 + SKU385: + - SUPPLIER0 + SKU386: + - SUPPLIER0 + SKU387: + - SUPPLIER0 + SKU388: + - SUPPLIER0 + SKU389: + - SUPPLIER0 + SKU39: + - SUPPLIER0 + SKU390: + - SUPPLIER0 + SKU391: + - SUPPLIER0 + SKU392: + - SUPPLIER0 + SKU393: + - SUPPLIER0 + SKU394: + - SUPPLIER0 + SKU395: + - SUPPLIER0 + SKU396: + - SUPPLIER0 + SKU397: + - SUPPLIER0 + SKU398: + - SUPPLIER0 + SKU399: + - SUPPLIER0 + SKU4: + - SUPPLIER0 + SKU40: + - SUPPLIER0 + SKU400: + - SUPPLIER0 + SKU401: + - SUPPLIER0 + SKU402: + - SUPPLIER0 + SKU403: + - SUPPLIER0 + SKU404: + - SUPPLIER0 + SKU405: + - SUPPLIER0 + SKU406: + - SUPPLIER0 + SKU407: + - SUPPLIER0 + SKU408: + - SUPPLIER0 + SKU409: + - SUPPLIER0 + SKU41: + - SUPPLIER0 + SKU410: + - SUPPLIER0 + SKU411: + - SUPPLIER0 + SKU412: + - SUPPLIER0 + SKU413: + - SUPPLIER0 + SKU414: + - SUPPLIER0 + SKU415: + - SUPPLIER0 + SKU416: + - SUPPLIER0 + SKU417: + - SUPPLIER0 + SKU418: + - SUPPLIER0 + SKU419: + - SUPPLIER0 + SKU42: + - SUPPLIER0 + SKU420: + - SUPPLIER0 + SKU421: + - SUPPLIER0 + SKU422: + - SUPPLIER0 + SKU423: + - SUPPLIER0 + SKU424: + - SUPPLIER0 + SKU425: + - SUPPLIER0 + SKU426: + - SUPPLIER0 + SKU427: + - SUPPLIER0 + SKU428: + - SUPPLIER0 + SKU429: + - SUPPLIER0 + SKU43: + - SUPPLIER0 + SKU430: + - SUPPLIER0 + SKU431: + - SUPPLIER0 + SKU432: + - SUPPLIER0 + SKU433: + - SUPPLIER0 + SKU434: + - SUPPLIER0 + SKU435: + - SUPPLIER0 + SKU436: + - SUPPLIER0 + SKU437: + - SUPPLIER0 + SKU438: + - SUPPLIER0 + SKU439: + - SUPPLIER0 + SKU44: + - SUPPLIER0 + SKU440: + - SUPPLIER0 + SKU441: + - SUPPLIER0 + SKU442: + - SUPPLIER0 + SKU443: + - SUPPLIER0 + SKU444: + - SUPPLIER0 + SKU445: + - SUPPLIER0 + SKU446: + - SUPPLIER0 + SKU447: + - SUPPLIER0 + SKU448: + - SUPPLIER0 + SKU449: + - SUPPLIER0 + SKU45: + - SUPPLIER0 + SKU450: + - SUPPLIER0 + SKU451: + - SUPPLIER0 + SKU452: + - SUPPLIER0 + SKU453: + - SUPPLIER0 + SKU454: + - SUPPLIER0 + SKU455: + - SUPPLIER0 + SKU456: + - SUPPLIER0 + SKU457: + - SUPPLIER0 + SKU458: + - SUPPLIER0 + SKU459: + - SUPPLIER0 + SKU46: + - SUPPLIER0 + SKU460: + - SUPPLIER0 + SKU461: + - SUPPLIER0 + SKU462: + - SUPPLIER0 + SKU463: + - SUPPLIER0 + SKU464: + - SUPPLIER0 + SKU465: + - SUPPLIER0 + SKU466: + - SUPPLIER0 + SKU467: + - SUPPLIER0 + SKU468: + - SUPPLIER0 + SKU469: + - SUPPLIER0 + SKU47: + - SUPPLIER0 + SKU470: + - SUPPLIER0 + SKU471: + - SUPPLIER0 + SKU472: + - SUPPLIER0 + SKU473: + - SUPPLIER0 + SKU474: + - SUPPLIER0 + SKU475: + - SUPPLIER0 + SKU476: + - SUPPLIER0 + SKU477: + - SUPPLIER0 + SKU478: + - SUPPLIER0 + SKU479: + - SUPPLIER0 + SKU48: + - SUPPLIER0 + SKU480: + - SUPPLIER0 + SKU481: + - SUPPLIER0 + SKU482: + - SUPPLIER0 + SKU483: + - SUPPLIER0 + SKU484: + - SUPPLIER0 + SKU485: + - SUPPLIER0 + SKU486: + - SUPPLIER0 + SKU487: + - SUPPLIER0 + SKU488: + - SUPPLIER0 + SKU489: + - SUPPLIER0 + SKU49: + - SUPPLIER0 + SKU490: + - SUPPLIER0 + SKU491: + - SUPPLIER0 + SKU492: + - SUPPLIER0 + SKU493: + - SUPPLIER0 + SKU494: + - SUPPLIER0 + SKU495: + - SUPPLIER0 + SKU496: + - SUPPLIER0 + SKU497: + - SUPPLIER0 + SKU498: + - SUPPLIER0 + SKU499: + - SUPPLIER0 + SKU5: + - SUPPLIER0 + SKU50: + - SUPPLIER0 + SKU500: + - SUPPLIER0 + SKU501: + - SUPPLIER0 + SKU502: + - SUPPLIER0 + SKU503: + - SUPPLIER0 + SKU504: + - SUPPLIER0 + SKU505: + - SUPPLIER0 + SKU506: + - SUPPLIER0 + SKU507: + - SUPPLIER0 + SKU508: + - SUPPLIER0 + SKU509: + - SUPPLIER0 + SKU51: + - SUPPLIER0 + SKU510: + - SUPPLIER0 + SKU511: + - SUPPLIER0 + SKU512: + - SUPPLIER0 + SKU513: + - SUPPLIER0 + SKU514: + - SUPPLIER0 + SKU515: + - SUPPLIER0 + SKU516: + - SUPPLIER0 + SKU517: + - SUPPLIER0 + SKU518: + - SUPPLIER0 + SKU519: + - SUPPLIER0 + SKU52: + - SUPPLIER0 + SKU520: + - SUPPLIER0 + SKU521: + - SUPPLIER0 + SKU522: + - SUPPLIER0 + SKU523: + - SUPPLIER0 + SKU524: + - SUPPLIER0 + SKU525: + - SUPPLIER0 + SKU526: + - SUPPLIER0 + SKU527: + - SUPPLIER0 + SKU528: + - SUPPLIER0 + SKU529: + - SUPPLIER0 + SKU53: + - SUPPLIER0 + SKU530: + - SUPPLIER0 + SKU531: + - SUPPLIER0 + SKU532: + - SUPPLIER0 + SKU533: + - SUPPLIER0 + SKU534: + - SUPPLIER0 + SKU535: + - SUPPLIER0 + SKU536: + - SUPPLIER0 + SKU537: + - SUPPLIER0 + SKU538: + - SUPPLIER0 + SKU539: + - SUPPLIER0 + SKU54: + - SUPPLIER0 + SKU540: + - SUPPLIER0 + SKU541: + - SUPPLIER0 + SKU542: + - SUPPLIER0 + SKU543: + - SUPPLIER0 + SKU544: + - SUPPLIER0 + SKU545: + - SUPPLIER0 + SKU546: + - SUPPLIER0 + SKU547: + - SUPPLIER0 + SKU548: + - SUPPLIER0 + SKU549: + - SUPPLIER0 + SKU55: + - SUPPLIER0 + SKU550: + - SUPPLIER0 + SKU551: + - SUPPLIER0 + SKU552: + - SUPPLIER0 + SKU553: + - SUPPLIER0 + SKU554: + - SUPPLIER0 + SKU555: + - SUPPLIER0 + SKU556: + - SUPPLIER0 + SKU557: + - SUPPLIER0 + SKU558: + - SUPPLIER0 + SKU559: + - SUPPLIER0 + SKU56: + - SUPPLIER0 + SKU560: + - SUPPLIER0 + SKU561: + - SUPPLIER0 + SKU562: + - SUPPLIER0 + SKU563: + - SUPPLIER0 + SKU564: + - SUPPLIER0 + SKU565: + - SUPPLIER0 + SKU566: + - SUPPLIER0 + SKU567: + - SUPPLIER0 + SKU568: + - SUPPLIER0 + SKU569: + - SUPPLIER0 + SKU57: + - SUPPLIER0 + SKU570: + - SUPPLIER0 + SKU571: + - SUPPLIER0 + SKU572: + - SUPPLIER0 + SKU573: + - SUPPLIER0 + SKU574: + - SUPPLIER0 + SKU575: + - SUPPLIER0 + SKU576: + - SUPPLIER0 + SKU577: + - SUPPLIER0 + SKU578: + - SUPPLIER0 + SKU579: + - SUPPLIER0 + SKU58: + - SUPPLIER0 + SKU580: + - SUPPLIER0 + SKU581: + - SUPPLIER0 + SKU582: + - SUPPLIER0 + SKU583: + - SUPPLIER0 + SKU584: + - SUPPLIER0 + SKU585: + - SUPPLIER0 + SKU586: + - SUPPLIER0 + SKU587: + - SUPPLIER0 + SKU588: + - SUPPLIER0 + SKU589: + - SUPPLIER0 + SKU59: + - SUPPLIER0 + SKU590: + - SUPPLIER0 + SKU591: + - SUPPLIER0 + SKU592: + - SUPPLIER0 + SKU593: + - SUPPLIER0 + SKU594: + - SUPPLIER0 + SKU595: + - SUPPLIER0 + SKU596: + - SUPPLIER0 + SKU597: + - SUPPLIER0 + SKU598: + - SUPPLIER0 + SKU599: + - SUPPLIER0 + SKU6: + - SUPPLIER0 + SKU60: + - SUPPLIER0 + SKU600: + - SUPPLIER0 + SKU601: + - SUPPLIER0 + SKU602: + - SUPPLIER0 + SKU603: + - SUPPLIER0 + SKU604: + - SUPPLIER0 + SKU605: + - SUPPLIER0 + SKU606: + - SUPPLIER0 + SKU607: + - SUPPLIER0 + SKU608: + - SUPPLIER0 + SKU609: + - SUPPLIER0 + SKU61: + - SUPPLIER0 + SKU610: + - SUPPLIER0 + SKU611: + - SUPPLIER0 + SKU612: + - SUPPLIER0 + SKU613: + - SUPPLIER0 + SKU614: + - SUPPLIER0 + SKU615: + - SUPPLIER0 + SKU616: + - SUPPLIER0 + SKU617: + - SUPPLIER0 + SKU618: + - SUPPLIER0 + SKU619: + - SUPPLIER0 + SKU62: + - SUPPLIER0 + SKU620: + - SUPPLIER0 + SKU621: + - SUPPLIER0 + SKU622: + - SUPPLIER0 + SKU623: + - SUPPLIER0 + SKU624: + - SUPPLIER0 + SKU625: + - SUPPLIER0 + SKU626: + - SUPPLIER0 + SKU627: + - SUPPLIER0 + SKU628: + - SUPPLIER0 + SKU629: + - SUPPLIER0 + SKU63: + - SUPPLIER0 + SKU630: + - SUPPLIER0 + SKU631: + - SUPPLIER0 + SKU632: + - SUPPLIER0 + SKU633: + - SUPPLIER0 + SKU634: + - SUPPLIER0 + SKU635: + - SUPPLIER0 + SKU636: + - SUPPLIER0 + SKU637: + - SUPPLIER0 + SKU638: + - SUPPLIER0 + SKU639: + - SUPPLIER0 + SKU64: + - SUPPLIER0 + SKU640: + - SUPPLIER0 + SKU641: + - SUPPLIER0 + SKU642: + - SUPPLIER0 + SKU643: + - SUPPLIER0 + SKU644: + - SUPPLIER0 + SKU645: + - SUPPLIER0 + SKU646: + - SUPPLIER0 + SKU647: + - SUPPLIER0 + SKU648: + - SUPPLIER0 + SKU649: + - SUPPLIER0 + SKU65: + - SUPPLIER0 + SKU650: + - SUPPLIER0 + SKU651: + - SUPPLIER0 + SKU652: + - SUPPLIER0 + SKU653: + - SUPPLIER0 + SKU654: + - SUPPLIER0 + SKU655: + - SUPPLIER0 + SKU656: + - SUPPLIER0 + SKU657: + - SUPPLIER0 + SKU658: + - SUPPLIER0 + SKU659: + - SUPPLIER0 + SKU66: + - SUPPLIER0 + SKU660: + - SUPPLIER0 + SKU661: + - SUPPLIER0 + SKU662: + - SUPPLIER0 + SKU663: + - SUPPLIER0 + SKU664: + - SUPPLIER0 + SKU665: + - SUPPLIER0 + SKU666: + - SUPPLIER0 + SKU667: + - SUPPLIER0 + SKU668: + - SUPPLIER0 + SKU669: + - SUPPLIER0 + SKU67: + - SUPPLIER0 + SKU670: + - SUPPLIER0 + SKU671: + - SUPPLIER0 + SKU672: + - SUPPLIER0 + SKU673: + - SUPPLIER0 + SKU674: + - SUPPLIER0 + SKU675: + - SUPPLIER0 + SKU676: + - SUPPLIER0 + SKU677: + - SUPPLIER0 + SKU678: + - SUPPLIER0 + SKU679: + - SUPPLIER0 + SKU68: + - SUPPLIER0 + SKU680: + - SUPPLIER0 + SKU681: + - SUPPLIER0 + SKU682: + - SUPPLIER0 + SKU683: + - SUPPLIER0 + SKU684: + - SUPPLIER0 + SKU685: + - SUPPLIER0 + SKU686: + - SUPPLIER0 + SKU687: + - SUPPLIER0 + SKU688: + - SUPPLIER0 + SKU689: + - SUPPLIER0 + SKU69: + - SUPPLIER0 + SKU690: + - SUPPLIER0 + SKU691: + - SUPPLIER0 + SKU692: + - SUPPLIER0 + SKU693: + - SUPPLIER0 + SKU694: + - SUPPLIER0 + SKU695: + - SUPPLIER0 + SKU696: + - SUPPLIER0 + SKU697: + - SUPPLIER0 + SKU698: + - SUPPLIER0 + SKU699: + - SUPPLIER0 + SKU7: + - SUPPLIER0 + SKU70: + - SUPPLIER0 + SKU700: + - SUPPLIER0 + SKU701: + - SUPPLIER0 + SKU702: + - SUPPLIER0 + SKU703: + - SUPPLIER0 + SKU704: + - SUPPLIER0 + SKU705: + - SUPPLIER0 + SKU706: + - SUPPLIER0 + SKU707: + - SUPPLIER0 + SKU708: + - SUPPLIER0 + SKU709: + - SUPPLIER0 + SKU71: + - SUPPLIER0 + SKU710: + - SUPPLIER0 + SKU711: + - SUPPLIER0 + SKU712: + - SUPPLIER0 + SKU713: + - SUPPLIER0 + SKU714: + - SUPPLIER0 + SKU715: + - SUPPLIER0 + SKU716: + - SUPPLIER0 + SKU717: + - SUPPLIER0 + SKU718: + - SUPPLIER0 + SKU719: + - SUPPLIER0 + SKU72: + - SUPPLIER0 + SKU720: + - SUPPLIER0 + SKU721: + - SUPPLIER0 + SKU722: + - SUPPLIER0 + SKU723: + - SUPPLIER0 + SKU724: + - SUPPLIER0 + SKU725: + - SUPPLIER0 + SKU726: + - SUPPLIER0 + SKU727: + - SUPPLIER0 + SKU728: + - SUPPLIER0 + SKU729: + - SUPPLIER0 + SKU73: + - SUPPLIER0 + SKU730: + - SUPPLIER0 + SKU731: + - SUPPLIER0 + SKU732: + - SUPPLIER0 + SKU733: + - SUPPLIER0 + SKU734: + - SUPPLIER0 + SKU735: + - SUPPLIER0 + SKU736: + - SUPPLIER0 + SKU737: + - SUPPLIER0 + SKU738: + - SUPPLIER0 + SKU739: + - SUPPLIER0 + SKU74: + - SUPPLIER0 + SKU740: + - SUPPLIER0 + SKU741: + - SUPPLIER0 + SKU742: + - SUPPLIER0 + SKU743: + - SUPPLIER0 + SKU744: + - SUPPLIER0 + SKU745: + - SUPPLIER0 + SKU746: + - SUPPLIER0 + SKU747: + - SUPPLIER0 + SKU748: + - SUPPLIER0 + SKU749: + - SUPPLIER0 + SKU75: + - SUPPLIER0 + SKU750: + - SUPPLIER0 + SKU751: + - SUPPLIER0 + SKU752: + - SUPPLIER0 + SKU753: + - SUPPLIER0 + SKU754: + - SUPPLIER0 + SKU755: + - SUPPLIER0 + SKU756: + - SUPPLIER0 + SKU757: + - SUPPLIER0 + SKU758: + - SUPPLIER0 + SKU759: + - SUPPLIER0 + SKU76: + - SUPPLIER0 + SKU760: + - SUPPLIER0 + SKU761: + - SUPPLIER0 + SKU762: + - SUPPLIER0 + SKU763: + - SUPPLIER0 + SKU764: + - SUPPLIER0 + SKU765: + - SUPPLIER0 + SKU766: + - SUPPLIER0 + SKU767: + - SUPPLIER0 + SKU768: + - SUPPLIER0 + SKU769: + - SUPPLIER0 + SKU77: + - SUPPLIER0 + SKU770: + - SUPPLIER0 + SKU771: + - SUPPLIER0 + SKU772: + - SUPPLIER0 + SKU773: + - SUPPLIER0 + SKU774: + - SUPPLIER0 + SKU775: + - SUPPLIER0 + SKU776: + - SUPPLIER0 + SKU777: + - SUPPLIER0 + SKU778: + - SUPPLIER0 + SKU779: + - SUPPLIER0 + SKU78: + - SUPPLIER0 + SKU780: + - SUPPLIER0 + SKU781: + - SUPPLIER0 + SKU782: + - SUPPLIER0 + SKU783: + - SUPPLIER0 + SKU784: + - SUPPLIER0 + SKU785: + - SUPPLIER0 + SKU786: + - SUPPLIER0 + SKU787: + - SUPPLIER0 + SKU788: + - SUPPLIER0 + SKU789: + - SUPPLIER0 + SKU79: + - SUPPLIER0 + SKU790: + - SUPPLIER0 + SKU791: + - SUPPLIER0 + SKU792: + - SUPPLIER0 + SKU793: + - SUPPLIER0 + SKU794: + - SUPPLIER0 + SKU795: + - SUPPLIER0 + SKU796: + - SUPPLIER0 + SKU797: + - SUPPLIER0 + SKU798: + - SUPPLIER0 + SKU799: + - SUPPLIER0 + SKU8: + - SUPPLIER0 + SKU80: + - SUPPLIER0 + SKU800: + - SUPPLIER0 + SKU801: + - SUPPLIER0 + SKU802: + - SUPPLIER0 + SKU803: + - SUPPLIER0 + SKU804: + - SUPPLIER0 + SKU805: + - SUPPLIER0 + SKU806: + - SUPPLIER0 + SKU807: + - SUPPLIER0 + SKU808: + - SUPPLIER0 + SKU809: + - SUPPLIER0 + SKU81: + - SUPPLIER0 + SKU810: + - SUPPLIER0 + SKU811: + - SUPPLIER0 + SKU812: + - SUPPLIER0 + SKU813: + - SUPPLIER0 + SKU814: + - SUPPLIER0 + SKU815: + - SUPPLIER0 + SKU816: + - SUPPLIER0 + SKU817: + - SUPPLIER0 + SKU818: + - SUPPLIER0 + SKU819: + - SUPPLIER0 + SKU82: + - SUPPLIER0 + SKU820: + - SUPPLIER0 + SKU821: + - SUPPLIER0 + SKU822: + - SUPPLIER0 + SKU823: + - SUPPLIER0 + SKU824: + - SUPPLIER0 + SKU825: + - SUPPLIER0 + SKU826: + - SUPPLIER0 + SKU827: + - SUPPLIER0 + SKU828: + - SUPPLIER0 + SKU829: + - SUPPLIER0 + SKU83: + - SUPPLIER0 + SKU830: + - SUPPLIER0 + SKU831: + - SUPPLIER0 + SKU832: + - SUPPLIER0 + SKU833: + - SUPPLIER0 + SKU834: + - SUPPLIER0 + SKU835: + - SUPPLIER0 + SKU836: + - SUPPLIER0 + SKU837: + - SUPPLIER0 + SKU838: + - SUPPLIER0 + SKU839: + - SUPPLIER0 + SKU84: + - SUPPLIER0 + SKU840: + - SUPPLIER0 + SKU841: + - SUPPLIER0 + SKU842: + - SUPPLIER0 + SKU843: + - SUPPLIER0 + SKU844: + - SUPPLIER0 + SKU845: + - SUPPLIER0 + SKU846: + - SUPPLIER0 + SKU847: + - SUPPLIER0 + SKU848: + - SUPPLIER0 + SKU849: + - SUPPLIER0 + SKU85: + - SUPPLIER0 + SKU850: + - SUPPLIER0 + SKU851: + - SUPPLIER0 + SKU852: + - SUPPLIER0 + SKU853: + - SUPPLIER0 + SKU854: + - SUPPLIER0 + SKU855: + - SUPPLIER0 + SKU856: + - SUPPLIER0 + SKU857: + - SUPPLIER0 + SKU858: + - SUPPLIER0 + SKU859: + - SUPPLIER0 + SKU86: + - SUPPLIER0 + SKU860: + - SUPPLIER0 + SKU861: + - SUPPLIER0 + SKU862: + - SUPPLIER0 + SKU863: + - SUPPLIER0 + SKU864: + - SUPPLIER0 + SKU865: + - SUPPLIER0 + SKU866: + - SUPPLIER0 + SKU867: + - SUPPLIER0 + SKU868: + - SUPPLIER0 + SKU869: + - SUPPLIER0 + SKU87: + - SUPPLIER0 + SKU870: + - SUPPLIER0 + SKU871: + - SUPPLIER0 + SKU872: + - SUPPLIER0 + SKU873: + - SUPPLIER0 + SKU874: + - SUPPLIER0 + SKU875: + - SUPPLIER0 + SKU876: + - SUPPLIER0 + SKU877: + - SUPPLIER0 + SKU878: + - SUPPLIER0 + SKU879: + - SUPPLIER0 + SKU88: + - SUPPLIER0 + SKU880: + - SUPPLIER0 + SKU881: + - SUPPLIER0 + SKU882: + - SUPPLIER0 + SKU883: + - SUPPLIER0 + SKU884: + - SUPPLIER0 + SKU885: + - SUPPLIER0 + SKU886: + - SUPPLIER0 + SKU887: + - SUPPLIER0 + SKU888: + - SUPPLIER0 + SKU889: + - SUPPLIER0 + SKU89: + - SUPPLIER0 + SKU890: + - SUPPLIER0 + SKU891: + - SUPPLIER0 + SKU892: + - SUPPLIER0 + SKU893: + - SUPPLIER0 + SKU894: + - SUPPLIER0 + SKU895: + - SUPPLIER0 + SKU896: + - SUPPLIER0 + SKU897: + - SUPPLIER0 + SKU898: + - SUPPLIER0 + SKU899: + - SUPPLIER0 + SKU9: + - SUPPLIER0 + SKU90: + - SUPPLIER0 + SKU900: + - SUPPLIER0 + SKU901: + - SUPPLIER0 + SKU902: + - SUPPLIER0 + SKU903: + - SUPPLIER0 + SKU904: + - SUPPLIER0 + SKU905: + - SUPPLIER0 + SKU906: + - SUPPLIER0 + SKU907: + - SUPPLIER0 + SKU908: + - SUPPLIER0 + SKU909: + - SUPPLIER0 + SKU91: + - SUPPLIER0 + SKU910: + - SUPPLIER0 + SKU911: + - SUPPLIER0 + SKU912: + - SUPPLIER0 + SKU913: + - SUPPLIER0 + SKU914: + - SUPPLIER0 + SKU915: + - SUPPLIER0 + SKU916: + - SUPPLIER0 + SKU917: + - SUPPLIER0 + SKU918: + - SUPPLIER0 + SKU919: + - SUPPLIER0 + SKU92: + - SUPPLIER0 + SKU920: + - SUPPLIER0 + SKU921: + - SUPPLIER0 + SKU922: + - SUPPLIER0 + SKU923: + - SUPPLIER0 + SKU924: + - SUPPLIER0 + SKU925: + - SUPPLIER0 + SKU926: + - SUPPLIER0 + SKU927: + - SUPPLIER0 + SKU928: + - SUPPLIER0 + SKU929: + - SUPPLIER0 + SKU93: + - SUPPLIER0 + SKU930: + - SUPPLIER0 + SKU931: + - SUPPLIER0 + SKU932: + - SUPPLIER0 + SKU933: + - SUPPLIER0 + SKU934: + - SUPPLIER0 + SKU935: + - SUPPLIER0 + SKU936: + - SUPPLIER0 + SKU937: + - SUPPLIER0 + SKU938: + - SUPPLIER0 + SKU939: + - SUPPLIER0 + SKU94: + - SUPPLIER0 + SKU940: + - SUPPLIER0 + SKU941: + - SUPPLIER0 + SKU942: + - SUPPLIER0 + SKU943: + - SUPPLIER0 + SKU944: + - SUPPLIER0 + SKU945: + - SUPPLIER0 + SKU946: + - SUPPLIER0 + SKU947: + - SUPPLIER0 + SKU948: + - SUPPLIER0 + SKU949: + - SUPPLIER0 + SKU95: + - SUPPLIER0 + SKU950: + - SUPPLIER0 + SKU951: + - SUPPLIER0 + SKU952: + - SUPPLIER0 + SKU953: + - SUPPLIER0 + SKU954: + - SUPPLIER0 + SKU955: + - SUPPLIER0 + SKU956: + - SUPPLIER0 + SKU957: + - SUPPLIER0 + SKU958: + - SUPPLIER0 + SKU959: + - SUPPLIER0 + SKU96: + - SUPPLIER0 + SKU960: + - SUPPLIER0 + SKU961: + - SUPPLIER0 + SKU962: + - SUPPLIER0 + SKU963: + - SUPPLIER0 + SKU964: + - SUPPLIER0 + SKU965: + - SUPPLIER0 + SKU966: + - SUPPLIER0 + SKU967: + - SUPPLIER0 + SKU968: + - SUPPLIER0 + SKU969: + - SUPPLIER0 + SKU97: + - SUPPLIER0 + SKU970: + - SUPPLIER0 + SKU971: + - SUPPLIER0 + SKU972: + - SUPPLIER0 + SKU973: + - SUPPLIER0 + SKU974: + - SUPPLIER0 + SKU975: + - SUPPLIER0 + SKU976: + - SUPPLIER0 + SKU977: + - SUPPLIER0 + SKU978: + - SUPPLIER0 + SKU979: + - SUPPLIER0 + SKU98: + - SUPPLIER0 + SKU980: + - SUPPLIER0 + SKU981: + - SUPPLIER0 + SKU982: + - SUPPLIER0 + SKU983: + - SUPPLIER0 + SKU984: + - SUPPLIER0 + SKU985: + - SUPPLIER0 + SKU986: + - SUPPLIER0 + SKU987: + - SUPPLIER0 + SKU988: + - SUPPLIER0 + SKU989: + - SUPPLIER0 + SKU99: + - SUPPLIER0 + SKU990: + - SUPPLIER0 + SKU991: + - SUPPLIER0 + SKU992: + - SUPPLIER0 + SKU993: + - SUPPLIER0 + SKU994: + - SUPPLIER0 + SKU995: + - SUPPLIER0 + SKU996: + - SUPPLIER0 + SKU997: + - SUPPLIER0 + SKU998: + - SUPPLIER0 + SKU999: + - SUPPLIER0 diff --git a/examples/supply_chain/envs/sample1/config.yml b/examples/supply_chain/topologies/sample1/config.yml similarity index 100% rename from examples/supply_chain/envs/sample1/config.yml rename to examples/supply_chain/topologies/sample1/config.yml From e39a7baafec5b6d778a3d355b1bda2386cfc7f87 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 15 Apr 2021 04:09:26 +0000 Subject: [PATCH 187/482] some renaming --- examples/supply_chain/README.md | 4 ++-- .../scripts/docker_compose_yml_generator.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/supply_chain/README.md b/examples/supply_chain/README.md index 11cf21cb9..d3b144847 100644 --- a/examples/supply_chain/README.md +++ b/examples/supply_chain/README.md @@ -1,8 +1,8 @@ # Supply Chain Scenario -This README contains instructions for running the supply chain scenario using the scripts provided under ```examples/supply_chain/scripts```. For details on state, action and reward shaping based on MARO's supply chain business engine, please refer to ```examples/supply_chain/sc_state_in_maro.md```. +This README contains instructions for running the supply chain scenario using the scripts provided under ```examples/supply_chain/scripts```. For details on state, action and reward shaping based on MARO's supply chain business engine, refer to ```examples/supply_chain/sc_state_in_maro.md```. To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: -1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This step only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. +1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. 2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the "distributed" section of ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). 3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py index 39612c0f9..343b01c99 100644 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -17,7 +17,7 @@ num_actors = config["distributed"]["num_actors"] redis_host = config["distributed"]["redis_host"] -docker_compose_yaml = { +docker_compose_manifest = { "version": "3.9", "services": { "redis": {"image": "redis:6", "container_name": redis_host}, @@ -33,12 +33,12 @@ for i in range(num_actors): actor_id = f"actor.{i}" - actor_template = deepcopy(docker_compose_yaml["services"]["learner"]) - del actor_template["build"] - actor_template["command"][-1] = "2" - actor_template["container_name"] = actor_id - actor_template["environment"] = [f"COMPONENT_NAME={actor_id}"] - docker_compose_yaml["services"][actor_id] = actor_template + actor_manifest = deepcopy(docker_compose_manifest["services"]["learner"]) + del actor_manifest["build"] + actor_manifest["command"][-1] = "2" + actor_manifest["container_name"] = actor_id + actor_manifest["environment"] = [f"COMPONENT_NAME={actor_id}"] + docker_compose_manifest["services"][actor_id] = actor_manifest with open(join(sc_code_dir, "docker-compose.yml"), "w") as fp: - yaml.safe_dump(docker_compose_yaml, fp) + yaml.safe_dump(docker_compose_manifest, fp) From 6238bbbd5b11ca708e5ff7eb41d7d4936ec2789d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 15 Apr 2021 04:19:36 +0000 Subject: [PATCH 188/482] updated README for supply chain regarding docker compose --- examples/supply_chain/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/supply_chain/README.md b/examples/supply_chain/README.md index d3b144847..6cc67f2d1 100644 --- a/examples/supply_chain/README.md +++ b/examples/supply_chain/README.md @@ -2,7 +2,7 @@ This README contains instructions for running the supply chain scenario using the scripts provided under ```examples/supply_chain/scripts```. For details on state, action and reward shaping based on MARO's supply chain business engine, refer to ```examples/supply_chain/sc_state_in_maro.md```. -To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: +The instructions require that you have Docker and Docker Compose installed and set up on your machine. For installing Docker, refer to https://docs.docker.com/get-docker/. For installing Docker Compose, refer to https://docs.docker.com/compose/install/. To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: 1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. 2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the "distributed" section of ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). 3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file From 3425d03efe1fb0d1eaa165f6796339132391c7c2 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 13:48:38 +0800 Subject: [PATCH 189/482] add missing lisence --- .../scenarios/supply_chain/units/simplemanufacture.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py index f9af16eef..d6c3b17c7 100644 --- a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from .manufacture import ManufactureUnit @@ -14,6 +17,5 @@ def step(self, tick: int): sku_num = len(self.facility.skus) unit_num_upper_bound = self.facility.storage.capacity // sku_num - current_product_number = self.facility.storage.get_product_number( - self.product_id) + current_product_number = self.facility.storage.get_product_number(self.product_id) self.manufacture_number = max(0, min(unit_num_upper_bound-current_product_number, production_rate)) From eea0303bb07f88150cdff8960dc20225b7568967 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 15:38:52 +0800 Subject: [PATCH 190/482] add outer seller unit and retailer facility, to support read demand from csv files --- .../hello_world/supply_chain/env_wrapper.py | 753 ------------------ examples/hello_world/supply_chain/hello.py | 19 +- .../supply_chain/outersample/config.yml | 208 +++++ examples/hello_world/supply_chain/pf_test.py | 36 - .../supply_chain/facilities/__init__.py | 1 + .../supply_chain/facilities/outretailer.py | 30 + .../supply_chain/topologies/core.yml | 6 + .../supply_chain/topologies/sample/config.yml | 297 +++++++ .../scenarios/supply_chain/units/__init__.py | 1 + .../supply_chain/units/outerseller.py | 117 +++ .../scenarios/supply_chain/units/seller.py | 12 +- 11 files changed, 678 insertions(+), 802 deletions(-) delete mode 100644 examples/hello_world/supply_chain/env_wrapper.py create mode 100644 examples/hello_world/supply_chain/outersample/config.yml delete mode 100644 examples/hello_world/supply_chain/pf_test.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/outretailer.py create mode 100644 maro/simulator/scenarios/supply_chain/topologies/sample/config.yml create mode 100644 maro/simulator/scenarios/supply_chain/units/outerseller.py diff --git a/examples/hello_world/supply_chain/env_wrapper.py b/examples/hello_world/supply_chain/env_wrapper.py deleted file mode 100644 index cdb3e30be..000000000 --- a/examples/hello_world/supply_chain/env_wrapper.py +++ /dev/null @@ -1,753 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.simulator import Env -from collections import defaultdict, namedtuple -import scipy.stats as st -import numpy as np - -from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction - - -def stock_constraint(f_state): - return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt']+7)*f_state['sale_mean']) - - -def is_replenish_constraint(f_state): - return (f_state['consumption_hist'][-1] > 0) - - -def low_profit(f_state): - return ((f_state['sku_price']-f_state['sku_cost']) * f_state['sale_mean'] <= 1000) - - -def low_stock_constraint(f_state): - return (0 < f_state['inventory_in_stock'] <= (f_state['max_vlt']+3)*f_state['sale_mean']) - - -def out_of_stock(f_state): - return (0 < f_state['inventory_in_stock']) - - -atoms = { - 'stock_constraint': stock_constraint, - 'is_replenish_constraint': is_replenish_constraint, - 'low_profit': low_profit, - 'low_stock_constraint': low_stock_constraint, - 'out_of_stock': out_of_stock -} - - -class UnitBaseInfo: - id: int = None - node_index: int = None - config: dict = None - summary: dict = None - - def __init__(self, unit_summary): - self.id = unit_summary["id"] - self.node_index = unit_summary["node_index"] - self.config = unit_summary.get("config", {}) - self.summary = unit_summary - - def __getitem__(self, key, default=None): - if key in self.summary: - return self.summary[key] - - return default - - -class SCEnvWrapper: - def __init__(self, env: Env): - self.env = env - self.balance_cal = BalanceSheetCalculator(env) - self.cur_balance_sheet_reward = None - self.storage_ss = env.snapshot_list["storage"] - - self._summary = env.summary['node_mapping'] - self._configs = env.configs - self._agent_types = self._summary["agent_types"] - self._units_mapping = self._summary["unit_mapping"] - self._agent_list = env.agent_idx_list - - self._sku_number = len(self._summary["skus"]) + 1 - self._max_price = self._summary["max_price"] - self._max_sources_per_facility = self._summary["max_sources_per_facility"] - - # state for each tick - self._cur_metrics = env.metrics - self._cur_facility_storage_products = None - - # TODO: this is fixed after env setup - self._cur_facility_storage_product_mapping = {} - - self._service_index_ppf_cache = {} - - - # facility -> { - # data_model_index:int, - # storage:UnitBaseInfo, - # distribution: UnitBaseInfo, - # product_id: { - # consumer: UnitBaseInfo, - # seller: UnitBaseInfo, - # manufacture: UnitBaseInfo - # } - # } - self.facility_levels = {} - - # unit id -> (facility id) - self.unit_2_facility_dict = {} - - for facility_id, facility in self._summary["facilities"].items(): - self.facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility['configs'], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } - - units = facility["units"] - - storage = units["storage"] - if storage is not None: - self.facility_levels[facility_id]["storage"] = UnitBaseInfo(storage) - - self.unit_2_facility_dict[storage["id"]] = facility_id - - distribution = units["distribution"] - - if distribution is not None: - self.facility_levels[facility_id]["distribution"] = UnitBaseInfo(distribution) - self.unit_2_facility_dict[distribution["id"]] = facility_id - - products = units["products"] - - if products: - for product_id, product in products.items(): - product_info = { - "skuproduct": UnitBaseInfo(product) - } - - self.unit_2_facility_dict[product["id"]] = facility_id - - seller = product['seller'] - - if seller is not None: - product_info["seller"] = UnitBaseInfo(seller) - self.unit_2_facility_dict[seller["id"]] = facility_id - - consumer = product["consumer"] - - if consumer is not None: - product_info["consumer"] = UnitBaseInfo(consumer) - self.unit_2_facility_dict[consumer["id"]] = facility_id - - manufacture = product["manufacture"] - - if manufacture is not None: - product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"] - ] = facility_id - - self.facility_levels[facility_id][product_id] = product_info - - def get_state(self, event): - self.cur_balance_sheet_reward = self.balance_cal.calc() - - return self._get_state() - - def get_action(self, action_by_agent): - env_action = {} - for agent_id, action in action_by_agent.items(): - # consumer action - if agent_id in self.state_info: - pd_id, sr_id = self.state_info[agent_id]["product_id"], self.state_info[agent_id]["source_id"] - env_action[agent_id] = ConsumerAction( - agent_id, pd_id, sr_id, action, 1) - # manufacturer action - else: - env_action[agent_id] = ManufactureAction(agent_id, action) - - return env_action - - def get_reward(self, tick=None): - wc = self.env.configs.settings["global_reward_weight_consumer"] - - parent_facility_balance = {} - - for f_id, sheet in self.cur_balance_sheet_reward.items(): - if f_id in self.unit_2_facility_dict: - # it is a product unit - parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] - else: - parent_facility_balance[f_id] = sheet - - consumer_reward_by_facility = { f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items() } - - rewards_by_agent = { - "consumer": {}, - "producer": {} - } - - for f_id, bsw in self.cur_balance_sheet_reward.items(): - rewards_by_agent["producer"][f_id] = bsw[1] - - for f_id, reward in consumer_reward_by_facility.items(): - rewards_by_agent["consumer"][f_id] = reward - - return rewards_by_agent - - def _get_state(self): - self._cur_metrics = self.env.metrics - - state = {} - - for agent_info in self._agent_list: - storage_index = self.facility_levels[agent_info.facility_id]['storage'].node_index - - storage_product_list = self.storage_ss[self.env.tick:storage_index:"product_list"].flatten().astype(np.int) - storage_product_number = self.storage_ss[self.env.tick:storage_index:"product_number"].flatten().astype(np.int) - - self._cur_facility_storage_products = { pid:pnum for pid, pnum in zip(storage_product_list, storage_product_number)} - - self._cur_facility_storage_product_mapping = {product_id: i for i, product_id in enumerate(storage_product_list)} - - f_state = self._state(agent_info) - - self._add_global_features(f_state) - - state[f"consumer.{agent_info.id}"] = f_state - state[f"producer.{agent_info.id}"] = f_state - - return self._serialize_state(state) - - def _state(self, agent_info): - state = {} - - self._add_facility_features(state, agent_info) - self._add_storage_features(state, agent_info) - self._add_bom_features(state, agent_info) - self._add_distributor_features(state, agent_info) - self._add_sale_features(state, agent_info) - self._add_vlt_features(state, agent_info) - self._add_consumer_features(state, agent_info) - self._add_price_features(state, agent_info) - - return state - - def _add_global_features(self, state): - state['global_time'] = self.env.tick - - def _add_facility_features(self, state, agent_info): - state['facility_type'] = [ - 1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] - - # NOTE: We cannot provide facility instance - state["facility"] = None - - state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] - - # NOTE: we have no constraint now - state['constraint_idx'] = [0] - - for atom_name in atoms.keys(): - state[atom_name] = list(np.ones(self._configs.settings['constraint_state_hist_len'])) - - # NOTE: named as facility_id but actually sku id - # NOTE: as our sku id start from 1, so we need expend one-hot length - state['facility_id'] = [0] * self._sku_number - - facility = self.facility_levels[agent_info.facility_id] - - if agent_info.is_facility: - # truely facility - state['facility_info'] = facility['config'] - state['sku_info'] = {} - - metrics = self._cur_metrics["facilities"][agent_info.facility_id] - - state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - else: - # a product unit - # 3rd slot is the facility id of this unit - state['facility_info'] = facility['config'] - state['sku_info'] = agent_info.sku - - metrics = self._cur_metrics["products"][agent_info.id] - state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - - # NOTE: ignore constraint here - - state['facility_id'][agent_info.sku.id] = 1 - - # NOTE: ignore atom here - - # NOTE: ignore this as we do not implement it now - state['echelon_level'] = 0 - - def _add_storage_features(self, state, agent_info): - facility = self.facility_levels[agent_info.facility_id] - - state['storage_levels'] = [0] * self._sku_number - - state['storage_capacity'] = facility['storage'].config["capacity"] - - state['storage_utilization'] = 0 - - for product_id, product_number in self._cur_facility_storage_products.items(): - state['storage_levels'][product_id] = product_number - state['storage_utilization'] += product_number - - def _add_bom_features(self, state, agent_info): - state['bom_inputs'] = [0] * self._sku_number - state['bom_outputs'] = [0] * self._sku_number - - if not agent_info.is_facility: - state['bom_inputs'][agent_info.sku.id] = 1 - state['bom_outputs'][agent_info.sku.id] = 1 - - def _add_vlt_features(self, state, agent_info): - sku_list = self._summary["skus"] - facility = self.facility_levels[agent_info.facility_id] - - current_source_list = [] - - # only for product unit - if agent_info.sku is not None: - current_source_list = facility["upstreams"].get(agent_info.sku.id, []) - - state['vlt'] = [0] * (self._max_sources_per_facility * self._sku_number) - state['max_vlt'] = 0 - - if not agent_info.is_facility: - # only for sku product - product_info = facility[agent_info.sku.id] - - if "consumer" in product_info and len(current_source_list) > 0: - state['max_vlt'] = product_info["skuproduct"]["max_vlt"] - - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - # NOTE: different with original code, our config can make sure that source has product we need - - if sku.id == agent_info.sku.id: - state['vlt'][i * len(sku_list) + j + 1] = facility["skus"][sku.id].vlt - - def _add_sale_features(self, state, agent_info): - state['sale_mean'] = 1.0 - state['sale_std'] = 1.0 - state['sale_gamma'] = 1.0 - state['service_level'] = 0.95 - state['total_backlog_demand'] = 0 - - settings = self.env.configs.settings - - hist_len = settings['sale_hist_len'] - consumption_hist_len = settings['consumption_hist_len'] - - state['sale_hist'] = [0] * hist_len - state['backlog_demand_hist'] = [0] * hist_len - state['consumption_hist'] = [0] * consumption_hist_len - state['pending_order'] = [0] * settings['pending_order_len'] - - if agent_info.is_facility: - return - - product_metrics = self._cur_metrics["products"][agent_info.id] - - # for product unit only - state['service_level'] = agent_info.sku.service_level - state['sale_mean'] = product_metrics["sale_mean"] - state['sale_gamma'] = state['sale_mean'] - state['sale_std'] = product_metrics["sale_std"] - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - if "consumer" in product_info: - # TODO: implement later - consumer_index = product_info["consumer"].node_index - - consumption_hist = self.env.snapshot_list["consumer"][[self.env.tick - i for i in range(consumption_hist_len-1, -1, -1)]:consumer_index:"latest_consumptions"] - consumption_hist = consumption_hist.flatten() - - state['consumption_hist'] = list(consumption_hist) - state['pending_order'] = list(product_metrics["pending_order_daily"]) - - if "seller" in product_info: - seller_index = product_info["seller"].node_index - seller_ss = self.env.snapshot_list["seller"] - - single_states = seller_ss[self.env.tick:seller_index:("total_demand")].flatten().astype(np.int) - hist_states = seller_ss[[self.env.tick - i for i in range(hist_len-1, -1, -1)]:seller_index:("sold", "demand")].flatten().reshape(-1, 2).astype(np.int) - - state['total_backlog_demand'] = single_states[0] - state['sale_hist'] = list(hist_states[:, 0]) - state['backlog_demand_hist'] = list(hist_states[:, 1]) - state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma - - def _add_distributor_features(self, state, agent_info): - state['distributor_in_transit_orders'] = 0 - state['distributor_in_transit_orders_qty'] = 0 - - facility = self.facility_levels[agent_info.facility_id] - - distribution = facility.get("distribution", None) - - if distribution is not None: - dist_states = self.env.snapshot_list["distribution"][self.env.tick:distribution.id:("remaining_order_quantity", "remaining_order_number")] - dist_states = dist_states.flatten().astype(np.int) - - state['distributor_in_transit_orders'] = dist_states[1] - state['distributor_in_transit_orders_qty'] = dist_states[0] - - def _add_consumer_features(self, state, agent_info): - state['consumer_source_export_mask'] = [0] * (self._max_sources_per_facility * self._sku_number) - state['consumer_source_inventory'] = [0] * self._sku_number - state['consumer_in_transit_orders'] = [0] * self._sku_number - - state['inventory_in_stock'] = 0 - state['inventory_in_transit'] = 0 - state['inventory_in_distribution'] = 0 - state['inventory_estimated'] = 0 - state['inventory_rop'] = 0 - state['is_over_stock'] = 0 - state['is_out_of_stock'] = 0 - state['is_below_rop'] = 0 - - if agent_info.is_facility: - return - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - if "consumer" not in product_info: - return - - source_list = facility["upstreams"].get(agent_info.sku.id, []) - - if len(source_list) == 0: - return - - sku_list = self._summary["skus"] - - for i, source in enumerate(source_list): - for j, sku in enumerate(sku_list.values()): - if sku.id == agent_info.sku.id: - state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = self.facility_levels[source]["skus"][sku.id].vlt - - in_transit_orders = self._cur_metrics['facilities'][agent_info.facility_id]["in_transit_orders"] - - for i, sku in enumerate(sku_list.values()): - state['consumer_in_transit_orders'][sku.id] += in_transit_orders[sku.id] - - state['inventory_in_stock'] = self._cur_facility_storage_products[agent_info.sku.id] - state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] - - pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] - - if pending_order is not None: - state['inventory_in_distribution'] = pending_order[agent_info.sku.id] - - state['inventory_estimated'] = (state['inventory_in_stock'] - + state['inventory_in_transit'] - - state['inventory_in_distribution']) - if (state['inventory_estimated'] >= 0.5*state['storage_capacity']): - state['is_over_stock'] = 1 - - if (state['inventory_estimated'] <= 0): - state['is_out_of_stock'] = 1 - - service_index = state['service_level'] - - if service_index not in self._service_index_ppf_cache: - self._service_index_ppf_cache[service_index] = st.norm.ppf(service_index) - - state['inventory_rop'] = (state['max_vlt']*state['sale_mean'] - + np.sqrt(state['max_vlt'])*state['sale_std']*self._service_index_ppf_cache[service_index]) - - if state['inventory_estimated'] < state['inventory_rop']: - state['is_below_rop'] = 1 - - def _add_price_features(self, state, agent_info): - state['max_price'] = self._max_price - state['sku_price'] = 0 - state['sku_cost'] = 0 - - if not agent_info.is_facility: - state['sku_price'] = agent_info.sku.price - state['sku_cost'] = agent_info.sku.cost - - def _serialize_state(self, state): - result = {} - - keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', - 'constraint_idx', 'is_accepted', 'consumption_hist']), - ('storage_capacity', ['storage_utilization']), - ('sale_gamma', ['sale_std', - 'sale_hist', - 'pending_order', - 'inventory_in_stock', - 'inventory_in_transit', - 'inventory_estimated', - 'inventory_rop']), - ('max_price', ['sku_price', 'sku_cost'])] - - for agent_id, agent_raw_state in state.items(): - result[agent_id] = [] - - for norm, fields in keys_in_state: - for field in fields: - vals = agent_raw_state[field] - - if not isinstance(vals, list): - vals = [vals] - if norm is not None: - vals = [ - max(0.0, min(100.0, x/(agent_raw_state[norm]+0.01))) for x in vals] - - result[agent_id].extend(vals) - result[agent_id] = np.array(result[agent_id]) - - return result - -class BalanceSheetCalculator: - consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") - seller_features = ("id", "sold", "demand", "price", "backlog_ratio") - manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - product_features = ("id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") - storage_features = ("capacity", "remaining_space") - vehicle_features= ("id", "payload", "unit_transport_cost") - - def __init__(self, env: Env): - self.env = env - self.consumer_ss = env.snapshot_list["consumer"] - self.seller_ss = env.snapshot_list["seller"] - self.manufacture_ss = env.snapshot_list["manufacture"] - self.storage_ss = env.snapshot_list["storage"] - self.distribution_ss = env.snapshot_list["distribution"] - self.vehicle_ss = env.snapshot_list["vehicle"] - self.product_ss = env.snapshot_list["product"] - self.products = [] - self.product_id2index_dict = {} - self.facility_levels = [] - - self.facilities = env.summary["node_mapping"]["facilities"] - - for facility_id, facility in self.facilities.items(): - pid_list = [] - distribution = facility["units"]["distribution"] - - for product_id, product in facility["units"]["products"].items(): - pid_list.append(product["id"]) - consumer = product["consumer"] - seller = product["seller"] - manufacture = product["manufacture"] - - self.product_id2index_dict[product["id"]] = len(self.products) - - self.products.append(( - product["id"], - product_id, - facility["units"]["storage"]["node_index"], - facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution["node_index"] if distribution is not None else None, - facility["downstreams"], - None if consumer is None else (consumer["id"], consumer["node_index"]), - None if seller is None else (seller["id"], seller["node_index"]), - None if manufacture is None else (manufacture["id"], manufacture["node_index"]), - )) - - self.facility_levels.append(( - facility_id, - pid_list, - facility["units"]["storage"]["node_index"], - facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution["node_index"] if distribution is not None else None, - [v["node_index"] for v in distribution["children"]] if distribution is not None else [] - )) - - self.total_balance_sheet = defaultdict(int) - - def calc(self): - tick = self.env.tick - # consumer - consumer_bs_states = self.consumer_ss[tick::self.consumer_features].flatten().reshape(-1, len(self.consumer_features)) - - # quantity * price - consumer_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] - - # balance_sheet_profit = 0 - # order_cost + order_product_cost - consumer_step_balance_sheet_loss = -1 * (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) - - # not sure about this. - reward_discount = 0 - - # consumer step reward: balance sheet los + profile * discount - consumer_step_reward = consumer_step_balance_sheet_loss + consumer_profit * reward_discount - - # seller - seller_bs_states = self.seller_ss[tick::self.seller_features].flatten().reshape(-1, len(self.seller_features)) - - # profit = sold * price - seller_balance_sheet_profit = seller_bs_states[:, 1] * seller_bs_states[:, 3] - - # loss = demand * price * backlog_ratio - seller_balance_sheet_loss = -1 * seller_bs_states[:, 2] * seller_bs_states[:, 3] * seller_bs_states[:, 4] - - # step reward = loss + profit - seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit - - # manufacture - man_bs_states = self.manufacture_ss[tick::self.manufacture_features].flatten().reshape(-1, len(self.manufacture_features)) - - # loss = manufacture number * cost - man_balance_sheet_profit_loss = -1 * man_bs_states[:, 1] * man_bs_states[:, 2] - - # step reward = loss - man_step_reward = man_balance_sheet_profit_loss - - # product - product_bs_states = self.product_ss[tick::self.product_features].flatten().reshape(-1, len(self.product_features)) - - # product distribution loss = check order + delay order penalty - product_distribution_balance_sheet_loss= -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) - - # product distribution profit = check order * price - product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] - - # result we need - product_step_reward = np.zeros((len(self.products,))) - product_balance_sheet_profit = np.zeros((len(self.products,))) - product_balance_sheet_loss = np.zeros((len(self.products, ))) - - # create product number mapping for storages - storages_product_map = {} - for storage_index in range(len(self.storage_ss)): - product_list = self.storage_ss[tick:storage_index:"product_list"].flatten().astype(np.int) - product_number = self.storage_ss[tick:storage_index:"product_number"].flatten().astype(np.int) - - storages_product_map[storage_index] = {pid:pnum for pid, pnum in zip(product_list, product_number)} - - # product balance sheet and reward - # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss - # profit = same as above - # reward = same as above - for i, product in enumerate(self.products): - id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = product - - if consumer: - product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer[1]] - product_step_reward[i] += consumer_step_reward[consumer[1]] - - if seller: - product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller[1]] - product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller[1]] - product_step_reward[i] += seller_step_reward[seller[1]] - - if manufacture: - product_balance_sheet_loss[i] += man_balance_sheet_profit_loss[manufacture[1]] - product_step_reward[i] += man_step_reward[manufacture[1]] - - storage_reward = -1 * storages_product_map[storage_index][product_id] * unit_storage_cost - - product_step_reward[i] += storage_reward - - product_balance_sheet_loss[i] += storage_reward - - if distribution_index is not None: - product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[distribution_index] - product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[distribution_index] - - product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + product_distribution_balance_sheet_profit[distribution_index] - - if downstreams and len(downstreams) > 0: - if product_id in downstreams: - for dfacility in downstreams[product_id]: - dproducts = self.facilities[dfacility]["units"]["products"] - - did = dproducts[product_id]["id"] - - product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] - product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] - product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] - - product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss - - # storage - storage_states = self.storage_ss[tick::self.storage_features].flatten().reshape(-1, len(self.storage_features)) - - # loss = (capacity-remaining space) * cost - storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) - - # vehicles - vehicle_states = self.vehicle_ss[tick::self.vehicle_features].flatten().reshape(-1, len(self.vehicle_features)) - - # loss = cost * payload - vehicle_balance_sheet_loss = -1 * vehicle_states[:, 1] * vehicle_states[:, 2] - vehicle_step_reward = vehicle_balance_sheet_loss - - facility_balance_sheet_loss = np.zeros((len(self.facility_levels),)) - facility_balance_sheet_profit = np.zeros((len(self.facility_levels),)) - facility_step_reward = np.zeros((len(self.facility_levels),)) - - # for facilities - for i, facility in enumerate(self.facility_levels): - id, pid_list, storage_index, unit_storage_cost, distribution_index, vehicle_indices = facility - - # storage balance sheet - # profit=0 - facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * unit_storage_cost - - # distribution balance sheet - if distribution_index is not None: - for vindex in vehicle_indices: - facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vindex] - # distribution unit do not provide reward - - # sku product unit balance sheet - for pid in pid_list: - facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] - facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] - facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] - - result = {} - - for id, bs, rw in zip([item[0] for item in self.products], product_balance_sheet, product_step_reward): - result[id] = (bs, rw) - - self.total_balance_sheet[id] += bs - - facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit - - for id, bs, rw in zip([item[0] for item in self.facility_levels], facility_balance_sheet, facility_step_reward): - result[id] = (bs, rw) - - self.total_balance_sheet[id] += bs - - return result - - -if __name__ == "__main__": - from time import time - - start_tick = 0 - durations = 100 - env = Env(scenario="supply_chain", topology="sample1", - start_tick=start_tick, durations=durations) - - ss = SCEnvWrapper(env) - env.step(None) - - states = ss.get_state(None) - - rewards = ss.get_reward(None) - - for id_, state in states["consumer"].items(): - print(id_, state.shape) - - for id_, state in states["producer"].items(): - print(id_, state.shape) - print(rewards) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 9edcc2e1f..1d75c38e3 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -1,16 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import numpy as np +import sys from maro.simulator import Env -from maro.simulator.scenarios.supply_chain import ConsumerAction -def main(): - start_tick = 0 +def main(topology_name: str): durations = 100 - env = Env(scenario="supply_chain", topology="sample1", start_tick=start_tick, durations=durations, max_snapshots=100) + + env = Env(scenario="supply_chain", topology=topology_name, durations=durations, max_snapshots=100) + total_episodes = 10 is_done = False @@ -28,4 +28,11 @@ def main(): if __name__ == "__main__": - main() + topology_name = "sample" + + if len(sys.argv) > 1: + topology_name = sys.argv[1] + + print("running topology:", topology_name) + + main(topology_name) diff --git a/examples/hello_world/supply_chain/outersample/config.yml b/examples/hello_world/supply_chain/outersample/config.yml new file mode 100644 index 000000000..a7e287db3 --- /dev/null +++ b/examples/hello_world/supply_chain/outersample/config.yml @@ -0,0 +1,208 @@ +facility_definitions: + WarehouseFacility: &warehouse_facility + class: "WarehouseFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + agent_type: 4 + consumer: + class: "ConsumerUnit" + config: + agent_type: 1 + + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + agent_type: 3 + consumer: + class: "ConsumerUnit" + manufacture: + class: "ManufactureUnit" + config: + agent_type: 0 + + RetailerFacility: &retailer_facility + class: "OutRetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "StoreProductUnit" + is_template: true + config: + agent_type: 5 + consumer: + class: "ConsumerUnit" + seller: + class: "OuterSellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: 2 + seller_sampler_type: data # data, model. What kind of sampler to use for seller. + sku_column: "SKU" # SKU column name + price_column: "Price" # Price column name + sale_column: "Sales" # Sales column name + datetime_column: "DT" # Datetime column name + file_path: "/path/to/data.csv" # full path to data file, override by each store instance + +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 100 + +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + vehicles: + - *normal_vehicle + - *normal_vehicle + config: + unit_price: 1 + +small_storage: &small_storage + config: + capacity: 10000 + unit_storage_cost: 1 + +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 12 + bom: + sku3: 10 + + - id: 2 + name: "sku2" + output_units_per_lot: 1 + + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + +world: + skus: *sku_definitions + + facilities: + - name: "Supplier_001" + definition_ref: "SupplierFacility" + skus: + sku3: + init_stock: 100 + product_unit_cost: 1 + production_rate: 1 + type: "production" + cost: 10 + price: 10 + vlt: 1 + children: + storage: *small_storage + distribution: *normal_distribution + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Warehouse_001" + definition_ref: "WarehouseFacility" + skus: + sku1: + init_stock: 1000 + price: 100 + vlt: 1 + sku2: + init_stock: 1000 + price: 100 + vlt: 1 + sku3: + init_stock: 1000 + price: 100 + vlt: 1 + children: + storage: *small_storage + distribution: *normal_distribution + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Retailer_001" + definition_ref: "RetailerFacility" + skus: + sku1: + price: 300 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 # optional + vlt: 1 + sku3: + price: 200 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + vlt: 1 + sku2: + price: 100 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + vlt: 1 + + children: + storage: *small_storage + + config: + order_cost: 0 + file_path: "store1.csv" # data file that contains demand of retailer 001 + topology: + Retailer_001: + sku3: + - "Supplier_001" + + grid: + size: [20, 20] + + facilities: + Supplier_001: [0, 0] + Warehouse_001: [6, 6] + Retailer_001: [10, 18] + + blocks: + railroad: + - [10, 10] + - [10, 11] + - [10, 12] + - [11, 12] + +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e7 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 + start_date_time: "2010-12-01" # start time for data in files diff --git a/examples/hello_world/supply_chain/pf_test.py b/examples/hello_world/supply_chain/pf_test.py deleted file mode 100644 index e28a0c01d..000000000 --- a/examples/hello_world/supply_chain/pf_test.py +++ /dev/null @@ -1,36 +0,0 @@ -import os, psutil - -import sys -import numpy as np -import pprint - -from tabulate import tabulate -from maro.simulator import Env -from maro.simulator.scenarios.supply_chain import ConsumerAction -from timeit import timeit - - -def go(env: Env): - env.reset() - - is_done = False - - while not is_done: - _, _, is_done = env.step(None) - - -if __name__ == "__main__": - topology = sys.argv[1] - durations = int(sys.argv[2] if len(sys.argv) > 2 else 100) - - env = Env(scenario="supply_chain", topology=topology, durations=durations) - - print(f"config: f{topology}, steps: {durations}") - - print("avg time cost: ", timeit(lambda : go(env), number=2)) - - process = psutil.Process(os.getpid()) - - memory = process.memory_info().rss/1024/1024 - - print("memory cost: ", memory) \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py index 71ba82a1b..e9e379710 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/__init__.py +++ b/maro/simulator/scenarios/supply_chain/facilities/__init__.py @@ -4,5 +4,6 @@ from .facility import FacilityBase from .retailer import RetailerFacility +from .outretailer import OutRetailerFacility from .supplier import SupplierFacility from .warehouse import WarehouseFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/outretailer.py b/maro/simulator/scenarios/supply_chain/facilities/outretailer.py new file mode 100644 index 000000000..22a8b0ae9 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/outretailer.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .retailer import RetailerFacility +from maro.simulator.scenarios.supply_chain.units import OuterSellerUnit, DataFileDemandSampler + + +sampler_mapping = { + "data": DataFileDemandSampler +} + + +class OutRetailerFacility(RetailerFacility): + def initialize(self): + super(OutRetailerFacility, self).initialize() + + # What kind of sampler we need? + sampler_cls = sampler_mapping[self.configs.get("seller_sampler_type", "data")] + + sampler = sampler_cls(self.configs, self.world) + + # Go though product to find sellers. + for product in self.products.values(): + seller = product.seller + + if seller is not None: + assert issubclass(type(seller), OuterSellerUnit) + + seller.sampler = sampler diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml index 6752cca6c..2f34f6458 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/core.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/core.yml @@ -47,6 +47,9 @@ units: SellerUnit: class: "SellerUnit" datamodel: "SellerDataModel" + OuterSellerUnit: + class: "OuterSellerUnit" + datamodel: "SellerDataModel" ManufactureUnit: class: "ManufactureUnit" datamodel: "ManufactureDataModel" @@ -73,3 +76,6 @@ facilities: RetailerFacility: class: "RetailerFacility" datamodel: "FacilityDataModel" + OutRetailerFacility: + class: "OutRetailerFacility" + datamodel: "FacilityDataModel" diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml new file mode 100644 index 000000000..f34f7ca3c --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml @@ -0,0 +1,297 @@ + +# TODO: which config to inherit +# base: "" + +#core: +# datamodels: "xxx" +# units: "xxx" +# facilities: "xxx" + + +facility_definitions: + # facility definition + WarehouseFacility: &warehouse_facility + class: "WarehouseFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + # if true then will call generate function of class type + is_template: true + # config will be passed to generator as parameters + config: + agent_type: 4 + consumer: + class: "ConsumerUnit" + config: + agent_type: 1 + + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + agent_type: 3 + consumer: + class: "ConsumerUnit" + manufacture: + class: "ManufactureUnit" + config: + agent_type: 0 + + RetailerFacility: &retailer_facility + class: "RetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "StoreProductUnit" + is_template: true + config: + agent_type: 5 + consumer: + class: "ConsumerUnit" + seller: + class: "SellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: 2 + +# common entity/unit definition as reference to simplify the file. +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 100 + +# a normal distribution definition +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + vehicles: + - *normal_vehicle + - *normal_vehicle + config: + unit_price: 1 + +small_storage: &small_storage + # config of data model of this unit + config: + # other config or storage unit + capacity: 10000 + unit_storage_cost: 1 + +midium_storage: &midium_storage + config: + capacity: 20000 + unit_storage_cost: 1 + +huge_storage: &huge_storage + config: + capacity: 30000 + unit_storage_cost: 1 + +# sku list in this world +# this list do not contains price, cost or other facility related attributes, +# but just base info, like name, id, bom +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 12 + # bill of material that used produce current sku, empty means do not need source material + bom: + # key is the source sku name, value is quantity needed to use per time to produce current sku + sku3: 10 + + - id: 2 + name: "sku2" + output_units_per_lot: 1 + + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + +# world definitions +world: + # here we use reference to make it each to edit. + skus: *sku_definitions + + # facilities in this world + facilities: + - name: "Supplier_001" # name of the facility + # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level + # use the facility definition as base, then we can override configs partially. + definition_ref: "SupplierFacility" + + # sku list of this facility + skus: + sku3: # sku name and attributes needed for this facility + init_stock: 100 + product_unit_cost: 1 + production_rate: 1 + type: "production" # production means this is the output production of this facility + cost: 10 + price: 10 + vlt: 1 + + # configuration of child units. + children: + # config of storage unit + storage: *small_storage + distribution: *normal_distribution + + # products use default config in core.yml + + # config of this facility + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Supplier_002" + definition_ref: "SupplierFacility" + + skus: + sku1: + init_stock: 100 + product_unit_cost: 1 + production_rate: 1 + type: "production" + cost: 10 + price: 100 + vlt: 1 + sku3: + init_stock: 100 + production_rate: 1 + type: "material" + cost: 10 + price: 100 + vlt: 1 + + children: + storage: *small_storage + distribution: *normal_distribution + + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Warehouse_001" + definition_ref: "WarehouseFacility" + + skus: + sku1: + init_stock: 1000 + price: 100 + vlt: 1 + sku2: + init_stock: 1000 + price: 100 + vlt: 1 + sku3: + init_stock: 1000 + price: 100 + vlt: 1 + + children: + storage: *huge_storage + distribution: *normal_distribution + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Retailer_001" + definition_ref: "RetailerFacility" + + skus: + sku1: + price: 300 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 # optional + vlt: 1 + sku3: + price: 200 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + vlt: 1 + sku2: + price: 100 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + vlt: 1 + + children: + storage: *midium_storage + + config: + order_cost: 0 + # topology used to specify the up/downstream for facilities + # we split it from facility, so that we can support configuration inherit to override it + # for a new topology + # TODO: change the name? + topology: + # key is current facility, value if upstream facilities that will provide a certain sku + Supplier_002: + # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", + # or any other facility in the list + sku3: + - "Supplier_001" + Warehouse_001: + sku1: + - "Supplier_002" + sku3: + - "Supplier_001" + Retailer_001: + sku1: + - "Supplier_002" + sku3: + - "Supplier_001" + + # map grid definitions + grid: + size: [20, 20] + + # facility position in grid + facilities: + Supplier_001: [0, 0] + Supplier_002: [3, 3] + Warehouse_001: [6, 6] + Retailer_001: [10, 18] + + # cells that un-traversable + blocks: + railroad: + - [10, 10] + - [10, 11] + - [10, 12] + - [11, 12] + +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e7 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index 8917fcd64..5f9170f58 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -8,6 +8,7 @@ from .simplemanufacture import SimpleManufactureUnit from .product import ProductUnit from .seller import SellerUnit +from .outerseller import OuterSellerUnit, SellerDemandSampler, DataFileDemandSampler from .skuunit import SkuUnit from .storage import StorageUnit from .storeproduct import StoreProductUnit diff --git a/maro/simulator/scenarios/supply_chain/units/outerseller.py b/maro/simulator/scenarios/supply_chain/units/outerseller.py new file mode 100644 index 000000000..79ac62989 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/outerseller.py @@ -0,0 +1,117 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import warnings + +from abc import ABC, abstractmethod +from collections import namedtuple +from csv import DictReader + +from dateutil import parser + +from .seller import SellerUnit + + +class SellerDemandSampler(ABC): + """Base class of seller unit demand sampler, you can inherit from this to read from file or predict from a model. + + Args: + configs (dict): Configuration from retailer facility, contains keys for a special sampler. + world (World): Current world this retail belongs to. + """ + def __init__(self, configs: dict, world): + self._configs = configs + self._world = world + + @abstractmethod + def sample_demand(self, product_id: int, tick: int) -> int: + """Sample the demand for specified product and tick. + + Args: + product_id (int): Id of product to sample. + tick (int): Tick of environment, NOTE: this tick is start from 0, you may need to transform it to your time system. + """ + pass + + +class DataFileDemandSampler(SellerDemandSampler): + """Sampler to read sample demand from data files, one store one file. + + NOTE: + This sampler need to configure the start time that to be treated as tick 0 in world.settings, or + it will use first row as start time. + + Args: + configs (dict): Configuration from retail facility, it should contains following keys. + . "file_path", the path to the data file + . "sku_column", column name contains sku name, this must be match with current seller, or will be ignored. + . "price_column", column name that will be treated as price. + . "sale_column", column name that will be treated as sale number (demand). + . "datetime_column", column name that contains datetime, NOTE: we will parse it that ignore the time zone. + """ + + SkuRow = namedtuple("SkuRow", ("price", "sales")) + + def __init__(self, configs: dict, world): + super(DataFileDemandSampler, self).__init__(configs, world) + + self._file_path = configs["file_path"] + + # If start date time is None, then will use first row as start date time (tick 0). + self._start_date_time = self._world.configs.settings["start_date_time"] + + if self._start_date_time is not None: + self._start_date_time = parser.parse(self._start_date_time, ignoretz=True) + + self._sku_column_name = configs.get("sku_column", "SKU") + self._price_column_name = configs.get("price_column", "Price") + self._sale_column_name = configs.get("sale_column", "Sales") + self._datetime_column_name = configs.get("datetime_column", "DT") + + # Tick -> sku -> (sale, price) + self._cache = {} + + self._cache_data() + + def sample_demand(self, product_id: int, tick: int) -> int: + if tick not in self._cache or product_id not in self._cache[tick]: + return 0 + + return self._cache[tick][product_id].sales + + def _cache_data(self): + with open(self._file_path, "rt") as fp: + reader = DictReader(fp) + + for row in reader: + sku_name = row[self._sku_column_name] + + sales = int(row[self._sale_column_name]) + price = float(row[self._price_column_name]) + date = parser.parse(row[self._datetime_column_name], ignoretz=True) + + if self._start_date_time is None: + self._start_date_time = date + + # So one day one tick. + target_tick = (date - self._start_date_time).days + + if target_tick not in self._cache: + self._cache[target_tick] = {} + + sku = self._world.get_sku_by_name(sku_name) + + if sku is not None: + self._cache[target_tick][sku.id] = DataFileDemandSampler.SkuRow(price, sales) + else: + warnings.warn(f"{sku_name} not configured in config file.") + + +class OuterSellerUnit(SellerUnit): + """Seller that demand is from out side sampler, like a data file or data model prediction.""" + + # Sample used to sample demand. + sampler: SellerDemandSampler = None + + def market_demand(self, tick: int) -> int: + return self.sampler.sample_demand(self.product_id, tick) diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index 23475aac3..17b431eee 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -37,6 +37,11 @@ def market_demand(self, tick: int) -> int: Returns: int: Demand number. """ + if len(self.demand_distribution) == 0: + # Generate demand distribution of this episode. + for _ in range(self.durations): + self.demand_distribution.append(int(np.random.gamma(self.gamma))) + return self.demand_distribution[tick] def initialize(self): @@ -49,10 +54,6 @@ def initialize(self): self.data_model.initialize(sku.price, sku.backlog_ratio) - # Generate demand distribution of this episode. - for _ in range(self.durations): - self.demand_distribution.append(int(np.random.gamma(self.gamma))) - self.sale_hist = [self.gamma] * self.config["sale_hist_len"] def step(self, tick: int): @@ -94,9 +95,6 @@ def reset(self): self.demand_distribution.clear() - for _ in range(self.durations): - self.demand_distribution.append(np.random.gamma(self.gamma)) - def sale_mean(self): return np.mean(self.sale_hist) From 35a9519c0b18d93e57bccb6842f20af5962fca7e Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 15:39:04 +0800 Subject: [PATCH 191/482] add missing sample data --- examples/hello_world/supply_chain/store1.csv | 481 +++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 examples/hello_world/supply_chain/store1.csv diff --git a/examples/hello_world/supply_chain/store1.csv b/examples/hello_world/supply_chain/store1.csv new file mode 100644 index 000000000..4e8add32d --- /dev/null +++ b/examples/hello_world/supply_chain/store1.csv @@ -0,0 +1,481 @@ +SKU,DT,Sales,Price,SaleMean +sku3,2011-07-04,32,430,24.578358208955223 +sku3,2011-02-07,24,430,24.578358208955223 +sku3,2011-02-10,12,430,24.578358208955223 +sku3,2011-02-11,13,430,24.578358208955223 +sku3,2011-02-13,2,430,24.578358208955223 +sku3,2011-02-14,38,430,24.578358208955223 +sku3,2011-02-15,117,430,24.578358208955223 +sku3,2011-02-16,13,430,24.578358208955223 +sku3,2011-02-17,12,430,24.578358208955223 +sku3,2011-02-18,25,430,24.578358208955223 +sku3,2011-02-20,14,430,24.578358208955223 +sku3,2011-02-21,42,430,24.578358208955223 +sku3,2011-02-22,51,430,24.578358208955223 +sku3,2011-02-23,2,430,24.578358208955223 +sku3,2011-02-24,47,430,24.578358208955223 +sku3,2011-02-25,12,430,24.578358208955223 +sku3,2011-02-27,6,430,24.578358208955223 +sku3,2011-02-08,3,430,24.578358208955223 +sku3,2011-02-04,13,430,24.578358208955223 +sku3,2011-03-01,4,430,24.578358208955223 +sku3,2011-02-03,12,430,24.578358208955223 +sku3,2011-01-16,5,430,24.578358208955223 +sku3,2011-01-17,1,430,24.578358208955223 +sku3,2011-01-19,26,430,24.578358208955223 +sku3,2011-01-20,193,430,24.578358208955223 +sku3,2011-01-21,2,430,24.578358208955223 +sku3,2011-01-23,12,430,24.578358208955223 +sku3,2011-01-24,72,430,24.578358208955223 +sku3,2011-01-25,15,430,24.578358208955223 +sku3,2011-01-26,5,430,24.578358208955223 +sku3,2011-01-27,15,430,24.578358208955223 +sku3,2011-01-28,36,430,24.578358208955223 +sku3,2011-01-30,6,430,24.578358208955223 +sku3,2011-01-31,27,430,24.578358208955223 +sku3,2011-02-01,3,430,24.578358208955223 +sku3,2011-02-02,39,430,24.578358208955223 +sku3,2011-02-28,2,430,24.578358208955223 +sku3,2011-03-02,12,430,24.578358208955223 +sku3,2011-07-05,4,430,24.578358208955223 +sku3,2011-03-25,19,430,24.578358208955223 +sku3,2011-03-28,41,430,24.578358208955223 +sku3,2011-03-29,74,430,24.578358208955223 +sku3,2011-03-30,6,430,24.578358208955223 +sku3,2011-03-31,37,430,24.578358208955223 +sku3,2011-04-01,22,430,24.578358208955223 +sku3,2011-04-03,2,430,24.578358208955223 +sku3,2011-04-05,21,430,24.578358208955223 +sku3,2011-04-07,39,430,24.578358208955223 +sku3,2011-04-08,26,430,24.578358208955223 +sku3,2011-04-10,7,430,24.578358208955223 +sku3,2011-04-11,50,430,24.578358208955223 +sku3,2011-04-12,29,430,24.578358208955223 +sku3,2011-04-13,17,430,24.578358208955223 +sku3,2011-04-14,21,430,24.578358208955223 +sku3,2011-04-15,36,430,24.578358208955223 +sku3,2011-03-27,53,430,24.578358208955223 +sku3,2011-03-24,17,430,24.578358208955223 +sku3,2011-03-03,16,430,24.578358208955223 +sku3,2011-03-23,15,430,24.578358208955223 +sku3,2011-03-04,15,430,24.578358208955223 +sku3,2011-03-06,7,430,24.578358208955223 +sku3,2011-03-07,6,430,24.578358208955223 +sku3,2011-03-09,51,430,24.578358208955223 +sku3,2011-03-10,40,430,24.578358208955223 +sku3,2011-03-11,96,430,24.578358208955223 +sku3,2011-03-13,8,430,24.578358208955223 +sku3,2011-03-14,107,430,24.578358208955223 +sku3,2011-03-15,4,430,24.578358208955223 +sku3,2011-03-16,27,430,24.578358208955223 +sku3,2011-03-17,42,430,24.578358208955223 +sku3,2011-03-18,114,430,24.578358208955223 +sku3,2011-03-20,7,430,24.578358208955223 +sku3,2011-03-21,26,430,24.578358208955223 +sku3,2011-03-22,17,430,24.578358208955223 +sku3,2011-01-14,1,430,24.578358208955223 +sku3,2011-01-13,12,430,24.578358208955223 +sku3,2011-01-12,18,430,24.578358208955223 +sku3,2011-07-01,1,430,24.578358208955223 +sku3,2011-06-29,4,430,24.578358208955223 +sku3,2011-06-28,24,430,24.578358208955223 +sku3,2011-06-26,3,430,24.578358208955223 +sku3,2011-06-23,14,430,24.578358208955223 +sku3,2011-06-22,6,430,24.578358208955223 +sku3,2011-06-21,1,430,24.578358208955223 +sku3,2011-06-20,10,430,24.578358208955223 +sku3,2011-06-19,11,430,24.578358208955223 +sku3,2011-06-17,25,430,24.578358208955223 +sku3,2011-06-16,5,430,24.578358208955223 +sku3,2011-06-15,5,430,24.578358208955223 +sku3,2011-06-14,6,430,24.578358208955223 +sku3,2011-06-13,24,430,24.578358208955223 +sku3,2011-06-10,12,430,24.578358208955223 +sku3,2011-06-09,15,430,24.578358208955223 +sku3,2011-06-30,33,430,24.578358208955223 +sku3,2010-12-01,1,430,24.578358208955223 +sku3,2011-01-11,3,430,24.578358208955223 +sku3,2010-12-03,9,430,24.578358208955223 +sku3,2011-07-06,12,430,24.578358208955223 +sku3,2011-07-07,73,430,24.578358208955223 +sku3,2011-01-06,29,430,24.578358208955223 +sku3,2011-07-03,1,430,24.578358208955223 +sku3,2011-01-04,12,430,24.578358208955223 +sku3,2010-12-20,13,430,24.578358208955223 +sku3,2010-12-17,14,430,24.578358208955223 +sku3,2010-12-16,12,430,24.578358208955223 +sku3,2010-12-13,2,430,24.578358208955223 +sku3,2010-12-12,9,430,24.578358208955223 +sku3,2010-12-10,24,430,24.578358208955223 +sku3,2010-12-09,25,430,24.578358208955223 +sku3,2010-12-07,2,430,24.578358208955223 +sku3,2010-12-06,3,430,24.578358208955223 +sku3,2010-12-05,1,430,24.578358208955223 +sku3,2011-06-08,233,430,24.578358208955223 +sku3,2011-06-07,7,430,24.578358208955223 +sku3,2011-06-06,12,430,24.578358208955223 +sku3,2011-06-05,109,430,24.578358208955223 +sku3,2011-05-11,2,430,24.578358208955223 +sku3,2011-05-10,50,430,24.578358208955223 +sku3,2011-05-09,21,430,24.578358208955223 +sku3,2011-05-08,43,430,24.578358208955223 +sku3,2011-05-06,28,430,24.578358208955223 +sku3,2011-05-05,26,430,24.578358208955223 +sku3,2011-05-04,12,430,24.578358208955223 +sku3,2011-05-03,31,430,24.578358208955223 +sku3,2011-04-28,24,430,24.578358208955223 +sku3,2011-04-26,18,430,24.578358208955223 +sku3,2011-04-21,8,430,24.578358208955223 +sku3,2011-04-20,36,430,24.578358208955223 +sku3,2011-04-19,65,430,24.578358208955223 +sku3,2011-01-07,54,430,24.578358208955223 +sku3,2011-01-10,7,430,24.578358208955223 +sku3,2011-05-12,123,430,24.578358208955223 +sku3,2011-05-13,17,430,24.578358208955223 +sku3,2011-05-15,2,430,24.578358208955223 +sku3,2011-05-25,31,430,24.578358208955223 +sku3,2011-06-03,15,430,24.578358208955223 +sku3,2011-06-02,24,430,24.578358208955223 +sku3,2011-06-01,35,430,24.578358208955223 +sku3,2011-05-31,5,430,24.578358208955223 +sku3,2011-05-29,6,430,24.578358208955223 +sku3,2011-05-27,12,430,24.578358208955223 +sku3,2011-05-24,28,430,24.578358208955223 +sku3,2011-05-16,13,430,24.578358208955223 +sku3,2011-05-23,15,430,24.578358208955223 +sku3,2011-05-22,14,430,24.578358208955223 +sku3,2011-05-20,14,430,24.578358208955223 +sku3,2011-05-19,56,430,24.578358208955223 +sku3,2011-05-18,13,430,24.578358208955223 +sku3,2011-05-17,12,430,24.578358208955223 +sku3,2011-01-05,7,430,24.578358208955223 +sku3,2011-04-04,13,430,24.578358208955223 +sku3,2011-11-27,5,430,24.578358208955223 +sku3,2011-10-09,2,430,24.578358208955223 +sku3,2011-10-25,17,430,24.578358208955223 +sku3,2011-10-24,2,430,24.578358208955223 +sku3,2011-10-23,7,430,24.578358208955223 +sku3,2011-10-21,36,430,24.578358208955223 +sku3,2011-10-20,14,430,24.578358208955223 +sku3,2011-10-19,9,430,24.578358208955223 +sku3,2011-10-18,6,430,24.578358208955223 +sku3,2011-10-17,30,430,24.578358208955223 +sku3,2011-10-14,2,430,24.578358208955223 +sku3,2011-10-13,13,430,24.578358208955223 +sku3,2011-10-12,2,430,24.578358208955223 +sku3,2011-10-11,25,430,24.578358208955223 +sku3,2011-10-10,26,430,24.578358208955223 +sku3,2011-10-07,14,430,24.578358208955223 +sku3,2011-09-18,30,430,24.578358208955223 +sku3,2011-10-06,39,430,24.578358208955223 +sku3,2011-10-05,108,430,24.578358208955223 +sku3,2011-10-04,13,430,24.578358208955223 +sku3,2011-10-03,3,430,24.578358208955223 +sku3,2011-09-29,24,430,24.578358208955223 +sku3,2011-09-28,18,430,24.578358208955223 +sku3,2011-09-27,38,430,24.578358208955223 +sku3,2011-09-26,2,430,24.578358208955223 +sku3,2011-09-25,7,430,24.578358208955223 +sku3,2011-09-23,14,430,24.578358208955223 +sku3,2011-09-22,29,430,24.578358208955223 +sku3,2011-09-21,115,430,24.578358208955223 +sku3,2011-09-20,1,430,24.578358208955223 +sku3,2011-10-26,24,430,24.578358208955223 +sku3,2011-10-28,96,430,24.578358208955223 +sku3,2011-10-30,7,430,24.578358208955223 +sku3,2011-10-31,8,430,24.578358208955223 +sku3,2011-12-09,13,430,24.578358208955223 +sku3,2011-12-08,6,430,24.578358208955223 +sku3,2011-12-07,99,430,24.578358208955223 +sku3,2011-12-06,2,430,24.578358208955223 +sku3,2011-12-05,12,430,24.578358208955223 +sku3,2011-12-04,13,430,24.578358208955223 +sku3,2011-12-02,19,430,24.578358208955223 +sku3,2011-12-01,16,430,24.578358208955223 +sku3,2011-11-29,16,430,24.578358208955223 +sku3,2011-11-28,27,430,24.578358208955223 +sku3,2011-11-25,2,430,24.578358208955223 +sku3,2011-11-24,50,430,24.578358208955223 +sku3,2011-11-23,23,430,24.578358208955223 +sku3,2011-11-22,2,430,24.578358208955223 +sku3,2011-11-21,3,430,24.578358208955223 +sku3,2011-11-20,3,430,24.578358208955223 +sku3,2011-11-17,14,430,24.578358208955223 +sku3,2011-11-16,5,430,24.578358208955223 +sku3,2011-11-15,1,430,24.578358208955223 +sku3,2011-11-13,27,430,24.578358208955223 +sku3,2011-11-11,18,430,24.578358208955223 +sku3,2011-11-10,12,430,24.578358208955223 +sku3,2011-11-08,16,430,24.578358208955223 +sku3,2011-11-06,17,430,24.578358208955223 +sku3,2011-11-04,30,430,24.578358208955223 +sku3,2011-11-03,4,430,24.578358208955223 +sku3,2011-11-01,14,430,24.578358208955223 +sku3,2011-09-19,12,430,24.578358208955223 +sku3,2011-04-17,3,430,24.578358208955223 +sku3,2011-09-16,24,430,24.578358208955223 +sku3,2011-07-24,14,430,24.578358208955223 +sku3,2011-08-05,12,430,24.578358208955223 +sku3,2011-08-04,43,430,24.578358208955223 +sku3,2011-08-03,26,430,24.578358208955223 +sku3,2011-08-02,136,430,24.578358208955223 +sku3,2011-08-01,12,430,24.578358208955223 +sku3,2011-07-31,99,430,24.578358208955223 +sku3,2011-07-29,12,430,24.578358208955223 +sku3,2011-07-28,36,430,24.578358208955223 +sku3,2011-07-27,121,430,24.578358208955223 +sku3,2011-07-26,24,430,24.578358208955223 +sku3,2011-07-25,28,430,24.578358208955223 +sku3,2011-07-22,9,430,24.578358208955223 +sku3,2011-08-09,96,430,24.578358208955223 +sku3,2011-07-21,12,430,24.578358208955223 +sku3,2011-07-20,21,430,24.578358208955223 +sku3,2011-07-19,36,430,24.578358208955223 +sku3,2011-07-18,39,430,24.578358208955223 +sku3,2011-07-17,13,430,24.578358208955223 +sku3,2011-07-13,14,430,24.578358208955223 +sku3,2011-07-12,54,430,24.578358208955223 +sku3,2011-07-11,55,430,24.578358208955223 +sku3,2011-07-10,3,430,24.578358208955223 +sku3,2011-07-08,3,430,24.578358208955223 +sku3,2011-09-15,8,430,24.578358208955223 +sku3,2011-08-08,12,430,24.578358208955223 +sku3,2011-04-18,10,430,24.578358208955223 +sku3,2011-08-10,20,430,24.578358208955223 +sku3,2011-08-30,27,430,24.578358208955223 +sku3,2011-08-11,3,430,24.578358208955223 +sku3,2011-09-13,111,430,24.578358208955223 +sku3,2011-09-12,1,430,24.578358208955223 +sku3,2011-09-11,15,430,24.578358208955223 +sku3,2011-09-14,37,430,24.578358208955223 +sku3,2011-09-09,13,430,24.578358208955223 +sku3,2011-09-08,16,430,24.578358208955223 +sku3,2011-09-07,2,430,24.578358208955223 +sku3,2011-09-04,20,430,24.578358208955223 +sku3,2011-09-02,19,430,24.578358208955223 +sku3,2011-09-01,30,430,24.578358208955223 +sku3,2011-09-05,26,430,24.578358208955223 +sku3,2011-08-28,4,430,24.578358208955223 +sku3,2011-08-21,22,430,24.578358208955223 +sku3,2011-08-12,26,430,24.578358208955223 +sku3,2011-08-26,36,430,24.578358208955223 +sku3,2011-08-15,16,430,24.578358208955223 +sku3,2011-08-17,12,430,24.578358208955223 +sku3,2011-08-18,13,430,24.578358208955223 +sku3,2011-08-14,12,430,24.578358208955223 +sku3,2011-08-22,12,430,24.578358208955223 +sku3,2011-08-23,42,430,24.578358208955223 +sku3,2011-08-24,2,430,24.578358208955223 +sku3,2011-08-25,13,430,24.578358208955223 +sku1,2011-12-02,19,220,19.22222222222222 +sku1,2011-12-01,2,220,19.22222222222222 +sku1,2011-12-06,7,220,19.22222222222222 +sku1,2011-12-07,62,220,19.22222222222222 +sku1,2011-12-08,13,220,19.22222222222222 +sku1,2011-11-30,22,220,19.22222222222222 +sku1,2011-11-27,9,220,19.22222222222222 +sku1,2011-12-05,21,220,19.22222222222222 +sku1,2011-11-29,18,220,19.22222222222222 +SKU5,2011-09-30,33,370,16.80263157894737 +SKU5,2011-10-02,14,370,16.80263157894737 +SKU5,2011-10-03,84,370,16.80263157894737 +SKU5,2011-10-04,10,370,16.80263157894737 +SKU5,2011-10-05,12,370,16.80263157894737 +SKU5,2011-10-06,14,370,16.80263157894737 +SKU5,2011-10-09,2,370,16.80263157894737 +SKU5,2011-10-13,6,370,16.80263157894737 +SKU5,2011-10-10,35,370,16.80263157894737 +SKU5,2011-10-11,25,370,16.80263157894737 +SKU5,2011-10-12,49,370,16.80263157894737 +SKU5,2011-09-28,9,370,16.80263157894737 +SKU5,2011-10-16,4,370,16.80263157894737 +SKU5,2011-10-17,7,370,16.80263157894737 +SKU5,2011-10-18,1,370,16.80263157894737 +SKU5,2011-10-19,6,370,16.80263157894737 +SKU5,2011-09-29,16,370,16.80263157894737 +SKU5,2011-09-23,15,370,16.80263157894737 +SKU5,2011-09-27,8,370,16.80263157894737 +SKU5,2011-09-26,30,370,16.80263157894737 +SKU5,2011-09-05,13,370,16.80263157894737 +SKU5,2011-09-06,3,370,16.80263157894737 +SKU5,2011-09-07,55,370,16.80263157894737 +SKU5,2011-09-08,10,370,16.80263157894737 +SKU5,2011-09-09,6,370,16.80263157894737 +SKU5,2011-09-11,31,370,16.80263157894737 +SKU5,2011-09-12,66,370,16.80263157894737 +SKU5,2011-09-15,6,370,16.80263157894737 +SKU5,2011-09-16,14,370,16.80263157894737 +SKU5,2011-09-19,13,370,16.80263157894737 +SKU5,2011-09-20,18,370,16.80263157894737 +SKU5,2011-09-21,6,370,16.80263157894737 +SKU5,2011-09-22,6,370,16.80263157894737 +SKU5,2011-10-21,14,370,16.80263157894737 +SKU5,2011-09-25,7,370,16.80263157894737 +SKU5,2011-10-20,10,370,16.80263157894737 +SKU5,2011-10-27,6,370,16.80263157894737 +SKU5,2011-10-24,8,370,16.80263157894737 +SKU5,2011-11-28,5,370,16.80263157894737 +SKU5,2011-11-18,15,370,16.80263157894737 +SKU5,2011-11-20,49,370,16.80263157894737 +SKU5,2011-11-22,3,370,16.80263157894737 +SKU5,2011-11-23,17,370,16.80263157894737 +SKU5,2011-11-24,1,370,16.80263157894737 +SKU5,2011-11-25,7,370,16.80263157894737 +SKU5,2011-11-27,9,370,16.80263157894737 +SKU5,2011-11-29,10,370,16.80263157894737 +SKU5,2011-10-25,55,370,16.80263157894737 +SKU5,2011-11-30,38,370,16.80263157894737 +SKU5,2011-12-02,13,370,16.80263157894737 +SKU5,2011-12-04,5,370,16.80263157894737 +SKU5,2011-12-05,22,370,16.80263157894737 +SKU5,2011-12-06,3,370,16.80263157894737 +SKU5,2011-12-08,20,370,16.80263157894737 +SKU5,2011-12-09,1,370,16.80263157894737 +SKU5,2011-11-17,2,370,16.80263157894737 +SKU5,2011-11-16,6,370,16.80263157894737 +SKU5,2011-11-15,4,370,16.80263157894737 +SKU5,2011-11-14,14,370,16.80263157894737 +SKU5,2011-10-26,6,370,16.80263157894737 +SKU5,2011-10-28,3,370,16.80263157894737 +SKU5,2011-10-30,9,370,16.80263157894737 +SKU5,2011-10-31,11,370,16.80263157894737 +SKU5,2011-11-01,4,370,16.80263157894737 +SKU5,2011-11-02,6,370,16.80263157894737 +SKU5,2011-11-03,12,370,16.80263157894737 +SKU5,2011-11-04,144,370,16.80263157894737 +SKU5,2011-11-06,16,370,16.80263157894737 +SKU5,2011-11-07,19,370,16.80263157894737 +SKU5,2011-11-08,7,370,16.80263157894737 +SKU5,2011-11-09,24,370,16.80263157894737 +SKU5,2011-11-10,8,370,16.80263157894737 +SKU5,2011-11-11,4,370,16.80263157894737 +SKU5,2011-11-13,10,370,16.80263157894737 +SKU5,2011-09-18,17,370,16.80263157894737 +SKU5,2011-12-07,6,370,16.80263157894737 +SKU4,2011-09-15,9,560,16.392857142857142 +SKU4,2011-07-22,10,560,16.392857142857142 +SKU4,2011-09-07,2,560,16.392857142857142 +SKU4,2011-09-05,6,560,16.392857142857142 +SKU4,2011-08-30,3,560,16.392857142857142 +SKU4,2011-08-10,1,560,16.392857142857142 +SKU4,2011-08-08,25,560,16.392857142857142 +SKU4,2011-08-04,1,560,16.392857142857142 +SKU4,2011-08-02,1,560,16.392857142857142 +SKU4,2011-07-25,4,560,16.392857142857142 +SKU4,2011-07-12,2,560,16.392857142857142 +SKU4,2011-07-20,302,560,16.392857142857142 +SKU4,2011-06-17,12,560,16.392857142857142 +SKU4,2011-06-23,7,560,16.392857142857142 +SKU4,2011-06-27,13,560,16.392857142857142 +SKU4,2011-06-29,1,560,16.392857142857142 +SKU4,2011-07-01,12,560,16.392857142857142 +SKU4,2011-07-04,24,560,16.392857142857142 +SKU4,2011-07-05,2,560,16.392857142857142 +SKU4,2011-09-09,2,560,16.392857142857142 +SKU4,2011-07-06,4,560,16.392857142857142 +SKU4,2011-09-27,3,560,16.392857142857142 +SKU4,2011-10-11,1,560,16.392857142857142 +SKU4,2011-11-03,1,560,16.392857142857142 +SKU4,2011-11-27,3,560,16.392857142857142 +SKU4,2011-11-29,1,560,16.392857142857142 +SKU4,2011-07-07,6,560,16.392857142857142 +SKU4,2011-09-28,1,560,16.392857142857142 +sku2,2011-11-27,4,350,14.121212121212121 +sku2,2011-11-18,56,350,14.121212121212121 +sku2,2011-11-20,33,350,14.121212121212121 +sku2,2011-11-22,1,350,14.121212121212121 +sku2,2011-11-23,11,350,14.121212121212121 +sku2,2011-11-24,1,350,14.121212121212121 +sku2,2011-11-25,1,350,14.121212121212121 +sku2,2011-12-02,31,350,14.121212121212121 +sku2,2011-11-28,7,350,14.121212121212121 +sku2,2011-11-29,1,350,14.121212121212121 +sku2,2011-12-01,6,350,14.121212121212121 +sku2,2011-12-04,7,350,14.121212121212121 +sku2,2011-12-05,13,350,14.121212121212121 +sku2,2011-12-08,4,350,14.121212121212121 +sku2,2011-11-14,2,350,14.121212121212121 +sku2,2011-11-15,2,350,14.121212121212121 +sku2,2011-08-11,31,350,14.121212121212121 +sku2,2011-11-11,32,350,14.121212121212121 +sku2,2011-03-07,8,350,14.121212121212121 +sku2,2011-03-17,56,350,14.121212121212121 +sku2,2011-03-22,28,350,14.121212121212121 +sku2,2011-03-28,1,350,14.121212121212121 +sku2,2011-03-30,1,350,14.121212121212121 +sku2,2011-11-08,4,350,14.121212121212121 +sku2,2011-04-06,6,350,14.121212121212121 +sku2,2011-04-07,1,350,14.121212121212121 +sku2,2011-04-11,28,350,14.121212121212121 +sku2,2011-04-12,28,350,14.121212121212121 +sku2,2011-04-15,1,350,14.121212121212121 +sku2,2011-04-18,2,350,14.121212121212121 +sku2,2011-04-26,28,350,14.121212121212121 +sku2,2011-04-28,30,350,14.121212121212121 +sku2,2011-05-09,28,350,14.121212121212121 +sku2,2011-05-11,28,350,14.121212121212121 +sku2,2011-05-12,28,350,14.121212121212121 +sku2,2011-05-17,3,350,14.121212121212121 +sku2,2011-03-10,1,350,14.121212121212121 +sku2,2011-02-28,1,350,14.121212121212121 +sku2,2011-06-03,6,350,14.121212121212121 +sku2,2011-02-24,1,350,14.121212121212121 +sku2,2010-12-01,34,350,14.121212121212121 +sku2,2010-12-03,1,350,14.121212121212121 +sku2,2010-12-05,28,350,14.121212121212121 +sku2,2010-12-06,5,350,14.121212121212121 +sku2,2010-12-07,5,350,14.121212121212121 +sku2,2010-12-09,3,350,14.121212121212121 +sku2,2010-12-10,2,350,14.121212121212121 +sku2,2010-12-17,7,350,14.121212121212121 +sku2,2010-12-23,1,350,14.121212121212121 +sku2,2011-01-10,28,350,14.121212121212121 +sku2,2011-01-12,1,350,14.121212121212121 +sku2,2011-01-20,1,350,14.121212121212121 +sku2,2011-01-31,28,350,14.121212121212121 +sku2,2011-02-02,4,350,14.121212121212121 +sku2,2011-02-07,1,350,14.121212121212121 +sku2,2011-02-14,19,350,14.121212121212121 +sku2,2011-02-18,28,350,14.121212121212121 +sku2,2011-05-25,28,350,14.121212121212121 +sku2,2011-04-04,56,350,14.121212121212121 +sku2,2011-06-07,1,350,14.121212121212121 +sku2,2011-08-24,28,350,14.121212121212121 +sku2,2011-08-31,2,350,14.121212121212121 +sku2,2011-09-19,28,350,14.121212121212121 +sku2,2011-09-21,3,350,14.121212121212121 +sku2,2011-09-26,2,350,14.121212121212121 +sku2,2011-06-13,6,350,14.121212121212121 +sku2,2011-09-29,28,350,14.121212121212121 +sku2,2011-10-03,1,350,14.121212121212121 +sku2,2011-10-05,56,350,14.121212121212121 +sku2,2011-10-09,6,350,14.121212121212121 +sku2,2011-10-10,2,350,14.121212121212121 +sku2,2011-10-14,2,350,14.121212121212121 +sku2,2011-10-17,3,350,14.121212121212121 +sku2,2011-10-19,1,350,14.121212121212121 +sku2,2011-10-24,3,350,14.121212121212121 +sku2,2011-10-25,14,350,14.121212121212121 +sku2,2011-10-27,1,350,14.121212121212121 +sku2,2011-10-30,3,350,14.121212121212121 +sku2,2011-10-31,7,350,14.121212121212121 +sku2,2011-11-02,2,350,14.121212121212121 +sku2,2011-11-03,1,350,14.121212121212121 +sku2,2011-11-04,20,350,14.121212121212121 +sku2,2011-08-25,28,350,14.121212121212121 +sku2,2011-09-27,28,350,14.121212121212121 +sku2,2011-08-17,28,350,14.121212121212121 +sku2,2011-08-04,28,350,14.121212121212121 +sku2,2011-07-14,18,350,14.121212121212121 +sku2,2011-08-05,1,350,14.121212121212121 +sku2,2011-07-07,10,350,14.121212121212121 +sku2,2011-07-04,1,350,14.121212121212121 +sku2,2011-06-29,4,350,14.121212121212121 +sku2,2011-06-24,6,350,14.121212121212121 +sku2,2011-06-20,2,350,14.121212121212121 +sku2,2011-06-17,3,350,14.121212121212121 +sku2,2011-07-22,1,350,14.121212121212121 +sku2,2011-07-21,113,350,14.121212121212121 +sku2,2011-07-31,28,350,14.121212121212121 +sku2,2011-07-28,28,350,14.121212121212121 +sku2,2011-07-15,10,350,14.121212121212121 From 58f4ed591e2979953839d37596024015cd8adb0c Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 15:43:59 +0800 Subject: [PATCH 192/482] rename --- .../supply_chain/outersample/config.yml | 2 +- examples/supply_chain/envs/sample1/config.yml | 30 +++++++++++++++++++ .../supply_chain/facilities/__init__.py | 2 +- .../{outretailer.py => outerretailer.py} | 4 +-- .../supply_chain/topologies/core.yml | 4 +-- 5 files changed, 36 insertions(+), 6 deletions(-) rename maro/simulator/scenarios/supply_chain/facilities/{outretailer.py => outerretailer.py} (87%) diff --git a/examples/hello_world/supply_chain/outersample/config.yml b/examples/hello_world/supply_chain/outersample/config.yml index a7e287db3..5fff828d4 100644 --- a/examples/hello_world/supply_chain/outersample/config.yml +++ b/examples/hello_world/supply_chain/outersample/config.yml @@ -36,7 +36,7 @@ facility_definitions: agent_type: 0 RetailerFacility: &retailer_facility - class: "OutRetailerFacility" + class: "OuterRetailerFacility" children: storage: class: "StorageUnit" diff --git a/examples/supply_chain/envs/sample1/config.yml b/examples/supply_chain/envs/sample1/config.yml index f34f7ca3c..eadec95f9 100644 --- a/examples/supply_chain/envs/sample1/config.yml +++ b/examples/supply_chain/envs/sample1/config.yml @@ -67,6 +67,35 @@ facility_definitions: config: agent_type: 2 + + # definition for outer retailer that read demand from csv files. + # NOTE: make sure you provide a valid file path for each retailer instance. + OuterRetailerFacility: &outerretailer_facility + class: "OuterRetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "StoreProductUnit" + is_template: true + config: + agent_type: 5 + consumer: + class: "ConsumerUnit" + seller: + class: "OuterSellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: 2 + seller_sampler_type: data # data, model. What kind of sampler to use for seller. + sku_column: "SKU" # SKU column name + price_column: "Price" # Price column name + sale_column: "Sales" # Sales column name + datetime_column: "DT" # Datetime column name + file_path: "/path/to/data.csv" # full path to data file, override by each store instance + + # common entity/unit definition as reference to simplify the file. normal_vehicle: &normal_vehicle class: "VehicleUnit" @@ -295,3 +324,4 @@ settings: gamma: 0.99 tail_timesteps: 7 heading_timesteps: 7 + start_date_time: "2010-12-01" # start time for data in files, for outer retailer diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py index e9e379710..f4e3ef1f3 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/__init__.py +++ b/maro/simulator/scenarios/supply_chain/facilities/__init__.py @@ -4,6 +4,6 @@ from .facility import FacilityBase from .retailer import RetailerFacility -from .outretailer import OutRetailerFacility +from .outerretailer import OuterRetailerFacility from .supplier import SupplierFacility from .warehouse import WarehouseFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/outretailer.py b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py similarity index 87% rename from maro/simulator/scenarios/supply_chain/facilities/outretailer.py rename to maro/simulator/scenarios/supply_chain/facilities/outerretailer.py index 22a8b0ae9..300b7158f 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/outretailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py @@ -11,9 +11,9 @@ } -class OutRetailerFacility(RetailerFacility): +class OuterRetailerFacility(RetailerFacility): def initialize(self): - super(OutRetailerFacility, self).initialize() + super(OuterRetailerFacility, self).initialize() # What kind of sampler we need? sampler_cls = sampler_mapping[self.configs.get("seller_sampler_type", "data")] diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml index 2f34f6458..f650719a2 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/core.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/core.yml @@ -76,6 +76,6 @@ facilities: RetailerFacility: class: "RetailerFacility" datamodel: "FacilityDataModel" - OutRetailerFacility: - class: "OutRetailerFacility" + OuterRetailerFacility: + class: "OuterRetailerFacility" datamodel: "FacilityDataModel" From 0017fa7853dc735ff3325e41a756a2ca9116808b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 15:45:14 +0800 Subject: [PATCH 193/482] comment about how to switch topology --- examples/hello_world/supply_chain/hello.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index 1d75c38e3..bd688879d 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -31,6 +31,7 @@ def main(topology_name: str): topology_name = "sample" if len(sys.argv) > 1: + # run: "python hello.py outersample" to run outer retailer sample. topology_name = sys.argv[1] print("running topology:", topology_name) From 6146698442423beb11a1a7ecd320ab6e3dbca51f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 15:48:57 +0800 Subject: [PATCH 194/482] formatting --- .../scenarios/supply_chain/facilities/outerretailer.py | 10 ++++++++-- .../scenarios/supply_chain/units/outerseller.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py index 300b7158f..89ea90e19 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py @@ -2,16 +2,22 @@ # Licensed under the MIT license. -from .retailer import RetailerFacility from maro.simulator.scenarios.supply_chain.units import OuterSellerUnit, DataFileDemandSampler +from .retailer import RetailerFacility - +# Mapping for supported sampler. sampler_mapping = { "data": DataFileDemandSampler } class OuterRetailerFacility(RetailerFacility): + """Retailer (store) facility that use outer data as seller demand. + + NOTE: + This require that all product seller is subclass of OuterSellerUnit. + """ + def initialize(self): super(OuterRetailerFacility, self).initialize() diff --git a/maro/simulator/scenarios/supply_chain/units/outerseller.py b/maro/simulator/scenarios/supply_chain/units/outerseller.py index 79ac62989..2e245b897 100644 --- a/maro/simulator/scenarios/supply_chain/units/outerseller.py +++ b/maro/simulator/scenarios/supply_chain/units/outerseller.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. import warnings - from abc import ABC, abstractmethod from collections import namedtuple from csv import DictReader @@ -19,6 +18,7 @@ class SellerDemandSampler(ABC): configs (dict): Configuration from retailer facility, contains keys for a special sampler. world (World): Current world this retail belongs to. """ + def __init__(self, configs: dict, world): self._configs = configs self._world = world @@ -68,7 +68,7 @@ def __init__(self, configs: dict, world): self._sale_column_name = configs.get("sale_column", "Sales") self._datetime_column_name = configs.get("datetime_column", "DT") - # Tick -> sku -> (sale, price) + # Tick -> sku -> (sale, price). self._cache = {} self._cache_data() From 0d3aec0259d6d93f4c776e039e34a3ae1cb703ab Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 16:36:12 +0800 Subject: [PATCH 195/482] remove gamma distribution demand cache, as we reset it each episode, add test for outer seller and retailer, fix failed tests --- examples/hello_world/supply_chain/hello.py | 2 +- .../scenarios/supply_chain/units/seller.py | 12 +-- .../simulator/scenarios/supply_chain/world.py | 4 +- tests/supply_chain/test_supply_chain.py | 76 ++++++++++++++++--- 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py index bd688879d..73f7441a4 100644 --- a/examples/hello_world/supply_chain/hello.py +++ b/examples/hello_world/supply_chain/hello.py @@ -31,7 +31,7 @@ def main(topology_name: str): topology_name = "sample" if len(sys.argv) > 1: - # run: "python hello.py outersample" to run outer retailer sample. + # run: "python hello.py outersample" under current folder to run outer retailer sample. topology_name = sys.argv[1] print("running topology:", topology_name) diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index 17b431eee..d2323cdcb 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -16,8 +16,6 @@ def __init__(self): super(SellerUnit, self).__init__() self.gamma = 0 - self.durations = 0 - self.demand_distribution = [] # Attribute cache. self.sold = 0 @@ -37,12 +35,7 @@ def market_demand(self, tick: int) -> int: Returns: int: Demand number. """ - if len(self.demand_distribution) == 0: - # Generate demand distribution of this episode. - for _ in range(self.durations): - self.demand_distribution.append(int(np.random.gamma(self.gamma))) - - return self.demand_distribution[tick] + return int(np.random.gamma(self.gamma)) def initialize(self): super(SellerUnit, self).initialize() @@ -50,7 +43,6 @@ def initialize(self): sku = self.facility.skus[self.product_id] self.gamma = sku.sale_gamma - self.durations = self.world.durations self.data_model.initialize(sku.price, sku.backlog_ratio) @@ -93,8 +85,6 @@ def post_step(self, tick: int): def reset(self): super(SellerUnit, self).reset() - self.demand_distribution.clear() - def sale_mean(self): return np.mean(self.sale_hist) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 2dc93064e..43ea116d3 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -12,7 +12,7 @@ from .facilities import FacilityBase from .frame_builder import build_frame from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef -from .units import UnitBase, ProductUnit +from .units import UnitBase, ProductUnit, SkuUnit AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id")) @@ -393,7 +393,7 @@ def get_node_mapping(self): for unit_id, unit in self.units.items(): sku = None - if isinstance(unit, ProductUnit): + if isinstance(unit, SkuUnit): sku = unit.facility.skus[unit.product_id] if unit.data_model is not None: diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index bec62fb88..796d622c2 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -707,9 +707,9 @@ def test_consumer_action(self): sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] # zero quantity will be ignore - action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 0, 1) + action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 0, 1, 0) - action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 10, 1) + action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 10, 1, 0) sku3_consumer_unit.set_action(action_with_zero) @@ -805,7 +805,7 @@ def test_consumer_on_order_reception(self): sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] - action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1) + action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1, 0) # 1st step must none action env.step(None) @@ -1233,8 +1233,7 @@ def test_seller_unit_initial_states(self): # from configuration self.assertEqual(10, sell_unit.gamma) - self.assertEqual(100, sell_unit.durations) - self.assertEqual(100, len(sell_unit.demand_distribution)) + self.assertEqual(0, sell_unit.sold) self.assertEqual(0, sell_unit.demand) self.assertEqual(0, sell_unit.total_sold) @@ -1250,8 +1249,6 @@ def test_seller_unit_initial_states(self): # from configuration self.assertEqual(10, sell_unit.gamma) - self.assertEqual(100, sell_unit.durations) - self.assertEqual(100, len(sell_unit.demand_distribution)) self.assertEqual(0, sell_unit.sold) self.assertEqual(0, sell_unit.demand) self.assertEqual(0, sell_unit.total_sold) @@ -1285,10 +1282,9 @@ def test_seller_unit_demand_states(self): # seller unit will try to count down the product number base on demand # default seller use gamma distribution on each tick - demand = sell_unit.demand_distribution[0] + demand = sell_unit.demand # demand should be same with original - self.assertEqual(demand, sell_unit.demand) self.assertEqual(demand, sell_unit.data_model.demand) actual_sold = min(demand, SKU3_INIT_NUMBER) @@ -1308,10 +1304,9 @@ def test_seller_unit_demand_states(self): # move to next step to check if state is correct env.step(None) - demand = sell_unit.demand_distribution[1] + demand = sell_unit.demand # demand should be same with original - self.assertEqual(demand, sell_unit.demand) self.assertEqual(demand, sell_unit.data_model.demand) actual_sold_2 = min(demand, SKU3_INIT_NUMBER - actual_sold) @@ -1384,6 +1379,65 @@ def test_seller_unit_customized(self): # total sold will keep same after tick 4 self.assertListEqual([0, 1, 3, 6, 10] + [10] * 95, list(total_sold_states)) + def test_outer_seller(self): + env = build_env("case_04", 100) + + index2unitid_mapping = {} + skuid2index_mapping = {} + + # find all the sellers + for unit_id, unit_detail in env.summary["node_mapping"]["unit_mapping"].items(): + if unit_detail[0] == "seller": + index = unit_detail[1] + sku = unit_detail[3] + index2unitid_mapping[index] = unit_id + skuid2index_mapping[sku.id] = index + + # tick 0 + env.step(None) + + seller_states = env.snapshot_list["seller"][0::"demand"].flatten().astype(np.int) + + # at tick 0 (2010-12-01) + # sku1 have 10 demand + self.assertEqual(10, seller_states[skuid2index_mapping[SKU1_ID]]) + + # sku2 have 20 demand + self.assertEqual(20, seller_states[skuid2index_mapping[SKU2_ID]]) + + # sku3 have 30 demand + self.assertEqual(30, seller_states[skuid2index_mapping[SKU3_ID]]) + + # tick 1 + env.step(None) + + seller_states = env.snapshot_list["seller"][1::"demand"].flatten().astype(np.int) + + # at tick 1 (2010-12-02) + # sku1 have 0 demand (no record in file) + self.assertEqual(0, seller_states[skuid2index_mapping[SKU1_ID]]) + + # sku2 have 20 demand + self.assertEqual(21, seller_states[skuid2index_mapping[SKU2_ID]]) + + # sku3 have 30 demand + self.assertEqual(31, seller_states[skuid2index_mapping[SKU3_ID]]) + + # tick 2 + env.step(None) + + seller_states = env.snapshot_list["seller"][2::"demand"].flatten().astype(np.int) + + # at tick 2 (2010-12-03) + # sku1 have 13 demand + self.assertEqual(13, seller_states[skuid2index_mapping[SKU1_ID]]) + + # sku2 have 20 demand + self.assertEqual(22, seller_states[skuid2index_mapping[SKU2_ID]]) + + # sku3 have 30 demand + self.assertEqual(0, seller_states[skuid2index_mapping[SKU3_ID]]) + if __name__ == '__main__': unittest.main() From 16d102534b25cb035b20640d6abcc8c4dfa1d748 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 16:54:09 +0800 Subject: [PATCH 196/482] add test for simple manufacture --- tests/supply_chain/test_supply_chain.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 796d622c2..1260a0941 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -1385,6 +1385,9 @@ def test_outer_seller(self): index2unitid_mapping = {} skuid2index_mapping = {} + # we only have one manufacture here + man_id = None + # find all the sellers for unit_id, unit_detail in env.summary["node_mapping"]["unit_mapping"].items(): if unit_detail[0] == "seller": @@ -1393,6 +1396,9 @@ def test_outer_seller(self): index2unitid_mapping[index] = unit_id skuid2index_mapping[sku.id] = index + if unit_detail[0] == "manufacture": + man_id = unit_id + # tick 0 env.step(None) @@ -1438,6 +1444,16 @@ def test_outer_seller(self): # sku3 have 30 demand self.assertEqual(0, seller_states[skuid2index_mapping[SKU3_ID]]) + # test if simple manufacture work correctly. + + action = ManufactureAction(man_id, 10) + + env.step({man_id: action}) + + man_states = env.snapshot_list["manufacture"][env.tick::"manufacturing_number"].flatten().astype(np.int) + + self.assertEqual(action.production_rate, man_states[0]) + if __name__ == '__main__': unittest.main() From 84da8ceb5363e517884a6963016eca5cb1164faf Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 17:33:25 +0800 Subject: [PATCH 197/482] linting fix --- maro/rl/__init__.py | 2 +- maro/rl/agent/abs_agent.py | 6 +++--- maro/rl/agent/ac.py | 15 +++++++-------- maro/rl/agent/ddpg.py | 10 +++++----- maro/rl/agent/dqn.py | 15 +++++++-------- maro/rl/agent/multi_agent_wrapper.py | 8 ++++---- maro/rl/agent/pg.py | 10 +++++----- maro/rl/distributed/actor.py | 4 ++-- maro/rl/distributed/actor_manager.py | 2 +- maro/rl/distributed/dispatcher.py | 4 ++-- maro/rl/distributed/learner.py | 2 +- maro/rl/distributed/trainer.py | 2 +- maro/rl/storage/abs_store.py | 3 ++- maro/rl/training/env_wrapper.py | 2 +- .../scenarios/supply_chain/business_engine.py | 10 ++++++---- .../supply_chain/datamodels/__init__.py | 2 +- .../supply_chain/datamodels/consumer.py | 2 +- .../supply_chain/datamodels/facility.py | 3 +-- .../supply_chain/datamodels/manufacture.py | 2 +- .../supply_chain/datamodels/seller.py | 2 +- .../supply_chain/datamodels/storage.py | 12 +++++++----- .../supply_chain/facilities/__init__.py | 2 +- .../supply_chain/facilities/facility.py | 18 ++++++++++++------ .../supply_chain/facilities/outerretailer.py | 4 +++- .../simulator/scenarios/supply_chain/parser.py | 7 ++++++- .../scenarios/supply_chain/units/__init__.py | 5 +++-- .../supply_chain/units/outerseller.py | 3 ++- .../supply_chain/units/simplemanufacture.py | 2 +- maro/simulator/scenarios/supply_chain/world.py | 6 ++++-- 29 files changed, 92 insertions(+), 73 deletions(-) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index d3069a4d8..aedb7b6fe 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -5,7 +5,7 @@ DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient, PolicyGradientConfig ) -from maro.rl.distributed import Actor, ActorManager, DistLearner +from maro.rl.distributed import Actor, ActorManager, DistLearner from maro.rl.exploration import ( AbsExplorer, EpsilonGreedyExplorer, GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer ) diff --git a/maro/rl/agent/abs_agent.py b/maro/rl/agent/abs_agent.py index 192ec65b5..83eeca9c3 100644 --- a/maro/rl/agent/abs_agent.py +++ b/maro/rl/agent/abs_agent.py @@ -6,7 +6,7 @@ import torch from maro.rl.model import AbsCoreModel -from maro.rl.storage import SimpleStore +from maro.rl.storage import SimpleStore class AbsAgent(ABC): @@ -29,8 +29,8 @@ class AbsAgent(ABC): to ``step``. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. - Defaults to 1. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for + training. Defaults to 1. """ def __init__( self, diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py index 5948389fa..bfaaf53d0 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/agent/ac.py @@ -1,12 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, Tuple +from typing import Tuple import numpy as np import torch from torch.distributions import Categorical -from torch.nn import MSELoss from maro.rl.model import SimpleMultiHeadModel from maro.rl.utils import get_log_prob, get_torch_loss_cls @@ -64,8 +63,8 @@ class ActorCritic(AbsAgent): to ``step``. Defaults to True. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. - Defaults to 1. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for + training. Defaults to 1. """ def __init__( self, @@ -75,7 +74,7 @@ def __init__( experience_memory_overwrite_type: str, empty_experience_memory_after_step: bool = True, min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 + min_experiences_to_trigger_learning: int = 1 ): if model.task_names is None or set(model.task_names) != {"actor", "critic"}: raise UnrecognizedTask(f"Expected model task names 'actor' and 'critic', but got {model.task_names}") @@ -83,7 +82,7 @@ def __init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning + min_experiences_to_trigger_learning=min_experiences_to_trigger_learning ) def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: @@ -105,7 +104,7 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: log_p = action_prob.log_prob(action) action, log_p = action.cpu().numpy(), log_p.cpu().numpy() return (action[0], log_p[0]) if is_single else (action, log_p) - + def step(self): batch = self.experience_memory.get() states = torch.from_numpy(np.asarray(batch["S"])).to(self.device) @@ -117,7 +116,7 @@ def step(self): state_values = self.model(states, task_name="critic").detach().squeeze() next_state_values = self.model(next_states, task_name="critic").detach().squeeze() return_est = rewards + self.config.reward_discount * next_state_values - advantages = return_est - state_values + advantages = return_est - state_values for i in range(self.config.train_iters): # actor loss log_p_new = get_log_prob(self.model(states, task_name="actor"), actions) diff --git a/maro/rl/agent/ddpg.py b/maro/rl/agent/ddpg.py index d5677a982..1822f4fa8 100644 --- a/maro/rl/agent/ddpg.py +++ b/maro/rl/agent/ddpg.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, Union +from typing import Union import numpy as np import torch @@ -24,7 +24,7 @@ class DDPGConfig: the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., loss = q_value_loss + ``policy_loss_coefficient`` * policy_loss. Defaults to 1.0. - soft_update_coefficient (float): Soft update coefficient, e.g., + soft_update_coefficient (float): Soft update coefficient, e.g., target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. Defaults to 1.0. """ @@ -66,8 +66,8 @@ class DDPG(AbsAgent): to ``step``. Defaults to False. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. - Defaults to 1. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for + training. Defaults to 1. explorer (NoiseExplorer): An NoiseExplorer instance for generating exploratory actions. Defaults to None. """ def __init__( @@ -87,7 +87,7 @@ def __init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning + min_experiences_to_trigger_learning=min_experiences_to_trigger_learning ) self._explorer = explorer self._target_model = model.copy() if model.trainable else None diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index 0c145c310..7ff51109d 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -7,7 +7,6 @@ import torch from maro.rl.model import SimpleMultiHeadModel -from maro.rl.storage import SimpleStore from maro.rl.utils import get_max, get_sampler_cls, get_td_errors, get_torch_loss_cls, select_by_actions from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask @@ -19,14 +18,14 @@ class DQNConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. - target_update_freq (int): Number of training rounds between target model updates. + target_update_freq (int): Number of training rounds between target model updates. train_iters (int): Number of batches to train the model on in each call to ``learn``. batch_size (int): Experience minibatch size. - sampler_cls: A string indicating the sampler class or a custom sampler class that provides the ``sample`` interface. - Defaults to "uniform". + sampler_cls: A string indicating the sampler class or a custom sampler class that provides the ``sample`` + interface. Defaults to "uniform". sampler_params (dict): Parameters for the sampler class. Defaults to None. epsilon (float): Exploration rate for epsilon-greedy exploration. Defaults to None. - soft_update_coefficient (float): Soft update coefficient, e.g., + soft_update_coefficient (float): Soft update coefficient, e.g., target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. Defaults to 1.0. double (bool): If True, the next Q values will be computed according to the double DQN algorithm, @@ -85,8 +84,8 @@ class DQN(AbsAgent): to ``step``. Defaults to False. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. - Defaults to 1. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for + training. Defaults to 1. """ def __init__( self, @@ -108,7 +107,7 @@ def __init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning + min_experiences_to_trigger_learning=min_experiences_to_trigger_learning ) self._sampler = self.config.sampler_cls(self.experience_memory, **self.config.sampler_params) self._training_counter = 0 diff --git a/maro/rl/agent/multi_agent_wrapper.py b/maro/rl/agent/multi_agent_wrapper.py index 7551cc1da..9245afe19 100644 --- a/maro/rl/agent/multi_agent_wrapper.py +++ b/maro/rl/agent/multi_agent_wrapper.py @@ -2,14 +2,14 @@ # Licensed under the MIT license. import os -from typing import List, Union +from typing import Union from .abs_agent import AbsAgent class MultiAgentWrapper: """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. - + Args: agent_dict (Union[AbsAgent, dict]): A single agent or a homogeneous set of agents that have the same method signatures. @@ -48,8 +48,8 @@ def set_exploration_params(self, params): def learn(self, experiences: dict) -> set: """Store experiences in the agents' experience memory. - - The top-level keys of ``experiences`` will be treated as agent IDs. + + The top-level keys of ``experiences`` will be treated as agent IDs. """ return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].learn(exp)} diff --git a/maro/rl/agent/pg.py b/maro/rl/agent/pg.py index 16ff16080..821f79287 100644 --- a/maro/rl/agent/pg.py +++ b/maro/rl/agent/pg.py @@ -29,7 +29,7 @@ class PolicyGradient(AbsAgent): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - + Args: model (SimpleMultiHeadModel): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. @@ -42,8 +42,8 @@ class PolicyGradient(AbsAgent): to ``step``. Defaults to True. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. - Defaults to 1. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for + training. Defaults to 1. """ def __init__( self, @@ -53,13 +53,13 @@ def __init__( experience_memory_overwrite_type: str, empty_experience_memory_after_step: bool = True, min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 + min_experiences_to_trigger_learning: int = 1 ): super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning + min_experiences_to_trigger_learning=min_experiences_to_trigger_learning ) def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 5aa6f8d4b..f037df84a 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -4,7 +4,7 @@ from os import getcwd from typing import Union -from maro.communication import Message, Proxy +from maro.communication import Proxy from maro.rl.agent import AbsAgent, MultiAgentWrapper from maro.rl.training import AbsEnvWrapper from maro.utils import Logger @@ -61,7 +61,7 @@ def run(self): while self.env.state and steps_to_go > 0: action = self.agent.choose_action(self.env.state) self.env.step(action) - steps_to_go -= 1 + steps_to_go -= 1 self._logger.info( f"Roll-out finished for ep {rollout_index}, segment {segment_index}" diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py index 7616d2b49..a7f3d30f4 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/distributed/actor_manager.py @@ -5,7 +5,7 @@ from os import getcwd from typing import Union -from maro.communication import Message, Proxy, SessionType +from maro.communication import Proxy, SessionType from maro.utils import Logger from .message_enums import MsgTag, MsgKey diff --git a/maro/rl/distributed/dispatcher.py b/maro/rl/distributed/dispatcher.py index 9ce7372e8..b574e51ac 100755 --- a/maro/rl/distributed/dispatcher.py +++ b/maro/rl/distributed/dispatcher.py @@ -10,8 +10,8 @@ class LRUQueue(object): """LRUQueue class using ZMQStream/IOLoop for event dispatching. - - Code adapted from https://zguide.zeromq.org/docs/chapter3/#A-High-Level-API-for-ZeroMQ. + + Code adapted from https://zguide.zeromq.org/docs/chapter3/#A-High-Level-API-for-ZeroMQ. """ def __init__(self): self._context = zmq.Context.instance() diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 18a292f11..173e3f3c2 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -56,7 +56,7 @@ def run(self): exploration_params=exploration_params if segment_index == 0 else None, required_actor_finishes=self.required_actor_finishes, discard_stale_experiences=self.discard_stale_experiences - ): + ): tl0 = time.time() updated_agents = self.agent.learn(exp) num_actor_finishes += done diff --git a/maro/rl/distributed/trainer.py b/maro/rl/distributed/trainer.py index 82777e1a7..c3ee0aa01 100755 --- a/maro/rl/distributed/trainer.py +++ b/maro/rl/distributed/trainer.py @@ -25,6 +25,6 @@ def train(sock, msg): del request["args"] del request["kwargs"] sock.send_multipart([client, b"", pickle.dumps(request)]) - + socket.on_recv_stream(train) IOLoop.instance().start() diff --git a/maro/rl/storage/abs_store.py b/maro/rl/storage/abs_store.py index 4e09e5b82..35e6869c1 100644 --- a/maro/rl/storage/abs_store.py +++ b/maro/rl/storage/abs_store.py @@ -7,6 +7,7 @@ class AbsStore(ABC): """A data store abstraction that supports get, put, update and sample operations.""" + def __init__(self): pass @@ -50,7 +51,7 @@ def filter(self, filters: Sequence[Callable]): Args: filters (Sequence[Callable]): Filter list, each item is a lambda function, - e.g., [lambda d: d['a'] == 1 and d['b'] == 1]. + e.g., [lambda d: d['a'] == 1 and d['b'] == 1]. Returns: Filtered indexes and corresponding objects. """ diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index f0075ec47..9246fa951 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -35,7 +35,7 @@ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = @property def step_index(self): return self._step_index - + @property def agent_idx_list(self): return self.env.agent_idx_list diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 702a45424..d22d2cfe1 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -3,13 +3,13 @@ import os +from typing import List, Tuple from maro.event_buffer import MaroEvents from maro.simulator.scenarios import AbsBusinessEngine -from typing import List, Tuple from .parser import ConfigParser, SupplyChainConfiguration -from .units import UnitBase, ProductUnit +from .units import ProductUnit, UnitBase from .world import World @@ -156,13 +156,15 @@ def get_metrics(self): "sale_mean": product.get_sale_mean(), "sale_std": product.get_sale_std(), "selling_price": product.get_selling_price(), - "pending_order_daily": None if product.consumer is None else product.consumer.pending_order_daily + "pending_order_daily": + None if product.consumer is None else product.consumer.pending_order_daily } for product in self._product_units }, "facilities": { facility.id: { "in_transit_orders": facility.get_in_transit_orders(), - "pending_order": None if facility.distribution is None else facility.distribution.get_pending_order() + "pending_order": + None if facility.distribution is None else facility.distribution.get_pending_order() } for facility in self.world.facilities.values() } } diff --git a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py index 99cc9e873..c1a8c9b9a 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py @@ -6,7 +6,7 @@ from .distribution import DistributionDataModel from .facility import FacilityDataModel from .manufacture import ManufactureDataModel +from .product import ProductDataModel from .seller import SellerDataModel from .storage import StorageDataModel from .vehicle import VehicleDataModel -from .product import ProductDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index b8e5f09b2..616577878 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -34,7 +34,7 @@ def __init__(self): self._price = 0 self._order_cost = 0 - def initialize(self, price:int, order_cost: int): + def initialize(self, price: int, order_cost: int): self._price = price self._order_cost = order_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/facility.py b/maro/simulator/scenarios/supply_chain/datamodels/facility.py index 64b99463e..8aae3d146 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/facility.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/facility.py @@ -2,8 +2,7 @@ # Licensed under the MIT license. -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node +from maro.backends.frame import node from .base import DataModelBase diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index 0d056143e..18bd7defe 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -30,4 +30,4 @@ def initialize(self, product_unit_cost): def reset(self): super(ManufactureDataModel, self).reset() - self.product_unit_cost = self._product_unit_cost \ No newline at end of file + self.product_unit_cost = self._product_unit_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index c187cb786..153e2045c 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -25,7 +25,7 @@ def __init__(self): self._price = 0 self._backlog_ratio = 0 - def initialize(self, price:int, backlog_ratio: float): + def initialize(self, price: int, backlog_ratio: float): self._price = price self._backlog_ratio = backlog_ratio diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py index 4e9aa71f1..2e1280961 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/storage.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/storage.py @@ -26,11 +26,13 @@ def __init__(self): self._product_list = None self._product_number = None - def initialize(self, - capacity: int = 0, - remaining_space: int = None, - product_list: list = None, - product_number: list = None): + def initialize( + self, + capacity: int = 0, + remaining_space: int = None, + product_list: list = None, + product_number: list = None + ): self._capacity = capacity self._remaining_space = remaining_space self._product_list = product_list diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py index f4e3ef1f3..7ce1f382d 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/__init__.py +++ b/maro/simulator/scenarios/supply_chain/facilities/__init__.py @@ -3,7 +3,7 @@ from .facility import FacilityBase -from .retailer import RetailerFacility from .outerretailer import OuterRetailerFacility +from .retailer import RetailerFacility from .supplier import SupplierFacility from .warehouse import WarehouseFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index e0a27b7c4..4acd42387 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -3,10 +3,12 @@ from abc import ABC from collections import defaultdict -from typing import List, Dict +from typing import Dict, List from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo -from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit +from maro.simulator.scenarios.supply_chain.units import (DistributionUnit, + ProductUnit, + StorageUnit) class FacilityBase(ABC): @@ -166,8 +168,12 @@ def get_node_info(self) -> dict: }, "configs": self.configs, "skus": self.skus, - "upstreams": {product_id: [f.id for f in source_list] for product_id, source_list in - self.upstreams.items()}, - "downstreams": {product_id: [f.id for f in source_list] for product_id, source_list in - self.downstreams.items()} + "upstreams": { + product_id: [f.id for f in source_list] + for product_id, source_list in self.upstreams.items() + }, + "downstreams": { + product_id: [f.id for f in source_list] + for product_id, source_list in self.downstreams.items() + } } diff --git a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py index 89ea90e19..eae0cc3bb 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py @@ -2,7 +2,9 @@ # Licensed under the MIT license. -from maro.simulator.scenarios.supply_chain.units import OuterSellerUnit, DataFileDemandSampler +from maro.simulator.scenarios.supply_chain.units import (DataFileDemandSampler, + OuterSellerUnit) + from .retailer import RetailerFacility # Mapping for supported sampler. diff --git a/maro/simulator/scenarios/supply_chain/parser.py b/maro/simulator/scenarios/supply_chain/parser.py index 9413376c2..16046e07a 100644 --- a/maro/simulator/scenarios/supply_chain/parser.py +++ b/maro/simulator/scenarios/supply_chain/parser.py @@ -183,7 +183,12 @@ def _parse_core_conf(self, conf: dict): module_path = module_conf["path"] for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_facility_definition(class_alias, class_def["class"], module_path, class_def.get("datamodel", None)) + self._result.add_facility_definition( + class_alias, + class_def["class"], + module_path, + class_def.get("datamodel", None) + ) def _parse_config(self): """Parse configurations.""" diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index 5f9170f58..fe0b67283 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -5,10 +5,11 @@ from .consumer import ConsumerUnit from .distribution import DistributionUnit from .manufacture import ManufactureUnit -from .simplemanufacture import SimpleManufactureUnit +from .outerseller import (DataFileDemandSampler, OuterSellerUnit, + SellerDemandSampler) from .product import ProductUnit from .seller import SellerUnit -from .outerseller import OuterSellerUnit, SellerDemandSampler, DataFileDemandSampler +from .simplemanufacture import SimpleManufactureUnit from .skuunit import SkuUnit from .storage import StorageUnit from .storeproduct import StoreProductUnit diff --git a/maro/simulator/scenarios/supply_chain/units/outerseller.py b/maro/simulator/scenarios/supply_chain/units/outerseller.py index 2e245b897..c7eb0b3a1 100644 --- a/maro/simulator/scenarios/supply_chain/units/outerseller.py +++ b/maro/simulator/scenarios/supply_chain/units/outerseller.py @@ -29,7 +29,8 @@ def sample_demand(self, product_id: int, tick: int) -> int: Args: product_id (int): Id of product to sample. - tick (int): Tick of environment, NOTE: this tick is start from 0, you may need to transform it to your time system. + tick (int): Tick of environment, NOTE: this tick is start from 0, + you may need to transform it to your time system. """ pass diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py index d6c3b17c7..8a21305d0 100644 --- a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py @@ -18,4 +18,4 @@ def step(self, tick: int): sku_num = len(self.facility.skus) unit_num_upper_bound = self.facility.storage.capacity // sku_num current_product_number = self.facility.storage.get_product_number(self.product_id) - self.manufacture_number = max(0, min(unit_num_upper_bound-current_product_number, production_rate)) + self.manufacture_number = max(0, min(unit_num_upper_bound - current_product_number, production_rate)) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 43ea116d3..d9036f411 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -6,13 +6,15 @@ from typing import List, Tuple, Union import networkx as nx + from maro.backends.frame import FrameBase from .easy_config import EasyConfig, SkuInfo from .facilities import FacilityBase from .frame_builder import build_frame -from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef -from .units import UnitBase, ProductUnit, SkuUnit +from .parser import (DataModelDef, FacilityDef, SupplyChainConfiguration, + UnitDef) +from .units import ProductUnit, SkuUnit, UnitBase AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id")) From 81b6d2b4455f91ef752e73c4625c9de9d121b152 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 15 Apr 2021 17:44:44 +0800 Subject: [PATCH 198/482] linting fix --- maro/simulator/scenarios/supply_chain/facilities/facility.py | 4 +--- .../scenarios/supply_chain/facilities/outerretailer.py | 3 +-- maro/simulator/scenarios/supply_chain/units/__init__.py | 3 +-- maro/simulator/scenarios/supply_chain/units/consumer.py | 1 - maro/simulator/scenarios/supply_chain/units/distribution.py | 2 +- maro/simulator/scenarios/supply_chain/world.py | 3 +-- 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index 4acd42387..c5a3e6781 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -6,9 +6,7 @@ from typing import Dict, List from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo -from maro.simulator.scenarios.supply_chain.units import (DistributionUnit, - ProductUnit, - StorageUnit) +from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit class FacilityBase(ABC): diff --git a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py index eae0cc3bb..1d8e85efe 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py +++ b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py @@ -2,8 +2,7 @@ # Licensed under the MIT license. -from maro.simulator.scenarios.supply_chain.units import (DataFileDemandSampler, - OuterSellerUnit) +from maro.simulator.scenarios.supply_chain.units import DataFileDemandSampler, OuterSellerUnit from .retailer import RetailerFacility diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index fe0b67283..c72c3b5d6 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -5,8 +5,7 @@ from .consumer import ConsumerUnit from .distribution import DistributionUnit from .manufacture import ManufactureUnit -from .outerseller import (DataFileDemandSampler, OuterSellerUnit, - SellerDemandSampler) +from .outerseller import DataFileDemandSampler, OuterSellerUnit, SellerDemandSampler from .product import ProductUnit from .seller import SellerUnit from .simplemanufacture import SimpleManufactureUnit diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index e82233d18..3e9b7c3a0 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. -import warnings from collections import Counter, defaultdict from scipy.ndimage.interpolation import shift diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py index 0bfd11379..5860e7a5a 100644 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. -from collections import defaultdict, deque, Counter +from collections import Counter, defaultdict, deque from typing import Dict, List from .order import Order diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index d9036f411..72e9b9783 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -12,8 +12,7 @@ from .easy_config import EasyConfig, SkuInfo from .facilities import FacilityBase from .frame_builder import build_frame -from .parser import (DataModelDef, FacilityDef, SupplyChainConfiguration, - UnitDef) +from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef from .units import ProductUnit, SkuUnit, UnitBase AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id")) From e85fb932665670ce8710697fbc9abf17094bbc2d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 16 Apr 2021 06:58:17 +0000 Subject: [PATCH 199/482] merged with chaos' outersampling logic --- examples/supply_chain/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 525a71e98..f02ee8505 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -91,7 +91,7 @@ distributed: # this is used to group all actor / learner processes belonging to the same job for communication purposes. # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. group: sc-dqn - num_actors: 6 # number of parallel roll-out actors + num_actors: 3 # number of parallel roll-out actors # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make # sure that a local redis server is running and listening on the port specified by "redis_port". @@ -99,6 +99,6 @@ distributed: redis_port: 6379 # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent # slow actors from dragging down the whole process. - required_actor_finishes: 4 + required_actor_finishes: 2 # If true, experiences from older segments (usually coming from slow actors) will not be used for learning. discard_stale_experiences: True From 04991f323fd447e9f35c5caeebb0dc2fdc5a2f5c Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 16 Apr 2021 16:25:06 +0000 Subject: [PATCH 200/482] added hierachical multi-agent structure --- docs/source/examples/multi_agent_dqn_cim.rst | 4 +- docs/source/key_components/rl_toolkit.rst | 2 +- examples/cim/ac/main.py | 4 +- examples/cim/dqn/main.py | 6 +- examples/supply_chain/agent.py | 17 -- examples/supply_chain/config.yml | 66 ++--- examples/supply_chain/distributed_launcher.py | 34 ++- examples/supply_chain/env_wrapper.py | 68 ++--- .../supply_chain/single_thread_launcher.py | 43 ++- maro/rl/__init__.py | 26 +- maro/rl/agent/__init__.py | 16 +- maro/rl/agent/abs_agent.py | 135 --------- maro/rl/agent/ac.py | 67 ++--- maro/rl/agent/agent.py | 263 ++++++++++++++++++ maro/rl/agent/agent_cls_index.py | 22 ++ maro/rl/agent/agent_manager.py | 64 +++++ maro/rl/agent/ddpg.py | 51 ++-- maro/rl/agent/dqn.py | 88 +++--- maro/rl/agent/experience_enum.py | 10 + maro/rl/agent/multi_agent_wrapper.py | 113 -------- maro/rl/agent/pg.py | 38 +-- maro/rl/distributed/actor.py | 8 +- maro/rl/distributed/actor_manager.py | 2 - maro/rl/distributed/learner.py | 8 +- maro/rl/model/__init__.py | 4 +- maro/rl/model/fc_block.py | 5 +- maro/rl/model/learning_model.py | 7 +- maro/rl/storage/__init__.py | 3 +- maro/rl/training/env_wrapper.py | 123 ++++---- maro/rl/training/learner.py | 16 +- maro/rl/utils/__init__.py | 8 +- maro/rl/utils/get_cls.py | 11 + maro/rl/utils/sampler_cls_index.py | 16 -- maro/rl/utils/torch_cls_index.py | 118 -------- .../rl_formulation.ipynb | 4 +- 35 files changed, 727 insertions(+), 743 deletions(-) delete mode 100644 examples/supply_chain/agent.py delete mode 100644 maro/rl/agent/abs_agent.py create mode 100644 maro/rl/agent/agent.py create mode 100644 maro/rl/agent/agent_cls_index.py create mode 100644 maro/rl/agent/agent_manager.py create mode 100644 maro/rl/agent/experience_enum.py delete mode 100644 maro/rl/agent/multi_agent_wrapper.py create mode 100644 maro/rl/utils/get_cls.py delete mode 100644 maro/rl/utils/sampler_cls_index.py delete mode 100644 maro/rl/utils/torch_cls_index.py diff --git a/docs/source/examples/multi_agent_dqn_cim.rst b/docs/source/examples/multi_agent_dqn_cim.rst index 8df2015a4..465171299 100644 --- a/docs/source/examples/multi_agent_dqn_cim.rst +++ b/docs/source/examples/multi_agent_dqn_cim.rst @@ -141,7 +141,7 @@ from the learner. .. code-block:: python def cim_dqn_actor(): env = Env(**training_config["env"]) - agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) + agent = AgentManager({name: get_dqn_agent() for name in env.agent_idx_list}) actor = Actor(env, agent, CIMTrajectoryForDQN, trajectory_kwargs=common_config) actor.worker(training_config["group"]) @@ -154,7 +154,7 @@ latest policies. .. code-block:: python def cim_dqn_learner(): env = Env(**training_config["env"]) - agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) + agent = AgentManager({name: get_dqn_agent() for name in env.agent_idx_list}) scheduler = TwoPhaseLinearParameterScheduler(training_config["max_episode"], **training_config["exploration"]) actor = ActorProxy( training_config["group"], training_config["num_actors"], diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index e1d8b6e6d..319de3685 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -103,7 +103,7 @@ The RL toolkit provides tools that make local and distributed training easy: distributed fashion by loading an ``Actor`` or ``ActorProxy`` instance, respectively. * Actor, which implements the ``roll_out`` method where the agent interacts with the environment for one episode. It consists of an environment instance and an agent (a single agent or multiple agents wrapped by - ``MultiAgentWrapper``). The class provides the worker() method which turns it to an event loop where roll-outs + ``AgentManager``). The class provides the worker() method which turns it to an event loop where roll-outs are performed on the learner's demand. In distributed RL, there are typically many actor processes running simultaneously to parallelize training data collection. * Actor proxy, which also implements the ``roll_out`` method with the same signature, but manages a set of remote diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index a41e2b549..a454fb55f 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -8,7 +8,7 @@ import numpy as np from maro.rl import ( - Actor, ActorCritic, ActorCriticConfig, FullyConnectedBlock, MultiAgentWrapper, Learner, Scheduler, + Actor, ActorCritic, ActorCriticConfig, FullyConnectedBlock, AgentManager, Learner, Scheduler, SimpleMultiHeadModel, OptimOption ) from maro.simulator import Env @@ -49,7 +49,7 @@ def get_ac_agent(): if __name__ == "__main__": set_seeds(1024) # for reproducibility env = Env(**config["training"]["env"]) - agent = MultiAgentWrapper({name: get_ac_agent() for name in env.agent_idx_list}) + agent = AgentManager({name: get_ac_agent() for name in env.agent_idx_list}) scheduler = Scheduler(config["training"]["max_episode"]) learner = Learner( CIMEnvWrapper(env, **config["shaping"]), agent, scheduler, diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 22937b618..ffd589bd4 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -8,7 +8,7 @@ from os.path import dirname, join, realpath from maro.rl import ( - Actor, ActorManager, DQN, DQNConfig, DistLearner, FullyConnectedBlock, MultiAgentWrapper, OptimOption, + Actor, ActorManager, DQN, DQNConfig, DistLearner, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel, TwoPhaseLinearParameterScheduler ) from maro.simulator import Env @@ -47,7 +47,7 @@ def get_dqn_agent(): def cim_dqn_learner(): - agent = MultiAgentWrapper({name: get_dqn_agent() for name in Env(**config["training"]["env"]).agent_idx_list}) + agent = AgentManager({name: get_dqn_agent() for name in Env(**config["training"]["env"]).agent_idx_list}) scheduler = TwoPhaseLinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) actor_manager = ActorManager( NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False} @@ -63,7 +63,7 @@ def cim_dqn_learner(): def cim_dqn_actor(): env = Env(**config["training"]["env"]) - agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) + agent = AgentManager({name: get_dqn_agent() for name in env.agent_idx_list}) actor = Actor( CIMEnvWrapper(env, **config["shaping"]), agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)} diff --git a/examples/supply_chain/agent.py b/examples/supply_chain/agent.py deleted file mode 100644 index 643f085aa..000000000 --- a/examples/supply_chain/agent.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.rl import DQN, DQNConfig, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel - - -def get_dqn_agent(config): - q_model = SimpleMultiHeadModel( - FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"]) - ) - return DQN(q_model, DQNConfig(**config["algorithm_config"]), **config["experience_memory"]) - - -def get_ppo_agent(config): - pass - -get_agent_func_map = {"dqn": get_dqn_agent} diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index f02ee8505..13725e3e9 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -4,21 +4,23 @@ agent: consumer: algorithm: dqn - model: # If you want to design your model used, edit the get_dqn_agent() codes in examples\supply_chain\agent.py - hidden_dims: - - 16 - - 8 - output_dim: 10 - activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. - optim_params: - lr: 0.001 + share_model: true + model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. + network: + hidden_dims: + - 16 + - 8 + output_dim: 10 + activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. + optim_params: + lr: 0.001 algorithm_config: reward_discount: .9 train_iters: 10 # number of gradient steps per call to agent.step() @@ -27,7 +29,7 @@ agent: target_update_freq: 5 # How many training iteration, to update DQN target model soft_update_coefficient: 0.1 double: false # whether to enable double DQN - experience_memory: + generic_config: experience_memory_size: 50000 # experience memory size # This determines how existing experiences are replaced when adding new experiences to a full experience # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, @@ -41,21 +43,23 @@ agent: min_experiences_to_trigger_learning: 10 producer: algorithm: dqn + share_model: false model: - hidden_dims: - - 16 - - 8 - output_dim: 10 - activation: leaky_relu - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop - optim_params: - lr: 0.001 + network: + hidden_dims: + - 16 + - 8 + output_dim: 10 + activation: leaky_relu + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop + optim_params: + lr: 0.001 algorithm_config: reward_discount: .9 train_iters: 10 @@ -64,7 +68,7 @@ agent: target_update_freq: 5 soft_update_coefficient: 0.1 double: false # double DQN - experience_memory: + generic_config: experience_memory_size: 50000 experience_memory_overwrite_type: random empty_experience_memory_after_step: false diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index f752f39b9..c9fae6c7e 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -10,7 +10,7 @@ from os.path import dirname, join, realpath from maro.rl import ( - Actor, ActorManager, DistLearner, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, + Actor, ActorManager, DistLearner, FullyConnectedBlock, LinearParameterScheduler, AgentManager, OptimOption, SimpleMultiHeadModel ) from maro.simulator import Env @@ -19,7 +19,7 @@ sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) from env_wrapper import SCEnvWrapper -from agent import get_agent_func_map +from agent import get_agent_func_map, get_q_model # Read the configuration @@ -27,8 +27,8 @@ with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: config = yaml.safe_load(config_file) -get_producer_agent = get_agent_func_map[config["agent"]["producer"]["algorithm"]] -get_consumer_agent = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] +get_producer_agent_func = get_agent_func_map[config["agent"]["producer"]["algorithm"]] +get_consumer_agent_func = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] # Get the env config path topology = config["training"]["env"]["topology"] @@ -44,13 +44,25 @@ makedirs(log_dir, exist_ok=True) +def get_sc_agents(agent_idx_list, type_): + assert type_ in {"producer", "consumer"} + q_model = get_q_model(config["agent"][type_]["model"]) if config["agent"][type_]["share_model"] else None + alg_type = config["agent"][type_]["algorithm"] + return { + f"{type_}.{info.id}": get_agent_func_map[alg_type](config["agent"][type_], q_model=q_model) + for info in agent_idx_list + } + + def sc_learner(): # create agents that update themselves using experiences collected from the actors. env = SCEnvWrapper(Env(**config["training"]["env"])) config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in env.agent_idx_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} - agent = MultiAgentWrapper({**producers, **consumers}) + + producers = get_sc_agents(env.agent_idx_list, "producer") + consumers = get_sc_agents(env.agent_idx_list, "consumer") + agent = AgentManager({**producers, **consumers}) + print("share model: ", agent.has_shared_model) # exploration schedule scheduler = LinearParameterScheduler(config["training"]["num_episodes"], **config["training"]["exploration"]) @@ -78,9 +90,11 @@ def sc_actor(): config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim # create agents to interact with the env - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in env.agent_idx_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} - agent = MultiAgentWrapper({**producers, **consumers}) + producers = get_sc_agents(env.agent_idx_list, "producer") + consumers = get_sc_agents(env.agent_idx_list, "consumer") + agent = AgentManager({**producers, **consumers}) + + print("share model: ", agent.has_shared_model) # create an actor that collects simulation data for the learner. actor = Actor(env, agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, log_dir=log_dir) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 8008dd4da..f029b29b5 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -196,7 +196,7 @@ def get_state(self, event): for sku_id, number in in_transit_orders.items(): self._facility_in_transit_orders[facility_id][sku_id] = number - final_state = {} + final_state = {"consumer": {}, "producer": {}} # calculate storage info first, then use it later to speed up. for facility_id, storage_index in self._facility2storage_index_dict.items(): @@ -228,12 +228,12 @@ def get_state(self, event): np_state = self._serialize_state(state) - final_state[f"consumer.{agent_info.id}"] = np_state - final_state[f"producer.{agent_info.id}"] = np_state + final_state["consumer"][agent_info.id] = np_state + final_state["producer"][agent_info.id] = np_state return final_state - def get_reward(self, tick=None, target_agents=None): + def get_reward(self, tick=None): wc = self.env.configs.settings["global_reward_weight_consumer"] parent_facility_balance = {} for f_id, sheet in self.cur_balance_sheet_reward.items(): @@ -247,11 +247,11 @@ def get_reward(self, tick=None, target_agents=None): self.cur_balance_sheet_reward.items()} return { - **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, - **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} + "producer": {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, + "consumer": {f_id: np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} } - def get_action(self, action_by_agent): + def get_action(self, action_info): # cache the sources for each consumer if not yet cached if not hasattr(self, "product2source"): self.product2source, self.consumer2product = {}, {} @@ -266,43 +266,33 @@ def get_action(self, action_by_agent): self.consumer2product[consumer_id] = product_id env_action = {} - for agent_id, action in action_by_agent.items(): - unit_id = int(agent_id.split(".")[1]) + # consumer action + for unit_id, action in action_info["consumer"].items(): + product_id = self.consumer2product.get(unit_id, 0) + sources = self.product2source.get(unit_id, []) + if sources: + source_id = sources[0] + action_number = int(int(action) * self._cur_metrics["products"][unit_id]["sale_mean"]) + # ignore 0 quantity to reduce action number + if action_number == 0: + continue + sku = self._units_mapping[unit_id][3] + reward_discount = 0 + env_action[unit_id] = ConsumerAction(unit_id, product_id, source_id, action_number, sku.vlt, reward_discount) + # producer action + for unit_id, action in action_info["producer"].items(): is_facility = unit_id not in self._units_mapping - # ignore facility to reduce action number if is_facility: continue - - # consumer action - if agent_id.startswith("consumer"): - product_id = self.consumer2product.get(unit_id, 0) - sources = self.product2source.get(unit_id, []) - - if sources: - source_id = sources[0] - - action_number = int(int(action) * self._cur_metrics["products"][unit_id]["sale_mean"]) - - # ignore 0 quantity to reduce action number - if action_number == 0: - continue - - sku = self._units_mapping[unit_id][3] - reward_discount = 0 - - env_action[unit_id] = ConsumerAction(unit_id, product_id, source_id, action_number, sku.vlt, reward_discount) - # manufacturer action - elif agent_id.startswith("producer"): - sku = self._units_mapping[unit_id][3] - action = sku.production_rate - - # ignore invalid actions - if action is None or action == 0: - continue - - env_action[unit_id] = ManufactureAction(unit_id, action) + # producer action + sku = self._units_mapping[unit_id][3] + action = sku.production_rate + # ignore invalid actions + if action is None or action == 0: + continue + env_action[unit_id] = ManufactureAction(unit_id, action) return env_action diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index 1bc18ba13..01b83a2c8 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -8,15 +8,16 @@ import numpy as np -from maro.rl import Learner, LinearParameterScheduler, MultiAgentWrapper +from maro.rl import ( + AGENT_CLS, AGENT_CONFIG, AgentGroup, AgentManager, FullyConnectedBlock, GenericAgentConfig, Learner, + LinearParameterScheduler, OptimOption, SimpleMultiHeadModel, get_cls +) from maro.simulator import Env from maro.utils import set_seeds sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) from env_wrapper import SCEnvWrapper -from agent import get_agent_func_map - # Single-threaded launcher if __name__ == "__main__": @@ -24,22 +25,42 @@ with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: config = yaml.safe_load(config_file) - get_producer_agent = get_agent_func_map[config["agent"]["producer"]["algorithm"]] - get_consumer_agent = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] - # Get the env config path topology = config["training"]["env"]["topology"] config["training"]["env"]["topology"] = join(dirname(realpath(__file__)), "topologies", topology) # create an env wrapper and obtain the input dimension for the agents env = SCEnvWrapper(Env(**config["training"]["env"])) - config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim + config["agent"]["producer"]["model"]["network"]["input_dim"] = env.dim + config["agent"]["consumer"]["model"]["network"]["input_dim"] = env.dim # create agents - agent_info_list = env.agent_idx_list - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in agent_info_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in agent_info_list} - agent = MultiAgentWrapper({**producers, **consumers}) + agent_names = [info.id for info in env.agent_idx_list] + + def get_model(model_config): + return SimpleMultiHeadModel( + FullyConnectedBlock(**model_config["network"]), + optim_option=OptimOption(**model_config["optimization"]) + ) + + def get_sc_agents(agent_names, agent_config): + agent_cls = get_cls(agent_config["algorithm"], AGENT_CLS) + algorithm_config_cls = get_cls(agent_config["algorithm"], AGENT_CONFIG) + algorithm_config = algorithm_config_cls(**agent_config["algorithm_config"]) + generic_config = GenericAgentConfig(**agent_config["generic_config"]) + if agent_config["share_model"]: + shared_model = get_model(agent_config["model"]) + return AgentGroup(agent_names, agent_cls, shared_model, algorithm_config, generic_config) + else: + return AgentManager({ + name: agent_cls(get_model(agent_config["model"]), algorithm_config, generic_config) + for name in agent_names + }) + + agent = AgentManager({ + "producer": get_sc_agents(agent_names, config["agent"]["producer"]), + "consumer": get_sc_agents(agent_names, config["agent"]["consumer"]) + }) # exploration schedule scheduler = LinearParameterScheduler(config["training"]["num_episodes"], **config["training"]["exploration"]) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index aedb7b6fe..f2b1fc6fe 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -2,33 +2,35 @@ # Licensed under the MIT license. from maro.rl.agent import ( - DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient, - PolicyGradientConfig + AGENT_CLS, AGENT_CONFIG, DDPG, DQN, TORCH_LOSS_CLS, AbsAgent, ActorCritic, ActorCriticConfig, AgentGroup, + AgentManager, DDPGConfig, DQNConfig, GenericAgentConfig, PolicyGradient, PolicyGradientConfig, TORCH_LOSS_CLS ) from maro.rl.distributed import Actor, ActorManager, DistLearner from maro.rl.exploration import ( AbsExplorer, EpsilonGreedyExplorer, GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer ) -from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel +from maro.rl.model import ( + TORCH_ACTIVATION_CLS, TORCH_LR_SCHEDULER_CLS, TORCH_OPTIM_CLS, AbsBlock, AbsCoreModel, FullyConnectedBlock, + OptimOption, SimpleMultiHeadModel +) from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler -from maro.rl.storage import AbsSampler, AbsStore, SimpleStore, UniformSampler +from maro.rl.storage import SAMPLER_CLS, AbsSampler, AbsStore, SimpleStore, UniformSampler from maro.rl.training import AbsEnvWrapper, Learner from maro.rl.utils import ( - get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_sampler_cls, get_torch_activation_cls, - get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward, + get_cls, get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_truncated_cumulative_reward, select_by_actions ) __all__ = [ - "AbsAgent", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", - "PolicyGradient", "PolicyGradientConfig", + "AGENT_CLS", "AGENT_CONFIG", "TORCH_LOSS_CLS", "AbsAgent", "ActorCritic", "ActorCriticConfig", "AgentGroup", "AgentManager", "DDPG", + "DDPGConfig", "DQN", "DQNConfig", "GenericAgentConfig", "PolicyGradient", "PolicyGradientConfig", "Actor", "ActorManager", "DistLearner", "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", - "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", + "TORCH_ACTIVATION_CLS", "TORCH_LR_SCHEDULER_CLS", "TORCH_OPTIM_CLS", "AbsBlock", "AbsCoreModel", + "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", - "AbsSampler", "AbsStore", "SimpleStore", "UniformSampler", + "SAMPLER_CLS", "AbsSampler", "AbsStore", "SimpleStore", "UniformSampler", "AbsEnvWrapper", "Learner", - "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_sampler_cls", - "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", + "get_cls", "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_truncated_cumulative_reward", "select_by_actions" ] diff --git a/maro/rl/agent/__init__.py b/maro/rl/agent/__init__.py index ee082cd64..534c6ad26 100644 --- a/maro/rl/agent/__init__.py +++ b/maro/rl/agent/__init__.py @@ -1,18 +1,24 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .abs_agent import AbsAgent from .ac import ActorCritic, ActorCriticConfig +from .agent import AbsAgent, AgentGroup, GenericAgentConfig +from .agent_cls_index import AGENT_CLS, AGENT_CONFIG +from .agent_manager import AgentManager from .ddpg import DDPG, DDPGConfig from .dqn import DQN, DQNConfig -from .multi_agent_wrapper import MultiAgentWrapper +from .experience_enum import Experience from .pg import PolicyGradient, PolicyGradientConfig +from .torch_loss_cls_index import TORCH_LOSS_CLS __all__ = [ - "AbsAgent", "ActorCritic", "ActorCriticConfig", + "AbsAgent", "AgentGroup", "GenericAgentConfig", + "AGENT_CLS", + "AgentManager", "DDPG", "DDPGConfig", "DQN", "DQNConfig", - "MultiAgentWrapper", - "PolicyGradient", "PolicyGradientConfig" + "Experience", + "PolicyGradient", "PolicyGradientConfig", + "TORCH_LOSS_CLS" ] diff --git a/maro/rl/agent/abs_agent.py b/maro/rl/agent/abs_agent.py deleted file mode 100644 index 83eeca9c3..000000000 --- a/maro/rl/agent/abs_agent.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - -import torch - -from maro.rl.model import AbsCoreModel -from maro.rl.storage import SimpleStore - - -class AbsAgent(ABC): - """Abstract RL agent class. - - It's a sandbox for the RL algorithm. Scenario-specific details will be excluded. - We focus on the abstraction algorithm development here. Environment observation and decision events will - be converted to a uniform format before calling in. The output will be converted to an environment - executable format before return back to the environment. Its key responsibility is optimizing policy based - on interaction with the environment. - - Args: - model (AbsCoreModel): Task model or container of task models required by the algorithm. - config: Settings for the algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. - """ - def __init__( - self, - model: AbsCoreModel, - config, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 - ): - self.model = model - self.config = config - self.experience_memory = SimpleStore( - ["S", "A", "R", "S_"], capacity=experience_memory_size, overwrite_type=experience_memory_overwrite_type - ) - self.empty_experience_memory_after_step = empty_experience_memory_after_step - self.min_new_experiences_to_trigger_learning = min_new_experiences_to_trigger_learning - self.min_experiences_to_trigger_learning = min_experiences_to_trigger_learning - self.device = torch.device('cpu') - self._version_index = 0 - - @property - def version(self): - return self._version_index - - @version.setter - def version(self, version_index): - self._version_index = version_index - - def to_device(self, device): - self.device = device - self.model = self.model.to(device) - - @abstractmethod - def choose_action(self, state): - """This method uses the underlying model(s) to compute an action from a shaped state. - - Args: - state: A state object shaped by a ``StateShaper`` to conform to the model input format. - - Returns: - The action to be taken given ``state``. It is usually necessary to use an ``ActionShaper`` to convert - this to an environment executable action. - """ - return NotImplementedError - - def set_exploration_params(self, **params): - pass - - def learn(self, experiences: dict) -> bool: - """Store experinces in the experience memory and train the model if necessary.""" - if set(experiences.keys()) != {"S", "A", "R", "S_"}: - raise ValueError("The keys of experiences must be {'S', 'A', 'R', 'S_'}") - self.experience_memory.put(experiences) - if ( - len(experiences["S"]) >= self.min_new_experiences_to_trigger_learning and - len(self.experience_memory) >= self.min_experiences_to_trigger_learning - ): - self.step() - self._version_index += 1 - if self.empty_experience_memory_after_step: - self.experience_memory.clear() - return True - return False - - @abstractmethod - def step(self): - """Algorithm-specific training logic. - - The parameters are data to train the underlying model on. Algorithm-specific loss and optimization - should be reflected here. - """ - return NotImplementedError - - def load_model(self, model): - """Load models from memory.""" - self.model.load_state_dict(model) - - def dump_model(self): - """Return the algorithm's trainable models.""" - return self.model.state_dict() - - def load_model_from_file(self, path: str): - """Load trainable models from disk. - - Load trainable models from the specified directory. The model file is always prefixed with the agent's name. - - Args: - path (str): path to the directory where the models are saved. - """ - self.model.load_state_dict(torch.load(path)) - - def dump_model_to_file(self, path: str): - """Dump the algorithm's trainable models to disk. - - Dump trainable models to the specified directory. The model file is always prefixed with the agent's name. - - Args: - path (str): path to the directory where the models are saved. - """ - torch.save(self.model.state_dict(), path) diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py index bfaaf53d0..827e22535 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/agent/ac.py @@ -7,11 +7,14 @@ import torch from torch.distributions import Categorical +from maro.rl.cls_index import TORCH_LOSS_CLS from maro.rl.model import SimpleMultiHeadModel -from maro.rl.utils import get_log_prob, get_torch_loss_cls +from maro.rl.storage import SimpleStore +from maro.rl.utils import get_cls, get_log_prob from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent +from .agent import AbsAgent, GenericAgentConfig +from .experience_enum import Experience class ActorCriticConfig: @@ -38,7 +41,7 @@ def __init__( clip_ratio: float = None ): self.reward_discount = reward_discount - self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() + self.critic_loss_func = get_cls(critic_loss_cls, TORCH_LOSS_CLS)() self.train_iters = train_iters self.actor_loss_coefficient = actor_loss_coefficient self.clip_ratio = clip_ratio @@ -52,38 +55,22 @@ class ActorCritic(AbsAgent): https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: - model (SimpleMultiHeadModel): Multi-task model that computes action distributions and state values. - It may or may not have a shared bottom stack. - config: Configuration for the AC algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. Defaults to True. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. + model (AbsCoreModel): Task model or container of task models required by the algorithm. + algorithm_config: Algorithm-specific configuration. + generic_config (GenericAgentConfig): Non-algorithm-specific configuration. + experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be + created at init time. Defaults to None. """ def __init__( self, model: SimpleMultiHeadModel, - config: ActorCriticConfig, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool = True, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 + algorithm_config, + generic_config: GenericAgentConfig, + experience_memory: SimpleStore = None ): if model.task_names is None or set(model.task_names) != {"actor", "critic"}: raise UnrecognizedTask(f"Expected model task names 'actor' and 'critic', but got {model.task_names}") - super().__init__( - model, config, experience_memory_size, experience_memory_overwrite_type, - empty_experience_memory_after_step, - min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning - ) + super().__init__(model, algorithm_config, generic_config, experience_memory=experience_memory) def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Use the actor (policy) model to generate stochastic actions. @@ -107,29 +94,31 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: def step(self): batch = self.experience_memory.get() - states = torch.from_numpy(np.asarray(batch["S"])).to(self.device) - actions = torch.from_numpy(np.asarray([act[0] for act in batch["A"]])).to(self.device) - log_p = torch.from_numpy(np.asarray([act[1] for act in batch["A"]])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch["R"])).to(self.device) - next_states = torch.from_numpy(np.asarray(batch["S_"])).to(self.device) + states = torch.from_numpy(np.asarray(batch[Experience.STATE])).to(self.device) + actions = torch.from_numpy(np.asarray([act[0] for act in batch[Experience.ACTION]])).to(self.device) + log_p = torch.from_numpy(np.asarray([act[1] for act in batch[Experience.ACTION]])).to(self.device) + rewards = torch.from_numpy(np.asarray(batch[Experience.REWARD])).to(self.device) + next_states = torch.from_numpy(np.asarray(batch[Experience.NEXT_STATE])).to(self.device) state_values = self.model(states, task_name="critic").detach().squeeze() next_state_values = self.model(next_states, task_name="critic").detach().squeeze() - return_est = rewards + self.config.reward_discount * next_state_values + return_est = rewards + self.algorithm_config.reward_discount * next_state_values advantages = return_est - state_values - for i in range(self.config.train_iters): + for i in range(self.algorithm_config.train_iters): # actor loss log_p_new = get_log_prob(self.model(states, task_name="actor"), actions) - if self.config.clip_ratio is not None: + if self.algorithm_config.clip_ratio is not None: ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) + clip_ratio = torch.clamp( + ratio, 1 - self.algorithm_config.clip_ratio, 1 + self.algorithm_config.clip_ratio + ) actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() else: actor_loss = -(log_p_new * advantages).mean() # critic_loss state_values = self.model(states, task_name="critic").squeeze() - critic_loss = self.config.critic_loss_func(state_values, return_est) - loss = critic_loss + self.config.actor_loss_coefficient * actor_loss + critic_loss = self.algorithm_config.critic_loss_func(state_values, return_est) + loss = critic_loss + self.algorithm_config.actor_loss_coefficient * actor_loss self.model.step(loss) diff --git a/maro/rl/agent/agent.py b/maro/rl/agent/agent.py new file mode 100644 index 000000000..1755144fd --- /dev/null +++ b/maro/rl/agent/agent.py @@ -0,0 +1,263 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from typing import Dict, Union + +import torch + +from maro.rl.model import AbsCoreModel +from maro.rl.storage import SimpleStore + +from .experience_enum import Experience + + +class GenericAgentConfig: + """Generic agent settings. + + Args: + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call + to ``step``. + min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. + Defaults to 1. + min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for + training. Defaults to 1. + """ + __slots__ = [ + "experience_memory_size", "experience_memory_overwrite_type", "empty_experience_memory_after_step", + "min_new_experiences_to_trigger_learning", "min_experiences_to_trigger_learning" + ] + + def __init__( + self, + experience_memory_size: int, + experience_memory_overwrite_type: str, + empty_experience_memory_after_step: bool, + min_new_experiences_to_trigger_learning: int = 1, + min_experiences_to_trigger_learning: int = 1 + ): + self.experience_memory_size = experience_memory_size + self.experience_memory_overwrite_type = experience_memory_overwrite_type + self.empty_experience_memory_after_step = empty_experience_memory_after_step + self.min_new_experiences_to_trigger_learning = min_new_experiences_to_trigger_learning + self.min_experiences_to_trigger_learning = min_experiences_to_trigger_learning + + +class AbsAgent(ABC): + """Abstract RL agent class. + + It's a sandbox for the RL algorithm. Scenario-specific details will be excluded. + We focus on the abstraction algorithm development here. Environment observation and decision events will + be converted to a uniform format before calling in. The output will be converted to an environment + executable format before return back to the environment. Its key responsibility is optimizing policy based + on interaction with the environment. + + Args: + model (AbsCoreModel): Task model or container of task models required by the algorithm. + algorithm_config: Algorithm-specific configuration. + generic_config (GenericAgentConfig): Non-algorithm-specific configuration. + experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be + created at init time. Defaults to None. + """ + def __init__( + self, + model: AbsCoreModel, + algorithm_config, + generic_config: GenericAgentConfig, + experience_memory: SimpleStore = None + ): + self.model = model + self.algorithm_config = algorithm_config + self.generic_config = generic_config + if not experience_memory: + self.experience_memory = SimpleStore( + [Experience.STATE, Experience.ACTION, Experience.REWARD, Experience.NEXT_STATE], + capacity=self.generic_config.experience_memory_size, + overwrite_type=self.generic_config.experience_memory_overwrite_type + ) + else: + self.experience_memory = experience_memory + + self.device = torch.device('cpu') + + def to_device(self, device): + self.device = device + self.model = self.model.to(device) + + @abstractmethod + def choose_action(self, state): + """This method uses the underlying model(s) to compute an action from a shaped state. + + Args: + state: A state object shaped by a ``StateShaper`` to conform to the model input format. + + Returns: + The action to be taken given ``state``. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + return NotImplementedError + + def set_exploration_params(self, **params): + pass + + def learn(self, experiences: dict) -> bool: + """Store experinces in the experience memory and train the model if necessary.""" + expected_keys = set(self.experience_memory.keys) + if set(experiences.keys()) != set(self.experience_memory.keys): + raise ValueError(f"The keys of experiences must be {expected_keys}") + self.experience_memory.put(experiences) + if ( + len(experiences[Experience.STATE]) >= self.generic_config.min_new_experiences_to_trigger_learning and + len(self.experience_memory) >= self.generic_config.min_experiences_to_trigger_learning + ): + self.step() + if self.generic_config.empty_experience_memory_after_step: + self.experience_memory.clear() + return True + return False + + @abstractmethod + def step(self): + """Algorithm-specific training logic. + + The parameters are data to train the underlying model on. Algorithm-specific loss and optimization + should be reflected here. + """ + return NotImplementedError + + def load_model(self, model): + """Load models from memory.""" + self.model.load_state_dict(model) + + def dump_model(self): + """Return the algorithm's trainable models.""" + return self.model.state_dict() + + def load_model_from_file(self, path: str): + """Load trainable models from disk. + + Load trainable models from the specified directory. The model file is always prefixed with the agent's name. + + Args: + path (str): path to the directory where the models are saved. + """ + self.model.load_state_dict(torch.load(path)) + + def dump_model_to_file(self, path: str): + """Dump the algorithm's trainable models to disk. + + Dump trainable models to the specified directory. The model file is always prefixed with the agent's name. + + Args: + path (str): path to the directory where the models are saved. + """ + torch.save(self.model.state_dict(), path) + + +class AgentGroup: + """Convenience wrapper of a set of agents that share the same underlying model. + + Args: + agent_dict (Union[AbsAgent, dict]): A single agent or a homogeneous set of agents that + share the same underlying model instance. + """ + + def __init__( + self, + agent_names: list, + agent_cls, + model, + algorithm_config, + generic_config: Union[GenericAgentConfig, Dict[str, GenericAgentConfig]], + experience_memory: Union[SimpleStore, Dict[str, SimpleStore]] = None + ): + self._members = agent_names + self._validate_agent_config(algorithm_config) + self._validate_agent_config(generic_config) + self.model = model + + def get_per_agent_obj(obj, agent_name): + return obj[agent_name] if isinstance(obj, dict) else obj + + self.agent_dict = { + name: agent_cls( + self.model, + get_per_agent_obj(algorithm_config, name), + get_per_agent_obj(generic_config, name), + experience_memory=get_per_agent_obj(experience_memory, name) + ) + for name in agent_names + } + + def __getitem__(self, agent_id): + if len(self.agent_dict) == 1: + return self.agent_dict["AGENT"] + else: + return self.agent_dict[agent_id] + + def __len__(self): + return len(self.agent_dict) + + @property + def members(self): + return self._members + + def choose_action(self, state_by_agent: dict): + return {agent_id: self.agent_dict[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} + + def set_exploration_params(self, params): + # Per-agent exploration parameters + if isinstance(params, dict) and params.keys() <= self.agent_dict.keys(): + for agent_id, params in params.items(): + self.agent_dict[agent_id].set_exploration_params(**params) + # Shared exploration parameters for all agents + else: + for agent in self.agent_dict.values(): + agent.set_exploration_params(**params) + + def learn(self, experiences: dict) -> set: + """Store experiences in the agents' experience memory. + + The top-level keys of ``experiences`` will be treated as agent IDs. + """ + return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].learn(exp)} + + def step(self): + for agent_id in agent_ids: + self.agent_dict[agent_id].step() + + def load_model(self, model): + """Load models from memory.""" + self.model.load_state_dict(model) + assert all(agent.model.state_dict() == self.model.state_dict() for agent in self.agent_dict.values()) + + def dump_model(self): + """Get agents' underlying models. + + This is usually used in distributed mode where models need to be broadcast to remote roll-out actors. + """ + return self.model.state_dict() + + def load_model_from_file(self, path): + """Load models from disk.""" + self.model.load_state_dict(torch.load(path)) + assert all(agent.model.state_dict() == self.model.state_dict() for agent in self.agent_dict.values()) + + def dump_model_to_file(self, path: str): + """Dump agents' models to disk. + + If the agents don't share models, each agent will use its own name to create a separate file under ``path`` + for dumping. + """ + torch.save(self.model.state_dict(), path) + + def _validate_agent_config(self, agent_config): + if isinstance(agent_config, dict): + expected_agent_names = set(self._members) + agent_names_in_config = set(agent_config.keys()) + if expected_agent_names != agent_names_in_config: + raise ValueError(f"Expected {expected_agent_names} as config keys, got {agent_names_in_config}") diff --git a/maro/rl/agent/agent_cls_index.py b/maro/rl/agent/agent_cls_index.py new file mode 100644 index 000000000..c1e4132cf --- /dev/null +++ b/maro/rl/agent/agent_cls_index.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac import ActorCritic, ActorCriticConfig +from .ddpg import DDPG, DDPGConfig +from .dqn import DQN, DQNConfig +from .pg import PolicyGradient, PolicyGradientConfig + + +AGENT_CLS = { + "ac": ActorCritic, + "ddpg": DDPG, + "dqn": DQN, + "pg": PolicyGradient +} + +AGENT_CONFIG = { + "ac": ActorCriticConfig, + "ddpg": DDPGConfig, + "dqn": DQNConfig, + "pg": PolicyGradientConfig +} diff --git a/maro/rl/agent/agent_manager.py b/maro/rl/agent/agent_manager.py new file mode 100644 index 000000000..c9c73e246 --- /dev/null +++ b/maro/rl/agent/agent_manager.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +from typing import Dict, Union + +from .agent import AbsAgent, AgentGroup + + +class AgentManager: + def __init__(self, child_agent_manager_dict: Dict[str, Union[AbsAgent, AgentGroup]]): + self.child_agent_manager_dict = child_agent_manager_dict + + def choose_action(self, state_dict: dict): + return {name: self.child_agent_manager_dict[name].choose_action(state) for name, state in state_dict.items()} + + def set_exploration_params(self, param_dict: dict): + if param_dict.keys() <= self.child_agent_manager_dict.keys(): + for name, param in param_dict.items(): + self.child_agent_manager_dict[name].set_exploration_params(param) + else: + for manager in self.child_agent_manager_dict.values(): + if isinstance(manager, AbsAgent): + manager.set_exploration_params(**param_dict) + else: + manager.set_exploration_params(param_dict) + + def learn(self, experience_dict: dict) -> set: + """Store experiences in the agents' experience memory. + + The top-level keys of ``experiences`` will be treated as child agent manager IDs. + """ + return {name: self.child_agent_manager_dict[name].learn(exp) for name, exp in experience_dict.items()} + + def step(self): + for manager in self.child_agent_manager_dict.values(): + manager.step() + + def load_model(self, model_dict: dict): + """Load models from memory.""" + for name, model in model_dict.items(): + self.child_agent_manager_dict[name].load_model(model) + + def dump_model(self): + """Get agents' underlying models. + + This is usually used in distributed mode where models need to be broadcast to remote roll-out actors. + """ + return {name: manager.dump_model() for name, manager in self.child_agent_manager_dict.items()} + + def load_model_from_file(self, dir_path): + """Load models from disk.""" + for name, manager in self.child_agent_manager_dict.items(): + manager.load_model_from_file(os.path.join(dir_path, name)) + + def dump_model_to_file(self, dir_path: str): + """Dump agents' models to disk. + + each agent will use its own name to create a separate file under ``path`` for dumping. + """ + for name, manager in self.child_agent_manager_dict.items(): + sub_dir = os.path.join(dir_path, name) + os.makedirs(sub_dir, exist_ok=True) + manager.dump_model_to_file(sub_dir) diff --git a/maro/rl/agent/ddpg.py b/maro/rl/agent/ddpg.py index 1822f4fa8..2b5030463 100644 --- a/maro/rl/agent/ddpg.py +++ b/maro/rl/agent/ddpg.py @@ -6,12 +6,14 @@ import numpy as np import torch +from maro.rl.cls_index import TORCH_LOSS_CLS from maro.rl.exploration import NoiseExplorer from maro.rl.model import SimpleMultiHeadModel -from maro.rl.utils import get_torch_loss_cls +from maro.rl.storage import SimpleStore +from maro.rl.utils import get_cls from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent +from .agent import AbsAgent, GenericAgentConfig class DDPGConfig: @@ -43,7 +45,7 @@ def __init__( ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq - self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() + self.q_value_loss_func = get_cls(q_value_loss_cls, TORCH_LOSS_CLS)() self.policy_loss_coefficient = policy_loss_coefficient self.soft_update_coefficient = soft_update_coefficient @@ -56,39 +58,24 @@ class DDPG(AbsAgent): https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg Args: - model (SimpleMultiHeadModel): DDPG policy and q-value models. - config (DDPGConfig): Configuration for DDPG algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. Defaults to False. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. + model (AbsCoreModel): Task model or container of task models required by the algorithm. + algorithm_config: Algorithm-specific configuration. + generic_config (GenericAgentConfig): Non-algorithm-specific configuration. + experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be + created at init time. Defaults to None. explorer (NoiseExplorer): An NoiseExplorer instance for generating exploratory actions. Defaults to None. """ def __init__( self, model: SimpleMultiHeadModel, - config: DDPGConfig, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool = False, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1, + algorithm_config, + generic_config: GenericAgentConfig, + experience_memory: SimpleStore = None, explorer: NoiseExplorer = None ): if model.task_names is None or set(model.task_names) != {"policy", "q_value"}: raise UnrecognizedTask(f"Expected model task names 'policy' and 'q_value', but got {model.task_names}") - super().__init__( - model, config, experience_memory_size, experience_memory_overwrite_type, - empty_experience_memory_after_step, - min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning - ) + super().__init__(model, algorithm_config, generic_config, experience_memory=experience_memory) self._explorer = explorer self._target_model = model.copy() if model.trainable else None self._train_cnt = 0 @@ -123,11 +110,11 @@ def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, nex next_q_values = self._target_model( torch.cat([next_states, next_actions], dim=1), task_name="q_value", training=False ).squeeze(1) # (N,) - target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) - q_value_loss = self.config.q_value_loss_func(current_q_values, target_q_values) + target_q_values = (rewards + self.algorithm_config.reward_discount * next_q_values).detach() # (N,) + q_value_loss = self.algorithm_config.q_value_loss_func(current_q_values, target_q_values) actions_from_model = self.model(states, task_name="policy") policy_loss = -self.model(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() - self.model.step(q_value_loss + self.config.policy_loss_coefficient * policy_loss) + self.model.step(q_value_loss + self.algorithm_config.policy_loss_coefficient * policy_loss) self._train_cnt += 1 - if self._train_cnt % self.config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.config.soft_update_coefficient) + if self._train_cnt % self.algorithm_config.target_update_freq == 0: + self._target_model.soft_update(self.model, self.algorithm_config.soft_update_coefficient) diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py index 7ff51109d..09c9bf2dd 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/agent/dqn.py @@ -6,11 +6,14 @@ import numpy as np import torch +from maro.rl.cls_index import SAMPLER_CLS, TORCH_LOSS_CLS from maro.rl.model import SimpleMultiHeadModel -from maro.rl.utils import get_max, get_sampler_cls, get_td_errors, get_torch_loss_cls, select_by_actions +from maro.rl.storage import SimpleStore +from maro.rl.utils import get_cls, get_max, get_td_errors, select_by_actions from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent +from .agent import AbsAgent, GenericAgentConfig +from .experience_enum import Experience class DQNConfig: @@ -59,13 +62,13 @@ def __init__( self.target_update_freq = target_update_freq self.train_iters = train_iters self.batch_size = batch_size - self.sampler_cls = get_sampler_cls(sampler_cls) + self.sampler_cls = get_cls(sampler_cls, SAMPLER_CLS) self.sampler_params = sampler_params if sampler_params else {} self.epsilon = epsilon self.soft_update_coefficient = soft_update_coefficient self.double = double self.advantage_type = advantage_type - self.loss_func = get_torch_loss_cls(loss_cls)() + self.loss_func = get_cls(loss_cls, TORCH_LOSS_CLS)() class DQN(AbsAgent): @@ -74,42 +77,29 @@ class DQN(AbsAgent): See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: - model (SimpleMultiHeadModel): Q-value model. - config (DQNConfig): Configuration for DQN algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. Defaults to False. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. + model (AbsCoreModel): Task model or container of task models required by the algorithm. + algorithm_config: Algorithm-specific configuration. + generic_config (GenericAgentConfig): Non-algorithm-specific configuration. + experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be + created at init time. Defaults to None. """ def __init__( self, model: SimpleMultiHeadModel, - config: DQNConfig, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool = False, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 + algorithm_config, + generic_config: GenericAgentConfig, + experience_memory: SimpleStore = None ): - if (config.advantage_type is not None and + if (algorithm_config.advantage_type is not None and (model.task_names is None or set(model.task_names) != {"state_value", "advantage"})): raise UnrecognizedTask( f"Expected model task names 'state_value' and 'advantage' since dueling DQN is used, " f"got {model.task_names}" ) - super().__init__( - model, config, experience_memory_size, experience_memory_overwrite_type, - empty_experience_memory_after_step, - min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning + super().__init__(model, algorithm_config, generic_config, experience_memory=experience_memory) + self._sampler = self.algorithm_config.sampler_cls( + self.experience_memory, **self.algorithm_config.sampler_params ) - self._sampler = self.config.sampler_cls(self.experience_memory, **self.config.sampler_params) self._training_counter = 0 self._target_model = model.copy() if model.trainable else None @@ -125,55 +115,63 @@ def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: num_actions = q_values.shape[1] greedy_action = q_values.argmax(dim=1).data.cpu() # No exploration - if self.config.epsilon == .0: + if self.algorithm_config.epsilon == .0: return greedy_action.item() if is_single else greedy_action.numpy() if is_single: - return greedy_action if np.random.random() > self.config.epsilon else np.random.choice(num_actions) + if np.random.random() > self.algorithm_config.epsilon: + return greedy_action + else: + return np.random.choice(num_actions) # batch inference return np.array([ - act if np.random.random() > self.config.epsilon else np.random.choice(num_actions) + act if np.random.random() > self.algorithm_config.epsilon else np.random.choice(num_actions) for act in greedy_action ]) def step(self): - for _ in range(self.config.train_iters): + for _ in range(self.algorithm_config.train_iters): # sample from the replay memory - indexes, batch = self._sampler.sample(self.config.batch_size) - states = torch.from_numpy(np.asarray(batch["S"])).to(self.device) - actions = torch.from_numpy(np.asarray(batch["A"])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch["R"])).to(self.device) - next_states = torch.from_numpy(np.asarray(batch["S_"])).to(self.device) + indexes, batch = self._sampler.sample(self.algorithm_config.batch_size) + states = torch.from_numpy(np.asarray(batch[Experience.STATE])).to(self.device) + actions = torch.from_numpy(np.asarray(batch[Experience.ACTION])).to(self.device) + rewards = torch.from_numpy(np.asarray(batch[Experience.REWARD])).to(self.device) + next_states = torch.from_numpy(np.asarray(batch[Experience.NEXT_STATE])).to(self.device) q_all = self._get_q_values(states) q = select_by_actions(q_all, actions) next_q_all_target = self._get_q_values(next_states, is_eval=False, training=False) - if self.config.double: + if self.algorithm_config.double: next_q_all_eval = self._get_q_values(next_states, training=False) next_q = select_by_actions(next_q_all_target, next_q_all_eval.max(dim=1)[1]) # (N,) else: next_q, _ = get_max(next_q_all_target) # (N,) - loss = get_td_errors(q, next_q, rewards, self.config.reward_discount, loss_func=self.config.loss_func) + loss = get_td_errors( + q, next_q, rewards, self.algorithm_config.reward_discount, loss_func=self.algorithm_config.loss_func + ) self.model.step(loss.mean()) self._training_counter += 1 - if self._training_counter % self.config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.config.soft_update_coefficient) + if self._training_counter % self.algorithm_config.target_update_freq == 0: + self._target_model.soft_update(self.model, self.algorithm_config.soft_update_coefficient) # update auxillary info for the next round of sampling self._sampler.update(indexes, loss.detach().numpy()) def set_exploration_params(self, epsilon): - self.config.epsilon = epsilon + self.algorithm_config.epsilon = epsilon def _get_q_values(self, states: torch.Tensor, is_eval: bool = True, training: bool = True): output = self.model(states, training=training) if is_eval else self._target_model(states, training=False) - if self.config.advantage_type is None: + if self.algorithm_config.advantage_type is None: return output else: state_values = output["state_value"] advantages = output["advantage"] # Use mean or max correction to address the identifiability issue - corrections = advantages.mean(1) if self.config.advantage_type == "mean" else advantages.max(1)[0] + if self.algorithm_config.advantage_type == "mean": + corrections = advantages.mean(1) + else: + corrections = advantages.max(1)[0] return state_values + advantages - corrections.unsqueeze(1) diff --git a/maro/rl/agent/experience_enum.py b/maro/rl/agent/experience_enum.py new file mode 100644 index 000000000..6c5bd2ced --- /dev/null +++ b/maro/rl/agent/experience_enum.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from enum import Enum + +class Experience(Enum): + STATE = "STATE" + ACTION = "ACTION" + REWARD = "REWARD" + NEXT_STATE = "NEXT_STATE" diff --git a/maro/rl/agent/multi_agent_wrapper.py b/maro/rl/agent/multi_agent_wrapper.py deleted file mode 100644 index 9245afe19..000000000 --- a/maro/rl/agent/multi_agent_wrapper.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -from typing import Union - -from .abs_agent import AbsAgent - - -class MultiAgentWrapper: - """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. - - Args: - agent_dict (Union[AbsAgent, dict]): A single agent or a homogeneous set of agents that have the same - method signatures. - """ - def __init__(self, agent_dict: Union[AbsAgent, dict]): - if isinstance(agent_dict, AbsAgent): - agent_dict = {"AGENT": agent_dict} - self.agent_dict = agent_dict - self._names = set(self.agent_dict.keys()) - - def __getitem__(self, agent_id): - if len(self.agent_dict) == 1: - return self.agent_dict["AGENT"] - else: - return self.agent_dict[agent_id] - - def __len__(self): - return len(self.agent_dict) - - @property - def names(self): - return self._names - - def choose_action(self, state_by_agent: dict): - return {agent_id: self.agent_dict[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} - - def set_exploration_params(self, params): - # Per-agent exploration parameters - if isinstance(params, dict) and params.keys() <= self.agent_dict.keys(): - for agent_id, params in params.items(): - self.agent_dict[agent_id].set_exploration_params(**params) - # Shared exploration parameters for all agents - else: - for agent in self.agent_dict.values(): - agent.set_exploration_params(**params) - - def learn(self, experiences: dict) -> set: - """Store experiences in the agents' experience memory. - - The top-level keys of ``experiences`` will be treated as agent IDs. - """ - return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].learn(exp)} - - def step(self, agent_ids=None): - if agent_ids is None: - for agent in self.agent_dict.values(): - agent.step() - elif not isinstance(agent_ids, set): - self.agent_dict[agent_ids].step() - else: - for agent_id in agent_ids: - self.agent_dict[agent_id].step() - - def load_model(self, model_dict: dict): - """Load models from memory.""" - for agent_id, model in model_dict.items(): - self.agent_dict[agent_id].load_model(model) - - def dump_model(self, agent_ids=None): - """Get agents' underlying models. - - This is usually used in distributed mode where models need to be broadcast to remote roll-out actors. - """ - if agent_ids is None: - return {agent_id: agent.dump_model() for agent_id, agent in self.agent_dict.items()} - elif not isinstance(agent_ids, set): - return self.agent_dict[agent_ids].dump_model() - else: - return {agent_id: self.agent_dict[agent_id].dump_model() for agent_id in agent_ids} - - def load_model_from_file(self, dir_path): - """Load models from disk.""" - for agent_id, agent in self.agent_dict.items(): - agent.load_model_from_file(os.path.join(dir_path, agent_id)) - - def dump_model_to_file(self, dir_path: str, agent_ids=None): - """Dump agents' models to disk. - - Each agent will use its own name to create a separate file under ``dir_path`` for dumping. - """ - os.makedirs(dir_path, exist_ok=True) - if agent_ids is None: - for agent_id, agent in self.agent_dict.items(): - agent.dump_model_to_file(os.path.join(dir_path, agent_id)) - elif not isinstance(agent_ids, set): - self.agent_dict[agent_ids].dump_model_to_file(os.path.join(dir_path, agent_ids)) - else: - for agent_id in agent_ids: - self.agent_dict[agent_id].dump_model_to_file(os.path.join(dir_path, agent_id)) - - def get_versions(self, agent_ids=None): - if agent_ids is None: - return {agent_id: agent.version for agent_id, agent in self.agent_dict.items()} - elif not isinstance(agent_ids, set): - return self.agent_dict[agent_ids].version - else: - return {agent_id: self.agent_dict[agent_id].version for agent_id in agent_ids} - - def set_versions(self, version_index_by_agent: dict): - for agent_id, version_index in version_index_by_agent.items(): - self.agent_dict[agent_id].version = version_index diff --git a/maro/rl/agent/pg.py b/maro/rl/agent/pg.py index 821f79287..0967dda6a 100644 --- a/maro/rl/agent/pg.py +++ b/maro/rl/agent/pg.py @@ -8,9 +8,10 @@ from torch.distributions import Categorical from maro.rl.model import SimpleMultiHeadModel +from maro.rl.storage import SimpleStore from maro.rl.utils import get_truncated_cumulative_reward -from .abs_agent import AbsAgent +from .agent import AbsAgent, GenericAgentConfig class PolicyGradientConfig: @@ -31,37 +32,12 @@ class PolicyGradient(AbsAgent): Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. Args: - model (SimpleMultiHeadModel): Multi-task model that computes action distributions and state values. - It may or may not have a shared bottom stack. - config (PolicyGradientConfig): Configuration for the PG algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. Defaults to True. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. + model (AbsCoreModel): Task model or container of task models required by the algorithm. + algorithm_config: Algorithm-specific configuration. + generic_config (GenericAgentConfig): Non-algorithm-specific configuration. + experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be + created at init time. Defaults to None. """ - def __init__( - self, - model: SimpleMultiHeadModel, - config: PolicyGradientConfig, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool = True, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 - ): - super().__init__( - model, config, experience_memory_size, experience_memory_overwrite_type, - empty_experience_memory_after_step, - min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning - ) - def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Use the actor (policy) model to generate stochastic actions. diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index f037df84a..51db7ec4f 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -5,7 +5,7 @@ from typing import Union from maro.communication import Proxy -from maro.rl.agent import AbsAgent, MultiAgentWrapper +from maro.rl.agent import AbsAgent, AgentManager from maro.rl.training import AbsEnvWrapper from maro.utils import Logger @@ -18,7 +18,7 @@ class Actor(object): Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. + agent (Union[AbsAgent, AgentManager]): Agent that interacts with the environment. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class @@ -27,14 +27,14 @@ class Actor(object): def __init__( self, env: AbsEnvWrapper, - agent: Union[AbsAgent, MultiAgentWrapper], + agent: Union[AbsAgent, AgentManager], group: str, proxy_options: dict = None, pull_experiences_with_copy: bool = False, log_dir: str = getcwd() ): self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent + self.agent = AgentManager(agent) if isinstance(agent, AbsAgent) else agent self._pull_experiences_with_copy = pull_experiences_with_copy if proxy_options is None: proxy_options = {} diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py index a7f3d30f4..f5f11fd7d 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/distributed/actor_manager.py @@ -15,8 +15,6 @@ class ActorManager(object): """Learner class for distributed training. Args: - agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. - scheduler (Scheduler): . num_actors (int): Expected number of actors in the group identified by ``group_name``. group_name (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the actors (and roll-out clients, if any). diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 173e3f3c2..375f3d858 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -7,7 +7,7 @@ from typing import Union from maro.communication import Message, Proxy, SessionType -from maro.rl.agent import AbsAgent, MultiAgentWrapper +from maro.rl.agent import AbsAgent, AgentManager from maro.rl.scheduling import Scheduler from maro.utils import Logger @@ -19,12 +19,12 @@ class DistLearner(object): """Learner class for distributed training. Args: - agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. + agent (Union[AbsAgent, AgentManager]): Learning agents. scheduler (Scheduler): A ``Scheduler`` instance for generating exploration parameters. """ def __init__( self, - agent: Union[AbsAgent, MultiAgentWrapper], + agent: Union[AbsAgent, AgentManager], scheduler: Scheduler, actor_manager: ActorManager, agent_update_interval: int = -1, @@ -33,7 +33,7 @@ def __init__( log_dir: str = getcwd() ): super().__init__() - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent + self.agent = AgentManager(agent) if isinstance(agent, AbsAgent) else agent self.scheduler = scheduler self.actor_manager = actor_manager self.agent_update_interval = agent_update_interval diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 00c54829d..7c6ab4129 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -4,9 +4,11 @@ from .abs_block import AbsBlock from .fc_block import FullyConnectedBlock from .learning_model import AbsCoreModel, OptimOption, SimpleMultiHeadModel +from .torch_cls_index import TORCH_ACTIVATION_CLS, TORCH_LR_SCHEDULER_CLS, TORCH_OPTIM_CLS __all__ = [ "AbsBlock", "FullyConnectedBlock", - "AbsCoreModel", "OptimOption", "SimpleMultiHeadModel" + "AbsCoreModel", "OptimOption", "SimpleMultiHeadModel", + "TORCH_ACTIVATION_CLS", "TORCH_LR_SCHEDULER_CLS", "TORCH_OPTIM_CLS" ] diff --git a/maro/rl/model/fc_block.py b/maro/rl/model/fc_block.py index a648a3fd3..ba659186e 100644 --- a/maro/rl/model/fc_block.py +++ b/maro/rl/model/fc_block.py @@ -6,7 +6,8 @@ import torch import torch.nn as nn -from maro.rl.utils import get_torch_activation_cls +from maro.rl.cls_index import TORCH_ACTIVATION_CLS +from maro.rl.utils import get_cls from .abs_block import AbsBlock @@ -53,7 +54,7 @@ def __init__( self._output_dim = output_dim # network features - self._activation = get_torch_activation_cls(activation)() if activation else None + self._activation = get_cls(activation, TORCH_ACTIVATION_CLS)() if activation else None self._head = head self._softmax = nn.Softmax(dim=1) if softmax else None self._batch_norm = batch_norm diff --git a/maro/rl/model/learning_model.py b/maro/rl/model/learning_model.py index 68a46d5f2..a07eede8e 100644 --- a/maro/rl/model/learning_model.py +++ b/maro/rl/model/learning_model.py @@ -7,7 +7,8 @@ import torch import torch.nn as nn -from maro.rl.utils import get_torch_lr_scheduler_cls, get_torch_optim_cls +from maro.rl.cls_index import TORCH_LR_SCHEDULER_CLS, TORCH_OPTIM_CLS +from maro.rl.utils import get_cls from maro.utils import clone from maro.utils.exception.rl_toolkit_exception import MissingOptimizer @@ -26,9 +27,9 @@ class OptimOption: __slots__ = ["optim_cls", "optim_params", "scheduler_cls", "scheduler_params"] def __init__(self, optim_cls, optim_params: dict, scheduler_cls=None, scheduler_params: dict = None): - self.optim_cls = get_torch_optim_cls(optim_cls) + self.optim_cls = get_cls(optim_cls, TORCH_OPTIM_CLS) self.optim_params = optim_params - self.scheduler_cls = get_torch_lr_scheduler_cls(scheduler_cls) + self.scheduler_cls = get_cls(scheduler_cls, TORCH_LR_SCHEDULER_CLS) self.scheduler_params = scheduler_params diff --git a/maro/rl/storage/__init__.py b/maro/rl/storage/__init__.py index 8b8173d2b..802ccc2d5 100644 --- a/maro/rl/storage/__init__.py +++ b/maro/rl/storage/__init__.py @@ -3,6 +3,7 @@ from .abs_store import AbsStore from .sampler import AbsSampler, UniformSampler +from .sampler_cls_index import SAMPLER_CLS from .simple_store import SimpleStore -__all__ = ["AbsSampler", "AbsStore", "SimpleStore", "UniformSampler"] +__all__ = ["SAMPLER_CLS", "AbsSampler", "AbsStore", "SimpleStore", "UniformSampler"] diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index 9246fa951..7d8e0d85b 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -7,6 +7,7 @@ from typing import List, Union from maro.simulator import Env +from maro.rl.agent import Experience class AbsEnvWrapper(ABC): @@ -23,14 +24,16 @@ class AbsEnvWrapper(ABC): """ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): self.env = env - self._step_index = None - self.replay = defaultdict(lambda: defaultdict(list)) + self.replay = {} self.state_info = None # context for converting model output to actions that can be executed by the env self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay + self.pending_reward_ticks = deque() # list of ticks whose actions have not been given a reward + self.action_history = {} # store the tick-to-action mapping + self._step_index = None self._total_reward = 0 + self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. self._state = None # the latest extracted state is kept here - self._acting_agents = deque() # list of (tick, acting_agent_list) for delayed reward evaluation @property def step_index(self): @@ -42,27 +45,10 @@ def agent_idx_list(self): def start(self, rollout_index: int = None): self._step_index = 0 - _, event, _ = self.env.step(None) - self._state = self.get_state(event) + _, self._event, _ = self.env.step(None) + self._state = self.get_state(self.env.tick) if self.save_replay: - for agent_id, state in self._state.items(): - replay = self.replay[agent_id] - if replay["S"]: - replay["S_"].append(state) - replay["S"].append(state) - assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 - - def pull_experiences(self, copy: bool = False): - experience, num_experiences = defaultdict(dict), 0 - for agent_id, replay in self.replay.items(): - num_complete = min(len(replay["R"]), len(replay["S_"])) - num_experiences += num_complete - for k, vals in replay.items(): - experience[agent_id][k] = vals[:num_complete] - if not copy: - del vals[:num_complete] - - return experience, num_experiences + self._record_transition_obj(self._state, Experience.STATE) @property def metrics(self): @@ -72,12 +58,22 @@ def metrics(self): def state(self): return self._state + @property + def event(self): + return self._event + @property def total_reward(self): return self._total_reward @abstractmethod - def get_state(self, event) -> dict: + def get_state(self, tick) -> dict: + """Compute the state for a given tick. + + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + """ pass @abstractmethod @@ -85,13 +81,12 @@ def get_action(self, action) -> dict: pass @abstractmethod - def get_reward(self, tick: int = None, target_agents: list = None) -> dict: + def get_reward(self, tick: int = None) -> dict: """User-defined reward evaluation. Args: tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed reward evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. - targets_agents (list): If given, rewards will be given only to these agents. Defaults to None. """ pass @@ -99,57 +94,89 @@ def step(self, action_by_agent: dict): # t0 = time.time() self._step_index += 1 env_action = self.get_action(action_by_agent) - self._acting_agents.append((self.env.tick, list(action_by_agent.keys()))) + self.pending_reward_ticks.append(self.env.tick) + self.action_history[self.env.tick] = env_action if len(env_action) == 1: env_action = list(env_action.values())[0] # t1 = time.time() - _, event, done = self.env.step(env_action) + _, self._event, done = self.env.step(env_action) # t2 = time.time() # self._tot_raw_step_time += t2 - t1 if self.save_replay: - for agent_id, action in action_by_agent.items(): - self.replay[agent_id]["A"].append(action) + self._record_transition_obj(action_by_agent, Experience.ACTION) """ If roll-out is complete, evaluate rewards for all remaining events except the last. Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ while ( - self._acting_agents and - (done or self.env.tick - self._acting_agents[0][0] >= self.reward_eval_delay) + self.pending_reward_ticks and + (done or self.env.tick - self.pending_reward_ticks[0] >= self.reward_eval_delay) ): - reward = self.get_reward(tick=self._acting_agents[0][0], target_agents=self._acting_agents[0][1]) - # assign rewards to the relevant agents - for agent_id in self._acting_agents[0][1]: - rw = reward.get(agent_id, 0) - self.replay[agent_id]["R"].append(rw) - self._total_reward += rw - self._acting_agents.popleft() + tick = self.pending_reward_ticks.popleft() + reward = self.get_reward(tick=tick) + self._record_transition_obj(reward, Experience.REWARD) + # assign rewards to the agents that took action at that tick if not done: - self._state = self.get_state(event) + self._state = self.get_state(self.env.tick) if self.save_replay: - for agent_id, state in self._state.items(): - replay = self.replay[agent_id] - if replay["S"]: - replay["S_"].append(state) - replay["S"].append(state) - assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 + self._record_transition_obj(self._state, Experience.STATE) + self._record_transition_obj(self._state, Experience.NEXT_STATE) # t3 = time.time() # self._tot_step_time += t3 - t0 else: self._state = None + self.process_replay_memory() + self.end_ep_callback() # print(f"total raw step time: {self._tot_raw_step_time}") # print(f"total step time: {self._tot_step_time}") # self._tot_raw_step_time = 0 # self._tot_step_time = 0 + def process_replay_memory(self): + def delete_incomplete_experieces(mem): + if Experience.REWARD in mem: + num_complete = min(len(mem[Experience.REWARD]), len(mem[Experience.NEXT_STATE])) + for key, vals in mem.items(): + del vals[num_complete:] + else: + for child_mem in mem.values(): + delete_incomplete_experieces(child_mem) + + delete_incomplete_experieces(self.replay) + + def end_ep_callback(self): + pass + def reset(self): self.env.reset() self.state_info = None self._total_reward = 0 self._state = None - self._acting_agents.clear() - self.replay = defaultdict(lambda: defaultdict(list)) + self.pending_reward_ticks.clear() + self.action_history.clear() + self.replay = {} + + def _record_transition_obj(self, obj, base_key: str): + def store_multi_level_obj(obj, mem, base_key: str): + if not isinstance(obj, dict): + mem.setdefault(base_key, []) + mem[base_key].append(obj) + else: + for key, child_obj in obj.items(): + mem.setdefault(key, {}) + store_multi_level_obj(child_obj, mem[key], base_key) + + store_multi_level_obj(obj, self.replay, base_key) + + def replay_info(self): + def helper(mem): + if Experience.REWARD in mem: + return {key: len(vals) for key, vals in mem.items()} + else: + return {key: helper(child_mem) for key, child_mem in mem.items()} + + return helper(self.replay) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 92f3b9d92..8d4579f95 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -5,7 +5,7 @@ from collections import defaultdict from typing import Dict, Union -from maro.rl.agent import AbsAgent, MultiAgentWrapper +from maro.rl.agent import AbsAgent, AgentManager from maro.rl.scheduling import Scheduler from maro.utils import InternalLogger @@ -18,12 +18,12 @@ class Learner(object): Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. + agent (Union[AbsAgent, AgentManager]): Agent that interacts with the environment. """ def __init__( self, env: AbsEnvWrapper, - agent: Union[AbsAgent, MultiAgentWrapper], + agent: Union[AbsAgent, AgentManager], scheduler: Scheduler, agent_update_interval: int = -1, log_env_metrics: bool = False @@ -32,7 +32,7 @@ def __init__( if agent_update_interval == 0: raise ValueError("agent_update_interval must be a positive integer or None.") self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent + self.agent = AgentManager(agent) if isinstance(agent, AbsAgent) else agent self.scheduler = scheduler self.agent_update_interval = agent_update_interval self.total_env_steps = 0 @@ -56,16 +56,16 @@ def run(self): not self.env.state or self.agent_update_interval != -1 and self.env.step_index % self.agent_update_interval == 0 ): - exp, num_exp = self.env.pull_experiences() + self.env.process_replay_memory() tl0 = time.time() - self.agent.learn(exp) + # print(self.env.replay_info()) + self.agent.learn(self.env.replay) self.total_learning_time += time.time() - tl0 self.total_env_steps += self.agent_update_interval - self.total_experiences_collected += num_exp self._logger.debug(f"total running time: {time.time() - t0}") self._logger.debug(f"total learning time: {self.total_learning_time}") self._logger.debug(f"total env steps: {self.total_env_steps}") - self._logger.info(f"total experiences collected: {self.total_experiences_collected}") + # self._logger.info(f"total experiences collected: {self.total_experiences_collected}") if not self.env.state: self._logger.info(f"total reward: {self.env.total_reward}") diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 69a92edb2..5c2808dbc 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,16 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .sampler_cls_index import get_sampler_cls -from .torch_cls_index import ( - get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls -) +from .get_cls import get_cls from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_truncated_cumulative_reward from .value_utils import get_log_prob, get_max, get_td_errors, select_by_actions __all__ = [ - "get_sampler_cls", - "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", + "get_cls", "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", "get_log_prob", "get_max", "get_td_errors", "select_by_actions" ] diff --git a/maro/rl/utils/get_cls.py b/maro/rl/utils/get_cls.py new file mode 100644 index 000000000..4b649cc74 --- /dev/null +++ b/maro/rl/utils/get_cls.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +def get_cls(cls_type, index: dict): + if isinstance(cls_type, str): + if cls_type not in index: + raise KeyError(f"A string sampler_type must be one of {list(cls_type.keys())}.") + return index[cls_type] + + return cls_type diff --git a/maro/rl/utils/sampler_cls_index.py b/maro/rl/utils/sampler_cls_index.py deleted file mode 100644 index 1910fe951..000000000 --- a/maro/rl/utils/sampler_cls_index.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.rl.storage import UniformSampler - -SAMPLER = { - "uniform": UniformSampler, -} - -def get_sampler_cls(sampler_type): - if isinstance(sampler_type, str): - if sampler_type not in SAMPLER: - raise KeyError(f"A string sampler_type must be one of {list(SAMPLER.keys())}.") - return SAMPLER[sampler_type] - - return sampler_type diff --git a/maro/rl/utils/torch_cls_index.py b/maro/rl/utils/torch_cls_index.py deleted file mode 100644 index 428b3879d..000000000 --- a/maro/rl/utils/torch_cls_index.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from torch import nn, optim -from torch.optim import lr_scheduler - -# For details, see https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity -TORCH_ACTIVATION = { - "elu": nn.ELU, - "hard_shrink": nn.Hardshrink, - "hard_sigmoid": nn.Hardsigmoid, - "hard_tanh": nn.Hardtanh, - "hardswish": nn.Hardswish, - "leaky_relu": nn.LeakyReLU, - "log_sigmoid": nn.LogSigmoid, - "multihead_attention": nn.MultiheadAttention, - "prelu": nn.PReLU, - "relu": nn.ReLU, - "relu6": nn.ReLU6, - "rrelu": nn.RReLU, - "selu": nn.SELU, - "celu": nn.CELU, - "gelu": nn.GELU, - "sigmoid": nn.Sigmoid, - "soft_plus": nn.Softplus, - "soft_shrink": nn.Softshrink, - "soft_sign": nn.Softsign, - "tanh": nn.Tanh, - "tanh_shrink": nn.Tanhshrink, - "threshold": nn.Threshold -} - -# For details, see https://pytorch.org/docs/stable/nn.html#loss-functions -TORCH_LOSS = { - "l1": nn.L1Loss, - "mse": nn.MSELoss, - "cross_entropy": nn.CrossEntropyLoss, - "ctc": nn.CTCLoss, - "nll": nn.NLLLoss, - "poisson_nll": nn.PoissonNLLLoss, - "kl": nn.KLDivLoss, - "bce": nn.BCELoss, - "bce_logits": nn.BCEWithLogitsLoss, - "margin_ranking": nn.MarginRankingLoss, - "hinge_embedding": nn.HingeEmbeddingLoss, - "multi_label_margin": nn.MultiLabelMarginLoss, - "multi_label_soft_margin": nn.MultiLabelSoftMarginLoss, - "smooth_l1": nn.SmoothL1Loss, - "soft_margin": nn.SoftMarginLoss, - "cosine_embedding": nn.CosineEmbeddingLoss, - "multi_margin": nn.MultiMarginLoss, - "triplet_margin": nn.TripletMarginLoss, -} - -# For details, see https://pytorch.org/docs/stable/optim.html -TORCH_OPTIM = { - "sgd": optim.SGD, - "asgd": optim.ASGD, - "adadelta": optim.Adadelta, - "adagrad": optim.Adagrad, - "adam": optim.Adam, - "adamax": optim.Adamax, - "adamw": optim.AdamW, - "sparse_adam": optim.SparseAdam, - "lbfgs": optim.LBFGS, - "rmsprop": optim.RMSprop, - "rprop": optim.Rprop -} - -# For details, see https://pytorch.org/docs/stable/optim.html -TORCH_LR_SCHEDULER = { - "lambda": lr_scheduler.LambdaLR, - "multiplicative": lr_scheduler.MultiplicativeLR, - "step": lr_scheduler.StepLR, - "multi_step": lr_scheduler.MultiStepLR, - "exponential": lr_scheduler.ExponentialLR, - "cosine_annealing": lr_scheduler.CosineAnnealingLR, - "reduce_on_plateau": lr_scheduler.ReduceLROnPlateau, - "cyclic": lr_scheduler.CyclicLR, - "one_cycle": lr_scheduler.OneCycleLR, - "cosine_annealing_warm_restarts": lr_scheduler.CosineAnnealingWarmRestarts -} - - -def get_torch_activation_cls(activation_type): - if isinstance(activation_type, str): - if activation_type not in TORCH_ACTIVATION: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_ACTIVATION.keys())}.") - return TORCH_ACTIVATION[activation_type] - - return activation_type - - -def get_torch_loss_cls(loss_type): - if isinstance(loss_type, str): - if loss_type not in TORCH_LOSS: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_LOSS.keys())}.") - return TORCH_LOSS[loss_type] - - return loss_type - - -def get_torch_optim_cls(optim_type): - if isinstance(optim_type, str): - if optim_type not in TORCH_OPTIM: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_OPTIM.keys())}.") - return TORCH_OPTIM[optim_type] - - return optim_type - - -def get_torch_lr_scheduler_cls(lr_scheduler_type): - if isinstance(lr_scheduler_type, str): - if lr_scheduler_type not in TORCH_LR_SCHEDULER: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_LR_SCHEDULER.keys())}.") - return TORCH_LR_SCHEDULER[lr_scheduler_type] - - return lr_scheduler_type diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 1b5f9802e..ab42251de 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -322,12 +322,12 @@ ], "source": [ "from maro.simulator import Env\n", - "from maro.rl import Actor, MultiAgentWrapper, OnPolicyLearner\n", + "from maro.rl import Actor, AgentManager, OnPolicyLearner\n", "from maro.utils import set_seeds\n", "\n", "set_seeds(1024) # for reproducibility\n", "env = Env(\"cim\", \"toy.4p_ssdd_l0.0\", durations=1120)\n", - "agent = MultiAgentWrapper({name: get_ac_agent() for name in env.agent_idx_list})\n", + "agent = AgentManager({name: get_ac_agent() for name in env.agent_idx_list})\n", "actor = Actor(env, agent, CIMTrajectory, trajectory_kwargs=common_config)\n", "learner = OnPolicyLearner(actor, 40) # 40 episodes\n", "learner.run()" From a824ea8872865b58a2bf2c9c00bdfea9a8ea68da Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 19 Apr 2021 08:19:16 +0000 Subject: [PATCH 201/482] rl refinement in progress --- docs/source/key_components/rl_toolkit.rst | 2 +- examples/supply_chain/env_wrapper.py | 6 + maro/rl/__init__.py | 10 +- maro/rl/agent/agent.py | 5 + maro/rl/{agent => algorithm}/__init__.py | 9 +- .../abs_algorithm.py} | 51 +------- maro/rl/{agent => algorithm}/ac.py | 53 ++++---- .../agent_manager.py} | 23 ++-- maro/rl/{agent => algorithm}/ddpg.py | 4 +- maro/rl/{agent => algorithm}/dqn.py | 98 ++++++-------- maro/rl/{agent => algorithm}/pg.py | 14 +- maro/rl/algorithm/torch_loss_cls_index.py | 26 ++++ maro/rl/distributed/actor.py | 8 +- maro/rl/distributed/actor_manager.py | 2 +- maro/rl/distributed/learner.py | 8 +- maro/rl/model/__init__.py | 7 +- .../{learning_model.py => core_model.py} | 120 ++++++++++-------- maro/rl/model/torch_cls_index.py | 57 +++++++++ maro/rl/storage/sampler_cls_index.py | 8 ++ maro/rl/training/env_wrapper.py | 83 ++++++------ maro/rl/training/learner.py | 8 +- maro/rl/utils/value_utils.py | 39 ------ maro/utils/exception/error_code.py | 1 - maro/utils/exception/rl_toolkit_exception.py | 6 - 24 files changed, 332 insertions(+), 316 deletions(-) create mode 100644 maro/rl/agent/agent.py rename maro/rl/{agent => algorithm}/__init__.py (67%) rename maro/rl/{agent/abs_agent.py => algorithm/abs_algorithm.py} (67%) rename maro/rl/{agent => algorithm}/ac.py (75%) rename maro/rl/{agent/multi_agent_wrapper.py => algorithm/agent_manager.py} (89%) rename maro/rl/{agent => algorithm}/ddpg.py (99%) rename maro/rl/{agent => algorithm}/dqn.py (64%) rename maro/rl/{agent => algorithm}/pg.py (89%) create mode 100644 maro/rl/algorithm/torch_loss_cls_index.py rename maro/rl/model/{learning_model.py => core_model.py} (67%) create mode 100644 maro/rl/model/torch_cls_index.py create mode 100644 maro/rl/storage/sampler_cls_index.py delete mode 100644 maro/rl/utils/value_utils.py diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index e1d8b6e6d..f78cecfe7 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -25,7 +25,7 @@ This decoupling is achieved by the Core Model abstraction described below. .. code-block:: python - class AbsAgent(ABC): + class AbsAlgorithm(ABC): def __init__(self, model: AbsCoreModel, config, experience_pool=None): self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.model = model.to(self.device) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 8008dd4da..7f136094c 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -164,6 +164,12 @@ def dim(self): return self._dim + def get_state_a(self): + pass + + def get_state_b(self): + pass + def get_state(self, event): cur_tick = self.env.tick settings: dict = self.env.configs.settings diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index aedb7b6fe..3117d3d83 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -1,9 +1,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.agent import ( - DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient, - PolicyGradientConfig +from maro.rl.algorithm import ( + DDPG, DQN, TORCH_LOSS_CLS, AbsAlgorithm, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, + PolicyGradient, PolicyGradientConfig ) from maro.rl.distributed import Actor, ActorManager, DistLearner from maro.rl.exploration import ( @@ -20,8 +20,8 @@ ) __all__ = [ - "AbsAgent", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", - "PolicyGradient", "PolicyGradientConfig", + "AbsAlgorithm", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", + "PolicyGradient", "PolicyGradientConfig", "TORCH_LOSS_CLS", "Actor", "ActorManager", "DistLearner", "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", diff --git a/maro/rl/agent/agent.py b/maro/rl/agent/agent.py new file mode 100644 index 000000000..b1eb5179c --- /dev/null +++ b/maro/rl/agent/agent.py @@ -0,0 +1,5 @@ + + + + def __init__(self, algorithm_cls, model, experience_memory): + self.agent \ No newline at end of file diff --git a/maro/rl/agent/__init__.py b/maro/rl/algorithm/__init__.py similarity index 67% rename from maro/rl/agent/__init__.py rename to maro/rl/algorithm/__init__.py index ee082cd64..6ac0616a0 100644 --- a/maro/rl/agent/__init__.py +++ b/maro/rl/algorithm/__init__.py @@ -1,18 +1,19 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .abs_agent import AbsAgent +from .abs_algorithm import AbsAlgorithm from .ac import ActorCritic, ActorCriticConfig from .ddpg import DDPG, DDPGConfig from .dqn import DQN, DQNConfig -from .multi_agent_wrapper import MultiAgentWrapper from .pg import PolicyGradient, PolicyGradientConfig +from .torch_loss_cls_index import TORCH_LOSS_CLS __all__ = [ - "AbsAgent", + "AbsAlgorithm", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", - "PolicyGradient", "PolicyGradientConfig" + "PolicyGradient", "PolicyGradientConfig", + "TORCH_LOSS_CLS" ] diff --git a/maro/rl/agent/abs_agent.py b/maro/rl/algorithm/abs_algorithm.py similarity index 67% rename from maro/rl/agent/abs_agent.py rename to maro/rl/algorithm/abs_algorithm.py index 83eeca9c3..80a0ede30 100644 --- a/maro/rl/agent/abs_agent.py +++ b/maro/rl/algorithm/abs_algorithm.py @@ -9,8 +9,8 @@ from maro.rl.storage import SimpleStore -class AbsAgent(ABC): - """Abstract RL agent class. +class AbsAlgorithm(ABC): + """Abstract RL algorithm class. It's a sandbox for the RL algorithm. Scenario-specific details will be excluded. We focus on the abstraction algorithm development here. Environment observation and decision events will @@ -21,10 +21,7 @@ class AbsAgent(ABC): Args: model (AbsCoreModel): Task model or container of task models required by the algorithm. config: Settings for the algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". + experience_memory (SimpleStore): Experience memory for storing experiences empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call to ``step``. min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. @@ -50,20 +47,6 @@ def __init__( self.empty_experience_memory_after_step = empty_experience_memory_after_step self.min_new_experiences_to_trigger_learning = min_new_experiences_to_trigger_learning self.min_experiences_to_trigger_learning = min_experiences_to_trigger_learning - self.device = torch.device('cpu') - self._version_index = 0 - - @property - def version(self): - return self._version_index - - @version.setter - def version(self, version_index): - self._version_index = version_index - - def to_device(self, device): - self.device = device - self.model = self.model.to(device) @abstractmethod def choose_action(self, state): @@ -105,31 +88,3 @@ def step(self): should be reflected here. """ return NotImplementedError - - def load_model(self, model): - """Load models from memory.""" - self.model.load_state_dict(model) - - def dump_model(self): - """Return the algorithm's trainable models.""" - return self.model.state_dict() - - def load_model_from_file(self, path: str): - """Load trainable models from disk. - - Load trainable models from the specified directory. The model file is always prefixed with the agent's name. - - Args: - path (str): path to the directory where the models are saved. - """ - self.model.load_state_dict(torch.load(path)) - - def dump_model_to_file(self, path: str): - """Dump the algorithm's trainable models to disk. - - Dump trainable models to the specified directory. The model file is always prefixed with the agent's name. - - Args: - path (str): path to the directory where the models are saved. - """ - torch.save(self.model.state_dict(), path) diff --git a/maro/rl/agent/ac.py b/maro/rl/algorithm/ac.py similarity index 75% rename from maro/rl/agent/ac.py rename to maro/rl/algorithm/ac.py index bfaaf53d0..5ce384612 100644 --- a/maro/rl/agent/ac.py +++ b/maro/rl/algorithm/ac.py @@ -7,11 +7,11 @@ import torch from torch.distributions import Categorical -from maro.rl.model import SimpleMultiHeadModel +from maro.rl.model import ParameterizedPolicyWithValueEstimator from maro.rl.utils import get_log_prob, get_torch_loss_cls from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent +from .abs_agent import AbsAlgorithm class ActorCriticConfig: @@ -44,7 +44,7 @@ def __init__( self.clip_ratio = clip_ratio -class ActorCritic(AbsAgent): +class ActorCritic(AbsAlgorithm): """Actor Critic algorithm with separate policy and value models. References: @@ -52,8 +52,8 @@ class ActorCritic(AbsAgent): https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: - model (SimpleMultiHeadModel): Multi-task model that computes action distributions and state values. - It may or may not have a shared bottom stack. + model (ParameterizedPolicyWithValueEstimator): Multi-task model that computes action distributions and + state values. It may or may not have a shared bottom stack. config: Configuration for the AC algorithm. experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of unlimited size. @@ -68,7 +68,7 @@ class ActorCritic(AbsAgent): """ def __init__( self, - model: SimpleMultiHeadModel, + model: ParameterizedPolicyWithValueEstimator, config: ActorCriticConfig, experience_memory_size: int, experience_memory_overwrite_type: str, @@ -76,8 +76,9 @@ def __init__( min_new_experiences_to_trigger_learning: int = 1, min_experiences_to_trigger_learning: int = 1 ): - if model.task_names is None or set(model.task_names) != {"actor", "critic"}: - raise UnrecognizedTask(f"Expected model task names 'actor' and 'critic', but got {model.task_names}") + if not isinstance(model, ParameterizedPolicyWithValueEstimator): + raise TypeError("model must be an instance of 'ParameterizedPolicyWithValueEstimator'") + super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, @@ -94,32 +95,29 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: Returns: Actions and corresponding log probabilities. """ - state = torch.from_numpy(state).to(self.device) - is_single = len(state.shape) == 1 - if is_single: - state = state.unsqueeze(dim=0) - - action_prob = Categorical(self.model(state, task_name="actor", training=False)) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - action, log_p = action.cpu().numpy(), log_p.cpu().numpy() - return (action[0], log_p[0]) if is_single else (action, log_p) + with torch.no_grad(): + actions, log_p = self.model.choose_action() + actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() + if len(actions) == 1: + return actions[0], log_p[0] + else: + return actions, log_p def step(self): batch = self.experience_memory.get() - states = torch.from_numpy(np.asarray(batch["S"])).to(self.device) - actions = torch.from_numpy(np.asarray([act[0] for act in batch["A"]])).to(self.device) - log_p = torch.from_numpy(np.asarray([act[1] for act in batch["A"]])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch["R"])).to(self.device) - next_states = torch.from_numpy(np.asarray(batch["S_"])).to(self.device) - - state_values = self.model(states, task_name="critic").detach().squeeze() - next_state_values = self.model(next_states, task_name="critic").detach().squeeze() + states, next_states = batch["S"], batch["S_"] + actions = torch.from_numpy(np.asarray([act[0] for act in batch["A"]])) + log_p = torch.from_numpy(np.asarray([act[1] for act in batch["A"]])) + rewards = torch.from_numpy(np.asarray(batch["R"])) + + state_values = self.model(states, output_action_probs=False).detach().squeeze() + next_state_values = self.model(next_states, output_action_probs=False).detach().squeeze() return_est = rewards + self.config.reward_discount * next_state_values advantages = return_est - state_values for i in range(self.config.train_iters): # actor loss - log_p_new = get_log_prob(self.model(states, task_name="actor"), actions) + action_prob, state_values = self.model(states) + log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) if self.config.clip_ratio is not None: ratio = torch.exp(log_p_new - log_p) clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) @@ -128,7 +126,6 @@ def step(self): actor_loss = -(log_p_new * advantages).mean() # critic_loss - state_values = self.model(states, task_name="critic").squeeze() critic_loss = self.config.critic_loss_func(state_values, return_est) loss = critic_loss + self.config.actor_loss_coefficient * actor_loss diff --git a/maro/rl/agent/multi_agent_wrapper.py b/maro/rl/algorithm/agent_manager.py similarity index 89% rename from maro/rl/agent/multi_agent_wrapper.py rename to maro/rl/algorithm/agent_manager.py index 9245afe19..6768ea7fe 100644 --- a/maro/rl/agent/multi_agent_wrapper.py +++ b/maro/rl/algorithm/agent_manager.py @@ -4,21 +4,28 @@ import os from typing import Union -from .abs_agent import AbsAgent +from .abs_agent import AbsAlgorithm -class MultiAgentWrapper: +class AgentManager: """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. Args: - agent_dict (Union[AbsAgent, dict]): A single agent or a homogeneous set of agents that have the same + agent_dict (Union[AbsAlgorithm, dict]): A single agent or a homogeneous set of agents that have the same method signatures. """ - def __init__(self, agent_dict: Union[AbsAgent, dict]): - if isinstance(agent_dict, AbsAgent): - agent_dict = {"AGENT": agent_dict} - self.agent_dict = agent_dict - self._names = set(self.agent_dict.keys()) + def __init__( + self, + agent_id_list: list, + policy_mapping: dict, + model_mapping: dict, + model_dict: dict, + experience_memory_dict: dict + ): + self.members = set() + self.agent_dict = {} + for agent_id, agent_cls in agent2cls.items(): + def __getitem__(self, agent_id): if len(self.agent_dict) == 1: diff --git a/maro/rl/agent/ddpg.py b/maro/rl/algorithm/ddpg.py similarity index 99% rename from maro/rl/agent/ddpg.py rename to maro/rl/algorithm/ddpg.py index 1822f4fa8..7a8d42393 100644 --- a/maro/rl/agent/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -11,7 +11,7 @@ from maro.rl.utils import get_torch_loss_cls from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent +from .abs_agent import AbsAlgorithm class DDPGConfig: @@ -48,7 +48,7 @@ def __init__( self.soft_update_coefficient = soft_update_coefficient -class DDPG(AbsAgent): +class DDPG(AbsAlgorithm): """The Deep Deterministic Policy Gradient (DDPG) algorithm. References: diff --git a/maro/rl/agent/dqn.py b/maro/rl/algorithm/dqn.py similarity index 64% rename from maro/rl/agent/dqn.py rename to maro/rl/algorithm/dqn.py index 7ff51109d..26ddc9e13 100644 --- a/maro/rl/agent/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -6,11 +6,10 @@ import numpy as np import torch -from maro.rl.model import SimpleMultiHeadModel +from maro.rl.model import QEstimatorForDiscreteActions from maro.rl.utils import get_max, get_sampler_cls, get_td_errors, get_torch_loss_cls, select_by_actions -from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAgent +from .abs_agent import AbsAlgorithm class DQNConfig: @@ -68,13 +67,13 @@ def __init__( self.loss_func = get_torch_loss_cls(loss_cls)() -class DQN(AbsAgent): +class DQN(AbsAlgorithm): """The Deep-Q-Networks algorithm. See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: - model (SimpleMultiHeadModel): Q-value model. + model (QEstimatorForDiscreteActions): Q-value model. config (DQNConfig): Configuration for DQN algorithm. experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of unlimited size. @@ -89,7 +88,7 @@ class DQN(AbsAgent): """ def __init__( self, - model: SimpleMultiHeadModel, + model: QEstimatorForDiscreteActions, config: DQNConfig, experience_memory_size: int, experience_memory_overwrite_type: str, @@ -97,12 +96,6 @@ def __init__( min_new_experiences_to_trigger_learning: int = 1, min_experiences_to_trigger_learning: int = 1 ): - if (config.advantage_type is not None and - (model.task_names is None or set(model.task_names) != {"state_value", "advantage"})): - raise UnrecognizedTask( - f"Expected model task names 'state_value' and 'advantage' since dueling DQN is used, " - f"got {model.task_names}" - ) super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, @@ -112,50 +105,44 @@ def __init__( self._sampler = self.config.sampler_cls(self.experience_memory, **self.config.sampler_params) self._training_counter = 0 self._target_model = model.copy() if model.trainable else None + self._target_model.eval() + self._num_actions = self.model.num_actions - def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: - state = torch.from_numpy(state) - if self.device: - state = state.to(self.device) - is_single = len(state.shape) == 1 - if is_single: - state = state.unsqueeze(dim=0) - - q_values = self._get_q_values(state, training=False) - num_actions = q_values.shape[1] - greedy_action = q_values.argmax(dim=1).data.cpu() - # No exploration - if self.config.epsilon == .0: - return greedy_action.item() if is_single else greedy_action.numpy() - - if is_single: - return greedy_action if np.random.random() > self.config.epsilon else np.random.choice(num_actions) - - # batch inference - return np.array([ - act if np.random.random() > self.config.epsilon else np.random.choice(num_actions) - for act in greedy_action - ]) + def choose_action(self, states) -> Union[int, np.ndarray]: + with torch.no_grad(): + self.model.eval() + actions, q_vals = self.model.choose_action(states) + + actions, q_vals = actions.cpu().numpy(), q_vals.cpu().numpy() + if len(actions) == 1: + return actions[0] if np.random.random() > self.config.epsilon else np.random.choice(self._num_actions) + else: + return np.array([ + action if np.random.random() > self.config.epsilon else np.random.choice(self._num_actions) + for action in actions + ]) def step(self): + self.model.train() for _ in range(self.config.train_iters): # sample from the replay memory indexes, batch = self._sampler.sample(self.config.batch_size) - states = torch.from_numpy(np.asarray(batch["S"])).to(self.device) - actions = torch.from_numpy(np.asarray(batch["A"])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch["R"])).to(self.device) - next_states = torch.from_numpy(np.asarray(batch["S_"])).to(self.device) - - q_all = self._get_q_values(states) - q = select_by_actions(q_all, actions) - next_q_all_target = self._get_q_values(next_states, is_eval=False, training=False) - if self.config.double: - next_q_all_eval = self._get_q_values(next_states, training=False) - next_q = select_by_actions(next_q_all_target, next_q_all_eval.max(dim=1)[1]) # (N,) - else: - next_q, _ = get_max(next_q_all_target) # (N,) - - loss = get_td_errors(q, next_q, rewards, self.config.reward_discount, loss_func=self.config.loss_func) + states, next_states = batch["S"], batch["S_"] + actions = torch.from_numpy(batch["A"]) + rewards = torch.from_numpy(batch["R"]) + q_values = self.model(states, actions) + # get next Q values + with torch.no_grad(): + if self.config.double: + next_q_values = self._target_model(next_states, self.model.choose_action(next_states)[0]) # (N,) + else: + next_q_values = self._target_model.choose_action(next_states)[1] # (N,) + + # get TD errors + target_q_values = (rewards + self.config.gamma * next_q_values).detach() # (N,) + loss = self.config.loss_func(q_values, target_q_values) + + # train and update target if necessary self.model.step(loss.mean()) self._training_counter += 1 if self._training_counter % self.config.target_update_freq == 0: @@ -166,14 +153,3 @@ def step(self): def set_exploration_params(self, epsilon): self.config.epsilon = epsilon - - def _get_q_values(self, states: torch.Tensor, is_eval: bool = True, training: bool = True): - output = self.model(states, training=training) if is_eval else self._target_model(states, training=False) - if self.config.advantage_type is None: - return output - else: - state_values = output["state_value"] - advantages = output["advantage"] - # Use mean or max correction to address the identifiability issue - corrections = advantages.mean(1) if self.config.advantage_type == "mean" else advantages.max(1)[0] - return state_values + advantages - corrections.unsqueeze(1) diff --git a/maro/rl/agent/pg.py b/maro/rl/algorithm/pg.py similarity index 89% rename from maro/rl/agent/pg.py rename to maro/rl/algorithm/pg.py index 821f79287..55e9e5f14 100644 --- a/maro/rl/agent/pg.py +++ b/maro/rl/algorithm/pg.py @@ -7,10 +7,10 @@ import torch from torch.distributions import Categorical -from maro.rl.model import SimpleMultiHeadModel +from maro.rl.model import ParameterizedPolicy from maro.rl.utils import get_truncated_cumulative_reward -from .abs_agent import AbsAgent +from .abs_agent import AbsAlgorithm class PolicyGradientConfig: @@ -25,13 +25,13 @@ def __init__(self, reward_discount: float): self.reward_discount = reward_discount -class PolicyGradient(AbsAgent): +class PolicyGradient(AbsAlgorithm): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. Args: - model (SimpleMultiHeadModel): Multi-task model that computes action distributions and state values. + model (ParameterizedPolicy): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. config (PolicyGradientConfig): Configuration for the PG algorithm. experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of @@ -47,14 +47,16 @@ class PolicyGradient(AbsAgent): """ def __init__( self, - model: SimpleMultiHeadModel, + model: ParameterizedPolicy, config: PolicyGradientConfig, experience_memory_size: int, experience_memory_overwrite_type: str, empty_experience_memory_after_step: bool = True, min_new_experiences_to_trigger_learning: int = 1, min_experiences_to_trigger_learning: int = 1 - ): + ): + if not isinstance(model, ParameterizedPolicyWithValueEstimator): + raise TypeError("model must be an instance of 'ParameterizedPolicy'") super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, diff --git a/maro/rl/algorithm/torch_loss_cls_index.py b/maro/rl/algorithm/torch_loss_cls_index.py new file mode 100644 index 000000000..4b08e1ef6 --- /dev/null +++ b/maro/rl/algorithm/torch_loss_cls_index.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from torch import nn + +# For details, see https://pytorch.org/docs/stable/nn.html#loss-functions +TORCH_LOSS_CLS = { + "l1": nn.L1Loss, + "mse": nn.MSELoss, + "cross_entropy": nn.CrossEntropyLoss, + "ctc": nn.CTCLoss, + "nll": nn.NLLLoss, + "poisson_nll": nn.PoissonNLLLoss, + "kl": nn.KLDivLoss, + "bce": nn.BCELoss, + "bce_logits": nn.BCEWithLogitsLoss, + "margin_ranking": nn.MarginRankingLoss, + "hinge_embedding": nn.HingeEmbeddingLoss, + "multi_label_margin": nn.MultiLabelMarginLoss, + "multi_label_soft_margin": nn.MultiLabelSoftMarginLoss, + "smooth_l1": nn.SmoothL1Loss, + "soft_margin": nn.SoftMarginLoss, + "cosine_embedding": nn.CosineEmbeddingLoss, + "multi_margin": nn.MultiMarginLoss, + "triplet_margin": nn.TripletMarginLoss, +} \ No newline at end of file diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index f037df84a..77d35030b 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -5,7 +5,7 @@ from typing import Union from maro.communication import Proxy -from maro.rl.agent import AbsAgent, MultiAgentWrapper +from maro.rl.agent import AbsAlgorithm, MultiAgentWrapper from maro.rl.training import AbsEnvWrapper from maro.utils import Logger @@ -18,7 +18,7 @@ class Actor(object): Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. + agent (Union[AbsAlgorithm, MultiAgentWrapper]): Agent that interacts with the environment. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class @@ -27,14 +27,14 @@ class Actor(object): def __init__( self, env: AbsEnvWrapper, - agent: Union[AbsAgent, MultiAgentWrapper], + agent: Union[AbsAlgorithm, MultiAgentWrapper], group: str, proxy_options: dict = None, pull_experiences_with_copy: bool = False, log_dir: str = getcwd() ): self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAlgorithm) else agent self._pull_experiences_with_copy = pull_experiences_with_copy if proxy_options is None: proxy_options = {} diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py index a7f3d30f4..7458d8afe 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/distributed/actor_manager.py @@ -15,7 +15,7 @@ class ActorManager(object): """Learner class for distributed training. Args: - agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. + agent (Union[AbsAlgorithm, MultiAgentWrapper]): Learning agents. scheduler (Scheduler): . num_actors (int): Expected number of actors in the group identified by ``group_name``. group_name (str): Identifier of the group to which the actor belongs. It must be the same group name diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 173e3f3c2..8ddb27918 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -7,7 +7,7 @@ from typing import Union from maro.communication import Message, Proxy, SessionType -from maro.rl.agent import AbsAgent, MultiAgentWrapper +from maro.rl.agent import AbsAlgorithm, MultiAgentWrapper from maro.rl.scheduling import Scheduler from maro.utils import Logger @@ -19,12 +19,12 @@ class DistLearner(object): """Learner class for distributed training. Args: - agent (Union[AbsAgent, MultiAgentWrapper]): Learning agents. + agent (Union[AbsAlgorithm, MultiAgentWrapper]): Learning agents. scheduler (Scheduler): A ``Scheduler`` instance for generating exploration parameters. """ def __init__( self, - agent: Union[AbsAgent, MultiAgentWrapper], + agent: Union[AbsAlgorithm, MultiAgentWrapper], scheduler: Scheduler, actor_manager: ActorManager, agent_update_interval: int = -1, @@ -33,7 +33,7 @@ def __init__( log_dir: str = getcwd() ): super().__init__() - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAlgorithm) else agent self.scheduler = scheduler self.actor_manager = actor_manager self.agent_update_interval = agent_update_interval diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 00c54829d..05d23e934 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -3,10 +3,13 @@ from .abs_block import AbsBlock from .fc_block import FullyConnectedBlock -from .learning_model import AbsCoreModel, OptimOption, SimpleMultiHeadModel +from .learning_model import ( + AbsCoreModel, OptimOption, ParameterizedPolicy, ParameterizedPolicyWithValueEstimator, QEstimatorForDiscreteActions +) __all__ = [ "AbsBlock", "FullyConnectedBlock", - "AbsCoreModel", "OptimOption", "SimpleMultiHeadModel" + "AbsCoreModel", "OptimOption", "ParameterizedPolicy", "ParameterizedPolicyWithValueEstimator", + "QEstimatorForDiscreteActions" ] diff --git a/maro/rl/model/learning_model.py b/maro/rl/model/core_model.py similarity index 67% rename from maro/rl/model/learning_model.py rename to maro/rl/model/core_model.py index 68a46d5f2..ab47bcf6a 100644 --- a/maro/rl/model/learning_model.py +++ b/maro/rl/model/core_model.py @@ -6,6 +6,7 @@ import torch import torch.nn as nn +from torch.distributions import Categorical from maro.rl.utils import get_torch_lr_scheduler_cls, get_torch_optim_cls from maro.utils import clone @@ -70,10 +71,12 @@ def __init__( if optim_option.scheduler_cls: self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) + self.device = torch.device('cpu') + @property def trainable(self) -> bool: return self.optimizer is not None - + @abstractmethod def forward(self, *args, **kwargs): raise NotImplementedError @@ -127,7 +130,7 @@ def copy(self, with_optimizer: bool = False): return model_copy -class SimpleMultiHeadModel(AbsCoreModel): +class QEstimatorForDiscreteActions(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: @@ -136,66 +139,71 @@ class SimpleMultiHeadModel(AbsCoreModel): component by ``shared_component_name``. optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for the components. Defaults to None. - shared_component_name (str): Name of the network component to be designated as the shared component at the - bottom of the architecture. Must be None or a key in ``component``. If only a single component - is present, this is ignored. Defaults to None. """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - shared_component_name: str = None - ): - super().__init__(component, optim_option=optim_option) - if isinstance(component, dict): - if shared_component_name is not None: - assert (shared_component_name in component), ( - f"shared_component_name must be one of {list(component.keys())}, got {shared_component_name}" - ) - self._task_names = [name for name in component if name != shared_component_name] - else: - self._task_names = None - self._shared_component_name = shared_component_name + @abstractmethod + def forward(self, states, actions: torch.tensor) -> torch.tensor: + raise NotImplementedError + + def choose_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the maximum Q-value and + the corresponding action index for each state. + """ + q_for_all_actions = self.forward(states) # (batch_size, num_actions) + greedy_q, actions = q_for_all_actions.max(dim=1) + return actions.detach(), greedy_q.detach() @property - def task_names(self): - return self._task_names + @abstractmethod + def num_actions(self): + raise NotImplementedError - def _forward(self, inputs, task_name: str = None): - if not isinstance(self._component, nn.ModuleDict): - return self._component(inputs) - if self._shared_component_name is not None: - inputs = self._component[self._shared_component_name](inputs) # features +class ParameterizedPolicy(AbsCoreModel): + """A compound network structure that consists of multiple task heads and an optional shared stack. - if task_name is None: - return {name: self._component[name](inputs) for name in self._task_names} + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + All components must have the same input dimension except the one designated as the shared + component by ``shared_component_name``. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for + the components. Defaults to None. + """ + @abstractmethod + def forward(self, states) -> torch.tensor: + raise NotImplementedError - if isinstance(task_name, list): - return {name: self._component[name](inputs) for name in task_name} - else: - return self._component[task_name](inputs) - - def forward(self, inputs, task_name: Union[str, List[str]] = None, training: bool = True): - """Feedforward computations for the given head(s). - - Args: - inputs: Inputs to the model. - task_name (str): The name of the task for which the network output is required. If the model contains only - one task module, the task_name is ignored and the output of that module will be returned. If the model - contains multiple task modules, then 1) if task_name is None, the output from all task modules will be - returned in the form of a dictionary; 2) if task_name is a list, the outputs from the task modules - specified in the list will be returned in the form of a dictionary; 3) if this is a single string, - the output from the corresponding task module will be returned. - training (bool): If true, all torch submodules will be set to training mode, and auto-differentiation - will be turned on. Defaults to True. - - Returns: - Outputs from the required head(s). + def choose_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the maximum Q-value and + the corresponding action index for each state. """ - self.train(mode=training) - if training: - return self._forward(inputs, task_name) + action_prob = Categorical(self.forward(states)) # (batch_size, num_actions) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + return action, log_p + + +class ParameterizedPolicyWithValueEstimator(AbsCoreModel): + """A compound network structure that consists of multiple task heads and an optional shared stack. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + All components must have the same input dimension except the one designated as the shared + component by ``shared_component_name``. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for + the components. Defaults to None. + """ + @abstractmethod + def forward(self, states, output_action_probs: bool = True, output_values: bool = True): + raise NotImplementedError - with torch.no_grad(): - return self._forward(inputs, task_name) + def choose_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the maximum Q-value and + the corresponding action index for each state. + """ + action_prob = Categorical(self.forward(state, output_values=False)) # (batch_size, num_actions) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + return action, log_p diff --git a/maro/rl/model/torch_cls_index.py b/maro/rl/model/torch_cls_index.py new file mode 100644 index 000000000..1a1c6e897 --- /dev/null +++ b/maro/rl/model/torch_cls_index.py @@ -0,0 +1,57 @@ +from torch import optim +from torch.optim import lr_scheduler + +# For details, see https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity +TORCH_ACTIVATION_CLS = { + "elu": nn.ELU, + "hard_shrink": nn.Hardshrink, + "hard_sigmoid": nn.Hardsigmoid, + "hard_tanh": nn.Hardtanh, + "hardswish": nn.Hardswish, + "leaky_relu": nn.LeakyReLU, + "log_sigmoid": nn.LogSigmoid, + "multihead_attention": nn.MultiheadAttention, + "prelu": nn.PReLU, + "relu": nn.ReLU, + "relu6": nn.ReLU6, + "rrelu": nn.RReLU, + "selu": nn.SELU, + "celu": nn.CELU, + "gelu": nn.GELU, + "sigmoid": nn.Sigmoid, + "soft_plus": nn.Softplus, + "soft_shrink": nn.Softshrink, + "soft_sign": nn.Softsign, + "tanh": nn.Tanh, + "tanh_shrink": nn.Tanhshrink, + "threshold": nn.Threshold +} + +# For details, see https://pytorch.org/docs/stable/optim.html +TORCH_OPTIM_CLS = { + "sgd": optim.SGD, + "asgd": optim.ASGD, + "adadelta": optim.Adadelta, + "adagrad": optim.Adagrad, + "adam": optim.Adam, + "adamax": optim.Adamax, + "adamw": optim.AdamW, + "sparse_adam": optim.SparseAdam, + "lbfgs": optim.LBFGS, + "rmsprop": optim.RMSprop, + "rprop": optim.Rprop +} + +# For details, see https://pytorch.org/docs/stable/optim.html +TORCH_LR_SCHEDULER_CLS = { + "lambda": lr_scheduler.LambdaLR, + "multiplicative": lr_scheduler.MultiplicativeLR, + "step": lr_scheduler.StepLR, + "multi_step": lr_scheduler.MultiStepLR, + "exponential": lr_scheduler.ExponentialLR, + "cosine_annealing": lr_scheduler.CosineAnnealingLR, + "reduce_on_plateau": lr_scheduler.ReduceLROnPlateau, + "cyclic": lr_scheduler.CyclicLR, + "one_cycle": lr_scheduler.OneCycleLR, + "cosine_annealing_warm_restarts": lr_scheduler.CosineAnnealingWarmRestarts +} diff --git a/maro/rl/storage/sampler_cls_index.py b/maro/rl/storage/sampler_cls_index.py new file mode 100644 index 000000000..907381af6 --- /dev/null +++ b/maro/rl/storage/sampler_cls_index.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .sampler import UniformSampler + +SAMPLER_CLS = { + "uniform": UniformSampler, +} diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/training/env_wrapper.py index 9246fa951..ba4c80d84 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/training/env_wrapper.py @@ -22,47 +22,32 @@ class AbsEnvWrapper(ABC): after executing an action. """ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): - self.env = env - self._step_index = None self.replay = defaultdict(lambda: defaultdict(list)) self.state_info = None # context for converting model output to actions that can be executed by the env self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay + self.pending_reward_ticks = deque() # list of ticks whose actions have not been given a reward + self.action_history = {} # store the tick-to-action mapping + self._step_index = None self._total_reward = 0 + self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. self._state = None # the latest extracted state is kept here - self._acting_agents = deque() # list of (tick, acting_agent_list) for delayed reward evaluation @property def step_index(self): return self._step_index - + @property def agent_idx_list(self): return self.env.agent_idx_list def start(self, rollout_index: int = None): self._step_index = 0 - _, event, _ = self.env.step(None) - self._state = self.get_state(event) + _, self._event, _ = self.env.step(None) + self._state = self.get_state(self.env.tick) if self.save_replay: for agent_id, state in self._state.items(): - replay = self.replay[agent_id] - if replay["S"]: - replay["S_"].append(state) - replay["S"].append(state) - assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 - - def pull_experiences(self, copy: bool = False): - experience, num_experiences = defaultdict(dict), 0 - for agent_id, replay in self.replay.items(): - num_complete = min(len(replay["R"]), len(replay["S_"])) - num_experiences += num_complete - for k, vals in replay.items(): - experience[agent_id][k] = vals[:num_complete] - if not copy: - del vals[:num_complete] - - return experience, num_experiences + self.replay[agent_id]["S"].append(state) @property def metrics(self): @@ -72,12 +57,22 @@ def metrics(self): def state(self): return self._state + @property + def event(self): + return self._event + @property def total_reward(self): return self._total_reward @abstractmethod - def get_state(self, event) -> dict: + def get_state(self, tick: int = None) -> dict: + """Compute the state for a given tick. + + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + """ pass @abstractmethod @@ -85,13 +80,12 @@ def get_action(self, action) -> dict: pass @abstractmethod - def get_reward(self, tick: int = None, target_agents: list = None) -> dict: + def get_reward(self, tick: int = None) -> dict: """User-defined reward evaluation. Args: tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed reward evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. - targets_agents (list): If given, rewards will be given only to these agents. Defaults to None. """ pass @@ -99,11 +93,12 @@ def step(self, action_by_agent: dict): # t0 = time.time() self._step_index += 1 env_action = self.get_action(action_by_agent) - self._acting_agents.append((self.env.tick, list(action_by_agent.keys()))) + self.pending_reward_ticks.append(self.env.tick) + self.action_history[self.env.tick] = env_action if len(env_action) == 1: env_action = list(env_action.values())[0] # t1 = time.time() - _, event, done = self.env.step(env_action) + _, self._event, done = self.env.step(env_action) # t2 = time.time() # self._tot_raw_step_time += t2 - t1 @@ -115,19 +110,19 @@ def step(self, action_by_agent: dict): Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ while ( - self._acting_agents and - (done or self.env.tick - self._acting_agents[0][0] >= self.reward_eval_delay) + self.pending_reward_ticks and + (done or self.env.tick - self.pending_reward_ticks[0] >= self.reward_eval_delay) ): - reward = self.get_reward(tick=self._acting_agents[0][0], target_agents=self._acting_agents[0][1]) - # assign rewards to the relevant agents - for agent_id in self._acting_agents[0][1]: + tick = self.pending_reward_ticks.popleft() + reward = self.get_reward(tick=tick) + # assign rewards to the agents that took action at that tick + for agent_id in self.action_history[tick]: rw = reward.get(agent_id, 0) self.replay[agent_id]["R"].append(rw) self._total_reward += rw - self._acting_agents.popleft() if not done: - self._state = self.get_state(event) + self._state = self.get_state(self.env.tick) if self.save_replay: for agent_id, state in self._state.items(): replay = self.replay[agent_id] @@ -140,16 +135,32 @@ def step(self, action_by_agent: dict): # self._tot_step_time += t3 - t0 else: self._state = None + self.post_process() + self.end_ep_callback() # print(f"total raw step time: {self._tot_raw_step_time}") # print(f"total step time: {self._tot_step_time}") # self._tot_raw_step_time = 0 # self._tot_step_time = 0 + def post_process(self): + num_experiences = 0 + for agent_id, replay in self.replay.items(): + num_complete = min(len(replay["R"]), len(replay["S_"])) + num_experiences += num_complete + for k, vals in replay.items(): + del vals[num_complete:] + + return experience, num_experiences + + def end_ep_callback(self): + pass + def reset(self): self.env.reset() self.state_info = None self._total_reward = 0 self._state = None - self._acting_agents.clear() + self.pending_reward_ticks.clear() + self.action_history.clear() self.replay = defaultdict(lambda: defaultdict(list)) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 92f3b9d92..b4f646d76 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -5,7 +5,7 @@ from collections import defaultdict from typing import Dict, Union -from maro.rl.agent import AbsAgent, MultiAgentWrapper +from maro.rl.agent import AbsAlgorithm, MultiAgentWrapper from maro.rl.scheduling import Scheduler from maro.utils import InternalLogger @@ -18,12 +18,12 @@ class Learner(object): Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. + agent (Union[AbsAlgorithm, MultiAgentWrapper]): Agent that interacts with the environment. """ def __init__( self, env: AbsEnvWrapper, - agent: Union[AbsAgent, MultiAgentWrapper], + agent: Union[AbsAlgorithm, MultiAgentWrapper], scheduler: Scheduler, agent_update_interval: int = -1, log_env_metrics: bool = False @@ -32,7 +32,7 @@ def __init__( if agent_update_interval == 0: raise ValueError("agent_update_interval must be a positive integer or None.") self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAlgorithm) else agent self.scheduler = scheduler self.agent_update_interval = agent_update_interval self.total_env_steps = 0 diff --git a/maro/rl/utils/value_utils.py b/maro/rl/utils/value_utils.py deleted file mode 100644 index b8dc5fd41..000000000 --- a/maro/rl/utils/value_utils.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Callable - -import torch - - -def select_by_actions(q_values: torch.Tensor, actions: torch.Tensor): - if len(actions.shape) == 1: - actions = actions.unsqueeze(1) # (N, 1) - - if actions.dtype != torch.int64: - actions = actions.to(torch.int64) - - return q_values.gather(1, actions).squeeze(1) - - -def get_max(q_values: torch.Tensor, expand_action_dim: bool = True): - """ - Given Q-values for a batch of states and all actions, return the maximum Q-value and - the corresponding action index for each state. - """ - greedy_q, actions = q_values.max(dim=1) - if expand_action_dim: - actions = actions.unsqueeze(1) - return greedy_q, actions - - -def get_td_errors( - q_values: torch.Tensor, next_q_values: torch.Tensor, rewards: torch.Tensor, gamma: float, - loss_func: Callable -): - target_q_values = (rewards + gamma * next_q_values).detach() # (N,) - return loss_func(q_values, target_q_values) - - -def get_log_prob(action_probs: torch.Tensor, actions: torch.Tensor): - return torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) diff --git a/maro/utils/exception/error_code.py b/maro/utils/exception/error_code.py index 7d3f9c13d..4e8a5ac99 100644 --- a/maro/utils/exception/error_code.py +++ b/maro/utils/exception/error_code.py @@ -49,5 +49,4 @@ # 4000-4999: Error codes for RL toolkit 4000: "Store Misalignment", 4001: "Missing Optimizer", - 4002: "Unrecognized Task", } diff --git a/maro/utils/exception/rl_toolkit_exception.py b/maro/utils/exception/rl_toolkit_exception.py index 759b8ac86..d97c72f62 100644 --- a/maro/utils/exception/rl_toolkit_exception.py +++ b/maro/utils/exception/rl_toolkit_exception.py @@ -15,9 +15,3 @@ class MissingOptimizer(MAROException): """Raised when the optimizers are missing when calling CoreModel's step() method.""" def __init__(self, msg: str = None): super().__init__(4001, msg) - - -class UnrecognizedTask(MAROException): - """Raised when a CoreModel has task names that are not unrecognized by an algorithm.""" - def __init__(self, msg: str = None): - super().__init__(4002, msg) From 66ac504eded82b2d60d5a9cf09177879eff69a88 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 20 Apr 2021 11:42:27 +0800 Subject: [PATCH 202/482] remove unused attributes --- maro/simulator/scenarios/supply_chain/datamodels/consumer.py | 1 - maro/simulator/scenarios/supply_chain/datamodels/product.py | 5 ----- maro/simulator/scenarios/supply_chain/datamodels/seller.py | 2 -- 3 files changed, 8 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index 616577878..81c3aafe1 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -19,7 +19,6 @@ class ConsumerDataModel(SkuDataModel): order_product_cost = NodeAttribute(AttributeType.UInt) latest_consumptions = NodeAttribute(AttributeType.Float) - pending_order_daily = NodeAttribute(AttributeType.UInt) order_quantity = NodeAttribute(AttributeType.UInt) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/product.py b/maro/simulator/scenarios/supply_chain/datamodels/product.py index 385bd6b97..559e475c7 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/product.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/product.py @@ -10,11 +10,6 @@ @node("product") class ProductDataModel(SkuDataModel): - - distribution_check_order = NodeAttribute(AttributeType.UInt) - distribution_transport_cost = NodeAttribute(AttributeType.Float) - distribution_delay_order_penalty = NodeAttribute(AttributeType.Float) - price = NodeAttribute(AttributeType.Float) def __init__(self): diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index 153e2045c..e7d13dade 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -11,8 +11,6 @@ @node("seller") class SellerDataModel(SkuDataModel): """Data model for seller unit.""" - total_sold = NodeAttribute(AttributeType.UInt) - demand = NodeAttribute(AttributeType.UInt) sold = NodeAttribute(AttributeType.UInt) total_demand = NodeAttribute(AttributeType.UInt) From 64e02ddc45e35fc9b809e7a9a3f0b64373020d9d Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 20 Apr 2021 11:42:50 +0800 Subject: [PATCH 203/482] update documents about states in builtin scenario and some samples --- docs/source/key_components/data_model.rst | 1152 +++++++++++++++++++++ 1 file changed, 1152 insertions(+) diff --git a/docs/source/key_components/data_model.rst b/docs/source/key_components/data_model.rst index 90605cb5a..eebd5fae9 100644 --- a/docs/source/key_components/data_model.rst +++ b/docs/source/key_components/data_model.rst @@ -259,3 +259,1155 @@ For better data access, we also provide some advanced features, including: # Also with dynamic implementation, we can get the const attributes which is shared between snapshot list, even without # any snapshot (need to provided one tick for padding). states = test_nodes_snapshots[0: [0, 1]: ["const_attribute", "const_attribute_2"]] + + + +States in built-in scenarios' snapshot list +------------------------------------------- + +TODO: move to environment part? + +Currently there are 3 ways to expose states in built-in scenarios: + +Summary +~~~~~~~~~~~ + +Summary(env.summary) is used to expose static states to outside, it provide 3 items by default: +node_mapping, node_detail and event payload. + +The "node_mapping" item usually contains node name and related index, but the structure may be different +for different scenario. + +The "node_detail" usually used to expose node definitions, like node name, attribute name and slot number, +this is useful if you want to know what attributes are support for a scenario. + +The "event_payload" used show that payload attributes of event in scenario, like "RETURN_FULL" event in +CIM scenario, it contains "src_port_idx", "dest_port_idx" and "quantity". + +Metrics +~~~~~~~ + +Metrics(env.metrics) is designed that used to expose raw states of reward since we have removed reward +support in v0.2 version, and it also can be used to export states that not supported by snapshot list, like dictionary or complex +structures. Currently there are 2 ways to get the metrics from environment: env.metrics, or 1st result from env.step. + +This metrics usually is a dictionary with several keys, but this is determined by business engine. + +Snapshot_list +~~~~~~~~~~~~~ + +Snapshot list is the history of nodes (or data model) for a scenario, it only support numberic data types now. +It supported slicing query with a numpy array, so it support batch operations, make it much faster than +using raw python objects. + +Nodes and attributes may different for different scenarios, following we will introduce about those in +built-in scenarios. + +NOTE: +Per tick state means that the attribute value will be reset to 0 after each step. + +CIM +--- + +Default settings for snapshot list +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Snapshot resolution: 1 + + +Max snapshot number: same as durations + +Nodes and attributes in scenario +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In CIM scenario, there are 3 node types: + + +port +++++ + +capacity +******** + +type: int +slots: 1 + +The capacity of port for stocking containers. + +empty +***** + +type: int +slots: 1 + +Empty container volume on the port. + +full +**** + +type: int +slots: 1 + +Laden container volume on the port. + +on_shipper +********** + +type: int +slots: 1 + +Empty containers, which are released to the shipper. + +on_consignee +************ + +type: int +slots: 1 + +Laden containers, which are delivered to the consignee. + +shortage +******** + +type: int +slots: 1 + +Per tick state. Shortage of empty container at current tick. + +acc_storage +*********** + +type: int +slots: 1 + +Accumulated shortage number to the current tick. + +booking +******* + +type: int +slots: 1 + +Per tick state. Order booking number of a port at the current tick. + +acc_booking +*********** + +type: int +slots: 1 + +Accumulated order booking number of a port to the current tick. + +fulfillment +*********** + +type: int +slots: 1 + +Fulfilled order number of a port at the current tick. + +acc_fulfillment +*************** + +type: int +slots: 1 + +Accumulated fulfilled order number of a port to the current tick. + +transfer_cost +************* + +type: float +slots: 1 + +Cost of transferring container, which also covers loading and discharging cost. + +vessel +++++++ + +capacity +******** + +type: int +slots: 1 + +The capacity of vessel for transferring containers. + +NOTE: +This attribute is ignored in current implementation. + +empty +***** + +type: int +slots: 1 + +Empty container volume on the vessel. + +full +**** + +type: int +slots: 1 + +Laden container volume on the vessel. + +remaining_space +*************** + +type: int +slots: 1 + +Remaining space of the vessel. + +early_discharge +*************** + +type: int +slots: 1 + +Discharged empty container number for loading laden containers. + +route_idx +********* + +type: int +slots: 1 + +Which route current vessel belongs to. + +last_loc_idx +************ + +type: int +slots: 1 + +Last stop port index in route, it is used to identify where is current vessel. + +next_loc_idx +************ + +type: int +slots: 1 + +Next stop port index in route, it is used to identify where is current vessel. + +past_stop_list +************** + +type: int +slots: dynamic + +NOTE: +This and following attribute are special, that its slot number is determined by configuration, +but different with a list attribute, its slot number is fixed at runtime. + +Stop indices that we have stopped in the past. + +past_stop_tick_list +******************* + +type: int +slots: dynamic + +Ticks that we stopped at the port in the past. + +future_stop_list +**************** + +type: int +slots: dynamic + +Stop indices that we will stop in the future. + +future_stop_tick_list +********************* + +type: int +slots: dynamic + +Ticks that we will stop in the future. + +matrices +++++++++ + +Matrices node is used to store big matrix for ports, vessels and containers. + +full_on_ports +************* + +type: int +slots: port number * port number + +Distribution of full from port to port. + +full_on_vessels +*************** + +type: int +slots: vessel number * port number + +Distribution of full from vessel to port. + +vessel_plans +************ + +type: int +slots: vessel number * port number + +Planed route info for vessels. + +How to +~~~~~~ + +How to use the matrix(s) +++++++++++++++++++++++++ + +Matrix is special that it only have one instance (index 0), and the value is saved as a flat 1 dim array, we can reshape it after querying. + +.. code-block:: python + + # assuming that we want to use full_on_ports attribute. + + tick = 0 + + # we can get the instance number of a node by calling the len method + port_number = len(env.snapshot_list["port"]) + + # this is a 1 dim numpy array + full_on_ports = env.snapshot_list["matrices"][tick::"full_on_ports"] + + # reshape it, then this is a 2 dim array that from port to port. + full_on_ports = full_on_ports.reshape(port_number, port_number) + +Citi-Bike +--------- + +Default settings for snapshot list +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Snapshot resolution: 60 + + +Max snapshot number: same as durations + +Nodes and attributes in scenario +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +station ++++++++ + +bikes +***** + +type: int +slots: 1 + +How many bikes avaiable in current station. + +shortage +******** + +type: int +slots: 1 + +Per tick state. Lack number of bikes in current station. + +trip_requirement +**************** + +type: int +slots: 1 + +Per tick states. How many requirements in current station. + +fulfillment +*********** + +type: int +slots: 1 + +How many requirement is fit in current station. + +capacity +******** + +type: int +slots: 1 + +Max number of bikes this station can take. + +id ++++ + +type: int +slots: 1 + +Id of current station. + +weekday +******* + +type: short +slots: 1 + +Weekday at current tick. + +temperature +*********** + +type: short +slots: 1 + +Temperature at current tick. + +weather +******* + +type: short +slots: 1 + +Weather at current tick. + +0: sunny, 1: rainy, 2: snowy, 3: sleet. + +holiday +******* + +type: short +slots: 1 + +If it is holidy at current tick. + +0: holiday, 1: not holiday + +extra_cost +********** + +type: int +slots: 1 + +Cost after we reach the capacity after executing action, we have to move extra bikes +to other stations. + +transfer_cost +************* + +type: int +slots: 1 + +Cost to execute action to transfer bikes to other station. + +failed_return +************* + +type: int +slots: 1 + +Per tick state. How many bikes failed to return to current station. + +min_bikes +********* + +type: int +slots: 1 + +Min bikes number in a frame. + +matrices +++++++++ + +trips_adj +********* + +type: int +slots: station number * station number + +Used to store trip requirement number between 2 stations. + + +VM-scheduling +------------- + +Default settings for snapshot list +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Snapshot resolution: 1 + + +Max snapshot number: same as durations + +Nodes and attributes in scenario +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Cluster ++++++++ + +id +*** + +type: short +slots: 1 + +Id of the cluster. + +region_id +********* + +type: short +slots: 1 + +Region is of current cluster. + +data_center_id +************** + +type: short +slots: 1 + +Data center id of current cluster. + +total_machine_num +****************** + +type: int +slots: 1 + +Total number of machines in the cluster. + +empty_machine_num +****************** + +type: int +slots: 1 + +The number of empty machines in this cluster. A empty machine means that its allocated CPU cores are 0. + +data_centers +++++++++++++ + +id +*** + +type: short +slots: 1 + +Id of current data center. + +region_id +********* + +type: short +slots: 1 + +Region id of current data center. + +zone_id +******* + +type: short +slots: 1 + +Zone id of current data center. + +total_machine_num +***************** + +type: int +slots: 1 + +Total number of machine in current data center. + +empty_machine_num +***************** + +type: int +slots: 1 + +The number of empty machines in current data center. + +pms ++++ + +Physical machine node. + +id +*** + +type: int +slots: 1 + +Id of current machine. + +cpu_cores_capacity +****************** + +type: short +slots: 1 + +Max number of cpu core can be used for current machine. + +memory_capacity +*************** + +type: short +slots: 1 + +Max number of memory can be used for current machine. + +pm_type +******* + +type: short +slots: 1 + +Type of current machine. + +cpu_cores_allocated +******************* + +type: short +slots: 1 + +How many cpu core is allocated. + +memory_allocated +**************** + +type: short +slots: 1 + +How many memory is allocated. + +cpu_utilization +*************** + +type: float +slots: 1 + +CPU utilization of current machine. + +energy_consumption +****************** + +type: float +slots: 1 + +Energy consumption of current machine. + +oversubscribable +**************** + +type: short +slots: 1 + +Physical machine type: non-oversubscribable is -1, empty: 0, oversubscribable is 1. + +region_id +********* + +type: short +slots: 1 + +Region id of current machine. + +zone_id +******* + +type: short +slots: 1 + +Zone id of current machine. + +data_center_id +************** + +type: short +slots: 1 + +Data center id of current machine. + +cluster_id +********** + +type: short +slots: 1 + +Cluster id of current machine. + +rack_id +******* + +type: short +slots: 1 + +Rack id of current machine. + +Rack +++++ + +id +*** + +type: int +slots: 1 + +Id of current rack. + +region_id +********* + +type: short +slots: 1 + +Region id of current rack. + +zone_id +******* + +type: short +slots: 1 + +Zone id of current rack. + +data_center_id +************** + +type: short +slots: 1 + +Data center id of current rack. + +cluster_id +********** + +type: short +slots: 1 + +Cluster id of current rack. + +total_machine_num +***************** + +type: int +slots: 1 + +Total number of machines on this rack. + +empty_machine_num +***************** + +type: int +slots: 1 + +Number of machines that not in use on this rack. + +regions ++++++++ + +id +*** + +type: short +slots: 1 + +Id of curent region. + +total_machine_num +***************** + +type: int +slots: 1 + +Total number of machines in this region. + +empty_machine_num +***************** + +type: int +slots: 1 + +Number of machines that not in use in this region. + +zones ++++++ + +id +*** + +type: short +slots: 1 + +Id of this zone. + +total_machine_num +***************** + +type: int +slots: 1 + +Total number of machines in this zone. + +empty_machine_num +***************** + +type: int +slots: 1 + +Number of machines that not in use in this zone. + + +supply_chain +------------ + +Supply chain is a special scenario that it support dynamic nodes in frame, it decide what +nodes to use in frame by configuration file, such if you defined only a supplier facility, then +following nodes will be added into frame (then in snapshot list): +consumer, manufacture, facility, product, storage, vehicle and distribution. + +And it only support dynamic backend for now, be careful about the shape of snapshot list querying result. + +We will list all the nodes below for a reference. + +Default settings for snapshot list +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Snapshot resolution: 1 + + +Max snapshot number: same as durations + +Nodes and attributes in scenario +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nodes in supply chain scenario have a hierarchical structure, like a sku product unit may be composed with +seller, consumer and manufacture units, but frame (snapshot list) cannot support this, so we +flatten the structure, append the parent id to the data model, so all the nodes will contain: + +id ++++ + +Unit id, this is different with node index, this is unique between facilities and units in current world. + +facility_id ++++++++++++ + +Which facility current unit belongs to. + +And for seller, consumer, manufacture and product units, they are sku specified, and product units contains the other +3 units, so they all contains following attributes: + +product_id +++++++++++ + +Which product (sku) this unit related to, one sku per unit. + +product_unit_id ++++++++++++++++ + +Which product unit this unit belongs to. + + +Following is the special attribute for each node. + +storage ++++++++ + +remaining_space +*************** + +type: unsigned int +slots: 1 + +How many avaiable space in current storage. + +capacity +******** + +type: unsigned int +slots: 1 + +Max space of current storage, one unit space per product (sku) for now. + +product_list +************ +type: unsigned int list +slots: dynamic + +What product (id) can be save in current storage. + +product number +************** + +type: unsigned int list +slots: dynamic + +How many product in current storage, aligned with product_list, used as a key-value pair. + +vehicle ++++++++ + +payload +******* + +type: unsigned int +slots: 1 + +How many product current vehicle carring. + +unit_transport_cost + +type: float +slots: 1 + +Transport cost per product. + +distribution +++++++++++++ + +remaining_order_quantity +************************ + +type: unsigned int +slots: 1 + +Sum of product number in current order list for now. + +remaining_order_number +********************** + +type: unsigned int +slots: 1 + +How many pending order for now. + +consumer +++++++++ + +total_purchased +*************** + +type: unsigned int +slots: 1 + +How many products this node purchased from start to now. + +total_received +************** + +type: unsigned int +slots: 1 + +How many products this node received from start to now. + +purchased +********* + +type: unsigned int +slots: 1 + +Per tick state. How many products purchased at current tick. + +received +******** + +type: unsigned int +slots: 1 + +Per tick state. How many products received from source facility at current tick. + +order_product_cost +****************** + +type: unsigned int +slots: 1 + +Cost per product to order. + +latest_consumptions +******************* + +type: float +slots: 1 + +Per tick states. Consumption of current tick, 1.0 if there is purchase, or 0. + +order_quantity +************** + +type: unsigned int +slots: 1 + +How many product to order, from action. + +price +***** + +type: float +slots: 1 + +Price per product. + +order_cost +********** + +type: float +slots: 1 + +Cost per order. + +reward_discount +*************** + +type: float +slots: 1 + +Reward discount from action. + +manufacture ++++++++++++ + +manufacturing_number +******************** + +type: unsigned int +slots: 1 + +How many products being produced at current tick, controlled by action. + +product_unit_cost +***************** + +type: float +slots: 1 + +Cost to procedue a product. + +seller +++++++ + +total_sold +********** + +type: unsigned int +slots: 1 + +Total products sold number from start to now. + +demand +****** + +type: unsigned int +slots: 1 + +Per tick state. Product demand at current tick. + +sold +**** + +type: unsigned int +slots: 1 + +Per tick state. How many products sold at current tick. + +total_demand +************ + +type: unsigned int +slots: 1 + +Total products demand from start to now. + +price +***** + +type: float +slots: 1 + +Product price. + +backlog_ratio +************* + +type: float +slots: 1 + +Backlog factor for current product. + +product ++++++++ + +price +***** + +type: float +slots: 1 + +Current product price. + +How to +~~~~~~ + +How to use result of dynamic backend? +************************************* + +Supply chain scenario only support dynamic backend for now, so the result from snapshot list will +be a 4 dim (tick, node, attribute, slots) Numpy array. + +.. code-block:: python + + # we want to query sold and demand history for sellers + hist_ticks = (0, 1, 2, 4) + + states = env.snapshot_list["seller"][hist_ticks::("sold", "demand")] + + # this states will a 4 dim result as below + """ + [ + [ + [ + [0.0], # sold (slot = 1) + [0.0] # demand (slot = 1) + ], # seller 0 + [...] # seller 1 + ], # tick 0 + [ + [...], + [...] + ], # tick 1 + [ + [...], + [...] + ], # tick 2 + [ + [...], + [...] + ] # tick 3 (latest) + ] + """ + + # assuming we want to sold history for seller 0 (node index) + cur_seller_hist = states[:, 0] + + sale_hist = cur_seller_hist[:, 0].flatten() + demand_hist = cur_seller_hist[:, 1].flatten() + +How to query a dyanmic list attribute? +************************************** + +With dyanmic backend, we cannot mix a list attribute and a normal attribute to query, we have to +query, it only support one tick, one node and one attribute querying. + +.. code-block:: python + + # quanry product and its number in a storage + + tick = 0 + node_index = 0 + storage_ss = env.snapshot_list["storage"] + + # we have to query them with 2 statements. + product_list = storage_ss[tick:node_index:"product_list"].flatten().astype(np.int) + product_number = storage_ss["storage"][tick:node_index:"product_number"].flatten().astype(np.int) + + product_level = {pid: pnum for pid, pnum in zip(product_list, product_number)} \ No newline at end of file From a2400701af835bb77dccc8f56dd4c28b2b85b552 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 21 Apr 2021 09:17:30 +0000 Subject: [PATCH 204/482] revamped agent/policy design --- docs/source/apidoc/maro.rl.rst | 4 +- docs/source/key_components/rl_toolkit.rst | 2 +- maro/rl/__init__.py | 6 +- maro/rl/agent/agent.py | 5 - maro/rl/algorithm/__init__.py | 6 +- maro/rl/algorithm/abs_algorithm.py | 90 ------------- maro/rl/algorithm/abs_policy.py | 59 +++++++++ maro/rl/algorithm/ac.py | 110 ++++++---------- maro/rl/algorithm/agent_manager.py | 120 ------------------ maro/rl/algorithm/ddpg.py | 91 +++++-------- maro/rl/algorithm/dqn.py | 117 +++++++---------- maro/rl/algorithm/pg.py | 4 +- maro/rl/algorithm/torch_loss_cls_index.py | 26 ---- maro/rl/distributed/actor.py | 14 +- maro/rl/distributed/actor_manager.py | 6 +- maro/rl/distributed/learner.py | 15 ++- maro/rl/distributed/message_enums.py | 2 +- maro/rl/exploration/abs_explorer.py | 2 +- .../rl/exploration/epsilon_greedy_explorer.py | 2 +- maro/rl/model/__init__.py | 7 +- maro/rl/model/core_model.py | 30 ++++- maro/rl/multi_agent/__init__.py | 6 + maro/rl/multi_agent/multi_agent_policy.py | 116 +++++++++++++++++ maro/rl/training/learner.py | 8 +- 24 files changed, 365 insertions(+), 483 deletions(-) delete mode 100644 maro/rl/agent/agent.py delete mode 100644 maro/rl/algorithm/abs_algorithm.py create mode 100644 maro/rl/algorithm/abs_policy.py delete mode 100644 maro/rl/algorithm/agent_manager.py delete mode 100644 maro/rl/algorithm/torch_loss_cls_index.py create mode 100644 maro/rl/multi_agent/__init__.py create mode 100644 maro/rl/multi_agent/multi_agent_policy.py diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index d521dd652..301548bf9 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -4,7 +4,7 @@ Agent maro.rl.agent.abs\_agent -------------------------------------------------------------------------------- -.. automodule:: maro.rl.agent.abs_agent +.. automodule:: maro.rl.agent.abs_policy :members: :undoc-members: :show-inheritance: @@ -40,7 +40,7 @@ Agent Manager maro.rl.agent.abs\_agent\_manager -------------------------------------------------------------------------------- -.. automodule:: maro.rl.agent.abs_agent_manager +.. automodule:: maro.rl.agent.abs_policy_manager :members: :undoc-members: :show-inheritance: diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index f78cecfe7..66116f775 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -25,7 +25,7 @@ This decoupling is achieved by the Core Model abstraction described below. .. code-block:: python - class AbsAlgorithm(ABC): + class AbsPolicy(ABC): def __init__(self, model: AbsCoreModel, config, experience_pool=None): self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.model = model.to(self.device) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 3117d3d83..12d1c99b3 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. from maro.rl.algorithm import ( - DDPG, DQN, TORCH_LOSS_CLS, AbsAlgorithm, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, + DDPG, DQN, AbsFixedPolicy, AbsTrainablePolicy, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig ) from maro.rl.distributed import Actor, ActorManager, DistLearner @@ -20,8 +20,8 @@ ) __all__ = [ - "AbsAlgorithm", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", - "PolicyGradient", "PolicyGradientConfig", "TORCH_LOSS_CLS", + "AbsFixedPolicy", "AbsTrainablePolicy", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", + "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "Actor", "ActorManager", "DistLearner", "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", diff --git a/maro/rl/agent/agent.py b/maro/rl/agent/agent.py deleted file mode 100644 index b1eb5179c..000000000 --- a/maro/rl/agent/agent.py +++ /dev/null @@ -1,5 +0,0 @@ - - - - def __init__(self, algorithm_cls, model, experience_memory): - self.agent \ No newline at end of file diff --git a/maro/rl/algorithm/__init__.py b/maro/rl/algorithm/__init__.py index 6ac0616a0..aa6a1eff4 100644 --- a/maro/rl/algorithm/__init__.py +++ b/maro/rl/algorithm/__init__.py @@ -1,19 +1,17 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .abs_algorithm import AbsAlgorithm +from .abs_policy import AbsFixedPolicy, AbsTrainablePolicy from .ac import ActorCritic, ActorCriticConfig from .ddpg import DDPG, DDPGConfig from .dqn import DQN, DQNConfig from .pg import PolicyGradient, PolicyGradientConfig -from .torch_loss_cls_index import TORCH_LOSS_CLS __all__ = [ - "AbsAlgorithm", + "AbsPolicy", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", "PolicyGradient", "PolicyGradientConfig", - "TORCH_LOSS_CLS" ] diff --git a/maro/rl/algorithm/abs_algorithm.py b/maro/rl/algorithm/abs_algorithm.py deleted file mode 100644 index 80a0ede30..000000000 --- a/maro/rl/algorithm/abs_algorithm.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - -import torch - -from maro.rl.model import AbsCoreModel -from maro.rl.storage import SimpleStore - - -class AbsAlgorithm(ABC): - """Abstract RL algorithm class. - - It's a sandbox for the RL algorithm. Scenario-specific details will be excluded. - We focus on the abstraction algorithm development here. Environment observation and decision events will - be converted to a uniform format before calling in. The output will be converted to an environment - executable format before return back to the environment. Its key responsibility is optimizing policy based - on interaction with the environment. - - Args: - model (AbsCoreModel): Task model or container of task models required by the algorithm. - config: Settings for the algorithm. - experience_memory (SimpleStore): Experience memory for storing experiences - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. - """ - def __init__( - self, - model: AbsCoreModel, - config, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 - ): - self.model = model - self.config = config - self.experience_memory = SimpleStore( - ["S", "A", "R", "S_"], capacity=experience_memory_size, overwrite_type=experience_memory_overwrite_type - ) - self.empty_experience_memory_after_step = empty_experience_memory_after_step - self.min_new_experiences_to_trigger_learning = min_new_experiences_to_trigger_learning - self.min_experiences_to_trigger_learning = min_experiences_to_trigger_learning - - @abstractmethod - def choose_action(self, state): - """This method uses the underlying model(s) to compute an action from a shaped state. - - Args: - state: A state object shaped by a ``StateShaper`` to conform to the model input format. - - Returns: - The action to be taken given ``state``. It is usually necessary to use an ``ActionShaper`` to convert - this to an environment executable action. - """ - return NotImplementedError - - def set_exploration_params(self, **params): - pass - - def learn(self, experiences: dict) -> bool: - """Store experinces in the experience memory and train the model if necessary.""" - if set(experiences.keys()) != {"S", "A", "R", "S_"}: - raise ValueError("The keys of experiences must be {'S', 'A', 'R', 'S_'}") - self.experience_memory.put(experiences) - if ( - len(experiences["S"]) >= self.min_new_experiences_to_trigger_learning and - len(self.experience_memory) >= self.min_experiences_to_trigger_learning - ): - self.step() - self._version_index += 1 - if self.empty_experience_memory_after_step: - self.experience_memory.clear() - return True - return False - - @abstractmethod - def step(self): - """Algorithm-specific training logic. - - The parameters are data to train the underlying model on. Algorithm-specific loss and optimization - should be reflected here. - """ - return NotImplementedError diff --git a/maro/rl/algorithm/abs_policy.py b/maro/rl/algorithm/abs_policy.py new file mode 100644 index 000000000..4626c2d39 --- /dev/null +++ b/maro/rl/algorithm/abs_policy.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod + + +class AbsFixedPolicy(ABC): + """Abstract fixed policy class. + + Args: + config: Settings for the algorithm. + """ + def __init__(self, config): + self.config = config + + @abstractmethod + def choose_action(self, state): + """Compute an action based on a state object. + + Args: + state: State object. + + Returns: + The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + raise NotImplementedError + + +class AbsTrainablePolicy(ABC): + """Abstract fixed policy class. + + Args: + config: Settings for the algorithm. + """ + def __init__(self, config): + self.config = config + + @abstractmethod + def choose_action(self, state): + """Compute an action based on a state object. + + Args: + state: State object. + + Returns: + The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + raise NotImplementedError + + @abstractmethod + def update(self, experience_obj): + """Algorithm-specific training logic. + + The parameters are data to train the underlying model on. Algorithm-specific loss and optimization + should be reflected here. + """ + raise NotImplementedError diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index 5ce384612..05ab0b1fe 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -1,17 +1,19 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import namedtuple from typing import Tuple import numpy as np import torch from torch.distributions import Categorical -from maro.rl.model import ParameterizedPolicyWithValueEstimator +from maro.rl.model import PolicyValueNetForDiscreteActionSpace from maro.rl.utils import get_log_prob, get_torch_loss_cls -from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAlgorithm +from .abs_policy import AbsPolicy + +ACExperience = namedtuple("ACExperience", ["state", "action", "reward", "next_state"]) class ActorCriticConfig: @@ -21,30 +23,27 @@ class ActorCriticConfig: reward_discount (float): Reward decay as defined in standard RL terminology. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - train_iters (int): Number of gradient descent steps per call to ``train``. actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. """ - __slots__ = ["reward_discount", "critic_loss_func", "train_iters", "actor_loss_coefficient", "clip_ratio"] + __slots__ = ["reward_discount", "critic_loss_func", "actor_loss_coefficient", "clip_ratio"] def __init__( self, reward_discount: float, - train_iters: int, critic_loss_cls="mse", actor_loss_coefficient: float = 1.0, clip_ratio: float = None ): self.reward_discount = reward_discount self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() - self.train_iters = train_iters self.actor_loss_coefficient = actor_loss_coefficient self.clip_ratio = clip_ratio -class ActorCritic(AbsAlgorithm): +class ActorCritic(AbsPolicy): """Actor Critic algorithm with separate policy and value models. References: @@ -52,41 +51,18 @@ class ActorCritic(AbsAlgorithm): https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: - model (ParameterizedPolicyWithValueEstimator): Multi-task model that computes action distributions and - state values. It may or may not have a shared bottom stack. + ac_net (PolicyValueNetForDiscreteActionSpace): Multi-task model that computes action distributions + and state values. config: Configuration for the AC algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. Defaults to True. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. """ - def __init__( - self, - model: ParameterizedPolicyWithValueEstimator, - config: ActorCriticConfig, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool = True, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 - ): - if not isinstance(model, ParameterizedPolicyWithValueEstimator): - raise TypeError("model must be an instance of 'ParameterizedPolicyWithValueEstimator'") + def __init__(self, ac_net: PolicyValueNetForDiscreteActionSpace, config: ActorCriticConfig): + if not isinstance(ac_net, PolicyValueNetForDiscreteActionSpace): + raise TypeError("model must be an instance of 'PolicyValueNetForDiscreteActionSpace'") - super().__init__( - model, config, experience_memory_size, experience_memory_overwrite_type, - empty_experience_memory_after_step, - min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning - ) + super().__init__(config) + self.ac_net = ac_net - def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: """Use the actor (policy) model to generate stochastic actions. Args: @@ -96,37 +72,35 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: Actions and corresponding log probabilities. """ with torch.no_grad(): - actions, log_p = self.model.choose_action() + actions, log_p = self.ac_net.choose_action(states) actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() - if len(actions) == 1: - return actions[0], log_p[0] - else: - return actions, log_p + return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def step(self): - batch = self.experience_memory.get() - states, next_states = batch["S"], batch["S_"] - actions = torch.from_numpy(np.asarray([act[0] for act in batch["A"]])) - log_p = torch.from_numpy(np.asarray([act[1] for act in batch["A"]])) - rewards = torch.from_numpy(np.asarray(batch["R"])) + def update(self, experience_obj: ACExperience): + if not isinstance(experience_obj, ACExperience): + raise TypeError(f"Expected experience object of type ACExperience, got {type(experience_obj)}") - state_values = self.model(states, output_action_probs=False).detach().squeeze() - next_state_values = self.model(next_states, output_action_probs=False).detach().squeeze() + states, next_states = experience_obj.state, experience_obj.next_state + actions = torch.from_numpy(np.asarray([act[0] for act in experience_obj.action])) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_obj.action])) + rewards = torch.from_numpy(np.asarray(experience_obj.reward)) + + state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() + next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() return_est = rewards + self.config.reward_discount * next_state_values advantages = return_est - state_values - for i in range(self.config.train_iters): - # actor loss - action_prob, state_values = self.model(states) - log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - if self.config.clip_ratio is not None: - ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() - else: - actor_loss = -(log_p_new * advantages).mean() - - # critic_loss - critic_loss = self.config.critic_loss_func(state_values, return_est) - loss = critic_loss + self.config.actor_loss_coefficient * actor_loss - - self.model.step(loss) + # actor loss + action_prob, state_values = self.ac_net(states) + log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) + if self.config.clip_ratio is not None: + ratio = torch.exp(log_p_new - log_p) + clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() + else: + actor_loss = -(log_p_new * advantages).mean() + + # critic_loss + critic_loss = self.config.critic_loss_func(state_values, return_est) + loss = critic_loss + self.config.actor_loss_coefficient * actor_loss + + self.ac_net.step(loss) diff --git a/maro/rl/algorithm/agent_manager.py b/maro/rl/algorithm/agent_manager.py deleted file mode 100644 index 6768ea7fe..000000000 --- a/maro/rl/algorithm/agent_manager.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -from typing import Union - -from .abs_agent import AbsAlgorithm - - -class AgentManager: - """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. - - Args: - agent_dict (Union[AbsAlgorithm, dict]): A single agent or a homogeneous set of agents that have the same - method signatures. - """ - def __init__( - self, - agent_id_list: list, - policy_mapping: dict, - model_mapping: dict, - model_dict: dict, - experience_memory_dict: dict - ): - self.members = set() - self.agent_dict = {} - for agent_id, agent_cls in agent2cls.items(): - - - def __getitem__(self, agent_id): - if len(self.agent_dict) == 1: - return self.agent_dict["AGENT"] - else: - return self.agent_dict[agent_id] - - def __len__(self): - return len(self.agent_dict) - - @property - def names(self): - return self._names - - def choose_action(self, state_by_agent: dict): - return {agent_id: self.agent_dict[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} - - def set_exploration_params(self, params): - # Per-agent exploration parameters - if isinstance(params, dict) and params.keys() <= self.agent_dict.keys(): - for agent_id, params in params.items(): - self.agent_dict[agent_id].set_exploration_params(**params) - # Shared exploration parameters for all agents - else: - for agent in self.agent_dict.values(): - agent.set_exploration_params(**params) - - def learn(self, experiences: dict) -> set: - """Store experiences in the agents' experience memory. - - The top-level keys of ``experiences`` will be treated as agent IDs. - """ - return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].learn(exp)} - - def step(self, agent_ids=None): - if agent_ids is None: - for agent in self.agent_dict.values(): - agent.step() - elif not isinstance(agent_ids, set): - self.agent_dict[agent_ids].step() - else: - for agent_id in agent_ids: - self.agent_dict[agent_id].step() - - def load_model(self, model_dict: dict): - """Load models from memory.""" - for agent_id, model in model_dict.items(): - self.agent_dict[agent_id].load_model(model) - - def dump_model(self, agent_ids=None): - """Get agents' underlying models. - - This is usually used in distributed mode where models need to be broadcast to remote roll-out actors. - """ - if agent_ids is None: - return {agent_id: agent.dump_model() for agent_id, agent in self.agent_dict.items()} - elif not isinstance(agent_ids, set): - return self.agent_dict[agent_ids].dump_model() - else: - return {agent_id: self.agent_dict[agent_id].dump_model() for agent_id in agent_ids} - - def load_model_from_file(self, dir_path): - """Load models from disk.""" - for agent_id, agent in self.agent_dict.items(): - agent.load_model_from_file(os.path.join(dir_path, agent_id)) - - def dump_model_to_file(self, dir_path: str, agent_ids=None): - """Dump agents' models to disk. - - Each agent will use its own name to create a separate file under ``dir_path`` for dumping. - """ - os.makedirs(dir_path, exist_ok=True) - if agent_ids is None: - for agent_id, agent in self.agent_dict.items(): - agent.dump_model_to_file(os.path.join(dir_path, agent_id)) - elif not isinstance(agent_ids, set): - self.agent_dict[agent_ids].dump_model_to_file(os.path.join(dir_path, agent_ids)) - else: - for agent_id in agent_ids: - self.agent_dict[agent_id].dump_model_to_file(os.path.join(dir_path, agent_id)) - - def get_versions(self, agent_ids=None): - if agent_ids is None: - return {agent_id: agent.version for agent_id, agent in self.agent_dict.items()} - elif not isinstance(agent_ids, set): - return self.agent_dict[agent_ids].version - else: - return {agent_id: self.agent_dict[agent_id].version for agent_id in agent_ids} - - def set_versions(self, version_index_by_agent: dict): - for agent_id, version_index in version_index_by_agent.items(): - self.agent_dict[agent_id].version = version_index diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index 7a8d42393..017ad76c5 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -1,18 +1,19 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import namedtuple from typing import Union import numpy as np import torch from maro.rl.exploration import NoiseExplorer -from maro.rl.model import SimpleMultiHeadModel +from maro.rl.model import PolicyValueNetForContinuousActionSpace from maro.rl.utils import get_torch_loss_cls -from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask -from .abs_agent import AbsAlgorithm +from .abs_policy import AbsPolicy +DDPGExperience = namedtuple("DDPGExperience", ["state", "action", "reward", "next_state"]) class DDPGConfig: """Configuration for the DDPG algorithm. @@ -48,7 +49,7 @@ def __init__( self.soft_update_coefficient = soft_update_coefficient -class DDPG(AbsAlgorithm): +class DDPG(AbsPolicy): """The Deep Deterministic Policy Gradient (DDPG) algorithm. References: @@ -56,78 +57,50 @@ class DDPG(AbsAlgorithm): https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg Args: - model (SimpleMultiHeadModel): DDPG policy and q-value models. + ac_net (PolicyValueNetForContinuousActionSpace): DDPG policy and q-value models. config (DDPGConfig): Configuration for DDPG algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. Defaults to False. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. - explorer (NoiseExplorer): An NoiseExplorer instance for generating exploratory actions. Defaults to None. """ - def __init__( - self, - model: SimpleMultiHeadModel, - config: DDPGConfig, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool = False, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1, - explorer: NoiseExplorer = None - ): - if model.task_names is None or set(model.task_names) != {"policy", "q_value"}: - raise UnrecognizedTask(f"Expected model task names 'policy' and 'q_value', but got {model.task_names}") - super().__init__( - model, config, experience_memory_size, experience_memory_overwrite_type, - empty_experience_memory_after_step, - min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning - ) + def __init__(self, ac_net: PolicyValueNetForContinuousActionSpace, config: DDPGConfig, explorer: NoiseExplorer = None): + if not isinstance(ac_net, PolicyValueNetForContinuousActionSpace): + raise TypeError("model must be an instance of 'PolicyValueNetForContinuousActionSpace'") + + super().__init__(config) + self.ac_net = ac_net + self.target_ac_net = ac_net.copy() if model.trainable else None self._explorer = explorer - self._target_model = model.copy() if model.trainable else None self._train_cnt = 0 - def choose_action(self, state) -> Union[float, np.ndarray]: - state = torch.from_numpy(state).to(self.device) - is_single = len(state.shape) == 1 - if is_single: - state = state.unsqueeze(dim=0) + def choose_action(self, states) -> Union[float, np.ndarray]: + with torch.no_grad(): + actions = self.ac_net.choose_action(states) - action = self.model(state, task_name="policy", training=False).data.cpu().numpy() - action_dim = action.shape[1] + actions = actions.cpu().numpy() if self._explorer: action = self._explorer(action) + + return actions[0] if len(actions) == 1 else actions - if action_dim == 1: - action = action.squeeze(axis=1) - - return action[0] if is_single else action + def update(self, experience_obj: DDPGExperience): + if not isinstance(experience_obj, DDPGExperience): + raise TypeError(f"Expected experience object of type DDPGExperience, got {type(experience_obj)}") - def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): - states = torch.from_numpy(states).to(self.device) - actual_actions = torch.from_numpy(actions).to(self.device) - rewards = torch.from_numpy(rewards).to(self.device) - next_states = torch.from_numpy(next_states).to(self.device) + states, next_states = experience_obj.state, experience_obj.next_state + actual_actions = torch.from_numpy(experience_obj.action) + rewards = torch.from_numpy(experience_obj.reward) if len(actual_actions.shape) == 1: actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - current_q_values = self.model(torch.cat([states, actual_actions], dim=1), task_name="q_value") + current_q_values = self.ac_net(torch.cat([states, actual_actions], dim=1), task_name="q_value") current_q_values = current_q_values.squeeze(dim=1) # (N,) - next_actions = self._target_model(states, task_name="policy", training=False) - next_q_values = self._target_model( + next_actions = self.target_ac_net(states, task_name="policy", training=False) + next_q_values = self.target_ac_net( torch.cat([next_states, next_actions], dim=1), task_name="q_value", training=False ).squeeze(1) # (N,) target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) q_value_loss = self.config.q_value_loss_func(current_q_values, target_q_values) - actions_from_model = self.model(states, task_name="policy") - policy_loss = -self.model(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() - self.model.step(q_value_loss + self.config.policy_loss_coefficient * policy_loss) + actions_from_model = self.ac_net(states, task_name="policy") + policy_loss = -self.ac_net(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() + self.ac_net.step(q_value_loss + self.config.policy_loss_coefficient * policy_loss) self._train_cnt += 1 if self._train_cnt % self.config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.config.soft_update_coefficient) + self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coefficient) diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index 26ddc9e13..aecd67c1f 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -1,15 +1,18 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import namedtuple from typing import Union import numpy as np import torch -from maro.rl.model import QEstimatorForDiscreteActions +from maro.rl.model import QNetForDiscreteActionSpace from maro.rl.utils import get_max, get_sampler_cls, get_td_errors, get_torch_loss_cls, select_by_actions -from .abs_agent import AbsAlgorithm +from .abs_policy import AbsPolicy + +DQNExperience = namedtuple("DQNExperience", ["state", "action", "reward", "next_state"]) class DQNConfig: @@ -18,11 +21,6 @@ class DQNConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. target_update_freq (int): Number of training rounds between target model updates. - train_iters (int): Number of batches to train the model on in each call to ``learn``. - batch_size (int): Experience minibatch size. - sampler_cls: A string indicating the sampler class or a custom sampler class that provides the ``sample`` - interface. Defaults to "uniform". - sampler_params (dict): Parameters for the sampler class. Defaults to None. epsilon (float): Exploration rate for epsilon-greedy exploration. Defaults to None. soft_update_coefficient (float): Soft update coefficient, e.g., target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. @@ -30,50 +28,38 @@ class DQNConfig: double (bool): If True, the next Q values will be computed according to the double DQN algorithm, i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. - advantage_type (str): Advantage mode for the dueling architecture. Defaults to None, in which - case it is assumed that the regular Q-value model is used. loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". """ __slots__ = [ "reward_discount", "target_update_freq", "train_iters", "batch_size", "sampler_cls", "sampler_params", - "epsilon", "soft_update_coefficient", "double", "advantage_type", "loss_func" + "epsilon", "soft_update_coefficient", "double", "loss_func" ] def __init__( self, reward_discount: float, target_update_freq: int, - train_iters: int, - batch_size: int, - sampler_cls="uniform", - sampler_params=None, epsilon: float = .0, soft_update_coefficient: float = 0.1, double: bool = True, - advantage_type: str = None, loss_cls="mse" ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq - self.train_iters = train_iters - self.batch_size = batch_size - self.sampler_cls = get_sampler_cls(sampler_cls) - self.sampler_params = sampler_params if sampler_params else {} self.epsilon = epsilon self.soft_update_coefficient = soft_update_coefficient self.double = double - self.advantage_type = advantage_type self.loss_func = get_torch_loss_cls(loss_cls)() -class DQN(AbsAlgorithm): +class DQN(AbsPolicy): """The Deep-Q-Networks algorithm. See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: - model (QEstimatorForDiscreteActions): Q-value model. + model (QNetForDiscreteActionSpace): Q-value model. config (DQNConfig): Configuration for DQN algorithm. experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of unlimited size. @@ -86,32 +72,21 @@ class DQN(AbsAlgorithm): min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for training. Defaults to 1. """ - def __init__( - self, - model: QEstimatorForDiscreteActions, - config: DQNConfig, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool = False, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 - ): - super().__init__( - model, config, experience_memory_size, experience_memory_overwrite_type, - empty_experience_memory_after_step, - min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning - ) - self._sampler = self.config.sampler_cls(self.experience_memory, **self.config.sampler_params) + def __init__(self, q_net: QNetForDiscreteActionSpace, config: DQNConfig): + if not isinstance(q_net, QNetForDiscreteActionSpace): + raise TypeError("model must be an instance of 'QNetForDiscreteActionSpace'") + + super().__init__(config) + self.q_net = q_net + self.target_q_net = model.copy() if model.trainable else None + self.target_q_net.eval() self._training_counter = 0 - self._target_model = model.copy() if model.trainable else None - self._target_model.eval() - self._num_actions = self.model.num_actions + self._num_actions = self.q_net.num_actions def choose_action(self, states) -> Union[int, np.ndarray]: with torch.no_grad(): - self.model.eval() - actions, q_vals = self.model.choose_action(states) + self.q_net.eval() + actions, q_vals = self.q_net.choose_action(states) actions, q_vals = actions.cpu().numpy(), q_vals.cpu().numpy() if len(actions) == 1: @@ -122,34 +97,32 @@ def choose_action(self, states) -> Union[int, np.ndarray]: for action in actions ]) - def step(self): - self.model.train() - for _ in range(self.config.train_iters): - # sample from the replay memory - indexes, batch = self._sampler.sample(self.config.batch_size) - states, next_states = batch["S"], batch["S_"] - actions = torch.from_numpy(batch["A"]) - rewards = torch.from_numpy(batch["R"]) - q_values = self.model(states, actions) - # get next Q values - with torch.no_grad(): - if self.config.double: - next_q_values = self._target_model(next_states, self.model.choose_action(next_states)[0]) # (N,) - else: - next_q_values = self._target_model.choose_action(next_states)[1] # (N,) - - # get TD errors - target_q_values = (rewards + self.config.gamma * next_q_values).detach() # (N,) - loss = self.config.loss_func(q_values, target_q_values) - - # train and update target if necessary - self.model.step(loss.mean()) - self._training_counter += 1 - if self._training_counter % self.config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.config.soft_update_coefficient) - - # update auxillary info for the next round of sampling - self._sampler.update(indexes, loss.detach().numpy()) + def update(self, experience_obj: DQNExperience): + if not isinstance(experience_obj, DQNExperience): + raise TypeError(f"Expected experience object of type DQNExperience, got {type(experience_obj)}") + + self.q_net.train() + # sample from the replay memory + states, next_states = experience_obj.state, experience_obj.next_state + actions = torch.from_numpy(experience_obj.action) + rewards = torch.from_numpy(experience_obj.reward) + q_values = self.q_net(states, actions) + # get next Q values + with torch.no_grad(): + if self.config.double: + next_q_values = self.target_q_net(next_states, self.q_net.choose_action(next_states)[0]) # (N,) + else: + next_q_values = self.target_q_net.choose_action(next_states)[1] # (N,) + + # get TD errors + target_q_values = (rewards + self.config.gamma * next_q_values).detach() # (N,) + loss = self.config.loss_func(q_values, target_q_values) + + # train and update target if necessary + self.q_net.step(loss.mean()) + self._training_counter += 1 + if self._training_counter % self.config.target_update_freq == 0: + self.target_q_net.soft_update(self.q_net, self.config.soft_update_coefficient) def set_exploration_params(self, epsilon): self.config.epsilon = epsilon diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index 55e9e5f14..4b597d2c3 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -10,7 +10,7 @@ from maro.rl.model import ParameterizedPolicy from maro.rl.utils import get_truncated_cumulative_reward -from .abs_agent import AbsAlgorithm +from .abs_policy import AbsPolicy class PolicyGradientConfig: @@ -25,7 +25,7 @@ def __init__(self, reward_discount: float): self.reward_discount = reward_discount -class PolicyGradient(AbsAlgorithm): +class PolicyGradient(AbsPolicy): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. diff --git a/maro/rl/algorithm/torch_loss_cls_index.py b/maro/rl/algorithm/torch_loss_cls_index.py deleted file mode 100644 index 4b08e1ef6..000000000 --- a/maro/rl/algorithm/torch_loss_cls_index.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from torch import nn - -# For details, see https://pytorch.org/docs/stable/nn.html#loss-functions -TORCH_LOSS_CLS = { - "l1": nn.L1Loss, - "mse": nn.MSELoss, - "cross_entropy": nn.CrossEntropyLoss, - "ctc": nn.CTCLoss, - "nll": nn.NLLLoss, - "poisson_nll": nn.PoissonNLLLoss, - "kl": nn.KLDivLoss, - "bce": nn.BCELoss, - "bce_logits": nn.BCEWithLogitsLoss, - "margin_ranking": nn.MarginRankingLoss, - "hinge_embedding": nn.HingeEmbeddingLoss, - "multi_label_margin": nn.MultiLabelMarginLoss, - "multi_label_soft_margin": nn.MultiLabelSoftMarginLoss, - "smooth_l1": nn.SmoothL1Loss, - "soft_margin": nn.SoftMarginLoss, - "cosine_embedding": nn.CosineEmbeddingLoss, - "multi_margin": nn.MultiMarginLoss, - "triplet_margin": nn.TripletMarginLoss, -} \ No newline at end of file diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py index 77d35030b..f775698d3 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/distributed/actor.py @@ -5,7 +5,7 @@ from typing import Union from maro.communication import Proxy -from maro.rl.agent import AbsAlgorithm, MultiAgentWrapper +from maro.rl.multi_agent import MultiAgentPolicy from maro.rl.training import AbsEnvWrapper from maro.utils import Logger @@ -18,7 +18,7 @@ class Actor(object): Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsAlgorithm, MultiAgentWrapper]): Agent that interacts with the environment. + policy (MultiAgentPolicy): Agent that interacts with the environment. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class @@ -27,14 +27,14 @@ class Actor(object): def __init__( self, env: AbsEnvWrapper, - agent: Union[AbsAlgorithm, MultiAgentWrapper], + policy: MultiAgentPolicy, group: str, proxy_options: dict = None, pull_experiences_with_copy: bool = False, log_dir: str = getcwd() ): self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAlgorithm) else agent + self.policy = policy self._pull_experiences_with_copy = pull_experiences_with_copy if proxy_options is None: proxy_options = {} @@ -52,14 +52,14 @@ def run(self): self.env.reset() # Load exploration parameters if MsgKey.EXPLORATION_PARAMS in msg.body: - self.agent.set_exploration_params(msg.body[MsgKey.EXPLORATION_PARAMS]) + self.policy.update_exploration_params(msg.body[MsgKey.EXPLORATION_PARAMS]) self.env.start(rollout_index=rollout_index) # get initial state starting_step_index = self.env.step_index - self.agent.load_model(msg.body[MsgKey.MODEL]) + self.policy.load(msg.body[MsgKey.POLICY]) steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while self.env.state and steps_to_go > 0: - action = self.agent.choose_action(self.env.state) + action = self.policy.choose_action(self.env.state) self.env.step(action) steps_to_go -= 1 diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py index 7458d8afe..47b52ab7c 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/distributed/actor_manager.py @@ -15,7 +15,7 @@ class ActorManager(object): """Learner class for distributed training. Args: - agent (Union[AbsAlgorithm, MultiAgentWrapper]): Learning agents. + agent (Union[AbsPolicy, MultiAgentWrapper]): Learning agents. scheduler (Scheduler): . num_actors (int): Expected number of actors in the group identified by ``group_name``. group_name (str): Identifier of the group to which the actor belongs. It must be the same group name @@ -50,7 +50,7 @@ def collect( rollout_index: int, segment_index: int, num_steps: int, - models: dict = None, + policy_dict: dict = None, exploration_params=None, required_actor_finishes: int = None, discard_stale_experiences: bool = True, @@ -64,7 +64,7 @@ def collect( MsgKey.ROLLOUT_INDEX: rollout_index, MsgKey.SEGMENT_INDEX: segment_index, MsgKey.NUM_STEPS: num_steps, - MsgKey.MODEL: models, + MsgKey.POLICY: policy_dict, MsgKey.RETURN_ENV_METRICS: return_env_metrics } diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py index 8ddb27918..f9ce5ae40 100644 --- a/maro/rl/distributed/learner.py +++ b/maro/rl/distributed/learner.py @@ -7,7 +7,7 @@ from typing import Union from maro.communication import Message, Proxy, SessionType -from maro.rl.agent import AbsAlgorithm, MultiAgentWrapper +from maro.rl.multi_agent import MultiAgentPolicy from maro.rl.scheduling import Scheduler from maro.utils import Logger @@ -19,12 +19,12 @@ class DistLearner(object): """Learner class for distributed training. Args: - agent (Union[AbsAlgorithm, MultiAgentWrapper]): Learning agents. + policy (MultiAgentPolicy): Learning agents. scheduler (Scheduler): A ``Scheduler`` instance for generating exploration parameters. """ def __init__( self, - agent: Union[AbsAlgorithm, MultiAgentWrapper], + policy: MultiAgentPolicy, scheduler: Scheduler, actor_manager: ActorManager, agent_update_interval: int = -1, @@ -33,7 +33,7 @@ def __init__( log_dir: str = getcwd() ): super().__init__() - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAlgorithm) else agent + self.policy = policy self.scheduler = scheduler self.actor_manager = actor_manager self.agent_update_interval = agent_update_interval @@ -46,19 +46,20 @@ def run(self): """Main learning loop.""" t0 = time.time() for exploration_params in self.scheduler: - updated_agents, num_actor_finishes, segment_index = self.agent.names, 0, 0 + num_actor_finishes, segment_index = 0, 0 while num_actor_finishes < self.required_actor_finishes: for exp, done in self.actor_manager.collect( self.scheduler.iter, segment_index, self.agent_update_interval, - models=self.agent.dump_model(agent_ids=updated_agents), + policy_dict=self.policy.policy_dict, exploration_params=exploration_params if segment_index == 0 else None, required_actor_finishes=self.required_actor_finishes, discard_stale_experiences=self.discard_stale_experiences ): tl0 = time.time() - updated_agents = self.agent.learn(exp) + self.policy.store_experiences(exp) + self.policy.update() num_actor_finishes += done self._total_learning_time += time.time() - tl0 self._logger.debug(f"total running time: {time.time() - t0}") diff --git a/maro/rl/distributed/message_enums.py b/maro/rl/distributed/message_enums.py index cf60aa879..efab023aa 100644 --- a/maro/rl/distributed/message_enums.py +++ b/maro/rl/distributed/message_enums.py @@ -23,7 +23,7 @@ class MsgKey(Enum): NUM_EXPERIENCES = "num_experiences" STATE = "state" TRAINING = "training" - MODEL = "model" + POLICY = "policy" VERSION = "version" EXPLORATION_PARAMS = "exploration_params" NUM_STEPS = "num_steps" diff --git a/maro/rl/exploration/abs_explorer.py b/maro/rl/exploration/abs_explorer.py index 40558b263..f64c62f8d 100644 --- a/maro/rl/exploration/abs_explorer.py +++ b/maro/rl/exploration/abs_explorer.py @@ -12,7 +12,7 @@ def __init__(self): pass @abstractmethod - def set_parameters(self, **exploration_params): + def update(self, exploration_params: dict): return NotImplementedError @abstractmethod diff --git a/maro/rl/exploration/epsilon_greedy_explorer.py b/maro/rl/exploration/epsilon_greedy_explorer.py index 5c9463140..bdd51825f 100644 --- a/maro/rl/exploration/epsilon_greedy_explorer.py +++ b/maro/rl/exploration/epsilon_greedy_explorer.py @@ -25,7 +25,7 @@ def __call__(self, action_index: Union[int, np.ndarray]): else: return self._get_exploration_action(action_index) - def set_parameters(self, *, epsilon: float): + def update(self, *, epsilon: float): self._epsilon = epsilon def _get_exploration_action(self, action_index): diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 05d23e934..c6815940c 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -4,12 +4,13 @@ from .abs_block import AbsBlock from .fc_block import FullyConnectedBlock from .learning_model import ( - AbsCoreModel, OptimOption, ParameterizedPolicy, ParameterizedPolicyWithValueEstimator, QEstimatorForDiscreteActions + AbsCoreModel, OptimOption, PolicyNetForDiscreteActionSpace, PolicyValueNetForContinuousActionSpace, + PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace ) __all__ = [ "AbsBlock", "FullyConnectedBlock", - "AbsCoreModel", "OptimOption", "ParameterizedPolicy", "ParameterizedPolicyWithValueEstimator", - "QEstimatorForDiscreteActions" + "AbsCoreModel", "OptimOption", "PolicyNetForDiscreteActionSpace", "PolicyValueNetForContinuousActionSpace", + "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace" ] diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index ab47bcf6a..0c9b01788 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -130,7 +130,7 @@ def copy(self, with_optimizer: bool = False): return model_copy -class QEstimatorForDiscreteActions(AbsCoreModel): +class QNetForDiscreteActionSpace(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: @@ -159,7 +159,7 @@ def num_actions(self): raise NotImplementedError -class ParameterizedPolicy(AbsCoreModel): +class PolicyNetForDiscreteActionSpace(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: @@ -184,7 +184,7 @@ def choose_action(self, states): return action, log_p -class ParameterizedPolicyWithValueEstimator(AbsCoreModel): +class PolicyValueNetForDiscreteActionSpace(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: @@ -203,7 +203,29 @@ def choose_action(self, states): Given Q-values for a batch of states and all actions, return the maximum Q-value and the corresponding action index for each state. """ - action_prob = Categorical(self.forward(state, output_values=False)) # (batch_size, num_actions) + action_prob = Categorical(self.forward(states, output_values=False)) # (batch_size, num_actions) action = action_prob.sample() log_p = action_prob.log_prob(action) return action, log_p + + +class PolicyValueNetForContinuousActionSpace(AbsCoreModel): + """A compound network structure that consists of multiple task heads and an optional shared stack. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + All components must have the same input dimension except the one designated as the shared + component by ``shared_component_name``. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for + the components. Defaults to None. + """ + @abstractmethod + def forward(self, states, actions, output_action: bool = True, output_values: bool = True): + raise NotImplementedError + + def choose_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the maximum Q-value and + the corresponding action index for each state. + """ + return self.forward(states, output_values=False) diff --git a/maro/rl/multi_agent/__init__.py b/maro/rl/multi_agent/__init__.py new file mode 100644 index 000000000..bd4d7a299 --- /dev/null +++ b/maro/rl/multi_agent/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .multi_agent_policy import MultiAgentPolicyForInference, MultiAgentPolicyForTraining + +__all__ = ["MultiAgentPolicyForInference", "MultiAgentPolicyForTraining"] diff --git a/maro/rl/multi_agent/multi_agent_policy.py b/maro/rl/multi_agent/multi_agent_policy.py new file mode 100644 index 000000000..387e54b68 --- /dev/null +++ b/maro/rl/multi_agent/multi_agent_policy.py @@ -0,0 +1,116 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import pickle +from typing import Dict, Union + +from maro.rl.algorithm import AbsFixedPolicy, AbsTrainablePolicy +from maro.rl.exploration import AbsExplorer +from maro.rl.storage import SimpleStore + + +class TrainingConfig: + __slots__ = ["sampler_cls", "sampler_params", "train_iters", "min_experiences_to_trigger_learning"] + + def __init__( + self, + sampler_cls, + sampler_params: dict, + train_iters: int, + min_experience_to_trigger_learning: int = 1 + ): + self.sampler_cls = sampler_cls + self.sampler_params = sampler_params + self.train_iters = train_iters + self.min_experiences_to_trigger_learning = min_experiences_to_trigger_learning + + +class MultiAgentPolicyForInference: + """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. + + Args: + + """ + def __init__( + self, + policy_dict: Dict[str, Union[AbsFixedPolicy, AbsTrainablePolicy]], + agent_to_policy: Dict[str, str], + explorer_dict: Dict[str, AbsExplorer] = None, + agent_to_explorer: Dict[str, str] = None + ): + self.policy_dict = policy_dict + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in agent_to_policy.items()} + + if explorer_dict is not None: + self.explorer = { + agent_id: explorer_dict[explorer_id] for agent_id, explorer_id in agent_to_explorer.items() + } + + def choose_action(self, state_by_agent: dict): + return {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} + + def update_exploration_params(self, param_dict: dict): + # Per-agent exploration parameters + for policy_id, params in param_dict.items(): + self.policy_dict[policy_id].set_exploration_params(params) + + def load(self, policy_dict: dict): + """Load models from memory.""" + self.policy_dict = policy_dict + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in agent_to_policy.items()} + + def load_from_file(self, path: str): + """Load models from disk.""" + with open(path, "rb") as fp: + pickle.load(fp) + + +class MultiAgentPolicyForTraining: + """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. + + Args: + + """ + def __init__( + self, + policy_dict: Dict[str, Union[AbsFixedPolicy, AbsTrainablePolicy]], + agent_to_policy: Dict[str, str], + experience_memory_dict: Dict[str, SimpleStore], + agent_to_experience_memory: Dict[str, str], + training_config_dict: Dict[str, TrainingConfig], + agent_to_training_config: Dict[str, str] + ): + self.policy_dict = policy_dict + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in agent_to_policy.items()} + + assert agent_to_experience_memory.keys() == agent_to_training_config.keys() + self.experience_memory = { + agent_id: experience_memory_dict[mem_id] for agent_id, mem_id in agent_to_experience_memory.items() + } + + self.training_config = { + agent_id: training_config_dict[config_id] for agent_id, config_id in agent_to_training_config.items() + } + + self.sampler = { + agent_id: cfg.sampler_cls(self.experience_memory[agent_id], **cfg.sampler_params) + for agent_id, cfg in self.training_config.items() + } + + def store_experiences(self, experiences_by_agent: dict): + """Store experiences in the agents' experience memory. + + The top-level keys of ``experiences`` will be treated as agent IDs. + """ + for agent_id, exp in experiences_by_agent.items(): + self.experience_memory[agent_id].put(exp) + + def update(self): + for agent_id, config in self.training_config.items(): + if len(self.experience_memory[agent_id]) >= config.min_experiences_to_trigger_learning: + for _ in config.train_iters: + self.policy[agent_id].update(self.sampler[agent_id].sample()) + + def save(self, path: dir): + with open(path, "wb") as fp: + pickle.save(self.policy_dict, path) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index b4f646d76..04e11c08c 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -5,7 +5,7 @@ from collections import defaultdict from typing import Dict, Union -from maro.rl.agent import AbsAlgorithm, MultiAgentWrapper +from maro.rl.agent import AbsPolicy, MultiAgentWrapper from maro.rl.scheduling import Scheduler from maro.utils import InternalLogger @@ -18,12 +18,12 @@ class Learner(object): Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsAlgorithm, MultiAgentWrapper]): Agent that interacts with the environment. + agent (Union[AbsPolicy, MultiAgentWrapper]): Agent that interacts with the environment. """ def __init__( self, env: AbsEnvWrapper, - agent: Union[AbsAlgorithm, MultiAgentWrapper], + agent: Union[AbsPolicy, MultiAgentWrapper], scheduler: Scheduler, agent_update_interval: int = -1, log_env_metrics: bool = False @@ -32,7 +32,7 @@ def __init__( if agent_update_interval == 0: raise ValueError("agent_update_interval must be a positive integer or None.") self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAlgorithm) else agent + self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsPolicy) else agent self.scheduler = scheduler self.agent_update_interval = agent_update_interval self.total_env_steps = 0 From ff76e88e4923cc2ec920cc482cf00ba4d95b5530 Mon Sep 17 00:00:00 2001 From: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> Date: Thu, 29 Apr 2021 15:17:51 +0800 Subject: [PATCH 205/482] V0.2 offline ILP method for vm scheduling (#275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ilp for vm scheduling runnable * remove code for debug * add simple time consumption analysis to README * update comment, add logger * change to offline interface, 1 formulation for 1 tick * fix the bug caused by wrong time index in ILP constraints * fix ILP bug caused by limited pm resource * 10k bug fixed * ilp for vm scheduling runnable * remove code for debug * add simple time consumption analysis to README * update comment, add logger * change to offline interface, 1 formulation for 1 tick * fix the bug caused by wrong time index in ILP constraints * fix ILP bug caused by limited pm resource * 10k bug fixed * V0.2 merged: MARO admin tool added, backend bug fixed, CLI doc updated (#324) * refine readme * feat: refine data push/pull (#138) * feat: refine data push/pull * test: add cli provision testing * fix: style fix * fix: add necessary comments * fix: from code review * add fall back function in weather download (#112) * fix deployment issue in multi envs * fix typo * fix ~/.maro not exist issue in build * skip deploy when build * update for comments * temporarily disable weather info * replace ecr with cim in setup.py * replace ecr in manifest * remove weather check when read data * fix station id issue * fix format * add TODO in comments * add noaa weather source * fix weather reset and weather comment * add comment for weather data url * some format update * add fall back function in weather download * update comment * update for comments * update comment * add period * fix for pylint * update for pylint check * added example docs (#136) * added example docs * added citibike greedy example doc * modified citibike doc * fixed PR comments * fixed more PR comments * fixed small formatting issue Co-authored-by: ysqyang * switch the key and value of handler_dict in decorator (#144) * switch the key and value of handler_dict in decorator * add dist decorator UT and fixed multithreading conflict in maro test suite * pr comments update. * resolved comments about decorator UT * rename handler_fun in dist decorator * change self.attr into class_name.attr * update UT tests comments * V0.1 annotation (#147) * refine the annotation of simulator core * remove reward from env(be) * format refined * white spaces test * left-padding spaces refined * format modifed * update the left-padding spaces of docstrings * code format updated * update according to comments * update according to PR comments Co-authored-by: Jinyu Wang * Event payload details for env.summary (#156) * key_list of events added for env.summary * code refined according to lint * 2 kinds of Payload added for CIM scenario; citi bike summary refined according to comments * code format refined * try trigger the git tests * update github workflow Co-authored-by: Jinyu Wang * V0.2 online lp for citi bike (#159) * key_list of events added for env.summary * code refined according to lint * 2 kinds of Payload added for CIM scenario; citi bike summary refined according to comments * code format refined * try trigger the git tests * update github workflow * online LP example added for citi bike * infeasible solution * infeasible solution fixed: call snapshot before any env.step() * experiment results of toy topos added * experiment results of toy topos added * experiment result update: better than naive baseline * PuLP version added * greedy experiment results update * citibike result update * modified according to PR comments * update experiment results and forecasting comparison * citi bike lp README updated * README updated * modified according to PR comments * update according to PR comments Co-authored-by: Jinyu Wang Co-authored-by: Jinyu Wang * V0.2 rl toolkit refinement (#165) * refined rl abstractions * fixed formattin issues * checked out error-code related code from v0.2_pg * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * renamed save_models to dump_models * 1. set default batch_norm_enabled to True; 2. used state_dict in dqn model saving * renamed dump_experience_store to dump_experience_pool * fixed a bug in the dump_experience_pool method * fixed some PR comments * fixed more PR comments * 1.fixed some PR comments; 2.added early_stopping_checker; 3.revised explorer class * fixed cim example according to rl toolkit changes * fixed some more PR comments * rewrote multi_process_launcher to eliminate the distributed section in config * 1. fixed a typo; 2. added logging before early stopping * fixed a bug * fixed a bug * fixed a bug * added early stopping feature to CIM exmaple * fixed a typo * fixed some issues with early stopping * changed early stopping metric func * fixed a bug * fixed a bug * added early stopping to dist mode cim * added experience collecting func * edited notebook according to changes in CIM example * fixed bugs in nb * fixed lint formatting issues * fixed a typo * fixed some PR comments * fixed more PR comments * revised docs * removed nb output * fixed a bug in simple_learner * fixed a typo in nb * fixed a bug * fixed a bug * fixed a bug * removed unused import * fixed a bug * 1. changed early stopping default config; 2. renamed param in early stopping checker and added typing * fixed some doc issues * added output to nb Co-authored-by: ysqyang * update according to flake8 * V0.2 Logical operator overloading for EarlyStoppingChecker (#178) * 1. added logical operator overloading for early stopping checker; 2. added mean value checker * fixed PR comments * removed learner.exit() in single_process_launcher * added another early stopping checker in example * fixed PR comments and lint issues * lint issue fix * fixed lint issues * fixed a bug * fixed a bug Co-authored-by: ysqyang * V0.2 skip connection (#176) * replaced IdentityLayers with nn.Identity * 1. added skip connection option in FC_net; 2. generalized learning model * added skip_connection option in config * removed type casting in fc_net * fixed lint formatting issues * refined docstring * added multi-head functionality to LearningModel * refined learning model docstring * added head_key param in learningModel forward * fixed PR comments * added top layer logic and is_top option in fc_net * fixed a bug * fixed a bug * reverted some changes in learning model * reverted some changes in learning model * added members to learning model to fix the mode issue * fixed a bug * fixed mode setting issue in learning model * removed learner.exit() in single_process_launcher * fixed PR comments * fixed rl/__init__ * fixed issues in example * fixed a bug * fixed a bug * fixed lint formatting issues * moved reward type casting to exp shaper Co-authored-by: ysqyang * fixed a bug in learner's test() (#193) Co-authored-by: ysqyang * V0.2 double dqn (#188) * added dueling action value model * renamed params in dueling_action_value_model * renamed shared_features to features * replaced IdentityLayers with nn.Identity * 1. added skip connection option in FC_net; 2. generalized learning model * added skip_connection option in config * removed type casting in fc_net * fixed lint formatting issues * refined docstring * mv dueling_actiovalue_model and fixed some bugs * added multi-head functionality to LearningModel * refined learning model docstring * added head_key param in learningModel forward * added double DQN and dueling features to DQN * fixed a bug * added DuelingQModelHead enum * fixed a bug * removed unwanted file * fixed PR comments * added top layer logic and is_top option in fc_net * fixed a bug * fixed a bug * reverted some changes in learning model * reverted some changes in learning model * added members to learning model to fix the mode issue * fixed a bug * fixed mode setting issue in learning model * fixed PR comments * revised cim example according to DQN changes * renamed eval_model to q_value_model in cim example * more fixes * fixed a bug * fixed a bug * added doc per PR comments * removed learner.exit() in single_process_launcher * removed learner.exit() in single_process_launcher * fixed PR comments * fixed rl/__init__ * fixed issues in example * fixed a bug * fixed a bug * fixed lint formatting issues * double DQN feature * fixed a bug * fixed a bug * fixed PR comments * fixed lint issue * 1. fixed PR comments related to load/dump; 2. removed abstract load/dump methods from AbsAlgorithm * added load_models in simple_learner * minor docstring edits * minor docstring edits * set is_double to true in DQN config Co-authored-by: ysqyang Co-authored-by: Arthur Jiang * V0.2 feature predefined image (#183) * feat: support predefined image provision * style: fix linting errors * style: fix linting errors * style: fix linting errors * style: fix linting errors * fix: error scripts invocation after using relative import * fix: missing init.py * fixed a bug in learner's test() * feat: add distributed_config for dqn example * test: update test for grass * test: update test for k8s * feat: add promptings for steps * fix: change relative imports to absolute imports Co-authored-by: ysqyang Co-authored-by: Arthur Jiang * V0.2 feature proxy rejoin (#158) * update dist decorator * replace proxy.get_peers by proxy.peers * update proxy rejoin (draft, not runable for proxy rejoin) * fix bugs in proxy * add message cache, and redesign rejoin parameter * feat: add checkpoint with test * update proxy.rejoin * fixed rejoin bug, rename func * add test example(temp) * feat: add FaultToleranceAgent, refine other MasterAgents and NodeAgents. * capital env vari name * rm json.dumps; change retries to 10; temp add warning level for rejoin * fix: unable to load FaultToleranceAgent, missing params * fix: delete mapping in StopJob if FaultTolerance is activated, add exception handler for FaultToleranceAgent * feat: add node_id to node_details * fix: add a new dependency for tests * style: meet linting requirements * style: remaining linting problems * lint fixed; rm temp test folder. * fixed lint f-string without placeholder * fix: add a flag for "remove_container", refine restart logic and Redis keys naming * proxy rejoin update. * variable rename. * fixed lint issues * fixed lint issues * add exit code for different error * feat: add special errors handler * add max rejoin times * remove unused import * add rejoin UT; resolve rejoin comments * lint fixed * fixed UT import problem * rm MessageCache in proxy * fix: refine key naming * update proxy rejoin; add topic for broadcast * feat: support predefined image provision * update UT for communication * add docstring for rejoin * fixed isort and zmq driver import * fixed isort and UT test * fix isort issue * proxy rejoin update (comments v2) * fixed isort error * style: fix linting errors * style: fix linting errors * style: fix linting errors * style: fix linting errors * feat: add exists method for checkpoint * fix: error scripts invocation after using relative import * fix: missing init.py * fixed a bug in learner's test() * add driver close and socket SUB disconnect for rejoin * feat: add distributed_config for dqn example * test: update test for grass * test: update test for k8s * feat: add promptings for steps * fix: change relative imports to absolute imports * fixed comments and update logger level * mv driver in proxy.__init__ for issue temp fixed. * Update docstring and comments * style: fix code reviews problems * fix code format Co-authored-by: Lyuchun Huang Co-authored-by: ysqyang * V0.2 feature cli windows (#203) * fix: change local mkdir to os.makedirs * fix: add utf8 encoding for logger * fix: add powershell.exe prefix to subprocess functions * feat: add debug_green * fix: use fsutil to create fix-size files in Windows * fix: use universal_newlines=True to handle encoding problem in different operating systems * fix: use temp file to do copy when the operating system is not Linux * fix: linting error * fix: use fsutil in test_k8s.py * feat: dynamic init ABS_PATH in GlobalParams * fix: use -Command to execute Powershell command * fix: refine code style in k8s_azure_executor.py, add Windows support for k8s mode * fix: problems in code review * EventBuffer refine (#197) * merge uniform event changes back * 1st step: move executing events into stack for better removing performance * flush event pool * typo * add option for env to enable event pool * refine stack functions * fix comment issues, add typings * lint fixing * lint fix * add missing fix * linting * lint * use linked list instead original event list and execute stack * add missing file * linting, and fixes * add missing file * linting fix * fixing comments * add missing file * rename event_list to event_linked_list * correct import path * change enable_event_pool to disable_finished_events * add missing file * V0.2 merge master (#214) * fix the visualization of docs/key_components/distributed_toolkit * add examples into isort ignore * refine import path for examples (#195) * refine import path for examples * refine indents * fixed formatting issues * update code style * add editorconfig-checker, add editorconfig path into lint, change super-linter version * change path for code saving in cim.gnn Co-authored-by: Jinyu Wang Co-authored-by: ysqyang Co-authored-by: Wenlei Shi * fix issue that sometimes there is conflict between distutils and setuptools (#208) * fix issue that cython and setuptools conflict * follow the accepted temp workaround * update comment, it should be conflict between setuptools and distutils * fixed bugs related to proxy interface changes Co-authored-by: Jinyu Wang Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> Co-authored-by: ysqyang Co-authored-by: Wenlei Shi Co-authored-by: Chaos Yu * typo fix * Bug fix: event buffer issue that cause Actions cannot be passed into business engine (#215) * bug fix * clear the reference after extract sub events, update ut to cover this issue Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> * fix flake8 style problem * V0.2 feature refine mode namings (#212) * feat: refine cli exception * feat: refine mode namings * EventBuffer refine (#197) * merge uniform event changes back * 1st step: move executing events into stack for better removing performance * flush event pool * typo * add option for env to enable event pool * refine stack functions * fix comment issues, add typings * lint fixing * lint fix * add missing fix * linting * lint * use linked list instead original event list and execute stack * add missing file * linting, and fixes * add missing file * linting fix * fixing comments * add missing file * rename event_list to event_linked_list * correct import path * change enable_event_pool to disable_finished_events * add missing file * fixed bugs in dist rl * feat: rename files * tests: set longer gracefully wait time * style: fix linting errors * style: fix linting errors * style: fix linting errors * fix: rm redundant variables * fix: refine error message Co-authored-by: Chaos Yu Co-authored-by: ysqyang * V0.2 vis new (#210) Co-authored-by: Wenlei Shi Co-authored-by: Chaos Yu * V0.2 local host process (#221) * Update local process (not ready) * update cli process mode * add setup/clear/template for maro process * fix process stop * add logger and rename parameters * add logger for setup/clear * fixed close not exist pid when given pid list. * Fixed comments and rename setup/clear with create/delete * update ProcessInternalError * V0.2 grass on premises (#220) * feat: refine cli exception * commit on v0.2_grass_on_premises Co-authored-by: Lyuchun Huang Co-authored-by: Chaos Yu Co-authored-by: ysqyang * V0.2 vm scheduling scenario (#189) * Initialize * Data center scenario init * Code style modification * V0.2 event buffer subevents expand (#180) * V0.2 rl toolkit refinement (#165) * refined rl abstractions * fixed formattin issues * checked out error-code related code from v0.2_pg * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * renamed save_models to dump_models * 1. set default batch_norm_enabled to True; 2. used state_dict in dqn model saving * renamed dump_experience_store to dump_experience_pool * fixed a bug in the dump_experience_pool method * fixed some PR comments * fixed more PR comments * 1.fixed some PR comments; 2.added early_stopping_checker; 3.revised explorer class * fixed cim example according to rl toolkit changes * fixed some more PR comments * rewrote multi_process_launcher to eliminate the distributed section in config * 1. fixed a typo; 2. added logging before early stopping * fixed a bug * fixed a bug * fixed a bug * added early stopping feature to CIM exmaple * fixed a typo * fixed some issues with early stopping * changed early stopping metric func * fixed a bug * fixed a bug * added early stopping to dist mode cim * added experience collecting func * edited notebook according to changes in CIM example * fixed bugs in nb * fixed lint formatting issues * fixed a typo * fixed some PR comments * fixed more PR comments * revised docs * removed nb output * fixed a bug in simple_learner * fixed a typo in nb * fixed a bug * fixed a bug * fixed a bug * removed unused import * fixed a bug * 1. changed early stopping default config; 2. renamed param in early stopping checker and added typing * fixed some doc issues * added output to nb Co-authored-by: ysqyang * unfold sub-events, insert after parent * remove event category, use different class instead, add helper functions to gen decision and action event * add a method to support add immediate event to cascade event with tick validation * fix ut issue * add action as 1st sub event to ensure the executing order Co-authored-by: ysqyang Co-authored-by: ysqyang * Data center scenario update * Code style update * Data scenario business engine update * Isort update * Fix lint code check * Fix based on PR comments. * Update based on PR comments. * Add decision payload * Add config file * Update utilization series logic * Update based on PR comment * Update based on PR * Update * Update * Add the ValidPm class * Update docs string and naming * Add energy consumption * Lint code fixed * Refining postpone function * Lint style update * Init data pipeline * Update based on PR comment * Add data pipeline download * Lint style update * Code style fix * Temp update * Data pipeline update * Add aria2p download function * Update based on PR comment * Update based on PR comment * Update based on PR comment * Update naming of variables * Rename topology * Renaming * Fix valid pm list * Pylint fix * Update comment * Update docstring and comment * Fix init import * Update tick issue * fix merge problem * update style * V0.2 datacenter data pipeline (#199) * Data pipeline update * Data pipeline update * Lint update * Update pipeline * Add vmid mapping * Update lint style * Add VM data analytics * Update notebook * Add binary converter * Modift vmtable yaml * Update binary meta file * Add cpu reader * random example added for data center * Fix bugs * Fix pylint * Add launcher * Fix pylint * best fit policy added * Add reset * Add config * Add config * Modify action object * Modify config * Fix naming * Modify config * Add snapshot list * Modify a spelling typo * Update based on PR comments. * Rename scenario to vm scheduling * Rename scenario * Update print messages * Lint fix * Lint fix * Rename scenario * Modify the calculation of cpu utilization * Add comment * Modify data pipeline path * Fix typo * Modify naming * Add unittest * Add comment * Unify naming * Fix data path typo * Update comments * Update snapshot features * Add take snapshot * Add summary keys * Update cpu reader * Update naming * Add unit test * Rename snapshot node * Add processed data pipeline * Modify config * Add comment * Lint style fix Co-authored-by: Jinyu Wang * Add package used in vm_scheduling * add aria2p to test requirement * best fit example: update the usage of snapshot * Add aria2p to test requriement * Remove finish event * Fix unittest * Add test dataset * Update based on PR comment * Refine cpu reader and unittest * Lint update * Refine based on PR comment * Add agent index * Add node maping * Refine based on PR comments * Renaming postpone_step * Renaming and refine based on PR comments * Rename config * Update Co-authored-by: Jinyu Wang Co-authored-by: Chaos Yu Co-authored-by: ysqyang Co-authored-by: ysqyang Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> * Resolve none action problem (#224) * V0.2 vm_scheduling notebook (#223) * Initialize * Data center scenario init * Code style modification * V0.2 event buffer subevents expand (#180) * V0.2 rl toolkit refinement (#165) * refined rl abstractions * fixed formattin issues * checked out error-code related code from v0.2_pg * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * renamed save_models to dump_models * 1. set default batch_norm_enabled to True; 2. used state_dict in dqn model saving * renamed dump_experience_store to dump_experience_pool * fixed a bug in the dump_experience_pool method * fixed some PR comments * fixed more PR comments * 1.fixed some PR comments; 2.added early_stopping_checker; 3.revised explorer class * fixed cim example according to rl toolkit changes * fixed some more PR comments * rewrote multi_process_launcher to eliminate the distributed section in config * 1. fixed a typo; 2. added logging before early stopping * fixed a bug * fixed a bug * fixed a bug * added early stopping feature to CIM exmaple * fixed a typo * fixed some issues with early stopping * changed early stopping metric func * fixed a bug * fixed a bug * added early stopping to dist mode cim * added experience collecting func * edited notebook according to changes in CIM example * fixed bugs in nb * fixed lint formatting issues * fixed a typo * fixed some PR comments * fixed more PR comments * revised docs * removed nb output * fixed a bug in simple_learner * fixed a typo in nb * fixed a bug * fixed a bug * fixed a bug * removed unused import * fixed a bug * 1. changed early stopping default config; 2. renamed param in early stopping checker and added typing * fixed some doc issues * added output to nb Co-authored-by: ysqyang * unfold sub-events, insert after parent * remove event category, use different class instead, add helper functions to gen decision and action event * add a method to support add immediate event to cascade event with tick validation * fix ut issue * add action as 1st sub event to ensure the executing order Co-authored-by: ysqyang Co-authored-by: ysqyang * Data center scenario update * Code style update * Data scenario business engine update * Isort update * Fix lint code check * Fix based on PR comments. * Update based on PR comments. * Add decision payload * Add config file * Update utilization series logic * Update based on PR comment * Update based on PR * Update * Update * Add the ValidPm class * Update docs string and naming * Add energy consumption * Lint code fixed * Refining postpone function * Lint style update * Init data pipeline * Update based on PR comment * Add data pipeline download * Lint style update * Code style fix * Temp update * Data pipeline update * Add aria2p download function * Update based on PR comment * Update based on PR comment * Update based on PR comment * Update naming of variables * Rename topology * Renaming * Fix valid pm list * Pylint fix * Update comment * Update docstring and comment * Fix init import * Update tick issue * fix merge problem * update style * V0.2 datacenter data pipeline (#199) * Data pipeline update * Data pipeline update * Lint update * Update pipeline * Add vmid mapping * Update lint style * Add VM data analytics * Update notebook * Add binary converter * Modift vmtable yaml * Update binary meta file * Add cpu reader * random example added for data center * Fix bugs * Fix pylint * Add launcher * Fix pylint * best fit policy added * Add reset * Add config * Add config * Modify action object * Modify config * Fix naming * Modify config * Add snapshot list * Modify a spelling typo * Update based on PR comments. * Rename scenario to vm scheduling * Rename scenario * Update print messages * Lint fix * Lint fix * Rename scenario * Modify the calculation of cpu utilization * Add comment * Modify data pipeline path * Fix typo * Modify naming * Add unittest * Add comment * Unify naming * Fix data path typo * Update comments * Update snapshot features * Add take snapshot * Add summary keys * Update cpu reader * Update naming * Add unit test * Rename snapshot node * Add processed data pipeline * Modify config * Add comment * Lint style fix Co-authored-by: Jinyu Wang * Add package used in vm_scheduling * add aria2p to test requirement * best fit example: update the usage of snapshot * Add aria2p to test requriement * Remove finish event * Fix unittest * Add test dataset * Update based on PR comment * Refine cpu reader and unittest * Lint update * Refine based on PR comment * Add agent index * Add node maping * Init vm shceduling notebook * Add notebook * Refine based on PR comments * Renaming postpone_step * Renaming and refine based on PR comments * Rename config * Update based on the v0.2_datacenter * Update notebook * Update * update filepath * notebook updated Co-authored-by: Jinyu Wang Co-authored-by: Chaos Yu Co-authored-by: ysqyang Co-authored-by: ysqyang Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> * Update process mode docs and fixed on premises (#226) * V0.2 Add github workflow integration (#222) * test: add github workflow integration * fix: split procedures && bug fixed * test: add training only restriction * fix: add 'approved' restriction * fix: change default ssh port to 22 * style: in one line * feat: add timeout for Subprocess.run * test: change default node_size to Standard_D2s_v3 * style: refine style * fix: add ssh_port param to on-premises mode * fix: add missing init.py * V0.2 explorer (#198) * overhauled exploration abstraction * fixed a bug * fixed a bug * fixed a bug * added exploration related methods to abs_agent * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * separated learning with exploration schedule and without * small fixes * moved explorer logic to actor side * fixed a bug * fixed a bug * fixed a bug * fixed a bug * removed unwanted param from simple agent manager * added noise explorer * fixed formatting * removed unnecessary comma * fixed PR comments * removed unwanted exception and imports * fixed a bug * fixed PR comments * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed lint issue * fixed a bug * fixed lint issue * fixed naming * combined exploration param generation and early stopping in scheduler * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed lint issues * fixed lint issue * moved logger inside scheduler * fixed a bug * fixed a bug * fixed a bug * fixed lint issues * removed epsilon parameter from choose_action * fixed some PR comments * fixed some PR comments * bug fix * bug fix * bug fix * removed explorer abstraction from agent * refined dqn example * fixed lint issues * simplified scheduler * removed early stopping from CIM dqn example * removed early stopping from cim example config * renamed early_stopping_callback to early_stopping_checker * removed action_dim from noise explorer classes and added some shape checks * modified NoiseExplorer's __call__ logic to batch processing * made NoiseExplorer's __call__ return type np array * renamed update to set_parameters in explorer * fixed old naming in test_grass Co-authored-by: ysqyang * V0.2 embedded optim (#191) * added dueling action value model * renamed params in dueling_action_value_model * renamed shared_features to features * replaced IdentityLayers with nn.Identity * 1. added skip connection option in FC_net; 2. generalized learning model * added skip_connection option in config * removed type casting in fc_net * fixed lint formatting issues * refined docstring * mv dueling_actiovalue_model and fixed some bugs * added multi-head functionality to LearningModel * refined learning model docstring * added head_key param in learningModel forward * added double DQN and dueling features to DQN * fixed a bug * added DuelingQModelHead enum * fixed a bug * removed unwanted file * fixed PR comments * added top layer logic and is_top option in fc_net * fixed a bug * fixed a bug * reverted some changes in learning model * reverted some changes in learning model * added members to learning model to fix the mode issue * fixed a bug * fixed mode setting issue in learning model * fixed PR comments * revised cim example according to DQN changes * renamed eval_model to q_value_model in cim example * more fixes * fixed a bug * fixed a bug * added doc per PR comments * removed learner.exit() in single_process_launcher * removed learner.exit() in single_process_launcher * fixed PR comments * fixed rl/__init__ * fixed issues in example * fixed a bug * fixed a bug * fixed lint formatting issues * double DQN feature * fixed a bug * fixed a bug * fixed PR comments * fixed lint issue * embedded optimizer into SingleHeadLearningModel * 1. fixed PR comments related to load/dump; 2. removed abstract load/dump methods from AbsAlgorithm * added load_models in simple_learner * minor docstring edits * minor docstring edits * minor docstring edits * mv optimizer options inside LearningMode * modified example accordingly * fixed a bug * fixed a bug * fixed a bug * added dueling DQN feature * revised and refined docstrings * fixed a bug * fixed lint issues * added load/dump functions to LearningModel * fixed a bug * fixed a bug * fixed lint issues * refined DQN docstrings * removed load/dump functions from DQN * added task validator * fixed decorator use * fixed a typo * fixed a bug * fixed lint issues * changed LearningModel's step() to take a single loss * revised learning model design * revised example * fixed a bug * fixed a bug * fixed a bug * fixed a bug * added decorator utils to algorithm * fixed a bug * renamed core_model to model * fixed a bug * 1. fixed lint formatting issues; 2. refined learning model docstrings * rm trailing whitespaces * added decorator for choose_action * fixed a bug * fixed a bug * fixed version-related issues * renamed add_zeroth_dim decorator to expand_dim * overhauled exploration abstraction * fixed a bug * fixed a bug * fixed a bug * added exploration related methods to abs_agent * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * separated learning with exploration schedule and without * small fixes * moved explorer logic to actor side * fixed a bug * fixed a bug * fixed a bug * fixed a bug * removed unwanted param from simple agent manager * small fixes * added shared_module property to LearningModel * added shared_module property to LearningModel * revised __getstate__ for LearningModel * fixed a bug * added soft_update function to learningModel * fixed a bug * revised learningModel * rm __getstate__ and __setstate__ from LearningModel * added noise explorer * fixed formatting * removed unnecessary comma * removed unnecessary comma * fixed PR comments * removed unwanted exception and imports * removed unwanted exception and imports * fixed a bug * fixed PR comments * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed lint issue * fixed a bug * fixed lint issue * fixed naming * combined exploration param generation and early stopping in scheduler * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed lint issues * fixed lint issue * moved logger inside scheduler * fixed a bug * fixed a bug * fixed a bug * fixed lint issues * fixed lint issue * removed epsilon parameter from choose_action * removed epsilon parameter from choose_action * changed agent manager's train parameter to experience_by_agent * fixed some PR comments * renamed zero_grad to zero_gradients in LearningModule * fixed some PR comments * bug fix * bug fix * bug fix * removed explorer abstraction from agent * added DEVICE env variable as first choice for torch device * refined dqn example * fixed lint issues * removed unwanted import in cim example * updated cim-dqn notebook * simplified scheduler * edited notebook according to merged scheduler changes * refined dimension check for learning module manager and removed num_actions from DQNConfig * bug fix for cim example * added notebook output * removed early stopping from CIM dqn example * removed early stopping from cim example config * moved decorator logic inside algorithms * renamed early_stopping_callback to early_stopping_checker * removed action_dim from noise explorer classes and added some shape checks * modified NoiseExplorer's __call__ logic to batch processing * made NoiseExplorer's __call__ return type np array * renamed update to set_parameters in explorer * fixed old naming in test_grass Co-authored-by: ysqyang * V0.2 VM scheduling docs (#228) * Initialize * Data center scenario init * Code style modification * V0.2 event buffer subevents expand (#180) * V0.2 rl toolkit refinement (#165) * refined rl abstractions * fixed formattin issues * checked out error-code related code from v0.2_pg * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * renamed save_models to dump_models * 1. set default batch_norm_enabled to True; 2. used state_dict in dqn model saving * renamed dump_experience_store to dump_experience_pool * fixed a bug in the dump_experience_pool method * fixed some PR comments * fixed more PR comments * 1.fixed some PR comments; 2.added early_stopping_checker; 3.revised explorer class * fixed cim example according to rl toolkit changes * fixed some more PR comments * rewrote multi_process_launcher to eliminate the distributed section in config * 1. fixed a typo; 2. added logging before early stopping * fixed a bug * fixed a bug * fixed a bug * added early stopping feature to CIM exmaple * fixed a typo * fixed some issues with early stopping * changed early stopping metric func * fixed a bug * fixed a bug * added early stopping to dist mode cim * added experience collecting func * edited notebook according to changes in CIM example * fixed bugs in nb * fixed lint formatting issues * fixed a typo * fixed some PR comments * fixed more PR comments * revised docs * removed nb output * fixed a bug in simple_learner * fixed a typo in nb * fixed a bug * fixed a bug * fixed a bug * removed unused import * fixed a bug * 1. changed early stopping default config; 2. renamed param in early stopping checker and added typing * fixed some doc issues * added output to nb Co-authored-by: ysqyang * unfold sub-events, insert after parent * remove event category, use different class instead, add helper functions to gen decision and action event * add a method to support add immediate event to cascade event with tick validation * fix ut issue * add action as 1st sub event to ensure the executing order Co-authored-by: ysqyang Co-authored-by: ysqyang * Data center scenario update * Code style update * Data scenario business engine update * Isort update * Fix lint code check * Fix based on PR comments. * Update based on PR comments. * Add decision payload * Add config file * Update utilization series logic * Update based on PR comment * Update based on PR * Update * Update * Add the ValidPm class * Update docs string and naming * Add energy consumption * Lint code fixed * Refining postpone function * Lint style update * Init data pipeline * Update based on PR comment * Add data pipeline download * Lint style update * Code style fix * Temp update * Data pipeline update * Add aria2p download function * Update based on PR comment * Update based on PR comment * Update based on PR comment * Update naming of variables * Rename topology * Renaming * Fix valid pm list * Pylint fix * Update comment * Update docstring and comment * Fix init import * Update tick issue * fix merge problem * update style * V0.2 datacenter data pipeline (#199) * Data pipeline update * Data pipeline update * Lint update * Update pipeline * Add vmid mapping * Update lint style * Add VM data analytics * Update notebook * Add binary converter * Modift vmtable yaml * Update binary meta file * Add cpu reader * random example added for data center * Fix bugs * Fix pylint * Add launcher * Fix pylint * best fit policy added * Add reset * Add config * Add config * Modify action object * Modify config * Fix naming * Modify config * Add snapshot list * Modify a spelling typo * Update based on PR comments. * Rename scenario to vm scheduling * Rename scenario * Update print messages * Lint fix * Lint fix * Rename scenario * Modify the calculation of cpu utilization * Add comment * Modify data pipeline path * Fix typo * Modify naming * Add unittest * Add comment * Unify naming * Fix data path typo * Update comments * Update snapshot features * Add take snapshot * Add summary keys * Update cpu reader * Update naming * Add unit test * Rename snapshot node * Add processed data pipeline * Modify config * Add comment * Lint style fix Co-authored-by: Jinyu Wang * Add package used in vm_scheduling * add aria2p to test requirement * best fit example: update the usage of snapshot * Add aria2p to test requriement * Remove finish event * Fix unittest * Add test dataset * Update based on PR comment * vm doc init * Update docs * Update docs * Update docs * Update docs * Remove old notebook * Update docs * Update docs * Add figure * Update docs Co-authored-by: Jinyu Wang Co-authored-by: Chaos Yu Co-authored-by: ysqyang Co-authored-by: ysqyang Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> * v0.2 VM Scheduling docs refinement (#231) * Fix typo * Refining vm scheduling docs * V0.2 store refinement (#234) * updated docs and images for rl toolkit * 1. fixed import formats for maro/rl; 2. changed decorators to hypers in store * fixed lint issues Co-authored-by: ysqyang * Fix bug (#237) vm scenario: fix the event type bug of the postpone event * V0.2 rl toolkit doc (#235) * updated docs and images for rl toolkit * updated cim example doc * updated cim exmaple docs * updated cim example rst * updated rl_toolkit and cim example docs * replaced q_module with q_net in example rst * refined doc * refined doc * updated figures * updated figures Co-authored-by: ysqyang * Merge V0.2 vis into V0.2 (#233) * Implemented dump snapshots and convert to CSV. * Let BE supports params when dump snapshot. * Refactor dump code to core.py * Implemented decision event dump. * replace is not '' with !='' * Fixed issues that code review mentioned. * removed path from hello.py * Changed import sort. * Fix import sorting in citi_bike/business_engine * visualization 0.1 * Updated lint configurations. * Fixed formatting error that caused lint errors. * render html title function * Try to fix lint errors. * flake-8 style fix * remove space around 18,35 * dump_csv_converter.py re-formatting. * files re-formatting. * style fixed * tab delete * white space fix * white space fix-2 * vis redundant function delete * refine * re-formatting after merged upstream. * Updated import section. * Updated import section. * pr refine * isort fix * white space * lint error * \n error * test continuation * indent * continuation of indent * indent 0.3 * comment update * comment update 0.2 * f-string update * f-string 0.2 * lint 0.3 * lint 0.4 * lint 0.4 * lint 0.5 * lint 0.6 * docstring update * data version deploy update * condition update * add whitespace * V0.2 vis dump feature enhancement. (#190) * Dumps added manifest file. * Code updated format by flake8 * Changed manifest file format for easy reading. * deploy info update; docs update * weird white space * Update dashboard_visualization.md * new endline? * delete dependency * delete irrelevant file * change scenario to enum, divide file path into a separated class * doc refine * doc update * params type * data structure update * doc&enum, formula refine * refine * add ut, refine doc * style refine * isort * strong type fix * os._exit delete * revert datalib * import new line * change test case * change file name & doc * change deploy path * delete params * revert file * delete duplicate file * delete single process * update naming * manually change import order * delete blank * edit error * requirement txt * style fix & refine * comments&docstring refine * add parameter name * test & dump * comments update * Added manifest file. (#201) Only a few changes that need to meet requirements of manifest file format. * comments fix * delete toolkit change * doc update * citi bike update * deploy path * datalib update * revert datalib * revert * maro file format * comments update * doc update * update param name * doc update * new link * image update * V0.2 visualization-0.1 (#181) * visualization 0.1 * render html title function * flake-8 style fix * style fixed * tab delete * white space fix * white space fix-2 * vis redundant function delete * refine * pr refine * isort fix * white space * lint error * \n error * test continuation * indent * continuation of indent * indent 0.3 * comment update * comment update 0.2 * f-string update * f-string 0.2 * lint 0.3 * lint 0.4 * lint 0.4 * lint 0.5 * lint 0.6 * docstring update * data version deploy update * condition update * add whitespace * deploy info update; docs update * weird white space * Update dashboard_visualization.md * new endline? * delete dependency * delete irrelevant file * change scenario to enum, divide file path into a separated class * fix the visualization of docs/key_components/distributed_toolkit * doc refine * doc update * params type * add examples into isort ignore * data structure update * doc&enum, formula refine * refine * add ut, refine doc * style refine * isort * strong type fix * os._exit delete * revert datalib * import new line * change test case * change file name & doc * change deploy path * delete params * revert file * delete duplicate file * delete single process * update naming * manually change import order * delete blank * edit error * requirement txt * style fix & refine * comments&docstring refine * add parameter name * test & dump * comments update * comments fix * delete toolkit change * doc update * citi bike update * deploy path * datalib update * revert datalib * revert * maro file format * comments update * doc update * update param name * doc update * new link * image update Co-authored-by: Jinyu Wang Co-authored-by: Miaoran Chen (Wicresoft) * image change * add reset snapshot * delete dump * add new line * add next steps * import change * relative import * add init file * import change * change utils file * change cliexpcetion to clierror * dashboard test * change result * change assertation * move not * unit test change * core change * unit test delete name_mapping_file * update cim business engine * doc update * change relative path * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * duc update * duc update * duc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * change import sequence * comments update * doc add pic * add dependency * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * Update dashboard_visualization.rst * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * doc update * delete white space * doc update * doc update * update doc * update doc * update doc Co-authored-by: Michael Li Co-authored-by: Miaoran Chen (Wicresoft) Co-authored-by: Jinyu Wang Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> * V0.2 docs process mode (#230) * Update process mode docs and fixed on premises * Update orchestration docs * Update process mode docs add JOB_NAME as env variable * fixed bugs * fixed isort issue * update docs index Co-authored-by: kaiqli * V0.2 learning model refinement (#236) * moved optimizer options to LearningModel * typo fix * fixed lint issues * updated notebook * misc edits * 1. renamed CIMAgent to DQNAgent; 2. moved create_dqn_agents to Agent section in notebook * renamed single_host_cim_learner ot cim_learner in notebook * updated notebook output * typo fix * removed dimension check in absence of shared stack * fixed a typo * fixed lint issues Co-authored-by: ysqyang * Update vm docs (#241) Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> * V0.2 info update (#240) * update readme * update version * refine reademe format * add vis gif * add citation * update citation * update badge Co-authored-by: Arthur Jiang * Fix typo (#242) * Fix typo * fix typo * fix * syntax fix (#253) * syntax fix * syntax fix * syntax fix * rm unwanted import Co-authored-by: ysqyang * V0.2 vm oversubscription (#246) * Remove topology * Update pipeline * Update pipeline * Update pipeline * Modify metafile * Add two attributes of VM * Update pipeline * Add vm category * Add todo * Add oversub config * Add oversubscription feature * Lint fix * Update based on PR comment. * Update pipeline * Update pipeline * Update config. * Update based on PR comment * Update * Add pm sku feature * Add sku setting * Add sku feature * Lint fix * Lint style * Update sku, overloading * Lint fix * Lint style * Fix bug * Modify config * Remove sky and replaced it by pm stype * Add and refactor vm category * Comment out cofig * Unify the enum format * Fix lint style * Fix import order * Update based on PR comment Co-authored-by: Jinyu Wang * V0.2 vm scheduling decision event (#257) * Fix data preparation bug * Add frame index * V0.2 PG, K-step and lambda return utils (#155) * fixed a bug * fixed lint issues * added load/dump functions to LearningModel * fixed a bug * fixed a bug * fixed lint issues * merged with v0.2_embedded_optims * refined DQN docstrings * removed load/dump functions from DQN * added task validator * fixed decorator use * fixed a typo * fixed a bug * revised * fixed lint issues * changed LearningModel's step() to take a single loss * revised learning model design * revised example * fixed a bug * fixed a bug * fixed a bug * fixed a bug * added decorator utils to algorithm * fixed a bug * renamed core_model to model * fixed a bug * 1. fixed lint formatting issues; 2. refined learning model docstrings * rm trailing whitespaces * added decorator for choose_action * fixed a bug * fixed a bug * fixed version-related issues * renamed add_zeroth_dim decorator to expand_dim * overhauled exploration abstraction * fixed a bug * fixed a bug * fixed a bug * added exploration related methods to abs_agent * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * separated learning with exploration schedule and without * small fixes * moved explorer logic to actor side * fixed a bug * fixed a bug * fixed a bug * fixed a bug * removed unwanted param from simple agent manager * small fixes * revised code based on revised abstractions * fixed some bugs * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * added shared_module property to LearningModel * added shared_module property to LearningModel * fixed a bug with k-step return in AC * fixed a bug * fixed a bug * merged pg, ac and ppo examples * fixed a bug * fixed a bug * fixed naming for ppo * renamed some variables in PPO * added ActionWithLogProbability return type for PO-type algorithms * fixed a bug * fixed a bug * fixed lint issues * revised __getstate__ for LearningModel * fixed a bug * added soft_update function to learningModel * fixed a bug * revised learningModel * rm __getstate__ and __setstate__ from LearningModel * added noise explorer * formatting * fixed formatting * removed unnecessary comma * removed unnecessary comma * removed unnecessary comma * fixed PR comments * removed unwanted exception and imports * removed unwanted exception and imports * fixed a bug * fixed PR comments * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed lint issue * fixed a bug * fixed lint issue * fixed naming * combined exploration param generation and early stopping in scheduler * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed lint issues * fixed lint issue * moved logger inside scheduler * fixed a bug * fixed a bug * fixed a bug * fixed lint issues * fixed lint issue * removed epsilon parameter from choose_action * removed epsilon parameter from choose_action * changed agent manager's train parameter to experience_by_agent * fixed some PR comments * renamed zero_grad to zero_gradients in LearningModule * fixed some PR comments * bug fix * bug fix * bug fix * removed explorer abstraction from agent * added DEVICE env variable as first choice for torch device * refined dqn example * fixed lint issues * removed unwanted import in cim example * updated cim-dqn notebook * simplified scheduler * edited notebook according to merged scheduler changes * refined dimension check for learning module manager and removed num_actions from DQNConfig * bug fix for cim example * added notebook output * updated cim PO example code according to changes in maro/rl * removed early stopping from CIM dqn example * combined ac and ppo and simplified example code and config * removed early stopping from cim example config * moved decorator logic inside algorithms * renamed early_stopping_callback to early_stopping_checker * put PG and AC under PolicyOptimization class and refined examples accordingly * fixed lint issues * removed action_dim from noise explorer classes and added some shape checks * modified NoiseExplorer's __call__ logic to batch processing * made NoiseExplorer's __call__ return type np array * renamed update to set_parameters in explorer * fixed old naming in test_grass * moved optimizer options to LearningModel * typo fix * fixed lint issues * updated notebook * updated cim example for policy optimization * typo fix * typo fix * typo fix * typo fix * misc edits * minor edits to rl_toolkit.rst * checked out docs from master * fixed typo in k-step shaper * fixed lint issues * bug fix in store * lint issue fix * changed default max_ep to 100 for policy_optimization algos * vis doc update to master (#244) * refine readme * feat: refine data push/pull (#138) * feat: refine data push/pull * test: add cli provision testing * fix: style fix * fix: add necessary comments * fix: from code review * add fall back function in weather download (#112) * fix deployment issue in multi envs * fix typo * fix ~/.maro not exist issue in build * skip deploy when build * update for comments * temporarily disable weather info * replace ecr with cim in setup.py * replace ecr in manifest * remove weather check when read data * fix station id issue * fix format * add TODO in comments * add noaa weather source * fix weather reset and weather comment * add comment for weather data url * some format update * add fall back function in weather download * update comment * update for comments * update comment * add period * fix for pylint * update for pylint check * added example docs (#136) * added example docs * added citibike greedy example doc * modified citibike doc * fixed PR comments * fixed more PR comments * fixed small formatting issue Co-authored-by: ysqyang * switch the key and value of handler_dict in decorator (#144) * switch the key and value of handler_dict in decorator * add dist decorator UT and fixed multithreading conflict in maro test suite * pr comments update. * resolved comments about decorator UT * rename handler_fun in dist decorator * change self.attr into class_name.attr * update UT tests comments * V0.1 annotation (#147) * refine the annotation of simulator core * remove reward from env(be) * format refined * white spaces test * left-padding spaces refined * format modifed * update the left-padding spaces of docstrings * code format updated * update according to comments * update according to PR comments Co-authored-by: Jinyu Wang * Event payload details for env.summary (#156) * key_list of events added for env.summary * code refined according to lint * 2 kinds of Payload added for CIM scenario; citi bike summary refined according to comments * code format refined * try trigger the git tests * update github workflow Co-authored-by: Jinyu Wang * Implemented dump snapshots and convert to CSV. * Let BE supports params when dump snapshot. * Refactor dump code to core.py * Implemented decision event dump. * V0.2 online lp for citi bike (#159) * key_list of events added for env.summary * code refined according to lint * 2 kinds of Payload added for CIM scenario; citi bike summary refined according to comments * code format refined * try trigger the git tests * update github workflow * online LP example added for citi bike * infeasible solution * infeasible solution fixed: call snapshot before any env.step() * experiment results of toy topos added * experiment results of toy topos added * experiment result update: better than naive baseline * PuLP version added * greedy experiment results update * citibike result update * modified according to PR comments * update experiment results and forecasting comparison * citi bike lp README updated * README updated * modified according to PR comments * update according to PR comments Co-authored-by: Jinyu Wang Co-authored-by: Jinyu Wang * V0.2 rl toolkit refinement (#165) * refined rl abstractions * fixed formattin issues * checked out error-code related code from v0.2_pg * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * fixed a bug * renamed save_models to dump_models * 1. set default batch_norm_enabled to True; 2. used state_dict in dqn model saving * renamed dump_experience_store to dump_experience_pool * fixed a bug in the dump_experience_pool method * fixed some PR comments * fixed more PR comments * 1.fixed some PR comments; 2.added early_stopping_checker; 3.revised explorer class * fixed cim example according to rl toolkit changes * fixed some more PR comments * rewrote multi_process_launcher to eliminate the distributed section in config * 1. fixed a typo; 2. added logging before early stopping * fixed a bug * fixed a bug * fixed a bug * added early stopping feature to CIM exmaple * fixed a typo * fixed some issues with early stopping * changed early stopping metric func * fixed a bug * fixed a bug * added early stopping to dist mode cim * added experience collecting func * edited notebook according to changes in CIM example * fixed bugs in nb * fixed lint formatting issues * fixed a typo * fixed some PR comments * fixed more PR comments * revised docs * removed nb output * fixed a bug in simple_learner * fixed a typo in nb * fixed a bug * fixed a bug * fixed a bug * removed unused import * fixed a bug * 1. changed early stopping default config; 2. renamed param in early stopping checker and added typing * fixed some doc issues * added output to nb Co-authored-by: ysqyang * replace is not '' with !='' * Fixed issues that code review mentioned. * removed path from hello.py * Changed import sort. * Fix import sorting in citi_bike/business_engine * visualization 0.1 * Updated lint configurations. * Fixed formatting error that caused lint errors. * render html title function * Try to fix lint errors. * flake-8 style fix * remove space around 18,35 * dump_csv_converter.py re-formatting. * files re-formatting. * style fixed * tab delete * white space fix * white space fix-2 * vis redundant function delete * refine * update according to flake8 * re-formatting after merged upstream. * Updated import section. * Updated import section. * V0.2 Logical operator overloading for EarlyStoppingChecker (#178) * 1. added logical operator overloading for early stopping checker; 2. added mean value checker * fixed PR comments * removed learner.exit() in single_process_launcher * added another early stopping checker in example * fixed PR comments and lint issues * lint issue fix * fixed lint issues * fixed a bug * fixed a bug Co-authored-by: ysqyang * V0.2 skip connection (#176) * replaced IdentityLayers with nn.Identity * 1. added skip connection option in FC_net; 2. generalized learning model * added skip_connection option in config * removed type casting in fc_net * fixed lint formatting issues * refined docstring * added multi-head functionality to LearningModel * refined learning model docstring * added head_key param in learningModel forward * fixed PR comments * added top layer logic and is_top option in fc_net * fixed a bug * fixed a bug * reverted some changes in learning model * reverted some changes in learning model * added members to learning model to fix the mode issue * fixed a bug * fixed mode setting issue in learning model * removed learner.exit() in single_process_launcher * fixed PR comments * fixed rl/__init__ * fixed issues in example * fixed a bug * fixed a bug * fixed lint formatting issues * moved reward type casting to exp shaper Co-authored-by: ysqyang * pr refine * isort fix * white space * lint error * \n error * test continuation * indent * continuation of indent * indent 0.3 * comment update * comment update 0.2 * f-string update * f-string 0.2 * lint 0.3 * lint 0.4 * lint 0.4 * lint 0.5 * lint 0.6 * docstring update * data version deploy update * condition update * add whitespace * V0.2 vis dump feature enhancement. (#190) * Dumps added manifest file. * Code updated format by flake8 * Changed manifest file format for easy reading. … * update package version * vis doc image update * add allocation counter to logger * add apply buffer size logic * update solver choices * Update README for offline LP example Co-authored-by: Jinyu Wang Co-authored-by: Arthur Jiang Co-authored-by: Arthur Jiang Co-authored-by: Romic Huang Co-authored-by: zhanyu wang Co-authored-by: ysqyang Co-authored-by: ysqyang Co-authored-by: kaiqli <59279714+kaiqli@users.noreply.github.com> Co-authored-by: Chaos Yu Co-authored-by: Wenlei Shi Co-authored-by: Michael Li Co-authored-by: kyu-kuanwei <72911362+kyu-kuanwei@users.noreply.github.com> Co-authored-by: Meroy Chen <39452768+Meroy9819@users.noreply.github.com> Co-authored-by: Miaoran Chen (Wicresoft) Co-authored-by: kaiqli Co-authored-by: Kuan Wei Yu Co-authored-by: MicrosoftHam <77261932+MicrosoftHam@users.noreply.github.com> Co-authored-by: aiming hao <37905948+hamcoder@users.noreply.github.com> Co-authored-by: zhanyu --- examples/vm_scheduling/offline_lp/README.md | 25 +++ examples/vm_scheduling/offline_lp/common.py | 21 ++ examples/vm_scheduling/offline_lp/config.yml | 24 +++ .../vm_scheduling/offline_lp/ilp_agent.py | 122 ++++++++++++ examples/vm_scheduling/offline_lp/launcher.py | 93 +++++++++ .../offline_lp/vm_scheduling_ilp.py | 179 ++++++++++++++++++ 6 files changed, 464 insertions(+) create mode 100644 examples/vm_scheduling/offline_lp/README.md create mode 100644 examples/vm_scheduling/offline_lp/common.py create mode 100644 examples/vm_scheduling/offline_lp/config.yml create mode 100644 examples/vm_scheduling/offline_lp/ilp_agent.py create mode 100644 examples/vm_scheduling/offline_lp/launcher.py create mode 100644 examples/vm_scheduling/offline_lp/vm_scheduling_ilp.py diff --git a/examples/vm_scheduling/offline_lp/README.md b/examples/vm_scheduling/offline_lp/README.md new file mode 100644 index 000000000..93a934a4d --- /dev/null +++ b/examples/vm_scheduling/offline_lp/README.md @@ -0,0 +1,25 @@ +# Offline Integer Linear Programming (ILP) For VM Scheduling + +## Data used in the formulation +The offline lp example directly uses the future VM information to formulate the future machine resource changes. +The data the formulation is based is extracted from the business engine directly (the data accessing way is not workable for online methods, just to act as a baseline method), including: +- the capacity (#core, #memory) of all the PMs +- the arrival time of the VM request +- the resource requirement (#core, #memory) of the VM request +- the lifetime of the VM +- all the existing VM-PM mapping + +## ILP configuration +Based on the data and the configuration for the problem formulation, the ILP solver will calculate to get an allocation solution. The configuration includes: +- **solver**: The solver used, CBC is the default solver for PuLP. While GLPK is another free solver that it can get more stable solution but need additional installation of glpk-utils. +- **plan_window_size**: How many ticks the formulation will take into account. +- **apply_buffer_size**: For each solution, the allocation of how many ticks will be applied. E.g. Assume the plan_window_size is 10 and the apply_buffer_size is 2. And assume that at tick 2, we trigger a formulation. Then formulation will simulate the status changes from tick 2 to tick 11 (plan window size is 10), and get an allocation plan for each VM requests in this time window. But only the allocation of VM requests during tick 2 to tick 3 (apply buffer size is 2) will be applied according to the solution. The first VM request comes at tick 4 will trigger a new formulation and get a new solution... +- **core_safety_remaining_ratio**: Used in online case, used to maintain a safety inventory. It is enough to set to 0 in offline case. +- **mem_safety_remaining_ratio**: Used in online case, used to maintain a safety inventory. It is enough to set to 0 in offline case. +- **successful_allocation_decay**: The decay factor of each successful allocation in the objective of the formulation. To be specific, a successful allocation for the first request in the formulation will counts 1, the one for the second request in the formulation will counts 1 * decay, the one for the third request in the formulation will counts 1 * decay * decay, ... +- **allocation_multiple_core_num**: If set up, the base of a successful allocation will be set up as #core the request required instead of 1. +- **dump_all_solution**: Whether to dump the lp formulation and solution to file or not. +- **dump_infeasible_solution**: If there exist infeasible solutions, whether to dump them to file or not. +- **stdout_solver_message**: Whether to enable the solver message output or not. Suggest to disable it if the solution spends not too much time. + +*Notes: For large problem scale, the free solver CBC and GLPK may take a long time to get the solution. Sometimes they even cannot handle large problem scale. We suggest you to use them only for small problem scale or buy some powerful solver if needed.* \ No newline at end of file diff --git a/examples/vm_scheduling/offline_lp/common.py b/examples/vm_scheduling/offline_lp/common.py new file mode 100644 index 000000000..b652279e6 --- /dev/null +++ b/examples/vm_scheduling/offline_lp/common.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from dataclasses import dataclass + +@dataclass +class IlpPmCapacity: + core: int + mem: int + +@dataclass +class IlpVmInfo: + id: int=-1 + pm_idx: int=-2 + core: int=-1 + mem: int=-1 + lifetime: int=-1 + arrival_env_tick: int=-1 + + def remaining_lifetime(self, env_tick: int): + return self.lifetime - (env_tick - self.arrival_env_tick) diff --git a/examples/vm_scheduling/offline_lp/config.yml b/examples/vm_scheduling/offline_lp/config.yml new file mode 100644 index 000000000..7703d85dc --- /dev/null +++ b/examples/vm_scheduling/offline_lp/config.yml @@ -0,0 +1,24 @@ +experiment_name: test + +env: + scenario: vm_scheduling + topology: azure.2019.10k + start_tick: 1 + durations: 200 + resolution: 1 + seed: 128 + +ilp: + solver: CBC # GLPK, CBC + plan_window_size: 200 # unit: tick + apply_buffer_size: 200 # unit: tick + performance: + core_safety_remaining_ratio: 0 + mem_safety_remaining_ratio: 0 + objective: + successful_allocation_decay: 1 + allocation_multiple_core_num: false + log: + dump_all_solution: false + dump_infeasible_solution: true + stdout_solver_message: false diff --git a/examples/vm_scheduling/offline_lp/ilp_agent.py b/examples/vm_scheduling/offline_lp/ilp_agent.py new file mode 100644 index 000000000..70f348aac --- /dev/null +++ b/examples/vm_scheduling/offline_lp/ilp_agent.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import numpy as np +import timeit +from collections import defaultdict, Counter +from typing import List, Set + +from maro.data_lib import BinaryReader +from maro.simulator.scenarios.vm_scheduling import PostponeAction, AllocateAction +from maro.simulator.scenarios.vm_scheduling.common import Action +from maro.utils import DottableDict, Logger + +from common import IlpPmCapacity, IlpVmInfo +from vm_scheduling_ilp import NOT_ALLOCATE_NOW, VmSchedulingILP + +class IlpAgent(): + def __init__( + self, + ilp_config: DottableDict, + pm_capacity: np.ndarray, + vm_table_path: str, + env_start_tick: int, + env_duration: int, + simulation_logger: Logger, + ilp_logger: Logger, + log_path: str + ): + self._simulation_logger = simulation_logger + self._ilp_logger = ilp_logger + + self._allocation_counter = Counter() + + pm_capacity: List[IlpPmCapacity] = [IlpPmCapacity(core=pm[0], mem=pm[1]) for pm in pm_capacity] + self.ilp = VmSchedulingILP(config=ilp_config, pm_capacity=pm_capacity, logger=ilp_logger, log_path=log_path) + self.ilp_plan_window_size = ilp_config.plan_window_size + self.ilp_apply_buffer_size = ilp_config.apply_buffer_size + + # Use the vm_item_picker to get the precise vm request info. + self.vm_reader = BinaryReader(vm_table_path) + self.vm_item_picker = self.vm_reader.items_tick_picker( + env_start_tick, + env_start_tick + env_duration, + time_unit="s" + ) + + # Used to keep the info already read from the vm_item_picker. + self.vm_req_dict = defaultdict(list) + self.env_tick_in_vm_req_dict = [] + self.allocated_vm_dict = {} + self.refreshed_allocated_vm_dict = {} + + self.last_solution_env_tick = -1 + self._vm_id_to_idx = {} + self.future_vm_req: List[IlpVmInfo] = [] + self.allocated_vm: List[IlpVmInfo] = [] + + def choose_action(self, env_tick: int, cur_vm_id: int, live_vm_set_list: List[Set[int]]) -> Action: + # Formulate and solve only when the new request goes beyond the apply buffer size of last ILP solution. + if self.last_solution_env_tick < 0 or env_tick >= self.last_solution_env_tick + self.ilp_apply_buffer_size: + self.last_solution_env_tick = env_tick + self._vm_id_to_idx = {} + self.future_vm_req.clear() + self.allocated_vm.clear() + + # To clear the outdated vm_req_dict data. + pop_num = 0 + for i, tick in enumerate(self.env_tick_in_vm_req_dict): + if tick < env_tick: + self.vm_req_dict.pop(tick) + pop_num += 1 + else: + break + self.env_tick_in_vm_req_dict = self.env_tick_in_vm_req_dict[pop_num:] + + # Read VM data from file. + for tick in range(env_tick, env_tick + self.ilp_plan_window_size + 1): + if tick not in self.vm_req_dict: + self.env_tick_in_vm_req_dict.append(tick) + self.vm_req_dict[tick] = [item for item in self.vm_item_picker.items(tick)] + + # Build the future_vm_req list for ILP. + for tick in range(env_tick, env_tick + self.ilp_plan_window_size + 1): + for vm in self.vm_req_dict[tick]: + vmInfo = IlpVmInfo( + id=vm.vm_id, + core=vm.vm_cpu_cores, + mem=vm.vm_memory, + lifetime=vm.vm_lifetime, + arrival_env_tick=tick + ) + if tick < env_tick + self.ilp_apply_buffer_size: + self.refreshed_allocated_vm_dict[vm.vm_id] = vmInfo + self._vm_id_to_idx[vm.vm_id] = len(self.future_vm_req) + self.future_vm_req.append(vmInfo) + + # Build the allocated_vm list for ILP. + for pm_idx in range(len(live_vm_set_list)): + for vm_id in live_vm_set_list[pm_idx]: + assert vm_id in self.allocated_vm_dict, f"ILP agent: vm_id {vm_id} not in allocated_vm_dict" + vm = self.allocated_vm_dict[vm_id] + vm.pm_idx = pm_idx + self.refreshed_allocated_vm_dict[vm_id] = vm + self.allocated_vm.append(vm) + + self.allocated_vm_dict.clear() + self.allocated_vm_dict = self.refreshed_allocated_vm_dict + self.refreshed_allocated_vm_dict = {} + + # Choose action by ILP, may trigger a new formulation and solution, + # may directly return the decision if the cur_vm_id is still in the apply buffer size of last solution. + chosen_pm_idx = self.ilp.choose_pm(env_tick, cur_vm_id, self.allocated_vm, self.future_vm_req, self._vm_id_to_idx) + self._simulation_logger.info(f"tick: {env_tick}, vm: {cur_vm_id} -> pm: {chosen_pm_idx}") + + if chosen_pm_idx == NOT_ALLOCATE_NOW: + return PostponeAction(vm_id=cur_vm_id, postpone_step=1) + else: + self._allocation_counter[self.future_vm_req[self._vm_id_to_idx[cur_vm_id]].core] += 1 + return AllocateAction(vm_id=cur_vm_id, pm_id=chosen_pm_idx) + + def report_allocation_summary(self): + self._simulation_logger.info(f"Allocation Counter(#core, #req): {sorted(self._allocation_counter.items())}") diff --git a/examples/vm_scheduling/offline_lp/launcher.py b/examples/vm_scheduling/offline_lp/launcher.py new file mode 100644 index 000000000..5fd264c73 --- /dev/null +++ b/examples/vm_scheduling/offline_lp/launcher.py @@ -0,0 +1,93 @@ +import io +import os +import shutil +import timeit +from typing import List, Set + +import pprint +import yaml + +from maro.simulator import Env +from maro.simulator.scenarios.vm_scheduling import DecisionPayload +from maro.simulator.scenarios.vm_scheduling.common import Action +from maro.utils import convert_dottable, Logger, LogFormat + +from ilp_agent import IlpAgent + +os.environ['LOG_LEVEL'] = 'CRITICAL' +FILE_PATH = os.path.split(os.path.realpath(__file__))[0] +CONFIG_PATH = os.path.join(FILE_PATH, "config.yml") +with io.open(CONFIG_PATH, "r") as in_file: + raw_config = yaml.safe_load(in_file) + config = convert_dottable(raw_config) + +LOG_PATH = os.path.join(FILE_PATH, "log", config.experiment_name) +if not os.path.exists(LOG_PATH): + os.makedirs(LOG_PATH) +simulation_logger = Logger(tag="simulation", format_=LogFormat.none, dump_folder=LOG_PATH, dump_mode="w", auto_timestamp=False) +ilp_logger = Logger(tag="ilp", format_=LogFormat.none, dump_folder=LOG_PATH, dump_mode="w", auto_timestamp=False) + +if __name__ == "__main__": + start_time = timeit.default_timer() + + env = Env( + scenario=config.env.scenario, + topology=config.env.topology, + start_tick=config.env.start_tick, + durations=config.env.durations, + snapshot_resolution=config.env.resolution + ) + shutil.copy( + os.path.join(env._business_engine._config_path, "config.yml"), + os.path.join(LOG_PATH, "BEconfig.yml") + ) + shutil.copy(CONFIG_PATH, os.path.join(LOG_PATH, "config.yml")) + + if config.env.seed is not None: + env.set_seed(config.env.seed) + + metrics: object = None + decision_event: DecisionPayload = None + is_done: bool = False + action: Action = None + + metrics, decision_event, is_done = env.step(None) + + # Get the core & memory capacity of all PMs in this environment. + pm_capacity = env.snapshot_list["pms"][ + env.frame_index::["cpu_cores_capacity", "memory_capacity"] + ].reshape(-1, 2) + pm_num = pm_capacity.shape[0] + + # ILP agent. + ilp_agent = IlpAgent( + ilp_config=config.ilp, + pm_capacity=pm_capacity, + vm_table_path=env.configs.VM_TABLE, + env_start_tick=config.env.start_tick, + env_duration=config.env.durations, + simulation_logger=simulation_logger, + ilp_logger=ilp_logger, + log_path=LOG_PATH + ) + + while not is_done: + # Get live VMs in each PM. + live_vm_set_list: List[Set[int]] = [env._business_engine._machines[idx]._live_vms for idx in range(pm_num)] + + action = ilp_agent.choose_action( + env_tick=env.tick, + cur_vm_id=decision_event.vm_id, + live_vm_set_list=live_vm_set_list, + ) + + metrics, decision_event, is_done = env.step(action) + + end_time = timeit.default_timer() + simulation_logger.info( + f"[Offline ILP] Topology: {config.env.topology}. Total ticks: {config.env.durations}." + f" Start tick: {config.env.start_tick}." + ) + simulation_logger.info(f"[Timer] {end_time - start_time:.2f} seconds to finish the simulation.") + ilp_agent.report_allocation_summary() + simulation_logger.info(pprint.pformat(metrics._original_dict)) diff --git a/examples/vm_scheduling/offline_lp/vm_scheduling_ilp.py b/examples/vm_scheduling/offline_lp/vm_scheduling_ilp.py new file mode 100644 index 000000000..80ffbc584 --- /dev/null +++ b/examples/vm_scheduling/offline_lp/vm_scheduling_ilp.py @@ -0,0 +1,179 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import timeit +from typing import List + +import math +import numpy as np +from pulp import PULP_CBC_CMD, GLPK, GUROBI_CMD, LpInteger, LpMaximize, LpProblem, LpStatus, LpVariable, lpSum + +from maro.utils import DottableDict, Logger + +from common import IlpPmCapacity, IlpVmInfo + + +# To indicate the decision of not allocate or cannot allocate any PM for current VM request. +NOT_ALLOCATE_NOW = -1 + +class VmSchedulingILP(): + def __init__(self, config: DottableDict, pm_capacity: List[IlpPmCapacity], logger: Logger, log_path: str): + self._logger = logger + self._log_path = log_path + + self._pm_capacity = pm_capacity + self._pm_num = len(self._pm_capacity) + + # LP solver. + msg = 1 if config.log.stdout_solver_message else 0 + if config.solver == "GLPK": + self._solver = GLPK(msg=msg) + elif config.solver == "CBC": + self._solver = PULP_CBC_CMD(msg=msg) + else: + print(f"Solver {config.solver} not added in ILP, choose from [GLPK, CBC]") + exit(0) + + # For formulation and action application. + self.plan_window_size = config.plan_window_size + self.apply_buffer_size = config.apply_buffer_size + + # For performance. + self.core_upper_ratio = 1 - config.performance.core_safety_remaining_ratio + self.mem_upper_ratio = 1 - config.performance.mem_safety_remaining_ratio + + # For objective. + self.successful_allocation_decay = config.objective.successful_allocation_decay + self.allocation_multiple_core_num = config.objective.allocation_multiple_core_num + + # For logger. + self.dump_all_solution = config.log.dump_all_solution + self.dump_infeasible_solution = config.log.dump_infeasible_solution + + # For problem formulation and application + self.last_solution_env_tick = -1 + + def _init_variables(self): + def _init_with_shape(shape: tuple): + return np.zeros(shape, dtype=np.int16).tolist() + + # Initialize the PM remaining capacity. + self._pm_allocated_core = _init_with_shape(shape=(self.plan_window_size, self._pm_num)) + self._pm_allocated_mem = _init_with_shape(shape=(self.plan_window_size, self._pm_num)) + for vm in self._allocated_vm: + last_tick = min(vm.remaining_lifetime(self._env_tick), self.plan_window_size) + for t in range(last_tick): + self._pm_allocated_core[t][vm.pm_idx] += vm.core + self._pm_allocated_mem[t][vm.pm_idx] += vm.mem + + # Initialize the PM-VM mapping variable. + self._vm_num = len(self._future_vm_req) + self._mapping = _init_with_shape(shape=(self._pm_num, self._vm_num)) + for pm_idx in range(self._pm_num): + for vm_idx in range(self._vm_num): + self._mapping[pm_idx][vm_idx] = LpVariable( + name=f"Place_VM{vm_idx}_{self._future_vm_req[vm_idx].id}_on_PM{pm_idx}", + lowBound=0, + upBound=1, + cat=LpInteger + ) + + def _add_constraints(self, problem: LpProblem): + # Mapping limitation: only 1 PM for a VM. + for vm_idx in range(self._vm_num): + problem += ( + lpSum(self._mapping[pm_idx][vm_idx] for pm_idx in range(self._pm_num)) <= 1, + f"Mapping_VM{vm_idx}_to_max_1_PM" + ) + + # PM capacity limitation: core + mem. + for t in range(self.plan_window_size): + for pm_idx in range(self._pm_num): + problem += ( + lpSum( + vm.core * self._mapping[pm_idx][vm_idx] + for vm_idx, vm in enumerate(self._future_vm_req) + if (vm.arrival_env_tick - self._env_tick <= t and vm.remaining_lifetime(self._env_tick) >= t) + ) + self._pm_allocated_core[t][pm_idx] <= self._pm_capacity[pm_idx].core * self.core_upper_ratio, + f"T{t}_PM{pm_idx}_core_capacity_limit" + ) + problem += ( + lpSum( + vm.mem * self._mapping[pm_idx][vm_idx] + for vm_idx, vm in enumerate(self._future_vm_req) + if (vm.arrival_env_tick - self._env_tick <= t and vm.remaining_lifetime(self._env_tick) >= t) + ) + self._pm_allocated_mem[t][pm_idx] <= self._pm_capacity[pm_idx].mem * self.mem_upper_ratio, + f"T{t}_PM{pm_idx}_mem_capacity_limit" + ) + + def _set_objective(self, problem: LpProblem): + # Reward for successful allocation. + # allocation_reward = lpSum( + # math.pow(self.successful_allocation_decay, self._future_vm_req[vm_idx].arrival_env_tick - self._env_tick) + # * lpSum(self._mapping[pm_idx][vm_idx] for pm_idx in range(self._pm_num)) + # * (self._future_vm_req[vm_idx].core if self.allocation_multiple_core_num else 1) + # for vm_idx in range(self._vm_num) + # ) + allocation_reward = lpSum( + math.pow(self.successful_allocation_decay, vm_idx) + * lpSum(self._mapping[pm_idx][vm_idx] for pm_idx in range(self._pm_num)) + * (self._future_vm_req[vm_idx].core if self.allocation_multiple_core_num else 1) + for vm_idx in range(self._vm_num) + ) + + problem += allocation_reward + + def _formulate_and_solve(self): + start_time = timeit.default_timer() + + problem = LpProblem( + name=f"VM_Scheduling_from_tick_{self._env_tick}", + sense=LpMaximize + ) + self._init_variables() + self._add_constraints(problem=problem) + self._set_objective(problem=problem) + problem.solve(self._solver) + + end_time = timeit.default_timer() + self._logger.info(f"[Timer] {end_time - start_time:.2f} seconds for tick {self._env_tick}.") + self._logger.info(f"Status: {LpStatus[problem.status]}") + if self.dump_all_solution or (self.dump_infeasible_solution and problem.status != 1): + problem.writeLP(os.path.join(self._log_path, f"solution_{self._env_tick}_{LpStatus[problem.status]}.lp")) + + for vm_idx, vm_req in enumerate(self._future_vm_req): + chosen_pm_idx = -1 + for pm_idx in range(self._pm_num): + if self._mapping[pm_idx][vm_idx].varValue: + chosen_pm_idx = pm_idx + break + self._logger.info(f"Solution tick: {self._env_tick}, vm: {self._future_vm_req[vm_idx].id} -> pm: {chosen_pm_idx}") + + + def choose_pm( + self, + env_tick: int, + vm_id: int, + allocated_vm: List[IlpVmInfo], + future_vm_req: List[IlpVmInfo], + vm_id_to_idx: dict, + ) -> int: + self._env_tick = env_tick + + if self.last_solution_env_tick < 0 or self._env_tick >= self.last_solution_env_tick + self.apply_buffer_size: + self._allocated_vm = allocated_vm + self._future_vm_req = future_vm_req + self._vm_id_to_idx = vm_id_to_idx + + self._formulate_and_solve() + self.last_solution_env_tick = self._env_tick + + assert vm_id in self._vm_id_to_idx, f"Tick {self._env_tick}, get vm_id {vm_id} not in vm_id_to_idx!" + vm_idx = self._vm_id_to_idx[vm_id] + + for pm_idx in range(self._pm_num): + if self._mapping[pm_idx][vm_idx].varValue: + return pm_idx + + return NOT_ALLOCATE_NOW From b51a5ceb640feba00b4ddf11f1fa0943f55f431e Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 29 Apr 2021 08:47:33 +0000 Subject: [PATCH 206/482] revamped rl design --- docs/source/apidoc/maro.rl.rst | 22 +- docs/source/key_components/rl_toolkit.rst | 14 +- examples/cim/ac/config.yml | 4 +- examples/cim/dqn/config.yml | 4 +- examples/supply_chain/agent.py | 17 -- examples/supply_chain/config.py | 26 +++ examples/supply_chain/config.yml | 89 +++----- examples/supply_chain/distributed_launcher.py | 70 +++--- examples/supply_chain/docker-compose.yml | 58 +++++ examples/supply_chain/exploration.py | 24 ++ examples/supply_chain/policies.py | 56 +++++ .../supply_chain/single_thread_launcher.py | 50 ++--- maro/rl/__init__.py | 49 +++-- maro/rl/algorithm/__init__.py | 5 +- maro/rl/algorithm/abs_policy.py | 59 ----- maro/rl/algorithm/ac.py | 50 +++-- maro/rl/algorithm/ddpg.py | 47 ++-- maro/rl/algorithm/dqn.py | 95 ++++---- maro/rl/algorithm/pg.py | 27 ++- maro/rl/algorithm/rl_policy_index.py | 56 +++++ maro/rl/distributed/__init__.py | 8 - maro/rl/distributed/learner.py | 72 ------ maro/rl/{training => }/env_wrapper.py | 68 +++--- maro/rl/experience/__init__.py | 8 + .../experience_memory.py} | 117 +++++----- maro/rl/{storage => experience}/sampler.py | 26 ++- .../sampler_cls_index.py | 2 +- maro/rl/exploration/__init__.py | 16 +- maro/rl/exploration/abs_exploration.py | 45 ++++ maro/rl/exploration/abs_explorer.py | 20 -- ...lorer.py => epsilon_greedy_exploration.py} | 16 +- maro/rl/exploration/exploration_scheduler.py | 128 +++++++++++ ...noise_explorer.py => noise_exploration.py} | 52 ++--- maro/rl/model/__init__.py | 2 +- maro/rl/model/core_model.py | 15 +- maro/rl/model/torch_cls_index.py | 57 ----- maro/rl/multi_agent/__init__.py | 6 - maro/rl/multi_agent/multi_agent_policy.py | 116 ---------- maro/rl/policy/__init__.py | 7 + maro/rl/policy/multi_agent_policy.py | 100 +++++++++ maro/rl/policy/policy.py | 162 ++++++++++++++ maro/rl/scheduling/__init__.py | 11 - maro/rl/scheduling/scheduler.py | 36 --- .../scheduling/simple_parameter_scheduler.py | 107 --------- maro/rl/storage/__init__.py | 8 - maro/rl/storage/abs_store.py | 58 ----- maro/rl/storage/sampler_cls_index.py | 8 - maro/rl/training/__init__.py | 5 +- maro/rl/{distributed => training}/actor.py | 43 +++- .../actor_manager.py | 52 ++++- .../{distributed => training}/dispatcher.py | 0 maro/rl/training/learner.py | 207 +++++++++++++----- .../message_enums.py | 8 +- maro/rl/training/test.py | 17 -- maro/rl/{distributed => training}/trainer.py | 0 maro/rl/utils/__init__.py | 6 +- maro/rl/utils/torch_cls_index.py | 8 +- maro/utils/exception/error_code.py | 2 +- maro/utils/exception/rl_toolkit_exception.py | 8 +- tests/test_store.py | 8 +- 60 files changed, 1310 insertions(+), 1147 deletions(-) delete mode 100644 examples/supply_chain/agent.py create mode 100644 examples/supply_chain/config.py create mode 100644 examples/supply_chain/docker-compose.yml create mode 100644 examples/supply_chain/exploration.py create mode 100644 examples/supply_chain/policies.py delete mode 100644 maro/rl/algorithm/abs_policy.py create mode 100644 maro/rl/algorithm/rl_policy_index.py delete mode 100644 maro/rl/distributed/__init__.py delete mode 100644 maro/rl/distributed/learner.py rename maro/rl/{training => }/env_wrapper.py (67%) create mode 100644 maro/rl/experience/__init__.py rename maro/rl/{storage/simple_store.py => experience/experience_memory.py} (64%) rename maro/rl/{storage => experience}/sampler.py (67%) rename maro/rl/{utils => experience}/sampler_cls_index.py (90%) create mode 100644 maro/rl/exploration/abs_exploration.py delete mode 100644 maro/rl/exploration/abs_explorer.py rename maro/rl/exploration/{epsilon_greedy_explorer.py => epsilon_greedy_exploration.py} (59%) create mode 100644 maro/rl/exploration/exploration_scheduler.py rename maro/rl/exploration/{noise_explorer.py => noise_exploration.py} (60%) delete mode 100644 maro/rl/model/torch_cls_index.py delete mode 100644 maro/rl/multi_agent/__init__.py delete mode 100644 maro/rl/multi_agent/multi_agent_policy.py create mode 100644 maro/rl/policy/__init__.py create mode 100644 maro/rl/policy/multi_agent_policy.py create mode 100644 maro/rl/policy/policy.py delete mode 100644 maro/rl/scheduling/__init__.py delete mode 100644 maro/rl/scheduling/scheduler.py delete mode 100644 maro/rl/scheduling/simple_parameter_scheduler.py delete mode 100644 maro/rl/storage/__init__.py delete mode 100644 maro/rl/storage/abs_store.py delete mode 100644 maro/rl/storage/sampler_cls_index.py rename maro/rl/{distributed => training}/actor.py (65%) rename maro/rl/{distributed => training}/actor_manager.py (66%) rename maro/rl/{distributed => training}/dispatcher.py (100%) rename maro/rl/{distributed => training}/message_enums.py (84%) delete mode 100644 maro/rl/training/test.py rename maro/rl/{distributed => training}/trainer.py (100%) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index 301548bf9..f62d75bbd 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -58,29 +58,29 @@ maro.rl.model.learning\_model :show-inheritance: -Explorer +Exploration ================================================================================ -maro.rl.exploration.abs\_explorer +maro.rl.exploration.abs\_exploration -------------------------------------------------------------------------------- -.. automodule:: maro.rl.exploration.abs_explorer +.. automodule:: maro.rl.exploration.abs_exploration :members: :undoc-members: :show-inheritance: -maro.rl.exploration.epsilon\_greedy\_explorer +maro.rl.exploration.epsilon\_greedy\_exploration -------------------------------------------------------------------------------- -.. automodule:: maro.rl.exploration.epsilon_greedy_explorer +.. automodule:: maro.rl.exploration.epsilon_greedy_exploration :members: :undoc-members: :show-inheritance: -maro.rl.exploration.noise\_explorer +maro.rl.exploration.noise\_exploration -------------------------------------------------------------------------------- -.. automodule:: maro.rl.exploration.noise_explorer +.. automodule:: maro.rl.exploration.noise_exploration :members: :undoc-members: :show-inheritance: @@ -121,18 +121,18 @@ maro.rl.shaping.abs\_shaper Storage ================================================================================ -maro.rl.storage.abs\_store +maro.rl.experience.abs\_store -------------------------------------------------------------------------------- -.. automodule:: maro.rl.storage.abs_store +.. automodule:: maro.rl.experience.abs_store :members: :undoc-members: :show-inheritance: -maro.rl.storage.simple\_store +maro.rl.experience.simple\_store -------------------------------------------------------------------------------- -.. automodule:: maro.rl.storage.simple_store +.. automodule:: maro.rl.experience.experience_memory :members: :undoc-members: :show-inheritance: diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 66116f775..e2801e278 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -72,22 +72,22 @@ And performing one gradient step is simply: model.learn(critic_loss + actor_loss) -Explorer +Exploration -------- MARO provides an abstraction for exploration in RL. Some RL algorithms such as DQN and DDPG require -explicit exploration governed by a set of parameters. The ``AbsExplorer`` class is designed to cater -to these needs. Simple exploration schemes, such as ``EpsilonGreedyExplorer`` for discrete action space -and ``UniformNoiseExplorer`` and ``GaussianNoiseExplorer`` for continuous action space, are provided in +explicit exploration governed by a set of parameters. The ``AbsExploration`` class is designed to cater +to these needs. Simple exploration schemes, such as ``EpsilonGreedyExploration`` for discrete action space +and ``UniformNoiseExploration`` and ``GaussianNoiseExploration`` for continuous action space, are provided in the toolkit. -As an example, the exploration for DQN may be carried out with the aid of an ``EpsilonGreedyExplorer``: +As an example, the exploration for DQN may be carried out with the aid of an ``EpsilonGreedyExploration``: .. code-block:: python - explorer = EpsilonGreedyExplorer(num_actions=10) + exploration = EpsilonGreedyExploration(num_actions=10) greedy_action = learning_model(state, training=False).argmax(dim=1).data - exploration_action = explorer(greedy_action) + exploration_action = exploration(greedy_action) Tools for Training diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index 45955023c..af5dde14f 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -65,8 +65,8 @@ agent: experience_memory_size: -1 experience_memory_overwrite_type: "rolling" empty_experience_memory_after_step: bool, - min_new_experiences_to_trigger_learning: 1 - min_experiences_to_trigger_learning: 1 + new_experience_trigger: 1 + min_experiences_to_trigger_training: 1 training: env: scenario: cim diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 422e92638..565d48dc6 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -54,8 +54,8 @@ agent: experience_memory_size: -1 experience_memory_overwrite_type: "rolling" empty_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 16 - min_experiences_to_trigger_learning: 1024 + new_experience_trigger: 16 + min_experiences_to_trigger_training: 1024 training: env: scenario: cim diff --git a/examples/supply_chain/agent.py b/examples/supply_chain/agent.py deleted file mode 100644 index 643f085aa..000000000 --- a/examples/supply_chain/agent.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.rl import DQN, DQNConfig, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel - - -def get_dqn_agent(config): - q_model = SimpleMultiHeadModel( - FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"]) - ) - return DQN(q_model, DQNConfig(**config["algorithm_config"]), **config["experience_memory"]) - - -def get_ppo_agent(config): - pass - -get_agent_func_map = {"dqn": get_dqn_agent} diff --git a/examples/supply_chain/config.py b/examples/supply_chain/config.py new file mode 100644 index 000000000..351930f41 --- /dev/null +++ b/examples/supply_chain/config.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +import yaml +from os import getenv +from os.path import dirname, join, realpath + +from maro.simulator import Env + +sc_code_dir = dirname(realpath(__file__)) +sys.path.insert(0, sc_code_dir) +from env_wrapper import SCEnvWrapper + + +default_config_path = join(dirname(realpath(__file__)), "config.yml") +with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: + config = yaml.safe_load(config_file) + +topology = config["env"]["topology"] +config["env"]["topology"] = join(sc_code_dir, "topologies", topology) +env = SCEnvWrapper(Env(**config["env"])) +consumer_agent_ids = [f"consumer.{info.id}" for info in env.agent_idx_list] +producer_agent_ids = [f"producer.{info.id}" for info in env.agent_idx_list] +config["agent_ids"] = consumer_agent_ids + producer_agent_ids +config["policy"]["consumer"]["model"]["input_dim"] = env.dim \ No newline at end of file diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index f02ee8505..7b7b07969 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -1,7 +1,20 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -agent: +env: + scenario: supply_chain + # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder + # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ + topology: sample1 + durations: 200 # number of ticks per episode +num_episodes: 10 # number of episodes to simulate +# Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps +# before returning experiences to the learner. The learner uses these experiences to update the agents' policies +# and sync the updated policies to the actors for the next learning cycle. +policy_update_interval: 60 +eval_points: 2 +log_env_metrics: false +policy: consumer: algorithm: dqn model: # If you want to design your model used, edit the get_dqn_agent() codes in examples\supply_chain\agent.py @@ -21,72 +34,28 @@ agent: lr: 0.001 algorithm_config: reward_discount: .9 - train_iters: 10 # number of gradient steps per call to agent.step() - batch_size: 128 # mini-batch size for DQN training loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. target_update_freq: 5 # How many training iteration, to update DQN target model soft_update_coefficient: 0.1 double: false # whether to enable double DQN + training_loop: + sampler_cls: uniform + batch_size: 128 + sampler_kwargs: + replace: true + train_iters: 10 + new_experience_trigger: 128 + # The minimum number of experiences in the experience memory required to trigger learning (agent.step()). Defaults to 1. + experience_memory_size_trigger: 1000 experience_memory: - experience_memory_size: 50000 # experience memory size + capacity: 50000 # experience memory size # This determines how existing experiences are replaced when adding new experiences to a full experience # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. - experience_memory_overwrite_type: random - # If true, the agent's experience memory will be emptied after calling agent.step(). - empty_experience_memory_after_step: false - # The minimum number of new experiences required to trigger learning (agent.step()). Defaults to 1. - min_new_experiences_to_trigger_learning: 10 - # The minimum number of experiences in the experience memory required to trigger learning (agent.step()). Defaults to 1. - min_experiences_to_trigger_learning: 10 - producer: - algorithm: dqn - model: - hidden_dims: - - 16 - - 8 - output_dim: 10 - activation: leaky_relu - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop - optim_params: - lr: 0.001 - algorithm_config: - reward_discount: .9 - train_iters: 10 - batch_size: 128 - loss_cls: mse - target_update_freq: 5 - soft_update_coefficient: 0.1 - double: false # double DQN - experience_memory: - experience_memory_size: 50000 - experience_memory_overwrite_type: random - empty_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 1 - min_experiences_to_trigger_learning: 1 -training: - env: - scenario: supply_chain - # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder - # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ - topology: sample1 - durations: 200 # number of ticks per episode - num_episodes: 3 # number of episodes to simulate - # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps - # before returning experiences to the learner. The learner uses these experiences to update the agents' policies - # and sync the updated policies to the actors for the next learning cycle. - agent_update_interval: 60 - exploration: - parameter_names: - - epsilon - start: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. - end: 0.0 + overwrite_type: random +exploration: + initial_value: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. + final_value: 0.0 distributed: # this is used to group all actor / learner processes belonging to the same job for communication purposes. # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. @@ -95,7 +64,7 @@ distributed: # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make # sure that a local redis server is running and listening on the port specified by "redis_port". - redis_host: redis-sc + redis_host: localhost redis_port: 6379 # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent # slow actors from dragging down the whole process. diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index f752f39b9..b6bdf8ec8 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -3,37 +3,23 @@ import argparse import sys -import time import yaml from multiprocessing import Process from os import getenv, makedirs from os.path import dirname, join, realpath -from maro.rl import ( - Actor, ActorManager, DistLearner, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, - OptimOption, SimpleMultiHeadModel -) +from maro.rl import Actor, ActorManager, Learner, MultiAgentPolicy from maro.simulator import Env from maro.utils import set_seeds sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) +from config import config from env_wrapper import SCEnvWrapper -from agent import get_agent_func_map +from exploration import exploration_dict, agent_to_exploration +from policies import policy_dict, agent_to_policy -# Read the configuration -DEFAULT_CONFIG_PATH = join(sc_code_dir, "config.yml") -with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: - config = yaml.safe_load(config_file) - -get_producer_agent = get_agent_func_map[config["agent"]["producer"]["algorithm"]] -get_consumer_agent = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] - -# Get the env config path -topology = config["training"]["env"]["topology"] -config["training"]["env"]["topology"] = join(sc_code_dir, "topologies", topology) - # for distributed / multi-process training GROUP = getenv("GROUP", default=config["distributed"]["group"]) REDIS_HOST = config["distributed"]["redis_host"] @@ -45,45 +31,43 @@ def sc_learner(): - # create agents that update themselves using experiences collected from the actors. - env = SCEnvWrapper(Env(**config["training"]["env"])) - config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in env.agent_idx_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} - agent = MultiAgentWrapper({**producers, **consumers}) - - # exploration schedule - scheduler = LinearParameterScheduler(config["training"]["num_episodes"], **config["training"]["exploration"]) - + # create a multi-agent policy. + policy = MultiAgentPolicy( + policy_dict, + agent_to_policy, + exploration_dict=exploration_dict, + agent_to_exploration=agent_to_exploration + ) + # create an actor manager to collect simulation data from multiple actors actor_manager = ActorManager( NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False}, log_dir=log_dir ) - # create a learner to start the training process - learner = DistLearner( - agent, scheduler, actor_manager, - agent_update_interval=config["training"]["agent_update_interval"], + # create a learner to start training + learner = Learner( + policy, None, config["num_episodes"], + actor_manager=actor_manager, + policy_update_interval=config["policy_update_interval"], + eval_points=config["eval_points"], required_actor_finishes=config["distributed"]["required_actor_finishes"], - discard_stale_experiences=config["distributed"]["discard_stale_experiences"], - log_dir=log_dir + log_env_metrics=config["log_env_metrics"] ) learner.run() def sc_actor(): # create an env wrapper for roll-out and obtain the input dimension for the agents - env = SCEnvWrapper(Env(**config["training"]["env"])) - config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - - # create agents to interact with the env - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in env.agent_idx_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} - agent = MultiAgentWrapper({**producers, **consumers}) - + env = SCEnvWrapper(Env(**config["env"])) + policy = MultiAgentPolicy( + policy_dict, + agent_to_policy, + exploration_dict=exploration_dict, + agent_to_exploration=agent_to_exploration + ) # create an actor that collects simulation data for the learner. - actor = Actor(env, agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, log_dir=log_dir) + actor = Actor(env, policy, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, log_dir=log_dir) actor.run() diff --git a/examples/supply_chain/docker-compose.yml b/examples/supply_chain/docker-compose.yml new file mode 100644 index 000000000..949068879 --- /dev/null +++ b/examples/supply_chain/docker-compose.yml @@ -0,0 +1,58 @@ +services: + actor.0: + command: + - python3 + - /maro/supply_chain/distributed_launcher.py + - -w + - '2' + container_name: actor.0 + environment: + - COMPONENT_NAME=actor.0 + image: maro-sc + volumes: + - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain + - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl + actor.1: + command: + - python3 + - /maro/supply_chain/distributed_launcher.py + - -w + - '2' + container_name: actor.1 + environment: + - COMPONENT_NAME=actor.1 + image: maro-sc + volumes: + - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain + - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl + actor.2: + command: + - python3 + - /maro/supply_chain/distributed_launcher.py + - -w + - '2' + container_name: actor.2 + environment: + - COMPONENT_NAME=actor.2 + image: maro-sc + volumes: + - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain + - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl + learner: + build: + context: /home/data_disk/yaqiu/maro + dockerfile: /home/data_disk/yaqiu/maro/docker_files/dev.df + command: + - python3 + - /maro/supply_chain/distributed_launcher.py + - -w + - '1' + container_name: learner + image: maro-sc + volumes: + - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain + - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl + redis: + container_name: redis-sc + image: redis:6 +version: '3.9' diff --git a/examples/supply_chain/exploration.py b/examples/supply_chain/exploration.py new file mode 100644 index 000000000..5d38d4df0 --- /dev/null +++ b/examples/supply_chain/exploration.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os.path import dirname, realpath + +from maro.rl import EpsilonGreedyExploration, LinearExplorationScheduler + +sc_code_dir = dirname(realpath(__file__)) +sys.path.insert(0, sc_code_dir) +from config import config + + +exploration = EpsilonGreedyExploration(config["policy"]["consumer"]["model"]["output_dim"]) +exploration.register_schedule( + LinearExplorationScheduler, "epsilon", config["num_episodes"], + initial_value=config["exploration"]["initial_value"], + kwargs={"final_value": config["exploration"]["final_value"]} +) + +exploration_dict = {"consumer": exploration} + +# all agents shared the same exploration object +agent_to_exploration = {agent_id: "consumer" for agent_id in config["agent_ids"] if agent_id.startswith("consumer")} diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py new file mode 100644 index 000000000..afd3ddefe --- /dev/null +++ b/examples/supply_chain/policies.py @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os.path import dirname, realpath + +import numpy as np + +import torch + +from maro.rl import ( + DQN, DQNConfig, ExperienceMemory, FullyConnectedBlock, NullPolicy, OptimOption, QNetForDiscreteActionSpace, + TrainingLoopConfig, get_sampler_cls +) + +sc_code_dir = dirname(realpath(__file__)) +sys.path.insert(0, sc_code_dir) +from config import config + +agent_ids = config["agent_ids"] +config = config["policy"] + + +class SimpleQNet(QNetForDiscreteActionSpace): + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component.forward(states) + + +def get_dqn_policy(config): + q_net = SimpleQNet(FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"])) + + exp_config = config["experience_memory"] + experience_memory = ExperienceMemory(capacity=exp_config["capacity"], overwrite_type=exp_config["overwrite_type"]) + + train_config = config["training_loop"] + generic_config = TrainingLoopConfig( + get_sampler_cls(train_config["sampler_cls"]), + train_config["batch_size"], + train_config["train_iters"], + sampler_kwargs=train_config["sampler_kwargs"], + new_experience_trigger=train_config["new_experience_trigger"], + experience_memory_size_trigger=train_config["experience_memory_size_trigger"] + ) + + alg_config = config["algorithm_config"] + special_config = DQNConfig(**alg_config) + + return DQN(q_net, experience_memory, generic_config, special_config) + +# all consumers share the same underlying policy +policy_dict = {"consumer": get_dqn_policy(config["consumer"]), "producer": NullPolicy()} + +agent_to_policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index 1bc18ba13..6df35efb2 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -2,48 +2,34 @@ # Licensed under the MIT license. import sys -import yaml -from os import getenv -from os.path import dirname, join, realpath +from os.path import dirname, realpath -import numpy as np - -from maro.rl import Learner, LinearParameterScheduler, MultiAgentWrapper +from maro.rl import Learner, MultiAgentPolicy from maro.simulator import Env -from maro.utils import set_seeds sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) +from config import config from env_wrapper import SCEnvWrapper -from agent import get_agent_func_map +from exploration import exploration_dict, agent_to_exploration +from policies import policy_dict, agent_to_policy # Single-threaded launcher if __name__ == "__main__": - default_config_path = join(dirname(realpath(__file__)), "config.yml") - with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: - config = yaml.safe_load(config_file) - - get_producer_agent = get_agent_func_map[config["agent"]["producer"]["algorithm"]] - get_consumer_agent = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] - - # Get the env config path - topology = config["training"]["env"]["topology"] - config["training"]["env"]["topology"] = join(dirname(realpath(__file__)), "topologies", topology) - - # create an env wrapper and obtain the input dimension for the agents - env = SCEnvWrapper(Env(**config["training"]["env"])) - config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - - # create agents - agent_info_list = env.agent_idx_list - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in agent_info_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in agent_info_list} - agent = MultiAgentWrapper({**producers, **consumers}) - - # exploration schedule - scheduler = LinearParameterScheduler(config["training"]["num_episodes"], **config["training"]["exploration"]) + env = SCEnvWrapper(Env(**config["env"])) + policy = MultiAgentPolicy( + policy_dict, + agent_to_policy, + exploration_dict=exploration_dict, + agent_to_exploration=agent_to_exploration + ) # create a learner to start training - learner = Learner(env, agent, scheduler, agent_update_interval=config["training"]["agent_update_interval"]) + learner = Learner( + policy, env, config["num_episodes"], + policy_update_interval=config["policy_update_interval"], + eval_points=config["eval_points"], + log_env_metrics=config["log_env_metrics"] + ) learner.run() diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 12d1c99b3..19bba3fdd 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -2,33 +2,38 @@ # Licensed under the MIT license. from maro.rl.algorithm import ( - DDPG, DQN, AbsFixedPolicy, AbsTrainablePolicy, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, - PolicyGradient, PolicyGradientConfig + DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, + get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) -from maro.rl.distributed import Actor, ActorManager, DistLearner +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.experience import AbsSampler, ExperienceMemory, ExperienceSet, Replay, UniformSampler, get_sampler_cls from maro.rl.exploration import ( - AbsExplorer, EpsilonGreedyExplorer, GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer + AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, + MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, UniformNoiseExploration ) -from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel -from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler -from maro.rl.storage import AbsSampler, AbsStore, SimpleStore, UniformSampler -from maro.rl.training import AbsEnvWrapper, Learner +from maro.rl.model import ( + AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, PolicyNetForDiscreteActionSpace, + PolicyValueNetForContinuousActionSpace, PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace +) +from maro.rl.policy import AbsCorePolicy, AbsFixedPolicy, MultiAgentPolicy, NullPolicy, RLPolicy, TrainingLoopConfig +from maro.rl.training import Actor, ActorManager, Learner from maro.rl.utils import ( - get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_sampler_cls, get_torch_activation_cls, - get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward, - select_by_actions + get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, + get_torch_optim_cls, get_truncated_cumulative_reward ) __all__ = [ - "AbsFixedPolicy", "AbsTrainablePolicy", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", - "DQNConfig", "PolicyGradient", "PolicyGradientConfig", - "Actor", "ActorManager", "DistLearner", - "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", - "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", - "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", - "AbsSampler", "AbsStore", "SimpleStore", "UniformSampler", - "AbsEnvWrapper", "Learner", - "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_sampler_cls", - "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", - "get_truncated_cumulative_reward", "select_by_actions" + "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", + "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", + "AbsEnvWrapper", + "AbsSampler", "ExperienceMemory", "ExperienceSet", "Replay", "UniformSampler", "get_sampler_cls", + "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", + "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", + "UniformNoiseExploration", + "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "PolicyNetForDiscreteActionSpace", + "PolicyValueNetForContinuousActionSpace", "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace", + "AbsCorePolicy", "AbsFixedPolicy", "MultiAgentPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig", + "Actor", "ActorManager", "Learner", + "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", + "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/algorithm/__init__.py b/maro/rl/algorithm/__init__.py index aa6a1eff4..8449e3e59 100644 --- a/maro/rl/algorithm/__init__.py +++ b/maro/rl/algorithm/__init__.py @@ -1,17 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .abs_policy import AbsFixedPolicy, AbsTrainablePolicy from .ac import ActorCritic, ActorCriticConfig from .ddpg import DDPG, DDPGConfig from .dqn import DQN, DQNConfig from .pg import PolicyGradient, PolicyGradientConfig +from .rl_policy_index import get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls __all__ = [ - "AbsPolicy", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", - "MultiAgentWrapper", "PolicyGradient", "PolicyGradientConfig", + "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls" ] diff --git a/maro/rl/algorithm/abs_policy.py b/maro/rl/algorithm/abs_policy.py deleted file mode 100644 index 4626c2d39..000000000 --- a/maro/rl/algorithm/abs_policy.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - - -class AbsFixedPolicy(ABC): - """Abstract fixed policy class. - - Args: - config: Settings for the algorithm. - """ - def __init__(self, config): - self.config = config - - @abstractmethod - def choose_action(self, state): - """Compute an action based on a state object. - - Args: - state: State object. - - Returns: - The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert - this to an environment executable action. - """ - raise NotImplementedError - - -class AbsTrainablePolicy(ABC): - """Abstract fixed policy class. - - Args: - config: Settings for the algorithm. - """ - def __init__(self, config): - self.config = config - - @abstractmethod - def choose_action(self, state): - """Compute an action based on a state object. - - Args: - state: State object. - - Returns: - The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert - this to an environment executable action. - """ - raise NotImplementedError - - @abstractmethod - def update(self, experience_obj): - """Algorithm-specific training logic. - - The parameters are data to train the underlying model on. Algorithm-specific loss and optimization - should be reflected here. - """ - raise NotImplementedError diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index 05ab0b1fe..f62d3dedc 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -8,12 +8,10 @@ import torch from torch.distributions import Categorical +from maro.rl.experience import ExperienceMemory, ExperienceSet from maro.rl.model import PolicyValueNetForDiscreteActionSpace -from maro.rl.utils import get_log_prob, get_torch_loss_cls - -from .abs_policy import AbsPolicy - -ACExperience = namedtuple("ACExperience", ["state", "action", "reward", "next_state"]) +from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.utils import get_torch_loss_cls class ActorCriticConfig: @@ -43,7 +41,7 @@ def __init__( self.clip_ratio = clip_ratio -class ActorCritic(AbsPolicy): +class ActorCritic(AbsCorePolicy): """Actor Critic algorithm with separate policy and value models. References: @@ -55,11 +53,17 @@ class ActorCritic(AbsPolicy): and state values. config: Configuration for the AC algorithm. """ - def __init__(self, ac_net: PolicyValueNetForDiscreteActionSpace, config: ActorCriticConfig): + def __init__( + self, + ac_net: PolicyValueNetForDiscreteActionSpace, + experience_memory: ExperienceMemory, + generic_config: TrainingLoopConfig, + special_config: ActorCriticConfig + ): if not isinstance(ac_net, PolicyValueNetForDiscreteActionSpace): raise TypeError("model must be an instance of 'PolicyValueNetForDiscreteActionSpace'") - super().__init__(config) + super().__init__(experience_memory, generic_config, special_config) self.ac_net = ac_net def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: @@ -76,31 +80,37 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def update(self, experience_obj: ACExperience): - if not isinstance(experience_obj, ACExperience): - raise TypeError(f"Expected experience object of type ACExperience, got {type(experience_obj)}") + def step(self, experience_set: ExperienceSet): + if not isinstance(experience_set, ExperienceSet): + raise TypeError(f"Expected experience object of type ACExperience, got {type(experience_set)}") - states, next_states = experience_obj.state, experience_obj.next_state - actions = torch.from_numpy(np.asarray([act[0] for act in experience_obj.action])) - log_p = torch.from_numpy(np.asarray([act[1] for act in experience_obj.action])) - rewards = torch.from_numpy(np.asarray(experience_obj.reward)) + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)) state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() - return_est = rewards + self.config.reward_discount * next_state_values + return_est = rewards + self.special_config.reward_discount * next_state_values advantages = return_est - state_values # actor loss action_prob, state_values = self.ac_net(states) log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - if self.config.clip_ratio is not None: + if self.special_config.clip_ratio is not None: ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) + clip_ratio = torch.clamp(ratio, 1 - self.special_config.clip_ratio, 1 + self.special_config.clip_ratio) actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() else: actor_loss = -(log_p_new * advantages).mean() # critic_loss - critic_loss = self.config.critic_loss_func(state_values, return_est) - loss = critic_loss + self.config.actor_loss_coefficient * actor_loss + critic_loss = self.special_config.critic_loss_func(state_values, return_est) + loss = critic_loss + self.special_config.actor_loss_coefficient * actor_loss self.ac_net.step(loss) + + def update(self, policy_state): + self.ac_net.load_state_dict(policy_state) + + def state(self): + return self.q_net.state_dict() \ No newline at end of file diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index 017ad76c5..dde61e068 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -7,13 +7,11 @@ import numpy as np import torch -from maro.rl.exploration import NoiseExplorer +from maro.rl.experience import ExperienceMemory, ExperienceSet from maro.rl.model import PolicyValueNetForContinuousActionSpace +from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig from maro.rl.utils import get_torch_loss_cls -from .abs_policy import AbsPolicy - -DDPGExperience = namedtuple("DDPGExperience", ["state", "action", "reward", "next_state"]) class DDPGConfig: """Configuration for the DDPG algorithm. @@ -49,7 +47,7 @@ def __init__( self.soft_update_coefficient = soft_update_coefficient -class DDPG(AbsPolicy): +class DDPG(AbsCorePolicy): """The Deep Deterministic Policy Gradient (DDPG) algorithm. References: @@ -60,33 +58,34 @@ class DDPG(AbsPolicy): ac_net (PolicyValueNetForContinuousActionSpace): DDPG policy and q-value models. config (DDPGConfig): Configuration for DDPG algorithm. """ - def __init__(self, ac_net: PolicyValueNetForContinuousActionSpace, config: DDPGConfig, explorer: NoiseExplorer = None): + def __init__( + self, + ac_net: PolicyValueNetForContinuousActionSpace, + experience_memory: ExperienceMemory, + generic_config: TrainingLoopConfig, + special_config: DDPGConfig + ): if not isinstance(ac_net, PolicyValueNetForContinuousActionSpace): raise TypeError("model must be an instance of 'PolicyValueNetForContinuousActionSpace'") - super().__init__(config) + super().__init__(experience_memory, generic_config, special_config) self.ac_net = ac_net self.target_ac_net = ac_net.copy() if model.trainable else None - self._explorer = explorer self._train_cnt = 0 def choose_action(self, states) -> Union[float, np.ndarray]: with torch.no_grad(): - actions = self.ac_net.choose_action(states) + actions = self.ac_net.choose_action(states).cpu().numpy() - actions = actions.cpu().numpy() - if self._explorer: - action = self._explorer(action) - return actions[0] if len(actions) == 1 else actions - def update(self, experience_obj: DDPGExperience): - if not isinstance(experience_obj, DDPGExperience): - raise TypeError(f"Expected experience object of type DDPGExperience, got {type(experience_obj)}") + def step(self, experience_set: ExperienceSet): + if not isinstance(experience_set, ExperienceSet): + raise TypeError(f"Expected experience object of type DDPGExperience, got {type(experience_set)}") - states, next_states = experience_obj.state, experience_obj.next_state - actual_actions = torch.from_numpy(experience_obj.action) - rewards = torch.from_numpy(experience_obj.reward) + states, next_states = experience_set.states, experience_set.next_states + actual_actions = torch.from_numpy(experience_set.actions) + rewards = torch.from_numpy(experience_set.rewards) if len(actual_actions.shape) == 1: actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) @@ -96,11 +95,11 @@ def update(self, experience_obj: DDPGExperience): next_q_values = self.target_ac_net( torch.cat([next_states, next_actions], dim=1), task_name="q_value", training=False ).squeeze(1) # (N,) - target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) - q_value_loss = self.config.q_value_loss_func(current_q_values, target_q_values) + target_q_values = (rewards + self.special_config.reward_discount * next_q_values).detach() # (N,) + q_value_loss = self.special_config.q_value_loss_func(current_q_values, target_q_values) actions_from_model = self.ac_net(states, task_name="policy") policy_loss = -self.ac_net(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() - self.ac_net.step(q_value_loss + self.config.policy_loss_coefficient * policy_loss) + self.ac_net.step(q_value_loss + self.special_config.policy_loss_coefficient * policy_loss) self._train_cnt += 1 - if self._train_cnt % self.config.target_update_freq == 0: - self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coefficient) + if self._train_cnt % self.special_config.target_update_freq == 0: + self.target_ac_net.soft_update(self.ac_net, self.special_config.soft_update_coefficient) diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index aecd67c1f..c516dc152 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -7,12 +7,10 @@ import numpy as np import torch +from maro.rl.experience import ExperienceMemory, ExperienceSet from maro.rl.model import QNetForDiscreteActionSpace -from maro.rl.utils import get_max, get_sampler_cls, get_td_errors, get_torch_loss_cls, select_by_actions - -from .abs_policy import AbsPolicy - -DQNExperience = namedtuple("DQNExperience", ["state", "action", "reward", "next_state"]) +from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.utils import get_torch_loss_cls class DQNConfig: @@ -21,7 +19,6 @@ class DQNConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. target_update_freq (int): Number of training rounds between target model updates. - epsilon (float): Exploration rate for epsilon-greedy exploration. Defaults to None. soft_update_coefficient (float): Soft update coefficient, e.g., target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. Defaults to 1.0. @@ -33,27 +30,25 @@ class DQNConfig: """ __slots__ = [ "reward_discount", "target_update_freq", "train_iters", "batch_size", "sampler_cls", "sampler_params", - "epsilon", "soft_update_coefficient", "double", "loss_func" + "soft_update_coefficient", "double", "loss_func" ] def __init__( self, reward_discount: float, target_update_freq: int, - epsilon: float = .0, soft_update_coefficient: float = 0.1, double: bool = True, loss_cls="mse" ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq - self.epsilon = epsilon self.soft_update_coefficient = soft_update_coefficient self.double = double self.loss_func = get_torch_loss_cls(loss_cls)() -class DQN(AbsPolicy): +class DQN(AbsCorePolicy): """The Deep-Q-Networks algorithm. See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. @@ -61,68 +56,64 @@ class DQN(AbsPolicy): Args: model (QNetForDiscreteActionSpace): Q-value model. config (DQNConfig): Configuration for DQN algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. Defaults to False. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. """ - def __init__(self, q_net: QNetForDiscreteActionSpace, config: DQNConfig): + def __init__( + self, + q_net: QNetForDiscreteActionSpace, + experience_memory: ExperienceMemory, + generic_config: TrainingLoopConfig, + special_config: DQNConfig, + ): if not isinstance(q_net, QNetForDiscreteActionSpace): raise TypeError("model must be an instance of 'QNetForDiscreteActionSpace'") - super().__init__(config) + super().__init__(experience_memory, generic_config, special_config) self.q_net = q_net - self.target_q_net = model.copy() if model.trainable else None + self.target_q_net = q_net.copy() if q_net.trainable else None self.target_q_net.eval() self._training_counter = 0 - self._num_actions = self.q_net.num_actions def choose_action(self, states) -> Union[int, np.ndarray]: with torch.no_grad(): self.q_net.eval() - actions, q_vals = self.q_net.choose_action(states) - - actions, q_vals = actions.cpu().numpy(), q_vals.cpu().numpy() - if len(actions) == 1: - return actions[0] if np.random.random() > self.config.epsilon else np.random.choice(self._num_actions) - else: - return np.array([ - action if np.random.random() > self.config.epsilon else np.random.choice(self._num_actions) - for action in actions - ]) - - def update(self, experience_obj: DQNExperience): - if not isinstance(experience_obj, DQNExperience): - raise TypeError(f"Expected experience object of type DQNExperience, got {type(experience_obj)}") - - self.q_net.train() + actions, _ = self.q_net.choose_action(states) + + actions = actions.cpu().numpy() + return actions[0] if len(actions) == 1 else actions + + def step(self, experience_set: ExperienceSet): + if not isinstance(experience_set, ExperienceSet): + raise TypeError( + f"Expected experience object of type AbsCorePolicy.experience_type, got {type(experience_set)}" + ) + + self.q_net.train() # sample from the replay memory - states, next_states = experience_obj.state, experience_obj.next_state - actions = torch.from_numpy(experience_obj.action) - rewards = torch.from_numpy(experience_obj.reward) - q_values = self.q_net(states, actions) + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray(experience_set.actions)) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)) + q_values = self.q_net.q_values(states, actions) # get next Q values with torch.no_grad(): - if self.config.double: - next_q_values = self.target_q_net(next_states, self.q_net.choose_action(next_states)[0]) # (N,) + if self.special_config.double: + next_q_values = self.target_q_net.q_values(next_states, self.q_net.choose_action(next_states)[0]) else: next_q_values = self.target_q_net.choose_action(next_states)[1] # (N,) # get TD errors - target_q_values = (rewards + self.config.gamma * next_q_values).detach() # (N,) - loss = self.config.loss_func(q_values, target_q_values) + target_q_values = (rewards + self.special_config.reward_discount * next_q_values).detach() # (N,) + loss = self.special_config.loss_func(q_values, target_q_values) # train and update target if necessary self.q_net.step(loss.mean()) self._training_counter += 1 - if self._training_counter % self.config.target_update_freq == 0: - self.target_q_net.soft_update(self.q_net, self.config.soft_update_coefficient) + if self._training_counter % self.special_config.target_update_freq == 0: + self.target_q_net.soft_update(self.q_net, self.special_config.soft_update_coefficient) + + def update(self, policy_state): + self.q_net.load_state_dict(policy_state) + self.target_q_net = self.q_net.copy() if self.q_net.trainable else None + self.target_q_net.eval() - def set_exploration_params(self, epsilon): - self.config.epsilon = epsilon + def state(self): + return self.q_net.state_dict() diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index 4b597d2c3..3d01e7db3 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -7,11 +7,10 @@ import torch from torch.distributions import Categorical -from maro.rl.model import ParameterizedPolicy +from maro.rl.model import PolicyNetForDiscreteActionSpace +from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_truncated_cumulative_reward -from .abs_policy import AbsPolicy - class PolicyGradientConfig: """Configuration for the Policy Gradient algorithm. @@ -25,7 +24,7 @@ def __init__(self, reward_discount: float): self.reward_discount = reward_discount -class PolicyGradient(AbsPolicy): +class PolicyGradient(AbsCorePolicy): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. @@ -40,28 +39,28 @@ class PolicyGradient(AbsPolicy): to be overwritten after its capacity has been reached. Must be "rolling" or "random". empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call to ``step``. Defaults to True. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. + new_experience_trigger (int): Minimum number of new experiences required to trigger learning. Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for + min_experiences_to_trigger_training (int): Minimum number of experiences in the experience memory required for training. Defaults to 1. """ def __init__( self, - model: ParameterizedPolicy, + model: PolicyNetForDiscreteActionSpace, config: PolicyGradientConfig, experience_memory_size: int, experience_memory_overwrite_type: str, empty_experience_memory_after_step: bool = True, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 + new_experience_trigger: int = 1, + min_experiences_to_trigger_training: int = 1 ): - if not isinstance(model, ParameterizedPolicyWithValueEstimator): - raise TypeError("model must be an instance of 'ParameterizedPolicy'") + if not isinstance(model, PolicyNetForDiscreteActionSpace): + raise TypeError("model must be an instance of 'PolicyNetForDiscreteActionSpace'") super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, - min_new_experiences_to_trigger_learning=min_new_experiences_to_trigger_learning, - min_experiences_to_trigger_learning=min_experiences_to_trigger_learning + new_experience_trigger=new_experience_trigger, + min_experiences_to_trigger_training=min_experiences_to_trigger_training ) def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: @@ -87,7 +86,7 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): states = torch.from_numpy(states).to(self.device) actions = torch.from_numpy(actions).to(self.device) - returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) + returns = get_truncated_cumulative_reward(rewards, self.special_config.reward_discount) returns = torch.from_numpy(returns).to(self.device) action_distributions = self.model(states) action_prob = action_distributions.gather(1, actions.unsqueeze(1)).squeeze() # (N, 1) diff --git a/maro/rl/algorithm/rl_policy_index.py b/maro/rl/algorithm/rl_policy_index.py new file mode 100644 index 000000000..397d0b51e --- /dev/null +++ b/maro/rl/algorithm/rl_policy_index.py @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac import ActorCritic, ActorCriticConfig, PolicyValueNetForDiscreteActionSpace +from .ddpg import DDPG, DDPGConfig, PolicyValueNetForContinuousActionSpace +from .dqn import DQN, DQNConfig, QNetForDiscreteActionSpace +from .pg import PolicyGradient, PolicyGradientConfig, PolicyNetForDiscreteActionSpace + + +RL_POLICY_INDEX = { + "ac": ActorCritic, + "dqn": DQN, + "ddpg": DDPG, + "pg": PolicyGradient +} + +RL_POLICY_CONFIG_INDEX = { + "ac": ActorCritic, + "dqn": DQNConfig, + "ddpg": DDPGConfig, + "pg": PolicyGradientConfig +} + +RL_POLICY_MODEL_INDEX = { + "ac": PolicyValueNetForDiscreteActionSpace, + "dqn": QNetForDiscreteActionSpace, + "ddpg": PolicyValueNetForContinuousActionSpace, + "pg": PolicyNetForDiscreteActionSpace +} + + +def get_rl_policy_cls(policy_type): + if isinstance(policy_type, str): + if policy_type not in RL_POLICY_INDEX: + raise KeyError(f"A string policy_type must be one of {list(RL_POLICY_INDEX.keys())}.") + return RL_POLICY_INDEX[policy_type] + + return policy_type + + +def get_rl_policy_config_cls(policy_config_type): + if isinstance(policy_config_type, str): + if policy_config_type not in RL_POLICY_CONFIG_INDEX: + raise KeyError(f"A string policy_config_type must be one of {list(RL_POLICY_CONFIG_INDEX.keys())}.") + return RL_POLICY_CONFIG_INDEX[policy_config_type] + + return policy_config_type + + +def get_rl_policy_model_cls(policy_model_type): + if isinstance(policy_model_type, str): + if policy_model_type not in RL_POLICY_MODEL_INDEX: + raise KeyError(f"A string policy_model_type must be one of {list(RL_POLICY_MODEL_INDEX.keys())}.") + return RL_POLICY_MODEL_INDEX[policy_model_type] + + return policy_model_type diff --git a/maro/rl/distributed/__init__.py b/maro/rl/distributed/__init__.py deleted file mode 100644 index a1513dd4a..000000000 --- a/maro/rl/distributed/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .actor import Actor -from .actor_manager import ActorManager -from .learner import DistLearner - -__all__ = ["Actor", "ActorManager", "DistLearner"] diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py deleted file mode 100644 index f9ce5ae40..000000000 --- a/maro/rl/distributed/learner.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from collections import defaultdict -from os import getcwd -from typing import Union - -from maro.communication import Message, Proxy, SessionType -from maro.rl.multi_agent import MultiAgentPolicy -from maro.rl.scheduling import Scheduler -from maro.utils import Logger - -from .actor_manager import ActorManager -from .message_enums import MsgTag, MsgKey - - -class DistLearner(object): - """Learner class for distributed training. - - Args: - policy (MultiAgentPolicy): Learning agents. - scheduler (Scheduler): A ``Scheduler`` instance for generating exploration parameters. - """ - def __init__( - self, - policy: MultiAgentPolicy, - scheduler: Scheduler, - actor_manager: ActorManager, - agent_update_interval: int = -1, - required_actor_finishes: str = None, - discard_stale_experiences: bool = True, - log_dir: str = getcwd() - ): - super().__init__() - self.policy = policy - self.scheduler = scheduler - self.actor_manager = actor_manager - self.agent_update_interval = agent_update_interval - self.required_actor_finishes = required_actor_finishes - self.discard_stale_experiences = discard_stale_experiences - self._total_learning_time = 0 - self._logger = Logger("LEARNER", dump_folder=log_dir) - - def run(self): - """Main learning loop.""" - t0 = time.time() - for exploration_params in self.scheduler: - num_actor_finishes, segment_index = 0, 0 - while num_actor_finishes < self.required_actor_finishes: - for exp, done in self.actor_manager.collect( - self.scheduler.iter, - segment_index, - self.agent_update_interval, - policy_dict=self.policy.policy_dict, - exploration_params=exploration_params if segment_index == 0 else None, - required_actor_finishes=self.required_actor_finishes, - discard_stale_experiences=self.discard_stale_experiences - ): - tl0 = time.time() - self.policy.store_experiences(exp) - self.policy.update() - num_actor_finishes += done - self._total_learning_time += time.time() - tl0 - self._logger.debug(f"total running time: {time.time() - t0}") - self._logger.debug(f"total learning time: {self._total_learning_time}") - self._logger.debug(f"total env steps: {self.actor_manager.total_env_steps}") - self._logger.info(f"total experiences collected: {self.actor_manager.total_experiences_collected}") - - segment_index += 1 - - self.actor_manager.exit() diff --git a/maro/rl/training/env_wrapper.py b/maro/rl/env_wrapper.py similarity index 67% rename from maro/rl/training/env_wrapper.py rename to maro/rl/env_wrapper.py index ba4c80d84..0b4e51728 100644 --- a/maro/rl/training/env_wrapper.py +++ b/maro/rl/env_wrapper.py @@ -6,6 +6,7 @@ from collections import defaultdict, deque from typing import List, Union +from maro.rl.experience import Replay from maro.simulator import Env @@ -22,7 +23,8 @@ class AbsEnvWrapper(ABC): after executing an action. """ def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): - self.replay = defaultdict(lambda: defaultdict(list)) + self.env = env + self.replay = defaultdict(Replay) self.state_info = None # context for converting model output to actions that can be executed by the env self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay @@ -41,13 +43,10 @@ def step_index(self): def agent_idx_list(self): return self.env.agent_idx_list - def start(self, rollout_index: int = None): + def start(self): self._step_index = 0 _, self._event, _ = self.env.step(None) self._state = self.get_state(self.env.tick) - if self.save_replay: - for agent_id, state in self._state.items(): - self.replay[agent_id]["S"].append(state) @property def metrics(self): @@ -94,7 +93,7 @@ def step(self, action_by_agent: dict): self._step_index += 1 env_action = self.get_action(action_by_agent) self.pending_reward_ticks.append(self.env.tick) - self.action_history[self.env.tick] = env_action + self.action_history[self.env.tick] = action_by_agent if len(env_action) == 1: env_action = list(env_action.values())[0] # t1 = time.time() @@ -102,40 +101,33 @@ def step(self, action_by_agent: dict): # t2 = time.time() # self._tot_raw_step_time += t2 - t1 - if self.save_replay: - for agent_id, action in action_by_agent.items(): - self.replay[agent_id]["A"].append(action) - """ - If roll-out is complete, evaluate rewards for all remaining events except the last. - Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ - while ( - self.pending_reward_ticks and - (done or self.env.tick - self.pending_reward_ticks[0] >= self.reward_eval_delay) - ): - tick = self.pending_reward_ticks.popleft() - reward = self.get_reward(tick=tick) - # assign rewards to the agents that took action at that tick - for agent_id in self.action_history[tick]: - rw = reward.get(agent_id, 0) - self.replay[agent_id]["R"].append(rw) - self._total_reward += rw + """ + If roll-out is complete, evaluate rewards for all remaining events except the last. + Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. + """ + while ( + self.pending_reward_ticks and + (done or self.env.tick - self.pending_reward_ticks[0] >= self.reward_eval_delay) + ): + tick = self.pending_reward_ticks.popleft() + reward = self.get_reward(tick=tick) + # assign rewards to the agents that took action at that tick + for agent_id in self.action_history[tick]: + rw = reward.get(agent_id, 0) + if not done and self.save_replay: + self.replay[agent_id].rewards.append(rw) + self._total_reward += rw if not done: + prev_state = self._state self._state = self.get_state(self.env.tick) if self.save_replay: - for agent_id, state in self._state.items(): - replay = self.replay[agent_id] - if replay["S"]: - replay["S_"].append(state) - replay["S"].append(state) - assert len(replay["S_"]) == len(replay["A"]) == len(replay["S"]) - 1 - + for agent_id, state in prev_state.items(): + self.replay[agent_id].add(state, action_by_agent[agent_id]) # t3 = time.time() # self._tot_step_time += t3 - t0 else: self._state = None - self.post_process() self.end_ep_callback() # print(f"total raw step time: {self._tot_raw_step_time}") @@ -143,16 +135,6 @@ def step(self, action_by_agent: dict): # self._tot_raw_step_time = 0 # self._tot_step_time = 0 - def post_process(self): - num_experiences = 0 - for agent_id, replay in self.replay.items(): - num_complete = min(len(replay["R"]), len(replay["S_"])) - num_experiences += num_complete - for k, vals in replay.items(): - del vals[num_complete:] - - return experience, num_experiences - def end_ep_callback(self): pass @@ -163,4 +145,4 @@ def reset(self): self._state = None self.pending_reward_ticks.clear() self.action_history.clear() - self.replay = defaultdict(lambda: defaultdict(list)) + self.replay = defaultdict(Replay) diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py new file mode 100644 index 000000000..10b26517b --- /dev/null +++ b/maro/rl/experience/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .sampler import AbsSampler, UniformSampler +from .sampler_cls_index import get_sampler_cls +from .experience_memory import ExperienceMemory, ExperienceSet, Replay + +__all__ = ["AbsSampler", "ExperienceMemory", "ExperienceSet", "Replay", "UniformSampler", "get_sampler_cls"] diff --git a/maro/rl/storage/simple_store.py b/maro/rl/experience/experience_memory.py similarity index 64% rename from maro/rl/storage/simple_store.py rename to maro/rl/experience/experience_memory.py index 4e6b58dc9..ce41e5938 100644 --- a/maro/rl/storage/simple_store.py +++ b/maro/rl/experience/experience_memory.py @@ -7,14 +7,54 @@ import numpy as np from maro.utils import clone -from maro.utils.exception.rl_toolkit_exception import StoreMisalignment +from maro.utils.exception.rl_toolkit_exception import InvalidExperience -from .abs_store import AbsStore +class ExperienceSet: -class SimpleStore(AbsStore): - """ - An implementation of ``AbsStore`` for experience storage in RL. + __slots__ = ["states", "actions", "rewards", "next_states"] + + def __init__(self, states: list, actions: list, rewards: list, next_states: list): + if not len(states) == len(actions) == len(rewards) == len(next_states): + raise InvalidExperience("values of contents should consist of lists of the same length") + + self.states = states + self.actions = actions + self.rewards = rewards + self.next_states = next_states + + def __len__(self): + return len(self.states) + + +class Replay(object): + def __init__(self): + self.states = [] + self.actions = [] + self.rewards = [] + + def add(self, state, action): + self.states.append(state) + self.actions.append(action) + + def take(self): + num_complete = min(len(self.rewards), len(self.states) - 1) + exp_set = ExperienceSet( + self.states[:num_complete], + self.actions[:num_complete], + self.rewards[:num_complete], + self.states[1:num_complete + 1] + ) + + del self.states[:num_complete] + del self.actions[:num_complete] + del self.rewards[:num_complete] + + return exp_set + + +class ExperienceMemory(object): + """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". This implementation uses a dictionary of lists as the internal data structure. The objects for each key are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during @@ -22,7 +62,6 @@ class SimpleStore(AbsStore): and limited storage are supported. Args: - keys (list): Keys to identify the stored lists of objects. capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. overwrite_type (str): If storage capacity is bounded, this specifies how existing entries are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: @@ -30,14 +69,15 @@ class SimpleStore(AbsStore): - "random", where overwrite occurs randomly among filled positions. Alternatively, the user may also specify overwrite positions (see ``put``). """ - def __init__(self, keys: list, capacity: int = -1, overwrite_type: str = None): + def __init__(self, capacity: int = -1, overwrite_type: str = None): super().__init__() if overwrite_type not in {"rolling", "random"}: raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") - self.keys = keys self._capacity = capacity self._overwrite_type = overwrite_type - self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self.keys} + self.data = { + key: [] if self._capacity == -1 else [None] * self._capacity for key in ExperienceSet.__slots__ + } self._size = 0 def __len__(self): @@ -60,12 +100,13 @@ def overwrite_type(self): """An string indicating the overwrite behavior when the store capacity is exceeded.""" return self._overwrite_type - def get(self, indexes: [int] = None) -> dict: + def get(self, indexes: [int] = None) -> ExperienceSet: if indexes is None: - return self.data - return {k: [self.data[k][i] for i in indexes] for k in self.data} + return ExperienceSet(*[self.data[k] for k in ExperienceSet.__slots__]) - def put(self, contents: Dict[str, List], overwrite_indexes: list = None) -> List[int]: + return ExperienceSet(*[[self.data[k][i] for i in indexes] for k in ExperienceSet.__slots__]) + + def put(self, experience_set: ExperienceSet, overwrite_indexes: list = None) -> List[int]: """Put new contents in the store. Args: @@ -78,46 +119,26 @@ def put(self, contents: Dict[str, List], overwrite_indexes: list = None) -> List Returns: The indexes where the newly added entries reside in the store. """ - if len(self.data) > 0: - expected_keys, actual_keys = set(self.data.keys()), set(contents.keys()) - if expected_keys != actual_keys: - raise StoreMisalignment(f"expected keys {expected_keys}, got {actual_keys}") - self.validate(contents) - added = contents[next(iter(contents))] - added_size = len(added) if isinstance(added, list) else 1 + added_size = len(experience_set) if self._capacity == -1: - for key, val in contents.items(): - self.data[key].extend(val) + for key in self.data: + self.data[key].extend(getattr(experience_set, key)) self._size += added_size return list(range(self._size - added_size, self._size)) else: write_indexes = self._get_update_indexes(added_size, overwrite_indexes=overwrite_indexes) - self.update(write_indexes, contents) + for key in self.data: + for index, value in zip(write_indexes, getattr(experience_set, key)): + self.data[key][index] = value + self._size = min(self._capacity, self._size + added_size) return write_indexes - def update(self, indexes: list, contents: Dict[str, List]): - """ - Update contents at given positions. - - Args: - indexes (list): Positions where updates are to be made. - contents (dict): Contents to write to the internal store at given positions. It is subject to - uniformity checks to ensure that all values have the same length. - - Returns: - The indexes where store contents are updated. - """ - self.validate(contents) - for key, val in contents.items(): - for index, value in zip(indexes, val): - self.data[key][index] = value - - return indexes - def clear(self): """Empty the store.""" - self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self.keys} + self.data = { + key: [] if self._capacity == -1 else [None] * self._capacity for key in ExperienceSet.__slots__ + } self._size = 0 def dumps(self): @@ -149,13 +170,3 @@ def _get_update_indexes(self, added_size: int, overwrite_indexes=None): write_indexes = list(range(self._size, self._capacity)) + list(random_indexes) return write_indexes - - @staticmethod - def validate(contents: Dict[str, List]): - # Ensure that all values are lists of the same length. - if any(not isinstance(val, list) for val in contents.values()): - raise TypeError("All values must be of type 'list'") - - reference_val = contents[list(contents.keys())[0]] - if any(len(val) != len(reference_val) for val in contents.values()): - raise StoreMisalignment("values of contents should consist of lists of the same length") diff --git a/maro/rl/storage/sampler.py b/maro/rl/experience/sampler.py similarity index 67% rename from maro/rl/storage/sampler.py rename to maro/rl/experience/sampler.py index 19135ee7d..1777f0471 100644 --- a/maro/rl/storage/sampler.py +++ b/maro/rl/experience/sampler.py @@ -2,18 +2,20 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod +from typing import List import numpy as np -from .abs_store import AbsStore +from .experience_memory import ExperienceMemory class AbsSampler(ABC): - def __init__(self, data: AbsStore): + def __init__(self, data: ExperienceMemory, batch_size: int): self.data = data + self.batch_size = batch_size @abstractmethod - def sample(self, size: int): + def sample(self) -> List[int]: raise NotImplementedError @abstractmethod @@ -23,12 +25,7 @@ def update(self): class UniformSampler(AbsSampler): - def __init__(self, data, replace: bool = True): - super().__init__(data) - self.replace = replace - - def sample(self, size: int): - """ + """ Obtain a random sample from the experience pool. Args: @@ -39,9 +36,14 @@ def sample(self, size: int): Returns: Sampled indexes and the corresponding objects, e.g., [1, 2, 3], ['a', 'b', 'c']. - """ - indexes = np.random.choice(len(self.data), size=size, replace=self.replace) - return indexes, self.data.get(indexes=indexes) + """ + def __init__(self, data: ExperienceMemory, batch_size: int, replace: bool = True): + super().__init__(data, batch_size) + self.replace = replace + + def sample(self): + indexes = np.random.choice(len(self.data), size=self.batch_size, replace=self.replace) + return indexes def update(self, indexes, values): pass diff --git a/maro/rl/utils/sampler_cls_index.py b/maro/rl/experience/sampler_cls_index.py similarity index 90% rename from maro/rl/utils/sampler_cls_index.py rename to maro/rl/experience/sampler_cls_index.py index 1910fe951..0154a05fa 100644 --- a/maro/rl/utils/sampler_cls_index.py +++ b/maro/rl/experience/sampler_cls_index.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.storage import UniformSampler +from .sampler import UniformSampler SAMPLER = { "uniform": UniformSampler, diff --git a/maro/rl/exploration/__init__.py b/maro/rl/exploration/__init__.py index e4a94ff21..883461b1d 100644 --- a/maro/rl/exploration/__init__.py +++ b/maro/rl/exploration/__init__.py @@ -1,8 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .abs_explorer import AbsExplorer -from .epsilon_greedy_explorer import EpsilonGreedyExplorer -from .noise_explorer import GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer +from .abs_exploration import AbsExploration, NullExploration +from .epsilon_greedy_exploration import EpsilonGreedyExploration +from .noise_exploration import GaussianNoiseExploration, NoiseExploration, UniformNoiseExploration +from .exploration_scheduler import ( + AbsExplorationScheduler, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler +) -__all__ = ["AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer"] +__all__ = [ + "AbsExploration", "NullExploration", + "EpsilonGreedyExploration", + "GaussianNoiseExploration", "NoiseExploration", "UniformNoiseExploration", + "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler" +] diff --git a/maro/rl/exploration/abs_exploration.py b/maro/rl/exploration/abs_exploration.py new file mode 100644 index 000000000..28f9f4c5c --- /dev/null +++ b/maro/rl/exploration/abs_exploration.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from typing import Callable + + +class AbsExploration(ABC): + """Abstract exploration class for generating exploration rates. + + """ + def __init__(self): + self.scheduler = {} + + def register_schedule( + self, + scheduler_cls, + param_name: str, + last_ep: int, + initial_value=None, + kwargs: dict = None + ): + if kwargs is None: + kwargs = {} + self.scheduler[param_name] = scheduler_cls(self, param_name, last_ep, initial_value=initial_value, **kwargs) + + @abstractmethod + def __call__(self, action): + return NotImplementedError + + @property + def parameters(self): + return {param_name: getattr(self, param_name) for param_name in self.scheduler} + + def step(self): + for scheduler in self.scheduler.values(): + scheduler.step() + + +class NullExploration(AbsExploration): + def __init__(self): + pass + + def __call__(self, action): + return action diff --git a/maro/rl/exploration/abs_explorer.py b/maro/rl/exploration/abs_explorer.py deleted file mode 100644 index f64c62f8d..000000000 --- a/maro/rl/exploration/abs_explorer.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - - -class AbsExplorer(ABC): - """Abstract explorer class for generating exploration rates. - - """ - def __init__(self): - pass - - @abstractmethod - def update(self, exploration_params: dict): - return NotImplementedError - - @abstractmethod - def __call__(self, action): - return NotImplementedError diff --git a/maro/rl/exploration/epsilon_greedy_explorer.py b/maro/rl/exploration/epsilon_greedy_exploration.py similarity index 59% rename from maro/rl/exploration/epsilon_greedy_explorer.py rename to maro/rl/exploration/epsilon_greedy_exploration.py index bdd51825f..b51cec44e 100644 --- a/maro/rl/exploration/epsilon_greedy_explorer.py +++ b/maro/rl/exploration/epsilon_greedy_exploration.py @@ -5,11 +5,11 @@ import numpy as np -from .abs_explorer import AbsExplorer +from .abs_exploration import AbsExploration -class EpsilonGreedyExplorer(AbsExplorer): - """Epsilon greedy explorer for discrete action spaces. +class EpsilonGreedyExploration(AbsExploration): + """Epsilon greedy exploration for discrete action spaces. Args: num_actions (int): Number of all possible actions. @@ -17,17 +17,17 @@ class EpsilonGreedyExplorer(AbsExplorer): def __init__(self, num_actions: int, epsilon: float = .0): super().__init__() self._num_actions = num_actions - self._epsilon = epsilon + self.epsilon = epsilon def __call__(self, action_index: Union[int, np.ndarray]): if isinstance(action_index, np.ndarray): - return [self._get_exploration_action(act) for act in action_index] + return np.array([self._get_exploration_action(act) for act in action_index]) else: return self._get_exploration_action(action_index) - def update(self, *, epsilon: float): - self._epsilon = epsilon + def set_params(self, *, epsilon: float): + self.epsilon = epsilon def _get_exploration_action(self, action_index): assert (action_index < self._num_actions), f"Invalid action: {action_index}" - return action_index if np.random.random() > self._epsilon else np.random.choice(self._num_actions) + return action_index if np.random.random() > self.epsilon else np.random.choice(self._num_actions) diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/exploration_scheduler.py new file mode 100644 index 000000000..f462836e7 --- /dev/null +++ b/maro/rl/exploration/exploration_scheduler.py @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractclassmethod +from typing import List, Tuple + +from maro.rl.exploration.abs_exploration import AbsExploration + + +class AbsExplorationScheduler: + def __init__( + self, + exploration: AbsExploration, + param_name: str, + last_ep: int, + initial_value=None + ): + self.exploration = exploration + self.param_name = param_name + self.last_ep = last_ep + if initial_value is not None: + setattr(self.exploration, self.param_name, initial_value) + + def get_value(self): + return getattr(self.exploration, self.param_name) + + @abstractclassmethod + def step(self): + raise NotImplementedError + + +class LinearExplorationScheduler(AbsExplorationScheduler): + """Static exploration parameter generator based on a linear schedule. + + Args: + max_iter (int): Maximum number of iterations. + parameter_names (List[str]): List of exploration parameter names. + start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. + These values must correspond to ``parameter_names``. + end (Union[float, list, tuple, np.ndarray]): Exploration parameter values rate for the last episode. + These values must correspond to ``parameter_names``. + """ + def __init__( + self, + exploration: AbsExploration, + param_name: str, + last_ep: int, + final_value: float, + initial_value: float = None, + ): + super().__init__(exploration, param_name, last_ep, initial_value=initial_value) + self.final_value = final_value + if self.last_ep > 1: + self.delta = (self.final_value - getattr(self.exploration, self.param_name)) / (self.last_ep - 1) + else: + self.delta = 0 + + def step(self): + if self.get_value() == self.final_value: + return + + setattr(self.exploration, self.param_name, self.get_value() + self.delta) + + +class MultiPhaseLinearExplorationScheduler(AbsExplorationScheduler): + """Exploration parameter generator based on two linear schedules joined together. + + Args: + max_iter (int): Maximum number of iterations. + parameter_names (List[str]): List of exploration parameter names. + split (float): The point where the switch from the first linear schedule to the second occurs. + start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. + These values must correspond to ``parameter_names``. + mid (Union[float, list, tuple, np.ndarray]): Exploration parameter values where the switch from the + first linear schedule to the second occurs. In other words, this is the exploration rate where the first + linear schedule ends and the second begins. These values must correspond to ``parameter_names``. + end (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the last episode. + These values must correspond to ``parameter_names``. + + Returns: + An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. + """ + def __init__( + self, + exploration: AbsExploration, + param_name: str, + last_ep: int, + splits: List[Tuple[int, float]], + final_value: float, + initial_value: float = None + ): + # validate splits + splits.append((1, initial_value)) + splits.append((last_ep, final_value)) + splits.sort() + for (ep, _), (ep2, _) in zip(splits, splits[1:]): + if ep == ep2: + raise ValueError("The zeroth element of split points must be unique") + + super().__init__(exploration, param_name, last_ep, initial_value=initial_value) + self.final_value = final_value + self._splits = splits + self._ep = 1 + self._split_index = 1 + self._delta = (self._splits[1][1] - self.get_value()) / (self._splits[1][0] - 1) + + def step(self): + if self._split_index == len(self._splits): + return + + setattr(self.exploration, self.param_name, self.get_value() + self._delta) + self._ep += 1 + if self._ep == self._splits[self._split_index][0]: + self._split_index += 1 + if self._split_index < len(self._splits): + self._delta = ( + (self._splits[self._split_index][1] - self._splits[self._split_index - 1][1]) / + (self._splits[self._split_index][0] - self._splits[self._split_index - 1][0]) + ) + + +if __name__ == "__main__": + from maro.rl.exploration.epsilon_greedy_exploration import EpsilonGreedyExploration + exploration = EpsilonGreedyExploration(5, epsilon=0.6) + scheduler = MultiPhaseLinearExplorationScheduler(exploration, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0) + for ep in range(1, scheduler.last_ep + 1): + print(f"ep = {ep}, value = {exploration.epsilon}") + scheduler.step() diff --git a/maro/rl/exploration/noise_explorer.py b/maro/rl/exploration/noise_exploration.py similarity index 60% rename from maro/rl/exploration/noise_explorer.py rename to maro/rl/exploration/noise_exploration.py index 999994cb4..560c0c5d2 100644 --- a/maro/rl/exploration/noise_explorer.py +++ b/maro/rl/exploration/noise_exploration.py @@ -6,11 +6,11 @@ import numpy as np -from .abs_explorer import AbsExplorer +from .abs_exploration import AbsExploration -class NoiseExplorer(AbsExplorer): - """Explorer that adds a random noise to a model-generated action.""" +class NoiseExploration(AbsExploration): + """Exploration that adds a random noise to a model-generated action.""" def __init__( self, min_action: Union[float, list, np.ndarray] = None, @@ -19,11 +19,11 @@ def __init__( if isinstance(min_action, (list, np.ndarray)) and isinstance(max_action, (list, np.ndarray)): assert len(min_action) == len(max_action), "min_action and max_action should have the same dimension." super().__init__() - self._min_action = min_action - self._max_action = max_action + self.min_action = min_action + self.max_action = max_action @abstractmethod - def set_parameters(self, **parameters): + def set_params(self, **parameters): raise NotImplementedError @abstractmethod @@ -31,8 +31,8 @@ def __call__(self, action) -> np.ndarray: raise NotImplementedError -class UniformNoiseExplorer(NoiseExplorer): - """Explorer that adds a random noise to a model-generated action sampled from a uniform distribution.""" +class UniformNoiseExploration(NoiseExploration): + """Exploration that adds a random noise to a model-generated action sampled from a uniform distribution.""" def __init__( self, min_action: Union[float, list, np.ndarray] = None, @@ -44,26 +44,22 @@ def __init__( assert len(noise_lower_bound) == len(noise_upper_bound), \ "noise_lower_bound and noise_upper_bound should have the same dimension." super().__init__(min_action, max_action) - self._noise_lower_bound = noise_lower_bound - self._noise_upper_bound = noise_upper_bound - - def set_parameters(self, *, noise_lower_bound, noise_upper_bound): - self._noise_lower_bound = noise_lower_bound - self._noise_upper_bound = noise_upper_bound + self.noise_lower_bound = noise_lower_bound + self.noise_upper_bound = noise_upper_bound def __call__(self, action: np.ndarray) -> np.ndarray: return np.array([self._get_exploration_action(act) for act in action]) def _get_exploration_action(self, action): - action += np.random.uniform(self._noise_lower_bound, self._noise_upper_bound) - if self._min_action is not None or self._max_action is not None: - return np.clip(action, self._min_action, self._max_action) + action += np.random.uniform(self.noise_lower_bound, self.noise_upper_bound) + if self.min_action is not None or self.max_action is not None: + return np.clip(action, self.min_action, self.max_action) else: return action -class GaussianNoiseExplorer(NoiseExplorer): - """Explorer that adds a random noise to a model-generated action sampled from a Gaussian distribution.""" +class GaussianNoiseExploration(NoiseExploration): + """Exploration that adds a random noise to a model-generated action sampled from a Gaussian distribution.""" def __init__( self, min_action: Union[float, list, np.ndarray] = None, @@ -77,21 +73,17 @@ def __init__( if is_relative and noise_mean != .0: raise ValueError("Standard deviation cannot be relative if noise mean is non-zero.") super().__init__(min_action, max_action) - self._noise_mean = noise_mean - self._noise_stddev = noise_stddev - self._is_relative = is_relative - - def set_parameters(self, *, noise_stddev, noise_mean=.0): - self._noise_stddev = noise_stddev - self._noise_mean = noise_mean + self.noise_mean = noise_mean + self.noise_stddev = noise_stddev + self.is_relative = is_relative def __call__(self, action: np.ndarray) -> np.ndarray: return np.array([self._get_exploration_action(act) for act in action]) def _get_exploration_action(self, action): - noise = np.random.normal(loc=self._noise_mean, scale=self._noise_stddev) - action += (noise * action) if self._is_relative else noise - if self._min_action is not None or self._max_action is not None: - return np.clip(action, self._min_action, self._max_action) + noise = np.random.normal(loc=self.noise_mean, scale=self.noise_stddev) + action += (noise * action) if self.is_relative else noise + if self.min_action is not None or self.max_action is not None: + return np.clip(action, self.min_action, self.max_action) else: return action diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index c6815940c..3f75ead50 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -3,7 +3,7 @@ from .abs_block import AbsBlock from .fc_block import FullyConnectedBlock -from .learning_model import ( +from .core_model import ( AbsCoreModel, OptimOption, PolicyNetForDiscreteActionSpace, PolicyValueNetForContinuousActionSpace, PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace ) diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 0c9b01788..81972e476 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -52,7 +52,7 @@ def __init__( optim_option: Union[OptimOption, Dict[str, OptimOption]] = None ): super().__init__() - self._component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) + self.component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) if optim_option is None: self.optimizer = None self.scheduler = None @@ -63,7 +63,7 @@ def __init__( if isinstance(optim_option, dict): self.optimizer = {} for name, opt in optim_option.items(): - self.optimizer[name] = opt.optim_cls(self._component[name].parameters(), **opt.optim_params) + self.optimizer[name] = opt.optim_cls(self.component[name].parameters(), **opt.optim_params) if opt.scheduler_cls: self.scheduler[name] = opt.scheduler_cls(self.optimizer[name], **opt.scheduler_params) else: @@ -141,7 +141,7 @@ class QNetForDiscreteActionSpace(AbsCoreModel): the components. Defaults to None. """ @abstractmethod - def forward(self, states, actions: torch.tensor) -> torch.tensor: + def forward(self, states) -> torch.tensor: raise NotImplementedError def choose_action(self, states): @@ -153,10 +153,11 @@ def choose_action(self, states): greedy_q, actions = q_for_all_actions.max(dim=1) return actions.detach(), greedy_q.detach() - @property - @abstractmethod - def num_actions(self): - raise NotImplementedError + def q_values(self, states, actions: torch.tensor): + if len(actions.shape) == 1: + actions = actions.unsqueeze(dim=1) + q_for_all_actions = self.forward(states) # (batch_size, num_actions) + return q_for_all_actions.gather(1, actions).squeeze(dim=1) class PolicyNetForDiscreteActionSpace(AbsCoreModel): diff --git a/maro/rl/model/torch_cls_index.py b/maro/rl/model/torch_cls_index.py deleted file mode 100644 index 1a1c6e897..000000000 --- a/maro/rl/model/torch_cls_index.py +++ /dev/null @@ -1,57 +0,0 @@ -from torch import optim -from torch.optim import lr_scheduler - -# For details, see https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity -TORCH_ACTIVATION_CLS = { - "elu": nn.ELU, - "hard_shrink": nn.Hardshrink, - "hard_sigmoid": nn.Hardsigmoid, - "hard_tanh": nn.Hardtanh, - "hardswish": nn.Hardswish, - "leaky_relu": nn.LeakyReLU, - "log_sigmoid": nn.LogSigmoid, - "multihead_attention": nn.MultiheadAttention, - "prelu": nn.PReLU, - "relu": nn.ReLU, - "relu6": nn.ReLU6, - "rrelu": nn.RReLU, - "selu": nn.SELU, - "celu": nn.CELU, - "gelu": nn.GELU, - "sigmoid": nn.Sigmoid, - "soft_plus": nn.Softplus, - "soft_shrink": nn.Softshrink, - "soft_sign": nn.Softsign, - "tanh": nn.Tanh, - "tanh_shrink": nn.Tanhshrink, - "threshold": nn.Threshold -} - -# For details, see https://pytorch.org/docs/stable/optim.html -TORCH_OPTIM_CLS = { - "sgd": optim.SGD, - "asgd": optim.ASGD, - "adadelta": optim.Adadelta, - "adagrad": optim.Adagrad, - "adam": optim.Adam, - "adamax": optim.Adamax, - "adamw": optim.AdamW, - "sparse_adam": optim.SparseAdam, - "lbfgs": optim.LBFGS, - "rmsprop": optim.RMSprop, - "rprop": optim.Rprop -} - -# For details, see https://pytorch.org/docs/stable/optim.html -TORCH_LR_SCHEDULER_CLS = { - "lambda": lr_scheduler.LambdaLR, - "multiplicative": lr_scheduler.MultiplicativeLR, - "step": lr_scheduler.StepLR, - "multi_step": lr_scheduler.MultiStepLR, - "exponential": lr_scheduler.ExponentialLR, - "cosine_annealing": lr_scheduler.CosineAnnealingLR, - "reduce_on_plateau": lr_scheduler.ReduceLROnPlateau, - "cyclic": lr_scheduler.CyclicLR, - "one_cycle": lr_scheduler.OneCycleLR, - "cosine_annealing_warm_restarts": lr_scheduler.CosineAnnealingWarmRestarts -} diff --git a/maro/rl/multi_agent/__init__.py b/maro/rl/multi_agent/__init__.py deleted file mode 100644 index bd4d7a299..000000000 --- a/maro/rl/multi_agent/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .multi_agent_policy import MultiAgentPolicyForInference, MultiAgentPolicyForTraining - -__all__ = ["MultiAgentPolicyForInference", "MultiAgentPolicyForTraining"] diff --git a/maro/rl/multi_agent/multi_agent_policy.py b/maro/rl/multi_agent/multi_agent_policy.py deleted file mode 100644 index 387e54b68..000000000 --- a/maro/rl/multi_agent/multi_agent_policy.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import pickle -from typing import Dict, Union - -from maro.rl.algorithm import AbsFixedPolicy, AbsTrainablePolicy -from maro.rl.exploration import AbsExplorer -from maro.rl.storage import SimpleStore - - -class TrainingConfig: - __slots__ = ["sampler_cls", "sampler_params", "train_iters", "min_experiences_to_trigger_learning"] - - def __init__( - self, - sampler_cls, - sampler_params: dict, - train_iters: int, - min_experience_to_trigger_learning: int = 1 - ): - self.sampler_cls = sampler_cls - self.sampler_params = sampler_params - self.train_iters = train_iters - self.min_experiences_to_trigger_learning = min_experiences_to_trigger_learning - - -class MultiAgentPolicyForInference: - """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. - - Args: - - """ - def __init__( - self, - policy_dict: Dict[str, Union[AbsFixedPolicy, AbsTrainablePolicy]], - agent_to_policy: Dict[str, str], - explorer_dict: Dict[str, AbsExplorer] = None, - agent_to_explorer: Dict[str, str] = None - ): - self.policy_dict = policy_dict - self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in agent_to_policy.items()} - - if explorer_dict is not None: - self.explorer = { - agent_id: explorer_dict[explorer_id] for agent_id, explorer_id in agent_to_explorer.items() - } - - def choose_action(self, state_by_agent: dict): - return {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} - - def update_exploration_params(self, param_dict: dict): - # Per-agent exploration parameters - for policy_id, params in param_dict.items(): - self.policy_dict[policy_id].set_exploration_params(params) - - def load(self, policy_dict: dict): - """Load models from memory.""" - self.policy_dict = policy_dict - self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in agent_to_policy.items()} - - def load_from_file(self, path: str): - """Load models from disk.""" - with open(path, "rb") as fp: - pickle.load(fp) - - -class MultiAgentPolicyForTraining: - """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. - - Args: - - """ - def __init__( - self, - policy_dict: Dict[str, Union[AbsFixedPolicy, AbsTrainablePolicy]], - agent_to_policy: Dict[str, str], - experience_memory_dict: Dict[str, SimpleStore], - agent_to_experience_memory: Dict[str, str], - training_config_dict: Dict[str, TrainingConfig], - agent_to_training_config: Dict[str, str] - ): - self.policy_dict = policy_dict - self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in agent_to_policy.items()} - - assert agent_to_experience_memory.keys() == agent_to_training_config.keys() - self.experience_memory = { - agent_id: experience_memory_dict[mem_id] for agent_id, mem_id in agent_to_experience_memory.items() - } - - self.training_config = { - agent_id: training_config_dict[config_id] for agent_id, config_id in agent_to_training_config.items() - } - - self.sampler = { - agent_id: cfg.sampler_cls(self.experience_memory[agent_id], **cfg.sampler_params) - for agent_id, cfg in self.training_config.items() - } - - def store_experiences(self, experiences_by_agent: dict): - """Store experiences in the agents' experience memory. - - The top-level keys of ``experiences`` will be treated as agent IDs. - """ - for agent_id, exp in experiences_by_agent.items(): - self.experience_memory[agent_id].put(exp) - - def update(self): - for agent_id, config in self.training_config.items(): - if len(self.experience_memory[agent_id]) >= config.min_experiences_to_trigger_learning: - for _ in config.train_iters: - self.policy[agent_id].update(self.sampler[agent_id].sample()) - - def save(self, path: dir): - with open(path, "wb") as fp: - pickle.save(self.policy_dict, path) diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py new file mode 100644 index 000000000..dde452fac --- /dev/null +++ b/maro/rl/policy/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .multi_agent_policy import MultiAgentPolicy +from .policy import AbsCorePolicy, AbsFixedPolicy, NullPolicy, RLPolicy, TrainingLoopConfig + +__all__ = ["AbsCorePolicy", "AbsFixedPolicy", "MultiAgentPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig"] diff --git a/maro/rl/policy/multi_agent_policy.py b/maro/rl/policy/multi_agent_policy.py new file mode 100644 index 000000000..fbff05260 --- /dev/null +++ b/maro/rl/policy/multi_agent_policy.py @@ -0,0 +1,100 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import pickle +import warnings +from collections import namedtuple +from typing import Dict, Union + +from maro.rl.exploration import AbsExploration, NullExploration +from maro.rl.experience import ExperienceMemory + +from .policy import AbsFixedPolicy, AbsCorePolicy + + +class MultiAgentPolicy: + """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. + + Args: + + """ + def __init__( + self, + policy_dict: Dict[str, Union[AbsFixedPolicy, AbsCorePolicy]], + agent_to_policy: Dict[str, str], + exploration_dict: Dict[str, AbsExploration] = None, + agent_to_exploration: Dict[str, str] = None + ): + self.policy_dict = policy_dict + self.agent_to_policy = agent_to_policy + self.policy = { + agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items() + } + + self.exploration_dict = exploration_dict + if exploration_dict: + self.agent_to_exploration = agent_to_exploration + self.exploration = { + agent_id: self.exploration_dict[exploration_id] + for agent_id, exploration_id in self.agent_to_exploration.items() + } + self.with_exploration = True + + def train_mode(self): + self.with_exploration = True + + def eval_mode(self): + self.with_exploration = False + + @property + def exploration_params(self): + if hasattr(self, "exploration"): + return {agent_id: exploration.parameters for agent_id, exploration in self.exploration.items()} + + def choose_action(self, state_by_agent: dict): + if self.exploration_dict and self.with_exploration: + return { + agent_id: + self.exploration[agent_id](self.policy[agent_id].choose_action(state)) + if agent_id in self.exploration else self.policy[agent_id].choose_action(state) + for agent_id, state in state_by_agent.items() + } + + return {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} + + def on_new_experiences(self, experiences_by_agent: dict): + for agent_id, exp in experiences_by_agent.items(): + if isinstance(self.policy[agent_id], AbsCorePolicy): + self.policy[agent_id].store_experiences(exp) + + return [ + policy_id for policy_id, policy in self.policy_dict.items() + if isinstance(policy, AbsCorePolicy) and policy.learn() + ] + + def update(self, policy_dict: dict): + """Load policies from memory.""" + if not policy_dict.keys() <= self.policy_dict.keys(): + raise Exception(f"Expected policies from {list(self.policy_dict.keys())}") + + for policy_id, policy in policy_dict.items(): + self.policy_dict[policy_id].update(policy) + + def exploration_step(self): + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + + def load(self, dir_path: str): + """Load models from disk.""" + for policy_id, policy in self.policy_dict.items(): + try: + policy.load(os.path.join(dir_path, policy_id)) + except FileNotFoundError: + warnings.warn(f"policy {policy_id} is skipped because no file is found") + + def save(self, dir_path: str): + os.makedirs(dir_path, exist_ok=True) + for policy_id, policy in self.policy_dict.items(): + policy.save(os.path.join(dir_path, policy_id)) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py new file mode 100644 index 000000000..7c48040c9 --- /dev/null +++ b/maro/rl/policy/policy.py @@ -0,0 +1,162 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from collections import namedtuple + +from maro.rl.exploration import AbsExploration +from maro.rl.experience import ExperienceMemory, ExperienceSet + + +class TrainingLoopConfig: + __slots__ = [ + "sampler_cls", "batch_size", "train_iters", "sampler_kwargs", "new_experience_trigger", + "experience_memory_size_trigger" + ] + + def __init__( + self, + sampler_cls, + batch_size: int, + train_iters: int, + sampler_kwargs: dict = None, + new_experience_trigger: int = 1, + experience_memory_size_trigger: int = 1 + ): + self.sampler_cls = sampler_cls + self.batch_size = batch_size + self.train_iters = train_iters + self.sampler_kwargs = sampler_kwargs if sampler_kwargs else {} + self.new_experience_trigger = new_experience_trigger + self.experience_memory_size_trigger = experience_memory_size_trigger + + +class AbsFixedPolicy(ABC): + """Abstract fixed policy class. + + Args: + config: Settings for the algorithm. + """ + def __init__(self): + pass + + @abstractmethod + def choose_action(self, state): + """Compute an action based on a state object. + + Args: + state: State object. + + Returns: + The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + raise NotImplementedError + + +class NullPolicy(AbsFixedPolicy): + def choose_action(self, state): + return None + + +class AbsCorePolicy(ABC): + def __init__(self, experience_memory: ExperienceMemory, generic_config: TrainingLoopConfig, special_config): + self.experience_memory = experience_memory + self.generic_config = generic_config + self.special_config = special_config + sampler_cls, batch_size = generic_config.sampler_cls, generic_config.batch_size + self.sampler = sampler_cls(experience_memory, batch_size, **generic_config.sampler_kwargs) + self._last_experience_memory_size = 0 + self._ready_to_learn = False + + @abstractmethod + def choose_action(self, state): + """Compute an action based on a state object. + + Args: + state: State object. + + Returns: + The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + raise NotImplementedError + + def store_experiences(self, experience_set: ExperienceSet): + self.experience_memory.put(experience_set) + cur_experience_memory_size = len(self.experience_memory) + print("cur exp mem size: ", cur_experience_memory_size) + num_new = cur_experience_memory_size - self._last_experience_memory_size + self._ready_to_learn = ( + num_new >= self.generic_config.new_experience_trigger and + cur_experience_memory_size >= self.generic_config.experience_memory_size_trigger + ) + + def learn(self): + if not self._ready_to_learn: + return False + + self._last_experience_memory_size = len(self.experience_memory) + exp_mem, sampler, config = self.experience_memory, self.sampler, self.generic_config + for _ in range(self.generic_config.train_iters): + self.step(self.experience_memory.get(self.sampler.sample())) + return True + + return False + + @abstractmethod + def step(self, experience_set: ExperienceSet): + raise NotImplementedError + + def state(self): + pass + + def update(self, policy_state): + pass + + def load(self, path: str): + pass + + def save(self, path: str): + pass + + +class RLPolicy(object): + """Abstract fixed policy class. + + Args: + config: Settings for the algorithm. + """ + def __init__(self, core_policy: AbsCorePolicy, exploration: AbsExploration = None): + self.core_policy = core_policy + self.exploration = exploration + + def choose_action(self, state): + """Compute an action based on a state object. + + Args: + state: State object. + + Returns: + The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + action = self.core_policy.choose_action(state) + return self.exploration(action) + + def learn(self): + """Algorithm-specific training logic. + + The parameters are data to train the underlying model on. Algorithm-specific loss and optimization + should be reflected here. + """ + self.core_policy.learn() + + def update(self, policy_state): + self.core_policy.load(policy_state) + + def load(self, path: str): + self.core_policy.load(path) + + def save(self, path: str): + self.core_policy.save(path) diff --git a/maro/rl/scheduling/__init__.py b/maro/rl/scheduling/__init__.py deleted file mode 100644 index 1b5c46b3b..000000000 --- a/maro/rl/scheduling/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .scheduler import Scheduler -from .simple_parameter_scheduler import LinearParameterScheduler, TwoPhaseLinearParameterScheduler - -__all__ = [ - "Scheduler", - "LinearParameterScheduler", - "TwoPhaseLinearParameterScheduler" -] diff --git a/maro/rl/scheduling/scheduler.py b/maro/rl/scheduling/scheduler.py deleted file mode 100644 index 75d22c702..000000000 --- a/maro/rl/scheduling/scheduler.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -class Scheduler(object): - """Scheduler that generates new parameters each iteration. - - Args: - max_iter (int): Maximum number of iterations. If -1, using the scheduler in a for-loop - will result in an infinite loop unless the ``check_for_stopping`` method is implemented. - """ - - def __init__(self, max_iter: int = -1): - if max_iter <= 0 and max_iter != -1: - raise ValueError("max_iter must be a positive integer or -1.") - self._max_iter = max_iter - self._iter_index = -1 - - def __iter__(self): - return self - - def __next__(self): - self._iter_index += 1 - if self._iter_index == self._max_iter or self.check_for_stopping(): - raise StopIteration - - return self.next_params() - - def next_params(self): - pass - - def check_for_stopping(self) -> bool: - return False - - @property - def iter(self): - return self._iter_index diff --git a/maro/rl/scheduling/simple_parameter_scheduler.py b/maro/rl/scheduling/simple_parameter_scheduler.py deleted file mode 100644 index ffe0c4988..000000000 --- a/maro/rl/scheduling/simple_parameter_scheduler.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import List, Union - -import numpy as np - -from .scheduler import Scheduler - - -class LinearParameterScheduler(Scheduler): - """Static exploration parameter generator based on a linear schedule. - - Args: - max_iter (int): Maximum number of iterations. - parameter_names (List[str]): List of exploration parameter names. - start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. - These values must correspond to ``parameter_names``. - end (Union[float, list, tuple, np.ndarray]): Exploration parameter values rate for the last episode. - These values must correspond to ``parameter_names``. - """ - def __init__( - self, - max_iter: int, - parameter_names: List[str], - start: Union[float, list, tuple, np.ndarray], - end: Union[float, list, tuple, np.ndarray] - ): - super().__init__(max_iter) - self._parameter_names = parameter_names - if isinstance(start, float): - self._current_values = start * np.ones(len(self._parameter_names)) - elif isinstance(start, (list, tuple)): - self._current_values = np.asarray(start) - else: - self._current_values = start - - if isinstance(end, float): - end = end * np.ones(len(self._parameter_names)) - elif isinstance(end, (list, tuple)): - end = np.asarray(end) - - self._delta = (end - self._current_values) / (self._max_iter - 1) if self._max_iter != 1 else 0 - - def next_params(self): - current_values = self._current_values.copy() - self._current_values += self._delta - return dict(zip(self._parameter_names, current_values)) - - -class TwoPhaseLinearParameterScheduler(Scheduler): - """Exploration parameter generator based on two linear schedules joined together. - - Args: - max_iter (int): Maximum number of iterations. - parameter_names (List[str]): List of exploration parameter names. - split (float): The point where the switch from the first linear schedule to the second occurs. - start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. - These values must correspond to ``parameter_names``. - mid (Union[float, list, tuple, np.ndarray]): Exploration parameter values where the switch from the - first linear schedule to the second occurs. In other words, this is the exploration rate where the first - linear schedule ends and the second begins. These values must correspond to ``parameter_names``. - end (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the last episode. - These values must correspond to ``parameter_names``. - - Returns: - An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. - """ - def __init__( - self, - max_iter: int, - parameter_names: List[str], - split: float, - start: Union[float, list, tuple, np.ndarray], - mid: Union[float, list, tuple, np.ndarray], - end: Union[float, list, tuple, np.ndarray] - ): - if split < 0 or split > 1.0: - raise ValueError("split must be a float between 0 and 1.") - super().__init__(max_iter) - self._parameter_names = parameter_names - self._split = int(self._max_iter * split) - if isinstance(start, float): - self._current_values = start * np.ones(len(self._parameter_names)) - elif isinstance(start, (list, tuple)): - self._current_values = np.asarray(start) - else: - self._current_values = start - - if isinstance(mid, float): - mid = mid * np.ones(len(self._parameter_names)) - elif isinstance(mid, (list, tuple)): - mid = np.asarray(mid) - - if isinstance(end, float): - end = end * np.ones(len(self._parameter_names)) - elif isinstance(end, (list, tuple)): - end = np.asarray(end) - - self._delta_1 = (mid - self._current_values) / self._split if self._split else 0 - phase_2_eps = self._max_iter - self._split - 1 - self._delta_2 = (end - mid) / phase_2_eps if phase_2_eps else 0 - - def next_params(self): - current_values = self._current_values.copy() - self._current_values += self._delta_1 if self._iter_index < self._split else self._delta_2 - return dict(zip(self._parameter_names, current_values)) diff --git a/maro/rl/storage/__init__.py b/maro/rl/storage/__init__.py deleted file mode 100644 index 8b8173d2b..000000000 --- a/maro/rl/storage/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .abs_store import AbsStore -from .sampler import AbsSampler, UniformSampler -from .simple_store import SimpleStore - -__all__ = ["AbsSampler", "AbsStore", "SimpleStore", "UniformSampler"] diff --git a/maro/rl/storage/abs_store.py b/maro/rl/storage/abs_store.py deleted file mode 100644 index 35e6869c1..000000000 --- a/maro/rl/storage/abs_store.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from typing import Callable, Sequence - - -class AbsStore(ABC): - """A data store abstraction that supports get, put, update and sample operations.""" - - def __init__(self): - pass - - @abstractmethod - def get(self, indexes: Sequence): - """Get contents. - - Args: - indexes: A sequence of indexes to retrieve contents at. - Returns: - Retrieved contents. - """ - pass - - def put(self, contents: Sequence): - """Put new contents. - - Args: - contents (Sequence): Contents to be added to the store. - Returns: - The indexes where the newly added entries reside in the store. - """ - pass - - @abstractmethod - def update(self, indexes: Sequence, contents: Sequence): - """Update the store contents at given positions. - - Args: - indexes (Sequence): Positions where updates are to be made. - contents (Sequence): Item list, which has the same length as indexes. - Returns: - The indexes where store contents are updated. - """ - pass - - def filter(self, filters: Sequence[Callable]): - """Multi-filter method. - - The input to one filter is the output from the previous filter. - - Args: - filters (Sequence[Callable]): Filter list, each item is a lambda function, - e.g., [lambda d: d['a'] == 1 and d['b'] == 1]. - Returns: - Filtered indexes and corresponding objects. - """ - pass diff --git a/maro/rl/storage/sampler_cls_index.py b/maro/rl/storage/sampler_cls_index.py deleted file mode 100644 index 907381af6..000000000 --- a/maro/rl/storage/sampler_cls_index.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .sampler import UniformSampler - -SAMPLER_CLS = { - "uniform": UniformSampler, -} diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 3ecff3bc9..e369990d8 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .env_wrapper import AbsEnvWrapper +from .actor import Actor +from .actor_manager import ActorManager from .learner import Learner -__all__ = ["AbsEnvWrapper", "Learner"] +__all__ = ["Actor", "ActorManager", "Learner"] diff --git a/maro/rl/distributed/actor.py b/maro/rl/training/actor.py similarity index 65% rename from maro/rl/distributed/actor.py rename to maro/rl/training/actor.py index f775698d3..a5de2186c 100644 --- a/maro/rl/distributed/actor.py +++ b/maro/rl/training/actor.py @@ -5,8 +5,8 @@ from typing import Union from maro.communication import Proxy -from maro.rl.multi_agent import MultiAgentPolicy -from maro.rl.training import AbsEnvWrapper +from maro.rl.policy import MultiAgentPolicy +from maro.rl.env_wrapper import AbsEnvWrapper from maro.utils import Logger from .message_enums import MsgKey, MsgTag @@ -29,13 +29,13 @@ def __init__( env: AbsEnvWrapper, policy: MultiAgentPolicy, group: str, + eval_env: AbsEnvWrapper = None, proxy_options: dict = None, - pull_experiences_with_copy: bool = False, log_dir: str = getcwd() ): self.env = env + self.eval_env = eval_env if eval_env else self.env self.policy = policy - self._pull_experiences_with_copy = pull_experiences_with_copy if proxy_options is None: proxy_options = {} self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) @@ -46,17 +46,17 @@ def run(self): if msg.tag == MsgTag.EXIT: self._logger.info("Exiting...") break - if msg.tag == MsgTag.ROLLOUT: + + if msg.tag == MsgTag.COLLECT: + self.policy.train_mode() rollout_index, segment_index = msg.body[MsgKey.ROLLOUT_INDEX], msg.body[MsgKey.SEGMENT_INDEX] if self.env.state is None: + self._logger.info(f"Training the policy with exploration parameters: {self.policy.exploration_params}") self.env.reset() - # Load exploration parameters - if MsgKey.EXPLORATION_PARAMS in msg.body: - self.policy.update_exploration_params(msg.body[MsgKey.EXPLORATION_PARAMS]) - self.env.start(rollout_index=rollout_index) # get initial state + self.env.start() # get initial state starting_step_index = self.env.step_index - self.policy.load(msg.body[MsgKey.POLICY]) + self.policy.update(msg.body[MsgKey.POLICY]) steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while self.env.state and steps_to_go > 0: action = self.policy.choose_action(self.env.state) @@ -67,7 +67,7 @@ def run(self): f"Roll-out finished for ep {rollout_index}, segment {segment_index}" f"(steps {starting_step_index} - {self.env.step_index})" ) - experiences, num_exp = self.env.pull_experiences(copy=self._pull_experiences_with_copy) + experiences, num_exp = self.env.get_experiences() return_info = { MsgKey.ENV_END: not self.env.state, MsgKey.ROLLOUT_INDEX: rollout_index, @@ -76,8 +76,27 @@ def run(self): MsgKey.NUM_STEPS: self.env.step_index - starting_step_index, MsgKey.NUM_EXPERIENCES: num_exp } + if msg.body[MsgKey.RETURN_ENV_METRICS]: return_info[MsgKey.METRICS] = self.env.metrics if not self.env.state: + self.policy.exploration_step() return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward - self._proxy.reply(msg, tag=MsgTag.ROLLOUT_DONE, body=return_info) + self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) + elif msg.tag == MsgTag.EVAL: + self._logger.info(f"Evaluating the policy") + self.policy.eval_mode() + self.eval_env.reset() + self.eval_env.start() # get initial state + + self.policy.update(msg.body[MsgKey.POLICY]) + while self.eval_env.state: + self.eval_env.step(self.policy.choose_action(self.eval_env.state)) + + self._logger.info( + f"Roll-out finished for ep {rollout_index}, segment {segment_index}" + f"(steps {starting_step_index} - {self.eval_env.step_index})" + ) + + return_info = {MsgKey.METRICS: self.env.metrics, MsgKey.TOTAL_REWARD: self.eval_env.total_reward} + self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/training/actor_manager.py similarity index 66% rename from maro/rl/distributed/actor_manager.py rename to maro/rl/training/actor_manager.py index 47b52ab7c..5b31de08d 100644 --- a/maro/rl/distributed/actor_manager.py +++ b/maro/rl/training/actor_manager.py @@ -3,6 +3,7 @@ from collections import defaultdict from os import getcwd +from random import choices from typing import Union from maro.communication import Proxy, SessionType @@ -34,6 +35,7 @@ def __init__( log_dir: str = getcwd() ): super().__init__() + self.num_actors = num_actors peers = {"actor": num_actors} if proxy_options is None: proxy_options = {} @@ -51,15 +53,12 @@ def collect( segment_index: int, num_steps: int, policy_dict: dict = None, - exploration_params=None, + exploration=None, required_actor_finishes: int = None, discard_stale_experiences: bool = True, return_env_metrics: bool = False ): """Collect experiences from actors.""" - if required_actor_finishes is None: - required_actor_finishes = len(self._actors) - msg_body = { MsgKey.ROLLOUT_INDEX: rollout_index, MsgKey.SEGMENT_INDEX: segment_index, @@ -68,24 +67,21 @@ def collect( MsgKey.RETURN_ENV_METRICS: return_env_metrics } - if exploration_params: - msg_body[MsgKey.EXPLORATION_PARAMS] = exploration_params - if self._log_env_metrics: self._logger.info(f"EPISODE-{rollout_index}, SEGMENT-{segment_index}: ") if exploration_params: self._logger.info(f"exploration_params: {exploration_params}") - self._proxy.ibroadcast("actor", MsgTag.ROLLOUT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent roll-out requests for ep-{rollout_index}, segment-{segment_index}") + self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) + self._logger.info(f"Sent collect requests for ep-{rollout_index}, segment-{segment_index}") # Receive roll-out results from remote actors num_finishes = 0 for msg in self._proxy.receive(): - if msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: + if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: self._logger.info( - f"Ignore a message of type {msg.tag} with ep {msg.body[MsgKey.ROLLOUT_INDEX]} " - f"(expected {rollout_index})" + f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.ROLLOUT_INDEX]} " + f"(expected message type {MsgTag.COLLECT} and roll-out index {rollout_index})" ) continue @@ -107,6 +103,38 @@ def collect( if num_finishes == required_actor_finishes: break + def evaluate(self, rollout_index: int, policy_dict: dict, num_actors: int): + """Collect experiences from actors.""" + msg_body = { + MsgKey.ROLLOUT_INDEX: rollout_index, + MsgKey.POLICY: policy_dict, + MsgKey.RETURN_ENV_METRICS: True + } + + actors = choices(self._actors, k=num_actors) + self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(actor_id, msg_body) for actor_id in actors]) + self._logger.info(f"Sent evaluation requests to {actors}") + + # Receive roll-out results from remote actors + num_finishes = 0 + for msg in self._proxy.receive(): + if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: + self._logger.info( + f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.ROLLOUT_INDEX]} " + f"(expected message type {MsgTag.EVAL} and roll-out index {rollout_index})" + ) + continue + + # log roll-out summary + if self._log_env_metrics: + env_metrics = msg.body[MsgKey.METRICS] + self._logger.info(f"env_metrics: {env_metrics}") + + if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: + num_finishes += 1 + if num_finishes == num_actors: + break + def exit(self): """Tell the remote actors to exit.""" self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) diff --git a/maro/rl/distributed/dispatcher.py b/maro/rl/training/dispatcher.py similarity index 100% rename from maro/rl/distributed/dispatcher.py rename to maro/rl/training/dispatcher.py diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 04e11c08c..60663eb9b 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -3,71 +3,180 @@ import time from collections import defaultdict -from typing import Dict, Union +from os import getcwd +from typing import Callable, Dict, List, Union -from maro.rl.agent import AbsPolicy, MultiAgentWrapper -from maro.rl.scheduling import Scheduler -from maro.utils import InternalLogger +from maro.communication import Message, Proxy, SessionType +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.policy import AbsCorePolicy, MultiAgentPolicy +from maro.utils import Logger -from .env_wrapper import AbsEnvWrapper +from .actor_manager import ActorManager +from .message_enums import MsgTag, MsgKey class Learner(object): """Learner class for distributed training. Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific - processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsPolicy, MultiAgentWrapper]): Agent that interacts with the environment. + policy (MultiAgentPolicy): Learning agents. + """ def __init__( self, + policy: MultiAgentPolicy, env: AbsEnvWrapper, - agent: Union[AbsPolicy, MultiAgentWrapper], - scheduler: Scheduler, - agent_update_interval: int = -1, - log_env_metrics: bool = False + num_episodes: int, + eval_env: AbsEnvWrapper = None, + actor_manager: ActorManager = None, + num_eval_actors: int = 1, + policy_update_interval: int = -1, + eval_points: Union[int, List[int]] = None, + required_actor_finishes: str = None, + discard_stale_experiences: bool = True, + log_env_metrics: bool = True, + log_dir: str = getcwd() ): - super().__init__() - if agent_update_interval == 0: - raise ValueError("agent_update_interval must be a positive integer or None.") + if env is None and actor_manager is None: + raise Exception("env and actor_manager cannot both be None") + + self._logger = Logger("LEARNER", dump_folder=log_dir) + self.policy = policy self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsPolicy) else agent - self.scheduler = scheduler - self.agent_update_interval = agent_update_interval - self.total_env_steps = 0 - self.total_experiences_collected = 0 - self.total_learning_time = 0 + self.num_episodes = num_episodes + self.eval_env = eval_env if eval_env else self.env + self.policy_update_interval = policy_update_interval + + if actor_manager: + self._logger.info( + "Found actor manager. Roll-outs will be performed by remote actors. Local env will not be used." + ) + self.actor_manager = actor_manager + + self.num_eval_actors = num_eval_actors + + # evaluation points + if eval_points is None: + eval_points = [] + elif isinstance(eval_points, int): + num_eval_points = num_episodes // eval_points + eval_points = [eval_points * i for i in range(1, num_eval_points + 1)] + + self.eval_points = eval_points + self.eval_points.sort() + if not self.eval_points or num_episodes != self.eval_points[-1]: + self.eval_points.append(num_episodes) + + self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_points}") + self._eval_point_index = 0 + + # distributed roll-out + self.actor_manager = actor_manager + if self.actor_manager: + if required_actor_finishes and required_actor_finishes > self.actor_manager.num_actors: + raise ValueError("required_actor_finishes cannot exceed the number of available actors") + + if required_actor_finishes is None: + required_actor_finishes = self.actor_manager.num_actors + self._logger.info(f"Required number of actor finishes is set to {required_actor_finishes}") + + self.required_actor_finishes = required_actor_finishes + self.discard_stale_experiences = discard_stale_experiences + self._log_env_metrics = log_env_metrics - self._logger = InternalLogger("LEARNER") + self._total_learning_time = 0 + self._total_env_steps = 0 + self._total_experiences_collected = 0 def run(self): - t0 = time.time() - for exploration_params in self.scheduler: - self.env.reset() - if exploration_params: - self.agent.set_exploration_params(exploration_params) - - self.env.start(rollout_index=self.scheduler.iter) # get initial state - while self.env.state: - action = self.agent.choose_action(self.env.state) - self.env.step(action) - if ( - not self.env.state or - self.agent_update_interval != -1 and self.env.step_index % self.agent_update_interval == 0 - ): - exp, num_exp = self.env.pull_experiences() - tl0 = time.time() - self.agent.learn(exp) - self.total_learning_time += time.time() - tl0 - self.total_env_steps += self.agent_update_interval - self.total_experiences_collected += num_exp - self._logger.debug(f"total running time: {time.time() - t0}") - self._logger.debug(f"total learning time: {self.total_learning_time}") - self._logger.debug(f"total env steps: {self.total_env_steps}") - self._logger.info(f"total experiences collected: {self.total_experiences_collected}") - if not self.env.state: - self._logger.info(f"total reward: {self.env.total_reward}") + for ep in range(1, self.num_episodes + 1): + self.train(ep) + if ep == self.eval_points[self._eval_point_index]: + self._eval_point_index += 1 + self.evaluate(self._eval_point_index) + + if self.actor_manager: + self.actor_manager.exit() + + def train(self, ep: int): + # local mode + if not self.actor_manager: + self._train_local(ep) + else: + self._train_remote(ep) + + def evaluate(self, ep: int): + self._logger.info("Evaluating the policy") + if self.eval_env: + self.policy.eval_mode() + self.eval_env.save_replay = False + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = self.policy.choose_action(self.eval_env.state) + self.eval_env.step(action) + + if not self.eval_env.state: + self._logger.info(f"total reward: {self.eval_env.total_reward}") if self._log_env_metrics: - self._logger.info(f"ep-{self.scheduler.iter}: {self.env.metrics} ({exploration_params})") + self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") + else: + self.actor_manager.evaluate( + ep, {id_: self.policy.policy_dict[id_] for id_ in updated_policy_ids}, self.num_eval_actors + ) + + def _train_local(self, ep: int): + t0 = time.time() + self._logger.info(f"Training the policy with exploration parameters: {self.policy.exploration_params}") + self.policy.train_mode() + self.env.save_replay = True + self.env.reset() + self.env.start() # get initial state + while self.env.state: + action = self.policy.choose_action(self.env.state) + self.env.step(action) + if ( + not self.env.state or + self.policy_update_interval != -1 and self.env.step_index % self.policy_update_interval == 0 + ): + exp_by_agent = {agent_id: replay.take() for agent_id, replay in self.env.replay.items()} + tl0 = time.time() + updated_policy_ids = self.policy.on_new_experiences(exp_by_agent) + print( + {name: len(policy.experience_memory) + for name, policy in self.policy.policy_dict.items() if isinstance(policy, AbsCorePolicy) + }) + self._total_learning_time += time.time() - tl0 + self._total_env_steps += self.policy_update_interval + self._total_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) + self._logger.debug(f"total running time: {time.time() - t0}") + self._logger.debug(f"total learning time: {self._total_learning_time}") + self._logger.debug(f"total env steps: {self._total_env_steps}") + self._logger.info(f"total experiences collected: {self._total_experiences_collected}") + if not self.env.state: + self._logger.info(f"total reward: {self.env.total_reward}") + + self.policy.exploration_step() + if self._log_env_metrics: + self._logger.info(f"ep {ep}: {self.env.metrics}") + + def _train_remote(self, ep: int): + updated_policy_ids, num_actor_finishes, segment_index = list(self.policy.policy_dict.keys()), 0, 0 + while num_actor_finishes < self.required_actor_finishes: + for exp, done in self.actor_manager.collect( + ep, segment_index, self.policy_update_interval, + policy_dict={id_: self.policy.policy_dict[id_] for id_ in updated_policy_ids}, + required_actor_finishes=self.required_actor_finishes, + discard_stale_experiences=self.discard_stale_experiences + ): + tl0 = time.time() + updated_policy_ids = self.policy.on_new_experiences(exp) + num_actor_finishes += done + self._total_learning_time += time.time() - tl0 + self._logger.debug(f"running time: {time.time() - t0}") + self._logger.debug(f"learning time: {self._total_learning_time}") + self._logger.debug(f"env steps: {self.actor_manager.total_env_steps}") + self._logger.info(f"experiences collected: {self.actor_manager.total_experiences_collected}") + + segment_index += 1 diff --git a/maro/rl/distributed/message_enums.py b/maro/rl/training/message_enums.py similarity index 84% rename from maro/rl/distributed/message_enums.py rename to maro/rl/training/message_enums.py index efab023aa..eb35de7d3 100644 --- a/maro/rl/distributed/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -2,14 +2,16 @@ class MsgTag(Enum): - ROLLOUT = "rollout" + COLLECT = "rollout" + EVAL = "eval" AGENT_UPDATE = "agent_update" CHOOSE_ACTION = "choose_action" ACTION = "action" EXPERIENCE_SYNC = "experience_sync" TRAIN = "train" ABORT_ROLLOUT = "abort_rollout" - ROLLOUT_DONE = "rollout_done" + EVAL_DONE = "eval_done" + COLLECT_DONE = "collect_done" EXIT = "exit" @@ -22,10 +24,8 @@ class MsgKey(Enum): EXPERIENCES = "experiences" NUM_EXPERIENCES = "num_experiences" STATE = "state" - TRAINING = "training" POLICY = "policy" VERSION = "version" - EXPLORATION_PARAMS = "exploration_params" NUM_STEPS = "num_steps" SEGMENT_INDEX = "segment_index" RETURN_ENV_METRICS = "return_env_metrics" diff --git a/maro/rl/training/test.py b/maro/rl/training/test.py deleted file mode 100644 index 7335f369b..000000000 --- a/maro/rl/training/test.py +++ /dev/null @@ -1,17 +0,0 @@ -import random -import time -from functools import reduce - -pool = range(100) -n = 100000 - -l = [random.choices(pool, k=10) for _ in range(n)] - -t0 = time.time() -for vals in l: - reduce(lambda x, y: x + y, vals) -t1 = time.time() -[reduce(lambda x, y: x + y, vals) for vals in l] -t2 = time.time() - -print(t1 - t0, t2 - t1) diff --git a/maro/rl/distributed/trainer.py b/maro/rl/training/trainer.py similarity index 100% rename from maro/rl/distributed/trainer.py rename to maro/rl/training/trainer.py diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 69a92edb2..4797c39c9 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,16 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .sampler_cls_index import get_sampler_cls from .torch_cls_index import ( get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls ) from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_truncated_cumulative_reward -from .value_utils import get_log_prob, get_max, get_td_errors, select_by_actions __all__ = [ - "get_sampler_cls", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", - "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", "get_log_prob", "get_max", - "get_td_errors", "select_by_actions" + "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/utils/torch_cls_index.py b/maro/rl/utils/torch_cls_index.py index 428b3879d..5d1ed29b8 100644 --- a/maro/rl/utils/torch_cls_index.py +++ b/maro/rl/utils/torch_cls_index.py @@ -85,7 +85,7 @@ def get_torch_activation_cls(activation_type): if isinstance(activation_type, str): if activation_type not in TORCH_ACTIVATION: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_ACTIVATION.keys())}.") + raise KeyError(f"A string activation_type must be one of {list(TORCH_ACTIVATION.keys())}.") return TORCH_ACTIVATION[activation_type] return activation_type @@ -94,7 +94,7 @@ def get_torch_activation_cls(activation_type): def get_torch_loss_cls(loss_type): if isinstance(loss_type, str): if loss_type not in TORCH_LOSS: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_LOSS.keys())}.") + raise KeyError(f"A string loss_type must be one of {list(TORCH_LOSS.keys())}.") return TORCH_LOSS[loss_type] return loss_type @@ -103,7 +103,7 @@ def get_torch_loss_cls(loss_type): def get_torch_optim_cls(optim_type): if isinstance(optim_type, str): if optim_type not in TORCH_OPTIM: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_OPTIM.keys())}.") + raise KeyError(f"A string optim_type must be one of {list(TORCH_OPTIM.keys())}.") return TORCH_OPTIM[optim_type] return optim_type @@ -112,7 +112,7 @@ def get_torch_optim_cls(optim_type): def get_torch_lr_scheduler_cls(lr_scheduler_type): if isinstance(lr_scheduler_type, str): if lr_scheduler_type not in TORCH_LR_SCHEDULER: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_LR_SCHEDULER.keys())}.") + raise KeyError(f"A string lr_scheduler_type must be one of {list(TORCH_LR_SCHEDULER.keys())}.") return TORCH_LR_SCHEDULER[lr_scheduler_type] return lr_scheduler_type diff --git a/maro/utils/exception/error_code.py b/maro/utils/exception/error_code.py index 4e8a5ac99..d11cabbae 100644 --- a/maro/utils/exception/error_code.py +++ b/maro/utils/exception/error_code.py @@ -47,6 +47,6 @@ 3003: "Deployment Error", # 4000-4999: Error codes for RL toolkit - 4000: "Store Misalignment", + 4000: "InvalidExperience", 4001: "Missing Optimizer", } diff --git a/maro/utils/exception/rl_toolkit_exception.py b/maro/utils/exception/rl_toolkit_exception.py index d97c72f62..271b2b92e 100644 --- a/maro/utils/exception/rl_toolkit_exception.py +++ b/maro/utils/exception/rl_toolkit_exception.py @@ -4,9 +4,11 @@ from .base_exception import MAROException -class StoreMisalignment(MAROException): - """Raised when a ``put`` operation on a ``SimpleStore`` would cause the underlying lists to have different - sizes.""" +class InvalidExperience(MAROException): + """ + Raised when the states, actions, rewards and next states passed to an ``ExperienceSet`` do not + have the same length. + """ def __init__(self, msg: str = None): super().__init__(4000, msg) diff --git a/tests/test_store.py b/tests/test_store.py index 8d848dbe6..28c640807 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -3,12 +3,12 @@ import unittest -from maro.rl import SimpleStore +from maro.rl import ExperienceMemory class TestUnboundedStore(unittest.TestCase): def setUp(self) -> None: - self.store = SimpleStore(["a", "b", "c"]) + self.store = ExperienceMemory(["a", "b", "c"]) def tearDown(self) -> None: self.store.clear() @@ -44,7 +44,7 @@ def test_filter(self): class TestFixedSizeStore(unittest.TestCase): def test_put_with_rolling_overwrite(self): - store = SimpleStore(["a", "b", "c"], capacity=5, overwrite_type="rolling") + store = ExperienceMemory(["a", "b", "c"], capacity=5, overwrite_type="rolling") indexes = store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) expected = [0, 1, 2] self.assertEqual(indexes, expected, msg=f"expected indexes = {expected}, got {indexes}") @@ -56,7 +56,7 @@ def test_put_with_rolling_overwrite(self): self.assertEqual(actual, expected, msg=f"expected store content = {expected}, got {actual}") def test_put_with_random_overwrite(self): - store = SimpleStore(["a", "b", "c"], capacity=5, overwrite_type="random") + store = ExperienceMemory(["a", "b", "c"], capacity=5, overwrite_type="random") indexes = store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) indexes_2 = store.put({"a": [10, 11, 12, 13], "b": [14, 15, 16, 17], "c": [18, 19, 20, 21]}) for i in indexes_2[2:]: From 208ecd936d990c2c668f70132191e3b96945c193 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 30 Apr 2021 11:21:33 +0000 Subject: [PATCH 207/482] more refinement and some bug fixes --- examples/supply_chain/config.py | 2 +- examples/supply_chain/config.yml | 7 +-- examples/supply_chain/distributed_launcher.py | 19 +++++--- examples/supply_chain/learner.py | 9 ++++ examples/supply_chain/policies.py | 19 ++------ maro/communication/proxy.py | 5 +- maro/rl/algorithm/ac.py | 4 +- maro/rl/algorithm/ddpg.py | 11 ++++- maro/rl/algorithm/dqn.py | 4 +- maro/rl/env_wrapper.py | 10 ++-- maro/rl/experience/experience_memory.py | 16 +++---- maro/rl/policy/multi_agent_policy.py | 48 ++++++++++++++----- maro/rl/policy/policy.py | 46 +++++++++--------- maro/rl/training/actor.py | 35 +++++++------- maro/rl/training/actor_manager.py | 36 +++++++------- maro/rl/training/learner.py | 40 +++++++++------- maro/rl/training/message_enums.py | 4 +- 17 files changed, 178 insertions(+), 137 deletions(-) create mode 100644 examples/supply_chain/learner.py diff --git a/examples/supply_chain/config.py b/examples/supply_chain/config.py index 351930f41..bdfd1002a 100644 --- a/examples/supply_chain/config.py +++ b/examples/supply_chain/config.py @@ -23,4 +23,4 @@ consumer_agent_ids = [f"consumer.{info.id}" for info in env.agent_idx_list] producer_agent_ids = [f"producer.{info.id}" for info in env.agent_idx_list] config["agent_ids"] = consumer_agent_ids + producer_agent_ids -config["policy"]["consumer"]["model"]["input_dim"] = env.dim \ No newline at end of file +config["policy"]["consumer"]["model"]["input_dim"] = env.dim diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 7b7b07969..87905c20c 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -11,9 +11,10 @@ num_episodes: 10 # number of episodes to simulate # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps # before returning experiences to the learner. The learner uses these experiences to update the agents' policies # and sync the updated policies to the actors for the next learning cycle. -policy_update_interval: 60 +policy_update_interval: -1 eval_points: 2 log_env_metrics: false +end_of_training_func_kwargs: policy: consumer: algorithm: dqn @@ -45,8 +46,8 @@ policy: replace: true train_iters: 10 new_experience_trigger: 128 - # The minimum number of experiences in the experience memory required to trigger learning (agent.step()). Defaults to 1. - experience_memory_size_trigger: 1000 + # The minimum number of experiences in the experience memory required for learning. Defaults to 1. + num_warmup_experiences: 1000 experience_memory: capacity: 50000 # experience memory size # This determines how existing experiences are replaced when adding new experiences to a full experience diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index b6bdf8ec8..a042f9a7c 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -17,6 +17,7 @@ from config import config from env_wrapper import SCEnvWrapper from exploration import exploration_dict, agent_to_exploration +from learner import SCLearner from policies import policy_dict, agent_to_policy @@ -46,18 +47,20 @@ def sc_learner(): ) # create a learner to start training - learner = Learner( + learner = SCLearner( policy, None, config["num_episodes"], actor_manager=actor_manager, policy_update_interval=config["policy_update_interval"], eval_points=config["eval_points"], required_actor_finishes=config["distributed"]["required_actor_finishes"], - log_env_metrics=config["log_env_metrics"] + log_env_metrics=config["log_env_metrics"], + end_of_training_kwargs=config["end_of_training_kwargs"], + log_dir=log_dir ) learner.run() -def sc_actor(): +def sc_actor(id_): # create an env wrapper for roll-out and obtain the input dimension for the agents env = SCEnvWrapper(Env(**config["env"])) policy = MultiAgentPolicy( @@ -67,7 +70,11 @@ def sc_actor(): agent_to_exploration=agent_to_exploration ) # create an actor that collects simulation data for the learner. - actor = Actor(env, policy, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, log_dir=log_dir) + actor = Actor( + env, policy, GROUP, + proxy_options={"component_name": id_, "redis_address": (REDIS_HOST, REDIS_PORT)}, + log_dir=log_dir + ) actor.run() @@ -80,7 +87,7 @@ def sc_actor(): args = parser.parse_args() if args.whoami == 0: - actor_processes = [Process(target=sc_actor) for i in range(NUM_ACTORS)] + actor_processes = [Process(target=sc_actor, args=(i + 1,)) for i in range(NUM_ACTORS)] learner_process = Process(target=sc_learner) for i, actor_process in enumerate(actor_processes): @@ -96,4 +103,4 @@ def sc_actor(): elif args.whoami == 1: sc_learner() elif args.whoami == 2: - sc_actor() + sc_actor(getenv("COMPONENT")) diff --git a/examples/supply_chain/learner.py b/examples/supply_chain/learner.py new file mode 100644 index 000000000..b76775d7c --- /dev/null +++ b/examples/supply_chain/learner.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.rl import Learner + + +class SCLearner(Learner): + def end_of_training(self, ep, segment, **kwargs): + pass diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py index afd3ddefe..ce9110e70 100644 --- a/examples/supply_chain/policies.py +++ b/examples/supply_chain/policies.py @@ -31,22 +31,11 @@ def forward(self, states): def get_dqn_policy(config): q_net = SimpleQNet(FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"])) + experience_memory = ExperienceMemory(**config["experience_memory"]) - exp_config = config["experience_memory"] - experience_memory = ExperienceMemory(capacity=exp_config["capacity"], overwrite_type=exp_config["overwrite_type"]) - - train_config = config["training_loop"] - generic_config = TrainingLoopConfig( - get_sampler_cls(train_config["sampler_cls"]), - train_config["batch_size"], - train_config["train_iters"], - sampler_kwargs=train_config["sampler_kwargs"], - new_experience_trigger=train_config["new_experience_trigger"], - experience_memory_size_trigger=train_config["experience_memory_size_trigger"] - ) - - alg_config = config["algorithm_config"] - special_config = DQNConfig(**alg_config) + config["training_loop"]["sampler_cls"] = get_sampler_cls(config["training_loop"]["sampler_cls"]) + generic_config = TrainingLoopConfig(**config["training_loop"]) + special_config = DQNConfig(**config["algorithm_config"]) return DQN(q_net, experience_memory, generic_config, special_config) diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 8f8d59f90..84862abcb 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -73,6 +73,7 @@ def __init__( group_name: str, component_type: str, expected_peers: dict, + component_name: str = None, driver_type: DriverType = DriverType.ZMQ, driver_parameters: dict = None, redis_address: Tuple = (HOST, PORT), @@ -91,8 +92,8 @@ def __init__( self._group_name = group_name self._component_type = component_type self._redis_hash_name = f"{self._group_name}:{self._component_type}" - if "COMPONENT_NAME" in os.environ: - self._name = os.getenv("COMPONENT_NAME") + if component_name is not None: + self._name = component_name else: unique_id = str(uuid.uuid1()).replace("-", "") self._name = f"{self._component_type}_{unique_id}" diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index f62d3dedc..a46191cfd 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -80,7 +80,7 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def step(self, experience_set: ExperienceSet): + def learn(self, experience_set: ExperienceSet): if not isinstance(experience_set, ExperienceSet): raise TypeError(f"Expected experience object of type ACExperience, got {type(experience_set)}") @@ -109,7 +109,7 @@ def step(self, experience_set: ExperienceSet): self.ac_net.step(loss) - def update(self, policy_state): + def load_state(self, policy_state): self.ac_net.load_state_dict(policy_state) def state(self): diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index dde61e068..cfa2276c9 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -70,7 +70,7 @@ def __init__( super().__init__(experience_memory, generic_config, special_config) self.ac_net = ac_net - self.target_ac_net = ac_net.copy() if model.trainable else None + self.target_ac_net = ac_net.copy() if self.ac_net.trainable else None self._train_cnt = 0 def choose_action(self, states) -> Union[float, np.ndarray]: @@ -79,7 +79,7 @@ def choose_action(self, states) -> Union[float, np.ndarray]: return actions[0] if len(actions) == 1 else actions - def step(self, experience_set: ExperienceSet): + def learn(self, experience_set: ExperienceSet): if not isinstance(experience_set, ExperienceSet): raise TypeError(f"Expected experience object of type DDPGExperience, got {type(experience_set)}") @@ -103,3 +103,10 @@ def step(self, experience_set: ExperienceSet): self._train_cnt += 1 if self._train_cnt % self.special_config.target_update_freq == 0: self.target_ac_net.soft_update(self.ac_net, self.special_config.soft_update_coefficient) + + def load_state(self, policy_state): + self.ac_net.load_state_dict(policy_state) + self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None + + def state(self): + return self.ac_net.state_dict() diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index c516dc152..3cf0a67c6 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -81,7 +81,7 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = actions.cpu().numpy() return actions[0] if len(actions) == 1 else actions - def step(self, experience_set: ExperienceSet): + def learn(self, experience_set: ExperienceSet): if not isinstance(experience_set, ExperienceSet): raise TypeError( f"Expected experience object of type AbsCorePolicy.experience_type, got {type(experience_set)}" @@ -110,7 +110,7 @@ def step(self, experience_set: ExperienceSet): if self._training_counter % self.special_config.target_update_freq == 0: self.target_q_net.soft_update(self.q_net, self.special_config.soft_update_coefficient) - def update(self, policy_state): + def load_state(self, policy_state): self.q_net.load_state_dict(policy_state) self.target_q_net = self.q_net.copy() if self.q_net.trainable else None self.target_q_net.eval() diff --git a/maro/rl/env_wrapper.py b/maro/rl/env_wrapper.py index 0b4e51728..9ce855844 100644 --- a/maro/rl/env_wrapper.py +++ b/maro/rl/env_wrapper.py @@ -123,21 +123,25 @@ def step(self, action_by_agent: dict): self._state = self.get_state(self.env.tick) if self.save_replay: for agent_id, state in prev_state.items(): - self.replay[agent_id].add(state, action_by_agent[agent_id]) + self.replay[agent_id].states.append(state) + self.replay[agent_id].actions.append(action_by_agent[agent_id]) # t3 = time.time() # self._tot_step_time += t3 - t0 else: self._state = None - self.end_ep_callback() + self.end_of_episode() # print(f"total raw step time: {self._tot_raw_step_time}") # print(f"total step time: {self._tot_step_time}") # self._tot_raw_step_time = 0 # self._tot_step_time = 0 - def end_ep_callback(self): + def end_of_episode(self): pass + def get_experiences(self): + return {agent_id: replay.to_experience_set() for agent_id, replay in self.replay.items()} + def reset(self): self.env.reset() self.state_info = None diff --git a/maro/rl/experience/experience_memory.py b/maro/rl/experience/experience_memory.py index ce41e5938..d82aa77c8 100644 --- a/maro/rl/experience/experience_memory.py +++ b/maro/rl/experience/experience_memory.py @@ -33,11 +33,8 @@ def __init__(self): self.actions = [] self.rewards = [] - def add(self, state, action): - self.states.append(state) - self.actions.append(action) - - def take(self): + def to_experience_set(self): + # print(len(self.rewards), len(self.states)) num_complete = min(len(self.rewards), len(self.states) - 1) exp_set = ExperienceSet( self.states[:num_complete], @@ -75,9 +72,8 @@ def __init__(self, capacity: int = -1, overwrite_type: str = None): raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") self._capacity = capacity self._overwrite_type = overwrite_type - self.data = { - key: [] if self._capacity == -1 else [None] * self._capacity for key in ExperienceSet.__slots__ - } + self._keys = ExperienceSet.__slots__ + self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self._keys} self._size = 0 def __len__(self): @@ -102,9 +98,9 @@ def overwrite_type(self): def get(self, indexes: [int] = None) -> ExperienceSet: if indexes is None: - return ExperienceSet(*[self.data[k] for k in ExperienceSet.__slots__]) + return ExperienceSet(*[self.data[k] for k in self._keys]) - return ExperienceSet(*[[self.data[k][i] for i in indexes] for k in ExperienceSet.__slots__]) + return ExperienceSet(*[[self.data[k][i] for i in indexes] for k in self._keys]) def put(self, experience_set: ExperienceSet, overwrite_indexes: list = None) -> List[int]: """Put new contents in the store. diff --git a/maro/rl/policy/multi_agent_policy.py b/maro/rl/policy/multi_agent_policy.py index fbff05260..f69367d5c 100644 --- a/maro/rl/policy/multi_agent_policy.py +++ b/maro/rl/policy/multi_agent_policy.py @@ -4,8 +4,8 @@ import os import pickle import warnings -from collections import namedtuple -from typing import Dict, Union +from collections import defaultdict, namedtuple +from typing import Dict, List, Union from maro.rl.exploration import AbsExploration, NullExploration from maro.rl.experience import ExperienceMemory @@ -31,6 +31,12 @@ def __init__( self.policy = { agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items() } + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent_to_policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + for policy_id, agent_ids in self.agent_groups_by_policy.items(): + self.agent_groups_by_policy[policy_id] = tuple(agent_ids) self.exploration_dict = exploration_dict if exploration_dict: @@ -40,6 +46,12 @@ def __init__( for agent_id, exploration_id in self.agent_to_exploration.items() } self.with_exploration = True + self.agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in agent_to_exploration.items(): + self.agent_groups_by_exploration[exploration_id].append(agent_id) + + for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): + self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) def train_mode(self): self.with_exploration = True @@ -50,7 +62,10 @@ def eval_mode(self): @property def exploration_params(self): if hasattr(self, "exploration"): - return {agent_id: exploration.parameters for agent_id, exploration in self.exploration.items()} + return { + agent_ids: self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self.agent_groups_by_exploration.items() + } def choose_action(self, state_by_agent: dict): if self.exploration_dict and self.with_exploration: @@ -63,29 +78,36 @@ def choose_action(self, state_by_agent: dict): return {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} - def on_new_experiences(self, experiences_by_agent: dict): + def store_experiences(self, experiences_by_agent: dict): for agent_id, exp in experiences_by_agent.items(): if isinstance(self.policy[agent_id], AbsCorePolicy): self.policy[agent_id].store_experiences(exp) + def update(self) -> List[str]: return [ policy_id for policy_id, policy in self.policy_dict.items() - if isinstance(policy, AbsCorePolicy) and policy.learn() + if isinstance(policy, AbsCorePolicy) and policy.update() ] - def update(self, policy_dict: dict): - """Load policies from memory.""" - if not policy_dict.keys() <= self.policy_dict.keys(): - raise Exception(f"Expected policies from {list(self.policy_dict.keys())}") - - for policy_id, policy in policy_dict.items(): - self.policy_dict[policy_id].update(policy) - def exploration_step(self): if self.exploration_dict: for exploration in self.exploration_dict.values(): exploration.step() + def load_state(self, policy_state_dict: dict): + """Load policies from memory.""" + if not policy_state_dict.keys() <= self.policy_dict.keys(): + raise Exception(f"Expected policies from {list(self.policy_state_dict.keys())}") + + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].load_state(policy_state) + + def state(self): + return { + policy_id: policy.state() for policy_id, policy in self.policy_dict.items() + if isinstance(policy, AbsCorePolicy) + } + def load(self, dir_path: str): """Load models from disk.""" for policy_id, policy in self.policy_dict.items(): diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 7c48040c9..e82f19e59 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -11,7 +11,7 @@ class TrainingLoopConfig: __slots__ = [ "sampler_cls", "batch_size", "train_iters", "sampler_kwargs", "new_experience_trigger", - "experience_memory_size_trigger" + "num_warmup_experiences" ] def __init__( @@ -21,14 +21,14 @@ def __init__( train_iters: int, sampler_kwargs: dict = None, new_experience_trigger: int = 1, - experience_memory_size_trigger: int = 1 + num_warmup_experiences: int = 1 ): self.sampler_cls = sampler_cls self.batch_size = batch_size self.train_iters = train_iters self.sampler_kwargs = sampler_kwargs if sampler_kwargs else {} self.new_experience_trigger = new_experience_trigger - self.experience_memory_size_trigger = experience_memory_size_trigger + self.num_warmup_experiences = num_warmup_experiences class AbsFixedPolicy(ABC): @@ -66,8 +66,9 @@ def __init__(self, experience_memory: ExperienceMemory, generic_config: Training self.special_config = special_config sampler_cls, batch_size = generic_config.sampler_cls, generic_config.batch_size self.sampler = sampler_cls(experience_memory, batch_size, **generic_config.sampler_kwargs) - self._last_experience_memory_size = 0 - self._ready_to_learn = False + self._last_exp_mem_size = 0 # experience memory size when the last update was made + self._warm_up = True + self._update_ready = False @abstractmethod def choose_action(self, state): @@ -84,34 +85,30 @@ def choose_action(self, state): def store_experiences(self, experience_set: ExperienceSet): self.experience_memory.put(experience_set) - cur_experience_memory_size = len(self.experience_memory) - print("cur exp mem size: ", cur_experience_memory_size) - num_new = cur_experience_memory_size - self._last_experience_memory_size - self._ready_to_learn = ( - num_new >= self.generic_config.new_experience_trigger and - cur_experience_memory_size >= self.generic_config.experience_memory_size_trigger - ) - - def learn(self): - if not self._ready_to_learn: + exp_mem_size = len(self.experience_memory) + self._warm_up = exp_mem_size < self.generic_config.num_warmup_experiences + self._update_ready = (exp_mem_size - self._last_exp_mem_size) >= self.generic_config.new_experience_trigger + + def update(self): + if self._warm_up or not self._update_ready: return False - self._last_experience_memory_size = len(self.experience_memory) + self._last_exp_mem_size = len(self.experience_memory) exp_mem, sampler, config = self.experience_memory, self.sampler, self.generic_config for _ in range(self.generic_config.train_iters): - self.step(self.experience_memory.get(self.sampler.sample())) + self.learn(self.experience_memory.get(self.sampler.sample())) return True return False @abstractmethod - def step(self, experience_set: ExperienceSet): + def learn(self, experience_set: ExperienceSet): raise NotImplementedError def state(self): pass - def update(self, policy_state): + def load_state(self, policy_state): pass def load(self, path: str): @@ -144,16 +141,19 @@ def choose_action(self, state): action = self.core_policy.choose_action(state) return self.exploration(action) - def learn(self): + def update(self): """Algorithm-specific training logic. The parameters are data to train the underlying model on. Algorithm-specific loss and optimization should be reflected here. """ - self.core_policy.learn() + self.core_policy.update() + + def learn(self, experience_set: ExperienceSet): + return self.core_policy.learn(experience_set) - def update(self, policy_state): - self.core_policy.load(policy_state) + def load_state(self, policy_state): + self.core_policy.load_state(policy_state) def load(self, path: str): self.core_policy.load(path) diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index a5de2186c..e0975669f 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -49,14 +49,14 @@ def run(self): if msg.tag == MsgTag.COLLECT: self.policy.train_mode() - rollout_index, segment_index = msg.body[MsgKey.ROLLOUT_INDEX], msg.body[MsgKey.SEGMENT_INDEX] + episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] if self.env.state is None: - self._logger.info(f"Training the policy with exploration parameters: {self.policy.exploration_params}") + self._logger.info(f"Training with exploration parameters: {self.policy.exploration_params}") self.env.reset() self.env.start() # get initial state - starting_step_index = self.env.step_index - self.policy.update(msg.body[MsgKey.POLICY]) + starting_step_index = self.env.step_index + 1 + self.policy.load_state(msg.body[MsgKey.POLICY]) steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while self.env.state and steps_to_go > 0: action = self.policy.choose_action(self.env.state) @@ -64,17 +64,15 @@ def run(self): steps_to_go -= 1 self._logger.info( - f"Roll-out finished for ep {rollout_index}, segment {segment_index}" + f"Roll-out finished for ep {episode_index}, segment {segment_index}" f"(steps {starting_step_index} - {self.env.step_index})" ) - experiences, num_exp = self.env.get_experiences() return_info = { MsgKey.ENV_END: not self.env.state, - MsgKey.ROLLOUT_INDEX: rollout_index, + MsgKey.EPISODE_INDEX: episode_index, MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.EXPERIENCES: experiences, - MsgKey.NUM_STEPS: self.env.step_index - starting_step_index, - MsgKey.NUM_EXPERIENCES: num_exp + MsgKey.EXPERIENCES: self.env.get_experiences(), + MsgKey.NUM_STEPS: self.env.step_index - starting_step_index + 1 } if msg.body[MsgKey.RETURN_ENV_METRICS]: @@ -84,19 +82,18 @@ def run(self): return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) elif msg.tag == MsgTag.EVAL: - self._logger.info(f"Evaluating the policy") + ep = msg.body[MsgKey.EPISODE_INDEX] + self._logger.info(f"Evaluation episode {ep}") self.policy.eval_mode() self.eval_env.reset() self.eval_env.start() # get initial state - - self.policy.update(msg.body[MsgKey.POLICY]) + self.policy.load_state(msg.body[MsgKey.POLICY]) while self.eval_env.state: self.eval_env.step(self.policy.choose_action(self.eval_env.state)) - self._logger.info( - f"Roll-out finished for ep {rollout_index}, segment {segment_index}" - f"(steps {starting_step_index} - {self.eval_env.step_index})" - ) - - return_info = {MsgKey.METRICS: self.env.metrics, MsgKey.TOTAL_REWARD: self.eval_env.total_reward} + return_info = { + MsgKey.METRICS: self.env.metrics, + MsgKey.TOTAL_REWARD: self.eval_env.total_reward, + MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] + } self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) diff --git a/maro/rl/training/actor_manager.py b/maro/rl/training/actor_manager.py index 5b31de08d..d21f509c2 100644 --- a/maro/rl/training/actor_manager.py +++ b/maro/rl/training/actor_manager.py @@ -49,7 +49,7 @@ def __init__( def collect( self, - rollout_index: int, + episode_index: int, segment_index: int, num_steps: int, policy_dict: dict = None, @@ -60,7 +60,7 @@ def collect( ): """Collect experiences from actors.""" msg_body = { - MsgKey.ROLLOUT_INDEX: rollout_index, + MsgKey.EPISODE_INDEX: episode_index, MsgKey.SEGMENT_INDEX: segment_index, MsgKey.NUM_STEPS: num_steps, MsgKey.POLICY: policy_dict, @@ -68,20 +68,20 @@ def collect( } if self._log_env_metrics: - self._logger.info(f"EPISODE-{rollout_index}, SEGMENT-{segment_index}: ") + self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") if exploration_params: self._logger.info(f"exploration_params: {exploration_params}") self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent collect requests for ep-{rollout_index}, segment-{segment_index}") + self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") # Receive roll-out results from remote actors num_finishes = 0 for msg in self._proxy.receive(): - if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: + if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: self._logger.info( - f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.ROLLOUT_INDEX]} " - f"(expected message type {MsgTag.COLLECT} and roll-out index {rollout_index})" + f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.EPISODE_INDEX]} " + f"(expected message type {MsgTag.COLLECT} and roll-out index {episode_index})" ) continue @@ -91,22 +91,23 @@ def collect( self._logger.info(f"env_metrics: {env_metrics}") if msg.body[MsgKey.SEGMENT_INDEX] == segment_index or not discard_stale_experiences: - self.total_experiences_collected += msg.body[MsgKey.NUM_EXPERIENCES] + experiences_by_agent = msg.body[MsgKey.EXPERIENCES] + self.total_experiences_collected += sum(len(exp) for exp in experiences_by_agent.values()) self.total_env_steps += msg.body[MsgKey.NUM_STEPS] is_env_end = msg.body[MsgKey.ENV_END] if is_env_end: self._logger.info(f"total rewards: {msg.body[MsgKey.TOTAL_REWARD]}") - yield msg.body[MsgKey.EXPERIENCES], is_env_end + yield experiences_by_agent, is_env_end if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: num_finishes += 1 if num_finishes == required_actor_finishes: break - def evaluate(self, rollout_index: int, policy_dict: dict, num_actors: int): + def evaluate(self, episode_index: int, policy_dict: dict, num_actors: int): """Collect experiences from actors.""" msg_body = { - MsgKey.ROLLOUT_INDEX: rollout_index, + MsgKey.EPISODE_INDEX: episode_index, MsgKey.POLICY: policy_dict, MsgKey.RETURN_ENV_METRICS: True } @@ -118,19 +119,18 @@ def evaluate(self, rollout_index: int, policy_dict: dict, num_actors: int): # Receive roll-out results from remote actors num_finishes = 0 for msg in self._proxy.receive(): - if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: + if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: self._logger.info( - f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.ROLLOUT_INDEX]} " - f"(expected message type {MsgTag.EVAL} and roll-out index {rollout_index})" + f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE_INDEX]} " + f"(expected message type {MsgTag.EVAL} and episode index {episode_index})" ) continue # log roll-out summary - if self._log_env_metrics: - env_metrics = msg.body[MsgKey.METRICS] - self._logger.info(f"env_metrics: {env_metrics}") + env_metrics = msg.body[MsgKey.METRICS] + self._logger.info(f"env metrics for evaluation episode {episode_index}: {env_metrics}") - if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: + if msg.body[MsgKey.EPISODE_INDEX] == episode_index: num_finishes += 1 if num_finishes == num_actors: break diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 60663eb9b..7e6bfa831 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -35,7 +35,8 @@ def __init__( required_actor_finishes: str = None, discard_stale_experiences: bool = True, log_env_metrics: bool = True, - log_dir: str = getcwd() + log_dir: str = getcwd(), + end_of_training_kwargs: dict = None ): if env is None and actor_manager is None: raise Exception("env and actor_manager cannot both be None") @@ -83,6 +84,7 @@ def __init__( self.required_actor_finishes = required_actor_finishes self.discard_stale_experiences = discard_stale_experiences + self.end_of_training_kwargs = end_of_training_kwargs if end_of_training_kwargs else {} self._log_env_metrics = log_env_metrics self._total_learning_time = 0 self._total_env_steps = 0 @@ -106,7 +108,7 @@ def train(self, ep: int): self._train_remote(ep) def evaluate(self, ep: int): - self._logger.info("Evaluating the policy") + self._logger.info("Evaluating...") if self.eval_env: self.policy.eval_mode() self.eval_env.save_replay = False @@ -122,13 +124,13 @@ def evaluate(self, ep: int): if self._log_env_metrics: self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") else: - self.actor_manager.evaluate( - ep, {id_: self.policy.policy_dict[id_] for id_ in updated_policy_ids}, self.num_eval_actors - ) + self.actor_manager.evaluate(ep, self.policy.state(), self.num_eval_actors) def _train_local(self, ep: int): t0 = time.time() - self._logger.info(f"Training the policy with exploration parameters: {self.policy.exploration_params}") + segement_index = 1 + self._logger.info(f"Training episode {ep}") + self._logger.debug(f"exploration parameters: {self.policy.exploration_params}") self.policy.train_mode() self.env.save_replay = True self.env.reset() @@ -140,43 +142,49 @@ def _train_local(self, ep: int): not self.env.state or self.policy_update_interval != -1 and self.env.step_index % self.policy_update_interval == 0 ): - exp_by_agent = {agent_id: replay.take() for agent_id, replay in self.env.replay.items()} + exp_by_agent = self.env.get_experiences() tl0 = time.time() - updated_policy_ids = self.policy.on_new_experiences(exp_by_agent) - print( - {name: len(policy.experience_memory) - for name, policy in self.policy.policy_dict.items() if isinstance(policy, AbsCorePolicy) - }) + self.policy.store_experiences(exp_by_agent) + updated_policy_ids = self.policy.update() + self.end_of_training(ep, segement_index, **self.end_of_training_kwargs) self._total_learning_time += time.time() - tl0 self._total_env_steps += self.policy_update_interval self._total_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) self._logger.debug(f"total running time: {time.time() - t0}") self._logger.debug(f"total learning time: {self._total_learning_time}") self._logger.debug(f"total env steps: {self._total_env_steps}") - self._logger.info(f"total experiences collected: {self._total_experiences_collected}") + self._logger.debug(f"total experiences collected: {self._total_experiences_collected}") if not self.env.state: self._logger.info(f"total reward: {self.env.total_reward}") + segement_index += 1 + self.policy.exploration_step() if self._log_env_metrics: self._logger.info(f"ep {ep}: {self.env.metrics}") def _train_remote(self, ep: int): + t0 = time.time() updated_policy_ids, num_actor_finishes, segment_index = list(self.policy.policy_dict.keys()), 0, 0 while num_actor_finishes < self.required_actor_finishes: for exp, done in self.actor_manager.collect( ep, segment_index, self.policy_update_interval, - policy_dict={id_: self.policy.policy_dict[id_] for id_ in updated_policy_ids}, + policy_dict=self.policy.state(), required_actor_finishes=self.required_actor_finishes, discard_stale_experiences=self.discard_stale_experiences ): tl0 = time.time() - updated_policy_ids = self.policy.on_new_experiences(exp) + self.policy.store_experiences(exp) + updated_policy_ids = self.policy.update() + self.end_of_training(ep, segment_index, **self.end_of_training_kwargs) num_actor_finishes += done self._total_learning_time += time.time() - tl0 self._logger.debug(f"running time: {time.time() - t0}") self._logger.debug(f"learning time: {self._total_learning_time}") self._logger.debug(f"env steps: {self.actor_manager.total_env_steps}") - self._logger.info(f"experiences collected: {self.actor_manager.total_experiences_collected}") + self._logger.debug(f"experiences collected: {self.actor_manager.total_experiences_collected}") segment_index += 1 + + def end_of_training(self, ep: int, segment: int, **kwargs): + pass diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index eb35de7d3..ad27abcb3 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -18,7 +18,8 @@ class MsgTag(Enum): class MsgKey(Enum): ACTION = "action" AGENT_ID = "agent_id" - ROLLOUT_INDEX = "rollout_index" + EPISODE_INDEX = "episode_index" + SEGMENT_INDEX = "segment_index" TIME_STEP = "time_step" METRICS = "metrics" EXPERIENCES = "experiences" @@ -27,7 +28,6 @@ class MsgKey(Enum): POLICY = "policy" VERSION = "version" NUM_STEPS = "num_steps" - SEGMENT_INDEX = "segment_index" RETURN_ENV_METRICS = "return_env_metrics" TOTAL_REWARD = "total_reward" ENV_END = "env_end" From 4c8aa68225da753b28a95f5199065681cd57d5b7 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 30 Apr 2021 11:32:10 +0000 Subject: [PATCH 208/482] checked out refined rl toolkit from v0.2_rl_refinement --- examples/supply_chain/config.py | 26 ++ examples/supply_chain/config.yml | 90 +++---- examples/supply_chain/distributed_launcher.py | 81 +++--- examples/supply_chain/docker-compose.yml | 58 +++++ examples/supply_chain/env_wrapper.py | 6 + examples/supply_chain/exploration.py | 24 ++ examples/supply_chain/learner.py | 9 + examples/supply_chain/policies.py | 45 ++++ .../supply_chain/single_thread_launcher.py | 50 ++-- maro/communication/proxy.py | 5 +- maro/rl/__init__.py | 51 ++-- maro/rl/algorithm/__init__.py | 16 ++ maro/rl/algorithm/ac.py | 116 +++++++++ maro/rl/algorithm/ddpg.py | 112 +++++++++ maro/rl/algorithm/dqn.py | 119 +++++++++ maro/rl/algorithm/pg.py | 94 +++++++ maro/rl/algorithm/rl_policy_index.py | 56 +++++ maro/rl/env_wrapper.py | 152 ++++++++++++ maro/rl/experience/__init__.py | 8 + maro/rl/experience/experience_memory.py | 168 +++++++++++++ maro/rl/experience/sampler.py | 49 ++++ maro/rl/experience/sampler_cls_index.py | 16 ++ maro/rl/exploration/__init__.py | 16 +- maro/rl/exploration/abs_exploration.py | 45 ++++ .../exploration/epsilon_greedy_exploration.py | 33 +++ maro/rl/exploration/exploration_scheduler.py | 128 ++++++++++ maro/rl/exploration/noise_exploration.py | 89 +++++++ maro/rl/model/__init__.py | 8 +- maro/rl/model/core_model.py | 232 ++++++++++++++++++ maro/rl/policy/__init__.py | 7 + maro/rl/policy/multi_agent_policy.py | 122 +++++++++ maro/rl/policy/policy.py | 162 ++++++++++++ maro/rl/training/__init__.py | 5 +- maro/rl/training/actor.py | 100 ++++++++ maro/rl/training/actor_manager.py | 141 +++++++++++ maro/rl/training/dispatcher.py | 58 +++++ maro/rl/training/learner.py | 215 ++++++++++++---- maro/rl/training/message_enums.py | 33 +++ maro/rl/training/trainer.py | 30 +++ maro/rl/utils/__init__.py | 6 +- maro/rl/utils/torch_cls_index.py | 8 +- maro/utils/exception/error_code.py | 3 +- maro/utils/exception/rl_toolkit_exception.py | 14 +- 43 files changed, 2567 insertions(+), 239 deletions(-) create mode 100644 examples/supply_chain/config.py create mode 100644 examples/supply_chain/docker-compose.yml create mode 100644 examples/supply_chain/exploration.py create mode 100644 examples/supply_chain/learner.py create mode 100644 examples/supply_chain/policies.py create mode 100644 maro/rl/algorithm/__init__.py create mode 100644 maro/rl/algorithm/ac.py create mode 100644 maro/rl/algorithm/ddpg.py create mode 100644 maro/rl/algorithm/dqn.py create mode 100644 maro/rl/algorithm/pg.py create mode 100644 maro/rl/algorithm/rl_policy_index.py create mode 100644 maro/rl/env_wrapper.py create mode 100644 maro/rl/experience/__init__.py create mode 100644 maro/rl/experience/experience_memory.py create mode 100644 maro/rl/experience/sampler.py create mode 100644 maro/rl/experience/sampler_cls_index.py create mode 100644 maro/rl/exploration/abs_exploration.py create mode 100644 maro/rl/exploration/epsilon_greedy_exploration.py create mode 100644 maro/rl/exploration/exploration_scheduler.py create mode 100644 maro/rl/exploration/noise_exploration.py create mode 100644 maro/rl/model/core_model.py create mode 100644 maro/rl/policy/__init__.py create mode 100644 maro/rl/policy/multi_agent_policy.py create mode 100644 maro/rl/policy/policy.py create mode 100644 maro/rl/training/actor.py create mode 100644 maro/rl/training/actor_manager.py create mode 100755 maro/rl/training/dispatcher.py create mode 100644 maro/rl/training/message_enums.py create mode 100755 maro/rl/training/trainer.py diff --git a/examples/supply_chain/config.py b/examples/supply_chain/config.py new file mode 100644 index 000000000..bdfd1002a --- /dev/null +++ b/examples/supply_chain/config.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +import yaml +from os import getenv +from os.path import dirname, join, realpath + +from maro.simulator import Env + +sc_code_dir = dirname(realpath(__file__)) +sys.path.insert(0, sc_code_dir) +from env_wrapper import SCEnvWrapper + + +default_config_path = join(dirname(realpath(__file__)), "config.yml") +with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: + config = yaml.safe_load(config_file) + +topology = config["env"]["topology"] +config["env"]["topology"] = join(sc_code_dir, "topologies", topology) +env = SCEnvWrapper(Env(**config["env"])) +consumer_agent_ids = [f"consumer.{info.id}" for info in env.agent_idx_list] +producer_agent_ids = [f"producer.{info.id}" for info in env.agent_idx_list] +config["agent_ids"] = consumer_agent_ids + producer_agent_ids +config["policy"]["consumer"]["model"]["input_dim"] = env.dim diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index f02ee8505..d59ffe24f 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -1,7 +1,21 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -agent: +env: + scenario: supply_chain + # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder + # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ + topology: sample1 + durations: 200 # number of ticks per episode +num_episodes: 10 # number of episodes to simulate +# Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps +# before returning experiences to the learner. The learner uses these experiences to update the agents' policies +# and sync the updated policies to the actors for the next learning cycle. +policy_update_interval: -1 +eval_points: 2 +log_env_metrics: false +end_of_training_kwargs: +policy: consumer: algorithm: dqn model: # If you want to design your model used, edit the get_dqn_agent() codes in examples\supply_chain\agent.py @@ -21,72 +35,28 @@ agent: lr: 0.001 algorithm_config: reward_discount: .9 - train_iters: 10 # number of gradient steps per call to agent.step() - batch_size: 128 # mini-batch size for DQN training loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. target_update_freq: 5 # How many training iteration, to update DQN target model soft_update_coefficient: 0.1 double: false # whether to enable double DQN + training_loop: + sampler_cls: uniform + batch_size: 128 + sampler_kwargs: + replace: true + train_iters: 10 + new_experience_trigger: 128 + # The minimum number of experiences in the experience memory required for learning. Defaults to 1. + num_warmup_experiences: 1000 experience_memory: - experience_memory_size: 50000 # experience memory size + capacity: 50000 # experience memory size # This determines how existing experiences are replaced when adding new experiences to a full experience # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. - experience_memory_overwrite_type: random - # If true, the agent's experience memory will be emptied after calling agent.step(). - empty_experience_memory_after_step: false - # The minimum number of new experiences required to trigger learning (agent.step()). Defaults to 1. - min_new_experiences_to_trigger_learning: 10 - # The minimum number of experiences in the experience memory required to trigger learning (agent.step()). Defaults to 1. - min_experiences_to_trigger_learning: 10 - producer: - algorithm: dqn - model: - hidden_dims: - - 16 - - 8 - output_dim: 10 - activation: leaky_relu - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop - optim_params: - lr: 0.001 - algorithm_config: - reward_discount: .9 - train_iters: 10 - batch_size: 128 - loss_cls: mse - target_update_freq: 5 - soft_update_coefficient: 0.1 - double: false # double DQN - experience_memory: - experience_memory_size: 50000 - experience_memory_overwrite_type: random - empty_experience_memory_after_step: false - min_new_experiences_to_trigger_learning: 1 - min_experiences_to_trigger_learning: 1 -training: - env: - scenario: supply_chain - # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder - # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ - topology: sample1 - durations: 200 # number of ticks per episode - num_episodes: 3 # number of episodes to simulate - # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps - # before returning experiences to the learner. The learner uses these experiences to update the agents' policies - # and sync the updated policies to the actors for the next learning cycle. - agent_update_interval: 60 - exploration: - parameter_names: - - epsilon - start: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. - end: 0.0 + overwrite_type: random +exploration: + initial_value: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. + final_value: 0.0 distributed: # this is used to group all actor / learner processes belonging to the same job for communication purposes. # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. @@ -95,7 +65,7 @@ distributed: # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make # sure that a local redis server is running and listening on the port specified by "redis_port". - redis_host: redis-sc + redis_host: localhost redis_port: 6379 # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent # slow actors from dragging down the whole process. diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index f752f39b9..f27f5a5d5 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -3,37 +3,24 @@ import argparse import sys -import time import yaml from multiprocessing import Process from os import getenv, makedirs from os.path import dirname, join, realpath -from maro.rl import ( - Actor, ActorManager, DistLearner, FullyConnectedBlock, LinearParameterScheduler, MultiAgentWrapper, - OptimOption, SimpleMultiHeadModel -) +from maro.rl import Actor, ActorManager, Learner, MultiAgentPolicy from maro.simulator import Env from maro.utils import set_seeds sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) +from config import config from env_wrapper import SCEnvWrapper -from agent import get_agent_func_map +from exploration import exploration_dict, agent_to_exploration +from learner import SCLearner +from policies import policy_dict, agent_to_policy -# Read the configuration -DEFAULT_CONFIG_PATH = join(sc_code_dir, "config.yml") -with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: - config = yaml.safe_load(config_file) - -get_producer_agent = get_agent_func_map[config["agent"]["producer"]["algorithm"]] -get_consumer_agent = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] - -# Get the env config path -topology = config["training"]["env"]["topology"] -config["training"]["env"]["topology"] = join(sc_code_dir, "topologies", topology) - # for distributed / multi-process training GROUP = getenv("GROUP", default=config["distributed"]["group"]) REDIS_HOST = config["distributed"]["redis_host"] @@ -45,45 +32,49 @@ def sc_learner(): - # create agents that update themselves using experiences collected from the actors. - env = SCEnvWrapper(Env(**config["training"]["env"])) - config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in env.agent_idx_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} - agent = MultiAgentWrapper({**producers, **consumers}) - - # exploration schedule - scheduler = LinearParameterScheduler(config["training"]["num_episodes"], **config["training"]["exploration"]) - + # create a multi-agent policy. + policy = MultiAgentPolicy( + policy_dict, + agent_to_policy, + exploration_dict=exploration_dict, + agent_to_exploration=agent_to_exploration + ) + # create an actor manager to collect simulation data from multiple actors actor_manager = ActorManager( NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False}, log_dir=log_dir ) - # create a learner to start the training process - learner = DistLearner( - agent, scheduler, actor_manager, - agent_update_interval=config["training"]["agent_update_interval"], + # create a learner to start training + learner = SCLearner( + policy, None, config["num_episodes"], + actor_manager=actor_manager, + policy_update_interval=config["policy_update_interval"], + eval_points=config["eval_points"], required_actor_finishes=config["distributed"]["required_actor_finishes"], - discard_stale_experiences=config["distributed"]["discard_stale_experiences"], + log_env_metrics=config["log_env_metrics"], + end_of_training_kwargs=config["end_of_training_kwargs"], log_dir=log_dir ) learner.run() -def sc_actor(): +def sc_actor(name: str): # create an env wrapper for roll-out and obtain the input dimension for the agents - env = SCEnvWrapper(Env(**config["training"]["env"])) - config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - - # create agents to interact with the env - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in env.agent_idx_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in env.agent_idx_list} - agent = MultiAgentWrapper({**producers, **consumers}) - + env = SCEnvWrapper(Env(**config["env"])) + policy = MultiAgentPolicy( + policy_dict, + agent_to_policy, + exploration_dict=exploration_dict, + agent_to_exploration=agent_to_exploration + ) # create an actor that collects simulation data for the learner. - actor = Actor(env, agent, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)}, log_dir=log_dir) + actor = Actor( + env, policy, GROUP, + proxy_options={"component_name": name, "redis_address": (REDIS_HOST, REDIS_PORT)}, + log_dir=log_dir + ) actor.run() @@ -96,7 +87,7 @@ def sc_actor(): args = parser.parse_args() if args.whoami == 0: - actor_processes = [Process(target=sc_actor) for i in range(NUM_ACTORS)] + actor_processes = [Process(target=sc_actor, args=(f"actor_{i + 1}",)) for i in range(NUM_ACTORS)] learner_process = Process(target=sc_learner) for i, actor_process in enumerate(actor_processes): @@ -112,4 +103,4 @@ def sc_actor(): elif args.whoami == 1: sc_learner() elif args.whoami == 2: - sc_actor() + sc_actor(getenv("COMPONENT")) diff --git a/examples/supply_chain/docker-compose.yml b/examples/supply_chain/docker-compose.yml new file mode 100644 index 000000000..949068879 --- /dev/null +++ b/examples/supply_chain/docker-compose.yml @@ -0,0 +1,58 @@ +services: + actor.0: + command: + - python3 + - /maro/supply_chain/distributed_launcher.py + - -w + - '2' + container_name: actor.0 + environment: + - COMPONENT_NAME=actor.0 + image: maro-sc + volumes: + - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain + - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl + actor.1: + command: + - python3 + - /maro/supply_chain/distributed_launcher.py + - -w + - '2' + container_name: actor.1 + environment: + - COMPONENT_NAME=actor.1 + image: maro-sc + volumes: + - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain + - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl + actor.2: + command: + - python3 + - /maro/supply_chain/distributed_launcher.py + - -w + - '2' + container_name: actor.2 + environment: + - COMPONENT_NAME=actor.2 + image: maro-sc + volumes: + - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain + - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl + learner: + build: + context: /home/data_disk/yaqiu/maro + dockerfile: /home/data_disk/yaqiu/maro/docker_files/dev.df + command: + - python3 + - /maro/supply_chain/distributed_launcher.py + - -w + - '1' + container_name: learner + image: maro-sc + volumes: + - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain + - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl + redis: + container_name: redis-sc + image: redis:6 +version: '3.9' diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 8008dd4da..7f136094c 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -164,6 +164,12 @@ def dim(self): return self._dim + def get_state_a(self): + pass + + def get_state_b(self): + pass + def get_state(self, event): cur_tick = self.env.tick settings: dict = self.env.configs.settings diff --git a/examples/supply_chain/exploration.py b/examples/supply_chain/exploration.py new file mode 100644 index 000000000..5d38d4df0 --- /dev/null +++ b/examples/supply_chain/exploration.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os.path import dirname, realpath + +from maro.rl import EpsilonGreedyExploration, LinearExplorationScheduler + +sc_code_dir = dirname(realpath(__file__)) +sys.path.insert(0, sc_code_dir) +from config import config + + +exploration = EpsilonGreedyExploration(config["policy"]["consumer"]["model"]["output_dim"]) +exploration.register_schedule( + LinearExplorationScheduler, "epsilon", config["num_episodes"], + initial_value=config["exploration"]["initial_value"], + kwargs={"final_value": config["exploration"]["final_value"]} +) + +exploration_dict = {"consumer": exploration} + +# all agents shared the same exploration object +agent_to_exploration = {agent_id: "consumer" for agent_id in config["agent_ids"] if agent_id.startswith("consumer")} diff --git a/examples/supply_chain/learner.py b/examples/supply_chain/learner.py new file mode 100644 index 000000000..b76775d7c --- /dev/null +++ b/examples/supply_chain/learner.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.rl import Learner + + +class SCLearner(Learner): + def end_of_training(self, ep, segment, **kwargs): + pass diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py new file mode 100644 index 000000000..ce9110e70 --- /dev/null +++ b/examples/supply_chain/policies.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os.path import dirname, realpath + +import numpy as np + +import torch + +from maro.rl import ( + DQN, DQNConfig, ExperienceMemory, FullyConnectedBlock, NullPolicy, OptimOption, QNetForDiscreteActionSpace, + TrainingLoopConfig, get_sampler_cls +) + +sc_code_dir = dirname(realpath(__file__)) +sys.path.insert(0, sc_code_dir) +from config import config + +agent_ids = config["agent_ids"] +config = config["policy"] + + +class SimpleQNet(QNetForDiscreteActionSpace): + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component.forward(states) + + +def get_dqn_policy(config): + q_net = SimpleQNet(FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"])) + experience_memory = ExperienceMemory(**config["experience_memory"]) + + config["training_loop"]["sampler_cls"] = get_sampler_cls(config["training_loop"]["sampler_cls"]) + generic_config = TrainingLoopConfig(**config["training_loop"]) + special_config = DQNConfig(**config["algorithm_config"]) + + return DQN(q_net, experience_memory, generic_config, special_config) + +# all consumers share the same underlying policy +policy_dict = {"consumer": get_dqn_policy(config["consumer"]), "producer": NullPolicy()} + +agent_to_policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index 1bc18ba13..6df35efb2 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -2,48 +2,34 @@ # Licensed under the MIT license. import sys -import yaml -from os import getenv -from os.path import dirname, join, realpath +from os.path import dirname, realpath -import numpy as np - -from maro.rl import Learner, LinearParameterScheduler, MultiAgentWrapper +from maro.rl import Learner, MultiAgentPolicy from maro.simulator import Env -from maro.utils import set_seeds sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) +from config import config from env_wrapper import SCEnvWrapper -from agent import get_agent_func_map +from exploration import exploration_dict, agent_to_exploration +from policies import policy_dict, agent_to_policy # Single-threaded launcher if __name__ == "__main__": - default_config_path = join(dirname(realpath(__file__)), "config.yml") - with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: - config = yaml.safe_load(config_file) - - get_producer_agent = get_agent_func_map[config["agent"]["producer"]["algorithm"]] - get_consumer_agent = get_agent_func_map[config["agent"]["consumer"]["algorithm"]] - - # Get the env config path - topology = config["training"]["env"]["topology"] - config["training"]["env"]["topology"] = join(dirname(realpath(__file__)), "topologies", topology) - - # create an env wrapper and obtain the input dimension for the agents - env = SCEnvWrapper(Env(**config["training"]["env"])) - config["agent"]["producer"]["model"]["input_dim"] = config["agent"]["consumer"]["model"]["input_dim"] = env.dim - - # create agents - agent_info_list = env.agent_idx_list - producers = {f"producer.{info.id}": get_producer_agent(config["agent"]["producer"]) for info in agent_info_list} - consumers = {f"consumer.{info.id}": get_consumer_agent(config["agent"]["consumer"]) for info in agent_info_list} - agent = MultiAgentWrapper({**producers, **consumers}) - - # exploration schedule - scheduler = LinearParameterScheduler(config["training"]["num_episodes"], **config["training"]["exploration"]) + env = SCEnvWrapper(Env(**config["env"])) + policy = MultiAgentPolicy( + policy_dict, + agent_to_policy, + exploration_dict=exploration_dict, + agent_to_exploration=agent_to_exploration + ) # create a learner to start training - learner = Learner(env, agent, scheduler, agent_update_interval=config["training"]["agent_update_interval"]) + learner = Learner( + policy, env, config["num_episodes"], + policy_update_interval=config["policy_update_interval"], + eval_points=config["eval_points"], + log_env_metrics=config["log_env_metrics"] + ) learner.run() diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 8f8d59f90..84862abcb 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -73,6 +73,7 @@ def __init__( group_name: str, component_type: str, expected_peers: dict, + component_name: str = None, driver_type: DriverType = DriverType.ZMQ, driver_parameters: dict = None, redis_address: Tuple = (HOST, PORT), @@ -91,8 +92,8 @@ def __init__( self._group_name = group_name self._component_type = component_type self._redis_hash_name = f"{self._group_name}:{self._component_type}" - if "COMPONENT_NAME" in os.environ: - self._name = os.getenv("COMPONENT_NAME") + if component_name is not None: + self._name = component_name else: unique_id = str(uuid.uuid1()).replace("-", "") self._name = f"{self._component_type}_{unique_id}" diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index aedb7b6fe..19bba3fdd 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -1,34 +1,39 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.agent import ( - DDPG, DQN, AbsAgent, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, MultiAgentWrapper, PolicyGradient, - PolicyGradientConfig +from maro.rl.algorithm import ( + DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, + get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) -from maro.rl.distributed import Actor, ActorManager, DistLearner +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.experience import AbsSampler, ExperienceMemory, ExperienceSet, Replay, UniformSampler, get_sampler_cls from maro.rl.exploration import ( - AbsExplorer, EpsilonGreedyExplorer, GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer + AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, + MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, UniformNoiseExploration ) -from maro.rl.model import AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel -from maro.rl.scheduling import LinearParameterScheduler, Scheduler, TwoPhaseLinearParameterScheduler -from maro.rl.storage import AbsSampler, AbsStore, SimpleStore, UniformSampler -from maro.rl.training import AbsEnvWrapper, Learner +from maro.rl.model import ( + AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, PolicyNetForDiscreteActionSpace, + PolicyValueNetForContinuousActionSpace, PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace +) +from maro.rl.policy import AbsCorePolicy, AbsFixedPolicy, MultiAgentPolicy, NullPolicy, RLPolicy, TrainingLoopConfig +from maro.rl.training import Actor, ActorManager, Learner from maro.rl.utils import ( - get_k_step_returns, get_lambda_returns, get_log_prob, get_max, get_sampler_cls, get_torch_activation_cls, - get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward, - select_by_actions + get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, + get_torch_optim_cls, get_truncated_cumulative_reward ) __all__ = [ - "AbsAgent", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "MultiAgentWrapper", - "PolicyGradient", "PolicyGradientConfig", - "Actor", "ActorManager", "DistLearner", - "AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer", - "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "SimpleMultiHeadModel", - "LinearParameterScheduler", "Scheduler", "TwoPhaseLinearParameterScheduler", - "AbsSampler", "AbsStore", "SimpleStore", "UniformSampler", - "AbsEnvWrapper", "Learner", - "get_k_step_returns", "get_lambda_returns", "get_log_prob", "get_max", "get_sampler_cls", - "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", - "get_truncated_cumulative_reward", "select_by_actions" + "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", + "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", + "AbsEnvWrapper", + "AbsSampler", "ExperienceMemory", "ExperienceSet", "Replay", "UniformSampler", "get_sampler_cls", + "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", + "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", + "UniformNoiseExploration", + "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "PolicyNetForDiscreteActionSpace", + "PolicyValueNetForContinuousActionSpace", "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace", + "AbsCorePolicy", "AbsFixedPolicy", "MultiAgentPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig", + "Actor", "ActorManager", "Learner", + "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", + "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/algorithm/__init__.py b/maro/rl/algorithm/__init__.py new file mode 100644 index 000000000..8449e3e59 --- /dev/null +++ b/maro/rl/algorithm/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac import ActorCritic, ActorCriticConfig +from .ddpg import DDPG, DDPGConfig +from .dqn import DQN, DQNConfig +from .pg import PolicyGradient, PolicyGradientConfig +from .rl_policy_index import get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls + +__all__ = [ + "ActorCritic", "ActorCriticConfig", + "DDPG", "DDPGConfig", + "DQN", "DQNConfig", + "PolicyGradient", "PolicyGradientConfig", + "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls" +] diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py new file mode 100644 index 000000000..a46191cfd --- /dev/null +++ b/maro/rl/algorithm/ac.py @@ -0,0 +1,116 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import namedtuple +from typing import Tuple + +import numpy as np +import torch +from torch.distributions import Categorical + +from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.model import PolicyValueNetForDiscreteActionSpace +from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.utils import get_torch_loss_cls + + +class ActorCriticConfig: + """Configuration for the Actor-Critic algorithm. + + Args: + reward_discount (float): Reward decay as defined in standard RL terminology. + critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing + the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". + actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., + loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. + clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, + in which case the actor loss is calculated using the usual policy gradient theorem. + """ + __slots__ = ["reward_discount", "critic_loss_func", "actor_loss_coefficient", "clip_ratio"] + + def __init__( + self, + reward_discount: float, + critic_loss_cls="mse", + actor_loss_coefficient: float = 1.0, + clip_ratio: float = None + ): + self.reward_discount = reward_discount + self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() + self.actor_loss_coefficient = actor_loss_coefficient + self.clip_ratio = clip_ratio + + +class ActorCritic(AbsCorePolicy): + """Actor Critic algorithm with separate policy and value models. + + References: + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f + + Args: + ac_net (PolicyValueNetForDiscreteActionSpace): Multi-task model that computes action distributions + and state values. + config: Configuration for the AC algorithm. + """ + def __init__( + self, + ac_net: PolicyValueNetForDiscreteActionSpace, + experience_memory: ExperienceMemory, + generic_config: TrainingLoopConfig, + special_config: ActorCriticConfig + ): + if not isinstance(ac_net, PolicyValueNetForDiscreteActionSpace): + raise TypeError("model must be an instance of 'PolicyValueNetForDiscreteActionSpace'") + + super().__init__(experience_memory, generic_config, special_config) + self.ac_net = ac_net + + def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: + """Use the actor (policy) model to generate stochastic actions. + + Args: + state: Input to the actor model. + + Returns: + Actions and corresponding log probabilities. + """ + with torch.no_grad(): + actions, log_p = self.ac_net.choose_action(states) + actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() + return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p + + def learn(self, experience_set: ExperienceSet): + if not isinstance(experience_set, ExperienceSet): + raise TypeError(f"Expected experience object of type ACExperience, got {type(experience_set)}") + + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)) + + state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() + next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() + return_est = rewards + self.special_config.reward_discount * next_state_values + advantages = return_est - state_values + # actor loss + action_prob, state_values = self.ac_net(states) + log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) + if self.special_config.clip_ratio is not None: + ratio = torch.exp(log_p_new - log_p) + clip_ratio = torch.clamp(ratio, 1 - self.special_config.clip_ratio, 1 + self.special_config.clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() + else: + actor_loss = -(log_p_new * advantages).mean() + + # critic_loss + critic_loss = self.special_config.critic_loss_func(state_values, return_est) + loss = critic_loss + self.special_config.actor_loss_coefficient * actor_loss + + self.ac_net.step(loss) + + def load_state(self, policy_state): + self.ac_net.load_state_dict(policy_state) + + def state(self): + return self.q_net.state_dict() \ No newline at end of file diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py new file mode 100644 index 000000000..cfa2276c9 --- /dev/null +++ b/maro/rl/algorithm/ddpg.py @@ -0,0 +1,112 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import namedtuple +from typing import Union + +import numpy as np +import torch + +from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.model import PolicyValueNetForContinuousActionSpace +from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.utils import get_torch_loss_cls + + +class DDPGConfig: + """Configuration for the DDPG algorithm. + + Args: + reward_discount (float): Reward decay as defined in standard RL terminology. + target_update_freq (int): Number of training rounds between policy target model updates. + q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for + the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". + policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., + loss = q_value_loss + ``policy_loss_coefficient`` * policy_loss. Defaults to 1.0. + soft_update_coefficient (float): Soft update coefficient, e.g., + target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. + Defaults to 1.0. + """ + __slots__ = [ + "reward_discount", "q_value_loss_func", "target_update_freq", "policy_loss_coefficient", + "soft_update_coefficient" + ] + + def __init__( + self, + reward_discount: float, + target_update_freq: int, + q_value_loss_cls="mse", + policy_loss_coefficient: float = 1.0, + soft_update_coefficient: float = 1.0, + ): + self.reward_discount = reward_discount + self.target_update_freq = target_update_freq + self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() + self.policy_loss_coefficient = policy_loss_coefficient + self.soft_update_coefficient = soft_update_coefficient + + +class DDPG(AbsCorePolicy): + """The Deep Deterministic Policy Gradient (DDPG) algorithm. + + References: + https://arxiv.org/pdf/1509.02971.pdf + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg + + Args: + ac_net (PolicyValueNetForContinuousActionSpace): DDPG policy and q-value models. + config (DDPGConfig): Configuration for DDPG algorithm. + """ + def __init__( + self, + ac_net: PolicyValueNetForContinuousActionSpace, + experience_memory: ExperienceMemory, + generic_config: TrainingLoopConfig, + special_config: DDPGConfig + ): + if not isinstance(ac_net, PolicyValueNetForContinuousActionSpace): + raise TypeError("model must be an instance of 'PolicyValueNetForContinuousActionSpace'") + + super().__init__(experience_memory, generic_config, special_config) + self.ac_net = ac_net + self.target_ac_net = ac_net.copy() if self.ac_net.trainable else None + self._train_cnt = 0 + + def choose_action(self, states) -> Union[float, np.ndarray]: + with torch.no_grad(): + actions = self.ac_net.choose_action(states).cpu().numpy() + + return actions[0] if len(actions) == 1 else actions + + def learn(self, experience_set: ExperienceSet): + if not isinstance(experience_set, ExperienceSet): + raise TypeError(f"Expected experience object of type DDPGExperience, got {type(experience_set)}") + + states, next_states = experience_set.states, experience_set.next_states + actual_actions = torch.from_numpy(experience_set.actions) + rewards = torch.from_numpy(experience_set.rewards) + if len(actual_actions.shape) == 1: + actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) + + current_q_values = self.ac_net(torch.cat([states, actual_actions], dim=1), task_name="q_value") + current_q_values = current_q_values.squeeze(dim=1) # (N,) + next_actions = self.target_ac_net(states, task_name="policy", training=False) + next_q_values = self.target_ac_net( + torch.cat([next_states, next_actions], dim=1), task_name="q_value", training=False + ).squeeze(1) # (N,) + target_q_values = (rewards + self.special_config.reward_discount * next_q_values).detach() # (N,) + q_value_loss = self.special_config.q_value_loss_func(current_q_values, target_q_values) + actions_from_model = self.ac_net(states, task_name="policy") + policy_loss = -self.ac_net(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() + self.ac_net.step(q_value_loss + self.special_config.policy_loss_coefficient * policy_loss) + self._train_cnt += 1 + if self._train_cnt % self.special_config.target_update_freq == 0: + self.target_ac_net.soft_update(self.ac_net, self.special_config.soft_update_coefficient) + + def load_state(self, policy_state): + self.ac_net.load_state_dict(policy_state) + self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None + + def state(self): + return self.ac_net.state_dict() diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py new file mode 100644 index 000000000..3cf0a67c6 --- /dev/null +++ b/maro/rl/algorithm/dqn.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import namedtuple +from typing import Union + +import numpy as np +import torch + +from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.model import QNetForDiscreteActionSpace +from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.utils import get_torch_loss_cls + + +class DQNConfig: + """Configuration for the DQN algorithm. + + Args: + reward_discount (float): Reward decay as defined in standard RL terminology. + target_update_freq (int): Number of training rounds between target model updates. + soft_update_coefficient (float): Soft update coefficient, e.g., + target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. + Defaults to 1.0. + double (bool): If True, the next Q values will be computed according to the double DQN algorithm, + i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). + See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. + loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class. If it is a string, + it must be a key in ``TORCH_LOSS``. Defaults to "mse". + """ + __slots__ = [ + "reward_discount", "target_update_freq", "train_iters", "batch_size", "sampler_cls", "sampler_params", + "soft_update_coefficient", "double", "loss_func" + ] + + def __init__( + self, + reward_discount: float, + target_update_freq: int, + soft_update_coefficient: float = 0.1, + double: bool = True, + loss_cls="mse" + ): + self.reward_discount = reward_discount + self.target_update_freq = target_update_freq + self.soft_update_coefficient = soft_update_coefficient + self.double = double + self.loss_func = get_torch_loss_cls(loss_cls)() + + +class DQN(AbsCorePolicy): + """The Deep-Q-Networks algorithm. + + See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. + + Args: + model (QNetForDiscreteActionSpace): Q-value model. + config (DQNConfig): Configuration for DQN algorithm. + """ + def __init__( + self, + q_net: QNetForDiscreteActionSpace, + experience_memory: ExperienceMemory, + generic_config: TrainingLoopConfig, + special_config: DQNConfig, + ): + if not isinstance(q_net, QNetForDiscreteActionSpace): + raise TypeError("model must be an instance of 'QNetForDiscreteActionSpace'") + + super().__init__(experience_memory, generic_config, special_config) + self.q_net = q_net + self.target_q_net = q_net.copy() if q_net.trainable else None + self.target_q_net.eval() + self._training_counter = 0 + + def choose_action(self, states) -> Union[int, np.ndarray]: + with torch.no_grad(): + self.q_net.eval() + actions, _ = self.q_net.choose_action(states) + + actions = actions.cpu().numpy() + return actions[0] if len(actions) == 1 else actions + + def learn(self, experience_set: ExperienceSet): + if not isinstance(experience_set, ExperienceSet): + raise TypeError( + f"Expected experience object of type AbsCorePolicy.experience_type, got {type(experience_set)}" + ) + + self.q_net.train() + # sample from the replay memory + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray(experience_set.actions)) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)) + q_values = self.q_net.q_values(states, actions) + # get next Q values + with torch.no_grad(): + if self.special_config.double: + next_q_values = self.target_q_net.q_values(next_states, self.q_net.choose_action(next_states)[0]) + else: + next_q_values = self.target_q_net.choose_action(next_states)[1] # (N,) + + # get TD errors + target_q_values = (rewards + self.special_config.reward_discount * next_q_values).detach() # (N,) + loss = self.special_config.loss_func(q_values, target_q_values) + + # train and update target if necessary + self.q_net.step(loss.mean()) + self._training_counter += 1 + if self._training_counter % self.special_config.target_update_freq == 0: + self.target_q_net.soft_update(self.q_net, self.special_config.soft_update_coefficient) + + def load_state(self, policy_state): + self.q_net.load_state_dict(policy_state) + self.target_q_net = self.q_net.copy() if self.q_net.trainable else None + self.target_q_net.eval() + + def state(self): + return self.q_net.state_dict() diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py new file mode 100644 index 000000000..3d01e7db3 --- /dev/null +++ b/maro/rl/algorithm/pg.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Tuple + +import numpy as np +import torch +from torch.distributions import Categorical + +from maro.rl.model import PolicyNetForDiscreteActionSpace +from maro.rl.policy import AbsCorePolicy +from maro.rl.utils import get_truncated_cumulative_reward + + +class PolicyGradientConfig: + """Configuration for the Policy Gradient algorithm. + + Args: + reward_discount (float): Reward decay as defined in standard RL terminology. + """ + __slots__ = ["reward_discount"] + + def __init__(self, reward_discount: float): + self.reward_discount = reward_discount + + +class PolicyGradient(AbsCorePolicy): + """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. + + Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + + Args: + model (ParameterizedPolicy): Multi-task model that computes action distributions and state values. + It may or may not have a shared bottom stack. + config (PolicyGradientConfig): Configuration for the PG algorithm. + experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of + unlimited size. + experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are + to be overwritten after its capacity has been reached. Must be "rolling" or "random". + empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call + to ``step``. Defaults to True. + new_experience_trigger (int): Minimum number of new experiences required to trigger learning. + Defaults to 1. + min_experiences_to_trigger_training (int): Minimum number of experiences in the experience memory required for + training. Defaults to 1. + """ + def __init__( + self, + model: PolicyNetForDiscreteActionSpace, + config: PolicyGradientConfig, + experience_memory_size: int, + experience_memory_overwrite_type: str, + empty_experience_memory_after_step: bool = True, + new_experience_trigger: int = 1, + min_experiences_to_trigger_training: int = 1 + ): + if not isinstance(model, PolicyNetForDiscreteActionSpace): + raise TypeError("model must be an instance of 'PolicyNetForDiscreteActionSpace'") + super().__init__( + model, config, experience_memory_size, experience_memory_overwrite_type, + empty_experience_memory_after_step, + new_experience_trigger=new_experience_trigger, + min_experiences_to_trigger_training=min_experiences_to_trigger_training + ) + + def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Use the actor (policy) model to generate stochastic actions. + + Args: + state: Input to the actor model. + + Returns: + Actions and corresponding log probabilities. + """ + state = torch.from_numpy(state).to(self.device) + is_single = len(state.shape) == 1 + if is_single: + state = state.unsqueeze(dim=0) + + action_prob = Categorical(self.model(state, training=False)) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + action, log_p = action.cpu().numpy(), log_p.cpu().numpy() + return (action[0], log_p[0]) if is_single else (action, log_p) + + def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): + states = torch.from_numpy(states).to(self.device) + actions = torch.from_numpy(actions).to(self.device) + returns = get_truncated_cumulative_reward(rewards, self.special_config.reward_discount) + returns = torch.from_numpy(returns).to(self.device) + action_distributions = self.model(states) + action_prob = action_distributions.gather(1, actions.unsqueeze(1)).squeeze() # (N, 1) + loss = -(torch.log(action_prob) * returns).mean() + self.model.step(loss) diff --git a/maro/rl/algorithm/rl_policy_index.py b/maro/rl/algorithm/rl_policy_index.py new file mode 100644 index 000000000..397d0b51e --- /dev/null +++ b/maro/rl/algorithm/rl_policy_index.py @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac import ActorCritic, ActorCriticConfig, PolicyValueNetForDiscreteActionSpace +from .ddpg import DDPG, DDPGConfig, PolicyValueNetForContinuousActionSpace +from .dqn import DQN, DQNConfig, QNetForDiscreteActionSpace +from .pg import PolicyGradient, PolicyGradientConfig, PolicyNetForDiscreteActionSpace + + +RL_POLICY_INDEX = { + "ac": ActorCritic, + "dqn": DQN, + "ddpg": DDPG, + "pg": PolicyGradient +} + +RL_POLICY_CONFIG_INDEX = { + "ac": ActorCritic, + "dqn": DQNConfig, + "ddpg": DDPGConfig, + "pg": PolicyGradientConfig +} + +RL_POLICY_MODEL_INDEX = { + "ac": PolicyValueNetForDiscreteActionSpace, + "dqn": QNetForDiscreteActionSpace, + "ddpg": PolicyValueNetForContinuousActionSpace, + "pg": PolicyNetForDiscreteActionSpace +} + + +def get_rl_policy_cls(policy_type): + if isinstance(policy_type, str): + if policy_type not in RL_POLICY_INDEX: + raise KeyError(f"A string policy_type must be one of {list(RL_POLICY_INDEX.keys())}.") + return RL_POLICY_INDEX[policy_type] + + return policy_type + + +def get_rl_policy_config_cls(policy_config_type): + if isinstance(policy_config_type, str): + if policy_config_type not in RL_POLICY_CONFIG_INDEX: + raise KeyError(f"A string policy_config_type must be one of {list(RL_POLICY_CONFIG_INDEX.keys())}.") + return RL_POLICY_CONFIG_INDEX[policy_config_type] + + return policy_config_type + + +def get_rl_policy_model_cls(policy_model_type): + if isinstance(policy_model_type, str): + if policy_model_type not in RL_POLICY_MODEL_INDEX: + raise KeyError(f"A string policy_model_type must be one of {list(RL_POLICY_MODEL_INDEX.keys())}.") + return RL_POLICY_MODEL_INDEX[policy_model_type] + + return policy_model_type diff --git a/maro/rl/env_wrapper.py b/maro/rl/env_wrapper.py new file mode 100644 index 000000000..9ce855844 --- /dev/null +++ b/maro/rl/env_wrapper.py @@ -0,0 +1,152 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from abc import ABC, abstractmethod +from collections import defaultdict, deque +from typing import List, Union + +from maro.rl.experience import Replay +from maro.simulator import Env + + +class AbsEnvWrapper(ABC): + """Environment wrapper that performs various shaping and other roll-out related logic. + + Args: + env (Env): Environment instance. + save_replay (bool): If True, the steps during roll-out will be recorded sequentially. This + includes states, actions and rewards. The decision events themselves will also be recorded + for delayed reward evaluation purposes. Defaults to True. + reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward + for the action taken for that event. Defaults to 0, which rewards are evaluated immediately + after executing an action. + """ + def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): + self.env = env + self.replay = defaultdict(Replay) + self.state_info = None # context for converting model output to actions that can be executed by the env + self.save_replay = save_replay + self.reward_eval_delay = reward_eval_delay + self.pending_reward_ticks = deque() # list of ticks whose actions have not been given a reward + self.action_history = {} # store the tick-to-action mapping + self._step_index = None + self._total_reward = 0 + self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. + self._state = None # the latest extracted state is kept here + + @property + def step_index(self): + return self._step_index + + @property + def agent_idx_list(self): + return self.env.agent_idx_list + + def start(self): + self._step_index = 0 + _, self._event, _ = self.env.step(None) + self._state = self.get_state(self.env.tick) + + @property + def metrics(self): + return self.env.metrics + + @property + def state(self): + return self._state + + @property + def event(self): + return self._event + + @property + def total_reward(self): + return self._total_reward + + @abstractmethod + def get_state(self, tick: int = None) -> dict: + """Compute the state for a given tick. + + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + """ + pass + + @abstractmethod + def get_action(self, action) -> dict: + pass + + @abstractmethod + def get_reward(self, tick: int = None) -> dict: + """User-defined reward evaluation. + + Args: + tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed + reward evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. + """ + pass + + def step(self, action_by_agent: dict): + # t0 = time.time() + self._step_index += 1 + env_action = self.get_action(action_by_agent) + self.pending_reward_ticks.append(self.env.tick) + self.action_history[self.env.tick] = action_by_agent + if len(env_action) == 1: + env_action = list(env_action.values())[0] + # t1 = time.time() + _, self._event, done = self.env.step(env_action) + # t2 = time.time() + # self._tot_raw_step_time += t2 - t1 + + """ + If roll-out is complete, evaluate rewards for all remaining events except the last. + Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. + """ + while ( + self.pending_reward_ticks and + (done or self.env.tick - self.pending_reward_ticks[0] >= self.reward_eval_delay) + ): + tick = self.pending_reward_ticks.popleft() + reward = self.get_reward(tick=tick) + # assign rewards to the agents that took action at that tick + for agent_id in self.action_history[tick]: + rw = reward.get(agent_id, 0) + if not done and self.save_replay: + self.replay[agent_id].rewards.append(rw) + self._total_reward += rw + + if not done: + prev_state = self._state + self._state = self.get_state(self.env.tick) + if self.save_replay: + for agent_id, state in prev_state.items(): + self.replay[agent_id].states.append(state) + self.replay[agent_id].actions.append(action_by_agent[agent_id]) + # t3 = time.time() + # self._tot_step_time += t3 - t0 + else: + self._state = None + self.end_of_episode() + + # print(f"total raw step time: {self._tot_raw_step_time}") + # print(f"total step time: {self._tot_step_time}") + # self._tot_raw_step_time = 0 + # self._tot_step_time = 0 + + def end_of_episode(self): + pass + + def get_experiences(self): + return {agent_id: replay.to_experience_set() for agent_id, replay in self.replay.items()} + + def reset(self): + self.env.reset() + self.state_info = None + self._total_reward = 0 + self._state = None + self.pending_reward_ticks.clear() + self.action_history.clear() + self.replay = defaultdict(Replay) diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py new file mode 100644 index 000000000..10b26517b --- /dev/null +++ b/maro/rl/experience/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .sampler import AbsSampler, UniformSampler +from .sampler_cls_index import get_sampler_cls +from .experience_memory import ExperienceMemory, ExperienceSet, Replay + +__all__ = ["AbsSampler", "ExperienceMemory", "ExperienceSet", "Replay", "UniformSampler", "get_sampler_cls"] diff --git a/maro/rl/experience/experience_memory.py b/maro/rl/experience/experience_memory.py new file mode 100644 index 000000000..d82aa77c8 --- /dev/null +++ b/maro/rl/experience/experience_memory.py @@ -0,0 +1,168 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from enum import Enum +from typing import Callable, Dict, List, Tuple, Union + +import numpy as np + +from maro.utils import clone +from maro.utils.exception.rl_toolkit_exception import InvalidExperience + + +class ExperienceSet: + + __slots__ = ["states", "actions", "rewards", "next_states"] + + def __init__(self, states: list, actions: list, rewards: list, next_states: list): + if not len(states) == len(actions) == len(rewards) == len(next_states): + raise InvalidExperience("values of contents should consist of lists of the same length") + + self.states = states + self.actions = actions + self.rewards = rewards + self.next_states = next_states + + def __len__(self): + return len(self.states) + + +class Replay(object): + def __init__(self): + self.states = [] + self.actions = [] + self.rewards = [] + + def to_experience_set(self): + # print(len(self.rewards), len(self.states)) + num_complete = min(len(self.rewards), len(self.states) - 1) + exp_set = ExperienceSet( + self.states[:num_complete], + self.actions[:num_complete], + self.rewards[:num_complete], + self.states[1:num_complete + 1] + ) + + del self.states[:num_complete] + del self.actions[:num_complete] + del self.rewards[:num_complete] + + return exp_set + + +class ExperienceMemory(object): + """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". + + This implementation uses a dictionary of lists as the internal data structure. The objects for each key + are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during + put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited + and limited storage are supported. + + Args: + capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. + overwrite_type (str): If storage capacity is bounded, this specifies how existing entries + are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: + - "rolling", where overwrite occurs sequentially with wrap-around. + - "random", where overwrite occurs randomly among filled positions. + Alternatively, the user may also specify overwrite positions (see ``put``). + """ + def __init__(self, capacity: int = -1, overwrite_type: str = None): + super().__init__() + if overwrite_type not in {"rolling", "random"}: + raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") + self._capacity = capacity + self._overwrite_type = overwrite_type + self._keys = ExperienceSet.__slots__ + self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self._keys} + self._size = 0 + + def __len__(self): + return self._size + + def __getitem__(self, index: int): + return {k: lst[index] for k, lst in self.data.items()} + + @property + def capacity(self): + """Store capacity. + + If negative, the store grows without bound. Otherwise, the number of items in the store will not exceed + this capacity. + """ + return self._capacity + + @property + def overwrite_type(self): + """An string indicating the overwrite behavior when the store capacity is exceeded.""" + return self._overwrite_type + + def get(self, indexes: [int] = None) -> ExperienceSet: + if indexes is None: + return ExperienceSet(*[self.data[k] for k in self._keys]) + + return ExperienceSet(*[[self.data[k][i] for i in indexes] for k in self._keys]) + + def put(self, experience_set: ExperienceSet, overwrite_indexes: list = None) -> List[int]: + """Put new contents in the store. + + Args: + contents (dict): Dictionary of items to add to the store. If the store is not empty, this must have the + same keys as the store itself. Otherwise an ``StoreMisalignment`` will be raised. + overwrite_indexes (list, optional): Indexes where the contents are to be overwritten. This is only + used when the store has a fixed capacity and putting ``contents`` in the store would exceed this + capacity. If this is None and overwriting is necessary, rolling or random overwriting will be done + according to the ``overwrite`` property. Defaults to None. + Returns: + The indexes where the newly added entries reside in the store. + """ + added_size = len(experience_set) + if self._capacity == -1: + for key in self.data: + self.data[key].extend(getattr(experience_set, key)) + self._size += added_size + return list(range(self._size - added_size, self._size)) + else: + write_indexes = self._get_update_indexes(added_size, overwrite_indexes=overwrite_indexes) + for key in self.data: + for index, value in zip(write_indexes, getattr(experience_set, key)): + self.data[key][index] = value + + self._size = min(self._capacity, self._size + added_size) + return write_indexes + + def clear(self): + """Empty the store.""" + self.data = { + key: [] if self._capacity == -1 else [None] * self._capacity for key in ExperienceSet.__slots__ + } + self._size = 0 + + def dumps(self): + """Return a deep copy of store contents.""" + return clone(dict(self.data)) + + def get_by_key(self, key): + """Get the contents of the store corresponding to ``key``.""" + return self.data[key] + + def _get_update_indexes(self, added_size: int, overwrite_indexes=None): + if added_size > self._capacity: + raise ValueError("size of added items should not exceed the store capacity.") + + num_overwrites = self._size + added_size - self._capacity + if num_overwrites < 0: + return list(range(self._size, self._size + added_size)) + + if overwrite_indexes is not None: + write_indexes = list(range(self._size, self._capacity)) + list(overwrite_indexes) + else: + # follow the overwrite rule set at init + if self._overwrite_type == "rolling": + # using the negative index convention for convenience + start_index = self._size - self._capacity + write_indexes = list(range(start_index, start_index + added_size)) + else: + random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) + write_indexes = list(range(self._size, self._capacity)) + list(random_indexes) + + return write_indexes diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py new file mode 100644 index 000000000..1777f0471 --- /dev/null +++ b/maro/rl/experience/sampler.py @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from typing import List + +import numpy as np + +from .experience_memory import ExperienceMemory + + +class AbsSampler(ABC): + def __init__(self, data: ExperienceMemory, batch_size: int): + self.data = data + self.batch_size = batch_size + + @abstractmethod + def sample(self) -> List[int]: + raise NotImplementedError + + @abstractmethod + def update(self): + """Update statistics used for sampling.""" + pass + + +class UniformSampler(AbsSampler): + """ + Obtain a random sample from the experience pool. + + Args: + size (int): Sample sizes for each round of sampling in the chain. If this is a single integer, it is + used as the sample size for all samplers in the chain. + weights (Union[list, np.ndarray]): Sampling weights. + replace (bool): If True, sampling is performed with replacement. Defaults to True. + Returns: + Sampled indexes and the corresponding objects, + e.g., [1, 2, 3], ['a', 'b', 'c']. + """ + def __init__(self, data: ExperienceMemory, batch_size: int, replace: bool = True): + super().__init__(data, batch_size) + self.replace = replace + + def sample(self): + indexes = np.random.choice(len(self.data), size=self.batch_size, replace=self.replace) + return indexes + + def update(self, indexes, values): + pass diff --git a/maro/rl/experience/sampler_cls_index.py b/maro/rl/experience/sampler_cls_index.py new file mode 100644 index 000000000..0154a05fa --- /dev/null +++ b/maro/rl/experience/sampler_cls_index.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .sampler import UniformSampler + +SAMPLER = { + "uniform": UniformSampler, +} + +def get_sampler_cls(sampler_type): + if isinstance(sampler_type, str): + if sampler_type not in SAMPLER: + raise KeyError(f"A string sampler_type must be one of {list(SAMPLER.keys())}.") + return SAMPLER[sampler_type] + + return sampler_type diff --git a/maro/rl/exploration/__init__.py b/maro/rl/exploration/__init__.py index e4a94ff21..883461b1d 100644 --- a/maro/rl/exploration/__init__.py +++ b/maro/rl/exploration/__init__.py @@ -1,8 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .abs_explorer import AbsExplorer -from .epsilon_greedy_explorer import EpsilonGreedyExplorer -from .noise_explorer import GaussianNoiseExplorer, NoiseExplorer, UniformNoiseExplorer +from .abs_exploration import AbsExploration, NullExploration +from .epsilon_greedy_exploration import EpsilonGreedyExploration +from .noise_exploration import GaussianNoiseExploration, NoiseExploration, UniformNoiseExploration +from .exploration_scheduler import ( + AbsExplorationScheduler, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler +) -__all__ = ["AbsExplorer", "EpsilonGreedyExplorer", "GaussianNoiseExplorer", "NoiseExplorer", "UniformNoiseExplorer"] +__all__ = [ + "AbsExploration", "NullExploration", + "EpsilonGreedyExploration", + "GaussianNoiseExploration", "NoiseExploration", "UniformNoiseExploration", + "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler" +] diff --git a/maro/rl/exploration/abs_exploration.py b/maro/rl/exploration/abs_exploration.py new file mode 100644 index 000000000..28f9f4c5c --- /dev/null +++ b/maro/rl/exploration/abs_exploration.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from typing import Callable + + +class AbsExploration(ABC): + """Abstract exploration class for generating exploration rates. + + """ + def __init__(self): + self.scheduler = {} + + def register_schedule( + self, + scheduler_cls, + param_name: str, + last_ep: int, + initial_value=None, + kwargs: dict = None + ): + if kwargs is None: + kwargs = {} + self.scheduler[param_name] = scheduler_cls(self, param_name, last_ep, initial_value=initial_value, **kwargs) + + @abstractmethod + def __call__(self, action): + return NotImplementedError + + @property + def parameters(self): + return {param_name: getattr(self, param_name) for param_name in self.scheduler} + + def step(self): + for scheduler in self.scheduler.values(): + scheduler.step() + + +class NullExploration(AbsExploration): + def __init__(self): + pass + + def __call__(self, action): + return action diff --git a/maro/rl/exploration/epsilon_greedy_exploration.py b/maro/rl/exploration/epsilon_greedy_exploration.py new file mode 100644 index 000000000..b51cec44e --- /dev/null +++ b/maro/rl/exploration/epsilon_greedy_exploration.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Union + +import numpy as np + +from .abs_exploration import AbsExploration + + +class EpsilonGreedyExploration(AbsExploration): + """Epsilon greedy exploration for discrete action spaces. + + Args: + num_actions (int): Number of all possible actions. + """ + def __init__(self, num_actions: int, epsilon: float = .0): + super().__init__() + self._num_actions = num_actions + self.epsilon = epsilon + + def __call__(self, action_index: Union[int, np.ndarray]): + if isinstance(action_index, np.ndarray): + return np.array([self._get_exploration_action(act) for act in action_index]) + else: + return self._get_exploration_action(action_index) + + def set_params(self, *, epsilon: float): + self.epsilon = epsilon + + def _get_exploration_action(self, action_index): + assert (action_index < self._num_actions), f"Invalid action: {action_index}" + return action_index if np.random.random() > self.epsilon else np.random.choice(self._num_actions) diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/exploration_scheduler.py new file mode 100644 index 000000000..f462836e7 --- /dev/null +++ b/maro/rl/exploration/exploration_scheduler.py @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractclassmethod +from typing import List, Tuple + +from maro.rl.exploration.abs_exploration import AbsExploration + + +class AbsExplorationScheduler: + def __init__( + self, + exploration: AbsExploration, + param_name: str, + last_ep: int, + initial_value=None + ): + self.exploration = exploration + self.param_name = param_name + self.last_ep = last_ep + if initial_value is not None: + setattr(self.exploration, self.param_name, initial_value) + + def get_value(self): + return getattr(self.exploration, self.param_name) + + @abstractclassmethod + def step(self): + raise NotImplementedError + + +class LinearExplorationScheduler(AbsExplorationScheduler): + """Static exploration parameter generator based on a linear schedule. + + Args: + max_iter (int): Maximum number of iterations. + parameter_names (List[str]): List of exploration parameter names. + start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. + These values must correspond to ``parameter_names``. + end (Union[float, list, tuple, np.ndarray]): Exploration parameter values rate for the last episode. + These values must correspond to ``parameter_names``. + """ + def __init__( + self, + exploration: AbsExploration, + param_name: str, + last_ep: int, + final_value: float, + initial_value: float = None, + ): + super().__init__(exploration, param_name, last_ep, initial_value=initial_value) + self.final_value = final_value + if self.last_ep > 1: + self.delta = (self.final_value - getattr(self.exploration, self.param_name)) / (self.last_ep - 1) + else: + self.delta = 0 + + def step(self): + if self.get_value() == self.final_value: + return + + setattr(self.exploration, self.param_name, self.get_value() + self.delta) + + +class MultiPhaseLinearExplorationScheduler(AbsExplorationScheduler): + """Exploration parameter generator based on two linear schedules joined together. + + Args: + max_iter (int): Maximum number of iterations. + parameter_names (List[str]): List of exploration parameter names. + split (float): The point where the switch from the first linear schedule to the second occurs. + start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. + These values must correspond to ``parameter_names``. + mid (Union[float, list, tuple, np.ndarray]): Exploration parameter values where the switch from the + first linear schedule to the second occurs. In other words, this is the exploration rate where the first + linear schedule ends and the second begins. These values must correspond to ``parameter_names``. + end (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the last episode. + These values must correspond to ``parameter_names``. + + Returns: + An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. + """ + def __init__( + self, + exploration: AbsExploration, + param_name: str, + last_ep: int, + splits: List[Tuple[int, float]], + final_value: float, + initial_value: float = None + ): + # validate splits + splits.append((1, initial_value)) + splits.append((last_ep, final_value)) + splits.sort() + for (ep, _), (ep2, _) in zip(splits, splits[1:]): + if ep == ep2: + raise ValueError("The zeroth element of split points must be unique") + + super().__init__(exploration, param_name, last_ep, initial_value=initial_value) + self.final_value = final_value + self._splits = splits + self._ep = 1 + self._split_index = 1 + self._delta = (self._splits[1][1] - self.get_value()) / (self._splits[1][0] - 1) + + def step(self): + if self._split_index == len(self._splits): + return + + setattr(self.exploration, self.param_name, self.get_value() + self._delta) + self._ep += 1 + if self._ep == self._splits[self._split_index][0]: + self._split_index += 1 + if self._split_index < len(self._splits): + self._delta = ( + (self._splits[self._split_index][1] - self._splits[self._split_index - 1][1]) / + (self._splits[self._split_index][0] - self._splits[self._split_index - 1][0]) + ) + + +if __name__ == "__main__": + from maro.rl.exploration.epsilon_greedy_exploration import EpsilonGreedyExploration + exploration = EpsilonGreedyExploration(5, epsilon=0.6) + scheduler = MultiPhaseLinearExplorationScheduler(exploration, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0) + for ep in range(1, scheduler.last_ep + 1): + print(f"ep = {ep}, value = {exploration.epsilon}") + scheduler.step() diff --git a/maro/rl/exploration/noise_exploration.py b/maro/rl/exploration/noise_exploration.py new file mode 100644 index 000000000..560c0c5d2 --- /dev/null +++ b/maro/rl/exploration/noise_exploration.py @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import abstractmethod +from typing import Union + +import numpy as np + +from .abs_exploration import AbsExploration + + +class NoiseExploration(AbsExploration): + """Exploration that adds a random noise to a model-generated action.""" + def __init__( + self, + min_action: Union[float, list, np.ndarray] = None, + max_action: Union[float, list, np.ndarray] = None + ): + if isinstance(min_action, (list, np.ndarray)) and isinstance(max_action, (list, np.ndarray)): + assert len(min_action) == len(max_action), "min_action and max_action should have the same dimension." + super().__init__() + self.min_action = min_action + self.max_action = max_action + + @abstractmethod + def set_params(self, **parameters): + raise NotImplementedError + + @abstractmethod + def __call__(self, action) -> np.ndarray: + raise NotImplementedError + + +class UniformNoiseExploration(NoiseExploration): + """Exploration that adds a random noise to a model-generated action sampled from a uniform distribution.""" + def __init__( + self, + min_action: Union[float, list, np.ndarray] = None, + max_action: Union[float, list, np.ndarray] = None, + noise_lower_bound: Union[float, list, np.ndarray] = .0, + noise_upper_bound: Union[float, list, np.ndarray] = .0 + ): + if isinstance(noise_upper_bound, (list, np.ndarray)) and isinstance(noise_upper_bound, (list, np.ndarray)): + assert len(noise_lower_bound) == len(noise_upper_bound), \ + "noise_lower_bound and noise_upper_bound should have the same dimension." + super().__init__(min_action, max_action) + self.noise_lower_bound = noise_lower_bound + self.noise_upper_bound = noise_upper_bound + + def __call__(self, action: np.ndarray) -> np.ndarray: + return np.array([self._get_exploration_action(act) for act in action]) + + def _get_exploration_action(self, action): + action += np.random.uniform(self.noise_lower_bound, self.noise_upper_bound) + if self.min_action is not None or self.max_action is not None: + return np.clip(action, self.min_action, self.max_action) + else: + return action + + +class GaussianNoiseExploration(NoiseExploration): + """Exploration that adds a random noise to a model-generated action sampled from a Gaussian distribution.""" + def __init__( + self, + min_action: Union[float, list, np.ndarray] = None, + max_action: Union[float, list, np.ndarray] = None, + noise_mean: Union[float, list, np.ndarray] = .0, + noise_stddev: Union[float, list, np.ndarray] = .0, + is_relative: bool = False + ): + if isinstance(noise_mean, (list, np.ndarray)) and isinstance(noise_stddev, (list, np.ndarray)): + assert len(noise_mean) == len(noise_stddev), "noise_mean and noise_stddev should have the same dimension." + if is_relative and noise_mean != .0: + raise ValueError("Standard deviation cannot be relative if noise mean is non-zero.") + super().__init__(min_action, max_action) + self.noise_mean = noise_mean + self.noise_stddev = noise_stddev + self.is_relative = is_relative + + def __call__(self, action: np.ndarray) -> np.ndarray: + return np.array([self._get_exploration_action(act) for act in action]) + + def _get_exploration_action(self, action): + noise = np.random.normal(loc=self.noise_mean, scale=self.noise_stddev) + action += (noise * action) if self.is_relative else noise + if self.min_action is not None or self.max_action is not None: + return np.clip(action, self.min_action, self.max_action) + else: + return action diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 00c54829d..3f75ead50 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -3,10 +3,14 @@ from .abs_block import AbsBlock from .fc_block import FullyConnectedBlock -from .learning_model import AbsCoreModel, OptimOption, SimpleMultiHeadModel +from .core_model import ( + AbsCoreModel, OptimOption, PolicyNetForDiscreteActionSpace, PolicyValueNetForContinuousActionSpace, + PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace +) __all__ = [ "AbsBlock", "FullyConnectedBlock", - "AbsCoreModel", "OptimOption", "SimpleMultiHeadModel" + "AbsCoreModel", "OptimOption", "PolicyNetForDiscreteActionSpace", "PolicyValueNetForContinuousActionSpace", + "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace" ] diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py new file mode 100644 index 000000000..81972e476 --- /dev/null +++ b/maro/rl/model/core_model.py @@ -0,0 +1,232 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import abstractmethod +from typing import Dict, List, Union + +import torch +import torch.nn as nn +from torch.distributions import Categorical + +from maro.rl.utils import get_torch_lr_scheduler_cls, get_torch_optim_cls +from maro.utils import clone +from maro.utils.exception.rl_toolkit_exception import MissingOptimizer + + +class OptimOption: + """Model optimization options. + Args: + optim_cls: A string indicating an optimizer class provided by torch.optim or custom subclass of + torch.optim.Optimizer. If a string is provided, it must be present in the ``TORCH_OPTIM`` index. + optim_params (dict): Parameters for the optimizer class. + scheduler_cls: A string indicating an lr-scheduler class provided by torch.optim.lr_scheduler or custom + subclass of torch.optim.lr_scheduler. If a string is provided, it must be present in the + ``TORCH_LR_SCHEDULER`` index. Defaults to None. + scheduler_params (dict): Parameters for the scheduler class. Defaults to None. + """ + __slots__ = ["optim_cls", "optim_params", "scheduler_cls", "scheduler_params"] + + def __init__(self, optim_cls, optim_params: dict, scheduler_cls=None, scheduler_params: dict = None): + self.optim_cls = get_torch_optim_cls(optim_cls) + self.optim_params = optim_params + self.scheduler_cls = get_torch_lr_scheduler_cls(scheduler_cls) + self.scheduler_params = scheduler_params + + +class AbsCoreModel(nn.Module): + """Trainable model that consists of multiple network components. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None + ): + super().__init__() + self.component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) + if optim_option is None: + self.optimizer = None + self.scheduler = None + self.eval() + for param in self.parameters(): + param.requires_grad = False + else: + if isinstance(optim_option, dict): + self.optimizer = {} + for name, opt in optim_option.items(): + self.optimizer[name] = opt.optim_cls(self.component[name].parameters(), **opt.optim_params) + if opt.scheduler_cls: + self.scheduler[name] = opt.scheduler_cls(self.optimizer[name], **opt.scheduler_params) + else: + self.optimizer = optim_option.optim_cls(self.parameters(), **optim_option.optim_params) + if optim_option.scheduler_cls: + self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) + + self.device = torch.device('cpu') + + @property + def trainable(self) -> bool: + return self.optimizer is not None + + @abstractmethod + def forward(self, *args, **kwargs): + raise NotImplementedError + + def step(self, loss): + """Use the loss to back-propagate gradients and apply them to the underlying parameters.""" + if self.optimizer is None: + raise MissingOptimizer("No optimizer registered to the model") + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.zero_grad() + else: + self.optimizer.zero_grad() + + # Obtain gradients through back-propagation + loss.backward() + + # Apply gradients + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.step() + else: + self.optimizer.step() + + def update_learning_rate(self, component_name: Union[str, List[str]] = None): + if not isinstance(self.scheduler, dict): + self.scheduler.step() + elif isinstance(component_name, str): + if component_name not in self.scheduler: + raise KeyError(f"Component {component_name} does not have a learning rate scheduler") + self.scheduler[component_name].step() + elif isinstance(component_name, list): + for key in component_name: + if key not in self.scheduler: + raise KeyError(f"Component {key} does not have a learning rate scheduler") + self.scheduler[key].step() + else: + for sch in self.scheduler.values(): + sch.step() + + def soft_update(self, other_model: nn.Module, tau: float): + for params, other_params in zip(self.parameters(), other_model.parameters()): + params.data = (1 - tau) * params.data + tau * other_params.data + + def copy(self, with_optimizer: bool = False): + model_copy = clone(self) + if not with_optimizer: + model_copy.optimizer = None + model_copy.scheduler = None + + return model_copy + + +class QNetForDiscreteActionSpace(AbsCoreModel): + """A compound network structure that consists of multiple task heads and an optional shared stack. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + All components must have the same input dimension except the one designated as the shared + component by ``shared_component_name``. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for + the components. Defaults to None. + """ + @abstractmethod + def forward(self, states) -> torch.tensor: + raise NotImplementedError + + def choose_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the maximum Q-value and + the corresponding action index for each state. + """ + q_for_all_actions = self.forward(states) # (batch_size, num_actions) + greedy_q, actions = q_for_all_actions.max(dim=1) + return actions.detach(), greedy_q.detach() + + def q_values(self, states, actions: torch.tensor): + if len(actions.shape) == 1: + actions = actions.unsqueeze(dim=1) + q_for_all_actions = self.forward(states) # (batch_size, num_actions) + return q_for_all_actions.gather(1, actions).squeeze(dim=1) + + +class PolicyNetForDiscreteActionSpace(AbsCoreModel): + """A compound network structure that consists of multiple task heads and an optional shared stack. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + All components must have the same input dimension except the one designated as the shared + component by ``shared_component_name``. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for + the components. Defaults to None. + """ + @abstractmethod + def forward(self, states) -> torch.tensor: + raise NotImplementedError + + def choose_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the maximum Q-value and + the corresponding action index for each state. + """ + action_prob = Categorical(self.forward(states)) # (batch_size, num_actions) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + return action, log_p + + +class PolicyValueNetForDiscreteActionSpace(AbsCoreModel): + """A compound network structure that consists of multiple task heads and an optional shared stack. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + All components must have the same input dimension except the one designated as the shared + component by ``shared_component_name``. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for + the components. Defaults to None. + """ + @abstractmethod + def forward(self, states, output_action_probs: bool = True, output_values: bool = True): + raise NotImplementedError + + def choose_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the maximum Q-value and + the corresponding action index for each state. + """ + action_prob = Categorical(self.forward(states, output_values=False)) # (batch_size, num_actions) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + return action, log_p + + +class PolicyValueNetForContinuousActionSpace(AbsCoreModel): + """A compound network structure that consists of multiple task heads and an optional shared stack. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + All components must have the same input dimension except the one designated as the shared + component by ``shared_component_name``. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for + the components. Defaults to None. + """ + @abstractmethod + def forward(self, states, actions, output_action: bool = True, output_values: bool = True): + raise NotImplementedError + + def choose_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the maximum Q-value and + the corresponding action index for each state. + """ + return self.forward(states, output_values=False) diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py new file mode 100644 index 000000000..dde452fac --- /dev/null +++ b/maro/rl/policy/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .multi_agent_policy import MultiAgentPolicy +from .policy import AbsCorePolicy, AbsFixedPolicy, NullPolicy, RLPolicy, TrainingLoopConfig + +__all__ = ["AbsCorePolicy", "AbsFixedPolicy", "MultiAgentPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig"] diff --git a/maro/rl/policy/multi_agent_policy.py b/maro/rl/policy/multi_agent_policy.py new file mode 100644 index 000000000..f69367d5c --- /dev/null +++ b/maro/rl/policy/multi_agent_policy.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import pickle +import warnings +from collections import defaultdict, namedtuple +from typing import Dict, List, Union + +from maro.rl.exploration import AbsExploration, NullExploration +from maro.rl.experience import ExperienceMemory + +from .policy import AbsFixedPolicy, AbsCorePolicy + + +class MultiAgentPolicy: + """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. + + Args: + + """ + def __init__( + self, + policy_dict: Dict[str, Union[AbsFixedPolicy, AbsCorePolicy]], + agent_to_policy: Dict[str, str], + exploration_dict: Dict[str, AbsExploration] = None, + agent_to_exploration: Dict[str, str] = None + ): + self.policy_dict = policy_dict + self.agent_to_policy = agent_to_policy + self.policy = { + agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items() + } + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent_to_policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + for policy_id, agent_ids in self.agent_groups_by_policy.items(): + self.agent_groups_by_policy[policy_id] = tuple(agent_ids) + + self.exploration_dict = exploration_dict + if exploration_dict: + self.agent_to_exploration = agent_to_exploration + self.exploration = { + agent_id: self.exploration_dict[exploration_id] + for agent_id, exploration_id in self.agent_to_exploration.items() + } + self.with_exploration = True + self.agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in agent_to_exploration.items(): + self.agent_groups_by_exploration[exploration_id].append(agent_id) + + for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): + self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) + + def train_mode(self): + self.with_exploration = True + + def eval_mode(self): + self.with_exploration = False + + @property + def exploration_params(self): + if hasattr(self, "exploration"): + return { + agent_ids: self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self.agent_groups_by_exploration.items() + } + + def choose_action(self, state_by_agent: dict): + if self.exploration_dict and self.with_exploration: + return { + agent_id: + self.exploration[agent_id](self.policy[agent_id].choose_action(state)) + if agent_id in self.exploration else self.policy[agent_id].choose_action(state) + for agent_id, state in state_by_agent.items() + } + + return {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} + + def store_experiences(self, experiences_by_agent: dict): + for agent_id, exp in experiences_by_agent.items(): + if isinstance(self.policy[agent_id], AbsCorePolicy): + self.policy[agent_id].store_experiences(exp) + + def update(self) -> List[str]: + return [ + policy_id for policy_id, policy in self.policy_dict.items() + if isinstance(policy, AbsCorePolicy) and policy.update() + ] + + def exploration_step(self): + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + + def load_state(self, policy_state_dict: dict): + """Load policies from memory.""" + if not policy_state_dict.keys() <= self.policy_dict.keys(): + raise Exception(f"Expected policies from {list(self.policy_state_dict.keys())}") + + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].load_state(policy_state) + + def state(self): + return { + policy_id: policy.state() for policy_id, policy in self.policy_dict.items() + if isinstance(policy, AbsCorePolicy) + } + + def load(self, dir_path: str): + """Load models from disk.""" + for policy_id, policy in self.policy_dict.items(): + try: + policy.load(os.path.join(dir_path, policy_id)) + except FileNotFoundError: + warnings.warn(f"policy {policy_id} is skipped because no file is found") + + def save(self, dir_path: str): + os.makedirs(dir_path, exist_ok=True) + for policy_id, policy in self.policy_dict.items(): + policy.save(os.path.join(dir_path, policy_id)) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py new file mode 100644 index 000000000..e82f19e59 --- /dev/null +++ b/maro/rl/policy/policy.py @@ -0,0 +1,162 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from collections import namedtuple + +from maro.rl.exploration import AbsExploration +from maro.rl.experience import ExperienceMemory, ExperienceSet + + +class TrainingLoopConfig: + __slots__ = [ + "sampler_cls", "batch_size", "train_iters", "sampler_kwargs", "new_experience_trigger", + "num_warmup_experiences" + ] + + def __init__( + self, + sampler_cls, + batch_size: int, + train_iters: int, + sampler_kwargs: dict = None, + new_experience_trigger: int = 1, + num_warmup_experiences: int = 1 + ): + self.sampler_cls = sampler_cls + self.batch_size = batch_size + self.train_iters = train_iters + self.sampler_kwargs = sampler_kwargs if sampler_kwargs else {} + self.new_experience_trigger = new_experience_trigger + self.num_warmup_experiences = num_warmup_experiences + + +class AbsFixedPolicy(ABC): + """Abstract fixed policy class. + + Args: + config: Settings for the algorithm. + """ + def __init__(self): + pass + + @abstractmethod + def choose_action(self, state): + """Compute an action based on a state object. + + Args: + state: State object. + + Returns: + The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + raise NotImplementedError + + +class NullPolicy(AbsFixedPolicy): + def choose_action(self, state): + return None + + +class AbsCorePolicy(ABC): + def __init__(self, experience_memory: ExperienceMemory, generic_config: TrainingLoopConfig, special_config): + self.experience_memory = experience_memory + self.generic_config = generic_config + self.special_config = special_config + sampler_cls, batch_size = generic_config.sampler_cls, generic_config.batch_size + self.sampler = sampler_cls(experience_memory, batch_size, **generic_config.sampler_kwargs) + self._last_exp_mem_size = 0 # experience memory size when the last update was made + self._warm_up = True + self._update_ready = False + + @abstractmethod + def choose_action(self, state): + """Compute an action based on a state object. + + Args: + state: State object. + + Returns: + The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + raise NotImplementedError + + def store_experiences(self, experience_set: ExperienceSet): + self.experience_memory.put(experience_set) + exp_mem_size = len(self.experience_memory) + self._warm_up = exp_mem_size < self.generic_config.num_warmup_experiences + self._update_ready = (exp_mem_size - self._last_exp_mem_size) >= self.generic_config.new_experience_trigger + + def update(self): + if self._warm_up or not self._update_ready: + return False + + self._last_exp_mem_size = len(self.experience_memory) + exp_mem, sampler, config = self.experience_memory, self.sampler, self.generic_config + for _ in range(self.generic_config.train_iters): + self.learn(self.experience_memory.get(self.sampler.sample())) + return True + + return False + + @abstractmethod + def learn(self, experience_set: ExperienceSet): + raise NotImplementedError + + def state(self): + pass + + def load_state(self, policy_state): + pass + + def load(self, path: str): + pass + + def save(self, path: str): + pass + + +class RLPolicy(object): + """Abstract fixed policy class. + + Args: + config: Settings for the algorithm. + """ + def __init__(self, core_policy: AbsCorePolicy, exploration: AbsExploration = None): + self.core_policy = core_policy + self.exploration = exploration + + def choose_action(self, state): + """Compute an action based on a state object. + + Args: + state: State object. + + Returns: + The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert + this to an environment executable action. + """ + action = self.core_policy.choose_action(state) + return self.exploration(action) + + def update(self): + """Algorithm-specific training logic. + + The parameters are data to train the underlying model on. Algorithm-specific loss and optimization + should be reflected here. + """ + self.core_policy.update() + + def learn(self, experience_set: ExperienceSet): + return self.core_policy.learn(experience_set) + + def load_state(self, policy_state): + self.core_policy.load_state(policy_state) + + def load(self, path: str): + self.core_policy.load(path) + + def save(self, path: str): + self.core_policy.save(path) diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 3ecff3bc9..e369990d8 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .env_wrapper import AbsEnvWrapper +from .actor import Actor +from .actor_manager import ActorManager from .learner import Learner -__all__ = ["AbsEnvWrapper", "Learner"] +__all__ = ["Actor", "ActorManager", "Learner"] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py new file mode 100644 index 000000000..863cd824a --- /dev/null +++ b/maro/rl/training/actor.py @@ -0,0 +1,100 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from os import getcwd +from typing import Union + +from maro.communication import Proxy +from maro.rl.policy import MultiAgentPolicy +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.utils import Logger + +from .message_enums import MsgKey, MsgTag + + +class Actor(object): + """On-demand roll-out executor. + + Args: + env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific + processing logic and stores transitions during roll-outs in a replay memory. + policy (MultiAgentPolicy): Agent that interacts with the environment. + group (str): Identifier of the group to which the actor belongs. It must be the same group name + assigned to the learner (and decision clients, if any). + proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to None. + """ + def __init__( + self, + env: AbsEnvWrapper, + policy: MultiAgentPolicy, + group: str, + eval_env: AbsEnvWrapper = None, + proxy_options: dict = None, + log_dir: str = getcwd() + ): + self.env = env + self.eval_env = eval_env if eval_env else self.env + self.policy = policy + if proxy_options is None: + proxy_options = {} + self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) + self._logger = Logger(self._proxy.name, dump_folder=log_dir) + + def run(self): + for msg in self._proxy.receive(): + if msg.tag == MsgTag.EXIT: + self._logger.info("Exiting...") + break + + if msg.tag == MsgTag.COLLECT: + self.policy.train_mode() + episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] + if self.env.state is None: + self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") + self._logger.debug(f"Exploration parameters: {self.policy.exploration_params}") + self.env.reset() + self.env.start() # get initial state + + starting_step_index = self.env.step_index + 1 + self.policy.load_state(msg.body[MsgKey.POLICY]) + steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] + while self.env.state and steps_to_go > 0: + action = self.policy.choose_action(self.env.state) + self.env.step(action) + steps_to_go -= 1 + + self._logger.info( + f"Roll-out finished for ep {episode_index}, segment {segment_index}" + f"(steps {starting_step_index} - {self.env.step_index})" + ) + return_info = { + MsgKey.ENV_END: not self.env.state, + MsgKey.EPISODE_INDEX: episode_index, + MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.EXPERIENCES: self.env.get_experiences(), + MsgKey.NUM_STEPS: self.env.step_index - starting_step_index + 1 + } + + if msg.body[MsgKey.RETURN_ENV_METRICS]: + return_info[MsgKey.METRICS] = self.env.metrics + if not self.env.state: + self.policy.exploration_step() + return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward + self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) + elif msg.tag == MsgTag.EVAL: + ep = msg.body[MsgKey.EPISODE_INDEX] + self._logger.info(f"Evaluation episode {ep}") + self.policy.eval_mode() + self.eval_env.reset() + self.eval_env.start() # get initial state + self.policy.load_state(msg.body[MsgKey.POLICY]) + while self.eval_env.state: + self.eval_env.step(self.policy.choose_action(self.eval_env.state)) + + return_info = { + MsgKey.METRICS: self.env.metrics, + MsgKey.TOTAL_REWARD: self.eval_env.total_reward, + MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] + } + self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) diff --git a/maro/rl/training/actor_manager.py b/maro/rl/training/actor_manager.py new file mode 100644 index 000000000..d21f509c2 --- /dev/null +++ b/maro/rl/training/actor_manager.py @@ -0,0 +1,141 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict +from os import getcwd +from random import choices +from typing import Union + +from maro.communication import Proxy, SessionType +from maro.utils import Logger + +from .message_enums import MsgTag, MsgKey + + +class ActorManager(object): + """Learner class for distributed training. + + Args: + agent (Union[AbsPolicy, MultiAgentWrapper]): Learning agents. + scheduler (Scheduler): . + num_actors (int): Expected number of actors in the group identified by ``group_name``. + group_name (str): Identifier of the group to which the actor belongs. It must be the same group name + assigned to the actors (and roll-out clients, if any). + proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to None. + update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger + learner updates, i.e., model training. + """ + def __init__( + self, + num_actors: int, + group_name: str, + proxy_options: dict = None, + log_env_metrics: bool = False, + log_dir: str = getcwd() + ): + super().__init__() + self.num_actors = num_actors + peers = {"actor": num_actors} + if proxy_options is None: + proxy_options = {} + self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_options) + self._actors = self._proxy.peers["actor"] # remote actor ID's + self.total_experiences_collected = 0 + self.total_env_steps = 0 + self.total_reward = defaultdict(float) + self._log_env_metrics = log_env_metrics + self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) + + def collect( + self, + episode_index: int, + segment_index: int, + num_steps: int, + policy_dict: dict = None, + exploration=None, + required_actor_finishes: int = None, + discard_stale_experiences: bool = True, + return_env_metrics: bool = False + ): + """Collect experiences from actors.""" + msg_body = { + MsgKey.EPISODE_INDEX: episode_index, + MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.NUM_STEPS: num_steps, + MsgKey.POLICY: policy_dict, + MsgKey.RETURN_ENV_METRICS: return_env_metrics + } + + if self._log_env_metrics: + self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") + if exploration_params: + self._logger.info(f"exploration_params: {exploration_params}") + + self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) + self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") + + # Receive roll-out results from remote actors + num_finishes = 0 + for msg in self._proxy.receive(): + if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: + self._logger.info( + f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.EPISODE_INDEX]} " + f"(expected message type {MsgTag.COLLECT} and roll-out index {episode_index})" + ) + continue + + # log roll-out summary + if self._log_env_metrics: + env_metrics = msg.body[MsgKey.METRICS] + self._logger.info(f"env_metrics: {env_metrics}") + + if msg.body[MsgKey.SEGMENT_INDEX] == segment_index or not discard_stale_experiences: + experiences_by_agent = msg.body[MsgKey.EXPERIENCES] + self.total_experiences_collected += sum(len(exp) for exp in experiences_by_agent.values()) + self.total_env_steps += msg.body[MsgKey.NUM_STEPS] + is_env_end = msg.body[MsgKey.ENV_END] + if is_env_end: + self._logger.info(f"total rewards: {msg.body[MsgKey.TOTAL_REWARD]}") + yield experiences_by_agent, is_env_end + + if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: + num_finishes += 1 + if num_finishes == required_actor_finishes: + break + + def evaluate(self, episode_index: int, policy_dict: dict, num_actors: int): + """Collect experiences from actors.""" + msg_body = { + MsgKey.EPISODE_INDEX: episode_index, + MsgKey.POLICY: policy_dict, + MsgKey.RETURN_ENV_METRICS: True + } + + actors = choices(self._actors, k=num_actors) + self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(actor_id, msg_body) for actor_id in actors]) + self._logger.info(f"Sent evaluation requests to {actors}") + + # Receive roll-out results from remote actors + num_finishes = 0 + for msg in self._proxy.receive(): + if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: + self._logger.info( + f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE_INDEX]} " + f"(expected message type {MsgTag.EVAL} and episode index {episode_index})" + ) + continue + + # log roll-out summary + env_metrics = msg.body[MsgKey.METRICS] + self._logger.info(f"env metrics for evaluation episode {episode_index}: {env_metrics}") + + if msg.body[MsgKey.EPISODE_INDEX] == episode_index: + num_finishes += 1 + if num_finishes == num_actors: + break + + def exit(self): + """Tell the remote actors to exit.""" + self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) + self._logger.info("Exiting...") diff --git a/maro/rl/training/dispatcher.py b/maro/rl/training/dispatcher.py new file mode 100755 index 000000000..b574e51ac --- /dev/null +++ b/maro/rl/training/dispatcher.py @@ -0,0 +1,58 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import deque + +import zmq +from zmq.eventloop.ioloop import IOLoop +from zmq.eventloop.zmqstream import ZMQStream + + +class LRUQueue(object): + """LRUQueue class using ZMQStream/IOLoop for event dispatching. + + Code adapted from https://zguide.zeromq.org/docs/chapter3/#A-High-Level-API-for-ZeroMQ. + """ + def __init__(self): + self._context = zmq.Context.instance() + frontend = self._context.socket(zmq.ROUTER) + frontend.bind("tcp://127.0.0.1:50000") + backend = self._context.socket(zmq.ROUTER) + backend.bind("tcp://127.0.0.1:50001") + self._workers = deque() + + self._frontend = ZMQStream(frontend) + self._backend = ZMQStream(backend) + self._backend.on_recv(self._handle_backend) + + def _handle_backend(self, msg): + # Queue worker ID for LRU routing + worker, empty, client = msg[:3] + # add worker back to the list of workers + self._workers.append(worker) + assert empty == b"" + # Third frame is READY or else a client reply address + # If client reply, send rest back to frontend + if client != b"READY": + empty, reply = msg[3:] + assert empty == b"" + self._frontend.send_multipart([client, b'', reply]) + + # Start accepting frontend messages now that at least one worker is free. + self._frontend.on_recv(self._handle_frontend) + + def _handle_frontend(self, msg): + # Now get next client request, route to LRU worker + # Client request is [address][empty][request] + client, empty, request = msg + assert empty == b"" + # Dequeue and drop the next worker address + self._backend.send_multipart([self._workers.popleft(), b'', client, b'', request]) + if not self._workers: + # stop receiving until workers become available again + self._frontend.stop_on_recv() + + +def start_dispatcher(): + dispatcher = LRUQueue() + IOLoop.instance().start() diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 92f3b9d92..7e6bfa831 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -3,71 +3,188 @@ import time from collections import defaultdict -from typing import Dict, Union +from os import getcwd +from typing import Callable, Dict, List, Union -from maro.rl.agent import AbsAgent, MultiAgentWrapper -from maro.rl.scheduling import Scheduler -from maro.utils import InternalLogger +from maro.communication import Message, Proxy, SessionType +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.policy import AbsCorePolicy, MultiAgentPolicy +from maro.utils import Logger -from .env_wrapper import AbsEnvWrapper +from .actor_manager import ActorManager +from .message_enums import MsgTag, MsgKey class Learner(object): """Learner class for distributed training. Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific - processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsAgent, MultiAgentWrapper]): Agent that interacts with the environment. + policy (MultiAgentPolicy): Learning agents. + """ def __init__( self, + policy: MultiAgentPolicy, env: AbsEnvWrapper, - agent: Union[AbsAgent, MultiAgentWrapper], - scheduler: Scheduler, - agent_update_interval: int = -1, - log_env_metrics: bool = False + num_episodes: int, + eval_env: AbsEnvWrapper = None, + actor_manager: ActorManager = None, + num_eval_actors: int = 1, + policy_update_interval: int = -1, + eval_points: Union[int, List[int]] = None, + required_actor_finishes: str = None, + discard_stale_experiences: bool = True, + log_env_metrics: bool = True, + log_dir: str = getcwd(), + end_of_training_kwargs: dict = None ): - super().__init__() - if agent_update_interval == 0: - raise ValueError("agent_update_interval must be a positive integer or None.") + if env is None and actor_manager is None: + raise Exception("env and actor_manager cannot both be None") + + self._logger = Logger("LEARNER", dump_folder=log_dir) + self.policy = policy self.env = env - self.agent = MultiAgentWrapper(agent) if isinstance(agent, AbsAgent) else agent - self.scheduler = scheduler - self.agent_update_interval = agent_update_interval - self.total_env_steps = 0 - self.total_experiences_collected = 0 - self.total_learning_time = 0 + self.num_episodes = num_episodes + self.eval_env = eval_env if eval_env else self.env + self.policy_update_interval = policy_update_interval + + if actor_manager: + self._logger.info( + "Found actor manager. Roll-outs will be performed by remote actors. Local env will not be used." + ) + self.actor_manager = actor_manager + + self.num_eval_actors = num_eval_actors + + # evaluation points + if eval_points is None: + eval_points = [] + elif isinstance(eval_points, int): + num_eval_points = num_episodes // eval_points + eval_points = [eval_points * i for i in range(1, num_eval_points + 1)] + + self.eval_points = eval_points + self.eval_points.sort() + if not self.eval_points or num_episodes != self.eval_points[-1]: + self.eval_points.append(num_episodes) + + self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_points}") + self._eval_point_index = 0 + + # distributed roll-out + self.actor_manager = actor_manager + if self.actor_manager: + if required_actor_finishes and required_actor_finishes > self.actor_manager.num_actors: + raise ValueError("required_actor_finishes cannot exceed the number of available actors") + + if required_actor_finishes is None: + required_actor_finishes = self.actor_manager.num_actors + self._logger.info(f"Required number of actor finishes is set to {required_actor_finishes}") + + self.required_actor_finishes = required_actor_finishes + self.discard_stale_experiences = discard_stale_experiences + + self.end_of_training_kwargs = end_of_training_kwargs if end_of_training_kwargs else {} self._log_env_metrics = log_env_metrics - self._logger = InternalLogger("LEARNER") + self._total_learning_time = 0 + self._total_env_steps = 0 + self._total_experiences_collected = 0 def run(self): - t0 = time.time() - for exploration_params in self.scheduler: - self.env.reset() - if exploration_params: - self.agent.set_exploration_params(exploration_params) - - self.env.start(rollout_index=self.scheduler.iter) # get initial state - while self.env.state: - action = self.agent.choose_action(self.env.state) - self.env.step(action) - if ( - not self.env.state or - self.agent_update_interval != -1 and self.env.step_index % self.agent_update_interval == 0 - ): - exp, num_exp = self.env.pull_experiences() - tl0 = time.time() - self.agent.learn(exp) - self.total_learning_time += time.time() - tl0 - self.total_env_steps += self.agent_update_interval - self.total_experiences_collected += num_exp - self._logger.debug(f"total running time: {time.time() - t0}") - self._logger.debug(f"total learning time: {self.total_learning_time}") - self._logger.debug(f"total env steps: {self.total_env_steps}") - self._logger.info(f"total experiences collected: {self.total_experiences_collected}") - if not self.env.state: - self._logger.info(f"total reward: {self.env.total_reward}") + for ep in range(1, self.num_episodes + 1): + self.train(ep) + if ep == self.eval_points[self._eval_point_index]: + self._eval_point_index += 1 + self.evaluate(self._eval_point_index) + + if self.actor_manager: + self.actor_manager.exit() + + def train(self, ep: int): + # local mode + if not self.actor_manager: + self._train_local(ep) + else: + self._train_remote(ep) + + def evaluate(self, ep: int): + self._logger.info("Evaluating...") + if self.eval_env: + self.policy.eval_mode() + self.eval_env.save_replay = False + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = self.policy.choose_action(self.eval_env.state) + self.eval_env.step(action) + + if not self.eval_env.state: + self._logger.info(f"total reward: {self.eval_env.total_reward}") if self._log_env_metrics: - self._logger.info(f"ep-{self.scheduler.iter}: {self.env.metrics} ({exploration_params})") + self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") + else: + self.actor_manager.evaluate(ep, self.policy.state(), self.num_eval_actors) + + def _train_local(self, ep: int): + t0 = time.time() + segement_index = 1 + self._logger.info(f"Training episode {ep}") + self._logger.debug(f"exploration parameters: {self.policy.exploration_params}") + self.policy.train_mode() + self.env.save_replay = True + self.env.reset() + self.env.start() # get initial state + while self.env.state: + action = self.policy.choose_action(self.env.state) + self.env.step(action) + if ( + not self.env.state or + self.policy_update_interval != -1 and self.env.step_index % self.policy_update_interval == 0 + ): + exp_by_agent = self.env.get_experiences() + tl0 = time.time() + self.policy.store_experiences(exp_by_agent) + updated_policy_ids = self.policy.update() + self.end_of_training(ep, segement_index, **self.end_of_training_kwargs) + self._total_learning_time += time.time() - tl0 + self._total_env_steps += self.policy_update_interval + self._total_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) + self._logger.debug(f"total running time: {time.time() - t0}") + self._logger.debug(f"total learning time: {self._total_learning_time}") + self._logger.debug(f"total env steps: {self._total_env_steps}") + self._logger.debug(f"total experiences collected: {self._total_experiences_collected}") + if not self.env.state: + self._logger.info(f"total reward: {self.env.total_reward}") + + segement_index += 1 + + self.policy.exploration_step() + if self._log_env_metrics: + self._logger.info(f"ep {ep}: {self.env.metrics}") + + def _train_remote(self, ep: int): + t0 = time.time() + updated_policy_ids, num_actor_finishes, segment_index = list(self.policy.policy_dict.keys()), 0, 0 + while num_actor_finishes < self.required_actor_finishes: + for exp, done in self.actor_manager.collect( + ep, segment_index, self.policy_update_interval, + policy_dict=self.policy.state(), + required_actor_finishes=self.required_actor_finishes, + discard_stale_experiences=self.discard_stale_experiences + ): + tl0 = time.time() + self.policy.store_experiences(exp) + updated_policy_ids = self.policy.update() + self.end_of_training(ep, segment_index, **self.end_of_training_kwargs) + num_actor_finishes += done + self._total_learning_time += time.time() - tl0 + self._logger.debug(f"running time: {time.time() - t0}") + self._logger.debug(f"learning time: {self._total_learning_time}") + self._logger.debug(f"env steps: {self.actor_manager.total_env_steps}") + self._logger.debug(f"experiences collected: {self.actor_manager.total_experiences_collected}") + + segment_index += 1 + + def end_of_training(self, ep: int, segment: int, **kwargs): + pass diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py new file mode 100644 index 000000000..ad27abcb3 --- /dev/null +++ b/maro/rl/training/message_enums.py @@ -0,0 +1,33 @@ +from enum import Enum + + +class MsgTag(Enum): + COLLECT = "rollout" + EVAL = "eval" + AGENT_UPDATE = "agent_update" + CHOOSE_ACTION = "choose_action" + ACTION = "action" + EXPERIENCE_SYNC = "experience_sync" + TRAIN = "train" + ABORT_ROLLOUT = "abort_rollout" + EVAL_DONE = "eval_done" + COLLECT_DONE = "collect_done" + EXIT = "exit" + + +class MsgKey(Enum): + ACTION = "action" + AGENT_ID = "agent_id" + EPISODE_INDEX = "episode_index" + SEGMENT_INDEX = "segment_index" + TIME_STEP = "time_step" + METRICS = "metrics" + EXPERIENCES = "experiences" + NUM_EXPERIENCES = "num_experiences" + STATE = "state" + POLICY = "policy" + VERSION = "version" + NUM_STEPS = "num_steps" + RETURN_ENV_METRICS = "return_env_metrics" + TOTAL_REWARD = "total_reward" + ENV_END = "env_end" diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py new file mode 100755 index 000000000..c3ee0aa01 --- /dev/null +++ b/maro/rl/training/trainer.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import pickle +from collections import deque + +import zmq +from zmq.eventloop.ioloop import IOLoop +from zmq.eventloop.zmqstream import ZMQStream + + +def trainer(id_: str): + socket = zmq.Context().socket(zmq.REQ) + socket.setsockopt_string(zmq.IDENTITY, f"Trainer_{id_}") + socket.connect("tcp://127.0.0.1:50001") + socket = ZMQStream(socket) + socket.send(b"READY") # register to the dispatcher + + def train(sock, msg): + client, _, request = msg + request = pickle.loads(request) + info = request["agent"].step(*request["args"], **request["kwargs"]) + request.update({"model": request["agent"].dump_model(), "info": info}) + del request["agent"] + del request["args"] + del request["kwargs"] + sock.send_multipart([client, b"", pickle.dumps(request)]) + + socket.on_recv_stream(train) + IOLoop.instance().start() diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 69a92edb2..4797c39c9 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,16 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .sampler_cls_index import get_sampler_cls from .torch_cls_index import ( get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls ) from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_truncated_cumulative_reward -from .value_utils import get_log_prob, get_max, get_td_errors, select_by_actions __all__ = [ - "get_sampler_cls", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", - "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward", "get_log_prob", "get_max", - "get_td_errors", "select_by_actions" + "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/utils/torch_cls_index.py b/maro/rl/utils/torch_cls_index.py index 428b3879d..5d1ed29b8 100644 --- a/maro/rl/utils/torch_cls_index.py +++ b/maro/rl/utils/torch_cls_index.py @@ -85,7 +85,7 @@ def get_torch_activation_cls(activation_type): if isinstance(activation_type, str): if activation_type not in TORCH_ACTIVATION: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_ACTIVATION.keys())}.") + raise KeyError(f"A string activation_type must be one of {list(TORCH_ACTIVATION.keys())}.") return TORCH_ACTIVATION[activation_type] return activation_type @@ -94,7 +94,7 @@ def get_torch_activation_cls(activation_type): def get_torch_loss_cls(loss_type): if isinstance(loss_type, str): if loss_type not in TORCH_LOSS: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_LOSS.keys())}.") + raise KeyError(f"A string loss_type must be one of {list(TORCH_LOSS.keys())}.") return TORCH_LOSS[loss_type] return loss_type @@ -103,7 +103,7 @@ def get_torch_loss_cls(loss_type): def get_torch_optim_cls(optim_type): if isinstance(optim_type, str): if optim_type not in TORCH_OPTIM: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_OPTIM.keys())}.") + raise KeyError(f"A string optim_type must be one of {list(TORCH_OPTIM.keys())}.") return TORCH_OPTIM[optim_type] return optim_type @@ -112,7 +112,7 @@ def get_torch_optim_cls(optim_type): def get_torch_lr_scheduler_cls(lr_scheduler_type): if isinstance(lr_scheduler_type, str): if lr_scheduler_type not in TORCH_LR_SCHEDULER: - raise KeyError(f"A string optim_cls must be one of {list(TORCH_LR_SCHEDULER.keys())}.") + raise KeyError(f"A string lr_scheduler_type must be one of {list(TORCH_LR_SCHEDULER.keys())}.") return TORCH_LR_SCHEDULER[lr_scheduler_type] return lr_scheduler_type diff --git a/maro/utils/exception/error_code.py b/maro/utils/exception/error_code.py index 7d3f9c13d..d11cabbae 100644 --- a/maro/utils/exception/error_code.py +++ b/maro/utils/exception/error_code.py @@ -47,7 +47,6 @@ 3003: "Deployment Error", # 4000-4999: Error codes for RL toolkit - 4000: "Store Misalignment", + 4000: "InvalidExperience", 4001: "Missing Optimizer", - 4002: "Unrecognized Task", } diff --git a/maro/utils/exception/rl_toolkit_exception.py b/maro/utils/exception/rl_toolkit_exception.py index 759b8ac86..271b2b92e 100644 --- a/maro/utils/exception/rl_toolkit_exception.py +++ b/maro/utils/exception/rl_toolkit_exception.py @@ -4,9 +4,11 @@ from .base_exception import MAROException -class StoreMisalignment(MAROException): - """Raised when a ``put`` operation on a ``SimpleStore`` would cause the underlying lists to have different - sizes.""" +class InvalidExperience(MAROException): + """ + Raised when the states, actions, rewards and next states passed to an ``ExperienceSet`` do not + have the same length. + """ def __init__(self, msg: str = None): super().__init__(4000, msg) @@ -15,9 +17,3 @@ class MissingOptimizer(MAROException): """Raised when the optimizers are missing when calling CoreModel's step() method.""" def __init__(self, msg: str = None): super().__init__(4001, msg) - - -class UnrecognizedTask(MAROException): - """Raised when a CoreModel has task names that are not unrecognized by an algorithm.""" - def __init__(self, msg: str = None): - super().__init__(4002, msg) From 6100491bf804d2a179758f000700a28a2722ff85 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 30 Apr 2021 19:41:21 +0800 Subject: [PATCH 209/482] small edits to logs --- maro/rl/training/actor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index e0975669f..863cd824a 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -51,7 +51,8 @@ def run(self): self.policy.train_mode() episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] if self.env.state is None: - self._logger.info(f"Training with exploration parameters: {self.policy.exploration_params}") + self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") + self._logger.debug(f"Exploration parameters: {self.policy.exploration_params}") self.env.reset() self.env.start() # get initial state From 54aba9f3c0741f777770862e69922332afff57c1 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 30 Apr 2021 11:42:09 +0000 Subject: [PATCH 210/482] checked out files from v0.2_rl_refinement --- maro/rl/model/fc_block.py | 5 ++--- maro/rl/training/actor.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/maro/rl/model/fc_block.py b/maro/rl/model/fc_block.py index ba659186e..a648a3fd3 100644 --- a/maro/rl/model/fc_block.py +++ b/maro/rl/model/fc_block.py @@ -6,8 +6,7 @@ import torch import torch.nn as nn -from maro.rl.cls_index import TORCH_ACTIVATION_CLS -from maro.rl.utils import get_cls +from maro.rl.utils import get_torch_activation_cls from .abs_block import AbsBlock @@ -54,7 +53,7 @@ def __init__( self._output_dim = output_dim # network features - self._activation = get_cls(activation, TORCH_ACTIVATION_CLS)() if activation else None + self._activation = get_torch_activation_cls(activation)() if activation else None self._head = head self._softmax = nn.Softmax(dim=1) if softmax else None self._batch_norm = batch_norm diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 863cd824a..e0975669f 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -51,8 +51,7 @@ def run(self): self.policy.train_mode() episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] if self.env.state is None: - self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") - self._logger.debug(f"Exploration parameters: {self.policy.exploration_params}") + self._logger.info(f"Training with exploration parameters: {self.policy.exploration_params}") self.env.reset() self.env.start() # get initial state From 4c16963bc3960c94053d9b66f86af5fc1f940340 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 30 Apr 2021 12:01:25 +0000 Subject: [PATCH 211/482] modified supply chain code for docker-compose --- examples/supply_chain/config.py | 2 +- examples/supply_chain/config.yml | 2 +- examples/supply_chain/docker-compose.yml | 2 +- examples/supply_chain/env_wrapper.py | 68 ++++++++++++++---------- examples/supply_chain/exploration.py | 2 +- examples/supply_chain/policies.py | 4 +- maro/rl/training/actor.py | 3 +- 7 files changed, 48 insertions(+), 35 deletions(-) diff --git a/examples/supply_chain/config.py b/examples/supply_chain/config.py index bdfd1002a..ab4987d14 100644 --- a/examples/supply_chain/config.py +++ b/examples/supply_chain/config.py @@ -23,4 +23,4 @@ consumer_agent_ids = [f"consumer.{info.id}" for info in env.agent_idx_list] producer_agent_ids = [f"producer.{info.id}" for info in env.agent_idx_list] config["agent_ids"] = consumer_agent_ids + producer_agent_ids -config["policy"]["consumer"]["model"]["input_dim"] = env.dim +config["policy"]["consumer"]["model"]["network"]["input_dim"] = env.dim diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 294d2026f..1212fef91 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -67,7 +67,7 @@ distributed: # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make # sure that a local redis server is running and listening on the port specified by "redis_port". - redis_host: localhost + redis_host: maro-redis redis_port: 6379 # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent # slow actors from dragging down the whole process. diff --git a/examples/supply_chain/docker-compose.yml b/examples/supply_chain/docker-compose.yml index 949068879..c00bd275d 100644 --- a/examples/supply_chain/docker-compose.yml +++ b/examples/supply_chain/docker-compose.yml @@ -53,6 +53,6 @@ services: - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl redis: - container_name: redis-sc + container_name: maro-redis image: redis:6 version: '3.9' diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 4bc034295..7f136094c 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -202,7 +202,7 @@ def get_state(self, event): for sku_id, number in in_transit_orders.items(): self._facility_in_transit_orders[facility_id][sku_id] = number - final_state = {"consumer": {}, "producer": {}} + final_state = {} # calculate storage info first, then use it later to speed up. for facility_id, storage_index in self._facility2storage_index_dict.items(): @@ -234,12 +234,12 @@ def get_state(self, event): np_state = self._serialize_state(state) - final_state["consumer"][agent_info.id] = np_state - final_state["producer"][agent_info.id] = np_state + final_state[f"consumer.{agent_info.id}"] = np_state + final_state[f"producer.{agent_info.id}"] = np_state return final_state - def get_reward(self, tick=None): + def get_reward(self, tick=None, target_agents=None): wc = self.env.configs.settings["global_reward_weight_consumer"] parent_facility_balance = {} for f_id, sheet in self.cur_balance_sheet_reward.items(): @@ -253,11 +253,11 @@ def get_reward(self, tick=None): self.cur_balance_sheet_reward.items()} return { - "producer": {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, - "consumer": {f_id: np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} + **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, + **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} } - def get_action(self, action_info): + def get_action(self, action_by_agent): # cache the sources for each consumer if not yet cached if not hasattr(self, "product2source"): self.product2source, self.consumer2product = {}, {} @@ -272,33 +272,43 @@ def get_action(self, action_info): self.consumer2product[consumer_id] = product_id env_action = {} - # consumer action - for unit_id, action in action_info["consumer"].items(): - product_id = self.consumer2product.get(unit_id, 0) - sources = self.product2source.get(unit_id, []) - if sources: - source_id = sources[0] - action_number = int(int(action) * self._cur_metrics["products"][unit_id]["sale_mean"]) - # ignore 0 quantity to reduce action number - if action_number == 0: - continue - sku = self._units_mapping[unit_id][3] - reward_discount = 0 - env_action[unit_id] = ConsumerAction(unit_id, product_id, source_id, action_number, sku.vlt, reward_discount) + for agent_id, action in action_by_agent.items(): + unit_id = int(agent_id.split(".")[1]) - # producer action - for unit_id, action in action_info["producer"].items(): is_facility = unit_id not in self._units_mapping + # ignore facility to reduce action number if is_facility: continue - # producer action - sku = self._units_mapping[unit_id][3] - action = sku.production_rate - # ignore invalid actions - if action is None or action == 0: - continue - env_action[unit_id] = ManufactureAction(unit_id, action) + + # consumer action + if agent_id.startswith("consumer"): + product_id = self.consumer2product.get(unit_id, 0) + sources = self.product2source.get(unit_id, []) + + if sources: + source_id = sources[0] + + action_number = int(int(action) * self._cur_metrics["products"][unit_id]["sale_mean"]) + + # ignore 0 quantity to reduce action number + if action_number == 0: + continue + + sku = self._units_mapping[unit_id][3] + reward_discount = 0 + + env_action[unit_id] = ConsumerAction(unit_id, product_id, source_id, action_number, sku.vlt, reward_discount) + # manufacturer action + elif agent_id.startswith("producer"): + sku = self._units_mapping[unit_id][3] + action = sku.production_rate + + # ignore invalid actions + if action is None or action == 0: + continue + + env_action[unit_id] = ManufactureAction(unit_id, action) return env_action diff --git a/examples/supply_chain/exploration.py b/examples/supply_chain/exploration.py index 5d38d4df0..d7f933e65 100644 --- a/examples/supply_chain/exploration.py +++ b/examples/supply_chain/exploration.py @@ -11,7 +11,7 @@ from config import config -exploration = EpsilonGreedyExploration(config["policy"]["consumer"]["model"]["output_dim"]) +exploration = EpsilonGreedyExploration(config["policy"]["consumer"]["model"]["network"]["output_dim"]) exploration.register_schedule( LinearExplorationScheduler, "epsilon", config["num_episodes"], initial_value=config["exploration"]["initial_value"], diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py index ce9110e70..e0168d2ef 100644 --- a/examples/supply_chain/policies.py +++ b/examples/supply_chain/policies.py @@ -30,7 +30,9 @@ def forward(self, states): def get_dqn_policy(config): - q_net = SimpleQNet(FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"])) + q_net = SimpleQNet( + FullyConnectedBlock(**config["model"]["network"]), optim_option=OptimOption(**config["model"]["optimization"]) + ) experience_memory = ExperienceMemory(**config["experience_memory"]) config["training_loop"]["sampler_cls"] = get_sampler_cls(config["training_loop"]["sampler_cls"]) diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index e0975669f..863cd824a 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -51,7 +51,8 @@ def run(self): self.policy.train_mode() episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] if self.env.state is None: - self._logger.info(f"Training with exploration parameters: {self.policy.exploration_params}") + self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") + self._logger.debug(f"Exploration parameters: {self.policy.exploration_params}") self.env.reset() self.env.start() # get initial state From dd19d7637c13820663f47bb5d83b40197944a1d3 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 30 Apr 2021 12:03:13 +0000 Subject: [PATCH 212/482] deleted all files related to supply chain --- examples/hello_world/supply_chain/hello.py | 39 - .../supply_chain/outersample/config.yml | 208 - examples/hello_world/supply_chain/store1.csv | 481 - examples/supply_chain/README.md | 8 - examples/supply_chain/__init__.py | 2 - examples/supply_chain/config.py | 26 - examples/supply_chain/config.yml | 74 - examples/supply_chain/distributed_launcher.py | 106 - examples/supply_chain/docker-compose.yml | 58 - examples/supply_chain/env_wrapper.py | 901 - examples/supply_chain/exploration.py | 24 - examples/supply_chain/learner.py | 9 - examples/supply_chain/policies.py | 45 - examples/supply_chain/sc_state_in_maro.md | 312 - examples/supply_chain/scripts/build.sh | 5 - .../scripts/docker_compose_yml_generator.py | 44 - examples/supply_chain/scripts/kill.sh | 4 - examples/supply_chain/scripts/run.sh | 5 - .../supply_chain/single_thread_launcher.py | 35 - .../supply_chain/topologies/random/config.yml | 29337 ---------------- .../topologies/sample1/config.yml | 327 - .../scenarios/supply_chain/__init__.py | 10 - .../scenarios/supply_chain/actions.py | 9 - .../scenarios/supply_chain/business_engine.py | 172 - .../supply_chain/datamodels/__init__.py | 12 - .../scenarios/supply_chain/datamodels/base.py | 45 - .../supply_chain/datamodels/consumer.py | 47 - .../supply_chain/datamodels/distribution.py | 22 - .../supply_chain/datamodels/facility.py | 17 - .../supply_chain/datamodels/manufacture.py | 33 - .../supply_chain/datamodels/product.py | 33 - .../supply_chain/datamodels/seller.py | 38 - .../supply_chain/datamodels/skumodel.py | 33 - .../supply_chain/datamodels/storage.py | 59 - .../supply_chain/datamodels/vehicle.py | 31 - .../scenarios/supply_chain/easy_config.py | 33 - .../supply_chain/facilities/__init__.py | 9 - .../supply_chain/facilities/facility.py | 177 - .../supply_chain/facilities/outerretailer.py | 37 - .../supply_chain/facilities/retailer.py | 10 - .../supply_chain/facilities/supplier.py | 10 - .../supply_chain/facilities/warehouse.py | 10 - .../scenarios/supply_chain/frame_builder.py | 18 - .../scenarios/supply_chain/parser.py | 231 - .../supply_chain/topologies/core.yml | 81 - .../supply_chain/topologies/sample/config.yml | 297 - .../scenarios/supply_chain/units/__init__.py | 16 - .../scenarios/supply_chain/units/consumer.py | 173 - .../supply_chain/units/distribution.py | 128 - .../supply_chain/units/manufacture.py | 95 - .../scenarios/supply_chain/units/order.py | 7 - .../supply_chain/units/outerseller.py | 118 - .../scenarios/supply_chain/units/product.py | 175 - .../scenarios/supply_chain/units/seller.py | 92 - .../supply_chain/units/simplemanufacture.py | 21 - .../scenarios/supply_chain/units/skuunit.py | 25 - .../scenarios/supply_chain/units/storage.py | 158 - .../supply_chain/units/storeproduct.py | 13 - .../scenarios/supply_chain/units/unitbase.py | 114 - .../scenarios/supply_chain/units/vehicle.py | 194 - .../simulator/scenarios/supply_chain/world.py | 461 - 61 files changed, 35314 deletions(-) delete mode 100644 examples/hello_world/supply_chain/hello.py delete mode 100644 examples/hello_world/supply_chain/outersample/config.yml delete mode 100644 examples/hello_world/supply_chain/store1.csv delete mode 100644 examples/supply_chain/README.md delete mode 100644 examples/supply_chain/__init__.py delete mode 100644 examples/supply_chain/config.py delete mode 100644 examples/supply_chain/config.yml delete mode 100644 examples/supply_chain/distributed_launcher.py delete mode 100644 examples/supply_chain/docker-compose.yml delete mode 100644 examples/supply_chain/env_wrapper.py delete mode 100644 examples/supply_chain/exploration.py delete mode 100644 examples/supply_chain/learner.py delete mode 100644 examples/supply_chain/policies.py delete mode 100644 examples/supply_chain/sc_state_in_maro.md delete mode 100644 examples/supply_chain/scripts/build.sh delete mode 100644 examples/supply_chain/scripts/docker_compose_yml_generator.py delete mode 100644 examples/supply_chain/scripts/kill.sh delete mode 100644 examples/supply_chain/scripts/run.sh delete mode 100644 examples/supply_chain/single_thread_launcher.py delete mode 100644 examples/supply_chain/topologies/random/config.yml delete mode 100644 examples/supply_chain/topologies/sample1/config.yml delete mode 100644 maro/simulator/scenarios/supply_chain/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/actions.py delete mode 100644 maro/simulator/scenarios/supply_chain/business_engine.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/base.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/consumer.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/distribution.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/facility.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/manufacture.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/product.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/seller.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/skumodel.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/storage.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/vehicle.py delete mode 100644 maro/simulator/scenarios/supply_chain/easy_config.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/facility.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/outerretailer.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/retailer.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/supplier.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/warehouse.py delete mode 100644 maro/simulator/scenarios/supply_chain/frame_builder.py delete mode 100644 maro/simulator/scenarios/supply_chain/parser.py delete mode 100644 maro/simulator/scenarios/supply_chain/topologies/core.yml delete mode 100644 maro/simulator/scenarios/supply_chain/topologies/sample/config.yml delete mode 100644 maro/simulator/scenarios/supply_chain/units/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/consumer.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/distribution.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/manufacture.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/order.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/outerseller.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/product.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/seller.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/simplemanufacture.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/skuunit.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/storage.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/storeproduct.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/unitbase.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/vehicle.py delete mode 100644 maro/simulator/scenarios/supply_chain/world.py diff --git a/examples/hello_world/supply_chain/hello.py b/examples/hello_world/supply_chain/hello.py deleted file mode 100644 index 73f7441a4..000000000 --- a/examples/hello_world/supply_chain/hello.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -import sys -from maro.simulator import Env - - -def main(topology_name: str): - durations = 100 - - env = Env(scenario="supply_chain", topology=topology_name, durations=durations, max_snapshots=100) - - total_episodes = 10 - - is_done = False - - for ep in range(total_episodes): - print("Current episode:", ep) - - while not is_done: - # This scenario require a dictionary of actions, which the key is the unit id, value if the action. - _, _, is_done = env.step(None) - - env.reset() - - is_done = False - - -if __name__ == "__main__": - topology_name = "sample" - - if len(sys.argv) > 1: - # run: "python hello.py outersample" under current folder to run outer retailer sample. - topology_name = sys.argv[1] - - print("running topology:", topology_name) - - main(topology_name) diff --git a/examples/hello_world/supply_chain/outersample/config.yml b/examples/hello_world/supply_chain/outersample/config.yml deleted file mode 100644 index 5fff828d4..000000000 --- a/examples/hello_world/supply_chain/outersample/config.yml +++ /dev/null @@ -1,208 +0,0 @@ -facility_definitions: - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 4 - consumer: - class: "ConsumerUnit" - config: - agent_type: 1 - - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 3 - consumer: - class: "ConsumerUnit" - manufacture: - class: "ManufactureUnit" - config: - agent_type: 0 - - RetailerFacility: &retailer_facility - class: "OuterRetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "OuterSellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: 2 - seller_sampler_type: data # data, model. What kind of sampler to use for seller. - sku_column: "SKU" # SKU column name - price_column: "Price" # Price column name - sale_column: "Sales" # Sales column name - datetime_column: "DT" # Datetime column name - file_path: "/path/to/data.csv" # full path to data file, override by each store instance - -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 100 - -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -small_storage: &small_storage - config: - capacity: 10000 - unit_storage_cost: 1 - -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 12 - bom: - sku3: 10 - - - id: 2 - name: "sku2" - output_units_per_lot: 1 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - -world: - skus: *sku_definitions - - facilities: - - name: "Supplier_001" - definition_ref: "SupplierFacility" - skus: - sku3: - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" - cost: 10 - price: 10 - vlt: 1 - children: - storage: *small_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - skus: - sku1: - init_stock: 1000 - price: 100 - vlt: 1 - sku2: - init_stock: 1000 - price: 100 - vlt: 1 - sku3: - init_stock: 1000 - price: 100 - vlt: 1 - children: - storage: *small_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Retailer_001" - definition_ref: "RetailerFacility" - skus: - sku1: - price: 300 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 # optional - vlt: 1 - sku3: - price: 200 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - sku2: - price: 100 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - - children: - storage: *small_storage - - config: - order_cost: 0 - file_path: "store1.csv" # data file that contains demand of retailer 001 - topology: - Retailer_001: - sku3: - - "Supplier_001" - - grid: - size: [20, 20] - - facilities: - Supplier_001: [0, 0] - Warehouse_001: [6, 6] - Retailer_001: [10, 18] - - blocks: - railroad: - - [10, 10] - - [10, 11] - - [10, 12] - - [11, 12] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 - start_date_time: "2010-12-01" # start time for data in files diff --git a/examples/hello_world/supply_chain/store1.csv b/examples/hello_world/supply_chain/store1.csv deleted file mode 100644 index 4e8add32d..000000000 --- a/examples/hello_world/supply_chain/store1.csv +++ /dev/null @@ -1,481 +0,0 @@ -SKU,DT,Sales,Price,SaleMean -sku3,2011-07-04,32,430,24.578358208955223 -sku3,2011-02-07,24,430,24.578358208955223 -sku3,2011-02-10,12,430,24.578358208955223 -sku3,2011-02-11,13,430,24.578358208955223 -sku3,2011-02-13,2,430,24.578358208955223 -sku3,2011-02-14,38,430,24.578358208955223 -sku3,2011-02-15,117,430,24.578358208955223 -sku3,2011-02-16,13,430,24.578358208955223 -sku3,2011-02-17,12,430,24.578358208955223 -sku3,2011-02-18,25,430,24.578358208955223 -sku3,2011-02-20,14,430,24.578358208955223 -sku3,2011-02-21,42,430,24.578358208955223 -sku3,2011-02-22,51,430,24.578358208955223 -sku3,2011-02-23,2,430,24.578358208955223 -sku3,2011-02-24,47,430,24.578358208955223 -sku3,2011-02-25,12,430,24.578358208955223 -sku3,2011-02-27,6,430,24.578358208955223 -sku3,2011-02-08,3,430,24.578358208955223 -sku3,2011-02-04,13,430,24.578358208955223 -sku3,2011-03-01,4,430,24.578358208955223 -sku3,2011-02-03,12,430,24.578358208955223 -sku3,2011-01-16,5,430,24.578358208955223 -sku3,2011-01-17,1,430,24.578358208955223 -sku3,2011-01-19,26,430,24.578358208955223 -sku3,2011-01-20,193,430,24.578358208955223 -sku3,2011-01-21,2,430,24.578358208955223 -sku3,2011-01-23,12,430,24.578358208955223 -sku3,2011-01-24,72,430,24.578358208955223 -sku3,2011-01-25,15,430,24.578358208955223 -sku3,2011-01-26,5,430,24.578358208955223 -sku3,2011-01-27,15,430,24.578358208955223 -sku3,2011-01-28,36,430,24.578358208955223 -sku3,2011-01-30,6,430,24.578358208955223 -sku3,2011-01-31,27,430,24.578358208955223 -sku3,2011-02-01,3,430,24.578358208955223 -sku3,2011-02-02,39,430,24.578358208955223 -sku3,2011-02-28,2,430,24.578358208955223 -sku3,2011-03-02,12,430,24.578358208955223 -sku3,2011-07-05,4,430,24.578358208955223 -sku3,2011-03-25,19,430,24.578358208955223 -sku3,2011-03-28,41,430,24.578358208955223 -sku3,2011-03-29,74,430,24.578358208955223 -sku3,2011-03-30,6,430,24.578358208955223 -sku3,2011-03-31,37,430,24.578358208955223 -sku3,2011-04-01,22,430,24.578358208955223 -sku3,2011-04-03,2,430,24.578358208955223 -sku3,2011-04-05,21,430,24.578358208955223 -sku3,2011-04-07,39,430,24.578358208955223 -sku3,2011-04-08,26,430,24.578358208955223 -sku3,2011-04-10,7,430,24.578358208955223 -sku3,2011-04-11,50,430,24.578358208955223 -sku3,2011-04-12,29,430,24.578358208955223 -sku3,2011-04-13,17,430,24.578358208955223 -sku3,2011-04-14,21,430,24.578358208955223 -sku3,2011-04-15,36,430,24.578358208955223 -sku3,2011-03-27,53,430,24.578358208955223 -sku3,2011-03-24,17,430,24.578358208955223 -sku3,2011-03-03,16,430,24.578358208955223 -sku3,2011-03-23,15,430,24.578358208955223 -sku3,2011-03-04,15,430,24.578358208955223 -sku3,2011-03-06,7,430,24.578358208955223 -sku3,2011-03-07,6,430,24.578358208955223 -sku3,2011-03-09,51,430,24.578358208955223 -sku3,2011-03-10,40,430,24.578358208955223 -sku3,2011-03-11,96,430,24.578358208955223 -sku3,2011-03-13,8,430,24.578358208955223 -sku3,2011-03-14,107,430,24.578358208955223 -sku3,2011-03-15,4,430,24.578358208955223 -sku3,2011-03-16,27,430,24.578358208955223 -sku3,2011-03-17,42,430,24.578358208955223 -sku3,2011-03-18,114,430,24.578358208955223 -sku3,2011-03-20,7,430,24.578358208955223 -sku3,2011-03-21,26,430,24.578358208955223 -sku3,2011-03-22,17,430,24.578358208955223 -sku3,2011-01-14,1,430,24.578358208955223 -sku3,2011-01-13,12,430,24.578358208955223 -sku3,2011-01-12,18,430,24.578358208955223 -sku3,2011-07-01,1,430,24.578358208955223 -sku3,2011-06-29,4,430,24.578358208955223 -sku3,2011-06-28,24,430,24.578358208955223 -sku3,2011-06-26,3,430,24.578358208955223 -sku3,2011-06-23,14,430,24.578358208955223 -sku3,2011-06-22,6,430,24.578358208955223 -sku3,2011-06-21,1,430,24.578358208955223 -sku3,2011-06-20,10,430,24.578358208955223 -sku3,2011-06-19,11,430,24.578358208955223 -sku3,2011-06-17,25,430,24.578358208955223 -sku3,2011-06-16,5,430,24.578358208955223 -sku3,2011-06-15,5,430,24.578358208955223 -sku3,2011-06-14,6,430,24.578358208955223 -sku3,2011-06-13,24,430,24.578358208955223 -sku3,2011-06-10,12,430,24.578358208955223 -sku3,2011-06-09,15,430,24.578358208955223 -sku3,2011-06-30,33,430,24.578358208955223 -sku3,2010-12-01,1,430,24.578358208955223 -sku3,2011-01-11,3,430,24.578358208955223 -sku3,2010-12-03,9,430,24.578358208955223 -sku3,2011-07-06,12,430,24.578358208955223 -sku3,2011-07-07,73,430,24.578358208955223 -sku3,2011-01-06,29,430,24.578358208955223 -sku3,2011-07-03,1,430,24.578358208955223 -sku3,2011-01-04,12,430,24.578358208955223 -sku3,2010-12-20,13,430,24.578358208955223 -sku3,2010-12-17,14,430,24.578358208955223 -sku3,2010-12-16,12,430,24.578358208955223 -sku3,2010-12-13,2,430,24.578358208955223 -sku3,2010-12-12,9,430,24.578358208955223 -sku3,2010-12-10,24,430,24.578358208955223 -sku3,2010-12-09,25,430,24.578358208955223 -sku3,2010-12-07,2,430,24.578358208955223 -sku3,2010-12-06,3,430,24.578358208955223 -sku3,2010-12-05,1,430,24.578358208955223 -sku3,2011-06-08,233,430,24.578358208955223 -sku3,2011-06-07,7,430,24.578358208955223 -sku3,2011-06-06,12,430,24.578358208955223 -sku3,2011-06-05,109,430,24.578358208955223 -sku3,2011-05-11,2,430,24.578358208955223 -sku3,2011-05-10,50,430,24.578358208955223 -sku3,2011-05-09,21,430,24.578358208955223 -sku3,2011-05-08,43,430,24.578358208955223 -sku3,2011-05-06,28,430,24.578358208955223 -sku3,2011-05-05,26,430,24.578358208955223 -sku3,2011-05-04,12,430,24.578358208955223 -sku3,2011-05-03,31,430,24.578358208955223 -sku3,2011-04-28,24,430,24.578358208955223 -sku3,2011-04-26,18,430,24.578358208955223 -sku3,2011-04-21,8,430,24.578358208955223 -sku3,2011-04-20,36,430,24.578358208955223 -sku3,2011-04-19,65,430,24.578358208955223 -sku3,2011-01-07,54,430,24.578358208955223 -sku3,2011-01-10,7,430,24.578358208955223 -sku3,2011-05-12,123,430,24.578358208955223 -sku3,2011-05-13,17,430,24.578358208955223 -sku3,2011-05-15,2,430,24.578358208955223 -sku3,2011-05-25,31,430,24.578358208955223 -sku3,2011-06-03,15,430,24.578358208955223 -sku3,2011-06-02,24,430,24.578358208955223 -sku3,2011-06-01,35,430,24.578358208955223 -sku3,2011-05-31,5,430,24.578358208955223 -sku3,2011-05-29,6,430,24.578358208955223 -sku3,2011-05-27,12,430,24.578358208955223 -sku3,2011-05-24,28,430,24.578358208955223 -sku3,2011-05-16,13,430,24.578358208955223 -sku3,2011-05-23,15,430,24.578358208955223 -sku3,2011-05-22,14,430,24.578358208955223 -sku3,2011-05-20,14,430,24.578358208955223 -sku3,2011-05-19,56,430,24.578358208955223 -sku3,2011-05-18,13,430,24.578358208955223 -sku3,2011-05-17,12,430,24.578358208955223 -sku3,2011-01-05,7,430,24.578358208955223 -sku3,2011-04-04,13,430,24.578358208955223 -sku3,2011-11-27,5,430,24.578358208955223 -sku3,2011-10-09,2,430,24.578358208955223 -sku3,2011-10-25,17,430,24.578358208955223 -sku3,2011-10-24,2,430,24.578358208955223 -sku3,2011-10-23,7,430,24.578358208955223 -sku3,2011-10-21,36,430,24.578358208955223 -sku3,2011-10-20,14,430,24.578358208955223 -sku3,2011-10-19,9,430,24.578358208955223 -sku3,2011-10-18,6,430,24.578358208955223 -sku3,2011-10-17,30,430,24.578358208955223 -sku3,2011-10-14,2,430,24.578358208955223 -sku3,2011-10-13,13,430,24.578358208955223 -sku3,2011-10-12,2,430,24.578358208955223 -sku3,2011-10-11,25,430,24.578358208955223 -sku3,2011-10-10,26,430,24.578358208955223 -sku3,2011-10-07,14,430,24.578358208955223 -sku3,2011-09-18,30,430,24.578358208955223 -sku3,2011-10-06,39,430,24.578358208955223 -sku3,2011-10-05,108,430,24.578358208955223 -sku3,2011-10-04,13,430,24.578358208955223 -sku3,2011-10-03,3,430,24.578358208955223 -sku3,2011-09-29,24,430,24.578358208955223 -sku3,2011-09-28,18,430,24.578358208955223 -sku3,2011-09-27,38,430,24.578358208955223 -sku3,2011-09-26,2,430,24.578358208955223 -sku3,2011-09-25,7,430,24.578358208955223 -sku3,2011-09-23,14,430,24.578358208955223 -sku3,2011-09-22,29,430,24.578358208955223 -sku3,2011-09-21,115,430,24.578358208955223 -sku3,2011-09-20,1,430,24.578358208955223 -sku3,2011-10-26,24,430,24.578358208955223 -sku3,2011-10-28,96,430,24.578358208955223 -sku3,2011-10-30,7,430,24.578358208955223 -sku3,2011-10-31,8,430,24.578358208955223 -sku3,2011-12-09,13,430,24.578358208955223 -sku3,2011-12-08,6,430,24.578358208955223 -sku3,2011-12-07,99,430,24.578358208955223 -sku3,2011-12-06,2,430,24.578358208955223 -sku3,2011-12-05,12,430,24.578358208955223 -sku3,2011-12-04,13,430,24.578358208955223 -sku3,2011-12-02,19,430,24.578358208955223 -sku3,2011-12-01,16,430,24.578358208955223 -sku3,2011-11-29,16,430,24.578358208955223 -sku3,2011-11-28,27,430,24.578358208955223 -sku3,2011-11-25,2,430,24.578358208955223 -sku3,2011-11-24,50,430,24.578358208955223 -sku3,2011-11-23,23,430,24.578358208955223 -sku3,2011-11-22,2,430,24.578358208955223 -sku3,2011-11-21,3,430,24.578358208955223 -sku3,2011-11-20,3,430,24.578358208955223 -sku3,2011-11-17,14,430,24.578358208955223 -sku3,2011-11-16,5,430,24.578358208955223 -sku3,2011-11-15,1,430,24.578358208955223 -sku3,2011-11-13,27,430,24.578358208955223 -sku3,2011-11-11,18,430,24.578358208955223 -sku3,2011-11-10,12,430,24.578358208955223 -sku3,2011-11-08,16,430,24.578358208955223 -sku3,2011-11-06,17,430,24.578358208955223 -sku3,2011-11-04,30,430,24.578358208955223 -sku3,2011-11-03,4,430,24.578358208955223 -sku3,2011-11-01,14,430,24.578358208955223 -sku3,2011-09-19,12,430,24.578358208955223 -sku3,2011-04-17,3,430,24.578358208955223 -sku3,2011-09-16,24,430,24.578358208955223 -sku3,2011-07-24,14,430,24.578358208955223 -sku3,2011-08-05,12,430,24.578358208955223 -sku3,2011-08-04,43,430,24.578358208955223 -sku3,2011-08-03,26,430,24.578358208955223 -sku3,2011-08-02,136,430,24.578358208955223 -sku3,2011-08-01,12,430,24.578358208955223 -sku3,2011-07-31,99,430,24.578358208955223 -sku3,2011-07-29,12,430,24.578358208955223 -sku3,2011-07-28,36,430,24.578358208955223 -sku3,2011-07-27,121,430,24.578358208955223 -sku3,2011-07-26,24,430,24.578358208955223 -sku3,2011-07-25,28,430,24.578358208955223 -sku3,2011-07-22,9,430,24.578358208955223 -sku3,2011-08-09,96,430,24.578358208955223 -sku3,2011-07-21,12,430,24.578358208955223 -sku3,2011-07-20,21,430,24.578358208955223 -sku3,2011-07-19,36,430,24.578358208955223 -sku3,2011-07-18,39,430,24.578358208955223 -sku3,2011-07-17,13,430,24.578358208955223 -sku3,2011-07-13,14,430,24.578358208955223 -sku3,2011-07-12,54,430,24.578358208955223 -sku3,2011-07-11,55,430,24.578358208955223 -sku3,2011-07-10,3,430,24.578358208955223 -sku3,2011-07-08,3,430,24.578358208955223 -sku3,2011-09-15,8,430,24.578358208955223 -sku3,2011-08-08,12,430,24.578358208955223 -sku3,2011-04-18,10,430,24.578358208955223 -sku3,2011-08-10,20,430,24.578358208955223 -sku3,2011-08-30,27,430,24.578358208955223 -sku3,2011-08-11,3,430,24.578358208955223 -sku3,2011-09-13,111,430,24.578358208955223 -sku3,2011-09-12,1,430,24.578358208955223 -sku3,2011-09-11,15,430,24.578358208955223 -sku3,2011-09-14,37,430,24.578358208955223 -sku3,2011-09-09,13,430,24.578358208955223 -sku3,2011-09-08,16,430,24.578358208955223 -sku3,2011-09-07,2,430,24.578358208955223 -sku3,2011-09-04,20,430,24.578358208955223 -sku3,2011-09-02,19,430,24.578358208955223 -sku3,2011-09-01,30,430,24.578358208955223 -sku3,2011-09-05,26,430,24.578358208955223 -sku3,2011-08-28,4,430,24.578358208955223 -sku3,2011-08-21,22,430,24.578358208955223 -sku3,2011-08-12,26,430,24.578358208955223 -sku3,2011-08-26,36,430,24.578358208955223 -sku3,2011-08-15,16,430,24.578358208955223 -sku3,2011-08-17,12,430,24.578358208955223 -sku3,2011-08-18,13,430,24.578358208955223 -sku3,2011-08-14,12,430,24.578358208955223 -sku3,2011-08-22,12,430,24.578358208955223 -sku3,2011-08-23,42,430,24.578358208955223 -sku3,2011-08-24,2,430,24.578358208955223 -sku3,2011-08-25,13,430,24.578358208955223 -sku1,2011-12-02,19,220,19.22222222222222 -sku1,2011-12-01,2,220,19.22222222222222 -sku1,2011-12-06,7,220,19.22222222222222 -sku1,2011-12-07,62,220,19.22222222222222 -sku1,2011-12-08,13,220,19.22222222222222 -sku1,2011-11-30,22,220,19.22222222222222 -sku1,2011-11-27,9,220,19.22222222222222 -sku1,2011-12-05,21,220,19.22222222222222 -sku1,2011-11-29,18,220,19.22222222222222 -SKU5,2011-09-30,33,370,16.80263157894737 -SKU5,2011-10-02,14,370,16.80263157894737 -SKU5,2011-10-03,84,370,16.80263157894737 -SKU5,2011-10-04,10,370,16.80263157894737 -SKU5,2011-10-05,12,370,16.80263157894737 -SKU5,2011-10-06,14,370,16.80263157894737 -SKU5,2011-10-09,2,370,16.80263157894737 -SKU5,2011-10-13,6,370,16.80263157894737 -SKU5,2011-10-10,35,370,16.80263157894737 -SKU5,2011-10-11,25,370,16.80263157894737 -SKU5,2011-10-12,49,370,16.80263157894737 -SKU5,2011-09-28,9,370,16.80263157894737 -SKU5,2011-10-16,4,370,16.80263157894737 -SKU5,2011-10-17,7,370,16.80263157894737 -SKU5,2011-10-18,1,370,16.80263157894737 -SKU5,2011-10-19,6,370,16.80263157894737 -SKU5,2011-09-29,16,370,16.80263157894737 -SKU5,2011-09-23,15,370,16.80263157894737 -SKU5,2011-09-27,8,370,16.80263157894737 -SKU5,2011-09-26,30,370,16.80263157894737 -SKU5,2011-09-05,13,370,16.80263157894737 -SKU5,2011-09-06,3,370,16.80263157894737 -SKU5,2011-09-07,55,370,16.80263157894737 -SKU5,2011-09-08,10,370,16.80263157894737 -SKU5,2011-09-09,6,370,16.80263157894737 -SKU5,2011-09-11,31,370,16.80263157894737 -SKU5,2011-09-12,66,370,16.80263157894737 -SKU5,2011-09-15,6,370,16.80263157894737 -SKU5,2011-09-16,14,370,16.80263157894737 -SKU5,2011-09-19,13,370,16.80263157894737 -SKU5,2011-09-20,18,370,16.80263157894737 -SKU5,2011-09-21,6,370,16.80263157894737 -SKU5,2011-09-22,6,370,16.80263157894737 -SKU5,2011-10-21,14,370,16.80263157894737 -SKU5,2011-09-25,7,370,16.80263157894737 -SKU5,2011-10-20,10,370,16.80263157894737 -SKU5,2011-10-27,6,370,16.80263157894737 -SKU5,2011-10-24,8,370,16.80263157894737 -SKU5,2011-11-28,5,370,16.80263157894737 -SKU5,2011-11-18,15,370,16.80263157894737 -SKU5,2011-11-20,49,370,16.80263157894737 -SKU5,2011-11-22,3,370,16.80263157894737 -SKU5,2011-11-23,17,370,16.80263157894737 -SKU5,2011-11-24,1,370,16.80263157894737 -SKU5,2011-11-25,7,370,16.80263157894737 -SKU5,2011-11-27,9,370,16.80263157894737 -SKU5,2011-11-29,10,370,16.80263157894737 -SKU5,2011-10-25,55,370,16.80263157894737 -SKU5,2011-11-30,38,370,16.80263157894737 -SKU5,2011-12-02,13,370,16.80263157894737 -SKU5,2011-12-04,5,370,16.80263157894737 -SKU5,2011-12-05,22,370,16.80263157894737 -SKU5,2011-12-06,3,370,16.80263157894737 -SKU5,2011-12-08,20,370,16.80263157894737 -SKU5,2011-12-09,1,370,16.80263157894737 -SKU5,2011-11-17,2,370,16.80263157894737 -SKU5,2011-11-16,6,370,16.80263157894737 -SKU5,2011-11-15,4,370,16.80263157894737 -SKU5,2011-11-14,14,370,16.80263157894737 -SKU5,2011-10-26,6,370,16.80263157894737 -SKU5,2011-10-28,3,370,16.80263157894737 -SKU5,2011-10-30,9,370,16.80263157894737 -SKU5,2011-10-31,11,370,16.80263157894737 -SKU5,2011-11-01,4,370,16.80263157894737 -SKU5,2011-11-02,6,370,16.80263157894737 -SKU5,2011-11-03,12,370,16.80263157894737 -SKU5,2011-11-04,144,370,16.80263157894737 -SKU5,2011-11-06,16,370,16.80263157894737 -SKU5,2011-11-07,19,370,16.80263157894737 -SKU5,2011-11-08,7,370,16.80263157894737 -SKU5,2011-11-09,24,370,16.80263157894737 -SKU5,2011-11-10,8,370,16.80263157894737 -SKU5,2011-11-11,4,370,16.80263157894737 -SKU5,2011-11-13,10,370,16.80263157894737 -SKU5,2011-09-18,17,370,16.80263157894737 -SKU5,2011-12-07,6,370,16.80263157894737 -SKU4,2011-09-15,9,560,16.392857142857142 -SKU4,2011-07-22,10,560,16.392857142857142 -SKU4,2011-09-07,2,560,16.392857142857142 -SKU4,2011-09-05,6,560,16.392857142857142 -SKU4,2011-08-30,3,560,16.392857142857142 -SKU4,2011-08-10,1,560,16.392857142857142 -SKU4,2011-08-08,25,560,16.392857142857142 -SKU4,2011-08-04,1,560,16.392857142857142 -SKU4,2011-08-02,1,560,16.392857142857142 -SKU4,2011-07-25,4,560,16.392857142857142 -SKU4,2011-07-12,2,560,16.392857142857142 -SKU4,2011-07-20,302,560,16.392857142857142 -SKU4,2011-06-17,12,560,16.392857142857142 -SKU4,2011-06-23,7,560,16.392857142857142 -SKU4,2011-06-27,13,560,16.392857142857142 -SKU4,2011-06-29,1,560,16.392857142857142 -SKU4,2011-07-01,12,560,16.392857142857142 -SKU4,2011-07-04,24,560,16.392857142857142 -SKU4,2011-07-05,2,560,16.392857142857142 -SKU4,2011-09-09,2,560,16.392857142857142 -SKU4,2011-07-06,4,560,16.392857142857142 -SKU4,2011-09-27,3,560,16.392857142857142 -SKU4,2011-10-11,1,560,16.392857142857142 -SKU4,2011-11-03,1,560,16.392857142857142 -SKU4,2011-11-27,3,560,16.392857142857142 -SKU4,2011-11-29,1,560,16.392857142857142 -SKU4,2011-07-07,6,560,16.392857142857142 -SKU4,2011-09-28,1,560,16.392857142857142 -sku2,2011-11-27,4,350,14.121212121212121 -sku2,2011-11-18,56,350,14.121212121212121 -sku2,2011-11-20,33,350,14.121212121212121 -sku2,2011-11-22,1,350,14.121212121212121 -sku2,2011-11-23,11,350,14.121212121212121 -sku2,2011-11-24,1,350,14.121212121212121 -sku2,2011-11-25,1,350,14.121212121212121 -sku2,2011-12-02,31,350,14.121212121212121 -sku2,2011-11-28,7,350,14.121212121212121 -sku2,2011-11-29,1,350,14.121212121212121 -sku2,2011-12-01,6,350,14.121212121212121 -sku2,2011-12-04,7,350,14.121212121212121 -sku2,2011-12-05,13,350,14.121212121212121 -sku2,2011-12-08,4,350,14.121212121212121 -sku2,2011-11-14,2,350,14.121212121212121 -sku2,2011-11-15,2,350,14.121212121212121 -sku2,2011-08-11,31,350,14.121212121212121 -sku2,2011-11-11,32,350,14.121212121212121 -sku2,2011-03-07,8,350,14.121212121212121 -sku2,2011-03-17,56,350,14.121212121212121 -sku2,2011-03-22,28,350,14.121212121212121 -sku2,2011-03-28,1,350,14.121212121212121 -sku2,2011-03-30,1,350,14.121212121212121 -sku2,2011-11-08,4,350,14.121212121212121 -sku2,2011-04-06,6,350,14.121212121212121 -sku2,2011-04-07,1,350,14.121212121212121 -sku2,2011-04-11,28,350,14.121212121212121 -sku2,2011-04-12,28,350,14.121212121212121 -sku2,2011-04-15,1,350,14.121212121212121 -sku2,2011-04-18,2,350,14.121212121212121 -sku2,2011-04-26,28,350,14.121212121212121 -sku2,2011-04-28,30,350,14.121212121212121 -sku2,2011-05-09,28,350,14.121212121212121 -sku2,2011-05-11,28,350,14.121212121212121 -sku2,2011-05-12,28,350,14.121212121212121 -sku2,2011-05-17,3,350,14.121212121212121 -sku2,2011-03-10,1,350,14.121212121212121 -sku2,2011-02-28,1,350,14.121212121212121 -sku2,2011-06-03,6,350,14.121212121212121 -sku2,2011-02-24,1,350,14.121212121212121 -sku2,2010-12-01,34,350,14.121212121212121 -sku2,2010-12-03,1,350,14.121212121212121 -sku2,2010-12-05,28,350,14.121212121212121 -sku2,2010-12-06,5,350,14.121212121212121 -sku2,2010-12-07,5,350,14.121212121212121 -sku2,2010-12-09,3,350,14.121212121212121 -sku2,2010-12-10,2,350,14.121212121212121 -sku2,2010-12-17,7,350,14.121212121212121 -sku2,2010-12-23,1,350,14.121212121212121 -sku2,2011-01-10,28,350,14.121212121212121 -sku2,2011-01-12,1,350,14.121212121212121 -sku2,2011-01-20,1,350,14.121212121212121 -sku2,2011-01-31,28,350,14.121212121212121 -sku2,2011-02-02,4,350,14.121212121212121 -sku2,2011-02-07,1,350,14.121212121212121 -sku2,2011-02-14,19,350,14.121212121212121 -sku2,2011-02-18,28,350,14.121212121212121 -sku2,2011-05-25,28,350,14.121212121212121 -sku2,2011-04-04,56,350,14.121212121212121 -sku2,2011-06-07,1,350,14.121212121212121 -sku2,2011-08-24,28,350,14.121212121212121 -sku2,2011-08-31,2,350,14.121212121212121 -sku2,2011-09-19,28,350,14.121212121212121 -sku2,2011-09-21,3,350,14.121212121212121 -sku2,2011-09-26,2,350,14.121212121212121 -sku2,2011-06-13,6,350,14.121212121212121 -sku2,2011-09-29,28,350,14.121212121212121 -sku2,2011-10-03,1,350,14.121212121212121 -sku2,2011-10-05,56,350,14.121212121212121 -sku2,2011-10-09,6,350,14.121212121212121 -sku2,2011-10-10,2,350,14.121212121212121 -sku2,2011-10-14,2,350,14.121212121212121 -sku2,2011-10-17,3,350,14.121212121212121 -sku2,2011-10-19,1,350,14.121212121212121 -sku2,2011-10-24,3,350,14.121212121212121 -sku2,2011-10-25,14,350,14.121212121212121 -sku2,2011-10-27,1,350,14.121212121212121 -sku2,2011-10-30,3,350,14.121212121212121 -sku2,2011-10-31,7,350,14.121212121212121 -sku2,2011-11-02,2,350,14.121212121212121 -sku2,2011-11-03,1,350,14.121212121212121 -sku2,2011-11-04,20,350,14.121212121212121 -sku2,2011-08-25,28,350,14.121212121212121 -sku2,2011-09-27,28,350,14.121212121212121 -sku2,2011-08-17,28,350,14.121212121212121 -sku2,2011-08-04,28,350,14.121212121212121 -sku2,2011-07-14,18,350,14.121212121212121 -sku2,2011-08-05,1,350,14.121212121212121 -sku2,2011-07-07,10,350,14.121212121212121 -sku2,2011-07-04,1,350,14.121212121212121 -sku2,2011-06-29,4,350,14.121212121212121 -sku2,2011-06-24,6,350,14.121212121212121 -sku2,2011-06-20,2,350,14.121212121212121 -sku2,2011-06-17,3,350,14.121212121212121 -sku2,2011-07-22,1,350,14.121212121212121 -sku2,2011-07-21,113,350,14.121212121212121 -sku2,2011-07-31,28,350,14.121212121212121 -sku2,2011-07-28,28,350,14.121212121212121 -sku2,2011-07-15,10,350,14.121212121212121 diff --git a/examples/supply_chain/README.md b/examples/supply_chain/README.md deleted file mode 100644 index 6cc67f2d1..000000000 --- a/examples/supply_chain/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Supply Chain Scenario - -This README contains instructions for running the supply chain scenario using the scripts provided under ```examples/supply_chain/scripts```. For details on state, action and reward shaping based on MARO's supply chain business engine, refer to ```examples/supply_chain/sc_state_in_maro.md```. - -The instructions require that you have Docker and Docker Compose installed and set up on your machine. For installing Docker, refer to https://docs.docker.com/get-docker/. For installing Docker Compose, refer to https://docs.docker.com/compose/install/. To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: -1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. -2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the "distributed" section of ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). -3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file diff --git a/examples/supply_chain/__init__.py b/examples/supply_chain/__init__.py deleted file mode 100644 index b14b47650..000000000 --- a/examples/supply_chain/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. diff --git a/examples/supply_chain/config.py b/examples/supply_chain/config.py deleted file mode 100644 index bdfd1002a..000000000 --- a/examples/supply_chain/config.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -import yaml -from os import getenv -from os.path import dirname, join, realpath - -from maro.simulator import Env - -sc_code_dir = dirname(realpath(__file__)) -sys.path.insert(0, sc_code_dir) -from env_wrapper import SCEnvWrapper - - -default_config_path = join(dirname(realpath(__file__)), "config.yml") -with open(getenv("CONFIG_PATH", default=default_config_path), "r") as config_file: - config = yaml.safe_load(config_file) - -topology = config["env"]["topology"] -config["env"]["topology"] = join(sc_code_dir, "topologies", topology) -env = SCEnvWrapper(Env(**config["env"])) -consumer_agent_ids = [f"consumer.{info.id}" for info in env.agent_idx_list] -producer_agent_ids = [f"producer.{info.id}" for info in env.agent_idx_list] -config["agent_ids"] = consumer_agent_ids + producer_agent_ids -config["policy"]["consumer"]["model"]["input_dim"] = env.dim diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml deleted file mode 100644 index 87905c20c..000000000 --- a/examples/supply_chain/config.yml +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -env: - scenario: supply_chain - # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder - # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ - topology: sample1 - durations: 200 # number of ticks per episode -num_episodes: 10 # number of episodes to simulate -# Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps -# before returning experiences to the learner. The learner uses these experiences to update the agents' policies -# and sync the updated policies to the actors for the next learning cycle. -policy_update_interval: -1 -eval_points: 2 -log_env_metrics: false -end_of_training_func_kwargs: -policy: - consumer: - algorithm: dqn - model: # If you want to design your model used, edit the get_dqn_agent() codes in examples\supply_chain\agent.py - hidden_dims: - - 16 - - 8 - output_dim: 10 - activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. - optim_params: - lr: 0.001 - algorithm_config: - reward_discount: .9 - loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. - target_update_freq: 5 # How many training iteration, to update DQN target model - soft_update_coefficient: 0.1 - double: false # whether to enable double DQN - training_loop: - sampler_cls: uniform - batch_size: 128 - sampler_kwargs: - replace: true - train_iters: 10 - new_experience_trigger: 128 - # The minimum number of experiences in the experience memory required for learning. Defaults to 1. - num_warmup_experiences: 1000 - experience_memory: - capacity: 50000 # experience memory size - # This determines how existing experiences are replaced when adding new experiences to a full experience - # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, - # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. - overwrite_type: random -exploration: - initial_value: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. - final_value: 0.0 -distributed: - # this is used to group all actor / learner processes belonging to the same job for communication purposes. - # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. - group: sc-dqn - num_actors: 3 # number of parallel roll-out actors - # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" - # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make - # sure that a local redis server is running and listening on the port specified by "redis_port". - redis_host: localhost - redis_port: 6379 - # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent - # slow actors from dragging down the whole process. - required_actor_finishes: 2 - # If true, experiences from older segments (usually coming from slow actors) will not be used for learning. - discard_stale_experiences: True diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py deleted file mode 100644 index a042f9a7c..000000000 --- a/examples/supply_chain/distributed_launcher.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import argparse -import sys -import yaml -from multiprocessing import Process -from os import getenv, makedirs -from os.path import dirname, join, realpath - -from maro.rl import Actor, ActorManager, Learner, MultiAgentPolicy -from maro.simulator import Env -from maro.utils import set_seeds - -sc_code_dir = dirname(realpath(__file__)) -sys.path.insert(0, sc_code_dir) -from config import config -from env_wrapper import SCEnvWrapper -from exploration import exploration_dict, agent_to_exploration -from learner import SCLearner -from policies import policy_dict, agent_to_policy - - -# for distributed / multi-process training -GROUP = getenv("GROUP", default=config["distributed"]["group"]) -REDIS_HOST = config["distributed"]["redis_host"] -REDIS_PORT = config["distributed"]["redis_port"] -NUM_ACTORS = config["distributed"]["num_actors"] - -log_dir = join(sc_code_dir, "logs", GROUP) -makedirs(log_dir, exist_ok=True) - - -def sc_learner(): - # create a multi-agent policy. - policy = MultiAgentPolicy( - policy_dict, - agent_to_policy, - exploration_dict=exploration_dict, - agent_to_exploration=agent_to_exploration - ) - - # create an actor manager to collect simulation data from multiple actors - actor_manager = ActorManager( - NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False}, - log_dir=log_dir - ) - - # create a learner to start training - learner = SCLearner( - policy, None, config["num_episodes"], - actor_manager=actor_manager, - policy_update_interval=config["policy_update_interval"], - eval_points=config["eval_points"], - required_actor_finishes=config["distributed"]["required_actor_finishes"], - log_env_metrics=config["log_env_metrics"], - end_of_training_kwargs=config["end_of_training_kwargs"], - log_dir=log_dir - ) - learner.run() - - -def sc_actor(id_): - # create an env wrapper for roll-out and obtain the input dimension for the agents - env = SCEnvWrapper(Env(**config["env"])) - policy = MultiAgentPolicy( - policy_dict, - agent_to_policy, - exploration_dict=exploration_dict, - agent_to_exploration=agent_to_exploration - ) - # create an actor that collects simulation data for the learner. - actor = Actor( - env, policy, GROUP, - proxy_options={"component_name": id_, "redis_address": (REDIS_HOST, REDIS_PORT)}, - log_dir=log_dir - ) - actor.run() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "-w", "--whoami", type=int, choices=[0, 1, 2], default=0, - help="Identity of this process: 0 - multi-process mode, 1 - learner, 2 - actor" - ) - - args = parser.parse_args() - if args.whoami == 0: - actor_processes = [Process(target=sc_actor, args=(i + 1,)) for i in range(NUM_ACTORS)] - learner_process = Process(target=sc_learner) - - for i, actor_process in enumerate(actor_processes): - set_seeds(i) # this is to ensure that the actors explore differently. - actor_process.start() - - learner_process.start() - - for actor_process in actor_processes: - actor_process.join() - - learner_process.join() - elif args.whoami == 1: - sc_learner() - elif args.whoami == 2: - sc_actor(getenv("COMPONENT")) diff --git a/examples/supply_chain/docker-compose.yml b/examples/supply_chain/docker-compose.yml deleted file mode 100644 index 949068879..000000000 --- a/examples/supply_chain/docker-compose.yml +++ /dev/null @@ -1,58 +0,0 @@ -services: - actor.0: - command: - - python3 - - /maro/supply_chain/distributed_launcher.py - - -w - - '2' - container_name: actor.0 - environment: - - COMPONENT_NAME=actor.0 - image: maro-sc - volumes: - - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl - actor.1: - command: - - python3 - - /maro/supply_chain/distributed_launcher.py - - -w - - '2' - container_name: actor.1 - environment: - - COMPONENT_NAME=actor.1 - image: maro-sc - volumes: - - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl - actor.2: - command: - - python3 - - /maro/supply_chain/distributed_launcher.py - - -w - - '2' - container_name: actor.2 - environment: - - COMPONENT_NAME=actor.2 - image: maro-sc - volumes: - - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl - learner: - build: - context: /home/data_disk/yaqiu/maro - dockerfile: /home/data_disk/yaqiu/maro/docker_files/dev.df - command: - - python3 - - /maro/supply_chain/distributed_launcher.py - - -w - - '1' - container_name: learner - image: maro-sc - volumes: - - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl - redis: - container_name: redis-sc - image: redis:6 -version: '3.9' diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py deleted file mode 100644 index 7f136094c..000000000 --- a/examples/supply_chain/env_wrapper.py +++ /dev/null @@ -1,901 +0,0 @@ -from maro.simulator import Env -from collections import defaultdict, namedtuple -import scipy.stats as st -import numpy as np -from maro.rl import AbsEnvWrapper -from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction - - -def stock_constraint(f_state): - return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean'] - - -def is_replenish_constraint(f_state): - return f_state['consumption_hist'][-1] > 0 - - -def low_profit(f_state): - return (f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000 - - -def low_stock_constraint(f_state): - return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean'] - - -def out_of_stock(f_state): - return 0 < f_state['inventory_in_stock'] - - -atoms = { - 'stock_constraint': stock_constraint, - 'is_replenish_constraint': is_replenish_constraint, - 'low_profit': low_profit, - 'low_stock_constraint': low_stock_constraint, - 'out_of_stock': out_of_stock -} - -# State extracted. -keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', - 'constraint_idx', 'is_accepted', 'consumption_hist']), - ('storage_capacity', ['storage_utilization']), - ('sale_gamma', ['sale_std', - 'sale_hist', - 'pending_order', - 'inventory_in_stock', - 'inventory_in_transit', - 'inventory_estimated', - 'inventory_rop']), - ('max_price', ['sku_price', 'sku_cost'])] - - -class UnitBaseInfo: - id: int = None - node_index: int = None - config: dict = None - summary: dict = None - - def __init__(self, unit_summary): - self.id = unit_summary["id"] - self.node_index = unit_summary["node_index"] - self.config = unit_summary.get("config", {}) - self.summary = unit_summary - - def __getitem__(self, key, default=None): - if key in self.summary: - return self.summary[key] - - return default - - -distribution_features = ("remaining_order_quantity", "remaining_order_number") -seller_features = ("total_demand", "sold", "demand") - - -class SCEnvWrapper(AbsEnvWrapper): - def __init__(self, env: Env): - super().__init__(env) - self.balance_cal = BalanceSheetCalculator(env) - self.cur_balance_sheet_reward = None - self.storage_ss = env.snapshot_list["storage"] - self.distribution_ss = env.snapshot_list["distribution"] - self.consumer_ss = env.snapshot_list["consumer"] - self.seller_ss = env.snapshot_list["seller"] - - self._summary = env.summary['node_mapping'] - self._configs = env.configs - self._agent_types = self._summary["agent_types"] - self._units_mapping = self._summary["unit_mapping"] - self._agent_list = env.agent_idx_list - - self._sku_number = len(self._summary["skus"]) + 1 - self._max_price = self._summary["max_price"] - self._max_sources_per_facility = self._summary["max_sources_per_facility"] - - # state for each tick - self._cur_metrics = env.metrics - - # cache for ppf value. - self._service_index_ppf_cache = {} - - # facility -> { - # data_model_index:int, - # storage:UnitBaseInfo, - # distribution: UnitBaseInfo, - # product_id: { - # consumer: UnitBaseInfo, - # seller: UnitBaseInfo, - # manufacture: UnitBaseInfo - # } - # } - self.facility_levels = {} - - # unit id -> (facility id) - self.unit_2_facility_dict = {} - - # our raw state - self._states = {} - - # facility id -> storage index - self._facility2storage_index_dict = {} - - # facility id -> product id -> number - self._storage_product_numbers = {} - - # facility id -> product_id -> index - self._storage_product_indices = {} - - # facility id -> storage product utilization - self._facility_product_utilization = {} - - # facility id -> in_transit_orders - self._facility_in_transit_orders = {} - - # current distribution states - self._cur_distribution_states = None - - # current consumer states - self._cur_consumer_states = None - - # current seller states - self._cur_seller_states = None - - # dim for state - self._dim = None - - # built internal helpers. - self._build_internal_helpers() - - @property - def dim(self): - """Calculate dim per shape.""" - if self._dim is None: - self._dim = 0 - - first_state = next(iter(self._states.values())) - - for _, state_keys in keys_in_state: - for key in state_keys: - val = first_state[key] - - if type(val) == list: - self._dim += len(val) - else: - self._dim += 1 - - return self._dim - - def get_state_a(self): - pass - - def get_state_b(self): - pass - - def get_state(self, event): - cur_tick = self.env.tick - settings: dict = self.env.configs.settings - consumption_hist_len = settings['consumption_hist_len'] - hist_len = settings['sale_hist_len'] - consumption_ticks = [cur_tick - - i for i in range(consumption_hist_len-1, -1, -1)] - hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] - - self.cur_balance_sheet_reward = self.balance_cal.calc() - self._cur_metrics = self.env.metrics - - self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features].flatten( - ).reshape(-1, 2).astype(np.int) - self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"].flatten( - ).reshape(-1, len(self.consumer_ss)) - self._cur_seller_states = self.seller_ss[hist_ticks::seller_features].astype( - np.int) - - # facility level states - for facility_id in self._facility_product_utilization: - # reset for each step - self._facility_product_utilization[facility_id] = 0 - - in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] - - self._facility_in_transit_orders[facility_id] = [ - 0] * self._sku_number - - for sku_id, number in in_transit_orders.items(): - self._facility_in_transit_orders[facility_id][sku_id] = number - - final_state = {} - - # calculate storage info first, then use it later to speed up. - for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"].flatten( - ).astype(np.int) - - for pid, index in self._storage_product_indices[facility_id].items(): - product_number = product_numbers[index] - - self._storage_product_numbers[facility_id][pid] = product_number - self._facility_product_utilization[facility_id] += product_number - - for agent_info in self._agent_list: - state = self._states[agent_info.id] - - storage_index = self._facility2storage_index_dict[agent_info.facility_id] - - self._update_facility_features(state, agent_info) - self._update_storage_features(state, agent_info) - # bom do not need to update - # self._add_bom_features(state, agent_info) - self._update_distribution_features(state, agent_info) - self._update_sale_features(state, agent_info) - # vlt do not need to update - # self._update_vlt_features(state, agent_info) - self._update_consumer_features(state, agent_info) - # self._add_price_features(state, agent_info) - self._update_global_features(state) - - np_state = self._serialize_state(state) - - final_state[f"consumer.{agent_info.id}"] = np_state - final_state[f"producer.{agent_info.id}"] = np_state - - return final_state - - def get_reward(self, tick=None, target_agents=None): - wc = self.env.configs.settings["global_reward_weight_consumer"] - parent_facility_balance = {} - for f_id, sheet in self.cur_balance_sheet_reward.items(): - if f_id in self.unit_2_facility_dict: - # it is a product unit - parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] - else: - parent_facility_balance[f_id] = sheet - - consumer_reward_by_facility = {f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in - self.cur_balance_sheet_reward.items()} - - return { - **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, - **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} - } - - def get_action(self, action_by_agent): - # cache the sources for each consumer if not yet cached - if not hasattr(self, "product2source"): - self.product2source, self.consumer2product = {}, {} - for facility in self.env.summary["node_mapping"]["facilities"].values(): - products = facility["units"]["products"] - for product_id, product in products.items(): - consumer = product["consumer"] - if consumer is not None: - consumer_id = consumer["id"] - product_unit_id = product["id"] - self.product2source[product_unit_id] = consumer["sources"] - self.consumer2product[consumer_id] = product_id - - env_action = {} - for agent_id, action in action_by_agent.items(): - unit_id = int(agent_id.split(".")[1]) - - is_facility = unit_id not in self._units_mapping - - # ignore facility to reduce action number - if is_facility: - continue - - # consumer action - if agent_id.startswith("consumer"): - product_id = self.consumer2product.get(unit_id, 0) - sources = self.product2source.get(unit_id, []) - - if sources: - source_id = sources[0] - - action_number = int(int(action) * self._cur_metrics["products"][unit_id]["sale_mean"]) - - # ignore 0 quantity to reduce action number - if action_number == 0: - continue - - sku = self._units_mapping[unit_id][3] - reward_discount = 0 - - env_action[unit_id] = ConsumerAction(unit_id, product_id, source_id, action_number, sku.vlt, reward_discount) - # manufacturer action - elif agent_id.startswith("producer"): - sku = self._units_mapping[unit_id][3] - action = sku.production_rate - - # ignore invalid actions - if action is None or action == 0: - continue - - env_action[unit_id] = ManufactureAction(unit_id, action) - - return env_action - - def _update_facility_features(self, state, agent_info): - state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - - def _update_storage_features(self, state, agent_info): - facility_id = agent_info.facility_id - state['storage_utilization'] = 0 - - state['storage_levels'] = self._storage_product_numbers[facility_id] - state['storage_utilization'] = self._facility_product_utilization[facility_id] - - def _update_sale_features(self, state, agent_info): - if agent_info.is_facility: - return - - product_metrics = self._cur_metrics["products"][agent_info.id] - - # for product unit only - state['sale_mean'] = product_metrics["sale_mean"] - state['sale_std'] = product_metrics["sale_std"] - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - if "seller" not in product_info: - # TODO: why gamma sale as mean? - state['sale_gamma'] = state['sale_mean'] - - if "consumer" in product_info: - consumer_index = product_info["consumer"].node_index - - state['consumption_hist'] = list( - self._cur_consumer_states[:, consumer_index]) - state['pending_order'] = list( - product_metrics["pending_order_daily"]) - - if "seller" in product_info: - seller_index = product_info["seller"].node_index - - seller_states = self._cur_seller_states[:, seller_index, :] - - # for total demand, we need latest one. - state['total_backlog_demand'] = seller_states[:, 0][-1][0] - state['sale_hist'] = list(seller_states[:, 1].flatten()) - state['backlog_demand_hist'] = list(seller_states[:, 2]) - - def _update_distribution_features(self, state, agent_info): - facility = self.facility_levels[agent_info.facility_id] - distribution = facility.get("distribution", None) - - if distribution is not None: - dist_states = self._cur_distribution_states[distribution.node_index] - state['distributor_in_transit_orders'] = dist_states[1] - state['distributor_in_transit_orders_qty'] = dist_states[0] - - def _update_consumer_features(self, state, agent_info): - if agent_info.is_facility: - return - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - if "consumer" not in product_info: - return - - state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - - product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] - state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] - state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] - - pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] - - if pending_order is not None: - state['inventory_in_distribution'] = pending_order[agent_info.sku.id] - - state['inventory_estimated'] = (state['inventory_in_stock'] - + state['inventory_in_transit'] - - state['inventory_in_distribution']) - if state['inventory_estimated'] >= 0.5 * state['storage_capacity']: - state['is_over_stock'] = 1 - - if state['inventory_estimated'] <= 0: - state['is_out_of_stock'] = 1 - - service_index = state['service_level'] - - if service_index not in self._service_index_ppf_cache: - self._service_index_ppf_cache[service_index] = st.norm.ppf( - service_index) - - ppf = self._service_index_ppf_cache[service_index] - - state['inventory_rop'] = (state['max_vlt'] * state['sale_mean'] - + np.sqrt(state['max_vlt']) * state['sale_std'] * ppf) - - if state['inventory_estimated'] < state['inventory_rop']: - state['is_below_rop'] = 1 - - def _update_global_features(self, state): - state["global_time"] = self.env.tick - - def _serialize_state(self, state): - result = [] - - for norm, fields in keys_in_state: - for field in fields: - vals = state[field] - if not isinstance(vals, list): - vals = [vals] - if norm is not None: - vals = [max(0.0, min(100.0, x / (state[norm] + 0.01))) - for x in vals] - result.extend(vals) - - return np.asarray(result, dtype=np.float32) - - def _build_internal_helpers(self): - # facility levels - for facility_id, facility in self._summary["facilities"].items(): - self.facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility['configs'], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } - - units = facility["units"] - - storage = units["storage"] - if storage is not None: - self.facility_levels[facility_id]["storage"] = UnitBaseInfo( - storage) - - self.unit_2_facility_dict[storage["id"]] = facility_id - - self._facility2storage_index_dict[facility_id] = storage["node_index"] - - self._storage_product_numbers[facility_id] = [ - 0] * self._sku_number - self._storage_product_indices[facility_id] = {} - self._facility_product_utilization[facility_id] = 0 - - for i, pid in enumerate(storage["product_list"]): - self._storage_product_indices[facility_id][pid] = i - self._storage_product_numbers[facility_id][pid] = 0 - - distribution = units["distribution"] - - if distribution is not None: - self.facility_levels[facility_id]["distribution"] = UnitBaseInfo( - distribution) - self.unit_2_facility_dict[distribution["id"]] = facility_id - - products = units["products"] - - if products: - for product_id, product in products.items(): - product_info = { - "skuproduct": UnitBaseInfo(product) - } - - self.unit_2_facility_dict[product["id"]] = facility_id - - seller = product['seller'] - - if seller is not None: - product_info["seller"] = UnitBaseInfo(seller) - self.unit_2_facility_dict[seller["id"]] = facility_id - - consumer = product["consumer"] - - if consumer is not None: - product_info["consumer"] = UnitBaseInfo(consumer) - self.unit_2_facility_dict[consumer["id"]] = facility_id - - manufacture = product["manufacture"] - - if manufacture is not None: - product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"] - ] = facility_id - - self.facility_levels[facility_id][product_id] = product_info - - # create initial state structure - self._build_init_state() - - def _build_init_state(self): - # we will build the final state with default and const values, - # then update dynamic part per step - for agent_info in self._agent_list: - state = {} - - facility = self.facility_levels[agent_info.facility_id] - - # global features - state["global_time"] = 0 - - # facility features - state["facility"] = None - state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] - state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] - state['constraint_idx'] = [0] - state['facility_id'] = [0] * self._sku_number - state['sku_info'] = {} if agent_info.is_facility else agent_info.sku - state['echelon_level'] = 0 - - state['facility_info'] = facility['config'] - state["is_positive_balance"] = 0 - - if not agent_info.is_facility: - state['facility_id'][agent_info.sku.id] = 1 - - for atom_name in atoms.keys(): - state[atom_name] = list( - np.ones(self._configs.settings['constraint_state_hist_len'])) - - # storage features - state['storage_levels'] = [0] * self._sku_number - state['storage_capacity'] = facility['storage'].config["capacity"] - state['storage_utilization'] = 0 - - # bom features - state['bom_inputs'] = [0] * self._sku_number - state['bom_outputs'] = [0] * self._sku_number - - if not agent_info.is_facility: - state['bom_inputs'][agent_info.sku.id] = 1 - state['bom_outputs'][agent_info.sku.id] = 1 - - # vlt features - sku_list = self._summary["skus"] - current_source_list = [] - - if agent_info.sku is not None: - current_source_list = facility["upstreams"].get( - agent_info.sku.id, []) - - state['vlt'] = [0] * \ - (self._max_sources_per_facility * self._sku_number) - state['max_vlt'] = 0 - - if not agent_info.is_facility: - # only for sku product - product_info = facility[agent_info.sku.id] - - if "consumer" in product_info and len(current_source_list) > 0: - state['max_vlt'] = product_info["skuproduct"]["max_vlt"] - - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - # NOTE: different with original code, our config can make sure that source has product we need - - if sku.id == agent_info.sku.id: - state['vlt'][i * len(sku_list) + j + - 1] = facility["skus"][sku.id].vlt - - # sale features - settings = self.env.configs.settings - hist_len = settings['sale_hist_len'] - consumption_hist_len = settings['consumption_hist_len'] - - state['sale_mean'] = 1.0 - state['sale_std'] = 1.0 - state['sale_gamma'] = 1.0 - state['service_level'] = 0.95 - state['total_backlog_demand'] = 0 - - state['sale_hist'] = [0] * hist_len - state['backlog_demand_hist'] = [0] * hist_len - state['consumption_hist'] = [0] * consumption_hist_len - state['pending_order'] = [0] * settings['pending_order_len'] - - if not agent_info.is_facility: - state['service_level'] = agent_info.sku.service_level - - product_info = facility[agent_info.sku.id] - - if "seller" in product_info: - state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma - - # distribution features - state['distributor_in_transit_orders'] = 0 - state['distributor_in_transit_orders_qty'] = 0 - - # consumer features - state['consumer_source_export_mask'] = [0] * \ - (self._max_sources_per_facility * self._sku_number) - state['consumer_source_inventory'] = [0] * self._sku_number - state['consumer_in_transit_orders'] = [0] * self._sku_number - - state['inventory_in_stock'] = 0 - state['inventory_in_transit'] = 0 - state['inventory_in_distribution'] = 0 - state['inventory_estimated'] = 0 - state['inventory_rop'] = 0 - state['is_over_stock'] = 0 - state['is_out_of_stock'] = 0 - state['is_below_rop'] = 0 - - if len(current_source_list) > 0: - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - if sku.id == agent_info.sku.id: - state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = \ - self.facility_levels[source]["skus"][sku.id].vlt - - # price features - state['max_price'] = self._max_price - state['sku_price'] = 0 - state['sku_cost'] = 0 - - if not agent_info.is_facility: - state['sku_price'] = agent_info.sku.price - state['sku_cost'] = agent_info.sku.cost - - self._states[agent_info.id] = state - - -class BalanceSheetCalculator: - consumer_features = ("id", "order_quantity", "price", - "order_cost", "order_product_cost", "reward_discount") - seller_features = ("id", "sold", "demand", "price", "backlog_ratio") - manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - product_features = ( - "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") - storage_features = ("capacity", "remaining_space") - vehicle_features = ("id", "payload", "unit_transport_cost") - - def __init__(self, env: Env): - self.env = env - self.consumer_ss = env.snapshot_list["consumer"] - self.seller_ss = env.snapshot_list["seller"] - self.manufacture_ss = env.snapshot_list["manufacture"] - self.storage_ss = env.snapshot_list["storage"] - self.distribution_ss = env.snapshot_list["distribution"] - self.vehicle_ss = env.snapshot_list["vehicle"] - self.product_ss = env.snapshot_list["product"] - self.products = [] - self.product_id2index_dict = {} - self.facility_levels = [] - - self.facilities = env.summary["node_mapping"]["facilities"] - - for facility_id, facility in self.facilities.items(): - pid_list = [] - distribution = facility["units"]["distribution"] - - for product_id, product in facility["units"]["products"].items(): - pid_list.append(product["id"]) - consumer = product["consumer"] - seller = product["seller"] - manufacture = product["manufacture"] - - self.product_id2index_dict[product["id"]] = len(self.products) - - self.products.append(( - product["id"], - product_id, - facility["units"]["storage"]["node_index"], - facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution["node_index"] if distribution is not None else None, - facility["downstreams"], - None if consumer is None else ( - consumer["id"], consumer["node_index"]), - None if seller is None else ( - seller["id"], seller["node_index"]), - None if manufacture is None else ( - manufacture["id"], manufacture["node_index"]), - )) - - self.facility_levels.append(( - facility_id, - pid_list, - facility["units"]["storage"]["node_index"], - facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution["node_index"] if distribution is not None else None, - [v["node_index"] for v in distribution["children"] - ] if distribution is not None else [] - )) - - self.total_balance_sheet = defaultdict(int) - - def calc(self): - tick = self.env.tick - # consumer - consumer_bs_states = self.consumer_ss[tick::self.consumer_features].flatten().reshape(-1, len( - self.consumer_features)) - - # quantity * price - consumer_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] - - # balance_sheet_profit = 0 - # order_cost + order_product_cost - consumer_step_balance_sheet_loss = -1 * \ - (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) - - # consumer step reward: balance sheet los + profile * discount - consumer_step_reward = consumer_step_balance_sheet_loss + \ - consumer_profit * consumer_bs_states[:, 5] - - # seller - seller_bs_states = self.seller_ss[tick::self.seller_features].flatten( - ).reshape(-1, len(self.seller_features)) - - # profit = sold * price - seller_balance_sheet_profit = seller_bs_states[:, - 1] * seller_bs_states[:, 3] - - # loss = demand * price * backlog_ratio - seller_balance_sheet_loss = -1 * \ - seller_bs_states[:, 2] * \ - seller_bs_states[:, 3] * seller_bs_states[:, 4] - - # step reward = loss + profit - seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit - - # manufacture - man_bs_states = self.manufacture_ss[tick::self.manufacture_features].flatten().reshape(-1, len( - self.manufacture_features)) - - # loss = manufacture number * cost - man_balance_sheet_profit_loss = -1 * \ - man_bs_states[:, 1] * man_bs_states[:, 2] - - # step reward = loss - man_step_reward = man_balance_sheet_profit_loss - - # product - product_bs_states = self.product_ss[tick::self.product_features].flatten().reshape(-1, - len(self.product_features)) - - # product distribution loss = check order + delay order penalty - product_distribution_balance_sheet_loss = -1 * \ - (product_bs_states[:, 3] + product_bs_states[:, 4]) - - # product distribution profit = check order * price - product_distribution_balance_sheet_profit = product_bs_states[:, - 2] * product_bs_states[:, 1] - - # result we need - product_step_reward = np.zeros((len(self.products, ))) - product_balance_sheet_profit = np.zeros((len(self.products, ))) - product_balance_sheet_loss = np.zeros((len(self.products, ))) - - # create product number mapping for storages - storages_product_map = {} - for storage_index in range(len(self.storage_ss)): - product_list = self.storage_ss[tick:storage_index:"product_list"].flatten( - ).astype(np.int) - product_number = self.storage_ss[tick:storage_index:"product_number"].flatten( - ).astype(np.int) - - storages_product_map[storage_index] = { - pid: pnum for pid, pnum in zip(product_list, product_number)} - - # product balance sheet and reward - # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss - # profit = same as above - # reward = same as above - for i, product in enumerate(self.products): - id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = product - - if consumer: - product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer[1]] - product_step_reward[i] += consumer_step_reward[consumer[1]] - - if seller: - product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller[1]] - product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller[1]] - product_step_reward[i] += seller_step_reward[seller[1]] - - if manufacture: - product_balance_sheet_loss[i] += man_balance_sheet_profit_loss[manufacture[1]] - product_step_reward[i] += man_step_reward[manufacture[1]] - - storage_reward = -1 * \ - storages_product_map[storage_index][product_id] * \ - unit_storage_cost - - product_step_reward[i] += storage_reward - - product_balance_sheet_loss[i] += storage_reward - - if distribution_index is not None: - product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[distribution_index] - product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[distribution_index] - - product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + \ - product_distribution_balance_sheet_profit[distribution_index] - - if downstreams and len(downstreams) > 0: - if product_id in downstreams: - for dfacility in downstreams[product_id]: - dproducts = self.facilities[dfacility]["units"]["products"] - - did = dproducts[product_id]["id"] - - product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] - product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] - product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] - - product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss - - # storage - storage_states = self.storage_ss[tick::self.storage_features].flatten( - ).reshape(-1, len(self.storage_features)) - - # loss = (capacity-remaining space) * cost - storage_balance_sheet_loss = -1 * \ - (storage_states[:, 0] - storage_states[:, 1]) - - # vehicles - vehicle_states = self.vehicle_ss[tick::self.vehicle_features].flatten( - ).reshape(-1, len(self.vehicle_features)) - - # loss = cost * payload - vehicle_balance_sheet_loss = -1 * \ - vehicle_states[:, 1] * vehicle_states[:, 2] - vehicle_step_reward = vehicle_balance_sheet_loss - - facility_balance_sheet_loss = np.zeros((len(self.facility_levels),)) - facility_balance_sheet_profit = np.zeros((len(self.facility_levels),)) - facility_step_reward = np.zeros((len(self.facility_levels),)) - - # for facilities - for i, facility in enumerate(self.facility_levels): - id, pid_list, storage_index, unit_storage_cost, distribution_index, vehicle_indices = facility - - # storage balance sheet - # profit=0 - facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * \ - unit_storage_cost - - # distribution balance sheet - if distribution_index is not None: - for vindex in vehicle_indices: - facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vindex] - # distribution unit do not provide reward - - # sku product unit balance sheet - for pid in pid_list: - facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] - facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] - facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] - - result = {} - - for id, bs, rw in zip([item[0] for item in self.products], product_balance_sheet, product_step_reward): - result[id] = (bs, rw) - - self.total_balance_sheet[id] += bs - - facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit - - for id, bs, rw in zip([item[0] for item in self.facility_levels], facility_balance_sheet, facility_step_reward): - result[id] = (bs, rw) - - self.total_balance_sheet[id] += bs - - return result - - -if __name__ == "__main__": - from time import time - import cProfile - - env = Env( - scenario="supply_chain", - topology="random", - durations=100, - max_snapshots=10) - - ss = SCEnvWrapper(env) - - env.step(None) - - start_time = time() - - # cProfile.run("ss.get_state(None)", sort="cumtime") - states = ss.get_state(None) - - end_time = time() - - print("time cost:", end_time - start_time) - - print("dim:", ss.dim) diff --git a/examples/supply_chain/exploration.py b/examples/supply_chain/exploration.py deleted file mode 100644 index 5d38d4df0..000000000 --- a/examples/supply_chain/exploration.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os.path import dirname, realpath - -from maro.rl import EpsilonGreedyExploration, LinearExplorationScheduler - -sc_code_dir = dirname(realpath(__file__)) -sys.path.insert(0, sc_code_dir) -from config import config - - -exploration = EpsilonGreedyExploration(config["policy"]["consumer"]["model"]["output_dim"]) -exploration.register_schedule( - LinearExplorationScheduler, "epsilon", config["num_episodes"], - initial_value=config["exploration"]["initial_value"], - kwargs={"final_value": config["exploration"]["final_value"]} -) - -exploration_dict = {"consumer": exploration} - -# all agents shared the same exploration object -agent_to_exploration = {agent_id: "consumer" for agent_id in config["agent_ids"] if agent_id.startswith("consumer")} diff --git a/examples/supply_chain/learner.py b/examples/supply_chain/learner.py deleted file mode 100644 index b76775d7c..000000000 --- a/examples/supply_chain/learner.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.rl import Learner - - -class SCLearner(Learner): - def end_of_training(self, ep, segment, **kwargs): - pass diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py deleted file mode 100644 index ce9110e70..000000000 --- a/examples/supply_chain/policies.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os.path import dirname, realpath - -import numpy as np - -import torch - -from maro.rl import ( - DQN, DQNConfig, ExperienceMemory, FullyConnectedBlock, NullPolicy, OptimOption, QNetForDiscreteActionSpace, - TrainingLoopConfig, get_sampler_cls -) - -sc_code_dir = dirname(realpath(__file__)) -sys.path.insert(0, sc_code_dir) -from config import config - -agent_ids = config["agent_ids"] -config = config["policy"] - - -class SimpleQNet(QNetForDiscreteActionSpace): - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component.forward(states) - - -def get_dqn_policy(config): - q_net = SimpleQNet(FullyConnectedBlock(**config["model"]), optim_option=OptimOption(**config["optimization"])) - experience_memory = ExperienceMemory(**config["experience_memory"]) - - config["training_loop"]["sampler_cls"] = get_sampler_cls(config["training_loop"]["sampler_cls"]) - generic_config = TrainingLoopConfig(**config["training_loop"]) - special_config = DQNConfig(**config["algorithm_config"]) - - return DQN(q_net, experience_memory, generic_config, special_config) - -# all consumers share the same underlying policy -policy_dict = {"consumer": get_dqn_policy(config["consumer"]), "producer": NullPolicy()} - -agent_to_policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} diff --git a/examples/supply_chain/sc_state_in_maro.md b/examples/supply_chain/sc_state_in_maro.md deleted file mode 100644 index 0d512e25d..000000000 --- a/examples/supply_chain/sc_state_in_maro.md +++ /dev/null @@ -1,312 +0,0 @@ -# SC state in MARO - - -## Env.summary - -MARO通过summary属性对外提供节点相关信息和其他在环境初始化后不会改变的信息。 -在supply chain场景中, summary中包括了以下部分 - -### unit mapping: - -```python -env.summary["node_mapping"]["unit_mapping"] -``` - -unit id 及其对应的data model名字和索引. - -### facilities: - -```python -env.summary["node_mapping"]["facilities"] -``` - -每个facility的层次结构,如sku list, units - -### skus: - -```python -env.summary["node_mapping"]["skus"] -``` - -当前配置中的所有sku - -### max_price: - -```python -env.summary["node_mapping"]["max_price"] -``` - -当前配置中最大的price - -### max_sources_per_facility: - -```python -env.summary["node_mapping"]["max_sources_per_facility"] -``` - -## States - -MARO中有两种对外提供动态状态(state)的方法。 - - -### Metrics: - -起初是为了对外提供reward相关的信息,但是也可以用来作为对外提供状态的接口,用来对外提供不能存放在snapshot list中的数据,比如字典,或者复杂的数据结构。 - -当前实现包含了以下内容: - -#### products: - -product unit相关信息(sale_mean, sale_std, pending_order_daily)。 - -#### facilities: - -facility相关信息(in_transit_orders, pending_order) - - -### Snapshot list: - -snapshot list是MARO中主要的对外提供状态的接口,它提供了所有节点的历史状态(默认为保存所有历史记录,可配置保存最新的N个来节省内存).返回的结果是numpy array,适合做batch操作。 - -snapshot list中的属性都是按照节点组织起来的,每个节点包括多个属性,每个属性可以有多个slot(array like), 同一种节点类型可以有多个instance.节点及其属性的定义可以在maro/simulator/scenarios/supply_chain/datamodels查看。 - -snapshot list的查询是通过slice接口实现的,形式如下: - -```python -env.snapshot_list["node name"][tick(s):node index(s):attribute name(s)] -> np.array - -``` - -该接口返回的是一个4维(tick, node, attribute, slot)的numpy数组(float). - -其中: -1. node name是定义节点是通过node装饰器提供的名字,当前实现包括如下节点: - -consumer, distribution, facility, manufacture, product, seller, storage, vehicle - - -2. tick(s): 可以是一个int, list或者None, 其中None表示查询当前所有历史记录的状态。 - -3. node index(s): 同tick,可以为int, list或者None,None表示查询当前节点类型的所有实例(instance). 使用中需要注意的是,节点(data model)的index和unit的id并不相同,unit的id是在facility和unit之间连续且唯一的,但是节点的index是每种data model类型内部的索引方式。 -所以在实际使用过程中,通常需要得到每个unit和facility对应的index,这部分信息在env.summary中可以得到。 - -4. attribute name(s): 在对应节点上定义过的属性名,可以为一个str,或者List[str] - - -## 示例 - -### 示例1:通过env.summary构建facility&unit的层级信息 - -这个层级信息可以帮我们在之后的操作中快速索引。 -更详细的可以参考examples/supply_chain/env_wrapper.py, _build_internal_helpers方法。 - -```python -# unit info -class UnitBaseInfo: - id: int = None - node_index: int = None - config: dict = None - summary: dict = None - - def __init__(self, unit_summary): - self.id = unit_summary["id"] - self.node_index = unit_summary["node_index"] - self.config = unit_summary.get("config", {}) - self.summary = unit_summary - - def __getitem__(self, key, default=None): - if key in self.summary: - return self.summary[key] - - return default - -# facility id -> { -# data_model_index: int, -# storage: UnitBaseInfo -# distribution: UnitBaseInfo -# product_id: { -# consumer: UnitBaseInfo -# seller: UnitBaseInfo -# manufacture: UnitBaseInfo -# } -#} -facility_levels = {} - -# 默认env.summary包含node_mapping, node_detail 和 event_payload3个部分, -# 这里只需要node——mapping -summary = env.summary["node_mapping"] - -for facility_id, facility in summary["facilities"].items(): - facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility["configs"], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } - - # facility所属的unit都挂在units下面。 - units = facility["units"] - - facility_levels[facility_id]["storage"] = UnitBaseInfo(units["storage"]) - facility_levels[facility_id]["distribution"] = UnitBaseInfo(units["distribution"]) - - # 所有的product unit - product_units = units["products"] - - if product_units: - for product_id, product in products.items(): - # product unit 本身也包含state - product_info = { - "product": UnitBaseInfo(product) - } - - # 每个product unit可能包括下面3个unit - # 注意,为了简单我们没有检查对应的key时候存在! - product_info["seller"] = UnitBaseInfo(product["seller"]) - product_info["consumer"] = UnitBaseInfo(product["consumer"]) - product_info["manufacture"] = UnitBaseInfo(product["manufacture"]) - - # 这里我们用product_id作为product 的key,可按照需求更改 - facility_levels[product_id] = product_info -``` - -### 示例2:通过env.summary构建unit id到node index的索引表 - -实际上,在示例的遍历过程中,我们就已经可以得到unit及其对应的node index索引表了,如果你不在意层级关系的话,可以通过unit_mapping快速得到这个索引。 - -```python - -# unit_mapping是一个字典,key是unit id, value是(data model name, data model node index, facility id)类型的tuple。 - -summary = env.summary["node_mapping"] - -unitid2index_mapping = {} - -for unit_id, unit_detail in summary["unit_mapping"].items(): - unitid2index_mapping[unit_id] = unit_detail[1] - -``` - -### 示例3:在state shaping过程中查询seller的销售和需求的历史,时间长度为hist_len - -```python - -# 模拟器当前时间 -cur_tick = env.tick - -# 需要查询的历史长度 -hist_len = 4 - -# 历史长度对象当前时间的时间序列 -ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] - -# 查询seller节点的过去4(含当前)个tick的sold和demand值 -# NOTE:因为这两个是都是整数,所以做一次类型转换 -seller_states =env.snapshot_list["seller"][ticks::("sold", "demand")].astype(np.int) - -# 结果应为4为numpy array -# 假设我们有2个seller -""" -[ - [ - [ - [0.0], # sold (slot = 1) - [0.0] # demand (slot = 1) - ], # seller 0 - [...] # seller 1 - ], # tick 0 - [ - [...], - [...] - ], # tick 1 - [ - [...], - [...] - ], # tick 2 - [ - [...], - [...] - ] # tick 3 (latest) -] -""" - -# 这样得到的结果就是所有的seller unit对应的销售和需求历史。 - -# 假设我们当前需要的seller unit的data model index 为 1 的话。 -cur_seller_node_index = 1 - -# 那么当前seller的销售和需求历史分别为: -cur_seller_hist = seller_states[:, cur_seller_node_index, :] - -# 第二个参数为0,是因为sold是我们查询的第一个属性 -sale_hist = cur_seller_hist[:, 0].flatten() -demand_hist = cur_seller_hist[:, 1].flatten() - -``` - -### 示例4:计算unit或facility的balance sheet - -详细的可以参考examples/supply_chain/env_wrapper.py中的BalanceSheetCalculator类。 - -```python - -# 假设我们需要计算seller, consumer, manufacture的balance sheet. -# 实际情况需要用这3个计算出对应的product unit的balance sheet,这里只是作为示例 - -# 计算所需要属性 -consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") -seller_features = ("id", "sold", "demand", "price", "backlog_ratio") -manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - -# 对应的3种data model snapshot list -consumer_ss = env.snapshot_list["consumer"] -seller_ss = env.snapshot_list["seller"] -manufacture_ss = env.snapshot_list["manufacture"] - -# 当前时间 -tick = env.tick - -# 3种unit对应的所有实例的state -# 这里用len(features)做reshape的原因是,当前用到的属性都是slots=1 -# 又因为我们的tick数量为1,这样reshape之后, 每行对应一个unit的实例,每列对应一个属性 -consumer_states = consumer_ss[tick::consumer_features].flatten().reshape(-1, len(consumer_features)) - -seller_states = seller_ss[tick::seller_features].flatten().reshape(-1, len(seller_features)) - -man_states = manufacture_ss[tick::manufacture_features].flatten().reshape(-1, len(manufacture_features)) - -# balance sheet计算,通常balance sheet 包含profit和loss两部分,这里分开保存。 - -# consumer部分 -# profit = quantity * price -consumer_profit = consumer_states[:, 1] * consumer_states[:, 2] - -# loss = -1 * (order_cost + order_product_cost) -consumer_loss = -1 * (consumer_states[:, 3] + consumer_states[:, 4]) - -# discount在代码里似乎没有用到 -reward_discount = 0 - -# consumer step reward -consumer_reward = consumer_loss + consumer_profit * reward_discount - -# seller部分 -# profit = sold * price -seller_profit = seller_states[:, 1] * seller_states[:, 3] - -# loss = -1 * demand * price * backlog_ratio -seller_loss = -1 * seller_states[:, 2] * seller_states[:, 3] * seller_states[:, 4] - -seller_reward = seller_profit + seller_loss - -# manufacture部分 -# profit = 0 -# loss = manufacture_number * cost -man_loss = -1 * man_states[:, 1] * man_states[:, 2] - -man_reward = man_loss - -# 这样我们就用numpy的batch操作完成3种unit的balance sheet和reward计算, -# 后续需要我们按照product/facility对这些结果做聚合, 这需要类似示例1这样的层级结构,具体可参考现有代码。 - -``` \ No newline at end of file diff --git a/examples/supply_chain/scripts/build.sh b/examples/supply_chain/scripts/build.sh deleted file mode 100644 index d86ecb01f..000000000 --- a/examples/supply_chain/scripts/build.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# script to build the docker image for running the supply chain scenario. -docker pull redis:6 -docker build -f ../../../docker_files/dev.df -t maro-sc:latest ../../../ diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py deleted file mode 100644 index 343b01c99..000000000 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ /dev/null @@ -1,44 +0,0 @@ -import yaml -from copy import deepcopy -from os import makedirs -from os.path import dirname, join, realpath - - -path = realpath(__file__) -script_dir = dirname(path) -sc_code_dir = dirname(script_dir) -root_dir = dirname(dirname(sc_code_dir)) -maro_rl_dir = join(root_dir, "maro", "rl") -config_path = join(sc_code_dir, "config.yml") -dockerfile_path = join(root_dir, "docker_files", "dev.df") - -with open(config_path, "r") as fp: - config = yaml.safe_load(fp) - num_actors = config["distributed"]["num_actors"] - redis_host = config["distributed"]["redis_host"] - -docker_compose_manifest = { - "version": "3.9", - "services": { - "redis": {"image": "redis:6", "container_name": redis_host}, - "learner": { - "build": {"context": root_dir, "dockerfile": dockerfile_path}, - "image": "maro-sc", - "container_name": "learner", - "volumes": [f"{sc_code_dir}:/maro/supply_chain", f"{maro_rl_dir}:/maro/maro/rl"], - "command": ["python3", "/maro/supply_chain/distributed_launcher.py", "-w", "1"] - } - } -} - -for i in range(num_actors): - actor_id = f"actor.{i}" - actor_manifest = deepcopy(docker_compose_manifest["services"]["learner"]) - del actor_manifest["build"] - actor_manifest["command"][-1] = "2" - actor_manifest["container_name"] = actor_id - actor_manifest["environment"] = [f"COMPONENT_NAME={actor_id}"] - docker_compose_manifest["services"][actor_id] = actor_manifest - -with open(join(sc_code_dir, "docker-compose.yml"), "w") as fp: - yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/supply_chain/scripts/kill.sh b/examples/supply_chain/scripts/kill.sh deleted file mode 100644 index 8603a50b5..000000000 --- a/examples/supply_chain/scripts/kill.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -# script to kill a previously launcher supply chain training job. -docker-compose -f ../docker-compose.yml down diff --git a/examples/supply_chain/scripts/run.sh b/examples/supply_chain/scripts/run.sh deleted file mode 100644 index 073b30513..000000000 --- a/examples/supply_chain/scripts/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# script to run the supply chain scenario in single-host multi-container mode. -python3 docker_compose_yml_generator.py -docker-compose -f ../docker-compose.yml up diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py deleted file mode 100644 index 6df35efb2..000000000 --- a/examples/supply_chain/single_thread_launcher.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os.path import dirname, realpath - -from maro.rl import Learner, MultiAgentPolicy -from maro.simulator import Env - -sc_code_dir = dirname(realpath(__file__)) -sys.path.insert(0, sc_code_dir) -from config import config -from env_wrapper import SCEnvWrapper -from exploration import exploration_dict, agent_to_exploration -from policies import policy_dict, agent_to_policy - - -# Single-threaded launcher -if __name__ == "__main__": - env = SCEnvWrapper(Env(**config["env"])) - policy = MultiAgentPolicy( - policy_dict, - agent_to_policy, - exploration_dict=exploration_dict, - agent_to_exploration=agent_to_exploration - ) - - # create a learner to start training - learner = Learner( - policy, env, config["num_episodes"], - policy_update_interval=config["policy_update_interval"], - eval_points=config["eval_points"], - log_env_metrics=config["log_env_metrics"] - ) - learner.run() diff --git a/examples/supply_chain/topologies/random/config.yml b/examples/supply_chain/topologies/random/config.yml deleted file mode 100644 index 9c3e72409..000000000 --- a/examples/supply_chain/topologies/random/config.yml +++ /dev/null @@ -1,29337 +0,0 @@ -facility_definitions: - RetailerFacility: - children: - products: - class: StoreProductUnit - config: - agent_type: 5 - consumer: - class: ConsumerUnit - seller: - class: SellerUnit - config: - sale_hist_len: 4 - is_template: true - storage: - class: StorageUnit - class: RetailerFacility - config: - agent_type: 2 - SupplierFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: 3 - consumer: - class: ConsumerUnit - manufacture: - class: ManufactureUnit - is_template: true - storage: - class: StorageUnit - class: SupplierFacility - config: - agent_type: 0 - WarehouseFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: 4 - consumer: - class: ConsumerUnit - is_template: true - storage: - class: StorageUnit - class: WarehouseFacility - config: - agent_type: 1 -normal_vehicle: &id001 - class: VehicleUnit - config: - patient: 100 - unit_transport_cost: 1 -settings: - constraint_state_hist_len: 4 - constraint_violate_reward: -1000000.0 - consumption_hist_len: 4 - downsampling_rate: 1 - episod_duration: 21 - gamma: 0.99 - global_reward_weight_consumer: 0.5 - global_reward_weight_producer: 0.5 - heading_timesteps: 7 - initial_balance: 100000 - pending_order_len: 4 - replenishment_discount: 0.9 - reward_normalization: 10000000.0 - sale_hist_len: 4 - tail_timesteps: 7 - total_echelons: 3 -world: - facilities: - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 200 - definition_ref: SupplierFacility - name: SUPPLIER0 - skus: - SKU0: - cost: 289 - init_stock: 3150 - price: 322 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU1: - cost: 255 - init_stock: 1100 - price: 284 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU10: - cost: 61 - init_stock: 1250 - price: 68 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU100: - cost: 14 - init_stock: 3250 - price: 16 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU101: - cost: 75 - init_stock: 3550 - price: 84 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU102: - cost: 295 - init_stock: 4650 - price: 328 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU103: - cost: 300 - init_stock: 4750 - price: 334 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU104: - cost: 44 - init_stock: 3250 - price: 49 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU105: - cost: 99 - init_stock: 2850 - price: 110 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU106: - cost: 225 - init_stock: 3650 - price: 251 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU107: - cost: 380 - init_stock: 4350 - price: 423 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU108: - cost: 412 - init_stock: 4600 - price: 458 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU109: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU11: - cost: 360 - init_stock: 2550 - price: 400 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU110: - cost: 59 - init_stock: 700 - price: 66 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU111: - cost: 234 - init_stock: 3050 - price: 260 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU112: - cost: 54 - init_stock: 4750 - price: 61 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU113: - cost: 313 - init_stock: 1550 - price: 348 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU114: - cost: 350 - init_stock: 1350 - price: 389 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU115: - cost: 257 - init_stock: 4300 - price: 286 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU116: - cost: 446 - init_stock: 3600 - price: 496 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU117: - cost: 288 - init_stock: 4600 - price: 320 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU118: - cost: 164 - init_stock: 1650 - price: 183 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU119: - cost: 188 - init_stock: 1600 - price: 209 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU12: - cost: 100 - init_stock: 4200 - price: 112 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU120: - cost: 108 - init_stock: 4900 - price: 121 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU121: - cost: 36 - init_stock: 4250 - price: 40 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU122: - cost: 393 - init_stock: 350 - price: 437 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU123: - cost: 209 - init_stock: 950 - price: 233 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU124: - cost: 163 - init_stock: 1800 - price: 182 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU125: - cost: 14 - init_stock: 4600 - price: 16 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU126: - cost: 32 - init_stock: 1950 - price: 36 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU127: - cost: 195 - init_stock: 1550 - price: 217 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU128: - cost: 148 - init_stock: 950 - price: 165 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU129: - cost: 128 - init_stock: 2500 - price: 143 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU13: - cost: 285 - init_stock: 2850 - price: 317 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU130: - cost: 313 - init_stock: 4900 - price: 348 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU131: - cost: 57 - init_stock: 2250 - price: 64 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU132: - cost: 384 - init_stock: 1050 - price: 427 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU133: - cost: 201 - init_stock: 1450 - price: 224 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU134: - cost: 302 - init_stock: 3850 - price: 336 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU135: - cost: 137 - init_stock: 5000 - price: 153 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU136: - cost: 179 - init_stock: 3550 - price: 199 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU137: - cost: 83 - init_stock: 3700 - price: 93 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU138: - cost: 205 - init_stock: 1800 - price: 228 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU139: - cost: 186 - init_stock: 1200 - price: 207 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU14: - cost: 241 - init_stock: 3100 - price: 268 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU140: - cost: 234 - init_stock: 1700 - price: 261 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU141: - cost: 171 - init_stock: 2050 - price: 190 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU142: - cost: 288 - init_stock: 1900 - price: 320 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU143: - cost: 286 - init_stock: 1300 - price: 318 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU144: - cost: 360 - init_stock: 600 - price: 400 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU145: - cost: 359 - init_stock: 4100 - price: 399 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU146: - cost: 159 - init_stock: 2400 - price: 177 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU147: - cost: 424 - init_stock: 2800 - price: 472 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU148: - cost: 281 - init_stock: 3850 - price: 313 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU149: - cost: 321 - init_stock: 3850 - price: 357 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU15: - cost: 46 - init_stock: 250 - price: 52 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU150: - cost: 95 - init_stock: 3500 - price: 106 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU151: - cost: 200 - init_stock: 3650 - price: 223 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU152: - cost: 9 - init_stock: 2550 - price: 10 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU153: - cost: 396 - init_stock: 3100 - price: 441 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU154: - cost: 69 - init_stock: 4250 - price: 77 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU155: - cost: 379 - init_stock: 2650 - price: 422 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU156: - cost: 9 - init_stock: 600 - price: 10 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU157: - cost: 369 - init_stock: 3750 - price: 410 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU158: - cost: 130 - init_stock: 4050 - price: 145 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU159: - cost: 173 - init_stock: 1250 - price: 193 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU16: - cost: 157 - init_stock: 2900 - price: 175 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU160: - cost: 413 - init_stock: 4250 - price: 459 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU161: - cost: 215 - init_stock: 2300 - price: 239 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU162: - cost: 142 - init_stock: 250 - price: 158 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU163: - cost: 437 - init_stock: 1950 - price: 486 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU164: - cost: 446 - init_stock: 5000 - price: 496 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU165: - cost: 246 - init_stock: 1650 - price: 274 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU166: - cost: 71 - init_stock: 4450 - price: 79 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU167: - cost: 398 - init_stock: 650 - price: 443 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU168: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU169: - cost: 332 - init_stock: 4900 - price: 369 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU17: - cost: 311 - init_stock: 450 - price: 346 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU170: - cost: 61 - init_stock: 2750 - price: 68 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU171: - cost: 358 - init_stock: 3800 - price: 398 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU172: - cost: 180 - init_stock: 3550 - price: 200 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU173: - cost: 157 - init_stock: 4800 - price: 175 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU174: - cost: 261 - init_stock: 3800 - price: 291 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU175: - cost: 75 - init_stock: 3750 - price: 84 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU176: - cost: 366 - init_stock: 3300 - price: 407 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU177: - cost: 231 - init_stock: 1550 - price: 257 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU178: - cost: 380 - init_stock: 250 - price: 423 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU179: - cost: 447 - init_stock: 4150 - price: 497 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU18: - cost: 232 - init_stock: 4050 - price: 258 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU180: - cost: 195 - init_stock: 2750 - price: 217 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU181: - cost: 128 - init_stock: 3000 - price: 143 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU182: - cost: 393 - init_stock: 4950 - price: 437 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU183: - cost: 130 - init_stock: 400 - price: 145 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU184: - cost: 65 - init_stock: 1200 - price: 73 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU185: - cost: 9 - init_stock: 4500 - price: 10 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU186: - cost: 323 - init_stock: 1100 - price: 359 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU187: - cost: 159 - init_stock: 1500 - price: 177 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU188: - cost: 351 - init_stock: 4350 - price: 391 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU189: - cost: 322 - init_stock: 1750 - price: 358 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU19: - cost: 429 - init_stock: 4550 - price: 477 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU190: - cost: 101 - init_stock: 850 - price: 113 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU191: - cost: 425 - init_stock: 2700 - price: 473 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU192: - cost: 373 - init_stock: 3050 - price: 415 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU193: - cost: 186 - init_stock: 1500 - price: 207 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU194: - cost: 388 - init_stock: 250 - price: 432 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU195: - cost: 196 - init_stock: 1550 - price: 218 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU196: - cost: 44 - init_stock: 3400 - price: 49 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU197: - cost: 272 - init_stock: 2850 - price: 303 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU198: - cost: 152 - init_stock: 2700 - price: 169 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU199: - cost: 404 - init_stock: 1150 - price: 449 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU2: - cost: 297 - init_stock: 3500 - price: 331 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU20: - cost: 301 - init_stock: 1250 - price: 335 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU200: - cost: 58 - init_stock: 1250 - price: 65 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU201: - cost: 93 - init_stock: 2950 - price: 104 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU202: - cost: 127 - init_stock: 3650 - price: 142 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU203: - cost: 396 - init_stock: 4100 - price: 440 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU204: - cost: 440 - init_stock: 2350 - price: 489 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU205: - cost: 117 - init_stock: 5000 - price: 130 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU206: - cost: 301 - init_stock: 550 - price: 335 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU207: - cost: 126 - init_stock: 4000 - price: 140 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU208: - cost: 441 - init_stock: 3850 - price: 491 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU209: - cost: 161 - init_stock: 1000 - price: 179 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU21: - cost: 110 - init_stock: 5000 - price: 123 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU210: - cost: 363 - init_stock: 3450 - price: 404 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU211: - cost: 156 - init_stock: 4550 - price: 174 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU212: - cost: 364 - init_stock: 3950 - price: 405 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU213: - cost: 108 - init_stock: 3200 - price: 121 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU214: - cost: 90 - init_stock: 500 - price: 101 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU215: - cost: 377 - init_stock: 2350 - price: 419 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU216: - cost: 297 - init_stock: 1150 - price: 330 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU217: - cost: 255 - init_stock: 3250 - price: 284 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU218: - cost: 184 - init_stock: 2950 - price: 205 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU219: - cost: 82 - init_stock: 2300 - price: 92 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU22: - cost: 443 - init_stock: 3300 - price: 493 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU220: - cost: 348 - init_stock: 4350 - price: 387 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU221: - cost: 35 - init_stock: 3900 - price: 39 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU222: - cost: 103 - init_stock: 1800 - price: 115 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU223: - cost: 176 - init_stock: 600 - price: 196 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU224: - cost: 348 - init_stock: 250 - price: 387 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU225: - cost: 147 - init_stock: 1450 - price: 164 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU226: - cost: 185 - init_stock: 650 - price: 206 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU227: - cost: 431 - init_stock: 1200 - price: 479 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU228: - cost: 12 - init_stock: 4500 - price: 14 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU229: - cost: 424 - init_stock: 2200 - price: 472 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU23: - cost: 348 - init_stock: 2100 - price: 387 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU230: - cost: 216 - init_stock: 1150 - price: 241 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU231: - cost: 43 - init_stock: 4050 - price: 48 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU232: - cost: 201 - init_stock: 4800 - price: 224 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU233: - cost: 324 - init_stock: 3750 - price: 360 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU234: - cost: 258 - init_stock: 250 - price: 287 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU235: - cost: 21 - init_stock: 2850 - price: 24 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU236: - cost: 139 - init_stock: 2750 - price: 155 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU237: - cost: 389 - init_stock: 2250 - price: 433 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU238: - cost: 57 - init_stock: 3300 - price: 64 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU239: - cost: 92 - init_stock: 4900 - price: 103 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU24: - cost: 87 - init_stock: 2350 - price: 97 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU240: - cost: 335 - init_stock: 2350 - price: 373 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU241: - cost: 395 - init_stock: 3550 - price: 439 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU242: - cost: 15 - init_stock: 2200 - price: 17 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU243: - cost: 316 - init_stock: 700 - price: 352 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU244: - cost: 156 - init_stock: 4100 - price: 174 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU245: - cost: 363 - init_stock: 3300 - price: 404 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU246: - cost: 270 - init_stock: 1500 - price: 300 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU247: - cost: 347 - init_stock: 1750 - price: 386 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU248: - cost: 319 - init_stock: 1450 - price: 355 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU249: - cost: 320 - init_stock: 1250 - price: 356 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU25: - cost: 144 - init_stock: 250 - price: 161 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU250: - cost: 114 - init_stock: 2700 - price: 127 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU251: - cost: 309 - init_stock: 3050 - price: 344 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU252: - cost: 162 - init_stock: 4150 - price: 181 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU253: - cost: 403 - init_stock: 800 - price: 448 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU254: - cost: 435 - init_stock: 2300 - price: 484 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU255: - cost: 261 - init_stock: 3350 - price: 290 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU256: - cost: 81 - init_stock: 3600 - price: 91 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU257: - cost: 313 - init_stock: 2850 - price: 348 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU258: - cost: 440 - init_stock: 2150 - price: 489 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU259: - cost: 299 - init_stock: 3450 - price: 333 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU26: - cost: 206 - init_stock: 3150 - price: 229 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU260: - cost: 438 - init_stock: 2600 - price: 487 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU261: - cost: 331 - init_stock: 1100 - price: 368 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU262: - cost: 298 - init_stock: 3900 - price: 332 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU263: - cost: 170 - init_stock: 3700 - price: 189 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU264: - cost: 324 - init_stock: 3950 - price: 361 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU265: - cost: 257 - init_stock: 2950 - price: 286 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU266: - cost: 115 - init_stock: 2350 - price: 128 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU267: - cost: 69 - init_stock: 4000 - price: 77 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU268: - cost: 198 - init_stock: 4450 - price: 221 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU269: - cost: 113 - init_stock: 2200 - price: 126 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU27: - cost: 333 - init_stock: 2800 - price: 370 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU270: - cost: 163 - init_stock: 3700 - price: 182 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU271: - cost: 207 - init_stock: 900 - price: 230 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU272: - cost: 329 - init_stock: 850 - price: 366 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU273: - cost: 378 - init_stock: 900 - price: 421 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU274: - cost: 26 - init_stock: 3850 - price: 29 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU275: - cost: 45 - init_stock: 2400 - price: 50 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU276: - cost: 146 - init_stock: 2700 - price: 163 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU277: - cost: 404 - init_stock: 2050 - price: 449 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU278: - cost: 94 - init_stock: 600 - price: 105 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU279: - cost: 45 - init_stock: 1950 - price: 51 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU28: - cost: 187 - init_stock: 2350 - price: 208 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU280: - cost: 265 - init_stock: 1650 - price: 295 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU281: - cost: 355 - init_stock: 3750 - price: 395 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU282: - cost: 56 - init_stock: 2300 - price: 63 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU283: - cost: 352 - init_stock: 4600 - price: 392 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU284: - cost: 309 - init_stock: 3350 - price: 344 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU285: - cost: 119 - init_stock: 4550 - price: 133 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU286: - cost: 303 - init_stock: 1950 - price: 337 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU287: - cost: 337 - init_stock: 2800 - price: 375 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU288: - cost: 162 - init_stock: 1900 - price: 181 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU289: - cost: 60 - init_stock: 1550 - price: 67 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU29: - cost: 220 - init_stock: 2900 - price: 245 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU290: - cost: 394 - init_stock: 3350 - price: 438 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU291: - cost: 84 - init_stock: 3050 - price: 94 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU292: - cost: 167 - init_stock: 250 - price: 186 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU293: - cost: 145 - init_stock: 2750 - price: 162 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU294: - cost: 368 - init_stock: 450 - price: 409 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU295: - cost: 391 - init_stock: 4650 - price: 435 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU296: - cost: 333 - init_stock: 4600 - price: 370 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU297: - cost: 268 - init_stock: 1900 - price: 298 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU298: - cost: 257 - init_stock: 1750 - price: 286 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU299: - cost: 330 - init_stock: 2550 - price: 367 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU3: - cost: 364 - init_stock: 600 - price: 405 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU30: - cost: 323 - init_stock: 3450 - price: 359 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU300: - cost: 338 - init_stock: 2900 - price: 376 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU301: - cost: 389 - init_stock: 4150 - price: 433 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU302: - cost: 165 - init_stock: 550 - price: 184 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU303: - cost: 221 - init_stock: 4700 - price: 246 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU304: - cost: 377 - init_stock: 1150 - price: 419 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU305: - cost: 445 - init_stock: 5000 - price: 495 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU306: - cost: 431 - init_stock: 2100 - price: 479 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU307: - cost: 189 - init_stock: 3900 - price: 210 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU308: - cost: 187 - init_stock: 250 - price: 208 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU309: - cost: 90 - init_stock: 4600 - price: 101 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU31: - cost: 62 - init_stock: 2800 - price: 69 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU310: - cost: 210 - init_stock: 2200 - price: 234 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU311: - cost: 275 - init_stock: 4000 - price: 306 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU312: - cost: 261 - init_stock: 1250 - price: 291 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU313: - cost: 291 - init_stock: 1900 - price: 324 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU314: - cost: 363 - init_stock: 1450 - price: 404 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU315: - cost: 423 - init_stock: 4200 - price: 471 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU316: - cost: 181 - init_stock: 900 - price: 202 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU317: - cost: 194 - init_stock: 1200 - price: 216 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU318: - cost: 250 - init_stock: 4250 - price: 278 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU319: - cost: 231 - init_stock: 2650 - price: 257 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU32: - cost: 302 - init_stock: 1100 - price: 336 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU320: - cost: 176 - init_stock: 1950 - price: 196 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU321: - cost: 60 - init_stock: 800 - price: 67 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU322: - cost: 216 - init_stock: 5000 - price: 240 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU323: - cost: 59 - init_stock: 1950 - price: 66 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU324: - cost: 397 - init_stock: 4650 - price: 442 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU325: - cost: 407 - init_stock: 3450 - price: 453 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU326: - cost: 310 - init_stock: 1200 - price: 345 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU327: - cost: 411 - init_stock: 700 - price: 457 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU328: - cost: 351 - init_stock: 2250 - price: 390 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU329: - cost: 60 - init_stock: 2100 - price: 67 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU33: - cost: 374 - init_stock: 4600 - price: 416 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU330: - cost: 275 - init_stock: 1950 - price: 306 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU331: - cost: 124 - init_stock: 2050 - price: 138 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU332: - cost: 351 - init_stock: 4800 - price: 390 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU333: - cost: 436 - init_stock: 2650 - price: 485 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU334: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU335: - cost: 72 - init_stock: 4050 - price: 80 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU336: - cost: 266 - init_stock: 1400 - price: 296 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU337: - cost: 220 - init_stock: 1450 - price: 245 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU338: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU339: - cost: 238 - init_stock: 3150 - price: 265 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU34: - cost: 85 - init_stock: 650 - price: 95 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU340: - cost: 432 - init_stock: 4350 - price: 480 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU341: - cost: 415 - init_stock: 3500 - price: 462 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU342: - cost: 129 - init_stock: 450 - price: 144 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU343: - cost: 279 - init_stock: 750 - price: 310 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU344: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU345: - cost: 397 - init_stock: 4450 - price: 442 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU346: - cost: 315 - init_stock: 2100 - price: 350 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU347: - cost: 447 - init_stock: 4100 - price: 497 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU348: - cost: 132 - init_stock: 1000 - price: 147 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU349: - cost: 60 - init_stock: 3350 - price: 67 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU35: - cost: 240 - init_stock: 4300 - price: 267 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU350: - cost: 251 - init_stock: 4600 - price: 279 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU351: - cost: 139 - init_stock: 3350 - price: 155 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU352: - cost: 63 - init_stock: 900 - price: 71 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU353: - cost: 227 - init_stock: 4650 - price: 253 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU354: - cost: 356 - init_stock: 3950 - price: 396 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU355: - cost: 56 - init_stock: 500 - price: 63 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU356: - cost: 313 - init_stock: 1450 - price: 348 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU357: - cost: 449 - init_stock: 4600 - price: 499 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU358: - cost: 242 - init_stock: 3450 - price: 269 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU359: - cost: 73 - init_stock: 3750 - price: 82 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU36: - cost: 94 - init_stock: 500 - price: 105 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU360: - cost: 435 - init_stock: 1250 - price: 484 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU361: - cost: 146 - init_stock: 4500 - price: 163 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU362: - cost: 417 - init_stock: 4350 - price: 464 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU363: - cost: 46 - init_stock: 3900 - price: 52 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU364: - cost: 348 - init_stock: 1650 - price: 387 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU365: - cost: 142 - init_stock: 4150 - price: 158 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU366: - cost: 399 - init_stock: 4300 - price: 444 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU367: - cost: 244 - init_stock: 2300 - price: 272 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU368: - cost: 424 - init_stock: 1650 - price: 472 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU369: - cost: 86 - init_stock: 3750 - price: 96 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU37: - cost: 59 - init_stock: 2950 - price: 66 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU370: - cost: 100 - init_stock: 750 - price: 112 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU371: - cost: 295 - init_stock: 250 - price: 328 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU372: - cost: 60 - init_stock: 1600 - price: 67 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU373: - cost: 52 - init_stock: 3350 - price: 58 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU374: - cost: 34 - init_stock: 2700 - price: 38 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU375: - cost: 254 - init_stock: 3600 - price: 283 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU376: - cost: 140 - init_stock: 4100 - price: 156 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU377: - cost: 70 - init_stock: 3350 - price: 78 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU378: - cost: 381 - init_stock: 1750 - price: 424 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU379: - cost: 9 - init_stock: 2450 - price: 11 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU38: - cost: 309 - init_stock: 2150 - price: 344 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU380: - cost: 353 - init_stock: 2650 - price: 393 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU381: - cost: 428 - init_stock: 300 - price: 476 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU382: - cost: 112 - init_stock: 3550 - price: 125 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU383: - cost: 273 - init_stock: 4600 - price: 304 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU384: - cost: 288 - init_stock: 450 - price: 320 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU385: - cost: 108 - init_stock: 650 - price: 120 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU386: - cost: 265 - init_stock: 1550 - price: 295 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU387: - cost: 100 - init_stock: 4850 - price: 112 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU388: - cost: 364 - init_stock: 2200 - price: 405 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU389: - cost: 338 - init_stock: 3500 - price: 376 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU39: - cost: 22 - init_stock: 2550 - price: 25 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU390: - cost: 129 - init_stock: 1950 - price: 144 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU391: - cost: 209 - init_stock: 3350 - price: 233 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU392: - cost: 146 - init_stock: 3700 - price: 163 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU393: - cost: 438 - init_stock: 3350 - price: 487 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU394: - cost: 138 - init_stock: 2650 - price: 154 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU395: - cost: 439 - init_stock: 1650 - price: 488 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU396: - cost: 299 - init_stock: 3050 - price: 333 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU397: - cost: 414 - init_stock: 2550 - price: 460 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU398: - cost: 210 - init_stock: 2900 - price: 234 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU399: - cost: 338 - init_stock: 1850 - price: 376 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU4: - cost: 395 - init_stock: 350 - price: 439 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU40: - cost: 441 - init_stock: 4950 - price: 490 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU400: - cost: 400 - init_stock: 4200 - price: 445 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU401: - cost: 369 - init_stock: 3900 - price: 410 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU402: - cost: 77 - init_stock: 350 - price: 86 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU403: - cost: 80 - init_stock: 4950 - price: 89 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU404: - cost: 258 - init_stock: 3050 - price: 287 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU405: - cost: 414 - init_stock: 950 - price: 460 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU406: - cost: 294 - init_stock: 5000 - price: 327 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU407: - cost: 23 - init_stock: 2300 - price: 26 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU408: - cost: 399 - init_stock: 400 - price: 444 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU409: - cost: 231 - init_stock: 4550 - price: 257 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU41: - cost: 126 - init_stock: 3950 - price: 141 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU410: - cost: 63 - init_stock: 800 - price: 70 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU411: - cost: 189 - init_stock: 4750 - price: 210 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU412: - cost: 257 - init_stock: 3100 - price: 286 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU413: - cost: 362 - init_stock: 4150 - price: 403 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU414: - cost: 148 - init_stock: 4350 - price: 165 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU415: - cost: 261 - init_stock: 1150 - price: 291 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU416: - cost: 205 - init_stock: 450 - price: 228 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU417: - cost: 398 - init_stock: 3600 - price: 443 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU418: - cost: 412 - init_stock: 650 - price: 458 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU419: - cost: 272 - init_stock: 4450 - price: 303 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU42: - cost: 415 - init_stock: 1300 - price: 462 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU420: - cost: 241 - init_stock: 2100 - price: 268 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU421: - cost: 192 - init_stock: 2300 - price: 214 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU422: - cost: 46 - init_stock: 2700 - price: 52 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU423: - cost: 169 - init_stock: 3300 - price: 188 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU424: - cost: 170 - init_stock: 550 - price: 189 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU425: - cost: 145 - init_stock: 600 - price: 162 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU426: - cost: 112 - init_stock: 4900 - price: 125 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU427: - cost: 371 - init_stock: 4700 - price: 413 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU428: - cost: 351 - init_stock: 3150 - price: 391 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU429: - cost: 35 - init_stock: 2050 - price: 39 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU43: - cost: 195 - init_stock: 3400 - price: 217 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU430: - cost: 194 - init_stock: 3650 - price: 216 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU431: - cost: 295 - init_stock: 1050 - price: 328 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU432: - cost: 22 - init_stock: 2300 - price: 25 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU433: - cost: 313 - init_stock: 2250 - price: 348 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU434: - cost: 33 - init_stock: 1500 - price: 37 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU435: - cost: 244 - init_stock: 1500 - price: 272 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU436: - cost: 64 - init_stock: 4050 - price: 72 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU437: - cost: 103 - init_stock: 700 - price: 115 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU438: - cost: 128 - init_stock: 500 - price: 143 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU439: - cost: 230 - init_stock: 1900 - price: 256 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU44: - cost: 324 - init_stock: 2400 - price: 360 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU440: - cost: 223 - init_stock: 4100 - price: 248 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU441: - cost: 227 - init_stock: 2800 - price: 253 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU442: - cost: 423 - init_stock: 4400 - price: 470 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU443: - cost: 196 - init_stock: 3650 - price: 218 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU444: - cost: 140 - init_stock: 300 - price: 156 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU445: - cost: 234 - init_stock: 4300 - price: 260 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU446: - cost: 447 - init_stock: 2450 - price: 497 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU447: - cost: 176 - init_stock: 250 - price: 196 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU448: - cost: 436 - init_stock: 3550 - price: 485 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU449: - cost: 253 - init_stock: 1900 - price: 282 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU45: - cost: 227 - init_stock: 500 - price: 253 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU450: - cost: 52 - init_stock: 4900 - price: 58 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU451: - cost: 421 - init_stock: 3450 - price: 468 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU452: - cost: 56 - init_stock: 3950 - price: 63 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU453: - cost: 33 - init_stock: 1750 - price: 37 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU454: - cost: 444 - init_stock: 4250 - price: 494 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU455: - cost: 122 - init_stock: 1300 - price: 136 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU456: - cost: 304 - init_stock: 1250 - price: 338 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU457: - cost: 152 - init_stock: 3200 - price: 169 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU458: - cost: 362 - init_stock: 350 - price: 403 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU459: - cost: 382 - init_stock: 1700 - price: 425 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU46: - cost: 269 - init_stock: 2500 - price: 299 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU460: - cost: 364 - init_stock: 3650 - price: 405 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU461: - cost: 225 - init_stock: 2400 - price: 251 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU462: - cost: 403 - init_stock: 450 - price: 448 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU463: - cost: 168 - init_stock: 4100 - price: 187 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU464: - cost: 251 - init_stock: 1700 - price: 279 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU465: - cost: 132 - init_stock: 5000 - price: 147 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU466: - cost: 87 - init_stock: 2100 - price: 97 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU467: - cost: 127 - init_stock: 1800 - price: 142 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU468: - cost: 49 - init_stock: 2500 - price: 55 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU469: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU47: - cost: 108 - init_stock: 1950 - price: 121 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU470: - cost: 335 - init_stock: 1600 - price: 373 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU471: - cost: 283 - init_stock: 3550 - price: 315 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU472: - cost: 378 - init_stock: 2950 - price: 421 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU473: - cost: 175 - init_stock: 1050 - price: 195 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU474: - cost: 403 - init_stock: 4300 - price: 448 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU475: - cost: 30 - init_stock: 4700 - price: 34 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU476: - cost: 225 - init_stock: 4500 - price: 251 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU477: - cost: 260 - init_stock: 2350 - price: 289 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU478: - cost: 431 - init_stock: 4400 - price: 479 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU479: - cost: 329 - init_stock: 1900 - price: 366 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU48: - cost: 306 - init_stock: 2900 - price: 340 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU480: - cost: 16 - init_stock: 1500 - price: 18 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU481: - cost: 297 - init_stock: 4400 - price: 330 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU482: - cost: 84 - init_stock: 4350 - price: 94 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU483: - cost: 268 - init_stock: 1700 - price: 298 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU484: - cost: 75 - init_stock: 3650 - price: 84 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU485: - cost: 286 - init_stock: 3350 - price: 318 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU486: - cost: 109 - init_stock: 2600 - price: 122 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU487: - cost: 249 - init_stock: 3300 - price: 277 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU488: - cost: 32 - init_stock: 1350 - price: 36 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU489: - cost: 53 - init_stock: 1550 - price: 59 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU49: - cost: 236 - init_stock: 4750 - price: 263 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU490: - cost: 144 - init_stock: 4050 - price: 161 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU491: - cost: 399 - init_stock: 3750 - price: 444 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU492: - cost: 47 - init_stock: 4000 - price: 53 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU493: - cost: 414 - init_stock: 1350 - price: 461 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU494: - cost: 130 - init_stock: 4700 - price: 145 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU495: - cost: 134 - init_stock: 3100 - price: 149 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU496: - cost: 307 - init_stock: 2950 - price: 342 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU497: - cost: 29 - init_stock: 450 - price: 33 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU498: - cost: 274 - init_stock: 3250 - price: 305 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU499: - cost: 276 - init_stock: 1450 - price: 307 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU5: - cost: 419 - init_stock: 3550 - price: 466 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU50: - cost: 253 - init_stock: 1850 - price: 282 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU500: - cost: 187 - init_stock: 2100 - price: 208 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU501: - cost: 174 - init_stock: 3800 - price: 194 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU502: - cost: 62 - init_stock: 3250 - price: 69 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU503: - cost: 187 - init_stock: 2850 - price: 208 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU504: - cost: 225 - init_stock: 1500 - price: 251 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU505: - cost: 383 - init_stock: 2950 - price: 426 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU506: - cost: 134 - init_stock: 4650 - price: 149 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU507: - cost: 168 - init_stock: 750 - price: 187 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU508: - cost: 244 - init_stock: 4800 - price: 272 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU509: - cost: 267 - init_stock: 4050 - price: 297 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU51: - cost: 265 - init_stock: 3350 - price: 295 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU510: - cost: 84 - init_stock: 450 - price: 94 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU511: - cost: 324 - init_stock: 3750 - price: 361 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU512: - cost: 420 - init_stock: 1100 - price: 467 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU513: - cost: 308 - init_stock: 350 - price: 343 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU514: - cost: 167 - init_stock: 3150 - price: 186 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU515: - cost: 130 - init_stock: 2700 - price: 145 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU516: - cost: 57 - init_stock: 1900 - price: 64 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU517: - cost: 105 - init_stock: 450 - price: 117 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU518: - cost: 23 - init_stock: 1050 - price: 26 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU519: - cost: 209 - init_stock: 2500 - price: 233 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU52: - cost: 19 - init_stock: 3350 - price: 22 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU520: - cost: 188 - init_stock: 1100 - price: 209 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU521: - cost: 198 - init_stock: 2500 - price: 220 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU522: - cost: 140 - init_stock: 4350 - price: 156 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU523: - cost: 58 - init_stock: 5000 - price: 65 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU524: - cost: 264 - init_stock: 4700 - price: 294 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU525: - cost: 388 - init_stock: 2100 - price: 432 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU526: - cost: 377 - init_stock: 1200 - price: 419 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU527: - cost: 117 - init_stock: 3500 - price: 131 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU528: - cost: 284 - init_stock: 1050 - price: 316 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU529: - cost: 268 - init_stock: 1550 - price: 298 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU53: - cost: 135 - init_stock: 3500 - price: 151 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU530: - cost: 117 - init_stock: 2250 - price: 131 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU531: - cost: 92 - init_stock: 3000 - price: 103 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU532: - cost: 315 - init_stock: 3850 - price: 351 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU533: - cost: 266 - init_stock: 2100 - price: 296 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU534: - cost: 382 - init_stock: 2050 - price: 425 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU535: - cost: 273 - init_stock: 4300 - price: 304 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU536: - cost: 322 - init_stock: 2600 - price: 358 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU537: - cost: 288 - init_stock: 450 - price: 321 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU538: - cost: 411 - init_stock: 2500 - price: 457 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU539: - cost: 148 - init_stock: 3900 - price: 165 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU54: - cost: 142 - init_stock: 2300 - price: 158 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU540: - cost: 349 - init_stock: 2100 - price: 388 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU541: - cost: 117 - init_stock: 1450 - price: 131 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU542: - cost: 34 - init_stock: 1300 - price: 38 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU543: - cost: 387 - init_stock: 4250 - price: 430 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU544: - cost: 311 - init_stock: 4850 - price: 346 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU545: - cost: 157 - init_stock: 750 - price: 175 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU546: - cost: 441 - init_stock: 4800 - price: 491 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU547: - cost: 144 - init_stock: 2200 - price: 161 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU548: - cost: 99 - init_stock: 300 - price: 111 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU549: - cost: 348 - init_stock: 3950 - price: 387 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU55: - cost: 433 - init_stock: 3250 - price: 482 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU550: - cost: 233 - init_stock: 4700 - price: 259 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU551: - cost: 41 - init_stock: 1550 - price: 46 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU552: - cost: 171 - init_stock: 4500 - price: 191 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU553: - cost: 187 - init_stock: 1500 - price: 208 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU554: - cost: 306 - init_stock: 2050 - price: 340 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU555: - cost: 440 - init_stock: 4200 - price: 489 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU556: - cost: 99 - init_stock: 1500 - price: 110 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU557: - cost: 300 - init_stock: 3650 - price: 334 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU558: - cost: 359 - init_stock: 850 - price: 399 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU559: - cost: 281 - init_stock: 2700 - price: 313 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU56: - cost: 339 - init_stock: 3700 - price: 377 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU560: - cost: 254 - init_stock: 2050 - price: 283 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU561: - cost: 181 - init_stock: 1950 - price: 202 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU562: - cost: 312 - init_stock: 4450 - price: 347 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU563: - cost: 360 - init_stock: 250 - price: 401 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU564: - cost: 98 - init_stock: 450 - price: 109 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU565: - cost: 51 - init_stock: 3750 - price: 57 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU566: - cost: 117 - init_stock: 550 - price: 131 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU567: - cost: 324 - init_stock: 450 - price: 361 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU568: - cost: 67 - init_stock: 3900 - price: 75 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU569: - cost: 425 - init_stock: 2400 - price: 473 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU57: - cost: 323 - init_stock: 2500 - price: 359 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU570: - cost: 349 - init_stock: 4150 - price: 388 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU571: - cost: 151 - init_stock: 1950 - price: 168 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU572: - cost: 23 - init_stock: 2250 - price: 26 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU573: - cost: 221 - init_stock: 2050 - price: 246 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU574: - cost: 84 - init_stock: 2500 - price: 94 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU575: - cost: 213 - init_stock: 3950 - price: 237 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU576: - cost: 238 - init_stock: 3900 - price: 265 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU577: - cost: 16 - init_stock: 4350 - price: 18 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU578: - cost: 90 - init_stock: 3550 - price: 100 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU579: - cost: 373 - init_stock: 450 - price: 415 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU58: - cost: 325 - init_stock: 2400 - price: 362 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU580: - cost: 182 - init_stock: 250 - price: 203 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU581: - cost: 136 - init_stock: 4300 - price: 152 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU582: - cost: 323 - init_stock: 4800 - price: 359 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU583: - cost: 77 - init_stock: 4350 - price: 86 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU584: - cost: 29 - init_stock: 2250 - price: 33 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU585: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU586: - cost: 54 - init_stock: 2200 - price: 61 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU587: - cost: 261 - init_stock: 3900 - price: 290 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU588: - cost: 428 - init_stock: 2200 - price: 476 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU589: - cost: 219 - init_stock: 1700 - price: 244 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU59: - cost: 130 - init_stock: 2550 - price: 145 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU590: - cost: 63 - init_stock: 1550 - price: 70 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU591: - cost: 185 - init_stock: 4200 - price: 206 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU592: - cost: 196 - init_stock: 2300 - price: 218 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU593: - cost: 111 - init_stock: 2200 - price: 124 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU594: - cost: 214 - init_stock: 2500 - price: 238 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU595: - cost: 65 - init_stock: 2150 - price: 73 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU596: - cost: 388 - init_stock: 350 - price: 432 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU597: - cost: 226 - init_stock: 4350 - price: 252 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU598: - cost: 313 - init_stock: 700 - price: 348 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU599: - cost: 48 - init_stock: 3400 - price: 54 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU6: - cost: 109 - init_stock: 4400 - price: 122 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU60: - cost: 180 - init_stock: 1950 - price: 200 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU600: - cost: 347 - init_stock: 3250 - price: 386 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU601: - cost: 124 - init_stock: 1950 - price: 138 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU602: - cost: 165 - init_stock: 4900 - price: 184 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU603: - cost: 250 - init_stock: 2100 - price: 278 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU604: - cost: 243 - init_stock: 2500 - price: 270 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU605: - cost: 259 - init_stock: 1200 - price: 288 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU606: - cost: 102 - init_stock: 550 - price: 114 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU607: - cost: 187 - init_stock: 3950 - price: 208 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU608: - cost: 35 - init_stock: 3650 - price: 39 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU609: - cost: 91 - init_stock: 950 - price: 102 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU61: - cost: 414 - init_stock: 3750 - price: 461 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU610: - cost: 404 - init_stock: 3400 - price: 449 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU611: - cost: 275 - init_stock: 3850 - price: 306 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU612: - cost: 351 - init_stock: 4400 - price: 391 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU613: - cost: 156 - init_stock: 350 - price: 174 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU614: - cost: 155 - init_stock: 750 - price: 173 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU615: - cost: 297 - init_stock: 4550 - price: 330 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU616: - cost: 208 - init_stock: 3650 - price: 232 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU617: - cost: 182 - init_stock: 3000 - price: 203 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU618: - cost: 69 - init_stock: 1150 - price: 77 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU619: - cost: 170 - init_stock: 4300 - price: 189 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU62: - cost: 111 - init_stock: 450 - price: 124 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU620: - cost: 207 - init_stock: 1950 - price: 231 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU621: - cost: 179 - init_stock: 3600 - price: 199 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU622: - cost: 227 - init_stock: 3250 - price: 253 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU623: - cost: 301 - init_stock: 2750 - price: 335 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU624: - cost: 433 - init_stock: 2700 - price: 482 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU625: - cost: 41 - init_stock: 4450 - price: 46 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU626: - cost: 405 - init_stock: 4450 - price: 451 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU627: - cost: 198 - init_stock: 4100 - price: 220 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU628: - cost: 111 - init_stock: 1300 - price: 124 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU629: - cost: 317 - init_stock: 4600 - price: 353 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU63: - cost: 61 - init_stock: 700 - price: 68 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU630: - cost: 109 - init_stock: 850 - price: 122 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU631: - cost: 266 - init_stock: 2450 - price: 296 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU632: - cost: 76 - init_stock: 4700 - price: 85 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU633: - cost: 165 - init_stock: 3350 - price: 184 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU634: - cost: 15 - init_stock: 3650 - price: 17 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU635: - cost: 306 - init_stock: 4650 - price: 341 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU636: - cost: 346 - init_stock: 1850 - price: 385 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU637: - cost: 74 - init_stock: 3050 - price: 83 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU638: - cost: 337 - init_stock: 400 - price: 375 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU639: - cost: 294 - init_stock: 2000 - price: 327 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU64: - cost: 32 - init_stock: 950 - price: 36 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU640: - cost: 247 - init_stock: 4550 - price: 275 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU641: - cost: 328 - init_stock: 4050 - price: 365 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU642: - cost: 372 - init_stock: 1250 - price: 414 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU643: - cost: 322 - init_stock: 2300 - price: 358 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU644: - cost: 41 - init_stock: 4100 - price: 46 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU645: - cost: 420 - init_stock: 4950 - price: 467 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU646: - cost: 170 - init_stock: 650 - price: 189 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU647: - cost: 272 - init_stock: 3850 - price: 303 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU648: - cost: 203 - init_stock: 2750 - price: 226 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU649: - cost: 178 - init_stock: 1250 - price: 198 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU65: - cost: 13 - init_stock: 4700 - price: 15 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU650: - cost: 315 - init_stock: 2800 - price: 351 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU651: - cost: 342 - init_stock: 4800 - price: 380 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU652: - cost: 110 - init_stock: 2000 - price: 123 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU653: - cost: 431 - init_stock: 4800 - price: 479 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU654: - cost: 59 - init_stock: 400 - price: 66 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU655: - cost: 299 - init_stock: 2100 - price: 333 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU656: - cost: 47 - init_stock: 3600 - price: 53 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU657: - cost: 65 - init_stock: 4050 - price: 73 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU658: - cost: 63 - init_stock: 1800 - price: 70 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU659: - cost: 18 - init_stock: 2800 - price: 21 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU66: - cost: 128 - init_stock: 4500 - price: 143 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU660: - cost: 438 - init_stock: 4150 - price: 487 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU661: - cost: 309 - init_stock: 3700 - price: 344 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU662: - cost: 334 - init_stock: 1900 - price: 372 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU663: - cost: 376 - init_stock: 2000 - price: 418 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU664: - cost: 315 - init_stock: 4200 - price: 351 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU665: - cost: 443 - init_stock: 2150 - price: 493 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU666: - cost: 306 - init_stock: 4400 - price: 341 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU667: - cost: 292 - init_stock: 650 - price: 325 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU668: - cost: 257 - init_stock: 3000 - price: 286 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU669: - cost: 66 - init_stock: 800 - price: 74 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU67: - cost: 222 - init_stock: 2650 - price: 247 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU670: - cost: 260 - init_stock: 4300 - price: 289 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU671: - cost: 240 - init_stock: 4150 - price: 267 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU672: - cost: 332 - init_stock: 650 - price: 369 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU673: - cost: 289 - init_stock: 2050 - price: 322 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU674: - cost: 200 - init_stock: 1100 - price: 223 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU675: - cost: 394 - init_stock: 1650 - price: 438 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU676: - cost: 219 - init_stock: 3050 - price: 244 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU677: - cost: 382 - init_stock: 2200 - price: 425 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU678: - cost: 151 - init_stock: 1800 - price: 168 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU679: - cost: 355 - init_stock: 2200 - price: 395 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU68: - cost: 152 - init_stock: 700 - price: 169 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU680: - cost: 234 - init_stock: 1500 - price: 260 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU681: - cost: 417 - init_stock: 4850 - price: 464 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU682: - cost: 333 - init_stock: 2650 - price: 370 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU683: - cost: 294 - init_stock: 3350 - price: 327 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU684: - cost: 319 - init_stock: 3950 - price: 355 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU685: - cost: 379 - init_stock: 2250 - price: 422 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU686: - cost: 56 - init_stock: 3150 - price: 63 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU687: - cost: 30 - init_stock: 3800 - price: 34 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU688: - cost: 435 - init_stock: 3850 - price: 484 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU689: - cost: 449 - init_stock: 750 - price: 499 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU69: - cost: 158 - init_stock: 3350 - price: 176 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU690: - cost: 393 - init_stock: 4150 - price: 437 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU691: - cost: 15 - init_stock: 3950 - price: 17 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU692: - cost: 202 - init_stock: 3250 - price: 225 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU693: - cost: 17 - init_stock: 3050 - price: 19 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU694: - cost: 187 - init_stock: 3750 - price: 208 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU695: - cost: 171 - init_stock: 350 - price: 190 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU696: - cost: 313 - init_stock: 2900 - price: 348 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU697: - cost: 409 - init_stock: 4000 - price: 455 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU698: - cost: 178 - init_stock: 3050 - price: 198 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU699: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU7: - cost: 90 - init_stock: 4800 - price: 101 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU70: - cost: 167 - init_stock: 1750 - price: 186 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU700: - cost: 66 - init_stock: 450 - price: 74 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU701: - cost: 359 - init_stock: 3850 - price: 399 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU702: - cost: 73 - init_stock: 1700 - price: 82 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU703: - cost: 326 - init_stock: 1900 - price: 363 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU704: - cost: 345 - init_stock: 4350 - price: 384 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU705: - cost: 115 - init_stock: 3150 - price: 128 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU706: - cost: 163 - init_stock: 4550 - price: 182 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU707: - cost: 16 - init_stock: 3450 - price: 18 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU708: - cost: 160 - init_stock: 1400 - price: 178 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU709: - cost: 91 - init_stock: 2650 - price: 102 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU71: - cost: 283 - init_stock: 2250 - price: 315 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU710: - cost: 226 - init_stock: 950 - price: 252 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU711: - cost: 252 - init_stock: 3450 - price: 281 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU712: - cost: 208 - init_stock: 550 - price: 232 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU713: - cost: 256 - init_stock: 2150 - price: 285 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU714: - cost: 71 - init_stock: 2050 - price: 79 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU715: - cost: 210 - init_stock: 850 - price: 234 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU716: - cost: 63 - init_stock: 3750 - price: 70 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU717: - cost: 310 - init_stock: 400 - price: 345 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU718: - cost: 321 - init_stock: 2900 - price: 357 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU719: - cost: 306 - init_stock: 3250 - price: 340 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU72: - cost: 412 - init_stock: 4250 - price: 458 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU720: - cost: 292 - init_stock: 2100 - price: 325 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU721: - cost: 65 - init_stock: 550 - price: 73 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU722: - cost: 352 - init_stock: 4850 - price: 392 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU723: - cost: 286 - init_stock: 4450 - price: 318 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU724: - cost: 360 - init_stock: 1650 - price: 400 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU725: - cost: 157 - init_stock: 1850 - price: 175 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU726: - cost: 412 - init_stock: 4450 - price: 458 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU727: - cost: 376 - init_stock: 2850 - price: 418 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU728: - cost: 427 - init_stock: 1850 - price: 475 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU729: - cost: 291 - init_stock: 1800 - price: 324 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU73: - cost: 190 - init_stock: 2700 - price: 212 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU730: - cost: 14 - init_stock: 650 - price: 16 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU731: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU732: - cost: 36 - init_stock: 3100 - price: 41 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU733: - cost: 283 - init_stock: 1300 - price: 315 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU734: - cost: 33 - init_stock: 2550 - price: 37 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU735: - cost: 239 - init_stock: 4950 - price: 266 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU736: - cost: 331 - init_stock: 1250 - price: 368 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU737: - cost: 427 - init_stock: 1550 - price: 475 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU738: - cost: 166 - init_stock: 2400 - price: 185 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU739: - cost: 427 - init_stock: 3700 - price: 475 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU74: - cost: 143 - init_stock: 2100 - price: 159 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU740: - cost: 351 - init_stock: 3200 - price: 390 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU741: - cost: 81 - init_stock: 2800 - price: 91 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU742: - cost: 169 - init_stock: 1100 - price: 188 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU743: - cost: 195 - init_stock: 2150 - price: 217 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU744: - cost: 341 - init_stock: 2900 - price: 379 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU745: - cost: 284 - init_stock: 4600 - price: 316 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU746: - cost: 393 - init_stock: 2000 - price: 437 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU747: - cost: 335 - init_stock: 3950 - price: 373 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU748: - cost: 247 - init_stock: 3300 - price: 275 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU749: - cost: 354 - init_stock: 2650 - price: 394 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU75: - cost: 117 - init_stock: 950 - price: 131 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU750: - cost: 230 - init_stock: 3100 - price: 256 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU751: - cost: 332 - init_stock: 3250 - price: 369 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU752: - cost: 233 - init_stock: 3900 - price: 259 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU753: - cost: 69 - init_stock: 3250 - price: 77 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU754: - cost: 348 - init_stock: 650 - price: 387 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU755: - cost: 318 - init_stock: 4000 - price: 354 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU756: - cost: 221 - init_stock: 4500 - price: 246 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU757: - cost: 36 - init_stock: 2850 - price: 40 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU758: - cost: 216 - init_stock: 2000 - price: 241 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU759: - cost: 391 - init_stock: 1900 - price: 435 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU76: - cost: 132 - init_stock: 3200 - price: 147 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU760: - cost: 158 - init_stock: 2550 - price: 176 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU761: - cost: 201 - init_stock: 2750 - price: 224 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU762: - cost: 237 - init_stock: 2550 - price: 264 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU763: - cost: 346 - init_stock: 3950 - price: 385 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU764: - cost: 314 - init_stock: 1700 - price: 349 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU765: - cost: 310 - init_stock: 650 - price: 345 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU766: - cost: 430 - init_stock: 1000 - price: 478 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU767: - cost: 85 - init_stock: 3800 - price: 95 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU768: - cost: 162 - init_stock: 4600 - price: 181 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU769: - cost: 21 - init_stock: 1450 - price: 24 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU77: - cost: 368 - init_stock: 3600 - price: 409 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU770: - cost: 135 - init_stock: 3100 - price: 150 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU771: - cost: 90 - init_stock: 350 - price: 101 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU772: - cost: 230 - init_stock: 300 - price: 256 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU773: - cost: 75 - init_stock: 4600 - price: 84 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU774: - cost: 402 - init_stock: 2950 - price: 447 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU775: - cost: 157 - init_stock: 4300 - price: 175 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU776: - cost: 92 - init_stock: 4250 - price: 103 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU777: - cost: 262 - init_stock: 2850 - price: 292 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU778: - cost: 182 - init_stock: 400 - price: 203 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU779: - cost: 105 - init_stock: 2150 - price: 117 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU78: - cost: 210 - init_stock: 650 - price: 234 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU780: - cost: 62 - init_stock: 4100 - price: 69 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU781: - cost: 334 - init_stock: 1800 - price: 372 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU782: - cost: 24 - init_stock: 500 - price: 27 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU783: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU784: - cost: 278 - init_stock: 2600 - price: 309 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU785: - cost: 171 - init_stock: 3500 - price: 191 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU786: - cost: 81 - init_stock: 1100 - price: 91 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU787: - cost: 324 - init_stock: 3050 - price: 360 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU788: - cost: 315 - init_stock: 1400 - price: 351 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU789: - cost: 137 - init_stock: 2900 - price: 153 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU79: - cost: 220 - init_stock: 550 - price: 245 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU790: - cost: 375 - init_stock: 3300 - price: 417 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU791: - cost: 120 - init_stock: 1400 - price: 134 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU792: - cost: 281 - init_stock: 1050 - price: 313 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU793: - cost: 175 - init_stock: 3800 - price: 195 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU794: - cost: 292 - init_stock: 4000 - price: 325 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU795: - cost: 248 - init_stock: 2400 - price: 276 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU796: - cost: 402 - init_stock: 2450 - price: 447 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU797: - cost: 90 - init_stock: 1950 - price: 100 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU798: - cost: 383 - init_stock: 1500 - price: 426 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU799: - cost: 56 - init_stock: 350 - price: 63 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU8: - cost: 112 - init_stock: 3150 - price: 125 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU80: - cost: 146 - init_stock: 3500 - price: 163 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU800: - cost: 268 - init_stock: 4700 - price: 298 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU801: - cost: 350 - init_stock: 1800 - price: 389 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU802: - cost: 155 - init_stock: 3100 - price: 173 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU803: - cost: 244 - init_stock: 950 - price: 272 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU804: - cost: 351 - init_stock: 2350 - price: 390 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU805: - cost: 54 - init_stock: 1650 - price: 61 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU806: - cost: 142 - init_stock: 2600 - price: 158 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU807: - cost: 407 - init_stock: 1300 - price: 453 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU808: - cost: 224 - init_stock: 4550 - price: 249 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU809: - cost: 49 - init_stock: 3250 - price: 55 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU81: - cost: 163 - init_stock: 3300 - price: 182 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU810: - cost: 248 - init_stock: 300 - price: 276 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU811: - cost: 228 - init_stock: 1850 - price: 254 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU812: - cost: 226 - init_stock: 4000 - price: 252 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU813: - cost: 227 - init_stock: 350 - price: 253 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU814: - cost: 436 - init_stock: 4850 - price: 485 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU815: - cost: 351 - init_stock: 1200 - price: 390 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU816: - cost: 136 - init_stock: 4550 - price: 152 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU817: - cost: 204 - init_stock: 250 - price: 227 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU818: - cost: 318 - init_stock: 2150 - price: 354 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU819: - cost: 271 - init_stock: 1350 - price: 302 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU82: - cost: 261 - init_stock: 1400 - price: 290 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU820: - cost: 237 - init_stock: 4100 - price: 264 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU821: - cost: 89 - init_stock: 3450 - price: 99 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU822: - cost: 122 - init_stock: 3500 - price: 136 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU823: - cost: 67 - init_stock: 1400 - price: 75 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU824: - cost: 153 - init_stock: 3500 - price: 170 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU825: - cost: 192 - init_stock: 750 - price: 214 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU826: - cost: 347 - init_stock: 2750 - price: 386 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU827: - cost: 133 - init_stock: 3200 - price: 148 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU828: - cost: 360 - init_stock: 3450 - price: 400 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU829: - cost: 54 - init_stock: 3700 - price: 61 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU83: - cost: 266 - init_stock: 850 - price: 296 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU830: - cost: 150 - init_stock: 1200 - price: 167 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU831: - cost: 235 - init_stock: 1450 - price: 262 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU832: - cost: 29 - init_stock: 4100 - price: 33 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU833: - cost: 360 - init_stock: 4900 - price: 400 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU834: - cost: 379 - init_stock: 250 - price: 422 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU835: - cost: 396 - init_stock: 2600 - price: 440 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU836: - cost: 290 - init_stock: 4800 - price: 323 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU837: - cost: 335 - init_stock: 1300 - price: 373 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU838: - cost: 410 - init_stock: 3850 - price: 456 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU839: - cost: 425 - init_stock: 3000 - price: 473 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU84: - cost: 286 - init_stock: 2100 - price: 318 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU840: - cost: 239 - init_stock: 2500 - price: 266 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU841: - cost: 256 - init_stock: 4250 - price: 285 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU842: - cost: 36 - init_stock: 4200 - price: 41 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU843: - cost: 324 - init_stock: 3600 - price: 360 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU844: - cost: 45 - init_stock: 3950 - price: 51 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU845: - cost: 259 - init_stock: 1100 - price: 288 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU846: - cost: 436 - init_stock: 1950 - price: 485 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU847: - cost: 349 - init_stock: 4550 - price: 388 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU848: - cost: 275 - init_stock: 2300 - price: 306 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU849: - cost: 288 - init_stock: 2000 - price: 320 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU85: - cost: 397 - init_stock: 5000 - price: 442 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU850: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU851: - cost: 47 - init_stock: 3200 - price: 53 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU852: - cost: 275 - init_stock: 2700 - price: 306 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU853: - cost: 347 - init_stock: 2800 - price: 386 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU854: - cost: 190 - init_stock: 3250 - price: 212 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU855: - cost: 68 - init_stock: 3600 - price: 76 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU856: - cost: 342 - init_stock: 1600 - price: 380 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU857: - cost: 415 - init_stock: 5000 - price: 462 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU858: - cost: 72 - init_stock: 1200 - price: 80 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU859: - cost: 193 - init_stock: 1400 - price: 215 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU86: - cost: 347 - init_stock: 4250 - price: 386 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU860: - cost: 281 - init_stock: 750 - price: 313 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU861: - cost: 429 - init_stock: 650 - price: 477 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU862: - cost: 216 - init_stock: 800 - price: 240 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU863: - cost: 423 - init_stock: 4650 - price: 470 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU864: - cost: 182 - init_stock: 950 - price: 203 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU865: - cost: 129 - init_stock: 950 - price: 144 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU866: - cost: 154 - init_stock: 4400 - price: 172 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU867: - cost: 449 - init_stock: 5000 - price: 499 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU868: - cost: 36 - init_stock: 2500 - price: 41 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU869: - cost: 87 - init_stock: 4850 - price: 97 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU87: - cost: 331 - init_stock: 3450 - price: 368 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU870: - cost: 252 - init_stock: 3350 - price: 281 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU871: - cost: 90 - init_stock: 4950 - price: 101 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU872: - cost: 119 - init_stock: 1600 - price: 133 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU873: - cost: 380 - init_stock: 1550 - price: 423 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU874: - cost: 422 - init_stock: 3000 - price: 469 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU875: - cost: 45 - init_stock: 1150 - price: 51 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU876: - cost: 272 - init_stock: 3050 - price: 303 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU877: - cost: 36 - init_stock: 1750 - price: 40 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU878: - cost: 24 - init_stock: 3400 - price: 27 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU879: - cost: 135 - init_stock: 5000 - price: 150 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU88: - cost: 423 - init_stock: 600 - price: 471 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU880: - cost: 389 - init_stock: 1550 - price: 433 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU881: - cost: 387 - init_stock: 3500 - price: 431 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU882: - cost: 174 - init_stock: 800 - price: 194 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU883: - cost: 255 - init_stock: 1900 - price: 284 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU884: - cost: 125 - init_stock: 1950 - price: 139 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU885: - cost: 44 - init_stock: 1350 - price: 49 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU886: - cost: 334 - init_stock: 3650 - price: 372 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU887: - cost: 239 - init_stock: 4350 - price: 266 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU888: - cost: 128 - init_stock: 450 - price: 143 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU889: - cost: 90 - init_stock: 1050 - price: 101 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU89: - cost: 124 - init_stock: 4650 - price: 138 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU890: - cost: 144 - init_stock: 5000 - price: 161 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU891: - cost: 320 - init_stock: 1100 - price: 356 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU892: - cost: 281 - init_stock: 4600 - price: 313 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU893: - cost: 206 - init_stock: 4950 - price: 229 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU894: - cost: 116 - init_stock: 3700 - price: 129 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU895: - cost: 207 - init_stock: 650 - price: 230 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU896: - cost: 260 - init_stock: 4000 - price: 289 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU897: - cost: 353 - init_stock: 3600 - price: 393 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU898: - cost: 429 - init_stock: 550 - price: 477 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU899: - cost: 209 - init_stock: 2400 - price: 233 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU9: - cost: 149 - init_stock: 4800 - price: 166 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU90: - cost: 408 - init_stock: 2550 - price: 454 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU900: - cost: 142 - init_stock: 2750 - price: 158 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU901: - cost: 193 - init_stock: 1450 - price: 215 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU902: - cost: 112 - init_stock: 4000 - price: 125 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU903: - cost: 321 - init_stock: 1900 - price: 357 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU904: - cost: 446 - init_stock: 2550 - price: 496 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU905: - cost: 224 - init_stock: 1550 - price: 249 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU906: - cost: 149 - init_stock: 2600 - price: 166 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU907: - cost: 19 - init_stock: 4150 - price: 22 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU908: - cost: 367 - init_stock: 3900 - price: 408 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU909: - cost: 433 - init_stock: 4800 - price: 482 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU91: - cost: 272 - init_stock: 800 - price: 303 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU910: - cost: 203 - init_stock: 1650 - price: 226 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU911: - cost: 414 - init_stock: 3500 - price: 461 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU912: - cost: 212 - init_stock: 1350 - price: 236 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU913: - cost: 289 - init_stock: 2300 - price: 322 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU914: - cost: 244 - init_stock: 3100 - price: 272 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU915: - cost: 303 - init_stock: 2800 - price: 337 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU916: - cost: 303 - init_stock: 4450 - price: 337 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU917: - cost: 232 - init_stock: 1500 - price: 258 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU918: - cost: 133 - init_stock: 2750 - price: 148 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU919: - cost: 353 - init_stock: 1250 - price: 393 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU92: - cost: 235 - init_stock: 3150 - price: 262 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU920: - cost: 321 - init_stock: 4300 - price: 357 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU921: - cost: 204 - init_stock: 3300 - price: 227 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU922: - cost: 100 - init_stock: 3350 - price: 112 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU923: - cost: 446 - init_stock: 2900 - price: 496 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU924: - cost: 284 - init_stock: 4250 - price: 316 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU925: - cost: 324 - init_stock: 750 - price: 360 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU926: - cost: 324 - init_stock: 3350 - price: 360 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU927: - cost: 234 - init_stock: 1050 - price: 260 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU928: - cost: 441 - init_stock: 4150 - price: 491 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU929: - cost: 323 - init_stock: 5000 - price: 359 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU93: - cost: 363 - init_stock: 3350 - price: 404 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU930: - cost: 178 - init_stock: 1400 - price: 198 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU931: - cost: 63 - init_stock: 700 - price: 71 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU932: - cost: 146 - init_stock: 3300 - price: 163 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU933: - cost: 101 - init_stock: 3900 - price: 113 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU934: - cost: 197 - init_stock: 3350 - price: 219 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU935: - cost: 327 - init_stock: 4700 - price: 364 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU936: - cost: 21 - init_stock: 250 - price: 24 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU937: - cost: 121 - init_stock: 850 - price: 135 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU938: - cost: 388 - init_stock: 1050 - price: 432 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU939: - cost: 155 - init_stock: 2950 - price: 173 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU94: - cost: 165 - init_stock: 2350 - price: 184 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU940: - cost: 12 - init_stock: 4650 - price: 14 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU941: - cost: 72 - init_stock: 2850 - price: 80 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU942: - cost: 181 - init_stock: 650 - price: 202 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU943: - cost: 124 - init_stock: 2450 - price: 138 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU944: - cost: 176 - init_stock: 2200 - price: 196 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU945: - cost: 126 - init_stock: 850 - price: 141 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU946: - cost: 292 - init_stock: 2450 - price: 325 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU947: - cost: 304 - init_stock: 4550 - price: 338 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU948: - cost: 382 - init_stock: 1400 - price: 425 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU949: - cost: 278 - init_stock: 250 - price: 309 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU95: - cost: 122 - init_stock: 1050 - price: 136 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU950: - cost: 91 - init_stock: 2700 - price: 102 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU951: - cost: 67 - init_stock: 900 - price: 75 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU952: - cost: 140 - init_stock: 550 - price: 156 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU953: - cost: 124 - init_stock: 1750 - price: 138 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU954: - cost: 266 - init_stock: 4300 - price: 296 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU955: - cost: 49 - init_stock: 3150 - price: 55 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU956: - cost: 253 - init_stock: 4250 - price: 282 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU957: - cost: 274 - init_stock: 2550 - price: 305 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU958: - cost: 332 - init_stock: 3300 - price: 369 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU959: - cost: 72 - init_stock: 4100 - price: 81 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU96: - cost: 158 - init_stock: 2200 - price: 176 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU960: - cost: 132 - init_stock: 300 - price: 147 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU961: - cost: 237 - init_stock: 2200 - price: 264 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU962: - cost: 318 - init_stock: 2200 - price: 354 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU963: - cost: 314 - init_stock: 1050 - price: 349 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU964: - cost: 219 - init_stock: 3650 - price: 244 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU965: - cost: 111 - init_stock: 2550 - price: 124 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU966: - cost: 271 - init_stock: 2200 - price: 302 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU967: - cost: 60 - init_stock: 2250 - price: 67 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU968: - cost: 252 - init_stock: 2450 - price: 281 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU969: - cost: 224 - init_stock: 300 - price: 249 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU97: - cost: 25 - init_stock: 1500 - price: 28 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU970: - cost: 219 - init_stock: 250 - price: 244 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU971: - cost: 331 - init_stock: 3650 - price: 368 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU972: - cost: 188 - init_stock: 3450 - price: 209 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU973: - cost: 243 - init_stock: 550 - price: 271 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU974: - cost: 153 - init_stock: 1600 - price: 170 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU975: - cost: 178 - init_stock: 1100 - price: 198 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU976: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU977: - cost: 210 - init_stock: 2700 - price: 234 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU978: - cost: 423 - init_stock: 3200 - price: 470 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU979: - cost: 398 - init_stock: 4800 - price: 443 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU98: - cost: 398 - init_stock: 1750 - price: 443 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU980: - cost: 35 - init_stock: 3400 - price: 39 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU981: - cost: 433 - init_stock: 3600 - price: 482 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU982: - cost: 191 - init_stock: 2600 - price: 213 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU983: - cost: 404 - init_stock: 4600 - price: 449 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU984: - cost: 208 - init_stock: 4550 - price: 232 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU985: - cost: 261 - init_stock: 2550 - price: 290 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU986: - cost: 247 - init_stock: 850 - price: 275 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU987: - cost: 390 - init_stock: 1700 - price: 434 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU988: - cost: 91 - init_stock: 1150 - price: 102 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU989: - cost: 435 - init_stock: 4650 - price: 484 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU99: - cost: 324 - init_stock: 2250 - price: 361 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU990: - cost: 97 - init_stock: 2850 - price: 108 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU991: - cost: 368 - init_stock: 950 - price: 409 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU992: - cost: 390 - init_stock: 4650 - price: 434 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU993: - cost: 130 - init_stock: 4300 - price: 145 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU994: - cost: 423 - init_stock: 2500 - price: 470 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU995: - cost: 216 - init_stock: 3800 - price: 241 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU996: - cost: 234 - init_stock: 3650 - price: 260 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU997: - cost: 360 - init_stock: 1700 - price: 400 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU998: - cost: 402 - init_stock: 2750 - price: 447 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU999: - cost: 71 - init_stock: 1150 - price: 79 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 500 - definition_ref: WarehouseFacility - name: WAREHOUSE0 - skus: - SKU0: - cost: 322 - init_stock: 1260 - price: 322 - service_level: 0.96 - vlt: 1 - SKU1: - cost: 284 - init_stock: 440 - price: 284 - service_level: 0.96 - vlt: 3 - SKU10: - cost: 68 - init_stock: 500 - price: 68 - service_level: 0.96 - vlt: 3 - SKU100: - cost: 16 - init_stock: 1300 - price: 16 - service_level: 0.96 - vlt: 3 - SKU101: - cost: 84 - init_stock: 1420 - price: 84 - service_level: 0.96 - vlt: 3 - SKU102: - cost: 328 - init_stock: 1860 - price: 328 - service_level: 0.96 - vlt: 2 - SKU103: - cost: 334 - init_stock: 1900 - price: 334 - service_level: 0.96 - vlt: 1 - SKU104: - cost: 49 - init_stock: 1300 - price: 49 - service_level: 0.96 - vlt: 3 - SKU105: - cost: 110 - init_stock: 1140 - price: 110 - service_level: 0.96 - vlt: 2 - SKU106: - cost: 251 - init_stock: 1460 - price: 251 - service_level: 0.96 - vlt: 3 - SKU107: - cost: 423 - init_stock: 1740 - price: 423 - service_level: 0.96 - vlt: 3 - SKU108: - cost: 458 - init_stock: 1840 - price: 458 - service_level: 0.96 - vlt: 3 - SKU109: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU11: - cost: 400 - init_stock: 1020 - price: 400 - service_level: 0.96 - vlt: 2 - SKU110: - cost: 66 - init_stock: 280 - price: 66 - service_level: 0.96 - vlt: 3 - SKU111: - cost: 260 - init_stock: 1220 - price: 260 - service_level: 0.96 - vlt: 1 - SKU112: - cost: 61 - init_stock: 1900 - price: 61 - service_level: 0.96 - vlt: 2 - SKU113: - cost: 348 - init_stock: 620 - price: 348 - service_level: 0.96 - vlt: 3 - SKU114: - cost: 389 - init_stock: 540 - price: 389 - service_level: 0.96 - vlt: 1 - SKU115: - cost: 286 - init_stock: 1720 - price: 286 - service_level: 0.96 - vlt: 2 - SKU116: - cost: 496 - init_stock: 1440 - price: 496 - service_level: 0.96 - vlt: 3 - SKU117: - cost: 320 - init_stock: 1840 - price: 320 - service_level: 0.96 - vlt: 3 - SKU118: - cost: 183 - init_stock: 660 - price: 183 - service_level: 0.96 - vlt: 1 - SKU119: - cost: 209 - init_stock: 640 - price: 209 - service_level: 0.96 - vlt: 3 - SKU12: - cost: 112 - init_stock: 1680 - price: 112 - service_level: 0.96 - vlt: 1 - SKU120: - cost: 121 - init_stock: 1960 - price: 121 - service_level: 0.96 - vlt: 1 - SKU121: - cost: 40 - init_stock: 1700 - price: 40 - service_level: 0.96 - vlt: 1 - SKU122: - cost: 437 - init_stock: 140 - price: 437 - service_level: 0.96 - vlt: 3 - SKU123: - cost: 233 - init_stock: 380 - price: 233 - service_level: 0.96 - vlt: 3 - SKU124: - cost: 182 - init_stock: 720 - price: 182 - service_level: 0.96 - vlt: 2 - SKU125: - cost: 16 - init_stock: 1840 - price: 16 - service_level: 0.96 - vlt: 2 - SKU126: - cost: 36 - init_stock: 780 - price: 36 - service_level: 0.96 - vlt: 3 - SKU127: - cost: 217 - init_stock: 620 - price: 217 - service_level: 0.96 - vlt: 2 - SKU128: - cost: 165 - init_stock: 380 - price: 165 - service_level: 0.96 - vlt: 1 - SKU129: - cost: 143 - init_stock: 1000 - price: 143 - service_level: 0.96 - vlt: 2 - SKU13: - cost: 317 - init_stock: 1140 - price: 317 - service_level: 0.96 - vlt: 3 - SKU130: - cost: 348 - init_stock: 1960 - price: 348 - service_level: 0.96 - vlt: 3 - SKU131: - cost: 64 - init_stock: 900 - price: 64 - service_level: 0.96 - vlt: 1 - SKU132: - cost: 427 - init_stock: 420 - price: 427 - service_level: 0.96 - vlt: 2 - SKU133: - cost: 224 - init_stock: 580 - price: 224 - service_level: 0.96 - vlt: 2 - SKU134: - cost: 336 - init_stock: 1540 - price: 336 - service_level: 0.96 - vlt: 3 - SKU135: - cost: 153 - init_stock: 2000 - price: 153 - service_level: 0.96 - vlt: 1 - SKU136: - cost: 199 - init_stock: 1420 - price: 199 - service_level: 0.96 - vlt: 3 - SKU137: - cost: 93 - init_stock: 1480 - price: 93 - service_level: 0.96 - vlt: 2 - SKU138: - cost: 228 - init_stock: 720 - price: 228 - service_level: 0.96 - vlt: 1 - SKU139: - cost: 207 - init_stock: 480 - price: 207 - service_level: 0.96 - vlt: 1 - SKU14: - cost: 268 - init_stock: 1240 - price: 268 - service_level: 0.96 - vlt: 1 - SKU140: - cost: 261 - init_stock: 680 - price: 261 - service_level: 0.96 - vlt: 3 - SKU141: - cost: 190 - init_stock: 820 - price: 190 - service_level: 0.96 - vlt: 1 - SKU142: - cost: 320 - init_stock: 760 - price: 320 - service_level: 0.96 - vlt: 3 - SKU143: - cost: 318 - init_stock: 520 - price: 318 - service_level: 0.96 - vlt: 3 - SKU144: - cost: 400 - init_stock: 240 - price: 400 - service_level: 0.96 - vlt: 1 - SKU145: - cost: 399 - init_stock: 1640 - price: 399 - service_level: 0.96 - vlt: 1 - SKU146: - cost: 177 - init_stock: 960 - price: 177 - service_level: 0.96 - vlt: 3 - SKU147: - cost: 472 - init_stock: 1120 - price: 472 - service_level: 0.96 - vlt: 3 - SKU148: - cost: 313 - init_stock: 1540 - price: 313 - service_level: 0.96 - vlt: 3 - SKU149: - cost: 357 - init_stock: 1540 - price: 357 - service_level: 0.96 - vlt: 3 - SKU15: - cost: 52 - init_stock: 100 - price: 52 - service_level: 0.96 - vlt: 2 - SKU150: - cost: 106 - init_stock: 1400 - price: 106 - service_level: 0.96 - vlt: 2 - SKU151: - cost: 223 - init_stock: 1460 - price: 223 - service_level: 0.96 - vlt: 1 - SKU152: - cost: 10 - init_stock: 1020 - price: 10 - service_level: 0.96 - vlt: 3 - SKU153: - cost: 441 - init_stock: 1240 - price: 441 - service_level: 0.96 - vlt: 1 - SKU154: - cost: 77 - init_stock: 1700 - price: 77 - service_level: 0.96 - vlt: 3 - SKU155: - cost: 422 - init_stock: 1060 - price: 422 - service_level: 0.96 - vlt: 1 - SKU156: - cost: 10 - init_stock: 240 - price: 10 - service_level: 0.96 - vlt: 1 - SKU157: - cost: 410 - init_stock: 1500 - price: 410 - service_level: 0.96 - vlt: 2 - SKU158: - cost: 145 - init_stock: 1620 - price: 145 - service_level: 0.96 - vlt: 3 - SKU159: - cost: 193 - init_stock: 500 - price: 193 - service_level: 0.96 - vlt: 2 - SKU16: - cost: 175 - init_stock: 1160 - price: 175 - service_level: 0.96 - vlt: 3 - SKU160: - cost: 459 - init_stock: 1700 - price: 459 - service_level: 0.96 - vlt: 1 - SKU161: - cost: 239 - init_stock: 920 - price: 239 - service_level: 0.96 - vlt: 2 - SKU162: - cost: 158 - init_stock: 100 - price: 158 - service_level: 0.96 - vlt: 2 - SKU163: - cost: 486 - init_stock: 780 - price: 486 - service_level: 0.96 - vlt: 2 - SKU164: - cost: 496 - init_stock: 2000 - price: 496 - service_level: 0.96 - vlt: 1 - SKU165: - cost: 274 - init_stock: 660 - price: 274 - service_level: 0.96 - vlt: 3 - SKU166: - cost: 79 - init_stock: 1780 - price: 79 - service_level: 0.96 - vlt: 1 - SKU167: - cost: 443 - init_stock: 260 - price: 443 - service_level: 0.96 - vlt: 1 - SKU168: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU169: - cost: 369 - init_stock: 1960 - price: 369 - service_level: 0.96 - vlt: 3 - SKU17: - cost: 346 - init_stock: 180 - price: 346 - service_level: 0.96 - vlt: 1 - SKU170: - cost: 68 - init_stock: 1100 - price: 68 - service_level: 0.96 - vlt: 2 - SKU171: - cost: 398 - init_stock: 1520 - price: 398 - service_level: 0.96 - vlt: 1 - SKU172: - cost: 200 - init_stock: 1420 - price: 200 - service_level: 0.96 - vlt: 2 - SKU173: - cost: 175 - init_stock: 1920 - price: 175 - service_level: 0.96 - vlt: 3 - SKU174: - cost: 291 - init_stock: 1520 - price: 291 - service_level: 0.96 - vlt: 2 - SKU175: - cost: 84 - init_stock: 1500 - price: 84 - service_level: 0.96 - vlt: 2 - SKU176: - cost: 407 - init_stock: 1320 - price: 407 - service_level: 0.96 - vlt: 1 - SKU177: - cost: 257 - init_stock: 620 - price: 257 - service_level: 0.96 - vlt: 1 - SKU178: - cost: 423 - init_stock: 100 - price: 423 - service_level: 0.96 - vlt: 2 - SKU179: - cost: 497 - init_stock: 1660 - price: 497 - service_level: 0.96 - vlt: 1 - SKU18: - cost: 258 - init_stock: 1620 - price: 258 - service_level: 0.96 - vlt: 1 - SKU180: - cost: 217 - init_stock: 1100 - price: 217 - service_level: 0.96 - vlt: 2 - SKU181: - cost: 143 - init_stock: 1200 - price: 143 - service_level: 0.96 - vlt: 1 - SKU182: - cost: 437 - init_stock: 1980 - price: 437 - service_level: 0.96 - vlt: 3 - SKU183: - cost: 145 - init_stock: 160 - price: 145 - service_level: 0.96 - vlt: 3 - SKU184: - cost: 73 - init_stock: 480 - price: 73 - service_level: 0.96 - vlt: 2 - SKU185: - cost: 10 - init_stock: 1800 - price: 10 - service_level: 0.96 - vlt: 2 - SKU186: - cost: 359 - init_stock: 440 - price: 359 - service_level: 0.96 - vlt: 1 - SKU187: - cost: 177 - init_stock: 600 - price: 177 - service_level: 0.96 - vlt: 3 - SKU188: - cost: 391 - init_stock: 1740 - price: 391 - service_level: 0.96 - vlt: 3 - SKU189: - cost: 358 - init_stock: 700 - price: 358 - service_level: 0.96 - vlt: 2 - SKU19: - cost: 477 - init_stock: 1820 - price: 477 - service_level: 0.96 - vlt: 3 - SKU190: - cost: 113 - init_stock: 340 - price: 113 - service_level: 0.96 - vlt: 3 - SKU191: - cost: 473 - init_stock: 1080 - price: 473 - service_level: 0.96 - vlt: 2 - SKU192: - cost: 415 - init_stock: 1220 - price: 415 - service_level: 0.96 - vlt: 2 - SKU193: - cost: 207 - init_stock: 600 - price: 207 - service_level: 0.96 - vlt: 2 - SKU194: - cost: 432 - init_stock: 100 - price: 432 - service_level: 0.96 - vlt: 2 - SKU195: - cost: 218 - init_stock: 620 - price: 218 - service_level: 0.96 - vlt: 2 - SKU196: - cost: 49 - init_stock: 1360 - price: 49 - service_level: 0.96 - vlt: 3 - SKU197: - cost: 303 - init_stock: 1140 - price: 303 - service_level: 0.96 - vlt: 1 - SKU198: - cost: 169 - init_stock: 1080 - price: 169 - service_level: 0.96 - vlt: 2 - SKU199: - cost: 449 - init_stock: 460 - price: 449 - service_level: 0.96 - vlt: 1 - SKU2: - cost: 331 - init_stock: 1400 - price: 331 - service_level: 0.96 - vlt: 3 - SKU20: - cost: 335 - init_stock: 500 - price: 335 - service_level: 0.96 - vlt: 3 - SKU200: - cost: 65 - init_stock: 500 - price: 65 - service_level: 0.96 - vlt: 1 - SKU201: - cost: 104 - init_stock: 1180 - price: 104 - service_level: 0.96 - vlt: 1 - SKU202: - cost: 142 - init_stock: 1460 - price: 142 - service_level: 0.96 - vlt: 1 - SKU203: - cost: 440 - init_stock: 1640 - price: 440 - service_level: 0.96 - vlt: 2 - SKU204: - cost: 489 - init_stock: 940 - price: 489 - service_level: 0.96 - vlt: 2 - SKU205: - cost: 130 - init_stock: 2000 - price: 130 - service_level: 0.96 - vlt: 3 - SKU206: - cost: 335 - init_stock: 220 - price: 335 - service_level: 0.96 - vlt: 2 - SKU207: - cost: 140 - init_stock: 1600 - price: 140 - service_level: 0.96 - vlt: 1 - SKU208: - cost: 491 - init_stock: 1540 - price: 491 - service_level: 0.96 - vlt: 1 - SKU209: - cost: 179 - init_stock: 400 - price: 179 - service_level: 0.96 - vlt: 3 - SKU21: - cost: 123 - init_stock: 2000 - price: 123 - service_level: 0.96 - vlt: 2 - SKU210: - cost: 404 - init_stock: 1380 - price: 404 - service_level: 0.96 - vlt: 3 - SKU211: - cost: 174 - init_stock: 1820 - price: 174 - service_level: 0.96 - vlt: 2 - SKU212: - cost: 405 - init_stock: 1580 - price: 405 - service_level: 0.96 - vlt: 3 - SKU213: - cost: 121 - init_stock: 1280 - price: 121 - service_level: 0.96 - vlt: 2 - SKU214: - cost: 101 - init_stock: 200 - price: 101 - service_level: 0.96 - vlt: 2 - SKU215: - cost: 419 - init_stock: 940 - price: 419 - service_level: 0.96 - vlt: 1 - SKU216: - cost: 330 - init_stock: 460 - price: 330 - service_level: 0.96 - vlt: 1 - SKU217: - cost: 284 - init_stock: 1300 - price: 284 - service_level: 0.96 - vlt: 2 - SKU218: - cost: 205 - init_stock: 1180 - price: 205 - service_level: 0.96 - vlt: 1 - SKU219: - cost: 92 - init_stock: 920 - price: 92 - service_level: 0.96 - vlt: 3 - SKU22: - cost: 493 - init_stock: 1320 - price: 493 - service_level: 0.96 - vlt: 1 - SKU220: - cost: 387 - init_stock: 1740 - price: 387 - service_level: 0.96 - vlt: 2 - SKU221: - cost: 39 - init_stock: 1560 - price: 39 - service_level: 0.96 - vlt: 2 - SKU222: - cost: 115 - init_stock: 720 - price: 115 - service_level: 0.96 - vlt: 2 - SKU223: - cost: 196 - init_stock: 240 - price: 196 - service_level: 0.96 - vlt: 2 - SKU224: - cost: 387 - init_stock: 100 - price: 387 - service_level: 0.96 - vlt: 2 - SKU225: - cost: 164 - init_stock: 580 - price: 164 - service_level: 0.96 - vlt: 1 - SKU226: - cost: 206 - init_stock: 260 - price: 206 - service_level: 0.96 - vlt: 2 - SKU227: - cost: 479 - init_stock: 480 - price: 479 - service_level: 0.96 - vlt: 3 - SKU228: - cost: 14 - init_stock: 1800 - price: 14 - service_level: 0.96 - vlt: 1 - SKU229: - cost: 472 - init_stock: 880 - price: 472 - service_level: 0.96 - vlt: 1 - SKU23: - cost: 387 - init_stock: 840 - price: 387 - service_level: 0.96 - vlt: 1 - SKU230: - cost: 241 - init_stock: 460 - price: 241 - service_level: 0.96 - vlt: 3 - SKU231: - cost: 48 - init_stock: 1620 - price: 48 - service_level: 0.96 - vlt: 3 - SKU232: - cost: 224 - init_stock: 1920 - price: 224 - service_level: 0.96 - vlt: 1 - SKU233: - cost: 360 - init_stock: 1500 - price: 360 - service_level: 0.96 - vlt: 2 - SKU234: - cost: 287 - init_stock: 100 - price: 287 - service_level: 0.96 - vlt: 1 - SKU235: - cost: 24 - init_stock: 1140 - price: 24 - service_level: 0.96 - vlt: 3 - SKU236: - cost: 155 - init_stock: 1100 - price: 155 - service_level: 0.96 - vlt: 2 - SKU237: - cost: 433 - init_stock: 900 - price: 433 - service_level: 0.96 - vlt: 3 - SKU238: - cost: 64 - init_stock: 1320 - price: 64 - service_level: 0.96 - vlt: 3 - SKU239: - cost: 103 - init_stock: 1960 - price: 103 - service_level: 0.96 - vlt: 1 - SKU24: - cost: 97 - init_stock: 940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU240: - cost: 373 - init_stock: 940 - price: 373 - service_level: 0.96 - vlt: 2 - SKU241: - cost: 439 - init_stock: 1420 - price: 439 - service_level: 0.96 - vlt: 2 - SKU242: - cost: 17 - init_stock: 880 - price: 17 - service_level: 0.96 - vlt: 1 - SKU243: - cost: 352 - init_stock: 280 - price: 352 - service_level: 0.96 - vlt: 1 - SKU244: - cost: 174 - init_stock: 1640 - price: 174 - service_level: 0.96 - vlt: 2 - SKU245: - cost: 404 - init_stock: 1320 - price: 404 - service_level: 0.96 - vlt: 2 - SKU246: - cost: 300 - init_stock: 600 - price: 300 - service_level: 0.96 - vlt: 2 - SKU247: - cost: 386 - init_stock: 700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU248: - cost: 355 - init_stock: 580 - price: 355 - service_level: 0.96 - vlt: 3 - SKU249: - cost: 356 - init_stock: 500 - price: 356 - service_level: 0.96 - vlt: 1 - SKU25: - cost: 161 - init_stock: 100 - price: 161 - service_level: 0.96 - vlt: 2 - SKU250: - cost: 127 - init_stock: 1080 - price: 127 - service_level: 0.96 - vlt: 3 - SKU251: - cost: 344 - init_stock: 1220 - price: 344 - service_level: 0.96 - vlt: 1 - SKU252: - cost: 181 - init_stock: 1660 - price: 181 - service_level: 0.96 - vlt: 1 - SKU253: - cost: 448 - init_stock: 320 - price: 448 - service_level: 0.96 - vlt: 1 - SKU254: - cost: 484 - init_stock: 920 - price: 484 - service_level: 0.96 - vlt: 3 - SKU255: - cost: 290 - init_stock: 1340 - price: 290 - service_level: 0.96 - vlt: 2 - SKU256: - cost: 91 - init_stock: 1440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU257: - cost: 348 - init_stock: 1140 - price: 348 - service_level: 0.96 - vlt: 1 - SKU258: - cost: 489 - init_stock: 860 - price: 489 - service_level: 0.96 - vlt: 1 - SKU259: - cost: 333 - init_stock: 1380 - price: 333 - service_level: 0.96 - vlt: 1 - SKU26: - cost: 229 - init_stock: 1260 - price: 229 - service_level: 0.96 - vlt: 1 - SKU260: - cost: 487 - init_stock: 1040 - price: 487 - service_level: 0.96 - vlt: 3 - SKU261: - cost: 368 - init_stock: 440 - price: 368 - service_level: 0.96 - vlt: 1 - SKU262: - cost: 332 - init_stock: 1560 - price: 332 - service_level: 0.96 - vlt: 3 - SKU263: - cost: 189 - init_stock: 1480 - price: 189 - service_level: 0.96 - vlt: 3 - SKU264: - cost: 361 - init_stock: 1580 - price: 361 - service_level: 0.96 - vlt: 1 - SKU265: - cost: 286 - init_stock: 1180 - price: 286 - service_level: 0.96 - vlt: 3 - SKU266: - cost: 128 - init_stock: 940 - price: 128 - service_level: 0.96 - vlt: 2 - SKU267: - cost: 77 - init_stock: 1600 - price: 77 - service_level: 0.96 - vlt: 1 - SKU268: - cost: 221 - init_stock: 1780 - price: 221 - service_level: 0.96 - vlt: 2 - SKU269: - cost: 126 - init_stock: 880 - price: 126 - service_level: 0.96 - vlt: 2 - SKU27: - cost: 370 - init_stock: 1120 - price: 370 - service_level: 0.96 - vlt: 3 - SKU270: - cost: 182 - init_stock: 1480 - price: 182 - service_level: 0.96 - vlt: 3 - SKU271: - cost: 230 - init_stock: 360 - price: 230 - service_level: 0.96 - vlt: 1 - SKU272: - cost: 366 - init_stock: 340 - price: 366 - service_level: 0.96 - vlt: 2 - SKU273: - cost: 421 - init_stock: 360 - price: 421 - service_level: 0.96 - vlt: 2 - SKU274: - cost: 29 - init_stock: 1540 - price: 29 - service_level: 0.96 - vlt: 2 - SKU275: - cost: 50 - init_stock: 960 - price: 50 - service_level: 0.96 - vlt: 1 - SKU276: - cost: 163 - init_stock: 1080 - price: 163 - service_level: 0.96 - vlt: 3 - SKU277: - cost: 449 - init_stock: 820 - price: 449 - service_level: 0.96 - vlt: 1 - SKU278: - cost: 105 - init_stock: 240 - price: 105 - service_level: 0.96 - vlt: 3 - SKU279: - cost: 51 - init_stock: 780 - price: 51 - service_level: 0.96 - vlt: 2 - SKU28: - cost: 208 - init_stock: 940 - price: 208 - service_level: 0.96 - vlt: 2 - SKU280: - cost: 295 - init_stock: 660 - price: 295 - service_level: 0.96 - vlt: 2 - SKU281: - cost: 395 - init_stock: 1500 - price: 395 - service_level: 0.96 - vlt: 1 - SKU282: - cost: 63 - init_stock: 920 - price: 63 - service_level: 0.96 - vlt: 2 - SKU283: - cost: 392 - init_stock: 1840 - price: 392 - service_level: 0.96 - vlt: 3 - SKU284: - cost: 344 - init_stock: 1340 - price: 344 - service_level: 0.96 - vlt: 3 - SKU285: - cost: 133 - init_stock: 1820 - price: 133 - service_level: 0.96 - vlt: 2 - SKU286: - cost: 337 - init_stock: 780 - price: 337 - service_level: 0.96 - vlt: 1 - SKU287: - cost: 375 - init_stock: 1120 - price: 375 - service_level: 0.96 - vlt: 3 - SKU288: - cost: 181 - init_stock: 760 - price: 181 - service_level: 0.96 - vlt: 1 - SKU289: - cost: 67 - init_stock: 620 - price: 67 - service_level: 0.96 - vlt: 1 - SKU29: - cost: 245 - init_stock: 1160 - price: 245 - service_level: 0.96 - vlt: 1 - SKU290: - cost: 438 - init_stock: 1340 - price: 438 - service_level: 0.96 - vlt: 1 - SKU291: - cost: 94 - init_stock: 1220 - price: 94 - service_level: 0.96 - vlt: 1 - SKU292: - cost: 186 - init_stock: 100 - price: 186 - service_level: 0.96 - vlt: 1 - SKU293: - cost: 162 - init_stock: 1100 - price: 162 - service_level: 0.96 - vlt: 2 - SKU294: - cost: 409 - init_stock: 180 - price: 409 - service_level: 0.96 - vlt: 2 - SKU295: - cost: 435 - init_stock: 1860 - price: 435 - service_level: 0.96 - vlt: 3 - SKU296: - cost: 370 - init_stock: 1840 - price: 370 - service_level: 0.96 - vlt: 3 - SKU297: - cost: 298 - init_stock: 760 - price: 298 - service_level: 0.96 - vlt: 1 - SKU298: - cost: 286 - init_stock: 700 - price: 286 - service_level: 0.96 - vlt: 2 - SKU299: - cost: 367 - init_stock: 1020 - price: 367 - service_level: 0.96 - vlt: 3 - SKU3: - cost: 405 - init_stock: 240 - price: 405 - service_level: 0.96 - vlt: 3 - SKU30: - cost: 359 - init_stock: 1380 - price: 359 - service_level: 0.96 - vlt: 2 - SKU300: - cost: 376 - init_stock: 1160 - price: 376 - service_level: 0.96 - vlt: 1 - SKU301: - cost: 433 - init_stock: 1660 - price: 433 - service_level: 0.96 - vlt: 2 - SKU302: - cost: 184 - init_stock: 220 - price: 184 - service_level: 0.96 - vlt: 2 - SKU303: - cost: 246 - init_stock: 1880 - price: 246 - service_level: 0.96 - vlt: 1 - SKU304: - cost: 419 - init_stock: 460 - price: 419 - service_level: 0.96 - vlt: 3 - SKU305: - cost: 495 - init_stock: 2000 - price: 495 - service_level: 0.96 - vlt: 1 - SKU306: - cost: 479 - init_stock: 840 - price: 479 - service_level: 0.96 - vlt: 2 - SKU307: - cost: 210 - init_stock: 1560 - price: 210 - service_level: 0.96 - vlt: 1 - SKU308: - cost: 208 - init_stock: 100 - price: 208 - service_level: 0.96 - vlt: 2 - SKU309: - cost: 101 - init_stock: 1840 - price: 101 - service_level: 0.96 - vlt: 2 - SKU31: - cost: 69 - init_stock: 1120 - price: 69 - service_level: 0.96 - vlt: 3 - SKU310: - cost: 234 - init_stock: 880 - price: 234 - service_level: 0.96 - vlt: 3 - SKU311: - cost: 306 - init_stock: 1600 - price: 306 - service_level: 0.96 - vlt: 3 - SKU312: - cost: 291 - init_stock: 500 - price: 291 - service_level: 0.96 - vlt: 1 - SKU313: - cost: 324 - init_stock: 760 - price: 324 - service_level: 0.96 - vlt: 1 - SKU314: - cost: 404 - init_stock: 580 - price: 404 - service_level: 0.96 - vlt: 1 - SKU315: - cost: 471 - init_stock: 1680 - price: 471 - service_level: 0.96 - vlt: 2 - SKU316: - cost: 202 - init_stock: 360 - price: 202 - service_level: 0.96 - vlt: 1 - SKU317: - cost: 216 - init_stock: 480 - price: 216 - service_level: 0.96 - vlt: 2 - SKU318: - cost: 278 - init_stock: 1700 - price: 278 - service_level: 0.96 - vlt: 1 - SKU319: - cost: 257 - init_stock: 1060 - price: 257 - service_level: 0.96 - vlt: 3 - SKU32: - cost: 336 - init_stock: 440 - price: 336 - service_level: 0.96 - vlt: 1 - SKU320: - cost: 196 - init_stock: 780 - price: 196 - service_level: 0.96 - vlt: 3 - SKU321: - cost: 67 - init_stock: 320 - price: 67 - service_level: 0.96 - vlt: 3 - SKU322: - cost: 240 - init_stock: 2000 - price: 240 - service_level: 0.96 - vlt: 1 - SKU323: - cost: 66 - init_stock: 780 - price: 66 - service_level: 0.96 - vlt: 2 - SKU324: - cost: 442 - init_stock: 1860 - price: 442 - service_level: 0.96 - vlt: 3 - SKU325: - cost: 453 - init_stock: 1380 - price: 453 - service_level: 0.96 - vlt: 2 - SKU326: - cost: 345 - init_stock: 480 - price: 345 - service_level: 0.96 - vlt: 2 - SKU327: - cost: 457 - init_stock: 280 - price: 457 - service_level: 0.96 - vlt: 2 - SKU328: - cost: 390 - init_stock: 900 - price: 390 - service_level: 0.96 - vlt: 1 - SKU329: - cost: 67 - init_stock: 840 - price: 67 - service_level: 0.96 - vlt: 1 - SKU33: - cost: 416 - init_stock: 1840 - price: 416 - service_level: 0.96 - vlt: 2 - SKU330: - cost: 306 - init_stock: 780 - price: 306 - service_level: 0.96 - vlt: 2 - SKU331: - cost: 138 - init_stock: 820 - price: 138 - service_level: 0.96 - vlt: 3 - SKU332: - cost: 390 - init_stock: 1920 - price: 390 - service_level: 0.96 - vlt: 2 - SKU333: - cost: 485 - init_stock: 1060 - price: 485 - service_level: 0.96 - vlt: 2 - SKU334: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 1 - SKU335: - cost: 80 - init_stock: 1620 - price: 80 - service_level: 0.96 - vlt: 1 - SKU336: - cost: 296 - init_stock: 560 - price: 296 - service_level: 0.96 - vlt: 1 - SKU337: - cost: 245 - init_stock: 580 - price: 245 - service_level: 0.96 - vlt: 2 - SKU338: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 3 - SKU339: - cost: 265 - init_stock: 1260 - price: 265 - service_level: 0.96 - vlt: 2 - SKU34: - cost: 95 - init_stock: 260 - price: 95 - service_level: 0.96 - vlt: 3 - SKU340: - cost: 480 - init_stock: 1740 - price: 480 - service_level: 0.96 - vlt: 2 - SKU341: - cost: 462 - init_stock: 1400 - price: 462 - service_level: 0.96 - vlt: 1 - SKU342: - cost: 144 - init_stock: 180 - price: 144 - service_level: 0.96 - vlt: 3 - SKU343: - cost: 310 - init_stock: 300 - price: 310 - service_level: 0.96 - vlt: 2 - SKU344: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU345: - cost: 442 - init_stock: 1780 - price: 442 - service_level: 0.96 - vlt: 2 - SKU346: - cost: 350 - init_stock: 840 - price: 350 - service_level: 0.96 - vlt: 3 - SKU347: - cost: 497 - init_stock: 1640 - price: 497 - service_level: 0.96 - vlt: 1 - SKU348: - cost: 147 - init_stock: 400 - price: 147 - service_level: 0.96 - vlt: 1 - SKU349: - cost: 67 - init_stock: 1340 - price: 67 - service_level: 0.96 - vlt: 3 - SKU35: - cost: 267 - init_stock: 1720 - price: 267 - service_level: 0.96 - vlt: 3 - SKU350: - cost: 279 - init_stock: 1840 - price: 279 - service_level: 0.96 - vlt: 3 - SKU351: - cost: 155 - init_stock: 1340 - price: 155 - service_level: 0.96 - vlt: 1 - SKU352: - cost: 71 - init_stock: 360 - price: 71 - service_level: 0.96 - vlt: 2 - SKU353: - cost: 253 - init_stock: 1860 - price: 253 - service_level: 0.96 - vlt: 2 - SKU354: - cost: 396 - init_stock: 1580 - price: 396 - service_level: 0.96 - vlt: 3 - SKU355: - cost: 63 - init_stock: 200 - price: 63 - service_level: 0.96 - vlt: 1 - SKU356: - cost: 348 - init_stock: 580 - price: 348 - service_level: 0.96 - vlt: 1 - SKU357: - cost: 499 - init_stock: 1840 - price: 499 - service_level: 0.96 - vlt: 3 - SKU358: - cost: 269 - init_stock: 1380 - price: 269 - service_level: 0.96 - vlt: 2 - SKU359: - cost: 82 - init_stock: 1500 - price: 82 - service_level: 0.96 - vlt: 3 - SKU36: - cost: 105 - init_stock: 200 - price: 105 - service_level: 0.96 - vlt: 2 - SKU360: - cost: 484 - init_stock: 500 - price: 484 - service_level: 0.96 - vlt: 1 - SKU361: - cost: 163 - init_stock: 1800 - price: 163 - service_level: 0.96 - vlt: 1 - SKU362: - cost: 464 - init_stock: 1740 - price: 464 - service_level: 0.96 - vlt: 2 - SKU363: - cost: 52 - init_stock: 1560 - price: 52 - service_level: 0.96 - vlt: 2 - SKU364: - cost: 387 - init_stock: 660 - price: 387 - service_level: 0.96 - vlt: 1 - SKU365: - cost: 158 - init_stock: 1660 - price: 158 - service_level: 0.96 - vlt: 3 - SKU366: - cost: 444 - init_stock: 1720 - price: 444 - service_level: 0.96 - vlt: 3 - SKU367: - cost: 272 - init_stock: 920 - price: 272 - service_level: 0.96 - vlt: 3 - SKU368: - cost: 472 - init_stock: 660 - price: 472 - service_level: 0.96 - vlt: 1 - SKU369: - cost: 96 - init_stock: 1500 - price: 96 - service_level: 0.96 - vlt: 2 - SKU37: - cost: 66 - init_stock: 1180 - price: 66 - service_level: 0.96 - vlt: 2 - SKU370: - cost: 112 - init_stock: 300 - price: 112 - service_level: 0.96 - vlt: 2 - SKU371: - cost: 328 - init_stock: 100 - price: 328 - service_level: 0.96 - vlt: 3 - SKU372: - cost: 67 - init_stock: 640 - price: 67 - service_level: 0.96 - vlt: 1 - SKU373: - cost: 58 - init_stock: 1340 - price: 58 - service_level: 0.96 - vlt: 3 - SKU374: - cost: 38 - init_stock: 1080 - price: 38 - service_level: 0.96 - vlt: 2 - SKU375: - cost: 283 - init_stock: 1440 - price: 283 - service_level: 0.96 - vlt: 1 - SKU376: - cost: 156 - init_stock: 1640 - price: 156 - service_level: 0.96 - vlt: 1 - SKU377: - cost: 78 - init_stock: 1340 - price: 78 - service_level: 0.96 - vlt: 3 - SKU378: - cost: 424 - init_stock: 700 - price: 424 - service_level: 0.96 - vlt: 1 - SKU379: - cost: 11 - init_stock: 980 - price: 11 - service_level: 0.96 - vlt: 1 - SKU38: - cost: 344 - init_stock: 860 - price: 344 - service_level: 0.96 - vlt: 2 - SKU380: - cost: 393 - init_stock: 1060 - price: 393 - service_level: 0.96 - vlt: 1 - SKU381: - cost: 476 - init_stock: 120 - price: 476 - service_level: 0.96 - vlt: 1 - SKU382: - cost: 125 - init_stock: 1420 - price: 125 - service_level: 0.96 - vlt: 2 - SKU383: - cost: 304 - init_stock: 1840 - price: 304 - service_level: 0.96 - vlt: 3 - SKU384: - cost: 320 - init_stock: 180 - price: 320 - service_level: 0.96 - vlt: 2 - SKU385: - cost: 120 - init_stock: 260 - price: 120 - service_level: 0.96 - vlt: 2 - SKU386: - cost: 295 - init_stock: 620 - price: 295 - service_level: 0.96 - vlt: 1 - SKU387: - cost: 112 - init_stock: 1940 - price: 112 - service_level: 0.96 - vlt: 3 - SKU388: - cost: 405 - init_stock: 880 - price: 405 - service_level: 0.96 - vlt: 2 - SKU389: - cost: 376 - init_stock: 1400 - price: 376 - service_level: 0.96 - vlt: 2 - SKU39: - cost: 25 - init_stock: 1020 - price: 25 - service_level: 0.96 - vlt: 3 - SKU390: - cost: 144 - init_stock: 780 - price: 144 - service_level: 0.96 - vlt: 2 - SKU391: - cost: 233 - init_stock: 1340 - price: 233 - service_level: 0.96 - vlt: 2 - SKU392: - cost: 163 - init_stock: 1480 - price: 163 - service_level: 0.96 - vlt: 2 - SKU393: - cost: 487 - init_stock: 1340 - price: 487 - service_level: 0.96 - vlt: 1 - SKU394: - cost: 154 - init_stock: 1060 - price: 154 - service_level: 0.96 - vlt: 2 - SKU395: - cost: 488 - init_stock: 660 - price: 488 - service_level: 0.96 - vlt: 3 - SKU396: - cost: 333 - init_stock: 1220 - price: 333 - service_level: 0.96 - vlt: 3 - SKU397: - cost: 460 - init_stock: 1020 - price: 460 - service_level: 0.96 - vlt: 1 - SKU398: - cost: 234 - init_stock: 1160 - price: 234 - service_level: 0.96 - vlt: 2 - SKU399: - cost: 376 - init_stock: 740 - price: 376 - service_level: 0.96 - vlt: 2 - SKU4: - cost: 439 - init_stock: 140 - price: 439 - service_level: 0.96 - vlt: 2 - SKU40: - cost: 490 - init_stock: 1980 - price: 490 - service_level: 0.96 - vlt: 3 - SKU400: - cost: 445 - init_stock: 1680 - price: 445 - service_level: 0.96 - vlt: 2 - SKU401: - cost: 410 - init_stock: 1560 - price: 410 - service_level: 0.96 - vlt: 1 - SKU402: - cost: 86 - init_stock: 140 - price: 86 - service_level: 0.96 - vlt: 3 - SKU403: - cost: 89 - init_stock: 1980 - price: 89 - service_level: 0.96 - vlt: 3 - SKU404: - cost: 287 - init_stock: 1220 - price: 287 - service_level: 0.96 - vlt: 1 - SKU405: - cost: 460 - init_stock: 380 - price: 460 - service_level: 0.96 - vlt: 1 - SKU406: - cost: 327 - init_stock: 2000 - price: 327 - service_level: 0.96 - vlt: 1 - SKU407: - cost: 26 - init_stock: 920 - price: 26 - service_level: 0.96 - vlt: 2 - SKU408: - cost: 444 - init_stock: 160 - price: 444 - service_level: 0.96 - vlt: 2 - SKU409: - cost: 257 - init_stock: 1820 - price: 257 - service_level: 0.96 - vlt: 2 - SKU41: - cost: 141 - init_stock: 1580 - price: 141 - service_level: 0.96 - vlt: 2 - SKU410: - cost: 70 - init_stock: 320 - price: 70 - service_level: 0.96 - vlt: 1 - SKU411: - cost: 210 - init_stock: 1900 - price: 210 - service_level: 0.96 - vlt: 3 - SKU412: - cost: 286 - init_stock: 1240 - price: 286 - service_level: 0.96 - vlt: 2 - SKU413: - cost: 403 - init_stock: 1660 - price: 403 - service_level: 0.96 - vlt: 3 - SKU414: - cost: 165 - init_stock: 1740 - price: 165 - service_level: 0.96 - vlt: 1 - SKU415: - cost: 291 - init_stock: 460 - price: 291 - service_level: 0.96 - vlt: 3 - SKU416: - cost: 228 - init_stock: 180 - price: 228 - service_level: 0.96 - vlt: 3 - SKU417: - cost: 443 - init_stock: 1440 - price: 443 - service_level: 0.96 - vlt: 1 - SKU418: - cost: 458 - init_stock: 260 - price: 458 - service_level: 0.96 - vlt: 3 - SKU419: - cost: 303 - init_stock: 1780 - price: 303 - service_level: 0.96 - vlt: 3 - SKU42: - cost: 462 - init_stock: 520 - price: 462 - service_level: 0.96 - vlt: 3 - SKU420: - cost: 268 - init_stock: 840 - price: 268 - service_level: 0.96 - vlt: 1 - SKU421: - cost: 214 - init_stock: 920 - price: 214 - service_level: 0.96 - vlt: 2 - SKU422: - cost: 52 - init_stock: 1080 - price: 52 - service_level: 0.96 - vlt: 3 - SKU423: - cost: 188 - init_stock: 1320 - price: 188 - service_level: 0.96 - vlt: 1 - SKU424: - cost: 189 - init_stock: 220 - price: 189 - service_level: 0.96 - vlt: 2 - SKU425: - cost: 162 - init_stock: 240 - price: 162 - service_level: 0.96 - vlt: 3 - SKU426: - cost: 125 - init_stock: 1960 - price: 125 - service_level: 0.96 - vlt: 3 - SKU427: - cost: 413 - init_stock: 1880 - price: 413 - service_level: 0.96 - vlt: 1 - SKU428: - cost: 391 - init_stock: 1260 - price: 391 - service_level: 0.96 - vlt: 3 - SKU429: - cost: 39 - init_stock: 820 - price: 39 - service_level: 0.96 - vlt: 1 - SKU43: - cost: 217 - init_stock: 1360 - price: 217 - service_level: 0.96 - vlt: 1 - SKU430: - cost: 216 - init_stock: 1460 - price: 216 - service_level: 0.96 - vlt: 2 - SKU431: - cost: 328 - init_stock: 420 - price: 328 - service_level: 0.96 - vlt: 1 - SKU432: - cost: 25 - init_stock: 920 - price: 25 - service_level: 0.96 - vlt: 3 - SKU433: - cost: 348 - init_stock: 900 - price: 348 - service_level: 0.96 - vlt: 2 - SKU434: - cost: 37 - init_stock: 600 - price: 37 - service_level: 0.96 - vlt: 1 - SKU435: - cost: 272 - init_stock: 600 - price: 272 - service_level: 0.96 - vlt: 3 - SKU436: - cost: 72 - init_stock: 1620 - price: 72 - service_level: 0.96 - vlt: 1 - SKU437: - cost: 115 - init_stock: 280 - price: 115 - service_level: 0.96 - vlt: 2 - SKU438: - cost: 143 - init_stock: 200 - price: 143 - service_level: 0.96 - vlt: 3 - SKU439: - cost: 256 - init_stock: 760 - price: 256 - service_level: 0.96 - vlt: 1 - SKU44: - cost: 360 - init_stock: 960 - price: 360 - service_level: 0.96 - vlt: 1 - SKU440: - cost: 248 - init_stock: 1640 - price: 248 - service_level: 0.96 - vlt: 2 - SKU441: - cost: 253 - init_stock: 1120 - price: 253 - service_level: 0.96 - vlt: 2 - SKU442: - cost: 470 - init_stock: 1760 - price: 470 - service_level: 0.96 - vlt: 2 - SKU443: - cost: 218 - init_stock: 1460 - price: 218 - service_level: 0.96 - vlt: 1 - SKU444: - cost: 156 - init_stock: 120 - price: 156 - service_level: 0.96 - vlt: 2 - SKU445: - cost: 260 - init_stock: 1720 - price: 260 - service_level: 0.96 - vlt: 2 - SKU446: - cost: 497 - init_stock: 980 - price: 497 - service_level: 0.96 - vlt: 1 - SKU447: - cost: 196 - init_stock: 100 - price: 196 - service_level: 0.96 - vlt: 1 - SKU448: - cost: 485 - init_stock: 1420 - price: 485 - service_level: 0.96 - vlt: 3 - SKU449: - cost: 282 - init_stock: 760 - price: 282 - service_level: 0.96 - vlt: 2 - SKU45: - cost: 253 - init_stock: 200 - price: 253 - service_level: 0.96 - vlt: 1 - SKU450: - cost: 58 - init_stock: 1960 - price: 58 - service_level: 0.96 - vlt: 2 - SKU451: - cost: 468 - init_stock: 1380 - price: 468 - service_level: 0.96 - vlt: 3 - SKU452: - cost: 63 - init_stock: 1580 - price: 63 - service_level: 0.96 - vlt: 1 - SKU453: - cost: 37 - init_stock: 700 - price: 37 - service_level: 0.96 - vlt: 3 - SKU454: - cost: 494 - init_stock: 1700 - price: 494 - service_level: 0.96 - vlt: 1 - SKU455: - cost: 136 - init_stock: 520 - price: 136 - service_level: 0.96 - vlt: 2 - SKU456: - cost: 338 - init_stock: 500 - price: 338 - service_level: 0.96 - vlt: 2 - SKU457: - cost: 169 - init_stock: 1280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU458: - cost: 403 - init_stock: 140 - price: 403 - service_level: 0.96 - vlt: 3 - SKU459: - cost: 425 - init_stock: 680 - price: 425 - service_level: 0.96 - vlt: 3 - SKU46: - cost: 299 - init_stock: 1000 - price: 299 - service_level: 0.96 - vlt: 3 - SKU460: - cost: 405 - init_stock: 1460 - price: 405 - service_level: 0.96 - vlt: 2 - SKU461: - cost: 251 - init_stock: 960 - price: 251 - service_level: 0.96 - vlt: 1 - SKU462: - cost: 448 - init_stock: 180 - price: 448 - service_level: 0.96 - vlt: 1 - SKU463: - cost: 187 - init_stock: 1640 - price: 187 - service_level: 0.96 - vlt: 3 - SKU464: - cost: 279 - init_stock: 680 - price: 279 - service_level: 0.96 - vlt: 3 - SKU465: - cost: 147 - init_stock: 2000 - price: 147 - service_level: 0.96 - vlt: 2 - SKU466: - cost: 97 - init_stock: 840 - price: 97 - service_level: 0.96 - vlt: 1 - SKU467: - cost: 142 - init_stock: 720 - price: 142 - service_level: 0.96 - vlt: 2 - SKU468: - cost: 55 - init_stock: 1000 - price: 55 - service_level: 0.96 - vlt: 2 - SKU469: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 3 - SKU47: - cost: 121 - init_stock: 780 - price: 121 - service_level: 0.96 - vlt: 1 - SKU470: - cost: 373 - init_stock: 640 - price: 373 - service_level: 0.96 - vlt: 2 - SKU471: - cost: 315 - init_stock: 1420 - price: 315 - service_level: 0.96 - vlt: 3 - SKU472: - cost: 421 - init_stock: 1180 - price: 421 - service_level: 0.96 - vlt: 2 - SKU473: - cost: 195 - init_stock: 420 - price: 195 - service_level: 0.96 - vlt: 1 - SKU474: - cost: 448 - init_stock: 1720 - price: 448 - service_level: 0.96 - vlt: 2 - SKU475: - cost: 34 - init_stock: 1880 - price: 34 - service_level: 0.96 - vlt: 1 - SKU476: - cost: 251 - init_stock: 1800 - price: 251 - service_level: 0.96 - vlt: 1 - SKU477: - cost: 289 - init_stock: 940 - price: 289 - service_level: 0.96 - vlt: 2 - SKU478: - cost: 479 - init_stock: 1760 - price: 479 - service_level: 0.96 - vlt: 2 - SKU479: - cost: 366 - init_stock: 760 - price: 366 - service_level: 0.96 - vlt: 1 - SKU48: - cost: 340 - init_stock: 1160 - price: 340 - service_level: 0.96 - vlt: 2 - SKU480: - cost: 18 - init_stock: 600 - price: 18 - service_level: 0.96 - vlt: 2 - SKU481: - cost: 330 - init_stock: 1760 - price: 330 - service_level: 0.96 - vlt: 1 - SKU482: - cost: 94 - init_stock: 1740 - price: 94 - service_level: 0.96 - vlt: 3 - SKU483: - cost: 298 - init_stock: 680 - price: 298 - service_level: 0.96 - vlt: 2 - SKU484: - cost: 84 - init_stock: 1460 - price: 84 - service_level: 0.96 - vlt: 3 - SKU485: - cost: 318 - init_stock: 1340 - price: 318 - service_level: 0.96 - vlt: 3 - SKU486: - cost: 122 - init_stock: 1040 - price: 122 - service_level: 0.96 - vlt: 1 - SKU487: - cost: 277 - init_stock: 1320 - price: 277 - service_level: 0.96 - vlt: 3 - SKU488: - cost: 36 - init_stock: 540 - price: 36 - service_level: 0.96 - vlt: 3 - SKU489: - cost: 59 - init_stock: 620 - price: 59 - service_level: 0.96 - vlt: 3 - SKU49: - cost: 263 - init_stock: 1900 - price: 263 - service_level: 0.96 - vlt: 3 - SKU490: - cost: 161 - init_stock: 1620 - price: 161 - service_level: 0.96 - vlt: 2 - SKU491: - cost: 444 - init_stock: 1500 - price: 444 - service_level: 0.96 - vlt: 2 - SKU492: - cost: 53 - init_stock: 1600 - price: 53 - service_level: 0.96 - vlt: 2 - SKU493: - cost: 461 - init_stock: 540 - price: 461 - service_level: 0.96 - vlt: 1 - SKU494: - cost: 145 - init_stock: 1880 - price: 145 - service_level: 0.96 - vlt: 1 - SKU495: - cost: 149 - init_stock: 1240 - price: 149 - service_level: 0.96 - vlt: 3 - SKU496: - cost: 342 - init_stock: 1180 - price: 342 - service_level: 0.96 - vlt: 1 - SKU497: - cost: 33 - init_stock: 180 - price: 33 - service_level: 0.96 - vlt: 3 - SKU498: - cost: 305 - init_stock: 1300 - price: 305 - service_level: 0.96 - vlt: 3 - SKU499: - cost: 307 - init_stock: 580 - price: 307 - service_level: 0.96 - vlt: 2 - SKU5: - cost: 466 - init_stock: 1420 - price: 466 - service_level: 0.96 - vlt: 2 - SKU50: - cost: 282 - init_stock: 740 - price: 282 - service_level: 0.96 - vlt: 3 - SKU500: - cost: 208 - init_stock: 840 - price: 208 - service_level: 0.96 - vlt: 3 - SKU501: - cost: 194 - init_stock: 1520 - price: 194 - service_level: 0.96 - vlt: 1 - SKU502: - cost: 69 - init_stock: 1300 - price: 69 - service_level: 0.96 - vlt: 2 - SKU503: - cost: 208 - init_stock: 1140 - price: 208 - service_level: 0.96 - vlt: 1 - SKU504: - cost: 251 - init_stock: 600 - price: 251 - service_level: 0.96 - vlt: 2 - SKU505: - cost: 426 - init_stock: 1180 - price: 426 - service_level: 0.96 - vlt: 1 - SKU506: - cost: 149 - init_stock: 1860 - price: 149 - service_level: 0.96 - vlt: 3 - SKU507: - cost: 187 - init_stock: 300 - price: 187 - service_level: 0.96 - vlt: 1 - SKU508: - cost: 272 - init_stock: 1920 - price: 272 - service_level: 0.96 - vlt: 1 - SKU509: - cost: 297 - init_stock: 1620 - price: 297 - service_level: 0.96 - vlt: 1 - SKU51: - cost: 295 - init_stock: 1340 - price: 295 - service_level: 0.96 - vlt: 3 - SKU510: - cost: 94 - init_stock: 180 - price: 94 - service_level: 0.96 - vlt: 1 - SKU511: - cost: 361 - init_stock: 1500 - price: 361 - service_level: 0.96 - vlt: 1 - SKU512: - cost: 467 - init_stock: 440 - price: 467 - service_level: 0.96 - vlt: 2 - SKU513: - cost: 343 - init_stock: 140 - price: 343 - service_level: 0.96 - vlt: 3 - SKU514: - cost: 186 - init_stock: 1260 - price: 186 - service_level: 0.96 - vlt: 3 - SKU515: - cost: 145 - init_stock: 1080 - price: 145 - service_level: 0.96 - vlt: 3 - SKU516: - cost: 64 - init_stock: 760 - price: 64 - service_level: 0.96 - vlt: 1 - SKU517: - cost: 117 - init_stock: 180 - price: 117 - service_level: 0.96 - vlt: 3 - SKU518: - cost: 26 - init_stock: 420 - price: 26 - service_level: 0.96 - vlt: 3 - SKU519: - cost: 233 - init_stock: 1000 - price: 233 - service_level: 0.96 - vlt: 2 - SKU52: - cost: 22 - init_stock: 1340 - price: 22 - service_level: 0.96 - vlt: 1 - SKU520: - cost: 209 - init_stock: 440 - price: 209 - service_level: 0.96 - vlt: 1 - SKU521: - cost: 220 - init_stock: 1000 - price: 220 - service_level: 0.96 - vlt: 3 - SKU522: - cost: 156 - init_stock: 1740 - price: 156 - service_level: 0.96 - vlt: 1 - SKU523: - cost: 65 - init_stock: 2000 - price: 65 - service_level: 0.96 - vlt: 3 - SKU524: - cost: 294 - init_stock: 1880 - price: 294 - service_level: 0.96 - vlt: 1 - SKU525: - cost: 432 - init_stock: 840 - price: 432 - service_level: 0.96 - vlt: 2 - SKU526: - cost: 419 - init_stock: 480 - price: 419 - service_level: 0.96 - vlt: 3 - SKU527: - cost: 131 - init_stock: 1400 - price: 131 - service_level: 0.96 - vlt: 1 - SKU528: - cost: 316 - init_stock: 420 - price: 316 - service_level: 0.96 - vlt: 3 - SKU529: - cost: 298 - init_stock: 620 - price: 298 - service_level: 0.96 - vlt: 3 - SKU53: - cost: 151 - init_stock: 1400 - price: 151 - service_level: 0.96 - vlt: 2 - SKU530: - cost: 131 - init_stock: 900 - price: 131 - service_level: 0.96 - vlt: 1 - SKU531: - cost: 103 - init_stock: 1200 - price: 103 - service_level: 0.96 - vlt: 3 - SKU532: - cost: 351 - init_stock: 1540 - price: 351 - service_level: 0.96 - vlt: 3 - SKU533: - cost: 296 - init_stock: 840 - price: 296 - service_level: 0.96 - vlt: 3 - SKU534: - cost: 425 - init_stock: 820 - price: 425 - service_level: 0.96 - vlt: 1 - SKU535: - cost: 304 - init_stock: 1720 - price: 304 - service_level: 0.96 - vlt: 2 - SKU536: - cost: 358 - init_stock: 1040 - price: 358 - service_level: 0.96 - vlt: 2 - SKU537: - cost: 321 - init_stock: 180 - price: 321 - service_level: 0.96 - vlt: 3 - SKU538: - cost: 457 - init_stock: 1000 - price: 457 - service_level: 0.96 - vlt: 1 - SKU539: - cost: 165 - init_stock: 1560 - price: 165 - service_level: 0.96 - vlt: 2 - SKU54: - cost: 158 - init_stock: 920 - price: 158 - service_level: 0.96 - vlt: 1 - SKU540: - cost: 388 - init_stock: 840 - price: 388 - service_level: 0.96 - vlt: 3 - SKU541: - cost: 131 - init_stock: 580 - price: 131 - service_level: 0.96 - vlt: 2 - SKU542: - cost: 38 - init_stock: 520 - price: 38 - service_level: 0.96 - vlt: 3 - SKU543: - cost: 430 - init_stock: 1700 - price: 430 - service_level: 0.96 - vlt: 2 - SKU544: - cost: 346 - init_stock: 1940 - price: 346 - service_level: 0.96 - vlt: 1 - SKU545: - cost: 175 - init_stock: 300 - price: 175 - service_level: 0.96 - vlt: 3 - SKU546: - cost: 491 - init_stock: 1920 - price: 491 - service_level: 0.96 - vlt: 3 - SKU547: - cost: 161 - init_stock: 880 - price: 161 - service_level: 0.96 - vlt: 2 - SKU548: - cost: 111 - init_stock: 120 - price: 111 - service_level: 0.96 - vlt: 1 - SKU549: - cost: 387 - init_stock: 1580 - price: 387 - service_level: 0.96 - vlt: 1 - SKU55: - cost: 482 - init_stock: 1300 - price: 482 - service_level: 0.96 - vlt: 3 - SKU550: - cost: 259 - init_stock: 1880 - price: 259 - service_level: 0.96 - vlt: 1 - SKU551: - cost: 46 - init_stock: 620 - price: 46 - service_level: 0.96 - vlt: 2 - SKU552: - cost: 191 - init_stock: 1800 - price: 191 - service_level: 0.96 - vlt: 3 - SKU553: - cost: 208 - init_stock: 600 - price: 208 - service_level: 0.96 - vlt: 1 - SKU554: - cost: 340 - init_stock: 820 - price: 340 - service_level: 0.96 - vlt: 1 - SKU555: - cost: 489 - init_stock: 1680 - price: 489 - service_level: 0.96 - vlt: 1 - SKU556: - cost: 110 - init_stock: 600 - price: 110 - service_level: 0.96 - vlt: 2 - SKU557: - cost: 334 - init_stock: 1460 - price: 334 - service_level: 0.96 - vlt: 2 - SKU558: - cost: 399 - init_stock: 340 - price: 399 - service_level: 0.96 - vlt: 1 - SKU559: - cost: 313 - init_stock: 1080 - price: 313 - service_level: 0.96 - vlt: 1 - SKU56: - cost: 377 - init_stock: 1480 - price: 377 - service_level: 0.96 - vlt: 1 - SKU560: - cost: 283 - init_stock: 820 - price: 283 - service_level: 0.96 - vlt: 1 - SKU561: - cost: 202 - init_stock: 780 - price: 202 - service_level: 0.96 - vlt: 3 - SKU562: - cost: 347 - init_stock: 1780 - price: 347 - service_level: 0.96 - vlt: 2 - SKU563: - cost: 401 - init_stock: 100 - price: 401 - service_level: 0.96 - vlt: 3 - SKU564: - cost: 109 - init_stock: 180 - price: 109 - service_level: 0.96 - vlt: 2 - SKU565: - cost: 57 - init_stock: 1500 - price: 57 - service_level: 0.96 - vlt: 3 - SKU566: - cost: 131 - init_stock: 220 - price: 131 - service_level: 0.96 - vlt: 2 - SKU567: - cost: 361 - init_stock: 180 - price: 361 - service_level: 0.96 - vlt: 1 - SKU568: - cost: 75 - init_stock: 1560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU569: - cost: 473 - init_stock: 960 - price: 473 - service_level: 0.96 - vlt: 2 - SKU57: - cost: 359 - init_stock: 1000 - price: 359 - service_level: 0.96 - vlt: 2 - SKU570: - cost: 388 - init_stock: 1660 - price: 388 - service_level: 0.96 - vlt: 2 - SKU571: - cost: 168 - init_stock: 780 - price: 168 - service_level: 0.96 - vlt: 1 - SKU572: - cost: 26 - init_stock: 900 - price: 26 - service_level: 0.96 - vlt: 3 - SKU573: - cost: 246 - init_stock: 820 - price: 246 - service_level: 0.96 - vlt: 2 - SKU574: - cost: 94 - init_stock: 1000 - price: 94 - service_level: 0.96 - vlt: 2 - SKU575: - cost: 237 - init_stock: 1580 - price: 237 - service_level: 0.96 - vlt: 1 - SKU576: - cost: 265 - init_stock: 1560 - price: 265 - service_level: 0.96 - vlt: 3 - SKU577: - cost: 18 - init_stock: 1740 - price: 18 - service_level: 0.96 - vlt: 3 - SKU578: - cost: 100 - init_stock: 1420 - price: 100 - service_level: 0.96 - vlt: 2 - SKU579: - cost: 415 - init_stock: 180 - price: 415 - service_level: 0.96 - vlt: 1 - SKU58: - cost: 362 - init_stock: 960 - price: 362 - service_level: 0.96 - vlt: 2 - SKU580: - cost: 203 - init_stock: 100 - price: 203 - service_level: 0.96 - vlt: 1 - SKU581: - cost: 152 - init_stock: 1720 - price: 152 - service_level: 0.96 - vlt: 2 - SKU582: - cost: 359 - init_stock: 1920 - price: 359 - service_level: 0.96 - vlt: 2 - SKU583: - cost: 86 - init_stock: 1740 - price: 86 - service_level: 0.96 - vlt: 1 - SKU584: - cost: 33 - init_stock: 900 - price: 33 - service_level: 0.96 - vlt: 2 - SKU585: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 2 - SKU586: - cost: 61 - init_stock: 880 - price: 61 - service_level: 0.96 - vlt: 2 - SKU587: - cost: 290 - init_stock: 1560 - price: 290 - service_level: 0.96 - vlt: 2 - SKU588: - cost: 476 - init_stock: 880 - price: 476 - service_level: 0.96 - vlt: 2 - SKU589: - cost: 244 - init_stock: 680 - price: 244 - service_level: 0.96 - vlt: 3 - SKU59: - cost: 145 - init_stock: 1020 - price: 145 - service_level: 0.96 - vlt: 1 - SKU590: - cost: 70 - init_stock: 620 - price: 70 - service_level: 0.96 - vlt: 2 - SKU591: - cost: 206 - init_stock: 1680 - price: 206 - service_level: 0.96 - vlt: 2 - SKU592: - cost: 218 - init_stock: 920 - price: 218 - service_level: 0.96 - vlt: 1 - SKU593: - cost: 124 - init_stock: 880 - price: 124 - service_level: 0.96 - vlt: 1 - SKU594: - cost: 238 - init_stock: 1000 - price: 238 - service_level: 0.96 - vlt: 1 - SKU595: - cost: 73 - init_stock: 860 - price: 73 - service_level: 0.96 - vlt: 3 - SKU596: - cost: 432 - init_stock: 140 - price: 432 - service_level: 0.96 - vlt: 2 - SKU597: - cost: 252 - init_stock: 1740 - price: 252 - service_level: 0.96 - vlt: 2 - SKU598: - cost: 348 - init_stock: 280 - price: 348 - service_level: 0.96 - vlt: 1 - SKU599: - cost: 54 - init_stock: 1360 - price: 54 - service_level: 0.96 - vlt: 2 - SKU6: - cost: 122 - init_stock: 1760 - price: 122 - service_level: 0.96 - vlt: 2 - SKU60: - cost: 200 - init_stock: 780 - price: 200 - service_level: 0.96 - vlt: 1 - SKU600: - cost: 386 - init_stock: 1300 - price: 386 - service_level: 0.96 - vlt: 1 - SKU601: - cost: 138 - init_stock: 780 - price: 138 - service_level: 0.96 - vlt: 3 - SKU602: - cost: 184 - init_stock: 1960 - price: 184 - service_level: 0.96 - vlt: 1 - SKU603: - cost: 278 - init_stock: 840 - price: 278 - service_level: 0.96 - vlt: 3 - SKU604: - cost: 270 - init_stock: 1000 - price: 270 - service_level: 0.96 - vlt: 3 - SKU605: - cost: 288 - init_stock: 480 - price: 288 - service_level: 0.96 - vlt: 2 - SKU606: - cost: 114 - init_stock: 220 - price: 114 - service_level: 0.96 - vlt: 3 - SKU607: - cost: 208 - init_stock: 1580 - price: 208 - service_level: 0.96 - vlt: 2 - SKU608: - cost: 39 - init_stock: 1460 - price: 39 - service_level: 0.96 - vlt: 1 - SKU609: - cost: 102 - init_stock: 380 - price: 102 - service_level: 0.96 - vlt: 2 - SKU61: - cost: 461 - init_stock: 1500 - price: 461 - service_level: 0.96 - vlt: 1 - SKU610: - cost: 449 - init_stock: 1360 - price: 449 - service_level: 0.96 - vlt: 1 - SKU611: - cost: 306 - init_stock: 1540 - price: 306 - service_level: 0.96 - vlt: 3 - SKU612: - cost: 391 - init_stock: 1760 - price: 391 - service_level: 0.96 - vlt: 2 - SKU613: - cost: 174 - init_stock: 140 - price: 174 - service_level: 0.96 - vlt: 3 - SKU614: - cost: 173 - init_stock: 300 - price: 173 - service_level: 0.96 - vlt: 1 - SKU615: - cost: 330 - init_stock: 1820 - price: 330 - service_level: 0.96 - vlt: 2 - SKU616: - cost: 232 - init_stock: 1460 - price: 232 - service_level: 0.96 - vlt: 1 - SKU617: - cost: 203 - init_stock: 1200 - price: 203 - service_level: 0.96 - vlt: 3 - SKU618: - cost: 77 - init_stock: 460 - price: 77 - service_level: 0.96 - vlt: 1 - SKU619: - cost: 189 - init_stock: 1720 - price: 189 - service_level: 0.96 - vlt: 2 - SKU62: - cost: 124 - init_stock: 180 - price: 124 - service_level: 0.96 - vlt: 2 - SKU620: - cost: 231 - init_stock: 780 - price: 231 - service_level: 0.96 - vlt: 3 - SKU621: - cost: 199 - init_stock: 1440 - price: 199 - service_level: 0.96 - vlt: 2 - SKU622: - cost: 253 - init_stock: 1300 - price: 253 - service_level: 0.96 - vlt: 3 - SKU623: - cost: 335 - init_stock: 1100 - price: 335 - service_level: 0.96 - vlt: 1 - SKU624: - cost: 482 - init_stock: 1080 - price: 482 - service_level: 0.96 - vlt: 2 - SKU625: - cost: 46 - init_stock: 1780 - price: 46 - service_level: 0.96 - vlt: 1 - SKU626: - cost: 451 - init_stock: 1780 - price: 451 - service_level: 0.96 - vlt: 3 - SKU627: - cost: 220 - init_stock: 1640 - price: 220 - service_level: 0.96 - vlt: 3 - SKU628: - cost: 124 - init_stock: 520 - price: 124 - service_level: 0.96 - vlt: 2 - SKU629: - cost: 353 - init_stock: 1840 - price: 353 - service_level: 0.96 - vlt: 1 - SKU63: - cost: 68 - init_stock: 280 - price: 68 - service_level: 0.96 - vlt: 3 - SKU630: - cost: 122 - init_stock: 340 - price: 122 - service_level: 0.96 - vlt: 1 - SKU631: - cost: 296 - init_stock: 980 - price: 296 - service_level: 0.96 - vlt: 2 - SKU632: - cost: 85 - init_stock: 1880 - price: 85 - service_level: 0.96 - vlt: 2 - SKU633: - cost: 184 - init_stock: 1340 - price: 184 - service_level: 0.96 - vlt: 2 - SKU634: - cost: 17 - init_stock: 1460 - price: 17 - service_level: 0.96 - vlt: 1 - SKU635: - cost: 341 - init_stock: 1860 - price: 341 - service_level: 0.96 - vlt: 1 - SKU636: - cost: 385 - init_stock: 740 - price: 385 - service_level: 0.96 - vlt: 1 - SKU637: - cost: 83 - init_stock: 1220 - price: 83 - service_level: 0.96 - vlt: 3 - SKU638: - cost: 375 - init_stock: 160 - price: 375 - service_level: 0.96 - vlt: 1 - SKU639: - cost: 327 - init_stock: 800 - price: 327 - service_level: 0.96 - vlt: 2 - SKU64: - cost: 36 - init_stock: 380 - price: 36 - service_level: 0.96 - vlt: 3 - SKU640: - cost: 275 - init_stock: 1820 - price: 275 - service_level: 0.96 - vlt: 1 - SKU641: - cost: 365 - init_stock: 1620 - price: 365 - service_level: 0.96 - vlt: 1 - SKU642: - cost: 414 - init_stock: 500 - price: 414 - service_level: 0.96 - vlt: 2 - SKU643: - cost: 358 - init_stock: 920 - price: 358 - service_level: 0.96 - vlt: 2 - SKU644: - cost: 46 - init_stock: 1640 - price: 46 - service_level: 0.96 - vlt: 2 - SKU645: - cost: 467 - init_stock: 1980 - price: 467 - service_level: 0.96 - vlt: 3 - SKU646: - cost: 189 - init_stock: 260 - price: 189 - service_level: 0.96 - vlt: 3 - SKU647: - cost: 303 - init_stock: 1540 - price: 303 - service_level: 0.96 - vlt: 3 - SKU648: - cost: 226 - init_stock: 1100 - price: 226 - service_level: 0.96 - vlt: 1 - SKU649: - cost: 198 - init_stock: 500 - price: 198 - service_level: 0.96 - vlt: 3 - SKU65: - cost: 15 - init_stock: 1880 - price: 15 - service_level: 0.96 - vlt: 2 - SKU650: - cost: 351 - init_stock: 1120 - price: 351 - service_level: 0.96 - vlt: 2 - SKU651: - cost: 380 - init_stock: 1920 - price: 380 - service_level: 0.96 - vlt: 3 - SKU652: - cost: 123 - init_stock: 800 - price: 123 - service_level: 0.96 - vlt: 2 - SKU653: - cost: 479 - init_stock: 1920 - price: 479 - service_level: 0.96 - vlt: 2 - SKU654: - cost: 66 - init_stock: 160 - price: 66 - service_level: 0.96 - vlt: 3 - SKU655: - cost: 333 - init_stock: 840 - price: 333 - service_level: 0.96 - vlt: 1 - SKU656: - cost: 53 - init_stock: 1440 - price: 53 - service_level: 0.96 - vlt: 2 - SKU657: - cost: 73 - init_stock: 1620 - price: 73 - service_level: 0.96 - vlt: 2 - SKU658: - cost: 70 - init_stock: 720 - price: 70 - service_level: 0.96 - vlt: 3 - SKU659: - cost: 21 - init_stock: 1120 - price: 21 - service_level: 0.96 - vlt: 1 - SKU66: - cost: 143 - init_stock: 1800 - price: 143 - service_level: 0.96 - vlt: 1 - SKU660: - cost: 487 - init_stock: 1660 - price: 487 - service_level: 0.96 - vlt: 1 - SKU661: - cost: 344 - init_stock: 1480 - price: 344 - service_level: 0.96 - vlt: 2 - SKU662: - cost: 372 - init_stock: 760 - price: 372 - service_level: 0.96 - vlt: 3 - SKU663: - cost: 418 - init_stock: 800 - price: 418 - service_level: 0.96 - vlt: 3 - SKU664: - cost: 351 - init_stock: 1680 - price: 351 - service_level: 0.96 - vlt: 1 - SKU665: - cost: 493 - init_stock: 860 - price: 493 - service_level: 0.96 - vlt: 3 - SKU666: - cost: 341 - init_stock: 1760 - price: 341 - service_level: 0.96 - vlt: 1 - SKU667: - cost: 325 - init_stock: 260 - price: 325 - service_level: 0.96 - vlt: 3 - SKU668: - cost: 286 - init_stock: 1200 - price: 286 - service_level: 0.96 - vlt: 3 - SKU669: - cost: 74 - init_stock: 320 - price: 74 - service_level: 0.96 - vlt: 1 - SKU67: - cost: 247 - init_stock: 1060 - price: 247 - service_level: 0.96 - vlt: 1 - SKU670: - cost: 289 - init_stock: 1720 - price: 289 - service_level: 0.96 - vlt: 1 - SKU671: - cost: 267 - init_stock: 1660 - price: 267 - service_level: 0.96 - vlt: 3 - SKU672: - cost: 369 - init_stock: 260 - price: 369 - service_level: 0.96 - vlt: 2 - SKU673: - cost: 322 - init_stock: 820 - price: 322 - service_level: 0.96 - vlt: 2 - SKU674: - cost: 223 - init_stock: 440 - price: 223 - service_level: 0.96 - vlt: 1 - SKU675: - cost: 438 - init_stock: 660 - price: 438 - service_level: 0.96 - vlt: 1 - SKU676: - cost: 244 - init_stock: 1220 - price: 244 - service_level: 0.96 - vlt: 1 - SKU677: - cost: 425 - init_stock: 880 - price: 425 - service_level: 0.96 - vlt: 2 - SKU678: - cost: 168 - init_stock: 720 - price: 168 - service_level: 0.96 - vlt: 1 - SKU679: - cost: 395 - init_stock: 880 - price: 395 - service_level: 0.96 - vlt: 2 - SKU68: - cost: 169 - init_stock: 280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU680: - cost: 260 - init_stock: 600 - price: 260 - service_level: 0.96 - vlt: 1 - SKU681: - cost: 464 - init_stock: 1940 - price: 464 - service_level: 0.96 - vlt: 3 - SKU682: - cost: 370 - init_stock: 1060 - price: 370 - service_level: 0.96 - vlt: 2 - SKU683: - cost: 327 - init_stock: 1340 - price: 327 - service_level: 0.96 - vlt: 3 - SKU684: - cost: 355 - init_stock: 1580 - price: 355 - service_level: 0.96 - vlt: 1 - SKU685: - cost: 422 - init_stock: 900 - price: 422 - service_level: 0.96 - vlt: 2 - SKU686: - cost: 63 - init_stock: 1260 - price: 63 - service_level: 0.96 - vlt: 1 - SKU687: - cost: 34 - init_stock: 1520 - price: 34 - service_level: 0.96 - vlt: 2 - SKU688: - cost: 484 - init_stock: 1540 - price: 484 - service_level: 0.96 - vlt: 3 - SKU689: - cost: 499 - init_stock: 300 - price: 499 - service_level: 0.96 - vlt: 1 - SKU69: - cost: 176 - init_stock: 1340 - price: 176 - service_level: 0.96 - vlt: 1 - SKU690: - cost: 437 - init_stock: 1660 - price: 437 - service_level: 0.96 - vlt: 2 - SKU691: - cost: 17 - init_stock: 1580 - price: 17 - service_level: 0.96 - vlt: 3 - SKU692: - cost: 225 - init_stock: 1300 - price: 225 - service_level: 0.96 - vlt: 1 - SKU693: - cost: 19 - init_stock: 1220 - price: 19 - service_level: 0.96 - vlt: 2 - SKU694: - cost: 208 - init_stock: 1500 - price: 208 - service_level: 0.96 - vlt: 3 - SKU695: - cost: 190 - init_stock: 140 - price: 190 - service_level: 0.96 - vlt: 2 - SKU696: - cost: 348 - init_stock: 1160 - price: 348 - service_level: 0.96 - vlt: 1 - SKU697: - cost: 455 - init_stock: 1600 - price: 455 - service_level: 0.96 - vlt: 1 - SKU698: - cost: 198 - init_stock: 1220 - price: 198 - service_level: 0.96 - vlt: 3 - SKU699: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 3 - SKU7: - cost: 101 - init_stock: 1920 - price: 101 - service_level: 0.96 - vlt: 2 - SKU70: - cost: 186 - init_stock: 700 - price: 186 - service_level: 0.96 - vlt: 1 - SKU700: - cost: 74 - init_stock: 180 - price: 74 - service_level: 0.96 - vlt: 2 - SKU701: - cost: 399 - init_stock: 1540 - price: 399 - service_level: 0.96 - vlt: 1 - SKU702: - cost: 82 - init_stock: 680 - price: 82 - service_level: 0.96 - vlt: 1 - SKU703: - cost: 363 - init_stock: 760 - price: 363 - service_level: 0.96 - vlt: 1 - SKU704: - cost: 384 - init_stock: 1740 - price: 384 - service_level: 0.96 - vlt: 2 - SKU705: - cost: 128 - init_stock: 1260 - price: 128 - service_level: 0.96 - vlt: 2 - SKU706: - cost: 182 - init_stock: 1820 - price: 182 - service_level: 0.96 - vlt: 2 - SKU707: - cost: 18 - init_stock: 1380 - price: 18 - service_level: 0.96 - vlt: 1 - SKU708: - cost: 178 - init_stock: 560 - price: 178 - service_level: 0.96 - vlt: 3 - SKU709: - cost: 102 - init_stock: 1060 - price: 102 - service_level: 0.96 - vlt: 3 - SKU71: - cost: 315 - init_stock: 900 - price: 315 - service_level: 0.96 - vlt: 2 - SKU710: - cost: 252 - init_stock: 380 - price: 252 - service_level: 0.96 - vlt: 2 - SKU711: - cost: 281 - init_stock: 1380 - price: 281 - service_level: 0.96 - vlt: 1 - SKU712: - cost: 232 - init_stock: 220 - price: 232 - service_level: 0.96 - vlt: 2 - SKU713: - cost: 285 - init_stock: 860 - price: 285 - service_level: 0.96 - vlt: 2 - SKU714: - cost: 79 - init_stock: 820 - price: 79 - service_level: 0.96 - vlt: 3 - SKU715: - cost: 234 - init_stock: 340 - price: 234 - service_level: 0.96 - vlt: 1 - SKU716: - cost: 70 - init_stock: 1500 - price: 70 - service_level: 0.96 - vlt: 2 - SKU717: - cost: 345 - init_stock: 160 - price: 345 - service_level: 0.96 - vlt: 2 - SKU718: - cost: 357 - init_stock: 1160 - price: 357 - service_level: 0.96 - vlt: 2 - SKU719: - cost: 340 - init_stock: 1300 - price: 340 - service_level: 0.96 - vlt: 3 - SKU72: - cost: 458 - init_stock: 1700 - price: 458 - service_level: 0.96 - vlt: 2 - SKU720: - cost: 325 - init_stock: 840 - price: 325 - service_level: 0.96 - vlt: 3 - SKU721: - cost: 73 - init_stock: 220 - price: 73 - service_level: 0.96 - vlt: 2 - SKU722: - cost: 392 - init_stock: 1940 - price: 392 - service_level: 0.96 - vlt: 1 - SKU723: - cost: 318 - init_stock: 1780 - price: 318 - service_level: 0.96 - vlt: 3 - SKU724: - cost: 400 - init_stock: 660 - price: 400 - service_level: 0.96 - vlt: 1 - SKU725: - cost: 175 - init_stock: 740 - price: 175 - service_level: 0.96 - vlt: 1 - SKU726: - cost: 458 - init_stock: 1780 - price: 458 - service_level: 0.96 - vlt: 3 - SKU727: - cost: 418 - init_stock: 1140 - price: 418 - service_level: 0.96 - vlt: 1 - SKU728: - cost: 475 - init_stock: 740 - price: 475 - service_level: 0.96 - vlt: 3 - SKU729: - cost: 324 - init_stock: 720 - price: 324 - service_level: 0.96 - vlt: 3 - SKU73: - cost: 212 - init_stock: 1080 - price: 212 - service_level: 0.96 - vlt: 2 - SKU730: - cost: 16 - init_stock: 260 - price: 16 - service_level: 0.96 - vlt: 2 - SKU731: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU732: - cost: 41 - init_stock: 1240 - price: 41 - service_level: 0.96 - vlt: 3 - SKU733: - cost: 315 - init_stock: 520 - price: 315 - service_level: 0.96 - vlt: 3 - SKU734: - cost: 37 - init_stock: 1020 - price: 37 - service_level: 0.96 - vlt: 3 - SKU735: - cost: 266 - init_stock: 1980 - price: 266 - service_level: 0.96 - vlt: 3 - SKU736: - cost: 368 - init_stock: 500 - price: 368 - service_level: 0.96 - vlt: 2 - SKU737: - cost: 475 - init_stock: 620 - price: 475 - service_level: 0.96 - vlt: 2 - SKU738: - cost: 185 - init_stock: 960 - price: 185 - service_level: 0.96 - vlt: 3 - SKU739: - cost: 475 - init_stock: 1480 - price: 475 - service_level: 0.96 - vlt: 2 - SKU74: - cost: 159 - init_stock: 840 - price: 159 - service_level: 0.96 - vlt: 1 - SKU740: - cost: 390 - init_stock: 1280 - price: 390 - service_level: 0.96 - vlt: 1 - SKU741: - cost: 91 - init_stock: 1120 - price: 91 - service_level: 0.96 - vlt: 1 - SKU742: - cost: 188 - init_stock: 440 - price: 188 - service_level: 0.96 - vlt: 1 - SKU743: - cost: 217 - init_stock: 860 - price: 217 - service_level: 0.96 - vlt: 3 - SKU744: - cost: 379 - init_stock: 1160 - price: 379 - service_level: 0.96 - vlt: 1 - SKU745: - cost: 316 - init_stock: 1840 - price: 316 - service_level: 0.96 - vlt: 2 - SKU746: - cost: 437 - init_stock: 800 - price: 437 - service_level: 0.96 - vlt: 2 - SKU747: - cost: 373 - init_stock: 1580 - price: 373 - service_level: 0.96 - vlt: 2 - SKU748: - cost: 275 - init_stock: 1320 - price: 275 - service_level: 0.96 - vlt: 2 - SKU749: - cost: 394 - init_stock: 1060 - price: 394 - service_level: 0.96 - vlt: 1 - SKU75: - cost: 131 - init_stock: 380 - price: 131 - service_level: 0.96 - vlt: 1 - SKU750: - cost: 256 - init_stock: 1240 - price: 256 - service_level: 0.96 - vlt: 3 - SKU751: - cost: 369 - init_stock: 1300 - price: 369 - service_level: 0.96 - vlt: 3 - SKU752: - cost: 259 - init_stock: 1560 - price: 259 - service_level: 0.96 - vlt: 3 - SKU753: - cost: 77 - init_stock: 1300 - price: 77 - service_level: 0.96 - vlt: 3 - SKU754: - cost: 387 - init_stock: 260 - price: 387 - service_level: 0.96 - vlt: 2 - SKU755: - cost: 354 - init_stock: 1600 - price: 354 - service_level: 0.96 - vlt: 3 - SKU756: - cost: 246 - init_stock: 1800 - price: 246 - service_level: 0.96 - vlt: 1 - SKU757: - cost: 40 - init_stock: 1140 - price: 40 - service_level: 0.96 - vlt: 3 - SKU758: - cost: 241 - init_stock: 800 - price: 241 - service_level: 0.96 - vlt: 2 - SKU759: - cost: 435 - init_stock: 760 - price: 435 - service_level: 0.96 - vlt: 1 - SKU76: - cost: 147 - init_stock: 1280 - price: 147 - service_level: 0.96 - vlt: 2 - SKU760: - cost: 176 - init_stock: 1020 - price: 176 - service_level: 0.96 - vlt: 3 - SKU761: - cost: 224 - init_stock: 1100 - price: 224 - service_level: 0.96 - vlt: 1 - SKU762: - cost: 264 - init_stock: 1020 - price: 264 - service_level: 0.96 - vlt: 1 - SKU763: - cost: 385 - init_stock: 1580 - price: 385 - service_level: 0.96 - vlt: 2 - SKU764: - cost: 349 - init_stock: 680 - price: 349 - service_level: 0.96 - vlt: 1 - SKU765: - cost: 345 - init_stock: 260 - price: 345 - service_level: 0.96 - vlt: 1 - SKU766: - cost: 478 - init_stock: 400 - price: 478 - service_level: 0.96 - vlt: 2 - SKU767: - cost: 95 - init_stock: 1520 - price: 95 - service_level: 0.96 - vlt: 1 - SKU768: - cost: 181 - init_stock: 1840 - price: 181 - service_level: 0.96 - vlt: 2 - SKU769: - cost: 24 - init_stock: 580 - price: 24 - service_level: 0.96 - vlt: 2 - SKU77: - cost: 409 - init_stock: 1440 - price: 409 - service_level: 0.96 - vlt: 1 - SKU770: - cost: 150 - init_stock: 1240 - price: 150 - service_level: 0.96 - vlt: 2 - SKU771: - cost: 101 - init_stock: 140 - price: 101 - service_level: 0.96 - vlt: 1 - SKU772: - cost: 256 - init_stock: 120 - price: 256 - service_level: 0.96 - vlt: 3 - SKU773: - cost: 84 - init_stock: 1840 - price: 84 - service_level: 0.96 - vlt: 1 - SKU774: - cost: 447 - init_stock: 1180 - price: 447 - service_level: 0.96 - vlt: 1 - SKU775: - cost: 175 - init_stock: 1720 - price: 175 - service_level: 0.96 - vlt: 3 - SKU776: - cost: 103 - init_stock: 1700 - price: 103 - service_level: 0.96 - vlt: 2 - SKU777: - cost: 292 - init_stock: 1140 - price: 292 - service_level: 0.96 - vlt: 2 - SKU778: - cost: 203 - init_stock: 160 - price: 203 - service_level: 0.96 - vlt: 3 - SKU779: - cost: 117 - init_stock: 860 - price: 117 - service_level: 0.96 - vlt: 1 - SKU78: - cost: 234 - init_stock: 260 - price: 234 - service_level: 0.96 - vlt: 3 - SKU780: - cost: 69 - init_stock: 1640 - price: 69 - service_level: 0.96 - vlt: 3 - SKU781: - cost: 372 - init_stock: 720 - price: 372 - service_level: 0.96 - vlt: 3 - SKU782: - cost: 27 - init_stock: 200 - price: 27 - service_level: 0.96 - vlt: 3 - SKU783: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 2 - SKU784: - cost: 309 - init_stock: 1040 - price: 309 - service_level: 0.96 - vlt: 3 - SKU785: - cost: 191 - init_stock: 1400 - price: 191 - service_level: 0.96 - vlt: 2 - SKU786: - cost: 91 - init_stock: 440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU787: - cost: 360 - init_stock: 1220 - price: 360 - service_level: 0.96 - vlt: 1 - SKU788: - cost: 351 - init_stock: 560 - price: 351 - service_level: 0.96 - vlt: 1 - SKU789: - cost: 153 - init_stock: 1160 - price: 153 - service_level: 0.96 - vlt: 2 - SKU79: - cost: 245 - init_stock: 220 - price: 245 - service_level: 0.96 - vlt: 1 - SKU790: - cost: 417 - init_stock: 1320 - price: 417 - service_level: 0.96 - vlt: 2 - SKU791: - cost: 134 - init_stock: 560 - price: 134 - service_level: 0.96 - vlt: 1 - SKU792: - cost: 313 - init_stock: 420 - price: 313 - service_level: 0.96 - vlt: 2 - SKU793: - cost: 195 - init_stock: 1520 - price: 195 - service_level: 0.96 - vlt: 2 - SKU794: - cost: 325 - init_stock: 1600 - price: 325 - service_level: 0.96 - vlt: 3 - SKU795: - cost: 276 - init_stock: 960 - price: 276 - service_level: 0.96 - vlt: 3 - SKU796: - cost: 447 - init_stock: 980 - price: 447 - service_level: 0.96 - vlt: 1 - SKU797: - cost: 100 - init_stock: 780 - price: 100 - service_level: 0.96 - vlt: 3 - SKU798: - cost: 426 - init_stock: 600 - price: 426 - service_level: 0.96 - vlt: 2 - SKU799: - cost: 63 - init_stock: 140 - price: 63 - service_level: 0.96 - vlt: 2 - SKU8: - cost: 125 - init_stock: 1260 - price: 125 - service_level: 0.96 - vlt: 3 - SKU80: - cost: 163 - init_stock: 1400 - price: 163 - service_level: 0.96 - vlt: 1 - SKU800: - cost: 298 - init_stock: 1880 - price: 298 - service_level: 0.96 - vlt: 2 - SKU801: - cost: 389 - init_stock: 720 - price: 389 - service_level: 0.96 - vlt: 2 - SKU802: - cost: 173 - init_stock: 1240 - price: 173 - service_level: 0.96 - vlt: 2 - SKU803: - cost: 272 - init_stock: 380 - price: 272 - service_level: 0.96 - vlt: 3 - SKU804: - cost: 390 - init_stock: 940 - price: 390 - service_level: 0.96 - vlt: 3 - SKU805: - cost: 61 - init_stock: 660 - price: 61 - service_level: 0.96 - vlt: 3 - SKU806: - cost: 158 - init_stock: 1040 - price: 158 - service_level: 0.96 - vlt: 1 - SKU807: - cost: 453 - init_stock: 520 - price: 453 - service_level: 0.96 - vlt: 3 - SKU808: - cost: 249 - init_stock: 1820 - price: 249 - service_level: 0.96 - vlt: 1 - SKU809: - cost: 55 - init_stock: 1300 - price: 55 - service_level: 0.96 - vlt: 1 - SKU81: - cost: 182 - init_stock: 1320 - price: 182 - service_level: 0.96 - vlt: 3 - SKU810: - cost: 276 - init_stock: 120 - price: 276 - service_level: 0.96 - vlt: 2 - SKU811: - cost: 254 - init_stock: 740 - price: 254 - service_level: 0.96 - vlt: 3 - SKU812: - cost: 252 - init_stock: 1600 - price: 252 - service_level: 0.96 - vlt: 1 - SKU813: - cost: 253 - init_stock: 140 - price: 253 - service_level: 0.96 - vlt: 3 - SKU814: - cost: 485 - init_stock: 1940 - price: 485 - service_level: 0.96 - vlt: 1 - SKU815: - cost: 390 - init_stock: 480 - price: 390 - service_level: 0.96 - vlt: 2 - SKU816: - cost: 152 - init_stock: 1820 - price: 152 - service_level: 0.96 - vlt: 3 - SKU817: - cost: 227 - init_stock: 100 - price: 227 - service_level: 0.96 - vlt: 2 - SKU818: - cost: 354 - init_stock: 860 - price: 354 - service_level: 0.96 - vlt: 2 - SKU819: - cost: 302 - init_stock: 540 - price: 302 - service_level: 0.96 - vlt: 1 - SKU82: - cost: 290 - init_stock: 560 - price: 290 - service_level: 0.96 - vlt: 1 - SKU820: - cost: 264 - init_stock: 1640 - price: 264 - service_level: 0.96 - vlt: 3 - SKU821: - cost: 99 - init_stock: 1380 - price: 99 - service_level: 0.96 - vlt: 1 - SKU822: - cost: 136 - init_stock: 1400 - price: 136 - service_level: 0.96 - vlt: 3 - SKU823: - cost: 75 - init_stock: 560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU824: - cost: 170 - init_stock: 1400 - price: 170 - service_level: 0.96 - vlt: 1 - SKU825: - cost: 214 - init_stock: 300 - price: 214 - service_level: 0.96 - vlt: 1 - SKU826: - cost: 386 - init_stock: 1100 - price: 386 - service_level: 0.96 - vlt: 2 - SKU827: - cost: 148 - init_stock: 1280 - price: 148 - service_level: 0.96 - vlt: 2 - SKU828: - cost: 400 - init_stock: 1380 - price: 400 - service_level: 0.96 - vlt: 1 - SKU829: - cost: 61 - init_stock: 1480 - price: 61 - service_level: 0.96 - vlt: 1 - SKU83: - cost: 296 - init_stock: 340 - price: 296 - service_level: 0.96 - vlt: 2 - SKU830: - cost: 167 - init_stock: 480 - price: 167 - service_level: 0.96 - vlt: 3 - SKU831: - cost: 262 - init_stock: 580 - price: 262 - service_level: 0.96 - vlt: 1 - SKU832: - cost: 33 - init_stock: 1640 - price: 33 - service_level: 0.96 - vlt: 1 - SKU833: - cost: 400 - init_stock: 1960 - price: 400 - service_level: 0.96 - vlt: 3 - SKU834: - cost: 422 - init_stock: 100 - price: 422 - service_level: 0.96 - vlt: 1 - SKU835: - cost: 440 - init_stock: 1040 - price: 440 - service_level: 0.96 - vlt: 2 - SKU836: - cost: 323 - init_stock: 1920 - price: 323 - service_level: 0.96 - vlt: 1 - SKU837: - cost: 373 - init_stock: 520 - price: 373 - service_level: 0.96 - vlt: 3 - SKU838: - cost: 456 - init_stock: 1540 - price: 456 - service_level: 0.96 - vlt: 1 - SKU839: - cost: 473 - init_stock: 1200 - price: 473 - service_level: 0.96 - vlt: 1 - SKU84: - cost: 318 - init_stock: 840 - price: 318 - service_level: 0.96 - vlt: 3 - SKU840: - cost: 266 - init_stock: 1000 - price: 266 - service_level: 0.96 - vlt: 1 - SKU841: - cost: 285 - init_stock: 1700 - price: 285 - service_level: 0.96 - vlt: 3 - SKU842: - cost: 41 - init_stock: 1680 - price: 41 - service_level: 0.96 - vlt: 1 - SKU843: - cost: 360 - init_stock: 1440 - price: 360 - service_level: 0.96 - vlt: 2 - SKU844: - cost: 51 - init_stock: 1580 - price: 51 - service_level: 0.96 - vlt: 3 - SKU845: - cost: 288 - init_stock: 440 - price: 288 - service_level: 0.96 - vlt: 3 - SKU846: - cost: 485 - init_stock: 780 - price: 485 - service_level: 0.96 - vlt: 1 - SKU847: - cost: 388 - init_stock: 1820 - price: 388 - service_level: 0.96 - vlt: 1 - SKU848: - cost: 306 - init_stock: 920 - price: 306 - service_level: 0.96 - vlt: 2 - SKU849: - cost: 320 - init_stock: 800 - price: 320 - service_level: 0.96 - vlt: 2 - SKU85: - cost: 442 - init_stock: 2000 - price: 442 - service_level: 0.96 - vlt: 3 - SKU850: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 2 - SKU851: - cost: 53 - init_stock: 1280 - price: 53 - service_level: 0.96 - vlt: 1 - SKU852: - cost: 306 - init_stock: 1080 - price: 306 - service_level: 0.96 - vlt: 2 - SKU853: - cost: 386 - init_stock: 1120 - price: 386 - service_level: 0.96 - vlt: 3 - SKU854: - cost: 212 - init_stock: 1300 - price: 212 - service_level: 0.96 - vlt: 3 - SKU855: - cost: 76 - init_stock: 1440 - price: 76 - service_level: 0.96 - vlt: 3 - SKU856: - cost: 380 - init_stock: 640 - price: 380 - service_level: 0.96 - vlt: 2 - SKU857: - cost: 462 - init_stock: 2000 - price: 462 - service_level: 0.96 - vlt: 1 - SKU858: - cost: 80 - init_stock: 480 - price: 80 - service_level: 0.96 - vlt: 3 - SKU859: - cost: 215 - init_stock: 560 - price: 215 - service_level: 0.96 - vlt: 3 - SKU86: - cost: 386 - init_stock: 1700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU860: - cost: 313 - init_stock: 300 - price: 313 - service_level: 0.96 - vlt: 2 - SKU861: - cost: 477 - init_stock: 260 - price: 477 - service_level: 0.96 - vlt: 2 - SKU862: - cost: 240 - init_stock: 320 - price: 240 - service_level: 0.96 - vlt: 2 - SKU863: - cost: 470 - init_stock: 1860 - price: 470 - service_level: 0.96 - vlt: 2 - SKU864: - cost: 203 - init_stock: 380 - price: 203 - service_level: 0.96 - vlt: 1 - SKU865: - cost: 144 - init_stock: 380 - price: 144 - service_level: 0.96 - vlt: 3 - SKU866: - cost: 172 - init_stock: 1760 - price: 172 - service_level: 0.96 - vlt: 1 - SKU867: - cost: 499 - init_stock: 2000 - price: 499 - service_level: 0.96 - vlt: 2 - SKU868: - cost: 41 - init_stock: 1000 - price: 41 - service_level: 0.96 - vlt: 2 - SKU869: - cost: 97 - init_stock: 1940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU87: - cost: 368 - init_stock: 1380 - price: 368 - service_level: 0.96 - vlt: 2 - SKU870: - cost: 281 - init_stock: 1340 - price: 281 - service_level: 0.96 - vlt: 1 - SKU871: - cost: 101 - init_stock: 1980 - price: 101 - service_level: 0.96 - vlt: 1 - SKU872: - cost: 133 - init_stock: 640 - price: 133 - service_level: 0.96 - vlt: 3 - SKU873: - cost: 423 - init_stock: 620 - price: 423 - service_level: 0.96 - vlt: 2 - SKU874: - cost: 469 - init_stock: 1200 - price: 469 - service_level: 0.96 - vlt: 1 - SKU875: - cost: 51 - init_stock: 460 - price: 51 - service_level: 0.96 - vlt: 2 - SKU876: - cost: 303 - init_stock: 1220 - price: 303 - service_level: 0.96 - vlt: 3 - SKU877: - cost: 40 - init_stock: 700 - price: 40 - service_level: 0.96 - vlt: 3 - SKU878: - cost: 27 - init_stock: 1360 - price: 27 - service_level: 0.96 - vlt: 3 - SKU879: - cost: 150 - init_stock: 2000 - price: 150 - service_level: 0.96 - vlt: 1 - SKU88: - cost: 471 - init_stock: 240 - price: 471 - service_level: 0.96 - vlt: 1 - SKU880: - cost: 433 - init_stock: 620 - price: 433 - service_level: 0.96 - vlt: 3 - SKU881: - cost: 431 - init_stock: 1400 - price: 431 - service_level: 0.96 - vlt: 3 - SKU882: - cost: 194 - init_stock: 320 - price: 194 - service_level: 0.96 - vlt: 1 - SKU883: - cost: 284 - init_stock: 760 - price: 284 - service_level: 0.96 - vlt: 1 - SKU884: - cost: 139 - init_stock: 780 - price: 139 - service_level: 0.96 - vlt: 2 - SKU885: - cost: 49 - init_stock: 540 - price: 49 - service_level: 0.96 - vlt: 3 - SKU886: - cost: 372 - init_stock: 1460 - price: 372 - service_level: 0.96 - vlt: 3 - SKU887: - cost: 266 - init_stock: 1740 - price: 266 - service_level: 0.96 - vlt: 1 - SKU888: - cost: 143 - init_stock: 180 - price: 143 - service_level: 0.96 - vlt: 2 - SKU889: - cost: 101 - init_stock: 420 - price: 101 - service_level: 0.96 - vlt: 2 - SKU89: - cost: 138 - init_stock: 1860 - price: 138 - service_level: 0.96 - vlt: 3 - SKU890: - cost: 161 - init_stock: 2000 - price: 161 - service_level: 0.96 - vlt: 3 - SKU891: - cost: 356 - init_stock: 440 - price: 356 - service_level: 0.96 - vlt: 3 - SKU892: - cost: 313 - init_stock: 1840 - price: 313 - service_level: 0.96 - vlt: 2 - SKU893: - cost: 229 - init_stock: 1980 - price: 229 - service_level: 0.96 - vlt: 1 - SKU894: - cost: 129 - init_stock: 1480 - price: 129 - service_level: 0.96 - vlt: 1 - SKU895: - cost: 230 - init_stock: 260 - price: 230 - service_level: 0.96 - vlt: 2 - SKU896: - cost: 289 - init_stock: 1600 - price: 289 - service_level: 0.96 - vlt: 3 - SKU897: - cost: 393 - init_stock: 1440 - price: 393 - service_level: 0.96 - vlt: 3 - SKU898: - cost: 477 - init_stock: 220 - price: 477 - service_level: 0.96 - vlt: 2 - SKU899: - cost: 233 - init_stock: 960 - price: 233 - service_level: 0.96 - vlt: 2 - SKU9: - cost: 166 - init_stock: 1920 - price: 166 - service_level: 0.96 - vlt: 2 - SKU90: - cost: 454 - init_stock: 1020 - price: 454 - service_level: 0.96 - vlt: 2 - SKU900: - cost: 158 - init_stock: 1100 - price: 158 - service_level: 0.96 - vlt: 1 - SKU901: - cost: 215 - init_stock: 580 - price: 215 - service_level: 0.96 - vlt: 3 - SKU902: - cost: 125 - init_stock: 1600 - price: 125 - service_level: 0.96 - vlt: 2 - SKU903: - cost: 357 - init_stock: 760 - price: 357 - service_level: 0.96 - vlt: 2 - SKU904: - cost: 496 - init_stock: 1020 - price: 496 - service_level: 0.96 - vlt: 1 - SKU905: - cost: 249 - init_stock: 620 - price: 249 - service_level: 0.96 - vlt: 3 - SKU906: - cost: 166 - init_stock: 1040 - price: 166 - service_level: 0.96 - vlt: 2 - SKU907: - cost: 22 - init_stock: 1660 - price: 22 - service_level: 0.96 - vlt: 3 - SKU908: - cost: 408 - init_stock: 1560 - price: 408 - service_level: 0.96 - vlt: 2 - SKU909: - cost: 482 - init_stock: 1920 - price: 482 - service_level: 0.96 - vlt: 1 - SKU91: - cost: 303 - init_stock: 320 - price: 303 - service_level: 0.96 - vlt: 2 - SKU910: - cost: 226 - init_stock: 660 - price: 226 - service_level: 0.96 - vlt: 2 - SKU911: - cost: 461 - init_stock: 1400 - price: 461 - service_level: 0.96 - vlt: 2 - SKU912: - cost: 236 - init_stock: 540 - price: 236 - service_level: 0.96 - vlt: 3 - SKU913: - cost: 322 - init_stock: 920 - price: 322 - service_level: 0.96 - vlt: 2 - SKU914: - cost: 272 - init_stock: 1240 - price: 272 - service_level: 0.96 - vlt: 3 - SKU915: - cost: 337 - init_stock: 1120 - price: 337 - service_level: 0.96 - vlt: 3 - SKU916: - cost: 337 - init_stock: 1780 - price: 337 - service_level: 0.96 - vlt: 3 - SKU917: - cost: 258 - init_stock: 600 - price: 258 - service_level: 0.96 - vlt: 3 - SKU918: - cost: 148 - init_stock: 1100 - price: 148 - service_level: 0.96 - vlt: 1 - SKU919: - cost: 393 - init_stock: 500 - price: 393 - service_level: 0.96 - vlt: 3 - SKU92: - cost: 262 - init_stock: 1260 - price: 262 - service_level: 0.96 - vlt: 2 - SKU920: - cost: 357 - init_stock: 1720 - price: 357 - service_level: 0.96 - vlt: 3 - SKU921: - cost: 227 - init_stock: 1320 - price: 227 - service_level: 0.96 - vlt: 2 - SKU922: - cost: 112 - init_stock: 1340 - price: 112 - service_level: 0.96 - vlt: 2 - SKU923: - cost: 496 - init_stock: 1160 - price: 496 - service_level: 0.96 - vlt: 2 - SKU924: - cost: 316 - init_stock: 1700 - price: 316 - service_level: 0.96 - vlt: 3 - SKU925: - cost: 360 - init_stock: 300 - price: 360 - service_level: 0.96 - vlt: 1 - SKU926: - cost: 360 - init_stock: 1340 - price: 360 - service_level: 0.96 - vlt: 2 - SKU927: - cost: 260 - init_stock: 420 - price: 260 - service_level: 0.96 - vlt: 3 - SKU928: - cost: 491 - init_stock: 1660 - price: 491 - service_level: 0.96 - vlt: 1 - SKU929: - cost: 359 - init_stock: 2000 - price: 359 - service_level: 0.96 - vlt: 3 - SKU93: - cost: 404 - init_stock: 1340 - price: 404 - service_level: 0.96 - vlt: 2 - SKU930: - cost: 198 - init_stock: 560 - price: 198 - service_level: 0.96 - vlt: 2 - SKU931: - cost: 71 - init_stock: 280 - price: 71 - service_level: 0.96 - vlt: 3 - SKU932: - cost: 163 - init_stock: 1320 - price: 163 - service_level: 0.96 - vlt: 2 - SKU933: - cost: 113 - init_stock: 1560 - price: 113 - service_level: 0.96 - vlt: 1 - SKU934: - cost: 219 - init_stock: 1340 - price: 219 - service_level: 0.96 - vlt: 3 - SKU935: - cost: 364 - init_stock: 1880 - price: 364 - service_level: 0.96 - vlt: 1 - SKU936: - cost: 24 - init_stock: 100 - price: 24 - service_level: 0.96 - vlt: 3 - SKU937: - cost: 135 - init_stock: 340 - price: 135 - service_level: 0.96 - vlt: 1 - SKU938: - cost: 432 - init_stock: 420 - price: 432 - service_level: 0.96 - vlt: 2 - SKU939: - cost: 173 - init_stock: 1180 - price: 173 - service_level: 0.96 - vlt: 3 - SKU94: - cost: 184 - init_stock: 940 - price: 184 - service_level: 0.96 - vlt: 3 - SKU940: - cost: 14 - init_stock: 1860 - price: 14 - service_level: 0.96 - vlt: 1 - SKU941: - cost: 80 - init_stock: 1140 - price: 80 - service_level: 0.96 - vlt: 3 - SKU942: - cost: 202 - init_stock: 260 - price: 202 - service_level: 0.96 - vlt: 3 - SKU943: - cost: 138 - init_stock: 980 - price: 138 - service_level: 0.96 - vlt: 1 - SKU944: - cost: 196 - init_stock: 880 - price: 196 - service_level: 0.96 - vlt: 1 - SKU945: - cost: 141 - init_stock: 340 - price: 141 - service_level: 0.96 - vlt: 2 - SKU946: - cost: 325 - init_stock: 980 - price: 325 - service_level: 0.96 - vlt: 1 - SKU947: - cost: 338 - init_stock: 1820 - price: 338 - service_level: 0.96 - vlt: 3 - SKU948: - cost: 425 - init_stock: 560 - price: 425 - service_level: 0.96 - vlt: 3 - SKU949: - cost: 309 - init_stock: 100 - price: 309 - service_level: 0.96 - vlt: 2 - SKU95: - cost: 136 - init_stock: 420 - price: 136 - service_level: 0.96 - vlt: 3 - SKU950: - cost: 102 - init_stock: 1080 - price: 102 - service_level: 0.96 - vlt: 2 - SKU951: - cost: 75 - init_stock: 360 - price: 75 - service_level: 0.96 - vlt: 2 - SKU952: - cost: 156 - init_stock: 220 - price: 156 - service_level: 0.96 - vlt: 3 - SKU953: - cost: 138 - init_stock: 700 - price: 138 - service_level: 0.96 - vlt: 3 - SKU954: - cost: 296 - init_stock: 1720 - price: 296 - service_level: 0.96 - vlt: 1 - SKU955: - cost: 55 - init_stock: 1260 - price: 55 - service_level: 0.96 - vlt: 1 - SKU956: - cost: 282 - init_stock: 1700 - price: 282 - service_level: 0.96 - vlt: 1 - SKU957: - cost: 305 - init_stock: 1020 - price: 305 - service_level: 0.96 - vlt: 2 - SKU958: - cost: 369 - init_stock: 1320 - price: 369 - service_level: 0.96 - vlt: 2 - SKU959: - cost: 81 - init_stock: 1640 - price: 81 - service_level: 0.96 - vlt: 1 - SKU96: - cost: 176 - init_stock: 880 - price: 176 - service_level: 0.96 - vlt: 3 - SKU960: - cost: 147 - init_stock: 120 - price: 147 - service_level: 0.96 - vlt: 3 - SKU961: - cost: 264 - init_stock: 880 - price: 264 - service_level: 0.96 - vlt: 1 - SKU962: - cost: 354 - init_stock: 880 - price: 354 - service_level: 0.96 - vlt: 1 - SKU963: - cost: 349 - init_stock: 420 - price: 349 - service_level: 0.96 - vlt: 1 - SKU964: - cost: 244 - init_stock: 1460 - price: 244 - service_level: 0.96 - vlt: 1 - SKU965: - cost: 124 - init_stock: 1020 - price: 124 - service_level: 0.96 - vlt: 1 - SKU966: - cost: 302 - init_stock: 880 - price: 302 - service_level: 0.96 - vlt: 3 - SKU967: - cost: 67 - init_stock: 900 - price: 67 - service_level: 0.96 - vlt: 3 - SKU968: - cost: 281 - init_stock: 980 - price: 281 - service_level: 0.96 - vlt: 2 - SKU969: - cost: 249 - init_stock: 120 - price: 249 - service_level: 0.96 - vlt: 1 - SKU97: - cost: 28 - init_stock: 600 - price: 28 - service_level: 0.96 - vlt: 3 - SKU970: - cost: 244 - init_stock: 100 - price: 244 - service_level: 0.96 - vlt: 2 - SKU971: - cost: 368 - init_stock: 1460 - price: 368 - service_level: 0.96 - vlt: 1 - SKU972: - cost: 209 - init_stock: 1380 - price: 209 - service_level: 0.96 - vlt: 1 - SKU973: - cost: 271 - init_stock: 220 - price: 271 - service_level: 0.96 - vlt: 2 - SKU974: - cost: 170 - init_stock: 640 - price: 170 - service_level: 0.96 - vlt: 1 - SKU975: - cost: 198 - init_stock: 440 - price: 198 - service_level: 0.96 - vlt: 3 - SKU976: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 2 - SKU977: - cost: 234 - init_stock: 1080 - price: 234 - service_level: 0.96 - vlt: 3 - SKU978: - cost: 470 - init_stock: 1280 - price: 470 - service_level: 0.96 - vlt: 3 - SKU979: - cost: 443 - init_stock: 1920 - price: 443 - service_level: 0.96 - vlt: 2 - SKU98: - cost: 443 - init_stock: 700 - price: 443 - service_level: 0.96 - vlt: 1 - SKU980: - cost: 39 - init_stock: 1360 - price: 39 - service_level: 0.96 - vlt: 3 - SKU981: - cost: 482 - init_stock: 1440 - price: 482 - service_level: 0.96 - vlt: 2 - SKU982: - cost: 213 - init_stock: 1040 - price: 213 - service_level: 0.96 - vlt: 3 - SKU983: - cost: 449 - init_stock: 1840 - price: 449 - service_level: 0.96 - vlt: 1 - SKU984: - cost: 232 - init_stock: 1820 - price: 232 - service_level: 0.96 - vlt: 3 - SKU985: - cost: 290 - init_stock: 1020 - price: 290 - service_level: 0.96 - vlt: 1 - SKU986: - cost: 275 - init_stock: 340 - price: 275 - service_level: 0.96 - vlt: 3 - SKU987: - cost: 434 - init_stock: 680 - price: 434 - service_level: 0.96 - vlt: 1 - SKU988: - cost: 102 - init_stock: 460 - price: 102 - service_level: 0.96 - vlt: 1 - SKU989: - cost: 484 - init_stock: 1860 - price: 484 - service_level: 0.96 - vlt: 1 - SKU99: - cost: 361 - init_stock: 900 - price: 361 - service_level: 0.96 - vlt: 3 - SKU990: - cost: 108 - init_stock: 1140 - price: 108 - service_level: 0.96 - vlt: 3 - SKU991: - cost: 409 - init_stock: 380 - price: 409 - service_level: 0.96 - vlt: 3 - SKU992: - cost: 434 - init_stock: 1860 - price: 434 - service_level: 0.96 - vlt: 3 - SKU993: - cost: 145 - init_stock: 1720 - price: 145 - service_level: 0.96 - vlt: 2 - SKU994: - cost: 470 - init_stock: 1000 - price: 470 - service_level: 0.96 - vlt: 3 - SKU995: - cost: 241 - init_stock: 1520 - price: 241 - service_level: 0.96 - vlt: 2 - SKU996: - cost: 260 - init_stock: 1460 - price: 260 - service_level: 0.96 - vlt: 3 - SKU997: - cost: 400 - init_stock: 680 - price: 400 - service_level: 0.96 - vlt: 1 - SKU998: - cost: 447 - init_stock: 1100 - price: 447 - service_level: 0.96 - vlt: 2 - SKU999: - cost: 79 - init_stock: 460 - price: 79 - service_level: 0.96 - vlt: 2 - - children: - storage: - config: - capacity: 1064900 - unit_storage_cost: 1 - config: - order_cost: 500 - definition_ref: RetailerFacility - name: STORE0 - skus: - SKU0: - constraint: null - cost: 322 - init_stock: 126 - max_stock: 1000 - price: 631 - sale_gamma: 63 - service_level: 0.95 - SKU1: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 284 - init_stock: 176 - max_stock: 1000 - price: 448 - sale_gamma: 22 - service_level: 0.95 - SKU10: - constraint: null - cost: 68 - init_stock: 150 - max_stock: 1000 - price: 116 - sale_gamma: 25 - service_level: 0.95 - SKU100: - constraint: G(low_profit -> low_stock_constraint) - cost: 16 - init_stock: 390 - max_stock: 1000 - price: 23 - sale_gamma: 65 - service_level: 0.95 - SKU101: - constraint: null - cost: 84 - init_stock: 497 - max_stock: 1000 - price: 149 - sale_gamma: 71 - service_level: 0.95 - SKU102: - constraint: null - cost: 328 - init_stock: 558 - max_stock: 1000 - price: 505 - sale_gamma: 93 - service_level: 0.95 - SKU103: - constraint: null - cost: 334 - init_stock: 285 - max_stock: 1000 - price: 601 - sale_gamma: 95 - service_level: 0.95 - SKU104: - constraint: null - cost: 49 - init_stock: 325 - max_stock: 1000 - price: 57 - sale_gamma: 65 - service_level: 0.95 - SKU105: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 110 - init_stock: 285 - max_stock: 1000 - price: 171 - sale_gamma: 57 - service_level: 0.95 - SKU106: - constraint: G(low_profit -> low_stock_constraint) - cost: 251 - init_stock: 584 - max_stock: 1000 - price: 454 - sale_gamma: 73 - service_level: 0.95 - SKU107: - constraint: G(stock_constraint) - cost: 423 - init_stock: 348 - max_stock: 1000 - price: 706 - sale_gamma: 87 - service_level: 0.95 - SKU108: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 458 - init_stock: 368 - max_stock: 1000 - price: 801 - sale_gamma: 92 - service_level: 0.95 - SKU109: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 88 - init_stock: 328 - max_stock: 1000 - price: 98 - sale_gamma: 82 - service_level: 0.95 - SKU11: - constraint: null - cost: 400 - init_stock: 306 - max_stock: 1000 - price: 680 - sale_gamma: 51 - service_level: 0.95 - SKU110: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 66 - init_stock: 112 - max_stock: 1000 - price: 100 - sale_gamma: 14 - service_level: 0.95 - SKU111: - constraint: G(stock_constraint) - cost: 260 - init_stock: 183 - max_stock: 1000 - price: 364 - sale_gamma: 61 - service_level: 0.95 - SKU112: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 61 - init_stock: 475 - max_stock: 1000 - price: 119 - sale_gamma: 95 - service_level: 0.95 - SKU113: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 155 - max_stock: 1000 - price: 678 - sale_gamma: 31 - service_level: 0.95 - SKU114: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 389 - init_stock: 162 - max_stock: 1000 - price: 564 - sale_gamma: 27 - service_level: 0.95 - SKU115: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 258 - max_stock: 1000 - price: 557 - sale_gamma: 86 - service_level: 0.95 - SKU116: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 288 - max_stock: 1000 - price: 768 - sale_gamma: 72 - service_level: 0.95 - SKU117: - constraint: null - cost: 320 - init_stock: 644 - max_stock: 1000 - price: 374 - sale_gamma: 92 - service_level: 0.95 - SKU118: - constraint: null - cost: 183 - init_stock: 66 - max_stock: 1000 - price: 208 - sale_gamma: 33 - service_level: 0.95 - SKU119: - constraint: null - cost: 209 - init_stock: 256 - max_stock: 1000 - price: 317 - sale_gamma: 32 - service_level: 0.95 - SKU12: - constraint: null - cost: 112 - init_stock: 336 - max_stock: 1000 - price: 210 - sale_gamma: 84 - service_level: 0.95 - SKU120: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 121 - init_stock: 392 - max_stock: 1000 - price: 151 - sale_gamma: 98 - service_level: 0.95 - SKU121: - constraint: null - cost: 40 - init_stock: 510 - max_stock: 1000 - price: 54 - sale_gamma: 85 - service_level: 0.95 - SKU122: - constraint: G(stock_constraint) - cost: 437 - init_stock: 35 - max_stock: 1000 - price: 589 - sale_gamma: 7 - service_level: 0.95 - SKU123: - constraint: G(stock_constraint) - cost: 233 - init_stock: 114 - max_stock: 1000 - price: 326 - sale_gamma: 19 - service_level: 0.95 - SKU124: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 108 - max_stock: 1000 - price: 298 - sale_gamma: 36 - service_level: 0.95 - SKU125: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 368 - max_stock: 1000 - price: 32 - sale_gamma: 92 - service_level: 0.95 - SKU126: - constraint: null - cost: 36 - init_stock: 156 - max_stock: 1000 - price: 40 - sale_gamma: 39 - service_level: 0.95 - SKU127: - constraint: G(stock_constraint) - cost: 217 - init_stock: 155 - max_stock: 1000 - price: 366 - sale_gamma: 31 - service_level: 0.95 - SKU128: - constraint: null - cost: 165 - init_stock: 95 - max_stock: 1000 - price: 283 - sale_gamma: 19 - service_level: 0.95 - SKU129: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 203 - sale_gamma: 50 - service_level: 0.95 - SKU13: - constraint: null - cost: 317 - init_stock: 456 - max_stock: 1000 - price: 573 - sale_gamma: 57 - service_level: 0.95 - SKU130: - constraint: null - cost: 348 - init_stock: 784 - max_stock: 1000 - price: 396 - sale_gamma: 98 - service_level: 0.95 - SKU131: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 64 - init_stock: 270 - max_stock: 1000 - price: 110 - sale_gamma: 45 - service_level: 0.95 - SKU132: - constraint: null - cost: 427 - init_stock: 105 - max_stock: 1000 - price: 678 - sale_gamma: 21 - service_level: 0.95 - SKU133: - constraint: null - cost: 224 - init_stock: 145 - max_stock: 1000 - price: 448 - sale_gamma: 29 - service_level: 0.95 - SKU134: - constraint: G(stock_constraint) - cost: 336 - init_stock: 539 - max_stock: 1000 - price: 470 - sale_gamma: 77 - service_level: 0.95 - SKU135: - constraint: null - cost: 153 - init_stock: 500 - max_stock: 1000 - price: 243 - sale_gamma: 100 - service_level: 0.95 - SKU136: - constraint: G(low_profit -> low_stock_constraint) - cost: 199 - init_stock: 497 - max_stock: 1000 - price: 370 - sale_gamma: 71 - service_level: 0.95 - SKU137: - constraint: G(stock_constraint) - cost: 93 - init_stock: 444 - max_stock: 1000 - price: 165 - sale_gamma: 74 - service_level: 0.95 - SKU138: - constraint: G(stock_constraint) - cost: 228 - init_stock: 180 - max_stock: 1000 - price: 253 - sale_gamma: 36 - service_level: 0.95 - SKU139: - constraint: G(stock_constraint) - cost: 207 - init_stock: 144 - max_stock: 1000 - price: 368 - sale_gamma: 24 - service_level: 0.95 - SKU14: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 268 - init_stock: 372 - max_stock: 1000 - price: 305 - sale_gamma: 62 - service_level: 0.95 - SKU140: - constraint: null - cost: 261 - init_stock: 238 - max_stock: 1000 - price: 357 - sale_gamma: 34 - service_level: 0.95 - SKU141: - constraint: null - cost: 190 - init_stock: 164 - max_stock: 1000 - price: 370 - sale_gamma: 41 - service_level: 0.95 - SKU142: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 320 - init_stock: 152 - max_stock: 1000 - price: 428 - sale_gamma: 38 - service_level: 0.95 - SKU143: - constraint: G(stock_constraint) - cost: 318 - init_stock: 182 - max_stock: 1000 - price: 566 - sale_gamma: 26 - service_level: 0.95 - SKU144: - constraint: null - cost: 400 - init_stock: 24 - max_stock: 1000 - price: 716 - sale_gamma: 12 - service_level: 0.95 - SKU145: - constraint: null - cost: 399 - init_stock: 328 - max_stock: 1000 - price: 774 - sale_gamma: 82 - service_level: 0.95 - SKU146: - constraint: null - cost: 177 - init_stock: 192 - max_stock: 1000 - price: 194 - sale_gamma: 48 - service_level: 0.95 - SKU147: - constraint: G(low_profit -> low_stock_constraint) - cost: 472 - init_stock: 392 - max_stock: 1000 - price: 840 - sale_gamma: 56 - service_level: 0.95 - SKU148: - constraint: G(stock_constraint) - cost: 313 - init_stock: 308 - max_stock: 1000 - price: 566 - sale_gamma: 77 - service_level: 0.95 - SKU149: - constraint: G(low_profit -> low_stock_constraint) - cost: 357 - init_stock: 539 - max_stock: 1000 - price: 549 - sale_gamma: 77 - service_level: 0.95 - SKU15: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 25 - max_stock: 1000 - price: 58 - sale_gamma: 5 - service_level: 0.95 - SKU150: - constraint: null - cost: 106 - init_stock: 210 - max_stock: 1000 - price: 159 - sale_gamma: 70 - service_level: 0.95 - SKU151: - constraint: G(low_profit -> low_stock_constraint) - cost: 223 - init_stock: 146 - max_stock: 1000 - price: 370 - sale_gamma: 73 - service_level: 0.95 - SKU152: - constraint: null - cost: 10 - init_stock: 408 - max_stock: 1000 - price: 14 - sale_gamma: 51 - service_level: 0.95 - SKU153: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 441 - init_stock: 372 - max_stock: 1000 - price: 538 - sale_gamma: 62 - service_level: 0.95 - SKU154: - constraint: null - cost: 77 - init_stock: 680 - max_stock: 1000 - price: 135 - sale_gamma: 85 - service_level: 0.95 - SKU155: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 159 - max_stock: 1000 - price: 641 - sale_gamma: 53 - service_level: 0.95 - SKU156: - constraint: null - cost: 10 - init_stock: 36 - max_stock: 1000 - price: 16 - sale_gamma: 12 - service_level: 0.95 - SKU157: - constraint: null - cost: 410 - init_stock: 525 - max_stock: 1000 - price: 594 - sale_gamma: 75 - service_level: 0.95 - SKU158: - constraint: null - cost: 145 - init_stock: 486 - max_stock: 1000 - price: 275 - sale_gamma: 81 - service_level: 0.95 - SKU159: - constraint: G(stock_constraint) - cost: 193 - init_stock: 100 - max_stock: 1000 - price: 368 - sale_gamma: 25 - service_level: 0.95 - SKU16: - constraint: null - cost: 175 - init_stock: 464 - max_stock: 1000 - price: 344 - sale_gamma: 58 - service_level: 0.95 - SKU160: - constraint: G(low_profit -> low_stock_constraint) - cost: 459 - init_stock: 510 - max_stock: 1000 - price: 876 - sale_gamma: 85 - service_level: 0.95 - SKU161: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 239 - init_stock: 230 - max_stock: 1000 - price: 478 - sale_gamma: 46 - service_level: 0.95 - SKU162: - constraint: null - cost: 158 - init_stock: 30 - max_stock: 1000 - price: 290 - sale_gamma: 5 - service_level: 0.95 - SKU163: - constraint: G(stock_constraint) - cost: 486 - init_stock: 273 - max_stock: 1000 - price: 704 - sale_gamma: 39 - service_level: 0.95 - SKU164: - constraint: null - cost: 496 - init_stock: 500 - max_stock: 1000 - price: 615 - sale_gamma: 100 - service_level: 0.95 - SKU165: - constraint: G(stock_constraint) - cost: 274 - init_stock: 231 - max_stock: 1000 - price: 548 - sale_gamma: 33 - service_level: 0.95 - SKU166: - constraint: null - cost: 79 - init_stock: 178 - max_stock: 1000 - price: 137 - sale_gamma: 89 - service_level: 0.95 - SKU167: - constraint: G(stock_constraint) - cost: 443 - init_stock: 52 - max_stock: 1000 - price: 854 - sale_gamma: 13 - service_level: 0.95 - SKU168: - constraint: null - cost: 357 - init_stock: 522 - max_stock: 1000 - price: 546 - sale_gamma: 87 - service_level: 0.95 - SKU169: - constraint: null - cost: 369 - init_stock: 686 - max_stock: 1000 - price: 505 - sale_gamma: 98 - service_level: 0.95 - SKU17: - constraint: null - cost: 346 - init_stock: 18 - max_stock: 1000 - price: 553 - sale_gamma: 9 - service_level: 0.95 - SKU170: - constraint: null - cost: 68 - init_stock: 275 - max_stock: 1000 - price: 113 - sale_gamma: 55 - service_level: 0.95 - SKU171: - constraint: G(low_profit -> low_stock_constraint) - cost: 398 - init_stock: 380 - max_stock: 1000 - price: 704 - sale_gamma: 76 - service_level: 0.95 - SKU172: - constraint: null - cost: 200 - init_stock: 426 - max_stock: 1000 - price: 222 - sale_gamma: 71 - service_level: 0.95 - SKU173: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 480 - max_stock: 1000 - price: 264 - sale_gamma: 96 - service_level: 0.95 - SKU174: - constraint: null - cost: 291 - init_stock: 380 - max_stock: 1000 - price: 582 - sale_gamma: 76 - service_level: 0.95 - SKU175: - constraint: null - cost: 84 - init_stock: 225 - max_stock: 1000 - price: 140 - sale_gamma: 75 - service_level: 0.95 - SKU176: - constraint: G(stock_constraint) - cost: 407 - init_stock: 330 - max_stock: 1000 - price: 610 - sale_gamma: 66 - service_level: 0.95 - SKU177: - constraint: null - cost: 257 - init_stock: 155 - max_stock: 1000 - price: 346 - sale_gamma: 31 - service_level: 0.95 - SKU178: - constraint: null - cost: 423 - init_stock: 20 - max_stock: 1000 - price: 499 - sale_gamma: 5 - service_level: 0.95 - SKU179: - constraint: null - cost: 497 - init_stock: 415 - max_stock: 1000 - price: 690 - sale_gamma: 83 - service_level: 0.95 - SKU18: - constraint: null - cost: 258 - init_stock: 405 - max_stock: 1000 - price: 387 - sale_gamma: 81 - service_level: 0.95 - SKU180: - constraint: null - cost: 217 - init_stock: 165 - max_stock: 1000 - price: 297 - sale_gamma: 55 - service_level: 0.95 - SKU181: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 214 - sale_gamma: 60 - service_level: 0.95 - SKU182: - constraint: null - cost: 437 - init_stock: 693 - max_stock: 1000 - price: 764 - sale_gamma: 99 - service_level: 0.95 - SKU183: - constraint: null - cost: 145 - init_stock: 56 - max_stock: 1000 - price: 178 - sale_gamma: 8 - service_level: 0.95 - SKU184: - constraint: null - cost: 73 - init_stock: 72 - max_stock: 1000 - price: 100 - sale_gamma: 24 - service_level: 0.95 - SKU185: - constraint: null - cost: 10 - init_stock: 360 - max_stock: 1000 - price: 14 - sale_gamma: 90 - service_level: 0.95 - SKU186: - constraint: null - cost: 359 - init_stock: 66 - max_stock: 1000 - price: 649 - sale_gamma: 22 - service_level: 0.95 - SKU187: - constraint: G(low_profit -> low_stock_constraint) - cost: 177 - init_stock: 120 - max_stock: 1000 - price: 249 - sale_gamma: 30 - service_level: 0.95 - SKU188: - constraint: null - cost: 391 - init_stock: 348 - max_stock: 1000 - price: 586 - sale_gamma: 87 - service_level: 0.95 - SKU189: - constraint: G(low_profit -> low_stock_constraint) - cost: 358 - init_stock: 210 - max_stock: 1000 - price: 400 - sale_gamma: 35 - service_level: 0.95 - SKU19: - constraint: G(stock_constraint) - cost: 477 - init_stock: 364 - max_stock: 1000 - price: 791 - sale_gamma: 91 - service_level: 0.95 - SKU190: - constraint: null - cost: 113 - init_stock: 102 - max_stock: 1000 - price: 153 - sale_gamma: 17 - service_level: 0.95 - SKU191: - constraint: G(stock_constraint) - cost: 473 - init_stock: 324 - max_stock: 1000 - price: 638 - sale_gamma: 54 - service_level: 0.95 - SKU192: - constraint: null - cost: 415 - init_stock: 244 - max_stock: 1000 - price: 539 - sale_gamma: 61 - service_level: 0.95 - SKU193: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 207 - init_stock: 120 - max_stock: 1000 - price: 353 - sale_gamma: 30 - service_level: 0.95 - SKU194: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 30 - max_stock: 1000 - price: 803 - sale_gamma: 5 - service_level: 0.95 - SKU195: - constraint: G(low_profit -> low_stock_constraint) - cost: 218 - init_stock: 93 - max_stock: 1000 - price: 248 - sale_gamma: 31 - service_level: 0.95 - SKU196: - constraint: null - cost: 49 - init_stock: 544 - max_stock: 1000 - price: 62 - sale_gamma: 68 - service_level: 0.95 - SKU197: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 285 - max_stock: 1000 - price: 509 - sale_gamma: 57 - service_level: 0.95 - SKU198: - constraint: G(low_profit -> low_stock_constraint) - cost: 169 - init_stock: 378 - max_stock: 1000 - price: 307 - sale_gamma: 54 - service_level: 0.95 - SKU199: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 138 - max_stock: 1000 - price: 794 - sale_gamma: 23 - service_level: 0.95 - SKU2: - constraint: null - cost: 331 - init_stock: 350 - max_stock: 1000 - price: 499 - sale_gamma: 70 - service_level: 0.95 - SKU20: - constraint: G(stock_constraint) - cost: 335 - init_stock: 200 - max_stock: 1000 - price: 649 - sale_gamma: 25 - service_level: 0.95 - SKU200: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 65 - init_stock: 150 - max_stock: 1000 - price: 85 - sale_gamma: 25 - service_level: 0.95 - SKU201: - constraint: G(stock_constraint) - cost: 104 - init_stock: 354 - max_stock: 1000 - price: 161 - sale_gamma: 59 - service_level: 0.95 - SKU202: - constraint: null - cost: 142 - init_stock: 365 - max_stock: 1000 - price: 222 - sale_gamma: 73 - service_level: 0.95 - SKU203: - constraint: null - cost: 440 - init_stock: 328 - max_stock: 1000 - price: 677 - sale_gamma: 82 - service_level: 0.95 - SKU204: - constraint: null - cost: 489 - init_stock: 188 - max_stock: 1000 - price: 831 - sale_gamma: 47 - service_level: 0.95 - SKU205: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 130 - init_stock: 500 - max_stock: 1000 - price: 175 - sale_gamma: 100 - service_level: 0.95 - SKU206: - constraint: null - cost: 335 - init_stock: 55 - max_stock: 1000 - price: 552 - sale_gamma: 11 - service_level: 0.95 - SKU207: - constraint: G(low_profit -> low_stock_constraint) - cost: 140 - init_stock: 240 - max_stock: 1000 - price: 170 - sale_gamma: 80 - service_level: 0.95 - SKU208: - constraint: null - cost: 491 - init_stock: 308 - max_stock: 1000 - price: 927 - sale_gamma: 77 - service_level: 0.95 - SKU209: - constraint: G(stock_constraint) - cost: 179 - init_stock: 120 - max_stock: 1000 - price: 322 - sale_gamma: 20 - service_level: 0.95 - SKU21: - constraint: null - cost: 123 - init_stock: 400 - max_stock: 1000 - price: 225 - sale_gamma: 100 - service_level: 0.95 - SKU210: - constraint: null - cost: 404 - init_stock: 345 - max_stock: 1000 - price: 468 - sale_gamma: 69 - service_level: 0.95 - SKU211: - constraint: null - cost: 174 - init_stock: 364 - max_stock: 1000 - price: 226 - sale_gamma: 91 - service_level: 0.95 - SKU212: - constraint: null - cost: 405 - init_stock: 632 - max_stock: 1000 - price: 534 - sale_gamma: 79 - service_level: 0.95 - SKU213: - constraint: null - cost: 121 - init_stock: 192 - max_stock: 1000 - price: 229 - sale_gamma: 64 - service_level: 0.95 - SKU214: - constraint: G(stock_constraint) - cost: 101 - init_stock: 60 - max_stock: 1000 - price: 144 - sale_gamma: 10 - service_level: 0.95 - SKU215: - constraint: null - cost: 419 - init_stock: 94 - max_stock: 1000 - price: 469 - sale_gamma: 47 - service_level: 0.95 - SKU216: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 330 - init_stock: 92 - max_stock: 1000 - price: 432 - sale_gamma: 23 - service_level: 0.95 - SKU217: - constraint: null - cost: 284 - init_stock: 455 - max_stock: 1000 - price: 312 - sale_gamma: 65 - service_level: 0.95 - SKU218: - constraint: G(low_profit -> low_stock_constraint) - cost: 205 - init_stock: 354 - max_stock: 1000 - price: 397 - sale_gamma: 59 - service_level: 0.95 - SKU219: - constraint: G(stock_constraint) - cost: 92 - init_stock: 368 - max_stock: 1000 - price: 135 - sale_gamma: 46 - service_level: 0.95 - SKU22: - constraint: null - cost: 493 - init_stock: 264 - max_stock: 1000 - price: 986 - sale_gamma: 66 - service_level: 0.95 - SKU220: - constraint: null - cost: 387 - init_stock: 435 - max_stock: 1000 - price: 572 - sale_gamma: 87 - service_level: 0.95 - SKU221: - constraint: null - cost: 39 - init_stock: 468 - max_stock: 1000 - price: 48 - sale_gamma: 78 - service_level: 0.95 - SKU222: - constraint: G(low_profit -> low_stock_constraint) - cost: 115 - init_stock: 180 - max_stock: 1000 - price: 210 - sale_gamma: 36 - service_level: 0.95 - SKU223: - constraint: G(low_profit -> low_stock_constraint) - cost: 196 - init_stock: 36 - max_stock: 1000 - price: 286 - sale_gamma: 12 - service_level: 0.95 - SKU224: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 387 - init_stock: 25 - max_stock: 1000 - price: 661 - sale_gamma: 5 - service_level: 0.95 - SKU225: - constraint: null - cost: 164 - init_stock: 116 - max_stock: 1000 - price: 216 - sale_gamma: 29 - service_level: 0.95 - SKU226: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 206 - init_stock: 65 - max_stock: 1000 - price: 350 - sale_gamma: 13 - service_level: 0.95 - SKU227: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 144 - max_stock: 1000 - price: 895 - sale_gamma: 24 - service_level: 0.95 - SKU228: - constraint: null - cost: 14 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 90 - service_level: 0.95 - SKU229: - constraint: null - cost: 472 - init_stock: 176 - max_stock: 1000 - price: 708 - sale_gamma: 44 - service_level: 0.95 - SKU23: - constraint: null - cost: 387 - init_stock: 210 - max_stock: 1000 - price: 700 - sale_gamma: 42 - service_level: 0.95 - SKU230: - constraint: null - cost: 241 - init_stock: 138 - max_stock: 1000 - price: 436 - sale_gamma: 23 - service_level: 0.95 - SKU231: - constraint: G(stock_constraint) - cost: 48 - init_stock: 486 - max_stock: 1000 - price: 96 - sale_gamma: 81 - service_level: 0.95 - SKU232: - constraint: G(low_profit -> low_stock_constraint) - cost: 224 - init_stock: 480 - max_stock: 1000 - price: 338 - sale_gamma: 96 - service_level: 0.95 - SKU233: - constraint: G(low_profit -> low_stock_constraint) - cost: 360 - init_stock: 300 - max_stock: 1000 - price: 428 - sale_gamma: 75 - service_level: 0.95 - SKU234: - constraint: null - cost: 287 - init_stock: 25 - max_stock: 1000 - price: 387 - sale_gamma: 5 - service_level: 0.95 - SKU235: - constraint: G(low_profit -> low_stock_constraint) - cost: 24 - init_stock: 228 - max_stock: 1000 - price: 32 - sale_gamma: 57 - service_level: 0.95 - SKU236: - constraint: G(low_profit -> low_stock_constraint) - cost: 155 - init_stock: 165 - max_stock: 1000 - price: 289 - sale_gamma: 55 - service_level: 0.95 - SKU237: - constraint: null - cost: 433 - init_stock: 270 - max_stock: 1000 - price: 779 - sale_gamma: 45 - service_level: 0.95 - SKU238: - constraint: G(stock_constraint) - cost: 64 - init_stock: 264 - max_stock: 1000 - price: 112 - sale_gamma: 66 - service_level: 0.95 - SKU239: - constraint: null - cost: 103 - init_stock: 490 - max_stock: 1000 - price: 139 - sale_gamma: 98 - service_level: 0.95 - SKU24: - constraint: null - cost: 97 - init_stock: 329 - max_stock: 1000 - price: 114 - sale_gamma: 47 - service_level: 0.95 - SKU240: - constraint: null - cost: 373 - init_stock: 188 - max_stock: 1000 - price: 738 - sale_gamma: 47 - service_level: 0.95 - SKU241: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 213 - max_stock: 1000 - price: 860 - sale_gamma: 71 - service_level: 0.95 - SKU242: - constraint: null - cost: 17 - init_stock: 88 - max_stock: 1000 - price: 29 - sale_gamma: 44 - service_level: 0.95 - SKU243: - constraint: null - cost: 352 - init_stock: 84 - max_stock: 1000 - price: 394 - sale_gamma: 14 - service_level: 0.95 - SKU244: - constraint: null - cost: 174 - init_stock: 410 - max_stock: 1000 - price: 226 - sale_gamma: 82 - service_level: 0.95 - SKU245: - constraint: G(low_profit -> low_stock_constraint) - cost: 404 - init_stock: 198 - max_stock: 1000 - price: 525 - sale_gamma: 66 - service_level: 0.95 - SKU246: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 300 - init_stock: 210 - max_stock: 1000 - price: 474 - sale_gamma: 30 - service_level: 0.95 - SKU247: - constraint: null - cost: 386 - init_stock: 210 - max_stock: 1000 - price: 497 - sale_gamma: 35 - service_level: 0.95 - SKU248: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 355 - init_stock: 174 - max_stock: 1000 - price: 539 - sale_gamma: 29 - service_level: 0.95 - SKU249: - constraint: null - cost: 356 - init_stock: 100 - max_stock: 1000 - price: 544 - sale_gamma: 25 - service_level: 0.95 - SKU25: - constraint: G(stock_constraint) - cost: 161 - init_stock: 35 - max_stock: 1000 - price: 249 - sale_gamma: 5 - service_level: 0.95 - SKU250: - constraint: null - cost: 127 - init_stock: 324 - max_stock: 1000 - price: 186 - sale_gamma: 54 - service_level: 0.95 - SKU251: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 344 - init_stock: 244 - max_stock: 1000 - price: 543 - sale_gamma: 61 - service_level: 0.95 - SKU252: - constraint: null - cost: 181 - init_stock: 415 - max_stock: 1000 - price: 334 - sale_gamma: 83 - service_level: 0.95 - SKU253: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 448 - init_stock: 32 - max_stock: 1000 - price: 864 - sale_gamma: 16 - service_level: 0.95 - SKU254: - constraint: null - cost: 484 - init_stock: 184 - max_stock: 1000 - price: 696 - sale_gamma: 46 - service_level: 0.95 - SKU255: - constraint: null - cost: 290 - init_stock: 335 - max_stock: 1000 - price: 568 - sale_gamma: 67 - service_level: 0.95 - SKU256: - constraint: null - cost: 91 - init_stock: 360 - max_stock: 1000 - price: 167 - sale_gamma: 72 - service_level: 0.95 - SKU257: - constraint: null - cost: 348 - init_stock: 171 - max_stock: 1000 - price: 497 - sale_gamma: 57 - service_level: 0.95 - SKU258: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 258 - max_stock: 1000 - price: 689 - sale_gamma: 43 - service_level: 0.95 - SKU259: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 207 - max_stock: 1000 - price: 559 - sale_gamma: 69 - service_level: 0.95 - SKU26: - constraint: null - cost: 229 - init_stock: 126 - max_stock: 1000 - price: 357 - sale_gamma: 63 - service_level: 0.95 - SKU260: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 364 - max_stock: 1000 - price: 866 - sale_gamma: 52 - service_level: 0.95 - SKU261: - constraint: null - cost: 368 - init_stock: 66 - max_stock: 1000 - price: 688 - sale_gamma: 22 - service_level: 0.95 - SKU262: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 332 - init_stock: 390 - max_stock: 1000 - price: 385 - sale_gamma: 78 - service_level: 0.95 - SKU263: - constraint: G(stock_constraint) - cost: 189 - init_stock: 444 - max_stock: 1000 - price: 372 - sale_gamma: 74 - service_level: 0.95 - SKU264: - constraint: null - cost: 361 - init_stock: 158 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU265: - constraint: null - cost: 286 - init_stock: 236 - max_stock: 1000 - price: 434 - sale_gamma: 59 - service_level: 0.95 - SKU266: - constraint: null - cost: 128 - init_stock: 282 - max_stock: 1000 - price: 172 - sale_gamma: 47 - service_level: 0.95 - SKU267: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 77 - init_stock: 320 - max_stock: 1000 - price: 130 - sale_gamma: 80 - service_level: 0.95 - SKU268: - constraint: null - cost: 221 - init_stock: 356 - max_stock: 1000 - price: 362 - sale_gamma: 89 - service_level: 0.95 - SKU269: - constraint: G(low_profit -> low_stock_constraint) - cost: 126 - init_stock: 132 - max_stock: 1000 - price: 175 - sale_gamma: 44 - service_level: 0.95 - SKU27: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 448 - max_stock: 1000 - price: 728 - sale_gamma: 56 - service_level: 0.95 - SKU270: - constraint: null - cost: 182 - init_stock: 370 - max_stock: 1000 - price: 263 - sale_gamma: 74 - service_level: 0.95 - SKU271: - constraint: G(stock_constraint) - cost: 230 - init_stock: 54 - max_stock: 1000 - price: 266 - sale_gamma: 18 - service_level: 0.95 - SKU272: - constraint: null - cost: 366 - init_stock: 51 - max_stock: 1000 - price: 625 - sale_gamma: 17 - service_level: 0.95 - SKU273: - constraint: null - cost: 421 - init_stock: 72 - max_stock: 1000 - price: 614 - sale_gamma: 18 - service_level: 0.95 - SKU274: - constraint: null - cost: 29 - init_stock: 308 - max_stock: 1000 - price: 42 - sale_gamma: 77 - service_level: 0.95 - SKU275: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 50 - init_stock: 144 - max_stock: 1000 - price: 76 - sale_gamma: 48 - service_level: 0.95 - SKU276: - constraint: G(stock_constraint) - cost: 163 - init_stock: 378 - max_stock: 1000 - price: 299 - sale_gamma: 54 - service_level: 0.95 - SKU277: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 82 - max_stock: 1000 - price: 507 - sale_gamma: 41 - service_level: 0.95 - SKU278: - constraint: null - cost: 105 - init_stock: 84 - max_stock: 1000 - price: 140 - sale_gamma: 12 - service_level: 0.95 - SKU279: - constraint: G(stock_constraint) - cost: 51 - init_stock: 273 - max_stock: 1000 - price: 61 - sale_gamma: 39 - service_level: 0.95 - SKU28: - constraint: G(stock_constraint) - cost: 208 - init_stock: 235 - max_stock: 1000 - price: 295 - sale_gamma: 47 - service_level: 0.95 - SKU280: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 198 - max_stock: 1000 - price: 436 - sale_gamma: 33 - service_level: 0.95 - SKU281: - constraint: null - cost: 395 - init_stock: 375 - max_stock: 1000 - price: 592 - sale_gamma: 75 - service_level: 0.95 - SKU282: - constraint: null - cost: 63 - init_stock: 184 - max_stock: 1000 - price: 112 - sale_gamma: 46 - service_level: 0.95 - SKU283: - constraint: null - cost: 392 - init_stock: 736 - max_stock: 1000 - price: 490 - sale_gamma: 92 - service_level: 0.95 - SKU284: - constraint: G(stock_constraint) - cost: 344 - init_stock: 268 - max_stock: 1000 - price: 643 - sale_gamma: 67 - service_level: 0.95 - SKU285: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 133 - init_stock: 546 - max_stock: 1000 - price: 148 - sale_gamma: 91 - service_level: 0.95 - SKU286: - constraint: null - cost: 337 - init_stock: 156 - max_stock: 1000 - price: 626 - sale_gamma: 39 - service_level: 0.95 - SKU287: - constraint: null - cost: 375 - init_stock: 224 - max_stock: 1000 - price: 660 - sale_gamma: 56 - service_level: 0.95 - SKU288: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 190 - max_stock: 1000 - price: 352 - sale_gamma: 38 - service_level: 0.95 - SKU289: - constraint: G(stock_constraint) - cost: 67 - init_stock: 62 - max_stock: 1000 - price: 91 - sale_gamma: 31 - service_level: 0.95 - SKU29: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 475 - sale_gamma: 58 - service_level: 0.95 - SKU290: - constraint: null - cost: 438 - init_stock: 268 - max_stock: 1000 - price: 779 - sale_gamma: 67 - service_level: 0.95 - SKU291: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 122 - max_stock: 1000 - price: 126 - sale_gamma: 61 - service_level: 0.95 - SKU292: - constraint: null - cost: 186 - init_stock: 15 - max_stock: 1000 - price: 344 - sale_gamma: 5 - service_level: 0.95 - SKU293: - constraint: G(stock_constraint) - cost: 162 - init_stock: 385 - max_stock: 1000 - price: 288 - sale_gamma: 55 - service_level: 0.95 - SKU294: - constraint: null - cost: 409 - init_stock: 45 - max_stock: 1000 - price: 605 - sale_gamma: 9 - service_level: 0.95 - SKU295: - constraint: null - cost: 435 - init_stock: 651 - max_stock: 1000 - price: 674 - sale_gamma: 93 - service_level: 0.95 - SKU296: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 552 - max_stock: 1000 - price: 580 - sale_gamma: 92 - service_level: 0.95 - SKU297: - constraint: null - cost: 298 - init_stock: 228 - max_stock: 1000 - price: 333 - sale_gamma: 38 - service_level: 0.95 - SKU298: - constraint: null - cost: 286 - init_stock: 140 - max_stock: 1000 - price: 500 - sale_gamma: 35 - service_level: 0.95 - SKU299: - constraint: G(stock_constraint) - cost: 367 - init_stock: 255 - max_stock: 1000 - price: 451 - sale_gamma: 51 - service_level: 0.95 - SKU3: - constraint: G(stock_constraint) - cost: 405 - init_stock: 48 - max_stock: 1000 - price: 733 - sale_gamma: 12 - service_level: 0.95 - SKU30: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 359 - init_stock: 276 - max_stock: 1000 - price: 581 - sale_gamma: 69 - service_level: 0.95 - SKU300: - constraint: G(low_profit -> low_stock_constraint) - cost: 376 - init_stock: 290 - max_stock: 1000 - price: 428 - sale_gamma: 58 - service_level: 0.95 - SKU301: - constraint: null - cost: 433 - init_stock: 581 - max_stock: 1000 - price: 857 - sale_gamma: 83 - service_level: 0.95 - SKU302: - constraint: null - cost: 184 - init_stock: 44 - max_stock: 1000 - price: 322 - sale_gamma: 11 - service_level: 0.95 - SKU303: - constraint: null - cost: 246 - init_stock: 188 - max_stock: 1000 - price: 487 - sale_gamma: 94 - service_level: 0.95 - SKU304: - constraint: G(low_profit -> low_stock_constraint) - cost: 419 - init_stock: 138 - max_stock: 1000 - price: 632 - sale_gamma: 23 - service_level: 0.95 - SKU305: - constraint: null - cost: 495 - init_stock: 500 - max_stock: 1000 - price: 772 - sale_gamma: 100 - service_level: 0.95 - SKU306: - constraint: null - cost: 479 - init_stock: 126 - max_stock: 1000 - price: 852 - sale_gamma: 42 - service_level: 0.95 - SKU307: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 210 - init_stock: 156 - max_stock: 1000 - price: 371 - sale_gamma: 78 - service_level: 0.95 - SKU308: - constraint: null - cost: 208 - init_stock: 25 - max_stock: 1000 - price: 262 - sale_gamma: 5 - service_level: 0.95 - SKU309: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 460 - max_stock: 1000 - price: 156 - sale_gamma: 92 - service_level: 0.95 - SKU31: - constraint: null - cost: 69 - init_stock: 280 - max_stock: 1000 - price: 120 - sale_gamma: 56 - service_level: 0.95 - SKU310: - constraint: null - cost: 234 - init_stock: 352 - max_stock: 1000 - price: 294 - sale_gamma: 44 - service_level: 0.95 - SKU311: - constraint: null - cost: 306 - init_stock: 400 - max_stock: 1000 - price: 437 - sale_gamma: 80 - service_level: 0.95 - SKU312: - constraint: G(stock_constraint) - cost: 291 - init_stock: 75 - max_stock: 1000 - price: 392 - sale_gamma: 25 - service_level: 0.95 - SKU313: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 324 - init_stock: 190 - max_stock: 1000 - price: 492 - sale_gamma: 38 - service_level: 0.95 - SKU314: - constraint: G(stock_constraint) - cost: 404 - init_stock: 116 - max_stock: 1000 - price: 456 - sale_gamma: 29 - service_level: 0.95 - SKU315: - constraint: G(stock_constraint) - cost: 471 - init_stock: 504 - max_stock: 1000 - price: 885 - sale_gamma: 84 - service_level: 0.95 - SKU316: - constraint: null - cost: 202 - init_stock: 90 - max_stock: 1000 - price: 282 - sale_gamma: 18 - service_level: 0.95 - SKU317: - constraint: null - cost: 216 - init_stock: 168 - max_stock: 1000 - price: 352 - sale_gamma: 24 - service_level: 0.95 - SKU318: - constraint: null - cost: 278 - init_stock: 255 - max_stock: 1000 - price: 350 - sale_gamma: 85 - service_level: 0.95 - SKU319: - constraint: null - cost: 257 - init_stock: 371 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU32: - constraint: null - cost: 336 - init_stock: 110 - max_stock: 1000 - price: 581 - sale_gamma: 22 - service_level: 0.95 - SKU320: - constraint: null - cost: 196 - init_stock: 156 - max_stock: 1000 - price: 258 - sale_gamma: 39 - service_level: 0.95 - SKU321: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 96 - max_stock: 1000 - price: 105 - sale_gamma: 16 - service_level: 0.95 - SKU322: - constraint: null - cost: 240 - init_stock: 600 - max_stock: 1000 - price: 453 - sale_gamma: 100 - service_level: 0.95 - SKU323: - constraint: G(stock_constraint) - cost: 66 - init_stock: 195 - max_stock: 1000 - price: 74 - sale_gamma: 39 - service_level: 0.95 - SKU324: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 442 - init_stock: 465 - max_stock: 1000 - price: 804 - sale_gamma: 93 - service_level: 0.95 - SKU325: - constraint: null - cost: 453 - init_stock: 345 - max_stock: 1000 - price: 579 - sale_gamma: 69 - service_level: 0.95 - SKU326: - constraint: null - cost: 345 - init_stock: 72 - max_stock: 1000 - price: 538 - sale_gamma: 24 - service_level: 0.95 - SKU327: - constraint: G(low_profit -> low_stock_constraint) - cost: 457 - init_stock: 84 - max_stock: 1000 - price: 749 - sale_gamma: 14 - service_level: 0.95 - SKU328: - constraint: null - cost: 390 - init_stock: 90 - max_stock: 1000 - price: 487 - sale_gamma: 45 - service_level: 0.95 - SKU329: - constraint: null - cost: 67 - init_stock: 84 - max_stock: 1000 - price: 126 - sale_gamma: 42 - service_level: 0.95 - SKU33: - constraint: G(low_profit -> low_stock_constraint) - cost: 416 - init_stock: 552 - max_stock: 1000 - price: 465 - sale_gamma: 92 - service_level: 0.95 - SKU330: - constraint: null - cost: 306 - init_stock: 273 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU331: - constraint: G(low_profit -> low_stock_constraint) - cost: 138 - init_stock: 164 - max_stock: 1000 - price: 180 - sale_gamma: 41 - service_level: 0.95 - SKU332: - constraint: null - cost: 390 - init_stock: 288 - max_stock: 1000 - price: 670 - sale_gamma: 96 - service_level: 0.95 - SKU333: - constraint: G(stock_constraint) - cost: 485 - init_stock: 318 - max_stock: 1000 - price: 800 - sale_gamma: 53 - service_level: 0.95 - SKU334: - constraint: null - cost: 183 - init_stock: 114 - max_stock: 1000 - price: 245 - sale_gamma: 57 - service_level: 0.95 - SKU335: - constraint: null - cost: 80 - init_stock: 243 - max_stock: 1000 - price: 141 - sale_gamma: 81 - service_level: 0.95 - SKU336: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 56 - max_stock: 1000 - price: 565 - sale_gamma: 28 - service_level: 0.95 - SKU337: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 394 - sale_gamma: 29 - service_level: 0.95 - SKU338: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 150 - sale_gamma: 81 - service_level: 0.95 - SKU339: - constraint: G(stock_constraint) - cost: 265 - init_stock: 441 - max_stock: 1000 - price: 368 - sale_gamma: 63 - service_level: 0.95 - SKU34: - constraint: null - cost: 95 - init_stock: 91 - max_stock: 1000 - price: 135 - sale_gamma: 13 - service_level: 0.95 - SKU340: - constraint: null - cost: 480 - init_stock: 435 - max_stock: 1000 - price: 633 - sale_gamma: 87 - service_level: 0.95 - SKU341: - constraint: G(low_profit -> low_stock_constraint) - cost: 462 - init_stock: 280 - max_stock: 1000 - price: 924 - sale_gamma: 70 - service_level: 0.95 - SKU342: - constraint: null - cost: 144 - init_stock: 72 - max_stock: 1000 - price: 216 - sale_gamma: 9 - service_level: 0.95 - SKU343: - constraint: null - cost: 310 - init_stock: 90 - max_stock: 1000 - price: 589 - sale_gamma: 15 - service_level: 0.95 - SKU344: - constraint: null - cost: 357 - init_stock: 348 - max_stock: 1000 - price: 442 - sale_gamma: 87 - service_level: 0.95 - SKU345: - constraint: null - cost: 442 - init_stock: 267 - max_stock: 1000 - price: 857 - sale_gamma: 89 - service_level: 0.95 - SKU346: - constraint: G(stock_constraint) - cost: 350 - init_stock: 336 - max_stock: 1000 - price: 549 - sale_gamma: 42 - service_level: 0.95 - SKU347: - constraint: G(low_profit -> low_stock_constraint) - cost: 497 - init_stock: 328 - max_stock: 1000 - price: 810 - sale_gamma: 82 - service_level: 0.95 - SKU348: - constraint: G(stock_constraint) - cost: 147 - init_stock: 100 - max_stock: 1000 - price: 277 - sale_gamma: 20 - service_level: 0.95 - SKU349: - constraint: null - cost: 67 - init_stock: 335 - max_stock: 1000 - price: 116 - sale_gamma: 67 - service_level: 0.95 - SKU35: - constraint: null - cost: 267 - init_stock: 430 - max_stock: 1000 - price: 373 - sale_gamma: 86 - service_level: 0.95 - SKU350: - constraint: G(stock_constraint) - cost: 279 - init_stock: 552 - max_stock: 1000 - price: 460 - sale_gamma: 92 - service_level: 0.95 - SKU351: - constraint: null - cost: 155 - init_stock: 335 - max_stock: 1000 - price: 294 - sale_gamma: 67 - service_level: 0.95 - SKU352: - constraint: null - cost: 71 - init_stock: 54 - max_stock: 1000 - price: 100 - sale_gamma: 18 - service_level: 0.95 - SKU353: - constraint: G(stock_constraint) - cost: 253 - init_stock: 465 - max_stock: 1000 - price: 437 - sale_gamma: 93 - service_level: 0.95 - SKU354: - constraint: G(low_profit -> low_stock_constraint) - cost: 396 - init_stock: 395 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU355: - constraint: G(stock_constraint) - cost: 63 - init_stock: 30 - max_stock: 1000 - price: 74 - sale_gamma: 10 - service_level: 0.95 - SKU356: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 87 - max_stock: 1000 - price: 441 - sale_gamma: 29 - service_level: 0.95 - SKU357: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 499 - init_stock: 460 - max_stock: 1000 - price: 868 - sale_gamma: 92 - service_level: 0.95 - SKU358: - constraint: null - cost: 269 - init_stock: 414 - max_stock: 1000 - price: 408 - sale_gamma: 69 - service_level: 0.95 - SKU359: - constraint: G(low_profit -> low_stock_constraint) - cost: 82 - init_stock: 600 - max_stock: 1000 - price: 110 - sale_gamma: 75 - service_level: 0.95 - SKU36: - constraint: null - cost: 105 - init_stock: 30 - max_stock: 1000 - price: 165 - sale_gamma: 10 - service_level: 0.95 - SKU360: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 75 - max_stock: 1000 - price: 682 - sale_gamma: 25 - service_level: 0.95 - SKU361: - constraint: null - cost: 163 - init_stock: 360 - max_stock: 1000 - price: 288 - sale_gamma: 90 - service_level: 0.95 - SKU362: - constraint: null - cost: 464 - init_stock: 435 - max_stock: 1000 - price: 867 - sale_gamma: 87 - service_level: 0.95 - SKU363: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 390 - max_stock: 1000 - price: 93 - sale_gamma: 78 - service_level: 0.95 - SKU364: - constraint: G(stock_constraint) - cost: 387 - init_stock: 66 - max_stock: 1000 - price: 472 - sale_gamma: 33 - service_level: 0.95 - SKU365: - constraint: null - cost: 158 - init_stock: 581 - max_stock: 1000 - price: 241 - sale_gamma: 83 - service_level: 0.95 - SKU366: - constraint: null - cost: 444 - init_stock: 344 - max_stock: 1000 - price: 639 - sale_gamma: 86 - service_level: 0.95 - SKU367: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 322 - max_stock: 1000 - price: 304 - sale_gamma: 46 - service_level: 0.95 - SKU368: - constraint: G(stock_constraint) - cost: 472 - init_stock: 198 - max_stock: 1000 - price: 613 - sale_gamma: 33 - service_level: 0.95 - SKU369: - constraint: null - cost: 96 - init_stock: 375 - max_stock: 1000 - price: 155 - sale_gamma: 75 - service_level: 0.95 - SKU37: - constraint: G(low_profit -> low_stock_constraint) - cost: 66 - init_stock: 177 - max_stock: 1000 - price: 110 - sale_gamma: 59 - service_level: 0.95 - SKU370: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 90 - max_stock: 1000 - price: 141 - sale_gamma: 15 - service_level: 0.95 - SKU371: - constraint: null - cost: 328 - init_stock: 25 - max_stock: 1000 - price: 511 - sale_gamma: 5 - service_level: 0.95 - SKU372: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 67 - init_stock: 192 - max_stock: 1000 - price: 134 - sale_gamma: 32 - service_level: 0.95 - SKU373: - constraint: null - cost: 58 - init_stock: 268 - max_stock: 1000 - price: 91 - sale_gamma: 67 - service_level: 0.95 - SKU374: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 216 - max_stock: 1000 - price: 73 - sale_gamma: 54 - service_level: 0.95 - SKU375: - constraint: G(low_profit -> low_stock_constraint) - cost: 283 - init_stock: 432 - max_stock: 1000 - price: 416 - sale_gamma: 72 - service_level: 0.95 - SKU376: - constraint: null - cost: 156 - init_stock: 164 - max_stock: 1000 - price: 291 - sale_gamma: 82 - service_level: 0.95 - SKU377: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 78 - init_stock: 536 - max_stock: 1000 - price: 134 - sale_gamma: 67 - service_level: 0.95 - SKU378: - constraint: G(low_profit -> low_stock_constraint) - cost: 424 - init_stock: 175 - max_stock: 1000 - price: 546 - sale_gamma: 35 - service_level: 0.95 - SKU379: - constraint: null - cost: 11 - init_stock: 196 - max_stock: 1000 - price: 20 - sale_gamma: 49 - service_level: 0.95 - SKU38: - constraint: null - cost: 344 - init_stock: 258 - max_stock: 1000 - price: 567 - sale_gamma: 43 - service_level: 0.95 - SKU380: - constraint: null - cost: 393 - init_stock: 212 - max_stock: 1000 - price: 605 - sale_gamma: 53 - service_level: 0.95 - SKU381: - constraint: null - cost: 476 - init_stock: 18 - max_stock: 1000 - price: 609 - sale_gamma: 6 - service_level: 0.95 - SKU382: - constraint: null - cost: 125 - init_stock: 426 - max_stock: 1000 - price: 177 - sale_gamma: 71 - service_level: 0.95 - SKU383: - constraint: null - cost: 304 - init_stock: 368 - max_stock: 1000 - price: 425 - sale_gamma: 92 - service_level: 0.95 - SKU384: - constraint: null - cost: 320 - init_stock: 54 - max_stock: 1000 - price: 460 - sale_gamma: 9 - service_level: 0.95 - SKU385: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 120 - init_stock: 91 - max_stock: 1000 - price: 184 - sale_gamma: 13 - service_level: 0.95 - SKU386: - constraint: G(stock_constraint) - cost: 295 - init_stock: 124 - max_stock: 1000 - price: 536 - sale_gamma: 31 - service_level: 0.95 - SKU387: - constraint: null - cost: 112 - init_stock: 582 - max_stock: 1000 - price: 154 - sale_gamma: 97 - service_level: 0.95 - SKU388: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 405 - init_stock: 264 - max_stock: 1000 - price: 635 - sale_gamma: 44 - service_level: 0.95 - SKU389: - constraint: G(stock_constraint) - cost: 376 - init_stock: 490 - max_stock: 1000 - price: 699 - sale_gamma: 70 - service_level: 0.95 - SKU39: - constraint: G(low_profit -> low_stock_constraint) - cost: 25 - init_stock: 204 - max_stock: 1000 - price: 45 - sale_gamma: 51 - service_level: 0.95 - SKU390: - constraint: null - cost: 144 - init_stock: 156 - max_stock: 1000 - price: 223 - sale_gamma: 39 - service_level: 0.95 - SKU391: - constraint: G(stock_constraint) - cost: 233 - init_stock: 402 - max_stock: 1000 - price: 403 - sale_gamma: 67 - service_level: 0.95 - SKU392: - constraint: null - cost: 163 - init_stock: 370 - max_stock: 1000 - price: 205 - sale_gamma: 74 - service_level: 0.95 - SKU393: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 402 - max_stock: 1000 - price: 701 - sale_gamma: 67 - service_level: 0.95 - SKU394: - constraint: null - cost: 154 - init_stock: 318 - max_stock: 1000 - price: 252 - sale_gamma: 53 - service_level: 0.95 - SKU395: - constraint: null - cost: 488 - init_stock: 198 - max_stock: 1000 - price: 854 - sale_gamma: 33 - service_level: 0.95 - SKU396: - constraint: null - cost: 333 - init_stock: 427 - max_stock: 1000 - price: 469 - sale_gamma: 61 - service_level: 0.95 - SKU397: - constraint: null - cost: 460 - init_stock: 153 - max_stock: 1000 - price: 791 - sale_gamma: 51 - service_level: 0.95 - SKU398: - constraint: G(stock_constraint) - cost: 234 - init_stock: 174 - max_stock: 1000 - price: 414 - sale_gamma: 58 - service_level: 0.95 - SKU399: - constraint: G(stock_constraint) - cost: 376 - init_stock: 148 - max_stock: 1000 - price: 612 - sale_gamma: 37 - service_level: 0.95 - SKU4: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 21 - max_stock: 1000 - price: 798 - sale_gamma: 7 - service_level: 0.95 - SKU40: - constraint: null - cost: 490 - init_stock: 594 - max_stock: 1000 - price: 563 - sale_gamma: 99 - service_level: 0.95 - SKU400: - constraint: G(low_profit -> low_stock_constraint) - cost: 445 - init_stock: 420 - max_stock: 1000 - price: 729 - sale_gamma: 84 - service_level: 0.95 - SKU401: - constraint: G(low_profit -> low_stock_constraint) - cost: 410 - init_stock: 312 - max_stock: 1000 - price: 774 - sale_gamma: 78 - service_level: 0.95 - SKU402: - constraint: G(low_profit -> low_stock_constraint) - cost: 86 - init_stock: 35 - max_stock: 1000 - price: 153 - sale_gamma: 7 - service_level: 0.95 - SKU403: - constraint: null - cost: 89 - init_stock: 594 - max_stock: 1000 - price: 133 - sale_gamma: 99 - service_level: 0.95 - SKU404: - constraint: null - cost: 287 - init_stock: 305 - max_stock: 1000 - price: 522 - sale_gamma: 61 - service_level: 0.95 - SKU405: - constraint: G(stock_constraint) - cost: 460 - init_stock: 114 - max_stock: 1000 - price: 584 - sale_gamma: 19 - service_level: 0.95 - SKU406: - constraint: null - cost: 327 - init_stock: 400 - max_stock: 1000 - price: 405 - sale_gamma: 100 - service_level: 0.95 - SKU407: - constraint: null - cost: 26 - init_stock: 184 - max_stock: 1000 - price: 41 - sale_gamma: 46 - service_level: 0.95 - SKU408: - constraint: null - cost: 444 - init_stock: 48 - max_stock: 1000 - price: 754 - sale_gamma: 8 - service_level: 0.95 - SKU409: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 257 - init_stock: 273 - max_stock: 1000 - price: 449 - sale_gamma: 91 - service_level: 0.95 - SKU41: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 141 - init_stock: 553 - max_stock: 1000 - price: 162 - sale_gamma: 79 - service_level: 0.95 - SKU410: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 70 - init_stock: 32 - max_stock: 1000 - price: 88 - sale_gamma: 16 - service_level: 0.95 - SKU411: - constraint: G(stock_constraint) - cost: 210 - init_stock: 380 - max_stock: 1000 - price: 405 - sale_gamma: 95 - service_level: 0.95 - SKU412: - constraint: null - cost: 286 - init_stock: 248 - max_stock: 1000 - price: 414 - sale_gamma: 62 - service_level: 0.95 - SKU413: - constraint: null - cost: 403 - init_stock: 581 - max_stock: 1000 - price: 801 - sale_gamma: 83 - service_level: 0.95 - SKU414: - constraint: G(stock_constraint) - cost: 165 - init_stock: 435 - max_stock: 1000 - price: 229 - sale_gamma: 87 - service_level: 0.95 - SKU415: - constraint: G(stock_constraint) - cost: 291 - init_stock: 184 - max_stock: 1000 - price: 372 - sale_gamma: 23 - service_level: 0.95 - SKU416: - constraint: null - cost: 228 - init_stock: 36 - max_stock: 1000 - price: 373 - sale_gamma: 9 - service_level: 0.95 - SKU417: - constraint: G(low_profit -> low_stock_constraint) - cost: 443 - init_stock: 288 - max_stock: 1000 - price: 872 - sale_gamma: 72 - service_level: 0.95 - SKU418: - constraint: null - cost: 458 - init_stock: 52 - max_stock: 1000 - price: 838 - sale_gamma: 13 - service_level: 0.95 - SKU419: - constraint: null - cost: 303 - init_stock: 712 - max_stock: 1000 - price: 448 - sale_gamma: 89 - service_level: 0.95 - SKU42: - constraint: null - cost: 462 - init_stock: 156 - max_stock: 1000 - price: 688 - sale_gamma: 26 - service_level: 0.95 - SKU420: - constraint: null - cost: 268 - init_stock: 84 - max_stock: 1000 - price: 506 - sale_gamma: 42 - service_level: 0.95 - SKU421: - constraint: G(stock_constraint) - cost: 214 - init_stock: 138 - max_stock: 1000 - price: 314 - sale_gamma: 46 - service_level: 0.95 - SKU422: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 432 - max_stock: 1000 - price: 97 - sale_gamma: 54 - service_level: 0.95 - SKU423: - constraint: G(low_profit -> low_stock_constraint) - cost: 188 - init_stock: 396 - max_stock: 1000 - price: 265 - sale_gamma: 66 - service_level: 0.95 - SKU424: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 66 - max_stock: 1000 - price: 317 - sale_gamma: 11 - service_level: 0.95 - SKU425: - constraint: G(stock_constraint) - cost: 162 - init_stock: 72 - max_stock: 1000 - price: 277 - sale_gamma: 12 - service_level: 0.95 - SKU426: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 125 - init_stock: 588 - max_stock: 1000 - price: 246 - sale_gamma: 98 - service_level: 0.95 - SKU427: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 413 - init_stock: 282 - max_stock: 1000 - price: 710 - sale_gamma: 94 - service_level: 0.95 - SKU428: - constraint: G(stock_constraint) - cost: 391 - init_stock: 378 - max_stock: 1000 - price: 629 - sale_gamma: 63 - service_level: 0.95 - SKU429: - constraint: G(stock_constraint) - cost: 39 - init_stock: 246 - max_stock: 1000 - price: 73 - sale_gamma: 41 - service_level: 0.95 - SKU43: - constraint: G(stock_constraint) - cost: 217 - init_stock: 272 - max_stock: 1000 - price: 353 - sale_gamma: 68 - service_level: 0.95 - SKU430: - constraint: null - cost: 216 - init_stock: 511 - max_stock: 1000 - price: 313 - sale_gamma: 73 - service_level: 0.95 - SKU431: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 328 - init_stock: 126 - max_stock: 1000 - price: 646 - sale_gamma: 21 - service_level: 0.95 - SKU432: - constraint: G(stock_constraint) - cost: 25 - init_stock: 368 - max_stock: 1000 - price: 35 - sale_gamma: 46 - service_level: 0.95 - SKU433: - constraint: null - cost: 348 - init_stock: 270 - max_stock: 1000 - price: 396 - sale_gamma: 45 - service_level: 0.95 - SKU434: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 90 - max_stock: 1000 - price: 40 - sale_gamma: 30 - service_level: 0.95 - SKU435: - constraint: G(stock_constraint) - cost: 272 - init_stock: 210 - max_stock: 1000 - price: 320 - sale_gamma: 30 - service_level: 0.95 - SKU436: - constraint: null - cost: 72 - init_stock: 405 - max_stock: 1000 - price: 105 - sale_gamma: 81 - service_level: 0.95 - SKU437: - constraint: G(stock_constraint) - cost: 115 - init_stock: 84 - max_stock: 1000 - price: 223 - sale_gamma: 14 - service_level: 0.95 - SKU438: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 40 - max_stock: 1000 - price: 190 - sale_gamma: 10 - service_level: 0.95 - SKU439: - constraint: G(stock_constraint) - cost: 256 - init_stock: 190 - max_stock: 1000 - price: 304 - sale_gamma: 38 - service_level: 0.95 - SKU44: - constraint: null - cost: 360 - init_stock: 240 - max_stock: 1000 - price: 669 - sale_gamma: 48 - service_level: 0.95 - SKU440: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 248 - init_stock: 246 - max_stock: 1000 - price: 357 - sale_gamma: 82 - service_level: 0.95 - SKU441: - constraint: null - cost: 253 - init_stock: 224 - max_stock: 1000 - price: 374 - sale_gamma: 56 - service_level: 0.95 - SKU442: - constraint: null - cost: 470 - init_stock: 352 - max_stock: 1000 - price: 629 - sale_gamma: 88 - service_level: 0.95 - SKU443: - constraint: null - cost: 218 - init_stock: 365 - max_stock: 1000 - price: 361 - sale_gamma: 73 - service_level: 0.95 - SKU444: - constraint: null - cost: 156 - init_stock: 18 - max_stock: 1000 - price: 182 - sale_gamma: 6 - service_level: 0.95 - SKU445: - constraint: null - cost: 260 - init_stock: 602 - max_stock: 1000 - price: 296 - sale_gamma: 86 - service_level: 0.95 - SKU446: - constraint: null - cost: 497 - init_stock: 147 - max_stock: 1000 - price: 690 - sale_gamma: 49 - service_level: 0.95 - SKU447: - constraint: null - cost: 196 - init_stock: 30 - max_stock: 1000 - price: 280 - sale_gamma: 5 - service_level: 0.95 - SKU448: - constraint: null - cost: 485 - init_stock: 497 - max_stock: 1000 - price: 742 - sale_gamma: 71 - service_level: 0.95 - SKU449: - constraint: null - cost: 282 - init_stock: 114 - max_stock: 1000 - price: 360 - sale_gamma: 38 - service_level: 0.95 - SKU45: - constraint: G(stock_constraint) - cost: 253 - init_stock: 30 - max_stock: 1000 - price: 351 - sale_gamma: 10 - service_level: 0.95 - SKU450: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 58 - init_stock: 294 - max_stock: 1000 - price: 77 - sale_gamma: 98 - service_level: 0.95 - SKU451: - constraint: null - cost: 468 - init_stock: 483 - max_stock: 1000 - price: 851 - sale_gamma: 69 - service_level: 0.95 - SKU452: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 63 - init_stock: 395 - max_stock: 1000 - price: 97 - sale_gamma: 79 - service_level: 0.95 - SKU453: - constraint: null - cost: 37 - init_stock: 140 - max_stock: 1000 - price: 65 - sale_gamma: 35 - service_level: 0.95 - SKU454: - constraint: G(low_profit -> low_stock_constraint) - cost: 494 - init_stock: 340 - max_stock: 1000 - price: 899 - sale_gamma: 85 - service_level: 0.95 - SKU455: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 136 - init_stock: 182 - max_stock: 1000 - price: 195 - sale_gamma: 26 - service_level: 0.95 - SKU456: - constraint: G(stock_constraint) - cost: 338 - init_stock: 75 - max_stock: 1000 - price: 659 - sale_gamma: 25 - service_level: 0.95 - SKU457: - constraint: null - cost: 169 - init_stock: 256 - max_stock: 1000 - price: 190 - sale_gamma: 64 - service_level: 0.95 - SKU458: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 403 - init_stock: 28 - max_stock: 1000 - price: 685 - sale_gamma: 7 - service_level: 0.95 - SKU459: - constraint: null - cost: 425 - init_stock: 272 - max_stock: 1000 - price: 731 - sale_gamma: 34 - service_level: 0.95 - SKU46: - constraint: G(low_profit -> low_stock_constraint) - cost: 299 - init_stock: 200 - max_stock: 1000 - price: 409 - sale_gamma: 50 - service_level: 0.95 - SKU460: - constraint: G(low_profit -> low_stock_constraint) - cost: 405 - init_stock: 292 - max_stock: 1000 - price: 558 - sale_gamma: 73 - service_level: 0.95 - SKU461: - constraint: G(stock_constraint) - cost: 251 - init_stock: 240 - max_stock: 1000 - price: 326 - sale_gamma: 48 - service_level: 0.95 - SKU462: - constraint: G(low_profit -> low_stock_constraint) - cost: 448 - init_stock: 36 - max_stock: 1000 - price: 757 - sale_gamma: 9 - service_level: 0.95 - SKU463: - constraint: null - cost: 187 - init_stock: 574 - max_stock: 1000 - price: 213 - sale_gamma: 82 - service_level: 0.95 - SKU464: - constraint: null - cost: 279 - init_stock: 272 - max_stock: 1000 - price: 438 - sale_gamma: 34 - service_level: 0.95 - SKU465: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 147 - init_stock: 400 - max_stock: 1000 - price: 288 - sale_gamma: 100 - service_level: 0.95 - SKU466: - constraint: G(stock_constraint) - cost: 97 - init_stock: 126 - max_stock: 1000 - price: 167 - sale_gamma: 42 - service_level: 0.95 - SKU467: - constraint: G(stock_constraint) - cost: 142 - init_stock: 252 - max_stock: 1000 - price: 230 - sale_gamma: 36 - service_level: 0.95 - SKU468: - constraint: G(stock_constraint) - cost: 55 - init_stock: 250 - max_stock: 1000 - price: 104 - sale_gamma: 50 - service_level: 0.95 - SKU469: - constraint: G(stock_constraint) - cost: 178 - init_stock: 105 - max_stock: 1000 - price: 331 - sale_gamma: 15 - service_level: 0.95 - SKU47: - constraint: G(stock_constraint) - cost: 121 - init_stock: 117 - max_stock: 1000 - price: 240 - sale_gamma: 39 - service_level: 0.95 - SKU470: - constraint: G(low_profit -> low_stock_constraint) - cost: 373 - init_stock: 128 - max_stock: 1000 - price: 548 - sale_gamma: 32 - service_level: 0.95 - SKU471: - constraint: G(low_profit -> low_stock_constraint) - cost: 315 - init_stock: 568 - max_stock: 1000 - price: 387 - sale_gamma: 71 - service_level: 0.95 - SKU472: - constraint: null - cost: 421 - init_stock: 177 - max_stock: 1000 - price: 627 - sale_gamma: 59 - service_level: 0.95 - SKU473: - constraint: G(low_profit -> low_stock_constraint) - cost: 195 - init_stock: 105 - max_stock: 1000 - price: 323 - sale_gamma: 21 - service_level: 0.95 - SKU474: - constraint: null - cost: 448 - init_stock: 602 - max_stock: 1000 - price: 775 - sale_gamma: 86 - service_level: 0.95 - SKU475: - constraint: null - cost: 34 - init_stock: 282 - max_stock: 1000 - price: 62 - sale_gamma: 94 - service_level: 0.95 - SKU476: - constraint: null - cost: 251 - init_stock: 180 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU477: - constraint: null - cost: 289 - init_stock: 329 - max_stock: 1000 - price: 338 - sale_gamma: 47 - service_level: 0.95 - SKU478: - constraint: null - cost: 479 - init_stock: 352 - max_stock: 1000 - price: 723 - sale_gamma: 88 - service_level: 0.95 - SKU479: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 366 - init_stock: 152 - max_stock: 1000 - price: 552 - sale_gamma: 38 - service_level: 0.95 - SKU48: - constraint: null - cost: 340 - init_stock: 232 - max_stock: 1000 - price: 387 - sale_gamma: 58 - service_level: 0.95 - SKU480: - constraint: G(stock_constraint) - cost: 18 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 30 - service_level: 0.95 - SKU481: - constraint: null - cost: 330 - init_stock: 352 - max_stock: 1000 - price: 422 - sale_gamma: 88 - service_level: 0.95 - SKU482: - constraint: G(stock_constraint) - cost: 94 - init_stock: 696 - max_stock: 1000 - price: 108 - sale_gamma: 87 - service_level: 0.95 - SKU483: - constraint: null - cost: 298 - init_stock: 170 - max_stock: 1000 - price: 533 - sale_gamma: 34 - service_level: 0.95 - SKU484: - constraint: null - cost: 84 - init_stock: 365 - max_stock: 1000 - price: 117 - sale_gamma: 73 - service_level: 0.95 - SKU485: - constraint: null - cost: 318 - init_stock: 536 - max_stock: 1000 - price: 543 - sale_gamma: 67 - service_level: 0.95 - SKU486: - constraint: G(low_profit -> low_stock_constraint) - cost: 122 - init_stock: 208 - max_stock: 1000 - price: 161 - sale_gamma: 52 - service_level: 0.95 - SKU487: - constraint: null - cost: 277 - init_stock: 264 - max_stock: 1000 - price: 362 - sale_gamma: 66 - service_level: 0.95 - SKU488: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 189 - max_stock: 1000 - price: 42 - sale_gamma: 27 - service_level: 0.95 - SKU489: - constraint: null - cost: 59 - init_stock: 124 - max_stock: 1000 - price: 97 - sale_gamma: 31 - service_level: 0.95 - SKU49: - constraint: null - cost: 263 - init_stock: 760 - max_stock: 1000 - price: 515 - sale_gamma: 95 - service_level: 0.95 - SKU490: - constraint: null - cost: 161 - init_stock: 486 - max_stock: 1000 - price: 264 - sale_gamma: 81 - service_level: 0.95 - SKU491: - constraint: null - cost: 444 - init_stock: 450 - max_stock: 1000 - price: 834 - sale_gamma: 75 - service_level: 0.95 - SKU492: - constraint: null - cost: 53 - init_stock: 240 - max_stock: 1000 - price: 63 - sale_gamma: 80 - service_level: 0.95 - SKU493: - constraint: null - cost: 461 - init_stock: 81 - max_stock: 1000 - price: 567 - sale_gamma: 27 - service_level: 0.95 - SKU494: - constraint: null - cost: 145 - init_stock: 282 - max_stock: 1000 - price: 242 - sale_gamma: 94 - service_level: 0.95 - SKU495: - constraint: G(stock_constraint) - cost: 149 - init_stock: 372 - max_stock: 1000 - price: 217 - sale_gamma: 62 - service_level: 0.95 - SKU496: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 342 - init_stock: 295 - max_stock: 1000 - price: 492 - sale_gamma: 59 - service_level: 0.95 - SKU497: - constraint: G(low_profit -> low_stock_constraint) - cost: 33 - init_stock: 45 - max_stock: 1000 - price: 42 - sale_gamma: 9 - service_level: 0.95 - SKU498: - constraint: null - cost: 305 - init_stock: 325 - max_stock: 1000 - price: 439 - sale_gamma: 65 - service_level: 0.95 - SKU499: - constraint: G(stock_constraint) - cost: 307 - init_stock: 174 - max_stock: 1000 - price: 472 - sale_gamma: 29 - service_level: 0.95 - SKU5: - constraint: G(stock_constraint) - cost: 466 - init_stock: 426 - max_stock: 1000 - price: 852 - sale_gamma: 71 - service_level: 0.95 - SKU50: - constraint: null - cost: 282 - init_stock: 185 - max_stock: 1000 - price: 516 - sale_gamma: 37 - service_level: 0.95 - SKU500: - constraint: null - cost: 208 - init_stock: 336 - max_stock: 1000 - price: 255 - sale_gamma: 42 - service_level: 0.95 - SKU501: - constraint: null - cost: 194 - init_stock: 152 - max_stock: 1000 - price: 265 - sale_gamma: 76 - service_level: 0.95 - SKU502: - constraint: null - cost: 69 - init_stock: 390 - max_stock: 1000 - price: 101 - sale_gamma: 65 - service_level: 0.95 - SKU503: - constraint: G(stock_constraint) - cost: 208 - init_stock: 228 - max_stock: 1000 - price: 280 - sale_gamma: 57 - service_level: 0.95 - SKU504: - constraint: null - cost: 251 - init_stock: 210 - max_stock: 1000 - price: 426 - sale_gamma: 30 - service_level: 0.95 - SKU505: - constraint: null - cost: 426 - init_stock: 295 - max_stock: 1000 - price: 515 - sale_gamma: 59 - service_level: 0.95 - SKU506: - constraint: null - cost: 149 - init_stock: 465 - max_stock: 1000 - price: 220 - sale_gamma: 93 - service_level: 0.95 - SKU507: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 187 - init_stock: 60 - max_stock: 1000 - price: 293 - sale_gamma: 15 - service_level: 0.95 - SKU508: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 576 - max_stock: 1000 - price: 432 - sale_gamma: 96 - service_level: 0.95 - SKU509: - constraint: G(low_profit -> low_stock_constraint) - cost: 297 - init_stock: 486 - max_stock: 1000 - price: 397 - sale_gamma: 81 - service_level: 0.95 - SKU51: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 469 - max_stock: 1000 - price: 477 - sale_gamma: 67 - service_level: 0.95 - SKU510: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 36 - max_stock: 1000 - price: 156 - sale_gamma: 9 - service_level: 0.95 - SKU511: - constraint: G(stock_constraint) - cost: 361 - init_stock: 450 - max_stock: 1000 - price: 480 - sale_gamma: 75 - service_level: 0.95 - SKU512: - constraint: null - cost: 467 - init_stock: 132 - max_stock: 1000 - price: 854 - sale_gamma: 22 - service_level: 0.95 - SKU513: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 343 - init_stock: 42 - max_stock: 1000 - price: 631 - sale_gamma: 7 - service_level: 0.95 - SKU514: - constraint: null - cost: 186 - init_stock: 504 - max_stock: 1000 - price: 332 - sale_gamma: 63 - service_level: 0.95 - SKU515: - constraint: null - cost: 145 - init_stock: 216 - max_stock: 1000 - price: 227 - sale_gamma: 54 - service_level: 0.95 - SKU516: - constraint: G(stock_constraint) - cost: 64 - init_stock: 114 - max_stock: 1000 - price: 96 - sale_gamma: 38 - service_level: 0.95 - SKU517: - constraint: null - cost: 117 - init_stock: 45 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU518: - constraint: G(stock_constraint) - cost: 26 - init_stock: 147 - max_stock: 1000 - price: 30 - sale_gamma: 21 - service_level: 0.95 - SKU519: - constraint: null - cost: 233 - init_stock: 250 - max_stock: 1000 - price: 410 - sale_gamma: 50 - service_level: 0.95 - SKU52: - constraint: null - cost: 22 - init_stock: 402 - max_stock: 1000 - price: 28 - sale_gamma: 67 - service_level: 0.95 - SKU520: - constraint: G(low_profit -> low_stock_constraint) - cost: 209 - init_stock: 44 - max_stock: 1000 - price: 248 - sale_gamma: 22 - service_level: 0.95 - SKU521: - constraint: G(low_profit -> low_stock_constraint) - cost: 220 - init_stock: 350 - max_stock: 1000 - price: 330 - sale_gamma: 50 - service_level: 0.95 - SKU522: - constraint: null - cost: 156 - init_stock: 522 - max_stock: 1000 - price: 277 - sale_gamma: 87 - service_level: 0.95 - SKU523: - constraint: null - cost: 65 - init_stock: 500 - max_stock: 1000 - price: 107 - sale_gamma: 100 - service_level: 0.95 - SKU524: - constraint: null - cost: 294 - init_stock: 188 - max_stock: 1000 - price: 464 - sale_gamma: 94 - service_level: 0.95 - SKU525: - constraint: G(stock_constraint) - cost: 432 - init_stock: 126 - max_stock: 1000 - price: 751 - sale_gamma: 42 - service_level: 0.95 - SKU526: - constraint: null - cost: 419 - init_stock: 168 - max_stock: 1000 - price: 716 - sale_gamma: 24 - service_level: 0.95 - SKU527: - constraint: null - cost: 131 - init_stock: 350 - max_stock: 1000 - price: 262 - sale_gamma: 70 - service_level: 0.95 - SKU528: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 84 - max_stock: 1000 - price: 581 - sale_gamma: 21 - service_level: 0.95 - SKU529: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 248 - max_stock: 1000 - price: 375 - sale_gamma: 31 - service_level: 0.95 - SKU53: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 151 - init_stock: 490 - max_stock: 1000 - price: 191 - sale_gamma: 70 - service_level: 0.95 - SKU530: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 135 - max_stock: 1000 - price: 205 - sale_gamma: 45 - service_level: 0.95 - SKU531: - constraint: null - cost: 103 - init_stock: 300 - max_stock: 1000 - price: 153 - sale_gamma: 60 - service_level: 0.95 - SKU532: - constraint: null - cost: 351 - init_stock: 539 - max_stock: 1000 - price: 431 - sale_gamma: 77 - service_level: 0.95 - SKU533: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 168 - max_stock: 1000 - price: 340 - sale_gamma: 42 - service_level: 0.95 - SKU534: - constraint: null - cost: 425 - init_stock: 82 - max_stock: 1000 - price: 548 - sale_gamma: 41 - service_level: 0.95 - SKU535: - constraint: null - cost: 304 - init_stock: 430 - max_stock: 1000 - price: 465 - sale_gamma: 86 - service_level: 0.95 - SKU536: - constraint: null - cost: 358 - init_stock: 208 - max_stock: 1000 - price: 415 - sale_gamma: 52 - service_level: 0.95 - SKU537: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 321 - init_stock: 45 - max_stock: 1000 - price: 455 - sale_gamma: 9 - service_level: 0.95 - SKU538: - constraint: null - cost: 457 - init_stock: 200 - max_stock: 1000 - price: 868 - sale_gamma: 50 - service_level: 0.95 - SKU539: - constraint: G(stock_constraint) - cost: 165 - init_stock: 234 - max_stock: 1000 - price: 229 - sale_gamma: 78 - service_level: 0.95 - SKU54: - constraint: G(low_profit -> low_stock_constraint) - cost: 158 - init_stock: 138 - max_stock: 1000 - price: 241 - sale_gamma: 46 - service_level: 0.95 - SKU540: - constraint: null - cost: 388 - init_stock: 294 - max_stock: 1000 - price: 496 - sale_gamma: 42 - service_level: 0.95 - SKU541: - constraint: null - cost: 131 - init_stock: 203 - max_stock: 1000 - price: 213 - sale_gamma: 29 - service_level: 0.95 - SKU542: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 130 - max_stock: 1000 - price: 42 - sale_gamma: 26 - service_level: 0.95 - SKU543: - constraint: G(stock_constraint) - cost: 430 - init_stock: 510 - max_stock: 1000 - price: 718 - sale_gamma: 85 - service_level: 0.95 - SKU544: - constraint: G(stock_constraint) - cost: 346 - init_stock: 194 - max_stock: 1000 - price: 474 - sale_gamma: 97 - service_level: 0.95 - SKU545: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU546: - constraint: null - cost: 491 - init_stock: 576 - max_stock: 1000 - price: 765 - sale_gamma: 96 - service_level: 0.95 - SKU547: - constraint: G(stock_constraint) - cost: 161 - init_stock: 264 - max_stock: 1000 - price: 251 - sale_gamma: 44 - service_level: 0.95 - SKU548: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 111 - init_stock: 36 - max_stock: 1000 - price: 217 - sale_gamma: 6 - service_level: 0.95 - SKU549: - constraint: G(stock_constraint) - cost: 387 - init_stock: 316 - max_stock: 1000 - price: 510 - sale_gamma: 79 - service_level: 0.95 - SKU55: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 455 - max_stock: 1000 - price: 896 - sale_gamma: 65 - service_level: 0.95 - SKU550: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 259 - init_stock: 282 - max_stock: 1000 - price: 502 - sale_gamma: 94 - service_level: 0.95 - SKU551: - constraint: null - cost: 46 - init_stock: 186 - max_stock: 1000 - price: 60 - sale_gamma: 31 - service_level: 0.95 - SKU552: - constraint: null - cost: 191 - init_stock: 450 - max_stock: 1000 - price: 315 - sale_gamma: 90 - service_level: 0.95 - SKU553: - constraint: G(low_profit -> low_stock_constraint) - cost: 208 - init_stock: 60 - max_stock: 1000 - price: 251 - sale_gamma: 30 - service_level: 0.95 - SKU554: - constraint: null - cost: 340 - init_stock: 82 - max_stock: 1000 - price: 387 - sale_gamma: 41 - service_level: 0.95 - SKU555: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 252 - max_stock: 1000 - price: 684 - sale_gamma: 84 - service_level: 0.95 - SKU556: - constraint: G(stock_constraint) - cost: 110 - init_stock: 90 - max_stock: 1000 - price: 213 - sale_gamma: 30 - service_level: 0.95 - SKU557: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 334 - init_stock: 365 - max_stock: 1000 - price: 661 - sale_gamma: 73 - service_level: 0.95 - SKU558: - constraint: null - cost: 399 - init_stock: 85 - max_stock: 1000 - price: 490 - sale_gamma: 17 - service_level: 0.95 - SKU559: - constraint: null - cost: 313 - init_stock: 270 - max_stock: 1000 - price: 591 - sale_gamma: 54 - service_level: 0.95 - SKU56: - constraint: null - cost: 377 - init_stock: 222 - max_stock: 1000 - price: 671 - sale_gamma: 74 - service_level: 0.95 - SKU560: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 283 - init_stock: 246 - max_stock: 1000 - price: 458 - sale_gamma: 41 - service_level: 0.95 - SKU561: - constraint: null - cost: 202 - init_stock: 312 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU562: - constraint: null - cost: 347 - init_stock: 356 - max_stock: 1000 - price: 666 - sale_gamma: 89 - service_level: 0.95 - SKU563: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 401 - init_stock: 30 - max_stock: 1000 - price: 509 - sale_gamma: 5 - service_level: 0.95 - SKU564: - constraint: null - cost: 109 - init_stock: 63 - max_stock: 1000 - price: 183 - sale_gamma: 9 - service_level: 0.95 - SKU565: - constraint: null - cost: 57 - init_stock: 525 - max_stock: 1000 - price: 90 - sale_gamma: 75 - service_level: 0.95 - SKU566: - constraint: null - cost: 131 - init_stock: 44 - max_stock: 1000 - price: 165 - sale_gamma: 11 - service_level: 0.95 - SKU567: - constraint: G(stock_constraint) - cost: 361 - init_stock: 27 - max_stock: 1000 - price: 465 - sale_gamma: 9 - service_level: 0.95 - SKU568: - constraint: null - cost: 75 - init_stock: 234 - max_stock: 1000 - price: 102 - sale_gamma: 78 - service_level: 0.95 - SKU569: - constraint: null - cost: 473 - init_stock: 192 - max_stock: 1000 - price: 591 - sale_gamma: 48 - service_level: 0.95 - SKU57: - constraint: G(stock_constraint) - cost: 359 - init_stock: 350 - max_stock: 1000 - price: 682 - sale_gamma: 50 - service_level: 0.95 - SKU570: - constraint: null - cost: 388 - init_stock: 581 - max_stock: 1000 - price: 686 - sale_gamma: 83 - service_level: 0.95 - SKU571: - constraint: G(stock_constraint) - cost: 168 - init_stock: 78 - max_stock: 1000 - price: 208 - sale_gamma: 39 - service_level: 0.95 - SKU572: - constraint: null - cost: 26 - init_stock: 225 - max_stock: 1000 - price: 41 - sale_gamma: 45 - service_level: 0.95 - SKU573: - constraint: null - cost: 246 - init_stock: 164 - max_stock: 1000 - price: 391 - sale_gamma: 41 - service_level: 0.95 - SKU574: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 150 - max_stock: 1000 - price: 166 - sale_gamma: 50 - service_level: 0.95 - SKU575: - constraint: null - cost: 237 - init_stock: 237 - max_stock: 1000 - price: 438 - sale_gamma: 79 - service_level: 0.95 - SKU576: - constraint: G(stock_constraint) - cost: 265 - init_stock: 468 - max_stock: 1000 - price: 416 - sale_gamma: 78 - service_level: 0.95 - SKU577: - constraint: G(low_profit -> low_stock_constraint) - cost: 18 - init_stock: 348 - max_stock: 1000 - price: 24 - sale_gamma: 87 - service_level: 0.95 - SKU578: - constraint: null - cost: 100 - init_stock: 284 - max_stock: 1000 - price: 148 - sale_gamma: 71 - service_level: 0.95 - SKU579: - constraint: null - cost: 415 - init_stock: 27 - max_stock: 1000 - price: 771 - sale_gamma: 9 - service_level: 0.95 - SKU58: - constraint: null - cost: 362 - init_stock: 240 - max_stock: 1000 - price: 521 - sale_gamma: 48 - service_level: 0.95 - SKU580: - constraint: null - cost: 203 - init_stock: 15 - max_stock: 1000 - price: 261 - sale_gamma: 5 - service_level: 0.95 - SKU581: - constraint: null - cost: 152 - init_stock: 516 - max_stock: 1000 - price: 185 - sale_gamma: 86 - service_level: 0.95 - SKU582: - constraint: G(stock_constraint) - cost: 359 - init_stock: 480 - max_stock: 1000 - price: 567 - sale_gamma: 96 - service_level: 0.95 - SKU583: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 86 - init_stock: 261 - max_stock: 1000 - price: 98 - sale_gamma: 87 - service_level: 0.95 - SKU584: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 33 - init_stock: 270 - max_stock: 1000 - price: 36 - sale_gamma: 45 - service_level: 0.95 - SKU585: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 53 - sale_gamma: 20 - service_level: 0.95 - SKU586: - constraint: G(stock_constraint) - cost: 61 - init_stock: 264 - max_stock: 1000 - price: 94 - sale_gamma: 44 - service_level: 0.95 - SKU587: - constraint: null - cost: 290 - init_stock: 468 - max_stock: 1000 - price: 423 - sale_gamma: 78 - service_level: 0.95 - SKU588: - constraint: G(stock_constraint) - cost: 476 - init_stock: 176 - max_stock: 1000 - price: 885 - sale_gamma: 44 - service_level: 0.95 - SKU589: - constraint: null - cost: 244 - init_stock: 170 - max_stock: 1000 - price: 380 - sale_gamma: 34 - service_level: 0.95 - SKU59: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 255 - max_stock: 1000 - price: 275 - sale_gamma: 51 - service_level: 0.95 - SKU590: - constraint: null - cost: 70 - init_stock: 93 - max_stock: 1000 - price: 103 - sale_gamma: 31 - service_level: 0.95 - SKU591: - constraint: G(stock_constraint) - cost: 206 - init_stock: 504 - max_stock: 1000 - price: 298 - sale_gamma: 84 - service_level: 0.95 - SKU592: - constraint: null - cost: 218 - init_stock: 276 - max_stock: 1000 - price: 418 - sale_gamma: 46 - service_level: 0.95 - SKU593: - constraint: null - cost: 124 - init_stock: 88 - max_stock: 1000 - price: 221 - sale_gamma: 44 - service_level: 0.95 - SKU594: - constraint: null - cost: 238 - init_stock: 150 - max_stock: 1000 - price: 459 - sale_gamma: 50 - service_level: 0.95 - SKU595: - constraint: null - cost: 73 - init_stock: 172 - max_stock: 1000 - price: 107 - sale_gamma: 43 - service_level: 0.95 - SKU596: - constraint: null - cost: 432 - init_stock: 35 - max_stock: 1000 - price: 540 - sale_gamma: 7 - service_level: 0.95 - SKU597: - constraint: G(stock_constraint) - cost: 252 - init_stock: 435 - max_stock: 1000 - price: 451 - sale_gamma: 87 - service_level: 0.95 - SKU598: - constraint: null - cost: 348 - init_stock: 28 - max_stock: 1000 - price: 612 - sale_gamma: 14 - service_level: 0.95 - SKU599: - constraint: null - cost: 54 - init_stock: 204 - max_stock: 1000 - price: 66 - sale_gamma: 68 - service_level: 0.95 - SKU6: - constraint: null - cost: 122 - init_stock: 616 - max_stock: 1000 - price: 195 - sale_gamma: 88 - service_level: 0.95 - SKU60: - constraint: G(stock_constraint) - cost: 200 - init_stock: 234 - max_stock: 1000 - price: 346 - sale_gamma: 39 - service_level: 0.95 - SKU600: - constraint: null - cost: 386 - init_stock: 325 - max_stock: 1000 - price: 505 - sale_gamma: 65 - service_level: 0.95 - SKU601: - constraint: null - cost: 138 - init_stock: 273 - max_stock: 1000 - price: 198 - sale_gamma: 39 - service_level: 0.95 - SKU602: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 184 - init_stock: 588 - max_stock: 1000 - price: 318 - sale_gamma: 98 - service_level: 0.95 - SKU603: - constraint: G(low_profit -> low_stock_constraint) - cost: 278 - init_stock: 252 - max_stock: 1000 - price: 550 - sale_gamma: 42 - service_level: 0.95 - SKU604: - constraint: G(stock_constraint) - cost: 270 - init_stock: 400 - max_stock: 1000 - price: 378 - sale_gamma: 50 - service_level: 0.95 - SKU605: - constraint: null - cost: 288 - init_stock: 120 - max_stock: 1000 - price: 443 - sale_gamma: 24 - service_level: 0.95 - SKU606: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 114 - init_stock: 66 - max_stock: 1000 - price: 161 - sale_gamma: 11 - service_level: 0.95 - SKU607: - constraint: null - cost: 208 - init_stock: 395 - max_stock: 1000 - price: 359 - sale_gamma: 79 - service_level: 0.95 - SKU608: - constraint: null - cost: 39 - init_stock: 365 - max_stock: 1000 - price: 49 - sale_gamma: 73 - service_level: 0.95 - SKU609: - constraint: null - cost: 102 - init_stock: 114 - max_stock: 1000 - price: 171 - sale_gamma: 19 - service_level: 0.95 - SKU61: - constraint: null - cost: 461 - init_stock: 150 - max_stock: 1000 - price: 645 - sale_gamma: 75 - service_level: 0.95 - SKU610: - constraint: null - cost: 449 - init_stock: 204 - max_stock: 1000 - price: 749 - sale_gamma: 68 - service_level: 0.95 - SKU611: - constraint: G(low_profit -> low_stock_constraint) - cost: 306 - init_stock: 539 - max_stock: 1000 - price: 489 - sale_gamma: 77 - service_level: 0.95 - SKU612: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 391 - init_stock: 616 - max_stock: 1000 - price: 613 - sale_gamma: 88 - service_level: 0.95 - SKU613: - constraint: G(stock_constraint) - cost: 174 - init_stock: 56 - max_stock: 1000 - price: 254 - sale_gamma: 7 - service_level: 0.95 - SKU614: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 45 - max_stock: 1000 - price: 318 - sale_gamma: 15 - service_level: 0.95 - SKU615: - constraint: G(stock_constraint) - cost: 330 - init_stock: 364 - max_stock: 1000 - price: 432 - sale_gamma: 91 - service_level: 0.95 - SKU616: - constraint: G(low_profit -> low_stock_constraint) - cost: 232 - init_stock: 219 - max_stock: 1000 - price: 382 - sale_gamma: 73 - service_level: 0.95 - SKU617: - constraint: null - cost: 203 - init_stock: 420 - max_stock: 1000 - price: 227 - sale_gamma: 60 - service_level: 0.95 - SKU618: - constraint: G(stock_constraint) - cost: 77 - init_stock: 138 - max_stock: 1000 - price: 97 - sale_gamma: 23 - service_level: 0.95 - SKU619: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 344 - max_stock: 1000 - price: 359 - sale_gamma: 86 - service_level: 0.95 - SKU62: - constraint: null - cost: 124 - init_stock: 36 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU620: - constraint: null - cost: 231 - init_stock: 156 - max_stock: 1000 - price: 267 - sale_gamma: 39 - service_level: 0.95 - SKU621: - constraint: null - cost: 199 - init_stock: 216 - max_stock: 1000 - price: 332 - sale_gamma: 72 - service_level: 0.95 - SKU622: - constraint: null - cost: 253 - init_stock: 455 - max_stock: 1000 - price: 468 - sale_gamma: 65 - service_level: 0.95 - SKU623: - constraint: null - cost: 335 - init_stock: 220 - max_stock: 1000 - price: 368 - sale_gamma: 55 - service_level: 0.95 - SKU624: - constraint: null - cost: 482 - init_stock: 270 - max_stock: 1000 - price: 824 - sale_gamma: 54 - service_level: 0.95 - SKU625: - constraint: null - cost: 46 - init_stock: 445 - max_stock: 1000 - price: 60 - sale_gamma: 89 - service_level: 0.95 - SKU626: - constraint: null - cost: 451 - init_stock: 534 - max_stock: 1000 - price: 694 - sale_gamma: 89 - service_level: 0.95 - SKU627: - constraint: null - cost: 220 - init_stock: 656 - max_stock: 1000 - price: 264 - sale_gamma: 82 - service_level: 0.95 - SKU628: - constraint: null - cost: 124 - init_stock: 156 - max_stock: 1000 - price: 229 - sale_gamma: 26 - service_level: 0.95 - SKU629: - constraint: G(stock_constraint) - cost: 353 - init_stock: 460 - max_stock: 1000 - price: 515 - sale_gamma: 92 - service_level: 0.95 - SKU63: - constraint: null - cost: 68 - init_stock: 70 - max_stock: 1000 - price: 86 - sale_gamma: 14 - service_level: 0.95 - SKU630: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 122 - init_stock: 34 - max_stock: 1000 - price: 137 - sale_gamma: 17 - service_level: 0.95 - SKU631: - constraint: null - cost: 296 - init_stock: 196 - max_stock: 1000 - price: 399 - sale_gamma: 49 - service_level: 0.95 - SKU632: - constraint: null - cost: 85 - init_stock: 470 - max_stock: 1000 - price: 141 - sale_gamma: 94 - service_level: 0.95 - SKU633: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 402 - max_stock: 1000 - price: 228 - sale_gamma: 67 - service_level: 0.95 - SKU634: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 17 - init_stock: 438 - max_stock: 1000 - price: 30 - sale_gamma: 73 - service_level: 0.95 - SKU635: - constraint: null - cost: 341 - init_stock: 372 - max_stock: 1000 - price: 654 - sale_gamma: 93 - service_level: 0.95 - SKU636: - constraint: G(stock_constraint) - cost: 385 - init_stock: 111 - max_stock: 1000 - price: 739 - sale_gamma: 37 - service_level: 0.95 - SKU637: - constraint: G(low_profit -> low_stock_constraint) - cost: 83 - init_stock: 305 - max_stock: 1000 - price: 118 - sale_gamma: 61 - service_level: 0.95 - SKU638: - constraint: G(stock_constraint) - cost: 375 - init_stock: 16 - max_stock: 1000 - price: 536 - sale_gamma: 8 - service_level: 0.95 - SKU639: - constraint: null - cost: 327 - init_stock: 160 - max_stock: 1000 - price: 487 - sale_gamma: 40 - service_level: 0.95 - SKU64: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 133 - max_stock: 1000 - price: 42 - sale_gamma: 19 - service_level: 0.95 - SKU640: - constraint: G(low_profit -> low_stock_constraint) - cost: 275 - init_stock: 546 - max_stock: 1000 - price: 382 - sale_gamma: 91 - service_level: 0.95 - SKU641: - constraint: null - cost: 365 - init_stock: 486 - max_stock: 1000 - price: 445 - sale_gamma: 81 - service_level: 0.95 - SKU642: - constraint: null - cost: 414 - init_stock: 150 - max_stock: 1000 - price: 554 - sale_gamma: 25 - service_level: 0.95 - SKU643: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 358 - init_stock: 322 - max_stock: 1000 - price: 461 - sale_gamma: 46 - service_level: 0.95 - SKU644: - constraint: G(low_profit -> low_stock_constraint) - cost: 46 - init_stock: 410 - max_stock: 1000 - price: 61 - sale_gamma: 82 - service_level: 0.95 - SKU645: - constraint: null - cost: 467 - init_stock: 594 - max_stock: 1000 - price: 742 - sale_gamma: 99 - service_level: 0.95 - SKU646: - constraint: G(low_profit -> low_stock_constraint) - cost: 189 - init_stock: 91 - max_stock: 1000 - price: 268 - sale_gamma: 13 - service_level: 0.95 - SKU647: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 462 - max_stock: 1000 - price: 369 - sale_gamma: 77 - service_level: 0.95 - SKU648: - constraint: G(stock_constraint) - cost: 226 - init_stock: 110 - max_stock: 1000 - price: 336 - sale_gamma: 55 - service_level: 0.95 - SKU649: - constraint: null - cost: 198 - init_stock: 175 - max_stock: 1000 - price: 229 - sale_gamma: 25 - service_level: 0.95 - SKU65: - constraint: null - cost: 15 - init_stock: 658 - max_stock: 1000 - price: 25 - sale_gamma: 94 - service_level: 0.95 - SKU650: - constraint: null - cost: 351 - init_stock: 224 - max_stock: 1000 - price: 498 - sale_gamma: 56 - service_level: 0.95 - SKU651: - constraint: null - cost: 380 - init_stock: 672 - max_stock: 1000 - price: 596 - sale_gamma: 96 - service_level: 0.95 - SKU652: - constraint: null - cost: 123 - init_stock: 280 - max_stock: 1000 - price: 168 - sale_gamma: 40 - service_level: 0.95 - SKU653: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 384 - max_stock: 1000 - price: 661 - sale_gamma: 96 - service_level: 0.95 - SKU654: - constraint: null - cost: 66 - init_stock: 32 - max_stock: 1000 - price: 110 - sale_gamma: 8 - service_level: 0.95 - SKU655: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 126 - max_stock: 1000 - price: 479 - sale_gamma: 42 - service_level: 0.95 - SKU656: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 53 - init_stock: 504 - max_stock: 1000 - price: 81 - sale_gamma: 72 - service_level: 0.95 - SKU657: - constraint: G(low_profit -> low_stock_constraint) - cost: 73 - init_stock: 567 - max_stock: 1000 - price: 110 - sale_gamma: 81 - service_level: 0.95 - SKU658: - constraint: null - cost: 70 - init_stock: 252 - max_stock: 1000 - price: 107 - sale_gamma: 36 - service_level: 0.95 - SKU659: - constraint: null - cost: 21 - init_stock: 336 - max_stock: 1000 - price: 27 - sale_gamma: 56 - service_level: 0.95 - SKU66: - constraint: null - cost: 143 - init_stock: 360 - max_stock: 1000 - price: 230 - sale_gamma: 90 - service_level: 0.95 - SKU660: - constraint: null - cost: 487 - init_stock: 415 - max_stock: 1000 - price: 560 - sale_gamma: 83 - service_level: 0.95 - SKU661: - constraint: null - cost: 344 - init_stock: 296 - max_stock: 1000 - price: 581 - sale_gamma: 74 - service_level: 0.95 - SKU662: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 266 - max_stock: 1000 - price: 628 - sale_gamma: 38 - service_level: 0.95 - SKU663: - constraint: G(stock_constraint) - cost: 418 - init_stock: 320 - max_stock: 1000 - price: 518 - sale_gamma: 40 - service_level: 0.95 - SKU664: - constraint: G(stock_constraint) - cost: 351 - init_stock: 420 - max_stock: 1000 - price: 494 - sale_gamma: 84 - service_level: 0.95 - SKU665: - constraint: null - cost: 493 - init_stock: 344 - max_stock: 1000 - price: 734 - sale_gamma: 43 - service_level: 0.95 - SKU666: - constraint: G(stock_constraint) - cost: 341 - init_stock: 176 - max_stock: 1000 - price: 572 - sale_gamma: 88 - service_level: 0.95 - SKU667: - constraint: null - cost: 325 - init_stock: 52 - max_stock: 1000 - price: 562 - sale_gamma: 13 - service_level: 0.95 - SKU668: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 300 - max_stock: 1000 - price: 463 - sale_gamma: 60 - service_level: 0.95 - SKU669: - constraint: null - cost: 74 - init_stock: 96 - max_stock: 1000 - price: 83 - sale_gamma: 16 - service_level: 0.95 - SKU67: - constraint: G(low_profit -> low_stock_constraint) - cost: 247 - init_stock: 159 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU670: - constraint: G(stock_constraint) - cost: 289 - init_stock: 258 - max_stock: 1000 - price: 430 - sale_gamma: 86 - service_level: 0.95 - SKU671: - constraint: G(stock_constraint) - cost: 267 - init_stock: 332 - max_stock: 1000 - price: 296 - sale_gamma: 83 - service_level: 0.95 - SKU672: - constraint: null - cost: 369 - init_stock: 78 - max_stock: 1000 - price: 420 - sale_gamma: 13 - service_level: 0.95 - SKU673: - constraint: G(low_profit -> low_stock_constraint) - cost: 322 - init_stock: 123 - max_stock: 1000 - price: 418 - sale_gamma: 41 - service_level: 0.95 - SKU674: - constraint: null - cost: 223 - init_stock: 66 - max_stock: 1000 - price: 263 - sale_gamma: 22 - service_level: 0.95 - SKU675: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 438 - init_stock: 99 - max_stock: 1000 - price: 529 - sale_gamma: 33 - service_level: 0.95 - SKU676: - constraint: null - cost: 244 - init_stock: 366 - max_stock: 1000 - price: 275 - sale_gamma: 61 - service_level: 0.95 - SKU677: - constraint: null - cost: 425 - init_stock: 176 - max_stock: 1000 - price: 658 - sale_gamma: 44 - service_level: 0.95 - SKU678: - constraint: null - cost: 168 - init_stock: 216 - max_stock: 1000 - price: 199 - sale_gamma: 36 - service_level: 0.95 - SKU679: - constraint: null - cost: 395 - init_stock: 132 - max_stock: 1000 - price: 734 - sale_gamma: 44 - service_level: 0.95 - SKU68: - constraint: G(stock_constraint) - cost: 169 - init_stock: 56 - max_stock: 1000 - price: 239 - sale_gamma: 14 - service_level: 0.95 - SKU680: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 260 - init_stock: 180 - max_stock: 1000 - price: 327 - sale_gamma: 30 - service_level: 0.95 - SKU681: - constraint: G(low_profit -> low_stock_constraint) - cost: 464 - init_stock: 776 - max_stock: 1000 - price: 510 - sale_gamma: 97 - service_level: 0.95 - SKU682: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 370 - init_stock: 318 - max_stock: 1000 - price: 407 - sale_gamma: 53 - service_level: 0.95 - SKU683: - constraint: G(low_profit -> low_stock_constraint) - cost: 327 - init_stock: 536 - max_stock: 1000 - price: 608 - sale_gamma: 67 - service_level: 0.95 - SKU684: - constraint: G(stock_constraint) - cost: 355 - init_stock: 158 - max_stock: 1000 - price: 401 - sale_gamma: 79 - service_level: 0.95 - SKU685: - constraint: null - cost: 422 - init_stock: 270 - max_stock: 1000 - price: 493 - sale_gamma: 45 - service_level: 0.95 - SKU686: - constraint: G(low_profit -> low_stock_constraint) - cost: 63 - init_stock: 378 - max_stock: 1000 - price: 122 - sale_gamma: 63 - service_level: 0.95 - SKU687: - constraint: G(low_profit -> low_stock_constraint) - cost: 34 - init_stock: 456 - max_stock: 1000 - price: 43 - sale_gamma: 76 - service_level: 0.95 - SKU688: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 462 - max_stock: 1000 - price: 759 - sale_gamma: 77 - service_level: 0.95 - SKU689: - constraint: G(stock_constraint) - cost: 499 - init_stock: 75 - max_stock: 1000 - price: 808 - sale_gamma: 15 - service_level: 0.95 - SKU69: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 176 - init_stock: 402 - max_stock: 1000 - price: 197 - sale_gamma: 67 - service_level: 0.95 - SKU690: - constraint: null - cost: 437 - init_stock: 415 - max_stock: 1000 - price: 498 - sale_gamma: 83 - service_level: 0.95 - SKU691: - constraint: null - cost: 17 - init_stock: 632 - max_stock: 1000 - price: 18 - sale_gamma: 79 - service_level: 0.95 - SKU692: - constraint: G(stock_constraint) - cost: 225 - init_stock: 195 - max_stock: 1000 - price: 258 - sale_gamma: 65 - service_level: 0.95 - SKU693: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 19 - init_stock: 244 - max_stock: 1000 - price: 21 - sale_gamma: 61 - service_level: 0.95 - SKU694: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 208 - init_stock: 300 - max_stock: 1000 - price: 386 - sale_gamma: 75 - service_level: 0.95 - SKU695: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 190 - init_stock: 49 - max_stock: 1000 - price: 361 - sale_gamma: 7 - service_level: 0.95 - SKU696: - constraint: G(low_profit -> low_stock_constraint) - cost: 348 - init_stock: 290 - max_stock: 1000 - price: 643 - sale_gamma: 58 - service_level: 0.95 - SKU697: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 455 - init_stock: 320 - max_stock: 1000 - price: 641 - sale_gamma: 80 - service_level: 0.95 - SKU698: - constraint: null - cost: 198 - init_stock: 488 - max_stock: 1000 - price: 316 - sale_gamma: 61 - service_level: 0.95 - SKU699: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 58 - sale_gamma: 20 - service_level: 0.95 - SKU7: - constraint: null - cost: 101 - init_stock: 480 - max_stock: 1000 - price: 131 - sale_gamma: 96 - service_level: 0.95 - SKU70: - constraint: G(stock_constraint) - cost: 186 - init_stock: 140 - max_stock: 1000 - price: 288 - sale_gamma: 35 - service_level: 0.95 - SKU700: - constraint: G(low_profit -> low_stock_constraint) - cost: 74 - init_stock: 45 - max_stock: 1000 - price: 130 - sale_gamma: 9 - service_level: 0.95 - SKU701: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 399 - init_stock: 462 - max_stock: 1000 - price: 770 - sale_gamma: 77 - service_level: 0.95 - SKU702: - constraint: G(stock_constraint) - cost: 82 - init_stock: 204 - max_stock: 1000 - price: 163 - sale_gamma: 34 - service_level: 0.95 - SKU703: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 363 - init_stock: 76 - max_stock: 1000 - price: 602 - sale_gamma: 38 - service_level: 0.95 - SKU704: - constraint: G(stock_constraint) - cost: 384 - init_stock: 261 - max_stock: 1000 - price: 518 - sale_gamma: 87 - service_level: 0.95 - SKU705: - constraint: null - cost: 128 - init_stock: 378 - max_stock: 1000 - price: 236 - sale_gamma: 63 - service_level: 0.95 - SKU706: - constraint: null - cost: 182 - init_stock: 455 - max_stock: 1000 - price: 242 - sale_gamma: 91 - service_level: 0.95 - SKU707: - constraint: null - cost: 18 - init_stock: 276 - max_stock: 1000 - price: 35 - sale_gamma: 69 - service_level: 0.95 - SKU708: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 178 - init_stock: 112 - max_stock: 1000 - price: 334 - sale_gamma: 28 - service_level: 0.95 - SKU709: - constraint: G(stock_constraint) - cost: 102 - init_stock: 212 - max_stock: 1000 - price: 173 - sale_gamma: 53 - service_level: 0.95 - SKU71: - constraint: null - cost: 315 - init_stock: 225 - max_stock: 1000 - price: 589 - sale_gamma: 45 - service_level: 0.95 - SKU710: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 252 - init_stock: 95 - max_stock: 1000 - price: 337 - sale_gamma: 19 - service_level: 0.95 - SKU711: - constraint: null - cost: 281 - init_stock: 414 - max_stock: 1000 - price: 320 - sale_gamma: 69 - service_level: 0.95 - SKU712: - constraint: null - cost: 232 - init_stock: 77 - max_stock: 1000 - price: 438 - sale_gamma: 11 - service_level: 0.95 - SKU713: - constraint: null - cost: 285 - init_stock: 129 - max_stock: 1000 - price: 413 - sale_gamma: 43 - service_level: 0.95 - SKU714: - constraint: null - cost: 79 - init_stock: 287 - max_stock: 1000 - price: 106 - sale_gamma: 41 - service_level: 0.95 - SKU715: - constraint: G(stock_constraint) - cost: 234 - init_stock: 34 - max_stock: 1000 - price: 271 - sale_gamma: 17 - service_level: 0.95 - SKU716: - constraint: null - cost: 70 - init_stock: 450 - max_stock: 1000 - price: 123 - sale_gamma: 75 - service_level: 0.95 - SKU717: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 345 - init_stock: 56 - max_stock: 1000 - price: 382 - sale_gamma: 8 - service_level: 0.95 - SKU718: - constraint: null - cost: 357 - init_stock: 174 - max_stock: 1000 - price: 692 - sale_gamma: 58 - service_level: 0.95 - SKU719: - constraint: null - cost: 340 - init_stock: 455 - max_stock: 1000 - price: 384 - sale_gamma: 65 - service_level: 0.95 - SKU72: - constraint: null - cost: 458 - init_stock: 595 - max_stock: 1000 - price: 870 - sale_gamma: 85 - service_level: 0.95 - SKU720: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 503 - sale_gamma: 42 - service_level: 0.95 - SKU721: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 73 - init_stock: 44 - max_stock: 1000 - price: 143 - sale_gamma: 11 - service_level: 0.95 - SKU722: - constraint: G(stock_constraint) - cost: 392 - init_stock: 194 - max_stock: 1000 - price: 572 - sale_gamma: 97 - service_level: 0.95 - SKU723: - constraint: null - cost: 318 - init_stock: 356 - max_stock: 1000 - price: 442 - sale_gamma: 89 - service_level: 0.95 - SKU724: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 198 - max_stock: 1000 - price: 520 - sale_gamma: 33 - service_level: 0.95 - SKU725: - constraint: null - cost: 175 - init_stock: 148 - max_stock: 1000 - price: 309 - sale_gamma: 37 - service_level: 0.95 - SKU726: - constraint: G(low_profit -> low_stock_constraint) - cost: 458 - init_stock: 356 - max_stock: 1000 - price: 567 - sale_gamma: 89 - service_level: 0.95 - SKU727: - constraint: null - cost: 418 - init_stock: 285 - max_stock: 1000 - price: 526 - sale_gamma: 57 - service_level: 0.95 - SKU728: - constraint: null - cost: 475 - init_stock: 185 - max_stock: 1000 - price: 864 - sale_gamma: 37 - service_level: 0.95 - SKU729: - constraint: null - cost: 324 - init_stock: 252 - max_stock: 1000 - price: 476 - sale_gamma: 36 - service_level: 0.95 - SKU73: - constraint: null - cost: 212 - init_stock: 216 - max_stock: 1000 - price: 248 - sale_gamma: 54 - service_level: 0.95 - SKU730: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 65 - max_stock: 1000 - price: 19 - sale_gamma: 13 - service_level: 0.95 - SKU731: - constraint: G(stock_constraint) - cost: 88 - init_stock: 410 - max_stock: 1000 - price: 155 - sale_gamma: 82 - service_level: 0.95 - SKU732: - constraint: null - cost: 41 - init_stock: 434 - max_stock: 1000 - price: 73 - sale_gamma: 62 - service_level: 0.95 - SKU733: - constraint: G(stock_constraint) - cost: 315 - init_stock: 104 - max_stock: 1000 - price: 541 - sale_gamma: 26 - service_level: 0.95 - SKU734: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 255 - max_stock: 1000 - price: 71 - sale_gamma: 51 - service_level: 0.95 - SKU735: - constraint: null - cost: 266 - init_stock: 594 - max_stock: 1000 - price: 409 - sale_gamma: 99 - service_level: 0.95 - SKU736: - constraint: G(stock_constraint) - cost: 368 - init_stock: 75 - max_stock: 1000 - price: 632 - sale_gamma: 25 - service_level: 0.95 - SKU737: - constraint: null - cost: 475 - init_stock: 93 - max_stock: 1000 - price: 655 - sale_gamma: 31 - service_level: 0.95 - SKU738: - constraint: null - cost: 185 - init_stock: 192 - max_stock: 1000 - price: 246 - sale_gamma: 48 - service_level: 0.95 - SKU739: - constraint: G(low_profit -> low_stock_constraint) - cost: 475 - init_stock: 296 - max_stock: 1000 - price: 612 - sale_gamma: 74 - service_level: 0.95 - SKU74: - constraint: null - cost: 159 - init_stock: 168 - max_stock: 1000 - price: 189 - sale_gamma: 42 - service_level: 0.95 - SKU740: - constraint: null - cost: 390 - init_stock: 256 - max_stock: 1000 - price: 549 - sale_gamma: 64 - service_level: 0.95 - SKU741: - constraint: G(stock_constraint) - cost: 91 - init_stock: 280 - max_stock: 1000 - price: 112 - sale_gamma: 56 - service_level: 0.95 - SKU742: - constraint: G(stock_constraint) - cost: 188 - init_stock: 110 - max_stock: 1000 - price: 374 - sale_gamma: 22 - service_level: 0.95 - SKU743: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 217 - init_stock: 301 - max_stock: 1000 - price: 431 - sale_gamma: 43 - service_level: 0.95 - SKU744: - constraint: G(low_profit -> low_stock_constraint) - cost: 379 - init_stock: 290 - max_stock: 1000 - price: 579 - sale_gamma: 58 - service_level: 0.95 - SKU745: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 368 - max_stock: 1000 - price: 376 - sale_gamma: 92 - service_level: 0.95 - SKU746: - constraint: null - cost: 437 - init_stock: 160 - max_stock: 1000 - price: 624 - sale_gamma: 40 - service_level: 0.95 - SKU747: - constraint: null - cost: 373 - init_stock: 474 - max_stock: 1000 - price: 589 - sale_gamma: 79 - service_level: 0.95 - SKU748: - constraint: null - cost: 275 - init_stock: 330 - max_stock: 1000 - price: 464 - sale_gamma: 66 - service_level: 0.95 - SKU749: - constraint: null - cost: 394 - init_stock: 106 - max_stock: 1000 - price: 705 - sale_gamma: 53 - service_level: 0.95 - SKU75: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 76 - max_stock: 1000 - price: 217 - sale_gamma: 19 - service_level: 0.95 - SKU750: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 310 - max_stock: 1000 - price: 463 - sale_gamma: 62 - service_level: 0.95 - SKU751: - constraint: null - cost: 369 - init_stock: 325 - max_stock: 1000 - price: 428 - sale_gamma: 65 - service_level: 0.95 - SKU752: - constraint: null - cost: 259 - init_stock: 546 - max_stock: 1000 - price: 435 - sale_gamma: 78 - service_level: 0.95 - SKU753: - constraint: G(stock_constraint) - cost: 77 - init_stock: 325 - max_stock: 1000 - price: 128 - sale_gamma: 65 - service_level: 0.95 - SKU754: - constraint: G(stock_constraint) - cost: 387 - init_stock: 91 - max_stock: 1000 - price: 545 - sale_gamma: 13 - service_level: 0.95 - SKU755: - constraint: null - cost: 354 - init_stock: 560 - max_stock: 1000 - price: 523 - sale_gamma: 80 - service_level: 0.95 - SKU756: - constraint: G(stock_constraint) - cost: 246 - init_stock: 360 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU757: - constraint: null - cost: 40 - init_stock: 399 - max_stock: 1000 - price: 76 - sale_gamma: 57 - service_level: 0.95 - SKU758: - constraint: null - cost: 241 - init_stock: 160 - max_stock: 1000 - price: 457 - sale_gamma: 40 - service_level: 0.95 - SKU759: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 435 - init_stock: 76 - max_stock: 1000 - price: 648 - sale_gamma: 38 - service_level: 0.95 - SKU76: - constraint: null - cost: 147 - init_stock: 256 - max_stock: 1000 - price: 191 - sale_gamma: 64 - service_level: 0.95 - SKU760: - constraint: null - cost: 176 - init_stock: 255 - max_stock: 1000 - price: 279 - sale_gamma: 51 - service_level: 0.95 - SKU761: - constraint: G(stock_constraint) - cost: 224 - init_stock: 275 - max_stock: 1000 - price: 250 - sale_gamma: 55 - service_level: 0.95 - SKU762: - constraint: G(stock_constraint) - cost: 264 - init_stock: 153 - max_stock: 1000 - price: 351 - sale_gamma: 51 - service_level: 0.95 - SKU763: - constraint: null - cost: 385 - init_stock: 316 - max_stock: 1000 - price: 677 - sale_gamma: 79 - service_level: 0.95 - SKU764: - constraint: null - cost: 349 - init_stock: 68 - max_stock: 1000 - price: 596 - sale_gamma: 34 - service_level: 0.95 - SKU765: - constraint: null - cost: 345 - init_stock: 52 - max_stock: 1000 - price: 472 - sale_gamma: 13 - service_level: 0.95 - SKU766: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 478 - init_stock: 140 - max_stock: 1000 - price: 855 - sale_gamma: 20 - service_level: 0.95 - SKU767: - constraint: null - cost: 95 - init_stock: 456 - max_stock: 1000 - price: 160 - sale_gamma: 76 - service_level: 0.95 - SKU768: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 644 - max_stock: 1000 - price: 244 - sale_gamma: 92 - service_level: 0.95 - SKU769: - constraint: null - cost: 24 - init_stock: 87 - max_stock: 1000 - price: 43 - sale_gamma: 29 - service_level: 0.95 - SKU77: - constraint: null - cost: 409 - init_stock: 144 - max_stock: 1000 - price: 687 - sale_gamma: 72 - service_level: 0.95 - SKU770: - constraint: null - cost: 150 - init_stock: 186 - max_stock: 1000 - price: 166 - sale_gamma: 62 - service_level: 0.95 - SKU771: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 35 - max_stock: 1000 - price: 182 - sale_gamma: 7 - service_level: 0.95 - SKU772: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 36 - max_stock: 1000 - price: 281 - sale_gamma: 6 - service_level: 0.95 - SKU773: - constraint: null - cost: 84 - init_stock: 368 - max_stock: 1000 - price: 117 - sale_gamma: 92 - service_level: 0.95 - SKU774: - constraint: null - cost: 447 - init_stock: 118 - max_stock: 1000 - price: 746 - sale_gamma: 59 - service_level: 0.95 - SKU775: - constraint: null - cost: 175 - init_stock: 688 - max_stock: 1000 - price: 192 - sale_gamma: 86 - service_level: 0.95 - SKU776: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 103 - init_stock: 425 - max_stock: 1000 - price: 172 - sale_gamma: 85 - service_level: 0.95 - SKU777: - constraint: null - cost: 292 - init_stock: 171 - max_stock: 1000 - price: 347 - sale_gamma: 57 - service_level: 0.95 - SKU778: - constraint: null - cost: 203 - init_stock: 40 - max_stock: 1000 - price: 288 - sale_gamma: 8 - service_level: 0.95 - SKU779: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 117 - init_stock: 215 - max_stock: 1000 - price: 186 - sale_gamma: 43 - service_level: 0.95 - SKU78: - constraint: null - cost: 234 - init_stock: 91 - max_stock: 1000 - price: 400 - sale_gamma: 13 - service_level: 0.95 - SKU780: - constraint: G(low_profit -> low_stock_constraint) - cost: 69 - init_stock: 410 - max_stock: 1000 - price: 102 - sale_gamma: 82 - service_level: 0.95 - SKU781: - constraint: null - cost: 372 - init_stock: 144 - max_stock: 1000 - price: 528 - sale_gamma: 36 - service_level: 0.95 - SKU782: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 70 - max_stock: 1000 - price: 35 - sale_gamma: 10 - service_level: 0.95 - SKU783: - constraint: null - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 139 - sale_gamma: 81 - service_level: 0.95 - SKU784: - constraint: null - cost: 309 - init_stock: 312 - max_stock: 1000 - price: 577 - sale_gamma: 52 - service_level: 0.95 - SKU785: - constraint: null - cost: 191 - init_stock: 210 - max_stock: 1000 - price: 255 - sale_gamma: 70 - service_level: 0.95 - SKU786: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 91 - init_stock: 132 - max_stock: 1000 - price: 102 - sale_gamma: 22 - service_level: 0.95 - SKU787: - constraint: null - cost: 360 - init_stock: 366 - max_stock: 1000 - price: 658 - sale_gamma: 61 - service_level: 0.95 - SKU788: - constraint: G(stock_constraint) - cost: 351 - init_stock: 112 - max_stock: 1000 - price: 417 - sale_gamma: 28 - service_level: 0.95 - SKU789: - constraint: G(stock_constraint) - cost: 153 - init_stock: 232 - max_stock: 1000 - price: 298 - sale_gamma: 58 - service_level: 0.95 - SKU79: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 245 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU790: - constraint: null - cost: 417 - init_stock: 330 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU791: - constraint: G(stock_constraint) - cost: 134 - init_stock: 56 - max_stock: 1000 - price: 155 - sale_gamma: 28 - service_level: 0.95 - SKU792: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 84 - max_stock: 1000 - price: 494 - sale_gamma: 21 - service_level: 0.95 - SKU793: - constraint: null - cost: 195 - init_stock: 532 - max_stock: 1000 - price: 282 - sale_gamma: 76 - service_level: 0.95 - SKU794: - constraint: null - cost: 325 - init_stock: 400 - max_stock: 1000 - price: 454 - sale_gamma: 80 - service_level: 0.95 - SKU795: - constraint: null - cost: 276 - init_stock: 288 - max_stock: 1000 - price: 549 - sale_gamma: 48 - service_level: 0.95 - SKU796: - constraint: null - cost: 447 - init_stock: 294 - max_stock: 1000 - price: 885 - sale_gamma: 49 - service_level: 0.95 - SKU797: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 100 - init_stock: 234 - max_stock: 1000 - price: 119 - sale_gamma: 39 - service_level: 0.95 - SKU798: - constraint: null - cost: 426 - init_stock: 180 - max_stock: 1000 - price: 630 - sale_gamma: 30 - service_level: 0.95 - SKU799: - constraint: null - cost: 63 - init_stock: 21 - max_stock: 1000 - price: 78 - sale_gamma: 7 - service_level: 0.95 - SKU8: - constraint: G(stock_constraint) - cost: 125 - init_stock: 252 - max_stock: 1000 - price: 196 - sale_gamma: 63 - service_level: 0.95 - SKU80: - constraint: G(stock_constraint) - cost: 163 - init_stock: 420 - max_stock: 1000 - price: 270 - sale_gamma: 70 - service_level: 0.95 - SKU800: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 470 - max_stock: 1000 - price: 455 - sale_gamma: 94 - service_level: 0.95 - SKU801: - constraint: G(low_profit -> low_stock_constraint) - cost: 389 - init_stock: 216 - max_stock: 1000 - price: 672 - sale_gamma: 36 - service_level: 0.95 - SKU802: - constraint: G(stock_constraint) - cost: 173 - init_stock: 310 - max_stock: 1000 - price: 281 - sale_gamma: 62 - service_level: 0.95 - SKU803: - constraint: null - cost: 272 - init_stock: 95 - max_stock: 1000 - price: 544 - sale_gamma: 19 - service_level: 0.95 - SKU804: - constraint: null - cost: 390 - init_stock: 188 - max_stock: 1000 - price: 491 - sale_gamma: 47 - service_level: 0.95 - SKU805: - constraint: null - cost: 61 - init_stock: 132 - max_stock: 1000 - price: 118 - sale_gamma: 33 - service_level: 0.95 - SKU806: - constraint: null - cost: 158 - init_stock: 156 - max_stock: 1000 - price: 186 - sale_gamma: 52 - service_level: 0.95 - SKU807: - constraint: null - cost: 453 - init_stock: 182 - max_stock: 1000 - price: 656 - sale_gamma: 26 - service_level: 0.95 - SKU808: - constraint: G(stock_constraint) - cost: 249 - init_stock: 182 - max_stock: 1000 - price: 435 - sale_gamma: 91 - service_level: 0.95 - SKU809: - constraint: null - cost: 55 - init_stock: 390 - max_stock: 1000 - price: 69 - sale_gamma: 65 - service_level: 0.95 - SKU81: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 528 - max_stock: 1000 - price: 223 - sale_gamma: 66 - service_level: 0.95 - SKU810: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 276 - init_stock: 42 - max_stock: 1000 - price: 322 - sale_gamma: 6 - service_level: 0.95 - SKU811: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 254 - init_stock: 148 - max_stock: 1000 - price: 322 - sale_gamma: 37 - service_level: 0.95 - SKU812: - constraint: null - cost: 252 - init_stock: 320 - max_stock: 1000 - price: 337 - sale_gamma: 80 - service_level: 0.95 - SKU813: - constraint: G(stock_constraint) - cost: 253 - init_stock: 49 - max_stock: 1000 - price: 333 - sale_gamma: 7 - service_level: 0.95 - SKU814: - constraint: null - cost: 485 - init_stock: 194 - max_stock: 1000 - price: 921 - sale_gamma: 97 - service_level: 0.95 - SKU815: - constraint: null - cost: 390 - init_stock: 168 - max_stock: 1000 - price: 429 - sale_gamma: 24 - service_level: 0.95 - SKU816: - constraint: null - cost: 152 - init_stock: 455 - max_stock: 1000 - price: 174 - sale_gamma: 91 - service_level: 0.95 - SKU817: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 227 - init_stock: 25 - max_stock: 1000 - price: 401 - sale_gamma: 5 - service_level: 0.95 - SKU818: - constraint: null - cost: 354 - init_stock: 215 - max_stock: 1000 - price: 534 - sale_gamma: 43 - service_level: 0.95 - SKU819: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 302 - init_stock: 135 - max_stock: 1000 - price: 480 - sale_gamma: 27 - service_level: 0.95 - SKU82: - constraint: null - cost: 290 - init_stock: 140 - max_stock: 1000 - price: 469 - sale_gamma: 28 - service_level: 0.95 - SKU820: - constraint: null - cost: 264 - init_stock: 410 - max_stock: 1000 - price: 464 - sale_gamma: 82 - service_level: 0.95 - SKU821: - constraint: G(stock_constraint) - cost: 99 - init_stock: 207 - max_stock: 1000 - price: 151 - sale_gamma: 69 - service_level: 0.95 - SKU822: - constraint: null - cost: 136 - init_stock: 490 - max_stock: 1000 - price: 266 - sale_gamma: 70 - service_level: 0.95 - SKU823: - constraint: null - cost: 75 - init_stock: 84 - max_stock: 1000 - price: 116 - sale_gamma: 28 - service_level: 0.95 - SKU824: - constraint: G(low_profit -> low_stock_constraint) - cost: 170 - init_stock: 420 - max_stock: 1000 - price: 200 - sale_gamma: 70 - service_level: 0.95 - SKU825: - constraint: null - cost: 214 - init_stock: 45 - max_stock: 1000 - price: 359 - sale_gamma: 15 - service_level: 0.95 - SKU826: - constraint: G(stock_constraint) - cost: 386 - init_stock: 330 - max_stock: 1000 - price: 428 - sale_gamma: 55 - service_level: 0.95 - SKU827: - constraint: G(stock_constraint) - cost: 148 - init_stock: 448 - max_stock: 1000 - price: 208 - sale_gamma: 64 - service_level: 0.95 - SKU828: - constraint: null - cost: 400 - init_stock: 276 - max_stock: 1000 - price: 627 - sale_gamma: 69 - service_level: 0.95 - SKU829: - constraint: null - cost: 61 - init_stock: 370 - max_stock: 1000 - price: 83 - sale_gamma: 74 - service_level: 0.95 - SKU83: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 68 - max_stock: 1000 - price: 535 - sale_gamma: 17 - service_level: 0.95 - SKU830: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 167 - init_stock: 144 - max_stock: 1000 - price: 252 - sale_gamma: 24 - service_level: 0.95 - SKU831: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 116 - max_stock: 1000 - price: 353 - sale_gamma: 29 - service_level: 0.95 - SKU832: - constraint: null - cost: 33 - init_stock: 164 - max_stock: 1000 - price: 48 - sale_gamma: 82 - service_level: 0.95 - SKU833: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 392 - max_stock: 1000 - price: 756 - sale_gamma: 98 - service_level: 0.95 - SKU834: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 10 - max_stock: 1000 - price: 662 - sale_gamma: 5 - service_level: 0.95 - SKU835: - constraint: null - cost: 440 - init_stock: 156 - max_stock: 1000 - price: 532 - sale_gamma: 52 - service_level: 0.95 - SKU836: - constraint: G(stock_constraint) - cost: 323 - init_stock: 192 - max_stock: 1000 - price: 552 - sale_gamma: 96 - service_level: 0.95 - SKU837: - constraint: null - cost: 373 - init_stock: 156 - max_stock: 1000 - price: 607 - sale_gamma: 26 - service_level: 0.95 - SKU838: - constraint: null - cost: 456 - init_stock: 308 - max_stock: 1000 - price: 711 - sale_gamma: 77 - service_level: 0.95 - SKU839: - constraint: null - cost: 473 - init_stock: 360 - max_stock: 1000 - price: 695 - sale_gamma: 60 - service_level: 0.95 - SKU84: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 318 - init_stock: 294 - max_stock: 1000 - price: 594 - sale_gamma: 42 - service_level: 0.95 - SKU840: - constraint: G(low_profit -> low_stock_constraint) - cost: 266 - init_stock: 150 - max_stock: 1000 - price: 436 - sale_gamma: 50 - service_level: 0.95 - SKU841: - constraint: G(stock_constraint) - cost: 285 - init_stock: 595 - max_stock: 1000 - price: 538 - sale_gamma: 85 - service_level: 0.95 - SKU842: - constraint: G(low_profit -> low_stock_constraint) - cost: 41 - init_stock: 252 - max_stock: 1000 - price: 70 - sale_gamma: 84 - service_level: 0.95 - SKU843: - constraint: null - cost: 360 - init_stock: 432 - max_stock: 1000 - price: 694 - sale_gamma: 72 - service_level: 0.95 - SKU844: - constraint: G(low_profit -> low_stock_constraint) - cost: 51 - init_stock: 474 - max_stock: 1000 - price: 63 - sale_gamma: 79 - service_level: 0.95 - SKU845: - constraint: G(low_profit -> low_stock_constraint) - cost: 288 - init_stock: 176 - max_stock: 1000 - price: 558 - sale_gamma: 22 - service_level: 0.95 - SKU846: - constraint: null - cost: 485 - init_stock: 234 - max_stock: 1000 - price: 940 - sale_gamma: 39 - service_level: 0.95 - SKU847: - constraint: G(low_profit -> low_stock_constraint) - cost: 388 - init_stock: 546 - max_stock: 1000 - price: 519 - sale_gamma: 91 - service_level: 0.95 - SKU848: - constraint: null - cost: 306 - init_stock: 184 - max_stock: 1000 - price: 358 - sale_gamma: 46 - service_level: 0.95 - SKU849: - constraint: G(low_profit -> low_stock_constraint) - cost: 320 - init_stock: 160 - max_stock: 1000 - price: 425 - sale_gamma: 40 - service_level: 0.95 - SKU85: - constraint: G(low_profit -> low_stock_constraint) - cost: 442 - init_stock: 400 - max_stock: 1000 - price: 583 - sale_gamma: 100 - service_level: 0.95 - SKU850: - constraint: G(stock_constraint) - cost: 183 - init_stock: 342 - max_stock: 1000 - price: 206 - sale_gamma: 57 - service_level: 0.95 - SKU851: - constraint: null - cost: 53 - init_stock: 384 - max_stock: 1000 - price: 96 - sale_gamma: 64 - service_level: 0.95 - SKU852: - constraint: null - cost: 306 - init_stock: 162 - max_stock: 1000 - price: 483 - sale_gamma: 54 - service_level: 0.95 - SKU853: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 386 - init_stock: 280 - max_stock: 1000 - price: 443 - sale_gamma: 56 - service_level: 0.95 - SKU854: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 212 - init_stock: 390 - max_stock: 1000 - price: 290 - sale_gamma: 65 - service_level: 0.95 - SKU855: - constraint: null - cost: 76 - init_stock: 432 - max_stock: 1000 - price: 114 - sale_gamma: 72 - service_level: 0.95 - SKU856: - constraint: G(low_profit -> low_stock_constraint) - cost: 380 - init_stock: 224 - max_stock: 1000 - price: 627 - sale_gamma: 32 - service_level: 0.95 - SKU857: - constraint: G(stock_constraint) - cost: 462 - init_stock: 500 - max_stock: 1000 - price: 646 - sale_gamma: 100 - service_level: 0.95 - SKU858: - constraint: null - cost: 80 - init_stock: 120 - max_stock: 1000 - price: 158 - sale_gamma: 24 - service_level: 0.95 - SKU859: - constraint: G(low_profit -> low_stock_constraint) - cost: 215 - init_stock: 224 - max_stock: 1000 - price: 298 - sale_gamma: 28 - service_level: 0.95 - SKU86: - constraint: null - cost: 386 - init_stock: 595 - max_stock: 1000 - price: 447 - sale_gamma: 85 - service_level: 0.95 - SKU860: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 105 - max_stock: 1000 - price: 435 - sale_gamma: 15 - service_level: 0.95 - SKU861: - constraint: null - cost: 477 - init_stock: 52 - max_stock: 1000 - price: 944 - sale_gamma: 13 - service_level: 0.95 - SKU862: - constraint: null - cost: 240 - init_stock: 64 - max_stock: 1000 - price: 412 - sale_gamma: 16 - service_level: 0.95 - SKU863: - constraint: null - cost: 470 - init_stock: 372 - max_stock: 1000 - price: 911 - sale_gamma: 93 - service_level: 0.95 - SKU864: - constraint: null - cost: 203 - init_stock: 114 - max_stock: 1000 - price: 341 - sale_gamma: 19 - service_level: 0.95 - SKU865: - constraint: G(low_profit -> low_stock_constraint) - cost: 144 - init_stock: 152 - max_stock: 1000 - price: 253 - sale_gamma: 19 - service_level: 0.95 - SKU866: - constraint: null - cost: 172 - init_stock: 264 - max_stock: 1000 - price: 201 - sale_gamma: 88 - service_level: 0.95 - SKU867: - constraint: G(low_profit -> low_stock_constraint) - cost: 499 - init_stock: 500 - max_stock: 1000 - price: 698 - sale_gamma: 100 - service_level: 0.95 - SKU868: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 41 - init_stock: 150 - max_stock: 1000 - price: 56 - sale_gamma: 50 - service_level: 0.95 - SKU869: - constraint: null - cost: 97 - init_stock: 388 - max_stock: 1000 - price: 111 - sale_gamma: 97 - service_level: 0.95 - SKU87: - constraint: null - cost: 368 - init_stock: 207 - max_stock: 1000 - price: 563 - sale_gamma: 69 - service_level: 0.95 - SKU870: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 281 - init_stock: 335 - max_stock: 1000 - price: 446 - sale_gamma: 67 - service_level: 0.95 - SKU871: - constraint: null - cost: 101 - init_stock: 396 - max_stock: 1000 - price: 112 - sale_gamma: 99 - service_level: 0.95 - SKU872: - constraint: null - cost: 133 - init_stock: 160 - max_stock: 1000 - price: 195 - sale_gamma: 32 - service_level: 0.95 - SKU873: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 423 - init_stock: 155 - max_stock: 1000 - price: 829 - sale_gamma: 31 - service_level: 0.95 - SKU874: - constraint: G(low_profit -> low_stock_constraint) - cost: 469 - init_stock: 360 - max_stock: 1000 - price: 689 - sale_gamma: 60 - service_level: 0.95 - SKU875: - constraint: G(stock_constraint) - cost: 51 - init_stock: 138 - max_stock: 1000 - price: 57 - sale_gamma: 23 - service_level: 0.95 - SKU876: - constraint: null - cost: 303 - init_stock: 427 - max_stock: 1000 - price: 427 - sale_gamma: 61 - service_level: 0.95 - SKU877: - constraint: null - cost: 40 - init_stock: 245 - max_stock: 1000 - price: 70 - sale_gamma: 35 - service_level: 0.95 - SKU878: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 476 - max_stock: 1000 - price: 32 - sale_gamma: 68 - service_level: 0.95 - SKU879: - constraint: null - cost: 150 - init_stock: 300 - max_stock: 1000 - price: 198 - sale_gamma: 100 - service_level: 0.95 - SKU88: - constraint: G(low_profit -> low_stock_constraint) - cost: 471 - init_stock: 36 - max_stock: 1000 - price: 824 - sale_gamma: 12 - service_level: 0.95 - SKU880: - constraint: null - cost: 433 - init_stock: 155 - max_stock: 1000 - price: 532 - sale_gamma: 31 - service_level: 0.95 - SKU881: - constraint: null - cost: 431 - init_stock: 420 - max_stock: 1000 - price: 560 - sale_gamma: 70 - service_level: 0.95 - SKU882: - constraint: G(stock_constraint) - cost: 194 - init_stock: 80 - max_stock: 1000 - price: 314 - sale_gamma: 16 - service_level: 0.95 - SKU883: - constraint: null - cost: 284 - init_stock: 152 - max_stock: 1000 - price: 479 - sale_gamma: 38 - service_level: 0.95 - SKU884: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 139 - init_stock: 195 - max_stock: 1000 - price: 222 - sale_gamma: 39 - service_level: 0.95 - SKU885: - constraint: G(low_profit -> low_stock_constraint) - cost: 49 - init_stock: 189 - max_stock: 1000 - price: 58 - sale_gamma: 27 - service_level: 0.95 - SKU886: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 584 - max_stock: 1000 - price: 602 - sale_gamma: 73 - service_level: 0.95 - SKU887: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 266 - init_stock: 174 - max_stock: 1000 - price: 494 - sale_gamma: 87 - service_level: 0.95 - SKU888: - constraint: G(low_profit -> low_stock_constraint) - cost: 143 - init_stock: 45 - max_stock: 1000 - price: 220 - sale_gamma: 9 - service_level: 0.95 - SKU889: - constraint: null - cost: 101 - init_stock: 147 - max_stock: 1000 - price: 199 - sale_gamma: 21 - service_level: 0.95 - SKU89: - constraint: null - cost: 138 - init_stock: 744 - max_stock: 1000 - price: 269 - sale_gamma: 93 - service_level: 0.95 - SKU890: - constraint: G(low_profit -> low_stock_constraint) - cost: 161 - init_stock: 600 - max_stock: 1000 - price: 247 - sale_gamma: 100 - service_level: 0.95 - SKU891: - constraint: null - cost: 356 - init_stock: 176 - max_stock: 1000 - price: 615 - sale_gamma: 22 - service_level: 0.95 - SKU892: - constraint: null - cost: 313 - init_stock: 552 - max_stock: 1000 - price: 516 - sale_gamma: 92 - service_level: 0.95 - SKU893: - constraint: null - cost: 229 - init_stock: 594 - max_stock: 1000 - price: 302 - sale_gamma: 99 - service_level: 0.95 - SKU894: - constraint: null - cost: 129 - init_stock: 222 - max_stock: 1000 - price: 176 - sale_gamma: 74 - service_level: 0.95 - SKU895: - constraint: null - cost: 230 - init_stock: 39 - max_stock: 1000 - price: 301 - sale_gamma: 13 - service_level: 0.95 - SKU896: - constraint: null - cost: 289 - init_stock: 400 - max_stock: 1000 - price: 465 - sale_gamma: 80 - service_level: 0.95 - SKU897: - constraint: null - cost: 393 - init_stock: 432 - max_stock: 1000 - price: 640 - sale_gamma: 72 - service_level: 0.95 - SKU898: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 477 - init_stock: 44 - max_stock: 1000 - price: 615 - sale_gamma: 11 - service_level: 0.95 - SKU899: - constraint: null - cost: 233 - init_stock: 240 - max_stock: 1000 - price: 309 - sale_gamma: 48 - service_level: 0.95 - SKU9: - constraint: null - cost: 166 - init_stock: 384 - max_stock: 1000 - price: 247 - sale_gamma: 96 - service_level: 0.95 - SKU90: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 454 - init_stock: 204 - max_stock: 1000 - price: 726 - sale_gamma: 51 - service_level: 0.95 - SKU900: - constraint: null - cost: 158 - init_stock: 165 - max_stock: 1000 - price: 263 - sale_gamma: 55 - service_level: 0.95 - SKU901: - constraint: G(stock_constraint) - cost: 215 - init_stock: 203 - max_stock: 1000 - price: 320 - sale_gamma: 29 - service_level: 0.95 - SKU902: - constraint: G(low_profit -> low_stock_constraint) - cost: 125 - init_stock: 560 - max_stock: 1000 - price: 205 - sale_gamma: 80 - service_level: 0.95 - SKU903: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 357 - init_stock: 266 - max_stock: 1000 - price: 517 - sale_gamma: 38 - service_level: 0.95 - SKU904: - constraint: G(low_profit -> low_stock_constraint) - cost: 496 - init_stock: 255 - max_stock: 1000 - price: 605 - sale_gamma: 51 - service_level: 0.95 - SKU905: - constraint: null - cost: 249 - init_stock: 217 - max_stock: 1000 - price: 383 - sale_gamma: 31 - service_level: 0.95 - SKU906: - constraint: G(low_profit -> low_stock_constraint) - cost: 166 - init_stock: 156 - max_stock: 1000 - price: 273 - sale_gamma: 52 - service_level: 0.95 - SKU907: - constraint: null - cost: 22 - init_stock: 498 - max_stock: 1000 - price: 33 - sale_gamma: 83 - service_level: 0.95 - SKU908: - constraint: null - cost: 408 - init_stock: 546 - max_stock: 1000 - price: 595 - sale_gamma: 78 - service_level: 0.95 - SKU909: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 480 - max_stock: 1000 - price: 703 - sale_gamma: 96 - service_level: 0.95 - SKU91: - constraint: G(low_profit -> low_stock_constraint) - cost: 303 - init_stock: 48 - max_stock: 1000 - price: 463 - sale_gamma: 16 - service_level: 0.95 - SKU910: - constraint: null - cost: 226 - init_stock: 132 - max_stock: 1000 - price: 350 - sale_gamma: 33 - service_level: 0.95 - SKU911: - constraint: null - cost: 461 - init_stock: 210 - max_stock: 1000 - price: 686 - sale_gamma: 70 - service_level: 0.95 - SKU912: - constraint: null - cost: 236 - init_stock: 135 - max_stock: 1000 - price: 328 - sale_gamma: 27 - service_level: 0.95 - SKU913: - constraint: null - cost: 322 - init_stock: 184 - max_stock: 1000 - price: 518 - sale_gamma: 46 - service_level: 0.95 - SKU914: - constraint: null - cost: 272 - init_stock: 434 - max_stock: 1000 - price: 446 - sale_gamma: 62 - service_level: 0.95 - SKU915: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 337 - init_stock: 280 - max_stock: 1000 - price: 545 - sale_gamma: 56 - service_level: 0.95 - SKU916: - constraint: null - cost: 337 - init_stock: 534 - max_stock: 1000 - price: 647 - sale_gamma: 89 - service_level: 0.95 - SKU917: - constraint: G(low_profit -> low_stock_constraint) - cost: 258 - init_stock: 120 - max_stock: 1000 - price: 366 - sale_gamma: 30 - service_level: 0.95 - SKU918: - constraint: G(stock_constraint) - cost: 148 - init_stock: 330 - max_stock: 1000 - price: 224 - sale_gamma: 55 - service_level: 0.95 - SKU919: - constraint: G(stock_constraint) - cost: 393 - init_stock: 125 - max_stock: 1000 - price: 711 - sale_gamma: 25 - service_level: 0.95 - SKU92: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 252 - max_stock: 1000 - price: 466 - sale_gamma: 63 - service_level: 0.95 - SKU920: - constraint: null - cost: 357 - init_stock: 344 - max_stock: 1000 - price: 696 - sale_gamma: 86 - service_level: 0.95 - SKU921: - constraint: G(low_profit -> low_stock_constraint) - cost: 227 - init_stock: 198 - max_stock: 1000 - price: 419 - sale_gamma: 66 - service_level: 0.95 - SKU922: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 201 - max_stock: 1000 - price: 123 - sale_gamma: 67 - service_level: 0.95 - SKU923: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 348 - max_stock: 1000 - price: 828 - sale_gamma: 58 - service_level: 0.95 - SKU924: - constraint: null - cost: 316 - init_stock: 425 - max_stock: 1000 - price: 423 - sale_gamma: 85 - service_level: 0.95 - SKU925: - constraint: null - cost: 360 - init_stock: 90 - max_stock: 1000 - price: 716 - sale_gamma: 15 - service_level: 0.95 - SKU926: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 360 - init_stock: 469 - max_stock: 1000 - price: 475 - sale_gamma: 67 - service_level: 0.95 - SKU927: - constraint: G(stock_constraint) - cost: 260 - init_stock: 147 - max_stock: 1000 - price: 439 - sale_gamma: 21 - service_level: 0.95 - SKU928: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 491 - init_stock: 415 - max_stock: 1000 - price: 869 - sale_gamma: 83 - service_level: 0.95 - SKU929: - constraint: null - cost: 359 - init_stock: 800 - max_stock: 1000 - price: 470 - sale_gamma: 100 - service_level: 0.95 - SKU93: - constraint: null - cost: 404 - init_stock: 268 - max_stock: 1000 - price: 557 - sale_gamma: 67 - service_level: 0.95 - SKU930: - constraint: G(low_profit -> low_stock_constraint) - cost: 198 - init_stock: 196 - max_stock: 1000 - price: 247 - sale_gamma: 28 - service_level: 0.95 - SKU931: - constraint: null - cost: 71 - init_stock: 56 - max_stock: 1000 - price: 101 - sale_gamma: 14 - service_level: 0.95 - SKU932: - constraint: null - cost: 163 - init_stock: 264 - max_stock: 1000 - price: 283 - sale_gamma: 66 - service_level: 0.95 - SKU933: - constraint: null - cost: 113 - init_stock: 156 - max_stock: 1000 - price: 179 - sale_gamma: 78 - service_level: 0.95 - SKU934: - constraint: G(stock_constraint) - cost: 219 - init_stock: 469 - max_stock: 1000 - price: 346 - sale_gamma: 67 - service_level: 0.95 - SKU935: - constraint: null - cost: 364 - init_stock: 376 - max_stock: 1000 - price: 527 - sale_gamma: 94 - service_level: 0.95 - SKU936: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 24 - init_stock: 20 - max_stock: 1000 - price: 41 - sale_gamma: 5 - service_level: 0.95 - SKU937: - constraint: G(stock_constraint) - cost: 135 - init_stock: 85 - max_stock: 1000 - price: 216 - sale_gamma: 17 - service_level: 0.95 - SKU938: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 63 - max_stock: 1000 - price: 704 - sale_gamma: 21 - service_level: 0.95 - SKU939: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 413 - max_stock: 1000 - price: 219 - sale_gamma: 59 - service_level: 0.95 - SKU94: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 235 - max_stock: 1000 - price: 261 - sale_gamma: 47 - service_level: 0.95 - SKU940: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 14 - init_stock: 279 - max_stock: 1000 - price: 24 - sale_gamma: 93 - service_level: 0.95 - SKU941: - constraint: G(stock_constraint) - cost: 80 - init_stock: 456 - max_stock: 1000 - price: 132 - sale_gamma: 57 - service_level: 0.95 - SKU942: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 202 - init_stock: 65 - max_stock: 1000 - price: 303 - sale_gamma: 13 - service_level: 0.95 - SKU943: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 182 - sale_gamma: 49 - service_level: 0.95 - SKU944: - constraint: null - cost: 196 - init_stock: 132 - max_stock: 1000 - price: 356 - sale_gamma: 44 - service_level: 0.95 - SKU945: - constraint: null - cost: 141 - init_stock: 85 - max_stock: 1000 - price: 176 - sale_gamma: 17 - service_level: 0.95 - SKU946: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 555 - sale_gamma: 49 - service_level: 0.95 - SKU947: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 338 - init_stock: 637 - max_stock: 1000 - price: 547 - sale_gamma: 91 - service_level: 0.95 - SKU948: - constraint: null - cost: 425 - init_stock: 112 - max_stock: 1000 - price: 476 - sale_gamma: 28 - service_level: 0.95 - SKU949: - constraint: G(stock_constraint) - cost: 309 - init_stock: 15 - max_stock: 1000 - price: 522 - sale_gamma: 5 - service_level: 0.95 - SKU95: - constraint: null - cost: 136 - init_stock: 84 - max_stock: 1000 - price: 214 - sale_gamma: 21 - service_level: 0.95 - SKU950: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 102 - init_stock: 324 - max_stock: 1000 - price: 128 - sale_gamma: 54 - service_level: 0.95 - SKU951: - constraint: G(low_profit -> low_stock_constraint) - cost: 75 - init_stock: 90 - max_stock: 1000 - price: 117 - sale_gamma: 18 - service_level: 0.95 - SKU952: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 156 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU953: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 230 - sale_gamma: 35 - service_level: 0.95 - SKU954: - constraint: null - cost: 296 - init_stock: 430 - max_stock: 1000 - price: 444 - sale_gamma: 86 - service_level: 0.95 - SKU955: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 55 - init_stock: 189 - max_stock: 1000 - price: 85 - sale_gamma: 63 - service_level: 0.95 - SKU956: - constraint: null - cost: 282 - init_stock: 425 - max_stock: 1000 - price: 397 - sale_gamma: 85 - service_level: 0.95 - SKU957: - constraint: null - cost: 305 - init_stock: 306 - max_stock: 1000 - price: 466 - sale_gamma: 51 - service_level: 0.95 - SKU958: - constraint: null - cost: 369 - init_stock: 462 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU959: - constraint: null - cost: 81 - init_stock: 164 - max_stock: 1000 - price: 127 - sale_gamma: 82 - service_level: 0.95 - SKU96: - constraint: G(low_profit -> low_stock_constraint) - cost: 176 - init_stock: 264 - max_stock: 1000 - price: 329 - sale_gamma: 44 - service_level: 0.95 - SKU960: - constraint: null - cost: 147 - init_stock: 42 - max_stock: 1000 - price: 235 - sale_gamma: 6 - service_level: 0.95 - SKU961: - constraint: null - cost: 264 - init_stock: 176 - max_stock: 1000 - price: 290 - sale_gamma: 44 - service_level: 0.95 - SKU962: - constraint: null - cost: 354 - init_stock: 176 - max_stock: 1000 - price: 392 - sale_gamma: 44 - service_level: 0.95 - SKU963: - constraint: null - cost: 349 - init_stock: 63 - max_stock: 1000 - price: 523 - sale_gamma: 21 - service_level: 0.95 - SKU964: - constraint: null - cost: 244 - init_stock: 219 - max_stock: 1000 - price: 480 - sale_gamma: 73 - service_level: 0.95 - SKU965: - constraint: G(stock_constraint) - cost: 124 - init_stock: 255 - max_stock: 1000 - price: 199 - sale_gamma: 51 - service_level: 0.95 - SKU966: - constraint: G(stock_constraint) - cost: 302 - init_stock: 308 - max_stock: 1000 - price: 474 - sale_gamma: 44 - service_level: 0.95 - SKU967: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 270 - max_stock: 1000 - price: 111 - sale_gamma: 45 - service_level: 0.95 - SKU968: - constraint: G(stock_constraint) - cost: 281 - init_stock: 294 - max_stock: 1000 - price: 528 - sale_gamma: 49 - service_level: 0.95 - SKU969: - constraint: G(low_profit -> low_stock_constraint) - cost: 249 - init_stock: 24 - max_stock: 1000 - price: 328 - sale_gamma: 6 - service_level: 0.95 - SKU97: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 28 - init_stock: 180 - max_stock: 1000 - price: 43 - sale_gamma: 30 - service_level: 0.95 - SKU970: - constraint: null - cost: 244 - init_stock: 30 - max_stock: 1000 - price: 373 - sale_gamma: 5 - service_level: 0.95 - SKU971: - constraint: null - cost: 368 - init_stock: 219 - max_stock: 1000 - price: 426 - sale_gamma: 73 - service_level: 0.95 - SKU972: - constraint: G(stock_constraint) - cost: 209 - init_stock: 345 - max_stock: 1000 - price: 307 - sale_gamma: 69 - service_level: 0.95 - SKU973: - constraint: null - cost: 271 - init_stock: 33 - max_stock: 1000 - price: 447 - sale_gamma: 11 - service_level: 0.95 - SKU974: - constraint: null - cost: 170 - init_stock: 192 - max_stock: 1000 - price: 285 - sale_gamma: 32 - service_level: 0.95 - SKU975: - constraint: G(stock_constraint) - cost: 198 - init_stock: 88 - max_stock: 1000 - price: 334 - sale_gamma: 22 - service_level: 0.95 - SKU976: - constraint: G(stock_constraint) - cost: 178 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU977: - constraint: G(stock_constraint) - cost: 234 - init_stock: 324 - max_stock: 1000 - price: 269 - sale_gamma: 54 - service_level: 0.95 - SKU978: - constraint: G(stock_constraint) - cost: 470 - init_stock: 320 - max_stock: 1000 - price: 921 - sale_gamma: 64 - service_level: 0.95 - SKU979: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 443 - init_stock: 480 - max_stock: 1000 - price: 753 - sale_gamma: 96 - service_level: 0.95 - SKU98: - constraint: G(stock_constraint) - cost: 443 - init_stock: 70 - max_stock: 1000 - price: 527 - sale_gamma: 35 - service_level: 0.95 - SKU980: - constraint: null - cost: 39 - init_stock: 340 - max_stock: 1000 - price: 60 - sale_gamma: 68 - service_level: 0.95 - SKU981: - constraint: null - cost: 482 - init_stock: 360 - max_stock: 1000 - price: 607 - sale_gamma: 72 - service_level: 0.95 - SKU982: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 213 - init_stock: 208 - max_stock: 1000 - price: 374 - sale_gamma: 52 - service_level: 0.95 - SKU983: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 276 - max_stock: 1000 - price: 776 - sale_gamma: 92 - service_level: 0.95 - SKU984: - constraint: G(stock_constraint) - cost: 232 - init_stock: 637 - max_stock: 1000 - price: 327 - sale_gamma: 91 - service_level: 0.95 - SKU985: - constraint: null - cost: 290 - init_stock: 153 - max_stock: 1000 - price: 420 - sale_gamma: 51 - service_level: 0.95 - SKU986: - constraint: null - cost: 275 - init_stock: 136 - max_stock: 1000 - price: 313 - sale_gamma: 17 - service_level: 0.95 - SKU987: - constraint: null - cost: 434 - init_stock: 204 - max_stock: 1000 - price: 516 - sale_gamma: 34 - service_level: 0.95 - SKU988: - constraint: null - cost: 102 - init_stock: 69 - max_stock: 1000 - price: 166 - sale_gamma: 23 - service_level: 0.95 - SKU989: - constraint: null - cost: 484 - init_stock: 558 - max_stock: 1000 - price: 653 - sale_gamma: 93 - service_level: 0.95 - SKU99: - constraint: null - cost: 361 - init_stock: 225 - max_stock: 1000 - price: 581 - sale_gamma: 45 - service_level: 0.95 - SKU990: - constraint: G(low_profit -> low_stock_constraint) - cost: 108 - init_stock: 228 - max_stock: 1000 - price: 174 - sale_gamma: 57 - service_level: 0.95 - SKU991: - constraint: null - cost: 409 - init_stock: 152 - max_stock: 1000 - price: 576 - sale_gamma: 19 - service_level: 0.95 - SKU992: - constraint: null - cost: 434 - init_stock: 651 - max_stock: 1000 - price: 685 - sale_gamma: 93 - service_level: 0.95 - SKU993: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 602 - max_stock: 1000 - price: 201 - sale_gamma: 86 - service_level: 0.95 - SKU994: - constraint: G(low_profit -> low_stock_constraint) - cost: 470 - init_stock: 300 - max_stock: 1000 - price: 916 - sale_gamma: 50 - service_level: 0.95 - SKU995: - constraint: null - cost: 241 - init_stock: 532 - max_stock: 1000 - price: 310 - sale_gamma: 76 - service_level: 0.95 - SKU996: - constraint: G(stock_constraint) - cost: 260 - init_stock: 438 - max_stock: 1000 - price: 416 - sale_gamma: 73 - service_level: 0.95 - SKU997: - constraint: G(stock_constraint) - cost: 400 - init_stock: 102 - max_stock: 1000 - price: 727 - sale_gamma: 34 - service_level: 0.95 - SKU998: - constraint: G(low_profit -> low_stock_constraint) - cost: 447 - init_stock: 165 - max_stock: 1000 - price: 688 - sale_gamma: 55 - service_level: 0.95 - SKU999: - constraint: null - cost: 79 - init_stock: 161 - max_stock: 1000 - price: 86 - sale_gamma: 23 - service_level: 0.95 - grid: - facilities: - STORE0: - - 9 - - 8 - SUPPLIER0: - - 14 - - 16 - WAREHOUSE0: - - 16 - - 18 - size: - - 20 - - 20 - skus: - - id: 0 - name: SKU0 - - id: 1 - name: SKU1 - - id: 2 - name: SKU2 - - id: 3 - name: SKU3 - - id: 4 - name: SKU4 - - id: 5 - name: SKU5 - - id: 6 - name: SKU6 - - id: 7 - name: SKU7 - - id: 8 - name: SKU8 - - id: 9 - name: SKU9 - - id: 10 - name: SKU10 - - id: 11 - name: SKU11 - - id: 12 - name: SKU12 - - id: 13 - name: SKU13 - - id: 14 - name: SKU14 - - id: 15 - name: SKU15 - - id: 16 - name: SKU16 - - id: 17 - name: SKU17 - - id: 18 - name: SKU18 - - id: 19 - name: SKU19 - - id: 20 - name: SKU20 - - id: 21 - name: SKU21 - - id: 22 - name: SKU22 - - id: 23 - name: SKU23 - - id: 24 - name: SKU24 - - id: 25 - name: SKU25 - - id: 26 - name: SKU26 - - id: 27 - name: SKU27 - - id: 28 - name: SKU28 - - id: 29 - name: SKU29 - - id: 30 - name: SKU30 - - id: 31 - name: SKU31 - - id: 32 - name: SKU32 - - id: 33 - name: SKU33 - - id: 34 - name: SKU34 - - id: 35 - name: SKU35 - - id: 36 - name: SKU36 - - id: 37 - name: SKU37 - - id: 38 - name: SKU38 - - id: 39 - name: SKU39 - - id: 40 - name: SKU40 - - id: 41 - name: SKU41 - - id: 42 - name: SKU42 - - id: 43 - name: SKU43 - - id: 44 - name: SKU44 - - id: 45 - name: SKU45 - - id: 46 - name: SKU46 - - id: 47 - name: SKU47 - - id: 48 - name: SKU48 - - id: 49 - name: SKU49 - - id: 50 - name: SKU50 - - id: 51 - name: SKU51 - - id: 52 - name: SKU52 - - id: 53 - name: SKU53 - - id: 54 - name: SKU54 - - id: 55 - name: SKU55 - - id: 56 - name: SKU56 - - id: 57 - name: SKU57 - - id: 58 - name: SKU58 - - id: 59 - name: SKU59 - - id: 60 - name: SKU60 - - id: 61 - name: SKU61 - - id: 62 - name: SKU62 - - id: 63 - name: SKU63 - - id: 64 - name: SKU64 - - id: 65 - name: SKU65 - - id: 66 - name: SKU66 - - id: 67 - name: SKU67 - - id: 68 - name: SKU68 - - id: 69 - name: SKU69 - - id: 70 - name: SKU70 - - id: 71 - name: SKU71 - - id: 72 - name: SKU72 - - id: 73 - name: SKU73 - - id: 74 - name: SKU74 - - id: 75 - name: SKU75 - - id: 76 - name: SKU76 - - id: 77 - name: SKU77 - - id: 78 - name: SKU78 - - id: 79 - name: SKU79 - - id: 80 - name: SKU80 - - id: 81 - name: SKU81 - - id: 82 - name: SKU82 - - id: 83 - name: SKU83 - - id: 84 - name: SKU84 - - id: 85 - name: SKU85 - - id: 86 - name: SKU86 - - id: 87 - name: SKU87 - - id: 88 - name: SKU88 - - id: 89 - name: SKU89 - - id: 90 - name: SKU90 - - id: 91 - name: SKU91 - - id: 92 - name: SKU92 - - id: 93 - name: SKU93 - - id: 94 - name: SKU94 - - id: 95 - name: SKU95 - - id: 96 - name: SKU96 - - id: 97 - name: SKU97 - - id: 98 - name: SKU98 - - id: 99 - name: SKU99 - - id: 100 - name: SKU100 - - id: 101 - name: SKU101 - - id: 102 - name: SKU102 - - id: 103 - name: SKU103 - - id: 104 - name: SKU104 - - id: 105 - name: SKU105 - - id: 106 - name: SKU106 - - id: 107 - name: SKU107 - - id: 108 - name: SKU108 - - id: 109 - name: SKU109 - - id: 110 - name: SKU110 - - id: 111 - name: SKU111 - - id: 112 - name: SKU112 - - id: 113 - name: SKU113 - - id: 114 - name: SKU114 - - id: 115 - name: SKU115 - - id: 116 - name: SKU116 - - id: 117 - name: SKU117 - - id: 118 - name: SKU118 - - id: 119 - name: SKU119 - - id: 120 - name: SKU120 - - id: 121 - name: SKU121 - - id: 122 - name: SKU122 - - id: 123 - name: SKU123 - - id: 124 - name: SKU124 - - id: 125 - name: SKU125 - - id: 126 - name: SKU126 - - id: 127 - name: SKU127 - - id: 128 - name: SKU128 - - id: 129 - name: SKU129 - - id: 130 - name: SKU130 - - id: 131 - name: SKU131 - - id: 132 - name: SKU132 - - id: 133 - name: SKU133 - - id: 134 - name: SKU134 - - id: 135 - name: SKU135 - - id: 136 - name: SKU136 - - id: 137 - name: SKU137 - - id: 138 - name: SKU138 - - id: 139 - name: SKU139 - - id: 140 - name: SKU140 - - id: 141 - name: SKU141 - - id: 142 - name: SKU142 - - id: 143 - name: SKU143 - - id: 144 - name: SKU144 - - id: 145 - name: SKU145 - - id: 146 - name: SKU146 - - id: 147 - name: SKU147 - - id: 148 - name: SKU148 - - id: 149 - name: SKU149 - - id: 150 - name: SKU150 - - id: 151 - name: SKU151 - - id: 152 - name: SKU152 - - id: 153 - name: SKU153 - - id: 154 - name: SKU154 - - id: 155 - name: SKU155 - - id: 156 - name: SKU156 - - id: 157 - name: SKU157 - - id: 158 - name: SKU158 - - id: 159 - name: SKU159 - - id: 160 - name: SKU160 - - id: 161 - name: SKU161 - - id: 162 - name: SKU162 - - id: 163 - name: SKU163 - - id: 164 - name: SKU164 - - id: 165 - name: SKU165 - - id: 166 - name: SKU166 - - id: 167 - name: SKU167 - - id: 168 - name: SKU168 - - id: 169 - name: SKU169 - - id: 170 - name: SKU170 - - id: 171 - name: SKU171 - - id: 172 - name: SKU172 - - id: 173 - name: SKU173 - - id: 174 - name: SKU174 - - id: 175 - name: SKU175 - - id: 176 - name: SKU176 - - id: 177 - name: SKU177 - - id: 178 - name: SKU178 - - id: 179 - name: SKU179 - - id: 180 - name: SKU180 - - id: 181 - name: SKU181 - - id: 182 - name: SKU182 - - id: 183 - name: SKU183 - - id: 184 - name: SKU184 - - id: 185 - name: SKU185 - - id: 186 - name: SKU186 - - id: 187 - name: SKU187 - - id: 188 - name: SKU188 - - id: 189 - name: SKU189 - - id: 190 - name: SKU190 - - id: 191 - name: SKU191 - - id: 192 - name: SKU192 - - id: 193 - name: SKU193 - - id: 194 - name: SKU194 - - id: 195 - name: SKU195 - - id: 196 - name: SKU196 - - id: 197 - name: SKU197 - - id: 198 - name: SKU198 - - id: 199 - name: SKU199 - - id: 200 - name: SKU200 - - id: 201 - name: SKU201 - - id: 202 - name: SKU202 - - id: 203 - name: SKU203 - - id: 204 - name: SKU204 - - id: 205 - name: SKU205 - - id: 206 - name: SKU206 - - id: 207 - name: SKU207 - - id: 208 - name: SKU208 - - id: 209 - name: SKU209 - - id: 210 - name: SKU210 - - id: 211 - name: SKU211 - - id: 212 - name: SKU212 - - id: 213 - name: SKU213 - - id: 214 - name: SKU214 - - id: 215 - name: SKU215 - - id: 216 - name: SKU216 - - id: 217 - name: SKU217 - - id: 218 - name: SKU218 - - id: 219 - name: SKU219 - - id: 220 - name: SKU220 - - id: 221 - name: SKU221 - - id: 222 - name: SKU222 - - id: 223 - name: SKU223 - - id: 224 - name: SKU224 - - id: 225 - name: SKU225 - - id: 226 - name: SKU226 - - id: 227 - name: SKU227 - - id: 228 - name: SKU228 - - id: 229 - name: SKU229 - - id: 230 - name: SKU230 - - id: 231 - name: SKU231 - - id: 232 - name: SKU232 - - id: 233 - name: SKU233 - - id: 234 - name: SKU234 - - id: 235 - name: SKU235 - - id: 236 - name: SKU236 - - id: 237 - name: SKU237 - - id: 238 - name: SKU238 - - id: 239 - name: SKU239 - - id: 240 - name: SKU240 - - id: 241 - name: SKU241 - - id: 242 - name: SKU242 - - id: 243 - name: SKU243 - - id: 244 - name: SKU244 - - id: 245 - name: SKU245 - - id: 246 - name: SKU246 - - id: 247 - name: SKU247 - - id: 248 - name: SKU248 - - id: 249 - name: SKU249 - - id: 250 - name: SKU250 - - id: 251 - name: SKU251 - - id: 252 - name: SKU252 - - id: 253 - name: SKU253 - - id: 254 - name: SKU254 - - id: 255 - name: SKU255 - - id: 256 - name: SKU256 - - id: 257 - name: SKU257 - - id: 258 - name: SKU258 - - id: 259 - name: SKU259 - - id: 260 - name: SKU260 - - id: 261 - name: SKU261 - - id: 262 - name: SKU262 - - id: 263 - name: SKU263 - - id: 264 - name: SKU264 - - id: 265 - name: SKU265 - - id: 266 - name: SKU266 - - id: 267 - name: SKU267 - - id: 268 - name: SKU268 - - id: 269 - name: SKU269 - - id: 270 - name: SKU270 - - id: 271 - name: SKU271 - - id: 272 - name: SKU272 - - id: 273 - name: SKU273 - - id: 274 - name: SKU274 - - id: 275 - name: SKU275 - - id: 276 - name: SKU276 - - id: 277 - name: SKU277 - - id: 278 - name: SKU278 - - id: 279 - name: SKU279 - - id: 280 - name: SKU280 - - id: 281 - name: SKU281 - - id: 282 - name: SKU282 - - id: 283 - name: SKU283 - - id: 284 - name: SKU284 - - id: 285 - name: SKU285 - - id: 286 - name: SKU286 - - id: 287 - name: SKU287 - - id: 288 - name: SKU288 - - id: 289 - name: SKU289 - - id: 290 - name: SKU290 - - id: 291 - name: SKU291 - - id: 292 - name: SKU292 - - id: 293 - name: SKU293 - - id: 294 - name: SKU294 - - id: 295 - name: SKU295 - - id: 296 - name: SKU296 - - id: 297 - name: SKU297 - - id: 298 - name: SKU298 - - id: 299 - name: SKU299 - - id: 300 - name: SKU300 - - id: 301 - name: SKU301 - - id: 302 - name: SKU302 - - id: 303 - name: SKU303 - - id: 304 - name: SKU304 - - id: 305 - name: SKU305 - - id: 306 - name: SKU306 - - id: 307 - name: SKU307 - - id: 308 - name: SKU308 - - id: 309 - name: SKU309 - - id: 310 - name: SKU310 - - id: 311 - name: SKU311 - - id: 312 - name: SKU312 - - id: 313 - name: SKU313 - - id: 314 - name: SKU314 - - id: 315 - name: SKU315 - - id: 316 - name: SKU316 - - id: 317 - name: SKU317 - - id: 318 - name: SKU318 - - id: 319 - name: SKU319 - - id: 320 - name: SKU320 - - id: 321 - name: SKU321 - - id: 322 - name: SKU322 - - id: 323 - name: SKU323 - - id: 324 - name: SKU324 - - id: 325 - name: SKU325 - - id: 326 - name: SKU326 - - id: 327 - name: SKU327 - - id: 328 - name: SKU328 - - id: 329 - name: SKU329 - - id: 330 - name: SKU330 - - id: 331 - name: SKU331 - - id: 332 - name: SKU332 - - id: 333 - name: SKU333 - - id: 334 - name: SKU334 - - id: 335 - name: SKU335 - - id: 336 - name: SKU336 - - id: 337 - name: SKU337 - - id: 338 - name: SKU338 - - id: 339 - name: SKU339 - - id: 340 - name: SKU340 - - id: 341 - name: SKU341 - - id: 342 - name: SKU342 - - id: 343 - name: SKU343 - - id: 344 - name: SKU344 - - id: 345 - name: SKU345 - - id: 346 - name: SKU346 - - id: 347 - name: SKU347 - - id: 348 - name: SKU348 - - id: 349 - name: SKU349 - - id: 350 - name: SKU350 - - id: 351 - name: SKU351 - - id: 352 - name: SKU352 - - id: 353 - name: SKU353 - - id: 354 - name: SKU354 - - id: 355 - name: SKU355 - - id: 356 - name: SKU356 - - id: 357 - name: SKU357 - - id: 358 - name: SKU358 - - id: 359 - name: SKU359 - - id: 360 - name: SKU360 - - id: 361 - name: SKU361 - - id: 362 - name: SKU362 - - id: 363 - name: SKU363 - - id: 364 - name: SKU364 - - id: 365 - name: SKU365 - - id: 366 - name: SKU366 - - id: 367 - name: SKU367 - - id: 368 - name: SKU368 - - id: 369 - name: SKU369 - - id: 370 - name: SKU370 - - id: 371 - name: SKU371 - - id: 372 - name: SKU372 - - id: 373 - name: SKU373 - - id: 374 - name: SKU374 - - id: 375 - name: SKU375 - - id: 376 - name: SKU376 - - id: 377 - name: SKU377 - - id: 378 - name: SKU378 - - id: 379 - name: SKU379 - - id: 380 - name: SKU380 - - id: 381 - name: SKU381 - - id: 382 - name: SKU382 - - id: 383 - name: SKU383 - - id: 384 - name: SKU384 - - id: 385 - name: SKU385 - - id: 386 - name: SKU386 - - id: 387 - name: SKU387 - - id: 388 - name: SKU388 - - id: 389 - name: SKU389 - - id: 390 - name: SKU390 - - id: 391 - name: SKU391 - - id: 392 - name: SKU392 - - id: 393 - name: SKU393 - - id: 394 - name: SKU394 - - id: 395 - name: SKU395 - - id: 396 - name: SKU396 - - id: 397 - name: SKU397 - - id: 398 - name: SKU398 - - id: 399 - name: SKU399 - - id: 400 - name: SKU400 - - id: 401 - name: SKU401 - - id: 402 - name: SKU402 - - id: 403 - name: SKU403 - - id: 404 - name: SKU404 - - id: 405 - name: SKU405 - - id: 406 - name: SKU406 - - id: 407 - name: SKU407 - - id: 408 - name: SKU408 - - id: 409 - name: SKU409 - - id: 410 - name: SKU410 - - id: 411 - name: SKU411 - - id: 412 - name: SKU412 - - id: 413 - name: SKU413 - - id: 414 - name: SKU414 - - id: 415 - name: SKU415 - - id: 416 - name: SKU416 - - id: 417 - name: SKU417 - - id: 418 - name: SKU418 - - id: 419 - name: SKU419 - - id: 420 - name: SKU420 - - id: 421 - name: SKU421 - - id: 422 - name: SKU422 - - id: 423 - name: SKU423 - - id: 424 - name: SKU424 - - id: 425 - name: SKU425 - - id: 426 - name: SKU426 - - id: 427 - name: SKU427 - - id: 428 - name: SKU428 - - id: 429 - name: SKU429 - - id: 430 - name: SKU430 - - id: 431 - name: SKU431 - - id: 432 - name: SKU432 - - id: 433 - name: SKU433 - - id: 434 - name: SKU434 - - id: 435 - name: SKU435 - - id: 436 - name: SKU436 - - id: 437 - name: SKU437 - - id: 438 - name: SKU438 - - id: 439 - name: SKU439 - - id: 440 - name: SKU440 - - id: 441 - name: SKU441 - - id: 442 - name: SKU442 - - id: 443 - name: SKU443 - - id: 444 - name: SKU444 - - id: 445 - name: SKU445 - - id: 446 - name: SKU446 - - id: 447 - name: SKU447 - - id: 448 - name: SKU448 - - id: 449 - name: SKU449 - - id: 450 - name: SKU450 - - id: 451 - name: SKU451 - - id: 452 - name: SKU452 - - id: 453 - name: SKU453 - - id: 454 - name: SKU454 - - id: 455 - name: SKU455 - - id: 456 - name: SKU456 - - id: 457 - name: SKU457 - - id: 458 - name: SKU458 - - id: 459 - name: SKU459 - - id: 460 - name: SKU460 - - id: 461 - name: SKU461 - - id: 462 - name: SKU462 - - id: 463 - name: SKU463 - - id: 464 - name: SKU464 - - id: 465 - name: SKU465 - - id: 466 - name: SKU466 - - id: 467 - name: SKU467 - - id: 468 - name: SKU468 - - id: 469 - name: SKU469 - - id: 470 - name: SKU470 - - id: 471 - name: SKU471 - - id: 472 - name: SKU472 - - id: 473 - name: SKU473 - - id: 474 - name: SKU474 - - id: 475 - name: SKU475 - - id: 476 - name: SKU476 - - id: 477 - name: SKU477 - - id: 478 - name: SKU478 - - id: 479 - name: SKU479 - - id: 480 - name: SKU480 - - id: 481 - name: SKU481 - - id: 482 - name: SKU482 - - id: 483 - name: SKU483 - - id: 484 - name: SKU484 - - id: 485 - name: SKU485 - - id: 486 - name: SKU486 - - id: 487 - name: SKU487 - - id: 488 - name: SKU488 - - id: 489 - name: SKU489 - - id: 490 - name: SKU490 - - id: 491 - name: SKU491 - - id: 492 - name: SKU492 - - id: 493 - name: SKU493 - - id: 494 - name: SKU494 - - id: 495 - name: SKU495 - - id: 496 - name: SKU496 - - id: 497 - name: SKU497 - - id: 498 - name: SKU498 - - id: 499 - name: SKU499 - - id: 500 - name: SKU500 - - id: 501 - name: SKU501 - - id: 502 - name: SKU502 - - id: 503 - name: SKU503 - - id: 504 - name: SKU504 - - id: 505 - name: SKU505 - - id: 506 - name: SKU506 - - id: 507 - name: SKU507 - - id: 508 - name: SKU508 - - id: 509 - name: SKU509 - - id: 510 - name: SKU510 - - id: 511 - name: SKU511 - - id: 512 - name: SKU512 - - id: 513 - name: SKU513 - - id: 514 - name: SKU514 - - id: 515 - name: SKU515 - - id: 516 - name: SKU516 - - id: 517 - name: SKU517 - - id: 518 - name: SKU518 - - id: 519 - name: SKU519 - - id: 520 - name: SKU520 - - id: 521 - name: SKU521 - - id: 522 - name: SKU522 - - id: 523 - name: SKU523 - - id: 524 - name: SKU524 - - id: 525 - name: SKU525 - - id: 526 - name: SKU526 - - id: 527 - name: SKU527 - - id: 528 - name: SKU528 - - id: 529 - name: SKU529 - - id: 530 - name: SKU530 - - id: 531 - name: SKU531 - - id: 532 - name: SKU532 - - id: 533 - name: SKU533 - - id: 534 - name: SKU534 - - id: 535 - name: SKU535 - - id: 536 - name: SKU536 - - id: 537 - name: SKU537 - - id: 538 - name: SKU538 - - id: 539 - name: SKU539 - - id: 540 - name: SKU540 - - id: 541 - name: SKU541 - - id: 542 - name: SKU542 - - id: 543 - name: SKU543 - - id: 544 - name: SKU544 - - id: 545 - name: SKU545 - - id: 546 - name: SKU546 - - id: 547 - name: SKU547 - - id: 548 - name: SKU548 - - id: 549 - name: SKU549 - - id: 550 - name: SKU550 - - id: 551 - name: SKU551 - - id: 552 - name: SKU552 - - id: 553 - name: SKU553 - - id: 554 - name: SKU554 - - id: 555 - name: SKU555 - - id: 556 - name: SKU556 - - id: 557 - name: SKU557 - - id: 558 - name: SKU558 - - id: 559 - name: SKU559 - - id: 560 - name: SKU560 - - id: 561 - name: SKU561 - - id: 562 - name: SKU562 - - id: 563 - name: SKU563 - - id: 564 - name: SKU564 - - id: 565 - name: SKU565 - - id: 566 - name: SKU566 - - id: 567 - name: SKU567 - - id: 568 - name: SKU568 - - id: 569 - name: SKU569 - - id: 570 - name: SKU570 - - id: 571 - name: SKU571 - - id: 572 - name: SKU572 - - id: 573 - name: SKU573 - - id: 574 - name: SKU574 - - id: 575 - name: SKU575 - - id: 576 - name: SKU576 - - id: 577 - name: SKU577 - - id: 578 - name: SKU578 - - id: 579 - name: SKU579 - - id: 580 - name: SKU580 - - id: 581 - name: SKU581 - - id: 582 - name: SKU582 - - id: 583 - name: SKU583 - - id: 584 - name: SKU584 - - id: 585 - name: SKU585 - - id: 586 - name: SKU586 - - id: 587 - name: SKU587 - - id: 588 - name: SKU588 - - id: 589 - name: SKU589 - - id: 590 - name: SKU590 - - id: 591 - name: SKU591 - - id: 592 - name: SKU592 - - id: 593 - name: SKU593 - - id: 594 - name: SKU594 - - id: 595 - name: SKU595 - - id: 596 - name: SKU596 - - id: 597 - name: SKU597 - - id: 598 - name: SKU598 - - id: 599 - name: SKU599 - - id: 600 - name: SKU600 - - id: 601 - name: SKU601 - - id: 602 - name: SKU602 - - id: 603 - name: SKU603 - - id: 604 - name: SKU604 - - id: 605 - name: SKU605 - - id: 606 - name: SKU606 - - id: 607 - name: SKU607 - - id: 608 - name: SKU608 - - id: 609 - name: SKU609 - - id: 610 - name: SKU610 - - id: 611 - name: SKU611 - - id: 612 - name: SKU612 - - id: 613 - name: SKU613 - - id: 614 - name: SKU614 - - id: 615 - name: SKU615 - - id: 616 - name: SKU616 - - id: 617 - name: SKU617 - - id: 618 - name: SKU618 - - id: 619 - name: SKU619 - - id: 620 - name: SKU620 - - id: 621 - name: SKU621 - - id: 622 - name: SKU622 - - id: 623 - name: SKU623 - - id: 624 - name: SKU624 - - id: 625 - name: SKU625 - - id: 626 - name: SKU626 - - id: 627 - name: SKU627 - - id: 628 - name: SKU628 - - id: 629 - name: SKU629 - - id: 630 - name: SKU630 - - id: 631 - name: SKU631 - - id: 632 - name: SKU632 - - id: 633 - name: SKU633 - - id: 634 - name: SKU634 - - id: 635 - name: SKU635 - - id: 636 - name: SKU636 - - id: 637 - name: SKU637 - - id: 638 - name: SKU638 - - id: 639 - name: SKU639 - - id: 640 - name: SKU640 - - id: 641 - name: SKU641 - - id: 642 - name: SKU642 - - id: 643 - name: SKU643 - - id: 644 - name: SKU644 - - id: 645 - name: SKU645 - - id: 646 - name: SKU646 - - id: 647 - name: SKU647 - - id: 648 - name: SKU648 - - id: 649 - name: SKU649 - - id: 650 - name: SKU650 - - id: 651 - name: SKU651 - - id: 652 - name: SKU652 - - id: 653 - name: SKU653 - - id: 654 - name: SKU654 - - id: 655 - name: SKU655 - - id: 656 - name: SKU656 - - id: 657 - name: SKU657 - - id: 658 - name: SKU658 - - id: 659 - name: SKU659 - - id: 660 - name: SKU660 - - id: 661 - name: SKU661 - - id: 662 - name: SKU662 - - id: 663 - name: SKU663 - - id: 664 - name: SKU664 - - id: 665 - name: SKU665 - - id: 666 - name: SKU666 - - id: 667 - name: SKU667 - - id: 668 - name: SKU668 - - id: 669 - name: SKU669 - - id: 670 - name: SKU670 - - id: 671 - name: SKU671 - - id: 672 - name: SKU672 - - id: 673 - name: SKU673 - - id: 674 - name: SKU674 - - id: 675 - name: SKU675 - - id: 676 - name: SKU676 - - id: 677 - name: SKU677 - - id: 678 - name: SKU678 - - id: 679 - name: SKU679 - - id: 680 - name: SKU680 - - id: 681 - name: SKU681 - - id: 682 - name: SKU682 - - id: 683 - name: SKU683 - - id: 684 - name: SKU684 - - id: 685 - name: SKU685 - - id: 686 - name: SKU686 - - id: 687 - name: SKU687 - - id: 688 - name: SKU688 - - id: 689 - name: SKU689 - - id: 690 - name: SKU690 - - id: 691 - name: SKU691 - - id: 692 - name: SKU692 - - id: 693 - name: SKU693 - - id: 694 - name: SKU694 - - id: 695 - name: SKU695 - - id: 696 - name: SKU696 - - id: 697 - name: SKU697 - - id: 698 - name: SKU698 - - id: 699 - name: SKU699 - - id: 700 - name: SKU700 - - id: 701 - name: SKU701 - - id: 702 - name: SKU702 - - id: 703 - name: SKU703 - - id: 704 - name: SKU704 - - id: 705 - name: SKU705 - - id: 706 - name: SKU706 - - id: 707 - name: SKU707 - - id: 708 - name: SKU708 - - id: 709 - name: SKU709 - - id: 710 - name: SKU710 - - id: 711 - name: SKU711 - - id: 712 - name: SKU712 - - id: 713 - name: SKU713 - - id: 714 - name: SKU714 - - id: 715 - name: SKU715 - - id: 716 - name: SKU716 - - id: 717 - name: SKU717 - - id: 718 - name: SKU718 - - id: 719 - name: SKU719 - - id: 720 - name: SKU720 - - id: 721 - name: SKU721 - - id: 722 - name: SKU722 - - id: 723 - name: SKU723 - - id: 724 - name: SKU724 - - id: 725 - name: SKU725 - - id: 726 - name: SKU726 - - id: 727 - name: SKU727 - - id: 728 - name: SKU728 - - id: 729 - name: SKU729 - - id: 730 - name: SKU730 - - id: 731 - name: SKU731 - - id: 732 - name: SKU732 - - id: 733 - name: SKU733 - - id: 734 - name: SKU734 - - id: 735 - name: SKU735 - - id: 736 - name: SKU736 - - id: 737 - name: SKU737 - - id: 738 - name: SKU738 - - id: 739 - name: SKU739 - - id: 740 - name: SKU740 - - id: 741 - name: SKU741 - - id: 742 - name: SKU742 - - id: 743 - name: SKU743 - - id: 744 - name: SKU744 - - id: 745 - name: SKU745 - - id: 746 - name: SKU746 - - id: 747 - name: SKU747 - - id: 748 - name: SKU748 - - id: 749 - name: SKU749 - - id: 750 - name: SKU750 - - id: 751 - name: SKU751 - - id: 752 - name: SKU752 - - id: 753 - name: SKU753 - - id: 754 - name: SKU754 - - id: 755 - name: SKU755 - - id: 756 - name: SKU756 - - id: 757 - name: SKU757 - - id: 758 - name: SKU758 - - id: 759 - name: SKU759 - - id: 760 - name: SKU760 - - id: 761 - name: SKU761 - - id: 762 - name: SKU762 - - id: 763 - name: SKU763 - - id: 764 - name: SKU764 - - id: 765 - name: SKU765 - - id: 766 - name: SKU766 - - id: 767 - name: SKU767 - - id: 768 - name: SKU768 - - id: 769 - name: SKU769 - - id: 770 - name: SKU770 - - id: 771 - name: SKU771 - - id: 772 - name: SKU772 - - id: 773 - name: SKU773 - - id: 774 - name: SKU774 - - id: 775 - name: SKU775 - - id: 776 - name: SKU776 - - id: 777 - name: SKU777 - - id: 778 - name: SKU778 - - id: 779 - name: SKU779 - - id: 780 - name: SKU780 - - id: 781 - name: SKU781 - - id: 782 - name: SKU782 - - id: 783 - name: SKU783 - - id: 784 - name: SKU784 - - id: 785 - name: SKU785 - - id: 786 - name: SKU786 - - id: 787 - name: SKU787 - - id: 788 - name: SKU788 - - id: 789 - name: SKU789 - - id: 790 - name: SKU790 - - id: 791 - name: SKU791 - - id: 792 - name: SKU792 - - id: 793 - name: SKU793 - - id: 794 - name: SKU794 - - id: 795 - name: SKU795 - - id: 796 - name: SKU796 - - id: 797 - name: SKU797 - - id: 798 - name: SKU798 - - id: 799 - name: SKU799 - - id: 800 - name: SKU800 - - id: 801 - name: SKU801 - - id: 802 - name: SKU802 - - id: 803 - name: SKU803 - - id: 804 - name: SKU804 - - id: 805 - name: SKU805 - - id: 806 - name: SKU806 - - id: 807 - name: SKU807 - - id: 808 - name: SKU808 - - id: 809 - name: SKU809 - - id: 810 - name: SKU810 - - id: 811 - name: SKU811 - - id: 812 - name: SKU812 - - id: 813 - name: SKU813 - - id: 814 - name: SKU814 - - id: 815 - name: SKU815 - - id: 816 - name: SKU816 - - id: 817 - name: SKU817 - - id: 818 - name: SKU818 - - id: 819 - name: SKU819 - - id: 820 - name: SKU820 - - id: 821 - name: SKU821 - - id: 822 - name: SKU822 - - id: 823 - name: SKU823 - - id: 824 - name: SKU824 - - id: 825 - name: SKU825 - - id: 826 - name: SKU826 - - id: 827 - name: SKU827 - - id: 828 - name: SKU828 - - id: 829 - name: SKU829 - - id: 830 - name: SKU830 - - id: 831 - name: SKU831 - - id: 832 - name: SKU832 - - id: 833 - name: SKU833 - - id: 834 - name: SKU834 - - id: 835 - name: SKU835 - - id: 836 - name: SKU836 - - id: 837 - name: SKU837 - - id: 838 - name: SKU838 - - id: 839 - name: SKU839 - - id: 840 - name: SKU840 - - id: 841 - name: SKU841 - - id: 842 - name: SKU842 - - id: 843 - name: SKU843 - - id: 844 - name: SKU844 - - id: 845 - name: SKU845 - - id: 846 - name: SKU846 - - id: 847 - name: SKU847 - - id: 848 - name: SKU848 - - id: 849 - name: SKU849 - - id: 850 - name: SKU850 - - id: 851 - name: SKU851 - - id: 852 - name: SKU852 - - id: 853 - name: SKU853 - - id: 854 - name: SKU854 - - id: 855 - name: SKU855 - - id: 856 - name: SKU856 - - id: 857 - name: SKU857 - - id: 858 - name: SKU858 - - id: 859 - name: SKU859 - - id: 860 - name: SKU860 - - id: 861 - name: SKU861 - - id: 862 - name: SKU862 - - id: 863 - name: SKU863 - - id: 864 - name: SKU864 - - id: 865 - name: SKU865 - - id: 866 - name: SKU866 - - id: 867 - name: SKU867 - - id: 868 - name: SKU868 - - id: 869 - name: SKU869 - - id: 870 - name: SKU870 - - id: 871 - name: SKU871 - - id: 872 - name: SKU872 - - id: 873 - name: SKU873 - - id: 874 - name: SKU874 - - id: 875 - name: SKU875 - - id: 876 - name: SKU876 - - id: 877 - name: SKU877 - - id: 878 - name: SKU878 - - id: 879 - name: SKU879 - - id: 880 - name: SKU880 - - id: 881 - name: SKU881 - - id: 882 - name: SKU882 - - id: 883 - name: SKU883 - - id: 884 - name: SKU884 - - id: 885 - name: SKU885 - - id: 886 - name: SKU886 - - id: 887 - name: SKU887 - - id: 888 - name: SKU888 - - id: 889 - name: SKU889 - - id: 890 - name: SKU890 - - id: 891 - name: SKU891 - - id: 892 - name: SKU892 - - id: 893 - name: SKU893 - - id: 894 - name: SKU894 - - id: 895 - name: SKU895 - - id: 896 - name: SKU896 - - id: 897 - name: SKU897 - - id: 898 - name: SKU898 - - id: 899 - name: SKU899 - - id: 900 - name: SKU900 - - id: 901 - name: SKU901 - - id: 902 - name: SKU902 - - id: 903 - name: SKU903 - - id: 904 - name: SKU904 - - id: 905 - name: SKU905 - - id: 906 - name: SKU906 - - id: 907 - name: SKU907 - - id: 908 - name: SKU908 - - id: 909 - name: SKU909 - - id: 910 - name: SKU910 - - id: 911 - name: SKU911 - - id: 912 - name: SKU912 - - id: 913 - name: SKU913 - - id: 914 - name: SKU914 - - id: 915 - name: SKU915 - - id: 916 - name: SKU916 - - id: 917 - name: SKU917 - - id: 918 - name: SKU918 - - id: 919 - name: SKU919 - - id: 920 - name: SKU920 - - id: 921 - name: SKU921 - - id: 922 - name: SKU922 - - id: 923 - name: SKU923 - - id: 924 - name: SKU924 - - id: 925 - name: SKU925 - - id: 926 - name: SKU926 - - id: 927 - name: SKU927 - - id: 928 - name: SKU928 - - id: 929 - name: SKU929 - - id: 930 - name: SKU930 - - id: 931 - name: SKU931 - - id: 932 - name: SKU932 - - id: 933 - name: SKU933 - - id: 934 - name: SKU934 - - id: 935 - name: SKU935 - - id: 936 - name: SKU936 - - id: 937 - name: SKU937 - - id: 938 - name: SKU938 - - id: 939 - name: SKU939 - - id: 940 - name: SKU940 - - id: 941 - name: SKU941 - - id: 942 - name: SKU942 - - id: 943 - name: SKU943 - - id: 944 - name: SKU944 - - id: 945 - name: SKU945 - - id: 946 - name: SKU946 - - id: 947 - name: SKU947 - - id: 948 - name: SKU948 - - id: 949 - name: SKU949 - - id: 950 - name: SKU950 - - id: 951 - name: SKU951 - - id: 952 - name: SKU952 - - id: 953 - name: SKU953 - - id: 954 - name: SKU954 - - id: 955 - name: SKU955 - - id: 956 - name: SKU956 - - id: 957 - name: SKU957 - - id: 958 - name: SKU958 - - id: 959 - name: SKU959 - - id: 960 - name: SKU960 - - id: 961 - name: SKU961 - - id: 962 - name: SKU962 - - id: 963 - name: SKU963 - - id: 964 - name: SKU964 - - id: 965 - name: SKU965 - - id: 966 - name: SKU966 - - id: 967 - name: SKU967 - - id: 968 - name: SKU968 - - id: 969 - name: SKU969 - - id: 970 - name: SKU970 - - id: 971 - name: SKU971 - - id: 972 - name: SKU972 - - id: 973 - name: SKU973 - - id: 974 - name: SKU974 - - id: 975 - name: SKU975 - - id: 976 - name: SKU976 - - id: 977 - name: SKU977 - - id: 978 - name: SKU978 - - id: 979 - name: SKU979 - - id: 980 - name: SKU980 - - id: 981 - name: SKU981 - - id: 982 - name: SKU982 - - id: 983 - name: SKU983 - - id: 984 - name: SKU984 - - id: 985 - name: SKU985 - - id: 986 - name: SKU986 - - id: 987 - name: SKU987 - - id: 988 - name: SKU988 - - id: 989 - name: SKU989 - - id: 990 - name: SKU990 - - id: 991 - name: SKU991 - - id: 992 - name: SKU992 - - id: 993 - name: SKU993 - - id: 994 - name: SKU994 - - id: 995 - name: SKU995 - - id: 996 - name: SKU996 - - id: 997 - name: SKU997 - - id: 998 - name: SKU998 - - id: 999 - name: SKU999 - topology: - STORE0: - SKU0: - - WAREHOUSE0 - SKU1: - - WAREHOUSE0 - SKU10: - - WAREHOUSE0 - SKU100: - - WAREHOUSE0 - SKU101: - - WAREHOUSE0 - SKU102: - - WAREHOUSE0 - SKU103: - - WAREHOUSE0 - SKU104: - - WAREHOUSE0 - SKU105: - - WAREHOUSE0 - SKU106: - - WAREHOUSE0 - SKU107: - - WAREHOUSE0 - SKU108: - - WAREHOUSE0 - SKU109: - - WAREHOUSE0 - SKU11: - - WAREHOUSE0 - SKU110: - - WAREHOUSE0 - SKU111: - - WAREHOUSE0 - SKU112: - - WAREHOUSE0 - SKU113: - - WAREHOUSE0 - SKU114: - - WAREHOUSE0 - SKU115: - - WAREHOUSE0 - SKU116: - - WAREHOUSE0 - SKU117: - - WAREHOUSE0 - SKU118: - - WAREHOUSE0 - SKU119: - - WAREHOUSE0 - SKU12: - - WAREHOUSE0 - SKU120: - - WAREHOUSE0 - SKU121: - - WAREHOUSE0 - SKU122: - - WAREHOUSE0 - SKU123: - - WAREHOUSE0 - SKU124: - - WAREHOUSE0 - SKU125: - - WAREHOUSE0 - SKU126: - - WAREHOUSE0 - SKU127: - - WAREHOUSE0 - SKU128: - - WAREHOUSE0 - SKU129: - - WAREHOUSE0 - SKU13: - - WAREHOUSE0 - SKU130: - - WAREHOUSE0 - SKU131: - - WAREHOUSE0 - SKU132: - - WAREHOUSE0 - SKU133: - - WAREHOUSE0 - SKU134: - - WAREHOUSE0 - SKU135: - - WAREHOUSE0 - SKU136: - - WAREHOUSE0 - SKU137: - - WAREHOUSE0 - SKU138: - - WAREHOUSE0 - SKU139: - - WAREHOUSE0 - SKU14: - - WAREHOUSE0 - SKU140: - - WAREHOUSE0 - SKU141: - - WAREHOUSE0 - SKU142: - - WAREHOUSE0 - SKU143: - - WAREHOUSE0 - SKU144: - - WAREHOUSE0 - SKU145: - - WAREHOUSE0 - SKU146: - - WAREHOUSE0 - SKU147: - - WAREHOUSE0 - SKU148: - - WAREHOUSE0 - SKU149: - - WAREHOUSE0 - SKU15: - - WAREHOUSE0 - SKU150: - - WAREHOUSE0 - SKU151: - - WAREHOUSE0 - SKU152: - - WAREHOUSE0 - SKU153: - - WAREHOUSE0 - SKU154: - - WAREHOUSE0 - SKU155: - - WAREHOUSE0 - SKU156: - - WAREHOUSE0 - SKU157: - - WAREHOUSE0 - SKU158: - - WAREHOUSE0 - SKU159: - - WAREHOUSE0 - SKU16: - - WAREHOUSE0 - SKU160: - - WAREHOUSE0 - SKU161: - - WAREHOUSE0 - SKU162: - - WAREHOUSE0 - SKU163: - - WAREHOUSE0 - SKU164: - - WAREHOUSE0 - SKU165: - - WAREHOUSE0 - SKU166: - - WAREHOUSE0 - SKU167: - - WAREHOUSE0 - SKU168: - - WAREHOUSE0 - SKU169: - - WAREHOUSE0 - SKU17: - - WAREHOUSE0 - SKU170: - - WAREHOUSE0 - SKU171: - - WAREHOUSE0 - SKU172: - - WAREHOUSE0 - SKU173: - - WAREHOUSE0 - SKU174: - - WAREHOUSE0 - SKU175: - - WAREHOUSE0 - SKU176: - - WAREHOUSE0 - SKU177: - - WAREHOUSE0 - SKU178: - - WAREHOUSE0 - SKU179: - - WAREHOUSE0 - SKU18: - - WAREHOUSE0 - SKU180: - - WAREHOUSE0 - SKU181: - - WAREHOUSE0 - SKU182: - - WAREHOUSE0 - SKU183: - - WAREHOUSE0 - SKU184: - - WAREHOUSE0 - SKU185: - - WAREHOUSE0 - SKU186: - - WAREHOUSE0 - SKU187: - - WAREHOUSE0 - SKU188: - - WAREHOUSE0 - SKU189: - - WAREHOUSE0 - SKU19: - - WAREHOUSE0 - SKU190: - - WAREHOUSE0 - SKU191: - - WAREHOUSE0 - SKU192: - - WAREHOUSE0 - SKU193: - - WAREHOUSE0 - SKU194: - - WAREHOUSE0 - SKU195: - - WAREHOUSE0 - SKU196: - - WAREHOUSE0 - SKU197: - - WAREHOUSE0 - SKU198: - - WAREHOUSE0 - SKU199: - - WAREHOUSE0 - SKU2: - - WAREHOUSE0 - SKU20: - - WAREHOUSE0 - SKU200: - - WAREHOUSE0 - SKU201: - - WAREHOUSE0 - SKU202: - - WAREHOUSE0 - SKU203: - - WAREHOUSE0 - SKU204: - - WAREHOUSE0 - SKU205: - - WAREHOUSE0 - SKU206: - - WAREHOUSE0 - SKU207: - - WAREHOUSE0 - SKU208: - - WAREHOUSE0 - SKU209: - - WAREHOUSE0 - SKU21: - - WAREHOUSE0 - SKU210: - - WAREHOUSE0 - SKU211: - - WAREHOUSE0 - SKU212: - - WAREHOUSE0 - SKU213: - - WAREHOUSE0 - SKU214: - - WAREHOUSE0 - SKU215: - - WAREHOUSE0 - SKU216: - - WAREHOUSE0 - SKU217: - - WAREHOUSE0 - SKU218: - - WAREHOUSE0 - SKU219: - - WAREHOUSE0 - SKU22: - - WAREHOUSE0 - SKU220: - - WAREHOUSE0 - SKU221: - - WAREHOUSE0 - SKU222: - - WAREHOUSE0 - SKU223: - - WAREHOUSE0 - SKU224: - - WAREHOUSE0 - SKU225: - - WAREHOUSE0 - SKU226: - - WAREHOUSE0 - SKU227: - - WAREHOUSE0 - SKU228: - - WAREHOUSE0 - SKU229: - - WAREHOUSE0 - SKU23: - - WAREHOUSE0 - SKU230: - - WAREHOUSE0 - SKU231: - - WAREHOUSE0 - SKU232: - - WAREHOUSE0 - SKU233: - - WAREHOUSE0 - SKU234: - - WAREHOUSE0 - SKU235: - - WAREHOUSE0 - SKU236: - - WAREHOUSE0 - SKU237: - - WAREHOUSE0 - SKU238: - - WAREHOUSE0 - SKU239: - - WAREHOUSE0 - SKU24: - - WAREHOUSE0 - SKU240: - - WAREHOUSE0 - SKU241: - - WAREHOUSE0 - SKU242: - - WAREHOUSE0 - SKU243: - - WAREHOUSE0 - SKU244: - - WAREHOUSE0 - SKU245: - - WAREHOUSE0 - SKU246: - - WAREHOUSE0 - SKU247: - - WAREHOUSE0 - SKU248: - - WAREHOUSE0 - SKU249: - - WAREHOUSE0 - SKU25: - - WAREHOUSE0 - SKU250: - - WAREHOUSE0 - SKU251: - - WAREHOUSE0 - SKU252: - - WAREHOUSE0 - SKU253: - - WAREHOUSE0 - SKU254: - - WAREHOUSE0 - SKU255: - - WAREHOUSE0 - SKU256: - - WAREHOUSE0 - SKU257: - - WAREHOUSE0 - SKU258: - - WAREHOUSE0 - SKU259: - - WAREHOUSE0 - SKU26: - - WAREHOUSE0 - SKU260: - - WAREHOUSE0 - SKU261: - - WAREHOUSE0 - SKU262: - - WAREHOUSE0 - SKU263: - - WAREHOUSE0 - SKU264: - - WAREHOUSE0 - SKU265: - - WAREHOUSE0 - SKU266: - - WAREHOUSE0 - SKU267: - - WAREHOUSE0 - SKU268: - - WAREHOUSE0 - SKU269: - - WAREHOUSE0 - SKU27: - - WAREHOUSE0 - SKU270: - - WAREHOUSE0 - SKU271: - - WAREHOUSE0 - SKU272: - - WAREHOUSE0 - SKU273: - - WAREHOUSE0 - SKU274: - - WAREHOUSE0 - SKU275: - - WAREHOUSE0 - SKU276: - - WAREHOUSE0 - SKU277: - - WAREHOUSE0 - SKU278: - - WAREHOUSE0 - SKU279: - - WAREHOUSE0 - SKU28: - - WAREHOUSE0 - SKU280: - - WAREHOUSE0 - SKU281: - - WAREHOUSE0 - SKU282: - - WAREHOUSE0 - SKU283: - - WAREHOUSE0 - SKU284: - - WAREHOUSE0 - SKU285: - - WAREHOUSE0 - SKU286: - - WAREHOUSE0 - SKU287: - - WAREHOUSE0 - SKU288: - - WAREHOUSE0 - SKU289: - - WAREHOUSE0 - SKU29: - - WAREHOUSE0 - SKU290: - - WAREHOUSE0 - SKU291: - - WAREHOUSE0 - SKU292: - - WAREHOUSE0 - SKU293: - - WAREHOUSE0 - SKU294: - - WAREHOUSE0 - SKU295: - - WAREHOUSE0 - SKU296: - - WAREHOUSE0 - SKU297: - - WAREHOUSE0 - SKU298: - - WAREHOUSE0 - SKU299: - - WAREHOUSE0 - SKU3: - - WAREHOUSE0 - SKU30: - - WAREHOUSE0 - SKU300: - - WAREHOUSE0 - SKU301: - - WAREHOUSE0 - SKU302: - - WAREHOUSE0 - SKU303: - - WAREHOUSE0 - SKU304: - - WAREHOUSE0 - SKU305: - - WAREHOUSE0 - SKU306: - - WAREHOUSE0 - SKU307: - - WAREHOUSE0 - SKU308: - - WAREHOUSE0 - SKU309: - - WAREHOUSE0 - SKU31: - - WAREHOUSE0 - SKU310: - - WAREHOUSE0 - SKU311: - - WAREHOUSE0 - SKU312: - - WAREHOUSE0 - SKU313: - - WAREHOUSE0 - SKU314: - - WAREHOUSE0 - SKU315: - - WAREHOUSE0 - SKU316: - - WAREHOUSE0 - SKU317: - - WAREHOUSE0 - SKU318: - - WAREHOUSE0 - SKU319: - - WAREHOUSE0 - SKU32: - - WAREHOUSE0 - SKU320: - - WAREHOUSE0 - SKU321: - - WAREHOUSE0 - SKU322: - - WAREHOUSE0 - SKU323: - - WAREHOUSE0 - SKU324: - - WAREHOUSE0 - SKU325: - - WAREHOUSE0 - SKU326: - - WAREHOUSE0 - SKU327: - - WAREHOUSE0 - SKU328: - - WAREHOUSE0 - SKU329: - - WAREHOUSE0 - SKU33: - - WAREHOUSE0 - SKU330: - - WAREHOUSE0 - SKU331: - - WAREHOUSE0 - SKU332: - - WAREHOUSE0 - SKU333: - - WAREHOUSE0 - SKU334: - - WAREHOUSE0 - SKU335: - - WAREHOUSE0 - SKU336: - - WAREHOUSE0 - SKU337: - - WAREHOUSE0 - SKU338: - - WAREHOUSE0 - SKU339: - - WAREHOUSE0 - SKU34: - - WAREHOUSE0 - SKU340: - - WAREHOUSE0 - SKU341: - - WAREHOUSE0 - SKU342: - - WAREHOUSE0 - SKU343: - - WAREHOUSE0 - SKU344: - - WAREHOUSE0 - SKU345: - - WAREHOUSE0 - SKU346: - - WAREHOUSE0 - SKU347: - - WAREHOUSE0 - SKU348: - - WAREHOUSE0 - SKU349: - - WAREHOUSE0 - SKU35: - - WAREHOUSE0 - SKU350: - - WAREHOUSE0 - SKU351: - - WAREHOUSE0 - SKU352: - - WAREHOUSE0 - SKU353: - - WAREHOUSE0 - SKU354: - - WAREHOUSE0 - SKU355: - - WAREHOUSE0 - SKU356: - - WAREHOUSE0 - SKU357: - - WAREHOUSE0 - SKU358: - - WAREHOUSE0 - SKU359: - - WAREHOUSE0 - SKU36: - - WAREHOUSE0 - SKU360: - - WAREHOUSE0 - SKU361: - - WAREHOUSE0 - SKU362: - - WAREHOUSE0 - SKU363: - - WAREHOUSE0 - SKU364: - - WAREHOUSE0 - SKU365: - - WAREHOUSE0 - SKU366: - - WAREHOUSE0 - SKU367: - - WAREHOUSE0 - SKU368: - - WAREHOUSE0 - SKU369: - - WAREHOUSE0 - SKU37: - - WAREHOUSE0 - SKU370: - - WAREHOUSE0 - SKU371: - - WAREHOUSE0 - SKU372: - - WAREHOUSE0 - SKU373: - - WAREHOUSE0 - SKU374: - - WAREHOUSE0 - SKU375: - - WAREHOUSE0 - SKU376: - - WAREHOUSE0 - SKU377: - - WAREHOUSE0 - SKU378: - - WAREHOUSE0 - SKU379: - - WAREHOUSE0 - SKU38: - - WAREHOUSE0 - SKU380: - - WAREHOUSE0 - SKU381: - - WAREHOUSE0 - SKU382: - - WAREHOUSE0 - SKU383: - - WAREHOUSE0 - SKU384: - - WAREHOUSE0 - SKU385: - - WAREHOUSE0 - SKU386: - - WAREHOUSE0 - SKU387: - - WAREHOUSE0 - SKU388: - - WAREHOUSE0 - SKU389: - - WAREHOUSE0 - SKU39: - - WAREHOUSE0 - SKU390: - - WAREHOUSE0 - SKU391: - - WAREHOUSE0 - SKU392: - - WAREHOUSE0 - SKU393: - - WAREHOUSE0 - SKU394: - - WAREHOUSE0 - SKU395: - - WAREHOUSE0 - SKU396: - - WAREHOUSE0 - SKU397: - - WAREHOUSE0 - SKU398: - - WAREHOUSE0 - SKU399: - - WAREHOUSE0 - SKU4: - - WAREHOUSE0 - SKU40: - - WAREHOUSE0 - SKU400: - - WAREHOUSE0 - SKU401: - - WAREHOUSE0 - SKU402: - - WAREHOUSE0 - SKU403: - - WAREHOUSE0 - SKU404: - - WAREHOUSE0 - SKU405: - - WAREHOUSE0 - SKU406: - - WAREHOUSE0 - SKU407: - - WAREHOUSE0 - SKU408: - - WAREHOUSE0 - SKU409: - - WAREHOUSE0 - SKU41: - - WAREHOUSE0 - SKU410: - - WAREHOUSE0 - SKU411: - - WAREHOUSE0 - SKU412: - - WAREHOUSE0 - SKU413: - - WAREHOUSE0 - SKU414: - - WAREHOUSE0 - SKU415: - - WAREHOUSE0 - SKU416: - - WAREHOUSE0 - SKU417: - - WAREHOUSE0 - SKU418: - - WAREHOUSE0 - SKU419: - - WAREHOUSE0 - SKU42: - - WAREHOUSE0 - SKU420: - - WAREHOUSE0 - SKU421: - - WAREHOUSE0 - SKU422: - - WAREHOUSE0 - SKU423: - - WAREHOUSE0 - SKU424: - - WAREHOUSE0 - SKU425: - - WAREHOUSE0 - SKU426: - - WAREHOUSE0 - SKU427: - - WAREHOUSE0 - SKU428: - - WAREHOUSE0 - SKU429: - - WAREHOUSE0 - SKU43: - - WAREHOUSE0 - SKU430: - - WAREHOUSE0 - SKU431: - - WAREHOUSE0 - SKU432: - - WAREHOUSE0 - SKU433: - - WAREHOUSE0 - SKU434: - - WAREHOUSE0 - SKU435: - - WAREHOUSE0 - SKU436: - - WAREHOUSE0 - SKU437: - - WAREHOUSE0 - SKU438: - - WAREHOUSE0 - SKU439: - - WAREHOUSE0 - SKU44: - - WAREHOUSE0 - SKU440: - - WAREHOUSE0 - SKU441: - - WAREHOUSE0 - SKU442: - - WAREHOUSE0 - SKU443: - - WAREHOUSE0 - SKU444: - - WAREHOUSE0 - SKU445: - - WAREHOUSE0 - SKU446: - - WAREHOUSE0 - SKU447: - - WAREHOUSE0 - SKU448: - - WAREHOUSE0 - SKU449: - - WAREHOUSE0 - SKU45: - - WAREHOUSE0 - SKU450: - - WAREHOUSE0 - SKU451: - - WAREHOUSE0 - SKU452: - - WAREHOUSE0 - SKU453: - - WAREHOUSE0 - SKU454: - - WAREHOUSE0 - SKU455: - - WAREHOUSE0 - SKU456: - - WAREHOUSE0 - SKU457: - - WAREHOUSE0 - SKU458: - - WAREHOUSE0 - SKU459: - - WAREHOUSE0 - SKU46: - - WAREHOUSE0 - SKU460: - - WAREHOUSE0 - SKU461: - - WAREHOUSE0 - SKU462: - - WAREHOUSE0 - SKU463: - - WAREHOUSE0 - SKU464: - - WAREHOUSE0 - SKU465: - - WAREHOUSE0 - SKU466: - - WAREHOUSE0 - SKU467: - - WAREHOUSE0 - SKU468: - - WAREHOUSE0 - SKU469: - - WAREHOUSE0 - SKU47: - - WAREHOUSE0 - SKU470: - - WAREHOUSE0 - SKU471: - - WAREHOUSE0 - SKU472: - - WAREHOUSE0 - SKU473: - - WAREHOUSE0 - SKU474: - - WAREHOUSE0 - SKU475: - - WAREHOUSE0 - SKU476: - - WAREHOUSE0 - SKU477: - - WAREHOUSE0 - SKU478: - - WAREHOUSE0 - SKU479: - - WAREHOUSE0 - SKU48: - - WAREHOUSE0 - SKU480: - - WAREHOUSE0 - SKU481: - - WAREHOUSE0 - SKU482: - - WAREHOUSE0 - SKU483: - - WAREHOUSE0 - SKU484: - - WAREHOUSE0 - SKU485: - - WAREHOUSE0 - SKU486: - - WAREHOUSE0 - SKU487: - - WAREHOUSE0 - SKU488: - - WAREHOUSE0 - SKU489: - - WAREHOUSE0 - SKU49: - - WAREHOUSE0 - SKU490: - - WAREHOUSE0 - SKU491: - - WAREHOUSE0 - SKU492: - - WAREHOUSE0 - SKU493: - - WAREHOUSE0 - SKU494: - - WAREHOUSE0 - SKU495: - - WAREHOUSE0 - SKU496: - - WAREHOUSE0 - SKU497: - - WAREHOUSE0 - SKU498: - - WAREHOUSE0 - SKU499: - - WAREHOUSE0 - SKU5: - - WAREHOUSE0 - SKU50: - - WAREHOUSE0 - SKU500: - - WAREHOUSE0 - SKU501: - - WAREHOUSE0 - SKU502: - - WAREHOUSE0 - SKU503: - - WAREHOUSE0 - SKU504: - - WAREHOUSE0 - SKU505: - - WAREHOUSE0 - SKU506: - - WAREHOUSE0 - SKU507: - - WAREHOUSE0 - SKU508: - - WAREHOUSE0 - SKU509: - - WAREHOUSE0 - SKU51: - - WAREHOUSE0 - SKU510: - - WAREHOUSE0 - SKU511: - - WAREHOUSE0 - SKU512: - - WAREHOUSE0 - SKU513: - - WAREHOUSE0 - SKU514: - - WAREHOUSE0 - SKU515: - - WAREHOUSE0 - SKU516: - - WAREHOUSE0 - SKU517: - - WAREHOUSE0 - SKU518: - - WAREHOUSE0 - SKU519: - - WAREHOUSE0 - SKU52: - - WAREHOUSE0 - SKU520: - - WAREHOUSE0 - SKU521: - - WAREHOUSE0 - SKU522: - - WAREHOUSE0 - SKU523: - - WAREHOUSE0 - SKU524: - - WAREHOUSE0 - SKU525: - - WAREHOUSE0 - SKU526: - - WAREHOUSE0 - SKU527: - - WAREHOUSE0 - SKU528: - - WAREHOUSE0 - SKU529: - - WAREHOUSE0 - SKU53: - - WAREHOUSE0 - SKU530: - - WAREHOUSE0 - SKU531: - - WAREHOUSE0 - SKU532: - - WAREHOUSE0 - SKU533: - - WAREHOUSE0 - SKU534: - - WAREHOUSE0 - SKU535: - - WAREHOUSE0 - SKU536: - - WAREHOUSE0 - SKU537: - - WAREHOUSE0 - SKU538: - - WAREHOUSE0 - SKU539: - - WAREHOUSE0 - SKU54: - - WAREHOUSE0 - SKU540: - - WAREHOUSE0 - SKU541: - - WAREHOUSE0 - SKU542: - - WAREHOUSE0 - SKU543: - - WAREHOUSE0 - SKU544: - - WAREHOUSE0 - SKU545: - - WAREHOUSE0 - SKU546: - - WAREHOUSE0 - SKU547: - - WAREHOUSE0 - SKU548: - - WAREHOUSE0 - SKU549: - - WAREHOUSE0 - SKU55: - - WAREHOUSE0 - SKU550: - - WAREHOUSE0 - SKU551: - - WAREHOUSE0 - SKU552: - - WAREHOUSE0 - SKU553: - - WAREHOUSE0 - SKU554: - - WAREHOUSE0 - SKU555: - - WAREHOUSE0 - SKU556: - - WAREHOUSE0 - SKU557: - - WAREHOUSE0 - SKU558: - - WAREHOUSE0 - SKU559: - - WAREHOUSE0 - SKU56: - - WAREHOUSE0 - SKU560: - - WAREHOUSE0 - SKU561: - - WAREHOUSE0 - SKU562: - - WAREHOUSE0 - SKU563: - - WAREHOUSE0 - SKU564: - - WAREHOUSE0 - SKU565: - - WAREHOUSE0 - SKU566: - - WAREHOUSE0 - SKU567: - - WAREHOUSE0 - SKU568: - - WAREHOUSE0 - SKU569: - - WAREHOUSE0 - SKU57: - - WAREHOUSE0 - SKU570: - - WAREHOUSE0 - SKU571: - - WAREHOUSE0 - SKU572: - - WAREHOUSE0 - SKU573: - - WAREHOUSE0 - SKU574: - - WAREHOUSE0 - SKU575: - - WAREHOUSE0 - SKU576: - - WAREHOUSE0 - SKU577: - - WAREHOUSE0 - SKU578: - - WAREHOUSE0 - SKU579: - - WAREHOUSE0 - SKU58: - - WAREHOUSE0 - SKU580: - - WAREHOUSE0 - SKU581: - - WAREHOUSE0 - SKU582: - - WAREHOUSE0 - SKU583: - - WAREHOUSE0 - SKU584: - - WAREHOUSE0 - SKU585: - - WAREHOUSE0 - SKU586: - - WAREHOUSE0 - SKU587: - - WAREHOUSE0 - SKU588: - - WAREHOUSE0 - SKU589: - - WAREHOUSE0 - SKU59: - - WAREHOUSE0 - SKU590: - - WAREHOUSE0 - SKU591: - - WAREHOUSE0 - SKU592: - - WAREHOUSE0 - SKU593: - - WAREHOUSE0 - SKU594: - - WAREHOUSE0 - SKU595: - - WAREHOUSE0 - SKU596: - - WAREHOUSE0 - SKU597: - - WAREHOUSE0 - SKU598: - - WAREHOUSE0 - SKU599: - - WAREHOUSE0 - SKU6: - - WAREHOUSE0 - SKU60: - - WAREHOUSE0 - SKU600: - - WAREHOUSE0 - SKU601: - - WAREHOUSE0 - SKU602: - - WAREHOUSE0 - SKU603: - - WAREHOUSE0 - SKU604: - - WAREHOUSE0 - SKU605: - - WAREHOUSE0 - SKU606: - - WAREHOUSE0 - SKU607: - - WAREHOUSE0 - SKU608: - - WAREHOUSE0 - SKU609: - - WAREHOUSE0 - SKU61: - - WAREHOUSE0 - SKU610: - - WAREHOUSE0 - SKU611: - - WAREHOUSE0 - SKU612: - - WAREHOUSE0 - SKU613: - - WAREHOUSE0 - SKU614: - - WAREHOUSE0 - SKU615: - - WAREHOUSE0 - SKU616: - - WAREHOUSE0 - SKU617: - - WAREHOUSE0 - SKU618: - - WAREHOUSE0 - SKU619: - - WAREHOUSE0 - SKU62: - - WAREHOUSE0 - SKU620: - - WAREHOUSE0 - SKU621: - - WAREHOUSE0 - SKU622: - - WAREHOUSE0 - SKU623: - - WAREHOUSE0 - SKU624: - - WAREHOUSE0 - SKU625: - - WAREHOUSE0 - SKU626: - - WAREHOUSE0 - SKU627: - - WAREHOUSE0 - SKU628: - - WAREHOUSE0 - SKU629: - - WAREHOUSE0 - SKU63: - - WAREHOUSE0 - SKU630: - - WAREHOUSE0 - SKU631: - - WAREHOUSE0 - SKU632: - - WAREHOUSE0 - SKU633: - - WAREHOUSE0 - SKU634: - - WAREHOUSE0 - SKU635: - - WAREHOUSE0 - SKU636: - - WAREHOUSE0 - SKU637: - - WAREHOUSE0 - SKU638: - - WAREHOUSE0 - SKU639: - - WAREHOUSE0 - SKU64: - - WAREHOUSE0 - SKU640: - - WAREHOUSE0 - SKU641: - - WAREHOUSE0 - SKU642: - - WAREHOUSE0 - SKU643: - - WAREHOUSE0 - SKU644: - - WAREHOUSE0 - SKU645: - - WAREHOUSE0 - SKU646: - - WAREHOUSE0 - SKU647: - - WAREHOUSE0 - SKU648: - - WAREHOUSE0 - SKU649: - - WAREHOUSE0 - SKU65: - - WAREHOUSE0 - SKU650: - - WAREHOUSE0 - SKU651: - - WAREHOUSE0 - SKU652: - - WAREHOUSE0 - SKU653: - - WAREHOUSE0 - SKU654: - - WAREHOUSE0 - SKU655: - - WAREHOUSE0 - SKU656: - - WAREHOUSE0 - SKU657: - - WAREHOUSE0 - SKU658: - - WAREHOUSE0 - SKU659: - - WAREHOUSE0 - SKU66: - - WAREHOUSE0 - SKU660: - - WAREHOUSE0 - SKU661: - - WAREHOUSE0 - SKU662: - - WAREHOUSE0 - SKU663: - - WAREHOUSE0 - SKU664: - - WAREHOUSE0 - SKU665: - - WAREHOUSE0 - SKU666: - - WAREHOUSE0 - SKU667: - - WAREHOUSE0 - SKU668: - - WAREHOUSE0 - SKU669: - - WAREHOUSE0 - SKU67: - - WAREHOUSE0 - SKU670: - - WAREHOUSE0 - SKU671: - - WAREHOUSE0 - SKU672: - - WAREHOUSE0 - SKU673: - - WAREHOUSE0 - SKU674: - - WAREHOUSE0 - SKU675: - - WAREHOUSE0 - SKU676: - - WAREHOUSE0 - SKU677: - - WAREHOUSE0 - SKU678: - - WAREHOUSE0 - SKU679: - - WAREHOUSE0 - SKU68: - - WAREHOUSE0 - SKU680: - - WAREHOUSE0 - SKU681: - - WAREHOUSE0 - SKU682: - - WAREHOUSE0 - SKU683: - - WAREHOUSE0 - SKU684: - - WAREHOUSE0 - SKU685: - - WAREHOUSE0 - SKU686: - - WAREHOUSE0 - SKU687: - - WAREHOUSE0 - SKU688: - - WAREHOUSE0 - SKU689: - - WAREHOUSE0 - SKU69: - - WAREHOUSE0 - SKU690: - - WAREHOUSE0 - SKU691: - - WAREHOUSE0 - SKU692: - - WAREHOUSE0 - SKU693: - - WAREHOUSE0 - SKU694: - - WAREHOUSE0 - SKU695: - - WAREHOUSE0 - SKU696: - - WAREHOUSE0 - SKU697: - - WAREHOUSE0 - SKU698: - - WAREHOUSE0 - SKU699: - - WAREHOUSE0 - SKU7: - - WAREHOUSE0 - SKU70: - - WAREHOUSE0 - SKU700: - - WAREHOUSE0 - SKU701: - - WAREHOUSE0 - SKU702: - - WAREHOUSE0 - SKU703: - - WAREHOUSE0 - SKU704: - - WAREHOUSE0 - SKU705: - - WAREHOUSE0 - SKU706: - - WAREHOUSE0 - SKU707: - - WAREHOUSE0 - SKU708: - - WAREHOUSE0 - SKU709: - - WAREHOUSE0 - SKU71: - - WAREHOUSE0 - SKU710: - - WAREHOUSE0 - SKU711: - - WAREHOUSE0 - SKU712: - - WAREHOUSE0 - SKU713: - - WAREHOUSE0 - SKU714: - - WAREHOUSE0 - SKU715: - - WAREHOUSE0 - SKU716: - - WAREHOUSE0 - SKU717: - - WAREHOUSE0 - SKU718: - - WAREHOUSE0 - SKU719: - - WAREHOUSE0 - SKU72: - - WAREHOUSE0 - SKU720: - - WAREHOUSE0 - SKU721: - - WAREHOUSE0 - SKU722: - - WAREHOUSE0 - SKU723: - - WAREHOUSE0 - SKU724: - - WAREHOUSE0 - SKU725: - - WAREHOUSE0 - SKU726: - - WAREHOUSE0 - SKU727: - - WAREHOUSE0 - SKU728: - - WAREHOUSE0 - SKU729: - - WAREHOUSE0 - SKU73: - - WAREHOUSE0 - SKU730: - - WAREHOUSE0 - SKU731: - - WAREHOUSE0 - SKU732: - - WAREHOUSE0 - SKU733: - - WAREHOUSE0 - SKU734: - - WAREHOUSE0 - SKU735: - - WAREHOUSE0 - SKU736: - - WAREHOUSE0 - SKU737: - - WAREHOUSE0 - SKU738: - - WAREHOUSE0 - SKU739: - - WAREHOUSE0 - SKU74: - - WAREHOUSE0 - SKU740: - - WAREHOUSE0 - SKU741: - - WAREHOUSE0 - SKU742: - - WAREHOUSE0 - SKU743: - - WAREHOUSE0 - SKU744: - - WAREHOUSE0 - SKU745: - - WAREHOUSE0 - SKU746: - - WAREHOUSE0 - SKU747: - - WAREHOUSE0 - SKU748: - - WAREHOUSE0 - SKU749: - - WAREHOUSE0 - SKU75: - - WAREHOUSE0 - SKU750: - - WAREHOUSE0 - SKU751: - - WAREHOUSE0 - SKU752: - - WAREHOUSE0 - SKU753: - - WAREHOUSE0 - SKU754: - - WAREHOUSE0 - SKU755: - - WAREHOUSE0 - SKU756: - - WAREHOUSE0 - SKU757: - - WAREHOUSE0 - SKU758: - - WAREHOUSE0 - SKU759: - - WAREHOUSE0 - SKU76: - - WAREHOUSE0 - SKU760: - - WAREHOUSE0 - SKU761: - - WAREHOUSE0 - SKU762: - - WAREHOUSE0 - SKU763: - - WAREHOUSE0 - SKU764: - - WAREHOUSE0 - SKU765: - - WAREHOUSE0 - SKU766: - - WAREHOUSE0 - SKU767: - - WAREHOUSE0 - SKU768: - - WAREHOUSE0 - SKU769: - - WAREHOUSE0 - SKU77: - - WAREHOUSE0 - SKU770: - - WAREHOUSE0 - SKU771: - - WAREHOUSE0 - SKU772: - - WAREHOUSE0 - SKU773: - - WAREHOUSE0 - SKU774: - - WAREHOUSE0 - SKU775: - - WAREHOUSE0 - SKU776: - - WAREHOUSE0 - SKU777: - - WAREHOUSE0 - SKU778: - - WAREHOUSE0 - SKU779: - - WAREHOUSE0 - SKU78: - - WAREHOUSE0 - SKU780: - - WAREHOUSE0 - SKU781: - - WAREHOUSE0 - SKU782: - - WAREHOUSE0 - SKU783: - - WAREHOUSE0 - SKU784: - - WAREHOUSE0 - SKU785: - - WAREHOUSE0 - SKU786: - - WAREHOUSE0 - SKU787: - - WAREHOUSE0 - SKU788: - - WAREHOUSE0 - SKU789: - - WAREHOUSE0 - SKU79: - - WAREHOUSE0 - SKU790: - - WAREHOUSE0 - SKU791: - - WAREHOUSE0 - SKU792: - - WAREHOUSE0 - SKU793: - - WAREHOUSE0 - SKU794: - - WAREHOUSE0 - SKU795: - - WAREHOUSE0 - SKU796: - - WAREHOUSE0 - SKU797: - - WAREHOUSE0 - SKU798: - - WAREHOUSE0 - SKU799: - - WAREHOUSE0 - SKU8: - - WAREHOUSE0 - SKU80: - - WAREHOUSE0 - SKU800: - - WAREHOUSE0 - SKU801: - - WAREHOUSE0 - SKU802: - - WAREHOUSE0 - SKU803: - - WAREHOUSE0 - SKU804: - - WAREHOUSE0 - SKU805: - - WAREHOUSE0 - SKU806: - - WAREHOUSE0 - SKU807: - - WAREHOUSE0 - SKU808: - - WAREHOUSE0 - SKU809: - - WAREHOUSE0 - SKU81: - - WAREHOUSE0 - SKU810: - - WAREHOUSE0 - SKU811: - - WAREHOUSE0 - SKU812: - - WAREHOUSE0 - SKU813: - - WAREHOUSE0 - SKU814: - - WAREHOUSE0 - SKU815: - - WAREHOUSE0 - SKU816: - - WAREHOUSE0 - SKU817: - - WAREHOUSE0 - SKU818: - - WAREHOUSE0 - SKU819: - - WAREHOUSE0 - SKU82: - - WAREHOUSE0 - SKU820: - - WAREHOUSE0 - SKU821: - - WAREHOUSE0 - SKU822: - - WAREHOUSE0 - SKU823: - - WAREHOUSE0 - SKU824: - - WAREHOUSE0 - SKU825: - - WAREHOUSE0 - SKU826: - - WAREHOUSE0 - SKU827: - - WAREHOUSE0 - SKU828: - - WAREHOUSE0 - SKU829: - - WAREHOUSE0 - SKU83: - - WAREHOUSE0 - SKU830: - - WAREHOUSE0 - SKU831: - - WAREHOUSE0 - SKU832: - - WAREHOUSE0 - SKU833: - - WAREHOUSE0 - SKU834: - - WAREHOUSE0 - SKU835: - - WAREHOUSE0 - SKU836: - - WAREHOUSE0 - SKU837: - - WAREHOUSE0 - SKU838: - - WAREHOUSE0 - SKU839: - - WAREHOUSE0 - SKU84: - - WAREHOUSE0 - SKU840: - - WAREHOUSE0 - SKU841: - - WAREHOUSE0 - SKU842: - - WAREHOUSE0 - SKU843: - - WAREHOUSE0 - SKU844: - - WAREHOUSE0 - SKU845: - - WAREHOUSE0 - SKU846: - - WAREHOUSE0 - SKU847: - - WAREHOUSE0 - SKU848: - - WAREHOUSE0 - SKU849: - - WAREHOUSE0 - SKU85: - - WAREHOUSE0 - SKU850: - - WAREHOUSE0 - SKU851: - - WAREHOUSE0 - SKU852: - - WAREHOUSE0 - SKU853: - - WAREHOUSE0 - SKU854: - - WAREHOUSE0 - SKU855: - - WAREHOUSE0 - SKU856: - - WAREHOUSE0 - SKU857: - - WAREHOUSE0 - SKU858: - - WAREHOUSE0 - SKU859: - - WAREHOUSE0 - SKU86: - - WAREHOUSE0 - SKU860: - - WAREHOUSE0 - SKU861: - - WAREHOUSE0 - SKU862: - - WAREHOUSE0 - SKU863: - - WAREHOUSE0 - SKU864: - - WAREHOUSE0 - SKU865: - - WAREHOUSE0 - SKU866: - - WAREHOUSE0 - SKU867: - - WAREHOUSE0 - SKU868: - - WAREHOUSE0 - SKU869: - - WAREHOUSE0 - SKU87: - - WAREHOUSE0 - SKU870: - - WAREHOUSE0 - SKU871: - - WAREHOUSE0 - SKU872: - - WAREHOUSE0 - SKU873: - - WAREHOUSE0 - SKU874: - - WAREHOUSE0 - SKU875: - - WAREHOUSE0 - SKU876: - - WAREHOUSE0 - SKU877: - - WAREHOUSE0 - SKU878: - - WAREHOUSE0 - SKU879: - - WAREHOUSE0 - SKU88: - - WAREHOUSE0 - SKU880: - - WAREHOUSE0 - SKU881: - - WAREHOUSE0 - SKU882: - - WAREHOUSE0 - SKU883: - - WAREHOUSE0 - SKU884: - - WAREHOUSE0 - SKU885: - - WAREHOUSE0 - SKU886: - - WAREHOUSE0 - SKU887: - - WAREHOUSE0 - SKU888: - - WAREHOUSE0 - SKU889: - - WAREHOUSE0 - SKU89: - - WAREHOUSE0 - SKU890: - - WAREHOUSE0 - SKU891: - - WAREHOUSE0 - SKU892: - - WAREHOUSE0 - SKU893: - - WAREHOUSE0 - SKU894: - - WAREHOUSE0 - SKU895: - - WAREHOUSE0 - SKU896: - - WAREHOUSE0 - SKU897: - - WAREHOUSE0 - SKU898: - - WAREHOUSE0 - SKU899: - - WAREHOUSE0 - SKU9: - - WAREHOUSE0 - SKU90: - - WAREHOUSE0 - SKU900: - - WAREHOUSE0 - SKU901: - - WAREHOUSE0 - SKU902: - - WAREHOUSE0 - SKU903: - - WAREHOUSE0 - SKU904: - - WAREHOUSE0 - SKU905: - - WAREHOUSE0 - SKU906: - - WAREHOUSE0 - SKU907: - - WAREHOUSE0 - SKU908: - - WAREHOUSE0 - SKU909: - - WAREHOUSE0 - SKU91: - - WAREHOUSE0 - SKU910: - - WAREHOUSE0 - SKU911: - - WAREHOUSE0 - SKU912: - - WAREHOUSE0 - SKU913: - - WAREHOUSE0 - SKU914: - - WAREHOUSE0 - SKU915: - - WAREHOUSE0 - SKU916: - - WAREHOUSE0 - SKU917: - - WAREHOUSE0 - SKU918: - - WAREHOUSE0 - SKU919: - - WAREHOUSE0 - SKU92: - - WAREHOUSE0 - SKU920: - - WAREHOUSE0 - SKU921: - - WAREHOUSE0 - SKU922: - - WAREHOUSE0 - SKU923: - - WAREHOUSE0 - SKU924: - - WAREHOUSE0 - SKU925: - - WAREHOUSE0 - SKU926: - - WAREHOUSE0 - SKU927: - - WAREHOUSE0 - SKU928: - - WAREHOUSE0 - SKU929: - - WAREHOUSE0 - SKU93: - - WAREHOUSE0 - SKU930: - - WAREHOUSE0 - SKU931: - - WAREHOUSE0 - SKU932: - - WAREHOUSE0 - SKU933: - - WAREHOUSE0 - SKU934: - - WAREHOUSE0 - SKU935: - - WAREHOUSE0 - SKU936: - - WAREHOUSE0 - SKU937: - - WAREHOUSE0 - SKU938: - - WAREHOUSE0 - SKU939: - - WAREHOUSE0 - SKU94: - - WAREHOUSE0 - SKU940: - - WAREHOUSE0 - SKU941: - - WAREHOUSE0 - SKU942: - - WAREHOUSE0 - SKU943: - - WAREHOUSE0 - SKU944: - - WAREHOUSE0 - SKU945: - - WAREHOUSE0 - SKU946: - - WAREHOUSE0 - SKU947: - - WAREHOUSE0 - SKU948: - - WAREHOUSE0 - SKU949: - - WAREHOUSE0 - SKU95: - - WAREHOUSE0 - SKU950: - - WAREHOUSE0 - SKU951: - - WAREHOUSE0 - SKU952: - - WAREHOUSE0 - SKU953: - - WAREHOUSE0 - SKU954: - - WAREHOUSE0 - SKU955: - - WAREHOUSE0 - SKU956: - - WAREHOUSE0 - SKU957: - - WAREHOUSE0 - SKU958: - - WAREHOUSE0 - SKU959: - - WAREHOUSE0 - SKU96: - - WAREHOUSE0 - SKU960: - - WAREHOUSE0 - SKU961: - - WAREHOUSE0 - SKU962: - - WAREHOUSE0 - SKU963: - - WAREHOUSE0 - SKU964: - - WAREHOUSE0 - SKU965: - - WAREHOUSE0 - SKU966: - - WAREHOUSE0 - SKU967: - - WAREHOUSE0 - SKU968: - - WAREHOUSE0 - SKU969: - - WAREHOUSE0 - SKU97: - - WAREHOUSE0 - SKU970: - - WAREHOUSE0 - SKU971: - - WAREHOUSE0 - SKU972: - - WAREHOUSE0 - SKU973: - - WAREHOUSE0 - SKU974: - - WAREHOUSE0 - SKU975: - - WAREHOUSE0 - SKU976: - - WAREHOUSE0 - SKU977: - - WAREHOUSE0 - SKU978: - - WAREHOUSE0 - SKU979: - - WAREHOUSE0 - SKU98: - - WAREHOUSE0 - SKU980: - - WAREHOUSE0 - SKU981: - - WAREHOUSE0 - SKU982: - - WAREHOUSE0 - SKU983: - - WAREHOUSE0 - SKU984: - - WAREHOUSE0 - SKU985: - - WAREHOUSE0 - SKU986: - - WAREHOUSE0 - SKU987: - - WAREHOUSE0 - SKU988: - - WAREHOUSE0 - SKU989: - - WAREHOUSE0 - SKU99: - - WAREHOUSE0 - SKU990: - - WAREHOUSE0 - SKU991: - - WAREHOUSE0 - SKU992: - - WAREHOUSE0 - SKU993: - - WAREHOUSE0 - SKU994: - - WAREHOUSE0 - SKU995: - - WAREHOUSE0 - SKU996: - - WAREHOUSE0 - SKU997: - - WAREHOUSE0 - SKU998: - - WAREHOUSE0 - SKU999: - - WAREHOUSE0 - WAREHOUSE0: - SKU0: - - SUPPLIER0 - SKU1: - - SUPPLIER0 - SKU10: - - SUPPLIER0 - SKU100: - - SUPPLIER0 - SKU101: - - SUPPLIER0 - SKU102: - - SUPPLIER0 - SKU103: - - SUPPLIER0 - SKU104: - - SUPPLIER0 - SKU105: - - SUPPLIER0 - SKU106: - - SUPPLIER0 - SKU107: - - SUPPLIER0 - SKU108: - - SUPPLIER0 - SKU109: - - SUPPLIER0 - SKU11: - - SUPPLIER0 - SKU110: - - SUPPLIER0 - SKU111: - - SUPPLIER0 - SKU112: - - SUPPLIER0 - SKU113: - - SUPPLIER0 - SKU114: - - SUPPLIER0 - SKU115: - - SUPPLIER0 - SKU116: - - SUPPLIER0 - SKU117: - - SUPPLIER0 - SKU118: - - SUPPLIER0 - SKU119: - - SUPPLIER0 - SKU12: - - SUPPLIER0 - SKU120: - - SUPPLIER0 - SKU121: - - SUPPLIER0 - SKU122: - - SUPPLIER0 - SKU123: - - SUPPLIER0 - SKU124: - - SUPPLIER0 - SKU125: - - SUPPLIER0 - SKU126: - - SUPPLIER0 - SKU127: - - SUPPLIER0 - SKU128: - - SUPPLIER0 - SKU129: - - SUPPLIER0 - SKU13: - - SUPPLIER0 - SKU130: - - SUPPLIER0 - SKU131: - - SUPPLIER0 - SKU132: - - SUPPLIER0 - SKU133: - - SUPPLIER0 - SKU134: - - SUPPLIER0 - SKU135: - - SUPPLIER0 - SKU136: - - SUPPLIER0 - SKU137: - - SUPPLIER0 - SKU138: - - SUPPLIER0 - SKU139: - - SUPPLIER0 - SKU14: - - SUPPLIER0 - SKU140: - - SUPPLIER0 - SKU141: - - SUPPLIER0 - SKU142: - - SUPPLIER0 - SKU143: - - SUPPLIER0 - SKU144: - - SUPPLIER0 - SKU145: - - SUPPLIER0 - SKU146: - - SUPPLIER0 - SKU147: - - SUPPLIER0 - SKU148: - - SUPPLIER0 - SKU149: - - SUPPLIER0 - SKU15: - - SUPPLIER0 - SKU150: - - SUPPLIER0 - SKU151: - - SUPPLIER0 - SKU152: - - SUPPLIER0 - SKU153: - - SUPPLIER0 - SKU154: - - SUPPLIER0 - SKU155: - - SUPPLIER0 - SKU156: - - SUPPLIER0 - SKU157: - - SUPPLIER0 - SKU158: - - SUPPLIER0 - SKU159: - - SUPPLIER0 - SKU16: - - SUPPLIER0 - SKU160: - - SUPPLIER0 - SKU161: - - SUPPLIER0 - SKU162: - - SUPPLIER0 - SKU163: - - SUPPLIER0 - SKU164: - - SUPPLIER0 - SKU165: - - SUPPLIER0 - SKU166: - - SUPPLIER0 - SKU167: - - SUPPLIER0 - SKU168: - - SUPPLIER0 - SKU169: - - SUPPLIER0 - SKU17: - - SUPPLIER0 - SKU170: - - SUPPLIER0 - SKU171: - - SUPPLIER0 - SKU172: - - SUPPLIER0 - SKU173: - - SUPPLIER0 - SKU174: - - SUPPLIER0 - SKU175: - - SUPPLIER0 - SKU176: - - SUPPLIER0 - SKU177: - - SUPPLIER0 - SKU178: - - SUPPLIER0 - SKU179: - - SUPPLIER0 - SKU18: - - SUPPLIER0 - SKU180: - - SUPPLIER0 - SKU181: - - SUPPLIER0 - SKU182: - - SUPPLIER0 - SKU183: - - SUPPLIER0 - SKU184: - - SUPPLIER0 - SKU185: - - SUPPLIER0 - SKU186: - - SUPPLIER0 - SKU187: - - SUPPLIER0 - SKU188: - - SUPPLIER0 - SKU189: - - SUPPLIER0 - SKU19: - - SUPPLIER0 - SKU190: - - SUPPLIER0 - SKU191: - - SUPPLIER0 - SKU192: - - SUPPLIER0 - SKU193: - - SUPPLIER0 - SKU194: - - SUPPLIER0 - SKU195: - - SUPPLIER0 - SKU196: - - SUPPLIER0 - SKU197: - - SUPPLIER0 - SKU198: - - SUPPLIER0 - SKU199: - - SUPPLIER0 - SKU2: - - SUPPLIER0 - SKU20: - - SUPPLIER0 - SKU200: - - SUPPLIER0 - SKU201: - - SUPPLIER0 - SKU202: - - SUPPLIER0 - SKU203: - - SUPPLIER0 - SKU204: - - SUPPLIER0 - SKU205: - - SUPPLIER0 - SKU206: - - SUPPLIER0 - SKU207: - - SUPPLIER0 - SKU208: - - SUPPLIER0 - SKU209: - - SUPPLIER0 - SKU21: - - SUPPLIER0 - SKU210: - - SUPPLIER0 - SKU211: - - SUPPLIER0 - SKU212: - - SUPPLIER0 - SKU213: - - SUPPLIER0 - SKU214: - - SUPPLIER0 - SKU215: - - SUPPLIER0 - SKU216: - - SUPPLIER0 - SKU217: - - SUPPLIER0 - SKU218: - - SUPPLIER0 - SKU219: - - SUPPLIER0 - SKU22: - - SUPPLIER0 - SKU220: - - SUPPLIER0 - SKU221: - - SUPPLIER0 - SKU222: - - SUPPLIER0 - SKU223: - - SUPPLIER0 - SKU224: - - SUPPLIER0 - SKU225: - - SUPPLIER0 - SKU226: - - SUPPLIER0 - SKU227: - - SUPPLIER0 - SKU228: - - SUPPLIER0 - SKU229: - - SUPPLIER0 - SKU23: - - SUPPLIER0 - SKU230: - - SUPPLIER0 - SKU231: - - SUPPLIER0 - SKU232: - - SUPPLIER0 - SKU233: - - SUPPLIER0 - SKU234: - - SUPPLIER0 - SKU235: - - SUPPLIER0 - SKU236: - - SUPPLIER0 - SKU237: - - SUPPLIER0 - SKU238: - - SUPPLIER0 - SKU239: - - SUPPLIER0 - SKU24: - - SUPPLIER0 - SKU240: - - SUPPLIER0 - SKU241: - - SUPPLIER0 - SKU242: - - SUPPLIER0 - SKU243: - - SUPPLIER0 - SKU244: - - SUPPLIER0 - SKU245: - - SUPPLIER0 - SKU246: - - SUPPLIER0 - SKU247: - - SUPPLIER0 - SKU248: - - SUPPLIER0 - SKU249: - - SUPPLIER0 - SKU25: - - SUPPLIER0 - SKU250: - - SUPPLIER0 - SKU251: - - SUPPLIER0 - SKU252: - - SUPPLIER0 - SKU253: - - SUPPLIER0 - SKU254: - - SUPPLIER0 - SKU255: - - SUPPLIER0 - SKU256: - - SUPPLIER0 - SKU257: - - SUPPLIER0 - SKU258: - - SUPPLIER0 - SKU259: - - SUPPLIER0 - SKU26: - - SUPPLIER0 - SKU260: - - SUPPLIER0 - SKU261: - - SUPPLIER0 - SKU262: - - SUPPLIER0 - SKU263: - - SUPPLIER0 - SKU264: - - SUPPLIER0 - SKU265: - - SUPPLIER0 - SKU266: - - SUPPLIER0 - SKU267: - - SUPPLIER0 - SKU268: - - SUPPLIER0 - SKU269: - - SUPPLIER0 - SKU27: - - SUPPLIER0 - SKU270: - - SUPPLIER0 - SKU271: - - SUPPLIER0 - SKU272: - - SUPPLIER0 - SKU273: - - SUPPLIER0 - SKU274: - - SUPPLIER0 - SKU275: - - SUPPLIER0 - SKU276: - - SUPPLIER0 - SKU277: - - SUPPLIER0 - SKU278: - - SUPPLIER0 - SKU279: - - SUPPLIER0 - SKU28: - - SUPPLIER0 - SKU280: - - SUPPLIER0 - SKU281: - - SUPPLIER0 - SKU282: - - SUPPLIER0 - SKU283: - - SUPPLIER0 - SKU284: - - SUPPLIER0 - SKU285: - - SUPPLIER0 - SKU286: - - SUPPLIER0 - SKU287: - - SUPPLIER0 - SKU288: - - SUPPLIER0 - SKU289: - - SUPPLIER0 - SKU29: - - SUPPLIER0 - SKU290: - - SUPPLIER0 - SKU291: - - SUPPLIER0 - SKU292: - - SUPPLIER0 - SKU293: - - SUPPLIER0 - SKU294: - - SUPPLIER0 - SKU295: - - SUPPLIER0 - SKU296: - - SUPPLIER0 - SKU297: - - SUPPLIER0 - SKU298: - - SUPPLIER0 - SKU299: - - SUPPLIER0 - SKU3: - - SUPPLIER0 - SKU30: - - SUPPLIER0 - SKU300: - - SUPPLIER0 - SKU301: - - SUPPLIER0 - SKU302: - - SUPPLIER0 - SKU303: - - SUPPLIER0 - SKU304: - - SUPPLIER0 - SKU305: - - SUPPLIER0 - SKU306: - - SUPPLIER0 - SKU307: - - SUPPLIER0 - SKU308: - - SUPPLIER0 - SKU309: - - SUPPLIER0 - SKU31: - - SUPPLIER0 - SKU310: - - SUPPLIER0 - SKU311: - - SUPPLIER0 - SKU312: - - SUPPLIER0 - SKU313: - - SUPPLIER0 - SKU314: - - SUPPLIER0 - SKU315: - - SUPPLIER0 - SKU316: - - SUPPLIER0 - SKU317: - - SUPPLIER0 - SKU318: - - SUPPLIER0 - SKU319: - - SUPPLIER0 - SKU32: - - SUPPLIER0 - SKU320: - - SUPPLIER0 - SKU321: - - SUPPLIER0 - SKU322: - - SUPPLIER0 - SKU323: - - SUPPLIER0 - SKU324: - - SUPPLIER0 - SKU325: - - SUPPLIER0 - SKU326: - - SUPPLIER0 - SKU327: - - SUPPLIER0 - SKU328: - - SUPPLIER0 - SKU329: - - SUPPLIER0 - SKU33: - - SUPPLIER0 - SKU330: - - SUPPLIER0 - SKU331: - - SUPPLIER0 - SKU332: - - SUPPLIER0 - SKU333: - - SUPPLIER0 - SKU334: - - SUPPLIER0 - SKU335: - - SUPPLIER0 - SKU336: - - SUPPLIER0 - SKU337: - - SUPPLIER0 - SKU338: - - SUPPLIER0 - SKU339: - - SUPPLIER0 - SKU34: - - SUPPLIER0 - SKU340: - - SUPPLIER0 - SKU341: - - SUPPLIER0 - SKU342: - - SUPPLIER0 - SKU343: - - SUPPLIER0 - SKU344: - - SUPPLIER0 - SKU345: - - SUPPLIER0 - SKU346: - - SUPPLIER0 - SKU347: - - SUPPLIER0 - SKU348: - - SUPPLIER0 - SKU349: - - SUPPLIER0 - SKU35: - - SUPPLIER0 - SKU350: - - SUPPLIER0 - SKU351: - - SUPPLIER0 - SKU352: - - SUPPLIER0 - SKU353: - - SUPPLIER0 - SKU354: - - SUPPLIER0 - SKU355: - - SUPPLIER0 - SKU356: - - SUPPLIER0 - SKU357: - - SUPPLIER0 - SKU358: - - SUPPLIER0 - SKU359: - - SUPPLIER0 - SKU36: - - SUPPLIER0 - SKU360: - - SUPPLIER0 - SKU361: - - SUPPLIER0 - SKU362: - - SUPPLIER0 - SKU363: - - SUPPLIER0 - SKU364: - - SUPPLIER0 - SKU365: - - SUPPLIER0 - SKU366: - - SUPPLIER0 - SKU367: - - SUPPLIER0 - SKU368: - - SUPPLIER0 - SKU369: - - SUPPLIER0 - SKU37: - - SUPPLIER0 - SKU370: - - SUPPLIER0 - SKU371: - - SUPPLIER0 - SKU372: - - SUPPLIER0 - SKU373: - - SUPPLIER0 - SKU374: - - SUPPLIER0 - SKU375: - - SUPPLIER0 - SKU376: - - SUPPLIER0 - SKU377: - - SUPPLIER0 - SKU378: - - SUPPLIER0 - SKU379: - - SUPPLIER0 - SKU38: - - SUPPLIER0 - SKU380: - - SUPPLIER0 - SKU381: - - SUPPLIER0 - SKU382: - - SUPPLIER0 - SKU383: - - SUPPLIER0 - SKU384: - - SUPPLIER0 - SKU385: - - SUPPLIER0 - SKU386: - - SUPPLIER0 - SKU387: - - SUPPLIER0 - SKU388: - - SUPPLIER0 - SKU389: - - SUPPLIER0 - SKU39: - - SUPPLIER0 - SKU390: - - SUPPLIER0 - SKU391: - - SUPPLIER0 - SKU392: - - SUPPLIER0 - SKU393: - - SUPPLIER0 - SKU394: - - SUPPLIER0 - SKU395: - - SUPPLIER0 - SKU396: - - SUPPLIER0 - SKU397: - - SUPPLIER0 - SKU398: - - SUPPLIER0 - SKU399: - - SUPPLIER0 - SKU4: - - SUPPLIER0 - SKU40: - - SUPPLIER0 - SKU400: - - SUPPLIER0 - SKU401: - - SUPPLIER0 - SKU402: - - SUPPLIER0 - SKU403: - - SUPPLIER0 - SKU404: - - SUPPLIER0 - SKU405: - - SUPPLIER0 - SKU406: - - SUPPLIER0 - SKU407: - - SUPPLIER0 - SKU408: - - SUPPLIER0 - SKU409: - - SUPPLIER0 - SKU41: - - SUPPLIER0 - SKU410: - - SUPPLIER0 - SKU411: - - SUPPLIER0 - SKU412: - - SUPPLIER0 - SKU413: - - SUPPLIER0 - SKU414: - - SUPPLIER0 - SKU415: - - SUPPLIER0 - SKU416: - - SUPPLIER0 - SKU417: - - SUPPLIER0 - SKU418: - - SUPPLIER0 - SKU419: - - SUPPLIER0 - SKU42: - - SUPPLIER0 - SKU420: - - SUPPLIER0 - SKU421: - - SUPPLIER0 - SKU422: - - SUPPLIER0 - SKU423: - - SUPPLIER0 - SKU424: - - SUPPLIER0 - SKU425: - - SUPPLIER0 - SKU426: - - SUPPLIER0 - SKU427: - - SUPPLIER0 - SKU428: - - SUPPLIER0 - SKU429: - - SUPPLIER0 - SKU43: - - SUPPLIER0 - SKU430: - - SUPPLIER0 - SKU431: - - SUPPLIER0 - SKU432: - - SUPPLIER0 - SKU433: - - SUPPLIER0 - SKU434: - - SUPPLIER0 - SKU435: - - SUPPLIER0 - SKU436: - - SUPPLIER0 - SKU437: - - SUPPLIER0 - SKU438: - - SUPPLIER0 - SKU439: - - SUPPLIER0 - SKU44: - - SUPPLIER0 - SKU440: - - SUPPLIER0 - SKU441: - - SUPPLIER0 - SKU442: - - SUPPLIER0 - SKU443: - - SUPPLIER0 - SKU444: - - SUPPLIER0 - SKU445: - - SUPPLIER0 - SKU446: - - SUPPLIER0 - SKU447: - - SUPPLIER0 - SKU448: - - SUPPLIER0 - SKU449: - - SUPPLIER0 - SKU45: - - SUPPLIER0 - SKU450: - - SUPPLIER0 - SKU451: - - SUPPLIER0 - SKU452: - - SUPPLIER0 - SKU453: - - SUPPLIER0 - SKU454: - - SUPPLIER0 - SKU455: - - SUPPLIER0 - SKU456: - - SUPPLIER0 - SKU457: - - SUPPLIER0 - SKU458: - - SUPPLIER0 - SKU459: - - SUPPLIER0 - SKU46: - - SUPPLIER0 - SKU460: - - SUPPLIER0 - SKU461: - - SUPPLIER0 - SKU462: - - SUPPLIER0 - SKU463: - - SUPPLIER0 - SKU464: - - SUPPLIER0 - SKU465: - - SUPPLIER0 - SKU466: - - SUPPLIER0 - SKU467: - - SUPPLIER0 - SKU468: - - SUPPLIER0 - SKU469: - - SUPPLIER0 - SKU47: - - SUPPLIER0 - SKU470: - - SUPPLIER0 - SKU471: - - SUPPLIER0 - SKU472: - - SUPPLIER0 - SKU473: - - SUPPLIER0 - SKU474: - - SUPPLIER0 - SKU475: - - SUPPLIER0 - SKU476: - - SUPPLIER0 - SKU477: - - SUPPLIER0 - SKU478: - - SUPPLIER0 - SKU479: - - SUPPLIER0 - SKU48: - - SUPPLIER0 - SKU480: - - SUPPLIER0 - SKU481: - - SUPPLIER0 - SKU482: - - SUPPLIER0 - SKU483: - - SUPPLIER0 - SKU484: - - SUPPLIER0 - SKU485: - - SUPPLIER0 - SKU486: - - SUPPLIER0 - SKU487: - - SUPPLIER0 - SKU488: - - SUPPLIER0 - SKU489: - - SUPPLIER0 - SKU49: - - SUPPLIER0 - SKU490: - - SUPPLIER0 - SKU491: - - SUPPLIER0 - SKU492: - - SUPPLIER0 - SKU493: - - SUPPLIER0 - SKU494: - - SUPPLIER0 - SKU495: - - SUPPLIER0 - SKU496: - - SUPPLIER0 - SKU497: - - SUPPLIER0 - SKU498: - - SUPPLIER0 - SKU499: - - SUPPLIER0 - SKU5: - - SUPPLIER0 - SKU50: - - SUPPLIER0 - SKU500: - - SUPPLIER0 - SKU501: - - SUPPLIER0 - SKU502: - - SUPPLIER0 - SKU503: - - SUPPLIER0 - SKU504: - - SUPPLIER0 - SKU505: - - SUPPLIER0 - SKU506: - - SUPPLIER0 - SKU507: - - SUPPLIER0 - SKU508: - - SUPPLIER0 - SKU509: - - SUPPLIER0 - SKU51: - - SUPPLIER0 - SKU510: - - SUPPLIER0 - SKU511: - - SUPPLIER0 - SKU512: - - SUPPLIER0 - SKU513: - - SUPPLIER0 - SKU514: - - SUPPLIER0 - SKU515: - - SUPPLIER0 - SKU516: - - SUPPLIER0 - SKU517: - - SUPPLIER0 - SKU518: - - SUPPLIER0 - SKU519: - - SUPPLIER0 - SKU52: - - SUPPLIER0 - SKU520: - - SUPPLIER0 - SKU521: - - SUPPLIER0 - SKU522: - - SUPPLIER0 - SKU523: - - SUPPLIER0 - SKU524: - - SUPPLIER0 - SKU525: - - SUPPLIER0 - SKU526: - - SUPPLIER0 - SKU527: - - SUPPLIER0 - SKU528: - - SUPPLIER0 - SKU529: - - SUPPLIER0 - SKU53: - - SUPPLIER0 - SKU530: - - SUPPLIER0 - SKU531: - - SUPPLIER0 - SKU532: - - SUPPLIER0 - SKU533: - - SUPPLIER0 - SKU534: - - SUPPLIER0 - SKU535: - - SUPPLIER0 - SKU536: - - SUPPLIER0 - SKU537: - - SUPPLIER0 - SKU538: - - SUPPLIER0 - SKU539: - - SUPPLIER0 - SKU54: - - SUPPLIER0 - SKU540: - - SUPPLIER0 - SKU541: - - SUPPLIER0 - SKU542: - - SUPPLIER0 - SKU543: - - SUPPLIER0 - SKU544: - - SUPPLIER0 - SKU545: - - SUPPLIER0 - SKU546: - - SUPPLIER0 - SKU547: - - SUPPLIER0 - SKU548: - - SUPPLIER0 - SKU549: - - SUPPLIER0 - SKU55: - - SUPPLIER0 - SKU550: - - SUPPLIER0 - SKU551: - - SUPPLIER0 - SKU552: - - SUPPLIER0 - SKU553: - - SUPPLIER0 - SKU554: - - SUPPLIER0 - SKU555: - - SUPPLIER0 - SKU556: - - SUPPLIER0 - SKU557: - - SUPPLIER0 - SKU558: - - SUPPLIER0 - SKU559: - - SUPPLIER0 - SKU56: - - SUPPLIER0 - SKU560: - - SUPPLIER0 - SKU561: - - SUPPLIER0 - SKU562: - - SUPPLIER0 - SKU563: - - SUPPLIER0 - SKU564: - - SUPPLIER0 - SKU565: - - SUPPLIER0 - SKU566: - - SUPPLIER0 - SKU567: - - SUPPLIER0 - SKU568: - - SUPPLIER0 - SKU569: - - SUPPLIER0 - SKU57: - - SUPPLIER0 - SKU570: - - SUPPLIER0 - SKU571: - - SUPPLIER0 - SKU572: - - SUPPLIER0 - SKU573: - - SUPPLIER0 - SKU574: - - SUPPLIER0 - SKU575: - - SUPPLIER0 - SKU576: - - SUPPLIER0 - SKU577: - - SUPPLIER0 - SKU578: - - SUPPLIER0 - SKU579: - - SUPPLIER0 - SKU58: - - SUPPLIER0 - SKU580: - - SUPPLIER0 - SKU581: - - SUPPLIER0 - SKU582: - - SUPPLIER0 - SKU583: - - SUPPLIER0 - SKU584: - - SUPPLIER0 - SKU585: - - SUPPLIER0 - SKU586: - - SUPPLIER0 - SKU587: - - SUPPLIER0 - SKU588: - - SUPPLIER0 - SKU589: - - SUPPLIER0 - SKU59: - - SUPPLIER0 - SKU590: - - SUPPLIER0 - SKU591: - - SUPPLIER0 - SKU592: - - SUPPLIER0 - SKU593: - - SUPPLIER0 - SKU594: - - SUPPLIER0 - SKU595: - - SUPPLIER0 - SKU596: - - SUPPLIER0 - SKU597: - - SUPPLIER0 - SKU598: - - SUPPLIER0 - SKU599: - - SUPPLIER0 - SKU6: - - SUPPLIER0 - SKU60: - - SUPPLIER0 - SKU600: - - SUPPLIER0 - SKU601: - - SUPPLIER0 - SKU602: - - SUPPLIER0 - SKU603: - - SUPPLIER0 - SKU604: - - SUPPLIER0 - SKU605: - - SUPPLIER0 - SKU606: - - SUPPLIER0 - SKU607: - - SUPPLIER0 - SKU608: - - SUPPLIER0 - SKU609: - - SUPPLIER0 - SKU61: - - SUPPLIER0 - SKU610: - - SUPPLIER0 - SKU611: - - SUPPLIER0 - SKU612: - - SUPPLIER0 - SKU613: - - SUPPLIER0 - SKU614: - - SUPPLIER0 - SKU615: - - SUPPLIER0 - SKU616: - - SUPPLIER0 - SKU617: - - SUPPLIER0 - SKU618: - - SUPPLIER0 - SKU619: - - SUPPLIER0 - SKU62: - - SUPPLIER0 - SKU620: - - SUPPLIER0 - SKU621: - - SUPPLIER0 - SKU622: - - SUPPLIER0 - SKU623: - - SUPPLIER0 - SKU624: - - SUPPLIER0 - SKU625: - - SUPPLIER0 - SKU626: - - SUPPLIER0 - SKU627: - - SUPPLIER0 - SKU628: - - SUPPLIER0 - SKU629: - - SUPPLIER0 - SKU63: - - SUPPLIER0 - SKU630: - - SUPPLIER0 - SKU631: - - SUPPLIER0 - SKU632: - - SUPPLIER0 - SKU633: - - SUPPLIER0 - SKU634: - - SUPPLIER0 - SKU635: - - SUPPLIER0 - SKU636: - - SUPPLIER0 - SKU637: - - SUPPLIER0 - SKU638: - - SUPPLIER0 - SKU639: - - SUPPLIER0 - SKU64: - - SUPPLIER0 - SKU640: - - SUPPLIER0 - SKU641: - - SUPPLIER0 - SKU642: - - SUPPLIER0 - SKU643: - - SUPPLIER0 - SKU644: - - SUPPLIER0 - SKU645: - - SUPPLIER0 - SKU646: - - SUPPLIER0 - SKU647: - - SUPPLIER0 - SKU648: - - SUPPLIER0 - SKU649: - - SUPPLIER0 - SKU65: - - SUPPLIER0 - SKU650: - - SUPPLIER0 - SKU651: - - SUPPLIER0 - SKU652: - - SUPPLIER0 - SKU653: - - SUPPLIER0 - SKU654: - - SUPPLIER0 - SKU655: - - SUPPLIER0 - SKU656: - - SUPPLIER0 - SKU657: - - SUPPLIER0 - SKU658: - - SUPPLIER0 - SKU659: - - SUPPLIER0 - SKU66: - - SUPPLIER0 - SKU660: - - SUPPLIER0 - SKU661: - - SUPPLIER0 - SKU662: - - SUPPLIER0 - SKU663: - - SUPPLIER0 - SKU664: - - SUPPLIER0 - SKU665: - - SUPPLIER0 - SKU666: - - SUPPLIER0 - SKU667: - - SUPPLIER0 - SKU668: - - SUPPLIER0 - SKU669: - - SUPPLIER0 - SKU67: - - SUPPLIER0 - SKU670: - - SUPPLIER0 - SKU671: - - SUPPLIER0 - SKU672: - - SUPPLIER0 - SKU673: - - SUPPLIER0 - SKU674: - - SUPPLIER0 - SKU675: - - SUPPLIER0 - SKU676: - - SUPPLIER0 - SKU677: - - SUPPLIER0 - SKU678: - - SUPPLIER0 - SKU679: - - SUPPLIER0 - SKU68: - - SUPPLIER0 - SKU680: - - SUPPLIER0 - SKU681: - - SUPPLIER0 - SKU682: - - SUPPLIER0 - SKU683: - - SUPPLIER0 - SKU684: - - SUPPLIER0 - SKU685: - - SUPPLIER0 - SKU686: - - SUPPLIER0 - SKU687: - - SUPPLIER0 - SKU688: - - SUPPLIER0 - SKU689: - - SUPPLIER0 - SKU69: - - SUPPLIER0 - SKU690: - - SUPPLIER0 - SKU691: - - SUPPLIER0 - SKU692: - - SUPPLIER0 - SKU693: - - SUPPLIER0 - SKU694: - - SUPPLIER0 - SKU695: - - SUPPLIER0 - SKU696: - - SUPPLIER0 - SKU697: - - SUPPLIER0 - SKU698: - - SUPPLIER0 - SKU699: - - SUPPLIER0 - SKU7: - - SUPPLIER0 - SKU70: - - SUPPLIER0 - SKU700: - - SUPPLIER0 - SKU701: - - SUPPLIER0 - SKU702: - - SUPPLIER0 - SKU703: - - SUPPLIER0 - SKU704: - - SUPPLIER0 - SKU705: - - SUPPLIER0 - SKU706: - - SUPPLIER0 - SKU707: - - SUPPLIER0 - SKU708: - - SUPPLIER0 - SKU709: - - SUPPLIER0 - SKU71: - - SUPPLIER0 - SKU710: - - SUPPLIER0 - SKU711: - - SUPPLIER0 - SKU712: - - SUPPLIER0 - SKU713: - - SUPPLIER0 - SKU714: - - SUPPLIER0 - SKU715: - - SUPPLIER0 - SKU716: - - SUPPLIER0 - SKU717: - - SUPPLIER0 - SKU718: - - SUPPLIER0 - SKU719: - - SUPPLIER0 - SKU72: - - SUPPLIER0 - SKU720: - - SUPPLIER0 - SKU721: - - SUPPLIER0 - SKU722: - - SUPPLIER0 - SKU723: - - SUPPLIER0 - SKU724: - - SUPPLIER0 - SKU725: - - SUPPLIER0 - SKU726: - - SUPPLIER0 - SKU727: - - SUPPLIER0 - SKU728: - - SUPPLIER0 - SKU729: - - SUPPLIER0 - SKU73: - - SUPPLIER0 - SKU730: - - SUPPLIER0 - SKU731: - - SUPPLIER0 - SKU732: - - SUPPLIER0 - SKU733: - - SUPPLIER0 - SKU734: - - SUPPLIER0 - SKU735: - - SUPPLIER0 - SKU736: - - SUPPLIER0 - SKU737: - - SUPPLIER0 - SKU738: - - SUPPLIER0 - SKU739: - - SUPPLIER0 - SKU74: - - SUPPLIER0 - SKU740: - - SUPPLIER0 - SKU741: - - SUPPLIER0 - SKU742: - - SUPPLIER0 - SKU743: - - SUPPLIER0 - SKU744: - - SUPPLIER0 - SKU745: - - SUPPLIER0 - SKU746: - - SUPPLIER0 - SKU747: - - SUPPLIER0 - SKU748: - - SUPPLIER0 - SKU749: - - SUPPLIER0 - SKU75: - - SUPPLIER0 - SKU750: - - SUPPLIER0 - SKU751: - - SUPPLIER0 - SKU752: - - SUPPLIER0 - SKU753: - - SUPPLIER0 - SKU754: - - SUPPLIER0 - SKU755: - - SUPPLIER0 - SKU756: - - SUPPLIER0 - SKU757: - - SUPPLIER0 - SKU758: - - SUPPLIER0 - SKU759: - - SUPPLIER0 - SKU76: - - SUPPLIER0 - SKU760: - - SUPPLIER0 - SKU761: - - SUPPLIER0 - SKU762: - - SUPPLIER0 - SKU763: - - SUPPLIER0 - SKU764: - - SUPPLIER0 - SKU765: - - SUPPLIER0 - SKU766: - - SUPPLIER0 - SKU767: - - SUPPLIER0 - SKU768: - - SUPPLIER0 - SKU769: - - SUPPLIER0 - SKU77: - - SUPPLIER0 - SKU770: - - SUPPLIER0 - SKU771: - - SUPPLIER0 - SKU772: - - SUPPLIER0 - SKU773: - - SUPPLIER0 - SKU774: - - SUPPLIER0 - SKU775: - - SUPPLIER0 - SKU776: - - SUPPLIER0 - SKU777: - - SUPPLIER0 - SKU778: - - SUPPLIER0 - SKU779: - - SUPPLIER0 - SKU78: - - SUPPLIER0 - SKU780: - - SUPPLIER0 - SKU781: - - SUPPLIER0 - SKU782: - - SUPPLIER0 - SKU783: - - SUPPLIER0 - SKU784: - - SUPPLIER0 - SKU785: - - SUPPLIER0 - SKU786: - - SUPPLIER0 - SKU787: - - SUPPLIER0 - SKU788: - - SUPPLIER0 - SKU789: - - SUPPLIER0 - SKU79: - - SUPPLIER0 - SKU790: - - SUPPLIER0 - SKU791: - - SUPPLIER0 - SKU792: - - SUPPLIER0 - SKU793: - - SUPPLIER0 - SKU794: - - SUPPLIER0 - SKU795: - - SUPPLIER0 - SKU796: - - SUPPLIER0 - SKU797: - - SUPPLIER0 - SKU798: - - SUPPLIER0 - SKU799: - - SUPPLIER0 - SKU8: - - SUPPLIER0 - SKU80: - - SUPPLIER0 - SKU800: - - SUPPLIER0 - SKU801: - - SUPPLIER0 - SKU802: - - SUPPLIER0 - SKU803: - - SUPPLIER0 - SKU804: - - SUPPLIER0 - SKU805: - - SUPPLIER0 - SKU806: - - SUPPLIER0 - SKU807: - - SUPPLIER0 - SKU808: - - SUPPLIER0 - SKU809: - - SUPPLIER0 - SKU81: - - SUPPLIER0 - SKU810: - - SUPPLIER0 - SKU811: - - SUPPLIER0 - SKU812: - - SUPPLIER0 - SKU813: - - SUPPLIER0 - SKU814: - - SUPPLIER0 - SKU815: - - SUPPLIER0 - SKU816: - - SUPPLIER0 - SKU817: - - SUPPLIER0 - SKU818: - - SUPPLIER0 - SKU819: - - SUPPLIER0 - SKU82: - - SUPPLIER0 - SKU820: - - SUPPLIER0 - SKU821: - - SUPPLIER0 - SKU822: - - SUPPLIER0 - SKU823: - - SUPPLIER0 - SKU824: - - SUPPLIER0 - SKU825: - - SUPPLIER0 - SKU826: - - SUPPLIER0 - SKU827: - - SUPPLIER0 - SKU828: - - SUPPLIER0 - SKU829: - - SUPPLIER0 - SKU83: - - SUPPLIER0 - SKU830: - - SUPPLIER0 - SKU831: - - SUPPLIER0 - SKU832: - - SUPPLIER0 - SKU833: - - SUPPLIER0 - SKU834: - - SUPPLIER0 - SKU835: - - SUPPLIER0 - SKU836: - - SUPPLIER0 - SKU837: - - SUPPLIER0 - SKU838: - - SUPPLIER0 - SKU839: - - SUPPLIER0 - SKU84: - - SUPPLIER0 - SKU840: - - SUPPLIER0 - SKU841: - - SUPPLIER0 - SKU842: - - SUPPLIER0 - SKU843: - - SUPPLIER0 - SKU844: - - SUPPLIER0 - SKU845: - - SUPPLIER0 - SKU846: - - SUPPLIER0 - SKU847: - - SUPPLIER0 - SKU848: - - SUPPLIER0 - SKU849: - - SUPPLIER0 - SKU85: - - SUPPLIER0 - SKU850: - - SUPPLIER0 - SKU851: - - SUPPLIER0 - SKU852: - - SUPPLIER0 - SKU853: - - SUPPLIER0 - SKU854: - - SUPPLIER0 - SKU855: - - SUPPLIER0 - SKU856: - - SUPPLIER0 - SKU857: - - SUPPLIER0 - SKU858: - - SUPPLIER0 - SKU859: - - SUPPLIER0 - SKU86: - - SUPPLIER0 - SKU860: - - SUPPLIER0 - SKU861: - - SUPPLIER0 - SKU862: - - SUPPLIER0 - SKU863: - - SUPPLIER0 - SKU864: - - SUPPLIER0 - SKU865: - - SUPPLIER0 - SKU866: - - SUPPLIER0 - SKU867: - - SUPPLIER0 - SKU868: - - SUPPLIER0 - SKU869: - - SUPPLIER0 - SKU87: - - SUPPLIER0 - SKU870: - - SUPPLIER0 - SKU871: - - SUPPLIER0 - SKU872: - - SUPPLIER0 - SKU873: - - SUPPLIER0 - SKU874: - - SUPPLIER0 - SKU875: - - SUPPLIER0 - SKU876: - - SUPPLIER0 - SKU877: - - SUPPLIER0 - SKU878: - - SUPPLIER0 - SKU879: - - SUPPLIER0 - SKU88: - - SUPPLIER0 - SKU880: - - SUPPLIER0 - SKU881: - - SUPPLIER0 - SKU882: - - SUPPLIER0 - SKU883: - - SUPPLIER0 - SKU884: - - SUPPLIER0 - SKU885: - - SUPPLIER0 - SKU886: - - SUPPLIER0 - SKU887: - - SUPPLIER0 - SKU888: - - SUPPLIER0 - SKU889: - - SUPPLIER0 - SKU89: - - SUPPLIER0 - SKU890: - - SUPPLIER0 - SKU891: - - SUPPLIER0 - SKU892: - - SUPPLIER0 - SKU893: - - SUPPLIER0 - SKU894: - - SUPPLIER0 - SKU895: - - SUPPLIER0 - SKU896: - - SUPPLIER0 - SKU897: - - SUPPLIER0 - SKU898: - - SUPPLIER0 - SKU899: - - SUPPLIER0 - SKU9: - - SUPPLIER0 - SKU90: - - SUPPLIER0 - SKU900: - - SUPPLIER0 - SKU901: - - SUPPLIER0 - SKU902: - - SUPPLIER0 - SKU903: - - SUPPLIER0 - SKU904: - - SUPPLIER0 - SKU905: - - SUPPLIER0 - SKU906: - - SUPPLIER0 - SKU907: - - SUPPLIER0 - SKU908: - - SUPPLIER0 - SKU909: - - SUPPLIER0 - SKU91: - - SUPPLIER0 - SKU910: - - SUPPLIER0 - SKU911: - - SUPPLIER0 - SKU912: - - SUPPLIER0 - SKU913: - - SUPPLIER0 - SKU914: - - SUPPLIER0 - SKU915: - - SUPPLIER0 - SKU916: - - SUPPLIER0 - SKU917: - - SUPPLIER0 - SKU918: - - SUPPLIER0 - SKU919: - - SUPPLIER0 - SKU92: - - SUPPLIER0 - SKU920: - - SUPPLIER0 - SKU921: - - SUPPLIER0 - SKU922: - - SUPPLIER0 - SKU923: - - SUPPLIER0 - SKU924: - - SUPPLIER0 - SKU925: - - SUPPLIER0 - SKU926: - - SUPPLIER0 - SKU927: - - SUPPLIER0 - SKU928: - - SUPPLIER0 - SKU929: - - SUPPLIER0 - SKU93: - - SUPPLIER0 - SKU930: - - SUPPLIER0 - SKU931: - - SUPPLIER0 - SKU932: - - SUPPLIER0 - SKU933: - - SUPPLIER0 - SKU934: - - SUPPLIER0 - SKU935: - - SUPPLIER0 - SKU936: - - SUPPLIER0 - SKU937: - - SUPPLIER0 - SKU938: - - SUPPLIER0 - SKU939: - - SUPPLIER0 - SKU94: - - SUPPLIER0 - SKU940: - - SUPPLIER0 - SKU941: - - SUPPLIER0 - SKU942: - - SUPPLIER0 - SKU943: - - SUPPLIER0 - SKU944: - - SUPPLIER0 - SKU945: - - SUPPLIER0 - SKU946: - - SUPPLIER0 - SKU947: - - SUPPLIER0 - SKU948: - - SUPPLIER0 - SKU949: - - SUPPLIER0 - SKU95: - - SUPPLIER0 - SKU950: - - SUPPLIER0 - SKU951: - - SUPPLIER0 - SKU952: - - SUPPLIER0 - SKU953: - - SUPPLIER0 - SKU954: - - SUPPLIER0 - SKU955: - - SUPPLIER0 - SKU956: - - SUPPLIER0 - SKU957: - - SUPPLIER0 - SKU958: - - SUPPLIER0 - SKU959: - - SUPPLIER0 - SKU96: - - SUPPLIER0 - SKU960: - - SUPPLIER0 - SKU961: - - SUPPLIER0 - SKU962: - - SUPPLIER0 - SKU963: - - SUPPLIER0 - SKU964: - - SUPPLIER0 - SKU965: - - SUPPLIER0 - SKU966: - - SUPPLIER0 - SKU967: - - SUPPLIER0 - SKU968: - - SUPPLIER0 - SKU969: - - SUPPLIER0 - SKU97: - - SUPPLIER0 - SKU970: - - SUPPLIER0 - SKU971: - - SUPPLIER0 - SKU972: - - SUPPLIER0 - SKU973: - - SUPPLIER0 - SKU974: - - SUPPLIER0 - SKU975: - - SUPPLIER0 - SKU976: - - SUPPLIER0 - SKU977: - - SUPPLIER0 - SKU978: - - SUPPLIER0 - SKU979: - - SUPPLIER0 - SKU98: - - SUPPLIER0 - SKU980: - - SUPPLIER0 - SKU981: - - SUPPLIER0 - SKU982: - - SUPPLIER0 - SKU983: - - SUPPLIER0 - SKU984: - - SUPPLIER0 - SKU985: - - SUPPLIER0 - SKU986: - - SUPPLIER0 - SKU987: - - SUPPLIER0 - SKU988: - - SUPPLIER0 - SKU989: - - SUPPLIER0 - SKU99: - - SUPPLIER0 - SKU990: - - SUPPLIER0 - SKU991: - - SUPPLIER0 - SKU992: - - SUPPLIER0 - SKU993: - - SUPPLIER0 - SKU994: - - SUPPLIER0 - SKU995: - - SUPPLIER0 - SKU996: - - SUPPLIER0 - SKU997: - - SUPPLIER0 - SKU998: - - SUPPLIER0 - SKU999: - - SUPPLIER0 diff --git a/examples/supply_chain/topologies/sample1/config.yml b/examples/supply_chain/topologies/sample1/config.yml deleted file mode 100644 index eadec95f9..000000000 --- a/examples/supply_chain/topologies/sample1/config.yml +++ /dev/null @@ -1,327 +0,0 @@ - -# TODO: which config to inherit -# base: "" - -#core: -# datamodels: "xxx" -# units: "xxx" -# facilities: "xxx" - - -facility_definitions: - # facility definition - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - # if true then will call generate function of class type - is_template: true - # config will be passed to generator as parameters - config: - agent_type: 4 - consumer: - class: "ConsumerUnit" - config: - agent_type: 1 - - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 3 - consumer: - class: "ConsumerUnit" - manufacture: - class: "ManufactureUnit" - config: - agent_type: 0 - - RetailerFacility: &retailer_facility - class: "RetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "SellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: 2 - - - # definition for outer retailer that read demand from csv files. - # NOTE: make sure you provide a valid file path for each retailer instance. - OuterRetailerFacility: &outerretailer_facility - class: "OuterRetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "OuterSellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: 2 - seller_sampler_type: data # data, model. What kind of sampler to use for seller. - sku_column: "SKU" # SKU column name - price_column: "Price" # Price column name - sale_column: "Sales" # Sales column name - datetime_column: "DT" # Datetime column name - file_path: "/path/to/data.csv" # full path to data file, override by each store instance - - -# common entity/unit definition as reference to simplify the file. -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 100 - -# a normal distribution definition -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -small_storage: &small_storage - # config of data model of this unit - config: - # other config or storage unit - capacity: 10000 - unit_storage_cost: 1 - -midium_storage: &midium_storage - config: - capacity: 20000 - unit_storage_cost: 1 - -huge_storage: &huge_storage - config: - capacity: 30000 - unit_storage_cost: 1 - -# sku list in this world -# this list do not contains price, cost or other facility related attributes, -# but just base info, like name, id, bom -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 12 - # bill of material that used produce current sku, empty means do not need source material - bom: - # key is the source sku name, value is quantity needed to use per time to produce current sku - sku3: 10 - - - id: 2 - name: "sku2" - output_units_per_lot: 1 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - -# world definitions -world: - # here we use reference to make it each to edit. - skus: *sku_definitions - - # facilities in this world - facilities: - - name: "Supplier_001" # name of the facility - # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level - # use the facility definition as base, then we can override configs partially. - definition_ref: "SupplierFacility" - - # sku list of this facility - skus: - sku3: # sku name and attributes needed for this facility - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" # production means this is the output production of this facility - cost: 10 - price: 10 - vlt: 1 - - # configuration of child units. - children: - # config of storage unit - storage: *small_storage - distribution: *normal_distribution - - # products use default config in core.yml - - # config of this facility - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Supplier_002" - definition_ref: "SupplierFacility" - - skus: - sku1: - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" - cost: 10 - price: 100 - vlt: 1 - sku3: - init_stock: 100 - production_rate: 1 - type: "material" - cost: 10 - price: 100 - vlt: 1 - - children: - storage: *small_storage - distribution: *normal_distribution - - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - - skus: - sku1: - init_stock: 1000 - price: 100 - vlt: 1 - sku2: - init_stock: 1000 - price: 100 - vlt: 1 - sku3: - init_stock: 1000 - price: 100 - vlt: 1 - - children: - storage: *huge_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Retailer_001" - definition_ref: "RetailerFacility" - - skus: - sku1: - price: 300 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 # optional - vlt: 1 - sku3: - price: 200 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - sku2: - price: 100 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - - children: - storage: *midium_storage - - config: - order_cost: 0 - # topology used to specify the up/downstream for facilities - # we split it from facility, so that we can support configuration inherit to override it - # for a new topology - # TODO: change the name? - topology: - # key is current facility, value if upstream facilities that will provide a certain sku - Supplier_002: - # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", - # or any other facility in the list - sku3: - - "Supplier_001" - Warehouse_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - Retailer_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - - # map grid definitions - grid: - size: [20, 20] - - # facility position in grid - facilities: - Supplier_001: [0, 0] - Supplier_002: [3, 3] - Warehouse_001: [6, 6] - Retailer_001: [10, 18] - - # cells that un-traversable - blocks: - railroad: - - [10, 10] - - [10, 11] - - [10, 12] - - [11, 12] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 - start_date_time: "2010-12-01" # start time for data in files, for outer retailer diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py deleted file mode 100644 index 03990fd5c..000000000 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .actions import ConsumerAction, ManufactureAction -from .datamodels import ( - ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel -) -from .facilities import FacilityBase, RetailerFacility, SupplierFacility, WarehouseFacility -from .units import ConsumerUnit, DistributionUnit, ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/actions.py b/maro/simulator/scenarios/supply_chain/actions.py deleted file mode 100644 index b6791a851..000000000 --- a/maro/simulator/scenarios/supply_chain/actions.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from collections import namedtuple - -ConsumerAction = namedtuple("ConsumerAction", ("id", "product_id", "source_id", "quantity", "vlt", "reward_discount")) - -ManufactureAction = namedtuple("ManufactureAction", ("id", "production_rate")) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py deleted file mode 100644 index d22d2cfe1..000000000 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -import os -from typing import List, Tuple - -from maro.event_buffer import MaroEvents -from maro.simulator.scenarios import AbsBusinessEngine - -from .parser import ConfigParser, SupplyChainConfiguration -from .units import ProductUnit, UnitBase -from .world import World - - -class SupplyChainBusinessEngine(AbsBusinessEngine): - def __init__(self, **kwargs): - super().__init__(scenario_name="supply_chain", **kwargs) - - self._register_events() - - self._build_world() - - self._product_units = [] - - # Prepare product unit for later using. - for unit in self.world.units.values(): - if issubclass(type(unit), ProductUnit): - self._product_units.append(unit) - - self._frame = self.world.frame - - self._node_mapping = self.world.get_node_mapping() - - # Used to cache the action from outside, then dispatch to units at the beginning of step. - self._action_cache = None - - self._metrics_cache = None - - @property - def frame(self): - return self._frame - - @property - def snapshots(self): - return self._frame.snapshots - - @property - def configs(self) -> SupplyChainConfiguration: - return self.world.configs - - def step(self, tick: int): - # Clear the metrics cache. - self._metrics_cache = None - - # NOTE: we have to dispatch the action here. - self._dispatch_action() - self._step_by_facility(tick) - - # We do not have payload here. - decision_event = self._event_buffer.gen_decision_event(tick, None) - - self._event_buffer.insert_event(decision_event) - - def post_step(self, tick: int): - self._post_step_by_facility(tick) - - return tick + 1 == self._max_tick - - def reset(self): - self._frame.reset() - - if self._frame.snapshots: - self._frame.snapshots.reset() - - self._reset_by_facility() - - def get_node_mapping(self) -> dict: - return self._node_mapping - - def get_agent_idx_list(self) -> List[Tuple[str, int]]: - """Get a list of agent index. - - Returns: - list: List of agent index. - """ - return self.world.agent_list - - def _step_by_facility(self, tick: int): - """Call step functions by facility. - - Args: - tick (int): Current tick. - """ - # Step first. - for facility in self.world.facilities.values(): - facility.step(tick) - - # Then flush states to frame before generate decision event. - for facility in self.world.facilities.values(): - facility.flush_states() - - def _post_step_by_facility(self, tick: int): - """Call post_step functions by facility.""" - for facility in self.world.facilities.values(): - facility.post_step(tick) - - def _reset_by_facility(self): - """Call reset functions by facility.""" - for facility in self.world.facilities.values(): - facility.reset() - - def _register_events(self): - self._event_buffer.register_event_handler( - MaroEvents.TAKE_ACTION, self._on_action_received) - - def _build_world(self): - self.update_config_root_path(__file__) - - # Core configuration always in topologies folder. - be_root = os.path.split(os.path.realpath(__file__))[0] - core_config = os.path.join(be_root, "topologies", "core.yml") - - config_path = os.path.join(self._config_path, "config.yml") - - parser = ConfigParser(core_config, config_path) - - conf = parser.parse() - - self.world = World() - - self.world.build(conf, self.calc_max_snapshots(), self._max_tick) - - def _on_action_received(self, event): - action = event.payload - - if action is not None and type(action) == dict and len(action) > 0: - self._action_cache = action - - def _dispatch_action(self): - if self._action_cache is not None: - # NOTE: we assume that the action is dictionary that key is the unit(agent) id, value is the real action. - for unit_id, action_obj in self._action_cache.items(): - entity = self.world.get_entity(unit_id) - - if entity is not None and issubclass(type(entity), UnitBase): - entity.set_action(action_obj) - - self._action_cache = None - - def get_metrics(self): - if self._metrics_cache is None: - self._metrics_cache = { - "products": { - product.id: { - "sale_mean": product.get_sale_mean(), - "sale_std": product.get_sale_std(), - "selling_price": product.get_selling_price(), - "pending_order_daily": - None if product.consumer is None else product.consumer.pending_order_daily - } for product in self._product_units - }, - "facilities": { - facility.id: { - "in_transit_orders": facility.get_in_transit_orders(), - "pending_order": - None if facility.distribution is None else facility.distribution.get_pending_order() - } for facility in self.world.facilities.values() - } - } - - return self._metrics_cache diff --git a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py deleted file mode 100644 index c1a8c9b9a..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .consumer import ConsumerDataModel -from .distribution import DistributionDataModel -from .facility import FacilityDataModel -from .manufacture import ManufactureDataModel -from .product import ProductDataModel -from .seller import SellerDataModel -from .storage import StorageDataModel -from .vehicle import VehicleDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py deleted file mode 100644 index 834864072..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/base.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, NodeBase - - -class DataModelBase(NodeBase): - """Base of all data model of this scenario.""" - # Id of related unit or facility, 0 is invalid by default. - id = NodeAttribute(AttributeType.Int) - - # Id of facility this unit belongs to. - facility_id = NodeAttribute(AttributeType.Int) - - def __init__(self): - self._unit_id = 0 - self._facility_id = 0 - - def initialize(self, **kwargs): - """Initialize the fields with configs, the config should be a dict. - - Args: - kwargs (dict): Configuration of related data model, used to hold value to - reset after frame reset. - """ - # Called from unit after frame is ready. - pass - - def reset(self): - """Reset after each episode.""" - # Called per episode. - self.id = self._unit_id - self.facility_id = self._facility_id - - def set_id(self, unit_id: int, facility_id: int): - """Used to assign id(s), so that it will be assigned after frame rest. - - Args: - unit_id (int): Id of related unit. - facility_id (int): Id of this unit belongs to. - """ - self._unit_id = unit_id - self._facility_id = facility_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py deleted file mode 100644 index 616577878..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .skumodel import SkuDataModel - - -@node("consumer") -class ConsumerDataModel(SkuDataModel): - """Data model for consumer unit.""" - total_purchased = NodeAttribute(AttributeType.UInt) - total_received = NodeAttribute(AttributeType.UInt) - - purchased = NodeAttribute(AttributeType.UInt) - received = NodeAttribute(AttributeType.UInt) - order_product_cost = NodeAttribute(AttributeType.UInt) - - latest_consumptions = NodeAttribute(AttributeType.Float) - pending_order_daily = NodeAttribute(AttributeType.UInt) - - order_quantity = NodeAttribute(AttributeType.UInt) - - price = NodeAttribute(AttributeType.Float) - order_cost = NodeAttribute(AttributeType.Float) - - reward_discount = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(ConsumerDataModel, self).__init__() - - self._price = 0 - self._order_cost = 0 - - def initialize(self, price: int, order_cost: int): - self._price = price - self._order_cost = order_cost - - self.reset() - - def reset(self): - super(ConsumerDataModel, self).reset() - - self.price = self._price - self.order_cost = self._order_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py deleted file mode 100644 index ee28e4c7f..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .base import DataModelBase - - -@node("distribution") -class DistributionDataModel(DataModelBase): - """Distribution data model for distribution unit.""" - - remaining_order_quantity = NodeAttribute(AttributeType.UInt) - remaining_order_number = NodeAttribute(AttributeType.UInt) - - def __init__(self): - super(DistributionDataModel, self).__init__() - - def reset(self): - super(DistributionDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/facility.py b/maro/simulator/scenarios/supply_chain/datamodels/facility.py deleted file mode 100644 index 8aae3d146..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/facility.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.frame import node - -from .base import DataModelBase - - -@node("facility") -class FacilityDataModel(DataModelBase): - - def __init__(self): - super(FacilityDataModel, self).__init__() - - def reset(self): - super(FacilityDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py deleted file mode 100644 index 18bd7defe..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .skumodel import SkuDataModel - - -@node("manufacture") -class ManufactureDataModel(SkuDataModel): - """Data model for manufacture unit.""" - # Number per tick, different with original manufacturing cost, we just provide number, and cost - # user can determine how to calculate the cost. - manufacturing_number = NodeAttribute(AttributeType.UInt) - - product_unit_cost = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(ManufactureDataModel, self).__init__() - - self._product_unit_cost = 0 - - def initialize(self, product_unit_cost): - self._product_unit_cost = product_unit_cost - - self.reset() - - def reset(self): - super(ManufactureDataModel, self).reset() - - self.product_unit_cost = self._product_unit_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/product.py b/maro/simulator/scenarios/supply_chain/datamodels/product.py deleted file mode 100644 index 385bd6b97..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/product.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .skumodel import SkuDataModel - - -@node("product") -class ProductDataModel(SkuDataModel): - - distribution_check_order = NodeAttribute(AttributeType.UInt) - distribution_transport_cost = NodeAttribute(AttributeType.Float) - distribution_delay_order_penalty = NodeAttribute(AttributeType.Float) - - price = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(ProductDataModel, self).__init__() - - self._price = 0 - - def initialize(self, price: float): - self._price = price - - self.reset() - - def reset(self): - super(ProductDataModel, self).reset() - - self.price = self._price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py deleted file mode 100644 index 153e2045c..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .skumodel import SkuDataModel - - -@node("seller") -class SellerDataModel(SkuDataModel): - """Data model for seller unit.""" - total_sold = NodeAttribute(AttributeType.UInt) - - demand = NodeAttribute(AttributeType.UInt) - sold = NodeAttribute(AttributeType.UInt) - total_demand = NodeAttribute(AttributeType.UInt) - total_sold = NodeAttribute(AttributeType.UInt) - price = NodeAttribute(AttributeType.Float) - backlog_ratio = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(SellerDataModel, self).__init__() - self._price = 0 - self._backlog_ratio = 0 - - def initialize(self, price: int, backlog_ratio: float): - self._price = price - self._backlog_ratio = backlog_ratio - - self.reset() - - def reset(self): - super(SellerDataModel, self).reset() - - self.backlog_ratio = self._backlog_ratio - self.price = self._price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py b/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py deleted file mode 100644 index 17a165f26..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute - -from .base import DataModelBase - - -class SkuDataModel(DataModelBase): - """Data model for sku related unit.""" - # Product id of this consumer belongs to. - product_id = NodeAttribute(AttributeType.UInt) - - # Parent unit id. - product_unit_id = NodeAttribute(AttributeType.UInt) - - def __init__(self): - super(SkuDataModel, self).__init__() - - self._product_id = 0 - self._product_unit_id = 0 - - def reset(self): - super(SkuDataModel, self).reset() - - self.product_id = self._product_id - self.product_unit_id = self._product_unit_id - - def set_product_id(self, product_id: int, product_unit_id: int): - self._product_id = product_id - self._product_unit_id = product_unit_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py deleted file mode 100644 index 2e1280961..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/storage.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .base import DataModelBase - - -@node("storage") -class StorageDataModel(DataModelBase): - """Data model for storage unit.""" - remaining_space = NodeAttribute(AttributeType.UInt) - capacity = NodeAttribute(AttributeType.UInt) - - # original is , used to save product and its number - product_list = NodeAttribute(AttributeType.UInt, 1, is_list=True) - product_number = NodeAttribute(AttributeType.UInt, 1, is_list=True) - - def __init__(self): - super(StorageDataModel, self).__init__() - - self._capacity = 0 - self._remaining_space = None - self._product_list = None - self._product_number = None - - def initialize( - self, - capacity: int = 0, - remaining_space: int = None, - product_list: list = None, - product_number: list = None - ): - self._capacity = capacity - self._remaining_space = remaining_space - self._product_list = product_list - self._product_number = product_number - - self.reset() - - def reset(self): - super(StorageDataModel, self).reset() - - self.capacity = self._capacity - - if self._remaining_space is not None: - self.remaining_space = self._remaining_space - else: - self.remaining_space = self._capacity - - if self._product_list is not None: - for id in self._product_list: - self.product_list.append(id) - - if self._product_number is not None: - for n in self._product_number: - self.product_number.append(n) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py deleted file mode 100644 index 34cb1df64..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .base import DataModelBase - - -@node("vehicle") -class VehicleDataModel(DataModelBase): - # Number of product. - payload = NodeAttribute(AttributeType.UInt) - - unit_transport_cost = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(VehicleDataModel, self).__init__() - - self._unit_transport_cost = 1 - - def initialize(self, unit_transport_cost: int = 1): - self._unit_transport_cost = unit_transport_cost - - self.reset() - - def reset(self): - super(VehicleDataModel, self).reset() - - self.unit_transport_cost = self._unit_transport_cost diff --git a/maro/simulator/scenarios/supply_chain/easy_config.py b/maro/simulator/scenarios/supply_chain/easy_config.py deleted file mode 100644 index 83e5f7551..000000000 --- a/maro/simulator/scenarios/supply_chain/easy_config.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -class EasyConfig(dict): - """A wrapper base on dictionary, give it ability to access key as property.""" - - # Default value for not exist keys. - default_values: dict = {} - - def __getattr__(self, key): - if key in self: - return self[key] - - if key in self.default_values: - return self.default_values[key] - - return None - - def __setattr__(self, key, value): - self[key] = value - - -class SkuInfo(EasyConfig): - """Sku information wrapper, with default property value.""" - default_values = { - "price": 0, - "vlt": 1, - "product_unit_cost": 0, - "backlog_ratio": 0, - "service_level": 0.90, - "cost": 0 - } diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py deleted file mode 100644 index 7ce1f382d..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .facility import FacilityBase -from .outerretailer import OuterRetailerFacility -from .retailer import RetailerFacility -from .supplier import SupplierFacility -from .warehouse import WarehouseFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py deleted file mode 100644 index c5a3e6781..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC -from collections import defaultdict -from typing import Dict, List - -from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo -from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit - - -class FacilityBase(ABC): - """Base of all facilities.""" - - # Id of this facility. - id: int = None - - # Name of this facility. - name: str = None - - # World of this facility belongs to. - world = None - - # Skus in this facility. - skus: Dict[int, SkuInfo] = None - - # Product units for each sku in this facility. - # Key is sku(product) id, value is the instance of product unit. - products: Dict[int, ProductUnit] = None - - # Storage unit in this facility. - storage: StorageUnit = None - - # Distribution unit in this facility. - distribution: DistributionUnit = None - - # Upstream facilities. - # Key is sku id, value is the list of product unit from upstream. - upstreams: Dict[int, List[ProductUnit]] = None - - # Down stream facilities, value same as upstreams. - downstreams: Dict[int, List[ProductUnit]] = None - - # Configuration of this facility. - configs: dict = None - - # Name of data model, from configuration. - data_model_name: str = None - - # Index of the data model node. - data_model_index: int = 0 - - data_model: object = None - - # Children of this facility (valid units). - children: list = None - - def __init__(self): - self.upstreams = {} - self.downstreams = {} - self.children = [] - self.skus = {} - - def parse_skus(self, configs: dict): - """Parse sku information from config. - - Args: - configs (dict): Configuration of skus belongs to this facility. - """ - for sku_name, sku_config in configs.items(): - global_sku = self.world.get_sku_by_name(sku_name) - facility_sku = SkuInfo(sku_config) - facility_sku.id = global_sku.id - - self.skus[global_sku.id] = facility_sku - - def parse_configs(self, configs: dict): - """Parse configuration of this facility. - - Args: - configs (dict): Configuration of this facility. - """ - self.configs = configs - - def get_config(self, key: str, default: object = None) -> object: - """Get specified configuration of facility. - - Args: - key (str): Key of the configuration. - default (object): Default value if key not exist, default is None. - - Returns: - object: value in configuration. - """ - return default if self.configs is None else self.configs.get(key, default) - - def initialize(self): - """Initialize this facility after frame is ready.""" - self.data_model.initialize() - - # Put valid units into the children, used to simplify following usage. - if self.storage is not None: - self.children.append(self.storage) - - if self.distribution is not None: - self.children.append(self.distribution) - - if self.products is not None: - for product in self.products.values(): - self.children.append(product) - - def step(self, tick: int): - """Push facility to next step. - - Args: - tick (int): Current simulator tick. - """ - for unit in self.children: - unit.step(tick) - - def flush_states(self): - """Flush states into frame.""" - for unit in self.children: - unit.flush_states() - - def post_step(self, tick: int): - """Post processing at the end of step.""" - for unit in self.children: - unit.post_step(tick) - - def reset(self): - """Reset facility for new episode.""" - for unit in self.children: - unit.reset() - - if self.data_model is not None: - self.data_model.reset() - - def get_in_transit_orders(self): - in_transit_orders = defaultdict(int) - - for product_id, product in self.products.items(): - if product.consumer is not None: - in_transit_orders[product_id] = product.consumer.get_in_transit_quantity() - - return in_transit_orders - - def set_action(self, action: object): - pass - - def get_node_info(self) -> dict: - products_info = {} - - for product_id, product in self.products.items(): - products_info[product_id] = product.get_unit_info() - - return { - "id": self.id, - "name": self.name, - "class": type(self), - "node_index": self.data_model_index, - "units": { - "storage": self.storage.get_unit_info() if self.storage is not None else None, - "distribution": self.distribution.get_unit_info() if self.distribution is not None else None, - "products": products_info - }, - "configs": self.configs, - "skus": self.skus, - "upstreams": { - product_id: [f.id for f in source_list] - for product_id, source_list in self.upstreams.items() - }, - "downstreams": { - product_id: [f.id for f in source_list] - for product_id, source_list in self.downstreams.items() - } - } diff --git a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py deleted file mode 100644 index 1d8e85efe..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.simulator.scenarios.supply_chain.units import DataFileDemandSampler, OuterSellerUnit - -from .retailer import RetailerFacility - -# Mapping for supported sampler. -sampler_mapping = { - "data": DataFileDemandSampler -} - - -class OuterRetailerFacility(RetailerFacility): - """Retailer (store) facility that use outer data as seller demand. - - NOTE: - This require that all product seller is subclass of OuterSellerUnit. - """ - - def initialize(self): - super(OuterRetailerFacility, self).initialize() - - # What kind of sampler we need? - sampler_cls = sampler_mapping[self.configs.get("seller_sampler_type", "data")] - - sampler = sampler_cls(self.configs, self.world) - - # Go though product to find sellers. - for product in self.products.values(): - seller = product.seller - - if seller is not None: - assert issubclass(type(seller), OuterSellerUnit) - - seller.sampler = sampler diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py deleted file mode 100644 index 6a08fc64d..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .facility import FacilityBase - - -class RetailerFacility(FacilityBase): - """Retail facility used to generate order from upstream, and sell products by demand.""" - pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py deleted file mode 100644 index 7ddface02..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .facility import FacilityBase - - -class SupplierFacility(FacilityBase): - """Supplier facilities used to produce products with material products.""" - pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py deleted file mode 100644 index 97725d171..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .facility import FacilityBase - - -class WarehouseFacility(FacilityBase): - """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" - pass diff --git a/maro/simulator/scenarios/supply_chain/frame_builder.py b/maro/simulator/scenarios/supply_chain/frame_builder.py deleted file mode 100644 index 9cde1d97a..000000000 --- a/maro/simulator/scenarios/supply_chain/frame_builder.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import List, Tuple - -from maro.backends.frame import FrameBase, FrameNode, NodeBase - - -def build_frame(enable_snapshot: bool, total_snapshots: int, nodes: List[Tuple[NodeBase, str, int]]): - class Frame(FrameBase): - def __init__(self): - # Inject the node definition to frame to support add node dynamically. - for node_cls, name, number in nodes: - setattr(Frame, name, FrameNode(node_cls, number)) - - super().__init__(enable_snapshot=enable_snapshot, total_snapshot=total_snapshots, backend_name="dynamic") - - return Frame() diff --git a/maro/simulator/scenarios/supply_chain/parser.py b/maro/simulator/scenarios/supply_chain/parser.py deleted file mode 100644 index 16046e07a..000000000 --- a/maro/simulator/scenarios/supply_chain/parser.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from collections import namedtuple -from importlib import import_module - -from yaml import safe_load - -DataModelDef = namedtuple("DataModelDef", ("alias", "module_path", "class_name", "class_type", "name_in_frame")) -UnitDef = namedtuple("UnitDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) -FacilityDef = namedtuple("FacilityDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) - - -def find_class_type(module_path: str, class_name: str) -> type: - """Find class type by module path and class name. - - Args: - module_path (str): Full path of the module. - class_name (str): Class name to find. - - Returns: - type: Type of specified class. - """ - target_module = import_module(module_path) - - return getattr(target_module, class_name) - - -def copy_dict(target: dict, source: dict): - """Copy values from source to target dict. - - Args: - target (dict): Target dictionary to copy to. - source (dict): Source dictionary to copy from. - """ - for k, v in source.items(): - if type(v) != dict: - target[k] = v - else: - if k not in target: - target[k] = {} - - copy_dict(target[k], v) - - -class SupplyChainConfiguration: - """Configuration of supply chain scenario.""" - - def __init__(self): - # Data model definitions. - self.data_models = {} - - # Unit definitions. - self.units = {} - - # Facility definitions. - self.facilities = {} - - # World configurations. - self.world = {} - - # Other settings. - self.settings = {} - - def add_data_definition(self, alias: str, class_name: str, module_path: str, name_in_frame: str): - """Add a data model definition. - - Args: - alias (str): Alias of this data model. - class_name (str): Name of class. - module_path (str): Full path of module. - name_in_frame (str): Data model name in frame. - """ - # Check conflicting. - assert alias not in self.data_models - - self.data_models[alias] = DataModelDef( - alias, - module_path, - class_name, - find_class_type(module_path, class_name), - name_in_frame - ) - - def add_unit_definition(self, alias: str, class_name: str, module_path: str, data_model: str): - """Add unit definition. - - Args: - alias (str): Alias of this data model. - class_name (str): Name of class. - module_path (str): Full path of module. - data_model (str): Data model used for this unit. - """ - assert alias not in self.units - - self.units[alias] = UnitDef( - alias, - module_path, - class_name, - find_class_type(module_path, class_name), - data_model - ) - - def add_facility_definition(self, alias: str, class_name: str, module_path: str, data_model_alias: str): - """Add a facility definition. - - Args: - alias (str): Alias of this facility. - class_name (str): Name of this class. - module_path (str): Full path of the module. - """ - assert alias not in self.facilities - - self.facilities[alias] = FacilityDef( - alias, - module_path, - class_name, - find_class_type(module_path, class_name), - data_model_alias - ) - - -class ConfigParser: - """Supply chain configuration parser.""" - - def __init__(self, core_path: str, config_path: str): - self._result = SupplyChainConfiguration() - - self._core_path = core_path - self._config_path = config_path - - def parse(self): - """Parse configuration of current scenario. - - Returns: - SupplyChainConfiguration: Configuration result of this scenario. - """ - self._parse_core() - self._parse_config() - - return self._result - - def _parse_core(self): - """Parse configuration from core.yml.""" - with open(self._core_path, "rt") as fp: - conf = safe_load(fp) - - self._parse_core_conf(conf) - - def _parse_core_conf(self, conf: dict): - # Data models. - if "datamodels" in conf: - for module_conf in conf["datamodels"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_data_definition( - class_alias, - class_def["class"], - module_path, - class_def["name_in_frame"] - ) - - # TODO: dup code - # Units. - if "units" in conf: - for module_conf in conf["units"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - # children not in unit definition - self._result.add_unit_definition( - class_alias, - class_def["class"], - module_path, - class_def.get("datamodel", None) - ) - - # Facilities. - if "facilities" in conf: - for module_conf in conf["facilities"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_facility_definition( - class_alias, - class_def["class"], - module_path, - class_def.get("datamodel", None) - ) - - def _parse_config(self): - """Parse configurations.""" - with open(self._config_path, "rt") as fp: - conf = safe_load(fp) - - # Read customized core part. - customized_core_conf = conf.get("core", None) - - if customized_core_conf is not None: - self._parse_core_conf(customized_core_conf) - - # Facility definitions is not required, but it would be much simple to config with it - facility_definitions = conf.get("facility_definitions", {}) - world_def = conf["world"] - - # Go through world configurations to generate a full one. - # . Copy other configurations first - for sub_conf_name in ("skus", "topology", "grid"): - self._result.world[sub_conf_name] = world_def[sub_conf_name] - - # . Copy facilities content different if without definition reference. - # or copy from definition first, then override with current. - self._result.world["facilities"] = [] - - for facility_conf in world_def["facilities"]: - facility_ref = facility_conf.get("definition_ref", None) - - facility = {} - - if facility_ref is not None: - # Copy definition from base. - copy_dict(facility, facility_definitions[facility_ref]) - - # Override with current. - copy_dict(facility, facility_conf) - - self._result.world["facilities"].append(facility) - - self._result.settings = conf.get("settings", {}) diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml deleted file mode 100644 index f650719a2..000000000 --- a/maro/simulator/scenarios/supply_chain/topologies/core.yml +++ /dev/null @@ -1,81 +0,0 @@ - -datamodels: - modules: - - path: "maro.simulator.scenarios.supply_chain.datamodels" - definitions: - StorageDataModel: - class: "StorageDataModel" - name_in_frame: "storage" - VehicleDataModel: - class: "VehicleDataModel" - name_in_frame: "vehicle" - DistributionDataModel: - class: "DistributionDataModel" - name_in_frame: "distribution" - ConsumerDataModel: - class: "ConsumerDataModel" - name_in_frame: "consumer" - SellerDataModel: - class: "SellerDataModel" - name_in_frame: "seller" - ManufactureDataModel: - class: "ManufactureDataModel" - name_in_frame: "manufacture" - ProductDataModel: - class: "ProductDataModel" - name_in_frame: "product" - FacilityDataModel: - class: "FacilityDataModel" - name_in_frame: "facility" - -units: - modules: - - path: "maro.simulator.scenarios.supply_chain.units" - definitions: - StorageUnit: - class: "StorageUnit" - datamodel: "StorageDataModel" - VehicleUnit: - class: "VehicleUnit" - datamodel: "VehicleDataModel" - DistributionUnit: - class: "DistributionUnit" - datamodel: "DistributionDataModel" - ConsumerUnit: - class: "ConsumerUnit" - datamodel: "ConsumerDataModel" - SellerUnit: - class: "SellerUnit" - datamodel: "SellerDataModel" - OuterSellerUnit: - class: "OuterSellerUnit" - datamodel: "SellerDataModel" - ManufactureUnit: - class: "ManufactureUnit" - datamodel: "ManufactureDataModel" - SimpleManufactureUnit: - class: "SimpleManufactureUnit" - datamodel: "ManufactureDataModel" - ProductUnit: - class: "ProductUnit" - datamodel: "ProductDataModel" - StoreProductUnit: - class: "StoreProductUnit" - datamodel: "ProductDataModel" - -facilities: - modules: - - path: "maro.simulator.scenarios.supply_chain.facilities" - definitions: - WarehouseFacility: - class: "WarehouseFacility" - datamodel: "FacilityDataModel" - SupplierFacility: - class: "SupplierFacility" - datamodel: "FacilityDataModel" - RetailerFacility: - class: "RetailerFacility" - datamodel: "FacilityDataModel" - OuterRetailerFacility: - class: "OuterRetailerFacility" - datamodel: "FacilityDataModel" diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml deleted file mode 100644 index f34f7ca3c..000000000 --- a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml +++ /dev/null @@ -1,297 +0,0 @@ - -# TODO: which config to inherit -# base: "" - -#core: -# datamodels: "xxx" -# units: "xxx" -# facilities: "xxx" - - -facility_definitions: - # facility definition - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - # if true then will call generate function of class type - is_template: true - # config will be passed to generator as parameters - config: - agent_type: 4 - consumer: - class: "ConsumerUnit" - config: - agent_type: 1 - - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 3 - consumer: - class: "ConsumerUnit" - manufacture: - class: "ManufactureUnit" - config: - agent_type: 0 - - RetailerFacility: &retailer_facility - class: "RetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "SellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: 2 - -# common entity/unit definition as reference to simplify the file. -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 100 - -# a normal distribution definition -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -small_storage: &small_storage - # config of data model of this unit - config: - # other config or storage unit - capacity: 10000 - unit_storage_cost: 1 - -midium_storage: &midium_storage - config: - capacity: 20000 - unit_storage_cost: 1 - -huge_storage: &huge_storage - config: - capacity: 30000 - unit_storage_cost: 1 - -# sku list in this world -# this list do not contains price, cost or other facility related attributes, -# but just base info, like name, id, bom -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 12 - # bill of material that used produce current sku, empty means do not need source material - bom: - # key is the source sku name, value is quantity needed to use per time to produce current sku - sku3: 10 - - - id: 2 - name: "sku2" - output_units_per_lot: 1 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - -# world definitions -world: - # here we use reference to make it each to edit. - skus: *sku_definitions - - # facilities in this world - facilities: - - name: "Supplier_001" # name of the facility - # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level - # use the facility definition as base, then we can override configs partially. - definition_ref: "SupplierFacility" - - # sku list of this facility - skus: - sku3: # sku name and attributes needed for this facility - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" # production means this is the output production of this facility - cost: 10 - price: 10 - vlt: 1 - - # configuration of child units. - children: - # config of storage unit - storage: *small_storage - distribution: *normal_distribution - - # products use default config in core.yml - - # config of this facility - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Supplier_002" - definition_ref: "SupplierFacility" - - skus: - sku1: - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" - cost: 10 - price: 100 - vlt: 1 - sku3: - init_stock: 100 - production_rate: 1 - type: "material" - cost: 10 - price: 100 - vlt: 1 - - children: - storage: *small_storage - distribution: *normal_distribution - - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - - skus: - sku1: - init_stock: 1000 - price: 100 - vlt: 1 - sku2: - init_stock: 1000 - price: 100 - vlt: 1 - sku3: - init_stock: 1000 - price: 100 - vlt: 1 - - children: - storage: *huge_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Retailer_001" - definition_ref: "RetailerFacility" - - skus: - sku1: - price: 300 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 # optional - vlt: 1 - sku3: - price: 200 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - sku2: - price: 100 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - - children: - storage: *midium_storage - - config: - order_cost: 0 - # topology used to specify the up/downstream for facilities - # we split it from facility, so that we can support configuration inherit to override it - # for a new topology - # TODO: change the name? - topology: - # key is current facility, value if upstream facilities that will provide a certain sku - Supplier_002: - # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", - # or any other facility in the list - sku3: - - "Supplier_001" - Warehouse_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - Retailer_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - - # map grid definitions - grid: - size: [20, 20] - - # facility position in grid - facilities: - Supplier_001: [0, 0] - Supplier_002: [3, 3] - Warehouse_001: [6, 6] - Retailer_001: [10, 18] - - # cells that un-traversable - blocks: - railroad: - - [10, 10] - - [10, 11] - - [10, 12] - - [11, 12] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py deleted file mode 100644 index c72c3b5d6..000000000 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .consumer import ConsumerUnit -from .distribution import DistributionUnit -from .manufacture import ManufactureUnit -from .outerseller import DataFileDemandSampler, OuterSellerUnit, SellerDemandSampler -from .product import ProductUnit -from .seller import SellerUnit -from .simplemanufacture import SimpleManufactureUnit -from .skuunit import SkuUnit -from .storage import StorageUnit -from .storeproduct import StoreProductUnit -from .unitbase import UnitBase -from .vehicle import VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py deleted file mode 100644 index 3e9b7c3a0..000000000 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from collections import Counter, defaultdict - -from scipy.ndimage.interpolation import shift - -from .order import Order -from .skuunit import SkuUnit - - -class ConsumerUnit(SkuUnit): - """Consumer unit used to generate order to purchase from upstream by action.""" - - def __init__(self): - super(ConsumerUnit, self).__init__() - - self.open_orders = defaultdict(Counter) - - # States in python side. - self.received = 0 - self.purchased = 0 - self.sources = [] - self.pending_order_daily = None - self.order_product_cost = 0 - - def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): - """Called after order product is received. - - Args: - source_id (int): Where is the product from (facility id). - product_id (int): What product we received. - quantity (int): How many we received. - original_quantity (int): How many we ordered. - """ - self.received += quantity - - self.update_open_orders(source_id, product_id, -original_quantity) - - def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): - """Update the order states. - - Args: - source_id (int): Where is the product from (facility id). - product_id (int): What product in the order. - qty_delta (int): Number of product to update (sum). - """ - # New order for product. - self.open_orders[source_id][product_id] += qty_delta - - def initialize(self): - super(ConsumerUnit, self).initialize() - - self.pending_order_daily = [0] * self.world.configs.settings["pending_order_len"] - - sku = self.facility.skus[self.product_id] - - order_cost = self.facility.get_config("order_cost") - - self.data_model.initialize(sku.price, order_cost) - - if self.facility.upstreams is not None: - # Construct sources from facility's upstreams. - sources = self.facility.upstreams.get(self.product_id, None) - - if sources is not None: - # Is we are a supplier facility? - is_supplier = self.parent.manufacture is not None - - # Current sku information. - sku = self.world.get_sku_by_id(self.product_id) - - for source_facility in sources: - # We are a supplier unit, then the consumer is used to purchase source materials from upstreams. - # Try to find who will provide this kind of material. - if is_supplier: - if source_facility.products is not None: - for source_sku_id in sku.bom.keys(): - if source_sku_id in source_facility.products: - # This is a valid source facility. - self.sources.append(source_facility.id) - else: - # If we are not a manufacturing, just check if upstream have this sku configuration. - if sku.id in source_facility.skus: - self.sources.append(source_facility.id) - - def step(self, tick: int): - self._update_pending_order() - - # NOTE: id == 0 means invalid,as our id is 1 based. - if not self.action or self.action.quantity <= 0 or self.action.product_id <= 0 or self.action.source_id == 0: - return - - # NOTE: we are using product unit as destination, - # so we expect the action.source_id is and id of product unit - self.update_open_orders(self.action.source_id, self.action.product_id, self.action.quantity) - - order = Order(self.facility, self.action.product_id, self.action.quantity, self.action.vlt) - - source_facility = self.world.get_facility_by_id(self.action.source_id) - - self.order_product_cost = source_facility.distribution.place_order(order) - - self.purchased = self.action.quantity - - if order.vlt < len(self.pending_order_daily): - self.pending_order_daily[order.vlt - 1] += order.quantity - - def flush_states(self): - if self.received > 0: - self.data_model.received = self.received - self.data_model.total_received += self.received - - if self.purchased > 0: - self.data_model.purchased = self.purchased - self.data_model.total_purchased += self.purchased - self.data_model.latest_consumptions = 1.0 - - if self.order_product_cost > 0: - self.data_model.order_product_cost = self.order_product_cost - - if self.action is not None and self.action.quantity > 0: - self.data_model.order_quantity = self.action.quantity - self.data_model.reward_discount = self.action.reward_discount - - def post_step(self, tick: int): - # Clear the action states per step. - if self.action is not None: - self.data_model.latest_consumptions = 0 - self.data_model.reward_discount = 0 - - if self.action.quantity > 0: - self.data_model.order_quantity = 0 - - # This will set action to None. - super(ConsumerUnit, self).post_step(tick) - - if self.received > 0: - self.data_model.received = 0 - self.received = 0 - - if self.purchased > 0: - self.data_model.purchased = 0 - self.purchased = 0 - - if self.order_product_cost > 0: - self.data_model.order_product_cost = 0 - - def reset(self): - super(ConsumerUnit, self).reset() - - self.pending_order_daily = [0] * self.world.configs.settings["pending_order_len"] - - self.open_orders.clear() - - def get_in_transit_quantity(self): - quantity = 0 - - for source_id, orders in self.open_orders.items(): - quantity += orders.get(self.product_id, 0) - - return quantity - - def _update_pending_order(self): - self.pending_order_daily = shift(self.pending_order_daily, -1, cval=0) - - def get_unit_info(self): - info = super().get_unit_info() - - info["sources"] = self.sources - - return info diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py deleted file mode 100644 index 5860e7a5a..000000000 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from collections import Counter, defaultdict, deque -from typing import Dict, List - -from .order import Order -from .unitbase import UnitBase -from .vehicle import VehicleUnit - - -class DistributionUnit(UnitBase): - """Unit that used to receive and execute orders from downstream facilities. - - One distribution can accept all kind of sku order. - """ - # Vehicle unit list of this distribution unit. - vehicles: List[VehicleUnit] = None - - def __init__(self): - super().__init__() - self.order_queue = deque() - - self.transportation_cost = Counter() - self.delay_order_penalty = Counter() - self.check_in_order = Counter() - - self.base_delay_order_penalty = 0 - - self._is_order_changed = False - - def get_pending_order(self) -> Dict[int, int]: - """Get orders that states is pending. - - Returns: - dict: Dictionary of order that key is product id, value is quantity. - """ - counter = defaultdict(int) - - for order in self.order_queue: - counter[order.product_id] += order.quantity - - for vehicle in self.vehicles: - if vehicle.is_enroute(): - counter[vehicle.product_id] += (vehicle.requested_quantity - vehicle.payload) - - return counter - - def place_order(self, order: Order) -> int: - """Place an order in the pending queue. - - Args: - order (Order): Order to insert. - - Returns: - int: Total price of this order. - """ - if order.quantity > 0: - sku = self.facility.skus[order.product_id] - - if sku is not None: - self._is_order_changed = True - - self.order_queue.append(order) - - order_total_price = sku.price * order.quantity - - self.check_in_order[order.product_id] += order.quantity - - return order_total_price - - return 0 - - def initialize(self): - super(DistributionUnit, self).initialize() - - self.base_delay_order_penalty = self.facility.get_config("delay_order_penalty", 0) - - def step(self, tick: int): - for vehicle in self.vehicles: - # If we have vehicle not on the way and there is any pending order. - if len(self.order_queue) > 0 and vehicle.requested_quantity == 0: - order = self.order_queue.popleft() - - # Schedule a job for available vehicle. - # TODO: why vlt is determined by order? - vehicle.schedule( - order.destination, - order.product_id, - order.quantity, - order.vlt - ) - - self._is_order_changed = True - - # Push vehicle. - vehicle.step(tick) - - self.transportation_cost[vehicle.product_id] += abs(vehicle.cost) - - # Update order's delay penalty per tick. - for order in self.order_queue: - self.delay_order_penalty[order.product_id] += self.base_delay_order_penalty - - def flush_states(self): - super(DistributionUnit, self).flush_states() - - for vehicle in self.vehicles: - vehicle.flush_states() - - if self._is_order_changed: - self._is_order_changed = False - - self.data_model.remaining_order_quantity = sum(order.quantity for order in self.order_queue) - self.data_model.remaining_order_number = len(self.order_queue) - - def reset(self): - super(DistributionUnit, self).reset() - - self.order_queue.clear() - self.transportation_cost.clear() - self.check_in_order.clear() - self.delay_order_penalty.clear() - - # Reset vehicles. - for vehicle in self.vehicles: - vehicle.reset() diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py deleted file mode 100644 index 228828922..000000000 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .skuunit import SkuUnit - - -class ManufactureUnit(SkuUnit): - """Unit that used to produce certain product(sku) with consume specified source skus. - - One manufacture unit per sku. - """ - - # Source material sku and related number per produce cycle. - bom: dict = None - - # How many production unit each produce cycle. - output_units_per_lot: int = None - - # How many unit we will consume each produce cycle. - input_units_per_lot: int = 0 - - # How many we procedure per current step. - manufacture_number: int = 0 - - def initialize(self): - super(ManufactureUnit, self).initialize() - - facility_sku_info = self.facility.skus[self.product_id] - product_unit_cost = facility_sku_info.product_unit_cost - - self.data_model.initialize(product_unit_cost) - - global_sku_info = self.world.get_sku_by_id(self.product_id) - - self.bom = global_sku_info.bom - self.output_units_per_lot = global_sku_info.output_units_per_lot - - if len(self.bom) > 0: - self.input_units_per_lot = sum(self.bom.values()) - - def step(self, tick: int): - # Try to produce production if we have positive rate. - if self.action is not None and self.action.production_rate > 0: - sku_num = len(self.facility.skus) - unit_num_upper_bound = self.facility.storage.capacity // sku_num - - # Compare with avg storage number. - current_product_number = self.facility.storage.get_product_number(self.product_id) - max_number_to_procedure = min( - unit_num_upper_bound - current_product_number, - self.action.production_rate * self.output_units_per_lot, - self.facility.storage.remaining_space - ) - - if max_number_to_procedure > 0: - space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot - - # Consider about the volume, we can produce all if space take per cycle <=1. - if space_taken_per_cycle > 1: - max_number_to_procedure = max_number_to_procedure // space_taken_per_cycle - - source_sku_to_take = {} - # Do we have enough source material? - for source_sku_id, source_sku_cost_number in self.bom.items(): - source_sku_available_number = self.facility.storage.get_product_number(source_sku_id) - - max_number_to_procedure = min( - source_sku_available_number // source_sku_cost_number, - max_number_to_procedure - ) - - if max_number_to_procedure <= 0: - break - - source_sku_to_take[source_sku_id] = max_number_to_procedure * source_sku_cost_number - - if max_number_to_procedure > 0: - self.manufacture_number = max_number_to_procedure - self.facility.storage.try_take_products(source_sku_to_take) - self.facility.storage.try_add_products({self.product_id: self.manufacture_number}) - else: - self.manufacture_number = 0 - - def flush_states(self): - if self.manufacture_number > 0: - self.data_model.manufacturing_number = self.manufacture_number - - def post_step(self, tick: int): - if self.manufacture_number > 0: - self.data_model.manufacturing_number = 0 - self.manufacture_number = 0 - - # NOTE: call super at last, since it will clear the action. - super(ManufactureUnit, self).post_step(tick) diff --git a/maro/simulator/scenarios/supply_chain/units/order.py b/maro/simulator/scenarios/supply_chain/units/order.py deleted file mode 100644 index 47a8335d0..000000000 --- a/maro/simulator/scenarios/supply_chain/units/order.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from collections import namedtuple - -Order = namedtuple("Order", ("destination", "product_id", "quantity", "vlt")) diff --git a/maro/simulator/scenarios/supply_chain/units/outerseller.py b/maro/simulator/scenarios/supply_chain/units/outerseller.py deleted file mode 100644 index c7eb0b3a1..000000000 --- a/maro/simulator/scenarios/supply_chain/units/outerseller.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import warnings -from abc import ABC, abstractmethod -from collections import namedtuple -from csv import DictReader - -from dateutil import parser - -from .seller import SellerUnit - - -class SellerDemandSampler(ABC): - """Base class of seller unit demand sampler, you can inherit from this to read from file or predict from a model. - - Args: - configs (dict): Configuration from retailer facility, contains keys for a special sampler. - world (World): Current world this retail belongs to. - """ - - def __init__(self, configs: dict, world): - self._configs = configs - self._world = world - - @abstractmethod - def sample_demand(self, product_id: int, tick: int) -> int: - """Sample the demand for specified product and tick. - - Args: - product_id (int): Id of product to sample. - tick (int): Tick of environment, NOTE: this tick is start from 0, - you may need to transform it to your time system. - """ - pass - - -class DataFileDemandSampler(SellerDemandSampler): - """Sampler to read sample demand from data files, one store one file. - - NOTE: - This sampler need to configure the start time that to be treated as tick 0 in world.settings, or - it will use first row as start time. - - Args: - configs (dict): Configuration from retail facility, it should contains following keys. - . "file_path", the path to the data file - . "sku_column", column name contains sku name, this must be match with current seller, or will be ignored. - . "price_column", column name that will be treated as price. - . "sale_column", column name that will be treated as sale number (demand). - . "datetime_column", column name that contains datetime, NOTE: we will parse it that ignore the time zone. - """ - - SkuRow = namedtuple("SkuRow", ("price", "sales")) - - def __init__(self, configs: dict, world): - super(DataFileDemandSampler, self).__init__(configs, world) - - self._file_path = configs["file_path"] - - # If start date time is None, then will use first row as start date time (tick 0). - self._start_date_time = self._world.configs.settings["start_date_time"] - - if self._start_date_time is not None: - self._start_date_time = parser.parse(self._start_date_time, ignoretz=True) - - self._sku_column_name = configs.get("sku_column", "SKU") - self._price_column_name = configs.get("price_column", "Price") - self._sale_column_name = configs.get("sale_column", "Sales") - self._datetime_column_name = configs.get("datetime_column", "DT") - - # Tick -> sku -> (sale, price). - self._cache = {} - - self._cache_data() - - def sample_demand(self, product_id: int, tick: int) -> int: - if tick not in self._cache or product_id not in self._cache[tick]: - return 0 - - return self._cache[tick][product_id].sales - - def _cache_data(self): - with open(self._file_path, "rt") as fp: - reader = DictReader(fp) - - for row in reader: - sku_name = row[self._sku_column_name] - - sales = int(row[self._sale_column_name]) - price = float(row[self._price_column_name]) - date = parser.parse(row[self._datetime_column_name], ignoretz=True) - - if self._start_date_time is None: - self._start_date_time = date - - # So one day one tick. - target_tick = (date - self._start_date_time).days - - if target_tick not in self._cache: - self._cache[target_tick] = {} - - sku = self._world.get_sku_by_name(sku_name) - - if sku is not None: - self._cache[target_tick][sku.id] = DataFileDemandSampler.SkuRow(price, sales) - else: - warnings.warn(f"{sku_name} not configured in config file.") - - -class OuterSellerUnit(SellerUnit): - """Seller that demand is from out side sampler, like a data file or data model prediction.""" - - # Sample used to sample demand. - sampler: SellerDemandSampler = None - - def market_demand(self, tick: int) -> int: - return self.sampler.sample_demand(self.product_id, tick) diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py deleted file mode 100644 index 1ef598b39..000000000 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import numpy as np - -from .consumer import ConsumerUnit -from .distribution import DistributionUnit -from .manufacture import ManufactureUnit -from .seller import SellerUnit -from .skuunit import SkuUnit -from .storage import StorageUnit - - -class ProductUnit(SkuUnit): - """Unit that used to group units of one special sku, usually contains consumer, seller and manufacture.""" - - # Consumer unit of current sku. - consumer: ConsumerUnit = None - - # Seller unit of current sku. - seller: SellerUnit = None - - # Manufacture unit of this sku. - manufacture: ManufactureUnit = None - - # Storage of this facility, always a reference of facility.storage. - storage: StorageUnit = None - - # Reference to facility's distribution unit. - distribution: DistributionUnit = None - - def initialize(self): - super().initialize() - - facility_sku = self.facility.skus[self.product_id] - - self.data_model.initialize(facility_sku.price) - - def step(self, tick: int): - for unit in self.children: - unit.step(tick) - - def flush_states(self): - for unit in self.children: - unit.flush_states() - - def post_step(self, tick: int): - super().post_step(tick) - - for unit in self.children: - unit.post_step(tick) - - def reset(self): - super().reset() - - for unit in self.children: - unit.reset() - - def get_unit_info(self) -> dict: - return { - "id": self.id, - "sku_id": self.product_id, - "max_vlt": self._get_max_vlt(), - "node_name": type(self.data_model).__node_name__ if self.data_model is not None else None, - "node_index": self.data_model_index if self.data_model is not None else None, - "class": type(self), - "config": self.config, - "consumer": self.consumer.get_unit_info() if self.consumer is not None else None, - "seller": self.seller.get_unit_info() if self.seller is not None else None, - "manufacture": self.manufacture.get_unit_info() if self.manufacture is not None else None - } - - # TODO: add following field into states. - def get_latest_sale(self): - sale = 0 - downstreams = self.facility.downstreams.get(self.product_id, []) - - for facility in downstreams: - sale += facility.products[self.product_id].get_latest_sale() - - return sale - - def get_sale_mean(self): - sale_mean = 0 - downstreams = self.facility.downstreams.get(self.product_id, []) - - for facility in downstreams: - sale_mean += facility.products[self.product_id].get_sale_mean() - - return sale_mean - - def get_sale_std(self): - sale_std = 0 - - downstreams = self.facility.downstreams.get(self.product_id, []) - - for facility in downstreams: - sale_std += facility.products[self.product_id].get_sale_std() - - return sale_std / np.sqrt(max(1, len(downstreams))) - - def get_selling_price(self): - price = 0.0 - downstreams = self.facility.downstreams.get(self.product_id, []) - - for facility in downstreams: - price = max(price, facility.products[self.product_id].get_selling_price()) - - return price - - def _get_max_vlt(self): - vlt = 1 - - if self.consumer is not None: - for source_facility_id in self.consumer.sources: - source_facility = self.world.get_facility_by_id(source_facility_id) - - source_vlt = source_facility.skus[self.product_id].vlt - - vlt = max(vlt, source_vlt) - - return vlt - - @staticmethod - def generate(facility, config: dict, unit_def: object): - """Generate product unit by sku information. - - Args: - facility (FacilityBase): Facility this product belongs to. - config (dict): Config of children unit. - unit_def (object): Definition of the unit (from config). - - Returns: - dict: Dictionary of product unit, key is the product id, value if ProductUnit. - """ - products_dict = {} - - if facility.skus is not None and len(facility.skus) > 0: - world = facility.world - - for sku_id, sku in facility.skus.items(): - sku_type = sku.type - - product_unit: ProductUnit = world.build_unit_by_type(unit_def, facility, facility) - product_unit.product_id = sku_id - product_unit.children = [] - product_unit.parse_configs(config) - product_unit.storage = product_unit.facility.storage - product_unit.distribution = product_unit.facility.distribution - - for child_name in ("manufacture", "consumer", "seller"): - conf = config.get(child_name, None) - - if conf is not None: - # Ignore manufacture unit if it is not for a production, even it is configured in config. - if sku_type != "production" and child_name == "manufacture": - continue - - # We produce the product, so we do not need to purchase it. - if sku_type == "production" and child_name == "consumer": - continue - - child_unit = world.build_unit(facility, product_unit, conf) - child_unit.product_id = sku_id - - setattr(product_unit, child_name, child_unit) - - # Parse config for unit. - child_unit.parse_configs(conf.get("config", {})) - - product_unit.children.append(child_unit) - - products_dict[sku_id] = product_unit - - return products_dict diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py deleted file mode 100644 index d2323cdcb..000000000 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -import numpy as np - -from .skuunit import SkuUnit - - -class SellerUnit(SkuUnit): - """ - Unit that used to generate product consume demand, and move demand product from current storage. - """ - - def __init__(self): - super(SellerUnit, self).__init__() - - self.gamma = 0 - - # Attribute cache. - self.sold = 0 - self.demand = 0 - self.total_sold = 0 - self.total_demand = 0 - self.price = 0 - - self.sale_hist = [] - - def market_demand(self, tick: int) -> int: - """Generate market demand for current tick. - - Args: - tick (int): Current simulator tick. - - Returns: - int: Demand number. - """ - return int(np.random.gamma(self.gamma)) - - def initialize(self): - super(SellerUnit, self).initialize() - - sku = self.facility.skus[self.product_id] - - self.gamma = sku.sale_gamma - - self.data_model.initialize(sku.price, sku.backlog_ratio) - - self.sale_hist = [self.gamma] * self.config["sale_hist_len"] - - def step(self, tick: int): - demand = self.market_demand(tick) - - # What seller does is just count down the product number. - sold_qty = self.facility.storage.take_available(self.product_id, demand) - - self.total_sold += sold_qty - self.sold = sold_qty - self.demand = demand - self.total_demand += demand - - self.sale_hist.append(demand) - self.sale_hist = self.sale_hist[1:] - - def flush_states(self): - if self.sold > 0: - self.data_model.sold = self.sold - self.data_model.total_sold = self.total_sold - - if self.demand > 0: - self.data_model.demand = self.demand - self.data_model.total_demand = self.total_demand - - def post_step(self, tick: int): - super(SellerUnit, self).post_step(tick) - - if self.sold > 0: - self.data_model.sold = 0 - self.sold = 0 - - if self.demand > 0: - self.data_model.demand = 0 - self.demand = 0 - - def reset(self): - super(SellerUnit, self).reset() - - def sale_mean(self): - return np.mean(self.sale_hist) - - def sale_std(self): - return np.std(self.sale_hist) diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py deleted file mode 100644 index 8a21305d0..000000000 --- a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .manufacture import ManufactureUnit - - -class SimpleManufactureUnit(ManufactureUnit): - """This simple manufacture unit will ignore source sku, just generate specified number of product.""" - - def step(self, tick: int): - # Try to produce production if we have positive rate. - self.manufacture_number = 0 - - if self.action is not None and self.action.production_rate > 0: - production_rate = self.action.production_rate - - sku_num = len(self.facility.skus) - unit_num_upper_bound = self.facility.storage.capacity // sku_num - current_product_number = self.facility.storage.get_product_number(self.product_id) - self.manufacture_number = max(0, min(unit_num_upper_bound - current_product_number, production_rate)) diff --git a/maro/simulator/scenarios/supply_chain/units/skuunit.py b/maro/simulator/scenarios/supply_chain/units/skuunit.py deleted file mode 100644 index da43bbe80..000000000 --- a/maro/simulator/scenarios/supply_chain/units/skuunit.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .unitbase import UnitBase - - -class SkuUnit(UnitBase): - """A sku related unit.""" - - # Product id (sku id), 0 means invalid. - product_id: int = 0 - - def initialize(self): - super(SkuUnit, self).initialize() - - if self.data_model is not None: - self.data_model.set_product_id(self.product_id, self.parent.id) - - def get_unit_info(self) -> dict: - info = super(SkuUnit, self).get_unit_info() - - info["sku_id"] = self.product_id - - return info diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py deleted file mode 100644 index c136bc5ee..000000000 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from typing import Dict - -from .unitbase import UnitBase - - -class StorageUnit(UnitBase): - """Unit that used to store skus.""" - - def __init__(self): - super().__init__() - - # We use these variables to hold changes at python side, flash to frame before taking snapshot. - # self.product_number = [] - # self.product_list = [] - - # Used to map from product id to slot index. - # self.product_index_mapping: Dict[int, int] = {} - self.capacity = 0 - self.remaining_space = 0 - self.product_level = {} - - # Which product's number has changed. - self._changed_product_cache = {} - - def try_add_products(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: - """Try to add products into storage. - - Args: - product_quantities (Dict[int, int]): Dictionary of product id and quantity need to add to storage. - all_or_nothing (bool): Failed if all product cannot be added, or add as many as it can. Default is True. - - Returns: - dict: Dictionary of product id and quantity success added. - """ - if all_or_nothing and self.remaining_space < sum(product_quantities.values()): - return {} - - unloaded_quantities = {} - - for product_id, quantity in product_quantities.items(): - unload_quantity = min(self.remaining_space, quantity) - - self.product_level[product_id] += unload_quantity - unloaded_quantities[product_id] = unload_quantity - - self._changed_product_cache[product_id] = True - - self.remaining_space -= unload_quantity - - return unloaded_quantities - - def try_take_products(self, product_quantities: Dict[int, int]) -> bool: - """Try to take specified number of product. - - Args: - product_quantities (Dict[int, int]): Dictionary of product id and quantity to take from storage. - - Returns: - bool: Is success to take? - """ - # Check if we can take all kinds of products? - for product_id, quantity in product_quantities.items(): - if self.product_level[product_id] < quantity: - return False - - # Take from storage. - for product_id, quantity in product_quantities.items(): - self.product_level[product_id] -= quantity - self._changed_product_cache[product_id] = True - - self.remaining_space += quantity - - return True - - def take_available(self, product_id: int, quantity: int) -> int: - """Take as much as available specified product from storage. - - Args: - product_id (int): Product to take. - quantity (int): Max quantity to take. - - Returns: - int: Actual quantity taken. - """ - available = self.product_level[product_id] - actual = min(available, quantity) - - self.product_level[product_id] -= actual - self._changed_product_cache[product_id] = True - - self.remaining_space += actual - - return actual - - def get_product_number(self, product_id: int) -> int: - """Get product number in storage. - - Args: - product_id (int): Product to check. - - Returns: - int: Available number of product. - """ - return self.product_level[product_id] - - def initialize(self): - super(StorageUnit, self).initialize() - - self.capacity = self.config.get("capacity", 100) - self.remaining_space = self.capacity - - for sku in self.facility.skus.values(): - self.product_level[sku.id] = sku.init_stock - self._changed_product_cache[sku.id] = False - - self.remaining_space -= sku.init_stock - - self.data_model.initialize( - capacity=self.capacity, - remaining_space=self.remaining_space, - product_list=[id for id in self.product_level.keys()], - product_number=[n for n in self.product_level.values()] - ) - - def flush_states(self): - # Write the changes to frame. - i = 0 - has_changes = False - for product_id, product_number in self.product_level.items(): - if self._changed_product_cache[product_id]: - has_changes = True - self._changed_product_cache[product_id] = False - - self.data_model.product_number[i] = product_number - i += 1 - - if has_changes: - self.data_model.remaining_space = self.remaining_space - - def reset(self): - super(StorageUnit, self).reset() - - self.remaining_space = self.capacity - - for sku in self.facility.skus.values(): - self.product_level[sku.id] = sku.init_stock - self.remaining_space -= sku.init_stock - - def get_unit_info(self): - info = super().get_unit_info() - - info["product_list"] = [i for i in self.product_level.keys()] - - return info diff --git a/maro/simulator/scenarios/supply_chain/units/storeproduct.py b/maro/simulator/scenarios/supply_chain/units/storeproduct.py deleted file mode 100644 index 7ce9a5396..000000000 --- a/maro/simulator/scenarios/supply_chain/units/storeproduct.py +++ /dev/null @@ -1,13 +0,0 @@ - -from .product import ProductUnit - - -class StoreProductUnit(ProductUnit): - def get_sale_mean(self): - return self.seller.sale_mean() - - def get_sale_std(self): - return self.seller.sale_std() - - def get_selling_price(self): - return self.facility.skus[self.product_id].price diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py deleted file mode 100644 index 6563a5aaa..000000000 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -class UnitBase: - """Base of all unit used to contain related logic. - - Typically one unit instance should bind to a data model instance, - that used to update states in frame. - - An unit will have following steps to initializing. - . Create instance with default constructor without parameter, means all unit constructor should not have parameter - or with default value. - . Unit.parse_configs is called with configurations from parent or config file. - . After frame is constructed, Unit.initialize is called to do data model related initializing. - . At the beginning of business_engine.step, Unit.step is called to go through related logic, - then after all the agent completed, Unit.flush_state will be called at the end of business_engine.step, - to tell agents to save their states. - . Unit.post_step is called in business_engine.post_step after step is finished. - . Unit.set_action is called when there is any action from out-side. - - """ - # Id of this unit. - id: int = 0 - - # Which this unit belongs to. - facility = None - - # Which world this unit belongs to. - world = None - - # Parent of this unit, it can be a facility or another unit. - parent: object = None - - # Child units, extended unit can add their own child as property, this is used as a collection. - children: list = None - - # Data model name in the frame, used to query binding data model instance. - data_model_name: str = None - - # Data model instance index in the frame, used to query binding data model instance. - data_model_index: int = None - - # Real data model binding with this unit. - data_model = None - - # Current action. - action: object = None - - # Current unit configurations. - config: dict = None - - def __init__(self): - pass - - def parse_configs(self, config: dict): - """Parse configurations from config. - - Args: - config (dict): Configuration from parent or config file. - """ - self.config = config - - def step(self, tick: int): - """Run related logic for current tick. - - Args: - tick (int): Current simulator tick. - """ - pass - - def flush_states(self): - """Flush states into frame for current tick. - """ - pass - - def post_step(self, tick: int): - """Post-processing for current step. - - Args: - tick (int): Current simulator tick. - """ - self.action = None - - def reset(self): - """Reset this unit for a new episode.""" - if self.data_model is not None: - self.data_model.reset() - - def initialize(self): - """Initialize this unit after data model is ready to use. - - NOTE: unit.data_model is available from this step. - """ - if self.data_model is not None: - self.data_model.set_id(self.id, self.facility.id) - - def set_action(self, action: object): - """Set action for this agent. - - Args: - action (object): Action from outside. - """ - self.action = action - - def get_unit_info(self) -> dict: - return { - "id": self.id, - "node_name": type(self.data_model).__node_name__, - "node_index": self.data_model_index, - "class": type(self), - "config": self.config, - "children": None if self.children is None else [c.get_unit_info() for c in self.children] - } diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py deleted file mode 100644 index d1751896b..000000000 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .unitbase import UnitBase - - -class VehicleUnit(UnitBase): - """Unit used to move production from source to destination by order.""" - - def __init__(self): - super().__init__() - # Max patient of current vehicle. - self.max_patient: int = None - - # Current products' destination. - self.destination = None - - # Path to destination. - self.path: list = None - - # Product to load - self.product_id = 0 - - # Steps to arrive destination. - self.steps = 0 - - # Payload on vehicle. - self.payload = 0 - - # Which product unit current product related to. - self.product = None - - # Current location in the path. - self.location = 0 - - # Velocity. - self.velocity = 0 - self.requested_quantity = 0 - self.patient = 0 - self.cost = 0 - self.unit_transport_cost = 0 - - def schedule(self, destination: object, product_id: int, quantity: int, vlt: int): - """Schedule a job for this vehicle. - - Args: - destination (FacilityBase): Destination facility. - product_id (int): What load from storage. - quantity (int): How many to load. - vlt (int): Velocity of vehicle. - """ - self.product_id = product_id - self.destination = destination - self.requested_quantity = quantity - - # Find the path from current entity to target. - self.path = self.world.find_path( - self.facility.x, - self.facility.y, - destination.x, - destination.y - ) - - if self.path is None: - raise Exception(f"Destination {destination} is unreachable") - - # Steps to destination. - self.steps = len(self.path) // vlt - - # We are waiting for product loading. - self.location = 0 - - self.velocity = vlt - - self.patient = self.max_patient - - def try_load(self, quantity: int) -> bool: - """Try to load specified number of scheduled product. - - Args: - quantity (int): Number to load. - """ - if self.facility.storage.try_take_products({self.product_id: quantity}): - self.payload = quantity - - # Write to frame, as we do not need to update it per tick. - self.data_model.payload = quantity - - return True - - return False - - def try_unload(self): - """Try unload products into destination's storage.""" - unloaded = self.destination.storage.try_add_products( - {self.product_id: self.payload}, - all_or_nothing=False - ) - - # Update order if we unloaded any. - if len(unloaded) > 0: - unloaded_units = sum(unloaded.values()) - - self.destination.products[self.product_id].consumer.on_order_reception( - self.facility.id, - self.product_id, - unloaded_units, - self.payload - ) - - self.payload -= unloaded_units - self.data_model.payload = self.payload - - def is_enroute(self): - return self.destination is not None - - def initialize(self): - super(VehicleUnit, self).initialize() - - patient = self.config.get("patient", 100) - self.unit_transport_cost = self.config.get("unit_transport_cost", 1) - - self.data_model.initialize(unit_transport_cost=self.unit_transport_cost) - - self.max_patient = patient - - def step(self, tick: int): - # If we have not arrive at destination yet. - if self.steps > 0: - # if we still not loaded enough productions yet. - if self.location == 0 and self.payload == 0: - # then try to load by requested. - - if self.try_load(self.requested_quantity): - # NOTE: here we return to simulate loading - return - else: - self.patient -= 1 - - # Failed to load, check the patient. - if self.patient < 0: - self.destination.products[self.product_id].consumer.update_open_orders( - self.facility.id, - self.product_id, - -self.requested_quantity - ) - - self._reset_internal_states() - self._reset_data_model() - - # Moving to destination - if self.payload > 0: - # Closer to destination until 0. - - self.location += self.velocity - self.steps -= 1 - - if self.location >= len(self.path): - self.location = len(self.path) - 1 - else: - # Avoid update under idle state. - if self.location > 0: - # Try to unload./////////////////////////////////////////////////////////////////// - if self.payload > 0: - self.try_unload() - - # Back to source if we unload all. - if self.payload == 0: - self._reset_internal_states() - self._reset_data_model() - - self.cost = self.payload * self.unit_transport_cost - - def reset(self): - super(VehicleUnit, self).reset() - - self._reset_internal_states() - self._reset_data_model() - - def _reset_internal_states(self): - self.destination = None - self.path = None - self.payload = 0 - self.product_id = 0 - self.steps = 0 - self.location = 0 - self.requested_quantity = 0 - self.velocity = 0 - self.patient = self.max_patient - - def _reset_data_model(self): - # Reset data model. - self.data_model.payload = 0 diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py deleted file mode 100644 index 72e9b9783..000000000 --- a/maro/simulator/scenarios/supply_chain/world.py +++ /dev/null @@ -1,461 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from collections import namedtuple -from typing import List, Tuple, Union - -import networkx as nx - -from maro.backends.frame import FrameBase - -from .easy_config import EasyConfig, SkuInfo -from .facilities import FacilityBase -from .frame_builder import build_frame -from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef -from .units import ProductUnit, SkuUnit, UnitBase - -AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id")) - - -class World: - """Supply chain world contains facilities and grid base map.""" - - def __init__(self): - # Frame for current world configuration. - self.frame: FrameBase = None - - # Current configuration. - self.configs: SupplyChainConfiguration = None - - # Durations of current simulation. - self.durations = 0 - - # All the entities in the world. - self.units = {} - - # All the facilities in this world. - self.facilities = {} - - # Entity id counter, every unit and facility have unique id. - self._id_counter = 1 - - # Grid of the world - self._graph: nx.Graph = None - - # Sku id to name mapping, used for querying. - self._sku_id2name_mapping = {} - - # All the sku in this world. - self._sku_collection = {} - - # Facility name to id mapping, used for querying. - self._facility_name2id_mapping = {} - - # Data model class collection, used to collection data model class and their number in frame. - self._data_class_collection = {} - - self.agent_list = [] - - self.agent_type_dict = {} - - self.max_sources_per_facility = 0 - self.max_price = 0 - - def get_sku_by_name(self, name: str) -> EasyConfig: - """Get sku information by name. - - Args: - name (str): Sku name to query. - - Returns: - EasyConfig: General information for sku, used as a dict, but support use key as property. - """ - return self._sku_collection.get(name, None) - - def get_sku_by_id(self, sku_id: int) -> EasyConfig: - """Get sku information by sku id. - - Args: - sku_id (int): Id of sku to query. - - Returns: - SkuInfo: General information for sku. - """ - return self._sku_collection[self._sku_id2name_mapping[sku_id]] - - def get_facility_by_id(self, facility_id: int) -> FacilityBase: - """Get facility by id. - - Args: - facility_id (int): Facility id to query. - - Returns: - FacilityBase: Facility instance. - """ - return self.facilities[facility_id] - - def get_facility_by_name(self, name: str): - """Get facility by name. - - Args: - name (str): Facility name to query. - - Returns: - FacilityBase: Facility instance. - """ - return self.facilities[self._facility_name2id_mapping[name]] - - def get_entity(self, id: int) -> Union[FacilityBase, UnitBase]: - """Get an entity(Unit or Facility) by id. - - Args: - id (int): Id to query. - - Returns: - Union[FacilityBase, UnitBase]: Unit or facility instance. - """ - if id in self.units: - return self.units[id] - else: - return self.facilities[id] - - def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: - """Find path to specified cell. - - Args: - start_x (int): Start cell position x. - start_y (int): Start cell position y. - goal_x (int): Destination cell position x. - goal_y (int): Destination cell position y. - - Returns: - List[Tuple[int, int]]: List of (x, y) position to target. - """ - return nx.astar_path(self._graph, source=(start_x, start_y), target=(goal_x, goal_y), weight="cost") - - def build(self, configs: SupplyChainConfiguration, snapshot_number: int, durations: int): - """Build world with configurations. - - Args: - configs (SupplyChainConfiguration): Configuration of current world. - snapshot_number (int): Number of snapshots to keep in memory. - durations (int): Durations of current simulation. - """ - self.durations = durations - self.configs = configs - - world_config = configs.world - - # Grab sku information for this world. - for sku_conf in world_config["skus"]: - sku = SkuInfo(sku_conf) - - self._sku_id2name_mapping[sku.id] = sku.name - self._sku_collection[sku.name] = sku - - # Collect bom info. - for sku_conf in world_config["skus"]: - sku = self._sku_collection[sku_conf["name"]] - sku.bom = {} - - bom = sku_conf.get("bom", {}) - - for material_sku_name, units_per_lot in bom.items(): - sku.bom[self._sku_collection[material_sku_name].id] = units_per_lot - - # Construct facilities. - for facility_conf in world_config["facilities"]: - facility_class_alias = facility_conf["class"] - facility_def: FacilityDef = self.configs.facilities[facility_class_alias] - facility_class_type = facility_def.class_type - - # Instance of facility. - facility = facility_class_type() - - # Normal properties. - facility.id = self._gen_id() - facility.name = facility_conf["name"] - facility.world = self - - # Parse sku info. - facility.parse_skus(facility_conf["skus"]) - - # Parse config for facility. - facility.parse_configs(facility_conf.get("config", {})) - - # Due with data model. - data_model_def: DataModelDef = self.configs.data_models[facility_def.data_model_alias] - - # Register the data model, so that it will help to generate related instance index. - facility.data_model_index = self._register_data_model(data_model_def.alias) - facility.data_model_name = data_model_def.name_in_frame - - # Build children (units). - for child_name, child_conf in facility_conf["children"].items(): - setattr(facility, child_name, self.build_unit(facility, facility, child_conf)) - - self.facilities[facility.id] = facility - - self._facility_name2id_mapping[facility.name] = facility.id - - # Build frame. - self.frame = self._build_frame(snapshot_number) - - # Assign data model instance. - for unit in self.units.values(): - if unit.data_model_name is not None: - unit.data_model = getattr(self.frame, unit.data_model_name)[unit.data_model_index] - - for facility in self.facilities.values(): - if facility.data_model_name is not None: - facility.data_model = getattr(self.frame, facility.data_model_name)[facility.data_model_index] - - # Construct the upstream topology. - topology = world_config["topology"] - - for cur_facility_name, topology_conf in topology.items(): - facility = self.get_facility_by_name(cur_facility_name) - - for sku_name, source_facilities in topology_conf.items(): - sku = self.get_sku_by_name(sku_name) - facility.upstreams[sku.id] = [] - - self.max_sources_per_facility = max(self.max_sources_per_facility, len(source_facilities)) - - for source_name in source_facilities: - source_facility = self.get_facility_by_name(source_name) - - facility.upstreams[sku.id].append(source_facility) - - if sku.id not in source_facility.downstreams: - source_facility.downstreams[sku.id] = [] - - source_facility.downstreams[sku.id].append(facility) - - # Call initialize method for facilities. - for facility in self.facilities.values(): - facility.initialize() - - # Call initialize method for units. - for unit in self.units.values(): - unit.initialize() - - # Construct the map grid. - grid_config = world_config["grid"] - - grid_width, grid_height = grid_config["size"] - - # Build our graph base one settings. - # This will create a full connect graph. - self._graph = nx.grid_2d_graph(grid_width, grid_height) - - # All edge weight will be 1 by default. - edge_weights = {e: 1 for e in self._graph.edges()} - - # Facility to cell will have 1 weight, cell to facility will have 4 cost. - for facility_name, pos in grid_config["facilities"].items(): - facility_id = self._facility_name2id_mapping[facility_name] - facility = self.facilities[facility_id] - facility.x = pos[0] - facility.y = pos[1] - pos = tuple(pos) - - # Neighbors to facility will have high cost. - for npos in ((pos[0] - 1, pos[1]), (pos[0] + 1, pos[1]), (pos[0], pos[1] - 1), (pos[0], pos[1] + 1)): - if 0 <= npos[0] < grid_width and 0 <= npos[1] < grid_height: - edge_weights[(npos, pos)] = 4 - - nx.set_edge_attributes(self._graph, edge_weights, "cost") - - # Collection agent list - for facility in self.facilities.values(): - agent_type = facility.configs["agent_type"] - - self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id)) - - self.agent_type_dict[agent_type] = True - - for sku in facility.skus.values(): - self.max_price = max(self.max_price, sku.price) - - for unit in self.units.values(): - if issubclass(type(unit), ProductUnit): - agent_type = unit.config["agent_type"] - - # unit or facility id, agent type, is facility, sku info, facility id - self.agent_list.append( - AgentInfo(unit.id, agent_type, False, unit.facility.skus[unit.product_id], unit.facility.id)) - - self.agent_type_dict[agent_type] = True - - def build_unit_by_type(self, unit_def: UnitDef, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): - unit = unit_def.class_type() - - unit.id = self._gen_id() - unit.parent = parent - unit.facility = facility - unit.world = self - - if unit_def.data_model_alias is not None: - # Due with data model. - data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] - - # Register the data model, so that it will help to generate related instance index. - unit.data_model_index = self._register_data_model(data_model_def.alias) - unit.data_model_name = data_model_def.name_in_frame - - self.units[unit.id] = unit - - return unit - - def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBase], config: dict) -> UnitBase: - """Build an unit by its configuration. - - Args: - facility (FacilityBase): Facility of this unit belongs to. - parent (Union[FacilityBase, UnitBase]): Parent of this unit belongs to, this may be same with facility, if - this unit is attached to a facility. - config (dict): Configuration of this unit. - - Returns: - UnitBase: Unit instance. - """ - unit_class_alias = config["class"] - unit_def: UnitDef = self.configs.units[unit_class_alias] - - is_template = config.get("is_template", False) - - # If it is not a template, then just use current configuration to generate unit. - if not is_template: - unit_instance = unit_def.class_type() - - # Assign normal properties. - unit_instance.id = self._gen_id() - unit_instance.world = self - unit_instance.facility = facility - unit_instance.parent = parent - - # Record the id. - self.units[unit_instance.id] = unit_instance - - # Due with data model. - data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] - - # Register the data model, so that it will help to generate related instance index. - unit_instance.data_model_index = self._register_data_model(data_model_def.alias) - unit_instance.data_model_name = data_model_def.name_in_frame - - # Parse the config is there is any. - unit_instance.parse_configs(config.get("config", {})) - - # Prepare children. - children_conf = config.get("children", None) - - if children_conf: - unit_instance.children = [] - - for child_name, child_conf in children_conf.items(): - # If child configuration is a dict, then we add it as a property by name (key). - if type(child_conf) == dict: - child_instance = self.build_unit(facility, unit_instance, child_conf) - - setattr(unit_instance, child_name, child_instance) - unit_instance.children.append(child_instance) - elif type(child_conf) == list: - # If child configuration is a list, then will treat it as list property, named same as key. - child_list = [] - for conf in child_conf: - child_list.append(self.build_unit(facility, unit_instance, conf)) - - setattr(unit_instance, child_name, child_list) - unit_instance.children.extend(child_list) - - return unit_instance - else: - # If this is template unit, then will use the class' static method 'generate' to generate sub-units. - children = unit_def.class_type.generate(facility, config.get("config"), unit_def) - - return children - - def get_node_mapping(self): - """Collect all the entities information. - - Returns: - dict: A dictionary contains 'mapping' for id to data model index mapping, - 'detail' for detail of units and facilities. - """ - facility_info_dict = { - facility_id: facility.get_node_info() for facility_id, facility in self.facilities.items() - } - - id2index_mapping = {} - - for unit_id, unit in self.units.items(): - sku = None - - if isinstance(unit, SkuUnit): - sku = unit.facility.skus[unit.product_id] - - if unit.data_model is not None: - id2index_mapping[unit_id] = (unit.data_model_name, unit.data_model_index, unit.facility.id, sku) - else: - id2index_mapping[unit_id] = (None, None, unit.facility.id, sku) - - return { - "agent_types": [k for k in self.agent_type_dict.keys()], - "unit_mapping": id2index_mapping, - "skus": {sku.id: sku for sku in self._sku_collection.values()}, - "facilities": facility_info_dict, - "max_price": self.max_price, - "max_sources_per_facility": self.max_sources_per_facility, - } - - def _register_data_model(self, alias: str) -> int: - """Register a data model alias, used to collect data model used in frame. - - Args: - alias (str): Class alias defined in core.yml. - - Returns: - int: Specified data model instance index after frame is built. - """ - if alias not in self._data_class_collection: - self._data_class_collection[alias] = 0 - - node_index = self._data_class_collection[alias] - - self._data_class_collection[alias] += 1 - - return node_index - - def _build_frame(self, snapshot_number: int) -> FrameBase: - """Build frame by current world definitions. - - Args: - snapshot_number (int): Number of snapshots to keep in memory. - - Returns: - FrameBase: Frame instance with data model in current configuration. - """ - data_class_in_frame = [] - - for alias, number in self._data_class_collection.items(): - data_model_def: DataModelDef = self.configs.data_models[alias] - data_class_in_frame.append(( - data_model_def.class_type, - data_model_def.name_in_frame, - number - )) - - frame = build_frame(True, snapshot_number, data_class_in_frame) - - return frame - - def _gen_id(self): - """Generate id for entities.""" - nid = self._id_counter - - self._id_counter += 1 - - return nid From 4123926d20a7801eea78b3da5154ef0c7aeb53eb Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 30 Apr 2021 12:06:15 +0000 Subject: [PATCH 213/482] deleted aks related files --- aks/actor.yml | 28 ---------------------------- aks/learner.yml | 27 --------------------------- aks/redis.yml | 38 -------------------------------------- 3 files changed, 93 deletions(-) delete mode 100644 aks/actor.yml delete mode 100644 aks/learner.yml delete mode 100644 aks/redis.yml diff --git a/aks/actor.yml b/aks/actor.yml deleted file mode 100644 index 01b90b67c..000000000 --- a/aks/actor.yml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: actor - labels: - role: actor -spec: - completions: 5 - parallelism: 5 - # backoffLimit: 2 - template: - metadata: - name: actor - spec: - containers: - - name: actor - image: maroacr.azurecr.cn/maro-dev:latest - command: ["python3", "/mnt/examples/cim/dqn/main.py", "-w", "2"] - volumeMounts: - - name: maro-test - mountPath: /mnt - restartPolicy: Never - volumes: - - name: maro-test - azureFile: - secretName: azure-secret - shareName: marosf - readOnly: false \ No newline at end of file diff --git a/aks/learner.yml b/aks/learner.yml deleted file mode 100644 index 9256f97b9..000000000 --- a/aks/learner.yml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: learner - labels: - role: learner -spec: - completions: 1 - # backoffLimit: 2 - template: - metadata: - name: learner - spec: - containers: - - name: learner - image: maroacr.azurecr.cn/maro-dev:latest - command: ["python3", "/mnt/examples/cim/dqn/main.py", "-w", "1"] - volumeMounts: - - name: maro-test - mountPath: /mnt - restartPolicy: Never - volumes: - - name: maro-test - azureFile: - secretName: azure-secret - shareName: marosf - readOnly: false \ No newline at end of file diff --git a/aks/redis.yml b/aks/redis.yml deleted file mode 100644 index 742b97574..000000000 --- a/aks/redis.yml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: apps/v1 # API version -kind: Deployment -metadata: - name: maro-redis # Unique name for the deployment - labels: - app: maro-redis # Labels to be applied to this deployment -spec: - selector: - matchLabels: # This deployment applies to the Pods matching these labels - app: maro-redis - replicas: 1 # Run a single pod in the deployment - template: # Template for the pods that will be created by this deployment - metadata: - labels: # Labels to be applied to the Pods in this deployment - app: maro-redis - spec: # Spec for the container which will be run inside the Pod. - containers: - - name: maro-redis - image: redis - resources: - requests: - cpu: 100m - memory: 100Mi - ports: - - containerPort: 6379 - ---- -apiVersion: v1 -kind: Service -metadata: - name: maro-redis -spec: - type: ClusterIP - selector: - app: maro-redis - ports: - - port: 6379 - targetPort: 6379 From ec9dcf80bde2e47165fbc26285f9475ede3f76fa Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 30 Apr 2021 13:24:39 +0000 Subject: [PATCH 214/482] made supply_chain scenario folder mountable --- docker_files/dev.df | 1 + examples/supply_chain/learner.py | 2 ++ .../supply_chain/scripts/docker_compose_yml_generator.py | 9 +++++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docker_files/dev.df b/docker_files/dev.df index 547473057..b0112040d 100644 --- a/docker_files/dev.df +++ b/docker_files/dev.df @@ -30,6 +30,7 @@ RUN bash /maro/scripts/install_maro.sh RUN pip cache purge RUN rm -r /maro/maro/rl +RUN rm -r /maro/maro/simulator/scenarios/supply_chain ENV PYTHONPATH=/maro diff --git a/examples/supply_chain/learner.py b/examples/supply_chain/learner.py index b76775d7c..74109f467 100644 --- a/examples/supply_chain/learner.py +++ b/examples/supply_chain/learner.py @@ -1,8 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from os.path import dirname, join, realpath from maro.rl import Learner +sc_code_dir = dirname(realpath(__file__)) class SCLearner(Learner): def end_of_training(self, ep, segment, **kwargs): diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py index 343b01c99..caf0c45b8 100644 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -9,6 +9,7 @@ sc_code_dir = dirname(script_dir) root_dir = dirname(dirname(sc_code_dir)) maro_rl_dir = join(root_dir, "maro", "rl") +maro_sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") config_path = join(sc_code_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") @@ -25,7 +26,11 @@ "build": {"context": root_dir, "dockerfile": dockerfile_path}, "image": "maro-sc", "container_name": "learner", - "volumes": [f"{sc_code_dir}:/maro/supply_chain", f"{maro_rl_dir}:/maro/maro/rl"], + "volumes": [ + f"{sc_code_dir}:/maro/supply_chain", + f"{maro_rl_dir}:/maro/maro/rl", + f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" + ], "command": ["python3", "/maro/supply_chain/distributed_launcher.py", "-w", "1"] } } @@ -37,7 +42,7 @@ del actor_manifest["build"] actor_manifest["command"][-1] = "2" actor_manifest["container_name"] = actor_id - actor_manifest["environment"] = [f"COMPONENT_NAME={actor_id}"] + actor_manifest["environment"] = [f"COMPONENT={actor_id}"] docker_compose_manifest["services"][actor_id] = actor_manifest with open(join(sc_code_dir, "docker-compose.yml"), "w") as fp: From a8c4ec7fe7aa42065a5ce8be74b27ac07721e4ff Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 2 May 2021 06:58:36 +0000 Subject: [PATCH 215/482] 1. removed obsolete file learning_model.py; 2. added torch device option for CoreModel --- examples/supply_chain/config.yml | 3 +- examples/supply_chain/policies.py | 4 +- maro/rl/model/core_model.py | 6 +- maro/rl/model/learning_model.py | 202 ------------------------------ 4 files changed, 9 insertions(+), 206 deletions(-) delete mode 100644 maro/rl/model/learning_model.py diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 1212fef91..2bccc7a22 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -20,7 +20,8 @@ policy: algorithm: dqn share_model: true model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. - network: + device: cpu + network: hidden_dims: - 16 - 8 diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py index e0168d2ef..66fddde64 100644 --- a/examples/supply_chain/policies.py +++ b/examples/supply_chain/policies.py @@ -31,7 +31,9 @@ def forward(self, states): def get_dqn_policy(config): q_net = SimpleQNet( - FullyConnectedBlock(**config["model"]["network"]), optim_option=OptimOption(**config["model"]["optimization"]) + FullyConnectedBlock(**config["model"]["network"]), + optim_option=OptimOption(**config["model"]["optimization"]), + device=config["model"]["device"] ) experience_memory = ExperienceMemory(**config["experience_memory"]) diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 81972e476..63de8505b 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -49,7 +49,8 @@ class AbsCoreModel(nn.Module): def __init__( self, component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = "cpu" ): super().__init__() self.component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) @@ -71,7 +72,8 @@ def __init__( if optim_option.scheduler_cls: self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) - self.device = torch.device('cpu') + self.device = torch.device(device) + self.to(self.device) @property def trainable(self) -> bool: diff --git a/maro/rl/model/learning_model.py b/maro/rl/model/learning_model.py deleted file mode 100644 index a07eede8e..000000000 --- a/maro/rl/model/learning_model.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import abstractmethod -from typing import Dict, List, Union - -import torch -import torch.nn as nn - -from maro.rl.cls_index import TORCH_LR_SCHEDULER_CLS, TORCH_OPTIM_CLS -from maro.rl.utils import get_cls -from maro.utils import clone -from maro.utils.exception.rl_toolkit_exception import MissingOptimizer - - -class OptimOption: - """Model optimization options. - Args: - optim_cls: A string indicating an optimizer class provided by torch.optim or custom subclass of - torch.optim.Optimizer. If a string is provided, it must be present in the ``TORCH_OPTIM`` index. - optim_params (dict): Parameters for the optimizer class. - scheduler_cls: A string indicating an lr-scheduler class provided by torch.optim.lr_scheduler or custom - subclass of torch.optim.lr_scheduler. If a string is provided, it must be present in the - ``TORCH_LR_SCHEDULER`` index. Defaults to None. - scheduler_params (dict): Parameters for the scheduler class. Defaults to None. - """ - __slots__ = ["optim_cls", "optim_params", "scheduler_cls", "scheduler_params"] - - def __init__(self, optim_cls, optim_params: dict, scheduler_cls=None, scheduler_params: dict = None): - self.optim_cls = get_cls(optim_cls, TORCH_OPTIM_CLS) - self.optim_params = optim_params - self.scheduler_cls = get_cls(scheduler_cls, TORCH_LR_SCHEDULER_CLS) - self.scheduler_params = scheduler_params - - -class AbsCoreModel(nn.Module): - """Trainable model that consists of multiple network components. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None - ): - super().__init__() - self._component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) - if optim_option is None: - self.optimizer = None - self.scheduler = None - self.eval() - for param in self.parameters(): - param.requires_grad = False - else: - if isinstance(optim_option, dict): - self.optimizer = {} - for name, opt in optim_option.items(): - self.optimizer[name] = opt.optim_cls(self._component[name].parameters(), **opt.optim_params) - if opt.scheduler_cls: - self.scheduler[name] = opt.scheduler_cls(self.optimizer[name], **opt.scheduler_params) - else: - self.optimizer = optim_option.optim_cls(self.parameters(), **optim_option.optim_params) - if optim_option.scheduler_cls: - self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) - - @property - def trainable(self) -> bool: - return self.optimizer is not None - - @abstractmethod - def forward(self, *args, **kwargs): - raise NotImplementedError - - def step(self, loss): - """Use the loss to back-propagate gradients and apply them to the underlying parameters.""" - if self.optimizer is None: - raise MissingOptimizer("No optimizer registered to the model") - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.zero_grad() - else: - self.optimizer.zero_grad() - - # Obtain gradients through back-propagation - loss.backward() - - # Apply gradients - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.step() - else: - self.optimizer.step() - - def update_learning_rate(self, component_name: Union[str, List[str]] = None): - if not isinstance(self.scheduler, dict): - self.scheduler.step() - elif isinstance(component_name, str): - if component_name not in self.scheduler: - raise KeyError(f"Component {component_name} does not have a learning rate scheduler") - self.scheduler[component_name].step() - elif isinstance(component_name, list): - for key in component_name: - if key not in self.scheduler: - raise KeyError(f"Component {key} does not have a learning rate scheduler") - self.scheduler[key].step() - else: - for sch in self.scheduler.values(): - sch.step() - - def soft_update(self, other_model: nn.Module, tau: float): - for params, other_params in zip(self.parameters(), other_model.parameters()): - params.data = (1 - tau) * params.data + tau * other_params.data - - def copy(self, with_optimizer: bool = False): - model_copy = clone(self) - if not with_optimizer: - model_copy.optimizer = None - model_copy.scheduler = None - - return model_copy - - -class SimpleMultiHeadModel(AbsCoreModel): - """A compound network structure that consists of multiple task heads and an optional shared stack. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - All components must have the same input dimension except the one designated as the shared - component by ``shared_component_name``. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for - the components. Defaults to None. - shared_component_name (str): Name of the network component to be designated as the shared component at the - bottom of the architecture. Must be None or a key in ``component``. If only a single component - is present, this is ignored. Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - shared_component_name: str = None - ): - super().__init__(component, optim_option=optim_option) - if isinstance(component, dict): - if shared_component_name is not None: - assert (shared_component_name in component), ( - f"shared_component_name must be one of {list(component.keys())}, got {shared_component_name}" - ) - self._task_names = [name for name in component if name != shared_component_name] - else: - self._task_names = None - self._shared_component_name = shared_component_name - - @property - def task_names(self): - return self._task_names - - def _forward(self, inputs, task_name: str = None): - if not isinstance(self._component, nn.ModuleDict): - return self._component(inputs) - - if self._shared_component_name is not None: - inputs = self._component[self._shared_component_name](inputs) # features - - if task_name is None: - return {name: self._component[name](inputs) for name in self._task_names} - - if isinstance(task_name, list): - return {name: self._component[name](inputs) for name in task_name} - else: - return self._component[task_name](inputs) - - def forward(self, inputs, task_name: Union[str, List[str]] = None, training: bool = True): - """Feedforward computations for the given head(s). - - Args: - inputs: Inputs to the model. - task_name (str): The name of the task for which the network output is required. If the model contains only - one task module, the task_name is ignored and the output of that module will be returned. If the model - contains multiple task modules, then 1) if task_name is None, the output from all task modules will be - returned in the form of a dictionary; 2) if task_name is a list, the outputs from the task modules - specified in the list will be returned in the form of a dictionary; 3) if this is a single string, - the output from the corresponding task module will be returned. - training (bool): If true, all torch submodules will be set to training mode, and auto-differentiation - will be turned on. Defaults to True. - - Returns: - Outputs from the required head(s). - """ - self.train(mode=training) - if training: - return self._forward(inputs, task_name) - - with torch.no_grad(): - return self._forward(inputs, task_name) From 1e4d09715cb34ec2aeed2d4a6885a95d489067c6 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 2 May 2021 07:18:03 +0000 Subject: [PATCH 216/482] added supply_chain/docker-compose.yml to gitignore --- .gitignore | 3 +- examples/supply_chain/docker-compose.yml | 58 ------------------------ 2 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 examples/supply_chain/docker-compose.yml diff --git a/.gitignore b/.gitignore index 9b443cf3e..4a729c484 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ maro_venv/ pyvenv.cfg htmlcov/ .coverage -*supply_chain_*/ \ No newline at end of file +*supply_chain_*/ +examples/supply_chain/docker-compose.yml \ No newline at end of file diff --git a/examples/supply_chain/docker-compose.yml b/examples/supply_chain/docker-compose.yml deleted file mode 100644 index c00bd275d..000000000 --- a/examples/supply_chain/docker-compose.yml +++ /dev/null @@ -1,58 +0,0 @@ -services: - actor.0: - command: - - python3 - - /maro/supply_chain/distributed_launcher.py - - -w - - '2' - container_name: actor.0 - environment: - - COMPONENT_NAME=actor.0 - image: maro-sc - volumes: - - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl - actor.1: - command: - - python3 - - /maro/supply_chain/distributed_launcher.py - - -w - - '2' - container_name: actor.1 - environment: - - COMPONENT_NAME=actor.1 - image: maro-sc - volumes: - - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl - actor.2: - command: - - python3 - - /maro/supply_chain/distributed_launcher.py - - -w - - '2' - container_name: actor.2 - environment: - - COMPONENT_NAME=actor.2 - image: maro-sc - volumes: - - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl - learner: - build: - context: /home/data_disk/yaqiu/maro - dockerfile: /home/data_disk/yaqiu/maro/docker_files/dev.df - command: - - python3 - - /maro/supply_chain/distributed_launcher.py - - -w - - '1' - container_name: learner - image: maro-sc - volumes: - - /home/data_disk/yaqiu/maro/examples/supply_chain:/maro/supply_chain - - /home/data_disk/yaqiu/maro/maro/rl:/maro/maro/rl - redis: - container_name: maro-redis - image: redis:6 -version: '3.9' From 7ec349f00a4bbb5d4d396356d4dd80acc3acea69 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 2 May 2021 07:20:32 +0000 Subject: [PATCH 217/482] changed default required_actor_finishes from 2 to 3 --- examples/supply_chain/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 2bccc7a22..0e16b3473 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -72,6 +72,6 @@ distributed: redis_port: 6379 # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent # slow actors from dragging down the whole process. - required_actor_finishes: 2 + required_actor_finishes: 3 # If true, experiences from older segments (usually coming from slow actors) will not be used for learning. discard_stale_experiences: True From 7ecbb8ad87fae661ca2219b939da8ab8f84f959d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 3 May 2021 16:18:33 +0000 Subject: [PATCH 218/482] bug fix in policy update --- maro/rl/policy/policy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index e82f19e59..2ce7ce6ad 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -94,12 +94,10 @@ def update(self): return False self._last_exp_mem_size = len(self.experience_memory) - exp_mem, sampler, config = self.experience_memory, self.sampler, self.generic_config for _ in range(self.generic_config.train_iters): self.learn(self.experience_memory.get(self.sampler.sample())) - return True - return False + return True @abstractmethod def learn(self, experience_set: ExperienceSet): From 85c9c7edcbde96bb3cf1c8d2fc685deacb550852 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 4 May 2021 03:13:02 +0000 Subject: [PATCH 219/482] added updated policy log in learner --- maro/rl/training/learner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 7e6bfa831..4a27bfa68 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -146,6 +146,7 @@ def _train_local(self, ep: int): tl0 = time.time() self.policy.store_experiences(exp_by_agent) updated_policy_ids = self.policy.update() + self._logger.info(f"updated policies {updated_policy_ids}") self.end_of_training(ep, segement_index, **self.end_of_training_kwargs) self._total_learning_time += time.time() - tl0 self._total_env_steps += self.policy_update_interval @@ -176,6 +177,7 @@ def _train_remote(self, ep: int): tl0 = time.time() self.policy.store_experiences(exp) updated_policy_ids = self.policy.update() + self._logger.info(f"updated policies {updated_policy_ids}") self.end_of_training(ep, segment_index, **self.end_of_training_kwargs) num_actor_finishes += done self._total_learning_time += time.time() - tl0 From a02305bd9979ebee31e53994963557575c0251f7 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 4 May 2021 03:19:43 +0000 Subject: [PATCH 220/482] refined logging --- maro/rl/training/learner.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 4a27bfa68..755aaa9aa 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -146,7 +146,8 @@ def _train_local(self, ep: int): tl0 = time.time() self.policy.store_experiences(exp_by_agent) updated_policy_ids = self.policy.update() - self._logger.info(f"updated policies {updated_policy_ids}") + if updated_policy_ids: + self._logger.info(f"updated policies {updated_policy_ids}") self.end_of_training(ep, segement_index, **self.end_of_training_kwargs) self._total_learning_time += time.time() - tl0 self._total_env_steps += self.policy_update_interval @@ -166,7 +167,7 @@ def _train_local(self, ep: int): def _train_remote(self, ep: int): t0 = time.time() - updated_policy_ids, num_actor_finishes, segment_index = list(self.policy.policy_dict.keys()), 0, 0 + updated_policy_ids, num_actor_finishes, segment_index = list(self.policy.policy_dict.keys()), 0, 1 while num_actor_finishes < self.required_actor_finishes: for exp, done in self.actor_manager.collect( ep, segment_index, self.policy_update_interval, @@ -177,7 +178,8 @@ def _train_remote(self, ep: int): tl0 = time.time() self.policy.store_experiences(exp) updated_policy_ids = self.policy.update() - self._logger.info(f"updated policies {updated_policy_ids}") + if updated_policy_ids: + self._logger.info(f"updated policies {updated_policy_ids}") self.end_of_training(ep, segment_index, **self.end_of_training_kwargs) num_actor_finishes += done self._total_learning_time += time.time() - tl0 From b709150560b4fa897e15ebc0fd3166ec60b5ff70 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 4 May 2021 10:28:50 +0000 Subject: [PATCH 221/482] refined policy training trigger logic --- maro/rl/policy/policy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 2ce7ce6ad..7f06c47c2 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -66,7 +66,7 @@ def __init__(self, experience_memory: ExperienceMemory, generic_config: Training self.special_config = special_config sampler_cls, batch_size = generic_config.sampler_cls, generic_config.batch_size self.sampler = sampler_cls(experience_memory, batch_size, **generic_config.sampler_kwargs) - self._last_exp_mem_size = 0 # experience memory size when the last update was made + self._num_new_exp = 0 # experience memory size when the last update was made self._warm_up = True self._update_ready = False @@ -85,15 +85,15 @@ def choose_action(self, state): def store_experiences(self, experience_set: ExperienceSet): self.experience_memory.put(experience_set) - exp_mem_size = len(self.experience_memory) - self._warm_up = exp_mem_size < self.generic_config.num_warmup_experiences - self._update_ready = (exp_mem_size - self._last_exp_mem_size) >= self.generic_config.new_experience_trigger + self._num_new_exp += len(experience_set) + self._warm_up = len(self.experience_memory) < self.generic_config.num_warmup_experiences + self._update_ready = self._num_new_exp >= self.generic_config.new_experience_trigger def update(self): if self._warm_up or not self._update_ready: return False - self._last_exp_mem_size = len(self.experience_memory) + self._num_new_exp = 0 for _ in range(self.generic_config.train_iters): self.learn(self.experience_memory.get(self.sampler.sample())) From 2a3b43d67d194533644a8c4571dc2af5dca46d31 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 7 May 2021 09:14:42 +0000 Subject: [PATCH 222/482] added policy update schedule feature --- examples/cim/ac/config.yml | 84 ++++---- examples/cim/ac/main.py | 6 +- examples/cim/dqn/config.yml | 2 +- maro/rl/__init__.py | 8 +- maro/rl/algorithm/ac.py | 10 +- maro/rl/algorithm/ddpg.py | 10 +- maro/rl/algorithm/dqn.py | 12 +- maro/rl/algorithm/pg.py | 13 +- maro/rl/experience/sampler.py | 15 +- maro/rl/exploration/abs_exploration.py | 4 +- maro/rl/model/core_model.py | 6 +- maro/rl/policy/__init__.py | 5 +- maro/rl/policy/multi_agent_policy.py | 51 ++--- maro/rl/policy/policy.py | 53 ++--- maro/rl/training/__init__.py | 6 +- maro/rl/training/actor_manager.py | 13 +- maro/rl/training/distributed_learner.py | 161 ++++++++++++++ maro/rl/training/learner.py | 190 ---------------- maro/rl/training/local_learner.py | 204 ++++++++++++++++++ maro/rl/training/policy_update_schedule.py | 63 ++++++ .../rl_formulation.ipynb | 2 +- 21 files changed, 578 insertions(+), 340 deletions(-) create mode 100644 maro/rl/training/distributed_learner.py delete mode 100644 maro/rl/training/learner.py create mode 100644 maro/rl/training/local_learner.py create mode 100644 maro/rl/training/policy_update_schedule.py diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index af5dde14f..b129f51c5 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -1,6 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +env: + scenario: cim + topology: toy.4p_ssdd_l0.0 + durations: 1120 +max_episode: 50 +policy_update_interval: -1 shaping: port_attributes: - empty @@ -26,51 +32,49 @@ shaping: fulfillment_factor: 1.0 shortage_factor: 1.0 time_decay: 0.97 -agent: +policy: model: actor: - hidden_dims: - - 256 - - 128 - - 64 - activation: tanh - softmax: true - batch_norm: False - head: true - critic: - hidden_dims: - - 256 - - 128 - - 64 - activation: leaky_relu - softmax: false - batch_norm: true - head: true - optimization: - actor: - optim_cls: adam - optim_params: - lr: 0.001 + network: + hidden_dims: + - 256 + - 128 + - 64 + activation: tanh + softmax: true + batch_norm: False + head: true + optimization: + optim_cls: adam + optim_params: + lr: 0.001 critic: - optim_cls: rmsprop - optim_params: - lr: 0.001 - algorithm: + network: + hidden_dims: + - 256 + - 128 + - 64 + activation: leaky_relu + softmax: false + batch_norm: true + head: true + optimization: + optim_cls: rmsprop + optim_params: + lr: 0.001 + algorithm_config: reward_discount: .0 critic_loss_cls: smooth_l1 - train_iters: 10 + num_steps: 10 actor_loss_coefficient: 0.1 # clip_ratio: 0.8 # for PPO - experience_memory: - experience_memory_size: -1 - experience_memory_overwrite_type: "rolling" - empty_experience_memory_after_step: bool, + training_loop_config: + sampler_cls: full + batch_size: -1 + num_steps: 10 + sampler_kwargs: new_experience_trigger: 1 - min_experiences_to_trigger_training: 1 -training: - env: - scenario: cim - topology: toy.4p_ssdd_l0.0 - durations: 1120 - max_episode: 50 - agent_update_interval: -1 \ No newline at end of file + num_warmup_experiences: 1 + experience_memory: + capacity: -1 + overwrite_type: "rolling" diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index a41e2b549..49c0a9ded 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -31,18 +31,18 @@ OUT_DIM = config["shaping"]["num_actions"] -def get_ac_agent(): +def get_ac_policy(): cfg = config["agent"] actor_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["actor"]) critic_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **cfg["model"]["critic"]) - ac_model = SimpleMultiHeadModel( + ac_net = SimpleMultiHeadModel( {"actor": actor_net, "critic": critic_net}, optim_option={ "actor": OptimOption(**cfg["optimization"]["actor"]), "critic": OptimOption(**cfg["optimization"]["critic"]) } ) - return ActorCritic(ac_model, ActorCriticConfig(**cfg["algorithm"]), **cfg["experience_memory"]) + return ActorCritic(ac_net, , ActorCriticConfig(**cfg["algorithm"])) # Single-threaded launcher diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 565d48dc6..ec1d3bd43 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -44,7 +44,7 @@ agent: lr: 0.05 algorithm: reward_discount: .0 - train_iters: 10 + num_steps: 10 batch_size: 128 loss_cls: smooth_l1 target_update_freq: 5 diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 19bba3fdd..26d66a574 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -15,8 +15,8 @@ AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, PolicyNetForDiscreteActionSpace, PolicyValueNetForContinuousActionSpace, PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace ) -from maro.rl.policy import AbsCorePolicy, AbsFixedPolicy, MultiAgentPolicy, NullPolicy, RLPolicy, TrainingLoopConfig -from maro.rl.training import Actor, ActorManager, Learner +from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy, TrainingLoopConfig +from maro.rl.training import Actor, ActorManager, DistributedLearner, LocalLearner, MultiPolicyUpdateSchedule from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward @@ -32,8 +32,8 @@ "UniformNoiseExploration", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "PolicyNetForDiscreteActionSpace", "PolicyValueNetForContinuousActionSpace", "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace", - "AbsCorePolicy", "AbsFixedPolicy", "MultiAgentPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig", - "Actor", "ActorManager", "Learner", + "AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig", + "Actor", "ActorManager", "DistributedLearner", "LocalLearner", "MultiPolicyUpdateSchedule", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index a46191cfd..6d72d13b8 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -80,14 +80,14 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def learn(self, experience_set: ExperienceSet): + def step(self, experience_set: ExperienceSet): if not isinstance(experience_set, ExperienceSet): - raise TypeError(f"Expected experience object of type ACExperience, got {type(experience_set)}") + raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])) - log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)) + actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.ac_net.device) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.ac_net.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.ac_net.device) state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index cfa2276c9..8f1cbc229 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -79,17 +79,17 @@ def choose_action(self, states) -> Union[float, np.ndarray]: return actions[0] if len(actions) == 1 else actions - def learn(self, experience_set: ExperienceSet): + def step(self, experience_set: ExperienceSet): if not isinstance(experience_set, ExperienceSet): - raise TypeError(f"Expected experience object of type DDPGExperience, got {type(experience_set)}") + raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") states, next_states = experience_set.states, experience_set.next_states - actual_actions = torch.from_numpy(experience_set.actions) - rewards = torch.from_numpy(experience_set.rewards) + actual_actions = torch.from_numpy(experience_set.actions).to(self.ac_net.device) + rewards = torch.from_numpy(experience_set.rewards).to(self.ac_net.device) if len(actual_actions.shape) == 1: actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - current_q_values = self.ac_net(torch.cat([states, actual_actions], dim=1), task_name="q_value") + current_q_values = self.ac_net(torch.cat([states, actual_actions], dim=1)) current_q_values = current_q_values.squeeze(dim=1) # (N,) next_actions = self.target_ac_net(states, task_name="policy", training=False) next_q_values = self.target_ac_net( diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index 3cf0a67c6..bfb81c915 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -29,7 +29,7 @@ class DQNConfig: it must be a key in ``TORCH_LOSS``. Defaults to "mse". """ __slots__ = [ - "reward_discount", "target_update_freq", "train_iters", "batch_size", "sampler_cls", "sampler_params", + "reward_discount", "target_update_freq", "num_steps", "batch_size", "sampler_cls", "sampler_params", "soft_update_coefficient", "double", "loss_func" ] @@ -81,17 +81,15 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = actions.cpu().numpy() return actions[0] if len(actions) == 1 else actions - def learn(self, experience_set: ExperienceSet): + def step(self, experience_set: ExperienceSet): if not isinstance(experience_set, ExperienceSet): - raise TypeError( - f"Expected experience object of type AbsCorePolicy.experience_type, got {type(experience_set)}" - ) + raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") self.q_net.train() # sample from the replay memory states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray(experience_set.actions)) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)) + actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.q_net.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.q_net.device) q_values = self.q_net.q_values(states, actions) # get next Q values with torch.no_grad(): diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index 3d01e7db3..b49a3966c 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -84,11 +84,14 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: return (action[0], log_p[0]) if is_single else (action, log_p) def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): - states = torch.from_numpy(states).to(self.device) - actions = torch.from_numpy(actions).to(self.device) + if not isinstance(experience_set, ExperienceSet): + raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") + + states = experience_set.states + actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)) returns = get_truncated_cumulative_reward(rewards, self.special_config.reward_discount) returns = torch.from_numpy(returns).to(self.device) - action_distributions = self.model(states) - action_prob = action_distributions.gather(1, actions.unsqueeze(1)).squeeze() # (N, 1) - loss = -(torch.log(action_prob) * returns).mean() + loss = -(log_p * returns).mean() self.model.step(loss) diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index 1777f0471..abafef94c 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -18,7 +18,6 @@ def __init__(self, data: ExperienceMemory, batch_size: int): def sample(self) -> List[int]: raise NotImplementedError - @abstractmethod def update(self): """Update statistics used for sampling.""" pass @@ -43,7 +42,15 @@ def __init__(self, data: ExperienceMemory, batch_size: int, replace: bool = True def sample(self): indexes = np.random.choice(len(self.data), size=self.batch_size, replace=self.replace) - return indexes + return indexes, self.data.get(indexes) - def update(self, indexes, values): - pass + +class FullSampler(AbsSampler): + """ + """ + def __init__(self, data: ExperienceMemory, batch_size: int = -1, empty_after_use: bool = True): + super().__init__(data, batch_size) + self.empty_after_use = empty_after_use + + def sample(self): + return self.data.get() \ No newline at end of file diff --git a/maro/rl/exploration/abs_exploration.py b/maro/rl/exploration/abs_exploration.py index 28f9f4c5c..bfcc10285 100644 --- a/maro/rl/exploration/abs_exploration.py +++ b/maro/rl/exploration/abs_exploration.py @@ -18,10 +18,8 @@ def register_schedule( param_name: str, last_ep: int, initial_value=None, - kwargs: dict = None + **kwargs ): - if kwargs is None: - kwargs = {} self.scheduler[param_name] = scheduler_cls(self, param_name, last_ep, initial_value=initial_value, **kwargs) @abstractmethod diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 81972e476..63de8505b 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -49,7 +49,8 @@ class AbsCoreModel(nn.Module): def __init__( self, component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = "cpu" ): super().__init__() self.component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) @@ -71,7 +72,8 @@ def __init__( if optim_option.scheduler_cls: self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) - self.device = torch.device('cpu') + self.device = torch.device(device) + self.to(self.device) @property def trainable(self) -> bool: diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index dde452fac..0edc80128 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .multi_agent_policy import MultiAgentPolicy -from .policy import AbsCorePolicy, AbsFixedPolicy, NullPolicy, RLPolicy, TrainingLoopConfig +from .policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy, TrainingLoopConfig -__all__ = ["AbsCorePolicy", "AbsFixedPolicy", "MultiAgentPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig"] +__all__ = ["AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig"] diff --git a/maro/rl/policy/multi_agent_policy.py b/maro/rl/policy/multi_agent_policy.py index f69367d5c..f6f8990f2 100644 --- a/maro/rl/policy/multi_agent_policy.py +++ b/maro/rl/policy/multi_agent_policy.py @@ -5,7 +5,7 @@ import pickle import warnings from collections import defaultdict, namedtuple -from typing import Dict, List, Union +from typing import Dict, List from maro.rl.exploration import AbsExploration, NullExploration from maro.rl.experience import ExperienceMemory @@ -21,16 +21,14 @@ class MultiAgentPolicy: """ def __init__( self, - policy_dict: Dict[str, Union[AbsFixedPolicy, AbsCorePolicy]], + policy_dict: Dict[str, AbsPolicy], agent_to_policy: Dict[str, str], exploration_dict: Dict[str, AbsExploration] = None, agent_to_exploration: Dict[str, str] = None ): self.policy_dict = policy_dict self.agent_to_policy = agent_to_policy - self.policy = { - agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items() - } + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items()} self.agent_groups_by_policy = defaultdict(list) for agent_id, policy_id in agent_to_policy.items(): self.agent_groups_by_policy[policy_id].append(agent_id) @@ -45,7 +43,7 @@ def __init__( agent_id: self.exploration_dict[exploration_id] for agent_id, exploration_id in self.agent_to_exploration.items() } - self.with_exploration = True + self.exploration_enabled = True self.agent_groups_by_exploration = defaultdict(list) for agent_id, exploration_id in agent_to_exploration.items(): self.agent_groups_by_exploration[exploration_id].append(agent_id) @@ -54,21 +52,15 @@ def __init__( self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) def train_mode(self): - self.with_exploration = True + if hasattr(self, "exploration_enabled"): + self.exploration_enabled = True def eval_mode(self): - self.with_exploration = False - - @property - def exploration_params(self): - if hasattr(self, "exploration"): - return { - agent_ids: self.exploration_dict[exploration_id].parameters - for exploration_id, agent_ids in self.agent_groups_by_exploration.items() - } + if hasattr(self, "exploration_enabled"): + self.exploration_enabled = False def choose_action(self, state_by_agent: dict): - if self.exploration_dict and self.with_exploration: + if self.exploration_dict and self.exploration_enabled: return { agent_id: self.exploration[agent_id](self.policy[agent_id].choose_action(state)) @@ -83,16 +75,13 @@ def store_experiences(self, experiences_by_agent: dict): if isinstance(self.policy[agent_id], AbsCorePolicy): self.policy[agent_id].store_experiences(exp) - def update(self) -> List[str]: - return [ - policy_id for policy_id, policy in self.policy_dict.items() - if isinstance(policy, AbsCorePolicy) and policy.update() - ] + def update(self, policy_ids: List[str] = None): + if policy_ids is None: + policy_ids = self.policy_dict.keys() - def exploration_step(self): - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() + for policy_id in policy_ids: + if self.policy_dict[policy_id].update(): + self.policy_dict[policy_id].post_update() def load_state(self, policy_state_dict: dict): """Load policies from memory.""" @@ -102,11 +91,11 @@ def load_state(self, policy_state_dict: dict): for policy_id, policy_state in policy_state_dict.items(): self.policy_dict[policy_id].load_state(policy_state) - def state(self): - return { - policy_id: policy.state() for policy_id, policy in self.policy_dict.items() - if isinstance(policy, AbsCorePolicy) - } + def get_state(self, policy_ids: List[str] = None): + if policy_ids is None: + policy_ids = self.policy_dict.keys() + + return {policy_id: self.policy_dict[policy_id].get_state() for policy_id in policy_ids} def load(self, dir_path: str): """Load models from disk.""" diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index e82f19e59..791086591 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -9,36 +9,29 @@ class TrainingLoopConfig: - __slots__ = [ - "sampler_cls", "batch_size", "train_iters", "sampler_kwargs", "new_experience_trigger", - "num_warmup_experiences" - ] + __slots__ = ["sampler_cls", "batch_size", "train_iters", "sampler_kwargs"] def __init__( self, sampler_cls, batch_size: int, train_iters: int, - sampler_kwargs: dict = None, - new_experience_trigger: int = 1, - num_warmup_experiences: int = 1 + sampler_kwargs: dict = None ): self.sampler_cls = sampler_cls self.batch_size = batch_size self.train_iters = train_iters self.sampler_kwargs = sampler_kwargs if sampler_kwargs else {} - self.new_experience_trigger = new_experience_trigger - self.num_warmup_experiences = num_warmup_experiences -class AbsFixedPolicy(ABC): +class AbsPolicy(ABC): """Abstract fixed policy class. Args: config: Settings for the algorithm. """ def __init__(self): - pass + super().__init__() @abstractmethod def choose_action(self, state): @@ -54,21 +47,24 @@ def choose_action(self, state): raise NotImplementedError -class NullPolicy(AbsFixedPolicy): +class NullPolicy(AbsPolicy): def choose_action(self, state): return None -class AbsCorePolicy(ABC): - def __init__(self, experience_memory: ExperienceMemory, generic_config: TrainingLoopConfig, special_config): +class AbsCorePolicy(AbsPolicy): + def __init__( + self, + experience_memory: ExperienceMemory, + generic_config: TrainingLoopConfig, + special_config + ): + super().__init__() self.experience_memory = experience_memory self.generic_config = generic_config self.special_config = special_config sampler_cls, batch_size = generic_config.sampler_cls, generic_config.batch_size self.sampler = sampler_cls(experience_memory, batch_size, **generic_config.sampler_kwargs) - self._last_exp_mem_size = 0 # experience memory size when the last update was made - self._warm_up = True - self._update_ready = False @abstractmethod def choose_action(self, state): @@ -85,30 +81,23 @@ def choose_action(self, state): def store_experiences(self, experience_set: ExperienceSet): self.experience_memory.put(experience_set) - exp_mem_size = len(self.experience_memory) - self._warm_up = exp_mem_size < self.generic_config.num_warmup_experiences - self._update_ready = (exp_mem_size - self._last_exp_mem_size) >= self.generic_config.new_experience_trigger def update(self): - if self._warm_up or not self._update_ready: - return False - - self._last_exp_mem_size = len(self.experience_memory) - exp_mem, sampler, config = self.experience_memory, self.sampler, self.generic_config for _ in range(self.generic_config.train_iters): - self.learn(self.experience_memory.get(self.sampler.sample())) - return True - - return False + indexes, sp = self.sampler.sample() + step_info = self.step(sp) + self.sampler.update(indexes, step_info) @abstractmethod - def learn(self, experience_set: ExperienceSet): + def step(self, experience_set: ExperienceSet): raise NotImplementedError - def state(self): + @abstractmethod + def get_state(self): pass - def load_state(self, policy_state): + @abstractmethod + def set_state(self, policy_state): pass def load(self, path: str): diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index e369990d8..2ac4b8a9e 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -3,6 +3,8 @@ from .actor import Actor from .actor_manager import ActorManager -from .learner import Learner +from .distributed_learner import DistributedLearner +from .local_learner import LocalLearner +from .policy_update_schedule import MultiPolicyUpdateSchedule -__all__ = ["Actor", "ActorManager", "Learner"] +__all__ = ["Actor", "ActorManager", "DistributedLearner", "LocalLearner", "MultiPolicyUpdateSchedule"] diff --git a/maro/rl/training/actor_manager.py b/maro/rl/training/actor_manager.py index d21f509c2..bffc834c7 100644 --- a/maro/rl/training/actor_manager.py +++ b/maro/rl/training/actor_manager.py @@ -31,6 +31,7 @@ def __init__( num_actors: int, group_name: str, proxy_options: dict = None, + required_finishes: int = None, log_env_metrics: bool = False, log_dir: str = getcwd() ): @@ -41,6 +42,15 @@ def __init__( proxy_options = {} self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_options) self._actors = self._proxy.peers["actor"] # remote actor ID's + + if required_finishes and required_finishes > self.num_actors: + raise ValueError("required_finishes cannot exceed the number of available actors") + + if required_finishes is None: + required_finishes = self.num_actors + self._logger.info(f"Required number of actor finishes is set to {required_finishes}") + + self.required_finishes = required_finishes self.total_experiences_collected = 0 self.total_env_steps = 0 self.total_reward = defaultdict(float) @@ -54,7 +64,6 @@ def collect( num_steps: int, policy_dict: dict = None, exploration=None, - required_actor_finishes: int = None, discard_stale_experiences: bool = True, return_env_metrics: bool = False ): @@ -101,7 +110,7 @@ def collect( if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: num_finishes += 1 - if num_finishes == required_actor_finishes: + if num_finishes == self.required_finishes: break def evaluate(self, episode_index: int, policy_dict: dict, num_actors: int): diff --git a/maro/rl/training/distributed_learner.py b/maro/rl/training/distributed_learner.py new file mode 100644 index 000000000..2e8e5c077 --- /dev/null +++ b/maro/rl/training/distributed_learner.py @@ -0,0 +1,161 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from collections import defaultdict +from os import getcwd +from typing import Callable, Dict, List, Union + +from maro.communication import Message, Proxy, SessionType +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.policy import AbsPolicy +from maro.utils import Logger + +from .actor_manager import ActorManager +from .message_enums import MsgTag, MsgKey +from .policy_update_schedule import MultiPolicyUpdateSchedule + + +class DistributedLearner(object): + """Learner class for distributed training. + + Args: + policy (MultiAgentPolicy): Learning agents. + + """ + def __init__( + self, + policy_dict: Dict[str, AbsPolicy], + agent_to_policy: Dict[str, str], + num_episodes: int, + policy_update_schedule: MultiPolicyUpdateSchedule, + actor_manager: ActorManager, + experience_update_interval: int = -1, + eval_env: AbsEnvWrapper = None, + eval_schedule: Union[int, List[int]] = None, + num_eval_actors: int = 1, + discard_stale_experiences: bool = True, + log_env_metrics: bool = True, + log_dir: str = getcwd(), + **end_of_episode_kwargs + ): + self._logger = Logger("LEARNER", dump_folder=log_dir) + self.policy_dict = policy_dict + self.agent_to_policy = agent_to_policy + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items()} + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent_to_policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + for policy_id, agent_ids in self.agent_groups_by_policy.items(): + self.agent_groups_by_policy[policy_id] = tuple(agent_ids) + + self.num_episodes = num_episodes + self.policy_update_schedule = policy_update_schedule + self.experience_update_interval = experience_update_interval + + # evaluation + self.eval_env = eval_env + self.num_eval_actors = num_eval_actors + + if eval_schedule is None: + eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + + self.eval_schedule = eval_schedule + self.eval_schedule.sort() + if not self.eval_schedule or num_episodes != self.eval_schedule[-1]: + self.eval_schedule.append(num_episodes) + + self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_schedule}") + self._eval_point_index = 0 + + self.actor_manager = actor_manager + self.discard_stale_experiences = discard_stale_experiences + + self.end_of_episode_kwargs = end_of_episode_kwargs + self._log_env_metrics = log_env_metrics + self._total_learning_time = 0 + self._total_env_steps = 0 + self._total_experiences_collected = 0 + + def run(self): + for ep in range(1, self.num_episodes + 1): + self._train(ep) + + policy_ids = self.policy_update_schedule.pop(ep) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") + self.end_of_episode(ep, **self.end_of_episode_kwargs) + + if ep == self.eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self._evaluate(self._eval_point_index) + + if self.actor_manager: + self.actor_manager.exit() + + def _train(self, ep: int): + t0 = time.time() + learning_time = 0 + num_experiences_collected = 0 + num_actor_finishes, segment_index = 0, 1 + + self.policy_update_schedule.enter(ep) + while num_actor_finishes < self.actor_manager.required_actor_finishes: + for exp_by_agent, done in self.actor_manager.collect( + ep, segment_index, self.experience_update_interval, + policy_dict=self.policy.state(), + discard_stale_experiences=self.discard_stale_experiences + ): + self._store_experiences(exp_by_agent) + num_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) + + # policy update + tl0 = time.time() + policy_ids = self.policy_update_schedule.pop(segment_index) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + learning_time += time.time() - tl0 + num_actor_finishes += done + segment_index += 1 + + self.policy_update_schedule.exit() + # performance details + self._logger.debug( + f"ep {ep} summary - " + f"running time: {time.time() - t0}" + f"env steps: {self.env.step_index}" + f"learning time: {learning_time}" + f"experiences collected: {num_experiences_collected}" + ) + + def _evaluate(self, ep: int): + self._logger.info("Evaluating...") + if self.eval_env: + self.policy.eval_mode() + self.eval_env.save_replay = False + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = self.policy.choose_action(self.eval_env.state) + self.eval_env.step(action) + + if not self.eval_env.state: + self._logger.info(f"total reward: {self.eval_env.total_reward}") + + if self._log_env_metrics: + self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") + else: + self.actor_manager.evaluate(ep, self.policy.state(), self.num_eval_actors) + + def end_of_episode(self, ep: int, **kwargs): + pass + + def _store_experiences(self, experiences_by_agent: dict): + for agent_id, exp in experiences_by_agent.items(): + if isinstance(self.policy[agent_id], AbsCorePolicy): + self.policy[agent_id].store_experiences(exp) \ No newline at end of file diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py deleted file mode 100644 index 7e6bfa831..000000000 --- a/maro/rl/training/learner.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from collections import defaultdict -from os import getcwd -from typing import Callable, Dict, List, Union - -from maro.communication import Message, Proxy, SessionType -from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.policy import AbsCorePolicy, MultiAgentPolicy -from maro.utils import Logger - -from .actor_manager import ActorManager -from .message_enums import MsgTag, MsgKey - - -class Learner(object): - """Learner class for distributed training. - - Args: - policy (MultiAgentPolicy): Learning agents. - - """ - def __init__( - self, - policy: MultiAgentPolicy, - env: AbsEnvWrapper, - num_episodes: int, - eval_env: AbsEnvWrapper = None, - actor_manager: ActorManager = None, - num_eval_actors: int = 1, - policy_update_interval: int = -1, - eval_points: Union[int, List[int]] = None, - required_actor_finishes: str = None, - discard_stale_experiences: bool = True, - log_env_metrics: bool = True, - log_dir: str = getcwd(), - end_of_training_kwargs: dict = None - ): - if env is None and actor_manager is None: - raise Exception("env and actor_manager cannot both be None") - - self._logger = Logger("LEARNER", dump_folder=log_dir) - self.policy = policy - self.env = env - self.num_episodes = num_episodes - self.eval_env = eval_env if eval_env else self.env - self.policy_update_interval = policy_update_interval - - if actor_manager: - self._logger.info( - "Found actor manager. Roll-outs will be performed by remote actors. Local env will not be used." - ) - self.actor_manager = actor_manager - - self.num_eval_actors = num_eval_actors - - # evaluation points - if eval_points is None: - eval_points = [] - elif isinstance(eval_points, int): - num_eval_points = num_episodes // eval_points - eval_points = [eval_points * i for i in range(1, num_eval_points + 1)] - - self.eval_points = eval_points - self.eval_points.sort() - if not self.eval_points or num_episodes != self.eval_points[-1]: - self.eval_points.append(num_episodes) - - self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_points}") - self._eval_point_index = 0 - - # distributed roll-out - self.actor_manager = actor_manager - if self.actor_manager: - if required_actor_finishes and required_actor_finishes > self.actor_manager.num_actors: - raise ValueError("required_actor_finishes cannot exceed the number of available actors") - - if required_actor_finishes is None: - required_actor_finishes = self.actor_manager.num_actors - self._logger.info(f"Required number of actor finishes is set to {required_actor_finishes}") - - self.required_actor_finishes = required_actor_finishes - self.discard_stale_experiences = discard_stale_experiences - - self.end_of_training_kwargs = end_of_training_kwargs if end_of_training_kwargs else {} - self._log_env_metrics = log_env_metrics - self._total_learning_time = 0 - self._total_env_steps = 0 - self._total_experiences_collected = 0 - - def run(self): - for ep in range(1, self.num_episodes + 1): - self.train(ep) - if ep == self.eval_points[self._eval_point_index]: - self._eval_point_index += 1 - self.evaluate(self._eval_point_index) - - if self.actor_manager: - self.actor_manager.exit() - - def train(self, ep: int): - # local mode - if not self.actor_manager: - self._train_local(ep) - else: - self._train_remote(ep) - - def evaluate(self, ep: int): - self._logger.info("Evaluating...") - if self.eval_env: - self.policy.eval_mode() - self.eval_env.save_replay = False - self.eval_env.reset() - self.eval_env.start() # get initial state - while self.eval_env.state: - action = self.policy.choose_action(self.eval_env.state) - self.eval_env.step(action) - - if not self.eval_env.state: - self._logger.info(f"total reward: {self.eval_env.total_reward}") - - if self._log_env_metrics: - self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") - else: - self.actor_manager.evaluate(ep, self.policy.state(), self.num_eval_actors) - - def _train_local(self, ep: int): - t0 = time.time() - segement_index = 1 - self._logger.info(f"Training episode {ep}") - self._logger.debug(f"exploration parameters: {self.policy.exploration_params}") - self.policy.train_mode() - self.env.save_replay = True - self.env.reset() - self.env.start() # get initial state - while self.env.state: - action = self.policy.choose_action(self.env.state) - self.env.step(action) - if ( - not self.env.state or - self.policy_update_interval != -1 and self.env.step_index % self.policy_update_interval == 0 - ): - exp_by_agent = self.env.get_experiences() - tl0 = time.time() - self.policy.store_experiences(exp_by_agent) - updated_policy_ids = self.policy.update() - self.end_of_training(ep, segement_index, **self.end_of_training_kwargs) - self._total_learning_time += time.time() - tl0 - self._total_env_steps += self.policy_update_interval - self._total_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) - self._logger.debug(f"total running time: {time.time() - t0}") - self._logger.debug(f"total learning time: {self._total_learning_time}") - self._logger.debug(f"total env steps: {self._total_env_steps}") - self._logger.debug(f"total experiences collected: {self._total_experiences_collected}") - if not self.env.state: - self._logger.info(f"total reward: {self.env.total_reward}") - - segement_index += 1 - - self.policy.exploration_step() - if self._log_env_metrics: - self._logger.info(f"ep {ep}: {self.env.metrics}") - - def _train_remote(self, ep: int): - t0 = time.time() - updated_policy_ids, num_actor_finishes, segment_index = list(self.policy.policy_dict.keys()), 0, 0 - while num_actor_finishes < self.required_actor_finishes: - for exp, done in self.actor_manager.collect( - ep, segment_index, self.policy_update_interval, - policy_dict=self.policy.state(), - required_actor_finishes=self.required_actor_finishes, - discard_stale_experiences=self.discard_stale_experiences - ): - tl0 = time.time() - self.policy.store_experiences(exp) - updated_policy_ids = self.policy.update() - self.end_of_training(ep, segment_index, **self.end_of_training_kwargs) - num_actor_finishes += done - self._total_learning_time += time.time() - tl0 - self._logger.debug(f"running time: {time.time() - t0}") - self._logger.debug(f"learning time: {self._total_learning_time}") - self._logger.debug(f"env steps: {self.actor_manager.total_env_steps}") - self._logger.debug(f"experiences collected: {self.actor_manager.total_experiences_collected}") - - segment_index += 1 - - def end_of_training(self, ep: int, segment: int, **kwargs): - pass diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py new file mode 100644 index 000000000..443389580 --- /dev/null +++ b/maro/rl/training/local_learner.py @@ -0,0 +1,204 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import heapq +import time +from collections import defaultdict, namedtuple +from os import getcwd +from typing import Callable, Dict, List, Tuple, Union + +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.policy import AbsPolicy +from maro.utils import Logger + +from .policy_update_schedule import MultiPolicyUpdateSchedule + + +class LocalLearner(object): + """Learner class for distributed training. + + Args: + policy (MultiAgentPolicy): Learning agents. + + """ + def __init__( + self, + policy_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str], + env: AbsEnvWrapper, + num_episodes: int, + policy_update_schedule: MultiPolicyUpdateSchedule, + exploration_dict: Dict[str, AbsExploration] = None, + agent_to_exploration: Dict[str, str] = None, + experience_update_interval: int = -1, + eval_env: AbsEnvWrapper = None, + eval_schedule: Union[int, List[int]] = None, + log_env_metrics: bool = True, + log_total_reward: bool = True, + log_dir: str = getcwd(), + **end_of_episode_kwargs + ): + self._logger = Logger("LEARNER", dump_folder=log_dir) + + # mappings between agents and policies + self.policy_dict = policy_dict + self.agent2policy = agent2policy + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent2policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + for policy_id, agent_ids in self.agent_groups_by_policy.items(): + self.agent_groups_by_policy[policy_id] = tuple(agent_ids) + + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent_to_policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + for policy_id, agent_ids in self.agent_groups_by_policy.items(): + self.agent_groups_by_policy[policy_id] = tuple(agent_ids) + + # mappings between exploration schemes and agents + self.exploration_dict = exploration_dict + if exploration_dict: + self.agent_to_exploration = agent_to_exploration + self.exploration = { + agent_id: self.exploration_dict[exploration_id] + for agent_id, exploration_id in self.agent_to_exploration.items() + } + self.exploration_enabled = True + self.agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in agent_to_exploration.items(): + self.agent_groups_by_exploration[exploration_id].append(agent_id) + + for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): + self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) + + self.env = env + self.num_episodes = num_episodes + self.policy_update_schedule = policy_update_schedule + self.eval_env = eval_env if eval_env else self.env + self.experience_update_interval = experience_update_interval + + # evaluation schedule + if eval_schedule is None: + eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + + self.eval_schedule = eval_schedule + self.eval_schedule.sort() + if not self.eval_schedule or num_episodes != self.eval_schedule[-1]: + self.eval_schedule.append(num_episodes) + + self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_schedule}") + self._eval_point_index = 0 + + self.end_of_episode_kwargs = end_of_episode_kwargs + self._log_env_metrics = log_env_metrics + self._log_total_reward = log_total_reward + + def run(self): + for ep in range(1, self.num_episodes + 1): + self._train(ep) + + policy_ids = self.policy_update_schedule.pop(ep) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") + + if ep == self.eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self._evaluate(self._eval_point_index) + + self.end_of_episode(ep, **self.end_of_episode_kwargs) + + def _train(self, ep: int): + t0 = time.time() + learning_time = 0 + num_experiences_collected = 0 + + self._logger.info(f"Training episode {ep}") + if hasattr(self, "exploration_dict"): + exploration_parameters = { + agent_ids: self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self.agent_groups_by_exploration.items() + } + self._logger.debug(f"Exploration parameters: {exploration_params}") + + self.env.save_replay = True + self.env.reset() + self.env.start() # get initial state + self.policy_update_schedule.enter(ep) + while self.env.state: + if self.exploration_dict: + action = { + agent_id: + self.exploration[agent_id](self.policy[agent_id].choose_action(state)) + if agent_id in self.exploration else self.policy[agent_id].choose_action(state) + for agent_id, state in self.env.state.items() + } + else: + action = {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in self.env.state.items()} + + self.env.step(action) + step_index = self.env.step_index + + # experience collection + if not self.env.state or step_index % self.experience_update_interval == 0: + exp_by_agent = self.env.get_experiences() + self._store_experiences(exp_by_agent) + num_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) + + # policy update + tl0 = time.time() + policy_ids = self.policy_update_schedule.pop(step_index) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + + self._logger.info(f"Updated policies {policy_ids} after step {step_index}") + learning_time += time.time() - tl0 + + # update the exploration parameters + self._exploration_step() + self.policy_update_schedule.exit() + # performance details + if self._log_env_metrics: + self._logger.info(f"ep {ep}: {self.env.metrics}") + if self._log_total_reward: + self._logger.info(f"ep {ep} total reward received: {self.env.total_reward}") + self._logger.debug( + f"ep {ep} summary - " + f"running time: {time.time() - t0}" + f"env steps: {self.env.step_index}" + f"learning time: {learning_time}" + f"experiences collected: {num_experiences_collected}" + ) + + def _evaluate(self, ep: int): + self._logger.info("Evaluating...") + self.eval_env.save_replay = False + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in self.env.state.items()} + self.eval_env.step(action) + + if self._log_env_metrics: + self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") + if not self.eval_env.state: + self._logger.info(f"total reward: {self.eval_env.total_reward}") + + def end_of_episode(self, ep: int, **kwargs): + pass + + def _store_experiences(self, experiences_by_agent: dict): + for agent_id, exp in experiences_by_agent.items(): + if isinstance(self.policy[agent_id], AbsCorePolicy): + self.policy[agent_id].store_experiences(exp) + + def _exploration_step(self): + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() diff --git a/maro/rl/training/policy_update_schedule.py b/maro/rl/training/policy_update_schedule.py new file mode 100644 index 000000000..ec2a44ddf --- /dev/null +++ b/maro/rl/training/policy_update_schedule.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import heapq +from collections import namedtuple +from typing import List, Union + +EpisodeBasedSchedule = namedtuple("EpisodeBasedSchedule", ["start", "interval"]) +StepBasedSchedule = namedtuple("StepBasedSchedule", ["start_ep", "interval", "end_ep_update"]) + + +class MultiPolicyUpdateSchedule: + """ + """ + def __init__(self, schedule_option: Union[EpisodeBasedSchedule, StepBasedSchedule, dict] = -1): + self._pending_steps = [] + self._pending_episodes = [] + if isinstance(schedule_option, dict): + self._step_schedule_opt = {} + self._episode_schedule_opt = {} + for policy_id, sch in schedule_option.items(): + if isinstance(sch, StepBasedSchedule): + self._step_schedule_opt[policy_id] = (sch.start_ep, sch.interval) + if sch.end_ep_update: + self._episode_schedule_opt[policy_id] = (sch.start_ep, 1) + elif isinstance(sch, EpisodeBasedSchedule): + self._episode_schedule_opt[policy_id] = (sch.start, sch.interval) + + for policy_id, (start, _) in self._episode_schedule_opt.items(): + heapq.heappush(self._pending_episodes, (start, policy_id)) + else: + self._episode_schedule_opt = None + self._step_schedule_opt = None + if isinstance(schedule_option, EpisodeBasedSchedule): + self._episode_schedule_opt = (schedule_option.start, schedule_option.interval) + else: + self._step_schedule_opt = (schedule_option.start_ep, schedule_option.interval) + + heapq.heappush(self._pending_episodes, (start, "*")) + + self._in_episode = False + + def enter(self, ep: int): + self._in_episode = True + for policy_id, (start_ep, interval) in self._step_schedule_opt.items(): + if ep >= start_ep: + heapq.heappush(self._pending_steps, (interval, policy_id)) + + def exit(self): + self._in_episode = False + self._pending_steps.clear() + + def pop(self, index: int) -> List[str]: + policy_ids = [] + pending = self._pending_steps if self._in_episode else self._pending_episodes + schedule_opt = self._step_schedule_opt if self._in_episode else self._episode_schedule_opt + while pending and pending[0][0] == index: + _, policy_id = heapq.heappop(pending) + policy_ids.append(policy_id) + next_index = index + schedule_opt[policy_id][1] + heapq.heappush(pending, (next_index, policy_id)) + + return policy_ids diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 1b5f9802e..b2e541038 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -203,7 +203,7 @@ " \"hyper_params\": {\n", " \"reward_discount\": .0,\n", " \"critic_loss_func\": nn.SmoothL1Loss(),\n", - " \"train_iters\": 10,\n", + " \"num_steps\": 10,\n", " \"actor_loss_coefficient\": 0.1, # loss = actor_loss_coefficient * actor_loss + critic_loss\n", " \"k\": 1, # for k-step return\n", " \"lam\": 0.0 # lambda return coefficient\n", From 43964730dd41d68708cad5206d4d089165900a65 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 10 May 2021 05:11:45 +0000 Subject: [PATCH 223/482] 1. added multi-agent update scheduler; 2. merged exp memory and sampler into ExperienceManager --- examples/cim/dqn/main.py | 9 +- examples/supply_chain/config.yml | 22 +- examples/supply_chain/distributed_launcher.py | 57 ++-- examples/supply_chain/exploration.py | 4 +- examples/supply_chain/learner.py | 6 +- examples/supply_chain/policies.py | 20 +- .../supply_chain/single_thread_launcher.py | 24 +- maro/rl/__init__.py | 16 +- maro/rl/agent/__init__.py | 24 -- maro/rl/agent/ac.py | 124 --------- maro/rl/agent/agent.py | 263 ------------------ maro/rl/agent/agent_cls_index.py | 22 -- maro/rl/agent/agent_manager.py | 64 ----- maro/rl/agent/ddpg.py | 120 -------- maro/rl/agent/dqn.py | 177 ------------ maro/rl/agent/experience_enum.py | 10 - maro/rl/agent/pg.py | 69 ----- maro/rl/algorithm/ac.py | 89 +++--- maro/rl/algorithm/ddpg.py | 74 ++--- maro/rl/algorithm/dqn.py | 89 +++--- maro/rl/algorithm/pg.py | 13 +- maro/rl/distributed/__init__.py | 8 - maro/rl/distributed/actor.py | 83 ------ maro/rl/distributed/actor_manager.py | 111 -------- maro/rl/distributed/dispatcher.py | 58 ---- maro/rl/distributed/learner.py | 71 ----- maro/rl/distributed/message_enums.py | 33 --- maro/rl/distributed/trainer.py | 30 -- maro/rl/experience/__init__.py | 7 +- maro/rl/experience/experience.py | 43 +++ maro/rl/experience/experience_manager.py | 122 ++++++++ maro/rl/experience/experience_memory.py | 168 ----------- maro/rl/experience/sampler.py | 49 ---- maro/rl/experience/sampler_cls_index.py | 16 -- maro/rl/exploration/abs_exploration.py | 4 +- maro/rl/exploration/abs_explorer.py | 20 -- .../rl/exploration/epsilon_greedy_explorer.py | 33 --- maro/rl/exploration/noise_explorer.py | 97 ------- maro/rl/model/core_model.py | 7 +- maro/rl/policy/__init__.py | 5 +- maro/rl/policy/multi_agent_policy.py | 122 -------- maro/rl/policy/policy.py | 78 ++---- maro/rl/training/__init__.py | 12 +- maro/rl/training/actor.py | 84 ++++-- maro/rl/training/actor_manager.py | 29 +- maro/rl/training/distributed_learner.py | 167 +++++++++++ maro/rl/training/local_learner.py | 205 ++++++++++++++ maro/rl/training/policy_update_schedule.py | 61 ++++ 48 files changed, 927 insertions(+), 2092 deletions(-) delete mode 100644 maro/rl/agent/__init__.py delete mode 100644 maro/rl/agent/ac.py delete mode 100644 maro/rl/agent/agent.py delete mode 100644 maro/rl/agent/agent_cls_index.py delete mode 100644 maro/rl/agent/agent_manager.py delete mode 100644 maro/rl/agent/ddpg.py delete mode 100644 maro/rl/agent/dqn.py delete mode 100644 maro/rl/agent/experience_enum.py delete mode 100644 maro/rl/agent/pg.py delete mode 100644 maro/rl/distributed/__init__.py delete mode 100644 maro/rl/distributed/actor.py delete mode 100644 maro/rl/distributed/actor_manager.py delete mode 100755 maro/rl/distributed/dispatcher.py delete mode 100644 maro/rl/distributed/learner.py delete mode 100644 maro/rl/distributed/message_enums.py delete mode 100755 maro/rl/distributed/trainer.py create mode 100644 maro/rl/experience/experience.py create mode 100644 maro/rl/experience/experience_manager.py delete mode 100644 maro/rl/experience/experience_memory.py delete mode 100644 maro/rl/experience/sampler.py delete mode 100644 maro/rl/experience/sampler_cls_index.py delete mode 100644 maro/rl/exploration/abs_explorer.py delete mode 100644 maro/rl/exploration/epsilon_greedy_explorer.py delete mode 100644 maro/rl/exploration/noise_explorer.py delete mode 100644 maro/rl/policy/multi_agent_policy.py create mode 100644 maro/rl/training/distributed_learner.py create mode 100644 maro/rl/training/local_learner.py create mode 100644 maro/rl/training/policy_update_schedule.py diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index ffd589bd4..f34e76fac 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -49,9 +49,7 @@ def get_dqn_agent(): def cim_dqn_learner(): agent = AgentManager({name: get_dqn_agent() for name in Env(**config["training"]["env"]).agent_idx_list}) scheduler = TwoPhaseLinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) - actor_manager = ActorManager( - NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False} - ) + actor_manager = ActorManager(NUM_ACTORS, GROUP, redis_address=(REDIS_HOST, REDIS_PORT), log_enable=False) learner = DistLearner( agent, scheduler, actor_manager, agent_update_interval=config["training"]["agent_update_interval"], @@ -64,10 +62,7 @@ def cim_dqn_learner(): def cim_dqn_actor(): env = Env(**config["training"]["env"]) agent = AgentManager({name: get_dqn_agent() for name in env.agent_idx_list}) - actor = Actor( - CIMEnvWrapper(env, **config["shaping"]), agent, GROUP, - proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)} - ) + actor = Actor(CIMEnvWrapper(env, **config["shaping"]), agent, GROUP, redis_address=(REDIS_HOST, REDIS_PORT)) actor.run() diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 0e16b3473..e3903fba0 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -11,10 +11,9 @@ num_episodes: 10 # number of episodes to simulate # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps # before returning experiences to the learner. The learner uses these experiences to update the agents' policies # and sync the updated policies to the actors for the next learning cycle. -policy_update_interval: -1 -eval_points: 2 +experience_update_interval: 35 +eval_schedule: 2 log_env_metrics: false -end_of_training_kwargs: policy: consumer: algorithm: dqn @@ -38,25 +37,20 @@ policy: lr: 0.001 algorithm_config: reward_discount: .9 + train_epochs: 10 + gradient_iters: 1 loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. target_update_freq: 5 # How many training iteration, to update DQN target model soft_update_coefficient: 0.1 double: false # whether to enable double DQN - training_loop: - sampler_cls: uniform - batch_size: 128 - sampler_kwargs: - replace: true - train_iters: 10 - new_experience_trigger: 128 - # The minimum number of experiences in the experience memory required for learning. Defaults to 1. - num_warmup_experiences: 1000 - experience_memory: + experience_manager: capacity: 50000 # experience memory size # This determines how existing experiences are replaced when adding new experiences to a full experience # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. overwrite_type: random + batch_size: 128 + replace: true exploration: initial_value: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. final_value: 0.0 @@ -68,7 +62,7 @@ distributed: # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make # sure that a local redis server is running and listening on the port specified by "redis_port". - redis_host: maro-redis + redis_host: localhost redis_port: 6379 # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent # slow actors from dragging down the whole process. diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index 2fbad5aec..c95c4a9de 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -3,12 +3,11 @@ import argparse import sys -import yaml from multiprocessing import Process from os import getenv, makedirs from os.path import dirname, join, realpath -from maro.rl import Actor, ActorManager, Learner, MultiAgentPolicy +from maro.rl import Actor, ActorManager, EpisodeBasedSchedule, MultiPolicyUpdateSchedule, StepBasedSchedule from maro.simulator import Env from maro.utils import set_seeds @@ -16,9 +15,9 @@ sys.path.insert(0, sc_code_dir) from config import config from env_wrapper import SCEnvWrapper -from exploration import exploration_dict, agent_to_exploration +from exploration import exploration_dict, agent2exploration from learner import SCLearner -from policies import policy_dict, agent_to_policy +from policies import policy_dict, agent2policy # for distributed / multi-process training @@ -31,60 +30,42 @@ makedirs(log_dir, exist_ok=True) -def get_sc_agents(agent_idx_list, type_): - assert type_ in {"producer", "consumer"} - q_model = get_q_model(config["agent"][type_]["model"]) if config["agent"][type_]["share_model"] else None - alg_type = config["agent"][type_]["algorithm"] - return { - f"{type_}.{info.id}": get_agent_func_map[alg_type](config["agent"][type_], q_model=q_model) - for info in agent_idx_list - } - - def sc_learner(): - # create a multi-agent policy. - policy = MultiAgentPolicy( - policy_dict, - agent_to_policy, - exploration_dict=exploration_dict, - agent_to_exploration=agent_to_exploration - ) - # create an actor manager to collect simulation data from multiple actors actor_manager = ActorManager( - NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False}, + NUM_ACTORS, GROUP, + redis_address=(REDIS_HOST, REDIS_PORT), + log_enable=False, log_dir=log_dir ) + policy_update_schedule = MultiPolicyUpdateSchedule( + {"consumer": StepBasedSchedule(3, 2, True), "producer": EpisodeBasedSchedule(2, 3)} + ) + # create a learner to start training learner = SCLearner( - policy, None, config["num_episodes"], + policy_dict, agent2policy, config["num_episodes"], policy_update_schedule, actor_manager=actor_manager, - policy_update_interval=config["policy_update_interval"], - eval_points=config["eval_points"], - required_actor_finishes=config["distributed"]["required_actor_finishes"], + experience_update_interval=config["experience_update_interval"], + eval_schedule=config["eval_schedule"], log_env_metrics=config["log_env_metrics"], - end_of_training_kwargs=config["end_of_training_kwargs"], log_dir=log_dir ) learner.run() def sc_actor(name: str): - # create an env wrapper for roll-out and obtain the input dimension for the agents - env = SCEnvWrapper(Env(**config["env"])) - policy = MultiAgentPolicy( - policy_dict, - agent_to_policy, - exploration_dict=exploration_dict, - agent_to_exploration=agent_to_exploration - ) # create an actor that collects simulation data for the learner. actor = Actor( - env, policy, GROUP, - proxy_options={"component_name": name, "redis_address": (REDIS_HOST, REDIS_PORT)}, + SCEnvWrapper(Env(**config["env"])), policy_dict, agent2policy, GROUP, + exploration_dict=exploration_dict, + agent2exploration=agent2exploration, + component_name=name, + redis_address=(REDIS_HOST, REDIS_PORT), log_dir=log_dir ) + actor.run() diff --git a/examples/supply_chain/exploration.py b/examples/supply_chain/exploration.py index d7f933e65..0528875ac 100644 --- a/examples/supply_chain/exploration.py +++ b/examples/supply_chain/exploration.py @@ -15,10 +15,10 @@ exploration.register_schedule( LinearExplorationScheduler, "epsilon", config["num_episodes"], initial_value=config["exploration"]["initial_value"], - kwargs={"final_value": config["exploration"]["final_value"]} + final_value=config["exploration"]["final_value"] ) exploration_dict = {"consumer": exploration} # all agents shared the same exploration object -agent_to_exploration = {agent_id: "consumer" for agent_id in config["agent_ids"] if agent_id.startswith("consumer")} +agent2exploration = {agent_id: "consumer" for agent_id in config["agent_ids"] if agent_id.startswith("consumer")} diff --git a/examples/supply_chain/learner.py b/examples/supply_chain/learner.py index 74109f467..236c99d4b 100644 --- a/examples/supply_chain/learner.py +++ b/examples/supply_chain/learner.py @@ -2,10 +2,10 @@ # Licensed under the MIT license. from os.path import dirname, join, realpath -from maro.rl import Learner +from maro.rl import DistributedLearner sc_code_dir = dirname(realpath(__file__)) -class SCLearner(Learner): - def end_of_training(self, ep, segment, **kwargs): +class SCLearner(DistributedLearner): + def end_of_training(self, ep, **kwargs): pass diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py index 66fddde64..a76366ca6 100644 --- a/examples/supply_chain/policies.py +++ b/examples/supply_chain/policies.py @@ -9,8 +9,8 @@ import torch from maro.rl import ( - DQN, DQNConfig, ExperienceMemory, FullyConnectedBlock, NullPolicy, OptimOption, QNetForDiscreteActionSpace, - TrainingLoopConfig, get_sampler_cls + DQN, DQNConfig, EpisodeBasedSchedule, FullyConnectedBlock, NullPolicy, + OptimOption, QNetForDiscreteActionSpace, StepBasedSchedule, UniformSampler ) sc_code_dir = dirname(realpath(__file__)) @@ -35,15 +35,13 @@ def get_dqn_policy(config): optim_option=OptimOption(**config["model"]["optimization"]), device=config["model"]["device"] ) - experience_memory = ExperienceMemory(**config["experience_memory"]) - - config["training_loop"]["sampler_cls"] = get_sampler_cls(config["training_loop"]["sampler_cls"]) - generic_config = TrainingLoopConfig(**config["training_loop"]) - special_config = DQNConfig(**config["algorithm_config"]) - - return DQN(q_net, experience_memory, generic_config, special_config) + experience_manager = UniformSampler(**config["experience_manager"]) + return DQN(q_net, experience_manager, DQNConfig(**config["algorithm_config"])) # all consumers share the same underlying policy -policy_dict = {"consumer": get_dqn_policy(config["consumer"]), "producer": NullPolicy()} +policy_dict = { + "consumer": get_dqn_policy(config["consumer"]), + "producer": get_dqn_policy(config["consumer"]) +} -agent_to_policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} +agent2policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index 6df35efb2..61da803b0 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -4,32 +4,28 @@ import sys from os.path import dirname, realpath -from maro.rl import Learner, MultiAgentPolicy +from maro.rl import EpisodeBasedSchedule, LocalLearner, MultiPolicyUpdateSchedule, StepBasedSchedule from maro.simulator import Env sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) from config import config from env_wrapper import SCEnvWrapper -from exploration import exploration_dict, agent_to_exploration -from policies import policy_dict, agent_to_policy +from exploration import exploration_dict, agent2exploration +from policies import agent2policy, policy_dict # Single-threaded launcher if __name__ == "__main__": env = SCEnvWrapper(Env(**config["env"])) - policy = MultiAgentPolicy( - policy_dict, - agent_to_policy, - exploration_dict=exploration_dict, - agent_to_exploration=agent_to_exploration - ) - + policy_update_schedule = MultiPolicyUpdateSchedule(EpisodeBasedSchedule(3, 2)) # create a learner to start training - learner = Learner( - policy, env, config["num_episodes"], - policy_update_interval=config["policy_update_interval"], - eval_points=config["eval_points"], + learner = LocalLearner( + policy_dict, agent2policy, env, config["num_episodes"], policy_update_schedule, + exploration_dict=exploration_dict, + agent2exploration=agent2exploration, + experience_update_interval=config["experience_update_interval"], + eval_schedule=config["eval_schedule"], log_env_metrics=config["log_env_metrics"] ) learner.run() diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 19bba3fdd..7938c1c27 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -6,7 +6,7 @@ get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.experience import AbsSampler, ExperienceMemory, ExperienceSet, Replay, UniformSampler, get_sampler_cls +from maro.rl.experience import AbsExperienceManager, ExperienceSet, Replay, UniformSampler, UseAndDispose from maro.rl.exploration import ( AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, UniformNoiseExploration @@ -15,8 +15,11 @@ AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, PolicyNetForDiscreteActionSpace, PolicyValueNetForContinuousActionSpace, PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace ) -from maro.rl.policy import AbsCorePolicy, AbsFixedPolicy, MultiAgentPolicy, NullPolicy, RLPolicy, TrainingLoopConfig -from maro.rl.training import Actor, ActorManager, Learner +from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy +from maro.rl.training import ( + Actor, ActorManager, DistributedLearner, EpisodeBasedSchedule, LocalLearner, MultiPolicyUpdateSchedule, + StepBasedSchedule +) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward @@ -26,14 +29,15 @@ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", "AbsEnvWrapper", - "AbsSampler", "ExperienceMemory", "ExperienceSet", "Replay", "UniformSampler", "get_sampler_cls", + "AbsExperienceManager", "ExperienceSet", "Replay", "UniformSampler", "UseAndDispose", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", "UniformNoiseExploration", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "PolicyNetForDiscreteActionSpace", "PolicyValueNetForContinuousActionSpace", "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace", - "AbsCorePolicy", "AbsFixedPolicy", "MultiAgentPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig", - "Actor", "ActorManager", "Learner", + "AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy", + "Actor", "ActorManager", "DistributedLearner", "EpisodeBasedSchedule", "LocalLearner", "MultiPolicyUpdateSchedule", + "StepBasedSchedule", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/agent/__init__.py b/maro/rl/agent/__init__.py deleted file mode 100644 index 534c6ad26..000000000 --- a/maro/rl/agent/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .ac import ActorCritic, ActorCriticConfig -from .agent import AbsAgent, AgentGroup, GenericAgentConfig -from .agent_cls_index import AGENT_CLS, AGENT_CONFIG -from .agent_manager import AgentManager -from .ddpg import DDPG, DDPGConfig -from .dqn import DQN, DQNConfig -from .experience_enum import Experience -from .pg import PolicyGradient, PolicyGradientConfig -from .torch_loss_cls_index import TORCH_LOSS_CLS - -__all__ = [ - "ActorCritic", "ActorCriticConfig", - "AbsAgent", "AgentGroup", "GenericAgentConfig", - "AGENT_CLS", - "AgentManager", - "DDPG", "DDPGConfig", - "DQN", "DQNConfig", - "Experience", - "PolicyGradient", "PolicyGradientConfig", - "TORCH_LOSS_CLS" -] diff --git a/maro/rl/agent/ac.py b/maro/rl/agent/ac.py deleted file mode 100644 index 827e22535..000000000 --- a/maro/rl/agent/ac.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Tuple - -import numpy as np -import torch -from torch.distributions import Categorical - -from maro.rl.cls_index import TORCH_LOSS_CLS -from maro.rl.model import SimpleMultiHeadModel -from maro.rl.storage import SimpleStore -from maro.rl.utils import get_cls, get_log_prob -from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask - -from .agent import AbsAgent, GenericAgentConfig -from .experience_enum import Experience - - -class ActorCriticConfig: - """Configuration for the Actor-Critic algorithm. - - Args: - reward_discount (float): Reward decay as defined in standard RL terminology. - critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing - the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - train_iters (int): Number of gradient descent steps per call to ``train``. - actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., - loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. - clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, - in which case the actor loss is calculated using the usual policy gradient theorem. - """ - __slots__ = ["reward_discount", "critic_loss_func", "train_iters", "actor_loss_coefficient", "clip_ratio"] - - def __init__( - self, - reward_discount: float, - train_iters: int, - critic_loss_cls="mse", - actor_loss_coefficient: float = 1.0, - clip_ratio: float = None - ): - self.reward_discount = reward_discount - self.critic_loss_func = get_cls(critic_loss_cls, TORCH_LOSS_CLS)() - self.train_iters = train_iters - self.actor_loss_coefficient = actor_loss_coefficient - self.clip_ratio = clip_ratio - - -class ActorCritic(AbsAgent): - """Actor Critic algorithm with separate policy and value models. - - References: - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f - - Args: - model (AbsCoreModel): Task model or container of task models required by the algorithm. - algorithm_config: Algorithm-specific configuration. - generic_config (GenericAgentConfig): Non-algorithm-specific configuration. - experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be - created at init time. Defaults to None. - """ - def __init__( - self, - model: SimpleMultiHeadModel, - algorithm_config, - generic_config: GenericAgentConfig, - experience_memory: SimpleStore = None - ): - if model.task_names is None or set(model.task_names) != {"actor", "critic"}: - raise UnrecognizedTask(f"Expected model task names 'actor' and 'critic', but got {model.task_names}") - super().__init__(model, algorithm_config, generic_config, experience_memory=experience_memory) - - def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """Use the actor (policy) model to generate stochastic actions. - - Args: - state: Input to the actor model. - - Returns: - Actions and corresponding log probabilities. - """ - state = torch.from_numpy(state).to(self.device) - is_single = len(state.shape) == 1 - if is_single: - state = state.unsqueeze(dim=0) - - action_prob = Categorical(self.model(state, task_name="actor", training=False)) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - action, log_p = action.cpu().numpy(), log_p.cpu().numpy() - return (action[0], log_p[0]) if is_single else (action, log_p) - - def step(self): - batch = self.experience_memory.get() - states = torch.from_numpy(np.asarray(batch[Experience.STATE])).to(self.device) - actions = torch.from_numpy(np.asarray([act[0] for act in batch[Experience.ACTION]])).to(self.device) - log_p = torch.from_numpy(np.asarray([act[1] for act in batch[Experience.ACTION]])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch[Experience.REWARD])).to(self.device) - next_states = torch.from_numpy(np.asarray(batch[Experience.NEXT_STATE])).to(self.device) - - state_values = self.model(states, task_name="critic").detach().squeeze() - next_state_values = self.model(next_states, task_name="critic").detach().squeeze() - return_est = rewards + self.algorithm_config.reward_discount * next_state_values - advantages = return_est - state_values - for i in range(self.algorithm_config.train_iters): - # actor loss - log_p_new = get_log_prob(self.model(states, task_name="actor"), actions) - if self.algorithm_config.clip_ratio is not None: - ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp( - ratio, 1 - self.algorithm_config.clip_ratio, 1 + self.algorithm_config.clip_ratio - ) - actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() - else: - actor_loss = -(log_p_new * advantages).mean() - - # critic_loss - state_values = self.model(states, task_name="critic").squeeze() - critic_loss = self.algorithm_config.critic_loss_func(state_values, return_est) - loss = critic_loss + self.algorithm_config.actor_loss_coefficient * actor_loss - - self.model.step(loss) diff --git a/maro/rl/agent/agent.py b/maro/rl/agent/agent.py deleted file mode 100644 index 1755144fd..000000000 --- a/maro/rl/agent/agent.py +++ /dev/null @@ -1,263 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from typing import Dict, Union - -import torch - -from maro.rl.model import AbsCoreModel -from maro.rl.storage import SimpleStore - -from .experience_enum import Experience - - -class GenericAgentConfig: - """Generic agent settings. - - Args: - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. - min_new_experiences_to_trigger_learning (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_learning (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. - """ - __slots__ = [ - "experience_memory_size", "experience_memory_overwrite_type", "empty_experience_memory_after_step", - "min_new_experiences_to_trigger_learning", "min_experiences_to_trigger_learning" - ] - - def __init__( - self, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool, - min_new_experiences_to_trigger_learning: int = 1, - min_experiences_to_trigger_learning: int = 1 - ): - self.experience_memory_size = experience_memory_size - self.experience_memory_overwrite_type = experience_memory_overwrite_type - self.empty_experience_memory_after_step = empty_experience_memory_after_step - self.min_new_experiences_to_trigger_learning = min_new_experiences_to_trigger_learning - self.min_experiences_to_trigger_learning = min_experiences_to_trigger_learning - - -class AbsAgent(ABC): - """Abstract RL agent class. - - It's a sandbox for the RL algorithm. Scenario-specific details will be excluded. - We focus on the abstraction algorithm development here. Environment observation and decision events will - be converted to a uniform format before calling in. The output will be converted to an environment - executable format before return back to the environment. Its key responsibility is optimizing policy based - on interaction with the environment. - - Args: - model (AbsCoreModel): Task model or container of task models required by the algorithm. - algorithm_config: Algorithm-specific configuration. - generic_config (GenericAgentConfig): Non-algorithm-specific configuration. - experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be - created at init time. Defaults to None. - """ - def __init__( - self, - model: AbsCoreModel, - algorithm_config, - generic_config: GenericAgentConfig, - experience_memory: SimpleStore = None - ): - self.model = model - self.algorithm_config = algorithm_config - self.generic_config = generic_config - if not experience_memory: - self.experience_memory = SimpleStore( - [Experience.STATE, Experience.ACTION, Experience.REWARD, Experience.NEXT_STATE], - capacity=self.generic_config.experience_memory_size, - overwrite_type=self.generic_config.experience_memory_overwrite_type - ) - else: - self.experience_memory = experience_memory - - self.device = torch.device('cpu') - - def to_device(self, device): - self.device = device - self.model = self.model.to(device) - - @abstractmethod - def choose_action(self, state): - """This method uses the underlying model(s) to compute an action from a shaped state. - - Args: - state: A state object shaped by a ``StateShaper`` to conform to the model input format. - - Returns: - The action to be taken given ``state``. It is usually necessary to use an ``ActionShaper`` to convert - this to an environment executable action. - """ - return NotImplementedError - - def set_exploration_params(self, **params): - pass - - def learn(self, experiences: dict) -> bool: - """Store experinces in the experience memory and train the model if necessary.""" - expected_keys = set(self.experience_memory.keys) - if set(experiences.keys()) != set(self.experience_memory.keys): - raise ValueError(f"The keys of experiences must be {expected_keys}") - self.experience_memory.put(experiences) - if ( - len(experiences[Experience.STATE]) >= self.generic_config.min_new_experiences_to_trigger_learning and - len(self.experience_memory) >= self.generic_config.min_experiences_to_trigger_learning - ): - self.step() - if self.generic_config.empty_experience_memory_after_step: - self.experience_memory.clear() - return True - return False - - @abstractmethod - def step(self): - """Algorithm-specific training logic. - - The parameters are data to train the underlying model on. Algorithm-specific loss and optimization - should be reflected here. - """ - return NotImplementedError - - def load_model(self, model): - """Load models from memory.""" - self.model.load_state_dict(model) - - def dump_model(self): - """Return the algorithm's trainable models.""" - return self.model.state_dict() - - def load_model_from_file(self, path: str): - """Load trainable models from disk. - - Load trainable models from the specified directory. The model file is always prefixed with the agent's name. - - Args: - path (str): path to the directory where the models are saved. - """ - self.model.load_state_dict(torch.load(path)) - - def dump_model_to_file(self, path: str): - """Dump the algorithm's trainable models to disk. - - Dump trainable models to the specified directory. The model file is always prefixed with the agent's name. - - Args: - path (str): path to the directory where the models are saved. - """ - torch.save(self.model.state_dict(), path) - - -class AgentGroup: - """Convenience wrapper of a set of agents that share the same underlying model. - - Args: - agent_dict (Union[AbsAgent, dict]): A single agent or a homogeneous set of agents that - share the same underlying model instance. - """ - - def __init__( - self, - agent_names: list, - agent_cls, - model, - algorithm_config, - generic_config: Union[GenericAgentConfig, Dict[str, GenericAgentConfig]], - experience_memory: Union[SimpleStore, Dict[str, SimpleStore]] = None - ): - self._members = agent_names - self._validate_agent_config(algorithm_config) - self._validate_agent_config(generic_config) - self.model = model - - def get_per_agent_obj(obj, agent_name): - return obj[agent_name] if isinstance(obj, dict) else obj - - self.agent_dict = { - name: agent_cls( - self.model, - get_per_agent_obj(algorithm_config, name), - get_per_agent_obj(generic_config, name), - experience_memory=get_per_agent_obj(experience_memory, name) - ) - for name in agent_names - } - - def __getitem__(self, agent_id): - if len(self.agent_dict) == 1: - return self.agent_dict["AGENT"] - else: - return self.agent_dict[agent_id] - - def __len__(self): - return len(self.agent_dict) - - @property - def members(self): - return self._members - - def choose_action(self, state_by_agent: dict): - return {agent_id: self.agent_dict[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} - - def set_exploration_params(self, params): - # Per-agent exploration parameters - if isinstance(params, dict) and params.keys() <= self.agent_dict.keys(): - for agent_id, params in params.items(): - self.agent_dict[agent_id].set_exploration_params(**params) - # Shared exploration parameters for all agents - else: - for agent in self.agent_dict.values(): - agent.set_exploration_params(**params) - - def learn(self, experiences: dict) -> set: - """Store experiences in the agents' experience memory. - - The top-level keys of ``experiences`` will be treated as agent IDs. - """ - return {agent_id for agent_id, exp in experiences.items() if self.agent_dict[agent_id].learn(exp)} - - def step(self): - for agent_id in agent_ids: - self.agent_dict[agent_id].step() - - def load_model(self, model): - """Load models from memory.""" - self.model.load_state_dict(model) - assert all(agent.model.state_dict() == self.model.state_dict() for agent in self.agent_dict.values()) - - def dump_model(self): - """Get agents' underlying models. - - This is usually used in distributed mode where models need to be broadcast to remote roll-out actors. - """ - return self.model.state_dict() - - def load_model_from_file(self, path): - """Load models from disk.""" - self.model.load_state_dict(torch.load(path)) - assert all(agent.model.state_dict() == self.model.state_dict() for agent in self.agent_dict.values()) - - def dump_model_to_file(self, path: str): - """Dump agents' models to disk. - - If the agents don't share models, each agent will use its own name to create a separate file under ``path`` - for dumping. - """ - torch.save(self.model.state_dict(), path) - - def _validate_agent_config(self, agent_config): - if isinstance(agent_config, dict): - expected_agent_names = set(self._members) - agent_names_in_config = set(agent_config.keys()) - if expected_agent_names != agent_names_in_config: - raise ValueError(f"Expected {expected_agent_names} as config keys, got {agent_names_in_config}") diff --git a/maro/rl/agent/agent_cls_index.py b/maro/rl/agent/agent_cls_index.py deleted file mode 100644 index c1e4132cf..000000000 --- a/maro/rl/agent/agent_cls_index.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .ac import ActorCritic, ActorCriticConfig -from .ddpg import DDPG, DDPGConfig -from .dqn import DQN, DQNConfig -from .pg import PolicyGradient, PolicyGradientConfig - - -AGENT_CLS = { - "ac": ActorCritic, - "ddpg": DDPG, - "dqn": DQN, - "pg": PolicyGradient -} - -AGENT_CONFIG = { - "ac": ActorCriticConfig, - "ddpg": DDPGConfig, - "dqn": DQNConfig, - "pg": PolicyGradientConfig -} diff --git a/maro/rl/agent/agent_manager.py b/maro/rl/agent/agent_manager.py deleted file mode 100644 index c9c73e246..000000000 --- a/maro/rl/agent/agent_manager.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -from typing import Dict, Union - -from .agent import AbsAgent, AgentGroup - - -class AgentManager: - def __init__(self, child_agent_manager_dict: Dict[str, Union[AbsAgent, AgentGroup]]): - self.child_agent_manager_dict = child_agent_manager_dict - - def choose_action(self, state_dict: dict): - return {name: self.child_agent_manager_dict[name].choose_action(state) for name, state in state_dict.items()} - - def set_exploration_params(self, param_dict: dict): - if param_dict.keys() <= self.child_agent_manager_dict.keys(): - for name, param in param_dict.items(): - self.child_agent_manager_dict[name].set_exploration_params(param) - else: - for manager in self.child_agent_manager_dict.values(): - if isinstance(manager, AbsAgent): - manager.set_exploration_params(**param_dict) - else: - manager.set_exploration_params(param_dict) - - def learn(self, experience_dict: dict) -> set: - """Store experiences in the agents' experience memory. - - The top-level keys of ``experiences`` will be treated as child agent manager IDs. - """ - return {name: self.child_agent_manager_dict[name].learn(exp) for name, exp in experience_dict.items()} - - def step(self): - for manager in self.child_agent_manager_dict.values(): - manager.step() - - def load_model(self, model_dict: dict): - """Load models from memory.""" - for name, model in model_dict.items(): - self.child_agent_manager_dict[name].load_model(model) - - def dump_model(self): - """Get agents' underlying models. - - This is usually used in distributed mode where models need to be broadcast to remote roll-out actors. - """ - return {name: manager.dump_model() for name, manager in self.child_agent_manager_dict.items()} - - def load_model_from_file(self, dir_path): - """Load models from disk.""" - for name, manager in self.child_agent_manager_dict.items(): - manager.load_model_from_file(os.path.join(dir_path, name)) - - def dump_model_to_file(self, dir_path: str): - """Dump agents' models to disk. - - each agent will use its own name to create a separate file under ``path`` for dumping. - """ - for name, manager in self.child_agent_manager_dict.items(): - sub_dir = os.path.join(dir_path, name) - os.makedirs(sub_dir, exist_ok=True) - manager.dump_model_to_file(sub_dir) diff --git a/maro/rl/agent/ddpg.py b/maro/rl/agent/ddpg.py deleted file mode 100644 index 2b5030463..000000000 --- a/maro/rl/agent/ddpg.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Union - -import numpy as np -import torch - -from maro.rl.cls_index import TORCH_LOSS_CLS -from maro.rl.exploration import NoiseExplorer -from maro.rl.model import SimpleMultiHeadModel -from maro.rl.storage import SimpleStore -from maro.rl.utils import get_cls -from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask - -from .agent import AbsAgent, GenericAgentConfig - - -class DDPGConfig: - """Configuration for the DDPG algorithm. - - Args: - reward_discount (float): Reward decay as defined in standard RL terminology. - target_update_freq (int): Number of training rounds between policy target model updates. - q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for - the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., - loss = q_value_loss + ``policy_loss_coefficient`` * policy_loss. Defaults to 1.0. - soft_update_coefficient (float): Soft update coefficient, e.g., - target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. - Defaults to 1.0. - """ - __slots__ = [ - "reward_discount", "q_value_loss_func", "target_update_freq", "policy_loss_coefficient", - "soft_update_coefficient" - ] - - def __init__( - self, - reward_discount: float, - target_update_freq: int, - q_value_loss_cls="mse", - policy_loss_coefficient: float = 1.0, - soft_update_coefficient: float = 1.0, - ): - self.reward_discount = reward_discount - self.target_update_freq = target_update_freq - self.q_value_loss_func = get_cls(q_value_loss_cls, TORCH_LOSS_CLS)() - self.policy_loss_coefficient = policy_loss_coefficient - self.soft_update_coefficient = soft_update_coefficient - - -class DDPG(AbsAgent): - """The Deep Deterministic Policy Gradient (DDPG) algorithm. - - References: - https://arxiv.org/pdf/1509.02971.pdf - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg - - Args: - model (AbsCoreModel): Task model or container of task models required by the algorithm. - algorithm_config: Algorithm-specific configuration. - generic_config (GenericAgentConfig): Non-algorithm-specific configuration. - experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be - created at init time. Defaults to None. - explorer (NoiseExplorer): An NoiseExplorer instance for generating exploratory actions. Defaults to None. - """ - def __init__( - self, - model: SimpleMultiHeadModel, - algorithm_config, - generic_config: GenericAgentConfig, - experience_memory: SimpleStore = None, - explorer: NoiseExplorer = None - ): - if model.task_names is None or set(model.task_names) != {"policy", "q_value"}: - raise UnrecognizedTask(f"Expected model task names 'policy' and 'q_value', but got {model.task_names}") - super().__init__(model, algorithm_config, generic_config, experience_memory=experience_memory) - self._explorer = explorer - self._target_model = model.copy() if model.trainable else None - self._train_cnt = 0 - - def choose_action(self, state) -> Union[float, np.ndarray]: - state = torch.from_numpy(state).to(self.device) - is_single = len(state.shape) == 1 - if is_single: - state = state.unsqueeze(dim=0) - - action = self.model(state, task_name="policy", training=False).data.cpu().numpy() - action_dim = action.shape[1] - if self._explorer: - action = self._explorer(action) - - if action_dim == 1: - action = action.squeeze(axis=1) - - return action[0] if is_single else action - - def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): - states = torch.from_numpy(states).to(self.device) - actual_actions = torch.from_numpy(actions).to(self.device) - rewards = torch.from_numpy(rewards).to(self.device) - next_states = torch.from_numpy(next_states).to(self.device) - if len(actual_actions.shape) == 1: - actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - - current_q_values = self.model(torch.cat([states, actual_actions], dim=1), task_name="q_value") - current_q_values = current_q_values.squeeze(dim=1) # (N,) - next_actions = self._target_model(states, task_name="policy", training=False) - next_q_values = self._target_model( - torch.cat([next_states, next_actions], dim=1), task_name="q_value", training=False - ).squeeze(1) # (N,) - target_q_values = (rewards + self.algorithm_config.reward_discount * next_q_values).detach() # (N,) - q_value_loss = self.algorithm_config.q_value_loss_func(current_q_values, target_q_values) - actions_from_model = self.model(states, task_name="policy") - policy_loss = -self.model(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() - self.model.step(q_value_loss + self.algorithm_config.policy_loss_coefficient * policy_loss) - self._train_cnt += 1 - if self._train_cnt % self.algorithm_config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.algorithm_config.soft_update_coefficient) diff --git a/maro/rl/agent/dqn.py b/maro/rl/agent/dqn.py deleted file mode 100644 index 09c9bf2dd..000000000 --- a/maro/rl/agent/dqn.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Union - -import numpy as np -import torch - -from maro.rl.cls_index import SAMPLER_CLS, TORCH_LOSS_CLS -from maro.rl.model import SimpleMultiHeadModel -from maro.rl.storage import SimpleStore -from maro.rl.utils import get_cls, get_max, get_td_errors, select_by_actions -from maro.utils.exception.rl_toolkit_exception import UnrecognizedTask - -from .agent import AbsAgent, GenericAgentConfig -from .experience_enum import Experience - - -class DQNConfig: - """Configuration for the DQN algorithm. - - Args: - reward_discount (float): Reward decay as defined in standard RL terminology. - target_update_freq (int): Number of training rounds between target model updates. - train_iters (int): Number of batches to train the model on in each call to ``learn``. - batch_size (int): Experience minibatch size. - sampler_cls: A string indicating the sampler class or a custom sampler class that provides the ``sample`` - interface. Defaults to "uniform". - sampler_params (dict): Parameters for the sampler class. Defaults to None. - epsilon (float): Exploration rate for epsilon-greedy exploration. Defaults to None. - soft_update_coefficient (float): Soft update coefficient, e.g., - target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. - Defaults to 1.0. - double (bool): If True, the next Q values will be computed according to the double DQN algorithm, - i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). - See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. - advantage_type (str): Advantage mode for the dueling architecture. Defaults to None, in which - case it is assumed that the regular Q-value model is used. - loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class. If it is a string, - it must be a key in ``TORCH_LOSS``. Defaults to "mse". - """ - __slots__ = [ - "reward_discount", "target_update_freq", "train_iters", "batch_size", "sampler_cls", "sampler_params", - "epsilon", "soft_update_coefficient", "double", "advantage_type", "loss_func" - ] - - def __init__( - self, - reward_discount: float, - target_update_freq: int, - train_iters: int, - batch_size: int, - sampler_cls="uniform", - sampler_params=None, - epsilon: float = .0, - soft_update_coefficient: float = 0.1, - double: bool = True, - advantage_type: str = None, - loss_cls="mse" - ): - self.reward_discount = reward_discount - self.target_update_freq = target_update_freq - self.train_iters = train_iters - self.batch_size = batch_size - self.sampler_cls = get_cls(sampler_cls, SAMPLER_CLS) - self.sampler_params = sampler_params if sampler_params else {} - self.epsilon = epsilon - self.soft_update_coefficient = soft_update_coefficient - self.double = double - self.advantage_type = advantage_type - self.loss_func = get_cls(loss_cls, TORCH_LOSS_CLS)() - - -class DQN(AbsAgent): - """The Deep-Q-Networks algorithm. - - See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. - - Args: - model (AbsCoreModel): Task model or container of task models required by the algorithm. - algorithm_config: Algorithm-specific configuration. - generic_config (GenericAgentConfig): Non-algorithm-specific configuration. - experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be - created at init time. Defaults to None. - """ - def __init__( - self, - model: SimpleMultiHeadModel, - algorithm_config, - generic_config: GenericAgentConfig, - experience_memory: SimpleStore = None - ): - if (algorithm_config.advantage_type is not None and - (model.task_names is None or set(model.task_names) != {"state_value", "advantage"})): - raise UnrecognizedTask( - f"Expected model task names 'state_value' and 'advantage' since dueling DQN is used, " - f"got {model.task_names}" - ) - super().__init__(model, algorithm_config, generic_config, experience_memory=experience_memory) - self._sampler = self.algorithm_config.sampler_cls( - self.experience_memory, **self.algorithm_config.sampler_params - ) - self._training_counter = 0 - self._target_model = model.copy() if model.trainable else None - - def choose_action(self, state: np.ndarray) -> Union[int, np.ndarray]: - state = torch.from_numpy(state) - if self.device: - state = state.to(self.device) - is_single = len(state.shape) == 1 - if is_single: - state = state.unsqueeze(dim=0) - - q_values = self._get_q_values(state, training=False) - num_actions = q_values.shape[1] - greedy_action = q_values.argmax(dim=1).data.cpu() - # No exploration - if self.algorithm_config.epsilon == .0: - return greedy_action.item() if is_single else greedy_action.numpy() - - if is_single: - if np.random.random() > self.algorithm_config.epsilon: - return greedy_action - else: - return np.random.choice(num_actions) - - # batch inference - return np.array([ - act if np.random.random() > self.algorithm_config.epsilon else np.random.choice(num_actions) - for act in greedy_action - ]) - - def step(self): - for _ in range(self.algorithm_config.train_iters): - # sample from the replay memory - indexes, batch = self._sampler.sample(self.algorithm_config.batch_size) - states = torch.from_numpy(np.asarray(batch[Experience.STATE])).to(self.device) - actions = torch.from_numpy(np.asarray(batch[Experience.ACTION])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch[Experience.REWARD])).to(self.device) - next_states = torch.from_numpy(np.asarray(batch[Experience.NEXT_STATE])).to(self.device) - - q_all = self._get_q_values(states) - q = select_by_actions(q_all, actions) - next_q_all_target = self._get_q_values(next_states, is_eval=False, training=False) - if self.algorithm_config.double: - next_q_all_eval = self._get_q_values(next_states, training=False) - next_q = select_by_actions(next_q_all_target, next_q_all_eval.max(dim=1)[1]) # (N,) - else: - next_q, _ = get_max(next_q_all_target) # (N,) - - loss = get_td_errors( - q, next_q, rewards, self.algorithm_config.reward_discount, loss_func=self.algorithm_config.loss_func - ) - self.model.step(loss.mean()) - self._training_counter += 1 - if self._training_counter % self.algorithm_config.target_update_freq == 0: - self._target_model.soft_update(self.model, self.algorithm_config.soft_update_coefficient) - - # update auxillary info for the next round of sampling - self._sampler.update(indexes, loss.detach().numpy()) - - def set_exploration_params(self, epsilon): - self.algorithm_config.epsilon = epsilon - - def _get_q_values(self, states: torch.Tensor, is_eval: bool = True, training: bool = True): - output = self.model(states, training=training) if is_eval else self._target_model(states, training=False) - if self.algorithm_config.advantage_type is None: - return output - else: - state_values = output["state_value"] - advantages = output["advantage"] - # Use mean or max correction to address the identifiability issue - if self.algorithm_config.advantage_type == "mean": - corrections = advantages.mean(1) - else: - corrections = advantages.max(1)[0] - return state_values + advantages - corrections.unsqueeze(1) diff --git a/maro/rl/agent/experience_enum.py b/maro/rl/agent/experience_enum.py deleted file mode 100644 index 6c5bd2ced..000000000 --- a/maro/rl/agent/experience_enum.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from enum import Enum - -class Experience(Enum): - STATE = "STATE" - ACTION = "ACTION" - REWARD = "REWARD" - NEXT_STATE = "NEXT_STATE" diff --git a/maro/rl/agent/pg.py b/maro/rl/agent/pg.py deleted file mode 100644 index 0967dda6a..000000000 --- a/maro/rl/agent/pg.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Tuple - -import numpy as np -import torch -from torch.distributions import Categorical - -from maro.rl.model import SimpleMultiHeadModel -from maro.rl.storage import SimpleStore -from maro.rl.utils import get_truncated_cumulative_reward - -from .agent import AbsAgent, GenericAgentConfig - - -class PolicyGradientConfig: - """Configuration for the Policy Gradient algorithm. - - Args: - reward_discount (float): Reward decay as defined in standard RL terminology. - """ - __slots__ = ["reward_discount"] - - def __init__(self, reward_discount: float): - self.reward_discount = reward_discount - - -class PolicyGradient(AbsAgent): - """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. - - Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - - Args: - model (AbsCoreModel): Task model or container of task models required by the algorithm. - algorithm_config: Algorithm-specific configuration. - generic_config (GenericAgentConfig): Non-algorithm-specific configuration. - experience_memory (SimpleStore): Experience memory for the agent. If None, an experience memory will be - created at init time. Defaults to None. - """ - def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """Use the actor (policy) model to generate stochastic actions. - - Args: - state: Input to the actor model. - - Returns: - Actions and corresponding log probabilities. - """ - state = torch.from_numpy(state).to(self.device) - is_single = len(state.shape) == 1 - if is_single: - state = state.unsqueeze(dim=0) - - action_prob = Categorical(self.model(state, training=False)) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - action, log_p = action.cpu().numpy(), log_p.cpu().numpy() - return (action[0], log_p[0]) if is_single else (action, log_p) - - def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): - states = torch.from_numpy(states).to(self.device) - actions = torch.from_numpy(actions).to(self.device) - returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) - returns = torch.from_numpy(returns).to(self.device) - action_distributions = self.model(states) - action_prob = action_distributions.gather(1, actions.unsqueeze(1)).squeeze() # (N, 1) - loss = -(torch.log(action_prob) * returns).mean() - self.model.step(loss) diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index a46191cfd..5d40cb04f 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -1,16 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import namedtuple from typing import Tuple import numpy as np import torch -from torch.distributions import Categorical -from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.experience import AbsExperienceManager from maro.rl.model import PolicyValueNetForDiscreteActionSpace -from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -19,6 +17,8 @@ class ActorCriticConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. + train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. + gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., @@ -26,16 +26,22 @@ class ActorCriticConfig: clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. """ - __slots__ = ["reward_discount", "critic_loss_func", "actor_loss_coefficient", "clip_ratio"] + __slots__ = [ + "reward_discount", "train_epochs", "gradient_iters", "critic_loss_func", "actor_loss_coefficient", "clip_ratio" + ] def __init__( self, reward_discount: float, + train_epochs: int = 1, + gradient_iters: int = 1, critic_loss_cls="mse", actor_loss_coefficient: float = 1.0, clip_ratio: float = None ): self.reward_discount = reward_discount + self.train_epochs = train_epochs + self.gradient_iters = gradient_iters self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.actor_loss_coefficient = actor_loss_coefficient self.clip_ratio = clip_ratio @@ -56,14 +62,13 @@ class ActorCritic(AbsCorePolicy): def __init__( self, ac_net: PolicyValueNetForDiscreteActionSpace, - experience_memory: ExperienceMemory, - generic_config: TrainingLoopConfig, - special_config: ActorCriticConfig + experience_manager: AbsExperienceManager, + config: ActorCriticConfig ): if not isinstance(ac_net, PolicyValueNetForDiscreteActionSpace): raise TypeError("model must be an instance of 'PolicyValueNetForDiscreteActionSpace'") - super().__init__(experience_memory, generic_config, special_config) + super().__init__(experience_manager, config) self.ac_net = ac_net def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: @@ -80,37 +85,39 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def learn(self, experience_set: ExperienceSet): - if not isinstance(experience_set, ExperienceSet): - raise TypeError(f"Expected experience object of type ACExperience, got {type(experience_set)}") - - states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])) - log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)) - - state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() - next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() - return_est = rewards + self.special_config.reward_discount * next_state_values - advantages = return_est - state_values - # actor loss - action_prob, state_values = self.ac_net(states) - log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - if self.special_config.clip_ratio is not None: - ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.special_config.clip_ratio, 1 + self.special_config.clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() - else: - actor_loss = -(log_p_new * advantages).mean() - - # critic_loss - critic_loss = self.special_config.critic_loss_func(state_values, return_est) - loss = critic_loss + self.special_config.actor_loss_coefficient * actor_loss - - self.ac_net.step(loss) - - def load_state(self, policy_state): + def update(self): + self.ac_net.train() + for _ in range(self.config.train_epochs): + experience_set = self.experience_manager.get() + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.ac_net.device) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.ac_net.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.ac_net.device) + + for _ in range(self.config.gradient_iters): + state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() + next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() + return_est = rewards + self.config.reward_discount * next_state_values + advantages = return_est - state_values + + # actor loss + action_probs, state_values = self.ac_net(states) + log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) + if self.config.clip_ratio is not None: + ratio = torch.exp(log_p_new - log_p) + clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() + else: + actor_loss = -(log_p_new * advantages).mean() + + # critic_loss + critic_loss = self.config.critic_loss_func(state_values, return_est) + loss = critic_loss + self.config.actor_loss_coefficient * actor_loss + + self.ac_net.step(loss) + + def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) - def state(self): - return self.q_net.state_dict() \ No newline at end of file + def get_state(self): + return self.q_net.state_dict() diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index cfa2276c9..a000b6ce6 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -7,9 +7,9 @@ import numpy as np import torch -from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.experience import AbsExperienceManager from maro.rl.model import PolicyValueNetForContinuousActionSpace -from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -19,6 +19,8 @@ class DDPGConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. target_update_freq (int): Number of training rounds between policy target model updates. + train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. + gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., @@ -28,20 +30,24 @@ class DDPGConfig: Defaults to 1.0. """ __slots__ = [ - "reward_discount", "q_value_loss_func", "target_update_freq", "policy_loss_coefficient", - "soft_update_coefficient" + "reward_discount", "target_update_freq", "train_epochs", "gradient_iters", "q_value_loss_func", + "policy_loss_coefficient", "soft_update_coefficient" ] def __init__( self, reward_discount: float, target_update_freq: int, + train_epochs: int = 1, + gradient_iters: int = 1, q_value_loss_cls="mse", policy_loss_coefficient: float = 1.0, soft_update_coefficient: float = 1.0, ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq + self.train_epochs = train_epochs + self.gradient_iters = gradient_iters self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() self.policy_loss_coefficient = policy_loss_coefficient self.soft_update_coefficient = soft_update_coefficient @@ -61,14 +67,13 @@ class DDPG(AbsCorePolicy): def __init__( self, ac_net: PolicyValueNetForContinuousActionSpace, - experience_memory: ExperienceMemory, - generic_config: TrainingLoopConfig, - special_config: DDPGConfig + experience_manager: AbsExperienceManager, + config: DDPGConfig ): if not isinstance(ac_net, PolicyValueNetForContinuousActionSpace): raise TypeError("model must be an instance of 'PolicyValueNetForContinuousActionSpace'") - super().__init__(experience_memory, generic_config, special_config) + super().__init__(experience_manager, config) self.ac_net = ac_net self.target_ac_net = ac_net.copy() if self.ac_net.trainable else None self._train_cnt = 0 @@ -79,34 +84,33 @@ def choose_action(self, states) -> Union[float, np.ndarray]: return actions[0] if len(actions) == 1 else actions - def learn(self, experience_set: ExperienceSet): - if not isinstance(experience_set, ExperienceSet): - raise TypeError(f"Expected experience object of type DDPGExperience, got {type(experience_set)}") - - states, next_states = experience_set.states, experience_set.next_states - actual_actions = torch.from_numpy(experience_set.actions) - rewards = torch.from_numpy(experience_set.rewards) - if len(actual_actions.shape) == 1: - actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - - current_q_values = self.ac_net(torch.cat([states, actual_actions], dim=1), task_name="q_value") - current_q_values = current_q_values.squeeze(dim=1) # (N,) - next_actions = self.target_ac_net(states, task_name="policy", training=False) - next_q_values = self.target_ac_net( - torch.cat([next_states, next_actions], dim=1), task_name="q_value", training=False - ).squeeze(1) # (N,) - target_q_values = (rewards + self.special_config.reward_discount * next_q_values).detach() # (N,) - q_value_loss = self.special_config.q_value_loss_func(current_q_values, target_q_values) - actions_from_model = self.ac_net(states, task_name="policy") - policy_loss = -self.ac_net(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() - self.ac_net.step(q_value_loss + self.special_config.policy_loss_coefficient * policy_loss) - self._train_cnt += 1 - if self._train_cnt % self.special_config.target_update_freq == 0: - self.target_ac_net.soft_update(self.ac_net, self.special_config.soft_update_coefficient) - - def load_state(self, policy_state): + def update(self): + self.ac_net.train() + for _ in range(self.config.train_epochs): + experience_set = self.experience_manager.get() + states, next_states = experience_set.states, experience_set.next_states + actual_actions = torch.from_numpy(experience_set.actions).to(self.ac_net.device) + rewards = torch.from_numpy(experience_set.rewards).to(self.ac_net.device) + if len(actual_actions.shape) == 1: + actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) + + with torch.no_grad(): + next_q_values = self.target_ac_net.value(next_states) + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + for _ in range(self.config.gradient_iters): + q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) + q_value_loss = self.config.q_value_loss_func(q_values, target_q_values) + policy_loss = -self.ac_net.value(states).mean() + loss = q_value_loss + self.config.policy_loss_coefficient * policy_loss + self.ac_net.step(loss) + self._train_cnt += 1 + if self._train_cnt % self.config.target_update_freq == 0: + self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coefficient) + + def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None - def state(self): + def get_state(self): return self.ac_net.state_dict() diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index 3cf0a67c6..5d6f55268 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -1,15 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import namedtuple from typing import Union import numpy as np import torch -from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.experience import AbsExperienceManager from maro.rl.model import QNetForDiscreteActionSpace -from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -19,6 +18,8 @@ class DQNConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. target_update_freq (int): Number of training rounds between target model updates. + train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. + gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. soft_update_coefficient (float): Soft update coefficient, e.g., target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. Defaults to 1.0. @@ -29,20 +30,24 @@ class DQNConfig: it must be a key in ``TORCH_LOSS``. Defaults to "mse". """ __slots__ = [ - "reward_discount", "target_update_freq", "train_iters", "batch_size", "sampler_cls", "sampler_params", - "soft_update_coefficient", "double", "loss_func" + "reward_discount", "target_update_freq", "train_epochs", "gradient_iters", "soft_update_coefficient", + "double", "loss_func" ] def __init__( self, reward_discount: float, target_update_freq: int, + train_epochs: int = 1, + gradient_iters: int = 1, soft_update_coefficient: float = 0.1, double: bool = True, loss_cls="mse" ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq + self.train_epochs = train_epochs + self.gradient_iters = gradient_iters self.soft_update_coefficient = soft_update_coefficient self.double = double self.loss_func = get_torch_loss_cls(loss_cls)() @@ -60,14 +65,13 @@ class DQN(AbsCorePolicy): def __init__( self, q_net: QNetForDiscreteActionSpace, - experience_memory: ExperienceMemory, - generic_config: TrainingLoopConfig, - special_config: DQNConfig, + experience_manager: AbsExperienceManager, + config: DQNConfig ): if not isinstance(q_net, QNetForDiscreteActionSpace): raise TypeError("model must be an instance of 'QNetForDiscreteActionSpace'") - super().__init__(experience_memory, generic_config, special_config) + super().__init__(experience_manager, config) self.q_net = q_net self.target_q_net = q_net.copy() if q_net.trainable else None self.target_q_net.eval() @@ -81,39 +85,46 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = actions.cpu().numpy() return actions[0] if len(actions) == 1 else actions - def learn(self, experience_set: ExperienceSet): - if not isinstance(experience_set, ExperienceSet): - raise TypeError( - f"Expected experience object of type AbsCorePolicy.experience_type, got {type(experience_set)}" - ) - - self.q_net.train() - # sample from the replay memory - states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray(experience_set.actions)) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)) - q_values = self.q_net.q_values(states, actions) - # get next Q values - with torch.no_grad(): - if self.special_config.double: - next_q_values = self.target_q_net.q_values(next_states, self.q_net.choose_action(next_states)[0]) + def update(self): + self.q_net.train() + for _ in range(self.config.train_epochs): + # sample from the replay memory + experience_set = self.experience_manager.get() + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.q_net.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.q_net.device) + if self.config.double: + for _ in range(self.config.gradient_iters): + # get target Q values + with torch.no_grad(): + actions_by_eval_q_net = self.q_net.choose_action(next_states)[0] + next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + # gradient steps + q_values = self.q_net.q_values(states, actions) + loss = self.config.loss_func(q_values, target_q_values) + self.q_net.step(loss.mean()) else: - next_q_values = self.target_q_net.choose_action(next_states)[1] # (N,) - - # get TD errors - target_q_values = (rewards + self.special_config.reward_discount * next_q_values).detach() # (N,) - loss = self.special_config.loss_func(q_values, target_q_values) - - # train and update target if necessary - self.q_net.step(loss.mean()) - self._training_counter += 1 - if self._training_counter % self.special_config.target_update_freq == 0: - self.target_q_net.soft_update(self.q_net, self.special_config.soft_update_coefficient) - - def load_state(self, policy_state): + # get target Q values + with torch.no_grad(): + next_q_values = self.target_q_net.choose_action(next_states)[1] # (N,) + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + # gradient steps + for _ in range(self.config.gradient_iters): + q_values = self.q_net.q_values(states, actions) + loss = self.config.loss_func(q_values, target_q_values) + self.q_net.step(loss.mean()) + + self._training_counter += 1 + if self._training_counter % self.config.target_update_freq == 0: + self.target_q_net.soft_update(self.q_net, self.config.soft_update_coefficient) + + def set_state(self, policy_state): self.q_net.load_state_dict(policy_state) self.target_q_net = self.q_net.copy() if self.q_net.trainable else None self.target_q_net.eval() - def state(self): + def get_state(self): return self.q_net.state_dict() diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index 3d01e7db3..b49a3966c 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -84,11 +84,14 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: return (action[0], log_p[0]) if is_single else (action, log_p) def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): - states = torch.from_numpy(states).to(self.device) - actions = torch.from_numpy(actions).to(self.device) + if not isinstance(experience_set, ExperienceSet): + raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") + + states = experience_set.states + actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)) returns = get_truncated_cumulative_reward(rewards, self.special_config.reward_discount) returns = torch.from_numpy(returns).to(self.device) - action_distributions = self.model(states) - action_prob = action_distributions.gather(1, actions.unsqueeze(1)).squeeze() # (N, 1) - loss = -(torch.log(action_prob) * returns).mean() + loss = -(log_p * returns).mean() self.model.step(loss) diff --git a/maro/rl/distributed/__init__.py b/maro/rl/distributed/__init__.py deleted file mode 100644 index a1513dd4a..000000000 --- a/maro/rl/distributed/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .actor import Actor -from .actor_manager import ActorManager -from .learner import DistLearner - -__all__ = ["Actor", "ActorManager", "DistLearner"] diff --git a/maro/rl/distributed/actor.py b/maro/rl/distributed/actor.py deleted file mode 100644 index 51db7ec4f..000000000 --- a/maro/rl/distributed/actor.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from os import getcwd -from typing import Union - -from maro.communication import Proxy -from maro.rl.agent import AbsAgent, AgentManager -from maro.rl.training import AbsEnvWrapper -from maro.utils import Logger - -from .message_enums import MsgKey, MsgTag - - -class Actor(object): - """On-demand roll-out executor. - - Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific - processing logic and stores transitions during roll-outs in a replay memory. - agent (Union[AbsAgent, AgentManager]): Agent that interacts with the environment. - group (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the learner (and decision clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. - """ - def __init__( - self, - env: AbsEnvWrapper, - agent: Union[AbsAgent, AgentManager], - group: str, - proxy_options: dict = None, - pull_experiences_with_copy: bool = False, - log_dir: str = getcwd() - ): - self.env = env - self.agent = AgentManager(agent) if isinstance(agent, AbsAgent) else agent - self._pull_experiences_with_copy = pull_experiences_with_copy - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) - self._logger = Logger(self._proxy.name, dump_folder=log_dir) - - def run(self): - for msg in self._proxy.receive(): - if msg.tag == MsgTag.EXIT: - self._logger.info("Exiting...") - break - if msg.tag == MsgTag.ROLLOUT: - rollout_index, segment_index = msg.body[MsgKey.ROLLOUT_INDEX], msg.body[MsgKey.SEGMENT_INDEX] - if self.env.state is None: - self.env.reset() - # Load exploration parameters - if MsgKey.EXPLORATION_PARAMS in msg.body: - self.agent.set_exploration_params(msg.body[MsgKey.EXPLORATION_PARAMS]) - self.env.start(rollout_index=rollout_index) # get initial state - - starting_step_index = self.env.step_index - self.agent.load_model(msg.body[MsgKey.MODEL]) - steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] - while self.env.state and steps_to_go > 0: - action = self.agent.choose_action(self.env.state) - self.env.step(action) - steps_to_go -= 1 - - self._logger.info( - f"Roll-out finished for ep {rollout_index}, segment {segment_index}" - f"(steps {starting_step_index} - {self.env.step_index})" - ) - experiences, num_exp = self.env.pull_experiences(copy=self._pull_experiences_with_copy) - return_info = { - MsgKey.ENV_END: not self.env.state, - MsgKey.ROLLOUT_INDEX: rollout_index, - MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.EXPERIENCES: experiences, - MsgKey.NUM_STEPS: self.env.step_index - starting_step_index, - MsgKey.NUM_EXPERIENCES: num_exp - } - if msg.body[MsgKey.RETURN_ENV_METRICS]: - return_info[MsgKey.METRICS] = self.env.metrics - if not self.env.state: - return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward - self._proxy.reply(msg, tag=MsgTag.ROLLOUT_DONE, body=return_info) diff --git a/maro/rl/distributed/actor_manager.py b/maro/rl/distributed/actor_manager.py deleted file mode 100644 index f5f11fd7d..000000000 --- a/maro/rl/distributed/actor_manager.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from os import getcwd -from typing import Union - -from maro.communication import Proxy, SessionType -from maro.utils import Logger - -from .message_enums import MsgTag, MsgKey - - -class ActorManager(object): - """Learner class for distributed training. - - Args: - num_actors (int): Expected number of actors in the group identified by ``group_name``. - group_name (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the actors (and roll-out clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. - update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger - learner updates, i.e., model training. - """ - def __init__( - self, - num_actors: int, - group_name: str, - proxy_options: dict = None, - log_env_metrics: bool = False, - log_dir: str = getcwd() - ): - super().__init__() - peers = {"actor": num_actors} - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_options) - self._actors = self._proxy.peers["actor"] # remote actor ID's - self.total_experiences_collected = 0 - self.total_env_steps = 0 - self.total_reward = defaultdict(float) - self._log_env_metrics = log_env_metrics - self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) - - def collect( - self, - rollout_index: int, - segment_index: int, - num_steps: int, - models: dict = None, - exploration_params=None, - required_actor_finishes: int = None, - discard_stale_experiences: bool = True, - return_env_metrics: bool = False - ): - """Collect experiences from actors.""" - if required_actor_finishes is None: - required_actor_finishes = len(self._actors) - - msg_body = { - MsgKey.ROLLOUT_INDEX: rollout_index, - MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.NUM_STEPS: num_steps, - MsgKey.MODEL: models, - MsgKey.RETURN_ENV_METRICS: return_env_metrics - } - - if exploration_params: - msg_body[MsgKey.EXPLORATION_PARAMS] = exploration_params - - if self._log_env_metrics: - self._logger.info(f"EPISODE-{rollout_index}, SEGMENT-{segment_index}: ") - if exploration_params: - self._logger.info(f"exploration_params: {exploration_params}") - - self._proxy.ibroadcast("actor", MsgTag.ROLLOUT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent roll-out requests for ep-{rollout_index}, segment-{segment_index}") - - # Receive roll-out results from remote actors - num_finishes = 0 - for msg in self._proxy.receive(): - if msg.body[MsgKey.ROLLOUT_INDEX] != rollout_index: - self._logger.info( - f"Ignore a message of type {msg.tag} with ep {msg.body[MsgKey.ROLLOUT_INDEX]} " - f"(expected {rollout_index})" - ) - continue - - # log roll-out summary - if self._log_env_metrics: - env_metrics = msg.body[MsgKey.METRICS] - self._logger.info(f"env_metrics: {env_metrics}") - - if msg.body[MsgKey.SEGMENT_INDEX] == segment_index or not discard_stale_experiences: - self.total_experiences_collected += msg.body[MsgKey.NUM_EXPERIENCES] - self.total_env_steps += msg.body[MsgKey.NUM_STEPS] - is_env_end = msg.body[MsgKey.ENV_END] - if is_env_end: - self._logger.info(f"total rewards: {msg.body[MsgKey.TOTAL_REWARD]}") - yield msg.body[MsgKey.EXPERIENCES], is_env_end - - if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: - num_finishes += 1 - if num_finishes == required_actor_finishes: - break - - def exit(self): - """Tell the remote actors to exit.""" - self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) - self._logger.info("Exiting...") diff --git a/maro/rl/distributed/dispatcher.py b/maro/rl/distributed/dispatcher.py deleted file mode 100755 index b574e51ac..000000000 --- a/maro/rl/distributed/dispatcher.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import deque - -import zmq -from zmq.eventloop.ioloop import IOLoop -from zmq.eventloop.zmqstream import ZMQStream - - -class LRUQueue(object): - """LRUQueue class using ZMQStream/IOLoop for event dispatching. - - Code adapted from https://zguide.zeromq.org/docs/chapter3/#A-High-Level-API-for-ZeroMQ. - """ - def __init__(self): - self._context = zmq.Context.instance() - frontend = self._context.socket(zmq.ROUTER) - frontend.bind("tcp://127.0.0.1:50000") - backend = self._context.socket(zmq.ROUTER) - backend.bind("tcp://127.0.0.1:50001") - self._workers = deque() - - self._frontend = ZMQStream(frontend) - self._backend = ZMQStream(backend) - self._backend.on_recv(self._handle_backend) - - def _handle_backend(self, msg): - # Queue worker ID for LRU routing - worker, empty, client = msg[:3] - # add worker back to the list of workers - self._workers.append(worker) - assert empty == b"" - # Third frame is READY or else a client reply address - # If client reply, send rest back to frontend - if client != b"READY": - empty, reply = msg[3:] - assert empty == b"" - self._frontend.send_multipart([client, b'', reply]) - - # Start accepting frontend messages now that at least one worker is free. - self._frontend.on_recv(self._handle_frontend) - - def _handle_frontend(self, msg): - # Now get next client request, route to LRU worker - # Client request is [address][empty][request] - client, empty, request = msg - assert empty == b"" - # Dequeue and drop the next worker address - self._backend.send_multipart([self._workers.popleft(), b'', client, b'', request]) - if not self._workers: - # stop receiving until workers become available again - self._frontend.stop_on_recv() - - -def start_dispatcher(): - dispatcher = LRUQueue() - IOLoop.instance().start() diff --git a/maro/rl/distributed/learner.py b/maro/rl/distributed/learner.py deleted file mode 100644 index 375f3d858..000000000 --- a/maro/rl/distributed/learner.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from collections import defaultdict -from os import getcwd -from typing import Union - -from maro.communication import Message, Proxy, SessionType -from maro.rl.agent import AbsAgent, AgentManager -from maro.rl.scheduling import Scheduler -from maro.utils import Logger - -from .actor_manager import ActorManager -from .message_enums import MsgTag, MsgKey - - -class DistLearner(object): - """Learner class for distributed training. - - Args: - agent (Union[AbsAgent, AgentManager]): Learning agents. - scheduler (Scheduler): A ``Scheduler`` instance for generating exploration parameters. - """ - def __init__( - self, - agent: Union[AbsAgent, AgentManager], - scheduler: Scheduler, - actor_manager: ActorManager, - agent_update_interval: int = -1, - required_actor_finishes: str = None, - discard_stale_experiences: bool = True, - log_dir: str = getcwd() - ): - super().__init__() - self.agent = AgentManager(agent) if isinstance(agent, AbsAgent) else agent - self.scheduler = scheduler - self.actor_manager = actor_manager - self.agent_update_interval = agent_update_interval - self.required_actor_finishes = required_actor_finishes - self.discard_stale_experiences = discard_stale_experiences - self._total_learning_time = 0 - self._logger = Logger("LEARNER", dump_folder=log_dir) - - def run(self): - """Main learning loop.""" - t0 = time.time() - for exploration_params in self.scheduler: - updated_agents, num_actor_finishes, segment_index = self.agent.names, 0, 0 - while num_actor_finishes < self.required_actor_finishes: - for exp, done in self.actor_manager.collect( - self.scheduler.iter, - segment_index, - self.agent_update_interval, - models=self.agent.dump_model(agent_ids=updated_agents), - exploration_params=exploration_params if segment_index == 0 else None, - required_actor_finishes=self.required_actor_finishes, - discard_stale_experiences=self.discard_stale_experiences - ): - tl0 = time.time() - updated_agents = self.agent.learn(exp) - num_actor_finishes += done - self._total_learning_time += time.time() - tl0 - self._logger.debug(f"total running time: {time.time() - t0}") - self._logger.debug(f"total learning time: {self._total_learning_time}") - self._logger.debug(f"total env steps: {self.actor_manager.total_env_steps}") - self._logger.info(f"total experiences collected: {self.actor_manager.total_experiences_collected}") - - segment_index += 1 - - self.actor_manager.exit() diff --git a/maro/rl/distributed/message_enums.py b/maro/rl/distributed/message_enums.py deleted file mode 100644 index cf60aa879..000000000 --- a/maro/rl/distributed/message_enums.py +++ /dev/null @@ -1,33 +0,0 @@ -from enum import Enum - - -class MsgTag(Enum): - ROLLOUT = "rollout" - AGENT_UPDATE = "agent_update" - CHOOSE_ACTION = "choose_action" - ACTION = "action" - EXPERIENCE_SYNC = "experience_sync" - TRAIN = "train" - ABORT_ROLLOUT = "abort_rollout" - ROLLOUT_DONE = "rollout_done" - EXIT = "exit" - - -class MsgKey(Enum): - ACTION = "action" - AGENT_ID = "agent_id" - ROLLOUT_INDEX = "rollout_index" - TIME_STEP = "time_step" - METRICS = "metrics" - EXPERIENCES = "experiences" - NUM_EXPERIENCES = "num_experiences" - STATE = "state" - TRAINING = "training" - MODEL = "model" - VERSION = "version" - EXPLORATION_PARAMS = "exploration_params" - NUM_STEPS = "num_steps" - SEGMENT_INDEX = "segment_index" - RETURN_ENV_METRICS = "return_env_metrics" - TOTAL_REWARD = "total_reward" - ENV_END = "env_end" diff --git a/maro/rl/distributed/trainer.py b/maro/rl/distributed/trainer.py deleted file mode 100755 index c3ee0aa01..000000000 --- a/maro/rl/distributed/trainer.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import pickle -from collections import deque - -import zmq -from zmq.eventloop.ioloop import IOLoop -from zmq.eventloop.zmqstream import ZMQStream - - -def trainer(id_: str): - socket = zmq.Context().socket(zmq.REQ) - socket.setsockopt_string(zmq.IDENTITY, f"Trainer_{id_}") - socket.connect("tcp://127.0.0.1:50001") - socket = ZMQStream(socket) - socket.send(b"READY") # register to the dispatcher - - def train(sock, msg): - client, _, request = msg - request = pickle.loads(request) - info = request["agent"].step(*request["args"], **request["kwargs"]) - request.update({"model": request["agent"].dump_model(), "info": info}) - del request["agent"] - del request["args"] - del request["kwargs"] - sock.send_multipart([client, b"", pickle.dumps(request)]) - - socket.on_recv_stream(train) - IOLoop.instance().start() diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 10b26517b..447195134 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -1,8 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .sampler import AbsSampler, UniformSampler -from .sampler_cls_index import get_sampler_cls -from .experience_memory import ExperienceMemory, ExperienceSet, Replay +from .experience import ExperienceSet, Replay +from .experience_manager import AbsExperienceManager, UniformSampler, UseAndDispose -__all__ = ["AbsSampler", "ExperienceMemory", "ExperienceSet", "Replay", "UniformSampler", "get_sampler_cls"] +__all__ = ["AbsExperienceManager", "ExperienceSet", "Replay", "UniformSampler", "UseAndDispose"] diff --git a/maro/rl/experience/experience.py b/maro/rl/experience/experience.py new file mode 100644 index 000000000..ee58c2721 --- /dev/null +++ b/maro/rl/experience/experience.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.utils.exception.rl_toolkit_exception import InvalidExperience + + +class ExperienceSet: + + __slots__ = ["states", "actions", "rewards", "next_states"] + + def __init__(self, states: list, actions: list, rewards: list, next_states: list): + if not len(states) == len(actions) == len(rewards) == len(next_states): + raise InvalidExperience("values of contents should consist of lists of the same length") + self.states = states + self.actions = actions + self.rewards = rewards + self.next_states = next_states + + def __len__(self): + return len(self.states) + + +class Replay(object): + def __init__(self): + self.states = [] + self.actions = [] + self.rewards = [] + + def to_experience_set(self): + # print(len(self.rewards), len(self.states)) + num_complete = min(len(self.rewards), len(self.states) - 1) + exp_set = ExperienceSet( + self.states[:num_complete], + self.actions[:num_complete], + self.rewards[:num_complete], + self.states[1:num_complete + 1] + ) + + del self.states[:num_complete] + del self.actions[:num_complete] + del self.rewards[:num_complete] + + return exp_set diff --git a/maro/rl/experience/experience_manager.py b/maro/rl/experience/experience_manager.py new file mode 100644 index 000000000..b1326ea1c --- /dev/null +++ b/maro/rl/experience/experience_manager.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod + +import numpy as np + +from .experience import ExperienceSet + + +class AbsExperienceManager(ABC): + """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". + + This implementation uses a dictionary of lists as the internal data structure. The objects for each key + are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during + put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited + and limited storage are supported. + + Args: + capacity (int): Maximum number of experiences that can be stored. + overwrite_type (str): If storage capacity is bounded, this specifies how existing entries + are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: + - "rolling", where overwrite occurs sequentially with wrap-around. + - "random", where overwrite occurs randomly among filled positions. + Alternatively, the user may also specify overwrite positions (see ``put``). + """ + def __init__(self, capacity: int, overwrite_type: str = "rolling"): + super().__init__() + if overwrite_type not in {"rolling", "random"}: + raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") + self._capacity = capacity + self._overwrite_type = overwrite_type + self.keys = ExperienceSet.__slots__ + self.data = {key: [None] * self._capacity for key in self.keys} + self._size = 0 + + @property + def size(self): + return self._size + + @property + def capacity(self): + return self._capacity + + @property + def overwrite_type(self): + return self._overwrite_type + + @abstractmethod + def get(self) -> ExperienceSet: + raise NotImplementedError + + def put(self, experience_set: ExperienceSet): + """Put new contents in the store. + + Args: + contents (dict): Dictionary of items to add to the store. If the store is not empty, this must have the + same keys as the store itself. Otherwise an ``StoreMisalignment`` will be raised. + + Returns: + The indexes where the newly added entries reside in the store. + """ + added_size = len(experience_set) + if added_size > self._capacity: + raise ValueError("size of added items should not exceed the capacity.") + + num_experiences = self._size + added_size + num_overwrites = num_experiences - self._capacity + if num_overwrites <= 0: + indexes = list(range(self._size, num_experiences)) + # follow the overwrite rule set at init + elif self._overwrite_type == "rolling": + # using the negative index convention for convenience + start_index = self._size - self._capacity + indexes = list(range(start_index, start_index + added_size)) + else: + random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) + indexes = list(range(self._size, self._capacity)) + list(random_indexes) + + for key in self.data: + for idx, val in zip(indexes, getattr(experience_set, key)): + self.data[key][idx] = val + + self._size = min(self._capacity, num_experiences) + + def clear(self): + """Empty the store.""" + self.data = {key: [None] * self._capacity for key in self.keys} + self._size = 0 + + +class UniformSampler(AbsExperienceManager): + """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". + + This implementation uses a dictionary of lists as the internal data structure. The objects for each key + are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during + put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited + and limited storage are supported. + + Args: + capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. + overwrite_type (str): If storage capacity is bounded, this specifies how existing entries + are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: + - "rolling", where overwrite occurs sequentially with wrap-around. + - "random", where overwrite occurs randomly among filled positions. + Alternatively, the user may also specify overwrite positions (see ``put``). + """ + def __init__(self, capacity: int, batch_size: int, overwrite_type: str = None, replace: bool = True): + super().__init__(capacity, overwrite_type=overwrite_type) + self.batch_size = batch_size + self.replace = replace + + def get(self) -> ExperienceSet: + indexes = np.random.choice(self._size, size=self.batch_size, replace=self.replace) + return ExperienceSet(*[[self.data[key][idx] for idx in indexes] for key in self.keys]) + + +class UseAndDispose(AbsExperienceManager): + def get(self) -> ExperienceSet: + exp_set = ExperienceSet(*[self.data[key] for key in self.keys]) + self.clear() + return exp_set diff --git a/maro/rl/experience/experience_memory.py b/maro/rl/experience/experience_memory.py deleted file mode 100644 index d82aa77c8..000000000 --- a/maro/rl/experience/experience_memory.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from enum import Enum -from typing import Callable, Dict, List, Tuple, Union - -import numpy as np - -from maro.utils import clone -from maro.utils.exception.rl_toolkit_exception import InvalidExperience - - -class ExperienceSet: - - __slots__ = ["states", "actions", "rewards", "next_states"] - - def __init__(self, states: list, actions: list, rewards: list, next_states: list): - if not len(states) == len(actions) == len(rewards) == len(next_states): - raise InvalidExperience("values of contents should consist of lists of the same length") - - self.states = states - self.actions = actions - self.rewards = rewards - self.next_states = next_states - - def __len__(self): - return len(self.states) - - -class Replay(object): - def __init__(self): - self.states = [] - self.actions = [] - self.rewards = [] - - def to_experience_set(self): - # print(len(self.rewards), len(self.states)) - num_complete = min(len(self.rewards), len(self.states) - 1) - exp_set = ExperienceSet( - self.states[:num_complete], - self.actions[:num_complete], - self.rewards[:num_complete], - self.states[1:num_complete + 1] - ) - - del self.states[:num_complete] - del self.actions[:num_complete] - del self.rewards[:num_complete] - - return exp_set - - -class ExperienceMemory(object): - """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". - - This implementation uses a dictionary of lists as the internal data structure. The objects for each key - are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during - put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited - and limited storage are supported. - - Args: - capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. - overwrite_type (str): If storage capacity is bounded, this specifies how existing entries - are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - - "rolling", where overwrite occurs sequentially with wrap-around. - - "random", where overwrite occurs randomly among filled positions. - Alternatively, the user may also specify overwrite positions (see ``put``). - """ - def __init__(self, capacity: int = -1, overwrite_type: str = None): - super().__init__() - if overwrite_type not in {"rolling", "random"}: - raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") - self._capacity = capacity - self._overwrite_type = overwrite_type - self._keys = ExperienceSet.__slots__ - self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self._keys} - self._size = 0 - - def __len__(self): - return self._size - - def __getitem__(self, index: int): - return {k: lst[index] for k, lst in self.data.items()} - - @property - def capacity(self): - """Store capacity. - - If negative, the store grows without bound. Otherwise, the number of items in the store will not exceed - this capacity. - """ - return self._capacity - - @property - def overwrite_type(self): - """An string indicating the overwrite behavior when the store capacity is exceeded.""" - return self._overwrite_type - - def get(self, indexes: [int] = None) -> ExperienceSet: - if indexes is None: - return ExperienceSet(*[self.data[k] for k in self._keys]) - - return ExperienceSet(*[[self.data[k][i] for i in indexes] for k in self._keys]) - - def put(self, experience_set: ExperienceSet, overwrite_indexes: list = None) -> List[int]: - """Put new contents in the store. - - Args: - contents (dict): Dictionary of items to add to the store. If the store is not empty, this must have the - same keys as the store itself. Otherwise an ``StoreMisalignment`` will be raised. - overwrite_indexes (list, optional): Indexes where the contents are to be overwritten. This is only - used when the store has a fixed capacity and putting ``contents`` in the store would exceed this - capacity. If this is None and overwriting is necessary, rolling or random overwriting will be done - according to the ``overwrite`` property. Defaults to None. - Returns: - The indexes where the newly added entries reside in the store. - """ - added_size = len(experience_set) - if self._capacity == -1: - for key in self.data: - self.data[key].extend(getattr(experience_set, key)) - self._size += added_size - return list(range(self._size - added_size, self._size)) - else: - write_indexes = self._get_update_indexes(added_size, overwrite_indexes=overwrite_indexes) - for key in self.data: - for index, value in zip(write_indexes, getattr(experience_set, key)): - self.data[key][index] = value - - self._size = min(self._capacity, self._size + added_size) - return write_indexes - - def clear(self): - """Empty the store.""" - self.data = { - key: [] if self._capacity == -1 else [None] * self._capacity for key in ExperienceSet.__slots__ - } - self._size = 0 - - def dumps(self): - """Return a deep copy of store contents.""" - return clone(dict(self.data)) - - def get_by_key(self, key): - """Get the contents of the store corresponding to ``key``.""" - return self.data[key] - - def _get_update_indexes(self, added_size: int, overwrite_indexes=None): - if added_size > self._capacity: - raise ValueError("size of added items should not exceed the store capacity.") - - num_overwrites = self._size + added_size - self._capacity - if num_overwrites < 0: - return list(range(self._size, self._size + added_size)) - - if overwrite_indexes is not None: - write_indexes = list(range(self._size, self._capacity)) + list(overwrite_indexes) - else: - # follow the overwrite rule set at init - if self._overwrite_type == "rolling": - # using the negative index convention for convenience - start_index = self._size - self._capacity - write_indexes = list(range(start_index, start_index + added_size)) - else: - random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) - write_indexes = list(range(self._size, self._capacity)) + list(random_indexes) - - return write_indexes diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py deleted file mode 100644 index 1777f0471..000000000 --- a/maro/rl/experience/sampler.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from typing import List - -import numpy as np - -from .experience_memory import ExperienceMemory - - -class AbsSampler(ABC): - def __init__(self, data: ExperienceMemory, batch_size: int): - self.data = data - self.batch_size = batch_size - - @abstractmethod - def sample(self) -> List[int]: - raise NotImplementedError - - @abstractmethod - def update(self): - """Update statistics used for sampling.""" - pass - - -class UniformSampler(AbsSampler): - """ - Obtain a random sample from the experience pool. - - Args: - size (int): Sample sizes for each round of sampling in the chain. If this is a single integer, it is - used as the sample size for all samplers in the chain. - weights (Union[list, np.ndarray]): Sampling weights. - replace (bool): If True, sampling is performed with replacement. Defaults to True. - Returns: - Sampled indexes and the corresponding objects, - e.g., [1, 2, 3], ['a', 'b', 'c']. - """ - def __init__(self, data: ExperienceMemory, batch_size: int, replace: bool = True): - super().__init__(data, batch_size) - self.replace = replace - - def sample(self): - indexes = np.random.choice(len(self.data), size=self.batch_size, replace=self.replace) - return indexes - - def update(self, indexes, values): - pass diff --git a/maro/rl/experience/sampler_cls_index.py b/maro/rl/experience/sampler_cls_index.py deleted file mode 100644 index 0154a05fa..000000000 --- a/maro/rl/experience/sampler_cls_index.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .sampler import UniformSampler - -SAMPLER = { - "uniform": UniformSampler, -} - -def get_sampler_cls(sampler_type): - if isinstance(sampler_type, str): - if sampler_type not in SAMPLER: - raise KeyError(f"A string sampler_type must be one of {list(SAMPLER.keys())}.") - return SAMPLER[sampler_type] - - return sampler_type diff --git a/maro/rl/exploration/abs_exploration.py b/maro/rl/exploration/abs_exploration.py index 28f9f4c5c..bfcc10285 100644 --- a/maro/rl/exploration/abs_exploration.py +++ b/maro/rl/exploration/abs_exploration.py @@ -18,10 +18,8 @@ def register_schedule( param_name: str, last_ep: int, initial_value=None, - kwargs: dict = None + **kwargs ): - if kwargs is None: - kwargs = {} self.scheduler[param_name] = scheduler_cls(self, param_name, last_ep, initial_value=initial_value, **kwargs) @abstractmethod diff --git a/maro/rl/exploration/abs_explorer.py b/maro/rl/exploration/abs_explorer.py deleted file mode 100644 index 40558b263..000000000 --- a/maro/rl/exploration/abs_explorer.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - - -class AbsExplorer(ABC): - """Abstract explorer class for generating exploration rates. - - """ - def __init__(self): - pass - - @abstractmethod - def set_parameters(self, **exploration_params): - return NotImplementedError - - @abstractmethod - def __call__(self, action): - return NotImplementedError diff --git a/maro/rl/exploration/epsilon_greedy_explorer.py b/maro/rl/exploration/epsilon_greedy_explorer.py deleted file mode 100644 index 5c9463140..000000000 --- a/maro/rl/exploration/epsilon_greedy_explorer.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Union - -import numpy as np - -from .abs_explorer import AbsExplorer - - -class EpsilonGreedyExplorer(AbsExplorer): - """Epsilon greedy explorer for discrete action spaces. - - Args: - num_actions (int): Number of all possible actions. - """ - def __init__(self, num_actions: int, epsilon: float = .0): - super().__init__() - self._num_actions = num_actions - self._epsilon = epsilon - - def __call__(self, action_index: Union[int, np.ndarray]): - if isinstance(action_index, np.ndarray): - return [self._get_exploration_action(act) for act in action_index] - else: - return self._get_exploration_action(action_index) - - def set_parameters(self, *, epsilon: float): - self._epsilon = epsilon - - def _get_exploration_action(self, action_index): - assert (action_index < self._num_actions), f"Invalid action: {action_index}" - return action_index if np.random.random() > self._epsilon else np.random.choice(self._num_actions) diff --git a/maro/rl/exploration/noise_explorer.py b/maro/rl/exploration/noise_explorer.py deleted file mode 100644 index 999994cb4..000000000 --- a/maro/rl/exploration/noise_explorer.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import abstractmethod -from typing import Union - -import numpy as np - -from .abs_explorer import AbsExplorer - - -class NoiseExplorer(AbsExplorer): - """Explorer that adds a random noise to a model-generated action.""" - def __init__( - self, - min_action: Union[float, list, np.ndarray] = None, - max_action: Union[float, list, np.ndarray] = None - ): - if isinstance(min_action, (list, np.ndarray)) and isinstance(max_action, (list, np.ndarray)): - assert len(min_action) == len(max_action), "min_action and max_action should have the same dimension." - super().__init__() - self._min_action = min_action - self._max_action = max_action - - @abstractmethod - def set_parameters(self, **parameters): - raise NotImplementedError - - @abstractmethod - def __call__(self, action) -> np.ndarray: - raise NotImplementedError - - -class UniformNoiseExplorer(NoiseExplorer): - """Explorer that adds a random noise to a model-generated action sampled from a uniform distribution.""" - def __init__( - self, - min_action: Union[float, list, np.ndarray] = None, - max_action: Union[float, list, np.ndarray] = None, - noise_lower_bound: Union[float, list, np.ndarray] = .0, - noise_upper_bound: Union[float, list, np.ndarray] = .0 - ): - if isinstance(noise_upper_bound, (list, np.ndarray)) and isinstance(noise_upper_bound, (list, np.ndarray)): - assert len(noise_lower_bound) == len(noise_upper_bound), \ - "noise_lower_bound and noise_upper_bound should have the same dimension." - super().__init__(min_action, max_action) - self._noise_lower_bound = noise_lower_bound - self._noise_upper_bound = noise_upper_bound - - def set_parameters(self, *, noise_lower_bound, noise_upper_bound): - self._noise_lower_bound = noise_lower_bound - self._noise_upper_bound = noise_upper_bound - - def __call__(self, action: np.ndarray) -> np.ndarray: - return np.array([self._get_exploration_action(act) for act in action]) - - def _get_exploration_action(self, action): - action += np.random.uniform(self._noise_lower_bound, self._noise_upper_bound) - if self._min_action is not None or self._max_action is not None: - return np.clip(action, self._min_action, self._max_action) - else: - return action - - -class GaussianNoiseExplorer(NoiseExplorer): - """Explorer that adds a random noise to a model-generated action sampled from a Gaussian distribution.""" - def __init__( - self, - min_action: Union[float, list, np.ndarray] = None, - max_action: Union[float, list, np.ndarray] = None, - noise_mean: Union[float, list, np.ndarray] = .0, - noise_stddev: Union[float, list, np.ndarray] = .0, - is_relative: bool = False - ): - if isinstance(noise_mean, (list, np.ndarray)) and isinstance(noise_stddev, (list, np.ndarray)): - assert len(noise_mean) == len(noise_stddev), "noise_mean and noise_stddev should have the same dimension." - if is_relative and noise_mean != .0: - raise ValueError("Standard deviation cannot be relative if noise mean is non-zero.") - super().__init__(min_action, max_action) - self._noise_mean = noise_mean - self._noise_stddev = noise_stddev - self._is_relative = is_relative - - def set_parameters(self, *, noise_stddev, noise_mean=.0): - self._noise_stddev = noise_stddev - self._noise_mean = noise_mean - - def __call__(self, action: np.ndarray) -> np.ndarray: - return np.array([self._get_exploration_action(act) for act in action]) - - def _get_exploration_action(self, action): - noise = np.random.normal(loc=self._noise_mean, scale=self._noise_stddev) - action += (noise * action) if self._is_relative else noise - if self._min_action is not None or self._max_action is not None: - return np.clip(action, self._min_action, self._max_action) - else: - return action diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 63de8505b..f516ced9a 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -223,7 +223,7 @@ class PolicyValueNetForContinuousActionSpace(AbsCoreModel): the components. Defaults to None. """ @abstractmethod - def forward(self, states, actions, output_action: bool = True, output_values: bool = True): + def forward(self, states, actions=None): raise NotImplementedError def choose_action(self, states): @@ -231,4 +231,7 @@ def choose_action(self, states): Given Q-values for a batch of states and all actions, return the maximum Q-value and the corresponding action index for each state. """ - return self.forward(states, output_values=False) + return self.forward(states) + + def value(self, states): + return self.forward(states, actions=self.forward(states)) diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index dde452fac..77e89d44b 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .multi_agent_policy import MultiAgentPolicy -from .policy import AbsCorePolicy, AbsFixedPolicy, NullPolicy, RLPolicy, TrainingLoopConfig +from .policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy -__all__ = ["AbsCorePolicy", "AbsFixedPolicy", "MultiAgentPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig"] +__all__ = ["AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy"] diff --git a/maro/rl/policy/multi_agent_policy.py b/maro/rl/policy/multi_agent_policy.py deleted file mode 100644 index f69367d5c..000000000 --- a/maro/rl/policy/multi_agent_policy.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import pickle -import warnings -from collections import defaultdict, namedtuple -from typing import Dict, List, Union - -from maro.rl.exploration import AbsExploration, NullExploration -from maro.rl.experience import ExperienceMemory - -from .policy import AbsFixedPolicy, AbsCorePolicy - - -class MultiAgentPolicy: - """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. - - Args: - - """ - def __init__( - self, - policy_dict: Dict[str, Union[AbsFixedPolicy, AbsCorePolicy]], - agent_to_policy: Dict[str, str], - exploration_dict: Dict[str, AbsExploration] = None, - agent_to_exploration: Dict[str, str] = None - ): - self.policy_dict = policy_dict - self.agent_to_policy = agent_to_policy - self.policy = { - agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items() - } - self.agent_groups_by_policy = defaultdict(list) - for agent_id, policy_id in agent_to_policy.items(): - self.agent_groups_by_policy[policy_id].append(agent_id) - - for policy_id, agent_ids in self.agent_groups_by_policy.items(): - self.agent_groups_by_policy[policy_id] = tuple(agent_ids) - - self.exploration_dict = exploration_dict - if exploration_dict: - self.agent_to_exploration = agent_to_exploration - self.exploration = { - agent_id: self.exploration_dict[exploration_id] - for agent_id, exploration_id in self.agent_to_exploration.items() - } - self.with_exploration = True - self.agent_groups_by_exploration = defaultdict(list) - for agent_id, exploration_id in agent_to_exploration.items(): - self.agent_groups_by_exploration[exploration_id].append(agent_id) - - for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): - self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) - - def train_mode(self): - self.with_exploration = True - - def eval_mode(self): - self.with_exploration = False - - @property - def exploration_params(self): - if hasattr(self, "exploration"): - return { - agent_ids: self.exploration_dict[exploration_id].parameters - for exploration_id, agent_ids in self.agent_groups_by_exploration.items() - } - - def choose_action(self, state_by_agent: dict): - if self.exploration_dict and self.with_exploration: - return { - agent_id: - self.exploration[agent_id](self.policy[agent_id].choose_action(state)) - if agent_id in self.exploration else self.policy[agent_id].choose_action(state) - for agent_id, state in state_by_agent.items() - } - - return {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} - - def store_experiences(self, experiences_by_agent: dict): - for agent_id, exp in experiences_by_agent.items(): - if isinstance(self.policy[agent_id], AbsCorePolicy): - self.policy[agent_id].store_experiences(exp) - - def update(self) -> List[str]: - return [ - policy_id for policy_id, policy in self.policy_dict.items() - if isinstance(policy, AbsCorePolicy) and policy.update() - ] - - def exploration_step(self): - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() - - def load_state(self, policy_state_dict: dict): - """Load policies from memory.""" - if not policy_state_dict.keys() <= self.policy_dict.keys(): - raise Exception(f"Expected policies from {list(self.policy_state_dict.keys())}") - - for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].load_state(policy_state) - - def state(self): - return { - policy_id: policy.state() for policy_id, policy in self.policy_dict.items() - if isinstance(policy, AbsCorePolicy) - } - - def load(self, dir_path: str): - """Load models from disk.""" - for policy_id, policy in self.policy_dict.items(): - try: - policy.load(os.path.join(dir_path, policy_id)) - except FileNotFoundError: - warnings.warn(f"policy {policy_id} is skipped because no file is found") - - def save(self, dir_path: str): - os.makedirs(dir_path, exist_ok=True) - for policy_id, policy in self.policy_dict.items(): - policy.save(os.path.join(dir_path, policy_id)) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 7f06c47c2..c1ed08af0 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -5,40 +5,17 @@ from collections import namedtuple from maro.rl.exploration import AbsExploration -from maro.rl.experience import ExperienceMemory, ExperienceSet - - -class TrainingLoopConfig: - __slots__ = [ - "sampler_cls", "batch_size", "train_iters", "sampler_kwargs", "new_experience_trigger", - "num_warmup_experiences" - ] - - def __init__( - self, - sampler_cls, - batch_size: int, - train_iters: int, - sampler_kwargs: dict = None, - new_experience_trigger: int = 1, - num_warmup_experiences: int = 1 - ): - self.sampler_cls = sampler_cls - self.batch_size = batch_size - self.train_iters = train_iters - self.sampler_kwargs = sampler_kwargs if sampler_kwargs else {} - self.new_experience_trigger = new_experience_trigger - self.num_warmup_experiences = num_warmup_experiences - - -class AbsFixedPolicy(ABC): +from maro.rl.experience import AbsExperienceManager, ExperienceSet + + +class AbsPolicy(ABC): """Abstract fixed policy class. Args: config: Settings for the algorithm. """ def __init__(self): - pass + super().__init__() @abstractmethod def choose_action(self, state): @@ -54,21 +31,16 @@ def choose_action(self, state): raise NotImplementedError -class NullPolicy(AbsFixedPolicy): +class NullPolicy(AbsPolicy): def choose_action(self, state): return None -class AbsCorePolicy(ABC): - def __init__(self, experience_memory: ExperienceMemory, generic_config: TrainingLoopConfig, special_config): - self.experience_memory = experience_memory - self.generic_config = generic_config - self.special_config = special_config - sampler_cls, batch_size = generic_config.sampler_cls, generic_config.batch_size - self.sampler = sampler_cls(experience_memory, batch_size, **generic_config.sampler_kwargs) - self._num_new_exp = 0 # experience memory size when the last update was made - self._warm_up = True - self._update_ready = False +class AbsCorePolicy(AbsPolicy): + def __init__(self, experience_manager: AbsExperienceManager, config): + super().__init__() + self.experience_manager = experience_manager + self.config = config @abstractmethod def choose_action(self, state): @@ -83,30 +55,16 @@ def choose_action(self, state): """ raise NotImplementedError - def store_experiences(self, experience_set: ExperienceSet): - self.experience_memory.put(experience_set) - self._num_new_exp += len(experience_set) - self._warm_up = len(self.experience_memory) < self.generic_config.num_warmup_experiences - self._update_ready = self._num_new_exp >= self.generic_config.new_experience_trigger - - def update(self): - if self._warm_up or not self._update_ready: - return False - - self._num_new_exp = 0 - for _ in range(self.generic_config.train_iters): - self.learn(self.experience_memory.get(self.sampler.sample())) - - return True - @abstractmethod - def learn(self, experience_set: ExperienceSet): + def update(self): raise NotImplementedError - def state(self): + @abstractmethod + def get_state(self): pass - def load_state(self, policy_state): + @abstractmethod + def set_state(self, policy_state): pass def load(self, path: str): @@ -150,8 +108,8 @@ def update(self): def learn(self, experience_set: ExperienceSet): return self.core_policy.learn(experience_set) - def load_state(self, policy_state): - self.core_policy.load_state(policy_state) + def set_state(self, policy_state): + self.core_policy.set_state(policy_state) def load(self, path: str): self.core_policy.load(path) diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index e369990d8..1acaff841 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -3,6 +3,14 @@ from .actor import Actor from .actor_manager import ActorManager -from .learner import Learner +from .distributed_learner import DistributedLearner +from .local_learner import LocalLearner +from .policy_update_schedule import EpisodeBasedSchedule, MultiPolicyUpdateSchedule, StepBasedSchedule -__all__ = ["Actor", "ActorManager", "Learner"] +__all__ = [ + "Actor", + "ActorManager", + "DistributedLearner", + "LocalLearner", + "EpisodeBasedSchedule", "MultiPolicyUpdateSchedule", "StepBasedSchedule" +] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 863cd824a..74767768d 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -1,12 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import defaultdict from os import getcwd -from typing import Union +from typing import Dict from maro.communication import Proxy -from maro.rl.policy import MultiAgentPolicy from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.exploration import AbsExploration +from maro.rl.policy import AbsPolicy from maro.utils import Logger from .message_enums import MsgKey, MsgTag @@ -21,24 +23,49 @@ class Actor(object): policy (MultiAgentPolicy): Agent that interacts with the environment. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. """ def __init__( self, env: AbsEnvWrapper, - policy: MultiAgentPolicy, + policy_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str], group: str, + exploration_dict: Dict[str, AbsExploration] = None, + agent2exploration: Dict[str, str] = None, eval_env: AbsEnvWrapper = None, - proxy_options: dict = None, - log_dir: str = getcwd() + log_dir: str = getcwd(), + **proxy_kwargs ): self.env = env self.eval_env = eval_env if eval_env else self.env - self.policy = policy - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) + + # mappings between agents and policies + self.policy_dict = policy_dict + self.agent2policy = agent2policy + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent2policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + # mappings between exploration schemes and agents + self.exploration_dict = exploration_dict + if exploration_dict: + self.agent2exploration = agent2exploration + self.exploration = { + agent_id: self.exploration_dict[exploration_id] + for agent_id, exploration_id in self.agent2exploration.items() + } + self.exploration_enabled = True + self.agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in agent2exploration.items(): + self.agent_groups_by_exploration[exploration_id].append(agent_id) + + for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): + self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) + + self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_kwargs) self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): @@ -48,19 +75,33 @@ def run(self): break if msg.tag == MsgTag.COLLECT: - self.policy.train_mode() episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] if self.env.state is None: self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") - self._logger.debug(f"Exploration parameters: {self.policy.exploration_params}") + if hasattr(self, "exploration_dict"): + exploration_params = { + agent_ids: self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self.agent_groups_by_exploration.items() + } + self._logger.debug(f"Exploration parameters: {exploration_params}") + self.env.reset() self.env.start() # get initial state + self._load_policy(msg.body[MsgKey.POLICY]) starting_step_index = self.env.step_index + 1 - self.policy.load_state(msg.body[MsgKey.POLICY]) steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while self.env.state and steps_to_go > 0: - action = self.policy.choose_action(self.env.state) + if self.exploration_dict: + action = { + id_: + self.exploration[id_](self.policy[id_].choose_action(st)) + if id_ in self.exploration else self.policy[id_].choose_action(st) + for id_, st in self.env.state.items() + } + else: + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.env.state.items()} + self.env.step(action) steps_to_go -= 1 @@ -79,18 +120,21 @@ def run(self): if msg.body[MsgKey.RETURN_ENV_METRICS]: return_info[MsgKey.METRICS] = self.env.metrics if not self.env.state: - self.policy.exploration_step() + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) elif msg.tag == MsgTag.EVAL: ep = msg.body[MsgKey.EPISODE_INDEX] self._logger.info(f"Evaluation episode {ep}") - self.policy.eval_mode() self.eval_env.reset() self.eval_env.start() # get initial state - self.policy.load_state(msg.body[MsgKey.POLICY]) + self._load_policy(msg.body[MsgKey.POLICY]) while self.eval_env.state: - self.eval_env.step(self.policy.choose_action(self.eval_env.state)) + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} + self.eval_env.step(action) return_info = { MsgKey.METRICS: self.env.metrics, @@ -98,3 +142,7 @@ def run(self): MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] } self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) + + def _load_policy(self, policy_state_dict): + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].set_state(policy_state) diff --git a/maro/rl/training/actor_manager.py b/maro/rl/training/actor_manager.py index d21f509c2..6ce7d0913 100644 --- a/maro/rl/training/actor_manager.py +++ b/maro/rl/training/actor_manager.py @@ -4,7 +4,6 @@ from collections import defaultdict from os import getcwd from random import choices -from typing import Union from maro.communication import Proxy, SessionType from maro.utils import Logger @@ -21,7 +20,7 @@ class ActorManager(object): num_actors (int): Expected number of actors in the group identified by ``group_name``. group_name (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the actors (and roll-out clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger learner updates, i.e., model training. @@ -30,22 +29,30 @@ def __init__( self, num_actors: int, group_name: str, - proxy_options: dict = None, + required_finishes: int = None, log_env_metrics: bool = False, - log_dir: str = getcwd() + log_dir: str = getcwd(), + **proxy_kwargs ): super().__init__() + self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) self.num_actors = num_actors peers = {"actor": num_actors} - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_options) + self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_kwargs) self._actors = self._proxy.peers["actor"] # remote actor ID's + + if required_finishes and required_finishes > self.num_actors: + raise ValueError("required_finishes cannot exceed the number of available actors") + + if required_finishes is None: + required_finishes = self.num_actors + self._logger.info(f"Required number of actor finishes is set to {required_finishes}") + + self.required_finishes = required_finishes self.total_experiences_collected = 0 self.total_env_steps = 0 self.total_reward = defaultdict(float) self._log_env_metrics = log_env_metrics - self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) def collect( self, @@ -53,8 +60,6 @@ def collect( segment_index: int, num_steps: int, policy_dict: dict = None, - exploration=None, - required_actor_finishes: int = None, discard_stale_experiences: bool = True, return_env_metrics: bool = False ): @@ -69,8 +74,6 @@ def collect( if self._log_env_metrics: self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") - if exploration_params: - self._logger.info(f"exploration_params: {exploration_params}") self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") @@ -101,7 +104,7 @@ def collect( if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: num_finishes += 1 - if num_finishes == required_actor_finishes: + if num_finishes == self.required_finishes: break def evaluate(self, episode_index: int, policy_dict: dict, num_actors: int): diff --git a/maro/rl/training/distributed_learner.py b/maro/rl/training/distributed_learner.py new file mode 100644 index 000000000..5b482d8c1 --- /dev/null +++ b/maro/rl/training/distributed_learner.py @@ -0,0 +1,167 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from collections import defaultdict +from os import getcwd +from typing import Dict, List, Union + +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.policy import AbsPolicy, AbsCorePolicy +from maro.utils import Logger + +from .actor_manager import ActorManager +from .policy_update_schedule import MultiPolicyUpdateSchedule + + +class DistributedLearner(object): + """Learner class for distributed training. + + Args: + policy (MultiAgentPolicy): Learning agents. + + """ + def __init__( + self, + policy_dict: Dict[str, AbsPolicy], + agent_to_policy: Dict[str, str], + num_episodes: int, + policy_update_schedule: MultiPolicyUpdateSchedule, + actor_manager: ActorManager, + experience_update_interval: int = -1, + eval_env: AbsEnvWrapper = None, + eval_schedule: Union[int, List[int]] = None, + num_eval_actors: int = 1, + discard_stale_experiences: bool = True, + log_env_metrics: bool = True, + log_dir: str = getcwd(), + **end_of_episode_kwargs + ): + self._logger = Logger("LEARNER", dump_folder=log_dir) + self.policy_dict = policy_dict + self.agent_to_policy = agent_to_policy + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items()} + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent_to_policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + for policy_id, agent_ids in self.agent_groups_by_policy.items(): + self.agent_groups_by_policy[policy_id] = tuple(agent_ids) + + self.num_episodes = num_episodes + self.policy_update_schedule = policy_update_schedule + self.experience_update_interval = experience_update_interval + + # evaluation + self.eval_env = eval_env + self.num_eval_actors = num_eval_actors + + if eval_schedule is None: + eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + + self.eval_schedule = eval_schedule + self.eval_schedule.sort() + if not self.eval_schedule or num_episodes != self.eval_schedule[-1]: + self.eval_schedule.append(num_episodes) + + self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_schedule}") + self._eval_point_index = 0 + + self.actor_manager = actor_manager + self.discard_stale_experiences = discard_stale_experiences + + self.end_of_episode_kwargs = end_of_episode_kwargs + self._log_env_metrics = log_env_metrics + + def run(self): + for ep in range(1, self.num_episodes + 1): + self._train(ep) + + policy_ids = self.policy_update_schedule.pop_episode(ep) + if policy_ids == ["*"]: + policy_ids = list(self.policy_dict.keys()) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + + if policy_ids: + self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") + + self.end_of_episode(ep, **self.end_of_episode_kwargs) + + if ep == self.eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self._evaluate(self._eval_point_index) + + if self.actor_manager: + self.actor_manager.exit() + + def _train(self, ep: int): + t0 = time.time() + learning_time = 0 + env_steps = 0 + num_experiences_collected = 0 + policy_ids, num_actor_finishes, segment_index = list(self.policy_dict.keys()), 0, 1 + + while num_actor_finishes < self.actor_manager.required_finishes: + # parallel experience collection + for exp_by_agent, done in self.actor_manager.collect( + ep, segment_index, self.experience_update_interval, + policy_dict={policy_id: self.policy_dict[policy_id].get_state() for policy_id in policy_ids}, + discard_stale_experiences=self.discard_stale_experiences + ): + for agent_id, exp in exp_by_agent.items(): + if isinstance(self.policy[agent_id], AbsCorePolicy): + self.policy[agent_id].experience_manager.put(exp) + + env_steps += self.experience_update_interval + num_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) + num_actor_finishes += done + + # policy update + tl0 = time.time() + policy_ids = self.policy_update_schedule.pop_step(ep, segment_index) + if policy_ids == ["*"]: + policy_ids = list(self.policy_dict.keys()) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + + if policy_ids: + self._logger.info(f"Updated policies {policy_ids} after segment {segment_index}") + + learning_time += time.time() - tl0 + + segment_index += 1 + + # performance details + self._logger.debug( + f"ep {ep} summary - " + f"running time: {time.time() - t0}" + f"env steps: {env_steps}" + f"learning time: {learning_time}" + f"experiences collected: {num_experiences_collected}" + ) + + def _evaluate(self, ep: int): + self._logger.info("Evaluating...") + if self.eval_env: + self.eval_env.save_replay = False + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} + self.eval_env.step(action) + + if not self.eval_env.state: + self._logger.info(f"total reward: {self.eval_env.total_reward}") + + if self._log_env_metrics: + self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") + else: + policy_state = {policy_id: policy.get_state() for policy_id, policy in self.policy_dict.items()} + self.actor_manager.evaluate(ep, policy_state, self.num_eval_actors) + + def end_of_episode(self, ep: int, **kwargs): + pass diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py new file mode 100644 index 000000000..b5b40cc3a --- /dev/null +++ b/maro/rl/training/local_learner.py @@ -0,0 +1,205 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import heapq +import time +from collections import defaultdict, namedtuple +from os import getcwd +from typing import Callable, Dict, List, Tuple, Union + +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.exploration import AbsExploration +from maro.rl.policy import AbsCorePolicy, AbsPolicy +from maro.utils import Logger + +from .policy_update_schedule import MultiPolicyUpdateSchedule + + +class LocalLearner(object): + """Learner class for distributed training. + + Args: + policy (MultiAgentPolicy): Learning agents. + + """ + def __init__( + self, + policy_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str], + env: AbsEnvWrapper, + num_episodes: int, + policy_update_schedule: MultiPolicyUpdateSchedule, + exploration_dict: Dict[str, AbsExploration] = None, + agent2exploration: Dict[str, str] = None, + experience_update_interval: int = -1, + eval_env: AbsEnvWrapper = None, + eval_schedule: Union[int, List[int]] = None, + log_env_metrics: bool = True, + log_total_reward: bool = True, + log_dir: str = getcwd(), + **end_of_episode_kwargs + ): + self._logger = Logger("LEARNER", dump_folder=log_dir) + + # mappings between agents and policies + self.policy_dict = policy_dict + self.agent2policy = agent2policy + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent2policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + for policy_id, agent_ids in self.agent_groups_by_policy.items(): + self.agent_groups_by_policy[policy_id] = tuple(agent_ids) + + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent2policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + for policy_id, agent_ids in self.agent_groups_by_policy.items(): + self.agent_groups_by_policy[policy_id] = tuple(agent_ids) + + # mappings between exploration schemes and agents + self.exploration_dict = exploration_dict + if exploration_dict: + self.agent2exploration = agent2exploration + self.exploration = { + agent_id: self.exploration_dict[exploration_id] + for agent_id, exploration_id in self.agent2exploration.items() + } + self.exploration_enabled = True + self.agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in agent2exploration.items(): + self.agent_groups_by_exploration[exploration_id].append(agent_id) + + for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): + self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) + + self.env = env + self.num_episodes = num_episodes + self.policy_update_schedule = policy_update_schedule + self.eval_env = eval_env if eval_env else self.env + self.experience_update_interval = experience_update_interval + + # evaluation schedule + if eval_schedule is None: + eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + + self.eval_schedule = eval_schedule + self.eval_schedule.sort() + if not self.eval_schedule or num_episodes != self.eval_schedule[-1]: + self.eval_schedule.append(num_episodes) + + self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_schedule}") + self._eval_point_index = 0 + + self.end_of_episode_kwargs = end_of_episode_kwargs + self._log_env_metrics = log_env_metrics + self._log_total_reward = log_total_reward + + def run(self): + for ep in range(1, self.num_episodes + 1): + self._train(ep) + + policy_ids = self.policy_update_schedule.pop_episode(ep) + if policy_ids == ["*"]: + policy_ids = list(self.policy_dict.keys()) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + + if policy_ids: + self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") + + if ep == self.eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self._evaluate(self._eval_point_index) + + self.end_of_episode(ep, **self.end_of_episode_kwargs) + + def _train(self, ep: int): + t0 = time.time() + learning_time = 0 + num_experiences_collected = 0 + + self._logger.info(f"Training episode {ep}") + if self.exploration_dict: + exploration_params = { + agent_ids: self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self.agent_groups_by_exploration.items() + } + self._logger.debug(f"Exploration parameters: {exploration_params}") + + self.env.save_replay = True + self.env.reset() + self.env.start() # get initial state + while self.env.state: + if self.exploration_dict: + action = { + id_: + self.exploration[id_](self.policy[id_].choose_action(st)) + if id_ in self.exploration else self.policy[id_].choose_action(st) + for id_, st in self.env.state.items() + } + else: + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.env.state.items()} + + self.env.step(action) + step_index = self.env.step_index + + # experience collection + if not self.env.state or step_index % self.experience_update_interval == 0: + exp_by_agent = self.env.get_experiences() + for agent_id, exp in exp_by_agent.items(): + if isinstance(self.policy[agent_id], AbsCorePolicy): + self.policy[agent_id].experience_manager.put(exp) + num_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) + + # policy update + tl0 = time.time() + policy_ids = self.policy_update_schedule.pop_step(ep, step_index) + if policy_ids == ["*"]: + policy_ids = list(self.policy_dict.keys()) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + + if policy_ids: + self._logger.info(f"Updated policies {policy_ids} after step {step_index}") + learning_time += time.time() - tl0 + + # update the exploration parameters + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + + # performance details + if self._log_env_metrics: + self._logger.info(f"ep {ep}: {self.env.metrics}") + if self._log_total_reward: + self._logger.info(f"ep {ep} total reward received: {self.env.total_reward}") + self._logger.debug( + f"ep {ep} summary - " + f"running time: {time.time() - t0}" + f"env steps: {self.env.step_index}" + f"learning time: {learning_time}" + f"experiences collected: {num_experiences_collected}" + ) + + def _evaluate(self, ep: int): + self._logger.info("Evaluating...") + self.eval_env.save_replay = False + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} + self.eval_env.step(action) + + if self._log_env_metrics: + self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") + if not self.eval_env.state: + self._logger.info(f"total reward: {self.eval_env.total_reward}") + + def end_of_episode(self, ep: int, **kwargs): + pass diff --git a/maro/rl/training/policy_update_schedule.py b/maro/rl/training/policy_update_schedule.py new file mode 100644 index 000000000..ef7fdef76 --- /dev/null +++ b/maro/rl/training/policy_update_schedule.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict, namedtuple +from typing import List, Union + +EpisodeBasedSchedule = namedtuple("EpisodeBasedSchedule", ["start", "interval"]) +StepBasedSchedule = namedtuple("StepBasedSchedule", ["start_ep", "interval", "end_ep_update"]) + + +class MultiPolicyUpdateSchedule: + """ + """ + def __init__(self, schedule_option: Union[EpisodeBasedSchedule, StepBasedSchedule, dict] = -1): + self._pending_steps = defaultdict(list) + self._pending_episodes = defaultdict(list) + self._step_schedule_opt = {} + self._episode_schedule_opt = {} + + if isinstance(schedule_option, dict): + for policy_id, sch in schedule_option.items(): + if isinstance(sch, StepBasedSchedule): + self._step_schedule_opt[policy_id] = (sch.start_ep, sch.interval) + if sch.end_ep_update: + self._episode_schedule_opt[policy_id] = (sch.start_ep, 1) + self._pending_episodes[sch.start_ep].append(policy_id) + self._pending_steps[sch.interval].append(policy_id) + elif isinstance(sch, EpisodeBasedSchedule): + self._episode_schedule_opt[policy_id] = (sch.start, sch.interval) + self._pending_episodes[sch.start].append(policy_id) + else: + if isinstance(schedule_option, StepBasedSchedule): + self._step_schedule_opt["*"] = (schedule_option.start_ep, schedule_option.interval) + self._pending_steps[schedule_option.interval].append("*") + if schedule_option.end_ep_update: + self._episode_schedule_opt["*"] = (schedule_option.start_ep, 1) + self._pending_episodes[schedule_option.start_ep].append("*") + else: + self._episode_schedule_opt["*"] = (schedule_option.start, schedule_option.interval) + self._pending_episodes[schedule_option.start].append("*") + + self._episode = None + + def pop_step(self, ep: int, step: int) -> List[str]: + for policy_id in self._pending_steps[step]: + next_step = step + self._step_schedule_opt[policy_id][1] + # if the pending_steps is filled, skip the loop altogether + if self._pending_steps[next_step]: + break + self._pending_steps[next_step].append(policy_id) + + return [ + policy_id for policy_id in self._pending_steps[step] if self._step_schedule_opt[policy_id][0] <= ep + ] + + def pop_episode(self, ep: int) -> List[str]: + for policy_id in self._pending_episodes[ep]: + next_ep = ep + self._episode_schedule_opt[policy_id][1] + self._pending_episodes[next_ep].append(policy_id) + + return self._pending_episodes[ep] From 559ba66bb2c152e306c7187172110dd765eefebd Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 10 May 2021 05:18:22 +0000 Subject: [PATCH 224/482] checked out latest changes from v0.2_sc --- maro/rl/__init__.py | 16 ++- maro/rl/algorithm/ac.py | 89 ++++++++------- maro/rl/algorithm/ddpg.py | 74 +++++++------ maro/rl/algorithm/dqn.py | 87 ++++++++------- maro/rl/experience/__init__.py | 7 +- maro/rl/experience/experience.py | 43 ++++++++ maro/rl/experience/experience_manager.py | 122 +++++++++++++++++++++ maro/rl/model/core_model.py | 7 +- maro/rl/policy/__init__.py | 4 +- maro/rl/policy/policy.py | 47 ++------ maro/rl/training/__init__.py | 10 +- maro/rl/training/actor.py | 84 +++++++++++--- maro/rl/training/actor_manager.py | 16 +-- maro/rl/training/dispatcher.py | 58 ---------- maro/rl/training/distributed_learner.py | 70 ++++++------ maro/rl/training/local_learner.py | 65 +++++------ maro/rl/training/policy_update_schedule.py | 78 +++++++------ maro/rl/training/trainer.py | 30 ----- 18 files changed, 517 insertions(+), 390 deletions(-) create mode 100644 maro/rl/experience/experience.py create mode 100644 maro/rl/experience/experience_manager.py delete mode 100755 maro/rl/training/dispatcher.py delete mode 100755 maro/rl/training/trainer.py diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 26d66a574..7938c1c27 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -6,7 +6,7 @@ get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.experience import AbsSampler, ExperienceMemory, ExperienceSet, Replay, UniformSampler, get_sampler_cls +from maro.rl.experience import AbsExperienceManager, ExperienceSet, Replay, UniformSampler, UseAndDispose from maro.rl.exploration import ( AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, UniformNoiseExploration @@ -15,8 +15,11 @@ AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, PolicyNetForDiscreteActionSpace, PolicyValueNetForContinuousActionSpace, PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace ) -from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy, TrainingLoopConfig -from maro.rl.training import Actor, ActorManager, DistributedLearner, LocalLearner, MultiPolicyUpdateSchedule +from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy +from maro.rl.training import ( + Actor, ActorManager, DistributedLearner, EpisodeBasedSchedule, LocalLearner, MultiPolicyUpdateSchedule, + StepBasedSchedule +) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward @@ -26,14 +29,15 @@ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", "AbsEnvWrapper", - "AbsSampler", "ExperienceMemory", "ExperienceSet", "Replay", "UniformSampler", "get_sampler_cls", + "AbsExperienceManager", "ExperienceSet", "Replay", "UniformSampler", "UseAndDispose", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", "UniformNoiseExploration", "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "PolicyNetForDiscreteActionSpace", "PolicyValueNetForContinuousActionSpace", "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace", - "AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig", - "Actor", "ActorManager", "DistributedLearner", "LocalLearner", "MultiPolicyUpdateSchedule", + "AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy", + "Actor", "ActorManager", "DistributedLearner", "EpisodeBasedSchedule", "LocalLearner", "MultiPolicyUpdateSchedule", + "StepBasedSchedule", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index 6d72d13b8..5d40cb04f 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -1,16 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import namedtuple from typing import Tuple import numpy as np import torch -from torch.distributions import Categorical -from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.experience import AbsExperienceManager from maro.rl.model import PolicyValueNetForDiscreteActionSpace -from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -19,6 +17,8 @@ class ActorCriticConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. + train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. + gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., @@ -26,16 +26,22 @@ class ActorCriticConfig: clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. """ - __slots__ = ["reward_discount", "critic_loss_func", "actor_loss_coefficient", "clip_ratio"] + __slots__ = [ + "reward_discount", "train_epochs", "gradient_iters", "critic_loss_func", "actor_loss_coefficient", "clip_ratio" + ] def __init__( self, reward_discount: float, + train_epochs: int = 1, + gradient_iters: int = 1, critic_loss_cls="mse", actor_loss_coefficient: float = 1.0, clip_ratio: float = None ): self.reward_discount = reward_discount + self.train_epochs = train_epochs + self.gradient_iters = gradient_iters self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.actor_loss_coefficient = actor_loss_coefficient self.clip_ratio = clip_ratio @@ -56,14 +62,13 @@ class ActorCritic(AbsCorePolicy): def __init__( self, ac_net: PolicyValueNetForDiscreteActionSpace, - experience_memory: ExperienceMemory, - generic_config: TrainingLoopConfig, - special_config: ActorCriticConfig + experience_manager: AbsExperienceManager, + config: ActorCriticConfig ): if not isinstance(ac_net, PolicyValueNetForDiscreteActionSpace): raise TypeError("model must be an instance of 'PolicyValueNetForDiscreteActionSpace'") - super().__init__(experience_memory, generic_config, special_config) + super().__init__(experience_manager, config) self.ac_net = ac_net def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: @@ -80,37 +85,39 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def step(self, experience_set: ExperienceSet): - if not isinstance(experience_set, ExperienceSet): - raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") - - states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.ac_net.device) - log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.ac_net.device) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.ac_net.device) - - state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() - next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() - return_est = rewards + self.special_config.reward_discount * next_state_values - advantages = return_est - state_values - # actor loss - action_prob, state_values = self.ac_net(states) - log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - if self.special_config.clip_ratio is not None: - ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.special_config.clip_ratio, 1 + self.special_config.clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() - else: - actor_loss = -(log_p_new * advantages).mean() - - # critic_loss - critic_loss = self.special_config.critic_loss_func(state_values, return_est) - loss = critic_loss + self.special_config.actor_loss_coefficient * actor_loss - - self.ac_net.step(loss) - - def load_state(self, policy_state): + def update(self): + self.ac_net.train() + for _ in range(self.config.train_epochs): + experience_set = self.experience_manager.get() + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.ac_net.device) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.ac_net.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.ac_net.device) + + for _ in range(self.config.gradient_iters): + state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() + next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() + return_est = rewards + self.config.reward_discount * next_state_values + advantages = return_est - state_values + + # actor loss + action_probs, state_values = self.ac_net(states) + log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) + if self.config.clip_ratio is not None: + ratio = torch.exp(log_p_new - log_p) + clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() + else: + actor_loss = -(log_p_new * advantages).mean() + + # critic_loss + critic_loss = self.config.critic_loss_func(state_values, return_est) + loss = critic_loss + self.config.actor_loss_coefficient * actor_loss + + self.ac_net.step(loss) + + def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) - def state(self): - return self.q_net.state_dict() \ No newline at end of file + def get_state(self): + return self.q_net.state_dict() diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index 8f1cbc229..a000b6ce6 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -7,9 +7,9 @@ import numpy as np import torch -from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.experience import AbsExperienceManager from maro.rl.model import PolicyValueNetForContinuousActionSpace -from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -19,6 +19,8 @@ class DDPGConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. target_update_freq (int): Number of training rounds between policy target model updates. + train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. + gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., @@ -28,20 +30,24 @@ class DDPGConfig: Defaults to 1.0. """ __slots__ = [ - "reward_discount", "q_value_loss_func", "target_update_freq", "policy_loss_coefficient", - "soft_update_coefficient" + "reward_discount", "target_update_freq", "train_epochs", "gradient_iters", "q_value_loss_func", + "policy_loss_coefficient", "soft_update_coefficient" ] def __init__( self, reward_discount: float, target_update_freq: int, + train_epochs: int = 1, + gradient_iters: int = 1, q_value_loss_cls="mse", policy_loss_coefficient: float = 1.0, soft_update_coefficient: float = 1.0, ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq + self.train_epochs = train_epochs + self.gradient_iters = gradient_iters self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() self.policy_loss_coefficient = policy_loss_coefficient self.soft_update_coefficient = soft_update_coefficient @@ -61,14 +67,13 @@ class DDPG(AbsCorePolicy): def __init__( self, ac_net: PolicyValueNetForContinuousActionSpace, - experience_memory: ExperienceMemory, - generic_config: TrainingLoopConfig, - special_config: DDPGConfig + experience_manager: AbsExperienceManager, + config: DDPGConfig ): if not isinstance(ac_net, PolicyValueNetForContinuousActionSpace): raise TypeError("model must be an instance of 'PolicyValueNetForContinuousActionSpace'") - super().__init__(experience_memory, generic_config, special_config) + super().__init__(experience_manager, config) self.ac_net = ac_net self.target_ac_net = ac_net.copy() if self.ac_net.trainable else None self._train_cnt = 0 @@ -79,34 +84,33 @@ def choose_action(self, states) -> Union[float, np.ndarray]: return actions[0] if len(actions) == 1 else actions - def step(self, experience_set: ExperienceSet): - if not isinstance(experience_set, ExperienceSet): - raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") - - states, next_states = experience_set.states, experience_set.next_states - actual_actions = torch.from_numpy(experience_set.actions).to(self.ac_net.device) - rewards = torch.from_numpy(experience_set.rewards).to(self.ac_net.device) - if len(actual_actions.shape) == 1: - actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - - current_q_values = self.ac_net(torch.cat([states, actual_actions], dim=1)) - current_q_values = current_q_values.squeeze(dim=1) # (N,) - next_actions = self.target_ac_net(states, task_name="policy", training=False) - next_q_values = self.target_ac_net( - torch.cat([next_states, next_actions], dim=1), task_name="q_value", training=False - ).squeeze(1) # (N,) - target_q_values = (rewards + self.special_config.reward_discount * next_q_values).detach() # (N,) - q_value_loss = self.special_config.q_value_loss_func(current_q_values, target_q_values) - actions_from_model = self.ac_net(states, task_name="policy") - policy_loss = -self.ac_net(torch.cat([states, actions_from_model], dim=1), task_name="q_value").mean() - self.ac_net.step(q_value_loss + self.special_config.policy_loss_coefficient * policy_loss) - self._train_cnt += 1 - if self._train_cnt % self.special_config.target_update_freq == 0: - self.target_ac_net.soft_update(self.ac_net, self.special_config.soft_update_coefficient) - - def load_state(self, policy_state): + def update(self): + self.ac_net.train() + for _ in range(self.config.train_epochs): + experience_set = self.experience_manager.get() + states, next_states = experience_set.states, experience_set.next_states + actual_actions = torch.from_numpy(experience_set.actions).to(self.ac_net.device) + rewards = torch.from_numpy(experience_set.rewards).to(self.ac_net.device) + if len(actual_actions.shape) == 1: + actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) + + with torch.no_grad(): + next_q_values = self.target_ac_net.value(next_states) + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + for _ in range(self.config.gradient_iters): + q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) + q_value_loss = self.config.q_value_loss_func(q_values, target_q_values) + policy_loss = -self.ac_net.value(states).mean() + loss = q_value_loss + self.config.policy_loss_coefficient * policy_loss + self.ac_net.step(loss) + self._train_cnt += 1 + if self._train_cnt % self.config.target_update_freq == 0: + self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coefficient) + + def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None - def state(self): + def get_state(self): return self.ac_net.state_dict() diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index bfb81c915..5d6f55268 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -1,15 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import namedtuple from typing import Union import numpy as np import torch -from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.experience import AbsExperienceManager from maro.rl.model import QNetForDiscreteActionSpace -from maro.rl.policy import AbsCorePolicy, TrainingLoopConfig +from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -19,6 +18,8 @@ class DQNConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. target_update_freq (int): Number of training rounds between target model updates. + train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. + gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. soft_update_coefficient (float): Soft update coefficient, e.g., target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. Defaults to 1.0. @@ -29,20 +30,24 @@ class DQNConfig: it must be a key in ``TORCH_LOSS``. Defaults to "mse". """ __slots__ = [ - "reward_discount", "target_update_freq", "num_steps", "batch_size", "sampler_cls", "sampler_params", - "soft_update_coefficient", "double", "loss_func" + "reward_discount", "target_update_freq", "train_epochs", "gradient_iters", "soft_update_coefficient", + "double", "loss_func" ] def __init__( self, reward_discount: float, target_update_freq: int, + train_epochs: int = 1, + gradient_iters: int = 1, soft_update_coefficient: float = 0.1, double: bool = True, loss_cls="mse" ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq + self.train_epochs = train_epochs + self.gradient_iters = gradient_iters self.soft_update_coefficient = soft_update_coefficient self.double = double self.loss_func = get_torch_loss_cls(loss_cls)() @@ -60,14 +65,13 @@ class DQN(AbsCorePolicy): def __init__( self, q_net: QNetForDiscreteActionSpace, - experience_memory: ExperienceMemory, - generic_config: TrainingLoopConfig, - special_config: DQNConfig, + experience_manager: AbsExperienceManager, + config: DQNConfig ): if not isinstance(q_net, QNetForDiscreteActionSpace): raise TypeError("model must be an instance of 'QNetForDiscreteActionSpace'") - super().__init__(experience_memory, generic_config, special_config) + super().__init__(experience_manager, config) self.q_net = q_net self.target_q_net = q_net.copy() if q_net.trainable else None self.target_q_net.eval() @@ -81,37 +85,46 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = actions.cpu().numpy() return actions[0] if len(actions) == 1 else actions - def step(self, experience_set: ExperienceSet): - if not isinstance(experience_set, ExperienceSet): - raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") - - self.q_net.train() - # sample from the replay memory - states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.q_net.device) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.q_net.device) - q_values = self.q_net.q_values(states, actions) - # get next Q values - with torch.no_grad(): - if self.special_config.double: - next_q_values = self.target_q_net.q_values(next_states, self.q_net.choose_action(next_states)[0]) + def update(self): + self.q_net.train() + for _ in range(self.config.train_epochs): + # sample from the replay memory + experience_set = self.experience_manager.get() + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.q_net.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.q_net.device) + if self.config.double: + for _ in range(self.config.gradient_iters): + # get target Q values + with torch.no_grad(): + actions_by_eval_q_net = self.q_net.choose_action(next_states)[0] + next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + # gradient steps + q_values = self.q_net.q_values(states, actions) + loss = self.config.loss_func(q_values, target_q_values) + self.q_net.step(loss.mean()) else: - next_q_values = self.target_q_net.choose_action(next_states)[1] # (N,) - - # get TD errors - target_q_values = (rewards + self.special_config.reward_discount * next_q_values).detach() # (N,) - loss = self.special_config.loss_func(q_values, target_q_values) - - # train and update target if necessary - self.q_net.step(loss.mean()) - self._training_counter += 1 - if self._training_counter % self.special_config.target_update_freq == 0: - self.target_q_net.soft_update(self.q_net, self.special_config.soft_update_coefficient) - - def load_state(self, policy_state): + # get target Q values + with torch.no_grad(): + next_q_values = self.target_q_net.choose_action(next_states)[1] # (N,) + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + # gradient steps + for _ in range(self.config.gradient_iters): + q_values = self.q_net.q_values(states, actions) + loss = self.config.loss_func(q_values, target_q_values) + self.q_net.step(loss.mean()) + + self._training_counter += 1 + if self._training_counter % self.config.target_update_freq == 0: + self.target_q_net.soft_update(self.q_net, self.config.soft_update_coefficient) + + def set_state(self, policy_state): self.q_net.load_state_dict(policy_state) self.target_q_net = self.q_net.copy() if self.q_net.trainable else None self.target_q_net.eval() - def state(self): + def get_state(self): return self.q_net.state_dict() diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 10b26517b..447195134 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -1,8 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .sampler import AbsSampler, UniformSampler -from .sampler_cls_index import get_sampler_cls -from .experience_memory import ExperienceMemory, ExperienceSet, Replay +from .experience import ExperienceSet, Replay +from .experience_manager import AbsExperienceManager, UniformSampler, UseAndDispose -__all__ = ["AbsSampler", "ExperienceMemory", "ExperienceSet", "Replay", "UniformSampler", "get_sampler_cls"] +__all__ = ["AbsExperienceManager", "ExperienceSet", "Replay", "UniformSampler", "UseAndDispose"] diff --git a/maro/rl/experience/experience.py b/maro/rl/experience/experience.py new file mode 100644 index 000000000..ee58c2721 --- /dev/null +++ b/maro/rl/experience/experience.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.utils.exception.rl_toolkit_exception import InvalidExperience + + +class ExperienceSet: + + __slots__ = ["states", "actions", "rewards", "next_states"] + + def __init__(self, states: list, actions: list, rewards: list, next_states: list): + if not len(states) == len(actions) == len(rewards) == len(next_states): + raise InvalidExperience("values of contents should consist of lists of the same length") + self.states = states + self.actions = actions + self.rewards = rewards + self.next_states = next_states + + def __len__(self): + return len(self.states) + + +class Replay(object): + def __init__(self): + self.states = [] + self.actions = [] + self.rewards = [] + + def to_experience_set(self): + # print(len(self.rewards), len(self.states)) + num_complete = min(len(self.rewards), len(self.states) - 1) + exp_set = ExperienceSet( + self.states[:num_complete], + self.actions[:num_complete], + self.rewards[:num_complete], + self.states[1:num_complete + 1] + ) + + del self.states[:num_complete] + del self.actions[:num_complete] + del self.rewards[:num_complete] + + return exp_set diff --git a/maro/rl/experience/experience_manager.py b/maro/rl/experience/experience_manager.py new file mode 100644 index 000000000..b1326ea1c --- /dev/null +++ b/maro/rl/experience/experience_manager.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod + +import numpy as np + +from .experience import ExperienceSet + + +class AbsExperienceManager(ABC): + """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". + + This implementation uses a dictionary of lists as the internal data structure. The objects for each key + are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during + put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited + and limited storage are supported. + + Args: + capacity (int): Maximum number of experiences that can be stored. + overwrite_type (str): If storage capacity is bounded, this specifies how existing entries + are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: + - "rolling", where overwrite occurs sequentially with wrap-around. + - "random", where overwrite occurs randomly among filled positions. + Alternatively, the user may also specify overwrite positions (see ``put``). + """ + def __init__(self, capacity: int, overwrite_type: str = "rolling"): + super().__init__() + if overwrite_type not in {"rolling", "random"}: + raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") + self._capacity = capacity + self._overwrite_type = overwrite_type + self.keys = ExperienceSet.__slots__ + self.data = {key: [None] * self._capacity for key in self.keys} + self._size = 0 + + @property + def size(self): + return self._size + + @property + def capacity(self): + return self._capacity + + @property + def overwrite_type(self): + return self._overwrite_type + + @abstractmethod + def get(self) -> ExperienceSet: + raise NotImplementedError + + def put(self, experience_set: ExperienceSet): + """Put new contents in the store. + + Args: + contents (dict): Dictionary of items to add to the store. If the store is not empty, this must have the + same keys as the store itself. Otherwise an ``StoreMisalignment`` will be raised. + + Returns: + The indexes where the newly added entries reside in the store. + """ + added_size = len(experience_set) + if added_size > self._capacity: + raise ValueError("size of added items should not exceed the capacity.") + + num_experiences = self._size + added_size + num_overwrites = num_experiences - self._capacity + if num_overwrites <= 0: + indexes = list(range(self._size, num_experiences)) + # follow the overwrite rule set at init + elif self._overwrite_type == "rolling": + # using the negative index convention for convenience + start_index = self._size - self._capacity + indexes = list(range(start_index, start_index + added_size)) + else: + random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) + indexes = list(range(self._size, self._capacity)) + list(random_indexes) + + for key in self.data: + for idx, val in zip(indexes, getattr(experience_set, key)): + self.data[key][idx] = val + + self._size = min(self._capacity, num_experiences) + + def clear(self): + """Empty the store.""" + self.data = {key: [None] * self._capacity for key in self.keys} + self._size = 0 + + +class UniformSampler(AbsExperienceManager): + """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". + + This implementation uses a dictionary of lists as the internal data structure. The objects for each key + are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during + put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited + and limited storage are supported. + + Args: + capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. + overwrite_type (str): If storage capacity is bounded, this specifies how existing entries + are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: + - "rolling", where overwrite occurs sequentially with wrap-around. + - "random", where overwrite occurs randomly among filled positions. + Alternatively, the user may also specify overwrite positions (see ``put``). + """ + def __init__(self, capacity: int, batch_size: int, overwrite_type: str = None, replace: bool = True): + super().__init__(capacity, overwrite_type=overwrite_type) + self.batch_size = batch_size + self.replace = replace + + def get(self) -> ExperienceSet: + indexes = np.random.choice(self._size, size=self.batch_size, replace=self.replace) + return ExperienceSet(*[[self.data[key][idx] for idx in indexes] for key in self.keys]) + + +class UseAndDispose(AbsExperienceManager): + def get(self) -> ExperienceSet: + exp_set = ExperienceSet(*[self.data[key] for key in self.keys]) + self.clear() + return exp_set diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 63de8505b..f516ced9a 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -223,7 +223,7 @@ class PolicyValueNetForContinuousActionSpace(AbsCoreModel): the components. Defaults to None. """ @abstractmethod - def forward(self, states, actions, output_action: bool = True, output_values: bool = True): + def forward(self, states, actions=None): raise NotImplementedError def choose_action(self, states): @@ -231,4 +231,7 @@ def choose_action(self, states): Given Q-values for a batch of states and all actions, return the maximum Q-value and the corresponding action index for each state. """ - return self.forward(states, output_values=False) + return self.forward(states) + + def value(self, states): + return self.forward(states, actions=self.forward(states)) diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 0edc80128..77e89d44b 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy, TrainingLoopConfig +from .policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy -__all__ = ["AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy", "TrainingLoopConfig"] +__all__ = ["AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy"] diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 791086591..c1ed08af0 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -5,23 +5,7 @@ from collections import namedtuple from maro.rl.exploration import AbsExploration -from maro.rl.experience import ExperienceMemory, ExperienceSet - - -class TrainingLoopConfig: - __slots__ = ["sampler_cls", "batch_size", "train_iters", "sampler_kwargs"] - - def __init__( - self, - sampler_cls, - batch_size: int, - train_iters: int, - sampler_kwargs: dict = None - ): - self.sampler_cls = sampler_cls - self.batch_size = batch_size - self.train_iters = train_iters - self.sampler_kwargs = sampler_kwargs if sampler_kwargs else {} +from maro.rl.experience import AbsExperienceManager, ExperienceSet class AbsPolicy(ABC): @@ -53,18 +37,10 @@ def choose_action(self, state): class AbsCorePolicy(AbsPolicy): - def __init__( - self, - experience_memory: ExperienceMemory, - generic_config: TrainingLoopConfig, - special_config - ): + def __init__(self, experience_manager: AbsExperienceManager, config): super().__init__() - self.experience_memory = experience_memory - self.generic_config = generic_config - self.special_config = special_config - sampler_cls, batch_size = generic_config.sampler_cls, generic_config.batch_size - self.sampler = sampler_cls(experience_memory, batch_size, **generic_config.sampler_kwargs) + self.experience_manager = experience_manager + self.config = config @abstractmethod def choose_action(self, state): @@ -79,17 +55,8 @@ def choose_action(self, state): """ raise NotImplementedError - def store_experiences(self, experience_set: ExperienceSet): - self.experience_memory.put(experience_set) - - def update(self): - for _ in range(self.generic_config.train_iters): - indexes, sp = self.sampler.sample() - step_info = self.step(sp) - self.sampler.update(indexes, step_info) - @abstractmethod - def step(self, experience_set: ExperienceSet): + def update(self): raise NotImplementedError @abstractmethod @@ -141,8 +108,8 @@ def update(self): def learn(self, experience_set: ExperienceSet): return self.core_policy.learn(experience_set) - def load_state(self, policy_state): - self.core_policy.load_state(policy_state) + def set_state(self, policy_state): + self.core_policy.set_state(policy_state) def load(self, path: str): self.core_policy.load(path) diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 2ac4b8a9e..1acaff841 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -5,6 +5,12 @@ from .actor_manager import ActorManager from .distributed_learner import DistributedLearner from .local_learner import LocalLearner -from .policy_update_schedule import MultiPolicyUpdateSchedule +from .policy_update_schedule import EpisodeBasedSchedule, MultiPolicyUpdateSchedule, StepBasedSchedule -__all__ = ["Actor", "ActorManager", "DistributedLearner", "LocalLearner", "MultiPolicyUpdateSchedule"] +__all__ = [ + "Actor", + "ActorManager", + "DistributedLearner", + "LocalLearner", + "EpisodeBasedSchedule", "MultiPolicyUpdateSchedule", "StepBasedSchedule" +] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 863cd824a..74767768d 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -1,12 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import defaultdict from os import getcwd -from typing import Union +from typing import Dict from maro.communication import Proxy -from maro.rl.policy import MultiAgentPolicy from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.exploration import AbsExploration +from maro.rl.policy import AbsPolicy from maro.utils import Logger from .message_enums import MsgKey, MsgTag @@ -21,24 +23,49 @@ class Actor(object): policy (MultiAgentPolicy): Agent that interacts with the environment. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. """ def __init__( self, env: AbsEnvWrapper, - policy: MultiAgentPolicy, + policy_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str], group: str, + exploration_dict: Dict[str, AbsExploration] = None, + agent2exploration: Dict[str, str] = None, eval_env: AbsEnvWrapper = None, - proxy_options: dict = None, - log_dir: str = getcwd() + log_dir: str = getcwd(), + **proxy_kwargs ): self.env = env self.eval_env = eval_env if eval_env else self.env - self.policy = policy - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_options) + + # mappings between agents and policies + self.policy_dict = policy_dict + self.agent2policy = agent2policy + self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self.agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent2policy.items(): + self.agent_groups_by_policy[policy_id].append(agent_id) + + # mappings between exploration schemes and agents + self.exploration_dict = exploration_dict + if exploration_dict: + self.agent2exploration = agent2exploration + self.exploration = { + agent_id: self.exploration_dict[exploration_id] + for agent_id, exploration_id in self.agent2exploration.items() + } + self.exploration_enabled = True + self.agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in agent2exploration.items(): + self.agent_groups_by_exploration[exploration_id].append(agent_id) + + for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): + self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) + + self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_kwargs) self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): @@ -48,19 +75,33 @@ def run(self): break if msg.tag == MsgTag.COLLECT: - self.policy.train_mode() episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] if self.env.state is None: self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") - self._logger.debug(f"Exploration parameters: {self.policy.exploration_params}") + if hasattr(self, "exploration_dict"): + exploration_params = { + agent_ids: self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self.agent_groups_by_exploration.items() + } + self._logger.debug(f"Exploration parameters: {exploration_params}") + self.env.reset() self.env.start() # get initial state + self._load_policy(msg.body[MsgKey.POLICY]) starting_step_index = self.env.step_index + 1 - self.policy.load_state(msg.body[MsgKey.POLICY]) steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while self.env.state and steps_to_go > 0: - action = self.policy.choose_action(self.env.state) + if self.exploration_dict: + action = { + id_: + self.exploration[id_](self.policy[id_].choose_action(st)) + if id_ in self.exploration else self.policy[id_].choose_action(st) + for id_, st in self.env.state.items() + } + else: + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.env.state.items()} + self.env.step(action) steps_to_go -= 1 @@ -79,18 +120,21 @@ def run(self): if msg.body[MsgKey.RETURN_ENV_METRICS]: return_info[MsgKey.METRICS] = self.env.metrics if not self.env.state: - self.policy.exploration_step() + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) elif msg.tag == MsgTag.EVAL: ep = msg.body[MsgKey.EPISODE_INDEX] self._logger.info(f"Evaluation episode {ep}") - self.policy.eval_mode() self.eval_env.reset() self.eval_env.start() # get initial state - self.policy.load_state(msg.body[MsgKey.POLICY]) + self._load_policy(msg.body[MsgKey.POLICY]) while self.eval_env.state: - self.eval_env.step(self.policy.choose_action(self.eval_env.state)) + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} + self.eval_env.step(action) return_info = { MsgKey.METRICS: self.env.metrics, @@ -98,3 +142,7 @@ def run(self): MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] } self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) + + def _load_policy(self, policy_state_dict): + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].set_state(policy_state) diff --git a/maro/rl/training/actor_manager.py b/maro/rl/training/actor_manager.py index bffc834c7..6ce7d0913 100644 --- a/maro/rl/training/actor_manager.py +++ b/maro/rl/training/actor_manager.py @@ -4,7 +4,6 @@ from collections import defaultdict from os import getcwd from random import choices -from typing import Union from maro.communication import Proxy, SessionType from maro.utils import Logger @@ -21,7 +20,7 @@ class ActorManager(object): num_actors (int): Expected number of actors in the group identified by ``group_name``. group_name (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the actors (and roll-out clients, if any). - proxy_options (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to None. update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger learner updates, i.e., model training. @@ -30,17 +29,16 @@ def __init__( self, num_actors: int, group_name: str, - proxy_options: dict = None, required_finishes: int = None, log_env_metrics: bool = False, - log_dir: str = getcwd() + log_dir: str = getcwd(), + **proxy_kwargs ): super().__init__() + self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) self.num_actors = num_actors peers = {"actor": num_actors} - if proxy_options is None: - proxy_options = {} - self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_options) + self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_kwargs) self._actors = self._proxy.peers["actor"] # remote actor ID's if required_finishes and required_finishes > self.num_actors: @@ -55,7 +53,6 @@ def __init__( self.total_env_steps = 0 self.total_reward = defaultdict(float) self._log_env_metrics = log_env_metrics - self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) def collect( self, @@ -63,7 +60,6 @@ def collect( segment_index: int, num_steps: int, policy_dict: dict = None, - exploration=None, discard_stale_experiences: bool = True, return_env_metrics: bool = False ): @@ -78,8 +74,6 @@ def collect( if self._log_env_metrics: self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") - if exploration_params: - self._logger.info(f"exploration_params: {exploration_params}") self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") diff --git a/maro/rl/training/dispatcher.py b/maro/rl/training/dispatcher.py deleted file mode 100755 index b574e51ac..000000000 --- a/maro/rl/training/dispatcher.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import deque - -import zmq -from zmq.eventloop.ioloop import IOLoop -from zmq.eventloop.zmqstream import ZMQStream - - -class LRUQueue(object): - """LRUQueue class using ZMQStream/IOLoop for event dispatching. - - Code adapted from https://zguide.zeromq.org/docs/chapter3/#A-High-Level-API-for-ZeroMQ. - """ - def __init__(self): - self._context = zmq.Context.instance() - frontend = self._context.socket(zmq.ROUTER) - frontend.bind("tcp://127.0.0.1:50000") - backend = self._context.socket(zmq.ROUTER) - backend.bind("tcp://127.0.0.1:50001") - self._workers = deque() - - self._frontend = ZMQStream(frontend) - self._backend = ZMQStream(backend) - self._backend.on_recv(self._handle_backend) - - def _handle_backend(self, msg): - # Queue worker ID for LRU routing - worker, empty, client = msg[:3] - # add worker back to the list of workers - self._workers.append(worker) - assert empty == b"" - # Third frame is READY or else a client reply address - # If client reply, send rest back to frontend - if client != b"READY": - empty, reply = msg[3:] - assert empty == b"" - self._frontend.send_multipart([client, b'', reply]) - - # Start accepting frontend messages now that at least one worker is free. - self._frontend.on_recv(self._handle_frontend) - - def _handle_frontend(self, msg): - # Now get next client request, route to LRU worker - # Client request is [address][empty][request] - client, empty, request = msg - assert empty == b"" - # Dequeue and drop the next worker address - self._backend.send_multipart([self._workers.popleft(), b'', client, b'', request]) - if not self._workers: - # stop receiving until workers become available again - self._frontend.stop_on_recv() - - -def start_dispatcher(): - dispatcher = LRUQueue() - IOLoop.instance().start() diff --git a/maro/rl/training/distributed_learner.py b/maro/rl/training/distributed_learner.py index 2e8e5c077..5b482d8c1 100644 --- a/maro/rl/training/distributed_learner.py +++ b/maro/rl/training/distributed_learner.py @@ -4,15 +4,13 @@ import time from collections import defaultdict from os import getcwd -from typing import Callable, Dict, List, Union +from typing import Dict, List, Union -from maro.communication import Message, Proxy, SessionType from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.policy import AbsPolicy +from maro.rl.policy import AbsPolicy, AbsCorePolicy from maro.utils import Logger from .actor_manager import ActorManager -from .message_enums import MsgTag, MsgKey from .policy_update_schedule import MultiPolicyUpdateSchedule @@ -77,18 +75,20 @@ def __init__( self.end_of_episode_kwargs = end_of_episode_kwargs self._log_env_metrics = log_env_metrics - self._total_learning_time = 0 - self._total_env_steps = 0 - self._total_experiences_collected = 0 def run(self): for ep in range(1, self.num_episodes + 1): self._train(ep) - policy_ids = self.policy_update_schedule.pop(ep) + policy_ids = self.policy_update_schedule.pop_episode(ep) + if policy_ids == ["*"]: + policy_ids = list(self.policy_dict.keys()) for policy_id in policy_ids: self.policy_dict[policy_id].update() - self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") + + if policy_ids: + self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") + self.end_of_episode(ep, **self.end_of_episode_kwargs) if ep == self.eval_schedule[self._eval_point_index]: @@ -101,34 +101,45 @@ def run(self): def _train(self, ep: int): t0 = time.time() learning_time = 0 + env_steps = 0 num_experiences_collected = 0 - num_actor_finishes, segment_index = 0, 1 + policy_ids, num_actor_finishes, segment_index = list(self.policy_dict.keys()), 0, 1 - self.policy_update_schedule.enter(ep) - while num_actor_finishes < self.actor_manager.required_actor_finishes: + while num_actor_finishes < self.actor_manager.required_finishes: + # parallel experience collection for exp_by_agent, done in self.actor_manager.collect( ep, segment_index, self.experience_update_interval, - policy_dict=self.policy.state(), + policy_dict={policy_id: self.policy_dict[policy_id].get_state() for policy_id in policy_ids}, discard_stale_experiences=self.discard_stale_experiences ): - self._store_experiences(exp_by_agent) - num_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) + for agent_id, exp in exp_by_agent.items(): + if isinstance(self.policy[agent_id], AbsCorePolicy): + self.policy[agent_id].experience_manager.put(exp) - # policy update - tl0 = time.time() - policy_ids = self.policy_update_schedule.pop(segment_index) - for policy_id in policy_ids: - self.policy_dict[policy_id].update() - learning_time += time.time() - tl0 + env_steps += self.experience_update_interval + num_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) num_actor_finishes += done - segment_index += 1 - self.policy_update_schedule.exit() + # policy update + tl0 = time.time() + policy_ids = self.policy_update_schedule.pop_step(ep, segment_index) + if policy_ids == ["*"]: + policy_ids = list(self.policy_dict.keys()) + for policy_id in policy_ids: + self.policy_dict[policy_id].update() + + if policy_ids: + self._logger.info(f"Updated policies {policy_ids} after segment {segment_index}") + + learning_time += time.time() - tl0 + + segment_index += 1 + # performance details self._logger.debug( f"ep {ep} summary - " f"running time: {time.time() - t0}" - f"env steps: {self.env.step_index}" + f"env steps: {env_steps}" f"learning time: {learning_time}" f"experiences collected: {num_experiences_collected}" ) @@ -136,12 +147,11 @@ def _train(self, ep: int): def _evaluate(self, ep: int): self._logger.info("Evaluating...") if self.eval_env: - self.policy.eval_mode() self.eval_env.save_replay = False self.eval_env.reset() self.eval_env.start() # get initial state while self.eval_env.state: - action = self.policy.choose_action(self.eval_env.state) + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} self.eval_env.step(action) if not self.eval_env.state: @@ -150,12 +160,8 @@ def _evaluate(self, ep: int): if self._log_env_metrics: self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") else: - self.actor_manager.evaluate(ep, self.policy.state(), self.num_eval_actors) + policy_state = {policy_id: policy.get_state() for policy_id, policy in self.policy_dict.items()} + self.actor_manager.evaluate(ep, policy_state, self.num_eval_actors) def end_of_episode(self, ep: int, **kwargs): pass - - def _store_experiences(self, experiences_by_agent: dict): - for agent_id, exp in experiences_by_agent.items(): - if isinstance(self.policy[agent_id], AbsCorePolicy): - self.policy[agent_id].store_experiences(exp) \ No newline at end of file diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py index 443389580..b5b40cc3a 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -8,7 +8,8 @@ from typing import Callable, Dict, List, Tuple, Union from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.policy import AbsPolicy +from maro.rl.exploration import AbsExploration +from maro.rl.policy import AbsCorePolicy, AbsPolicy from maro.utils import Logger from .policy_update_schedule import MultiPolicyUpdateSchedule @@ -29,7 +30,7 @@ def __init__( num_episodes: int, policy_update_schedule: MultiPolicyUpdateSchedule, exploration_dict: Dict[str, AbsExploration] = None, - agent_to_exploration: Dict[str, str] = None, + agent2exploration: Dict[str, str] = None, experience_update_interval: int = -1, eval_env: AbsEnvWrapper = None, eval_schedule: Union[int, List[int]] = None, @@ -52,7 +53,7 @@ def __init__( self.agent_groups_by_policy[policy_id] = tuple(agent_ids) self.agent_groups_by_policy = defaultdict(list) - for agent_id, policy_id in agent_to_policy.items(): + for agent_id, policy_id in agent2policy.items(): self.agent_groups_by_policy[policy_id].append(agent_id) for policy_id, agent_ids in self.agent_groups_by_policy.items(): @@ -61,14 +62,14 @@ def __init__( # mappings between exploration schemes and agents self.exploration_dict = exploration_dict if exploration_dict: - self.agent_to_exploration = agent_to_exploration + self.agent2exploration = agent2exploration self.exploration = { agent_id: self.exploration_dict[exploration_id] - for agent_id, exploration_id in self.agent_to_exploration.items() + for agent_id, exploration_id in self.agent2exploration.items() } self.exploration_enabled = True self.agent_groups_by_exploration = defaultdict(list) - for agent_id, exploration_id in agent_to_exploration.items(): + for agent_id, exploration_id in agent2exploration.items(): self.agent_groups_by_exploration[exploration_id].append(agent_id) for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): @@ -103,10 +104,14 @@ def run(self): for ep in range(1, self.num_episodes + 1): self._train(ep) - policy_ids = self.policy_update_schedule.pop(ep) + policy_ids = self.policy_update_schedule.pop_episode(ep) + if policy_ids == ["*"]: + policy_ids = list(self.policy_dict.keys()) for policy_id in policy_ids: self.policy_dict[policy_id].update() - self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") + + if policy_ids: + self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") if ep == self.eval_schedule[self._eval_point_index]: self._eval_point_index += 1 @@ -120,8 +125,8 @@ def _train(self, ep: int): num_experiences_collected = 0 self._logger.info(f"Training episode {ep}") - if hasattr(self, "exploration_dict"): - exploration_parameters = { + if self.exploration_dict: + exploration_params = { agent_ids: self.exploration_dict[exploration_id].parameters for exploration_id, agent_ids in self.agent_groups_by_exploration.items() } @@ -130,17 +135,16 @@ def _train(self, ep: int): self.env.save_replay = True self.env.reset() self.env.start() # get initial state - self.policy_update_schedule.enter(ep) while self.env.state: if self.exploration_dict: action = { - agent_id: - self.exploration[agent_id](self.policy[agent_id].choose_action(state)) - if agent_id in self.exploration else self.policy[agent_id].choose_action(state) - for agent_id, state in self.env.state.items() + id_: + self.exploration[id_](self.policy[id_].choose_action(st)) + if id_ in self.exploration else self.policy[id_].choose_action(st) + for id_, st in self.env.state.items() } else: - action = {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in self.env.state.items()} + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.env.state.items()} self.env.step(action) step_index = self.env.step_index @@ -148,21 +152,28 @@ def _train(self, ep: int): # experience collection if not self.env.state or step_index % self.experience_update_interval == 0: exp_by_agent = self.env.get_experiences() - self._store_experiences(exp_by_agent) + for agent_id, exp in exp_by_agent.items(): + if isinstance(self.policy[agent_id], AbsCorePolicy): + self.policy[agent_id].experience_manager.put(exp) num_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) # policy update tl0 = time.time() - policy_ids = self.policy_update_schedule.pop(step_index) + policy_ids = self.policy_update_schedule.pop_step(ep, step_index) + if policy_ids == ["*"]: + policy_ids = list(self.policy_dict.keys()) for policy_id in policy_ids: self.policy_dict[policy_id].update() - self._logger.info(f"Updated policies {policy_ids} after step {step_index}") + if policy_ids: + self._logger.info(f"Updated policies {policy_ids} after step {step_index}") learning_time += time.time() - tl0 # update the exploration parameters - self._exploration_step() - self.policy_update_schedule.exit() + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + # performance details if self._log_env_metrics: self._logger.info(f"ep {ep}: {self.env.metrics}") @@ -182,7 +193,7 @@ def _evaluate(self, ep: int): self.eval_env.reset() self.eval_env.start() # get initial state while self.eval_env.state: - action = {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in self.env.state.items()} + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} self.eval_env.step(action) if self._log_env_metrics: @@ -192,13 +203,3 @@ def _evaluate(self, ep: int): def end_of_episode(self, ep: int, **kwargs): pass - - def _store_experiences(self, experiences_by_agent: dict): - for agent_id, exp in experiences_by_agent.items(): - if isinstance(self.policy[agent_id], AbsCorePolicy): - self.policy[agent_id].store_experiences(exp) - - def _exploration_step(self): - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() diff --git a/maro/rl/training/policy_update_schedule.py b/maro/rl/training/policy_update_schedule.py index ec2a44ddf..ef7fdef76 100644 --- a/maro/rl/training/policy_update_schedule.py +++ b/maro/rl/training/policy_update_schedule.py @@ -1,8 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import heapq -from collections import namedtuple +from collections import defaultdict, namedtuple from typing import List, Union EpisodeBasedSchedule = namedtuple("EpisodeBasedSchedule", ["start", "interval"]) @@ -13,51 +12,50 @@ class MultiPolicyUpdateSchedule: """ """ def __init__(self, schedule_option: Union[EpisodeBasedSchedule, StepBasedSchedule, dict] = -1): - self._pending_steps = [] - self._pending_episodes = [] + self._pending_steps = defaultdict(list) + self._pending_episodes = defaultdict(list) + self._step_schedule_opt = {} + self._episode_schedule_opt = {} + if isinstance(schedule_option, dict): - self._step_schedule_opt = {} - self._episode_schedule_opt = {} for policy_id, sch in schedule_option.items(): if isinstance(sch, StepBasedSchedule): self._step_schedule_opt[policy_id] = (sch.start_ep, sch.interval) if sch.end_ep_update: self._episode_schedule_opt[policy_id] = (sch.start_ep, 1) + self._pending_episodes[sch.start_ep].append(policy_id) + self._pending_steps[sch.interval].append(policy_id) elif isinstance(sch, EpisodeBasedSchedule): self._episode_schedule_opt[policy_id] = (sch.start, sch.interval) - - for policy_id, (start, _) in self._episode_schedule_opt.items(): - heapq.heappush(self._pending_episodes, (start, policy_id)) + self._pending_episodes[sch.start].append(policy_id) else: - self._episode_schedule_opt = None - self._step_schedule_opt = None - if isinstance(schedule_option, EpisodeBasedSchedule): - self._episode_schedule_opt = (schedule_option.start, schedule_option.interval) + if isinstance(schedule_option, StepBasedSchedule): + self._step_schedule_opt["*"] = (schedule_option.start_ep, schedule_option.interval) + self._pending_steps[schedule_option.interval].append("*") + if schedule_option.end_ep_update: + self._episode_schedule_opt["*"] = (schedule_option.start_ep, 1) + self._pending_episodes[schedule_option.start_ep].append("*") else: - self._step_schedule_opt = (schedule_option.start_ep, schedule_option.interval) - - heapq.heappush(self._pending_episodes, (start, "*")) - - self._in_episode = False - - def enter(self, ep: int): - self._in_episode = True - for policy_id, (start_ep, interval) in self._step_schedule_opt.items(): - if ep >= start_ep: - heapq.heappush(self._pending_steps, (interval, policy_id)) - - def exit(self): - self._in_episode = False - self._pending_steps.clear() - - def pop(self, index: int) -> List[str]: - policy_ids = [] - pending = self._pending_steps if self._in_episode else self._pending_episodes - schedule_opt = self._step_schedule_opt if self._in_episode else self._episode_schedule_opt - while pending and pending[0][0] == index: - _, policy_id = heapq.heappop(pending) - policy_ids.append(policy_id) - next_index = index + schedule_opt[policy_id][1] - heapq.heappush(pending, (next_index, policy_id)) - - return policy_ids + self._episode_schedule_opt["*"] = (schedule_option.start, schedule_option.interval) + self._pending_episodes[schedule_option.start].append("*") + + self._episode = None + + def pop_step(self, ep: int, step: int) -> List[str]: + for policy_id in self._pending_steps[step]: + next_step = step + self._step_schedule_opt[policy_id][1] + # if the pending_steps is filled, skip the loop altogether + if self._pending_steps[next_step]: + break + self._pending_steps[next_step].append(policy_id) + + return [ + policy_id for policy_id in self._pending_steps[step] if self._step_schedule_opt[policy_id][0] <= ep + ] + + def pop_episode(self, ep: int) -> List[str]: + for policy_id in self._pending_episodes[ep]: + next_ep = ep + self._episode_schedule_opt[policy_id][1] + self._pending_episodes[next_ep].append(policy_id) + + return self._pending_episodes[ep] diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py deleted file mode 100755 index c3ee0aa01..000000000 --- a/maro/rl/training/trainer.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import pickle -from collections import deque - -import zmq -from zmq.eventloop.ioloop import IOLoop -from zmq.eventloop.zmqstream import ZMQStream - - -def trainer(id_: str): - socket = zmq.Context().socket(zmq.REQ) - socket.setsockopt_string(zmq.IDENTITY, f"Trainer_{id_}") - socket.connect("tcp://127.0.0.1:50001") - socket = ZMQStream(socket) - socket.send(b"READY") # register to the dispatcher - - def train(sock, msg): - client, _, request = msg - request = pickle.loads(request) - info = request["agent"].step(*request["args"], **request["kwargs"]) - request.update({"model": request["agent"].dump_model(), "info": info}) - del request["agent"] - del request["args"] - del request["kwargs"] - sock.send_multipart([client, b"", pickle.dumps(request)]) - - socket.on_recv_stream(train) - IOLoop.instance().start() From 645d84796ae5fd29c3bd5899df0a3ea6d8456953 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 10 May 2021 05:24:07 +0000 Subject: [PATCH 225/482] rm obsolete files --- maro/rl/experience/experience_memory.py | 168 ------------------------ maro/rl/experience/sampler.py | 56 -------- maro/rl/experience/sampler_cls_index.py | 16 --- maro/rl/policy/multi_agent_policy.py | 111 ---------------- 4 files changed, 351 deletions(-) delete mode 100644 maro/rl/experience/experience_memory.py delete mode 100644 maro/rl/experience/sampler.py delete mode 100644 maro/rl/experience/sampler_cls_index.py delete mode 100644 maro/rl/policy/multi_agent_policy.py diff --git a/maro/rl/experience/experience_memory.py b/maro/rl/experience/experience_memory.py deleted file mode 100644 index d82aa77c8..000000000 --- a/maro/rl/experience/experience_memory.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from enum import Enum -from typing import Callable, Dict, List, Tuple, Union - -import numpy as np - -from maro.utils import clone -from maro.utils.exception.rl_toolkit_exception import InvalidExperience - - -class ExperienceSet: - - __slots__ = ["states", "actions", "rewards", "next_states"] - - def __init__(self, states: list, actions: list, rewards: list, next_states: list): - if not len(states) == len(actions) == len(rewards) == len(next_states): - raise InvalidExperience("values of contents should consist of lists of the same length") - - self.states = states - self.actions = actions - self.rewards = rewards - self.next_states = next_states - - def __len__(self): - return len(self.states) - - -class Replay(object): - def __init__(self): - self.states = [] - self.actions = [] - self.rewards = [] - - def to_experience_set(self): - # print(len(self.rewards), len(self.states)) - num_complete = min(len(self.rewards), len(self.states) - 1) - exp_set = ExperienceSet( - self.states[:num_complete], - self.actions[:num_complete], - self.rewards[:num_complete], - self.states[1:num_complete + 1] - ) - - del self.states[:num_complete] - del self.actions[:num_complete] - del self.rewards[:num_complete] - - return exp_set - - -class ExperienceMemory(object): - """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". - - This implementation uses a dictionary of lists as the internal data structure. The objects for each key - are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during - put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited - and limited storage are supported. - - Args: - capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. - overwrite_type (str): If storage capacity is bounded, this specifies how existing entries - are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - - "rolling", where overwrite occurs sequentially with wrap-around. - - "random", where overwrite occurs randomly among filled positions. - Alternatively, the user may also specify overwrite positions (see ``put``). - """ - def __init__(self, capacity: int = -1, overwrite_type: str = None): - super().__init__() - if overwrite_type not in {"rolling", "random"}: - raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") - self._capacity = capacity - self._overwrite_type = overwrite_type - self._keys = ExperienceSet.__slots__ - self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self._keys} - self._size = 0 - - def __len__(self): - return self._size - - def __getitem__(self, index: int): - return {k: lst[index] for k, lst in self.data.items()} - - @property - def capacity(self): - """Store capacity. - - If negative, the store grows without bound. Otherwise, the number of items in the store will not exceed - this capacity. - """ - return self._capacity - - @property - def overwrite_type(self): - """An string indicating the overwrite behavior when the store capacity is exceeded.""" - return self._overwrite_type - - def get(self, indexes: [int] = None) -> ExperienceSet: - if indexes is None: - return ExperienceSet(*[self.data[k] for k in self._keys]) - - return ExperienceSet(*[[self.data[k][i] for i in indexes] for k in self._keys]) - - def put(self, experience_set: ExperienceSet, overwrite_indexes: list = None) -> List[int]: - """Put new contents in the store. - - Args: - contents (dict): Dictionary of items to add to the store. If the store is not empty, this must have the - same keys as the store itself. Otherwise an ``StoreMisalignment`` will be raised. - overwrite_indexes (list, optional): Indexes where the contents are to be overwritten. This is only - used when the store has a fixed capacity and putting ``contents`` in the store would exceed this - capacity. If this is None and overwriting is necessary, rolling or random overwriting will be done - according to the ``overwrite`` property. Defaults to None. - Returns: - The indexes where the newly added entries reside in the store. - """ - added_size = len(experience_set) - if self._capacity == -1: - for key in self.data: - self.data[key].extend(getattr(experience_set, key)) - self._size += added_size - return list(range(self._size - added_size, self._size)) - else: - write_indexes = self._get_update_indexes(added_size, overwrite_indexes=overwrite_indexes) - for key in self.data: - for index, value in zip(write_indexes, getattr(experience_set, key)): - self.data[key][index] = value - - self._size = min(self._capacity, self._size + added_size) - return write_indexes - - def clear(self): - """Empty the store.""" - self.data = { - key: [] if self._capacity == -1 else [None] * self._capacity for key in ExperienceSet.__slots__ - } - self._size = 0 - - def dumps(self): - """Return a deep copy of store contents.""" - return clone(dict(self.data)) - - def get_by_key(self, key): - """Get the contents of the store corresponding to ``key``.""" - return self.data[key] - - def _get_update_indexes(self, added_size: int, overwrite_indexes=None): - if added_size > self._capacity: - raise ValueError("size of added items should not exceed the store capacity.") - - num_overwrites = self._size + added_size - self._capacity - if num_overwrites < 0: - return list(range(self._size, self._size + added_size)) - - if overwrite_indexes is not None: - write_indexes = list(range(self._size, self._capacity)) + list(overwrite_indexes) - else: - # follow the overwrite rule set at init - if self._overwrite_type == "rolling": - # using the negative index convention for convenience - start_index = self._size - self._capacity - write_indexes = list(range(start_index, start_index + added_size)) - else: - random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) - write_indexes = list(range(self._size, self._capacity)) + list(random_indexes) - - return write_indexes diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py deleted file mode 100644 index abafef94c..000000000 --- a/maro/rl/experience/sampler.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from typing import List - -import numpy as np - -from .experience_memory import ExperienceMemory - - -class AbsSampler(ABC): - def __init__(self, data: ExperienceMemory, batch_size: int): - self.data = data - self.batch_size = batch_size - - @abstractmethod - def sample(self) -> List[int]: - raise NotImplementedError - - def update(self): - """Update statistics used for sampling.""" - pass - - -class UniformSampler(AbsSampler): - """ - Obtain a random sample from the experience pool. - - Args: - size (int): Sample sizes for each round of sampling in the chain. If this is a single integer, it is - used as the sample size for all samplers in the chain. - weights (Union[list, np.ndarray]): Sampling weights. - replace (bool): If True, sampling is performed with replacement. Defaults to True. - Returns: - Sampled indexes and the corresponding objects, - e.g., [1, 2, 3], ['a', 'b', 'c']. - """ - def __init__(self, data: ExperienceMemory, batch_size: int, replace: bool = True): - super().__init__(data, batch_size) - self.replace = replace - - def sample(self): - indexes = np.random.choice(len(self.data), size=self.batch_size, replace=self.replace) - return indexes, self.data.get(indexes) - - -class FullSampler(AbsSampler): - """ - """ - def __init__(self, data: ExperienceMemory, batch_size: int = -1, empty_after_use: bool = True): - super().__init__(data, batch_size) - self.empty_after_use = empty_after_use - - def sample(self): - return self.data.get() \ No newline at end of file diff --git a/maro/rl/experience/sampler_cls_index.py b/maro/rl/experience/sampler_cls_index.py deleted file mode 100644 index 0154a05fa..000000000 --- a/maro/rl/experience/sampler_cls_index.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .sampler import UniformSampler - -SAMPLER = { - "uniform": UniformSampler, -} - -def get_sampler_cls(sampler_type): - if isinstance(sampler_type, str): - if sampler_type not in SAMPLER: - raise KeyError(f"A string sampler_type must be one of {list(SAMPLER.keys())}.") - return SAMPLER[sampler_type] - - return sampler_type diff --git a/maro/rl/policy/multi_agent_policy.py b/maro/rl/policy/multi_agent_policy.py deleted file mode 100644 index f6f8990f2..000000000 --- a/maro/rl/policy/multi_agent_policy.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import pickle -import warnings -from collections import defaultdict, namedtuple -from typing import Dict, List - -from maro.rl.exploration import AbsExploration, NullExploration -from maro.rl.experience import ExperienceMemory - -from .policy import AbsFixedPolicy, AbsCorePolicy - - -class MultiAgentPolicy: - """Convenience wrapper of a set of agents that exposes similar interfaces as a single agent. - - Args: - - """ - def __init__( - self, - policy_dict: Dict[str, AbsPolicy], - agent_to_policy: Dict[str, str], - exploration_dict: Dict[str, AbsExploration] = None, - agent_to_exploration: Dict[str, str] = None - ): - self.policy_dict = policy_dict - self.agent_to_policy = agent_to_policy - self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items()} - self.agent_groups_by_policy = defaultdict(list) - for agent_id, policy_id in agent_to_policy.items(): - self.agent_groups_by_policy[policy_id].append(agent_id) - - for policy_id, agent_ids in self.agent_groups_by_policy.items(): - self.agent_groups_by_policy[policy_id] = tuple(agent_ids) - - self.exploration_dict = exploration_dict - if exploration_dict: - self.agent_to_exploration = agent_to_exploration - self.exploration = { - agent_id: self.exploration_dict[exploration_id] - for agent_id, exploration_id in self.agent_to_exploration.items() - } - self.exploration_enabled = True - self.agent_groups_by_exploration = defaultdict(list) - for agent_id, exploration_id in agent_to_exploration.items(): - self.agent_groups_by_exploration[exploration_id].append(agent_id) - - for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): - self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) - - def train_mode(self): - if hasattr(self, "exploration_enabled"): - self.exploration_enabled = True - - def eval_mode(self): - if hasattr(self, "exploration_enabled"): - self.exploration_enabled = False - - def choose_action(self, state_by_agent: dict): - if self.exploration_dict and self.exploration_enabled: - return { - agent_id: - self.exploration[agent_id](self.policy[agent_id].choose_action(state)) - if agent_id in self.exploration else self.policy[agent_id].choose_action(state) - for agent_id, state in state_by_agent.items() - } - - return {agent_id: self.policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} - - def store_experiences(self, experiences_by_agent: dict): - for agent_id, exp in experiences_by_agent.items(): - if isinstance(self.policy[agent_id], AbsCorePolicy): - self.policy[agent_id].store_experiences(exp) - - def update(self, policy_ids: List[str] = None): - if policy_ids is None: - policy_ids = self.policy_dict.keys() - - for policy_id in policy_ids: - if self.policy_dict[policy_id].update(): - self.policy_dict[policy_id].post_update() - - def load_state(self, policy_state_dict: dict): - """Load policies from memory.""" - if not policy_state_dict.keys() <= self.policy_dict.keys(): - raise Exception(f"Expected policies from {list(self.policy_state_dict.keys())}") - - for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].load_state(policy_state) - - def get_state(self, policy_ids: List[str] = None): - if policy_ids is None: - policy_ids = self.policy_dict.keys() - - return {policy_id: self.policy_dict[policy_id].get_state() for policy_id in policy_ids} - - def load(self, dir_path: str): - """Load models from disk.""" - for policy_id, policy in self.policy_dict.items(): - try: - policy.load(os.path.join(dir_path, policy_id)) - except FileNotFoundError: - warnings.warn(f"policy {policy_id} is skipped because no file is found") - - def save(self, dir_path: str): - os.makedirs(dir_path, exist_ok=True) - for policy_id, policy in self.policy_dict.items(): - policy.save(os.path.join(dir_path, policy_id)) From 9eb7c555188b6ddaa7343784558a26cebee3778d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 10 May 2021 05:30:19 +0000 Subject: [PATCH 226/482] rm obsolete files --- maro/rl/scheduling/__init__.py | 11 - maro/rl/scheduling/scheduler.py | 36 ---- .../scheduling/simple_parameter_scheduler.py | 107 ---------- maro/rl/storage/__init__.py | 9 - maro/rl/storage/abs_store.py | 58 ------ maro/rl/storage/sampler.py | 47 ----- maro/rl/storage/simple_store.py | 161 --------------- maro/rl/training/dispatcher.py | 58 ------ maro/rl/training/learner.py | 194 ------------------ maro/rl/training/test.py | 17 -- maro/rl/training/trainer.py | 30 --- 11 files changed, 728 deletions(-) delete mode 100644 maro/rl/scheduling/__init__.py delete mode 100644 maro/rl/scheduling/scheduler.py delete mode 100644 maro/rl/scheduling/simple_parameter_scheduler.py delete mode 100644 maro/rl/storage/__init__.py delete mode 100644 maro/rl/storage/abs_store.py delete mode 100644 maro/rl/storage/sampler.py delete mode 100644 maro/rl/storage/simple_store.py delete mode 100755 maro/rl/training/dispatcher.py delete mode 100644 maro/rl/training/learner.py delete mode 100644 maro/rl/training/test.py delete mode 100755 maro/rl/training/trainer.py diff --git a/maro/rl/scheduling/__init__.py b/maro/rl/scheduling/__init__.py deleted file mode 100644 index 1b5c46b3b..000000000 --- a/maro/rl/scheduling/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .scheduler import Scheduler -from .simple_parameter_scheduler import LinearParameterScheduler, TwoPhaseLinearParameterScheduler - -__all__ = [ - "Scheduler", - "LinearParameterScheduler", - "TwoPhaseLinearParameterScheduler" -] diff --git a/maro/rl/scheduling/scheduler.py b/maro/rl/scheduling/scheduler.py deleted file mode 100644 index 75d22c702..000000000 --- a/maro/rl/scheduling/scheduler.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -class Scheduler(object): - """Scheduler that generates new parameters each iteration. - - Args: - max_iter (int): Maximum number of iterations. If -1, using the scheduler in a for-loop - will result in an infinite loop unless the ``check_for_stopping`` method is implemented. - """ - - def __init__(self, max_iter: int = -1): - if max_iter <= 0 and max_iter != -1: - raise ValueError("max_iter must be a positive integer or -1.") - self._max_iter = max_iter - self._iter_index = -1 - - def __iter__(self): - return self - - def __next__(self): - self._iter_index += 1 - if self._iter_index == self._max_iter or self.check_for_stopping(): - raise StopIteration - - return self.next_params() - - def next_params(self): - pass - - def check_for_stopping(self) -> bool: - return False - - @property - def iter(self): - return self._iter_index diff --git a/maro/rl/scheduling/simple_parameter_scheduler.py b/maro/rl/scheduling/simple_parameter_scheduler.py deleted file mode 100644 index ffe0c4988..000000000 --- a/maro/rl/scheduling/simple_parameter_scheduler.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import List, Union - -import numpy as np - -from .scheduler import Scheduler - - -class LinearParameterScheduler(Scheduler): - """Static exploration parameter generator based on a linear schedule. - - Args: - max_iter (int): Maximum number of iterations. - parameter_names (List[str]): List of exploration parameter names. - start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. - These values must correspond to ``parameter_names``. - end (Union[float, list, tuple, np.ndarray]): Exploration parameter values rate for the last episode. - These values must correspond to ``parameter_names``. - """ - def __init__( - self, - max_iter: int, - parameter_names: List[str], - start: Union[float, list, tuple, np.ndarray], - end: Union[float, list, tuple, np.ndarray] - ): - super().__init__(max_iter) - self._parameter_names = parameter_names - if isinstance(start, float): - self._current_values = start * np.ones(len(self._parameter_names)) - elif isinstance(start, (list, tuple)): - self._current_values = np.asarray(start) - else: - self._current_values = start - - if isinstance(end, float): - end = end * np.ones(len(self._parameter_names)) - elif isinstance(end, (list, tuple)): - end = np.asarray(end) - - self._delta = (end - self._current_values) / (self._max_iter - 1) if self._max_iter != 1 else 0 - - def next_params(self): - current_values = self._current_values.copy() - self._current_values += self._delta - return dict(zip(self._parameter_names, current_values)) - - -class TwoPhaseLinearParameterScheduler(Scheduler): - """Exploration parameter generator based on two linear schedules joined together. - - Args: - max_iter (int): Maximum number of iterations. - parameter_names (List[str]): List of exploration parameter names. - split (float): The point where the switch from the first linear schedule to the second occurs. - start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. - These values must correspond to ``parameter_names``. - mid (Union[float, list, tuple, np.ndarray]): Exploration parameter values where the switch from the - first linear schedule to the second occurs. In other words, this is the exploration rate where the first - linear schedule ends and the second begins. These values must correspond to ``parameter_names``. - end (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the last episode. - These values must correspond to ``parameter_names``. - - Returns: - An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. - """ - def __init__( - self, - max_iter: int, - parameter_names: List[str], - split: float, - start: Union[float, list, tuple, np.ndarray], - mid: Union[float, list, tuple, np.ndarray], - end: Union[float, list, tuple, np.ndarray] - ): - if split < 0 or split > 1.0: - raise ValueError("split must be a float between 0 and 1.") - super().__init__(max_iter) - self._parameter_names = parameter_names - self._split = int(self._max_iter * split) - if isinstance(start, float): - self._current_values = start * np.ones(len(self._parameter_names)) - elif isinstance(start, (list, tuple)): - self._current_values = np.asarray(start) - else: - self._current_values = start - - if isinstance(mid, float): - mid = mid * np.ones(len(self._parameter_names)) - elif isinstance(mid, (list, tuple)): - mid = np.asarray(mid) - - if isinstance(end, float): - end = end * np.ones(len(self._parameter_names)) - elif isinstance(end, (list, tuple)): - end = np.asarray(end) - - self._delta_1 = (mid - self._current_values) / self._split if self._split else 0 - phase_2_eps = self._max_iter - self._split - 1 - self._delta_2 = (end - mid) / phase_2_eps if phase_2_eps else 0 - - def next_params(self): - current_values = self._current_values.copy() - self._current_values += self._delta_1 if self._iter_index < self._split else self._delta_2 - return dict(zip(self._parameter_names, current_values)) diff --git a/maro/rl/storage/__init__.py b/maro/rl/storage/__init__.py deleted file mode 100644 index 802ccc2d5..000000000 --- a/maro/rl/storage/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .abs_store import AbsStore -from .sampler import AbsSampler, UniformSampler -from .sampler_cls_index import SAMPLER_CLS -from .simple_store import SimpleStore - -__all__ = ["SAMPLER_CLS", "AbsSampler", "AbsStore", "SimpleStore", "UniformSampler"] diff --git a/maro/rl/storage/abs_store.py b/maro/rl/storage/abs_store.py deleted file mode 100644 index 35e6869c1..000000000 --- a/maro/rl/storage/abs_store.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from typing import Callable, Sequence - - -class AbsStore(ABC): - """A data store abstraction that supports get, put, update and sample operations.""" - - def __init__(self): - pass - - @abstractmethod - def get(self, indexes: Sequence): - """Get contents. - - Args: - indexes: A sequence of indexes to retrieve contents at. - Returns: - Retrieved contents. - """ - pass - - def put(self, contents: Sequence): - """Put new contents. - - Args: - contents (Sequence): Contents to be added to the store. - Returns: - The indexes where the newly added entries reside in the store. - """ - pass - - @abstractmethod - def update(self, indexes: Sequence, contents: Sequence): - """Update the store contents at given positions. - - Args: - indexes (Sequence): Positions where updates are to be made. - contents (Sequence): Item list, which has the same length as indexes. - Returns: - The indexes where store contents are updated. - """ - pass - - def filter(self, filters: Sequence[Callable]): - """Multi-filter method. - - The input to one filter is the output from the previous filter. - - Args: - filters (Sequence[Callable]): Filter list, each item is a lambda function, - e.g., [lambda d: d['a'] == 1 and d['b'] == 1]. - Returns: - Filtered indexes and corresponding objects. - """ - pass diff --git a/maro/rl/storage/sampler.py b/maro/rl/storage/sampler.py deleted file mode 100644 index 19135ee7d..000000000 --- a/maro/rl/storage/sampler.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - -import numpy as np - -from .abs_store import AbsStore - - -class AbsSampler(ABC): - def __init__(self, data: AbsStore): - self.data = data - - @abstractmethod - def sample(self, size: int): - raise NotImplementedError - - @abstractmethod - def update(self): - """Update statistics used for sampling.""" - pass - - -class UniformSampler(AbsSampler): - def __init__(self, data, replace: bool = True): - super().__init__(data) - self.replace = replace - - def sample(self, size: int): - """ - Obtain a random sample from the experience pool. - - Args: - size (int): Sample sizes for each round of sampling in the chain. If this is a single integer, it is - used as the sample size for all samplers in the chain. - weights (Union[list, np.ndarray]): Sampling weights. - replace (bool): If True, sampling is performed with replacement. Defaults to True. - Returns: - Sampled indexes and the corresponding objects, - e.g., [1, 2, 3], ['a', 'b', 'c']. - """ - indexes = np.random.choice(len(self.data), size=size, replace=self.replace) - return indexes, self.data.get(indexes=indexes) - - def update(self, indexes, values): - pass diff --git a/maro/rl/storage/simple_store.py b/maro/rl/storage/simple_store.py deleted file mode 100644 index 4e6b58dc9..000000000 --- a/maro/rl/storage/simple_store.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from enum import Enum -from typing import Callable, Dict, List, Tuple, Union - -import numpy as np - -from maro.utils import clone -from maro.utils.exception.rl_toolkit_exception import StoreMisalignment - -from .abs_store import AbsStore - - -class SimpleStore(AbsStore): - """ - An implementation of ``AbsStore`` for experience storage in RL. - - This implementation uses a dictionary of lists as the internal data structure. The objects for each key - are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during - put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited - and limited storage are supported. - - Args: - keys (list): Keys to identify the stored lists of objects. - capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. - overwrite_type (str): If storage capacity is bounded, this specifies how existing entries - are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - - "rolling", where overwrite occurs sequentially with wrap-around. - - "random", where overwrite occurs randomly among filled positions. - Alternatively, the user may also specify overwrite positions (see ``put``). - """ - def __init__(self, keys: list, capacity: int = -1, overwrite_type: str = None): - super().__init__() - if overwrite_type not in {"rolling", "random"}: - raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") - self.keys = keys - self._capacity = capacity - self._overwrite_type = overwrite_type - self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self.keys} - self._size = 0 - - def __len__(self): - return self._size - - def __getitem__(self, index: int): - return {k: lst[index] for k, lst in self.data.items()} - - @property - def capacity(self): - """Store capacity. - - If negative, the store grows without bound. Otherwise, the number of items in the store will not exceed - this capacity. - """ - return self._capacity - - @property - def overwrite_type(self): - """An string indicating the overwrite behavior when the store capacity is exceeded.""" - return self._overwrite_type - - def get(self, indexes: [int] = None) -> dict: - if indexes is None: - return self.data - return {k: [self.data[k][i] for i in indexes] for k in self.data} - - def put(self, contents: Dict[str, List], overwrite_indexes: list = None) -> List[int]: - """Put new contents in the store. - - Args: - contents (dict): Dictionary of items to add to the store. If the store is not empty, this must have the - same keys as the store itself. Otherwise an ``StoreMisalignment`` will be raised. - overwrite_indexes (list, optional): Indexes where the contents are to be overwritten. This is only - used when the store has a fixed capacity and putting ``contents`` in the store would exceed this - capacity. If this is None and overwriting is necessary, rolling or random overwriting will be done - according to the ``overwrite`` property. Defaults to None. - Returns: - The indexes where the newly added entries reside in the store. - """ - if len(self.data) > 0: - expected_keys, actual_keys = set(self.data.keys()), set(contents.keys()) - if expected_keys != actual_keys: - raise StoreMisalignment(f"expected keys {expected_keys}, got {actual_keys}") - self.validate(contents) - added = contents[next(iter(contents))] - added_size = len(added) if isinstance(added, list) else 1 - if self._capacity == -1: - for key, val in contents.items(): - self.data[key].extend(val) - self._size += added_size - return list(range(self._size - added_size, self._size)) - else: - write_indexes = self._get_update_indexes(added_size, overwrite_indexes=overwrite_indexes) - self.update(write_indexes, contents) - self._size = min(self._capacity, self._size + added_size) - return write_indexes - - def update(self, indexes: list, contents: Dict[str, List]): - """ - Update contents at given positions. - - Args: - indexes (list): Positions where updates are to be made. - contents (dict): Contents to write to the internal store at given positions. It is subject to - uniformity checks to ensure that all values have the same length. - - Returns: - The indexes where store contents are updated. - """ - self.validate(contents) - for key, val in contents.items(): - for index, value in zip(indexes, val): - self.data[key][index] = value - - return indexes - - def clear(self): - """Empty the store.""" - self.data = {key: [] if self._capacity == -1 else [None] * self._capacity for key in self.keys} - self._size = 0 - - def dumps(self): - """Return a deep copy of store contents.""" - return clone(dict(self.data)) - - def get_by_key(self, key): - """Get the contents of the store corresponding to ``key``.""" - return self.data[key] - - def _get_update_indexes(self, added_size: int, overwrite_indexes=None): - if added_size > self._capacity: - raise ValueError("size of added items should not exceed the store capacity.") - - num_overwrites = self._size + added_size - self._capacity - if num_overwrites < 0: - return list(range(self._size, self._size + added_size)) - - if overwrite_indexes is not None: - write_indexes = list(range(self._size, self._capacity)) + list(overwrite_indexes) - else: - # follow the overwrite rule set at init - if self._overwrite_type == "rolling": - # using the negative index convention for convenience - start_index = self._size - self._capacity - write_indexes = list(range(start_index, start_index + added_size)) - else: - random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) - write_indexes = list(range(self._size, self._capacity)) + list(random_indexes) - - return write_indexes - - @staticmethod - def validate(contents: Dict[str, List]): - # Ensure that all values are lists of the same length. - if any(not isinstance(val, list) for val in contents.values()): - raise TypeError("All values must be of type 'list'") - - reference_val = contents[list(contents.keys())[0]] - if any(len(val) != len(reference_val) for val in contents.values()): - raise StoreMisalignment("values of contents should consist of lists of the same length") diff --git a/maro/rl/training/dispatcher.py b/maro/rl/training/dispatcher.py deleted file mode 100755 index b574e51ac..000000000 --- a/maro/rl/training/dispatcher.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import deque - -import zmq -from zmq.eventloop.ioloop import IOLoop -from zmq.eventloop.zmqstream import ZMQStream - - -class LRUQueue(object): - """LRUQueue class using ZMQStream/IOLoop for event dispatching. - - Code adapted from https://zguide.zeromq.org/docs/chapter3/#A-High-Level-API-for-ZeroMQ. - """ - def __init__(self): - self._context = zmq.Context.instance() - frontend = self._context.socket(zmq.ROUTER) - frontend.bind("tcp://127.0.0.1:50000") - backend = self._context.socket(zmq.ROUTER) - backend.bind("tcp://127.0.0.1:50001") - self._workers = deque() - - self._frontend = ZMQStream(frontend) - self._backend = ZMQStream(backend) - self._backend.on_recv(self._handle_backend) - - def _handle_backend(self, msg): - # Queue worker ID for LRU routing - worker, empty, client = msg[:3] - # add worker back to the list of workers - self._workers.append(worker) - assert empty == b"" - # Third frame is READY or else a client reply address - # If client reply, send rest back to frontend - if client != b"READY": - empty, reply = msg[3:] - assert empty == b"" - self._frontend.send_multipart([client, b'', reply]) - - # Start accepting frontend messages now that at least one worker is free. - self._frontend.on_recv(self._handle_frontend) - - def _handle_frontend(self, msg): - # Now get next client request, route to LRU worker - # Client request is [address][empty][request] - client, empty, request = msg - assert empty == b"" - # Dequeue and drop the next worker address - self._backend.send_multipart([self._workers.popleft(), b'', client, b'', request]) - if not self._workers: - # stop receiving until workers become available again - self._frontend.stop_on_recv() - - -def start_dispatcher(): - dispatcher = LRUQueue() - IOLoop.instance().start() diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py deleted file mode 100644 index 755aaa9aa..000000000 --- a/maro/rl/training/learner.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from collections import defaultdict -from os import getcwd -from typing import Callable, Dict, List, Union - -from maro.communication import Message, Proxy, SessionType -from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.policy import AbsCorePolicy, MultiAgentPolicy -from maro.utils import Logger - -from .actor_manager import ActorManager -from .message_enums import MsgTag, MsgKey - - -class Learner(object): - """Learner class for distributed training. - - Args: - policy (MultiAgentPolicy): Learning agents. - - """ - def __init__( - self, - policy: MultiAgentPolicy, - env: AbsEnvWrapper, - num_episodes: int, - eval_env: AbsEnvWrapper = None, - actor_manager: ActorManager = None, - num_eval_actors: int = 1, - policy_update_interval: int = -1, - eval_points: Union[int, List[int]] = None, - required_actor_finishes: str = None, - discard_stale_experiences: bool = True, - log_env_metrics: bool = True, - log_dir: str = getcwd(), - end_of_training_kwargs: dict = None - ): - if env is None and actor_manager is None: - raise Exception("env and actor_manager cannot both be None") - - self._logger = Logger("LEARNER", dump_folder=log_dir) - self.policy = policy - self.env = env - self.num_episodes = num_episodes - self.eval_env = eval_env if eval_env else self.env - self.policy_update_interval = policy_update_interval - - if actor_manager: - self._logger.info( - "Found actor manager. Roll-outs will be performed by remote actors. Local env will not be used." - ) - self.actor_manager = actor_manager - - self.num_eval_actors = num_eval_actors - - # evaluation points - if eval_points is None: - eval_points = [] - elif isinstance(eval_points, int): - num_eval_points = num_episodes // eval_points - eval_points = [eval_points * i for i in range(1, num_eval_points + 1)] - - self.eval_points = eval_points - self.eval_points.sort() - if not self.eval_points or num_episodes != self.eval_points[-1]: - self.eval_points.append(num_episodes) - - self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_points}") - self._eval_point_index = 0 - - # distributed roll-out - self.actor_manager = actor_manager - if self.actor_manager: - if required_actor_finishes and required_actor_finishes > self.actor_manager.num_actors: - raise ValueError("required_actor_finishes cannot exceed the number of available actors") - - if required_actor_finishes is None: - required_actor_finishes = self.actor_manager.num_actors - self._logger.info(f"Required number of actor finishes is set to {required_actor_finishes}") - - self.required_actor_finishes = required_actor_finishes - self.discard_stale_experiences = discard_stale_experiences - - self.end_of_training_kwargs = end_of_training_kwargs if end_of_training_kwargs else {} - self._log_env_metrics = log_env_metrics - self._total_learning_time = 0 - self._total_env_steps = 0 - self._total_experiences_collected = 0 - - def run(self): - for ep in range(1, self.num_episodes + 1): - self.train(ep) - if ep == self.eval_points[self._eval_point_index]: - self._eval_point_index += 1 - self.evaluate(self._eval_point_index) - - if self.actor_manager: - self.actor_manager.exit() - - def train(self, ep: int): - # local mode - if not self.actor_manager: - self._train_local(ep) - else: - self._train_remote(ep) - - def evaluate(self, ep: int): - self._logger.info("Evaluating...") - if self.eval_env: - self.policy.eval_mode() - self.eval_env.save_replay = False - self.eval_env.reset() - self.eval_env.start() # get initial state - while self.eval_env.state: - action = self.policy.choose_action(self.eval_env.state) - self.eval_env.step(action) - - if not self.eval_env.state: - self._logger.info(f"total reward: {self.eval_env.total_reward}") - - if self._log_env_metrics: - self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") - else: - self.actor_manager.evaluate(ep, self.policy.state(), self.num_eval_actors) - - def _train_local(self, ep: int): - t0 = time.time() - segement_index = 1 - self._logger.info(f"Training episode {ep}") - self._logger.debug(f"exploration parameters: {self.policy.exploration_params}") - self.policy.train_mode() - self.env.save_replay = True - self.env.reset() - self.env.start() # get initial state - while self.env.state: - action = self.policy.choose_action(self.env.state) - self.env.step(action) - if ( - not self.env.state or - self.policy_update_interval != -1 and self.env.step_index % self.policy_update_interval == 0 - ): - exp_by_agent = self.env.get_experiences() - tl0 = time.time() - self.policy.store_experiences(exp_by_agent) - updated_policy_ids = self.policy.update() - if updated_policy_ids: - self._logger.info(f"updated policies {updated_policy_ids}") - self.end_of_training(ep, segement_index, **self.end_of_training_kwargs) - self._total_learning_time += time.time() - tl0 - self._total_env_steps += self.policy_update_interval - self._total_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) - self._logger.debug(f"total running time: {time.time() - t0}") - self._logger.debug(f"total learning time: {self._total_learning_time}") - self._logger.debug(f"total env steps: {self._total_env_steps}") - self._logger.debug(f"total experiences collected: {self._total_experiences_collected}") - if not self.env.state: - self._logger.info(f"total reward: {self.env.total_reward}") - - segement_index += 1 - - self.policy.exploration_step() - if self._log_env_metrics: - self._logger.info(f"ep {ep}: {self.env.metrics}") - - def _train_remote(self, ep: int): - t0 = time.time() - updated_policy_ids, num_actor_finishes, segment_index = list(self.policy.policy_dict.keys()), 0, 1 - while num_actor_finishes < self.required_actor_finishes: - for exp, done in self.actor_manager.collect( - ep, segment_index, self.policy_update_interval, - policy_dict=self.policy.state(), - required_actor_finishes=self.required_actor_finishes, - discard_stale_experiences=self.discard_stale_experiences - ): - tl0 = time.time() - self.policy.store_experiences(exp) - updated_policy_ids = self.policy.update() - if updated_policy_ids: - self._logger.info(f"updated policies {updated_policy_ids}") - self.end_of_training(ep, segment_index, **self.end_of_training_kwargs) - num_actor_finishes += done - self._total_learning_time += time.time() - tl0 - self._logger.debug(f"running time: {time.time() - t0}") - self._logger.debug(f"learning time: {self._total_learning_time}") - self._logger.debug(f"env steps: {self.actor_manager.total_env_steps}") - self._logger.debug(f"experiences collected: {self.actor_manager.total_experiences_collected}") - - segment_index += 1 - - def end_of_training(self, ep: int, segment: int, **kwargs): - pass diff --git a/maro/rl/training/test.py b/maro/rl/training/test.py deleted file mode 100644 index 7335f369b..000000000 --- a/maro/rl/training/test.py +++ /dev/null @@ -1,17 +0,0 @@ -import random -import time -from functools import reduce - -pool = range(100) -n = 100000 - -l = [random.choices(pool, k=10) for _ in range(n)] - -t0 = time.time() -for vals in l: - reduce(lambda x, y: x + y, vals) -t1 = time.time() -[reduce(lambda x, y: x + y, vals) for vals in l] -t2 = time.time() - -print(t1 - t0, t2 - t1) diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py deleted file mode 100755 index c3ee0aa01..000000000 --- a/maro/rl/training/trainer.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import pickle -from collections import deque - -import zmq -from zmq.eventloop.ioloop import IOLoop -from zmq.eventloop.zmqstream import ZMQStream - - -def trainer(id_: str): - socket = zmq.Context().socket(zmq.REQ) - socket.setsockopt_string(zmq.IDENTITY, f"Trainer_{id_}") - socket.connect("tcp://127.0.0.1:50001") - socket = ZMQStream(socket) - socket.send(b"READY") # register to the dispatcher - - def train(sock, msg): - client, _, request = msg - request = pickle.loads(request) - info = request["agent"].step(*request["args"], **request["kwargs"]) - request.update({"model": request["agent"].dump_model(), "info": info}) - del request["agent"] - del request["args"] - del request["kwargs"] - sock.send_multipart([client, b"", pickle.dumps(request)]) - - socket.on_recv_stream(train) - IOLoop.instance().start() From 1ed83d448376c774f48bf182ddaeeb62c9f8469b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 10 May 2021 15:02:20 +0800 Subject: [PATCH 227/482] rename agent type to string, expose facility or unit that marked with agent type as agents --- .../supply_chain/topologies/sample/config.yml | 21 +++++++++++++----- .../simulator/scenarios/supply_chain/world.py | 22 ++++++++++--------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml index f34f7ca3c..6cc394c9d 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml @@ -23,11 +23,14 @@ facility_definitions: is_template: true # config will be passed to generator as parameters config: - agent_type: 4 + agent_type: "product" consumer: class: "ConsumerUnit" + config: + agent_type: "consumer" config: - agent_type: 1 + # mark current unit/facility as a agent, out side may use this to choose policy. + agent_type: "warehouse" SupplierFacility: &supplier_facility class: "SupplierFacility" @@ -40,13 +43,17 @@ facility_definitions: class: "ProductUnit" is_template: true config: - agent_type: 3 + agent_type: "product" consumer: class: "ConsumerUnit" + config: + agent_type: "consumer" manufacture: class: "ManufactureUnit" + config: + agent_type: "producer" config: - agent_type: 0 + agent_type: "supplier" RetailerFacility: &retailer_facility class: "RetailerFacility" @@ -57,15 +64,17 @@ facility_definitions: class: "StoreProductUnit" is_template: true config: - agent_type: 5 + agent_type: "product" consumer: class: "ConsumerUnit" + config: + agent_type: "consumer" seller: class: "SellerUnit" config: sale_hist_len: 4 config: - agent_type: 2 + agent_type: "retailer" # common entity/unit definition as reference to simplify the file. normal_vehicle: &normal_vehicle diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 72e9b9783..4fe3f15f9 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -270,24 +270,26 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio # Collection agent list for facility in self.facilities.values(): - agent_type = facility.configs["agent_type"] + agent_type = facility.configs.get("agent_type", None) - self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id)) + if agent_type is not None: + self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id)) - self.agent_type_dict[agent_type] = True + self.agent_type_dict[agent_type] = type(facility).__name__ - for sku in facility.skus.values(): - self.max_price = max(self.max_price, sku.price) + for sku in facility.skus.values(): + self.max_price = max(self.max_price, sku.price) + # Find units that contains agent type to expose as agent. for unit in self.units.values(): - if issubclass(type(unit), ProductUnit): - agent_type = unit.config["agent_type"] + agent_type = unit.config.get("agent_type", None) - # unit or facility id, agent type, is facility, sku info, facility id + if agent_type is not None: + # Unit or facility id, agent type, is facility, sku info, facility id. self.agent_list.append( AgentInfo(unit.id, agent_type, False, unit.facility.skus[unit.product_id], unit.facility.id)) - self.agent_type_dict[agent_type] = True + self.agent_type_dict[agent_type] = type(unit).__name__ def build_unit_by_type(self, unit_def: UnitDef, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): unit = unit_def.class_type() @@ -403,7 +405,7 @@ def get_node_mapping(self): id2index_mapping[unit_id] = (None, None, unit.facility.id, sku) return { - "agent_types": [k for k in self.agent_type_dict.keys()], + "agent_types": {k:v for k,v in self.agent_type_dict.items()}, "unit_mapping": id2index_mapping, "skus": {sku.id: sku for sku in self._sku_collection.values()}, "facilities": facility_info_dict, From 30d0c92635c389a17227a2e26a533fe0c7f4cbb4 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 10 May 2021 15:36:13 +0800 Subject: [PATCH 228/482] change1 --- examples/supply_chain/env_wrapper.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 8008dd4da..03a9ebdda 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -35,7 +35,7 @@ def out_of_stock(f_state): } # State extracted. -keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', +keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'constraint_idx', 'is_accepted', 'consumption_hist']), ('storage_capacity', ['storage_utilization']), ('sale_gamma', ['sale_std', @@ -47,6 +47,9 @@ def out_of_stock(f_state): 'inventory_rop']), ('max_price', ['sku_price', 'sku_cost'])] +# Sku related agent types +sku_agent_types = {"consumer", "producer", "product"} + class UnitBaseInfo: id: int = None @@ -190,8 +193,7 @@ def get_state(self, event): in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] - self._facility_in_transit_orders[facility_id] = [ - 0] * self._sku_number + self._facility_in_transit_orders[facility_id] = [0] * self._sku_number for sku_id, number in in_transit_orders.items(): self._facility_in_transit_orders[facility_id][sku_id] = number @@ -228,8 +230,8 @@ def get_state(self, event): np_state = self._serialize_state(state) - final_state[f"consumer.{agent_info.id}"] = np_state - final_state[f"producer.{agent_info.id}"] = np_state + # agent_info.agent_type -> policy + final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state return final_state @@ -317,12 +319,15 @@ def _update_storage_features(self, state, agent_info): state['storage_utilization'] = self._facility_product_utilization[facility_id] def _update_sale_features(self, state, agent_info): - if agent_info.is_facility: + if agent_info.agent_type not in sku_agent_types: return - product_metrics = self._cur_metrics["products"][agent_info.id] + # Get product unit id for current agent. + product_unit_id = agent_info.id if agent_info.agent_type == "product" else agent_info.parent_id + + product_metrics = self._cur_metrics["products"][product_unit_id] - # for product unit only + # TODO: shall we keep this for both consumer and producer in same product unit? state['sale_mean'] = product_metrics["sale_mean"] state['sale_std'] = product_metrics["sale_std"] @@ -346,7 +351,7 @@ def _update_sale_features(self, state, agent_info): seller_states = self._cur_seller_states[:, seller_index, :] - # for total demand, we need latest one. + # For total demand, we need latest one. state['total_backlog_demand'] = seller_states[:, 0][-1][0] state['sale_hist'] = list(seller_states[:, 1].flatten()) state['backlog_demand_hist'] = list(seller_states[:, 2]) @@ -364,6 +369,7 @@ def _update_consumer_features(self, state, agent_info): if agent_info.is_facility: return + # TODO: shall ignore following for other agent type? facility = self.facility_levels[agent_info.facility_id] product_info = facility[agent_info.sku.id] From beabfd31799824f24a523cfce95270c6b2c59958 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 10 May 2021 17:29:49 +0800 Subject: [PATCH 229/482] correct the agent list, and related reward and states --- examples/supply_chain/config.py | 5 +- examples/supply_chain/env_wrapper.py | 140 ++++++++++++------ examples/supply_chain/policies.py | 4 +- .../supply_chain/topologies/random/config.yml | 19 ++- .../topologies/sample1/config.yml | 20 ++- maro/rl/policy/policy.py | 5 +- .../supply_chain/datamodels/product.py | 3 + .../supply_chain/topologies/sample/config.yml | 6 +- .../scenarios/supply_chain/units/product.py | 35 +++++ .../simulator/scenarios/supply_chain/world.py | 18 ++- 10 files changed, 184 insertions(+), 71 deletions(-) diff --git a/examples/supply_chain/config.py b/examples/supply_chain/config.py index ab4987d14..ec09bde62 100644 --- a/examples/supply_chain/config.py +++ b/examples/supply_chain/config.py @@ -20,7 +20,6 @@ topology = config["env"]["topology"] config["env"]["topology"] = join(sc_code_dir, "topologies", topology) env = SCEnvWrapper(Env(**config["env"])) -consumer_agent_ids = [f"consumer.{info.id}" for info in env.agent_idx_list] -producer_agent_ids = [f"producer.{info.id}" for info in env.agent_idx_list] -config["agent_ids"] = consumer_agent_ids + producer_agent_ids + +config["agent_ids"] = [f"{info.agent_type}.{info.id}" for info in env.agent_idx_list] config["policy"]["consumer"]["model"]["network"]["input_dim"] = env.dim diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 221591ea2..05d3e937b 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -242,21 +242,27 @@ def get_state(self, event): return final_state def get_reward(self, tick=None, target_agents=None): - wc = self.env.configs.settings["global_reward_weight_consumer"] - parent_facility_balance = {} - for f_id, sheet in self.cur_balance_sheet_reward.items(): - if f_id in self.unit_2_facility_dict: - # it is a product unit - parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] - else: - parent_facility_balance[f_id] = sheet - - consumer_reward_by_facility = {f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in - self.cur_balance_sheet_reward.items()} + # wc = self.env.configs.settings["global_reward_weight_consumer"] + # parent_facility_balance = {} + # for f_id, sheet in self.cur_balance_sheet_reward.items(): + # if f_id in self.unit_2_facility_dict: + # # it is a product unit + # parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] + # else: + # parent_facility_balance[f_id] = sheet + + # TODO: or still same as before? but with the bsw is reward of consumer and producer, not product unit. + # TODO: or consumer is a special case here? + # consumer_reward_by_facility = {f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in + # self.cur_balance_sheet_reward.items()} + # + # return { + # **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, + # **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} + # } return { - **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, - **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} + f"{bwt[2]}.{f_id}": np.float32(bwt[1]) for f_id, bwt in self.cur_balance_sheet_reward.items() } def get_action(self, action_by_agent): @@ -300,7 +306,14 @@ def get_action(self, action_by_agent): sku = self._units_mapping[unit_id][3] reward_discount = 0 - env_action[unit_id] = ConsumerAction(unit_id, product_id, source_id, action_number, sku.vlt, reward_discount) + env_action[unit_id] = ConsumerAction( + unit_id, + product_id, + source_id, + action_number, + sku.vlt, + reward_discount + ) # manufacturer action elif agent_id.startswith("producer"): sku = self._units_mapping[unit_id][3] @@ -703,29 +716,30 @@ def __init__(self, env: Env): def calc(self): tick = self.env.tick + # consumer - consumer_bs_states = self.consumer_ss[tick::self.consumer_features].flatten().reshape(-1, len( - self.consumer_features)) + consumer_bs_states = self.consumer_ss[tick::self.consumer_features]\ + .flatten()\ + .reshape(-1, len(self.consumer_features)) # quantity * price consumer_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] # balance_sheet_profit = 0 # order_cost + order_product_cost - consumer_step_balance_sheet_loss = -1 * \ - (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) + consumer_step_balance_sheet_loss = -1 * (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) # consumer step reward: balance sheet los + profile * discount consumer_step_reward = consumer_step_balance_sheet_loss + \ consumer_profit * consumer_bs_states[:, 5] # seller - seller_bs_states = self.seller_ss[tick::self.seller_features].flatten( - ).reshape(-1, len(self.seller_features)) + seller_bs_states = self.seller_ss[tick::self.seller_features]\ + .flatten()\ + .reshape(-1, len(self.seller_features)) # profit = sold * price - seller_balance_sheet_profit = seller_bs_states[:, - 1] * seller_bs_states[:, 3] + seller_balance_sheet_profit = seller_bs_states[:, 1] * seller_bs_states[:, 3] # loss = demand * price * backlog_ratio seller_balance_sheet_loss = -1 * \ @@ -736,8 +750,9 @@ def calc(self): seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit # manufacture - man_bs_states = self.manufacture_ss[tick::self.manufacture_features].flatten().reshape(-1, len( - self.manufacture_features)) + man_bs_states = self.manufacture_ss[tick::self.manufacture_features]\ + .flatten()\ + .reshape(-1, len(self.manufacture_features)) # loss = manufacture number * cost man_balance_sheet_profit_loss = -1 * \ @@ -747,16 +762,16 @@ def calc(self): man_step_reward = man_balance_sheet_profit_loss # product - product_bs_states = self.product_ss[tick::self.product_features].flatten().reshape(-1, - len(self.product_features)) + product_bs_states = self.product_ss[tick::self.product_features]\ + .flatten()\ + .reshape(-1, len(self.product_features)) # product distribution loss = check order + delay order penalty product_distribution_balance_sheet_loss = -1 * \ (product_bs_states[:, 3] + product_bs_states[:, 4]) # product distribution profit = check order * price - product_distribution_balance_sheet_profit = product_bs_states[:, - 2] * product_bs_states[:, 1] + product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] # result we need product_step_reward = np.zeros((len(self.products, ))) @@ -766,13 +781,17 @@ def calc(self): # create product number mapping for storages storages_product_map = {} for storage_index in range(len(self.storage_ss)): - product_list = self.storage_ss[tick:storage_index:"product_list"].flatten( - ).astype(np.int) - product_number = self.storage_ss[tick:storage_index:"product_number"].flatten( - ).astype(np.int) + product_list = self.storage_ss[tick:storage_index:"product_list"]\ + .flatten()\ + .astype(np.int) + + product_number = self.storage_ss[tick:storage_index:"product_number"]\ + .flatten()\ + .astype(np.int) storages_product_map[storage_index] = { - pid: pnum for pid, pnum in zip(product_list, product_number)} + pid: pnum for pid, pnum in zip(product_list, product_number) + } # product balance sheet and reward # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss @@ -823,20 +842,22 @@ def calc(self): product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss # storage - storage_states = self.storage_ss[tick::self.storage_features].flatten( - ).reshape(-1, len(self.storage_features)) + storage_states = self.storage_ss[tick::self.storage_features]\ + .flatten()\ + .reshape(-1, len(self.storage_features)) # loss = (capacity-remaining space) * cost - storage_balance_sheet_loss = -1 * \ - (storage_states[:, 0] - storage_states[:, 1]) + storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) # vehicles - vehicle_states = self.vehicle_ss[tick::self.vehicle_features].flatten( - ).reshape(-1, len(self.vehicle_features)) + vehicle_states = self.vehicle_ss[tick::self.vehicle_features]\ + .flatten()\ + .reshape(-1, len(self.vehicle_features)) # loss = cost * payload vehicle_balance_sheet_loss = -1 * \ vehicle_states[:, 1] * vehicle_states[:, 2] + vehicle_step_reward = vehicle_balance_sheet_loss facility_balance_sheet_loss = np.zeros((len(self.facility_levels),)) @@ -864,20 +885,44 @@ def calc(self): facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] + # Final result for current tick, key is the facility/unit id, value is tuple of balance sheet and reward. result = {} + # For product units. for id, bs, rw in zip([item[0] for item in self.products], product_balance_sheet, product_step_reward): - result[id] = (bs, rw) + result[id] = (bs, rw, "product") self.total_balance_sheet[id] += bs facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit + # For facilities. for id, bs, rw in zip([item[0] for item in self.facility_levels], facility_balance_sheet, facility_step_reward): - result[id] = (bs, rw) + result[id] = (bs, rw, "facility") self.total_balance_sheet[id] += bs + # For consumers. + consumer_step_balance_sheet = consumer_profit + consumer_step_balance_sheet_loss + + for id, bs, rw in zip(consumer_bs_states[:, 0], consumer_step_balance_sheet, consumer_step_reward): + result[int(id)] = (bs, rw, "consumer") + + self.total_balance_sheet[id] += bs + + # For producers. + man_step_balance_sheet = man_balance_sheet_profit_loss + + for id, bs, rw in zip(man_bs_states[:, 0], man_step_balance_sheet, man_step_reward): + result[int(id)] = (bs, rw, "producer") + + self.total_balance_sheet[id] += bs + + # NOTE: add followings if you need. + # For storages. + # For distributions. + # For vehicles. + return result @@ -887,7 +932,7 @@ def calc(self): env = Env( scenario="supply_chain", - topology="random", + topology="sample", durations=100, max_snapshots=10) @@ -900,8 +945,11 @@ def calc(self): # cProfile.run("ss.get_state(None)", sort="cumtime") states = ss.get_state(None) - end_time = time() - - print("time cost:", end_time - start_time) + print(ss.cur_balance_sheet_reward) + # print(states) - print("dim:", ss.dim) + # end_time = time() + # + # print("time cost:", end_time - start_time) + # + # print("dim:", ss.dim) diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py index a76366ca6..3044f504c 100644 --- a/examples/supply_chain/policies.py +++ b/examples/supply_chain/policies.py @@ -41,7 +41,9 @@ def get_dqn_policy(config): # all consumers share the same underlying policy policy_dict = { "consumer": get_dqn_policy(config["consumer"]), - "producer": get_dqn_policy(config["consumer"]) + "producer": get_dqn_policy(config["consumer"]), + "facility": NullPolicy(), + "product": NullPolicy() } agent2policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} diff --git a/examples/supply_chain/topologies/random/config.yml b/examples/supply_chain/topologies/random/config.yml index 9c3e72409..f4f2af16c 100644 --- a/examples/supply_chain/topologies/random/config.yml +++ b/examples/supply_chain/topologies/random/config.yml @@ -4,9 +4,10 @@ facility_definitions: products: class: StoreProductUnit config: - agent_type: 5 + agent_type: product consumer: class: ConsumerUnit + config: consumer seller: class: SellerUnit config: @@ -16,7 +17,7 @@ facility_definitions: class: StorageUnit class: RetailerFacility config: - agent_type: 2 + agent_type: facility SupplierFacility: children: distribution: @@ -24,17 +25,21 @@ facility_definitions: products: class: ProductUnit config: - agent_type: 3 + agent_type: product consumer: class: ConsumerUnit + config: + agent_type: consumer manufacture: class: ManufactureUnit + config: + agent_type: producer is_template: true storage: class: StorageUnit class: SupplierFacility config: - agent_type: 0 + agent_type: facility WarehouseFacility: children: distribution: @@ -42,15 +47,17 @@ facility_definitions: products: class: ProductUnit config: - agent_type: 4 + agent_type: product consumer: class: ConsumerUnit + config: + agent_type: consumer is_template: true storage: class: StorageUnit class: WarehouseFacility config: - agent_type: 1 + agent_type: facility normal_vehicle: &id001 class: VehicleUnit config: diff --git a/examples/supply_chain/topologies/sample1/config.yml b/examples/supply_chain/topologies/sample1/config.yml index eadec95f9..ffcd92d45 100644 --- a/examples/supply_chain/topologies/sample1/config.yml +++ b/examples/supply_chain/topologies/sample1/config.yml @@ -23,11 +23,13 @@ facility_definitions: is_template: true # config will be passed to generator as parameters config: - agent_type: 4 + agent_type: "product" consumer: class: "ConsumerUnit" + config: + agent_type: "consumer" config: - agent_type: 1 + agent_type: "facility" SupplierFacility: &supplier_facility class: "SupplierFacility" @@ -40,13 +42,17 @@ facility_definitions: class: "ProductUnit" is_template: true config: - agent_type: 3 + agent_type: "product" consumer: class: "ConsumerUnit" + config: + agent_type: "consumer" manufacture: class: "ManufactureUnit" + config: + agent_type: "producer" config: - agent_type: 0 + agent_type: "facility" RetailerFacility: &retailer_facility class: "RetailerFacility" @@ -57,15 +63,17 @@ facility_definitions: class: "StoreProductUnit" is_template: true config: - agent_type: 5 + agent_type: "product" consumer: class: "ConsumerUnit" + config: + agent_type: "consumer" seller: class: "SellerUnit" config: sale_hist_len: 4 config: - agent_type: 2 + agent_type: "facility" # definition for outer retailer that read demand from csv files. diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index c1ed08af0..61f563fd3 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -35,6 +35,9 @@ class NullPolicy(AbsPolicy): def choose_action(self, state): return None + def update(self): + pass + class AbsCorePolicy(AbsPolicy): def __init__(self, experience_manager: AbsExperienceManager, config): @@ -95,7 +98,7 @@ def choose_action(self, state): this to an environment executable action. """ action = self.core_policy.choose_action(state) - return self.exploration(action) + return self.exploration(action) def update(self): """Algorithm-specific training logic. diff --git a/maro/simulator/scenarios/supply_chain/datamodels/product.py b/maro/simulator/scenarios/supply_chain/datamodels/product.py index 559e475c7..b69fcf9d2 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/product.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/product.py @@ -11,6 +11,9 @@ @node("product") class ProductDataModel(SkuDataModel): price = NodeAttribute(AttributeType.Float) + distribution_check_order = NodeAttribute(AttributeType.UInt) + distribution_transport_cost = NodeAttribute(AttributeType.Float) + distribution_delay_order_penalty = NodeAttribute(AttributeType.Float) def __init__(self): super(ProductDataModel, self).__init__() diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml index 6cc394c9d..40c01fedb 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml @@ -30,7 +30,7 @@ facility_definitions: agent_type: "consumer" config: # mark current unit/facility as a agent, out side may use this to choose policy. - agent_type: "warehouse" + agent_type: "facility" SupplierFacility: &supplier_facility class: "SupplierFacility" @@ -53,7 +53,7 @@ facility_definitions: config: agent_type: "producer" config: - agent_type: "supplier" + agent_type: "facility" RetailerFacility: &retailer_facility class: "RetailerFacility" @@ -74,7 +74,7 @@ facility_definitions: config: sale_hist_len: 4 config: - agent_type: "retailer" + agent_type: "facility" # common entity/unit definition as reference to simplify the file. normal_vehicle: &normal_vehicle diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index 1ef598b39..82bae15c4 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -29,6 +29,11 @@ class ProductUnit(SkuUnit): # Reference to facility's distribution unit. distribution: DistributionUnit = None + # Internal states to track distribution. + _checkin_order = 0 + _transport_cost = 0 + _delay_order_penalty = 0 + def initialize(self): super().initialize() @@ -40,16 +45,46 @@ def step(self, tick: int): for unit in self.children: unit.step(tick) + if self.distribution is not None: + self._checkin_order = self.distribution.check_in_order[self.product_id] + self._transport_cost = self.distribution.transportation_cost[self.product_id] + self._delay_order_penalty = self.distribution.delay_order_penalty[self.product_id] + + self.distribution.check_in_order[self.product_id] = 0 + self.distribution.transportation_cost[self.product_id] = 0 + self.distribution.delay_order_penalty[self.product_id] = 0 + def flush_states(self): for unit in self.children: unit.flush_states() + if self._checkin_order > 0: + self.data_model.distribution_check_order = self._checkin_order + + if self._transport_cost > 0: + self.data_model.distribution_transport_cost = self._transport_cost + + if self._delay_order_penalty > 0: + self.data_model.distribution_delay_order_penalty = self._delay_order_penalty + def post_step(self, tick: int): super().post_step(tick) for unit in self.children: unit.post_step(tick) + if self._checkin_order > 0: + self.data_model.distribution_check_order = 0 + self._checkin_order = 0 + + if self._transport_cost > 0: + self.data_model.distribution_transport_cost = 0 + self._transport_cost = 0 + + if self._delay_order_penalty > 0: + self.data_model.distribution_delay_order_penalty = 0 + self._delay_order_penalty = 0 + def reset(self): super().reset() diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 4fe3f15f9..afbb6caf7 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -15,7 +15,7 @@ from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef from .units import ProductUnit, SkuUnit, UnitBase -AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id")) +AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id", "parent_id")) class World: @@ -273,7 +273,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio agent_type = facility.configs.get("agent_type", None) if agent_type is not None: - self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id)) + self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id, None)) self.agent_type_dict[agent_type] = type(facility).__name__ @@ -285,9 +285,17 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio agent_type = unit.config.get("agent_type", None) if agent_type is not None: - # Unit or facility id, agent type, is facility, sku info, facility id. + # Unit or facility id, agent type, is facility, sku info, facility id, parent id. self.agent_list.append( - AgentInfo(unit.id, agent_type, False, unit.facility.skus[unit.product_id], unit.facility.id)) + AgentInfo( + unit.id, + agent_type, + False, + unit.facility.skus[unit.product_id], + unit.facility.id, + unit.parent.id + ) + ) self.agent_type_dict[agent_type] = type(unit).__name__ @@ -405,7 +413,7 @@ def get_node_mapping(self): id2index_mapping[unit_id] = (None, None, unit.facility.id, sku) return { - "agent_types": {k:v for k,v in self.agent_type_dict.items()}, + "agent_types": {k: v for k, v in self.agent_type_dict.items()}, "unit_mapping": id2index_mapping, "skus": {sku.id: sku for sku in self._sku_collection.values()}, "facilities": facility_info_dict, From eaf5791acfcd9d7fd30d574d5f543b6c180d8a4b Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 10 May 2021 17:40:19 +0800 Subject: [PATCH 230/482] rename skuunit to avoid mis-understand --- maro/simulator/scenarios/supply_chain/__init__.py | 2 +- maro/simulator/scenarios/supply_chain/units/__init__.py | 2 +- maro/simulator/scenarios/supply_chain/units/consumer.py | 4 ++-- .../supply_chain/units/{skuunit.py => extendunitbase.py} | 8 ++++---- .../simulator/scenarios/supply_chain/units/manufacture.py | 4 ++-- maro/simulator/scenarios/supply_chain/units/product.py | 4 ++-- maro/simulator/scenarios/supply_chain/units/seller.py | 4 ++-- maro/simulator/scenarios/supply_chain/units/storage.py | 5 ----- maro/simulator/scenarios/supply_chain/world.py | 4 ++-- 9 files changed, 16 insertions(+), 21 deletions(-) rename maro/simulator/scenarios/supply_chain/units/{skuunit.py => extendunitbase.py} (70%) diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index 03990fd5c..b6e6162ff 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -7,4 +7,4 @@ ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel ) from .facilities import FacilityBase, RetailerFacility, SupplierFacility, WarehouseFacility -from .units import ConsumerUnit, DistributionUnit, ProductUnit, SellerUnit, SkuUnit, StorageUnit, UnitBase, VehicleUnit +from .units import ConsumerUnit, DistributionUnit, ProductUnit, SellerUnit, ExtendUnitBase, StorageUnit, UnitBase, VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index c72c3b5d6..6fef50d02 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -9,7 +9,7 @@ from .product import ProductUnit from .seller import SellerUnit from .simplemanufacture import SimpleManufactureUnit -from .skuunit import SkuUnit +from .extendunitbase import ExtendUnitBase from .storage import StorageUnit from .storeproduct import StoreProductUnit from .unitbase import UnitBase diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 3e9b7c3a0..2b06eafa0 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -7,10 +7,10 @@ from scipy.ndimage.interpolation import shift from .order import Order -from .skuunit import SkuUnit +from .extendunitbase import ExtendUnitBase -class ConsumerUnit(SkuUnit): +class ConsumerUnit(ExtendUnitBase): """Consumer unit used to generate order to purchase from upstream by action.""" def __init__(self): diff --git a/maro/simulator/scenarios/supply_chain/units/skuunit.py b/maro/simulator/scenarios/supply_chain/units/extendunitbase.py similarity index 70% rename from maro/simulator/scenarios/supply_chain/units/skuunit.py rename to maro/simulator/scenarios/supply_chain/units/extendunitbase.py index da43bbe80..a8106012d 100644 --- a/maro/simulator/scenarios/supply_chain/units/skuunit.py +++ b/maro/simulator/scenarios/supply_chain/units/extendunitbase.py @@ -5,20 +5,20 @@ from .unitbase import UnitBase -class SkuUnit(UnitBase): - """A sku related unit.""" +class ExtendUnitBase(UnitBase): + """A base of sku related unit.""" # Product id (sku id), 0 means invalid. product_id: int = 0 def initialize(self): - super(SkuUnit, self).initialize() + super(ExtendUnitBase, self).initialize() if self.data_model is not None: self.data_model.set_product_id(self.product_id, self.parent.id) def get_unit_info(self) -> dict: - info = super(SkuUnit, self).get_unit_info() + info = super(ExtendUnitBase, self).get_unit_info() info["sku_id"] = self.product_id diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index 228828922..e54da60fa 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -2,10 +2,10 @@ # Licensed under the MIT license. -from .skuunit import SkuUnit +from .extendunitbase import ExtendUnitBase -class ManufactureUnit(SkuUnit): +class ManufactureUnit(ExtendUnitBase): """Unit that used to produce certain product(sku) with consume specified source skus. One manufacture unit per sku. diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index 82bae15c4..e1349e6f1 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -7,11 +7,11 @@ from .distribution import DistributionUnit from .manufacture import ManufactureUnit from .seller import SellerUnit -from .skuunit import SkuUnit +from .extendunitbase import ExtendUnitBase from .storage import StorageUnit -class ProductUnit(SkuUnit): +class ProductUnit(ExtendUnitBase): """Unit that used to group units of one special sku, usually contains consumer, seller and manufacture.""" # Consumer unit of current sku. diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index d2323cdcb..3db33b376 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -4,10 +4,10 @@ import numpy as np -from .skuunit import SkuUnit +from .extendunitbase import ExtendUnitBase -class SellerUnit(SkuUnit): +class SellerUnit(ExtendUnitBase): """ Unit that used to generate product consume demand, and move demand product from current storage. """ diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index c136bc5ee..827b7e992 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -13,12 +13,7 @@ class StorageUnit(UnitBase): def __init__(self): super().__init__() - # We use these variables to hold changes at python side, flash to frame before taking snapshot. - # self.product_number = [] - # self.product_list = [] - # Used to map from product id to slot index. - # self.product_index_mapping: Dict[int, int] = {} self.capacity = 0 self.remaining_space = 0 self.product_level = {} diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index afbb6caf7..42058b5db 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -13,7 +13,7 @@ from .facilities import FacilityBase from .frame_builder import build_frame from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef -from .units import ProductUnit, SkuUnit, UnitBase +from .units import ProductUnit, ExtendUnitBase, UnitBase AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id", "parent_id")) @@ -404,7 +404,7 @@ def get_node_mapping(self): for unit_id, unit in self.units.items(): sku = None - if isinstance(unit, SkuUnit): + if isinstance(unit, ExtendUnitBase): sku = unit.facility.skus[unit.product_id] if unit.data_model is not None: From 3399f32e5fa41d8d800e683b337b592e88c031cb Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 10 May 2021 18:00:01 +0800 Subject: [PATCH 231/482] remove un-used import --- maro/simulator/scenarios/supply_chain/world.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 42058b5db..2f2da7cf8 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -13,7 +13,7 @@ from .facilities import FacilityBase from .frame_builder import build_frame from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef -from .units import ProductUnit, ExtendUnitBase, UnitBase +from .units import ExtendUnitBase, UnitBase AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id", "parent_id")) @@ -300,6 +300,16 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio self.agent_type_dict[agent_type] = type(unit).__name__ def build_unit_by_type(self, unit_def: UnitDef, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): + """Build an unit by its type. + + Args: + unit_def (UnitDef): Definition of this unit. + parent (Union[FacilityBase, UnitBase]): Parent of this unit. + facility (FacilityBase): Facility this unit belongs to. + + Returns: + UnitBase: Unit instance. + """ unit = unit_def.class_type() unit.id = self._gen_id() From 2a3d085e87121d04ce9e103d37d3cef6e3558b91 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 10 May 2021 18:33:11 +0800 Subject: [PATCH 232/482] skumodel to extend --- examples/supply_chain/env_wrapper.py | 102 ++++++++++-------- .../supply_chain/datamodels/consumer.py | 4 +- .../datamodels/{skumodel.py => extend.py} | 6 +- .../supply_chain/datamodels/manufacture.py | 4 +- .../supply_chain/datamodels/product.py | 4 +- .../supply_chain/datamodels/seller.py | 4 +- 6 files changed, 66 insertions(+), 58 deletions(-) rename maro/simulator/scenarios/supply_chain/datamodels/{skumodel.py => extend.py} (85%) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 05d3e937b..9c24b4c43 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -101,14 +101,15 @@ def __init__(self, env: Env): self._service_index_ppf_cache = {} # facility -> { - # data_model_index:int, - # storage:UnitBaseInfo, - # distribution: UnitBaseInfo, - # product_id: { - # consumer: UnitBaseInfo, - # seller: UnitBaseInfo, - # manufacture: UnitBaseInfo - # } + # data_model_index:int, + # storage:UnitBaseInfo, + # distribution: UnitBaseInfo, + # sku_id: { + # skuproduct: UnitBaseInfo, + # consumer: UnitBaseInfo, + # seller: UnitBaseInfo, + # manufacture: UnitBaseInfo + # } # } self.facility_levels = {} @@ -145,6 +146,13 @@ def __init__(self, env: Env): # dim for state self._dim = None + # use this to quick find relationship between units (consumer, manufacture, seller or product) and product unit. + # unit id -> (product unit id, facility id, seller id, consumer id, manufacture id) + self._unit2product_mapping = {} + + # agent (unit id) -> AgentInfo + self._agent_id2info_mapping = {} + # built internal helpers. self._build_internal_helpers() @@ -185,12 +193,17 @@ def get_state(self, event): self.cur_balance_sheet_reward = self.balance_cal.calc() self._cur_metrics = self.env.metrics - self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features].flatten( - ).reshape(-1, 2).astype(np.int) - self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"].flatten( - ).reshape(-1, len(self.consumer_ss)) - self._cur_seller_states = self.seller_ss[hist_ticks::seller_features].astype( - np.int) + self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features]\ + .flatten()\ + .reshape(-1, 2)\ + .astype(np.int) + + self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"]\ + .flatten()\ + .reshape(-1, len(self.consumer_ss)) + + self._cur_seller_states = self.seller_ss[hist_ticks::seller_features]\ + .astype(np.int) # facility level states for facility_id in self._facility_product_utilization: @@ -208,8 +221,9 @@ def get_state(self, event): # calculate storage info first, then use it later to speed up. for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"].flatten( - ).astype(np.int) + product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"]\ + .flatten()\ + .astype(np.int) for pid, index in self._storage_product_indices[facility_id].items(): product_number = product_numbers[index] @@ -225,13 +239,11 @@ def get_state(self, event): self._update_facility_features(state, agent_info) self._update_storage_features(state, agent_info) # bom do not need to update - # self._add_bom_features(state, agent_info) self._update_distribution_features(state, agent_info) self._update_sale_features(state, agent_info) # vlt do not need to update - # self._update_vlt_features(state, agent_info) self._update_consumer_features(state, agent_info) - # self._add_price_features(state, agent_info) + # price do not need to update self._update_global_features(state) np_state = self._serialize_state(state) @@ -242,27 +254,12 @@ def get_state(self, event): return final_state def get_reward(self, tick=None, target_agents=None): - # wc = self.env.configs.settings["global_reward_weight_consumer"] - # parent_facility_balance = {} - # for f_id, sheet in self.cur_balance_sheet_reward.items(): - # if f_id in self.unit_2_facility_dict: - # # it is a product unit - # parent_facility_balance[f_id] = self.cur_balance_sheet_reward[self.unit_2_facility_dict[f_id]] - # else: - # parent_facility_balance[f_id] = sheet - - # TODO: or still same as before? but with the bsw is reward of consumer and producer, not product unit. - # TODO: or consumer is a special case here? - # consumer_reward_by_facility = {f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in - # self.cur_balance_sheet_reward.items()} - # - # return { - # **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, - # **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} - # } - + # get related product, seller, consumer, manufacture unit id + # NOTE: this mapping does not contain facility id, so if id is not exist, then means it is a facility + # product_unit_id, facility_id, seller_id, consumer_id, producer_id = self._unit2product_mapping[id] return { - f"{bwt[2]}.{f_id}": np.float32(bwt[1]) for f_id, bwt in self.cur_balance_sheet_reward.items() + f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}": np.float32(bwt[1]) + for f_id, bwt in self.cur_balance_sheet_reward.items() } def get_action(self, action_by_agent): @@ -346,7 +343,6 @@ def _update_sale_features(self, state, agent_info): product_metrics = self._cur_metrics["products"][product_unit_id] - # TODO: shall we keep this for both consumer and producer in same product unit? state['sale_mean'] = product_metrics["sale_mean"] state['sale_std'] = product_metrics["sale_std"] @@ -388,7 +384,6 @@ def _update_consumer_features(self, state, agent_info): if agent_info.is_facility: return - # TODO: shall ignore following for other agent type? facility = self.facility_levels[agent_info.facility_id] product_info = facility[agent_info.sku.id] @@ -448,6 +443,9 @@ def _serialize_state(self, state): return np.asarray(result, dtype=np.float32) def _build_internal_helpers(self): + for agent_info in self.env.agent_idx_list: + self._agent_id2info_mapping[agent_info.id] = agent_info + # facility levels for facility_id, facility in self._summary["facilities"].items(): self.facility_levels[facility_id] = { @@ -515,6 +513,16 @@ def _build_internal_helpers(self): self.facility_levels[facility_id][product_id] = product_info + for unit in (seller, consumer, manufacture, product): + if unit is not None: + self._unit2product_mapping[unit["id"]] = ( + product["id"], + facility_id, + seller["id"] if seller is not None else None, + consumer["id"] if consumer is not None else None, + manufacture["id"] if manufacture is not None else None + ) + # create initial state structure self._build_init_state() @@ -890,7 +898,7 @@ def calc(self): # For product units. for id, bs, rw in zip([item[0] for item in self.products], product_balance_sheet, product_step_reward): - result[id] = (bs, rw, "product") + result[id] = (bs, rw) self.total_balance_sheet[id] += bs @@ -898,7 +906,7 @@ def calc(self): # For facilities. for id, bs, rw in zip([item[0] for item in self.facility_levels], facility_balance_sheet, facility_step_reward): - result[id] = (bs, rw, "facility") + result[id] = (bs, rw) self.total_balance_sheet[id] += bs @@ -906,7 +914,7 @@ def calc(self): consumer_step_balance_sheet = consumer_profit + consumer_step_balance_sheet_loss for id, bs, rw in zip(consumer_bs_states[:, 0], consumer_step_balance_sheet, consumer_step_reward): - result[int(id)] = (bs, rw, "consumer") + result[int(id)] = (bs, rw) self.total_balance_sheet[id] += bs @@ -914,7 +922,7 @@ def calc(self): man_step_balance_sheet = man_balance_sheet_profit_loss for id, bs, rw in zip(man_bs_states[:, 0], man_step_balance_sheet, man_step_reward): - result[int(id)] = (bs, rw, "producer") + result[int(id)] = (bs, rw) self.total_balance_sheet[id] += bs @@ -944,8 +952,8 @@ def calc(self): # cProfile.run("ss.get_state(None)", sort="cumtime") states = ss.get_state(None) - - print(ss.cur_balance_sheet_reward) + # print(env.agent_idx_list) + # print(ss.cur_balance_sheet_reward) # print(states) # end_time = time() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py index 81c3aafe1..5cd9855e5 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -5,11 +5,11 @@ from maro.backends.backend import AttributeType from maro.backends.frame import NodeAttribute, node -from .skumodel import SkuDataModel +from .extend import ExtendDataModel @node("consumer") -class ConsumerDataModel(SkuDataModel): +class ConsumerDataModel(ExtendDataModel): """Data model for consumer unit.""" total_purchased = NodeAttribute(AttributeType.UInt) total_received = NodeAttribute(AttributeType.UInt) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py b/maro/simulator/scenarios/supply_chain/datamodels/extend.py similarity index 85% rename from maro/simulator/scenarios/supply_chain/datamodels/skumodel.py rename to maro/simulator/scenarios/supply_chain/datamodels/extend.py index 17a165f26..e16c09c05 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/skumodel.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/extend.py @@ -8,7 +8,7 @@ from .base import DataModelBase -class SkuDataModel(DataModelBase): +class ExtendDataModel(DataModelBase): """Data model for sku related unit.""" # Product id of this consumer belongs to. product_id = NodeAttribute(AttributeType.UInt) @@ -17,13 +17,13 @@ class SkuDataModel(DataModelBase): product_unit_id = NodeAttribute(AttributeType.UInt) def __init__(self): - super(SkuDataModel, self).__init__() + super(ExtendDataModel, self).__init__() self._product_id = 0 self._product_unit_id = 0 def reset(self): - super(SkuDataModel, self).reset() + super(ExtendDataModel, self).reset() self.product_id = self._product_id self.product_unit_id = self._product_unit_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py index 18bd7defe..f2039c498 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -5,11 +5,11 @@ from maro.backends.backend import AttributeType from maro.backends.frame import NodeAttribute, node -from .skumodel import SkuDataModel +from .extend import ExtendDataModel @node("manufacture") -class ManufactureDataModel(SkuDataModel): +class ManufactureDataModel(ExtendDataModel): """Data model for manufacture unit.""" # Number per tick, different with original manufacturing cost, we just provide number, and cost # user can determine how to calculate the cost. diff --git a/maro/simulator/scenarios/supply_chain/datamodels/product.py b/maro/simulator/scenarios/supply_chain/datamodels/product.py index b69fcf9d2..3c3d4fa56 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/product.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/product.py @@ -5,11 +5,11 @@ from maro.backends.backend import AttributeType from maro.backends.frame import NodeAttribute, node -from .skumodel import SkuDataModel +from .extend import ExtendDataModel @node("product") -class ProductDataModel(SkuDataModel): +class ProductDataModel(ExtendDataModel): price = NodeAttribute(AttributeType.Float) distribution_check_order = NodeAttribute(AttributeType.UInt) distribution_transport_cost = NodeAttribute(AttributeType.Float) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py index e7d13dade..ce4b4b9b9 100644 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -5,11 +5,11 @@ from maro.backends.backend import AttributeType from maro.backends.frame import NodeAttribute, node -from .skumodel import SkuDataModel +from .extend import ExtendDataModel @node("seller") -class SellerDataModel(SkuDataModel): +class SellerDataModel(ExtendDataModel): """Data model for seller unit.""" demand = NodeAttribute(AttributeType.UInt) sold = NodeAttribute(AttributeType.UInt) From 0a4552c05a4b36a50f664daf30216908b338b08f Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 11 May 2021 03:10:19 +0000 Subject: [PATCH 233/482] merged env_wrapper and business engine changes and refined policy update schedule logic --- examples/supply_chain/config.py | 1 + examples/supply_chain/config.yml | 49 ++++++++++++++++++- examples/supply_chain/distributed_launcher.py | 8 +-- examples/supply_chain/policies.py | 40 +++++++++------ .../supply_chain/single_thread_launcher.py | 5 +- maro/rl/policy/policy.py | 4 -- maro/rl/training/actor.py | 13 +++-- maro/rl/training/distributed_learner.py | 29 ++++++++--- maro/rl/training/local_learner.py | 15 +++--- 9 files changed, 115 insertions(+), 49 deletions(-) diff --git a/examples/supply_chain/config.py b/examples/supply_chain/config.py index ec09bde62..682226615 100644 --- a/examples/supply_chain/config.py +++ b/examples/supply_chain/config.py @@ -23,3 +23,4 @@ config["agent_ids"] = [f"{info.agent_type}.{info.id}" for info in env.agent_idx_list] config["policy"]["consumer"]["model"]["network"]["input_dim"] = env.dim +config["policy"]["producer"]["model"]["network"]["input_dim"] = env.dim diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index e3903fba0..422f901d7 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -17,7 +17,6 @@ log_env_metrics: false policy: consumer: algorithm: dqn - share_model: true model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. device: cpu network: @@ -51,6 +50,52 @@ policy: overwrite_type: random batch_size: 128 replace: true + update_schedule: + type: step # "step" or "episode" + args: + start_ep: 3 # must be a positive number since episode is 1-based. + interval: 2 + end_ep_update: true + producer: + algorithm: dqn + model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. + device: cpu + network: + hidden_dims: + - 16 + - 8 + output_dim: 10 + activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. + optim_params: + lr: 0.001 + algorithm_config: + reward_discount: .9 + train_epochs: 10 + gradient_iters: 1 + loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. + target_update_freq: 5 # How many training iteration, to update DQN target model + soft_update_coefficient: 0.1 + double: false # whether to enable double DQN + experience_manager: + capacity: 50000 # experience memory size + # This determines how existing experiences are replaced when adding new experiences to a full experience + # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, + # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. + overwrite_type: random + batch_size: 128 + replace: true + update_schedule: + type: episode # "step" or "episode" + args: + start: 2 # must be a positive number since episode is 1-based + interval: 2 exploration: initial_value: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. final_value: 0.0 @@ -62,7 +107,7 @@ distributed: # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make # sure that a local redis server is running and listening on the port specified by "redis_port". - redis_host: localhost + redis_host: maro-redis redis_port: 6379 # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent # slow actors from dragging down the whole process. diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index c95c4a9de..fec5d0e05 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -7,7 +7,7 @@ from os import getenv, makedirs from os.path import dirname, join, realpath -from maro.rl import Actor, ActorManager, EpisodeBasedSchedule, MultiPolicyUpdateSchedule, StepBasedSchedule +from maro.rl import Actor, ActorManager from maro.simulator import Env from maro.utils import set_seeds @@ -17,7 +17,7 @@ from env_wrapper import SCEnvWrapper from exploration import exploration_dict, agent2exploration from learner import SCLearner -from policies import policy_dict, agent2policy +from policies import agent2policy, policy_dict, policy_update_schedule # for distributed / multi-process training @@ -39,10 +39,6 @@ def sc_learner(): log_dir=log_dir ) - policy_update_schedule = MultiPolicyUpdateSchedule( - {"consumer": StepBasedSchedule(3, 2, True), "producer": EpisodeBasedSchedule(2, 3)} - ) - # create a learner to start training learner = SCLearner( policy_dict, agent2policy, config["num_episodes"], policy_update_schedule, diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py index 3044f504c..e07b76ea8 100644 --- a/examples/supply_chain/policies.py +++ b/examples/supply_chain/policies.py @@ -9,8 +9,8 @@ import torch from maro.rl import ( - DQN, DQNConfig, EpisodeBasedSchedule, FullyConnectedBlock, NullPolicy, - OptimOption, QNetForDiscreteActionSpace, StepBasedSchedule, UniformSampler + DQN, DQNConfig, EpisodeBasedSchedule, FullyConnectedBlock, NullPolicy, OptimOption, QNetForDiscreteActionSpace, + StepBasedSchedule, UniformSampler ) sc_code_dir = dirname(realpath(__file__)) @@ -18,7 +18,7 @@ from config import config agent_ids = config["agent_ids"] -config = config["policy"] +policy_ids = ["consumer", "producer", "facility", "product"] class SimpleQNet(QNetForDiscreteActionSpace): @@ -29,21 +29,33 @@ def forward(self, states): return self.component.forward(states) -def get_dqn_policy(config): +def get_dqn_policy(cfg): q_net = SimpleQNet( - FullyConnectedBlock(**config["model"]["network"]), - optim_option=OptimOption(**config["model"]["optimization"]), - device=config["model"]["device"] + FullyConnectedBlock(**cfg["model"]["network"]), + optim_option=OptimOption(**cfg["model"]["optimization"]), + device=cfg["model"]["device"] ) - experience_manager = UniformSampler(**config["experience_manager"]) - return DQN(q_net, experience_manager, DQNConfig(**config["algorithm_config"])) + experience_manager = UniformSampler(**cfg["experience_manager"]) + return DQN(q_net, experience_manager, DQNConfig(**cfg["algorithm_config"])) -# all consumers share the same underlying policy + +null_policy = NullPolicy() policy_dict = { - "consumer": get_dqn_policy(config["consumer"]), - "producer": get_dqn_policy(config["consumer"]), - "facility": NullPolicy(), - "product": NullPolicy() + policy_id: get_dqn_policy(config["policy"][policy_id]) if policy_id in config["policy"] else null_policy + for policy_id in policy_ids } agent2policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} + +# update schedules +schedule_type = {"step": StepBasedSchedule, "episode": EpisodeBasedSchedule} + +def get_policy_update_schedule(cfg): + return schedule_type[cfg["type"]](**cfg["args"]) + +# policy update schedule can be a dict or single EpisodeBasedSchedule or StepBasedSchedule. +# The latter indicates that all policies shared the same update schedule +policy_update_schedule = { + policy_id: get_policy_update_schedule(config["policy"][policy_id]["update_schedule"]) + for policy_id in policy_ids if policy_id in config["policy"] +} \ No newline at end of file diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index 61da803b0..89cc01efd 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -4,7 +4,7 @@ import sys from os.path import dirname, realpath -from maro.rl import EpisodeBasedSchedule, LocalLearner, MultiPolicyUpdateSchedule, StepBasedSchedule +from maro.rl import EpisodeBasedSchedule, LocalLearner, StepBasedSchedule from maro.simulator import Env sc_code_dir = dirname(realpath(__file__)) @@ -12,13 +12,12 @@ from config import config from env_wrapper import SCEnvWrapper from exploration import exploration_dict, agent2exploration -from policies import agent2policy, policy_dict +from policies import agent2policy, policy_dict, policy_update_schedule # Single-threaded launcher if __name__ == "__main__": env = SCEnvWrapper(Env(**config["env"])) - policy_update_schedule = MultiPolicyUpdateSchedule(EpisodeBasedSchedule(3, 2)) # create a learner to start training learner = LocalLearner( policy_dict, agent2policy, env, config["num_episodes"], policy_update_schedule, diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 61f563fd3..f435e1f11 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from collections import namedtuple from maro.rl.exploration import AbsExploration from maro.rl.experience import AbsExperienceManager, ExperienceSet @@ -35,9 +34,6 @@ class NullPolicy(AbsPolicy): def choose_action(self, state): return None - def update(self): - pass - class AbsCorePolicy(AbsPolicy): def __init__(self, experience_manager: AbsExperienceManager, config): diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 74767768d..aa98646d9 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -88,7 +88,9 @@ def run(self): self.env.reset() self.env.start() # get initial state - self._load_policy(msg.body[MsgKey.POLICY]) + # load policies + self._load_policy_states(msg.body[MsgKey.POLICY]) + starting_step_index = self.env.step_index + 1 steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while self.env.state and steps_to_go > 0: @@ -131,7 +133,7 @@ def run(self): self._logger.info(f"Evaluation episode {ep}") self.eval_env.reset() self.eval_env.start() # get initial state - self._load_policy(msg.body[MsgKey.POLICY]) + self._load_policy_states(msg.body[MsgKey.POLICY]) while self.eval_env.state: action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} self.eval_env.step(action) @@ -142,7 +144,10 @@ def run(self): MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] } self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) - - def _load_policy(self, policy_state_dict): + + def _load_policy_states(self, policy_state_dict): for policy_id, policy_state in policy_state_dict.items(): self.policy_dict[policy_id].set_state(policy_state) + + if policy_state_dict: + self._logger.info(f"updated policies {list(policy_state_dict.keys())}") diff --git a/maro/rl/training/distributed_learner.py b/maro/rl/training/distributed_learner.py index 5b482d8c1..b67d711a8 100644 --- a/maro/rl/training/distributed_learner.py +++ b/maro/rl/training/distributed_learner.py @@ -11,7 +11,7 @@ from maro.utils import Logger from .actor_manager import ActorManager -from .policy_update_schedule import MultiPolicyUpdateSchedule +from .policy_update_schedule import EpisodeBasedSchedule, MultiPolicyUpdateSchedule, StepBasedSchedule class DistributedLearner(object): @@ -26,7 +26,7 @@ def __init__( policy_dict: Dict[str, AbsPolicy], agent_to_policy: Dict[str, str], num_episodes: int, - policy_update_schedule: MultiPolicyUpdateSchedule, + policy_update_schedule: Union[EpisodeBasedSchedule, StepBasedSchedule, dict], actor_manager: ActorManager, experience_update_interval: int = -1, eval_env: AbsEnvWrapper = None, @@ -49,7 +49,15 @@ def __init__( self.agent_groups_by_policy[policy_id] = tuple(agent_ids) self.num_episodes = num_episodes - self.policy_update_schedule = policy_update_schedule + self.policy_update_schedule = MultiPolicyUpdateSchedule(policy_update_schedule) + if isinstance(policy_update_schedule, dict): + self._updatable_policy_ids = list(policy_update_schedule.keys()) + else: + self._updatable_policy_ids = [ + policy_id for policy_id, policy in self.policy_dict.items() if hasattr(policy, "update") + ] + self._updated_policy_ids = self._updatable_policy_ids + self.experience_update_interval = experience_update_interval # evaluation @@ -82,13 +90,15 @@ def run(self): policy_ids = self.policy_update_schedule.pop_episode(ep) if policy_ids == ["*"]: - policy_ids = list(self.policy_dict.keys()) + policy_ids = self._updatable_policy_ids + for policy_id in policy_ids: self.policy_dict[policy_id].update() if policy_ids: self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") + self._updated_policy_ids = policy_ids self.end_of_episode(ep, **self.end_of_episode_kwargs) if ep == self.eval_schedule[self._eval_point_index]: @@ -103,13 +113,13 @@ def _train(self, ep: int): learning_time = 0 env_steps = 0 num_experiences_collected = 0 - policy_ids, num_actor_finishes, segment_index = list(self.policy_dict.keys()), 0, 1 + num_actor_finishes, segment_index = 0, 1 while num_actor_finishes < self.actor_manager.required_finishes: # parallel experience collection for exp_by_agent, done in self.actor_manager.collect( ep, segment_index, self.experience_update_interval, - policy_dict={policy_id: self.policy_dict[policy_id].get_state() for policy_id in policy_ids}, + policy_dict={policy_id: self.policy_dict[policy_id].get_state() for policy_id in self._updated_policy_ids}, discard_stale_experiences=self.discard_stale_experiences ): for agent_id, exp in exp_by_agent.items(): @@ -124,13 +134,14 @@ def _train(self, ep: int): tl0 = time.time() policy_ids = self.policy_update_schedule.pop_step(ep, segment_index) if policy_ids == ["*"]: - policy_ids = list(self.policy_dict.keys()) + policy_ids = [policy_id for policy_id, policy in self.policy_dict.items() if hasattr(policy, "update")] for policy_id in policy_ids: self.policy_dict[policy_id].update() if policy_ids: self._logger.info(f"Updated policies {policy_ids} after segment {segment_index}") + self._updated_policy_ids = policy_ids learning_time += time.time() - tl0 segment_index += 1 @@ -160,7 +171,9 @@ def _evaluate(self, ep: int): if self._log_env_metrics: self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") else: - policy_state = {policy_id: policy.get_state() for policy_id, policy in self.policy_dict.items()} + policy_state = { + policy_id: self.policy_dict[policy_id].get_state() for policy_id in self._updatable_policy_ids + } self.actor_manager.evaluate(ep, policy_state, self.num_eval_actors) def end_of_episode(self, ep: int, **kwargs): diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py index b5b40cc3a..5082d89b0 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -1,18 +1,17 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import heapq import time -from collections import defaultdict, namedtuple +from collections import defaultdict from os import getcwd -from typing import Callable, Dict, List, Tuple, Union +from typing import Dict, List, Union from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsCorePolicy, AbsPolicy from maro.utils import Logger -from .policy_update_schedule import MultiPolicyUpdateSchedule +from .policy_update_schedule import EpisodeBasedSchedule, MultiPolicyUpdateSchedule, StepBasedSchedule class LocalLearner(object): @@ -28,7 +27,7 @@ def __init__( agent2policy: Dict[str, str], env: AbsEnvWrapper, num_episodes: int, - policy_update_schedule: MultiPolicyUpdateSchedule, + policy_update_schedule: Union[EpisodeBasedSchedule, StepBasedSchedule, dict], exploration_dict: Dict[str, AbsExploration] = None, agent2exploration: Dict[str, str] = None, experience_update_interval: int = -1, @@ -77,7 +76,7 @@ def __init__( self.env = env self.num_episodes = num_episodes - self.policy_update_schedule = policy_update_schedule + self.policy_update_schedule = MultiPolicyUpdateSchedule(policy_update_schedule) self.eval_env = eval_env if eval_env else self.env self.experience_update_interval = experience_update_interval @@ -106,7 +105,7 @@ def run(self): policy_ids = self.policy_update_schedule.pop_episode(ep) if policy_ids == ["*"]: - policy_ids = list(self.policy_dict.keys()) + policy_ids = [policy_id for policy_id, policy in self.policy_dict.items() if hasattr(policy, "update")] for policy_id in policy_ids: self.policy_dict[policy_id].update() @@ -161,7 +160,7 @@ def _train(self, ep: int): tl0 = time.time() policy_ids = self.policy_update_schedule.pop_step(ep, step_index) if policy_ids == ["*"]: - policy_ids = list(self.policy_dict.keys()) + policy_ids = [policy_id for policy_id, policy in self.policy_dict.items() if hasattr(policy, "update")] for policy_id in policy_ids: self.policy_dict[policy_id].update() From 0a9851ac5969c2b08340df964096bf224d309b2b Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 11 May 2021 03:17:01 +0000 Subject: [PATCH 234/482] small edits --- maro/rl/training/distributed_learner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/rl/training/distributed_learner.py b/maro/rl/training/distributed_learner.py index b67d711a8..e152734e6 100644 --- a/maro/rl/training/distributed_learner.py +++ b/maro/rl/training/distributed_learner.py @@ -134,7 +134,7 @@ def _train(self, ep: int): tl0 = time.time() policy_ids = self.policy_update_schedule.pop_step(ep, segment_index) if policy_ids == ["*"]: - policy_ids = [policy_id for policy_id, policy in self.policy_dict.items() if hasattr(policy, "update")] + policy_ids = self._updatable_policy_ids for policy_id in policy_ids: self.policy_dict[policy_id].update() From c35615b22c90292bbb17c97a0849871cacd367fe Mon Sep 17 00:00:00 2001 From: lesong Date: Tue, 11 May 2021 08:17:34 +0000 Subject: [PATCH 235/482] add baseline --- docker_files/dev.df | 1 + examples/supply_chain/config.yml | 2 +- examples/supply_chain/distributed_launcher.py | 15 +- examples/supply_chain/env_wrapper.py | 84 +++++++--- examples/supply_chain/or_policies.py | 38 +++++ examples/supply_chain/render_tools.py | 157 ++++++++++++++++++ .../supply_chain/single_thread_launcher.py | 11 +- 7 files changed, 278 insertions(+), 30 deletions(-) create mode 100644 examples/supply_chain/or_policies.py create mode 100644 examples/supply_chain/render_tools.py diff --git a/docker_files/dev.df b/docker_files/dev.df index b0112040d..c905e2fc3 100644 --- a/docker_files/dev.df +++ b/docker_files/dev.df @@ -19,6 +19,7 @@ RUN pip install --no-cache-dir Cython==0.29.14 RUN pip install --no-cache-dir pyaml==20.4.0 RUN pip install --no-cache-dir pyzmq==19.0.2 RUN pip install --no-cache-dir numpy==1.19.1 +RUN pip install --no-cache-dir matplotlib RUN pip install --no-cache-dir torch==1.6.0 RUN pip install --no-cache-dir scipy RUN pip install --no-cache-dir redis diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 0e16b3473..602c841b5 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -5,7 +5,7 @@ env: scenario: supply_chain # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ - topology: sample1 + topology: test durations: 200 # number of ticks per episode num_episodes: 10 # number of episodes to simulate # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index 2fbad5aec..0b6a942c8 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - +import os import argparse import sys import yaml @@ -18,7 +18,9 @@ from env_wrapper import SCEnvWrapper from exploration import exploration_dict, agent_to_exploration from learner import SCLearner -from policies import policy_dict, agent_to_policy +# from policies import policy_dict, agent_to_policy +from or_policies import policy_dict, agent_to_policy +from render_tools import SimulationTracker # for distributed / multi-process training @@ -67,7 +69,14 @@ def sc_learner(): end_of_training_kwargs=config["end_of_training_kwargs"], log_dir=log_dir ) - learner.run() + # learner.run() + + env = SCEnvWrapper(Env(**config["env"])) + tracker = SimulationTracker(60, 1, env, learner) + loc_path = '/maro/supply_chain/output/' + facility_types = [5] + os.system(f"rm {loc_path}/*") + tracker.run_and_render(loc_path, facility_types) def sc_actor(name: str): diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 7f136094c..2f4e036c9 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -145,6 +145,11 @@ def __init__(self, env: Env): # built internal helpers. self._build_internal_helpers() + self.stock_status = {} + self.demand_status = {} + self.order_in_transit_status = {} + self.order_to_distribute_status = {} + @property def dim(self): """Calculate dim per shape.""" @@ -164,11 +169,55 @@ def dim(self): return self._dim - def get_state_a(self): - pass - - def get_state_b(self): - pass + def get_or_policy_state(self, state, agent_info): + state = {'is_facility': agent_info.is_facility} + if agent_info.is_facility: + return state + id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ + self.balance_cal.products[self.balance_cal.product_id2index_dict[agent_info.id]] + facility = self.facility_levels[agent_info.facility_id] + state['unit_storage_cost'] = unit_storage_cost + state['order_cost'] = 1 + product_info = facility[agent_info.sku.id] + if "consumer" in product_info: + consumer_index = product_info["consumer"].node_index + state['order_cost'] = self.consumer_ss[self.env.tick:consumer_index:"order_cost"] + state['storage_capacity'] = facility['storage'].config["capacity"] + state['storage_levels'] = self._storage_product_numbers[agent_info.facility_id] + state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] + state['sale_mean'] = self._cur_metrics["products"][agent_info.id]['sale_mean'] + state['sale_std'] = self._cur_metrics["products"][agent_info.id]['sale_std'] + state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + state['vlt'] = agent_info.sku.vlt + state['service_level'] = agent_info.sku.service_level + return state + + def get_rl_policy_state(self, state, agent_info): + self._update_facility_features(state, agent_info) + self._update_storage_features(state, agent_info) + # bom do not need to update + # self._add_bom_features(state, agent_info) + self._update_distribution_features(state, agent_info) + self._update_sale_features(state, agent_info) + # vlt do not need to update + # self._update_vlt_features(state, agent_info) + self._update_consumer_features(state, agent_info) + # self._add_price_features(state, agent_info) + self._update_global_features(state) + + self.stock_status[agent_info.id] = state['inventory_in_stock'] + + self.demand_status[agent_info.id] = state['sale_hist'][-1] + + self.order_in_transit_status[agent_info.id] = state['inventory_in_transit'] + + self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] + + self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} + self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} + + np_state = self._serialize_state(state) + return np_state def get_state(self, event): cur_tick = self.env.tick @@ -189,6 +238,7 @@ def get_state(self, event): self._cur_seller_states = self.seller_ss[hist_ticks::seller_features].astype( np.int) + # facility level states for facility_id in self._facility_product_utilization: # reset for each step @@ -217,23 +267,10 @@ def get_state(self, event): for agent_info in self._agent_list: state = self._states[agent_info.id] - - storage_index = self._facility2storage_index_dict[agent_info.facility_id] - - self._update_facility_features(state, agent_info) - self._update_storage_features(state, agent_info) - # bom do not need to update - # self._add_bom_features(state, agent_info) - self._update_distribution_features(state, agent_info) - self._update_sale_features(state, agent_info) - # vlt do not need to update - # self._update_vlt_features(state, agent_info) - self._update_consumer_features(state, agent_info) - # self._add_price_features(state, agent_info) - self._update_global_features(state) - - np_state = self._serialize_state(state) - + # storage_index = self._facility2storage_index_dict[agent_info.facility_id] + + np_state = self.get_rl_policy_state(state, agent_info) + np_state = self.get_or_policy_state(state, agent_info) final_state[f"consumer.{agent_info.id}"] = np_state final_state[f"producer.{agent_info.id}"] = np_state @@ -252,7 +289,7 @@ def get_reward(self, tick=None, target_agents=None): consumer_reward_by_facility = {f_id: wc * parent_facility_balance[f_id][0] + (1 - wc) * bsw[1] for f_id, bsw in self.cur_balance_sheet_reward.items()} - return { + return { **{f"producer.{f_id}": np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()}, **{f"consumer.{f_id}": np.float32(reward) for f_id, reward in consumer_reward_by_facility.items()} } @@ -893,6 +930,7 @@ def calc(self): # cProfile.run("ss.get_state(None)", sort="cumtime") states = ss.get_state(None) + print(states) end_time = time() diff --git a/examples/supply_chain/or_policies.py b/examples/supply_chain/or_policies.py new file mode 100644 index 000000000..ccb6aae1f --- /dev/null +++ b/examples/supply_chain/or_policies.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os.path import dirname, realpath + +import numpy as np + +import torch + +from maro.rl import ( + DQN, DQNConfig, ExperienceMemory, FullyConnectedBlock, NullPolicy, OptimOption, QNetForDiscreteActionSpace, + TrainingLoopConfig, get_sampler_cls +) + +from or_policy.minmax_policy import ConsumerMinMaxPolicy +from or_policy.base_policy import ProducerBaselinePolicy + +sc_code_dir = dirname(realpath(__file__)) +sys.path.insert(0, sc_code_dir) +from config import config + +agent_ids = config["agent_ids"] +config = config["policy"] + + + +def get_base_consumer_policy(config): + return ConsumerMinMaxPolicy(config) + +def get_base_producer_policy(config): + return ProducerBaselinePolicy(config) + +# all consumers share the same underlying policy +policy_dict = {"consumer": get_base_consumer_policy(config["consumer"]), + "producer": get_base_producer_policy(None)} + +agent_to_policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py new file mode 100644 index 000000000..3edfece01 --- /dev/null +++ b/examples/supply_chain/render_tools.py @@ -0,0 +1,157 @@ +import numpy as np +import matplotlib.pyplot as plt +import os + + +class SimulationTracker: + def __init__(self, episod_len, n_episods, env, learner): + self.episod_len = episod_len + self.global_balances = np.zeros((n_episods, episod_len)) + self.global_rewards = np.zeros((n_episods, episod_len)) + self.env = env + self.learner = learner + self.facility_names = self.env._agent_list + self.step_balances = np.zeros( + (n_episods, self.episod_len, len(self.facility_names))) + self.step_rewards = np.zeros( + (n_episods, self.episod_len, len(self.facility_names))) + self.n_episods = n_episods + self.sku_to_track = None + self.stock_status = None + self.stock_in_transit_status = None + self.reward_status = None + self.demand_status = None + self.reward_discount_status = None + self.order_to_distribute = None + + def add_sample(self, episod, t, global_balance, global_reward, step_balances, step_rewards): + self.global_balances[episod, t] = global_balance + self.global_rewards[episod, t] = global_reward + for i, f in enumerate(self.facility_names): + self.step_balances[episod, t, i] = step_balances[f.id] + self.step_rewards[episod, t, i] = step_rewards[f.id] + + def add_sku_status(self, episod, t, stock, order_in_transit, demands, rewards, balances, order_to_distribute): + if self.sku_to_track is None: + self.sku_to_track = set( + list(stock.keys()) + list(order_in_transit.keys()) + list(demands.keys())) + self.stock_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.stock_in_transit_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.demand_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.reward_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.balance_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.order_to_distribute = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + for i, sku_name in enumerate(self.sku_to_track): + self.stock_status[episod, t, i] = stock[sku_name] + self.stock_in_transit_status[episod, + t, i] = order_in_transit[sku_name] + self.demand_status[episod, t, i] = demands[sku_name] + self.reward_status[episod, t, i] = rewards[sku_name] + self.balance_status[episod, t, i] = balances[sku_name] + self.order_to_distribute[episod, t, + i] = order_to_distribute[sku_name] + + def render_sku(self, loc_path): + for i, sku_name in enumerate(self.sku_to_track): + fig, ax = plt.subplots(3, 1, figsize=(25, 10)) + x = np.linspace(0, self.episod_len, self.episod_len) + stock = self.stock_status[0, :, i] + order_in_transit = self.stock_in_transit_status[0, :, i] + demand = self.demand_status[0, :, i] + reward = self.reward_status[0, :, i] + balance = self.balance_status[0, :, i] + order_to_distribute = self.order_to_distribute[0, :, i] + ax[0].set_title('SKU Stock Status by Episod') + for y_label, y in [('stock', stock), + ('order_in_transit', order_in_transit), + ('demand', demand), + ('order_to_distribute', order_to_distribute)]: + ax[0].plot(x, y, label=y_label) + + ax[1].set_title('SKU Reward / Balance Status by Episod') + ax[1].plot(x, balance, label='Balance') + ax_r = ax[1].twinx() + ax_r.plot(x, reward, label='Reward', color='r') + fig.legend() + fig.savefig(f"{loc_path}/{sku_name}.png") + plt.close(fig=fig) + + def render(self, file_name, metrics, facility_types): + fig, axs = plt.subplots(2, 1, figsize=(25, 10)) + x = np.linspace(0, self.episod_len, self.episod_len) + + _agent_list = [] + _step_idx = [] + for i, agent_info in enumerate(self.facility_names): + if agent_info.agent_type in facility_types: + _agent_list.append(agent_info.id) + _step_idx.append(i) + _step_metrics = [metrics[0, :, i] for i in _step_idx] + + # axs[0].set_title('Global balance') + # axs[0].plot(x, self.global_balances.T) + + axs[0].set_title('Cumulative Sum') + axs[0].plot(x, np.cumsum(np.sum(_step_metrics, axis=0))) + + axs[1].set_title('Breakdown by Agent (One Episod)') + axs[1].plot(x, np.cumsum(_step_metrics, axis=1).T) + axs[1].legend(_agent_list, loc='upper left') + + fig.savefig(file_name) + plt.close(fig=fig) + # plt.show() + + def run_wth_render(self, facility_types): + self.env.reset() + self.env.start() + self.learner.policy.eval_mode() + for epoch in range(self.episod_len): + action = self.learner.policy.choose_action(self.env.state) + self.learner._logger.info(f"epoch: {epoch}, action: {action}") + self.env.step(action) + self.learner._logger.info(f"epoch: {epoch}, action: {self.env.get_action(action)}") + if hasattr(self.env, "consumer2product"): + self.learner._logger.info(f"consumer2product: {self.env.consumer2product}") + self.env.get_reward() + step_balances = self.env.balance_status + step_rewards = self.env.reward_status + + self.add_sample(0, epoch, sum(step_balances.values()), sum( + step_rewards.values()), step_balances, step_rewards) + stock_status = self.env.stock_status + order_in_transit_status = self.env.order_in_transit_status + demand_status = self.env.demand_status + reward_status = self.env.reward_status + balance_status = self.env.balance_status + order_to_distribute_status = self.env.order_to_distribute_status + + self.add_sku_status(0, epoch, stock_status, + order_in_transit_status, demand_status, + reward_status, balance_status, + order_to_distribute_status) + + _step_idx = [] + for i, agent_info in enumerate(self.facility_names): + if agent_info.agent_type in facility_types: + _step_idx.append(i) + _step_metrics = [self.step_rewards[0, :, i] for i in _step_idx] + _step_metrics_list = np.cumsum(np.sum(_step_metrics, axis=0)) + return np.sum(_step_metrics), _step_metrics_list + + def run_and_render(self, loc_path, facility_types): + metric, metric_list = self.run_wth_render( + facility_types=facility_types) + os.makedirs(loc_path, exist_ok=True) + self.render('%s/plot_balance.png' % + loc_path, self.step_balances, facility_types) + self.render('%s/plot_reward.png' % + loc_path, self.step_rewards, facility_types) + self.render_sku(loc_path) + return metric, metric_list diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index 6df35efb2..b7acd5afe 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -12,8 +12,9 @@ from config import config from env_wrapper import SCEnvWrapper from exploration import exploration_dict, agent_to_exploration -from policies import policy_dict, agent_to_policy - +# from policies import policy_dict, agent_to_policy +from or_policies import policy_dict, agent_to_policy +from render_tools import SimulationTracker # Single-threaded launcher if __name__ == "__main__": @@ -32,4 +33,8 @@ eval_points=config["eval_points"], log_env_metrics=config["log_env_metrics"] ) - learner.run() + # learner.run() + tracker = SimulationTracker(60, 1, env, learner) + loc_path = '/maro/supply_chain/output/' + facility_types = [5] + tracker.run_and_render(loc_path, facility_types) \ No newline at end of file From a6f5efbb9441313ffdafd37fa839d3814fb5f3cc Mon Sep 17 00:00:00 2001 From: lesong Date: Tue, 11 May 2021 08:17:57 +0000 Subject: [PATCH 236/482] add baseline --- .../supply_chain/or_policy/base_policy.py | 44 +++++++++++++++++ examples/supply_chain/or_policy/eoq_policy.py | 48 +++++++++++++++++++ .../supply_chain/or_policy/minmax_policy.py | 38 +++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 examples/supply_chain/or_policy/base_policy.py create mode 100644 examples/supply_chain/or_policy/eoq_policy.py create mode 100644 examples/supply_chain/or_policy/minmax_policy.py diff --git a/examples/supply_chain/or_policy/base_policy.py b/examples/supply_chain/or_policy/base_policy.py new file mode 100644 index 000000000..07d912c43 --- /dev/null +++ b/examples/supply_chain/or_policy/base_policy.py @@ -0,0 +1,44 @@ +import numpy as np +from maro.rl.policy import AbsFixedPolicy +import random +import scipy.stats as st + + +class ProducerBaselinePolicy(AbsFixedPolicy): + + def __init__(self, config): + self.config = config + # self.num_actions = config["model"]["network"]["output_dim"] + + def choose_action(self, state): + return state.get('product_rate', 500) + + +class ConsumerBaselinePolicy(AbsFixedPolicy): + + def __init__(self, config): + self.config = config + self.num_actions = config["model"]["network"]["output_dim"] + + def choose_action(self, state): + if state['is_facility']: + return 0 + # consumer_source_inventory + available_inventory = np.array(state['storage_levels']) + inflight_orders = np.array(state['consumer_in_transit_orders']) + booked_inventory = available_inventory + inflight_orders + + # stop placing orders when the facilty runs out of capacity + if np.sum(booked_inventory) > state['storage_capacity']: + return 0 + + most_needed_product_id = state['product_idx'] + sale_mean, sale_std = state['sale_mean'], state['sale_std'] + service_level = state['service_level'] + vlt_buffer_days = 7 + vlt = vlt_buffer_days + state['vlt'] + if booked_inventory[most_needed_product_id] > vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level): + return 0 + consumer_action_space_size = self.num_actions + consumer_quantity = random.randint(0, consumer_action_space_size-1) + return consumer_quantity diff --git a/examples/supply_chain/or_policy/eoq_policy.py b/examples/supply_chain/or_policy/eoq_policy.py new file mode 100644 index 000000000..8e6dd41ed --- /dev/null +++ b/examples/supply_chain/or_policy/eoq_policy.py @@ -0,0 +1,48 @@ +import numpy as np +import random as rnd +import scipy.stats as st +from or_policy.base_policy import ConsumerBaselinePolicy + + +# Q = \sqrt{2DK/h} +# Q - optimal order quantity +# D - annual demand quantity +# K - fixed cost per order, setup cost (not per unit, typically cost of ordering and shipping and handling. This is not the cost of goods) +# h - annual holding cost per unit, +# also known as carrying cost or storage cost (capital cost, warehouse space, +# refrigeration, insurance, etc. usually not related to the unit production cost) +class ConsumerEOQPolicy(ConsumerBaselinePolicy): + + def __init__(self, config): + ConsumerBaselinePolicy.__init__(self, config) + + def _get_consumer_quantity(self, state): + order_cost = state['order_cost'] + holding_cost = state['unit_storage_cost'] + sale_gamma = state['sale_mean'] + consumer_quantity = int(np.sqrt(2*sale_gamma*order_cost / holding_cost) / sale_gamma) + return consumer_quantity + + def compute_action(self, state): + if state['is_facility']: + return 0 + # consumer_source_inventory + available_inventory = np.array(state['storage_levels']) + inflight_orders = np.array(state['consumer_in_transit_orders']) + booked_inventory = available_inventory + inflight_orders + + # stop placing orders when the facilty runs out of capacity + if np.sum(booked_inventory) > state['storage_capacity']: + return 0 + + most_needed_product_id = state['product_idx'] + vlt_buffer_days = 7 + vlt = vlt_buffer_days + state['vlt'] + sale_mean, sale_std = state['sale_mean'], state['sale_std'] + service_level = state['service_level'] + + # whether replenishment point is reached + if booked_inventory[most_needed_product_id] > vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level): + return 0 + consumer_quantity = self._get_consumer_quantity(state) + return consumer_quantity diff --git a/examples/supply_chain/or_policy/minmax_policy.py b/examples/supply_chain/or_policy/minmax_policy.py new file mode 100644 index 000000000..1cee7c453 --- /dev/null +++ b/examples/supply_chain/or_policy/minmax_policy.py @@ -0,0 +1,38 @@ +import numpy as np +import random as rnd +import scipy.stats as st +from or_policy.base_policy import ConsumerBaselinePolicy + + +# parameters: (r, R), calculate according to VLT, demand variances, and service level +# replenish R - S units whenever the current stock is less than r +# S denotes the number of units in stock +class ConsumerMinMaxPolicy(ConsumerBaselinePolicy): + + def __init__(self, config): + ConsumerBaselinePolicy.__init__(self, config) + + def compute_action(self, state): + if state['is_facility']: + return 0 + # consumer_source_inventory + available_inventory = np.array(state['storage_levels']) + inflight_orders = np.array(state['consumer_in_transit_orders']) + booked_inventory = available_inventory + inflight_orders + + # stop placing orders when the facilty runs out of capacity + if np.sum(booked_inventory) > state['storage_capacity']: + return 0 + + most_needed_product_id = state['product_idx'] + # stop placing orders if no risk of out of stock + vlt_buffer_days = 10 + vlt = state['vlt'] + vlt_buffer_days + sale_mean, sale_std = state['sale_mean'], state['sale_std'] + service_level = state['service_level'] + r = (vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level)) + if booked_inventory[most_needed_product_id] > r: + return 0 + R = 3*r + consumer_quantity = int((R - r) / sale_mean) + return consumer_quantity \ No newline at end of file From e9a112f5aa84210ea419db58ea99c4b376fac253 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Tue, 11 May 2021 18:34:34 +0800 Subject: [PATCH 237/482] correct consumer action issue --- examples/supply_chain/env_wrapper.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 9c24b4c43..ddbde77fa 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -264,16 +264,15 @@ def get_reward(self, tick=None, target_agents=None): def get_action(self, action_by_agent): # cache the sources for each consumer if not yet cached - if not hasattr(self, "product2source"): - self.product2source, self.consumer2product = {}, {} + if not hasattr(self, "consumer2source"): + self.consumer2source, self.consumer2product = {}, {} for facility in self.env.summary["node_mapping"]["facilities"].values(): products = facility["units"]["products"] for product_id, product in products.items(): consumer = product["consumer"] if consumer is not None: consumer_id = consumer["id"] - product_unit_id = product["id"] - self.product2source[product_unit_id] = consumer["sources"] + self.consumer2source[consumer_id] = consumer["sources"] self.consumer2product[consumer_id] = product_id env_action = {} @@ -289,12 +288,12 @@ def get_action(self, action_by_agent): # consumer action if agent_id.startswith("consumer"): product_id = self.consumer2product.get(unit_id, 0) - sources = self.product2source.get(unit_id, []) + sources = self.consumer2source.get(unit_id, []) if sources: source_id = sources[0] - - action_number = int(int(action) * self._cur_metrics["products"][unit_id]["sale_mean"]) + product_unit_id = self._unit2product_mapping[unit_id][0] + action_number = int(int(action) * self._cur_metrics["products"][product_unit_id]["sale_mean"]) # ignore 0 quantity to reduce action number if action_number == 0: From 3ecf8c32277fda203c2a4a54dede52c6b0ef1413 Mon Sep 17 00:00:00 2001 From: lesong Date: Tue, 11 May 2021 11:14:35 +0000 Subject: [PATCH 238/482] add baseline --- examples/supply_chain/env_wrapper.py | 18 +++++++++++++----- examples/supply_chain/render_tools.py | 14 +++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 93430d2c7..3afce1168 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -183,11 +183,19 @@ def dim(self): return self._dim def get_or_policy_state(self, state, agent_info): - state = {'is_facility': agent_info.is_facility} + state = {'is_facility': not (agent_info.agent_type in sku_agent_types)} if agent_info.is_facility: return state + + product_unit_id = agent_info.id if agent_info.agent_type == "product" else agent_info.parent_id + id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ - self.balance_cal.products[self.balance_cal.product_id2index_dict[agent_info.id]] + self.balance_cal.products[self.balance_cal.product_id2index_dict[product_unit_id]] + + product_metrics = self._cur_metrics["products"][product_unit_id] + state['sale_mean'] = product_metrics["sale_mean"] + state['sale_std'] = product_metrics["sale_std"] + facility = self.facility_levels[agent_info.facility_id] state['unit_storage_cost'] = unit_storage_cost state['order_cost'] = 1 @@ -198,8 +206,6 @@ def get_or_policy_state(self, state, agent_info): state['storage_capacity'] = facility['storage'].config["capacity"] state['storage_levels'] = self._storage_product_numbers[agent_info.facility_id] state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - state['sale_mean'] = self._cur_metrics["products"][agent_info.id]['sale_mean'] - state['sale_std'] = self._cur_metrics["products"][agent_info.id]['sale_std'] state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] state['vlt'] = agent_info.sku.vlt state['service_level'] = agent_info.sku.service_level @@ -333,7 +339,7 @@ def get_action(self, action_by_agent): if agent_id.startswith("consumer"): product_id = self.consumer2product.get(unit_id, 0) sources = self.product2source.get(unit_id, []) - + print(sources) if sources: source_id = sources[0] @@ -386,6 +392,8 @@ def _update_sale_features(self, state, agent_info): product_metrics = self._cur_metrics["products"][product_unit_id] + + state['sale_mean'] = product_metrics["sale_mean"] state['sale_std'] = product_metrics["sale_std"] diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py index 3edfece01..43bd581d3 100644 --- a/examples/supply_chain/render_tools.py +++ b/examples/supply_chain/render_tools.py @@ -1,6 +1,7 @@ import numpy as np import matplotlib.pyplot as plt import os +from env_wrapper import sku_agent_types class SimulationTracker: @@ -10,7 +11,10 @@ def __init__(self, episod_len, n_episods, env, learner): self.global_rewards = np.zeros((n_episods, episod_len)) self.env = env self.learner = learner - self.facility_names = self.env._agent_list + self.facility_names = [] + for agent in self.env._agent_list: + if agent.agent_type in sku_agent_types: + self.facility_names.append(agent) self.step_balances = np.zeros( (n_episods, self.episod_len, len(self.facility_names))) self.step_rewards = np.zeros( @@ -33,8 +37,7 @@ def add_sample(self, episod, t, global_balance, global_reward, step_balances, st def add_sku_status(self, episod, t, stock, order_in_transit, demands, rewards, balances, order_to_distribute): if self.sku_to_track is None: - self.sku_to_track = set( - list(stock.keys()) + list(order_in_transit.keys()) + list(demands.keys())) + self.sku_to_track = list(rewards.keys()) self.stock_status = np.zeros( (self.n_episods, self.episod_len, len(self.sku_to_track))) self.stock_in_transit_status = np.zeros( @@ -111,9 +114,10 @@ def render(self, file_name, metrics, facility_types): def run_wth_render(self, facility_types): self.env.reset() self.env.start() - self.learner.policy.eval_mode() + # self.learner.policy.eval_mode() for epoch in range(self.episod_len): - action = self.learner.policy.choose_action(self.env.state) + # action = self.learner.policy.choose_action(self.env.state) + action = {id_: self.learner.policy[id_].choose_action(st) for id_, st in self.env.state.items()} self.learner._logger.info(f"epoch: {epoch}, action: {action}") self.env.step(action) self.learner._logger.info(f"epoch: {epoch}, action: {self.env.get_action(action)}") From 0587acb9fbe8af6861f4831932e832954b1a288d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 12 May 2021 07:38:42 +0000 Subject: [PATCH 239/482] fixed multi-policy schedule bug --- maro/rl/training/policy_update_schedule.py | 25 ++++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/maro/rl/training/policy_update_schedule.py b/maro/rl/training/policy_update_schedule.py index ef7fdef76..c32f02d28 100644 --- a/maro/rl/training/policy_update_schedule.py +++ b/maro/rl/training/policy_update_schedule.py @@ -12,8 +12,8 @@ class MultiPolicyUpdateSchedule: """ """ def __init__(self, schedule_option: Union[EpisodeBasedSchedule, StepBasedSchedule, dict] = -1): - self._pending_steps = defaultdict(list) - self._pending_episodes = defaultdict(list) + self._pending_steps = defaultdict(set) + self._pending_episodes = defaultdict(set) self._step_schedule_opt = {} self._episode_schedule_opt = {} @@ -23,31 +23,28 @@ def __init__(self, schedule_option: Union[EpisodeBasedSchedule, StepBasedSchedul self._step_schedule_opt[policy_id] = (sch.start_ep, sch.interval) if sch.end_ep_update: self._episode_schedule_opt[policy_id] = (sch.start_ep, 1) - self._pending_episodes[sch.start_ep].append(policy_id) - self._pending_steps[sch.interval].append(policy_id) + self._pending_episodes[sch.start_ep].add(policy_id) + self._pending_steps[sch.interval].add(policy_id) elif isinstance(sch, EpisodeBasedSchedule): self._episode_schedule_opt[policy_id] = (sch.start, sch.interval) - self._pending_episodes[sch.start].append(policy_id) + self._pending_episodes[sch.start].add(policy_id) else: if isinstance(schedule_option, StepBasedSchedule): self._step_schedule_opt["*"] = (schedule_option.start_ep, schedule_option.interval) - self._pending_steps[schedule_option.interval].append("*") + self._pending_steps[schedule_option.interval].add("*") if schedule_option.end_ep_update: self._episode_schedule_opt["*"] = (schedule_option.start_ep, 1) - self._pending_episodes[schedule_option.start_ep].append("*") + self._pending_episodes[schedule_option.start_ep].add("*") else: self._episode_schedule_opt["*"] = (schedule_option.start, schedule_option.interval) - self._pending_episodes[schedule_option.start].append("*") + self._pending_episodes[schedule_option.start].add("*") self._episode = None def pop_step(self, ep: int, step: int) -> List[str]: for policy_id in self._pending_steps[step]: next_step = step + self._step_schedule_opt[policy_id][1] - # if the pending_steps is filled, skip the loop altogether - if self._pending_steps[next_step]: - break - self._pending_steps[next_step].append(policy_id) + self._pending_steps[next_step].add(policy_id) return [ policy_id for policy_id in self._pending_steps[step] if self._step_schedule_opt[policy_id][0] <= ep @@ -56,6 +53,6 @@ def pop_step(self, ep: int, step: int) -> List[str]: def pop_episode(self, ep: int) -> List[str]: for policy_id in self._pending_episodes[ep]: next_ep = ep + self._episode_schedule_opt[policy_id][1] - self._pending_episodes[next_ep].append(policy_id) + self._pending_episodes[next_ep].add(policy_id) - return self._pending_episodes[ep] + return list(self._pending_episodes[ep]) From a32ef87a6ad84a4612efa29d2f66354c7174a64f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 12 May 2021 15:59:10 +0800 Subject: [PATCH 240/482] fix issues that stock is empty. --- examples/supply_chain/config.yml | 12 ++++++------ examples/supply_chain/env_wrapper.py | 16 ++++++++-------- examples/supply_chain/single_thread_launcher.py | 6 +++--- examples/supply_chain/topologies/test/config.yml | 8 ++++---- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 422f901d7..d5d4c1ac3 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -1,11 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -env: +env: scenario: supply_chain # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ - topology: sample1 + topology: test durations: 200 # number of ticks per episode num_episodes: 10 # number of episodes to simulate # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps @@ -39,7 +39,7 @@ policy: train_epochs: 10 gradient_iters: 1 loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. - target_update_freq: 5 # How many training iteration, to update DQN target model + target_update_freq: 5 # How many training iteration, to update DQN target model soft_update_coefficient: 0.1 double: false # whether to enable double DQN experience_manager: @@ -80,7 +80,7 @@ policy: train_epochs: 10 gradient_iters: 1 loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. - target_update_freq: 5 # How many training iteration, to update DQN target model + target_update_freq: 5 # How many training iteration, to update DQN target model soft_update_coefficient: 0.1 double: false # whether to enable double DQN experience_manager: @@ -95,7 +95,7 @@ policy: type: episode # "step" or "episode" args: start: 2 # must be a positive number since episode is 1-based - interval: 2 + interval: 2 exploration: initial_value: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. final_value: 0.0 @@ -112,5 +112,5 @@ distributed: # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent # slow actors from dragging down the whole process. required_actor_finishes: 3 - # If true, experiences from older segments (usually coming from slow actors) will not be used for learning. + # If true, experiences from older segments (usually coming from slow actors) will not be used for learning. discard_stale_experiences: True diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 82bf2f50c..352ab0938 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -191,7 +191,7 @@ def get_or_policy_state(self, state, agent_info): id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ self.balance_cal.products[self.balance_cal.product_id2index_dict[product_unit_id]] - + product_metrics = self._cur_metrics["products"][product_unit_id] state['sale_mean'] = product_metrics["sale_mean"] state['sale_std'] = product_metrics["sale_std"] @@ -223,7 +223,7 @@ def get_rl_policy_state(self, state, agent_info): self._update_consumer_features(state, agent_info) # self._add_price_features(state, agent_info) self._update_global_features(state) - + self.stock_status[agent_info.id] = state['inventory_in_stock'] self.demand_status[agent_info.id] = state['sale_hist'][-1] @@ -295,7 +295,7 @@ def get_state(self, event): storage_index = self._facility2storage_index_dict[agent_info.facility_id] np_state = self.get_rl_policy_state(state, agent_info) - np_state = self.get_or_policy_state(state, agent_info) + # np_state = self.get_or_policy_state(state, agent_info) # agent_info.agent_type -> policy final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state @@ -390,8 +390,6 @@ def _update_sale_features(self, state, agent_info): product_metrics = self._cur_metrics["products"][product_unit_id] - - state['sale_mean'] = product_metrics["sale_mean"] state['sale_std'] = product_metrics["sale_std"] @@ -436,12 +434,14 @@ def _update_consumer_features(self, state, agent_info): facility = self.facility_levels[agent_info.facility_id] product_info = facility[agent_info.sku.id] - if "consumer" not in product_info: - return + # if "consumer" not in product_info: + # return state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + # FIX: we need plus 1 to this, as it is 0 based index, but we already aligned with 1 more + # slot to use sku id as index ( 1 based). + product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index f179c4917..8b4c4a818 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -13,7 +13,7 @@ from env_wrapper import SCEnvWrapper from exploration import exploration_dict, agent2exploration from policies import agent2policy, policy_dict, policy_update_schedule - +from render_tools import SimulationTracker # Single-threaded launcher if __name__ == "__main__": @@ -29,6 +29,6 @@ ) # learner.run() tracker = SimulationTracker(60, 1, env, learner) - loc_path = '/maro/supply_chain/output/' - facility_types = [5] + loc_path = './output/' + facility_types = ["product"] tracker.run_and_render(loc_path, facility_types) diff --git a/examples/supply_chain/topologies/test/config.yml b/examples/supply_chain/topologies/test/config.yml index 4c8194af2..015688428 100644 --- a/examples/supply_chain/topologies/test/config.yml +++ b/examples/supply_chain/topologies/test/config.yml @@ -48,7 +48,7 @@ facility_definitions: config: agent_type: "consumer" manufacture: - class: "ManufactureUnit" + class: "SimpleManufactureUnit" config: agent_type: "producer" config: @@ -255,7 +255,7 @@ world: config: delay_order_penalty: 10 order_cost: 0 - + - name: "Warehouse_001" definition_ref: "WarehouseFacility" @@ -279,7 +279,7 @@ world: config: delay_order_penalty: 10 order_cost: 0 - + - name: "Retailer_001" definition_ref: "RetailerFacility" @@ -311,7 +311,7 @@ world: config: order_cost: 0 - + # topology used to specify the up/downstream for facilities # we split it from facility, so that we can support configuration inherit to override it # for a new topology From db24113ce80dd34e1bd57ee9b60ade7e3b4b5649 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 12 May 2021 16:34:38 +0800 Subject: [PATCH 241/482] update dependencies --- pyproject.toml | 2 +- setup.py | 38 ++++++++++++++++++------------------- tests/requirements.test.txt | 32 ++++++++++++++++--------------- 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5e8f73687..f10f195ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,2 @@ [build-system] -requires = ["setuptools", "wheel", "numpy == 1.19.1"] +requires = ["setuptools", "wheel", "numpy<1.19.0"] diff --git a/setup.py b/setup.py index 6aae2f921..2f955e9fc 100644 --- a/setup.py +++ b/setup.py @@ -119,28 +119,28 @@ "Topic :: Scientific/Engineering :: Artificial Intelligence"], python_requires=">=3.6,<3.8", setup_requires=[ - "numpy==1.19.1", + "numpy<1.19.0", ], install_requires=[ # TODO: use a helper function to collect these - "numpy==1.19.1", - "torch==1.6.0", - "holidays==0.10.3", - "pyaml==20.4.0", - "redis==3.5.3", - "pyzmq==19.0.2", - "requests==2.25.1", - "psutil==5.7.2", - "deepdiff==5.2.2", - "azure-storage-blob==12.6.0", - "azure-storage-common==2.1.0", - "geopy==2.0.0", - "pandas==0.25.3", - "PyYAML==5.4", - "paramiko==2.7.2", - "kubernetes==12.0.1", - "prompt_toolkit==2.0.10", - "stringcase==1.2.0" + "numpy<1.19.0", + "torch<1.8.0", + "holidays>=0.10.3", + "pyaml>=20.4.0", + "redis>=3.5.3", + "pyzmq<22.1.0", + "requests<=2.26.0", + "psutil<5.9.0", + "deepdiff>=5.2.2", + "azure-storage-blob<12.9.0", + "azure-storage-common>=2.1.0", + "geopy>=2.0.0", + "pandas<1.2", + "PyYAML<5.5.0", + "paramiko>=2.7.2", + "kubernetes>=12.0.1", + "prompt_toolkit<3.1.0", + "stringcase>=1.2.0", ], entry_points={ "console_scripts": [ diff --git a/tests/requirements.test.txt b/tests/requirements.test.txt index 72959d5da..b3f9c1a6c 100644 --- a/tests/requirements.test.txt +++ b/tests/requirements.test.txt @@ -1,22 +1,24 @@ -matplotlib==3.1.2 +matplotlib>=3.1.2 geopy -pandas -numpy==1.19.1 -holidays -pyaml -redis -pyzmq +pandas<1.2 +numpy<1.19.0 +holidays>=0.10.3 +pyaml>=20.4.0 +redis>=3.5.3 +pyzmq<22.1.0 influxdb -requests -psutil -deepdiff -azure-storage-blob -azure-storage-common -torch +requests<=2.26.0 +psutil<5.9.0 +deepdiff>=5.2.2 +azure-storage-blob<12.9.0 +azure-storage-common>=2.1.0 +torch<1.8.0 pytest coverage termgraph -paramiko==2.7.2 +paramiko>=2.7.2 pytz==2019.3 aria2p==0.9.1 -kubernetes +kubernetes>=12.0.1 +PyYAML<5.5.0 + From 4db7ef8566d3aba2477090ba2798a39acb56254c Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 12 May 2021 16:38:27 +0800 Subject: [PATCH 242/482] update build dependencies --- maro/requirements.build.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/maro/requirements.build.txt b/maro/requirements.build.txt index 6cf79955f..36b5b725f 100644 --- a/maro/requirements.build.txt +++ b/maro/requirements.build.txt @@ -1,6 +1,6 @@ pyjwt -numpy==1.19.1 -Cython==0.29.14 -altair==4.1.0 -streamlit==0.69.1 -tqdm==4.51.0 +numpy<1.19.0 +Cython>=0.29.14 +altair>=4.1.0 +streamlit>=0.69.1 +tqdm>=4.51.0 From 9f45a9d2a8ad2db6ac3c0ead19f0c557cf55f5b6 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 12 May 2021 18:23:53 +0800 Subject: [PATCH 243/482] bug fix that not updated the stoage --- .../scenarios/supply_chain/units/simplemanufacture.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py index 8a21305d0..c4a2c81af 100644 --- a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py @@ -19,3 +19,6 @@ def step(self, tick: int): unit_num_upper_bound = self.facility.storage.capacity // sku_num current_product_number = self.facility.storage.get_product_number(self.product_id) self.manufacture_number = max(0, min(unit_num_upper_bound - current_product_number, production_rate)) + + if self.manufacture_number > 0: + self.facility.storage.try_add_products({self.product_id: self.manufacture_number}) From 211f4d5f7392702b1bf9472e07c3da53c64739c1 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 12 May 2021 18:24:17 +0800 Subject: [PATCH 244/482] bug fix that agent type keys should be type name --- maro/simulator/scenarios/supply_chain/world.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 2f2da7cf8..8bfe0043b 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -275,7 +275,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio if agent_type is not None: self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id, None)) - self.agent_type_dict[agent_type] = type(facility).__name__ + self.agent_type_dict[type(facility).__name__] = agent_type for sku in facility.skus.values(): self.max_price = max(self.max_price, sku.price) @@ -297,7 +297,7 @@ def build(self, configs: SupplyChainConfiguration, snapshot_number: int, duratio ) ) - self.agent_type_dict[agent_type] = type(unit).__name__ + self.agent_type_dict[type(unit).__name__] = agent_type def build_unit_by_type(self, unit_def: UnitDef, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): """Build an unit by its type. From 469fc9e5d8442a33aa31a19f7694b0fd34394aaa Mon Sep 17 00:00:00 2001 From: chaosyu Date: Wed, 12 May 2021 18:24:51 +0800 Subject: [PATCH 245/482] add facility type, split demand to another plot --- examples/supply_chain/render_tools.py | 341 +++++++++++++------------- 1 file changed, 173 insertions(+), 168 deletions(-) diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py index 7c1b5177d..38290bc17 100644 --- a/examples/supply_chain/render_tools.py +++ b/examples/supply_chain/render_tools.py @@ -1,168 +1,173 @@ -import numpy as np -import matplotlib.pyplot as plt -import os -from env_wrapper import sku_agent_types - - -class SimulationTracker: - def __init__(self, episod_len, n_episods, env, learner): - self.episod_len = episod_len - self.global_balances = np.zeros((n_episods, episod_len)) - self.global_rewards = np.zeros((n_episods, episod_len)) - self.env = env - self.learner = learner - self.facility_names = [] - for agent in self.env._agent_list: - if agent.agent_type in sku_agent_types: - self.facility_names.append(agent) - self.step_balances = np.zeros( - (n_episods, self.episod_len, len(self.facility_names))) - self.step_rewards = np.zeros( - (n_episods, self.episod_len, len(self.facility_names))) - self.n_episods = n_episods - self.sku_to_track = None - self.stock_status = None - self.stock_in_transit_status = None - self.reward_status = None - self.demand_status = None - self.reward_discount_status = None - self.order_to_distribute = None - - def add_sample(self, episod, t, global_balance, global_reward, step_balances, step_rewards): - self.global_balances[episod, t] = global_balance - self.global_rewards[episod, t] = global_reward - for i, f in enumerate(self.facility_names): - self.step_balances[episod, t, i] = step_balances[f.id] - self.step_rewards[episod, t, i] = step_rewards[f.id] - - def add_sku_status(self, episod, t, stock, order_in_transit, demands, rewards, balances, order_to_distribute): - if self.sku_to_track is None: - self.sku_to_track = list(rewards.keys()) - self.stock_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) - self.stock_in_transit_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) - self.demand_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) - self.reward_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) - self.balance_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) - self.order_to_distribute = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) - for i, sku_name in enumerate(self.sku_to_track): - self.stock_status[episod, t, i] = stock[sku_name] - self.stock_in_transit_status[episod, - t, i] = order_in_transit[sku_name] - self.demand_status[episod, t, i] = demands[sku_name] - self.reward_status[episod, t, i] = rewards[sku_name] - self.balance_status[episod, t, i] = balances[sku_name] - self.order_to_distribute[episod, t, - i] = order_to_distribute[sku_name] - - def render_sku(self, loc_path): - sku_name_dict = {} - for agent in self.env._agent_list: - if agent.is_facility: - sku_name = f"{agent.facility_id}_{agent.agent_type}" - else: - sku_name = f"{agent.id}_{agent.sku.id}_{agent.agent_type}" - sku_name_dict[agent.id] = sku_name - - for i, sku_name in enumerate(self.sku_to_track): - fig, ax = plt.subplots(3, 1, figsize=(25, 10)) - x = np.linspace(0, self.episod_len, self.episod_len) - stock = self.stock_status[0, :, i] - order_in_transit = self.stock_in_transit_status[0, :, i] - demand = self.demand_status[0, :, i] - reward = self.reward_status[0, :, i] - balance = self.balance_status[0, :, i] - order_to_distribute = self.order_to_distribute[0, :, i] - ax[0].set_title('SKU Stock Status by Episod') - for y_label, y in [('stock', stock), - ('order_in_transit', order_in_transit), - ('demand', demand), - ('order_to_distribute', order_to_distribute)]: - ax[0].plot(x, y, label=y_label) - - ax[1].set_title('SKU Reward / Balance Status by Episod') - ax[1].plot(x, balance, label='Balance') - ax_r = ax[1].twinx() - ax_r.plot(x, reward, label='Reward', color='r') - fig.legend() - fig.savefig(f"{loc_path}/{sku_name_dict[sku_name]}.png") - plt.close(fig=fig) - - def render(self, file_name, metrics, facility_types): - fig, axs = plt.subplots(2, 1, figsize=(25, 10)) - x = np.linspace(0, self.episod_len, self.episod_len) - - _agent_list = [] - _step_idx = [] - for i, agent_info in enumerate(self.facility_names): - if agent_info.agent_type in facility_types: - _agent_list.append(agent_info.id) - _step_idx.append(i) - _step_metrics = [metrics[0, :, i] for i in _step_idx] - - # axs[0].set_title('Global balance') - # axs[0].plot(x, self.global_balances.T) - - axs[0].set_title('Cumulative Sum') - axs[0].plot(x, np.cumsum(np.sum(_step_metrics, axis=0))) - - axs[1].set_title('Breakdown by Agent (One Episod)') - axs[1].plot(x, np.cumsum(_step_metrics, axis=1).T) - axs[1].legend(_agent_list, loc='upper left') - - fig.savefig(file_name) - plt.close(fig=fig) - # plt.show() - - def run_wth_render(self, facility_types): - self.env.reset() - self.env.start() - # self.learner.policy.eval_mode() - for epoch in range(self.episod_len): - # action = self.learner.policy.choose_action(self.env.state) - action = {id_: self.learner.policy[id_].choose_action(st) for id_, st in self.env.state.items()} - self.learner._logger.info(f"epoch: {epoch}, action: {action}") - self.env.step(action) - self.learner._logger.info(f"epoch: {epoch}, action: {self.env.get_action(action)}") - if hasattr(self.env, "consumer2product"): - self.learner._logger.info(f"consumer2product: {self.env.consumer2product}") - self.env.get_reward() - step_balances = self.env.balance_status - step_rewards = self.env.reward_status - - self.add_sample(0, epoch, sum(step_balances.values()), sum( - step_rewards.values()), step_balances, step_rewards) - stock_status = self.env.stock_status - order_in_transit_status = self.env.order_in_transit_status - demand_status = self.env.demand_status - reward_status = self.env.reward_status - balance_status = self.env.balance_status - order_to_distribute_status = self.env.order_to_distribute_status - - self.add_sku_status(0, epoch, stock_status, - order_in_transit_status, demand_status, - reward_status, balance_status, - order_to_distribute_status) - - _step_idx = [] - for i, agent_info in enumerate(self.facility_names): - if agent_info.agent_type in facility_types: - _step_idx.append(i) - _step_metrics = [self.step_rewards[0, :, i] for i in _step_idx] - _step_metrics_list = np.cumsum(np.sum(_step_metrics, axis=0)) - return np.sum(_step_metrics), _step_metrics_list - - def run_and_render(self, loc_path, facility_types): - metric, metric_list = self.run_wth_render( - facility_types=facility_types) - self.render('%s/plot_balance.png' % - loc_path, self.step_balances, facility_types) - self.render('%s/plot_reward.png' % - loc_path, self.step_rewards, facility_types) - self.render_sku(loc_path) - return metric, metric_list +import numpy as np +import matplotlib.pyplot as plt +import os +from env_wrapper import sku_agent_types + + +class SimulationTracker: + def __init__(self, episod_len, n_episods, env, learner): + self.episod_len = episod_len + self.global_balances = np.zeros((n_episods, episod_len)) + self.global_rewards = np.zeros((n_episods, episod_len)) + self.env = env + self.learner = learner + self.facility_names = [] + for agent in self.env._agent_list: + if agent.agent_type in sku_agent_types: + self.facility_names.append(agent) + self.step_balances = np.zeros( + (n_episods, self.episod_len, len(self.facility_names))) + self.step_rewards = np.zeros( + (n_episods, self.episod_len, len(self.facility_names))) + self.n_episods = n_episods + self.sku_to_track = None + self.stock_status = None + self.stock_in_transit_status = None + self.reward_status = None + self.demand_status = None + self.reward_discount_status = None + self.order_to_distribute = None + + def add_sample(self, episod, t, global_balance, global_reward, step_balances, step_rewards): + self.global_balances[episod, t] = global_balance + self.global_rewards[episod, t] = global_reward + for i, f in enumerate(self.facility_names): + self.step_balances[episod, t, i] = step_balances[f.id] + self.step_rewards[episod, t, i] = step_rewards[f.id] + + def add_sku_status(self, episod, t, stock, order_in_transit, demands, rewards, balances, order_to_distribute): + if self.sku_to_track is None: + self.sku_to_track = list(rewards.keys()) + self.stock_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.stock_in_transit_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.demand_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.reward_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.balance_status = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + self.order_to_distribute = np.zeros( + (self.n_episods, self.episod_len, len(self.sku_to_track))) + for i, sku_name in enumerate(self.sku_to_track): + self.stock_status[episod, t, i] = stock[sku_name] + self.stock_in_transit_status[episod, + t, i] = order_in_transit[sku_name] + self.demand_status[episod, t, i] = demands[sku_name] + self.reward_status[episod, t, i] = rewards[sku_name] + self.balance_status[episod, t, i] = balances[sku_name] + self.order_to_distribute[episod, t, + i] = order_to_distribute[sku_name] + + def render_sku(self, loc_path): + sku_name_dict = {} + facility_type_dict = {} + for agent in self.env._agent_list: + if agent.is_facility: + sku_name = f"{agent.facility_id}_{agent.agent_type}" + else: + sku_name = f"{agent.id}_{agent.sku.id}_{agent.agent_type}" + sku_name_dict[agent.id] = sku_name + facility_type_dict[agent.id] = self.env.env.summary["node_mapping"]["facilities"][agent.facility_id]['class'].__name__ + + for i, sku_name in enumerate(self.sku_to_track): + fig, ax = plt.subplots(4, 1, figsize=(25, 10)) + x = np.linspace(0, self.episod_len, self.episod_len) + stock = self.stock_status[0, :, i] + order_in_transit = self.stock_in_transit_status[0, :, i] + demand = self.demand_status[0, :, i] + reward = self.reward_status[0, :, i] + balance = self.balance_status[0, :, i] + order_to_distribute = self.order_to_distribute[0, :, i] + ax[0].set_title('SKU Stock Status by Episod') + for y_label, y in [('stock', stock), + ('order_in_transit', order_in_transit), + ('order_to_distribute', order_to_distribute)]: + ax[0].plot(x, y, label=y_label) + + ax[1].set_title('SKU Reward / Balance Status by Episod') + ax[1].plot(x, balance, label='Balance') + ax_r = ax[1].twinx() + ax_r.plot(x, reward, label='Reward', color='r') + + ax[3].set_title('SKU demand') + ax[3].plot(x, demand, label="Demand") + + fig.legend() + fig.savefig(f"{loc_path}/{facility_type_dict[sku_name]}_{sku_name_dict[sku_name]}.png") + plt.close(fig=fig) + + def render(self, file_name, metrics, facility_types): + fig, axs = plt.subplots(2, 1, figsize=(25, 10)) + x = np.linspace(0, self.episod_len, self.episod_len) + + _agent_list = [] + _step_idx = [] + for i, agent_info in enumerate(self.facility_names): + if agent_info.agent_type in facility_types: + _agent_list.append(agent_info.id) + _step_idx.append(i) + _step_metrics = [metrics[0, :, i] for i in _step_idx] + + # axs[0].set_title('Global balance') + # axs[0].plot(x, self.global_balances.T) + + axs[0].set_title('Cumulative Sum') + axs[0].plot(x, np.cumsum(np.sum(_step_metrics, axis=0))) + + axs[1].set_title('Breakdown by Agent (One Episod)') + axs[1].plot(x, np.cumsum(_step_metrics, axis=1).T) + axs[1].legend(_agent_list, loc='upper left') + + fig.savefig(file_name) + plt.close(fig=fig) + # plt.show() + + def run_wth_render(self, facility_types): + self.env.reset() + self.env.start() + # self.learner.policy.eval_mode() + for epoch in range(self.episod_len): + # action = self.learner.policy.choose_action(self.env.state) + action = {id_: self.learner.policy[id_].choose_action(st) for id_, st in self.env.state.items()} + self.learner._logger.info(f"epoch: {epoch}, action: {action}") + self.env.step(action) + self.learner._logger.info(f"epoch: {epoch}, action: {self.env.get_action(action)}") + if hasattr(self.env, "consumer2product"): + self.learner._logger.info(f"consumer2product: {self.env.consumer2product}") + self.env.get_reward() + step_balances = self.env.balance_status + step_rewards = self.env.reward_status + + self.add_sample(0, epoch, sum(step_balances.values()), sum( + step_rewards.values()), step_balances, step_rewards) + stock_status = self.env.stock_status + order_in_transit_status = self.env.order_in_transit_status + demand_status = self.env.demand_status + reward_status = self.env.reward_status + balance_status = self.env.balance_status + order_to_distribute_status = self.env.order_to_distribute_status + + self.add_sku_status(0, epoch, stock_status, + order_in_transit_status, demand_status, + reward_status, balance_status, + order_to_distribute_status) + + _step_idx = [] + for i, agent_info in enumerate(self.facility_names): + if agent_info.agent_type in facility_types: + _step_idx.append(i) + _step_metrics = [self.step_rewards[0, :, i] for i in _step_idx] + _step_metrics_list = np.cumsum(np.sum(_step_metrics, axis=0)) + return np.sum(_step_metrics), _step_metrics_list + + def run_and_render(self, loc_path, facility_types): + metric, metric_list = self.run_wth_render( + facility_types=facility_types) + self.render('%s/plot_balance.png' % + loc_path, self.step_balances, facility_types) + self.render('%s/plot_reward.png' % + loc_path, self.step_rewards, facility_types) + self.render_sku(loc_path) + return metric, metric_list From ac8c2a4f446fca3bd6a1350212262aa01afaa45a Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 13 May 2021 10:47:22 +0800 Subject: [PATCH 246/482] bug fix: consumer does not reset order cost --- maro/simulator/scenarios/supply_chain/units/consumer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 2b06eafa0..097d60c19 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -146,6 +146,7 @@ def post_step(self, tick: int): if self.order_product_cost > 0: self.data_model.order_product_cost = 0 + self.order_product_cost = 0 def reset(self): super(ConsumerUnit, self).reset() From 263e88a71764f3d1e1c3ca1b1657b7fb46ed77b9 Mon Sep 17 00:00:00 2001 From: lesong Date: Thu, 13 May 2021 02:51:02 +0000 Subject: [PATCH 247/482] fix vehicle routing --- examples/supply_chain/env_wrapper.py | 6 ++-- .../supply_chain/or_policy/minmax_policy.py | 3 +- .../supply_chain/topologies/test/config.yml | 36 +++++++++---------- .../scenarios/supply_chain/units/vehicle.py | 21 +++++------ 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 352ab0938..2e16c92e3 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -202,11 +202,11 @@ def get_or_policy_state(self, state, agent_info): product_info = facility[agent_info.sku.id] if "consumer" in product_info: consumer_index = product_info["consumer"].node_index - state['order_cost'] = self.consumer_ss[self.env.tick:consumer_index:"order_cost"] + state['order_cost'] = self.consumer_ss[self.env.tick:consumer_index:"order_cost"].flatten()[0] state['storage_capacity'] = facility['storage'].config["capacity"] state['storage_levels'] = self._storage_product_numbers[agent_info.facility_id] state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 state['vlt'] = agent_info.sku.vlt state['service_level'] = agent_info.sku.service_level return state @@ -295,7 +295,7 @@ def get_state(self, event): storage_index = self._facility2storage_index_dict[agent_info.facility_id] np_state = self.get_rl_policy_state(state, agent_info) - # np_state = self.get_or_policy_state(state, agent_info) + np_state = self.get_or_policy_state(state, agent_info) # agent_info.agent_type -> policy final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state diff --git a/examples/supply_chain/or_policy/minmax_policy.py b/examples/supply_chain/or_policy/minmax_policy.py index 1cee7c453..873a41297 100644 --- a/examples/supply_chain/or_policy/minmax_policy.py +++ b/examples/supply_chain/or_policy/minmax_policy.py @@ -26,11 +26,12 @@ def compute_action(self, state): most_needed_product_id = state['product_idx'] # stop placing orders if no risk of out of stock - vlt_buffer_days = 10 + vlt_buffer_days = 1 vlt = state['vlt'] + vlt_buffer_days sale_mean, sale_std = state['sale_mean'], state['sale_std'] service_level = state['service_level'] r = (vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level)) + print(booked_inventory, most_needed_product_id, r) if booked_inventory[most_needed_product_id] > r: return 0 R = 3*r diff --git a/examples/supply_chain/topologies/test/config.yml b/examples/supply_chain/topologies/test/config.yml index 015688428..e7d7c6665 100644 --- a/examples/supply_chain/topologies/test/config.yml +++ b/examples/supply_chain/topologies/test/config.yml @@ -43,10 +43,6 @@ facility_definitions: is_template: true config: agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" manufacture: class: "SimpleManufactureUnit" config: @@ -108,7 +104,7 @@ facility_definitions: normal_vehicle: &normal_vehicle class: "VehicleUnit" config: - patient: 10 + patient: 2 # a normal distribution definition normal_distribution: &normal_distribution @@ -166,17 +162,17 @@ huge_storage: &huge_storage # config of data model of this unit config: # other config or storage unit - capacity: 10000 + capacity: 5000 unit_storage_cost: 1 medium_storage: &medium_storage config: - capacity: 5000 + capacity: 20000 unit_storage_cost: 1 small_storage: &small_storage config: - capacity: 3000 + capacity: 10000 unit_storage_cost: 1 # sku list in this world @@ -285,24 +281,24 @@ world: skus: sku1: - price: 100 - cost: 10 - init_stock: 1000 - sale_gamma: 100 + price: 150 + cost: 100 + init_stock: 80 + sale_gamma: 20 backlog_ratio: 0.1 # optional vlt: 1 sku2: - price: 200 - cost: 10 - init_stock: 1000 - sale_gamma: 100 + price: 300 + cost: 200 + init_stock: 400 + sale_gamma: 80 backlog_ratio: 0.1 vlt: 1 sku3: - price: 300 - cost: 10 - init_stock: 1000 - sale_gamma: 100 + price: 450 + cost: 300 + init_stock: 200 + sale_gamma: 50 backlog_ratio: 0.1 vlt: 1 diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index d1751896b..c0c126fb2 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -66,7 +66,8 @@ def schedule(self, destination: object, product_id: int, quantity: int, vlt: int raise Exception(f"Destination {destination} is unreachable") # Steps to destination. - self.steps = len(self.path) // vlt + # self.steps = len(self.path) // vlt + self.steps = vlt # We are waiting for product loading. self.location = 0 @@ -160,15 +161,15 @@ def step(self, tick: int): self.location = len(self.path) - 1 else: # Avoid update under idle state. - if self.location > 0: - # Try to unload./////////////////////////////////////////////////////////////////// - if self.payload > 0: - self.try_unload() - - # Back to source if we unload all. - if self.payload == 0: - self._reset_internal_states() - self._reset_data_model() + # if self.location > 0: + # Try to unload./////////////////////////////////////////////////////////////////// + if self.payload > 0: + self.try_unload() + + # Back to source if we unload all. + # if self.payload == 0: + self._reset_internal_states() + self._reset_data_model() self.cost = self.payload * self.unit_transport_cost From d625db9d30c228e1611595e035bc45a69fac02ce Mon Sep 17 00:00:00 2001 From: lesong Date: Thu, 13 May 2021 03:33:30 +0000 Subject: [PATCH 248/482] baseline policies --- examples/supply_chain/topologies/test/config.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/supply_chain/topologies/test/config.yml b/examples/supply_chain/topologies/test/config.yml index e7d7c6665..652b94f18 100644 --- a/examples/supply_chain/topologies/test/config.yml +++ b/examples/supply_chain/topologies/test/config.yml @@ -162,17 +162,17 @@ huge_storage: &huge_storage # config of data model of this unit config: # other config or storage unit - capacity: 5000 + capacity: 10000 unit_storage_cost: 1 medium_storage: &medium_storage config: - capacity: 20000 + capacity: 5000 unit_storage_cost: 1 small_storage: &small_storage config: - capacity: 10000 + capacity: 3000 unit_storage_cost: 1 # sku list in this world @@ -216,7 +216,7 @@ world: skus: sku1: # sku name and attributes needed for this facility init_stock: 1000 - product_unit_cost: 1 + product_unit_cost: 90 production_rate: 100 type: "production" # production means this is the output production of this facility cost: 90 @@ -224,7 +224,7 @@ world: vlt: 2 sku2: init_stock: 1000 - product_unit_cost: 1 + product_unit_cost: 150 production_rate: 100 type: "production" cost: 150 @@ -233,7 +233,7 @@ world: sku3: init_stock: 1000 production_rate: 100 - product_unit_cost: 1 + product_unit_cost: 200 type: "production" cost: 200 price: 300 From a911917404eba863f6a1686ea0e5c23b96233d20 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 13 May 2021 14:41:38 +0800 Subject: [PATCH 249/482] correct the product reward calculation order, from downstreams to upstreams --- examples/supply_chain/env_wrapper.py | 83 +++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 2e16c92e3..cc02e8e9d 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -295,7 +295,7 @@ def get_state(self, event): storage_index = self._facility2storage_index_dict[agent_info.facility_id] np_state = self.get_rl_policy_state(state, agent_info) - np_state = self.get_or_policy_state(state, agent_info) + #np_state = self.get_or_policy_state(state, agent_info) # agent_info.agent_type -> policy final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state @@ -348,7 +348,8 @@ def get_action(self, action_by_agent): continue sku = self._units_mapping[unit_id][3] - reward_discount = 0 + # give it 1 means no discount, not 0 + reward_discount = 1 env_action[unit_id] = ConsumerAction( unit_id, @@ -744,13 +745,23 @@ def __init__(self, env: Env): self.product_id2index_dict[product["id"]] = len(self.products) + downstream_product_units = [] + downstreams = facility["downstreams"] + + if downstreams and len(downstreams) > 0 and product_id in downstreams: + for dfacility in downstreams[product_id]: + dproducts = self.facilities[dfacility]["units"]["products"] + + downstream_product_units.append(dproducts[product_id]["id"]) + self.products.append(( product["id"], product_id, + product["node_index"], facility["units"]["storage"]["node_index"], facility["units"]["storage"]["config"]["unit_storage_cost"], distribution["node_index"] if distribution is not None else None, - facility["downstreams"], + downstream_product_units, None if consumer is None else ( consumer["id"], consumer["node_index"]), None if seller is None else ( @@ -769,8 +780,55 @@ def __init__(self, env: Env): ] if distribution is not None else [] )) + # TODO: order products make sure calculate reward from downstream to upstream + tmp_product_unit_dict = {} + + for product in self.products: + tmp_product_unit_dict[product[0]] = product + + self._ordered_products = [] + + tmp_stack = [] + + for product in self.products: + # skip if already being processed + if tmp_product_unit_dict[product[0]] is None: + continue + + for dproduct in product[6]: + # push downstream id to stack + tmp_stack.append(dproduct) + + # insert current product to list head + self._ordered_products.insert(0, product) + # mark it as processed + tmp_product_unit_dict[product[0]] = None + + while len(tmp_stack) > 0: + # process downstream of product unit in stack + dproduct_unit_id = tmp_stack.pop() + + # if it was processed then ignore + if tmp_product_unit_dict[dproduct_unit_id] is None: + continue + + # or extract it downstreams + dproduct_unit = tmp_product_unit_dict[dproduct_unit_id] + + dproduct_downstreams = dproduct_unit[6] + + for dproduct in dproduct_downstreams: + tmp_stack.append(dproduct) + + # current unit in final list + self._ordered_products.insert(0, dproduct_unit) + tmp_product_unit_dict[dproduct_unit_id] = None + self.total_balance_sheet = defaultdict(int) + # tick -> (product unit id, sku id, manufacture number, manufacture cost, checkin order, delay penaty) + self._supplier_reward_factors = {} + def calc(self): tick = self.env.tick @@ -854,8 +912,8 @@ def calc(self): # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss # profit = same as above # reward = same as above - for i, product in enumerate(self.products): - id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = product + for product in self._ordered_products: + id, product_id, i, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = product if consumer: product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer[1]] @@ -885,16 +943,11 @@ def calc(self): product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + \ product_distribution_balance_sheet_profit[distribution_index] - if downstreams and len(downstreams) > 0: - if product_id in downstreams: - for dfacility in downstreams[product_id]: - dproducts = self.facilities[dfacility]["units"]["products"] - - did = dproducts[product_id]["id"] - - product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] - product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] - product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] + if len(downstreams) > 0: + for did in downstreams: + product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] + product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] + product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss From 44acf00ff4a7613c62740523d22fe3c22f2eb013 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 13 May 2021 11:14:20 +0000 Subject: [PATCH 250/482] checked out files from v0.2_sc2 --- maro/rl/training/learner.py | 122 ++++++++++ maro/rl/training/policy_manager.py | 98 ++++++++ maro/rl/training/rollout_manager.py | 339 ++++++++++++++++++++++++++++ 3 files changed, 559 insertions(+) create mode 100644 maro/rl/training/learner.py create mode 100644 maro/rl/training/policy_manager.py create mode 100644 maro/rl/training/rollout_manager.py diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py new file mode 100644 index 000000000..782a149a2 --- /dev/null +++ b/maro/rl/training/learner.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict, namedtuple +from os import getcwd +from typing import List, Union + +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.utils import Logger + +from .policy_manager import AbsPolicyManager +from .rollout_manager import AbsRolloutManager + +InterEpisodeSchedule = namedtuple("InterEpisodeSchedule", ["start", "interval"]) +IntraEpisodeSchedule = namedtuple("IntraEpisodeSchedule", ["start_ep", "interval", "end_ep_update"]) + + +class Learner: + """Learner class for distributed training. + + Args: + policy (MultiAgentPolicy): Learning agents. + + """ + def __init__( + self, + policy_manager: AbsPolicyManager, + rollout_manager: AbsRolloutManager, + num_episodes: int, + eval_schedule: Union[int, List[int]] = None, + log_dir: str = getcwd(), + **end_of_episode_kwargs + ): + self.logger = Logger("LEARNER", dump_folder=log_dir) + self.policy_manager = policy_manager + self.rollout_manager = rollout_manager + + self.num_episodes = num_episodes + + # self._init_update_schedule(policy_update_schedule) + + # evaluation schedule + if eval_schedule is None: + eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + + self._eval_schedule = eval_schedule + self._eval_schedule.sort() + if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: + self._eval_schedule.append(num_episodes) + + self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") + self._eval_point_index = 0 + + self._end_of_episode_kwargs = end_of_episode_kwargs + self._updated_policy_ids = self.policy_manager.names + self._last_step_set = {} + + def run(self): + for ep in range(1, self.num_episodes + 1): + self._train(ep) + if ep == self._eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self.rollout_manager.evaluate(self._eval_point_index) + + def _train(self, ep: int): + num_experiences_collected = 0 + segment = 0 + self.rollout_manager.reset() + while not self.rollout_manager.episode_complete: + segment += 1 + # experience collection + policy_state_dict = self.policy_manager.get_state() + exp_by_agent = self.rollout_manager.collect(ep, segment, policy_state_dict) + self.policy_manager.on_experiences(exp_by_agent) + num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) + + # performance details + self.logger.debug(f"ep {ep} summary - experiences collected: {num_experiences_collected}") + + self.end_of_episode(ep, **self._end_of_episode_kwargs) + + def end_of_episode(self, ep: int, **kwargs): + pass + + def _init_update_schedule(self, schedule): + self._pending_policies_by_segment = defaultdict(set) + self._pending_policies_by_episode = defaultdict(list) + self._step_schedule_opt = {} + if isinstance(schedule, dict): + for policy_id, sch in schedule.items(): + if isinstance(sch, IntraEpisodeSchedule): + self._step_schedule_opt[policy_id] = (sch.start_ep, sch.interval) + if sch.end_ep_update: + for ep in range(sch.start_ep, self.num_episodes + 1): + self._pending_policies_by_episode[ep].append(policy_id) + self._pending_policies_by_segment[sch.interval].add(policy_id) + elif isinstance(sch, InterEpisodeSchedule): + for ep in range(sch.start, self.num_episodes + 1, step=sch.interval): + self._pending_policies_by_episode[ep].append(policy_id) + else: + if isinstance(schedule, IntraEpisodeSchedule): + self._step_schedule_opt["*"] = (schedule.start_ep, schedule.interval) + self._pending_policies_by_segment[schedule.interval].add("*") + if schedule.end_ep_update: + for ep in range(schedule.start_ep, self.num_episodes + 1): + self._pending_policies_by_episode[ep] = self.policy_manager.names + else: + for ep in range(schedule.start, self.num_episodes + 1, step=schedule.interval): + self._pending_policies_by_episode[ep] = self.policy_manager.names + + def _get_pending_policy_ids(self, ep: int, segment: int) -> List[str]: + for policy_id in self._pending_policies_by_segment[segment]: + if segment == self._last_step_set[policy_id]: + next_segment = segment + self._step_schedule_opt[policy_id][1] + self._pending_policies_by_segment[next_segment].append(policy_id) + + return [ + policy_id for policy_id in self._pending_policies_by_segment[segment] if self._step_schedule_opt[policy_id][0] <= ep + ] diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py new file mode 100644 index 000000000..a97cea1ab --- /dev/null +++ b/maro/rl/training/policy_manager.py @@ -0,0 +1,98 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from collections import defaultdict, namedtuple +from os import getcwd +from typing import Dict, Union + +from maro.rl.experience import ExperienceSet +from maro.rl.policy import AbsPolicy +from maro.utils import Logger + +ExperienceTrigger = namedtuple("ExperienceTrigger", ["trigger", "warmup"], defaults=[1]) + + +class AbsPolicyManager(ABC): + def __init__(self, agent2policy: Dict[str, str]): + self.agent2policy = agent2policy + self._policy_names = list(agent2policy.values()) + + @property + def names(self): + return self._policy_names + + @abstractmethod + def on_experiences(self, exp_by_agent: Dict[str, ExperienceSet]): + raise NotImplementedError + + @abstractmethod + def choose_action(self, state_by_agent): + raise NotImplementedError + + @abstractmethod + def get_state(self): + raise NotImplementedError + + +class LocalPolicyManager(AbsPolicyManager): + def __init__( + self, + policy_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str], + experience_trigger: Union[ExperienceTrigger, dict] = None, + log_dir: str = getcwd() + ): + super().__init__(agent2policy) + self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) + self.policy_dict = policy_dict + self._policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self._agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in self.agent2policy.items(): + self._agent_groups_by_policy[policy_id].append(agent_id) + + if experience_trigger is None: + self._experience_trigger = {} + elif isinstance(experience_trigger, dict): + self._experience_trigger = experience_trigger + else: + self._experience_trigger = { + policy_id: experience_trigger for policy_id, policy in self.policy_dict.items() + if hasattr(policy, "experience_manager") and hasattr(policy, "update") + } + + self._new_exp_counter = defaultdict(int) + self._updated_policy_ids = set() + + def choose_action(self, state_by_agent: dict): + return {agent_id: self._policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} + + def on_experiences(self, exp_by_agent: Dict[str, ExperienceSet]): + for agent_id, exp in exp_by_agent.items(): + policy_id = self.agent2policy[agent_id] + policy = self.policy_dict[policy_id] + if hasattr(policy, "experience_manager"): + self._new_exp_counter[policy_id] += exp.size + policy.experience_manager.put(exp) + + for policy_id, policy in self.policy_dict.items(): + if hasattr(policy, "experience_manager"): + print(f"Policy {policy_id}: exp mem size = {policy.experience_manager.size}, new exp = {self._new_exp_counter[policy_id]}") + if ( + policy_id not in self._experience_trigger or + policy.experience_manager.size >= self._experience_trigger[policy_id].warmup and + self._new_exp_counter[policy_id] >= self._experience_trigger[policy_id].trigger + ): + policy.update() + self._new_exp_counter[policy_id] = 0 + self._updated_policy_ids.add(policy_id) + + if self._updated_policy_ids: + self._logger.info(f"Updated policies {self._updated_policy_ids}") + + def get_state(self): + policy_state_dict = { + policy_id: self.policy_dict[policy_id].get_state() for policy_id in self._updated_policy_ids + } + self._updated_policy_ids.clear() + return policy_state_dict diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py new file mode 100644 index 000000000..3a525b794 --- /dev/null +++ b/maro/rl/training/rollout_manager.py @@ -0,0 +1,339 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from abc import ABC, abstractmethod +from collections import defaultdict +from os import getcwd +from random import choices +from typing import Dict + +from maro.communication import Proxy, SessionType +from maro.rl.experience import ExperienceSet +from maro.rl.exploration import AbsExploration +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.policy import AbsPolicy +from maro.utils import Logger + +from .message_enums import MsgTag, MsgKey + + +class AbsRolloutManager(ABC): + """Learner class for distributed training. + + Args: + agent (Union[AbsPolicy, MultiAgentWrapper]): Learning agents. + scheduler (Scheduler): . + num_actors (int): Expected number of actors in the group identified by ``group_name``. + group_name (str): Identifier of the group to which the actor belongs. It must be the same group name + assigned to the actors (and roll-out clients, if any). + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to None. + update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger + learner updates, i.e., model training. + """ + def __init__(self): + super().__init__() + self.episode_complete = False + + @abstractmethod + def collect(self, ep: int, segment: int, policy_state_dict: dict): + """Collect experiences from actors.""" + raise NotImplementedError + + @abstractmethod + def evaluate(self, policy_dict: dict): + raise NotImplementedError + + def reset(self): + self.episode_complete = False + + +class LocalRolloutManager(AbsRolloutManager): + def __init__( + self, + env: AbsEnvWrapper, + policy_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str], + exploration_dict: Dict[str, AbsExploration] = None, + agent2exploration: Dict[str, str] = None, + num_steps: int = -1, + eval_env: AbsEnvWrapper = None, + log_env_metrics: bool = True, + log_total_reward: bool = True, + log_dir: str = getcwd(), + ): + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + self._logger = Logger("LOCAL_ROLLOUT_MANAGER", dump_folder=log_dir) + + self.env = env + self.eval_env = eval_env if eval_env else self.env + + # mappings between agents and policies + self.policy_dict = policy_dict + self._agent2policy = agent2policy + self._policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self._agent2policy.items()} + self._agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent2policy.items(): + self._agent_groups_by_policy[policy_id].append(agent_id) + + # mappings between exploration schemes and agents + self.exploration_dict = exploration_dict + if exploration_dict: + self._agent2exploration = agent2exploration + self._exploration = { + agent_id: self.exploration_dict[exploration_id] + for agent_id, exploration_id in self._agent2exploration.items() + } + self._agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in self._agent2exploration.items(): + self._agent_groups_by_exploration[exploration_id].append(agent_id) + + self._num_steps = num_steps if num_steps > 0 else float("inf") + self._log_env_metrics = log_env_metrics + self._log_total_reward = log_total_reward + self._eval_ep = 0 + + def collect(self, ep: int, segment: int, policy_state_dict: dict): + t0 = time.time() + learning_time = 0 + num_experiences_collected = 0 + + if self.exploration_dict: + exploration_params = { + tuple(agent_ids): self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self._agent_groups_by_exploration.items() + } + self._logger.debug(f"Exploration parameters: {exploration_params}") + + if self.env.state is None: + self._logger.info(f"Collecting data from episode {ep}, segment {segment}") + if hasattr(self, "exploration_dict"): + exploration_params = { + tuple(agent_ids): self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self._agent_groups_by_exploration.items() + } + self._logger.debug(f"Exploration parameters: {exploration_params}") + + self.env.reset() + self.env.start() # get initial state + + # load policies + self._load_policy_states(policy_state_dict) + + start_step_index = self.env.step_index + 1 + steps_to_go = self._num_steps + while self.env.state and steps_to_go > 0: + if self.exploration_dict: + action = { + id_: + self._exploration[id_](self._policy[id_].choose_action(st)) + if id_ in self._exploration else self._policy[id_].choose_action(st) + for id_, st in self.env.state.items() + } + else: + action = {id_: self._policy[id_].choose_action(st) for id_, st in self.env.state.items()} + + self.env.step(action) + steps_to_go -= 1 + + self._logger.info( + f"Roll-out finished for ep {ep}, segment {segment}" + f"(steps {start_step_index} - {self.env.step_index})" + ) + + # update the exploration parameters if an episode is finished + if not self.env.state and self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + + # performance details + if not self.env.state: + if self._log_env_metrics: + self._logger.info(f"ep {ep}: {self.env.metrics}") + if self._log_total_reward: + self._logger.info(f"ep {ep} total reward received: {self.env.total_reward}") + + self._logger.debug( + f"ep {ep} summary - " + f"running time: {time.time() - t0}" + f"env steps: {self.env.step_index}" + f"learning time: {learning_time}" + f"experiences collected: {num_experiences_collected}" + ) + + return self.env.get_experiences() + + def evaluate(self, policy_state_dict: dict): + self._logger.info("Evaluating...") + self._eval_ep += 1 + self._load_policy_states(policy_state_dict) + self.eval_env.save_replay = False + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = {id_: self._policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} + self.eval_env.step(action) + + if not self.eval_env.state: + self._logger.info(f"total reward: {self.eval_env.total_reward}") + + if self._log_env_metrics: + self._logger.info(f"eval ep {self._eval_ep}: {self.eval_env.metrics}") + + def _load_policy_states(self, policy_state_dict: dict): + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].set_state(policy_state) + + if policy_state_dict: + self._logger.info(f"updated policies {list(policy_state_dict.keys())}") + + +class ParallelRolloutManager(AbsRolloutManager): + """Learner class for distributed training. + + Args: + agent (Union[AbsPolicy, MultiAgentWrapper]): Learning agents. + num_actors (int): Expected number of actors in the group identified by ``group_name``. + group_name (str): Identifier of the group to which the actor belongs. It must be the same group name + assigned to the actors (and roll-out clients, if any). + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to None. + update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger + learner updates, i.e., model training. + """ + def __init__( + self, + num_actors: int, + group_name: str, + num_steps: int, + required_finishes: int = None, + max_staleness: int = 0, + num_eval_actors: int = 1, + log_env_metrics: bool = False, + log_dir: str = getcwd(), + **proxy_kwargs + ): + super().__init__() + if required_finishes and required_finishes > num_actors: + raise ValueError("required_finishes cannot exceed the number of available actors") + if num_eval_actors > num_actors: + raise ValueError("num_eval_actors cannot exceed the number of available actors") + + self._logger = Logger("PARALLEL_ROLLOUT_MANAGER", dump_folder=log_dir) + self.num_actors = num_actors + peers = {"actor": num_actors} + self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_kwargs) + self._actors = self._proxy.peers["actor"] # remote actor ID's + + if required_finishes is None: + required_finishes = self.num_actors + self._logger.info(f"Required number of actor finishes is set to {required_finishes}") + + self.required_finishes = required_finishes + self._num_steps = num_steps + self._max_staleness = max_staleness + self.total_experiences_collected = 0 + self.total_env_steps = 0 + self.total_reward = defaultdict(float) + self._log_env_metrics = log_env_metrics + + self._num_eval_actors = num_eval_actors + self._eval_ep = 0 + + def collect( + self, + episode_index: int, + segment_index: int, + policy_state_dict: dict + ): + """Collect experiences from actors.""" + msg_body = { + MsgKey.EPISODE_INDEX: episode_index, + MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.NUM_STEPS: self._num_steps, + MsgKey.POLICY: policy_state_dict, + MsgKey.RETURN_ENV_METRICS: self._log_env_metrics + } + + if self._log_env_metrics: + self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") + + self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) + self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") + + # Receive roll-out results from remote actors + combined_exp_by_agent = defaultdict(ExperienceSet) + num_segment_finishes = num_episode_finishes = 0 + for msg in self._proxy.receive(): + if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: + self._logger.info( + f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.EPISODE_INDEX]} " + f"(expected message type {MsgTag.COLLECT} and roll-out index {episode_index})" + ) + continue + + # log roll-out summary + if self._log_env_metrics: + env_metrics = msg.body[MsgKey.METRICS] + self._logger.info(f"env_metrics: {env_metrics}") + + if segment_index - msg.body[MsgKey.SEGMENT_INDEX] <= self._max_staleness: + exp_by_agent = msg.body[MsgKey.EXPERIENCES] + self.total_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) + self.total_env_steps += msg.body[MsgKey.NUM_STEPS] + is_episode_end = msg.body[MsgKey.EPISODE_END] + if is_episode_end: + self._logger.info(f"total rewards: {msg.body[MsgKey.TOTAL_REWARD]}") + num_episode_finishes += is_episode_end + if num_episode_finishes == self.required_finishes: + self.episode_complete = True + + for agent_id, exp in exp_by_agent.items(): + combined_exp_by_agent[agent_id].extend(exp) + + if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: + num_segment_finishes += 1 + if num_segment_finishes == self.required_finishes: + break + + return combined_exp_by_agent + + def evaluate(self, policy_state_dict: dict): + """Evaluate .""" + self._eval_ep += 1 + msg_body = { + MsgKey.EPISODE_INDEX: self._eval_ep, + MsgKey.POLICY: policy_state_dict, + MsgKey.RETURN_ENV_METRICS: True + } + + actors = choices(self._actors, k=self._num_eval_actors) + self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(actor_id, msg_body) for actor_id in actors]) + self._logger.info(f"Sent evaluation requests to {actors}") + + # Receive roll-out results from remote actors + num_finishes = 0 + for msg in self._proxy.receive(): + if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE_INDEX] != self._eval_ep: + self._logger.info( + f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE_INDEX]} " + f"(expected message type {MsgTag.EVAL} and episode index {self._eval_ep})" + ) + continue + + # log roll-out summary + env_metrics = msg.body[MsgKey.METRICS] + self._logger.info(f"env metrics for evaluation episode {self._eval_ep}: {env_metrics}") + + if msg.body[MsgKey.EPISODE_INDEX] == self._eval_ep: + num_finishes += 1 + if num_finishes == self._num_eval_actors: + break + + def exit(self): + """Tell the remote actors to exit.""" + self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) + self._logger.info("Exiting...") From 11bd8c7ba9b57a43af151973ce74fe4d2ef220ea Mon Sep 17 00:00:00 2001 From: chaosyu Date: Thu, 13 May 2021 20:01:11 +0800 Subject: [PATCH 251/482] fix bug that checkin order using incorrect index --- examples/supply_chain/env_wrapper.py | 25 ++++++++++++------- .../supply_chain/topologies/test/config.yml | 2 +- .../scenarios/supply_chain/units/product.py | 13 ++++++---- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index cc02e8e9d..e8c69d658 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -160,6 +160,9 @@ def __init__(self, env: Env): self.stock_status = {} self.demand_status = {} + # key: product unit id, value: number + self.orders_from_downstreams = {} + self.consumer_orders = {} self.order_in_transit_status = {} self.order_to_distribute_status = {} @@ -295,7 +298,7 @@ def get_state(self, event): storage_index = self._facility2storage_index_dict[agent_info.facility_id] np_state = self.get_rl_policy_state(state, agent_info) - #np_state = self.get_or_policy_state(state, agent_info) + np_state = self.get_or_policy_state(state, agent_info) # agent_info.agent_type -> policy final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state @@ -348,7 +351,7 @@ def get_action(self, action_by_agent): continue sku = self._units_mapping[unit_id][3] - # give it 1 means no discount, not 0 + reward_discount = 1 env_action[unit_id] = ConsumerAction( @@ -359,6 +362,10 @@ def get_action(self, action_by_agent): sku.vlt, reward_discount ) + + self.consumer_orders[product_unit_id] = action_number + self.orders_from_downstreams[self.facility_levels[source_id][product_id]["skuproduct"].id] = action_number + # manufacturer action elif agent_id.startswith("producer"): sku = self._units_mapping[unit_id][3] @@ -838,7 +845,7 @@ def calc(self): .reshape(-1, len(self.consumer_features)) # quantity * price - consumer_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] + order_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] # balance_sheet_profit = 0 # order_cost + order_product_cost @@ -846,7 +853,7 @@ def calc(self): # consumer step reward: balance sheet los + profile * discount consumer_step_reward = consumer_step_balance_sheet_loss + \ - consumer_profit * consumer_bs_states[:, 5] + order_profit * consumer_bs_states[:, 5] # seller seller_bs_states = self.seller_ss[tick::self.seller_features]\ @@ -937,11 +944,11 @@ def calc(self): product_balance_sheet_loss[i] += storage_reward if distribution_index is not None: - product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[distribution_index] - product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[distribution_index] + product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[i] + product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[i] - product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + \ - product_distribution_balance_sheet_profit[distribution_index] + product_step_reward[i] += product_distribution_balance_sheet_loss[i] + \ + product_distribution_balance_sheet_profit[i] if len(downstreams) > 0: for did in downstreams: @@ -1008,7 +1015,7 @@ def calc(self): # For consumers. - consumer_step_balance_sheet = consumer_profit + consumer_step_balance_sheet_loss + consumer_step_balance_sheet = order_profit + consumer_step_balance_sheet_loss for id, bs, rw in zip(consumer_bs_states[:, 0], consumer_step_balance_sheet, consumer_step_reward): result[int(id)] = (bs, rw) diff --git a/examples/supply_chain/topologies/test/config.yml b/examples/supply_chain/topologies/test/config.yml index 652b94f18..636e8ecc5 100644 --- a/examples/supply_chain/topologies/test/config.yml +++ b/examples/supply_chain/topologies/test/config.yml @@ -258,7 +258,7 @@ world: skus: sku1: init_stock: 1000 - price: 100 + price: 200 vlt: 2 sku2: init_stock: 1000 diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index e1349e6f1..917e99275 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -45,6 +45,10 @@ def step(self, tick: int): for unit in self.children: unit.step(tick) + def flush_states(self): + for unit in self.children: + unit.flush_states() + if self.distribution is not None: self._checkin_order = self.distribution.check_in_order[self.product_id] self._transport_cost = self.distribution.transportation_cost[self.product_id] @@ -54,10 +58,6 @@ def step(self, tick: int): self.distribution.transportation_cost[self.product_id] = 0 self.distribution.delay_order_penalty[self.product_id] = 0 - def flush_states(self): - for unit in self.children: - unit.flush_states() - if self._checkin_order > 0: self.data_model.distribution_check_order = self._checkin_order @@ -183,7 +183,10 @@ def generate(facility, config: dict, unit_def: object): product_unit.storage = product_unit.facility.storage product_unit.distribution = product_unit.facility.distribution - for child_name in ("manufacture", "consumer", "seller"): + # NOTE: BE CAREFUL about the order, product unit will use this order update children, + # the order may affect the states. + # Here we make sure consumer is the first one, so it can place order first. + for child_name in ("consumer", "seller", "manufacture"): conf = config.get(child_name, None) if conf is not None: From bbaffc9c90fadf55d4bacbd9f07e549ae636342a Mon Sep 17 00:00:00 2001 From: lesong Date: Thu, 13 May 2021 13:43:01 +0000 Subject: [PATCH 252/482] config change --- examples/supply_chain/config.py | 1 + examples/supply_chain/config.yml | 65 +++++-------------- examples/supply_chain/distributed_launcher.py | 16 ++--- examples/supply_chain/env_wrapper.py | 42 +++++++----- examples/supply_chain/learner.py | 10 +++ examples/supply_chain/or_policies.py | 11 +++- .../supply_chain/or_policy/minmax_policy.py | 2 +- examples/supply_chain/policies.py | 32 +++++++-- examples/supply_chain/render_tools.py | 2 +- .../supply_chain/topologies/test/config.yml | 18 ++--- maro/rl/training/distributed_learner.py | 3 +- 11 files changed, 106 insertions(+), 96 deletions(-) diff --git a/examples/supply_chain/config.py b/examples/supply_chain/config.py index 682226615..fac05d6c4 100644 --- a/examples/supply_chain/config.py +++ b/examples/supply_chain/config.py @@ -24,3 +24,4 @@ config["agent_ids"] = [f"{info.agent_type}.{info.id}" for info in env.agent_idx_list] config["policy"]["consumer"]["model"]["network"]["input_dim"] = env.dim config["policy"]["producer"]["model"]["network"]["input_dim"] = env.dim +config["policy"]["consumerstore"]["model"]["network"]["input_dim"] = env.dim diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index d5d4c1ac3..35535f94f 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -6,23 +6,24 @@ env: # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ topology: test - durations: 200 # number of ticks per episode -num_episodes: 10 # number of episodes to simulate + durations: 64 # number of ticks per episode +num_episodes: 1000 # number of episodes to simulate # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps # before returning experiences to the learner. The learner uses these experiences to update the agents' policies # and sync the updated policies to the actors for the next learning cycle. -experience_update_interval: 35 -eval_schedule: 2 +experience_update_interval: 32 +eval_schedule: 20 log_env_metrics: false policy: - consumer: + consumerstore: algorithm: dqn model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. device: cpu network: hidden_dims: - - 16 - - 8 + - 256 + - 128 + - 32 output_dim: 10 activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. softmax: false @@ -31,16 +32,16 @@ policy: head: true dropout_p: 0.0 optimization: - optim_cls: rmsprop # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. + optim_cls: adam # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. optim_params: lr: 0.001 algorithm_config: - reward_discount: .9 + reward_discount: .99 train_epochs: 10 gradient_iters: 1 loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. target_update_freq: 5 # How many training iteration, to update DQN target model - soft_update_coefficient: 0.1 + soft_update_coefficient: 0.01 double: false # whether to enable double DQN experience_manager: capacity: 50000 # experience memory size @@ -56,49 +57,17 @@ policy: start_ep: 3 # must be a positive number since episode is 1-based. interval: 2 end_ep_update: true + consumer: + model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. + network: + output_dim: 10 producer: - algorithm: dqn model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. - device: cpu network: - hidden_dims: - - 16 - - 8 output_dim: 10 - activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. - optim_params: - lr: 0.001 - algorithm_config: - reward_discount: .9 - train_epochs: 10 - gradient_iters: 1 - loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. - target_update_freq: 5 # How many training iteration, to update DQN target model - soft_update_coefficient: 0.1 - double: false # whether to enable double DQN - experience_manager: - capacity: 50000 # experience memory size - # This determines how existing experiences are replaced when adding new experiences to a full experience - # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, - # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. - overwrite_type: random - batch_size: 128 - replace: true - update_schedule: - type: episode # "step" or "episode" - args: - start: 2 # must be a positive number since episode is 1-based - interval: 2 exploration: - initial_value: 0.4 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. - final_value: 0.0 + initial_value: 0.9 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. + final_value: 0.01 distributed: # this is used to group all actor / learner processes belonging to the same job for communication purposes. # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index 735ec3a9a..048da1597 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -17,10 +17,9 @@ from env_wrapper import SCEnvWrapper from exploration import exploration_dict, agent2exploration from learner import SCLearner -from or_policies import agent2policy, policy_dict, policy_update_schedule -# from policies import agent2policy, policy_dict, policy_update_schedule +# from or_policies import agent2policy, policy_dict, policy_update_schedule +from policies import agent2policy, policy_dict, policy_update_schedule -from render_tools import SimulationTracker # for distributed / multi-process training @@ -43,22 +42,17 @@ def sc_learner(): ) # create a learner to start training + env = SCEnvWrapper(Env(**config["env"])) learner = SCLearner( policy_dict, agent2policy, config["num_episodes"], policy_update_schedule, actor_manager=actor_manager, experience_update_interval=config["experience_update_interval"], eval_schedule=config["eval_schedule"], + eval_env = env, log_env_metrics=config["log_env_metrics"], log_dir=log_dir ) - # learner.run() - - env = SCEnvWrapper(Env(**config["env"])) - tracker = SimulationTracker(60, 1, env, learner) - loc_path = '/maro/supply_chain/output/' - facility_types = ["product"] - os.system(f"rm {loc_path}/*") - tracker.run_and_render(loc_path, facility_types) + learner.run() def sc_actor(name: str): diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 2e16c92e3..16455d653 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -50,7 +50,7 @@ def out_of_stock(f_state): ('max_price', ['sku_price', 'sku_cost'])] # Sku related agent types -sku_agent_types = {"consumer", "producer", "product"} +sku_agent_types = {"consumer", "consumerstore", "producer", "product", "productstore"} class UnitBaseInfo: @@ -187,7 +187,7 @@ def get_or_policy_state(self, state, agent_info): if agent_info.is_facility: return state - product_unit_id = agent_info.id if agent_info.agent_type == "product" else agent_info.parent_id + product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id id, product_id, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ self.balance_cal.products[self.balance_cal.product_id2index_dict[product_unit_id]] @@ -295,7 +295,8 @@ def get_state(self, event): storage_index = self._facility2storage_index_dict[agent_info.facility_id] np_state = self.get_rl_policy_state(state, agent_info) - np_state = self.get_or_policy_state(state, agent_info) + if agent_info.agent_type in ["consumer", "producer"]: + np_state = self.get_or_policy_state(state, agent_info) # agent_info.agent_type -> policy final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state @@ -386,7 +387,7 @@ def _update_sale_features(self, state, agent_info): return # Get product unit id for current agent. - product_unit_id = agent_info.id if agent_info.agent_type == "product" else agent_info.parent_id + product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id product_metrics = self._cur_metrics["products"][product_unit_id] @@ -729,6 +730,7 @@ def __init__(self, env: Env): self.products = [] self.product_id2index_dict = {} self.facility_levels = [] + self.consumer_id2product = {} self.facilities = env.summary["node_mapping"]["facilities"] @@ -739,6 +741,8 @@ def __init__(self, env: Env): for product_id, product in facility["units"]["products"].items(): pid_list.append(product["id"]) consumer = product["consumer"] + if consumer is not None: + self.consumer_id2product[consumer["id"]] = product["id"] seller = product["seller"] manufacture = product["manufacture"] @@ -787,8 +791,9 @@ def calc(self): consumer_step_balance_sheet_loss = -1 * (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) # consumer step reward: balance sheet los + profile * discount - consumer_step_reward = consumer_step_balance_sheet_loss + \ - consumer_profit * consumer_bs_states[:, 5] + # consumer_step_reward = consumer_step_balance_sheet_loss + \ + # consumer_profit * consumer_bs_states[:, 5] + consumer_step_reward = consumer_step_balance_sheet_loss # seller seller_bs_states = self.seller_ss[tick::self.seller_features]\ @@ -823,7 +828,7 @@ def calc(self): .flatten()\ .reshape(-1, len(self.product_features)) - # product distribution loss = check order + delay order penalty + # product distribution loss = transportation cost + delay order penalty product_distribution_balance_sheet_loss = -1 * \ (product_bs_states[:, 3] + product_bs_states[:, 4]) @@ -885,16 +890,16 @@ def calc(self): product_step_reward[i] += product_distribution_balance_sheet_loss[distribution_index] + \ product_distribution_balance_sheet_profit[distribution_index] - if downstreams and len(downstreams) > 0: - if product_id in downstreams: - for dfacility in downstreams[product_id]: - dproducts = self.facilities[dfacility]["units"]["products"] + # if downstreams and len(downstreams) > 0: + # if product_id in downstreams: + # for dfacility in downstreams[product_id]: + # dproducts = self.facilities[dfacility]["units"]["products"] - did = dproducts[product_id]["id"] + # did = dproducts[product_id]["id"] - product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] - product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] - product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] + # product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] + # product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] + # product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss @@ -958,9 +963,10 @@ def calc(self): consumer_step_balance_sheet = consumer_profit + consumer_step_balance_sheet_loss for id, bs, rw in zip(consumer_bs_states[:, 0], consumer_step_balance_sheet, consumer_step_reward): - result[int(id)] = (bs, rw) - - self.total_balance_sheet[id] += bs + # result[int(id)] = (bs, rw) + # let reward of a consumer equate its parent product + result[int(id)] = result[self.consumer_id2product[int(id)]] + self.total_balance_sheet[id] += result[int(id)][0] # For producers. man_step_balance_sheet = man_balance_sheet_profit_loss diff --git a/examples/supply_chain/learner.py b/examples/supply_chain/learner.py index 236c99d4b..09d6ebbb3 100644 --- a/examples/supply_chain/learner.py +++ b/examples/supply_chain/learner.py @@ -1,11 +1,21 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import os from os.path import dirname, join, realpath from maro.rl import DistributedLearner +from render_tools import SimulationTracker sc_code_dir = dirname(realpath(__file__)) class SCLearner(DistributedLearner): + + def _evaluate(self, ep: int): + tracker = SimulationTracker(60, 1, self.eval_env, self) + loc_path = '/maro/supply_chain/output/' + facility_types = ["product"] + os.system(f"rm {loc_path}/*") + tracker.run_and_render(loc_path, facility_types) + def end_of_training(self, ep, **kwargs): pass diff --git a/examples/supply_chain/or_policies.py b/examples/supply_chain/or_policies.py index 1b8008067..3484e293c 100644 --- a/examples/supply_chain/or_policies.py +++ b/examples/supply_chain/or_policies.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from examples.supply_chain.policies import get_dqn_policy import sys from os.path import dirname, realpath @@ -14,6 +15,7 @@ ) from or_policy.minmax_policy import ConsumerMinMaxPolicy +from or_policy.eoq_policy import ConsumerEOQPolicy from or_policy.base_policy import ProducerBaselinePolicy sc_code_dir = dirname(realpath(__file__)) @@ -21,13 +23,16 @@ from config import config agent_ids = config["agent_ids"] -policy_ids = ["consumer", "producer", "facility", "product"] +policy_ids = ["consumer", "producer", "facility", "product", "storeproduct"] config = config["policy"] def get_base_consumer_policy(config): return ConsumerMinMaxPolicy(config) +def get_eoq_consumer_policy(config): + return ConsumerEOQPolicy(config) + def get_base_producer_policy(config): return ProducerBaselinePolicy(config) @@ -37,8 +42,10 @@ def get_base_producer_policy(config): policy_dict = { 'consumer': get_base_consumer_policy(config['consumer']), 'producer': get_base_producer_policy(config['producer']), + 'storeconsumer': get_dqn_policy(config['storeconsumer']), 'facility': null_policy, - 'product': null_policy + 'product': null_policy, + 'storeproduct': null_policy } agent2policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} diff --git a/examples/supply_chain/or_policy/minmax_policy.py b/examples/supply_chain/or_policy/minmax_policy.py index 873a41297..53f00ca8b 100644 --- a/examples/supply_chain/or_policy/minmax_policy.py +++ b/examples/supply_chain/or_policy/minmax_policy.py @@ -26,7 +26,7 @@ def compute_action(self, state): most_needed_product_id = state['product_idx'] # stop placing orders if no risk of out of stock - vlt_buffer_days = 1 + vlt_buffer_days = 10 vlt = state['vlt'] + vlt_buffer_days sale_mean, sale_std = state['sale_mean'], state['sale_std'] service_level = state['service_level'] diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py index e07b76ea8..6d9fe80df 100644 --- a/examples/supply_chain/policies.py +++ b/examples/supply_chain/policies.py @@ -13,13 +13,19 @@ StepBasedSchedule, UniformSampler ) +from or_policy.minmax_policy import ConsumerMinMaxPolicy +from or_policy.eoq_policy import ConsumerEOQPolicy +from or_policy.base_policy import ProducerBaselinePolicy + sc_code_dir = dirname(realpath(__file__)) sys.path.insert(0, sc_code_dir) from config import config + agent_ids = config["agent_ids"] -policy_ids = ["consumer", "producer", "facility", "product"] +policy_ids = ["consumerstore", "consumer", "producer", "facility", "product", "productstore"] +config = config["policy"] class SimpleQNet(QNetForDiscreteActionSpace): def forward(self, states): @@ -38,13 +44,29 @@ def get_dqn_policy(cfg): experience_manager = UniformSampler(**cfg["experience_manager"]) return DQN(q_net, experience_manager, DQNConfig(**cfg["algorithm_config"])) +def get_base_consumer_policy(config): + return ConsumerMinMaxPolicy(config) + +def get_eoq_consumer_policy(config): + return ConsumerEOQPolicy(config) + +def get_base_producer_policy(config): + return ProducerBaselinePolicy(config) + +agent_to_policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} null_policy = NullPolicy() + policy_dict = { - policy_id: get_dqn_policy(config["policy"][policy_id]) if policy_id in config["policy"] else null_policy - for policy_id in policy_ids + 'consumer': get_base_consumer_policy(config['consumer']), + 'producer': get_base_producer_policy(config['producer']), + 'consumerstore': get_dqn_policy(config['consumerstore']), + 'facility': null_policy, + 'product': null_policy, + 'productstore': null_policy } + agent2policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} # update schedules @@ -56,6 +78,6 @@ def get_policy_update_schedule(cfg): # policy update schedule can be a dict or single EpisodeBasedSchedule or StepBasedSchedule. # The latter indicates that all policies shared the same update schedule policy_update_schedule = { - policy_id: get_policy_update_schedule(config["policy"][policy_id]["update_schedule"]) - for policy_id in policy_ids if policy_id in config["policy"] + policy_id: get_policy_update_schedule(config[policy_id]["update_schedule"]) + for policy_id in policy_ids if policy_id in ['consumerstore'] } \ No newline at end of file diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py index 38290bc17..da06c8586 100644 --- a/examples/supply_chain/render_tools.py +++ b/examples/supply_chain/render_tools.py @@ -105,7 +105,7 @@ def render(self, file_name, metrics, facility_types): _agent_list = [] _step_idx = [] for i, agent_info in enumerate(self.facility_names): - if agent_info.agent_type in facility_types: + if agent_info.agent_type in ['productstore']: _agent_list.append(agent_info.id) _step_idx.append(i) _step_metrics = [metrics[0, :, i] for i in _step_idx] diff --git a/examples/supply_chain/topologies/test/config.yml b/examples/supply_chain/topologies/test/config.yml index 652b94f18..b19afb777 100644 --- a/examples/supply_chain/topologies/test/config.yml +++ b/examples/supply_chain/topologies/test/config.yml @@ -59,11 +59,11 @@ facility_definitions: class: "StoreProductUnit" is_template: true config: - agent_type: "product" + agent_type: "productstore" consumer: class: "ConsumerUnit" config: - agent_type: "consumer" + agent_type: "consumerstore" seller: class: "SellerUnit" config: @@ -217,7 +217,7 @@ world: sku1: # sku name and attributes needed for this facility init_stock: 1000 product_unit_cost: 90 - production_rate: 100 + production_rate: 1000 type: "production" # production means this is the output production of this facility cost: 90 price: 100 @@ -225,7 +225,7 @@ world: sku2: init_stock: 1000 product_unit_cost: 150 - production_rate: 100 + production_rate: 1000 type: "production" cost: 150 price: 200 @@ -273,29 +273,29 @@ world: storage: *medium_storage distribution: *normal_distribution config: - delay_order_penalty: 10 - order_cost: 0 + delay_order_penalty: 1 + order_cost: 10 - name: "Retailer_001" definition_ref: "RetailerFacility" skus: sku1: - price: 150 + price: 200 cost: 100 init_stock: 80 sale_gamma: 20 backlog_ratio: 0.1 # optional vlt: 1 sku2: - price: 300 + price: 350 cost: 200 init_stock: 400 sale_gamma: 80 backlog_ratio: 0.1 vlt: 1 sku3: - price: 450 + price: 650 cost: 300 init_stock: 200 sale_gamma: 50 diff --git a/maro/rl/training/distributed_learner.py b/maro/rl/training/distributed_learner.py index e152734e6..33f446c8d 100644 --- a/maro/rl/training/distributed_learner.py +++ b/maro/rl/training/distributed_learner.py @@ -86,8 +86,9 @@ def __init__( def run(self): for ep in range(1, self.num_episodes + 1): + self._logger.info(f"Learner episode: {ep}") self._train(ep) - + policy_ids = self.policy_update_schedule.pop_episode(ep) if policy_ids == ["*"]: policy_ids = self._updatable_policy_ids From 32120d08a02c4aa7c1953a76f61d2d0d217a5272 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 14 May 2021 12:05:32 +0000 Subject: [PATCH 253/482] refined replay buffer design --- maro/rl/__init__.py | 4 +- maro/rl/env_wrapper/__init__.py | 7 ++ maro/rl/{ => env_wrapper}/env_wrapper.py | 59 ++++++------- maro/rl/env_wrapper/replay_buffer.py | 108 +++++++++++++++++++++++ maro/rl/experience/experience.py | 26 ------ maro/rl/training/rollout_manager.py | 1 - 6 files changed, 144 insertions(+), 61 deletions(-) create mode 100644 maro/rl/env_wrapper/__init__.py rename maro/rl/{ => env_wrapper}/env_wrapper.py (67%) create mode 100644 maro/rl/env_wrapper/replay_buffer.py diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 66c34e850..9d9cea10a 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -5,7 +5,7 @@ DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) -from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.env_wrapper import AbsEnvWrapper, AbsReplayBuffer, FIFOReplayBuffer, FixedSizeReplayBuffer from maro.rl.experience import AbsExperienceManager, ExperienceSet, ReplayBuffer, UniformSampler, UseAndDispose from maro.rl.exploration import ( AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, @@ -28,7 +28,7 @@ __all__ = [ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", - "AbsEnvWrapper", + "AbsEnvWrapper", "AbsReplayBuffer", "FIFOReplayBuffer", "FixedSizeReplayBuffer", "AbsExperienceManager", "ExperienceSet", "ReplayBuffer", "UniformSampler", "UseAndDispose", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", diff --git a/maro/rl/env_wrapper/__init__.py b/maro/rl/env_wrapper/__init__.py new file mode 100644 index 000000000..8599ce113 --- /dev/null +++ b/maro/rl/env_wrapper/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .env_wrapper import AbsEnvWrapper +from .replay_buffer import AbsReplayBuffer, FIFOReplayBuffer, FixedSizeReplayBuffer, + +__all__ = ["AbsEnvWrapper", "AbsReplayBuffer", "FIFOReplayBuffer", "FixedSizeReplayBuffer"] diff --git a/maro/rl/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py similarity index 67% rename from maro/rl/env_wrapper.py rename to maro/rl/env_wrapper/env_wrapper.py index f25c5539a..8efb27052 100644 --- a/maro/rl/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -3,32 +3,37 @@ import time from abc import ABC, abstractmethod -from collections import defaultdict, deque +from collections import deque +from typing import Dict -from maro.rl.experience import ReplayBuffer from maro.simulator import Env +from .replay_buffer import AbsReplayBuffer + class AbsEnvWrapper(ABC): """Environment wrapper that performs various shaping and other roll-out related logic. Args: env (Env): Environment instance. - save_replay (bool): If True, the steps during roll-out will be recorded sequentially. This - includes states, actions and rewards. The decision events themselves will also be recorded - for delayed reward evaluation purposes. Defaults to True. + replay_buffer (dict): Replay buffers for recording transitions experienced by agents in sequential fashion. + Transitions will only be recorded for those agents whose IDs appear in the keys of the dictionary. + Defaults to None, in which case no transition will be recorded. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which rewards are evaluated immediately after executing an action. """ - def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): + def __init__( + self, + env: Env, + replay_buffer: Dict[str, AbsReplayBuffer] = None, + reward_eval_delay: int = 0 + ): self.env = env - self.replay = defaultdict(ReplayBuffer) + self.replay = replay_buffer self.state_info = None # context for converting model output to actions that can be executed by the env - self.save_replay = save_replay self.reward_eval_delay = reward_eval_delay - self._pending_reward_ticks = deque() # list of ticks whose actions have not been given a reward - self.action_history = {} # store the tick-to-action mapping + self._pending_reward_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None self._total_reward = 0 self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. @@ -58,10 +63,6 @@ def event(self): def total_reward(self): return self._total_reward - @property - def num_experiences(self): - return {agent_id: replay.size() for agent_id, replay in self.replay.items()} - def start(self): self._step_index = 0 _, self._event, _ = self.env.step(None) @@ -95,8 +96,7 @@ def step(self, action_by_agent: dict): # t0 = time.time() self._step_index += 1 env_action = self.get_action(action_by_agent) - self._pending_reward_ticks.append(self.env.tick) - self.action_history[self.env.tick] = action_by_agent + self._pending_reward_cache.append((self._state, action_by_agent, self.env.tick)) if len(env_action) == 1: env_action = list(env_action.values())[0] # t1 = time.time() @@ -109,25 +109,20 @@ def step(self, action_by_agent: dict): Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ while ( - self._pending_reward_ticks and - (done or self.env.tick - self._pending_reward_ticks[0] >= self.reward_eval_delay) + self._pending_reward_cache and + (done or self.env.tick - self._pending_reward_cache[0][2] >= self.reward_eval_delay) ): - tick = self._pending_reward_ticks.popleft() + state, action, tick = self._pending_reward_cache.popleft() reward = self.get_reward(tick=tick) # assign rewards to the agents that took action at that tick - for agent_id in self.action_history[tick]: + for agent_id, act in action.items(): rw = reward.get(agent_id, 0) - if not done and self.save_replay: - self.replay[agent_id].rewards.append(rw) + if not done and self.replay: + self.replay[agent_id].push(state[agent_id], act, rw) self._total_reward += rw if not done: - prev_state = self._state self._state = self.get_state(self.env.tick) - if self.save_replay: - for agent_id, state in prev_state.items(): - self.replay[agent_id].states.append(state) - self.replay[agent_id].actions.append(action_by_agent[agent_id]) # t3 = time.time() # self._tot_step_time += t3 - t0 else: @@ -144,15 +139,15 @@ def end_of_episode(self): def get_experiences(self, agent_ids: list = None): if agent_ids is None: - return {agent_id: replay.to_experience_set() for agent_id, replay in self.replay.items()} + return {agent_id: replay.batch() for agent_id, replay in self.replay.items()} else: - return {agent_id: self.replay[agent_id].to_experience_set() for agent_id in agent_ids} + return {agent_id: self.replay[agent_id].batch() for agent_id in agent_ids} def reset(self): self.env.reset() self.state_info = None self._total_reward = 0 self._state = None - self._pending_reward_ticks.clear() - self.action_history.clear() - self.replay = defaultdict(ReplayBuffer) + self._pending_reward_cache.clear() + for replay in self.replay.values(): + replay.clear() diff --git a/maro/rl/env_wrapper/replay_buffer.py b/maro/rl/env_wrapper/replay_buffer.py new file mode 100644 index 000000000..f6409027e --- /dev/null +++ b/maro/rl/env_wrapper/replay_buffer.py @@ -0,0 +1,108 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod + +import numpy as np + +from maro.rl.experience import ExperienceSet + + +class AbsReplayBuffer(ABC): + def __init__(self): + super().__init__() + + @abstractmethod + def push(self, state, action, reward): + raise NotImplementedError + + @abstractmethod + def batch(self) -> ExperienceSet: + raise NotImplementedError + + @abstractmethod + def clear(self): + raise NotImplementedError + + +class FIFOReplayBuffer(AbsReplayBuffer): + def __init__(self): + super().__init__() + self.states = [] + self.actions = [] + self.rewards = [] + self.next_states = [] + + def push(self, state, action, reward): + if self.states: + self.next_states.append(state) + + self.states.append(state) + self.actions.append(action) + self.rewards.append(reward) + + def batch(self) -> ExperienceSet: + exp_set = ExperienceSet( + states=self.states[:-1], + actions=self.actions[:-1], + rewards=self.rewards[:-1], + next_states=self.next_states + ) + + del self.states[:-1] + del self.actions[:-1] + del self.rewards[:-1] + self.next_states.clear() + + return exp_set + + def clear(self): + self.states.clear() + self.actions.clear() + self.rewards.clear() + self.next_states.clear() + + +class FixedSizeReplayBuffer(AbsReplayBuffer): + def __init__(self, capacity: int, batch_size: int): + super().__init__() + if batch_size <= 0: + raise ValueError("batch_size must be a positive integer since batch_mode is set to 'random'") + if batch_size > capacity: + raise ValueError(f"batch_size cannot exceed the buffer capacity ({capacity})") + + self.capacity = capacity + self.batch_size = batch_size + self.states = np.empty(capacity, dtype=object) + self.actions = np.empty(capacity, dtype=object) + self.rewards = np.empty(capacity, dtype=object) + self.next_states = np.empty(capacity, dtype=object) + self._size = 0 + self._index = 0 + + def push(self, state, action, reward): + if self.states[self._index - 1]: + self.next_states[self._index - 1] = state + self._size = min(self._size + 1, self.capacity) + + self.states[self._index] = state + self.actions[self._index] = action + self.rewards[self._index] = reward + self._index = (self._index + 1) % self.capacity + + def batch(self) -> ExperienceSet: + indexes = np.random.choice(self._size, size=self.batch_size) + return ExperienceSet( + states=list(self.states[indexes]), + actions=list(self.actions[indexes]), + rewards=list(self.rewards[indexes]), + next_states=list(self.next_states[indexes]) + ) + + def clear(self): + self.states = np.empty(self.capacity, dtype=object) + self.actions = np.empty(self.capacity, dtype=object) + self.rewards = np.empty(self.capacity, dtype=object) + self.next_states = np.empty(self.capacity, dtype=object) + self._size = 0 + self._index = 0 diff --git a/maro/rl/experience/experience.py b/maro/rl/experience/experience.py index ad57613b2..d115c97c3 100644 --- a/maro/rl/experience/experience.py +++ b/maro/rl/experience/experience.py @@ -28,29 +28,3 @@ def extend(self, other): self.actions += other.actions self.rewards += other.rewards self.next_states += other.next_states - - -class ReplayBuffer(object): - def __init__(self): - self.states = [] - self.actions = [] - self.rewards = [] - - def to_experience_set(self): - # print(len(self.rewards), len(self.states)) - num_complete = self.size() - exp_set = ExperienceSet( - states=self.states[:num_complete], - actions=self.actions[:num_complete], - rewards=self.rewards[:num_complete], - next_states=self.states[1:num_complete + 1] - ) - - del self.states[:num_complete] - del self.actions[:num_complete] - del self.rewards[:num_complete] - - return exp_set - - def size(self): - return min(len(self.rewards), len(self.states) - 1) diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 3a525b794..674f2b4b8 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -170,7 +170,6 @@ def evaluate(self, policy_state_dict: dict): self._logger.info("Evaluating...") self._eval_ep += 1 self._load_policy_states(policy_state_dict) - self.eval_env.save_replay = False self.eval_env.reset() self.eval_env.start() # get initial state while self.eval_env.state: From b632e520a0d360c2a354f377c712dd9aab5c7fd6 Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 17 May 2021 13:51:01 +0800 Subject: [PATCH 254/482] remove cached action after reset --- maro/simulator/scenarios/supply_chain/business_engine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index d22d2cfe1..033e0aa25 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -75,6 +75,8 @@ def reset(self): self._reset_by_facility() + self._action_cache = None + def get_node_mapping(self) -> dict: return self._node_mapping From 3c066061f963d6e41472d83e9c671976d22e2a1f Mon Sep 17 00:00:00 2001 From: chaosyu Date: Mon, 17 May 2021 14:05:09 +0800 Subject: [PATCH 255/482] reset action after reset --- maro/simulator/scenarios/supply_chain/units/unitbase.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py index 6563a5aaa..6d239e52d 100644 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -87,6 +87,8 @@ def reset(self): if self.data_model is not None: self.data_model.reset() + self.action = None + def initialize(self): """Initialize this unit after data model is ready to use. From ddf9805004e1bc527d390e6d331182c73b074f4d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 18 May 2021 03:44:05 +0000 Subject: [PATCH 256/482] updated doc for replay buffer and policy --- maro/rl/algorithm/ac.py | 3 +- maro/rl/algorithm/ddpg.py | 3 +- maro/rl/algorithm/dqn.py | 3 +- maro/rl/env_wrapper/env_wrapper.py | 36 +++++---- maro/rl/env_wrapper/replay_buffer.py | 31 ++++++++ maro/rl/policy/policy.py | 105 +++++++++------------------ 6 files changed, 94 insertions(+), 87 deletions(-) diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index 5d40cb04f..ef17ba3db 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -68,7 +68,8 @@ def __init__( if not isinstance(ac_net, PolicyValueNetForDiscreteActionSpace): raise TypeError("model must be an instance of 'PolicyValueNetForDiscreteActionSpace'") - super().__init__(experience_manager, config) + super().__init__(experience_manager) + self.config = config self.ac_net = ac_net def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index a000b6ce6..d88705ecb 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -73,7 +73,8 @@ def __init__( if not isinstance(ac_net, PolicyValueNetForContinuousActionSpace): raise TypeError("model must be an instance of 'PolicyValueNetForContinuousActionSpace'") - super().__init__(experience_manager, config) + super().__init__(experience_manager) + self.config = config self.ac_net = ac_net self.target_ac_net = ac_net.copy() if self.ac_net.trainable else None self._train_cnt = 0 diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index 5d6f55268..7658b5849 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -71,7 +71,8 @@ def __init__( if not isinstance(q_net, QNetForDiscreteActionSpace): raise TypeError("model must be an instance of 'QNetForDiscreteActionSpace'") - super().__init__(experience_manager, config) + super().__init__(experience_manager) + self.config = config self.q_net = q_net self.target_q_net = q_net.copy() if q_net.trainable else None self.target_q_net.eval() diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 8efb27052..76fe93758 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -12,7 +12,7 @@ class AbsEnvWrapper(ABC): - """Environment wrapper that performs various shaping and other roll-out related logic. + """Environment wrapper that performs shaping, transition caching and experience generation. Args: env (Env): Environment instance. @@ -20,7 +20,7 @@ class AbsEnvWrapper(ABC): Transitions will only be recorded for those agents whose IDs appear in the keys of the dictionary. Defaults to None, in which case no transition will be recorded. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward - for the action taken for that event. Defaults to 0, which rewards are evaluated immediately + for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. """ def __init__( @@ -41,6 +41,7 @@ def __init__( @property def step_index(self): + """Number of environmental steps taken so far.""" return self._step_index @property @@ -53,6 +54,7 @@ def metrics(self): @property def state(self): + """The current environmental state.""" return self._state @property @@ -61,9 +63,11 @@ def event(self): @property def total_reward(self): + """The total reward achieved so far.""" return self._total_reward def start(self): + """Generate the initial environmental state at the beginning of a simulation episode.""" self._step_index = 0 _, self._event, _ = self.env.step(None) self._state = self.get_state(self.env.tick) @@ -79,20 +83,27 @@ def get_state(self, tick: int = None) -> dict: pass @abstractmethod - def get_action(self, action) -> dict: + def get_action(self, action): + """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" pass @abstractmethod - def get_reward(self, tick: int = None) -> dict: - """User-defined reward evaluation. + def get_reward(self, tick: int = None): + """Evaluate the reward for an action. Args: - tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed - reward evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. + tick (int): If given, the reward for the action that occured at this tick will be evaluated (in the case + of delayed reward evaluation). Otherwise, the reward is evaluated for the latest action. + Defaults to None. """ pass def step(self, action_by_agent: dict): + """Wrapper for env.step(). + + The new transition is stored in the replay buffer or cached in a separate data structure if the + reward cannot be determined yet due to a non-zero ``reward_eval_delay``. + """ # t0 = time.time() self._step_index += 1 env_action = self.get_action(action_by_agent) @@ -105,7 +116,7 @@ def step(self, action_by_agent: dict): # self._tot_raw_step_time += t2 - t1 """ - If roll-out is complete, evaluate rewards for all remaining events except the last. + If this is the final step, evaluate rewards for all remaining events except the last. Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ while ( @@ -135,13 +146,12 @@ def step(self, action_by_agent: dict): # self._tot_step_time = 0 def end_of_episode(self): + """Custom processing logic at the end of an episode.""" pass - def get_experiences(self, agent_ids: list = None): - if agent_ids is None: - return {agent_id: replay.batch() for agent_id, replay in self.replay.items()} - else: - return {agent_id: self.replay[agent_id].batch() for agent_id in agent_ids} + def get_experiences(self): + """Get per-agent experiences from the replay buffer.""" + return {agent_id: replay.batch() for agent_id, replay in self.replay.items()} def reset(self): self.env.reset() diff --git a/maro/rl/env_wrapper/replay_buffer.py b/maro/rl/env_wrapper/replay_buffer.py index f6409027e..44741bd04 100644 --- a/maro/rl/env_wrapper/replay_buffer.py +++ b/maro/rl/env_wrapper/replay_buffer.py @@ -9,23 +9,29 @@ class AbsReplayBuffer(ABC): + """Replay buffer to be used in an EnvWrapper for caching transitions and generating experience sets. + """ def __init__(self): super().__init__() @abstractmethod def push(self, state, action, reward): + """Add a new transition to the buffer.""" raise NotImplementedError @abstractmethod def batch(self) -> ExperienceSet: + """Generate an ExperienceSet from the buffer.""" raise NotImplementedError @abstractmethod def clear(self): + """Empty the buffer.""" raise NotImplementedError class FIFOReplayBuffer(AbsReplayBuffer): + """A FIFO-based replay buffer that empties itself after an experience set is generated.""" def __init__(self): super().__init__() self.states = [] @@ -34,6 +40,11 @@ def __init__(self): self.next_states = [] def push(self, state, action, reward): + """Add a new transition to buffer. + + If this is not the first transition, the state will correspond to the "next_state" for the transition + that came before it. + """ if self.states: self.next_states.append(state) @@ -42,6 +53,11 @@ def push(self, state, action, reward): self.rewards.append(reward) def batch(self) -> ExperienceSet: + """Convert all "SARS" transitions to experience sets and subsequently empty the buffer. + + After this operation, "states", "actions" and "rewards" will have one element remaining that corresponds + to the last transition for which the next state has yet to be determined. The "next_states" will be empty. + """ exp_set = ExperienceSet( states=self.states[:-1], actions=self.actions[:-1], @@ -57,6 +73,7 @@ def batch(self) -> ExperienceSet: return exp_set def clear(self): + """Empty the buffer.""" self.states.clear() self.actions.clear() self.rewards.clear() @@ -64,6 +81,13 @@ def clear(self): class FixedSizeReplayBuffer(AbsReplayBuffer): + """An implementation of the replay buffer that maintains a fixed-size buffer. + + Args: + capacity (int): Capacity of the buffer. Once the the buffer size has reached capacity, newly added + transitions will replace existing entries starting from the oldest. + batch_size (int): Size of experience sets generated from the buffer. + """ def __init__(self, capacity: int, batch_size: int): super().__init__() if batch_size <= 0: @@ -81,6 +105,11 @@ def __init__(self, capacity: int, batch_size: int): self._index = 0 def push(self, state, action, reward): + """Add a new transition to buffer. + + If this is not the first transition, the state will correspond to the "next_state" for the transition + that came before it. + """ if self.states[self._index - 1]: self.next_states[self._index - 1] = state self._size = min(self._size + 1, self.capacity) @@ -91,6 +120,7 @@ def push(self, state, action, reward): self._index = (self._index + 1) % self.capacity def batch(self) -> ExperienceSet: + """Generate an ExperienceSet from a random sample of transitions in the buffer.""" indexes = np.random.choice(self._size, size=self.batch_size) return ExperienceSet( states=list(self.states[indexes]), @@ -100,6 +130,7 @@ def batch(self) -> ExperienceSet: ) def clear(self): + """Empty the buffer.""" self.states = np.empty(self.capacity, dtype=object) self.actions = np.empty(self.capacity, dtype=object) self.rewards = np.empty(self.capacity, dtype=object) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index f435e1f11..5fb4be54a 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -3,115 +3,78 @@ from abc import ABC, abstractmethod -from maro.rl.exploration import AbsExploration -from maro.rl.experience import AbsExperienceManager, ExperienceSet +from maro.rl.experience import AbsExperienceManager class AbsPolicy(ABC): - """Abstract fixed policy class. - - Args: - config: Settings for the algorithm. - """ + """Abstract policy class.""" def __init__(self): super().__init__() @abstractmethod def choose_action(self, state): - """Compute an action based on a state object. - - Args: - state: State object. - - Returns: - The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert - this to an environment executable action. - """ raise NotImplementedError class NullPolicy(AbsPolicy): + """Dummy policy that does nothing. + + Note that the meaning of a "None" action may depend on the scenario. + """ def choose_action(self, state): return None class AbsCorePolicy(AbsPolicy): - def __init__(self, experience_manager: AbsExperienceManager, config): + """Policy that can update itself using simulation experiences. + + Reinforcement learning (RL) policies should inherit from this. + + Args: + experience_manager (AbsExperienceManager): An experience manager that exposes put() and get() interfaces + for storing and retrieving experiences for training. + """ + def __init__(self, experience_manager: AbsExperienceManager): super().__init__() self.experience_manager = experience_manager - self.config = config @abstractmethod def choose_action(self, state): - """Compute an action based on a state object. - - Args: - state: State object. - - Returns: - The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert - this to an environment executable action. - """ raise NotImplementedError @abstractmethod def update(self): + """Policy update logic is implemented here. + + This usually includes retrieving experiences as training samples from the experience manager and + updating the underlying models using these samples. + """ raise NotImplementedError @abstractmethod def get_state(self): + """Return the current state of the policy. + + The implementation must be in correspondence with that of ``set_state``. For example, if a torch model + is contained in the policy, ``get_state`` may include a call to ``state_dict()`` on the model, while + ``set_state`` should accordingly include ``load_state_dict()``. + """ pass @abstractmethod def set_state(self, policy_state): + """Set the policy state to ``policy_state``. + + The implementation must be in correspondence with that of ``get_state``. For example, if a torch model + is contained in the policy, ``set_state`` may include a call to ``load_state_dict()`` on the model, while + ``get_state`` should accordingly include ``state_dict()``. + """ pass def load(self, path: str): + """Load the policy state from disk.""" pass def save(self, path: str): + """Save the policy state to disk.""" pass - - -class RLPolicy(object): - """Abstract fixed policy class. - - Args: - config: Settings for the algorithm. - """ - def __init__(self, core_policy: AbsCorePolicy, exploration: AbsExploration = None): - self.core_policy = core_policy - self.exploration = exploration - - def choose_action(self, state): - """Compute an action based on a state object. - - Args: - state: State object. - - Returns: - The action to be taken given the state object. It is usually necessary to use an ``ActionShaper`` to convert - this to an environment executable action. - """ - action = self.core_policy.choose_action(state) - return self.exploration(action) - - def update(self): - """Algorithm-specific training logic. - - The parameters are data to train the underlying model on. Algorithm-specific loss and optimization - should be reflected here. - """ - self.core_policy.update() - - def learn(self, experience_set: ExperienceSet): - return self.core_policy.learn(experience_set) - - def set_state(self, policy_state): - self.core_policy.set_state(policy_state) - - def load(self, path: str): - self.core_policy.load(path) - - def save(self, path: str): - self.core_policy.save(path) From 261843de8c33e043c7e8199cd4d6af797e78c6db Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 18 May 2021 11:50:06 +0000 Subject: [PATCH 257/482] added doc for policy, learner, rollout manager and policy manager --- maro/rl/__init__.py | 4 +- maro/rl/env_wrapper/env_wrapper.py | 2 +- maro/rl/training/__init__.py | 4 +- maro/rl/training/actor.py | 30 ++++++++-- maro/rl/training/learner.py | 60 ++++++-------------- maro/rl/training/policy_manager.py | 85 +++++++++++++++++++++-------- maro/rl/training/rollout_manager.py | 83 ++++++++++++++++++---------- 7 files changed, 164 insertions(+), 104 deletions(-) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 9d9cea10a..4e699f2bb 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -17,7 +17,7 @@ ) from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy from maro.rl.training import ( - AbsPolicyManager, AbsRolloutManager, Actor, ExperienceTrigger, Learner, LocalPolicyManager, LocalRolloutManager, + AbsPolicyManager, AbsRolloutManager, Actor, PolicyUpdateTrigger, Learner, LocalPolicyManager, LocalRolloutManager, ParallelRolloutManager ) from maro.rl.utils import ( @@ -36,7 +36,7 @@ "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "PolicyNetForDiscreteActionSpace", "PolicyValueNetForContinuousActionSpace", "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace", "AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy", - "AbsPolicyManager", "AbsRolloutManager", "Actor", "ExperienceTrigger", "Learner", "LocalPolicyManager", "LocalRolloutManager", + "AbsPolicyManager", "AbsRolloutManager", "Actor", "PolicyUpdateTrigger", "Learner", "LocalPolicyManager", "LocalRolloutManager", "ParallelRolloutManager", "StepBasedSchedule", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 76fe93758..9ae0d3c85 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -12,7 +12,7 @@ class AbsEnvWrapper(ABC): - """Environment wrapper that performs shaping, transition caching and experience generation. + """Environment wrapper that performs scenario-specific processing, transition caching and experience generation. Args: env (Env): Environment instance. diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 34f892f42..6ff39fb49 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -3,13 +3,13 @@ from .actor import Actor from .learner import Learner -from .policy_manager import AbsPolicyManager, ExperienceTrigger, LocalPolicyManager +from .policy_manager import AbsPolicyManager, PolicyUpdateTrigger, LocalPolicyManager from .rollout_manager import AbsRolloutManager, LocalRolloutManager, ParallelRolloutManager __all__ = [ "Actor", "Learner", - "AbsPolicyManager", "ExperienceTrigger", "LocalPolicyManager", + "AbsPolicyManager", "PolicyUpdateTrigger", "LocalPolicyManager", "AbsRolloutManager", "LocalRolloutManager", "ParallelRolloutManager" ] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 6e482d327..0c25ca8fb 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -15,16 +15,25 @@ class Actor(object): - """On-demand roll-out executor. + """On-demand roll-out worker. Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance that wraps an ``Env`` instance with scenario-specific - processing logic and stores transitions during roll-outs in a replay memory. - policy (MultiAgentPolicy): Agent that interacts with the environment. + env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences + for policy training / update. + policy_dict (Dict[str, AbsPolicy]): A set of named policies for inference. + agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's + queries to the correct policy. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). + exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. + agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct + an agent's query to the correct exploration scheme. Defaults to None. + eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used + as the evaluation environment. Defaults to None. + log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory + will be used to save the log files generated by it. Defaults to the current working directory. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. + for details. """ def __init__( self, @@ -69,9 +78,20 @@ def __init__( self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): + """Start the event loop. + + The event loop handles 3 types of messages from the roll-out manager: + 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps + and the collected experiences will be sent back to the roll-out manager; + 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire + duration of the evaluation environment. + 3) EXIT, upon which the actor will break out of the event loop and the process will terminate. + + """ for msg in self._proxy.receive(): if msg.tag == MsgTag.EXIT: self._logger.info("Exiting...") + self._proxy.close() break if msg.tag == MsgTag.COLLECT: diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 0bd381130..66a20d089 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import defaultdict, namedtuple from os import getcwd from typing import List, Union @@ -10,16 +9,25 @@ from .policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager -InterEpisodeSchedule = namedtuple("InterEpisodeSchedule", ["start", "interval"]) -IntraEpisodeSchedule = namedtuple("IntraEpisodeSchedule", ["start_ep", "interval", "end_ep_update"]) - class Learner: - """Learner class for distributed training. + """Main controller for learning. Args: - policy (MultiAgentPolicy): Learning agents. - + policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. + rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data + collection. + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on how the implementation of the roll-out manager. + eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will + will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated + at the end of the training episodes given in the list. In any case, the policies will be evaluated + at the end of the last training episode. Defaults to None, in which case the policies will only be + evaluated after the last training episode. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time + and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + end_of_episode_kwargs: Keyword arguments for custom end-of-episode processing. """ def __init__( self, @@ -58,6 +66,7 @@ def __init__( self._last_step_set = {} def run(self): + """Entry point for executing a learning workflow.""" for ep in range(1, self.num_episodes + 1): self._train(ep) if ep == self._eval_schedule[self._eval_point_index]: @@ -82,40 +91,5 @@ def _train(self, ep: int): self.end_of_episode(ep, **self._end_of_episode_kwargs) def end_of_episode(self, ep: int, **kwargs): + """Custom end-of-episode processing is implemented here.""" pass - - def _init_update_schedule(self, schedule): - self._pending_policies_by_segment = defaultdict(set) - self._pending_policies_by_episode = defaultdict(list) - self._step_schedule_opt = {} - if isinstance(schedule, dict): - for policy_id, sch in schedule.items(): - if isinstance(sch, IntraEpisodeSchedule): - self._step_schedule_opt[policy_id] = (sch.start_ep, sch.interval) - if sch.end_ep_update: - for ep in range(sch.start_ep, self.num_episodes + 1): - self._pending_policies_by_episode[ep].append(policy_id) - self._pending_policies_by_segment[sch.interval].add(policy_id) - elif isinstance(sch, InterEpisodeSchedule): - for ep in range(sch.start, self.num_episodes + 1, step=sch.interval): - self._pending_policies_by_episode[ep].append(policy_id) - else: - if isinstance(schedule, IntraEpisodeSchedule): - self._step_schedule_opt["*"] = (schedule.start_ep, schedule.interval) - self._pending_policies_by_segment[schedule.interval].add("*") - if schedule.end_ep_update: - for ep in range(schedule.start_ep, self.num_episodes + 1): - self._pending_policies_by_episode[ep] = self.policy_manager.names - else: - for ep in range(schedule.start, self.num_episodes + 1, step=schedule.interval): - self._pending_policies_by_episode[ep] = self.policy_manager.names - - def _get_pending_policy_ids(self, ep: int, segment: int) -> List[str]: - for policy_id in self._pending_policies_by_segment[segment]: - if segment == self._last_step_set[policy_id]: - next_segment = segment + self._step_schedule_opt[policy_id][1] - self._pending_policies_by_segment[next_segment].append(policy_id) - - return [ - policy_id for policy_id in self._pending_policies_by_segment[segment] if self._step_schedule_opt[policy_id][0] <= ep - ] diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index a97cea1ab..ea7e58f7e 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod from collections import defaultdict, namedtuple +from email.policy import default from os import getcwd from typing import Dict, Union @@ -10,39 +11,72 @@ from maro.rl.policy import AbsPolicy from maro.utils import Logger -ExperienceTrigger = namedtuple("ExperienceTrigger", ["trigger", "warmup"], defaults=[1]) +PolicyUpdateTrigger = namedtuple( + "PolicyUpdateTrigger", ["min_new_experiences", "num_warmup_experiences"], defaults=[1] +) class AbsPolicyManager(ABC): + """Controller for policy updates. + + The actual policy instances may reside here or be distributed on a set of remote nodes. + + Args: + agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct + incoming experiences (which are bucketed by agent ID) to the correct policy's experience manager. + """ def __init__(self, agent2policy: Dict[str, str]): self.agent2policy = agent2policy self._policy_names = list(agent2policy.values()) @property def names(self): + """Return the list of policy names""" return self._policy_names @abstractmethod def on_experiences(self, exp_by_agent: Dict[str, ExperienceSet]): + """Logic for handling incoming experiences is implemented here.""" raise NotImplementedError @abstractmethod def choose_action(self, state_by_agent): + """Choose an action based on the state.""" raise NotImplementedError @abstractmethod def get_state(self): + """Return the latest policy states.""" raise NotImplementedError class LocalPolicyManager(AbsPolicyManager): + """Policy manager that contains the actual policy instances. + + Args: + policy_dict (Dict[str, AbsPolicy]): A set of named policies. + agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct + incoming experiences (which are bucketed by agent ID) to the correct policy's experience manager. + update_trigger (Union[PolicyUpdateTrigger, dict]): Conditions for triggering policy updates. If a + dictionary is provided, the triggers will be applied to the policies by name. If a single + ``PolicyUpdateTrigger`` is provided, the trigger will be applied to all updatable policies, i.e., + those that have the ``experience_manager`` attribute and the ``update`` interface. Defaults to + None, in which case a default updatable trigger will be applied to every updatable policy, meaning that + these policies will be updated as long as new experiences are available. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time + and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ def __init__( self, policy_dict: Dict[str, AbsPolicy], agent2policy: Dict[str, str], - experience_trigger: Union[ExperienceTrigger, dict] = None, + update_trigger: Union[PolicyUpdateTrigger, dict] = None, log_dir: str = getcwd() - ): + ): + if isinstance(update_trigger, dict) and not update_trigger.keys() <= policy_dict.keys(): + raise ValueError(f"The keys for update_trigger must be a subset of {list(policy_dict.keys())}") + super().__init__(agent2policy) self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) self.policy_dict = policy_dict @@ -51,15 +85,17 @@ def __init__( for agent_id, policy_id in self.agent2policy.items(): self._agent_groups_by_policy[policy_id].append(agent_id) - if experience_trigger is None: - self._experience_trigger = {} - elif isinstance(experience_trigger, dict): - self._experience_trigger = experience_trigger + self._updatable_policy_dict = { + policy_id: policy for policy_id, policy in self.policy_dict.items() + if hasattr(policy, "experience_manager") and hasattr(policy, "update") + } + if update_trigger is None: + default_trigger = PolicyUpdateTrigger(min_new_experiences==1, num_warmup_experiences=1) + self._update_trigger = {policy_id: default_trigger for policy_id in self._updatable_policy_dict} + elif isinstance(update_trigger, dict): + self._update_trigger = update_trigger else: - self._experience_trigger = { - policy_id: experience_trigger for policy_id, policy in self.policy_dict.items() - if hasattr(policy, "experience_manager") and hasattr(policy, "update") - } + self._update_trigger = {policy_id: update_trigger for policy_id in self._updatable_policy_dict} self._new_exp_counter = defaultdict(int) self._updated_policy_ids = set() @@ -68,6 +104,12 @@ def choose_action(self, state_by_agent: dict): return {agent_id: self._policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} def on_experiences(self, exp_by_agent: Dict[str, ExperienceSet]): + """Store experiences and update policies if possible. + + The incoming experiences are expected to be grouped by agent ID and will be stored in the corresponding + policy's experience manager by looking up the agent-to-policy mapping. Policies whose update conditions + have been met will then be updated. + """ for agent_id, exp in exp_by_agent.items(): policy_id = self.agent2policy[agent_id] policy = self.policy_dict[policy_id] @@ -75,17 +117,16 @@ def on_experiences(self, exp_by_agent: Dict[str, ExperienceSet]): self._new_exp_counter[policy_id] += exp.size policy.experience_manager.put(exp) - for policy_id, policy in self.policy_dict.items(): - if hasattr(policy, "experience_manager"): - print(f"Policy {policy_id}: exp mem size = {policy.experience_manager.size}, new exp = {self._new_exp_counter[policy_id]}") - if ( - policy_id not in self._experience_trigger or - policy.experience_manager.size >= self._experience_trigger[policy_id].warmup and - self._new_exp_counter[policy_id] >= self._experience_trigger[policy_id].trigger - ): - policy.update() - self._new_exp_counter[policy_id] = 0 - self._updated_policy_ids.add(policy_id) + for policy_id, policy in self._updatable_policy_dict.items(): + print(f"Policy {policy_id}: exp mem size = {policy.experience_manager.size}, new exp = {self._new_exp_counter[policy_id]}") + if ( + policy_id not in self._update_trigger or + policy.experience_manager.size >= self._update_trigger[policy_id].num_warmup_experiences and + self._new_exp_counter[policy_id] >= self._update_trigger[policy_id].min_new_experiences + ): + policy.update() + self._new_exp_counter[policy_id] = 0 + self._updated_policy_ids.add(policy_id) if self._updated_policy_ids: self._logger.info(f"Updated policies {self._updated_policy_ids}") diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 674f2b4b8..38114fb26 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -19,30 +19,19 @@ class AbsRolloutManager(ABC): - """Learner class for distributed training. - - Args: - agent (Union[AbsPolicy, MultiAgentWrapper]): Learning agents. - scheduler (Scheduler): . - num_actors (int): Expected number of actors in the group identified by ``group_name``. - group_name (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the actors (and roll-out clients, if any). - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. - update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger - learner updates, i.e., model training. - """ + """Controller for simulation data collection.""" def __init__(self): super().__init__() self.episode_complete = False @abstractmethod def collect(self, ep: int, segment: int, policy_state_dict: dict): - """Collect experiences from actors.""" + """Collect simulation data, i.e., experiences for training.""" raise NotImplementedError @abstractmethod - def evaluate(self, policy_dict: dict): + def evaluate(self, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``.""" raise NotImplementedError def reset(self): @@ -50,6 +39,29 @@ def reset(self): class LocalRolloutManager(AbsRolloutManager): + """Controller for a single local roll-out actor. + + Args: + env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences + for policy training / update. + policy_dict (Dict[str, AbsPolicy]): A set of named policies for inference. + agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's + queries to the correct policy. + exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. + agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct + an agent's query to the correct exploration scheme. Defaults to None. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used + as the evaluation environment. Defaults to None. + log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an episode. + Defaults to True. + log_total_reward (bool): If True, the total reward will be logged at the end of an episode. Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + def __init__( self, env: AbsEnvWrapper, @@ -97,6 +109,7 @@ def __init__( self._eval_ep = 0 def collect(self, ep: int, segment: int, policy_state_dict: dict): + """Collect simulation data, i.e., experiences for training.""" t0 = time.time() learning_time = 0 num_experiences_collected = 0 @@ -167,6 +180,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): return self.env.get_experiences() def evaluate(self, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``.""" self._logger.info("Evaluating...") self._eval_ep += 1 self._load_policy_states(policy_state_dict) @@ -191,23 +205,33 @@ def _load_policy_states(self, policy_state_dict: dict): class ParallelRolloutManager(AbsRolloutManager): - """Learner class for distributed training. - + """Controller for a set of remote roll-out actors. + Args: - agent (Union[AbsPolicy, MultiAgentWrapper]): Learning agents. - num_actors (int): Expected number of actors in the group identified by ``group_name``. - group_name (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the actors (and roll-out clients, if any). + num_actors (int): Number of remote roll-out actors. + group (str): Identifier of the group to which the actor belongs. It must be the same group name + assigned to the learner (and decision clients, if any). + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + required_finishes (int): Number of actor completions required to return from ``collect``. Defaults to None, + in which case the number is set to ``num_actors``. + max_staleness (int): Maximum allowable staleness measured in the number of calls to ``collect``. Experiences + collected from calls to ``collect`` within ``max_staleness`` calls ago will be returned to the learner. + Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. + num_eval_actors (int): Number of actors required for evaluation. Defaults to 1. + log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an episode. + Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working + directory. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. - update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger - learner updates, i.e., model training. + for details. """ def __init__( self, num_actors: int, - group_name: str, - num_steps: int, + group: str, + num_steps: int = -1, required_finishes: int = None, max_staleness: int = 0, num_eval_actors: int = 1, @@ -224,7 +248,7 @@ def __init__( self._logger = Logger("PARALLEL_ROLLOUT_MANAGER", dump_folder=log_dir) self.num_actors = num_actors peers = {"actor": num_actors} - self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_kwargs) + self._proxy = Proxy(group, "actor_manager", peers, **proxy_kwargs) self._actors = self._proxy.peers["actor"] # remote actor ID's if required_finishes is None: @@ -248,7 +272,7 @@ def collect( segment_index: int, policy_state_dict: dict ): - """Collect experiences from actors.""" + """Collect simulation data, i.e., experiences for training.""" msg_body = { MsgKey.EPISODE_INDEX: episode_index, MsgKey.SEGMENT_INDEX: segment_index, @@ -301,7 +325,7 @@ def collect( return combined_exp_by_agent def evaluate(self, policy_state_dict: dict): - """Evaluate .""" + """Evaluate the performance of ``policy_state_dict``.""" self._eval_ep += 1 msg_body = { MsgKey.EPISODE_INDEX: self._eval_ep, @@ -335,4 +359,5 @@ def evaluate(self, policy_state_dict: dict): def exit(self): """Tell the remote actors to exit.""" self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) + self._proxy.close() self._logger.info("Exiting...") From 91d9ae1fa207f287ca833de18daa64987fea1cba Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 19 May 2021 06:56:49 +0000 Subject: [PATCH 258/482] core model subclass renaming and partial doc update --- docs/source/key_components/rl_toolkit.rst | 62 +++++++++++++---------- examples/cim/ac/config.yml | 21 +++----- maro/rl/__init__.py | 22 ++++---- maro/rl/algorithm/ac.py | 10 ++-- maro/rl/algorithm/ddpg.py | 10 ++-- maro/rl/algorithm/dqn.py | 10 ++-- maro/rl/algorithm/pg.py | 8 +-- maro/rl/model/__init__.py | 6 +-- maro/rl/model/core_model.py | 8 +-- maro/rl/policy/__init__.py | 4 +- maro/rl/training/policy_manager.py | 3 +- 11 files changed, 82 insertions(+), 82 deletions(-) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index e2801e278..51f08f590 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -5,18 +5,18 @@ RL Toolkit MARO provides a full-stack abstraction for reinforcement learning (RL), which enables users to apply predefined and customized components to various scenarios. The main abstractions include fundamental components such as `Agent <#agent>`_\ and `Shaper <#shaper>`_\ , and training routine -controllers such as `Actor <#actor>` and `Learner <#learner>`. +controllers such as `Actor <#actor>`_\ and `Learner <#learner>`_. -Agent ------ +Policy +------ -The Agent is the kernel abstraction of the RL formulation for a real-world problem. -Our abstraction decouples agent and its underlying model so that an agent can exist -as an RL paradigm independent of the inner workings of the models it uses to generate -actions or estimate values. For example, the actor-critic algorithm does not need to -concern itself with the structures and optimizing schemes of the actor and critic models. -This decoupling is achieved by the Core Model abstraction described below. +A policy is used by an agent to decide what action to take given an observation of the environment. +Accordingly, the abstract ``AbsPolicy`` class exposes a ``choose_action`` interface. This abstraction +can encompass both static policies, such as rule-based policies, and updatable policies, such as RL +policies. The latter is abstracted through the ``AbsCorePolicy`` sub-class which also exposes a ``update`` +interface. By default, updatable policies require an experience manager to store and retrieve simulation +data (in the form of "experiences") based on which updates can be made. .. image:: ../images/rl/agent.svg @@ -26,23 +26,32 @@ This decoupling is achieved by the Core Model abstraction described below. .. code-block:: python class AbsPolicy(ABC): - def __init__(self, model: AbsCoreModel, config, experience_pool=None): - self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - self.model = model.to(self.device) - self.config = config - self._experience_pool = experience_pool + @abstractmethod + def choose_action(self, state): + raise NotImplementedError + + + class AbsCorePolicy(AbsPolicy): + def __init__(self, experience_manager: AbsExperienceManager): + super().__init__() + self.experience_manager = experience_manager + + @abstractmethod + def update(self): + raise NotImplementedError Core Model ---------- -MARO provides an abstraction for the underlying models used by agents to form policies and estimate values. -The abstraction consists of ``AbsBlock`` and ``AbsCoreModel``, both of which subclass torch's nn.Module. -The ``AbsBlock`` represents the smallest structural unit of an NN-based model. For instance, the ``FullyConnectedBlock`` -provided in the toolkit is a stack of fully connected layers with features like batch normalization, -drop-out and skip connection. The ``AbsCoreModel`` is a collection of network components with -embedded optimizers and serves as an agent's "brain" by providing a unified interface to it. regardless of how many individual models it requires and how -complex the model architecture might be. +In the deep reinforcement learning (DRL) world, a core policy usually includes one or more neural-network-based models, +which may be used to compute action preferences or estimate state / action values. The core model abstraction is designed +to decouple the the inner workings of these models from the algorithmic aspects of the policy that uses them. For example, +the actor-critic algorithm does not need to concern itself with the structures and optimizing schemes of the actor and +critic models. The abstraction consists of ``AbsBlock`` and ``AbsCoreModel``, both of which subclass torch's nn.Module. +The ``AbsBlock`` represents the smallest structural unit of an NN-based model. For instance, the ``FullyConnectedBlock`` +is a stack of fully connected layers with features like batch normalization, drop-out and skip connection. +The ``AbsCoreModel`` is a collection of network components with embedded optimizers. Several classes are designed As an example, the initialization of the actor-critic algorithm may look like this: @@ -50,7 +59,7 @@ As an example, the initialization of the actor-critic algorithm may look like th actor_stack = FullyConnectedBlock(...) critic_stack = FullyConnectedBlock(...) - model = SimpleMultiHeadModel( + ac_model = SimpleMultiHeadModel( {"actor": actor_stack, "critic": critic_stack}, optim_option={ "actor": OptimizerOption(cls=Adam, params={"lr": 0.001}) @@ -73,11 +82,10 @@ And performing one gradient step is simply: Exploration --------- +----------- -MARO provides an abstraction for exploration in RL. Some RL algorithms such as DQN and DDPG require -explicit exploration governed by a set of parameters. The ``AbsExploration`` class is designed to cater -to these needs. Simple exploration schemes, such as ``EpsilonGreedyExploration`` for discrete action space +Some RL algorithms such as DQN and DDPG require explicit exploration governed by a set of parameters. The +``AbsExploration`` class is designed to cater to these needs. Simple exploration schemes, such as ``EpsilonGreedyExploration`` for discrete action space and ``UniformNoiseExploration`` and ``GaussianNoiseExploration`` for continuous action space, are provided in the toolkit. @@ -91,7 +99,7 @@ As an example, the exploration for DQN may be carried out with the aid of an ``E Tools for Training ------------------------------- +------------------ .. image:: ../images/rl/learner_actor.svg :target: ../images/rl/learner_actor.svg diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index b129f51c5..e1f1f505f 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -2,11 +2,10 @@ # Licensed under the MIT license. env: - scenario: cim - topology: toy.4p_ssdd_l0.0 - durations: 1120 + scenario: cim + topology: toy.4p_ssdd_l0.0 + durations: 1120 max_episode: 50 -policy_update_interval: -1 shaping: port_attributes: - empty @@ -68,13 +67,9 @@ policy: num_steps: 10 actor_loss_coefficient: 0.1 # clip_ratio: 0.8 # for PPO - training_loop_config: - sampler_cls: full - batch_size: -1 - num_steps: 10 - sampler_kwargs: - new_experience_trigger: 1 - num_warmup_experiences: 1 - experience_memory: - capacity: -1 + experience_manager: + capacity: 100000 overwrite_type: "rolling" + update_trigger: + min_new_experiences: 1 + num_warmup_experiences: 1 diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 4e699f2bb..1b8be749d 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -6,16 +6,17 @@ get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) from maro.rl.env_wrapper import AbsEnvWrapper, AbsReplayBuffer, FIFOReplayBuffer, FixedSizeReplayBuffer -from maro.rl.experience import AbsExperienceManager, ExperienceSet, ReplayBuffer, UniformSampler, UseAndDispose +from maro.rl.experience import AbsExperienceManager, ExperienceSet, UniformSampler, UseAndDispose from maro.rl.exploration import ( - AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, - MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, UniformNoiseExploration + AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, + LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, + UniformNoiseExploration ) from maro.rl.model import ( - AbsBlock, AbsCoreModel, FullyConnectedBlock, OptimOption, PolicyNetForDiscreteActionSpace, - PolicyValueNetForContinuousActionSpace, PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace + AbsBlock, AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscreteActorNet, DiscreteQNet, FullyConnectedBlock, + OptimOption ) -from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy +from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( AbsPolicyManager, AbsRolloutManager, Actor, PolicyUpdateTrigger, Learner, LocalPolicyManager, LocalRolloutManager, ParallelRolloutManager @@ -29,16 +30,15 @@ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", "AbsEnvWrapper", "AbsReplayBuffer", "FIFOReplayBuffer", "FixedSizeReplayBuffer", - "AbsExperienceManager", "ExperienceSet", "ReplayBuffer", "UniformSampler", "UseAndDispose", + "AbsExperienceManager", "ExperienceSet", "UniformSampler", "UseAndDispose", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", "UniformNoiseExploration", - "AbsBlock", "AbsCoreModel", "FullyConnectedBlock", "OptimOption", "PolicyNetForDiscreteActionSpace", - "PolicyValueNetForContinuousActionSpace", "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace", - "AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy", + "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscreteActorNet", "DiscreteQNet", + "FullyConnectedBlock", "OptimOption", + "AbsCorePolicy", "AbsPolicy", "NullPolicy", "AbsPolicyManager", "AbsRolloutManager", "Actor", "PolicyUpdateTrigger", "Learner", "LocalPolicyManager", "LocalRolloutManager", "ParallelRolloutManager", - "StepBasedSchedule", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index ef17ba3db..dc772a157 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -7,7 +7,7 @@ import torch from maro.rl.experience import AbsExperienceManager -from maro.rl.model import PolicyValueNetForDiscreteActionSpace +from maro.rl.model import DiscreteACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -55,18 +55,18 @@ class ActorCritic(AbsCorePolicy): https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: - ac_net (PolicyValueNetForDiscreteActionSpace): Multi-task model that computes action distributions + ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. config: Configuration for the AC algorithm. """ def __init__( self, - ac_net: PolicyValueNetForDiscreteActionSpace, + ac_net: DiscreteACNet, experience_manager: AbsExperienceManager, config: ActorCriticConfig ): - if not isinstance(ac_net, PolicyValueNetForDiscreteActionSpace): - raise TypeError("model must be an instance of 'PolicyValueNetForDiscreteActionSpace'") + if not isinstance(ac_net, DiscreteACNet): + raise TypeError("model must be an instance of 'DiscreteACNet'") super().__init__(experience_manager) self.config = config diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index d88705ecb..0bfdb38c8 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -8,7 +8,7 @@ import torch from maro.rl.experience import AbsExperienceManager -from maro.rl.model import PolicyValueNetForContinuousActionSpace +from maro.rl.model import ContinuousACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -61,17 +61,17 @@ class DDPG(AbsCorePolicy): https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg Args: - ac_net (PolicyValueNetForContinuousActionSpace): DDPG policy and q-value models. + ac_net (ContinuousACNet): DDPG policy and q-value models. config (DDPGConfig): Configuration for DDPG algorithm. """ def __init__( self, - ac_net: PolicyValueNetForContinuousActionSpace, + ac_net: ContinuousACNet, experience_manager: AbsExperienceManager, config: DDPGConfig ): - if not isinstance(ac_net, PolicyValueNetForContinuousActionSpace): - raise TypeError("model must be an instance of 'PolicyValueNetForContinuousActionSpace'") + if not isinstance(ac_net, ContinuousACNet): + raise TypeError("model must be an instance of 'ContinuousACNet'") super().__init__(experience_manager) self.config = config diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index 7658b5849..f2cdb8449 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -7,7 +7,7 @@ import torch from maro.rl.experience import AbsExperienceManager -from maro.rl.model import QNetForDiscreteActionSpace +from maro.rl.model import DiscreteQNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -59,17 +59,17 @@ class DQN(AbsCorePolicy): See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: - model (QNetForDiscreteActionSpace): Q-value model. + model (DiscreteQNet): Q-value model. config (DQNConfig): Configuration for DQN algorithm. """ def __init__( self, - q_net: QNetForDiscreteActionSpace, + q_net: DiscreteQNet, experience_manager: AbsExperienceManager, config: DQNConfig ): - if not isinstance(q_net, QNetForDiscreteActionSpace): - raise TypeError("model must be an instance of 'QNetForDiscreteActionSpace'") + if not isinstance(q_net, DiscreteQNet): + raise TypeError("model must be an instance of 'DiscreteQNet'") super().__init__(experience_manager) self.config = config diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index b49a3966c..ebe66b580 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -7,7 +7,7 @@ import torch from torch.distributions import Categorical -from maro.rl.model import PolicyNetForDiscreteActionSpace +from maro.rl.model import DiscreteActorNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_truncated_cumulative_reward @@ -46,7 +46,7 @@ class PolicyGradient(AbsCorePolicy): """ def __init__( self, - model: PolicyNetForDiscreteActionSpace, + model: DiscreteActorNet, config: PolicyGradientConfig, experience_memory_size: int, experience_memory_overwrite_type: str, @@ -54,8 +54,8 @@ def __init__( new_experience_trigger: int = 1, min_experiences_to_trigger_training: int = 1 ): - if not isinstance(model, PolicyNetForDiscreteActionSpace): - raise TypeError("model must be an instance of 'PolicyNetForDiscreteActionSpace'") + if not isinstance(model, DiscreteActorNet): + raise TypeError("model must be an instance of 'DiscreteActorNet'") super().__init__( model, config, experience_memory_size, experience_memory_overwrite_type, empty_experience_memory_after_step, diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 3f75ead50..fcb70e565 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -4,13 +4,11 @@ from .abs_block import AbsBlock from .fc_block import FullyConnectedBlock from .core_model import ( - AbsCoreModel, OptimOption, PolicyNetForDiscreteActionSpace, PolicyValueNetForContinuousActionSpace, - PolicyValueNetForDiscreteActionSpace, QNetForDiscreteActionSpace + AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscreteActorNet, DiscreteQNet, OptimOption ) __all__ = [ "AbsBlock", "FullyConnectedBlock", - "AbsCoreModel", "OptimOption", "PolicyNetForDiscreteActionSpace", "PolicyValueNetForContinuousActionSpace", - "PolicyValueNetForDiscreteActionSpace", "QNetForDiscreteActionSpace" + "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscreteActorNet", "DiscreteQNet", "OptimOption" ] diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index f516ced9a..8e58b116b 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -132,7 +132,7 @@ def copy(self, with_optimizer: bool = False): return model_copy -class QNetForDiscreteActionSpace(AbsCoreModel): +class DiscreteQNet(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: @@ -162,7 +162,7 @@ def q_values(self, states, actions: torch.tensor): return q_for_all_actions.gather(1, actions).squeeze(dim=1) -class PolicyNetForDiscreteActionSpace(AbsCoreModel): +class DiscreteActorNet(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: @@ -187,7 +187,7 @@ def choose_action(self, states): return action, log_p -class PolicyValueNetForDiscreteActionSpace(AbsCoreModel): +class DiscreteACNet(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: @@ -212,7 +212,7 @@ def choose_action(self, states): return action, log_p -class PolicyValueNetForContinuousActionSpace(AbsCoreModel): +class ContinuousACNet(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 77e89d44b..4d97b0289 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .policy import AbsCorePolicy, AbsPolicy, NullPolicy, RLPolicy +from .policy import AbsCorePolicy, AbsPolicy, NullPolicy -__all__ = ["AbsCorePolicy", "AbsPolicy", "NullPolicy", "RLPolicy"] +__all__ = ["AbsCorePolicy", "AbsPolicy", "NullPolicy"] diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index ea7e58f7e..03f7ebee2 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -3,7 +3,6 @@ from abc import ABC, abstractmethod from collections import defaultdict, namedtuple -from email.policy import default from os import getcwd from typing import Dict, Union @@ -90,7 +89,7 @@ def __init__( if hasattr(policy, "experience_manager") and hasattr(policy, "update") } if update_trigger is None: - default_trigger = PolicyUpdateTrigger(min_new_experiences==1, num_warmup_experiences=1) + default_trigger = PolicyUpdateTrigger(min_new_experiences=1, num_warmup_experiences=1) self._update_trigger = {policy_id: default_trigger for policy_id in self._updatable_policy_dict} elif isinstance(update_trigger, dict): self._update_trigger = update_trigger From 8a0abc03fc14b749c92dd04969d5eb71dc7de976 Mon Sep 17 00:00:00 2001 From: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> Date: Thu, 20 May 2021 11:35:33 +0800 Subject: [PATCH 259/482] update rl init (#341) --- maro/rl/__init__.py | 6 +++--- maro/rl/env_wrapper/__init__.py | 2 +- maro/rl/experience/__init__.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 1b8be749d..6bb48df55 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. from maro.rl.algorithm import ( - DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, + ActorCritic, ActorCriticConfig, DDPG, DDPGConfig, DQN, DQNConfig, PolicyGradient, PolicyGradientConfig, get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) from maro.rl.env_wrapper import AbsEnvWrapper, AbsReplayBuffer, FIFOReplayBuffer, FixedSizeReplayBuffer @@ -18,8 +18,8 @@ ) from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( - AbsPolicyManager, AbsRolloutManager, Actor, PolicyUpdateTrigger, Learner, LocalPolicyManager, LocalRolloutManager, - ParallelRolloutManager + AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalPolicyManager, LocalRolloutManager, + ParallelRolloutManager, PolicyUpdateTrigger ) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, diff --git a/maro/rl/env_wrapper/__init__.py b/maro/rl/env_wrapper/__init__.py index 8599ce113..4b3e8fd0a 100644 --- a/maro/rl/env_wrapper/__init__.py +++ b/maro/rl/env_wrapper/__init__.py @@ -2,6 +2,6 @@ # Licensed under the MIT license. from .env_wrapper import AbsEnvWrapper -from .replay_buffer import AbsReplayBuffer, FIFOReplayBuffer, FixedSizeReplayBuffer, +from .replay_buffer import AbsReplayBuffer, FIFOReplayBuffer, FixedSizeReplayBuffer __all__ = ["AbsEnvWrapper", "AbsReplayBuffer", "FIFOReplayBuffer", "FixedSizeReplayBuffer"] diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index a52ca0f37..31183d0d8 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .experience import ExperienceSet, ReplayBuffer +from .experience import ExperienceSet from .experience_manager import AbsExperienceManager, UniformSampler, UseAndDispose -__all__ = ["AbsExperienceManager", "ExperienceSet", "ReplayBuffer", "UniformSampler", "UseAndDispose"] +__all__ = ["AbsExperienceManager", "ExperienceSet", "UniformSampler", "UseAndDispose"] From 4d79e5c1ff8eb4aacddde6250bf3bfa5effbc245 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 20 May 2021 03:48:45 +0000 Subject: [PATCH 260/482] more docs and small code edits --- docs/source/key_components/rl_toolkit.rst | 5 +- examples/cim/env_wrapper.py | 2 +- maro/rl/__init__.py | 4 +- maro/rl/algorithm/ac.py | 20 +++-- maro/rl/algorithm/ddpg.py | 24 +++--- maro/rl/algorithm/dqn.py | 32 ++++---- maro/rl/algorithm/pg.py | 89 ++++++++++------------- maro/rl/algorithm/rl_policy_index.py | 18 ++--- maro/rl/env_wrapper/env_wrapper.py | 2 +- maro/rl/model/__init__.py | 4 +- maro/rl/model/core_model.py | 42 +++++------ maro/rl/model/fc_block.py | 3 +- maro/rl/policy/policy.py | 2 +- 13 files changed, 115 insertions(+), 132 deletions(-) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 51f08f590..13aa70ce0 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -11,7 +11,7 @@ controllers such as `Actor <#actor>`_\ and `Learner <#learner>`_. Policy ------ -A policy is used by an agent to decide what action to take given an observation of the environment. +A policy is a mechanism used by an agent to determine an action to take given an observation of the environment. Accordingly, the abstract ``AbsPolicy`` class exposes a ``choose_action`` interface. This abstraction can encompass both static policies, such as rule-based policies, and updatable policies, such as RL policies. The latter is abstracted through the ``AbsCorePolicy`` sub-class which also exposes a ``update`` @@ -51,7 +51,8 @@ the actor-critic algorithm does not need to concern itself with the structures a critic models. The abstraction consists of ``AbsBlock`` and ``AbsCoreModel``, both of which subclass torch's nn.Module. The ``AbsBlock`` represents the smallest structural unit of an NN-based model. For instance, the ``FullyConnectedBlock`` is a stack of fully connected layers with features like batch normalization, drop-out and skip connection. -The ``AbsCoreModel`` is a collection of network components with embedded optimizers. Several classes are designed +The ``AbsCoreModel`` is a collection of network components with embedded optimizers. Subclasses of +``AbsCoreModel`` provided for use with specific RL algorithms include As an example, the initialization of the actor-critic algorithm may look like this: diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index b4be56c96..de5f27765 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -40,7 +40,7 @@ def get_state(self, event): state = np.concatenate((port_features, vessel_features)) return {port_idx: state} - def get_action(self, action_by_agent): + def to_env_action(self, action_by_agent): vessel_snapshots = self.env.snapshot_list["vessels"] action_info = list(action_by_agent.values())[0] model_action = action_info[0] if isinstance(action_info, tuple) else action_info diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 1b8be749d..79fd5f2da 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -13,7 +13,7 @@ UniformNoiseExploration ) from maro.rl.model import ( - AbsBlock, AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscreteActorNet, DiscreteQNet, FullyConnectedBlock, + AbsBlock, AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, FullyConnectedBlock, OptimOption ) from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy @@ -34,7 +34,7 @@ "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", "UniformNoiseExploration", - "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscreteActorNet", "DiscreteQNet", + "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", "AbsPolicyManager", "AbsRolloutManager", "Actor", "PolicyUpdateTrigger", "Learner", "LocalPolicyManager", "LocalRolloutManager", diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index dc772a157..fdafffd40 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -57,20 +57,18 @@ class ActorCritic(AbsCorePolicy): Args: ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. + experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces + for storing and retrieving experiences for training. config: Configuration for the AC algorithm. """ - def __init__( - self, - ac_net: DiscreteACNet, - experience_manager: AbsExperienceManager, - config: ActorCriticConfig - ): + def __init__(self, ac_net: DiscreteACNet, experience_manager: AbsExperienceManager, config: ActorCriticConfig): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") super().__init__(experience_manager) - self.config = config self.ac_net = ac_net + self.config = config + self.device = self.ac_net.device def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: """Use the actor (policy) model to generate stochastic actions. @@ -82,7 +80,7 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: Actions and corresponding log probabilities. """ with torch.no_grad(): - actions, log_p = self.ac_net.choose_action(states) + actions, log_p = self.ac_net.get_action(states) actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p @@ -91,9 +89,9 @@ def update(self): for _ in range(self.config.train_epochs): experience_set = self.experience_manager.get() states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.ac_net.device) - log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.ac_net.device) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.ac_net.device) + actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.device) + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) for _ in range(self.config.gradient_iters): state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index 0bfdb38c8..087c35b2a 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -62,26 +62,28 @@ class DDPG(AbsCorePolicy): Args: ac_net (ContinuousACNet): DDPG policy and q-value models. + experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces + for storing and retrieving experiences for training. config (DDPGConfig): Configuration for DDPG algorithm. """ - def __init__( - self, - ac_net: ContinuousACNet, - experience_manager: AbsExperienceManager, - config: DDPGConfig - ): + def __init__(self, ac_net: ContinuousACNet, experience_manager: AbsExperienceManager, config: DDPGConfig): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") super().__init__(experience_manager) - self.config = config self.ac_net = ac_net - self.target_ac_net = ac_net.copy() if self.ac_net.trainable else None + if self.ac_net.trainable: + self.target_ac_net = ac_net.copy() + self.target_ac_net.eval() + else: + self.target_ac_net = None + self.config = config + self.device = self.ac_net.device self._train_cnt = 0 def choose_action(self, states) -> Union[float, np.ndarray]: with torch.no_grad(): - actions = self.ac_net.choose_action(states).cpu().numpy() + actions = self.ac_net.get_action(states).cpu().numpy() return actions[0] if len(actions) == 1 else actions @@ -90,8 +92,8 @@ def update(self): for _ in range(self.config.train_epochs): experience_set = self.experience_manager.get() states, next_states = experience_set.states, experience_set.next_states - actual_actions = torch.from_numpy(experience_set.actions).to(self.ac_net.device) - rewards = torch.from_numpy(experience_set.rewards).to(self.ac_net.device) + actual_actions = torch.from_numpy(experience_set.actions).to(self.device) + rewards = torch.from_numpy(experience_set.rewards).to(self.device) if len(actual_actions.shape) == 1: actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index f2cdb8449..b10cb4bed 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -59,46 +59,48 @@ class DQN(AbsCorePolicy): See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: - model (DiscreteQNet): Q-value model. + q_net (DiscreteQNet): Q-value model. + experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces + for storing and retrieving experiences for training. config (DQNConfig): Configuration for DQN algorithm. """ - def __init__( - self, - q_net: DiscreteQNet, - experience_manager: AbsExperienceManager, - config: DQNConfig - ): + def __init__(self, q_net: DiscreteQNet, experience_manager: AbsExperienceManager, config: DQNConfig): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") super().__init__(experience_manager) - self.config = config self.q_net = q_net - self.target_q_net = q_net.copy() if q_net.trainable else None - self.target_q_net.eval() + if self.q_net.trainable: + self.target_q_net = q_net.copy() + self.target_q_net.eval() + else: + self.target_q_net = None + self.config = config + self.device = self.q_net.device self._training_counter = 0 def choose_action(self, states) -> Union[int, np.ndarray]: with torch.no_grad(): self.q_net.eval() - actions, _ = self.q_net.choose_action(states) + actions, _ = self.q_net.get_action(states) actions = actions.cpu().numpy() return actions[0] if len(actions) == 1 else actions def update(self): + assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() for _ in range(self.config.train_epochs): # sample from the replay memory experience_set = self.experience_manager.get() states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.q_net.device) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.q_net.device) + actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) if self.config.double: for _ in range(self.config.gradient_iters): # get target Q values with torch.no_grad(): - actions_by_eval_q_net = self.q_net.choose_action(next_states)[0] + actions_by_eval_q_net = self.q_net.get_action(next_states)[0] next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) @@ -109,7 +111,7 @@ def update(self): else: # get target Q values with torch.no_grad(): - next_q_values = self.target_q_net.choose_action(next_states)[1] # (N,) + next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) # gradient steps diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index ebe66b580..8149afbe5 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -1,13 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from maro.rl.experience.experience_manager import AbsExperienceManager from typing import Tuple import numpy as np import torch -from torch.distributions import Categorical -from maro.rl.model import DiscreteActorNet +from maro.rl.model import DiscretePolicyNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_truncated_cumulative_reward @@ -30,40 +30,23 @@ class PolicyGradient(AbsCorePolicy): Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. Args: - model (ParameterizedPolicy): Multi-task model that computes action distributions and state values. + policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. + experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces + for storing and retrieving experiences for training. config (PolicyGradientConfig): Configuration for the PG algorithm. - experience_memory_size (int): Size of the experience memory. If it is -1, the experience memory is of - unlimited size. - experience_memory_overwrite_type (str): A string indicating how experiences in the experience memory are - to be overwritten after its capacity has been reached. Must be "rolling" or "random". - empty_experience_memory_after_step (bool): If True, the experience memory will be emptied after each call - to ``step``. Defaults to True. - new_experience_trigger (int): Minimum number of new experiences required to trigger learning. - Defaults to 1. - min_experiences_to_trigger_training (int): Minimum number of experiences in the experience memory required for - training. Defaults to 1. """ def __init__( - self, - model: DiscreteActorNet, - config: PolicyGradientConfig, - experience_memory_size: int, - experience_memory_overwrite_type: str, - empty_experience_memory_after_step: bool = True, - new_experience_trigger: int = 1, - min_experiences_to_trigger_training: int = 1 + self, policy_net: DiscretePolicyNet, experience_manager: AbsExperienceManager, config: PolicyGradientConfig, ): - if not isinstance(model, DiscreteActorNet): - raise TypeError("model must be an instance of 'DiscreteActorNet'") - super().__init__( - model, config, experience_memory_size, experience_memory_overwrite_type, - empty_experience_memory_after_step, - new_experience_trigger=new_experience_trigger, - min_experiences_to_trigger_training=min_experiences_to_trigger_training - ) - - def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + if not isinstance(policy_net, DiscretePolicyNet): + raise TypeError("model must be an instance of 'DiscretePolicyNet'") + super().__init__(experience_manager) + self.policy_net = policy_net + self.config = config + self.device = self.policy_net.device + + def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Use the actor (policy) model to generate stochastic actions. Args: @@ -72,26 +55,28 @@ def choose_action(self, state: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: Returns: Actions and corresponding log probabilities. """ - state = torch.from_numpy(state).to(self.device) - is_single = len(state.shape) == 1 - if is_single: - state = state.unsqueeze(dim=0) - - action_prob = Categorical(self.model(state, training=False)) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - action, log_p = action.cpu().numpy(), log_p.cpu().numpy() - return (action[0], log_p[0]) if is_single else (action, log_p) - - def step(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray): - if not isinstance(experience_set, ExperienceSet): - raise TypeError(f"Expected experience object of type ExperienceSet, got {type(experience_set)}") - - states = experience_set.states - actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])) - log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)) - returns = get_truncated_cumulative_reward(rewards, self.special_config.reward_discount) + with torch.no_grad(): + actions, log_p = self.policy_net.get_action(states) + actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() + return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p + + def update(self): + """ + This should be called at the end of a simulation episode and the experiences obtained from + the experience manager's ``get`` method should be a sequential set, i.e., in the order in + which they are generated during the simulation. Otherwise, the return values may be meaningless. + """ + self.policy_net.train() + experience_set = self.experience_manager.get() + log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) + returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) returns = torch.from_numpy(returns).to(self.device) loss = -(log_p * returns).mean() - self.model.step(loss) + self.policy_net.step(loss) + + def set_state(self, policy_state): + self.policy_net.load_state_dict(policy_state) + + def get_state(self): + return self.policy_net.state_dict() diff --git a/maro/rl/algorithm/rl_policy_index.py b/maro/rl/algorithm/rl_policy_index.py index 397d0b51e..331ebf7bc 100644 --- a/maro/rl/algorithm/rl_policy_index.py +++ b/maro/rl/algorithm/rl_policy_index.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .ac import ActorCritic, ActorCriticConfig, PolicyValueNetForDiscreteActionSpace -from .ddpg import DDPG, DDPGConfig, PolicyValueNetForContinuousActionSpace -from .dqn import DQN, DQNConfig, QNetForDiscreteActionSpace -from .pg import PolicyGradient, PolicyGradientConfig, PolicyNetForDiscreteActionSpace +from .ac import ActorCritic, ActorCriticConfig, DiscreteACNet +from .ddpg import DDPG, DDPGConfig, ContinuousACNet +from .dqn import DQN, DQNConfig, DiscreteQNet +from .pg import PolicyGradient, PolicyGradientConfig, DiscretePolicyNet RL_POLICY_INDEX = { @@ -15,17 +15,17 @@ } RL_POLICY_CONFIG_INDEX = { - "ac": ActorCritic, + "ac": ActorCriticConfig, "dqn": DQNConfig, "ddpg": DDPGConfig, "pg": PolicyGradientConfig } RL_POLICY_MODEL_INDEX = { - "ac": PolicyValueNetForDiscreteActionSpace, - "dqn": QNetForDiscreteActionSpace, - "ddpg": PolicyValueNetForContinuousActionSpace, - "pg": PolicyNetForDiscreteActionSpace + "ac": DiscreteACNet, + "dqn": DiscreteQNet, + "ddpg": ContinuousACNet, + "pg": DiscretePolicyNet } diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 9ae0d3c85..1bd8c502c 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -83,7 +83,7 @@ def get_state(self, tick: int = None) -> dict: pass @abstractmethod - def get_action(self, action): + def to_env_action(self, action): """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" pass diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index fcb70e565..74746b038 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -4,11 +4,11 @@ from .abs_block import AbsBlock from .fc_block import FullyConnectedBlock from .core_model import ( - AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscreteActorNet, DiscreteQNet, OptimOption + AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, OptimOption ) __all__ = [ "AbsBlock", "FullyConnectedBlock", - "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscreteActorNet", "DiscreteQNet", "OptimOption" + "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "OptimOption" ] diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 8e58b116b..2529cc53c 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -123,30 +123,26 @@ def soft_update(self, other_model: nn.Module, tau: float): for params, other_params in zip(self.parameters(), other_model.parameters()): params.data = (1 - tau) * params.data + tau * other_params.data - def copy(self, with_optimizer: bool = False): + def copy(self, with_optimizer: bool = False, device: str = None): model_copy = clone(self) if not with_optimizer: model_copy.optimizer = None model_copy.scheduler = None + device = self.device if device is None else torch.device(device) + model_copy.to(device) + return model_copy class DiscreteQNet(AbsCoreModel): - """A compound network structure that consists of multiple task heads and an optional shared stack. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - All components must have the same input dimension except the one designated as the shared - component by ``shared_component_name``. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for - the components. Defaults to None. - """ + """NN-based Q-value model.""" @abstractmethod def forward(self, states) -> torch.tensor: + """Output Q values""" raise NotImplementedError - def choose_action(self, states): + def get_action(self, states): """ Given Q-values for a batch of states and all actions, return the maximum Q-value and the corresponding action index for each state. @@ -162,7 +158,7 @@ def q_values(self, states, actions: torch.tensor): return q_for_all_actions.gather(1, actions).squeeze(dim=1) -class DiscreteActorNet(AbsCoreModel): +class DiscretePolicyNet(AbsCoreModel): """A compound network structure that consists of multiple task heads and an optional shared stack. Args: @@ -176,7 +172,7 @@ class DiscreteActorNet(AbsCoreModel): def forward(self, states) -> torch.tensor: raise NotImplementedError - def choose_action(self, states): + def get_action(self, states): """ Given Q-values for a batch of states and all actions, return the maximum Q-value and the corresponding action index for each state. @@ -201,10 +197,10 @@ class DiscreteACNet(AbsCoreModel): def forward(self, states, output_action_probs: bool = True, output_values: bool = True): raise NotImplementedError - def choose_action(self, states): + def get_action(self, states): """ - Given Q-values for a batch of states and all actions, return the maximum Q-value and - the corresponding action index for each state. + Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value + for each state. """ action_prob = Categorical(self.forward(states, output_values=False)) # (batch_size, num_actions) action = action_prob.sample() @@ -219,19 +215,17 @@ class ContinuousACNet(AbsCoreModel): component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. All components must have the same input dimension except the one designated as the shared component by ``shared_component_name``. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for - the components. Defaults to None. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for the components. + Defaults to None. """ @abstractmethod def forward(self, states, actions=None): raise NotImplementedError - def choose_action(self, states): - """ - Given Q-values for a batch of states and all actions, return the maximum Q-value and - the corresponding action index for each state. - """ + def get_action(self, states): + """Compute the actions given a batch of states.""" return self.forward(states) def value(self, states): - return self.forward(states, actions=self.forward(states)) + """Compute the Q-values for a batch of states using the actions computed from them.""" + return self.forward(states, actions=self.get_action(states)) diff --git a/maro/rl/model/fc_block.py b/maro/rl/model/fc_block.py index a648a3fd3..811d2cf43 100644 --- a/maro/rl/model/fc_block.py +++ b/maro/rl/model/fc_block.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from collections import OrderedDict +from typing import List import torch import torch.nn as nn @@ -37,7 +38,7 @@ def __init__( self, input_dim: int, output_dim: int, - hidden_dims: [int], + hidden_dims: List[int], activation="relu", head: bool = False, softmax: bool = False, diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 5fb4be54a..286e3281a 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -31,7 +31,7 @@ class AbsCorePolicy(AbsPolicy): Reinforcement learning (RL) policies should inherit from this. Args: - experience_manager (AbsExperienceManager): An experience manager that exposes put() and get() interfaces + experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces for storing and retrieving experiences for training. """ def __init__(self, experience_manager: AbsExperienceManager): From 1966c54f845cbb6ff5cb05f27f65a9b5c38671f9 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 21 May 2021 06:46:38 +0000 Subject: [PATCH 261/482] merged replay buffer and experience manager --- docs/source/key_components/rl_toolkit.rst | 22 +++- maro/rl/__init__.py | 8 +- maro/rl/algorithm/ac.py | 13 +- maro/rl/algorithm/pg.py | 9 +- maro/rl/env_wrapper/env_wrapper.py | 59 +++++---- maro/rl/experience/__init__.py | 7 +- maro/rl/experience/experience.py | 30 ----- maro/rl/experience/experience_manager.py | 122 ------------------ maro/rl/experience/experience_memory.py | 150 ++++++++++++++++++++++ maro/rl/experience/sampler.py | 32 +++++ maro/rl/model/core_model.py | 99 ++++++++------ maro/rl/training/learner.py | 2 - maro/rl/training/policy_manager.py | 1 + maro/rl/training/rollout_manager.py | 4 +- 14 files changed, 303 insertions(+), 255 deletions(-) delete mode 100644 maro/rl/experience/experience.py delete mode 100644 maro/rl/experience/experience_manager.py create mode 100644 maro/rl/experience/experience_memory.py create mode 100644 maro/rl/experience/sampler.py diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 13aa70ce0..5832ed769 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -16,7 +16,7 @@ Accordingly, the abstract ``AbsPolicy`` class exposes a ``choose_action`` interf can encompass both static policies, such as rule-based policies, and updatable policies, such as RL policies. The latter is abstracted through the ``AbsCorePolicy`` sub-class which also exposes a ``update`` interface. By default, updatable policies require an experience manager to store and retrieve simulation -data (in the form of "experiences") based on which updates can be made. +data (in the form of "experiences sets") based on which updates can be made. .. image:: ../images/rl/agent.svg @@ -50,16 +50,24 @@ to decouple the the inner workings of these models from the algorithmic aspects the actor-critic algorithm does not need to concern itself with the structures and optimizing schemes of the actor and critic models. The abstraction consists of ``AbsBlock`` and ``AbsCoreModel``, both of which subclass torch's nn.Module. The ``AbsBlock`` represents the smallest structural unit of an NN-based model. For instance, the ``FullyConnectedBlock`` -is a stack of fully connected layers with features like batch normalization, drop-out and skip connection. -The ``AbsCoreModel`` is a collection of network components with embedded optimizers. Subclasses of -``AbsCoreModel`` provided for use with specific RL algorithms include +is a stack of fully connected layers with features like batch normalization, drop-out and skip connection. The ``AbsCoreModel`` +is a collection of network components with embedded optimizers. Subclasses of ``AbsCoreModel`` provided for use with specific +RL algorithms include ``DiscreteQNet`` for DQN, ``DiscretePolicyNet`` for Policy Gradient, ``DiscreteACNet`` for Actor-Critic +and ``ContinuousACNet`` for DDPG. -As an example, the initialization of the actor-critic algorithm may look like this: +The code snippet below shows how to create a model for the actor-critic algorithm with a shared bottom stack: .. code-block:: python - actor_stack = FullyConnectedBlock(...) - critic_stack = FullyConnectedBlock(...) + class MyACModel(DiscreteACNet): + def forward(self, states): + features = self.component["representation"](states) + + + + shared_stack = FullyConnectedBlock(...) + actor_head = FullyConnectedBlock(...) + critic_head = FullyConnectedBlock(...) ac_model = SimpleMultiHeadModel( {"actor": actor_stack, "critic": critic_stack}, optim_option={ diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index b3506267b..e6d32ddc3 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -5,8 +5,8 @@ ActorCritic, ActorCriticConfig, DDPG, DDPGConfig, DQN, DQNConfig, PolicyGradient, PolicyGradientConfig, get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) -from maro.rl.env_wrapper import AbsEnvWrapper, AbsReplayBuffer, FIFOReplayBuffer, FixedSizeReplayBuffer -from maro.rl.experience import AbsExperienceManager, ExperienceSet, UniformSampler, UseAndDispose +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.experience import AbsSampler, ExperienceMemory, ExperienceSet, UniformSampler from maro.rl.exploration import ( AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, @@ -29,8 +29,8 @@ __all__ = [ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", - "AbsEnvWrapper", "AbsReplayBuffer", "FIFOReplayBuffer", "FixedSizeReplayBuffer", - "AbsExperienceManager", "ExperienceSet", "UniformSampler", "UseAndDispose", + "AbsEnvWrapper", + "AbsSampler", "ExperienceMemory", "ExperienceSet", "UniformSampler", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", "UniformNoiseExploration", diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index fdafffd40..bf74f5111 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -71,14 +71,7 @@ def __init__(self, ac_net: DiscreteACNet, experience_manager: AbsExperienceManag self.device = self.ac_net.device def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: - """Use the actor (policy) model to generate stochastic actions. - - Args: - state: Input to the actor model. - - Returns: - Actions and corresponding log probabilities. - """ + """Return actions and log probabilities for given states.""" with torch.no_grad(): actions, log_p = self.ac_net.get_action(states) actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() @@ -94,8 +87,8 @@ def update(self): rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) for _ in range(self.config.gradient_iters): - state_values = self.ac_net(states, output_action_probs=False).detach().squeeze() - next_state_values = self.ac_net(next_states, output_action_probs=False).detach().squeeze() + state_values = self.ac_net(states, actor=False)[1].detach().squeeze() + next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() return_est = rewards + self.config.reward_discount * next_state_values advantages = return_est - state_values diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index 8149afbe5..8eb735f1d 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -47,14 +47,7 @@ def __init__( self.device = self.policy_net.device def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """Use the actor (policy) model to generate stochastic actions. - - Args: - state: Input to the actor model. - - Returns: - Actions and corresponding log probabilities. - """ + """Return actions and log probabilities for given states.""" with torch.no_grad(): actions, log_p = self.policy_net.get_action(states) actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 1bd8c502c..fe7551f0c 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -1,14 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import time from abc import ABC, abstractmethod from collections import deque from typing import Dict from maro.simulator import Env - -from .replay_buffer import AbsReplayBuffer +from maro.rl.experience import AbsExperienceManager, ExperienceSet class AbsEnvWrapper(ABC): @@ -26,7 +24,7 @@ class AbsEnvWrapper(ABC): def __init__( self, env: Env, - replay_buffer: Dict[str, AbsReplayBuffer] = None, + replay_buffer: Dict[str, AbsExperienceManager] = None, reward_eval_delay: int = 0 ): self.env = env @@ -80,12 +78,12 @@ def get_state(self, tick: int = None) -> dict: tick (int): The tick for which to compute the environmental state. If computing the current state, use tick=self.env.tick. """ - pass + raise NotImplementedError @abstractmethod def to_env_action(self, action): """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" - pass + raise NotImplementedError @abstractmethod def get_reward(self, tick: int = None): @@ -96,6 +94,13 @@ def get_reward(self, tick: int = None): of delayed reward evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. """ + raise NotImplementedError + + def get_transition_info(self): + """Get additional info for a transition. + + The returned transition info will be stored in the experience manager alongside states, actions, rewards. + """ pass def step(self, action_by_agent: dict): @@ -104,16 +109,23 @@ def step(self, action_by_agent: dict): The new transition is stored in the replay buffer or cached in a separate data structure if the reward cannot be determined yet due to a non-zero ``reward_eval_delay``. """ - # t0 = time.time() self._step_index += 1 - env_action = self.get_action(action_by_agent) - self._pending_reward_cache.append((self._state, action_by_agent, self.env.tick)) + env_action = self.to_env_action(action_by_agent) if len(env_action) == 1: env_action = list(env_action.values())[0] - # t1 = time.time() + pre_action_tick = self.env.tick _, self._event, done = self.env.step(env_action) - # t2 = time.time() - # self._tot_raw_step_time += t2 - t1 + + if not done: + prev_state = self._state # previous env state + transition_info = self.get_transition_info() + self._state = self.get_state(self.env.tick) # current env state + self._pending_reward_cache.append( + (prev_state, action_by_agent, self._state, transition_info, pre_action_tick) + ) + else: + self._state = None + self.end_of_episode() """ If this is the final step, evaluate rewards for all remaining events except the last. @@ -121,30 +133,17 @@ def step(self, action_by_agent: dict): """ while ( self._pending_reward_cache and - (done or self.env.tick - self._pending_reward_cache[0][2] >= self.reward_eval_delay) + (done or self.env.tick - self._pending_reward_cache[0][-1] >= self.reward_eval_delay) ): - state, action, tick = self._pending_reward_cache.popleft() + state, action, state_, info, tick = self._pending_reward_cache.popleft() reward = self.get_reward(tick=tick) # assign rewards to the agents that took action at that tick for agent_id, act in action.items(): - rw = reward.get(agent_id, 0) - if not done and self.replay: - self.replay[agent_id].push(state[agent_id], act, rw) + st, st_, rw = state[agent_id], state_[agent_id], reward.get(agent_id, .0) + if not done and self.replay and agent_id in self.replay: + self.replay[agent_id].put(ExperienceSet([st], [act], [rw], [st_], [info])) self._total_reward += rw - if not done: - self._state = self.get_state(self.env.tick) - # t3 = time.time() - # self._tot_step_time += t3 - t0 - else: - self._state = None - self.end_of_episode() - - # print(f"total raw step time: {self._tot_raw_step_time}") - # print(f"total step time: {self._tot_step_time}") - # self._tot_raw_step_time = 0 - # self._tot_step_time = 0 - def end_of_episode(self): """Custom processing logic at the end of an episode.""" pass diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 31183d0d8..6818e8e13 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .experience import ExperienceSet -from .experience_manager import AbsExperienceManager, UniformSampler, UseAndDispose +from .experience_memory import ExperienceMemory, ExperienceSet +from .sampler import AbsSampler, UniformSampler -__all__ = ["AbsExperienceManager", "ExperienceSet", "UniformSampler", "UseAndDispose"] + +__all__ = ["AbsSampler", "ExperienceMemory", "ExperienceSet", "UniformSampler"] diff --git a/maro/rl/experience/experience.py b/maro/rl/experience/experience.py deleted file mode 100644 index d115c97c3..000000000 --- a/maro/rl/experience/experience.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.utils.exception.rl_toolkit_exception import InvalidExperience - - -class ExperienceSet: - - __slots__ = ["states", "actions", "rewards", "next_states"] - - def __init__(self, states: list = None, actions: list = None, rewards: list = None, next_states: list = None): - if states is None: - states, actions, rewards, next_states = [], [], [], [] - - if not len(states) == len(actions) == len(rewards) == len(next_states): - raise InvalidExperience("values of contents should consist of lists of the same length") - self.states = states - self.actions = actions - self.rewards = rewards - self.next_states = next_states - - @property - def size(self): - return len(self.states) - - def extend(self, other): - self.states += other.states - self.actions += other.actions - self.rewards += other.rewards - self.next_states += other.next_states diff --git a/maro/rl/experience/experience_manager.py b/maro/rl/experience/experience_manager.py deleted file mode 100644 index 3610e5973..000000000 --- a/maro/rl/experience/experience_manager.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - -import numpy as np - -from .experience import ExperienceSet - - -class AbsExperienceManager(ABC): - """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". - - This implementation uses a dictionary of lists as the internal data structure. The objects for each key - are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during - put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited - and limited storage are supported. - - Args: - capacity (int): Maximum number of experiences that can be stored. - overwrite_type (str): If storage capacity is bounded, this specifies how existing entries - are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - - "rolling", where overwrite occurs sequentially with wrap-around. - - "random", where overwrite occurs randomly among filled positions. - Alternatively, the user may also specify overwrite positions (see ``put``). - """ - def __init__(self, capacity: int, overwrite_type: str = "rolling"): - super().__init__() - if overwrite_type not in {"rolling", "random"}: - raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") - self._capacity = capacity - self._overwrite_type = overwrite_type - self.keys = ExperienceSet.__slots__ - self.data = {key: [None] * self._capacity for key in self.keys} - self._size = 0 - - @property - def size(self): - return self._size - - @property - def capacity(self): - return self._capacity - - @property - def overwrite_type(self): - return self._overwrite_type - - @abstractmethod - def get(self) -> ExperienceSet: - raise NotImplementedError - - def put(self, experience_set: ExperienceSet): - """Put new contents in the store. - - Args: - contents (dict): Dictionary of items to add to the store. If the store is not empty, this must have the - same keys as the store itself. Otherwise an ``StoreMisalignment`` will be raised. - - Returns: - The indexes where the newly added entries reside in the store. - """ - added_size = experience_set.size - if added_size > self._capacity: - raise ValueError("size of added items should not exceed the capacity.") - - num_experiences = self._size + added_size - num_overwrites = num_experiences - self._capacity - if num_overwrites <= 0: - indexes = list(range(self._size, num_experiences)) - # follow the overwrite rule set at init - elif self._overwrite_type == "rolling": - # using the negative index convention for convenience - start_index = self._size - self._capacity - indexes = list(range(start_index, start_index + added_size)) - else: - random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) - indexes = list(range(self._size, self._capacity)) + list(random_indexes) - - for key in self.data: - for idx, val in zip(indexes, getattr(experience_set, key)): - self.data[key][idx] = val - - self._size = min(self._capacity, num_experiences) - - def clear(self): - """Empty the store.""" - self.data = {key: [None] * self._capacity for key in self.keys} - self._size = 0 - - -class UniformSampler(AbsExperienceManager): - """Experience memory that stores RL experiences in the form of "state", "action", "reward", "next_state". - - This implementation uses a dictionary of lists as the internal data structure. The objects for each key - are stored in a list. To be useful for experience storage in RL, uniformity checks are performed during - put operations to ensure that the list lengths stay the same for all keys at all times. Both unlimited - and limited storage are supported. - - Args: - capacity (int): If negative, the store is of unlimited capacity. Defaults to -1. - overwrite_type (str): If storage capacity is bounded, this specifies how existing entries - are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - - "rolling", where overwrite occurs sequentially with wrap-around. - - "random", where overwrite occurs randomly among filled positions. - Alternatively, the user may also specify overwrite positions (see ``put``). - """ - def __init__(self, capacity: int, batch_size: int, overwrite_type: str = None, replace: bool = True): - super().__init__(capacity, overwrite_type=overwrite_type) - self.batch_size = batch_size - self.replace = replace - - def get(self) -> ExperienceSet: - indexes = np.random.choice(self._size, size=self.batch_size, replace=self.replace) - return ExperienceSet(*[[self.data[key][idx] for idx in indexes] for key in self.keys]) - - -class UseAndDispose(AbsExperienceManager): - def get(self) -> ExperienceSet: - exp_set = ExperienceSet(*[self.data[key] for key in self.keys]) - self.clear() - return exp_set diff --git a/maro/rl/experience/experience_memory.py b/maro/rl/experience/experience_memory.py new file mode 100644 index 000000000..bf5c9c9d4 --- /dev/null +++ b/maro/rl/experience/experience_memory.py @@ -0,0 +1,150 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import numpy as np + +from maro.utils.exception.rl_toolkit_exception import InvalidExperience + + +class ExperienceSet: + """Wrapper for a set of experiences. + + An experience consists of state, action, reward, next state and auxillary information. + """ + __slots__ = ["states", "actions", "rewards", "next_states", "info"] + + def __init__( + self, + states: list = None, + actions: list = None, + rewards: list = None, + next_states: list = None, + info: list = None + ): + if states is None: + states, actions, rewards, next_states, info = [], [], [], [], [] + + if not len(states) == len(actions) == len(rewards) == len(next_states) == len(info): + raise InvalidExperience("values of contents should consist of lists of the same length") + self.states = states + self.actions = actions + self.rewards = rewards + self.next_states = next_states + self.info = info + + @property + def size(self): + return len(self.states) + + def extend(self, other): + """Concatenate the set with another experience set.""" + self.states += other.states + self.actions += other.actions + self.rewards += other.rewards + self.next_states += other.next_states + self.info += other.info + + +class ExperienceMemory: + """Storage facility for simulation experiences. + + This implementation uses a dictionary of lists as the internal data structure. The objects for each key + are stored in a list. + + Args: + capacity (int): Maximum number of experiences that can be stored. + overwrite_type (str): If storage capacity is bounded, this specifies how existing entries + are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: + - "rolling", where overwrite occurs sequentially with wrap-around. + - "random", where overwrite occurs randomly among filled positions. + """ + def __init__(self, capacity: int, overwrite_type: str = "rolling"): + super().__init__() + if overwrite_type not in {"rolling", "random"}: + raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") + self._capacity = capacity + self._overwrite_type = overwrite_type + self._keys = ExperienceSet.__slots__ + self.data = {key: [None] * self._capacity for key in self._keys} + self._size = 0 + self._index = 0 + self.sampler = None + + @property + def capacity(self): + """Capacity of the memory.""" + return self._capacity + + @property + def overwrite_type(self): + """Overwrite method after the memory has reached capacity.""" + return self._overwrite_type + + @property + def size(self): + """Current number of experiences stored.""" + return self._size + + @property + def keys(self): + """Keys as specified by ``ExperienceSet``.""" + return self._keys + + def put(self, experience_set: ExperienceSet): + """Put a experience set in the store. + + Args: + experience_set (ExperienceSet): Experience set to be put in the store. If the store is full, + existing items will be overwritten according to the ``overwrite_type`` property. + + """ + added_size = experience_set.size + if added_size > self._capacity: + raise ValueError("size of added items should not exceed the capacity.") + + num_experiences = self._size + added_size + num_overwrites = num_experiences - self._capacity + if num_overwrites <= 0: + indexes = list(range(self._size, num_experiences)) + # follow the overwrite rule set at init + elif self._overwrite_type == "rolling": + # using the negative index convention for convenience + start_index = self._size - self._capacity + indexes = list(range(start_index, start_index + added_size)) + else: + random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) + indexes = list(range(self._size, self._capacity)) + list(random_indexes) + + for key in self.data: + for idx, val in zip(indexes, getattr(experience_set, key)): + self.data[key][idx] = val + + self._size = min(self._capacity, num_experiences) + + def get(self): + """Retrieve an experience set from the memory. + + If not sampler has been registered, the entirety of the stored data will be returned in the form of + an ``ExperienceSet`` and the memory will be cleared. Otherwise, a sample from the memory will be + returned according to the sampling logic defined by the registered sampler. + """ + if self.sampler is None: + ret = ExperienceSet(*[self.data[key] for key in self._keys]) + self.clear() + return ret + else: + return self.sampler.get() + + def register_sampler(self, sampler_cls, **sampler_params): + """Register a sampler to the experience memory. + + Args: + sampler_cls: Type of sampler to be registered. Must be a subclass of ``AbsSampler``. + sampler_params: Keyword parameters for ``sampler_cls``. + """ + self.sampler = sampler_cls(self, **sampler_params) + + def clear(self): + """Empty the memory.""" + self.data = {key: [None] * self._capacity for key in self._keys} + self._size = 0 diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py new file mode 100644 index 000000000..a136edc35 --- /dev/null +++ b/maro/rl/experience/sampler.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from maro.rl.experience.experience_memory import ExperienceMemory + +import numpy as np + +from .experience_memory import ExperienceMemory, ExperienceSet + + +class AbsSampler(ABC): + def __init__(self, experience_memory: ExperienceMemory): + super().__init__() + self.experience_memory = experience_memory + + @abstractmethod + def get(self) -> ExperienceSet: + raise NotImplementedError + + +class UniformSampler(AbsSampler): + def __init__(self, experience_memory: ExperienceMemory, batch_size: int, replace: bool = True): + super().__init__(experience_memory) + self.batch_size = batch_size + self.replace = replace + + def get(self) -> ExperienceSet: + indexes = np.random.choice(self.experience_memory.size, size=self.batch_size, replace=self.replace) + return ExperienceSet( + *[[self.experience_memory.data[key][idx] for idx in indexes] for key in self.experience_memory.keys] + ) diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 2529cc53c..7d97e1a7b 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -15,6 +15,7 @@ class OptimOption: """Model optimization options. + Args: optim_cls: A string indicating an optimizer class provided by torch.optim or custom subclass of torch.optim.Optimizer. If a string is provided, it must be present in the ``TORCH_OPTIM`` index. @@ -77,6 +78,7 @@ def __init__( @property def trainable(self) -> bool: + """Return True if at least one optimizer is registered.""" return self.optimizer is not None @abstractmethod @@ -84,7 +86,11 @@ def forward(self, *args, **kwargs): raise NotImplementedError def step(self, loss): - """Use the loss to back-propagate gradients and apply them to the underlying parameters.""" + """Use the loss to back-propagate gradients and apply them to the underlying parameters. + + Args: + loss: Result of a computation graph that involves the underlying parameters. + """ if self.optimizer is None: raise MissingOptimizer("No optimizer registered to the model") if isinstance(self.optimizer, dict): @@ -120,10 +126,26 @@ def update_learning_rate(self, component_name: Union[str, List[str]] = None): sch.step() def soft_update(self, other_model: nn.Module, tau: float): + """Soft-update model parameters using another model. + + The update formulae is: param = (1 - tau) * param + tau * pther_param. + + Args: + other_model: The model to update the current model with. + tau (float): Soft-update coefficient. + """ for params, other_params in zip(self.parameters(), other_model.parameters()): params.data = (1 - tau) * params.data + tau * other_params.data def copy(self, with_optimizer: bool = False, device: str = None): + """Return a deep copy of the instance; + + Args: + with_opimizer (bool): If True, the registered optimizers will also be deep copied. + Defaults to False. + device (str): The device the copied instance should be placed on. Defaults to None, + in which case the copied instance will be placed on the same device as the instance itself. + """ model_copy = clone(self) if not with_optimizer: model_copy.optimizer = None @@ -136,22 +158,23 @@ def copy(self, with_optimizer: bool = False, device: str = None): class DiscreteQNet(AbsCoreModel): - """NN-based Q-value model.""" + """Q-value model for finite and discrete action spaces.""" @abstractmethod def forward(self, states) -> torch.tensor: - """Output Q values""" + """Compute the Q-values for all actions as a tensor of shape (batch_size, action_space_size).""" raise NotImplementedError def get_action(self, states): """ - Given Q-values for a batch of states and all actions, return the maximum Q-value and - the corresponding action index for each state. + Given Q-values for a batch of states and all actions, return the action index and the corresponding + Q-values for each state. """ q_for_all_actions = self.forward(states) # (batch_size, num_actions) greedy_q, actions = q_for_all_actions.max(dim=1) return actions.detach(), greedy_q.detach() def q_values(self, states, actions: torch.tensor): + """Return the Q-values for a batch of states and actions.""" if len(actions.shape) == 1: actions = actions.unsqueeze(dim=1) q_for_all_actions = self.forward(states) # (batch_size, num_actions) @@ -159,23 +182,22 @@ def q_values(self, states, actions: torch.tensor): class DiscretePolicyNet(AbsCoreModel): - """A compound network structure that consists of multiple task heads and an optional shared stack. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - All components must have the same input dimension except the one designated as the shared - component by ``shared_component_name``. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for - the components. Defaults to None. - """ + """Parameterized policy for finite and discrete action spaces.""" @abstractmethod def forward(self, states) -> torch.tensor: + """Compute action probabilities corresponding to each state in ``states``. + + The output must be a torch tensor with shape (batch_size, action_space_size). + + Args: + states: State batch to compute action probabilities for. + """ raise NotImplementedError def get_action(self, states): """ - Given Q-values for a batch of states and all actions, return the maximum Q-value and - the corresponding action index for each state. + Given a batch of states, return actions selected based on the probabilities computed by ``forward`` + and the corresponding log probabilities. """ action_prob = Categorical(self.forward(states)) # (batch_size, num_actions) action = action_prob.sample() @@ -184,17 +206,20 @@ def get_action(self, states): class DiscreteACNet(AbsCoreModel): - """A compound network structure that consists of multiple task heads and an optional shared stack. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - All components must have the same input dimension except the one designated as the shared - component by ``shared_component_name``. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for - the components. Defaults to None. - """ + """Model container for the actor-critic architecture for finite and discrete action spaces.""" @abstractmethod - def forward(self, states, output_action_probs: bool = True, output_values: bool = True): + def forward(self, states, actor: bool = True, critic: bool = True) -> tuple: + """Compute action probabilities and values for a state batch. + + The output is a tuple of (action_probs, values), where action probs is a tensor of shape + (batch_size, action_space_size) and values is a tensor of shape (batch_size,). If only one + of these two is needed, the return value for the other one can be set to None. + + Args: + states: State batch to compute action probabilities and values for. + actor (bool): If True, the first element of the output will be actin probabilities. Defaults to True. + critic (bool): If True, the second element of the output will be state values. Defaults to True. + """ raise NotImplementedError def get_action(self, states): @@ -202,28 +227,28 @@ def get_action(self, states): Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value for each state. """ - action_prob = Categorical(self.forward(states, output_values=False)) # (batch_size, num_actions) + action_prob = Categorical(self.forward(states, critic=False)[0]) # (batch_size, action_space_size) action = action_prob.sample() log_p = action_prob.log_prob(action) return action, log_p class ContinuousACNet(AbsCoreModel): - """A compound network structure that consists of multiple task heads and an optional shared stack. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - All components must have the same input dimension except the one designated as the shared - component by ``shared_component_name``. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer option for the components. - Defaults to None. - """ + """Model container for the actor-critic architecture for continuous action spaces.""" @abstractmethod def forward(self, states, actions=None): + """Compute actions for a batch of states or Q-values for a batch of states and actions. + + Args: + states: State batch to compute the Q-values for. + actions: Action batch. If None, the output should be a batch of actions corresponding to + the state batch. Otherwise, the output should be the Q-values for the given states and + actions. Defaults to None. + """ raise NotImplementedError def get_action(self, states): - """Compute the actions given a batch of states.""" + """Compute actions given a batch of states.""" return self.forward(states) def value(self, states): diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 66a20d089..dbc1befc3 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -44,8 +44,6 @@ def __init__( self.num_episodes = num_episodes - # self._init_update_schedule(policy_update_schedule) - # evaluation schedule if eval_schedule is None: eval_schedule = [] diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index 03f7ebee2..e2e7d211a 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -131,6 +131,7 @@ def on_experiences(self, exp_by_agent: Dict[str, ExperienceSet]): self._logger.info(f"Updated policies {self._updated_policy_ids}") def get_state(self): + """Return the states of updated policies since the last call.""" policy_state_dict = { policy_id: self.policy_dict[policy_id].get_state() for policy_id in self._updated_policy_ids } diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 38114fb26..2b80f481a 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -82,7 +82,7 @@ def __init__( self.env = env self.eval_env = eval_env if eval_env else self.env - + # mappings between agents and policies self.policy_dict = policy_dict self._agent2policy = agent2policy @@ -109,7 +109,7 @@ def __init__( self._eval_ep = 0 def collect(self, ep: int, segment: int, policy_state_dict: dict): - """Collect simulation data, i.e., experiences for training.""" + """Collect simulation data for training.""" t0 = time.time() learning_time = 0 num_experiences_collected = 0 From 42e6ff83413958e7448cae7004de63978140a915 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 21 May 2021 15:29:43 +0000 Subject: [PATCH 262/482] fixed bugs & updated CIM notebook --- docs/source/key_components/rl_toolkit.rst | 4 +- examples/cim/ac/config.yml | 2 +- examples/cim/env_wrapper.py | 18 +- maro/rl/algorithm/ac.py | 25 +- maro/rl/algorithm/ddpg.py | 12 +- maro/rl/algorithm/dqn.py | 12 +- maro/rl/algorithm/pg.py | 12 +- maro/rl/env_wrapper/__init__.py | 3 +- maro/rl/env_wrapper/env_wrapper.py | 73 ++-- maro/rl/env_wrapper/replay_buffer.py | 139 ------- maro/rl/experience/experience_memory.py | 4 +- maro/rl/policy/policy.py | 10 +- maro/rl/training/actor.py | 6 +- maro/rl/training/policy_manager.py | 60 ++- maro/rl/training/rollout_manager.py | 16 +- .../rl_formulation.ipynb | 359 +++++++++--------- 16 files changed, 322 insertions(+), 433 deletions(-) delete mode 100644 maro/rl/env_wrapper/replay_buffer.py diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 5832ed769..520dab9c5 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -32,9 +32,9 @@ data (in the form of "experiences sets") based on which updates can be made. class AbsCorePolicy(AbsPolicy): - def __init__(self, experience_manager: AbsExperienceManager): + def __init__(self, experience_memory: ExperienceMemory): super().__init__() - self.experience_manager = experience_manager + self.experience_memory = experience_memory @abstractmethod def update(self): diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index e1f1f505f..7d3d57e88 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -67,7 +67,7 @@ policy: num_steps: 10 actor_loss_coefficient: 0.1 # clip_ratio: 0.8 # for PPO - experience_manager: + experience_memory: capacity: 100000 overwrite_type: "rolling" update_trigger: diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index de5f27765..ee825130b 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import defaultdict - import numpy as np from maro.rl import AbsEnvWrapper @@ -26,18 +24,22 @@ def __init__( self.time_decay = time_decay self.finite_vessel_space = finite_vessel_space self.has_early_discharge = has_early_discharge + self._last_action_tick = None - def get_state(self, event): + def get_state(self, tick=None): + if tick is None: + tick = self.env.tick vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] - tick, port_idx, vessel_idx = event.tick, event.port_idx, event.vessel_idx + port_idx, vessel_idx = self.event.port_idx, self.event.vessel_idx ticks = [max(0, tick - rt) for rt in range(self.look_back - 1)] future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] self.state_info = { - "tick": tick, "action_scope": event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx + "tick": tick, "action_scope": self.event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx } state = np.concatenate((port_features, vessel_features)) + self._last_action_tick = tick return {port_idx: state} def to_env_action(self, action_by_agent): @@ -63,10 +65,10 @@ def to_env_action(self, action_by_agent): return {port: Action(vessel, port, actual_action, action_type)} - def get_reward(self, tick=None, target_agents=None): + def get_reward(self, tick=None): """Delayed reward evaluation.""" if tick is None: - tick = self.env.tick + tick = self._last_action_tick port_snapshots = self.env.snapshot_list["ports"] start_tick = tick + 1 ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) @@ -79,7 +81,7 @@ def get_reward(self, tick=None, target_agents=None): ] return { - target_agents[0]: + self.action_history[tick]: np.float32( self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - self.shortage_factor * np.dot(future_shortage, decay_list) diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index bf74f5111..d1e205fbc 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience import AbsExperienceManager +from maro.rl.experience import ExperienceMemory from maro.rl.model import DiscreteACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -57,15 +57,15 @@ class ActorCritic(AbsCorePolicy): Args: ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. - experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces - for storing and retrieving experiences for training. + experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + for training. config: Configuration for the AC algorithm. """ - def __init__(self, ac_net: DiscreteACNet, experience_manager: AbsExperienceManager, config: ActorCriticConfig): + def __init__(self, ac_net: DiscreteACNet, experience_memory: ExperienceMemory, config: ActorCriticConfig): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") - super().__init__(experience_manager) + super().__init__(experience_memory) self.ac_net = ac_net self.config = config self.device = self.ac_net.device @@ -75,25 +75,26 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: with torch.no_grad(): actions, log_p = self.ac_net.get_action(states) actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() - return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p + return (actions[0], log_p[0]) if len(actions) == 1 else (actions, log_p) def update(self): self.ac_net.train() for _ in range(self.config.train_epochs): - experience_set = self.experience_manager.get() + experience_set = self.experience_memory.get() states, next_states = experience_set.states, experience_set.next_states actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.device) log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) for _ in range(self.config.gradient_iters): - state_values = self.ac_net(states, actor=False)[1].detach().squeeze() - next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() + action_probs, state_values = self.ac_net(states) + state_values = state_values.squeeze() + with torch.no_grad(): + next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() return_est = rewards + self.config.reward_discount * next_state_values advantages = return_est - state_values # actor loss - action_probs, state_values = self.ac_net(states) log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) if self.config.clip_ratio is not None: ratio = torch.exp(log_p_new - log_p) @@ -108,8 +109,10 @@ def update(self): self.ac_net.step(loss) + self.experience_memory.clear() + def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) def get_state(self): - return self.q_net.state_dict() + return self.ac_net.state_dict() diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index 087c35b2a..7190943e4 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -7,7 +7,7 @@ import numpy as np import torch -from maro.rl.experience import AbsExperienceManager +from maro.rl.experience import ExperienceMemory from maro.rl.model import ContinuousACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -62,15 +62,15 @@ class DDPG(AbsCorePolicy): Args: ac_net (ContinuousACNet): DDPG policy and q-value models. - experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces - for storing and retrieving experiences for training. + experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + for training. config (DDPGConfig): Configuration for DDPG algorithm. """ - def __init__(self, ac_net: ContinuousACNet, experience_manager: AbsExperienceManager, config: DDPGConfig): + def __init__(self, ac_net: ContinuousACNet, experience_memory: ExperienceMemory, config: DDPGConfig): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") - super().__init__(experience_manager) + super().__init__(experience_memory) self.ac_net = ac_net if self.ac_net.trainable: self.target_ac_net = ac_net.copy() @@ -90,7 +90,7 @@ def choose_action(self, states) -> Union[float, np.ndarray]: def update(self): self.ac_net.train() for _ in range(self.config.train_epochs): - experience_set = self.experience_manager.get() + experience_set = self.experience_memory.get() states, next_states = experience_set.states, experience_set.next_states actual_actions = torch.from_numpy(experience_set.actions).to(self.device) rewards = torch.from_numpy(experience_set.rewards).to(self.device) diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index b10cb4bed..5abb646db 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience import AbsExperienceManager +from maro.rl.experience import ExperienceMemory from maro.rl.model import DiscreteQNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -60,15 +60,15 @@ class DQN(AbsCorePolicy): Args: q_net (DiscreteQNet): Q-value model. - experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces - for storing and retrieving experiences for training. + experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + for training. config (DQNConfig): Configuration for DQN algorithm. """ - def __init__(self, q_net: DiscreteQNet, experience_manager: AbsExperienceManager, config: DQNConfig): + def __init__(self, q_net: DiscreteQNet, experience_memory: ExperienceMemory, config: DQNConfig): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(experience_manager) + super().__init__(experience_memory) self.q_net = q_net if self.q_net.trainable: self.target_q_net = q_net.copy() @@ -92,7 +92,7 @@ def update(self): self.q_net.train() for _ in range(self.config.train_epochs): # sample from the replay memory - experience_set = self.experience_manager.get() + experience_set = self.experience_memory.get() states, next_states = experience_set.states, experience_set.next_states actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index 8eb735f1d..ec81c6dfa 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.experience.experience_manager import AbsExperienceManager +from maro.rl.experience.experience_memory import ExperienceMemory from typing import Tuple import numpy as np @@ -32,16 +32,16 @@ class PolicyGradient(AbsCorePolicy): Args: policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. - experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces - for storing and retrieving experiences for training. + experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + for training. config (PolicyGradientConfig): Configuration for the PG algorithm. """ def __init__( - self, policy_net: DiscretePolicyNet, experience_manager: AbsExperienceManager, config: PolicyGradientConfig, + self, policy_net: DiscretePolicyNet, experience_memory: ExperienceMemory, config: PolicyGradientConfig, ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__(experience_manager) + super().__init__(experience_memory) self.policy_net = policy_net self.config = config self.device = self.policy_net.device @@ -60,7 +60,7 @@ def update(self): which they are generated during the simulation. Otherwise, the return values may be meaningless. """ self.policy_net.train() - experience_set = self.experience_manager.get() + experience_set = self.experience_memory.get() log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) diff --git a/maro/rl/env_wrapper/__init__.py b/maro/rl/env_wrapper/__init__.py index 4b3e8fd0a..13de38a26 100644 --- a/maro/rl/env_wrapper/__init__.py +++ b/maro/rl/env_wrapper/__init__.py @@ -2,6 +2,5 @@ # Licensed under the MIT license. from .env_wrapper import AbsEnvWrapper -from .replay_buffer import AbsReplayBuffer, FIFOReplayBuffer, FixedSizeReplayBuffer -__all__ = ["AbsEnvWrapper", "AbsReplayBuffer", "FIFOReplayBuffer", "FixedSizeReplayBuffer"] +__all__ = ["AbsEnvWrapper"] diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index fe7551f0c..2533f376e 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -2,11 +2,11 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from collections import deque +from collections import defaultdict, deque from typing import Dict from maro.simulator import Env -from maro.rl.experience import AbsExperienceManager, ExperienceSet +from maro.rl.experience import ExperienceSet class AbsEnvWrapper(ABC): @@ -14,23 +14,22 @@ class AbsEnvWrapper(ABC): Args: env (Env): Environment instance. - replay_buffer (dict): Replay buffers for recording transitions experienced by agents in sequential fashion. - Transitions will only be recorded for those agents whose IDs appear in the keys of the dictionary. - Defaults to None, in which case no transition will be recorded. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. + save_replay (bool): If True, transitions for some or all agents will in stored in internal replay buffers. + replay_agent_ids (list): List of agent IDs whose transitions will be stored in internal replay buffers. + If ``save_replay`` is False, this is ignored. Otherwise, if it is None, it will be set to all agents in + Defaults to None. """ - def __init__( - self, - env: Env, - replay_buffer: Dict[str, AbsExperienceManager] = None, - reward_eval_delay: int = 0 - ): + def __init__(self, env: Env, reward_eval_delay: int = 0, save_replay: bool = True, replay_agent_ids: list = None): self.env = env - self.replay = replay_buffer self.state_info = None # context for converting model output to actions that can be executed by the env self.reward_eval_delay = reward_eval_delay + self.action_history = defaultdict(dict) + self.save_replay = save_replay + self.replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids + self._replay_buffer = {agent_id: defaultdict(list) for agent_id in self.replay_agent_ids} self._pending_reward_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None self._total_reward = 0 @@ -41,7 +40,7 @@ def __init__( def step_index(self): """Number of environmental steps taken so far.""" return self._step_index - + @property def agent_idx_list(self): return self.env.agent_idx_list @@ -111,18 +110,14 @@ def step(self, action_by_agent: dict): """ self._step_index += 1 env_action = self.to_env_action(action_by_agent) - if len(env_action) == 1: - env_action = list(env_action.values())[0] - pre_action_tick = self.env.tick + for agent_id, action in action_by_agent.items(): + self.action_history[self.env.tick][agent_id] = action + transition_info = self.get_transition_info() + self._pending_reward_cache.append((self._state, action_by_agent, transition_info, self.env.tick)) _, self._event, done = self.env.step(env_action) if not done: - prev_state = self._state # previous env state - transition_info = self.get_transition_info() self._state = self.get_state(self.env.tick) # current env state - self._pending_reward_cache.append( - (prev_state, action_by_agent, self._state, transition_info, pre_action_tick) - ) else: self._state = None self.end_of_episode() @@ -135,14 +130,18 @@ def step(self, action_by_agent: dict): self._pending_reward_cache and (done or self.env.tick - self._pending_reward_cache[0][-1] >= self.reward_eval_delay) ): - state, action, state_, info, tick = self._pending_reward_cache.popleft() + state, action, info, tick = self._pending_reward_cache.popleft() reward = self.get_reward(tick=tick) # assign rewards to the agents that took action at that tick - for agent_id, act in action.items(): - st, st_, rw = state[agent_id], state_[agent_id], reward.get(agent_id, .0) - if not done and self.replay and agent_id in self.replay: - self.replay[agent_id].put(ExperienceSet([st], [act], [rw], [st_], [info])) - self._total_reward += rw + if self.save_replay: + for agent_id, st in state.items(): + self._total_reward += reward[agent_id] + if agent_id in self._replay_buffer: + buf = self._replay_buffer[agent_id] + buf["states"].append(st) + buf["actions"].append(action[agent_id]) + buf["rewards"].append(reward[agent_id]) + buf["info"].append(info[agent_id] if info else None) def end_of_episode(self): """Custom processing logic at the end of an episode.""" @@ -150,7 +149,23 @@ def end_of_episode(self): def get_experiences(self): """Get per-agent experiences from the replay buffer.""" - return {agent_id: replay.batch() for agent_id, replay in self.replay.items()} + exp_by_agent = {} + for agent_id in self.replay_agent_ids: + buf = self._replay_buffer[agent_id] + exp_set = ExperienceSet( + buf["states"][:-1], + buf["actions"][:-1], + buf["rewards"][:-1], + buf["states"][1:], + buf["info"][:-1], + ) + del buf["states"][:-1] + del buf["actions"][:-1] + del buf["rewards"][:-1] + del buf["info"][:-1] + exp_by_agent[agent_id] = exp_set + + return exp_by_agent def reset(self): self.env.reset() @@ -158,5 +173,5 @@ def reset(self): self._total_reward = 0 self._state = None self._pending_reward_cache.clear() - for replay in self.replay.values(): + for replay in self._replay_buffer.values(): replay.clear() diff --git a/maro/rl/env_wrapper/replay_buffer.py b/maro/rl/env_wrapper/replay_buffer.py deleted file mode 100644 index 44741bd04..000000000 --- a/maro/rl/env_wrapper/replay_buffer.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - -import numpy as np - -from maro.rl.experience import ExperienceSet - - -class AbsReplayBuffer(ABC): - """Replay buffer to be used in an EnvWrapper for caching transitions and generating experience sets. - """ - def __init__(self): - super().__init__() - - @abstractmethod - def push(self, state, action, reward): - """Add a new transition to the buffer.""" - raise NotImplementedError - - @abstractmethod - def batch(self) -> ExperienceSet: - """Generate an ExperienceSet from the buffer.""" - raise NotImplementedError - - @abstractmethod - def clear(self): - """Empty the buffer.""" - raise NotImplementedError - - -class FIFOReplayBuffer(AbsReplayBuffer): - """A FIFO-based replay buffer that empties itself after an experience set is generated.""" - def __init__(self): - super().__init__() - self.states = [] - self.actions = [] - self.rewards = [] - self.next_states = [] - - def push(self, state, action, reward): - """Add a new transition to buffer. - - If this is not the first transition, the state will correspond to the "next_state" for the transition - that came before it. - """ - if self.states: - self.next_states.append(state) - - self.states.append(state) - self.actions.append(action) - self.rewards.append(reward) - - def batch(self) -> ExperienceSet: - """Convert all "SARS" transitions to experience sets and subsequently empty the buffer. - - After this operation, "states", "actions" and "rewards" will have one element remaining that corresponds - to the last transition for which the next state has yet to be determined. The "next_states" will be empty. - """ - exp_set = ExperienceSet( - states=self.states[:-1], - actions=self.actions[:-1], - rewards=self.rewards[:-1], - next_states=self.next_states - ) - - del self.states[:-1] - del self.actions[:-1] - del self.rewards[:-1] - self.next_states.clear() - - return exp_set - - def clear(self): - """Empty the buffer.""" - self.states.clear() - self.actions.clear() - self.rewards.clear() - self.next_states.clear() - - -class FixedSizeReplayBuffer(AbsReplayBuffer): - """An implementation of the replay buffer that maintains a fixed-size buffer. - - Args: - capacity (int): Capacity of the buffer. Once the the buffer size has reached capacity, newly added - transitions will replace existing entries starting from the oldest. - batch_size (int): Size of experience sets generated from the buffer. - """ - def __init__(self, capacity: int, batch_size: int): - super().__init__() - if batch_size <= 0: - raise ValueError("batch_size must be a positive integer since batch_mode is set to 'random'") - if batch_size > capacity: - raise ValueError(f"batch_size cannot exceed the buffer capacity ({capacity})") - - self.capacity = capacity - self.batch_size = batch_size - self.states = np.empty(capacity, dtype=object) - self.actions = np.empty(capacity, dtype=object) - self.rewards = np.empty(capacity, dtype=object) - self.next_states = np.empty(capacity, dtype=object) - self._size = 0 - self._index = 0 - - def push(self, state, action, reward): - """Add a new transition to buffer. - - If this is not the first transition, the state will correspond to the "next_state" for the transition - that came before it. - """ - if self.states[self._index - 1]: - self.next_states[self._index - 1] = state - self._size = min(self._size + 1, self.capacity) - - self.states[self._index] = state - self.actions[self._index] = action - self.rewards[self._index] = reward - self._index = (self._index + 1) % self.capacity - - def batch(self) -> ExperienceSet: - """Generate an ExperienceSet from a random sample of transitions in the buffer.""" - indexes = np.random.choice(self._size, size=self.batch_size) - return ExperienceSet( - states=list(self.states[indexes]), - actions=list(self.actions[indexes]), - rewards=list(self.rewards[indexes]), - next_states=list(self.next_states[indexes]) - ) - - def clear(self): - """Empty the buffer.""" - self.states = np.empty(self.capacity, dtype=object) - self.actions = np.empty(self.capacity, dtype=object) - self.rewards = np.empty(self.capacity, dtype=object) - self.next_states = np.empty(self.capacity, dtype=object) - self._size = 0 - self._index = 0 diff --git a/maro/rl/experience/experience_memory.py b/maro/rl/experience/experience_memory.py index bf5c9c9d4..d0b00f309 100644 --- a/maro/rl/experience/experience_memory.py +++ b/maro/rl/experience/experience_memory.py @@ -129,9 +129,7 @@ def get(self): returned according to the sampling logic defined by the registered sampler. """ if self.sampler is None: - ret = ExperienceSet(*[self.data[key] for key in self._keys]) - self.clear() - return ret + return ExperienceSet(*[self.data[key][:self._size] for key in self._keys]) else: return self.sampler.get() diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 286e3281a..a60a5e60e 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod -from maro.rl.experience import AbsExperienceManager +from maro.rl.experience import ExperienceMemory class AbsPolicy(ABC): @@ -31,12 +31,12 @@ class AbsCorePolicy(AbsPolicy): Reinforcement learning (RL) policies should inherit from this. Args: - experience_manager (AbsExperienceManager): An experience manager with put() and get() interfaces - for storing and retrieving experiences for training. + experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + for training. """ - def __init__(self, experience_manager: AbsExperienceManager): + def __init__(self, experience_memory: ExperienceMemory): super().__init__() - self.experience_manager = experience_manager + self.experience_memory = experience_memory @abstractmethod def choose_action(self, state): diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 0c25ca8fb..e5c7189b9 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -131,11 +131,15 @@ def run(self): f"Roll-out finished for ep {episode_index}, segment {segment_index}" f"(steps {starting_step_index} - {self.env.step_index})" ) + exp_by_agent = self.env.get_experiences() + for agent_id, exp_set in exp_by_agent.items(): + self.policy[agent_id].experience_memory.put(exp_set) + return_info = { MsgKey.EPISODE_END: not self.env.state, MsgKey.EPISODE_INDEX: episode_index, MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.EXPERIENCES: self.env.get_experiences(), + MsgKey.EXPERIENCES: exp_by_agent, MsgKey.NUM_STEPS: self.env.step_index - starting_step_index + 1 } diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index e2e7d211a..27f244a71 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -19,28 +19,19 @@ class AbsPolicyManager(ABC): """Controller for policy updates. The actual policy instances may reside here or be distributed on a set of remote nodes. - - Args: - agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct - incoming experiences (which are bucketed by agent ID) to the correct policy's experience manager. """ - def __init__(self, agent2policy: Dict[str, str]): - self.agent2policy = agent2policy - self._policy_names = list(agent2policy.values()) + def __init__(self): + pass @property - def names(self): - """Return the list of policy names""" - return self._policy_names - @abstractmethod - def on_experiences(self, exp_by_agent: Dict[str, ExperienceSet]): - """Logic for handling incoming experiences is implemented here.""" + def names(self): + """Return the list of policy names.""" raise NotImplementedError @abstractmethod - def choose_action(self, state_by_agent): - """Choose an action based on the state.""" + def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + """Logic for handling incoming experiences is implemented here.""" raise NotImplementedError @abstractmethod @@ -54,12 +45,10 @@ class LocalPolicyManager(AbsPolicyManager): Args: policy_dict (Dict[str, AbsPolicy]): A set of named policies. - agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct - incoming experiences (which are bucketed by agent ID) to the correct policy's experience manager. update_trigger (Union[PolicyUpdateTrigger, dict]): Conditions for triggering policy updates. If a dictionary is provided, the triggers will be applied to the policies by name. If a single ``PolicyUpdateTrigger`` is provided, the trigger will be applied to all updatable policies, i.e., - those that have the ``experience_manager`` attribute and the ``update`` interface. Defaults to + those that have the ``experience_memory`` attribute and the ``update`` interface. Defaults to None, in which case a default updatable trigger will be applied to every updatable policy, meaning that these policies will be updated as long as new experiences are available. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time @@ -69,24 +58,20 @@ class LocalPolicyManager(AbsPolicyManager): def __init__( self, policy_dict: Dict[str, AbsPolicy], - agent2policy: Dict[str, str], update_trigger: Union[PolicyUpdateTrigger, dict] = None, - log_dir: str = getcwd() + log_dir: str = getcwd() ): if isinstance(update_trigger, dict) and not update_trigger.keys() <= policy_dict.keys(): raise ValueError(f"The keys for update_trigger must be a subset of {list(policy_dict.keys())}") - super().__init__(agent2policy) + super().__init__() + self._names = list(policy_dict.keys()) self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) self.policy_dict = policy_dict - self._policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} - self._agent_groups_by_policy = defaultdict(list) - for agent_id, policy_id in self.agent2policy.items(): - self._agent_groups_by_policy[policy_id].append(agent_id) self._updatable_policy_dict = { policy_id: policy for policy_id, policy in self.policy_dict.items() - if hasattr(policy, "experience_manager") and hasattr(policy, "update") + if hasattr(policy, "experience_memory") and hasattr(policy, "update") } if update_trigger is None: default_trigger = PolicyUpdateTrigger(min_new_experiences=1, num_warmup_experiences=1) @@ -99,28 +84,27 @@ def __init__( self._new_exp_counter = defaultdict(int) self._updated_policy_ids = set() - def choose_action(self, state_by_agent: dict): - return {agent_id: self._policy[agent_id].choose_action(state) for agent_id, state in state_by_agent.items()} + @property + def names(self): + return self._names - def on_experiences(self, exp_by_agent: Dict[str, ExperienceSet]): + def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): """Store experiences and update policies if possible. - The incoming experiences are expected to be grouped by agent ID and will be stored in the corresponding - policy's experience manager by looking up the agent-to-policy mapping. Policies whose update conditions - have been met will then be updated. + The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding + policy's experience manager. Policies whose update conditions have been met will then be updated. """ - for agent_id, exp in exp_by_agent.items(): - policy_id = self.agent2policy[agent_id] + for policy_id, exp in exp_by_policy.items(): policy = self.policy_dict[policy_id] - if hasattr(policy, "experience_manager"): + if hasattr(policy, "experience_memory"): self._new_exp_counter[policy_id] += exp.size - policy.experience_manager.put(exp) + policy.experience_memory.put(exp) for policy_id, policy in self._updatable_policy_dict.items(): - print(f"Policy {policy_id}: exp mem size = {policy.experience_manager.size}, new exp = {self._new_exp_counter[policy_id]}") + print(f"Policy {policy_id}: exp mem size = {policy.experience_memory.size}, new exp = {self._new_exp_counter[policy_id]}") if ( policy_id not in self._update_trigger or - policy.experience_manager.size >= self._update_trigger[policy_id].num_warmup_experiences and + policy.experience_memory.size >= self._update_trigger[policy_id].num_warmup_experiences and self._new_exp_counter[policy_id] >= self._update_trigger[policy_id].min_new_experiences ): policy.update() diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 2b80f481a..3811b5fe7 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -123,7 +123,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): if self.env.state is None: self._logger.info(f"Collecting data from episode {ep}, segment {segment}") - if hasattr(self, "exploration_dict"): + if self.exploration_dict: exploration_params = { tuple(agent_ids): self.exploration_dict[exploration_id].parameters for exploration_id, agent_ids in self._agent_groups_by_exploration.items() @@ -158,9 +158,11 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): ) # update the exploration parameters if an episode is finished - if not self.env.state and self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() + if not self.env.state: + self.episode_complete = True + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() # performance details if not self.env.state: @@ -171,9 +173,9 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): self._logger.debug( f"ep {ep} summary - " - f"running time: {time.time() - t0}" - f"env steps: {self.env.step_index}" - f"learning time: {learning_time}" + f"running time: {time.time() - t0} " + f"env steps: {self.env.step_index} " + f"learning time: {learning_time} " f"experiences collected: {num_experiences_collected}" ) diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index b2e541038..f97bbe5ce 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -21,11 +21,11 @@ "common_config = {\n", " \"port_attributes\": [\"empty\", \"full\", \"on_shipper\", \"on_consignee\", \"booking\", \"shortage\", \"fulfillment\"],\n", " \"vessel_attributes\": [\"empty\", \"full\", \"remaining_space\"],\n", - " \"action_space\": list(np.linspace(-1.0, 1.0, 21)),\n", " # Parameters for computing states\n", " \"look_back\": 7,\n", " \"max_ports_downstream\": 2,\n", " # Parameters for computing actions\n", + " \"num_actions\": 21,\n", " \"finite_vessel_space\": True,\n", " \"has_early_discharge\": True,\n", " # Parameters for computing rewards\n", @@ -49,65 +49,75 @@ "metadata": {}, "outputs": [], "source": [ - "from collections import defaultdict\n", "import numpy as np\n", - "from maro.rl import Trajectory\n", + "from maro.rl import AbsEnvWrapper\n", "from maro.simulator.scenarios.cim.common import Action, ActionType\n", "\n", "\n", - "class CIMTrajectory(Trajectory):\n", + "class CIMEnvWrapper(AbsEnvWrapper):\n", " def __init__(\n", - " self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream,\n", - " reward_eval_delay, fulfillment_factor, shortage_factor, time_decay,\n", + " self, env, save_replay=True, replay_agent_ids=None, *, port_attributes, vessel_attributes, num_actions,\n", + " look_back,max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay,\n", " finite_vessel_space=True, has_early_discharge=True \n", " ):\n", - " super().__init__(env)\n", + " super().__init__(env, save_replay=save_replay, replay_agent_ids=replay_agent_ids, reward_eval_delay=reward_eval_delay)\n", " self.port_attributes = port_attributes\n", " self.vessel_attributes = vessel_attributes\n", - " self.action_space = action_space\n", + " self.action_space = list(np.linspace(-1.0, 1.0, num_actions))\n", " self.look_back = look_back\n", " self.max_ports_downstream = max_ports_downstream\n", - " self.reward_eval_delay = reward_eval_delay\n", " self.fulfillment_factor = fulfillment_factor\n", " self.shortage_factor = shortage_factor\n", " self.time_decay = time_decay\n", " self.finite_vessel_space = finite_vessel_space\n", " self.has_early_discharge = has_early_discharge\n", + " self._last_action_tick = None\n", "\n", - " def get_state(self, event):\n", + " def get_state(self, tick=None):\n", + " if tick is None:\n", + " tick = self.env.tick\n", " vessel_snapshots, port_snapshots = self.env.snapshot_list[\"vessels\"], self.env.snapshot_list[\"ports\"]\n", - " tick, port_idx, vessel_idx = event.tick, event.port_idx, event.vessel_idx\n", + " port_idx, vessel_idx = self.event.port_idx, self.event.vessel_idx\n", " ticks = [max(0, tick - rt) for rt in range(self.look_back - 1)]\n", " future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int')\n", " port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes]\n", " vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes]\n", - " return {port_idx: np.concatenate((port_features, vessel_features))}\n", + " self.state_info = {\n", + " \"tick\": tick, \"action_scope\": self.event.action_scope, \"port_idx\": port_idx, \"vessel_idx\": vessel_idx\n", + " }\n", + " state = np.concatenate((port_features, vessel_features))\n", + " self._last_action_tick = tick\n", + " return {port_idx: state}\n", "\n", - " def get_action(self, action_by_agent, event):\n", + " def to_env_action(self, action_by_agent):\n", " vessel_snapshots = self.env.snapshot_list[\"vessels\"]\n", " action_info = list(action_by_agent.values())[0]\n", " model_action = action_info[0] if isinstance(action_info, tuple) else action_info\n", - " scope, tick, port, vessel = event.action_scope, event.tick, event.port_idx, event.vessel_idx\n", + " tick, port, vessel = self.state_info[\"tick\"], self.state_info[\"port_idx\"], self.state_info[\"vessel_idx\"]\n", " zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero.\n", " vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float(\"inf\")\n", " early_discharge = vessel_snapshots[tick:vessel:\"early_discharge\"][0] if self.has_early_discharge else 0\n", " percent = abs(self.action_space[model_action])\n", "\n", + " action_scope = self.state_info[\"action_scope\"]\n", " if model_action < zero_action_idx:\n", " action_type = ActionType.LOAD\n", - " actual_action = min(round(percent * scope.load), vessel_space)\n", + " actual_action = min(round(percent * action_scope.load), vessel_space)\n", " elif model_action > zero_action_idx:\n", " action_type = ActionType.DISCHARGE\n", - " plan_action = percent * (scope.discharge + early_discharge) - early_discharge\n", - " actual_action = round(plan_action) if plan_action > 0 else round(percent * scope.discharge)\n", + " plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge\n", + " actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge)\n", " else:\n", " actual_action, action_type = 0, None\n", "\n", - " return {port: Action(vessel, port, actual_action, action_type)}\n", + " return Action(vessel, port, actual_action, action_type)\n", "\n", - " def get_offline_reward(self, event):\n", + " def get_reward(self, tick=None):\n", + " \"\"\"Delayed reward evaluation.\"\"\"\n", + " if tick is None:\n", + " tick = self._last_action_tick\n", " port_snapshots = self.env.snapshot_list[\"ports\"]\n", - " start_tick = event.tick + 1\n", + " start_tick = tick + 1\n", " ticks = list(range(start_tick, start_tick + self.reward_eval_delay))\n", "\n", " future_fulfillment = port_snapshots[ticks::\"fulfillment\"]\n", @@ -117,40 +127,20 @@ " for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay)\n", " ]\n", "\n", - " tot_fulfillment = np.dot(future_fulfillment, decay_list)\n", - " tot_shortage = np.dot(future_shortage, decay_list)\n", - "\n", - " return np.float32(self.fulfillment_factor * tot_fulfillment - self.shortage_factor * tot_shortage)\n", - "\n", - " def on_env_feedback(self, event, state_by_agent, action_by_agent, reward):\n", - " self.trajectory[\"event\"].append(event)\n", - " self.trajectory[\"state\"].append(state_by_agent)\n", - " self.trajectory[\"action\"].append(action_by_agent)\n", - " \n", - " def on_finish(self):\n", - " training_data = {}\n", - " for event, state, action in zip(self.trajectory[\"event\"], self.trajectory[\"state\"], self.trajectory[\"action\"]):\n", - " agent_id = list(state.keys())[0]\n", - " data = training_data.setdefault(agent_id, {\"args\": [[] for _ in range(4)]})\n", - " data[\"args\"][0].append(state[agent_id]) # state\n", - " data[\"args\"][1].append(action[agent_id][0]) # action\n", - " data[\"args\"][2].append(action[agent_id][1]) # log_p\n", - " data[\"args\"][3].append(self.get_offline_reward(event)) # reward\n", - "\n", - " for agent_id in training_data:\n", - " training_data[agent_id][\"args\"] = [\n", - " np.asarray(vals, dtype=np.float32 if i == 3 else None)\n", - " for i, vals in enumerate(training_data[agent_id][\"args\"])\n", - " ]\n", - "\n", - " return training_data" + " return {\n", + " agent_id: np.float32(\n", + " self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - \n", + " self.shortage_factor * np.dot(future_shortage, decay_list)\n", + " )\n", + " for agent_id in self.action_history[tick]\n", + " }" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## [Agent](https://maro.readthedocs.io/en/latest/key_components/rl_toolkit.html#agent)\n", + "## [Policy](https://maro.readthedocs.io/en/latest/key_components/rl_toolkit.html#agent)\n", "\n", "The out-of-the-box ActorCritic is used as our agent." ] @@ -161,10 +151,9 @@ "metadata": {}, "outputs": [], "source": [ - "import torch.nn as nn\n", - "from torch.optim import Adam, RMSprop\n", + "import torch\n", "\n", - "from maro.rl import ActorCritic, ActorCriticConfig, FullyConnectedBlock, OptimOption, SimpleMultiHeadModel\n", + "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceMemory, FullyConnectedBlock, OptimOption\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", @@ -175,48 +164,65 @@ " len(common_config[\"vessel_attributes\"])\n", ")\n", "\n", - "agent_config = {\n", - " \"model\": {\n", - " \"actor\": {\n", - " \"input_dim\": input_dim,\n", - " \"output_dim\": len(common_config[\"action_space\"]),\n", - " \"hidden_dims\": [256, 128, 64],\n", - " \"activation\": nn.Tanh,\n", - " \"softmax\": True,\n", - " \"batch_norm\": False,\n", - " \"head\": True\n", + "policy_config = {\n", + " \"model\": { \n", + " \"network\": {\n", + " \"actor\": {\n", + " \"input_dim\": input_dim,\n", + " \"output_dim\": common_config[\"num_actions\"],\n", + " \"hidden_dims\": [256, 128, 64],\n", + " \"activation\": \"tanh\",\n", + " \"softmax\": True,\n", + " \"batch_norm\": False,\n", + " \"head\": True\n", + " },\n", + " \"critic\": {\n", + " \"input_dim\": input_dim,\n", + " \"output_dim\": 1,\n", + " \"hidden_dims\": [256, 128, 64],\n", + " \"activation\": \"leaky_relu\",\n", + " \"softmax\": False,\n", + " \"batch_norm\": True,\n", + " \"head\": True\n", + " }\n", " },\n", - " \"critic\": {\n", - " \"input_dim\": input_dim,\n", - " \"output_dim\": 1,\n", - " \"hidden_dims\": [256, 128, 64],\n", - " \"activation\": nn.LeakyReLU,\n", - " \"softmax\": False,\n", - " \"batch_norm\": True,\n", - " \"head\": True\n", + " \"optimization\": {\n", + " \"actor\": OptimOption(optim_cls=\"adam\", optim_params={\"lr\": 0.001}),\n", + " \"critic\": OptimOption(optim_cls=\"rmsprop\", optim_params={\"lr\": 0.001})\n", " }\n", " },\n", - " \"optimization\": {\n", - " \"actor\": OptimOption(optim_cls=Adam, optim_params={\"lr\": 0.001}),\n", - " \"critic\": OptimOption(optim_cls=RMSprop, optim_params={\"lr\": 0.001})\n", + " \"experience_memory\": {\n", + " \"capacity\": 10000\n", " },\n", - " \"hyper_params\": {\n", + " \"algorithm_config\": {\n", " \"reward_discount\": .0,\n", - " \"critic_loss_func\": nn.SmoothL1Loss(),\n", - " \"num_steps\": 10,\n", + " \"train_epochs\": 10,\n", + " \"gradient_iters\": 1,\n", " \"actor_loss_coefficient\": 0.1, # loss = actor_loss_coefficient * actor_loss + critic_loss\n", - " \"k\": 1, # for k-step return\n", - " \"lam\": 0.0 # lambda return coefficient\n", + " \"critic_loss_cls\": \"smooth_l1\",\n", " }\n", "}\n", "\n", - "def get_ac_agent():\n", - " actor_net = FullyConnectedBlock(**agent_config[\"model\"][\"actor\"])\n", - " critic_net = FullyConnectedBlock(**agent_config[\"model\"][\"critic\"])\n", - " ac_model = SimpleMultiHeadModel(\n", - " {\"actor\": actor_net, \"critic\": critic_net}, optim_option=agent_config[\"optimization\"],\n", - " )\n", - " return ActorCritic(ac_model, ActorCriticConfig(**agent_config[\"hyper_params\"]))" + "\n", + "class MyACNet(DiscreteACNet):\n", + " def forward(self, states, actor: bool = True, critic: bool = True):\n", + " states = torch.from_numpy(np.asarray(states))\n", + " if len(states.shape) == 1:\n", + " states = states.unsqueeze(dim=0)\n", + "\n", + " states = states.to(self.device)\n", + " return (\n", + " self.component[\"actor\"](states) if actor else None,\n", + " self.component[\"critic\"](states) if critic else None\n", + " )\n", + "\n", + "\n", + "def get_ac_policy():\n", + " actor = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"actor\"])\n", + " critic = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"critic\"])\n", + " ac_net = MyACNet({\"actor\": actor, \"critic\": critic}, optim_option=policy_config[\"model\"][\"optimization\"])\n", + " experience_memory = ExperienceMemory(policy_config[\"experience_memory\"][\"capacity\"])\n", + " return ActorCritic(ac_net, experience_memory, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" ] }, { @@ -230,106 +236,121 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "14:54:17 | LEARNER | INFO | ep-0: {'order_requirements': 2240000, 'container_shortage': 1422736, 'operation_number': 4220466}\n", - "14:54:19 | LEARNER | INFO | Agent learning finished\n", - "14:54:23 | LEARNER | INFO | ep-1: {'order_requirements': 2240000, 'container_shortage': 1330641, 'operation_number': 3919970}\n", - "14:54:24 | LEARNER | INFO | Agent learning finished\n", - "14:54:29 | LEARNER | INFO | ep-2: {'order_requirements': 2240000, 'container_shortage': 996878, 'operation_number': 3226186}\n", - "14:54:30 | LEARNER | INFO | Agent learning finished\n", - "14:54:34 | LEARNER | INFO | ep-3: {'order_requirements': 2240000, 'container_shortage': 703662, 'operation_number': 3608511}\n", - "14:54:36 | LEARNER | INFO | Agent learning finished\n", - "14:54:40 | LEARNER | INFO | ep-4: {'order_requirements': 2240000, 'container_shortage': 601934, 'operation_number': 3579281}\n", - "14:54:41 | LEARNER | INFO | Agent learning finished\n", - "14:54:45 | LEARNER | INFO | ep-5: {'order_requirements': 2240000, 'container_shortage': 629344, 'operation_number': 3456707}\n", - "14:54:47 | LEARNER | INFO | Agent learning finished\n", - "14:54:51 | LEARNER | INFO | ep-6: {'order_requirements': 2240000, 'container_shortage': 560709, 'operation_number': 3511869}\n", - "14:54:52 | LEARNER | INFO | Agent learning finished\n", - "14:54:56 | LEARNER | INFO | ep-7: {'order_requirements': 2240000, 'container_shortage': 483549, 'operation_number': 3613713}\n", - "14:54:57 | LEARNER | INFO | Agent learning finished\n", - "14:55:02 | LEARNER | INFO | ep-8: {'order_requirements': 2240000, 'container_shortage': 390332, 'operation_number': 3817820}\n", - "14:55:03 | LEARNER | INFO | Agent learning finished\n", - "14:55:07 | LEARNER | INFO | ep-9: {'order_requirements': 2240000, 'container_shortage': 361151, 'operation_number': 3823994}\n", - "14:55:08 | LEARNER | INFO | Agent learning finished\n", - "14:55:13 | LEARNER | INFO | ep-10: {'order_requirements': 2240000, 'container_shortage': 442086, 'operation_number': 3647343}\n", - "14:55:14 | LEARNER | INFO | Agent learning finished\n", - "14:55:18 | LEARNER | INFO | ep-11: {'order_requirements': 2240000, 'container_shortage': 390846, 'operation_number': 3784078}\n", - "14:55:19 | LEARNER | INFO | Agent learning finished\n", - "14:55:24 | LEARNER | INFO | ep-12: {'order_requirements': 2240000, 'container_shortage': 309105, 'operation_number': 3896184}\n", - "14:55:25 | LEARNER | INFO | Agent learning finished\n", - "14:55:29 | LEARNER | INFO | ep-13: {'order_requirements': 2240000, 'container_shortage': 430801, 'operation_number': 3787247}\n", - "14:55:30 | LEARNER | INFO | Agent learning finished\n", - "14:55:35 | LEARNER | INFO | ep-14: {'order_requirements': 2240000, 'container_shortage': 368042, 'operation_number': 3793428}\n", - "14:55:36 | LEARNER | INFO | Agent learning finished\n", - "14:55:40 | LEARNER | INFO | ep-15: {'order_requirements': 2240000, 'container_shortage': 383015, 'operation_number': 3829184}\n", - "14:55:41 | LEARNER | INFO | Agent learning finished\n", - "14:55:46 | LEARNER | INFO | ep-16: {'order_requirements': 2240000, 'container_shortage': 373584, 'operation_number': 3772635}\n", - "14:55:47 | LEARNER | INFO | Agent learning finished\n", - "14:55:51 | LEARNER | INFO | ep-17: {'order_requirements': 2240000, 'container_shortage': 411397, 'operation_number': 3644350}\n", - "14:55:53 | LEARNER | INFO | Agent learning finished\n", - "14:55:57 | LEARNER | INFO | ep-18: {'order_requirements': 2240000, 'container_shortage': 307861, 'operation_number': 3842550}\n", - "14:55:58 | LEARNER | INFO | Agent learning finished\n", - "14:56:02 | LEARNER | INFO | ep-19: {'order_requirements': 2240000, 'container_shortage': 324650, 'operation_number': 3848202}\n", - "14:56:04 | LEARNER | INFO | Agent learning finished\n", - "14:56:08 | LEARNER | INFO | ep-20: {'order_requirements': 2240000, 'container_shortage': 367267, 'operation_number': 3739414}\n", - "14:56:09 | LEARNER | INFO | Agent learning finished\n", - "14:56:13 | LEARNER | INFO | ep-21: {'order_requirements': 2240000, 'container_shortage': 326153, 'operation_number': 3822407}\n", - "14:56:15 | LEARNER | INFO | Agent learning finished\n", - "14:56:19 | LEARNER | INFO | ep-22: {'order_requirements': 2240000, 'container_shortage': 466237, 'operation_number': 3516845}\n", - "14:56:20 | LEARNER | INFO | Agent learning finished\n", - "14:56:25 | LEARNER | INFO | ep-23: {'order_requirements': 2240000, 'container_shortage': 429538, 'operation_number': 3603386}\n", - "14:56:26 | LEARNER | INFO | Agent learning finished\n", - "14:56:30 | LEARNER | INFO | ep-24: {'order_requirements': 2240000, 'container_shortage': 241307, 'operation_number': 3986364}\n", - "14:56:31 | LEARNER | INFO | Agent learning finished\n", - "14:56:36 | LEARNER | INFO | ep-25: {'order_requirements': 2240000, 'container_shortage': 260224, 'operation_number': 3971519}\n", - "14:56:37 | LEARNER | INFO | Agent learning finished\n", - "14:56:41 | LEARNER | INFO | ep-26: {'order_requirements': 2240000, 'container_shortage': 190507, 'operation_number': 4060439}\n", - "14:56:42 | LEARNER | INFO | Agent learning finished\n", - "14:56:47 | LEARNER | INFO | ep-27: {'order_requirements': 2240000, 'container_shortage': 152822, 'operation_number': 4146195}\n", - "14:56:48 | LEARNER | INFO | Agent learning finished\n", - "14:56:52 | LEARNER | INFO | ep-28: {'order_requirements': 2240000, 'container_shortage': 91878, 'operation_number': 4300404}\n", - "14:56:53 | LEARNER | INFO | Agent learning finished\n", - "14:56:58 | LEARNER | INFO | ep-29: {'order_requirements': 2240000, 'container_shortage': 78752, 'operation_number': 4297044}\n", - "14:56:59 | LEARNER | INFO | Agent learning finished\n", - "14:57:03 | LEARNER | INFO | ep-30: {'order_requirements': 2240000, 'container_shortage': 202098, 'operation_number': 4047921}\n", - "14:57:04 | LEARNER | INFO | Agent learning finished\n", - "14:57:09 | LEARNER | INFO | ep-31: {'order_requirements': 2240000, 'container_shortage': 161871, 'operation_number': 4113281}\n", - "14:57:10 | LEARNER | INFO | Agent learning finished\n", - "14:57:14 | LEARNER | INFO | ep-32: {'order_requirements': 2240000, 'container_shortage': 74649, 'operation_number': 4311775}\n", - "14:57:16 | LEARNER | INFO | Agent learning finished\n", - "14:57:20 | LEARNER | INFO | ep-33: {'order_requirements': 2240000, 'container_shortage': 54402, 'operation_number': 4330703}\n", - "14:57:21 | LEARNER | INFO | Agent learning finished\n", - "14:57:26 | LEARNER | INFO | ep-34: {'order_requirements': 2240000, 'container_shortage': 42802, 'operation_number': 4353353}\n", - "14:57:27 | LEARNER | INFO | Agent learning finished\n", - "14:57:31 | LEARNER | INFO | ep-35: {'order_requirements': 2240000, 'container_shortage': 49236, 'operation_number': 4346898}\n", - "14:57:32 | LEARNER | INFO | Agent learning finished\n", - "14:57:37 | LEARNER | INFO | ep-36: {'order_requirements': 2240000, 'container_shortage': 74055, 'operation_number': 4280054}\n", - "14:57:38 | LEARNER | INFO | Agent learning finished\n", - "14:57:42 | LEARNER | INFO | ep-37: {'order_requirements': 2240000, 'container_shortage': 66899, 'operation_number': 4312042}\n", - "14:57:43 | LEARNER | INFO | Agent learning finished\n", - "14:57:48 | LEARNER | INFO | ep-38: {'order_requirements': 2240000, 'container_shortage': 29641, 'operation_number': 4385481}\n", - "14:57:49 | LEARNER | INFO | Agent learning finished\n", - "14:57:53 | LEARNER | INFO | ep-39: {'order_requirements': 2240000, 'container_shortage': 56018, 'operation_number': 4354815}\n", - "14:57:54 | LEARNER | INFO | Agent learning finished\n" + "15:27:27 | LEARNER | INFO | Policy will be evaluated at the end of episodes [40]\n", + "15:27:27 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 1, segment 1\n", + "15:27:31 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 1, segment 1(steps 1 - 795)\n", + "15:27:31 | LOCAL_ROLLOUT_MANAGER | INFO | ep 1: {'order_requirements': 2240000, 'container_shortage': 1422736, 'operation_number': 4220466}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:27:32 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:27:32 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 2, segment 1\n", + "15:27:32 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:27:36 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 2, segment 1(steps 1 - 795)\n", + "15:27:36 | LOCAL_ROLLOUT_MANAGER | INFO | ep 2: {'order_requirements': 2240000, 'container_shortage': 1331225, 'operation_number': 3918538}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:27:38 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:27:38 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 3, segment 1\n", + "15:27:38 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:27:42 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 3, segment 1(steps 1 - 795)\n", + "15:27:42 | LOCAL_ROLLOUT_MANAGER | INFO | ep 3: {'order_requirements': 2240000, 'container_shortage': 988986, 'operation_number': 3207994}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:27:43 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:27:43 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 4, segment 1\n", + "15:27:43 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:27:47 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 4, segment 1(steps 1 - 795)\n", + "15:27:47 | LOCAL_ROLLOUT_MANAGER | INFO | ep 4: {'order_requirements': 2240000, 'container_shortage': 684603, 'operation_number': 3650267}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:27:48 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:27:48 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 5, segment 1\n", + "15:27:48 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:27:52 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 5, segment 1(steps 1 - 795)\n", + "15:27:52 | LOCAL_ROLLOUT_MANAGER | INFO | ep 5: {'order_requirements': 2240000, 'container_shortage': 596823, 'operation_number': 3578915}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:27:54 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:27:54 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 6, segment 1\n", + "15:27:54 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:27:58 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 6, segment 1(steps 1 - 795)\n", + "15:27:58 | LOCAL_ROLLOUT_MANAGER | INFO | ep 6: {'order_requirements': 2240000, 'container_shortage': 640579, 'operation_number': 3461426}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:27:59 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:27:59 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 7, segment 1\n", + "15:27:59 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:28:03 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 7, segment 1(steps 1 - 795)\n", + "15:28:03 | LOCAL_ROLLOUT_MANAGER | INFO | ep 7: {'order_requirements': 2240000, 'container_shortage': 543005, 'operation_number': 3567518}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:28:04 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:28:04 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 8, segment 1\n", + "15:28:04 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:28:08 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 8, segment 1(steps 1 - 795)\n", + "15:28:08 | LOCAL_ROLLOUT_MANAGER | INFO | ep 8: {'order_requirements': 2240000, 'container_shortage': 488595, 'operation_number': 3602507}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:28:09 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:28:09 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 9, segment 1\n", + "15:28:09 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:28:14 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 9, segment 1(steps 1 - 795)\n", + "15:28:14 | LOCAL_ROLLOUT_MANAGER | INFO | ep 9: {'order_requirements': 2240000, 'container_shortage': 387606, 'operation_number': 3836217}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:28:15 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:28:15 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 10, segment 1\n", + "15:28:15 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", + "15:28:19 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 10, segment 1(steps 1 - 795)\n", + "15:28:19 | LOCAL_ROLLOUT_MANAGER | INFO | ep 10: {'order_requirements': 2240000, 'container_shortage': 341466, 'operation_number': 3862685}\n", + "Policy 0: exp mem size = 158, new exp = 158\n", + "Policy 1: exp mem size = 158, new exp = 158\n", + "Policy 2: exp mem size = 317, new exp = 317\n", + "Policy 3: exp mem size = 158, new exp = 158\n", + "15:28:20 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", + "15:28:20 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 11, segment 1\n", + "15:28:20 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n" ] } ], "source": [ "from maro.simulator import Env\n", - "from maro.rl import Actor, MultiAgentWrapper, OnPolicyLearner\n", + "from maro.rl import Learner, LocalRolloutManager, LocalPolicyManager\n", "from maro.utils import set_seeds\n", "\n", "set_seeds(1024) # for reproducibility\n", "env = Env(\"cim\", \"toy.4p_ssdd_l0.0\", durations=1120)\n", - "agent = MultiAgentWrapper({name: get_ac_agent() for name in env.agent_idx_list})\n", - "actor = Actor(env, agent, CIMTrajectory, trajectory_kwargs=common_config)\n", - "learner = OnPolicyLearner(actor, 40) # 40 episodes\n", + "env_wrapper = CIMEnvWrapper(env, **common_config)\n", + "policy_dict = {agent_id: get_ac_policy() for agent_id in env.agent_idx_list}\n", + "agent2policy = {agent_id: agent_id for agent_id in env.agent_idx_list}\n", + "rollout_manager = LocalRolloutManager(env_wrapper, policy_dict, agent2policy, log_total_reward=False)\n", + "policy_manager = LocalPolicyManager(policy_dict)\n", + "learner = Learner(policy_manager, rollout_manager, 40) # 40 episodes\n", "learner.run()" ] } From 9ec52a721992f1533a868862f2fa06787d7e6de3 Mon Sep 17 00:00:00 2001 From: Lei Song Date: Mon, 24 May 2021 09:32:42 +0800 Subject: [PATCH 263/482] add notebook demo; --- examples/supply_chain/sc_demo.ipynb | 310 ++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 examples/supply_chain/sc_demo.ipynb diff --git a/examples/supply_chain/sc_demo.ipynb b/examples/supply_chain/sc_demo.ipynb new file mode 100644 index 000000000..6594c586a --- /dev/null +++ b/examples/supply_chain/sc_demo.ipynb @@ -0,0 +1,310 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 1a: Customize your supply chain - defining facility template (optional)\n", + "- Facility templates are to define different types of facilities in a supply chain\n", + "- A facility template defines not only what kinds of agents it contains but also how they perform (class)\n", + "- In a facility template, you can also assign agents to different \"agent_type\" to group agents\n", + "- For example, the following codes define a store template of a retailer:\n", + "-- Each store can be seen as a facility with a list of \"products\" and \"storage\" capacity\n", + "-- Each product in the store has both \"consumer\" and \"seller\" behaviors to denote that it can place replenishing orders and be sold to customers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + " RetailerFacility: &retailer_facility \n", + " class: \"RetailerFacility\" \n", + " children:\n", + " storage:\n", + " class: \"StorageUnit\"\n", + " products:\n", + " class: \"StoreProductUnit\"\n", + " is_template: true\n", + " config:\n", + " agent_type: \"productstore\"\n", + " consumer:\n", + " class: \"ConsumerUnit\"\n", + " config:\n", + " agent_type: \"consumerstore\"\n", + " seller:\n", + " class: \"SellerUnit\"\n", + " config:\n", + " sale_hist_len: 4\n", + " config:\n", + " agent_type: \"facility\"\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 1b: Instantiate all facilities in the supply chain\n", + "- Refer a facility template defined before\n", + "- Instantiate parameters of each sub-agent\n", + "- Below is an example showing how a supplier and a retailer can be instantiated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + " facilities:\n", + " - name: \"Supplier_001\" # name of the facility\n", + " definition_ref: \"SupplierFacility\"\n", + " # sku list of this facility\n", + " skus:\n", + " sku1: # sku name and attributes needed for this facility\n", + " init_stock: 1000\n", + " product_unit_cost: 90\n", + " production_rate: 1000\n", + " type: \"production\"\n", + " cost: 90\n", + " price: 100\n", + " vlt: 2\n", + " sku2:\n", + " init_stock: 1000\n", + " product_unit_cost: 150\n", + " production_rate: 1000\n", + " type: \"production\"\n", + " cost: 150\n", + " price: 200\n", + " vlt: 3\n", + " children:\n", + " storage: \n", + " distribution:\n", + " # config of this facility\n", + " config:\n", + " delay_order_penalty: 100\n", + " order_cost: 10\n", + " \n", + " - name: \"Retailer_001\"\n", + " definition_ref: \"RetailerFacility\"\n", + " skus:\n", + " sku1:\n", + " price: 200\n", + " cost: 100\n", + " init_stock: 40\n", + " sale_gamma: 20\n", + " backlog_ratio: 0.1\n", + " vlt: 1\n", + " sku2:\n", + " price: 350\n", + " cost: 200\n", + " init_stock: 160\n", + " sale_gamma: 80\n", + " backlog_ratio: 0.1\n", + " vlt: 1\n", + " children:\n", + " storage: *small_storage\n", + "\"\"\"\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# In case we want to define a retailer store with demands of SKUs are sampled from historical data, this can be simply done by changing the \"class\" of seller, see below for instance." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"\"\"\n", + " RetailerFacility: &raw_data_retailer_facility\n", + " class: \"OuterRetailerFacility\"\n", + " children:\n", + " storage:\n", + " class: \"StorageUnit\"\n", + " products:\n", + " class: \"StoreProductUnit\"\n", + " is_template: true\n", + " config:\n", + " agent_type: \"consumer\"\n", + " consumer:\n", + " class: \"ConsumerUnit\"\n", + " seller:\n", + " class: \"RawDataSellerUnit\" ######################\n", + " config:\n", + " sale_hist_len: 4\n", + " config:\n", + " agent_type: \"facility\"\n", + " seller_sampler_type: data\n", + " sku_column: \"SKU\"\n", + " price_column: \"Price\"\n", + " sale_column: \"Sales\"\n", + " datetime_column: \"DT\"\n", + " file_path: \"/path/to/data.csv\"\n", + " \n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 2: Defining fulfillment relation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + " topology:\n", + " Warehouse_001:\n", + " sku1:\n", + " - \"Supplier_001\"\n", + " sku2:\n", + " - \"Supplier_001\"\n", + " sku3:\n", + " - \"Supplier_001\"\n", + " Retailer_001:\n", + " sku1:\n", + " - \"Warehouse_001\"\n", + " sku2:\n", + " - \"Warehouse_001\"\n", + " sku3:\n", + " - \"Warehouse_001\"\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 3: Defining inventory policies" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'maro'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mtorch\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m from maro.rl import (\n\u001b[0m\u001b[0;32m 7\u001b[0m \u001b[0mDQN\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mDQNConfig\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mEpisodeBasedSchedule\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mFullyConnectedBlock\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mNullPolicy\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mOptimOption\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mQNetForDiscreteActionSpace\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[0mStepBasedSchedule\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mUniformSampler\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'maro'" + ] + } + ], + "source": [ + "import sys\n", + "from os.path import dirname, realpath\n", + "import numpy as np\n", + "import torch\n", + "\n", + "from maro.rl import (\n", + " DQN, DQNConfig, EpisodeBasedSchedule, FullyConnectedBlock, NullPolicy, OptimOption, QNetForDiscreteActionSpace,\n", + " StepBasedSchedule, UniformSampler\n", + ")\n", + "\n", + "from or_policy.minmax_policy import ConsumerMinMaxPolicy\n", + "from or_policy.eoq_policy import ConsumerEOQPolicy as ProducerEOQPolicy\n", + "from or_policy.base_policy import ProducerBaselinePolicy\n", + "\n", + "sc_code_dir = dirname(realpath(__file__))\n", + "sys.path.insert(0, sc_code_dir)\n", + "from config import config\n", + "\n", + "def get_dqn_consumer_policy(config):\n", + " q_net = SimpleQNet(\n", + " FullyConnectedBlock(**cfg[\"model\"][\"network\"]),\n", + " optim_option=OptimOption(**cfg[\"model\"][\"optimization\"]),\n", + " device=cfg[\"model\"][\"device\"]\n", + " )\n", + " experience_manager = UniformSampler(**cfg[\"experience_manager\"])\n", + " return DQN(q_net, experience_manager, DQNConfig(**cfg[\"algorithm_config\"]))\n", + "\n", + "def get_eoq_producer_policy(config):\n", + " return ProducerEOQPolicy(config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 4: Mapping agents to policies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config = {}\n", + "policy_dict = {\n", + " 'consumer': get_dqn_consumer_policy(config),\n", + " 'producer': get_eoq_producer_policy(config)\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 5: Training and evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/bin/bash: run.sh: No such file or directory\n" + ] + } + ], + "source": [ + "!bash run.sh" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From cad7fc6ade25dc691eab23420cff0693f070e13c Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 24 May 2021 02:46:16 +0000 Subject: [PATCH 264/482] updated rl_toolkit.rst --- docs/source/key_components/rl_toolkit.rst | 98 ++++++++++++++--------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 520dab9c5..58be5249b 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -11,12 +11,12 @@ controllers such as `Actor <#actor>`_\ and `Learner <#learner>`_. Policy ------ -A policy is a mechanism used by an agent to determine an action to take given an observation of the environment. -Accordingly, the abstract ``AbsPolicy`` class exposes a ``choose_action`` interface. This abstraction -can encompass both static policies, such as rule-based policies, and updatable policies, such as RL -policies. The latter is abstracted through the ``AbsCorePolicy`` sub-class which also exposes a ``update`` -interface. By default, updatable policies require an experience manager to store and retrieve simulation -data (in the form of "experiences sets") based on which updates can be made. +A policy is a an agent's mechanism to determine an action to take given an observation of the environment. +Accordingly, the abstract ``AbsPolicy`` class exposes a ``choose_action`` interface. This abstraction encompasses +both static policies, such as rule-based policies, and updatable policies, such as RL policies. The latter is +abstracted through the ``AbsCorePolicy`` sub-class which also exposes a ``update`` interface. By default, updatable +policies require an experience manager to store and retrieve simulation data (in the form of "experiences sets") based +on which updates can be made. .. image:: ../images/rl/agent.svg @@ -60,34 +60,50 @@ The code snippet below shows how to create a model for the actor-critic algorith .. code-block:: python class MyACModel(DiscreteACNet): - def forward(self, states): + def forward(self, states, actor=True, critic=True): features = self.component["representation"](states) - + return ( + self.component["actor"](features) if actor else None, + self.component["critic"](features) if critic else None + ) - shared_stack = FullyConnectedBlock(...) + representation_stack = FullyConnectedBlock(...) actor_head = FullyConnectedBlock(...) critic_head = FullyConnectedBlock(...) ac_model = SimpleMultiHeadModel( - {"actor": actor_stack, "critic": critic_stack}, + {"representation": representation_stack, "actor": actor_head, "critic": critic_head}, optim_option={ - "actor": OptimizerOption(cls=Adam, params={"lr": 0.001}) - "critic": OptimizerOption(cls=RMSprop, params={"lr": 0.0001}) + "representation": OptimizerOption(cls="adam", params={"lr": 0.0001}), + "actor": OptimizerOption(cls="adam", params={"lr": 0.001}), + "critic": OptimizerOption(cls="rmsprop", params={"lr": 0.0001}) } ) - agent = ActorCritic("actor_critic", learning_model, config) -Choosing an action is simply: +To generate stochastic actions given a batch of states, call ``get_action`` on the model instance: .. code-block:: python - model(state, task_name="actor", training=False) + action, log_p = ac_model.get_action(state) -And performing one gradient step is simply: +To performing a single gradient step on the model, call the ``step`` function: .. code-block:: python - model.learn(critic_loss + actor_loss) + ac_model.step(critic_loss + actor_loss) + +Here it is assumed that the losses have been computed using the same model instance and the gradients have +been generated for the internal components. + + +Experience +---------- + +An ``ExperienceSet`` is a synonym for training data for RL policies. The data originate from the simulator and +get processed and organized into a set of transitions in the form of (state, action, reward, next_state, info). +An ``ExperienceMemory`` is a storage facility for experience sets and is maintained by a policy to store experiences +it generates. + Exploration @@ -103,10 +119,26 @@ As an example, the exploration for DQN may be carried out with the aid of an ``E .. code-block:: python exploration = EpsilonGreedyExploration(num_actions=10) - greedy_action = learning_model(state, training=False).argmax(dim=1).data + greedy_action = q_net.get_action(state) exploration_action = exploration(greedy_action) +Environment Wrapper +------------------- + +An environment wrapper a wrapper for a raw MARO environment that provides unified interfaces to the external +RL workflow through user-defined state, action and reward shaping. It is also responsible for caching transitions and preparing experiences +for training. It is necessary to implement an environment wrapper for the environemtn of your choice in order to +run the RL workflow using the training tools described below. Key methods that need to be defined for an environment +wrapper include: +* ``get_state``, which converts observations of an environment into model input. For example, the observation +may be represented by a multi-level data structure, which gets encoded to a one-dimensional vector as input to +a neural network. +* ``to_env_action``, which provides model output with necessary context so that it can be executed by the +environment simulator. +* ``get_reward``, for evaluating rewards. + + Tools for Training ------------------ @@ -115,24 +147,12 @@ Tools for Training :alt: RL Overview The RL toolkit provides tools that make local and distributed training easy: -* Learner, the central controller of the learning process, which consists of collecting simulation data from - remote actors and training the agents with them. The training data collection can be done in local or - distributed fashion by loading an ``Actor`` or ``ActorProxy`` instance, respectively. -* Actor, which implements the ``roll_out`` method where the agent interacts with the environment for one - episode. It consists of an environment instance and an agent (a single agent or multiple agents wrapped by - ``MultiAgentWrapper``). The class provides the worker() method which turns it to an event loop where roll-outs - are performed on the learner's demand. In distributed RL, there are typically many actor processes running - simultaneously to parallelize training data collection. -* Actor proxy, which also implements the ``roll_out`` method with the same signature, but manages a set of remote - actors for parallel data collection. -* Trajectory, which is primarily responsible for translating between scenario-specific information and model - input / output. It implements the following methods which are used as callbacks in the actor's roll-out loop: - * ``get_state``, which converts observations of an environment into model input. For example, the observation - may be represented by a multi-level data structure, which gets encoded by a state shaper to a one-dimensional - vector as input to a neural network. The state shaper usually goes hand in hand with the underlying policy - or value models. - * ``get_action``, which provides model output with necessary context so that it can be executed by the - environment simulator. - * ``get_reward``, which computes a reward for a given action. - * ``on_env_feedback``, which defines things to do upon getting feedback from the environment. - * ``on_finish``, which defines things to do upon completion of a roll-out episode. +* Learner, which consists of a roll-out manager and a policy manager, is the controller for a learning process. +The learner process executes training cycles that alternate between data collection and policy updates. +* Rollout manager, which is respnsible for collecting simulation data. The ``LocalRolloutManager`` performs roll-outs +locally, while the ``ParallelRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in +parallel. +* Actor, which consists of an environment instance and a set of policies that agents use to interact with it, is a +remote roll-out worker instance managed by a ``ParallelRolloutManager``. +* Policy manager, which manages a set of policies and controls their updates. The policy instances may reside in the +manager (``LocalPolicyManager``) or be distributed on a set of remote nodes (``ParallelPolicyManager``, to be implemented). From 71174808d3d7e2ab96fad4d58cbbcc139f61d17d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 24 May 2021 06:50:48 +0000 Subject: [PATCH 265/482] fixed lint issues and added name attr to policy --- docs/source/key_components/rl_toolkit.rst | 9 +-- maro/rl/__init__.py | 6 +- maro/rl/algorithm/ddpg.py | 3 +- maro/rl/algorithm/dqn.py | 2 +- maro/rl/algorithm/pg.py | 6 +- maro/rl/algorithm/rl_policy_index.py | 6 +- maro/rl/env_wrapper/env_wrapper.py | 9 ++- maro/rl/experience/__init__.py | 1 - maro/rl/experience/experience_memory.py | 8 +-- maro/rl/experience/sampler.py | 1 - maro/rl/exploration/__init__.py | 6 +- maro/rl/exploration/abs_exploration.py | 3 +- maro/rl/exploration/exploration_scheduler.py | 13 +++-- maro/rl/model/__init__.py | 4 +- maro/rl/model/core_model.py | 10 ++-- maro/rl/policy/policy.py | 39 +++++++++++-- maro/rl/training/__init__.py | 4 +- maro/rl/training/actor.py | 28 ++++----- maro/rl/training/learner.py | 4 +- maro/rl/training/policy_manager.py | 60 +++----------------- maro/rl/training/rollout_manager.py | 34 +++++------ maro/utils/exception/rl_toolkit_exception.py | 2 +- 22 files changed, 121 insertions(+), 137 deletions(-) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 58be5249b..8377ba3f7 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -100,10 +100,11 @@ Experience ---------- An ``ExperienceSet`` is a synonym for training data for RL policies. The data originate from the simulator and -get processed and organized into a set of transitions in the form of (state, action, reward, next_state, info). -An ``ExperienceMemory`` is a storage facility for experience sets and is maintained by a policy to store experiences -it generates. - +get processed and organized into a set of transitions in the form of (state, action, reward, next_state, info), +where ''info'' contains information about the transition that is not encoded in the state but may be necessary +for sampling purposes. An ``ExperienceMemory`` is a storage facility for experience sets and is maintained by +a policy for storing and retrieving training data. Sampling from the experience memory can be customized by +registering a user-defined sampler to it. Exploration diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index e6d32ddc3..25964f4f0 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. from maro.rl.algorithm import ( - ActorCritic, ActorCriticConfig, DDPG, DDPGConfig, DQN, DQNConfig, PolicyGradient, PolicyGradientConfig, + DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) from maro.rl.env_wrapper import AbsEnvWrapper @@ -19,7 +19,7 @@ from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalPolicyManager, LocalRolloutManager, - ParallelRolloutManager, PolicyUpdateTrigger + ParallelRolloutManager ) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, @@ -37,7 +37,7 @@ "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", - "AbsPolicyManager", "AbsRolloutManager", "Actor", "PolicyUpdateTrigger", "Learner", "LocalPolicyManager", "LocalRolloutManager", + "AbsPolicyManager", "AbsRolloutManager", "Actor", "Learner", "LocalPolicyManager", "LocalRolloutManager", "ParallelRolloutManager", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index 7190943e4..ed3ecec3c 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import namedtuple from typing import Union import numpy as np @@ -96,7 +95,7 @@ def update(self): rewards = torch.from_numpy(experience_set.rewards).to(self.device) if len(actual_actions.shape) == 1: actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - + with torch.no_grad(): next_q_values = self.target_ac_net.value(next_states) target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index 5abb646db..a91ef54a2 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -88,7 +88,7 @@ def choose_action(self, states) -> Union[int, np.ndarray]: return actions[0] if len(actions) == 1 else actions def update(self): - assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." + assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() for _ in range(self.config.train_epochs): # sample from the replay memory diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index ec81c6dfa..fe6c9e274 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.experience.experience_memory import ExperienceMemory from typing import Tuple import numpy as np import torch +from maro.rl.experience.experience_memory import ExperienceMemory from maro.rl.model import DiscretePolicyNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_truncated_cumulative_reward @@ -38,7 +38,7 @@ class PolicyGradient(AbsCorePolicy): """ def __init__( self, policy_net: DiscretePolicyNet, experience_memory: ExperienceMemory, config: PolicyGradientConfig, - ): + ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") super().__init__(experience_memory) @@ -57,7 +57,7 @@ def update(self): """ This should be called at the end of a simulation episode and the experiences obtained from the experience manager's ``get`` method should be a sequential set, i.e., in the order in - which they are generated during the simulation. Otherwise, the return values may be meaningless. + which they are generated during the simulation. Otherwise, the return values may be meaningless. """ self.policy_net.train() experience_set = self.experience_memory.get() diff --git a/maro/rl/algorithm/rl_policy_index.py b/maro/rl/algorithm/rl_policy_index.py index 331ebf7bc..7864d71c8 100644 --- a/maro/rl/algorithm/rl_policy_index.py +++ b/maro/rl/algorithm/rl_policy_index.py @@ -2,9 +2,9 @@ # Licensed under the MIT license. from .ac import ActorCritic, ActorCriticConfig, DiscreteACNet -from .ddpg import DDPG, DDPGConfig, ContinuousACNet -from .dqn import DQN, DQNConfig, DiscreteQNet -from .pg import PolicyGradient, PolicyGradientConfig, DiscretePolicyNet +from .ddpg import DDPG, ContinuousACNet, DDPGConfig +from .dqn import DQN, DiscreteQNet, DQNConfig +from .pg import DiscretePolicyNet, PolicyGradient, PolicyGradientConfig RL_POLICY_INDEX = { diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 2533f376e..9c2250ab2 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -3,10 +3,9 @@ from abc import ABC, abstractmethod from collections import defaultdict, deque -from typing import Dict -from maro.simulator import Env from maro.rl.experience import ExperienceSet +from maro.simulator import Env class AbsEnvWrapper(ABC): @@ -28,7 +27,7 @@ def __init__(self, env: Env, reward_eval_delay: int = 0, save_replay: bool = Tru self.reward_eval_delay = reward_eval_delay self.action_history = defaultdict(dict) self.save_replay = save_replay - self.replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids + self.replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids self._replay_buffer = {agent_id: defaultdict(list) for agent_id in self.replay_agent_ids} self._pending_reward_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None @@ -162,8 +161,8 @@ def get_experiences(self): del buf["states"][:-1] del buf["actions"][:-1] del buf["rewards"][:-1] - del buf["info"][:-1] - exp_by_agent[agent_id] = exp_set + del buf["info"][:-1] + exp_by_agent[agent_id] = exp_set return exp_by_agent diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 6818e8e13..0a720b09d 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -4,5 +4,4 @@ from .experience_memory import ExperienceMemory, ExperienceSet from .sampler import AbsSampler, UniformSampler - __all__ = ["AbsSampler", "ExperienceMemory", "ExperienceSet", "UniformSampler"] diff --git a/maro/rl/experience/experience_memory.py b/maro/rl/experience/experience_memory.py index d0b00f309..0b446f2b3 100644 --- a/maro/rl/experience/experience_memory.py +++ b/maro/rl/experience/experience_memory.py @@ -8,7 +8,7 @@ class ExperienceSet: """Wrapper for a set of experiences. - + An experience consists of state, action, reward, next state and auxillary information. """ __slots__ = ["states", "actions", "rewards", "next_states", "info"] @@ -120,13 +120,13 @@ def put(self, experience_set: ExperienceSet): self.data[key][idx] = val self._size = min(self._capacity, num_experiences) - + def get(self): """Retrieve an experience set from the memory. If not sampler has been registered, the entirety of the stored data will be returned in the form of an ``ExperienceSet`` and the memory will be cleared. Otherwise, a sample from the memory will be - returned according to the sampling logic defined by the registered sampler. + returned according to the sampling logic defined by the registered sampler. """ if self.sampler is None: return ExperienceSet(*[self.data[key][:self._size] for key in self._keys]) @@ -135,7 +135,7 @@ def get(self): def register_sampler(self, sampler_cls, **sampler_params): """Register a sampler to the experience memory. - + Args: sampler_cls: Type of sampler to be registered. Must be a subclass of ``AbsSampler``. sampler_params: Keyword parameters for ``sampler_cls``. diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index a136edc35..8d5a2fd0f 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from maro.rl.experience.experience_memory import ExperienceMemory import numpy as np diff --git a/maro/rl/exploration/__init__.py b/maro/rl/exploration/__init__.py index 883461b1d..0c01452aa 100644 --- a/maro/rl/exploration/__init__.py +++ b/maro/rl/exploration/__init__.py @@ -3,14 +3,14 @@ from .abs_exploration import AbsExploration, NullExploration from .epsilon_greedy_exploration import EpsilonGreedyExploration -from .noise_exploration import GaussianNoiseExploration, NoiseExploration, UniformNoiseExploration from .exploration_scheduler import ( AbsExplorationScheduler, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler ) +from .noise_exploration import GaussianNoiseExploration, NoiseExploration, UniformNoiseExploration __all__ = [ "AbsExploration", "NullExploration", "EpsilonGreedyExploration", - "GaussianNoiseExploration", "NoiseExploration", "UniformNoiseExploration", - "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler" + "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", + "GaussianNoiseExploration", "NoiseExploration", "UniformNoiseExploration" ] diff --git a/maro/rl/exploration/abs_exploration.py b/maro/rl/exploration/abs_exploration.py index bfcc10285..2195e9167 100644 --- a/maro/rl/exploration/abs_exploration.py +++ b/maro/rl/exploration/abs_exploration.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from typing import Callable class AbsExploration(ABC): @@ -24,7 +23,7 @@ def register_schedule( @abstractmethod def __call__(self, action): - return NotImplementedError + return NotImplementedError @property def parameters(self): diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/exploration_scheduler.py index f462836e7..af5d40029 100644 --- a/maro/rl/exploration/exploration_scheduler.py +++ b/maro/rl/exploration/exploration_scheduler.py @@ -1,13 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from abc import ABC, abstractclassmethod +from abc import ABC, abstractmethod from typing import List, Tuple from maro.rl.exploration.abs_exploration import AbsExploration -class AbsExplorationScheduler: +class AbsExplorationScheduler(ABC): def __init__( self, exploration: AbsExploration, @@ -15,6 +15,7 @@ def __init__( last_ep: int, initial_value=None ): + super().__init__() self.exploration = exploration self.param_name = param_name self.last_ep = last_ep @@ -24,7 +25,7 @@ def __init__( def get_value(self): return getattr(self.exploration, self.param_name) - @abstractclassmethod + @abstractmethod def step(self): raise NotImplementedError @@ -89,7 +90,7 @@ def __init__( final_value: float, initial_value: float = None ): - # validate splits + # validate splits splits.append((1, initial_value)) splits.append((last_ep, final_value)) splits.sort() @@ -122,7 +123,9 @@ def step(self): if __name__ == "__main__": from maro.rl.exploration.epsilon_greedy_exploration import EpsilonGreedyExploration exploration = EpsilonGreedyExploration(5, epsilon=0.6) - scheduler = MultiPhaseLinearExplorationScheduler(exploration, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0) + scheduler = MultiPhaseLinearExplorationScheduler( + exploration, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0 + ) for ep in range(1, scheduler.last_ep + 1): print(f"ep = {ep}, value = {exploration.epsilon}") scheduler.step() diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 74746b038..476f4c78e 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -2,13 +2,13 @@ # Licensed under the MIT license. from .abs_block import AbsBlock -from .fc_block import FullyConnectedBlock from .core_model import ( AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, OptimOption ) +from .fc_block import FullyConnectedBlock __all__ = [ "AbsBlock", + "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "OptimOption", "FullyConnectedBlock", - "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "OptimOption" ] diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 7d97e1a7b..923597ba2 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -80,14 +80,14 @@ def __init__( def trainable(self) -> bool: """Return True if at least one optimizer is registered.""" return self.optimizer is not None - + @abstractmethod def forward(self, *args, **kwargs): raise NotImplementedError def step(self, loss): """Use the loss to back-propagate gradients and apply them to the underlying parameters. - + Args: loss: Result of a computation graph that involves the underlying parameters. """ @@ -210,10 +210,10 @@ class DiscreteACNet(AbsCoreModel): @abstractmethod def forward(self, states, actor: bool = True, critic: bool = True) -> tuple: """Compute action probabilities and values for a state batch. - + The output is a tuple of (action_probs, values), where action probs is a tensor of shape (batch_size, action_space_size) and values is a tensor of shape (batch_size,). If only one - of these two is needed, the return value for the other one can be set to None. + of these two is needed, the return value for the other one can be set to None. Args: states: State batch to compute action probabilities and values for. @@ -243,7 +243,7 @@ def forward(self, states, actions=None): states: State batch to compute the Q-values for. actions: Action batch. If None, the output should be a batch of actions corresponding to the state batch. Otherwise, the output should be the Q-values for the given states and - actions. Defaults to None. + actions. Defaults to None. """ raise NotImplementedError diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index a60a5e60e..5f0712c7f 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -3,14 +3,19 @@ from abc import ABC, abstractmethod -from maro.rl.experience import ExperienceMemory +from maro.rl.experience import ExperienceMemory, ExperienceSet class AbsPolicy(ABC): """Abstract policy class.""" - def __init__(self): + def __init__(self, name: str): + self._name = name super().__init__() + @property + def name(self): + return self._name + @abstractmethod def choose_action(self, state): raise NotImplementedError @@ -33,10 +38,21 @@ class AbsCorePolicy(AbsPolicy): Args: experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences for training. + update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. + warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. + Defaults to 1. """ - def __init__(self, experience_memory: ExperienceMemory): + def __init__( + self, + experience_memory: ExperienceMemory, + update_trigger: int = 1, + warmup: int = 1 + ): super().__init__() self.experience_memory = experience_memory + self.update_trigger = update_trigger + self.warmup = warmup + self._new_exp_counter = 0 @abstractmethod def choose_action(self, state): @@ -45,7 +61,7 @@ def choose_action(self, state): @abstractmethod def update(self): """Policy update logic is implemented here. - + This usually includes retrieving experiences as training samples from the experience manager and updating the underlying models using these samples. """ @@ -54,7 +70,7 @@ def update(self): @abstractmethod def get_state(self): """Return the current state of the policy. - + The implementation must be in correspondence with that of ``set_state``. For example, if a torch model is contained in the policy, ``get_state`` may include a call to ``state_dict()`` on the model, while ``set_state`` should accordingly include ``load_state_dict()``. @@ -71,6 +87,19 @@ def set_state(self, policy_state): """ pass + def on_experiences(self, exp: ExperienceSet) -> bool: + self.experience_memory.put(exp) + self._new_exp_counter += exp.size + print( + f"Policy {self._name}: exp mem size = {self.experience_memory.size}, new exp = {self._new_exp_counter}" + ) + if self.experience_memory.size >= self.warmup and self._new_exp_counter >= self.update_trigger: + self.update() + self._new_exp_counter = 0 + return True + + return False + def load(self, path: str): """Load the policy state from disk.""" pass diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 6ff39fb49..1fbbee4ac 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -3,13 +3,13 @@ from .actor import Actor from .learner import Learner -from .policy_manager import AbsPolicyManager, PolicyUpdateTrigger, LocalPolicyManager +from .policy_manager import AbsPolicyManager, LocalPolicyManager from .rollout_manager import AbsRolloutManager, LocalRolloutManager, ParallelRolloutManager __all__ = [ "Actor", "Learner", - "AbsPolicyManager", "PolicyUpdateTrigger", "LocalPolicyManager", + "AbsPolicyManager", "LocalPolicyManager", "AbsRolloutManager", "LocalRolloutManager", "ParallelRolloutManager" ] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index e5c7189b9..61161e28c 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -3,7 +3,7 @@ from collections import defaultdict from os import getcwd -from typing import Dict +from typing import List, Dict from maro.communication import Proxy from maro.rl.env_wrapper import AbsEnvWrapper @@ -20,7 +20,7 @@ class Actor(object): Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences for policy training / update. - policy_dict (Dict[str, AbsPolicy]): A set of named policies for inference. + policies (List[AbsPolicy]): A list of policies for inference. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. group (str): Identifier of the group to which the actor belongs. It must be the same group name @@ -38,7 +38,7 @@ class Actor(object): def __init__( self, env: AbsEnvWrapper, - policy_dict: Dict[str, AbsPolicy], + policies: List[AbsPolicy], agent2policy: Dict[str, str], group: str, exploration_dict: Dict[str, AbsExploration] = None, @@ -51,9 +51,9 @@ def __init__( self.eval_env = eval_env if eval_env else self.env # mappings between agents and policies - self.policy_dict = policy_dict + self.policy_dict = {policy.name: policy for policy in policies} self.agent2policy = agent2policy - self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} self.agent_groups_by_policy = defaultdict(list) for agent_id, policy_id in agent2policy.items(): self.agent_groups_by_policy[policy_id].append(agent_id) @@ -79,13 +79,13 @@ def __init__( def run(self): """Start the event loop. - - The event loop handles 3 types of messages from the roll-out manager: - 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps - and the collected experiences will be sent back to the roll-out manager; - 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire - duration of the evaluation environment. - 3) EXIT, upon which the actor will break out of the event loop and the process will terminate. + + The event loop handles 3 types of messages from the roll-out manager: + 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps + and the collected experiences will be sent back to the roll-out manager; + 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire + duration of the evaluation environment. + 3) EXIT, upon which the actor will break out of the event loop and the process will terminate. """ for msg in self._proxy.receive(): @@ -114,7 +114,7 @@ def run(self): starting_step_index = self.env.step_index + 1 steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while self.env.state and steps_to_go > 0: - if self.exploration_dict: + if self.exploration_dict: action = { id_: self.exploration[id_](self.policy[id_].choose_action(st)) @@ -165,7 +165,7 @@ def run(self): return_info = { MsgKey.METRICS: self.env.metrics, MsgKey.TOTAL_REWARD: self.eval_env.total_reward, - MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] + MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] } self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index dbc1befc3..4d2dbc2db 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -27,7 +27,7 @@ class Learner: log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. - end_of_episode_kwargs: Keyword arguments for custom end-of-episode processing. + end_of_episode_kwargs: Keyword arguments for custom end-of-episode processing. """ def __init__( self, @@ -86,7 +86,7 @@ def _train(self, ep: int): # performance details self.logger.debug(f"ep {ep} summary - experiences collected: {num_experiences_collected}") - self.end_of_episode(ep, **self._end_of_episode_kwargs) + self.end_of_episode(ep, **self._end_of_episode_kwargs) def end_of_episode(self, ep: int, **kwargs): """Custom end-of-episode processing is implemented here.""" diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index 27f244a71..258fa048f 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -2,22 +2,18 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from collections import defaultdict, namedtuple +from collections import defaultdict from os import getcwd -from typing import Dict, Union +from typing import List, Dict from maro.rl.experience import ExperienceSet from maro.rl.policy import AbsPolicy from maro.utils import Logger -PolicyUpdateTrigger = namedtuple( - "PolicyUpdateTrigger", ["min_new_experiences", "num_warmup_experiences"], defaults=[1] -) - class AbsPolicyManager(ABC): """Controller for policy updates. - + The actual policy instances may reside here or be distributed on a set of remote nodes. """ def __init__(self): @@ -44,43 +40,16 @@ class LocalPolicyManager(AbsPolicyManager): """Policy manager that contains the actual policy instances. Args: - policy_dict (Dict[str, AbsPolicy]): A set of named policies. - update_trigger (Union[PolicyUpdateTrigger, dict]): Conditions for triggering policy updates. If a - dictionary is provided, the triggers will be applied to the policies by name. If a single - ``PolicyUpdateTrigger`` is provided, the trigger will be applied to all updatable policies, i.e., - those that have the ``experience_memory`` attribute and the ``update`` interface. Defaults to - None, in which case a default updatable trigger will be applied to every updatable policy, meaning that - these policies will be updated as long as new experiences are available. + policies (List[AbsPolicy]): A list of policies. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ - def __init__( - self, - policy_dict: Dict[str, AbsPolicy], - update_trigger: Union[PolicyUpdateTrigger, dict] = None, - log_dir: str = getcwd() - ): - if isinstance(update_trigger, dict) and not update_trigger.keys() <= policy_dict.keys(): - raise ValueError(f"The keys for update_trigger must be a subset of {list(policy_dict.keys())}") - + def __init__(self, policies: List[AbsPolicy], log_dir: str = getcwd()): super().__init__() - self._names = list(policy_dict.keys()) + self._names = [policy.name for policy in policies] self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) - self.policy_dict = policy_dict - - self._updatable_policy_dict = { - policy_id: policy for policy_id, policy in self.policy_dict.items() - if hasattr(policy, "experience_memory") and hasattr(policy, "update") - } - if update_trigger is None: - default_trigger = PolicyUpdateTrigger(min_new_experiences=1, num_warmup_experiences=1) - self._update_trigger = {policy_id: default_trigger for policy_id in self._updatable_policy_dict} - elif isinstance(update_trigger, dict): - self._update_trigger = update_trigger - else: - self._update_trigger = {policy_id: update_trigger for policy_id in self._updatable_policy_dict} - + self.policy_dict = {policy.name: policy for policy in policies} self._new_exp_counter = defaultdict(int) self._updated_policy_ids = set() @@ -95,20 +64,7 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): policy's experience manager. Policies whose update conditions have been met will then be updated. """ for policy_id, exp in exp_by_policy.items(): - policy = self.policy_dict[policy_id] - if hasattr(policy, "experience_memory"): - self._new_exp_counter[policy_id] += exp.size - policy.experience_memory.put(exp) - - for policy_id, policy in self._updatable_policy_dict.items(): - print(f"Policy {policy_id}: exp mem size = {policy.experience_memory.size}, new exp = {self._new_exp_counter[policy_id]}") - if ( - policy_id not in self._update_trigger or - policy.experience_memory.size >= self._update_trigger[policy_id].num_warmup_experiences and - self._new_exp_counter[policy_id] >= self._update_trigger[policy_id].min_new_experiences - ): - policy.update() - self._new_exp_counter[policy_id] = 0 + if self.policy_dict[policy_id].on_experiences(exp): self._updated_policy_ids.add(policy_id) if self._updated_policy_ids: diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 3811b5fe7..b29938906 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -6,16 +6,16 @@ from collections import defaultdict from os import getcwd from random import choices -from typing import Dict +from typing import List, Dict from maro.communication import Proxy, SessionType +from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.experience import ExperienceSet from maro.rl.exploration import AbsExploration -from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.policy import AbsPolicy from maro.utils import Logger -from .message_enums import MsgTag, MsgKey +from .message_enums import MsgKey, MsgTag class AbsRolloutManager(ABC): @@ -40,11 +40,11 @@ def reset(self): class LocalRolloutManager(AbsRolloutManager): """Controller for a single local roll-out actor. - + Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences for policy training / update. - policy_dict (Dict[str, AbsPolicy]): A set of named policies for inference. + policies (List[AbsPolicy]): A set of named policies for inference. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. @@ -54,8 +54,8 @@ class LocalRolloutManager(AbsRolloutManager): case the roll-out will be executed until the end of the environment. eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used as the evaluation environment. Defaults to None. - log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an episode. - Defaults to True. + log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an + episode. Defaults to True. log_total_reward (bool): If True, the total reward will be logged at the end of an episode. Defaults to True. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working @@ -65,7 +65,7 @@ class LocalRolloutManager(AbsRolloutManager): def __init__( self, env: AbsEnvWrapper, - policy_dict: Dict[str, AbsPolicy], + policies: List[AbsPolicy], agent2policy: Dict[str, str], exploration_dict: Dict[str, AbsExploration] = None, agent2exploration: Dict[str, str] = None, @@ -84,9 +84,9 @@ def __init__( self.eval_env = eval_env if eval_env else self.env # mappings between agents and policies - self.policy_dict = policy_dict + self.policy_dict = {policy.name: policy for policy in policies} self._agent2policy = agent2policy - self._policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self._agent2policy.items()} + self._policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self._agent2policy.items()} self._agent_groups_by_policy = defaultdict(list) for agent_id, policy_id in agent2policy.items(): self._agent_groups_by_policy[policy_id].append(agent_id) @@ -139,7 +139,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): start_step_index = self.env.step_index + 1 steps_to_go = self._num_steps while self.env.state and steps_to_go > 0: - if self.exploration_dict: + if self.exploration_dict: action = { id_: self._exploration[id_](self._policy[id_].choose_action(st)) @@ -174,7 +174,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): self._logger.debug( f"ep {ep} summary - " f"running time: {time.time() - t0} " - f"env steps: {self.env.step_index} " + f"env steps: {self.env.step_index} " f"learning time: {learning_time} " f"experiences collected: {num_experiences_collected}" ) @@ -197,7 +197,7 @@ def evaluate(self, policy_state_dict: dict): if self._log_env_metrics: self._logger.info(f"eval ep {self._eval_ep}: {self.eval_env.metrics}") - + def _load_policy_states(self, policy_state_dict: dict): for policy_id, policy_state in policy_state_dict.items(): self.policy_dict[policy_id].set_state(policy_state) @@ -208,7 +208,7 @@ def _load_policy_states(self, policy_state_dict: dict): class ParallelRolloutManager(AbsRolloutManager): """Controller for a set of remote roll-out actors. - + Args: num_actors (int): Number of remote roll-out actors. group (str): Identifier of the group to which the actor belongs. It must be the same group name @@ -219,10 +219,10 @@ class ParallelRolloutManager(AbsRolloutManager): in which case the number is set to ``num_actors``. max_staleness (int): Maximum allowable staleness measured in the number of calls to ``collect``. Experiences collected from calls to ``collect`` within ``max_staleness`` calls ago will be returned to the learner. - Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. + Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. num_eval_actors (int): Number of actors required for evaluation. Defaults to 1. - log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an episode. - Defaults to True. + log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an + episode. Defaults to True. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. diff --git a/maro/utils/exception/rl_toolkit_exception.py b/maro/utils/exception/rl_toolkit_exception.py index 271b2b92e..bf82231ee 100644 --- a/maro/utils/exception/rl_toolkit_exception.py +++ b/maro/utils/exception/rl_toolkit_exception.py @@ -6,7 +6,7 @@ class InvalidExperience(MAROException): """ - Raised when the states, actions, rewards and next states passed to an ``ExperienceSet`` do not + Raised when the states, actions, rewards and next states passed to an ``ExperienceSet`` do not have the same length. """ def __init__(self, msg: str = None): From 1ded117177dc821a0a48dc867522884046cedd3c Mon Sep 17 00:00:00 2001 From: lesong Date: Mon, 24 May 2021 07:56:22 +0000 Subject: [PATCH 266/482] add sc demo; --- examples/supply_chain/learner.py | 2 +- examples/supply_chain/policies.py | 3 +- examples/supply_chain/sc_demo.ipynb | 1976 ++++++++++++++++- .../supply_chain/single_thread_launcher.py | 2 +- 4 files changed, 1944 insertions(+), 39 deletions(-) diff --git a/examples/supply_chain/learner.py b/examples/supply_chain/learner.py index 401a3614e..9c2ff6110 100644 --- a/examples/supply_chain/learner.py +++ b/examples/supply_chain/learner.py @@ -13,7 +13,7 @@ class SCLearner(DistributedLearner): def _evaluate(self, ep: int): tracker = SimulationTracker(60, 1, self.eval_env, self) loc_path = '/maro/supply_chain/output/' - facility_types = ["product"] + facility_types = ["productstore"] tracker.run_and_render(loc_path, facility_types) def end_of_training(self, ep, **kwargs): diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py index 6d9fe80df..8c2d326f6 100644 --- a/examples/supply_chain/policies.py +++ b/examples/supply_chain/policies.py @@ -66,7 +66,6 @@ def get_base_producer_policy(config): 'productstore': null_policy } - agent2policy = {agent_id: agent_id.split(".")[0] for agent_id in agent_ids} # update schedules @@ -80,4 +79,4 @@ def get_policy_update_schedule(cfg): policy_update_schedule = { policy_id: get_policy_update_schedule(config[policy_id]["update_schedule"]) for policy_id in policy_ids if policy_id in ['consumerstore'] -} \ No newline at end of file +} diff --git a/examples/supply_chain/sc_demo.ipynb b/examples/supply_chain/sc_demo.ipynb index 6594c586a..253b0a47a 100644 --- a/examples/supply_chain/sc_demo.ipynb +++ b/examples/supply_chain/sc_demo.ipynb @@ -9,8 +9,8 @@ "- A facility template defines not only what kinds of agents it contains but also how they perform (class)\n", "- In a facility template, you can also assign agents to different \"agent_type\" to group agents\n", "- For example, the following codes define a store template of a retailer:\n", - "-- Each store can be seen as a facility with a list of \"products\" and \"storage\" capacity\n", - "-- Each product in the store has both \"consumer\" and \"seller\" behaviors to denote that it can place replenishing orders and be sold to customers." + " - Each store can be seen as a facility with a list of \"products\" and \"storage\" capacity\n", + " - Each product in the store has both \"consumer\" and \"seller\" behaviors to denote that it can place replenishing orders and be sold to customers." ] }, { @@ -121,8 +121,10 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ "\"\"\"\n", " RetailerFacility: &raw_data_retailer_facility\n", @@ -138,7 +140,7 @@ " consumer:\n", " class: \"ConsumerUnit\"\n", " seller:\n", - " class: \"RawDataSellerUnit\" ######################\n", + " class: \"RawDataSellerUnit\" \n", " config:\n", " sale_hist_len: 4\n", " config:\n", @@ -194,21 +196,9 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 19, "metadata": {}, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'maro'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0mtorch\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m from maro.rl import (\n\u001b[0m\u001b[0;32m 7\u001b[0m \u001b[0mDQN\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mDQNConfig\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mEpisodeBasedSchedule\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mFullyConnectedBlock\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mNullPolicy\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mOptimOption\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mQNetForDiscreteActionSpace\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[0mStepBasedSchedule\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mUniformSampler\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mModuleNotFoundError\u001b[0m: No module named 'maro'" - ] - } - ], + "outputs": [], "source": [ "import sys\n", "from os.path import dirname, realpath\n", @@ -224,18 +214,27 @@ "from or_policy.eoq_policy import ConsumerEOQPolicy as ProducerEOQPolicy\n", "from or_policy.base_policy import ProducerBaselinePolicy\n", "\n", - "sc_code_dir = dirname(realpath(__file__))\n", - "sys.path.insert(0, sc_code_dir)\n", + "# sc_code_dir = dirname(realpath(__file__))\n", + "sys.path.insert(0, './')\n", "from config import config\n", + "config = config[\"policy\"]\n", + "\n", + "\n", + "class SimpleQNet(QNetForDiscreteActionSpace):\n", + " def forward(self, states):\n", + " states = torch.from_numpy(np.asarray(states)).to(self.device)\n", + " if len(states.shape) == 1:\n", + " states = states.unsqueeze(dim=0)\n", + " return self.component.forward(states)\n", "\n", "def get_dqn_consumer_policy(config):\n", " q_net = SimpleQNet(\n", - " FullyConnectedBlock(**cfg[\"model\"][\"network\"]),\n", - " optim_option=OptimOption(**cfg[\"model\"][\"optimization\"]),\n", - " device=cfg[\"model\"][\"device\"]\n", + " FullyConnectedBlock(**config[\"model\"][\"network\"]),\n", + " optim_option=OptimOption(**config[\"model\"][\"optimization\"]),\n", + " device=config[\"model\"][\"device\"]\n", " )\n", - " experience_manager = UniformSampler(**cfg[\"experience_manager\"])\n", - " return DQN(q_net, experience_manager, DQNConfig(**cfg[\"algorithm_config\"]))\n", + " experience_manager = UniformSampler(**config[\"experience_manager\"])\n", + " return DQN(q_net, experience_manager, DQNConfig(**config[\"algorithm_config\"]))\n", "\n", "def get_eoq_producer_policy(config):\n", " return ProducerEOQPolicy(config)" @@ -250,14 +249,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ - "config = {}\n", "policy_dict = {\n", - " 'consumer': get_dqn_consumer_policy(config),\n", - " 'producer': get_eoq_producer_policy(config)\n", + " 'consumer': get_dqn_consumer_policy(config['consumerstore']),\n", + " 'producer': get_eoq_producer_policy(config['producer'])\n", "}" ] }, @@ -265,24 +263,1927 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Step 5: Training and evaluation" + "# Step 5: Training and evaluation - a complete example" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", + "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", + "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", + "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", + "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", + "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", + "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", + "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", + "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", + "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" + ] + }, + { + "data": { + "text/plain": [ + "(699539.0,\n", + " array([-107446., -337711., -469576., -414720., -350976., -445740.,\n", + " -392867., -340848., -288374., -238377., -308160., -239586.,\n", + " -191989., -131588., -95545., -59955., -24627., -101303.,\n", + " -40223., -82306., -103775., -53078., 7854., 59014.,\n", + " 46774., 29581., 68737., 120257., 178911., 216574.,\n", + " 222380., 127016., 180089., 236694., 157455., 211692.,\n", + " 93297., 148858., 213709., 270959., 325391., 379745.,\n", + " 422295., 447683., 498381., 556115., 445558., 500108.,\n", + " 553042., 606973., 651083., 618500., 617280., 680871.,\n", + " 735990., 631406., 648491., 708376., 767371., 699539.]))" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import os\n", + "import sys\n", + "import yaml\n", + "from os.path import dirname, realpath, join\n", + "\n", + "from maro.rl import EpisodeBasedSchedule, LocalLearner, StepBasedSchedule\n", + "from maro.simulator import Env\n", + "\n", + "sc_code_dir = './'\n", + "sys.path.insert(0, sc_code_dir)\n", + "import yaml\n", + "from os import getenv\n", + "from env_wrapper import SCEnvWrapper\n", + "from exploration import exploration_dict, agent2exploration\n", + "from render_tools import SimulationTracker\n", + "from env_wrapper import SCEnvWrapper\n", + "from policies import policy_dict, policy_update_schedule, get_base_consumer_policy, get_eoq_consumer_policy\n", + "\n", + "\n", + "default_config_path = join(sc_code_dir, \"config.yml\")\n", + "with open(getenv(\"CONFIG_PATH\", default=default_config_path), \"r\") as config_file:\n", + " config = yaml.safe_load(config_file)\n", + " \n", + " \n", + "topology = 'test_or'\n", + "config[\"env\"][\"topology\"] = join(sc_code_dir, \"topologies\", topology)\n", + "\n", + "\n", + "env = SCEnvWrapper(Env(**config[\"env\"]))\n", + "config[\"agent_ids\"] = [f\"{info.agent_type}.{info.id}\" for info in env.agent_idx_list]\n", + "config[\"policy\"][\"consumer\"][\"model\"][\"network\"][\"input_dim\"] = env.dim\n", + "config[\"policy\"][\"producer\"][\"model\"][\"network\"][\"input_dim\"] = env.dim\n", + "config[\"policy\"][\"consumerstore\"][\"model\"][\"network\"][\"input_dim\"] = env.dim\n", + "\n", + "# policy_dict.update({'consumer': get_base_consumer_policy(config['policy']['consumer'])})\n", + "policy_dict.update({'consumer': get_eoq_consumer_policy(config['policy']['consumer'])})\n", + "\n", + "\n", + "agent_ids = config[\"agent_ids\"]\n", + "agent2policy = {agent_id: agent_id.split(\".\")[0] for agent_id in agent_ids}\n", + "agent_to_policy = {agent_id: agent_id.split(\".\")[0] for agent_id in agent_ids}\n", + "\n", + "\n", + "env = SCEnvWrapper(Env(**config[\"env\"]))\n", + "# create a learner to start training\n", + "learner = LocalLearner(\n", + " policy_dict, \n", + " agent2policy, \n", + " env, \n", + " config[\"num_episodes\"], \n", + " policy_update_schedule,\n", + " exploration_dict=exploration_dict,\n", + " agent2exploration=agent2exploration,\n", + " experience_update_interval=config[\"experience_update_interval\"],\n", + " eval_schedule=config[\"eval_schedule\"],\n", + " log_env_metrics=config[\"log_env_metrics\"]\n", + ")\n", + "# learner.run()\n", + "tracker = SimulationTracker(60, 1, env, learner)\n", + "loc_path = './output/'\n", + "os.system(f'rm -rf {loc_path}/*')\n", + "facility_types = [\"productstore\"]\n", + "tracker.run_and_render(loc_path, facility_types)" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RetailerFacility_109_1_productstore.png SupplierFacility_52_3_product.png\r\n", + "RetailerFacility_110_1_consumer.png\t SupplierFacility_53_3_producer.png\r\n", + "RetailerFacility_112_2_productstore.png WarehouseFacility_101_1_product.png\r\n", + "RetailerFacility_113_2_consumer.png\t WarehouseFacility_102_1_consumer.png\r\n", + "RetailerFacility_115_3_productstore.png WarehouseFacility_103_2_product.png\r\n", + "RetailerFacility_116_3_consumer.png\t WarehouseFacility_104_2_consumer.png\r\n", + "SupplierFacility_48_1_product.png\t WarehouseFacility_105_3_product.png\r\n", + "SupplierFacility_49_1_producer.png\t WarehouseFacility_106_3_consumer.png\r\n", + "SupplierFacility_50_2_product.png\t plot_balance.png\r\n", + "SupplierFacility_51_2_producer.png\t plot_reward.png\r\n" + ] + } + ], + "source": [ + "!ls output/" + ] + }, + { + "cell_type": "code", + "execution_count": 30, "metadata": {}, "outputs": [ { - "name": "stderr", + "name": "stdout", "output_type": "stream", "text": [ - "/bin/bash: run.sh: No such file or directory\n" + "Populating the interactive namespace from numpy and matplotlib\n" ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABwgAAALQCAYAAACExiRjAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAEAAElEQVR4nOzdd3xV9f3H8dfJBhISshc7O4Gwkb0REFHAvaoWF1XrtlbrzzraOluxda+KW6aCiGyRPQMkJGGP7EH2vvf8/riBIgIyktyM9/Px4AGce+45n5tAcnPe5/P5GqZpIiIiIiIiIiIiIiIiIiItg4O9CxARERERERERERERERGRhqOAUERERERERERERERERKQFUUAoIiIiIiIiIiIiIiIi0oIoIBQRERERERERERERERFpQZzsXYCIiIiIiIiIiIiIiIjUvS1btvg7OTm9D8ShprGWyArsqqmpmda7d+/skx9QQCgiIiIiIiIiIiIiItIMOTk5vR8YGBjt5+d3zMHBwbR3PdKwrFarkZOTE5OZmfk+MOnkx5QWi4iIiIiIiIiIiIiINE9xfn5+RQoHWyYHBwfTz8+vEFsH6S8fs0M9IiIiIiIiIiIiIiIiUv8cFA62bLWf/1/lgQoIRUREREREREREREREpEE8++yz/sXFxReUTz300EPBTz/9dEBd19QSKSAUERERERERERERERGRBvHOO+8ElJSUKJ+yM30CREREREREREREREREpM4VFRU5DB8+PCwyMjImPDw89uGHHw7Kzs52HjZsWET//v0jAN555x3viIiImPDw8Nh77rkn5PhzZ82a1TYmJiY6MjIyZsCAARGnHvvVV1/1HTp0aHhJSYnRkK+puXCydwEiIiIiIiIiIiIiIiLS/MyZM6dtYGBg9cqVK/cC5OXlOX755Ze+q1atSg0KCqo5ePCg8zPPPBOyZcuW3X5+fjVDhgyJmDlzpteoUaNK7r333k4rV65MjoqKqsrKynI8+bh/+9vf/JYuXeq5ePHiva1atdIaixdAAaGIiIiIiIiIiIiIiEgz9+ishPapmcWt6/KYEYEeZS9fFX/kTI/36tWr/Mknn2x/zz33hFxxxRWF48aNKzn58Z9//rnNJZdcUhwcHFwDcO211+avWrXK3dHR0ezXr19xVFRUFUBAQIDl+HO++uorn6CgoKrFixfvc3V1VTh4gTRiVEREREREREREREREROpc9+7dK7du3ZrUrVu38ieffDLkkUceCTr5cdM8fb5nmiaGcfrJoZGRkeVHjx51PXDggHPdV9xyqINQRERERERERERERESkmTtbp199OXjwoLO/v3/N9OnT8z08PKz//e9/fdq0aWMpLCx0CAoKYujQoaWPP/54+4yMDCc/P7+ab775xnv69OnZI0aMKH344Yc7JicnuxwfMXq8i7BHjx5lf/jDH3ImTZoU9uOPP+7p1KlTdUO/ruZAAaGIiIiIiIiIiIiIiIjUuS1btrR64oknQh0cHHBycjLffPPNQ6tXr3YfP358uL+/f/WGDRtSn3766bRhw4ZFmKZpjBo1qvCmm24qAJgxY8bByZMnh1mtVnx8fKrXrl275/hxL7300pK///3vR8ePHx++fPny1KCgoBq7vcgmyjhT+6aIiIiIiIiIiIiIiIg0XQkJCQfj4+Nz7V2H2FdCQoJvfHx8p5O3aQ1CERERERERERERERERkRZEAaGIiIiIiIiIiIiIiIhIC6KAUERERERERERERERERKQFUUAoIiIiIiIiIiIiIiIi0oIoIBQRERERERERERERERFpQRQQioiIiIiIiIiIiIiIiLQgCghFREREREREREREREREWhAFhCIiIiIiIiIiIiIiItIoPPTQQ8FPP/10wMUe57PPPvP885//HHi+z0tJSXF5++23vS/2/L/l5PpmzpzptWXLFrf6PufJnBryZCIiIiIiIiIiIiIiIiIAVqsV0zRxdHS84GNUV1fj7Oz8q+033nhjIVB4vsfbs2eP61dffeV9991355/ruS7EyfXNmzfPq6amprB3794VdXLwc6AOQhEREREREREREREREakXzzzzTEB4eHhseHh47LPPPuufkpLi0qVLl9ibbrqpQ2xsbMy+fftcHn/88cBOnTrFDRw4MGLPnj2ux5+bmJjoOmTIkPDY2Njo3r17R27bts0NYOrUqZ2mTZsW2r9//4jp06eHnu68M2bM8Lnllls6HN//1ltvbd+zZ8+o0NDQbh999FG7M9X75JNPhmzevNk9Kioq5q9//av/jBkzfMaPH99l5MiRYUOGDIkoLCx0GDBgQERMTEx0REREzKeffuoFts7DLl26xF533XUdw8LCYgcNGhReUlJiADz//PP+Xbt2jY2IiIiZOHFil5PrW7JkSZulS5d6PfXUU6FRUVExiYmJrmeqrS6pg1BERERERERERERERKS5m/eH9mQnta7TY/rHlHHlf46c6eHVq1e3/vzzz322bNmy2zRNevfuHT1q1KjigwcPur333nsHP/3008OrV69uPXfuXO+dO3cmVVdX06NHj5iePXuWAUybNq3ju+++e6hbt26Vy5cvb3PPPfd0WL9+fSrAvn373NasWZPq5HRuUVdWVpbz5s2bk7dv3+42efLksNtuu+3Y6fZ74YUX0l599dWAFStW7AVbkLd161b3HTt2JAYEBFiqq6tZuHDhXm9vb2tGRoZT//79o2644YYCgMOHD7t9+umn+wcOHHhowoQJXT755JN206dPz58xY0bgoUOHdrZq1crMzc39RbvkmDFjSkePHl0wceLEwjPVVB8UEIqIiIiIiIiIiIiIiEidW7lypfuECRMK2rZtawW47LLLjq1YscIjKCioatSoUaUAK1ascJ8wYUKBh4eHFWDs2LEFAIWFhQ7btm1zv/rqq7seP15VVZVx/M9Tpkw5dq7hIMCkSZMKHB0d6d27d0VeXt55zQkdMmRIUUBAgAXAarUaDzzwQOj69evdHRwcyM7Odjl69KgTQEhISOXAgQPLAXr27Fl28OBBV4DIyMjyyZMnd540aVLBjTfeWHA+564vCghFRERERERERERERESau7N0+tUX0zRPu71169bWk/9uGMav9rFYLHh4eNQkJycnne4Y7u7u1tNtPxM3N7cTxZyprjM5ud533nnHOy8vz2nnzp27XV1dzZCQkG7l5eUOAC4uLicO7OjoaB7fvmLFij2LFi3ymDdvntdLL70UvGfPnl3nVUA90BqEIiIiIiIiIiIiIiIiUudGjhxZ8v3333sVFxc7FBUVOXz//fftRowYUXzqPgsXLvQqKSkxjh075rBkyRIvAG9vb2toaGjVhx9+2A7AarWybt26VvVds6enp6WkpMTxTI8XFhY6+vr6Vru6uprfffedR3p6usvZjmexWNi3b5/L5ZdfXvzmm28eLS4udiwsLPzF8d3d3S1FRUUNmtmpg1BERERERERERERERETq3ODBg8tuuOGGvF69ekUD3HzzzTm+vr6WU/eZPHlyflxcXGxISEhlv379So4/9sUXX+y/4447Or744otBNTU1xuTJk/MHDBhQXp819+vXr9zJycmMjIyMueGGG3LbtWv3i3qnTZuWP378+LC4uLjo2NjYss6dO1ec7Xg1NTXGDTfc0Lm4uNjRNE3jrrvuyjr1Y3DjjTfm33PPPZ3efvvtgFmzZu2LjY2trI/XdjLjfNsoRUREREREREREREREpPFLSEg4GB8fn2vvOsS+EhISfOPj4zudvE0jRkVERERERERERERERERaEI0YFRERERERERERERERkSbp9ddf93nrrbcCTt7Wt2/fkpkzZx4+2/M2btzY6pZbbul88jYXFxfrjh07kuujzsZGI0ZFRERERERERERERESaIY0YFdCIUREREREREREREREREZEWTwGhiIiIiIiIiIiIiIiISAuigFBERERERERERERERESkBVFAKCIiIiIiIiIiIiIiItKCKCAUERERERERERERERGRRuGhhx4KfvrppwMu9jgLFizwWLJkSZu6qKl169Y9AQ4ePOg8bty4LmfaLzc31/Ef//iH39mO1bNnz6jj9Y0YMSLsfOqYOXOm15YtW9zO5zlnooBQREREREREREREREREGpzVasVisVzUMaqrq0+7ffny5R6rV692v6iDn6JTp07VP/zww/4zPZ6Xl+f4wQcf+J/usZqaGgC2bduWfKHnnzdvnteOHTtaXejzT+ZUFwcRERERERERERERERGRxusva/7Sfu+xva3r8phh7cLKnhv03JGz7fPMM88EfPbZZ74AN998c861115bMH78+PCBAwcWb9myxX3+/Pl7P/jgA++vvvrKNzg4uMrHx6e6Z8+eZQCJiYmud999d4f8/HwnNzc36/vvv3+oZ8+eFVOnTu3Url27mp07d7bu3r172XvvvXf05HOmpKS4fPLJJ34ODg7m119/7fOvf/3rcJcuXap+97vfdcrLy3Py8fGp+eSTTw6Gh4dXna7m5ORkl+uuu65LTU2NMWrUqMKTjztx4sTwPXv2JG7evNnttttu61xdXW1YrVZmz56974knngg5cuSIa1RUVMywYcOKLr/88sLnnnsuyN/fvzopKan1vn37Elu3bt2zrKxsG0BxcbHjmDFjuu7fv9+tf//+xTNnzjzs6OjIyft89NFH7RYsWOB599135yxdutRr/fr1Hi+++GLQ7Nmz9wGc7uNzLp87dRCKiIiIiIiIiIiIiIhInVu9enXrzz//3GfLli27N2/evPuTTz7xy83NdTx48KDbbbfdlrd79+6krKwsp7lz53rv3LkzacGCBXsTEhJOjAWdNm1axzfffPNwYmLi7pdffvnoPffc0+H4Y/v27XNbs2ZN6qnhIEBkZGTVLbfcknP33XdnJScnJ40bN67k7rvv7nDDDTfkpaamJl177bV599xzT/sz1T19+vQO06ZNy9m1a9fuwMDA07YovvHGG37Tp0/PSk5OTtqxY8fuzp07V7366qtH27dvX5mcnJz0zjvvHAXYsWNHm5dffjlt3759iaceY+fOnW1ef/31IykpKYkHDx50/eSTT9qdqaYxY8aUjh49uuD5558/mpycnBQbG1t5to/Pb1EHoYiIiIiIiIiIiIiISDP3W51+9WHlypXuEyZMKGjbtq0V4LLLLju2YsUKj6CgoKpRo0aVAqxYscJ9woQJBR4eHlaAsWPHFgAUFhY6bNu2zf3qq6/uevx4VVVVxvE/T5ky5ZiT07nHXNu2bWuzaNGifQD33HNP/l//+tfQM+27detW9+P73nXXXXnPPffcr/YdMGBA6SuvvBJ09OhRl+uuu+5Yt27dKk93rO7du5dGRUWdtlOxW7dupTExMVUA11xzTf7q1avdb7vttmPn8np+6+PzWxQQioiIiIiIiIiIiIiISJ0zTfO021u3bm09+e+G8etcy2Kx4OHhUZOcnJx0umO4u7tbT7e9rjg4OJy++Fp33313/pAhQ0rnzp3rOX78+Ig333zzYGRk5K9CwlNf68lOfd3H/37y9vLy8tOGfr/18fktGjEqIiIiIiIiIiIiIiIidW7kyJEl33//vVdxcbFDUVGRw/fff99uxIgRxafus3DhQq+SkhLj2LFjDkuWLPEC8Pb2toaGhlZ9+OGH7QCsVivr1q1rda7n9vDwsBQXFzse/3vPnj1L33///XYA77zzjnefPn1KzvTcXr16lbz33nveAO+9957P6fZJSkpyiY6Ornzqqaeyx44dW7B9+/ZWnp6eltLS0nPO3nbu3NkmOTnZxWKxMGvWLO8hQ4YUA/j4+FRv3brVzWKxMH/+/BNjR93d3S1FRUUOcPEfHwWEIiIiIiIiIiIiIiIiUucGDx5cdsMNN+T16tUrunfv3tE333xzjq+vr+XUfSZPnpwfFxcXO3HixK79+vU7Edx98cUX+z/66CPfyMjImPDw8NjZs2d7neu5p06dWrBw4UKvqKiomB9++MH9rbfeOjxz5kzfiIiImC+++MLnzTffPOPI1TfffPPwu+++6x8XFxddWFjoeLp9Zs6c6R0REREbFRUVs2fPHre77rorLzAw0NK7d++S8PDw2LvuuuuMI0yP69GjR8nDDz8cGhEREduhQ4fKm2++uQDgr3/9a9oVV1wRNmDAgMiAgIATayDeeOON+TNmzAiMjo6OSUxMdL2Yj49xpvZOERERERERERERERERaboSEhIOxsfH59q7DrGvhIQE3/j4+E4nb1MHoYiIiIiIiIiIiIiIiEgL4mTvAkREREREREREREREREQuxOuvv+7z1ltvBZy8rW/fviUzZ848/FvPffzxxwPnz5/vffK2K664Iv/FF1/MrOs6GxuNGBUREREREREREREREWmGNGJUQCNGRURERERERERERERERFo8BYQiIiIiIiIiIiIiIiIiLYgCQhEREREREREREREREZEWRAGhiIiIiIiIiIiIiIiI1AtHR8feUVFRMZGRkTExMTHRS5YsafNbz2ndunXPhqitJXOydwEiIiIiIiIiIiIiIiLSPLm6ulqTk5OTAGbPnt32z3/+c+iYMWNS7F1XS6cOQhEREREREREREREREal3hYWFjp6enjW1f3YYMGBARExMTHRERETMp59+6nWa/U+7T0pKikuXLl1ir7vuuo5hYWGxgwYNCi8pKTEAdu3a5Tpw4MCI4x2LiYmJrgB/+ctfAuLi4qIjIiJiHnzwweCGe9WNkzoIRUREREREREREREREmrlHZyW0T80sbl2Xx4wI9Ch7+ar4I2fbp7Ky0iEqKiqmsrLSyM3Ndf7+++9TAVq3bm1duHDhXm9vb2tGRoZT//79o2644YYCB4f/9badaR+Aw4cPu3366af7Bw4ceGjChAldPvnkk3bTp0/Pv+GGGzo/8sgjmbfccktBWVmZYbFYjDlz5rTdu3ev244dO3abpsno0aPDFi1a5D5+/PiSuvx4NCUKCEVERERERERERERERKRenDxidOnSpW1uu+22zqmpqYlWq9V44IEHQtevX+/u4OBAdna2y9GjR506dOhQc/y5Z9oHICQkpHLgwIHlAD179iw7ePCg67FjxxyysrJcbrnllgKA1q1bm4D5ww8/tP3pp5/axsTExACUlZU5JCcnuykgFBERERERERERERERkWbrtzr9GsLo0aNLjx075pSRkeE0e/Zsz7y8PKedO3fudnV1NUNCQrqVl5f/Ymm8d955x/tM+7i4uJjH93N0dDTLy8sdTNM89ZQAmKbJAw88kPHoo4/m1usLbEK0BqGIiIiIiIiIiIiIiIjUu23btrlZrVYCAgJqCgsLHX19fatdXV3N7777ziM9Pd3l1P3PZZ+TeXt7WwMDA6tmzpzpBVBeXm4UFxc7jB8/vmjmzJm+hYWFDgAHDhxwTktLa9FNdC36xYuIiIiIiIiIiIiIiEj9Ob4GIdg6+d56662DTk5OTJs2LX/8+PFhcXFx0bGxsWWdO3euOPW557LPqT799NMDd9xxR8fnnnsu2NnZ2fzmm2/2TZkypSgxMdGtb9++UWBb2/Czzz47EBISUvNbx2uujDO1W4qIiIiIiIiIiIiIiEjTlZCQcDA+Pl5jNVu4hIQE3/j4+E4nb9OIUREREREREREREREREZEWRAGhiIiIiIiIiIiIiIiISAuigFBERERERERERERERESkBVFAKCIiIiIiIiIiIiIiItKCKCAUERERERERERERERERaUEUEIqIiIiIiDRzhmE8YxjGp/auoy4ZhjHcMIyjjaCOPxuG8X4dH7NRvDYREREREWm+FBCKiIiIiIicxDCMwYZhrDUMo9AwjHzDMNYYhtG39rFbDcP4+aR929Y+PtswjHDDMEzDMJxOOd7HhmE8f4ZzeRmG8aFhGJmGYRQbhpFqGMbjJz1uGoYRVl+v9Qw1hda+ntzaj8FOwzBurX2s0+le428c76BhGKPrreA6UPs5qjIMo+SkXwnn8lzTNP9mmua0+q5RRERERKSpcnR07B0VFRUTFhYWGxkZGfPMM88EWCwWe5cFwEMPPRT89NNPB9i7Dns45x/qREREREREmjvDMNoCC4B7gK8BF2AIUHmafdsBi4G9wC1A6AWc8p9AGyAaKAQigLgLqb0OzQQSgI7YXnc3INCuFTWMl0zTfMreRYiIiIiINDeurq7W5OTkJIC0tDSnq6++ukthYaHjP//5z3R719aSqYNQRERERETkfyIATNP8wjRNi2ma5aZp/mia5o6TdzIMwxdYDiQCN5mmWXOB5+sLfG6a5jHTNK2maSabpjmr9hw/1e6TUNvRdm3t9jsMw9hb2934rWEYwSfVFWsYxpLax7IMw/jzqSc0DMPZMIwvarsEXc5Q08emaZaaplljmuY20zQX1T52vKaC2poGGIbR1TCM5YZh5NV2HX5mGIZX7blmAh2A72r3f+x04zNP7jI0DKOfYRibDcMoqn0Nr53tA1g74jO39hg31m7rW/tcp5P2m2oYxvazHesMxz/eNXmnYRjphmFkGIbx8EmPnxjfahiGm2EYn9Z+LAoMw9hkGEZA7WPBtZ+v/NrP3x0nHaNVbRfjMcMwkmo/ByIiIiIizU5ISEjN+++/f/Cjjz7yt1qt1NTUcNddd4XGxcVFR0RExLz88su+AAsWLPDo27dv5IQJE7p06tQpbvr06SFvvfWWd7du3aIjIiJiEhMTXQE+//xzz+7du0dFR0fHDBw4MOLIkSNOYOsMvPrqqzv169cvMjQ0tNvzzz/vf7yGxx9/PLBTp05xAwcOjNizZ4+rfT4S9qcOQhERERERkf9JBSyGYfwX+BJYb5rmsVP28QZWAauBe0zTNC/ifOuBF2q7EX82TXPP8QdM0xxqGIYJxJumuRfAMIyRwN+BsdjCyVdq6xxqGIYHsLR22+WAMxBz8skMw2gFzAJysAWbp5vrsx74j2EYbwBrTdM8fNJjQ4EDgNfxULR2BOrfsYWHbYHZwDPAA6Zp3mwYxhBgmmmaS2v3H/4bH5PXgddN05xpGIY7Z++oDAR8gRDgEuB7wzA2m6a5yTCMPGAMcDzcvAlbd+SFGgGEA12A5YZhJBx/TSf5HeAJtMfWfdkDKK997Atsn7NgIApYYhjGftM0lwH/B3St/dXmpJpFREREROrMo7MS2qdmFreuy2NGBHqUvXxV/JHzeU5MTEyV1WolLS3N6auvvvLy9PS07Nq1a3d5ebnRt2/fqMsvv7wIIDk5udWsWbP2+/v713Ts2LGbq6tr7s6dO3c/99xz/q+++qr/hx9+eGTMmDEl1113XbKDgwOvvfaa77PPPhv43nvvHQXYu3ev29q1a1MKCgoco6Oj4x599NGcjRs3tpo7d673zp07k6qrq+nRo0dMz549y+ryY9JUqINQRERERESklmmaRcBgwATeA3Jqu75OXpOiPbZOw48uMhwEuA/4DLgXSKrtLBt/lv1vBD40TXOraZqVwBPAAMMwOgETgUzTNF81TbPCNM1i0zQ3nPTctsAPwD7gtjOEgwBXYws//wIcMAxju1G7BuPpmKa51zTNJaZpVpqmmQO8Bgw7lxd/BtVAmGEYvqZplpimuf439v9L7blXAQuBa2q3/xdbKIhhGN7ApcDnZznOI7Vdf8d//feUx/9a21W5E/gIuP4MtfsAYbUdqFtM0ywyDKM9tn9Xj9d+brYD7wM31z7vGuAF0zTzTdM8Asz4jdcsIiIiItKkHf9RaunSpW2//vprn6ioqJiePXtGHzt2zCkpKckNoFu3bqUdO3asbtWqldmhQ4fK8ePHFwLEx8eXHz582AXgwIEDLkOGDAmPiIiImTFjRmBycnKr4+cYO3ZsQatWrcygoKAab2/v6qNHjzqtWLHCfcKECQUeHh5Wb29v69ixYwsa/tU3DuogFBEREREROYlpmruBWwEMw4gCPgX+xf8CoQTgG2CRYRijTNPcVrv9+JhR55P+fPzv1Wc4VznwN+Bvtesf/gn4xjCMDqZp5p/mKcHA1pOeX1LbKReCLbjcd5aXdkltLdefLdis7Zj8E/Cn2lGqrwDzDMM47RqLhmH4Ywu0hgAe2G5EPbXr8nz8HngWSDYM4wC2YG7BGfY9Zppm6Ul/P4TtYwS2z9vu2i7Ea4DVpmlmnOW8r/zGGoQn3xV9CNvajKeaie3z8GXtmNVPgSdra8o3TbP4lGP0qf1z8GmOLyIiIiJSp86306++JCUluTg6OhISElJjmqbx6quvHp46dWrRyfssWLDAw9XV9cTPLQ4ODri5uZnH/2yxWAyAe++9t8Mf//jHzBtvvLFwwYIFHs8+++yJJRhOfr6joyM1NTUGgGEY9f0SmwR1EIqIiIiIiJyBaZrJwMecMubSNM3XgX9gGxN5/LEMbEFgp1MO05lzCHxquxf/hm3EZOcz7JYOdDz+F8Mw2mDrWEvDFjB1PcspfsQ2CnTZKR2RZ6spF1tAGIxttOrpgsW/127vbppmW2xdeyf/xH3qc0qBE2ONDMNwBPxOOuce0zSvB/yBF4FZta/zdNqd8lgHbB8jTNNMA9YBk7F16l3MeFGwBX+/Os/JTNOsNk3zr6ZpxgADsXV13lK7r3ftGNiTj5FW++eM0xxfRERERKTZSU9Pd7rjjjs63nbbbdkODg6MGTOm8K233vKrrKw0AHbs2OFaVFR0ztlVcXGxY4cOHaoBPv74Y5/f2n/kyJElCxcu9CopKTGOHTvmsGTJEq8LfjFNnAJCERERERGRWoZhRBmG8fDxbrna0ZDXY1uX7xdM03wJ23p5Sw3DiKwd2Tkb25qCPoZhOBuGcT22dQBPu6acYRh/MQyjr2EYLoZhuAF/BAqAlNpdsrCteXfc58BthmH0MAzDFVuguME0zYPAAiDQMIwHDMNwNQzDwzCM/qep+XNsIaHvGWp60TCMOMMwnGoDrXuAvaZp5mFbu9B6Sk0eQAlQYBhGCPDoKYc89TWkAm6GYVxmGIYz8BTgetL5bzIMw880TWvtxwLgTONQAf5a+/Ebgi2Q++akxz4BHsPW7Tf3LMc4F38xDKO1YRixwG3AV6fuYBjGCMMwutWGnkXYAmNL7djQtcDfDcNwMwyjO7ZOyc9qn/o18IRhGO1q/+3dd5G1ioiIiIg0GpWVlQ5RUVExYWFhsSNGjIgYNWpU0SuvvJIO8OCDD+ZGRUVVdOvWLTo8PDz2jjvu6FhdXX3OLX5PPvlk+vXXX9+1d+/ekT4+PjW/tf/gwYPLJk+enB8XFxc7ceLErv369Su5mNfWlBkXv2SGiIiIiIhI81AbcP0TGAR4YQuoFgCP1q4ldyswzTTNwSc953lsI0mHAfnAy8B4bF1yScBjpmmuOcP5ngKuw9YxVgPsAP5smuba2sfvBv4PaAXcaZrm17XbHgXaYQud7jZN82jt/nHYQsteQCXwL9M0/2EYxjPY1sU7vibf89jCtJGnjjI1DOMNYBwQBJQDG2pf/+7ax5/FFho61+5XjC2IiwT2YuvUe9A0zeMh6xXAG9jWQHzeNM1Xaj+OfwccgZewrcE4zTTNpYZhfAqMrf34HQKeNE1z3mk+dsOxjfB8C3gQKKvdd+ZJ+7QGMoG5pmn+7nSfg9r9PgZuAKpO2lxhmqZv7fqOB4C7gGew3Wj7Wm3Yyskf29pA+BkgFFto+hXwkGmaNbXB39vYOguPAS+bpvn2SXW+DUzC1m34EfDH4x9DEREREZELlZCQcDA+Pj7X3nWIfSUkJPjGx8d3OnmbAkIRERERERFptgzD2AfcZZrm0gt8fidsAaGzaZq/eUeyiIiIiEhjooBQ4PQBoUaMioiIiIiISLNkGMZUbGsgLrd3LSIiIiIiIo2Jk70LEBEREREREalrhmGsxLb+48216xmKiIiIiIhILQWEIiIiIiIi0uyYpjm8jo5zEDDq4lgiIiIiIiKNhUaMioiIiIiIiIiIiIiIiLQgCghFREREREREREREREREWhCNGBWxM19fX7NTp072LkNEREREREREREREzmLLli25pmn62buOpsbR0bF3eHh4ucViMdq3b1/59ddfH/D19bU0dB39+vWLfOWVV44MHTq0rKHP3RgpIBSxs06dOrF582Z7lyEiIiIiIiIiIiIiZ2EYxiF719AUubq6WpOTk5MApkyZ0unll1/2e/HFFzPr85zV1dU4OzvX5ymaPI0YFRERERERERERERERkXp3ySWXlKalpbkAJCYmug4ZMiQ8NjY2unfv3pHbtm1zq6mpITQ0tJvVaiU3N9fRwcGh96JFi9wBevfuHblr1y7XFStWtO7Zs2dUdHR0TM+ePaMSEhJcAWbMmOEzfvz4LiNHjgwbMmRIRElJiTFx4sQuERERMZdddlmXiooKw56vvbFRB6GIiIiIiIiIiIiIiEhzd/vt7dm1q3WdHjMurowPPzxyLrvW1NSwYsUKj9///ve5ANOmTev47rvvHurWrVvl8uXL29xzzz0d1q9fn9q5c+eKrVu3uu3Zs8c1JiambOXKle7Dhw8vzczMdImLi6vMz8932LhxY7KzszPz5s3zeOyxx0IXL168D2Dr1q3uO3bsSAwICLA888wzAa1atbKmpqYmbdiwodWgQYNi6vS1N3EKCEVERERERERERERERKReVFZWOkRFRcWkpaW5xMXFlV155ZVFhYWFDtu2bXO/+uqrux7fr6qqygAYOHBg8bJlyzwOHDjg+uijj2Z88MEHfj/99FNJfHx8KUB+fr7jtdde2/ngwYNuhmGY1dXVJzoDhwwZUhQQEGAB+Pnnn93vv//+bID+/fuXR0REaO3BkyggFJHm5dOrYP/Kuj9uKy+4Yzl4daj7Y4uIiIiIiIiIiIjUt3Ps9Ktrx9cgzMvLcxw7dmzYP/7xD//p06fnenh41Bxfm/Bkw4cPL3nzzTf9srKyXF577bW0f/7zn4HLli3zGDx4cDHA448/HjJs2LDiJUuW7EtJSXEZOXJk5PHntm7d2nrysQxDU0XPRAGhiDQfeftg7xKIGA/+0XV3XNMK6/4Da16Hy16tu+OKiIiIiIiIiIiItBA+Pj6WGTNmHL7qqqvCHn300ZzQ0NCqDz/8sN3tt99+zGq1smHDhlYDBgwoHz58eOm0adM6t2/fvrJ169ZmbGxs2SeffOI3d+7cPQBFRUWOoaGhVQDvvPOO75nON3jw4JJPP/3U+/LLLy/etGmTW2pqat2OV23iFBCKSPOROMf2+2WvgGdo3R67PB+2zoShj4JHYN0eW0RERERERERERKQFGDRoUHl0dHT5+++/3+6LL77Yf8cdd3R88cUXg2pqaozJkyfnDxgwoLxVq1ZmYGBgVZ8+fUoBhgwZUvLtt9969+vXrxzg8ccfz5w2bVrnGTNmBA4ZMqToTOd65JFHsq+77rrOERERMbGxsWXdunUrbajX2RQYpmnauwaRFq1Pnz7m5s2b7V1G8/DmAHBtC79fXPfHztsH/+4Dl0yHS1+o++OLiIiIiIiIiIhIo2YYxhbTNPvYu47zkZCQcDA+Pj7X3nWIfSUkJPjGx8d3Onmbg51qERGpW9m7ITsJ4qbWz/F9ukLcVbD5IyjNq59ziIiIiIiIiIiIiIg0AAWEItI87JoDhgPEXFF/5xjyEFSXwoa36u8cIiIiIiIiIiIiIiL1TAGhiDR9pmlbf7DTYPAIqL/z+EdD9OWw4V2oKKy/84iIiIiIiIiIiIiI1CMFhCLS9GXugLy9EDul/s815GGoLISN79X/uUREREREREREREQujtVqtRr2LkLsp/bzbz11uwJCEWn6ds0GByeInlT/5wruCWFjYP2bUFVa/+cTERERERERERERuXC7cnJyPBUStkxWq9XIycnxBHad+piTHeoREak7pgm75kKXEdDGp2HOOfQR+PBS2PIxDPhDw5xTRERERERERERE5DzV1NRMy8zMfD8zMzMONY21RFZgV01NzbRTH1BAKCJN29HNUHgYRjzRcOfscAl0GgJr34A+vwdnt4Y7t4iIiIiIiIiIiMg56t27dzbQAKPXpKlRWiwiTVviHHB0gajLGva8Qx6G4gzY/lnDnldERERERERERERE5CIpIBSRpstqhcS5tjUB3Twb9txdhkNIH1jzL7BUN+y5RUREREREREREREQuggJCEWm6Dq+zdfHFTWn4cxuGbS3CgsOw85uGP7+IiIiIiIiIiIiIyAVSQCgiTdeu2eDUCiLG2ef8EeMgoBusfg2sFvvUICIiIiIiIiIiIiJynhQQigCGYXxoGEa2YRi7TtrmbRjGEsMw9tT+3u6kx54wDGOvYRgphmFcetL23oZh7Kx9bIZhGEZDv5YWw1IDSfMhchy4utunBsOAIQ9B3h5bLSIiIiIiIiIiIiIiTYACQhGbj4FT29D+BCwzTTMcWFb7dwzDiAGuA2Jrn/OmYRiOtc95C7gTCK/9ZafWthbg4E9QlguxdhgverKYK8AnHFa/CqZp31pERERERKRJqai2MPWttdw9cwvfJaRTVlVj75JEREREpIVwsncBIo2BaZo/GYbR6ZTNVwDDa//8X2Al8Hjt9i9N06wEDhiGsRfoZxjGQaCtaZrrAAzD+AS4ElhUz+W3TLvmgIsHhI+xbx0OjjDkYZh3N6T+AJHj7VuPiIiIiIg0GYsTM9ly6BherZ35ITETN2cHRkUFcFn3IEZE+tPKxfG3DyIiIiIicgEUEIqcWYBpmhkApmlmGIbhX7s9BFh/0n5Ha7dV1/751O1S12qqYPe3EDUBnFvZuxrodhWs/Bv89IptXUJNlhURERERkXMwa8tRQrxaserR4Ww5dIyFOzP4fmcmC3dm0MrZkVHR/kzsHsTwSH/cnBUWioiIiEjdUUAocv5Ol/6YZ9n+6wMYxp3YRpHSoUOHuquspdi/AioKIW6qvSuxcXSGwQ/Cggdh/0roOsLeFYmIiIiISCOXXlDOz3tzuW9kOE6ODvTv4kP/Lj783+WxbDyQz4Id6fywK5MFOzJo4+LIqGhbZ+GwCD+FhSIiIiJy0RQQipxZlmEYQbXdg0FAdu32o0D7k/YLBdJrt4eeZvuvmKb5LvAuQJ8+fbRw3fnaNRvcvKBLIwrietwIq16yrUWogFBERERERH7D3G1pmCZM7fXLwTOODgYDuvowoKsPf50Uy4YD+SzYkcEPuzL4NiEdd1cnxsQEcFm3IIZE+OLqpLBQRERERM6fAkKRM/sW+B3wj9rf55+0/XPDMF4DgoFwYKNpmhbDMIoNw7gE2ADcArzR8GU3c9XlkPw9xF4JTi72ruZ/nFxh4P2w+Ak4vB46XGLvikREREREpJEyTZNZW47Sr7M3HX3anHE/J0cHBoX5MijMl2eviGX9/jwW7sjgh8RM5m5Lw8PViTGxAUzsHsTgMD9cnBwa8FWIiIiISFOmgFAEMAzjC2A44GsYxlHg/7AFg18bhvF74DBwNYBpmomGYXwNJAE1wB9M07TUHuoe4GOgFbCo9pfUpT1LoKoY4qbYu5Jf6/07WP2KbS3Cm2bZuxoREREREWmkth4+xoHcUu4Z3vWcn+Ps6MCQcD+GhPvx3JVxrNmby8IdGSxOzGTO1jTaujkxNjaQy7oHMTjMF2dHhYUiIiIicmaGaWq6oYg99enTx9y8ebO9y2g6vrkVDqyGh1PAsRHe47D6VVj2LNy5EoJ72rsaERERERFphJ6Ys5N529LY9NRo3F0v7ueaqhora/bmsmBHBj8mZVJcUYNnK2fGxAQwOtqfweF+F30OERERsTEMY4tpmn3sXYdIXdA7RBFpOipLIOUH6Hlj4wwHAfpOg59ftwWF135q72pERERERKSRqai2sCAhnfHdAuskuHNxcmBElD8jovyprInj5z21YWFiJrO2HMXZ0aB/Zx9GRvkzMsqfTr5nHmkqIiIiIi1HI73CLiJyGqk/QE05xDbC8aLHuXlC/7vgp5cgezf4R9u7IhERERERaUQWJ2ZSXFnDVb1D6/zYrk6OjIoOYFR0ANUWK1sOHWNFcjbLkrN5dkESzy5IootfG0ZG+jMy2p++nbw1ilRERESkhdKIURE704jR8/DFDZC+FR5MAodG/ENsWT78Mw6iLoOp79m7GhERERERaURu/mAD+3NKWf3YCBwcjAY77+G8MpYnZ7E8JYf1+/KosljxcHViaIQfI6P8GR7ph4+7a4PVIyIi0hRpxKg0J+ogFJGmobwA9i6xjfBszOEgQGtv6Hs7rPsPDP8T+HS1d0UiIiIiItIIZBSW8/PeXO4bGd6g4SBAB5/W3DqoM7cO6kxpZQ1r9uayPDmb5cnZLNyZgWFAj/ZeJ7oLY4LaYhgNW6OIiIiINBwFhCLSNKR8D5YqiJtq70rOzYB7YcO7sOZfMOkNe1cjIiIiIiKNwJytaZgmTO0VYtc62rg6MTY2kLGxgZimSWJ6EctrR5G+tjSVV5ekEtjWjRG16xYOCvOhtYsuIYmIiIg0J3p3JyJNw67Z4NUBQnrbu5Jz4xEIvW6BLR/DsMfBs+7XFxERERERkabDNE1mbzlKv87edPRpY+9yTjAMg7gQT+JCPLl/VDg5xZWsTMlmRUo23yWk88XGw7g4OTCgiw+jom2BYWi71vYuW0REREQukgJCEWn8SvNg/0pbV15TGnEz6I+w5SNYMwMmvGTvakRERERExI62Hi5gf24pdw9v3EsQ+Hm4cnWf9lzdpz1VNVY2H8xnWXI2K5KzeXp+Ik/PTyQ6qC2jo/0ZHR1AtxDPBh+XKiIiIiIXTwGhiDR+u78Faw3ETbF3JefHqz3EXwdb/wtDHwF3f3tXJCIiIiIidjJry1FaOTsyoVuQvUs5Zy5ODgwM82VgmC9/mRjD/pwSlidnsyQpi/+s2Msby/fi5+F6IiwcFOaLm7OjvcsWERERkXOggFBEGr/EOeATBoHd7V3J+Rv8EGz/HNb9G8Y8a+9qRERERETEDiqqLSxISGd8t0DcXZvupZgufu508XNn2pAuHCutYmVqNkt3Z/NdQgZfbDyCm7MDg8P8GB3tz8hof/w93OxdsoiIiIicQdN9VyoiLUNxFhz8GYY80rTGix7n0xVip8CmD2DQA9Da294ViYiIiIhIA1ucmElxZQ1X9W4+a5O3a+PC5J6hTO4ZSlWNlQ0H8li229ZduHR3FgA92nvZugtjAogM8MBoij/TiYg0UUuSslizN5dBYb4M7OpDmyZ8g4qI1A/DNE171yDSovXp08fcvHmzvctovDa8C4sehekbwD/K3tVcmKwkeGsADPsTjHjC3tWIiIiIiEgDu/mDDezPKWX1YyOa/Xp9pmmSnFnMst1ZLNmdTcKRAgBC27VidHQAo6MD6NfZGxcnB/sWKiLSjBWWVTP05RUUllcD4Oxo0LeTN8Mi/Bge6U9EgLtu2rhAhmFsMU2zj73rEKkLum1ARBq3XbPBP6bphoMAATEQNRE2vAUD/gBube1dkYiIiIiINJCMwnJ+3pvLfSPDm304CGAYBtFBbYkOasu9I8PJLqpgeXI2S3dn8cXGw3y89iAerk4MjfRjTHQAwyP98GrtYu+yRUSalTdX7aWoopp5fxhEWVUNq1JyWJWaw98XJfP3RckEtnWrDQv9GBjmi2crZ3uXLCJ2oIBQRBqvwqNwZD2MfMrelVy8IQ9D8gLY/AEMftDe1YiIiIiISAOZszUN04SpvULsXYpd+Ld147p+HbiuXwfKqyys2ZvL0t1ZLN2dzcIdGTg6GPTp2I6xsYGMjQmgvXdre5csItKkpReU89Gag0zuEUKP9l4ADOzqyxMTosksrGBVajarUnP4flcGX20+gqODQa8OXgyP9GdYhB8xQW1bxA0tIqIRoyJ2pxGjZ7H2DfjxKbhvq20tv6Zu5hTISIAHdoKLfugVEREREWnuTNNk1Kur8PVw5eu7Bti7nEbFajXZkVbIkqRMliZlk5JVDEB0UFvGxAQwNiaA2OC2GoEnInKeHvkmgW8T0ln+8DBC2535+lONxcq2IwUnugt3phUC4OvuytAIX4ZF+DEk3A/vNuryPplGjEpzooBQxM4UEJ7FuyPAtMJdq+xdSd04tBY+Gg/jXoRL7rZ3NSIiIiIiUs+2HDrG1LfW8tJV3bmmT3t7l9OoHcorZUlSFj8mZrH5UD5WE0K8Wp0IC/t29sbZUesWioicze6MIibMWM0dQ7rw5wnR5/XcnOJKVu+xhYU/peZwrKwaw4D4UC+GRfgxLNKP+FAvHFt4d6ECQmlOFBCK2JkCwjPI3w8zesKY52DQ/faupu58NAHyD8Aft4OTq72rERERERGRevTEnJ3M25bGpqdG4+6qVV7OVV5JJcuSs/kxMYvVe3KorLHi2cqZUVH+jI0NYGiEH61d9PEUETnVrR9tZOuhY/z02IiLWt/VYjXZmVbIqpQcVqZmk3CkAKsJXq2dGRLux7TBnYmvHV/a0igglOZE76ZEpHFKnGv7PXayfeuoa0Mehk+nQMIX0PtWe1cjIiIiIiL1pKLawoKEdMbHBSocPE8+7q5c06c91/RpT1lVDT+l5vJjUibLk7OZsy0NFycHhoT5MjY2gFHRAfi66+ZLEZG1e3NZmZLDnydEXVQ4CODoYNCjvRc92nvxx9HhHCut4ue9uaxKtXUYXtMntI6qFhF70jtUEWmcds2B9v3Bq5mN4ek6EoJ7wc//hB43gaO+DIuIiIiINEeLEzMprqzhqt66iHoxWrs4MS4ukHFxgdRYrGw6eIwfkzL5MTGLZcnZGMZOendox9jYAMbGBNLJt429SxYRaXBWq8nfFyUT4tWKWwZ0qvPjt2vjwuXxwVweH4zVqomEIs2FrkyLSOOTkwJZu2xr9TU3hgFDH4Evb4BdsyH+WntXdG5KcqDwiL2rsK9WXuDdxd5ViIiIiEgTMWvLUUK8WnFJFx97l9JsODk6MKCrDwO6+vD0xBiSMopOrFv4t++T+dv3yUQEuNeuWxhI91BPDKNlr5UlIi3Dgp0Z7Ewr5LVr4nFzdqzXczm08DUIRZoTBYQi0vjsmgMYEHulvSupHxHjwT8WVr8K3a4GBwd7V3R2Viu8PxIKDtu7EvsyHOC+LQoJRUREROpSdYXt5kCziXQjtA0Cz9/uCMwoLOfnvbncNyJMF1LriWEYxAZ7EhvsyQOjIziSX8aSpCyWJGXx9qr9/GfFPkK8WjE+LpAJ3YPo2d5LYaGINEuVNRZeXpxMdFBbruwRYu9yRKQJUUAoIo2LaULiHOg0GDwC7V1N/XBwgCEPwezfQ/J3EHOFvSs6u6ObbOHg0EchpIWuwVxVYvt8Jc61rSMpIiIi0oiZponVtK0f1OgtexbW/8feVZw7N094OAWcW511tzlb0zBNmKrxog2mvXdrbh/cmdsHd+ZYaRXLkrNZtDOD/647yPs/HyDY043x3YKY0M0WFiq4FZHm4rP1hzmSX85/b++mr20icl4UEIpI45K1C3JT4ZJ77F1J/YqdDMufh43vNf6AMGkeOLrCwPvBra29q7Gf9W9B4rw6DQgzCstxMAwC2rrV2TFFREREXv0xlXd/2k//Lt4Mi/BjeKQ/Xf3aNL7uKavFNna/81AY+Ed7V/PbclNh8ROwZwnETDrjbqZpMnvLUfp18qajj9bDs4d2bVy4qncoV/UOpbC8mqVJWXy/M4OZ6w7xwc8HCPJ0Y3xcEJd1V1goIk1bUUU1byzfw+AwX4aG+9q7HBFpYhQQikjjsmsOGI4Q3chDs4vl4Ajdr4VVL0JxZuPtlrRaIWk+hI1u2eEg2ELdH5+E/P11Mma0qsbK5W+sIbekkm4hnoyM8md0dACxwW11gUJEREQuWHJmEW+t2kdciCcZhRU8v3A3zy/cTXvvVgyP8Gd4pB8DuvrQ2qURXA44vA5KMmHc3yB8tL2r+W1dhtuWCUicc9aAcOvhAvbnlnL3sK4NV5uckWcrZ6b2DmVq71CKKv4XFn66/hAfrjk5LAykZ/t2ei8uIk3K2yv3caysmj+Nj2p8NwKJSKPXCH4iEBGpZZq2O4i7DIc2Pvaupv7FTYFV/7AFcP3vsnc1p5e2GYrSYPQz9q7E/mKusAWEifNsI2Iv0vLkLHJLKrmub3v2ZJcwY/keXl+2B38PV0ZF+zMyKoDBYb60cqnfxcVFRESk+bBaTZ6cu4u2bk58fGtf2rVx4Uh+GStTc1iZnM2sLUeZuf4QLk4O9O/szYhIW2DY2ddO3YWJc8GpFYRf2vDnvhCOTrZgMOFLqCoFl9N3B87acpRWzo5M6B7UwAXKb2nr5syUXqFM6WULC5ftzmLhjsxfhIXj4gKZ2D1IYaGINHoZheV88PMBruwRTFyIp73LEZEmSAGhiDQeaVuh4BAMe8zelTQMv0gIiLN1TTbWgDBxnm28aMQ4e1dif17tbWswJs2rk4Bw1pajBLR15YXJ3XB0MMgvrWJFcjbLk7P5LiGDLzYewdXJgYFdfRgVHcCoaH+CPM++1o2IiIi0bF9vPsKWQ8d46arutGvjAtjWZbv5ko7cfElHKqotbDqYz4rkHFamZvPsgiSeXQAdvFszItI2ivSSLj4Nc4OSpcZ2o1zEWHB1r//z1ZXYybD5Q0hdbLvh7xQV1RYWJKQzPi4Qd1ddcmnM2ro5M7lnKJN7hlJcUc2y3dks3JnBZxsO89GagwS2dWN8t0Au6xZErw4KC0Wk8fnnklRMEx4eG2nvUkSkidK7VRFpPBLngIMzRE20dyUNJ3YyLH8OCo+CZ6i9q/klq9UWhoWN0njR42KvhB+fuugxoznFlaxIyeGOIV1wrL3Q4N3G5cToo6oaK5sO5rN0dxbLdmezImUXT82DmKC2jI72Z2R0AN1DPHWRQkQalMVq4mCg0UUijVReSSX/+CGZfp28uarX6d9Xujk7MiTcjyHhfjxNDIfzyliZms3KlBy+2nyE/647hKuTA5d08WF4pB8jIv3p5FtPa+gdWgOlORD765CtUes4CNr427ofTxMQLk7MpLiyhqt6N7L39nJWHm7OXNkzhCt7hlBcUc3y5GwW7vhlWDguLpDLugfRW2GhiDQCKZnFzNpylNsGdaa9d2t7lyMiTZQCQhFpHKxWWydd+Bho5WXvahpO3BRbQJg4FwbeZ+9qfun4eNFR/2fvShqPmCtsAeFFjhmdvz0Ni9Xkqt4hp33cxcmBQWG+DArz5emJMezLKWHp7myW787m3yv2MmP5XnzdXRkZ5ceo6ACGhPs2jnWEpHkrOAxObuDub+9KxA6O5Jcx9a21tHF1YlJ8MFf0CKaLXxPq+BFpAf6+KJmSihqenxx3zuFFB5/W3DKgE7cM6ERFtYUNB/JZmZLNqpQc/vpdEn/9LolOPq0ZXjuK9JIuPrg511F3YeJccG4D4WPr5ngNxcHR9p5w20yoLPlV9+OsLUcJ8WrFJV1awJIJzZSHmzNX9Ajhih6/DAs/33iYj9cexNfdhUu6+HBJFx8GdPWhi71G9IpIi/biD8m0cXXi3hFh9i5FRJowXU0UkcbhyAYoTofYZ+1dScPy7gLBPW1rLza2gDBxHji6QKTGi57g1QFCel/UmFHTNPlm81F6tPcizN/jN/c3DIMwfw/C/D24e1hXjpVWsSo1h6W7s1i0K5OvNx/FxcmBAV18TnQXhnhpFKnUsbJ8eHMgVJVA+34QdZmt29unq70rkwZQUlnDHZ9sprzaQlc/9xNrpnYP9WRSfDCXxwcT0NbN3mXKSSqqLezJKiEmuO2JTnVp3jbsz2PWlqPcPawrEQG//f7idNycHRkW4cewCD+4HA7llbIyJYcVKdl8URuMuDk7MCTcj3GxgYyK9sertcuFFWypgd3f2t5nujTBroe4KbDpPUj9AbpddWJzRmE5P+/N5b4RYeowayZODgtLKmtYtjuLlSk5rNuXx4IdGQAEtHW1hYW1gWEH79YKDBuJrKIKvNu44OzoYO9SROrU+v15LE/O5vFxUSdGiouIXAgFhCLSOOyaDU6tIHK8vStpeHFT62RsZZ2yWm1rwnQdBW5a6PoXYq6EJX+B/APg3fm8n56YXkRKVjHPXxl3Qadv18blxPijaottFOny3dksS87mL/MT+cv8RGKC2jKlVwhTeoXirR8WpC6s+48tHBx4L+xfBUuetv3yi4LICbawMLgnOOjiS3NjtZo88OV2UrOK+fi2fgyN8COzsIIFO9KZtz2N5xfu5oXvdzOwqw9XxIcwrlsgbd2c7V12i2a1mvzhs60sS87G192VMTEBjIsLZEAXH1yc9H+0OaqqsfLUvF2EeLXi/lF110XQ0acNvxvYht8NtHUXrt+fx4rkbH5MymJJUhZODgYDuvowNjaQS2MC8D+fGwUO/gRlebZx+01R+0vAI8jWBXlSQDhnaxqmCVM1XrRZcnd1OhEWmqbJwbwy1u3LY/3+PNbuy2P+9nQAgj3dbB2GXW2hoUb/2cfixEzu+XQLnq2cbeNhuwVzSRdvnBQWShNnmiZ//343QZ5u3Daok73LEZEmzjBN0941iLRoffr0MTdv3mzvMuzLUgOvRUHHgXDNJ/aupuEVHoV/xsLIv8DQR+xdjc2RTfDBaJj8DsRfZ+9qGpdjh+D17jD6GRj84Hk//ZlvE/l842E2PTkaz1Z1exF9f04Jy3Zns2BnBglHCnB2NBgTE8A1fdozJNxPXSRyYcqPwb+6Q9cR//saXXAYUhZB8gI4uAZMi+1CaeQEW3dhpyHgpHC6OXjph2TeXLmP/7s8htsG/fqmiL3ZJXy7PY35CekcyivDxcmBkZH+XNkzmOGR/nU3ilDO2dur9vGPRcncMqAjeSVVrEjJpqzKgoebE6OjA7g0NpBhEX60ctHnprl4c+VeXvohhfdv6cPomIB6P59pmuw4WsgPiZn8sCuTA7mlGAb06tCOcbGBXBobSAef3whE5t9rC9ce3QfOTbQDedGfYPOH8OhecGuLaZqMenUVvu6ufH33AHtXJw3MNE325ZSybn8e62tDw7zSKgBC27U60V14SRcfgjXto95tPXyM699dT0SAB1382rA0KYvSKgvebVwYFxfIxO5B9O/so5+PpElasCOdez/fxstXdefqPu3tXU6LZBjGFtM0+9i7DpG6oIBQxM4UEAL7V8InV9guPMdcYe9q7OODS23dOfessXclNoufhI3v1l7wUAfhr7w7Akwr3LXqvJ5WWWOh/9+WMSTcjzeu71lPxdmkZBbz9eYjzN2WRn5pFcGeblzVO5Sr+7TXXcxyflb+A1b+He7+GQK7/frxsnzY8yMkL4S9S6G6DFzb2taUjboMwsaAW9uGr1su2txtR3nwqwSu79eBv02OO+u4NNM0SThayLxtaSzYkU5uSRUebk6Mjwvkih4hXNJFF+EawqaD+Vz37noujQ3gPzf0wjAMKqotrN6Ty+LETJYkZVFYXo2bswPDI/wZFxfIiCj/Or9hRRrOkfwyxvxzFUPD/Xj3loa/TmWaJnuyS/hhly0sTMooAiAmqC3j4gIZFxdIuL/7L79+WKrh5TDb2oNT32vwmuvM4Q3w4ViY/C7EX8uWQ8eY+tZaXpranWv66oJtS2eaJqlZJazbl8u6/XlsOJBPQVk1AB19Wp8IDAd08Tm/7lv5TQdzS5ny1lrcXZ2YM30gvu6uVFRbWJmSw4Id6SzbnU15tQVfdxfGxwVxWfcg+nby1vsUaRKqaqyM+ecqWjk7svD+Ifp3aycKCKU5UUAoYmcKCIFv77eNGH10Lzi30LspN7wDix6DP2wEv0j71mKa8K9uEBAHN3xp31oaqzUzbGNG799+XmNGF+3M4J7PtvLf2/vZ1vdpAFU1VpbuzuKrTUf4aU8OpgmDwny4pk97Lo0NVHePnF1Foe3rQachcN1nv71/dbltBGnyAluHYVkuODhDl2G2sDByAngE1n/dctG2Hj7Gde+up2d7L2b+vv95jaassVhPjFpbnJhJSWUN/h6uXB4fzBU9gukW4qm1mepBXkklE2asppWzI9/eN/i0o16rLVY2Hsjnh12ZLE7MJLu4EmdHg4FdfRkXF8iYmAB83V3tUL1cCNM0mfbfzazbn8eSh4Y1ijWID+eVsTgxkx8SM9ly6BgAXXzbcGlcIONiA+ke6omxdxl8NhWu/7JpLy9gtdq+RwbGwQ1f8cScnczblsamp0bj7qrVXOSXrFaT5Mxi1u3PY92+PDYcyKO4ogaALn5tGNDFh3FxgQzs6qsL/hchr6SSqW+tpbC8mjnTB9HZt82v9imvsrAiJZuFOzJYlpxFRbUVPw9XJsQFcln3YPp0bKc1RKXR+njNAZ75LomPbuvLiEh/e5fTYikglOZEAaGInbX4gNBSDa+E2zpMmvIdxBerOMs2ZnXoYzDiCfvWcnQzvD9K40XP5sSY0b/C4AfO+Wm//3gTu9ILWfunUXb5wT+9oJxZW47y9eYjHD1WTls3J67sGcI1fdoTF6JOUTmNn16G5c/DnasguMf5PddqgaObbGFh8kLbOqsAIX1sYWHURPCLqPOS5eKlF5Qz6d9raO3iyPw/DKLdRaxlWlFtYdnubOZvT2NlSg5VFitdfNswqUcwV/QIOe2FOzl/FqvJrR9tZMOBfOZOH0hs8G9/TbdaTbYdKbCFObsyOZxfhoMBfTp528ZExgU2isBJzmxxYiZ3zdzCnydEcefQrvYu51eyiypYnJTFj4mZrN2Xh8VqEuzpxn/avE+34p8wHt2Ho0sT75xa/CRseIeKB1Pp+8omxsQE8Nq1PexdlTQBFqtJUnoR6/bnsm5fHhsP5FNaZSGgrStX9gxhSs9QIgM97F1mk1JeZeGG99eTlF7E53dcQu+O7X7zOWVVNSxPtoWFy5OzqayxEtDWlQndgpjYPYie7RUWSuNRXFHNsJdXEhngwed39NcNd3akgFCaEwWEInbW4gPCPUvgs6ua/h3EdeHjiVCcCfduAnu+0dN40XPz7gjAhDtXntPu2cUVDPj7cu4c2oXHx0XVa2m/xWo1Wbc/j682HeGHxEyqaqzEBrfl2r7tuSI+BM/WGjUnQGWxrTMitB/c+PXFHcs0ISflf2Fh+lbbdp8wW1dhxDho3w8c9W/P3sqqarj67XUcyitjzvSBRATU3cXJwrJqFu3KYP72dNYfyMM0IT7Uk8k9Q7iqT3t13FyEGcv28NqSVP42uRs39O9w3s83TZPdGcUsTrR1FiZnFgPQPdSTS2vXlAvzd6/rsutXVRk4uYHDuXe/NiWllTWMfm0Vnq2c+e6+wTg7Nu7XWVBWxbLd2SzZdYSX9k9hibU3f3P5I2NiArg0LpCBXX1wdWqCUw1qb6zb2utvTFnbic+n9WdgmK+9q5Im6PgNNXO3HWVlSg41VpPY4LZM7hnCFT1C8PNQd/fZWKwm93y6hSW7s3jrxl6Miws672OUVNawbHcWC3dksDI1h6oaK0GebkzoZhtD2rO9lwIZsatXf0zhjeV7+fbeQXQP9bJ3OS2aAkJpThQQithZiw8I594NKd/DI3vAqYX/0LP5I1jwANy1GoK626eGE+NFY+GGr+xTQ1Ox5nVY8vQ5jxl976f9vPD9bpY+NKxRXWQtLKtm3vY0vtp0hKSMIlydHBgXF8i1fdpzSRcf3THbkv38L1j6fzBtGYTW8c8+Rem2r/3JC+HAarBWg6snhI20rUkVNgbcG2YMr/yP1Wryh8+38kNiJh/+ri8joupvbFFGYTkLEjKYtz2NxPQiPNycuKF/B24b2JlAzybeUdTA1u7N5cYPNnBFfDD/vLZHnVy8PJBbeqKzcPuRAgDC/N0ZFxvIZd2DiAr0aNwXSUtz4d99wLk1xE2BuKsgKN6+N2DVsRcWJvHe6gPMunsAfTp527ucc5fyA3xxLZsGvsPMvEiWJ2dTUlnTdNcsNU34V3cSKgOYzp9Z/dgIvXeSi5ZXUsl3CenM2ZbGjqOFODoYDA33ZXKvUMbGBGiJgFOYpslfv0vi47UHeXpiDLcPPvclIM6kuKKaZbuzWbAjg59SbRMQQrxacVn3IC7rFmQbl9yMvqdI45dVVMGwl1cwJiaQN67vae9yWjwFhNKcKCAUsbMWHRBWV9jGi0ZPgiv/Y+9q7K80z/bxGHQ/jH7GPjUcHy965dvQ43r71NBUHDsIr8ef05hR0zQZ96/VtHZ1ZO70QQ1S3oXYlVbI15uPMG9bGkUVNbT3bsU1vdtzVZ9Qgjw1Zq5FqSqFf3W3XVC/eU79nquiCA6sgtTFtq7ykkzb9uBetrAwYiwE9Wy2XUCNyWtLUpmxbA9PTojmjqFdGuy8248U8N7q/SzamYGDYTCpRzB3DOlCdFDbBquhqcouqmDCjJ/xbOXEt/cOpk09dGFmFJbzY2IWP+zKZMOBPKwmhPu7Myk+mEk9guno0wjHxP74FKz7D3QdCftXgrXG1rHc7WpbWOgbZu8KL8rujCImvvEzV/cO5R9T7XRT2YWacxek/lB7c6ALlTUW1u7NY8GOjCa7ZmnJd0/guvkd3uv3A9Mv62fvcqSZ2ZtdzJytaczblkZ6YQUerk6M7xbIlF6h9OvkrUCa/92I+fvBnfnLxJg6P35RRTVLk7JYsCOD1XtyqLbYxiUPi/RnWIQfg8J88DjNur9if0fyy/jn/DV08zEYGxtAiFdre5d0dg5O4NXhtDc0PTFnB7O2HGXZQ8Pp4NPIX0cLoIBQmhMFhCK/wTCMg0AxYAFqTNPsYxiGN/AV0Ak4CFxjmuax2v2fAH5fu//9pmkuPtvxW3RAuHsBfHUj3DQHwkbZu5rG4dOpkJsKf9xhn7vca9dR4dG90Mqr4c/f1Lw73Pb7b4wZ3Xm0kMv//TMvTI7jxv4d672si1VRbWFxYiZfbTrC2n15OBgwNMKPK3oEMyLSH6/WF74emTQRa/8NPz4Jt/8IHfo33HlNEzJ3wJ4fIfVH2xqGmNDaF8LH2ALDriP19akefJeQzn1fbOPq3qG8dFV3u1yQP5JfxodrDvDVpiOUVVkYEu7LHUO6MCTct9EHBPZQY7Fy4/sbSDhawLf3Dq7TcbBnkldSyfe7MvluezobD+YDEN/ei0nxwVzePQj/to2g+7M4y3YDT8wVMOUdKMuH3d/Czllw8GfAtN380O1qiJ0CniH2rvi8WK0mV729loN5ZSx7aNhFrRHa4I7fHBgzCa749c2BTXXN0q+//Y5rtt5E3shX8Bl6h73LkWbKajVZfyCPOVvTWLQzg9IqCyFerZjcM4TJvULo6td4JpQ0pAU70rn3821M6BbIv6/vVe+BaWFZNT8mZbJ0dxZr9uZRUlmDk4NB747tGBbpx/AIf6KDGnmXfQuxJ6uYr959gSdq3sbRaELXvie/C/HX/mLTnqxiLv3XT/xuYCf+7/JYOxUmJ1NAKM2JAkKR31AbEPYxTTP3pG0vAfmmaf7DMIw/Ae1M03zcMIwY4AugHxAMLAUiTNO0nOn4LTognHW77a7uh1PBUesOAbDtM5g/HaYth9DeDXvu2hFJ+Edf/HpjLcXxEYx/TIB2nc642//N38WXm46w8cnReLZqWneXHs4r45stR5i15SgZhRU4Ohj06+TN6JgAxsYE0N5bdy82O9XltovrfpHwu+/sW0tpHuxbZgsM9y6F8mNgOEKHS2oDw0ttX7N0Eeai7DhawNVvr6N7qCefTutv93XACsuq+WzjIT5ec5Ds4kqiAj24Y0gXLo8PxsVJnaTHvbI4hX+v2MsrV8dzVe/QBj9/WkE5CxLS+TYhncT0IgwDBnTxYVJ8MOPjguy3nu2ix2Hje7Y1nX26/vKxonRInGsLC9O3AgZ0HAjdroKYK6F14x/V+eXGw/xpzk5evqo7V/dpb+9yzk/yQvjyBrhpNoSNPuuuZ1qzdFKPEC6PD8LfoxGE0dimRIx6ZSWfV9xDYKdouHmuvUuSFqC8ysKPSZnM3prGz3tysJq2mzWm9gphYvdgvJvSjQMXYeOBfG56f8OJ9y8NPXq12mJly6FjrErNYVVKDkkZRQD4e7gyNMKP4ZF+DA7z1c2VdrDt8DGWffQ0j5ifUBo6jKq4a9hy6BibDuaTWViBk4MDcSFt6dPJm+hAD5wayzq+q18FwwHuWfuLn2+m/XczG/bnseqxES3m/3djp4BQmhMFhCK/4QwBYQow3DTNDMMwgoCVpmlG1nYPYprm32v3Www8Y5rmujMdv8UGhFWl8HIYxF8HE/9p72oaj/IC253Vfe+AcX9r2HMf3QLvj4Qr34IeNzTsuZuq42NGxzwLg/542l0qayz0/9syhob7MaMJrxVgtZrsSCtkSVImS5KySM0qASAq0IMxMQGMjg6gW4inxhw1BxvegUWPwa0LodNge1fzP1aLbQzynsW2wDBzp21721BbWBhxKXQeCi7102Gyek8ObyzbS7/O3oyI8qNH+3ZNZ42ss8gqqmDSv3/GycGB+fcOwte98awHXFlj4dvt6by3ej+pWSUEtHXltkGdub5fhyZ3s0VdW5mSza0fbeKaPqG8dFW8vcthb3YJ3yak811COgdyS3F2NBgW4c+kHsGMjvantUsD3QhWmAYzekD3a07bofYLeftg12zY+Y1teoODk61DudvVEDkBXBtfN05eSSUjX11FZKAHX915SdPrUJk9DfYug0dSwfHc/w+fumapgwEDu/pyRY9gLo0LpK0dR/ttOXSMqW+tZVHscqL3f2R7bW187VaPtDzZRRXM357O7K1HSc4sxtnRYHikP1N7hTAiyt/uN/3Ul73ZJUx9ay0+7i7Mvntgo+imzi6qsIWFqTms3pNLYXk1Dgb0aO/F8NpxpPp5qf79nJrDjs8eZ7oxm7KwibS+7iNwsv37ME2TXWlFzNl2lG+3p5NXWoV3Gxcu7x7ElF6h9l9bcvsXMO/uX9xIs/FAPte8s45HL43kDyOa9oj05kQBoTQnCghFfoNhGAeAY4AJvGOa5ruGYRSYpul10j7HTNNsZxjGv4H1pml+Wrv9A2CRaZqzznT8FhsQ7poDs25rfBegG4Mvrof07fBgYsOuufXjU7D+bY0XPV/vDLPd5XfnitM+/P3ODKZ/tpVPbu/H0Ai/Bi6u/hzKK2VJUhZLkrLYdDAfqwkBbV0ZHR3A6JgABnb1abYXJJq16grbxXXvLnDb9/au5uyK0m1rFu75EfatgOpScHS1fU8JHwtxU8Ddv05OVVFtYfRrqygsq6a0qgarCZ6tnBkS7suISH+GRvjh59F4grVzVVFt4dp31rEnu4TZ9wxstGv+mabJqtQc3lu9nzV782jj4sh1/Tpw26BOhLZreV3M6QXlXDZjNQFt3Zg7fRCtXBrP19rjF97mb0/jux3pZBVV0trFkTExAUyKD2ZIuF/9doEueBC2zoT7tkC7cxzpbZq2Gw52zYKds6HoKDi1gshxtrAwbDQ4NY7/3w9/ncD87Wl8/8chDTJStk5Vl9tuDoybCpNmXPBh9mYX8+32dOYnpHMorwwXJwdGRflzRY8Qhkf6NXgH0RNzdjJvWxpb7gig9YfDYeK/oM9tDVqDyHFJ6UXM3XaUedvTySmuxLOVMxO6BTI2JpABXX0a/P9HfckurmDKm2upqLYwd/qgRjnRpMZiJeFoYW13YTY70goxTfBu48LQcF+GRfoxJNyvUd2Y1Rws2pFGzjcPcYvjD5THXk+rqf8Bh9P/u6+2WFm9J4fZW9NYkpRFVY2Vrn5tmNIrlCt7hhDi1aqBqwdqquD17rZJLrfMxzRNpry1loyCClY8MrxRvedr6RQQSnOigFDkNxiGEWyaZrphGP7AEuA+4NszBIT/AdadEhB+b5rm7FOOeSdwJ0CHDh16Hzp0qIFeTSNSmAZJ86D/3Wd8w9Zi7ZwFs38Pty2yjb1qCBoveuFOjBndcdqLkbd/vImk9CLW/Glks+g2Op1jpVUsT85m6e4sVqXmUFZloY2LI8Mi/RgdHcDIKK1b2GRseh8WPgy3zIcuw+1dzbmrqYRDa2sDw8WQtxec3KD3rTDw/oteY+w/K/by8uIUPp/Wn9hgT1bvzWFFsu0O8dySSgC6h3oyPMKPYZH+9Gjv1ej/v5umyf1fbmfBjnTevbkPY2IC7F3SOdmVVsj7q/fz3Y4MAC7rFsQdQ7rQLdTTzpU1jGqLleveXU9yRhHf3je4Ua85ZbGabDyQz7cJ6SzalUFBWTVerZ0ZHxfEpPhg+nf2rtsuimOH4I3e0OvmC59OYbXCkQ22sDBxLpTlgZsnRF9uCws7DbHb+9b1+/O47t313DO8K4+Pi7JLDRcl6Vv4+ma4eR50HXHRhzNNk+1HCpi/PZ0FO9LJLanCw82J8XGBXNkjhP5dfOr963BFtYW+zy9lTEwAr10Tb/v35xkKv/u2Xs8r8ltqLFbW7MtjztajLE3KorTKQmsXR4aG+zEmxvbevDF03F2I0soarnt3PXuzS/jqrkvoHupl75LOSV5JJT/vzWVlSg4/peaQV1qFYUC3EE+GRfgxLMKPHu29Gs+oyyboq/X7cVz4R65y/InKPnfjetk/znkZgsLyahbtzGDO1rQTayxf0sWbKb1CGR8XiEdDdqr//E9Y+gzctZpFuX7c89lWXpzajWv7dmi4GuQ3KSCU5kQBoch5MAzjGaAEuAONGJX6Ulliu8O6501w2SsNc860LfCexotekBNjRp+DQff/4qHsogoG/GM5dw3twmNN8WLeBaiotrBuXx4/JmWxbHcW2cWVODoY9O3UjjExgYyJDqCDT+O7y1ew3bH6Ri/wCILf/9i01/XLSYE1M2DHl4ABPW+EwQ+eda3QM8ksrGDkqysZGu7H2zf/cm1Yq9UkMb2IlSnZrEzNYdvhY1hN8GrtzNBw29ozwyL88GmEd4e/sWwPry5J5bFxkUwf3vTGFaUXlPPRmgN8sfEIJZU1DOjiw51DuzAswq9Zj+762/e7efen/bxxfU8ujw+2dznnrKrGdpf+twnpLEnKoqzKQkBbVy7vHsykHsF0C6mDkV7z74UdX8P92y76pgAALNWwf5UtLNz9HVSVgHsAdBlxXuMxz4nhYLtpLiDmtA9X1ViZMGM1FdUWljw4rGl2EHxzKxxYDQ+n1Pna4zUWK2v35TF/ezqLEzMpqazB38OVy+ODmdwzhNjgtvUyMm7+9jT++OV2Pp/Wn4FhvrD8edv6UQ+n1FkHu8jFqqyxvTdfkpTF0t1ZZBVV4mBAn07ejI0JYExMAB196mc8e12rsVi545PNtqkCt/RhVHTTuLnpVCe/f1yVmsPW2vePbd2cGB0dwIRuQQyJ8NU0lvPw7orddFh+H+McN1E15E+4jPzTBf8scyS/jLnb0piz9SgH88pwc3ZgbEwgU3qFMDjMt/5D3PJj8Fos1qiJjDpwA86OBt/fP0ThcSOjgFCaEwWEImdhGEYbwME0zeLaPy8BngVGAXmmaf7DMIw/Ad6maT5mGEYs8DnQDwgGlgHhpmlaznQOBYRyWl//Dg6tgYeS6/wiymn9+BdY/5bGi16od4bZOgruWP6Lze/+tI+/fZ/MsoeHNeouj/pypnULIwNq1y2MCaC71uFoPLb8F767H26cDeGj7V1N3Th2CNb8C7Z9alvDsPu1MORh8D33QOyhr7azYGcGSx8c9pvhdkFZFT/tybVd8En5393h3UM8GR7pz/BIP7qH2r+78IddGdz96VYm9wzhtWvim946Zicpqqjmq41H+HDNATIKKwjzd+eOIZ25okdIsxmldtySpCzu+GQzN13Sgeev7Gbvci5YWVUNS3dn8+32dFalZlNtMeno05oRkf6MjPKnX2fv8//c5e2Df/eFfnfA+BfrvujqckhdbFuvMH2bbfJCXSrLg9A+ttH7p/n/+ObKvbz0Qwof/K6JXhBvwLXHK6otLNudzfztaaxMyaHKYiUiwN02Mq5HCIGebnV2rps/2MD+nFJWPzbC9l4mKxHeGgiXvQp9p9XZeUTqitVqsjOtkKW7bcsEJGcWAxAR4M7oaFtYGB/q1Sjfm5umyZ/n7uKLjYd5/so4brrkHMdINwGFZdX8vDeX5cnZLEnKpKiiBg9XJ0bHBHCZwsKzMk2TVxduo9+G+xnquJOasX/HaeD0Ojv21sMFzN12lO8SMigsr8bX3ZUrewQzpVcoMcH1OJp/0Z+wbnyPgeX/5IXfXdo0v/c3cwoIpTlRQChyFoZhdAHm1v7VCfjcNM0XDMPwAb4GOgCHgatN08yvfc6TwO1ADfCAaZqLznYOBYRyWsfHMDXEmL8T40Wj4MZv6vdczdXxMSAnjRk1TZNL//UT7q5OzJk+yL71NRKnW7fQ38OVIeF+tnU4wnyb7LijJs9SbRuN1trHFnQ34cDotIrSYe0bsPkjsFRC7GQY8sgZu3WO23r4GFPeXMsfRnTl0UvPrwvYajXZlV7IiuQcVqZms/1IAaYJ7Vo7MyzCj+G1axd6N/C/+cT0Qq56ax1RQR58ccclzSZEq7ZYWbgjg3d/2k9SRtGJCzhjYgLo3bFdk7/r+kh+GZfNWE0Hn9bMuntgs/m8FZZVs2hXBj8kZrJuXx6VNVZaOTsyKMyXEVF+jIj0J/hc1gCacxckzYc/JoBHE7yItvE9+P6R047fPJJfxph/rmJYhB/v3NxEr0MlzrV1EP5uAXQe0mCnLSirYmHtyLgth45hGDCoqy9TeoVwaWwgbVwv/Ca8jMJyBv5jOfeNCOOhsZG2jaYJ/+kPbfzgtoV19CpE6s/hvLITYeHGg/lYrCZ+Hq6MjvZnTEwAA7v6NprvN8fHvTfZMcvnqKrGypp9uXy/I4Mfk7IoLK8+ERZO6BbEkPDG8zmxN4vV5LlZa5m46wF6OeyFSTNw6HVzvZyrssbCiuRs5mxNY0WK7eamLn5tGBrux6AwX/p38aZtHY4hLc3aj9tbvVnQZiqTHnmvSd/M11wpIJTmRAGhiJ0pIJTTqi633WkdNwUmvVG/5zo+XvSKN21j+OT85R+AGT1+MWZ0x9ECJv17DX+f0o3r+2m9gFMdK61iRUo2y5Kz+XlPLoXl1Sc6rYZF+DFU63A0rG2fwfzpcP1XEDnO3tXUn5JsWPdv2PSBbVxg1EQY+ggE9/zVrlaryeQ315BRWMGKR4Zf1IVkgPzSKlbvyWFlim3twvza7sL4UC8ujQ3k8vggQtvV7/jd7OIKrvz3Gkxg/r2D8Peou06axsI0Tdbuy+PDnw/w054cqi0m7Vo7MzIqgDEx/gwJ97voz2VDq6yxcM3b69ifW8rC+4Y02zHN5VUW1u3PZUVyDsuTs0krKAcgKtCDEVH+jIj0p1eH03xfyEmFN/vDJdPh0hfsUHkdqKmEGb2gbRD8fsmJmzRM0+T3/93M+v15LH1o2LmFpY3RVzfb1nZ8aLfd1nA8mFtqGxm37ShH8stp7eLIuNhApvQKZUDX81+v8HhYserR4b8cz7ji77DqRXg4GTwC6/hViNSfgjLbe/OlSdmsTMn+xbqFo2vXLWzom5qOm7vtKA9+lcAVPYL55zU9GmWHY32oqrGydl8u3+/MYHGiLSx0d3VidLQ/E7oFMTTCr8WGhZU1Fp7+bDm/2/cwkY5pOFz1AUbslQ1y7mOlVSzYkc7S3dlsOJBHRbUVRweD+FBPBof5MijMl54d2uHidOE/x762JJWIn+5jvFsijg8ngVs9divKBVFAKM2JAkIRO1NA2DJZrSbTPtlMcUU11/Rpz2Xdg2jtcsoFyzl3wp4f4eFUcKrHH8ZOjBfdA63a1d95mrt3hoKD04kxo3+Zt4uvNx9h01Oj6/RuwubIYjVJOFrAT6k5/JSaw/YjBVhN8HBzYlBXX4ZF2gLDkKZ6YbSxs9TAf/qBSxu466fm1z14OmX5sOFtWP82VBZC+FgY+ii073dil1lbjvLINwm8dk08U3qF1unpLbUjvlamZLM8OZsdRwsB6NOxHZN6BDOhWxC+dbxuYUW1hevfW09yRjHf3D2AuBDPOj1+Y1RcUc1PqbksScpkeXI2RRU1uDg5MDjMlzExAYyK8se/beMPSZ/5NpGP1x7k7Zt6My6uZQQOpmmyN7uEFbX/RzYfPEaN1aStmxNDI/wYGeX/v/U9v7nNNv7zgR3QxtfepV+4zR/Bggfghm8gYiwAP+zK5O5Pt/DkhGjuGNrFvvVdqMoSeLkr9LoFJrxs72owTZPNh44xZ2saC3akU1xRQ0BbV67sGcKUnqFEBnqc0zFGvboKX3dXvr57wC8fzE62BdbjX4L+d9XTqxCpX2dbt3BM7SjSTr4Ns27h2r25/O6jjfTu2I7/3t6vxY7arK5da3XhjnR+TMqioMwWFo6K9ueyFhYWllbW8OTHi7gv7VE6Oh3D6YbPIMw+yyNU1ljYeqiANXtz+XlvLjuO2n6Obe3iSL/O3gwO82VwuC+RAR7n3AWYXVTB8FdW8ruO+Tx+5B649G8w4A/1/ErkfCkglOZEAaGInSkgbJmO3wUZ0NaVrKJK3F2dmNQjmOv6tqdbiKftzWPKD/DFtb+4UFTnTBNe7w6+kXDTrPo5R0ux+jVY9ld4YCcVbULo/7dlDI/04/Xrft2ZJGd3fB2On1Jz+GlPDhmFFQB09WvD0Ag/hkX40b+zD61cWsYPwfVux9cw5w649lOIvtze1TSsikLbaL91/4HyfOg8DIY+SknQJYx4dRUhXq2Yc8/Aer9T/XBeGd/tSGf+9jRSs0pwdDAYFObLpPhgLo0NwOMibzIwTZOHv05gzrY03rqxF+O7BdVR5U1HtcXKpoP5J8YcHz1m607r0d6LMTG2i53h/u6NboTTwh0Z/OHzrdw+qDNPX372kbjNWVFFNT/vyWVFcjYrUnLILanEMGBS4DH+dexecuKn43vFC027q+T4qOdW7eDOlZRWWRj92io8Wznz3X2DcW6qHfU7Z8Hs38Nti6DjQHtX8wvH1yucu+0oK1NyqLGaxAa3ZUqvUCbFB+PncfobNbYcOsbUt9by0tTuXNO3/a93eHMAuHnC7T/U8ysQqX+mabup6fj3z+PrFnb1a8PIKH9GRgXQp1O7evkalZxZxNVvrSPIy41v7h6IZyvddAn/Cwu/35HB4qTMX4SFE7oFMawZh4XHSqt48v25PJX/BH7OlTjfMhs6XGLvsk4oLK9m/f68E4Hh/pxSAHzdXRhU2104KMz3rDe+/nnuTr7edISlDw2j03dXQ8FhuH87ODatCRjNnQJCaU4UEIrYmQLClqe8ysLIV1fi5+HKvOmD2HL4GF9uPMLCnelUVFuJDmrLdX3bc2U3Pzz/Ew2RE2Dy2/VTTNpWeG8EXPEf6HlT/ZyjpcjfDzN6wtjnWeh+FX/4fCszf9+PIeF+9q6sSTveRbIq1TaWccOBfKpqrLg4OdC/szdDa9cvbIwX9psEqwXevAQcnOHun8GhiV6AvliVJbDlI1gzA0qzOeIRz5N543jo7nvo0aFhO6uTM4v4dns63yakc/RYOS5ODoyK8mdSfDAjovwv6ILP26v28Y9FyTw0JoL7R4XXQ9WNUPp2yEk57UMmJhmFFexKK2RXWiGH821hoa+HC3HBnsSFeNLZpzWODfX/wcERIsaBq/svNh/MLWXiGz8T5u/O13cNuKhRVc2J1WqSmF7E8uRs+m28n9jKbQypfB0XDx+GR/gxIsqfweG+F9W9X2OxUm0xqbJYqaqxUn3S7+5uTgR51lNH+/Fxz9d9zgv7OvPe6gPMvmcAvTt618/5GsKXN9rG2T+Y1Ki/x+SVVPJdQjpztqWx42ghjg4GQ8N9mdIrlDExAb/42vvEnJ3M25bGpqdG4366kcWrXoIVL9hGqrYNbsBXIVL/juSXsSQpixUp2azfn0e1xcTjeHd3pD/DI2u7uy9SRmE5U95ci9U0mTN9kCaJnEG1xcq6fXm1Y0gzOVZWTRsXR0ZF29YsHB55bmGhxWpSVWP7XldZY6GyxkrlSX+v+sXfrVhMk76d2tXf98PTyCys4Jl3v+D5kv/Dw80R11vnQVB8g53/QqQXlLNmb25tYJhHbkklAF1825wICwd08cGzte09y76cEsb+8ydu6t+Bv14RB8nfw5fXw9QPoNtV9nwpcgoFhNKcKCAUsTMFhC3Pv5fv4ZUfU/nqzkvo38XnxPaiimq+3Z7OV5uOsDOtEBcnBz7x+YQ+ZT/h+NheDOd6ePO95Glb58wje6B1E7741Fi8PQQcXbjN6e8kZxbz8+Mjz3tNGzm7imoLGw7ksyrF1l24N7sEgMC2bgyN8GVohB+Dw3zxam2fNVKanF2zYdbtcNVHtjVPW7rqcvJXv0/lqtcIMvIhuJdt9Gjk+AYfvWqaJlsPF/BdQjoLdqSTW1KFh6sTY2MDmdQjmEFdfc5pjc4lSVncOXMzE7sHM+O6Hi0jSM/caRv7bFrtXcm5C78Ubvz6xF8rqi1MeXMt6YXlLLx/iC6Mnk5GArwzlLIBj/CD320sT87mp9QciipqcHIw6N2xHb7urqcN+aosJlU1FlsIeGLb/x63/saPyH07teOq3qFM6BZ00R2+v2CpgTf7U2E6E5f5FFf36cDfp3Svu+M3tIoi25rafW6H8f+wdzXnbG92MXO2pjF3WxoZhRV4uDoxoVsQU3qF0C3Uk/4vLGNMTACvXdvj9AfI3QP/7gOX/h0GTG/Q2kUaUkllDT/vyWV5chYrUnLIKbZ1d/do78WoKH9GRPkTE9T2vN97FFdUc/Xb6ziSX8bXdw8gNrj5j0WvC9UWK+v357Fwx//CQg8Xg+7BrSm3Ov8i4Ds1+Kv5rW98p2EY0K+Tt208flwQ7epxjcoDuaW8+O5/eanyOVzbtMX19gXg27RuejNNk9SsEn6uDQzX78+jrMqCgwHdQr0YHOZDwpFCth8pYOWjw23LDVit8J++4OIOd65sGUtBNBEKCKU5UUAoYmcKCFuW7OIKRry8ksHhvrxz85nfS+xKK+TrzUfI3LaId3mep1z/RPCAq7mqV2jdrZdkmvB6PPhGaLxoXakdMzq4cgZXDO/Po5dG2buiZi+toJzVtd2FP+/NpbiiBgcD4tt7MTzCn2GRfnQP8WzaY+fqi9UKbw+ydRFOX2frYhLu/GQzG/dm8tPYDNpufgMKDkFAHAx9BKIn2eXjVGOxsm5/Ht9uT+eHxEyKK2rwdXdhQrcgrugRTK8O7U578S05s4ipb66la20HWnMdN/ULpgkfjoO8PfC778Dp/L5nllZZ2HQwnzV7c1m3L4/iyhqcHR3o3cGLQWF+DOrqg497HV8A2zXb1m10/Ze2MBpbh9IXGw/z4a19GBkVULfnay4+vxYOr4MHdtrGOWL7v7L1cAErUrL5eU8u5dUWnB0dcHFywMXRwMXJAWdHh5O2OeB80naX2u3OJ/9+yvMO55cxe+tR9ueU4ubswLjYQK7q3Z4BXX3q5KYga8LXOMy9g8ccHuLPjzzRtG94SfgK5t4Jt/8IHfrbu5rzZrWarN+fx5xtaSzamUFplQWv1s4UlFXz+bT+DAw7y5qXbw0G51YwbUnDFSxiR1arya70QpYnZ7MiOZuE2vWVgzzdGB7pz6gofwaF+f7mEgHVFiu3f7yJtfvy+OjWvgyN0DSWC1FtsbJ+Xw6+391KWPEmdrr1Zqv7MJLaDsJ09Tzx/c7V6fjvjqf8/X+///oxR6otVpbtzubbhDT25ZTi5GAwNMKPSfHBjIkJoM3puqsvUGJ6IW++/x6vWF/CoW0Qrrd/B14d6uz49lJVY2X7kYITgeH2IwVYrCYPj4ngvpMnfmz+EBY8CLcuhE6D7Vew/IICQmlOFBCK2JkCwpbliTk7+WbzEZY8NIzO57Cwe0VlJcarUWxx6MYNBXfh6GAwMsqf6/q2Z1iE3zl1j5xR+jZ4d7jGi9al2jGjz1XfyI0PvEQXP/fffo7UmRqLlYSjBaxKyWHVHtsi8aYJ3m1cGBLuy7AIP4ZG+NnuxhRI+ha+vhmmvA/dr7Z3NY3Cz3tyuemDDTw2LpLpw8Ns3Ty7ZsFPr9gCpzb+4PLbX7vrkxXbqOqyqhrKqiyYJjg5GrRxcaKNqyPOjg4YQJVnRy7LvJMiqyvf3juYgLq6uaSx2/4FzLsbJv0bet18UYeqtljZfPCYbd2l3ZkcqR1FGhfS1jZGLcqf+FCviw+FLNW2DvTqUvjDRubtyueBr7Zz97Cu/Gm8bjQ5raOb4f1RMPIpW5dvAzNNk21HCpi95SjfJqRTXFFDsKcbU3qFMrV36Dm9xzuTLzccoNfCywhs60rbhzY37Zs3Pr/O1tH7wM5GPV70XJRV1bAkKYvZW9OwWK3MvL3/2W8+Wv0qLHsWHtgFXqdZp1CkmcsurmBlSg7Ld2ezek8OpVUWXJwcGNjVh5FR/oyI9Ke9d+tfPMc0TR75Zgeztx7lpau6c00f/d+5KCv+Dqv+AVETbaPXi46Cowt0HQkxV9puSmrldVGnME2TpAzbePzvEtJJL6zAzdmB0dEBTIoPZlikH65OF/59bNPBfD79+D+8zL8wfcJxvXU+eDTPG6eKK6rZnVFM747tfvnesroc/hkLoX3hhq/sV6D8ggJCaU4UEIrYmQLCliMls5jxr//E7wZ24v8ujz33Jy54CBK+4MBtCXyVkM+sLUfJLakkoK0rV/duzzV92tPBp/VvH+dUS/4P1v1b40XrkGma7H2uF1ZHFyKf3GDvclq8/NIqVu/JsQWGqTnklVYB0C3Ek2ERfgyP9KNHe6+LC9qbKtOEd4bYfuD8w8amfQG6jtRYrEyYsZqKais/Pjj0l912VgskzYPUHxvV2Mpqq5WMggqOFpSTXVSBCXi4OdHBy5WwnCV8bh1DtzveI769l71LbRjlBbaxfu062TqW6jCQOD4WaunuLFamZLPl0DGstTcgHP96MizC78K7vQ6shv9OJL/PgwzeeAlxwZ58fkf/lvn16VzMnGK70emBHeDqYddSKqottcHRUX5KzcFqQu+OthGkl3UPOq+1EHNLKhn16ipu9dzGgwV/gynvQfdr6rH6elReYBsv2v8uuPQFe1fT8E5am5qB99m7Gmmpds2GdW8C9r3uZzVNSistFJVXU1hRTVWN7b2Um7Mjbd2cadvKiTYuTmw3w5hy4HLuHxXJg2Mi7Fpzk5e6GD6/BuJvgCvftL33T9sCiXMhab4tLHRwtoWFsVdC5ISLDgutVpPNh47xbUIa3+/MJL+0irZuToyPC2JSj2Au6XJ+nfYrkrNZ/PlrvODwDpbAnrj8bja0ati1wRuNlf+AlX+HP2wCP/3faAwUEEpzooBQxM4UELYct3y4kYQjBax6dPj5XUA8+DN8fBlc9SHETaXaYmV5cjZfbTrCypRsrCYM7OrDdf06MDYm4NxGyJ0YLxoON82+8Bclv5BwpIDFbz/GY85f6Y7xRsZqNUlML2JVajarUnPYetg2wsXDzelEd+GwCH8CPVtIl1XKIvjiOrjyLehxg72raRT+u/Yg//dtIu/c3JtLYwPtXc55yyup5PtdmXy3PZ2NB/P5P6f/cqvTjxi3/wAdLrF3eQ1j0eOw8V24YwUE96jXUxWUVfHTnlxWJGezMiWbY2XVOBjQq0M7RtR2RkQHeZzXuks1X9+ONelbrnb4J+/88eqW8/XofB1eDx9eCmOehUF/tHc1v5BVVMHcbWnM2nKUvdkluDo5cGlsIFf1DmVQmO9vXhh9+OsE5m9PY9H9gwifO+F/N3E41t2otgaz/XOYdw9MWw6hve1djX28MwwMB7hzhb0rkZYobx+8NQjaBoN3Z3tX8wtlVRbySivJK6misLwa04S2jpX0Ipk5QQ8x+c6nW8aayfUlf79tUpBXB/j9Etu445NZrbawMGmeLSwsPFLnYWG1xcqavbl8uz2dxYmZlFZZ8PNwZWL3ICbFB9OjvddZP8fzt6exfdaL/J/Tf6nqMASXG78E1xY8nac019ZF2P1amDTD3tUICgileVFAKGJnCghbhpUp2dz60SaeuiyaaUO6nN+TrRbbm8GQ3nDdZ794KKOwnFmbj/LV5iMcPVaOZytnruwRzLi4IPp0aofzmToPjo8XrYMRbPI/T83bycYtm/nR8QEY+wIMvNfeJckZFJZVs2ZfLqtScliZmk1WUSUAUYEetrAw0o8+Hb1xcWqG3TumCe+NgPJjcO9mcDz37pbm6lhpFcNfWUlcSFs+/X3/Jn9RKq2gnOzcPHp+Nx6c3eCu1bbfm7PMnfDOUOhzO1z2aoOe2mI1SThawMrkbJanZLMrrQiAwLZujIjyY3ikP4PDfM+6Ho9pmvz182U8knojlSED8LlzXgNV3wR9PBFyUuCPCeByARMUGoBpmuw4Wsis2hGkheXVBLZ1Y3KvEKb2CiXM/9cXOdfty+P699YzfXhXHhsXBckL4csb4Io3oeeNdngVF+mzqyE72dbl2cS/pl6wn/8FS//P9m+1XSd7VyMtidUKH0+ArCT4w3pbSNhIFVVU8/OeXJYlZXHXkUcIr0zCmL4O2nW0d2lNU1UZfDDWFvrdteq3v/ac2ll4IiwcYRtDGjXhorv2KqotJ9YrXJGcQ5XFSgfv1kyKD2ZSj2AiAn45CeCTtQfI+f4FHnb6hurw8Thf83Hzfx97Lr57wHbzzYO7wN3f3tW0eAoIpTlRQChiZwoIm7/jY+sqa6wseXDYhQUOi/5kW5z60T3g5vmrh61Wk7X78vhy02F+TMqiqsZKWzcnRkT5Mzo6gGGRfr8ccaXxonWuotpCvxeWMjLKn38V3AdObjBtqb3LknNgmiYpWcW2sDAlh82H8qm2mLRxcWRAV98TowNPXSelydqzBD67Cia9Ab1usXc1jcLT83fx6fpDLPrjUCID7TuusE7tXQqfTrWt0TbyKXtXU39MEz4cZ1sn8r4tdh8/lV1kW3dpRUo2q/fkUlJZg4ujA/06e9d2F/r9ao3arzcd4bHZO/g4aiPDD/4Lrv/StjaQ/NKBn+C/l8O4f8Al99i7mnNSWWO7MDpry1FWpeZgsZr07ODFVb1Dmdg9GM9WzlTVHB9xbGHJg8No5eL4v5s5yvJt/66b0s0cZfnwSjhcMh3GPmfvauzn2EHbxI7Rf4XBD9i7GmlJ1r0Ji59oepMiCo7AmwMgpCfcPL/Jr13a4EwT5t4FO76GG2dB+Ojzf37aVkicY1urvPCwLSzsMtzWWRh12UW/xyosr2ZxYibfJaSzZm8uVtN2g+akHsFc3j2YOVuO0mrVM9zptBBLt2txvPLNptlFXx9y99hG6Q97HEb82d7VtHgKCKU5UUAoYmcKCJu/zzcc5s9zd/LWjb0Y3y3owg5yZBN8MBqufBt6XH/WXUsra1i9J5elu7NYnpxNfmkVTg4G/bt4Mzo6gNFR/rT/dCB4d4Wb51xYPfIrC3akc+/n2/j09/0ZnPExLH8OHkwEz1B7lybnqaSyhnX78liZks3KlBzSCsoB6OrXhlHRAYyM8qdPx3ZNc20w04QPxkBxlu2Cs9MFrpfWjCRnFjHh9dXcdElHnr0izt7l1L25d8POb+DOVRDYDF8fwPYvYN7djbIrvqrGyuZD+axIzmZFSg57s0sA6OTTmuGR/oyM8sezlTPXvLOO3h3bMfPWXji+OxSqS22jJU8dC9aSHQ+CCw7B/dubZDdBdlEF87bbRpCmZpXg4uTA2JgA3F2d+HLTET68tQ8jowL+94TjN3RM/Bf0uc1udZ+3rTPh23vhzpUQ3NPe1djXeyPBWgN3/WTvSqSlOD5atPNQuOGrptfBu+Vj+O6PtmkAfafZu5qmZeN78P0jMOJJGPbYxR3reFiYNBcS59eGhU62sDDmSltYeJE3GucUV7JwRzrfJqSz9XABDlh5wekDrndagaXPNBwnvKyQ+FRfXG8btf5gYqOdotBSKCCU5kQBoYidKSBs3koqaxj+8go6+7bh67sGXPjYOtOEf3UH/yi48ZtzfprFarL9yDGWJGWzbHcWe7JLiDUOsND1SRZ1eZKA4XfQI9QLh/NYLFxO79aPNpKaWczqx0fieGw/vNELLv0bDPiDvUuTi2CaJvtzS1lV2w20fn8e1RaTtm5ODI/0Z1S0P8Mj/PFs3UQ6O/atgJlXwmWvQd/f27sauzNNkxvf30BiehErHxlOuzbNMDAty4d/97Wtifr7pc3vLuzyAtvd1O06we0/NvoLSUfyy1iRks2K5GzW7sujssYKgJ+HK9/fPwQ/D1c4sBr+O1F3iJ/qeEfshFeg3x32ruaimKbJrrQiZm05wvyEdArKqrk0NoB3bu5z6o62mzqKMuD+reDkap+Cz9fMybY1sO7f3vTCibq29g348Sm4byv4dLV3NdLcNaHRomdkmravIUc2gkaNnrvDG2yf+7DRcN0Xdft+yDQhfSskzrOtW1hwGDDq9HuS1QTTasHRrMYc/AjGqKf0/eN0Dq2Fj8brZ7lGQAGhNCcKCEXsTAFh8/by4mT+s2If8/8wiPj2Xhd3sCVPw7r/XNRY0IO5pRR89yTdDv2XflVvk2d1x9fdlZFRfoyODmBwuC+tXZrZxeMGkFVUwYC/L2P68DAeuTTStvGtwbbOj2lL7Fuc1KmSyhpWp+awLNl2gT+vtApHB4M+HdsxOjqAUdH+vxod2GiYpu0HymOH4I/bm86F5nq0ODGTu2Zu4dkrYrllQCd7l1N/ds2GWbfD2Odh4H32rqZuLXocNr4Ld6yA4B72rua8lFdZWL8/jzV7c7k8PviX7xNmT7ON9/rDevA+z7WLmyPThPdHQUl2bfdz8/n6VVljYeOBfHq098LD7TQ3mxy/sWP8y9D/zgav77yV5tnGiw76I4z+P3tXY38FR+BfcTDyLzD0EXtXI81dUx0teiqNGj0/xVm2dZidW9k6t1t51d+5TBPSt8GeH6G6rO6PH9gdul1V98dtLkzT1pleUQj3bgIHR3tX1GIpIJTmRFeBRUTqSXpBOe+vPsAVPYIvPhwEiJ0Ca16H3d9B799d0CE6+bSGopXQdQTLp05hZWo2S3dns2hXJl9vPoqrkwODwnxPBB0BbZve+C57mLstDasJU3ufNE409krbmNHCoxoz2oy4uzoxvlsQ47sF1XboFrA8OYtlu7N54fvdvPD9bjr7tmFUlD8jo/3p28kb58YyivTgz3B4ne0iczO6uH6hKqotvLBwNxEB7tzQr4O9y6lfsVNgxzew/AXbSKjmEjhl7rSFg31ub3LhIEArF0fbmoRR/r9+cMxzkLLIFoDe8LXuok9dDGlb4PIZze7rl6uTI0PC/c68Q5fh0HEQrH7VNkK3sY+d3f0tmBaInWzvShoHr/YQ2s/WeaOAUOpT3j5Y9iyEXwrxZ1+SotHzag+XPm8bNbrlQ40aPRtLNXxzqy0wuml2/YaDYHs/EtLL9ksanmHYbvabdZvtfWL0RHtXJCLNQCO5YiUi0vy8vDgFE3j0eEfZxQqKt60buGv2hR8jcwccOwCxV+LZ2pkreoTwxvU92fqXMXw+rT839O/Anuxi/jx3J/3/toxJ//6ZGcv2kJheiDrOT880TWZtOUrfTu3o7Nvmfw8cvzCW9K19CpN65+hg0LtjOx69NIofHhjK6sdG8OwVsbT3bs0n6w5xw3sb6P3cEu77YhvztqVRUFZl34JXvQjugdDrFvvW0Uh8uOYAh/PL+L/LY5vmepLnwzBsa/k4ONkutv0/e/cd31S9PnD8803SvRdtoaUtpdDB3siQIYrgXle97u11e9177597XLde9boVJwrKkr1XS2mB7r33SnJ+f5yAgIAUkp6O5/165ZU2yfl+n7Q9TXKe8zzf7vD/XNPgp9vAKwim3Wd0NM7nHwlT7tbP0N8+1+hojKVpsPBxvY1sV66IOVJK6etJ1RfD2veMjubvpX4LIf0hYrDRkXQeg86Aki1Qnml0JKK7stvhu+vA7A4nv9g9TioZcTH0mwrzHoCqbKOj6bzmPwi5y+GUV7rvWtNiX0mnQGBfWPGq0ZEIIbqJbn40RAghjLE5v5pvNxRwxcQ4ooKctHi0UvoBhuw/9BZbRyJ1DigzJO57ppmb2cQx/UN58OQUltw+lXm3TOb2EwZiMSle+C2D2S8v5ZinFnD3N5v5ZWsxdc1tR/98uomNedXsKK3nrJH7VQmGxEP4YH2dBtEjRAd7c9H4WP572Rg2PDCD/1wwkpmDIlixs5ybP9/IiEfnc85/VvDm4p3sKK3r2KR7zgr9f8eEG8FNKoNLapt5dcEOjk8OZ0L/UKPD6RgBfeD4RyBrCWz42Ohojt6mzyBvJcx4RE8Sdkdjr4awJPjlTmhrMjoa42z7QT/B6di7wNxF1nt1ttgJeiXh0hegtcHoaA6uvkx/rUk5vXskKJwl+VT9OvVbY+MQ3deq/zi6RDzVNdcdPBCl9KSXMsF31+tJULGvLV/Bytdg7DUw5GyjoxEdxWyBcf/S9/m8NUZHI4ToBiRBKIQQTqZpGo/9uI1QX3eunRLv3MEHnQmaHdK+O5LA9AMT/Y495BqGSikGhPtx3dT+fPOvCay+5zieOXMIw6ID+XFTEdd8vI7hj8znvLdW8ubinWwv7uBERyfz1bp8PN1MzBoc+dc7U06FvFV6m1HRo/h4WJg5KIJnzhrK6nuO49t/HcN1U/tT12LlybnpHPf8EqY8t4hHfkhj+Y5y2mwuPuix5BnwCYORl7p2ni7i6V/Ssdo07p2dZHQoHWvEJXqrwl/vhbpio6M5ck3VMP9+iBoNQ7txRZnZDWY9C9W5emKoJ7LbYdGTEJIAg3v4wc+p90JDmd5Wt7Pa9p3+PlXai+7Lvzf0HS8JQuEa3am16P52txrN/kNvNSr+VJIG398A0eP0tuSiZxl+AXgGwIpXjI5ECNENSIJQCCGc7NfUElZnV3LzcQPw83Tyme69kqBXMmz9pv3b7m4vmnxauzYL8/PgnNHRvHHBSNY/MIPPrhrHFZP6UdXYypNz0znhxSVMeGoBd3+zhXmpxTS0WNsfWxfV3Gbj+02FnDgo8sC/62RpMyrAZFIM7xvEv48fyNybJrH8rmk8etog4kJ9+HhVDue/s4oRjlak320soKbRyRW6eWtg5wJ9vQp3J1U0d2Ebcqv4Zn0Bl0+KIybE5+836E5MJn0NN2sz/NyF18Ja+AQ0VsCs5/Tn1J3FTdITY0tfhMpdRkfT8dK+hdI0mHKXfsZ8TxY9BvrP0Nejbq41OpoDS50DoQP196piXyln6H/LpelGRyK6k92tRS3ucPJL3bNyV1qN/lVzDXx+AXj4wTkf6r9/0bN4+Oknfm77ASqzjI5GCNHFdfNP1EII0bFarXaemruNhF6+nDs62jWTpJyhrzNQU9C+7Q7SXrQ93MwmxvUL4a4T9TXXVtw9jafOGMzgqAB+2FTIVR+tY9gj8zj/7ZW8vWQXmSXdu7pwfloJdc3Wv7YX3S20P4QPkjajYh+9A724cFwMH1w6ho0PzODNC0dyoqMV6U2fbWTEY3qF7rtLs8ipcEIruSXPgFcwjLr86Mfq4ux2jYd/SCPMz4PrpvY3OhxjhPbXky3bfuiaJy8Ub4E1b+t/z72HGR1Nx5jxqF5NOPfO7rF+5OGy22DRU3qb1ZQzjI6mc5h6DzRV6e0EO5u6YsheKu1FDyb5FEBJFaFwrt2tRWc+pa9d2x1Jq9F92e3w7bVQnQNnfwh+EUZHJIwy9mr9+M7KN4yORAjRxUmCUAghnOijlTlkVzRyz+wkLGYX/Ysd5DhI1p4DDJqmJ6niJoNPiNNCiQzw4twxfXnzwlGsv38Gn145jssmxFFR38rjP29jxgtLmPj0Qu79dgvz00q6XXXhV+vy6R3gyfh+h/iZJp/maDPazoSu6BG83S2ckPJnK9Jv/nUMV0/uR0VDC4/+mMaxzy5ixvOLeWpuOutyKrHZ25kcKNwAmfNg/HXg4euaJ9GFzNlYwMa8au6cmYivRw+uRjrmBogYrFcRNlUZHc3hs9vhp9v0hPe0e42OpuP4R8KUu/V9eftco6PpOFu+hPIMmHp3968UPVx9RsDA2bD8Vb3VbmeS9j2gSXvRg/GLgNiJkPpNz0r0C9fpzq1F9yetRv+09HnY/hMc/zjEjDc6GmEk/956l4kNH0NjpdHRCCG6MPmkJYQQTlLd2MrLv2cyKSGUKQPCXDdRSDxEDtUPMByu4i16a7KU01wWlrvFxPj4EO6elcSvt0xm+V3TeOL0waT09mfOhgKu/O9ahj8ynwveWcU7f+xiR2l9l64uLK5p5o/MMs4cGYXJdIgz5Xf/zLd1wUod0aFMJsWIvkHcMTORebccy5Lbp/LAScmE+Xnwzh+7OPONFYx5/Ddu+3ITv2w9zHa+i5/V16cYc5Xrn0An19Bi5am56QyNCuCM4X2MDsdYZjc45VVoKId59xsdzeHb/BnkrYQZD4NXkNHRdKyxV+uVdL/cCW1NRkfjerY2vXowYjAknmx0NJ3L1HugpQZWvGZ0JPtK/VZvLdor0ehIOq+U0/Skd2ma0ZGIrq4ntBbdn7QahR2/w4LH9KTQ2KuNjkZ0BuOvg7YGWPe+0ZEIIbowSRAKIYSTvLJgB7XNbdwzKwnl6g9pg86EgnWH328+bY6jvWjHHWTrHejF+WP78tZFo9jwwPH874qxXDIhlpLaZh77aRvHPb+YSc8s5P45W/l9WwmNrV2ruvDbDQXYNQ7eXnS30ATolaK3eBWiHfqGeHPZxDj+d+U41t0/g5fPG86E/qHMSy3mmo/XMfzR+Vzy/mo+XplDUc0BEgZFm/UzjMddB57+Hf8EOpnXF+2gtK6FB09JOXRSv6foPUyvJNzwEexaZHQ0f6+pGuY/AFFjYOj5RkfT8cxuMPs5qM6FpS8YHY3rbfpUXzd56r1SPbi/iEF6d4KVb3SeioHaQr3NobSCPbSkU/U2iUeylrgQe+sJrUX319NbjVbnwtdX6Cdi9JSksPh7EYMgfhqsehOsLUZHI4ToouTTlhBCOEF2eQP/XZHNP0ZFkxTZAQfid7dvOpw2o5qmJ6ec3F60PdwtJo7pH8o9s5KYf+uxLL1zKo+dNojECH++Xp/P5R+uZdgj87nw3VW8uzSLXWWdu7pQ0zS+WpfHmNhgYkJ8/n6DlNP1qpfaQtcHJ7qlAC83Thnam5fPG866+2fwvyvHcuG4GLLKG7hvzlbGP7mAk175gxd/y2BrQY2+/yx5Fjz85QxjILeikbf/yOKM4X0Y0beHVZ4dypS7IDgefrgJWhuNjubQFj4BjRV6kqynJoxiJ+pVA0tf1FvLdVfWVr36ufcIGDDT6Gg6pyl3Q2s9LHvJ6Eh0ad+htxc9zehIOjffMIidpL9/78Tvc0Un15Nai+6vp7YabWuGzy/U1+b9x0fgfhifP0XPMf56qC+BLV8ZHYkQoovqoZ+uhRDCuZ6am46b2cStxw/omAkD++pVFIdzBnLJVqjc2akO2kQFeXPBuBjeuXgUGx6YwceXj+WicTEUVjfx6I9pTPu/xRz77CIe+G4rC9NLaWq1GR3yPjbkVbOzrOHvqwd32/2zT5M2o+LouZlNHBMfyv0nJbPotinMv2Uyd85MxMNi5qXfMznplaVc8MT7sO178gZciNVdqgef+HkbFpPijpnS+m4fbl5wyst6q66FjxsdzcEVbYY1b8Ooy/UW2z3ZjEf1asJf7uq+CYYNH0FNrl49KBUSB9YrEQafBavfgvpSo6PR34+GD9a7JohDSzldf19evMXoSERX1BNbi+6vp7Ua1TT4+d9QtBHOeFNfbkSIvcVP0zsWrXi1+743FEK4lCQIhRDiKK3OquSX1GKuOTaeXn6eHTfxoDOgZAuUZRz6calzOry9aHt4WMxMTAjlvpOS+f3fU/jjjqk8etogEnr58uXafC79YA3DHpnHxe+t5v1lWWSVNxgdMl+ty8fLzcysIYfZ0md3m9G0OS6NS/Q8SikSwv24dko8X197DGvuPY5nzxrCTe7f06B5cvKaIYx+/Dfu+GoTC9JLaLF2rmR7R1i+o5xfUou5bmp/IgI68H90VxE7EUZeAitf11tXdzZ2O/x8O3gFw7R7jY7GeP6RevVY5jzYPtfoaJyvrRmWPAfRY6H/dKOj6dyOvQuszXpFqZGq8yB/NQw63dg4uoqkU/T35e1ZS1yI3fa0Fn2657QW3V9PazW6/kPY8DFMvh0Gnmh0NKIzUkpfNqA0DXb+bnQ0QoguSBKEQghxFOx2jcd+SiPC35MrJ/Xr2MmTTwPUoQ8waJqelIqbZFh70faKDvbmwnExvHvJaDY8MIP/XjaGf46NIa+ykYd/SGPqc4uY8uxCHvo+lUXbS2lu69iER3ObjR82FXLioAh8PSyHv2HKafoHemkzKlwo1N3G2V7rGNOwCPdjrubJfx7L5AFhzN1SzGUfrGXko79xw6cb+GlzEQ0tXWvdzyNhtdl5+Ic0ooK8uHxinNHhdF4zHgHfcPj+RrC1GR3NvjZ/prdonvEweEl7WEBvGxyWBHPv7PytYdtr3QdQVyjVg4cjtL/eXnDtu1BbZFwcad/p1ymSIDwsPiHQ71hpMyrab5/WoucaHY2xekqr0fx1+klS8dP1k4OEOJhBZ4JfJCx/xehIhBBdkCQIhRDiKHy/qZDN+TXcfsJAvNzNHTu5fyTETICtXx/8AEPJVqjY4Ugmdj2ebmYmDwjjgZOTWXDbFJbcPpVHTk0hLtSHz9bkcsn7axj68DwueX81Hy7PJqfC9dWF89JKqGu2Hn570d12/w6kzahwtqpsWP02fHwWPBMHX14MfhG4TbyREwdH8tK5w1l7/3G8f+loThoSybId5Vz3v/UMf3Q+V3y4lq/W5VPd2Gr0s3CJT1fnsr2kjvtmJ+Hp1sH/o7sSzwCY/bz+mrHsRaOj+VNTNcy7X2+pPfR8o6PpPMxu+lqMNbmw9AWjo3Ge1kb44//0Ndr6HWt0NF3DsXeA3QpLnzcuhtRvIHIYBHfwiXJdWcoZ+mt34QajIxFdhbQW/avu3mq0oRy+uAj8IuDMd8Ak72PFIVjc9RPIdi3SW/MLIUQ7tKP0QQghxN6a22w880s6g/r4c/rwPsYEMegM+OlWKEmFiEF/vX93e9GkztletL36hnhz0fhYLhofS3ObjVVZlSxML2VxRhkPfp8KQFyoD9MSe3F8cjijYoMxm5z7Afqrdfn0CfRiXL92VmSGDYBeyXpF57hrnBqT6GGsrXo1auY8/VLuaDMcHA8jL4UBx+snD1g89mziYTEzdWAvpg7sxeOna6zJruSXrcXMSy3mt20lmE2K8f1COGFQBCckh9PLv+u34qxubOX/5mfozyslwuhwOr/EWXoF0OJn9BZ4YQONjggWPgFNlTD7GzDJeY37iJ0Ig8+GZS/plSTdYU2iNe9AQymc81+jI+k6gmJh+AV65eUxN+pVNR2pKkdvTXzcwx07b1eXOBt+vFmvIuwzwuhoRFewu7Xoaf/pua1F97e71ejr4/VWoxd9333eK9is8NWl0FgOl88D72CjIxJdwchL9TbtK17T16sUQojDJAlCIYQ4Qu8uzaKwppn/O2cYJicnoQ5b8ql625HUb/6aINzdXjR2IviEGhKeK3m6mTl2QBjHDggDILu8gUXbS1m4vYyPVuTw7tIsgn3cmZ7Yi+NTIpjYP/SoqzyLa5pZmlnG9dMSjux3nnwaLHpSbwUmH+5Fe9QVQ+Z8yPwVdi6C1jowu+uJwFGXQcLxh50gMJsU4/qFMK5fCA+enMyWghp+2VrML1uLuX/OVu6fs5URfQOZOSiCmSmR9A3xdu1zc5EXf8uktqmNB05ORsmZ9ofnxGdg50K91eilc4090Fa0Gda8DaMuh8ihxsXRmc14FLb/orca/eeXXbuipKVOr16NnwYx442OpmuZdBts/B/88ZxeWdSRUr/Vr6W9aPt4B+t/66lz9BbPXXnfFa4nrUUPbner0R9u0luNjr7C6IicY8GjkLUETn1d3gOJw+cVCMMv1N8/T38AAgw6iV0I0eVIglAIIY5AWV0Lry/cwXFJ4YyPN3BtP59QvQ3X1q9h2v37HmAoSdXbi46/zrj4OlBsqA+XhMZxyYQ46lusLN5exry0Yn5JLebLdfl4upmYnBDG8SkRTE/sRZCPe7vn+GZDPnYNzhxxhG+2U06DRU/Atu/1FiBCHIzdpldlZM6DjF+h2NEqxr8PDD5TTwjGHQsevkc1jVKKIVGBDIkK5PYTBrKjtF5PFqYW88TP6TzxczpJkf7MTIlg5qAIYlyULLSYFGaTcloiL6Okjo9W5nD+2L4kRfo7ZcwewbcXzHwS5lyrr2s25kpj4rDb4efbwCsYpt1rTAxdgX8kTLkL5t0L23/Wq5K6qtVvQWOFvvagaJ/AaL3V3rr3YcLNENyB662mfgt9RkJQTMfN2V2knA6Z1+qv9VGjjI5GdFbSWvTvjbhYT7bPewD6H6dXVndlad/rJ8yMugyG/9PoaERXM+5aWP2mXnV8/KNGRyOE6CIkQSiEEEfghd8yaLHauXtWotGh6OuYfH+9vo7J3m2K0uaAMkFi92gv2h6+HhZmD4lk9pBIWq12VmdVMi+tmHmpJcxLK8GkYHRsMMenRHB8cjjRwX+f9NA0ja/W5jMmLpiYEJ8jCyxsIIQl6R9iJUEo9tdYCTsX6AnBHb/prRWVCaLHwvQH9aRgeIrLDg4ppUgI9yMh3I8bpieQV9nIr6l6ZeGLv2fwwm8ZLpl3b7sThW5mE2aTOuj3FrNpr/sct5lMe77fWdaAj7uZW2d0gjaZXc3Q82DLl/DbQzBgZse3LATY/BnkrdLPnPcK6vj5u5KxV8OGj2HuXfpaTO4urvi1toKtxbljtjbAspf16hhJlByZSf+GDR/prcVOe61j5qzcBUUb4fjHOma+7mbgLL0TQOq38ncvDk5ai/697tRqtCxDP0mrzyiY+ZTR0YiuKChG7zK17gOYfDt4yomSQoi/JwlCIYRop4ySOj5bnctF42OJDzu66h2nSDoJfrxFryLcnSDUND0JFTsRfMMMDc9o7hYTExNCmZgQysOnpLC1oHZPsvDRH9N49Mc0kiL9mZEczvHJ4aT09j9gFdP63Gp2lTdwzZSjXOcp5TRY9JTeMtJP1kXr0TRNr/TN+EVvH5q/GjQ7eIfoycCEGXoLMoPWHYkO9uaKSf24YlI/SuuaWbCtlMrGVqfPo2lgs2tY7Ro2ux2rXcNq0xy32bHZNdr2fO94jE3/eu/vm9pse773cjPz1JlDCD6CSuEeTyk46UX9QNuPt3R868qmaph3P0SN0ZOV4tDMbjD7OfhgNix9wTUVl9YW/X/U1q/0lqbWJufPATD1HteM2xP4R+rteFf9Bybd2jFrUu5uL5p8muvn6o68AiF+uv5znPFo101oCNeR1qKHb+9Wo67sgKBpelePtO+gdJvzxy/aDBZPfS3evdYSF6JdjrlBf23Z8FGP6SYlhDg6kiAUQoh2evynbfh4WLhxeoLRoei8gqD/dMc6Jo4DDKVpUJEJ4/9ldHSdilKKwVEBDI4K4N/HDyS7vIH5aSXMTyvhlQWZvPx7Jn0CvfYkC0fHBeNm1g/YfLUuHy83M7MGH+XZu7vXIUz7HsZedfRPSnQtNivkrYT0nyD9R6jO1W+PHKavIzXgBOg9HExHt16ms/Xy8+TcMX2NDkN0lKAYmH4//HKXXk045JyOm3vh43r17Oxv5ID54YqdCIPP1luSDT3XOckhu01f/2jrV5D2A7TUgHcoDDvfNS0sA2Og9zDnj9uTTLxFbzO66Ck4823Xz5f6rZ7IN6LKuLtIOR0y5kL+Gug71uhoRGcirUXbb3er0fkP6ifZOavVqKZB0Sa9O0/ad3r1tDJDWKLz36f4R+qf52XtOHE0+ozU16lf+QaMuRrMcuhfCHFo8l9CCCHaYUlGGYszyrhnVmLnqkwZdKZehZS/GvqO0z8c9dD2ou0RG+rDlZP7ceXkfpTXt7BgWynz0kr4dHUuHyzPJsDLjemJvZieFM6Pmwo5cXAEvh5H+dLZK1FvM5o2RxKEPUVro946NP0nfT9tqgSzB/Sb8mdSUKpJRWcz5irY8hXMvVOvZPUJdf2cRZthzTsw+gqIHOr6+bqTGY/q1X1z7zzyqk9Ng/y1elJw6zfQUArufnqngsFnQdwUOcjUmfmG6fvtspf0lqO9XNgGv3wHFG+RFnhHa+CJ+vuB1G8kQSj2Ja1F28+ZrUZ3JwVTv9WTglVZelIwbjJMuEn/jO0T4tz4hXCm8dfDZ+fpxxwGn2V0NEKITk4+4QkhxGGy2TUe/2kb0cFeXHxMrNHh7GvgiXo7kq1f6+uVpc2R9qLtFOrrwTmjozlndDSNrVaWZJQzL62Y37eV8s2GAgDOGhnlnMmkzWj311CuJwPTf9aTg9Ym8AzQ13RLnK23FfPoBC2KhTgYkxlOfRX+M0mvJDzzHdfOZ7fDz7eBVzBMdUGbzO7OPxKm3AXz7oXtP+v/Zw5XSZqeFNzyFVTn6AmLAcfrVYkJx4Obl+viFs51zI16kn3Rk3DOh66bJ/VbQOnrHIkj5+mvVzqlzoETnpSqaaHb3Vp0wExpLdpeR9NqVNP0dVVT5+ifpauy9aRgv2P1Cu3EkyQpKLqOATMhpD8sf0U/mVyqkIUQhyAJQiGcTCk1E3gJMAPvaJomp9Z2E1+szWN7SR2vnT8CD0vnav+Hh59+EC91Dgy/EMozYOw1RkfVZXm7W5g5KIKZgyKw2uysya4iv6qR8f2c9KFQ2ox2T5VZ+oH59J/0s741O/hHwYiL9IP1Mcfo64UJ0VX0StIrkRY/pSeLBpzgurk2fQp5q+DU1/W1uUT7jb0aNnwMc++CflPB3fvgj63K1k8q2vI1lKb+eRB0yl36/yvPgA4LWziRTwiMuxaWPKtX+EUMds08qd9A3/Hg39s14/ckKafrLcfzVurvE0TPtndr0ZNelIP6R6I9rUY1DQo3/Nk+tCobTBaIO1Z//5N4kmFrgQtxVEwmff3BH2+BnGX6yeNCCHEQStM0o2MQottQSpmBDGAGkA+sAc7TNC3tYNuMGjVKW7t2bQdFKI5UfYuVKc8uIibEm6+uGY/qjB/WUr+FLy+BvsfoBxn+nSEVhJ3Za2PBOwQu/dnoSMSR2t1+KP0n/VKaqt8ePggGztIPskcOlYM7omuztsCbk6GlHq5bqZ+Q4mxNVfDKKH3tvEt/kSqao5G9FD6YDZPvgGn7VWLWl+rvFbZ8pbckB73rwOCz9RNX5D1D99BUBS8OhbhJcO4nzh+/NB1eHwsnPisnOTlDSz08G6+f4Df7OaOjEUZb8Tr8erfeWnTYeUZH03VV5+mtRnsP+2urUU2DwvWOSsHv9Mp5k0Vv/Z98mv7+XZKCojtoa4IXUiBqNJz/udHRdDtKqXWapo0yOg4hnEEqCIVwrjHADk3TdgEopT4DTgUOmiDsqWqa2iiqacLDYsbdYsLDYvrz2mzqdAm4NxfvpLy+hbcuGtnpYtsj4QRw84Hc5RA7SQ70dXbJp8Hip6XNaFdja9PPwkz/SW8fWpuvr/fZdzyc8ISeGAyOMzpKIZzH4gGnvArvzoDfHnbNAeyFT+hrc876VpKDRyt2op7wW/ai3prOO0SvTtryFWQt1iubwwfBcQ9ByhkQFGN0xMLZvILgmOth4eN6ZUzv4c4dP20OenvRU5w7bk/l4at3AUn7Dk58Wm/vLHomaS3qPPu3Gh19BRSshzTHmoLVuX8mBSffLklB0T25eelrEy96EsoyIGyA0REJITopqSAUwomUUmcBMzVNu8Lx/YXAWE3Trj/YNj21gvDqn+5nSc6mg95vUgqTArXPtUKpv9639+1KgcL5CbyS2maCfNxJ6NXJ1wwr2w4NZXoVhp8saN+ptTXqH1S9g8Ei6zt1CbZWvTLDbtWTgl5B+u/PK1hah4rur3IX1Bbqry3KmUk8DeqK9BMlguOdOG4PZmuFgnX6wU9bm54UdPMEnzD94naI1qOie7DboGCNvp6kZ6Bzx24s19e9dlX70p6ooRzK0sE3XN9vRc/UXK1X7fcZAWZ3o6PpHkq2QksdmNzA2qx39fAMBJ9Q/QQa2d9Ed2drg/w1+sko7s7vApIYOZo7pz7r9HG7AqkgFN2JvBoK4VwHykz9JQuvlLoKuAqgb9++ro6pU+rl78mAcD/smoZdA81xbdc0NMf13l//edtej7Xb97r/z/tccdqDh8VE3+AucEDNLxLaGsA71OhIxN9x89bX2WquAWqMjkYcDmXWDyZ4B+u/OyVn+YseJDBGP8jWUOr8sd289fGFc5jdISgOavL1xKtPmGtaw4rOy2TW96mqbKgvdvLgSk5CczbvYL3So7Hc6EiEkZSCkP6SHHSmkAQ9SWjx1KsKJSkoehqzG/j3gbpCaG1w/vgtchxDiO5AXhmFcK58IHqv76OAwv0fpGnaW8BboFcQdkxoncujk+79+wcJIYQQQgghhBBCCCGEEMLpZJEPIZxrDZCglIpTSrkD5wLfGxyTEEIIIYQQQgghhBBCCCHEHlJBKIQTaZpmVUpdD/wKmIH3NE1LNTgsIYQQQgghhBBCCCGEEEKIPSRBKISTaZr2M/Cz0XEIIYQQQgghhBBCCCGEEEIciLQYFUIIIYQQQgghhBBCCCGEEKIHUZqmGR2DED2aUqoMyDE6DoOEAuVGByFENyH7kxDOIfuSEM4j+5MQziH7khDOIfuSEM7Tk/enGE3TwowOQghnkAShEMIwSqm1mqaNMjoOIboD2Z+EcA7Zl4RwHtmfhHAO2ZeEcA7Zl4RwHtmfhOgepMWoEEIIIYQQQgghhBBCCCGEED2IJAiFEEIIIYQQQgghhBBCCCGE6EEkQSiEMNJbRgcgRDci+5MQziH7khDOI/uTEM4h+5IQziH7khDOI/uTEN2ArEEohBBCCCGEEEIIIYQQQgghRA8iFYRCCCGEEEIIIYQQQgghhBBC9CCSIBRCGEIpNVMptV0ptUMpdZfR8QjRlSil3lNKlSqltu51W7BSar5SKtNxHWRkjEJ0BUqpaKXUQqXUNqVUqlLqJsftsj8JwymlNKVUfwPmjXXMbWnHNp5KqdVKqU2Ofelhx+09al9SSj2klPq4E8QxVyl1sZPH7BTPradQSpmVUhuUUj86vu9R+5IQzqKUylZKbVFKbVRKrXXcJvuTEO2klApUSn2llEp3fHYaL/uSEN2DJAiFEB1OKWUGXgNOBJKB85RSycZGJUSX8gEwc7/b7gJ+1zQtAfjd8b0Q4tCswL81TUsCxgHXOV6PZH/qYEqpiUqp5UqpGqVUpVJqmVJqtOO+S5RSS/d6rL/j/q+VUgkHSmYppT5QSj12kLkeUkq1KaXqlVLVjnnHu/YZOp9Syl0pVa6U8j3AfdlKqSbHc6xSSv2klIp2YTgtwDRN04YCw4CZSqlxOGlfas/fx2GM1e4EqBH2+x3uvrx6ONtqmnaipmkfujpG4VI3Adv2+l5el4Q4clM1TRumadoox/eyPwnRfi8Bv2ialggMRX+Nkn1JiG5AEoRCCCOMAXZomrZL07RW4DPgVINjEqLL0DRtCVC5382nArsPBn4InNaRMQnRFWmaVqRp2nrH13XoH3T7IPtTh1JK+QM/Aq8Awei/g4fRk077PzYI+A3IAf4BtB3htJ9rmuYLhAILgS+PcJyjdhSJqsnARk3T6g9y/8mO5xgJlKD/fF1C0+2Ow81x0XDCvtSev49u6GRN03z3ulxvdEDC9ZRSUcBs4J29bpbXJSGcR/YnIdrB8V5sMvAugKZprZqmVSP7khDdgiQIhRBG6APk7fV9vuM2IcSRC9c0rQj0pAfQy+B4hOhSlFKxwHBgFbI/dbQBAJqmfappmk3TtCZN0+ZpmrZ57wcppUKBBUAqcIGmadajndgxxidAH6VUmGOeAKXUu0qpIqVUgVLqMUf3A5RSOUqpkY6vL3BUoiU7vr9CKTXH8fUYpdQKR4VikVLqVaWU+17PRVNKXaeUygQyHbfd7nhsoVLqssMIfxbw82E8x2bgK/SuDbvnn+1oX1irlMpTSj10sO2VUpc6WknVKaV2KaWu3uu+KUqpfKXUv5Xe+roN/QSW+ZqmrQLCgduUUjlAOhCvlPJybDvOURVYrfTWpFMOEsJB/z6UUknAf4DxuytCD+P5LXFcVzu2Ga/2a5+5f5Who0pxl+NnkKWU+uchfuSeSqnPHY9dr5Qa6hjjdqXU1/v9bF9RSr14iLEOyBHPMsf2NUpv9zV9r/sXKaWucHzdXym12PG4cqXU53s97hil1BrHfWuUUsfsdV+cY7s6pdR89GS66BgvAncA9r1uk9clIY6MBsxTSq1TSl3luE32JyHapx9QBrzveH/1jlLKB9mXhOgWJEEohDCCOsBtWodHIYQQQgBKb9H4NXCzpmm1RsfTA2UANqXUh0qpE9WB1y8JBhajJ3Av0zTNfoDHtJsjaXcRUAFUOW7+EL39bH/0pPHxwBWO+xYDUxxfTwZ2Acfu9f1ix9c24Bb0pMp4YDrwr/2mPw0YCyQrpWYCtwEzgATguMMIfxbw02E8R2/0asuVe93cgP68A9Erla5VSp12kCFKgZMAf+BS4AWl1Ii97o8AAviz+tYOHKOUGgR4ASOBY9B/h42AXSnVxxH7Y47bbwO+3p2k3c9B/z40TdsGXAOscFTYBR7G85vsuA50bLPiIM8bAMcBsJeBEzVN83M8l42H2ORU9IrUYOB/wByllBvwMXrr1UDHuBb038tHh5r/EMai//2FAg8C3yilgg/wuEeBeUAQEIWjktTx2J8czy0EeB74SSkV4tjuf8A6x/iPAk5d01AcmFLqJKBU07R1RsciRDcxQdO0EejLm1ynlJr8dxsIIf7CAowA3tA0bTj6+yxpJypENyEJQiGEEfKBvdfBiQIKDYpFiO6iRCkVCeC4LjU4HiG6BMeB+6+BTzRN+8Zxs+xPHciRlJ2IfrLQ20CZUup7pVT4Xg+LRq8ke1/TNGecVHSOo9qsCbgSOEvTNKtjzhPRk8UNmqaVAi8A5zq2W8yfCcFJwJN7fX+s4340TVunadpKTdOsmqZlA2/u9bjdntQ0rVLTtCbgHMdz26ppWgPw0KGCV0r1A9w0Tdt+iIfNcTzHWvTE47O779A0bZGmaVs0TbM7KjU/PUB8ux/7k6ZpOx1tRBejJ5sm7fWQNuARTdPaNE37GahHrxY8Eb3V6KOaphWgn1VepGlaC3AB8LOmaT87YpgPrEVPeu4//+H8fey/zWE/v8NkBwYppbwcrYlTD/HYdZqmfaVpWht60s0TGOc4s34JcLbjcTOB8r9JBM1xVFjuvly5132lwIuOn/vnwHb0ZOj+2oAYoLemac2apu1er3E2kKlp2keOv9NP0X9vJyul+gKjgfs1TWtxtDb/4RBxCueZAJyilMpGX4ZhmqO6VV6XhDgCmqYVOq5LgW/RlzuR/UmI9skH8h3dIUDvTDEC2ZeE6BYkQSiEMMIaIMHRusgd/aDb9wbHJERX9z1/nt1/MfCdgbEI0SUopRT6WhrbNE17fq+7ZH/qYJqmbdM07RJN06KAQUBv9DZ7u21CrzKbq5Qavtftu9uMuu03pBuHXp/wC0e1WTiwFb3KDfREihtQtDspg57c290yaTEwSSkVAZiBz4EJSm9RG4CjskwpNUAp9aNSqlgpVQs8wV9bNO7dbr33ft/nHCJ20JM7f9de9DTHc/QArgcWO+JGKTVWKbVQKVWmlKpBr8I7YAtJR9XeSqVUpePnMWu/x1YAQbsr49CTruPQT/5S6AkP2HdfigHO3jv5hZ4EjDxQDIfx97F/zIf9/P6OI2H7D8cYRUqpn5RSiYfYZM/v0VHpmu+IF/Tq1AscX1/A31cPnqZpWuBel7f3uq9gv2R5zl7z7O0O9N/DaqVUqvqzfW1v/vp3loNeCdobqHI8973vEy6madrdmqZFaZoWi/4ZaYGmaRcgr0tCtJtSykcp5bf7a/SOAFuR/UmIdtE0rRjIU0oNdNw0HUhD9iUhugVJEAohOpxjvZ/rgV+BbegH6Q51JrYQYi9KqU+BFcBApa/9dDnwFDBD6etZzXB8L4Q4tAnAhegVGhsdl1nI/mQoTdPSgQ/QE0F73/4S+u9ivqN9JUAReiIwdr9h4jiMhIamaeXA1cBDjjOf84AWIHSvpIy/pmkpjsfvQG+TeSOwRNO0OqAYuApYulfr0zfQq7ESNE3zB+7hry3W907uFLFvd4W+fxP6YbUXdcRsc1TH2tCTcKC3j/weiNY0LQB9Hb+/tIBXSnmgV9g+h77OTCB6YnL/x0YCC5VSmx1fr0Ov2msGZh9gX8oDPtov+eWjadrf7msH+Ps4UEXpoZ7fgR7fAHjv9X3EfnP+qmnaDMdzS0evZDyYPb9HpZSJfTtlzAGGOP5+T0Jf//JI9XGc5LBbXw7QkUPTtGJN067UNK03+t/660qp/o7Hxuz38L5AAfrfY5DjgPre9wnjyOuSEO0XDixVSm0CVgM/aZr2C7I/CXEkbgA+cbzXG4Z+8pvsS0J0AxajAxBC9EyOFlR/d+a7EOIANE077yB3Te/QQITo4hyt9g60Li7I/tRhHNVYs4HPNU3LV0pFA+ex75p5AGia9owjafWbUupYTdO2K6W+Bh53tF+sBc4CkoG5hzO/pmnpSqlfgTs0TbtFKTUP+D+l1P3o7TLjgChHe03QqwivB65zfL/I8f2jew3r54il3vH8rgXKDhHGF8D7Sqn/Atnoa8odkFLKC71F2qLDeX6OJNIp6GvQbdsrvkpN05qVUmOA89Fbh+7PHb0CsQywKqVO5M8KjD0cbTyHO+bLBj7WNM2ulHoPSAROB0qAMUqp9ejr8a1RSp0A/IZetTkO2KFpWv5+8f/d30cJEKWUctc0rfUwnl8ZesvQfujrG4Je+Xmno7VmDXD3XvOHo6/39zt6dWQ9erL1YEYqpc5AT1DeiJ5wXun4OTUrpb5CT2Cu1jQt9xDj/J1ewI1KqdfR17NM4gDvrZVSZ6Ov0ZiPvs6m5oj/Z+AVpdT56H9/Z6LvNz9qmlaulFoLPKyUugf97+1kpONHh9I0bRGO/VzTtArkdUmIdtE0bRcw9AC3y/4kRDtpmrYRGHWAu2RfEqKLkwpCIYQQQgghhJHq0BMwq5RSDejJlK3Avw/0YE3THgXeAX5XSsUD/wIqgc3oa59cD8zWNK2kHTE8C1yllOoFXISeGEtDT6h8xb6tLxejJ6CWHOR70Nuhnu94bm+jtyI9KE3T5qK3zFwA7HBcH8x09IRP8988px+UUvXoicrHgYv36tjwL+ARpVQd8AB6guhAcdWhJ7m+QP9ZnE/7kkS3AVvQ28tXAk8DJk3T8oBT0Ssry9ArCm/nwJ9P/+7vYwGQChQrpcr/7vlpmtbo+Hksc7Q3HedYA/Fz9L+hdcCPe81vcsxV6HgOxzrGP5jv0FuSVqFXKJ/hWI9wtw+Bwfx9e1Fw/A73uny7132rgASg3PF8znIc9N7faPSfXT367+4mTdOyHI89yfHcKtBbkZ7kqKoF/Xc91vGcHwT+exjxCiGEEEIIIboQte+yBUIIIYQQQgghOitHxdhWTdNeNzoW0X6OKsV0IELTtNojHOMS4ApN0yb+3WOFEEIIIYQQ4mCkxagQQgghhBBCdB0bgR+MDkK0n2NNwluBz440OSiEEEIIIYQQziIJQiGEEEIIIYToIjRNe8voGET7KaV80NdLzAFmGhyOEEIIIYQQQkiLUSGEEEIIIYQQQgghhBBCCCF6kgMtAi+EEEIIIYQQQgghhBBCCCGE6KYkQSiEEEIIIYQQQgghhBBCCCFEDyJrEAphMJPJpHl5eRkdhhBCCCGEEEIIIYQQQohDaGxs1DRNk8Ir0S1IglAIg3l5edHQ0GB0GEIIIYQQQgghhBBCCCEOQSnVZHQMQjiLZLqFEEIIIYQQQgghhBBCCCGE6EEkQSiEEEIIIYQQQgghhBBCCCFEDyIJQiGE6EZaG5upyMozOgwhhBBCCCGEEEIIIYQQnZisQSiEEF1YTVEZ2T/+RuOCJfivW0V8Vir+dhtFG7YSOSTR6PBEF9Zc10Du4lVULVmB2rCBwIw02jw8qU8YCCmDCBgzgsgJowiIDDM6VCGEEKLLaa6tpywtk5qMLJpy8rD4+eIVG01g/zhC+vfFzdPD6BCFEEII0YVpdjv5a7bQWlNL38lj5L2FEOKAlKZpRscgRI/m4+OjNTQ0GB2G6CKKt2SQ/+N8bEv+IGzzWmILd2FCw6pM7IoeSE1cf0Yv/oE1j7zI6PtvMjpc0UU0VtWQu3AFNUtXoTZsIDRjK32LsrBodgCqvfzIjx2IpaWFPgU78Wtp3LNtSUAYxX370zQgCfPgwQSNG0HU+BF4+vsa9XSE6NE0u52a/BKqs/JQFjNmNzfMnu57ri1ubpg9PbB4uGNxd8PsZkGZnNtURLPbsba20dbUTFtzK7bmFqzNLVhbWrG16N/bWlqwtbZhb27BJyqC6HHDnRqD6Do0u5225lbcvT2NDsVp2ppbKEvfRfX2XTTuyqItKwdTXh4exYX4lRUTXFVCUGPtQbe3o6j0DaQ6MJT6kHBawiOxR0Riju6DR0w0fv1iCOofS2B0hNP33+4k86eF1KXvwN7UhL25Ga25Ga1Jv1bNLdDSjGpuhtZWTC3NmFpaMLW2YG5rxdzSgqWtBUtbG5a2FtzaWnFva8XN2kqbxY3iqH7UJyRhGjKYwLEjiZo4Cu+gAKOfshA9XlN1HcUb06jenEbL9kzIz8fvnDNJ+eepRocmDqGpuo6KjCx8wkMJiultdDh/a+V19xDxzaeUJQ7BPnYsodOPJWbyaCwe7obGZW1pJev3FVT88hvuK5fTd9sGQuurAGi2uJMdPYDqlKFYxo0l/LjJRI0eLO8jjpBSqlHTNB+j4xDCGSRBKITBJEEoDsbWZiVnyWrK5v6OecVyolLXEVFTBkCDuxc7E4bQMHIsfsdNod9J0/AOCsDWZqXJN4DUqbMZ+8sXBj+DnkOz22lpaKKhvJKmsiqaK6toqaimraoGa3U1tuoa7DU1UFuLqbYWc3095oY63BvqsXp60hIajj0iEtUnEvfoKHxiowmI70twP+dXENSVVpC3YDm1y1Zh2biR0MxUoktzMTuSgRU+geT3S6IxZSieY0cRMW0CEYMG7PngoNntlKTuoGT5Wpo2bMKSmkpQVgZRxdl42NoAsCkThaF9KIvpT0tiCh7DhhAybgR9Rg02/EOTEN2BZrdTmVNA2YZU6rakY8vIxJK9i4D8bMJL8/Fvrm/XeG0mM1aTBZvJjNVsxmq2YDf9eW2zOK7NFpTdjtlmxWyzYrG26dd2Kxar49pmw91ubdf8rSYL5es203tYUru26+k0u53aonIqM3ZSuyOb5l05aLl5mAoL8CopxG6x4HbvvST/Y7bRoR5U2mc/4HbH7QRVleKdm9UlEiy2NisVO3Ko3L6Thh3ZtGXnQF4e7kUF+JYWEVRZQkhdFSb2/Zxd4+lLRXA4dWERNEf0RouKxhzTF+9+sfj3j6W1ppb6XTm05BZgy8vHXFyER2kRvuWlBNWUEdxQ85dYWsxuVASEUhMURmNoOG3hEdCnD5boPnjF9sW/XwwhA2K7xM/V2apyCvGLi95zstOBNFvcad19cXPHanGjzc0Dq7s7VjcPbO7u2Nw9sLt7YPfwwO7hCe7uqIYGAndlEFW0C++2FkBP6haGRFIWM4CWxGTchg8ldPxIQ977aHY79eVVVO3Moa2+iYCY3gTG9JH3YEehpaGRpooamqpraK6sweLpTuTwFPmZGqSutIKSDanUbEmndXsG5p078cnPIawkj1615fs8ttVkwd1uZUvSaDyefpIBJ083KOqeydZmpXJXLlWZ2TRk5dKak4e9oABLcRGepcX4VZQSXFO+571rQXAkvcvyO3XSSrPbKQ2OwGS3YbHZCGrUX58b3TzJ6pdC7bCReE2cQPTsqYTERbs0lqbqOnb++Dt18xfiu3Yl/XZswae1CYDC4EgKUkagTZyEOdCftpWr8d+ygdic9D2vXTWevuTEp9AweDheE8YTNXMyof1jXRpzdyEJQtGdSIJQCINJglDs1lRdx665C6n9bRE+q1cSl7lpT6VWqV8IeSkjaRs3ntCZ04idMu6gH0i3pIzFp7qCfgU7OjL8LqutuYWG0koayipprqikuaKKtopq2qqqsNfUoFXXoNXUYKrTE3uW+jrcG+vxaGzAq6ker+YGfJobD+uAeJvJTL2nD02ePjR5+dLi5Y17cxOB1eUENVTvSdLtZkdR5RNAdUAo9SFhtISGY4uMxBQZiXvfKLxjogjoF0NwfN8DVl7UFJSS9/tS6lesxm3TRnplphJdnr/n/lL/UAr7JdE0aChe48bQe9oxhA2MO6IPZNaWVgrWbqFi5XpaNm7CIz2NsJwd9C4v2PO8Wsxu5EfEUhU3AGtKCl7DhxI6aih+UeH4hgRhspjbPa/o2oq3ZJD1yDMoux3NZAKzGZQJZTKhWcxgMukXs1n/u9z9GMfXavfXZjPKbAKTfq0sFvqdfzrBcVFGP8UjptntVOzIoXRDGg1bt2HNzMQjaxf+hTlElBbg2/pnJa9NmSgJCqciIprGvrFo8f2xREeB3Y7W2gZWK/a2VrBa93yvtenXqrUNzWZF7f7e2gZWG6qtDWWzoqxWTFbHta0Nu8mMZrFgd3NHs1jQ3NzR3CxoFjdwd0dzcwM3/Wu1+9rdDdzcUe7umNzdUO5umDw8MLm7YW9tI+WWq9gw82zG/vQ/A3/if8/a0krBms1YPD1w9/PB3dcXD38fPHy8XFKFWVtYSsX2XdRl7qI5Oxd7Xj4WR/LPv7yE0OoyvNua99nOpkxU+AVTFdyL4Ipiwuoq2TDiWIJfeo6YiaOcGuPRyFu1ifJ/3czw9Yuo9vIjsKmOdc+8wcjbrzE6tENafd8zDHvy3r+85je6eVIW1Iva0Aiawntj6xOFOSYar/5x+Cf0IzQxHp+QwKOau6WhkcqdudTsyKYxK4/W3DxwHGT1KivBv6qMkAP8TQDUu3tTGRBCXVAYTWHhWHuFQ+/eWPr0xis2Gv+4aIIT4o46xs5kwysfMPzGS1n7xCuET5uEm7cXbr5euPt44+Hrg5un+1Hvt3arjcL1qZSuWEvL+k14pKcSmpVBn/3e++RFxlLVbyC2lEF4jxxG5MTRhCbEtnv+tuYWKnflUpuVT0NuPq15hdiKijAVF+NWXop3RRn+NRUE1VbiZW3ZN1YUNd7+1PgHUR8YQnNwGNbQMLSICMwREXhEReIT3Rv/mCiC4qK6VBs6W5uVtuYW2pqasTW3Ym1twdrchq25GWtLK6019bRWVdNWW4u1uhZ7XT32ujq02jpUQz2qvg5zQyPmxnrcmhpwa2rEo6kRz5ZGvJob8WptPuD7/N3vayv7DcCWrL+vDZ8wmvDk+E6d3HCG8p25tNbWY/H0wM3bC4uXB+4+Xrh7eTrlue/uhlCyYSt1W9Npy9iBJWsnfvk59CrN+8sJE+W+wZSFR1EfFYM1Lh63xAEEDEqk17AUPPx92HjPkwx871WCGmvYMOJYAp97krip4486zp5Ms9upK6ukansWtbuyac7Jx5aXjyoowL20BJ/yEgKrSgmpr/rLZ1yrMlHpF0x1UBgNIeG0RkSgRfZGFRUy9ufPyPjhdwacNM2gZ/b3Mn5cwICTp7PmoecZdf9NFK5PpejXRdiWryB46wZi8zNxs9sAPeFZlDQM6+gxhBx3LDFTxx9Vx4SqnEKyv59Hy8LFBG1YTb/c7bjZbdhRZPWOp3zYKCzHTibqpOMIT0444BjWllbylq+j7Pc/0FavITRtEzGFO/ecUFMcEEZhwiBaRozCb9IxxBw/Cb9eIUccc3clCULRnUiCUAiDSYKw56opKGXX1z/RsmjJPm/uALIi4igdMgrzpIn0PmkGkUMGHvaHrRUXXs+YT96gsbi0U7+Ra21sJv1/c7A3t6Lt/tDgeE3a/dqkdn9v1/56/1+2cQxst6M1NmGrrkarrkHV1mCqq8NSX4tbfR0ejfV4Ntbj3dSAT3PDXw6gHEiL2Y0GT28aPX1p9vKhxduXVh9fbD5+2Hx9sfv7g78/poAAzIEBWAIDcQsKwCMkEM/gILzCgvAJDT7kQWRrSytV2flU78yjISuXlvwC7PkFmEqK8SgtxqeyjMDqMoLr/5pIBKjyDqAqMJT64DBs7h6EZ2fQu7Joz/1FgeEUxyfRPHQ43uNG02faBELj+/7tcz9azbX15C9fR9WqDdg2b8Y7I53wvB2EO6phd7OjqPf0od7LlyZvP5p9/Gj19cfq74/NPwAtMBAVGIg5OAhLcBDuocF4hobgHR6CT3ioSxOMdqsNm9WKyWzG7CbLNzvTqpnnMPbXL2l088Bst6M0DZNmP2TFx+FaP2oqI9YscEKUrlVTVEbe78toTNuOLTMTj+xdBBbmElGWv+fsXtBPMCgOjqQyMprmvnFo/fvjlTSQ4KFJhA9J6tLtGVdPO50hf/xMQ8YOl59pfTRWnHcN4z9784D3NVvcabG40+LmQau7B21uHrS5e2L18MDq4YnN3RObpyd2dw80T080Ly80Ly+Ulxd4eqJVVGIpzMertIiAilJCqkv3+f2Dnvwr9w+hOrgXDWERtEb2gago3GKi8YmPI3BAP0ITYvacQNRUXcem2x5i0Ef/wautmbXHnUH8q88QOiDO5T+rg6nJL2bbv25n5E+f0mpxZ8uF1zL46ftpjIsnP2EIw9ctNCy2w7GzTwJubS0UXXI1nv1i8EvoR0hif/wjQztFQmB35VhlRha1O3JozsnDWliIKizEvbQEr4pSPZFYW4GntfUv2ze4e1EZEEptYOi+icSo3nj1jcavn55I9A0NMuDZtc/KMy9j+HcfQ001Hj7eHTr3n+991mPbtBnvjG1E5mYSVle55zHVXn4URPWnPiERhgwhYORQrM0tNOflYy0oguJizKUleJaX4ltVTmBt5Z4qlf1Ve/lR7R9CfVAIzSG99N9bRASWPr0xeXnRVlyMvagEU1kJ7uVleFWW41dTQVBd1QETyrvHrPEPpj4wlObgUNpCw9DCw7FERODWOxxlMmNvbcXe0orW1orW2obW2orW0qqfgNLWpl+3tkJrG1jb9BNP2lqhTT8pxdTW6jgJpW3PtXnPdRsmmxWz1VGx7qhcN9usuFnbsNhtWGxW3GzWv1Trtut3ZXGn0cOLZg9vWjy8aPHyodXLG6u3DzZvH+w+Pth9/cDXF+Xnh9nPD1OAP/aGBmybt+CdkU5EbuY+VWu1Hj4URMVTGz8QBg/Cd+Qw+kwYTWDfyCOOszPJW7mB8IljcLcd+OTIVpOFNosbbWb92mp2w+qo0LVa3LC5uWOzuGFzd8duccPu7o7dTb+Ym5vwL8z9SzcEO4rSwDAqwqNpiI7FHh+PR+IA/FMSiRiRclgnN9SXV7Hl9kcY9Olb+LQ0sX7CTCKef4qoMUOc9aPpMYq3ZKBNnkxkdclf7qvx9KUyMIy6kF40h0Vg690bU5/eePTti09cNEEJsQTFRR/wM1VNfjE+ffuw5uzLGf/5Wx3xVI7IinOvZvQX79CQW0BAVMRf7m+qriN7/h/ULPwD97WriUrfRK+6CkD/n5MVk0jN0JG4TziGqBOn0isp/oDzaHY7RZvSKfh+HtrSpYRvWUtMSQ4ArWYLO2OTqR41Du+pxxJ7ygwCIsOO+Dk1VdeRs2AZ1YuWYVm3lojtm4mqKAT0/S83PIaypCHYR44ieOrEo050dgeSIBTdiSQIhTCYJAh7prVPv8GAh+7Av7meFrMbO+OSqRkxBu+pk4k96bgDvtE8XJvf/ZwhV5zLlve/YvAlZzoxaudafe/TjHniLpfP0+DuRYOnD41evjQ7EnttPn7Y/Pyx+/mhBQSgAgIwBwViDgzAPTgIj5BgvEL1xJ5vr5AOP7B0KLY2K1VZeVTvyqU+K1c/e7ygEFNxEe5lJfhWlOLe0kR57ABahw7Dd/xooqdN7HQHJWoKSilcvpb6zWnYqqqgqhpVU42ptga3ulrc62vxaqjDq7EO36aGfaqlDsSmTNR7eNPg7UeTly92sxllt2Gy21F2OybN7vhav820+1qzOxJT+v3mPdf6/XsnqhrdPNg6YSYBN1/HgJOnd4qDwV1ZW3ML9SG92DV0PCOX//KX+zW7HbvNjq3Nit1mQ7PZsVkdX1vt2K1W7HYbms2Gvc2G3WZHs+tf5z3yNGO+/5iClRuIHjvUgGd3eOrLq2jq13/PQeNWs4Wi0D5URfalOSYO1b8/3skDCR6aQvighG7byix3xXqijhnFqnOvZvynbxgdzgFV5RTikRDPjoHDaTnrHOxNjWiNTWhNTajGJmhuQjU3oZqa9TXMmpswt7RgbmnGrbUZt9YW3FpbcG/TLx5trfucoLI7+VcVHE5jWAStkb0hOhq3mL749IshKLEfIfExR/Q3UJmVT8aNdzHi58+xms1sPvtyBr30WIcmeVoaGtlw5+Mkv/syPi2NerLyjf/bc6LKylMvYsRPn9KUV3hUB7hcKXvxamKnjGXlTQ8w7sWHjQ7nqGh2O7UlFVRnZlOXnUdzbj7W/AIoKsKtpPjPRGJNxQFPpFrz0AuMfvDmjg+8Hbb3G4TdZCZpxyajQ9mjOreIgj9WU7d2A6atWwnYmU5Uwa497eD21mJ2o9IvmNrAEBqDe9EaFoY9PAJT79549OmNd0wf/GOjCeoXdVTvUxuraqjalU9dTj5N+YW0FhbpycTSEtzKS/GqqsCvuoLAuqq/fS92MDZlwmoy02Z229PG2ma27Lm2mS16K2uzBZvFDbvFDZubG3azZa+KdTfsFsueKnXNccHdHSxu4O6GcncHN71KXbm5odw9UO5uWPz8sAT64x7oj0egP57BAXgGBeITHOC019WaglIKlq6hbt0G2LIFvx3bicrfuU+Sq8wvmOLo/jQMSMI0eDBBY0cQNWEUXoF+Tomho6ydOIuUVQvYcucjaDY7tLSgtbQ4rluhtQXV2opqbUG1tqFaWzC1tmJq231p09f5tLZhadOTwRZrK27WNtosblRG9qWpbyxav3g8EwcSNDSJ8KFJePo55zh8TX4xaf9+gGHf/Bc3Wxvrp59O3xeeJGLQgautXE2z29k+Zz41735I79V/UPXkswy54lxDYjlc68afQMraRWy84hYssTF4x/YlIL4vIQPijnod+i3JYwgsLyaqOLvTftbKCY+hLjiMQdvWHvY2xVszKfhlIW3LlhO4eT1xOel7lucoCQijYOBQWkePwWfUCBo3b8WyYjnRqev2JBZrPXzIGjiUxrHHEDhjKnEzj3XaPnEw1blF5M5bTMOylXhtWEffHVv2VO+2mi1kRQ+g9cFHOvUxJ1eSBKHoTiRBKITBJEHYs9QUlZF59sWMWjaX7XEp2J58mviTpjo1AVVTVIZf73BWXXQD4z98yWnjOtvaibOI2byKmi++0W9QynGlHN+bHFdqn+s9t+//eJPaM4a7nw/eYSH4hgZJxVc3YW1ppaGskvqSCprKymkuq6StohJrRdVfE4y1NSi7Dc1kRjOb9daVe329u5Xl7vsx/3k/jou2p62l2dHq0ow5N4dBf8zFu62ZXb3jKTvvYpJuvxb/8FCjfzxd0qY3/8fQa/7Jxlf/y7DrLnTq2OU7svFPTGDDjDMYO/dzp47tTCuvu4dxrz/J2sdeJuqUEwhL7Ndj/2etHzOd/ltWo3KyO2X1+4rzrmXsZ2+St3gVMZNHO2XM3evXttQ3OvVA9cEUrN1C8Q23MXLlPCp9Asi85lZGPHanS1sJanY7G154l/AnHqRPZRGbB43H75UXiJsydp/Hbf9uPgNPO57VD/wfYx6+1WXxHI0V513DmM/fpipjZ49Zn2dPC7kdOdTtyqU5N5++zzxCQcIghq/tvNWeTdV1WEKCWHvWZZ26CgX0LgXFW7ZTtnojZh9v/GKjCIyPwT88pNMdHG+uradyVx71eYVodjtmTw/MHh6YPdwwu3tg8XLH7OGBm4c7Zk8P3Lw8cPPw6LEt5DW7nbLtWRQvX0vjuo2Y01IJ2rmdqOLsPRW8e69d2Zw8iOSn7iegTy+DIz+4nGXriJo0hjWnX8y4r98zOpyjUr4jm50338vwuV+gKcWGk85jwAuPd1h7+qxFqyj+z3vEzPue3lXFNFvcaXL3osnDi+DszKNOtLlK2uc/kXzuSay44HrGf/SK08dfdcdjjH32fnIWr3ba+y1nylm2jpiJo1h1y0OMff7BIx6npaGRnAUrqPx9CZY1q+mdvmmf7j+l/qHkpYzEeswEwk6cTuyxYw3/X6rZ7RRvzaBw3hLaVqzEb8tGTI8/RtLZswyNyyiSIBTdiSQIhTCYJAh7jtRPviPkuisJra1gzcU3MPo/z7jsYGBWZD/qQsMZsmWFS8Z3huLAXhQkDmPkynlGhyLEYasvryL1uTcI+eQD+udn6lWFE08k8JbrSZg9tdMdzOvM1k6cRcK6P/CqKHNJi5rV089gyJKfaMzY2SnXImxpaKQmsi+lfWLbdQZyd5X500ISTprGymvuZNwbTxkdzj5q8osxx8ezffiEbvGalfHjAtpuvY2UzA3khUZRdtcDDL/lcqf//8r44Xdst9xK0s7NZEXEUf/4Uwy+7JwDPlaz2ykMi6Iisi9Dtq50ahzOoNntFIX2oSKyL4NTVxkdjqHWTD2V/qsXEVBTafjByoNJ/eQ7Ui44jU1vfMTQay4wOhwh9mFrs1K4bgvlK9fTvEFftzs0K4OosnzWTZrF6CU/Gh3iQa2dOIvk1Qto2p7ZqVuCt0fhxm3k33I3Ixf/QIvFnU1nX0rycw+7pJq9cOM2cl5/j4gfvyGuaBdWZSItZQwtZ/2DxGsvJGfeHwy68HRWXHQD4z982enzHy1bm5Wsfsn41VXjn73TJdWv5RlZBA+MZ9UlNzH+/RecPv7RWnHlbYx/5/8oSc046Bp/R6p8RzZFS9cQMmxQu5aYEcaQBKHoVjRNk4tc5GLgxdvbWxPdW3N9g7bizMs0G0rLDY3Stn//m8vnXHXcmVqNh49ma7O6fK4jUbhpm6aBtvKWh4wORYgjlvHjAm3VcWdqDW6emgbajt79tZW3P6rVFJcZHVqn11BZrTW4eWorZ5zlsjmy/1ijaaAtv+gGl81xNFbd+7Smgbb5/S+MDqXT2Jw0WivzDdaaauuNDmUfyy+4TtNA27VgudGhOI3dZtM2vvGRlhURq2mgpcelaKmf/eiUsQs3bdPWHDNT00Ar8w3SVt39lGZtbfvb7Zafe7XWpkxa+a5cp8ThTNu+/kXTQFv90PNGh2K4Vfc/p2mgZS1aZXQoB7X8kps1DbTq/BKjQxHisC0/63LNhtJ2/rbM6FAOKPuPNZoNpS0/63KjQ3GJ7KVrtbVjZ+j/Ozx9teVX3qY1VFYf9bjlu3K1lf9+WNvWb7CmgaaBtq3fYG3lLQ9pZTty/vL4teOO15os7lrB+tSjntvZdr93XfPEqy6dZ1vcIC0zKsGlcxyp7X2TtO19k4wOQ3QCQIPWCY4py0UuzrjI6QhCCOFCWYtWkT9gKOO+fo81x59FSMZWBpw83fUTH3MM/i0N5C5b5/q5jkDB93oFRuiJxxkciRBHLmH2VMbM/wpbfj6r7nwczaQY++z9WKKjWD3tdDJ++B3Nbv/7gXqgtLf+h3dbM74Xu66yI2biKDYOnUjiV/+lqbrOZfMcCVublYi3XmVHVAKDLuqZ63YciLrrLkLrK9n0hPNbVh2pmqIyUr78gPWjphI3dbzR4TiNMpkYes0FROdmsua+ZwkqLyb53JPYMHIKOUuPrKK1rrSCFf+4iuARQ0hZvZAV51+LZ9ZOxjxx52G1zo246mIsmp0dr31wRPO7Us27H9BidiPx2ouMDsVwfU6dCUDJT523mtZn7UqyIuI6datGIfaX/OIT1Hv6UHfr7UaHckDld9xHs5sHA595yOhQXCJmwkhGrpzHjl8Wkz1wKOPffo7GvnGsvOUhWhrat/5mfXkVax59ic2DjyEgPpax//cgHo31rLji3xSuTyVx52bGPv/gnnV49xb17qvYlaLkiuuc9dScoq60gvgXn2Rb/BBG3nmtS+eqnnkS/fMzKdy4zaXztFdJWiYDcrdRcXzPbKkphOi+JEEohBAuYLfaWHnj/fQ+bhJBNWVsfPW/jP31C7yDAjpk/sgTpwJQ+mvnXB/GtuQPaj18iN1vDSIhuiK/XiGMfeoe4nPSyfxxAVsnz2bQ0l8YcMpx7OqbyKo7HqOutMLoMDsV8+efUeofSuI5s106j9udtxPUWMPmJzpXm6bNr31I37I8am68VdoH7SXlgtPI6JtEn7dfwdZmNTocANLueRz/lgYCHn/Y6FBcwuxmYfSjt+GXm8WKK/5N/9Q1RE0ey6rjz6Y8I+uwxrC2tLLqjsdo6xfP+C/eZvOEE6jdtIXxn7yOb2jQYccSN3U82RGx+M/56kifjku0NbeQsHguqSMnd8r1MTta7+FJlPqHYvnjD6NDOSBbm5V+GZspHdb51q4S4lAC+vQi7eJ/MXTzclI//d7ocPaRs2QNw1fMY9NpF3TKtu3O1P+EyQzduJT0r+dSEhnLuBcfpioqjjX3P4e1pfWg27U0NLLh5fdZP3YGlsgIRj9wM2H5u1hzzhVkLVhOXNEuxr/9HL2HJx9y/vDkBDZdcgPD1y9i8zufOfvpHbHUf91BSEM1lpdedPl71+jLzgcg991PXDpPe2W/8z8Ael98rsGRCCGEc8kahEIYTNYg7H5Kt+2k5MzzGLxtDRuHTiTq608OeHagK2l2O9V+QewYM4XRC7/r0LkPR3ZEHDXhvRm6aZnRoQjhEnWlFaQ99x9C//ch8QWOtQonzdLXKpw1pUcnhWryi/GKiWb9qRcw7pv3XTqXZreTGTcI74ZaIouyD6uKydU0u53MfoPwrq8hsiinU8TUmWx48V2G33IF6555g5G3X2NoLHWlFdhjYtmVPJLh6xYZGktHqcopZPsNdzLip8+wms1sPvtyBr302AETfZrdzua3PyPwwXuIKckhNWE47i++QMKsY494/hWX38r4916gePN2IgYPOJqn4jSb3/mMIVeex4aX3mP4jZcaHU6nsHbCicRsWU1odVmnez3bOe8P4k+YzNonXmHU3dcbHY4Q7dJcW09tdBxVwb0YsHNLp9m/1o0/gcT1f9CasYOgmN5Gh9NhNLudrf/9Gs8HHyAhN528sGhKb7+H4bdciclixtZmZdun39P44cckLpuHf0sDlT4BZB57IgGXX8zA044/ot9hS0MjpbEDQSnCszNcslZ3e+St2kT4MaPYOHk2YxbO6ZA5d/ZJoNXLm6QdmzpkvsOxNWkU/hWl9C3NNToU0QnIGoSiO+kc7zaEEKKbWPfcW3iMGEb8js2suutJhq5f3OHJQdBbh+UkDCE8dUOHz/13qnOLiC3JpnFM92nVJsT+/HqFMPaZe+mXm07GD7+zddIsBv3xMwNOnu60qkJrSyt1pRWUZ2RRsHYLu35fTvqceWz96Ft2/LLYSc/E+ba//iHudiuhV13i8rmUyUTd9TcSVVHAptf/6/L5Dkfq/75nQM42Cq64TpKDBzD0+kvIDYsm8KXnDW/Rm3rPEwQ01+P7aPesHjyQoJjejPv+I8pWrWfbyGMZ97/XaY2NY9Vtj9DW3LLncTvnL2ProHEMveafmGw2Nrz8Psnpa48qOQgQffUlAGS/4dqTB9qj9cOPqfXwIfnyfxgdSqdhmzSJsLpK8tdsMTqUvyib+zsAUScdb3AkQrSfp78v2TfczsDsNDa88oHR4QCQvXg1w1fOZ/PpF/ao5CDo7yMHX3I2/bNS2fDSe9jMZkbecS1ZMYmsmn0+lSHhDLr4TFKW/sL2sVPZ/M5n+FeUMvanT0k8Y+YRJ3g9fLypfOIZosvyWH/rg05+Vu1Xfu2NtFnc6Pf2ix02Z+lxsxi4cwvlO7I7bM5Dqc4tInH7BgqmzjQ6FCGEcDqpIBTCYFJB2D3UlpSz/ZxLGb3kRzJikvD67BOixw03NKYVV97G+Hf+j6rsgk71YW7DKx8w/MZL2fbFTySdLf37Rc+hVxW+QegnHxJfuINGN0+2Tp6FPTERraEBGhsxNTSgmpowNTdhbmrE0tyEW3MTbi3NuLc249HShGdbC56tzbjbD96C0Y4ib+kaYiaM7MBneHhSB47Ev6qMqOLsDjkz3trSSmnvWGoDQ0ncudnl8/2dzYPH0zt7O76FeXj6yUmnB7L6gf9jzKO36ZVbBiVl6sursPaNIWfAEIZuXGpIDJ1Bxo8LaPv37aRkrCcvNIqSm27HvnARoxbModbLl/SrbmHEE3c7tbohIyYZk91G/7ztThvzSDVV16H16sXWSScy5vdvjA6n08hZupaYSaNZfe/TjHnsDqPD2ce6ccfTJ30jEdWlRocixBGxtrRS0DcBgD65mVg83A2NZ/3YGQzcuIy2zJ0E9o00NBaj2dqsbHjmDSKff5KwmjJSh03Efu55pFx1Pp7+vk6fb8OIYxm4dTX1m7bSKyne6eMfji0ffMngS89hxVW3M/7NZzps3qyFK4ibdgyr7nycsU/d02HzHsyah19g9EO3kvH9bww4ebrR4YhOQCoIRXciCUIhDCYJwq4v7fOfCLrmcsJqyljzz38x6u3ncPP0MDosUj/9npTzT2Xj6/9l2LUXGh3OHivPuJTh338CNdV4+HgbHY4QHU6z28n8aSHVL77GoD9+xrtNr8ppNVlodvOg2d2TVndPWjy8aPPwpM3TC6unF3ZPL2xe3mheXth9fMDbG+Xjg/L2xuTni9nHB7OfD0opEq++kPWzz2Xc9x8Z/Gz3VbptJ6HJCay66HrGf9hx6wKuvOUhxr34MOlfzyXxDOPO/N3x6xL6zzxWP4HjrWcNi6Oza21spqp3X8p7RZGSsd6QGFZecyfj3nyG7XPmMfDUGYbE0FnsbiUa8NC9xBZn02q2sP6UC0h65WkC+vRy+nwrb7iPca8+Tt6K9YafbLXumTcYeee/2PrRtwy64DRDY+lMNLudKv9gdo6cxOjFPxgdzh6a3U5ZYC9yB49m1LK5RocjxBFb/9J7jLj5clbf8xRjHr/TsDiyFq0ibuo4Vpx7NeM//Y9hcXQ2tjYrbc0tLj/Rq2BdKqFjh7Nl7HRD/qdZW1rJjxmIxdpKWE5mh3521+x2Cnr1pTIymiFbVnTYvAezYeRU+uzYSmhFMSaL2ehwRCcgCULRnUiCUAgnU0rNBF4CzMA7mqY9dajHS4Kw62ptbGbdpTcy9ot3KAyJpP6d90k8rfO0M2qqrsMtOJA1/7iyU32gy4hNxurmTnLmRqNDEcJwzXUNtDY24RXg59QTC9ZOOJEB6/7AUlSAd1CA08Y9Wiuvv5dxrz3R4Qf+G6tqaOsdxa6UUQxfu7DD5t3fumNmMnDdH9iyswmIDDMsjq5g5Y33M+6Vx0j/+hcSzzihQ+durKqhOTqG/LikTnFQqrOwtVlJ/fArQkcOoffwZJfN8+eJBDcw/sOXXDbP4dg4fDK9d6YRUlEsLYH3s37MdCIzU4msKjY6lD0KN6TRe0QKq/79CGOfu9/ocIQ4YprdTkb/IQRVlOCfl+WS6rTDsX7scQzYuALbzp0EREUYEkNPt+LC6xn/8Wuk/u87Us47pUPnXnXrw4x94SE2vPw+w2+4pEPnBlhx9hWM+uYDGvOLDH3f3FRdB2FhbD7+dMb+9KlhcYjO5XAShO09PiyEUWQNQiGcSCllBl4DTgSSgfOUUq47giIMk7N0LbkDhzD+i7dZO/10grandqrkIIBXoB9ZUQn4r19jdCh7NFbVEJeXQc2ocUaHIkSn4Onng394qNOrjr1vvB7/lga2/t+bTh33aIV8/zWZfRM7vCrIOyiAtNMuYOi6xeStNGZt1oK1Wxi2cj5bTjlPkoOHYfCD/6bay4+mx57o8Lk33/8MwQ01uD9s/Lo/nYnZzcKQK851aXIQoFdSPNsShtH7lzmGrkNZnVtEyubl7Jp+kiQHD6D1mIlEVpdQtDnd6FD2KPhxPgC9Zh9ncCRCHB1lMmF74kl61Zaz8Y5HDYlh1+/LGbH6d7acdbEkBw007JUnKQoMx/vWm7G2tHbYvNW5RSS+8Rxbk0Yx7LqLOmzevQVf8A/c7DYy3zU2Kbftwy/xsrbg84+zDY1DdC1yfFh0JZIgFMK5xgA7NE3bpWlaK/AZcKrBMQkn0ux2Vt36MOFTJxBSWcqGl99nzG9f4xMSaHRoB1Q5ZCRxWam0NbcYHQoAu35cgJvdhve0Y40ORYhuLensE8mKiCP4w3cNPcC+t7yVG0jI207FyWcYMn/CY3djNZspfOhJQ+bPv/8xbCYTCY/fa8j8XY1PSCDbzr6E4RuWkL14dYfN21xbT/8P32BL0mhD29H2dPVnnEVMaS67fltmWAzb3/gQN7uN0KsvNSyGzixstn5iXP53vxocyZ/sfyyl1sOHmEljjA5FiKOWfO7JbBpyDMkfvk5NQcevqVl99/3UeXiT/PQDHT63+JNXoB9FDz1BXHEWa+98vMPm3X7tv/FtacTn9Vc7ZM3wA0mYPY1S/1DMc741ZP7dbF9/Q42nLwPPmW1oHKLLkePDosuQBKEQztUHyNvr+3zHbaIbKM/IYsvQiYx94SG2J4/CtmmjIa022sM8aSLebS1kL1hpdCgA1P22EDuKuFN69npOQriaMpkovfAy+udnkPmTcS0195b/xvvYUfS77jJD5g+N78vGKacw9PfvqMjK+/sNnKh8Zy7D5n/LxqmnEjogrkPn7sqSHr2bRjdPyu5/pMPm3Hj/M4TWV2F+UKoHjTTg2ktoM5kpfee/hsXg+/UX5IZFE3/8JMNi6Mxip4yl1tMXbfESo0PZI2LzGrIGDpX1oUS34ff8s/g2N5B28z0dOu/O+csYsWYBqWdeItWDncDwGy5hc8pYkv/zfx3yHjZ78WpGzv2ctSecTdyUsS6f72BMFjNZE48jcdMKvc2nAdqaWxi4ZjEZY6Y4veOL6Pbk+LDoMiRBKIRzqQPc9peFPpVSVyml1iql1lqt1g4ISxytDS++i3n4MBLS17Pq9kcZsuEPQvvHGh3W34qaNR2Ayt8WGRuIg9+aFWT16Y9/eKjRoQjR7aXccR0N7l7U/J+xa3iBXn0dNXcO2waOoFdSvGFxhD98D57WVjIeeKZD582893HcbFYiH5XqwfYI7BvJ5lnnMHzp3A5pY9hc10D8+6+ROmAEyf+Qs8SNFBTTm7SUMcTO/8GQKujirZmkZG6g4MTTDauc6OzMbhZ2JQ4ncnPnaGVflVNITEkOjWPGGx2KEE7Tb/oxrJs0i+FzPqIkLbPD5q29+z5qPXxIekZOlukMlMlEwJuv4dXWzM7Lb3TpXJrdTt2/bqDBw5uBb77g0rkOh8+55+BlbWHbh18aMn/6Zz/i31yP5UxjOqCITs2y+7iu43LVfvcf1vFhIToD+bQjhHPlA9F7fR8FFO7/IE3T3tI0bZSmaaMsFlnTpLPb/O7nDL/lCspDIilbspyxz9zXZQ4Whaf0p9QvBPMq4ysI25pb6LdjC+XDRhsdihA9gm9oEFunncyQpb9QnVtkaCw7fvmD6LI8Gs40du2OmAkj2ThsEonf/LfDzkSuK60gZc4nbBwzrcPXXuwO4p64D7sykXu366sINz38f4TVVaLdf7/L5xJ/r/XsfxBZXcL2OfM6fO7s194FIPq6yzt87q6kefwEosvyKM/IMjoUcn74DYCA46YaHIkQztXn5WcxaXZybrizQ+bbOe8Phq9bROrZlxLQp1eHzCn+XsyEkaw7/WLGLJzD9u/mu2yeTa9/zOC01Wy75t8E9o102TyHK/Gc2VR7+WH/6htD5m/8/EuaLB4kXnSWIfOLTs26+7iu4/LWfvcf1vFhITqDrnGEW4iuYw2QoJSKU0q5A+cC3xsckzhKTd/MocHdi77b1tN3/Aijw2kXZTKRnzSMPmkbjA6FrPlL8W5rwTJF1h8UoqP0uuNmPGxtpD/1sqFxVLzzAa0mCwP/ZfxaXu533UlQYy2bn+iYysrUB5/Fv6UBvwekevBIhCcnsHHybIbO+9qlbbVaGhqJfedVtsUPIeX8U1w2jzh8iVdfQLPFnZr3PurwucN++IaMmCSixgzp8Lm7kuATjwMgpxOsQ9i8aDGtZgv9Zk0xOhQhnKr3sCTWzz6PkQu/I2fpWpfPV3v3A9R6+JD8jKw92NkM+s9zlPkFY7rhBmxtzu9E1dLQSOhD95ATHsPIJ+92+vhHwuLhTsbYaQxYs4jWxuYOndtutRG37DfSh47HK9CvQ+cW3YIcHxZdhiQIhXAiTdOswPXAr8A24AtN01KNjUocrV7rV7FzwFA8fLyNDuWItI4ZR2R1CWXbdxkaR+UvvwMQI+sPCtFh4qaOZ1v8EKK++C92q82QGGxtVvr99gOpQ4/pFGeiJ519IhkxSfR57w2XHFzZW0tDI/GfvM3WpFEkzJaqliMV/tj9uFvbyLjvSZfNsfHRFwmvKcN6X9fpEtDd+fUKIW34ROIXz8Xa0tph8+YsWUN8QSaVp0q1wN/pd/wkGt08sS5YZHQoBK5fzc7YZDz9fIwORQinG/jyUzS5eVJ50+0unWfHr0sYvn4Rqf+4jIDIMJfOJdrPNzSInLsfJiFvO+se/D+nj7/htkeIqiig5vGnO9V6e+5nnYl/SwPpn37XofNm/rSAXnUVWE89rUPnFd2DHB8WXYl8+hXCyTRN+1nTtAGapsVrmva40fGIo1OVU0hccRYN4yYYHcoRC5w+GYC8nxYYGofHyuXkh/QmdECcoXEI0dM0XH4VURWFbP3wK0PmT//iJ3rVVWA/91xD5t+fMpmou/5moioK2fzahy6da9MTr+gtK++4w6XzdHcxE0aycdQUUr75iLrSCqeP39rYTMybL7E9LoVBF53p9PHFUTj3PELrq9j26Q8dNmXhG+9hUyb633BZh83ZVVk83NkxYCi9Nq42NI7m2nr65aRTPWKMoXEI4SpBMb3Zcv6VDF+/iPRvXFexW3f3/dR6+pIi1YOd1sg7/0Va/6EkvPQENfnFThu3fGcug957hY1DJzLk8n84bVxnSLzoDBrdPGn6vGM/y1R+8gVtJjMDLjuvQ+cV3YccHxZdhSQIhRDiELK/nQtAkKOFUlcUN2MiLWY3Wv9YalgMmt1ObPoGigaPMiwGIXqqwTdeSoVPILbXXjdk/voPP6bRzZPkqy4wZP4DGXbTZRQGR+L18osum8PWZiXy7dfYETVAkk5O4Pvgffg315P60HNOH3vj4y8RUV1Kyz1SPdjZJF9xLvXu3jR99EmHzKfZ7cTM+460pFGE9o/tkDm7usZxE4grzjJ0rdudPy3A3W7Fa+pkw2IQwtWGPPcQ5b5B2O+8E81ud/r4mT8vZviGJaSeezn+4aFOH184hzKZ8PzP6/g31ZN+5c1OG3fXVTfjbm0l5D/GLktwIJ5+PmwbMYn4Fb+7vPvHbprdTtSiX0kfOKJTdEARQghXkk/AQghxCC2/L6TJ4kG/mVOMDuWIefh4sys2iaBNrl+z4mByl68nqLEWJk4yLAYheioPH28yTjqHIRuXUrwlo0Pnbm1sJvGPX0gbM7VTrd1hdrOQd/HVJGZtJf2ruS6ZY9MrHxBdlkfNjbdI0skJBpw0ja1Jo4j/+C1aGhqdNm5bcwtR/3mJjL5JDL7sHKeNK5zD09+XbWOnkrh8vlN/7wezfc58elcW0XRW56qe6MwCTpgOQNacXwyLofa3RQDEnSxt7EX35RMSyM6rbyF5xyY2v/U/p4/fcK+jevCp+5w+tnCuftOPYe2scxn165fsnPfHUY+345fFjFowh/WnXUj0uOFOiNAFzjid0PoqMubM65DpcpeuI7osj8ZZJ3XIfEIIYSQ5WiGEEIcQtn4VOxMG4+7taXQoR6Vm2CjicrfTXNdgyPzFP/0GQMRJcuBGCCPE3nMLStPIeuKFDp037f0vCWiux+3CzlM9uNvg+2+m2suPpiefcfrYmt2O70vPkx/Sm2E3X+H08Xusu+4mrK6STY+/4rQhNzz5Gr0ri2i8625J5HZS7hf+E//mera97/rWYjXvfkiL2Y2k6y52+VzdRb9ZU2i2uNNi4DqEPqtXkh0RS0BUhGExCNERRjx2B/khvfF/5EGnVlJl/rSQYRv/IPXcK6R6sItIfPMFar38aP3X9UdVUarZ7bRdfyPVPv4kv+7898TOMvCyc2k1W6j55PMOma/ww08BiLv8nx0ynxBCGEk+BQshxEHUFJQSV7iTurFdd/3B3dwnTcDdZiV7/tGfYXgkzEuXUu4bRNTowYbML0RPFzkkkc3DJjLgh89obWzusHltn3xClbc/yRd3vhab3kEBbDv9AoauX0zeyg1OHTv1k+8YkLuNgitvwOxmcerYPVnKBaeR2TeR3m+94pQDo9aWVnq/9jw7ohIYerUcAOqski88gypvf6yffurSeawtrSQs/pnUkZPx6xXi0rm6Ew8fb3b2G0ToupWGzG9rsxKXuYmSoaMNmV+IjuTm6UHJHfcRV7SL9U+95rRxG++9n2ovPwY9LdWDXUVAn15k3nIPSTs3s+6JV494nPXPvUnSzs3suOnuTp0c9g0NYtugscQs/tUlLXb3F/rbXDJikuiVFO/yuYQQwmiSIBRCiIPI+vYXTGh7Wid1ZX1nTwOg+rfFhszfZ+tacpNHSHWGEAYyXfcvQhqq2fLq+x0yX0NFNUlrF5Ex6QTcPD06ZM72SnjkLtrMFgoffNKp49qffppy3yCG3nODU8ft6ZTJRP0ttxFVUcDGF9856vE2PvsfoioKqLtdqgc7MzdPDzImnkDymoU0VtW4bJ60j74huKEG9U9JFrdX7ZjxxBXsoLakvMPnzl60Cr+WRsyTpI296BmG33oVO6IGEPX8k05pvZzx4wKGblpG+vlXyskRXcyoh/5NRt8kYp9+mLrSinZv31RdR58nHmRnnwRGPvRvF0ToXK0nn0ZkdQk75y916TzFWzNJyE2n4vjZLp1HCCE6C/kkLIQQB9H8+wJazG70cyTXurLQ/rEUBEfisbrjz+4u3ppJZHUJreOO6fC5hRB/GnTJ2RQER+L1zlsdMt+2tz7Gu60Fv0s7b6u+0Pi+bJp6CsN+n0NFVp5Txtzxy2KGpK4i8/wr8PTzccqY4k9Dr7+E3LBoAl96/qjOILe1Wen18nPs6h3P0H9d5MQIhSv4XnIB3m0tpLlg3a3dWv77MbUePiRfLusPtpffjGmYNTtZ38/v8LnLf/0dgD6y/qDoIUwWM02PPkFkdQkb7j76E5yaHNWDKU/c44ToREcyWczw2qsE11eRes1t7d5+4033EVFTRstz/9clOl70v+J8bMpE2X8/c+k82e9+AkCfS89z6TxCCNFZSIJQCCEOImTdSnb0G9RtDvAWJY8gevvmDmnJsbf8H/SFxENP7PqVmEJ0ZSaLmbxzLiI5cyNZi1a5fD63zz+jOCCMxDNnunyuoxHxyD142NrIeOBpp4xX8/AT1Hl4k/LIHU4ZT+zLZDFTcvWNxBdksuX9L494nA3PvUnfsjyq/32XfoBNdGqJZ82i1C8E8xeuWXuoqbqO5FULSJ9wPB4+3i6ZozuLP2UGbSYzjfMXdPjclmXLKAkII2LQgA6fWwijDL7kTLYkjWbguy8fVeXu9u/mM3Tzcrb98yqpHuyiBpw0jbXTT2fkdx+Rs3TtYW9XvDWTYf97k3VjZ5B87skujNB5gmJ6kz5gOJG/z3XpPH5zfyQnPIa+40e4dB4hhOgsJEEohBAHUFtSTr/8TGrHdp+qN9vYsYTWV1K0Kb1j512yhHp3b+Kmd/21HIXo6gbedSMtZjdKn37BpfNU5xaRvGUlWTNO6fTJl77jR7Bh+GQSv/mIpuq6oxorf/Vmhq36ja2nnN+p13Hp6obefT2l/qGYnz6ypK7daiPspWfJjohl2I2XOjk64QpmNwu7ps0mZdMyaorKnD5+2lsf49PahPelUk16JLwC/dgZk0TQ2o7tVKHZ7USnriNf2tiLHsjzuWcJaqwl9dYHjniMlvsepMrbn8FP3uvEyERH6//2SzS5e1F71b8O+2Tg/GtuQqHR+62XXRydc9XPOpnYkmxylq1zyfjVuUUMzNhA4dTOfYKjEEI4k7yLFkKIA8iaMw+zZsfv+O5T9RZ6/BQACn/+vUPn7bVxDbsGDOkSbUuE6O6CYnqz+ZjjSVnwA/XlVS6bZ/vr7+Nmt9Hrys7bXnRvnnffSVBjLZsfe/Goxim4/zFsJhMJj0mbLldy9/Zk10VXk5K5gfRvfm339htefJeYkhwqbrmj0yewxZ+Cr7gYd5uVjNc/dPrYps8+o9QvhKR/nOT0sXuKqtHjic/Z5tJ1IvdXtHk7veoqsB7TfU7oE+JwJcw6lnXjjmfol+9RviO73dunz5nHkK0rSL/ganxDg5wfoOgwwXFRbLvudgZvW8OGF9/728enf/MLo5bNZeM5VxA5JLEDInSe2CvOB6Dwg09dMn7Gu59i0eyEXiDtxoUQPYckCIUQ4gAaf1tAq8lC/MnHGR2K08QcO5YGdy9sy5d32Jw1+cXEFWfRMGZ8h80phDg0v1tvxLe1kdRnXnfdHN98SU54DP2O6xqVw4lnzmR7bDJ9PvgPtjbrEY1RviObYb/PYcO00wgdEOfkCMX+Bj90G9VefjQ99kS7trNbbQQ//zQ5vfoy7JYrXRSdcIWEWVPID+mN5zdH3lr2QGryi0nZtJxd00+Sk5mOgvdxU3Gz29j1Q8ediFb4o77mYdiJ3ef9uhDtEf7Ss7jZrOy84a52b9t6/4NUeQcwRNYe7BZGPn4Xu3rH0/uRew7ZEcNutWG+5RZK/UMZ8mr73kN1BuHJCWTEJBEy72eXjO/2w3cUB4TR/8RjXTK+EEJ0RpIgFEKIAwheu4Kdccl4BfoZHYrTWDzcyYpPIWSza9pxHEjWd/qBm4AZUztsTiHEoQ085Th2RCXQ6+P3XLImafHWTJIzN1J44uldpuWbMplouOEWoioK2fTKB0c0Rua9T+Bms9L70fucG5w4IJ+QQLadeTHDNywhe/Hqw95u42sfEle0i7Ibb5NkUBejTCbyZpxC8ra1lO/Mddq46a99gLvdSujV0m72aPQ79XhsykRdB65DaPtjKbUePsRMHtNhcwrRmUSNGcL6489kxLyvyF+9+bC3S//mV4ZsXcn2C6/GJyTQdQGKDmPxcKf5+ZeIqClj43V3HvRxax99iYTcdHLveADvoIAOjNB5Kk44iQG52yjekuHUcRurakjaspKciTO6zGcYIYRwBvmPJ4QQ+2moqCY+J53q0d2v6q1u+GjiCnbSUFHdIfM1L1xEq8lCv1mSIBSis1AmE5UXXk5c0S62H0F7xr+T/bre2qjvdZc5fWxXGnrDJeSH9MbnlRfanTitLSln0HefsHHMdKLHDnVRhGJ/SY/fQ6ObB2UPPHpYj9fsdgKefYq80CiG3Xa1i6MTrhB5zaWYNTs7X3vfaWP6fv0FuWHRxB8/yWlj9kR+vULYFZWA/6qO61QRvnkNWdLGXvRw8a88TZvZQslNtx/2Nq0POKoHn5Tqwe4k+R+zWTvhREZ+/vYBE8b15VX0e/5x0uMGMfLu6wyI0DmiLj0PgOx3P3HquOnvf4mntRWfc8926rhCCNHZSYJQCCH2s+u7eVg0O74zphkditN5T5mEWbOT9fPCDpkvcP1qdsUm4env2yHzCSEOz6Dbr6HOw5v6F19x+tihP3xNRkwSfUYNdvrYrmR2s1BwyTUMzE4j/etf2rVt2oPP4tfSiN+DUj3YkQL7RrL5xHMYtnQuRZvT//bxm17/mPiCTIqv/zcWD/cOiFA4W+yxY8iKiMP/+2+cMl7x1kySMjdS0IUqnjuzihFj6b9rKy0NjS6fqzq3iNjibJqkjb3o4UL7x7Lp7MsYuXIemT8v/tvHp3/zC0NSV7H9omu6bAWZOLiYd1+lzexG+VV/TQBuue5OQusrMb38Upd+zYseN5zsiFj8fv7BqePav/mGKm9/Es+e5dRxhRCis+u6rwhCCOEi9fMXYFUm+p16vNGhOF3srOkA1C36w+VzNdfW0y8nnaqRY10+lxCifbyDAkidcTpDVsynIivPaePmLF1L//xMKk89y2ljdqQh991Mlbc/zU89c9jbNNc10P+Td9iSNJqEWbJeSUeLffx+NBS59xy6ilCz2/F55gkKgiMZfte/Oig64Qols08naedmp7QWy379PUxoRF93uRMiEx7Tp+Jha2Pnj64/ES37h98A8D9uisvnEqKzG/TCo/r7l9v+voqw7b4HqPAJZMgTd3dAZKKjhQ3sx5bLb2LYpqVs+s/He24vWLuFkV+/z5pjT2bASV3/ROjiabNI3LGJyqx8p4zX2tjMgLVL2DFmipxEJoTocSRBKIQQ+wlcs4KdMYndcj2GgD69yAmPwXvtKpfPtfOH33G3W/GeJgfMheiMIu+6BXe7lYwnX3bamEVvvI9Nmeh/Xddcy8sr0I/tp13A0PVLyF2x/rC22fzkq4TWV8IdB1/vRbhOxKAENk6ezZBfvz7kQaLNb39GQt52Cv91qxz46eKir7kEgOzX3j3qscK+1yueo8YMOeqxBMSdegIANfN+d/lcTYsWSxt7IRz8eoWw/fIbGbxtDVs++PKgj9v25c8M3raGzIuvlerBbmzEcw+S06svIffdSXNdAwCl19xIm9mNuLdeMjg65wi7+FzMmp0d7/7PKeNt/+x7/FsacDvzTKeMJ4QQXYnSNM3oGITo0Xx8fLSGhgajwxAOTdV1mEOCWXfGxYz/8h2jw3GJ1dPPYODy+fjVVWOymF02z4pLb2bsBy9Tl19MQJ9eLptHCHHkUhNHEVyST6/S/KNew0mz2yno1ZeqXr0ZnLbaSRF2vIqsPHwT4tk47VTGzjv4QTYAW5uVwt5xNHv50D87rUu3a+rKcpatI3riaFadfw3jP3n9L/drdjsZ8YPxr64gtCgHN08PA6IUzpQRm4zJZqV/3pFXEeYsWUPMsWNYeeP9jHvpESdG17NlRfajLqQXQ7audOk86fF6Ujdx51/X2RKiJ2ppaKQyqh8NvgH0y0o74Oe8rcmjiczdgXd+Ll6BfgZEKTrKlg++ZPCl57Dy8lvxnTyBQRefyYor/s34t58zOjSn0Ox2isKiKIuKY+imZUc93qqZ5zB4wfeYystleRRxWJRSjZqm+RgdhxDOIEcxhBBiLzt/+E2vejuu67fdOBh1zDEENNeTt3KDS+fxXb2CnMg4SQ4K0Ym1XHk1kdUlbHnn06MeK/PnRURVFNB85jlOiMw4IXHRbJx2KsMWfEf5ztxDPnbTy+8TXZ5P7Y23SnLQQDETRrJx1BRSvvmIutKKv9y/9YOvGJidRu41N0tysJuoPOVM+udnHnal74EUvumoeL7hMidGJkpHjKV/xibamltcNkdzbT39srdRNWKMy+YQoqvx8PEm/+a76J+fwYbn3/rL/Wmf/8SgbWvJvORfkhzsAQZfcjbrR01l6IevEXDbzRQGRzL8+YeNDstplMlE7rEnkJS6mtqS8qMay2610W/576QPPUaSg0KIHkmOZAghxF7q5v2OTZnod/oJRofiMr1m6q2YSn9d5LI5rC2t9NuxhdKho102hxDi6A2+7iLK/IJRb7xx1GNVvv0BrWYLA/51sRMiM1bkw3fjZrOy44GnD/oYzW7H7+XnyQ/pw7CbJMFgNN8H78O/uZ7Uh/Y9M16z23F7/DGKA8IYfv/NxgQnnC7+ukuxoyh4/f0j2l6z2+n7yxzSkkYR2j/WucH1cJYpx+Ld1kzW/KUum2PX3MW42614TZE29kLsbcQ915MV2Y/wZx77S5Jee/BByn2DGPrYXQZFJzpa5Luvo9CILsuj5P5H8fTrXsVOgeefg7vNSsZ7nx/VOBk//k5YXSX2U09zTmBCCNHFSIJQCCH24r9qObuiEvDrFWJ0KC4TPXYYNZ6+aMuXu2yOrN9X4NPahFkO3AjRqbl5erDj1PMYvGUlBetSj3gcW5uV+IU/kTpsIgGRYU6M0Bh9x49g04jJJH77EY1VNQd8TOrHc0jITafgyuuPuj2rOHoDTprG1qRRxH/8Fi0NjXtuT/14Dom7tpBz1Y24e3saGKFwprCB/dg2YDh9fv0OzW5v9/bbv/+NPpVFNJ31DxdE17PFnD4TgMqf57tsjprfFgIQe/JxLptDiK7I7Gah9oGHiaooZP19z+y5PfXT70nZvo4dl14n1YM9SOSQRLbc9zQrT/onw27smuuDH8qA046n3DcY05w5RzVO1Sdf0GYyk3Dpuc4JTAghuhhJEAohhENzXQP9d22lYuQ4o0NxKZPFTPaAIYSnHnlbrr9T8ctvAPQ95XiXzSGEcI5+d9+EXSlyn3rhiMfY9tkPhNVVop13vhMjM5bX3XcS2FTHlsdfOuD92tNPU+4bzNB7bujgyMTBaHfcRVhdJZsef2XPbabHHqXUP5RhD95qYGTCFerPOJu+ZXnsPIJKtZp3PqTF7EbSdV2/4rmzCe0fS15YNJ4rjn5NqIPxXrWc7PBYAvtGumwOIbqqIVedT1r/ocS/+QINFdX6jQ89TLlvMMMeu9PQ2ETHG/3gzYz74eNu2QrfZDGzc8J0Ejcspbm2/ojG0Ox2ohb9SnriSFkaRQjRY3W/VwghhDhCu35agIetDc/p3Xf9wd0aR40lpiSHmoJSl4zvvmIZhcGR9EqKd8n4QgjnCU9OYPPIY0n8+Uua6xqOaIzGDz6i3t2b5CvPc3J0xhl4xglsj0sh6v3/YGuz7nNf5s+LGZy2mszzL+927Zq6skEXnU5m30R6v/UKtjYrqZ9+T3LmRnZddh0ePt5GhyecbOC1F9NmMlP+9oft2s7a0kr/RT+TOnJyt+4YYaSiIaPpl77hL/87ncHWZiUuYzMlQ0Y5fWwhugNlMmF6+mlC66vYfNvDpH7yHSkZ69lx2XWyvprodrzPPRvvtmbS//vNEW2f88daosvzaZx9spMjE0KIrkMShEII4VDz6+/YUcQ5WiN1Z35TJwOQ/fPvTh9bs9uJ3baegkEjnT62EMI13K67jqDGWra+9G67t21paCRx2Ty2jZvWrQ48KZOJxhtvoU9lEZte3neds7qHH6POw5uUR+4wKDpxIMpkou6mfxNVUcDGF99Be+QRvWLi4duMDk24QGDfSNIGjSX29x+xW22HvV3ax98S0lAN53efiufOxjTlWPxbGsheuMLpY+csWY1/SwPmyZOcPrYQ3UXiGSewYcQUBv/vLdzuuZsyv2CGPSrvWUT3k3juKdR6+tL21VdHtH3RB58C0O9yeU8ghOi5JEEohBAOfquWkdU7vke0log7cQo2ZaJx0R9OHztv1UaCG2rQJkx0+thCCNdIueA08sKi8X3v7XZvm/beF/i3NOBx0QUuiMxYQ667mPyQPvi+8sKedc7yVm1i6JoFbD31n/iHhxocodjf0BsuIS8smr6P3sug9HXsuOTabpW4FvtqO/sfRFSXkjFn3mFv0/Lfj6n18CHlCllryFWiTtVPtiv7+Tenj102Vz+5rfdJM5w+thDdSfBLz+LV1syA3G3svPwGeS0U3ZKbpwfbRx/LgFULaWtuaff2Yb/9zPa4FMIG9nNBdEII0TVIglD0CEqph5RSBUqpjY7LrL3uu1sptUMptV0pdcJet49USm1x3PeyUko5bvdQSn3uuH2VUip2r20uVkplOi6yqEkX0trYTHzmFspGjDU6lA7hExJIVp94/NavdvrYxT/OByB89nFOH1sI4Romi5mCcy8mMWsrO+e178QB+yf/o9IngOQLTndRdMYxu1kouOwaBuRsY9uXcwEovP8xrCYzCY/fY3B04kDMbhaKr76RsLpKKnwCGfqorLfUnSVecyHNFndq3v/osB7fVF1H8srfSZ9wvLSddaGIwQMoDIrAfZnzT0Qzr1hOqX8okUMGOn1sIbqTmImjWHvC2RQGRzLskduNDkcIl7GcdSYBzfVs/+Kndm1XtDmd/vkZVB0/20WRCXFgcoxadDaSIBQ9yQuapg1zXH4GUEolA+cCKcBM4HWllNnx+DeAq4AEx2V338nLgSpN0/oDLwBPO8YKBh4ExgJjgAeVUkEd8szEUdv1yyK8rC14TJ9qdCgdpmLISOJ2pWFtaXXquGrZMip9Aug7frhTxxVCuFbSnTfQZPGg/LmXD3ub+vIqUtYtJvPYE7F4uLswOuMMuecmqrwDaHn6Gcozshi+YA4bp59GaP9Yo0MTBzH07utJ7zeYnTfdhVegn9HhCBfyDQ0ibcQk+i+ee1jvZ9Le/gSf1ia8L7mwA6Lr2QoGjyIubf2e6mtniUpdR17yCJRJDmUI8XdG//AJoXm7ZL1k0a0lXnQWjW4eNHz6Rbu2y3n3fwD0uVTaiwpDyDFq0WnIu2rR050KfKZpWoumaVnADmCMUioS8Nc0bYWmaRrwX+C0vbb50PH1V8B0x5kbJwDzNU2r1DStCpjPn/+wRSdXNVdvgRR7+okGR9JxzBMn4NPaRM7iVU4dt/eWteQkDpcDN0J0MQF9erFl0okMXvQDtSXlh7XNtjf+i6e1lYDLu+8JiV6BfqSfcSHDNywh95+XY7bb6fPofUaHJQ7B3duTxJ2bGfO4VA/2BOq88whpqGbbp9/97WNNn31GqV8ISeee3AGR9WzapEkENdaQu3y908Ys2pxORE0Z1vHHOG1MIbozk8WMu7en0WEI4VJegX6kD5tAv2Xz27Umsd/cH8mOiCV67FAXRidEu8gxamEIOXorepLrlVKblVLv7XXWRB8gb6/H5Dtu6+P4ev/b99lG0zQrUAOEHGIs0QX4rFxGdkQsQTG9jQ6lw/SeOQ2A8nmLnDZm2fZd9KksomX8BKeNKYToOEG33Yx3WwtpT716WI93//JzCoMiGHja8S6OzFgDHrmDZos7I9YuZOOY6USNGWJ0SEIIh6TL/kG9uzdN//3fIR9Xk19MysZl7Jp+EmY3SwdF13P1PkU/BlX8/a9OG7PgB72NfajjPawQQggBYD/1NMLqKsn4/vDWvq3MyidxxyaKpvWcE8RFpyPHqEWnIQlC0W0opX5TSm09wOVU9FLseGAYUAT83+7NDjCUdojbj3Sb/WO9Sim1Vim11mq1HvxJiQ5hbWklPmMTJcN6xvqDu0UOTaTcNxjzKudVEOZ+px8ECpk53WljCiE6TsKsY8mISSLy0w/+ti1cZVY+KVtXkTPj5G5fMRwSF82maacC4P/AvQZHI4TYm6e/L9vGTSNx+XxaGhoP+rjtr3+Iu91KyJWXdmB0PVefUYMo8wvGvGyp08a0/fEHdR7exE4d77QxhRBCdH0Jl51Lq8lC9SefH9bjd773GWbNTtiF/3BxZKIbs+w+ruu4XLX3nV3pGLUQ3ftojuhRNE07TtO0QQe4fKdpWommaTZN0+zA2+j9l0E/gyJ6r2GigELH7VEHuH2fbZRSFiAAqDzEWAeK9S1N00ZpmjbKYpEzmI22a94f+LQ2YZk2xehQOpQymchNHEpkmvNaP1kXL6HRzZO446SCUIiuqvriK4gpySHt0x8O+bjM197HotmJuKZnHGxP/ugNtv73GxJmHWt0KEKI/Xhc+E/8WxpIe+/g6w/5fP0FuWHR9J85qQMj67mUyUTuoNH03bLGaesQ9tq0lqyEoVIBKoQQYh8BkWGkJ48mavGvh/Wa4/bDdxQFhhN/vLwnEEfMuvu4ruPy1t53dqVj1EJIglD0CI5+zbudDmx1fP09cK5SykMpFYe+Xh9uQAAAhUpJREFU0OtqTdOKgDql1DhH7+aLgO/22mb3YktnAQscPaB/BY5XSgU5ysOPd9wmOrnKn/V2RTGn97x23K1jxtGnsojyHdlOGS9s4xp29h+MxcPdKeMJITrekH9fRY2nLy0vH7rNqP+cr8iKiCOuh1Ry+PUKYdCFpxsdhhDiAJIuOJ0qb3/sn352wPtL0jJJythAwYmnd/uK587EOnEiveoqKFyfetRj1RSUElecRcOYcU6ITAghRHfTfPKpRFUUkrVw5SEf11BRTdLWVeRMmiHvCYQh5Bi16GzkP6HoKZ5RSm1RSm0GpgK3AGialgp8AaQBvwDXaZq2e1Xja4F30BeF3QnMddz+LhCilNoB3Arc5RirEngUWOO4POK4TXRyXiuWkhsWTWj/WKND6XCB0ycDkPvTgqMeq6aojNjCXdSP7hnJAiG6K09/X7adeBaD1y6kPCPrgI8p2pxO0s7NFJ90RgdHJ4QQf+Xm6UHGpBNIWruIxqqav9yf9ep7mNCIvu5yA6LrucJn6+vTFn539Mejsn+YB4D/cVOOeiwhhBDdT/yV52NHUfLBp4d83PYPvsTD1obfeWd3UGRC/IUcoxaditKTykIIo/j4+GgNDQ1Gh9Fj2dqsNPoFkDZ5FmPnfWl0OB2uua4BU1Ag60+7iHFfvXtUY216838MveafbP3oWwZdcJpzAhRCGCJv1Saixw1jxSU3Mf79F/9y/4qr72D8W89SuD6V3sOTOz5AIYTYT9pnP5B83imsfeJVRt193T737YgegN1sYUB2mkHR9Uya3U61XzA7Rk9m9KLvj2qsFedcycivP8BWUYlXoJ+TIhRCCNGdpCUMw6uhjrjCnQd9zNqJs4jfsAy/yjLpfCSOmFKqUdM0H6PjEMIZpIJQCNGjZf2+DL+WRsxTe+aaUp5+Puzqm0jAxrVHPVbjgkW0mizEnzTdCZEJIYwUPXYom1PGEv/NJ1hbWv9yf/iP35AeN0iSg0KITiPxrFmU+odi/nLfdQhzlq6lf34mlaeeZVBkPZcymchOGk6fLUf/PjNw/WqyYgZKclAIIcRB1Z54MnFFu8hfvfmA97c2NjNg7RIyx0yV5KAQQjhIglAI0aOV/6SvP9j3tBMNjsQ41UNH0i97Gy0NjUc1TuC6VXLgRohuxHb1NfSqLWfzfz7e5/bsxavpV7iTmtPkYLsQovMwWczsmjablE3LqSko3XN74RvvYVMm+t9wmYHR9VwtEybRu7KI4q2ZRzxGc10D8dlpVI0Y68TIhBBCdDcxV/wTgPz3/3fA+9M//Q7/lgbczzqzI8MSQohOTRKEQogezWP5UvJDetMrKd7oUAzjPmkiHrY2suYvPeIx9hy4GT7GiZEJIYw0+KrzKQkIw+2tN/e5veg/72NVJuKvu9SgyIQQ4sBCrrj4/9m77/ioqvz/4++TCgkBQhIIJBAIvbeAgIBgA3tbXXtdsW9zf7u6+93Vrd/d/a6r666rYu9tRUVXQbFQpIceOiEJ6Z30MjPn90dGDEhJIJmbZF7Px2MeM7fOJ5B7M3Pf95yjEI9Lu558UVJDF5f9Fr2v7cOT/HKs6bYg+vyzJUmZHyw66X3sX7RUIW6XOs3yzx4/AABN03vMMO3pO1SRiz866vLqt99VVXAnDbuRcdQB4BsEhAD8lsfl1oAdG5Q9xr9Drb4XnClJKv182UnvI/W/XzRcuJk9q2WKAuC4oNAQpV52nUZvX6sDqzdK8l5s//QDbR8xSdED+zlcIQAcbtB5Zygzqo86L/iPJGn3h58rrjhH1d/7vsOV+a8BZ05TWWi4PF8tPel9lH72pSQp4aKzW6osAEAHVXTuBRq6P0X5Ow4fh9Djcmvgys+1c/zp6hTB0HEA8A0CQgB+K23pGnWvLpc5w7/vRo4ZmqjsyFiFrF190vsoW/KVJKn/xee0UFUA2oLBv/qR6gMClfXnxyR9e7G99koutgNoe0xAgA7MuUQjdiarcG+aSp95UbWBwRp2941Ol+a3AoODtH/YOMVuXnvS+whbu0rpvRIUmdCnBSsDAHREvW++RpK0/7nXDpu/e+Fniq4olufSSx2oCgDaLgJCAH4r/6NPJUnxl811uBLnZQ8fp/gdm2Q9npPaPmzNKqXF9lf3fr1buDIATooe1F9bJp+lEYsXqLq0XCXPvtRwsf0uLrYDaJv63HGLAq1He//xrAZ99bFSJs5U117RTpfl16qnnK5+BQdUuC+j2dt6XG4N2LVZeaOTWqEyAEBHkzA9Sek9+yn84w8Pm1/62tuqCwjSkFuvdqgyAGibCAgB+K2Qr5crp3sv9R4zzOlSHOc+bYp6lhcpL2Vv87etd2nAns3KGzupFSoD4LROP7xHXWsqtPWv/z50sT2iZ5TTZQHAUSXMnKT9vRM14tl/KKqyVLr2WqdL8nuR5zV0DZr+XvPHIUxfvlZdaypkpk9v6bIAAB1U9pnnadjODSrNyJHUMExC/NLF2jkiiZuGAOAIBIQA/JL1eNR/e7IyxxBqSVLUubMkSZkff97sbfd/sVIRtVUKnDGjhasC0BaM+P6FSuvVX8Me/YOiK0pkrrnG6ZIA4LhyL7hMXWsqVBYarhG30iWy0xLnzFRVcKhcX37V7G3zP2n4bBp38bktXBUAoKOKvuEaBVmPdj/3hqSG4WXii7JVfcFFDlcGAG0PASEAv5SxIlk9Kg9KM/17/MFv9J89VVXBoXIvX9HsbQu9F27iL57T0mUBaANMQIDyrr9FXWsqVB4apuG30S0PgLYt4e5bJUk7p52jThHhDleD4E6h2jdojHpuXNPsbQNXrlR+RJR6j6XHDwBA0wyaO0O53XsqZOH7kqTcF9+UR0YDf0CvAgBwJAJCAH4p96PFkqQ+lxBqSVJQaIj2DxipHluSm71tyKqvldu9p2JHD2mFygC0BSN+fo/KQ8O0Y/pcLrYDaPP6jB+hTf9+WYnP/MPpUuBVcdrpGpCTqoOZuc3aLj4lWQdGTJAJ4NIFAKBpTECA0macq+FbV6uisEQxX3yi3YmjFD2ov9OlAUCbw6dsAH4paPly5XeNVp8JI50upc0omzBJAzL3qKrkYJO3sR6P+m3fqMyRE1uxMgBOi+gZpfJV6zTizWedLgUAmmTcXTcoemA/p8uAV7c5Z0qS9r//aZO3yd26W7Gl+aqfOq21ygIAdFBdr71Koe56bfv1XzQoc49K51zgdEkA0CYREALwO9bjUcK29coYPYm7kRsJO2O6gqxH+xctbfI2Weu3KbqiWO7p01uxMgBtQZ/xI9QlOtLpMgAA7VDiBWeqNjBYNZ9/0eRtMj9qCBOj557ZWmUBADqooVecp+Lwbhr77GOSpL630r0oABwNV8YBOMZ6PI68b+bazQ2h1oyZjrx/W5Vw/lmSpLIvljV5m+yPPpMkxZ5/TqvUBAAAgPavU0S49iWOVNSGpo9D6F62XBUhYRpwJi0IAQDNExgcpL1Tz1ZnV632905UXNJop0sCgDaJgBCAI7a++I7S4gY1exySlpCzsOFu5N4Xn+vz927LuvfrrYyYvuq8fm3TN1q+XKWdI9R32oTWKwwAAADt3sFJU5V4YLfK84uatH7PzeuVOmSMAoODWrkyAEBHFHrVFZKk3LPOc7gSAGi7CAgBOKJLQrz65aVrx93/z+fvHbBsqQq7RKrvaeN8/t5tXe7ICUrYvbnJrTv7bF2v/cPGKyAosJUrAwAAQHvW5ZyzFGg92r/wsxOuezArXwk5+1WZNMUHlQEAOqKRN16hVfP+n4b+7gGnSwGANouAEIAjBsyeqvVnX66J/31DGas2+Ox9rcejvtvWK31kEuMPHs20qYqsKlPm2s0nXLVwb5rii7JUO4VunwAAAHB8Ay85W/UBgapc8uUJ1037aIkCZNX17FmtXxgAoEMKCg3R1Kf/qh4D4p0uBQDaLK6OA3DMwCcfUW1QqIrv/rHP3jN74w71Olgg1/QZPnvP9qTXnDMlSTmfnPjCTcYHDXd/R3q3AQAAAI4lLLKbUvsNVeS6VSdct+rLpaoPCFTiBXzOBAAAAFoLASEAx0QP7KetN9+jcZuWa9vLC3zynlkfLJIk9bpojk/er73pO22Cyjp1kV114gs3dV8tVVVwqBLnzPRBZQAAAGjvipOmKjFth6pLy4+7Xvf1q5Xab6g6d4/wUWUAAACA/yEgBOCo8X97SNmRsQp78Ody17ta/f3MsqUqCeumhBlJrf5e7VFAUKDSBo1Wz63JJ1w3etNapQ4cpeBOoT6oDAAAAO1d2FmzFOJxad+HS465Tk15pQambVfJ+Mk+rAwAAADwPwSEABzVKSJcOQ8+pMTsfUp++JFWf7+4Leu0f8QExh88jsqkyUrITdPBnIJjrlOeX6QBWftUnjTFh5UBAACgPRtw2Vx5ZFT+6RfHXCdt8TKFuF0KnX2GDysDAAAA/A9XyAE4bsL987QzcbQSH/+LKgpLWu19crfuVp+SXNWdzviDx9PljBkKkFX6x8e+cLN/4WcKtB51OWu2DysDAABAe9a1V7RS4wep63HGISz5rGEs7IQLz/ZVWQAAAIBfIiAE4DgTEKCAxx5VdEWJtt73YKu9z4H3P5EkxVxwbqu9R0fQ//xZcpsAVX614pjrVH7+lVwmQIkXneXDygAAANDeFY4/TQP3bVVtZdVRl3des0rpPfupx4B4H1cGAAAA+BcCQnQYxpgrjTEpxhiPMSbpiGUPGmP2GmN2GWPmNJo/0Riz1bvscWOM8c4PNca85Z2/xhjTv9E2Nxlj9ngfNzWaP8C77h7vtiE++LE7jCEXnaX1p5+nCe88r5wtO1vlPexXS3WwUxcNmE23mMcT0TNKab0T1WXD2mOu0y15jVL7DlV4VHffFQYAAIB2L+Ss2erkqtP+T5Z+Z5nH5daAXZuUN5rxwgEAQMfD9Wu0NQSE6Ei2Sbpc0rLGM40xIyRdLWmkpLmS/m2MCfQuflLSPEmDvY+53vm3SSqx1g6S9Kikv3j31UPSQ5JOkzRZ0kPGmEjvNn+R9Ki1drCkEu8+0AzxT/1D1hhl3/njVtl/n81rlTp8ggKCAk+8sp8rHDNBA/Zuk7ve9Z1ltZVVGpiaouIJkx2oDAAAAO3ZgEsbvnKVLlrynWXpK9arW02FzIzpvi4LAADAF7h+jTaFgBAdhrV2h7V211EWXSLpTWttrbV2v6S9kiYbY3pL6mqtXWWttZJelnRpo21e8r7+j6SzvHdnzJH0mbW22FpbIukzSXO9y870rivvtt/sC00UO2qwNl51myauWqydCxa36L4LdqUqvihLtdO42NAUAdOmqUtdlTJWrPvOsv2fLFWou16hs89woDIAAAC0Z5EJfZQW219ha1Z+Z1n+xw2hYZ8LGH8QAAB0PFy/RltDQAh/ECfpQKPpTO+8OO/rI+cfto211iXpoKSo4+wrSlKpd90j94VmGPP4n1QQ0UPmZ/fLejwttt/0BQ3jD0adx8WGpuhz3pmSpPzFX35nWcmnX0iSEi46x6c1AQAAoGPIGzdZA3dvlqu27rD5gSu/VkFED/WZMNKhygAAABzB9Ws4goAQ7YoxZokxZttRHpccb7OjzLPHmX8y2xxvX98tyJh5xpj1xpj1Ltd3u3D0Z+FR3bX/x7/U0P0pSv7Lky22X/dXS1UREqYBZ5/eYvvsyPpMGKmi8O4KWLXqO8vC1qxUes9+6jEg3oHKAAAA0N4Fzpql8LpqpX624rD5cSkbdGD4BJkALlUAAIA2K+ib67rex7zGCzvK9Wv4Bz51o12x1p5trR11lMcHx9ksU1LfRtPxkrK98+OPMv+wbYwxQZK6SSo+zr4KJXX3rnvkvo72c8y31iZZa5OCgoKOtZrfSvrNj7U3frDi//o71ZRVtMg+Yzet0b6hYxUUyti7TWECApQxdJx6p2w8bL673qUBuzYrb8wkhyoDAABAe5dw6RxJUvEn345DmLttj3qX5qlu6jSnygIAAGgK1zfXdb2P+Y0XdpTr1/APBITwBwslXW2MCTXGDFDDYK5rrbU5ksqNMVO8fTDfKOmDRtvc5H39PUlfePt5XizpXGNMpHdw13MlLfYu+9K7rrzbHu+kj+MICApU7Z//T7Gl+dr004dOeX+F+zKUkJ+hqqmMP9gctZMmK74oS8X7v+3JIH3ZWnWtrZSZOcPBygAAANCexQxNVGZUnDqt/LYFYeZ/G8LC6LlnOlUWAACAU7h+DUcQEKLDMMZcZozJlDRV0n+NMYslyVqbIultSdslLZJ0j7XW7d3sLknPqmHg132SPvHOf05SlDFmr6SfSnrAu69iSb+XtM77+J13niT9QtJPvdtEefeBkzTyuku0ccIsjX7lSRXuTTulfaW/t0iSFMn4g83S7awzJEkZ//3i0LwC713ecRed60hNAAAA6Biyx05W4s6N8rgavpq5ly1TZUhn9Z891eHKAAAAWgfXr9HWmIbgGIBTwsPDbWVlpdNltEkH1mxWr2lJ2jTrIk3+fMFJ72fNBddo9GfvK7isVMGdQluwwo6tpqxCAZGRSr7iZk19+xlJUvLUOeq7fYNiSvIYGwYAAAAnbd1vH9Okh3+i1CVfK/GsaUqNG6TK7lEanbLG6dIAAACOyRhTZa0Nd7oOoCVwdRdAm9X3tLHacNG1Svrife377OuT3k/PDWu0b/AYwsFm6tS1i/b3G6JuG9dJkqzHo74pyTowciLhIAAAAE5J3CUN4xDmf/SpDuYUqH92qiom03oQAAAA8BWu8AJo04Y/8X86GBah6h/+SNbjafb2JenZGpC7XxVTTm+F6jq+kjETlZi2Q/U1tcreuEM9y4vkmsa/JQAAAE5Nn3HDldu9p0JWrlDaR0sUIKuIs2Y5XRYAAADgNwgIAbRp3eJ6avcdP9Woncna/O9Xm739/vcbxh/sPpfxB09G8Izp6uSq0/4lXyv7o08lST0v4N8SAAAApy5z1CQlpCSr6vOvVB8QqMQLZjtdEgAAAOA3CAgBtHkT/vSA0nv2U9Rvf6W6qppmbVv3+ZeqCQpR4vmzWqe4Di7+/IaLNMWffSW7fIUOduqihBmTHa4KAAAAHYFn5gxFVZZq4CcLtL/vEIVFdnO6JAAAAMBvEBACaPOCO4Wq9Ld/VN/CTG148E/N2jZmwxrtGzhaoeFhrVRdx9ZrxGDldotR8No1it2yTvuHjlNAUKDTZQEAAKAD6H3RuZKknmWFKh7PTWgAAACALxEQAmgXxsy7VltHTNbw+Y/qYGZuk7Y5mJWvAVl7VXbatFaurmPLGj5eAzevUr+CA6o+barT5QAAAKCDiJ88VoVdIiVJIbNmOlwNAAAA4F8ICAG0CyYgQF3+9Zi61FZpx10/a9I2+99frABZdT33rFaurmOrP22KuleXS5Ii55zpcDUAAADoKExAgNJHJkmS+l98rsPVAAAAAP6FgBBAuzFg9lStP/tyTfz4TWWs2nDC9Ws+/0J1gUEaeBEB4amIOmeWJDWM5Th3lqO1AAAAoGPp+usHtfruB9VjQLzTpQAAAAB+xVhrna4B8Gvh4eG2srLS6TLajcJ9Geo0Yrj2jpiocRuXHXfdPQnDVR8SqhF7NvmmuA6qvqZWrohuSh0wQiN3nziYBQAAAAAAADoiY0yVtTbc6TqAlkALQgDtSvTAftp6090at2m5tr284JjrlecXKfHAbh2czPiDpyq4U6i2/vy3sr/5jdOlAAAAAAAAAABaAC0IAYfRgrD5asorVZwwSDWdw5WQtlOBwUHfWWfzM29o7LxrtfWF/2j0zVc4UCUAAAAAAACAjoQWhOhIaEEIoN3pFBGunAcfUmL2PiU//MhR16n67AvVBwRq4CVn+7g6AAAAAAAAAADaNloQAg6jBeHJsR6Pdg0ep+j8THXav09doiMPW74rcZSsjIalbnWoQgAAAAAAAAAdCS0I0ZHQghBAu2QCAhTw2KOKrijR1nsfOGxZVclBJabvVMmkqQ5VBwAAAAAAAABA20VACKDdGnLRWVp/+nma8J8XlLNl56H5+z74VMEet8LPOdPB6gAAAAAAAAAAaJsICAG0a/FP/UPWGGXf+eND8yoWfyGXCVDipXOcKwwAAAAAAAAAgDaKgBBAuxY7arA2XnWbJq5arJ0LFkuSuq9bqdS+Q78zLiEAAAAAAAAAACAgBNABjHn8TyqI6CFz/09VXVqugWnbVZw0xemyAAAAAAAAAABokwgIAbR74VHdtf/Hv9TQtO3actWtCnG71Ons2U6XBQAAAAAAAABAm2SstU7XAPi18PBwW1lZ6XQZ7Z673qW0ASM0MGuPPDIqz85Tt94xTpcFAAAAAAAAoIMwxlRZa8OdrgNoCbQgBNAhBAYHqeYv/ydJSo0fRDgIAAAAAAAAAMAxBDldAAC0lJHXXaJVi+5R8IjhTpcCAAAAAAAAAECbRRejgMPoYhQAAAAAAAAA2j66GEVHQhejAAAAAAAAAAAAgB8hIAQAAAAAAAAAAAD8CAEhAAAAAAAAAAAA4EcYgxBwmDHGI6na6TocEiTJ5XQRQAfB8QS0DI4loOVwPAEtg2MJaBkcS0DL8efjqbO1loZX6BAICAE4xhiz3lqb5HQdQEfA8QS0DI4loOVwPAEtg2MJaBkcS0DL4XgCOgaSbgAAAAAAAAAAAMCPEBACAAAAAAAAAAAAfoSAEICT5jtdANCBcDwBLYNjCWg5HE9Ay+BYAloGxxLQcjiegA6AMQgBAAAAAAAAAAAAP0ILQgAAAAAAAAAAAMCPEBACcIQxZq4xZpcxZq8x5gGn6wHaE2PM88aYfGPMtkbzehhjPjPG7PE+RzpZI9AeGGP6GmO+NMbsMMakGGN+5J3P8QQ0gzGmkzFmrTFms/dY+q13PscScBKMMYHGmI3GmI+80xxLwEkwxqQZY7YaYzYZY9Z753E8Ac1kjOlujPmPMWan97vTVI4loGMgIATgc8aYQElPSDpP0ghJ1xhjRjhbFdCuvChp7hHzHpD0ubV2sKTPvdMAjs8l6X5r7XBJUyTd4/17xPEENE+tpDOttWMljZM01xgzRRxLwMn6kaQdjaY5loCTN9taO85am+Sd5ngCmu8fkhZZa4dJGquGv1EcS0AHQEAIwAmTJe211qZaa+skvSnpEodrAtoNa+0yScVHzL5E0kve1y9JutSXNQHtkbU2x1q7wfu6XA1fdOPE8QQ0i21Q4Z0M9j6sOJaAZjPGxEu6QNKzjWZzLAEth+MJaAZjTFdJMyU9J0nW2jprbak4loAOgYAQgBPiJB1oNJ3pnQfg5PWy1uZIDaGHpJ4O1wO0K8aY/pLGS1ojjieg2bxdIm6SlC/pM2stxxJwch6T9HNJnkbzOJaAk2MlfWqMSTbGzPPO43gCmidRUoGkF7zdXz9rjAkXxxLQIRAQAnCCOco86/MqAACQZIzpIuldST+21pY5XQ/QHllr3dbacZLiJU02xoxyuCSg3THGXCgp31qb7HQtQAdxurV2ghqGN7nHGDPT6YKAdihI0gRJT1prx0uqFN2JAh0GASEAJ2RK6ttoOl5StkO1AB1FnjGmtyR5n/MdrgdoF4wxwWoIB1+z1i7wzuZ4Ak6St8upr9QwVi7HEtA8p0u62BiTpoZhGM40xrwqjiXgpFhrs73P+ZLeU8NwJxxPQPNkSsr09g4hSf9RQ2DIsQR0AASEAJywTtJgY8wAY0yIpKslLXS4JqC9WyjpJu/rmyR94GAtQLtgjDFqGEtjh7X2740WcTwBzWCMiTHGdPe+7izpbEk7xbEENIu19kFrbby1tr8aviN9Ya29XhxLQLMZY8KNMRHfvJZ0rqRt4ngCmsVamyvpgDFmqHfWWZK2i2MJ6BCMtfTqB8D3jDHnq2F8jUBJz1tr/+hsRUD7YYx5Q9IsSdGS8iQ9JOl9SW9L6icpQ9KV1tpih0oE2gVjzHRJyyVt1bdjPf1SDeMQcjwBTWSMGSPpJTV8rguQ9La19nfGmChxLAEnxRgzS9LPrLUXciwBzWeMSVRDq0GpoYvE1621f+R4AprPGDNO0rOSQiSlSrpF3s984lgC2jUCQgAAAAAAAAAAAMCP0MUoAAAAAAAAAAAA4EcICAEAAAAAAAAAAAA/QkAIAAAAAAAAAAAA+BECQgAAAAAAAAAAAMCPEBACTWSMed4Yk2+M2XaM5cYY87gxZq8xZosxZoKvawQAAAAAAAAAADgRAkKg6V6UNPc4y8+TNNj7mCfpSR/UBAAAAAAAAAAA0CwEhEATWWuXSSo+ziqXSHrZNlgtqbsxprdvqgMAAAAAAAAAAGgaAkKg5cRJOtBoOtM7DwAAAAAAAAAAoM0IcroAoAMxR5lnj7qiMfPU0A2pwsPDJw4bNqw16wIAAAAAAAAAnKLk5ORCa22M03UALYGAEGg5mZL6NpqOl5R9tBWttfMlzZekpKQku379+tavDgAAAAAAAABw0owx6U7XALQUuhgFWs5CSTeaBlMkHbTW5jhdFAAAAAAAAAAAQGO0IASayBjzhqRZkqKNMZmSHpIULEnW2qckfSzpfEl7JVVJusWZSgEAAAAAAAAAAI6NgBBoImvtNSdYbiXd46NyAAAAAAAAAAAATgpdjAIAAAAAAAAAAAB+hIAQAAAAAAAAAAAA8CMEhAAAAAAAAAAAAIAfISAEAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAAAAAAAAAADwIwSEAAAAAAAAAAAAgB8hIAQAAAAAAAAAAAD8CAEhAAAAAAAAAAAA4EcICAEAAAAAAAAAAAA/QkAIAAAAAAAAAAAA+BECQqAZjDFzjTG7jDF7jTEPHGV5N2PMh8aYzcaYFGPMLU7UCQAAAAAAAAAAcCwEhEATGWMCJT0h6TxJIyRdY4wZccRq90jabq0dK2mWpEeMMSE+LRQAAAAAAAAAAOA4CAiBppssaa+1NtVaWyfpTUmXHLGOlRRhjDGSukgqluTybZkAAAAAAAAAAADHRkAINF2cpAONpjO98xr7l6ThkrIlbZX0I2utxzflAQAAAAAAAAAAnBgBIdB05ijz7BHTcyRtktRH0jhJ/zLGdP3OjoyZZ4xZb4xZX1BQ0NJ1AgAAAAAAAAAAHBMBIdB0mZL6NpqOV0NLwcZukbTANtgrab+kYUfuyFo731qbZK1NiomJabWCAQAAAAAAAAAAjkRACDTdOkmDjTEDjDEhkq6WtPCIdTIknSVJxphekoZKSvVplQAAAAAAAAAAAMcR5HQBQHthrXUZY+6VtFhSoKTnrbUpxpg7vcufkvR7SS8aY7aqoUvSX1hrCx0rGgAAAAAAAAAA4AgEhEAzWGs/lvTxEfOeavQ6W9K5vq4LAAAAAAAAAACgqehiFAAAAAAAAAAAAPAjBIQAAAAAAAAAAACAHyEgBAAAAAAAAAAAAPwIASEAAAAAAAAAAADgRwgIAQAAAAAAAAAAAD9CQAgAAAAAAAAAAAD4EQJCAAAAAAAAAAAAwI8QEAIAAAAAAAAAAAB+hIAQAAAAAAAAAAAA8CMEhAAAAAAAAAAAAIAfISAEAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkKgGYwxc40xu4wxe40xDxxjnVnGmE3GmBRjzFJf1wgAAAAAAAAAAHA8QU4XALQXxphASU9IOkdSpqR1xpiF1trtjdbpLunfkuZaazOMMT0dKRYAAAAAAAAAAOAYaEEINN1kSXuttanW2jpJb0q65Ih1rpW0wFqbIUnW2nwf1wgAAAAAAAAAAHBcBIRA08VJOtBoOtM7r7EhkiKNMV8ZY5KNMTf6rDoAAAAAAAAAAIAmoItRoOnMUebZI6aDJE2UdJakzpJWGWNWW2t3H7YjY+ZJmidJ/fr1a4VSAQAAAAAAAAAAjo4WhEDTZUrq22g6XlL2UdZZZK2ttNYWSlomaeyRO7LWzrfWJllrk2JiYlqtYAAAAAAAAAAAgCMREAJNt07SYGPMAGNMiKSrJS08Yp0PJM0wxgQZY8IknSZph4/rBAAAAAAAAAAAOCa6GAWayFrrMsbcK2mxpEBJz1trU4wxd3qXP2Wt3WGMWSRpiySPpGettducqxoAAAAAAAAAAOBwxtojh1AD4EtJSUl2/fr1TpcBAAAAAAAAADgOY0yytTbJ6TqAlkAXowAAAAAAAAAAAIAfISAEAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAAAAAAAAAADwIwSEAAAAAAAAAAAAgB8hIAQAAAAAAAAAAAD8CAEhAAAAAAAAAAAA4EcICAEAAAAAAAAAAAA/QkAIAAAAAAAAAAAA+BECQgAAAAAAAAAAAMCPEBACAAAAAAAAAAAAfoSAEGgGY8xcY8wuY8xeY8wDx1lvkjHGbYz5ni/rAwAAAAAAAAAAOBECQqCJjDGBkp6QdJ6kEZKuMcaMOMZ6f5G02LcVAgAAAAAAAAAAnBgBIdB0kyXttdamWmvrJL0p6ZKjrHefpHcl5fuyOAAAAAAAAAAAgKYgIASaLk7SgUbTmd55hxhj4iRdJukpH9YFAAAAAAAAAADQZASEQNOZo8yzR0w/JukX1lr3cXdkzDxjzHpjzPqCgoKWqg8AAAAAAAAAAOCEgpwuAGhHMiX1bTQdLyn7iHWSJL1pjJGkaEnnG2Nc1tr3G69krZ0vab4kJSUlHRkyAgAAAAAAAAAAtBoCQqDp1kkabIwZIClL0tWSrm28grV2wDevjTEvSvroyHAQAAAAAAAAAADASQSEQBNZa13GmHslLZYUKOl5a22KMeZO73LGHQQAAAAAAAAAAG0eASHQDNbajyV9fMS8owaD1tqbfVETAAAAAAAAAABAcwQ4XQAAAAAAAAAAAAAA3yEgBAAAAAAAAAAAAPwIASEAAAAAAAAAAADgRwgIAQAAAAAAAAAAAD9CQAgAAAAAAAAAAAD4EQJCAAAAAAAAAAAAwI8QEAIAAAAAAAAAAAB+hIAQAAAAAAAAAAAA8CMEhAAAAAAAAAAAAIAfISAEAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBBoBmPMXGPMLmPMXmPMA0dZfp0xZov3sdIYM9aJOgEAAAAAAAAAAI6FgBBoImNMoKQnJJ0naYSka4wxI45Ybb+kM6y1YyT9XtJ831YJAAAAAAAAAABwfASEQNNNlrTXWptqra2T9KakSxqvYK1daa0t8U6ulhTv4xoBAAAAAAAAAACOi4AQaLo4SQcaTWd65x3LbZI+adWKAAAAAAAAAAAAminI6QKAdsQcZZ496orGzFZDQDj9GMvnSZonSf369Wup+gAAAAAAAAAAAE6IFoRA02VK6ttoOl5S9pErGWPGSHpW0iXW2qKj7chaO99am2StTYqJiWmVYgEAAAAAAAAAAI6GgBBounWSBhtjBhhjQiRdLWlh4xWMMf0kLZB0g7V2twM1AgAAAAAAAAAAHBddjAJNZK11GWPulbRYUqCk5621KcaYO73Ln5L0G0lRkv5tjJEkl7U2yamaAQAAAAAAAAAAjmSsPeoQagB8JCkpya5fv97pMgAAAAAAAAAAx2GMSaZBCDoKuhgFAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAAAAAAAAAADwIwSEAAAAAAAAAAAAgB8hIAQAAAAAAAAAAAD8CAEhAAAAAAAAAAAA4EcICAEAAAAAAAAAAAA/QkAIAAAAAAAAAAAA+BECQgAAAAAAAAAAAMCPEBACAAAAAAAAAAAAfoSAEGgGY8xcY8wuY8xeY8wDR1lujDGPe5dvMcZMcKJOAAAAAAAAAACAYyEgBJrIGBMo6QlJ50kaIekaY8yII1Y7T9Jg72OepCd9WiQAAAAAAAAAAMAJEBACTTdZ0l5rbaq1tk7Sm5IuOWKdSyS9bBusltTdGNPb14UCAAAAAAAAAAAcCwEh0HRxkg40ms70zmvuOgAAAAAAAAAAAI4JcroAoB0xR5lnT2IdGWPmqaELUkmqNcZsO8XaAOBI0ZIKnS4CQIfDuQVAa+DcAqA1cG4B0BqGOl0A0FIICIGmy5TUt9F0vKTsk1hH1tr5kuZLkjFmvbU2qWVLBeDvOLcAaA2cWwC0Bs4tAFoD5xYArcEYs97pGoCWQhejQNOtkzTYGDPAGBMi6WpJC49YZ6GkG02DKZIOWmtzfF0oAAAAAAAAAADAsdCCEGgia63LGHOvpMWSAiU9b61NMcbc6V3+lKSPJZ0vaa+kKkm3OFUvAAAAAAAAAADA0RAQAs1grf1YDSFg43lPNXptJd3TzN3Ob4HSAOBInFsAtAbOLQBaA+cWAK2BcwuA1sC5BR2GacgzAAAAAAAAAAAAAPgDxiAEAAAAAAAAAAAA/AgBIeAjxpi5xphdxpi9xpgHjrLcGGMe9y7fYoyZ4ESdANqXJpxbrvOeU7YYY1YaY8Y6USeA9uVE55ZG600yxriNMd/zZX0A2qemnFuMMbOMMZuMMSnGmKW+rhFA+9OE70TdjDEfGmM2e88ttzhRJ4D2wxjzvDEm3xiz7RjLuY6LDoGAEPABY0ygpCcknSdphKRrjDEjjljtPEmDvY95kp70aZEA2p0mnlv2SzrDWjtG0u9FX/kATqCJ55Zv1vuLpMW+rRBtiTfMyXS6jhMxxtxsjFnhdB3+rCnnFmNMd0n/lnSxtXakpCt9XSeA9qWJn1vukbTdWjtW0ixJjxhjQnxaKID25kVJc4+znOu46BAICAHfmCxpr7U21VpbJ+lNSZccsc4lkl62DVZL6m6M6e3rQgG0Kyc8t1hrV1prS7yTqyXF+7hGAO1PUz63SNJ9kt6VlO/L4vBdxpjp3lbiB40xxcaYr40xk7zLDgvGjDFdvcvfNcYMNsZYY0zQEft70RjzB1//HOjwmnJuuVbSAmtthiRZazm/ADiRppxbrKQIY4yR1EVSsSSXb8sE0J5Ya5ep4VxxLFzHRYdAQAj4RpykA42mM73zmrsOADTW3PPGbZI+adWKAHQEJzy3GGPiJF0m6Skf1oWjMMZ0lfSRpH9K6qGG/6vfSqo9yrqRkpZISpf0fUn1vqsUaNLnliGSIo0xXxljko0xN/qsOgDtVVPOLf+SNFxStqStkn5krfX4pjwAHRTXcdEhEBACvmGOMs+exDoA0FiTzxvGmNlqCAh/0aoVAegImnJueUzSL6y17tYvBycwRJKstW9Ya93W2mpr7afW2i2NVzLGREv6QlKKpOuttSfVcsIY09nbwrDEGLNd0qQjlvfxtk4sMMbsN8b8sNGyh40x7xhjXjXGlBtjthpjhhhjHvSO8XLAGHNuo/VvMcbs8K6baoy5o9GyWcaYTGPM/d5tcxqPKWWMiTLGLDTGlBlj1koaeDI/L1pUU84tQZImSrpA0hxJvzbGDGntwgC0a005t8yRtElSH0njJP3Le4MNAJwsruOiQyAgBHwjU1LfRtPxarhzrbnrAEBjTTpvGGPGSHpW0iXW2iIf1Qag/WrKuSVJ0pvGmDRJ35P0b2PMpT6pDkfaLcltjHnJGHOet5XgkXpIWippjaRbT7HVxENqCNsGquGC603fLDDGBEj6UNJmNdxBfZakHxtj5jTa/iJJr0iKlLRRDWNYBnjX/52kpxutmy/pQkldJd0i6VFjzIRGy2MldfNue5ukJxr9/E9IqpHUW9Kt3gec1dTvRIustZXW2kJJyySN9VF9ANqnppxbblFD98XWWrtXDeO0D/NRfQA6Jq7jokMgIAR8Y52kwcaYAd6BsK+WtPCIdRZKutE0mCLpoLU2x9eFAmhXTnhuMcb0k7RA0g3W2t0O1Aig/TnhucVaO8Ba299a21/SfyTdba193+eVQtbaMknT1XDH8jOSCrwt53o1Wq2vGloavmCtPdU7m6+S9EdrbbG19oCkxxstmyQpxlr7O2ttnbU21VvT1Y3WWW6tXextwfiOpBhJf7bW1qth3Kj+xpju3p/tv9bafd4LukslfSppRqN91Uv6nbW23lr7saQKSUONMYGSrpD0G2/QtE3SS6f4c+PUNeU70QeSZhhjgowxYZJOk7TDx3UCaF+acm7JUMNNK/L+fRwqKdWnVQLoaLiOiw4h6MSrADhV1lqXMeZeNdwhHSjpeWttijHmTu/ypyR9LOl8SXslVanhDjcAOKYmnlt+IylKDa17JMllrU1yqmYAbV8Tzy1oQ6y1OyTdLEnGmGGSXlVDN7DXeFfZrIYw7hNjzFnW2o3e+d90Mxrc6PU308can7CPDh9vJb3R6wRJfYwxpY3mBUpa3mg6r9HrakmFjbqqrfY+d5FUaow5Tw0tFoeo4ebWMDWMHfWNoiO6Sq3ybhujhu+6x6oTDmjKucVau8MYs0jSFkkeSc96A14AOKomfm75vaQXjTFb1dAt4C+8rZQB4KiMMW9ImiUp2hiTqYbPpMES13HRsZhTv4EUAAAAANBWeC+U3mGtHW2MuVnSD6y1040xP5f0c0mzrLXbvC3tqiWN9YaM32y/UtJ8a+2LR9n3fkl3WWsXeadvl/SQtTbeGDNV0svW2sHHqOthSYOstdd7p89WQwDU3zsdpIZgsq+kAkklkm6U9IG1tt4Y876kbdba/zHGzJL0qrU2vtH+0yT9QNKXauhedLS1dqd32R8lnWGtnd6sf0wAAAAA6KDoYhQAAAAA2iljzDBjzP3GmHjvdF81tBxcfeS61tq/SvqHpCXGmKHelnvvSvqjMSbKGBNsjLlG0ghJnxzjLd+W9KAxJtL7nvc1WrZWUpkx5hfGmM7GmEBjzChjzKST+NFCJIWqISh0eVsTntuUDb0/1wJJDxtjwowxI9RorEQAAAAAAAEhAAAAALRn5WoYp22NMaZSDcHgNkn3H21la+3vJT0r6XNjzEBJd0sqVkOXjvmS7pV0gbU272jbS/qtGrrr3K+GMQFfabRvt6SLJI3zLi/0vle35v5Q1tpyST9UQyBZIulafXdMqeO5Vw3djeZKelHSC82tAQAAAAA6MroYBQAAAAAAAAAAAPwILQgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAAAAAAAAAADwI0FOFwD4u+joaNu/f3+nywAAAAAAAAAAHEdycnKhtTbG6TqAlkBACDisf//+Wr9+vdNlAAAAAAAAAACOwxiT7nQNQEuhi1EAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAABAh2Gt1ZtrM5RaUOF0KQAAAAAAtFkEhAAAAAA6jL99uksPLNiqu17doHq3x+lyAAAAAABokwgIAQAAAHQIL3y9X098uU+T+kdqV165nlme6nRJAAAAAAC0SQSEAAAAANq9hZuz9buPtuvcEb30xu1TNHdkrB7/fI8yiqqcLg0AAAAAgDaHgBAAAABAu7Z8T4Huf3uTJiX00OPXjFdQYIAeuniEAo3Rrz/YJmut0yUCAAAAANCmEBACAAAAaLc2HyjVHa8ka2BMFz1zU5I6BQdKknp366yfzRmqpbsL9N+tOQ5XCQAAAABA20JACByHMeZ5Y0y+MWZbo3k9jDGfGWP2eJ8jGy170Biz1xizyxgzx5mqAQAA/ENqQYVueXGdeoSH6KVbJ6tb5+DDlt84tb9Gx3XTbz/croPV9Q5VCQAAAABA20NACBzfi5LmHjHvAUmfW2sHS/rcOy1jzAhJV0sa6d3m38aYQN+VCgAA4D/yymp0w3NrJUkv3zpZvbp2+s46gQFGf7pstIoqavW3xbt8XSIAAAAAAG0WASFwHNbaZZKKj5h9iaSXvK9fknRpo/lvWmtrrbX7Je2VNNkXdQIAAPiTg9X1uun5tSqpqtOLt0xSYkyXY647Or6bbprWX6+uSdfGjBIfVgkAAAAAQNtFQAg0Xy9rbY4keZ97eufHSTrQaL1M7zwAAAC0kJp6t25/ab32FVTo6Rsmakx89xNuc/+5Q9UropMeXLBV9W5P6xcJAAAAAEAbR0AItBxzlHn2qCsaM88Ys94Ys76goKCVywIAAOgYXG6PfvjGRq1LL9YjV43TjMExTdquS2iQHr54pHbmluuFr/e3cpUAAAAAALR9BIRA8+UZY3pLkvc53zs/U1LfRuvFS8o+2g6stfOttUnW2qSYmKZd2AIAAPBn1lr9z/vb9On2PD104QhdPLZPs7afM7KXzh7eS49+tkeZJVWtVCUAAAAAAO0DASHQfAsl3eR9fZOkDxrNv9oYE2qMGSBpsKS1DtQHAADQ4fz9s916c90B3TN7oG4+fUCztzfG6LeXjJQx0m8+SJG1R+3oAQAAAAAAv0BACByHMeYNSaskDTXGZBpjbpP0Z0nnGGP2SDrHOy1rbYqktyVtl7RI0j3WWrczlQMAAHQcL369X//8Yq+untRXPzt36EnvJ657Z/30nCH6Yme+Fm3LbcEKAQAAAABoXwx3zgLOSkpKsuvXr3e6DAAAgDbpw83Z+uGbG3X28F568roJCgo8tXscXW6PLv7X1yqqrNWSn56hiE7BLVQpAAAAgI7OGJNsrU1yug6gJdCCEAA6kI0ZJbrn9Q3KKq12uhQAAE7Zij2F+unbmzQpoYf+ec34Uw4HJSkoMEB/uny08str9cinu1ugSgAAAAAA2h8CQgDoIPYXVurWF9fpv1tydNVTq5RRVOV0SeggquvceuDdLXp+xX7G7ALgM1syS3XHK+s1MKaLnrkpSZ2CA1ts3+P6dtcNUxL00qo0bT5Q2mL7BQAAAACgvSAgBIAOoLiyTre8sFbGGD1x7QRV1rl05dMrtTe/wunS0M5V1rp0y4tr9ea6A/rdR9v1P+9vk8vtcbosAB3c/sJK3fLCOnUPC9FLt05Wt84t3w3oz+YMVUyXUP3yva2c1wAAAAAAfoeAEADauZp6t37w0jrlHKzRMzcm6YIxvfXmvClye6yunr9KO3PLnC4R7VRZTb1ueG6N1qWV6NHvj9WdZwzUa2sydPvL61VZ63K6PAAdVH5ZjW54bo2spFdum6xeXTu1yvt07RSshy4aqZTsMr20Kr1V3gMAAABwynMr9uuX721VPTfDATgGAkIAaMc8HqufvLVJGw+U6rHvj9PEhEhJ0rDYrnrrjqkKCgjQ1fNXa1vWQYcrRXtTUlmn655Zoy2ZB/Wva8brsvHxeuC8YfrjZaO0dHeBvj9/lfLLapwuE0AHU1ZTrxufX6viyjq9cPMkJcZ0adX3O390rGYPjdEjn+5SNuP3wgEr9xbqlhfW6g8fbed3EAAAtJiPt+bo9x9t1+trMvSTtzbJ7WG4EADfRUAIAO3Ynz7eoU+25epX5w/XeaN7H7ZsYEwXvX3HVHUJDdI1z6xWcnqJQ1WivSkor9U1z6zWrrxyzb9x4mG/W9edlqDnbpqk1IJKXfbvldqdV+5gpQA6kpp6t25/ab32FVTo6Rsmamzf7q3+nsYY/e6SUfJYq4cXprT6+wHf2JVbrptfWKtrn12jrVllemFlmmb+9Uv99K1N2pFD7w8AAODk7cgp0/1vb9aEft31/+YM1UdbcvTLBVtlLSEhgMMZTgyAs5KSkuz69eudLgPt0Itf79fDH27XzdP666GLRsgYc9T1skurde0zq5VfXqvnbpqkqQOjfFwp2pPcgzW69tnVyi6t1rM3TtL0wdFHXW9b1kHd+uI6Vde79fT1EzVt0NHXA4CmcHus7n4tWYtT8vSPq8fpknFxPn3/p5bu058/2an5N0zUuSNjffre8C95ZTX6+6e79U7yAYWHBune2YN007T+Kqyo1fMr0vTmugxV1bl1xpAY3TEzUVMHRh3zMx5aj7VWxZV1Si+uUkZRldKLqpReVKn04ipFdArSHy4dpfjIMKfLBI7rha/3a+nuAv3q/OEa3CvC6XKAdqu6zq1deeXakVN26NGvR7j+dPkohQYFOl3ed5RU1uniJ1aozuXRh/dOV8+unfTIp7v0zy/26tbTB+jXFw7ns8UpMsYkW2uTnK4DaAkEhIDDCAhxMj5NydUdrybr7OG99NT1ExUYcPwPd/llNbru2TXKKK7S/BuTdMaQGB9VivYks6RK1z6zRkUVtXrhlsmaPKDHcdfPKq3WLS+s1f7CSv358jG6YmK8jyoF0JFYa/XL97bpjbUZeuiiEbrl9AE+r6He7dFF/1yhg9X1+uynZ6hLaJDPa0DHVlHr0vyl+/TM8v1yeTy6cWp/3Tt7kCLDQw5b72BVvV5dk64Xvk5TYUWtRsd107yZiTpvVKyCAukAqCV5PFY5ZTUNwZ83BMwobnidUVSl8kbjLRsj9e7aSf2iwpSSXabAAKN/XD2ez9Ros5Zsz9MPXl6vACMFBQTontmDdNesgQoJ8p/zSHJ6sf73453q1bWTZgyO1owhMYrr3tnpstCGWWuVW1bjDQHLtd0bBqYVVuqb3jnDQwI1qGcXbc48qAtG99bj14w/4fUYX3K5Pbrx+bVan1ait++cqnHeHjmstfrdR9v1wtdp+uGZg/TTc4c6W2g7R0CIjoSAEHAYASGaa9OBUl09f5WGxnbVm7dPUeeQpt2xVlRRq+ufW6t9+RV64roJOmdEr1auFO1JWmGlrn1mtSpqXXrp1ska3y+ySdsdrK7XXa8ma+W+Iv3k7CH64VmDuBsRQLP8/dNdevyLvbp71kD9fO4wx+pITi/RFU+u1G3TB+jXF45wrA50LC63R2+uO6DHluxWYUWdLhzTWz+fM0z9oo7f+qym3q33NmbpmWWpSi2sVN8enfWD6Ym6MileYSEE2E1V63LrQHH1oeCvcUvAzOJq1bk9h9YNDjTqGxmmflFhSugRpoSocCVEhSkhKkzxkWHqFNzwmTutsFJ3vpqsXXnl+snZQ3Tv7EEKaEMXh4G9+eW69ImVSowJ15PXT9RfPtmphZuzNaRXF/3v5WMOjVvfUbk9Vk8t3ae/f7ZbvSJC5bZWeWW1kqSBMeGaMThGZwyJ0WmJPTif+rFal1t78iq0PadMO3O8rQNzy1RaVX9onb49Omt4bFcN793wGNG7q+IjOysgwOiZZan648c7dOPUBP324pFt5jvw7z/arudW7Nf/fW+Mrkzqe9gya60eeHer3lp/QA+eN0x3nDHQoSrbPwJCdCQEhIDDCAjRHBlFVbr8ya/VOSRQ7919uqK7hDZr+4NV9brxhbVKyTqof1w9XheM6X3ijdDh7ckr13XPrpHLY/XKbZM1sk+3Zm1f5/LogQVbtGBDlr43MV7/e/loBdPKAfB7LrdHxZV1yi+vVWFFrQrKa1VYUed9bnjkl9dqb36Fvp/UV3++YrTjF1d+9d5WvbE2Qwvvna5Rcc07FwKNWWv12fY8/XnRTqUWVGpy/x765QXDD93J31Qej9VnO/I0f1mqktNL1D0sWDdOSdCN0/o3+3Ogv0grrNSCjVn6aEu29hdWqvElj/CQQPWLCvcGgN+GgP16hKlP985NbgVSXefWL9/bqvc2ZunMYT316FXj1C0suJV+IqDpDlbX69InvlZ5Tb0W3jtdfbwt5r7Ymaf/eW+bcspqdNPU/vrZnKEdsrV8flmNfvzWJq3cV6SLxvbRny4bpS6hQdqTX6Fluwu0bE+h1qQWqdblUUhggJL6R2rG4BjNGBytEb27EvZ3UPnlNdqRc3gXofsKKuX2NgvsFBygobFdNaJ3xKEwcGhshLp2Ov55/U8f79D8Zan66TlD9MOzBvviRzmud5Mzdf87m3XztP56+OKRR13H7bH60Zsb9dGWHP3h0lG6fkqCj6vsGAgI0ZEQEAIOIyBEU5VW1enyJ1equLJO7941TQNjupzUfspr6nXri+uUnF6iv105VpdPoFtIf5aSfVA3PLdWgQFGr/3gNA05yfFJrLV6bMke/ePzPZo+KFr/vn7CCb9QAWh5tS63HluyR+9vzFJIUIDCQoIUHhKoziGBCg8JUlhooMK+eR0SpLCQQIWFfjMd2DDvsOlAhYcGKTQoQMYYuT0N43IVHBb6NXquqFVheZ0KK2pVXFWno33VCA8JVHREqGK6hCq6S6iG9+6qe2YPbBPdJx6srtdZjyxVn+6d9N7dp7epLqM6mqKKWn22PU+LUnJVUlWvX8wdqmkDO8Z4tpsOlOpPH+/Q2v3FSowJ1wNzh+mcEb1OOQBfn1asp5elasmOPIUEBuh7E+N1+4xE9Y8Ob6HK26/Sqjp9tCVHCzZkakNGqYyRTh8YrYkJkYdaAfbrEa7oLiEtdiOCtVavrE7X7z/art7dOuvJ6yc0+yYroCW5PVa3vbROK/YU6o15UzSp/+HDBVTUuvR/i3bq5dXp6tOts/5w2SjNHtrToWpb3pe78vWztzerss6l3108SlcmxR/1eK+pd2tdWrGW7ynUst0F2plbLkmK7hKi6YOiNXNIjKYPjlbPiE6+/hH81rasg/rH53u0Ib2kxfdd5/aovObbLqN7d+vkDQG/DQP7R4Wf1Gc+j8fqZ//ZrAUbsvTHy0bputOcC9s2HyjVlU+v0sR+kXr5tsnHvWG33u3Rna8k64td+fr7VWN12XiuCTUXASE6EgJCwGEEhGiKmnq3bnxurTYdKNWrPzjthGPDnUhVnUu3v7xeK/cV6Y+Xjta1p/VroUrRFFml1Xp/Y5Y+2pKjuO6d9aOzBmt0vO8vKG06UKobn1uj8NAgvfaD05R4kqFzY++sP6AHF2zVoJ5d9PzNkw7dtQyg9W3PLtNP396knbnlOnt4L3UJDVRlnVtVdS5V1rpVXedWZZ1LVXVuVda6VOvynHinXgFG6hwcqOp696ExWBrrFBygmIiGwC+mS+i3AaD3OSYiRDFdOik6IqTNd+e1cHO2fvjGRv324pG6aVp/p8tptuLKOv3f4l1ak1p0qGXE9EHR3xnrzgnZpdVanJKrRdtytS6tWB4r9esRJiurA8XV+n5SX/3y/OHttiVWRlGV/rp4pz7akqPoLiH60dlDdPWkvi3eqn5fQYWeXZ6qd5OzVO/xaO7IWM2bmdjk7sE7ijqXR0t3F2jBhkx9viNfdW6PBvfsoismxuvScXGK7eabi/sbMkp096sbVFJVpz9dNpoxmeGYvy7aqX9/te+ErYKS04v1i3e3am9+hS4Z10e/uXCEotpxi+Q6l0d/+3SX5i9L1bDYCP3r2vEa1LPpNz3mldVo+Z5CLd9ToBV7ClVUWSdJGt67q2YOjtaMwTFK6h95qJthtJyU7IN6bMkefbY9T107Bem8Ub0VHNSyN2cFGKOEqHAN7x2hEb27qntYy34eqnd7NO/l9Vq6u0D/vm6C5o7yfS9N+eU1uvifXyso0GjhvdPVowmf+Wrq3br1xXVas79YT1w7QXNHxfqg0o6DgBAdCQEh4DACQpyIx2P1o7c26cPN2frnNeN10dg+LbLfmnq37n5tg77Yma/fXDhCt04f0CL7xdGV19Trk225WrAhU6tTiyVJE/p11978CpXVuHT28F768dmDfdal3bq0Yt3ywjpFhgfr9R9MUd8exx8LqTlW7CnUXa8mKyw0UM/fPIm76YFW5vZYPb1snx79bLe6dQ7RX783WmcOO/E4s26PVVWjwLCqzt3wus6lqtqG5+ojpruEBn0bBDZ6Dg8JdLx70JZirdWNz6/VxoxSLfnpGT4LGU6V22P1+toM/W3xLlXUujQ1MUpbMktVVuOSMdKYuG6aMThGM4fEaHy/7j7rCnp/YaUWbcvVopRcbT5QKkka2itCc0bFau7IWA3vHaFal0ePLdmjZ5anqkd4iH538UidN7r9dINeUlmnf325Vy+vSlNggNG8GYmad8bAVu++L7+8Ri+tTNMrq9JVVuPS5AE9dMfMRM0e2rPDdpNnrdXWrINasCFLCzdnq7iyTlHhIbp4XB9dMSFeI/t0deRcVFhRq/te36hVqUW67rR++s1FIxQaRJgA3/loS7bufX2jrpncT/97+egTrl/rcuvfX+7Tv7/aqy6hQfrNRSN06bi4dve3PKOoSve9sUGbMw/qhikJ+tUFw08pyPN4rLbnlGnZngIt212g5PQS1butOgUH6LQBUZo5JEYzB0drUM8u7e7fqi3ZkVOmfyzZo0UpuYroFKQfTE/ULdP7t9seaKrqXLru2TVKyS7Ty7dO1pTEKJ+9d53Lo2ufWa1t2Qe14K7TNaJP1yZvW1nr0vXPrdG2rIN69qZJOmNITCtW2rEQEKIjISAEHEZAiBP58yc79dTSfXrgvGG6s4UHka5zefSjNzfqk225+vncobp71qAW3b+/c3usVuwt1IINmVqckquaeo/6R4Xp8gnxumx8nPr2CFNZTb1eWJGmZ1ekqrzGpTkje+nHZw/R8N5N/2DfXCv3Fuq2l9ard7dOeu3209S7W8u38tuZW6ZbX1ing9X1euK6CZrVgbovAtqStMJK3f/OZiWnl+j80bH6w6Wjm3TXMI4vvahS5z66TGcN76l/XzfR6XJOKDm9RA8t3KZtWWWamhil314yUkN6Rcjl9mhL1kEt312oZXsKtOlAqdweqy6hQZo6MOpQy4iW7KLSWqsdOeValJKrxdtytSuvoeu2sfHdDoWCx2qxvi3roH7x7halZJdpzshe+t0lo9Sra9sNaGvq3XppZZqe+HKvKmpdunJiX/303CE+r7mi1qW31h3Qc8tTlX2wRoN7dtHtMxN1ybg+HSakyi6t1vubsrRgQ5b25lcoJChA54zopSsmxGnG4Jg2Mfaxy+3R3z7draeW7tPYvt315HUTHOlJwVqrVfuK9PSyVGWXVuuayf30/Ul9Fd4Bx5tDg+3ZZbriyZUa2aerXr99ikKCmn487M4r1y/e3aKNGaU6Y0iM/njZKMVHttyNg63pw83Z+uWCrTJG+uv3xrRKy63KWpdWpxYd6o40tbBSkjSoZxf95sIRmkmg0iy7csv1j8936+OtuYoIDdKt0wfo1ukD1K1z+wwGGyuprNOVT69S3sEavXXH1GYFdafil+9t1etrMk76ZvKDVfW65pnVSi2s0Mu3nnpvVf6CgBAdCQEh4DACQhzPa2vS9av3tum60/rpD5eOapW7FF1uj+5/Z7M+2JStH545SD85Zwh3Q56inbllWrAhS+9vzFJ+ea26dQ7WRWN76/IJ8Rrft/tR/30PVtfruRX79cKK/Sqvdem8UbH60dmDNSy2Zb9YfLkzX3e8mqwBUeF69QenKSai9boTyiur0S0vrNOuvHL94dJRumYyXdkCLcVaq9fWZOiP/92h4ECj3186SheP7cP5uwU98eVe/d/iXXr+5qQmtch0QmFFrf78yU79JzlTsV076VcXDNeFY3of8/fgYHW9Vu0r1DLvhc7MkmpJDd18zhjcMO7S1IFRzb6D3+Ox2pRZ2tBScFuuMoqrFGCkSf17aO6oWJ07MlZxTQxKXG6Pnl2xX49+tlshgQF68PzhunpS3zbVIs7jsVq4OVv/t3iXskqrNWtojB44b1iL/81urnq3R//dkqOnlu7TztxyxXbtpLtnD9T3J/Vtl0FhRa1Li7y9L6xKLZK10qT+kbp8QrzOH927zV5QXrQtRz97Z4tCggL0+NXjNX2wb8bWdLk9+mRbrp5etk/bssoU3SVEfXuEaWNGqbp1DtYNUxJ007T+rfrZryOy1mp/YaXWp5coOa1EyRklCgsJ1ONXj28T438WV9bpon+ukNtjtfC+009q3Dy3x+qVVWn66+JdkqSfnTtUN03r32bH4a2uc+u3H6bozXUHNDEhUv+4epzPQs0DxVVatqdAzyxLVVpRleaOjNX/XDi8TYaq27IO6q+LdymtsFJnDuupOSNjNXlAD0f+X/fkleuxz/fo4605Cg8J0q2n99dt0xPbbZfix5JdWq0rnlwpl8fq3TunqV9U6/5efHO96K5ZA/WLucNOej+FFbX6/tOrlFdWq9dvP01j4ru3XJEdFAEhOhICQsBhBIQ4li935uu2l9Zp1tCemn/DRAW14p3Rbo/VLxds1VvrD2jezEQ9eN4wLjI3U355jRZuyta7G7K0I6dMQQFGs4f11BUT4jR7WM8mX5g7WFWvZ1ek6oWv01RR69IFo3vrR2cP1pBeTR9H41gWbcvVfW9s0NDYCL1862k+aWVUUevSPa9t0NLdBbpn9kD97Nyh/G4Bpyj3YI1+8e4WLd1doBmDo/XX741plZbA/q7O5dEFjy9XVZ1bn/10ZpsaO9Hl9uiV1en6+2e7VVPv1m3TE3XfmYOa1ULIWqu0oiot93ajtmpfkSrr3AoMMJrQr7tmDI7RjMHRGhPf/agXE11uj9buL25oKZiSq7yyWgUHGk0bGK25o2J1zoheij6FMa3SCiv14IKtWpVapNMG9ND/Xj66RcbKPVUr9xXqfz/eqa1ZBzWyT1f98vzhOn2QbwKgprLWavmeQv3ri71am1as3t066e7Zg3RVUnybDwrdHquV+wq1YEOWFm3LVXW9W/16hOnyCXG6bHycEqKcD2SaYl9Bhe56NVl78yt0/7lDddcZA1st5K6qc+ntdQf07Ir9yiypVmJ0uH4wI1GXT4hTp+BAJaeXaP6yffp0e56CAwN0xYQ4/WBGoga2geOpLaqpd2tr1kElp5dofVqJNmSUqNg7Jl23zsGa0K+7Nh0olTFGz9yYpIkJzo39We/26Mbn1io5o0Tv3DFVY/t2P6X9ZZVW61fvbdVXuwo0rm93/eWKMRoae+rfQVrSztwy3fv6Ru0rqNA9swbpx2cPbtXvycdSU+/Ws8tT9a8v90qS7pk1SLfPTGwT4xRmlVbrkcW79N6mLHXrHKzxfbtr5b4i1bo8igoP0TkjemnOqFhNGxjV6n8T9uaX6x+f79VHW7IVFhyom0/vr9tnJLb4OIBtyZ68cl359Cp17xys/9w17ZQ+Cx3PurRiXTN/taYPjtZzN0065eA352C1rnxqVUOvBPOmtrljv60hIERHQkAInARjzFBJbzWalSjpN5K6S7pdUoF3/i+ttR8fb18EhDiarZkH9f35q5QYE6635k31SZdAHo/Vbz9M0Uur0nXj1AQ9fNHINnW3fltUU+/Wp9vztGBDppbtLpDHSmP7dtcVE+J04Zg+pxTAlVTW6dkVqXrx6zRV1bt1weje+vHZgzWo58l9UP9gU5Z++vZmjYnvphdvmezTu+5dbo9+/UGK3liboUvG9dFfvzemzV+gxHd985mRgNdZCzdn69fvb1Oty61fnT9c109J4P+kFa1LK9aVT63SHTMT9eD5w50uR5K0JrVIDy1M0c7ccs0YHK2HLx7ZIhf661webcgo0fI9BVq+p1Bbsw7K2oYL4tMHRWvmkGhNSYzS3vwKLdqWqyU78lRSVa9OwQGaNaSn5o6K1exhPVv074u1Vu+sz9Qf/rtdNS6PfnTWYM2bmejz7iRLKuv00ZaGm4A2HShVXPfO+tmcIbpkbFyb/qxkrdXXe4v06JLdSk4vUZ9unXTPmYN05cS+zeqC0Bd25ZZrwYZMvb8pS3llteraKUgXju2jy8fHaWJCZLs8z1XWuvTAgq36cHO2zh7eS49cNbZFj4/Cilq9vDJNL69OV2lVvSb06647zhioc4b3OurvZWpBhZ5dsV//Sc5Uvdujc4b30h1nDHQ04GoLCsprlZxeouT0YiWnl2hbVpnq3B5J0oDocE1MiFRSQqQmJkRqYEwXBQQY7S+s1C0vrFXOwRo99v1xjo2Z+tsPU/TC12l65MqxumJifIvs09qGFtK//XC7yqrrdfesgbrnzEGOf3b/pueE33+0XV07B+ux749rEzdnZJVW6w8fbdcn23KVEBWmhy8aqdnDnBlaoaymXv/+cp+e/3q/JOnW0wforlkD1a1zsCprXVq6u0CLtuXqi535qqh1KSI0SGcO76m5I2N1xtCYFr0RKrWgQo9/vkcfbM5W5+BA3TStIRj0l27wk9NLdN2zqzW4Z4TemDelxcckzi6t1sX/WqGunYL13j2nt9jflvSiSl351CpZSe/cMbVNtJJuqwgI0ZEQEAKnyBgTKClL0mmSbpFUYa39W1O3JyDEkTJLqnTZv1cqJDBA7909TT19OI6NtVZ//mSnnl6Wqu8n9dWfLh/dZruWcYrHY7U2rVjvbcjSx1tzVF7rUp9unXTZhDhdNj5eg3q27N3YxZV1emZ5ql5amabqercuHttHPzxrcLMuBr+97oB+sWCLJvXvoedvntTiX1CawlqrJ5fu018X7dLkAT00/4aJHfrO0faurKZeO3PKtSOnTDtzy7Q9p1y7cstkZBQdEaKYLqGK7hKqmIjvPsd4nzuHEAK3pNKqOv3P+9v00ZYcjevbXX+/amybaE3lDx54d4veSc7UR/dNb9XxYU8kv6xGf/p4h97flK247p316wuHa87I2FYLTooqavX1viIt212g5XsKlFdWe2hZRKcgnT28l+aMjNUZQ2Ja/XjPL6vRwx+m6OOtuRreu6v+csXoVu/+qs7l0Ze78rVgQ6a+2JmverfV0F4RumpSX113Wr820Uqkqb5pUfjokt3amNEQcN535iBdMTHe0bH7CsprtXBzthZsyFRKdkPvC7OGxujyCfE6c1jPdvVvfCzWWr24Mk1//O8OxUd21lM3TDzlrmjTCiv1zPJU/Sc5U7Uuj84Z0Ut3zExUUv+mjRtVUF6rl1el6eVV6TpYXa+khEjNm5mos48RLHYkHo/VnvyKhtaB3kAwvahKkhQSFKAxcd00sX+kJvZrCASjjtPyp7iyTj94aZ02HijVr84frtumD/BpkP2f5Ez97J3NuvX0AfrNRSNafP/FlXX6w0fbtWBjlgbGhOvPV4zRpCb+jrW0g1X1emDBFn2yLVdnDInRI1eNbbVWWSdr+Z4CPbQwRakFlTp7eE/95sKRrd695DfqXB69ujpd//xij0qr63XZuDjdP2foMbv2rql3a+W+Qi3alqvPtn97s88ZQ2I0d1SszhzW66QDp/2Flfrn53v0/qYshQYF6sZpCZo3I/G4x1JH9cXOPN3+crKmJDZ8B2+pkL2m3q0rn1ql/YWVev+eaSd9A/Gx7Mkr1/fnr1bn4EC9c+dUR8bSbQ8ICNGREBACp8gYc66kh6y1pxtjHhYBIU7Bwep6fe/Jlcotq9G7d01rkW4lm8taq8eW7NE/Pt+jS8b10SNXjnWk25a2JrWgQu9tzNKCDVnKKq1WeEigzhvdW5dPiNOUAVGtfkGlqKJW85el6uVV6ap1uXXJuDjdd+agEwYEr6xK068/SNGMwdGaf0OS46HNB5uy9P/e2aK+PTrrxVsmq2+Ptjdehz/xeKwOlFRpR05DCLgjp0w7csoOjUsmSZFhwRreu6uGxXZVgGlosVBQUavC8joVVNQe6nbrSOEhgUcNEL99DlFMRKiiwkPVKTigXbYO8ZWvduXr5//ZouLKOv347MG684yBnJd9qLSqTmc9slT9osL07p3TfH4Bvd7t0Ytfp+mxJbtV77a644xE3T1rkE/P59Y2XFRfnVqkfj3CNG1gtCMt0Ban5Oo3H2xTQXmtbps+QD85Z0iLtniw1mpz5kEt2JCphZuzVVpVr+guobp0XB9dNiFOI3p3bdfnKmutlu4u0KNL9mjzgVL17dFZ980erMsmxPksKKypd2vJjjwt2JClpbsL5PZYjY7rpsvGx+nicX3a3EX/lrI+rVh3v7ZBZTX1+vPlY3Tp+Lhm72NjRonmL0vVopRcBQcE6HJvV6Ene3NaZa1Lb68/oGeX71dWabUSY8I1b0aiLh0f1yHCWanhZ9ycWarktBKtT2/oLrS8xiVJigoPaWgd2D9SExN6aFRc12ZfwK+pd+snb23SJ9tyddPUBP3mopE+ubly04FSXfX0Kk3qH6mXbpncqp8JvtqVr1+9t01ZpdW6YUqCfj53qCKaOU7tqUhOL9EP39iovLIa/XzuUP1gemKbDbLrXB49//V+Pf75Hrk8VneeMVB3zxrYaseTtVYfb83VXxfvVHpRlU4fFKUHzxuuUXHdmrwPl9ujtWnFWrwtV4u83YUHBRhNGxSt85rRXXh6UaUe/3yv3t+UpeBAoxun9te8mYkd9pzeVO8mZ+r+dzbrgjG99c+rx5/y7661Vve/vVkLNmbpmRuTdM6I1hkje1vWQV0zf7ViIkL11h1TGbv2KAgI0ZEQEAKnyBjzvKQN1tp/eQPCmyWVSVov6X5rbcnxticgxDfqXB7d9PxarU8v1ku3Tta0gc52mfLkV/v0l0U7NXdkrB6/Znyb64qqtVXVubRmf7G35USh9uZXKMBI0wfH6PLxcTp3ZC9HxqMqrKjV00v36ZXV6apzeXTp+Dj98MzBR+3+49nlqfrDf3fo7OE99a9rJ7SZiz1rUos075VkBQcaPXfTpFMeLwVNU1Xn0s7cb0PAHTnl2plTpso6tyQpwDR0pTW8d1cN791VI7zPvbqGHveCeL3bo+LKOhWUfxMcHh4gHpquqFVpVf1R9xFgpLCQIIWFBCo8tOG54RGk8NDAQ8vCQoIUHhKosEbrhIcEKSz0iGXBgQoKbPmLR8GBAT49jiprXfrjxzv0+poMDenVRX+/alyzLvqg5by3MVM/eWuz/nDpKF0/JcFn77tyb6F+szBFe/MrNHtojB66aKTfd/dUVlOvP3+yU6+vyVDfHp31v5eN0fTBp/aZKau0Wu9vzNK7GzKVWlCp0KAAnTsyVpdPiNOMQdEdLpC31urLXfl6bMkebck8qH49wnTfmYN02fi4VvlZrbVan16iBRsy9dGWHJXXuBTbtZMuHR+nyyfEOXJDnBPyy2t07+sbtXZ/sW6amqBfXTDihJ+vPZ6G/6unl6ZqbVqxunYK0vVTEnTztP4t1suIy+3Rf7fmaP6yVKVklykmIlQ3T+uv609LULcw3wVBTVXrcquoouFzR2FF7RHPh88vr3Ud2m5Iry6amNDjUJehCVFhLRL4ezxW//vJDj2zfL/OHt5Lj18zrlW/I+SX1eiif61QcGCAPrx3uiJ90GVjZa1Lj3y6Wy+s3K9eEZ30h0tH6azhPVv1hgmPp6H3kb9/tltx3Tvr8WvGa1w7+c6Qe7BGf/x4hz7cnK34yM76zYUjdM6IXi3677U+rVh//HiHNmaUamivCD1w/jDNGhJzSu/h8VhtyizV4m25+mRbrjKKqxRgpKT+PTR3ZKzmjIr9TqvEjKIq/fOLPVqwMUtBAUbXT0nQHWckqmeE73pBauueXrpP//vJTt00NUEPXzzylP6Pvvl+/9NzhuiHZw1uwSq/a31asW54bq0SosL05rwp9P5zBAJCdCQEhMApMMaESMqWNNJam2eM6SWpUJKV9HtJva21tx5lu3mS5klSv379Jqanp/uwarRFje8Ee/T7Y3XZ+JYZQ+JUvfD1fv32w+2aPTRGT14/sc0ETK3BWqsdOeVatqehK7V1+0tU5/YoNChAkwf00BlDYnTR2D7q5cMuX48nv7xGTy9N1aur0+XyWF3mDQq/6crmn5/v0SOf7dYFo3vr0e+Pa3MB7978Ct3y4lrllNZo6sAozfXeocqXyVNjrVWty6PCitpDXYTuyG0IA9OKKvXNx76I0CBvEBhxKBAc0iui1Vsk1bk8Kqo8/GJeUWWdquvcqqx1q6rOpaq6hufDp92qrHOpqtZ9aFwgJwQYaXjvrkpKiNSEhEgl9e9xzO6bTtX6tGL99O3NOlBSpdtnJOqn5wzp0Ofgts5aq+ufW6OV+4o0ICr8O8dP726dWvTCX87Bav3hvzv03y056tujsx66cGSrX4xtb9akFunBBVuVWlipKyfG61cXDG/Wxavymnp9si1XCzZkanVqsSRp8oAeumJCnM4b3VtdfdhCxinWWn2+I1+PLtmtlOwy9Y8K031nDtYl4/q0SFCYVlipBRuz9N7GTB0orlZYSKDmjorVFRPiNSUxyi+7sa93e/SXT3bq2RX7NaFfdz1x3QT17vbdvyO1Lrc+2Jit+ctTtTe/Qn26ddKt0wfo6sn9Wq2reGutVu4r0lNL92n5nkKFhwTq6sn9dOv0Aa32t+4b9W6PiirqDn02KPhO8PdtAHiw+ug3G3XtFHSop4LoRl2ej+jTVRP6RrZ62PnyqjQ9vDBFo+O66dmbJrVKi5tal1vXzF+tHTnlWnD3NJ93e70xo0QPvLtVu/LKFR4SqH5R4UroEaaEqDAlRIUrISpM/XqEqU/3zqd0fOeX1+inb23Wir2FunBMb/3p8tHt8py8al+RHlq4TbvzKnTGkBg9fPFIDTjFm3z2FVTor4t2anFKnnp1DdX95wzVFRPjW/x8aq3VztxyLdqWq0XbcrUrr1ySNCa+m+aMjNVpA3ronfWZendDpgICjK47rZ/uOmOgT4dHaU/++N/temb5fv3s3CG698yTC/ZW7CnUjc+v0bkjYvXv6yb4pCXtij2FuvXFdRrep6te+8FpjgxV0lYREKIjISAEToEx5hJJ91hrzz3Ksv6SPrLWjjrePmhBCEn6+6e79PgXe3X/OUN0XyvfCdZcr6/J0K/e36rQoAAN7fXtxdDhvbtqWO+Idvll7RsF5bVasbdAy3YXavmeQhVWNIyvNCw2QjMGR2vG4BhNHtCjTV+Uzy+r0ZNL9+m1NRlye6yumBCniE7Bem7Ffl0+Pk5//d6YNtvyobCiVs+t2K9F23K1v7BSxkhJCZGaMzJWc0bG+kX3o1V1LhVX1h0KwapqXaqs+zYYq6z9NiD7JjSrrj9KeFb7bbDmOeKjXUJUmIbHdj0s0IiP7Nxug4Z6t+fwf49vwsNG0xW1Lnla4TNuSVWdNmaUatOBUlV5W1727tapISxMiFRSQg8N7x1xSsdcrcutx5bs0dNL96lP98565MqxOi0xqqV+BJyCoopavbo6Q9tzDmpHTrkyiqsOLeseFqxhsRGHtcAd1LNLs/9+1Lrcem7Ffv3z873yWKu7Zw3SHWcktum/Q06qqXfrn1/s0dNLU9U9LFgPXzxSF4zufczzm9tjtWJvoRZsyNTilFzV1HvUPypMl0+I12Xj4/zi787RWGv12fY8PbZkj7bnlCkxOlz3nTVIF4+Na/ZF54NV9fpoa7YWbMhScnqJjJFOHxityyfEac7IWIVzcVGS9N8tOfp//9mssJBAPX7N+EM9hxysrtfrazL0wtf7lV9eq2GxEbrzjIG6YExvn44XuT27TPOX7dOHW3JkJF00to/mzUxsViDl+qaHgaO07jsy+Cs5Rg8DEaFBh8K+Y46BHBGqqPCQNnGeXLI9T/e9sVFRXUL04i2TWnRsMGutHlywVW+uO6Anrp2gC8b0brF9N0edy6P3NmYe+juYVlSpzOLqw27gCg40io/0Boc9wg4Fif2jwxQfGXbc/6uluwt0/9ubVFHr0m8vHqmrkvq228+sUsPn1pdWpumxJXtU5/Lo9pkDdM/sQc1uZVpYUat/LNmj19dmqFNQgO48Y6BumzHAZz3apBZUaHFKnhal5GrzgVJJDeN2Xju5n+6aNbDN3ETbVnk8Vj97p+GG8P+9fLSumdyvWdtnFFXp4idWqFdEJy24e5pP/5Z+mpKru17boEn9I/XiLZPbxLm2LSAgREdCQAicAmPMm5IWW2tf8E73ttbmeF//RNJp1tqrj7cPAkK8ve6Afv7uFn0/qa/+fMXoNvkF6Ou9hfpiZ/6hrgkbf4mPj+zc6IJow8XRvpFhbXJsiFqXW8lpJVq6p0DLdxdqe06ZJKlHeIimD4rWzCExmjE4ul1+wck9WKMnv9qrN9YeUJ3bo2sm99UfLx3dJv8fjmSt1e68ioY7VFNytcP7/zIqrqvOG9Vbc0bGnvT4Om2FtVaZJdWHuvb8pmVfelHViTdWwxfwQ91pHq/7zdBAdQ4JVPfOIRoa20VDY7typ2crcLk92pFTruT0Yq1PL1FyeolyDtZIkjoHB2pc3+7eMY0iNb5fpLp1btqNFDtyyvSTtzZpZ265rp7UV/9z4Qj+/9qw8pp67fJ22/vNGJ67cstVXd8QHgcGGA2MCT/sxprhvSOO2VJ66e4C/XZhilILK3XuiF769YUj/Dawaq7t2WV6YMEWbck8qLOH99TvLx11WKusnbllWrAhS+9vzFJ+ea26dQ7WRWN76/IJ8Rrft3ub/OzlBI/H6tPteXpsyW7tzC3XwJhw/fCswbpwTJ/jBoX1bo+W7irQgo2ZWrI9X3Vujwb37KLLJ8Tr0vF9jtpCDtLe/HLd8Uqy9hdW6idnD1FZTb3eWHtAFbUuTR8UrXkzEzVjcLSjv59ZpdV6fsV+vbE2Q1V1bs0YHK3bZyQqJiL0hN17FlfV6WiXm8Iaj1F8KPjr9G0A2Kj1X3u8EL0ls1S3vrhedS635t+YpCktdJPPK6vT9ev3t+me2QP1/+YMa5F9thS3xyq3rEbpRZVKL6pSelGVMoobXmcUVR3W1asxUmzXTurXI0z9o8LVL+qbIDFcH23J1tPLUjUsNkL/vGa8Bneg7ofzy2v05493asHGLPXp1kn/c+EInTcq9oTHd3WdW8+tSNVTS1NVXe/WNZP76kdnDXF0TLjs0mqt3V+sKYlRiu3W/r43O6Xe7dHtL6/Xst0FevL6iZozMrZJ21XWunTFkyuVc7BGC+89XQlRvu9q/oNNWfrxW5t0xpAYzb8hqc31TuQEAkJ0JASEwEkyxoRJOiAp0Vp70DvvFUnj1NDFaJqkO74JDI+FgNC/LdtdoFteXKdpA6P0/M2TfHpn8Mmy1iqvrNZ7QbTsUGi4v7DyUMul8JBADY09orVhbITP7xq31mpfQYWW7S7Usj0FWpNarOp6t4IDjSYmRGrG4BjNHByjkX26tosgrSlyDlZr84GDmjOyZce58KW0wkotTmkICzdmlEqSBvXsorkjYzV3VKxG9unapn+2mnr3odDgUCCYW6bymoaLI8ZI/aPCNbx3hIbFdlVs104KC20I/zofNqbet+Ffezg3+Lvs0uqGsDCtWMkZJdqeXSaPbfj/HtIzQhP7R2piv0gl9Y9Uvx6Hj3vk9ljNX5aqv3+2S906h+gvV4zWWcN7OfjT4GS5PVbpRZXf3gjgfWR7A2RJiu4SomGx37bo7dsjTM8uT9XilDz1jwrTwxeP1KyhPR38Kdonl9ujF1em6W+f7lJQQIB+du4QuTxWCzZkaXtOmYICjGYP66krJsRp9rCeCg1qf8GDr3g8VotScvWPJXu0K69cg3p20Y/OGqwLRvc+9HnJWqutWQe1YEOWFm7OVnFlnaLCQ3TxuD66fHy8RsW17b/VbUVFrUs//89mfbw1V4EBRheM7q15MxPb3HizB6vq9eqadL3wddqhHjcaCw0KUEzEES37vM8xXUK+7fqzS6hftCI9UFylm19YqwPF1fq/K8foknFxp7S/NalFuu7ZNZo5JEbP3pjUrr63WGtVXFmn9OKGsLAhQKxUenHD6yN/n66f0k//c8GIdhkON8W6tGL95oMU7cgp0/RB0Xr44hFHbWnq9li9m5ypRz7bpbyyWp0zopd+MXdYu79p0t9V1bl07TNrtD2nTK/cOvmEvYRYa3XP6xu0aFuuXrp1smYMjvFRpd/1+poM/fK9rTp/dKwev3p8m+2lyFcICNGREBACDiMg9F/bs8t01dOrFB/ZWe/cOVUR7birTqnh7sbdeccPRRJ6hB0WGg6MCW/x4MNjrbZllWnZ7oaxBL+5KJsYHa4ZgxtaCZ6WGEWrnHYi92CNPt3eMPbF6tQieawU172z5o6K1XmjYjWhX6RjF0msbbhb+pvf928C87QjwvJhR4xVNrSX78Ny+F5lrUubDpQqOb1E69NLtDG95NAd9NFdQpWU0NDCcEhshB7/fI+S00t03qhY/fGy0eoR3vRx1NA+lFbVHR4a5pZpd16F6lwNXbJ1Dg7UvWcO0g9mDCC4OkUZRVX61ftbtXxPoSRpbHw3XT4hXheN7cOx1Uwej9XH23L0jyV7tCe/QkN6ddHdswYp+2C1FmzI0t78CoUEBuicEb10+YQ4zRwSww0tJ8Faqy925mtIr4g232q4pt6tL3bmS1KjIDBEXUKDCISPcLCqXvNeWa81+4v1/+YM1d2zBp7Uv1FWabUu/ucKdQsL1vv3nN6uh3c4mspalzKKG0LDyLAQv+hW3eX26PW1Gfrb4l2qqnPrtukDdN9Zg9UlNEjWWi3dXaA/f7JTO3PLNbZvd/3q/OGaPKCH02WjhZRU1ul7T61Ufnmt3r5j6nG7bv7XF3v0t09361fnD9ftMxN9WOXRPbs8VX/47w59b2K8/nrFmHZ1s0JLIyBER0JACDiMgNA/WWt1zqPLVFHj0nv3TOuwXS99063iztzDW1KkNbFbxVMR0SlI0wc1jCM4Y3B0m7/gghMrrqzTku0NY1+s2FOoOrdHMRGhOndEL80dFaspiVEtfmGy8Xh3RRV1h3URujO3/Xa3C99ze6z25JdrfVqJNnhDw2/GsIvoFKTfXzJKl4zrwwVWP+Jye5RaWKm9+RUa17e7+nTvmJ8FnGCt1arUIvWMCG3RMcD8ldtj9d+tOfrHkt3aV1ApqWHM4MsnxOuC0b3VLaxjBRZAS6l1ufXz/2zRB5uydc3kvvr9JaOa1eqmus6tK59eqfTCKr13z+m0HutgCitq9X+Ldumt9QfUq2uo7pk9SJ+m5GnF3kL16xGmn88detxxddF+ZZVW63tPrmzo6eCuaUe9VvH5jjz94OX1unRcnP5+1dg283vw2JLdemzJHt00NUEPXzyyzdTlawSE6EgICAGHERD6p1255Zrz2DL98bJRuu60BKfL8bnKWpd25pYrvejbllYtaUB0mMbGd/f7bi86svKaen2xM1+LU3L15c4CVde71a1zsM4a3lNzR8aqT/fOqqpzq7LOpeo6typrXUdMN4R+lXVuVde5Dk03hIEN61XVulXn9nznvTsFB2horyO60O0d0eHu6Ebryy+r0bbsgxrZp1u7HPsUgH9xe6xW7StS3x6dHRkDCWiPrLV65NPd+teXe3XGkBg9cd2EJvVkYq3Vj9/apIWbs/XsjUl0Pd6Bbcwo0W8+SNHWrIPqHhas+84crOun9KNHgQ5ud165rnxqlXqEh+idO6cqusu340ruza/QpU98rf7RYfrPndPaVJe71lr96eMdemb5fv358tG6enI/p0tyBAEhOhICQsBhBIT+6fHP9+jRJbu15pdnqWcEF4WBU1FT79ay3QVatC1XS3bkqczbre2xGKOGcf5Cvh3nLzz02+fOwY2mQwIbxgUMDVK3zsEaGhuh/lHhCqRVIAAAAJrozbUZ+tX72zS0V4Sev3mSYrsd/zvgM8tS9cePd+hn5w7RvWcO9lGVcIrbY5WcXqKhsRHq1pmbDv1Fcnqxrnt2jYb0itDrt09Rl9AgldXU69J/fa2D1fVaeN90xbXBHiastXp1TYaunBjfpsJLXyIgREdCQAg4jIDQP13w+HJ1Cg7Uu3dNc7oUoEOpc3m0Pq1Y5bUuhYcEecO9wEaBYJA6BQf4bVcoAAAAcMbS3QW6+9Vkde0crBdumaRhsUcfe2zZ7gLd/MJazR0VqyeuncDnVqAD+2Jnnm5/OVlTE6P07E1Juvu1DVq2u0Cv3z6FsSfbMAJCdCQEhIDDCAj9z4HiKs3465f65fnDNG/mQKfLAQAAAAD4QEr2Qd364jpV1rr15PUTNGNwzGHL04sqdfG/vlbvbp307l3TFN6E7kgBtG//Sc7Uz97ZrL49OutAcbV+f+ko3TDF/4aiaU8ICNGRMDgTAPjYp9vzJEnnjoh1uBIAAAAAgK+M7NNN7919uuIjO+uWF9bp7fUHDi2rqHXp9pfXyxhp/g1JhIOAn/jexHg9cN4wHSiu1jWT++r60/xzXD8AzuDTBgD42OKUXA3tFaH+0eFOlwIAAAAA8KE+3Tvr7Tun6p7XNujn/9mizJJq/fiswbr/7U3am1+hl289Tf2iwpwuE4AP3TEzUTMHx2hIry50KwzApwgIAcCHiipqtT6tWPfOHuR0KQAAAAAAB3TtFKznb56kXy7Yqsc/36NPU3K1M7dc/3PBcE0fHO10eQB8zBijEX2OPi4pALQmuhgF4IiaercWp+TK38ZBXbIjTx4rnTuS7kUBAAAAwF8FBwbor98bo/vPGaKdueW6fHycbps+wOmyAACAH6EFIQBHvL8xSw8s2Kp375qmiQmRTpfjM4tT8hTXvbNGcmcYAAAAAPg1Y4zuO2uwLhzbR/16hNG1IAAA8ClaEAJwxEVj+6hLaJBeW53udCk+U1Hr0oo9hZozMpYvfgAAAAAASdKA6HAFBvAdEQAA+BYBIQBHhIcG6bLxcfpoS46KK+ucLscnvtqVrzq3R3NG9nK6FAAAAAAAAACAHyMgBOCY66ckqM7t0TvrDzhdik8sTslTVHiIkvr3cLoUAAAAAAAAAIAfIyAE4JihsRGa3L+HXluTIY/HOl1Oq6p1ufXlznydPbwXXccAAAAAAAAAABxFQAjAUddPTVBGcZWW7SlwupRWtWpfkSpqXZoziu5FAQAAAAAAAADOIiAETpIxJs0Ys9UYs8kYs947r4cx5jNjzB7vc6TTdbZ1c0fGKrpLiF5dneF0Ka1qcUqewkMCNW1gtNOlAAAAAAAAAAD8HAEhcGpmW2vHWWuTvNMPSPrcWjtY0ufeaRxHSFCArkrqqy925imrtNrpclqF22P12fY8zRrWU52CA50uBwAAAAAAAADg5wgIgZZ1iaSXvK9fknSpc6W0H9ee1k9W0htrOmYrwo0ZJSqsqNW5I+heFAAAAAAAAADgPAJC4ORZSZ8aY5KNMfO883pZa3Mkyfvc07Hq2pH4yDCdObSn3lx3QHUuj9PltLjFKbkKDjSaPYxfBwAAAAAAAACA8wgIgZN3urV2gqTzJN1jjJnZ1A2NMfOMMeuNMesLCgpar8J25PopCSqsqNWn23OdLqVFWWu1OCVP0wZGq2unYKfLAQAAAAAAAACAgBA4WdbabO9zvqT3JE2WlGeM6S1J3uf8Y2w731qbZK1NiomJ8VXJbdrMITHq26OzXlmV7nQpLWpnbrkyiqs0Z2Ss06UAAAAAAAAAACCJgBA4KcaYcGNMxDevJZ0raZukhZJu8q52k6QPnKmw/QkMMLp2coLW7C/Wnrxyp8tpMYtTcmWMdA7jDwIAAAAAAAAA2ggCQuDk9JK0whizWdJaSf+11i6S9GdJ5xhj9kg6xzuNJroqKV4hgQF6bU2G06W0mMUpeZrYL1IxEaFOlwIAAAAAAAAAgCQpyOkCgPbIWpsqaexR5hdJOsv3FXUMUV1Cdf7oWL2bnKn/N2eowkPb9ynqQHGVduSU6VfnD3e6FAAAAAAAAAAADqEFIYA25fopCSqvdWnh5mynSzlli1NyJYnxBwEAAAAAAAAAbQoBIYA2ZWJCpIbFRuiVVemy1jpdzin5NCVPw2Ij1C8qzOlSAAAAAAAAAAA4hIAQQJtijNH1UxK0PadMGw+UOl3OSSusqNW69GJaDwIAAAAAAAAA2hwCQgBtzqXj4xQeEqhXV6c7XcpJW7I9T9bSvSgAAAAAAAAAoO0hIATQ5nQJDdJlE+L00ZYclVTWOV3OSVmckqu+PTpreO8Ip0sBAAAAAAAAAOAwBIQA2qTrpySozuXRO8kHnC6l2cpr6vX13iLNGRErY4zT5QAAAAAAAAAAcBgCQgBt0rDYrprUP1KvrcmQx2OdLqdZvtpVoDq3R+fSvSgAAAAAAAAAoA0iIATQZl0/JUHpRVVasbfQ6VKaZXFKrqLCQzQxIdLpUgAAAAAAAAAA+A4CQgBt1txRsYoKD9Erq9OdLqXJal1ufbWrQOeM6KXAALoXBQAAAAAAAAC0PQSEANqs0KBAXTWprz7fkafs0mqny2mSlXuLVFHr0hy6FwUAAAAAAAAAtFEEhADatGsn95OV9ObaDKdLaZLFKbnqEhqkaYOinC4FAAAAAAAAAICjIiAE0Kb17RGm2UN76o11B1Tv9jhdznG5PVafbc/TrKExCg0KdLocAAAAAAAAAACOioAQQJt3/ZR+Kiiv1acpeU6XclzJ6SUqqqyje1EAAAAAAAAAQJtGQAigzTtjSE/FR3bWK6vTnC7luBan5CokMECzhsY4XQoAAAAAAAAAAMdEQAigzQsMMLr2tH5anVqsvfnlTpdzVNZafbo9V6cPilJEp2CnywEAAAAAAAAA4JgICAG0C1cl9VVwoNGrqzOcLuWoduSU60BxNd2LAgAAAAAAAADaPAJC4CQYY/oaY740xuwwxqQYY37knf+wMSbLGLPJ+zjf6Vo7iuguoTp/dG+9m5ypqjqX0+V8x+KUXAUY6ewRvZwuBQAAAAAAAACA4yIgBE6OS9L91trhkqZIuscYM8K77FFr7Tjv42PnSux4rp+SoPJalxZuyna6lO9YnJKrpIQeiu4S6nQpAAAAAAAAAAAcFwEhcBKstTnW2g3e1+WSdkiKc7aqji8pIVJDe0XoldXpstY6Xc4hGUVV2plbrnNH0noQAAAAAAAAAND2ERACp8gY01/SeElrvLPuNcZsMcY8b4yJdK6yjscYo+unJiglu0ybDpQ6Xc4hi1NyJYnxBwEAAAAAAAAA7QIBIXAKjDFdJL0r6cfW2jJJT0oaKGmcpBxJjxxju3nGmPXGmPUFBQW+KrdDuGx8nMJDAvXq6gynSzlkcUquhvfuqr49wpwuBQAAAAAAAACAEyIgBE6SMSZYDeHga9baBZJkrc2z1rqttR5Jz0iafLRtrbXzrbVJ1tqkmJgY3xXdAXQJDdKl4+P00ZZslVTWOV2OCsprlZxRojl0LwoAAAAAAAAAaCcICIGTYIwxkp6TtMNa+/dG83s3Wu0ySdt8XZs/uH5KgmpdHv0nOdPpUvTZ9jxZS/eiAAAAAAAAAID2g4AQODmnS7pB0pnGmE3ex/mS/mqM2WqM2SJptqSfOFplBzW8d1clJUTqtTXp8niso7UsTslVvx5hGhYb4WgdAAAAAAAAAAA0VZDTBQDtkbV2hSRzlEUf+7oWf3X9lAT9+K1N+npfoWYMdqab1rKaeq3cV6ibp/VXQ6NSAAAAAAAAAADaPloQAmiXzhsdqx7hIXplVbpjNXy5M1/1bkv3ogAAAAAAAACAdoWAEEC7FBoUqKuS/n97dxurd3nXAfz7O6dPKy1pS5+wT4C2UEpYYQyqsKbbOlqVCG+MU0eWvSEmM07jQ6bJNBpJ9I1xLxbjxE0StpGFqUO3UWBuuogM6BzS0zIFLKVAz+FhPG6s0F6+OPdmw8jiOdzn3N7n//kkzbn/V0/u63fSfM/Tt/f135A7D43nyee/O5AZbj84npVLFubijcsHsj8AAAAAAEyHghAYWr982ca0JJ+557FZ3/uVV0/kqw9O5D3nr8nIiONFAQAAAAAYHgpCYGhtWLE4u7asys33HMmrJ07O6t53Pfx0Xj5+Inu2rZnVfQEAAAAA4M1SEAJD7X07NmXixe/ljoPjs7rvvgPjWbpwXn7qx1fO6r4AAAAAAPBmKQiBobbr3NVZt+wtuenuR2dtzxMnW+48NJ5d563Ognk+jQIAAAAAMFz8ZhsYaqMjlV+6bGPueviZPDTx0qzsed/hZ/PMy8cdLwoAAAAAwFBSEAJD7xfeviHzRyuf+vrsvIpw39h4Fswbya5zV8/KfgAAAAAA0E8KQmDorVyyMHsvODO37D+a7xx/bUb3aq1l39ixXPETK7Nk4bwZ3QsAAAAAAGaCghCYE67dsSkvvvJa/uH+J2Z0n7EnXsjjz33X8aIAAAAAAAwtBSEwJ7z9rOXZsmZJbrr7yIzuc/vYsYxUsnurghAAAAAAgOGkIATmhKrKtTs25YHHn8/9jz03Y/vsGxvPJWetyBlLFs7YHgAAAAAAMJMUhMCccc1F67J4wWh+5ab9uf4LB7P/0Wdz8mTr2/MffvrlfGv8xezZtrZvzwkAAAAAALNt3qAHAOiXpYvm5y+vfVs++a+Hc+Ndj+avvvbfWb10Ya7ctiZ7t52Zy85Zkfmj0/9/EfvGjiVJrjzf8aIAAAAAAAwvBSEwp7xj86q8Y/OqvPDKq/nKgxPZN3Ysn9v/eG66+0iWLZ6f3VvXZO+2tbli88osmj86pee+/eB4tv3Y6dmwYvEMTQ8AAAAAADNPQQjMSacvmp+rt6/L1dvX5ZVXT+Sf//Op7DtwLPvGjuWW/Udz2oLR7DpvdfZuW5t3nrc6Sxb+6E+HEy++km8c+XZ+Y/eWWfoIAAAAAABgZigIoc+qam+SjyYZTXJDa+1PBjxS5y2aP5o929Zmz7a1Of7aydz9yDP50oFjuePgsXzhP57Mgnkj2bl5ZfZsW5vdW9dk+WkLfug57jg4ntbi/oMAAAAAAAw9BSH0UVWNJvlYkvckOZrk3qq6tbV2cLCT8X0L5o1k55ZV2bllVf74mguy/9Fv57beKwvvPDSR0ZHKjnNWZG+vUFx9+qIkyb6x8Ww6Y3G2rFky4I8AAAAAAADeHAUh9NelSR5qrT2SJFV1c5KrkygI/x8aHalcevaKXHr2inzkqq154PHnc9uBY7ntwLF85PNj+f1bx3LxxuXZvXVN/u3hp/OBy89OVQ16bAAAAAAAeFMUhNBf65I8dsr10SSXDWgWpqCqcuH6Zblw/bL89p5z89DES/lSryz809seTOJ4UQAAAAAA5gYFIfTXG728rP3QO1Vdl+S6JNm4ceNMz8QUVVU2r1mazWuW5tfevTlHnvlODj/zct62afmgRwMAAAAAgDdtZNADwBxzNMmGU67XJ3ni9e/UWvt4a+2S1tolq1atmrXhmJ6NZyzOzi3+nQAAAAAAmBsUhNBf9ybZXFVnV9WCJO9NcuuAZwIAAAAAAPgBR4xCH7XWXquqX02yL8lokk+01sYGPBYAAAAAAMAPKAihz1prX0zyxUHPAQAAAAAA8EaqtTboGaDTquqpJI8Oeo4BWZnk6UEPAXOEPEF/yBL0jzxBf8gS9IcsQf90OU+bWmurBj0E9IOCEBiYqrqvtXbJoOeAuUCeoD9kCfpHnqA/ZAn6Q5agf+QJ5oaRQQ8AAAAAAAAAzB4FIQAAAAAAAHSIghAYpI8PegCYQ+QJ+kOWoH/kCfpDlqA/ZAn6R55gDnAPQgAAAAAAAOgQryAEAAAAAACADlEQAgNRVXur6ltV9VBVfXjQ88AwqapPVNVEVR04ZW1FVd1RVf/Ve7t8kDPCMKiqDVX1lao6VFVjVfWh3ro8wRRU1aKquqeq7u9l6Q9767IE01BVo1X171X1j71rWYJpqKrDVfVAVX2zqu7rrckTTFFVLauqW6rqwd7PTj8pSzA3KAiBWVdVo0k+luSnk5yf5Ber6vzBTgVD5W+S7H3d2oeTfLm1tjnJl3vXwI/2WpLfbK1tTbIjyQd7X4/kCabme0ne1Vp7a5LtSfZW1Y7IEkzXh5IcOuValmD63tla295au6R3LU8wdR9Ncltr7bwkb83k1yhZgjlAQQgMwqVJHmqtPdJaO57k5iRXD3gmGBqttX9J8uzrlq9OcmPv8Y1JrpnNmWAYtdaebK19o/f4xUz+oLsu8gRT0ia91Luc3/vTIkswZVW1PsnPJrnhlGVZgv6RJ5iCqjo9yc4kf50krbXjrbXnIkswJygIgUFYl+SxU66P9taA6VvTWnsymSw9kqwe8DwwVKrqrCQXJfl65AmmrHck4jeTTCS5o7UmSzA9f57kd5KcPGVNlmB6WpLbq2p/VV3XW5MnmJpzkjyV5JO9469vqKrTIkswJygIgUGoN1hrsz4FACSpqiVJPpfk11trLwx6HhhGrbUTrbXtSdYnubSqLhjwSDB0quqqJBOttf2DngXmiMtbaxdn8vYmH6yqnYMeCIbQvCQXJ/mL1tpFSV6O40RhzlAQAoNwNMmGU67XJ3liQLPAXDFeVWcmSe/txIDngaFQVfMzWQ5+qrX2t71leYJp6h059dVM3itXlmBqLk/yc1V1OJO3YXhXVd0UWYJpaa090Xs7keTvMnm7E3mCqTma5GjvdIgkuSWThaEswRygIAQG4d4km6vq7KpakOS9SW4d8Eww7G5N8v7e4/cn+fwAZ4GhUFWVyXtpHGqt/dkpfyVPMAVVtaqqlvUevyXJ7iQPRpZgSlprv9taW99aOyuTPyP9U2vtfZElmLKqOq2qln7/cZIrkxyIPMGUtNaOJXmsqs7tLb07ycHIEswJ1ZpT/YDZV1U/k8n7a4wm+URr7frBTgTDo6o+k2RXkpVJxpP8QZK/T/LZJBuTHEny8621Zwc0IgyFqroiydeSPJD/vdfT72XyPoTyBP9HVXVhkhsz+X3dSJLPttb+qKrOiCzBtFTVriS/1Vq7SpZg6qrqnEy+ajCZPCLx06216+UJpq6qtie5IcmCJI8k+UB63/NFlmCoKQgBAAAAAACgQxwxCgAAAAAAAB2iIAQAAAAAAIAOURACAAAAAABAhygIAQAAAAAAoEMUhAAAAAAAANAhCkIAAAAAAADoEAUhAAAAAAAAdIiCEAAAAAAAADpEQQgAAAAAAAAdoiAEAAAAAACADlEQAgAAAAAAQIcoCAEAAAAAAKBDFIQAAAAAAADQIQpCAAAAAAAA6BAFIQAAAAAAAHSIghAAAAAAAAA6REEIAAAAAAAAHaIgBAAAAAAAgA5REAIAAAAAAECHKAgBAAAAAACgQxSEAAAAAAAA0CEKQgAAAAAAAOgQBSEAAAAAAAB0iIIQAAAAAAAAOkRBCAAAAAAAAB2iIAQAAAAAAIAOURACAAAAAABAhygIAQAAAAAAoEMUhAAAAAAAANAhCkIAAAAAAADoEAUhAAAAAAAAdIiCEAAAAAAAADpEQQgAAAAAAAAdoiAEAAAAAACADlEQAgAAAAAAQIcoCAEAAAAAAKBDFIQAAAAAAADQIQpCAAAAAAAA6BAFIQAAAAAAAHSIghAAAAAAAAA6REEIAAAAAAAAHaIgBAAAAAAAgA5REAIAAAAAAECHKAgBAAAAAACgQxSEAAAAAAAA0CEKQgAAAAAAAOgQBSEAAAAAAAB0iIIQAAAAAAAAOkRBCAAAAAAAAB2iIAQAAAAAAIAOURACAAAAAABAhygIAQAAAAAAoEMUhAAAAAAAANAhCkIAAAAAAADoEAUhAAAAAAAAdIiCEAAAAAAAADpEQQgAAAAAAAAdoiAEAAAAAACADlEQAgAAAAAAQIcoCAEAAAAAAKBDFIQAAAAAAADQIQpCAAAAAAAA6BAFIQAAAAAAAHSIghAAAAAAAAA6REEIAAAAAAAAHaIgBAAAAAAAgA5REAIAAAAAAECHKAgBAAAAAACgQxSEAAAAAAAA0CEKQgAAAAAAAOgQBSEAAAAAAAB0iIIQAAAAAAAAOkRBCAAAAAAAAB2iIAQAAAAAAIAO+R9U1umWDuYYWwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ - "!bash run.sh" + "%pylab inline\n", + "from IPython.display import display, Image\n", + "display(Image(filename='output/RetailerFacility_112_2_productstore.png'))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "plt.imshow?" ] } ], @@ -302,7 +2203,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.7.10" + }, + "metadata": { + "interpreter": { + "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" + } } }, "nbformat": 4, diff --git a/examples/supply_chain/single_thread_launcher.py b/examples/supply_chain/single_thread_launcher.py index 8b4c4a818..6af820dfc 100644 --- a/examples/supply_chain/single_thread_launcher.py +++ b/examples/supply_chain/single_thread_launcher.py @@ -30,5 +30,5 @@ # learner.run() tracker = SimulationTracker(60, 1, env, learner) loc_path = './output/' - facility_types = ["product"] + facility_types = ["productstore"] tracker.run_and_render(loc_path, facility_types) From d86a564349e804198737ff71749f14a81b233197 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 24 May 2021 08:25:17 +0000 Subject: [PATCH 267/482] 1. added local learner; 2. added init docstring for special model classes; 3. updated notebook --- maro/rl/__init__.py | 6 +- maro/rl/algorithm/ac.py | 16 +- maro/rl/algorithm/ddpg.py | 16 +- maro/rl/algorithm/dqn.py | 16 +- maro/rl/algorithm/pg.py | 14 +- maro/rl/env_wrapper/env_wrapper.py | 17 +- maro/rl/model/core_model.py | 106 +++++++++- maro/rl/policy/policy.py | 4 +- maro/rl/training/__init__.py | 2 + maro/rl/training/actor.py | 150 +++++++------- maro/rl/training/local_learner.py | 194 ++++++++++++++++++ maro/rl/training/message_enums.py | 3 + maro/rl/training/rollout_manager.py | 19 +- .../rl_formulation.ipynb | 119 +---------- 14 files changed, 468 insertions(+), 214 deletions(-) create mode 100644 maro/rl/training/local_learner.py diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 25964f4f0..dbcda1e28 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -18,7 +18,7 @@ ) from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( - AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalPolicyManager, LocalRolloutManager, + AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalLearner, LocalPolicyManager, LocalRolloutManager, ParallelRolloutManager ) from maro.rl.utils import ( @@ -37,8 +37,8 @@ "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", - "AbsPolicyManager", "AbsRolloutManager", "Actor", "Learner", "LocalPolicyManager", "LocalRolloutManager", - "ParallelRolloutManager", + "AbsPolicyManager", "AbsRolloutManager", "Actor", "Learner", "LocalLearner", "LocalPolicyManager", + "LocalRolloutManager", "ParallelRolloutManager", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index d1e205fbc..bcf7f4373 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -55,17 +55,29 @@ class ActorCritic(AbsCorePolicy): https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: + name (str): Policy name. ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences for training. config: Configuration for the AC algorithm. + update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. + warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. + Defaults to 1. """ - def __init__(self, ac_net: DiscreteACNet, experience_memory: ExperienceMemory, config: ActorCriticConfig): + def __init__( + self, + name: str, + ac_net: DiscreteACNet, + experience_memory: ExperienceMemory, + config: ActorCriticConfig, + update_trigger: int = 1, + warmup: int = 1, + ): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") - super().__init__(experience_memory) + super().__init__(name, experience_memory, update_trigger=update_trigger, warmup=warmup) self.ac_net = ac_net self.config = config self.device = self.ac_net.device diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index ed3ecec3c..1057c58ee 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -60,16 +60,28 @@ class DDPG(AbsCorePolicy): https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg Args: + name (str): Policy name. ac_net (ContinuousACNet): DDPG policy and q-value models. experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences for training. config (DDPGConfig): Configuration for DDPG algorithm. + update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. + warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. + Defaults to 1. """ - def __init__(self, ac_net: ContinuousACNet, experience_memory: ExperienceMemory, config: DDPGConfig): + def __init__( + self, + name: str, + ac_net: ContinuousACNet, + experience_memory: ExperienceMemory, + config: DDPGConfig, + update_trigger: int = 1, + warmup: int = 1, + ): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") - super().__init__(experience_memory) + super().__init__(name, experience_memory, update_trigger=update_trigger, warmup=warmup) self.ac_net = ac_net if self.ac_net.trainable: self.target_ac_net = ac_net.copy() diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index a91ef54a2..07d2a717a 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -59,16 +59,28 @@ class DQN(AbsCorePolicy): See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: + name (str): Policy name. q_net (DiscreteQNet): Q-value model. experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences for training. config (DQNConfig): Configuration for DQN algorithm. + update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. + warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. + Defaults to 1. """ - def __init__(self, q_net: DiscreteQNet, experience_memory: ExperienceMemory, config: DQNConfig): + def __init__( + self, + name: str, + q_net: DiscreteQNet, + experience_memory: ExperienceMemory, + config: DQNConfig, + update_trigger: int = 1, + warmup: int = 1, + ): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(experience_memory) + super().__init__(name, experience_memory, update_trigger=update_trigger, warmup=warmup) self.q_net = q_net if self.q_net.trainable: self.target_q_net = q_net.copy() diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index fe6c9e274..24f409b2d 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -30,18 +30,28 @@ class PolicyGradient(AbsCorePolicy): Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. Args: + name (str): Policy name. policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences for training. config (PolicyGradientConfig): Configuration for the PG algorithm. + update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. + warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. + Defaults to 1. """ def __init__( - self, policy_net: DiscretePolicyNet, experience_memory: ExperienceMemory, config: PolicyGradientConfig, + self, + name: str, + policy_net: DiscretePolicyNet, + experience_memory: ExperienceMemory, + config: PolicyGradientConfig, + update_trigger: int = 1, + warmup: int = 1 ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__(experience_memory) + super().__init__(name, experience_memory, update_trigger=update_trigger, warmup=warmup) self.policy_net = policy_net self.config = config self.device = self.policy_net.device diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 9c2250ab2..036442105 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -75,6 +75,9 @@ def get_state(self, tick: int = None) -> dict: Args: tick (int): The tick for which to compute the environmental state. If computing the current state, use tick=self.env.tick. + + Returns: + A dictionary with (agent ID, state) as key-value pairs. """ raise NotImplementedError @@ -88,9 +91,13 @@ def get_reward(self, tick: int = None): """Evaluate the reward for an action. Args: - tick (int): If given, the reward for the action that occured at this tick will be evaluated (in the case - of delayed reward evaluation). Otherwise, the reward is evaluated for the latest action. - Defaults to None. + tick (int): Evaluate the reward for the action that occured at the given tick. The tick may be + None, in which case the reward is evaluated for the latest action (i.e., immediate reward). + Otherwise, it must be a key in the ``action_history`` attribute (i.e., there must be an action + at that tick). Defaults to None. + + Returns: + A dictionary with (agent ID, reward) as key-value pairs. """ raise NotImplementedError @@ -98,6 +105,10 @@ def get_transition_info(self): """Get additional info for a transition. The returned transition info will be stored in the experience manager alongside states, actions, rewards. + + Returns: + A dictionary with (agent ID, transition_info) as key-value pairs. + """ pass diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 923597ba2..6b90c759b 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -46,12 +46,15 @@ class AbsCoreModel(nn.Module): the component names and optimizers created for them. Note that it is possible to freeze certain components while optimizing others by providing a subset of the keys in ``component``. Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. """ def __init__( self, component: Union[nn.Module, Dict[str, nn.Module]], optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = "cpu" + device: str = None ): super().__init__() self.component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) @@ -73,7 +76,10 @@ def __init__( if optim_option.scheduler_cls: self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) - self.device = torch.device(device) + if device is None: + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + else: + self.device = torch.device(device) self.to(self.device) @property @@ -158,7 +164,29 @@ def copy(self, with_optimizer: bool = False, device: str = None): class DiscreteQNet(AbsCoreModel): - """Q-value model for finite and discrete action spaces.""" + """Q-value model for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + @abstractmethod def forward(self, states) -> torch.tensor: """Compute the Q-values for all actions as a tensor of shape (batch_size, action_space_size).""" @@ -182,7 +210,29 @@ def q_values(self, states, actions: torch.tensor): class DiscretePolicyNet(AbsCoreModel): - """Parameterized policy for finite and discrete action spaces.""" + """Parameterized policy for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + @abstractmethod def forward(self, states) -> torch.tensor: """Compute action probabilities corresponding to each state in ``states``. @@ -206,7 +256,29 @@ def get_action(self, states): class DiscreteACNet(AbsCoreModel): - """Model container for the actor-critic architecture for finite and discrete action spaces.""" + """Model container for the actor-critic architecture for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + @abstractmethod def forward(self, states, actor: bool = True, critic: bool = True) -> tuple: """Compute action probabilities and values for a state batch. @@ -234,7 +306,29 @@ def get_action(self, states): class ContinuousACNet(AbsCoreModel): - """Model container for the actor-critic architecture for continuous action spaces.""" + """Model container for the actor-critic architecture for continuous action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + @abstractmethod def forward(self, states, actions=None): """Compute actions for a batch of states or Q-values for a batch of states and actions. diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 5f0712c7f..b4d401160 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -36,6 +36,7 @@ class AbsCorePolicy(AbsPolicy): Reinforcement learning (RL) policies should inherit from this. Args: + name (str): Policy name. experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences for training. update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. @@ -44,11 +45,12 @@ class AbsCorePolicy(AbsPolicy): """ def __init__( self, + name: str, experience_memory: ExperienceMemory, update_trigger: int = 1, warmup: int = 1 ): - super().__init__() + super().__init__(name) self.experience_memory = experience_memory self.update_trigger = update_trigger self.warmup = warmup diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 1fbbee4ac..c8e3a193a 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -3,6 +3,7 @@ from .actor import Actor from .learner import Learner +from .local_learner import LocalLearner from .policy_manager import AbsPolicyManager, LocalPolicyManager from .rollout_manager import AbsRolloutManager, LocalRolloutManager, ParallelRolloutManager @@ -10,6 +11,7 @@ __all__ = [ "Actor", "Learner", + "LocalLearner", "AbsPolicyManager", "LocalPolicyManager", "AbsRolloutManager", "LocalRolloutManager", "ParallelRolloutManager" ] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 61161e28c..22809d0bb 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -6,6 +6,7 @@ from typing import List, Dict from maro.communication import Proxy +from maro.rl import policy from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsPolicy @@ -95,79 +96,86 @@ def run(self): break if msg.tag == MsgTag.COLLECT: - episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] - if self.env.state is None: - self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") - if hasattr(self, "exploration_dict"): - exploration_params = { - agent_ids: self.exploration_dict[exploration_id].parameters - for exploration_id, agent_ids in self.agent_groups_by_exploration.items() - } - self._logger.debug(f"Exploration parameters: {exploration_params}") - - self.env.reset() - self.env.start() # get initial state - - # load policies - self._load_policy_states(msg.body[MsgKey.POLICY]) - - starting_step_index = self.env.step_index + 1 - steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] - while self.env.state and steps_to_go > 0: - if self.exploration_dict: - action = { - id_: - self.exploration[id_](self.policy[id_].choose_action(st)) - if id_ in self.exploration else self.policy[id_].choose_action(st) - for id_, st in self.env.state.items() - } - else: - action = {id_: self.policy[id_].choose_action(st) for id_, st in self.env.state.items()} - - self.env.step(action) - steps_to_go -= 1 - - self._logger.info( - f"Roll-out finished for ep {episode_index}, segment {segment_index}" - f"(steps {starting_step_index} - {self.env.step_index})" - ) - exp_by_agent = self.env.get_experiences() - for agent_id, exp_set in exp_by_agent.items(): - self.policy[agent_id].experience_memory.put(exp_set) - - return_info = { - MsgKey.EPISODE_END: not self.env.state, - MsgKey.EPISODE_INDEX: episode_index, - MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.EXPERIENCES: exp_by_agent, - MsgKey.NUM_STEPS: self.env.step_index - starting_step_index + 1 - } - - if msg.body[MsgKey.RETURN_ENV_METRICS]: - return_info[MsgKey.METRICS] = self.env.metrics - if not self.env.state: - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() - - return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward - self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) + self._collect(msg) elif msg.tag == MsgTag.EVAL: - ep = msg.body[MsgKey.EPISODE_INDEX] - self._logger.info(f"Evaluation episode {ep}") - self.eval_env.reset() - self.eval_env.start() # get initial state - self._load_policy_states(msg.body[MsgKey.POLICY]) - while self.eval_env.state: - action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} - self.eval_env.step(action) - - return_info = { - MsgKey.METRICS: self.env.metrics, - MsgKey.TOTAL_REWARD: self.eval_env.total_reward, - MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] + self._evaluate(msg) + + def _collect(self, msg): + episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] + if self.env.state is None: + self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") + if hasattr(self, "exploration_dict"): + exploration_params = { + agent_ids: self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self.agent_groups_by_exploration.items() + } + self._logger.debug(f"Exploration parameters: {exploration_params}") + + self.env.reset() + self.env.start() # get initial state + + # load policies + self._load_policy_states(msg.body[MsgKey.POLICY]) + + starting_step_index = self.env.step_index + 1 + steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] + while self.env.state and steps_to_go > 0: + if self.exploration_dict: + action = { + id_: + self.exploration[id_](self.policy[id_].choose_action(st)) + if id_ in self.exploration else self.policy[id_].choose_action(st) + for id_, st in self.env.state.items() } - self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) + else: + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.env.state.items()} + + self.env.step(action) + steps_to_go -= 1 + + self._logger.info( + f"Roll-out finished for ep {episode_index}, segment {segment_index}" + f"(steps {starting_step_index} - {self.env.step_index})" + ) + exp_by_agent = self.env.get_experiences() + for agent_id, exp in exp_by_agent.items(): + self.policy[agent_id].experience_memory.put(exp) + + ret_exp = {id_: policy.experience_memory.get() for id_, policy in self.policy_dict.items()} + return_info = { + MsgKey.EPISODE_END: not self.env.state, + MsgKey.EPISODE_INDEX: episode_index, + MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.EXPERIENCES: ret_exp, + MsgKey.NUM_STEPS: self.env.step_index - starting_step_index + 1 + } + + if msg.body[MsgKey.RETURN_ENV_METRICS]: + return_info[MsgKey.METRICS] = self.env.metrics + if not self.env.state: + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + + return_info[MsgKey.TOTAL_REWARD] = self.env.total_reward + self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) + + def _evaluate(self, msg): + ep = msg.body[MsgKey.EPISODE_INDEX] + self._logger.info(f"Evaluation episode {ep}") + self.eval_env.reset() + self.eval_env.start() # get initial state + self._load_policy_states(msg.body[MsgKey.POLICY]) + while self.eval_env.state: + action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} + self.eval_env.step(action) + + return_info = { + MsgKey.METRICS: self.env.metrics, + MsgKey.TOTAL_REWARD: self.eval_env.total_reward, + MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] + } + self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) def _load_policy_states(self, policy_state_dict): for policy_id, policy_state in policy_state_dict.items(): diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py new file mode 100644 index 000000000..524214cee --- /dev/null +++ b/maro/rl/training/local_learner.py @@ -0,0 +1,194 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from collections import defaultdict +from os import getcwd +from typing import List, Dict, Union + +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.exploration import AbsExploration +from maro.rl.policy import AbsPolicy +from maro.utils import Logger + + +class LocalLearner: + """Controller for a single-threaded learning workflow. + + Args: + env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences + for policy training / update. + policies (List[AbsPolicy]): A set of named policies for inference. + agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's + queries to the correct policy. + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on how the implementation of the roll-out manager. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. Defaults to -1, in which case a complete + episode is rolled out in each call to ``collect``. + exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. + agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct + an agent's query to the correct exploration scheme. Defaults to None. + eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will + will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated + at the end of the training episodes given in the list. In any case, the policies will be evaluated + at the end of the last training episode. Defaults to None, in which case the policies will only be + evaluated after the last training episode. + eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used + as the evaluation environment. Defaults to None. + log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an + episode. Defaults to True. + log_total_reward (bool): If True, the total reward will be logged at the end of an episode. Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + + def __init__( + self, + env: AbsEnvWrapper, + policies: List[AbsPolicy], + agent2policy: Dict[str, str], + num_episodes: int, + num_steps: int = -1, + exploration_dict: Dict[str, AbsExploration] = None, + agent2exploration: Dict[str, str] = None, + eval_schedule: Union[int, List[int]] = None, + eval_env: AbsEnvWrapper = None, + log_env_metrics: bool = True, + log_total_reward: bool = True, + log_dir: str = getcwd(), + ): + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + self._logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) + self.env = env + self.eval_env = eval_env if eval_env else self.env + + # mappings between agents and policies + self.policy_dict = {policy.name: policy for policy in policies} + self._agent2policy = agent2policy + self._policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self._agent2policy.items()} + self._agent_groups_by_policy = defaultdict(list) + for agent_id, policy_id in agent2policy.items(): + self._agent_groups_by_policy[policy_id].append(agent_id) + + self.num_episodes = num_episodes + self._num_steps = num_steps if num_steps > 0 else float("inf") + + # mappings between exploration schemes and agents + self.exploration_dict = exploration_dict + if exploration_dict: + self._agent2exploration = agent2exploration + self._exploration = { + agent_id: self.exploration_dict[exploration_id] + for agent_id, exploration_id in self._agent2exploration.items() + } + self._agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in self._agent2exploration.items(): + self._agent_groups_by_exploration[exploration_id].append(agent_id) + + # evaluation schedule + if eval_schedule is None: + eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + + self._eval_schedule = eval_schedule + self._eval_schedule.sort() + if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: + self._eval_schedule.append(num_episodes) + self._eval_point_index = 0 + + self._log_env_metrics = log_env_metrics + self._log_total_reward = log_total_reward + self._eval_ep = 0 + + def run(self): + """Entry point for executing a learning workflow.""" + for ep in range(1, self.num_episodes + 1): + self._train(ep) + if ep == self._eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self._evaluate(self._eval_point_index) + + def _train(self, ep: int): + """Collect simulation data for training.""" + t0 = time.time() + learning_time = 0 + num_experiences_collected = 0 + + if self.exploration_dict: + exploration_params = { + tuple(agent_ids): self.exploration_dict[exploration_id].parameters + for exploration_id, agent_ids in self._agent_groups_by_exploration.items() + } + self._logger.debug(f"Exploration parameters: {exploration_params}") + + self.env.reset() + self.env.start() # get initial state + segment = 0 + while self.env.state: + segment += 1 + for agent_id, exp in self._collect(ep, segment).items(): + self._policy[agent_id].on_experiences(exp) + + # update the exploration parameters if an episode is finished + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + + # performance details + if self._log_env_metrics: + self._logger.info(f"ep {ep}: {self.env.metrics}") + if self._log_total_reward: + self._logger.info(f"ep {ep} total reward received: {self.env.total_reward}") + + self._logger.debug( + f"ep {ep} summary - " + f"running time: {time.time() - t0} " + f"env steps: {self.env.step_index} " + f"learning time: {learning_time} " + f"experiences collected: {num_experiences_collected}" + ) + + def _evaluate(self): + """Policy evaluation.""" + self._logger.info("Evaluating...") + self._eval_ep += 1 + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = {id_: self._policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} + self.eval_env.step(action) + + if self._log_env_metrics: + self._logger.info(f"eval ep {self._eval_ep}: {self.eval_env.metrics}") + + if not self.eval_env.state: + self._logger.info(f"total reward: {self.eval_env.total_reward}") + + def _collect(self, ep, segment): + start_step_index = self.env.step_index + 1 + steps_to_go = self._num_steps + while self.env.state and steps_to_go: + if self.exploration_dict: + action = { + id_: + self._exploration[id_](self._policy[id_].choose_action(st)) + if id_ in self._exploration else self._policy[id_].choose_action(st) + for id_, st in self.env.state.items() + } + else: + action = {id_: self._policy[id_].choose_action(st) for id_, st in self.env.state.items()} + self.env.step(action) + steps_to_go -= 1 + + self._logger.info( + f"Roll-out finished for ep {ep}, segment {segment}" + f"(steps {start_step_index} - {self.env.step_index})" + ) + + return self.env.get_experiences() diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index f49621ced..ea8dd19e9 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from enum import Enum diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index b29938906..a91e9f634 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -268,12 +268,7 @@ def __init__( self._num_eval_actors = num_eval_actors self._eval_ep = 0 - def collect( - self, - episode_index: int, - segment_index: int, - policy_state_dict: dict - ): + def collect(self, episode_index: int, segment_index: int, policy_state_dict: dict): """Collect simulation data, i.e., experiences for training.""" msg_body = { MsgKey.EPISODE_INDEX: episode_index, @@ -290,7 +285,7 @@ def collect( self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") # Receive roll-out results from remote actors - combined_exp_by_agent = defaultdict(ExperienceSet) + combined_exp_by_policy = defaultdict(ExperienceSet) num_segment_finishes = num_episode_finishes = 0 for msg in self._proxy.receive(): if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: @@ -306,8 +301,8 @@ def collect( self._logger.info(f"env_metrics: {env_metrics}") if segment_index - msg.body[MsgKey.SEGMENT_INDEX] <= self._max_staleness: - exp_by_agent = msg.body[MsgKey.EXPERIENCES] - self.total_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) + exp_by_policy = msg.body[MsgKey.EXPERIENCES] + self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) self.total_env_steps += msg.body[MsgKey.NUM_STEPS] is_episode_end = msg.body[MsgKey.EPISODE_END] if is_episode_end: @@ -316,15 +311,15 @@ def collect( if num_episode_finishes == self.required_finishes: self.episode_complete = True - for agent_id, exp in exp_by_agent.items(): - combined_exp_by_agent[agent_id].extend(exp) + for policy_id, exp in exp_by_policy.items(): + combined_exp_by_policy[policy_id].extend(exp) if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: num_segment_finishes += 1 if num_segment_finishes == self.required_finishes: break - return combined_exp_by_agent + return combined_exp_by_policy def evaluate(self, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``.""" diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index f97bbe5ce..01182883e 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -45,7 +45,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -147,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -217,12 +217,12 @@ " )\n", "\n", "\n", - "def get_ac_policy():\n", + "def get_ac_policy(name):\n", " actor = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"actor\"])\n", " critic = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"critic\"])\n", " ac_net = MyACNet({\"actor\": actor, \"critic\": critic}, optim_option=policy_config[\"model\"][\"optimization\"])\n", " experience_memory = ExperienceMemory(policy_config[\"experience_memory\"][\"capacity\"])\n", - " return ActorCritic(ac_net, experience_memory, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" + " return ActorCritic(name, ac_net, experience_memory, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" ] }, { @@ -238,119 +238,18 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "15:27:27 | LEARNER | INFO | Policy will be evaluated at the end of episodes [40]\n", - "15:27:27 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 1, segment 1\n", - "15:27:31 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 1, segment 1(steps 1 - 795)\n", - "15:27:31 | LOCAL_ROLLOUT_MANAGER | INFO | ep 1: {'order_requirements': 2240000, 'container_shortage': 1422736, 'operation_number': 4220466}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:27:32 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:27:32 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 2, segment 1\n", - "15:27:32 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:27:36 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 2, segment 1(steps 1 - 795)\n", - "15:27:36 | LOCAL_ROLLOUT_MANAGER | INFO | ep 2: {'order_requirements': 2240000, 'container_shortage': 1331225, 'operation_number': 3918538}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:27:38 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:27:38 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 3, segment 1\n", - "15:27:38 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:27:42 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 3, segment 1(steps 1 - 795)\n", - "15:27:42 | LOCAL_ROLLOUT_MANAGER | INFO | ep 3: {'order_requirements': 2240000, 'container_shortage': 988986, 'operation_number': 3207994}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:27:43 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:27:43 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 4, segment 1\n", - "15:27:43 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:27:47 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 4, segment 1(steps 1 - 795)\n", - "15:27:47 | LOCAL_ROLLOUT_MANAGER | INFO | ep 4: {'order_requirements': 2240000, 'container_shortage': 684603, 'operation_number': 3650267}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:27:48 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:27:48 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 5, segment 1\n", - "15:27:48 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:27:52 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 5, segment 1(steps 1 - 795)\n", - "15:27:52 | LOCAL_ROLLOUT_MANAGER | INFO | ep 5: {'order_requirements': 2240000, 'container_shortage': 596823, 'operation_number': 3578915}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:27:54 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:27:54 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 6, segment 1\n", - "15:27:54 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:27:58 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 6, segment 1(steps 1 - 795)\n", - "15:27:58 | LOCAL_ROLLOUT_MANAGER | INFO | ep 6: {'order_requirements': 2240000, 'container_shortage': 640579, 'operation_number': 3461426}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:27:59 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:27:59 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 7, segment 1\n", - "15:27:59 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:28:03 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 7, segment 1(steps 1 - 795)\n", - "15:28:03 | LOCAL_ROLLOUT_MANAGER | INFO | ep 7: {'order_requirements': 2240000, 'container_shortage': 543005, 'operation_number': 3567518}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:28:04 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:28:04 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 8, segment 1\n", - "15:28:04 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:28:08 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 8, segment 1(steps 1 - 795)\n", - "15:28:08 | LOCAL_ROLLOUT_MANAGER | INFO | ep 8: {'order_requirements': 2240000, 'container_shortage': 488595, 'operation_number': 3602507}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:28:09 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:28:09 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 9, segment 1\n", - "15:28:09 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:28:14 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 9, segment 1(steps 1 - 795)\n", - "15:28:14 | LOCAL_ROLLOUT_MANAGER | INFO | ep 9: {'order_requirements': 2240000, 'container_shortage': 387606, 'operation_number': 3836217}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:28:15 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:28:15 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 10, segment 1\n", - "15:28:15 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n", - "15:28:19 | LOCAL_ROLLOUT_MANAGER | INFO | Roll-out finished for ep 10, segment 1(steps 1 - 795)\n", - "15:28:19 | LOCAL_ROLLOUT_MANAGER | INFO | ep 10: {'order_requirements': 2240000, 'container_shortage': 341466, 'operation_number': 3862685}\n", - "Policy 0: exp mem size = 158, new exp = 158\n", - "Policy 1: exp mem size = 158, new exp = 158\n", - "Policy 2: exp mem size = 317, new exp = 317\n", - "Policy 3: exp mem size = 158, new exp = 158\n", - "15:28:20 | LOCAL_POLICY_MANAGER | INFO | Updated policies {0, 1, 2, 3}\n", - "15:28:20 | LOCAL_ROLLOUT_MANAGER | INFO | Collecting data from episode 11, segment 1\n", - "15:28:20 | LOCAL_ROLLOUT_MANAGER | INFO | updated policies [0, 1, 2, 3]\n" - ] - } - ], + "outputs": [], "source": [ "from maro.simulator import Env\n", - "from maro.rl import Learner, LocalRolloutManager, LocalPolicyManager\n", + "from maro.rl import LocalLearner\n", "from maro.utils import set_seeds\n", "\n", "set_seeds(1024) # for reproducibility\n", "env = Env(\"cim\", \"toy.4p_ssdd_l0.0\", durations=1120)\n", "env_wrapper = CIMEnvWrapper(env, **common_config)\n", - "policy_dict = {agent_id: get_ac_policy() for agent_id in env.agent_idx_list}\n", + "policies = [get_ac_policy(id_) for id_ in env.agent_idx_list]\n", "agent2policy = {agent_id: agent_id for agent_id in env.agent_idx_list}\n", - "rollout_manager = LocalRolloutManager(env_wrapper, policy_dict, agent2policy, log_total_reward=False)\n", - "policy_manager = LocalPolicyManager(policy_dict)\n", - "learner = Learner(policy_manager, rollout_manager, 40) # 40 episodes\n", + "learner = LocalLearner(env_wrapper, policies, agent2policy, 40) # 40 episodes\n", "learner.run()" ] } From b5423f287eec42dc401cd537e6cdb8dadc2846a5 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 24 May 2021 08:33:50 +0000 Subject: [PATCH 268/482] lint issue fix --- maro/rl/algorithm/rl_policy_index.py | 1 - maro/rl/env_wrapper/env_wrapper.py | 2 +- maro/rl/model/__init__.py | 4 +--- maro/rl/model/core_model.py | 6 +++--- maro/rl/policy/policy.py | 6 +++--- maro/rl/training/__init__.py | 1 - maro/rl/training/actor.py | 3 +-- maro/rl/training/local_learner.py | 5 ++--- maro/rl/training/policy_manager.py | 2 +- maro/rl/training/rollout_manager.py | 2 +- 10 files changed, 13 insertions(+), 19 deletions(-) diff --git a/maro/rl/algorithm/rl_policy_index.py b/maro/rl/algorithm/rl_policy_index.py index 7864d71c8..45351beb6 100644 --- a/maro/rl/algorithm/rl_policy_index.py +++ b/maro/rl/algorithm/rl_policy_index.py @@ -6,7 +6,6 @@ from .dqn import DQN, DiscreteQNet, DQNConfig from .pg import DiscretePolicyNet, PolicyGradient, PolicyGradientConfig - RL_POLICY_INDEX = { "ac": ActorCritic, "dqn": DQN, diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 036442105..89dac600f 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -105,7 +105,7 @@ def get_transition_info(self): """Get additional info for a transition. The returned transition info will be stored in the experience manager alongside states, actions, rewards. - + Returns: A dictionary with (agent ID, transition_info) as key-value pairs. diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 476f4c78e..8b1bb5af4 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -2,9 +2,7 @@ # Licensed under the MIT license. from .abs_block import AbsBlock -from .core_model import ( - AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, OptimOption -) +from .core_model import AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, OptimOption from .fc_block import FullyConnectedBlock __all__ = [ diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 6b90c759b..b594a7230 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -211,7 +211,7 @@ def q_values(self, states, actions: torch.tensor): class DiscretePolicyNet(AbsCoreModel): """Parameterized policy for finite and discrete action spaces. - + Args: component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. @@ -257,7 +257,7 @@ def get_action(self, states): class DiscreteACNet(AbsCoreModel): """Model container for the actor-critic architecture for finite and discrete action spaces. - + Args: component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. @@ -307,7 +307,7 @@ def get_action(self, states): class ContinuousACNet(AbsCoreModel): """Model container for the actor-critic architecture for continuous action spaces. - + Args: component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index b4d401160..5f7187250 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -22,9 +22,9 @@ def choose_action(self, state): class NullPolicy(AbsPolicy): - """Dummy policy that does nothing. - - Note that the meaning of a "None" action may depend on the scenario. + """Dummy policy that does nothing. + + Note that the meaning of a "None" action may depend on the scenario. """ def choose_action(self, state): return None diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index c8e3a193a..afe4d4545 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -7,7 +7,6 @@ from .policy_manager import AbsPolicyManager, LocalPolicyManager from .rollout_manager import AbsRolloutManager, LocalRolloutManager, ParallelRolloutManager - __all__ = [ "Actor", "Learner", diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 22809d0bb..91f8c9dba 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -3,10 +3,9 @@ from collections import defaultdict from os import getcwd -from typing import List, Dict +from typing import Dict, List from maro.communication import Proxy -from maro.rl import policy from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsPolicy diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py index 524214cee..23a66a6c9 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -4,7 +4,7 @@ import time from collections import defaultdict from os import getcwd -from typing import List, Dict, Union +from typing import Dict, List, Union from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.exploration import AbsExploration @@ -24,8 +24,7 @@ class LocalLearner: num_episodes (int): Number of training episodes. Each training episode may contain one or more collect-update cycles, depending on how the implementation of the roll-out manager. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. Defaults to -1, in which case a complete - episode is rolled out in each call to ``collect``. + case the roll-out will be executed until the end of the environment. exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct an agent's query to the correct exploration scheme. Defaults to None. diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index 258fa048f..fee13b0d7 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from os import getcwd -from typing import List, Dict +from typing import Dict, List from maro.rl.experience import ExperienceSet from maro.rl.policy import AbsPolicy diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index a91e9f634..f10ccbee7 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -6,7 +6,7 @@ from collections import defaultdict from os import getcwd from random import choices -from typing import List, Dict +from typing import Dict, List from maro.communication import Proxy, SessionType from maro.rl.env_wrapper import AbsEnvWrapper From 89438d379f33e5e931ec2b38126082ec76faaa64 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 24 May 2021 08:37:31 +0000 Subject: [PATCH 269/482] typo fix --- examples/cim/ac/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index 49c0a9ded..011071497 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -42,7 +42,7 @@ def get_ac_policy(): "critic": OptimOption(**cfg["optimization"]["critic"]) } ) - return ActorCritic(ac_net, , ActorCriticConfig(**cfg["algorithm"])) + return ActorCritic(ac_net, ActorCriticConfig(**cfg["algorithm"])) # Single-threaded launcher From c8f27783fcd9611b2b60885b5989b90204e1e428 Mon Sep 17 00:00:00 2001 From: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> Date: Tue, 25 May 2021 14:01:36 +0800 Subject: [PATCH 270/482] V0.2 rl refinement cim (#342) * add cim example local mode * update main to be compatible with the interface changes * add local learner mode to cim dqn example --- examples/cim/dqn/config.yml | 54 +++++----- examples/cim/dqn/main.py | 150 +++++++++++++++++----------- examples/cim/dqn/qnet.py | 14 +++ examples/cim/env_wrapper.py | 82 ++++++++------- maro/rl/env_wrapper/env_wrapper.py | 6 +- maro/rl/training/learner.py | 2 +- maro/rl/training/local_learner.py | 2 +- maro/rl/training/rollout_manager.py | 2 +- 8 files changed, 185 insertions(+), 127 deletions(-) create mode 100644 examples/cim/dqn/qnet.py diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index ec1d3bd43..5356f991b 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -1,6 +1,24 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +training: + mode: local # local, distributed + env: + scenario: cim + topology: toy.4p_ssdd_l0.0 + durations: 1120 + num_episodes: 5 + agent_update_interval: 200 + eval_schedule: 1 + exploration: + last_ep: 4 + initial_value: 0.8 + splits: + - + - 2 + - 0.3 + final_value: 0.0 + shaping: port_attributes: - empty @@ -26,7 +44,8 @@ shaping: fulfillment_factor: 1.0 shortage_factor: 1.0 time_decay: 0.97 -agent: + +policy: model: hidden_dims: - 256 @@ -42,37 +61,22 @@ agent: optim_cls: rmsprop optim_params: lr: 0.05 - algorithm: + algorithm: reward_discount: .0 - num_steps: 10 - batch_size: 128 - loss_cls: smooth_l1 target_update_freq: 5 + train_epochs: 10 + gradient_iters: 1 soft_update_coefficient: 0.1 double: false + loss_cls: smooth_l1 experience_memory: - experience_memory_size: -1 - experience_memory_overwrite_type: "rolling" - empty_experience_memory_after_step: false - new_experience_trigger: 16 - min_experiences_to_trigger_training: 1024 -training: - env: - scenario: cim - topology: toy.4p_ssdd_l0.0 - durations: 1120 - max_episode: 5 - agent_update_interval: 200 - exploration: - parameter_names: - - epsilon - split: 0.5 - start: 0.4 - mid: 0.32 - end: 0.0 + capacity: 10000 + # batch_size: 128 + overwrite_type: "rolling" + distributed: group: cim-dqn num_actors: 3 redis_host: localhost redis_port: 6379 - required_actor_finishes: 2 \ No newline at end of file + required_actor_finishes: 2 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 22937b618..9f12f2334 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -1,99 +1,127 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import argparse +import os import yaml -from multiprocessing import Process -from os import getenv -from os.path import dirname, join, realpath +# from multiprocessing import Process from maro.rl import ( - Actor, ActorManager, DQN, DQNConfig, DistLearner, FullyConnectedBlock, MultiAgentWrapper, OptimOption, - SimpleMultiHeadModel, TwoPhaseLinearParameterScheduler + DQN, DQNConfig, EpsilonGreedyExploration, ExperienceMemory, FullyConnectedBlock, + MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalPolicyManager, + LocalRolloutManager, UniformSampler, OptimOption ) from maro.simulator import Env -from maro.utils import set_seeds +# from maro.utils import set_seeds from examples.cim.env_wrapper import CIMEnvWrapper +from examples.cim.dqn.qnet import QNet -DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") -with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: +FILE_PATH = os.path.dirname(os.path.realpath(__file__)) +log_dir = os.path.join(FILE_PATH, "log") + +DEFAULT_CONFIG_PATH = os.path.join(FILE_PATH, "config.yml") +with open(os.getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: config = yaml.safe_load(config_file) # model input and output dimensions IN_DIM = ( - (config["shaping"]["look_back"] + 1) * - (config["shaping"]["max_ports_downstream"] + 1) * - len(config["shaping"]["port_attributes"]) + - len(config["shaping"]["vessel_attributes"]) + (config["shaping"]["look_back"] + 1) + * (config["shaping"]["max_ports_downstream"] + 1) + * len(config["shaping"]["port_attributes"]) + + len(config["shaping"]["vessel_attributes"]) ) OUT_DIM = config["shaping"]["num_actions"] -# for distributed / multi-process training -GROUP = getenv("GROUP", default=config["distributed"]["group"]) -REDIS_HOST = getenv("REDISHOST", default=config["distributed"]["redis_host"]) -REDIS_PORT = getenv("REDISPORT", default=config["distributed"]["redis_port"]) -NUM_ACTORS = int(getenv("NUMACTORS", default=config["distributed"]["num_actors"])) +# # for distributed / multi-process training +# GROUP = getenv("GROUP", default=config["distributed"]["group"]) +# REDIS_HOST = getenv("REDISHOST", default=config["distributed"]["redis_host"]) +# REDIS_PORT = getenv("REDISPORT", default=config["distributed"]["redis_port"]) +# NUM_ACTORS = int(getenv("NUMACTORS", default=config["distributed"]["num_actors"])) -def get_dqn_agent(): - cfg = config["agent"] - q_model = SimpleMultiHeadModel( +def get_independent_policy(policy_id): + cfg = config["policy"] + qnet = QNet( FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]), optim_option=OptimOption(**cfg["optimization"]) ) - return DQN(q_model, DQNConfig(**cfg["algorithm"]), **cfg["experience_memory"]) + return DQN( + name=policy_id, + q_net=qnet, + experience_memory=ExperienceMemory(**cfg["experience_memory"]), + config=DQNConfig(**cfg["algorithm"]) + ) -def cim_dqn_learner(): - agent = MultiAgentWrapper({name: get_dqn_agent() for name in Env(**config["training"]["env"]).agent_idx_list}) - scheduler = TwoPhaseLinearParameterScheduler(config["training"]["max_episode"], **config["training"]["exploration"]) - actor_manager = ActorManager( - NUM_ACTORS, GROUP, proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT), "log_enable": False} +def learner_with_both_local(): + env = Env(**config["training"]["env"]) + policy_list = [get_independent_policy(policy_id=i) for i in env.agent_idx_list] + agent2policy = {i: i for i in env.agent_idx_list} + + rollout_manager = LocalRolloutManager( + env=CIMEnvWrapper(env, **config["shaping"]), + policies=policy_list, + agent2policy=agent2policy, + exploration_dict=None, + agent2exploration=None, + num_steps=-1, + eval_env=CIMEnvWrapper(Env(**config["training"]["env"]), **config["shaping"]), + log_env_metrics=True, + log_total_reward=True, + log_dir=log_dir ) - learner = DistLearner( - agent, scheduler, actor_manager, - agent_update_interval=config["training"]["agent_update_interval"], - required_actor_finishes=config["distributed"]["required_actor_finishes"], - discard_stale_experiences=False + + policy_manager = LocalPolicyManager( + policies=policy_list, + log_dir=log_dir ) + + learner = Learner( + policy_manager=policy_manager, + rollout_manager=rollout_manager, + num_episodes=config["training"]["num_episodes"], + eval_schedule=[], + log_dir=log_dir + ) + learner.run() -def cim_dqn_actor(): +def local_learner_mode(): env = Env(**config["training"]["env"]) - agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) - actor = Actor( - CIMEnvWrapper(env, **config["shaping"]), agent, GROUP, - proxy_options={"redis_address": (REDIS_HOST, REDIS_PORT)} - ) - actor.run() - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "-w", "--whoami", type=int, choices=[0, 1, 2], default=0, - help="Identity of this process: 0 - multi-process mode, 1 - learner, 2 - actor" + exploration_config = config["training"]["exploration"] + exploration_config["splits"] = [(item[0], item[1]) for item in exploration_config["splits"]] + epsilon_greedy = EpsilonGreedyExploration(num_actions=config["shaping"]["num_actions"]) + epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **exploration_config ) - args = parser.parse_args() - if args.whoami == 0: - actor_processes = [Process(target=cim_dqn_actor) for i in range(NUM_ACTORS)] - learner_process = Process(target=cim_dqn_learner) - - for i, actor_process in enumerate(actor_processes): - set_seeds(i) # this is to ensure that the actors explore differently. - actor_process.start() + local_learner = LocalLearner( + env=CIMEnvWrapper(env, **config["shaping"]), + policies=[get_independent_policy(policy_id=i) for i in env.agent_idx_list], + agent2policy={i: i for i in env.agent_idx_list}, + num_episodes=config["training"]["num_episodes"], + num_steps=-1, + exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, + agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, + eval_schedule=config["training"]["eval_schedule"], + eval_env=CIMEnvWrapper(Env(**config["training"]["env"]), **config["shaping"]), + log_env_metrics=True, + log_total_reward=True, + log_dir=log_dir + ) - learner_process.start() + local_learner.run() - for actor_process in actor_processes: - actor_process.join() - learner_process.join() - elif args.whoami == 1: - cim_dqn_learner() - elif args.whoami == 2: - cim_dqn_actor() +if __name__ == "__main__": + if config["training"]["env"] == "local": + local_learner_mode() + elif config["training"]["env"] == "distributed": + print("Not implement yet.") + else: + print("Two modes are supported: local or distributed.") diff --git a/examples/cim/dqn/qnet.py b/examples/cim/dqn/qnet.py new file mode 100644 index 000000000..6c8c6d18a --- /dev/null +++ b/examples/cim/dqn/qnet.py @@ -0,0 +1,14 @@ +import numpy as np +import torch +import torch.nn as nn +from maro.rl import DiscreteQNet, OptimOption + +class QNet(DiscreteQNet): + def __init__(self, component: nn.Module, optim_option: OptimOption=None, device='cpu'): + super().__init__(component, optim_option=optim_option, device=device) + + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component(states) diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index ee825130b..e8bf1557c 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -11,7 +11,7 @@ class CIMEnvWrapper(AbsEnvWrapper): def __init__( self, env, *, port_attributes, vessel_attributes, num_actions, look_back, max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, - finite_vessel_space=True, has_early_discharge=True + finite_vessel_space=True, has_early_discharge=True ): super().__init__(env, reward_eval_delay=reward_eval_delay) self.port_attributes = port_attributes @@ -36,54 +36,64 @@ def get_state(self, tick=None): port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] self.state_info = { - "tick": tick, "action_scope": self.event.action_scope, "port_idx": port_idx, "vessel_idx": vessel_idx + port_idx: { + "tick": tick, + "action_scope": self.event.action_scope, + "port_idx": port_idx, + "vessel_idx": vessel_idx + } } state = np.concatenate((port_features, vessel_features)) self._last_action_tick = tick return {port_idx: state} - def to_env_action(self, action_by_agent): - vessel_snapshots = self.env.snapshot_list["vessels"] - action_info = list(action_by_agent.values())[0] - model_action = action_info[0] if isinstance(action_info, tuple) else action_info - tick, port, vessel = self.state_info["tick"], self.state_info["port_idx"], self.state_info["vessel_idx"] - zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. - vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") - early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 - percent = abs(self.action_space[model_action]) + def to_env_action(self, action_by_agent: dict): + env_action = {} + for agent_id, action_info in action_by_agent.items(): + state_info = self.state_info[agent_id] + tick, port, vessel, action_scope = ( + state_info["tick"], state_info["port_idx"], state_info["vessel_idx"], state_info["action_scope"] + ) + vessel_snapshots = self.env.snapshot_list["vessels"] + vessel_space = ( + vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") + ) + early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 - action_scope = self.state_info["action_scope"] - if model_action < zero_action_idx: - action_type = ActionType.LOAD - actual_action = min(round(percent * action_scope.load), vessel_space) - elif model_action > zero_action_idx: - action_type = ActionType.DISCHARGE - plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge - actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) - else: - actual_action, action_type = 0, None + model_action = action_info[0] if isinstance(action_info, tuple) else action_info + percent = abs(self.action_space[model_action]) + zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. + if model_action < zero_action_idx: + action_type = ActionType.LOAD + actual_action = min(round(percent * action_scope.load), vessel_space) + elif model_action > zero_action_idx: + action_type = ActionType.DISCHARGE + plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge + actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) + else: + actual_action, action_type = 0, None - return {port: Action(vessel, port, actual_action, action_type)} + env_action[agent_id] = Action( + vessel_idx=vessel, port_idx=port, quantity=actual_action, action_type=action_type + ) + return env_action def get_reward(self, tick=None): """Delayed reward evaluation.""" if tick is None: tick = self._last_action_tick - port_snapshots = self.env.snapshot_list["ports"] start_tick = tick + 1 ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) - future_fulfillment = port_snapshots[ticks::"fulfillment"] - future_shortage = port_snapshots[ticks::"shortage"] - decay_list = [ - self.time_decay ** i for i in range(self.reward_eval_delay) - for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay) - ] + ports = list(self.action_history[tick].keys()) - return { - self.action_history[tick]: - np.float32( - self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - - self.shortage_factor * np.dot(future_shortage, decay_list) - ) - } + port_snapshots = self.env.snapshot_list["ports"] + future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) + future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) + + decay_list = [self.time_decay ** i for i in range(self.reward_eval_delay)] + rewards = np.float32( + self.fulfillment_factor * np.dot(future_fulfillment.T, decay_list) + - self.shortage_factor * np.dot(future_shortage.T, decay_list) + ) + return {agent_id: reward for agent_id, reward in zip(ports, rewards)} diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 89dac600f..7f19222a2 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -119,11 +119,13 @@ def step(self, action_by_agent: dict): reward cannot be determined yet due to a non-zero ``reward_eval_delay``. """ self._step_index += 1 - env_action = self.to_env_action(action_by_agent) - for agent_id, action in action_by_agent.items(): + env_action_dict = self.to_env_action(action_by_agent) + for agent_id, action in env_action_dict.items(): self.action_history[self.env.tick][agent_id] = action transition_info = self.get_transition_info() self._pending_reward_cache.append((self._state, action_by_agent, transition_info, self.env.tick)) + + env_action = list(env_action_dict.values()) _, self._event, done = self.env.step(env_action) if not done: diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 4d2dbc2db..01c3cc0ef 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -69,7 +69,7 @@ def run(self): self._train(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 - self.rollout_manager.evaluate(self._eval_point_index) + self.rollout_manager.evaluate(self.policy_manager.get_state()) def _train(self, ep: int): num_experiences_collected = 0 diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py index 23a66a6c9..8ddc42918 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -111,7 +111,7 @@ def run(self): self._train(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 - self._evaluate(self._eval_point_index) + self._evaluate() def _train(self, ep: int): """Collect simulation data for training.""" diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index f10ccbee7..03abf26d3 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -93,13 +93,13 @@ def __init__( # mappings between exploration schemes and agents self.exploration_dict = exploration_dict + self._agent_groups_by_exploration = defaultdict(list) if exploration_dict: self._agent2exploration = agent2exploration self._exploration = { agent_id: self.exploration_dict[exploration_id] for agent_id, exploration_id in self._agent2exploration.items() } - self._agent_groups_by_exploration = defaultdict(list) for agent_id, exploration_id in self._agent2exploration.items(): self._agent_groups_by_exploration[exploration_id].append(agent_id) From 5d6f08fa3d13087f89fba2da0b15c39f308d40ba Mon Sep 17 00:00:00 2001 From: Jinyu Wang Date: Tue, 25 May 2021 18:50:16 +0800 Subject: [PATCH 271/482] fix vessel planning issue --- maro/simulator/scenarios/cim/business_engine.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/maro/simulator/scenarios/cim/business_engine.py b/maro/simulator/scenarios/cim/business_engine.py index 83bca08e8..f7ee16ff2 100644 --- a/maro/simulator/scenarios/cim/business_engine.py +++ b/maro/simulator/scenarios/cim/business_engine.py @@ -172,7 +172,9 @@ def step(self, tick: int): vessel.set_stop_list(past_stops, future_stops) # Update vessel plans. - for plan_port_idx, plan_tick in self._data_cntr.vessel_planned_stops[vessel_idx, vessel.route_idx, loc_idx]: + for plan_port_idx, plan_tick in self._data_cntr.vessel_planned_stops[ + vessel_idx, vessel.route_idx, vessel.last_loc_idx + ]: self._vessel_plans[vessel_idx, plan_port_idx] = plan_tick if loc_idx > 0 and stop.arrive_tick == tick: From ac993099ba3c57e661da6aade250732368dd8f6e Mon Sep 17 00:00:00 2001 From: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> Date: Wed, 26 May 2021 15:18:24 +0800 Subject: [PATCH 272/482] V0.2 rl refinement cim (#347) * add cim example local mode * update main to be compatible with the interface changes * add local learner mode to cim dqn example * add multi-process mode to cim dqn example and remove the one with both local --- examples/cim/dqn/config.yml | 19 +++--- examples/cim/dqn/main.py | 126 ++++++++++++++++++++++-------------- 2 files changed, 87 insertions(+), 58 deletions(-) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 5356f991b..f7f485f50 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -1,13 +1,21 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +experiment_name: test + training: - mode: local # local, distributed + mode: multi-process # local, multi-process + multi-process: + group: cim-dqn + num_actors: 3 + num_eval_actors: 3 + redis_host: localhost + redis_port: 6379 env: scenario: cim topology: toy.4p_ssdd_l0.0 durations: 1120 - num_episodes: 5 + num_episodes: 100 agent_update_interval: 200 eval_schedule: 1 exploration: @@ -73,10 +81,3 @@ policy: capacity: 10000 # batch_size: 128 overwrite_type: "rolling" - -distributed: - group: cim-dqn - num_actors: 3 - redis_host: localhost - redis_port: 6379 - required_actor_finishes: 2 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 9f12f2334..629103028 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -3,27 +3,28 @@ import os import yaml -# from multiprocessing import Process +from multiprocessing import Process from maro.rl import ( - DQN, DQNConfig, EpsilonGreedyExploration, ExperienceMemory, FullyConnectedBlock, + Actor, DQN, DQNConfig, EpsilonGreedyExploration, ExperienceMemory, FullyConnectedBlock, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalPolicyManager, - LocalRolloutManager, UniformSampler, OptimOption + LocalRolloutManager, OptimOption, ParallelRolloutManager, UniformSampler ) from maro.simulator import Env -# from maro.utils import set_seeds +from maro.utils import set_seeds from examples.cim.env_wrapper import CIMEnvWrapper from examples.cim.dqn.qnet import QNet FILE_PATH = os.path.dirname(os.path.realpath(__file__)) -log_dir = os.path.join(FILE_PATH, "log") DEFAULT_CONFIG_PATH = os.path.join(FILE_PATH, "config.yml") with open(os.getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: config = yaml.safe_load(config_file) +log_dir = os.path.join(FILE_PATH, "log", config["experiment_name"]) + # model input and output dimensions IN_DIM = ( (config["shaping"]["look_back"] + 1) @@ -33,12 +34,6 @@ ) OUT_DIM = config["shaping"]["num_actions"] -# # for distributed / multi-process training -# GROUP = getenv("GROUP", default=config["distributed"]["group"]) -# REDIS_HOST = getenv("REDISHOST", default=config["distributed"]["redis_host"]) -# REDIS_PORT = getenv("REDISPORT", default=config["distributed"]["redis_port"]) -# NUM_ACTORS = int(getenv("NUMACTORS", default=config["distributed"]["num_actors"])) - def get_independent_policy(policy_id): cfg = config["policy"] @@ -54,40 +49,6 @@ def get_independent_policy(policy_id): ) -def learner_with_both_local(): - env = Env(**config["training"]["env"]) - policy_list = [get_independent_policy(policy_id=i) for i in env.agent_idx_list] - agent2policy = {i: i for i in env.agent_idx_list} - - rollout_manager = LocalRolloutManager( - env=CIMEnvWrapper(env, **config["shaping"]), - policies=policy_list, - agent2policy=agent2policy, - exploration_dict=None, - agent2exploration=None, - num_steps=-1, - eval_env=CIMEnvWrapper(Env(**config["training"]["env"]), **config["shaping"]), - log_env_metrics=True, - log_total_reward=True, - log_dir=log_dir - ) - - policy_manager = LocalPolicyManager( - policies=policy_list, - log_dir=log_dir - ) - - learner = Learner( - policy_manager=policy_manager, - rollout_manager=rollout_manager, - num_episodes=config["training"]["num_episodes"], - eval_schedule=[], - log_dir=log_dir - ) - - learner.run() - - def local_learner_mode(): env = Env(**config["training"]["env"]) @@ -118,10 +79,77 @@ def local_learner_mode(): local_learner.run() +def get_dqn_actor_process(): + env = Env(**config["training"]["env"]) + policy_list = [get_independent_policy(policy_id=i) for i in env.agent_idx_list] + + actor = Actor( + env=CIMEnvWrapper(env, **config["shaping"]), + policies=policy_list, + agent2policy={i: i for i in env.agent_idx_list}, + group=config["training"]["multi-process"]["group"], + exploration_dict=None, + agent2exploration=None, + eval_env=CIMEnvWrapper(Env(**config["training"]["env"]), **config["shaping"]), + log_dir=log_dir, + redis_address=( + config["training"]["multi-process"]["redis_host"], + config["training"]["multi-process"]["redis_port"] + ) + ) + actor.run() + + +def get_dqn_learner_process(): + env = Env(**config["training"]["env"]) + policy_list = [get_independent_policy(policy_id=i) for i in env.agent_idx_list] + + policy_manager = LocalPolicyManager(policies=policy_list, log_dir=log_dir) + + rollout_manager = ParallelRolloutManager( + num_actors=config["training"]["multi-process"]["num_actors"], + group=config["training"]["multi-process"]["group"], + num_steps=-1, + required_finishes=None, + max_staleness=0, + num_eval_actors=config["training"]["multi-process"]["num_eval_actors"], + log_env_metrics=False, + log_dir=log_dir, + redis_address=( + config["training"]["multi-process"]["redis_host"], + config["training"]["multi-process"]["redis_port"] + ) + ) + + learner = Learner( + policy_manager=policy_manager, + rollout_manager=rollout_manager, + num_episodes=config["training"]["num_episodes"], + eval_schedule=[], + log_dir=log_dir + ) + learner.run() + + +def multi_process_mode(): + actor_processes = [Process(target=get_dqn_actor_process) for i in range(config["training"]["multi-process"]["num_actors"])] + for i, actor_process in enumerate(actor_processes): + set_seeds(i) + actor_process.start() + + learner_process = Process(target=get_dqn_learner_process) + learner_process.start() + + for actor_process in actor_processes: + actor_process.join() + learner_process.join() + + if __name__ == "__main__": - if config["training"]["env"] == "local": + if config["training"]["mode"] == "local": local_learner_mode() - elif config["training"]["env"] == "distributed": - print("Not implement yet.") + elif config["training"]["mode"] == "multi-process": + multi_process_mode() else: - print("Two modes are supported: local or distributed.") + print("Two modes are supported: local or multi-process.") + From af5d2bf2f240e1d98c6d801c70323a21d5f5673d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 27 May 2021 04:18:50 +0000 Subject: [PATCH 273/482] 1. added early stopper; 2. refined receive logic for parallel rollout manager --- examples/cim/dqn/config.yml | 115 +++++++------ examples/cim/dqn/main.py | 79 +++------ examples/cim/dqn/qnet.py | 3 +- maro/communication/driver/zmq_driver.py | 2 +- maro/rl/__init__.py | 8 +- maro/rl/env_wrapper/env_wrapper.py | 15 +- maro/rl/exploration/abs_exploration.py | 4 +- maro/rl/exploration/exploration_scheduler.py | 40 ++--- maro/rl/training/__init__.py | 2 + maro/rl/training/actor.py | 3 +- maro/rl/training/early_stopper.py | 17 ++ maro/rl/training/learner.py | 25 ++- maro/rl/training/local_learner.py | 42 ++--- maro/rl/training/message_enums.py | 4 +- maro/rl/training/policy_manager.py | 16 +- maro/rl/training/rollout_manager.py | 168 ++++++++++--------- 16 files changed, 268 insertions(+), 275 deletions(-) create mode 100644 maro/rl/training/early_stopper.py diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 5356f991b..adb987377 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -1,67 +1,64 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -training: - mode: local # local, distributed - env: +mode: local # local, distributed +num_episodes: 100 +num_steps: -1 +eval_schedule: 50 +env: + basic: scenario: cim topology: toy.4p_ssdd_l0.0 durations: 1120 - num_episodes: 5 - agent_update_interval: 200 - eval_schedule: 1 - exploration: - last_ep: 4 - initial_value: 0.8 - splits: - - - - 2 - - 0.3 - final_value: 0.0 - -shaping: - port_attributes: - - empty - - full - - on_shipper - - on_consignee - - booking - - shortage - - fulfillment - vessel_attributes: - - empty - - full - - remaining_space - num_actions: 21 - # Parameters for computing states - look_back: 7 - max_ports_downstream: 2 - # Parameters for computing actions - finite_vessel_space: true - has_early_discharge: true - # Parameters for computing rewards - reward_eval_delay: 99 - fulfillment_factor: 1.0 - shortage_factor: 1.0 - time_decay: 0.97 - + wrapper: + port_attributes: + - empty + - full + - on_shipper + - on_consignee + - booking + - shortage + - fulfillment + vessel_attributes: + - empty + - full + - remaining_space + num_actions: 21 + # Parameters for computing states + look_back: 7 + max_ports_downstream: 2 + # Parameters for computing actions + finite_vessel_space: true + has_early_discharge: true + # Parameters for computing rewards + reward_eval_delay: 99 + fulfillment_factor: 1.0 + shortage_factor: 1.0 + time_decay: 0.97 +exploration: + last_ep: 100 + initial_value: 0.4 + final_value: 0.0 + splits: + - [50, 0.32] policy: model: - hidden_dims: - - 256 - - 128 - - 64 - activation: leaky_relu - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop - optim_params: - lr: 0.05 - algorithm: + network: + hidden_dims: + - 256 + - 128 + - 64 + activation: leaky_relu + softmax: false + batch_norm: true + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: rmsprop + optim_params: + lr: 0.05 + algorithm_config: reward_discount: .0 target_update_freq: 5 train_epochs: 10 @@ -70,10 +67,10 @@ policy: double: false loss_cls: smooth_l1 experience_memory: - capacity: 10000 - # batch_size: 128 + capacity: 1000000 overwrite_type: "rolling" - + sampling: + batch_size: 128 distributed: group: cim-dqn num_actors: 3 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 9f12f2334..2f412d564 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -8,7 +8,7 @@ from maro.rl import ( DQN, DQNConfig, EpsilonGreedyExploration, ExperienceMemory, FullyConnectedBlock, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalPolicyManager, - LocalRolloutManager, UniformSampler, OptimOption + LocalRolloutManager, OptimOption ) from maro.simulator import Env # from maro.utils import set_seeds @@ -26,12 +26,12 @@ # model input and output dimensions IN_DIM = ( - (config["shaping"]["look_back"] + 1) - * (config["shaping"]["max_ports_downstream"] + 1) - * len(config["shaping"]["port_attributes"]) - + len(config["shaping"]["vessel_attributes"]) + (config["env"]["wrapper"]["look_back"] + 1) + * (config["env"]["wrapper"]["max_ports_downstream"] + 1) + * len(config["env"]["wrapper"]["port_attributes"]) + + len(config["env"]["wrapper"]["vessel_attributes"]) ) -OUT_DIM = config["shaping"]["num_actions"] +OUT_DIM = config["env"]["wrapper"]["num_actions"] # # for distributed / multi-process training # GROUP = getenv("GROUP", default=config["distributed"]["group"]) @@ -43,75 +43,36 @@ def get_independent_policy(policy_id): cfg = config["policy"] qnet = QNet( - FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]), - optim_option=OptimOption(**cfg["optimization"]) + FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["network"]), + optim_option=OptimOption(**cfg["model"]["optimization"]) ) return DQN( name=policy_id, q_net=qnet, experience_memory=ExperienceMemory(**cfg["experience_memory"]), - config=DQNConfig(**cfg["algorithm"]) + config=DQNConfig(**cfg["algorithm_config"]) ) -def learner_with_both_local(): - env = Env(**config["training"]["env"]) - policy_list = [get_independent_policy(policy_id=i) for i in env.agent_idx_list] - agent2policy = {i: i for i in env.agent_idx_list} - - rollout_manager = LocalRolloutManager( - env=CIMEnvWrapper(env, **config["shaping"]), - policies=policy_list, - agent2policy=agent2policy, - exploration_dict=None, - agent2exploration=None, - num_steps=-1, - eval_env=CIMEnvWrapper(Env(**config["training"]["env"]), **config["shaping"]), - log_env_metrics=True, - log_total_reward=True, - log_dir=log_dir - ) - - policy_manager = LocalPolicyManager( - policies=policy_list, - log_dir=log_dir - ) - - learner = Learner( - policy_manager=policy_manager, - rollout_manager=rollout_manager, - num_episodes=config["training"]["num_episodes"], - eval_schedule=[], - log_dir=log_dir - ) - - learner.run() - - def local_learner_mode(): - env = Env(**config["training"]["env"]) - - exploration_config = config["training"]["exploration"] - exploration_config["splits"] = [(item[0], item[1]) for item in exploration_config["splits"]] - epsilon_greedy = EpsilonGreedyExploration(num_actions=config["shaping"]["num_actions"]) + env = Env(**config["env"]["basic"]) + num_actions = config["env"]["wrapper"]["num_actions"] + epsilon_greedy = EpsilonGreedyExploration(num_actions=num_actions) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, param_name="epsilon", - **exploration_config + **config["exploration"] ) - local_learner = LocalLearner( - env=CIMEnvWrapper(env, **config["shaping"]), + env=CIMEnvWrapper(env, **config["env"]["wrapper"]), policies=[get_independent_policy(policy_id=i) for i in env.agent_idx_list], agent2policy={i: i for i in env.agent_idx_list}, - num_episodes=config["training"]["num_episodes"], - num_steps=-1, + num_episodes=config["num_episodes"], + num_steps=config["num_steps"], exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, - eval_schedule=config["training"]["eval_schedule"], - eval_env=CIMEnvWrapper(Env(**config["training"]["env"]), **config["shaping"]), - log_env_metrics=True, - log_total_reward=True, + eval_schedule=config["eval_schedule"], + log_env_summary=True, log_dir=log_dir ) @@ -119,9 +80,9 @@ def local_learner_mode(): if __name__ == "__main__": - if config["training"]["env"] == "local": + if config["mode"] == "local": local_learner_mode() - elif config["training"]["env"] == "distributed": + elif config["mode"] == "distributed": print("Not implement yet.") else: print("Two modes are supported: local or distributed.") diff --git a/examples/cim/dqn/qnet.py b/examples/cim/dqn/qnet.py index 6c8c6d18a..5eecf1f85 100644 --- a/examples/cim/dqn/qnet.py +++ b/examples/cim/dqn/qnet.py @@ -3,8 +3,9 @@ import torch.nn as nn from maro.rl import DiscreteQNet, OptimOption + class QNet(DiscreteQNet): - def __init__(self, component: nn.Module, optim_option: OptimOption=None, device='cpu'): + def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): super().__init__(component, optim_option=optim_option, device=device) def forward(self, states): diff --git a/maro/communication/driver/zmq_driver.py b/maro/communication/driver/zmq_driver.py index 1998fcce0..e51698422 100644 --- a/maro/communication/driver/zmq_driver.py +++ b/maro/communication/driver/zmq_driver.py @@ -184,7 +184,7 @@ def receive(self, is_continuous: bool = True, timeout: int = None): recv_message = pickle.loads(recv_message) self._logger.debug(f"Receive a message from {recv_message.source} through broadcast receiver.") else: - self._logger.debug(f"Cannot receive any message within {receive_timeout}.") + self._logger.debug(f"No message received within {receive_timeout}.") return yield recv_message diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index dbcda1e28..62891c73e 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -18,8 +18,8 @@ ) from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( - AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalLearner, LocalPolicyManager, LocalRolloutManager, - ParallelRolloutManager + AbsEarlyStopper, AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalLearner, LocalPolicyManager, + LocalRolloutManager, ParallelRolloutManager ) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, @@ -37,8 +37,8 @@ "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", - "AbsPolicyManager", "AbsRolloutManager", "Actor", "Learner", "LocalLearner", "LocalPolicyManager", - "LocalRolloutManager", "ParallelRolloutManager", + "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Actor", "Learner", "LocalLearner", + "LocalPolicyManager", "LocalRolloutManager", "ParallelRolloutManager", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/env_wrapper/env_wrapper.py index 7f19222a2..202e9e817 100644 --- a/maro/rl/env_wrapper/env_wrapper.py +++ b/maro/rl/env_wrapper/env_wrapper.py @@ -31,7 +31,7 @@ def __init__(self, env: Env, reward_eval_delay: int = 0, save_replay: bool = Tru self._replay_buffer = {agent_id: defaultdict(list) for agent_id in self.replay_agent_ids} self._pending_reward_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None - self._total_reward = 0 + self._total_reward = defaultdict(int) self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. self._state = None # the latest extracted state is kept here @@ -45,8 +45,8 @@ def agent_idx_list(self): return self.env.agent_idx_list @property - def metrics(self): - return self.env.metrics + def summary(self): + return self.env.metrics, self._total_reward @property def state(self): @@ -57,11 +57,6 @@ def state(self): def event(self): return self._event - @property - def total_reward(self): - """The total reward achieved so far.""" - return self._total_reward - def start(self): """Generate the initial environmental state at the beginning of a simulation episode.""" self._step_index = 0 @@ -147,7 +142,7 @@ def step(self, action_by_agent: dict): # assign rewards to the agents that took action at that tick if self.save_replay: for agent_id, st in state.items(): - self._total_reward += reward[agent_id] + self._total_reward[agent_id] += reward[agent_id] if agent_id in self._replay_buffer: buf = self._replay_buffer[agent_id] buf["states"].append(st) @@ -182,7 +177,7 @@ def get_experiences(self): def reset(self): self.env.reset() self.state_info = None - self._total_reward = 0 + self._total_reward.clear() self._state = None self._pending_reward_cache.clear() for replay in self._replay_buffer.values(): diff --git a/maro/rl/exploration/abs_exploration.py b/maro/rl/exploration/abs_exploration.py index 2195e9167..b66a596cd 100644 --- a/maro/rl/exploration/abs_exploration.py +++ b/maro/rl/exploration/abs_exploration.py @@ -5,9 +5,7 @@ class AbsExploration(ABC): - """Abstract exploration class for generating exploration rates. - - """ + """Abstract exploration class for generating exploration rates.""" def __init__(self): self.scheduler = {} diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/exploration_scheduler.py index af5d40029..940d759f3 100644 --- a/maro/rl/exploration/exploration_scheduler.py +++ b/maro/rl/exploration/exploration_scheduler.py @@ -31,15 +31,15 @@ def step(self): class LinearExplorationScheduler(AbsExplorationScheduler): - """Static exploration parameter generator based on a linear schedule. + """Linear exploration parameter schedule. Args: - max_iter (int): Maximum number of iterations. - parameter_names (List[str]): List of exploration parameter names. - start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. - These values must correspond to ``parameter_names``. - end (Union[float, list, tuple, np.ndarray]): Exploration parameter values rate for the last episode. - These values must correspond to ``parameter_names``. + exploration (AbsExploration): An exploration instance to which the scheduler is applied. + param_name (str): Name of the exploration parameter to which the scheduler is applied. + last_ep (int): Last episode. + final_value (float): The value of the exploration parameter corresponding to ``last_ep``. + initial_value (float): The initial value for the exploration parameter. If this is None, the + value as specified in the exploration instance will be used as the initial value. Defaults to None. """ def __init__( self, @@ -64,19 +64,19 @@ def step(self): class MultiPhaseLinearExplorationScheduler(AbsExplorationScheduler): - """Exploration parameter generator based on two linear schedules joined together. + """Exploration parameter schedule that consists of multiple linear phases. Args: - max_iter (int): Maximum number of iterations. - parameter_names (List[str]): List of exploration parameter names. - split (float): The point where the switch from the first linear schedule to the second occurs. - start (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the first episode. - These values must correspond to ``parameter_names``. - mid (Union[float, list, tuple, np.ndarray]): Exploration parameter values where the switch from the - first linear schedule to the second occurs. In other words, this is the exploration rate where the first - linear schedule ends and the second begins. These values must correspond to ``parameter_names``. - end (Union[float, list, tuple, np.ndarray]): Exploration parameter values for the last episode. - These values must correspond to ``parameter_names``. + exploration (AbsExploration): An exploration instance to which the scheduler is applied. + param_name (str): Name of the exploration parameter to which the scheduler is applied. + last_ep (int): Last episode. + splits (List[Tuple[int, float]]): List of points that separate adjacent linear phases. Each + point is a (episode, parameter_value) tuple that indicates the end of one linear phase and + the start of another. These points do not have to be given in any particular order. There + cannot be two points with the same first element (episode), or a ``ValueError`` will be raised. + final_value (float): The value of the exploration parameter corresponding to ``last_ep``. + initial_value (float): The initial value for the exploration parameter. If this is None, the + value as specified in the exploration instance will be used as the initial value. Defaults to None. Returns: An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. @@ -91,8 +91,8 @@ def __init__( initial_value: float = None ): # validate splits - splits.append((1, initial_value)) - splits.append((last_ep, final_value)) + splits.append([1, initial_value]) + splits.append([last_ep, final_value]) splits.sort() for (ep, _), (ep2, _) in zip(splits, splits[1:]): if ep == ep2: diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index afe4d4545..3b0b8d313 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from .actor import Actor +from .early_stopper import AbsEarlyStopper from .learner import Learner from .local_learner import LocalLearner from .policy_manager import AbsPolicyManager, LocalPolicyManager @@ -9,6 +10,7 @@ __all__ = [ "Actor", + "AbsEarlyStopper", "Learner", "LocalLearner", "AbsPolicyManager", "LocalPolicyManager", diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 91f8c9dba..4534ee805 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -146,11 +146,10 @@ def _collect(self, msg): MsgKey.EPISODE_INDEX: episode_index, MsgKey.SEGMENT_INDEX: segment_index, MsgKey.EXPERIENCES: ret_exp, + MsgKey.ENV_METRICS: self.env.metrics, MsgKey.NUM_STEPS: self.env.step_index - starting_step_index + 1 } - if msg.body[MsgKey.RETURN_ENV_METRICS]: - return_info[MsgKey.METRICS] = self.env.metrics if not self.env.state: if self.exploration_dict: for exploration in self.exploration_dict.values(): diff --git a/maro/rl/training/early_stopper.py b/maro/rl/training/early_stopper.py new file mode 100644 index 000000000..20cfb1561 --- /dev/null +++ b/maro/rl/training/early_stopper.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod + + +class AbsEarlyStopper(ABC): + def __init__(self): + super().__init__() + self.metric_history = [] + + def push(self, metric): + self.metric_history.append(metric) + + @abstractmethod + def stop(self) -> bool: + raise NotImplementedError diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 01c3cc0ef..7b596a28f 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -6,6 +6,7 @@ from maro.utils import Logger +from .early_stopper import AbsEarlyStopper from .policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager @@ -13,6 +14,10 @@ class Learner: """Main controller for learning. + This should be used in multi-process or distributed settings where either the policy manager or the roll-out + manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such + as duplicate experience storage. Use ``LocalLearner`` instead. + Args: policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data @@ -24,6 +29,8 @@ class Learner: at the end of the training episodes given in the list. In any case, the policies will be evaluated at the end of the last training episode. Defaults to None, in which case the policies will only be evaluated after the last training episode. + early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the + environment metrics are met following an evaluation episode. Default to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -35,6 +42,7 @@ def __init__( rollout_manager: AbsRolloutManager, num_episodes: int, eval_schedule: Union[int, List[int]] = None, + early_stopper: AbsEarlyStopper = None, log_dir: str = getcwd(), **end_of_episode_kwargs ): @@ -59,6 +67,8 @@ def __init__( self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") self._eval_point_index = 0 + self.early_stopper = early_stopper + self._end_of_episode_kwargs = end_of_episode_kwargs self._updated_policy_ids = self.policy_manager.names self._last_step_set = {} @@ -69,7 +79,14 @@ def run(self): self._train(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 - self.rollout_manager.evaluate(self.policy_manager.get_state()) + env_metrics_dict = self.rollout_manager.evaluate(self.policy_manager.get_state()) + # performance details + self._logger.info(f"ep {ep}: {env_metrics_dict}") + # early stopping check + if self.early_stopper: + self.early_stopper.push(self.eval_env.metrics) + if self.early_stopper.stop(): + return def _train(self, ep: int): num_experiences_collected = 0 @@ -79,9 +96,9 @@ def _train(self, ep: int): segment += 1 # experience collection policy_state_dict = self.policy_manager.get_state() - exp_by_agent = self.rollout_manager.collect(ep, segment, policy_state_dict) - self.policy_manager.on_experiences(exp_by_agent) - num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) + exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict) + self.policy_manager.on_experiences(exp_by_policy) + num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) # performance details self.logger.debug(f"ep {ep} summary - experiences collected: {num_experiences_collected}") diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py index 8ddc42918..4e36fd52a 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -11,13 +11,15 @@ from maro.rl.policy import AbsPolicy from maro.utils import Logger +from .early_stopper import AbsEarlyStopper + class LocalLearner: - """Controller for a single-threaded learning workflow. + """Controller for single-threaded learning workflows. Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences - for policy training / update. + env (AbsEnvWrapper): Environment wrapper instance to interact with a set of agents and collect experiences + for policy updates. policies (List[AbsPolicy]): A set of named policies for inference. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. @@ -35,9 +37,10 @@ class LocalLearner: evaluated after the last training episode. eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used as the evaluation environment. Defaults to None. - log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an - episode. Defaults to True. - log_total_reward (bool): If True, the total reward will be logged at the end of an episode. Defaults to True. + early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the + environment metrics are met following an evaluation episode. Default to None. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of + each episode. Defaults to True. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -54,8 +57,8 @@ def __init__( agent2exploration: Dict[str, str] = None, eval_schedule: Union[int, List[int]] = None, eval_env: AbsEnvWrapper = None, - log_env_metrics: bool = True, - log_total_reward: bool = True, + early_stopper: AbsEarlyStopper = None, + log_env_summary: bool = True, log_dir: str = getcwd(), ): if num_steps == 0 or num_steps < -1: @@ -101,8 +104,9 @@ def __init__( self._eval_schedule.append(num_episodes) self._eval_point_index = 0 - self._log_env_metrics = log_env_metrics - self._log_total_reward = log_total_reward + self.early_stopper = early_stopper + + self._log_env_summary = log_env_summary self._eval_ep = 0 def run(self): @@ -112,6 +116,11 @@ def run(self): if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 self._evaluate() + # early stopping check + if self.early_stopper: + self.early_stopper.push(self.eval_env.summary) + if self.early_stopper.stop(): + return def _train(self, ep: int): """Collect simulation data for training.""" @@ -140,10 +149,8 @@ def _train(self, ep: int): exploration.step() # performance details - if self._log_env_metrics: - self._logger.info(f"ep {ep}: {self.env.metrics}") - if self._log_total_reward: - self._logger.info(f"ep {ep} total reward received: {self.env.total_reward}") + if self._log_env_summary: + self._logger.info(f"ep {ep}: {self.env.summary}") self._logger.debug( f"ep {ep} summary - " @@ -163,11 +170,8 @@ def _evaluate(self): action = {id_: self._policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} self.eval_env.step(action) - if self._log_env_metrics: - self._logger.info(f"eval ep {self._eval_ep}: {self.eval_env.metrics}") - - if not self.eval_env.state: - self._logger.info(f"total reward: {self.eval_env.total_reward}") + # performance details + self._logger.info(f"evaluation ep {self._eval_ep}: {self.eval_env.summary}") def _collect(self, ep, segment): start_step_index = self.env.step_index + 1 diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index ea8dd19e9..00d345ccb 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -24,13 +24,11 @@ class MsgKey(Enum): EPISODE_INDEX = "episode_index" SEGMENT_INDEX = "segment_index" TIME_STEP = "time_step" - METRICS = "metrics" + ENV_SUMMARY = "env_summary" EXPERIENCES = "experiences" NUM_EXPERIENCES = "num_experiences" STATE = "state" POLICY = "policy" VERSION = "version" NUM_STEPS = "num_steps" - RETURN_ENV_METRICS = "return_env_metrics" - TOTAL_REWARD = "total_reward" EPISODE_END = "episode_end" diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index fee13b0d7..a3fb974e9 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -51,7 +51,7 @@ def __init__(self, policies: List[AbsPolicy], log_dir: str = getcwd()): self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) self.policy_dict = {policy.name: policy for policy in policies} self._new_exp_counter = defaultdict(int) - self._updated_policy_ids = set() + self._updated_policy_names = set() @property def names(self): @@ -63,17 +63,17 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding policy's experience manager. Policies whose update conditions have been met will then be updated. """ - for policy_id, exp in exp_by_policy.items(): - if self.policy_dict[policy_id].on_experiences(exp): - self._updated_policy_ids.add(policy_id) + for policy_name, exp in exp_by_policy.items(): + if self.policy_dict[policy_name].on_experiences(exp): + self._updated_policy_names.add(policy_name) - if self._updated_policy_ids: - self._logger.info(f"Updated policies {self._updated_policy_ids}") + if self._updated_policy_names: + self._logger.info(f"Updated policies {self._updated_policy_names}") def get_state(self): """Return the states of updated policies since the last call.""" policy_state_dict = { - policy_id: self.policy_dict[policy_id].get_state() for policy_id in self._updated_policy_ids + policy_name: self.policy_dict[policy_name].get_state() for policy_name in self._updated_policy_names } - self._updated_policy_ids.clear() + self._updated_policy_names.clear() return policy_state_dict diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 03abf26d3..add603563 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -26,12 +26,28 @@ def __init__(self): @abstractmethod def collect(self, ep: int, segment: int, policy_state_dict: dict): - """Collect simulation data, i.e., experiences for training.""" + """Collect simulation data, i.e., experiences for training. + + Args: + ep (int): Current episode index. + segment (int): Current segment index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Experiences for policy training. + """ raise NotImplementedError @abstractmethod def evaluate(self, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``.""" + """Evaluate the performance of ``policy_state_dict``. + + Args: + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Environment summary. + """ raise NotImplementedError def reset(self): @@ -54,12 +70,11 @@ class LocalRolloutManager(AbsRolloutManager): case the roll-out will be executed until the end of the environment. eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used as the evaluation environment. Defaults to None. - log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an - episode. Defaults to True. - log_total_reward (bool): If True, the total reward will be logged at the end of an episode. Defaults to True. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current working - directory. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of + each episode. Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. """ def __init__( @@ -71,8 +86,7 @@ def __init__( agent2exploration: Dict[str, str] = None, num_steps: int = -1, eval_env: AbsEnvWrapper = None, - log_env_metrics: bool = True, - log_total_reward: bool = True, + log_env_summary: bool = True, log_dir: str = getcwd(), ): if num_steps == 0 or num_steps < -1: @@ -86,10 +100,12 @@ def __init__( # mappings between agents and policies self.policy_dict = {policy.name: policy for policy in policies} self._agent2policy = agent2policy - self._policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self._agent2policy.items()} + self._policy = { + agent_id: self.policy_dict[policy_name] for agent_id, policy_name in self._agent2policy.items() + } self._agent_groups_by_policy = defaultdict(list) - for agent_id, policy_id in agent2policy.items(): - self._agent_groups_by_policy[policy_id].append(agent_id) + for agent_id, policy_name in agent2policy.items(): + self._agent_groups_by_policy[policy_name].append(agent_id) # mappings between exploration schemes and agents self.exploration_dict = exploration_dict @@ -104,8 +120,7 @@ def __init__( self._agent_groups_by_exploration[exploration_id].append(agent_id) self._num_steps = num_steps if num_steps > 0 else float("inf") - self._log_env_metrics = log_env_metrics - self._log_total_reward = log_total_reward + self._log_env_summary = log_env_summary self._eval_ep = 0 def collect(self, ep: int, segment: int, policy_state_dict: dict): @@ -164,12 +179,9 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): for exploration in self.exploration_dict.values(): exploration.step() - # performance details - if not self.env.state: - if self._log_env_metrics: - self._logger.info(f"ep {ep}: {self.env.metrics}") - if self._log_total_reward: - self._logger.info(f"ep {ep} total reward received: {self.env.total_reward}") + # performance details + if self._log_env_summary: + self._logger.info(f"ep {ep}: {self.env.summary}") self._logger.debug( f"ep {ep} summary - " @@ -179,7 +191,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): f"experiences collected: {num_experiences_collected}" ) - return self.env.get_experiences() + return self.env.get_experiences(), self.env.summary def evaluate(self, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``.""" @@ -192,15 +204,14 @@ def evaluate(self, policy_state_dict: dict): action = {id_: self._policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} self.eval_env.step(action) - if not self.eval_env.state: - self._logger.info(f"total reward: {self.eval_env.total_reward}") + if self._log_env_summary: + self._logger.info(f"eval ep {self._eval_ep}: {self.eval_env.summary}") - if self._log_env_metrics: - self._logger.info(f"eval ep {self._eval_ep}: {self.eval_env.metrics}") + return self.eval_env.summary def _load_policy_states(self, policy_state_dict: dict): - for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].set_state(policy_state) + for policy_name, policy_state in policy_state_dict.items(): + self.policy_dict[policy_name].set_state(policy_state) if policy_state_dict: self._logger.info(f"updated policies {list(policy_state_dict.keys())}") @@ -215,17 +226,20 @@ class ParallelRolloutManager(AbsRolloutManager): assigned to the learner (and decision clients, if any). num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - required_finishes (int): Number of actor completions required to return from ``collect``. Defaults to None, - in which case the number is set to ``num_actors``. + max_receive_attempts (int): Maximum number of attempts to receive actor results in ``collect``. Defaults to + None, in which case the number is set to ``num_actors``. + receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the actors. This + This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the + desired amount of data from actors. Defaults to 0. max_staleness (int): Maximum allowable staleness measured in the number of calls to ``collect``. Experiences collected from calls to ``collect`` within ``max_staleness`` calls ago will be returned to the learner. Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. num_eval_actors (int): Number of actors required for evaluation. Defaults to 1. - log_env_metrics (bool): If True, the metrics provided by the environment will be logged at the end of an - episode. Defaults to True. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current working - directory. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of + each episode. Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. """ @@ -234,16 +248,15 @@ def __init__( num_actors: int, group: str, num_steps: int = -1, - required_finishes: int = None, + max_receive_attempts: int = None, + receive_timeout: int = 0, max_staleness: int = 0, num_eval_actors: int = 1, - log_env_metrics: bool = False, + log_env_summary: bool = True, log_dir: str = getcwd(), **proxy_kwargs ): super().__init__() - if required_finishes and required_finishes > num_actors: - raise ValueError("required_finishes cannot exceed the number of available actors") if num_eval_actors > num_actors: raise ValueError("num_eval_actors cannot exceed the number of available actors") @@ -253,84 +266,75 @@ def __init__( self._proxy = Proxy(group, "actor_manager", peers, **proxy_kwargs) self._actors = self._proxy.peers["actor"] # remote actor ID's - if required_finishes is None: - required_finishes = self.num_actors - self._logger.info(f"Required number of actor finishes is set to {required_finishes}") + if max_receive_attempts is None: + max_receive_attempts = self.num_actors + self._logger.info(f"Maximum receive attempts is set to {max_receive_attempts}") + + self.max_receive_attempts = max_receive_attempts + self.receive_timeout = receive_timeout - self.required_finishes = required_finishes self._num_steps = num_steps self._max_staleness = max_staleness self.total_experiences_collected = 0 self.total_env_steps = 0 - self.total_reward = defaultdict(float) - self._log_env_metrics = log_env_metrics + self._log_env_summary = log_env_summary self._num_eval_actors = num_eval_actors self._eval_ep = 0 def collect(self, episode_index: int, segment_index: int, policy_state_dict: dict): """Collect simulation data, i.e., experiences for training.""" + if self._log_env_summary: + self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") + msg_body = { MsgKey.EPISODE_INDEX: episode_index, MsgKey.SEGMENT_INDEX: segment_index, MsgKey.NUM_STEPS: self._num_steps, - MsgKey.POLICY: policy_state_dict, - MsgKey.RETURN_ENV_METRICS: self._log_env_metrics + MsgKey.POLICY: policy_state_dict } - - if self._log_env_metrics: - self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") - self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") # Receive roll-out results from remote actors combined_exp_by_policy = defaultdict(ExperienceSet) - num_segment_finishes = num_episode_finishes = 0 - for msg in self._proxy.receive(): + num_finishes = 0 + for _ in range(self.max_receive_attempts): + msg = self._proxy.receive(is_continuous=False, timeout=self.receive_timeout) if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: self._logger.info( - f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.EPISODE_INDEX]} " - f"(expected message type {MsgTag.COLLECT} and roll-out index {episode_index})" + f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE_INDEX]} " + f"(expected message type {MsgTag.COLLECT} and episode index {episode_index})" ) continue - # log roll-out summary - if self._log_env_metrics: - env_metrics = msg.body[MsgKey.METRICS] - self._logger.info(f"env_metrics: {env_metrics}") - if segment_index - msg.body[MsgKey.SEGMENT_INDEX] <= self._max_staleness: exp_by_policy = msg.body[MsgKey.EXPERIENCES] self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) self.total_env_steps += msg.body[MsgKey.NUM_STEPS] - is_episode_end = msg.body[MsgKey.EPISODE_END] - if is_episode_end: - self._logger.info(f"total rewards: {msg.body[MsgKey.TOTAL_REWARD]}") - num_episode_finishes += is_episode_end - if num_episode_finishes == self.required_finishes: - self.episode_complete = True - - for policy_id, exp in exp_by_policy.items(): - combined_exp_by_policy[policy_id].extend(exp) - - if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: - num_segment_finishes += 1 - if num_segment_finishes == self.required_finishes: - break + + for policy_name, exp in exp_by_policy.items(): + combined_exp_by_policy[policy_name].extend(exp) + + if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: + self.episode_complete = msg.body[MsgKey.EPISODE_END] + if self.episode_complete: + # log roll-out summary + if self._log_env_summary: + self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") + num_finishes += 1 + if num_finishes == self.num_actors: + break return combined_exp_by_policy def evaluate(self, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``.""" self._eval_ep += 1 - msg_body = { - MsgKey.EPISODE_INDEX: self._eval_ep, - MsgKey.POLICY: policy_state_dict, - MsgKey.RETURN_ENV_METRICS: True - } + msg_body = {MsgKey.EPISODE_INDEX: self._eval_ep, MsgKey.POLICY: policy_state_dict} actors = choices(self._actors, k=self._num_eval_actors) + env_summary_dict = {} self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(actor_id, msg_body) for actor_id in actors]) self._logger.info(f"Sent evaluation requests to {actors}") @@ -344,15 +348,15 @@ def evaluate(self, policy_state_dict: dict): ) continue - # log roll-out summary - env_metrics = msg.body[MsgKey.METRICS] - self._logger.info(f"env metrics for evaluation episode {self._eval_ep}: {env_metrics}") + env_summary_dict[msg.source] = msg.body[MsgKey.METRICS] if msg.body[MsgKey.EPISODE_INDEX] == self._eval_ep: num_finishes += 1 if num_finishes == self._num_eval_actors: break + return env_summary_dict + def exit(self): """Tell the remote actors to exit.""" self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) From cf764da78c4006f7d503b9cca4e10544aee246fc Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 27 May 2021 08:55:11 +0000 Subject: [PATCH 274/482] 1. changed default experience manager get() to uniform sampling; 2. added sampler_cls and sampler_params to experience manager init --- docs/source/apidoc/maro.rl.rst | 2 +- docs/source/key_components/rl_toolkit.rst | 6 +-- examples/cim/ac/config.yml | 2 +- examples/cim/dqn/config.yml | 3 +- examples/cim/dqn/main.py | 4 +- examples/proxy/broadcast.py | 12 ++--- examples/proxy/scatter.py | 20 ++++----- examples/proxy/send.py | 10 ++--- maro/communication/proxy.py | 2 +- maro/rl/__init__.py | 4 +- maro/rl/algorithm/ac.py | 12 ++--- maro/rl/algorithm/ddpg.py | 10 ++--- maro/rl/algorithm/dqn.py | 10 ++--- maro/rl/algorithm/pg.py | 10 ++--- maro/rl/experience/__init__.py | 6 +-- ...rience_memory.py => experience_manager.py} | 45 ++++++++++++------- maro/rl/experience/sampler.py | 21 ++------- maro/rl/policy/policy.py | 14 +++--- maro/rl/training/actor.py | 6 ++- .../rl_formulation.ipynb | 8 ++-- tests/communication/test_proxy.py | 15 +++---- tests/communication/test_rejoin.py | 2 +- tests/communication/test_zmq_driver.py | 7 ++- tests/test_store.py | 8 ++-- 24 files changed, 117 insertions(+), 122 deletions(-) rename maro/rl/experience/{experience_memory.py => experience_manager.py} (73%) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index f62d75bbd..50f1f950d 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -132,7 +132,7 @@ maro.rl.experience.abs\_store maro.rl.experience.simple\_store -------------------------------------------------------------------------------- -.. automodule:: maro.rl.experience.experience_memory +.. automodule:: maro.rl.experience.experience_manager :members: :undoc-members: :show-inheritance: diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 8377ba3f7..a201e5f04 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -32,9 +32,9 @@ on which updates can be made. class AbsCorePolicy(AbsPolicy): - def __init__(self, experience_memory: ExperienceMemory): + def __init__(self, experience_manager: ExperienceManager): super().__init__() - self.experience_memory = experience_memory + self.experience_manager = experience_manager @abstractmethod def update(self): @@ -102,7 +102,7 @@ Experience An ``ExperienceSet`` is a synonym for training data for RL policies. The data originate from the simulator and get processed and organized into a set of transitions in the form of (state, action, reward, next_state, info), where ''info'' contains information about the transition that is not encoded in the state but may be necessary -for sampling purposes. An ``ExperienceMemory`` is a storage facility for experience sets and is maintained by +for sampling purposes. An ``ExperienceManager`` is a storage facility for experience sets and is maintained by a policy for storing and retrieving training data. Sampling from the experience memory can be customized by registering a user-defined sampler to it. diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index 7d3d57e88..e1f1f505f 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -67,7 +67,7 @@ policy: num_steps: 10 actor_loss_coefficient: 0.1 # clip_ratio: 0.8 # for PPO - experience_memory: + experience_manager: capacity: 100000 overwrite_type: "rolling" update_trigger: diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 163cdc1a4..137b8abe2 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -67,10 +67,9 @@ policy: soft_update_coefficient: 0.1 double: false loss_cls: smooth_l1 - experience_memory: + experience_manager: capacity: 1000000 overwrite_type: "rolling" - sampling: batch_size: 128 multi-process: group: cim-dqn diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 4679d902d..44a93d68e 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -6,7 +6,7 @@ from multiprocessing import Process from maro.rl import ( - Actor, DQN, DQNConfig, EpsilonGreedyExploration, ExperienceMemory, FullyConnectedBlock, + Actor, DQN, DQNConfig, EpsilonGreedyExploration, ExperienceManager, FullyConnectedBlock, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalPolicyManager, OptimOption, ParallelRolloutManager ) @@ -44,7 +44,7 @@ def get_independent_policy(policy_id): return DQN( name=policy_id, q_net=qnet, - experience_memory=ExperienceMemory(**cfg["experience_memory"]), + experience_manager=ExperienceManager(**cfg["experience_manager"]), config=DQNConfig(**cfg["algorithm_config"]) ) diff --git a/examples/proxy/broadcast.py b/examples/proxy/broadcast.py index 417737cc3..482391b6b 100644 --- a/examples/proxy/broadcast.py +++ b/examples/proxy/broadcast.py @@ -21,13 +21,13 @@ def worker(group_name): print(f"{proxy.name}'s counter is {counter}.") # Nonrecurring receive the message from the proxy. - for msg in proxy.receive(is_continuous=False): - print(f"{proxy.name} receive message from {msg.source}.") + msg = proxy.receive_once() + print(f"{proxy.name} received message from {msg.source}.") - if msg.tag == "INC": - counter += 1 - print(f"{proxy.name} receive INC request, {proxy.name}'s count is {counter}.") - proxy.reply(message=msg, tag="done") + if msg.tag == "INC": + counter += 1 + print(f"{proxy.name} receive INC request, {proxy.name}'s count is {counter}.") + proxy.reply(message=msg, tag="done") def master(group_name: str, worker_num: int, is_immediate: bool = False): diff --git a/examples/proxy/scatter.py b/examples/proxy/scatter.py index fd0f2b6f7..36cbb295c 100644 --- a/examples/proxy/scatter.py +++ b/examples/proxy/scatter.py @@ -21,12 +21,12 @@ def summation_worker(group_name): expected_peers={"master": 1}) # Nonrecurring receive the message from the proxy. - for msg in proxy.receive(is_continuous=False): - print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.body}.") + msg = proxy.receive_once() + print(f"{proxy.name} received message from {msg.source}. the payload is {msg.body}.") - if msg.tag == "job": - replied_payload = sum(msg.body) - proxy.reply(message=msg, tag="sum", body=replied_payload) + if msg.tag == "job": + replied_payload = sum(msg.body) + proxy.reply(message=msg, tag="sum", body=replied_payload) def multiplication_worker(group_name): @@ -41,12 +41,12 @@ def multiplication_worker(group_name): expected_peers={"master": 1}) # Nonrecurring receive the message from the proxy. - for msg in proxy.receive(is_continuous=False): - print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.body}.") + msg = proxy.receive_once() + print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.body}.") - if msg.tag == "job": - replied_payload = np.prod(msg.body) - proxy.reply(message=msg, tag="multiply", body=replied_payload) + if msg.tag == "job": + replied_payload = np.prod(msg.body) + proxy.reply(message=msg, tag="multiply", body=replied_payload) def master(group_name: str, sum_worker_number: int, multiply_worker_number: int, is_immediate: bool = False): diff --git a/examples/proxy/send.py b/examples/proxy/send.py index 7b8914143..e35c92a1e 100644 --- a/examples/proxy/send.py +++ b/examples/proxy/send.py @@ -21,12 +21,12 @@ def worker(group_name): expected_peers={"master": 1}) # Nonrecurring receive the message from the proxy. - for msg in proxy.receive(is_continuous=False): - print(f"{proxy.name} receive message from {msg.source}. the payload is {msg.body}.") + msg = proxy.receive_once() + print(f"{proxy.name} received message from {msg.source}. the payload is {msg.body}.") - if msg.tag == "sum": - replied_payload = sum(msg.body) - proxy.reply(message=msg, tag="sum", body=replied_payload) + if msg.tag == "sum": + replied_payload = sum(msg.body) + proxy.reply(message=msg, tag="sum", body=replied_payload) def master(group_name: str, is_immediate: bool = False): diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 9fa67fa40..74cf0a2f4 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -359,7 +359,7 @@ def receive_by_id(self, targets: List[str], timeout: int = None) -> List[Message return received_messages # Wait for incoming messages. - for msg in self._driver.receive(is_continuous=True, timeout=timeout): + for msg in self._driver.receive(timeout=timeout): if not msg: return received_messages diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 62891c73e..1b4c7703b 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -6,7 +6,7 @@ get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.experience import AbsSampler, ExperienceMemory, ExperienceSet, UniformSampler +from maro.rl.experience import AbsSampler, ExperienceManager, ExperienceSet from maro.rl.exploration import ( AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, @@ -30,7 +30,7 @@ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", "AbsEnvWrapper", - "AbsSampler", "ExperienceMemory", "ExperienceSet", "UniformSampler", + "AbsSampler", "ExperienceManager", "ExperienceSet", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", "UniformNoiseExploration", diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index bcf7f4373..8f13b483b 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience import ExperienceMemory +from maro.rl.experience import ExperienceManager from maro.rl.model import DiscreteACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -58,7 +58,7 @@ class ActorCritic(AbsCorePolicy): name (str): Policy name. ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. - experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config: Configuration for the AC algorithm. update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. @@ -69,7 +69,7 @@ def __init__( self, name: str, ac_net: DiscreteACNet, - experience_memory: ExperienceMemory, + experience_manager: ExperienceManager, config: ActorCriticConfig, update_trigger: int = 1, warmup: int = 1, @@ -77,7 +77,7 @@ def __init__( if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") - super().__init__(name, experience_memory, update_trigger=update_trigger, warmup=warmup) + super().__init__(name, experience_manager, update_trigger=update_trigger, warmup=warmup) self.ac_net = ac_net self.config = config self.device = self.ac_net.device @@ -92,7 +92,7 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: def update(self): self.ac_net.train() for _ in range(self.config.train_epochs): - experience_set = self.experience_memory.get() + experience_set = self.experience_manager.get() states, next_states = experience_set.states, experience_set.next_states actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.device) log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) @@ -121,7 +121,7 @@ def update(self): self.ac_net.step(loss) - self.experience_memory.clear() + self.experience_manager.clear() def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithm/ddpg.py index 1057c58ee..7e1273aed 100644 --- a/maro/rl/algorithm/ddpg.py +++ b/maro/rl/algorithm/ddpg.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience import ExperienceMemory +from maro.rl.experience import ExperienceManager from maro.rl.model import ContinuousACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -62,7 +62,7 @@ class DDPG(AbsCorePolicy): Args: name (str): Policy name. ac_net (ContinuousACNet): DDPG policy and q-value models. - experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config (DDPGConfig): Configuration for DDPG algorithm. update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. @@ -73,7 +73,7 @@ def __init__( self, name: str, ac_net: ContinuousACNet, - experience_memory: ExperienceMemory, + experience_manager: ExperienceManager, config: DDPGConfig, update_trigger: int = 1, warmup: int = 1, @@ -81,7 +81,7 @@ def __init__( if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") - super().__init__(name, experience_memory, update_trigger=update_trigger, warmup=warmup) + super().__init__(name, experience_manager, update_trigger=update_trigger, warmup=warmup) self.ac_net = ac_net if self.ac_net.trainable: self.target_ac_net = ac_net.copy() @@ -101,7 +101,7 @@ def choose_action(self, states) -> Union[float, np.ndarray]: def update(self): self.ac_net.train() for _ in range(self.config.train_epochs): - experience_set = self.experience_memory.get() + experience_set = self.experience_manager.get() states, next_states = experience_set.states, experience_set.next_states actual_actions = torch.from_numpy(experience_set.actions).to(self.device) rewards = torch.from_numpy(experience_set.rewards).to(self.device) diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithm/dqn.py index 07d2a717a..0e7ef713a 100644 --- a/maro/rl/algorithm/dqn.py +++ b/maro/rl/algorithm/dqn.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience import ExperienceMemory +from maro.rl.experience import ExperienceManager from maro.rl.model import DiscreteQNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -61,7 +61,7 @@ class DQN(AbsCorePolicy): Args: name (str): Policy name. q_net (DiscreteQNet): Q-value model. - experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config (DQNConfig): Configuration for DQN algorithm. update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. @@ -72,7 +72,7 @@ def __init__( self, name: str, q_net: DiscreteQNet, - experience_memory: ExperienceMemory, + experience_manager: ExperienceManager, config: DQNConfig, update_trigger: int = 1, warmup: int = 1, @@ -80,7 +80,7 @@ def __init__( if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(name, experience_memory, update_trigger=update_trigger, warmup=warmup) + super().__init__(name, experience_manager, update_trigger=update_trigger, warmup=warmup) self.q_net = q_net if self.q_net.trainable: self.target_q_net = q_net.copy() @@ -104,7 +104,7 @@ def update(self): self.q_net.train() for _ in range(self.config.train_epochs): # sample from the replay memory - experience_set = self.experience_memory.get() + experience_set = self.experience_manager.get() states, next_states = experience_set.states, experience_set.next_states actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithm/pg.py index 24f409b2d..8e3f80a5d 100644 --- a/maro/rl/algorithm/pg.py +++ b/maro/rl/algorithm/pg.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience.experience_memory import ExperienceMemory +from maro.rl.experience.experience_manager import ExperienceManager from maro.rl.model import DiscretePolicyNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_truncated_cumulative_reward @@ -33,7 +33,7 @@ class PolicyGradient(AbsCorePolicy): name (str): Policy name. policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. - experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config (PolicyGradientConfig): Configuration for the PG algorithm. update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. @@ -44,14 +44,14 @@ def __init__( self, name: str, policy_net: DiscretePolicyNet, - experience_memory: ExperienceMemory, + experience_manager: ExperienceManager, config: PolicyGradientConfig, update_trigger: int = 1, warmup: int = 1 ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__(name, experience_memory, update_trigger=update_trigger, warmup=warmup) + super().__init__(name, experience_manager, update_trigger=update_trigger, warmup=warmup) self.policy_net = policy_net self.config = config self.device = self.policy_net.device @@ -70,7 +70,7 @@ def update(self): which they are generated during the simulation. Otherwise, the return values may be meaningless. """ self.policy_net.train() - experience_set = self.experience_memory.get() + experience_set = self.experience_manager.get() log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 0a720b09d..1f40ec329 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .experience_memory import ExperienceMemory, ExperienceSet -from .sampler import AbsSampler, UniformSampler +from .experience_manager import ExperienceManager, ExperienceSet +from .sampler import AbsSampler -__all__ = ["AbsSampler", "ExperienceMemory", "ExperienceSet", "UniformSampler"] +__all__ = ["AbsSampler", "ExperienceManager", "ExperienceSet"] diff --git a/maro/rl/experience/experience_memory.py b/maro/rl/experience/experience_manager.py similarity index 73% rename from maro/rl/experience/experience_memory.py rename to maro/rl/experience/experience_manager.py index 0b446f2b3..aaac4624e 100644 --- a/maro/rl/experience/experience_memory.py +++ b/maro/rl/experience/experience_manager.py @@ -45,7 +45,7 @@ def extend(self, other): self.info += other.info -class ExperienceMemory: +class ExperienceManager: """Storage facility for simulation experiences. This implementation uses a dictionary of lists as the internal data structure. The objects for each key @@ -57,8 +57,25 @@ class ExperienceMemory: are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - "rolling", where overwrite occurs sequentially with wrap-around. - "random", where overwrite occurs randomly among filled positions. + batch_size (int): Batch size for the default uniform sampling. This can be set to -1 if the required + batch size is the number of items stored. To get the whole data, set this to -1 and ``replace`` to + False. If a sampler is registered, this is ignored, and the batch size will be determined by the + sampler. Defaults to 32. + replace (bool): A flag indicating whether the default uniform sampling is with replacement or without. + Ignored if a sampler is registered. Defaults to True. + sampler_cls: Type of sampler to be registered. Must be a subclass of ``AbsSampler``. + Defaults to UnifromSampler. + sampler_params (dict): Keyword parameters for ``sampler_cls``. """ - def __init__(self, capacity: int, overwrite_type: str = "rolling"): + def __init__( + self, + capacity: int, + overwrite_type: str = "rolling", + batch_size: int = 32, + replace: bool = True, + sampler_cls=None, + sampler_params: dict = None, + ): super().__init__() if overwrite_type not in {"rolling", "random"}: raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") @@ -66,9 +83,11 @@ def __init__(self, capacity: int, overwrite_type: str = "rolling"): self._overwrite_type = overwrite_type self._keys = ExperienceSet.__slots__ self.data = {key: [None] * self._capacity for key in self._keys} + self.batch_size = batch_size + self.replace = replace + self.sampler = None if sampler_cls is None else sampler_cls(self, **sampler_params) self._size = 0 self._index = 0 - self.sampler = None @property def capacity(self): @@ -124,24 +143,16 @@ def put(self, experience_set: ExperienceSet): def get(self): """Retrieve an experience set from the memory. - If not sampler has been registered, the entirety of the stored data will be returned in the form of - an ``ExperienceSet`` and the memory will be cleared. Otherwise, a sample from the memory will be - returned according to the sampling logic defined by the registered sampler. + If not sampler has been registered, a uniformly random sample of the stored data will be returned + in the form of an ``ExperienceSet`` and the memory will be cleared. Otherwise, a sample from the + memory will be returned according to the sampling logic defined by the registered sampler. """ - if self.sampler is None: - return ExperienceSet(*[self.data[key][:self._size] for key in self._keys]) + if not self.sampler: + indexes = np.random.choice(self._size, size=self.batch_size, replace=self.replace) + return ExperienceSet(*[[self.data[key][idx] for idx in indexes] for key in self._keys]) else: return self.sampler.get() - def register_sampler(self, sampler_cls, **sampler_params): - """Register a sampler to the experience memory. - - Args: - sampler_cls: Type of sampler to be registered. Must be a subclass of ``AbsSampler``. - sampler_params: Keyword parameters for ``sampler_cls``. - """ - self.sampler = sampler_cls(self, **sampler_params) - def clear(self): """Empty the memory.""" self.data = {key: [None] * self._capacity for key in self._keys} diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index 8d5a2fd0f..291bf2d52 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -3,29 +3,14 @@ from abc import ABC, abstractmethod -import numpy as np - -from .experience_memory import ExperienceMemory, ExperienceSet +from .experience_manager import ExperienceManager, ExperienceSet class AbsSampler(ABC): - def __init__(self, experience_memory: ExperienceMemory): + def __init__(self, experience_manager: ExperienceManager): super().__init__() - self.experience_memory = experience_memory + self.experience_manager = experience_manager @abstractmethod def get(self) -> ExperienceSet: raise NotImplementedError - - -class UniformSampler(AbsSampler): - def __init__(self, experience_memory: ExperienceMemory, batch_size: int, replace: bool = True): - super().__init__(experience_memory) - self.batch_size = batch_size - self.replace = replace - - def get(self) -> ExperienceSet: - indexes = np.random.choice(self.experience_memory.size, size=self.batch_size, replace=self.replace) - return ExperienceSet( - *[[self.experience_memory.data[key][idx] for idx in indexes] for key in self.experience_memory.keys] - ) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 6db289103..8fec23408 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod -from maro.rl.experience import ExperienceMemory, ExperienceSet +from maro.rl.experience import ExperienceManager, ExperienceSet class AbsPolicy(ABC): @@ -37,7 +37,7 @@ class AbsCorePolicy(AbsPolicy): Args: name (str): Policy name. - experience_memory (ExperienceMemory): An experience manager for storing and retrieving experiences + experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. @@ -46,12 +46,12 @@ class AbsCorePolicy(AbsPolicy): def __init__( self, name: str, - experience_memory: ExperienceMemory, + experience_manager: ExperienceManager, update_trigger: int = 1, warmup: int = 1 ): super().__init__(name) - self.experience_memory = experience_memory + self.experience_manager = experience_manager self.update_trigger = update_trigger self.warmup = warmup self._new_exp_counter = 0 @@ -90,12 +90,12 @@ def set_state(self, policy_state): pass def on_experiences(self, exp: ExperienceSet) -> bool: - self.experience_memory.put(exp) + self.experience_manager.put(exp) self._new_exp_counter += exp.size print( - f"Policy {self._name}: exp mem size = {self.experience_memory.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" + f"Policy {self._name}: exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" ) - if self.experience_memory.size >= self.warmup and self._new_exp_counter >= self.update_trigger: + if self.experience_manager.size >= self.warmup and self._new_exp_counter >= self.update_trigger: self.update() self._new_exp_counter = 0 return True diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 942e38a23..74e2eb590 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -138,9 +138,11 @@ def _collect(self, msg): ) exp_by_agent = self.env.get_experiences() for agent_id, exp in exp_by_agent.items(): - self.policy[agent_id].experience_memory.put(exp) + self.policy[agent_id].experience_manager.put(exp) - ret_exp = {id_: policy.experience_memory.get() for id_, policy in self.policy_dict.items()} + ret_exp = { + policy_name: policy.experience_manager.get() for policy_name, policy in self.policy_dict.items() + } return_info = { MsgKey.EPISODE_END: not self.env.state, MsgKey.EPISODE_INDEX: episode_index, diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 01182883e..4b61598c3 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -153,7 +153,7 @@ "source": [ "import torch\n", "\n", - "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceMemory, FullyConnectedBlock, OptimOption\n", + "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceManager, FullyConnectedBlock, OptimOption\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", @@ -191,7 +191,7 @@ " \"critic\": OptimOption(optim_cls=\"rmsprop\", optim_params={\"lr\": 0.001})\n", " }\n", " },\n", - " \"experience_memory\": {\n", + " \"experience_manager\": {\n", " \"capacity\": 10000\n", " },\n", " \"algorithm_config\": {\n", @@ -221,8 +221,8 @@ " actor = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"actor\"])\n", " critic = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"critic\"])\n", " ac_net = MyACNet({\"actor\": actor, \"critic\": critic}, optim_option=policy_config[\"model\"][\"optimization\"])\n", - " experience_memory = ExperienceMemory(policy_config[\"experience_memory\"][\"capacity\"])\n", - " return ActorCritic(name, ac_net, experience_memory, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" + " experience_manager = ExperienceManager(policy_config[\"experience_manager\"][\"capacity\"])\n", + " return ActorCritic(name, ac_net, experience_manager, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" ] }, { diff --git a/tests/communication/test_proxy.py b/tests/communication/test_proxy.py index 715cb80b4..7065cc7a9 100644 --- a/tests/communication/test_proxy.py +++ b/tests/communication/test_proxy.py @@ -11,8 +11,7 @@ def message_receive(proxy): - for received_message in proxy.receive(is_continuous=False): - return received_message.body + return proxy.receive_once().body @unittest.skipUnless(os.environ.get("test_with_redis", False), "require redis") @@ -55,8 +54,8 @@ def test_send(self): ) TestProxy.master_proxy.isend(send_msg) - for receive_message in worker_proxy.receive(is_continuous=False): - self.assertEqual(send_msg.body, receive_message.body) + recv_msg = worker_proxy.receive_once() + self.assertEqual(send_msg.body, recv_msg.body) def test_scatter(self): scatter_payload = ["worker_1", "worker_2", "worker_3", "worker_4", "worker_5"] @@ -72,8 +71,8 @@ def test_scatter(self): ) for i, worker_proxy in enumerate(TestProxy.worker_proxies): - for msg in worker_proxy.receive(is_continuous=False): - self.assertEqual(scatter_payload[i], msg.body) + msg = worker_proxy.receive_once() + self.assertEqual(scatter_payload[i], msg.body) def test_broadcast(self): with ThreadPoolExecutor(max_workers=len(TestProxy.worker_proxies)) as executor: @@ -101,8 +100,8 @@ def test_reply(self): ) session_id_list = TestProxy.master_proxy.isend(send_msg) - for receive_message in worker_proxy.receive(is_continuous=False): - worker_proxy.reply(message=receive_message, tag="unit_test", body="world!") + recv_message = worker_proxy.receive_once() + worker_proxy.reply(message=recv_message, tag="unit_test", body="world!") replied_msg_list = TestProxy.master_proxy.receive_by_id(session_id_list) self.assertEqual(send_msg.body + replied_msg_list[0].body, "hello world!") diff --git a/tests/communication/test_rejoin.py b/tests/communication/test_rejoin.py index 40ac22394..af79c2520 100644 --- a/tests/communication/test_rejoin.py +++ b/tests/communication/test_rejoin.py @@ -31,7 +31,7 @@ def actor_init(queue, redis_port): ) # Continuously receive messages from proxy. - for msg in proxy.receive(is_continuous=True): + for msg in proxy.receive(): print(f"receive message from master. {msg.tag}") if msg.tag == "cont": proxy.reply(message=msg, tag="recv", body="successful receive!") diff --git a/tests/communication/test_zmq_driver.py b/tests/communication/test_zmq_driver.py index 58308b6b4..2c98a27fb 100644 --- a/tests/communication/test_zmq_driver.py +++ b/tests/communication/test_zmq_driver.py @@ -9,8 +9,7 @@ def message_receive(driver): - for received_message in driver.receive(is_continuous=False): - return received_message.body + return driver.receive_once().body @unittest.skipUnless(os.environ.get("test_with_zmq", False), "require zmq") @@ -49,8 +48,8 @@ def test_send(self): ) TestDriver.sender.send(message) - for received_message in TestDriver.receivers[peer].receive(is_continuous=False): - self.assertEqual(received_message.body, message.body) + recv_message = TestDriver.receivers[peer].receive_once() + self.assertEqual(recv_message.body, message.body) def test_broadcast(self): executor = ThreadPoolExecutor(max_workers=len(TestDriver.peer_list)) diff --git a/tests/test_store.py b/tests/test_store.py index 28c640807..82dc3e24b 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -3,12 +3,12 @@ import unittest -from maro.rl import ExperienceMemory +from maro.rl import ExperienceManager class TestUnboundedStore(unittest.TestCase): def setUp(self) -> None: - self.store = ExperienceMemory(["a", "b", "c"]) + self.store = ExperienceManager(["a", "b", "c"]) def tearDown(self) -> None: self.store.clear() @@ -44,7 +44,7 @@ def test_filter(self): class TestFixedSizeStore(unittest.TestCase): def test_put_with_rolling_overwrite(self): - store = ExperienceMemory(["a", "b", "c"], capacity=5, overwrite_type="rolling") + store = ExperienceManager(["a", "b", "c"], capacity=5, overwrite_type="rolling") indexes = store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) expected = [0, 1, 2] self.assertEqual(indexes, expected, msg=f"expected indexes = {expected}, got {indexes}") @@ -56,7 +56,7 @@ def test_put_with_rolling_overwrite(self): self.assertEqual(actual, expected, msg=f"expected store content = {expected}, got {actual}") def test_put_with_random_overwrite(self): - store = ExperienceMemory(["a", "b", "c"], capacity=5, overwrite_type="random") + store = ExperienceManager(["a", "b", "c"], capacity=5, overwrite_type="random") indexes = store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) indexes_2 = store.put({"a": [10, 11, 12, 13], "b": [14, 15, 16, 17], "c": [18, 19, 20, 21]}) for i in indexes_2[2:]: From 3456b4389bc3df19f5e56b245fc999f6cfbbd792 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 27 May 2021 11:47:37 +0000 Subject: [PATCH 275/482] refined proxy coding style --- examples/cim/dqn/config.yml | 11 ++++-- examples/cim/dqn/main.py | 7 ++-- maro/communication/proxy.py | 46 +++++++++++------------- maro/rl/experience/experience_manager.py | 5 ++- maro/rl/training/actor.py | 5 ++- maro/rl/training/rollout_manager.py | 6 ++-- 6 files changed, 44 insertions(+), 36 deletions(-) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 137b8abe2..d93e0b539 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -68,9 +68,14 @@ policy: double: false loss_cls: smooth_l1 experience_manager: - capacity: 1000000 - overwrite_type: "rolling" - batch_size: 128 + rollout: # for experience managers in actor processes + capacity: 1000 + overwrite_type: "rolling" + batch_size: -1 + training: # for experience managers in the learner process + capacity: 100000 + overwrite_type: "rolling" + batch_size: 128 multi-process: group: cim-dqn num_actors: 3 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 44a93d68e..4236d837d 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -35,16 +35,17 @@ OUT_DIM = config["env"]["wrapper"]["num_actions"] -def get_independent_policy(policy_id): +def get_independent_policy(policy_id, training: bool = True): cfg = config["policy"] qnet = QNet( FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["network"]), optim_option=OptimOption(**cfg["model"]["optimization"]) ) + exp_cfg = cfg["experience_manager"]["training"] if training else cfg["experience_manager"]["rollout"] return DQN( name=policy_id, q_net=qnet, - experience_manager=ExperienceManager(**cfg["experience_manager"]), + experience_manager=ExperienceManager(**exp_cfg), config=DQNConfig(**cfg["algorithm_config"]) ) @@ -76,7 +77,7 @@ def local_learner_mode(): def get_dqn_actor_process(): env = Env(**config["env"]["basic"]) num_actions = config["env"]["wrapper"]["num_actions"] - policy_list = [get_independent_policy(policy_id=i) for i in env.agent_idx_list] + policy_list = [get_independent_policy(policy_id=i, training=False) for i in env.agent_idx_list] epsilon_greedy = EpsilonGreedyExploration(num_actions=num_actions) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 74cf0a2f4..bec4fdeb3 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -178,6 +178,7 @@ def __init__( self._join() + def _signal_handler(self, signum, frame): self._redis_connection.hdel(self._redis_hash_name, self._name) self._logger.critical(f"{self._name} received Signal: {signum} at frame: {frame}") @@ -243,44 +244,37 @@ def _get_peers_list(self): if not self._peers_info_dict: raise PeersMissError(f"Cannot get {self._name}\'s peers.") - for peer_type in self._peers_info_dict.keys(): - peer_hash_name, peer_number = self._peers_info_dict[peer_type] - retry_number = 0 - expected_peers_name = [] - while retry_number < self._max_retries: - if self._redis_connection.hlen(peer_hash_name) >= peer_number: - expected_peers_name = self._redis_connection.hkeys(peer_hash_name) - expected_peers_name = [peer.decode() for peer in expected_peers_name] - if len(expected_peers_name) > peer_number: - expected_peers_name = expected_peers_name[:peer_number] - self._logger.info(f"{self._name} successfully get all {peer_type}\'s name.") + for peer_type, (peer_hash_name, num_expected) in self._peers_info_dict.items(): + registered_peers, next_retry = [], self._retry_interval_base_value + for _ in range(self._max_retries): + if self._redis_connection.hlen(peer_hash_name) >= num_expected: + registered_peers = [peer.decode() for peer in self._redis_connection.hkeys(peer_hash_name)] + if len(registered_peers) > num_expected: + del registered_peers[num_expected:] + self._logger.info(f"{self._name} successfully get all {peer_type}\'s names.") break else: self._logger.warn( - f"{self._name} failed to get {peer_type}\'s name. Retrying in " - f"{self._retry_interval_base_value * (2 ** retry_number)} seconds." + f"{self._name} failed to get {peer_type}\'s name. Retrying in {next_retry} seconds." ) - time.sleep(self._retry_interval_base_value * (2 ** retry_number)) - retry_number += 1 + time.sleep(next_retry) + next_retry *= 2 - if not expected_peers_name: + if not registered_peers: raise InformationUncompletedError( - f"{self._name} failure to get enough number of {peer_type} from redis." + f"{self._name} failed to get the required number of {peer_type}s from redis." ) - self._onboard_peer_dict[peer_type] = {peer_name: None for peer_name in expected_peers_name} + self._onboard_peer_dict[peer_type] = {peer_name: None for peer_name in registered_peers} self._onboard_peers_start_time = time.time() def _build_connection(self): """Grabbing all peers' address from Redis, and connect all peers in driver.""" - for peer_type in self._peers_info_dict.keys(): + for peer_type, info in self._peers_info_dict.items(): name_list = list(self._onboard_peer_dict[peer_type].keys()) try: - peers_socket_value = self._redis_connection.hmget( - self._peers_info_dict[peer_type].hash_table_name, - name_list - ) + peers_socket_value = self._redis_connection.hmget(info.hash_table_name, name_list) for idx, peer_name in enumerate(name_list): self._onboard_peer_dict[peer_type][peer_name] = json.loads(peers_socket_value[idx]) self._logger.info(f"{self._name} successfully get {peer_name}\'s socket address") @@ -449,7 +443,7 @@ def _broadcast( body=None ) -> List[str]: """Broadcast message to all peers, and return list of session id.""" - if component_type not in list(self._onboard_peer_dict.keys()): + if component_type not in self._onboard_peer_dict: self._logger.error( f"peer_type: {component_type} cannot be recognized. Please check the input of proxy.broadcast." ) @@ -534,8 +528,8 @@ def _send(self, message: Message) -> Union[List[str], None]: # Check message cache. if ( self._enable_message_cache - and message.destination in list(self._onboard_peer_dict[peer_type].keys()) - and message.destination in list(self._message_cache_for_exited_peers.keys()) + and message.destination in self._onboard_peer_dict[peer_type] + and message.destination in self._message_cache_for_exited_peers ): self._logger.info(f"Sending pending message to {message.destination}.") for pending_message in self._message_cache_for_exited_peers[message.destination]: diff --git a/maro/rl/experience/experience_manager.py b/maro/rl/experience/experience_manager.py index aaac4624e..afda224cf 100644 --- a/maro/rl/experience/experience_manager.py +++ b/maro/rl/experience/experience_manager.py @@ -79,6 +79,8 @@ def __init__( super().__init__() if overwrite_type not in {"rolling", "random"}: raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") + if batch_size <= 0 and batch_size != -1: + raise ValueError(f"batch_size must be -1 or a positive integer") self._capacity = capacity self._overwrite_type = overwrite_type self._keys = ExperienceSet.__slots__ @@ -147,8 +149,9 @@ def get(self): in the form of an ``ExperienceSet`` and the memory will be cleared. Otherwise, a sample from the memory will be returned according to the sampling logic defined by the registered sampler. """ + batch_size = self._size if self.batch_size == -1 else self.batch_size if not self.sampler: - indexes = np.random.choice(self._size, size=self.batch_size, replace=self.replace) + indexes = np.random.choice(self._size, size=batch_size, replace=self.replace) return ExperienceSet(*[[self.data[key][idx] for idx in indexes] for key in self._keys]) else: return self.sampler.get() diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 74e2eb590..911ed3f1a 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -74,7 +74,9 @@ def __init__( for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) - self._proxy = Proxy(group, "actor", {"actor_manager": 1}, **proxy_kwargs) + self._proxy = Proxy(group, "actor", {"rollout_manager": 1}, **proxy_kwargs) + print("actor name: ", self._proxy.name) + print("redis_info as seen from actor: ", self._proxy._redis_connection.hgetall(self._proxy._redis_hash_name)) self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): @@ -143,6 +145,7 @@ def _collect(self, msg): ret_exp = { policy_name: policy.experience_manager.get() for policy_name, policy in self.policy_dict.items() } + print("exp_by_policy: ", {pid: exp.size for pid, exp in ret_exp.items()}) return_info = { MsgKey.EPISODE_END: not self.env.state, MsgKey.EPISODE_INDEX: episode_index, diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index abb95896b..f4ae8e80d 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -263,7 +263,9 @@ def __init__( self._logger = Logger("PARALLEL_ROLLOUT_MANAGER", dump_folder=log_dir) self.num_actors = num_actors peers = {"actor": num_actors} - self._proxy = Proxy(group, "actor_manager", peers, **proxy_kwargs) + self._proxy = Proxy(group, "rollout_manager", peers, **proxy_kwargs) + print("rollout manager name: ", self._proxy.name) + print("redis_info as seen from rollout manager: ", self._proxy._redis_connection.hgetall(self._proxy._redis_hash_name)) self._actors = self._proxy.peers["actor"] # remote actor ID's if max_receive_attempts is None: @@ -294,7 +296,7 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic MsgKey.POLICY: policy_state_dict } self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") + self._logger.info(f"Sent collect requests to {self._actors} for ep-{episode_index}, segment-{segment_index}") # Receive roll-out results from remote actors combined_exp_by_policy = defaultdict(ExperienceSet) From fed8467e90d2e49e5ba3e55efbf7cb93055c2191 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 28 May 2021 09:20:12 +0000 Subject: [PATCH 276/482] updated images and refined doc --- docs/source/key_components/rl_toolkit.rst | 88 ++++++++++---------- examples/cim/dqn/main.py | 1 + maro/rl/exploration/exploration_scheduler.py | 21 ++++- maro/rl/training/actor.py | 4 +- maro/rl/training/rollout_manager.py | 2 - 5 files changed, 61 insertions(+), 55 deletions(-) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index a201e5f04..3f526e857 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -2,26 +2,57 @@ RL Toolkit ========== -MARO provides a full-stack abstraction for reinforcement learning (RL), which enables users to -apply predefined and customized components to various scenarios. The main abstractions include -fundamental components such as `Agent <#agent>`_\ and `Shaper <#shaper>`_\ , and training routine -controllers such as `Actor <#actor>`_\ and `Learner <#learner>`_. +MARO provides a full-stack abstraction for reinforcement learning (RL) which includes various customizable +components. At the top level of a training workflow are: +* Learner, which consists of a roll-out manager and a policy manager, is the controller for a learning process. +The learner process executes training cycles that alternate between data collection and policy updates. +* Rollout manager, which is responsible for collecting simulation data. The ``LocalRolloutManager`` performs roll-outs +locally, while the ``ParallelRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in parallel. +* Policy manager, which manages a set of policies and controls their updates. The policy instances may reside in the +manager (``LocalPolicyManager``) or be distributed on a set of remote nodes (``ParallelPolicyManager``, to be implemented). +* Actor, which consists of an environment instance and a set of policies that agents use to interact with it, is a +remote roll-out worker instance managed by a ``ParallelRolloutManager``. + +.. image:: ../images/rl/learner.svg + :target: ../images/rl/learner.svg + :alt: Overview + +.. image:: ../images/rl/rollout_manager.svg + :target: ../images/rl/rollout_manager.svg + :alt: Overview + +.. image:: ../images/rl/policy_manager.svg + :target: ../images/rl/policy_manager.svg + :alt: RL Overview + + +Environment Wrapper +------------------- + +To use the training components described above, it is necessary to implement an environment wrapper for the environment of +your choice. An environment wrapper serves as a bridge between a simulator and the policies that interact with it by providing +unified interfaces to the interaction workflow. It is also responsible for caching transitions and preparing experiences for +training. Key methods that need to be implemented for an environment wrapper include: +* ``get_state``, which encodes agents' observations into policy input. The encoded state for each agent must correspond +to the policy used by the agent. +* ``to_env_action``, which provides model output with context so that it can be executed by the environment simulator. +* ``get_reward``, for evaluating rewards. + +.. image:: ../images/rl/env_wrapper.svg + :target: ../images/rl/env_wrapper.svg + :alt: Environment Wrapper Policy ------ -A policy is a an agent's mechanism to determine an action to take given an observation of the environment. +A policy is a an agent's mechanism to choose actions based on its observations of the environment. Accordingly, the abstract ``AbsPolicy`` class exposes a ``choose_action`` interface. This abstraction encompasses both static policies, such as rule-based policies, and updatable policies, such as RL policies. The latter is abstracted through the ``AbsCorePolicy`` sub-class which also exposes a ``update`` interface. By default, updatable -policies require an experience manager to store and retrieve simulation data (in the form of "experiences sets") based -on which updates can be made. - +policies require an experience manager to store and retrieve simulation data (in the form of "experiences sets") +based on which updates can be made. -.. image:: ../images/rl/agent.svg - :target: ../images/rl/agent.svg - :alt: Agent .. code-block:: python @@ -122,38 +153,3 @@ As an example, the exploration for DQN may be carried out with the aid of an ``E exploration = EpsilonGreedyExploration(num_actions=10) greedy_action = q_net.get_action(state) exploration_action = exploration(greedy_action) - - -Environment Wrapper -------------------- - -An environment wrapper a wrapper for a raw MARO environment that provides unified interfaces to the external -RL workflow through user-defined state, action and reward shaping. It is also responsible for caching transitions and preparing experiences -for training. It is necessary to implement an environment wrapper for the environemtn of your choice in order to -run the RL workflow using the training tools described below. Key methods that need to be defined for an environment -wrapper include: -* ``get_state``, which converts observations of an environment into model input. For example, the observation -may be represented by a multi-level data structure, which gets encoded to a one-dimensional vector as input to -a neural network. -* ``to_env_action``, which provides model output with necessary context so that it can be executed by the -environment simulator. -* ``get_reward``, for evaluating rewards. - - -Tools for Training ------------------- - -.. image:: ../images/rl/learner_actor.svg - :target: ../images/rl/learner_actor.svg - :alt: RL Overview - -The RL toolkit provides tools that make local and distributed training easy: -* Learner, which consists of a roll-out manager and a policy manager, is the controller for a learning process. -The learner process executes training cycles that alternate between data collection and policy updates. -* Rollout manager, which is respnsible for collecting simulation data. The ``LocalRolloutManager`` performs roll-outs -locally, while the ``ParallelRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in -parallel. -* Actor, which consists of an environment instance and a set of policies that agents use to interact with it, is a -remote roll-out worker instance managed by a ``ParallelRolloutManager``. -* Policy manager, which manages a set of policies and controls their updates. The policy instances may reside in the -manager (``LocalPolicyManager``) or be distributed on a set of remote nodes (``ParallelPolicyManager``, to be implemented). diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 4236d837d..2445217ff 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import os +import time import yaml from multiprocessing import Process diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/exploration_scheduler.py index 940d759f3..ff9a489f4 100644 --- a/maro/rl/exploration/exploration_scheduler.py +++ b/maro/rl/exploration/exploration_scheduler.py @@ -8,6 +8,18 @@ class AbsExplorationScheduler(ABC): + """Abstract exploration scheduler. + + Each exploration scheduler is registered to a single parameter of an exploration instance. + + Args: + exploration (AbsExploration): An exploration instance to which the scheduler is applied. + param_name (str): Name of the exploration parameter to which the scheduler is applied. + last_ep (int): Last episode. + initial_value: Initial value for the exploration parameter. If None, the value the exploration + instance is instantiated with will be used as the initial value. Defaults to None. + """ + def __init__( self, exploration: AbsExploration, @@ -38,9 +50,10 @@ class LinearExplorationScheduler(AbsExplorationScheduler): param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. - initial_value (float): The initial value for the exploration parameter. If this is None, the - value as specified in the exploration instance will be used as the initial value. Defaults to None. + initial_value: Initial value for the exploration parameter. If None, the value the exploration + instance is instantiated with will be used as the initial value. Defaults to None. """ + def __init__( self, exploration: AbsExploration, @@ -75,8 +88,8 @@ class MultiPhaseLinearExplorationScheduler(AbsExplorationScheduler): the start of another. These points do not have to be given in any particular order. There cannot be two points with the same first element (episode), or a ``ValueError`` will be raised. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. - initial_value (float): The initial value for the exploration parameter. If this is None, the - value as specified in the exploration instance will be used as the initial value. Defaults to None. + initial_value: Initial value for the exploration parameter. If None, the value the exploration + instance is instantiated with will be used as the initial value. Defaults to None. Returns: An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 911ed3f1a..835fdb54f 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -75,8 +75,6 @@ def __init__( self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) self._proxy = Proxy(group, "actor", {"rollout_manager": 1}, **proxy_kwargs) - print("actor name: ", self._proxy.name) - print("redis_info as seen from actor: ", self._proxy._redis_connection.hgetall(self._proxy._redis_hash_name)) self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): @@ -145,7 +143,7 @@ def _collect(self, msg): ret_exp = { policy_name: policy.experience_manager.get() for policy_name, policy in self.policy_dict.items() } - print("exp_by_policy: ", {pid: exp.size for pid, exp in ret_exp.items()}) + return_info = { MsgKey.EPISODE_END: not self.env.state, MsgKey.EPISODE_INDEX: episode_index, diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index f4ae8e80d..716f3f374 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -264,8 +264,6 @@ def __init__( self.num_actors = num_actors peers = {"actor": num_actors} self._proxy = Proxy(group, "rollout_manager", peers, **proxy_kwargs) - print("rollout manager name: ", self._proxy.name) - print("redis_info as seen from rollout manager: ", self._proxy._redis_connection.hgetall(self._proxy._redis_hash_name)) self._actors = self._proxy.peers["actor"] # remote actor ID's if max_receive_attempts is None: From 03bcb41cd4192836ac5b1a87d84435d551166566 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 28 May 2021 18:54:21 +0800 Subject: [PATCH 277/482] updated images --- docs/source/images/rl/agent.svg | 3 --- docs/source/images/rl/core_model.svg | 3 +++ docs/source/images/rl/env_wrapper.svg | 3 +++ docs/source/images/rl/learner.svg | 3 +++ docs/source/images/rl/learner_actor.svg | 3 --- docs/source/images/rl/policy_manager.svg | 3 +++ docs/source/images/rl/rollout_manager.svg | 3 +++ docs/source/key_components/rl_toolkit.rst | 20 +++++++++++++------- 8 files changed, 28 insertions(+), 13 deletions(-) delete mode 100644 docs/source/images/rl/agent.svg create mode 100644 docs/source/images/rl/core_model.svg create mode 100644 docs/source/images/rl/env_wrapper.svg create mode 100644 docs/source/images/rl/learner.svg delete mode 100644 docs/source/images/rl/learner_actor.svg create mode 100644 docs/source/images/rl/policy_manager.svg create mode 100644 docs/source/images/rl/rollout_manager.svg diff --git a/docs/source/images/rl/agent.svg b/docs/source/images/rl/agent.svg deleted file mode 100644 index 8359bb8bb..000000000 --- a/docs/source/images/rl/agent.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Loss Function
Loss Function
Model
Model
Model
Model
Loss Function
Loss Function
Agent
Agent
train
train
choose_action
choo...
Model
Model
 Component
Comp...
Experience Pool
(optional)
Experience Pool...
samples
samples
store
store
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/core_model.svg b/docs/source/images/rl/core_model.svg new file mode 100644 index 000000000..0fd1af68c --- /dev/null +++ b/docs/source/images/rl/core_model.svg @@ -0,0 +1,3 @@ + + +
Multi-head Model
Multi-head Model
%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Task%20Stack%201%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D15%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22260.63%22%20y%3D%22500%22%20width%3D%2298.75%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
%3CmxGraphModel%...
......
.......
......
.......
Head 1
Head 1
Optimizer
Optimizer
Optimizer
Optimizer
Head N
Head N
Shared Stack
Shared Stack
Optimizer
Optimizer
Multi-head NN model - a Core Model implementation 
Multi-head NN model - a Core Model implementation 
Core Model
Core Model
%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Task%20Stack%201%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D15%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22260.63%22%20y%3D%22500%22%20width%3D%2298.75%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
%3CmxGraphModel%...
......
.......
step
step
Component 1
Component 1
Optimizer
Optimizer
Optimizer
Optimizer
Component N
Component N
Core Model
Core Model
forward
forw...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/env_wrapper.svg b/docs/source/images/rl/env_wrapper.svg new file mode 100644 index 000000000..c0aef1f00 --- /dev/null +++ b/docs/source/images/rl/env_wrapper.svg @@ -0,0 +1,3 @@ + + +
get_state()
get_state()
Environment Simulator
Environment Simul...
to_env_action()
to_env_action()
Environment Wrapper
Environment Wrapper
Agent
Agent
Agent
Agent
Policy
Policy
state
state
action
action
execute
execute
Replay Buffer
Replay Buffer
get_reward()
get_reward()
experiences
experiences
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/learner.svg b/docs/source/images/rl/learner.svg new file mode 100644 index 000000000..3c4ee705f --- /dev/null +++ b/docs/source/images/rl/learner.svg @@ -0,0 +1,3 @@ + + +
Learner
Learner
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
experiences
expe...
Policy
Manager
Policy...
updated policies
updated pol...
Learner
Lear...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/learner_actor.svg b/docs/source/images/rl/learner_actor.svg deleted file mode 100644 index ae0495e19..000000000 --- a/docs/source/images/rl/learner_actor.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Agent(s)
Agent(s)
Roll-out Executor
Roll-out Executor
Actor
Actor
Learner
Learner
roll-out request
roll-o...
roll-out result
roll-o...
train
train
decision request
decision r...
roll out
roll o...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/policy_manager.svg b/docs/source/images/rl/policy_manager.svg new file mode 100644 index 000000000..2a725fa7a --- /dev/null +++ b/docs/source/images/rl/policy_manager.svg @@ -0,0 +1,3 @@ + + +
Local Policy Manager
Local Policy Manager
policies
policies
on_experiences
on_experiences
Parallel Policy Manager
Parallel Policy Manager
on_experiences
on_experiences
policy proxy
policy proxy
remote training config
remote training...
Policy Trainer 1
Policy Trainer 1
get_state
get_state
get_state
get_state
subset of policies
subset of pol...
training message
traini...
policy state
policy state
Local Policy Manager
Local Policy Manager
Parallel Policy Manager
Parallel Policy Manager
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/rollout_manager.svg b/docs/source/images/rl/rollout_manager.svg new file mode 100644 index 000000000..29a8325fd --- /dev/null +++ b/docs/source/images/rl/rollout_manager.svg @@ -0,0 +1,3 @@ + + +
Actor
Actor
Local Roll-out Manager
Local Roll-out Manager
training
environment wrapper
training...
policies
policies
collect
colle...
evaluation environment wrapper
evaluation envi...
evaluate
evalu...
Parallel Roll-out Manager
Parallel Roll-out Manager
collect
colle...
evaluate
evalu...
training env wrapper
training env wra...
eval env wrapper
eval env wrapper
policies
policies
actor proxy
actor proxy
roll-out message
roll-out messa...
results
results
remote roll-out config
remote roll-out...
Local Roll-out Manager
Local Roll-out Manager
Parallel Roll-out Manager
Parallel Roll-out Manager
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 3f526e857..ba37c9e03 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -1,26 +1,31 @@ - RL Toolkit ========== MARO provides a full-stack abstraction for reinforcement learning (RL) which includes various customizable components. At the top level of a training workflow are: -* Learner, which consists of a roll-out manager and a policy manager, is the controller for a learning process. -The learner process executes training cycles that alternate between data collection and policy updates. + +* Learner, which consists of a roll-out manager and a policy manager, is the controller for a learning + process. The learner process executes training cycles that alternate between data collection and policy + updates. * Rollout manager, which is responsible for collecting simulation data. The ``LocalRolloutManager`` performs roll-outs -locally, while the ``ParallelRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in parallel. + locally, while the ``ParallelRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in parallel. * Policy manager, which manages a set of policies and controls their updates. The policy instances may reside in the -manager (``LocalPolicyManager``) or be distributed on a set of remote nodes (``ParallelPolicyManager``, to be implemented). + manager (``LocalPolicyManager``) or be distributed on a set of remote nodes (``ParallelPolicyManager``, to be implemented) + for parallelized training. * Actor, which consists of an environment instance and a set of policies that agents use to interact with it, is a -remote roll-out worker instance managed by a ``ParallelRolloutManager``. + remote roll-out worker instance managed by a ``ParallelRolloutManager``. + .. image:: ../images/rl/learner.svg :target: ../images/rl/learner.svg :alt: Overview + .. image:: ../images/rl/rollout_manager.svg :target: ../images/rl/rollout_manager.svg :alt: Overview + .. image:: ../images/rl/policy_manager.svg :target: ../images/rl/policy_manager.svg :alt: RL Overview @@ -33,8 +38,9 @@ To use the training components described above, it is necessary to implement an your choice. An environment wrapper serves as a bridge between a simulator and the policies that interact with it by providing unified interfaces to the interaction workflow. It is also responsible for caching transitions and preparing experiences for training. Key methods that need to be implemented for an environment wrapper include: + * ``get_state``, which encodes agents' observations into policy input. The encoded state for each agent must correspond -to the policy used by the agent. + to the policy used by the agent. * ``to_env_action``, which provides model output with context so that it can be executed by the environment simulator. * ``get_reward``, for evaluating rewards. From f044471af984d6512aa31c1fd5089cd21b5a7e5b Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 31 May 2021 08:57:22 +0000 Subject: [PATCH 278/482] updated CIM-AC example --- examples/cim/ac/config.yml | 88 ++++++++++++++++++++------------------ examples/cim/ac/main.py | 59 ++++++++++++++----------- examples/cim/dqn/main.py | 2 +- maro/rl/algorithm/ac.py | 9 ++-- 4 files changed, 89 insertions(+), 69 deletions(-) diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml index e1f1f505f..37f1d4997 100644 --- a/examples/cim/ac/config.yml +++ b/examples/cim/ac/config.yml @@ -1,40 +1,44 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +experiment_name: cim_ac +mode: multi-process # local, multi-process +num_episodes: 50 +num_steps: -1 env: - scenario: cim - topology: toy.4p_ssdd_l0.0 - durations: 1120 -max_episode: 50 -shaping: - port_attributes: - - empty - - full - - on_shipper - - on_consignee - - booking - - shortage - - fulfillment - vessel_attributes: - - empty - - full - - remaining_space - num_actions: 21 - # Parameters for computing states - look_back: 7 - max_ports_downstream: 2 - # Parameters for computing actions - finite_vessel_space: true - has_early_discharge: true - # Parameters for computing rewards - reward_eval_delay: 99 - fulfillment_factor: 1.0 - shortage_factor: 1.0 - time_decay: 0.97 + basic: + scenario: cim + topology: toy.4p_ssdd_l0.0 + durations: 1120 + wrapper: + port_attributes: + - empty + - full + - on_shipper + - on_consignee + - booking + - shortage + - fulfillment + vessel_attributes: + - empty + - full + - remaining_space + num_actions: 21 + # Parameters for computing states + look_back: 7 + max_ports_downstream: 2 + # Parameters for computing actions + finite_vessel_space: true + has_early_discharge: true + # Parameters for computing rewards + reward_eval_delay: 99 + fulfillment_factor: 1.0 + shortage_factor: 1.0 + time_decay: 0.97 policy: model: - actor: - network: + network: + actor: hidden_dims: - 256 - 128 @@ -43,12 +47,7 @@ policy: softmax: true batch_norm: False head: true - optimization: - optim_cls: adam - optim_params: - lr: 0.001 - critic: - network: + critic: hidden_dims: - 256 - 128 @@ -57,19 +56,26 @@ policy: softmax: false batch_norm: true head: true - optimization: + optimization: + actor: + optim_cls: adam + optim_params: + lr: 0.001 + critic: optim_cls: rmsprop optim_params: lr: 0.001 algorithm_config: reward_discount: .0 critic_loss_cls: smooth_l1 - num_steps: 10 + train_epochs: 10 actor_loss_coefficient: 0.1 # clip_ratio: 0.8 # for PPO experience_manager: - capacity: 100000 + capacity: 400 overwrite_type: "rolling" + batch_size: -1 + replace: False update_trigger: min_new_experiences: 1 - num_warmup_experiences: 1 + num_warmup_experiences: 1 \ No newline at end of file diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index 011071497..68c777ed1 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -6,10 +6,10 @@ from os.path import dirname, join, realpath import numpy as np +import torch from maro.rl import ( - Actor, ActorCritic, ActorCriticConfig, FullyConnectedBlock, MultiAgentWrapper, Learner, Scheduler, - SimpleMultiHeadModel, OptimOption + ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceManager, FullyConnectedBlock, LocalLearner, OptimOption ) from maro.simulator import Env from maro.utils import set_seeds @@ -23,37 +23,48 @@ # model input and output dimensions IN_DIM = ( - (config["shaping"]["look_back"] + 1) * - (config["shaping"]["max_ports_downstream"] + 1) * - len(config["shaping"]["port_attributes"]) + - len(config["shaping"]["vessel_attributes"]) + (config["env"]["wrapper"]["look_back"] + 1) * + (config["env"]["wrapper"]["max_ports_downstream"] + 1) * + len(config["env"]["wrapper"]["port_attributes"]) + + len(config["env"]["wrapper"]["vessel_attributes"]) ) -OUT_DIM = config["shaping"]["num_actions"] +OUT_DIM = config["env"]["wrapper"]["num_actions"] -def get_ac_policy(): - cfg = config["agent"] - actor_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["actor"]) - critic_net = FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **cfg["model"]["critic"]) - ac_net = SimpleMultiHeadModel( - {"actor": actor_net, "critic": critic_net}, +def get_ac_policy(name): + class MyACNET(DiscreteACNet): + def forward(self, states, actor: bool = True, critic: bool = True): + states = torch.from_numpy(np.asarray(states)) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + + states = states.to(self.device) + return ( + self.component["actor"](states) if actor else None, + self.component["critic"](states) if critic else None + ) + + cfg = config["policy"] + ac_net = MyACNET( + component={ + "actor": FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["network"]["actor"]), + "critic": FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **cfg["model"]["network"]["critic"]) + }, optim_option={ - "actor": OptimOption(**cfg["optimization"]["actor"]), - "critic": OptimOption(**cfg["optimization"]["critic"]) + "actor": OptimOption(**cfg["model"]["optimization"]["actor"]), + "critic": OptimOption(**cfg["model"]["optimization"]["critic"]) } ) - return ActorCritic(ac_net, ActorCriticConfig(**cfg["algorithm"])) + experience_manager = ExperienceManager(**cfg["experience_manager"]) + return ActorCritic(name, ac_net, experience_manager, ActorCriticConfig(**cfg["algorithm_config"])) # Single-threaded launcher if __name__ == "__main__": set_seeds(1024) # for reproducibility - env = Env(**config["training"]["env"]) - agent = MultiAgentWrapper({name: get_ac_agent() for name in env.agent_idx_list}) - scheduler = Scheduler(config["training"]["max_episode"]) - learner = Learner( - CIMEnvWrapper(env, **config["shaping"]), agent, scheduler, - agent_update_interval=config["training"]["agent_update_interval"], - log_env_metrics=True - ) + env = Env(**config["env"]["basic"]) + env_wrapper = CIMEnvWrapper(env, **config["env"]["wrapper"]) + policies = [get_ac_policy(id_) for id_ in env.agent_idx_list] + agent2policy = {agent_id: agent_id for agent_id in env.agent_idx_list} + learner = LocalLearner(env_wrapper, policies, agent2policy, 40) # 40 episodes learner.run() diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 2445217ff..685509610 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -71,7 +71,6 @@ def local_learner_mode(): eval_schedule=config["eval_schedule"], log_dir=log_dir ) - local_learner.run() @@ -116,6 +115,7 @@ def get_dqn_learner_process(): num_episodes=config["num_episodes"], log_dir=log_dir ) + time.sleep(5) learner.run() diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithm/ac.py index 8f13b483b..9eb39d8df 100644 --- a/maro/rl/algorithm/ac.py +++ b/maro/rl/algorithm/ac.py @@ -50,14 +50,15 @@ def __init__( class ActorCritic(AbsCorePolicy): """Actor Critic algorithm with separate policy and value models. + In accordance with its on-policy nature, the experiences manager is emptied at the end of each ``update()`` call. + References: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: name (str): Policy name. - ac_net (DiscreteACNet): Multi-task model that computes action distributions - and state values. + ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config: Configuration for the AC algorithm. @@ -65,6 +66,7 @@ class ActorCritic(AbsCorePolicy): warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. Defaults to 1. """ + def __init__( self, name: str, @@ -72,7 +74,7 @@ def __init__( experience_manager: ExperienceManager, config: ActorCriticConfig, update_trigger: int = 1, - warmup: int = 1, + warmup: int = 1 ): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") @@ -121,6 +123,7 @@ def update(self): self.ac_net.step(loss) + # Empty the experience manager due to the on-policy nature of the algorithm. self.experience_manager.clear() def set_state(self, policy_state): From 16928716344f63b4c715a586fbf29d23c02b32b8 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 1 Jun 2021 18:17:03 +0800 Subject: [PATCH 279/482] refined proxy retry logic --- maro/communication/proxy.py | 50 +++++++++++-------- .../communication/utils/default_parameters.py | 8 ++- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index bec4fdeb3..cee387e87 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -27,8 +27,10 @@ _PEER_INFO = namedtuple("PEER_INFO", ["hash_table_name", "expected_number"]) HOST = default_parameters.proxy.redis.host PORT = default_parameters.proxy.redis.port -MAX_RETRIES = default_parameters.proxy.redis.max_retries -BASE_RETRY_INTERVAL = default_parameters.proxy.redis.base_retry_interval +INITIAL_REDIS_CONNECT_RETRY_INTERVAL = default_parameters.proxy.redis.initial_retry_interval +MAX_REDIS_CONNECT_RETRIES = default_parameters.proxy.redis.max_retries +INITIAL_PEER_DISCOVERY_RETRY_INTERVAL = default_parameters.proxy.peer_discovery.initial_retry_interval +MAX_PEER_DISCOVERY_RETRIES = default_parameters.proxy.peer_discovery.max_retries DELAY_FOR_SLOW_JOINER = default_parameters.proxy.delay_for_slow_joiner ENABLE_REJOIN = default_parameters.proxy.peer_rejoin.enable # Only enable at real k8s cluster or grass cluster PEERS_CATCH_LIFETIME = default_parameters.proxy.peer_rejoin.peers_catch_lifetime @@ -55,8 +57,12 @@ class Proxy: Defaults to ``DriverType.ZMQ``. driver_parameters (Dict): The arguments for communication driver class initial. Defaults to None. redis_address (Tuple): Hostname and port of the Redis server. Defaults to ("localhost", 6379). - max_retries (int): Maximum number of retries before raising an exception. Defaults to 5. - retry_interval_base_value (float): The time interval between attempts. Defaults to 0.1. + initial_redis_connect_retry_interval: Base value for the wait time between retries to connect to Redis. + Retries follow the exponential backoff algorithm. Defaults to 0.1. + max_redis_connect_retries: Maximum number of retries to connect to Redis. Defaults to 5. + initial_peer_discovery_retry_interval: Base value for the wait time between retries to find peers. + Retries follow the exponential backoff algorithm. Defaults to 0.1. + max_peer_discovery_retries: Maximum number of retries to find peers. Defaults to 5. log_enable (bool): Open internal logger or not. Defaults to True. enable_rejoin (bool): Allow peers rejoin or not. Defaults to False, and must use with maro cli. minimal_peers Union[int, dict]: The minimal number of peers for each peer type. @@ -77,8 +83,10 @@ def __init__( driver_type: DriverType = DriverType.ZMQ, driver_parameters: dict = None, redis_address: Tuple = (HOST, PORT), - max_retries: int = MAX_RETRIES, - retry_interval_base_value: float = BASE_RETRY_INTERVAL, + initial_redis_connect_retry_interval: int = INITIAL_REDIS_CONNECT_RETRY_INTERVAL, + max_redis_connect_retries: int = MAX_REDIS_CONNECT_RETRIES, + initial_peer_discovery_retry_interval: int = INITIAL_PEER_DISCOVERY_RETRY_INTERVAL, + max_peer_discovery_retries: int = MAX_PEER_DISCOVERY_RETRIES, log_enable: bool = True, enable_rejoin: bool = ENABLE_REJOIN, minimal_peers: Union[int, dict] = MINIMAL_PEERS, @@ -97,8 +105,10 @@ def __init__( else: unique_id = str(uuid.uuid1()).replace("-", "") self._name = f"{self._component_type}_{unique_id}" - self._max_retries = max_retries - self._retry_interval_base_value = retry_interval_base_value + self._initial_redis_connect_retry_interval = initial_redis_connect_retry_interval + self._max_redis_connect_retries = max_redis_connect_retries + self._initial_peer_discovery_retry_interval = initial_peer_discovery_retry_interval + self._max_peer_discovery_retries = max_peer_discovery_retries self._log_enable = log_enable self._logger = InternalLogger(component_name=self._name + "_proxy") if self._log_enable else DummyLogger() @@ -115,27 +125,27 @@ def __init__( # Initialize connection to the redis server. self._redis_connection = redis.Redis(host=redis_address[0], port=redis_address[1], socket_keepalive=True) - num_tries = 0 - while num_tries < self._max_retries: + next_retry, success = self._initial_redis_connect_retry_interval, False + for _ in range(self._max_redis_connect_retries): try: self._redis_connection.ping() + success = True break except Exception as e: - retry_time = self._retry_interval_base_value * (2 ** num_tries) self._logger.error( - f"{self._name} failed to connect to the redis server due to {e}. Retrying in {retry_time} seconds." + f"{self._name} failed to connect to Redis due to {e}. Retrying in {next_retry} seconds." ) - num_tries += 1 - time.sleep(retry_time) + time.sleep(next_retry) + next_retry *= 2 - if num_tries == self._max_retries: - self._logger.error(f"{self._name} failed to connect to the redis server.") - sys.exit(NON_RESTART_EXIT_CODE) - else: + if success: self._logger.info( f"{self._name} is successfully connected to the redis server " f"at {redis_address[0]}:{redis_address[1]}." ) + else: + self._logger.error(f"{self._name} failed to connect to the redis server.") + sys.exit(NON_RESTART_EXIT_CODE) # Record the peer's redis information. self._peers_info_dict = {} @@ -245,8 +255,8 @@ def _get_peers_list(self): raise PeersMissError(f"Cannot get {self._name}\'s peers.") for peer_type, (peer_hash_name, num_expected) in self._peers_info_dict.items(): - registered_peers, next_retry = [], self._retry_interval_base_value - for _ in range(self._max_retries): + registered_peers, next_retry = [], self._initial_peer_discovery_retry_interval + for _ in range(self._max_peer_discovery_retries): if self._redis_connection.hlen(peer_hash_name) >= num_expected: registered_peers = [peer.decode() for peer in self._redis_connection.hkeys(peer_hash_name)] if len(registered_peers) > num_expected: diff --git a/maro/communication/utils/default_parameters.py b/maro/communication/utils/default_parameters.py index a1e9a70f0..2a7e2c92d 100644 --- a/maro/communication/utils/default_parameters.py +++ b/maro/communication/utils/default_parameters.py @@ -6,11 +6,15 @@ proxy = convert_dottable({ "fault_tolerant": False, "delay_for_slow_joiner": 3, + "peer_discovery": { + "initial_retry_interval": 0.1, + "max_retries": 10 + }, "redis": { "host": "localhost", "port": 6379, - "max_retries": 10, - "base_retry_interval": 0.1 + "initial_retry_interval": 0.1, + "max_retries": 10 }, "peer_rejoin": { "enable": False, From 2a3e90d16e5b0f974e7f823cd9b4863cd492f287 Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Tue, 1 Jun 2021 11:28:33 +0000 Subject: [PATCH 280/482] call policy update only for AbsCorePolicy --- maro/rl/training/local_learner.py | 5 +++-- maro/rl/training/policy_manager.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py index 4e36fd52a..c6acc548f 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -8,7 +8,7 @@ from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.exploration import AbsExploration -from maro.rl.policy import AbsPolicy +from maro.rl.policy import AbsPolicy, AbsCorePolicy from maro.utils import Logger from .early_stopper import AbsEarlyStopper @@ -141,7 +141,8 @@ def _train(self, ep: int): while self.env.state: segment += 1 for agent_id, exp in self._collect(ep, segment).items(): - self._policy[agent_id].on_experiences(exp) + if isinstance(self._policy[agent_id], AbsCorePolicy): + self._policy[agent_id].on_experiences(exp) # update the exploration parameters if an episode is finished if self.exploration_dict: diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index a3fb974e9..63c3da80f 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -7,7 +7,7 @@ from typing import Dict, List from maro.rl.experience import ExperienceSet -from maro.rl.policy import AbsPolicy +from maro.rl.policy import AbsPolicy, AbsCorePolicy from maro.utils import Logger @@ -64,8 +64,9 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): policy's experience manager. Policies whose update conditions have been met will then be updated. """ for policy_name, exp in exp_by_policy.items(): - if self.policy_dict[policy_name].on_experiences(exp): - self._updated_policy_names.add(policy_name) + if isinstance(self.policy_dict[policy_name], AbsCorePolicy): + if self.policy_dict[policy_name].on_experiences(exp): + self._updated_policy_names.add(policy_name) if self._updated_policy_names: self._logger.info(f"Updated policies {self._updated_policy_names}") From 2e925abae4aea8a08c8c9dcc4e825f6a08189f16 Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Wed, 2 Jun 2021 07:02:39 +0000 Subject: [PATCH 281/482] add limitation of AbsCorePolicy in Actor.collect() --- maro/rl/training/actor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 835fdb54f..58c223626 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -8,7 +8,7 @@ from maro.communication import Proxy from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.exploration import AbsExploration -from maro.rl.policy import AbsPolicy +from maro.rl.policy import AbsCorePolicy, AbsPolicy from maro.utils import Logger from .message_enums import MsgKey, MsgTag @@ -141,7 +141,9 @@ def _collect(self, msg): self.policy[agent_id].experience_manager.put(exp) ret_exp = { - policy_name: policy.experience_manager.get() for policy_name, policy in self.policy_dict.items() + policy_name: policy.experience_manager.get() + for policy_name, policy in self.policy_dict.items() + if isinstance(policy, AbsCorePolicy) } return_info = { From 03c26b811e65a2b32c41c1f9fdc5e79f453b27f7 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 2 Jun 2021 09:23:04 +0000 Subject: [PATCH 282/482] refined actor to return only experiences for policies that received new experiences --- maro/rl/training/actor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 58c223626..5d7d59bd7 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -136,15 +136,14 @@ def _collect(self, msg): f"Roll-out finished for ep {episode_index}, segment {segment_index}" f"(steps {starting_step_index} - {self.env.step_index})" ) + + policies_with_new_exp = set() exp_by_agent = self.env.get_experiences() for agent_id, exp in exp_by_agent.items(): self.policy[agent_id].experience_manager.put(exp) + policies_with_new_exp.add(self.agent2policy[agent_id]) - ret_exp = { - policy_name: policy.experience_manager.get() - for policy_name, policy in self.policy_dict.items() - if isinstance(policy, AbsCorePolicy) - } + ret_exp = {name: self.policy_dict[name].experience_manager.get() for name in policies_with_new_exp} return_info = { MsgKey.EPISODE_END: not self.env.state, From aff0f44cccaf14e24c6ed75d568a08aed2d0dbca Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Wed, 2 Jun 2021 10:25:20 +0000 Subject: [PATCH 283/482] fix MsgKey issue in rollout_manager --- maro/rl/training/rollout_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 716f3f374..2fd5a11a5 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -27,7 +27,7 @@ def __init__(self): @abstractmethod def collect(self, ep: int, segment: int, policy_state_dict: dict): """Collect simulation data, i.e., experiences for training. - + Args: ep (int): Current episode index. segment (int): Current segment index. @@ -249,7 +249,7 @@ def __init__( group: str, num_steps: int = -1, max_receive_attempts: int = None, - receive_timeout: int = None, + receive_timeout: int = None, max_staleness: int = 0, num_eval_actors: int = 1, log_env_summary: bool = True, @@ -348,7 +348,7 @@ def evaluate(self, policy_state_dict: dict): ) continue - env_summary_dict[msg.source] = msg.body[MsgKey.METRICS] + env_summary_dict[msg.source] = msg.body[MsgKey.ENV_SUMMARY] if msg.body[MsgKey.EPISODE_INDEX] == self._eval_ep: num_finishes += 1 From 8c055623b11f7eecc2262f4abd05f56854a6a10d Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Wed, 2 Jun 2021 10:32:37 +0000 Subject: [PATCH 284/482] fix typo in learner --- maro/rl/training/learner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 7b596a28f..5d85920c6 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -16,7 +16,7 @@ class Learner: This should be used in multi-process or distributed settings where either the policy manager or the roll-out manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such - as duplicate experience storage. Use ``LocalLearner`` instead. + as duplicate experience storage. Use ``LocalLearner`` instead. Args: policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. @@ -81,7 +81,7 @@ def run(self): self._eval_point_index += 1 env_metrics_dict = self.rollout_manager.evaluate(self.policy_manager.get_state()) # performance details - self._logger.info(f"ep {ep}: {env_metrics_dict}") + self.logger.info(f"ep {ep}: {env_metrics_dict}") # early stopping check if self.early_stopper: self.early_stopper.push(self.eval_env.metrics) From cc0c555e0ee96279dcf83f328eb8f72016e7942e Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Thu, 3 Jun 2021 03:20:01 +0000 Subject: [PATCH 285/482] call exit function for parallel rollout manager --- maro/rl/training/learner.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 5d85920c6..d4b2aa49d 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -8,7 +8,7 @@ from .early_stopper import AbsEarlyStopper from .policy_manager import AbsPolicyManager -from .rollout_manager import AbsRolloutManager +from .rollout_manager import AbsRolloutManager, ParallelRolloutManager class Learner: @@ -86,7 +86,10 @@ def run(self): if self.early_stopper: self.early_stopper.push(self.eval_env.metrics) if self.early_stopper.stop(): - return + break + + if isinstance(self.rollout_manager, ParallelRolloutManager): + self.rollout_manager.exit() def _train(self, ep: int): num_experiences_collected = 0 From 034e5bbe99dadbb34d3ef0adba9ee41e8e41e181 Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Thu, 3 Jun 2021 03:24:12 +0000 Subject: [PATCH 286/482] update supply chain example distributed training scripts --- examples/supply_chain/config.yml | 13 +++++++++++-- examples/supply_chain/main.py | 5 +++-- examples/supply_chain/render_tools.py | 7 +++++-- examples/supply_chain/scripts/build.sh | 5 ++++- .../scripts/docker_compose_yml_generator.py | 4 ++-- examples/supply_chain/scripts/kill.sh | 4 +++- examples/supply_chain/scripts/run.sh | 6 ++++-- scripts/install_maro.sh | 4 ++-- setup.py | 2 ++ 9 files changed, 36 insertions(+), 14 deletions(-) diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml index 765b4fb8f..bc9685049 100644 --- a/examples/supply_chain/config.yml +++ b/examples/supply_chain/config.yml @@ -1,19 +1,26 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +experiment_name: test + env: scenario: supply_chain # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder - # that contains a single config.yml and shoud be placed under examples/supply_chain/envs/ + # that contains a single config.yml and should be placed under examples/supply_chain/envs/ topology: test durations: 64 # number of ticks per episode + num_episodes: 1000 # number of episodes to simulate + # Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps # before returning experiences to the learner. The learner uses these experiences to update the agents' policies # and sync the updated policies to the actors for the next learning cycle. experience_update_interval: 64 + eval_schedule: 100 -log_env_metrics: false + +log_env_summary: true + policy: consumerstore: algorithm: dqn @@ -65,10 +72,12 @@ policy: model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. network: output_dim: 10 + exploration: last_ep: 800 initial_value: 0.8 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. final_value: 0.0 + distributed: # this is used to group all actor / learner processes belonging to the same job for communication purposes. # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. diff --git a/examples/supply_chain/main.py b/examples/supply_chain/main.py index f5854aab0..0e84fde3f 100644 --- a/examples/supply_chain/main.py +++ b/examples/supply_chain/main.py @@ -28,6 +28,7 @@ with open(config_path, "r") as config_file: CONFIG = yaml.safe_load(config_file) LOG_DIR = os.path.join(SC_CODE_DIR, "logs", CONFIG["experiment_name"]) +OUTPUT_DIR = os.path.join(LOG_DIR, "output") # AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id", "parent_id")) def agent_info_to_agent_id(info: AgentInfo) -> str: @@ -55,7 +56,7 @@ def single_thread_mode(config, env_wrapper): tracker = SimulationTracker(60, 1, env_wrapper, learner) tracker.run_and_render( - loc_path=os.path.join(SC_CODE_DIR, "output"), + loc_path=OUTPUT_DIR, facility_types=["productstore"] ) @@ -152,7 +153,7 @@ def multi_process_mode(config): print(f"******** [multi-process mode] automatically change the 'redis_host' field to 'localhost' ********") multi_process_mode(CONFIG) elif args.whoami == 1: - sc_learner() + sc_learner(CONFIG) elif args.whoami == 2: component_name = os.getenv("COMPONENT") sc_actor(component_name, CONFIG, env_wrapper) diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py index 30e0d1365..50def780b 100644 --- a/examples/supply_chain/render_tools.py +++ b/examples/supply_chain/render_tools.py @@ -1,7 +1,8 @@ -import numpy as np import matplotlib.pyplot as plt +import numpy as np import os -from env_wrapper import sku_agent_types + +from examples.supply_chain.env_wrapper import sku_agent_types class SimulationTracker: @@ -161,6 +162,8 @@ def run_wth_render(self, facility_types): return np.sum(_step_metrics), _step_metrics_list def run_and_render(self, loc_path, facility_types): + if not os.path.exists(loc_path): + os.makedirs(loc_path) metric, metric_list = self.run_wth_render( facility_types=facility_types) self.render('%s/plot_balance.png' % diff --git a/examples/supply_chain/scripts/build.sh b/examples/supply_chain/scripts/build.sh index d86ecb01f..6f7a6522f 100644 --- a/examples/supply_chain/scripts/build.sh +++ b/examples/supply_chain/scripts/build.sh @@ -1,5 +1,8 @@ #!/bin/bash +BASEDIR=$(dirname "$0") +ROOTDIR=$BASEDIR/../../../ + # script to build the docker image for running the supply chain scenario. docker pull redis:6 -docker build -f ../../../docker_files/dev.df -t maro-sc:latest ../../../ +docker build -f $ROOTDIR/docker_files/dev.df -t maro-sc:latest $ROOTDIR diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py index cb3a44723..9a1702def 100644 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ b/examples/supply_chain/scripts/docker_compose_yml_generator.py @@ -27,11 +27,11 @@ "image": "maro-sc", "container_name": "learner", "volumes": [ - f"{sc_code_dir}:/maro/supply_chain", + f"{sc_code_dir}:/maro/examples/supply_chain", f"{maro_rl_dir}:/maro/maro/rl", f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" ], - "command": ["python3", "/maro/supply_chain/distributed_launcher.py", "-w", "1"] + "command": ["python3", "/maro/examples/supply_chain/main.py", "-w", "1"] } } } diff --git a/examples/supply_chain/scripts/kill.sh b/examples/supply_chain/scripts/kill.sh index 8603a50b5..a2716efbc 100644 --- a/examples/supply_chain/scripts/kill.sh +++ b/examples/supply_chain/scripts/kill.sh @@ -1,4 +1,6 @@ #!/bin/bash +BASEDIR=$(dirname "$0") + # script to kill a previously launcher supply chain training job. -docker-compose -f ../docker-compose.yml down +docker-compose -f $BASEDIR/../docker-compose.yml down diff --git a/examples/supply_chain/scripts/run.sh b/examples/supply_chain/scripts/run.sh index 073b30513..92ca64344 100644 --- a/examples/supply_chain/scripts/run.sh +++ b/examples/supply_chain/scripts/run.sh @@ -1,5 +1,7 @@ #!/bin/bash +BASEDIR=$(dirname "$0") + # script to run the supply chain scenario in single-host multi-container mode. -python3 docker_compose_yml_generator.py -docker-compose -f ../docker-compose.yml up +python3 $BASEDIR/docker_compose_yml_generator.py +docker-compose -f $BASEDIR/../docker-compose.yml up diff --git a/scripts/install_maro.sh b/scripts/install_maro.sh index d9ea10b60..a77d0811c 100644 --- a/scripts/install_maro.sh +++ b/scripts/install_maro.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Script to install maro in editable mode on linux/darwin, +# Script to install maro in editable mode on linux/darwin, # usually for development. if [[ "$OSTYPE" == "linux-gnu"* ]]; then @@ -16,4 +16,4 @@ pip install -r ./maro/requirements.build.txt bash scripts/compile_cython.sh # Install MARO in editable mode. -pip install -e . \ No newline at end of file +pip install -e . diff --git a/setup.py b/setup.py index c8e5c5c27..269258fed 100644 --- a/setup.py +++ b/setup.py @@ -141,6 +141,8 @@ "kubernetes>=12.0.1", "prompt_toolkit<3.1.0", "stringcase>=1.2.0", + "networkx>=2.4", + "scipy>=1.5.2" ], entry_points={ "console_scripts": [ From 13d7d9b07a13f139e070a7c2da48ce467677b9bc Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 3 Jun 2021 06:17:51 +0000 Subject: [PATCH 287/482] 1. moved exploration scheduling to rollout manager; 2. fixed bug in lr schedule registration in core model; 3. added parallel policy manager prorotype --- examples/cim/dqn/main.py | 18 +++++++------ maro/rl/model/core_model.py | 2 +- maro/rl/training/actor.py | 35 ++++++++++++++---------- maro/rl/training/learner.py | 15 ++++++----- maro/rl/training/message_enums.py | 4 +-- maro/rl/training/policy_manager.py | 42 +++++++++++++++++++++++++++++ maro/rl/training/rollout_manager.py | 22 ++++++++++++++- maro/rl/training/trainer.py | 22 +++++++++++++++ 8 files changed, 127 insertions(+), 33 deletions(-) create mode 100644 maro/rl/training/trainer.py diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 685509610..c4848b6c3 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -78,18 +78,12 @@ def get_dqn_actor_process(): env = Env(**config["env"]["basic"]) num_actions = config["env"]["wrapper"]["num_actions"] policy_list = [get_independent_policy(policy_id=i, training=False) for i in env.agent_idx_list] - epsilon_greedy = EpsilonGreedyExploration(num_actions=num_actions) - epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - **config["exploration"] - ) actor = Actor( env=CIMEnvWrapper(env, **config["env"]["wrapper"]), policies=policy_list, agent2policy={i: i for i in env.agent_idx_list}, group=config["multi-process"]["group"], - exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, + exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=num_actions)}, agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, log_dir=log_dir, redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) @@ -100,11 +94,19 @@ def get_dqn_actor_process(): def get_dqn_learner_process(): env = Env(**config["env"]["basic"]) policy_list = [get_independent_policy(policy_id=i) for i in env.agent_idx_list] - policy_manager = LocalPolicyManager(policies=policy_list, log_dir=log_dir) + + epsilon_greedy = EpsilonGreedyExploration(num_actions=config["env"]["wrapper"]["num_actions"]) + epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + last_ep=config["num_episodes"], + **config["exploration"] + ) rollout_manager = ParallelRolloutManager( num_actors=config["multi-process"]["num_actors"], group=config["multi-process"]["group"], + exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, log_dir=log_dir, redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) ) diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index b594a7230..d6f851971 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -66,7 +66,7 @@ def __init__( param.requires_grad = False else: if isinstance(optim_option, dict): - self.optimizer = {} + self.optimizer, self.scheduler = {}, {} for name, opt in optim_option.items(): self.optimizer[name] = opt.optim_cls(self.component[name].parameters(), **opt.optim_params) if opt.scheduler_cls: diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 5d7d59bd7..cd4740c01 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -101,21 +101,33 @@ def run(self): def _collect(self, msg): episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] + # load policies + self._load_policy_states(msg.body[MsgKey.POLICY]) + # set exploration parameters + exploration_params = None + if MsgKey.EXPLORATION in msg.body: + updated_exploration_param_names = {} + for exploration_name, param_dict in msg.body[MsgKey.EXPLORATION].items(): + updated_exploration_param_names[exploration_name] = param_dict.keys() + for param_name, value in param_dict.items(): + setattr(self.exploration_dict[exploration_name], param_name, value) + + exploration_params = { + agent_ids: { + param_name: getattr(self.exploration_dict[exploration_name], param_name) + for param_name in updated_exploration_param_names[exploration_name] + } + for exploration_name, agent_ids in self.agent_groups_by_exploration.items() + } + if self.env.state is None: self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") - if hasattr(self, "exploration_dict"): - exploration_params = { - agent_ids: self.exploration_dict[exploration_id].parameters - for exploration_id, agent_ids in self.agent_groups_by_exploration.items() - } - self._logger.debug(f"Exploration parameters: {exploration_params}") + if hasattr(self, "exploration_dict"): + self._logger.info(f"Exploration parameters: {exploration_params}") self.env.reset() self.env.start() # get initial state - # load policies - self._load_policy_states(msg.body[MsgKey.POLICY]) - starting_step_index = self.env.step_index + 1 steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while self.env.state and steps_to_go > 0: @@ -154,11 +166,6 @@ def _collect(self, msg): MsgKey.NUM_STEPS: self.env.step_index - starting_step_index + 1 } - if not self.env.state: - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() - self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) def _evaluate(self, msg): diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 5d85920c6..9bf322234 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -23,14 +23,14 @@ class Learner: rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data collection. num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on how the implementation of the roll-out manager. + collect-update cycles, depending on the implementation of the roll-out manager. eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated at the end of the training episodes given in the list. In any case, the policies will be evaluated at the end of the last training episode. Defaults to None, in which case the policies will only be evaluated after the last training episode. early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metrics are met following an evaluation episode. Default to None. + environment metric are met following an evaluation episode. Default to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -79,14 +79,15 @@ def run(self): self._train(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 - env_metrics_dict = self.rollout_manager.evaluate(self.policy_manager.get_state()) + env_metric_dict = self.rollout_manager.evaluate(self.policy_manager.get_state()) # performance details - self.logger.info(f"ep {ep}: {env_metrics_dict}") + self.logger.info(f"ep {ep}: {env_metric_dict}") # early stopping check if self.early_stopper: - self.early_stopper.push(self.eval_env.metrics) - if self.early_stopper.stop(): - return + for env_metric in env_metric_dict.values(): + self.early_stopper.push(env_metric) + if self.early_stopper.stop(): + return def _train(self, ep: int): num_experiences_collected = 0 diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index 00d345ccb..5957000a5 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -7,10 +7,9 @@ class MsgTag(Enum): COLLECT = "rollout" EVAL = "eval" - AGENT_UPDATE = "agent_update" + POLICY_UPDATE = "agent_update" CHOOSE_ACTION = "choose_action" ACTION = "action" - EXPERIENCE_SYNC = "experience_sync" TRAIN = "train" ABORT_ROLLOUT = "abort_rollout" EVAL_DONE = "eval_done" @@ -29,6 +28,7 @@ class MsgKey(Enum): NUM_EXPERIENCES = "num_experiences" STATE = "state" POLICY = "policy" + EXPLORATION = "exploration" VERSION = "version" NUM_STEPS = "num_steps" EPISODE_END = "episode_end" diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index 63c3da80f..8255fefe3 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -6,10 +6,13 @@ from os import getcwd from typing import Dict, List +from maro.communication import Proxy, SessionType from maro.rl.experience import ExperienceSet from maro.rl.policy import AbsPolicy, AbsCorePolicy from maro.utils import Logger +from .message_enums import MsgKey, MsgTag + class AbsPolicyManager(ABC): """Controller for policy updates. @@ -78,3 +81,42 @@ def get_state(self): } self._updated_policy_names.clear() return policy_state_dict + + +class ParallelPolicyManager(AbsPolicyManager): + def __init__( + self, + policy2trainer: Dict[str, str], + group: str, + log_dir: str = getcwd(), + **proxy_kwargs + ): + super().__init__() + self._logger = Logger("PARALLEL_POLICY_MANAGER", dump_folder=log_dir) + self.policy2trainer = policy2trainer + self._names = list(self.policy2trainer.keys()) + peers = {"trainer": len(set(self.policy2trainer.values()))} + self._proxy = Proxy(group, "policy_manager", peers, **proxy_kwargs) + + @property + def names(self): + return self._names + + def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + msg_body_by_dest = defaultdict(dict) + for policy_name, exp in exp_by_policy.items(): + trainer_id = self.policy2trainer[policy_name] + if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp + + self._proxy.iscatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())) + + def get_state(self): + policy_state_dict = {} + for msg in self._proxy.receive(): + if msg.tag == MsgTag.POLICY_UPDATE: + for policy_name, state in msg.body[MsgKey.POLICY].items(): + policy_state_dict[policy_name] = state + + return policy_state_dict diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 2fd5a11a5..752e89692 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -224,6 +224,8 @@ class ParallelRolloutManager(AbsRolloutManager): num_actors (int): Number of remote roll-out actors. group (str): Identifier of the group to which the actor belongs. It must be the same group name assigned to the learner (and decision clients, if any). + exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. The exploration parameters + from these instances will be broadcast to all actors. Defaults to None. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. max_receive_attempts (int): Maximum number of attempts to receive actor results in ``collect``. Defaults to @@ -247,6 +249,7 @@ def __init__( self, num_actors: int, group: str, + exploration_dict: Dict[str, AbsExploration] = None, num_steps: int = -1, max_receive_attempts: int = None, receive_timeout: int = None, @@ -266,6 +269,9 @@ def __init__( self._proxy = Proxy(group, "rollout_manager", peers, **proxy_kwargs) self._actors = self._proxy.peers["actor"] # remote actor ID's + self.exploration_dict = exploration_dict + self._num_steps = num_steps + if max_receive_attempts is None: max_receive_attempts = self.num_actors self._logger.info(f"Maximum receive attempts is set to {max_receive_attempts}") @@ -273,7 +279,6 @@ def __init__( self.max_receive_attempts = max_receive_attempts self.receive_timeout = receive_timeout - self._num_steps = num_steps self._max_staleness = max_staleness self.total_experiences_collected = 0 self.total_env_steps = 0 @@ -282,6 +287,8 @@ def __init__( self._num_eval_actors = num_eval_actors self._eval_ep = 0 + self._exploration_update = True + def collect(self, episode_index: int, segment_index: int, policy_state_dict: dict): """Collect simulation data, i.e., experiences for training.""" if self._log_env_summary: @@ -293,6 +300,13 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic MsgKey.NUM_STEPS: self._num_steps, MsgKey.POLICY: policy_state_dict } + + if self._exploration_update: + msg_body[MsgKey.EXPLORATION] = { + name: exploration.parameters for name, exploration in self.exploration_dict.items() + } + self._exploration_update = False + self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) self._logger.info(f"Sent collect requests to {self._actors} for ep-{episode_index}, segment-{segment_index}") @@ -325,6 +339,12 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic num_finishes += 1 if num_finishes == self.num_actors: break + + if self.episode_complete: + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + self._exploration_update = True return combined_exp_by_policy diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py new file mode 100644 index 000000000..f9dce6cc8 --- /dev/null +++ b/maro/rl/training/trainer.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from collections import defaultdict +from os import getcwd +from typing import Dict, List + +from maro.communication import Proxy, SessionType +from maro.rl.experience import ExperienceSet +from maro.rl.policy import AbsPolicy, AbsCorePolicy +from maro.utils import Logger + +from .message_enums import MsgKey, MsgTag + + +class Trainer: + def __init__(self) -> None: + pass + + def run(self): + pass From 6dbfd3656cd3bf872629626a128109c19e50be37 Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Thu, 3 Jun 2021 06:42:35 +0000 Subject: [PATCH 288/482] reformat render --- .../supply_chain/or_policy/base_policy.py | 5 +- examples/supply_chain/or_policy/eoq_policy.py | 3 + .../supply_chain/or_policy/minmax_policy.py | 3 + examples/supply_chain/render_tools.py | 94 ++++++++++--------- 4 files changed, 58 insertions(+), 47 deletions(-) diff --git a/examples/supply_chain/or_policy/base_policy.py b/examples/supply_chain/or_policy/base_policy.py index 7c9cd9a88..bcad01a89 100644 --- a/examples/supply_chain/or_policy/base_policy.py +++ b/examples/supply_chain/or_policy/base_policy.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + import random import numpy as np @@ -32,7 +35,7 @@ def choose_action(self, state): inflight_orders = np.array(state['consumer_in_transit_orders']) booked_inventory = available_inventory + inflight_orders - # stop placing orders when the facilty runs out of capacity + # stop placing orders when the facility runs out of capacity if np.sum(booked_inventory) > state['storage_capacity']: return 0 diff --git a/examples/supply_chain/or_policy/eoq_policy.py b/examples/supply_chain/or_policy/eoq_policy.py index 84eecfe8b..15a11ae53 100644 --- a/examples/supply_chain/or_policy/eoq_policy.py +++ b/examples/supply_chain/or_policy/eoq_policy.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + import random as rnd import numpy as np diff --git a/examples/supply_chain/or_policy/minmax_policy.py b/examples/supply_chain/or_policy/minmax_policy.py index 96872c1fa..6e69d34a6 100644 --- a/examples/supply_chain/or_policy/minmax_policy.py +++ b/examples/supply_chain/or_policy/minmax_policy.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + import random as rnd import numpy as np diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py index 50def780b..29a237a76 100644 --- a/examples/supply_chain/render_tools.py +++ b/examples/supply_chain/render_tools.py @@ -1,26 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + import matplotlib.pyplot as plt import numpy as np import os -from examples.supply_chain.env_wrapper import sku_agent_types +from examples.supply_chain.env_wrapper import sku_agent_types, SCEnvWrapper class SimulationTracker: - def __init__(self, episod_len, n_episods, env, learner): - self.episod_len = episod_len - self.global_balances = np.zeros((n_episods, episod_len)) - self.global_rewards = np.zeros((n_episods, episod_len)) + def __init__(self, episode_len: int, n_episodes: int, env: SCEnvWrapper, learner): + self.episode_len = episode_len + self.n_episodes = n_episodes self.env = env self.learner = learner + + self.global_balances = np.zeros((n_episodes, episode_len)) + self.global_rewards = np.zeros((n_episodes, episode_len)) + self.facility_names = [] for agent in self.env._agent_list: if agent.agent_type in sku_agent_types: self.facility_names.append(agent) + self.step_balances = np.zeros( - (n_episods, self.episod_len, len(self.facility_names))) + (n_episodes, self.episode_len, len(self.facility_names))) self.step_rewards = np.zeros( - (n_episods, self.episod_len, len(self.facility_names))) - self.n_episods = n_episods + (n_episodes, self.episode_len, len(self.facility_names))) + self.sku_to_track = None self.stock_status = None self.stock_in_transit_status = None @@ -29,39 +36,37 @@ def __init__(self, episod_len, n_episods, env, learner): self.reward_discount_status = None self.order_to_distribute = None - def add_sample(self, episod, t, global_balance, global_reward, step_balances, step_rewards): - self.global_balances[episod, t] = global_balance - self.global_rewards[episod, t] = global_reward + def _add_sample(self, episode, t, global_balance, global_reward, step_balances, step_rewards): + self.global_balances[episode, t] = global_balance + self.global_rewards[episode, t] = global_reward for i, f in enumerate(self.facility_names): - self.step_balances[episod, t, i] = step_balances[f.id] - self.step_rewards[episod, t, i] = step_rewards[f.id] + self.step_balances[episode, t, i] = step_balances[f.id] + self.step_rewards[episode, t, i] = step_rewards[f.id] - def add_sku_status(self, episod, t, stock, order_in_transit, demands, rewards, balances, order_to_distribute): + def _add_sku_status(self, episode, t, stock, order_in_transit, demands, rewards, balances, order_to_distribute): if self.sku_to_track is None: self.sku_to_track = list(rewards.keys()) self.stock_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) + (self.n_episodes, self.episode_len, len(self.sku_to_track))) self.stock_in_transit_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) + (self.n_episodes, self.episode_len, len(self.sku_to_track))) self.demand_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) + (self.n_episodes, self.episode_len, len(self.sku_to_track))) self.reward_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) + (self.n_episodes, self.episode_len, len(self.sku_to_track))) self.balance_status = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) + (self.n_episodes, self.episode_len, len(self.sku_to_track))) self.order_to_distribute = np.zeros( - (self.n_episods, self.episod_len, len(self.sku_to_track))) + (self.n_episodes, self.episode_len, len(self.sku_to_track))) for i, sku_name in enumerate(self.sku_to_track): - self.stock_status[episod, t, i] = stock[sku_name] - self.stock_in_transit_status[episod, - t, i] = order_in_transit[sku_name] - self.demand_status[episod, t, i] = demands[sku_name] - self.reward_status[episod, t, i] = rewards[sku_name] - self.balance_status[episod, t, i] = balances[sku_name] - self.order_to_distribute[episod, t, - i] = order_to_distribute[sku_name] - - def render_sku(self, loc_path): + self.stock_status[episode, t, i] = stock[sku_name] + self.stock_in_transit_status[episode, t, i] = order_in_transit[sku_name] + self.demand_status[episode, t, i] = demands[sku_name] + self.reward_status[episode, t, i] = rewards[sku_name] + self.balance_status[episode, t, i] = balances[sku_name] + self.order_to_distribute[episode, t, i] = order_to_distribute[sku_name] + + def _render_sku(self, loc_path): sku_name_dict = {} facility_type_dict = {} for agent in self.env._agent_list: @@ -74,7 +79,7 @@ def render_sku(self, loc_path): for i, sku_name in enumerate(self.sku_to_track): fig, ax = plt.subplots(4, 1, figsize=(25, 10)) - x = np.linspace(0, self.episod_len, self.episod_len) + x = np.linspace(0, self.episode_len, self.episode_len) stock = self.stock_status[0, :, i] order_in_transit = self.stock_in_transit_status[0, :, i] demand = self.demand_status[0, :, i] @@ -96,12 +101,12 @@ def render_sku(self, loc_path): ax[3].plot(x, demand, label="Demand") fig.legend() - fig.savefig(f"{loc_path}/{facility_type_dict[sku_name]}_{sku_name_dict[sku_name]}.png") + fig.savefig(os.path.join(loc_path, f"{facility_type_dict[sku_name]}_{sku_name_dict[sku_name]}.png")) plt.close(fig=fig) - def render(self, file_name, metrics, facility_types): + def _render(self, file_name, metrics, facility_types): fig, axs = plt.subplots(2, 1, figsize=(25, 10)) - x = np.linspace(0, self.episod_len, self.episod_len) + x = np.linspace(0, self.episode_len, self.episode_len) _agent_list = [] _step_idx = [] @@ -117,7 +122,7 @@ def render(self, file_name, metrics, facility_types): axs[0].set_title('Cumulative Sum') axs[0].plot(x, np.cumsum(np.sum(_step_metrics, axis=0))) - axs[1].set_title('Breakdown by Agent (One Episod)') + axs[1].set_title('Breakdown by Agent (One Episode)') axs[1].plot(x, np.cumsum(_step_metrics, axis=1).T) axs[1].legend(_agent_list, loc='upper left') @@ -125,10 +130,10 @@ def render(self, file_name, metrics, facility_types): plt.close(fig=fig) # plt.show() - def run_wth_render(self, facility_types): + def _run_wth_render(self, facility_types): self.env.reset() self.env.start() - for epoch in range(self.episod_len): + for epoch in range(self.episode_len): action = {id_: self.learner._policy[id_].choose_action(st) for id_, st in self.env.state.items()} self.learner._logger.info(f"epoch: {epoch}, action: {action}") self.env.step(action) @@ -139,7 +144,7 @@ def run_wth_render(self, facility_types): step_balances = self.env.balance_status step_rewards = self.env.reward_status - self.add_sample(0, epoch, sum(step_balances.values()), sum( + self._add_sample(0, epoch, sum(step_balances.values()), sum( step_rewards.values()), step_balances, step_rewards) stock_status = self.env.stock_status order_in_transit_status = self.env.order_in_transit_status @@ -148,7 +153,7 @@ def run_wth_render(self, facility_types): balance_status = self.env.balance_status order_to_distribute_status = self.env.order_to_distribute_status - self.add_sku_status(0, epoch, stock_status, + self._add_sku_status(0, epoch, stock_status, order_in_transit_status, demand_status, reward_status, balance_status, order_to_distribute_status) @@ -164,11 +169,8 @@ def run_wth_render(self, facility_types): def run_and_render(self, loc_path, facility_types): if not os.path.exists(loc_path): os.makedirs(loc_path) - metric, metric_list = self.run_wth_render( - facility_types=facility_types) - self.render('%s/plot_balance.png' % - loc_path, self.step_balances, facility_types) - self.render('%s/plot_reward.png' % - loc_path, self.step_rewards, facility_types) - self.render_sku(loc_path) + metric, metric_list = self._run_wth_render(facility_types=facility_types) + self._render(os.path.join(loc_path, "plot_balance.png"), self.step_balances, facility_types) + self._render(os.path.join(loc_path, "plot_reward.png"), self.step_rewards, facility_types) + self._render_sku(loc_path) return metric, metric_list From 146eeb6a97759a51b4853b52326bf8d1fa839617 Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Thu, 3 Jun 2021 08:47:26 +0000 Subject: [PATCH 289/482] fix supply chain business engine action type problem --- examples/supply_chain/render_tools.py | 4 ++-- .../scenarios/supply_chain/business_engine.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py index 29a237a76..8643ed0ab 100644 --- a/examples/supply_chain/render_tools.py +++ b/examples/supply_chain/render_tools.py @@ -86,13 +86,13 @@ def _render_sku(self, loc_path): reward = self.reward_status[0, :, i] balance = self.balance_status[0, :, i] order_to_distribute = self.order_to_distribute[0, :, i] - ax[0].set_title('SKU Stock Status by Episod') + ax[0].set_title('SKU Stock Status by Episode') for y_label, y in [('stock', stock), ('order_in_transit', order_in_transit), ('order_to_distribute', order_to_distribute)]: ax[0].plot(x, y, label=y_label) - ax[1].set_title('SKU Reward / Balance Status by Episod') + ax[1].set_title('SKU Reward / Balance Status by Episode') ax[1].plot(x, balance, label='Balance') ax_r = ax[1].twinx() ax_r.plot(x, reward, label='Reward', color='r') diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index 033e0aa25..c17c713b3 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -136,17 +136,17 @@ def _build_world(self): def _on_action_received(self, event): action = event.payload - if action is not None and type(action) == dict and len(action) > 0: + if action is not None and type(action) == list and len(action) > 0: self._action_cache = action def _dispatch_action(self): if self._action_cache is not None: - # NOTE: we assume that the action is dictionary that key is the unit(agent) id, value is the real action. - for unit_id, action_obj in self._action_cache.items(): - entity = self.world.get_entity(unit_id) + # NOTE: we assume that the action_cache is a list of action, and each action has an id field. + for action in self._action_cache: + entity = self.world.get_entity(action.id) if entity is not None and issubclass(type(entity), UnitBase): - entity.set_action(action_obj) + entity.set_action(action) self._action_cache = None From a80f6c359d8dc969276d1481198b8d36f9d305ec Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Thu, 3 Jun 2021 09:03:07 +0000 Subject: [PATCH 290/482] reset supply chain example render figsize from 4 to 3 --- examples/supply_chain/render_tools.py | 15 +- examples/supply_chain/single_launcher.ipynb | 835 -------------------- 2 files changed, 9 insertions(+), 841 deletions(-) delete mode 100644 examples/supply_chain/single_launcher.ipynb diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py index 8643ed0ab..66718a2e4 100644 --- a/examples/supply_chain/render_tools.py +++ b/examples/supply_chain/render_tools.py @@ -78,7 +78,6 @@ def _render_sku(self, loc_path): facility_type_dict[agent.id] = self.env.env.summary["node_mapping"]["facilities"][agent.facility_id]['class'].__name__ for i, sku_name in enumerate(self.sku_to_track): - fig, ax = plt.subplots(4, 1, figsize=(25, 10)) x = np.linspace(0, self.episode_len, self.episode_len) stock = self.stock_status[0, :, i] order_in_transit = self.stock_in_transit_status[0, :, i] @@ -86,10 +85,14 @@ def _render_sku(self, loc_path): reward = self.reward_status[0, :, i] balance = self.balance_status[0, :, i] order_to_distribute = self.order_to_distribute[0, :, i] + + fig, ax = plt.subplots(3, 1, figsize=(25, 10)) ax[0].set_title('SKU Stock Status by Episode') - for y_label, y in [('stock', stock), - ('order_in_transit', order_in_transit), - ('order_to_distribute', order_to_distribute)]: + for y_label, y in [ + ('stock', stock), + ('order_in_transit', order_in_transit), + ('order_to_distribute', order_to_distribute) + ]: ax[0].plot(x, y, label=y_label) ax[1].set_title('SKU Reward / Balance Status by Episode') @@ -97,8 +100,8 @@ def _render_sku(self, loc_path): ax_r = ax[1].twinx() ax_r.plot(x, reward, label='Reward', color='r') - ax[3].set_title('SKU demand') - ax[3].plot(x, demand, label="Demand") + ax[2].set_title('SKU demand') + ax[2].plot(x, demand, label="Demand") fig.legend() fig.savefig(os.path.join(loc_path, f"{facility_type_dict[sku_name]}_{sku_name_dict[sku_name]}.png")) diff --git a/examples/supply_chain/single_launcher.ipynb b/examples/supply_chain/single_launcher.ipynb deleted file mode 100644 index d8c39154e..000000000 --- a/examples/supply_chain/single_launcher.ipynb +++ /dev/null @@ -1,835 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "acd7d48c", - "metadata": {}, - "outputs": [], - "source": [ - "from maro.simulator import Env\n", - "from pprint import pprint" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "681386af", - "metadata": {}, - "outputs": [], - "source": [ - "env = Env(scenario=\"supply_chain\", topology=\"sample\", durations=64)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "875f0f8a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[AgentInfo(id=1, agent_type='facility', is_facility=True, sku=None, facility_id=1, parent_id=None),\n", - " AgentInfo(id=8, agent_type='facility', is_facility=True, sku=None, facility_id=8, parent_id=None),\n", - " AgentInfo(id=17, agent_type='facility', is_facility=True, sku=None, facility_id=17, parent_id=None),\n", - " AgentInfo(id=28, agent_type='facility', is_facility=True, sku=None, facility_id=28, parent_id=None),\n", - " AgentInfo(id=6, agent_type='product', is_facility=False, sku={'init_stock': 100, 'product_unit_cost': 1, 'production_rate': 1, 'type': 'production', 'cost': 10, 'price': 10, 'vlt': 1, 'id': 3}, facility_id=1, parent_id=1),\n", - " AgentInfo(id=7, agent_type='producer', is_facility=False, sku={'init_stock': 100, 'product_unit_cost': 1, 'production_rate': 1, 'type': 'production', 'cost': 10, 'price': 10, 'vlt': 1, 'id': 3}, facility_id=1, parent_id=6),\n", - " AgentInfo(id=13, agent_type='product', is_facility=False, sku={'init_stock': 100, 'product_unit_cost': 1, 'production_rate': 1, 'type': 'production', 'cost': 10, 'price': 100, 'vlt': 1, 'id': 1}, facility_id=8, parent_id=8),\n", - " AgentInfo(id=14, agent_type='producer', is_facility=False, sku={'init_stock': 100, 'product_unit_cost': 1, 'production_rate': 1, 'type': 'production', 'cost': 10, 'price': 100, 'vlt': 1, 'id': 1}, facility_id=8, parent_id=13),\n", - " AgentInfo(id=15, agent_type='product', is_facility=False, sku={'init_stock': 100, 'production_rate': 1, 'type': 'material', 'cost': 10, 'price': 100, 'vlt': 1, 'id': 3}, facility_id=8, parent_id=8),\n", - " AgentInfo(id=16, agent_type='consumer', is_facility=False, sku={'init_stock': 100, 'production_rate': 1, 'type': 'material', 'cost': 10, 'price': 100, 'vlt': 1, 'id': 3}, facility_id=8, parent_id=15),\n", - " AgentInfo(id=22, agent_type='product', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 1}, facility_id=17, parent_id=17),\n", - " AgentInfo(id=23, agent_type='consumer', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 1}, facility_id=17, parent_id=22),\n", - " AgentInfo(id=24, agent_type='product', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 2}, facility_id=17, parent_id=17),\n", - " AgentInfo(id=25, agent_type='consumer', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 2}, facility_id=17, parent_id=24),\n", - " AgentInfo(id=26, agent_type='product', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 3}, facility_id=17, parent_id=17),\n", - " AgentInfo(id=27, agent_type='consumer', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 3}, facility_id=17, parent_id=26),\n", - " AgentInfo(id=30, agent_type='product', is_facility=False, sku={'price': 300, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 1}, facility_id=28, parent_id=28),\n", - " AgentInfo(id=31, agent_type='consumer', is_facility=False, sku={'price': 300, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 1}, facility_id=28, parent_id=30),\n", - " AgentInfo(id=33, agent_type='product', is_facility=False, sku={'price': 200, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 3}, facility_id=28, parent_id=28),\n", - " AgentInfo(id=34, agent_type='consumer', is_facility=False, sku={'price': 200, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 3}, facility_id=28, parent_id=33),\n", - " AgentInfo(id=36, agent_type='product', is_facility=False, sku={'price': 100, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 2}, facility_id=28, parent_id=28),\n", - " AgentInfo(id=37, agent_type='consumer', is_facility=False, sku={'price': 100, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 2}, facility_id=28, parent_id=36)]\n" - ] - } - ], - "source": [ - "pprint(env.agent_idx_list)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "8e4837de", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'event_payload': {},\n", - " 'node_detail': {'consumer': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'latest_consumptions': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'order_cost': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'order_product_cost': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'order_quantity': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'price': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'product_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_unit_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'purchased': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'received': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'reward_discount': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'total_purchased': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'total_received': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 7},\n", - " 'distribution': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'remaining_order_number': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'remaining_order_quantity': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 3},\n", - " 'facility': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'}},\n", - " 'number': 4},\n", - " 'manufacture': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'manufacturing_number': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_unit_cost': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'product_unit_id': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 2},\n", - " 'product': {'attributes': {'distribution_check_order': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'distribution_delay_order_penalty': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'distribution_transport_cost': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'price': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'product_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_unit_id': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 9},\n", - " 'seller': {'attributes': {'backlog_ratio': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'demand': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'price': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'product_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_unit_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'sold': {'slots': 1, 'type': 'uint'},\n", - " 'total_demand': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'total_sold': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 3},\n", - " 'storage': {'attributes': {'capacity': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'product_list': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_number': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'remaining_space': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 4},\n", - " 'vehicle': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'payload': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'unit_transport_cost': {'slots': 1,\n", - " 'type': 'float'}},\n", - " 'number': 6}},\n", - " 'node_mapping': {'agent_types': {'ConsumerUnit': 'consumer',\n", - " 'ManufactureUnit': 'producer',\n", - " 'ProductUnit': 'product',\n", - " 'RetailerFacility': 'facility',\n", - " 'StoreProductUnit': 'product',\n", - " 'SupplierFacility': 'facility',\n", - " 'WarehouseFacility': 'facility'},\n", - " 'facilities': {1: {'class': ,\n", - " 'configs': {'agent_type': 'facility',\n", - " 'delay_order_penalty': 10,\n", - " 'order_cost': 0},\n", - " 'downstreams': {3: [8, 17, 28]},\n", - " 'id': 1,\n", - " 'name': 'Supplier_001',\n", - " 'node_index': 0,\n", - " 'skus': {3: {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 10,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}},\n", - " 'units': {'distribution': {'children': [{'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 4,\n", - " 'node_index': 0,\n", - " 'node_name': 'vehicle'},\n", - " {'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 5,\n", - " 'node_index': 1,\n", - " 'node_name': 'vehicle'}],\n", - " 'class': ,\n", - " 'config': {'unit_price': 1},\n", - " 'id': 3,\n", - " 'node_index': 0,\n", - " 'node_name': 'distribution'},\n", - " 'products': {3: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'manufacture': {'class': 'ManufactureUnit',\n", - " 'config': {'agent_type': 'producer'}}},\n", - " 'consumer': None,\n", - " 'id': 6,\n", - " 'manufacture': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'producer'},\n", - " 'id': 7,\n", - " 'node_index': 0,\n", - " 'node_name': 'manufacture',\n", - " 'sku_id': 3},\n", - " 'max_vlt': 1,\n", - " 'node_index': 0,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 3}},\n", - " 'storage': {'children': None,\n", - " 'class': ,\n", - " 'config': {'capacity': 10000,\n", - " 'unit_storage_cost': 1},\n", - " 'id': 2,\n", - " 'node_index': 0,\n", - " 'node_name': 'storage',\n", - " 'product_list': [3]}},\n", - " 'upstreams': {}},\n", - " 8: {'class': ,\n", - " 'configs': {'agent_type': 'facility',\n", - " 'delay_order_penalty': 10,\n", - " 'order_cost': 0},\n", - " 'downstreams': {1: [17, 28]},\n", - " 'id': 8,\n", - " 'name': 'Supplier_002',\n", - " 'node_index': 1,\n", - " 'skus': {1: {'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1},\n", - " 3: {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'production_rate': 1,\n", - " 'type': 'material',\n", - " 'vlt': 1}},\n", - " 'units': {'distribution': {'children': [{'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 11,\n", - " 'node_index': 2,\n", - " 'node_name': 'vehicle'},\n", - " {'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 12,\n", - " 'node_index': 3,\n", - " 'node_name': 'vehicle'}],\n", - " 'class': ,\n", - " 'config': {'unit_price': 1},\n", - " 'id': 10,\n", - " 'node_index': 1,\n", - " 'node_name': 'distribution'},\n", - " 'products': {1: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'manufacture': {'class': 'ManufactureUnit',\n", - " 'config': {'agent_type': 'producer'}}},\n", - " 'consumer': None,\n", - " 'id': 13,\n", - " 'manufacture': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'producer'},\n", - " 'id': 14,\n", - " 'node_index': 1,\n", - " 'node_name': 'manufacture',\n", - " 'sku_id': 1},\n", - " 'max_vlt': 1,\n", - " 'node_index': 1,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 1},\n", - " 3: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'manufacture': {'class': 'ManufactureUnit',\n", - " 'config': {'agent_type': 'producer'}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 16,\n", - " 'node_index': 0,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 3,\n", - " 'sources': [1]},\n", - " 'id': 15,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 2,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 3}},\n", - " 'storage': {'children': None,\n", - " 'class': ,\n", - " 'config': {'capacity': 10000,\n", - " 'unit_storage_cost': 1},\n", - " 'id': 9,\n", - " 'node_index': 1,\n", - " 'node_name': 'storage',\n", - " 'product_list': [1,\n", - " 3]}},\n", - " 'upstreams': {3: [1]}},\n", - " 17: {'class': ,\n", - " 'configs': {'agent_type': 'facility',\n", - " 'delay_order_penalty': 10,\n", - " 'order_cost': 0},\n", - " 'downstreams': {},\n", - " 'id': 17,\n", - " 'name': 'Warehouse_001',\n", - " 'node_index': 2,\n", - " 'skus': {1: {'id': 1,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1},\n", - " 2: {'id': 2,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1},\n", - " 3: {'id': 3,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}},\n", - " 'units': {'distribution': {'children': [{'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 20,\n", - " 'node_index': 4,\n", - " 'node_name': 'vehicle'},\n", - " {'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 21,\n", - " 'node_index': 5,\n", - " 'node_name': 'vehicle'}],\n", - " 'class': ,\n", - " 'config': {'unit_price': 1},\n", - " 'id': 19,\n", - " 'node_index': 2,\n", - " 'node_name': 'distribution'},\n", - " 'products': {1: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 23,\n", - " 'node_index': 1,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 1,\n", - " 'sources': [8]},\n", - " 'id': 22,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 3,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 1},\n", - " 2: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 25,\n", - " 'node_index': 2,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 2,\n", - " 'sources': []},\n", - " 'id': 24,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 4,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 2},\n", - " 3: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 27,\n", - " 'node_index': 3,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 3,\n", - " 'sources': [1]},\n", - " 'id': 26,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 5,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 3}},\n", - " 'storage': {'children': None,\n", - " 'class': ,\n", - " 'config': {'capacity': 30000,\n", - " 'unit_storage_cost': 1},\n", - " 'id': 18,\n", - " 'node_index': 2,\n", - " 'node_name': 'storage',\n", - " 'product_list': [1,\n", - " 2,\n", - " 3]}},\n", - " 'upstreams': {1: [8], 3: [1]}},\n", - " 28: {'class': ,\n", - " 'configs': {'agent_type': 'facility',\n", - " 'order_cost': 0},\n", - " 'downstreams': {},\n", - " 'id': 28,\n", - " 'name': 'Retailer_001',\n", - " 'node_index': 3,\n", - " 'skus': {1: {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 300,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1},\n", - " 2: {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 2,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1},\n", - " 3: {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 200,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}},\n", - " 'units': {'distribution': None,\n", - " 'products': {1: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'seller': {'class': 'SellerUnit',\n", - " 'config': {'sale_hist_len': 4}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 31,\n", - " 'node_index': 4,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 1,\n", - " 'sources': [8]},\n", - " 'id': 30,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 6,\n", - " 'node_name': 'product',\n", - " 'seller': {'children': None,\n", - " 'class': ,\n", - " 'config': {'sale_hist_len': 4},\n", - " 'id': 32,\n", - " 'node_index': 0,\n", - " 'node_name': 'seller',\n", - " 'sku_id': 1},\n", - " 'sku_id': 1},\n", - " 2: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'seller': {'class': 'SellerUnit',\n", - " 'config': {'sale_hist_len': 4}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 37,\n", - " 'node_index': 6,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 2,\n", - " 'sources': []},\n", - " 'id': 36,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 8,\n", - " 'node_name': 'product',\n", - " 'seller': {'children': None,\n", - " 'class': ,\n", - " 'config': {'sale_hist_len': 4},\n", - " 'id': 38,\n", - " 'node_index': 2,\n", - " 'node_name': 'seller',\n", - " 'sku_id': 2},\n", - " 'sku_id': 2},\n", - " 3: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'seller': {'class': 'SellerUnit',\n", - " 'config': {'sale_hist_len': 4}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 34,\n", - " 'node_index': 5,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 3,\n", - " 'sources': [1]},\n", - " 'id': 33,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 7,\n", - " 'node_name': 'product',\n", - " 'seller': {'children': None,\n", - " 'class': ,\n", - " 'config': {'sale_hist_len': 4},\n", - " 'id': 35,\n", - " 'node_index': 1,\n", - " 'node_name': 'seller',\n", - " 'sku_id': 3},\n", - " 'sku_id': 3}},\n", - " 'storage': {'children': None,\n", - " 'class': ,\n", - " 'config': {'capacity': 20000,\n", - " 'unit_storage_cost': 1},\n", - " 'id': 29,\n", - " 'node_index': 3,\n", - " 'node_name': 'storage',\n", - " 'product_list': [1,\n", - " 3,\n", - " 2]}},\n", - " 'upstreams': {1: [8], 3: [1]}}},\n", - " 'max_price': 300,\n", - " 'max_sources_per_facility': 1,\n", - " 'skus': {1: {'bom': {3: 10},\n", - " 'id': 1,\n", - " 'name': 'sku1',\n", - " 'output_units_per_lot': 12},\n", - " 2: {'bom': {},\n", - " 'id': 2,\n", - " 'name': 'sku2',\n", - " 'output_units_per_lot': 1},\n", - " 3: {'bom': {},\n", - " 'id': 3,\n", - " 'name': 'sku3',\n", - " 'output_units_per_lot': 1}},\n", - " 'unit_mapping': {2: ('storage', 0, 1, None),\n", - " 3: ('distribution', 0, 1, None),\n", - " 4: ('vehicle', 0, 1, None),\n", - " 5: ('vehicle', 1, 1, None),\n", - " 6: ('product',\n", - " 0,\n", - " 1,\n", - " {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 10,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}),\n", - " 7: ('manufacture',\n", - " 0,\n", - " 1,\n", - " {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 10,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}),\n", - " 9: ('storage', 1, 8, None),\n", - " 10: ('distribution', 1, 8, None),\n", - " 11: ('vehicle', 2, 8, None),\n", - " 12: ('vehicle', 3, 8, None),\n", - " 13: ('product',\n", - " 1,\n", - " 8,\n", - " {'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}),\n", - " 14: ('manufacture',\n", - " 1,\n", - " 8,\n", - " {'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}),\n", - " 15: ('product',\n", - " 2,\n", - " 8,\n", - " {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'production_rate': 1,\n", - " 'type': 'material',\n", - " 'vlt': 1}),\n", - " 16: ('consumer',\n", - " 0,\n", - " 8,\n", - " {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'production_rate': 1,\n", - " 'type': 'material',\n", - " 'vlt': 1}),\n", - " 18: ('storage', 2, 17, None),\n", - " 19: ('distribution', 2, 17, None),\n", - " 20: ('vehicle', 4, 17, None),\n", - " 21: ('vehicle', 5, 17, None),\n", - " 22: ('product',\n", - " 3,\n", - " 17,\n", - " {'id': 1,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 23: ('consumer',\n", - " 1,\n", - " 17,\n", - " {'id': 1,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 24: ('product',\n", - " 4,\n", - " 17,\n", - " {'id': 2,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 25: ('consumer',\n", - " 2,\n", - " 17,\n", - " {'id': 2,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 26: ('product',\n", - " 5,\n", - " 17,\n", - " {'id': 3,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 27: ('consumer',\n", - " 3,\n", - " 17,\n", - " {'id': 3,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 29: ('storage', 3, 28, None),\n", - " 30: ('product',\n", - " 6,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 300,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 31: ('consumer',\n", - " 4,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 300,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 32: ('seller',\n", - " 0,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 300,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 33: ('product',\n", - " 7,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 200,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 34: ('consumer',\n", - " 5,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 200,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 35: ('seller',\n", - " 1,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 3,\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 'init_stock': 100,\n", - " 'price': 200,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 36: ('product',\n", - " 8,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 2,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 37: ('consumer',\n", - " 6,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 2,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 38: ('seller',\n", - " 2,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 2,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1})}}}\n" - ] - } - ], - "source": [ - "pprint(env.summary)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "504e25a0", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.10" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 731bc5928de30bf2d9e719fbf8990a9696d21191 Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Thu, 3 Jun 2021 10:28:26 +0000 Subject: [PATCH 291/482] Add render to all modes of supply chain example --- .../supply_chain/evaluation_with_render.py | 81 +++++++++++++++++++ examples/supply_chain/learner.py | 20 ----- examples/supply_chain/main.py | 16 ++-- examples/supply_chain/render_tools.py | 27 ++++--- 4 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 examples/supply_chain/evaluation_with_render.py delete mode 100644 examples/supply_chain/learner.py diff --git a/examples/supply_chain/evaluation_with_render.py b/examples/supply_chain/evaluation_with_render.py new file mode 100644 index 000000000..23469f557 --- /dev/null +++ b/examples/supply_chain/evaluation_with_render.py @@ -0,0 +1,81 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +from os import getcwd + +from maro.rl import Actor, LocalLearner + +from examples.supply_chain.render_tools import SimulationTracker +from maro.rl.training.message_enums import MsgKey + + +EPISODE_LEN = 60 +N_EPISODE = 1 +FACILITY_TYPES = ["productstore"] + + +class RenderActor(Actor): + def __init__( + self, env, policies, agent2policy, group, exploration_dict=None, + agent2exploration=None, eval_env=None, log_dir=getcwd(), **proxy_kwargs + ): + super().__init__( + env, policies, agent2policy, group, exploration_dict=exploration_dict, agent2exploration=agent2exploration, + eval_env=eval_env, log_dir=log_dir, **proxy_kwargs + ) + self._log_dir = log_dir + + def _evaluate(self, msg): + log_dir = os.path.join(self._log_dir, f"ep_{msg.body[MsgKey.EPISODE_INDEX]}", self._proxy._name) + tracker = SimulationTracker( + episode_len=EPISODE_LEN, + n_episodes=N_EPISODE, + env=self.eval_env, + policies=self.policy, + log_dir=log_dir, + logger_name=f"SimulationTracker.{self._proxy._name}" + ) + tracker.run_and_render(facility_types=FACILITY_TYPES) + + return super()._evaluate(msg) + + +class RenderLocalLearner(LocalLearner): + def __init__( + self, env, policies, agent2policy, num_episodes, num_steps=-1, exploration_dict=None, agent2exploration=None, + eval_schedule=None, eval_env=None, early_stopper=None, log_env_summary=True, log_dir=getcwd() + ): + super().__init__( + env, policies, agent2policy, num_episodes, num_steps=num_steps, exploration_dict=exploration_dict, + agent2exploration=agent2exploration, eval_schedule=eval_schedule, eval_env=eval_env, + early_stopper=early_stopper, log_env_summary=log_env_summary, log_dir=log_dir + ) + self._log_dir = log_dir + + def run(self): + """Entry point for executing a learning workflow.""" + for ep in range(1, self.num_episodes + 1): + self._train(ep) + if ep == self._eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self._evaluate() + self._run_and_render(ep) + # early stopping check + if self.early_stopper: + self.early_stopper.push(self.eval_env.summary) + if self.early_stopper.stop(): + return + + def _run_and_render(self, ep: int): + log_dir = os.path.join(self._log_dir, f"ep_{ep}") + tracker = SimulationTracker( + episode_len=EPISODE_LEN, + n_episodes=N_EPISODE, + env=self.eval_env, + policies=self._policy, + log_dir=log_dir, + logger_name="SimulationTracker" + ) + tracker.run_and_render(facility_types=FACILITY_TYPES) + diff --git a/examples/supply_chain/learner.py b/examples/supply_chain/learner.py deleted file mode 100644 index 9c2ff6110..000000000 --- a/examples/supply_chain/learner.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -from os.path import dirname, join, realpath -from maro.rl import DistributedLearner -from render_tools import SimulationTracker - -sc_code_dir = dirname(realpath(__file__)) - -class SCLearner(DistributedLearner): - - def _evaluate(self, ep: int): - tracker = SimulationTracker(60, 1, self.eval_env, self) - loc_path = '/maro/supply_chain/output/' - facility_types = ["productstore"] - tracker.run_and_render(loc_path, facility_types) - - def end_of_training(self, ep, **kwargs): - pass diff --git a/examples/supply_chain/main.py b/examples/supply_chain/main.py index 0e84fde3f..9e39209d8 100644 --- a/examples/supply_chain/main.py +++ b/examples/supply_chain/main.py @@ -9,16 +9,15 @@ import yaml -from maro.rl import Actor, Learner, LocalLearner, LocalPolicyManager, ParallelRolloutManager +from maro.rl import Learner, LocalPolicyManager, ParallelRolloutManager from maro.simulator import Env from maro.simulator.scenarios.supply_chain.world import AgentInfo from maro.utils import set_seeds from examples.supply_chain.env_wrapper import SCEnvWrapper +from examples.supply_chain.evaluation_with_render import RenderActor, RenderLocalLearner from examples.supply_chain.exploration import get_exploration_mapping -# from examples.supply_chain.learner import SCLearner from examples.supply_chain.policies import get_policy_mapping, get_replay_agent_ids -from examples.supply_chain.render_tools import SimulationTracker # logging.basicConfig(level=logging.DEBUG) @@ -39,7 +38,7 @@ def single_thread_mode(config, env_wrapper): policies, agent2policy = get_policy_mapping(config) # create a learner to start training - learner = LocalLearner( + learner = RenderLocalLearner( env=env_wrapper, policies=policies, agent2policy=agent2policy, @@ -53,12 +52,7 @@ def single_thread_mode(config, env_wrapper): log_env_summary=config["log_env_summary"], log_dir=LOG_DIR ) - - tracker = SimulationTracker(60, 1, env_wrapper, learner) - tracker.run_and_render( - loc_path=OUTPUT_DIR, - facility_types=["productstore"] - ) + learner.run() def sc_learner(config): policies, _ = get_policy_mapping(config) @@ -92,7 +86,7 @@ def sc_learner(config): def sc_actor(component_name: str, config, env_wrapper): policies, agent2policy = get_policy_mapping(config) exploration_dict, agent2exploration = get_exploration_mapping(config) - actor = Actor( + actor = RenderActor( env=env_wrapper, policies=policies, agent2policy=agent2policy, diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py index 66718a2e4..a03bdfb2d 100644 --- a/examples/supply_chain/render_tools.py +++ b/examples/supply_chain/render_tools.py @@ -5,15 +5,20 @@ import numpy as np import os +from maro.utils import Logger + from examples.supply_chain.env_wrapper import sku_agent_types, SCEnvWrapper class SimulationTracker: - def __init__(self, episode_len: int, n_episodes: int, env: SCEnvWrapper, learner): + def __init__(self, episode_len: int, n_episodes: int, env: SCEnvWrapper, policies: dict, log_dir="./", logger_name=None): self.episode_len = episode_len self.n_episodes = n_episodes self.env = env - self.learner = learner + self.policies = policies + self.log_dir = log_dir + + self.logger = Logger(logger_name if logger_name else "SimulationTracker", dump_folder=self.log_dir) self.global_balances = np.zeros((n_episodes, episode_len)) self.global_rewards = np.zeros((n_episodes, episode_len)) @@ -137,12 +142,12 @@ def _run_wth_render(self, facility_types): self.env.reset() self.env.start() for epoch in range(self.episode_len): - action = {id_: self.learner._policy[id_].choose_action(st) for id_, st in self.env.state.items()} - self.learner._logger.info(f"epoch: {epoch}, action: {action}") + action = {agent_id: self.policies[agent_id].choose_action(st) for agent_id, st in self.env.state.items()} + self.logger.info(f"epoch: {epoch}, action: {action}") self.env.step(action) - self.learner._logger.info(f"epoch: {epoch}, action: {self.env.to_env_action(action)}") + self.logger.info(f"epoch: {epoch}, action: {self.env.to_env_action(action)}") if hasattr(self.env, "consumer2product"): - self.learner._logger.info(f"consumer2product: {self.env.consumer2product}") + self.logger.info(f"consumer2product: {self.env.consumer2product}") self.env.get_reward() step_balances = self.env.balance_status step_rewards = self.env.reward_status @@ -169,11 +174,9 @@ def _run_wth_render(self, facility_types): _step_metrics_list = np.cumsum(np.sum(_step_metrics, axis=0)) return np.sum(_step_metrics), _step_metrics_list - def run_and_render(self, loc_path, facility_types): - if not os.path.exists(loc_path): - os.makedirs(loc_path) + def run_and_render(self, facility_types): metric, metric_list = self._run_wth_render(facility_types=facility_types) - self._render(os.path.join(loc_path, "plot_balance.png"), self.step_balances, facility_types) - self._render(os.path.join(loc_path, "plot_reward.png"), self.step_rewards, facility_types) - self._render_sku(loc_path) + self._render(os.path.join(self.log_dir, "plot_balance.png"), self.step_balances, facility_types) + self._render(os.path.join(self.log_dir, "plot_reward.png"), self.step_rewards, facility_types) + self._render_sku(self.log_dir) return metric, metric_list From ebe5065b9df79f4e01aec4d32285fb2667e8eb94 Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Thu, 3 Jun 2021 10:30:20 +0000 Subject: [PATCH 292/482] fix or policy typos --- examples/supply_chain/or_policy/eoq_policy.py | 2 +- examples/supply_chain/or_policy/minmax_policy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/supply_chain/or_policy/eoq_policy.py b/examples/supply_chain/or_policy/eoq_policy.py index 15a11ae53..3bb092042 100644 --- a/examples/supply_chain/or_policy/eoq_policy.py +++ b/examples/supply_chain/or_policy/eoq_policy.py @@ -28,7 +28,7 @@ def _get_consumer_quantity(self, state): consumer_quantity = int(np.sqrt(2*sale_gamma*order_cost / holding_cost) / sale_gamma) return consumer_quantity - def compute_action(self, state): + def choose_action(self, state): if state['is_facility']: return 0 # consumer_source_inventory diff --git a/examples/supply_chain/or_policy/minmax_policy.py b/examples/supply_chain/or_policy/minmax_policy.py index 6e69d34a6..4a6ec903c 100644 --- a/examples/supply_chain/or_policy/minmax_policy.py +++ b/examples/supply_chain/or_policy/minmax_policy.py @@ -17,7 +17,7 @@ class ConsumerMinMaxPolicy(ConsumerBaselinePolicy): def __init__(self, name:str, config: dict): super(ConsumerMinMaxPolicy, self).__init__(name=name, config=config) - def compute_action(self, state): + def choose_action(self, state): if state['is_facility']: return 0 # consumer_source_inventory From 0549a14f75f011a6a1482786e32834b5f6b2e39a Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 4 Jun 2021 01:49:32 +0000 Subject: [PATCH 293/482] 1. added parallel policy manager prototype; 2. used training ep for evaluation episodes --- examples/cim/dqn/config.yml | 17 ++++++---- examples/cim/dqn/main.py | 49 ++++++++++++++++++++++++----- maro/rl/__init__.py | 4 +-- maro/rl/training/__init__.py | 8 +++-- maro/rl/training/actor.py | 5 ++- maro/rl/training/learner.py | 4 +-- maro/rl/training/local_learner.py | 9 ++---- maro/rl/training/message_enums.py | 3 +- maro/rl/training/policy_manager.py | 7 ++--- maro/rl/training/rollout_manager.py | 41 +++++++++++++++--------- maro/rl/training/trainer.py | 39 +++++++++++++++++------ 11 files changed, 129 insertions(+), 57 deletions(-) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index d93e0b539..99a10847b 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -3,9 +3,10 @@ experiment_name: cim_dqn mode: multi-process # local, multi-process -num_episodes: 100 +num_episodes: 8 +eval_schedule: 2 num_steps: -1 -eval_schedule: 50 +# eval_schedule: 50 env: basic: scenario: cim @@ -37,11 +38,10 @@ env: shortage_factor: 1.0 time_decay: 0.97 exploration: - last_ep: 100 initial_value: 0.4 final_value: 0.0 splits: - - [50, 0.32] + - [4, 0.32] policy: model: network: @@ -72,13 +72,18 @@ policy: capacity: 1000 overwrite_type: "rolling" batch_size: -1 + replace: false training: # for experience managers in the learner process capacity: 100000 overwrite_type: "rolling" batch_size: 128 + replace: true + update_trigger: 32 + warmup: 1000 multi-process: + policy_training_mode: local # local, parallel group: cim-dqn - num_actors: 3 + num_actors: 2 redis_host: localhost redis_port: 6379 - max_receive_attempts: 3 + max_receive_attempts: 2 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index c4848b6c3..dc19e4e61 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -9,7 +9,7 @@ from maro.rl import ( Actor, DQN, DQNConfig, EpsilonGreedyExploration, ExperienceManager, FullyConnectedBlock, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalPolicyManager, - OptimOption, ParallelRolloutManager + OptimOption, ParallelPolicyManager, ParallelRolloutManager, Trainer ) from maro.simulator import Env from maro.utils import set_seeds @@ -26,6 +26,9 @@ log_dir = os.path.join(FILE_PATH, "logs", config["experiment_name"]) +# agent IDs +AGENT_IDS = Env(**config["env"]["basic"]).agent_idx_list + # model input and output dimensions IN_DIM = ( (config["env"]["wrapper"]["look_back"] + 1) @@ -47,7 +50,9 @@ def get_independent_policy(policy_id, training: bool = True): name=policy_id, q_net=qnet, experience_manager=ExperienceManager(**exp_cfg), - config=DQNConfig(**cfg["algorithm_config"]) + config=DQNConfig(**cfg["algorithm_config"]), + update_trigger=cfg["update_trigger"], + warmup=cfg["warmup"] ) @@ -91,11 +96,17 @@ def get_dqn_actor_process(): actor.run() -def get_dqn_learner_process(): - env = Env(**config["env"]["basic"]) - policy_list = [get_independent_policy(policy_id=i) for i in env.agent_idx_list] - policy_manager = LocalPolicyManager(policies=policy_list, log_dir=log_dir) +def get_dqn_trainer_process(name, policy_names): + trainer = Trainer( + policies=[get_independent_policy(name) for name in policy_names], + group=config["multi-process"]["group"], + name=name, + log_dir=log_dir + ) + trainer.run() + +def get_dqn_learner_process(): epsilon_greedy = EpsilonGreedyExploration(num_actions=config["env"]["wrapper"]["num_actions"]) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, @@ -103,6 +114,7 @@ def get_dqn_learner_process(): last_ep=config["num_episodes"], **config["exploration"] ) + rollout_manager = ParallelRolloutManager( num_actors=config["multi-process"]["num_actors"], group=config["multi-process"]["group"], @@ -111,13 +123,24 @@ def get_dqn_learner_process(): redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) ) + policy_list = [get_independent_policy(policy_id=i) for i in AGENT_IDS] + if config["multi-process"]["policy_training_mode"] == "local": + policy_manager = LocalPolicyManager(policies=policy_list, log_dir=log_dir) + else: + policy_manager = ParallelPolicyManager( + policy2trainer={i: f"TRAINER.{i}" for i in AGENT_IDS}, + group=config["multi-process"]["group"], + log_dir=log_dir + ) learner = Learner( policy_manager=policy_manager, rollout_manager=rollout_manager, num_episodes=config["num_episodes"], + eval_schedule=config["eval_schedule"], log_dir=log_dir ) - time.sleep(5) + + time.sleep(10) learner.run() @@ -127,11 +150,23 @@ def multi_process_mode(): set_seeds(i) actor_process.start() + if config["multi-process"]["policy_training_mode"] == "parallel": + trainer_processes = [ + Process(target=get_dqn_trainer_process, args=(f"TRAINER.{id_}", [id_],)) for id_ in AGENT_IDS + ] + for trainer_process in trainer_processes: + trainer_process.start() + learner_process = Process(target=get_dqn_learner_process) learner_process.start() for actor_process in actor_processes: actor_process.join() + + if config["multi-process"]["policy_training_mode"] == "parallel": + for trainer_process in trainer_processes: + trainer_process.join() + learner_process.join() diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 1b4c7703b..0937263c2 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -19,7 +19,7 @@ from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( AbsEarlyStopper, AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalLearner, LocalPolicyManager, - LocalRolloutManager, ParallelRolloutManager + LocalRolloutManager, ParallelPolicyManager, ParallelRolloutManager, Trainer ) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, @@ -38,7 +38,7 @@ "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Actor", "Learner", "LocalLearner", - "LocalPolicyManager", "LocalRolloutManager", "ParallelRolloutManager", + "LocalPolicyManager", "LocalRolloutManager", "ParallelPolicyManager", "ParallelRolloutManager", "Trainer", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 3b0b8d313..a59cca0e4 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -5,14 +5,16 @@ from .early_stopper import AbsEarlyStopper from .learner import Learner from .local_learner import LocalLearner -from .policy_manager import AbsPolicyManager, LocalPolicyManager +from .policy_manager import AbsPolicyManager, LocalPolicyManager, ParallelPolicyManager from .rollout_manager import AbsRolloutManager, LocalRolloutManager, ParallelRolloutManager +from .trainer import Trainer __all__ = [ "Actor", "AbsEarlyStopper", "Learner", "LocalLearner", - "AbsPolicyManager", "LocalPolicyManager", - "AbsRolloutManager", "LocalRolloutManager", "ParallelRolloutManager" + "AbsPolicyManager", "LocalPolicyManager", "ParallelPolicyManager", + "AbsRolloutManager", "LocalRolloutManager", "ParallelRolloutManager", + "Trainer" ] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index cd4740c01..810b83c70 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -8,7 +8,7 @@ from maro.communication import Proxy from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.exploration import AbsExploration -from maro.rl.policy import AbsCorePolicy, AbsPolicy +from maro.rl.policy import AbsPolicy from maro.utils import Logger from .message_enums import MsgKey, MsgTag @@ -169,8 +169,7 @@ def _collect(self, msg): self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) def _evaluate(self, msg): - ep = msg.body[MsgKey.EPISODE_INDEX] - self._logger.info(f"Evaluation episode {ep}") + self._logger.info(f"Evaluating...") self.eval_env.reset() self.eval_env.start() # get initial state self._load_policy_states(msg.body[MsgKey.POLICY]) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 62ee922dd..7f49b0258 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -79,9 +79,9 @@ def run(self): self._train(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 - env_metric_dict = self.rollout_manager.evaluate(self.policy_manager.get_state()) + env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) # performance details - self.logger.info(f"ep {ep}: {env_metric_dict}") + self.logger.info(f"Evaluation result: {env_metric_dict}") # early stopping check if self.early_stopper: for env_metric in env_metric_dict.values(): diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py index c6acc548f..2de438801 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -107,7 +107,6 @@ def __init__( self.early_stopper = early_stopper self._log_env_summary = log_env_summary - self._eval_ep = 0 def run(self): """Entry point for executing a learning workflow.""" @@ -125,7 +124,6 @@ def run(self): def _train(self, ep: int): """Collect simulation data for training.""" t0 = time.time() - learning_time = 0 num_experiences_collected = 0 if self.exploration_dict: @@ -141,6 +139,7 @@ def _train(self, ep: int): while self.env.state: segment += 1 for agent_id, exp in self._collect(ep, segment).items(): + num_experiences_collected += exp.size if isinstance(self._policy[agent_id], AbsCorePolicy): self._policy[agent_id].on_experiences(exp) @@ -153,18 +152,16 @@ def _train(self, ep: int): if self._log_env_summary: self._logger.info(f"ep {ep}: {self.env.summary}") - self._logger.debug( + self._logger.info( f"ep {ep} summary - " f"running time: {time.time() - t0} " f"env steps: {self.env.step_index} " - f"learning time: {learning_time} " f"experiences collected: {num_experiences_collected}" ) def _evaluate(self): """Policy evaluation.""" self._logger.info("Evaluating...") - self._eval_ep += 1 self.eval_env.reset() self.eval_env.start() # get initial state while self.eval_env.state: @@ -172,7 +169,7 @@ def _evaluate(self): self.eval_env.step(action) # performance details - self._logger.info(f"evaluation ep {self._eval_ep}: {self.eval_env.summary}") + self._logger.info(f"Evaluation result: {self.eval_env.summary}") def _collect(self, ep, segment): start_step_index = self.env.step_index + 1 diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index 5957000a5..e0a600ed5 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -7,7 +7,8 @@ class MsgTag(Enum): COLLECT = "rollout" EVAL = "eval" - POLICY_UPDATE = "agent_update" + GET_POLICY_STATE = "get_policy_state" + POLICY_STATE = "policy_state" CHOOSE_ACTION = "choose_action" ACTION = "action" TRAIN = "train" diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index 8255fefe3..50f57d504 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -114,9 +114,8 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): def get_state(self): policy_state_dict = {} - for msg in self._proxy.receive(): - if msg.tag == MsgTag.POLICY_UPDATE: - for policy_name, state in msg.body[MsgKey.POLICY].items(): - policy_state_dict[policy_name] = state + for reply in self._proxy.broadcast("trainer", MsgTag.GET_POLICY_STATE, SessionType.TASK): + for policy_name, policy_state in reply.body[MsgKey.POLICY].items(): + policy_state_dict[policy_name] = policy_state return policy_state_dict diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 752e89692..3bc990ada 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -39,10 +39,11 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): raise NotImplementedError @abstractmethod - def evaluate(self, policy_state_dict: dict): + def evaluate(self, ep: int, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``. Args: + ep (int): Current training episode index. policy_state_dict (dict): Policy states to use for simulation. Returns: @@ -121,7 +122,6 @@ def __init__( self._num_steps = num_steps if num_steps > 0 else float("inf") self._log_env_summary = log_env_summary - self._eval_ep = 0 def collect(self, ep: int, segment: int, policy_state_dict: dict): """Collect simulation data for training.""" @@ -193,10 +193,17 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): return self.env.get_experiences(), self.env.summary - def evaluate(self, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``.""" + def evaluate(self, ep: int, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``. + + Args: + ep (int): Current training episode index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Environment summary. + """ self._logger.info("Evaluating...") - self._eval_ep += 1 self._load_policy_states(policy_state_dict) self.eval_env.reset() self.eval_env.start() # get initial state @@ -205,7 +212,7 @@ def evaluate(self, policy_state_dict: dict): self.eval_env.step(action) if self._log_env_summary: - self._logger.info(f"eval ep {self._eval_ep}: {self.eval_env.summary}") + self._logger.info(f"Evaluation result: {self.eval_env.summary}") return self.eval_env.summary @@ -285,7 +292,6 @@ def __init__( self._log_env_summary = log_env_summary self._num_eval_actors = num_eval_actors - self._eval_ep = 0 self._exploration_update = True @@ -348,10 +354,17 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic return combined_exp_by_policy - def evaluate(self, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``.""" - self._eval_ep += 1 - msg_body = {MsgKey.EPISODE_INDEX: self._eval_ep, MsgKey.POLICY: policy_state_dict} + def evaluate(self, ep: int, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``. + + Args: + ep (int): Current training episode index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Environment summary. + """ + msg_body = {MsgKey.EPISODE_INDEX: ep, MsgKey.POLICY: policy_state_dict} actors = choices(self._actors, k=self._num_eval_actors) env_summary_dict = {} @@ -361,16 +374,16 @@ def evaluate(self, policy_state_dict: dict): # Receive roll-out results from remote actors num_finishes = 0 for msg in self._proxy.receive(): - if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE_INDEX] != self._eval_ep: + if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE_INDEX] != ep: self._logger.info( f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE_INDEX]} " - f"(expected message type {MsgTag.EVAL} and episode index {self._eval_ep})" + f"(expected message type {MsgTag.EVAL_DONE} and episode index {ep})" ) continue env_summary_dict[msg.source] = msg.body[MsgKey.ENV_SUMMARY] - if msg.body[MsgKey.EPISODE_INDEX] == self._eval_ep: + if msg.body[MsgKey.EPISODE_INDEX] == ep: num_finishes += 1 if num_finishes == self._num_eval_actors: break diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index f9dce6cc8..4796b6601 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -1,22 +1,43 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from abc import ABC, abstractmethod -from collections import defaultdict from os import getcwd -from typing import Dict, List +from typing import List -from maro.communication import Proxy, SessionType -from maro.rl.experience import ExperienceSet -from maro.rl.policy import AbsPolicy, AbsCorePolicy +from maro.communication import Proxy +from maro.rl.policy import AbsPolicy from maro.utils import Logger from .message_enums import MsgKey, MsgTag class Trainer: - def __init__(self) -> None: - pass + def __init__( + self, + policies: List[AbsPolicy], + group: str, + name: str, + log_dir: str = getcwd(), + **proxy_kwargs + ): + self.policy_dict = {policy.name: policy for policy in policies} + self._proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=name, **proxy_kwargs) + self._logger = Logger(self._proxy.name, dump_folder=log_dir) + self._updated = {policy_name: True for policy_name in self.policy_dict} def run(self): - pass + for msg in self._proxy.receive(): + if msg.tag == MsgTag.EXIT: + self._logger.info("Exiting...") + self._proxy.close() + break + + if msg.tag == MsgTag.TRAIN: + for name, exp in msg.body[MsgKey.EXPERIENCES].items(): + self._updated[name] = self.policy_dict[name].on_experiences(exp) + elif msg.tag == MsgTag.GET_POLICY_STATE: + updated_policy_state_dict = { + name: policy.get_state() for name, policy in self.policy_dict.items() if self._updated[name] + } + self._proxy.reply(msg, tag=MsgTag.POLICY_STATE, body={MsgKey.POLICY: updated_policy_state_dict}) + self._updated = {policy_name: False for policy_name in self.policy_dict} From 023a9d3374ca92ef9d6d953851603cd31db1c9f2 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 9 Jun 2021 08:20:06 +0000 Subject: [PATCH 294/482] refined parallel policy manager --- examples/cim/dqn/config.yml | 41 ++++++--- examples/cim/dqn/main.py | 84 +++++++++++-------- maro/rl/policy/policy.py | 3 + maro/rl/training/__init__.py | 4 +- maro/rl/training/learner.py | 24 ++++-- maro/rl/training/policy_manager.py | 58 +++++++------ .../training/{trainer.py => policy_server.py} | 24 +++--- maro/rl/training/rollout_manager.py | 2 +- 8 files changed, 152 insertions(+), 88 deletions(-) rename maro/rl/training/{trainer.py => policy_server.py} (55%) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 99a10847b..594b85846 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -3,15 +3,15 @@ experiment_name: cim_dqn mode: multi-process # local, multi-process -num_episodes: 8 -eval_schedule: 2 +num_episodes: 3 +# eval_schedule: 2 num_steps: -1 -# eval_schedule: 50 env: basic: scenario: cim - topology: toy.4p_ssdd_l0.0 - durations: 1120 + topology: global_trade.22p_l0.0 + # topology: global_trade.22p_l0.0 + durations: 560 wrapper: port_attributes: - empty @@ -41,14 +41,31 @@ exploration: initial_value: 0.4 final_value: 0.0 splits: - - [4, 0.32] + - [2, 0.3] policy: model: network: hidden_dims: - 256 + - 256 + - 256 + - 256 + - 256 + - 128 + - 128 - 128 + - 128 + - 128 + - 64 + - 64 + - 64 + - 64 - 64 + - 32 + - 32 + - 32 + - 32 + - 32 activation: leaky_relu softmax: false batch_norm: true @@ -63,7 +80,6 @@ policy: reward_discount: .0 target_update_freq: 5 train_epochs: 10 - gradient_iters: 1 soft_update_coefficient: 0.1 double: false loss_cls: smooth_l1 @@ -78,12 +94,15 @@ policy: overwrite_type: "rolling" batch_size: 128 replace: true - update_trigger: 32 - warmup: 1000 + update_trigger: 16 + warmup: 1 multi-process: - policy_training_mode: local # local, parallel + rollout_mode: local # local, parallel + policy_training_mode: parallel # local, parallel group: cim-dqn num_actors: 2 + num_policy_servers: 3 redis_host: localhost redis_port: 6379 - max_receive_attempts: 2 + # max_receive_attempts: 2 + # receive_timeout: 100 # in milli-seconds diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index dc19e4e61..e0fcb9803 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -8,8 +8,8 @@ from maro.rl import ( Actor, DQN, DQNConfig, EpsilonGreedyExploration, ExperienceManager, FullyConnectedBlock, - MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalPolicyManager, - OptimOption, ParallelPolicyManager, ParallelRolloutManager, Trainer + MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalPolicyManager, LocalRolloutManager, + OptimOption, ParallelPolicyManager, ParallelRolloutManager, PolicyServer ) from maro.simulator import Env from maro.utils import set_seeds @@ -28,6 +28,8 @@ # agent IDs AGENT_IDS = Env(**config["env"]["basic"]).agent_idx_list +NUM_POLICY_SERVERS = config["multi-process"]["num_policy_servers"] +POLICY2SERVER = {id_: f"SERVER.{id_ % NUM_POLICY_SERVERS}" for id_ in AGENT_IDS} # model input and output dimensions IN_DIM = ( @@ -79,7 +81,7 @@ def local_learner_mode(): local_learner.run() -def get_dqn_actor_process(): +def get_actor_process(): env = Env(**config["env"]["basic"]) num_actions = config["env"]["wrapper"]["num_actions"] policy_list = [get_independent_policy(policy_id=i, training=False) for i in env.agent_idx_list] @@ -96,17 +98,17 @@ def get_dqn_actor_process(): actor.run() -def get_dqn_trainer_process(name, policy_names): - trainer = Trainer( - policies=[get_independent_policy(name) for name in policy_names], +def get_policy_server_process(server_id): + server = PolicyServer( + policies=[get_independent_policy(id_) for id_ in AGENT_IDS if id_ % NUM_POLICY_SERVERS == server_id], group=config["multi-process"]["group"], - name=name, + name=f"SERVER.{server_id}", log_dir=log_dir ) - trainer.run() + server.run() -def get_dqn_learner_process(): +def get_learner_process(): epsilon_greedy = EpsilonGreedyExploration(num_actions=config["env"]["wrapper"]["num_actions"]) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, @@ -115,20 +117,34 @@ def get_dqn_learner_process(): **config["exploration"] ) - rollout_manager = ParallelRolloutManager( - num_actors=config["multi-process"]["num_actors"], - group=config["multi-process"]["group"], - exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, - log_dir=log_dir, - redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) - ) + if config["multi-process"]["rollout_mode"] == "local": + env = Env(**config["env"]["basic"]) + num_actions = config["env"]["wrapper"]["num_actions"] + rollout_manager = LocalRolloutManager( + env=CIMEnvWrapper(env, **config["env"]["wrapper"]), + policies=[get_independent_policy(policy_id=i) for i in env.agent_idx_list], + agent2policy={i: i for i in env.agent_idx_list}, + exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=num_actions)}, + agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, + log_dir=log_dir + ) + else: + rollout_manager = ParallelRolloutManager( + num_actors=config["multi-process"]["num_actors"], + group=config["multi-process"]["group"], + exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, + # max_receive_attempts=config["multi-process"]["max_receive_attempts"], + # receive_timeout=config["multi-process"]["receive_timeout"], + log_dir=log_dir, + redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) + ) policy_list = [get_independent_policy(policy_id=i) for i in AGENT_IDS] if config["multi-process"]["policy_training_mode"] == "local": policy_manager = LocalPolicyManager(policies=policy_list, log_dir=log_dir) else: policy_manager = ParallelPolicyManager( - policy2trainer={i: f"TRAINER.{i}" for i in AGENT_IDS}, + policy2server=POLICY2SERVER, group=config["multi-process"]["group"], log_dir=log_dir ) @@ -136,36 +152,38 @@ def get_dqn_learner_process(): policy_manager=policy_manager, rollout_manager=rollout_manager, num_episodes=config["num_episodes"], - eval_schedule=config["eval_schedule"], + # eval_schedule=config["eval_schedule"], log_dir=log_dir ) - time.sleep(10) + time.sleep(5) learner.run() def multi_process_mode(): - actor_processes = [Process(target=get_dqn_actor_process) for _ in range(config["multi-process"]["num_actors"])] - for i, actor_process in enumerate(actor_processes): - set_seeds(i) - actor_process.start() + actor_processes = [Process(target=get_actor_process) for _ in range(config["multi-process"]["num_actors"])] + if config["multi-process"]["rollout_mode"] == "parallel": + for i, actor_process in enumerate(actor_processes): + set_seeds(i) + actor_process.start() if config["multi-process"]["policy_training_mode"] == "parallel": - trainer_processes = [ - Process(target=get_dqn_trainer_process, args=(f"TRAINER.{id_}", [id_],)) for id_ in AGENT_IDS + server_processes = [ + Process(target=get_policy_server_process, args=(server_id,)) for server_id in range(NUM_POLICY_SERVERS) ] - for trainer_process in trainer_processes: - trainer_process.start() + for server_process in server_processes: + server_process.start() - learner_process = Process(target=get_dqn_learner_process) + learner_process = Process(target=get_learner_process) learner_process.start() - for actor_process in actor_processes: - actor_process.join() - + if config["multi-process"]["rollout_mode"] == "parallel": + for actor_process in actor_processes: + actor_process.join() + if config["multi-process"]["policy_training_mode"] == "parallel": - for trainer_process in trainer_processes: - trainer_process.join() + for server_process in server_processes: + server_process.join() learner_process.join() diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 8fec23408..883da3345 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import time from abc import ABC, abstractmethod from maro.rl.experience import ExperienceManager, ExperienceSet @@ -96,7 +97,9 @@ def on_experiences(self, exp: ExperienceSet) -> bool: f"Policy {self._name}: exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" ) if self.experience_manager.size >= self.warmup and self._new_exp_counter >= self.update_trigger: + t0 = time.time() self.update() + print(f"policy update time: {time.time() - t0}") self._new_exp_counter = 0 return True diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index a59cca0e4..c7632be3b 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -6,8 +6,8 @@ from .learner import Learner from .local_learner import LocalLearner from .policy_manager import AbsPolicyManager, LocalPolicyManager, ParallelPolicyManager +from .policy_server import PolicyServer from .rollout_manager import AbsRolloutManager, LocalRolloutManager, ParallelRolloutManager -from .trainer import Trainer __all__ = [ "Actor", @@ -15,6 +15,6 @@ "Learner", "LocalLearner", "AbsPolicyManager", "LocalPolicyManager", "ParallelPolicyManager", + "PolicyServer", "AbsRolloutManager", "LocalRolloutManager", "ParallelRolloutManager", - "Trainer" ] diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 7f49b0258..e1beeb88f 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import time from os import getcwd from typing import List, Union @@ -8,7 +9,7 @@ from .early_stopper import AbsEarlyStopper from .policy_manager import AbsPolicyManager -from .rollout_manager import AbsRolloutManager, ParallelRolloutManager +from .rollout_manager import AbsRolloutManager class Learner: @@ -89,23 +90,32 @@ def run(self): if self.early_stopper.stop(): return - if isinstance(self.rollout_manager, ParallelRolloutManager): + if hasattr(self.rollout_manager, "exit"): self.rollout_manager.exit() + if hasattr(self.policy_manager, "exit"): + self.policy_manager.exit() + def _train(self, ep: int): - num_experiences_collected = 0 - segment = 0 + total_policy_update_time = 0 + num_experiences_collected = segment = 0 self.rollout_manager.reset() + policy_state_dict = self.policy_manager.get_state() while not self.rollout_manager.episode_complete: segment += 1 # experience collection - policy_state_dict = self.policy_manager.get_state() exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict) - self.policy_manager.on_experiences(exp_by_policy) + t0 = time.time() + policy_state_dict = self.policy_manager.on_experiences(exp_by_policy) + total_policy_update_time += time.time() - t0 num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) # performance details - self.logger.debug(f"ep {ep} summary - experiences collected: {num_experiences_collected}") + self.logger.debug( + f"ep {ep} summary - " + f"experiences collected: {num_experiences_collected} " + f"total policy update time: {total_policy_update_time}" + ) self.end_of_episode(ep, **self._end_of_episode_kwargs) diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index 50f57d504..0d2f2ac81 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import time from abc import ABC, abstractmethod from collections import defaultdict from os import getcwd @@ -54,7 +55,6 @@ def __init__(self, policies: List[AbsPolicy], log_dir: str = getcwd()): self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) self.policy_dict = {policy.name: policy for policy in policies} self._new_exp_counter = defaultdict(int) - self._updated_policy_names = set() @property def names(self): @@ -66,36 +66,36 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding policy's experience manager. Policies whose update conditions have been met will then be updated. """ - for policy_name, exp in exp_by_policy.items(): - if isinstance(self.policy_dict[policy_name], AbsCorePolicy): - if self.policy_dict[policy_name].on_experiences(exp): - self._updated_policy_names.add(policy_name) + t0 = time.time() + updated = { + name: self.policy_dict[name].get_state() + for name, exp in exp_by_policy.items() + if isinstance(self.policy_dict[name], AbsCorePolicy) and self.policy_dict[name].on_experiences(exp) + } + + if updated: + self._logger.info(f"Updated policies {list(updated.keys())}") - if self._updated_policy_names: - self._logger.info(f"Updated policies {self._updated_policy_names}") + self._logger.debug(f"policy update time: {time.time() - t0}") + return updated def get_state(self): - """Return the states of updated policies since the last call.""" - policy_state_dict = { - policy_name: self.policy_dict[policy_name].get_state() for policy_name in self._updated_policy_names - } - self._updated_policy_names.clear() - return policy_state_dict + return {name: policy.get_state() for name, policy in self.policy_dict.items()} class ParallelPolicyManager(AbsPolicyManager): def __init__( self, - policy2trainer: Dict[str, str], + policy2server: Dict[str, str], group: str, log_dir: str = getcwd(), **proxy_kwargs ): super().__init__() self._logger = Logger("PARALLEL_POLICY_MANAGER", dump_folder=log_dir) - self.policy2trainer = policy2trainer - self._names = list(self.policy2trainer.keys()) - peers = {"trainer": len(set(self.policy2trainer.values()))} + self.policy2server = policy2server + self._names = list(self.policy2server.keys()) + peers = {"policy_server": len(set(self.policy2server.values()))} self._proxy = Proxy(group, "policy_manager", peers, **proxy_kwargs) @property @@ -103,19 +103,29 @@ def names(self): return self._names def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): - msg_body_by_dest = defaultdict(dict) + msg_body_by_dest, policy_state_dict = defaultdict(dict), {} for policy_name, exp in exp_by_policy.items(): - trainer_id = self.policy2trainer[policy_name] - if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp + policy_server_id = self.policy2server[policy_name] + if MsgKey.EXPERIENCES not in msg_body_by_dest[policy_server_id]: + msg_body_by_dest[policy_server_id][MsgKey.EXPERIENCES] = {} + msg_body_by_dest[policy_server_id][MsgKey.EXPERIENCES][policy_name] = exp - self._proxy.iscatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())) + for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): + for policy_name, policy_state in reply.body[MsgKey.POLICY].items(): + policy_state_dict[policy_name] = policy_state + + return policy_state_dict def get_state(self): policy_state_dict = {} - for reply in self._proxy.broadcast("trainer", MsgTag.GET_POLICY_STATE, SessionType.TASK): + for reply in self._proxy.broadcast("policy_server", MsgTag.GET_POLICY_STATE, SessionType.TASK): for policy_name, policy_state in reply.body[MsgKey.POLICY].items(): policy_state_dict[policy_name] = policy_state return policy_state_dict + + def exit(self): + """Tell the remote actors to exit.""" + self._proxy.ibroadcast("policy_server", MsgTag.EXIT, SessionType.NOTIFICATION) + self._proxy.close() + self._logger.info("Exiting...") diff --git a/maro/rl/training/trainer.py b/maro/rl/training/policy_server.py similarity index 55% rename from maro/rl/training/trainer.py rename to maro/rl/training/policy_server.py index 4796b6601..a3c2bded9 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/policy_server.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import time from os import getcwd from typing import List @@ -11,7 +12,7 @@ from .message_enums import MsgKey, MsgTag -class Trainer: +class PolicyServer: def __init__( self, policies: List[AbsPolicy], @@ -21,9 +22,8 @@ def __init__( **proxy_kwargs ): self.policy_dict = {policy.name: policy for policy in policies} - self._proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=name, **proxy_kwargs) + self._proxy = Proxy(group, "policy_server", {"policy_manager": 1}, component_name=name, **proxy_kwargs) self._logger = Logger(self._proxy.name, dump_folder=log_dir) - self._updated = {policy_name: True for policy_name in self.policy_dict} def run(self): for msg in self._proxy.receive(): @@ -33,11 +33,15 @@ def run(self): break if msg.tag == MsgTag.TRAIN: - for name, exp in msg.body[MsgKey.EXPERIENCES].items(): - self._updated[name] = self.policy_dict[name].on_experiences(exp) - elif msg.tag == MsgTag.GET_POLICY_STATE: - updated_policy_state_dict = { - name: policy.get_state() for name, policy in self.policy_dict.items() if self._updated[name] + t0 = time.time() + updated = { + name: self.policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() + if self.policy_dict[name].on_experiences(exp) } - self._proxy.reply(msg, tag=MsgTag.POLICY_STATE, body={MsgKey.POLICY: updated_policy_state_dict}) - self._updated = {policy_name: False for policy_name in self.policy_dict} + t1 = time.time() + self._logger.debug(f"total policy update time: {t1 - t0}") + self._proxy.reply(msg, body={MsgKey.POLICY: updated}) + self._logger.debug(f"reply time: {time.time() - t1}") + elif msg.tag == MsgTag.GET_POLICY_STATE: + policy_state_dict = {name: policy.get_state() for name, policy in self.policy_dict.items()} + self._proxy.reply(msg, tag=MsgTag.POLICY_STATE, body={MsgKey.POLICY: policy_state_dict}) diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 3bc990ada..295bed11d 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -191,7 +191,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): f"experiences collected: {num_experiences_collected}" ) - return self.env.get_experiences(), self.env.summary + return self.env.get_experiences() def evaluate(self, ep: int, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``. From 5a57e0116c78ff9278b9963bc41c925ed5a20dbb Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 9 Jun 2021 08:21:39 +0000 Subject: [PATCH 295/482] updated rl/__init__/py --- maro/rl/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 0937263c2..00a6c4038 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -19,7 +19,7 @@ from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( AbsEarlyStopper, AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalLearner, LocalPolicyManager, - LocalRolloutManager, ParallelPolicyManager, ParallelRolloutManager, Trainer + LocalRolloutManager, ParallelPolicyManager, ParallelRolloutManager, PolicyServer ) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, @@ -38,7 +38,7 @@ "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Actor", "Learner", "LocalLearner", - "LocalPolicyManager", "LocalRolloutManager", "ParallelPolicyManager", "ParallelRolloutManager", "Trainer", + "LocalPolicyManager", "LocalRolloutManager", "ParallelPolicyManager", "ParallelRolloutManager", "PolicyServer", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] From 1f62f3a90db95f8d7b064bfe856bfa1c5a344d09 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 9 Jun 2021 09:25:42 +0000 Subject: [PATCH 296/482] fixed lint issues and CIM local learner bugs --- examples/cim/dqn/main.py | 2 +- maro/communication/proxy.py | 1 - maro/rl/experience/experience_manager.py | 4 ++-- maro/rl/exploration/exploration_scheduler.py | 4 ++-- maro/rl/policy/policy.py | 3 ++- maro/rl/training/actor.py | 4 ++-- maro/rl/training/learner.py | 4 ++-- maro/rl/training/local_learner.py | 8 ++++---- maro/rl/training/policy_manager.py | 8 ++++---- maro/rl/training/rollout_manager.py | 10 +++++----- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index e0fcb9803..684090db5 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -65,6 +65,7 @@ def local_learner_mode(): epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, param_name="epsilon", + last_ep=config["num_episodes"], **config["exploration"] ) local_learner = LocalLearner( @@ -75,7 +76,6 @@ def local_learner_mode(): num_steps=config["num_steps"], exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, - eval_schedule=config["eval_schedule"], log_dir=log_dir ) local_learner.run() diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index cee387e87..4dcc499d5 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -188,7 +188,6 @@ def __init__( self._join() - def _signal_handler(self, signum, frame): self._redis_connection.hdel(self._redis_hash_name, self._name) self._logger.critical(f"{self._name} received Signal: {signum} at frame: {frame}") diff --git a/maro/rl/experience/experience_manager.py b/maro/rl/experience/experience_manager.py index afda224cf..d9d2913bb 100644 --- a/maro/rl/experience/experience_manager.py +++ b/maro/rl/experience/experience_manager.py @@ -80,7 +80,7 @@ def __init__( if overwrite_type not in {"rolling", "random"}: raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") if batch_size <= 0 and batch_size != -1: - raise ValueError(f"batch_size must be -1 or a positive integer") + raise ValueError("batch_size must be -1 or a positive integer") self._capacity = capacity self._overwrite_type = overwrite_type self._keys = ExperienceSet.__slots__ @@ -149,7 +149,7 @@ def get(self): in the form of an ``ExperienceSet`` and the memory will be cleared. Otherwise, a sample from the memory will be returned according to the sampling logic defined by the registered sampler. """ - batch_size = self._size if self.batch_size == -1 else self.batch_size + batch_size = self._size if self.batch_size == -1 else self.batch_size if not self.sampler: indexes = np.random.choice(self._size, size=batch_size, replace=self.replace) return ExperienceSet(*[[self.data[key][idx] for idx in indexes] for key in self._keys]) diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/exploration_scheduler.py index ff9a489f4..140e858dd 100644 --- a/maro/rl/exploration/exploration_scheduler.py +++ b/maro/rl/exploration/exploration_scheduler.py @@ -48,7 +48,7 @@ class LinearExplorationScheduler(AbsExplorationScheduler): Args: exploration (AbsExploration): An exploration instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. - last_ep (int): Last episode. + last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is instantiated with will be used as the initial value. Defaults to None. @@ -86,7 +86,7 @@ class MultiPhaseLinearExplorationScheduler(AbsExplorationScheduler): splits (List[Tuple[int, float]]): List of points that separate adjacent linear phases. Each point is a (episode, parameter_value) tuple that indicates the end of one linear phase and the start of another. These points do not have to be given in any particular order. There - cannot be two points with the same first element (episode), or a ``ValueError`` will be raised. + cannot be two points with the same first element (episode), or a ``ValueError`` will be raised. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is instantiated with will be used as the initial value. Defaults to None. diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 883da3345..c981b0105 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -94,7 +94,8 @@ def on_experiences(self, exp: ExperienceSet) -> bool: self.experience_manager.put(exp) self._new_exp_counter += exp.size print( - f"Policy {self._name}: exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" + f"Policy {self._name}: exp mem size = {self.experience_manager.size}, incoming: {exp.size}, " + f"new exp = {self._new_exp_counter}" ) if self.experience_manager.size >= self.warmup and self._new_exp_counter >= self.update_trigger: t0 = time.time() diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py index 810b83c70..72e14b1bd 100644 --- a/maro/rl/training/actor.py +++ b/maro/rl/training/actor.py @@ -122,7 +122,7 @@ def _collect(self, msg): if self.env.state is None: self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") - if hasattr(self, "exploration_dict"): + if hasattr(self, "exploration_dict"): self._logger.info(f"Exploration parameters: {exploration_params}") self.env.reset() @@ -169,7 +169,7 @@ def _collect(self, msg): self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) def _evaluate(self, msg): - self._logger.info(f"Evaluating...") + self._logger.info("Evaluating...") self.eval_env.reset() self.eval_env.start() # get initial state self._load_policy_states(msg.body[MsgKey.POLICY]) diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index e1beeb88f..089ccc389 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -112,9 +112,9 @@ def _train(self, ep: int): # performance details self.logger.debug( - f"ep {ep} summary - " + f"ep {ep} summary - " f"experiences collected: {num_experiences_collected} " - f"total policy update time: {total_policy_update_time}" + f"total policy update time: {total_policy_update_time}" ) self.end_of_episode(ep, **self._end_of_episode_kwargs) diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/local_learner.py index 2de438801..3b274ca7e 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -8,7 +8,7 @@ from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.exploration import AbsExploration -from maro.rl.policy import AbsPolicy, AbsCorePolicy +from maro.rl.policy import AbsCorePolicy, AbsPolicy from maro.utils import Logger from .early_stopper import AbsEarlyStopper @@ -39,8 +39,8 @@ class LocalLearner: as the evaluation environment. Defaults to None. early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the environment metrics are met following an evaluation episode. Default to None. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of - each episode. Defaults to True. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -168,7 +168,7 @@ def _evaluate(self): action = {id_: self._policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} self.eval_env.step(action) - # performance details + # performance details self._logger.info(f"Evaluation result: {self.eval_env.summary}") def _collect(self, ep, segment): diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/policy_manager.py index 0d2f2ac81..28fd7054f 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/policy_manager.py @@ -9,7 +9,7 @@ from maro.communication import Proxy, SessionType from maro.rl.experience import ExperienceSet -from maro.rl.policy import AbsPolicy, AbsCorePolicy +from maro.rl.policy import AbsCorePolicy, AbsPolicy from maro.utils import Logger from .message_enums import MsgKey, MsgTag @@ -70,7 +70,7 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): updated = { name: self.policy_dict[name].get_state() for name, exp in exp_by_policy.items() - if isinstance(self.policy_dict[name], AbsCorePolicy) and self.policy_dict[name].on_experiences(exp) + if isinstance(self.policy_dict[name], AbsCorePolicy) and self.policy_dict[name].on_experiences(exp) } if updated: @@ -83,7 +83,7 @@ def get_state(self): return {name: policy.get_state() for name, policy in self.policy_dict.items()} -class ParallelPolicyManager(AbsPolicyManager): +class ParallelPolicyManager(AbsPolicyManager): def __init__( self, policy2server: Dict[str, str], @@ -108,7 +108,7 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): policy_server_id = self.policy2server[policy_name] if MsgKey.EXPERIENCES not in msg_body_by_dest[policy_server_id]: msg_body_by_dest[policy_server_id][MsgKey.EXPERIENCES] = {} - msg_body_by_dest[policy_server_id][MsgKey.EXPERIENCES][policy_name] = exp + msg_body_by_dest[policy_server_id][MsgKey.EXPERIENCES][policy_name] = exp for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): for policy_name, policy_state in reply.body[MsgKey.POLICY].items(): diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 295bed11d..4b2081e44 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -71,8 +71,8 @@ class LocalRolloutManager(AbsRolloutManager): case the roll-out will be executed until the end of the environment. eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used as the evaluation environment. Defaults to None. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of - each episode. Defaults to True. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -244,8 +244,8 @@ class ParallelRolloutManager(AbsRolloutManager): collected from calls to ``collect`` within ``max_staleness`` calls ago will be returned to the learner. Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. num_eval_actors (int): Number of actors required for evaluation. Defaults to 1. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of - each episode. Defaults to True. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -345,7 +345,7 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic num_finishes += 1 if num_finishes == self.num_actors: break - + if self.episode_complete: if self.exploration_dict: for exploration in self.exploration_dict.values(): From 6208d860a332d2f99421d7e353ddcaa601872653 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 9 Jun 2021 09:32:29 +0000 Subject: [PATCH 297/482] deleted unwanted supply_chain test files --- tests/supply_chain/simple_seller.py | 7 - tests/supply_chain/test_supply_chain.py | 1459 ----------------------- 2 files changed, 1466 deletions(-) delete mode 100644 tests/supply_chain/simple_seller.py delete mode 100644 tests/supply_chain/test_supply_chain.py diff --git a/tests/supply_chain/simple_seller.py b/tests/supply_chain/simple_seller.py deleted file mode 100644 index 04e3c72e1..000000000 --- a/tests/supply_chain/simple_seller.py +++ /dev/null @@ -1,7 +0,0 @@ - -from maro.simulator.scenarios.supply_chain import SellerUnit - - -class SimpleSellerUnit(SellerUnit): - def market_demand(self, tick: int) -> int: - return tick diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py deleted file mode 100644 index 1260a0941..000000000 --- a/tests/supply_chain/test_supply_chain.py +++ /dev/null @@ -1,1459 +0,0 @@ -import os -import unittest - -import numpy as np - -from maro.simulator import Env -from maro.simulator.scenarios.supply_chain import ManufactureAction, ConsumerAction -from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase, VehicleUnit, \ - DistributionUnit, SellerUnit -from maro.simulator.scenarios.supply_chain.units.order import Order - - -def build_env(case_name: str, durations: int): - case_folder = os.path.join("tests", "data", "supply_chain", case_name) - - # config_path = os.path.join(case_folder, "config.yml") - - env = Env(scenario="supply_chain", topology=case_folder, durations=durations) - - return env - - -def get_product_dict_from_storage(env: Env, frame_index: int, node_index: int): - product_list = env.snapshot_list["storage"][frame_index:node_index:"product_list"].flatten().astype(np.int) - product_number = env.snapshot_list["storage"][frame_index:node_index:"product_number"].flatten().astype(np.int) - - return {pid: pnum for pid, pnum in zip(product_list, product_number)} - - -SKU1_ID = 1 -SKU2_ID = 2 -SKU3_ID = 3 -SKU4_ID = 4 - - -class MyTestCase(unittest.TestCase): - """ - manufacture unit testing: - - 1. with input sku - . meet the storage limitation - . not meet the storage limitation - . with enough source sku - . without enough source sku - . with product rate - . without product rate - 2. without input sku - . meet the storage limitation - . not meet the storage limitation - . with product rate - . without product rate - - """ - - def test_manufacture_meet_storage_limitation(self): - """Test sku3 manufacturing.""" - env = build_env("case_01", 100) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space") - - manufacture_nodes = env.snapshot_list["manufacture"] - manufacture_number = len(manufacture_nodes) - manufacture_features = ( - "id", "facility_id", "manufacturing_number", "product_id", - "product_unit_cost" - ) - - ############################### TICK: 0 ###################################### - - # tick 0 passed, no product manufacturing. - env.step(None) - - states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, - -1).astype(np.int) - - # try to find which one is sku3 manufacture unit. - for index, state in enumerate(states): - # Id of sku3 is 3. - if state[3] == SKU3_ID: - sku3_data_model_index = index - sku3_manufacture_id = state[0] - sku3_facility_id = state[1] - - # try to find sku3's storage from env.summary - sku3_storage_index = env.summary["node_mapping"]["facilities"][sku3_facility_id]["units"]["storage"]["node_index"] - - storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) - - # there should be 80 units been taken at the beginning according to the config file. - # so remaining space should be 20 - self.assertEqual(20, storage_states[3]) - # capacity is 100 by config - self.assertEqual(100, storage_states[2]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) - - # number should be same as configuration at beginning. - # 80 sku3 - self.assertEqual(80, product_dict[SKU3_ID]) - - # all the id is greater than 0 - self.assertGreater(sku3_manufacture_id, 0) - - ############################### TICK: 1 ###################################### - - # pass an action to start manufacturing for this tick. - action = ManufactureAction(sku3_manufacture_id, 1) - - env.step({action.id: action}) - - states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) - - # Sku3 produce rate is 1 per tick, so manufacturing_number should be 1. - self.assertEqual(1, states[2]) - - storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) - - # now remaining space should be 19 - self.assertEqual(19, storage_states[3]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) - - # sku3 number should be 80 + 1 - self.assertEqual(80 + 1, product_dict[SKU3_ID]) - - ############################### TICK: 2 ###################################### - - # leave the action as none will cause manufacture unit stop manufacturing. - env.step(None) - - states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) - - # so manufacturing_number should be 0 - self.assertEqual(0, states[2]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) - - # sku3 number should be same as last tick - self.assertEqual(80 + 1, product_dict[SKU3_ID]) - - # let is generate 20, but actually it can only procedure 19 because the storage will reach the limitation - env.step({sku3_manufacture_id: ManufactureAction(sku3_manufacture_id, 20)}) - - states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) - - # so manufacture_number should be 19 instead 20 - self.assertEqual(19, states[2]) - - storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) - - # now remaining space should be 0 - self.assertEqual(0, storage_states[3]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) - - # sku3 number should be 100 - self.assertEqual(80 + 1 + 19, product_dict[SKU3_ID]) - - def test_manufacture_meet_source_lack(self): - """Test sku4 manufacturing, this sku supplier does not have enough source material at the begging - , so it cannot produce anything without consumer purchase.""" - env = build_env("case_01", 100) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space") - - manufacture_nodes = env.snapshot_list["manufacture"] - manufacture_number = len(manufacture_nodes) - manufacture_features = ( - "id", "facility_id", "manufacturing_number", "product_id", - "product_unit_cost" - ) - - ############################### TICK: 0 ###################################### - - # tick 0 passed, no product manufacturing. - env.step(None) - - states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, - -1).astype(np.int) - - # try to find which one is sku3 manufacture unit. - for index, state in enumerate(states): - # Id of sku4 is 4. - if state[3] == SKU4_ID: - sku4_data_model_index = index - sku4_manufacture_id = state[0] - sku4_facility_id = state[1] - - # try to find sku4's storage from env.summary - sku4_storage_index = env.summary["node_mapping"]["facilities"][sku4_facility_id]["units"]["storage"]["node_index"] - - # the storage should be same as initialized (50 + 0). - storage_states = storage_nodes[env.frame_index:sku4_storage_index:storage_features].flatten().astype(np.int) - - # capacity is same as configured. - self.assertEqual(200, storage_states[2]) - - # remaining space should be capacity - (50+0) - self.assertEqual(200 - (50 + 0), storage_states[3]) - - # no manufacture number as we have not pass any action - manufature_states = manufacture_nodes[ - env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype(np.int) - - # manufacturing_number should be 0 - self.assertEqual(0, manufature_states[2]) - - # output product id should be same as configured. - self.assertEqual(4, manufature_states[3]) - - # product unit cost should be same as configured. - self.assertEqual(4, manufature_states[4]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) - - # 50 sku4 at beginning - self.assertEqual(50, product_dict[SKU4_ID]) - - # 0 sku2 - self.assertEqual(0, product_dict[SKU2_ID]) - - ############################### TICK: 1 - end ###################################### - - is_done = False - - while not is_done: - # push to the end, the storage should not changed, no matter what production rate we give it. - _, _, is_done = env.step({sku4_manufacture_id: ManufactureAction(sku4_manufacture_id, 10)}) - - manufature_states = manufacture_nodes[ - env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype( - np.int) - - # manufacturing_number should be 0 - self.assertEqual(0, manufature_states[2]) - - # output product id should be same as configured. - self.assertEqual(SKU4_ID, manufature_states[3]) - - # product unit cost should be same as configured. - self.assertEqual(4, manufature_states[4]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) - - # 50 sku4 at beginning - self.assertEqual(50, product_dict[SKU4_ID]) - - # 0 sku2 - self.assertEqual(0, product_dict[SKU2_ID]) - - def test_manufacture_meet_avg_storage_limitation(self): - """Test on sku1, it is configured with nearly full initial states.""" - - env = build_env("case_01", 100) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space") - - manufacture_nodes = env.snapshot_list["manufacture"] - manufacture_number = len(manufacture_nodes) - manufacture_features = ( - "id", "facility_id", "manufacturing_number", "product_id", - "product_unit_cost" - ) - - ############################### TICK: 0 ###################################### - - # tick 0 passed, no product manufacturing, verified in above case, pass checking it here. - env.step(None) - - states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, - -1).astype(np.int) - # try to find which one is sku3 manufacture unit. - for index, state in enumerate(states): - # Id of sku1 is 1. - if state[3] == SKU1_ID: - sku1_data_model_index = index - sku1_manufacture_id = state[0] - sku1_facility_id = state[1] - - sku1_storage_index = env.summary["node_mapping"]["facilities"][sku1_facility_id]["units"]["storage"]["node_index"] - - ############################### TICK: 1 ###################################### - - # ask sku1 manufacture start manufacturing, rate is 10. - env.step({sku1_manufacture_id: ManufactureAction(sku1_storage_index, 10)}) - - storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) - manufacture_states = manufacture_nodes[ - env.frame_index:sku1_data_model_index:manufacture_features].flatten().astype(np.int) - - # we can produce 4 sku1, as it will meet storage avg limitation per sku - self.assertEqual(4, manufacture_states[2]) - - # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) - self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku1_storage_index) - - # number of sku1 should 100, just reach the avg storage capacity limitation - self.assertEqual(100, product_dict[SKU1_ID]) - - # 4 sku1 cost 4*2 source material (sku3) - self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) - - ############################### TICK: 1 ###################################### - - # then fix the product rate to 20 every tick, but the manufacture will do nothing, as we have to enough space - - is_done = False - - while not is_done: - _, _, is_done = env.step({sku1_manufacture_id: ManufactureAction(sku1_storage_index, 20)}) - - storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) - manufacture_states = manufacture_nodes[ - env.frame_index:sku1_data_model_index:manufacture_features].flatten().astype(np.int) - - # but manufacture number is 0 - self.assertEqual(0, manufacture_states[2]) - - # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) - self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku1_storage_index) - - # number of sku1 should 100, just reach the avg storage capacity limitation - self.assertEqual(100, product_dict[SKU1_ID]) - - # 4 sku1 cost 4*2 source material (sku3) - self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) - - """ - Storage test: - - . take available - . enough - . not enough - . try add products - . meet whole storage capacity limitation - . fail if all - . not fail if all - . enough space - . try take products - . have enough - . not enough - . get product number - - """ - - def test_storage_take_available(self): - env = build_env("case_01", 100) - - env.step(None) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "capacity", "remaining_space") - - # find first storage unit id - storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] - - # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - capacity = storage_states[1] - init_remaining_space = storage_states[2] - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # call take_available for each product in storage. - products_taken = {} - for product_id, product_number in init_product_dict.items(): - num = np.random.randint(0, product_number) - actual_num = storage_unit.take_available(product_id, num) - - # we should get the number we want. - self.assertEqual(num, actual_num) - - products_taken[product_id] = num - - # check if internal state correct - for product_id, num in products_taken.items(): - remaining_num = storage_unit.product_level[product_id] - - self.assertEqual(init_product_dict[product_id] - num, remaining_num) - - # call env.step will cause states write into snapshot - env.step(None) - - product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - for product_id, num in products_taken.items(): - remaining_num = product_dict[product_id] - - self.assertEqual(init_product_dict[product_id] - num, remaining_num) - - # then take more than exist number for 1st product(sku) - lot_taken_product_id, lot_taken_product_number = product_dict.popitem() - - lot_taken_product_number += 100 - - actual_num = storage_unit.take_available(lot_taken_product_id, lot_taken_product_number) - - # we should get all available - self.assertEqual(actual_num, lot_taken_product_number - 100) - - # take snapshot - env.step(None) - - product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # the product number should be 0, as we took all available - self.assertEqual(0, product_dict[lot_taken_product_id]) - - def test_storage_try_add_products(self): - """ - NOTE: - try_add_products method do not check avg storage capacity checking, so we will ignore it here. - - """ - env = build_env("case_01", 100) - - env.step(None) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "capacity", "remaining_space") - - # find first storage unit id - storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] - - # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - capacity = storage_states[1] - init_remaining_space = storage_states[2] - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - first_product_id = [id for id in init_product_dict.keys()][0] - - # try put products out of capacity with all_or_nothing == True - products_to_put = {} - - avg_max_product_number = init_remaining_space // len(init_product_dict) - - for product_id in init_product_dict.keys(): - products_to_put[product_id] = avg_max_product_number + 1 - - result = storage_unit.try_add_products(products_to_put, all_or_nothing=True) - - # the method will return an empty dictionary if fail to add - self.assertEqual(0, len(result)) - - # so remaining space should not change - self.assertEqual(init_remaining_space, storage_unit.remaining_space) - - # each product number should be same as before - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_level[product_id]) - - # if we set all_or_nothing=False, then part of the product will be added to storage, and cause remaining space being 0 - result = storage_unit.try_add_products(products_to_put, all_or_nothing=False) - - self.assertEqual(0, storage_unit.remaining_space) - - # take snapshot - env.step(None) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - # remaining space in snapshot should be 0 - self.assertEqual(0, storage_states[2]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # total product number should be same as capacity - self.assertEqual(capacity, sum(product_dict.values())) - - #################################################### - #################################################### - # reset the env for next case - env.reset() - - # check the state after reset - self.assertEqual(capacity, storage_unit.capacity) - self.assertEqual(init_remaining_space, storage_unit.remaining_space) - - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_level[product_id]) - - def test_storage_try_take_products(self): - env = build_env("case_01", 100) - - env.step(None) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "capacity", "remaining_space") - - # find first storage unit id - storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] - - # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - capacity = storage_states[1] - init_remaining_space = storage_states[2] - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - product_to_take = {} - - for product_id, product_number in init_product_dict.items(): - product_to_take[product_id] = product_number + 1 - - # which this setting, it will return false, as no enough product for ous - self.assertFalse(storage_unit.try_take_products(product_to_take)) - - # so remaining space and product number should same as before - self.assertEqual(init_remaining_space, storage_unit.remaining_space) - - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, storage_unit.product_level[product_id]) - - # try to get all products - for product_id, product_number in product_to_take.items(): - product_to_take[product_id] = product_number - 1 - - self.assertTrue(storage_unit.try_take_products(product_to_take)) - - # now the remaining space should be same as capacity as we take all - self.assertEqual(capacity, storage_unit.remaining_space) - - # take snapshot - env.step(None) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - # remaining space should be same as capacity in snapshot - self.assertEqual(storage_states[1], storage_states[2]) - - def test_storage_get_product_number(self): - env = build_env("case_01", 100) - - env.step(None) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "capacity", "remaining_space") - - # find first storage unit id - storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] - - # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # number in object should be same with states - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_level[product_id]) - - # should not change even after reset - env.reset() - env.step(None) - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # number in object should be same with states - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_level[product_id]) - - """ - - Consumer test: - - . initial state - . state after reset - . set_action directly from code - . set_action by env.step - . call on_order_reception directly to simulation order arrived - . call update_open_orders directly - - """ - - def test_consumer_init_state(self): - """ - NOTE: we will use consumer on Supplier_SKU1, as it contains a source for sku3 (Supplier_SKU3) - """ - env = build_env("case_01", 100) - - # print(env.summary) - # we can get the consumer from env.summary - - # NOTE: though we are test with sku1, but the consumer is for sku3, as it is the source material from source - sku3_consumer_unit: ConsumerUnit - sku3_supplier_faiclity_id: int - sku3_consumer_data_model_index: int - sku3_product_unit_id: int - - for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): - if facility_defail["name"] == "Supplier_SKU1": - # try to find sku3 consumer - sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) - sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] - - if facility_defail["name"] == "Supplier_SKU3": - sku3_supplier_faiclity_id = facility_defail["id"] - - sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] - - # check initial state - self.assertEqual(0, sku3_consumer_unit.received) - self.assertEqual(0, sku3_consumer_unit.purchased) - self.assertEqual(0, sku3_consumer_unit.order_product_cost) - self.assertEqual(SKU3_ID, sku3_consumer_unit.product_id) - - # check data model state - # order cost from configuration - self.assertEqual(200, sku3_consumer_unit.data_model.order_cost) - self.assertEqual(0, sku3_consumer_unit.data_model.total_purchased) - self.assertEqual(0, sku3_consumer_unit.data_model.total_received) - - # NOTE: 0 is an invalid(initial) id - self.assertEqual(SKU3_ID, sku3_consumer_unit.data_model.product_id) - self.assertEqual(sku3_consumer_unit_id, sku3_consumer_unit.data_model.id) - self.assertEqual(sku3_product_unit_id, sku3_consumer_unit.data_model.product_unit_id) - self.assertEqual(0, sku3_consumer_unit.data_model.order_quantity) - self.assertEqual(0, sku3_consumer_unit.data_model.purchased) - self.assertEqual(0, sku3_consumer_unit.data_model.received) - self.assertEqual(0, sku3_consumer_unit.data_model.order_product_cost) - - # check sources - for source_facility_id in sku3_consumer_unit.sources: - source_facility: FacilityBase = env._business_engine.world.get_facility_by_id(source_facility_id) - - # check if source facility contains the sku3 config - self.assertTrue(SKU3_ID in source_facility.skus) - - env.step(None) - - # check state - features = ( - "id", - "facility_id", - "product_id", - "order_cost", - "total_purchased", - "total_received", - "order_quantity", - "purchased", - "received", - "order_product_cost" - ) - - consumer_nodes = env.snapshot_list["consumer"] - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # Nothing happened at tick 0, so most states will be 0 - self.assertTrue((states[4:] == 0).all()) - - self.assertEqual(sku3_consumer_unit_id, states[0]) - self.assertEqual(SKU3_ID, states[2]) - - env.reset() - env.step(None) - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # Nothing happened at tick 0, so most states will be 0 - self.assertTrue((states[4:] == 0).all()) - - self.assertEqual(sku3_consumer_unit_id, states[0]) - self.assertEqual(SKU3_ID, states[2]) - - def test_consumer_action(self): - env = build_env("case_01", 100) - - sku3_consumer_unit: ConsumerUnit - sku3_supplier_faiclity_id: int - sku3_consumer_data_model_index: int - sku3_product_unit_id: int - - for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): - if facility_defail["name"] == "Supplier_SKU1": - sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) - sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] - - if facility_defail["name"] == "Supplier_SKU3": - sku3_supplier_faiclity_id = facility_defail["id"] - - sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] - - # zero quantity will be ignore - action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 0, 1, 0) - - action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 10, 1, 0) - - sku3_consumer_unit.set_action(action_with_zero) - - env.step(None) - - features = ( - "id", - "facility_id", - "product_id", - "order_cost", - "total_purchased", - "total_received", - "product_id", - "order_quantity", - "purchased", - "received", - "order_product_cost" - ) - - consumer_nodes = env.snapshot_list["consumer"] - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # Nothing happened at tick 0, at the action will be recorded - self.assertEqual(action_with_zero.product_id, states[6]) - self.assertEqual(action_with_zero.quantity, states[8]) - - self.assertEqual(sku3_consumer_unit_id, states[0]) - self.assertEqual(SKU3_ID, states[2]) - - # NOTE: we cannot set_action directly here, as post_step will clear the action before starting next tick - env.step({action.id: action}) - - self.assertEqual(action.quantity, sku3_consumer_unit.purchased) - self.assertEqual(0, sku3_consumer_unit.received) - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # action field should be recorded - self.assertEqual(action.product_id, states[6]) - self.assertEqual(action.quantity, states[8]) - - # total purchased should be same as purchased at this tick. - self.assertEqual(action.quantity, states[4]) - - # no received now - self.assertEqual(0, states[5]) - - # purchased same as quantity - self.assertEqual(action.quantity, states[8]) - - # no receives - self.assertEqual(0, states[9]) - - # same action for next step, so total_XXX will be changed to double - env.step({action.id: action}) - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # action field should be recorded - self.assertEqual(action.product_id, states[6]) - self.assertEqual(action.quantity, states[8]) - - # total purchased should be same as purchased at this tick. - self.assertEqual(action.quantity * 2, states[4]) - - # no received now - self.assertEqual(0, states[5]) - - # purchased same as quantity - self.assertEqual(action.quantity, states[8]) - - # no receives - self.assertEqual(0, states[9]) - - def test_consumer_on_order_reception(self): - env = build_env("case_01", 100) - - sku3_consumer_unit: ConsumerUnit - sku3_supplier_facility_id: int - sku3_consumer_data_model_index: int - sku3_product_unit_id: int - - for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): - if facility_defail["name"] == "Supplier_SKU1": - sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) - sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] - - if facility_defail["name"] == "Supplier_SKU3": - sku3_supplier_facility_id = facility_defail["id"] - - sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] - - action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1, 0) - - # 1st step must none action - env.step(None) - - env.step({action.id: action}) - - # simulate purchased product is arrived by vehicle unit - sku3_consumer_unit.on_order_reception(sku3_supplier_facility_id, SKU3_ID, 10, 10) - - # now all order is done - self.assertEqual(0, sku3_consumer_unit.open_orders[sku3_supplier_facility_id][SKU3_ID]) - self.assertEqual(10, sku3_consumer_unit.received) - - env.step(None) - - consumer_nodes = env.snapshot_list["consumer"] - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"received"].flatten().astype(np.int) - - # NOTE: we cannot test the received state by calling on_order_reception directly, - # as it will be cleared by env.step, do it on vehicle unit test. - - """ - Vehicle unit test: - - . initial state - . if vehicle arrive at destination within special vlt - . schedule job - . try_load until patient <= 0 to cancel the schedule - . try_load until patient > 0 to load order - . try_unload - . target storage cannot take all - . target storage can take all - """ - - def test_vehicle_unit_state(self): - env = build_env("case_02", 100) - - # try to find first vehicle unit we meet - vehicle_unit: VehicleUnit - vehicle_unit_id: int - vehicle_unit_data_model_index: int - - for id, info in env.summary["node_mapping"]["unit_mapping"].items(): - if info[0] == "vehicle": - vehicle_unit_id = id - vehicle_unit = env._business_engine.world.get_entity(id) - vehicle_unit_data_model_index = vehicle_unit.data_model_index - - break - - # check initial state according to configuration file - self.assertEqual(10, vehicle_unit.max_patient) - - self.assertEqual(0, vehicle_unit.requested_quantity) - # not destination at first - self.assertIsNone(vehicle_unit.destination) - # no path - self.assertIsNone(vehicle_unit.path) - # no product - self.assertEqual(0, vehicle_unit.product_id) - # no steps - self.assertEqual(0, vehicle_unit.steps) - # - self.assertEqual(0, vehicle_unit.payload) - # - self.assertIsNone(vehicle_unit.product) - # - self.assertEqual(0, vehicle_unit.location) - # - self.assertEqual(0, vehicle_unit.velocity) - - # state in frame - self.assertEqual(0, vehicle_unit.data_model.payload) - self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - - # reset to check again - env.step(None) - env.reset() - - # check initial state according to configuration file - self.assertEqual(10, vehicle_unit.max_patient) - - # not destination at first - self.assertIsNone(vehicle_unit.destination) - # no path - self.assertIsNone(vehicle_unit.path) - # no product - self.assertEqual(0, vehicle_unit.product_id) - # no steps - self.assertEqual(0, vehicle_unit.steps) - # - self.assertEqual(0, vehicle_unit.payload) - # - self.assertIsNone(vehicle_unit.product) - # - self.assertEqual(0, vehicle_unit.location) - # - self.assertEqual(0, vehicle_unit.velocity) - # - self.assertEqual(0, vehicle_unit.requested_quantity) - - # state in frame - self.assertEqual(0, vehicle_unit.data_model.payload) - self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - - def test_vehicle_unit_schedule(self): - env = build_env("case_02", 100) - - # try to find first vehicle unit of Supplier - vehicle_unit: VehicleUnit - dest_facility: FacilityBase - - for id, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - # make sure the upstream in the only one supplier in config - self.assertEqual(1, len(dest_facility.upstreams)) - self.assertEqual(1, len(dest_facility.upstreams[SKU3_ID])) - - # schedule job vehicle unit manually, from supplier to warehouse - vehicle_unit.schedule(dest_facility, SKU3_ID, 20, 2) - - # step to take snapshot - env.step(None) - - vehicle_nodes = env.snapshot_list["vehicle"] - - # check internal states - self.assertEqual(dest_facility, vehicle_unit.destination) - self.assertEqual(SKU3_ID, vehicle_unit.product_id) - self.assertEqual(20, vehicle_unit.requested_quantity) - self.assertEqual(2, vehicle_unit.velocity) - # 6/2 - self.assertEqual(3, vehicle_unit.steps) - - features = ( - "id", - "facility_id", - "payload", - "unit_transport_cost" - ) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # source id - self.assertEqual(vehicle_unit.facility.id, states[1]) - # payload should be 20, as we already env.step - self.assertEqual(20, states[2]) - - # push the vehicle on the way - env.step(None) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # payload - self.assertEqual(20, states[2]) - - env.step(None) - env.step(None) - - # next step vehicle will try to unload the products - env.step(None) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # the product is unloaded, vehicle states will be reset to initial - # not destination at first - self.assertIsNone(vehicle_unit.destination) - self.assertIsNone(vehicle_unit.path) - self.assertEqual(0, vehicle_unit.product_id) - self.assertEqual(0, vehicle_unit.steps) - self.assertEqual(0, vehicle_unit.payload) - self.assertIsNone(vehicle_unit.product) - self.assertEqual(0, vehicle_unit.location) - self.assertEqual(0, vehicle_unit.velocity) - self.assertEqual(0, vehicle_unit.requested_quantity) - - # check states - self.assertEqual(0, states[2]) - self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - - def test_vehicle_unit_no_patient(self): - """ - NOTE: with patient is tried in above case after schedule the job - """ - env = build_env("case_02", 100) - - # try to find first vehicle unit of Supplier - vehicle_unit: VehicleUnit - dest_facility: FacilityBase - - for id, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - # there is 80 sku3 in supplier, lets schedule a job for 100, to make sure it will fail to try load - vehicle_unit.schedule(dest_facility, SKU3_ID, 100, 3) - - # push env to next step - env.step(None) - - self.assertEqual(100, vehicle_unit.requested_quantity) - - # the patient will -1 as no enough product so load - self.assertEqual(10 - 1, vehicle_unit.patient) - - # no payload - self.assertEqual(0, vehicle_unit.payload) - self.assertEqual(0, vehicle_unit.data_model.payload) - - # step 9 ticks, patient will be 0 - for i in range(10 - 1): - env.step(None) - - self.assertEqual(10 - 1 - (i + 1), vehicle_unit.patient) - - vehicle_nodes = env.snapshot_list["vehicle"] - features = ( - "id", - "facility_id", - "payload", - "unit_transport_cost" - ) - - states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) - - # no payload from start to now - self.assertListEqual([0] * 10, list(states)) - - # push env to next step, vehicle will be reset to initial state - env.step(None) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # the product is unloaded, vehicle states will be reset to initial - # not destination at first - self.assertIsNone(vehicle_unit.destination) - self.assertIsNone(vehicle_unit.path) - self.assertEqual(0, vehicle_unit.product_id) - self.assertEqual(0, vehicle_unit.steps) - self.assertEqual(0, vehicle_unit.payload) - self.assertIsNone(vehicle_unit.product) - self.assertEqual(0, vehicle_unit.location) - self.assertEqual(0, vehicle_unit.velocity) - self.assertEqual(0, vehicle_unit.requested_quantity) - - # check states - - self.assertEqual(0, states[2]) - self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - - def test_vehicle_unit_cannot_unload_at_destination(self): - """ - NOTE: If vehicle cannot unload at destination, it will keep waiting, until success to unload. - - """ - env = build_env("case_02", 100) - - # try to find first vehicle unit of Supplier - vehicle_unit: VehicleUnit - dest_facility: FacilityBase - - for id, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - # move all 80 sku3 to destination, will cause vehicle keep waiting there - vehicle_unit.schedule(dest_facility, SKU3_ID, 80, 2) - - # step to the end. - is_done = False - - while not is_done: - _, _, is_done = env.step(None) - - vehicle_nodes = env.snapshot_list["vehicle"] - features = ( - "id", - "facility_id", - "source", - "destination", - "payload", - "product_id", - "requested_quantity", - "steps", - "unit_transport_cost" - ) - - # payload should be 80 for first 4 ticks, as it is on the way - # then it will unload 100 - 10 - 10 - 10 = 70 products, as this is the remaining space of destination storage - # so then it will keep waiting to unload remaining 10 - payload_states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) - self.assertListEqual([80] * 4 + [10] * 96, list(payload_states)) - - """ - Distribution unit test: - - . initial state - . place order - . dispatch orders without available vehicle - . dispatch order with vehicle - """ - - def test_distribution_unit_initial_state(self): - env = build_env("case_02", 100) - - # try to find first vehicle unit of Supplier - dist_unit: DistributionUnit - dest_facility: FacilityBase - - for id, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - self.assertEqual(0, len(dist_unit.order_queue)) - - # reset - env.reset() - - self.assertEqual(0, len(dist_unit.order_queue)) - - def test_distribution_unit_dispatch_order(self): - env = build_env("case_02", 100) - - # try to find first vehicle unit of Supplier - dist_unit: DistributionUnit - dest_facility: FacilityBase - - for id, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - first_vehicle: VehicleUnit = dist_unit.vehicles[0] - - order = Order(dest_facility, SKU3_ID, 10, 2) - - dist_unit.place_order(order) - - # check if order is saved - self.assertEqual(1, len(dist_unit.order_queue)) - - # check get pending order correct - pending_order = dist_unit.get_pending_order() - - self.assertDictEqual({3: 10}, pending_order) - - # same as vehicle schedule case, distribution will try to schedule this order to vehicles from beginning to end - # so it will dispatch this order to first vehicle - env.step(None) - - self.assertEqual(dest_facility, first_vehicle.destination) - self.assertEqual(10, first_vehicle.requested_quantity) - self.assertEqual(2, first_vehicle.velocity) - self.assertEqual(SKU3_ID, first_vehicle.product_id) - - # since we already test vehicle unit, do not check the it again here - - # add another order to check pending order - dist_unit.place_order(order) - - pending_order = dist_unit.get_pending_order() - - self.assertDictEqual({3: 10}, pending_order) - - # another order, will cause the pending order increase - dist_unit.place_order(order) - - pending_order = dist_unit.get_pending_order() - - # 2 pending orders - self.assertDictEqual({3: 20}, pending_order) - - # now we have only one available vehicle, 2 pending order - # next step will cause delay_order_penalty - env.step(None) - - second_vehicle = dist_unit.vehicles[1] - - self.assertEqual(dest_facility, second_vehicle.destination) - self.assertEqual(10, second_vehicle.requested_quantity) - self.assertEqual(2, second_vehicle.velocity) - self.assertEqual(SKU3_ID, second_vehicle.product_id) - - """ - Seller unit test: - . initial state - . with a customized seller unit - . with built in one - """ - - def test_seller_unit_initial_states(self): - env = build_env("case_02", 100) - - # find seller for sku3 from retailer facility - sell_unit: SellerUnit - source_facility: FacilityBase - - for id, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Retailer_001": - for pid, pdetail in info["units"]["products"].items(): - if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) - - if info["name"] == "Warehouse_001": - source_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - # from configuration - self.assertEqual(10, sell_unit.gamma) - - self.assertEqual(0, sell_unit.sold) - self.assertEqual(0, sell_unit.demand) - self.assertEqual(0, sell_unit.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - # - self.assertEqual(0, sell_unit.data_model.sold) - self.assertEqual(0, sell_unit.data_model.demand) - self.assertEqual(0, sell_unit.data_model.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - env.reset() - - # from configuration - self.assertEqual(10, sell_unit.gamma) - self.assertEqual(0, sell_unit.sold) - self.assertEqual(0, sell_unit.demand) - self.assertEqual(0, sell_unit.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - # - self.assertEqual(0, sell_unit.data_model.sold) - self.assertEqual(0, sell_unit.data_model.demand) - self.assertEqual(0, sell_unit.data_model.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - def test_seller_unit_demand_states(self): - env = build_env("case_02", 100) - - # find seller for sku3 from retailer facility - sell_unit: SellerUnit - source_facility: FacilityBase - - for id, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Retailer_001": - for pid, pdetail in info["units"]["products"].items(): - if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) - - if info["name"] == "Warehouse_001": - source_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - SKU3_INIT_NUMBER = sell_unit.facility.skus[SKU3_ID].init_stock - - env.step(None) - - # seller unit will try to count down the product number base on demand - # default seller use gamma distribution on each tick - demand = sell_unit.demand - - # demand should be same with original - self.assertEqual(demand, sell_unit.data_model.demand) - - actual_sold = min(demand, SKU3_INIT_NUMBER) - # sold may be not same as demand, depend on remaining number in storage - self.assertEqual(actual_sold, sell_unit.sold) - self.assertEqual(actual_sold, sell_unit.data_model.sold) - self.assertEqual(actual_sold, sell_unit.total_sold) - self.assertEqual(actual_sold, sell_unit.data_model.total_sold) - - states = env.snapshot_list["seller"][ - env.frame_index:sell_unit.data_model_index:("sold", "demand", "total_sold")].flatten().astype(np.int) - - self.assertEqual(actual_sold, states[0]) - self.assertEqual(demand, states[1]) - self.assertEqual(actual_sold, states[2]) - - # move to next step to check if state is correct - env.step(None) - - demand = sell_unit.demand - - # demand should be same with original - self.assertEqual(demand, sell_unit.data_model.demand) - - actual_sold_2 = min(demand, SKU3_INIT_NUMBER - actual_sold) - - # sold may be not same as demand, depend on remaining number in storage - self.assertEqual(actual_sold_2, sell_unit.sold) - self.assertEqual(actual_sold_2, sell_unit.data_model.sold) - self.assertEqual(actual_sold + actual_sold_2, sell_unit.total_sold) - self.assertEqual(actual_sold + actual_sold_2, sell_unit.data_model.total_sold) - - states = env.snapshot_list["seller"][ - env.frame_index:sell_unit.data_model_index:("sold", "demand", "total_sold")].flatten().astype(np.int) - - self.assertEqual(actual_sold_2, states[0]) - self.assertEqual(demand, states[1]) - self.assertEqual(actual_sold + actual_sold_2, states[2]) - - def test_seller_unit_customized(self): - env = build_env("case_03", 100) - - # find seller for sku3 from retailer facility - sell_unit: SellerUnit - source_facility: FacilityBase - - for id, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Retailer_001": - for pid, pdetail in info["units"]["products"].items(): - if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) - - if info["name"] == "Warehouse_001": - source_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - # NOTE: - # this simple seller unit return demands that same as current tick - env.step(None) - - # so tick 0 will have demand == 0 - # from configuration - self.assertEqual(0, sell_unit.sold) - self.assertEqual(0, sell_unit.demand) - self.assertEqual(0, sell_unit.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - # - self.assertEqual(0, sell_unit.data_model.sold) - self.assertEqual(0, sell_unit.data_model.demand) - self.assertEqual(0, sell_unit.data_model.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - is_done = False - - while not is_done: - _, _, is_done = env.step(None) - - # check demand history, it should be same as tick - seller_nodes = env.snapshot_list["seller"] - - demand_states = seller_nodes[:sell_unit.data_model_index:"demand"].flatten().astype(np.int) - - self.assertListEqual([i for i in range(100)], list(demand_states)) - - # check sold states - # it should be 0 after tick 4 - sold_states = seller_nodes[:sell_unit.data_model_index:"sold"].flatten().astype(np.int) - self.assertListEqual([0, 1, 2, 3, 4] + [0] * 95, list(sold_states)) - - # total sold - total_sold_states = seller_nodes[:sell_unit.data_model_index:"total_sold"].flatten().astype(np.int) - # total sold will keep same after tick 4 - self.assertListEqual([0, 1, 3, 6, 10] + [10] * 95, list(total_sold_states)) - - def test_outer_seller(self): - env = build_env("case_04", 100) - - index2unitid_mapping = {} - skuid2index_mapping = {} - - # we only have one manufacture here - man_id = None - - # find all the sellers - for unit_id, unit_detail in env.summary["node_mapping"]["unit_mapping"].items(): - if unit_detail[0] == "seller": - index = unit_detail[1] - sku = unit_detail[3] - index2unitid_mapping[index] = unit_id - skuid2index_mapping[sku.id] = index - - if unit_detail[0] == "manufacture": - man_id = unit_id - - # tick 0 - env.step(None) - - seller_states = env.snapshot_list["seller"][0::"demand"].flatten().astype(np.int) - - # at tick 0 (2010-12-01) - # sku1 have 10 demand - self.assertEqual(10, seller_states[skuid2index_mapping[SKU1_ID]]) - - # sku2 have 20 demand - self.assertEqual(20, seller_states[skuid2index_mapping[SKU2_ID]]) - - # sku3 have 30 demand - self.assertEqual(30, seller_states[skuid2index_mapping[SKU3_ID]]) - - # tick 1 - env.step(None) - - seller_states = env.snapshot_list["seller"][1::"demand"].flatten().astype(np.int) - - # at tick 1 (2010-12-02) - # sku1 have 0 demand (no record in file) - self.assertEqual(0, seller_states[skuid2index_mapping[SKU1_ID]]) - - # sku2 have 20 demand - self.assertEqual(21, seller_states[skuid2index_mapping[SKU2_ID]]) - - # sku3 have 30 demand - self.assertEqual(31, seller_states[skuid2index_mapping[SKU3_ID]]) - - # tick 2 - env.step(None) - - seller_states = env.snapshot_list["seller"][2::"demand"].flatten().astype(np.int) - - # at tick 2 (2010-12-03) - # sku1 have 13 demand - self.assertEqual(13, seller_states[skuid2index_mapping[SKU1_ID]]) - - # sku2 have 20 demand - self.assertEqual(22, seller_states[skuid2index_mapping[SKU2_ID]]) - - # sku3 have 30 demand - self.assertEqual(0, seller_states[skuid2index_mapping[SKU3_ID]]) - - # test if simple manufacture work correctly. - - action = ManufactureAction(man_id, 10) - - env.step({man_id: action}) - - man_states = env.snapshot_list["manufacture"][env.tick::"manufacturing_number"].flatten().astype(np.int) - - self.assertEqual(action.production_rate, man_states[0]) - - -if __name__ == '__main__': - unittest.main() From 11ca4be00dc78e426171fac1f642118677f82b04 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 9 Jun 2021 09:48:36 +0000 Subject: [PATCH 298/482] revised default config for cim-dqn --- examples/cim/dqn/config.yml | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 594b85846..07972912e 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -3,15 +3,14 @@ experiment_name: cim_dqn mode: multi-process # local, multi-process -num_episodes: 3 +num_episodes: 100 # eval_schedule: 2 num_steps: -1 env: basic: scenario: cim topology: global_trade.22p_l0.0 - # topology: global_trade.22p_l0.0 - durations: 560 + durations: 1120 wrapper: port_attributes: - empty @@ -41,30 +40,14 @@ exploration: initial_value: 0.4 final_value: 0.0 splits: - - [2, 0.3] + - [50, 0.32] policy: model: network: hidden_dims: - 256 - - 256 - - 256 - - 256 - - 256 - - 128 - - 128 - - 128 - - 128 - 128 - 64 - - 64 - - 64 - - 64 - - 64 - - 32 - - 32 - - 32 - - 32 - 32 activation: leaky_relu softmax: false @@ -97,11 +80,11 @@ policy: update_trigger: 16 warmup: 1 multi-process: - rollout_mode: local # local, parallel + rollout_mode: parallel # local, parallel policy_training_mode: parallel # local, parallel group: cim-dqn - num_actors: 2 - num_policy_servers: 3 + num_actors: 3 + num_policy_servers: 2 redis_host: localhost redis_port: 6379 # max_receive_attempts: 2 From 36d41783c36e78a23e039bf2903d4176c7741e4d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 10 Jun 2021 07:48:27 +0000 Subject: [PATCH 299/482] removed test_store.py as it is no longer needed --- tests/test_store.py | 67 --------------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 tests/test_store.py diff --git a/tests/test_store.py b/tests/test_store.py deleted file mode 100644 index 82dc3e24b..000000000 --- a/tests/test_store.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import unittest - -from maro.rl import ExperienceManager - - -class TestUnboundedStore(unittest.TestCase): - def setUp(self) -> None: - self.store = ExperienceManager(["a", "b", "c"]) - - def tearDown(self) -> None: - self.store.clear() - - def test_put(self): - indexes = self.store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) - expected = [0, 1, 2] - self.assertEqual(indexes, expected, msg=f"expected returned indexes = {expected}, got {indexes}") - indexes = self.store.put({"a": [10, 11], "b": [12, 13], "c": [14, 15]}) - expected = [3, 4] - self.assertEqual(indexes, expected, msg=f"expected returned indexes = {expected}, got {indexes}") - - def test_get(self): - self.store.put({"a": [1, 2, 3, 4], "b": [5, 6, 7, 8], "c": [9, 10, 11, 12]}) - indexes = [1, 3] - actual = self.store.get(indexes) - expected = {"a": [2, 4], "b": [6, 8], "c": [10, 12]} - self.assertEqual(actual, expected, msg=f"expected {expected}, got {actual}") - - def test_update(self): - self.store.put({"a": [1, 2, 3, 4, 5], "b": [6, 7, 8, 9, 10], "c": [11, 12, 13, 14, 15]}) - self.store.update([0, 3], {"a": [-1, -4], "c": [-11, -14]}) - actual = self.store.dumps() - expected = {"a": [-1, 2, 3, -4, 5], "b": [6, 7, 8, 9, 10], "c": [-11, 12, 13, -14, 15]} - self.assertEqual(actual, expected, msg=f"expected store content = {expected}, got {actual}") - - def test_filter(self): - self.store.put({"a": [1, 2, 3, 4, 5], "b": [6, 7, 8, 9, 10], "c": [11, 12, 13, 14, 15]}) - result = self.store.apply_multi_filters(filters=[lambda x: x["a"] > 2, lambda x: sum(x.values()) % 2 == 0])[1] - expected = {"a": [3, 5], "b": [8, 10], "c": [13, 15]} - self.assertEqual(result, expected, msg=f"expected {expected}, got {result}") - - -class TestFixedSizeStore(unittest.TestCase): - def test_put_with_rolling_overwrite(self): - store = ExperienceManager(["a", "b", "c"], capacity=5, overwrite_type="rolling") - indexes = store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) - expected = [0, 1, 2] - self.assertEqual(indexes, expected, msg=f"expected indexes = {expected}, got {indexes}") - indexes = store.put({"a": [10, 11, 12, 13], "b": [14, 15, 16, 17], "c": [18, 19, 20, 21]}) - expected = [-2, -1, 0, 1] - self.assertEqual(indexes, expected, msg=f"expected indexes = {expected}, got {indexes}") - actual = store.dumps() - expected = {"a": [12, 13, 3, 10, 11], "b": [16, 17, 6, 14, 15], "c": [20, 21, 9, 18, 19]} - self.assertEqual(actual, expected, msg=f"expected store content = {expected}, got {actual}") - - def test_put_with_random_overwrite(self): - store = ExperienceManager(["a", "b", "c"], capacity=5, overwrite_type="random") - indexes = store.put({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) - indexes_2 = store.put({"a": [10, 11, 12, 13], "b": [14, 15, 16, 17], "c": [18, 19, 20, 21]}) - for i in indexes_2[2:]: - self.assertIn(i, indexes, msg=f"expected overwrite index in {indexes}, got {i}") - - -if __name__ == "__main__": - unittest.main() From 0fab08b7c599b5d846919a35febd16a3abfa3d4e Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 11 Jun 2021 11:41:34 +0000 Subject: [PATCH 300/482] 1. changed Actor class to rollout_worker function; 2. renamed algorithm to algorithms --- docs/source/apidoc/maro.rl.rst | 124 +++++++----- docs/source/key_components/rl_toolkit.rst | 14 +- examples/cim/dqn/config.yml | 2 +- examples/cim/dqn/main.py | 91 ++++----- examples/cim/dqn/policy.py | 31 +++ examples/cim/dqn/qnet.py | 15 -- maro/rl/__init__.py | 12 +- maro/rl/{algorithm => algorithms}/__init__.py | 0 maro/rl/{algorithm => algorithms}/ac.py | 0 maro/rl/{algorithm => algorithms}/ddpg.py | 0 maro/rl/{algorithm => algorithms}/dqn.py | 0 maro/rl/{algorithm => algorithms}/pg.py | 0 .../rl_policy_index.py | 0 maro/rl/training/__init__.py | 12 +- maro/rl/training/actor.py | 191 ------------------ maro/rl/training/decision_generator.py | 75 +++++++ maro/rl/training/learner.py | 20 +- maro/rl/training/message_enums.py | 4 +- maro/rl/training/policy_server.py | 2 +- maro/rl/training/rollout_manager.py | 82 ++++---- maro/rl/training/rollout_worker.py | 152 ++++++++++++++ ...{policy_manager.py => training_manager.py} | 10 +- 22 files changed, 447 insertions(+), 390 deletions(-) create mode 100644 examples/cim/dqn/policy.py delete mode 100644 examples/cim/dqn/qnet.py rename maro/rl/{algorithm => algorithms}/__init__.py (100%) rename maro/rl/{algorithm => algorithms}/ac.py (100%) rename maro/rl/{algorithm => algorithms}/ddpg.py (100%) rename maro/rl/{algorithm => algorithms}/dqn.py (100%) rename maro/rl/{algorithm => algorithms}/pg.py (100%) rename maro/rl/{algorithm => algorithms}/rl_policy_index.py (100%) delete mode 100644 maro/rl/training/actor.py create mode 100644 maro/rl/training/decision_generator.py create mode 100644 maro/rl/training/rollout_worker.py rename maro/rl/training/{policy_manager.py => training_manager.py} (94%) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index 50f1f950d..97c8b78cf 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -1,58 +1,66 @@ -Agent +Algorithms ================================================================================ -maro.rl.agent.abs\_agent +maro.rl.algorithms.ac -------------------------------------------------------------------------------- -.. automodule:: maro.rl.agent.abs_policy +.. automodule:: maro.rl.algorithms.ac :members: :undoc-members: :show-inheritance: -maro.rl.agent.dqn +maro.rl.algorithms.dqn -------------------------------------------------------------------------------- -.. automodule:: maro.rl.agent.dqn +.. automodule:: maro.rl.algorithms.dqn :members: :undoc-members: :show-inheritance: -maro.rl.agent.ddpg +maro.rl.algorithms.ddpg -------------------------------------------------------------------------------- -.. automodule:: maro.rl.agent.ddpg +.. automodule:: maro.rl.algorithms.ddpg :members: :undoc-members: :show-inheritance: -maro.rl.agent.policy\_optimization +maro.rl.algorithms.pg -------------------------------------------------------------------------------- -.. automodule:: maro.rl.agent.policy_optimization +.. automodule:: maro.rl.agent.pg :members: :undoc-members: :show-inheritance: -Agent Manager +Environment Wrapper ================================================================================ -maro.rl.agent.abs\_agent\_manager +maro.rl.env_wrapper.env_wrapper -------------------------------------------------------------------------------- -.. automodule:: maro.rl.agent.abs_policy_manager +.. automodule:: maro.rl.env_wrapper.env_wrapper :members: :undoc-members: :show-inheritance: -Model +Experience ================================================================================ -maro.rl.model.learning\_model +maro.rl.experience.experience_manager +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.experience.experience_manager + :members: + :undoc-members: + :show-inheritance: + +maro.rl.experience.sampler -------------------------------------------------------------------------------- -.. automodule:: maro.rl.model.torch.learning_model +.. automodule:: maro.rl.experience.sampler :members: :undoc-members: :show-inheritance: @@ -61,7 +69,7 @@ maro.rl.model.learning\_model Exploration ================================================================================ -maro.rl.exploration.abs\_exploration +maro.rl.exploration.abs_exploration -------------------------------------------------------------------------------- .. automodule:: maro.rl.exploration.abs_exploration @@ -69,7 +77,7 @@ maro.rl.exploration.abs\_exploration :undoc-members: :show-inheritance: -maro.rl.exploration.epsilon\_greedy\_exploration +maro.rl.exploration.epsilon_greedy_exploration -------------------------------------------------------------------------------- .. automodule:: maro.rl.exploration.epsilon_greedy_exploration @@ -77,7 +85,7 @@ maro.rl.exploration.epsilon\_greedy\_exploration :undoc-members: :show-inheritance: -maro.rl.exploration.noise\_exploration +maro.rl.exploration.noise_exploration -------------------------------------------------------------------------------- .. automodule:: maro.rl.exploration.noise_exploration @@ -85,114 +93,118 @@ maro.rl.exploration.noise\_exploration :undoc-members: :show-inheritance: +maro.rl.exploration.exploration_scheduler +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.exploration.exploration_scheduler + :members: + :undoc-members: + :show-inheritance: + -Scheduler +Model ================================================================================ -maro.rl.scheduling.scheduler +maro.rl.model.abs_block +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.model.abs_block + :members: + :undoc-members: + :show-inheritance: + +maro.rl.model.core_model -------------------------------------------------------------------------------- -.. automodule:: maro.rl.scheduling.scheduler +.. automodule:: maro.rl.model.core_model :members: :undoc-members: :show-inheritance: -maro.rl.scheduling.simple\_parameter\_scheduler +maro.rl.model.fc_block -------------------------------------------------------------------------------- -.. automodule:: maro.rl.scheduling.simple_parameter_scheduler +.. automodule:: maro.rl.model.fc_block :members: :undoc-members: :show-inheritance: -Shaping +Policy ================================================================================ -maro.rl.shaping.abs\_shaper +maro.rl.policy.policy -------------------------------------------------------------------------------- -.. automodule:: maro.rl.shaping.abs_shaper +.. automodule:: maro.rl.policy.policy :members: :undoc-members: :show-inheritance: -Storage +Training ================================================================================ -maro.rl.experience.abs\_store +maro.rl.training.decision_generator -------------------------------------------------------------------------------- -.. automodule:: maro.rl.experience.abs_store +.. automodule:: maro.rl.training.decision_generator :members: :undoc-members: :show-inheritance: -maro.rl.experience.simple\_store +maro.rl.training.early_stopper -------------------------------------------------------------------------------- -.. automodule:: maro.rl.experience.experience_manager +.. automodule:: maro.rl.training.early_stopper :members: :undoc-members: :show-inheritance: - -Actor -================================================================================ - -maro.rl.actor.abs\_actor +maro.rl.training.learner -------------------------------------------------------------------------------- -.. automodule:: maro.rl.actor.abs_actor +.. automodule:: maro.rl.training.learner :members: :undoc-members: :show-inheritance: -maro.rl.actor.simple\_actor +maro.rl.training.local_learner -------------------------------------------------------------------------------- -.. automodule:: maro.rl.actor.simple_actor +.. automodule:: maro.rl.training.local_learner :members: :undoc-members: :show-inheritance: - -Learner -================================================================================ - -maro.rl.learner.abs\_learner +maro.rl.training.policy_server -------------------------------------------------------------------------------- -.. automodule:: maro.rl.learner.abs_learner +.. automodule:: maro.rl.training.policy_server :members: :undoc-members: :show-inheritance: -maro.rl.learner.simple\_learner +maro.rl.training.rollout_manager -------------------------------------------------------------------------------- -.. automodule:: maro.rl.learner.simple_learner +.. automodule:: maro.rl.training.rollout_manager :members: :undoc-members: :show-inheritance: - -Distributed Topologies -================================================================================ - -maro.rl.dist\_topologies.common +maro.rl.training.rollout_worker -------------------------------------------------------------------------------- -.. automodule:: maro.rl.dist_topologies.common +.. automodule:: maro.rl.training.rollout_worker :members: :undoc-members: :show-inheritance: -maro.rl.dist\_topologies.single\_learner\_multi\_actor\_sync\_mode +maro.rl.training.training_manager -------------------------------------------------------------------------------- -.. automodule:: maro.rl.dist_topologies.single_learner_multi_actor_sync_mode +.. automodule:: maro.rl.training.training_manager :members: :undoc-members: - :show-inheritance: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index ba37c9e03..a449faaf2 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -4,15 +4,15 @@ RL Toolkit MARO provides a full-stack abstraction for reinforcement learning (RL) which includes various customizable components. At the top level of a training workflow are: -* Learner, which consists of a roll-out manager and a policy manager, is the controller for a learning +* Learner, which consists of a roll-out manager and a training manager, is the controller for a learning process. The learner process executes training cycles that alternate between data collection and policy updates. * Rollout manager, which is responsible for collecting simulation data. The ``LocalRolloutManager`` performs roll-outs locally, while the ``ParallelRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in parallel. -* Policy manager, which manages a set of policies and controls their updates. The policy instances may reside in the - manager (``LocalPolicyManager``) or be distributed on a set of remote nodes (``ParallelPolicyManager``, to be implemented) - for parallelized training. -* Actor, which consists of an environment instance and a set of policies that agents use to interact with it, is a +* Training manager, which manages a set of policies and controls their updates. The policy instances may reside in the + manager (``LocalTrainingManager``) or be distributed on a set of remote nodes (``ParallelTrainingManager``) for parallelized + training. +* roll-out worker, which consists of an environment instance and a set of policies that agents use to interact with it, is a remote roll-out worker instance managed by a ``ParallelRolloutManager``. @@ -26,8 +26,8 @@ components. At the top level of a training workflow are: :alt: Overview -.. image:: ../images/rl/policy_manager.svg - :target: ../images/rl/policy_manager.svg +.. image:: ../images/rl/training_manager.svg + :target: ../images/rl/training_manager.svg :alt: RL Overview diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 07972912e..287654586 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -83,7 +83,7 @@ multi-process: rollout_mode: parallel # local, parallel policy_training_mode: parallel # local, parallel group: cim-dqn - num_actors: 3 + num_rollout_workers: 3 num_policy_servers: 2 redis_host: localhost redis_port: 6379 diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 684090db5..de50354bb 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -1,21 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from maro.rl.training import decision_generator import os import time import yaml from multiprocessing import Process from maro.rl import ( - Actor, DQN, DQNConfig, EpsilonGreedyExploration, ExperienceManager, FullyConnectedBlock, - MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalPolicyManager, LocalRolloutManager, - OptimOption, ParallelPolicyManager, ParallelRolloutManager, PolicyServer + EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalDecisionGenerator, + LocalRolloutManager, LocalTrainingManager, ParallelRolloutManager, ParallelTrainingManager, PolicyServer, + rollout_worker ) from maro.simulator import Env from maro.utils import set_seeds from examples.cim.env_wrapper import CIMEnvWrapper -from examples.cim.dqn.qnet import QNet +from examples.cim.dqn.policy import get_independent_policy FILE_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -26,42 +27,24 @@ log_dir = os.path.join(FILE_PATH, "logs", config["experiment_name"]) -# agent IDs AGENT_IDS = Env(**config["env"]["basic"]).agent_idx_list NUM_POLICY_SERVERS = config["multi-process"]["num_policy_servers"] POLICY2SERVER = {id_: f"SERVER.{id_ % NUM_POLICY_SERVERS}" for id_ in AGENT_IDS} +NUM_ACTIONS = config["env"]["wrapper"]["num_actions"] -# model input and output dimensions -IN_DIM = ( +# Obtain model input and output dimensions from env wrapper settings +config["policy"]["model"]["network"]["input_dim"] = ( (config["env"]["wrapper"]["look_back"] + 1) * (config["env"]["wrapper"]["max_ports_downstream"] + 1) * len(config["env"]["wrapper"]["port_attributes"]) + len(config["env"]["wrapper"]["vessel_attributes"]) ) -OUT_DIM = config["env"]["wrapper"]["num_actions"] - - -def get_independent_policy(policy_id, training: bool = True): - cfg = config["policy"] - qnet = QNet( - FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["network"]), - optim_option=OptimOption(**cfg["model"]["optimization"]) - ) - exp_cfg = cfg["experience_manager"]["training"] if training else cfg["experience_manager"]["rollout"] - return DQN( - name=policy_id, - q_net=qnet, - experience_manager=ExperienceManager(**exp_cfg), - config=DQNConfig(**cfg["algorithm_config"]), - update_trigger=cfg["update_trigger"], - warmup=cfg["warmup"] - ) +config["policy"]["model"]["network"]["output_dim"] = config["env"]["wrapper"]["num_actions"] def local_learner_mode(): env = Env(**config["env"]["basic"]) - num_actions = config["env"]["wrapper"]["num_actions"] - epsilon_greedy = EpsilonGreedyExploration(num_actions=num_actions) + epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, param_name="epsilon", @@ -70,7 +53,7 @@ def local_learner_mode(): ) local_learner = LocalLearner( env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - policies=[get_independent_policy(policy_id=i) for i in env.agent_idx_list], + policies=[get_independent_policy(config["policy"], i) for i in env.agent_idx_list], agent2policy={i: i for i in env.agent_idx_list}, num_episodes=config["num_episodes"], num_steps=config["num_steps"], @@ -81,26 +64,30 @@ def local_learner_mode(): local_learner.run() -def get_actor_process(): +def get_rollout_worker_process(): env = Env(**config["env"]["basic"]) - num_actions = config["env"]["wrapper"]["num_actions"] - policy_list = [get_independent_policy(policy_id=i, training=False) for i in env.agent_idx_list] - actor = Actor( - env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - policies=policy_list, + decision_generator = LocalDecisionGenerator( agent2policy={i: i for i in env.agent_idx_list}, + policies=[get_independent_policy(config["policy"], i, training=False) for i in env.agent_idx_list], + log_dir=log_dir + ) + rollout_worker( + env=CIMEnvWrapper(env, **config["env"]["wrapper"]), + decision_generator=decision_generator, group=config["multi-process"]["group"], - exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=num_actions)}, + exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, log_dir=log_dir, redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) ) - actor.run() def get_policy_server_process(server_id): server = PolicyServer( - policies=[get_independent_policy(id_) for id_ in AGENT_IDS if id_ % NUM_POLICY_SERVERS == server_id], + policies=[ + get_independent_policy(config["policy"], agent_id) + for agent_id in AGENT_IDS if agent_id % NUM_POLICY_SERVERS == server_id + ], group=config["multi-process"]["group"], name=f"SERVER.{server_id}", log_dir=log_dir @@ -109,7 +96,7 @@ def get_policy_server_process(server_id): def get_learner_process(): - epsilon_greedy = EpsilonGreedyExploration(num_actions=config["env"]["wrapper"]["num_actions"]) + epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, param_name="epsilon", @@ -119,18 +106,17 @@ def get_learner_process(): if config["multi-process"]["rollout_mode"] == "local": env = Env(**config["env"]["basic"]) - num_actions = config["env"]["wrapper"]["num_actions"] rollout_manager = LocalRolloutManager( env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - policies=[get_independent_policy(policy_id=i) for i in env.agent_idx_list], + policies=[get_independent_policy(config["policy"], i) for i in env.agent_idx_list], agent2policy={i: i for i in env.agent_idx_list}, - exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=num_actions)}, + exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, log_dir=log_dir ) else: rollout_manager = ParallelRolloutManager( - num_actors=config["multi-process"]["num_actors"], + num_rollout_workers=config["multi-process"]["num_rollout_workers"], group=config["multi-process"]["group"], exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, # max_receive_attempts=config["multi-process"]["max_receive_attempts"], @@ -139,17 +125,17 @@ def get_learner_process(): redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) ) - policy_list = [get_independent_policy(policy_id=i) for i in AGENT_IDS] + policy_list = [get_independent_policy(config["policy"], i) for i in AGENT_IDS] if config["multi-process"]["policy_training_mode"] == "local": - policy_manager = LocalPolicyManager(policies=policy_list, log_dir=log_dir) + training_manager = LocalTrainingManager(policies=policy_list, log_dir=log_dir) else: - policy_manager = ParallelPolicyManager( + training_manager = ParallelTrainingManager( policy2server=POLICY2SERVER, group=config["multi-process"]["group"], log_dir=log_dir ) learner = Learner( - policy_manager=policy_manager, + training_manager=training_manager, rollout_manager=rollout_manager, num_episodes=config["num_episodes"], # eval_schedule=config["eval_schedule"], @@ -161,11 +147,14 @@ def get_learner_process(): def multi_process_mode(): - actor_processes = [Process(target=get_actor_process) for _ in range(config["multi-process"]["num_actors"])] + rollout_worker_processes = [ + Process(target=get_rollout_worker_process) + for _ in range(config["multi-process"]["num_rollout_workers"]) + ] if config["multi-process"]["rollout_mode"] == "parallel": - for i, actor_process in enumerate(actor_processes): + for i, rollout_worker_process in enumerate(rollout_worker_processes): set_seeds(i) - actor_process.start() + rollout_worker_process.start() if config["multi-process"]["policy_training_mode"] == "parallel": server_processes = [ @@ -178,8 +167,8 @@ def multi_process_mode(): learner_process.start() if config["multi-process"]["rollout_mode"] == "parallel": - for actor_process in actor_processes: - actor_process.join() + for rollout_worker_process in rollout_worker_processes: + rollout_worker_process.join() if config["multi-process"]["policy_training_mode"] == "parallel": for server_process in server_processes: diff --git a/examples/cim/dqn/policy.py b/examples/cim/dqn/policy.py new file mode 100644 index 000000000..0385bc056 --- /dev/null +++ b/examples/cim/dqn/policy.py @@ -0,0 +1,31 @@ +import numpy as np +import torch +import torch.nn as nn +from maro.rl import DQN, DQNConfig, DiscreteQNet, ExperienceManager, FullyConnectedBlock, OptimOption + + +class QNet(DiscreteQNet): + def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): + super().__init__(component, optim_option=optim_option, device=device) + + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component(states) + + +def get_independent_policy(cfg, name, training: bool = True): + qnet = QNet( + FullyConnectedBlock(**cfg["model"]["network"]), + optim_option=OptimOption(**cfg["model"]["optimization"]) + ) + exp_cfg = cfg["experience_manager"]["training"] if training else cfg["experience_manager"]["rollout"] + return DQN( + name=name, + q_net=qnet, + experience_manager=ExperienceManager(**exp_cfg), + config=DQNConfig(**cfg["algorithm_config"]), + update_trigger=cfg["update_trigger"], + warmup=cfg["warmup"] + ) \ No newline at end of file diff --git a/examples/cim/dqn/qnet.py b/examples/cim/dqn/qnet.py deleted file mode 100644 index 5eecf1f85..000000000 --- a/examples/cim/dqn/qnet.py +++ /dev/null @@ -1,15 +0,0 @@ -import numpy as np -import torch -import torch.nn as nn -from maro.rl import DiscreteQNet, OptimOption - - -class QNet(DiscreteQNet): - def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): - super().__init__(component, optim_option=optim_option, device=device) - - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component(states) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 00a6c4038..8a31f438c 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.algorithm import ( +from maro.rl.algorithms import ( DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) @@ -18,8 +18,9 @@ ) from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( - AbsEarlyStopper, AbsPolicyManager, AbsRolloutManager, Actor, Learner, LocalLearner, LocalPolicyManager, - LocalRolloutManager, ParallelPolicyManager, ParallelRolloutManager, PolicyServer + AbsDecisionGenerator, AbsEarlyStopper, AbsRolloutManager, AbsTrainingManager, Learner, LocalDecisionGenerator, + LocalLearner, LocalRolloutManager, LocalTrainingManager, ParallelRolloutManager, ParallelTrainingManager, + PolicyServer, rollout_worker ) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, @@ -37,8 +38,9 @@ "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", - "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Actor", "Learner", "LocalLearner", - "LocalPolicyManager", "LocalRolloutManager", "ParallelPolicyManager", "ParallelRolloutManager", "PolicyServer", + "AbsDecisionGenerator", "AbsEarlyStopper", "AbsRolloutManager", "AbsTrainingManager", "Learner", + "LocalDecisionGenerator","LocalLearner", "LocalRolloutManager", "LocalTrainingManager", "ParallelRolloutManager", + "ParallelTrainingManager", "PolicyServer", "rollout_worker", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/algorithm/__init__.py b/maro/rl/algorithms/__init__.py similarity index 100% rename from maro/rl/algorithm/__init__.py rename to maro/rl/algorithms/__init__.py diff --git a/maro/rl/algorithm/ac.py b/maro/rl/algorithms/ac.py similarity index 100% rename from maro/rl/algorithm/ac.py rename to maro/rl/algorithms/ac.py diff --git a/maro/rl/algorithm/ddpg.py b/maro/rl/algorithms/ddpg.py similarity index 100% rename from maro/rl/algorithm/ddpg.py rename to maro/rl/algorithms/ddpg.py diff --git a/maro/rl/algorithm/dqn.py b/maro/rl/algorithms/dqn.py similarity index 100% rename from maro/rl/algorithm/dqn.py rename to maro/rl/algorithms/dqn.py diff --git a/maro/rl/algorithm/pg.py b/maro/rl/algorithms/pg.py similarity index 100% rename from maro/rl/algorithm/pg.py rename to maro/rl/algorithms/pg.py diff --git a/maro/rl/algorithm/rl_policy_index.py b/maro/rl/algorithms/rl_policy_index.py similarity index 100% rename from maro/rl/algorithm/rl_policy_index.py rename to maro/rl/algorithms/rl_policy_index.py diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index c7632be3b..5f1af6783 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -1,20 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .actor import Actor +from .decision_generator import AbsDecisionGenerator, LocalDecisionGenerator from .early_stopper import AbsEarlyStopper from .learner import Learner from .local_learner import LocalLearner -from .policy_manager import AbsPolicyManager, LocalPolicyManager, ParallelPolicyManager from .policy_server import PolicyServer from .rollout_manager import AbsRolloutManager, LocalRolloutManager, ParallelRolloutManager +from .rollout_worker import rollout_worker +from .training_manager import AbsTrainingManager, LocalTrainingManager, ParallelTrainingManager __all__ = [ - "Actor", + "AbsDecisionGenerator", "LocalDecisionGenerator", "AbsEarlyStopper", "Learner", "LocalLearner", - "AbsPolicyManager", "LocalPolicyManager", "ParallelPolicyManager", - "PolicyServer", + "PolicyServer", "PolicyServerGateway", "AbsRolloutManager", "LocalRolloutManager", "ParallelRolloutManager", + "rollout_worker", + "AbsTrainingManager", "LocalTrainingManager", "ParallelTrainingManager" ] diff --git a/maro/rl/training/actor.py b/maro/rl/training/actor.py deleted file mode 100644 index 72e14b1bd..000000000 --- a/maro/rl/training/actor.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from os import getcwd -from typing import Dict, List - -from maro.communication import Proxy -from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.exploration import AbsExploration -from maro.rl.policy import AbsPolicy -from maro.utils import Logger - -from .message_enums import MsgKey, MsgTag - - -class Actor(object): - """On-demand roll-out worker. - - Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences - for policy training / update. - policies (List[AbsPolicy]): A list of policies for inference. - agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's - queries to the correct policy. - group (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the learner (and decision clients, if any). - exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. - agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct - an agent's query to the correct exploration scheme. Defaults to None. - eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used - as the evaluation environment. Defaults to None. - log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory - will be used to save the log files generated by it. Defaults to the current working directory. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. - """ - def __init__( - self, - env: AbsEnvWrapper, - policies: List[AbsPolicy], - agent2policy: Dict[str, str], - group: str, - exploration_dict: Dict[str, AbsExploration] = None, - agent2exploration: Dict[str, str] = None, - eval_env: AbsEnvWrapper = None, - log_dir: str = getcwd(), - **proxy_kwargs - ): - self.env = env - self.eval_env = eval_env if eval_env else self.env - - # mappings between agents and policies - self.policy_dict = {policy.name: policy for policy in policies} - self.agent2policy = agent2policy - self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} - self.agent_groups_by_policy = defaultdict(list) - for agent_id, policy_id in agent2policy.items(): - self.agent_groups_by_policy[policy_id].append(agent_id) - - # mappings between exploration schemes and agents - self.exploration_dict = exploration_dict - if exploration_dict: - self.agent2exploration = agent2exploration - self.exploration = { - agent_id: self.exploration_dict[exploration_id] - for agent_id, exploration_id in self.agent2exploration.items() - } - self.exploration_enabled = True - self.agent_groups_by_exploration = defaultdict(list) - for agent_id, exploration_id in agent2exploration.items(): - self.agent_groups_by_exploration[exploration_id].append(agent_id) - - for exploration_id, agent_ids in self.agent_groups_by_exploration.items(): - self.agent_groups_by_exploration[exploration_id] = tuple(agent_ids) - - self._proxy = Proxy(group, "actor", {"rollout_manager": 1}, **proxy_kwargs) - self._logger = Logger(self._proxy.name, dump_folder=log_dir) - - def run(self): - """Start the event loop. - - The event loop handles 3 types of messages from the roll-out manager: - 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps - and the collected experiences will be sent back to the roll-out manager; - 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire - duration of the evaluation environment. - 3) EXIT, upon which the actor will break out of the event loop and the process will terminate. - - """ - for msg in self._proxy.receive(): - if msg.tag == MsgTag.EXIT: - self._logger.info("Exiting...") - self._proxy.close() - break - - if msg.tag == MsgTag.COLLECT: - self._collect(msg) - elif msg.tag == MsgTag.EVAL: - self._evaluate(msg) - - def _collect(self, msg): - episode_index, segment_index = msg.body[MsgKey.EPISODE_INDEX], msg.body[MsgKey.SEGMENT_INDEX] - # load policies - self._load_policy_states(msg.body[MsgKey.POLICY]) - # set exploration parameters - exploration_params = None - if MsgKey.EXPLORATION in msg.body: - updated_exploration_param_names = {} - for exploration_name, param_dict in msg.body[MsgKey.EXPLORATION].items(): - updated_exploration_param_names[exploration_name] = param_dict.keys() - for param_name, value in param_dict.items(): - setattr(self.exploration_dict[exploration_name], param_name, value) - - exploration_params = { - agent_ids: { - param_name: getattr(self.exploration_dict[exploration_name], param_name) - for param_name in updated_exploration_param_names[exploration_name] - } - for exploration_name, agent_ids in self.agent_groups_by_exploration.items() - } - - if self.env.state is None: - self._logger.info(f"Training episode {msg.body[MsgKey.EPISODE_INDEX]}") - if hasattr(self, "exploration_dict"): - self._logger.info(f"Exploration parameters: {exploration_params}") - - self.env.reset() - self.env.start() # get initial state - - starting_step_index = self.env.step_index + 1 - steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] - while self.env.state and steps_to_go > 0: - if self.exploration_dict: - action = { - id_: - self.exploration[id_](self.policy[id_].choose_action(st)) - if id_ in self.exploration else self.policy[id_].choose_action(st) - for id_, st in self.env.state.items() - } - else: - action = {id_: self.policy[id_].choose_action(st) for id_, st in self.env.state.items()} - - self.env.step(action) - steps_to_go -= 1 - - self._logger.info( - f"Roll-out finished for ep {episode_index}, segment {segment_index}" - f"(steps {starting_step_index} - {self.env.step_index})" - ) - - policies_with_new_exp = set() - exp_by_agent = self.env.get_experiences() - for agent_id, exp in exp_by_agent.items(): - self.policy[agent_id].experience_manager.put(exp) - policies_with_new_exp.add(self.agent2policy[agent_id]) - - ret_exp = {name: self.policy_dict[name].experience_manager.get() for name in policies_with_new_exp} - - return_info = { - MsgKey.EPISODE_END: not self.env.state, - MsgKey.EPISODE_INDEX: episode_index, - MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.EXPERIENCES: ret_exp, - MsgKey.ENV_SUMMARY: self.env.summary, - MsgKey.NUM_STEPS: self.env.step_index - starting_step_index + 1 - } - - self._proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) - - def _evaluate(self, msg): - self._logger.info("Evaluating...") - self.eval_env.reset() - self.eval_env.start() # get initial state - self._load_policy_states(msg.body[MsgKey.POLICY]) - while self.eval_env.state: - action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} - self.eval_env.step(action) - - return_info = { - MsgKey.ENV_SUMMARY: self.env.summary, - MsgKey.EPISODE_INDEX: msg.body[MsgKey.EPISODE_INDEX] - } - self._proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) - - def _load_policy_states(self, policy_state_dict): - for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].set_state(policy_state) - - if policy_state_dict: - self._logger.info(f"updated policies {list(policy_state_dict.keys())}") diff --git a/maro/rl/training/decision_generator.py b/maro/rl/training/decision_generator.py new file mode 100644 index 000000000..3fd3a1e80 --- /dev/null +++ b/maro/rl/training/decision_generator.py @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from collections import defaultdict +from os import getcwd +from typing import Dict, List + +from maro.rl.policy import AbsPolicy +from maro.utils import Logger + + +class AbsDecisionGenerator(ABC): + def __init__(self, agent2policy: Dict[str, str]): + super().__init__() + self.agent2policy = agent2policy + + @abstractmethod + def choose_action(self, state: dict, ep: int, step: int) -> dict: + """Generate an action based on the given state. + + Args: + state (dict): Dicitionary of agents' states based on which action decisions will be made. + ep (int): Current episode. + step (int): Current step. + """ + raise NotImplementedError + + +class LocalDecisionGenerator(AbsDecisionGenerator): + """Local decision generator. + + Args: + agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's + queries to the correct policy. + policies (List[AbsPolicy]): A list of policies for inference. + log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory + will be used to save the log files generated by it. Defaults to the current working directory. + """ + def __init__(self, agent2policy: Dict[str, str], policies: List[AbsPolicy], log_dir: str = getcwd()): + super().__init__(agent2policy) + self.policy_dict = {policy.name: policy for policy in policies} + self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self._logger = Logger("local_decision_generator", dump_folder=log_dir) + + def choose_action(self, state: dict, ep: int, step: int) -> dict: + """Generate an action based on the given state. + + Args: + state (dict): Dicitionary of agents' states based on which action decisions will be made. + ep (int): Current episode. + step (int): Current step. + """ + return {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} + + def store_experiences(self, exp_by_agent: dict) -> set: + """Store agent experiences in the policies' experience managers.""" + policies_with_new_exp = set() + for agent_id, exp in exp_by_agent.items(): + self.policy[agent_id].experience_manager.put(exp) + policies_with_new_exp.add(self.agent2policy[agent_id]) + + return policies_with_new_exp + + def get_experiences_by_policy(self, policy_names: List[str]): + """Get experiences by policy names.""" + return {name: self.policy_dict[name].experience_manager.get() for name in policy_names} + + def update(self, policy_state_dict: dict): + """Update policy states.""" + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].set_state(policy_state) + + if policy_state_dict: + self._logger.info(f"updated policies {list(policy_state_dict.keys())}") diff --git a/maro/rl/training/learner.py b/maro/rl/training/learner.py index 089ccc389..7eaff0abb 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/learner.py @@ -8,8 +8,8 @@ from maro.utils import Logger from .early_stopper import AbsEarlyStopper -from .policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager +from .training_manager import AbsTrainingManager class Learner: @@ -20,7 +20,7 @@ class Learner: as duplicate experience storage. Use ``LocalLearner`` instead. Args: - policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. + training_manager (AbsTrainingManager): An ``AbsTrainingManager`` instance that controls policy updates. rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data collection. num_episodes (int): Number of training episodes. Each training episode may contain one or more @@ -39,7 +39,7 @@ class Learner: """ def __init__( self, - policy_manager: AbsPolicyManager, + training_manager: AbsTrainingManager, rollout_manager: AbsRolloutManager, num_episodes: int, eval_schedule: Union[int, List[int]] = None, @@ -48,7 +48,7 @@ def __init__( **end_of_episode_kwargs ): self.logger = Logger("LEARNER", dump_folder=log_dir) - self.policy_manager = policy_manager + self.training_manager = training_manager self.rollout_manager = rollout_manager self.num_episodes = num_episodes @@ -71,7 +71,7 @@ def __init__( self.early_stopper = early_stopper self._end_of_episode_kwargs = end_of_episode_kwargs - self._updated_policy_ids = self.policy_manager.names + self._updated_policy_ids = self.training_manager.names self._last_step_set = {} def run(self): @@ -80,7 +80,7 @@ def run(self): self._train(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 - env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) + env_metric_dict = self.rollout_manager.evaluate(ep, self.training_manager.get_state()) # performance details self.logger.info(f"Evaluation result: {env_metric_dict}") # early stopping check @@ -93,20 +93,20 @@ def run(self): if hasattr(self.rollout_manager, "exit"): self.rollout_manager.exit() - if hasattr(self.policy_manager, "exit"): - self.policy_manager.exit() + if hasattr(self.training_manager, "exit"): + self.training_manager.exit() def _train(self, ep: int): total_policy_update_time = 0 num_experiences_collected = segment = 0 self.rollout_manager.reset() - policy_state_dict = self.policy_manager.get_state() + policy_state_dict = self.training_manager.get_state() while not self.rollout_manager.episode_complete: segment += 1 # experience collection exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict) t0 = time.time() - policy_state_dict = self.policy_manager.on_experiences(exp_by_policy) + policy_state_dict = self.training_manager.on_experiences(exp_by_policy) total_policy_update_time += time.time() - t0 num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index e0a600ed5..002a6b168 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -21,8 +21,8 @@ class MsgTag(Enum): class MsgKey(Enum): ACTION = "action" AGENT_ID = "agent_id" - EPISODE_INDEX = "episode_index" - SEGMENT_INDEX = "segment_index" + EPISODE = "episode_index" + SEGMENT = "segment_index" TIME_STEP = "time_step" ENV_SUMMARY = "env_summary" EXPERIENCES = "experiences" diff --git a/maro/rl/training/policy_server.py b/maro/rl/training/policy_server.py index a3c2bded9..08272e715 100644 --- a/maro/rl/training/policy_server.py +++ b/maro/rl/training/policy_server.py @@ -22,7 +22,7 @@ def __init__( **proxy_kwargs ): self.policy_dict = {policy.name: policy for policy in policies} - self._proxy = Proxy(group, "policy_server", {"policy_manager": 1}, component_name=name, **proxy_kwargs) + self._proxy = Proxy(group, "policy_server", {"training_manager": 1}, component_name=name, **proxy_kwargs) self._logger = Logger(self._proxy.name, dump_folder=log_dir) def run(self): diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 4b2081e44..80b2463f5 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -56,7 +56,7 @@ def reset(self): class LocalRolloutManager(AbsRolloutManager): - """Controller for a single local roll-out actor. + """Local roll-out controller. Args: env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences @@ -225,25 +225,25 @@ def _load_policy_states(self, policy_state_dict: dict): class ParallelRolloutManager(AbsRolloutManager): - """Controller for a set of remote roll-out actors. + """Controller for a set of remote roll-out workers. Args: - num_actors (int): Number of remote roll-out actors. - group (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the learner (and decision clients, if any). + num_rollout_workers (int): Number of remote roll-out workers. + group (str): Group name for the roll-out manager and the set of roll-out workers managed by it. The roll-out + workers' processes must be assigned this group name in order to form a communicating cluster. exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. The exploration parameters - from these instances will be broadcast to all actors. Defaults to None. + from these instances will be broadcast to all workers. Defaults to None. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - max_receive_attempts (int): Maximum number of attempts to receive actor results in ``collect``. Defaults to - None, in which case the number is set to ``num_actors``. - receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the actors. This + max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to + None, in which case the number is set to ``num_rollout_workers``. + receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the workers. This This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the - desired amount of data from actors. Defaults to None, in which case each receive attempt is blocking. + desired amount of data from workers. Defaults to None, in which case each receive attempt is blocking. max_staleness (int): Maximum allowable staleness measured in the number of calls to ``collect``. Experiences collected from calls to ``collect`` within ``max_staleness`` calls ago will be returned to the learner. Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. - num_eval_actors (int): Number of actors required for evaluation. Defaults to 1. + num_eval_workers (int): Number of workers for evaluation. Defaults to 1. log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of each episode. Defaults to True. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at @@ -254,33 +254,33 @@ class ParallelRolloutManager(AbsRolloutManager): """ def __init__( self, - num_actors: int, + num_rollout_workers: int, group: str, exploration_dict: Dict[str, AbsExploration] = None, num_steps: int = -1, max_receive_attempts: int = None, receive_timeout: int = None, max_staleness: int = 0, - num_eval_actors: int = 1, + num_eval_workers: int = 1, log_env_summary: bool = True, log_dir: str = getcwd(), **proxy_kwargs ): super().__init__() - if num_eval_actors > num_actors: - raise ValueError("num_eval_actors cannot exceed the number of available actors") + if num_eval_workers > num_rollout_workers: + raise ValueError("num_eval_workers cannot exceed the number of available workers") self._logger = Logger("PARALLEL_ROLLOUT_MANAGER", dump_folder=log_dir) - self.num_actors = num_actors - peers = {"actor": num_actors} + self.num_rollout_workers = num_rollout_workers + peers = {"rollout_worker": num_rollout_workers} self._proxy = Proxy(group, "rollout_manager", peers, **proxy_kwargs) - self._actors = self._proxy.peers["actor"] # remote actor ID's + self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's self.exploration_dict = exploration_dict self._num_steps = num_steps if max_receive_attempts is None: - max_receive_attempts = self.num_actors + max_receive_attempts = self.num_rollout_workers self._logger.info(f"Maximum receive attempts is set to {max_receive_attempts}") self.max_receive_attempts = max_receive_attempts @@ -291,7 +291,7 @@ def __init__( self.total_env_steps = 0 self._log_env_summary = log_env_summary - self._num_eval_actors = num_eval_actors + self._num_eval_workers = num_eval_workers self._exploration_update = True @@ -301,8 +301,8 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") msg_body = { - MsgKey.EPISODE_INDEX: episode_index, - MsgKey.SEGMENT_INDEX: segment_index, + MsgKey.EPISODE: episode_index, + MsgKey.SEGMENT: segment_index, MsgKey.NUM_STEPS: self._num_steps, MsgKey.POLICY: policy_state_dict } @@ -313,22 +313,22 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic } self._exploration_update = False - self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent collect requests to {self._actors} for ep-{episode_index}, segment-{segment_index}") + self._proxy.ibroadcast("rollout_worker", MsgTag.COLLECT, SessionType.TASK, body=msg_body) + self._logger.info(f"Sent collect requests to {self._workers} for ep-{episode_index}, segment-{segment_index}") - # Receive roll-out results from remote actors + # Receive roll-out results from remote workers combined_exp_by_policy = defaultdict(ExperienceSet) num_finishes = 0 for _ in range(self.max_receive_attempts): msg = self._proxy.receive_once(timeout=self.receive_timeout) - if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: + if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE] != episode_index: self._logger.info( - f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE_INDEX]} " + f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE]} " f"(expected message type {MsgTag.COLLECT} and episode index {episode_index})" ) continue - if segment_index - msg.body[MsgKey.SEGMENT_INDEX] <= self._max_staleness: + if segment_index - msg.body[MsgKey.SEGMENT] <= self._max_staleness: exp_by_policy = msg.body[MsgKey.EXPERIENCES] self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) self.total_env_steps += msg.body[MsgKey.NUM_STEPS] @@ -336,14 +336,14 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic for policy_name, exp in exp_by_policy.items(): combined_exp_by_policy[policy_name].extend(exp) - if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: + if msg.body[MsgKey.SEGMENT] == segment_index: self.episode_complete = msg.body[MsgKey.EPISODE_END] if self.episode_complete: # log roll-out summary if self._log_env_summary: self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") num_finishes += 1 - if num_finishes == self.num_actors: + if num_finishes == self.num_rollout_workers: break if self.episode_complete: @@ -364,34 +364,34 @@ def evaluate(self, ep: int, policy_state_dict: dict): Returns: Environment summary. """ - msg_body = {MsgKey.EPISODE_INDEX: ep, MsgKey.POLICY: policy_state_dict} + msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY: policy_state_dict} - actors = choices(self._actors, k=self._num_eval_actors) + workers = choices(self._workers, k=self._num_eval_workers) env_summary_dict = {} - self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(actor_id, msg_body) for actor_id in actors]) - self._logger.info(f"Sent evaluation requests to {actors}") + self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(worker_id, msg_body) for worker_id in workers]) + self._logger.info(f"Sent evaluation requests to {workers}") - # Receive roll-out results from remote actors + # Receive roll-out results from remote workers num_finishes = 0 for msg in self._proxy.receive(): - if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE_INDEX] != ep: + if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE] != ep: self._logger.info( - f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE_INDEX]} " + f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE]} " f"(expected message type {MsgTag.EVAL_DONE} and episode index {ep})" ) continue env_summary_dict[msg.source] = msg.body[MsgKey.ENV_SUMMARY] - if msg.body[MsgKey.EPISODE_INDEX] == ep: + if msg.body[MsgKey.EPISODE] == ep: num_finishes += 1 - if num_finishes == self._num_eval_actors: + if num_finishes == self._num_eval_workers: break return env_summary_dict def exit(self): - """Tell the remote actors to exit.""" - self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) + """Tell the remote workers to exit.""" + self._proxy.ibroadcast("rollout_worker", MsgTag.EXIT, SessionType.NOTIFICATION) self._proxy.close() self._logger.info("Exiting...") diff --git a/maro/rl/training/rollout_worker.py b/maro/rl/training/rollout_worker.py new file mode 100644 index 000000000..6fda4e9ea --- /dev/null +++ b/maro/rl/training/rollout_worker.py @@ -0,0 +1,152 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict +from os import getcwd +from typing import Dict + +from maro.communication import Proxy +from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.exploration import AbsExploration +from maro.utils import Logger + +from .decision_generator import AbsDecisionGenerator +from .message_enums import MsgKey, MsgTag + + +def rollout_worker( + env: AbsEnvWrapper, + decision_generator: AbsDecisionGenerator, + group: str, + exploration_dict: Dict[str, AbsExploration] = None, + agent2exploration: Dict[str, str] = None, + eval_env: AbsEnvWrapper = None, + log_dir: str = getcwd(), + **proxy_kwargs +): + """Roll-out worker process. + + Args: + env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences + for policy training / update. + decision_generator (AbsDecisionGenerator): Source of action decisions which could be local or remote + depending on the implementation. + group (str): Group name for all roll-out workers and the roll-out manager that manages them. The roll-out + manager process must be assigned this group name in order to form a communicating cluster. + exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. + agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct + an agent's query to the correct exploration scheme. Defaults to None. + eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used + as the evaluation environment. Defaults to None. + log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory + will be used to save the log files generated by it. Defaults to the current working directory. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. + """ + eval_env = eval_env if eval_env else env + # mappings between exploration schemes and agents + if exploration_dict: + exploration_by_agent = { + agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() + } + agent_groups_by_exploration = defaultdict(list) + for agent_id, exploration_id in agent2exploration.items(): + agent_groups_by_exploration[exploration_id].append(agent_id) + + for exploration_id, agent_ids in agent_groups_by_exploration.items(): + agent_groups_by_exploration[exploration_id] = tuple(agent_ids) + + proxy = Proxy(group, "rollout_worker", {"rollout_manager": 1}, **proxy_kwargs) + logger = Logger(proxy.name, dump_folder=log_dir) + + def collect(msg): + ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] + # load policies + if hasattr(decision_generator, "update"): + decision_generator.update(msg.body[MsgKey.POLICY]) + # set exploration parameters + exploration_params = None + if MsgKey.EXPLORATION in msg.body: + updated_exploration_param_names = {} + for exploration_name, param_dict in msg.body[MsgKey.EXPLORATION].items(): + updated_exploration_param_names[exploration_name] = param_dict.keys() + for param_name, value in param_dict.items(): + setattr(exploration_dict[exploration_name], param_name, value) + + exploration_params = { + agent_ids: { + param_name: getattr(exploration_dict[exploration_name], param_name) + for param_name in updated_exploration_param_names[exploration_name] + } + for exploration_name, agent_ids in agent_groups_by_exploration.items() + } + logger.info(f"Exploration parameters: {exploration_params}") + + if env.state is None: + logger.info(f"Training episode {msg.body[MsgKey.EPISODE]}") + env.reset() + env.start() # get initial state + + starting_step_index = env.step_index + 1 + steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] + while env.state and steps_to_go > 0: + action = decision_generator.choose_action(env.state, ep, env.step_index) + if exploration_dict: + for agent_id in action: + action[agent_id] = exploration_by_agent[agent_id](action[agent_id]) + env.step(action) + steps_to_go -= 1 + + logger.info( + f"Roll-out finished for ep {ep}, segment {segment}" + f"(steps {starting_step_index} - {env.step_index})" + ) + + if hasattr(decision_generator, "store_experiences"): + policy_names = decision_generator.store_experiences(env.get_experiences()) + ret_exp = decision_generator.get_experiences_by_policy(policy_names) + + return_info = { + MsgKey.EPISODE_END: not env.state, + MsgKey.EPISODE: ep, + MsgKey.SEGMENT: segment, + MsgKey.EXPERIENCES: ret_exp, + MsgKey.ENV_SUMMARY: env.summary, + MsgKey.NUM_STEPS: env.step_index - starting_step_index + 1 + } + + proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) + + def evaluate(msg): + logger.info(f"Evaluating...") + ep = msg.body[MsgKey.EPISODE] + eval_env.reset() + eval_env.start() # get initial state + if hasattr(decision_generator, "update"): + decision_generator.update(msg.body[MsgKey.POLICY]) + while eval_env.state: + action = decision_generator.choose_action(env.state, ep, eval_env.step_index) + eval_env.step(action) + + return_info = {MsgKey.ENV_SUMMARY: env.summary, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} + proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) + + """ + The event loop handles 3 types of messages from the roll-out manager: + 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps + and the collected experiences will be sent back to the roll-out manager; + 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire + duration of the evaluation environment. + 3) EXIT, upon which it will break out of the event loop and the process will terminate. + + """ + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.COLLECT: + collect(msg) + elif msg.tag == MsgTag.EVAL: + evaluate(msg) \ No newline at end of file diff --git a/maro/rl/training/policy_manager.py b/maro/rl/training/training_manager.py similarity index 94% rename from maro/rl/training/policy_manager.py rename to maro/rl/training/training_manager.py index 28fd7054f..c8e781ffe 100644 --- a/maro/rl/training/policy_manager.py +++ b/maro/rl/training/training_manager.py @@ -15,7 +15,7 @@ from .message_enums import MsgKey, MsgTag -class AbsPolicyManager(ABC): +class AbsTrainingManager(ABC): """Controller for policy updates. The actual policy instances may reside here or be distributed on a set of remote nodes. @@ -40,7 +40,7 @@ def get_state(self): raise NotImplementedError -class LocalPolicyManager(AbsPolicyManager): +class LocalTrainingManager(AbsTrainingManager): """Policy manager that contains the actual policy instances. Args: @@ -83,7 +83,7 @@ def get_state(self): return {name: policy.get_state() for name, policy in self.policy_dict.items()} -class ParallelPolicyManager(AbsPolicyManager): +class ParallelTrainingManager(AbsTrainingManager): def __init__( self, policy2server: Dict[str, str], @@ -92,11 +92,11 @@ def __init__( **proxy_kwargs ): super().__init__() - self._logger = Logger("PARALLEL_POLICY_MANAGER", dump_folder=log_dir) + self._logger = Logger("PARALLEL_TRAINING_MANAGER", dump_folder=log_dir) self.policy2server = policy2server self._names = list(self.policy2server.keys()) peers = {"policy_server": len(set(self.policy2server.values()))} - self._proxy = Proxy(group, "policy_manager", peers, **proxy_kwargs) + self._proxy = Proxy(group, "training_manager", peers, **proxy_kwargs) @property def names(self): From 3b5faeb367c8d77782e0b68cafec73ab9b4a84c1 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 11 Jun 2021 12:07:49 +0000 Subject: [PATCH 301/482] updated figures --- docs/source/images/rl/learner.svg | 2 +- docs/source/images/rl/policy_manager.svg | 3 --- docs/source/images/rl/training_manager.svg | 3 +++ maro/rl/training/message_enums.py | 5 ++--- 4 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 docs/source/images/rl/policy_manager.svg create mode 100644 docs/source/images/rl/training_manager.svg diff --git a/docs/source/images/rl/learner.svg b/docs/source/images/rl/learner.svg index 3c4ee705f..b3b647ae2 100644 --- a/docs/source/images/rl/learner.svg +++ b/docs/source/images/rl/learner.svg @@ -1,3 +1,3 @@ -
Learner
Learner
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
experiences
expe...
Policy
Manager
Policy...
updated policies
updated pol...
Learner
Lear...
Viewer does not support full SVG 1.1
\ No newline at end of file +
Learner
Learner
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
experiences
expe...
Training
Manager
Training...
updated policies
updated pol...
Learner
Lear...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/policy_manager.svg b/docs/source/images/rl/policy_manager.svg deleted file mode 100644 index 2a725fa7a..000000000 --- a/docs/source/images/rl/policy_manager.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Local Policy Manager
Local Policy Manager
policies
policies
on_experiences
on_experiences
Parallel Policy Manager
Parallel Policy Manager
on_experiences
on_experiences
policy proxy
policy proxy
remote training config
remote training...
Policy Trainer 1
Policy Trainer 1
get_state
get_state
get_state
get_state
subset of policies
subset of pol...
training message
traini...
policy state
policy state
Local Policy Manager
Local Policy Manager
Parallel Policy Manager
Parallel Policy Manager
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/training_manager.svg b/docs/source/images/rl/training_manager.svg new file mode 100644 index 000000000..0491ebce4 --- /dev/null +++ b/docs/source/images/rl/training_manager.svg @@ -0,0 +1,3 @@ + + +
Local Training Manager
Local Training Manager
policies
policies
on_experiences
on_experiences
Parallel Training Manager
Parallel Training Manager
on_experiences
on_experiences
policy proxy
policy proxy
remote training config
remote training...
Policy Server 1
Policy Server 1
get_state
get_state
get_state
get_state
subset of policies
subset of pol...
training message
traini...
policy state
policy state
Local Training Manager
Local Training Manager
Parallel Training Manager
Parallel Training Manager
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index 002a6b168..9ab4d7820 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -21,9 +21,8 @@ class MsgTag(Enum): class MsgKey(Enum): ACTION = "action" AGENT_ID = "agent_id" - EPISODE = "episode_index" - SEGMENT = "segment_index" - TIME_STEP = "time_step" + EPISODE = "episode" + SEGMENT = "segment" ENV_SUMMARY = "env_summary" EXPERIENCES = "experiences" NUM_EXPERIENCES = "num_experiences" From 79111627a2eb80e87a9e4725a9e88548c483800d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 11 Jun 2021 12:12:54 +0000 Subject: [PATCH 302/482] removed unwanted import --- examples/cim/dqn/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index de50354bb..c632ba6ee 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.training import decision_generator import os import time import yaml From 4f2182f9d0521607613b19a185d71f28e67f4299 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 15 Jun 2021 03:51:36 +0000 Subject: [PATCH 303/482] refactored CIM-DQN example --- examples/cim/dqn/config.yml | 4 +- examples/cim/dqn/general.py | 28 ++++ examples/cim/dqn/main.py | 197 +++++---------------------- examples/cim/dqn/policy.py | 5 +- examples/cim/dqn/rollout_manager.py | 76 +++++++++++ examples/cim/dqn/training_manager.py | 46 +++++++ 6 files changed, 189 insertions(+), 167 deletions(-) create mode 100644 examples/cim/dqn/general.py create mode 100644 examples/cim/dqn/rollout_manager.py create mode 100644 examples/cim/dqn/training_manager.py diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 287654586..0d099697d 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -3,7 +3,7 @@ experiment_name: cim_dqn mode: multi-process # local, multi-process -num_episodes: 100 +num_episodes: 3 # eval_schedule: 2 num_steps: -1 env: @@ -81,7 +81,7 @@ policy: warmup: 1 multi-process: rollout_mode: parallel # local, parallel - policy_training_mode: parallel # local, parallel + policy_training_mode: local # local, parallel group: cim-dqn num_rollout_workers: 3 num_policy_servers: 2 diff --git a/examples/cim/dqn/general.py b/examples/cim/dqn/general.py new file mode 100644 index 000000000..653d471d5 --- /dev/null +++ b/examples/cim/dqn/general.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import yaml + +from maro.simulator import Env + +FILE_PATH = os.path.dirname(os.path.realpath(__file__)) + +DEFAULT_CONFIG_PATH = os.path.join(FILE_PATH, "config.yml") +with open(os.getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: + config = yaml.safe_load(config_file) + +log_dir = os.path.join(FILE_PATH, "logs", config["experiment_name"]) + +# Obtain model input and output dimensions from env wrapper settings +config["policy"]["model"]["network"]["input_dim"] = ( + (config["env"]["wrapper"]["look_back"] + 1) + * (config["env"]["wrapper"]["max_ports_downstream"] + 1) + * len(config["env"]["wrapper"]["port_attributes"]) + + len(config["env"]["wrapper"]["vessel_attributes"]) +) +config["policy"]["model"]["network"]["output_dim"] = config["env"]["wrapper"]["num_actions"] + +NUM_ACTIONS = config["env"]["wrapper"]["num_actions"] +AGENT_IDS = Env(**config["env"]["basic"]).agent_idx_list +NUM_POLICY_SERVERS = config["multi-process"]["num_policy_servers"] diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index c632ba6ee..f314f3311 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -2,184 +2,53 @@ # Licensed under the MIT license. import os +import sys import time -import yaml -from multiprocessing import Process -from maro.rl import ( - EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner, LocalDecisionGenerator, - LocalRolloutManager, LocalTrainingManager, ParallelRolloutManager, ParallelTrainingManager, PolicyServer, - rollout_worker -) +from maro.rl import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner from maro.simulator import Env -from maro.utils import set_seeds -from examples.cim.env_wrapper import CIMEnvWrapper -from examples.cim.dqn.policy import get_independent_policy +dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory +cim_path = os.path.dirname(dqn_path) # CIM example directory +sys.path.insert(0, cim_path) +sys.path.insert(0, dqn_path) +from env_wrapper import CIMEnvWrapper +from general import NUM_ACTIONS, config, log_dir +from policy import get_independent_policy +from rollout_manager import rollout_manager +from training_manager import training_manager -FILE_PATH = os.path.dirname(os.path.realpath(__file__)) - -DEFAULT_CONFIG_PATH = os.path.join(FILE_PATH, "config.yml") -with open(os.getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: - config = yaml.safe_load(config_file) - -log_dir = os.path.join(FILE_PATH, "logs", config["experiment_name"]) - -AGENT_IDS = Env(**config["env"]["basic"]).agent_idx_list -NUM_POLICY_SERVERS = config["multi-process"]["num_policy_servers"] -POLICY2SERVER = {id_: f"SERVER.{id_ % NUM_POLICY_SERVERS}" for id_ in AGENT_IDS} -NUM_ACTIONS = config["env"]["wrapper"]["num_actions"] - -# Obtain model input and output dimensions from env wrapper settings -config["policy"]["model"]["network"]["input_dim"] = ( - (config["env"]["wrapper"]["look_back"] + 1) - * (config["env"]["wrapper"]["max_ports_downstream"] + 1) - * len(config["env"]["wrapper"]["port_attributes"]) - + len(config["env"]["wrapper"]["vessel_attributes"]) -) -config["policy"]["model"]["network"]["output_dim"] = config["env"]["wrapper"]["num_actions"] - - -def local_learner_mode(): - env = Env(**config["env"]["basic"]) - epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) - epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - last_ep=config["num_episodes"], - **config["exploration"] - ) - local_learner = LocalLearner( - env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - policies=[get_independent_policy(config["policy"], i) for i in env.agent_idx_list], - agent2policy={i: i for i in env.agent_idx_list}, - num_episodes=config["num_episodes"], - num_steps=config["num_steps"], - exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, - agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, - log_dir=log_dir - ) - local_learner.run() - - -def get_rollout_worker_process(): - env = Env(**config["env"]["basic"]) - decision_generator = LocalDecisionGenerator( - agent2policy={i: i for i in env.agent_idx_list}, - policies=[get_independent_policy(config["policy"], i, training=False) for i in env.agent_idx_list], - log_dir=log_dir - ) - rollout_worker( - env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - decision_generator=decision_generator, - group=config["multi-process"]["group"], - exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, - agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, - log_dir=log_dir, - redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) - ) - - -def get_policy_server_process(server_id): - server = PolicyServer( - policies=[ - get_independent_policy(config["policy"], agent_id) - for agent_id in AGENT_IDS if agent_id % NUM_POLICY_SERVERS == server_id - ], - group=config["multi-process"]["group"], - name=f"SERVER.{server_id}", - log_dir=log_dir - ) - server.run() - - -def get_learner_process(): - epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) - epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - last_ep=config["num_episodes"], - **config["exploration"] - ) - - if config["multi-process"]["rollout_mode"] == "local": +if __name__ == "__main__": + if config["mode"] == "local": env = Env(**config["env"]["basic"]) - rollout_manager = LocalRolloutManager( + epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) + epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + last_ep=config["num_episodes"], + **config["exploration"] + ) + local_learner = LocalLearner( env=CIMEnvWrapper(env, **config["env"]["wrapper"]), policies=[get_independent_policy(config["policy"], i) for i in env.agent_idx_list], agent2policy={i: i for i in env.agent_idx_list}, - exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, + num_episodes=config["num_episodes"], + num_steps=config["num_steps"], + exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, log_dir=log_dir ) - else: - rollout_manager = ParallelRolloutManager( - num_rollout_workers=config["multi-process"]["num_rollout_workers"], - group=config["multi-process"]["group"], - exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, - # max_receive_attempts=config["multi-process"]["max_receive_attempts"], - # receive_timeout=config["multi-process"]["receive_timeout"], - log_dir=log_dir, - redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) - ) - - policy_list = [get_independent_policy(config["policy"], i) for i in AGENT_IDS] - if config["multi-process"]["policy_training_mode"] == "local": - training_manager = LocalTrainingManager(policies=policy_list, log_dir=log_dir) - else: - training_manager = ParallelTrainingManager( - policy2server=POLICY2SERVER, - group=config["multi-process"]["group"], + local_learner.run() + elif config["mode"] == "multi-process": + learner = Learner( + training_manager=training_manager, + rollout_manager=rollout_manager, + num_episodes=config["num_episodes"], + # eval_schedule=config["eval_schedule"], log_dir=log_dir ) - learner = Learner( - training_manager=training_manager, - rollout_manager=rollout_manager, - num_episodes=config["num_episodes"], - # eval_schedule=config["eval_schedule"], - log_dir=log_dir - ) - - time.sleep(5) - learner.run() - - -def multi_process_mode(): - rollout_worker_processes = [ - Process(target=get_rollout_worker_process) - for _ in range(config["multi-process"]["num_rollout_workers"]) - ] - if config["multi-process"]["rollout_mode"] == "parallel": - for i, rollout_worker_process in enumerate(rollout_worker_processes): - set_seeds(i) - rollout_worker_process.start() - - if config["multi-process"]["policy_training_mode"] == "parallel": - server_processes = [ - Process(target=get_policy_server_process, args=(server_id,)) for server_id in range(NUM_POLICY_SERVERS) - ] - for server_process in server_processes: - server_process.start() - - learner_process = Process(target=get_learner_process) - learner_process.start() - - if config["multi-process"]["rollout_mode"] == "parallel": - for rollout_worker_process in rollout_worker_processes: - rollout_worker_process.join() - - if config["multi-process"]["policy_training_mode"] == "parallel": - for server_process in server_processes: - server_process.join() - - learner_process.join() - - -if __name__ == "__main__": - if config["mode"] == "local": - local_learner_mode() - elif config["mode"] == "multi-process": - multi_process_mode() + time.sleep(5) + learner.run() else: print("Two modes are supported: local or multi-process.") diff --git a/examples/cim/dqn/policy.py b/examples/cim/dqn/policy.py index 0385bc056..61cef8ba0 100644 --- a/examples/cim/dqn/policy.py +++ b/examples/cim/dqn/policy.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + import numpy as np import torch import torch.nn as nn @@ -28,4 +31,4 @@ def get_independent_policy(cfg, name, training: bool = True): config=DQNConfig(**cfg["algorithm_config"]), update_trigger=cfg["update_trigger"], warmup=cfg["warmup"] - ) \ No newline at end of file + ) diff --git a/examples/cim/dqn/rollout_manager.py b/examples/cim/dqn/rollout_manager.py new file mode 100644 index 000000000..802cfd0da --- /dev/null +++ b/examples/cim/dqn/rollout_manager.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys +from multiprocessing import Process + +from maro.rl import ( + EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, LocalDecisionGenerator, + LocalRolloutManager, ParallelRolloutManager, rollout_worker +) +from maro.simulator import Env +from maro.utils import set_seeds + +dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory +cim_path = os.path.dirname(dqn_path) # CIM example directory +sys.path.insert(0, cim_path) +sys.path.insert(0, dqn_path) +from env_wrapper import CIMEnvWrapper +from general import NUM_ACTIONS, config, log_dir +from policy import get_independent_policy + + +def get_rollout_worker_process(): + env = Env(**config["env"]["basic"]) + decision_generator = LocalDecisionGenerator( + agent2policy={i: i for i in env.agent_idx_list}, + policies=[get_independent_policy(config["policy"], i, training=False) for i in env.agent_idx_list], + log_dir=log_dir + ) + rollout_worker( + env=CIMEnvWrapper(env, **config["env"]["wrapper"]), + decision_generator=decision_generator, + group=config["multi-process"]["group"], + exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, + agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, + log_dir=log_dir, + redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) + ) + + +epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) +epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + last_ep=config["num_episodes"], + **config["exploration"] +) +if config["multi-process"]["rollout_mode"] == "local": + env = Env(**config["env"]["basic"]) + rollout_manager = LocalRolloutManager( + env=CIMEnvWrapper(env, **config["env"]["wrapper"]), + policies=[get_independent_policy(config["policy"], i) for i in env.agent_idx_list], + agent2policy={i: i for i in env.agent_idx_list}, + exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, + agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, + log_dir=log_dir + ) +else: + rollout_worker_processes = [ + Process(target=get_rollout_worker_process) for _ in range(config["multi-process"]["num_rollout_workers"]) + ] + + for i, rollout_worker_process in enumerate(rollout_worker_processes): + set_seeds(i) + rollout_worker_process.start() + + rollout_manager = ParallelRolloutManager( + num_rollout_workers=config["multi-process"]["num_rollout_workers"], + group=config["multi-process"]["group"], + exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, + # max_receive_attempts=config["multi-process"]["max_receive_attempts"], + # receive_timeout=config["multi-process"]["receive_timeout"], + log_dir=log_dir, + redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) + ) diff --git a/examples/cim/dqn/training_manager.py b/examples/cim/dqn/training_manager.py new file mode 100644 index 000000000..895f61d76 --- /dev/null +++ b/examples/cim/dqn/training_manager.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys +from multiprocessing import Process + +from maro.rl import LocalTrainingManager, ParallelTrainingManager, PolicyServer + +dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory +cim_path = os.path.dirname(dqn_path) # CIM example directory +sys.path.insert(0, cim_path) +sys.path.insert(0, dqn_path) +from general import AGENT_IDS, NUM_POLICY_SERVERS, config, log_dir +from policy import get_independent_policy + + +def get_policy_server_process(server_id): + server = PolicyServer( + policies=[ + get_independent_policy(config["policy"], agent_id) + for agent_id in AGENT_IDS if agent_id % NUM_POLICY_SERVERS == server_id + ], + group=config["multi-process"]["group"], + name=f"SERVER.{server_id}", + log_dir=log_dir + ) + server.run() + + +policy_list = [get_independent_policy(config["policy"], i) for i in AGENT_IDS] +if config["multi-process"]["policy_training_mode"] == "local": + training_manager = LocalTrainingManager(policies=policy_list, log_dir=log_dir) +else: + server_processes = [ + Process(target=get_policy_server_process, args=(server_id,)) for server_id in range(NUM_POLICY_SERVERS) + ] + + for server_process in server_processes: + server_process.start() + + training_manager = ParallelTrainingManager( + policy2server={id_: f"SERVER.{id_ % NUM_POLICY_SERVERS}" for id_ in AGENT_IDS}, # policy-server mapping + group=config["multi-process"]["group"], + log_dir=log_dir + ) From 2b1541b3813f43d43f0fef3f5e14e1d6cef1fd01 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 16 Jun 2021 15:42:45 +0000 Subject: [PATCH 304/482] added MultiProcessRolloutManager and MultiProcessTrainingManager --- docs/source/apidoc/maro.rl.rst | 4 +- docs/source/key_components/rl_toolkit.rst | 6 +- examples/cim/dqn/config.yml | 20 +- examples/cim/dqn/general.py | 6 +- examples/cim/dqn/main.py | 5 +- examples/cim/dqn/policy.py | 26 +- examples/cim/dqn/rollout_manager.py | 78 +++--- examples/cim/dqn/training_manager.py | 38 +-- maro/rl/__init__.py | 10 +- maro/rl/algorithms/dqn.py | 3 +- .../exploration/epsilon_greedy_exploration.py | 3 - maro/rl/training/__init__.py | 20 +- maro/rl/training/decision_generator.py | 45 +++- maro/rl/training/message_enums.py | 2 +- maro/rl/training/policy_server.py | 47 ---- maro/rl/training/rollout_manager.py | 229 +++++++++++++++--- maro/rl/training/rollout_worker.py | 174 ++++++++----- maro/rl/training/trainer.py | 62 +++++ maro/rl/training/training_manager.py | 99 ++++++-- 19 files changed, 594 insertions(+), 283 deletions(-) delete mode 100644 maro/rl/training/policy_server.py create mode 100644 maro/rl/training/trainer.py diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index 97c8b78cf..0e5babc17 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -177,10 +177,10 @@ maro.rl.training.local_learner :undoc-members: :show-inheritance: -maro.rl.training.policy_server +maro.rl.training.trainer -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.policy_server +.. automodule:: maro.rl.training.trainer :members: :undoc-members: :show-inheritance: diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index a449faaf2..69bc98322 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -8,12 +8,12 @@ components. At the top level of a training workflow are: process. The learner process executes training cycles that alternate between data collection and policy updates. * Rollout manager, which is responsible for collecting simulation data. The ``LocalRolloutManager`` performs roll-outs - locally, while the ``ParallelRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in parallel. + locally, while the ``MultiNodeRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in parallel. * Training manager, which manages a set of policies and controls their updates. The policy instances may reside in the - manager (``LocalTrainingManager``) or be distributed on a set of remote nodes (``ParallelTrainingManager``) for parallelized + manager (``LocalTrainingManager``) or be distributed on a set of remote nodes (``MultiNodeTrainingManager``) for parallelized training. * roll-out worker, which consists of an environment instance and a set of policies that agents use to interact with it, is a - remote roll-out worker instance managed by a ``ParallelRolloutManager``. + remote roll-out worker instance managed by a ``MultiNodeRolloutManager``. .. image:: ../images/rl/learner.svg diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 0d099697d..81dfdd4b3 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -3,14 +3,14 @@ experiment_name: cim_dqn mode: multi-process # local, multi-process -num_episodes: 3 +num_episodes: 10 # eval_schedule: 2 -num_steps: -1 +num_steps: 50 env: basic: scenario: cim - topology: global_trade.22p_l0.0 - durations: 1120 + topology: toy.4p_ssdd_l0.0 + durations: 280 wrapper: port_attributes: - empty @@ -40,8 +40,8 @@ exploration: initial_value: 0.4 final_value: 0.0 splits: - - [50, 0.32] -policy: + - [5, 0.32] +policies: model: network: hidden_dims: @@ -79,12 +79,12 @@ policy: replace: true update_trigger: 16 warmup: 1 -multi-process: - rollout_mode: parallel # local, parallel - policy_training_mode: local # local, parallel +distributed: + rollout_mode: multi-process # local, multi-process + policy_training_mode: multi-process # local, multi-process group: cim-dqn num_rollout_workers: 3 - num_policy_servers: 2 + num_trainers: 2 redis_host: localhost redis_port: 6379 # max_receive_attempts: 2 diff --git a/examples/cim/dqn/general.py b/examples/cim/dqn/general.py index 653d471d5..1087f31d3 100644 --- a/examples/cim/dqn/general.py +++ b/examples/cim/dqn/general.py @@ -15,14 +15,14 @@ log_dir = os.path.join(FILE_PATH, "logs", config["experiment_name"]) # Obtain model input and output dimensions from env wrapper settings -config["policy"]["model"]["network"]["input_dim"] = ( +config["policies"]["model"]["network"]["input_dim"] = ( (config["env"]["wrapper"]["look_back"] + 1) * (config["env"]["wrapper"]["max_ports_downstream"] + 1) * len(config["env"]["wrapper"]["port_attributes"]) + len(config["env"]["wrapper"]["vessel_attributes"]) ) -config["policy"]["model"]["network"]["output_dim"] = config["env"]["wrapper"]["num_actions"] +config["policies"]["model"]["network"]["output_dim"] = config["env"]["wrapper"]["num_actions"] NUM_ACTIONS = config["env"]["wrapper"]["num_actions"] AGENT_IDS = Env(**config["env"]["basic"]).agent_idx_list -NUM_POLICY_SERVERS = config["multi-process"]["num_policy_servers"] +NUM_POLICY_TRAINERS = config["distributed"]["num_trainers"] diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index f314f3311..87315a5e3 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -14,7 +14,7 @@ sys.path.insert(0, dqn_path) from env_wrapper import CIMEnvWrapper from general import NUM_ACTIONS, config, log_dir -from policy import get_independent_policy +from policy import get_independent_policy_for_training from rollout_manager import rollout_manager from training_manager import training_manager @@ -31,7 +31,7 @@ ) local_learner = LocalLearner( env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - policies=[get_independent_policy(config["policy"], i) for i in env.agent_idx_list], + policies=[get_independent_policy_for_training(config["policy"], i) for i in env.agent_idx_list], agent2policy={i: i for i in env.agent_idx_list}, num_episodes=config["num_episodes"], num_steps=config["num_steps"], @@ -48,7 +48,6 @@ # eval_schedule=config["eval_schedule"], log_dir=log_dir ) - time.sleep(5) learner.run() else: print("Two modes are supported: local or multi-process.") diff --git a/examples/cim/dqn/policy.py b/examples/cim/dqn/policy.py index 61cef8ba0..d28303ff7 100644 --- a/examples/cim/dqn/policy.py +++ b/examples/cim/dqn/policy.py @@ -1,11 +1,20 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import os +import sys + import numpy as np import torch import torch.nn as nn from maro.rl import DQN, DQNConfig, DiscreteQNet, ExperienceManager, FullyConnectedBlock, OptimOption +dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory +cim_path = os.path.dirname(dqn_path) # CIM example directory +sys.path.insert(0, cim_path) +sys.path.insert(0, dqn_path) +from general import config + class QNet(DiscreteQNet): def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): @@ -18,17 +27,28 @@ def forward(self, states): return self.component(states) -def get_independent_policy(cfg, name, training: bool = True): +def get_independent_policy_for_training(name): + cfg = config["policies"] qnet = QNet( FullyConnectedBlock(**cfg["model"]["network"]), optim_option=OptimOption(**cfg["model"]["optimization"]) ) - exp_cfg = cfg["experience_manager"]["training"] if training else cfg["experience_manager"]["rollout"] return DQN( name=name, q_net=qnet, - experience_manager=ExperienceManager(**exp_cfg), + experience_manager=ExperienceManager(**cfg["experience_manager"]["training"]), config=DQNConfig(**cfg["algorithm_config"]), update_trigger=cfg["update_trigger"], warmup=cfg["warmup"] ) + + +def get_independent_policy_for_rollout(name): + cfg = config["policies"] + qnet = QNet(FullyConnectedBlock(**cfg["model"]["network"])) + return DQN( + name=name, + q_net=qnet, + experience_manager=ExperienceManager(**cfg["experience_manager"]["rollout"]), + config=DQNConfig(**cfg["algorithm_config"]) + ) diff --git a/examples/cim/dqn/rollout_manager.py b/examples/cim/dqn/rollout_manager.py index 802cfd0da..596aaf46c 100644 --- a/examples/cim/dqn/rollout_manager.py +++ b/examples/cim/dqn/rollout_manager.py @@ -3,74 +3,58 @@ import os import sys -from multiprocessing import Process from maro.rl import ( EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, LocalDecisionGenerator, - LocalRolloutManager, ParallelRolloutManager, rollout_worker + LocalRolloutManager, MultiProcessRolloutManager ) from maro.simulator import Env -from maro.utils import set_seeds dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory cim_path = os.path.dirname(dqn_path) # CIM example directory sys.path.insert(0, cim_path) sys.path.insert(0, dqn_path) from env_wrapper import CIMEnvWrapper -from general import NUM_ACTIONS, config, log_dir -from policy import get_independent_policy +from general import AGENT_IDS, NUM_ACTIONS, config, log_dir +from policy import get_independent_policy_for_rollout -def get_rollout_worker_process(): - env = Env(**config["env"]["basic"]) - decision_generator = LocalDecisionGenerator( - agent2policy={i: i for i in env.agent_idx_list}, - policies=[get_independent_policy(config["policy"], i, training=False) for i in env.agent_idx_list], - log_dir=log_dir +def get_env(): + return CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]) + +def get_decision_generator(): + epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) + epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + last_ep=config["num_episodes"], + **config["exploration"] ) - rollout_worker( - env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - decision_generator=decision_generator, - group=config["multi-process"]["group"], - exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, - agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, - log_dir=log_dir, - redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) + return LocalDecisionGenerator( + agent2policy={i: i for i in AGENT_IDS}, + policies=[get_independent_policy_for_rollout(i) for i in AGENT_IDS], + exploration_dict={f"EpsilonGreedy": epsilon_greedy}, + agent2exploration={i: "EpsilonGreedy" for i in AGENT_IDS}, + log_dir=log_dir ) -epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) -epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - last_ep=config["num_episodes"], - **config["exploration"] -) -if config["multi-process"]["rollout_mode"] == "local": +if config["distributed"]["rollout_mode"] == "local": env = Env(**config["env"]["basic"]) rollout_manager = LocalRolloutManager( - env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - policies=[get_independent_policy(config["policy"], i) for i in env.agent_idx_list], - agent2policy={i: i for i in env.agent_idx_list}, - exploration_dict={f"EpsilonGreedy1": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, - agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, + CIMEnvWrapper(env, **config["env"]["wrapper"]), + [get_independent_policy_for_rollout(i) for i in env.agent_idx_list], + {i: i for i in env.agent_idx_list}, + num_steps=config["num_steps"], + exploration_dict={f"EpsilonGreedy": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, + agent2exploration={i: "EpsilonGreedy" for i in env.agent_idx_list}, log_dir=log_dir ) else: - rollout_worker_processes = [ - Process(target=get_rollout_worker_process) for _ in range(config["multi-process"]["num_rollout_workers"]) - ] - - for i, rollout_worker_process in enumerate(rollout_worker_processes): - set_seeds(i) - rollout_worker_process.start() - - rollout_manager = ParallelRolloutManager( - num_rollout_workers=config["multi-process"]["num_rollout_workers"], - group=config["multi-process"]["group"], - exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, - # max_receive_attempts=config["multi-process"]["max_receive_attempts"], - # receive_timeout=config["multi-process"]["receive_timeout"], + rollout_manager = MultiProcessRolloutManager( + config["distributed"]["num_rollout_workers"], + get_env, + get_decision_generator, + num_steps=config["num_steps"], log_dir=log_dir, - redis_address=(config["multi-process"]["redis_host"], config["multi-process"]["redis_port"]) ) diff --git a/examples/cim/dqn/training_manager.py b/examples/cim/dqn/training_manager.py index 895f61d76..01285a932 100644 --- a/examples/cim/dqn/training_manager.py +++ b/examples/cim/dqn/training_manager.py @@ -3,44 +3,24 @@ import os import sys -from multiprocessing import Process -from maro.rl import LocalTrainingManager, ParallelTrainingManager, PolicyServer +from maro.rl import LocalTrainingManager, MultiProcessTrainingManager dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory cim_path = os.path.dirname(dqn_path) # CIM example directory sys.path.insert(0, cim_path) sys.path.insert(0, dqn_path) -from general import AGENT_IDS, NUM_POLICY_SERVERS, config, log_dir -from policy import get_independent_policy +from general import AGENT_IDS, NUM_POLICY_TRAINERS, config, log_dir +from policy import get_independent_policy_for_training -def get_policy_server_process(server_id): - server = PolicyServer( - policies=[ - get_independent_policy(config["policy"], agent_id) - for agent_id in AGENT_IDS if agent_id % NUM_POLICY_SERVERS == server_id - ], - group=config["multi-process"]["group"], - name=f"SERVER.{server_id}", - log_dir=log_dir +if config["distributed"]["policy_training_mode"] == "local": + training_manager = LocalTrainingManager( + [get_independent_policy_for_training(i) for i in AGENT_IDS], log_dir=log_dir ) - server.run() - - -policy_list = [get_independent_policy(config["policy"], i) for i in AGENT_IDS] -if config["multi-process"]["policy_training_mode"] == "local": - training_manager = LocalTrainingManager(policies=policy_list, log_dir=log_dir) else: - server_processes = [ - Process(target=get_policy_server_process, args=(server_id,)) for server_id in range(NUM_POLICY_SERVERS) - ] - - for server_process in server_processes: - server_process.start() - - training_manager = ParallelTrainingManager( - policy2server={id_: f"SERVER.{id_ % NUM_POLICY_SERVERS}" for id_ in AGENT_IDS}, # policy-server mapping - group=config["multi-process"]["group"], + training_manager = MultiProcessTrainingManager( + {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping + {i: get_independent_policy_for_training for i in AGENT_IDS}, log_dir=log_dir ) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 8a31f438c..5cb443ef8 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -19,8 +19,9 @@ from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( AbsDecisionGenerator, AbsEarlyStopper, AbsRolloutManager, AbsTrainingManager, Learner, LocalDecisionGenerator, - LocalLearner, LocalRolloutManager, LocalTrainingManager, ParallelRolloutManager, ParallelTrainingManager, - PolicyServer, rollout_worker + LocalLearner, LocalRolloutManager, LocalTrainingManager, MultiNodeRolloutManager, MultiNodeTrainingManager, + MultiProcessRolloutManager, MultiProcessTrainingManager, rollout_worker_node, rollout_worker_process, trainer_node, + trainer_process ) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, @@ -39,8 +40,9 @@ "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", "AbsDecisionGenerator", "AbsEarlyStopper", "AbsRolloutManager", "AbsTrainingManager", "Learner", - "LocalDecisionGenerator","LocalLearner", "LocalRolloutManager", "LocalTrainingManager", "ParallelRolloutManager", - "ParallelTrainingManager", "PolicyServer", "rollout_worker", + "LocalDecisionGenerator","LocalLearner", "LocalRolloutManager", "LocalTrainingManager", "MultiNodeRolloutManager", + "MultiNodeTrainingManager", "MultiProcessRolloutManager", "MultiProcessTrainingManager", "rollout_worker_node", + "rollout_worker_process", "trainer_node", "trainer_process", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index 0e7ef713a..48701638b 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -139,7 +139,8 @@ def update(self): def set_state(self, policy_state): self.q_net.load_state_dict(policy_state) self.target_q_net = self.q_net.copy() if self.q_net.trainable else None - self.target_q_net.eval() + if self.target_q_net: + self.target_q_net.eval() def get_state(self): return self.q_net.state_dict() diff --git a/maro/rl/exploration/epsilon_greedy_exploration.py b/maro/rl/exploration/epsilon_greedy_exploration.py index b51cec44e..036c3e0b5 100644 --- a/maro/rl/exploration/epsilon_greedy_exploration.py +++ b/maro/rl/exploration/epsilon_greedy_exploration.py @@ -25,9 +25,6 @@ def __call__(self, action_index: Union[int, np.ndarray]): else: return self._get_exploration_action(action_index) - def set_params(self, *, epsilon: float): - self.epsilon = epsilon - def _get_exploration_action(self, action_index): assert (action_index < self._num_actions), f"Invalid action: {action_index}" return action_index if np.random.random() > self.epsilon else np.random.choice(self._num_actions) diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 5f1af6783..52cdf1669 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -5,18 +5,22 @@ from .early_stopper import AbsEarlyStopper from .learner import Learner from .local_learner import LocalLearner -from .policy_server import PolicyServer -from .rollout_manager import AbsRolloutManager, LocalRolloutManager, ParallelRolloutManager -from .rollout_worker import rollout_worker -from .training_manager import AbsTrainingManager, LocalTrainingManager, ParallelTrainingManager +from .rollout_manager import ( + AbsRolloutManager, LocalRolloutManager, MultiProcessRolloutManager, MultiNodeRolloutManager +) +from .rollout_worker import rollout_worker_node, rollout_worker_process +from .trainer import trainer_node, trainer_process +from .training_manager import ( + AbsTrainingManager, LocalTrainingManager, MultiNodeTrainingManager, MultiProcessTrainingManager +) __all__ = [ "AbsDecisionGenerator", "LocalDecisionGenerator", "AbsEarlyStopper", "Learner", "LocalLearner", - "PolicyServer", "PolicyServerGateway", - "AbsRolloutManager", "LocalRolloutManager", "ParallelRolloutManager", - "rollout_worker", - "AbsTrainingManager", "LocalTrainingManager", "ParallelTrainingManager" + "AbsRolloutManager", "LocalRolloutManager", "MultiProcessRolloutManager", "MultiNodeRolloutManager", + "rollout_worker_node", "rollout_worker_process", + "trainer_node", "trainer_process", + "AbsTrainingManager", "LocalTrainingManager", "MultiNodeTrainingManager", "MultiProcessTrainingManager" ] diff --git a/maro/rl/training/decision_generator.py b/maro/rl/training/decision_generator.py index 3fd3a1e80..18d517af4 100644 --- a/maro/rl/training/decision_generator.py +++ b/maro/rl/training/decision_generator.py @@ -2,18 +2,30 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from collections import defaultdict + from os import getcwd from typing import Dict, List +from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsPolicy from maro.utils import Logger class AbsDecisionGenerator(ABC): - def __init__(self, agent2policy: Dict[str, str]): + def __init__( + self, + agent2policy: Dict[str, str], + exploration_dict: Dict[str, AbsExploration] = None, + agent2exploration: Dict[str, str] = None + ): super().__init__() self.agent2policy = agent2policy + self.exploration_dict = exploration_dict + if self.exploration_dict: + self.exploration_by_agent = { + agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() + } + self.exploring = True # Flag indicating that exploration is turned on. @abstractmethod def choose_action(self, state: dict, ep: int, step: int) -> dict: @@ -26,6 +38,17 @@ def choose_action(self, state: dict, ep: int, step: int) -> dict: """ raise NotImplementedError + def exploration_step(self): + for exploration in self.exploration_dict.values(): + exploration.step() + print(f"epsilon: {exploration.epsilon}") + + def exploit(self): + self.exploring = False + + def explore(self): + self.exploring = True + class LocalDecisionGenerator(AbsDecisionGenerator): """Local decision generator. @@ -37,8 +60,15 @@ class LocalDecisionGenerator(AbsDecisionGenerator): log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ - def __init__(self, agent2policy: Dict[str, str], policies: List[AbsPolicy], log_dir: str = getcwd()): - super().__init__(agent2policy) + def __init__( + self, + agent2policy: Dict[str, str], + policies: List[AbsPolicy], + exploration_dict: Dict[str, AbsExploration] = None, + agent2exploration: Dict[str, str] = None, + log_dir: str = getcwd() + ): + super().__init__(agent2policy, exploration_dict=exploration_dict, agent2exploration=agent2exploration) self.policy_dict = {policy.name: policy for policy in policies} self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} self._logger = Logger("local_decision_generator", dump_folder=log_dir) @@ -51,7 +81,12 @@ def choose_action(self, state: dict, ep: int, step: int) -> dict: ep (int): Current episode. step (int): Current step. """ - return {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} + action_by_agent = {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} + if self.exploring and self.exploration_dict: + for agent_id in action_by_agent: + action_by_agent[agent_id] = self.exploration_by_agent[agent_id](action_by_agent[agent_id]) + + return action_by_agent def store_experiences(self, exp_by_agent: dict) -> set: """Store agent experiences in the policies' experience managers.""" diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index 9ab4d7820..5d6623198 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -28,7 +28,7 @@ class MsgKey(Enum): NUM_EXPERIENCES = "num_experiences" STATE = "state" POLICY = "policy" - EXPLORATION = "exploration" + EXPLORATION_STEP = "exploration_step" VERSION = "version" NUM_STEPS = "num_steps" EPISODE_END = "episode_end" diff --git a/maro/rl/training/policy_server.py b/maro/rl/training/policy_server.py deleted file mode 100644 index 08272e715..000000000 --- a/maro/rl/training/policy_server.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import List - -from maro.communication import Proxy -from maro.rl.policy import AbsPolicy -from maro.utils import Logger - -from .message_enums import MsgKey, MsgTag - - -class PolicyServer: - def __init__( - self, - policies: List[AbsPolicy], - group: str, - name: str, - log_dir: str = getcwd(), - **proxy_kwargs - ): - self.policy_dict = {policy.name: policy for policy in policies} - self._proxy = Proxy(group, "policy_server", {"training_manager": 1}, component_name=name, **proxy_kwargs) - self._logger = Logger(self._proxy.name, dump_folder=log_dir) - - def run(self): - for msg in self._proxy.receive(): - if msg.tag == MsgTag.EXIT: - self._logger.info("Exiting...") - self._proxy.close() - break - - if msg.tag == MsgTag.TRAIN: - t0 = time.time() - updated = { - name: self.policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() - if self.policy_dict[name].on_experiences(exp) - } - t1 = time.time() - self._logger.debug(f"total policy update time: {t1 - t0}") - self._proxy.reply(msg, body={MsgKey.POLICY: updated}) - self._logger.debug(f"reply time: {time.time() - t1}") - elif msg.tag == MsgTag.GET_POLICY_STATE: - policy_state_dict = {name: policy.get_state() for name, policy in self.policy_dict.items()} - self._proxy.reply(msg, tag=MsgTag.POLICY_STATE, body={MsgKey.POLICY: policy_state_dict}) diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 80b2463f5..031548c70 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -1,12 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from pickle import FALSE import time from abc import ABC, abstractmethod from collections import defaultdict +from multiprocessing import Pipe, Process from os import getcwd from random import choices -from typing import Dict, List +from typing import Callable, Dict, List from maro.communication import Proxy, SessionType from maro.rl.env_wrapper import AbsEnvWrapper @@ -15,7 +17,9 @@ from maro.rl.policy import AbsPolicy from maro.utils import Logger +from .decision_generator import AbsDecisionGenerator from .message_enums import MsgKey, MsgTag +from .rollout_worker import rollout_worker_process class AbsRolloutManager(ABC): @@ -93,6 +97,7 @@ def __init__( if num_steps == 0 or num_steps < -1: raise ValueError("num_steps must be a positive integer or -1") + super().__init__() self._logger = Logger("LOCAL_ROLLOUT_MANAGER", dump_folder=log_dir) self.env = env @@ -124,7 +129,16 @@ def __init__( self._log_env_summary = log_env_summary def collect(self, ep: int, segment: int, policy_state_dict: dict): - """Collect simulation data for training.""" + """Collect simulation data, i.e., experiences for training. + + Args: + ep (int): Current episode index. + segment (int): Current segment index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Experiences for policy training. + """ t0 = time.time() learning_time = 0 num_experiences_collected = 0 @@ -224,19 +238,152 @@ def _load_policy_states(self, policy_state_dict: dict): self._logger.info(f"updated policies {list(policy_state_dict.keys())}") -class ParallelRolloutManager(AbsRolloutManager): +class MultiProcessRolloutManager(AbsRolloutManager): + """Roll-out manager that spawn a set of roll-out worker processes for parallel data collection. + + Args: + num_workers (int): Number of remote roll-out workers. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to + None, in which case the number is set to ``num_workers``. + receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the workers. This + This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the + desired amount of data from workers. Defaults to None, in which case each receive attempt is blocking. + max_staleness (int): Maximum allowable staleness measured in the number of calls to ``collect``. Experiences + collected from calls to ``collect`` within ``max_staleness`` calls ago will be returned to the learner. + Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. + num_eval_workers (int): Number of workers for evaluation. Defaults to 1. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + def __init__( + self, + num_workers: int, + create_env_wrapper_func: Callable[[], AbsEnvWrapper], + create_decision_generator_func: Callable[[], AbsDecisionGenerator], + create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, + num_steps: int = -1, + num_eval_workers: int = 1, + log_env_summary: bool = True, + log_dir: str = getcwd(), + ): + super().__init__() + self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) + self._num_workers = num_workers + self._num_steps = num_steps + self._log_env_summary = log_env_summary + self._num_eval_workers = num_eval_workers + self.total_experiences_collected = 0 + self.total_env_steps = 0 + self._exploration_step = False + + self._worker_processes = [] + self._manager_ends = [] + for index in range(self._num_workers): + manager_end, worker_end = Pipe() + self._manager_ends.append(manager_end) + worker = Process( + target=rollout_worker_process, + args=( + index, + worker_end, + create_env_wrapper_func, + create_decision_generator_func, + create_eval_env_wrapper_func, + log_dir + ) + ) + self._worker_processes.append(worker) + worker.start() + + def collect(self, ep: int, segment: int, policy_state_dict: dict): + """Collect simulation data, i.e., experiences for training. + + Args: + ep (int): Current episode index. + segment (int): Current segment index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Experiences for policy training. + """ + rollout_req = { + "type": "collect", + "episode": ep, + "segment": segment, + "num_steps": self._num_steps, + "policy": policy_state_dict, + "exploration_step": self._exploration_step + } + + for conn in self._manager_ends: + conn.send(rollout_req) + + if self._exploration_step: + self._exploration_step = False + + combined_exp_by_policy = defaultdict(ExperienceSet) + for conn in self._manager_ends: + result = conn.recv() + exp_by_policy = result["experiences"] + self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) + self.total_env_steps += result["num_steps"] + + for policy_name, exp in exp_by_policy.items(): + combined_exp_by_policy[policy_name].extend(exp) + + # log roll-out summary + self.episode_complete = result["episode_end"] + if self.episode_complete and self._log_env_summary: + env_summary = result["env_summary"] + self._logger.info(f"env summary: {env_summary}") + + if self.episode_complete: + self._exploration_step = True + + return combined_exp_by_policy + + def evaluate(self, ep: int, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``. + + Args: + ep (int): Current training episode index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Environment summary. + """ + for conn in self._manager_ends: + conn.send({"type": "evaluate", "episode": ep, "policy": policy_state_dict}) + + env_summary_dict = {} + for conn in self._manager_ends: + result = conn.recv() + env_summary_dict[result["worker_id"]] = result["env_summary"] + + return env_summary_dict + + def exit(self): + """Tell the worker processes to exit.""" + for conn in self._manager_ends: + conn.send({"type": "quit"}) + + +class MultiNodeRolloutManager(AbsRolloutManager): """Controller for a set of remote roll-out workers. Args: - num_rollout_workers (int): Number of remote roll-out workers. + num_workers (int): Number of remote roll-out workers. group (str): Group name for the roll-out manager and the set of roll-out workers managed by it. The roll-out workers' processes must be assigned this group name in order to form a communicating cluster. - exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. The exploration parameters - from these instances will be broadcast to all workers. Defaults to None. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to - None, in which case the number is set to ``num_rollout_workers``. + None, in which case the number is set to ``num_workers``. receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the workers. This This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the desired amount of data from workers. Defaults to None, in which case each receive attempt is blocking. @@ -254,9 +401,8 @@ class ParallelRolloutManager(AbsRolloutManager): """ def __init__( self, - num_rollout_workers: int, + num_workers: int, group: str, - exploration_dict: Dict[str, AbsExploration] = None, num_steps: int = -1, max_receive_attempts: int = None, receive_timeout: int = None, @@ -266,21 +412,20 @@ def __init__( log_dir: str = getcwd(), **proxy_kwargs ): - super().__init__() - if num_eval_workers > num_rollout_workers: + if num_eval_workers > num_workers: raise ValueError("num_eval_workers cannot exceed the number of available workers") - self._logger = Logger("PARALLEL_ROLLOUT_MANAGER", dump_folder=log_dir) - self.num_rollout_workers = num_rollout_workers - peers = {"rollout_worker": num_rollout_workers} + super().__init__() + self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) + self.num_workers = num_workers + peers = {"rollout_worker": num_workers} self._proxy = Proxy(group, "rollout_manager", peers, **proxy_kwargs) self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's - self.exploration_dict = exploration_dict self._num_steps = num_steps if max_receive_attempts is None: - max_receive_attempts = self.num_rollout_workers + max_receive_attempts = self.num_workers self._logger.info(f"Maximum receive attempts is set to {max_receive_attempts}") self.max_receive_attempts = max_receive_attempts @@ -293,42 +438,49 @@ def __init__( self._num_eval_workers = num_eval_workers - self._exploration_update = True + self._exploration_step = False - def collect(self, episode_index: int, segment_index: int, policy_state_dict: dict): - """Collect simulation data, i.e., experiences for training.""" + def collect(self, ep: int, segment: int, policy_state_dict: dict): + """Collect simulation data, i.e., experiences for training. + + Args: + ep (int): Current episode index. + segment (int): Current segment index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Experiences for policy training. + """ if self._log_env_summary: - self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") + self._logger.info(f"EPISODE-{ep}, SEGMENT-{segment}: ") msg_body = { - MsgKey.EPISODE: episode_index, - MsgKey.SEGMENT: segment_index, + MsgKey.EPISODE: ep, + MsgKey.SEGMENT: segment, MsgKey.NUM_STEPS: self._num_steps, - MsgKey.POLICY: policy_state_dict + MsgKey.POLICY: policy_state_dict, + MsgKey.EXPLORATION_STEP: self._exploration_step } - if self._exploration_update: - msg_body[MsgKey.EXPLORATION] = { - name: exploration.parameters for name, exploration in self.exploration_dict.items() - } - self._exploration_update = False - self._proxy.ibroadcast("rollout_worker", MsgTag.COLLECT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent collect requests to {self._workers} for ep-{episode_index}, segment-{segment_index}") + self._logger.info(f"Sent collect requests to {self._workers} for ep-{ep}, segment-{segment}") + + if self._exploration_step: + self._exploration_step = False # Receive roll-out results from remote workers combined_exp_by_policy = defaultdict(ExperienceSet) num_finishes = 0 for _ in range(self.max_receive_attempts): msg = self._proxy.receive_once(timeout=self.receive_timeout) - if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE] != episode_index: + if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE] != ep: self._logger.info( f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE]} " - f"(expected message type {MsgTag.COLLECT} and episode index {episode_index})" + f"(expected message type {MsgTag.COLLECT} and episode index {ep})" ) continue - if segment_index - msg.body[MsgKey.SEGMENT] <= self._max_staleness: + if segment - msg.body[MsgKey.SEGMENT] <= self._max_staleness: exp_by_policy = msg.body[MsgKey.EXPERIENCES] self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) self.total_env_steps += msg.body[MsgKey.NUM_STEPS] @@ -336,21 +488,18 @@ def collect(self, episode_index: int, segment_index: int, policy_state_dict: dic for policy_name, exp in exp_by_policy.items(): combined_exp_by_policy[policy_name].extend(exp) - if msg.body[MsgKey.SEGMENT] == segment_index: + if msg.body[MsgKey.SEGMENT] == segment: self.episode_complete = msg.body[MsgKey.EPISODE_END] if self.episode_complete: # log roll-out summary if self._log_env_summary: self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") num_finishes += 1 - if num_finishes == self.num_rollout_workers: + if num_finishes == self.num_workers: break - + if self.episode_complete: - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() - self._exploration_update = True + self._exploration_step = True return combined_exp_by_policy diff --git a/maro/rl/training/rollout_worker.py b/maro/rl/training/rollout_worker.py index 6fda4e9ea..5f58fd1a1 100644 --- a/maro/rl/training/rollout_worker.py +++ b/maro/rl/training/rollout_worker.py @@ -1,26 +1,104 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import defaultdict +from multiprocessing.connection import Connection from os import getcwd -from typing import Dict +from typing import Callable from maro.communication import Proxy from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.exploration import AbsExploration -from maro.utils import Logger +from maro.utils import Logger, set_seeds from .decision_generator import AbsDecisionGenerator from .message_enums import MsgKey, MsgTag -def rollout_worker( - env: AbsEnvWrapper, - decision_generator: AbsDecisionGenerator, +def rollout_worker_process( + index: int, + conn: Connection, + create_env_wrapper_func: Callable[[], AbsEnvWrapper], + create_decision_generator_func: Callable[[], AbsDecisionGenerator], + create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper], + log_dir: str +): + set_seeds(index) + env_wrapper = create_env_wrapper_func() + eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() + decision_generator = create_decision_generator_func() + logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) + + def collect(msg): + ep, segment = msg["episode"], msg["segment"] + # load policies + if hasattr(decision_generator, "update"): + decision_generator.update(msg["policy"]) + + # update exploration parameters + decision_generator.explore() + if msg["exploration_step"]: + decision_generator.exploration_step() + + if env_wrapper.state is None: + logger.info(f"Training episode {ep}") + env_wrapper.reset() + env_wrapper.start() # get initial state + + starting_step_index = env_wrapper.step_index + 1 + steps_to_go = float("inf") if msg["num_steps"] == -1 else msg["num_steps"] + while env_wrapper.state and steps_to_go > 0: + action = decision_generator.choose_action(env_wrapper.state, ep, env_wrapper.step_index) + env_wrapper.step(action) + steps_to_go -= 1 + + logger.info( + f"Roll-out finished for ep {ep}, segment {segment}" + f"(steps {starting_step_index} - {env_wrapper.step_index})" + ) + + if hasattr(decision_generator, "store_experiences"): + policy_names = decision_generator.store_experiences(env_wrapper.get_experiences()) + ret_exp = decision_generator.get_experiences_by_policy(policy_names) + + return_info = { + "worker_index": index, + "episode_end": not env_wrapper.state, + "experiences": ret_exp, + "env_summary": env_wrapper.summary, + "num_steps": env_wrapper.step_index - starting_step_index + 1 + } + + conn.send(return_info) + + def evaluate(msg): + logger.info(f"Evaluating...") + eval_env_wrapper.reset() + eval_env_wrapper.start() # get initial state + decision_generator.exploit() + if hasattr(decision_generator, "update"): + decision_generator.update(msg["policy"]) + while eval_env_wrapper.state: + action = decision_generator.choose_action( + eval_env_wrapper.state, msg["episode"], eval_env_wrapper.step_index + ) + eval_env_wrapper.step(action) + + conn.send({"worker_id": index, "env_summary": eval_env_wrapper.summary}) + + while True: + msg = conn.recv() + if msg["type"] == "collect": + collect(msg) + elif msg["type"] == "evaluate": + evaluate(msg) + elif msg["type"] == "quit": + break + + +def rollout_worker_node( + create_env_wrapper_func: Callable[[], AbsEnvWrapper], + create_decision_generator_func: Callable[[], AbsDecisionGenerator], group: str, - exploration_dict: Dict[str, AbsExploration] = None, - agent2exploration: Dict[str, str] = None, - eval_env: AbsEnvWrapper = None, + create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, log_dir: str = getcwd(), **proxy_kwargs ): @@ -43,18 +121,9 @@ def rollout_worker( proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. """ - eval_env = eval_env if eval_env else env - # mappings between exploration schemes and agents - if exploration_dict: - exploration_by_agent = { - agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() - } - agent_groups_by_exploration = defaultdict(list) - for agent_id, exploration_id in agent2exploration.items(): - agent_groups_by_exploration[exploration_id].append(agent_id) - - for exploration_id, agent_ids in agent_groups_by_exploration.items(): - agent_groups_by_exploration[exploration_id] = tuple(agent_ids) + env_wrapper = create_env_wrapper_func() + eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() + decision_generator = create_decision_generator_func() proxy = Proxy(group, "rollout_worker", {"rollout_manager": 1}, **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) @@ -65,54 +134,38 @@ def collect(msg): if hasattr(decision_generator, "update"): decision_generator.update(msg.body[MsgKey.POLICY]) # set exploration parameters - exploration_params = None - if MsgKey.EXPLORATION in msg.body: - updated_exploration_param_names = {} - for exploration_name, param_dict in msg.body[MsgKey.EXPLORATION].items(): - updated_exploration_param_names[exploration_name] = param_dict.keys() - for param_name, value in param_dict.items(): - setattr(exploration_dict[exploration_name], param_name, value) - - exploration_params = { - agent_ids: { - param_name: getattr(exploration_dict[exploration_name], param_name) - for param_name in updated_exploration_param_names[exploration_name] - } - for exploration_name, agent_ids in agent_groups_by_exploration.items() - } - logger.info(f"Exploration parameters: {exploration_params}") - - if env.state is None: + decision_generator.explore() + if msg.body[MsgKey.EXPLORATION_STEP]: + decision_generator.exploration_step() + + if env_wrapper.state is None: logger.info(f"Training episode {msg.body[MsgKey.EPISODE]}") - env.reset() - env.start() # get initial state + env_wrapper.reset() + env_wrapper.start() # get initial state - starting_step_index = env.step_index + 1 + starting_step_index = env_wrapper.step_index + 1 steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] - while env.state and steps_to_go > 0: - action = decision_generator.choose_action(env.state, ep, env.step_index) - if exploration_dict: - for agent_id in action: - action[agent_id] = exploration_by_agent[agent_id](action[agent_id]) - env.step(action) + while env_wrapper.state and steps_to_go > 0: + action = decision_generator.choose_action(env_wrapper.state, ep, env_wrapper.step_index) + env_wrapper.step(action) steps_to_go -= 1 logger.info( f"Roll-out finished for ep {ep}, segment {segment}" - f"(steps {starting_step_index} - {env.step_index})" + f"(steps {starting_step_index} - {env_wrapper.step_index})" ) if hasattr(decision_generator, "store_experiences"): - policy_names = decision_generator.store_experiences(env.get_experiences()) + policy_names = decision_generator.store_experiences(env_wrapper.get_experiences()) ret_exp = decision_generator.get_experiences_by_policy(policy_names) return_info = { - MsgKey.EPISODE_END: not env.state, + MsgKey.EPISODE_END: not env_wrapper.state, MsgKey.EPISODE: ep, MsgKey.SEGMENT: segment, MsgKey.EXPERIENCES: ret_exp, - MsgKey.ENV_SUMMARY: env.summary, - MsgKey.NUM_STEPS: env.step_index - starting_step_index + 1 + MsgKey.ENV_SUMMARY: env_wrapper.summary, + MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1 } proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) @@ -120,15 +173,16 @@ def collect(msg): def evaluate(msg): logger.info(f"Evaluating...") ep = msg.body[MsgKey.EPISODE] - eval_env.reset() - eval_env.start() # get initial state + eval_env_wrapper.reset() + eval_env_wrapper.start() # get initial state + decision_generator.exploit() if hasattr(decision_generator, "update"): decision_generator.update(msg.body[MsgKey.POLICY]) - while eval_env.state: - action = decision_generator.choose_action(env.state, ep, eval_env.step_index) - eval_env.step(action) + while eval_env_wrapper.state: + action = decision_generator.choose_action(eval_env_wrapper.state, ep, eval_env_wrapper.step_index) + eval_env_wrapper.step(action) - return_info = {MsgKey.ENV_SUMMARY: env.summary, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} + return_info = {MsgKey.ENV_SUMMARY: eval_env_wrapper.summary, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) """ diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py new file mode 100644 index 000000000..22c588e94 --- /dev/null +++ b/maro/rl/training/trainer.py @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from multiprocessing.connection import Connection +from os import getcwd +from typing import Callable, Dict + +from maro.communication import Proxy +from maro.utils import Logger + +from .message_enums import MsgKey, MsgTag + + +def trainer_process(trainer_id: str, conn: Connection, create_policy_func_dict: Dict[str, Callable], log_dir: str): + policy_dict = {policy_name: func(policy_name) for policy_name, func in create_policy_func_dict.items()} + logger = Logger("TRAINER", dump_folder=log_dir) + while True: + msg = conn.recv() + if msg["type"] == "train": + t0 = time.time() + updated = { + name: policy_dict[name].get_state() for name, exp in msg["experiences"].items() + if policy_dict[name].on_experiences(exp) + } + logger.debug(f"total policy update time: {time.time() - t0}") + conn.send({"policy": updated}) + elif msg["type"] == "get_policy_state": + policy_state_dict = {name: policy.get_state() for name, policy in policy_dict.items()} + conn.send({"policy": policy_state_dict}) + elif msg["type"] == "quit": + break + + +def trainer_node( + trainer_id: str, + create_policy_func_dict: Dict[str, Callable], + group: str, + log_dir: str = getcwd(), + **proxy_kwargs +): + policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} + proxy = Proxy(group, "trainer", {"training_manager": 1}, component_name=trainer_id, **proxy_kwargs) + logger = Logger(proxy.name, dump_folder=log_dir) + + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.TRAIN: + t0 = time.time() + updated = { + name: policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() + if policy_dict[name].on_experiences(exp) + } + logger.debug(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, body={MsgKey.POLICY: updated}) + elif msg.tag == MsgTag.GET_POLICY_STATE: + policy_state_dict = {name: policy.get_state() for name, policy in policy_dict.items()} + proxy.reply(msg, tag=MsgTag.POLICY_STATE, body={MsgKey.POLICY: policy_state_dict}) diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/training_manager.py index c8e781ffe..1b79a2dc4 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/training_manager.py @@ -4,8 +4,9 @@ import time from abc import ABC, abstractmethod from collections import defaultdict +from multiprocessing import Pipe, Process from os import getcwd -from typing import Dict, List +from typing import Callable, Dict, List from maro.communication import Proxy, SessionType from maro.rl.experience import ExperienceSet @@ -13,6 +14,7 @@ from maro.utils import Logger from .message_enums import MsgKey, MsgTag +from .trainer import trainer_process class AbsTrainingManager(ABC): @@ -83,19 +85,88 @@ def get_state(self): return {name: policy.get_state() for name, policy in self.policy_dict.items()} -class ParallelTrainingManager(AbsTrainingManager): +class MultiProcessTrainingManager(AbsTrainingManager): def __init__( self, - policy2server: Dict[str, str], + policy2trainer: Dict[str, str], + create_policy_func_dict: Dict[str, Callable], + log_dir: str = getcwd(), + ): + super().__init__() + self._logger = Logger("TRAINING_MANAGER", dump_folder=log_dir) + self.policy2trainer = policy2trainer + self._names = list(self.policy2trainer.keys()) + self._trainer2policies = defaultdict(list) + for policy_name, trainer_name in policy2trainer.items(): + self._trainer2policies[trainer_name].append(policy_name) + + self._trainer_processes = [] + self._manager_end = {} + for trainer_id, policy_names in self._trainer2policies.items(): + manager_end, trainer_end = Pipe() + self._manager_end[trainer_id] = manager_end + trainer = Process( + target=trainer_process, + args=( + trainer_id, + trainer_end, + {name: create_policy_func_dict[name] for name in policy_names}, + log_dir + ) + ) + self._trainer_processes.append(trainer) + trainer.start() + + @property + def names(self): + return self._names + + def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + for trainer_id, conn in self._manager_end.items(): + conn.send({ + "type": "train", + "experiences": {name: exp_by_policy[name] for name in self._trainer2policies[trainer_id]} + }) + + policy_state_dict = {} + for conn in self._manager_end.values(): + result = conn.recv() + for policy_name, policy_state in result["policy"].items(): + policy_state_dict[policy_name] = policy_state + + return policy_state_dict + + def get_state(self): + policy_state_dict = {} + for conn in self._manager_end.values(): + conn.send({"type": "get_policy_state"}) + + for conn in self._manager_end.values(): + result = conn.recv() + for policy_name, policy_state in result["policy"].items(): + policy_state_dict[policy_name] = policy_state + + return policy_state_dict + + def exit(self): + """Tell the trainer processes to exit.""" + for conn in self._manager_end.values(): + conn.send({"type": "quit"}) + + +class MultiNodeTrainingManager(AbsTrainingManager): + def __init__( + self, + policy2trainer: Dict[str, str], group: str, log_dir: str = getcwd(), **proxy_kwargs ): super().__init__() - self._logger = Logger("PARALLEL_TRAINING_MANAGER", dump_folder=log_dir) - self.policy2server = policy2server - self._names = list(self.policy2server.keys()) - peers = {"policy_server": len(set(self.policy2server.values()))} + self._logger = Logger("TRAINING_MANAGER", dump_folder=log_dir) + self.policy2trainer = policy2trainer + self._names = list(self.policy2trainer.keys()) + peers = {"trainer": len(set(self.policy2trainer.values()))} self._proxy = Proxy(group, "training_manager", peers, **proxy_kwargs) @property @@ -105,10 +176,10 @@ def names(self): def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): msg_body_by_dest, policy_state_dict = defaultdict(dict), {} for policy_name, exp in exp_by_policy.items(): - policy_server_id = self.policy2server[policy_name] - if MsgKey.EXPERIENCES not in msg_body_by_dest[policy_server_id]: - msg_body_by_dest[policy_server_id][MsgKey.EXPERIENCES] = {} - msg_body_by_dest[policy_server_id][MsgKey.EXPERIENCES][policy_name] = exp + trainer_id = self.policy2trainer[policy_name] + if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): for policy_name, policy_state in reply.body[MsgKey.POLICY].items(): @@ -118,14 +189,14 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): def get_state(self): policy_state_dict = {} - for reply in self._proxy.broadcast("policy_server", MsgTag.GET_POLICY_STATE, SessionType.TASK): + for reply in self._proxy.broadcast("trainer", MsgTag.GET_POLICY_STATE, SessionType.TASK): for policy_name, policy_state in reply.body[MsgKey.POLICY].items(): policy_state_dict[policy_name] = policy_state return policy_state_dict def exit(self): - """Tell the remote actors to exit.""" - self._proxy.ibroadcast("policy_server", MsgTag.EXIT, SessionType.NOTIFICATION) + """Tell the remote trainers to exit.""" + self._proxy.ibroadcast("trainer", MsgTag.EXIT, SessionType.NOTIFICATION) self._proxy.close() self._logger.info("Exiting...") From 6392fcf239e3e43b1469b33b058cfdbaba358ebc Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 17 Jun 2021 12:58:02 +0000 Subject: [PATCH 305/482] updated doc --- docs/source/key_components/rl_toolkit.rst | 10 ++---- maro/rl/training/rollout_manager.py | 29 +++++++++-------- maro/rl/training/rollout_worker.py | 39 +++++++++++++++-------- maro/rl/training/trainer.py | 23 +++++++++++++ maro/rl/training/training_manager.py | 26 +++++++++++++++ 5 files changed, 93 insertions(+), 34 deletions(-) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 69bc98322..f00c764b6 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -7,13 +7,9 @@ components. At the top level of a training workflow are: * Learner, which consists of a roll-out manager and a training manager, is the controller for a learning process. The learner process executes training cycles that alternate between data collection and policy updates. -* Rollout manager, which is responsible for collecting simulation data. The ``LocalRolloutManager`` performs roll-outs - locally, while the ``MultiNodeRolloutManager`` manages a set of remote ``Actor``s to collect simulation data in parallel. -* Training manager, which manages a set of policies and controls their updates. The policy instances may reside in the - manager (``LocalTrainingManager``) or be distributed on a set of remote nodes (``MultiNodeTrainingManager``) for parallelized - training. -* roll-out worker, which consists of an environment instance and a set of policies that agents use to interact with it, is a - remote roll-out worker instance managed by a ``MultiNodeRolloutManager``. +* Rollout manager, which is responsible for collecting simulation data, in local or distributed fashion. +* Training manager, which manages a set of policies and controls their updates. The policy instances may + reside with the manager or be distributed amongst a set of processes or remote nodes for parallelized training. .. image:: ../images/rl/learner.svg diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 031548c70..786fad308 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -239,20 +239,22 @@ def _load_policy_states(self, policy_state_dict: dict): class MultiProcessRolloutManager(AbsRolloutManager): - """Roll-out manager that spawn a set of roll-out worker processes for parallel data collection. + """Roll-out manager that spawns a set of roll-out worker processes for parallel data collection. Args: num_workers (int): Number of remote roll-out workers. + create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an + environment wrapper for training data collection. The function should take no parameters and return an + environment wrapper instance. + create_decision_generator_func (Callable): Function to be used by each spawned roll-out worker to create a + decision generator for interacting with the environment. The function should take no parameters and return + an ``AbsDecisionGenerator`` instance. + create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an + environment wrapper for evaluation. The function should take no parameters and return an environment + wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the + worker processes. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to - None, in which case the number is set to ``num_workers``. - receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the workers. This - This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the - desired amount of data from workers. Defaults to None, in which case each receive attempt is blocking. - max_staleness (int): Maximum allowable staleness measured in the number of calls to ``collect``. Experiences - collected from calls to ``collect`` within ``max_staleness`` calls ago will be returned to the learner. - Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. num_eval_workers (int): Number of workers for evaluation. Defaults to 1. log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of each episode. Defaults to True. @@ -357,7 +359,8 @@ def evaluate(self, ep: int, policy_state_dict: dict): Returns: Environment summary. """ - for conn in self._manager_ends: + eval_worker_conns = choices(self._manager_ends, k=self._num_eval_workers) + for conn in eval_worker_conns: conn.send({"type": "evaluate", "episode": ep, "policy": policy_state_dict}) env_summary_dict = {} @@ -374,12 +377,12 @@ def exit(self): class MultiNodeRolloutManager(AbsRolloutManager): - """Controller for a set of remote roll-out workers. + """Controller for a set of remote roll-out workers, possibly distributed on different computation nodes. Args: num_workers (int): Number of remote roll-out workers. - group (str): Group name for the roll-out manager and the set of roll-out workers managed by it. The roll-out - workers' processes must be assigned this group name in order to form a communicating cluster. + group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager + that manages them. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to diff --git a/maro/rl/training/rollout_worker.py b/maro/rl/training/rollout_worker.py index 5f58fd1a1..d5f712234 100644 --- a/maro/rl/training/rollout_worker.py +++ b/maro/rl/training/rollout_worker.py @@ -21,6 +21,20 @@ def rollout_worker_process( create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper], log_dir: str ): + """Roll-out worker process that can be spawned by a ``MultiProcessRolloutManager``. + + Args: + index (int): Index for the worker process. This is used for bookkeeping by the parent manager process. + conn (Connection): Connection end for exchanging messages with the manager process. + create_env_wrapper_func (Callable): Function to create an environment wrapper for training data collection. + The function should take no parameters and return an environment wrapper instance. + create_decision_generator_func (Callable): Function to create a decision generator for interacting with + the environment. The function should take no parameters and return an ``AbsDecisionGenerator`` instance. + create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function + should take no parameters and return an environment wrapper instance. If this is None, the training + environment wrapper will be used for evaluation. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ set_seeds(index) env_wrapper = create_env_wrapper_func() eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() @@ -102,22 +116,19 @@ def rollout_worker_node( log_dir: str = getcwd(), **proxy_kwargs ): - """Roll-out worker process. + """Roll-out worker process that can be launched on separate computation nodes. Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences - for policy training / update. - decision_generator (AbsDecisionGenerator): Source of action decisions which could be local or remote - depending on the implementation. - group (str): Group name for all roll-out workers and the roll-out manager that manages them. The roll-out - manager process must be assigned this group name in order to form a communicating cluster. - exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. - agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct - an agent's query to the correct exploration scheme. Defaults to None. - eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used - as the evaluation environment. Defaults to None. - log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory - will be used to save the log files generated by it. Defaults to the current working directory. + create_env_wrapper_func (Callable): Function to create an environment wrapper for roll-out. The function + should take no parameters and return an environment wrapper instance. + create_decision_generator_func (Callable): Function to create a decision generator for interacting with + the environment. The function should take no parameters and return an ``AbsDecisionGenerator`` instance. + group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager + that manages them. + create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function + should take no parameters and return an environment wrapper instance. If this is None, the training + environment wrapper will be used for evaluation. + log_dir (str): Directory to store logs in. Defaults to the current working directory. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. """ diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index 22c588e94..3eaa60b32 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -13,6 +13,16 @@ def trainer_process(trainer_id: str, conn: Connection, create_policy_func_dict: Dict[str, Callable], log_dir: str): + """Policy trainer process which can be spawned by a ``MultiProcessTrainingManager``. + + Args: + trainer_id (str): Identifier for the trainer process for bookkeeping by the parent manager process. + conn (Connection): Connection end for exchanging messages with the manager process. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ policy_dict = {policy_name: func(policy_name) for policy_name, func in create_policy_func_dict.items()} logger = Logger("TRAINER", dump_folder=log_dir) while True: @@ -39,6 +49,19 @@ def trainer_node( log_dir: str = getcwd(), **proxy_kwargs ): + """Policy trainer process that can be launched on separate computation nodes. + + Args: + trainer_id (str): Identifier for the trainer process for bookkeeping by the parent manager process. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. + group (str): Group name for the training cluster, which includes all trainers and a training manager that + manages them. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. + """ policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} proxy = Proxy(group, "trainer", {"training_manager": 1}, component_name=trainer_id, **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/training_manager.py index 1b79a2dc4..d23606089 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/training_manager.py @@ -86,6 +86,17 @@ def get_state(self): class MultiProcessTrainingManager(AbsTrainingManager): + """Training manager that spawns a set of trainer processes for parallel training. + + Args: + policy2trainer (dict): Mapping from policy names to trainer IDs. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ def __init__( self, policy2trainer: Dict[str, str], @@ -155,6 +166,21 @@ def exit(self): class MultiNodeTrainingManager(AbsTrainingManager): + """Training manager that spawns a set of trainer processes for parallel training. + + Args: + policy2trainer (dict): Mapping from policy names to trainer IDs. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. + group (str): Group name for the training cluster, which includes all trainers and a training manager that + manages them. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. + """ def __init__( self, policy2trainer: Dict[str, str], From 5089f7cddffaf6142e1012e951045a8cf8023ea9 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 18 Jun 2021 05:58:43 +0000 Subject: [PATCH 306/482] lint issue fix --- maro/rl/training/rollout_manager.py | 3 +-- maro/rl/training/rollout_worker.py | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index 786fad308..cc55ecec4 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from pickle import FALSE import time from abc import ABC, abstractmethod from collections import defaultdict @@ -500,7 +499,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): num_finishes += 1 if num_finishes == self.num_workers: break - + if self.episode_complete: self._exploration_step = True diff --git a/maro/rl/training/rollout_worker.py b/maro/rl/training/rollout_worker.py index d5f712234..a617dc0da 100644 --- a/maro/rl/training/rollout_worker.py +++ b/maro/rl/training/rollout_worker.py @@ -24,7 +24,7 @@ def rollout_worker_process( """Roll-out worker process that can be spawned by a ``MultiProcessRolloutManager``. Args: - index (int): Index for the worker process. This is used for bookkeeping by the parent manager process. + index (int): Index for the worker process. This is used for bookkeeping by the parent manager process. conn (Connection): Connection end for exchanging messages with the manager process. create_env_wrapper_func (Callable): Function to create an environment wrapper for training data collection. The function should take no parameters and return an environment wrapper instance. @@ -37,7 +37,7 @@ def rollout_worker_process( """ set_seeds(index) env_wrapper = create_env_wrapper_func() - eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() + eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() decision_generator = create_decision_generator_func() logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) @@ -84,7 +84,7 @@ def collect(msg): conn.send(return_info) def evaluate(msg): - logger.info(f"Evaluating...") + logger.info("Evaluating...") eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state decision_generator.exploit() @@ -133,7 +133,7 @@ def rollout_worker_node( for details. """ env_wrapper = create_env_wrapper_func() - eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() + eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() decision_generator = create_decision_generator_func() proxy = Proxy(group, "rollout_worker", {"rollout_manager": 1}, **proxy_kwargs) @@ -182,7 +182,7 @@ def collect(msg): proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) def evaluate(msg): - logger.info(f"Evaluating...") + logger.info("Evaluating...") ep = msg.body[MsgKey.EPISODE] eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state @@ -214,4 +214,4 @@ def evaluate(msg): if msg.tag == MsgTag.COLLECT: collect(msg) elif msg.tag == MsgTag.EVAL: - evaluate(msg) \ No newline at end of file + evaluate(msg) From 41a7b2743228f5551d15b517a9e1572301affb34 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 18 Jun 2021 06:13:01 +0000 Subject: [PATCH 307/482] lint issue fix --- maro/rl/__init__.py | 2 +- maro/rl/training/__init__.py | 2 +- maro/rl/training/decision_generator.py | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 5cb443ef8..9c52c5231 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -40,7 +40,7 @@ "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", "AbsDecisionGenerator", "AbsEarlyStopper", "AbsRolloutManager", "AbsTrainingManager", "Learner", - "LocalDecisionGenerator","LocalLearner", "LocalRolloutManager", "LocalTrainingManager", "MultiNodeRolloutManager", + "LocalDecisionGenerator", "LocalLearner", "LocalRolloutManager", "LocalTrainingManager", "MultiNodeRolloutManager", "MultiNodeTrainingManager", "MultiProcessRolloutManager", "MultiProcessTrainingManager", "rollout_worker_node", "rollout_worker_process", "trainer_node", "trainer_process", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 52cdf1669..d51dcaf94 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -6,7 +6,7 @@ from .learner import Learner from .local_learner import LocalLearner from .rollout_manager import ( - AbsRolloutManager, LocalRolloutManager, MultiProcessRolloutManager, MultiNodeRolloutManager + AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager ) from .rollout_worker import rollout_worker_node, rollout_worker_process from .trainer import trainer_node, trainer_process diff --git a/maro/rl/training/decision_generator.py b/maro/rl/training/decision_generator.py index 18d517af4..9711b9aee 100644 --- a/maro/rl/training/decision_generator.py +++ b/maro/rl/training/decision_generator.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod - from os import getcwd from typing import Dict, List @@ -25,12 +24,12 @@ def __init__( self.exploration_by_agent = { agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() } - self.exploring = True # Flag indicating that exploration is turned on. + self.exploring = True # Flag indicating that exploration is turned on. @abstractmethod def choose_action(self, state: dict, ep: int, step: int) -> dict: """Generate an action based on the given state. - + Args: state (dict): Dicitionary of agents' states based on which action decisions will be made. ep (int): Current episode. @@ -75,7 +74,7 @@ def __init__( def choose_action(self, state: dict, ep: int, step: int) -> dict: """Generate an action based on the given state. - + Args: state (dict): Dicitionary of agents' states based on which action decisions will be made. ep (int): Current episode. From 35cf25a68f109430ef88ced0e24716aac1735886 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 18 Jun 2021 06:14:59 +0000 Subject: [PATCH 308/482] fixed import formatting --- maro/rl/training/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index d51dcaf94..5c4a37ec9 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -5,9 +5,7 @@ from .early_stopper import AbsEarlyStopper from .learner import Learner from .local_learner import LocalLearner -from .rollout_manager import ( - AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager -) +from .rollout_manager import AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager from .rollout_worker import rollout_worker_node, rollout_worker_process from .trainer import trainer_node, trainer_process from .training_manager import ( From ceadf4f270426c2c39947133b9eeffd3a12cddd1 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 18 Jun 2021 17:24:47 +0800 Subject: [PATCH 309/482] [Feature] Prioritized Experience Replay (#355) * added prioritized experience replay * deleted unwanted supply_chain test files * fixed import order * import fix * fixed lint issues * fixed import formatting * added note in docstring that rank-based PER has yet to be implemented Co-authored-by: ysqyang --- examples/cim/dqn/config.yml | 5 +- maro/rl/__init__.py | 4 +- maro/rl/algorithms/dqn.py | 58 +++++------ maro/rl/experience/__init__.py | 4 +- maro/rl/experience/experience_manager.py | 4 +- maro/rl/experience/sampler.py | 119 +++++++++++++++++++++++ 6 files changed, 155 insertions(+), 39 deletions(-) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 81dfdd4b3..f4ca5b472 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -65,7 +65,6 @@ policies: train_epochs: 10 soft_update_coefficient: 0.1 double: false - loss_cls: smooth_l1 experience_manager: rollout: # for experience managers in actor processes capacity: 1000 @@ -76,7 +75,9 @@ policies: capacity: 100000 overwrite_type: "rolling" batch_size: 128 - replace: true + alpha: 0.6 + beta: 0.4 + beta_step: 0.001 update_trigger: 16 warmup: 1 distributed: diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 9c52c5231..7f8a8c4ba 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -6,7 +6,7 @@ get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.experience import AbsSampler, ExperienceManager, ExperienceSet +from maro.rl.experience import AbsSampler, ExperienceManager, ExperienceSet, PrioritizedSampler from maro.rl.exploration import ( AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, @@ -32,7 +32,7 @@ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", "AbsEnvWrapper", - "AbsSampler", "ExperienceManager", "ExperienceSet", + "AbsSampler", "ExperienceManager", "ExperienceSet", "PrioritizedSampler", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", "UniformNoiseExploration", diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index 48701638b..257323d38 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -6,10 +6,9 @@ import numpy as np import torch -from maro.rl.experience import ExperienceManager +from maro.rl.experience import ExperienceManager, PrioritizedSampler from maro.rl.model import DiscreteQNet from maro.rl.policy import AbsCorePolicy -from maro.rl.utils import get_torch_loss_cls class DQNConfig: @@ -19,19 +18,16 @@ class DQNConfig: reward_discount (float): Reward decay as defined in standard RL terminology. target_update_freq (int): Number of training rounds between target model updates. train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. - gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. soft_update_coefficient (float): Soft update coefficient, e.g., target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. Defaults to 1.0. double (bool): If True, the next Q values will be computed according to the double DQN algorithm, i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. - loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class. If it is a string, - it must be a key in ``TORCH_LOSS``. Defaults to "mse". """ __slots__ = [ "reward_discount", "target_update_freq", "train_epochs", "gradient_iters", "soft_update_coefficient", - "double", "loss_func" + "double", ] def __init__( @@ -39,18 +35,14 @@ def __init__( reward_discount: float, target_update_freq: int, train_epochs: int = 1, - gradient_iters: int = 1, soft_update_coefficient: float = 0.1, - double: bool = True, - loss_cls="mse" + double: bool = True ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq self.train_epochs = train_epochs - self.gradient_iters = gradient_iters self.soft_update_coefficient = soft_update_coefficient self.double = double - self.loss_func = get_torch_loss_cls(loss_cls)() class DQN(AbsCorePolicy): @@ -90,6 +82,7 @@ def __init__( self.config = config self.device = self.q_net.device self._training_counter = 0 + self.prioritized_experience_replay = isinstance(self.experience_manager.sampler, PrioritizedSampler) def choose_action(self, states) -> Union[int, np.ndarray]: with torch.no_grad(): @@ -108,30 +101,31 @@ def update(self): states, next_states = experience_set.states, experience_set.next_states actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) - if self.config.double: - for _ in range(self.config.gradient_iters): - # get target Q values - with torch.no_grad(): - actions_by_eval_q_net = self.q_net.get_action(next_states)[0] - next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) - target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) - - # gradient steps - q_values = self.q_net.q_values(states, actions) - loss = self.config.loss_func(q_values, target_q_values) - self.q_net.step(loss.mean()) - else: - # get target Q values - with torch.no_grad(): + if self.prioritized_experience_replay: + indexes = [info["index"] for info in experience_set.info] + is_weights = torch.tensor([info["is_weight"] for info in experience_set.info]).to(self.device) + + # get target Q values + with torch.no_grad(): + if self.config.double: + actions_by_eval_q_net = self.q_net.get_action(next_states)[0] + next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) + else: next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) - target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) - # gradient steps - for _ in range(self.config.gradient_iters): - q_values = self.q_net.q_values(states, actions) - loss = self.config.loss_func(q_values, target_q_values) - self.q_net.step(loss.mean()) + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + # gradient step + q_values = self.q_net.q_values(states, actions) + if self.prioritized_experience_replay: + td_errors = target_q_values - q_values + loss = (td_errors * is_weights).mean() + self.experience_manager.sampler.update(indexes, td_errors.detach().cpu().numpy()) + else: + loss = torch.nn.MSELoss(q_values, target_q_values) + self.q_net.step(loss) + # soft-update target network self._training_counter += 1 if self._training_counter % self.config.target_update_freq == 0: self.target_q_net.soft_update(self.q_net, self.config.soft_update_coefficient) diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 1f40ec329..da9097898 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -2,6 +2,6 @@ # Licensed under the MIT license. from .experience_manager import ExperienceManager, ExperienceSet -from .sampler import AbsSampler +from .sampler import AbsSampler, PrioritizedSampler -__all__ = ["AbsSampler", "ExperienceManager", "ExperienceSet"] +__all__ = ["AbsSampler", "ExperienceManager", "ExperienceSet", "PrioritizedSampler"] diff --git a/maro/rl/experience/experience_manager.py b/maro/rl/experience/experience_manager.py index d9d2913bb..b17392d06 100644 --- a/maro/rl/experience/experience_manager.py +++ b/maro/rl/experience/experience_manager.py @@ -74,7 +74,7 @@ def __init__( batch_size: int = 32, replace: bool = True, sampler_cls=None, - sampler_params: dict = None, + **sampler_params ): super().__init__() if overwrite_type not in {"rolling", "random"}: @@ -141,6 +141,8 @@ def put(self, experience_set: ExperienceSet): self.data[key][idx] = val self._size = min(self._capacity, num_experiences) + if self.sampler: + self.sampler.on_put(experience_set, indexes) def get(self): """Retrieve an experience set from the memory. diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index 291bf2d52..1a2fb510d 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -2,15 +2,134 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod +from typing import List + +import numpy as np from .experience_manager import ExperienceManager, ExperienceSet class AbsSampler(ABC): + """Sampler class. + + Args: + experience_manager (ExperienceManager): experience manager the sampler is associated with. + """ def __init__(self, experience_manager: ExperienceManager): super().__init__() self.experience_manager = experience_manager @abstractmethod def get(self) -> ExperienceSet: + """Sampling logic is defined here.""" raise NotImplementedError + + def on_put(self, experience_set: ExperienceSet, indexes: List[int]): + """Callback to be executed after calling experience_manager.put().""" + pass + + +class PrioritizedSampler(AbsSampler): + """Sampler for Prioritized Experience Replay (PER). + + References: + https://arxiv.org/pdf/1511.05952.pdf + https://github.com/rlcode/per + + The implementation here is based on direct proportional prioritization (the first variant in the paper). + The rank-based variant is not implemented here. + + Args: + experience_manager (ExperienceManager): experience manager the sampler is associated with. + batch_size (int): mini-batch size. Defaults to 32. + alpha (float): Prioritization strength. Sampling probabilities are calculated according to + P = p_i^alpha / sum(p_k^alpha). Defaults to 0.6. + beta (float): Bias annealing strength using weighted importance sampling (IS) techniques. + IS weights are calculated according to (N * P)^(-beta), where P is the sampling probability. + This value of ``beta`` should not exceed 1.0, which corresponds to full annealing. Defaults to 0.4. + beta_step (float): The amount ``beta`` is incremented by after each get() call until it reaches 1.0. + Defaults to 0.001. + """ + def __init__( + self, + experience_manager: ExperienceManager, + batch_size: int = 32, + alpha: float = 0.6, + beta: float = 0.4, + beta_step: float = 0.001 + ): + if beta > 1.0: + raise ValueError("beta should be between 0.0 and 1.0") + super().__init__(experience_manager) + self._sum_tree = np.zeros(2 * self.experience_manager.capacity - 1) + self.batch_size = batch_size + self.alpha = alpha + self.beta = beta + self.beta_step = beta_step + self.eps = 1e-7 + self._max_priority = 1e8 + + def total(self): + """Return the sum of priorities over all experiences.""" + return self._sum_tree[0] + + def on_put(self, experience_set: ExperienceSet, indexes: List[int]): + """Set the priorities of newly added experiences to the maximum value.""" + self.update(indexes, [self._max_priority] * len(indexes)) + + def update(self, indexes, td_errors): + """Update priority values at given indexes.""" + for idx, err in zip(indexes, td_errors): + priority = self._get_priority(err) + tree_idx = idx + self.experience_manager.capacity - 1 + delta = priority - self._sum_tree[tree_idx] + self._sum_tree[tree_idx] = priority + self._update(tree_idx, delta) + + def get(self): + """Priority-based sampling.""" + indexes, priorities = [], [] + segment_len = self.total() / self.batch_size + for i in range(self.batch_size): + low, high = segment_len * i, segment_len * (i + 1) + sampled_val = np.random.uniform(low=low, high=high) + idx = self._get(0, sampled_val) + data_idx = idx - self.experience_manager.capacity + 1 + indexes.append(data_idx) + priorities.append(self._sum_tree[idx]) + + self.beta = min(1., self.beta + self.beta_step) + sampling_probabilities = priorities / self.total() + is_weights = np.power(self.experience_manager.size * sampling_probabilities, -self.beta) + is_weights /= is_weights.max() + + return ExperienceSet( + states=[self.experience_manager.data["states"][idx] for idx in indexes], + actions=[self.experience_manager.data["actions"][idx] for idx in indexes], + rewards=[self.experience_manager.data["rewards"][idx] for idx in indexes], + next_states=[self.experience_manager.data["next_states"][idx] for idx in indexes], + info=[{"index": idx, "is_weight": wt} for idx, wt in zip(indexes, is_weights)] + ) + + def _get_priority(self, error): + return (np.abs(error) + self.eps) ** self.alpha + + def _update(self, idx, delta): + """Propagate priority change all the way to the root node.""" + parent = (idx - 1) // 2 + self._sum_tree[parent] += delta + if parent != 0: + self._update(parent, delta) + + def _get(self, idx, sampled_val): + """Get a leaf node according to a randomly sampled value.""" + left = 2 * idx + 1 + right = left + 1 + + if left >= len(self._sum_tree): + return idx + + if sampled_val <= self._sum_tree[left]: + return self._get(left, sampled_val) + else: + return self._get(right, sampled_val - self._sum_tree[left]) From 248d1e47a71ba50a61d29dc329676b92996c22df Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 18 Jun 2021 11:18:34 +0000 Subject: [PATCH 310/482] rm AbsDecisionGenerator --- examples/cim/dqn/rollout_manager.py | 6 +-- maro/rl/__init__.py | 12 ++--- maro/rl/training/__init__.py | 4 +- maro/rl/training/decision_generator.py | 65 +++++++++----------------- maro/rl/training/rollout_manager.py | 5 +- maro/rl/training/rollout_worker.py | 10 ++-- 6 files changed, 41 insertions(+), 61 deletions(-) diff --git a/examples/cim/dqn/rollout_manager.py b/examples/cim/dqn/rollout_manager.py index 596aaf46c..694448b2f 100644 --- a/examples/cim/dqn/rollout_manager.py +++ b/examples/cim/dqn/rollout_manager.py @@ -5,8 +5,8 @@ import sys from maro.rl import ( - EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, LocalDecisionGenerator, - LocalRolloutManager, MultiProcessRolloutManager + DecisionGenerator, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, LocalRolloutManager, + MultiProcessRolloutManager ) from maro.simulator import Env @@ -30,7 +30,7 @@ def get_decision_generator(): last_ep=config["num_episodes"], **config["exploration"] ) - return LocalDecisionGenerator( + return DecisionGenerator( agent2policy={i: i for i in AGENT_IDS}, policies=[get_independent_policy_for_rollout(i) for i in AGENT_IDS], exploration_dict={f"EpsilonGreedy": epsilon_greedy}, diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 9c52c5231..40987ba16 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -18,8 +18,8 @@ ) from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( - AbsDecisionGenerator, AbsEarlyStopper, AbsRolloutManager, AbsTrainingManager, Learner, LocalDecisionGenerator, - LocalLearner, LocalRolloutManager, LocalTrainingManager, MultiNodeRolloutManager, MultiNodeTrainingManager, + AbsEarlyStopper, AbsRolloutManager, AbsTrainingManager, DecisionGenerator, Learner, LocalLearner, + LocalRolloutManager, LocalTrainingManager, MultiNodeRolloutManager, MultiNodeTrainingManager, MultiProcessRolloutManager, MultiProcessTrainingManager, rollout_worker_node, rollout_worker_process, trainer_node, trainer_process ) @@ -39,10 +39,10 @@ "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", - "AbsDecisionGenerator", "AbsEarlyStopper", "AbsRolloutManager", "AbsTrainingManager", "Learner", - "LocalDecisionGenerator", "LocalLearner", "LocalRolloutManager", "LocalTrainingManager", "MultiNodeRolloutManager", - "MultiNodeTrainingManager", "MultiProcessRolloutManager", "MultiProcessTrainingManager", "rollout_worker_node", - "rollout_worker_process", "trainer_node", "trainer_process", + "AbsEarlyStopper", "AbsRolloutManager", "AbsTrainingManager", "DecisionGenerator", "Learner", "LocalLearner", + "LocalRolloutManager", "LocalTrainingManager", "MultiNodeRolloutManager", "MultiNodeTrainingManager", + "MultiProcessRolloutManager", "MultiProcessTrainingManager", "rollout_worker_node", "rollout_worker_process", + "trainer_node", "trainer_process", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 5c4a37ec9..821f28d8b 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .decision_generator import AbsDecisionGenerator, LocalDecisionGenerator +from .decision_generator import DecisionGenerator from .early_stopper import AbsEarlyStopper from .learner import Learner from .local_learner import LocalLearner @@ -13,7 +13,7 @@ ) __all__ = [ - "AbsDecisionGenerator", "LocalDecisionGenerator", + "DecisionGenerator", "AbsEarlyStopper", "Learner", "LocalLearner", diff --git a/maro/rl/training/decision_generator.py b/maro/rl/training/decision_generator.py index 9711b9aee..883f20148 100644 --- a/maro/rl/training/decision_generator.py +++ b/maro/rl/training/decision_generator.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from abc import ABC, abstractmethod from os import getcwd from typing import Dict, List @@ -10,52 +9,16 @@ from maro.utils import Logger -class AbsDecisionGenerator(ABC): - def __init__( - self, - agent2policy: Dict[str, str], - exploration_dict: Dict[str, AbsExploration] = None, - agent2exploration: Dict[str, str] = None - ): - super().__init__() - self.agent2policy = agent2policy - self.exploration_dict = exploration_dict - if self.exploration_dict: - self.exploration_by_agent = { - agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() - } - self.exploring = True # Flag indicating that exploration is turned on. - - @abstractmethod - def choose_action(self, state: dict, ep: int, step: int) -> dict: - """Generate an action based on the given state. - - Args: - state (dict): Dicitionary of agents' states based on which action decisions will be made. - ep (int): Current episode. - step (int): Current step. - """ - raise NotImplementedError - - def exploration_step(self): - for exploration in self.exploration_dict.values(): - exploration.step() - print(f"epsilon: {exploration.epsilon}") - - def exploit(self): - self.exploring = False - - def explore(self): - self.exploring = True - - -class LocalDecisionGenerator(AbsDecisionGenerator): - """Local decision generator. +class DecisionGenerator: + """Multi-agent multi-policy decision generator with exploration. Args: agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. policies (List[AbsPolicy]): A list of policies for inference. + exploration_dict (Dict[str, AbsExploration]): A dictionary of named ``AbsExploration`` instances. Defaults + to None. + agent2exploration (Dict[str, str]): Mapping from agent names to exploration instance names. Defaults to None. log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ @@ -70,6 +33,13 @@ def __init__( super().__init__(agent2policy, exploration_dict=exploration_dict, agent2exploration=agent2exploration) self.policy_dict = {policy.name: policy for policy in policies} self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self.agent2policy = agent2policy + self.exploration_dict = exploration_dict + if self.exploration_dict: + self.exploration_by_agent = { + agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() + } + self.exploring = True # Flag indicating that exploration is turned on. self._logger = Logger("local_decision_generator", dump_folder=log_dir) def choose_action(self, state: dict, ep: int, step: int) -> dict: @@ -107,3 +77,14 @@ def update(self, policy_state_dict: dict): if policy_state_dict: self._logger.info(f"updated policies {list(policy_state_dict.keys())}") + + def exploration_step(self): + for exploration in self.exploration_dict.values(): + exploration.step() + print(f"epsilon: {exploration.epsilon}") + + def exploit(self): + self.exploring = False + + def explore(self): + self.exploring = True diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index cc55ecec4..badcceecf 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -16,7 +16,6 @@ from maro.rl.policy import AbsPolicy from maro.utils import Logger -from .decision_generator import AbsDecisionGenerator from .message_enums import MsgKey, MsgTag from .rollout_worker import rollout_worker_process @@ -247,7 +246,7 @@ class MultiProcessRolloutManager(AbsRolloutManager): environment wrapper instance. create_decision_generator_func (Callable): Function to be used by each spawned roll-out worker to create a decision generator for interacting with the environment. The function should take no parameters and return - an ``AbsDecisionGenerator`` instance. + a ``DecisionGenerator`` instance. create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an environment wrapper for evaluation. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the @@ -265,7 +264,7 @@ def __init__( self, num_workers: int, create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_decision_generator_func: Callable[[], AbsDecisionGenerator], + create_decision_generator_func: Callable[[], DecisionGenerator], create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, num_steps: int = -1, num_eval_workers: int = 1, diff --git a/maro/rl/training/rollout_worker.py b/maro/rl/training/rollout_worker.py index a617dc0da..87e4739de 100644 --- a/maro/rl/training/rollout_worker.py +++ b/maro/rl/training/rollout_worker.py @@ -9,7 +9,7 @@ from maro.rl.env_wrapper import AbsEnvWrapper from maro.utils import Logger, set_seeds -from .decision_generator import AbsDecisionGenerator +from .decision_generator import DecisionGenerator from .message_enums import MsgKey, MsgTag @@ -17,7 +17,7 @@ def rollout_worker_process( index: int, conn: Connection, create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_decision_generator_func: Callable[[], AbsDecisionGenerator], + create_decision_generator_func: Callable[[], DecisionGenerator], create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper], log_dir: str ): @@ -29,7 +29,7 @@ def rollout_worker_process( create_env_wrapper_func (Callable): Function to create an environment wrapper for training data collection. The function should take no parameters and return an environment wrapper instance. create_decision_generator_func (Callable): Function to create a decision generator for interacting with - the environment. The function should take no parameters and return an ``AbsDecisionGenerator`` instance. + the environment. The function should take no parameters and return a ``DecisionGenerator`` instance. create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation. @@ -110,7 +110,7 @@ def evaluate(msg): def rollout_worker_node( create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_decision_generator_func: Callable[[], AbsDecisionGenerator], + create_decision_generator_func: Callable[[], DecisionGenerator], group: str, create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, log_dir: str = getcwd(), @@ -122,7 +122,7 @@ def rollout_worker_node( create_env_wrapper_func (Callable): Function to create an environment wrapper for roll-out. The function should take no parameters and return an environment wrapper instance. create_decision_generator_func (Callable): Function to create a decision generator for interacting with - the environment. The function should take no parameters and return an ``AbsDecisionGenerator`` instance. + the environment. The function should take no parameters and return a ``DecisionGenerator`` instance. group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager that manages them. create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function From 85e304a4b3cf5d9da2de691785acff25667a9015 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 18 Jun 2021 11:29:23 +0000 Subject: [PATCH 311/482] small fixes --- maro/rl/training/decision_generator.py | 2 +- maro/rl/training/rollout_manager.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/maro/rl/training/decision_generator.py b/maro/rl/training/decision_generator.py index 883f20148..d0924e4bc 100644 --- a/maro/rl/training/decision_generator.py +++ b/maro/rl/training/decision_generator.py @@ -18,7 +18,7 @@ class DecisionGenerator: policies (List[AbsPolicy]): A list of policies for inference. exploration_dict (Dict[str, AbsExploration]): A dictionary of named ``AbsExploration`` instances. Defaults to None. - agent2exploration (Dict[str, str]): Mapping from agent names to exploration instance names. Defaults to None. + agent2exploration (Dict[str, str]): Mapping from agent names to exploration instance names. Defaults to None. log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/rollout_manager.py index badcceecf..95c39c48f 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/rollout_manager.py @@ -16,6 +16,7 @@ from maro.rl.policy import AbsPolicy from maro.utils import Logger +from .decision_generator import DecisionGenerator from .message_enums import MsgKey, MsgTag from .rollout_worker import rollout_worker_process From 2601970b636b7480b8c86bdeedaa208ef6e4692b Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 18 Jun 2021 11:32:36 +0000 Subject: [PATCH 312/482] bug fix --- maro/rl/training/decision_generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/maro/rl/training/decision_generator.py b/maro/rl/training/decision_generator.py index d0924e4bc..0c9047aa9 100644 --- a/maro/rl/training/decision_generator.py +++ b/maro/rl/training/decision_generator.py @@ -30,7 +30,6 @@ def __init__( agent2exploration: Dict[str, str] = None, log_dir: str = getcwd() ): - super().__init__(agent2policy, exploration_dict=exploration_dict, agent2exploration=agent2exploration) self.policy_dict = {policy.name: policy for policy in policies} self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} self.agent2policy = agent2policy From f72e8841591dfa9d19e413e46bda2fd162e42a69 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 20 Jun 2021 09:08:31 +0000 Subject: [PATCH 313/482] reorganized training folder structure --- docs/source/apidoc/maro.rl.rst | 8 +- docs/source/images/rl/learner.svg | 2 +- docs/source/key_components/rl_toolkit.rst | 6 +- examples/cim/ac/main.py | 6 +- examples/cim/dqn/config.yml | 2 +- examples/cim/dqn/main.py | 5 +- examples/cim/dqn/policy.py | 3 +- ...{training_manager.py => policy_manager.py} | 6 +- examples/cim/dqn/rollout_manager.py | 14 ++-- maro/rl/__init__.py | 20 ++--- maro/rl/algorithms/dqn.py | 4 +- maro/rl/policy/policy.py | 3 + maro/rl/training/__init__.py | 28 +++---- maro/rl/training/policy_manager/__init__.py | 10 +++ .../policy_manager.py} | 27 +++--- .../training/{ => policy_manager}/trainer.py | 9 +- maro/rl/training/sync/__init__.py | 18 ++++ maro/rl/training/{ => sync}/early_stopper.py | 0 maro/rl/training/{ => sync}/learner.py | 20 ++--- maro/rl/training/{ => sync}/local_learner.py | 84 ++++--------------- .../rl/training/{ => sync}/rollout_manager.py | 23 ++--- maro/rl/training/{ => sync}/rollout_worker.py | 75 ++++++++--------- maro/rl/{env_wrapper => wrappers}/__init__.py | 3 +- .../agent_wrapper.py} | 31 +++---- .../{env_wrapper => wrappers}/env_wrapper.py | 0 25 files changed, 191 insertions(+), 216 deletions(-) rename examples/cim/dqn/{training_manager.py => policy_manager.py} (82%) create mode 100644 maro/rl/training/policy_manager/__init__.py rename maro/rl/training/{training_manager.py => policy_manager/policy_manager.py} (91%) rename maro/rl/training/{ => policy_manager}/trainer.py (89%) create mode 100644 maro/rl/training/sync/__init__.py rename maro/rl/training/{ => sync}/early_stopper.py (100%) rename maro/rl/training/{ => sync}/learner.py (89%) rename maro/rl/training/{ => sync}/local_learner.py (59%) rename maro/rl/training/{ => sync}/rollout_manager.py (97%) rename maro/rl/training/{ => sync}/rollout_worker.py (74%) rename maro/rl/{env_wrapper => wrappers}/__init__.py (57%) rename maro/rl/{training/decision_generator.py => wrappers/agent_wrapper.py} (80%) rename maro/rl/{env_wrapper => wrappers}/env_wrapper.py (100%) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index 0e5babc17..e93061ffe 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -145,10 +145,10 @@ maro.rl.policy.policy Training ================================================================================ -maro.rl.training.decision_generator +maro.rl.training.agent_wrapper -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.decision_generator +.. automodule:: maro.rl.training.agent_wrapper :members: :undoc-members: :show-inheritance: @@ -201,10 +201,10 @@ maro.rl.training.rollout_worker :undoc-members: :show-inheritance: -maro.rl.training.training_manager +maro.rl.training.policy_manager -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.training_manager +.. automodule:: maro.rl.training.policy_manager :members: :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/source/images/rl/learner.svg b/docs/source/images/rl/learner.svg index b3b647ae2..9c8affe16 100644 --- a/docs/source/images/rl/learner.svg +++ b/docs/source/images/rl/learner.svg @@ -1,3 +1,3 @@ -
Learner
Learner
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
experiences
expe...
Training
Manager
Training...
updated policies
updated pol...
Learner
Lear...
Viewer does not support full SVG 1.1
\ No newline at end of file +
Learner
Learner
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
experiences
expe...
Training
Manager
Training...
updated policies
updated pol...
Learner
Lear...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index f00c764b6..5d3188405 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -8,7 +8,7 @@ components. At the top level of a training workflow are: process. The learner process executes training cycles that alternate between data collection and policy updates. * Rollout manager, which is responsible for collecting simulation data, in local or distributed fashion. -* Training manager, which manages a set of policies and controls their updates. The policy instances may +* Policy manager, which manages a set of policies and controls their updates. The policy instances may reside with the manager or be distributed amongst a set of processes or remote nodes for parallelized training. @@ -22,8 +22,8 @@ components. At the top level of a training workflow are: :alt: Overview -.. image:: ../images/rl/training_manager.svg - :target: ../images/rl/training_manager.svg +.. image:: ../images/rl/policy_manager.svg + :target: ../images/rl/policy_manager.svg :alt: RL Overview diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py index 68c777ed1..6b3a6a348 100644 --- a/examples/cim/ac/main.py +++ b/examples/cim/ac/main.py @@ -9,7 +9,8 @@ import torch from maro.rl import ( - ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceManager, FullyConnectedBlock, LocalLearner, OptimOption + ActorCritic, ActorCriticConfig, AgentWrapper, DiscreteACNet, ExperienceManager, FullyConnectedBlock, LocalLearner, + OptimOption ) from maro.simulator import Env from maro.utils import set_seeds @@ -66,5 +67,6 @@ def forward(self, states, actor: bool = True, critic: bool = True): env_wrapper = CIMEnvWrapper(env, **config["env"]["wrapper"]) policies = [get_ac_policy(id_) for id_ in env.agent_idx_list] agent2policy = {agent_id: agent_id for agent_id in env.agent_idx_list} - learner = LocalLearner(env_wrapper, policies, agent2policy, 40) # 40 episodes + agent_wrapper = AgentWrapper(policies, agent2policy) + learner = LocalLearner(env_wrapper, agent_wrapper, 40) # 40 episodes learner.run() diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index f4ca5b472..90c4b0f8b 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -10,7 +10,7 @@ env: basic: scenario: cim topology: toy.4p_ssdd_l0.0 - durations: 280 + durations: 560 wrapper: port_attributes: - empty diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/main.py index 87315a5e3..bd64db2eb 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/main.py @@ -3,7 +3,6 @@ import os import sys -import time from maro.rl import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner from maro.simulator import Env @@ -16,7 +15,7 @@ from general import NUM_ACTIONS, config, log_dir from policy import get_independent_policy_for_training from rollout_manager import rollout_manager -from training_manager import training_manager +from policy_manager import policy_manager if __name__ == "__main__": @@ -42,7 +41,7 @@ local_learner.run() elif config["mode"] == "multi-process": learner = Learner( - training_manager=training_manager, + policy_manager=policy_manager, rollout_manager=rollout_manager, num_episodes=config["num_episodes"], # eval_schedule=config["eval_schedule"], diff --git a/examples/cim/dqn/policy.py b/examples/cim/dqn/policy.py index d28303ff7..fb14fcfe5 100644 --- a/examples/cim/dqn/policy.py +++ b/examples/cim/dqn/policy.py @@ -50,5 +50,6 @@ def get_independent_policy_for_rollout(name): name=name, q_net=qnet, experience_manager=ExperienceManager(**cfg["experience_manager"]["rollout"]), - config=DQNConfig(**cfg["algorithm_config"]) + config=DQNConfig(**cfg["algorithm_config"]), + update_trigger=1e8 # set to a large number to ensure that the roll-out workers don't update policies ) diff --git a/examples/cim/dqn/training_manager.py b/examples/cim/dqn/policy_manager.py similarity index 82% rename from examples/cim/dqn/training_manager.py rename to examples/cim/dqn/policy_manager.py index 01285a932..9bf7d8392 100644 --- a/examples/cim/dqn/training_manager.py +++ b/examples/cim/dqn/policy_manager.py @@ -4,7 +4,7 @@ import os import sys -from maro.rl import LocalTrainingManager, MultiProcessTrainingManager +from maro.rl import LocalPolicyManager, MultiProcessPolicyManager dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory cim_path = os.path.dirname(dqn_path) # CIM example directory @@ -15,11 +15,11 @@ if config["distributed"]["policy_training_mode"] == "local": - training_manager = LocalTrainingManager( + policy_manager = LocalPolicyManager( [get_independent_policy_for_training(i) for i in AGENT_IDS], log_dir=log_dir ) else: - training_manager = MultiProcessTrainingManager( + policy_manager = MultiProcessPolicyManager( {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping {i: get_independent_policy_for_training for i in AGENT_IDS}, log_dir=log_dir diff --git a/examples/cim/dqn/rollout_manager.py b/examples/cim/dqn/rollout_manager.py index 694448b2f..8d0ae27c8 100644 --- a/examples/cim/dqn/rollout_manager.py +++ b/examples/cim/dqn/rollout_manager.py @@ -5,7 +5,7 @@ import sys from maro.rl import ( - DecisionGenerator, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, LocalRolloutManager, + AgentWrapper, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, LocalRolloutManager, MultiProcessRolloutManager ) from maro.simulator import Env @@ -19,10 +19,10 @@ from policy import get_independent_policy_for_rollout -def get_env(): +def get_env_wrapper(): return CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]) -def get_decision_generator(): +def get_agent_wrapper(): epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, @@ -30,9 +30,9 @@ def get_decision_generator(): last_ep=config["num_episodes"], **config["exploration"] ) - return DecisionGenerator( - agent2policy={i: i for i in AGENT_IDS}, + return AgentWrapper( policies=[get_independent_policy_for_rollout(i) for i in AGENT_IDS], + agent2policy={i: i for i in AGENT_IDS}, exploration_dict={f"EpsilonGreedy": epsilon_greedy}, agent2exploration={i: "EpsilonGreedy" for i in AGENT_IDS}, log_dir=log_dir @@ -53,8 +53,8 @@ def get_decision_generator(): else: rollout_manager = MultiProcessRolloutManager( config["distributed"]["num_rollout_workers"], - get_env, - get_decision_generator, + get_env_wrapper, + get_agent_wrapper, num_steps=config["num_steps"], log_dir=log_dir, ) diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 0148c6b82..308aed9f8 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -5,7 +5,6 @@ DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) -from maro.rl.env_wrapper import AbsEnvWrapper from maro.rl.experience import AbsSampler, ExperienceManager, ExperienceSet, PrioritizedSampler from maro.rl.exploration import ( AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, @@ -18,20 +17,19 @@ ) from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy from maro.rl.training import ( - AbsEarlyStopper, AbsRolloutManager, AbsTrainingManager, DecisionGenerator, Learner, LocalLearner, - LocalRolloutManager, LocalTrainingManager, MultiNodeRolloutManager, MultiNodeTrainingManager, - MultiProcessRolloutManager, MultiProcessTrainingManager, rollout_worker_node, rollout_worker_process, trainer_node, - trainer_process + AbsEarlyStopper, AbsPolicyManager, AbsRolloutManager, Learner, LocalLearner, LocalPolicyManager, + LocalRolloutManager, MultiNodePolicyManager, MultiNodeRolloutManager, MultiProcessPolicyManager, + MultiProcessRolloutManager, rollout_worker_node, rollout_worker_process, trainer_node, trainer_process ) from maro.rl.utils import ( get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward ) +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper __all__ = [ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", - "AbsEnvWrapper", "AbsSampler", "ExperienceManager", "ExperienceSet", "PrioritizedSampler", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", @@ -39,10 +37,10 @@ "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "FullyConnectedBlock", "OptimOption", "AbsCorePolicy", "AbsPolicy", "NullPolicy", - "AbsEarlyStopper", "AbsRolloutManager", "AbsTrainingManager", "DecisionGenerator", "Learner", "LocalLearner", - "LocalRolloutManager", "LocalTrainingManager", "MultiNodeRolloutManager", "MultiNodeTrainingManager", - "MultiProcessRolloutManager", "MultiProcessTrainingManager", "rollout_worker_node", "rollout_worker_process", - "trainer_node", "trainer_process", + "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Learner", "LocalLearner", "LocalPolicyManager", + "LocalRolloutManager", "MultiNodePolicyManager", "MultiNodeRolloutManager", "MultiProcessPolicyManager", + "MultiProcessRolloutManager", "rollout_worker_node", "rollout_worker_process", "trainer_node", "trainer_process", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", - "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward" + "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward", + "AbsEnvWrapper", "AgentWrapper" ] diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index 257323d38..6259f91a7 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -83,6 +83,8 @@ def __init__( self.device = self.q_net.device self._training_counter = 0 self.prioritized_experience_replay = isinstance(self.experience_manager.sampler, PrioritizedSampler) + if not self.prioritized_experience_replay: + self._loss_func = torch.nn.MSELoss() def choose_action(self, states) -> Union[int, np.ndarray]: with torch.no_grad(): @@ -122,7 +124,7 @@ def update(self): loss = (td_errors * is_weights).mean() self.experience_manager.sampler.update(indexes, td_errors.detach().cpu().numpy()) else: - loss = torch.nn.MSELoss(q_values, target_q_values) + loss = self._loss_func(q_values, target_q_values) self.q_net.step(loss) # soft-update target network diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index c981b0105..3477dd9d5 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -91,6 +91,9 @@ def set_state(self, policy_state): pass def on_experiences(self, exp: ExperienceSet) -> bool: + """ + Store incoming experiences and update if necessary. + """ self.experience_manager.put(exp) self._new_exp_counter += exp.size print( diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 821f28d8b..fef1e8098 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -1,24 +1,18 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .decision_generator import DecisionGenerator -from .early_stopper import AbsEarlyStopper -from .learner import Learner -from .local_learner import LocalLearner -from .rollout_manager import AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager -from .rollout_worker import rollout_worker_node, rollout_worker_process -from .trainer import trainer_node, trainer_process -from .training_manager import ( - AbsTrainingManager, LocalTrainingManager, MultiNodeTrainingManager, MultiProcessTrainingManager +from .sync import ( + AbsEarlyStopper, AbsRolloutManager, Learner, LocalLearner, LocalRolloutManager, MultiNodeRolloutManager, + MultiProcessRolloutManager, rollout_worker_node, rollout_worker_process +) +from .policy_manager import ( + AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, trainer_node, + trainer_process ) __all__ = [ - "DecisionGenerator", - "AbsEarlyStopper", - "Learner", - "LocalLearner", - "AbsRolloutManager", "LocalRolloutManager", "MultiProcessRolloutManager", "MultiNodeRolloutManager", - "rollout_worker_node", "rollout_worker_process", - "trainer_node", "trainer_process", - "AbsTrainingManager", "LocalTrainingManager", "MultiNodeTrainingManager", "MultiProcessTrainingManager" + "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Learner", "LocalLearner", "LocalPolicyManager", + "LocalRolloutManager", "MultiNodePolicyManager", "MultiNodeRolloutManager", "MultiProcessPolicyManager", + "MultiProcessRolloutManager", "rollout_worker_node", "rollout_worker_process", "trainer_node", + "trainer_process" ] diff --git a/maro/rl/training/policy_manager/__init__.py b/maro/rl/training/policy_manager/__init__.py new file mode 100644 index 000000000..9fdb5df84 --- /dev/null +++ b/maro/rl/training/policy_manager/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .policy_manager import AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager +from .trainer import trainer_node, trainer_process + +__all__ = [ + "AbsPolicyManager", "LocalPolicyManager", "MultiNodePolicyManager", "MultiProcessPolicyManager", + "trainer_node", "trainer_process", +] \ No newline at end of file diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/policy_manager/policy_manager.py similarity index 91% rename from maro/rl/training/training_manager.py rename to maro/rl/training/policy_manager/policy_manager.py index d23606089..e088e430f 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -13,12 +13,12 @@ from maro.rl.policy import AbsCorePolicy, AbsPolicy from maro.utils import Logger -from .message_enums import MsgKey, MsgTag +from ..message_enums import MsgKey, MsgTag from .trainer import trainer_process -class AbsTrainingManager(ABC): - """Controller for policy updates. +class AbsPolicyManager(ABC): + """Manage all policies. The actual policy instances may reside here or be distributed on a set of remote nodes. """ @@ -42,7 +42,7 @@ def get_state(self): raise NotImplementedError -class LocalTrainingManager(AbsTrainingManager): +class LocalPolicyManager(AbsPolicyManager): """Policy manager that contains the actual policy instances. Args: @@ -85,8 +85,8 @@ def get_state(self): return {name: policy.get_state() for name, policy in self.policy_dict.items()} -class MultiProcessTrainingManager(AbsTrainingManager): - """Training manager that spawns a set of trainer processes for parallel training. +class MultiProcessPolicyManager(AbsPolicyManager): + """Policy manager that spawns a set of trainer processes for parallel training. Args: policy2trainer (dict): Mapping from policy names to trainer IDs. @@ -104,7 +104,7 @@ def __init__( log_dir: str = getcwd(), ): super().__init__() - self._logger = Logger("TRAINING_MANAGER", dump_folder=log_dir) + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self.policy2trainer = policy2trainer self._names = list(self.policy2trainer.keys()) self._trainer2policies = defaultdict(list) @@ -119,11 +119,10 @@ def __init__( trainer = Process( target=trainer_process, args=( - trainer_id, trainer_end, {name: create_policy_func_dict[name] for name in policy_names}, - log_dir - ) + ), + kwargs={"log_dir": log_dir} ) self._trainer_processes.append(trainer) trainer.start() @@ -165,8 +164,8 @@ def exit(self): conn.send({"type": "quit"}) -class MultiNodeTrainingManager(AbsTrainingManager): - """Training manager that spawns a set of trainer processes for parallel training. +class MultiNodePolicyManager(AbsPolicyManager): + """Policy manager that communicates with a set of remote nodes for parallel training. Args: policy2trainer (dict): Mapping from policy names to trainer IDs. @@ -189,11 +188,11 @@ def __init__( **proxy_kwargs ): super().__init__() - self._logger = Logger("TRAINING_MANAGER", dump_folder=log_dir) + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self.policy2trainer = policy2trainer self._names = list(self.policy2trainer.keys()) peers = {"trainer": len(set(self.policy2trainer.values()))} - self._proxy = Proxy(group, "training_manager", peers, **proxy_kwargs) + self._proxy = Proxy(group, "policy_manager", peers, **proxy_kwargs) @property def names(self): diff --git a/maro/rl/training/trainer.py b/maro/rl/training/policy_manager/trainer.py similarity index 89% rename from maro/rl/training/trainer.py rename to maro/rl/training/policy_manager/trainer.py index 3eaa60b32..41b4b10e1 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -9,14 +9,13 @@ from maro.communication import Proxy from maro.utils import Logger -from .message_enums import MsgKey, MsgTag +from ..message_enums import MsgKey, MsgTag -def trainer_process(trainer_id: str, conn: Connection, create_policy_func_dict: Dict[str, Callable], log_dir: str): - """Policy trainer process which can be spawned by a ``MultiProcessTrainingManager``. +def trainer_process(conn: Connection, create_policy_func_dict: Dict[str, Callable], log_dir: str = getcwd()): + """Policy trainer process which can be spawned by a ``MultiProcessPolicyManager``. Args: - trainer_id (str): Identifier for the trainer process for bookkeeping by the parent manager process. conn (Connection): Connection end for exchanging messages with the manager process. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` @@ -63,7 +62,7 @@ def trainer_node( for details. """ policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} - proxy = Proxy(group, "trainer", {"training_manager": 1}, component_name=trainer_id, **proxy_kwargs) + proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=trainer_id, **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) for msg in proxy.receive(): diff --git a/maro/rl/training/sync/__init__.py b/maro/rl/training/sync/__init__.py new file mode 100644 index 000000000..8b9ca4fde --- /dev/null +++ b/maro/rl/training/sync/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .early_stopper import AbsEarlyStopper +from .learner import Learner +from .local_learner import LocalLearner +from .rollout_manager import ( + AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager +) +from .rollout_worker import rollout_worker_node, rollout_worker_process + +__all__ = [ + "AbsEarlyStopper", + "Learner", + "LocalLearner", + "AbsRolloutManager", "LocalRolloutManager", "MultiProcessRolloutManager", "MultiNodeRolloutManager", + "rollout_worker_node", "rollout_worker_process" +] \ No newline at end of file diff --git a/maro/rl/training/early_stopper.py b/maro/rl/training/sync/early_stopper.py similarity index 100% rename from maro/rl/training/early_stopper.py rename to maro/rl/training/sync/early_stopper.py diff --git a/maro/rl/training/learner.py b/maro/rl/training/sync/learner.py similarity index 89% rename from maro/rl/training/learner.py rename to maro/rl/training/sync/learner.py index 7eaff0abb..a40511aa4 100644 --- a/maro/rl/training/learner.py +++ b/maro/rl/training/sync/learner.py @@ -9,7 +9,7 @@ from .early_stopper import AbsEarlyStopper from .rollout_manager import AbsRolloutManager -from .training_manager import AbsTrainingManager +from ..policy_manager.policy_manager import AbsPolicyManager class Learner: @@ -20,7 +20,7 @@ class Learner: as duplicate experience storage. Use ``LocalLearner`` instead. Args: - training_manager (AbsTrainingManager): An ``AbsTrainingManager`` instance that controls policy updates. + policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data collection. num_episodes (int): Number of training episodes. Each training episode may contain one or more @@ -39,7 +39,7 @@ class Learner: """ def __init__( self, - training_manager: AbsTrainingManager, + policy_manager: AbsPolicyManager, rollout_manager: AbsRolloutManager, num_episodes: int, eval_schedule: Union[int, List[int]] = None, @@ -48,7 +48,7 @@ def __init__( **end_of_episode_kwargs ): self.logger = Logger("LEARNER", dump_folder=log_dir) - self.training_manager = training_manager + self.policy_manager = policy_manager self.rollout_manager = rollout_manager self.num_episodes = num_episodes @@ -71,7 +71,7 @@ def __init__( self.early_stopper = early_stopper self._end_of_episode_kwargs = end_of_episode_kwargs - self._updated_policy_ids = self.training_manager.names + self._updated_policy_ids = self.policy_manager.names self._last_step_set = {} def run(self): @@ -80,7 +80,7 @@ def run(self): self._train(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 - env_metric_dict = self.rollout_manager.evaluate(ep, self.training_manager.get_state()) + env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) # performance details self.logger.info(f"Evaluation result: {env_metric_dict}") # early stopping check @@ -93,20 +93,20 @@ def run(self): if hasattr(self.rollout_manager, "exit"): self.rollout_manager.exit() - if hasattr(self.training_manager, "exit"): - self.training_manager.exit() + if hasattr(self.policy_manager, "exit"): + self.policy_manager.exit() def _train(self, ep: int): total_policy_update_time = 0 num_experiences_collected = segment = 0 self.rollout_manager.reset() - policy_state_dict = self.training_manager.get_state() + policy_state_dict = self.policy_manager.get_state() while not self.rollout_manager.episode_complete: segment += 1 # experience collection exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict) t0 = time.time() - policy_state_dict = self.training_manager.on_experiences(exp_by_policy) + policy_state_dict = self.policy_manager.on_experiences(exp_by_policy) total_policy_update_time += time.time() - t0 num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) diff --git a/maro/rl/training/local_learner.py b/maro/rl/training/sync/local_learner.py similarity index 59% rename from maro/rl/training/local_learner.py rename to maro/rl/training/sync/local_learner.py index 3b274ca7e..0c5b827d5 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/training/sync/local_learner.py @@ -1,14 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from maro.rl.wrappers.agent_wrapper import AgentWrapper import time -from collections import defaultdict from os import getcwd -from typing import Dict, List, Union +from typing import List, Union -from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.exploration import AbsExploration -from maro.rl.policy import AbsCorePolicy, AbsPolicy +from maro.rl.wrappers import AbsEnvWrapper from maro.utils import Logger from .early_stopper import AbsEarlyStopper @@ -18,18 +16,13 @@ class LocalLearner: """Controller for single-threaded learning workflows. Args: - env (AbsEnvWrapper): Environment wrapper instance to interact with a set of agents and collect experiences - for policy updates. - policies (List[AbsPolicy]): A set of named policies for inference. - agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's - queries to the correct policy. + env_wrapper (AbsEnvWrapper): Environment wrapper instance to interact with a set of agents and collect + experiences for learning. + agent_wrapper (AgentWrapper): Multi-policy wrapper that interacts with the ``env_wrapper`` directly. num_episodes (int): Number of training episodes. Each training episode may contain one or more collect-update cycles, depending on how the implementation of the roll-out manager. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. - agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct - an agent's query to the correct exploration scheme. Defaults to None. eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated at the end of the training episodes given in the list. In any case, the policies will be evaluated @@ -48,13 +41,10 @@ class LocalLearner: def __init__( self, - env: AbsEnvWrapper, - policies: List[AbsPolicy], - agent2policy: Dict[str, str], + env_wrapper: AbsEnvWrapper, + agent_wrapper: AgentWrapper, num_episodes: int, num_steps: int = -1, - exploration_dict: Dict[str, AbsExploration] = None, - agent2exploration: Dict[str, str] = None, eval_schedule: Union[int, List[int]] = None, eval_env: AbsEnvWrapper = None, early_stopper: AbsEarlyStopper = None, @@ -65,32 +55,13 @@ def __init__( raise ValueError("num_steps must be a positive integer or -1") self._logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) - self.env = env + self.env = env_wrapper self.eval_env = eval_env if eval_env else self.env - - # mappings between agents and policies - self.policy_dict = {policy.name: policy for policy in policies} - self._agent2policy = agent2policy - self._policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self._agent2policy.items()} - self._agent_groups_by_policy = defaultdict(list) - for agent_id, policy_id in agent2policy.items(): - self._agent_groups_by_policy[policy_id].append(agent_id) + self.agent = agent_wrapper self.num_episodes = num_episodes self._num_steps = num_steps if num_steps > 0 else float("inf") - # mappings between exploration schemes and agents - self.exploration_dict = exploration_dict - if exploration_dict: - self._agent2exploration = agent2exploration - self._exploration = { - agent_id: self.exploration_dict[exploration_id] - for agent_id, exploration_id in self._agent2exploration.items() - } - self._agent_groups_by_exploration = defaultdict(list) - for agent_id, exploration_id in self._agent2exploration.items(): - self._agent_groups_by_exploration[exploration_id].append(agent_id) - # evaluation schedule if eval_schedule is None: eval_schedule = [] @@ -126,27 +97,17 @@ def _train(self, ep: int): t0 = time.time() num_experiences_collected = 0 - if self.exploration_dict: - exploration_params = { - tuple(agent_ids): self.exploration_dict[exploration_id].parameters - for exploration_id, agent_ids in self._agent_groups_by_exploration.items() - } - self._logger.debug(f"Exploration parameters: {exploration_params}") - + self.agent.explore() self.env.reset() self.env.start() # get initial state segment = 0 while self.env.state: segment += 1 - for agent_id, exp in self._collect(ep, segment).items(): - num_experiences_collected += exp.size - if isinstance(self._policy[agent_id], AbsCorePolicy): - self._policy[agent_id].on_experiences(exp) - + exp_by_agent = self._collect(ep, segment) + self.agent.on_experiences(exp_by_agent) + num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) # update the exploration parameters if an episode is finished - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() + self.agent.exploration_step() # performance details if self._log_env_summary: @@ -162,11 +123,11 @@ def _train(self, ep: int): def _evaluate(self): """Policy evaluation.""" self._logger.info("Evaluating...") + self.agent.exploit() self.eval_env.reset() self.eval_env.start() # get initial state while self.eval_env.state: - action = {id_: self._policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} - self.eval_env.step(action) + self.eval_env.step(self.agent.choose_action(self.eval_env.state)) # performance details self._logger.info(f"Evaluation result: {self.eval_env.summary}") @@ -175,16 +136,7 @@ def _collect(self, ep, segment): start_step_index = self.env.step_index + 1 steps_to_go = self._num_steps while self.env.state and steps_to_go: - if self.exploration_dict: - action = { - id_: - self._exploration[id_](self._policy[id_].choose_action(st)) - if id_ in self._exploration else self._policy[id_].choose_action(st) - for id_, st in self.env.state.items() - } - else: - action = {id_: self._policy[id_].choose_action(st) for id_, st in self.env.state.items()} - self.env.step(action) + self.env.step(self.agent.choose_action(self.env.state)) steps_to_go -= 1 self._logger.info( diff --git a/maro/rl/training/rollout_manager.py b/maro/rl/training/sync/rollout_manager.py similarity index 97% rename from maro/rl/training/rollout_manager.py rename to maro/rl/training/sync/rollout_manager.py index 95c39c48f..688e7026e 100644 --- a/maro/rl/training/rollout_manager.py +++ b/maro/rl/training/sync/rollout_manager.py @@ -10,15 +10,14 @@ from typing import Callable, Dict, List from maro.communication import Proxy, SessionType -from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.rl.experience import ExperienceSet from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsPolicy from maro.utils import Logger -from .decision_generator import DecisionGenerator -from .message_enums import MsgKey, MsgTag from .rollout_worker import rollout_worker_process +from ..message_enums import MsgKey, MsgTag class AbsRolloutManager(ABC): @@ -245,13 +244,13 @@ class MultiProcessRolloutManager(AbsRolloutManager): create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an environment wrapper for training data collection. The function should take no parameters and return an environment wrapper instance. - create_decision_generator_func (Callable): Function to be used by each spawned roll-out worker to create a + create_agent_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create a decision generator for interacting with the environment. The function should take no parameters and return - a ``DecisionGenerator`` instance. + a ``AgentWrapper`` instance. create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an environment wrapper for evaluation. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the - worker processes. + worker processes. Defaults to None. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. num_eval_workers (int): Number of workers for evaluation. Defaults to 1. @@ -265,7 +264,7 @@ def __init__( self, num_workers: int, create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_decision_generator_func: Callable[[], DecisionGenerator], + create_agent_wrapper_func: Callable[[], AgentWrapper], create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, num_steps: int = -1, num_eval_workers: int = 1, @@ -293,10 +292,12 @@ def __init__( index, worker_end, create_env_wrapper_func, - create_decision_generator_func, - create_eval_env_wrapper_func, - log_dir - ) + create_agent_wrapper_func, + ), + kwargs={ + "create_eval_env_wrapper_func": create_eval_env_wrapper_func, + "log_dir": log_dir + } ) self._worker_processes.append(worker) worker.start() diff --git a/maro/rl/training/rollout_worker.py b/maro/rl/training/sync/rollout_worker.py similarity index 74% rename from maro/rl/training/rollout_worker.py rename to maro/rl/training/sync/rollout_worker.py index 87e4739de..c4c702649 100644 --- a/maro/rl/training/rollout_worker.py +++ b/maro/rl/training/sync/rollout_worker.py @@ -6,20 +6,19 @@ from typing import Callable from maro.communication import Proxy -from maro.rl.env_wrapper import AbsEnvWrapper +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger, set_seeds -from .decision_generator import DecisionGenerator -from .message_enums import MsgKey, MsgTag +from ..message_enums import MsgKey, MsgTag def rollout_worker_process( index: int, conn: Connection, create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_decision_generator_func: Callable[[], DecisionGenerator], - create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper], - log_dir: str + create_agent_wrapper_func: Callable[[], AgentWrapper], + create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, + log_dir: str = getcwd() ): """Roll-out worker process that can be spawned by a ``MultiProcessRolloutManager``. @@ -28,29 +27,29 @@ def rollout_worker_process( conn (Connection): Connection end for exchanging messages with the manager process. create_env_wrapper_func (Callable): Function to create an environment wrapper for training data collection. The function should take no parameters and return an environment wrapper instance. - create_decision_generator_func (Callable): Function to create a decision generator for interacting with - the environment. The function should take no parameters and return a ``DecisionGenerator`` instance. + create_agent_wrapper_func (Callable): Function to create a decision generator for interacting with + the environment. The function should take no parameters and return a ``AgentWrapper`` instance. create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function should take no parameters and return an environment wrapper instance. If this is None, the training - environment wrapper will be used for evaluation. + environment wrapper will be used for evaluation. Defaults to None. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ set_seeds(index) env_wrapper = create_env_wrapper_func() eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() - decision_generator = create_decision_generator_func() + agent_wrapper = create_agent_wrapper_func() logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) def collect(msg): ep, segment = msg["episode"], msg["segment"] # load policies - if hasattr(decision_generator, "update"): - decision_generator.update(msg["policy"]) + if hasattr(agent_wrapper, "update"): + agent_wrapper.update(msg["policy"]) # update exploration parameters - decision_generator.explore() + agent_wrapper.explore() if msg["exploration_step"]: - decision_generator.exploration_step() + agent_wrapper.exploration_step() if env_wrapper.state is None: logger.info(f"Training episode {ep}") @@ -60,7 +59,7 @@ def collect(msg): starting_step_index = env_wrapper.step_index + 1 steps_to_go = float("inf") if msg["num_steps"] == -1 else msg["num_steps"] while env_wrapper.state and steps_to_go > 0: - action = decision_generator.choose_action(env_wrapper.state, ep, env_wrapper.step_index) + action = agent_wrapper.choose_action(env_wrapper.state) env_wrapper.step(action) steps_to_go -= 1 @@ -69,9 +68,8 @@ def collect(msg): f"(steps {starting_step_index} - {env_wrapper.step_index})" ) - if hasattr(decision_generator, "store_experiences"): - policy_names = decision_generator.store_experiences(env_wrapper.get_experiences()) - ret_exp = decision_generator.get_experiences_by_policy(policy_names) + policy_names = agent_wrapper.on_experiences(env_wrapper.get_experiences()) + ret_exp = agent_wrapper.get_experiences_by_policy(policy_names) return_info = { "worker_index": index, @@ -87,13 +85,11 @@ def evaluate(msg): logger.info("Evaluating...") eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state - decision_generator.exploit() - if hasattr(decision_generator, "update"): - decision_generator.update(msg["policy"]) + agent_wrapper.exploit() + if hasattr(agent_wrapper, "update"): + agent_wrapper.update(msg["policy"]) while eval_env_wrapper.state: - action = decision_generator.choose_action( - eval_env_wrapper.state, msg["episode"], eval_env_wrapper.step_index - ) + action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) conn.send({"worker_id": index, "env_summary": eval_env_wrapper.summary}) @@ -110,7 +106,7 @@ def evaluate(msg): def rollout_worker_node( create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_decision_generator_func: Callable[[], DecisionGenerator], + create_agent_wrapper_func: Callable[[], AgentWrapper], group: str, create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, log_dir: str = getcwd(), @@ -121,8 +117,8 @@ def rollout_worker_node( Args: create_env_wrapper_func (Callable): Function to create an environment wrapper for roll-out. The function should take no parameters and return an environment wrapper instance. - create_decision_generator_func (Callable): Function to create a decision generator for interacting with - the environment. The function should take no parameters and return a ``DecisionGenerator`` instance. + create_agent_wrapper_func (Callable): Function to create a decision generator for interacting with + the environment. The function should take no parameters and return a ``AgentWrapper`` instance. group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager that manages them. create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function @@ -134,7 +130,7 @@ def rollout_worker_node( """ env_wrapper = create_env_wrapper_func() eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() - decision_generator = create_decision_generator_func() + agent_wrapper = create_agent_wrapper_func() proxy = Proxy(group, "rollout_worker", {"rollout_manager": 1}, **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) @@ -142,12 +138,12 @@ def rollout_worker_node( def collect(msg): ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] # load policies - if hasattr(decision_generator, "update"): - decision_generator.update(msg.body[MsgKey.POLICY]) + if hasattr(agent_wrapper, "update"): + agent_wrapper.update(msg.body[MsgKey.POLICY]) # set exploration parameters - decision_generator.explore() + agent_wrapper.explore() if msg.body[MsgKey.EXPLORATION_STEP]: - decision_generator.exploration_step() + agent_wrapper.exploration_step() if env_wrapper.state is None: logger.info(f"Training episode {msg.body[MsgKey.EPISODE]}") @@ -157,7 +153,7 @@ def collect(msg): starting_step_index = env_wrapper.step_index + 1 steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] while env_wrapper.state and steps_to_go > 0: - action = decision_generator.choose_action(env_wrapper.state, ep, env_wrapper.step_index) + action = agent_wrapper.choose_action(env_wrapper.state) env_wrapper.step(action) steps_to_go -= 1 @@ -166,9 +162,8 @@ def collect(msg): f"(steps {starting_step_index} - {env_wrapper.step_index})" ) - if hasattr(decision_generator, "store_experiences"): - policy_names = decision_generator.store_experiences(env_wrapper.get_experiences()) - ret_exp = decision_generator.get_experiences_by_policy(policy_names) + policy_names = agent_wrapper.on_experiences(env_wrapper.get_experiences()) + ret_exp = agent_wrapper.get_experiences_by_policy(policy_names) return_info = { MsgKey.EPISODE_END: not env_wrapper.state, @@ -186,11 +181,11 @@ def evaluate(msg): ep = msg.body[MsgKey.EPISODE] eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state - decision_generator.exploit() - if hasattr(decision_generator, "update"): - decision_generator.update(msg.body[MsgKey.POLICY]) + agent_wrapper.exploit() + if hasattr(agent_wrapper, "update"): + agent_wrapper.update(msg.body[MsgKey.POLICY]) while eval_env_wrapper.state: - action = decision_generator.choose_action(eval_env_wrapper.state, ep, eval_env_wrapper.step_index) + action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) return_info = {MsgKey.ENV_SUMMARY: eval_env_wrapper.summary, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} diff --git a/maro/rl/env_wrapper/__init__.py b/maro/rl/wrappers/__init__.py similarity index 57% rename from maro/rl/env_wrapper/__init__.py rename to maro/rl/wrappers/__init__.py index 13de38a26..9f45fdfc5 100644 --- a/maro/rl/env_wrapper/__init__.py +++ b/maro/rl/wrappers/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .agent_wrapper import AgentWrapper from .env_wrapper import AbsEnvWrapper -__all__ = ["AbsEnvWrapper"] +__all__ = ["AbsEnvWrapper", "AgentWrapper"] diff --git a/maro/rl/training/decision_generator.py b/maro/rl/wrappers/agent_wrapper.py similarity index 80% rename from maro/rl/training/decision_generator.py rename to maro/rl/wrappers/agent_wrapper.py index 0c9047aa9..cfd38aae3 100644 --- a/maro/rl/training/decision_generator.py +++ b/maro/rl/wrappers/agent_wrapper.py @@ -9,13 +9,13 @@ from maro.utils import Logger -class DecisionGenerator: - """Multi-agent multi-policy decision generator with exploration. +class AgentWrapper: + """Multi-agent wrapper that interacts with an ``EnvWrapper`` with a unified inferface. Args: + policies (List[AbsPolicy]): A list of policies for inference. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. - policies (List[AbsPolicy]): A list of policies for inference. exploration_dict (Dict[str, AbsExploration]): A dictionary of named ``AbsExploration`` instances. Defaults to None. agent2exploration (Dict[str, str]): Mapping from agent names to exploration instance names. Defaults to None. @@ -24,43 +24,43 @@ class DecisionGenerator: """ def __init__( self, - agent2policy: Dict[str, str], policies: List[AbsPolicy], + agent2policy: Dict[str, str], exploration_dict: Dict[str, AbsExploration] = None, agent2exploration: Dict[str, str] = None, log_dir: str = getcwd() ): self.policy_dict = {policy.name: policy for policy in policies} - self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} self.agent2policy = agent2policy + self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} self.exploration_dict = exploration_dict if self.exploration_dict: self.exploration_by_agent = { agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() } self.exploring = True # Flag indicating that exploration is turned on. - self._logger = Logger("local_decision_generator", dump_folder=log_dir) + self._logger = Logger("local_agent_wrapper", dump_folder=log_dir) - def choose_action(self, state: dict, ep: int, step: int) -> dict: + def choose_action(self, state: dict) -> dict: """Generate an action based on the given state. Args: state (dict): Dicitionary of agents' states based on which action decisions will be made. - ep (int): Current episode. - step (int): Current step. """ action_by_agent = {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} if self.exploring and self.exploration_dict: for agent_id in action_by_agent: - action_by_agent[agent_id] = self.exploration_by_agent[agent_id](action_by_agent[agent_id]) + if agent_id in self.exploration_by_agent: + action_by_agent[agent_id] = self.exploration_by_agent[agent_id](action_by_agent[agent_id]) return action_by_agent - def store_experiences(self, exp_by_agent: dict) -> set: + def on_experiences(self, exp_by_agent: dict) -> set: """Store agent experiences in the policies' experience managers.""" policies_with_new_exp = set() for agent_id, exp in exp_by_agent.items(): - self.policy[agent_id].experience_manager.put(exp) + if hasattr(self.policy[agent_id], "on_experiences"): + self.policy[agent_id].on_experiences(exp) policies_with_new_exp.add(self.agent2policy[agent_id]) return policies_with_new_exp @@ -78,9 +78,10 @@ def update(self, policy_state_dict: dict): self._logger.info(f"updated policies {list(policy_state_dict.keys())}") def exploration_step(self): - for exploration in self.exploration_dict.values(): - exploration.step() - print(f"epsilon: {exploration.epsilon}") + if self.exploration_dict: + for exploration in self.exploration_dict.values(): + exploration.step() + # print(f"epsilon: {exploration.epsilon}") def exploit(self): self.exploring = False diff --git a/maro/rl/env_wrapper/env_wrapper.py b/maro/rl/wrappers/env_wrapper.py similarity index 100% rename from maro/rl/env_wrapper/env_wrapper.py rename to maro/rl/wrappers/env_wrapper.py From 4f4d5bbe57b75bdda67f43c83495325916e79e26 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 20 Jun 2021 09:18:06 +0000 Subject: [PATCH 314/482] fixed lint issues --- maro/rl/algorithms/dqn.py | 2 +- maro/rl/training/__init__.py | 8 ++++---- maro/rl/training/policy_manager/__init__.py | 2 +- maro/rl/training/sync/__init__.py | 6 ++---- maro/rl/training/sync/learner.py | 2 +- maro/rl/training/sync/local_learner.py | 7 +++---- maro/rl/training/sync/rollout_manager.py | 4 ++-- maro/rl/training/sync/rollout_worker.py | 1 - 8 files changed, 14 insertions(+), 18 deletions(-) diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index 6259f91a7..a58bfe27c 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -84,7 +84,7 @@ def __init__( self._training_counter = 0 self.prioritized_experience_replay = isinstance(self.experience_manager.sampler, PrioritizedSampler) if not self.prioritized_experience_replay: - self._loss_func = torch.nn.MSELoss() + self._loss_func = torch.nn.MSELoss() def choose_action(self, states) -> Union[int, np.ndarray]: with torch.no_grad(): diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index fef1e8098..2e72381bc 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -1,14 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .sync import ( - AbsEarlyStopper, AbsRolloutManager, Learner, LocalLearner, LocalRolloutManager, MultiNodeRolloutManager, - MultiProcessRolloutManager, rollout_worker_node, rollout_worker_process -) from .policy_manager import ( AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, trainer_node, trainer_process ) +from .sync import ( + AbsEarlyStopper, AbsRolloutManager, Learner, LocalLearner, LocalRolloutManager, MultiNodeRolloutManager, + MultiProcessRolloutManager, rollout_worker_node, rollout_worker_process +) __all__ = [ "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Learner", "LocalLearner", "LocalPolicyManager", diff --git a/maro/rl/training/policy_manager/__init__.py b/maro/rl/training/policy_manager/__init__.py index 9fdb5df84..9c877a4f9 100644 --- a/maro/rl/training/policy_manager/__init__.py +++ b/maro/rl/training/policy_manager/__init__.py @@ -7,4 +7,4 @@ __all__ = [ "AbsPolicyManager", "LocalPolicyManager", "MultiNodePolicyManager", "MultiProcessPolicyManager", "trainer_node", "trainer_process", -] \ No newline at end of file +] diff --git a/maro/rl/training/sync/__init__.py b/maro/rl/training/sync/__init__.py index 8b9ca4fde..0f75e1bd0 100644 --- a/maro/rl/training/sync/__init__.py +++ b/maro/rl/training/sync/__init__.py @@ -4,9 +4,7 @@ from .early_stopper import AbsEarlyStopper from .learner import Learner from .local_learner import LocalLearner -from .rollout_manager import ( - AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager -) +from .rollout_manager import AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager from .rollout_worker import rollout_worker_node, rollout_worker_process __all__ = [ @@ -15,4 +13,4 @@ "LocalLearner", "AbsRolloutManager", "LocalRolloutManager", "MultiProcessRolloutManager", "MultiNodeRolloutManager", "rollout_worker_node", "rollout_worker_process" -] \ No newline at end of file +] diff --git a/maro/rl/training/sync/learner.py b/maro/rl/training/sync/learner.py index a40511aa4..55ed5e931 100644 --- a/maro/rl/training/sync/learner.py +++ b/maro/rl/training/sync/learner.py @@ -7,9 +7,9 @@ from maro.utils import Logger +from ..policy_manager.policy_manager import AbsPolicyManager from .early_stopper import AbsEarlyStopper from .rollout_manager import AbsRolloutManager -from ..policy_manager.policy_manager import AbsPolicyManager class Learner: diff --git a/maro/rl/training/sync/local_learner.py b/maro/rl/training/sync/local_learner.py index 0c5b827d5..a7c34a971 100644 --- a/maro/rl/training/sync/local_learner.py +++ b/maro/rl/training/sync/local_learner.py @@ -1,12 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.wrappers.agent_wrapper import AgentWrapper import time from os import getcwd from typing import List, Union -from maro.rl.wrappers import AbsEnvWrapper +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger from .early_stopper import AbsEarlyStopper @@ -18,7 +17,7 @@ class LocalLearner: Args: env_wrapper (AbsEnvWrapper): Environment wrapper instance to interact with a set of agents and collect experiences for learning. - agent_wrapper (AgentWrapper): Multi-policy wrapper that interacts with the ``env_wrapper`` directly. + agent_wrapper (AgentWrapper): Multi-policy wrapper that interacts with the ``env_wrapper`` directly. num_episodes (int): Number of training episodes. Each training episode may contain one or more collect-update cycles, depending on how the implementation of the roll-out manager. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which @@ -104,7 +103,7 @@ def _train(self, ep: int): while self.env.state: segment += 1 exp_by_agent = self._collect(ep, segment) - self.agent.on_experiences(exp_by_agent) + self.agent.on_experiences(exp_by_agent) num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) # update the exploration parameters if an episode is finished self.agent.exploration_step() diff --git a/maro/rl/training/sync/rollout_manager.py b/maro/rl/training/sync/rollout_manager.py index 688e7026e..ee558ef32 100644 --- a/maro/rl/training/sync/rollout_manager.py +++ b/maro/rl/training/sync/rollout_manager.py @@ -16,8 +16,8 @@ from maro.rl.policy import AbsPolicy from maro.utils import Logger -from .rollout_worker import rollout_worker_process from ..message_enums import MsgKey, MsgTag +from .rollout_worker import rollout_worker_process class AbsRolloutManager(ABC): @@ -296,7 +296,7 @@ def __init__( ), kwargs={ "create_eval_env_wrapper_func": create_eval_env_wrapper_func, - "log_dir": log_dir + "log_dir": log_dir } ) self._worker_processes.append(worker) diff --git a/maro/rl/training/sync/rollout_worker.py b/maro/rl/training/sync/rollout_worker.py index c4c702649..b368516b3 100644 --- a/maro/rl/training/sync/rollout_worker.py +++ b/maro/rl/training/sync/rollout_worker.py @@ -178,7 +178,6 @@ def collect(msg): def evaluate(msg): logger.info("Evaluating...") - ep = msg.body[MsgKey.EPISODE] eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state agent_wrapper.exploit() From 96b9cceeeac386d17568e70d4af97e5b54730fa9 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 20 Jun 2021 09:22:46 +0000 Subject: [PATCH 315/482] fixed lint issues --- maro/rl/training/sync/rollout_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/rl/training/sync/rollout_manager.py b/maro/rl/training/sync/rollout_manager.py index ee558ef32..e5fd93617 100644 --- a/maro/rl/training/sync/rollout_manager.py +++ b/maro/rl/training/sync/rollout_manager.py @@ -10,10 +10,10 @@ from typing import Callable, Dict, List from maro.communication import Proxy, SessionType -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.rl.experience import ExperienceSet from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsPolicy +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger from ..message_enums import MsgKey, MsgTag From 78c225aec379d2322998f7024fccbbf4e83df55c Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 21 Jun 2021 08:19:10 +0000 Subject: [PATCH 316/482] policy manager refined --- examples/cim/dqn/policy_manager.py | 6 +- maro/rl/training/message_enums.py | 5 +- .../training/policy_manager/policy_manager.py | 119 +++++++----------- maro/rl/training/policy_manager/trainer.py | 34 +++-- maro/rl/training/sync/learner.py | 6 +- maro/rl/training/sync/rollout_manager.py | 6 +- maro/rl/training/sync/rollout_worker.py | 15 +-- 7 files changed, 88 insertions(+), 103 deletions(-) diff --git a/examples/cim/dqn/policy_manager.py b/examples/cim/dqn/policy_manager.py index 9bf7d8392..cc8ff5126 100644 --- a/examples/cim/dqn/policy_manager.py +++ b/examples/cim/dqn/policy_manager.py @@ -14,12 +14,12 @@ from policy import get_independent_policy_for_training +policies = [get_independent_policy_for_training(i) for i in AGENT_IDS] if config["distributed"]["policy_training_mode"] == "local": - policy_manager = LocalPolicyManager( - [get_independent_policy_for_training(i) for i in AGENT_IDS], log_dir=log_dir - ) + policy_manager = LocalPolicyManager(policies, log_dir=log_dir) else: policy_manager = MultiProcessPolicyManager( + policies, {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping {i: get_independent_policy_for_training for i in AGENT_IDS}, log_dir=log_dir diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index 5d6623198..f089ca4f3 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -7,7 +7,7 @@ class MsgTag(Enum): COLLECT = "rollout" EVAL = "eval" - GET_POLICY_STATE = "get_policy_state" + INIT_POLICY_STATE = "init_policy_state" POLICY_STATE = "policy_state" CHOOSE_ACTION = "choose_action" ACTION = "action" @@ -23,11 +23,12 @@ class MsgKey(Enum): AGENT_ID = "agent_id" EPISODE = "episode" SEGMENT = "segment" + STEP = "step" ENV_SUMMARY = "env_summary" EXPERIENCES = "experiences" NUM_EXPERIENCES = "num_experiences" STATE = "state" - POLICY = "policy" + POLICY_STATE = "policy_state" EXPLORATION_STEP = "exploration_step" VERSION = "version" NUM_STEPS = "num_steps" diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index e088e430f..e0f770e0a 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -8,7 +8,7 @@ from os import getcwd from typing import Callable, Dict, List -from maro.communication import Proxy, SessionType +from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.experience import ExperienceSet from maro.rl.policy import AbsCorePolicy, AbsPolicy from maro.utils import Logger @@ -20,48 +20,42 @@ class AbsPolicyManager(ABC): """Manage all policies. - The actual policy instances may reside here or be distributed on a set of remote nodes. + The actual policy instances may reside here or be distributed on a set of processes or remote nodes. + + Args: + policies (List[AbsPolicy]): A list of policies managed by the manager. """ - def __init__(self): - pass + def __init__(self, policies: List[AbsPolicy]): + for policy in policies: + if not isinstance(policy, AbsCorePolicy): + raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") - @property - @abstractmethod - def names(self): - """Return the list of policy names.""" - raise NotImplementedError + super().__init__() + self.policy_dict = {policy.name: policy for policy in policies} @abstractmethod def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): """Logic for handling incoming experiences is implemented here.""" raise NotImplementedError - @abstractmethod def get_state(self): - """Return the latest policy states.""" - raise NotImplementedError + return {policy_name: policy.get_state() for policy_name, policy in self.policy_dict.items()} class LocalPolicyManager(AbsPolicyManager): """Policy manager that contains the actual policy instances. Args: - policies (List[AbsPolicy]): A list of policies. + policies (List[AbsPolicy]): A list of policies managed by the manager. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__(self, policies: List[AbsPolicy], log_dir: str = getcwd()): - super().__init__() - self._names = [policy.name for policy in policies] - self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) - self.policy_dict = {policy.name: policy for policy in policies} + super().__init__(policies) + self._logger = Logger("LOCAL_TRAINING_MANAGER", dump_folder=log_dir) self._new_exp_counter = defaultdict(int) - @property - def names(self): - return self._names - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): """Store experiences and update policies if possible. @@ -69,26 +63,25 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): policy's experience manager. Policies whose update conditions have been met will then be updated. """ t0 = time.time() - updated = { - name: self.policy_dict[name].get_state() - for name, exp in exp_by_policy.items() - if isinstance(self.policy_dict[name], AbsCorePolicy) and self.policy_dict[name].on_experiences(exp) - } + updated = [] + for policy_name, exp in exp_by_policy.items(): + if ( + isinstance(self.policy_dict[policy_name], AbsCorePolicy) and + self.policy_dict[policy_name].on_experiences(exp) + ): + updated.append(policy_name) if updated: - self._logger.info(f"Updated policies {list(updated.keys())}") + self._logger.info(f"Updated policies {updated}") self._logger.debug(f"policy update time: {time.time() - t0}") - return updated - - def get_state(self): - return {name: policy.get_state() for name, policy in self.policy_dict.items()} class MultiProcessPolicyManager(AbsPolicyManager): """Policy manager that spawns a set of trainer processes for parallel training. Args: + policies (List[AbsPolicy]): A list of policies managed by the manager. policy2trainer (dict): Mapping from policy names to trainer IDs. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` @@ -99,17 +92,17 @@ class MultiProcessPolicyManager(AbsPolicyManager): """ def __init__( self, + policies: List[AbsPolicy], policy2trainer: Dict[str, str], create_policy_func_dict: Dict[str, Callable], log_dir: str = getcwd(), ): - super().__init__() + super().__init__(policies) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self.policy2trainer = policy2trainer - self._names = list(self.policy2trainer.keys()) self._trainer2policies = defaultdict(list) - for policy_name, trainer_name in policy2trainer.items(): - self._trainer2policies[trainer_name].append(policy_name) + for policy_name, trainer_id in policy2trainer.items(): + self._trainer2policies[trainer_id].append(policy_name) self._trainer_processes = [] self._manager_end = {} @@ -119,18 +112,16 @@ def __init__( trainer = Process( target=trainer_process, args=( + trainer_id, trainer_end, {name: create_policy_func_dict[name] for name in policy_names}, + {name: self.policy_dict[name].get_state() for name in self._trainer2policies[trainer_id]} ), kwargs={"log_dir": log_dir} ) self._trainer_processes.append(trainer) trainer.start() - @property - def names(self): - return self._names - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): for trainer_id, conn in self._manager_end.items(): conn.send({ @@ -138,25 +129,10 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): "experiences": {name: exp_by_policy[name] for name in self._trainer2policies[trainer_id]} }) - policy_state_dict = {} for conn in self._manager_end.values(): result = conn.recv() for policy_name, policy_state in result["policy"].items(): - policy_state_dict[policy_name] = policy_state - - return policy_state_dict - - def get_state(self): - policy_state_dict = {} - for conn in self._manager_end.values(): - conn.send({"type": "get_policy_state"}) - - for conn in self._manager_end.values(): - result = conn.recv() - for policy_name, policy_state in result["policy"].items(): - policy_state_dict[policy_name] = policy_state - - return policy_state_dict + self.policy_dict[policy_name].set_state(policy_state) def exit(self): """Tell the trainer processes to exit.""" @@ -185,21 +161,26 @@ def __init__( policy2trainer: Dict[str, str], group: str, log_dir: str = getcwd(), - **proxy_kwargs + proxy_kwargs: dict = {} ): super().__init__() self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self.policy2trainer = policy2trainer - self._names = list(self.policy2trainer.keys()) + self._trainer2policies = defaultdict(list) + for policy_name, trainer_name in self.policy2trainer.items(): + self._trainer2policies[trainer_name].append(policy_name) peers = {"trainer": len(set(self.policy2trainer.values()))} self._proxy = Proxy(group, "policy_manager", peers, **proxy_kwargs) - - @property - def names(self): - return self._names + for trainer_name, policy_names in self._trainer2policies.items(): + self._proxy.send( + SessionMessage( + MsgTag.POLICY_STATE, self._proxy.name, + body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} + ) + ) def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): - msg_body_by_dest, policy_state_dict = defaultdict(dict), {} + msg_body_by_dest = defaultdict(dict) for policy_name, exp in exp_by_policy.items(): trainer_id = self.policy2trainer[policy_name] if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: @@ -207,18 +188,8 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): - for policy_name, policy_state in reply.body[MsgKey.POLICY].items(): - policy_state_dict[policy_name] = policy_state - - return policy_state_dict - - def get_state(self): - policy_state_dict = {} - for reply in self._proxy.broadcast("trainer", MsgTag.GET_POLICY_STATE, SessionType.TASK): - for policy_name, policy_state in reply.body[MsgKey.POLICY].items(): - policy_state_dict[policy_name] = policy_state - - return policy_state_dict + for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): + self.policy_dict[policy_name].set_state(policy_state) def exit(self): """Tell the remote trainers to exit.""" diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py index 41b4b10e1..811df5c38 100644 --- a/maro/rl/training/policy_manager/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -12,10 +12,17 @@ from ..message_enums import MsgKey, MsgTag -def trainer_process(conn: Connection, create_policy_func_dict: Dict[str, Callable], log_dir: str = getcwd()): +def trainer_process( + trainer_id: int, + conn: Connection, + create_policy_func_dict: Dict[str, Callable], + initial_policy_states: dict, + log_dir: str = getcwd() +): """Policy trainer process which can be spawned by a ``MultiProcessPolicyManager``. Args: + trainer_id (int): Integer trainer ID. conn (Connection): Connection end for exchanging messages with the manager process. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` @@ -24,6 +31,10 @@ def trainer_process(conn: Connection, create_policy_func_dict: Dict[str, Callabl """ policy_dict = {policy_name: func(policy_name) for policy_name, func in create_policy_func_dict.items()} logger = Logger("TRAINER", dump_folder=log_dir) + for name, state in initial_policy_states.items(): + policy_dict[name].set_state(state) + logger.info(f"Trainer {trainer_id} initialized policy {name}") + while True: msg = conn.recv() if msg["type"] == "train": @@ -46,7 +57,7 @@ def trainer_node( create_policy_func_dict: Dict[str, Callable], group: str, log_dir: str = getcwd(), - **proxy_kwargs + proxy_kwargs: dict = {} ): """Policy trainer process that can be launched on separate computation nodes. @@ -71,14 +82,17 @@ def trainer_node( proxy.close() break - if msg.tag == MsgTag.TRAIN: + if msg.tag == MsgTag.INIT_POLICY_STATE: + for name, state in msg.body[MsgKey.POLICY_STATE].items(): + policy_dict[name].set_state(state) + logger.info(f"Trainer {trainer_id} initialized policy {name}") + elif msg.tag == MsgTag.TRAIN: t0 = time.time() - updated = { - name: policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() - if policy_dict[name].on_experiences(exp) + msg_body = { + MsgKey.POLICY_STATE: { + name: policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() + if policy_dict[name].on_experiences(exp) + } } logger.debug(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, body={MsgKey.POLICY: updated}) - elif msg.tag == MsgTag.GET_POLICY_STATE: - policy_state_dict = {name: policy.get_state() for name, policy in policy_dict.items()} - proxy.reply(msg, tag=MsgTag.POLICY_STATE, body={MsgKey.POLICY: policy_state_dict}) + proxy.reply(msg, body=msg_body) diff --git a/maro/rl/training/sync/learner.py b/maro/rl/training/sync/learner.py index 55ed5e931..7842c14d9 100644 --- a/maro/rl/training/sync/learner.py +++ b/maro/rl/training/sync/learner.py @@ -71,7 +71,6 @@ def __init__( self.early_stopper = early_stopper self._end_of_episode_kwargs = end_of_episode_kwargs - self._updated_policy_ids = self.policy_manager.names self._last_step_set = {} def run(self): @@ -100,13 +99,12 @@ def _train(self, ep: int): total_policy_update_time = 0 num_experiences_collected = segment = 0 self.rollout_manager.reset() - policy_state_dict = self.policy_manager.get_state() while not self.rollout_manager.episode_complete: segment += 1 # experience collection - exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict) + exp_by_policy = self.rollout_manager.collect(ep, segment, self.policy_manager.get_state()) t0 = time.time() - policy_state_dict = self.policy_manager.on_experiences(exp_by_policy) + self.policy_manager.on_experiences(exp_by_policy) total_policy_update_time += time.time() - t0 num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) diff --git a/maro/rl/training/sync/rollout_manager.py b/maro/rl/training/sync/rollout_manager.py index e5fd93617..9c0a1b057 100644 --- a/maro/rl/training/sync/rollout_manager.py +++ b/maro/rl/training/sync/rollout_manager.py @@ -413,7 +413,7 @@ def __init__( num_eval_workers: int = 1, log_env_summary: bool = True, log_dir: str = getcwd(), - **proxy_kwargs + proxy_kwargs: dict = {} ): if num_eval_workers > num_workers: raise ValueError("num_eval_workers cannot exceed the number of available workers") @@ -461,7 +461,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): MsgKey.EPISODE: ep, MsgKey.SEGMENT: segment, MsgKey.NUM_STEPS: self._num_steps, - MsgKey.POLICY: policy_state_dict, + MsgKey.POLICY_STATE: policy_state_dict, MsgKey.EXPLORATION_STEP: self._exploration_step } @@ -516,7 +516,7 @@ def evaluate(self, ep: int, policy_state_dict: dict): Returns: Environment summary. """ - msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY: policy_state_dict} + msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY_STATE: policy_state_dict} workers = choices(self._workers, k=self._num_eval_workers) env_summary_dict = {} diff --git a/maro/rl/training/sync/rollout_worker.py b/maro/rl/training/sync/rollout_worker.py index b368516b3..efb18940d 100644 --- a/maro/rl/training/sync/rollout_worker.py +++ b/maro/rl/training/sync/rollout_worker.py @@ -68,8 +68,8 @@ def collect(msg): f"(steps {starting_step_index} - {env_wrapper.step_index})" ) - policy_names = agent_wrapper.on_experiences(env_wrapper.get_experiences()) - ret_exp = agent_wrapper.get_experiences_by_policy(policy_names) + policies_with_new_exp = agent_wrapper.on_experiences(env_wrapper.get_experiences()) + ret_exp = agent_wrapper.get_experiences_by_policy(policies_with_new_exp) return_info = { "worker_index": index, @@ -83,9 +83,9 @@ def collect(msg): def evaluate(msg): logger.info("Evaluating...") + agent_wrapper.exploit() eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state - agent_wrapper.exploit() if hasattr(agent_wrapper, "update"): agent_wrapper.update(msg["policy"]) while eval_env_wrapper.state: @@ -110,7 +110,7 @@ def rollout_worker_node( group: str, create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, log_dir: str = getcwd(), - **proxy_kwargs + proxy_kwargs: dict = {} ): """Roll-out worker process that can be launched on separate computation nodes. @@ -137,9 +137,10 @@ def rollout_worker_node( def collect(msg): ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] + # load policies - if hasattr(agent_wrapper, "update"): - agent_wrapper.update(msg.body[MsgKey.POLICY]) + if msg.body[MsgKey.POLICY_STATE]: + agent_wrapper.update(msg.body[MsgKey.POLICY_STATE]) # set exploration parameters agent_wrapper.explore() if msg.body[MsgKey.EXPLORATION_STEP]: @@ -182,7 +183,7 @@ def evaluate(msg): eval_env_wrapper.start() # get initial state agent_wrapper.exploit() if hasattr(agent_wrapper, "update"): - agent_wrapper.update(msg.body[MsgKey.POLICY]) + agent_wrapper.update(msg.body[MsgKey.POLICY_STATE]) while eval_env_wrapper.state: action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) From 9acae8048ffc331330a9e4a0415065530fc9c52e Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 21 Jun 2021 08:22:23 +0000 Subject: [PATCH 317/482] lint fix --- maro/rl/training/policy_manager/trainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py index 811df5c38..f8a9f2abb 100644 --- a/maro/rl/training/policy_manager/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -89,7 +89,7 @@ def trainer_node( elif msg.tag == MsgTag.TRAIN: t0 = time.time() msg_body = { - MsgKey.POLICY_STATE: { + MsgKey.POLICY_STATE: { name: policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() if policy_dict[name].on_experiences(exp) } From 424cabb7cc73c22878975574425b9c3ab82bf71c Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 21 Jun 2021 09:05:46 +0000 Subject: [PATCH 318/482] restructured CIM-dqn sync code --- examples/cim/dqn/agent_wrapper.py | 30 ++++++++++++++++ examples/cim/dqn/{ => sync_mode}/main.py | 4 ++- .../dqn/{ => sync_mode}/rollout_manager.py | 35 +++++-------------- 3 files changed, 41 insertions(+), 28 deletions(-) create mode 100644 examples/cim/dqn/agent_wrapper.py rename examples/cim/dqn/{ => sync_mode}/main.py (91%) rename examples/cim/dqn/{ => sync_mode}/rollout_manager.py (51%) diff --git a/examples/cim/dqn/agent_wrapper.py b/examples/cim/dqn/agent_wrapper.py new file mode 100644 index 000000000..6cdfa86a2 --- /dev/null +++ b/examples/cim/dqn/agent_wrapper.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +from maro.rl import AgentWrapper, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler + +dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory +cim_path = os.path.dirname(dqn_path) # CIM example directory +sys.path.insert(0, cim_path) +sys.path.insert(0, dqn_path) +from general import AGENT_IDS, NUM_ACTIONS, config, log_dir +from policy import get_independent_policy_for_rollout + +def get_agent_wrapper(): + epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) + epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + last_ep=config["num_episodes"], + **config["exploration"] + ) + return AgentWrapper( + policies=[get_independent_policy_for_rollout(i) for i in AGENT_IDS], + agent2policy={i: i for i in AGENT_IDS}, + exploration_dict={f"EpsilonGreedy": epsilon_greedy}, + agent2exploration={i: "EpsilonGreedy" for i in AGENT_IDS}, + log_dir=log_dir + ) diff --git a/examples/cim/dqn/main.py b/examples/cim/dqn/sync_mode/main.py similarity index 91% rename from examples/cim/dqn/main.py rename to examples/cim/dqn/sync_mode/main.py index bd64db2eb..0b15190da 100644 --- a/examples/cim/dqn/main.py +++ b/examples/cim/dqn/sync_mode/main.py @@ -7,10 +7,12 @@ from maro.rl import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner from maro.simulator import Env -dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory +sync_mode_path = os.path.dirname(os.path.realpath(__file__)) # DQN sync mode directory +dqn_path = os.path.dirname(sync_mode_path) # DQN directory cim_path = os.path.dirname(dqn_path) # CIM example directory sys.path.insert(0, cim_path) sys.path.insert(0, dqn_path) +sys.path.insert(0, sync_mode_path) from env_wrapper import CIMEnvWrapper from general import NUM_ACTIONS, config, log_dir from policy import get_independent_policy_for_training diff --git a/examples/cim/dqn/rollout_manager.py b/examples/cim/dqn/sync_mode/rollout_manager.py similarity index 51% rename from examples/cim/dqn/rollout_manager.py rename to examples/cim/dqn/sync_mode/rollout_manager.py index 8d0ae27c8..acc1caa49 100644 --- a/examples/cim/dqn/rollout_manager.py +++ b/examples/cim/dqn/sync_mode/rollout_manager.py @@ -4,41 +4,22 @@ import os import sys -from maro.rl import ( - AgentWrapper, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, LocalRolloutManager, - MultiProcessRolloutManager -) +from maro.rl import EpsilonGreedyExploration, LocalRolloutManager, MultiProcessRolloutManager + from maro.simulator import Env -dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory +sync_mode_path = os.path.dirname(os.path.realpath(__file__)) # DQN sync mode directory +dqn_path = os.path.dirname(sync_mode_path) # DQN directory cim_path = os.path.dirname(dqn_path) # CIM example directory sys.path.insert(0, cim_path) sys.path.insert(0, dqn_path) +sys.path.insert(0, sync_mode_path) +from agent_wrapper import get_agent_wrapper from env_wrapper import CIMEnvWrapper -from general import AGENT_IDS, NUM_ACTIONS, config, log_dir +from general import NUM_ACTIONS, config, log_dir from policy import get_independent_policy_for_rollout -def get_env_wrapper(): - return CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]) - -def get_agent_wrapper(): - epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) - epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - last_ep=config["num_episodes"], - **config["exploration"] - ) - return AgentWrapper( - policies=[get_independent_policy_for_rollout(i) for i in AGENT_IDS], - agent2policy={i: i for i in AGENT_IDS}, - exploration_dict={f"EpsilonGreedy": epsilon_greedy}, - agent2exploration={i: "EpsilonGreedy" for i in AGENT_IDS}, - log_dir=log_dir - ) - - if config["distributed"]["rollout_mode"] == "local": env = Env(**config["env"]["basic"]) rollout_manager = LocalRolloutManager( @@ -53,7 +34,7 @@ def get_agent_wrapper(): else: rollout_manager = MultiProcessRolloutManager( config["distributed"]["num_rollout_workers"], - get_env_wrapper, + lambda: CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]), get_agent_wrapper, num_steps=config["num_steps"], log_dir=log_dir, From 18f73f2c3f0ad429dcbb91e158a330dcbd417b47 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 22 Jun 2021 09:18:13 +0000 Subject: [PATCH 319/482] added policy version index and used it as a measure of experience staleness --- .../training/policy_manager/policy_manager.py | 30 ++++++-- maro/rl/training/sync/learner.py | 5 +- maro/rl/training/sync/rollout_manager.py | 75 +++++++++++-------- maro/rl/training/sync/rollout_worker.py | 13 ++-- maro/rl/wrappers/agent_wrapper.py | 2 +- 5 files changed, 82 insertions(+), 43 deletions(-) diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index e0f770e0a..2aae681e3 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -32,6 +32,12 @@ def __init__(self, policies: List[AbsPolicy]): super().__init__() self.policy_dict = {policy.name: policy for policy in policies} + self.updated = set(self.policy_dict.keys()) + self._version = 0 + + @property + def version(self): + return self._version @abstractmethod def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): @@ -39,7 +45,10 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): raise NotImplementedError def get_state(self): - return {policy_name: policy.get_state() for policy_name, policy in self.policy_dict.items()} + return {name: self.policy_dict[name].get_state() for name in self.updated} + + def reset_update_status(self): + self.updated.clear() class LocalPolicyManager(AbsPolicyManager): @@ -63,16 +72,15 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): policy's experience manager. Policies whose update conditions have been met will then be updated. """ t0 = time.time() - updated = [] for policy_name, exp in exp_by_policy.items(): if ( isinstance(self.policy_dict[policy_name], AbsCorePolicy) and self.policy_dict[policy_name].on_experiences(exp) ): - updated.append(policy_name) + self.updated.add(policy_name) - if updated: - self._logger.info(f"Updated policies {updated}") + if self.updated: + self._logger.info(f"Updated policies {self.updated}") self._logger.debug(f"policy update time: {time.time() - t0}") @@ -133,6 +141,10 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): result = conn.recv() for policy_name, policy_state in result["policy"].items(): self.policy_dict[policy_name].set_state(policy_state) + self.updated.add(policy_name) + + if self.updated: + self._version += 1 def exit(self): """Tell the trainer processes to exit.""" @@ -144,6 +156,7 @@ class MultiNodePolicyManager(AbsPolicyManager): """Policy manager that communicates with a set of remote nodes for parallel training. Args: + policies (List[AbsPolicy]): A list of policies managed by the manager. policy2trainer (dict): Mapping from policy names to trainer IDs. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` @@ -158,12 +171,13 @@ class MultiNodePolicyManager(AbsPolicyManager): """ def __init__( self, + policies: List[AbsPolicy], policy2trainer: Dict[str, str], group: str, log_dir: str = getcwd(), proxy_kwargs: dict = {} ): - super().__init__() + super().__init__(policies) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self.policy2trainer = policy2trainer self._trainer2policies = defaultdict(list) @@ -190,6 +204,10 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): self.policy_dict[policy_name].set_state(policy_state) + self.updated.add(policy_name) + + if self.updated: + self._version += 1 def exit(self): """Tell the remote trainers to exit.""" diff --git a/maro/rl/training/sync/learner.py b/maro/rl/training/sync/learner.py index 7842c14d9..794ce83ec 100644 --- a/maro/rl/training/sync/learner.py +++ b/maro/rl/training/sync/learner.py @@ -102,7 +102,10 @@ def _train(self, ep: int): while not self.rollout_manager.episode_complete: segment += 1 # experience collection - exp_by_policy = self.rollout_manager.collect(ep, segment, self.policy_manager.get_state()) + policy_state_dict = self.policy_manager.get_state() + self.policy_manager.reset_update_status() + policy_version = self.policy_manager.version + exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) t0 = time.time() self.policy_manager.on_experiences(exp_by_policy) total_policy_update_time += time.time() - t0 diff --git a/maro/rl/training/sync/rollout_manager.py b/maro/rl/training/sync/rollout_manager.py index 9c0a1b057..bf5a7640d 100644 --- a/maro/rl/training/sync/rollout_manager.py +++ b/maro/rl/training/sync/rollout_manager.py @@ -9,6 +9,8 @@ from random import choices from typing import Callable, Dict, List +from bleach import VERSION + from maro.communication import Proxy, SessionType from maro.rl.experience import ExperienceSet from maro.rl.exploration import AbsExploration @@ -27,13 +29,14 @@ def __init__(self): self.episode_complete = False @abstractmethod - def collect(self, ep: int, segment: int, policy_state_dict: dict): + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): """Collect simulation data, i.e., experiences for training. Args: ep (int): Current episode index. segment (int): Current segment index. policy_state_dict (dict): Policy states to use for simulation. + version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. Returns: Experiences for policy training. @@ -126,13 +129,14 @@ def __init__( self._num_steps = num_steps if num_steps > 0 else float("inf") self._log_env_summary = log_env_summary - def collect(self, ep: int, segment: int, policy_state_dict: dict): + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): """Collect simulation data, i.e., experiences for training. Args: ep (int): Current episode index. segment (int): Current segment index. policy_state_dict (dict): Policy states to use for simulation. + version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. Returns: Experiences for policy training. @@ -302,13 +306,14 @@ def __init__( self._worker_processes.append(worker) worker.start() - def collect(self, ep: int, segment: int, policy_state_dict: dict): + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): """Collect simulation data, i.e., experiences for training. Args: ep (int): Current episode index. segment (int): Current segment index. policy_state_dict (dict): Policy states to use for simulation. + version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. Returns: Experiences for policy training. @@ -325,6 +330,8 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): for conn in self._manager_ends: conn.send(rollout_req) + self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") + if self._exploration_step: self._exploration_step = False @@ -390,9 +397,9 @@ class MultiNodeRolloutManager(AbsRolloutManager): receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the workers. This This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the desired amount of data from workers. Defaults to None, in which case each receive attempt is blocking. - max_staleness (int): Maximum allowable staleness measured in the number of calls to ``collect``. Experiences - collected from calls to ``collect`` within ``max_staleness`` calls ago will be returned to the learner. - Defaults to 0, in which case only experiences from the latest call to ``collect`` will be returned. + max_lag (int): Maximum policy version lag allowed for experiences collected from remote roll-out workers. + Experiences collected using policy versions older than (current_version - max_lag) will be discarded. + Defaults to 0, in which case only experiences collected using the latest policy version will be returned. num_eval_workers (int): Number of workers for evaluation. Defaults to 1. log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of each episode. Defaults to True. @@ -409,7 +416,7 @@ def __init__( num_steps: int = -1, max_receive_attempts: int = None, receive_timeout: int = None, - max_staleness: int = 0, + max_lag: int = 0, num_eval_workers: int = 1, log_env_summary: bool = True, log_dir: str = getcwd(), @@ -434,7 +441,7 @@ def __init__( self.max_receive_attempts = max_receive_attempts self.receive_timeout = receive_timeout - self._max_staleness = max_staleness + self._max_lag = max_lag self.total_experiences_collected = 0 self.total_env_steps = 0 self._log_env_summary = log_env_summary @@ -443,13 +450,14 @@ def __init__( self._exploration_step = False - def collect(self, ep: int, segment: int, policy_state_dict: dict): + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): """Collect simulation data, i.e., experiences for training. Args: ep (int): Current episode index. segment (int): Current segment index. policy_state_dict (dict): Policy states to use for simulation. + version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. Returns: Experiences for policy training. @@ -462,11 +470,12 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): MsgKey.SEGMENT: segment, MsgKey.NUM_STEPS: self._num_steps, MsgKey.POLICY_STATE: policy_state_dict, + MsgKey.VERSION: version, MsgKey.EXPLORATION_STEP: self._exploration_step } self._proxy.ibroadcast("rollout_worker", MsgTag.COLLECT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent collect requests to {self._workers} for ep-{ep}, segment-{segment}") + self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") if self._exploration_step: self._exploration_step = False @@ -476,30 +485,36 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict): num_finishes = 0 for _ in range(self.max_receive_attempts): msg = self._proxy.receive_once(timeout=self.receive_timeout) - if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE] != ep: + if msg.tag != MsgTag.COLLECT_DONE: self._logger.info( - f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE]} " - f"(expected message type {MsgTag.COLLECT} and episode index {ep})" + f"Ignored a message of type {msg.tag} (expected message type {MsgTag.COLLECT_DONE})" + ) + continue + + if version - msg.body[MsgKey.VERSION] > self._max_lag: + self._logger.info( + f"Ignored a message because it contains experiences generated using a stale policy version. " + f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " + f"got {msg.body[MsgKey.VERSION]}" ) continue - if segment - msg.body[MsgKey.SEGMENT] <= self._max_staleness: - exp_by_policy = msg.body[MsgKey.EXPERIENCES] - self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) - self.total_env_steps += msg.body[MsgKey.NUM_STEPS] - - for policy_name, exp in exp_by_policy.items(): - combined_exp_by_policy[policy_name].extend(exp) - - if msg.body[MsgKey.SEGMENT] == segment: - self.episode_complete = msg.body[MsgKey.EPISODE_END] - if self.episode_complete: - # log roll-out summary - if self._log_env_summary: - self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") - num_finishes += 1 - if num_finishes == self.num_workers: - break + exp_by_policy = msg.body[MsgKey.EXPERIENCES] + self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) + self.total_env_steps += msg.body[MsgKey.NUM_STEPS] + + for policy_name, exp in exp_by_policy.items(): + combined_exp_by_policy[policy_name].extend(exp) + + if msg.body[MsgKey.SEGMENT] == segment: + self.episode_complete = msg.body[MsgKey.EPISODE_END] + if self.episode_complete: + # log roll-out summary + if self._log_env_summary: + self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") + num_finishes += 1 + if num_finishes == self.num_workers: + break if self.episode_complete: self._exploration_step = True diff --git a/maro/rl/training/sync/rollout_worker.py b/maro/rl/training/sync/rollout_worker.py index efb18940d..8278f86e1 100644 --- a/maro/rl/training/sync/rollout_worker.py +++ b/maro/rl/training/sync/rollout_worker.py @@ -5,6 +5,8 @@ from os import getcwd from typing import Callable +from bleach import VERSION + from maro.communication import Proxy from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger, set_seeds @@ -43,8 +45,7 @@ def rollout_worker_process( def collect(msg): ep, segment = msg["episode"], msg["segment"] # load policies - if hasattr(agent_wrapper, "update"): - agent_wrapper.update(msg["policy"]) + agent_wrapper.set_policy_states(msg["policy"]) # update exploration parameters agent_wrapper.explore() @@ -87,7 +88,7 @@ def evaluate(msg): eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state if hasattr(agent_wrapper, "update"): - agent_wrapper.update(msg["policy"]) + agent_wrapper.set_policy_states(msg["policy"]) while eval_env_wrapper.state: action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) @@ -140,7 +141,8 @@ def collect(msg): # load policies if msg.body[MsgKey.POLICY_STATE]: - agent_wrapper.update(msg.body[MsgKey.POLICY_STATE]) + agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) + # set exploration parameters agent_wrapper.explore() if msg.body[MsgKey.EXPLORATION_STEP]: @@ -170,6 +172,7 @@ def collect(msg): MsgKey.EPISODE_END: not env_wrapper.state, MsgKey.EPISODE: ep, MsgKey.SEGMENT: segment, + MsgKey.VERSION: msg.body[MsgKey.VERSION], MsgKey.EXPERIENCES: ret_exp, MsgKey.ENV_SUMMARY: env_wrapper.summary, MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1 @@ -183,7 +186,7 @@ def evaluate(msg): eval_env_wrapper.start() # get initial state agent_wrapper.exploit() if hasattr(agent_wrapper, "update"): - agent_wrapper.update(msg.body[MsgKey.POLICY_STATE]) + agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) while eval_env_wrapper.state: action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) diff --git a/maro/rl/wrappers/agent_wrapper.py b/maro/rl/wrappers/agent_wrapper.py index cfd38aae3..0a33be8bc 100644 --- a/maro/rl/wrappers/agent_wrapper.py +++ b/maro/rl/wrappers/agent_wrapper.py @@ -69,7 +69,7 @@ def get_experiences_by_policy(self, policy_names: List[str]): """Get experiences by policy names.""" return {name: self.policy_dict[name].experience_manager.get() for name in policy_names} - def update(self, policy_state_dict: dict): + def set_policy_states(self, policy_state_dict: dict): """Update policy states.""" for policy_id, policy_state in policy_state_dict.items(): self.policy_dict[policy_id].set_state(policy_state) From 49d93c22624c7c05b6af797be20d2687b197809e Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 22 Jun 2021 09:22:39 +0000 Subject: [PATCH 320/482] lint issue fix --- maro/rl/training/policy_manager/policy_manager.py | 2 +- maro/rl/training/sync/rollout_manager.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index 2aae681e3..094b8e40a 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -46,7 +46,7 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): def get_state(self): return {name: self.policy_dict[name].get_state() for name in self.updated} - + def reset_update_status(self): self.updated.clear() diff --git a/maro/rl/training/sync/rollout_manager.py b/maro/rl/training/sync/rollout_manager.py index bf5a7640d..fde175545 100644 --- a/maro/rl/training/sync/rollout_manager.py +++ b/maro/rl/training/sync/rollout_manager.py @@ -9,8 +9,6 @@ from random import choices from typing import Callable, Dict, List -from bleach import VERSION - from maro.communication import Proxy, SessionType from maro.rl.experience import ExperienceSet from maro.rl.exploration import AbsExploration @@ -494,7 +492,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if version - msg.body[MsgKey.VERSION] > self._max_lag: self._logger.info( f"Ignored a message because it contains experiences generated using a stale policy version. " - f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " + f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " f"got {msg.body[MsgKey.VERSION]}" ) continue From bc96c5ec8b40193e414930c5162441d2b75fe6ce Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 22 Jun 2021 09:29:26 +0000 Subject: [PATCH 321/482] lint issue fix --- maro/rl/training/sync/rollout_worker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/maro/rl/training/sync/rollout_worker.py b/maro/rl/training/sync/rollout_worker.py index 8278f86e1..346309131 100644 --- a/maro/rl/training/sync/rollout_worker.py +++ b/maro/rl/training/sync/rollout_worker.py @@ -5,8 +5,6 @@ from os import getcwd from typing import Callable -from bleach import VERSION - from maro.communication import Proxy from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger, set_seeds From 1bb4b5634df161e45089439d24c81f6c17cc7c20 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 22 Jun 2021 11:00:31 +0000 Subject: [PATCH 322/482] switched log_dir and proxy_kwargs order --- maro/rl/training/policy_manager/policy_manager.py | 8 ++++---- maro/rl/training/policy_manager/trainer.py | 8 ++++---- maro/rl/training/sync/rollout_manager.py | 8 ++++---- maro/rl/training/sync/rollout_worker.py | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index 094b8e40a..5d6550b54 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -163,19 +163,19 @@ class MultiNodePolicyManager(AbsPolicyManager): instance. group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. """ def __init__( self, policies: List[AbsPolicy], policy2trainer: Dict[str, str], group: str, - log_dir: str = getcwd(), - proxy_kwargs: dict = {} + proxy_kwargs: dict = {}, + log_dir: str = getcwd() ): super().__init__(policies) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py index f8a9f2abb..966cf1c7c 100644 --- a/maro/rl/training/policy_manager/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -56,8 +56,8 @@ def trainer_node( trainer_id: str, create_policy_func_dict: Dict[str, Callable], group: str, - log_dir: str = getcwd(), - proxy_kwargs: dict = {} + proxy_kwargs: dict = {}, + log_dir: str = getcwd() ): """Policy trainer process that can be launched on separate computation nodes. @@ -68,9 +68,9 @@ def trainer_node( instance. group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. - log_dir (str): Directory to store logs in. Defaults to the current working directory. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. """ policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=trainer_id, **proxy_kwargs) diff --git a/maro/rl/training/sync/rollout_manager.py b/maro/rl/training/sync/rollout_manager.py index fde175545..c9f420746 100644 --- a/maro/rl/training/sync/rollout_manager.py +++ b/maro/rl/training/sync/rollout_manager.py @@ -401,11 +401,11 @@ class MultiNodeRolloutManager(AbsRolloutManager): num_eval_workers (int): Number of workers for evaluation. Defaults to 1. log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of each episode. Defaults to True. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. """ def __init__( self, @@ -417,8 +417,8 @@ def __init__( max_lag: int = 0, num_eval_workers: int = 1, log_env_summary: bool = True, - log_dir: str = getcwd(), - proxy_kwargs: dict = {} + proxy_kwargs: dict = {}, + log_dir: str = getcwd() ): if num_eval_workers > num_workers: raise ValueError("num_eval_workers cannot exceed the number of available workers") diff --git a/maro/rl/training/sync/rollout_worker.py b/maro/rl/training/sync/rollout_worker.py index 346309131..1629d68fd 100644 --- a/maro/rl/training/sync/rollout_worker.py +++ b/maro/rl/training/sync/rollout_worker.py @@ -108,8 +108,8 @@ def rollout_worker_node( create_agent_wrapper_func: Callable[[], AgentWrapper], group: str, create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, - log_dir: str = getcwd(), - proxy_kwargs: dict = {} + proxy_kwargs: dict = {}, + log_dir: str = getcwd() ): """Roll-out worker process that can be launched on separate computation nodes. @@ -123,9 +123,9 @@ def rollout_worker_node( create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation. - log_dir (str): Directory to store logs in. Defaults to the current working directory. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. """ env_wrapper = create_env_wrapper_func() eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() From 20c63857a085c092290622e231bbd30ad8104ecd Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 23 Jun 2021 15:27:58 +0000 Subject: [PATCH 323/482] cim example refinement --- examples/cim/dqn/config.yml | 16 +++++++++------- examples/cim/dqn/general.py | 2 +- examples/cim/dqn/policy_manager.py | 14 +++++++++++--- examples/cim/dqn/sync_mode/rollout_manager.py | 4 ++-- maro/rl/training/__init__.py | 2 +- maro/rl/training/message_enums.py | 2 ++ .../rl/training/policy_manager/policy_manager.py | 2 +- maro/rl/training/policy_manager/trainer.py | 9 ++++----- .../rl/training/{sync => sync_tools}/__init__.py | 0 .../{sync => sync_tools}/early_stopper.py | 0 maro/rl/training/{sync => sync_tools}/learner.py | 0 .../{sync => sync_tools}/local_learner.py | 0 .../{sync => sync_tools}/rollout_manager.py | 0 .../{sync => sync_tools}/rollout_worker.py | 8 ++++---- 14 files changed, 35 insertions(+), 24 deletions(-) rename maro/rl/training/{sync => sync_tools}/__init__.py (100%) rename maro/rl/training/{sync => sync_tools}/early_stopper.py (100%) rename maro/rl/training/{sync => sync_tools}/learner.py (100%) rename maro/rl/training/{sync => sync_tools}/local_learner.py (100%) rename maro/rl/training/{sync => sync_tools}/rollout_manager.py (100%) rename maro/rl/training/{sync => sync_tools}/rollout_worker.py (97%) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 90c4b0f8b..38b2c15eb 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -80,13 +80,15 @@ policies: beta_step: 0.001 update_trigger: 16 warmup: 1 -distributed: - rollout_mode: multi-process # local, multi-process - policy_training_mode: multi-process # local, multi-process - group: cim-dqn +sync: + rollout_mode: multi-process # single-process, multi-process, multi-node num_rollout_workers: 3 - num_trainers: 2 - redis_host: localhost - redis_port: 6379 # max_receive_attempts: 2 # receive_timeout: 100 # in milli-seconds +policy_manager: + group: cim-dqn-policy-manager + policy_training_mode: multi-process # single-process, multi-process, multi-node + num_trainers: 2 +redis: + host: localhost + port: 6379 \ No newline at end of file diff --git a/examples/cim/dqn/general.py b/examples/cim/dqn/general.py index 1087f31d3..74e2303f9 100644 --- a/examples/cim/dqn/general.py +++ b/examples/cim/dqn/general.py @@ -25,4 +25,4 @@ NUM_ACTIONS = config["env"]["wrapper"]["num_actions"] AGENT_IDS = Env(**config["env"]["basic"]).agent_idx_list -NUM_POLICY_TRAINERS = config["distributed"]["num_trainers"] +NUM_POLICY_TRAINERS = config["policy_manager"]["num_trainers"] diff --git a/examples/cim/dqn/policy_manager.py b/examples/cim/dqn/policy_manager.py index cc8ff5126..27ba817b1 100644 --- a/examples/cim/dqn/policy_manager.py +++ b/examples/cim/dqn/policy_manager.py @@ -4,7 +4,7 @@ import os import sys -from maro.rl import LocalPolicyManager, MultiProcessPolicyManager +from maro.rl import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory cim_path = os.path.dirname(dqn_path) # CIM example directory @@ -15,12 +15,20 @@ policies = [get_independent_policy_for_training(i) for i in AGENT_IDS] -if config["distributed"]["policy_training_mode"] == "local": +if config["policy_manager"]["policy_training_mode"] == "single-process": policy_manager = LocalPolicyManager(policies, log_dir=log_dir) -else: +elif config["policy_manager"]["policy_training_mode"] == "multi-process": policy_manager = MultiProcessPolicyManager( policies, {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping {i: get_independent_policy_for_training for i in AGENT_IDS}, log_dir=log_dir ) +elif config["policy_manager"]["policy_training_mode"] == "multi-node": + policy_manager = MultiNodePolicyManager( + policies, + {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping + config["policy_manager"]["group"], + proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + log_dir=log_dir + ) diff --git a/examples/cim/dqn/sync_mode/rollout_manager.py b/examples/cim/dqn/sync_mode/rollout_manager.py index acc1caa49..d99f71e57 100644 --- a/examples/cim/dqn/sync_mode/rollout_manager.py +++ b/examples/cim/dqn/sync_mode/rollout_manager.py @@ -20,7 +20,7 @@ from policy import get_independent_policy_for_rollout -if config["distributed"]["rollout_mode"] == "local": +if config["sync"]["rollout_mode"] == "single-process": env = Env(**config["env"]["basic"]) rollout_manager = LocalRolloutManager( CIMEnvWrapper(env, **config["env"]["wrapper"]), @@ -33,7 +33,7 @@ ) else: rollout_manager = MultiProcessRolloutManager( - config["distributed"]["num_rollout_workers"], + config["sync"]["num_rollout_workers"], lambda: CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]), get_agent_wrapper, num_steps=config["num_steps"], diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 2e72381bc..bcea443d0 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -5,7 +5,7 @@ AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, trainer_node, trainer_process ) -from .sync import ( +from .sync_tools import ( AbsEarlyStopper, AbsRolloutManager, Learner, LocalLearner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager, rollout_worker_node, rollout_worker_process ) diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py index f089ca4f3..d55676ec2 100644 --- a/maro/rl/training/message_enums.py +++ b/maro/rl/training/message_enums.py @@ -8,6 +8,8 @@ class MsgTag(Enum): COLLECT = "rollout" EVAL = "eval" INIT_POLICY_STATE = "init_policy_state" + INIT_POLICY_STATE_DONE = "init_policy_state_done" + GET_INITIAL_POLICY_STATE = "get_initial_policy_state" POLICY_STATE = "policy_state" CHOOSE_ACTION = "choose_action" ACTION = "action" diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index 5d6550b54..d59c27c14 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -188,7 +188,7 @@ def __init__( for trainer_name, policy_names in self._trainer2policies.items(): self._proxy.send( SessionMessage( - MsgTag.POLICY_STATE, self._proxy.name, + MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} ) ) diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py index 966cf1c7c..821dae776 100644 --- a/maro/rl/training/policy_manager/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -53,7 +53,6 @@ def trainer_process( def trainer_node( - trainer_id: str, create_policy_func_dict: Dict[str, Callable], group: str, proxy_kwargs: dict = {}, @@ -62,7 +61,6 @@ def trainer_node( """Policy trainer process that can be launched on separate computation nodes. Args: - trainer_id (str): Identifier for the trainer process for bookkeeping by the parent manager process. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. @@ -72,8 +70,8 @@ def trainer_node( for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ - policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} - proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=trainer_id, **proxy_kwargs) + policy_dict = {policy_name: func(policy_name) for policy_name, func in create_policy_func_dict.items()} + proxy = Proxy(group, "trainer", {"policy_manager": 1}, **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) for msg in proxy.receive(): @@ -85,7 +83,8 @@ def trainer_node( if msg.tag == MsgTag.INIT_POLICY_STATE: for name, state in msg.body[MsgKey.POLICY_STATE].items(): policy_dict[name].set_state(state) - logger.info(f"Trainer {trainer_id} initialized policy {name}") + logger.info(f"{proxy.name} initialized policy {name}") + proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) elif msg.tag == MsgTag.TRAIN: t0 = time.time() msg_body = { diff --git a/maro/rl/training/sync/__init__.py b/maro/rl/training/sync_tools/__init__.py similarity index 100% rename from maro/rl/training/sync/__init__.py rename to maro/rl/training/sync_tools/__init__.py diff --git a/maro/rl/training/sync/early_stopper.py b/maro/rl/training/sync_tools/early_stopper.py similarity index 100% rename from maro/rl/training/sync/early_stopper.py rename to maro/rl/training/sync_tools/early_stopper.py diff --git a/maro/rl/training/sync/learner.py b/maro/rl/training/sync_tools/learner.py similarity index 100% rename from maro/rl/training/sync/learner.py rename to maro/rl/training/sync_tools/learner.py diff --git a/maro/rl/training/sync/local_learner.py b/maro/rl/training/sync_tools/local_learner.py similarity index 100% rename from maro/rl/training/sync/local_learner.py rename to maro/rl/training/sync_tools/local_learner.py diff --git a/maro/rl/training/sync/rollout_manager.py b/maro/rl/training/sync_tools/rollout_manager.py similarity index 100% rename from maro/rl/training/sync/rollout_manager.py rename to maro/rl/training/sync_tools/rollout_manager.py diff --git a/maro/rl/training/sync/rollout_worker.py b/maro/rl/training/sync_tools/rollout_worker.py similarity index 97% rename from maro/rl/training/sync/rollout_worker.py rename to maro/rl/training/sync_tools/rollout_worker.py index 1629d68fd..32519021b 100644 --- a/maro/rl/training/sync/rollout_worker.py +++ b/maro/rl/training/sync_tools/rollout_worker.py @@ -63,8 +63,8 @@ def collect(msg): steps_to_go -= 1 logger.info( - f"Roll-out finished for ep {ep}, segment {segment}" - f"(steps {starting_step_index} - {env_wrapper.step_index})" + f"Roll-out finished (episode {ep}, segment {segment}, " + f"steps {starting_step_index} - {env_wrapper.step_index})" ) policies_with_new_exp = agent_wrapper.on_experiences(env_wrapper.get_experiences()) @@ -159,8 +159,8 @@ def collect(msg): steps_to_go -= 1 logger.info( - f"Roll-out finished for ep {ep}, segment {segment}" - f"(steps {starting_step_index} - {env_wrapper.step_index})" + f"Roll-out finished (episode {ep}, segment {segment}, " + f"steps {starting_step_index} - {env_wrapper.step_index})" ) policy_names = agent_wrapper.on_experiences(env_wrapper.get_experiences()) From 42c24ab86bbeabbeb53cb6ad5113499114a9d660 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 28 Jun 2021 04:03:36 +0000 Subject: [PATCH 324/482] eval schedule sorted only when it's a list --- maro/rl/training/sync_tools/learner.py | 10 +++---- maro/rl/training/sync_tools/local_learner.py | 28 +++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/maro/rl/training/sync_tools/learner.py b/maro/rl/training/sync_tools/learner.py index 794ce83ec..5f149135c 100644 --- a/maro/rl/training/sync_tools/learner.py +++ b/maro/rl/training/sync_tools/learner.py @@ -59,11 +59,11 @@ def __init__( elif isinstance(eval_schedule, int): num_eval_schedule = num_episodes // eval_schedule eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - - self._eval_schedule = eval_schedule - self._eval_schedule.sort() - if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - self._eval_schedule.append(num_episodes) + else: + self._eval_schedule = eval_schedule + self._eval_schedule.sort() + if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: + self._eval_schedule.append(num_episodes) self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") self._eval_point_index = 0 diff --git a/maro/rl/training/sync_tools/local_learner.py b/maro/rl/training/sync_tools/local_learner.py index a7c34a971..c54f5b3ee 100644 --- a/maro/rl/training/sync_tools/local_learner.py +++ b/maro/rl/training/sync_tools/local_learner.py @@ -53,7 +53,7 @@ def __init__( if num_steps == 0 or num_steps < -1: raise ValueError("num_steps must be a positive integer or -1") - self._logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) + self.logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) self.env = env_wrapper self.eval_env = eval_env if eval_env else self.env self.agent = agent_wrapper @@ -63,15 +63,17 @@ def __init__( # evaluation schedule if eval_schedule is None: - eval_schedule = [] + self.eval_schedule = [] elif isinstance(eval_schedule, int): num_eval_schedule = num_episodes // eval_schedule - eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - - self._eval_schedule = eval_schedule - self._eval_schedule.sort() - if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - self._eval_schedule.append(num_episodes) + self.eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + else: + self._eval_schedule = eval_schedule + self._eval_schedule.sort() + if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: + self._eval_schedule.append(num_episodes) + + self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") self._eval_point_index = 0 self.early_stopper = early_stopper @@ -110,9 +112,9 @@ def _train(self, ep: int): # performance details if self._log_env_summary: - self._logger.info(f"ep {ep}: {self.env.summary}") + self.logger.info(f"ep {ep}: {self.env.summary}") - self._logger.info( + self.logger.info( f"ep {ep} summary - " f"running time: {time.time() - t0} " f"env steps: {self.env.step_index} " @@ -121,7 +123,7 @@ def _train(self, ep: int): def _evaluate(self): """Policy evaluation.""" - self._logger.info("Evaluating...") + self.logger.info("Evaluating...") self.agent.exploit() self.eval_env.reset() self.eval_env.start() # get initial state @@ -129,7 +131,7 @@ def _evaluate(self): self.eval_env.step(self.agent.choose_action(self.eval_env.state)) # performance details - self._logger.info(f"Evaluation result: {self.eval_env.summary}") + self.logger.info(f"Evaluation result: {self.eval_env.summary}") def _collect(self, ep, segment): start_step_index = self.env.step_index + 1 @@ -138,7 +140,7 @@ def _collect(self, ep, segment): self.env.step(self.agent.choose_action(self.env.state)) steps_to_go -= 1 - self._logger.info( + self.logger.info( f"Roll-out finished for ep {ep}, segment {segment}" f"(steps {start_step_index} - {self.env.step_index})" ) From 8db90d51033759dabd1606f49a69aa9f6ba1e6aa Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 28 Jun 2021 04:05:05 +0000 Subject: [PATCH 325/482] eval schedule sorted only when it's a list --- maro/rl/training/sync_tools/learner.py | 4 ++-- maro/rl/training/sync_tools/local_learner.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/maro/rl/training/sync_tools/learner.py b/maro/rl/training/sync_tools/learner.py index 5f149135c..c81cffb7e 100644 --- a/maro/rl/training/sync_tools/learner.py +++ b/maro/rl/training/sync_tools/learner.py @@ -55,10 +55,10 @@ def __init__( # evaluation schedule if eval_schedule is None: - eval_schedule = [] + self._eval_schedule = [] elif isinstance(eval_schedule, int): num_eval_schedule = num_episodes // eval_schedule - eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] else: self._eval_schedule = eval_schedule self._eval_schedule.sort() diff --git a/maro/rl/training/sync_tools/local_learner.py b/maro/rl/training/sync_tools/local_learner.py index c54f5b3ee..0a1c2dae1 100644 --- a/maro/rl/training/sync_tools/local_learner.py +++ b/maro/rl/training/sync_tools/local_learner.py @@ -63,10 +63,10 @@ def __init__( # evaluation schedule if eval_schedule is None: - self.eval_schedule = [] + self._eval_schedule = [] elif isinstance(eval_schedule, int): num_eval_schedule = num_episodes // eval_schedule - self.eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] else: self._eval_schedule = eval_schedule self._eval_schedule.sort() From 81f574a07d0ec71ea0a7a64c10220c9da1cf86f9 Mon Sep 17 00:00:00 2001 From: "Wang.Jinyu" Date: Mon, 28 Jun 2021 08:55:43 +0000 Subject: [PATCH 326/482] update sc env wrapper --- examples/supply_chain/env_wrapper-2.py | 1187 +++++++++++++++++ examples/supply_chain/env_wrapper.py | 99 +- .../supply_chain/or_policy/minmax_policy.py | 2 +- 3 files changed, 1240 insertions(+), 48 deletions(-) create mode 100644 examples/supply_chain/env_wrapper-2.py diff --git a/examples/supply_chain/env_wrapper-2.py b/examples/supply_chain/env_wrapper-2.py new file mode 100644 index 000000000..f32277c3e --- /dev/null +++ b/examples/supply_chain/env_wrapper-2.py @@ -0,0 +1,1187 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict, namedtuple +from typing import List + +import scipy.stats as st +import numpy as np + +from maro.rl import AbsEnvWrapper +from maro.simulator import Env +from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction + +# from exploration import exploration_dict, agent2exploration +# from learner import SCLearner + +def stock_constraint(f_state): + return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean'] + + +def is_replenish_constraint(f_state): + return f_state['consumption_hist'][-1] > 0 + + +def low_profit(f_state): + return (f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000 + + +def low_stock_constraint(f_state): + return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean'] + + +def out_of_stock(f_state): + return 0 < f_state['inventory_in_stock'] + + +atoms = { + 'stock_constraint': stock_constraint, + 'is_replenish_constraint': is_replenish_constraint, + 'low_profit': low_profit, + 'low_stock_constraint': low_stock_constraint, + 'out_of_stock': out_of_stock +} + +# State extracted. +keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'consumption_hist']), + ('storage_capacity', ['storage_utilization']), + ('sale_mean', ['sale_std', + 'sale_hist', + 'pending_order', + 'inventory_in_stock', + 'inventory_in_transit', + 'inventory_estimated', + 'inventory_rop']), + ('max_price', ['sku_price', 'sku_cost'])] + +# Sku related agent types +sku_agent_types = {"consumer", "consumerstore", "producer", "product", "productstore"} + + +class UnitBaseInfo: + id: int = None + node_index: int = None + config: dict = None + summary: dict = None + + def __init__(self, unit_summary): + self.id = unit_summary["id"] + self.node_index = unit_summary["node_index"] + self.config = unit_summary.get("config", {}) + self.summary = unit_summary + + def __getitem__(self, key, default=None): + if key in self.summary: + return self.summary[key] + + return default + + +distribution_features = ("remaining_order_quantity", "remaining_order_number") +seller_features = ("total_demand", "sold", "demand") + + +class SCEnvWrapper(AbsEnvWrapper): + def __init__(self, env: Env, reward_eval_delay: int=0, save_replay: bool=True, replay_agent_ids: list=None): + super().__init__(env, reward_eval_delay, save_replay, replay_agent_ids) + self.balance_cal = BalanceSheetCalculator(env) + self.cur_balance_sheet_reward = None + self.storage_ss = env.snapshot_list["storage"] + self.distribution_ss = env.snapshot_list["distribution"] + self.consumer_ss = env.snapshot_list["consumer"] + self.seller_ss = env.snapshot_list["seller"] + + self._summary = env.summary['node_mapping'] + self._configs = env.configs + self._agent_types = self._summary["agent_types"] + self._units_mapping = self._summary["unit_mapping"] + self._agent_list = env.agent_idx_list + + self._sku_number = len(self._summary["skus"]) + 1 + self._max_price = self._summary["max_price"] + self._max_sources_per_facility = self._summary["max_sources_per_facility"] + + # state for each tick + self._cur_metrics = env.metrics + + # cache for ppf value. + self._service_index_ppf_cache = {} + + # facility -> { + # data_model_index:int, + # storage:UnitBaseInfo, + # distribution: UnitBaseInfo, + # sku_id: { + # skuproduct: UnitBaseInfo, + # consumer: UnitBaseInfo, + # seller: UnitBaseInfo, + # manufacture: UnitBaseInfo + # } + # } + self.facility_levels = {} + + # unit id -> (facility id) + self.unit_2_facility_dict = {} + + # our raw state + self._states = {} + + # facility id -> storage index + self._facility2storage_index_dict = {} + + # facility id -> product id -> number + self._storage_product_numbers = {} + + # facility id -> product_id -> index + self._storage_product_indices = {} + + # facility id -> storage product utilization + self._facility_product_utilization = {} + + # facility id -> in_transit_orders + self._facility_in_transit_orders = {} + + # current distribution states + self._cur_distribution_states = None + + # current consumer states + self._cur_consumer_states = None + + # current seller states + self._cur_seller_states = None + + # dim for state + self._dim = None + + # use this to quick find relationship between units (consumer, manufacture, seller or product) and product unit. + # unit id -> (product unit id, facility id, seller id, consumer id, manufacture id) + self._unit2product_mapping = {} + + # agent (unit id) -> AgentInfo + self._agent_id2info_mapping = {} + + # built internal helpers. + self._build_internal_helpers() + + self.stock_status = {} + self.demand_status = {} + # key: product unit id, value: number + self.orders_from_downstreams = {} + self.consumer_orders = {} + self.order_in_transit_status = {} + self.order_to_distribute_status = {} + + @property + def dim(self): + """Calculate dim per shape.""" + if self._dim is None: + self._dim = 0 + + first_state = next(iter(self._states.values())) + + for _, state_keys in keys_in_state: + for key in state_keys: + val = first_state[key] + + if type(val) == list: + self._dim += len(val) + else: + self._dim += 1 + + return self._dim + + def get_or_policy_state(self, state, agent_info): + state = {'is_facility': not (agent_info.agent_type in sku_agent_types)} + if agent_info.is_facility: + return state + + product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id + id, product_id, _, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ + self.balance_cal.products[self.balance_cal.product_id2index_dict[product_unit_id]] + + product_metrics = self._cur_metrics["products"][product_unit_id] + state['sale_mean'] = product_metrics["sale_mean"] + state['sale_std'] = product_metrics["sale_std"] + + facility = self.facility_levels[agent_info.facility_id] + state['unit_storage_cost'] = unit_storage_cost + state['order_cost'] = 1 + product_info = facility[agent_info.sku.id] + if "consumer" in product_info: + consumer_index = product_info["consumer"].node_index + state['order_cost'] = self.consumer_ss[self.env.tick:consumer_index:"order_cost"].flatten()[0] + state['storage_capacity'] = facility['storage'].config["capacity"] + state['storage_levels'] = self._storage_product_numbers[agent_info.facility_id] + state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] + state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 + state['vlt'] = agent_info.sku.vlt + state['service_level'] = agent_info.sku.service_level + return state + + def get_rl_policy_state(self, state, agent_info): + self._update_facility_features(state, agent_info) + self._update_storage_features(state, agent_info) + # bom do not need to update + # self._add_bom_features(state, agent_info) + self._update_distribution_features(state, agent_info) + self._update_sale_features(state, agent_info) + # vlt do not need to update + # self._update_vlt_features(state, agent_info) + self._update_consumer_features(state, agent_info) + # self._add_price_features(state, agent_info) + self._update_global_features(state) + + self.stock_status[agent_info.id] = state['inventory_in_stock'] + + self.demand_status[agent_info.id] = state['sale_hist'][-1] + + self.order_in_transit_status[agent_info.id] = state['inventory_in_transit'] + + self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] + + self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} + self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} + + np_state = self._serialize_state(state) + return np_state + + def get_state(self, event): + cur_tick = self.env.tick + settings: dict = self.env.configs.settings + consumption_hist_len = settings['consumption_hist_len'] + hist_len = settings['sale_hist_len'] + consumption_ticks = [cur_tick - i for i in range(consumption_hist_len-1, -1, -1)] + hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] + + self.cur_balance_sheet_reward = self.balance_cal.calc() + self._cur_metrics = self.env.metrics + + self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features] \ + .flatten() \ + .reshape(-1, len(distribution_features)) \ + .astype(np.int) + + self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"] \ + .flatten() \ + .reshape(-1, len(self.consumer_ss)) + + self._cur_seller_states = self.seller_ss[hist_ticks::seller_features] \ + .astype(np.int) + + + # facility level states + for facility_id in self._facility_product_utilization: + # reset for each step + self._facility_product_utilization[facility_id] = 0 + + in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] + + self._facility_in_transit_orders[facility_id] = [0] * self._sku_number + + for sku_id, number in in_transit_orders.items(): + self._facility_in_transit_orders[facility_id][sku_id] = number + + final_state = {} + + # calculate storage info first, then use it later to speed up. + for facility_id, storage_index in self._facility2storage_index_dict.items(): + product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"] \ + .flatten() \ + .astype(np.int) + + for pid, index in self._storage_product_indices[facility_id].items(): + product_number = product_numbers[index] + + self._storage_product_numbers[facility_id][pid] = product_number + self._facility_product_utilization[facility_id] += product_number + + for agent_info in self._agent_list: + state = self._states[agent_info.id] + + # storage_index = self._facility2storage_index_dict[agent_info.facility_id] + + np_state = self.get_rl_policy_state(state, agent_info) + if agent_info.agent_type in ["consumer", "producer"]: + np_state = self.get_or_policy_state(state, agent_info) + + # agent_info.agent_type -> policy + final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state + + return final_state + + def get_reward(self, tick=None, target_agents=None): + # get related product, seller, consumer, manufacture unit id + # NOTE: this mapping does not contain facility id, so if id is not exist, then means it is a facility + # product_unit_id, facility_id, seller_id, consumer_id, producer_id = self._unit2product_mapping[id] + # return { + # f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}": np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) + # for f_id, bwt in self.cur_balance_sheet_reward.items() + # } + self.cur_balance_sheet_reward = self.balance_cal.calc() + rewards = defaultdict(float) + for f_id, bwt in self.cur_balance_sheet_reward.items(): + agent = self._agent_id2info_mapping[f_id] + if agent.agent_type == 'consumerstore': + rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) + else: + rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = 0 + + return rewards + + def to_env_action(self, action_by_agent): + # cache the sources for each consumer if not yet cached + if not hasattr(self, "consumer2source"): + self.consumer2source, self.consumer2product = {}, {} + for facility in self.env.summary["node_mapping"]["facilities"].values(): + products = facility["units"]["products"] + for product_id, product in products.items(): + consumer = product["consumer"] + if consumer is not None: + consumer_id = consumer["id"] + self.consumer2source[consumer_id] = consumer["sources"] + self.consumer2product[consumer_id] = product_id + + env_action = {} + for agent_id, action in action_by_agent.items(): + unit_id = int(agent_id.split(".")[1]) + + is_facility = unit_id not in self._units_mapping + + # ignore facility to reduce action number + if is_facility: + continue + + # consumer action + if agent_id.startswith("consumer"): + product_id = self.consumer2product.get(unit_id, 0) + sources = self.consumer2source.get(unit_id, []) + if sources: + source_id = sources[0] + product_unit_id = self._unit2product_mapping[unit_id][0] + action_number = int(int(action) * self._cur_metrics["products"][product_unit_id]["sale_mean"]) + + # ignore 0 quantity to reduce action number + if action_number == 0: + continue + + sku = self._units_mapping[unit_id][3] + + reward_discount = 1 + + env_action[unit_id] = ConsumerAction( + unit_id, + product_id, + source_id, + action_number, + sku.vlt, + reward_discount + ) + + self.consumer_orders[product_unit_id] = action_number + self.orders_from_downstreams[self.facility_levels[source_id][product_id]["skuproduct"].id] = action_number + + # manufacturer action + elif agent_id.startswith("producer"): + sku = self._units_mapping[unit_id][3] + action = sku.production_rate + + # ignore invalid actions + if action is None or action == 0: + continue + + env_action[unit_id] = ManufactureAction(unit_id, action) + + return env_action + + def _update_facility_features(self, state, agent_info): + state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 + + def _update_storage_features(self, state, agent_info): + facility_id = agent_info.facility_id + state['storage_utilization'] = 0 + + state['storage_levels'] = self._storage_product_numbers[facility_id] + state['storage_utilization'] = self._facility_product_utilization[facility_id] + + def _update_sale_features(self, state, agent_info): + if agent_info.agent_type not in sku_agent_types: + return + + # Get product unit id for current agent. + product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id + + product_metrics = self._cur_metrics["products"][product_unit_id] + + state['sale_mean'] = product_metrics["sale_mean"] + state['sale_std'] = product_metrics["sale_std"] + + facility = self.facility_levels[agent_info.facility_id] + product_info = facility[agent_info.sku.id] + + if "seller" not in product_info: + # TODO: why gamma sale as mean? + state['sale_gamma'] = state['sale_mean'] + + if "consumer" in product_info: + consumer_index = product_info["consumer"].node_index + + state['consumption_hist'] = list( + self._cur_consumer_states[:, consumer_index]) + state['pending_order'] = list( + product_metrics["pending_order_daily"]) + + if "seller" in product_info: + seller_index = product_info["seller"].node_index + + seller_states = self._cur_seller_states[:, seller_index, :] + + # For total demand, we need latest one. + state['total_backlog_demand'] = seller_states[:, 0][-1][0] + state['sale_hist'] = list(seller_states[:, 1].flatten()) + state['backlog_demand_hist'] = list(seller_states[:, 2]) + + def _update_distribution_features(self, state, agent_info): + facility = self.facility_levels[agent_info.facility_id] + distribution = facility.get("distribution", None) + + if distribution is not None: + dist_states = self._cur_distribution_states[distribution.node_index] + state['distributor_in_transit_orders'] = dist_states[1] + state['distributor_in_transit_orders_qty'] = dist_states[0] + + def _update_consumer_features(self, state, agent_info): + if agent_info.is_facility: + return + + facility = self.facility_levels[agent_info.facility_id] + product_info = facility[agent_info.sku.id] + + # if "consumer" not in product_info: + # return + + state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] + + # FIX: we need plus 1 to this, as it is 0 based index, but we already aligned with 1 more + # slot to use sku id as index ( 1 based). + product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 + state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] + state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] + + pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] + + if pending_order is not None: + state['inventory_in_distribution'] = pending_order[agent_info.sku.id] + + state['inventory_estimated'] = (state['inventory_in_stock'] + + state['inventory_in_transit'] + - state['inventory_in_distribution']) + if state['inventory_estimated'] >= 0.5 * state['storage_capacity']: + state['is_over_stock'] = 1 + + if state['inventory_estimated'] <= 0: + state['is_out_of_stock'] = 1 + + service_index = state['service_level'] + + if service_index not in self._service_index_ppf_cache: + self._service_index_ppf_cache[service_index] = st.norm.ppf( + service_index) + + ppf = self._service_index_ppf_cache[service_index] + + state['inventory_rop'] = (state['max_vlt'] * state['sale_mean'] + + np.sqrt(state['max_vlt']) * state['sale_std'] * ppf) + + if state['inventory_estimated'] < state['inventory_rop']: + state['is_below_rop'] = 1 + + def _update_global_features(self, state): + state["global_time"] = self.env.tick + + def _serialize_state(self, state): + result = [] + + for norm, fields in keys_in_state: + for field in fields: + vals = state[field] + if not isinstance(vals, list): + vals = [vals] + if norm is not None: + vals = [max(0.0, min(20.0, x / (state[norm] + 0.01))) + for x in vals] + result.extend(vals) + + return np.asarray(result, dtype=np.float32) + + def _build_internal_helpers(self): + for agent_info in self.env.agent_idx_list: + self._agent_id2info_mapping[agent_info.id] = agent_info + + # facility levels + for facility_id, facility in self._summary["facilities"].items(): + self.facility_levels[facility_id] = { + "node_index": facility["node_index"], + "config": facility['configs'], + "upstreams": facility["upstreams"], + "skus": facility["skus"] + } + + units = facility["units"] + + storage = units["storage"] + if storage is not None: + self.facility_levels[facility_id]["storage"] = UnitBaseInfo( + storage) + + self.unit_2_facility_dict[storage["id"]] = facility_id + + self._facility2storage_index_dict[facility_id] = storage["node_index"] + + self._storage_product_numbers[facility_id] = [0] * self._sku_number + self._storage_product_indices[facility_id] = {} + self._facility_product_utilization[facility_id] = 0 + + for i, pid in enumerate(storage["product_list"]): + self._storage_product_indices[facility_id][pid] = i + self._storage_product_numbers[facility_id][pid] = 0 + + distribution = units["distribution"] + + if distribution is not None: + self.facility_levels[facility_id]["distribution"] = UnitBaseInfo( + distribution) + self.unit_2_facility_dict[distribution["id"]] = facility_id + + products = units["products"] + + if products: + for product_id, product in products.items(): + product_info = { + "skuproduct": UnitBaseInfo(product) + } + + self.unit_2_facility_dict[product["id"]] = facility_id + + seller = product['seller'] + + if seller is not None: + product_info["seller"] = UnitBaseInfo(seller) + self.unit_2_facility_dict[seller["id"]] = facility_id + + consumer = product["consumer"] + + if consumer is not None: + product_info["consumer"] = UnitBaseInfo(consumer) + self.unit_2_facility_dict[consumer["id"]] = facility_id + + manufacture = product["manufacture"] + + if manufacture is not None: + product_info["manufacture"] = UnitBaseInfo(manufacture) + self.unit_2_facility_dict[manufacture["id"] + ] = facility_id + + self.facility_levels[facility_id][product_id] = product_info + + for unit in (seller, consumer, manufacture, product): + if unit is not None: + self._unit2product_mapping[unit["id"]] = ( + product["id"], + facility_id, + seller["id"] if seller is not None else None, + consumer["id"] if consumer is not None else None, + manufacture["id"] if manufacture is not None else None + ) + + # create initial state structure + self._build_init_state() + + def _build_init_state(self): + # we will build the final state with default and const values, + # then update dynamic part per step + for agent_info in self._agent_list: + state = {} + + facility = self.facility_levels[agent_info.facility_id] + + # global features + state["global_time"] = 0 + + # facility features + state["facility"] = None + state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] + state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] + state['constraint_idx'] = [0] + state['facility_id'] = [0] * self._sku_number + state['sku_info'] = {} if agent_info.is_facility else agent_info.sku + state['echelon_level'] = 0 + + state['facility_info'] = facility['config'] + state["is_positive_balance"] = 0 + + if not agent_info.is_facility: + state['facility_id'][agent_info.sku.id] = 1 + + for atom_name in atoms.keys(): + state[atom_name] = list( + np.ones(self._configs.settings['constraint_state_hist_len'])) + + # storage features + state['storage_levels'] = [0] * self._sku_number + state['storage_capacity'] = facility['storage'].config["capacity"] + state['storage_utilization'] = 0 + + # bom features + state['bom_inputs'] = [0] * self._sku_number + state['bom_outputs'] = [0] * self._sku_number + + if not agent_info.is_facility: + state['bom_inputs'][agent_info.sku.id] = 1 + state['bom_outputs'][agent_info.sku.id] = 1 + + # vlt features + sku_list = self._summary["skus"] + current_source_list = [] + + if agent_info.sku is not None: + current_source_list = facility["upstreams"].get( + agent_info.sku.id, []) + + state['vlt'] = [0] * \ + (self._max_sources_per_facility * self._sku_number) + state['max_vlt'] = 0 + + if not agent_info.is_facility: + # only for sku product + product_info = facility[agent_info.sku.id] + + if "consumer" in product_info and len(current_source_list) > 0: + state['max_vlt'] = product_info["skuproduct"]["max_vlt"] + + for i, source in enumerate(current_source_list): + for j, sku in enumerate(sku_list.values()): + # NOTE: different with original code, our config can make sure that source has product we need + + if sku.id == agent_info.sku.id: + state['vlt'][i * len(sku_list) + j + + 1] = facility["skus"][sku.id].vlt + + # sale features + settings = self.env.configs.settings + hist_len = settings['sale_hist_len'] + consumption_hist_len = settings['consumption_hist_len'] + + state['sale_mean'] = 1.0 + state['sale_std'] = 1.0 + state['sale_gamma'] = 1.0 + state['service_level'] = 0.95 + state['total_backlog_demand'] = 0 + + state['sale_hist'] = [0] * hist_len + state['backlog_demand_hist'] = [0] * hist_len + state['consumption_hist'] = [0] * consumption_hist_len + state['pending_order'] = [0] * settings['pending_order_len'] + + if not agent_info.is_facility: + state['service_level'] = agent_info.sku.service_level + + product_info = facility[agent_info.sku.id] + + if "seller" in product_info: + state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma + + # distribution features + state['distributor_in_transit_orders'] = 0 + state['distributor_in_transit_orders_qty'] = 0 + + # consumer features + state['consumer_source_export_mask'] = [0] * \ + (self._max_sources_per_facility * self._sku_number) + state['consumer_source_inventory'] = [0] * self._sku_number + state['consumer_in_transit_orders'] = [0] * self._sku_number + + state['inventory_in_stock'] = 0 + state['inventory_in_transit'] = 0 + state['inventory_in_distribution'] = 0 + state['inventory_estimated'] = 0 + state['inventory_rop'] = 0 + state['is_over_stock'] = 0 + state['is_out_of_stock'] = 0 + state['is_below_rop'] = 0 + + if len(current_source_list) > 0: + for i, source in enumerate(current_source_list): + for j, sku in enumerate(sku_list.values()): + if sku.id == agent_info.sku.id: + state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = \ + self.facility_levels[source]["skus"][sku.id].vlt + + # price features + state['max_price'] = self._max_price + state['sku_price'] = 0 + state['sku_cost'] = 0 + + if not agent_info.is_facility: + state['sku_price'] = agent_info.sku.price + state['sku_cost'] = agent_info.sku.cost + + self._states[agent_info.id] = state + + +ProductInfo = namedtuple( + "ProductInfo", + ( + "unit_id", + "sku_id", + "node_index", + "storage_index", + "unit_storage_cost", + "distribution_index", + "downstream_product_units", + "consumer_id_index_tuple", + "seller_id_index_tuple", + "manufacture_id_index_tuple" + ) +) + +FacilityLevelInfo = namedtuple( + "FacilityLevelInfo", + ( + "unit_id", + "product_unit_id_list", + "storage_index", + "unit_storage_cost", + "distribution_index", + "vehicle_index_list" + ) +) + + +class BalanceSheetCalculator: + def __init__(self, env: Env): + self.env = env + self.products: List[ProductInfo] = [] + self.product_id2index_dict = {} + self.facility_levels: List[FacilityLevelInfo] = [] + self.consumer_id2product = {} + + self.facilities = env.summary["node_mapping"]["facilities"] + + for facility_id, facility in self.facilities.items(): + pid_list = [] + distribution = facility["units"]["distribution"] + + for product_id, product in facility["units"]["products"].items(): + pid_list.append(product["id"]) + consumer = product["consumer"] + if consumer is not None: + self.consumer_id2product[consumer["id"]] = product["id"] + seller = product["seller"] + manufacture = product["manufacture"] + + self.product_id2index_dict[product["id"]] = len(self.products) + + downstream_product_units = [] + downstreams = facility["downstreams"] + + if downstreams and len(downstreams) > 0 and product_id in downstreams: + for dfacility in downstreams[product_id]: + dproducts = self.facilities[dfacility]["units"]["products"] + + downstream_product_units.append(dproducts[product_id]["id"]) + + self.products.append( + ProductInfo( + unit_id=product["id"], + sku_id=product_id, + node_index=product["node_index"], + storage_index=facility["units"]["storage"]["node_index"], + unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution_index=distribution["node_index"] if distribution is not None else None, + downstream_product_units=downstream_product_units, + consumer_id_index_tuple=None if consumer is None else (consumer["id"], consumer["node_index"]), + seller_id_index_tuple=None if seller is None else (seller["id"], seller["node_index"]), + manufacture_id_index_tuple=None if manufacture is None else (manufacture["id"], manufacture["node_index"]) + ) + ) + + self.facility_levels.append( + FacilityLevelInfo( + unit_id=facility_id, + product_unit_id_list=pid_list, + storage_index=facility["units"]["storage"]["node_index"], + unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution_index=distribution["node_index"] if distribution is not None else None, + vehicle_index_list=[ + v["node_index"] for v in distribution["children"] + ] if distribution is not None else [] + ) + ) + + # TODO: order products make sure calculate reward from downstream to upstream + tmp_product_unit_dict = {} + + for product in self.products: + tmp_product_unit_dict[product.unit_id] = product + + self._ordered_products = [] + + tmp_stack = [] + + for product in self.products: + # skip if already being processed + if tmp_product_unit_dict[product.unit_id] is None: + continue + + for dproduct in product.downstream_product_units: + # push downstream id to stack + tmp_stack.append(dproduct) + + # insert current product to list head + self._ordered_products.insert(0, product) + # mark it as processed + tmp_product_unit_dict[product.unit_id] = None + + while len(tmp_stack) > 0: + # process downstream of product unit in stack + dproduct_unit_id = tmp_stack.pop() + + # if it was processed then ignore + if tmp_product_unit_dict[dproduct_unit_id] is None: + continue + + # or extract it downstreams + dproduct_unit = tmp_product_unit_dict[dproduct_unit_id] + + dproduct_downstreams = dproduct_unit.downstream_product_units + + for dproduct in dproduct_downstreams: + tmp_stack.append(dproduct) + + # current unit in final list + self._ordered_products.insert(0, dproduct_unit) + tmp_product_unit_dict[dproduct_unit_id] = None + + self.total_balance_sheet = defaultdict(int) + + # tick -> (product unit id, sku id, manufacture number, manufacture cost, checkin order, delay penaty) + self._supplier_reward_factors = {} + + def _check_attribute_keys(self, target_type: str, attribute: str): + valid_target_types = list(self.env.summary["node_detail"].keys()) + assert target_type in valid_target_types, f"Target_type {target_type} not in {valid_target_types}!" + + valid_attributes = list(self.env.summary["node_detail"][target_type]["attributes"].keys()) + assert attribute in valid_attributes, ( + f"Attribute {attribute} not valid for {target_type}. " + f"Valid attributes: {valid_attributes}" + ) + return + + def _get_attributes(self, target_type: str, attribute: str, tick: int=None) -> np.ndarray: + self._check_attribute_keys(target_type, attribute) + + if tick == None: + tick = self.env.tick + + return self.env.snapshot_list[target_type][tick::attribute].flatten() + + def _get_list_attributes(self, target_type: str, attribute: str, tick: int=None) -> List[np.ndarray]: + self._check_attribute_keys(target_type, attribute) + + if tick == None: + tick = self.env.tick + + indexes = list(range(len(self.env.snapshot_list[target_type]))) + return [self.env.snapshot_list[target_type][tick:index:attribute].flatten() for index in indexes] + + def _calc_consumer(self): + #### Consumer + consumer_ids = self._get_attributes("consumer", "id").astype(np.int) + + # quantity * price + order_profit = ( + self._get_attributes("consumer", "order_quantity") + * self._get_attributes("consumer", "price") + ) + + # order_cost + order_product_cost + consumer_step_balance_sheet_loss = -1 * ( + self._get_attributes("consumer", "order_cost") + + self._get_attributes("consumer", "order_product_cost") + ) + + # consumer step reward: balance sheet los + profile * discount + # consumer_step_reward = ( + # consumer_step_balance_sheet_loss + # + order_profit * self._get_attributes("consumer", "reward_discount") + # ) + consumer_step_reward = consumer_step_balance_sheet_loss + + consumer_step_balance_sheet = order_profit + consumer_step_balance_sheet_loss + + return consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet + + def _calc_seller(self): + #### Seller + # profit = sold * price + seller_balance_sheet_profit = ( + self._get_attributes("seller", "sold") + * self._get_attributes("seller", "price") + ) + + # loss = demand * price * backlog_ratio + seller_balance_sheet_loss = -1 * ( + self._get_attributes("seller", "demand") + * self._get_attributes("seller", "price") + * self._get_attributes("seller", "backlog_ratio") + ) + + # step reward = loss + profit + seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit + + return seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward + + def _calc_manufacture(self): + #### manufacture + manufacture_ids = self._get_attributes("manufacture", "id").astype(np.int) + + # loss = manufacture number * cost + manufacture_balance_sheet_loss = -1 * ( + self._get_attributes("manufacture", "manufacturing_number") + * self._get_attributes("manufacture", "product_unit_cost") + ) + + # step reward = loss + manufacture_step_reward = manufacture_balance_sheet_loss + manufacture_step_balance_sheet = manufacture_balance_sheet_loss + + return manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet + + def _calc_storage(self): + #### storage + # loss = (capacity-remaining space) * cost + storage_balance_sheet_loss = -1 * ( + self._get_attributes("storage", "capacity") + - self._get_attributes("storage", "remaining_space") + ) + + # create product number mapping for storages + product_list = self._get_list_attributes("storage", "product_list") + product_number = self._get_list_attributes("storage", "product_number") + storages_product_map = { + idx: { + id: num + for id, num in zip(id_list.astype(np.int), num_list.astype(np.int)) + } + for idx, (id_list, num_list) in enumerate(zip(product_list, product_number)) + } + + return storage_balance_sheet_loss, storages_product_map + + def _calc_vehicle(self): + ## vehicles + # loss = cost * payload + vehicle_balance_sheet_loss = -1 * ( + self._get_attributes("vehicle", "payload") + * self._get_attributes("vehicle", "unit_transport_cost") + ) + vehicle_step_reward = vehicle_balance_sheet_loss + return vehicle_balance_sheet_loss, vehicle_step_reward + + def _calc_product_distribution(self): + #### product + # product distribution profit = check order * price + product_distribution_balance_sheet_profit = ( + self._get_attributes("product", "distribution_check_order") + * self._get_attributes("product", "price") + ) + # product distribution loss = transportation cost + delay order penalty + product_distribution_balance_sheet_loss = -1 * ( + self._get_attributes("product", "distribution_transport_cost") + + self._get_attributes("product", "distribution_delay_order_penalty") + ) + return product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss + + def _calc_product( + self, + consumer_step_balance_sheet_loss, + consumer_step_reward, + seller_balance_sheet_profit, + seller_balance_sheet_loss, + seller_step_reward, + manufacture_balance_sheet_loss, + manufacture_step_reward, + storages_product_map, + product_distribution_balance_sheet_profit, + product_distribution_balance_sheet_loss, + ): + num_products = len(self.products) + product_step_reward = np.zeros(num_products) + product_balance_sheet_profit = np.zeros(num_products) + product_balance_sheet_loss = np.zeros(num_products) + + # product = consumer + seller + manufacture + storage + distribution + downstreams + for product in self._ordered_products: + i = product.node_index + + if product.consumer_id_index_tuple: + consumer_index = product.consumer_id_index_tuple[1] + product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer_index] + product_step_reward[i] += consumer_step_reward[consumer_index] + + if product.seller_id_index_tuple: + seller_index = product.seller_id_index_tuple[1] + product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller_index] + product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller_index] + product_step_reward[i] += seller_step_reward[seller_index] + + if product.manufacture_id_index_tuple: + manufacture_index = product.manufacture_id_index_tuple[1] + product_balance_sheet_loss[i] += manufacture_balance_sheet_loss[manufacture_index] + product_step_reward[i] += manufacture_step_reward[manufacture_index] + + storage_reward = -1 * storages_product_map[product.storage_index][product.sku_id] * product.unit_storage_cost + product_step_reward[i] += storage_reward + product_balance_sheet_loss[i] += storage_reward + + if product.distribution_index is not None: + product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[i] + product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[i] + product_step_reward[i] += product_distribution_balance_sheet_loss[i] + product_distribution_balance_sheet_profit[i] + + if len(product.downstream_product_units) > 0: + for did in product.downstream_product_units: + product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] + product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] + product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] + + product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss + + return product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet + + def _calc_facility( + self, + storage_balance_sheet_loss, + vehicle_balance_sheet_loss, + product_balance_sheet_profit, + product_balance_sheet_loss, + product_step_reward + ): + num_facilities = len(self.facility_levels) + facility_balance_sheet_loss = np.zeros(num_facilities) + facility_balance_sheet_profit = np.zeros(num_facilities) + facility_step_reward = np.zeros(num_facilities) + + # for facilities + for i, facility in enumerate(self.facility_levels): + # storage balance sheet + # profit=0 + facility_balance_sheet_loss[i] += storage_balance_sheet_loss[facility.storage_index] * facility.unit_storage_cost + + # distribution balance sheet + if facility.distribution_index is not None: + for vidx in facility.vehicle_index_list: + facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vidx] + # distribution unit do not provide reward + + # sku product unit balance sheet + for pid in facility.product_unit_id_list: + facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] + facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] + facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] + + facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit + + return facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet + + def calc(self): + #### Basic Units: Loss, Profit, Reward + consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet = self._calc_consumer() + seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward = self._calc_seller() + manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet = self._calc_manufacture() + storage_balance_sheet_loss, storages_product_map = self._calc_storage() + vehicle_balance_sheet_loss, vehicle_step_reward = self._calc_vehicle() + product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss = self._calc_product_distribution() + ######################################################################## + + #### Loss, profit, reward for each product + product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet = self._calc_product( + consumer_step_balance_sheet_loss, + consumer_step_reward, + seller_balance_sheet_profit, + seller_balance_sheet_loss, + seller_step_reward, + manufacture_balance_sheet_loss, + manufacture_step_reward, + storages_product_map, + product_distribution_balance_sheet_profit, + product_distribution_balance_sheet_loss + ) + ######################################################################## + + #### Loss, profit, reward for each facility + # facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet = self._calc_facility( + # storage_balance_sheet_loss, + # vehicle_balance_sheet_loss, + # product_balance_sheet_profit, + # product_balance_sheet_loss, + # product_step_reward + # ) + ######################################################################## + + # Final result for current tick, key is the facility/unit id, value is tuple of balance sheet and reward. + result = {} + + # For product units. + for id, bs, rw in zip([product.unit_id for product in self.products], product_balance_sheet, product_step_reward): + result[id] = (bs, rw) + self.total_balance_sheet[id] += bs + + # For consumers. + for id, bs, rw in zip(consumer_ids, consumer_step_balance_sheet, consumer_step_reward): + # result[id] = (bs, rw) + # let reward of a consumer equate its parent product + result[id] = result[self.consumer_id2product[id]] + self.total_balance_sheet[id] += result[id][0] + + # For producers. + for id, bs, rw in zip(manufacture_ids, manufacture_step_balance_sheet, manufacture_step_reward): + result[id] = (bs, rw) + self.total_balance_sheet[id] += bs + + # NOTE: add followings if you need. + # For storages. + # For distributions. + # For vehicles. + + return result + + +if __name__ == "__main__": + from time import time + import cProfile + + env = Env( + scenario="supply_chain", + topology="sample", + durations=100, + max_snapshots=10) + + ss = SCEnvWrapper(env) + + env.step(None) + + start_time = time() + + # cProfile.run("ss.get_state(None)", sort="cumtime") + states = ss.get_state(None) + print(env.agent_idx_list) + print(ss.cur_balance_sheet_reward) + print(states) + + # end_time = time() + # + # print("time cost:", end_time - start_time) + # + # print("dim:", ss.dim) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 7bff3974b..df742cc28 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -1,8 +1,13 @@ -from maro.simulator import Env +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from collections import defaultdict, namedtuple + import scipy.stats as st import numpy as np + from maro.rl import AbsEnvWrapper +from maro.simulator import Env from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction # from exploration import exploration_dict, agent2exploration @@ -244,23 +249,22 @@ def get_state(self, event): settings: dict = self.env.configs.settings consumption_hist_len = settings['consumption_hist_len'] hist_len = settings['sale_hist_len'] - consumption_ticks = [cur_tick - - i for i in range(consumption_hist_len-1, -1, -1)] + consumption_ticks = [cur_tick - i for i in range(consumption_hist_len-1, -1, -1)] hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] self.cur_balance_sheet_reward = self.balance_cal.calc() self._cur_metrics = self.env.metrics - self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features]\ - .flatten()\ - .reshape(-1, 2)\ + self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features] \ + .flatten() \ + .reshape(-1, len(distribution_features)) \ .astype(np.int) - self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"]\ - .flatten()\ + self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"] \ + .flatten() \ .reshape(-1, len(self.consumer_ss)) - self._cur_seller_states = self.seller_ss[hist_ticks::seller_features]\ + self._cur_seller_states = self.seller_ss[hist_ticks::seller_features] \ .astype(np.int) @@ -280,8 +284,8 @@ def get_state(self, event): # calculate storage info first, then use it later to speed up. for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"]\ - .flatten()\ + product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"] \ + .flatten() \ .astype(np.int) for pid, index in self._storage_product_indices[facility_id].items(): @@ -293,7 +297,7 @@ def get_state(self, event): for agent_info in self._agent_list: state = self._states[agent_info.id] - storage_index = self._facility2storage_index_dict[agent_info.facility_id] + # storage_index = self._facility2storage_index_dict[agent_info.facility_id] np_state = self.get_rl_policy_state(state, agent_info) if agent_info.agent_type in ["consumer", "producer"]: @@ -532,8 +536,7 @@ def _build_internal_helpers(self): self._facility2storage_index_dict[facility_id] = storage["node_index"] - self._storage_product_numbers[facility_id] = [ - 0] * self._sku_number + self._storage_product_numbers[facility_id] = [0] * self._sku_number self._storage_product_indices[facility_id] = {} self._facility_product_utilization[facility_id] = 0 @@ -781,12 +784,9 @@ def __init__(self, env: Env): facility["units"]["storage"]["config"]["unit_storage_cost"], distribution["node_index"] if distribution is not None else None, downstream_product_units, - None if consumer is None else ( - consumer["id"], consumer["node_index"]), - None if seller is None else ( - seller["id"], seller["node_index"]), - None if manufacture is None else ( - manufacture["id"], manufacture["node_index"]), + None if consumer is None else (consumer["id"], consumer["node_index"]), + None if seller is None else (seller["id"], seller["node_index"]), + None if manufacture is None else (manufacture["id"], manufacture["node_index"]), )) self.facility_levels.append(( @@ -795,8 +795,7 @@ def __init__(self, env: Env): facility["units"]["storage"]["node_index"], facility["units"]["storage"]["config"]["unit_storage_cost"], distribution["node_index"] if distribution is not None else None, - [v["node_index"] for v in distribution["children"] - ] if distribution is not None else [] + [v["node_index"] for v in distribution["children"]] if distribution is not None else [] )) # TODO: order products make sure calculate reward from downstream to upstream @@ -851,7 +850,10 @@ def __init__(self, env: Env): def calc(self): tick = self.env.tick - # consumer + ## consumer + # consumer_features = ( + # "id", "order_quantity", "price", "order_cost", "order_product_cost", "reward_discount" + # ) consumer_bs_states = self.consumer_ss[tick::self.consumer_features]\ .flatten()\ .reshape(-1, len(self.consumer_features)) @@ -868,8 +870,11 @@ def calc(self): # order_profit * consumer_bs_states[:, 5] consumer_step_reward = consumer_step_balance_sheet_loss - # seller - seller_bs_states = self.seller_ss[tick::self.seller_features]\ + ######################################################################## + + ## seller + # seller_features = ("id", "sold", "demand", "price", "backlog_ratio") + seller_bs_states = self.seller_ss[tick::self.seller_features] \ .flatten()\ .reshape(-1, len(self.seller_features)) @@ -877,33 +882,37 @@ def calc(self): seller_balance_sheet_profit = seller_bs_states[:, 1] * seller_bs_states[:, 3] # loss = demand * price * backlog_ratio - seller_balance_sheet_loss = -1 * \ - seller_bs_states[:, 2] * \ - seller_bs_states[:, 3] * seller_bs_states[:, 4] + seller_balance_sheet_loss = -1 * seller_bs_states[:, 2] * seller_bs_states[:, 3] * seller_bs_states[:, 4] # step reward = loss + profit seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit - # manufacture - man_bs_states = self.manufacture_ss[tick::self.manufacture_features]\ + ######################################################################## + + ## manufacture + # manufacture_features = ("id", "manufacturing_number", "product_unit_cost") + man_bs_states = self.manufacture_ss[tick::self.manufacture_features] \ .flatten()\ .reshape(-1, len(self.manufacture_features)) # loss = manufacture number * cost - man_balance_sheet_profit_loss = -1 * \ - man_bs_states[:, 1] * man_bs_states[:, 2] + man_balance_sheet_profit_loss = -1 * man_bs_states[:, 1] * man_bs_states[:, 2] # step reward = loss man_step_reward = man_balance_sheet_profit_loss - # product - product_bs_states = self.product_ss[tick::self.product_features]\ + ######################################################################## + + ## product + # product_features = ( + # "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty" + # ) + product_bs_states = self.product_ss[tick::self.product_features] \ .flatten()\ .reshape(-1, len(self.product_features)) # product distribution loss = transportation cost + delay order penalty - product_distribution_balance_sheet_loss = -1 * \ - (product_bs_states[:, 3] + product_bs_states[:, 4]) + product_distribution_balance_sheet_loss = -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) # product distribution profit = check order * price product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] @@ -916,11 +925,11 @@ def calc(self): # create product number mapping for storages storages_product_map = {} for storage_index in range(len(self.storage_ss)): - product_list = self.storage_ss[tick:storage_index:"product_list"]\ + product_list = self.storage_ss[tick:storage_index:"product_list"] \ .flatten()\ .astype(np.int) - product_number = self.storage_ss[tick:storage_index:"product_number"]\ + product_number = self.storage_ss[tick:storage_index:"product_number"] \ .flatten()\ .astype(np.int) @@ -948,9 +957,7 @@ def calc(self): product_balance_sheet_loss[i] += man_balance_sheet_profit_loss[manufacture[1]] product_step_reward[i] += man_step_reward[manufacture[1]] - storage_reward = -1 * \ - storages_product_map[storage_index][product_id] * \ - unit_storage_cost + storage_reward = -1 * storages_product_map[storage_index][product_id] * unit_storage_cost product_step_reward[i] += storage_reward @@ -972,7 +979,7 @@ def calc(self): product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss # storage - storage_states = self.storage_ss[tick::self.storage_features]\ + storage_states = self.storage_ss[tick::self.storage_features] \ .flatten()\ .reshape(-1, len(self.storage_features)) @@ -980,13 +987,12 @@ def calc(self): storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) # vehicles - vehicle_states = self.vehicle_ss[tick::self.vehicle_features]\ + vehicle_states = self.vehicle_ss[tick::self.vehicle_features] \ .flatten()\ .reshape(-1, len(self.vehicle_features)) # loss = cost * payload - vehicle_balance_sheet_loss = -1 * \ - vehicle_states[:, 1] * vehicle_states[:, 2] + vehicle_balance_sheet_loss = -1 * vehicle_states[:, 1] * vehicle_states[:, 2] vehicle_step_reward = vehicle_balance_sheet_loss @@ -1000,8 +1006,7 @@ def calc(self): # storage balance sheet # profit=0 - facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * \ - unit_storage_cost + facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * unit_storage_cost # distribution balance sheet if distribution_index is not None: diff --git a/examples/supply_chain/or_policy/minmax_policy.py b/examples/supply_chain/or_policy/minmax_policy.py index 4a6ec903c..c4dfd7fde 100644 --- a/examples/supply_chain/or_policy/minmax_policy.py +++ b/examples/supply_chain/or_policy/minmax_policy.py @@ -25,7 +25,7 @@ def choose_action(self, state): inflight_orders = np.array(state['consumer_in_transit_orders']) booked_inventory = available_inventory + inflight_orders - # stop placing orders when the facilty runs out of capacity + # stop placing orders when the facility runs out of capacity if np.sum(booked_inventory) > state['storage_capacity']: return 0 From 5ad21e45732eacf696a2da7eeb3d9a16992a2251 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 28 Jun 2021 11:28:06 +0000 Subject: [PATCH 327/482] added docker scripts for cim-dqn --- examples/cim/dqn/config.yml | 15 +++-- examples/cim/dqn/policy_manager.py | 34 ---------- .../cim/dqn/policy_manager/policy_manager.py | 38 +++++++++++ examples/cim/dqn/policy_manager/trainer.py | 24 +++++++ examples/cim/dqn/scripts/build.sh | 8 +++ .../cim/dqn/scripts/docker_compose_yml.py | 64 +++++++++++++++++++ examples/cim/dqn/scripts/kill.sh | 6 ++ examples/cim/dqn/scripts/run.sh | 7 ++ examples/cim/dqn/sync_mode/docker-compose.yml | 5 ++ examples/cim/dqn/sync_mode/learner.py | 28 ++++++++ examples/cim/dqn/sync_mode/main.py | 54 ---------------- examples/cim/dqn/sync_mode/rollout_manager.py | 50 +++++++++------ examples/cim/dqn/sync_mode/rollout_worker.py | 29 +++++++++ .../training/policy_manager/policy_manager.py | 9 +-- maro/rl/training/policy_manager/trainer.py | 12 ++-- .../rl/training/sync_tools/rollout_manager.py | 4 +- maro/rl/training/sync_tools/rollout_worker.py | 21 +++--- maro/rl/wrappers/env_wrapper.py | 2 +- 18 files changed, 267 insertions(+), 143 deletions(-) delete mode 100644 examples/cim/dqn/policy_manager.py create mode 100644 examples/cim/dqn/policy_manager/policy_manager.py create mode 100644 examples/cim/dqn/policy_manager/trainer.py create mode 100644 examples/cim/dqn/scripts/build.sh create mode 100644 examples/cim/dqn/scripts/docker_compose_yml.py create mode 100644 examples/cim/dqn/scripts/kill.sh create mode 100644 examples/cim/dqn/scripts/run.sh create mode 100644 examples/cim/dqn/sync_mode/docker-compose.yml create mode 100644 examples/cim/dqn/sync_mode/learner.py delete mode 100644 examples/cim/dqn/sync_mode/main.py create mode 100644 examples/cim/dqn/sync_mode/rollout_worker.py diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml index 38b2c15eb..4f711c8b5 100644 --- a/examples/cim/dqn/config.yml +++ b/examples/cim/dqn/config.yml @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -experiment_name: cim_dqn -mode: multi-process # local, multi-process +experiment_name: cim-dqn +mode: sync num_episodes: 10 -# eval_schedule: 2 +eval_schedule: 2 num_steps: 50 env: basic: @@ -80,9 +80,10 @@ policies: beta_step: 0.001 update_trigger: 16 warmup: 1 -sync: - rollout_mode: multi-process # single-process, multi-process, multi-node - num_rollout_workers: 3 +roll_out: + group: cim-dqn-rollout + mode: multi-node # single-process, multi-process, multi-node + num_workers: 3 # max_receive_attempts: 2 # receive_timeout: 100 # in milli-seconds policy_manager: @@ -90,5 +91,5 @@ policy_manager: policy_training_mode: multi-process # single-process, multi-process, multi-node num_trainers: 2 redis: - host: localhost + host: maro-redis port: 6379 \ No newline at end of file diff --git a/examples/cim/dqn/policy_manager.py b/examples/cim/dqn/policy_manager.py deleted file mode 100644 index 27ba817b1..000000000 --- a/examples/cim/dqn/policy_manager.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager - -dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory -cim_path = os.path.dirname(dqn_path) # CIM example directory -sys.path.insert(0, cim_path) -sys.path.insert(0, dqn_path) -from general import AGENT_IDS, NUM_POLICY_TRAINERS, config, log_dir -from policy import get_independent_policy_for_training - - -policies = [get_independent_policy_for_training(i) for i in AGENT_IDS] -if config["policy_manager"]["policy_training_mode"] == "single-process": - policy_manager = LocalPolicyManager(policies, log_dir=log_dir) -elif config["policy_manager"]["policy_training_mode"] == "multi-process": - policy_manager = MultiProcessPolicyManager( - policies, - {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping - {i: get_independent_policy_for_training for i in AGENT_IDS}, - log_dir=log_dir - ) -elif config["policy_manager"]["policy_training_mode"] == "multi-node": - policy_manager = MultiNodePolicyManager( - policies, - {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping - config["policy_manager"]["group"], - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, - log_dir=log_dir - ) diff --git a/examples/cim/dqn/policy_manager/policy_manager.py b/examples/cim/dqn/policy_manager/policy_manager.py new file mode 100644 index 000000000..d45aa875f --- /dev/null +++ b/examples/cim/dqn/policy_manager/policy_manager.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +from maro.rl import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager + +dqn_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # DQN directory +sys.path.insert(0, dqn_path) +from general import AGENT_IDS, NUM_POLICY_TRAINERS, config, log_dir +from policy import get_independent_policy_for_training + +def get_policy_manager(): + policies = [get_independent_policy_for_training(i) for i in AGENT_IDS] + training_mode = config["policy_manager"]["policy_training_mode"] + if training_mode == "single-process": + return LocalPolicyManager(policies, log_dir=log_dir) + if training_mode == "multi-process": + return MultiProcessPolicyManager( + policies, + {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping + {i: get_independent_policy_for_training for i in AGENT_IDS}, + log_dir=log_dir + ) + if training_mode == "multi-node": + return MultiNodePolicyManager( + config["policy_manager"]["group"], + policies, + {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping + proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + log_dir=log_dir + ) + + raise ValueError( + f"Unsupported policy training mode: {training_mode}. " + f"Supported modes: single-process, multi-process, multi-node" + ) diff --git a/examples/cim/dqn/policy_manager/trainer.py b/examples/cim/dqn/policy_manager/trainer.py new file mode 100644 index 000000000..20dc18a55 --- /dev/null +++ b/examples/cim/dqn/policy_manager/trainer.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +from maro.rl import trainer_node + +dqn_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # DQN async mode directory +sys.path.insert(0, dqn_path) +from general import AGENT_IDS, config, log_dir +from policy import get_independent_policy_for_training + + +if __name__ == "__main__": + trainer_node( + config["policy_manager"]["group"], + [get_independent_policy_for_training(id_) for id_ in AGENT_IDS], + proxy_kwargs={ + "component_name": os.environ["TRAINERID"], + "redis_address": (config["redis"]["host"], config["redis"]["port"]) + }, + log_dir=log_dir + ) diff --git a/examples/cim/dqn/scripts/build.sh b/examples/cim/dqn/scripts/build.sh new file mode 100644 index 000000000..5068c917d --- /dev/null +++ b/examples/cim/dqn/scripts/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +BASEDIR=$(dirname "$0") +ROOTDIR=$BASEDIR/../../../../../ + +# script to build the docker image for running the CIM scenario. +docker pull redis:6 +docker build -f $ROOTDIR/docker_files/dev.df -t maro-cim:latest $ROOTDIR \ No newline at end of file diff --git a/examples/cim/dqn/scripts/docker_compose_yml.py b/examples/cim/dqn/scripts/docker_compose_yml.py new file mode 100644 index 000000000..fd29c17f7 --- /dev/null +++ b/examples/cim/dqn/scripts/docker_compose_yml.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import yaml +from copy import deepcopy +from os.path import dirname, join, realpath + +path = realpath(__file__) +script_dir = dirname(path) +cim_dqn_sync_dir = join(dirname(script_dir), "sync_mode") +cim_dqn_dir = dirname(cim_dqn_sync_dir) +cim_dir = dirname(cim_dqn_dir) +root_dir = dirname(dirname(cim_dir)) +maro_rl_dir = join(root_dir, "maro", "rl") +maro_comm_dir = join(root_dir, "maro", "communication") +config_path = join(cim_dqn_dir, "config.yml") +dockerfile_path = join(root_dir, "docker_files", "dev.df") + +with open(config_path, "r") as fp: + config = yaml.safe_load(fp) + num_trainers = config["policy_manager"]["num_trainers"] + redis_host = config["redis"]["host"] + +docker_compose_manifest = {"version": "3.9", "services": {"redis": {"image": "redis:6", "container_name": redis_host}}} +common_spec = { + "build": {"context": root_dir, "dockerfile": dockerfile_path}, + "image": "maro-cim", + "volumes": [ + f"{cim_dir}:/maro/cim", + f"{maro_rl_dir}:/maro/maro/rl", + f"{maro_comm_dir}:/maro/maro/communication" + ] +} + +# trainer spec +if config["policy_manager"]["policy_training_mode"] == "multi-node": + for i in range(num_trainers): + trainer_id = f"TRAINER.{i}" + trainer_spec = deepcopy(common_spec) + del trainer_spec["build"] + trainer_spec["command"] = "python3 /maro/cim/dqn/policy_manager/trainer.py" + trainer_spec["container_name"] = trainer_id + trainer_spec["environment"] = [f"TRAINERID={trainer_id}"] + docker_compose_manifest["services"][trainer_id] = trainer_spec + +# learner_spec +docker_compose_manifest["services"]["learner"] = { + **common_spec, + **{"container_name": "learner", "command": "python3 /maro/cim/dqn/sync_mode/learner.py"} +} + +# rollout worker spec +if config["roll_out"]["mode"] == "multi-node": + for i in range(config["roll_out"]["num_workers"]): + actor_id = f"ROLLOUT_WORKER.{i}" + actor_spec = deepcopy(common_spec) + del actor_spec["build"] + actor_spec["command"] = "python3 /maro/cim/dqn/sync_mode/rollout_worker.py" + actor_spec["container_name"] = actor_id + actor_spec["environment"] = [f"WORKERID={actor_id}"] + docker_compose_manifest["services"][actor_id] = actor_spec + +with open(join(cim_dqn_dir, "docker-compose.yml"), "w") as fp: + yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/cim/dqn/scripts/kill.sh b/examples/cim/dqn/scripts/kill.sh new file mode 100644 index 000000000..a081af0b5 --- /dev/null +++ b/examples/cim/dqn/scripts/kill.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +BASEDIR=$(dirname "$0") + +# script to kill a previously launched training job. +docker-compose -f $BASEDIR/../docker-compose.yml down \ No newline at end of file diff --git a/examples/cim/dqn/scripts/run.sh b/examples/cim/dqn/scripts/run.sh new file mode 100644 index 000000000..37ac6143f --- /dev/null +++ b/examples/cim/dqn/scripts/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +BASEDIR=$(dirname "$0") + +# script to run the CIM scenario in single-host multi-container mode. +python3 $BASEDIR/docker_compose_yml.py +docker-compose -f $BASEDIR/../docker-compose.yml up \ No newline at end of file diff --git a/examples/cim/dqn/sync_mode/docker-compose.yml b/examples/cim/dqn/sync_mode/docker-compose.yml new file mode 100644 index 000000000..c29321712 --- /dev/null +++ b/examples/cim/dqn/sync_mode/docker-compose.yml @@ -0,0 +1,5 @@ +services: + redis: + container_name: localhost + image: redis:6 +version: '3.9' diff --git a/examples/cim/dqn/sync_mode/learner.py b/examples/cim/dqn/sync_mode/learner.py new file mode 100644 index 000000000..1d59852a6 --- /dev/null +++ b/examples/cim/dqn/sync_mode/learner.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys +import time + +from maro.rl import Learner + +sync_mode_path = os.path.dirname(os.path.realpath(__file__)) # DQN sync mode directory +dqn_path = os.path.dirname(sync_mode_path) # DQN directory +sys.path.insert(0, dqn_path) +sys.path.insert(0, sync_mode_path) +from general import config, log_dir +from policy_manager.policy_manager import get_policy_manager +from rollout_manager import get_rollout_manager + + +if __name__ == "__main__": + learner = Learner( + policy_manager=get_policy_manager(), + rollout_manager=get_rollout_manager(), + num_episodes=config["num_episodes"], + eval_schedule=config["eval_schedule"], + log_dir=log_dir + ) + time.sleep(10) + learner.run() diff --git a/examples/cim/dqn/sync_mode/main.py b/examples/cim/dqn/sync_mode/main.py deleted file mode 100644 index 0b15190da..000000000 --- a/examples/cim/dqn/sync_mode/main.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler, Learner, LocalLearner -from maro.simulator import Env - -sync_mode_path = os.path.dirname(os.path.realpath(__file__)) # DQN sync mode directory -dqn_path = os.path.dirname(sync_mode_path) # DQN directory -cim_path = os.path.dirname(dqn_path) # CIM example directory -sys.path.insert(0, cim_path) -sys.path.insert(0, dqn_path) -sys.path.insert(0, sync_mode_path) -from env_wrapper import CIMEnvWrapper -from general import NUM_ACTIONS, config, log_dir -from policy import get_independent_policy_for_training -from rollout_manager import rollout_manager -from policy_manager import policy_manager - - -if __name__ == "__main__": - if config["mode"] == "local": - env = Env(**config["env"]["basic"]) - epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) - epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - last_ep=config["num_episodes"], - **config["exploration"] - ) - local_learner = LocalLearner( - env=CIMEnvWrapper(env, **config["env"]["wrapper"]), - policies=[get_independent_policy_for_training(config["policy"], i) for i in env.agent_idx_list], - agent2policy={i: i for i in env.agent_idx_list}, - num_episodes=config["num_episodes"], - num_steps=config["num_steps"], - exploration_dict={f"EpsilonGreedy1": epsilon_greedy}, - agent2exploration={i: f"EpsilonGreedy1" for i in env.agent_idx_list}, - log_dir=log_dir - ) - local_learner.run() - elif config["mode"] == "multi-process": - learner = Learner( - policy_manager=policy_manager, - rollout_manager=rollout_manager, - num_episodes=config["num_episodes"], - # eval_schedule=config["eval_schedule"], - log_dir=log_dir - ) - learner.run() - else: - print("Two modes are supported: local or multi-process.") diff --git a/examples/cim/dqn/sync_mode/rollout_manager.py b/examples/cim/dqn/sync_mode/rollout_manager.py index d99f71e57..c0812ead2 100644 --- a/examples/cim/dqn/sync_mode/rollout_manager.py +++ b/examples/cim/dqn/sync_mode/rollout_manager.py @@ -4,8 +4,7 @@ import os import sys -from maro.rl import EpsilonGreedyExploration, LocalRolloutManager, MultiProcessRolloutManager - +from maro.rl import EpsilonGreedyExploration, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager from maro.simulator import Env sync_mode_path = os.path.dirname(os.path.realpath(__file__)) # DQN sync mode directory @@ -19,23 +18,34 @@ from general import NUM_ACTIONS, config, log_dir from policy import get_independent_policy_for_rollout +def get_rollout_manager(): + rollout_mode = config["roll_out"]["mode"] + if rollout_mode == "single-process": + env = Env(**config["env"]["basic"]) + return LocalRolloutManager( + CIMEnvWrapper(env, **config["env"]["wrapper"]), + [get_independent_policy_for_rollout(i) for i in env.agent_idx_list], + {i: i for i in env.agent_idx_list}, + num_steps=config["num_steps"], + exploration_dict={f"EpsilonGreedy": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, + agent2exploration={i: "EpsilonGreedy" for i in env.agent_idx_list}, + log_dir=log_dir + ) + if rollout_mode == "multi-process": + return MultiProcessRolloutManager( + config["sync"]["num_rollout_workers"], + lambda: CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]), + get_agent_wrapper, + num_steps=config["num_steps"], + log_dir=log_dir, + ) + if rollout_mode == "multi-node": + return MultiNodeRolloutManager( + config["roll_out"]["group"], + config["roll_out"]["num_workers"], + proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} + ) -if config["sync"]["rollout_mode"] == "single-process": - env = Env(**config["env"]["basic"]) - rollout_manager = LocalRolloutManager( - CIMEnvWrapper(env, **config["env"]["wrapper"]), - [get_independent_policy_for_rollout(i) for i in env.agent_idx_list], - {i: i for i in env.agent_idx_list}, - num_steps=config["num_steps"], - exploration_dict={f"EpsilonGreedy": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, - agent2exploration={i: "EpsilonGreedy" for i in env.agent_idx_list}, - log_dir=log_dir - ) -else: - rollout_manager = MultiProcessRolloutManager( - config["sync"]["num_rollout_workers"], - lambda: CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]), - get_agent_wrapper, - num_steps=config["num_steps"], - log_dir=log_dir, + raise ValueError( + f"Unsupported roll-out mode: {rollout_mode}. Supported modes: single-process, multi-process, multi-node" ) diff --git a/examples/cim/dqn/sync_mode/rollout_worker.py b/examples/cim/dqn/sync_mode/rollout_worker.py new file mode 100644 index 000000000..c1ee6eb7a --- /dev/null +++ b/examples/cim/dqn/sync_mode/rollout_worker.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +from maro.rl import rollout_worker_node +from maro.simulator import Env + +dqn_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # DQN directory +cim_path = os.path.dirname(dqn_path) +sys.path.insert(0, dqn_path) +sys.path.insert(0, cim_path) +from agent_wrapper import get_agent_wrapper +from env_wrapper import CIMEnvWrapper +from general import config, log_dir + + +if __name__ == "__main__": + rollout_worker_node( + config["roll_out"]["group"], + CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]), + get_agent_wrapper(), + proxy_kwargs={ + "component_name": os.environ["WORKERID"], + "redis_address": (config["redis"]["host"], config["redis"]["port"]) + }, + log_dir=log_dir + ) diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index d59c27c14..9a09714c1 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -156,13 +156,10 @@ class MultiNodePolicyManager(AbsPolicyManager): """Policy manager that communicates with a set of remote nodes for parallel training. Args: - policies (List[AbsPolicy]): A list of policies managed by the manager. - policy2trainer (dict): Mapping from policy names to trainer IDs. - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. + policies (List[AbsPolicy]): A list of policies managed by the manager. + policy2trainer (dict): Mapping from policy names to trainer IDs. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at @@ -171,9 +168,9 @@ class MultiNodePolicyManager(AbsPolicyManager): """ def __init__( self, + group: str, policies: List[AbsPolicy], policy2trainer: Dict[str, str], - group: str, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py index 821dae776..91de2f452 100644 --- a/maro/rl/training/policy_manager/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -4,9 +4,10 @@ import time from multiprocessing.connection import Connection from os import getcwd -from typing import Callable, Dict +from typing import Callable, Dict, List from maro.communication import Proxy +from maro.rl.policy import AbsCorePolicy from maro.utils import Logger from ..message_enums import MsgKey, MsgTag @@ -53,24 +54,22 @@ def trainer_process( def trainer_node( - create_policy_func_dict: Dict[str, Callable], group: str, + policies: List[AbsCorePolicy], proxy_kwargs: dict = {}, log_dir: str = getcwd() ): """Policy trainer process that can be launched on separate computation nodes. Args: - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. + policies (List[AbsCorePolicy]): List of policies maintained by the trainer. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ - policy_dict = {policy_name: func(policy_name) for policy_name, func in create_policy_func_dict.items()} + policy_dict = {policy.name: policy for policy in policies} proxy = Proxy(group, "trainer", {"policy_manager": 1}, **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) @@ -93,5 +92,6 @@ def trainer_node( if policy_dict[name].on_experiences(exp) } } + logger.info(f"updated policies {list(msg_body[MsgKey.POLICY_STATE].keys())}") logger.debug(f"total policy update time: {time.time() - t0}") proxy.reply(msg, body=msg_body) diff --git a/maro/rl/training/sync_tools/rollout_manager.py b/maro/rl/training/sync_tools/rollout_manager.py index c9f420746..c73b68b12 100644 --- a/maro/rl/training/sync_tools/rollout_manager.py +++ b/maro/rl/training/sync_tools/rollout_manager.py @@ -385,9 +385,9 @@ class MultiNodeRolloutManager(AbsRolloutManager): """Controller for a set of remote roll-out workers, possibly distributed on different computation nodes. Args: - num_workers (int): Number of remote roll-out workers. group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager that manages them. + num_workers (int): Number of remote roll-out workers. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to @@ -409,8 +409,8 @@ class MultiNodeRolloutManager(AbsRolloutManager): """ def __init__( self, - num_workers: int, group: str, + num_workers: int, num_steps: int = -1, max_receive_attempts: int = None, receive_timeout: int = None, diff --git a/maro/rl/training/sync_tools/rollout_worker.py b/maro/rl/training/sync_tools/rollout_worker.py index 32519021b..bca2ffd94 100644 --- a/maro/rl/training/sync_tools/rollout_worker.py +++ b/maro/rl/training/sync_tools/rollout_worker.py @@ -104,32 +104,27 @@ def evaluate(msg): def rollout_worker_node( - create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_agent_wrapper_func: Callable[[], AgentWrapper], group: str, - create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, + env_wrapper: AbsEnvWrapper, + agent_wrapper: AgentWrapper, + eval_env_wrapper: AbsEnvWrapper = None, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): """Roll-out worker process that can be launched on separate computation nodes. Args: - create_env_wrapper_func (Callable): Function to create an environment wrapper for roll-out. The function - should take no parameters and return an environment wrapper instance. - create_agent_wrapper_func (Callable): Function to create a decision generator for interacting with - the environment. The function should take no parameters and return a ``AgentWrapper`` instance. group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager that manages them. - create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function - should take no parameters and return an environment wrapper instance. If this is None, the training - environment wrapper will be used for evaluation. + env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. + agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. + eval_env_wrapper (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training + environment wrapper will be used for evaluation. Defaults to None. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ - env_wrapper = create_env_wrapper_func() - eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() - agent_wrapper = create_agent_wrapper_func() + eval_env_wrapper = env_wrapper if not eval_env_wrapper else eval_env_wrapper proxy = Proxy(group, "rollout_worker", {"rollout_manager": 1}, **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) diff --git a/maro/rl/wrappers/env_wrapper.py b/maro/rl/wrappers/env_wrapper.py index 202e9e817..22edd4b69 100644 --- a/maro/rl/wrappers/env_wrapper.py +++ b/maro/rl/wrappers/env_wrapper.py @@ -77,7 +77,7 @@ def get_state(self, tick: int = None) -> dict: raise NotImplementedError @abstractmethod - def to_env_action(self, action): + def to_env_action(self, action) -> dict: """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" raise NotImplementedError From a56d4c2a527ce0ac2ba3706df28e7d7d653f5389 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 29 Jun 2021 16:36:10 +0000 Subject: [PATCH 328/482] refactored example folder structure and added workflow templates --- examples/cim/ac.py | 120 ++++++++++++++++++ examples/cim/ac/config.yml | 81 ------------ examples/cim/ac/main.py | 72 ----------- examples/cim/agent_wrapper.py | 35 +++++ examples/cim/dqn.py | 96 ++++++++++++++ examples/cim/dqn/__init__.py | 2 - examples/cim/dqn/agent_wrapper.py | 30 ----- examples/cim/dqn/config.yml | 95 -------------- examples/cim/dqn/general.py | 28 ---- examples/cim/dqn/policy.py | 55 -------- .../cim/dqn/scripts/docker_compose_yml.py | 64 ---------- examples/cim/dqn/sync_mode/docker-compose.yml | 5 - examples/cim/dqn/sync_mode/rollout_manager.py | 51 -------- examples/cim/dqn/sync_mode/rollout_worker.py | 29 ----- examples/cim/env_wrapper.py | 43 +++++++ examples/cim/meta.py | 13 ++ examples/config.yml | 22 ++++ examples/general.py | 26 ++++ examples/{cim/dqn => }/scripts/build.sh | 0 examples/scripts/docker_compose_yml.py | 62 +++++++++ examples/{cim/dqn => }/scripts/kill.sh | 0 examples/{cim/dqn => }/scripts/run.sh | 0 .../dqn/sync_mode => templates}/learner.py | 2 +- .../policy_manager/policy_manager.py | 22 ++-- .../policy_manager/trainer.py | 11 +- .../rollout_manager/rollout_manager.py | 39 ++++++ .../rollout_manager/rollout_worker.py | 23 ++++ maro/rl/exploration/exploration_scheduler.py | 4 +- .../training/policy_manager/policy_manager.py | 36 +++--- maro/rl/training/policy_manager/trainer.py | 18 ++- .../rl/training/sync_tools/rollout_manager.py | 109 ++++------------ maro/rl/training/sync_tools/rollout_worker.py | 19 +-- maro/rl/wrappers/agent_wrapper.py | 9 +- 33 files changed, 565 insertions(+), 656 deletions(-) create mode 100644 examples/cim/ac.py delete mode 100644 examples/cim/ac/config.yml delete mode 100644 examples/cim/ac/main.py create mode 100644 examples/cim/agent_wrapper.py create mode 100644 examples/cim/dqn.py delete mode 100644 examples/cim/dqn/__init__.py delete mode 100644 examples/cim/dqn/agent_wrapper.py delete mode 100644 examples/cim/dqn/config.yml delete mode 100644 examples/cim/dqn/general.py delete mode 100644 examples/cim/dqn/policy.py delete mode 100644 examples/cim/dqn/scripts/docker_compose_yml.py delete mode 100644 examples/cim/dqn/sync_mode/docker-compose.yml delete mode 100644 examples/cim/dqn/sync_mode/rollout_manager.py delete mode 100644 examples/cim/dqn/sync_mode/rollout_worker.py create mode 100644 examples/cim/meta.py create mode 100644 examples/config.yml create mode 100644 examples/general.py rename examples/{cim/dqn => }/scripts/build.sh (100%) create mode 100644 examples/scripts/docker_compose_yml.py rename examples/{cim/dqn => }/scripts/kill.sh (100%) rename examples/{cim/dqn => }/scripts/run.sh (100%) rename examples/{cim/dqn/sync_mode => templates}/learner.py (92%) rename examples/{cim/dqn => templates}/policy_manager/policy_manager.py (57%) rename examples/{cim/dqn => templates}/policy_manager/trainer.py (52%) create mode 100644 examples/templates/rollout_manager/rollout_manager.py create mode 100644 examples/templates/rollout_manager/rollout_worker.py diff --git a/examples/cim/ac.py b/examples/cim/ac.py new file mode 100644 index 000000000..f4dc3cfc9 --- /dev/null +++ b/examples/cim/ac.py @@ -0,0 +1,120 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +import numpy as np +import torch + +from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceManager, FullyConnectedBlock, OptimOption + +cim_path = os.path.dirname(os.path.dirname(__file__)) +sys.path.insert(cim_path) +from env_wrapper import config as env_config + +""" +experience_manager: + capacity: 400 + overwrite_type: "rolling" + batch_size: -1 + replace: False + update_trigger: + min_new_experiences: 1 + num_warmup_experiences: 1 + + +""" + + + +config = { + "model": { + "network": { + "actor": { + "hidden_dims": [256, 128, 64], + "activation": "tanh", + "softmax": True, + "batch_norm": False, + "head": True + }, + "critic": { + "hidden_dims": [256, 128, 64], + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "head": True + } + }, + "optimization": { + "actor": { + "optim_cls": "adam", + "optim_params": {"lr": 0.001} + }, + "critic": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.001} + } + } + }, + "algorithm": { + "reward_discount": .0, + "critic_loss_cls": "smooth_l1", + "train_epochs": 10, + "actor_loss_coefficient": 0.1, + # "clip_ratio": 0.8 # for PPO + }, + "experience_manager": { + "rollout": { # for experience managers in actor processes + "capacity": 1000, + "overwrite_type": "rolling", + "batch_size": -1, + "replace": False + }, + "training": { # for experience managers in the learner process + "capacity": 100000, + "overwrite_type": "rolling", + "batch_size": 128, + "alpha": 0.6, + "beta": 0.4, + "beta_step": 0.001 + } + }, + "update_trigger": 1, + "warmup": 1 +} + +config["model"]["network"]["input_dim"] = ( + (env_config["wrapper"]["look_back"] + 1) + * (env_config["wrapper"]["max_ports_downstream"] + 1) + * len(env_config["wrapper"]["port_attributes"]) + + len(env_config["wrapper"]["vessel_attributes"]) +) +config["model"]["network"]["output_dim"] = env_config["wrapper"]["num_actions"] + +def get_ac_policy(name): + class MyACNET(DiscreteACNet): + def forward(self, states, actor: bool = True, critic: bool = True): + states = torch.from_numpy(np.asarray(states)) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + + states = states.to(self.device) + return ( + self.component["actor"](states) if actor else None, + self.component["critic"](states) if critic else None + ) + + cfg = config["policy"] + ac_net = MyACNET( + component={ + "actor": FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["network"]["actor"]), + "critic": FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **cfg["model"]["network"]["critic"]) + }, + optim_option={ + "actor": OptimOption(**cfg["model"]["optimization"]["actor"]), + "critic": OptimOption(**cfg["model"]["optimization"]["critic"]) + } + ) + experience_manager = ExperienceManager(**cfg["experience_manager"]) + return ActorCritic(name, ac_net, experience_manager, ActorCriticConfig(**cfg["algorithm_config"])) diff --git a/examples/cim/ac/config.yml b/examples/cim/ac/config.yml deleted file mode 100644 index 37f1d4997..000000000 --- a/examples/cim/ac/config.yml +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -experiment_name: cim_ac -mode: multi-process # local, multi-process -num_episodes: 50 -num_steps: -1 -env: - basic: - scenario: cim - topology: toy.4p_ssdd_l0.0 - durations: 1120 - wrapper: - port_attributes: - - empty - - full - - on_shipper - - on_consignee - - booking - - shortage - - fulfillment - vessel_attributes: - - empty - - full - - remaining_space - num_actions: 21 - # Parameters for computing states - look_back: 7 - max_ports_downstream: 2 - # Parameters for computing actions - finite_vessel_space: true - has_early_discharge: true - # Parameters for computing rewards - reward_eval_delay: 99 - fulfillment_factor: 1.0 - shortage_factor: 1.0 - time_decay: 0.97 -policy: - model: - network: - actor: - hidden_dims: - - 256 - - 128 - - 64 - activation: tanh - softmax: true - batch_norm: False - head: true - critic: - hidden_dims: - - 256 - - 128 - - 64 - activation: leaky_relu - softmax: false - batch_norm: true - head: true - optimization: - actor: - optim_cls: adam - optim_params: - lr: 0.001 - critic: - optim_cls: rmsprop - optim_params: - lr: 0.001 - algorithm_config: - reward_discount: .0 - critic_loss_cls: smooth_l1 - train_epochs: 10 - actor_loss_coefficient: 0.1 - # clip_ratio: 0.8 # for PPO - experience_manager: - capacity: 400 - overwrite_type: "rolling" - batch_size: -1 - replace: False - update_trigger: - min_new_experiences: 1 - num_warmup_experiences: 1 \ No newline at end of file diff --git a/examples/cim/ac/main.py b/examples/cim/ac/main.py deleted file mode 100644 index 6b3a6a348..000000000 --- a/examples/cim/ac/main.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import yaml -from os import getenv -from os.path import dirname, join, realpath - -import numpy as np -import torch - -from maro.rl import ( - ActorCritic, ActorCriticConfig, AgentWrapper, DiscreteACNet, ExperienceManager, FullyConnectedBlock, LocalLearner, - OptimOption -) -from maro.simulator import Env -from maro.utils import set_seeds - -from examples.cim.env_wrapper import CIMEnvWrapper - - -DEFAULT_CONFIG_PATH = join(dirname(realpath(__file__)), "config.yml") -with open(getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: - config = yaml.safe_load(config_file) - -# model input and output dimensions -IN_DIM = ( - (config["env"]["wrapper"]["look_back"] + 1) * - (config["env"]["wrapper"]["max_ports_downstream"] + 1) * - len(config["env"]["wrapper"]["port_attributes"]) + - len(config["env"]["wrapper"]["vessel_attributes"]) -) -OUT_DIM = config["env"]["wrapper"]["num_actions"] - - -def get_ac_policy(name): - class MyACNET(DiscreteACNet): - def forward(self, states, actor: bool = True, critic: bool = True): - states = torch.from_numpy(np.asarray(states)) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - - states = states.to(self.device) - return ( - self.component["actor"](states) if actor else None, - self.component["critic"](states) if critic else None - ) - - cfg = config["policy"] - ac_net = MyACNET( - component={ - "actor": FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["network"]["actor"]), - "critic": FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **cfg["model"]["network"]["critic"]) - }, - optim_option={ - "actor": OptimOption(**cfg["model"]["optimization"]["actor"]), - "critic": OptimOption(**cfg["model"]["optimization"]["critic"]) - } - ) - experience_manager = ExperienceManager(**cfg["experience_manager"]) - return ActorCritic(name, ac_net, experience_manager, ActorCriticConfig(**cfg["algorithm_config"])) - - -# Single-threaded launcher -if __name__ == "__main__": - set_seeds(1024) # for reproducibility - env = Env(**config["env"]["basic"]) - env_wrapper = CIMEnvWrapper(env, **config["env"]["wrapper"]) - policies = [get_ac_policy(id_) for id_ in env.agent_idx_list] - agent2policy = {agent_id: agent_id for agent_id in env.agent_idx_list} - agent_wrapper = AgentWrapper(policies, agent2policy) - learner = LocalLearner(env_wrapper, agent_wrapper, 40) # 40 episodes - learner.run() diff --git a/examples/cim/agent_wrapper.py b/examples/cim/agent_wrapper.py new file mode 100644 index 000000000..dd5e4143f --- /dev/null +++ b/examples/cim/agent_wrapper.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +from maro.rl import AgentWrapper, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler + +cim_path = os.path.dirname(__file__) +sys.path.insert(0, cim_path) +from dqn import get_dqn_policy_for_rollout +from env_wrapper import env_config +from meta import CIM_POLICY_NAMES + + +exploration_config = { + "last_ep": 10, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(5, 0.32)] +} + +def get_agent_wrapper(): + epsilon_greedy = EpsilonGreedyExploration(num_actions=env_config["wrapper"]["num_actions"]) + epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **exploration_config + ) + return AgentWrapper( + policies=[get_dqn_policy_for_rollout(name) for name in CIM_POLICY_NAMES], + agent2policy={name: name for name in CIM_POLICY_NAMES}, + exploration_dict={f"EpsilonGreedy": epsilon_greedy}, + agent2exploration={name: "EpsilonGreedy" for name in CIM_POLICY_NAMES} + ) diff --git a/examples/cim/dqn.py b/examples/cim/dqn.py new file mode 100644 index 000000000..f0afe1e31 --- /dev/null +++ b/examples/cim/dqn.py @@ -0,0 +1,96 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +import numpy as np +import torch +import torch.nn as nn +from maro.rl import DQN, DQNConfig, DiscreteQNet, ExperienceManager, FullyConnectedBlock, OptimOption + +cim_path = os.path.dirname(os.path.dirname(__file__)) +sys.path.insert(0, cim_path) +from env_wrapper import CIM_STATE_DIM, env_config + +config = { + "model": { + "network": { + "input_dim": CIM_STATE_DIM, + "hidden_dims": [256, 128, 64, 32], + "output_dim": env_config["wrapper"]["num_actions"], + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 + }, + "optimization": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.05} + } + }, + "algorithm": { + "reward_discount": .0, + "target_update_freq": 5, + "train_epochs": 10, + "soft_update_coefficient": 0.1, + "double": False + }, + "experience_manager": { + "rollout": { # for experience managers in actor processes + "capacity": 1000, + "overwrite_type": "rolling", + "batch_size": -1, + "replace": False + }, + "training": { # for experience managers in the learner process + "capacity": 100000, + "overwrite_type": "rolling", + "batch_size": 128, + "alpha": 0.6, + "beta": 0.4, + "beta_step": 0.001 + } + }, + "update_trigger": 16, + "warmup": 1 +} + + +class QNet(DiscreteQNet): + def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): + super().__init__(component, optim_option=optim_option, device=device) + + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component(states) + + +def get_dqn_policy_for_training(name): + qnet = QNet( + FullyConnectedBlock(**config["model"]["network"]), + optim_option=OptimOption(**config["model"]["optimization"]) + ) + return DQN( + name=name, + q_net=qnet, + experience_manager=ExperienceManager(**config["experience_manager"]["training"]), + config=DQNConfig(**config["algorithm"]), + update_trigger=config["update_trigger"], + warmup=config["warmup"] + ) + + +def get_dqn_policy_for_rollout(name): + qnet = QNet(FullyConnectedBlock(**config["model"]["network"])) + return DQN( + name=name, + q_net=qnet, + experience_manager=ExperienceManager(**config["experience_manager"]["rollout"]), + config=DQNConfig(**config["algorithm"]), + update_trigger=1e8 # set to a large number to ensure that the roll-out workers don't update policies + ) diff --git a/examples/cim/dqn/__init__.py b/examples/cim/dqn/__init__.py deleted file mode 100644 index b14b47650..000000000 --- a/examples/cim/dqn/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. diff --git a/examples/cim/dqn/agent_wrapper.py b/examples/cim/dqn/agent_wrapper.py deleted file mode 100644 index 6cdfa86a2..000000000 --- a/examples/cim/dqn/agent_wrapper.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl import AgentWrapper, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler - -dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory -cim_path = os.path.dirname(dqn_path) # CIM example directory -sys.path.insert(0, cim_path) -sys.path.insert(0, dqn_path) -from general import AGENT_IDS, NUM_ACTIONS, config, log_dir -from policy import get_independent_policy_for_rollout - -def get_agent_wrapper(): - epsilon_greedy = EpsilonGreedyExploration(num_actions=NUM_ACTIONS) - epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - last_ep=config["num_episodes"], - **config["exploration"] - ) - return AgentWrapper( - policies=[get_independent_policy_for_rollout(i) for i in AGENT_IDS], - agent2policy={i: i for i in AGENT_IDS}, - exploration_dict={f"EpsilonGreedy": epsilon_greedy}, - agent2exploration={i: "EpsilonGreedy" for i in AGENT_IDS}, - log_dir=log_dir - ) diff --git a/examples/cim/dqn/config.yml b/examples/cim/dqn/config.yml deleted file mode 100644 index 4f711c8b5..000000000 --- a/examples/cim/dqn/config.yml +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -experiment_name: cim-dqn -mode: sync -num_episodes: 10 -eval_schedule: 2 -num_steps: 50 -env: - basic: - scenario: cim - topology: toy.4p_ssdd_l0.0 - durations: 560 - wrapper: - port_attributes: - - empty - - full - - on_shipper - - on_consignee - - booking - - shortage - - fulfillment - vessel_attributes: - - empty - - full - - remaining_space - num_actions: 21 - # Parameters for computing states - look_back: 7 - max_ports_downstream: 2 - # Parameters for computing actions - finite_vessel_space: true - has_early_discharge: true - # Parameters for computing rewards - reward_eval_delay: 99 - fulfillment_factor: 1.0 - shortage_factor: 1.0 - time_decay: 0.97 -exploration: - initial_value: 0.4 - final_value: 0.0 - splits: - - [5, 0.32] -policies: - model: - network: - hidden_dims: - - 256 - - 128 - - 64 - - 32 - activation: leaky_relu - softmax: false - batch_norm: true - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: rmsprop - optim_params: - lr: 0.05 - algorithm_config: - reward_discount: .0 - target_update_freq: 5 - train_epochs: 10 - soft_update_coefficient: 0.1 - double: false - experience_manager: - rollout: # for experience managers in actor processes - capacity: 1000 - overwrite_type: "rolling" - batch_size: -1 - replace: false - training: # for experience managers in the learner process - capacity: 100000 - overwrite_type: "rolling" - batch_size: 128 - alpha: 0.6 - beta: 0.4 - beta_step: 0.001 - update_trigger: 16 - warmup: 1 -roll_out: - group: cim-dqn-rollout - mode: multi-node # single-process, multi-process, multi-node - num_workers: 3 - # max_receive_attempts: 2 - # receive_timeout: 100 # in milli-seconds -policy_manager: - group: cim-dqn-policy-manager - policy_training_mode: multi-process # single-process, multi-process, multi-node - num_trainers: 2 -redis: - host: maro-redis - port: 6379 \ No newline at end of file diff --git a/examples/cim/dqn/general.py b/examples/cim/dqn/general.py deleted file mode 100644 index 74e2303f9..000000000 --- a/examples/cim/dqn/general.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import yaml - -from maro.simulator import Env - -FILE_PATH = os.path.dirname(os.path.realpath(__file__)) - -DEFAULT_CONFIG_PATH = os.path.join(FILE_PATH, "config.yml") -with open(os.getenv("CONFIG_PATH", default=DEFAULT_CONFIG_PATH), "r") as config_file: - config = yaml.safe_load(config_file) - -log_dir = os.path.join(FILE_PATH, "logs", config["experiment_name"]) - -# Obtain model input and output dimensions from env wrapper settings -config["policies"]["model"]["network"]["input_dim"] = ( - (config["env"]["wrapper"]["look_back"] + 1) - * (config["env"]["wrapper"]["max_ports_downstream"] + 1) - * len(config["env"]["wrapper"]["port_attributes"]) - + len(config["env"]["wrapper"]["vessel_attributes"]) -) -config["policies"]["model"]["network"]["output_dim"] = config["env"]["wrapper"]["num_actions"] - -NUM_ACTIONS = config["env"]["wrapper"]["num_actions"] -AGENT_IDS = Env(**config["env"]["basic"]).agent_idx_list -NUM_POLICY_TRAINERS = config["policy_manager"]["num_trainers"] diff --git a/examples/cim/dqn/policy.py b/examples/cim/dqn/policy.py deleted file mode 100644 index fb14fcfe5..000000000 --- a/examples/cim/dqn/policy.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -import numpy as np -import torch -import torch.nn as nn -from maro.rl import DQN, DQNConfig, DiscreteQNet, ExperienceManager, FullyConnectedBlock, OptimOption - -dqn_path = os.path.dirname(os.path.realpath(__file__)) # DQN directory -cim_path = os.path.dirname(dqn_path) # CIM example directory -sys.path.insert(0, cim_path) -sys.path.insert(0, dqn_path) -from general import config - - -class QNet(DiscreteQNet): - def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): - super().__init__(component, optim_option=optim_option, device=device) - - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component(states) - - -def get_independent_policy_for_training(name): - cfg = config["policies"] - qnet = QNet( - FullyConnectedBlock(**cfg["model"]["network"]), - optim_option=OptimOption(**cfg["model"]["optimization"]) - ) - return DQN( - name=name, - q_net=qnet, - experience_manager=ExperienceManager(**cfg["experience_manager"]["training"]), - config=DQNConfig(**cfg["algorithm_config"]), - update_trigger=cfg["update_trigger"], - warmup=cfg["warmup"] - ) - - -def get_independent_policy_for_rollout(name): - cfg = config["policies"] - qnet = QNet(FullyConnectedBlock(**cfg["model"]["network"])) - return DQN( - name=name, - q_net=qnet, - experience_manager=ExperienceManager(**cfg["experience_manager"]["rollout"]), - config=DQNConfig(**cfg["algorithm_config"]), - update_trigger=1e8 # set to a large number to ensure that the roll-out workers don't update policies - ) diff --git a/examples/cim/dqn/scripts/docker_compose_yml.py b/examples/cim/dqn/scripts/docker_compose_yml.py deleted file mode 100644 index fd29c17f7..000000000 --- a/examples/cim/dqn/scripts/docker_compose_yml.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import yaml -from copy import deepcopy -from os.path import dirname, join, realpath - -path = realpath(__file__) -script_dir = dirname(path) -cim_dqn_sync_dir = join(dirname(script_dir), "sync_mode") -cim_dqn_dir = dirname(cim_dqn_sync_dir) -cim_dir = dirname(cim_dqn_dir) -root_dir = dirname(dirname(cim_dir)) -maro_rl_dir = join(root_dir, "maro", "rl") -maro_comm_dir = join(root_dir, "maro", "communication") -config_path = join(cim_dqn_dir, "config.yml") -dockerfile_path = join(root_dir, "docker_files", "dev.df") - -with open(config_path, "r") as fp: - config = yaml.safe_load(fp) - num_trainers = config["policy_manager"]["num_trainers"] - redis_host = config["redis"]["host"] - -docker_compose_manifest = {"version": "3.9", "services": {"redis": {"image": "redis:6", "container_name": redis_host}}} -common_spec = { - "build": {"context": root_dir, "dockerfile": dockerfile_path}, - "image": "maro-cim", - "volumes": [ - f"{cim_dir}:/maro/cim", - f"{maro_rl_dir}:/maro/maro/rl", - f"{maro_comm_dir}:/maro/maro/communication" - ] -} - -# trainer spec -if config["policy_manager"]["policy_training_mode"] == "multi-node": - for i in range(num_trainers): - trainer_id = f"TRAINER.{i}" - trainer_spec = deepcopy(common_spec) - del trainer_spec["build"] - trainer_spec["command"] = "python3 /maro/cim/dqn/policy_manager/trainer.py" - trainer_spec["container_name"] = trainer_id - trainer_spec["environment"] = [f"TRAINERID={trainer_id}"] - docker_compose_manifest["services"][trainer_id] = trainer_spec - -# learner_spec -docker_compose_manifest["services"]["learner"] = { - **common_spec, - **{"container_name": "learner", "command": "python3 /maro/cim/dqn/sync_mode/learner.py"} -} - -# rollout worker spec -if config["roll_out"]["mode"] == "multi-node": - for i in range(config["roll_out"]["num_workers"]): - actor_id = f"ROLLOUT_WORKER.{i}" - actor_spec = deepcopy(common_spec) - del actor_spec["build"] - actor_spec["command"] = "python3 /maro/cim/dqn/sync_mode/rollout_worker.py" - actor_spec["container_name"] = actor_id - actor_spec["environment"] = [f"WORKERID={actor_id}"] - docker_compose_manifest["services"][actor_id] = actor_spec - -with open(join(cim_dqn_dir, "docker-compose.yml"), "w") as fp: - yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/cim/dqn/sync_mode/docker-compose.yml b/examples/cim/dqn/sync_mode/docker-compose.yml deleted file mode 100644 index c29321712..000000000 --- a/examples/cim/dqn/sync_mode/docker-compose.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - redis: - container_name: localhost - image: redis:6 -version: '3.9' diff --git a/examples/cim/dqn/sync_mode/rollout_manager.py b/examples/cim/dqn/sync_mode/rollout_manager.py deleted file mode 100644 index c0812ead2..000000000 --- a/examples/cim/dqn/sync_mode/rollout_manager.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl import EpsilonGreedyExploration, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager -from maro.simulator import Env - -sync_mode_path = os.path.dirname(os.path.realpath(__file__)) # DQN sync mode directory -dqn_path = os.path.dirname(sync_mode_path) # DQN directory -cim_path = os.path.dirname(dqn_path) # CIM example directory -sys.path.insert(0, cim_path) -sys.path.insert(0, dqn_path) -sys.path.insert(0, sync_mode_path) -from agent_wrapper import get_agent_wrapper -from env_wrapper import CIMEnvWrapper -from general import NUM_ACTIONS, config, log_dir -from policy import get_independent_policy_for_rollout - -def get_rollout_manager(): - rollout_mode = config["roll_out"]["mode"] - if rollout_mode == "single-process": - env = Env(**config["env"]["basic"]) - return LocalRolloutManager( - CIMEnvWrapper(env, **config["env"]["wrapper"]), - [get_independent_policy_for_rollout(i) for i in env.agent_idx_list], - {i: i for i in env.agent_idx_list}, - num_steps=config["num_steps"], - exploration_dict={f"EpsilonGreedy": EpsilonGreedyExploration(num_actions=NUM_ACTIONS)}, - agent2exploration={i: "EpsilonGreedy" for i in env.agent_idx_list}, - log_dir=log_dir - ) - if rollout_mode == "multi-process": - return MultiProcessRolloutManager( - config["sync"]["num_rollout_workers"], - lambda: CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]), - get_agent_wrapper, - num_steps=config["num_steps"], - log_dir=log_dir, - ) - if rollout_mode == "multi-node": - return MultiNodeRolloutManager( - config["roll_out"]["group"], - config["roll_out"]["num_workers"], - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} - ) - - raise ValueError( - f"Unsupported roll-out mode: {rollout_mode}. Supported modes: single-process, multi-process, multi-node" - ) diff --git a/examples/cim/dqn/sync_mode/rollout_worker.py b/examples/cim/dqn/sync_mode/rollout_worker.py deleted file mode 100644 index c1ee6eb7a..000000000 --- a/examples/cim/dqn/sync_mode/rollout_worker.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl import rollout_worker_node -from maro.simulator import Env - -dqn_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # DQN directory -cim_path = os.path.dirname(dqn_path) -sys.path.insert(0, dqn_path) -sys.path.insert(0, cim_path) -from agent_wrapper import get_agent_wrapper -from env_wrapper import CIMEnvWrapper -from general import config, log_dir - - -if __name__ == "__main__": - rollout_worker_node( - config["roll_out"]["group"], - CIMEnvWrapper(Env(**config["env"]["basic"]), **config["env"]["wrapper"]), - get_agent_wrapper(), - proxy_kwargs={ - "component_name": os.environ["WORKERID"], - "redis_address": (config["redis"]["host"], config["redis"]["port"]) - }, - log_dir=log_dir - ) diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index e8bf1557c..8dabe1466 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -4,6 +4,7 @@ import numpy as np from maro.rl import AbsEnvWrapper +from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType @@ -25,6 +26,14 @@ def __init__( self.finite_vessel_space = finite_vessel_space self.has_early_discharge = has_early_discharge self._last_action_tick = None + self._state_dim = ( + (self.look_back + 1) * (self.max_ports_downstream + 1) * len(self.port_attributes) + + len(self.vessel_attributes) + ) + + @property + def state_dim(self): + return self._state_dim def get_state(self, tick=None): if tick is None: @@ -97,3 +106,37 @@ def get_reward(self, tick=None): - self.shortage_factor * np.dot(future_shortage.T, decay_list) ) return {agent_id: reward for agent_id, reward in zip(ports, rewards)} + + +env_config = { + "basic": { + "scenario": "cim", + "topology": "toy.4p_ssdd_l0.0", + "durations": 560 + }, + "wrapper": { + "port_attributes": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], + "vessel_attributes": ["empty", "full", "remaining_space"], + "num_actions": 21, + # Parameters for computing states + "look_back": 7, + "max_ports_downstream": 2, + # Parameters for computing actions + "finite_vessel_space": True, + "has_early_discharge": True, + # Parameters for computing rewards + "reward_eval_delay": 99, + "fulfillment_factor": 1.0, + "shortage_factor": 1.0, + "time_decay": 0.97 + } +} + +def get_cim_env_wrapper(): + return CIMEnvWrapper(Env(**env_config["basic"]), **env_config["wrapper"]) + + +tmp_env_wrapper = get_cim_env_wrapper() +CIM_AGENT_IDS = tmp_env_wrapper.agent_idx_list +CIM_STATE_DIM = tmp_env_wrapper.state_dim +del tmp_env_wrapper diff --git a/examples/cim/meta.py b/examples/cim/meta.py new file mode 100644 index 000000000..344a405cc --- /dev/null +++ b/examples/cim/meta.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +cim_path = os.path.dirname(__file__) +sys.path.insert(0, cim_path) +from dqn import get_dqn_policy_for_rollout, get_dqn_policy_for_training +from env_wrapper import CIM_AGENT_IDS + +CIM_POLICY_NAMES = CIM_AGENT_IDS # use agent IDs as policy names since each agent uses a separate policy +CIM_CREATE_POLICY_FUNC = {name: get_dqn_policy_for_training for name in CIM_POLICY_NAMES} diff --git a/examples/config.yml b/examples/config.yml new file mode 100644 index 000000000..4ab535535 --- /dev/null +++ b/examples/config.yml @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +experiment_name: cim-dqn +scenario: cim +mode: sync +num_episodes: 10 +eval_schedule: 2 +num_steps: 50 +rollout: + group: rollout + mode: multi-node # single-process, multi-process, multi-node + num_workers: 3 + # max_receive_attempts: 2 + # receive_timeout: 100 # in milli-seconds +policy_manager: + group: policy-manager + training_mode: multi-process # single-process, multi-process, multi-node + num_trainers: 2 +redis: + host: maro-redis + port: 6379 \ No newline at end of file diff --git a/examples/general.py b/examples/general.py new file mode 100644 index 000000000..405adc675 --- /dev/null +++ b/examples/general.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys +import yaml + +example_dir = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, example_dir) + +from cim.env_wrapper import get_cim_env_wrapper +from cim.agent_wrapper import get_agent_wrapper +from cim.meta import CIM_AGENT_IDS, CIM_CREATE_POLICY_FUNC, CIM_POLICY_NAMES + +config_path = os.path.join(example_dir, "config.yml") +with open(config_path, "r") as config_file: + config = yaml.safe_load(config_file) + +log_dir = os.path.join(example_dir, "logs", config["experiment_name"]) + +get_env_wrapper_func_index = {"cim": get_cim_env_wrapper} +get_agent_wrapper_func_index = {"cim": get_agent_wrapper} + +agent_ids_index = {"cim": CIM_AGENT_IDS} +policy_names_index = {"cim": CIM_POLICY_NAMES} +create_policy_func_index = {"cim": CIM_CREATE_POLICY_FUNC} diff --git a/examples/cim/dqn/scripts/build.sh b/examples/scripts/build.sh similarity index 100% rename from examples/cim/dqn/scripts/build.sh rename to examples/scripts/build.sh diff --git a/examples/scripts/docker_compose_yml.py b/examples/scripts/docker_compose_yml.py new file mode 100644 index 000000000..9d13d9464 --- /dev/null +++ b/examples/scripts/docker_compose_yml.py @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import yaml +from copy import deepcopy +from os.path import dirname, join, realpath + +path = realpath(__file__) +script_dir = dirname(path) +example_dir = dirname(script_dir) +root_dir = dirname(example_dir) +maro_rl_dir = join(root_dir, "maro", "rl") +maro_comm_dir = join(root_dir, "maro", "communication") +config_path = join(example_dir, "config.yml") +dockerfile_path = join(root_dir, "docker_files", "dev.df") + +with open(config_path, "r") as fp: + config = yaml.safe_load(fp) + num_trainers = config["policy_manager"]["num_trainers"] + redis_host = config["redis"]["host"] + +docker_compose_manifest = {"version": "3.9", "services": {"redis": {"image": "redis:6", "container_name": redis_host}}} +common_spec = { + "build": {"context": root_dir, "dockerfile": dockerfile_path}, + "image": "maro-cim", + "volumes": [ + f"{example_dir}:/maro/examples", + f"{maro_rl_dir}:/maro/maro/rl", + f"{maro_comm_dir}:/maro/maro/communication" + ] +} + +# trainer spec +if config["policy_manager"]["training_mode"] == "multi-node": + for trainer_id in range(num_trainers): + str_id = f"trainer.{trainer_id}" + trainer_spec = deepcopy(common_spec) + del trainer_spec["build"] + trainer_spec["command"] = "python3 /maro/examples/templates/policy_manager/trainer.py" + trainer_spec["container_name"] = str_id + trainer_spec["environment"] = [f"TRAINERID={trainer_id}"] + docker_compose_manifest["services"][str_id] = trainer_spec + +# learner_spec +docker_compose_manifest["services"]["learner"] = { + **common_spec, + **{"container_name": "learner", "command": "python3 /maro/examples/templates/learner.py"} +} + +# rollout worker spec +if config["rollout"]["mode"] == "multi-node": + for worker_id in range(config["rollout"]["num_workers"]): + str_id = f"rollout_worker.{worker_id}" + worker_spec = deepcopy(common_spec) + del worker_spec["build"] + worker_spec["command"] = "python3 /maro/examples/templates/rollout_manager/rollout_worker.py" + worker_spec["container_name"] = str_id + worker_spec["environment"] = [f"WORKERID={worker_id}"] + docker_compose_manifest["services"][str_id] = worker_spec + +with open(join(example_dir, "docker-compose.yml"), "w") as fp: + yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/cim/dqn/scripts/kill.sh b/examples/scripts/kill.sh similarity index 100% rename from examples/cim/dqn/scripts/kill.sh rename to examples/scripts/kill.sh diff --git a/examples/cim/dqn/scripts/run.sh b/examples/scripts/run.sh similarity index 100% rename from examples/cim/dqn/scripts/run.sh rename to examples/scripts/run.sh diff --git a/examples/cim/dqn/sync_mode/learner.py b/examples/templates/learner.py similarity index 92% rename from examples/cim/dqn/sync_mode/learner.py rename to examples/templates/learner.py index 1d59852a6..f87f4a4b9 100644 --- a/examples/cim/dqn/sync_mode/learner.py +++ b/examples/templates/learner.py @@ -13,7 +13,7 @@ sys.path.insert(0, sync_mode_path) from general import config, log_dir from policy_manager.policy_manager import get_policy_manager -from rollout_manager import get_rollout_manager +from rollout_manager.rollout_manager import get_rollout_manager if __name__ == "__main__": diff --git a/examples/cim/dqn/policy_manager/policy_manager.py b/examples/templates/policy_manager/policy_manager.py similarity index 57% rename from examples/cim/dqn/policy_manager/policy_manager.py rename to examples/templates/policy_manager/policy_manager.py index d45aa875f..bef7a1245 100644 --- a/examples/cim/dqn/policy_manager/policy_manager.py +++ b/examples/templates/policy_manager/policy_manager.py @@ -6,28 +6,30 @@ from maro.rl import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager -dqn_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # DQN directory -sys.path.insert(0, dqn_path) -from general import AGENT_IDS, NUM_POLICY_TRAINERS, config, log_dir -from policy import get_independent_policy_for_training +example_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # example directory +sys.path.insert(0, example_dir) +from general import config, create_policy_func_index, policy_names_index, log_dir def get_policy_manager(): - policies = [get_independent_policy_for_training(i) for i in AGENT_IDS] - training_mode = config["policy_manager"]["policy_training_mode"] + scenario = config["scenario"] + training_mode = config["policy_manager"]["training_mode"] + num_trainers = config["policy_manager"]["num_trainers"] + policy_names = policy_names_index[scenario] + policies = [create_policy_func_index[scenario][name](name) for name in policy_names] if training_mode == "single-process": return LocalPolicyManager(policies, log_dir=log_dir) if training_mode == "multi-process": return MultiProcessPolicyManager( policies, - {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping - {i: get_independent_policy_for_training for i in AGENT_IDS}, + num_trainers, + create_policy_func_index[scenario], log_dir=log_dir ) if training_mode == "multi-node": return MultiNodePolicyManager( - config["policy_manager"]["group"], policies, - {id_: f"TRAINER.{id_ % NUM_POLICY_TRAINERS}" for id_ in AGENT_IDS}, # policy-trainer mapping + config["policy_manager"]["group"], + num_trainers, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/examples/cim/dqn/policy_manager/trainer.py b/examples/templates/policy_manager/trainer.py similarity index 52% rename from examples/cim/dqn/policy_manager/trainer.py rename to examples/templates/policy_manager/trainer.py index 20dc18a55..5ce83ec5e 100644 --- a/examples/cim/dqn/policy_manager/trainer.py +++ b/examples/templates/policy_manager/trainer.py @@ -8,17 +8,14 @@ dqn_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # DQN async mode directory sys.path.insert(0, dqn_path) -from general import AGENT_IDS, config, log_dir -from policy import get_independent_policy_for_training +from general import config, create_policy_func_index, log_dir if __name__ == "__main__": trainer_node( config["policy_manager"]["group"], - [get_independent_policy_for_training(id_) for id_ in AGENT_IDS], - proxy_kwargs={ - "component_name": os.environ["TRAINERID"], - "redis_address": (config["redis"]["host"], config["redis"]["port"]) - }, + int(os.environ["TRAINERID"]), + create_policy_func_index[config["scenario"]], + proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/examples/templates/rollout_manager/rollout_manager.py b/examples/templates/rollout_manager/rollout_manager.py new file mode 100644 index 000000000..bd25d34c7 --- /dev/null +++ b/examples/templates/rollout_manager/rollout_manager.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +from maro.rl import LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager + +example_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # example directory +sys.path.insert(0, example_dir) +from general import config, get_agent_wrapper_func_index, get_env_wrapper_func_index, log_dir + + +def get_rollout_manager(): + rollout_mode = config["rollout"]["mode"] + if rollout_mode == "single-process": + return LocalRolloutManager( + get_env_wrapper_func_index[config["scenario"]](), + num_steps=config["num_steps"], + log_dir=log_dir + ) + if rollout_mode == "multi-process": + return MultiProcessRolloutManager( + config["rollout"]["num_workers"], + get_env_wrapper_func_index[config["scenario"]], + get_agent_wrapper_func_index[config["scenario"]], + num_steps=config["num_steps"], + log_dir=log_dir, + ) + if rollout_mode == "multi-node": + return MultiNodeRolloutManager( + config["rollout"]["group"], + config["rollout"]["num_workers"], + proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} + ) + + raise ValueError( + f"Unsupported roll-out mode: {rollout_mode}. Supported modes: single-process, multi-process, multi-node" + ) diff --git a/examples/templates/rollout_manager/rollout_worker.py b/examples/templates/rollout_manager/rollout_worker.py new file mode 100644 index 000000000..330f3231c --- /dev/null +++ b/examples/templates/rollout_manager/rollout_worker.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os import environ +from os.path import dirname, realpath + +from maro.rl import rollout_worker_node + +example_path = dirname(dirname(dirname(realpath(__file__)))) # example directory +sys.path.insert(0, example_path) +from general import config, get_agent_wrapper_func_index, get_env_wrapper_func_index, log_dir + + +if __name__ == "__main__": + rollout_worker_node( + config["rollout"]["group"], + int(environ["WORKERID"]), + get_env_wrapper_func_index[config["scenario"]](), + get_agent_wrapper_func_index[config["scenario"]](), + proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + log_dir=log_dir + ) diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/exploration_scheduler.py index 140e858dd..78448e748 100644 --- a/maro/rl/exploration/exploration_scheduler.py +++ b/maro/rl/exploration/exploration_scheduler.py @@ -104,8 +104,8 @@ def __init__( initial_value: float = None ): # validate splits - splits.append([1, initial_value]) - splits.append([last_ep, final_value]) + splits.append((1, initial_value)) + splits.append((last_ep, final_value)) splits.sort() for (ep, _), (ep2, _) in zip(splits, splits[1:]): if ep == ep2: diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index 9a09714c1..4fc0f41eb 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -14,7 +14,7 @@ from maro.utils import Logger from ..message_enums import MsgKey, MsgTag -from .trainer import trainer_process +from .trainer import trainer_process class AbsPolicyManager(ABC): @@ -101,16 +101,18 @@ class MultiProcessPolicyManager(AbsPolicyManager): def __init__( self, policies: List[AbsPolicy], - policy2trainer: Dict[str, str], + num_trainers: int, create_policy_func_dict: Dict[str, Callable], log_dir: str = getcwd(), ): super().__init__(policies) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - self.policy2trainer = policy2trainer + self._policy2trainer = {} self._trainer2policies = defaultdict(list) - for policy_name, trainer_id in policy2trainer.items(): - self._trainer2policies[trainer_id].append(policy_name) + for i, policy in enumerate(policies): + trainer_id = i % num_trainers + self._policy2trainer[policy.name] = f"TRAINER.{trainer_id}" + self._trainer2policies[f"TRAINER.{trainer_id}"].append(policy.name) self._trainer_processes = [] self._manager_end = {} @@ -156,10 +158,11 @@ class MultiNodePolicyManager(AbsPolicyManager): """Policy manager that communicates with a set of remote nodes for parallel training. Args: + policies (List[AbsPolicy]): A list of policies managed by the manager. group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. - policies (List[AbsPolicy]): A list of policies managed by the manager. - policy2trainer (dict): Mapping from policy names to trainer IDs. + num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where + 0 <= i < num_trainers. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at @@ -168,20 +171,23 @@ class MultiNodePolicyManager(AbsPolicyManager): """ def __init__( self, - group: str, policies: List[AbsPolicy], - policy2trainer: Dict[str, str], + group: str, + num_trainers: int, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): super().__init__(policies) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - self.policy2trainer = policy2trainer - self._trainer2policies = defaultdict(list) - for policy_name, trainer_name in self.policy2trainer.items(): - self._trainer2policies[trainer_name].append(policy_name) - peers = {"trainer": len(set(self.policy2trainer.values()))} + peers = {"trainer": [f"TRAINER.{idx}" for idx in range(num_trainers)]} self._proxy = Proxy(group, "policy_manager", peers, **proxy_kwargs) + + self._policy2trainer = {} + self._trainer2policies = defaultdict(list) + for i, policy in enumerate(policies): + trainer_id = i % num_trainers + self._policy2trainer[policy.name] = f"TRAINER.{trainer_id}" + self._trainer2policies[f"TRAINER.{trainer_id}"].append(policy.name) for trainer_name, policy_names in self._trainer2policies.items(): self._proxy.send( SessionMessage( @@ -193,7 +199,7 @@ def __init__( def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): msg_body_by_dest = defaultdict(dict) for policy_name, exp in exp_by_policy.items(): - trainer_id = self.policy2trainer[policy_name] + trainer_id = self._policy2trainer[policy_name] if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py index 91de2f452..e00bd57ba 100644 --- a/maro/rl/training/policy_manager/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -4,10 +4,9 @@ import time from multiprocessing.connection import Connection from os import getcwd -from typing import Callable, Dict, List +from typing import Callable, Dict from maro.communication import Proxy -from maro.rl.policy import AbsCorePolicy from maro.utils import Logger from ..message_enums import MsgKey, MsgTag @@ -34,7 +33,7 @@ def trainer_process( logger = Logger("TRAINER", dump_folder=log_dir) for name, state in initial_policy_states.items(): policy_dict[name].set_state(state) - logger.info(f"Trainer {trainer_id} initialized policy {name}") + logger.info(f"{trainer_id} initialized policy {name}") while True: msg = conn.recv() @@ -55,7 +54,8 @@ def trainer_process( def trainer_node( group: str, - policies: List[AbsCorePolicy], + trainer_id: int, + create_policy_func_dict: Dict[str, Callable], proxy_kwargs: dict = {}, log_dir: str = getcwd() ): @@ -64,13 +64,16 @@ def trainer_node( Args: group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. - policies (List[AbsCorePolicy]): List of policies maintained by the trainer. + trainer_id (int): Integer trainer ID. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ - policy_dict = {policy.name: policy for policy in policies} - proxy = Proxy(group, "trainer", {"policy_manager": 1}, **proxy_kwargs) + policy_dict = {} + proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=f"TRAINER.{trainer_id}", **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) for msg in proxy.receive(): @@ -81,6 +84,7 @@ def trainer_node( if msg.tag == MsgTag.INIT_POLICY_STATE: for name, state in msg.body[MsgKey.POLICY_STATE].items(): + policy_dict[name] = create_policy_func_dict[name](name) policy_dict[name].set_state(state) logger.info(f"{proxy.name} initialized policy {name}") proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) diff --git a/maro/rl/training/sync_tools/rollout_manager.py b/maro/rl/training/sync_tools/rollout_manager.py index c73b68b12..187becfc8 100644 --- a/maro/rl/training/sync_tools/rollout_manager.py +++ b/maro/rl/training/sync_tools/rollout_manager.py @@ -7,12 +7,10 @@ from multiprocessing import Pipe, Process from os import getcwd from random import choices -from typing import Callable, Dict, List +from typing import Callable from maro.communication import Proxy, SessionType from maro.rl.experience import ExperienceSet -from maro.rl.exploration import AbsExploration -from maro.rl.policy import AbsPolicy from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger @@ -62,18 +60,13 @@ class LocalRolloutManager(AbsRolloutManager): """Local roll-out controller. Args: - env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect experiences - for policy training / update. - policies (List[AbsPolicy]): A set of named policies for inference. - agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's - queries to the correct policy. - exploration_dict (Dict[str, AbsExploration]): A set of named exploration schemes. Defaults to None. - agent2exploration (Dict[str, str]): Mapping from agent ID's to exploration scheme ID's. This is used to direct - an agent's query to the correct exploration scheme. Defaults to None. + env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect + experiences for policy training / update. + agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used - as the evaluation environment. Defaults to None. + eval_env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be + used as the evaluation environment. Defaults to None. log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end of each episode. Defaults to True. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at @@ -83,13 +76,10 @@ class LocalRolloutManager(AbsRolloutManager): def __init__( self, - env: AbsEnvWrapper, - policies: List[AbsPolicy], - agent2policy: Dict[str, str], - exploration_dict: Dict[str, AbsExploration] = None, - agent2exploration: Dict[str, str] = None, + env_wrapper: AbsEnvWrapper, + agent_wrapper: AgentWrapper, num_steps: int = -1, - eval_env: AbsEnvWrapper = None, + eval_env_wrapper: AbsEnvWrapper = None, log_env_summary: bool = True, log_dir: str = getcwd(), ): @@ -99,30 +89,9 @@ def __init__( super().__init__() self._logger = Logger("LOCAL_ROLLOUT_MANAGER", dump_folder=log_dir) - self.env = env - self.eval_env = eval_env if eval_env else self.env - - # mappings between agents and policies - self.policy_dict = {policy.name: policy for policy in policies} - self._agent2policy = agent2policy - self._policy = { - agent_id: self.policy_dict[policy_name] for agent_id, policy_name in self._agent2policy.items() - } - self._agent_groups_by_policy = defaultdict(list) - for agent_id, policy_name in agent2policy.items(): - self._agent_groups_by_policy[policy_name].append(agent_id) - - # mappings between exploration schemes and agents - self.exploration_dict = exploration_dict - self._agent_groups_by_exploration = defaultdict(list) - if exploration_dict: - self._agent2exploration = agent2exploration - self._exploration = { - agent_id: self.exploration_dict[exploration_id] - for agent_id, exploration_id in self._agent2exploration.items() - } - for agent_id, exploration_id in self._agent2exploration.items(): - self._agent_groups_by_exploration[exploration_id].append(agent_id) + self.env = env_wrapper + self.eval_env = eval_env_wrapper if eval_env_wrapper else self.env + self.agent = agent_wrapper self._num_steps = num_steps if num_steps > 0 else float("inf") self._log_env_summary = log_env_summary @@ -139,45 +108,26 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): Returns: Experiences for policy training. """ + self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") t0 = time.time() learning_time = 0 num_experiences_collected = 0 - if self.exploration_dict: - exploration_params = { - tuple(agent_ids): self.exploration_dict[exploration_id].parameters - for exploration_id, agent_ids in self._agent_groups_by_exploration.items() - } - self._logger.debug(f"Exploration parameters: {exploration_params}") - + # start of new episode if self.env.state is None: - self._logger.info(f"Collecting data from episode {ep}, segment {segment}") - if self.exploration_dict: - exploration_params = { - tuple(agent_ids): self.exploration_dict[exploration_id].parameters - for exploration_id, agent_ids in self._agent_groups_by_exploration.items() - } - self._logger.debug(f"Exploration parameters: {exploration_params}") - self.env.reset() self.env.start() # get initial state + self.agent.exploration_step() # load policies - self._load_policy_states(policy_state_dict) + self.agent.set_policy_states(policy_state_dict) + # update exploration parameters + self.agent.explore() start_step_index = self.env.step_index + 1 steps_to_go = self._num_steps while self.env.state and steps_to_go > 0: - if self.exploration_dict: - action = { - id_: - self._exploration[id_](self._policy[id_].choose_action(st)) - if id_ in self._exploration else self._policy[id_].choose_action(st) - for id_, st in self.env.state.items() - } - else: - action = {id_: self._policy[id_].choose_action(st) for id_, st in self.env.state.items()} - + action = self.agent.choose_action(self.env.state) self.env.step(action) steps_to_go -= 1 @@ -189,10 +139,6 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): # update the exploration parameters if an episode is finished if not self.env.state: self.episode_complete = True - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() - # performance details if self._log_env_summary: self._logger.info(f"ep {ep}: {self.env.summary}") @@ -218,11 +164,12 @@ def evaluate(self, ep: int, policy_state_dict: dict): Environment summary. """ self._logger.info("Evaluating...") - self._load_policy_states(policy_state_dict) + self.agent.set_policy_states(policy_state_dict) + self.agent.exploit() self.eval_env.reset() self.eval_env.start() # get initial state while self.eval_env.state: - action = {id_: self._policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} + action = self.agent.choose_action(self.eval_env.state) self.eval_env.step(action) if self._log_env_summary: @@ -230,13 +177,6 @@ def evaluate(self, ep: int, policy_state_dict: dict): return self.eval_env.summary - def _load_policy_states(self, policy_state_dict: dict): - for policy_name, policy_state in policy_state_dict.items(): - self.policy_dict[policy_name].set_state(policy_state) - - if policy_state_dict: - self._logger.info(f"updated policies {list(policy_state_dict.keys())}") - class MultiProcessRolloutManager(AbsRolloutManager): """Roll-out manager that spawns a set of roll-out worker processes for parallel data collection. @@ -429,7 +369,7 @@ def __init__( peers = {"rollout_worker": num_workers} self._proxy = Proxy(group, "rollout_manager", peers, **proxy_kwargs) self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's - + print(self._workers) self._num_steps = num_steps if max_receive_attempts is None: @@ -460,9 +400,6 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): Returns: Experiences for policy training. """ - if self._log_env_summary: - self._logger.info(f"EPISODE-{ep}, SEGMENT-{segment}: ") - msg_body = { MsgKey.EPISODE: ep, MsgKey.SEGMENT: segment, diff --git a/maro/rl/training/sync_tools/rollout_worker.py b/maro/rl/training/sync_tools/rollout_worker.py index bca2ffd94..b176b57eb 100644 --- a/maro/rl/training/sync_tools/rollout_worker.py +++ b/maro/rl/training/sync_tools/rollout_worker.py @@ -82,11 +82,10 @@ def collect(msg): def evaluate(msg): logger.info("Evaluating...") + agent_wrapper.set_policy_states(msg["policy"]) agent_wrapper.exploit() eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state - if hasattr(agent_wrapper, "update"): - agent_wrapper.set_policy_states(msg["policy"]) while eval_env_wrapper.state: action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) @@ -105,6 +104,7 @@ def evaluate(msg): def rollout_worker_node( group: str, + worker_id: int, env_wrapper: AbsEnvWrapper, agent_wrapper: AgentWrapper, eval_env_wrapper: AbsEnvWrapper = None, @@ -116,6 +116,8 @@ def rollout_worker_node( Args: group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager that manages them. + worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". + This is used for bookkeeping by the parent manager. env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. eval_env_wrapper (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training @@ -126,15 +128,17 @@ def rollout_worker_node( """ eval_env_wrapper = env_wrapper if not eval_env_wrapper else eval_env_wrapper - proxy = Proxy(group, "rollout_worker", {"rollout_manager": 1}, **proxy_kwargs) + proxy = Proxy( + group, "rollout_worker", {"rollout_manager": 1}, + component_name=f"ROLLOUT_WORKER.{int(worker_id)}", **proxy_kwargs + ) logger = Logger(proxy.name, dump_folder=log_dir) def collect(msg): ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] # load policies - if msg.body[MsgKey.POLICY_STATE]: - agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) + agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) # set exploration parameters agent_wrapper.explore() @@ -175,11 +179,10 @@ def collect(msg): def evaluate(msg): logger.info("Evaluating...") + agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) + agent_wrapper.exploit() eval_env_wrapper.reset() eval_env_wrapper.start() # get initial state - agent_wrapper.exploit() - if hasattr(agent_wrapper, "update"): - agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) while eval_env_wrapper.state: action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) diff --git a/maro/rl/wrappers/agent_wrapper.py b/maro/rl/wrappers/agent_wrapper.py index 0a33be8bc..13d0406a0 100644 --- a/maro/rl/wrappers/agent_wrapper.py +++ b/maro/rl/wrappers/agent_wrapper.py @@ -1,12 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from os import getcwd from typing import Dict, List from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsPolicy -from maro.utils import Logger class AgentWrapper: @@ -27,8 +25,7 @@ def __init__( policies: List[AbsPolicy], agent2policy: Dict[str, str], exploration_dict: Dict[str, AbsExploration] = None, - agent2exploration: Dict[str, str] = None, - log_dir: str = getcwd() + agent2exploration: Dict[str, str] = None ): self.policy_dict = {policy.name: policy for policy in policies} self.agent2policy = agent2policy @@ -39,7 +36,6 @@ def __init__( agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() } self.exploring = True # Flag indicating that exploration is turned on. - self._logger = Logger("local_agent_wrapper", dump_folder=log_dir) def choose_action(self, state: dict) -> dict: """Generate an action based on the given state. @@ -74,9 +70,6 @@ def set_policy_states(self, policy_state_dict: dict): for policy_id, policy_state in policy_state_dict.items(): self.policy_dict[policy_id].set_state(policy_state) - if policy_state_dict: - self._logger.info(f"updated policies {list(policy_state_dict.keys())}") - def exploration_step(self): if self.exploration_dict: for exploration in self.exploration_dict.values(): From f427b070cd1c67f1ef52aab9e11d9f7764c21547 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 30 Jun 2021 04:19:26 +0000 Subject: [PATCH 329/482] fixed lint issues --- maro/rl/training/policy_manager/policy_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index 4fc0f41eb..8283197d5 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -14,7 +14,8 @@ from maro.utils import Logger from ..message_enums import MsgKey, MsgTag -from .trainer import trainer_process + +from .trainer import trainer_process class AbsPolicyManager(ABC): @@ -162,7 +163,7 @@ class MultiNodePolicyManager(AbsPolicyManager): group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where - 0 <= i < num_trainers. + 0 <= i < num_trainers. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at From b8dc7e42975b1841796b2ef90859c04ba496d955 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 30 Jun 2021 04:24:52 +0000 Subject: [PATCH 330/482] fixed lint issues --- maro/rl/training/policy_manager/policy_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index 8283197d5..58a934f95 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -14,7 +14,6 @@ from maro.utils import Logger from ..message_enums import MsgKey, MsgTag - from .trainer import trainer_process From 92a51da4f0d15ee4f1e6db96712bfbdc536a1de7 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 30 Jun 2021 06:10:49 +0000 Subject: [PATCH 331/482] fixed template bugs --- examples/cim/ac.py | 34 ++++------------ examples/cim/agent_wrapper.py | 4 +- examples/cim/dqn.py | 32 +++++++-------- examples/config.yml | 2 +- examples/templates/learner.py | 8 ++-- .../policy_manager/policy_manager.py | 8 ++-- examples/templates/policy_manager/trainer.py | 5 ++- maro/rl/algorithms/ac.py | 4 +- maro/rl/algorithms/ddpg.py | 4 +- maro/rl/algorithms/dqn.py | 4 +- maro/rl/algorithms/pg.py | 4 +- maro/rl/policy/policy.py | 14 ++----- .../training/policy_manager/policy_manager.py | 40 +++++++++---------- maro/rl/training/policy_manager/trainer.py | 4 +- .../rl/training/sync_tools/rollout_manager.py | 2 +- maro/rl/training/sync_tools/rollout_worker.py | 4 +- maro/rl/wrappers/agent_wrapper.py | 6 +-- 17 files changed, 72 insertions(+), 107 deletions(-) diff --git a/examples/cim/ac.py b/examples/cim/ac.py index f4dc3cfc9..0e24cfa1a 100644 --- a/examples/cim/ac.py +++ b/examples/cim/ac.py @@ -10,36 +10,25 @@ from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceManager, FullyConnectedBlock, OptimOption cim_path = os.path.dirname(os.path.dirname(__file__)) -sys.path.insert(cim_path) -from env_wrapper import config as env_config - -""" -experience_manager: - capacity: 400 - overwrite_type: "rolling" - batch_size: -1 - replace: False - update_trigger: - min_new_experiences: 1 - num_warmup_experiences: 1 - - -""" - - +sys.path.insert(0, cim_path) +from env_wrapper import CIM_STATE_DIM, env_config config = { "model": { "network": { "actor": { + "input_dim": CIM_STATE_DIM, "hidden_dims": [256, 128, 64], + "output_dim": env_config["wrapper"]["num_actions"], "activation": "tanh", "softmax": True, "batch_norm": False, "head": True }, "critic": { + "input_dim": CIM_STATE_DIM, "hidden_dims": [256, 128, 64], + "output_dim": env_config["wrapper"]["num_actions"], "activation": "leaky_relu", "softmax": False, "batch_norm": True, @@ -84,13 +73,6 @@ "warmup": 1 } -config["model"]["network"]["input_dim"] = ( - (env_config["wrapper"]["look_back"] + 1) - * (env_config["wrapper"]["max_ports_downstream"] + 1) - * len(env_config["wrapper"]["port_attributes"]) - + len(env_config["wrapper"]["vessel_attributes"]) -) -config["model"]["network"]["output_dim"] = env_config["wrapper"]["num_actions"] def get_ac_policy(name): class MyACNET(DiscreteACNet): @@ -108,8 +90,8 @@ def forward(self, states, actor: bool = True, critic: bool = True): cfg = config["policy"] ac_net = MyACNET( component={ - "actor": FullyConnectedBlock(input_dim=IN_DIM, output_dim=OUT_DIM, **cfg["model"]["network"]["actor"]), - "critic": FullyConnectedBlock(input_dim=IN_DIM, output_dim=1, **cfg["model"]["network"]["critic"]) + "actor": FullyConnectedBlock(**cfg["model"]["network"]["actor"]), + "critic": FullyConnectedBlock(**cfg["model"]["network"]["critic"]) }, optim_option={ "actor": OptimOption(**cfg["model"]["optimization"]["actor"]), diff --git a/examples/cim/agent_wrapper.py b/examples/cim/agent_wrapper.py index dd5e4143f..f28ec9e88 100644 --- a/examples/cim/agent_wrapper.py +++ b/examples/cim/agent_wrapper.py @@ -28,8 +28,8 @@ def get_agent_wrapper(): **exploration_config ) return AgentWrapper( - policies=[get_dqn_policy_for_rollout(name) for name in CIM_POLICY_NAMES], - agent2policy={name: name for name in CIM_POLICY_NAMES}, + {name: get_dqn_policy_for_rollout() for name in CIM_POLICY_NAMES}, + {name: name for name in CIM_POLICY_NAMES}, exploration_dict={f"EpsilonGreedy": epsilon_greedy}, agent2exploration={name: "EpsilonGreedy" for name in CIM_POLICY_NAMES} ) diff --git a/examples/cim/dqn.py b/examples/cim/dqn.py index f0afe1e31..6487498a5 100644 --- a/examples/cim/dqn.py +++ b/examples/cim/dqn.py @@ -60,37 +60,35 @@ class QNet(DiscreteQNet): - def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): - super().__init__(component, optim_option=optim_option, device=device) + def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): + super().__init__(component, optim_option=optim_option, device=device) - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component(states) + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component(states) -def get_dqn_policy_for_training(name): +def get_dqn_policy_for_training(): qnet = QNet( FullyConnectedBlock(**config["model"]["network"]), optim_option=OptimOption(**config["model"]["optimization"]) ) return DQN( - name=name, - q_net=qnet, - experience_manager=ExperienceManager(**config["experience_manager"]["training"]), - config=DQNConfig(**config["algorithm"]), + qnet, + ExperienceManager(**config["experience_manager"]["training"]), + DQNConfig(**config["algorithm"]), update_trigger=config["update_trigger"], warmup=config["warmup"] ) -def get_dqn_policy_for_rollout(name): +def get_dqn_policy_for_rollout(): qnet = QNet(FullyConnectedBlock(**config["model"]["network"])) return DQN( - name=name, - q_net=qnet, - experience_manager=ExperienceManager(**config["experience_manager"]["rollout"]), - config=DQNConfig(**config["algorithm"]), + qnet, + ExperienceManager(**config["experience_manager"]["rollout"]), + DQNConfig(**config["algorithm"]), update_trigger=1e8 # set to a large number to ensure that the roll-out workers don't update policies ) diff --git a/examples/config.yml b/examples/config.yml index 4ab535535..0ebd73184 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -15,7 +15,7 @@ rollout: # receive_timeout: 100 # in milli-seconds policy_manager: group: policy-manager - training_mode: multi-process # single-process, multi-process, multi-node + training_mode: multi-node # single-process, multi-process, multi-node num_trainers: 2 redis: host: maro-redis diff --git a/examples/templates/learner.py b/examples/templates/learner.py index f87f4a4b9..b2c53432f 100644 --- a/examples/templates/learner.py +++ b/examples/templates/learner.py @@ -7,10 +7,10 @@ from maro.rl import Learner -sync_mode_path = os.path.dirname(os.path.realpath(__file__)) # DQN sync mode directory -dqn_path = os.path.dirname(sync_mode_path) # DQN directory -sys.path.insert(0, dqn_path) -sys.path.insert(0, sync_mode_path) +template_path = os.path.dirname(os.path.realpath(__file__)) +example_path = os.path.dirname(template_path) +sys.path.insert(0, template_path) +sys.path.insert(0, example_path) from general import config, log_dir from policy_manager.policy_manager import get_policy_manager from rollout_manager.rollout_manager import get_rollout_manager diff --git a/examples/templates/policy_manager/policy_manager.py b/examples/templates/policy_manager/policy_manager.py index bef7a1245..437eee471 100644 --- a/examples/templates/policy_manager/policy_manager.py +++ b/examples/templates/policy_manager/policy_manager.py @@ -15,19 +15,19 @@ def get_policy_manager(): training_mode = config["policy_manager"]["training_mode"] num_trainers = config["policy_manager"]["num_trainers"] policy_names = policy_names_index[scenario] - policies = [create_policy_func_index[scenario][name](name) for name in policy_names] + policy_dict = {name: create_policy_func_index[scenario][name]() for name in policy_names} if training_mode == "single-process": - return LocalPolicyManager(policies, log_dir=log_dir) + return LocalPolicyManager(policy_dict, log_dir=log_dir) if training_mode == "multi-process": return MultiProcessPolicyManager( - policies, + policy_dict, num_trainers, create_policy_func_index[scenario], log_dir=log_dir ) if training_mode == "multi-node": return MultiNodePolicyManager( - policies, + policy_dict, config["policy_manager"]["group"], num_trainers, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, diff --git a/examples/templates/policy_manager/trainer.py b/examples/templates/policy_manager/trainer.py index 5ce83ec5e..4f5d55853 100644 --- a/examples/templates/policy_manager/trainer.py +++ b/examples/templates/policy_manager/trainer.py @@ -3,11 +3,12 @@ import os import sys +from os.path import dirname, realpath from maro.rl import trainer_node -dqn_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # DQN async mode directory -sys.path.insert(0, dqn_path) +example_path = dirname(dirname(dirname(realpath(__file__)))) # DQN async mode directory +sys.path.insert(0, example_path) from general import config, create_policy_func_index, log_dir diff --git a/maro/rl/algorithms/ac.py b/maro/rl/algorithms/ac.py index 9eb39d8df..f3bd4df40 100644 --- a/maro/rl/algorithms/ac.py +++ b/maro/rl/algorithms/ac.py @@ -57,7 +57,6 @@ class ActorCritic(AbsCorePolicy): https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: - name (str): Policy name. ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. @@ -69,7 +68,6 @@ class ActorCritic(AbsCorePolicy): def __init__( self, - name: str, ac_net: DiscreteACNet, experience_manager: ExperienceManager, config: ActorCriticConfig, @@ -79,7 +77,7 @@ def __init__( if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") - super().__init__(name, experience_manager, update_trigger=update_trigger, warmup=warmup) + super().__init__(experience_manager, update_trigger=update_trigger, warmup=warmup) self.ac_net = ac_net self.config = config self.device = self.ac_net.device diff --git a/maro/rl/algorithms/ddpg.py b/maro/rl/algorithms/ddpg.py index 7e1273aed..cb7844689 100644 --- a/maro/rl/algorithms/ddpg.py +++ b/maro/rl/algorithms/ddpg.py @@ -60,7 +60,6 @@ class DDPG(AbsCorePolicy): https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg Args: - name (str): Policy name. ac_net (ContinuousACNet): DDPG policy and q-value models. experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. @@ -71,7 +70,6 @@ class DDPG(AbsCorePolicy): """ def __init__( self, - name: str, ac_net: ContinuousACNet, experience_manager: ExperienceManager, config: DDPGConfig, @@ -81,7 +79,7 @@ def __init__( if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") - super().__init__(name, experience_manager, update_trigger=update_trigger, warmup=warmup) + super().__init__(experience_manager, update_trigger=update_trigger, warmup=warmup) self.ac_net = ac_net if self.ac_net.trainable: self.target_ac_net = ac_net.copy() diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index a58bfe27c..aafffc717 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -51,7 +51,6 @@ class DQN(AbsCorePolicy): See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: - name (str): Policy name. q_net (DiscreteQNet): Q-value model. experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. @@ -62,7 +61,6 @@ class DQN(AbsCorePolicy): """ def __init__( self, - name: str, q_net: DiscreteQNet, experience_manager: ExperienceManager, config: DQNConfig, @@ -72,7 +70,7 @@ def __init__( if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(name, experience_manager, update_trigger=update_trigger, warmup=warmup) + super().__init__(experience_manager, update_trigger=update_trigger, warmup=warmup) self.q_net = q_net if self.q_net.trainable: self.target_q_net = q_net.copy() diff --git a/maro/rl/algorithms/pg.py b/maro/rl/algorithms/pg.py index 8e3f80a5d..6792d784f 100644 --- a/maro/rl/algorithms/pg.py +++ b/maro/rl/algorithms/pg.py @@ -30,7 +30,6 @@ class PolicyGradient(AbsCorePolicy): Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. Args: - name (str): Policy name. policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences @@ -42,7 +41,6 @@ class PolicyGradient(AbsCorePolicy): """ def __init__( self, - name: str, policy_net: DiscretePolicyNet, experience_manager: ExperienceManager, config: PolicyGradientConfig, @@ -51,7 +49,7 @@ def __init__( ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__(name, experience_manager, update_trigger=update_trigger, warmup=warmup) + super().__init__(experience_manager, update_trigger=update_trigger, warmup=warmup) self.policy_net = policy_net self.config = config self.device = self.policy_net.device diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 3477dd9d5..377ddc2dd 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -9,14 +9,9 @@ class AbsPolicy(ABC): """Abstract policy class.""" - def __init__(self, name: str): - self._name = name + def __init__(self): super().__init__() - @property - def name(self): - return self._name - @abstractmethod def choose_action(self, state): raise NotImplementedError @@ -37,7 +32,6 @@ class AbsCorePolicy(AbsPolicy): Reinforcement learning (RL) policies should inherit from this. Args: - name (str): Policy name. experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. @@ -46,12 +40,11 @@ class AbsCorePolicy(AbsPolicy): """ def __init__( self, - name: str, experience_manager: ExperienceManager, update_trigger: int = 1, warmup: int = 1 ): - super().__init__(name) + super().__init__() self.experience_manager = experience_manager self.update_trigger = update_trigger self.warmup = warmup @@ -97,8 +90,7 @@ def on_experiences(self, exp: ExperienceSet) -> bool: self.experience_manager.put(exp) self._new_exp_counter += exp.size print( - f"Policy {self._name}: exp mem size = {self.experience_manager.size}, incoming: {exp.size}, " - f"new exp = {self._new_exp_counter}" + f"exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" ) if self.experience_manager.size >= self.warmup and self._new_exp_counter >= self.update_trigger: t0 = time.time() diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index 58a934f95..00cbd9641 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -23,15 +23,15 @@ class AbsPolicyManager(ABC): The actual policy instances may reside here or be distributed on a set of processes or remote nodes. Args: - policies (List[AbsPolicy]): A list of policies managed by the manager. + policy_dict (Dict[str, AbsCorePolicy]): A list of policies managed by the manager. """ - def __init__(self, policies: List[AbsPolicy]): - for policy in policies: + def __init__(self, policy_dict: Dict[str, AbsCorePolicy]): + for policy in policy_dict.values(): if not isinstance(policy, AbsCorePolicy): raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") super().__init__() - self.policy_dict = {policy.name: policy for policy in policies} + self.policy_dict = policy_dict self.updated = set(self.policy_dict.keys()) self._version = 0 @@ -55,13 +55,13 @@ class LocalPolicyManager(AbsPolicyManager): """Policy manager that contains the actual policy instances. Args: - policies (List[AbsPolicy]): A list of policies managed by the manager. + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ - def __init__(self, policies: List[AbsPolicy], log_dir: str = getcwd()): - super().__init__(policies) + def __init__(self, policy_dict: Dict[str, AbsCorePolicy], log_dir: str = getcwd()): + super().__init__(policy_dict) self._logger = Logger("LOCAL_TRAINING_MANAGER", dump_folder=log_dir) self._new_exp_counter = defaultdict(int) @@ -89,7 +89,7 @@ class MultiProcessPolicyManager(AbsPolicyManager): """Policy manager that spawns a set of trainer processes for parallel training. Args: - policies (List[AbsPolicy]): A list of policies managed by the manager. + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. policy2trainer (dict): Mapping from policy names to trainer IDs. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` @@ -100,19 +100,19 @@ class MultiProcessPolicyManager(AbsPolicyManager): """ def __init__( self, - policies: List[AbsPolicy], + policy_dict: Dict[str, AbsCorePolicy], num_trainers: int, create_policy_func_dict: Dict[str, Callable], log_dir: str = getcwd(), ): - super().__init__(policies) + super().__init__(policy_dict) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._policy2trainer = {} self._trainer2policies = defaultdict(list) - for i, policy in enumerate(policies): + for i, name in enumerate(self.policy_dict): trainer_id = i % num_trainers - self._policy2trainer[policy.name] = f"TRAINER.{trainer_id}" - self._trainer2policies[f"TRAINER.{trainer_id}"].append(policy.name) + self._policy2trainer[name] = f"TRAINER.{trainer_id}" + self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) self._trainer_processes = [] self._manager_end = {} @@ -158,7 +158,7 @@ class MultiNodePolicyManager(AbsPolicyManager): """Policy manager that communicates with a set of remote nodes for parallel training. Args: - policies (List[AbsPolicy]): A list of policies managed by the manager. + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where @@ -171,23 +171,23 @@ class MultiNodePolicyManager(AbsPolicyManager): """ def __init__( self, - policies: List[AbsPolicy], + policy_dict: Dict[str, AbsCorePolicy], group: str, num_trainers: int, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): - super().__init__(policies) + super().__init__(policy_dict) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - peers = {"trainer": [f"TRAINER.{idx}" for idx in range(num_trainers)]} + peers = {"trainer": num_trainers} self._proxy = Proxy(group, "policy_manager", peers, **proxy_kwargs) self._policy2trainer = {} self._trainer2policies = defaultdict(list) - for i, policy in enumerate(policies): + for i, name in enumerate(self.policy_dict): trainer_id = i % num_trainers - self._policy2trainer[policy.name] = f"TRAINER.{trainer_id}" - self._trainer2policies[f"TRAINER.{trainer_id}"].append(policy.name) + self._policy2trainer[name] = f"TRAINER.{trainer_id}" + self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) for trainer_name, policy_names in self._trainer2policies.items(): self._proxy.send( SessionMessage( diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py index e00bd57ba..fd50ac16e 100644 --- a/maro/rl/training/policy_manager/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -29,7 +29,7 @@ def trainer_process( instance. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ - policy_dict = {policy_name: func(policy_name) for policy_name, func in create_policy_func_dict.items()} + policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} logger = Logger("TRAINER", dump_folder=log_dir) for name, state in initial_policy_states.items(): policy_dict[name].set_state(state) @@ -84,7 +84,7 @@ def trainer_node( if msg.tag == MsgTag.INIT_POLICY_STATE: for name, state in msg.body[MsgKey.POLICY_STATE].items(): - policy_dict[name] = create_policy_func_dict[name](name) + policy_dict[name] = create_policy_func_dict[name]() policy_dict[name].set_state(state) logger.info(f"{proxy.name} initialized policy {name}") proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) diff --git a/maro/rl/training/sync_tools/rollout_manager.py b/maro/rl/training/sync_tools/rollout_manager.py index 187becfc8..2a6645ed7 100644 --- a/maro/rl/training/sync_tools/rollout_manager.py +++ b/maro/rl/training/sync_tools/rollout_manager.py @@ -119,7 +119,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): self.env.start() # get initial state self.agent.exploration_step() - # load policies + # set policy states self.agent.set_policy_states(policy_state_dict) # update exploration parameters self.agent.explore() diff --git a/maro/rl/training/sync_tools/rollout_worker.py b/maro/rl/training/sync_tools/rollout_worker.py index b176b57eb..3d178a3e7 100644 --- a/maro/rl/training/sync_tools/rollout_worker.py +++ b/maro/rl/training/sync_tools/rollout_worker.py @@ -42,7 +42,7 @@ def rollout_worker_process( def collect(msg): ep, segment = msg["episode"], msg["segment"] - # load policies + # set policy states agent_wrapper.set_policy_states(msg["policy"]) # update exploration parameters @@ -137,7 +137,7 @@ def rollout_worker_node( def collect(msg): ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] - # load policies + # set policy states agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) # set exploration parameters diff --git a/maro/rl/wrappers/agent_wrapper.py b/maro/rl/wrappers/agent_wrapper.py index 13d0406a0..4821a9fd4 100644 --- a/maro/rl/wrappers/agent_wrapper.py +++ b/maro/rl/wrappers/agent_wrapper.py @@ -11,7 +11,7 @@ class AgentWrapper: """Multi-agent wrapper that interacts with an ``EnvWrapper`` with a unified inferface. Args: - policies (List[AbsPolicy]): A list of policies for inference. + policy_dict (Dict[str, AbsPolicy]): Policies for inference. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. exploration_dict (Dict[str, AbsExploration]): A dictionary of named ``AbsExploration`` instances. Defaults @@ -22,12 +22,12 @@ class AgentWrapper: """ def __init__( self, - policies: List[AbsPolicy], + policy_dict: Dict[str, AbsPolicy], agent2policy: Dict[str, str], exploration_dict: Dict[str, AbsExploration] = None, agent2exploration: Dict[str, str] = None ): - self.policy_dict = {policy.name: policy for policy in policies} + self.policy_dict = policy_dict self.agent2policy = agent2policy self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} self.exploration_dict = exploration_dict From 31b68f3e38055ea5994c147194347e9472d9a4b6 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 30 Jun 2021 06:14:29 +0000 Subject: [PATCH 332/482] removed unused imports --- maro/rl/training/policy_manager/policy_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index 00cbd9641..d74d6d0c1 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -6,11 +6,11 @@ from collections import defaultdict from multiprocessing import Pipe, Process from os import getcwd -from typing import Callable, Dict, List +from typing import Callable, Dict from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.experience import ExperienceSet -from maro.rl.policy import AbsCorePolicy, AbsPolicy +from maro.rl.policy import AbsCorePolicy from maro.utils import Logger from ..message_enums import MsgKey, MsgTag From bab8128545489d2811c695cdef9beb6430bbea3b Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 30 Jun 2021 07:22:12 +0000 Subject: [PATCH 333/482] refactoring sc in progress --- examples/cim/agent_wrapper.py | 4 +- examples/supply_chain/README.md | 8 + examples/supply_chain/__init__.py | 2 + examples/supply_chain/agent_wrapper.py | 76 + examples/supply_chain/config.yml | 95 + examples/supply_chain/dqn.py | 85 + examples/supply_chain/env_wrapper.py | 1187 + .../supply_chain/evaluation_with_render.py | 81 + examples/supply_chain/main.py | 153 + examples/supply_chain/meta.py | 13 + examples/supply_chain/or_policies.py | 132 + examples/supply_chain/or_policy/eoq_policy.py | 12 + examples/supply_chain/render_tools.py | 182 + examples/supply_chain/sc_state_in_maro.md | 312 + .../supply_chain/topologies/random/config.yml | 29344 ++++++++++++++++ .../topologies/sample1/config.yml | 335 + .../supply_chain/topologies/test/config.yml | 367 + .../scenarios/supply_chain/__init__.py | 10 + .../scenarios/supply_chain/actions.py | 9 + .../scenarios/supply_chain/business_engine.py | 174 + .../supply_chain/datamodels/__init__.py | 12 + .../scenarios/supply_chain/datamodels/base.py | 45 + .../supply_chain/datamodels/consumer.py | 46 + .../supply_chain/datamodels/distribution.py | 22 + .../supply_chain/datamodels/extend.py | 33 + .../supply_chain/datamodels/facility.py | 17 + .../supply_chain/datamodels/manufacture.py | 33 + .../supply_chain/datamodels/product.py | 31 + .../supply_chain/datamodels/seller.py | 36 + .../supply_chain/datamodels/storage.py | 59 + .../supply_chain/datamodels/vehicle.py | 31 + .../scenarios/supply_chain/easy_config.py | 33 + .../supply_chain/facilities/__init__.py | 9 + .../supply_chain/facilities/facility.py | 177 + .../supply_chain/facilities/outerretailer.py | 37 + .../supply_chain/facilities/retailer.py | 10 + .../supply_chain/facilities/supplier.py | 10 + .../supply_chain/facilities/warehouse.py | 10 + .../scenarios/supply_chain/frame_builder.py | 18 + .../scenarios/supply_chain/parser.py | 231 + .../supply_chain/topologies/core.yml | 81 + .../supply_chain/topologies/sample/config.yml | 306 + .../scenarios/supply_chain/units/__init__.py | 16 + .../scenarios/supply_chain/units/consumer.py | 174 + .../supply_chain/units/distribution.py | 128 + .../supply_chain/units/extendunitbase.py | 25 + .../supply_chain/units/manufacture.py | 95 + .../scenarios/supply_chain/units/order.py | 7 + .../supply_chain/units/outerseller.py | 118 + .../scenarios/supply_chain/units/product.py | 213 + .../scenarios/supply_chain/units/seller.py | 92 + .../supply_chain/units/simplemanufacture.py | 24 + .../scenarios/supply_chain/units/storage.py | 158 + .../supply_chain/units/storeproduct.py | 13 + .../scenarios/supply_chain/units/unitbase.py | 116 + .../scenarios/supply_chain/units/vehicle.py | 195 + .../simulator/scenarios/supply_chain/world.py | 481 + 57 files changed, 35721 insertions(+), 2 deletions(-) create mode 100644 examples/supply_chain/README.md create mode 100644 examples/supply_chain/__init__.py create mode 100644 examples/supply_chain/agent_wrapper.py create mode 100644 examples/supply_chain/config.yml create mode 100644 examples/supply_chain/dqn.py create mode 100644 examples/supply_chain/env_wrapper.py create mode 100644 examples/supply_chain/evaluation_with_render.py create mode 100644 examples/supply_chain/main.py create mode 100644 examples/supply_chain/meta.py create mode 100644 examples/supply_chain/or_policies.py create mode 100644 examples/supply_chain/or_policy/eoq_policy.py create mode 100644 examples/supply_chain/render_tools.py create mode 100644 examples/supply_chain/sc_state_in_maro.md create mode 100644 examples/supply_chain/topologies/random/config.yml create mode 100644 examples/supply_chain/topologies/sample1/config.yml create mode 100644 examples/supply_chain/topologies/test/config.yml create mode 100644 maro/simulator/scenarios/supply_chain/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/actions.py create mode 100644 maro/simulator/scenarios/supply_chain/business_engine.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/base.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/consumer.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/distribution.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/extend.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/facility.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/manufacture.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/product.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/seller.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/storage.py create mode 100644 maro/simulator/scenarios/supply_chain/datamodels/vehicle.py create mode 100644 maro/simulator/scenarios/supply_chain/easy_config.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/facility.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/outerretailer.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/retailer.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/supplier.py create mode 100644 maro/simulator/scenarios/supply_chain/facilities/warehouse.py create mode 100644 maro/simulator/scenarios/supply_chain/frame_builder.py create mode 100644 maro/simulator/scenarios/supply_chain/parser.py create mode 100644 maro/simulator/scenarios/supply_chain/topologies/core.yml create mode 100644 maro/simulator/scenarios/supply_chain/topologies/sample/config.yml create mode 100644 maro/simulator/scenarios/supply_chain/units/__init__.py create mode 100644 maro/simulator/scenarios/supply_chain/units/consumer.py create mode 100644 maro/simulator/scenarios/supply_chain/units/distribution.py create mode 100644 maro/simulator/scenarios/supply_chain/units/extendunitbase.py create mode 100644 maro/simulator/scenarios/supply_chain/units/manufacture.py create mode 100644 maro/simulator/scenarios/supply_chain/units/order.py create mode 100644 maro/simulator/scenarios/supply_chain/units/outerseller.py create mode 100644 maro/simulator/scenarios/supply_chain/units/product.py create mode 100644 maro/simulator/scenarios/supply_chain/units/seller.py create mode 100644 maro/simulator/scenarios/supply_chain/units/simplemanufacture.py create mode 100644 maro/simulator/scenarios/supply_chain/units/storage.py create mode 100644 maro/simulator/scenarios/supply_chain/units/storeproduct.py create mode 100644 maro/simulator/scenarios/supply_chain/units/unitbase.py create mode 100644 maro/simulator/scenarios/supply_chain/units/vehicle.py create mode 100644 maro/simulator/scenarios/supply_chain/world.py diff --git a/examples/cim/agent_wrapper.py b/examples/cim/agent_wrapper.py index f28ec9e88..97ef87846 100644 --- a/examples/cim/agent_wrapper.py +++ b/examples/cim/agent_wrapper.py @@ -10,7 +10,7 @@ sys.path.insert(0, cim_path) from dqn import get_dqn_policy_for_rollout from env_wrapper import env_config -from meta import CIM_POLICY_NAMES +from meta import CIM_AGENT_IDS, CIM_POLICY_NAMES exploration_config = { @@ -31,5 +31,5 @@ def get_agent_wrapper(): {name: get_dqn_policy_for_rollout() for name in CIM_POLICY_NAMES}, {name: name for name in CIM_POLICY_NAMES}, exploration_dict={f"EpsilonGreedy": epsilon_greedy}, - agent2exploration={name: "EpsilonGreedy" for name in CIM_POLICY_NAMES} + agent2exploration={name: "EpsilonGreedy" for name in CIM_AGENT_IDS} ) diff --git a/examples/supply_chain/README.md b/examples/supply_chain/README.md new file mode 100644 index 000000000..6cc67f2d1 --- /dev/null +++ b/examples/supply_chain/README.md @@ -0,0 +1,8 @@ +# Supply Chain Scenario + +This README contains instructions for running the supply chain scenario using the scripts provided under ```examples/supply_chain/scripts```. For details on state, action and reward shaping based on MARO's supply chain business engine, refer to ```examples/supply_chain/sc_state_in_maro.md```. + +The instructions require that you have Docker and Docker Compose installed and set up on your machine. For installing Docker, refer to https://docs.docker.com/get-docker/. For installing Docker Compose, refer to https://docs.docker.com/compose/install/. To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: +1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. +2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the "distributed" section of ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). +3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file diff --git a/examples/supply_chain/__init__.py b/examples/supply_chain/__init__.py new file mode 100644 index 000000000..b14b47650 --- /dev/null +++ b/examples/supply_chain/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. diff --git a/examples/supply_chain/agent_wrapper.py b/examples/supply_chain/agent_wrapper.py new file mode 100644 index 000000000..dcdf065d4 --- /dev/null +++ b/examples/supply_chain/agent_wrapper.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +from maro.rl import AgentWrapper, EpsilonGreedyExploration, LinearExplorationScheduler, NullPolicy + +sc_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, sc_path) +from dqn import get_dqn_policy_for_rollout +from or_policies import ( + get_consumer_baseline_policy, get_consumer_eoq_policy, get_consumer_minmax_policy, get_producer_baseline_policy +) + + +def get_policy_mapping(config): + # policy_ids = ["consumerstore", "consumer", "producer", "facility", "product", "productstore"] + policies = [ + get_consumer_min_policy("consumer", config["policy"]["consumer"]), + get_base_producer_policy("producer", config["policy"]["producer"]), + get_dqn_policy("consumerstore", config["policy"]["consumerstore"]), + NullPolicy(name="facility"), + NullPolicy(name="product"), + NullPolicy(name="productstore") + ] + + agent2policy = {agent_id: agent_id.split(".")[0] for agent_id in config["agent_id_list"]} + return policies, agent2policy + +def get_replay_agent_ids(agent_id_list) -> List[str]: + replay_agent_ids = [agent_id for agent_id in agent_id_list if agent_id.startswith("consumerstore")] + return replay_agent_ids + + +def get_exploration_mapping(config) -> (dict, dict): + exploration = EpsilonGreedyExploration( + num_actions=config["policy"]["consumer"]["model"]["network"]["output_dim"] + ) + exploration.register_schedule( + scheduler_cls=LinearExplorationScheduler, + param_name="epsilon", + last_ep=config["exploration"]["last_ep"], + initial_value=config["exploration"]["initial_value"], + final_value=config["exploration"]["final_value"] + ) + + exploration_dict = {"consumerstore": exploration} + agent2exploration = { + agent_id: "consumerstore" + for agent_id in config["agent_id_list"] if agent_id.startswith("consumerstore") + } + + return exploration_dict, agent2exploration + + +exploration_config = { + "last_ep": 10, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(5, 0.32)] +} + +def get_agent_wrapper(): + epsilon_greedy = EpsilonGreedyExploration(num_actions=env_config["wrapper"]["num_actions"]) + epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **exploration_config + ) + return AgentWrapper( + {name: get_dqn_policy_for_rollout() for name in CIM_POLICY_NAMES}, + {name: name for name in CIM_POLICY_NAMES}, + exploration_dict={f"EpsilonGreedy": epsilon_greedy}, + agent2exploration={name: "EpsilonGreedy" for name in CIM_POLICY_NAMES} + ) diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml new file mode 100644 index 000000000..bc9685049 --- /dev/null +++ b/examples/supply_chain/config.yml @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +experiment_name: test + +env: + scenario: supply_chain + # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder + # that contains a single config.yml and should be placed under examples/supply_chain/envs/ + topology: test + durations: 64 # number of ticks per episode + +num_episodes: 1000 # number of episodes to simulate + +# Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps +# before returning experiences to the learner. The learner uses these experiences to update the agents' policies +# and sync the updated policies to the actors for the next learning cycle. +experience_update_interval: 64 + +eval_schedule: 100 + +log_env_summary: true + +policy: + consumerstore: + algorithm: dqn + model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. + device: cpu + network: + hidden_dims: + - 256 + - 128 + - 32 + output_dim: 10 + activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. + softmax: true + batch_norm: false + skip_connection: false + head: true + dropout_p: 0.0 + optimization: + optim_cls: adam # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. + optim_params: + lr: 0.0005 + algorithm_config: + reward_discount: .99 + train_epochs: 10 + gradient_iters: 1 + loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. + target_update_freq: 4 # How many training iteration, to update DQN target model + soft_update_coefficient: 0.01 + double: true # whether to enable double DQN + experience_manager: + capacity: 128000 # experience memory size + # This determines how existing experiences are replaced when adding new experiences to a full experience + # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, + # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. + overwrite_type: rolling + batch_size: 2560 + replace: true + update_schedule: + type: step # "step" or "episode" + args: + start_ep: 3 # must be a positive number since episode is 1-based. + interval: 1 + end_ep_update: true + consumer: + model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. + network: + output_dim: 10 + producer: + model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. + network: + output_dim: 10 + +exploration: + last_ep: 800 + initial_value: 0.8 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. + final_value: 0.0 + +distributed: + # this is used to group all actor / learner processes belonging to the same job for communication purposes. + # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. + group: sc-dqn + num_actors: 3 # number of parallel roll-out actors + # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" + # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make + # sure that a local redis server is running and listening on the port specified by "redis_port". + redis_host: maro-redis + redis_port: 6379 + # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent + # slow actors from dragging down the whole process. + required_actor_finishes: 3 + # If true, experiences from older segments (usually coming from slow actors) will not be used for learning. + discard_stale_experiences: True diff --git a/examples/supply_chain/dqn.py b/examples/supply_chain/dqn.py new file mode 100644 index 000000000..31f82f054 --- /dev/null +++ b/examples/supply_chain/dqn.py @@ -0,0 +1,85 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import numpy as np +import torch + +from maro.rl import DQN, DQNConfig, FullyConnectedBlock, OptimOption, DiscreteQNet, ExperienceManager + +config = { + "model": { # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. + "device": "cpu", + "network": { + "hidden_dims": [256, 128, 32], + "output_dim": 10, + "activation": "leaky_relu", # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. + "softmax": True, + "batch_norm": False, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 + }, + "optimization": { + "optim_cls": "adam", # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. + "optim_params": {"lr": 0.0005} + } + }, + "algorithm": { + "reward_discount": .99, + "train_epochs": 10, + "target_update_freq": 4, # How many training iteration, to update DQN target model + "soft_update_coefficient": 0.01, + "double": True # whether to enable double DQN + }, + "experience_manager": { + "rollout": { # for experience managers in actor processes + "capacity": 1000, + # This determines how existing experiences are replaced when adding new experiences to a full experience + # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, + # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. + "overwrite_type": "rolling", + "batch_size": 128, + "replace": False + }, + "training": { # for experience managers in the learner process + "capacity": 128000, + "overwrite_type": "rolling", + "batch_size": 2560, + "replace": True + } + }, + "update_trigger": 16, + "warmup": 1 +} + + +class QNet(DiscreteQNet): + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component(states) + + +def get_dqn_policy_for_rollout(): + qnet = QNet(FullyConnectedBlock(**config["model"]["network"])) + return DQN( + qnet, + ExperienceManager(**config["experience_manager"]["rollout"]), + DQNConfig(**config["algorithm"]), + update_trigger=1e8 # set to a large number to ensure that the roll-out workers don't update policies + ) + + +def get_dqn_policy_for_training(): + qnet = QNet( + FullyConnectedBlock(**config["model"]["network"]), + optim_option=OptimOption(**config["model"]["optimization"]) + ) + return DQN( + qnet, + ExperienceManager(**config["experience_manager"]["training"]), + DQNConfig(**config["algorithm"]), + update_trigger=config["update_trigger"], + warmup=config["warmup"] + ) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py new file mode 100644 index 000000000..f32277c3e --- /dev/null +++ b/examples/supply_chain/env_wrapper.py @@ -0,0 +1,1187 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict, namedtuple +from typing import List + +import scipy.stats as st +import numpy as np + +from maro.rl import AbsEnvWrapper +from maro.simulator import Env +from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction + +# from exploration import exploration_dict, agent2exploration +# from learner import SCLearner + +def stock_constraint(f_state): + return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean'] + + +def is_replenish_constraint(f_state): + return f_state['consumption_hist'][-1] > 0 + + +def low_profit(f_state): + return (f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000 + + +def low_stock_constraint(f_state): + return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean'] + + +def out_of_stock(f_state): + return 0 < f_state['inventory_in_stock'] + + +atoms = { + 'stock_constraint': stock_constraint, + 'is_replenish_constraint': is_replenish_constraint, + 'low_profit': low_profit, + 'low_stock_constraint': low_stock_constraint, + 'out_of_stock': out_of_stock +} + +# State extracted. +keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'consumption_hist']), + ('storage_capacity', ['storage_utilization']), + ('sale_mean', ['sale_std', + 'sale_hist', + 'pending_order', + 'inventory_in_stock', + 'inventory_in_transit', + 'inventory_estimated', + 'inventory_rop']), + ('max_price', ['sku_price', 'sku_cost'])] + +# Sku related agent types +sku_agent_types = {"consumer", "consumerstore", "producer", "product", "productstore"} + + +class UnitBaseInfo: + id: int = None + node_index: int = None + config: dict = None + summary: dict = None + + def __init__(self, unit_summary): + self.id = unit_summary["id"] + self.node_index = unit_summary["node_index"] + self.config = unit_summary.get("config", {}) + self.summary = unit_summary + + def __getitem__(self, key, default=None): + if key in self.summary: + return self.summary[key] + + return default + + +distribution_features = ("remaining_order_quantity", "remaining_order_number") +seller_features = ("total_demand", "sold", "demand") + + +class SCEnvWrapper(AbsEnvWrapper): + def __init__(self, env: Env, reward_eval_delay: int=0, save_replay: bool=True, replay_agent_ids: list=None): + super().__init__(env, reward_eval_delay, save_replay, replay_agent_ids) + self.balance_cal = BalanceSheetCalculator(env) + self.cur_balance_sheet_reward = None + self.storage_ss = env.snapshot_list["storage"] + self.distribution_ss = env.snapshot_list["distribution"] + self.consumer_ss = env.snapshot_list["consumer"] + self.seller_ss = env.snapshot_list["seller"] + + self._summary = env.summary['node_mapping'] + self._configs = env.configs + self._agent_types = self._summary["agent_types"] + self._units_mapping = self._summary["unit_mapping"] + self._agent_list = env.agent_idx_list + + self._sku_number = len(self._summary["skus"]) + 1 + self._max_price = self._summary["max_price"] + self._max_sources_per_facility = self._summary["max_sources_per_facility"] + + # state for each tick + self._cur_metrics = env.metrics + + # cache for ppf value. + self._service_index_ppf_cache = {} + + # facility -> { + # data_model_index:int, + # storage:UnitBaseInfo, + # distribution: UnitBaseInfo, + # sku_id: { + # skuproduct: UnitBaseInfo, + # consumer: UnitBaseInfo, + # seller: UnitBaseInfo, + # manufacture: UnitBaseInfo + # } + # } + self.facility_levels = {} + + # unit id -> (facility id) + self.unit_2_facility_dict = {} + + # our raw state + self._states = {} + + # facility id -> storage index + self._facility2storage_index_dict = {} + + # facility id -> product id -> number + self._storage_product_numbers = {} + + # facility id -> product_id -> index + self._storage_product_indices = {} + + # facility id -> storage product utilization + self._facility_product_utilization = {} + + # facility id -> in_transit_orders + self._facility_in_transit_orders = {} + + # current distribution states + self._cur_distribution_states = None + + # current consumer states + self._cur_consumer_states = None + + # current seller states + self._cur_seller_states = None + + # dim for state + self._dim = None + + # use this to quick find relationship between units (consumer, manufacture, seller or product) and product unit. + # unit id -> (product unit id, facility id, seller id, consumer id, manufacture id) + self._unit2product_mapping = {} + + # agent (unit id) -> AgentInfo + self._agent_id2info_mapping = {} + + # built internal helpers. + self._build_internal_helpers() + + self.stock_status = {} + self.demand_status = {} + # key: product unit id, value: number + self.orders_from_downstreams = {} + self.consumer_orders = {} + self.order_in_transit_status = {} + self.order_to_distribute_status = {} + + @property + def dim(self): + """Calculate dim per shape.""" + if self._dim is None: + self._dim = 0 + + first_state = next(iter(self._states.values())) + + for _, state_keys in keys_in_state: + for key in state_keys: + val = first_state[key] + + if type(val) == list: + self._dim += len(val) + else: + self._dim += 1 + + return self._dim + + def get_or_policy_state(self, state, agent_info): + state = {'is_facility': not (agent_info.agent_type in sku_agent_types)} + if agent_info.is_facility: + return state + + product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id + id, product_id, _, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ + self.balance_cal.products[self.balance_cal.product_id2index_dict[product_unit_id]] + + product_metrics = self._cur_metrics["products"][product_unit_id] + state['sale_mean'] = product_metrics["sale_mean"] + state['sale_std'] = product_metrics["sale_std"] + + facility = self.facility_levels[agent_info.facility_id] + state['unit_storage_cost'] = unit_storage_cost + state['order_cost'] = 1 + product_info = facility[agent_info.sku.id] + if "consumer" in product_info: + consumer_index = product_info["consumer"].node_index + state['order_cost'] = self.consumer_ss[self.env.tick:consumer_index:"order_cost"].flatten()[0] + state['storage_capacity'] = facility['storage'].config["capacity"] + state['storage_levels'] = self._storage_product_numbers[agent_info.facility_id] + state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] + state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 + state['vlt'] = agent_info.sku.vlt + state['service_level'] = agent_info.sku.service_level + return state + + def get_rl_policy_state(self, state, agent_info): + self._update_facility_features(state, agent_info) + self._update_storage_features(state, agent_info) + # bom do not need to update + # self._add_bom_features(state, agent_info) + self._update_distribution_features(state, agent_info) + self._update_sale_features(state, agent_info) + # vlt do not need to update + # self._update_vlt_features(state, agent_info) + self._update_consumer_features(state, agent_info) + # self._add_price_features(state, agent_info) + self._update_global_features(state) + + self.stock_status[agent_info.id] = state['inventory_in_stock'] + + self.demand_status[agent_info.id] = state['sale_hist'][-1] + + self.order_in_transit_status[agent_info.id] = state['inventory_in_transit'] + + self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] + + self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} + self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} + + np_state = self._serialize_state(state) + return np_state + + def get_state(self, event): + cur_tick = self.env.tick + settings: dict = self.env.configs.settings + consumption_hist_len = settings['consumption_hist_len'] + hist_len = settings['sale_hist_len'] + consumption_ticks = [cur_tick - i for i in range(consumption_hist_len-1, -1, -1)] + hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] + + self.cur_balance_sheet_reward = self.balance_cal.calc() + self._cur_metrics = self.env.metrics + + self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features] \ + .flatten() \ + .reshape(-1, len(distribution_features)) \ + .astype(np.int) + + self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"] \ + .flatten() \ + .reshape(-1, len(self.consumer_ss)) + + self._cur_seller_states = self.seller_ss[hist_ticks::seller_features] \ + .astype(np.int) + + + # facility level states + for facility_id in self._facility_product_utilization: + # reset for each step + self._facility_product_utilization[facility_id] = 0 + + in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] + + self._facility_in_transit_orders[facility_id] = [0] * self._sku_number + + for sku_id, number in in_transit_orders.items(): + self._facility_in_transit_orders[facility_id][sku_id] = number + + final_state = {} + + # calculate storage info first, then use it later to speed up. + for facility_id, storage_index in self._facility2storage_index_dict.items(): + product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"] \ + .flatten() \ + .astype(np.int) + + for pid, index in self._storage_product_indices[facility_id].items(): + product_number = product_numbers[index] + + self._storage_product_numbers[facility_id][pid] = product_number + self._facility_product_utilization[facility_id] += product_number + + for agent_info in self._agent_list: + state = self._states[agent_info.id] + + # storage_index = self._facility2storage_index_dict[agent_info.facility_id] + + np_state = self.get_rl_policy_state(state, agent_info) + if agent_info.agent_type in ["consumer", "producer"]: + np_state = self.get_or_policy_state(state, agent_info) + + # agent_info.agent_type -> policy + final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state + + return final_state + + def get_reward(self, tick=None, target_agents=None): + # get related product, seller, consumer, manufacture unit id + # NOTE: this mapping does not contain facility id, so if id is not exist, then means it is a facility + # product_unit_id, facility_id, seller_id, consumer_id, producer_id = self._unit2product_mapping[id] + # return { + # f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}": np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) + # for f_id, bwt in self.cur_balance_sheet_reward.items() + # } + self.cur_balance_sheet_reward = self.balance_cal.calc() + rewards = defaultdict(float) + for f_id, bwt in self.cur_balance_sheet_reward.items(): + agent = self._agent_id2info_mapping[f_id] + if agent.agent_type == 'consumerstore': + rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) + else: + rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = 0 + + return rewards + + def to_env_action(self, action_by_agent): + # cache the sources for each consumer if not yet cached + if not hasattr(self, "consumer2source"): + self.consumer2source, self.consumer2product = {}, {} + for facility in self.env.summary["node_mapping"]["facilities"].values(): + products = facility["units"]["products"] + for product_id, product in products.items(): + consumer = product["consumer"] + if consumer is not None: + consumer_id = consumer["id"] + self.consumer2source[consumer_id] = consumer["sources"] + self.consumer2product[consumer_id] = product_id + + env_action = {} + for agent_id, action in action_by_agent.items(): + unit_id = int(agent_id.split(".")[1]) + + is_facility = unit_id not in self._units_mapping + + # ignore facility to reduce action number + if is_facility: + continue + + # consumer action + if agent_id.startswith("consumer"): + product_id = self.consumer2product.get(unit_id, 0) + sources = self.consumer2source.get(unit_id, []) + if sources: + source_id = sources[0] + product_unit_id = self._unit2product_mapping[unit_id][0] + action_number = int(int(action) * self._cur_metrics["products"][product_unit_id]["sale_mean"]) + + # ignore 0 quantity to reduce action number + if action_number == 0: + continue + + sku = self._units_mapping[unit_id][3] + + reward_discount = 1 + + env_action[unit_id] = ConsumerAction( + unit_id, + product_id, + source_id, + action_number, + sku.vlt, + reward_discount + ) + + self.consumer_orders[product_unit_id] = action_number + self.orders_from_downstreams[self.facility_levels[source_id][product_id]["skuproduct"].id] = action_number + + # manufacturer action + elif agent_id.startswith("producer"): + sku = self._units_mapping[unit_id][3] + action = sku.production_rate + + # ignore invalid actions + if action is None or action == 0: + continue + + env_action[unit_id] = ManufactureAction(unit_id, action) + + return env_action + + def _update_facility_features(self, state, agent_info): + state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 + + def _update_storage_features(self, state, agent_info): + facility_id = agent_info.facility_id + state['storage_utilization'] = 0 + + state['storage_levels'] = self._storage_product_numbers[facility_id] + state['storage_utilization'] = self._facility_product_utilization[facility_id] + + def _update_sale_features(self, state, agent_info): + if agent_info.agent_type not in sku_agent_types: + return + + # Get product unit id for current agent. + product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id + + product_metrics = self._cur_metrics["products"][product_unit_id] + + state['sale_mean'] = product_metrics["sale_mean"] + state['sale_std'] = product_metrics["sale_std"] + + facility = self.facility_levels[agent_info.facility_id] + product_info = facility[agent_info.sku.id] + + if "seller" not in product_info: + # TODO: why gamma sale as mean? + state['sale_gamma'] = state['sale_mean'] + + if "consumer" in product_info: + consumer_index = product_info["consumer"].node_index + + state['consumption_hist'] = list( + self._cur_consumer_states[:, consumer_index]) + state['pending_order'] = list( + product_metrics["pending_order_daily"]) + + if "seller" in product_info: + seller_index = product_info["seller"].node_index + + seller_states = self._cur_seller_states[:, seller_index, :] + + # For total demand, we need latest one. + state['total_backlog_demand'] = seller_states[:, 0][-1][0] + state['sale_hist'] = list(seller_states[:, 1].flatten()) + state['backlog_demand_hist'] = list(seller_states[:, 2]) + + def _update_distribution_features(self, state, agent_info): + facility = self.facility_levels[agent_info.facility_id] + distribution = facility.get("distribution", None) + + if distribution is not None: + dist_states = self._cur_distribution_states[distribution.node_index] + state['distributor_in_transit_orders'] = dist_states[1] + state['distributor_in_transit_orders_qty'] = dist_states[0] + + def _update_consumer_features(self, state, agent_info): + if agent_info.is_facility: + return + + facility = self.facility_levels[agent_info.facility_id] + product_info = facility[agent_info.sku.id] + + # if "consumer" not in product_info: + # return + + state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] + + # FIX: we need plus 1 to this, as it is 0 based index, but we already aligned with 1 more + # slot to use sku id as index ( 1 based). + product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 + state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] + state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] + + pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] + + if pending_order is not None: + state['inventory_in_distribution'] = pending_order[agent_info.sku.id] + + state['inventory_estimated'] = (state['inventory_in_stock'] + + state['inventory_in_transit'] + - state['inventory_in_distribution']) + if state['inventory_estimated'] >= 0.5 * state['storage_capacity']: + state['is_over_stock'] = 1 + + if state['inventory_estimated'] <= 0: + state['is_out_of_stock'] = 1 + + service_index = state['service_level'] + + if service_index not in self._service_index_ppf_cache: + self._service_index_ppf_cache[service_index] = st.norm.ppf( + service_index) + + ppf = self._service_index_ppf_cache[service_index] + + state['inventory_rop'] = (state['max_vlt'] * state['sale_mean'] + + np.sqrt(state['max_vlt']) * state['sale_std'] * ppf) + + if state['inventory_estimated'] < state['inventory_rop']: + state['is_below_rop'] = 1 + + def _update_global_features(self, state): + state["global_time"] = self.env.tick + + def _serialize_state(self, state): + result = [] + + for norm, fields in keys_in_state: + for field in fields: + vals = state[field] + if not isinstance(vals, list): + vals = [vals] + if norm is not None: + vals = [max(0.0, min(20.0, x / (state[norm] + 0.01))) + for x in vals] + result.extend(vals) + + return np.asarray(result, dtype=np.float32) + + def _build_internal_helpers(self): + for agent_info in self.env.agent_idx_list: + self._agent_id2info_mapping[agent_info.id] = agent_info + + # facility levels + for facility_id, facility in self._summary["facilities"].items(): + self.facility_levels[facility_id] = { + "node_index": facility["node_index"], + "config": facility['configs'], + "upstreams": facility["upstreams"], + "skus": facility["skus"] + } + + units = facility["units"] + + storage = units["storage"] + if storage is not None: + self.facility_levels[facility_id]["storage"] = UnitBaseInfo( + storage) + + self.unit_2_facility_dict[storage["id"]] = facility_id + + self._facility2storage_index_dict[facility_id] = storage["node_index"] + + self._storage_product_numbers[facility_id] = [0] * self._sku_number + self._storage_product_indices[facility_id] = {} + self._facility_product_utilization[facility_id] = 0 + + for i, pid in enumerate(storage["product_list"]): + self._storage_product_indices[facility_id][pid] = i + self._storage_product_numbers[facility_id][pid] = 0 + + distribution = units["distribution"] + + if distribution is not None: + self.facility_levels[facility_id]["distribution"] = UnitBaseInfo( + distribution) + self.unit_2_facility_dict[distribution["id"]] = facility_id + + products = units["products"] + + if products: + for product_id, product in products.items(): + product_info = { + "skuproduct": UnitBaseInfo(product) + } + + self.unit_2_facility_dict[product["id"]] = facility_id + + seller = product['seller'] + + if seller is not None: + product_info["seller"] = UnitBaseInfo(seller) + self.unit_2_facility_dict[seller["id"]] = facility_id + + consumer = product["consumer"] + + if consumer is not None: + product_info["consumer"] = UnitBaseInfo(consumer) + self.unit_2_facility_dict[consumer["id"]] = facility_id + + manufacture = product["manufacture"] + + if manufacture is not None: + product_info["manufacture"] = UnitBaseInfo(manufacture) + self.unit_2_facility_dict[manufacture["id"] + ] = facility_id + + self.facility_levels[facility_id][product_id] = product_info + + for unit in (seller, consumer, manufacture, product): + if unit is not None: + self._unit2product_mapping[unit["id"]] = ( + product["id"], + facility_id, + seller["id"] if seller is not None else None, + consumer["id"] if consumer is not None else None, + manufacture["id"] if manufacture is not None else None + ) + + # create initial state structure + self._build_init_state() + + def _build_init_state(self): + # we will build the final state with default and const values, + # then update dynamic part per step + for agent_info in self._agent_list: + state = {} + + facility = self.facility_levels[agent_info.facility_id] + + # global features + state["global_time"] = 0 + + # facility features + state["facility"] = None + state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] + state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] + state['constraint_idx'] = [0] + state['facility_id'] = [0] * self._sku_number + state['sku_info'] = {} if agent_info.is_facility else agent_info.sku + state['echelon_level'] = 0 + + state['facility_info'] = facility['config'] + state["is_positive_balance"] = 0 + + if not agent_info.is_facility: + state['facility_id'][agent_info.sku.id] = 1 + + for atom_name in atoms.keys(): + state[atom_name] = list( + np.ones(self._configs.settings['constraint_state_hist_len'])) + + # storage features + state['storage_levels'] = [0] * self._sku_number + state['storage_capacity'] = facility['storage'].config["capacity"] + state['storage_utilization'] = 0 + + # bom features + state['bom_inputs'] = [0] * self._sku_number + state['bom_outputs'] = [0] * self._sku_number + + if not agent_info.is_facility: + state['bom_inputs'][agent_info.sku.id] = 1 + state['bom_outputs'][agent_info.sku.id] = 1 + + # vlt features + sku_list = self._summary["skus"] + current_source_list = [] + + if agent_info.sku is not None: + current_source_list = facility["upstreams"].get( + agent_info.sku.id, []) + + state['vlt'] = [0] * \ + (self._max_sources_per_facility * self._sku_number) + state['max_vlt'] = 0 + + if not agent_info.is_facility: + # only for sku product + product_info = facility[agent_info.sku.id] + + if "consumer" in product_info and len(current_source_list) > 0: + state['max_vlt'] = product_info["skuproduct"]["max_vlt"] + + for i, source in enumerate(current_source_list): + for j, sku in enumerate(sku_list.values()): + # NOTE: different with original code, our config can make sure that source has product we need + + if sku.id == agent_info.sku.id: + state['vlt'][i * len(sku_list) + j + + 1] = facility["skus"][sku.id].vlt + + # sale features + settings = self.env.configs.settings + hist_len = settings['sale_hist_len'] + consumption_hist_len = settings['consumption_hist_len'] + + state['sale_mean'] = 1.0 + state['sale_std'] = 1.0 + state['sale_gamma'] = 1.0 + state['service_level'] = 0.95 + state['total_backlog_demand'] = 0 + + state['sale_hist'] = [0] * hist_len + state['backlog_demand_hist'] = [0] * hist_len + state['consumption_hist'] = [0] * consumption_hist_len + state['pending_order'] = [0] * settings['pending_order_len'] + + if not agent_info.is_facility: + state['service_level'] = agent_info.sku.service_level + + product_info = facility[agent_info.sku.id] + + if "seller" in product_info: + state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma + + # distribution features + state['distributor_in_transit_orders'] = 0 + state['distributor_in_transit_orders_qty'] = 0 + + # consumer features + state['consumer_source_export_mask'] = [0] * \ + (self._max_sources_per_facility * self._sku_number) + state['consumer_source_inventory'] = [0] * self._sku_number + state['consumer_in_transit_orders'] = [0] * self._sku_number + + state['inventory_in_stock'] = 0 + state['inventory_in_transit'] = 0 + state['inventory_in_distribution'] = 0 + state['inventory_estimated'] = 0 + state['inventory_rop'] = 0 + state['is_over_stock'] = 0 + state['is_out_of_stock'] = 0 + state['is_below_rop'] = 0 + + if len(current_source_list) > 0: + for i, source in enumerate(current_source_list): + for j, sku in enumerate(sku_list.values()): + if sku.id == agent_info.sku.id: + state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = \ + self.facility_levels[source]["skus"][sku.id].vlt + + # price features + state['max_price'] = self._max_price + state['sku_price'] = 0 + state['sku_cost'] = 0 + + if not agent_info.is_facility: + state['sku_price'] = agent_info.sku.price + state['sku_cost'] = agent_info.sku.cost + + self._states[agent_info.id] = state + + +ProductInfo = namedtuple( + "ProductInfo", + ( + "unit_id", + "sku_id", + "node_index", + "storage_index", + "unit_storage_cost", + "distribution_index", + "downstream_product_units", + "consumer_id_index_tuple", + "seller_id_index_tuple", + "manufacture_id_index_tuple" + ) +) + +FacilityLevelInfo = namedtuple( + "FacilityLevelInfo", + ( + "unit_id", + "product_unit_id_list", + "storage_index", + "unit_storage_cost", + "distribution_index", + "vehicle_index_list" + ) +) + + +class BalanceSheetCalculator: + def __init__(self, env: Env): + self.env = env + self.products: List[ProductInfo] = [] + self.product_id2index_dict = {} + self.facility_levels: List[FacilityLevelInfo] = [] + self.consumer_id2product = {} + + self.facilities = env.summary["node_mapping"]["facilities"] + + for facility_id, facility in self.facilities.items(): + pid_list = [] + distribution = facility["units"]["distribution"] + + for product_id, product in facility["units"]["products"].items(): + pid_list.append(product["id"]) + consumer = product["consumer"] + if consumer is not None: + self.consumer_id2product[consumer["id"]] = product["id"] + seller = product["seller"] + manufacture = product["manufacture"] + + self.product_id2index_dict[product["id"]] = len(self.products) + + downstream_product_units = [] + downstreams = facility["downstreams"] + + if downstreams and len(downstreams) > 0 and product_id in downstreams: + for dfacility in downstreams[product_id]: + dproducts = self.facilities[dfacility]["units"]["products"] + + downstream_product_units.append(dproducts[product_id]["id"]) + + self.products.append( + ProductInfo( + unit_id=product["id"], + sku_id=product_id, + node_index=product["node_index"], + storage_index=facility["units"]["storage"]["node_index"], + unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution_index=distribution["node_index"] if distribution is not None else None, + downstream_product_units=downstream_product_units, + consumer_id_index_tuple=None if consumer is None else (consumer["id"], consumer["node_index"]), + seller_id_index_tuple=None if seller is None else (seller["id"], seller["node_index"]), + manufacture_id_index_tuple=None if manufacture is None else (manufacture["id"], manufacture["node_index"]) + ) + ) + + self.facility_levels.append( + FacilityLevelInfo( + unit_id=facility_id, + product_unit_id_list=pid_list, + storage_index=facility["units"]["storage"]["node_index"], + unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution_index=distribution["node_index"] if distribution is not None else None, + vehicle_index_list=[ + v["node_index"] for v in distribution["children"] + ] if distribution is not None else [] + ) + ) + + # TODO: order products make sure calculate reward from downstream to upstream + tmp_product_unit_dict = {} + + for product in self.products: + tmp_product_unit_dict[product.unit_id] = product + + self._ordered_products = [] + + tmp_stack = [] + + for product in self.products: + # skip if already being processed + if tmp_product_unit_dict[product.unit_id] is None: + continue + + for dproduct in product.downstream_product_units: + # push downstream id to stack + tmp_stack.append(dproduct) + + # insert current product to list head + self._ordered_products.insert(0, product) + # mark it as processed + tmp_product_unit_dict[product.unit_id] = None + + while len(tmp_stack) > 0: + # process downstream of product unit in stack + dproduct_unit_id = tmp_stack.pop() + + # if it was processed then ignore + if tmp_product_unit_dict[dproduct_unit_id] is None: + continue + + # or extract it downstreams + dproduct_unit = tmp_product_unit_dict[dproduct_unit_id] + + dproduct_downstreams = dproduct_unit.downstream_product_units + + for dproduct in dproduct_downstreams: + tmp_stack.append(dproduct) + + # current unit in final list + self._ordered_products.insert(0, dproduct_unit) + tmp_product_unit_dict[dproduct_unit_id] = None + + self.total_balance_sheet = defaultdict(int) + + # tick -> (product unit id, sku id, manufacture number, manufacture cost, checkin order, delay penaty) + self._supplier_reward_factors = {} + + def _check_attribute_keys(self, target_type: str, attribute: str): + valid_target_types = list(self.env.summary["node_detail"].keys()) + assert target_type in valid_target_types, f"Target_type {target_type} not in {valid_target_types}!" + + valid_attributes = list(self.env.summary["node_detail"][target_type]["attributes"].keys()) + assert attribute in valid_attributes, ( + f"Attribute {attribute} not valid for {target_type}. " + f"Valid attributes: {valid_attributes}" + ) + return + + def _get_attributes(self, target_type: str, attribute: str, tick: int=None) -> np.ndarray: + self._check_attribute_keys(target_type, attribute) + + if tick == None: + tick = self.env.tick + + return self.env.snapshot_list[target_type][tick::attribute].flatten() + + def _get_list_attributes(self, target_type: str, attribute: str, tick: int=None) -> List[np.ndarray]: + self._check_attribute_keys(target_type, attribute) + + if tick == None: + tick = self.env.tick + + indexes = list(range(len(self.env.snapshot_list[target_type]))) + return [self.env.snapshot_list[target_type][tick:index:attribute].flatten() for index in indexes] + + def _calc_consumer(self): + #### Consumer + consumer_ids = self._get_attributes("consumer", "id").astype(np.int) + + # quantity * price + order_profit = ( + self._get_attributes("consumer", "order_quantity") + * self._get_attributes("consumer", "price") + ) + + # order_cost + order_product_cost + consumer_step_balance_sheet_loss = -1 * ( + self._get_attributes("consumer", "order_cost") + + self._get_attributes("consumer", "order_product_cost") + ) + + # consumer step reward: balance sheet los + profile * discount + # consumer_step_reward = ( + # consumer_step_balance_sheet_loss + # + order_profit * self._get_attributes("consumer", "reward_discount") + # ) + consumer_step_reward = consumer_step_balance_sheet_loss + + consumer_step_balance_sheet = order_profit + consumer_step_balance_sheet_loss + + return consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet + + def _calc_seller(self): + #### Seller + # profit = sold * price + seller_balance_sheet_profit = ( + self._get_attributes("seller", "sold") + * self._get_attributes("seller", "price") + ) + + # loss = demand * price * backlog_ratio + seller_balance_sheet_loss = -1 * ( + self._get_attributes("seller", "demand") + * self._get_attributes("seller", "price") + * self._get_attributes("seller", "backlog_ratio") + ) + + # step reward = loss + profit + seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit + + return seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward + + def _calc_manufacture(self): + #### manufacture + manufacture_ids = self._get_attributes("manufacture", "id").astype(np.int) + + # loss = manufacture number * cost + manufacture_balance_sheet_loss = -1 * ( + self._get_attributes("manufacture", "manufacturing_number") + * self._get_attributes("manufacture", "product_unit_cost") + ) + + # step reward = loss + manufacture_step_reward = manufacture_balance_sheet_loss + manufacture_step_balance_sheet = manufacture_balance_sheet_loss + + return manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet + + def _calc_storage(self): + #### storage + # loss = (capacity-remaining space) * cost + storage_balance_sheet_loss = -1 * ( + self._get_attributes("storage", "capacity") + - self._get_attributes("storage", "remaining_space") + ) + + # create product number mapping for storages + product_list = self._get_list_attributes("storage", "product_list") + product_number = self._get_list_attributes("storage", "product_number") + storages_product_map = { + idx: { + id: num + for id, num in zip(id_list.astype(np.int), num_list.astype(np.int)) + } + for idx, (id_list, num_list) in enumerate(zip(product_list, product_number)) + } + + return storage_balance_sheet_loss, storages_product_map + + def _calc_vehicle(self): + ## vehicles + # loss = cost * payload + vehicle_balance_sheet_loss = -1 * ( + self._get_attributes("vehicle", "payload") + * self._get_attributes("vehicle", "unit_transport_cost") + ) + vehicle_step_reward = vehicle_balance_sheet_loss + return vehicle_balance_sheet_loss, vehicle_step_reward + + def _calc_product_distribution(self): + #### product + # product distribution profit = check order * price + product_distribution_balance_sheet_profit = ( + self._get_attributes("product", "distribution_check_order") + * self._get_attributes("product", "price") + ) + # product distribution loss = transportation cost + delay order penalty + product_distribution_balance_sheet_loss = -1 * ( + self._get_attributes("product", "distribution_transport_cost") + + self._get_attributes("product", "distribution_delay_order_penalty") + ) + return product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss + + def _calc_product( + self, + consumer_step_balance_sheet_loss, + consumer_step_reward, + seller_balance_sheet_profit, + seller_balance_sheet_loss, + seller_step_reward, + manufacture_balance_sheet_loss, + manufacture_step_reward, + storages_product_map, + product_distribution_balance_sheet_profit, + product_distribution_balance_sheet_loss, + ): + num_products = len(self.products) + product_step_reward = np.zeros(num_products) + product_balance_sheet_profit = np.zeros(num_products) + product_balance_sheet_loss = np.zeros(num_products) + + # product = consumer + seller + manufacture + storage + distribution + downstreams + for product in self._ordered_products: + i = product.node_index + + if product.consumer_id_index_tuple: + consumer_index = product.consumer_id_index_tuple[1] + product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer_index] + product_step_reward[i] += consumer_step_reward[consumer_index] + + if product.seller_id_index_tuple: + seller_index = product.seller_id_index_tuple[1] + product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller_index] + product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller_index] + product_step_reward[i] += seller_step_reward[seller_index] + + if product.manufacture_id_index_tuple: + manufacture_index = product.manufacture_id_index_tuple[1] + product_balance_sheet_loss[i] += manufacture_balance_sheet_loss[manufacture_index] + product_step_reward[i] += manufacture_step_reward[manufacture_index] + + storage_reward = -1 * storages_product_map[product.storage_index][product.sku_id] * product.unit_storage_cost + product_step_reward[i] += storage_reward + product_balance_sheet_loss[i] += storage_reward + + if product.distribution_index is not None: + product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[i] + product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[i] + product_step_reward[i] += product_distribution_balance_sheet_loss[i] + product_distribution_balance_sheet_profit[i] + + if len(product.downstream_product_units) > 0: + for did in product.downstream_product_units: + product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] + product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] + product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] + + product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss + + return product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet + + def _calc_facility( + self, + storage_balance_sheet_loss, + vehicle_balance_sheet_loss, + product_balance_sheet_profit, + product_balance_sheet_loss, + product_step_reward + ): + num_facilities = len(self.facility_levels) + facility_balance_sheet_loss = np.zeros(num_facilities) + facility_balance_sheet_profit = np.zeros(num_facilities) + facility_step_reward = np.zeros(num_facilities) + + # for facilities + for i, facility in enumerate(self.facility_levels): + # storage balance sheet + # profit=0 + facility_balance_sheet_loss[i] += storage_balance_sheet_loss[facility.storage_index] * facility.unit_storage_cost + + # distribution balance sheet + if facility.distribution_index is not None: + for vidx in facility.vehicle_index_list: + facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vidx] + # distribution unit do not provide reward + + # sku product unit balance sheet + for pid in facility.product_unit_id_list: + facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] + facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] + facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] + + facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit + + return facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet + + def calc(self): + #### Basic Units: Loss, Profit, Reward + consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet = self._calc_consumer() + seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward = self._calc_seller() + manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet = self._calc_manufacture() + storage_balance_sheet_loss, storages_product_map = self._calc_storage() + vehicle_balance_sheet_loss, vehicle_step_reward = self._calc_vehicle() + product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss = self._calc_product_distribution() + ######################################################################## + + #### Loss, profit, reward for each product + product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet = self._calc_product( + consumer_step_balance_sheet_loss, + consumer_step_reward, + seller_balance_sheet_profit, + seller_balance_sheet_loss, + seller_step_reward, + manufacture_balance_sheet_loss, + manufacture_step_reward, + storages_product_map, + product_distribution_balance_sheet_profit, + product_distribution_balance_sheet_loss + ) + ######################################################################## + + #### Loss, profit, reward for each facility + # facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet = self._calc_facility( + # storage_balance_sheet_loss, + # vehicle_balance_sheet_loss, + # product_balance_sheet_profit, + # product_balance_sheet_loss, + # product_step_reward + # ) + ######################################################################## + + # Final result for current tick, key is the facility/unit id, value is tuple of balance sheet and reward. + result = {} + + # For product units. + for id, bs, rw in zip([product.unit_id for product in self.products], product_balance_sheet, product_step_reward): + result[id] = (bs, rw) + self.total_balance_sheet[id] += bs + + # For consumers. + for id, bs, rw in zip(consumer_ids, consumer_step_balance_sheet, consumer_step_reward): + # result[id] = (bs, rw) + # let reward of a consumer equate its parent product + result[id] = result[self.consumer_id2product[id]] + self.total_balance_sheet[id] += result[id][0] + + # For producers. + for id, bs, rw in zip(manufacture_ids, manufacture_step_balance_sheet, manufacture_step_reward): + result[id] = (bs, rw) + self.total_balance_sheet[id] += bs + + # NOTE: add followings if you need. + # For storages. + # For distributions. + # For vehicles. + + return result + + +if __name__ == "__main__": + from time import time + import cProfile + + env = Env( + scenario="supply_chain", + topology="sample", + durations=100, + max_snapshots=10) + + ss = SCEnvWrapper(env) + + env.step(None) + + start_time = time() + + # cProfile.run("ss.get_state(None)", sort="cumtime") + states = ss.get_state(None) + print(env.agent_idx_list) + print(ss.cur_balance_sheet_reward) + print(states) + + # end_time = time() + # + # print("time cost:", end_time - start_time) + # + # print("dim:", ss.dim) diff --git a/examples/supply_chain/evaluation_with_render.py b/examples/supply_chain/evaluation_with_render.py new file mode 100644 index 000000000..23469f557 --- /dev/null +++ b/examples/supply_chain/evaluation_with_render.py @@ -0,0 +1,81 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +from os import getcwd + +from maro.rl import Actor, LocalLearner + +from examples.supply_chain.render_tools import SimulationTracker +from maro.rl.training.message_enums import MsgKey + + +EPISODE_LEN = 60 +N_EPISODE = 1 +FACILITY_TYPES = ["productstore"] + + +class RenderActor(Actor): + def __init__( + self, env, policies, agent2policy, group, exploration_dict=None, + agent2exploration=None, eval_env=None, log_dir=getcwd(), **proxy_kwargs + ): + super().__init__( + env, policies, agent2policy, group, exploration_dict=exploration_dict, agent2exploration=agent2exploration, + eval_env=eval_env, log_dir=log_dir, **proxy_kwargs + ) + self._log_dir = log_dir + + def _evaluate(self, msg): + log_dir = os.path.join(self._log_dir, f"ep_{msg.body[MsgKey.EPISODE_INDEX]}", self._proxy._name) + tracker = SimulationTracker( + episode_len=EPISODE_LEN, + n_episodes=N_EPISODE, + env=self.eval_env, + policies=self.policy, + log_dir=log_dir, + logger_name=f"SimulationTracker.{self._proxy._name}" + ) + tracker.run_and_render(facility_types=FACILITY_TYPES) + + return super()._evaluate(msg) + + +class RenderLocalLearner(LocalLearner): + def __init__( + self, env, policies, agent2policy, num_episodes, num_steps=-1, exploration_dict=None, agent2exploration=None, + eval_schedule=None, eval_env=None, early_stopper=None, log_env_summary=True, log_dir=getcwd() + ): + super().__init__( + env, policies, agent2policy, num_episodes, num_steps=num_steps, exploration_dict=exploration_dict, + agent2exploration=agent2exploration, eval_schedule=eval_schedule, eval_env=eval_env, + early_stopper=early_stopper, log_env_summary=log_env_summary, log_dir=log_dir + ) + self._log_dir = log_dir + + def run(self): + """Entry point for executing a learning workflow.""" + for ep in range(1, self.num_episodes + 1): + self._train(ep) + if ep == self._eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self._evaluate() + self._run_and_render(ep) + # early stopping check + if self.early_stopper: + self.early_stopper.push(self.eval_env.summary) + if self.early_stopper.stop(): + return + + def _run_and_render(self, ep: int): + log_dir = os.path.join(self._log_dir, f"ep_{ep}") + tracker = SimulationTracker( + episode_len=EPISODE_LEN, + n_episodes=N_EPISODE, + env=self.eval_env, + policies=self._policy, + log_dir=log_dir, + logger_name="SimulationTracker" + ) + tracker.run_and_render(facility_types=FACILITY_TYPES) + diff --git a/examples/supply_chain/main.py b/examples/supply_chain/main.py new file mode 100644 index 000000000..9e39209d8 --- /dev/null +++ b/examples/supply_chain/main.py @@ -0,0 +1,153 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import logging +import os +import sys +from argparse import ArgumentParser +from multiprocessing import Process + +import yaml + +from maro.rl import Learner, LocalPolicyManager, ParallelRolloutManager +from maro.simulator import Env +from maro.simulator.scenarios.supply_chain.world import AgentInfo +from maro.utils import set_seeds + +from examples.supply_chain.env_wrapper import SCEnvWrapper +from examples.supply_chain.evaluation_with_render import RenderActor, RenderLocalLearner +from examples.supply_chain.exploration import get_exploration_mapping +from examples.supply_chain.policies import get_policy_mapping, get_replay_agent_ids + + +# logging.basicConfig(level=logging.DEBUG) + +SC_CODE_DIR = os.path.dirname(os.path.realpath(__file__)) +config_path = os.getenv("CONFIG_PATH", default=os.path.join(SC_CODE_DIR, "config.yml")) +with open(config_path, "r") as config_file: + CONFIG = yaml.safe_load(config_file) +LOG_DIR = os.path.join(SC_CODE_DIR, "logs", CONFIG["experiment_name"]) +OUTPUT_DIR = os.path.join(LOG_DIR, "output") + +# AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id", "parent_id")) +def agent_info_to_agent_id(info: AgentInfo) -> str: + return f"{info.agent_type}.{info.id}" + +def single_thread_mode(config, env_wrapper): + exploration_dict, agent2exploration = get_exploration_mapping(config) + policies, agent2policy = get_policy_mapping(config) + + # create a learner to start training + learner = RenderLocalLearner( + env=env_wrapper, + policies=policies, + agent2policy=agent2policy, + num_episodes=config["num_episodes"], + num_steps=-1, + exploration_dict=exploration_dict, + agent2exploration=agent2exploration, + eval_schedule=config["eval_schedule"], + eval_env=None, + early_stopper=None, + log_env_summary=config["log_env_summary"], + log_dir=LOG_DIR + ) + learner.run() + +def sc_learner(config): + policies, _ = get_policy_mapping(config) + + policy_manager = LocalPolicyManager(policies=policies, log_dir=LOG_DIR) + + rollout_manager = ParallelRolloutManager( + num_actors=config["distributed"]["num_actors"], + group=config["distributed"]["group"], + num_steps=-1, + max_receive_attempts=None, + receive_timeout=None, + max_staleness=0, + num_eval_actors=1, + log_env_summary=True, + log_dir=LOG_DIR, + component_name="rollout_manager", + redis_address=(config["distributed"]["redis_host"], config["distributed"]["redis_port"]), + ) + + learner = Learner( + policy_manager=policy_manager, + rollout_manager=rollout_manager, + num_episodes=config["num_episodes"], + eval_schedule=config["eval_schedule"], + early_stopper=None, + log_dir=LOG_DIR + ) + learner.run() + +def sc_actor(component_name: str, config, env_wrapper): + policies, agent2policy = get_policy_mapping(config) + exploration_dict, agent2exploration = get_exploration_mapping(config) + actor = RenderActor( + env=env_wrapper, + policies=policies, + agent2policy=agent2policy, + group=config["distributed"]["group"], + exploration_dict=exploration_dict, + agent2exploration=agent2exploration, + eval_env=SCEnvWrapper(Env(**config["env"]), replay_agent_ids=config["replay_agent_ids"]), + log_dir=LOG_DIR, + component_name=component_name, + redis_address=(config["distributed"]["redis_host"], config["distributed"]["redis_port"]), + ) + actor.run() + +def multi_process_mode(config): + actor_processes = [ + Process( + target=sc_actor, + args=(f"actor_{i + 1}", config, SCEnvWrapper(Env(**config["env"]), replay_agent_ids=config["replay_agent_ids"]), ) + ) + for i in range(config["distributed"]["num_actors"]) + ] + learner_process = Process(target=sc_learner, args=(config, )) + + for i, actor_process in enumerate(actor_processes): + set_seeds(i) + actor_process.start() + learner_process.start() + + for actor_process in actor_processes: + actor_process.join() + learner_process.join() + + +if __name__ == "__main__": + CONFIG["env"]["topology"] = os.path.join(SC_CODE_DIR, "topologies", CONFIG["env"]["topology"]) + + env = Env(**CONFIG["env"]) + CONFIG["agent_id_list"] = [agent_info_to_agent_id(info) for info in env.agent_idx_list] + CONFIG["replay_agent_ids"] = get_replay_agent_ids(CONFIG["agent_id_list"]) + env_wrapper = SCEnvWrapper(env, replay_agent_ids=CONFIG["replay_agent_ids"]) + + CONFIG["policy"]["consumer"]["model"]["network"]["input_dim"] = env_wrapper.dim + CONFIG["policy"]["producer"]["model"]["network"]["input_dim"] = env_wrapper.dim + CONFIG["policy"]["consumerstore"]["model"]["network"]["input_dim"] = env_wrapper.dim + + parser = ArgumentParser() + parser.add_argument( + "-w", "--whoami", type=int, choices=[-1, 0, 1, 2], default=0, + help="Identify of this process: -1 - single thread mode, 0 - multi-process mode, 1 - learner, 2 - actor" + ) + args = parser.parse_args() + + if args.whoami == -1: + single_thread_mode(CONFIG, env_wrapper) + elif args.whoami == 0: + if CONFIG["distributed"]["redis_host"] != "localhost": + CONFIG["distributed"]["redis_host"] = "localhost" + print(f"******** [multi-process mode] automatically change the 'redis_host' field to 'localhost' ********") + multi_process_mode(CONFIG) + elif args.whoami == 1: + sc_learner(CONFIG) + elif args.whoami == 2: + component_name = os.getenv("COMPONENT") + sc_actor(component_name, CONFIG, env_wrapper) diff --git a/examples/supply_chain/meta.py b/examples/supply_chain/meta.py new file mode 100644 index 000000000..8ab11f6f5 --- /dev/null +++ b/examples/supply_chain/meta.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +cim_path = os.path.dirname(__file__) +sys.path.insert(0, cim_path) +from dqn import get_dqn_policy_for_rollout, get_dqn_policy_for_training +from env_wrapper import CIM_AGENT_IDS + +SC_POLICY_NAMES = ["consumer", "consumer_store", "producer"] # use agent IDs as policy names since each agent uses a separate policy +SC_CREATE_POLICY_FUNC = {name: get_dqn_policy_for_training for name in CIM_POLICY_NAMES} diff --git a/examples/supply_chain/or_policies.py b/examples/supply_chain/or_policies.py new file mode 100644 index 000000000..11cb797e2 --- /dev/null +++ b/examples/supply_chain/or_policies.py @@ -0,0 +1,132 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import random + +import numpy as np +import scipy.stats as st + +from maro.rl.policy import AbsPolicy + + +class ProducerBaselinePolicy(AbsPolicy): + def __init__(self): + super().__init__() + + def choose_action(self, state): + return state.get('product_rate', 500) + + +class ConsumerBaselinePolicy(AbsPolicy): + def __init__(self, num_actions: int): + super().__init__() + self.num_actions = num_actions + + def choose_action(self, state): + if state['is_facility']: + return 0 + # consumer_source_inventory + available_inventory = np.array(state['storage_levels']) + inflight_orders = np.array(state['consumer_in_transit_orders']) + booked_inventory = available_inventory + inflight_orders + + # stop placing orders when the facility runs out of capacity + if np.sum(booked_inventory) > state['storage_capacity']: + return 0 + + most_needed_product_id = state['product_idx'] + sale_mean, sale_std = state['sale_mean'], state['sale_std'] + service_level = state['service_level'] + vlt_buffer_days = 7 + vlt = vlt_buffer_days + state['vlt'] + if booked_inventory[most_needed_product_id] > vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level): + return 0 + consumer_action_space_size = self.num_actions + consumer_quantity = random.randint(0, consumer_action_space_size-1) + return consumer_quantity + + +# Q = \sqrt{2DK/h} +# Q - optimal order quantity +# D - annual demand quantity +# K - fixed cost per order, setup cost (not per unit, typically cost of ordering and shipping and handling. This is not the cost of goods) +# h - annual holding cost per unit, +# also known as carrying cost or storage cost (capital cost, warehouse space, +# refrigeration, insurance, etc. usually not related to the unit production cost) +class ConsumerEOQPolicy(AbsPolicy): + def _get_consumer_quantity(self, state): + order_cost = state['order_cost'] + holding_cost = state['unit_storage_cost'] + sale_gamma = state['sale_mean'] + consumer_quantity = int(np.sqrt(2*sale_gamma*order_cost / holding_cost) / sale_gamma) + return consumer_quantity + + def choose_action(self, state): + if state['is_facility']: + return 0 + # consumer_source_inventory + available_inventory = np.array(state['storage_levels']) + inflight_orders = np.array(state['consumer_in_transit_orders']) + booked_inventory = available_inventory + inflight_orders + + # stop placing orders when the facilty runs out of capacity + if np.sum(booked_inventory) > state['storage_capacity']: + return 0 + + most_needed_product_id = state['product_idx'] + vlt_buffer_days = 7 + vlt = vlt_buffer_days + state['vlt'] + sale_mean, sale_std = state['sale_mean'], state['sale_std'] + service_level = state['service_level'] + + # whether replenishment point is reached + if booked_inventory[most_needed_product_id] > vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level): + return 0 + consumer_quantity = self._get_consumer_quantity(state) + return consumer_quantity + + +# parameters: (r, R), calculate according to VLT, demand variances, and service level +# replenish R - S units whenever the current stock is less than r +# S denotes the number of units in stock +class ConsumerMinMaxPolicy(AbsPolicy): + def choose_action(self, state): + if state['is_facility']: + return 0 + # consumer_source_inventory + available_inventory = np.array(state['storage_levels']) + inflight_orders = np.array(state['consumer_in_transit_orders']) + booked_inventory = available_inventory + inflight_orders + + # stop placing orders when the facility runs out of capacity + if np.sum(booked_inventory) > state['storage_capacity']: + return 0 + + most_needed_product_id = state['product_idx'] + # stop placing orders if no risk of out of stock + vlt_buffer_days = 10 + vlt = state['vlt'] + vlt_buffer_days + sale_mean, sale_std = state['sale_mean'], state['sale_std'] + service_level = state['service_level'] + r = (vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level)) + print(booked_inventory, most_needed_product_id, r) + if booked_inventory[most_needed_product_id] > r: + return 0 + R = 3*r + consumer_quantity = int((R - r) / sale_mean) + return consumer_quantity + + +CONSUMER_NUM_ACTIONS = 10 + +def get_producer_baseline_policy(): + return ProducerBaselinePolicy() + +def get_consumer_baseline_policy(): + return ConsumerBaselinePolicy(CONSUMER_NUM_ACTIONS) + +def get_consumer_minmax_policy(): + return ConsumerMinMaxPolicy() + +def get_consumer_eoq_policy(): + return ConsumerEOQPolicy() diff --git a/examples/supply_chain/or_policy/eoq_policy.py b/examples/supply_chain/or_policy/eoq_policy.py new file mode 100644 index 000000000..c9e4b5f9e --- /dev/null +++ b/examples/supply_chain/or_policy/eoq_policy.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import random as rnd + +import numpy as np + + +from examples.supply_chain.or_policy.base_policy import ConsumerBaselinePolicy + + + diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py new file mode 100644 index 000000000..a03bdfb2d --- /dev/null +++ b/examples/supply_chain/render_tools.py @@ -0,0 +1,182 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import matplotlib.pyplot as plt +import numpy as np +import os + +from maro.utils import Logger + +from examples.supply_chain.env_wrapper import sku_agent_types, SCEnvWrapper + + +class SimulationTracker: + def __init__(self, episode_len: int, n_episodes: int, env: SCEnvWrapper, policies: dict, log_dir="./", logger_name=None): + self.episode_len = episode_len + self.n_episodes = n_episodes + self.env = env + self.policies = policies + self.log_dir = log_dir + + self.logger = Logger(logger_name if logger_name else "SimulationTracker", dump_folder=self.log_dir) + + self.global_balances = np.zeros((n_episodes, episode_len)) + self.global_rewards = np.zeros((n_episodes, episode_len)) + + self.facility_names = [] + for agent in self.env._agent_list: + if agent.agent_type in sku_agent_types: + self.facility_names.append(agent) + + self.step_balances = np.zeros( + (n_episodes, self.episode_len, len(self.facility_names))) + self.step_rewards = np.zeros( + (n_episodes, self.episode_len, len(self.facility_names))) + + self.sku_to_track = None + self.stock_status = None + self.stock_in_transit_status = None + self.reward_status = None + self.demand_status = None + self.reward_discount_status = None + self.order_to_distribute = None + + def _add_sample(self, episode, t, global_balance, global_reward, step_balances, step_rewards): + self.global_balances[episode, t] = global_balance + self.global_rewards[episode, t] = global_reward + for i, f in enumerate(self.facility_names): + self.step_balances[episode, t, i] = step_balances[f.id] + self.step_rewards[episode, t, i] = step_rewards[f.id] + + def _add_sku_status(self, episode, t, stock, order_in_transit, demands, rewards, balances, order_to_distribute): + if self.sku_to_track is None: + self.sku_to_track = list(rewards.keys()) + self.stock_status = np.zeros( + (self.n_episodes, self.episode_len, len(self.sku_to_track))) + self.stock_in_transit_status = np.zeros( + (self.n_episodes, self.episode_len, len(self.sku_to_track))) + self.demand_status = np.zeros( + (self.n_episodes, self.episode_len, len(self.sku_to_track))) + self.reward_status = np.zeros( + (self.n_episodes, self.episode_len, len(self.sku_to_track))) + self.balance_status = np.zeros( + (self.n_episodes, self.episode_len, len(self.sku_to_track))) + self.order_to_distribute = np.zeros( + (self.n_episodes, self.episode_len, len(self.sku_to_track))) + for i, sku_name in enumerate(self.sku_to_track): + self.stock_status[episode, t, i] = stock[sku_name] + self.stock_in_transit_status[episode, t, i] = order_in_transit[sku_name] + self.demand_status[episode, t, i] = demands[sku_name] + self.reward_status[episode, t, i] = rewards[sku_name] + self.balance_status[episode, t, i] = balances[sku_name] + self.order_to_distribute[episode, t, i] = order_to_distribute[sku_name] + + def _render_sku(self, loc_path): + sku_name_dict = {} + facility_type_dict = {} + for agent in self.env._agent_list: + if agent.is_facility: + sku_name = f"{agent.facility_id}_{agent.agent_type}" + else: + sku_name = f"{agent.id}_{agent.sku.id}_{agent.agent_type}" + sku_name_dict[agent.id] = sku_name + facility_type_dict[agent.id] = self.env.env.summary["node_mapping"]["facilities"][agent.facility_id]['class'].__name__ + + for i, sku_name in enumerate(self.sku_to_track): + x = np.linspace(0, self.episode_len, self.episode_len) + stock = self.stock_status[0, :, i] + order_in_transit = self.stock_in_transit_status[0, :, i] + demand = self.demand_status[0, :, i] + reward = self.reward_status[0, :, i] + balance = self.balance_status[0, :, i] + order_to_distribute = self.order_to_distribute[0, :, i] + + fig, ax = plt.subplots(3, 1, figsize=(25, 10)) + ax[0].set_title('SKU Stock Status by Episode') + for y_label, y in [ + ('stock', stock), + ('order_in_transit', order_in_transit), + ('order_to_distribute', order_to_distribute) + ]: + ax[0].plot(x, y, label=y_label) + + ax[1].set_title('SKU Reward / Balance Status by Episode') + ax[1].plot(x, balance, label='Balance') + ax_r = ax[1].twinx() + ax_r.plot(x, reward, label='Reward', color='r') + + ax[2].set_title('SKU demand') + ax[2].plot(x, demand, label="Demand") + + fig.legend() + fig.savefig(os.path.join(loc_path, f"{facility_type_dict[sku_name]}_{sku_name_dict[sku_name]}.png")) + plt.close(fig=fig) + + def _render(self, file_name, metrics, facility_types): + fig, axs = plt.subplots(2, 1, figsize=(25, 10)) + x = np.linspace(0, self.episode_len, self.episode_len) + + _agent_list = [] + _step_idx = [] + for i, agent_info in enumerate(self.facility_names): + if agent_info.agent_type in ['productstore']: + _agent_list.append(agent_info.id) + _step_idx.append(i) + _step_metrics = [metrics[0, :, i] for i in _step_idx] + + # axs[0].set_title('Global balance') + # axs[0].plot(x, self.global_balances.T) + + axs[0].set_title('Cumulative Sum') + axs[0].plot(x, np.cumsum(np.sum(_step_metrics, axis=0))) + + axs[1].set_title('Breakdown by Agent (One Episode)') + axs[1].plot(x, np.cumsum(_step_metrics, axis=1).T) + axs[1].legend(_agent_list, loc='upper left') + + fig.savefig(file_name) + plt.close(fig=fig) + # plt.show() + + def _run_wth_render(self, facility_types): + self.env.reset() + self.env.start() + for epoch in range(self.episode_len): + action = {agent_id: self.policies[agent_id].choose_action(st) for agent_id, st in self.env.state.items()} + self.logger.info(f"epoch: {epoch}, action: {action}") + self.env.step(action) + self.logger.info(f"epoch: {epoch}, action: {self.env.to_env_action(action)}") + if hasattr(self.env, "consumer2product"): + self.logger.info(f"consumer2product: {self.env.consumer2product}") + self.env.get_reward() + step_balances = self.env.balance_status + step_rewards = self.env.reward_status + + self._add_sample(0, epoch, sum(step_balances.values()), sum( + step_rewards.values()), step_balances, step_rewards) + stock_status = self.env.stock_status + order_in_transit_status = self.env.order_in_transit_status + demand_status = self.env.demand_status + reward_status = self.env.reward_status + balance_status = self.env.balance_status + order_to_distribute_status = self.env.order_to_distribute_status + + self._add_sku_status(0, epoch, stock_status, + order_in_transit_status, demand_status, + reward_status, balance_status, + order_to_distribute_status) + + _step_idx = [] + for i, agent_info in enumerate(self.facility_names): + if agent_info.agent_type in facility_types: + _step_idx.append(i) + _step_metrics = [self.step_rewards[0, :, i] for i in _step_idx] + _step_metrics_list = np.cumsum(np.sum(_step_metrics, axis=0)) + return np.sum(_step_metrics), _step_metrics_list + + def run_and_render(self, facility_types): + metric, metric_list = self._run_wth_render(facility_types=facility_types) + self._render(os.path.join(self.log_dir, "plot_balance.png"), self.step_balances, facility_types) + self._render(os.path.join(self.log_dir, "plot_reward.png"), self.step_rewards, facility_types) + self._render_sku(self.log_dir) + return metric, metric_list diff --git a/examples/supply_chain/sc_state_in_maro.md b/examples/supply_chain/sc_state_in_maro.md new file mode 100644 index 000000000..0d512e25d --- /dev/null +++ b/examples/supply_chain/sc_state_in_maro.md @@ -0,0 +1,312 @@ +# SC state in MARO + + +## Env.summary + +MARO通过summary属性对外提供节点相关信息和其他在环境初始化后不会改变的信息。 +在supply chain场景中, summary中包括了以下部分 + +### unit mapping: + +```python +env.summary["node_mapping"]["unit_mapping"] +``` + +unit id 及其对应的data model名字和索引. + +### facilities: + +```python +env.summary["node_mapping"]["facilities"] +``` + +每个facility的层次结构,如sku list, units + +### skus: + +```python +env.summary["node_mapping"]["skus"] +``` + +当前配置中的所有sku + +### max_price: + +```python +env.summary["node_mapping"]["max_price"] +``` + +当前配置中最大的price + +### max_sources_per_facility: + +```python +env.summary["node_mapping"]["max_sources_per_facility"] +``` + +## States + +MARO中有两种对外提供动态状态(state)的方法。 + + +### Metrics: + +起初是为了对外提供reward相关的信息,但是也可以用来作为对外提供状态的接口,用来对外提供不能存放在snapshot list中的数据,比如字典,或者复杂的数据结构。 + +当前实现包含了以下内容: + +#### products: + +product unit相关信息(sale_mean, sale_std, pending_order_daily)。 + +#### facilities: + +facility相关信息(in_transit_orders, pending_order) + + +### Snapshot list: + +snapshot list是MARO中主要的对外提供状态的接口,它提供了所有节点的历史状态(默认为保存所有历史记录,可配置保存最新的N个来节省内存).返回的结果是numpy array,适合做batch操作。 + +snapshot list中的属性都是按照节点组织起来的,每个节点包括多个属性,每个属性可以有多个slot(array like), 同一种节点类型可以有多个instance.节点及其属性的定义可以在maro/simulator/scenarios/supply_chain/datamodels查看。 + +snapshot list的查询是通过slice接口实现的,形式如下: + +```python +env.snapshot_list["node name"][tick(s):node index(s):attribute name(s)] -> np.array + +``` + +该接口返回的是一个4维(tick, node, attribute, slot)的numpy数组(float). + +其中: +1. node name是定义节点是通过node装饰器提供的名字,当前实现包括如下节点: + +consumer, distribution, facility, manufacture, product, seller, storage, vehicle + + +2. tick(s): 可以是一个int, list或者None, 其中None表示查询当前所有历史记录的状态。 + +3. node index(s): 同tick,可以为int, list或者None,None表示查询当前节点类型的所有实例(instance). 使用中需要注意的是,节点(data model)的index和unit的id并不相同,unit的id是在facility和unit之间连续且唯一的,但是节点的index是每种data model类型内部的索引方式。 +所以在实际使用过程中,通常需要得到每个unit和facility对应的index,这部分信息在env.summary中可以得到。 + +4. attribute name(s): 在对应节点上定义过的属性名,可以为一个str,或者List[str] + + +## 示例 + +### 示例1:通过env.summary构建facility&unit的层级信息 + +这个层级信息可以帮我们在之后的操作中快速索引。 +更详细的可以参考examples/supply_chain/env_wrapper.py, _build_internal_helpers方法。 + +```python +# unit info +class UnitBaseInfo: + id: int = None + node_index: int = None + config: dict = None + summary: dict = None + + def __init__(self, unit_summary): + self.id = unit_summary["id"] + self.node_index = unit_summary["node_index"] + self.config = unit_summary.get("config", {}) + self.summary = unit_summary + + def __getitem__(self, key, default=None): + if key in self.summary: + return self.summary[key] + + return default + +# facility id -> { +# data_model_index: int, +# storage: UnitBaseInfo +# distribution: UnitBaseInfo +# product_id: { +# consumer: UnitBaseInfo +# seller: UnitBaseInfo +# manufacture: UnitBaseInfo +# } +#} +facility_levels = {} + +# 默认env.summary包含node_mapping, node_detail 和 event_payload3个部分, +# 这里只需要node——mapping +summary = env.summary["node_mapping"] + +for facility_id, facility in summary["facilities"].items(): + facility_levels[facility_id] = { + "node_index": facility["node_index"], + "config": facility["configs"], + "upstreams": facility["upstreams"], + "skus": facility["skus"] + } + + # facility所属的unit都挂在units下面。 + units = facility["units"] + + facility_levels[facility_id]["storage"] = UnitBaseInfo(units["storage"]) + facility_levels[facility_id]["distribution"] = UnitBaseInfo(units["distribution"]) + + # 所有的product unit + product_units = units["products"] + + if product_units: + for product_id, product in products.items(): + # product unit 本身也包含state + product_info = { + "product": UnitBaseInfo(product) + } + + # 每个product unit可能包括下面3个unit + # 注意,为了简单我们没有检查对应的key时候存在! + product_info["seller"] = UnitBaseInfo(product["seller"]) + product_info["consumer"] = UnitBaseInfo(product["consumer"]) + product_info["manufacture"] = UnitBaseInfo(product["manufacture"]) + + # 这里我们用product_id作为product 的key,可按照需求更改 + facility_levels[product_id] = product_info +``` + +### 示例2:通过env.summary构建unit id到node index的索引表 + +实际上,在示例的遍历过程中,我们就已经可以得到unit及其对应的node index索引表了,如果你不在意层级关系的话,可以通过unit_mapping快速得到这个索引。 + +```python + +# unit_mapping是一个字典,key是unit id, value是(data model name, data model node index, facility id)类型的tuple。 + +summary = env.summary["node_mapping"] + +unitid2index_mapping = {} + +for unit_id, unit_detail in summary["unit_mapping"].items(): + unitid2index_mapping[unit_id] = unit_detail[1] + +``` + +### 示例3:在state shaping过程中查询seller的销售和需求的历史,时间长度为hist_len + +```python + +# 模拟器当前时间 +cur_tick = env.tick + +# 需要查询的历史长度 +hist_len = 4 + +# 历史长度对象当前时间的时间序列 +ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] + +# 查询seller节点的过去4(含当前)个tick的sold和demand值 +# NOTE:因为这两个是都是整数,所以做一次类型转换 +seller_states =env.snapshot_list["seller"][ticks::("sold", "demand")].astype(np.int) + +# 结果应为4为numpy array +# 假设我们有2个seller +""" +[ + [ + [ + [0.0], # sold (slot = 1) + [0.0] # demand (slot = 1) + ], # seller 0 + [...] # seller 1 + ], # tick 0 + [ + [...], + [...] + ], # tick 1 + [ + [...], + [...] + ], # tick 2 + [ + [...], + [...] + ] # tick 3 (latest) +] +""" + +# 这样得到的结果就是所有的seller unit对应的销售和需求历史。 + +# 假设我们当前需要的seller unit的data model index 为 1 的话。 +cur_seller_node_index = 1 + +# 那么当前seller的销售和需求历史分别为: +cur_seller_hist = seller_states[:, cur_seller_node_index, :] + +# 第二个参数为0,是因为sold是我们查询的第一个属性 +sale_hist = cur_seller_hist[:, 0].flatten() +demand_hist = cur_seller_hist[:, 1].flatten() + +``` + +### 示例4:计算unit或facility的balance sheet + +详细的可以参考examples/supply_chain/env_wrapper.py中的BalanceSheetCalculator类。 + +```python + +# 假设我们需要计算seller, consumer, manufacture的balance sheet. +# 实际情况需要用这3个计算出对应的product unit的balance sheet,这里只是作为示例 + +# 计算所需要属性 +consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") +seller_features = ("id", "sold", "demand", "price", "backlog_ratio") +manufacture_features = ("id", "manufacturing_number", "product_unit_cost") + +# 对应的3种data model snapshot list +consumer_ss = env.snapshot_list["consumer"] +seller_ss = env.snapshot_list["seller"] +manufacture_ss = env.snapshot_list["manufacture"] + +# 当前时间 +tick = env.tick + +# 3种unit对应的所有实例的state +# 这里用len(features)做reshape的原因是,当前用到的属性都是slots=1 +# 又因为我们的tick数量为1,这样reshape之后, 每行对应一个unit的实例,每列对应一个属性 +consumer_states = consumer_ss[tick::consumer_features].flatten().reshape(-1, len(consumer_features)) + +seller_states = seller_ss[tick::seller_features].flatten().reshape(-1, len(seller_features)) + +man_states = manufacture_ss[tick::manufacture_features].flatten().reshape(-1, len(manufacture_features)) + +# balance sheet计算,通常balance sheet 包含profit和loss两部分,这里分开保存。 + +# consumer部分 +# profit = quantity * price +consumer_profit = consumer_states[:, 1] * consumer_states[:, 2] + +# loss = -1 * (order_cost + order_product_cost) +consumer_loss = -1 * (consumer_states[:, 3] + consumer_states[:, 4]) + +# discount在代码里似乎没有用到 +reward_discount = 0 + +# consumer step reward +consumer_reward = consumer_loss + consumer_profit * reward_discount + +# seller部分 +# profit = sold * price +seller_profit = seller_states[:, 1] * seller_states[:, 3] + +# loss = -1 * demand * price * backlog_ratio +seller_loss = -1 * seller_states[:, 2] * seller_states[:, 3] * seller_states[:, 4] + +seller_reward = seller_profit + seller_loss + +# manufacture部分 +# profit = 0 +# loss = manufacture_number * cost +man_loss = -1 * man_states[:, 1] * man_states[:, 2] + +man_reward = man_loss + +# 这样我们就用numpy的batch操作完成3种unit的balance sheet和reward计算, +# 后续需要我们按照product/facility对这些结果做聚合, 这需要类似示例1这样的层级结构,具体可参考现有代码。 + +``` \ No newline at end of file diff --git a/examples/supply_chain/topologies/random/config.yml b/examples/supply_chain/topologies/random/config.yml new file mode 100644 index 000000000..f4f2af16c --- /dev/null +++ b/examples/supply_chain/topologies/random/config.yml @@ -0,0 +1,29344 @@ +facility_definitions: + RetailerFacility: + children: + products: + class: StoreProductUnit + config: + agent_type: product + consumer: + class: ConsumerUnit + config: consumer + seller: + class: SellerUnit + config: + sale_hist_len: 4 + is_template: true + storage: + class: StorageUnit + class: RetailerFacility + config: + agent_type: facility + SupplierFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: product + consumer: + class: ConsumerUnit + config: + agent_type: consumer + manufacture: + class: ManufactureUnit + config: + agent_type: producer + is_template: true + storage: + class: StorageUnit + class: SupplierFacility + config: + agent_type: facility + WarehouseFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: product + consumer: + class: ConsumerUnit + config: + agent_type: consumer + is_template: true + storage: + class: StorageUnit + class: WarehouseFacility + config: + agent_type: facility +normal_vehicle: &id001 + class: VehicleUnit + config: + patient: 100 + unit_transport_cost: 1 +settings: + constraint_state_hist_len: 4 + constraint_violate_reward: -1000000.0 + consumption_hist_len: 4 + downsampling_rate: 1 + episod_duration: 21 + gamma: 0.99 + global_reward_weight_consumer: 0.5 + global_reward_weight_producer: 0.5 + heading_timesteps: 7 + initial_balance: 100000 + pending_order_len: 4 + replenishment_discount: 0.9 + reward_normalization: 10000000.0 + sale_hist_len: 4 + tail_timesteps: 7 + total_echelons: 3 +world: + facilities: + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5324500 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 200 + definition_ref: SupplierFacility + name: SUPPLIER0 + skus: + SKU0: + cost: 289 + init_stock: 3150 + price: 322 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU1: + cost: 255 + init_stock: 1100 + price: 284 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU10: + cost: 61 + init_stock: 1250 + price: 68 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU100: + cost: 14 + init_stock: 3250 + price: 16 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU101: + cost: 75 + init_stock: 3550 + price: 84 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU102: + cost: 295 + init_stock: 4650 + price: 328 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU103: + cost: 300 + init_stock: 4750 + price: 334 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU104: + cost: 44 + init_stock: 3250 + price: 49 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU105: + cost: 99 + init_stock: 2850 + price: 110 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU106: + cost: 225 + init_stock: 3650 + price: 251 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU107: + cost: 380 + init_stock: 4350 + price: 423 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU108: + cost: 412 + init_stock: 4600 + price: 458 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU109: + cost: 79 + init_stock: 4100 + price: 88 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU11: + cost: 360 + init_stock: 2550 + price: 400 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU110: + cost: 59 + init_stock: 700 + price: 66 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU111: + cost: 234 + init_stock: 3050 + price: 260 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU112: + cost: 54 + init_stock: 4750 + price: 61 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU113: + cost: 313 + init_stock: 1550 + price: 348 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU114: + cost: 350 + init_stock: 1350 + price: 389 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU115: + cost: 257 + init_stock: 4300 + price: 286 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU116: + cost: 446 + init_stock: 3600 + price: 496 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU117: + cost: 288 + init_stock: 4600 + price: 320 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU118: + cost: 164 + init_stock: 1650 + price: 183 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU119: + cost: 188 + init_stock: 1600 + price: 209 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU12: + cost: 100 + init_stock: 4200 + price: 112 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU120: + cost: 108 + init_stock: 4900 + price: 121 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU121: + cost: 36 + init_stock: 4250 + price: 40 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU122: + cost: 393 + init_stock: 350 + price: 437 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU123: + cost: 209 + init_stock: 950 + price: 233 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU124: + cost: 163 + init_stock: 1800 + price: 182 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU125: + cost: 14 + init_stock: 4600 + price: 16 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU126: + cost: 32 + init_stock: 1950 + price: 36 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU127: + cost: 195 + init_stock: 1550 + price: 217 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU128: + cost: 148 + init_stock: 950 + price: 165 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU129: + cost: 128 + init_stock: 2500 + price: 143 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU13: + cost: 285 + init_stock: 2850 + price: 317 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU130: + cost: 313 + init_stock: 4900 + price: 348 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU131: + cost: 57 + init_stock: 2250 + price: 64 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU132: + cost: 384 + init_stock: 1050 + price: 427 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU133: + cost: 201 + init_stock: 1450 + price: 224 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU134: + cost: 302 + init_stock: 3850 + price: 336 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU135: + cost: 137 + init_stock: 5000 + price: 153 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU136: + cost: 179 + init_stock: 3550 + price: 199 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU137: + cost: 83 + init_stock: 3700 + price: 93 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU138: + cost: 205 + init_stock: 1800 + price: 228 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU139: + cost: 186 + init_stock: 1200 + price: 207 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU14: + cost: 241 + init_stock: 3100 + price: 268 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU140: + cost: 234 + init_stock: 1700 + price: 261 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU141: + cost: 171 + init_stock: 2050 + price: 190 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU142: + cost: 288 + init_stock: 1900 + price: 320 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU143: + cost: 286 + init_stock: 1300 + price: 318 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU144: + cost: 360 + init_stock: 600 + price: 400 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU145: + cost: 359 + init_stock: 4100 + price: 399 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU146: + cost: 159 + init_stock: 2400 + price: 177 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU147: + cost: 424 + init_stock: 2800 + price: 472 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU148: + cost: 281 + init_stock: 3850 + price: 313 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU149: + cost: 321 + init_stock: 3850 + price: 357 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU15: + cost: 46 + init_stock: 250 + price: 52 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU150: + cost: 95 + init_stock: 3500 + price: 106 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU151: + cost: 200 + init_stock: 3650 + price: 223 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU152: + cost: 9 + init_stock: 2550 + price: 10 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU153: + cost: 396 + init_stock: 3100 + price: 441 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU154: + cost: 69 + init_stock: 4250 + price: 77 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU155: + cost: 379 + init_stock: 2650 + price: 422 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU156: + cost: 9 + init_stock: 600 + price: 10 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU157: + cost: 369 + init_stock: 3750 + price: 410 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU158: + cost: 130 + init_stock: 4050 + price: 145 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU159: + cost: 173 + init_stock: 1250 + price: 193 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU16: + cost: 157 + init_stock: 2900 + price: 175 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU160: + cost: 413 + init_stock: 4250 + price: 459 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU161: + cost: 215 + init_stock: 2300 + price: 239 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU162: + cost: 142 + init_stock: 250 + price: 158 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU163: + cost: 437 + init_stock: 1950 + price: 486 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU164: + cost: 446 + init_stock: 5000 + price: 496 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU165: + cost: 246 + init_stock: 1650 + price: 274 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU166: + cost: 71 + init_stock: 4450 + price: 79 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU167: + cost: 398 + init_stock: 650 + price: 443 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU168: + cost: 321 + init_stock: 4350 + price: 357 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU169: + cost: 332 + init_stock: 4900 + price: 369 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU17: + cost: 311 + init_stock: 450 + price: 346 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU170: + cost: 61 + init_stock: 2750 + price: 68 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU171: + cost: 358 + init_stock: 3800 + price: 398 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU172: + cost: 180 + init_stock: 3550 + price: 200 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU173: + cost: 157 + init_stock: 4800 + price: 175 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU174: + cost: 261 + init_stock: 3800 + price: 291 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU175: + cost: 75 + init_stock: 3750 + price: 84 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU176: + cost: 366 + init_stock: 3300 + price: 407 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU177: + cost: 231 + init_stock: 1550 + price: 257 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU178: + cost: 380 + init_stock: 250 + price: 423 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU179: + cost: 447 + init_stock: 4150 + price: 497 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU18: + cost: 232 + init_stock: 4050 + price: 258 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU180: + cost: 195 + init_stock: 2750 + price: 217 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU181: + cost: 128 + init_stock: 3000 + price: 143 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU182: + cost: 393 + init_stock: 4950 + price: 437 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU183: + cost: 130 + init_stock: 400 + price: 145 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU184: + cost: 65 + init_stock: 1200 + price: 73 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU185: + cost: 9 + init_stock: 4500 + price: 10 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU186: + cost: 323 + init_stock: 1100 + price: 359 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU187: + cost: 159 + init_stock: 1500 + price: 177 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU188: + cost: 351 + init_stock: 4350 + price: 391 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU189: + cost: 322 + init_stock: 1750 + price: 358 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU19: + cost: 429 + init_stock: 4550 + price: 477 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU190: + cost: 101 + init_stock: 850 + price: 113 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU191: + cost: 425 + init_stock: 2700 + price: 473 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU192: + cost: 373 + init_stock: 3050 + price: 415 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU193: + cost: 186 + init_stock: 1500 + price: 207 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU194: + cost: 388 + init_stock: 250 + price: 432 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU195: + cost: 196 + init_stock: 1550 + price: 218 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU196: + cost: 44 + init_stock: 3400 + price: 49 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU197: + cost: 272 + init_stock: 2850 + price: 303 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU198: + cost: 152 + init_stock: 2700 + price: 169 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU199: + cost: 404 + init_stock: 1150 + price: 449 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU2: + cost: 297 + init_stock: 3500 + price: 331 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU20: + cost: 301 + init_stock: 1250 + price: 335 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU200: + cost: 58 + init_stock: 1250 + price: 65 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU201: + cost: 93 + init_stock: 2950 + price: 104 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU202: + cost: 127 + init_stock: 3650 + price: 142 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU203: + cost: 396 + init_stock: 4100 + price: 440 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU204: + cost: 440 + init_stock: 2350 + price: 489 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU205: + cost: 117 + init_stock: 5000 + price: 130 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU206: + cost: 301 + init_stock: 550 + price: 335 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU207: + cost: 126 + init_stock: 4000 + price: 140 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU208: + cost: 441 + init_stock: 3850 + price: 491 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU209: + cost: 161 + init_stock: 1000 + price: 179 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU21: + cost: 110 + init_stock: 5000 + price: 123 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU210: + cost: 363 + init_stock: 3450 + price: 404 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU211: + cost: 156 + init_stock: 4550 + price: 174 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU212: + cost: 364 + init_stock: 3950 + price: 405 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU213: + cost: 108 + init_stock: 3200 + price: 121 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU214: + cost: 90 + init_stock: 500 + price: 101 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU215: + cost: 377 + init_stock: 2350 + price: 419 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU216: + cost: 297 + init_stock: 1150 + price: 330 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU217: + cost: 255 + init_stock: 3250 + price: 284 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU218: + cost: 184 + init_stock: 2950 + price: 205 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU219: + cost: 82 + init_stock: 2300 + price: 92 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU22: + cost: 443 + init_stock: 3300 + price: 493 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU220: + cost: 348 + init_stock: 4350 + price: 387 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU221: + cost: 35 + init_stock: 3900 + price: 39 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU222: + cost: 103 + init_stock: 1800 + price: 115 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU223: + cost: 176 + init_stock: 600 + price: 196 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU224: + cost: 348 + init_stock: 250 + price: 387 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU225: + cost: 147 + init_stock: 1450 + price: 164 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU226: + cost: 185 + init_stock: 650 + price: 206 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU227: + cost: 431 + init_stock: 1200 + price: 479 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU228: + cost: 12 + init_stock: 4500 + price: 14 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU229: + cost: 424 + init_stock: 2200 + price: 472 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU23: + cost: 348 + init_stock: 2100 + price: 387 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU230: + cost: 216 + init_stock: 1150 + price: 241 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU231: + cost: 43 + init_stock: 4050 + price: 48 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU232: + cost: 201 + init_stock: 4800 + price: 224 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU233: + cost: 324 + init_stock: 3750 + price: 360 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU234: + cost: 258 + init_stock: 250 + price: 287 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU235: + cost: 21 + init_stock: 2850 + price: 24 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU236: + cost: 139 + init_stock: 2750 + price: 155 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU237: + cost: 389 + init_stock: 2250 + price: 433 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU238: + cost: 57 + init_stock: 3300 + price: 64 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU239: + cost: 92 + init_stock: 4900 + price: 103 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU24: + cost: 87 + init_stock: 2350 + price: 97 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU240: + cost: 335 + init_stock: 2350 + price: 373 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU241: + cost: 395 + init_stock: 3550 + price: 439 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU242: + cost: 15 + init_stock: 2200 + price: 17 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU243: + cost: 316 + init_stock: 700 + price: 352 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU244: + cost: 156 + init_stock: 4100 + price: 174 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU245: + cost: 363 + init_stock: 3300 + price: 404 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU246: + cost: 270 + init_stock: 1500 + price: 300 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU247: + cost: 347 + init_stock: 1750 + price: 386 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU248: + cost: 319 + init_stock: 1450 + price: 355 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU249: + cost: 320 + init_stock: 1250 + price: 356 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU25: + cost: 144 + init_stock: 250 + price: 161 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU250: + cost: 114 + init_stock: 2700 + price: 127 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU251: + cost: 309 + init_stock: 3050 + price: 344 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU252: + cost: 162 + init_stock: 4150 + price: 181 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU253: + cost: 403 + init_stock: 800 + price: 448 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU254: + cost: 435 + init_stock: 2300 + price: 484 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU255: + cost: 261 + init_stock: 3350 + price: 290 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU256: + cost: 81 + init_stock: 3600 + price: 91 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU257: + cost: 313 + init_stock: 2850 + price: 348 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU258: + cost: 440 + init_stock: 2150 + price: 489 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU259: + cost: 299 + init_stock: 3450 + price: 333 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU26: + cost: 206 + init_stock: 3150 + price: 229 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU260: + cost: 438 + init_stock: 2600 + price: 487 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU261: + cost: 331 + init_stock: 1100 + price: 368 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU262: + cost: 298 + init_stock: 3900 + price: 332 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU263: + cost: 170 + init_stock: 3700 + price: 189 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU264: + cost: 324 + init_stock: 3950 + price: 361 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU265: + cost: 257 + init_stock: 2950 + price: 286 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU266: + cost: 115 + init_stock: 2350 + price: 128 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU267: + cost: 69 + init_stock: 4000 + price: 77 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU268: + cost: 198 + init_stock: 4450 + price: 221 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU269: + cost: 113 + init_stock: 2200 + price: 126 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU27: + cost: 333 + init_stock: 2800 + price: 370 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU270: + cost: 163 + init_stock: 3700 + price: 182 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU271: + cost: 207 + init_stock: 900 + price: 230 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU272: + cost: 329 + init_stock: 850 + price: 366 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU273: + cost: 378 + init_stock: 900 + price: 421 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU274: + cost: 26 + init_stock: 3850 + price: 29 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU275: + cost: 45 + init_stock: 2400 + price: 50 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU276: + cost: 146 + init_stock: 2700 + price: 163 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU277: + cost: 404 + init_stock: 2050 + price: 449 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU278: + cost: 94 + init_stock: 600 + price: 105 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU279: + cost: 45 + init_stock: 1950 + price: 51 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU28: + cost: 187 + init_stock: 2350 + price: 208 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU280: + cost: 265 + init_stock: 1650 + price: 295 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU281: + cost: 355 + init_stock: 3750 + price: 395 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU282: + cost: 56 + init_stock: 2300 + price: 63 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU283: + cost: 352 + init_stock: 4600 + price: 392 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU284: + cost: 309 + init_stock: 3350 + price: 344 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU285: + cost: 119 + init_stock: 4550 + price: 133 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU286: + cost: 303 + init_stock: 1950 + price: 337 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU287: + cost: 337 + init_stock: 2800 + price: 375 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU288: + cost: 162 + init_stock: 1900 + price: 181 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU289: + cost: 60 + init_stock: 1550 + price: 67 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU29: + cost: 220 + init_stock: 2900 + price: 245 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU290: + cost: 394 + init_stock: 3350 + price: 438 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU291: + cost: 84 + init_stock: 3050 + price: 94 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU292: + cost: 167 + init_stock: 250 + price: 186 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU293: + cost: 145 + init_stock: 2750 + price: 162 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU294: + cost: 368 + init_stock: 450 + price: 409 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU295: + cost: 391 + init_stock: 4650 + price: 435 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU296: + cost: 333 + init_stock: 4600 + price: 370 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU297: + cost: 268 + init_stock: 1900 + price: 298 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU298: + cost: 257 + init_stock: 1750 + price: 286 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU299: + cost: 330 + init_stock: 2550 + price: 367 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU3: + cost: 364 + init_stock: 600 + price: 405 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU30: + cost: 323 + init_stock: 3450 + price: 359 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU300: + cost: 338 + init_stock: 2900 + price: 376 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU301: + cost: 389 + init_stock: 4150 + price: 433 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU302: + cost: 165 + init_stock: 550 + price: 184 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU303: + cost: 221 + init_stock: 4700 + price: 246 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU304: + cost: 377 + init_stock: 1150 + price: 419 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU305: + cost: 445 + init_stock: 5000 + price: 495 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU306: + cost: 431 + init_stock: 2100 + price: 479 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU307: + cost: 189 + init_stock: 3900 + price: 210 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU308: + cost: 187 + init_stock: 250 + price: 208 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU309: + cost: 90 + init_stock: 4600 + price: 101 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU31: + cost: 62 + init_stock: 2800 + price: 69 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU310: + cost: 210 + init_stock: 2200 + price: 234 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU311: + cost: 275 + init_stock: 4000 + price: 306 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU312: + cost: 261 + init_stock: 1250 + price: 291 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU313: + cost: 291 + init_stock: 1900 + price: 324 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU314: + cost: 363 + init_stock: 1450 + price: 404 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU315: + cost: 423 + init_stock: 4200 + price: 471 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU316: + cost: 181 + init_stock: 900 + price: 202 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU317: + cost: 194 + init_stock: 1200 + price: 216 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU318: + cost: 250 + init_stock: 4250 + price: 278 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU319: + cost: 231 + init_stock: 2650 + price: 257 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU32: + cost: 302 + init_stock: 1100 + price: 336 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU320: + cost: 176 + init_stock: 1950 + price: 196 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU321: + cost: 60 + init_stock: 800 + price: 67 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU322: + cost: 216 + init_stock: 5000 + price: 240 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU323: + cost: 59 + init_stock: 1950 + price: 66 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU324: + cost: 397 + init_stock: 4650 + price: 442 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU325: + cost: 407 + init_stock: 3450 + price: 453 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU326: + cost: 310 + init_stock: 1200 + price: 345 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU327: + cost: 411 + init_stock: 700 + price: 457 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU328: + cost: 351 + init_stock: 2250 + price: 390 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU329: + cost: 60 + init_stock: 2100 + price: 67 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU33: + cost: 374 + init_stock: 4600 + price: 416 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU330: + cost: 275 + init_stock: 1950 + price: 306 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU331: + cost: 124 + init_stock: 2050 + price: 138 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU332: + cost: 351 + init_stock: 4800 + price: 390 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU333: + cost: 436 + init_stock: 2650 + price: 485 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU334: + cost: 164 + init_stock: 2850 + price: 183 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU335: + cost: 72 + init_stock: 4050 + price: 80 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU336: + cost: 266 + init_stock: 1400 + price: 296 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU337: + cost: 220 + init_stock: 1450 + price: 245 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU338: + cost: 78 + init_stock: 4050 + price: 87 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU339: + cost: 238 + init_stock: 3150 + price: 265 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU34: + cost: 85 + init_stock: 650 + price: 95 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU340: + cost: 432 + init_stock: 4350 + price: 480 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU341: + cost: 415 + init_stock: 3500 + price: 462 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU342: + cost: 129 + init_stock: 450 + price: 144 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU343: + cost: 279 + init_stock: 750 + price: 310 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU344: + cost: 321 + init_stock: 4350 + price: 357 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU345: + cost: 397 + init_stock: 4450 + price: 442 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU346: + cost: 315 + init_stock: 2100 + price: 350 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU347: + cost: 447 + init_stock: 4100 + price: 497 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU348: + cost: 132 + init_stock: 1000 + price: 147 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU349: + cost: 60 + init_stock: 3350 + price: 67 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU35: + cost: 240 + init_stock: 4300 + price: 267 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU350: + cost: 251 + init_stock: 4600 + price: 279 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU351: + cost: 139 + init_stock: 3350 + price: 155 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU352: + cost: 63 + init_stock: 900 + price: 71 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU353: + cost: 227 + init_stock: 4650 + price: 253 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU354: + cost: 356 + init_stock: 3950 + price: 396 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU355: + cost: 56 + init_stock: 500 + price: 63 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU356: + cost: 313 + init_stock: 1450 + price: 348 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU357: + cost: 449 + init_stock: 4600 + price: 499 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU358: + cost: 242 + init_stock: 3450 + price: 269 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU359: + cost: 73 + init_stock: 3750 + price: 82 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU36: + cost: 94 + init_stock: 500 + price: 105 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU360: + cost: 435 + init_stock: 1250 + price: 484 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU361: + cost: 146 + init_stock: 4500 + price: 163 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU362: + cost: 417 + init_stock: 4350 + price: 464 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU363: + cost: 46 + init_stock: 3900 + price: 52 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU364: + cost: 348 + init_stock: 1650 + price: 387 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU365: + cost: 142 + init_stock: 4150 + price: 158 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU366: + cost: 399 + init_stock: 4300 + price: 444 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU367: + cost: 244 + init_stock: 2300 + price: 272 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU368: + cost: 424 + init_stock: 1650 + price: 472 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU369: + cost: 86 + init_stock: 3750 + price: 96 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU37: + cost: 59 + init_stock: 2950 + price: 66 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU370: + cost: 100 + init_stock: 750 + price: 112 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU371: + cost: 295 + init_stock: 250 + price: 328 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU372: + cost: 60 + init_stock: 1600 + price: 67 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU373: + cost: 52 + init_stock: 3350 + price: 58 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU374: + cost: 34 + init_stock: 2700 + price: 38 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU375: + cost: 254 + init_stock: 3600 + price: 283 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU376: + cost: 140 + init_stock: 4100 + price: 156 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU377: + cost: 70 + init_stock: 3350 + price: 78 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU378: + cost: 381 + init_stock: 1750 + price: 424 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU379: + cost: 9 + init_stock: 2450 + price: 11 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU38: + cost: 309 + init_stock: 2150 + price: 344 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU380: + cost: 353 + init_stock: 2650 + price: 393 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU381: + cost: 428 + init_stock: 300 + price: 476 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU382: + cost: 112 + init_stock: 3550 + price: 125 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU383: + cost: 273 + init_stock: 4600 + price: 304 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU384: + cost: 288 + init_stock: 450 + price: 320 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU385: + cost: 108 + init_stock: 650 + price: 120 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU386: + cost: 265 + init_stock: 1550 + price: 295 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU387: + cost: 100 + init_stock: 4850 + price: 112 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU388: + cost: 364 + init_stock: 2200 + price: 405 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU389: + cost: 338 + init_stock: 3500 + price: 376 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU39: + cost: 22 + init_stock: 2550 + price: 25 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU390: + cost: 129 + init_stock: 1950 + price: 144 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU391: + cost: 209 + init_stock: 3350 + price: 233 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU392: + cost: 146 + init_stock: 3700 + price: 163 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU393: + cost: 438 + init_stock: 3350 + price: 487 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU394: + cost: 138 + init_stock: 2650 + price: 154 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU395: + cost: 439 + init_stock: 1650 + price: 488 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU396: + cost: 299 + init_stock: 3050 + price: 333 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU397: + cost: 414 + init_stock: 2550 + price: 460 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU398: + cost: 210 + init_stock: 2900 + price: 234 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU399: + cost: 338 + init_stock: 1850 + price: 376 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU4: + cost: 395 + init_stock: 350 + price: 439 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU40: + cost: 441 + init_stock: 4950 + price: 490 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU400: + cost: 400 + init_stock: 4200 + price: 445 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU401: + cost: 369 + init_stock: 3900 + price: 410 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU402: + cost: 77 + init_stock: 350 + price: 86 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU403: + cost: 80 + init_stock: 4950 + price: 89 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU404: + cost: 258 + init_stock: 3050 + price: 287 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU405: + cost: 414 + init_stock: 950 + price: 460 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU406: + cost: 294 + init_stock: 5000 + price: 327 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU407: + cost: 23 + init_stock: 2300 + price: 26 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU408: + cost: 399 + init_stock: 400 + price: 444 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU409: + cost: 231 + init_stock: 4550 + price: 257 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU41: + cost: 126 + init_stock: 3950 + price: 141 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU410: + cost: 63 + init_stock: 800 + price: 70 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU411: + cost: 189 + init_stock: 4750 + price: 210 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU412: + cost: 257 + init_stock: 3100 + price: 286 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU413: + cost: 362 + init_stock: 4150 + price: 403 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU414: + cost: 148 + init_stock: 4350 + price: 165 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU415: + cost: 261 + init_stock: 1150 + price: 291 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU416: + cost: 205 + init_stock: 450 + price: 228 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU417: + cost: 398 + init_stock: 3600 + price: 443 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU418: + cost: 412 + init_stock: 650 + price: 458 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU419: + cost: 272 + init_stock: 4450 + price: 303 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU42: + cost: 415 + init_stock: 1300 + price: 462 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU420: + cost: 241 + init_stock: 2100 + price: 268 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU421: + cost: 192 + init_stock: 2300 + price: 214 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU422: + cost: 46 + init_stock: 2700 + price: 52 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU423: + cost: 169 + init_stock: 3300 + price: 188 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU424: + cost: 170 + init_stock: 550 + price: 189 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU425: + cost: 145 + init_stock: 600 + price: 162 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU426: + cost: 112 + init_stock: 4900 + price: 125 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU427: + cost: 371 + init_stock: 4700 + price: 413 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU428: + cost: 351 + init_stock: 3150 + price: 391 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU429: + cost: 35 + init_stock: 2050 + price: 39 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU43: + cost: 195 + init_stock: 3400 + price: 217 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU430: + cost: 194 + init_stock: 3650 + price: 216 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU431: + cost: 295 + init_stock: 1050 + price: 328 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU432: + cost: 22 + init_stock: 2300 + price: 25 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU433: + cost: 313 + init_stock: 2250 + price: 348 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU434: + cost: 33 + init_stock: 1500 + price: 37 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU435: + cost: 244 + init_stock: 1500 + price: 272 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU436: + cost: 64 + init_stock: 4050 + price: 72 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU437: + cost: 103 + init_stock: 700 + price: 115 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU438: + cost: 128 + init_stock: 500 + price: 143 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU439: + cost: 230 + init_stock: 1900 + price: 256 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU44: + cost: 324 + init_stock: 2400 + price: 360 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU440: + cost: 223 + init_stock: 4100 + price: 248 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU441: + cost: 227 + init_stock: 2800 + price: 253 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU442: + cost: 423 + init_stock: 4400 + price: 470 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU443: + cost: 196 + init_stock: 3650 + price: 218 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU444: + cost: 140 + init_stock: 300 + price: 156 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU445: + cost: 234 + init_stock: 4300 + price: 260 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU446: + cost: 447 + init_stock: 2450 + price: 497 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU447: + cost: 176 + init_stock: 250 + price: 196 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU448: + cost: 436 + init_stock: 3550 + price: 485 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU449: + cost: 253 + init_stock: 1900 + price: 282 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU45: + cost: 227 + init_stock: 500 + price: 253 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU450: + cost: 52 + init_stock: 4900 + price: 58 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU451: + cost: 421 + init_stock: 3450 + price: 468 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU452: + cost: 56 + init_stock: 3950 + price: 63 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU453: + cost: 33 + init_stock: 1750 + price: 37 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU454: + cost: 444 + init_stock: 4250 + price: 494 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU455: + cost: 122 + init_stock: 1300 + price: 136 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU456: + cost: 304 + init_stock: 1250 + price: 338 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU457: + cost: 152 + init_stock: 3200 + price: 169 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU458: + cost: 362 + init_stock: 350 + price: 403 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU459: + cost: 382 + init_stock: 1700 + price: 425 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU46: + cost: 269 + init_stock: 2500 + price: 299 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU460: + cost: 364 + init_stock: 3650 + price: 405 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU461: + cost: 225 + init_stock: 2400 + price: 251 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU462: + cost: 403 + init_stock: 450 + price: 448 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU463: + cost: 168 + init_stock: 4100 + price: 187 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU464: + cost: 251 + init_stock: 1700 + price: 279 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU465: + cost: 132 + init_stock: 5000 + price: 147 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU466: + cost: 87 + init_stock: 2100 + price: 97 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU467: + cost: 127 + init_stock: 1800 + price: 142 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU468: + cost: 49 + init_stock: 2500 + price: 55 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU469: + cost: 160 + init_stock: 750 + price: 178 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU47: + cost: 108 + init_stock: 1950 + price: 121 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU470: + cost: 335 + init_stock: 1600 + price: 373 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU471: + cost: 283 + init_stock: 3550 + price: 315 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU472: + cost: 378 + init_stock: 2950 + price: 421 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU473: + cost: 175 + init_stock: 1050 + price: 195 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU474: + cost: 403 + init_stock: 4300 + price: 448 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU475: + cost: 30 + init_stock: 4700 + price: 34 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU476: + cost: 225 + init_stock: 4500 + price: 251 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU477: + cost: 260 + init_stock: 2350 + price: 289 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU478: + cost: 431 + init_stock: 4400 + price: 479 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU479: + cost: 329 + init_stock: 1900 + price: 366 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU48: + cost: 306 + init_stock: 2900 + price: 340 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU480: + cost: 16 + init_stock: 1500 + price: 18 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU481: + cost: 297 + init_stock: 4400 + price: 330 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU482: + cost: 84 + init_stock: 4350 + price: 94 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU483: + cost: 268 + init_stock: 1700 + price: 298 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU484: + cost: 75 + init_stock: 3650 + price: 84 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU485: + cost: 286 + init_stock: 3350 + price: 318 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU486: + cost: 109 + init_stock: 2600 + price: 122 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU487: + cost: 249 + init_stock: 3300 + price: 277 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU488: + cost: 32 + init_stock: 1350 + price: 36 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU489: + cost: 53 + init_stock: 1550 + price: 59 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU49: + cost: 236 + init_stock: 4750 + price: 263 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU490: + cost: 144 + init_stock: 4050 + price: 161 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU491: + cost: 399 + init_stock: 3750 + price: 444 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU492: + cost: 47 + init_stock: 4000 + price: 53 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU493: + cost: 414 + init_stock: 1350 + price: 461 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU494: + cost: 130 + init_stock: 4700 + price: 145 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU495: + cost: 134 + init_stock: 3100 + price: 149 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU496: + cost: 307 + init_stock: 2950 + price: 342 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU497: + cost: 29 + init_stock: 450 + price: 33 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU498: + cost: 274 + init_stock: 3250 + price: 305 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU499: + cost: 276 + init_stock: 1450 + price: 307 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU5: + cost: 419 + init_stock: 3550 + price: 466 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU50: + cost: 253 + init_stock: 1850 + price: 282 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU500: + cost: 187 + init_stock: 2100 + price: 208 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU501: + cost: 174 + init_stock: 3800 + price: 194 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU502: + cost: 62 + init_stock: 3250 + price: 69 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU503: + cost: 187 + init_stock: 2850 + price: 208 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU504: + cost: 225 + init_stock: 1500 + price: 251 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU505: + cost: 383 + init_stock: 2950 + price: 426 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU506: + cost: 134 + init_stock: 4650 + price: 149 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU507: + cost: 168 + init_stock: 750 + price: 187 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU508: + cost: 244 + init_stock: 4800 + price: 272 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU509: + cost: 267 + init_stock: 4050 + price: 297 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU51: + cost: 265 + init_stock: 3350 + price: 295 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU510: + cost: 84 + init_stock: 450 + price: 94 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU511: + cost: 324 + init_stock: 3750 + price: 361 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU512: + cost: 420 + init_stock: 1100 + price: 467 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU513: + cost: 308 + init_stock: 350 + price: 343 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU514: + cost: 167 + init_stock: 3150 + price: 186 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU515: + cost: 130 + init_stock: 2700 + price: 145 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU516: + cost: 57 + init_stock: 1900 + price: 64 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU517: + cost: 105 + init_stock: 450 + price: 117 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU518: + cost: 23 + init_stock: 1050 + price: 26 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU519: + cost: 209 + init_stock: 2500 + price: 233 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU52: + cost: 19 + init_stock: 3350 + price: 22 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU520: + cost: 188 + init_stock: 1100 + price: 209 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU521: + cost: 198 + init_stock: 2500 + price: 220 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU522: + cost: 140 + init_stock: 4350 + price: 156 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU523: + cost: 58 + init_stock: 5000 + price: 65 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU524: + cost: 264 + init_stock: 4700 + price: 294 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU525: + cost: 388 + init_stock: 2100 + price: 432 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU526: + cost: 377 + init_stock: 1200 + price: 419 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU527: + cost: 117 + init_stock: 3500 + price: 131 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU528: + cost: 284 + init_stock: 1050 + price: 316 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU529: + cost: 268 + init_stock: 1550 + price: 298 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU53: + cost: 135 + init_stock: 3500 + price: 151 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU530: + cost: 117 + init_stock: 2250 + price: 131 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU531: + cost: 92 + init_stock: 3000 + price: 103 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU532: + cost: 315 + init_stock: 3850 + price: 351 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU533: + cost: 266 + init_stock: 2100 + price: 296 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU534: + cost: 382 + init_stock: 2050 + price: 425 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU535: + cost: 273 + init_stock: 4300 + price: 304 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU536: + cost: 322 + init_stock: 2600 + price: 358 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU537: + cost: 288 + init_stock: 450 + price: 321 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU538: + cost: 411 + init_stock: 2500 + price: 457 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU539: + cost: 148 + init_stock: 3900 + price: 165 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU54: + cost: 142 + init_stock: 2300 + price: 158 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU540: + cost: 349 + init_stock: 2100 + price: 388 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU541: + cost: 117 + init_stock: 1450 + price: 131 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU542: + cost: 34 + init_stock: 1300 + price: 38 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU543: + cost: 387 + init_stock: 4250 + price: 430 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU544: + cost: 311 + init_stock: 4850 + price: 346 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU545: + cost: 157 + init_stock: 750 + price: 175 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU546: + cost: 441 + init_stock: 4800 + price: 491 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU547: + cost: 144 + init_stock: 2200 + price: 161 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU548: + cost: 99 + init_stock: 300 + price: 111 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU549: + cost: 348 + init_stock: 3950 + price: 387 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU55: + cost: 433 + init_stock: 3250 + price: 482 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU550: + cost: 233 + init_stock: 4700 + price: 259 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU551: + cost: 41 + init_stock: 1550 + price: 46 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU552: + cost: 171 + init_stock: 4500 + price: 191 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU553: + cost: 187 + init_stock: 1500 + price: 208 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU554: + cost: 306 + init_stock: 2050 + price: 340 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU555: + cost: 440 + init_stock: 4200 + price: 489 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU556: + cost: 99 + init_stock: 1500 + price: 110 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU557: + cost: 300 + init_stock: 3650 + price: 334 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU558: + cost: 359 + init_stock: 850 + price: 399 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU559: + cost: 281 + init_stock: 2700 + price: 313 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU56: + cost: 339 + init_stock: 3700 + price: 377 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU560: + cost: 254 + init_stock: 2050 + price: 283 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU561: + cost: 181 + init_stock: 1950 + price: 202 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU562: + cost: 312 + init_stock: 4450 + price: 347 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU563: + cost: 360 + init_stock: 250 + price: 401 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU564: + cost: 98 + init_stock: 450 + price: 109 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU565: + cost: 51 + init_stock: 3750 + price: 57 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU566: + cost: 117 + init_stock: 550 + price: 131 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU567: + cost: 324 + init_stock: 450 + price: 361 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU568: + cost: 67 + init_stock: 3900 + price: 75 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU569: + cost: 425 + init_stock: 2400 + price: 473 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU57: + cost: 323 + init_stock: 2500 + price: 359 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU570: + cost: 349 + init_stock: 4150 + price: 388 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU571: + cost: 151 + init_stock: 1950 + price: 168 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU572: + cost: 23 + init_stock: 2250 + price: 26 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU573: + cost: 221 + init_stock: 2050 + price: 246 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU574: + cost: 84 + init_stock: 2500 + price: 94 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU575: + cost: 213 + init_stock: 3950 + price: 237 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU576: + cost: 238 + init_stock: 3900 + price: 265 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU577: + cost: 16 + init_stock: 4350 + price: 18 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU578: + cost: 90 + init_stock: 3550 + price: 100 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU579: + cost: 373 + init_stock: 450 + price: 415 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU58: + cost: 325 + init_stock: 2400 + price: 362 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU580: + cost: 182 + init_stock: 250 + price: 203 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU581: + cost: 136 + init_stock: 4300 + price: 152 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU582: + cost: 323 + init_stock: 4800 + price: 359 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU583: + cost: 77 + init_stock: 4350 + price: 86 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU584: + cost: 29 + init_stock: 2250 + price: 33 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU585: + cost: 27 + init_stock: 1000 + price: 31 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU586: + cost: 54 + init_stock: 2200 + price: 61 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU587: + cost: 261 + init_stock: 3900 + price: 290 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU588: + cost: 428 + init_stock: 2200 + price: 476 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU589: + cost: 219 + init_stock: 1700 + price: 244 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU59: + cost: 130 + init_stock: 2550 + price: 145 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU590: + cost: 63 + init_stock: 1550 + price: 70 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU591: + cost: 185 + init_stock: 4200 + price: 206 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU592: + cost: 196 + init_stock: 2300 + price: 218 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU593: + cost: 111 + init_stock: 2200 + price: 124 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU594: + cost: 214 + init_stock: 2500 + price: 238 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU595: + cost: 65 + init_stock: 2150 + price: 73 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU596: + cost: 388 + init_stock: 350 + price: 432 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU597: + cost: 226 + init_stock: 4350 + price: 252 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU598: + cost: 313 + init_stock: 700 + price: 348 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU599: + cost: 48 + init_stock: 3400 + price: 54 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU6: + cost: 109 + init_stock: 4400 + price: 122 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU60: + cost: 180 + init_stock: 1950 + price: 200 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU600: + cost: 347 + init_stock: 3250 + price: 386 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU601: + cost: 124 + init_stock: 1950 + price: 138 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU602: + cost: 165 + init_stock: 4900 + price: 184 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU603: + cost: 250 + init_stock: 2100 + price: 278 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU604: + cost: 243 + init_stock: 2500 + price: 270 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU605: + cost: 259 + init_stock: 1200 + price: 288 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU606: + cost: 102 + init_stock: 550 + price: 114 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU607: + cost: 187 + init_stock: 3950 + price: 208 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU608: + cost: 35 + init_stock: 3650 + price: 39 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU609: + cost: 91 + init_stock: 950 + price: 102 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU61: + cost: 414 + init_stock: 3750 + price: 461 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU610: + cost: 404 + init_stock: 3400 + price: 449 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU611: + cost: 275 + init_stock: 3850 + price: 306 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU612: + cost: 351 + init_stock: 4400 + price: 391 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU613: + cost: 156 + init_stock: 350 + price: 174 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU614: + cost: 155 + init_stock: 750 + price: 173 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU615: + cost: 297 + init_stock: 4550 + price: 330 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU616: + cost: 208 + init_stock: 3650 + price: 232 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU617: + cost: 182 + init_stock: 3000 + price: 203 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU618: + cost: 69 + init_stock: 1150 + price: 77 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU619: + cost: 170 + init_stock: 4300 + price: 189 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU62: + cost: 111 + init_stock: 450 + price: 124 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU620: + cost: 207 + init_stock: 1950 + price: 231 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU621: + cost: 179 + init_stock: 3600 + price: 199 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU622: + cost: 227 + init_stock: 3250 + price: 253 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU623: + cost: 301 + init_stock: 2750 + price: 335 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU624: + cost: 433 + init_stock: 2700 + price: 482 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU625: + cost: 41 + init_stock: 4450 + price: 46 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU626: + cost: 405 + init_stock: 4450 + price: 451 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU627: + cost: 198 + init_stock: 4100 + price: 220 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU628: + cost: 111 + init_stock: 1300 + price: 124 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU629: + cost: 317 + init_stock: 4600 + price: 353 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU63: + cost: 61 + init_stock: 700 + price: 68 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU630: + cost: 109 + init_stock: 850 + price: 122 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU631: + cost: 266 + init_stock: 2450 + price: 296 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU632: + cost: 76 + init_stock: 4700 + price: 85 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU633: + cost: 165 + init_stock: 3350 + price: 184 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU634: + cost: 15 + init_stock: 3650 + price: 17 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU635: + cost: 306 + init_stock: 4650 + price: 341 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU636: + cost: 346 + init_stock: 1850 + price: 385 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU637: + cost: 74 + init_stock: 3050 + price: 83 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU638: + cost: 337 + init_stock: 400 + price: 375 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU639: + cost: 294 + init_stock: 2000 + price: 327 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU64: + cost: 32 + init_stock: 950 + price: 36 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU640: + cost: 247 + init_stock: 4550 + price: 275 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU641: + cost: 328 + init_stock: 4050 + price: 365 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU642: + cost: 372 + init_stock: 1250 + price: 414 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU643: + cost: 322 + init_stock: 2300 + price: 358 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU644: + cost: 41 + init_stock: 4100 + price: 46 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU645: + cost: 420 + init_stock: 4950 + price: 467 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU646: + cost: 170 + init_stock: 650 + price: 189 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU647: + cost: 272 + init_stock: 3850 + price: 303 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU648: + cost: 203 + init_stock: 2750 + price: 226 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU649: + cost: 178 + init_stock: 1250 + price: 198 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU65: + cost: 13 + init_stock: 4700 + price: 15 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU650: + cost: 315 + init_stock: 2800 + price: 351 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU651: + cost: 342 + init_stock: 4800 + price: 380 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU652: + cost: 110 + init_stock: 2000 + price: 123 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU653: + cost: 431 + init_stock: 4800 + price: 479 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU654: + cost: 59 + init_stock: 400 + price: 66 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU655: + cost: 299 + init_stock: 2100 + price: 333 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU656: + cost: 47 + init_stock: 3600 + price: 53 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU657: + cost: 65 + init_stock: 4050 + price: 73 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU658: + cost: 63 + init_stock: 1800 + price: 70 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU659: + cost: 18 + init_stock: 2800 + price: 21 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU66: + cost: 128 + init_stock: 4500 + price: 143 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU660: + cost: 438 + init_stock: 4150 + price: 487 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU661: + cost: 309 + init_stock: 3700 + price: 344 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU662: + cost: 334 + init_stock: 1900 + price: 372 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU663: + cost: 376 + init_stock: 2000 + price: 418 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU664: + cost: 315 + init_stock: 4200 + price: 351 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU665: + cost: 443 + init_stock: 2150 + price: 493 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU666: + cost: 306 + init_stock: 4400 + price: 341 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU667: + cost: 292 + init_stock: 650 + price: 325 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU668: + cost: 257 + init_stock: 3000 + price: 286 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU669: + cost: 66 + init_stock: 800 + price: 74 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU67: + cost: 222 + init_stock: 2650 + price: 247 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU670: + cost: 260 + init_stock: 4300 + price: 289 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU671: + cost: 240 + init_stock: 4150 + price: 267 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU672: + cost: 332 + init_stock: 650 + price: 369 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU673: + cost: 289 + init_stock: 2050 + price: 322 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU674: + cost: 200 + init_stock: 1100 + price: 223 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU675: + cost: 394 + init_stock: 1650 + price: 438 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU676: + cost: 219 + init_stock: 3050 + price: 244 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU677: + cost: 382 + init_stock: 2200 + price: 425 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU678: + cost: 151 + init_stock: 1800 + price: 168 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU679: + cost: 355 + init_stock: 2200 + price: 395 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU68: + cost: 152 + init_stock: 700 + price: 169 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU680: + cost: 234 + init_stock: 1500 + price: 260 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU681: + cost: 417 + init_stock: 4850 + price: 464 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU682: + cost: 333 + init_stock: 2650 + price: 370 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU683: + cost: 294 + init_stock: 3350 + price: 327 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU684: + cost: 319 + init_stock: 3950 + price: 355 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU685: + cost: 379 + init_stock: 2250 + price: 422 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU686: + cost: 56 + init_stock: 3150 + price: 63 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU687: + cost: 30 + init_stock: 3800 + price: 34 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU688: + cost: 435 + init_stock: 3850 + price: 484 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU689: + cost: 449 + init_stock: 750 + price: 499 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU69: + cost: 158 + init_stock: 3350 + price: 176 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU690: + cost: 393 + init_stock: 4150 + price: 437 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU691: + cost: 15 + init_stock: 3950 + price: 17 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU692: + cost: 202 + init_stock: 3250 + price: 225 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU693: + cost: 17 + init_stock: 3050 + price: 19 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU694: + cost: 187 + init_stock: 3750 + price: 208 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU695: + cost: 171 + init_stock: 350 + price: 190 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU696: + cost: 313 + init_stock: 2900 + price: 348 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU697: + cost: 409 + init_stock: 4000 + price: 455 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU698: + cost: 178 + init_stock: 3050 + price: 198 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU699: + cost: 27 + init_stock: 1000 + price: 31 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU7: + cost: 90 + init_stock: 4800 + price: 101 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU70: + cost: 167 + init_stock: 1750 + price: 186 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU700: + cost: 66 + init_stock: 450 + price: 74 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU701: + cost: 359 + init_stock: 3850 + price: 399 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU702: + cost: 73 + init_stock: 1700 + price: 82 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU703: + cost: 326 + init_stock: 1900 + price: 363 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU704: + cost: 345 + init_stock: 4350 + price: 384 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU705: + cost: 115 + init_stock: 3150 + price: 128 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU706: + cost: 163 + init_stock: 4550 + price: 182 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU707: + cost: 16 + init_stock: 3450 + price: 18 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU708: + cost: 160 + init_stock: 1400 + price: 178 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU709: + cost: 91 + init_stock: 2650 + price: 102 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU71: + cost: 283 + init_stock: 2250 + price: 315 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU710: + cost: 226 + init_stock: 950 + price: 252 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU711: + cost: 252 + init_stock: 3450 + price: 281 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU712: + cost: 208 + init_stock: 550 + price: 232 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU713: + cost: 256 + init_stock: 2150 + price: 285 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU714: + cost: 71 + init_stock: 2050 + price: 79 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU715: + cost: 210 + init_stock: 850 + price: 234 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU716: + cost: 63 + init_stock: 3750 + price: 70 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU717: + cost: 310 + init_stock: 400 + price: 345 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU718: + cost: 321 + init_stock: 2900 + price: 357 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU719: + cost: 306 + init_stock: 3250 + price: 340 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU72: + cost: 412 + init_stock: 4250 + price: 458 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU720: + cost: 292 + init_stock: 2100 + price: 325 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU721: + cost: 65 + init_stock: 550 + price: 73 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU722: + cost: 352 + init_stock: 4850 + price: 392 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU723: + cost: 286 + init_stock: 4450 + price: 318 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU724: + cost: 360 + init_stock: 1650 + price: 400 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU725: + cost: 157 + init_stock: 1850 + price: 175 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU726: + cost: 412 + init_stock: 4450 + price: 458 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU727: + cost: 376 + init_stock: 2850 + price: 418 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU728: + cost: 427 + init_stock: 1850 + price: 475 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU729: + cost: 291 + init_stock: 1800 + price: 324 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU73: + cost: 190 + init_stock: 2700 + price: 212 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU730: + cost: 14 + init_stock: 650 + price: 16 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU731: + cost: 79 + init_stock: 4100 + price: 88 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU732: + cost: 36 + init_stock: 3100 + price: 41 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU733: + cost: 283 + init_stock: 1300 + price: 315 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU734: + cost: 33 + init_stock: 2550 + price: 37 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU735: + cost: 239 + init_stock: 4950 + price: 266 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU736: + cost: 331 + init_stock: 1250 + price: 368 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU737: + cost: 427 + init_stock: 1550 + price: 475 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU738: + cost: 166 + init_stock: 2400 + price: 185 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU739: + cost: 427 + init_stock: 3700 + price: 475 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU74: + cost: 143 + init_stock: 2100 + price: 159 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU740: + cost: 351 + init_stock: 3200 + price: 390 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU741: + cost: 81 + init_stock: 2800 + price: 91 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU742: + cost: 169 + init_stock: 1100 + price: 188 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU743: + cost: 195 + init_stock: 2150 + price: 217 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU744: + cost: 341 + init_stock: 2900 + price: 379 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU745: + cost: 284 + init_stock: 4600 + price: 316 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU746: + cost: 393 + init_stock: 2000 + price: 437 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU747: + cost: 335 + init_stock: 3950 + price: 373 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU748: + cost: 247 + init_stock: 3300 + price: 275 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU749: + cost: 354 + init_stock: 2650 + price: 394 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU75: + cost: 117 + init_stock: 950 + price: 131 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU750: + cost: 230 + init_stock: 3100 + price: 256 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU751: + cost: 332 + init_stock: 3250 + price: 369 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU752: + cost: 233 + init_stock: 3900 + price: 259 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU753: + cost: 69 + init_stock: 3250 + price: 77 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU754: + cost: 348 + init_stock: 650 + price: 387 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU755: + cost: 318 + init_stock: 4000 + price: 354 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU756: + cost: 221 + init_stock: 4500 + price: 246 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU757: + cost: 36 + init_stock: 2850 + price: 40 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU758: + cost: 216 + init_stock: 2000 + price: 241 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU759: + cost: 391 + init_stock: 1900 + price: 435 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU76: + cost: 132 + init_stock: 3200 + price: 147 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU760: + cost: 158 + init_stock: 2550 + price: 176 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU761: + cost: 201 + init_stock: 2750 + price: 224 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU762: + cost: 237 + init_stock: 2550 + price: 264 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU763: + cost: 346 + init_stock: 3950 + price: 385 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU764: + cost: 314 + init_stock: 1700 + price: 349 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU765: + cost: 310 + init_stock: 650 + price: 345 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU766: + cost: 430 + init_stock: 1000 + price: 478 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU767: + cost: 85 + init_stock: 3800 + price: 95 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU768: + cost: 162 + init_stock: 4600 + price: 181 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU769: + cost: 21 + init_stock: 1450 + price: 24 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU77: + cost: 368 + init_stock: 3600 + price: 409 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU770: + cost: 135 + init_stock: 3100 + price: 150 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU771: + cost: 90 + init_stock: 350 + price: 101 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU772: + cost: 230 + init_stock: 300 + price: 256 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU773: + cost: 75 + init_stock: 4600 + price: 84 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU774: + cost: 402 + init_stock: 2950 + price: 447 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU775: + cost: 157 + init_stock: 4300 + price: 175 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU776: + cost: 92 + init_stock: 4250 + price: 103 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU777: + cost: 262 + init_stock: 2850 + price: 292 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU778: + cost: 182 + init_stock: 400 + price: 203 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU779: + cost: 105 + init_stock: 2150 + price: 117 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU78: + cost: 210 + init_stock: 650 + price: 234 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU780: + cost: 62 + init_stock: 4100 + price: 69 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU781: + cost: 334 + init_stock: 1800 + price: 372 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU782: + cost: 24 + init_stock: 500 + price: 27 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU783: + cost: 78 + init_stock: 4050 + price: 87 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU784: + cost: 278 + init_stock: 2600 + price: 309 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU785: + cost: 171 + init_stock: 3500 + price: 191 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU786: + cost: 81 + init_stock: 1100 + price: 91 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU787: + cost: 324 + init_stock: 3050 + price: 360 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU788: + cost: 315 + init_stock: 1400 + price: 351 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU789: + cost: 137 + init_stock: 2900 + price: 153 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU79: + cost: 220 + init_stock: 550 + price: 245 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU790: + cost: 375 + init_stock: 3300 + price: 417 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU791: + cost: 120 + init_stock: 1400 + price: 134 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU792: + cost: 281 + init_stock: 1050 + price: 313 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU793: + cost: 175 + init_stock: 3800 + price: 195 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU794: + cost: 292 + init_stock: 4000 + price: 325 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU795: + cost: 248 + init_stock: 2400 + price: 276 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU796: + cost: 402 + init_stock: 2450 + price: 447 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU797: + cost: 90 + init_stock: 1950 + price: 100 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU798: + cost: 383 + init_stock: 1500 + price: 426 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU799: + cost: 56 + init_stock: 350 + price: 63 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU8: + cost: 112 + init_stock: 3150 + price: 125 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU80: + cost: 146 + init_stock: 3500 + price: 163 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU800: + cost: 268 + init_stock: 4700 + price: 298 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU801: + cost: 350 + init_stock: 1800 + price: 389 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU802: + cost: 155 + init_stock: 3100 + price: 173 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU803: + cost: 244 + init_stock: 950 + price: 272 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU804: + cost: 351 + init_stock: 2350 + price: 390 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU805: + cost: 54 + init_stock: 1650 + price: 61 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU806: + cost: 142 + init_stock: 2600 + price: 158 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU807: + cost: 407 + init_stock: 1300 + price: 453 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU808: + cost: 224 + init_stock: 4550 + price: 249 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU809: + cost: 49 + init_stock: 3250 + price: 55 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU81: + cost: 163 + init_stock: 3300 + price: 182 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU810: + cost: 248 + init_stock: 300 + price: 276 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU811: + cost: 228 + init_stock: 1850 + price: 254 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU812: + cost: 226 + init_stock: 4000 + price: 252 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU813: + cost: 227 + init_stock: 350 + price: 253 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU814: + cost: 436 + init_stock: 4850 + price: 485 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU815: + cost: 351 + init_stock: 1200 + price: 390 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU816: + cost: 136 + init_stock: 4550 + price: 152 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU817: + cost: 204 + init_stock: 250 + price: 227 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU818: + cost: 318 + init_stock: 2150 + price: 354 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU819: + cost: 271 + init_stock: 1350 + price: 302 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU82: + cost: 261 + init_stock: 1400 + price: 290 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU820: + cost: 237 + init_stock: 4100 + price: 264 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU821: + cost: 89 + init_stock: 3450 + price: 99 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU822: + cost: 122 + init_stock: 3500 + price: 136 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU823: + cost: 67 + init_stock: 1400 + price: 75 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU824: + cost: 153 + init_stock: 3500 + price: 170 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU825: + cost: 192 + init_stock: 750 + price: 214 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU826: + cost: 347 + init_stock: 2750 + price: 386 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU827: + cost: 133 + init_stock: 3200 + price: 148 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU828: + cost: 360 + init_stock: 3450 + price: 400 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU829: + cost: 54 + init_stock: 3700 + price: 61 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU83: + cost: 266 + init_stock: 850 + price: 296 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU830: + cost: 150 + init_stock: 1200 + price: 167 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU831: + cost: 235 + init_stock: 1450 + price: 262 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU832: + cost: 29 + init_stock: 4100 + price: 33 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU833: + cost: 360 + init_stock: 4900 + price: 400 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU834: + cost: 379 + init_stock: 250 + price: 422 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU835: + cost: 396 + init_stock: 2600 + price: 440 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU836: + cost: 290 + init_stock: 4800 + price: 323 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU837: + cost: 335 + init_stock: 1300 + price: 373 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU838: + cost: 410 + init_stock: 3850 + price: 456 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU839: + cost: 425 + init_stock: 3000 + price: 473 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU84: + cost: 286 + init_stock: 2100 + price: 318 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU840: + cost: 239 + init_stock: 2500 + price: 266 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU841: + cost: 256 + init_stock: 4250 + price: 285 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU842: + cost: 36 + init_stock: 4200 + price: 41 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU843: + cost: 324 + init_stock: 3600 + price: 360 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU844: + cost: 45 + init_stock: 3950 + price: 51 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU845: + cost: 259 + init_stock: 1100 + price: 288 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU846: + cost: 436 + init_stock: 1950 + price: 485 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU847: + cost: 349 + init_stock: 4550 + price: 388 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU848: + cost: 275 + init_stock: 2300 + price: 306 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU849: + cost: 288 + init_stock: 2000 + price: 320 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU85: + cost: 397 + init_stock: 5000 + price: 442 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU850: + cost: 164 + init_stock: 2850 + price: 183 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU851: + cost: 47 + init_stock: 3200 + price: 53 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU852: + cost: 275 + init_stock: 2700 + price: 306 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU853: + cost: 347 + init_stock: 2800 + price: 386 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU854: + cost: 190 + init_stock: 3250 + price: 212 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU855: + cost: 68 + init_stock: 3600 + price: 76 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU856: + cost: 342 + init_stock: 1600 + price: 380 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU857: + cost: 415 + init_stock: 5000 + price: 462 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU858: + cost: 72 + init_stock: 1200 + price: 80 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU859: + cost: 193 + init_stock: 1400 + price: 215 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU86: + cost: 347 + init_stock: 4250 + price: 386 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU860: + cost: 281 + init_stock: 750 + price: 313 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU861: + cost: 429 + init_stock: 650 + price: 477 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU862: + cost: 216 + init_stock: 800 + price: 240 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU863: + cost: 423 + init_stock: 4650 + price: 470 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU864: + cost: 182 + init_stock: 950 + price: 203 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU865: + cost: 129 + init_stock: 950 + price: 144 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU866: + cost: 154 + init_stock: 4400 + price: 172 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU867: + cost: 449 + init_stock: 5000 + price: 499 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU868: + cost: 36 + init_stock: 2500 + price: 41 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU869: + cost: 87 + init_stock: 4850 + price: 97 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU87: + cost: 331 + init_stock: 3450 + price: 368 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU870: + cost: 252 + init_stock: 3350 + price: 281 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU871: + cost: 90 + init_stock: 4950 + price: 101 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU872: + cost: 119 + init_stock: 1600 + price: 133 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU873: + cost: 380 + init_stock: 1550 + price: 423 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU874: + cost: 422 + init_stock: 3000 + price: 469 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU875: + cost: 45 + init_stock: 1150 + price: 51 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU876: + cost: 272 + init_stock: 3050 + price: 303 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU877: + cost: 36 + init_stock: 1750 + price: 40 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU878: + cost: 24 + init_stock: 3400 + price: 27 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU879: + cost: 135 + init_stock: 5000 + price: 150 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU88: + cost: 423 + init_stock: 600 + price: 471 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU880: + cost: 389 + init_stock: 1550 + price: 433 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU881: + cost: 387 + init_stock: 3500 + price: 431 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU882: + cost: 174 + init_stock: 800 + price: 194 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU883: + cost: 255 + init_stock: 1900 + price: 284 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU884: + cost: 125 + init_stock: 1950 + price: 139 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU885: + cost: 44 + init_stock: 1350 + price: 49 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU886: + cost: 334 + init_stock: 3650 + price: 372 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU887: + cost: 239 + init_stock: 4350 + price: 266 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU888: + cost: 128 + init_stock: 450 + price: 143 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU889: + cost: 90 + init_stock: 1050 + price: 101 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU89: + cost: 124 + init_stock: 4650 + price: 138 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU890: + cost: 144 + init_stock: 5000 + price: 161 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU891: + cost: 320 + init_stock: 1100 + price: 356 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU892: + cost: 281 + init_stock: 4600 + price: 313 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU893: + cost: 206 + init_stock: 4950 + price: 229 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU894: + cost: 116 + init_stock: 3700 + price: 129 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU895: + cost: 207 + init_stock: 650 + price: 230 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU896: + cost: 260 + init_stock: 4000 + price: 289 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU897: + cost: 353 + init_stock: 3600 + price: 393 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU898: + cost: 429 + init_stock: 550 + price: 477 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU899: + cost: 209 + init_stock: 2400 + price: 233 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU9: + cost: 149 + init_stock: 4800 + price: 166 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU90: + cost: 408 + init_stock: 2550 + price: 454 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU900: + cost: 142 + init_stock: 2750 + price: 158 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU901: + cost: 193 + init_stock: 1450 + price: 215 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU902: + cost: 112 + init_stock: 4000 + price: 125 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU903: + cost: 321 + init_stock: 1900 + price: 357 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU904: + cost: 446 + init_stock: 2550 + price: 496 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU905: + cost: 224 + init_stock: 1550 + price: 249 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU906: + cost: 149 + init_stock: 2600 + price: 166 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU907: + cost: 19 + init_stock: 4150 + price: 22 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU908: + cost: 367 + init_stock: 3900 + price: 408 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU909: + cost: 433 + init_stock: 4800 + price: 482 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU91: + cost: 272 + init_stock: 800 + price: 303 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU910: + cost: 203 + init_stock: 1650 + price: 226 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU911: + cost: 414 + init_stock: 3500 + price: 461 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU912: + cost: 212 + init_stock: 1350 + price: 236 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU913: + cost: 289 + init_stock: 2300 + price: 322 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU914: + cost: 244 + init_stock: 3100 + price: 272 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU915: + cost: 303 + init_stock: 2800 + price: 337 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU916: + cost: 303 + init_stock: 4450 + price: 337 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU917: + cost: 232 + init_stock: 1500 + price: 258 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU918: + cost: 133 + init_stock: 2750 + price: 148 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU919: + cost: 353 + init_stock: 1250 + price: 393 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU92: + cost: 235 + init_stock: 3150 + price: 262 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU920: + cost: 321 + init_stock: 4300 + price: 357 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU921: + cost: 204 + init_stock: 3300 + price: 227 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU922: + cost: 100 + init_stock: 3350 + price: 112 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU923: + cost: 446 + init_stock: 2900 + price: 496 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU924: + cost: 284 + init_stock: 4250 + price: 316 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU925: + cost: 324 + init_stock: 750 + price: 360 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU926: + cost: 324 + init_stock: 3350 + price: 360 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU927: + cost: 234 + init_stock: 1050 + price: 260 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU928: + cost: 441 + init_stock: 4150 + price: 491 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU929: + cost: 323 + init_stock: 5000 + price: 359 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU93: + cost: 363 + init_stock: 3350 + price: 404 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU930: + cost: 178 + init_stock: 1400 + price: 198 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU931: + cost: 63 + init_stock: 700 + price: 71 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU932: + cost: 146 + init_stock: 3300 + price: 163 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU933: + cost: 101 + init_stock: 3900 + price: 113 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU934: + cost: 197 + init_stock: 3350 + price: 219 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU935: + cost: 327 + init_stock: 4700 + price: 364 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU936: + cost: 21 + init_stock: 250 + price: 24 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU937: + cost: 121 + init_stock: 850 + price: 135 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU938: + cost: 388 + init_stock: 1050 + price: 432 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU939: + cost: 155 + init_stock: 2950 + price: 173 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU94: + cost: 165 + init_stock: 2350 + price: 184 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU940: + cost: 12 + init_stock: 4650 + price: 14 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU941: + cost: 72 + init_stock: 2850 + price: 80 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU942: + cost: 181 + init_stock: 650 + price: 202 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU943: + cost: 124 + init_stock: 2450 + price: 138 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU944: + cost: 176 + init_stock: 2200 + price: 196 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU945: + cost: 126 + init_stock: 850 + price: 141 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU946: + cost: 292 + init_stock: 2450 + price: 325 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU947: + cost: 304 + init_stock: 4550 + price: 338 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU948: + cost: 382 + init_stock: 1400 + price: 425 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU949: + cost: 278 + init_stock: 250 + price: 309 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU95: + cost: 122 + init_stock: 1050 + price: 136 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU950: + cost: 91 + init_stock: 2700 + price: 102 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU951: + cost: 67 + init_stock: 900 + price: 75 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU952: + cost: 140 + init_stock: 550 + price: 156 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU953: + cost: 124 + init_stock: 1750 + price: 138 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU954: + cost: 266 + init_stock: 4300 + price: 296 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU955: + cost: 49 + init_stock: 3150 + price: 55 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU956: + cost: 253 + init_stock: 4250 + price: 282 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU957: + cost: 274 + init_stock: 2550 + price: 305 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU958: + cost: 332 + init_stock: 3300 + price: 369 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU959: + cost: 72 + init_stock: 4100 + price: 81 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU96: + cost: 158 + init_stock: 2200 + price: 176 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU960: + cost: 132 + init_stock: 300 + price: 147 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU961: + cost: 237 + init_stock: 2200 + price: 264 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU962: + cost: 318 + init_stock: 2200 + price: 354 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU963: + cost: 314 + init_stock: 1050 + price: 349 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU964: + cost: 219 + init_stock: 3650 + price: 244 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU965: + cost: 111 + init_stock: 2550 + price: 124 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU966: + cost: 271 + init_stock: 2200 + price: 302 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU967: + cost: 60 + init_stock: 2250 + price: 67 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU968: + cost: 252 + init_stock: 2450 + price: 281 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU969: + cost: 224 + init_stock: 300 + price: 249 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU97: + cost: 25 + init_stock: 1500 + price: 28 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU970: + cost: 219 + init_stock: 250 + price: 244 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU971: + cost: 331 + init_stock: 3650 + price: 368 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU972: + cost: 188 + init_stock: 3450 + price: 209 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU973: + cost: 243 + init_stock: 550 + price: 271 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU974: + cost: 153 + init_stock: 1600 + price: 170 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU975: + cost: 178 + init_stock: 1100 + price: 198 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU976: + cost: 160 + init_stock: 750 + price: 178 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU977: + cost: 210 + init_stock: 2700 + price: 234 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU978: + cost: 423 + init_stock: 3200 + price: 470 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU979: + cost: 398 + init_stock: 4800 + price: 443 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU98: + cost: 398 + init_stock: 1750 + price: 443 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU980: + cost: 35 + init_stock: 3400 + price: 39 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU981: + cost: 433 + init_stock: 3600 + price: 482 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU982: + cost: 191 + init_stock: 2600 + price: 213 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU983: + cost: 404 + init_stock: 4600 + price: 449 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU984: + cost: 208 + init_stock: 4550 + price: 232 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU985: + cost: 261 + init_stock: 2550 + price: 290 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU986: + cost: 247 + init_stock: 850 + price: 275 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU987: + cost: 390 + init_stock: 1700 + price: 434 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU988: + cost: 91 + init_stock: 1150 + price: 102 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU989: + cost: 435 + init_stock: 4650 + price: 484 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU99: + cost: 324 + init_stock: 2250 + price: 361 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU990: + cost: 97 + init_stock: 2850 + price: 108 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU991: + cost: 368 + init_stock: 950 + price: 409 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU992: + cost: 390 + init_stock: 4650 + price: 434 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU993: + cost: 130 + init_stock: 4300 + price: 145 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU994: + cost: 423 + init_stock: 2500 + price: 470 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU995: + cost: 216 + init_stock: 3800 + price: 241 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU996: + cost: 234 + init_stock: 3650 + price: 260 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU997: + cost: 360 + init_stock: 1700 + price: 400 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU998: + cost: 402 + init_stock: 2750 + price: 447 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU999: + cost: 71 + init_stock: 1150 + price: 79 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5324500 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 500 + definition_ref: WarehouseFacility + name: WAREHOUSE0 + skus: + SKU0: + cost: 322 + init_stock: 1260 + price: 322 + service_level: 0.96 + vlt: 1 + SKU1: + cost: 284 + init_stock: 440 + price: 284 + service_level: 0.96 + vlt: 3 + SKU10: + cost: 68 + init_stock: 500 + price: 68 + service_level: 0.96 + vlt: 3 + SKU100: + cost: 16 + init_stock: 1300 + price: 16 + service_level: 0.96 + vlt: 3 + SKU101: + cost: 84 + init_stock: 1420 + price: 84 + service_level: 0.96 + vlt: 3 + SKU102: + cost: 328 + init_stock: 1860 + price: 328 + service_level: 0.96 + vlt: 2 + SKU103: + cost: 334 + init_stock: 1900 + price: 334 + service_level: 0.96 + vlt: 1 + SKU104: + cost: 49 + init_stock: 1300 + price: 49 + service_level: 0.96 + vlt: 3 + SKU105: + cost: 110 + init_stock: 1140 + price: 110 + service_level: 0.96 + vlt: 2 + SKU106: + cost: 251 + init_stock: 1460 + price: 251 + service_level: 0.96 + vlt: 3 + SKU107: + cost: 423 + init_stock: 1740 + price: 423 + service_level: 0.96 + vlt: 3 + SKU108: + cost: 458 + init_stock: 1840 + price: 458 + service_level: 0.96 + vlt: 3 + SKU109: + cost: 88 + init_stock: 1640 + price: 88 + service_level: 0.96 + vlt: 2 + SKU11: + cost: 400 + init_stock: 1020 + price: 400 + service_level: 0.96 + vlt: 2 + SKU110: + cost: 66 + init_stock: 280 + price: 66 + service_level: 0.96 + vlt: 3 + SKU111: + cost: 260 + init_stock: 1220 + price: 260 + service_level: 0.96 + vlt: 1 + SKU112: + cost: 61 + init_stock: 1900 + price: 61 + service_level: 0.96 + vlt: 2 + SKU113: + cost: 348 + init_stock: 620 + price: 348 + service_level: 0.96 + vlt: 3 + SKU114: + cost: 389 + init_stock: 540 + price: 389 + service_level: 0.96 + vlt: 1 + SKU115: + cost: 286 + init_stock: 1720 + price: 286 + service_level: 0.96 + vlt: 2 + SKU116: + cost: 496 + init_stock: 1440 + price: 496 + service_level: 0.96 + vlt: 3 + SKU117: + cost: 320 + init_stock: 1840 + price: 320 + service_level: 0.96 + vlt: 3 + SKU118: + cost: 183 + init_stock: 660 + price: 183 + service_level: 0.96 + vlt: 1 + SKU119: + cost: 209 + init_stock: 640 + price: 209 + service_level: 0.96 + vlt: 3 + SKU12: + cost: 112 + init_stock: 1680 + price: 112 + service_level: 0.96 + vlt: 1 + SKU120: + cost: 121 + init_stock: 1960 + price: 121 + service_level: 0.96 + vlt: 1 + SKU121: + cost: 40 + init_stock: 1700 + price: 40 + service_level: 0.96 + vlt: 1 + SKU122: + cost: 437 + init_stock: 140 + price: 437 + service_level: 0.96 + vlt: 3 + SKU123: + cost: 233 + init_stock: 380 + price: 233 + service_level: 0.96 + vlt: 3 + SKU124: + cost: 182 + init_stock: 720 + price: 182 + service_level: 0.96 + vlt: 2 + SKU125: + cost: 16 + init_stock: 1840 + price: 16 + service_level: 0.96 + vlt: 2 + SKU126: + cost: 36 + init_stock: 780 + price: 36 + service_level: 0.96 + vlt: 3 + SKU127: + cost: 217 + init_stock: 620 + price: 217 + service_level: 0.96 + vlt: 2 + SKU128: + cost: 165 + init_stock: 380 + price: 165 + service_level: 0.96 + vlt: 1 + SKU129: + cost: 143 + init_stock: 1000 + price: 143 + service_level: 0.96 + vlt: 2 + SKU13: + cost: 317 + init_stock: 1140 + price: 317 + service_level: 0.96 + vlt: 3 + SKU130: + cost: 348 + init_stock: 1960 + price: 348 + service_level: 0.96 + vlt: 3 + SKU131: + cost: 64 + init_stock: 900 + price: 64 + service_level: 0.96 + vlt: 1 + SKU132: + cost: 427 + init_stock: 420 + price: 427 + service_level: 0.96 + vlt: 2 + SKU133: + cost: 224 + init_stock: 580 + price: 224 + service_level: 0.96 + vlt: 2 + SKU134: + cost: 336 + init_stock: 1540 + price: 336 + service_level: 0.96 + vlt: 3 + SKU135: + cost: 153 + init_stock: 2000 + price: 153 + service_level: 0.96 + vlt: 1 + SKU136: + cost: 199 + init_stock: 1420 + price: 199 + service_level: 0.96 + vlt: 3 + SKU137: + cost: 93 + init_stock: 1480 + price: 93 + service_level: 0.96 + vlt: 2 + SKU138: + cost: 228 + init_stock: 720 + price: 228 + service_level: 0.96 + vlt: 1 + SKU139: + cost: 207 + init_stock: 480 + price: 207 + service_level: 0.96 + vlt: 1 + SKU14: + cost: 268 + init_stock: 1240 + price: 268 + service_level: 0.96 + vlt: 1 + SKU140: + cost: 261 + init_stock: 680 + price: 261 + service_level: 0.96 + vlt: 3 + SKU141: + cost: 190 + init_stock: 820 + price: 190 + service_level: 0.96 + vlt: 1 + SKU142: + cost: 320 + init_stock: 760 + price: 320 + service_level: 0.96 + vlt: 3 + SKU143: + cost: 318 + init_stock: 520 + price: 318 + service_level: 0.96 + vlt: 3 + SKU144: + cost: 400 + init_stock: 240 + price: 400 + service_level: 0.96 + vlt: 1 + SKU145: + cost: 399 + init_stock: 1640 + price: 399 + service_level: 0.96 + vlt: 1 + SKU146: + cost: 177 + init_stock: 960 + price: 177 + service_level: 0.96 + vlt: 3 + SKU147: + cost: 472 + init_stock: 1120 + price: 472 + service_level: 0.96 + vlt: 3 + SKU148: + cost: 313 + init_stock: 1540 + price: 313 + service_level: 0.96 + vlt: 3 + SKU149: + cost: 357 + init_stock: 1540 + price: 357 + service_level: 0.96 + vlt: 3 + SKU15: + cost: 52 + init_stock: 100 + price: 52 + service_level: 0.96 + vlt: 2 + SKU150: + cost: 106 + init_stock: 1400 + price: 106 + service_level: 0.96 + vlt: 2 + SKU151: + cost: 223 + init_stock: 1460 + price: 223 + service_level: 0.96 + vlt: 1 + SKU152: + cost: 10 + init_stock: 1020 + price: 10 + service_level: 0.96 + vlt: 3 + SKU153: + cost: 441 + init_stock: 1240 + price: 441 + service_level: 0.96 + vlt: 1 + SKU154: + cost: 77 + init_stock: 1700 + price: 77 + service_level: 0.96 + vlt: 3 + SKU155: + cost: 422 + init_stock: 1060 + price: 422 + service_level: 0.96 + vlt: 1 + SKU156: + cost: 10 + init_stock: 240 + price: 10 + service_level: 0.96 + vlt: 1 + SKU157: + cost: 410 + init_stock: 1500 + price: 410 + service_level: 0.96 + vlt: 2 + SKU158: + cost: 145 + init_stock: 1620 + price: 145 + service_level: 0.96 + vlt: 3 + SKU159: + cost: 193 + init_stock: 500 + price: 193 + service_level: 0.96 + vlt: 2 + SKU16: + cost: 175 + init_stock: 1160 + price: 175 + service_level: 0.96 + vlt: 3 + SKU160: + cost: 459 + init_stock: 1700 + price: 459 + service_level: 0.96 + vlt: 1 + SKU161: + cost: 239 + init_stock: 920 + price: 239 + service_level: 0.96 + vlt: 2 + SKU162: + cost: 158 + init_stock: 100 + price: 158 + service_level: 0.96 + vlt: 2 + SKU163: + cost: 486 + init_stock: 780 + price: 486 + service_level: 0.96 + vlt: 2 + SKU164: + cost: 496 + init_stock: 2000 + price: 496 + service_level: 0.96 + vlt: 1 + SKU165: + cost: 274 + init_stock: 660 + price: 274 + service_level: 0.96 + vlt: 3 + SKU166: + cost: 79 + init_stock: 1780 + price: 79 + service_level: 0.96 + vlt: 1 + SKU167: + cost: 443 + init_stock: 260 + price: 443 + service_level: 0.96 + vlt: 1 + SKU168: + cost: 357 + init_stock: 1740 + price: 357 + service_level: 0.96 + vlt: 2 + SKU169: + cost: 369 + init_stock: 1960 + price: 369 + service_level: 0.96 + vlt: 3 + SKU17: + cost: 346 + init_stock: 180 + price: 346 + service_level: 0.96 + vlt: 1 + SKU170: + cost: 68 + init_stock: 1100 + price: 68 + service_level: 0.96 + vlt: 2 + SKU171: + cost: 398 + init_stock: 1520 + price: 398 + service_level: 0.96 + vlt: 1 + SKU172: + cost: 200 + init_stock: 1420 + price: 200 + service_level: 0.96 + vlt: 2 + SKU173: + cost: 175 + init_stock: 1920 + price: 175 + service_level: 0.96 + vlt: 3 + SKU174: + cost: 291 + init_stock: 1520 + price: 291 + service_level: 0.96 + vlt: 2 + SKU175: + cost: 84 + init_stock: 1500 + price: 84 + service_level: 0.96 + vlt: 2 + SKU176: + cost: 407 + init_stock: 1320 + price: 407 + service_level: 0.96 + vlt: 1 + SKU177: + cost: 257 + init_stock: 620 + price: 257 + service_level: 0.96 + vlt: 1 + SKU178: + cost: 423 + init_stock: 100 + price: 423 + service_level: 0.96 + vlt: 2 + SKU179: + cost: 497 + init_stock: 1660 + price: 497 + service_level: 0.96 + vlt: 1 + SKU18: + cost: 258 + init_stock: 1620 + price: 258 + service_level: 0.96 + vlt: 1 + SKU180: + cost: 217 + init_stock: 1100 + price: 217 + service_level: 0.96 + vlt: 2 + SKU181: + cost: 143 + init_stock: 1200 + price: 143 + service_level: 0.96 + vlt: 1 + SKU182: + cost: 437 + init_stock: 1980 + price: 437 + service_level: 0.96 + vlt: 3 + SKU183: + cost: 145 + init_stock: 160 + price: 145 + service_level: 0.96 + vlt: 3 + SKU184: + cost: 73 + init_stock: 480 + price: 73 + service_level: 0.96 + vlt: 2 + SKU185: + cost: 10 + init_stock: 1800 + price: 10 + service_level: 0.96 + vlt: 2 + SKU186: + cost: 359 + init_stock: 440 + price: 359 + service_level: 0.96 + vlt: 1 + SKU187: + cost: 177 + init_stock: 600 + price: 177 + service_level: 0.96 + vlt: 3 + SKU188: + cost: 391 + init_stock: 1740 + price: 391 + service_level: 0.96 + vlt: 3 + SKU189: + cost: 358 + init_stock: 700 + price: 358 + service_level: 0.96 + vlt: 2 + SKU19: + cost: 477 + init_stock: 1820 + price: 477 + service_level: 0.96 + vlt: 3 + SKU190: + cost: 113 + init_stock: 340 + price: 113 + service_level: 0.96 + vlt: 3 + SKU191: + cost: 473 + init_stock: 1080 + price: 473 + service_level: 0.96 + vlt: 2 + SKU192: + cost: 415 + init_stock: 1220 + price: 415 + service_level: 0.96 + vlt: 2 + SKU193: + cost: 207 + init_stock: 600 + price: 207 + service_level: 0.96 + vlt: 2 + SKU194: + cost: 432 + init_stock: 100 + price: 432 + service_level: 0.96 + vlt: 2 + SKU195: + cost: 218 + init_stock: 620 + price: 218 + service_level: 0.96 + vlt: 2 + SKU196: + cost: 49 + init_stock: 1360 + price: 49 + service_level: 0.96 + vlt: 3 + SKU197: + cost: 303 + init_stock: 1140 + price: 303 + service_level: 0.96 + vlt: 1 + SKU198: + cost: 169 + init_stock: 1080 + price: 169 + service_level: 0.96 + vlt: 2 + SKU199: + cost: 449 + init_stock: 460 + price: 449 + service_level: 0.96 + vlt: 1 + SKU2: + cost: 331 + init_stock: 1400 + price: 331 + service_level: 0.96 + vlt: 3 + SKU20: + cost: 335 + init_stock: 500 + price: 335 + service_level: 0.96 + vlt: 3 + SKU200: + cost: 65 + init_stock: 500 + price: 65 + service_level: 0.96 + vlt: 1 + SKU201: + cost: 104 + init_stock: 1180 + price: 104 + service_level: 0.96 + vlt: 1 + SKU202: + cost: 142 + init_stock: 1460 + price: 142 + service_level: 0.96 + vlt: 1 + SKU203: + cost: 440 + init_stock: 1640 + price: 440 + service_level: 0.96 + vlt: 2 + SKU204: + cost: 489 + init_stock: 940 + price: 489 + service_level: 0.96 + vlt: 2 + SKU205: + cost: 130 + init_stock: 2000 + price: 130 + service_level: 0.96 + vlt: 3 + SKU206: + cost: 335 + init_stock: 220 + price: 335 + service_level: 0.96 + vlt: 2 + SKU207: + cost: 140 + init_stock: 1600 + price: 140 + service_level: 0.96 + vlt: 1 + SKU208: + cost: 491 + init_stock: 1540 + price: 491 + service_level: 0.96 + vlt: 1 + SKU209: + cost: 179 + init_stock: 400 + price: 179 + service_level: 0.96 + vlt: 3 + SKU21: + cost: 123 + init_stock: 2000 + price: 123 + service_level: 0.96 + vlt: 2 + SKU210: + cost: 404 + init_stock: 1380 + price: 404 + service_level: 0.96 + vlt: 3 + SKU211: + cost: 174 + init_stock: 1820 + price: 174 + service_level: 0.96 + vlt: 2 + SKU212: + cost: 405 + init_stock: 1580 + price: 405 + service_level: 0.96 + vlt: 3 + SKU213: + cost: 121 + init_stock: 1280 + price: 121 + service_level: 0.96 + vlt: 2 + SKU214: + cost: 101 + init_stock: 200 + price: 101 + service_level: 0.96 + vlt: 2 + SKU215: + cost: 419 + init_stock: 940 + price: 419 + service_level: 0.96 + vlt: 1 + SKU216: + cost: 330 + init_stock: 460 + price: 330 + service_level: 0.96 + vlt: 1 + SKU217: + cost: 284 + init_stock: 1300 + price: 284 + service_level: 0.96 + vlt: 2 + SKU218: + cost: 205 + init_stock: 1180 + price: 205 + service_level: 0.96 + vlt: 1 + SKU219: + cost: 92 + init_stock: 920 + price: 92 + service_level: 0.96 + vlt: 3 + SKU22: + cost: 493 + init_stock: 1320 + price: 493 + service_level: 0.96 + vlt: 1 + SKU220: + cost: 387 + init_stock: 1740 + price: 387 + service_level: 0.96 + vlt: 2 + SKU221: + cost: 39 + init_stock: 1560 + price: 39 + service_level: 0.96 + vlt: 2 + SKU222: + cost: 115 + init_stock: 720 + price: 115 + service_level: 0.96 + vlt: 2 + SKU223: + cost: 196 + init_stock: 240 + price: 196 + service_level: 0.96 + vlt: 2 + SKU224: + cost: 387 + init_stock: 100 + price: 387 + service_level: 0.96 + vlt: 2 + SKU225: + cost: 164 + init_stock: 580 + price: 164 + service_level: 0.96 + vlt: 1 + SKU226: + cost: 206 + init_stock: 260 + price: 206 + service_level: 0.96 + vlt: 2 + SKU227: + cost: 479 + init_stock: 480 + price: 479 + service_level: 0.96 + vlt: 3 + SKU228: + cost: 14 + init_stock: 1800 + price: 14 + service_level: 0.96 + vlt: 1 + SKU229: + cost: 472 + init_stock: 880 + price: 472 + service_level: 0.96 + vlt: 1 + SKU23: + cost: 387 + init_stock: 840 + price: 387 + service_level: 0.96 + vlt: 1 + SKU230: + cost: 241 + init_stock: 460 + price: 241 + service_level: 0.96 + vlt: 3 + SKU231: + cost: 48 + init_stock: 1620 + price: 48 + service_level: 0.96 + vlt: 3 + SKU232: + cost: 224 + init_stock: 1920 + price: 224 + service_level: 0.96 + vlt: 1 + SKU233: + cost: 360 + init_stock: 1500 + price: 360 + service_level: 0.96 + vlt: 2 + SKU234: + cost: 287 + init_stock: 100 + price: 287 + service_level: 0.96 + vlt: 1 + SKU235: + cost: 24 + init_stock: 1140 + price: 24 + service_level: 0.96 + vlt: 3 + SKU236: + cost: 155 + init_stock: 1100 + price: 155 + service_level: 0.96 + vlt: 2 + SKU237: + cost: 433 + init_stock: 900 + price: 433 + service_level: 0.96 + vlt: 3 + SKU238: + cost: 64 + init_stock: 1320 + price: 64 + service_level: 0.96 + vlt: 3 + SKU239: + cost: 103 + init_stock: 1960 + price: 103 + service_level: 0.96 + vlt: 1 + SKU24: + cost: 97 + init_stock: 940 + price: 97 + service_level: 0.96 + vlt: 2 + SKU240: + cost: 373 + init_stock: 940 + price: 373 + service_level: 0.96 + vlt: 2 + SKU241: + cost: 439 + init_stock: 1420 + price: 439 + service_level: 0.96 + vlt: 2 + SKU242: + cost: 17 + init_stock: 880 + price: 17 + service_level: 0.96 + vlt: 1 + SKU243: + cost: 352 + init_stock: 280 + price: 352 + service_level: 0.96 + vlt: 1 + SKU244: + cost: 174 + init_stock: 1640 + price: 174 + service_level: 0.96 + vlt: 2 + SKU245: + cost: 404 + init_stock: 1320 + price: 404 + service_level: 0.96 + vlt: 2 + SKU246: + cost: 300 + init_stock: 600 + price: 300 + service_level: 0.96 + vlt: 2 + SKU247: + cost: 386 + init_stock: 700 + price: 386 + service_level: 0.96 + vlt: 2 + SKU248: + cost: 355 + init_stock: 580 + price: 355 + service_level: 0.96 + vlt: 3 + SKU249: + cost: 356 + init_stock: 500 + price: 356 + service_level: 0.96 + vlt: 1 + SKU25: + cost: 161 + init_stock: 100 + price: 161 + service_level: 0.96 + vlt: 2 + SKU250: + cost: 127 + init_stock: 1080 + price: 127 + service_level: 0.96 + vlt: 3 + SKU251: + cost: 344 + init_stock: 1220 + price: 344 + service_level: 0.96 + vlt: 1 + SKU252: + cost: 181 + init_stock: 1660 + price: 181 + service_level: 0.96 + vlt: 1 + SKU253: + cost: 448 + init_stock: 320 + price: 448 + service_level: 0.96 + vlt: 1 + SKU254: + cost: 484 + init_stock: 920 + price: 484 + service_level: 0.96 + vlt: 3 + SKU255: + cost: 290 + init_stock: 1340 + price: 290 + service_level: 0.96 + vlt: 2 + SKU256: + cost: 91 + init_stock: 1440 + price: 91 + service_level: 0.96 + vlt: 3 + SKU257: + cost: 348 + init_stock: 1140 + price: 348 + service_level: 0.96 + vlt: 1 + SKU258: + cost: 489 + init_stock: 860 + price: 489 + service_level: 0.96 + vlt: 1 + SKU259: + cost: 333 + init_stock: 1380 + price: 333 + service_level: 0.96 + vlt: 1 + SKU26: + cost: 229 + init_stock: 1260 + price: 229 + service_level: 0.96 + vlt: 1 + SKU260: + cost: 487 + init_stock: 1040 + price: 487 + service_level: 0.96 + vlt: 3 + SKU261: + cost: 368 + init_stock: 440 + price: 368 + service_level: 0.96 + vlt: 1 + SKU262: + cost: 332 + init_stock: 1560 + price: 332 + service_level: 0.96 + vlt: 3 + SKU263: + cost: 189 + init_stock: 1480 + price: 189 + service_level: 0.96 + vlt: 3 + SKU264: + cost: 361 + init_stock: 1580 + price: 361 + service_level: 0.96 + vlt: 1 + SKU265: + cost: 286 + init_stock: 1180 + price: 286 + service_level: 0.96 + vlt: 3 + SKU266: + cost: 128 + init_stock: 940 + price: 128 + service_level: 0.96 + vlt: 2 + SKU267: + cost: 77 + init_stock: 1600 + price: 77 + service_level: 0.96 + vlt: 1 + SKU268: + cost: 221 + init_stock: 1780 + price: 221 + service_level: 0.96 + vlt: 2 + SKU269: + cost: 126 + init_stock: 880 + price: 126 + service_level: 0.96 + vlt: 2 + SKU27: + cost: 370 + init_stock: 1120 + price: 370 + service_level: 0.96 + vlt: 3 + SKU270: + cost: 182 + init_stock: 1480 + price: 182 + service_level: 0.96 + vlt: 3 + SKU271: + cost: 230 + init_stock: 360 + price: 230 + service_level: 0.96 + vlt: 1 + SKU272: + cost: 366 + init_stock: 340 + price: 366 + service_level: 0.96 + vlt: 2 + SKU273: + cost: 421 + init_stock: 360 + price: 421 + service_level: 0.96 + vlt: 2 + SKU274: + cost: 29 + init_stock: 1540 + price: 29 + service_level: 0.96 + vlt: 2 + SKU275: + cost: 50 + init_stock: 960 + price: 50 + service_level: 0.96 + vlt: 1 + SKU276: + cost: 163 + init_stock: 1080 + price: 163 + service_level: 0.96 + vlt: 3 + SKU277: + cost: 449 + init_stock: 820 + price: 449 + service_level: 0.96 + vlt: 1 + SKU278: + cost: 105 + init_stock: 240 + price: 105 + service_level: 0.96 + vlt: 3 + SKU279: + cost: 51 + init_stock: 780 + price: 51 + service_level: 0.96 + vlt: 2 + SKU28: + cost: 208 + init_stock: 940 + price: 208 + service_level: 0.96 + vlt: 2 + SKU280: + cost: 295 + init_stock: 660 + price: 295 + service_level: 0.96 + vlt: 2 + SKU281: + cost: 395 + init_stock: 1500 + price: 395 + service_level: 0.96 + vlt: 1 + SKU282: + cost: 63 + init_stock: 920 + price: 63 + service_level: 0.96 + vlt: 2 + SKU283: + cost: 392 + init_stock: 1840 + price: 392 + service_level: 0.96 + vlt: 3 + SKU284: + cost: 344 + init_stock: 1340 + price: 344 + service_level: 0.96 + vlt: 3 + SKU285: + cost: 133 + init_stock: 1820 + price: 133 + service_level: 0.96 + vlt: 2 + SKU286: + cost: 337 + init_stock: 780 + price: 337 + service_level: 0.96 + vlt: 1 + SKU287: + cost: 375 + init_stock: 1120 + price: 375 + service_level: 0.96 + vlt: 3 + SKU288: + cost: 181 + init_stock: 760 + price: 181 + service_level: 0.96 + vlt: 1 + SKU289: + cost: 67 + init_stock: 620 + price: 67 + service_level: 0.96 + vlt: 1 + SKU29: + cost: 245 + init_stock: 1160 + price: 245 + service_level: 0.96 + vlt: 1 + SKU290: + cost: 438 + init_stock: 1340 + price: 438 + service_level: 0.96 + vlt: 1 + SKU291: + cost: 94 + init_stock: 1220 + price: 94 + service_level: 0.96 + vlt: 1 + SKU292: + cost: 186 + init_stock: 100 + price: 186 + service_level: 0.96 + vlt: 1 + SKU293: + cost: 162 + init_stock: 1100 + price: 162 + service_level: 0.96 + vlt: 2 + SKU294: + cost: 409 + init_stock: 180 + price: 409 + service_level: 0.96 + vlt: 2 + SKU295: + cost: 435 + init_stock: 1860 + price: 435 + service_level: 0.96 + vlt: 3 + SKU296: + cost: 370 + init_stock: 1840 + price: 370 + service_level: 0.96 + vlt: 3 + SKU297: + cost: 298 + init_stock: 760 + price: 298 + service_level: 0.96 + vlt: 1 + SKU298: + cost: 286 + init_stock: 700 + price: 286 + service_level: 0.96 + vlt: 2 + SKU299: + cost: 367 + init_stock: 1020 + price: 367 + service_level: 0.96 + vlt: 3 + SKU3: + cost: 405 + init_stock: 240 + price: 405 + service_level: 0.96 + vlt: 3 + SKU30: + cost: 359 + init_stock: 1380 + price: 359 + service_level: 0.96 + vlt: 2 + SKU300: + cost: 376 + init_stock: 1160 + price: 376 + service_level: 0.96 + vlt: 1 + SKU301: + cost: 433 + init_stock: 1660 + price: 433 + service_level: 0.96 + vlt: 2 + SKU302: + cost: 184 + init_stock: 220 + price: 184 + service_level: 0.96 + vlt: 2 + SKU303: + cost: 246 + init_stock: 1880 + price: 246 + service_level: 0.96 + vlt: 1 + SKU304: + cost: 419 + init_stock: 460 + price: 419 + service_level: 0.96 + vlt: 3 + SKU305: + cost: 495 + init_stock: 2000 + price: 495 + service_level: 0.96 + vlt: 1 + SKU306: + cost: 479 + init_stock: 840 + price: 479 + service_level: 0.96 + vlt: 2 + SKU307: + cost: 210 + init_stock: 1560 + price: 210 + service_level: 0.96 + vlt: 1 + SKU308: + cost: 208 + init_stock: 100 + price: 208 + service_level: 0.96 + vlt: 2 + SKU309: + cost: 101 + init_stock: 1840 + price: 101 + service_level: 0.96 + vlt: 2 + SKU31: + cost: 69 + init_stock: 1120 + price: 69 + service_level: 0.96 + vlt: 3 + SKU310: + cost: 234 + init_stock: 880 + price: 234 + service_level: 0.96 + vlt: 3 + SKU311: + cost: 306 + init_stock: 1600 + price: 306 + service_level: 0.96 + vlt: 3 + SKU312: + cost: 291 + init_stock: 500 + price: 291 + service_level: 0.96 + vlt: 1 + SKU313: + cost: 324 + init_stock: 760 + price: 324 + service_level: 0.96 + vlt: 1 + SKU314: + cost: 404 + init_stock: 580 + price: 404 + service_level: 0.96 + vlt: 1 + SKU315: + cost: 471 + init_stock: 1680 + price: 471 + service_level: 0.96 + vlt: 2 + SKU316: + cost: 202 + init_stock: 360 + price: 202 + service_level: 0.96 + vlt: 1 + SKU317: + cost: 216 + init_stock: 480 + price: 216 + service_level: 0.96 + vlt: 2 + SKU318: + cost: 278 + init_stock: 1700 + price: 278 + service_level: 0.96 + vlt: 1 + SKU319: + cost: 257 + init_stock: 1060 + price: 257 + service_level: 0.96 + vlt: 3 + SKU32: + cost: 336 + init_stock: 440 + price: 336 + service_level: 0.96 + vlt: 1 + SKU320: + cost: 196 + init_stock: 780 + price: 196 + service_level: 0.96 + vlt: 3 + SKU321: + cost: 67 + init_stock: 320 + price: 67 + service_level: 0.96 + vlt: 3 + SKU322: + cost: 240 + init_stock: 2000 + price: 240 + service_level: 0.96 + vlt: 1 + SKU323: + cost: 66 + init_stock: 780 + price: 66 + service_level: 0.96 + vlt: 2 + SKU324: + cost: 442 + init_stock: 1860 + price: 442 + service_level: 0.96 + vlt: 3 + SKU325: + cost: 453 + init_stock: 1380 + price: 453 + service_level: 0.96 + vlt: 2 + SKU326: + cost: 345 + init_stock: 480 + price: 345 + service_level: 0.96 + vlt: 2 + SKU327: + cost: 457 + init_stock: 280 + price: 457 + service_level: 0.96 + vlt: 2 + SKU328: + cost: 390 + init_stock: 900 + price: 390 + service_level: 0.96 + vlt: 1 + SKU329: + cost: 67 + init_stock: 840 + price: 67 + service_level: 0.96 + vlt: 1 + SKU33: + cost: 416 + init_stock: 1840 + price: 416 + service_level: 0.96 + vlt: 2 + SKU330: + cost: 306 + init_stock: 780 + price: 306 + service_level: 0.96 + vlt: 2 + SKU331: + cost: 138 + init_stock: 820 + price: 138 + service_level: 0.96 + vlt: 3 + SKU332: + cost: 390 + init_stock: 1920 + price: 390 + service_level: 0.96 + vlt: 2 + SKU333: + cost: 485 + init_stock: 1060 + price: 485 + service_level: 0.96 + vlt: 2 + SKU334: + cost: 183 + init_stock: 1140 + price: 183 + service_level: 0.96 + vlt: 1 + SKU335: + cost: 80 + init_stock: 1620 + price: 80 + service_level: 0.96 + vlt: 1 + SKU336: + cost: 296 + init_stock: 560 + price: 296 + service_level: 0.96 + vlt: 1 + SKU337: + cost: 245 + init_stock: 580 + price: 245 + service_level: 0.96 + vlt: 2 + SKU338: + cost: 87 + init_stock: 1620 + price: 87 + service_level: 0.96 + vlt: 3 + SKU339: + cost: 265 + init_stock: 1260 + price: 265 + service_level: 0.96 + vlt: 2 + SKU34: + cost: 95 + init_stock: 260 + price: 95 + service_level: 0.96 + vlt: 3 + SKU340: + cost: 480 + init_stock: 1740 + price: 480 + service_level: 0.96 + vlt: 2 + SKU341: + cost: 462 + init_stock: 1400 + price: 462 + service_level: 0.96 + vlt: 1 + SKU342: + cost: 144 + init_stock: 180 + price: 144 + service_level: 0.96 + vlt: 3 + SKU343: + cost: 310 + init_stock: 300 + price: 310 + service_level: 0.96 + vlt: 2 + SKU344: + cost: 357 + init_stock: 1740 + price: 357 + service_level: 0.96 + vlt: 2 + SKU345: + cost: 442 + init_stock: 1780 + price: 442 + service_level: 0.96 + vlt: 2 + SKU346: + cost: 350 + init_stock: 840 + price: 350 + service_level: 0.96 + vlt: 3 + SKU347: + cost: 497 + init_stock: 1640 + price: 497 + service_level: 0.96 + vlt: 1 + SKU348: + cost: 147 + init_stock: 400 + price: 147 + service_level: 0.96 + vlt: 1 + SKU349: + cost: 67 + init_stock: 1340 + price: 67 + service_level: 0.96 + vlt: 3 + SKU35: + cost: 267 + init_stock: 1720 + price: 267 + service_level: 0.96 + vlt: 3 + SKU350: + cost: 279 + init_stock: 1840 + price: 279 + service_level: 0.96 + vlt: 3 + SKU351: + cost: 155 + init_stock: 1340 + price: 155 + service_level: 0.96 + vlt: 1 + SKU352: + cost: 71 + init_stock: 360 + price: 71 + service_level: 0.96 + vlt: 2 + SKU353: + cost: 253 + init_stock: 1860 + price: 253 + service_level: 0.96 + vlt: 2 + SKU354: + cost: 396 + init_stock: 1580 + price: 396 + service_level: 0.96 + vlt: 3 + SKU355: + cost: 63 + init_stock: 200 + price: 63 + service_level: 0.96 + vlt: 1 + SKU356: + cost: 348 + init_stock: 580 + price: 348 + service_level: 0.96 + vlt: 1 + SKU357: + cost: 499 + init_stock: 1840 + price: 499 + service_level: 0.96 + vlt: 3 + SKU358: + cost: 269 + init_stock: 1380 + price: 269 + service_level: 0.96 + vlt: 2 + SKU359: + cost: 82 + init_stock: 1500 + price: 82 + service_level: 0.96 + vlt: 3 + SKU36: + cost: 105 + init_stock: 200 + price: 105 + service_level: 0.96 + vlt: 2 + SKU360: + cost: 484 + init_stock: 500 + price: 484 + service_level: 0.96 + vlt: 1 + SKU361: + cost: 163 + init_stock: 1800 + price: 163 + service_level: 0.96 + vlt: 1 + SKU362: + cost: 464 + init_stock: 1740 + price: 464 + service_level: 0.96 + vlt: 2 + SKU363: + cost: 52 + init_stock: 1560 + price: 52 + service_level: 0.96 + vlt: 2 + SKU364: + cost: 387 + init_stock: 660 + price: 387 + service_level: 0.96 + vlt: 1 + SKU365: + cost: 158 + init_stock: 1660 + price: 158 + service_level: 0.96 + vlt: 3 + SKU366: + cost: 444 + init_stock: 1720 + price: 444 + service_level: 0.96 + vlt: 3 + SKU367: + cost: 272 + init_stock: 920 + price: 272 + service_level: 0.96 + vlt: 3 + SKU368: + cost: 472 + init_stock: 660 + price: 472 + service_level: 0.96 + vlt: 1 + SKU369: + cost: 96 + init_stock: 1500 + price: 96 + service_level: 0.96 + vlt: 2 + SKU37: + cost: 66 + init_stock: 1180 + price: 66 + service_level: 0.96 + vlt: 2 + SKU370: + cost: 112 + init_stock: 300 + price: 112 + service_level: 0.96 + vlt: 2 + SKU371: + cost: 328 + init_stock: 100 + price: 328 + service_level: 0.96 + vlt: 3 + SKU372: + cost: 67 + init_stock: 640 + price: 67 + service_level: 0.96 + vlt: 1 + SKU373: + cost: 58 + init_stock: 1340 + price: 58 + service_level: 0.96 + vlt: 3 + SKU374: + cost: 38 + init_stock: 1080 + price: 38 + service_level: 0.96 + vlt: 2 + SKU375: + cost: 283 + init_stock: 1440 + price: 283 + service_level: 0.96 + vlt: 1 + SKU376: + cost: 156 + init_stock: 1640 + price: 156 + service_level: 0.96 + vlt: 1 + SKU377: + cost: 78 + init_stock: 1340 + price: 78 + service_level: 0.96 + vlt: 3 + SKU378: + cost: 424 + init_stock: 700 + price: 424 + service_level: 0.96 + vlt: 1 + SKU379: + cost: 11 + init_stock: 980 + price: 11 + service_level: 0.96 + vlt: 1 + SKU38: + cost: 344 + init_stock: 860 + price: 344 + service_level: 0.96 + vlt: 2 + SKU380: + cost: 393 + init_stock: 1060 + price: 393 + service_level: 0.96 + vlt: 1 + SKU381: + cost: 476 + init_stock: 120 + price: 476 + service_level: 0.96 + vlt: 1 + SKU382: + cost: 125 + init_stock: 1420 + price: 125 + service_level: 0.96 + vlt: 2 + SKU383: + cost: 304 + init_stock: 1840 + price: 304 + service_level: 0.96 + vlt: 3 + SKU384: + cost: 320 + init_stock: 180 + price: 320 + service_level: 0.96 + vlt: 2 + SKU385: + cost: 120 + init_stock: 260 + price: 120 + service_level: 0.96 + vlt: 2 + SKU386: + cost: 295 + init_stock: 620 + price: 295 + service_level: 0.96 + vlt: 1 + SKU387: + cost: 112 + init_stock: 1940 + price: 112 + service_level: 0.96 + vlt: 3 + SKU388: + cost: 405 + init_stock: 880 + price: 405 + service_level: 0.96 + vlt: 2 + SKU389: + cost: 376 + init_stock: 1400 + price: 376 + service_level: 0.96 + vlt: 2 + SKU39: + cost: 25 + init_stock: 1020 + price: 25 + service_level: 0.96 + vlt: 3 + SKU390: + cost: 144 + init_stock: 780 + price: 144 + service_level: 0.96 + vlt: 2 + SKU391: + cost: 233 + init_stock: 1340 + price: 233 + service_level: 0.96 + vlt: 2 + SKU392: + cost: 163 + init_stock: 1480 + price: 163 + service_level: 0.96 + vlt: 2 + SKU393: + cost: 487 + init_stock: 1340 + price: 487 + service_level: 0.96 + vlt: 1 + SKU394: + cost: 154 + init_stock: 1060 + price: 154 + service_level: 0.96 + vlt: 2 + SKU395: + cost: 488 + init_stock: 660 + price: 488 + service_level: 0.96 + vlt: 3 + SKU396: + cost: 333 + init_stock: 1220 + price: 333 + service_level: 0.96 + vlt: 3 + SKU397: + cost: 460 + init_stock: 1020 + price: 460 + service_level: 0.96 + vlt: 1 + SKU398: + cost: 234 + init_stock: 1160 + price: 234 + service_level: 0.96 + vlt: 2 + SKU399: + cost: 376 + init_stock: 740 + price: 376 + service_level: 0.96 + vlt: 2 + SKU4: + cost: 439 + init_stock: 140 + price: 439 + service_level: 0.96 + vlt: 2 + SKU40: + cost: 490 + init_stock: 1980 + price: 490 + service_level: 0.96 + vlt: 3 + SKU400: + cost: 445 + init_stock: 1680 + price: 445 + service_level: 0.96 + vlt: 2 + SKU401: + cost: 410 + init_stock: 1560 + price: 410 + service_level: 0.96 + vlt: 1 + SKU402: + cost: 86 + init_stock: 140 + price: 86 + service_level: 0.96 + vlt: 3 + SKU403: + cost: 89 + init_stock: 1980 + price: 89 + service_level: 0.96 + vlt: 3 + SKU404: + cost: 287 + init_stock: 1220 + price: 287 + service_level: 0.96 + vlt: 1 + SKU405: + cost: 460 + init_stock: 380 + price: 460 + service_level: 0.96 + vlt: 1 + SKU406: + cost: 327 + init_stock: 2000 + price: 327 + service_level: 0.96 + vlt: 1 + SKU407: + cost: 26 + init_stock: 920 + price: 26 + service_level: 0.96 + vlt: 2 + SKU408: + cost: 444 + init_stock: 160 + price: 444 + service_level: 0.96 + vlt: 2 + SKU409: + cost: 257 + init_stock: 1820 + price: 257 + service_level: 0.96 + vlt: 2 + SKU41: + cost: 141 + init_stock: 1580 + price: 141 + service_level: 0.96 + vlt: 2 + SKU410: + cost: 70 + init_stock: 320 + price: 70 + service_level: 0.96 + vlt: 1 + SKU411: + cost: 210 + init_stock: 1900 + price: 210 + service_level: 0.96 + vlt: 3 + SKU412: + cost: 286 + init_stock: 1240 + price: 286 + service_level: 0.96 + vlt: 2 + SKU413: + cost: 403 + init_stock: 1660 + price: 403 + service_level: 0.96 + vlt: 3 + SKU414: + cost: 165 + init_stock: 1740 + price: 165 + service_level: 0.96 + vlt: 1 + SKU415: + cost: 291 + init_stock: 460 + price: 291 + service_level: 0.96 + vlt: 3 + SKU416: + cost: 228 + init_stock: 180 + price: 228 + service_level: 0.96 + vlt: 3 + SKU417: + cost: 443 + init_stock: 1440 + price: 443 + service_level: 0.96 + vlt: 1 + SKU418: + cost: 458 + init_stock: 260 + price: 458 + service_level: 0.96 + vlt: 3 + SKU419: + cost: 303 + init_stock: 1780 + price: 303 + service_level: 0.96 + vlt: 3 + SKU42: + cost: 462 + init_stock: 520 + price: 462 + service_level: 0.96 + vlt: 3 + SKU420: + cost: 268 + init_stock: 840 + price: 268 + service_level: 0.96 + vlt: 1 + SKU421: + cost: 214 + init_stock: 920 + price: 214 + service_level: 0.96 + vlt: 2 + SKU422: + cost: 52 + init_stock: 1080 + price: 52 + service_level: 0.96 + vlt: 3 + SKU423: + cost: 188 + init_stock: 1320 + price: 188 + service_level: 0.96 + vlt: 1 + SKU424: + cost: 189 + init_stock: 220 + price: 189 + service_level: 0.96 + vlt: 2 + SKU425: + cost: 162 + init_stock: 240 + price: 162 + service_level: 0.96 + vlt: 3 + SKU426: + cost: 125 + init_stock: 1960 + price: 125 + service_level: 0.96 + vlt: 3 + SKU427: + cost: 413 + init_stock: 1880 + price: 413 + service_level: 0.96 + vlt: 1 + SKU428: + cost: 391 + init_stock: 1260 + price: 391 + service_level: 0.96 + vlt: 3 + SKU429: + cost: 39 + init_stock: 820 + price: 39 + service_level: 0.96 + vlt: 1 + SKU43: + cost: 217 + init_stock: 1360 + price: 217 + service_level: 0.96 + vlt: 1 + SKU430: + cost: 216 + init_stock: 1460 + price: 216 + service_level: 0.96 + vlt: 2 + SKU431: + cost: 328 + init_stock: 420 + price: 328 + service_level: 0.96 + vlt: 1 + SKU432: + cost: 25 + init_stock: 920 + price: 25 + service_level: 0.96 + vlt: 3 + SKU433: + cost: 348 + init_stock: 900 + price: 348 + service_level: 0.96 + vlt: 2 + SKU434: + cost: 37 + init_stock: 600 + price: 37 + service_level: 0.96 + vlt: 1 + SKU435: + cost: 272 + init_stock: 600 + price: 272 + service_level: 0.96 + vlt: 3 + SKU436: + cost: 72 + init_stock: 1620 + price: 72 + service_level: 0.96 + vlt: 1 + SKU437: + cost: 115 + init_stock: 280 + price: 115 + service_level: 0.96 + vlt: 2 + SKU438: + cost: 143 + init_stock: 200 + price: 143 + service_level: 0.96 + vlt: 3 + SKU439: + cost: 256 + init_stock: 760 + price: 256 + service_level: 0.96 + vlt: 1 + SKU44: + cost: 360 + init_stock: 960 + price: 360 + service_level: 0.96 + vlt: 1 + SKU440: + cost: 248 + init_stock: 1640 + price: 248 + service_level: 0.96 + vlt: 2 + SKU441: + cost: 253 + init_stock: 1120 + price: 253 + service_level: 0.96 + vlt: 2 + SKU442: + cost: 470 + init_stock: 1760 + price: 470 + service_level: 0.96 + vlt: 2 + SKU443: + cost: 218 + init_stock: 1460 + price: 218 + service_level: 0.96 + vlt: 1 + SKU444: + cost: 156 + init_stock: 120 + price: 156 + service_level: 0.96 + vlt: 2 + SKU445: + cost: 260 + init_stock: 1720 + price: 260 + service_level: 0.96 + vlt: 2 + SKU446: + cost: 497 + init_stock: 980 + price: 497 + service_level: 0.96 + vlt: 1 + SKU447: + cost: 196 + init_stock: 100 + price: 196 + service_level: 0.96 + vlt: 1 + SKU448: + cost: 485 + init_stock: 1420 + price: 485 + service_level: 0.96 + vlt: 3 + SKU449: + cost: 282 + init_stock: 760 + price: 282 + service_level: 0.96 + vlt: 2 + SKU45: + cost: 253 + init_stock: 200 + price: 253 + service_level: 0.96 + vlt: 1 + SKU450: + cost: 58 + init_stock: 1960 + price: 58 + service_level: 0.96 + vlt: 2 + SKU451: + cost: 468 + init_stock: 1380 + price: 468 + service_level: 0.96 + vlt: 3 + SKU452: + cost: 63 + init_stock: 1580 + price: 63 + service_level: 0.96 + vlt: 1 + SKU453: + cost: 37 + init_stock: 700 + price: 37 + service_level: 0.96 + vlt: 3 + SKU454: + cost: 494 + init_stock: 1700 + price: 494 + service_level: 0.96 + vlt: 1 + SKU455: + cost: 136 + init_stock: 520 + price: 136 + service_level: 0.96 + vlt: 2 + SKU456: + cost: 338 + init_stock: 500 + price: 338 + service_level: 0.96 + vlt: 2 + SKU457: + cost: 169 + init_stock: 1280 + price: 169 + service_level: 0.96 + vlt: 2 + SKU458: + cost: 403 + init_stock: 140 + price: 403 + service_level: 0.96 + vlt: 3 + SKU459: + cost: 425 + init_stock: 680 + price: 425 + service_level: 0.96 + vlt: 3 + SKU46: + cost: 299 + init_stock: 1000 + price: 299 + service_level: 0.96 + vlt: 3 + SKU460: + cost: 405 + init_stock: 1460 + price: 405 + service_level: 0.96 + vlt: 2 + SKU461: + cost: 251 + init_stock: 960 + price: 251 + service_level: 0.96 + vlt: 1 + SKU462: + cost: 448 + init_stock: 180 + price: 448 + service_level: 0.96 + vlt: 1 + SKU463: + cost: 187 + init_stock: 1640 + price: 187 + service_level: 0.96 + vlt: 3 + SKU464: + cost: 279 + init_stock: 680 + price: 279 + service_level: 0.96 + vlt: 3 + SKU465: + cost: 147 + init_stock: 2000 + price: 147 + service_level: 0.96 + vlt: 2 + SKU466: + cost: 97 + init_stock: 840 + price: 97 + service_level: 0.96 + vlt: 1 + SKU467: + cost: 142 + init_stock: 720 + price: 142 + service_level: 0.96 + vlt: 2 + SKU468: + cost: 55 + init_stock: 1000 + price: 55 + service_level: 0.96 + vlt: 2 + SKU469: + cost: 178 + init_stock: 300 + price: 178 + service_level: 0.96 + vlt: 3 + SKU47: + cost: 121 + init_stock: 780 + price: 121 + service_level: 0.96 + vlt: 1 + SKU470: + cost: 373 + init_stock: 640 + price: 373 + service_level: 0.96 + vlt: 2 + SKU471: + cost: 315 + init_stock: 1420 + price: 315 + service_level: 0.96 + vlt: 3 + SKU472: + cost: 421 + init_stock: 1180 + price: 421 + service_level: 0.96 + vlt: 2 + SKU473: + cost: 195 + init_stock: 420 + price: 195 + service_level: 0.96 + vlt: 1 + SKU474: + cost: 448 + init_stock: 1720 + price: 448 + service_level: 0.96 + vlt: 2 + SKU475: + cost: 34 + init_stock: 1880 + price: 34 + service_level: 0.96 + vlt: 1 + SKU476: + cost: 251 + init_stock: 1800 + price: 251 + service_level: 0.96 + vlt: 1 + SKU477: + cost: 289 + init_stock: 940 + price: 289 + service_level: 0.96 + vlt: 2 + SKU478: + cost: 479 + init_stock: 1760 + price: 479 + service_level: 0.96 + vlt: 2 + SKU479: + cost: 366 + init_stock: 760 + price: 366 + service_level: 0.96 + vlt: 1 + SKU48: + cost: 340 + init_stock: 1160 + price: 340 + service_level: 0.96 + vlt: 2 + SKU480: + cost: 18 + init_stock: 600 + price: 18 + service_level: 0.96 + vlt: 2 + SKU481: + cost: 330 + init_stock: 1760 + price: 330 + service_level: 0.96 + vlt: 1 + SKU482: + cost: 94 + init_stock: 1740 + price: 94 + service_level: 0.96 + vlt: 3 + SKU483: + cost: 298 + init_stock: 680 + price: 298 + service_level: 0.96 + vlt: 2 + SKU484: + cost: 84 + init_stock: 1460 + price: 84 + service_level: 0.96 + vlt: 3 + SKU485: + cost: 318 + init_stock: 1340 + price: 318 + service_level: 0.96 + vlt: 3 + SKU486: + cost: 122 + init_stock: 1040 + price: 122 + service_level: 0.96 + vlt: 1 + SKU487: + cost: 277 + init_stock: 1320 + price: 277 + service_level: 0.96 + vlt: 3 + SKU488: + cost: 36 + init_stock: 540 + price: 36 + service_level: 0.96 + vlt: 3 + SKU489: + cost: 59 + init_stock: 620 + price: 59 + service_level: 0.96 + vlt: 3 + SKU49: + cost: 263 + init_stock: 1900 + price: 263 + service_level: 0.96 + vlt: 3 + SKU490: + cost: 161 + init_stock: 1620 + price: 161 + service_level: 0.96 + vlt: 2 + SKU491: + cost: 444 + init_stock: 1500 + price: 444 + service_level: 0.96 + vlt: 2 + SKU492: + cost: 53 + init_stock: 1600 + price: 53 + service_level: 0.96 + vlt: 2 + SKU493: + cost: 461 + init_stock: 540 + price: 461 + service_level: 0.96 + vlt: 1 + SKU494: + cost: 145 + init_stock: 1880 + price: 145 + service_level: 0.96 + vlt: 1 + SKU495: + cost: 149 + init_stock: 1240 + price: 149 + service_level: 0.96 + vlt: 3 + SKU496: + cost: 342 + init_stock: 1180 + price: 342 + service_level: 0.96 + vlt: 1 + SKU497: + cost: 33 + init_stock: 180 + price: 33 + service_level: 0.96 + vlt: 3 + SKU498: + cost: 305 + init_stock: 1300 + price: 305 + service_level: 0.96 + vlt: 3 + SKU499: + cost: 307 + init_stock: 580 + price: 307 + service_level: 0.96 + vlt: 2 + SKU5: + cost: 466 + init_stock: 1420 + price: 466 + service_level: 0.96 + vlt: 2 + SKU50: + cost: 282 + init_stock: 740 + price: 282 + service_level: 0.96 + vlt: 3 + SKU500: + cost: 208 + init_stock: 840 + price: 208 + service_level: 0.96 + vlt: 3 + SKU501: + cost: 194 + init_stock: 1520 + price: 194 + service_level: 0.96 + vlt: 1 + SKU502: + cost: 69 + init_stock: 1300 + price: 69 + service_level: 0.96 + vlt: 2 + SKU503: + cost: 208 + init_stock: 1140 + price: 208 + service_level: 0.96 + vlt: 1 + SKU504: + cost: 251 + init_stock: 600 + price: 251 + service_level: 0.96 + vlt: 2 + SKU505: + cost: 426 + init_stock: 1180 + price: 426 + service_level: 0.96 + vlt: 1 + SKU506: + cost: 149 + init_stock: 1860 + price: 149 + service_level: 0.96 + vlt: 3 + SKU507: + cost: 187 + init_stock: 300 + price: 187 + service_level: 0.96 + vlt: 1 + SKU508: + cost: 272 + init_stock: 1920 + price: 272 + service_level: 0.96 + vlt: 1 + SKU509: + cost: 297 + init_stock: 1620 + price: 297 + service_level: 0.96 + vlt: 1 + SKU51: + cost: 295 + init_stock: 1340 + price: 295 + service_level: 0.96 + vlt: 3 + SKU510: + cost: 94 + init_stock: 180 + price: 94 + service_level: 0.96 + vlt: 1 + SKU511: + cost: 361 + init_stock: 1500 + price: 361 + service_level: 0.96 + vlt: 1 + SKU512: + cost: 467 + init_stock: 440 + price: 467 + service_level: 0.96 + vlt: 2 + SKU513: + cost: 343 + init_stock: 140 + price: 343 + service_level: 0.96 + vlt: 3 + SKU514: + cost: 186 + init_stock: 1260 + price: 186 + service_level: 0.96 + vlt: 3 + SKU515: + cost: 145 + init_stock: 1080 + price: 145 + service_level: 0.96 + vlt: 3 + SKU516: + cost: 64 + init_stock: 760 + price: 64 + service_level: 0.96 + vlt: 1 + SKU517: + cost: 117 + init_stock: 180 + price: 117 + service_level: 0.96 + vlt: 3 + SKU518: + cost: 26 + init_stock: 420 + price: 26 + service_level: 0.96 + vlt: 3 + SKU519: + cost: 233 + init_stock: 1000 + price: 233 + service_level: 0.96 + vlt: 2 + SKU52: + cost: 22 + init_stock: 1340 + price: 22 + service_level: 0.96 + vlt: 1 + SKU520: + cost: 209 + init_stock: 440 + price: 209 + service_level: 0.96 + vlt: 1 + SKU521: + cost: 220 + init_stock: 1000 + price: 220 + service_level: 0.96 + vlt: 3 + SKU522: + cost: 156 + init_stock: 1740 + price: 156 + service_level: 0.96 + vlt: 1 + SKU523: + cost: 65 + init_stock: 2000 + price: 65 + service_level: 0.96 + vlt: 3 + SKU524: + cost: 294 + init_stock: 1880 + price: 294 + service_level: 0.96 + vlt: 1 + SKU525: + cost: 432 + init_stock: 840 + price: 432 + service_level: 0.96 + vlt: 2 + SKU526: + cost: 419 + init_stock: 480 + price: 419 + service_level: 0.96 + vlt: 3 + SKU527: + cost: 131 + init_stock: 1400 + price: 131 + service_level: 0.96 + vlt: 1 + SKU528: + cost: 316 + init_stock: 420 + price: 316 + service_level: 0.96 + vlt: 3 + SKU529: + cost: 298 + init_stock: 620 + price: 298 + service_level: 0.96 + vlt: 3 + SKU53: + cost: 151 + init_stock: 1400 + price: 151 + service_level: 0.96 + vlt: 2 + SKU530: + cost: 131 + init_stock: 900 + price: 131 + service_level: 0.96 + vlt: 1 + SKU531: + cost: 103 + init_stock: 1200 + price: 103 + service_level: 0.96 + vlt: 3 + SKU532: + cost: 351 + init_stock: 1540 + price: 351 + service_level: 0.96 + vlt: 3 + SKU533: + cost: 296 + init_stock: 840 + price: 296 + service_level: 0.96 + vlt: 3 + SKU534: + cost: 425 + init_stock: 820 + price: 425 + service_level: 0.96 + vlt: 1 + SKU535: + cost: 304 + init_stock: 1720 + price: 304 + service_level: 0.96 + vlt: 2 + SKU536: + cost: 358 + init_stock: 1040 + price: 358 + service_level: 0.96 + vlt: 2 + SKU537: + cost: 321 + init_stock: 180 + price: 321 + service_level: 0.96 + vlt: 3 + SKU538: + cost: 457 + init_stock: 1000 + price: 457 + service_level: 0.96 + vlt: 1 + SKU539: + cost: 165 + init_stock: 1560 + price: 165 + service_level: 0.96 + vlt: 2 + SKU54: + cost: 158 + init_stock: 920 + price: 158 + service_level: 0.96 + vlt: 1 + SKU540: + cost: 388 + init_stock: 840 + price: 388 + service_level: 0.96 + vlt: 3 + SKU541: + cost: 131 + init_stock: 580 + price: 131 + service_level: 0.96 + vlt: 2 + SKU542: + cost: 38 + init_stock: 520 + price: 38 + service_level: 0.96 + vlt: 3 + SKU543: + cost: 430 + init_stock: 1700 + price: 430 + service_level: 0.96 + vlt: 2 + SKU544: + cost: 346 + init_stock: 1940 + price: 346 + service_level: 0.96 + vlt: 1 + SKU545: + cost: 175 + init_stock: 300 + price: 175 + service_level: 0.96 + vlt: 3 + SKU546: + cost: 491 + init_stock: 1920 + price: 491 + service_level: 0.96 + vlt: 3 + SKU547: + cost: 161 + init_stock: 880 + price: 161 + service_level: 0.96 + vlt: 2 + SKU548: + cost: 111 + init_stock: 120 + price: 111 + service_level: 0.96 + vlt: 1 + SKU549: + cost: 387 + init_stock: 1580 + price: 387 + service_level: 0.96 + vlt: 1 + SKU55: + cost: 482 + init_stock: 1300 + price: 482 + service_level: 0.96 + vlt: 3 + SKU550: + cost: 259 + init_stock: 1880 + price: 259 + service_level: 0.96 + vlt: 1 + SKU551: + cost: 46 + init_stock: 620 + price: 46 + service_level: 0.96 + vlt: 2 + SKU552: + cost: 191 + init_stock: 1800 + price: 191 + service_level: 0.96 + vlt: 3 + SKU553: + cost: 208 + init_stock: 600 + price: 208 + service_level: 0.96 + vlt: 1 + SKU554: + cost: 340 + init_stock: 820 + price: 340 + service_level: 0.96 + vlt: 1 + SKU555: + cost: 489 + init_stock: 1680 + price: 489 + service_level: 0.96 + vlt: 1 + SKU556: + cost: 110 + init_stock: 600 + price: 110 + service_level: 0.96 + vlt: 2 + SKU557: + cost: 334 + init_stock: 1460 + price: 334 + service_level: 0.96 + vlt: 2 + SKU558: + cost: 399 + init_stock: 340 + price: 399 + service_level: 0.96 + vlt: 1 + SKU559: + cost: 313 + init_stock: 1080 + price: 313 + service_level: 0.96 + vlt: 1 + SKU56: + cost: 377 + init_stock: 1480 + price: 377 + service_level: 0.96 + vlt: 1 + SKU560: + cost: 283 + init_stock: 820 + price: 283 + service_level: 0.96 + vlt: 1 + SKU561: + cost: 202 + init_stock: 780 + price: 202 + service_level: 0.96 + vlt: 3 + SKU562: + cost: 347 + init_stock: 1780 + price: 347 + service_level: 0.96 + vlt: 2 + SKU563: + cost: 401 + init_stock: 100 + price: 401 + service_level: 0.96 + vlt: 3 + SKU564: + cost: 109 + init_stock: 180 + price: 109 + service_level: 0.96 + vlt: 2 + SKU565: + cost: 57 + init_stock: 1500 + price: 57 + service_level: 0.96 + vlt: 3 + SKU566: + cost: 131 + init_stock: 220 + price: 131 + service_level: 0.96 + vlt: 2 + SKU567: + cost: 361 + init_stock: 180 + price: 361 + service_level: 0.96 + vlt: 1 + SKU568: + cost: 75 + init_stock: 1560 + price: 75 + service_level: 0.96 + vlt: 2 + SKU569: + cost: 473 + init_stock: 960 + price: 473 + service_level: 0.96 + vlt: 2 + SKU57: + cost: 359 + init_stock: 1000 + price: 359 + service_level: 0.96 + vlt: 2 + SKU570: + cost: 388 + init_stock: 1660 + price: 388 + service_level: 0.96 + vlt: 2 + SKU571: + cost: 168 + init_stock: 780 + price: 168 + service_level: 0.96 + vlt: 1 + SKU572: + cost: 26 + init_stock: 900 + price: 26 + service_level: 0.96 + vlt: 3 + SKU573: + cost: 246 + init_stock: 820 + price: 246 + service_level: 0.96 + vlt: 2 + SKU574: + cost: 94 + init_stock: 1000 + price: 94 + service_level: 0.96 + vlt: 2 + SKU575: + cost: 237 + init_stock: 1580 + price: 237 + service_level: 0.96 + vlt: 1 + SKU576: + cost: 265 + init_stock: 1560 + price: 265 + service_level: 0.96 + vlt: 3 + SKU577: + cost: 18 + init_stock: 1740 + price: 18 + service_level: 0.96 + vlt: 3 + SKU578: + cost: 100 + init_stock: 1420 + price: 100 + service_level: 0.96 + vlt: 2 + SKU579: + cost: 415 + init_stock: 180 + price: 415 + service_level: 0.96 + vlt: 1 + SKU58: + cost: 362 + init_stock: 960 + price: 362 + service_level: 0.96 + vlt: 2 + SKU580: + cost: 203 + init_stock: 100 + price: 203 + service_level: 0.96 + vlt: 1 + SKU581: + cost: 152 + init_stock: 1720 + price: 152 + service_level: 0.96 + vlt: 2 + SKU582: + cost: 359 + init_stock: 1920 + price: 359 + service_level: 0.96 + vlt: 2 + SKU583: + cost: 86 + init_stock: 1740 + price: 86 + service_level: 0.96 + vlt: 1 + SKU584: + cost: 33 + init_stock: 900 + price: 33 + service_level: 0.96 + vlt: 2 + SKU585: + cost: 31 + init_stock: 400 + price: 31 + service_level: 0.96 + vlt: 2 + SKU586: + cost: 61 + init_stock: 880 + price: 61 + service_level: 0.96 + vlt: 2 + SKU587: + cost: 290 + init_stock: 1560 + price: 290 + service_level: 0.96 + vlt: 2 + SKU588: + cost: 476 + init_stock: 880 + price: 476 + service_level: 0.96 + vlt: 2 + SKU589: + cost: 244 + init_stock: 680 + price: 244 + service_level: 0.96 + vlt: 3 + SKU59: + cost: 145 + init_stock: 1020 + price: 145 + service_level: 0.96 + vlt: 1 + SKU590: + cost: 70 + init_stock: 620 + price: 70 + service_level: 0.96 + vlt: 2 + SKU591: + cost: 206 + init_stock: 1680 + price: 206 + service_level: 0.96 + vlt: 2 + SKU592: + cost: 218 + init_stock: 920 + price: 218 + service_level: 0.96 + vlt: 1 + SKU593: + cost: 124 + init_stock: 880 + price: 124 + service_level: 0.96 + vlt: 1 + SKU594: + cost: 238 + init_stock: 1000 + price: 238 + service_level: 0.96 + vlt: 1 + SKU595: + cost: 73 + init_stock: 860 + price: 73 + service_level: 0.96 + vlt: 3 + SKU596: + cost: 432 + init_stock: 140 + price: 432 + service_level: 0.96 + vlt: 2 + SKU597: + cost: 252 + init_stock: 1740 + price: 252 + service_level: 0.96 + vlt: 2 + SKU598: + cost: 348 + init_stock: 280 + price: 348 + service_level: 0.96 + vlt: 1 + SKU599: + cost: 54 + init_stock: 1360 + price: 54 + service_level: 0.96 + vlt: 2 + SKU6: + cost: 122 + init_stock: 1760 + price: 122 + service_level: 0.96 + vlt: 2 + SKU60: + cost: 200 + init_stock: 780 + price: 200 + service_level: 0.96 + vlt: 1 + SKU600: + cost: 386 + init_stock: 1300 + price: 386 + service_level: 0.96 + vlt: 1 + SKU601: + cost: 138 + init_stock: 780 + price: 138 + service_level: 0.96 + vlt: 3 + SKU602: + cost: 184 + init_stock: 1960 + price: 184 + service_level: 0.96 + vlt: 1 + SKU603: + cost: 278 + init_stock: 840 + price: 278 + service_level: 0.96 + vlt: 3 + SKU604: + cost: 270 + init_stock: 1000 + price: 270 + service_level: 0.96 + vlt: 3 + SKU605: + cost: 288 + init_stock: 480 + price: 288 + service_level: 0.96 + vlt: 2 + SKU606: + cost: 114 + init_stock: 220 + price: 114 + service_level: 0.96 + vlt: 3 + SKU607: + cost: 208 + init_stock: 1580 + price: 208 + service_level: 0.96 + vlt: 2 + SKU608: + cost: 39 + init_stock: 1460 + price: 39 + service_level: 0.96 + vlt: 1 + SKU609: + cost: 102 + init_stock: 380 + price: 102 + service_level: 0.96 + vlt: 2 + SKU61: + cost: 461 + init_stock: 1500 + price: 461 + service_level: 0.96 + vlt: 1 + SKU610: + cost: 449 + init_stock: 1360 + price: 449 + service_level: 0.96 + vlt: 1 + SKU611: + cost: 306 + init_stock: 1540 + price: 306 + service_level: 0.96 + vlt: 3 + SKU612: + cost: 391 + init_stock: 1760 + price: 391 + service_level: 0.96 + vlt: 2 + SKU613: + cost: 174 + init_stock: 140 + price: 174 + service_level: 0.96 + vlt: 3 + SKU614: + cost: 173 + init_stock: 300 + price: 173 + service_level: 0.96 + vlt: 1 + SKU615: + cost: 330 + init_stock: 1820 + price: 330 + service_level: 0.96 + vlt: 2 + SKU616: + cost: 232 + init_stock: 1460 + price: 232 + service_level: 0.96 + vlt: 1 + SKU617: + cost: 203 + init_stock: 1200 + price: 203 + service_level: 0.96 + vlt: 3 + SKU618: + cost: 77 + init_stock: 460 + price: 77 + service_level: 0.96 + vlt: 1 + SKU619: + cost: 189 + init_stock: 1720 + price: 189 + service_level: 0.96 + vlt: 2 + SKU62: + cost: 124 + init_stock: 180 + price: 124 + service_level: 0.96 + vlt: 2 + SKU620: + cost: 231 + init_stock: 780 + price: 231 + service_level: 0.96 + vlt: 3 + SKU621: + cost: 199 + init_stock: 1440 + price: 199 + service_level: 0.96 + vlt: 2 + SKU622: + cost: 253 + init_stock: 1300 + price: 253 + service_level: 0.96 + vlt: 3 + SKU623: + cost: 335 + init_stock: 1100 + price: 335 + service_level: 0.96 + vlt: 1 + SKU624: + cost: 482 + init_stock: 1080 + price: 482 + service_level: 0.96 + vlt: 2 + SKU625: + cost: 46 + init_stock: 1780 + price: 46 + service_level: 0.96 + vlt: 1 + SKU626: + cost: 451 + init_stock: 1780 + price: 451 + service_level: 0.96 + vlt: 3 + SKU627: + cost: 220 + init_stock: 1640 + price: 220 + service_level: 0.96 + vlt: 3 + SKU628: + cost: 124 + init_stock: 520 + price: 124 + service_level: 0.96 + vlt: 2 + SKU629: + cost: 353 + init_stock: 1840 + price: 353 + service_level: 0.96 + vlt: 1 + SKU63: + cost: 68 + init_stock: 280 + price: 68 + service_level: 0.96 + vlt: 3 + SKU630: + cost: 122 + init_stock: 340 + price: 122 + service_level: 0.96 + vlt: 1 + SKU631: + cost: 296 + init_stock: 980 + price: 296 + service_level: 0.96 + vlt: 2 + SKU632: + cost: 85 + init_stock: 1880 + price: 85 + service_level: 0.96 + vlt: 2 + SKU633: + cost: 184 + init_stock: 1340 + price: 184 + service_level: 0.96 + vlt: 2 + SKU634: + cost: 17 + init_stock: 1460 + price: 17 + service_level: 0.96 + vlt: 1 + SKU635: + cost: 341 + init_stock: 1860 + price: 341 + service_level: 0.96 + vlt: 1 + SKU636: + cost: 385 + init_stock: 740 + price: 385 + service_level: 0.96 + vlt: 1 + SKU637: + cost: 83 + init_stock: 1220 + price: 83 + service_level: 0.96 + vlt: 3 + SKU638: + cost: 375 + init_stock: 160 + price: 375 + service_level: 0.96 + vlt: 1 + SKU639: + cost: 327 + init_stock: 800 + price: 327 + service_level: 0.96 + vlt: 2 + SKU64: + cost: 36 + init_stock: 380 + price: 36 + service_level: 0.96 + vlt: 3 + SKU640: + cost: 275 + init_stock: 1820 + price: 275 + service_level: 0.96 + vlt: 1 + SKU641: + cost: 365 + init_stock: 1620 + price: 365 + service_level: 0.96 + vlt: 1 + SKU642: + cost: 414 + init_stock: 500 + price: 414 + service_level: 0.96 + vlt: 2 + SKU643: + cost: 358 + init_stock: 920 + price: 358 + service_level: 0.96 + vlt: 2 + SKU644: + cost: 46 + init_stock: 1640 + price: 46 + service_level: 0.96 + vlt: 2 + SKU645: + cost: 467 + init_stock: 1980 + price: 467 + service_level: 0.96 + vlt: 3 + SKU646: + cost: 189 + init_stock: 260 + price: 189 + service_level: 0.96 + vlt: 3 + SKU647: + cost: 303 + init_stock: 1540 + price: 303 + service_level: 0.96 + vlt: 3 + SKU648: + cost: 226 + init_stock: 1100 + price: 226 + service_level: 0.96 + vlt: 1 + SKU649: + cost: 198 + init_stock: 500 + price: 198 + service_level: 0.96 + vlt: 3 + SKU65: + cost: 15 + init_stock: 1880 + price: 15 + service_level: 0.96 + vlt: 2 + SKU650: + cost: 351 + init_stock: 1120 + price: 351 + service_level: 0.96 + vlt: 2 + SKU651: + cost: 380 + init_stock: 1920 + price: 380 + service_level: 0.96 + vlt: 3 + SKU652: + cost: 123 + init_stock: 800 + price: 123 + service_level: 0.96 + vlt: 2 + SKU653: + cost: 479 + init_stock: 1920 + price: 479 + service_level: 0.96 + vlt: 2 + SKU654: + cost: 66 + init_stock: 160 + price: 66 + service_level: 0.96 + vlt: 3 + SKU655: + cost: 333 + init_stock: 840 + price: 333 + service_level: 0.96 + vlt: 1 + SKU656: + cost: 53 + init_stock: 1440 + price: 53 + service_level: 0.96 + vlt: 2 + SKU657: + cost: 73 + init_stock: 1620 + price: 73 + service_level: 0.96 + vlt: 2 + SKU658: + cost: 70 + init_stock: 720 + price: 70 + service_level: 0.96 + vlt: 3 + SKU659: + cost: 21 + init_stock: 1120 + price: 21 + service_level: 0.96 + vlt: 1 + SKU66: + cost: 143 + init_stock: 1800 + price: 143 + service_level: 0.96 + vlt: 1 + SKU660: + cost: 487 + init_stock: 1660 + price: 487 + service_level: 0.96 + vlt: 1 + SKU661: + cost: 344 + init_stock: 1480 + price: 344 + service_level: 0.96 + vlt: 2 + SKU662: + cost: 372 + init_stock: 760 + price: 372 + service_level: 0.96 + vlt: 3 + SKU663: + cost: 418 + init_stock: 800 + price: 418 + service_level: 0.96 + vlt: 3 + SKU664: + cost: 351 + init_stock: 1680 + price: 351 + service_level: 0.96 + vlt: 1 + SKU665: + cost: 493 + init_stock: 860 + price: 493 + service_level: 0.96 + vlt: 3 + SKU666: + cost: 341 + init_stock: 1760 + price: 341 + service_level: 0.96 + vlt: 1 + SKU667: + cost: 325 + init_stock: 260 + price: 325 + service_level: 0.96 + vlt: 3 + SKU668: + cost: 286 + init_stock: 1200 + price: 286 + service_level: 0.96 + vlt: 3 + SKU669: + cost: 74 + init_stock: 320 + price: 74 + service_level: 0.96 + vlt: 1 + SKU67: + cost: 247 + init_stock: 1060 + price: 247 + service_level: 0.96 + vlt: 1 + SKU670: + cost: 289 + init_stock: 1720 + price: 289 + service_level: 0.96 + vlt: 1 + SKU671: + cost: 267 + init_stock: 1660 + price: 267 + service_level: 0.96 + vlt: 3 + SKU672: + cost: 369 + init_stock: 260 + price: 369 + service_level: 0.96 + vlt: 2 + SKU673: + cost: 322 + init_stock: 820 + price: 322 + service_level: 0.96 + vlt: 2 + SKU674: + cost: 223 + init_stock: 440 + price: 223 + service_level: 0.96 + vlt: 1 + SKU675: + cost: 438 + init_stock: 660 + price: 438 + service_level: 0.96 + vlt: 1 + SKU676: + cost: 244 + init_stock: 1220 + price: 244 + service_level: 0.96 + vlt: 1 + SKU677: + cost: 425 + init_stock: 880 + price: 425 + service_level: 0.96 + vlt: 2 + SKU678: + cost: 168 + init_stock: 720 + price: 168 + service_level: 0.96 + vlt: 1 + SKU679: + cost: 395 + init_stock: 880 + price: 395 + service_level: 0.96 + vlt: 2 + SKU68: + cost: 169 + init_stock: 280 + price: 169 + service_level: 0.96 + vlt: 2 + SKU680: + cost: 260 + init_stock: 600 + price: 260 + service_level: 0.96 + vlt: 1 + SKU681: + cost: 464 + init_stock: 1940 + price: 464 + service_level: 0.96 + vlt: 3 + SKU682: + cost: 370 + init_stock: 1060 + price: 370 + service_level: 0.96 + vlt: 2 + SKU683: + cost: 327 + init_stock: 1340 + price: 327 + service_level: 0.96 + vlt: 3 + SKU684: + cost: 355 + init_stock: 1580 + price: 355 + service_level: 0.96 + vlt: 1 + SKU685: + cost: 422 + init_stock: 900 + price: 422 + service_level: 0.96 + vlt: 2 + SKU686: + cost: 63 + init_stock: 1260 + price: 63 + service_level: 0.96 + vlt: 1 + SKU687: + cost: 34 + init_stock: 1520 + price: 34 + service_level: 0.96 + vlt: 2 + SKU688: + cost: 484 + init_stock: 1540 + price: 484 + service_level: 0.96 + vlt: 3 + SKU689: + cost: 499 + init_stock: 300 + price: 499 + service_level: 0.96 + vlt: 1 + SKU69: + cost: 176 + init_stock: 1340 + price: 176 + service_level: 0.96 + vlt: 1 + SKU690: + cost: 437 + init_stock: 1660 + price: 437 + service_level: 0.96 + vlt: 2 + SKU691: + cost: 17 + init_stock: 1580 + price: 17 + service_level: 0.96 + vlt: 3 + SKU692: + cost: 225 + init_stock: 1300 + price: 225 + service_level: 0.96 + vlt: 1 + SKU693: + cost: 19 + init_stock: 1220 + price: 19 + service_level: 0.96 + vlt: 2 + SKU694: + cost: 208 + init_stock: 1500 + price: 208 + service_level: 0.96 + vlt: 3 + SKU695: + cost: 190 + init_stock: 140 + price: 190 + service_level: 0.96 + vlt: 2 + SKU696: + cost: 348 + init_stock: 1160 + price: 348 + service_level: 0.96 + vlt: 1 + SKU697: + cost: 455 + init_stock: 1600 + price: 455 + service_level: 0.96 + vlt: 1 + SKU698: + cost: 198 + init_stock: 1220 + price: 198 + service_level: 0.96 + vlt: 3 + SKU699: + cost: 31 + init_stock: 400 + price: 31 + service_level: 0.96 + vlt: 3 + SKU7: + cost: 101 + init_stock: 1920 + price: 101 + service_level: 0.96 + vlt: 2 + SKU70: + cost: 186 + init_stock: 700 + price: 186 + service_level: 0.96 + vlt: 1 + SKU700: + cost: 74 + init_stock: 180 + price: 74 + service_level: 0.96 + vlt: 2 + SKU701: + cost: 399 + init_stock: 1540 + price: 399 + service_level: 0.96 + vlt: 1 + SKU702: + cost: 82 + init_stock: 680 + price: 82 + service_level: 0.96 + vlt: 1 + SKU703: + cost: 363 + init_stock: 760 + price: 363 + service_level: 0.96 + vlt: 1 + SKU704: + cost: 384 + init_stock: 1740 + price: 384 + service_level: 0.96 + vlt: 2 + SKU705: + cost: 128 + init_stock: 1260 + price: 128 + service_level: 0.96 + vlt: 2 + SKU706: + cost: 182 + init_stock: 1820 + price: 182 + service_level: 0.96 + vlt: 2 + SKU707: + cost: 18 + init_stock: 1380 + price: 18 + service_level: 0.96 + vlt: 1 + SKU708: + cost: 178 + init_stock: 560 + price: 178 + service_level: 0.96 + vlt: 3 + SKU709: + cost: 102 + init_stock: 1060 + price: 102 + service_level: 0.96 + vlt: 3 + SKU71: + cost: 315 + init_stock: 900 + price: 315 + service_level: 0.96 + vlt: 2 + SKU710: + cost: 252 + init_stock: 380 + price: 252 + service_level: 0.96 + vlt: 2 + SKU711: + cost: 281 + init_stock: 1380 + price: 281 + service_level: 0.96 + vlt: 1 + SKU712: + cost: 232 + init_stock: 220 + price: 232 + service_level: 0.96 + vlt: 2 + SKU713: + cost: 285 + init_stock: 860 + price: 285 + service_level: 0.96 + vlt: 2 + SKU714: + cost: 79 + init_stock: 820 + price: 79 + service_level: 0.96 + vlt: 3 + SKU715: + cost: 234 + init_stock: 340 + price: 234 + service_level: 0.96 + vlt: 1 + SKU716: + cost: 70 + init_stock: 1500 + price: 70 + service_level: 0.96 + vlt: 2 + SKU717: + cost: 345 + init_stock: 160 + price: 345 + service_level: 0.96 + vlt: 2 + SKU718: + cost: 357 + init_stock: 1160 + price: 357 + service_level: 0.96 + vlt: 2 + SKU719: + cost: 340 + init_stock: 1300 + price: 340 + service_level: 0.96 + vlt: 3 + SKU72: + cost: 458 + init_stock: 1700 + price: 458 + service_level: 0.96 + vlt: 2 + SKU720: + cost: 325 + init_stock: 840 + price: 325 + service_level: 0.96 + vlt: 3 + SKU721: + cost: 73 + init_stock: 220 + price: 73 + service_level: 0.96 + vlt: 2 + SKU722: + cost: 392 + init_stock: 1940 + price: 392 + service_level: 0.96 + vlt: 1 + SKU723: + cost: 318 + init_stock: 1780 + price: 318 + service_level: 0.96 + vlt: 3 + SKU724: + cost: 400 + init_stock: 660 + price: 400 + service_level: 0.96 + vlt: 1 + SKU725: + cost: 175 + init_stock: 740 + price: 175 + service_level: 0.96 + vlt: 1 + SKU726: + cost: 458 + init_stock: 1780 + price: 458 + service_level: 0.96 + vlt: 3 + SKU727: + cost: 418 + init_stock: 1140 + price: 418 + service_level: 0.96 + vlt: 1 + SKU728: + cost: 475 + init_stock: 740 + price: 475 + service_level: 0.96 + vlt: 3 + SKU729: + cost: 324 + init_stock: 720 + price: 324 + service_level: 0.96 + vlt: 3 + SKU73: + cost: 212 + init_stock: 1080 + price: 212 + service_level: 0.96 + vlt: 2 + SKU730: + cost: 16 + init_stock: 260 + price: 16 + service_level: 0.96 + vlt: 2 + SKU731: + cost: 88 + init_stock: 1640 + price: 88 + service_level: 0.96 + vlt: 2 + SKU732: + cost: 41 + init_stock: 1240 + price: 41 + service_level: 0.96 + vlt: 3 + SKU733: + cost: 315 + init_stock: 520 + price: 315 + service_level: 0.96 + vlt: 3 + SKU734: + cost: 37 + init_stock: 1020 + price: 37 + service_level: 0.96 + vlt: 3 + SKU735: + cost: 266 + init_stock: 1980 + price: 266 + service_level: 0.96 + vlt: 3 + SKU736: + cost: 368 + init_stock: 500 + price: 368 + service_level: 0.96 + vlt: 2 + SKU737: + cost: 475 + init_stock: 620 + price: 475 + service_level: 0.96 + vlt: 2 + SKU738: + cost: 185 + init_stock: 960 + price: 185 + service_level: 0.96 + vlt: 3 + SKU739: + cost: 475 + init_stock: 1480 + price: 475 + service_level: 0.96 + vlt: 2 + SKU74: + cost: 159 + init_stock: 840 + price: 159 + service_level: 0.96 + vlt: 1 + SKU740: + cost: 390 + init_stock: 1280 + price: 390 + service_level: 0.96 + vlt: 1 + SKU741: + cost: 91 + init_stock: 1120 + price: 91 + service_level: 0.96 + vlt: 1 + SKU742: + cost: 188 + init_stock: 440 + price: 188 + service_level: 0.96 + vlt: 1 + SKU743: + cost: 217 + init_stock: 860 + price: 217 + service_level: 0.96 + vlt: 3 + SKU744: + cost: 379 + init_stock: 1160 + price: 379 + service_level: 0.96 + vlt: 1 + SKU745: + cost: 316 + init_stock: 1840 + price: 316 + service_level: 0.96 + vlt: 2 + SKU746: + cost: 437 + init_stock: 800 + price: 437 + service_level: 0.96 + vlt: 2 + SKU747: + cost: 373 + init_stock: 1580 + price: 373 + service_level: 0.96 + vlt: 2 + SKU748: + cost: 275 + init_stock: 1320 + price: 275 + service_level: 0.96 + vlt: 2 + SKU749: + cost: 394 + init_stock: 1060 + price: 394 + service_level: 0.96 + vlt: 1 + SKU75: + cost: 131 + init_stock: 380 + price: 131 + service_level: 0.96 + vlt: 1 + SKU750: + cost: 256 + init_stock: 1240 + price: 256 + service_level: 0.96 + vlt: 3 + SKU751: + cost: 369 + init_stock: 1300 + price: 369 + service_level: 0.96 + vlt: 3 + SKU752: + cost: 259 + init_stock: 1560 + price: 259 + service_level: 0.96 + vlt: 3 + SKU753: + cost: 77 + init_stock: 1300 + price: 77 + service_level: 0.96 + vlt: 3 + SKU754: + cost: 387 + init_stock: 260 + price: 387 + service_level: 0.96 + vlt: 2 + SKU755: + cost: 354 + init_stock: 1600 + price: 354 + service_level: 0.96 + vlt: 3 + SKU756: + cost: 246 + init_stock: 1800 + price: 246 + service_level: 0.96 + vlt: 1 + SKU757: + cost: 40 + init_stock: 1140 + price: 40 + service_level: 0.96 + vlt: 3 + SKU758: + cost: 241 + init_stock: 800 + price: 241 + service_level: 0.96 + vlt: 2 + SKU759: + cost: 435 + init_stock: 760 + price: 435 + service_level: 0.96 + vlt: 1 + SKU76: + cost: 147 + init_stock: 1280 + price: 147 + service_level: 0.96 + vlt: 2 + SKU760: + cost: 176 + init_stock: 1020 + price: 176 + service_level: 0.96 + vlt: 3 + SKU761: + cost: 224 + init_stock: 1100 + price: 224 + service_level: 0.96 + vlt: 1 + SKU762: + cost: 264 + init_stock: 1020 + price: 264 + service_level: 0.96 + vlt: 1 + SKU763: + cost: 385 + init_stock: 1580 + price: 385 + service_level: 0.96 + vlt: 2 + SKU764: + cost: 349 + init_stock: 680 + price: 349 + service_level: 0.96 + vlt: 1 + SKU765: + cost: 345 + init_stock: 260 + price: 345 + service_level: 0.96 + vlt: 1 + SKU766: + cost: 478 + init_stock: 400 + price: 478 + service_level: 0.96 + vlt: 2 + SKU767: + cost: 95 + init_stock: 1520 + price: 95 + service_level: 0.96 + vlt: 1 + SKU768: + cost: 181 + init_stock: 1840 + price: 181 + service_level: 0.96 + vlt: 2 + SKU769: + cost: 24 + init_stock: 580 + price: 24 + service_level: 0.96 + vlt: 2 + SKU77: + cost: 409 + init_stock: 1440 + price: 409 + service_level: 0.96 + vlt: 1 + SKU770: + cost: 150 + init_stock: 1240 + price: 150 + service_level: 0.96 + vlt: 2 + SKU771: + cost: 101 + init_stock: 140 + price: 101 + service_level: 0.96 + vlt: 1 + SKU772: + cost: 256 + init_stock: 120 + price: 256 + service_level: 0.96 + vlt: 3 + SKU773: + cost: 84 + init_stock: 1840 + price: 84 + service_level: 0.96 + vlt: 1 + SKU774: + cost: 447 + init_stock: 1180 + price: 447 + service_level: 0.96 + vlt: 1 + SKU775: + cost: 175 + init_stock: 1720 + price: 175 + service_level: 0.96 + vlt: 3 + SKU776: + cost: 103 + init_stock: 1700 + price: 103 + service_level: 0.96 + vlt: 2 + SKU777: + cost: 292 + init_stock: 1140 + price: 292 + service_level: 0.96 + vlt: 2 + SKU778: + cost: 203 + init_stock: 160 + price: 203 + service_level: 0.96 + vlt: 3 + SKU779: + cost: 117 + init_stock: 860 + price: 117 + service_level: 0.96 + vlt: 1 + SKU78: + cost: 234 + init_stock: 260 + price: 234 + service_level: 0.96 + vlt: 3 + SKU780: + cost: 69 + init_stock: 1640 + price: 69 + service_level: 0.96 + vlt: 3 + SKU781: + cost: 372 + init_stock: 720 + price: 372 + service_level: 0.96 + vlt: 3 + SKU782: + cost: 27 + init_stock: 200 + price: 27 + service_level: 0.96 + vlt: 3 + SKU783: + cost: 87 + init_stock: 1620 + price: 87 + service_level: 0.96 + vlt: 2 + SKU784: + cost: 309 + init_stock: 1040 + price: 309 + service_level: 0.96 + vlt: 3 + SKU785: + cost: 191 + init_stock: 1400 + price: 191 + service_level: 0.96 + vlt: 2 + SKU786: + cost: 91 + init_stock: 440 + price: 91 + service_level: 0.96 + vlt: 3 + SKU787: + cost: 360 + init_stock: 1220 + price: 360 + service_level: 0.96 + vlt: 1 + SKU788: + cost: 351 + init_stock: 560 + price: 351 + service_level: 0.96 + vlt: 1 + SKU789: + cost: 153 + init_stock: 1160 + price: 153 + service_level: 0.96 + vlt: 2 + SKU79: + cost: 245 + init_stock: 220 + price: 245 + service_level: 0.96 + vlt: 1 + SKU790: + cost: 417 + init_stock: 1320 + price: 417 + service_level: 0.96 + vlt: 2 + SKU791: + cost: 134 + init_stock: 560 + price: 134 + service_level: 0.96 + vlt: 1 + SKU792: + cost: 313 + init_stock: 420 + price: 313 + service_level: 0.96 + vlt: 2 + SKU793: + cost: 195 + init_stock: 1520 + price: 195 + service_level: 0.96 + vlt: 2 + SKU794: + cost: 325 + init_stock: 1600 + price: 325 + service_level: 0.96 + vlt: 3 + SKU795: + cost: 276 + init_stock: 960 + price: 276 + service_level: 0.96 + vlt: 3 + SKU796: + cost: 447 + init_stock: 980 + price: 447 + service_level: 0.96 + vlt: 1 + SKU797: + cost: 100 + init_stock: 780 + price: 100 + service_level: 0.96 + vlt: 3 + SKU798: + cost: 426 + init_stock: 600 + price: 426 + service_level: 0.96 + vlt: 2 + SKU799: + cost: 63 + init_stock: 140 + price: 63 + service_level: 0.96 + vlt: 2 + SKU8: + cost: 125 + init_stock: 1260 + price: 125 + service_level: 0.96 + vlt: 3 + SKU80: + cost: 163 + init_stock: 1400 + price: 163 + service_level: 0.96 + vlt: 1 + SKU800: + cost: 298 + init_stock: 1880 + price: 298 + service_level: 0.96 + vlt: 2 + SKU801: + cost: 389 + init_stock: 720 + price: 389 + service_level: 0.96 + vlt: 2 + SKU802: + cost: 173 + init_stock: 1240 + price: 173 + service_level: 0.96 + vlt: 2 + SKU803: + cost: 272 + init_stock: 380 + price: 272 + service_level: 0.96 + vlt: 3 + SKU804: + cost: 390 + init_stock: 940 + price: 390 + service_level: 0.96 + vlt: 3 + SKU805: + cost: 61 + init_stock: 660 + price: 61 + service_level: 0.96 + vlt: 3 + SKU806: + cost: 158 + init_stock: 1040 + price: 158 + service_level: 0.96 + vlt: 1 + SKU807: + cost: 453 + init_stock: 520 + price: 453 + service_level: 0.96 + vlt: 3 + SKU808: + cost: 249 + init_stock: 1820 + price: 249 + service_level: 0.96 + vlt: 1 + SKU809: + cost: 55 + init_stock: 1300 + price: 55 + service_level: 0.96 + vlt: 1 + SKU81: + cost: 182 + init_stock: 1320 + price: 182 + service_level: 0.96 + vlt: 3 + SKU810: + cost: 276 + init_stock: 120 + price: 276 + service_level: 0.96 + vlt: 2 + SKU811: + cost: 254 + init_stock: 740 + price: 254 + service_level: 0.96 + vlt: 3 + SKU812: + cost: 252 + init_stock: 1600 + price: 252 + service_level: 0.96 + vlt: 1 + SKU813: + cost: 253 + init_stock: 140 + price: 253 + service_level: 0.96 + vlt: 3 + SKU814: + cost: 485 + init_stock: 1940 + price: 485 + service_level: 0.96 + vlt: 1 + SKU815: + cost: 390 + init_stock: 480 + price: 390 + service_level: 0.96 + vlt: 2 + SKU816: + cost: 152 + init_stock: 1820 + price: 152 + service_level: 0.96 + vlt: 3 + SKU817: + cost: 227 + init_stock: 100 + price: 227 + service_level: 0.96 + vlt: 2 + SKU818: + cost: 354 + init_stock: 860 + price: 354 + service_level: 0.96 + vlt: 2 + SKU819: + cost: 302 + init_stock: 540 + price: 302 + service_level: 0.96 + vlt: 1 + SKU82: + cost: 290 + init_stock: 560 + price: 290 + service_level: 0.96 + vlt: 1 + SKU820: + cost: 264 + init_stock: 1640 + price: 264 + service_level: 0.96 + vlt: 3 + SKU821: + cost: 99 + init_stock: 1380 + price: 99 + service_level: 0.96 + vlt: 1 + SKU822: + cost: 136 + init_stock: 1400 + price: 136 + service_level: 0.96 + vlt: 3 + SKU823: + cost: 75 + init_stock: 560 + price: 75 + service_level: 0.96 + vlt: 2 + SKU824: + cost: 170 + init_stock: 1400 + price: 170 + service_level: 0.96 + vlt: 1 + SKU825: + cost: 214 + init_stock: 300 + price: 214 + service_level: 0.96 + vlt: 1 + SKU826: + cost: 386 + init_stock: 1100 + price: 386 + service_level: 0.96 + vlt: 2 + SKU827: + cost: 148 + init_stock: 1280 + price: 148 + service_level: 0.96 + vlt: 2 + SKU828: + cost: 400 + init_stock: 1380 + price: 400 + service_level: 0.96 + vlt: 1 + SKU829: + cost: 61 + init_stock: 1480 + price: 61 + service_level: 0.96 + vlt: 1 + SKU83: + cost: 296 + init_stock: 340 + price: 296 + service_level: 0.96 + vlt: 2 + SKU830: + cost: 167 + init_stock: 480 + price: 167 + service_level: 0.96 + vlt: 3 + SKU831: + cost: 262 + init_stock: 580 + price: 262 + service_level: 0.96 + vlt: 1 + SKU832: + cost: 33 + init_stock: 1640 + price: 33 + service_level: 0.96 + vlt: 1 + SKU833: + cost: 400 + init_stock: 1960 + price: 400 + service_level: 0.96 + vlt: 3 + SKU834: + cost: 422 + init_stock: 100 + price: 422 + service_level: 0.96 + vlt: 1 + SKU835: + cost: 440 + init_stock: 1040 + price: 440 + service_level: 0.96 + vlt: 2 + SKU836: + cost: 323 + init_stock: 1920 + price: 323 + service_level: 0.96 + vlt: 1 + SKU837: + cost: 373 + init_stock: 520 + price: 373 + service_level: 0.96 + vlt: 3 + SKU838: + cost: 456 + init_stock: 1540 + price: 456 + service_level: 0.96 + vlt: 1 + SKU839: + cost: 473 + init_stock: 1200 + price: 473 + service_level: 0.96 + vlt: 1 + SKU84: + cost: 318 + init_stock: 840 + price: 318 + service_level: 0.96 + vlt: 3 + SKU840: + cost: 266 + init_stock: 1000 + price: 266 + service_level: 0.96 + vlt: 1 + SKU841: + cost: 285 + init_stock: 1700 + price: 285 + service_level: 0.96 + vlt: 3 + SKU842: + cost: 41 + init_stock: 1680 + price: 41 + service_level: 0.96 + vlt: 1 + SKU843: + cost: 360 + init_stock: 1440 + price: 360 + service_level: 0.96 + vlt: 2 + SKU844: + cost: 51 + init_stock: 1580 + price: 51 + service_level: 0.96 + vlt: 3 + SKU845: + cost: 288 + init_stock: 440 + price: 288 + service_level: 0.96 + vlt: 3 + SKU846: + cost: 485 + init_stock: 780 + price: 485 + service_level: 0.96 + vlt: 1 + SKU847: + cost: 388 + init_stock: 1820 + price: 388 + service_level: 0.96 + vlt: 1 + SKU848: + cost: 306 + init_stock: 920 + price: 306 + service_level: 0.96 + vlt: 2 + SKU849: + cost: 320 + init_stock: 800 + price: 320 + service_level: 0.96 + vlt: 2 + SKU85: + cost: 442 + init_stock: 2000 + price: 442 + service_level: 0.96 + vlt: 3 + SKU850: + cost: 183 + init_stock: 1140 + price: 183 + service_level: 0.96 + vlt: 2 + SKU851: + cost: 53 + init_stock: 1280 + price: 53 + service_level: 0.96 + vlt: 1 + SKU852: + cost: 306 + init_stock: 1080 + price: 306 + service_level: 0.96 + vlt: 2 + SKU853: + cost: 386 + init_stock: 1120 + price: 386 + service_level: 0.96 + vlt: 3 + SKU854: + cost: 212 + init_stock: 1300 + price: 212 + service_level: 0.96 + vlt: 3 + SKU855: + cost: 76 + init_stock: 1440 + price: 76 + service_level: 0.96 + vlt: 3 + SKU856: + cost: 380 + init_stock: 640 + price: 380 + service_level: 0.96 + vlt: 2 + SKU857: + cost: 462 + init_stock: 2000 + price: 462 + service_level: 0.96 + vlt: 1 + SKU858: + cost: 80 + init_stock: 480 + price: 80 + service_level: 0.96 + vlt: 3 + SKU859: + cost: 215 + init_stock: 560 + price: 215 + service_level: 0.96 + vlt: 3 + SKU86: + cost: 386 + init_stock: 1700 + price: 386 + service_level: 0.96 + vlt: 2 + SKU860: + cost: 313 + init_stock: 300 + price: 313 + service_level: 0.96 + vlt: 2 + SKU861: + cost: 477 + init_stock: 260 + price: 477 + service_level: 0.96 + vlt: 2 + SKU862: + cost: 240 + init_stock: 320 + price: 240 + service_level: 0.96 + vlt: 2 + SKU863: + cost: 470 + init_stock: 1860 + price: 470 + service_level: 0.96 + vlt: 2 + SKU864: + cost: 203 + init_stock: 380 + price: 203 + service_level: 0.96 + vlt: 1 + SKU865: + cost: 144 + init_stock: 380 + price: 144 + service_level: 0.96 + vlt: 3 + SKU866: + cost: 172 + init_stock: 1760 + price: 172 + service_level: 0.96 + vlt: 1 + SKU867: + cost: 499 + init_stock: 2000 + price: 499 + service_level: 0.96 + vlt: 2 + SKU868: + cost: 41 + init_stock: 1000 + price: 41 + service_level: 0.96 + vlt: 2 + SKU869: + cost: 97 + init_stock: 1940 + price: 97 + service_level: 0.96 + vlt: 2 + SKU87: + cost: 368 + init_stock: 1380 + price: 368 + service_level: 0.96 + vlt: 2 + SKU870: + cost: 281 + init_stock: 1340 + price: 281 + service_level: 0.96 + vlt: 1 + SKU871: + cost: 101 + init_stock: 1980 + price: 101 + service_level: 0.96 + vlt: 1 + SKU872: + cost: 133 + init_stock: 640 + price: 133 + service_level: 0.96 + vlt: 3 + SKU873: + cost: 423 + init_stock: 620 + price: 423 + service_level: 0.96 + vlt: 2 + SKU874: + cost: 469 + init_stock: 1200 + price: 469 + service_level: 0.96 + vlt: 1 + SKU875: + cost: 51 + init_stock: 460 + price: 51 + service_level: 0.96 + vlt: 2 + SKU876: + cost: 303 + init_stock: 1220 + price: 303 + service_level: 0.96 + vlt: 3 + SKU877: + cost: 40 + init_stock: 700 + price: 40 + service_level: 0.96 + vlt: 3 + SKU878: + cost: 27 + init_stock: 1360 + price: 27 + service_level: 0.96 + vlt: 3 + SKU879: + cost: 150 + init_stock: 2000 + price: 150 + service_level: 0.96 + vlt: 1 + SKU88: + cost: 471 + init_stock: 240 + price: 471 + service_level: 0.96 + vlt: 1 + SKU880: + cost: 433 + init_stock: 620 + price: 433 + service_level: 0.96 + vlt: 3 + SKU881: + cost: 431 + init_stock: 1400 + price: 431 + service_level: 0.96 + vlt: 3 + SKU882: + cost: 194 + init_stock: 320 + price: 194 + service_level: 0.96 + vlt: 1 + SKU883: + cost: 284 + init_stock: 760 + price: 284 + service_level: 0.96 + vlt: 1 + SKU884: + cost: 139 + init_stock: 780 + price: 139 + service_level: 0.96 + vlt: 2 + SKU885: + cost: 49 + init_stock: 540 + price: 49 + service_level: 0.96 + vlt: 3 + SKU886: + cost: 372 + init_stock: 1460 + price: 372 + service_level: 0.96 + vlt: 3 + SKU887: + cost: 266 + init_stock: 1740 + price: 266 + service_level: 0.96 + vlt: 1 + SKU888: + cost: 143 + init_stock: 180 + price: 143 + service_level: 0.96 + vlt: 2 + SKU889: + cost: 101 + init_stock: 420 + price: 101 + service_level: 0.96 + vlt: 2 + SKU89: + cost: 138 + init_stock: 1860 + price: 138 + service_level: 0.96 + vlt: 3 + SKU890: + cost: 161 + init_stock: 2000 + price: 161 + service_level: 0.96 + vlt: 3 + SKU891: + cost: 356 + init_stock: 440 + price: 356 + service_level: 0.96 + vlt: 3 + SKU892: + cost: 313 + init_stock: 1840 + price: 313 + service_level: 0.96 + vlt: 2 + SKU893: + cost: 229 + init_stock: 1980 + price: 229 + service_level: 0.96 + vlt: 1 + SKU894: + cost: 129 + init_stock: 1480 + price: 129 + service_level: 0.96 + vlt: 1 + SKU895: + cost: 230 + init_stock: 260 + price: 230 + service_level: 0.96 + vlt: 2 + SKU896: + cost: 289 + init_stock: 1600 + price: 289 + service_level: 0.96 + vlt: 3 + SKU897: + cost: 393 + init_stock: 1440 + price: 393 + service_level: 0.96 + vlt: 3 + SKU898: + cost: 477 + init_stock: 220 + price: 477 + service_level: 0.96 + vlt: 2 + SKU899: + cost: 233 + init_stock: 960 + price: 233 + service_level: 0.96 + vlt: 2 + SKU9: + cost: 166 + init_stock: 1920 + price: 166 + service_level: 0.96 + vlt: 2 + SKU90: + cost: 454 + init_stock: 1020 + price: 454 + service_level: 0.96 + vlt: 2 + SKU900: + cost: 158 + init_stock: 1100 + price: 158 + service_level: 0.96 + vlt: 1 + SKU901: + cost: 215 + init_stock: 580 + price: 215 + service_level: 0.96 + vlt: 3 + SKU902: + cost: 125 + init_stock: 1600 + price: 125 + service_level: 0.96 + vlt: 2 + SKU903: + cost: 357 + init_stock: 760 + price: 357 + service_level: 0.96 + vlt: 2 + SKU904: + cost: 496 + init_stock: 1020 + price: 496 + service_level: 0.96 + vlt: 1 + SKU905: + cost: 249 + init_stock: 620 + price: 249 + service_level: 0.96 + vlt: 3 + SKU906: + cost: 166 + init_stock: 1040 + price: 166 + service_level: 0.96 + vlt: 2 + SKU907: + cost: 22 + init_stock: 1660 + price: 22 + service_level: 0.96 + vlt: 3 + SKU908: + cost: 408 + init_stock: 1560 + price: 408 + service_level: 0.96 + vlt: 2 + SKU909: + cost: 482 + init_stock: 1920 + price: 482 + service_level: 0.96 + vlt: 1 + SKU91: + cost: 303 + init_stock: 320 + price: 303 + service_level: 0.96 + vlt: 2 + SKU910: + cost: 226 + init_stock: 660 + price: 226 + service_level: 0.96 + vlt: 2 + SKU911: + cost: 461 + init_stock: 1400 + price: 461 + service_level: 0.96 + vlt: 2 + SKU912: + cost: 236 + init_stock: 540 + price: 236 + service_level: 0.96 + vlt: 3 + SKU913: + cost: 322 + init_stock: 920 + price: 322 + service_level: 0.96 + vlt: 2 + SKU914: + cost: 272 + init_stock: 1240 + price: 272 + service_level: 0.96 + vlt: 3 + SKU915: + cost: 337 + init_stock: 1120 + price: 337 + service_level: 0.96 + vlt: 3 + SKU916: + cost: 337 + init_stock: 1780 + price: 337 + service_level: 0.96 + vlt: 3 + SKU917: + cost: 258 + init_stock: 600 + price: 258 + service_level: 0.96 + vlt: 3 + SKU918: + cost: 148 + init_stock: 1100 + price: 148 + service_level: 0.96 + vlt: 1 + SKU919: + cost: 393 + init_stock: 500 + price: 393 + service_level: 0.96 + vlt: 3 + SKU92: + cost: 262 + init_stock: 1260 + price: 262 + service_level: 0.96 + vlt: 2 + SKU920: + cost: 357 + init_stock: 1720 + price: 357 + service_level: 0.96 + vlt: 3 + SKU921: + cost: 227 + init_stock: 1320 + price: 227 + service_level: 0.96 + vlt: 2 + SKU922: + cost: 112 + init_stock: 1340 + price: 112 + service_level: 0.96 + vlt: 2 + SKU923: + cost: 496 + init_stock: 1160 + price: 496 + service_level: 0.96 + vlt: 2 + SKU924: + cost: 316 + init_stock: 1700 + price: 316 + service_level: 0.96 + vlt: 3 + SKU925: + cost: 360 + init_stock: 300 + price: 360 + service_level: 0.96 + vlt: 1 + SKU926: + cost: 360 + init_stock: 1340 + price: 360 + service_level: 0.96 + vlt: 2 + SKU927: + cost: 260 + init_stock: 420 + price: 260 + service_level: 0.96 + vlt: 3 + SKU928: + cost: 491 + init_stock: 1660 + price: 491 + service_level: 0.96 + vlt: 1 + SKU929: + cost: 359 + init_stock: 2000 + price: 359 + service_level: 0.96 + vlt: 3 + SKU93: + cost: 404 + init_stock: 1340 + price: 404 + service_level: 0.96 + vlt: 2 + SKU930: + cost: 198 + init_stock: 560 + price: 198 + service_level: 0.96 + vlt: 2 + SKU931: + cost: 71 + init_stock: 280 + price: 71 + service_level: 0.96 + vlt: 3 + SKU932: + cost: 163 + init_stock: 1320 + price: 163 + service_level: 0.96 + vlt: 2 + SKU933: + cost: 113 + init_stock: 1560 + price: 113 + service_level: 0.96 + vlt: 1 + SKU934: + cost: 219 + init_stock: 1340 + price: 219 + service_level: 0.96 + vlt: 3 + SKU935: + cost: 364 + init_stock: 1880 + price: 364 + service_level: 0.96 + vlt: 1 + SKU936: + cost: 24 + init_stock: 100 + price: 24 + service_level: 0.96 + vlt: 3 + SKU937: + cost: 135 + init_stock: 340 + price: 135 + service_level: 0.96 + vlt: 1 + SKU938: + cost: 432 + init_stock: 420 + price: 432 + service_level: 0.96 + vlt: 2 + SKU939: + cost: 173 + init_stock: 1180 + price: 173 + service_level: 0.96 + vlt: 3 + SKU94: + cost: 184 + init_stock: 940 + price: 184 + service_level: 0.96 + vlt: 3 + SKU940: + cost: 14 + init_stock: 1860 + price: 14 + service_level: 0.96 + vlt: 1 + SKU941: + cost: 80 + init_stock: 1140 + price: 80 + service_level: 0.96 + vlt: 3 + SKU942: + cost: 202 + init_stock: 260 + price: 202 + service_level: 0.96 + vlt: 3 + SKU943: + cost: 138 + init_stock: 980 + price: 138 + service_level: 0.96 + vlt: 1 + SKU944: + cost: 196 + init_stock: 880 + price: 196 + service_level: 0.96 + vlt: 1 + SKU945: + cost: 141 + init_stock: 340 + price: 141 + service_level: 0.96 + vlt: 2 + SKU946: + cost: 325 + init_stock: 980 + price: 325 + service_level: 0.96 + vlt: 1 + SKU947: + cost: 338 + init_stock: 1820 + price: 338 + service_level: 0.96 + vlt: 3 + SKU948: + cost: 425 + init_stock: 560 + price: 425 + service_level: 0.96 + vlt: 3 + SKU949: + cost: 309 + init_stock: 100 + price: 309 + service_level: 0.96 + vlt: 2 + SKU95: + cost: 136 + init_stock: 420 + price: 136 + service_level: 0.96 + vlt: 3 + SKU950: + cost: 102 + init_stock: 1080 + price: 102 + service_level: 0.96 + vlt: 2 + SKU951: + cost: 75 + init_stock: 360 + price: 75 + service_level: 0.96 + vlt: 2 + SKU952: + cost: 156 + init_stock: 220 + price: 156 + service_level: 0.96 + vlt: 3 + SKU953: + cost: 138 + init_stock: 700 + price: 138 + service_level: 0.96 + vlt: 3 + SKU954: + cost: 296 + init_stock: 1720 + price: 296 + service_level: 0.96 + vlt: 1 + SKU955: + cost: 55 + init_stock: 1260 + price: 55 + service_level: 0.96 + vlt: 1 + SKU956: + cost: 282 + init_stock: 1700 + price: 282 + service_level: 0.96 + vlt: 1 + SKU957: + cost: 305 + init_stock: 1020 + price: 305 + service_level: 0.96 + vlt: 2 + SKU958: + cost: 369 + init_stock: 1320 + price: 369 + service_level: 0.96 + vlt: 2 + SKU959: + cost: 81 + init_stock: 1640 + price: 81 + service_level: 0.96 + vlt: 1 + SKU96: + cost: 176 + init_stock: 880 + price: 176 + service_level: 0.96 + vlt: 3 + SKU960: + cost: 147 + init_stock: 120 + price: 147 + service_level: 0.96 + vlt: 3 + SKU961: + cost: 264 + init_stock: 880 + price: 264 + service_level: 0.96 + vlt: 1 + SKU962: + cost: 354 + init_stock: 880 + price: 354 + service_level: 0.96 + vlt: 1 + SKU963: + cost: 349 + init_stock: 420 + price: 349 + service_level: 0.96 + vlt: 1 + SKU964: + cost: 244 + init_stock: 1460 + price: 244 + service_level: 0.96 + vlt: 1 + SKU965: + cost: 124 + init_stock: 1020 + price: 124 + service_level: 0.96 + vlt: 1 + SKU966: + cost: 302 + init_stock: 880 + price: 302 + service_level: 0.96 + vlt: 3 + SKU967: + cost: 67 + init_stock: 900 + price: 67 + service_level: 0.96 + vlt: 3 + SKU968: + cost: 281 + init_stock: 980 + price: 281 + service_level: 0.96 + vlt: 2 + SKU969: + cost: 249 + init_stock: 120 + price: 249 + service_level: 0.96 + vlt: 1 + SKU97: + cost: 28 + init_stock: 600 + price: 28 + service_level: 0.96 + vlt: 3 + SKU970: + cost: 244 + init_stock: 100 + price: 244 + service_level: 0.96 + vlt: 2 + SKU971: + cost: 368 + init_stock: 1460 + price: 368 + service_level: 0.96 + vlt: 1 + SKU972: + cost: 209 + init_stock: 1380 + price: 209 + service_level: 0.96 + vlt: 1 + SKU973: + cost: 271 + init_stock: 220 + price: 271 + service_level: 0.96 + vlt: 2 + SKU974: + cost: 170 + init_stock: 640 + price: 170 + service_level: 0.96 + vlt: 1 + SKU975: + cost: 198 + init_stock: 440 + price: 198 + service_level: 0.96 + vlt: 3 + SKU976: + cost: 178 + init_stock: 300 + price: 178 + service_level: 0.96 + vlt: 2 + SKU977: + cost: 234 + init_stock: 1080 + price: 234 + service_level: 0.96 + vlt: 3 + SKU978: + cost: 470 + init_stock: 1280 + price: 470 + service_level: 0.96 + vlt: 3 + SKU979: + cost: 443 + init_stock: 1920 + price: 443 + service_level: 0.96 + vlt: 2 + SKU98: + cost: 443 + init_stock: 700 + price: 443 + service_level: 0.96 + vlt: 1 + SKU980: + cost: 39 + init_stock: 1360 + price: 39 + service_level: 0.96 + vlt: 3 + SKU981: + cost: 482 + init_stock: 1440 + price: 482 + service_level: 0.96 + vlt: 2 + SKU982: + cost: 213 + init_stock: 1040 + price: 213 + service_level: 0.96 + vlt: 3 + SKU983: + cost: 449 + init_stock: 1840 + price: 449 + service_level: 0.96 + vlt: 1 + SKU984: + cost: 232 + init_stock: 1820 + price: 232 + service_level: 0.96 + vlt: 3 + SKU985: + cost: 290 + init_stock: 1020 + price: 290 + service_level: 0.96 + vlt: 1 + SKU986: + cost: 275 + init_stock: 340 + price: 275 + service_level: 0.96 + vlt: 3 + SKU987: + cost: 434 + init_stock: 680 + price: 434 + service_level: 0.96 + vlt: 1 + SKU988: + cost: 102 + init_stock: 460 + price: 102 + service_level: 0.96 + vlt: 1 + SKU989: + cost: 484 + init_stock: 1860 + price: 484 + service_level: 0.96 + vlt: 1 + SKU99: + cost: 361 + init_stock: 900 + price: 361 + service_level: 0.96 + vlt: 3 + SKU990: + cost: 108 + init_stock: 1140 + price: 108 + service_level: 0.96 + vlt: 3 + SKU991: + cost: 409 + init_stock: 380 + price: 409 + service_level: 0.96 + vlt: 3 + SKU992: + cost: 434 + init_stock: 1860 + price: 434 + service_level: 0.96 + vlt: 3 + SKU993: + cost: 145 + init_stock: 1720 + price: 145 + service_level: 0.96 + vlt: 2 + SKU994: + cost: 470 + init_stock: 1000 + price: 470 + service_level: 0.96 + vlt: 3 + SKU995: + cost: 241 + init_stock: 1520 + price: 241 + service_level: 0.96 + vlt: 2 + SKU996: + cost: 260 + init_stock: 1460 + price: 260 + service_level: 0.96 + vlt: 3 + SKU997: + cost: 400 + init_stock: 680 + price: 400 + service_level: 0.96 + vlt: 1 + SKU998: + cost: 447 + init_stock: 1100 + price: 447 + service_level: 0.96 + vlt: 2 + SKU999: + cost: 79 + init_stock: 460 + price: 79 + service_level: 0.96 + vlt: 2 + - children: + storage: + config: + capacity: 1064900 + unit_storage_cost: 1 + config: + order_cost: 500 + definition_ref: RetailerFacility + name: STORE0 + skus: + SKU0: + constraint: null + cost: 322 + init_stock: 126 + max_stock: 1000 + price: 631 + sale_gamma: 63 + service_level: 0.95 + SKU1: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 284 + init_stock: 176 + max_stock: 1000 + price: 448 + sale_gamma: 22 + service_level: 0.95 + SKU10: + constraint: null + cost: 68 + init_stock: 150 + max_stock: 1000 + price: 116 + sale_gamma: 25 + service_level: 0.95 + SKU100: + constraint: G(low_profit -> low_stock_constraint) + cost: 16 + init_stock: 390 + max_stock: 1000 + price: 23 + sale_gamma: 65 + service_level: 0.95 + SKU101: + constraint: null + cost: 84 + init_stock: 497 + max_stock: 1000 + price: 149 + sale_gamma: 71 + service_level: 0.95 + SKU102: + constraint: null + cost: 328 + init_stock: 558 + max_stock: 1000 + price: 505 + sale_gamma: 93 + service_level: 0.95 + SKU103: + constraint: null + cost: 334 + init_stock: 285 + max_stock: 1000 + price: 601 + sale_gamma: 95 + service_level: 0.95 + SKU104: + constraint: null + cost: 49 + init_stock: 325 + max_stock: 1000 + price: 57 + sale_gamma: 65 + service_level: 0.95 + SKU105: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 110 + init_stock: 285 + max_stock: 1000 + price: 171 + sale_gamma: 57 + service_level: 0.95 + SKU106: + constraint: G(low_profit -> low_stock_constraint) + cost: 251 + init_stock: 584 + max_stock: 1000 + price: 454 + sale_gamma: 73 + service_level: 0.95 + SKU107: + constraint: G(stock_constraint) + cost: 423 + init_stock: 348 + max_stock: 1000 + price: 706 + sale_gamma: 87 + service_level: 0.95 + SKU108: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 458 + init_stock: 368 + max_stock: 1000 + price: 801 + sale_gamma: 92 + service_level: 0.95 + SKU109: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 88 + init_stock: 328 + max_stock: 1000 + price: 98 + sale_gamma: 82 + service_level: 0.95 + SKU11: + constraint: null + cost: 400 + init_stock: 306 + max_stock: 1000 + price: 680 + sale_gamma: 51 + service_level: 0.95 + SKU110: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 66 + init_stock: 112 + max_stock: 1000 + price: 100 + sale_gamma: 14 + service_level: 0.95 + SKU111: + constraint: G(stock_constraint) + cost: 260 + init_stock: 183 + max_stock: 1000 + price: 364 + sale_gamma: 61 + service_level: 0.95 + SKU112: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 61 + init_stock: 475 + max_stock: 1000 + price: 119 + sale_gamma: 95 + service_level: 0.95 + SKU113: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 155 + max_stock: 1000 + price: 678 + sale_gamma: 31 + service_level: 0.95 + SKU114: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 389 + init_stock: 162 + max_stock: 1000 + price: 564 + sale_gamma: 27 + service_level: 0.95 + SKU115: + constraint: G(low_profit -> low_stock_constraint) + cost: 286 + init_stock: 258 + max_stock: 1000 + price: 557 + sale_gamma: 86 + service_level: 0.95 + SKU116: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 496 + init_stock: 288 + max_stock: 1000 + price: 768 + sale_gamma: 72 + service_level: 0.95 + SKU117: + constraint: null + cost: 320 + init_stock: 644 + max_stock: 1000 + price: 374 + sale_gamma: 92 + service_level: 0.95 + SKU118: + constraint: null + cost: 183 + init_stock: 66 + max_stock: 1000 + price: 208 + sale_gamma: 33 + service_level: 0.95 + SKU119: + constraint: null + cost: 209 + init_stock: 256 + max_stock: 1000 + price: 317 + sale_gamma: 32 + service_level: 0.95 + SKU12: + constraint: null + cost: 112 + init_stock: 336 + max_stock: 1000 + price: 210 + sale_gamma: 84 + service_level: 0.95 + SKU120: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 121 + init_stock: 392 + max_stock: 1000 + price: 151 + sale_gamma: 98 + service_level: 0.95 + SKU121: + constraint: null + cost: 40 + init_stock: 510 + max_stock: 1000 + price: 54 + sale_gamma: 85 + service_level: 0.95 + SKU122: + constraint: G(stock_constraint) + cost: 437 + init_stock: 35 + max_stock: 1000 + price: 589 + sale_gamma: 7 + service_level: 0.95 + SKU123: + constraint: G(stock_constraint) + cost: 233 + init_stock: 114 + max_stock: 1000 + price: 326 + sale_gamma: 19 + service_level: 0.95 + SKU124: + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 108 + max_stock: 1000 + price: 298 + sale_gamma: 36 + service_level: 0.95 + SKU125: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 16 + init_stock: 368 + max_stock: 1000 + price: 32 + sale_gamma: 92 + service_level: 0.95 + SKU126: + constraint: null + cost: 36 + init_stock: 156 + max_stock: 1000 + price: 40 + sale_gamma: 39 + service_level: 0.95 + SKU127: + constraint: G(stock_constraint) + cost: 217 + init_stock: 155 + max_stock: 1000 + price: 366 + sale_gamma: 31 + service_level: 0.95 + SKU128: + constraint: null + cost: 165 + init_stock: 95 + max_stock: 1000 + price: 283 + sale_gamma: 19 + service_level: 0.95 + SKU129: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 300 + max_stock: 1000 + price: 203 + sale_gamma: 50 + service_level: 0.95 + SKU13: + constraint: null + cost: 317 + init_stock: 456 + max_stock: 1000 + price: 573 + sale_gamma: 57 + service_level: 0.95 + SKU130: + constraint: null + cost: 348 + init_stock: 784 + max_stock: 1000 + price: 396 + sale_gamma: 98 + service_level: 0.95 + SKU131: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 64 + init_stock: 270 + max_stock: 1000 + price: 110 + sale_gamma: 45 + service_level: 0.95 + SKU132: + constraint: null + cost: 427 + init_stock: 105 + max_stock: 1000 + price: 678 + sale_gamma: 21 + service_level: 0.95 + SKU133: + constraint: null + cost: 224 + init_stock: 145 + max_stock: 1000 + price: 448 + sale_gamma: 29 + service_level: 0.95 + SKU134: + constraint: G(stock_constraint) + cost: 336 + init_stock: 539 + max_stock: 1000 + price: 470 + sale_gamma: 77 + service_level: 0.95 + SKU135: + constraint: null + cost: 153 + init_stock: 500 + max_stock: 1000 + price: 243 + sale_gamma: 100 + service_level: 0.95 + SKU136: + constraint: G(low_profit -> low_stock_constraint) + cost: 199 + init_stock: 497 + max_stock: 1000 + price: 370 + sale_gamma: 71 + service_level: 0.95 + SKU137: + constraint: G(stock_constraint) + cost: 93 + init_stock: 444 + max_stock: 1000 + price: 165 + sale_gamma: 74 + service_level: 0.95 + SKU138: + constraint: G(stock_constraint) + cost: 228 + init_stock: 180 + max_stock: 1000 + price: 253 + sale_gamma: 36 + service_level: 0.95 + SKU139: + constraint: G(stock_constraint) + cost: 207 + init_stock: 144 + max_stock: 1000 + price: 368 + sale_gamma: 24 + service_level: 0.95 + SKU14: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 268 + init_stock: 372 + max_stock: 1000 + price: 305 + sale_gamma: 62 + service_level: 0.95 + SKU140: + constraint: null + cost: 261 + init_stock: 238 + max_stock: 1000 + price: 357 + sale_gamma: 34 + service_level: 0.95 + SKU141: + constraint: null + cost: 190 + init_stock: 164 + max_stock: 1000 + price: 370 + sale_gamma: 41 + service_level: 0.95 + SKU142: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 320 + init_stock: 152 + max_stock: 1000 + price: 428 + sale_gamma: 38 + service_level: 0.95 + SKU143: + constraint: G(stock_constraint) + cost: 318 + init_stock: 182 + max_stock: 1000 + price: 566 + sale_gamma: 26 + service_level: 0.95 + SKU144: + constraint: null + cost: 400 + init_stock: 24 + max_stock: 1000 + price: 716 + sale_gamma: 12 + service_level: 0.95 + SKU145: + constraint: null + cost: 399 + init_stock: 328 + max_stock: 1000 + price: 774 + sale_gamma: 82 + service_level: 0.95 + SKU146: + constraint: null + cost: 177 + init_stock: 192 + max_stock: 1000 + price: 194 + sale_gamma: 48 + service_level: 0.95 + SKU147: + constraint: G(low_profit -> low_stock_constraint) + cost: 472 + init_stock: 392 + max_stock: 1000 + price: 840 + sale_gamma: 56 + service_level: 0.95 + SKU148: + constraint: G(stock_constraint) + cost: 313 + init_stock: 308 + max_stock: 1000 + price: 566 + sale_gamma: 77 + service_level: 0.95 + SKU149: + constraint: G(low_profit -> low_stock_constraint) + cost: 357 + init_stock: 539 + max_stock: 1000 + price: 549 + sale_gamma: 77 + service_level: 0.95 + SKU15: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 25 + max_stock: 1000 + price: 58 + sale_gamma: 5 + service_level: 0.95 + SKU150: + constraint: null + cost: 106 + init_stock: 210 + max_stock: 1000 + price: 159 + sale_gamma: 70 + service_level: 0.95 + SKU151: + constraint: G(low_profit -> low_stock_constraint) + cost: 223 + init_stock: 146 + max_stock: 1000 + price: 370 + sale_gamma: 73 + service_level: 0.95 + SKU152: + constraint: null + cost: 10 + init_stock: 408 + max_stock: 1000 + price: 14 + sale_gamma: 51 + service_level: 0.95 + SKU153: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 441 + init_stock: 372 + max_stock: 1000 + price: 538 + sale_gamma: 62 + service_level: 0.95 + SKU154: + constraint: null + cost: 77 + init_stock: 680 + max_stock: 1000 + price: 135 + sale_gamma: 85 + service_level: 0.95 + SKU155: + constraint: G(low_profit -> low_stock_constraint) + cost: 422 + init_stock: 159 + max_stock: 1000 + price: 641 + sale_gamma: 53 + service_level: 0.95 + SKU156: + constraint: null + cost: 10 + init_stock: 36 + max_stock: 1000 + price: 16 + sale_gamma: 12 + service_level: 0.95 + SKU157: + constraint: null + cost: 410 + init_stock: 525 + max_stock: 1000 + price: 594 + sale_gamma: 75 + service_level: 0.95 + SKU158: + constraint: null + cost: 145 + init_stock: 486 + max_stock: 1000 + price: 275 + sale_gamma: 81 + service_level: 0.95 + SKU159: + constraint: G(stock_constraint) + cost: 193 + init_stock: 100 + max_stock: 1000 + price: 368 + sale_gamma: 25 + service_level: 0.95 + SKU16: + constraint: null + cost: 175 + init_stock: 464 + max_stock: 1000 + price: 344 + sale_gamma: 58 + service_level: 0.95 + SKU160: + constraint: G(low_profit -> low_stock_constraint) + cost: 459 + init_stock: 510 + max_stock: 1000 + price: 876 + sale_gamma: 85 + service_level: 0.95 + SKU161: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 239 + init_stock: 230 + max_stock: 1000 + price: 478 + sale_gamma: 46 + service_level: 0.95 + SKU162: + constraint: null + cost: 158 + init_stock: 30 + max_stock: 1000 + price: 290 + sale_gamma: 5 + service_level: 0.95 + SKU163: + constraint: G(stock_constraint) + cost: 486 + init_stock: 273 + max_stock: 1000 + price: 704 + sale_gamma: 39 + service_level: 0.95 + SKU164: + constraint: null + cost: 496 + init_stock: 500 + max_stock: 1000 + price: 615 + sale_gamma: 100 + service_level: 0.95 + SKU165: + constraint: G(stock_constraint) + cost: 274 + init_stock: 231 + max_stock: 1000 + price: 548 + sale_gamma: 33 + service_level: 0.95 + SKU166: + constraint: null + cost: 79 + init_stock: 178 + max_stock: 1000 + price: 137 + sale_gamma: 89 + service_level: 0.95 + SKU167: + constraint: G(stock_constraint) + cost: 443 + init_stock: 52 + max_stock: 1000 + price: 854 + sale_gamma: 13 + service_level: 0.95 + SKU168: + constraint: null + cost: 357 + init_stock: 522 + max_stock: 1000 + price: 546 + sale_gamma: 87 + service_level: 0.95 + SKU169: + constraint: null + cost: 369 + init_stock: 686 + max_stock: 1000 + price: 505 + sale_gamma: 98 + service_level: 0.95 + SKU17: + constraint: null + cost: 346 + init_stock: 18 + max_stock: 1000 + price: 553 + sale_gamma: 9 + service_level: 0.95 + SKU170: + constraint: null + cost: 68 + init_stock: 275 + max_stock: 1000 + price: 113 + sale_gamma: 55 + service_level: 0.95 + SKU171: + constraint: G(low_profit -> low_stock_constraint) + cost: 398 + init_stock: 380 + max_stock: 1000 + price: 704 + sale_gamma: 76 + service_level: 0.95 + SKU172: + constraint: null + cost: 200 + init_stock: 426 + max_stock: 1000 + price: 222 + sale_gamma: 71 + service_level: 0.95 + SKU173: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 175 + init_stock: 480 + max_stock: 1000 + price: 264 + sale_gamma: 96 + service_level: 0.95 + SKU174: + constraint: null + cost: 291 + init_stock: 380 + max_stock: 1000 + price: 582 + sale_gamma: 76 + service_level: 0.95 + SKU175: + constraint: null + cost: 84 + init_stock: 225 + max_stock: 1000 + price: 140 + sale_gamma: 75 + service_level: 0.95 + SKU176: + constraint: G(stock_constraint) + cost: 407 + init_stock: 330 + max_stock: 1000 + price: 610 + sale_gamma: 66 + service_level: 0.95 + SKU177: + constraint: null + cost: 257 + init_stock: 155 + max_stock: 1000 + price: 346 + sale_gamma: 31 + service_level: 0.95 + SKU178: + constraint: null + cost: 423 + init_stock: 20 + max_stock: 1000 + price: 499 + sale_gamma: 5 + service_level: 0.95 + SKU179: + constraint: null + cost: 497 + init_stock: 415 + max_stock: 1000 + price: 690 + sale_gamma: 83 + service_level: 0.95 + SKU18: + constraint: null + cost: 258 + init_stock: 405 + max_stock: 1000 + price: 387 + sale_gamma: 81 + service_level: 0.95 + SKU180: + constraint: null + cost: 217 + init_stock: 165 + max_stock: 1000 + price: 297 + sale_gamma: 55 + service_level: 0.95 + SKU181: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 300 + max_stock: 1000 + price: 214 + sale_gamma: 60 + service_level: 0.95 + SKU182: + constraint: null + cost: 437 + init_stock: 693 + max_stock: 1000 + price: 764 + sale_gamma: 99 + service_level: 0.95 + SKU183: + constraint: null + cost: 145 + init_stock: 56 + max_stock: 1000 + price: 178 + sale_gamma: 8 + service_level: 0.95 + SKU184: + constraint: null + cost: 73 + init_stock: 72 + max_stock: 1000 + price: 100 + sale_gamma: 24 + service_level: 0.95 + SKU185: + constraint: null + cost: 10 + init_stock: 360 + max_stock: 1000 + price: 14 + sale_gamma: 90 + service_level: 0.95 + SKU186: + constraint: null + cost: 359 + init_stock: 66 + max_stock: 1000 + price: 649 + sale_gamma: 22 + service_level: 0.95 + SKU187: + constraint: G(low_profit -> low_stock_constraint) + cost: 177 + init_stock: 120 + max_stock: 1000 + price: 249 + sale_gamma: 30 + service_level: 0.95 + SKU188: + constraint: null + cost: 391 + init_stock: 348 + max_stock: 1000 + price: 586 + sale_gamma: 87 + service_level: 0.95 + SKU189: + constraint: G(low_profit -> low_stock_constraint) + cost: 358 + init_stock: 210 + max_stock: 1000 + price: 400 + sale_gamma: 35 + service_level: 0.95 + SKU19: + constraint: G(stock_constraint) + cost: 477 + init_stock: 364 + max_stock: 1000 + price: 791 + sale_gamma: 91 + service_level: 0.95 + SKU190: + constraint: null + cost: 113 + init_stock: 102 + max_stock: 1000 + price: 153 + sale_gamma: 17 + service_level: 0.95 + SKU191: + constraint: G(stock_constraint) + cost: 473 + init_stock: 324 + max_stock: 1000 + price: 638 + sale_gamma: 54 + service_level: 0.95 + SKU192: + constraint: null + cost: 415 + init_stock: 244 + max_stock: 1000 + price: 539 + sale_gamma: 61 + service_level: 0.95 + SKU193: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 207 + init_stock: 120 + max_stock: 1000 + price: 353 + sale_gamma: 30 + service_level: 0.95 + SKU194: + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 30 + max_stock: 1000 + price: 803 + sale_gamma: 5 + service_level: 0.95 + SKU195: + constraint: G(low_profit -> low_stock_constraint) + cost: 218 + init_stock: 93 + max_stock: 1000 + price: 248 + sale_gamma: 31 + service_level: 0.95 + SKU196: + constraint: null + cost: 49 + init_stock: 544 + max_stock: 1000 + price: 62 + sale_gamma: 68 + service_level: 0.95 + SKU197: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 303 + init_stock: 285 + max_stock: 1000 + price: 509 + sale_gamma: 57 + service_level: 0.95 + SKU198: + constraint: G(low_profit -> low_stock_constraint) + cost: 169 + init_stock: 378 + max_stock: 1000 + price: 307 + sale_gamma: 54 + service_level: 0.95 + SKU199: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 138 + max_stock: 1000 + price: 794 + sale_gamma: 23 + service_level: 0.95 + SKU2: + constraint: null + cost: 331 + init_stock: 350 + max_stock: 1000 + price: 499 + sale_gamma: 70 + service_level: 0.95 + SKU20: + constraint: G(stock_constraint) + cost: 335 + init_stock: 200 + max_stock: 1000 + price: 649 + sale_gamma: 25 + service_level: 0.95 + SKU200: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 65 + init_stock: 150 + max_stock: 1000 + price: 85 + sale_gamma: 25 + service_level: 0.95 + SKU201: + constraint: G(stock_constraint) + cost: 104 + init_stock: 354 + max_stock: 1000 + price: 161 + sale_gamma: 59 + service_level: 0.95 + SKU202: + constraint: null + cost: 142 + init_stock: 365 + max_stock: 1000 + price: 222 + sale_gamma: 73 + service_level: 0.95 + SKU203: + constraint: null + cost: 440 + init_stock: 328 + max_stock: 1000 + price: 677 + sale_gamma: 82 + service_level: 0.95 + SKU204: + constraint: null + cost: 489 + init_stock: 188 + max_stock: 1000 + price: 831 + sale_gamma: 47 + service_level: 0.95 + SKU205: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 130 + init_stock: 500 + max_stock: 1000 + price: 175 + sale_gamma: 100 + service_level: 0.95 + SKU206: + constraint: null + cost: 335 + init_stock: 55 + max_stock: 1000 + price: 552 + sale_gamma: 11 + service_level: 0.95 + SKU207: + constraint: G(low_profit -> low_stock_constraint) + cost: 140 + init_stock: 240 + max_stock: 1000 + price: 170 + sale_gamma: 80 + service_level: 0.95 + SKU208: + constraint: null + cost: 491 + init_stock: 308 + max_stock: 1000 + price: 927 + sale_gamma: 77 + service_level: 0.95 + SKU209: + constraint: G(stock_constraint) + cost: 179 + init_stock: 120 + max_stock: 1000 + price: 322 + sale_gamma: 20 + service_level: 0.95 + SKU21: + constraint: null + cost: 123 + init_stock: 400 + max_stock: 1000 + price: 225 + sale_gamma: 100 + service_level: 0.95 + SKU210: + constraint: null + cost: 404 + init_stock: 345 + max_stock: 1000 + price: 468 + sale_gamma: 69 + service_level: 0.95 + SKU211: + constraint: null + cost: 174 + init_stock: 364 + max_stock: 1000 + price: 226 + sale_gamma: 91 + service_level: 0.95 + SKU212: + constraint: null + cost: 405 + init_stock: 632 + max_stock: 1000 + price: 534 + sale_gamma: 79 + service_level: 0.95 + SKU213: + constraint: null + cost: 121 + init_stock: 192 + max_stock: 1000 + price: 229 + sale_gamma: 64 + service_level: 0.95 + SKU214: + constraint: G(stock_constraint) + cost: 101 + init_stock: 60 + max_stock: 1000 + price: 144 + sale_gamma: 10 + service_level: 0.95 + SKU215: + constraint: null + cost: 419 + init_stock: 94 + max_stock: 1000 + price: 469 + sale_gamma: 47 + service_level: 0.95 + SKU216: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 330 + init_stock: 92 + max_stock: 1000 + price: 432 + sale_gamma: 23 + service_level: 0.95 + SKU217: + constraint: null + cost: 284 + init_stock: 455 + max_stock: 1000 + price: 312 + sale_gamma: 65 + service_level: 0.95 + SKU218: + constraint: G(low_profit -> low_stock_constraint) + cost: 205 + init_stock: 354 + max_stock: 1000 + price: 397 + sale_gamma: 59 + service_level: 0.95 + SKU219: + constraint: G(stock_constraint) + cost: 92 + init_stock: 368 + max_stock: 1000 + price: 135 + sale_gamma: 46 + service_level: 0.95 + SKU22: + constraint: null + cost: 493 + init_stock: 264 + max_stock: 1000 + price: 986 + sale_gamma: 66 + service_level: 0.95 + SKU220: + constraint: null + cost: 387 + init_stock: 435 + max_stock: 1000 + price: 572 + sale_gamma: 87 + service_level: 0.95 + SKU221: + constraint: null + cost: 39 + init_stock: 468 + max_stock: 1000 + price: 48 + sale_gamma: 78 + service_level: 0.95 + SKU222: + constraint: G(low_profit -> low_stock_constraint) + cost: 115 + init_stock: 180 + max_stock: 1000 + price: 210 + sale_gamma: 36 + service_level: 0.95 + SKU223: + constraint: G(low_profit -> low_stock_constraint) + cost: 196 + init_stock: 36 + max_stock: 1000 + price: 286 + sale_gamma: 12 + service_level: 0.95 + SKU224: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 387 + init_stock: 25 + max_stock: 1000 + price: 661 + sale_gamma: 5 + service_level: 0.95 + SKU225: + constraint: null + cost: 164 + init_stock: 116 + max_stock: 1000 + price: 216 + sale_gamma: 29 + service_level: 0.95 + SKU226: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 206 + init_stock: 65 + max_stock: 1000 + price: 350 + sale_gamma: 13 + service_level: 0.95 + SKU227: + constraint: G(low_profit -> low_stock_constraint) + cost: 479 + init_stock: 144 + max_stock: 1000 + price: 895 + sale_gamma: 24 + service_level: 0.95 + SKU228: + constraint: null + cost: 14 + init_stock: 180 + max_stock: 1000 + price: 21 + sale_gamma: 90 + service_level: 0.95 + SKU229: + constraint: null + cost: 472 + init_stock: 176 + max_stock: 1000 + price: 708 + sale_gamma: 44 + service_level: 0.95 + SKU23: + constraint: null + cost: 387 + init_stock: 210 + max_stock: 1000 + price: 700 + sale_gamma: 42 + service_level: 0.95 + SKU230: + constraint: null + cost: 241 + init_stock: 138 + max_stock: 1000 + price: 436 + sale_gamma: 23 + service_level: 0.95 + SKU231: + constraint: G(stock_constraint) + cost: 48 + init_stock: 486 + max_stock: 1000 + price: 96 + sale_gamma: 81 + service_level: 0.95 + SKU232: + constraint: G(low_profit -> low_stock_constraint) + cost: 224 + init_stock: 480 + max_stock: 1000 + price: 338 + sale_gamma: 96 + service_level: 0.95 + SKU233: + constraint: G(low_profit -> low_stock_constraint) + cost: 360 + init_stock: 300 + max_stock: 1000 + price: 428 + sale_gamma: 75 + service_level: 0.95 + SKU234: + constraint: null + cost: 287 + init_stock: 25 + max_stock: 1000 + price: 387 + sale_gamma: 5 + service_level: 0.95 + SKU235: + constraint: G(low_profit -> low_stock_constraint) + cost: 24 + init_stock: 228 + max_stock: 1000 + price: 32 + sale_gamma: 57 + service_level: 0.95 + SKU236: + constraint: G(low_profit -> low_stock_constraint) + cost: 155 + init_stock: 165 + max_stock: 1000 + price: 289 + sale_gamma: 55 + service_level: 0.95 + SKU237: + constraint: null + cost: 433 + init_stock: 270 + max_stock: 1000 + price: 779 + sale_gamma: 45 + service_level: 0.95 + SKU238: + constraint: G(stock_constraint) + cost: 64 + init_stock: 264 + max_stock: 1000 + price: 112 + sale_gamma: 66 + service_level: 0.95 + SKU239: + constraint: null + cost: 103 + init_stock: 490 + max_stock: 1000 + price: 139 + sale_gamma: 98 + service_level: 0.95 + SKU24: + constraint: null + cost: 97 + init_stock: 329 + max_stock: 1000 + price: 114 + sale_gamma: 47 + service_level: 0.95 + SKU240: + constraint: null + cost: 373 + init_stock: 188 + max_stock: 1000 + price: 738 + sale_gamma: 47 + service_level: 0.95 + SKU241: + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 213 + max_stock: 1000 + price: 860 + sale_gamma: 71 + service_level: 0.95 + SKU242: + constraint: null + cost: 17 + init_stock: 88 + max_stock: 1000 + price: 29 + sale_gamma: 44 + service_level: 0.95 + SKU243: + constraint: null + cost: 352 + init_stock: 84 + max_stock: 1000 + price: 394 + sale_gamma: 14 + service_level: 0.95 + SKU244: + constraint: null + cost: 174 + init_stock: 410 + max_stock: 1000 + price: 226 + sale_gamma: 82 + service_level: 0.95 + SKU245: + constraint: G(low_profit -> low_stock_constraint) + cost: 404 + init_stock: 198 + max_stock: 1000 + price: 525 + sale_gamma: 66 + service_level: 0.95 + SKU246: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 300 + init_stock: 210 + max_stock: 1000 + price: 474 + sale_gamma: 30 + service_level: 0.95 + SKU247: + constraint: null + cost: 386 + init_stock: 210 + max_stock: 1000 + price: 497 + sale_gamma: 35 + service_level: 0.95 + SKU248: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 355 + init_stock: 174 + max_stock: 1000 + price: 539 + sale_gamma: 29 + service_level: 0.95 + SKU249: + constraint: null + cost: 356 + init_stock: 100 + max_stock: 1000 + price: 544 + sale_gamma: 25 + service_level: 0.95 + SKU25: + constraint: G(stock_constraint) + cost: 161 + init_stock: 35 + max_stock: 1000 + price: 249 + sale_gamma: 5 + service_level: 0.95 + SKU250: + constraint: null + cost: 127 + init_stock: 324 + max_stock: 1000 + price: 186 + sale_gamma: 54 + service_level: 0.95 + SKU251: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 344 + init_stock: 244 + max_stock: 1000 + price: 543 + sale_gamma: 61 + service_level: 0.95 + SKU252: + constraint: null + cost: 181 + init_stock: 415 + max_stock: 1000 + price: 334 + sale_gamma: 83 + service_level: 0.95 + SKU253: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 448 + init_stock: 32 + max_stock: 1000 + price: 864 + sale_gamma: 16 + service_level: 0.95 + SKU254: + constraint: null + cost: 484 + init_stock: 184 + max_stock: 1000 + price: 696 + sale_gamma: 46 + service_level: 0.95 + SKU255: + constraint: null + cost: 290 + init_stock: 335 + max_stock: 1000 + price: 568 + sale_gamma: 67 + service_level: 0.95 + SKU256: + constraint: null + cost: 91 + init_stock: 360 + max_stock: 1000 + price: 167 + sale_gamma: 72 + service_level: 0.95 + SKU257: + constraint: null + cost: 348 + init_stock: 171 + max_stock: 1000 + price: 497 + sale_gamma: 57 + service_level: 0.95 + SKU258: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 258 + max_stock: 1000 + price: 689 + sale_gamma: 43 + service_level: 0.95 + SKU259: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 207 + max_stock: 1000 + price: 559 + sale_gamma: 69 + service_level: 0.95 + SKU26: + constraint: null + cost: 229 + init_stock: 126 + max_stock: 1000 + price: 357 + sale_gamma: 63 + service_level: 0.95 + SKU260: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 364 + max_stock: 1000 + price: 866 + sale_gamma: 52 + service_level: 0.95 + SKU261: + constraint: null + cost: 368 + init_stock: 66 + max_stock: 1000 + price: 688 + sale_gamma: 22 + service_level: 0.95 + SKU262: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 332 + init_stock: 390 + max_stock: 1000 + price: 385 + sale_gamma: 78 + service_level: 0.95 + SKU263: + constraint: G(stock_constraint) + cost: 189 + init_stock: 444 + max_stock: 1000 + price: 372 + sale_gamma: 74 + service_level: 0.95 + SKU264: + constraint: null + cost: 361 + init_stock: 158 + max_stock: 1000 + price: 566 + sale_gamma: 79 + service_level: 0.95 + SKU265: + constraint: null + cost: 286 + init_stock: 236 + max_stock: 1000 + price: 434 + sale_gamma: 59 + service_level: 0.95 + SKU266: + constraint: null + cost: 128 + init_stock: 282 + max_stock: 1000 + price: 172 + sale_gamma: 47 + service_level: 0.95 + SKU267: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 77 + init_stock: 320 + max_stock: 1000 + price: 130 + sale_gamma: 80 + service_level: 0.95 + SKU268: + constraint: null + cost: 221 + init_stock: 356 + max_stock: 1000 + price: 362 + sale_gamma: 89 + service_level: 0.95 + SKU269: + constraint: G(low_profit -> low_stock_constraint) + cost: 126 + init_stock: 132 + max_stock: 1000 + price: 175 + sale_gamma: 44 + service_level: 0.95 + SKU27: + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 448 + max_stock: 1000 + price: 728 + sale_gamma: 56 + service_level: 0.95 + SKU270: + constraint: null + cost: 182 + init_stock: 370 + max_stock: 1000 + price: 263 + sale_gamma: 74 + service_level: 0.95 + SKU271: + constraint: G(stock_constraint) + cost: 230 + init_stock: 54 + max_stock: 1000 + price: 266 + sale_gamma: 18 + service_level: 0.95 + SKU272: + constraint: null + cost: 366 + init_stock: 51 + max_stock: 1000 + price: 625 + sale_gamma: 17 + service_level: 0.95 + SKU273: + constraint: null + cost: 421 + init_stock: 72 + max_stock: 1000 + price: 614 + sale_gamma: 18 + service_level: 0.95 + SKU274: + constraint: null + cost: 29 + init_stock: 308 + max_stock: 1000 + price: 42 + sale_gamma: 77 + service_level: 0.95 + SKU275: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 50 + init_stock: 144 + max_stock: 1000 + price: 76 + sale_gamma: 48 + service_level: 0.95 + SKU276: + constraint: G(stock_constraint) + cost: 163 + init_stock: 378 + max_stock: 1000 + price: 299 + sale_gamma: 54 + service_level: 0.95 + SKU277: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 82 + max_stock: 1000 + price: 507 + sale_gamma: 41 + service_level: 0.95 + SKU278: + constraint: null + cost: 105 + init_stock: 84 + max_stock: 1000 + price: 140 + sale_gamma: 12 + service_level: 0.95 + SKU279: + constraint: G(stock_constraint) + cost: 51 + init_stock: 273 + max_stock: 1000 + price: 61 + sale_gamma: 39 + service_level: 0.95 + SKU28: + constraint: G(stock_constraint) + cost: 208 + init_stock: 235 + max_stock: 1000 + price: 295 + sale_gamma: 47 + service_level: 0.95 + SKU280: + constraint: G(low_profit -> low_stock_constraint) + cost: 295 + init_stock: 198 + max_stock: 1000 + price: 436 + sale_gamma: 33 + service_level: 0.95 + SKU281: + constraint: null + cost: 395 + init_stock: 375 + max_stock: 1000 + price: 592 + sale_gamma: 75 + service_level: 0.95 + SKU282: + constraint: null + cost: 63 + init_stock: 184 + max_stock: 1000 + price: 112 + sale_gamma: 46 + service_level: 0.95 + SKU283: + constraint: null + cost: 392 + init_stock: 736 + max_stock: 1000 + price: 490 + sale_gamma: 92 + service_level: 0.95 + SKU284: + constraint: G(stock_constraint) + cost: 344 + init_stock: 268 + max_stock: 1000 + price: 643 + sale_gamma: 67 + service_level: 0.95 + SKU285: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 133 + init_stock: 546 + max_stock: 1000 + price: 148 + sale_gamma: 91 + service_level: 0.95 + SKU286: + constraint: null + cost: 337 + init_stock: 156 + max_stock: 1000 + price: 626 + sale_gamma: 39 + service_level: 0.95 + SKU287: + constraint: null + cost: 375 + init_stock: 224 + max_stock: 1000 + price: 660 + sale_gamma: 56 + service_level: 0.95 + SKU288: + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 190 + max_stock: 1000 + price: 352 + sale_gamma: 38 + service_level: 0.95 + SKU289: + constraint: G(stock_constraint) + cost: 67 + init_stock: 62 + max_stock: 1000 + price: 91 + sale_gamma: 31 + service_level: 0.95 + SKU29: + constraint: null + cost: 245 + init_stock: 174 + max_stock: 1000 + price: 475 + sale_gamma: 58 + service_level: 0.95 + SKU290: + constraint: null + cost: 438 + init_stock: 268 + max_stock: 1000 + price: 779 + sale_gamma: 67 + service_level: 0.95 + SKU291: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 122 + max_stock: 1000 + price: 126 + sale_gamma: 61 + service_level: 0.95 + SKU292: + constraint: null + cost: 186 + init_stock: 15 + max_stock: 1000 + price: 344 + sale_gamma: 5 + service_level: 0.95 + SKU293: + constraint: G(stock_constraint) + cost: 162 + init_stock: 385 + max_stock: 1000 + price: 288 + sale_gamma: 55 + service_level: 0.95 + SKU294: + constraint: null + cost: 409 + init_stock: 45 + max_stock: 1000 + price: 605 + sale_gamma: 9 + service_level: 0.95 + SKU295: + constraint: null + cost: 435 + init_stock: 651 + max_stock: 1000 + price: 674 + sale_gamma: 93 + service_level: 0.95 + SKU296: + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 552 + max_stock: 1000 + price: 580 + sale_gamma: 92 + service_level: 0.95 + SKU297: + constraint: null + cost: 298 + init_stock: 228 + max_stock: 1000 + price: 333 + sale_gamma: 38 + service_level: 0.95 + SKU298: + constraint: null + cost: 286 + init_stock: 140 + max_stock: 1000 + price: 500 + sale_gamma: 35 + service_level: 0.95 + SKU299: + constraint: G(stock_constraint) + cost: 367 + init_stock: 255 + max_stock: 1000 + price: 451 + sale_gamma: 51 + service_level: 0.95 + SKU3: + constraint: G(stock_constraint) + cost: 405 + init_stock: 48 + max_stock: 1000 + price: 733 + sale_gamma: 12 + service_level: 0.95 + SKU30: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 359 + init_stock: 276 + max_stock: 1000 + price: 581 + sale_gamma: 69 + service_level: 0.95 + SKU300: + constraint: G(low_profit -> low_stock_constraint) + cost: 376 + init_stock: 290 + max_stock: 1000 + price: 428 + sale_gamma: 58 + service_level: 0.95 + SKU301: + constraint: null + cost: 433 + init_stock: 581 + max_stock: 1000 + price: 857 + sale_gamma: 83 + service_level: 0.95 + SKU302: + constraint: null + cost: 184 + init_stock: 44 + max_stock: 1000 + price: 322 + sale_gamma: 11 + service_level: 0.95 + SKU303: + constraint: null + cost: 246 + init_stock: 188 + max_stock: 1000 + price: 487 + sale_gamma: 94 + service_level: 0.95 + SKU304: + constraint: G(low_profit -> low_stock_constraint) + cost: 419 + init_stock: 138 + max_stock: 1000 + price: 632 + sale_gamma: 23 + service_level: 0.95 + SKU305: + constraint: null + cost: 495 + init_stock: 500 + max_stock: 1000 + price: 772 + sale_gamma: 100 + service_level: 0.95 + SKU306: + constraint: null + cost: 479 + init_stock: 126 + max_stock: 1000 + price: 852 + sale_gamma: 42 + service_level: 0.95 + SKU307: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 210 + init_stock: 156 + max_stock: 1000 + price: 371 + sale_gamma: 78 + service_level: 0.95 + SKU308: + constraint: null + cost: 208 + init_stock: 25 + max_stock: 1000 + price: 262 + sale_gamma: 5 + service_level: 0.95 + SKU309: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 101 + init_stock: 460 + max_stock: 1000 + price: 156 + sale_gamma: 92 + service_level: 0.95 + SKU31: + constraint: null + cost: 69 + init_stock: 280 + max_stock: 1000 + price: 120 + sale_gamma: 56 + service_level: 0.95 + SKU310: + constraint: null + cost: 234 + init_stock: 352 + max_stock: 1000 + price: 294 + sale_gamma: 44 + service_level: 0.95 + SKU311: + constraint: null + cost: 306 + init_stock: 400 + max_stock: 1000 + price: 437 + sale_gamma: 80 + service_level: 0.95 + SKU312: + constraint: G(stock_constraint) + cost: 291 + init_stock: 75 + max_stock: 1000 + price: 392 + sale_gamma: 25 + service_level: 0.95 + SKU313: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 324 + init_stock: 190 + max_stock: 1000 + price: 492 + sale_gamma: 38 + service_level: 0.95 + SKU314: + constraint: G(stock_constraint) + cost: 404 + init_stock: 116 + max_stock: 1000 + price: 456 + sale_gamma: 29 + service_level: 0.95 + SKU315: + constraint: G(stock_constraint) + cost: 471 + init_stock: 504 + max_stock: 1000 + price: 885 + sale_gamma: 84 + service_level: 0.95 + SKU316: + constraint: null + cost: 202 + init_stock: 90 + max_stock: 1000 + price: 282 + sale_gamma: 18 + service_level: 0.95 + SKU317: + constraint: null + cost: 216 + init_stock: 168 + max_stock: 1000 + price: 352 + sale_gamma: 24 + service_level: 0.95 + SKU318: + constraint: null + cost: 278 + init_stock: 255 + max_stock: 1000 + price: 350 + sale_gamma: 85 + service_level: 0.95 + SKU319: + constraint: null + cost: 257 + init_stock: 371 + max_stock: 1000 + price: 454 + sale_gamma: 53 + service_level: 0.95 + SKU32: + constraint: null + cost: 336 + init_stock: 110 + max_stock: 1000 + price: 581 + sale_gamma: 22 + service_level: 0.95 + SKU320: + constraint: null + cost: 196 + init_stock: 156 + max_stock: 1000 + price: 258 + sale_gamma: 39 + service_level: 0.95 + SKU321: + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 96 + max_stock: 1000 + price: 105 + sale_gamma: 16 + service_level: 0.95 + SKU322: + constraint: null + cost: 240 + init_stock: 600 + max_stock: 1000 + price: 453 + sale_gamma: 100 + service_level: 0.95 + SKU323: + constraint: G(stock_constraint) + cost: 66 + init_stock: 195 + max_stock: 1000 + price: 74 + sale_gamma: 39 + service_level: 0.95 + SKU324: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 442 + init_stock: 465 + max_stock: 1000 + price: 804 + sale_gamma: 93 + service_level: 0.95 + SKU325: + constraint: null + cost: 453 + init_stock: 345 + max_stock: 1000 + price: 579 + sale_gamma: 69 + service_level: 0.95 + SKU326: + constraint: null + cost: 345 + init_stock: 72 + max_stock: 1000 + price: 538 + sale_gamma: 24 + service_level: 0.95 + SKU327: + constraint: G(low_profit -> low_stock_constraint) + cost: 457 + init_stock: 84 + max_stock: 1000 + price: 749 + sale_gamma: 14 + service_level: 0.95 + SKU328: + constraint: null + cost: 390 + init_stock: 90 + max_stock: 1000 + price: 487 + sale_gamma: 45 + service_level: 0.95 + SKU329: + constraint: null + cost: 67 + init_stock: 84 + max_stock: 1000 + price: 126 + sale_gamma: 42 + service_level: 0.95 + SKU33: + constraint: G(low_profit -> low_stock_constraint) + cost: 416 + init_stock: 552 + max_stock: 1000 + price: 465 + sale_gamma: 92 + service_level: 0.95 + SKU330: + constraint: null + cost: 306 + init_stock: 273 + max_stock: 1000 + price: 385 + sale_gamma: 39 + service_level: 0.95 + SKU331: + constraint: G(low_profit -> low_stock_constraint) + cost: 138 + init_stock: 164 + max_stock: 1000 + price: 180 + sale_gamma: 41 + service_level: 0.95 + SKU332: + constraint: null + cost: 390 + init_stock: 288 + max_stock: 1000 + price: 670 + sale_gamma: 96 + service_level: 0.95 + SKU333: + constraint: G(stock_constraint) + cost: 485 + init_stock: 318 + max_stock: 1000 + price: 800 + sale_gamma: 53 + service_level: 0.95 + SKU334: + constraint: null + cost: 183 + init_stock: 114 + max_stock: 1000 + price: 245 + sale_gamma: 57 + service_level: 0.95 + SKU335: + constraint: null + cost: 80 + init_stock: 243 + max_stock: 1000 + price: 141 + sale_gamma: 81 + service_level: 0.95 + SKU336: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 56 + max_stock: 1000 + price: 565 + sale_gamma: 28 + service_level: 0.95 + SKU337: + constraint: null + cost: 245 + init_stock: 174 + max_stock: 1000 + price: 394 + sale_gamma: 29 + service_level: 0.95 + SKU338: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 87 + init_stock: 324 + max_stock: 1000 + price: 150 + sale_gamma: 81 + service_level: 0.95 + SKU339: + constraint: G(stock_constraint) + cost: 265 + init_stock: 441 + max_stock: 1000 + price: 368 + sale_gamma: 63 + service_level: 0.95 + SKU34: + constraint: null + cost: 95 + init_stock: 91 + max_stock: 1000 + price: 135 + sale_gamma: 13 + service_level: 0.95 + SKU340: + constraint: null + cost: 480 + init_stock: 435 + max_stock: 1000 + price: 633 + sale_gamma: 87 + service_level: 0.95 + SKU341: + constraint: G(low_profit -> low_stock_constraint) + cost: 462 + init_stock: 280 + max_stock: 1000 + price: 924 + sale_gamma: 70 + service_level: 0.95 + SKU342: + constraint: null + cost: 144 + init_stock: 72 + max_stock: 1000 + price: 216 + sale_gamma: 9 + service_level: 0.95 + SKU343: + constraint: null + cost: 310 + init_stock: 90 + max_stock: 1000 + price: 589 + sale_gamma: 15 + service_level: 0.95 + SKU344: + constraint: null + cost: 357 + init_stock: 348 + max_stock: 1000 + price: 442 + sale_gamma: 87 + service_level: 0.95 + SKU345: + constraint: null + cost: 442 + init_stock: 267 + max_stock: 1000 + price: 857 + sale_gamma: 89 + service_level: 0.95 + SKU346: + constraint: G(stock_constraint) + cost: 350 + init_stock: 336 + max_stock: 1000 + price: 549 + sale_gamma: 42 + service_level: 0.95 + SKU347: + constraint: G(low_profit -> low_stock_constraint) + cost: 497 + init_stock: 328 + max_stock: 1000 + price: 810 + sale_gamma: 82 + service_level: 0.95 + SKU348: + constraint: G(stock_constraint) + cost: 147 + init_stock: 100 + max_stock: 1000 + price: 277 + sale_gamma: 20 + service_level: 0.95 + SKU349: + constraint: null + cost: 67 + init_stock: 335 + max_stock: 1000 + price: 116 + sale_gamma: 67 + service_level: 0.95 + SKU35: + constraint: null + cost: 267 + init_stock: 430 + max_stock: 1000 + price: 373 + sale_gamma: 86 + service_level: 0.95 + SKU350: + constraint: G(stock_constraint) + cost: 279 + init_stock: 552 + max_stock: 1000 + price: 460 + sale_gamma: 92 + service_level: 0.95 + SKU351: + constraint: null + cost: 155 + init_stock: 335 + max_stock: 1000 + price: 294 + sale_gamma: 67 + service_level: 0.95 + SKU352: + constraint: null + cost: 71 + init_stock: 54 + max_stock: 1000 + price: 100 + sale_gamma: 18 + service_level: 0.95 + SKU353: + constraint: G(stock_constraint) + cost: 253 + init_stock: 465 + max_stock: 1000 + price: 437 + sale_gamma: 93 + service_level: 0.95 + SKU354: + constraint: G(low_profit -> low_stock_constraint) + cost: 396 + init_stock: 395 + max_stock: 1000 + price: 566 + sale_gamma: 79 + service_level: 0.95 + SKU355: + constraint: G(stock_constraint) + cost: 63 + init_stock: 30 + max_stock: 1000 + price: 74 + sale_gamma: 10 + service_level: 0.95 + SKU356: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 87 + max_stock: 1000 + price: 441 + sale_gamma: 29 + service_level: 0.95 + SKU357: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 499 + init_stock: 460 + max_stock: 1000 + price: 868 + sale_gamma: 92 + service_level: 0.95 + SKU358: + constraint: null + cost: 269 + init_stock: 414 + max_stock: 1000 + price: 408 + sale_gamma: 69 + service_level: 0.95 + SKU359: + constraint: G(low_profit -> low_stock_constraint) + cost: 82 + init_stock: 600 + max_stock: 1000 + price: 110 + sale_gamma: 75 + service_level: 0.95 + SKU36: + constraint: null + cost: 105 + init_stock: 30 + max_stock: 1000 + price: 165 + sale_gamma: 10 + service_level: 0.95 + SKU360: + constraint: G(low_profit -> low_stock_constraint) + cost: 484 + init_stock: 75 + max_stock: 1000 + price: 682 + sale_gamma: 25 + service_level: 0.95 + SKU361: + constraint: null + cost: 163 + init_stock: 360 + max_stock: 1000 + price: 288 + sale_gamma: 90 + service_level: 0.95 + SKU362: + constraint: null + cost: 464 + init_stock: 435 + max_stock: 1000 + price: 867 + sale_gamma: 87 + service_level: 0.95 + SKU363: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 390 + max_stock: 1000 + price: 93 + sale_gamma: 78 + service_level: 0.95 + SKU364: + constraint: G(stock_constraint) + cost: 387 + init_stock: 66 + max_stock: 1000 + price: 472 + sale_gamma: 33 + service_level: 0.95 + SKU365: + constraint: null + cost: 158 + init_stock: 581 + max_stock: 1000 + price: 241 + sale_gamma: 83 + service_level: 0.95 + SKU366: + constraint: null + cost: 444 + init_stock: 344 + max_stock: 1000 + price: 639 + sale_gamma: 86 + service_level: 0.95 + SKU367: + constraint: G(low_profit -> low_stock_constraint) + cost: 272 + init_stock: 322 + max_stock: 1000 + price: 304 + sale_gamma: 46 + service_level: 0.95 + SKU368: + constraint: G(stock_constraint) + cost: 472 + init_stock: 198 + max_stock: 1000 + price: 613 + sale_gamma: 33 + service_level: 0.95 + SKU369: + constraint: null + cost: 96 + init_stock: 375 + max_stock: 1000 + price: 155 + sale_gamma: 75 + service_level: 0.95 + SKU37: + constraint: G(low_profit -> low_stock_constraint) + cost: 66 + init_stock: 177 + max_stock: 1000 + price: 110 + sale_gamma: 59 + service_level: 0.95 + SKU370: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 112 + init_stock: 90 + max_stock: 1000 + price: 141 + sale_gamma: 15 + service_level: 0.95 + SKU371: + constraint: null + cost: 328 + init_stock: 25 + max_stock: 1000 + price: 511 + sale_gamma: 5 + service_level: 0.95 + SKU372: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 67 + init_stock: 192 + max_stock: 1000 + price: 134 + sale_gamma: 32 + service_level: 0.95 + SKU373: + constraint: null + cost: 58 + init_stock: 268 + max_stock: 1000 + price: 91 + sale_gamma: 67 + service_level: 0.95 + SKU374: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 38 + init_stock: 216 + max_stock: 1000 + price: 73 + sale_gamma: 54 + service_level: 0.95 + SKU375: + constraint: G(low_profit -> low_stock_constraint) + cost: 283 + init_stock: 432 + max_stock: 1000 + price: 416 + sale_gamma: 72 + service_level: 0.95 + SKU376: + constraint: null + cost: 156 + init_stock: 164 + max_stock: 1000 + price: 291 + sale_gamma: 82 + service_level: 0.95 + SKU377: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 78 + init_stock: 536 + max_stock: 1000 + price: 134 + sale_gamma: 67 + service_level: 0.95 + SKU378: + constraint: G(low_profit -> low_stock_constraint) + cost: 424 + init_stock: 175 + max_stock: 1000 + price: 546 + sale_gamma: 35 + service_level: 0.95 + SKU379: + constraint: null + cost: 11 + init_stock: 196 + max_stock: 1000 + price: 20 + sale_gamma: 49 + service_level: 0.95 + SKU38: + constraint: null + cost: 344 + init_stock: 258 + max_stock: 1000 + price: 567 + sale_gamma: 43 + service_level: 0.95 + SKU380: + constraint: null + cost: 393 + init_stock: 212 + max_stock: 1000 + price: 605 + sale_gamma: 53 + service_level: 0.95 + SKU381: + constraint: null + cost: 476 + init_stock: 18 + max_stock: 1000 + price: 609 + sale_gamma: 6 + service_level: 0.95 + SKU382: + constraint: null + cost: 125 + init_stock: 426 + max_stock: 1000 + price: 177 + sale_gamma: 71 + service_level: 0.95 + SKU383: + constraint: null + cost: 304 + init_stock: 368 + max_stock: 1000 + price: 425 + sale_gamma: 92 + service_level: 0.95 + SKU384: + constraint: null + cost: 320 + init_stock: 54 + max_stock: 1000 + price: 460 + sale_gamma: 9 + service_level: 0.95 + SKU385: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 120 + init_stock: 91 + max_stock: 1000 + price: 184 + sale_gamma: 13 + service_level: 0.95 + SKU386: + constraint: G(stock_constraint) + cost: 295 + init_stock: 124 + max_stock: 1000 + price: 536 + sale_gamma: 31 + service_level: 0.95 + SKU387: + constraint: null + cost: 112 + init_stock: 582 + max_stock: 1000 + price: 154 + sale_gamma: 97 + service_level: 0.95 + SKU388: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 405 + init_stock: 264 + max_stock: 1000 + price: 635 + sale_gamma: 44 + service_level: 0.95 + SKU389: + constraint: G(stock_constraint) + cost: 376 + init_stock: 490 + max_stock: 1000 + price: 699 + sale_gamma: 70 + service_level: 0.95 + SKU39: + constraint: G(low_profit -> low_stock_constraint) + cost: 25 + init_stock: 204 + max_stock: 1000 + price: 45 + sale_gamma: 51 + service_level: 0.95 + SKU390: + constraint: null + cost: 144 + init_stock: 156 + max_stock: 1000 + price: 223 + sale_gamma: 39 + service_level: 0.95 + SKU391: + constraint: G(stock_constraint) + cost: 233 + init_stock: 402 + max_stock: 1000 + price: 403 + sale_gamma: 67 + service_level: 0.95 + SKU392: + constraint: null + cost: 163 + init_stock: 370 + max_stock: 1000 + price: 205 + sale_gamma: 74 + service_level: 0.95 + SKU393: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 402 + max_stock: 1000 + price: 701 + sale_gamma: 67 + service_level: 0.95 + SKU394: + constraint: null + cost: 154 + init_stock: 318 + max_stock: 1000 + price: 252 + sale_gamma: 53 + service_level: 0.95 + SKU395: + constraint: null + cost: 488 + init_stock: 198 + max_stock: 1000 + price: 854 + sale_gamma: 33 + service_level: 0.95 + SKU396: + constraint: null + cost: 333 + init_stock: 427 + max_stock: 1000 + price: 469 + sale_gamma: 61 + service_level: 0.95 + SKU397: + constraint: null + cost: 460 + init_stock: 153 + max_stock: 1000 + price: 791 + sale_gamma: 51 + service_level: 0.95 + SKU398: + constraint: G(stock_constraint) + cost: 234 + init_stock: 174 + max_stock: 1000 + price: 414 + sale_gamma: 58 + service_level: 0.95 + SKU399: + constraint: G(stock_constraint) + cost: 376 + init_stock: 148 + max_stock: 1000 + price: 612 + sale_gamma: 37 + service_level: 0.95 + SKU4: + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 21 + max_stock: 1000 + price: 798 + sale_gamma: 7 + service_level: 0.95 + SKU40: + constraint: null + cost: 490 + init_stock: 594 + max_stock: 1000 + price: 563 + sale_gamma: 99 + service_level: 0.95 + SKU400: + constraint: G(low_profit -> low_stock_constraint) + cost: 445 + init_stock: 420 + max_stock: 1000 + price: 729 + sale_gamma: 84 + service_level: 0.95 + SKU401: + constraint: G(low_profit -> low_stock_constraint) + cost: 410 + init_stock: 312 + max_stock: 1000 + price: 774 + sale_gamma: 78 + service_level: 0.95 + SKU402: + constraint: G(low_profit -> low_stock_constraint) + cost: 86 + init_stock: 35 + max_stock: 1000 + price: 153 + sale_gamma: 7 + service_level: 0.95 + SKU403: + constraint: null + cost: 89 + init_stock: 594 + max_stock: 1000 + price: 133 + sale_gamma: 99 + service_level: 0.95 + SKU404: + constraint: null + cost: 287 + init_stock: 305 + max_stock: 1000 + price: 522 + sale_gamma: 61 + service_level: 0.95 + SKU405: + constraint: G(stock_constraint) + cost: 460 + init_stock: 114 + max_stock: 1000 + price: 584 + sale_gamma: 19 + service_level: 0.95 + SKU406: + constraint: null + cost: 327 + init_stock: 400 + max_stock: 1000 + price: 405 + sale_gamma: 100 + service_level: 0.95 + SKU407: + constraint: null + cost: 26 + init_stock: 184 + max_stock: 1000 + price: 41 + sale_gamma: 46 + service_level: 0.95 + SKU408: + constraint: null + cost: 444 + init_stock: 48 + max_stock: 1000 + price: 754 + sale_gamma: 8 + service_level: 0.95 + SKU409: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 257 + init_stock: 273 + max_stock: 1000 + price: 449 + sale_gamma: 91 + service_level: 0.95 + SKU41: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 141 + init_stock: 553 + max_stock: 1000 + price: 162 + sale_gamma: 79 + service_level: 0.95 + SKU410: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 70 + init_stock: 32 + max_stock: 1000 + price: 88 + sale_gamma: 16 + service_level: 0.95 + SKU411: + constraint: G(stock_constraint) + cost: 210 + init_stock: 380 + max_stock: 1000 + price: 405 + sale_gamma: 95 + service_level: 0.95 + SKU412: + constraint: null + cost: 286 + init_stock: 248 + max_stock: 1000 + price: 414 + sale_gamma: 62 + service_level: 0.95 + SKU413: + constraint: null + cost: 403 + init_stock: 581 + max_stock: 1000 + price: 801 + sale_gamma: 83 + service_level: 0.95 + SKU414: + constraint: G(stock_constraint) + cost: 165 + init_stock: 435 + max_stock: 1000 + price: 229 + sale_gamma: 87 + service_level: 0.95 + SKU415: + constraint: G(stock_constraint) + cost: 291 + init_stock: 184 + max_stock: 1000 + price: 372 + sale_gamma: 23 + service_level: 0.95 + SKU416: + constraint: null + cost: 228 + init_stock: 36 + max_stock: 1000 + price: 373 + sale_gamma: 9 + service_level: 0.95 + SKU417: + constraint: G(low_profit -> low_stock_constraint) + cost: 443 + init_stock: 288 + max_stock: 1000 + price: 872 + sale_gamma: 72 + service_level: 0.95 + SKU418: + constraint: null + cost: 458 + init_stock: 52 + max_stock: 1000 + price: 838 + sale_gamma: 13 + service_level: 0.95 + SKU419: + constraint: null + cost: 303 + init_stock: 712 + max_stock: 1000 + price: 448 + sale_gamma: 89 + service_level: 0.95 + SKU42: + constraint: null + cost: 462 + init_stock: 156 + max_stock: 1000 + price: 688 + sale_gamma: 26 + service_level: 0.95 + SKU420: + constraint: null + cost: 268 + init_stock: 84 + max_stock: 1000 + price: 506 + sale_gamma: 42 + service_level: 0.95 + SKU421: + constraint: G(stock_constraint) + cost: 214 + init_stock: 138 + max_stock: 1000 + price: 314 + sale_gamma: 46 + service_level: 0.95 + SKU422: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 432 + max_stock: 1000 + price: 97 + sale_gamma: 54 + service_level: 0.95 + SKU423: + constraint: G(low_profit -> low_stock_constraint) + cost: 188 + init_stock: 396 + max_stock: 1000 + price: 265 + sale_gamma: 66 + service_level: 0.95 + SKU424: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 66 + max_stock: 1000 + price: 317 + sale_gamma: 11 + service_level: 0.95 + SKU425: + constraint: G(stock_constraint) + cost: 162 + init_stock: 72 + max_stock: 1000 + price: 277 + sale_gamma: 12 + service_level: 0.95 + SKU426: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 125 + init_stock: 588 + max_stock: 1000 + price: 246 + sale_gamma: 98 + service_level: 0.95 + SKU427: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 413 + init_stock: 282 + max_stock: 1000 + price: 710 + sale_gamma: 94 + service_level: 0.95 + SKU428: + constraint: G(stock_constraint) + cost: 391 + init_stock: 378 + max_stock: 1000 + price: 629 + sale_gamma: 63 + service_level: 0.95 + SKU429: + constraint: G(stock_constraint) + cost: 39 + init_stock: 246 + max_stock: 1000 + price: 73 + sale_gamma: 41 + service_level: 0.95 + SKU43: + constraint: G(stock_constraint) + cost: 217 + init_stock: 272 + max_stock: 1000 + price: 353 + sale_gamma: 68 + service_level: 0.95 + SKU430: + constraint: null + cost: 216 + init_stock: 511 + max_stock: 1000 + price: 313 + sale_gamma: 73 + service_level: 0.95 + SKU431: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 328 + init_stock: 126 + max_stock: 1000 + price: 646 + sale_gamma: 21 + service_level: 0.95 + SKU432: + constraint: G(stock_constraint) + cost: 25 + init_stock: 368 + max_stock: 1000 + price: 35 + sale_gamma: 46 + service_level: 0.95 + SKU433: + constraint: null + cost: 348 + init_stock: 270 + max_stock: 1000 + price: 396 + sale_gamma: 45 + service_level: 0.95 + SKU434: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 90 + max_stock: 1000 + price: 40 + sale_gamma: 30 + service_level: 0.95 + SKU435: + constraint: G(stock_constraint) + cost: 272 + init_stock: 210 + max_stock: 1000 + price: 320 + sale_gamma: 30 + service_level: 0.95 + SKU436: + constraint: null + cost: 72 + init_stock: 405 + max_stock: 1000 + price: 105 + sale_gamma: 81 + service_level: 0.95 + SKU437: + constraint: G(stock_constraint) + cost: 115 + init_stock: 84 + max_stock: 1000 + price: 223 + sale_gamma: 14 + service_level: 0.95 + SKU438: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 40 + max_stock: 1000 + price: 190 + sale_gamma: 10 + service_level: 0.95 + SKU439: + constraint: G(stock_constraint) + cost: 256 + init_stock: 190 + max_stock: 1000 + price: 304 + sale_gamma: 38 + service_level: 0.95 + SKU44: + constraint: null + cost: 360 + init_stock: 240 + max_stock: 1000 + price: 669 + sale_gamma: 48 + service_level: 0.95 + SKU440: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 248 + init_stock: 246 + max_stock: 1000 + price: 357 + sale_gamma: 82 + service_level: 0.95 + SKU441: + constraint: null + cost: 253 + init_stock: 224 + max_stock: 1000 + price: 374 + sale_gamma: 56 + service_level: 0.95 + SKU442: + constraint: null + cost: 470 + init_stock: 352 + max_stock: 1000 + price: 629 + sale_gamma: 88 + service_level: 0.95 + SKU443: + constraint: null + cost: 218 + init_stock: 365 + max_stock: 1000 + price: 361 + sale_gamma: 73 + service_level: 0.95 + SKU444: + constraint: null + cost: 156 + init_stock: 18 + max_stock: 1000 + price: 182 + sale_gamma: 6 + service_level: 0.95 + SKU445: + constraint: null + cost: 260 + init_stock: 602 + max_stock: 1000 + price: 296 + sale_gamma: 86 + service_level: 0.95 + SKU446: + constraint: null + cost: 497 + init_stock: 147 + max_stock: 1000 + price: 690 + sale_gamma: 49 + service_level: 0.95 + SKU447: + constraint: null + cost: 196 + init_stock: 30 + max_stock: 1000 + price: 280 + sale_gamma: 5 + service_level: 0.95 + SKU448: + constraint: null + cost: 485 + init_stock: 497 + max_stock: 1000 + price: 742 + sale_gamma: 71 + service_level: 0.95 + SKU449: + constraint: null + cost: 282 + init_stock: 114 + max_stock: 1000 + price: 360 + sale_gamma: 38 + service_level: 0.95 + SKU45: + constraint: G(stock_constraint) + cost: 253 + init_stock: 30 + max_stock: 1000 + price: 351 + sale_gamma: 10 + service_level: 0.95 + SKU450: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 58 + init_stock: 294 + max_stock: 1000 + price: 77 + sale_gamma: 98 + service_level: 0.95 + SKU451: + constraint: null + cost: 468 + init_stock: 483 + max_stock: 1000 + price: 851 + sale_gamma: 69 + service_level: 0.95 + SKU452: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 63 + init_stock: 395 + max_stock: 1000 + price: 97 + sale_gamma: 79 + service_level: 0.95 + SKU453: + constraint: null + cost: 37 + init_stock: 140 + max_stock: 1000 + price: 65 + sale_gamma: 35 + service_level: 0.95 + SKU454: + constraint: G(low_profit -> low_stock_constraint) + cost: 494 + init_stock: 340 + max_stock: 1000 + price: 899 + sale_gamma: 85 + service_level: 0.95 + SKU455: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 136 + init_stock: 182 + max_stock: 1000 + price: 195 + sale_gamma: 26 + service_level: 0.95 + SKU456: + constraint: G(stock_constraint) + cost: 338 + init_stock: 75 + max_stock: 1000 + price: 659 + sale_gamma: 25 + service_level: 0.95 + SKU457: + constraint: null + cost: 169 + init_stock: 256 + max_stock: 1000 + price: 190 + sale_gamma: 64 + service_level: 0.95 + SKU458: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 403 + init_stock: 28 + max_stock: 1000 + price: 685 + sale_gamma: 7 + service_level: 0.95 + SKU459: + constraint: null + cost: 425 + init_stock: 272 + max_stock: 1000 + price: 731 + sale_gamma: 34 + service_level: 0.95 + SKU46: + constraint: G(low_profit -> low_stock_constraint) + cost: 299 + init_stock: 200 + max_stock: 1000 + price: 409 + sale_gamma: 50 + service_level: 0.95 + SKU460: + constraint: G(low_profit -> low_stock_constraint) + cost: 405 + init_stock: 292 + max_stock: 1000 + price: 558 + sale_gamma: 73 + service_level: 0.95 + SKU461: + constraint: G(stock_constraint) + cost: 251 + init_stock: 240 + max_stock: 1000 + price: 326 + sale_gamma: 48 + service_level: 0.95 + SKU462: + constraint: G(low_profit -> low_stock_constraint) + cost: 448 + init_stock: 36 + max_stock: 1000 + price: 757 + sale_gamma: 9 + service_level: 0.95 + SKU463: + constraint: null + cost: 187 + init_stock: 574 + max_stock: 1000 + price: 213 + sale_gamma: 82 + service_level: 0.95 + SKU464: + constraint: null + cost: 279 + init_stock: 272 + max_stock: 1000 + price: 438 + sale_gamma: 34 + service_level: 0.95 + SKU465: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 147 + init_stock: 400 + max_stock: 1000 + price: 288 + sale_gamma: 100 + service_level: 0.95 + SKU466: + constraint: G(stock_constraint) + cost: 97 + init_stock: 126 + max_stock: 1000 + price: 167 + sale_gamma: 42 + service_level: 0.95 + SKU467: + constraint: G(stock_constraint) + cost: 142 + init_stock: 252 + max_stock: 1000 + price: 230 + sale_gamma: 36 + service_level: 0.95 + SKU468: + constraint: G(stock_constraint) + cost: 55 + init_stock: 250 + max_stock: 1000 + price: 104 + sale_gamma: 50 + service_level: 0.95 + SKU469: + constraint: G(stock_constraint) + cost: 178 + init_stock: 105 + max_stock: 1000 + price: 331 + sale_gamma: 15 + service_level: 0.95 + SKU47: + constraint: G(stock_constraint) + cost: 121 + init_stock: 117 + max_stock: 1000 + price: 240 + sale_gamma: 39 + service_level: 0.95 + SKU470: + constraint: G(low_profit -> low_stock_constraint) + cost: 373 + init_stock: 128 + max_stock: 1000 + price: 548 + sale_gamma: 32 + service_level: 0.95 + SKU471: + constraint: G(low_profit -> low_stock_constraint) + cost: 315 + init_stock: 568 + max_stock: 1000 + price: 387 + sale_gamma: 71 + service_level: 0.95 + SKU472: + constraint: null + cost: 421 + init_stock: 177 + max_stock: 1000 + price: 627 + sale_gamma: 59 + service_level: 0.95 + SKU473: + constraint: G(low_profit -> low_stock_constraint) + cost: 195 + init_stock: 105 + max_stock: 1000 + price: 323 + sale_gamma: 21 + service_level: 0.95 + SKU474: + constraint: null + cost: 448 + init_stock: 602 + max_stock: 1000 + price: 775 + sale_gamma: 86 + service_level: 0.95 + SKU475: + constraint: null + cost: 34 + init_stock: 282 + max_stock: 1000 + price: 62 + sale_gamma: 94 + service_level: 0.95 + SKU476: + constraint: null + cost: 251 + init_stock: 180 + max_stock: 1000 + price: 361 + sale_gamma: 90 + service_level: 0.95 + SKU477: + constraint: null + cost: 289 + init_stock: 329 + max_stock: 1000 + price: 338 + sale_gamma: 47 + service_level: 0.95 + SKU478: + constraint: null + cost: 479 + init_stock: 352 + max_stock: 1000 + price: 723 + sale_gamma: 88 + service_level: 0.95 + SKU479: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 366 + init_stock: 152 + max_stock: 1000 + price: 552 + sale_gamma: 38 + service_level: 0.95 + SKU48: + constraint: null + cost: 340 + init_stock: 232 + max_stock: 1000 + price: 387 + sale_gamma: 58 + service_level: 0.95 + SKU480: + constraint: G(stock_constraint) + cost: 18 + init_stock: 180 + max_stock: 1000 + price: 21 + sale_gamma: 30 + service_level: 0.95 + SKU481: + constraint: null + cost: 330 + init_stock: 352 + max_stock: 1000 + price: 422 + sale_gamma: 88 + service_level: 0.95 + SKU482: + constraint: G(stock_constraint) + cost: 94 + init_stock: 696 + max_stock: 1000 + price: 108 + sale_gamma: 87 + service_level: 0.95 + SKU483: + constraint: null + cost: 298 + init_stock: 170 + max_stock: 1000 + price: 533 + sale_gamma: 34 + service_level: 0.95 + SKU484: + constraint: null + cost: 84 + init_stock: 365 + max_stock: 1000 + price: 117 + sale_gamma: 73 + service_level: 0.95 + SKU485: + constraint: null + cost: 318 + init_stock: 536 + max_stock: 1000 + price: 543 + sale_gamma: 67 + service_level: 0.95 + SKU486: + constraint: G(low_profit -> low_stock_constraint) + cost: 122 + init_stock: 208 + max_stock: 1000 + price: 161 + sale_gamma: 52 + service_level: 0.95 + SKU487: + constraint: null + cost: 277 + init_stock: 264 + max_stock: 1000 + price: 362 + sale_gamma: 66 + service_level: 0.95 + SKU488: + constraint: G(low_profit -> low_stock_constraint) + cost: 36 + init_stock: 189 + max_stock: 1000 + price: 42 + sale_gamma: 27 + service_level: 0.95 + SKU489: + constraint: null + cost: 59 + init_stock: 124 + max_stock: 1000 + price: 97 + sale_gamma: 31 + service_level: 0.95 + SKU49: + constraint: null + cost: 263 + init_stock: 760 + max_stock: 1000 + price: 515 + sale_gamma: 95 + service_level: 0.95 + SKU490: + constraint: null + cost: 161 + init_stock: 486 + max_stock: 1000 + price: 264 + sale_gamma: 81 + service_level: 0.95 + SKU491: + constraint: null + cost: 444 + init_stock: 450 + max_stock: 1000 + price: 834 + sale_gamma: 75 + service_level: 0.95 + SKU492: + constraint: null + cost: 53 + init_stock: 240 + max_stock: 1000 + price: 63 + sale_gamma: 80 + service_level: 0.95 + SKU493: + constraint: null + cost: 461 + init_stock: 81 + max_stock: 1000 + price: 567 + sale_gamma: 27 + service_level: 0.95 + SKU494: + constraint: null + cost: 145 + init_stock: 282 + max_stock: 1000 + price: 242 + sale_gamma: 94 + service_level: 0.95 + SKU495: + constraint: G(stock_constraint) + cost: 149 + init_stock: 372 + max_stock: 1000 + price: 217 + sale_gamma: 62 + service_level: 0.95 + SKU496: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 342 + init_stock: 295 + max_stock: 1000 + price: 492 + sale_gamma: 59 + service_level: 0.95 + SKU497: + constraint: G(low_profit -> low_stock_constraint) + cost: 33 + init_stock: 45 + max_stock: 1000 + price: 42 + sale_gamma: 9 + service_level: 0.95 + SKU498: + constraint: null + cost: 305 + init_stock: 325 + max_stock: 1000 + price: 439 + sale_gamma: 65 + service_level: 0.95 + SKU499: + constraint: G(stock_constraint) + cost: 307 + init_stock: 174 + max_stock: 1000 + price: 472 + sale_gamma: 29 + service_level: 0.95 + SKU5: + constraint: G(stock_constraint) + cost: 466 + init_stock: 426 + max_stock: 1000 + price: 852 + sale_gamma: 71 + service_level: 0.95 + SKU50: + constraint: null + cost: 282 + init_stock: 185 + max_stock: 1000 + price: 516 + sale_gamma: 37 + service_level: 0.95 + SKU500: + constraint: null + cost: 208 + init_stock: 336 + max_stock: 1000 + price: 255 + sale_gamma: 42 + service_level: 0.95 + SKU501: + constraint: null + cost: 194 + init_stock: 152 + max_stock: 1000 + price: 265 + sale_gamma: 76 + service_level: 0.95 + SKU502: + constraint: null + cost: 69 + init_stock: 390 + max_stock: 1000 + price: 101 + sale_gamma: 65 + service_level: 0.95 + SKU503: + constraint: G(stock_constraint) + cost: 208 + init_stock: 228 + max_stock: 1000 + price: 280 + sale_gamma: 57 + service_level: 0.95 + SKU504: + constraint: null + cost: 251 + init_stock: 210 + max_stock: 1000 + price: 426 + sale_gamma: 30 + service_level: 0.95 + SKU505: + constraint: null + cost: 426 + init_stock: 295 + max_stock: 1000 + price: 515 + sale_gamma: 59 + service_level: 0.95 + SKU506: + constraint: null + cost: 149 + init_stock: 465 + max_stock: 1000 + price: 220 + sale_gamma: 93 + service_level: 0.95 + SKU507: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 187 + init_stock: 60 + max_stock: 1000 + price: 293 + sale_gamma: 15 + service_level: 0.95 + SKU508: + constraint: G(low_profit -> low_stock_constraint) + cost: 272 + init_stock: 576 + max_stock: 1000 + price: 432 + sale_gamma: 96 + service_level: 0.95 + SKU509: + constraint: G(low_profit -> low_stock_constraint) + cost: 297 + init_stock: 486 + max_stock: 1000 + price: 397 + sale_gamma: 81 + service_level: 0.95 + SKU51: + constraint: G(low_profit -> low_stock_constraint) + cost: 295 + init_stock: 469 + max_stock: 1000 + price: 477 + sale_gamma: 67 + service_level: 0.95 + SKU510: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 36 + max_stock: 1000 + price: 156 + sale_gamma: 9 + service_level: 0.95 + SKU511: + constraint: G(stock_constraint) + cost: 361 + init_stock: 450 + max_stock: 1000 + price: 480 + sale_gamma: 75 + service_level: 0.95 + SKU512: + constraint: null + cost: 467 + init_stock: 132 + max_stock: 1000 + price: 854 + sale_gamma: 22 + service_level: 0.95 + SKU513: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 343 + init_stock: 42 + max_stock: 1000 + price: 631 + sale_gamma: 7 + service_level: 0.95 + SKU514: + constraint: null + cost: 186 + init_stock: 504 + max_stock: 1000 + price: 332 + sale_gamma: 63 + service_level: 0.95 + SKU515: + constraint: null + cost: 145 + init_stock: 216 + max_stock: 1000 + price: 227 + sale_gamma: 54 + service_level: 0.95 + SKU516: + constraint: G(stock_constraint) + cost: 64 + init_stock: 114 + max_stock: 1000 + price: 96 + sale_gamma: 38 + service_level: 0.95 + SKU517: + constraint: null + cost: 117 + init_stock: 45 + max_stock: 1000 + price: 168 + sale_gamma: 9 + service_level: 0.95 + SKU518: + constraint: G(stock_constraint) + cost: 26 + init_stock: 147 + max_stock: 1000 + price: 30 + sale_gamma: 21 + service_level: 0.95 + SKU519: + constraint: null + cost: 233 + init_stock: 250 + max_stock: 1000 + price: 410 + sale_gamma: 50 + service_level: 0.95 + SKU52: + constraint: null + cost: 22 + init_stock: 402 + max_stock: 1000 + price: 28 + sale_gamma: 67 + service_level: 0.95 + SKU520: + constraint: G(low_profit -> low_stock_constraint) + cost: 209 + init_stock: 44 + max_stock: 1000 + price: 248 + sale_gamma: 22 + service_level: 0.95 + SKU521: + constraint: G(low_profit -> low_stock_constraint) + cost: 220 + init_stock: 350 + max_stock: 1000 + price: 330 + sale_gamma: 50 + service_level: 0.95 + SKU522: + constraint: null + cost: 156 + init_stock: 522 + max_stock: 1000 + price: 277 + sale_gamma: 87 + service_level: 0.95 + SKU523: + constraint: null + cost: 65 + init_stock: 500 + max_stock: 1000 + price: 107 + sale_gamma: 100 + service_level: 0.95 + SKU524: + constraint: null + cost: 294 + init_stock: 188 + max_stock: 1000 + price: 464 + sale_gamma: 94 + service_level: 0.95 + SKU525: + constraint: G(stock_constraint) + cost: 432 + init_stock: 126 + max_stock: 1000 + price: 751 + sale_gamma: 42 + service_level: 0.95 + SKU526: + constraint: null + cost: 419 + init_stock: 168 + max_stock: 1000 + price: 716 + sale_gamma: 24 + service_level: 0.95 + SKU527: + constraint: null + cost: 131 + init_stock: 350 + max_stock: 1000 + price: 262 + sale_gamma: 70 + service_level: 0.95 + SKU528: + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 84 + max_stock: 1000 + price: 581 + sale_gamma: 21 + service_level: 0.95 + SKU529: + constraint: G(low_profit -> low_stock_constraint) + cost: 298 + init_stock: 248 + max_stock: 1000 + price: 375 + sale_gamma: 31 + service_level: 0.95 + SKU53: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 151 + init_stock: 490 + max_stock: 1000 + price: 191 + sale_gamma: 70 + service_level: 0.95 + SKU530: + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 135 + max_stock: 1000 + price: 205 + sale_gamma: 45 + service_level: 0.95 + SKU531: + constraint: null + cost: 103 + init_stock: 300 + max_stock: 1000 + price: 153 + sale_gamma: 60 + service_level: 0.95 + SKU532: + constraint: null + cost: 351 + init_stock: 539 + max_stock: 1000 + price: 431 + sale_gamma: 77 + service_level: 0.95 + SKU533: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 168 + max_stock: 1000 + price: 340 + sale_gamma: 42 + service_level: 0.95 + SKU534: + constraint: null + cost: 425 + init_stock: 82 + max_stock: 1000 + price: 548 + sale_gamma: 41 + service_level: 0.95 + SKU535: + constraint: null + cost: 304 + init_stock: 430 + max_stock: 1000 + price: 465 + sale_gamma: 86 + service_level: 0.95 + SKU536: + constraint: null + cost: 358 + init_stock: 208 + max_stock: 1000 + price: 415 + sale_gamma: 52 + service_level: 0.95 + SKU537: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 321 + init_stock: 45 + max_stock: 1000 + price: 455 + sale_gamma: 9 + service_level: 0.95 + SKU538: + constraint: null + cost: 457 + init_stock: 200 + max_stock: 1000 + price: 868 + sale_gamma: 50 + service_level: 0.95 + SKU539: + constraint: G(stock_constraint) + cost: 165 + init_stock: 234 + max_stock: 1000 + price: 229 + sale_gamma: 78 + service_level: 0.95 + SKU54: + constraint: G(low_profit -> low_stock_constraint) + cost: 158 + init_stock: 138 + max_stock: 1000 + price: 241 + sale_gamma: 46 + service_level: 0.95 + SKU540: + constraint: null + cost: 388 + init_stock: 294 + max_stock: 1000 + price: 496 + sale_gamma: 42 + service_level: 0.95 + SKU541: + constraint: null + cost: 131 + init_stock: 203 + max_stock: 1000 + price: 213 + sale_gamma: 29 + service_level: 0.95 + SKU542: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 38 + init_stock: 130 + max_stock: 1000 + price: 42 + sale_gamma: 26 + service_level: 0.95 + SKU543: + constraint: G(stock_constraint) + cost: 430 + init_stock: 510 + max_stock: 1000 + price: 718 + sale_gamma: 85 + service_level: 0.95 + SKU544: + constraint: G(stock_constraint) + cost: 346 + init_stock: 194 + max_stock: 1000 + price: 474 + sale_gamma: 97 + service_level: 0.95 + SKU545: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 175 + init_stock: 75 + max_stock: 1000 + price: 323 + sale_gamma: 15 + service_level: 0.95 + SKU546: + constraint: null + cost: 491 + init_stock: 576 + max_stock: 1000 + price: 765 + sale_gamma: 96 + service_level: 0.95 + SKU547: + constraint: G(stock_constraint) + cost: 161 + init_stock: 264 + max_stock: 1000 + price: 251 + sale_gamma: 44 + service_level: 0.95 + SKU548: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 111 + init_stock: 36 + max_stock: 1000 + price: 217 + sale_gamma: 6 + service_level: 0.95 + SKU549: + constraint: G(stock_constraint) + cost: 387 + init_stock: 316 + max_stock: 1000 + price: 510 + sale_gamma: 79 + service_level: 0.95 + SKU55: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 482 + init_stock: 455 + max_stock: 1000 + price: 896 + sale_gamma: 65 + service_level: 0.95 + SKU550: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 259 + init_stock: 282 + max_stock: 1000 + price: 502 + sale_gamma: 94 + service_level: 0.95 + SKU551: + constraint: null + cost: 46 + init_stock: 186 + max_stock: 1000 + price: 60 + sale_gamma: 31 + service_level: 0.95 + SKU552: + constraint: null + cost: 191 + init_stock: 450 + max_stock: 1000 + price: 315 + sale_gamma: 90 + service_level: 0.95 + SKU553: + constraint: G(low_profit -> low_stock_constraint) + cost: 208 + init_stock: 60 + max_stock: 1000 + price: 251 + sale_gamma: 30 + service_level: 0.95 + SKU554: + constraint: null + cost: 340 + init_stock: 82 + max_stock: 1000 + price: 387 + sale_gamma: 41 + service_level: 0.95 + SKU555: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 252 + max_stock: 1000 + price: 684 + sale_gamma: 84 + service_level: 0.95 + SKU556: + constraint: G(stock_constraint) + cost: 110 + init_stock: 90 + max_stock: 1000 + price: 213 + sale_gamma: 30 + service_level: 0.95 + SKU557: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 334 + init_stock: 365 + max_stock: 1000 + price: 661 + sale_gamma: 73 + service_level: 0.95 + SKU558: + constraint: null + cost: 399 + init_stock: 85 + max_stock: 1000 + price: 490 + sale_gamma: 17 + service_level: 0.95 + SKU559: + constraint: null + cost: 313 + init_stock: 270 + max_stock: 1000 + price: 591 + sale_gamma: 54 + service_level: 0.95 + SKU56: + constraint: null + cost: 377 + init_stock: 222 + max_stock: 1000 + price: 671 + sale_gamma: 74 + service_level: 0.95 + SKU560: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 283 + init_stock: 246 + max_stock: 1000 + price: 458 + sale_gamma: 41 + service_level: 0.95 + SKU561: + constraint: null + cost: 202 + init_stock: 312 + max_stock: 1000 + price: 385 + sale_gamma: 39 + service_level: 0.95 + SKU562: + constraint: null + cost: 347 + init_stock: 356 + max_stock: 1000 + price: 666 + sale_gamma: 89 + service_level: 0.95 + SKU563: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 401 + init_stock: 30 + max_stock: 1000 + price: 509 + sale_gamma: 5 + service_level: 0.95 + SKU564: + constraint: null + cost: 109 + init_stock: 63 + max_stock: 1000 + price: 183 + sale_gamma: 9 + service_level: 0.95 + SKU565: + constraint: null + cost: 57 + init_stock: 525 + max_stock: 1000 + price: 90 + sale_gamma: 75 + service_level: 0.95 + SKU566: + constraint: null + cost: 131 + init_stock: 44 + max_stock: 1000 + price: 165 + sale_gamma: 11 + service_level: 0.95 + SKU567: + constraint: G(stock_constraint) + cost: 361 + init_stock: 27 + max_stock: 1000 + price: 465 + sale_gamma: 9 + service_level: 0.95 + SKU568: + constraint: null + cost: 75 + init_stock: 234 + max_stock: 1000 + price: 102 + sale_gamma: 78 + service_level: 0.95 + SKU569: + constraint: null + cost: 473 + init_stock: 192 + max_stock: 1000 + price: 591 + sale_gamma: 48 + service_level: 0.95 + SKU57: + constraint: G(stock_constraint) + cost: 359 + init_stock: 350 + max_stock: 1000 + price: 682 + sale_gamma: 50 + service_level: 0.95 + SKU570: + constraint: null + cost: 388 + init_stock: 581 + max_stock: 1000 + price: 686 + sale_gamma: 83 + service_level: 0.95 + SKU571: + constraint: G(stock_constraint) + cost: 168 + init_stock: 78 + max_stock: 1000 + price: 208 + sale_gamma: 39 + service_level: 0.95 + SKU572: + constraint: null + cost: 26 + init_stock: 225 + max_stock: 1000 + price: 41 + sale_gamma: 45 + service_level: 0.95 + SKU573: + constraint: null + cost: 246 + init_stock: 164 + max_stock: 1000 + price: 391 + sale_gamma: 41 + service_level: 0.95 + SKU574: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 150 + max_stock: 1000 + price: 166 + sale_gamma: 50 + service_level: 0.95 + SKU575: + constraint: null + cost: 237 + init_stock: 237 + max_stock: 1000 + price: 438 + sale_gamma: 79 + service_level: 0.95 + SKU576: + constraint: G(stock_constraint) + cost: 265 + init_stock: 468 + max_stock: 1000 + price: 416 + sale_gamma: 78 + service_level: 0.95 + SKU577: + constraint: G(low_profit -> low_stock_constraint) + cost: 18 + init_stock: 348 + max_stock: 1000 + price: 24 + sale_gamma: 87 + service_level: 0.95 + SKU578: + constraint: null + cost: 100 + init_stock: 284 + max_stock: 1000 + price: 148 + sale_gamma: 71 + service_level: 0.95 + SKU579: + constraint: null + cost: 415 + init_stock: 27 + max_stock: 1000 + price: 771 + sale_gamma: 9 + service_level: 0.95 + SKU58: + constraint: null + cost: 362 + init_stock: 240 + max_stock: 1000 + price: 521 + sale_gamma: 48 + service_level: 0.95 + SKU580: + constraint: null + cost: 203 + init_stock: 15 + max_stock: 1000 + price: 261 + sale_gamma: 5 + service_level: 0.95 + SKU581: + constraint: null + cost: 152 + init_stock: 516 + max_stock: 1000 + price: 185 + sale_gamma: 86 + service_level: 0.95 + SKU582: + constraint: G(stock_constraint) + cost: 359 + init_stock: 480 + max_stock: 1000 + price: 567 + sale_gamma: 96 + service_level: 0.95 + SKU583: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 86 + init_stock: 261 + max_stock: 1000 + price: 98 + sale_gamma: 87 + service_level: 0.95 + SKU584: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 33 + init_stock: 270 + max_stock: 1000 + price: 36 + sale_gamma: 45 + service_level: 0.95 + SKU585: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 100 + max_stock: 1000 + price: 53 + sale_gamma: 20 + service_level: 0.95 + SKU586: + constraint: G(stock_constraint) + cost: 61 + init_stock: 264 + max_stock: 1000 + price: 94 + sale_gamma: 44 + service_level: 0.95 + SKU587: + constraint: null + cost: 290 + init_stock: 468 + max_stock: 1000 + price: 423 + sale_gamma: 78 + service_level: 0.95 + SKU588: + constraint: G(stock_constraint) + cost: 476 + init_stock: 176 + max_stock: 1000 + price: 885 + sale_gamma: 44 + service_level: 0.95 + SKU589: + constraint: null + cost: 244 + init_stock: 170 + max_stock: 1000 + price: 380 + sale_gamma: 34 + service_level: 0.95 + SKU59: + constraint: G(low_profit -> low_stock_constraint) + cost: 145 + init_stock: 255 + max_stock: 1000 + price: 275 + sale_gamma: 51 + service_level: 0.95 + SKU590: + constraint: null + cost: 70 + init_stock: 93 + max_stock: 1000 + price: 103 + sale_gamma: 31 + service_level: 0.95 + SKU591: + constraint: G(stock_constraint) + cost: 206 + init_stock: 504 + max_stock: 1000 + price: 298 + sale_gamma: 84 + service_level: 0.95 + SKU592: + constraint: null + cost: 218 + init_stock: 276 + max_stock: 1000 + price: 418 + sale_gamma: 46 + service_level: 0.95 + SKU593: + constraint: null + cost: 124 + init_stock: 88 + max_stock: 1000 + price: 221 + sale_gamma: 44 + service_level: 0.95 + SKU594: + constraint: null + cost: 238 + init_stock: 150 + max_stock: 1000 + price: 459 + sale_gamma: 50 + service_level: 0.95 + SKU595: + constraint: null + cost: 73 + init_stock: 172 + max_stock: 1000 + price: 107 + sale_gamma: 43 + service_level: 0.95 + SKU596: + constraint: null + cost: 432 + init_stock: 35 + max_stock: 1000 + price: 540 + sale_gamma: 7 + service_level: 0.95 + SKU597: + constraint: G(stock_constraint) + cost: 252 + init_stock: 435 + max_stock: 1000 + price: 451 + sale_gamma: 87 + service_level: 0.95 + SKU598: + constraint: null + cost: 348 + init_stock: 28 + max_stock: 1000 + price: 612 + sale_gamma: 14 + service_level: 0.95 + SKU599: + constraint: null + cost: 54 + init_stock: 204 + max_stock: 1000 + price: 66 + sale_gamma: 68 + service_level: 0.95 + SKU6: + constraint: null + cost: 122 + init_stock: 616 + max_stock: 1000 + price: 195 + sale_gamma: 88 + service_level: 0.95 + SKU60: + constraint: G(stock_constraint) + cost: 200 + init_stock: 234 + max_stock: 1000 + price: 346 + sale_gamma: 39 + service_level: 0.95 + SKU600: + constraint: null + cost: 386 + init_stock: 325 + max_stock: 1000 + price: 505 + sale_gamma: 65 + service_level: 0.95 + SKU601: + constraint: null + cost: 138 + init_stock: 273 + max_stock: 1000 + price: 198 + sale_gamma: 39 + service_level: 0.95 + SKU602: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 184 + init_stock: 588 + max_stock: 1000 + price: 318 + sale_gamma: 98 + service_level: 0.95 + SKU603: + constraint: G(low_profit -> low_stock_constraint) + cost: 278 + init_stock: 252 + max_stock: 1000 + price: 550 + sale_gamma: 42 + service_level: 0.95 + SKU604: + constraint: G(stock_constraint) + cost: 270 + init_stock: 400 + max_stock: 1000 + price: 378 + sale_gamma: 50 + service_level: 0.95 + SKU605: + constraint: null + cost: 288 + init_stock: 120 + max_stock: 1000 + price: 443 + sale_gamma: 24 + service_level: 0.95 + SKU606: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 114 + init_stock: 66 + max_stock: 1000 + price: 161 + sale_gamma: 11 + service_level: 0.95 + SKU607: + constraint: null + cost: 208 + init_stock: 395 + max_stock: 1000 + price: 359 + sale_gamma: 79 + service_level: 0.95 + SKU608: + constraint: null + cost: 39 + init_stock: 365 + max_stock: 1000 + price: 49 + sale_gamma: 73 + service_level: 0.95 + SKU609: + constraint: null + cost: 102 + init_stock: 114 + max_stock: 1000 + price: 171 + sale_gamma: 19 + service_level: 0.95 + SKU61: + constraint: null + cost: 461 + init_stock: 150 + max_stock: 1000 + price: 645 + sale_gamma: 75 + service_level: 0.95 + SKU610: + constraint: null + cost: 449 + init_stock: 204 + max_stock: 1000 + price: 749 + sale_gamma: 68 + service_level: 0.95 + SKU611: + constraint: G(low_profit -> low_stock_constraint) + cost: 306 + init_stock: 539 + max_stock: 1000 + price: 489 + sale_gamma: 77 + service_level: 0.95 + SKU612: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 391 + init_stock: 616 + max_stock: 1000 + price: 613 + sale_gamma: 88 + service_level: 0.95 + SKU613: + constraint: G(stock_constraint) + cost: 174 + init_stock: 56 + max_stock: 1000 + price: 254 + sale_gamma: 7 + service_level: 0.95 + SKU614: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 173 + init_stock: 45 + max_stock: 1000 + price: 318 + sale_gamma: 15 + service_level: 0.95 + SKU615: + constraint: G(stock_constraint) + cost: 330 + init_stock: 364 + max_stock: 1000 + price: 432 + sale_gamma: 91 + service_level: 0.95 + SKU616: + constraint: G(low_profit -> low_stock_constraint) + cost: 232 + init_stock: 219 + max_stock: 1000 + price: 382 + sale_gamma: 73 + service_level: 0.95 + SKU617: + constraint: null + cost: 203 + init_stock: 420 + max_stock: 1000 + price: 227 + sale_gamma: 60 + service_level: 0.95 + SKU618: + constraint: G(stock_constraint) + cost: 77 + init_stock: 138 + max_stock: 1000 + price: 97 + sale_gamma: 23 + service_level: 0.95 + SKU619: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 344 + max_stock: 1000 + price: 359 + sale_gamma: 86 + service_level: 0.95 + SKU62: + constraint: null + cost: 124 + init_stock: 36 + max_stock: 1000 + price: 168 + sale_gamma: 9 + service_level: 0.95 + SKU620: + constraint: null + cost: 231 + init_stock: 156 + max_stock: 1000 + price: 267 + sale_gamma: 39 + service_level: 0.95 + SKU621: + constraint: null + cost: 199 + init_stock: 216 + max_stock: 1000 + price: 332 + sale_gamma: 72 + service_level: 0.95 + SKU622: + constraint: null + cost: 253 + init_stock: 455 + max_stock: 1000 + price: 468 + sale_gamma: 65 + service_level: 0.95 + SKU623: + constraint: null + cost: 335 + init_stock: 220 + max_stock: 1000 + price: 368 + sale_gamma: 55 + service_level: 0.95 + SKU624: + constraint: null + cost: 482 + init_stock: 270 + max_stock: 1000 + price: 824 + sale_gamma: 54 + service_level: 0.95 + SKU625: + constraint: null + cost: 46 + init_stock: 445 + max_stock: 1000 + price: 60 + sale_gamma: 89 + service_level: 0.95 + SKU626: + constraint: null + cost: 451 + init_stock: 534 + max_stock: 1000 + price: 694 + sale_gamma: 89 + service_level: 0.95 + SKU627: + constraint: null + cost: 220 + init_stock: 656 + max_stock: 1000 + price: 264 + sale_gamma: 82 + service_level: 0.95 + SKU628: + constraint: null + cost: 124 + init_stock: 156 + max_stock: 1000 + price: 229 + sale_gamma: 26 + service_level: 0.95 + SKU629: + constraint: G(stock_constraint) + cost: 353 + init_stock: 460 + max_stock: 1000 + price: 515 + sale_gamma: 92 + service_level: 0.95 + SKU63: + constraint: null + cost: 68 + init_stock: 70 + max_stock: 1000 + price: 86 + sale_gamma: 14 + service_level: 0.95 + SKU630: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 122 + init_stock: 34 + max_stock: 1000 + price: 137 + sale_gamma: 17 + service_level: 0.95 + SKU631: + constraint: null + cost: 296 + init_stock: 196 + max_stock: 1000 + price: 399 + sale_gamma: 49 + service_level: 0.95 + SKU632: + constraint: null + cost: 85 + init_stock: 470 + max_stock: 1000 + price: 141 + sale_gamma: 94 + service_level: 0.95 + SKU633: + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 402 + max_stock: 1000 + price: 228 + sale_gamma: 67 + service_level: 0.95 + SKU634: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 17 + init_stock: 438 + max_stock: 1000 + price: 30 + sale_gamma: 73 + service_level: 0.95 + SKU635: + constraint: null + cost: 341 + init_stock: 372 + max_stock: 1000 + price: 654 + sale_gamma: 93 + service_level: 0.95 + SKU636: + constraint: G(stock_constraint) + cost: 385 + init_stock: 111 + max_stock: 1000 + price: 739 + sale_gamma: 37 + service_level: 0.95 + SKU637: + constraint: G(low_profit -> low_stock_constraint) + cost: 83 + init_stock: 305 + max_stock: 1000 + price: 118 + sale_gamma: 61 + service_level: 0.95 + SKU638: + constraint: G(stock_constraint) + cost: 375 + init_stock: 16 + max_stock: 1000 + price: 536 + sale_gamma: 8 + service_level: 0.95 + SKU639: + constraint: null + cost: 327 + init_stock: 160 + max_stock: 1000 + price: 487 + sale_gamma: 40 + service_level: 0.95 + SKU64: + constraint: G(low_profit -> low_stock_constraint) + cost: 36 + init_stock: 133 + max_stock: 1000 + price: 42 + sale_gamma: 19 + service_level: 0.95 + SKU640: + constraint: G(low_profit -> low_stock_constraint) + cost: 275 + init_stock: 546 + max_stock: 1000 + price: 382 + sale_gamma: 91 + service_level: 0.95 + SKU641: + constraint: null + cost: 365 + init_stock: 486 + max_stock: 1000 + price: 445 + sale_gamma: 81 + service_level: 0.95 + SKU642: + constraint: null + cost: 414 + init_stock: 150 + max_stock: 1000 + price: 554 + sale_gamma: 25 + service_level: 0.95 + SKU643: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 358 + init_stock: 322 + max_stock: 1000 + price: 461 + sale_gamma: 46 + service_level: 0.95 + SKU644: + constraint: G(low_profit -> low_stock_constraint) + cost: 46 + init_stock: 410 + max_stock: 1000 + price: 61 + sale_gamma: 82 + service_level: 0.95 + SKU645: + constraint: null + cost: 467 + init_stock: 594 + max_stock: 1000 + price: 742 + sale_gamma: 99 + service_level: 0.95 + SKU646: + constraint: G(low_profit -> low_stock_constraint) + cost: 189 + init_stock: 91 + max_stock: 1000 + price: 268 + sale_gamma: 13 + service_level: 0.95 + SKU647: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 303 + init_stock: 462 + max_stock: 1000 + price: 369 + sale_gamma: 77 + service_level: 0.95 + SKU648: + constraint: G(stock_constraint) + cost: 226 + init_stock: 110 + max_stock: 1000 + price: 336 + sale_gamma: 55 + service_level: 0.95 + SKU649: + constraint: null + cost: 198 + init_stock: 175 + max_stock: 1000 + price: 229 + sale_gamma: 25 + service_level: 0.95 + SKU65: + constraint: null + cost: 15 + init_stock: 658 + max_stock: 1000 + price: 25 + sale_gamma: 94 + service_level: 0.95 + SKU650: + constraint: null + cost: 351 + init_stock: 224 + max_stock: 1000 + price: 498 + sale_gamma: 56 + service_level: 0.95 + SKU651: + constraint: null + cost: 380 + init_stock: 672 + max_stock: 1000 + price: 596 + sale_gamma: 96 + service_level: 0.95 + SKU652: + constraint: null + cost: 123 + init_stock: 280 + max_stock: 1000 + price: 168 + sale_gamma: 40 + service_level: 0.95 + SKU653: + constraint: G(low_profit -> low_stock_constraint) + cost: 479 + init_stock: 384 + max_stock: 1000 + price: 661 + sale_gamma: 96 + service_level: 0.95 + SKU654: + constraint: null + cost: 66 + init_stock: 32 + max_stock: 1000 + price: 110 + sale_gamma: 8 + service_level: 0.95 + SKU655: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 126 + max_stock: 1000 + price: 479 + sale_gamma: 42 + service_level: 0.95 + SKU656: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 53 + init_stock: 504 + max_stock: 1000 + price: 81 + sale_gamma: 72 + service_level: 0.95 + SKU657: + constraint: G(low_profit -> low_stock_constraint) + cost: 73 + init_stock: 567 + max_stock: 1000 + price: 110 + sale_gamma: 81 + service_level: 0.95 + SKU658: + constraint: null + cost: 70 + init_stock: 252 + max_stock: 1000 + price: 107 + sale_gamma: 36 + service_level: 0.95 + SKU659: + constraint: null + cost: 21 + init_stock: 336 + max_stock: 1000 + price: 27 + sale_gamma: 56 + service_level: 0.95 + SKU66: + constraint: null + cost: 143 + init_stock: 360 + max_stock: 1000 + price: 230 + sale_gamma: 90 + service_level: 0.95 + SKU660: + constraint: null + cost: 487 + init_stock: 415 + max_stock: 1000 + price: 560 + sale_gamma: 83 + service_level: 0.95 + SKU661: + constraint: null + cost: 344 + init_stock: 296 + max_stock: 1000 + price: 581 + sale_gamma: 74 + service_level: 0.95 + SKU662: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 266 + max_stock: 1000 + price: 628 + sale_gamma: 38 + service_level: 0.95 + SKU663: + constraint: G(stock_constraint) + cost: 418 + init_stock: 320 + max_stock: 1000 + price: 518 + sale_gamma: 40 + service_level: 0.95 + SKU664: + constraint: G(stock_constraint) + cost: 351 + init_stock: 420 + max_stock: 1000 + price: 494 + sale_gamma: 84 + service_level: 0.95 + SKU665: + constraint: null + cost: 493 + init_stock: 344 + max_stock: 1000 + price: 734 + sale_gamma: 43 + service_level: 0.95 + SKU666: + constraint: G(stock_constraint) + cost: 341 + init_stock: 176 + max_stock: 1000 + price: 572 + sale_gamma: 88 + service_level: 0.95 + SKU667: + constraint: null + cost: 325 + init_stock: 52 + max_stock: 1000 + price: 562 + sale_gamma: 13 + service_level: 0.95 + SKU668: + constraint: G(low_profit -> low_stock_constraint) + cost: 286 + init_stock: 300 + max_stock: 1000 + price: 463 + sale_gamma: 60 + service_level: 0.95 + SKU669: + constraint: null + cost: 74 + init_stock: 96 + max_stock: 1000 + price: 83 + sale_gamma: 16 + service_level: 0.95 + SKU67: + constraint: G(low_profit -> low_stock_constraint) + cost: 247 + init_stock: 159 + max_stock: 1000 + price: 454 + sale_gamma: 53 + service_level: 0.95 + SKU670: + constraint: G(stock_constraint) + cost: 289 + init_stock: 258 + max_stock: 1000 + price: 430 + sale_gamma: 86 + service_level: 0.95 + SKU671: + constraint: G(stock_constraint) + cost: 267 + init_stock: 332 + max_stock: 1000 + price: 296 + sale_gamma: 83 + service_level: 0.95 + SKU672: + constraint: null + cost: 369 + init_stock: 78 + max_stock: 1000 + price: 420 + sale_gamma: 13 + service_level: 0.95 + SKU673: + constraint: G(low_profit -> low_stock_constraint) + cost: 322 + init_stock: 123 + max_stock: 1000 + price: 418 + sale_gamma: 41 + service_level: 0.95 + SKU674: + constraint: null + cost: 223 + init_stock: 66 + max_stock: 1000 + price: 263 + sale_gamma: 22 + service_level: 0.95 + SKU675: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 438 + init_stock: 99 + max_stock: 1000 + price: 529 + sale_gamma: 33 + service_level: 0.95 + SKU676: + constraint: null + cost: 244 + init_stock: 366 + max_stock: 1000 + price: 275 + sale_gamma: 61 + service_level: 0.95 + SKU677: + constraint: null + cost: 425 + init_stock: 176 + max_stock: 1000 + price: 658 + sale_gamma: 44 + service_level: 0.95 + SKU678: + constraint: null + cost: 168 + init_stock: 216 + max_stock: 1000 + price: 199 + sale_gamma: 36 + service_level: 0.95 + SKU679: + constraint: null + cost: 395 + init_stock: 132 + max_stock: 1000 + price: 734 + sale_gamma: 44 + service_level: 0.95 + SKU68: + constraint: G(stock_constraint) + cost: 169 + init_stock: 56 + max_stock: 1000 + price: 239 + sale_gamma: 14 + service_level: 0.95 + SKU680: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 260 + init_stock: 180 + max_stock: 1000 + price: 327 + sale_gamma: 30 + service_level: 0.95 + SKU681: + constraint: G(low_profit -> low_stock_constraint) + cost: 464 + init_stock: 776 + max_stock: 1000 + price: 510 + sale_gamma: 97 + service_level: 0.95 + SKU682: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 370 + init_stock: 318 + max_stock: 1000 + price: 407 + sale_gamma: 53 + service_level: 0.95 + SKU683: + constraint: G(low_profit -> low_stock_constraint) + cost: 327 + init_stock: 536 + max_stock: 1000 + price: 608 + sale_gamma: 67 + service_level: 0.95 + SKU684: + constraint: G(stock_constraint) + cost: 355 + init_stock: 158 + max_stock: 1000 + price: 401 + sale_gamma: 79 + service_level: 0.95 + SKU685: + constraint: null + cost: 422 + init_stock: 270 + max_stock: 1000 + price: 493 + sale_gamma: 45 + service_level: 0.95 + SKU686: + constraint: G(low_profit -> low_stock_constraint) + cost: 63 + init_stock: 378 + max_stock: 1000 + price: 122 + sale_gamma: 63 + service_level: 0.95 + SKU687: + constraint: G(low_profit -> low_stock_constraint) + cost: 34 + init_stock: 456 + max_stock: 1000 + price: 43 + sale_gamma: 76 + service_level: 0.95 + SKU688: + constraint: G(low_profit -> low_stock_constraint) + cost: 484 + init_stock: 462 + max_stock: 1000 + price: 759 + sale_gamma: 77 + service_level: 0.95 + SKU689: + constraint: G(stock_constraint) + cost: 499 + init_stock: 75 + max_stock: 1000 + price: 808 + sale_gamma: 15 + service_level: 0.95 + SKU69: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 176 + init_stock: 402 + max_stock: 1000 + price: 197 + sale_gamma: 67 + service_level: 0.95 + SKU690: + constraint: null + cost: 437 + init_stock: 415 + max_stock: 1000 + price: 498 + sale_gamma: 83 + service_level: 0.95 + SKU691: + constraint: null + cost: 17 + init_stock: 632 + max_stock: 1000 + price: 18 + sale_gamma: 79 + service_level: 0.95 + SKU692: + constraint: G(stock_constraint) + cost: 225 + init_stock: 195 + max_stock: 1000 + price: 258 + sale_gamma: 65 + service_level: 0.95 + SKU693: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 19 + init_stock: 244 + max_stock: 1000 + price: 21 + sale_gamma: 61 + service_level: 0.95 + SKU694: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 208 + init_stock: 300 + max_stock: 1000 + price: 386 + sale_gamma: 75 + service_level: 0.95 + SKU695: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 190 + init_stock: 49 + max_stock: 1000 + price: 361 + sale_gamma: 7 + service_level: 0.95 + SKU696: + constraint: G(low_profit -> low_stock_constraint) + cost: 348 + init_stock: 290 + max_stock: 1000 + price: 643 + sale_gamma: 58 + service_level: 0.95 + SKU697: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 455 + init_stock: 320 + max_stock: 1000 + price: 641 + sale_gamma: 80 + service_level: 0.95 + SKU698: + constraint: null + cost: 198 + init_stock: 488 + max_stock: 1000 + price: 316 + sale_gamma: 61 + service_level: 0.95 + SKU699: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 100 + max_stock: 1000 + price: 58 + sale_gamma: 20 + service_level: 0.95 + SKU7: + constraint: null + cost: 101 + init_stock: 480 + max_stock: 1000 + price: 131 + sale_gamma: 96 + service_level: 0.95 + SKU70: + constraint: G(stock_constraint) + cost: 186 + init_stock: 140 + max_stock: 1000 + price: 288 + sale_gamma: 35 + service_level: 0.95 + SKU700: + constraint: G(low_profit -> low_stock_constraint) + cost: 74 + init_stock: 45 + max_stock: 1000 + price: 130 + sale_gamma: 9 + service_level: 0.95 + SKU701: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 399 + init_stock: 462 + max_stock: 1000 + price: 770 + sale_gamma: 77 + service_level: 0.95 + SKU702: + constraint: G(stock_constraint) + cost: 82 + init_stock: 204 + max_stock: 1000 + price: 163 + sale_gamma: 34 + service_level: 0.95 + SKU703: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 363 + init_stock: 76 + max_stock: 1000 + price: 602 + sale_gamma: 38 + service_level: 0.95 + SKU704: + constraint: G(stock_constraint) + cost: 384 + init_stock: 261 + max_stock: 1000 + price: 518 + sale_gamma: 87 + service_level: 0.95 + SKU705: + constraint: null + cost: 128 + init_stock: 378 + max_stock: 1000 + price: 236 + sale_gamma: 63 + service_level: 0.95 + SKU706: + constraint: null + cost: 182 + init_stock: 455 + max_stock: 1000 + price: 242 + sale_gamma: 91 + service_level: 0.95 + SKU707: + constraint: null + cost: 18 + init_stock: 276 + max_stock: 1000 + price: 35 + sale_gamma: 69 + service_level: 0.95 + SKU708: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 178 + init_stock: 112 + max_stock: 1000 + price: 334 + sale_gamma: 28 + service_level: 0.95 + SKU709: + constraint: G(stock_constraint) + cost: 102 + init_stock: 212 + max_stock: 1000 + price: 173 + sale_gamma: 53 + service_level: 0.95 + SKU71: + constraint: null + cost: 315 + init_stock: 225 + max_stock: 1000 + price: 589 + sale_gamma: 45 + service_level: 0.95 + SKU710: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 252 + init_stock: 95 + max_stock: 1000 + price: 337 + sale_gamma: 19 + service_level: 0.95 + SKU711: + constraint: null + cost: 281 + init_stock: 414 + max_stock: 1000 + price: 320 + sale_gamma: 69 + service_level: 0.95 + SKU712: + constraint: null + cost: 232 + init_stock: 77 + max_stock: 1000 + price: 438 + sale_gamma: 11 + service_level: 0.95 + SKU713: + constraint: null + cost: 285 + init_stock: 129 + max_stock: 1000 + price: 413 + sale_gamma: 43 + service_level: 0.95 + SKU714: + constraint: null + cost: 79 + init_stock: 287 + max_stock: 1000 + price: 106 + sale_gamma: 41 + service_level: 0.95 + SKU715: + constraint: G(stock_constraint) + cost: 234 + init_stock: 34 + max_stock: 1000 + price: 271 + sale_gamma: 17 + service_level: 0.95 + SKU716: + constraint: null + cost: 70 + init_stock: 450 + max_stock: 1000 + price: 123 + sale_gamma: 75 + service_level: 0.95 + SKU717: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 345 + init_stock: 56 + max_stock: 1000 + price: 382 + sale_gamma: 8 + service_level: 0.95 + SKU718: + constraint: null + cost: 357 + init_stock: 174 + max_stock: 1000 + price: 692 + sale_gamma: 58 + service_level: 0.95 + SKU719: + constraint: null + cost: 340 + init_stock: 455 + max_stock: 1000 + price: 384 + sale_gamma: 65 + service_level: 0.95 + SKU72: + constraint: null + cost: 458 + init_stock: 595 + max_stock: 1000 + price: 870 + sale_gamma: 85 + service_level: 0.95 + SKU720: + constraint: null + cost: 325 + init_stock: 294 + max_stock: 1000 + price: 503 + sale_gamma: 42 + service_level: 0.95 + SKU721: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 73 + init_stock: 44 + max_stock: 1000 + price: 143 + sale_gamma: 11 + service_level: 0.95 + SKU722: + constraint: G(stock_constraint) + cost: 392 + init_stock: 194 + max_stock: 1000 + price: 572 + sale_gamma: 97 + service_level: 0.95 + SKU723: + constraint: null + cost: 318 + init_stock: 356 + max_stock: 1000 + price: 442 + sale_gamma: 89 + service_level: 0.95 + SKU724: + constraint: G(low_profit -> low_stock_constraint) + cost: 400 + init_stock: 198 + max_stock: 1000 + price: 520 + sale_gamma: 33 + service_level: 0.95 + SKU725: + constraint: null + cost: 175 + init_stock: 148 + max_stock: 1000 + price: 309 + sale_gamma: 37 + service_level: 0.95 + SKU726: + constraint: G(low_profit -> low_stock_constraint) + cost: 458 + init_stock: 356 + max_stock: 1000 + price: 567 + sale_gamma: 89 + service_level: 0.95 + SKU727: + constraint: null + cost: 418 + init_stock: 285 + max_stock: 1000 + price: 526 + sale_gamma: 57 + service_level: 0.95 + SKU728: + constraint: null + cost: 475 + init_stock: 185 + max_stock: 1000 + price: 864 + sale_gamma: 37 + service_level: 0.95 + SKU729: + constraint: null + cost: 324 + init_stock: 252 + max_stock: 1000 + price: 476 + sale_gamma: 36 + service_level: 0.95 + SKU73: + constraint: null + cost: 212 + init_stock: 216 + max_stock: 1000 + price: 248 + sale_gamma: 54 + service_level: 0.95 + SKU730: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 16 + init_stock: 65 + max_stock: 1000 + price: 19 + sale_gamma: 13 + service_level: 0.95 + SKU731: + constraint: G(stock_constraint) + cost: 88 + init_stock: 410 + max_stock: 1000 + price: 155 + sale_gamma: 82 + service_level: 0.95 + SKU732: + constraint: null + cost: 41 + init_stock: 434 + max_stock: 1000 + price: 73 + sale_gamma: 62 + service_level: 0.95 + SKU733: + constraint: G(stock_constraint) + cost: 315 + init_stock: 104 + max_stock: 1000 + price: 541 + sale_gamma: 26 + service_level: 0.95 + SKU734: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 255 + max_stock: 1000 + price: 71 + sale_gamma: 51 + service_level: 0.95 + SKU735: + constraint: null + cost: 266 + init_stock: 594 + max_stock: 1000 + price: 409 + sale_gamma: 99 + service_level: 0.95 + SKU736: + constraint: G(stock_constraint) + cost: 368 + init_stock: 75 + max_stock: 1000 + price: 632 + sale_gamma: 25 + service_level: 0.95 + SKU737: + constraint: null + cost: 475 + init_stock: 93 + max_stock: 1000 + price: 655 + sale_gamma: 31 + service_level: 0.95 + SKU738: + constraint: null + cost: 185 + init_stock: 192 + max_stock: 1000 + price: 246 + sale_gamma: 48 + service_level: 0.95 + SKU739: + constraint: G(low_profit -> low_stock_constraint) + cost: 475 + init_stock: 296 + max_stock: 1000 + price: 612 + sale_gamma: 74 + service_level: 0.95 + SKU74: + constraint: null + cost: 159 + init_stock: 168 + max_stock: 1000 + price: 189 + sale_gamma: 42 + service_level: 0.95 + SKU740: + constraint: null + cost: 390 + init_stock: 256 + max_stock: 1000 + price: 549 + sale_gamma: 64 + service_level: 0.95 + SKU741: + constraint: G(stock_constraint) + cost: 91 + init_stock: 280 + max_stock: 1000 + price: 112 + sale_gamma: 56 + service_level: 0.95 + SKU742: + constraint: G(stock_constraint) + cost: 188 + init_stock: 110 + max_stock: 1000 + price: 374 + sale_gamma: 22 + service_level: 0.95 + SKU743: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 217 + init_stock: 301 + max_stock: 1000 + price: 431 + sale_gamma: 43 + service_level: 0.95 + SKU744: + constraint: G(low_profit -> low_stock_constraint) + cost: 379 + init_stock: 290 + max_stock: 1000 + price: 579 + sale_gamma: 58 + service_level: 0.95 + SKU745: + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 368 + max_stock: 1000 + price: 376 + sale_gamma: 92 + service_level: 0.95 + SKU746: + constraint: null + cost: 437 + init_stock: 160 + max_stock: 1000 + price: 624 + sale_gamma: 40 + service_level: 0.95 + SKU747: + constraint: null + cost: 373 + init_stock: 474 + max_stock: 1000 + price: 589 + sale_gamma: 79 + service_level: 0.95 + SKU748: + constraint: null + cost: 275 + init_stock: 330 + max_stock: 1000 + price: 464 + sale_gamma: 66 + service_level: 0.95 + SKU749: + constraint: null + cost: 394 + init_stock: 106 + max_stock: 1000 + price: 705 + sale_gamma: 53 + service_level: 0.95 + SKU75: + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 76 + max_stock: 1000 + price: 217 + sale_gamma: 19 + service_level: 0.95 + SKU750: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 310 + max_stock: 1000 + price: 463 + sale_gamma: 62 + service_level: 0.95 + SKU751: + constraint: null + cost: 369 + init_stock: 325 + max_stock: 1000 + price: 428 + sale_gamma: 65 + service_level: 0.95 + SKU752: + constraint: null + cost: 259 + init_stock: 546 + max_stock: 1000 + price: 435 + sale_gamma: 78 + service_level: 0.95 + SKU753: + constraint: G(stock_constraint) + cost: 77 + init_stock: 325 + max_stock: 1000 + price: 128 + sale_gamma: 65 + service_level: 0.95 + SKU754: + constraint: G(stock_constraint) + cost: 387 + init_stock: 91 + max_stock: 1000 + price: 545 + sale_gamma: 13 + service_level: 0.95 + SKU755: + constraint: null + cost: 354 + init_stock: 560 + max_stock: 1000 + price: 523 + sale_gamma: 80 + service_level: 0.95 + SKU756: + constraint: G(stock_constraint) + cost: 246 + init_stock: 360 + max_stock: 1000 + price: 361 + sale_gamma: 90 + service_level: 0.95 + SKU757: + constraint: null + cost: 40 + init_stock: 399 + max_stock: 1000 + price: 76 + sale_gamma: 57 + service_level: 0.95 + SKU758: + constraint: null + cost: 241 + init_stock: 160 + max_stock: 1000 + price: 457 + sale_gamma: 40 + service_level: 0.95 + SKU759: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 435 + init_stock: 76 + max_stock: 1000 + price: 648 + sale_gamma: 38 + service_level: 0.95 + SKU76: + constraint: null + cost: 147 + init_stock: 256 + max_stock: 1000 + price: 191 + sale_gamma: 64 + service_level: 0.95 + SKU760: + constraint: null + cost: 176 + init_stock: 255 + max_stock: 1000 + price: 279 + sale_gamma: 51 + service_level: 0.95 + SKU761: + constraint: G(stock_constraint) + cost: 224 + init_stock: 275 + max_stock: 1000 + price: 250 + sale_gamma: 55 + service_level: 0.95 + SKU762: + constraint: G(stock_constraint) + cost: 264 + init_stock: 153 + max_stock: 1000 + price: 351 + sale_gamma: 51 + service_level: 0.95 + SKU763: + constraint: null + cost: 385 + init_stock: 316 + max_stock: 1000 + price: 677 + sale_gamma: 79 + service_level: 0.95 + SKU764: + constraint: null + cost: 349 + init_stock: 68 + max_stock: 1000 + price: 596 + sale_gamma: 34 + service_level: 0.95 + SKU765: + constraint: null + cost: 345 + init_stock: 52 + max_stock: 1000 + price: 472 + sale_gamma: 13 + service_level: 0.95 + SKU766: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 478 + init_stock: 140 + max_stock: 1000 + price: 855 + sale_gamma: 20 + service_level: 0.95 + SKU767: + constraint: null + cost: 95 + init_stock: 456 + max_stock: 1000 + price: 160 + sale_gamma: 76 + service_level: 0.95 + SKU768: + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 644 + max_stock: 1000 + price: 244 + sale_gamma: 92 + service_level: 0.95 + SKU769: + constraint: null + cost: 24 + init_stock: 87 + max_stock: 1000 + price: 43 + sale_gamma: 29 + service_level: 0.95 + SKU77: + constraint: null + cost: 409 + init_stock: 144 + max_stock: 1000 + price: 687 + sale_gamma: 72 + service_level: 0.95 + SKU770: + constraint: null + cost: 150 + init_stock: 186 + max_stock: 1000 + price: 166 + sale_gamma: 62 + service_level: 0.95 + SKU771: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 101 + init_stock: 35 + max_stock: 1000 + price: 182 + sale_gamma: 7 + service_level: 0.95 + SKU772: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 36 + max_stock: 1000 + price: 281 + sale_gamma: 6 + service_level: 0.95 + SKU773: + constraint: null + cost: 84 + init_stock: 368 + max_stock: 1000 + price: 117 + sale_gamma: 92 + service_level: 0.95 + SKU774: + constraint: null + cost: 447 + init_stock: 118 + max_stock: 1000 + price: 746 + sale_gamma: 59 + service_level: 0.95 + SKU775: + constraint: null + cost: 175 + init_stock: 688 + max_stock: 1000 + price: 192 + sale_gamma: 86 + service_level: 0.95 + SKU776: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 103 + init_stock: 425 + max_stock: 1000 + price: 172 + sale_gamma: 85 + service_level: 0.95 + SKU777: + constraint: null + cost: 292 + init_stock: 171 + max_stock: 1000 + price: 347 + sale_gamma: 57 + service_level: 0.95 + SKU778: + constraint: null + cost: 203 + init_stock: 40 + max_stock: 1000 + price: 288 + sale_gamma: 8 + service_level: 0.95 + SKU779: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 117 + init_stock: 215 + max_stock: 1000 + price: 186 + sale_gamma: 43 + service_level: 0.95 + SKU78: + constraint: null + cost: 234 + init_stock: 91 + max_stock: 1000 + price: 400 + sale_gamma: 13 + service_level: 0.95 + SKU780: + constraint: G(low_profit -> low_stock_constraint) + cost: 69 + init_stock: 410 + max_stock: 1000 + price: 102 + sale_gamma: 82 + service_level: 0.95 + SKU781: + constraint: null + cost: 372 + init_stock: 144 + max_stock: 1000 + price: 528 + sale_gamma: 36 + service_level: 0.95 + SKU782: + constraint: G(low_profit -> low_stock_constraint) + cost: 27 + init_stock: 70 + max_stock: 1000 + price: 35 + sale_gamma: 10 + service_level: 0.95 + SKU783: + constraint: null + cost: 87 + init_stock: 324 + max_stock: 1000 + price: 139 + sale_gamma: 81 + service_level: 0.95 + SKU784: + constraint: null + cost: 309 + init_stock: 312 + max_stock: 1000 + price: 577 + sale_gamma: 52 + service_level: 0.95 + SKU785: + constraint: null + cost: 191 + init_stock: 210 + max_stock: 1000 + price: 255 + sale_gamma: 70 + service_level: 0.95 + SKU786: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 91 + init_stock: 132 + max_stock: 1000 + price: 102 + sale_gamma: 22 + service_level: 0.95 + SKU787: + constraint: null + cost: 360 + init_stock: 366 + max_stock: 1000 + price: 658 + sale_gamma: 61 + service_level: 0.95 + SKU788: + constraint: G(stock_constraint) + cost: 351 + init_stock: 112 + max_stock: 1000 + price: 417 + sale_gamma: 28 + service_level: 0.95 + SKU789: + constraint: G(stock_constraint) + cost: 153 + init_stock: 232 + max_stock: 1000 + price: 298 + sale_gamma: 58 + service_level: 0.95 + SKU79: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 245 + init_stock: 66 + max_stock: 1000 + price: 276 + sale_gamma: 11 + service_level: 0.95 + SKU790: + constraint: null + cost: 417 + init_stock: 330 + max_stock: 1000 + price: 704 + sale_gamma: 66 + service_level: 0.95 + SKU791: + constraint: G(stock_constraint) + cost: 134 + init_stock: 56 + max_stock: 1000 + price: 155 + sale_gamma: 28 + service_level: 0.95 + SKU792: + constraint: G(low_profit -> low_stock_constraint) + cost: 313 + init_stock: 84 + max_stock: 1000 + price: 494 + sale_gamma: 21 + service_level: 0.95 + SKU793: + constraint: null + cost: 195 + init_stock: 532 + max_stock: 1000 + price: 282 + sale_gamma: 76 + service_level: 0.95 + SKU794: + constraint: null + cost: 325 + init_stock: 400 + max_stock: 1000 + price: 454 + sale_gamma: 80 + service_level: 0.95 + SKU795: + constraint: null + cost: 276 + init_stock: 288 + max_stock: 1000 + price: 549 + sale_gamma: 48 + service_level: 0.95 + SKU796: + constraint: null + cost: 447 + init_stock: 294 + max_stock: 1000 + price: 885 + sale_gamma: 49 + service_level: 0.95 + SKU797: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 100 + init_stock: 234 + max_stock: 1000 + price: 119 + sale_gamma: 39 + service_level: 0.95 + SKU798: + constraint: null + cost: 426 + init_stock: 180 + max_stock: 1000 + price: 630 + sale_gamma: 30 + service_level: 0.95 + SKU799: + constraint: null + cost: 63 + init_stock: 21 + max_stock: 1000 + price: 78 + sale_gamma: 7 + service_level: 0.95 + SKU8: + constraint: G(stock_constraint) + cost: 125 + init_stock: 252 + max_stock: 1000 + price: 196 + sale_gamma: 63 + service_level: 0.95 + SKU80: + constraint: G(stock_constraint) + cost: 163 + init_stock: 420 + max_stock: 1000 + price: 270 + sale_gamma: 70 + service_level: 0.95 + SKU800: + constraint: G(low_profit -> low_stock_constraint) + cost: 298 + init_stock: 470 + max_stock: 1000 + price: 455 + sale_gamma: 94 + service_level: 0.95 + SKU801: + constraint: G(low_profit -> low_stock_constraint) + cost: 389 + init_stock: 216 + max_stock: 1000 + price: 672 + sale_gamma: 36 + service_level: 0.95 + SKU802: + constraint: G(stock_constraint) + cost: 173 + init_stock: 310 + max_stock: 1000 + price: 281 + sale_gamma: 62 + service_level: 0.95 + SKU803: + constraint: null + cost: 272 + init_stock: 95 + max_stock: 1000 + price: 544 + sale_gamma: 19 + service_level: 0.95 + SKU804: + constraint: null + cost: 390 + init_stock: 188 + max_stock: 1000 + price: 491 + sale_gamma: 47 + service_level: 0.95 + SKU805: + constraint: null + cost: 61 + init_stock: 132 + max_stock: 1000 + price: 118 + sale_gamma: 33 + service_level: 0.95 + SKU806: + constraint: null + cost: 158 + init_stock: 156 + max_stock: 1000 + price: 186 + sale_gamma: 52 + service_level: 0.95 + SKU807: + constraint: null + cost: 453 + init_stock: 182 + max_stock: 1000 + price: 656 + sale_gamma: 26 + service_level: 0.95 + SKU808: + constraint: G(stock_constraint) + cost: 249 + init_stock: 182 + max_stock: 1000 + price: 435 + sale_gamma: 91 + service_level: 0.95 + SKU809: + constraint: null + cost: 55 + init_stock: 390 + max_stock: 1000 + price: 69 + sale_gamma: 65 + service_level: 0.95 + SKU81: + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 528 + max_stock: 1000 + price: 223 + sale_gamma: 66 + service_level: 0.95 + SKU810: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 276 + init_stock: 42 + max_stock: 1000 + price: 322 + sale_gamma: 6 + service_level: 0.95 + SKU811: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 254 + init_stock: 148 + max_stock: 1000 + price: 322 + sale_gamma: 37 + service_level: 0.95 + SKU812: + constraint: null + cost: 252 + init_stock: 320 + max_stock: 1000 + price: 337 + sale_gamma: 80 + service_level: 0.95 + SKU813: + constraint: G(stock_constraint) + cost: 253 + init_stock: 49 + max_stock: 1000 + price: 333 + sale_gamma: 7 + service_level: 0.95 + SKU814: + constraint: null + cost: 485 + init_stock: 194 + max_stock: 1000 + price: 921 + sale_gamma: 97 + service_level: 0.95 + SKU815: + constraint: null + cost: 390 + init_stock: 168 + max_stock: 1000 + price: 429 + sale_gamma: 24 + service_level: 0.95 + SKU816: + constraint: null + cost: 152 + init_stock: 455 + max_stock: 1000 + price: 174 + sale_gamma: 91 + service_level: 0.95 + SKU817: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 227 + init_stock: 25 + max_stock: 1000 + price: 401 + sale_gamma: 5 + service_level: 0.95 + SKU818: + constraint: null + cost: 354 + init_stock: 215 + max_stock: 1000 + price: 534 + sale_gamma: 43 + service_level: 0.95 + SKU819: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 302 + init_stock: 135 + max_stock: 1000 + price: 480 + sale_gamma: 27 + service_level: 0.95 + SKU82: + constraint: null + cost: 290 + init_stock: 140 + max_stock: 1000 + price: 469 + sale_gamma: 28 + service_level: 0.95 + SKU820: + constraint: null + cost: 264 + init_stock: 410 + max_stock: 1000 + price: 464 + sale_gamma: 82 + service_level: 0.95 + SKU821: + constraint: G(stock_constraint) + cost: 99 + init_stock: 207 + max_stock: 1000 + price: 151 + sale_gamma: 69 + service_level: 0.95 + SKU822: + constraint: null + cost: 136 + init_stock: 490 + max_stock: 1000 + price: 266 + sale_gamma: 70 + service_level: 0.95 + SKU823: + constraint: null + cost: 75 + init_stock: 84 + max_stock: 1000 + price: 116 + sale_gamma: 28 + service_level: 0.95 + SKU824: + constraint: G(low_profit -> low_stock_constraint) + cost: 170 + init_stock: 420 + max_stock: 1000 + price: 200 + sale_gamma: 70 + service_level: 0.95 + SKU825: + constraint: null + cost: 214 + init_stock: 45 + max_stock: 1000 + price: 359 + sale_gamma: 15 + service_level: 0.95 + SKU826: + constraint: G(stock_constraint) + cost: 386 + init_stock: 330 + max_stock: 1000 + price: 428 + sale_gamma: 55 + service_level: 0.95 + SKU827: + constraint: G(stock_constraint) + cost: 148 + init_stock: 448 + max_stock: 1000 + price: 208 + sale_gamma: 64 + service_level: 0.95 + SKU828: + constraint: null + cost: 400 + init_stock: 276 + max_stock: 1000 + price: 627 + sale_gamma: 69 + service_level: 0.95 + SKU829: + constraint: null + cost: 61 + init_stock: 370 + max_stock: 1000 + price: 83 + sale_gamma: 74 + service_level: 0.95 + SKU83: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 68 + max_stock: 1000 + price: 535 + sale_gamma: 17 + service_level: 0.95 + SKU830: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 167 + init_stock: 144 + max_stock: 1000 + price: 252 + sale_gamma: 24 + service_level: 0.95 + SKU831: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 116 + max_stock: 1000 + price: 353 + sale_gamma: 29 + service_level: 0.95 + SKU832: + constraint: null + cost: 33 + init_stock: 164 + max_stock: 1000 + price: 48 + sale_gamma: 82 + service_level: 0.95 + SKU833: + constraint: G(low_profit -> low_stock_constraint) + cost: 400 + init_stock: 392 + max_stock: 1000 + price: 756 + sale_gamma: 98 + service_level: 0.95 + SKU834: + constraint: G(low_profit -> low_stock_constraint) + cost: 422 + init_stock: 10 + max_stock: 1000 + price: 662 + sale_gamma: 5 + service_level: 0.95 + SKU835: + constraint: null + cost: 440 + init_stock: 156 + max_stock: 1000 + price: 532 + sale_gamma: 52 + service_level: 0.95 + SKU836: + constraint: G(stock_constraint) + cost: 323 + init_stock: 192 + max_stock: 1000 + price: 552 + sale_gamma: 96 + service_level: 0.95 + SKU837: + constraint: null + cost: 373 + init_stock: 156 + max_stock: 1000 + price: 607 + sale_gamma: 26 + service_level: 0.95 + SKU838: + constraint: null + cost: 456 + init_stock: 308 + max_stock: 1000 + price: 711 + sale_gamma: 77 + service_level: 0.95 + SKU839: + constraint: null + cost: 473 + init_stock: 360 + max_stock: 1000 + price: 695 + sale_gamma: 60 + service_level: 0.95 + SKU84: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 318 + init_stock: 294 + max_stock: 1000 + price: 594 + sale_gamma: 42 + service_level: 0.95 + SKU840: + constraint: G(low_profit -> low_stock_constraint) + cost: 266 + init_stock: 150 + max_stock: 1000 + price: 436 + sale_gamma: 50 + service_level: 0.95 + SKU841: + constraint: G(stock_constraint) + cost: 285 + init_stock: 595 + max_stock: 1000 + price: 538 + sale_gamma: 85 + service_level: 0.95 + SKU842: + constraint: G(low_profit -> low_stock_constraint) + cost: 41 + init_stock: 252 + max_stock: 1000 + price: 70 + sale_gamma: 84 + service_level: 0.95 + SKU843: + constraint: null + cost: 360 + init_stock: 432 + max_stock: 1000 + price: 694 + sale_gamma: 72 + service_level: 0.95 + SKU844: + constraint: G(low_profit -> low_stock_constraint) + cost: 51 + init_stock: 474 + max_stock: 1000 + price: 63 + sale_gamma: 79 + service_level: 0.95 + SKU845: + constraint: G(low_profit -> low_stock_constraint) + cost: 288 + init_stock: 176 + max_stock: 1000 + price: 558 + sale_gamma: 22 + service_level: 0.95 + SKU846: + constraint: null + cost: 485 + init_stock: 234 + max_stock: 1000 + price: 940 + sale_gamma: 39 + service_level: 0.95 + SKU847: + constraint: G(low_profit -> low_stock_constraint) + cost: 388 + init_stock: 546 + max_stock: 1000 + price: 519 + sale_gamma: 91 + service_level: 0.95 + SKU848: + constraint: null + cost: 306 + init_stock: 184 + max_stock: 1000 + price: 358 + sale_gamma: 46 + service_level: 0.95 + SKU849: + constraint: G(low_profit -> low_stock_constraint) + cost: 320 + init_stock: 160 + max_stock: 1000 + price: 425 + sale_gamma: 40 + service_level: 0.95 + SKU85: + constraint: G(low_profit -> low_stock_constraint) + cost: 442 + init_stock: 400 + max_stock: 1000 + price: 583 + sale_gamma: 100 + service_level: 0.95 + SKU850: + constraint: G(stock_constraint) + cost: 183 + init_stock: 342 + max_stock: 1000 + price: 206 + sale_gamma: 57 + service_level: 0.95 + SKU851: + constraint: null + cost: 53 + init_stock: 384 + max_stock: 1000 + price: 96 + sale_gamma: 64 + service_level: 0.95 + SKU852: + constraint: null + cost: 306 + init_stock: 162 + max_stock: 1000 + price: 483 + sale_gamma: 54 + service_level: 0.95 + SKU853: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 386 + init_stock: 280 + max_stock: 1000 + price: 443 + sale_gamma: 56 + service_level: 0.95 + SKU854: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 212 + init_stock: 390 + max_stock: 1000 + price: 290 + sale_gamma: 65 + service_level: 0.95 + SKU855: + constraint: null + cost: 76 + init_stock: 432 + max_stock: 1000 + price: 114 + sale_gamma: 72 + service_level: 0.95 + SKU856: + constraint: G(low_profit -> low_stock_constraint) + cost: 380 + init_stock: 224 + max_stock: 1000 + price: 627 + sale_gamma: 32 + service_level: 0.95 + SKU857: + constraint: G(stock_constraint) + cost: 462 + init_stock: 500 + max_stock: 1000 + price: 646 + sale_gamma: 100 + service_level: 0.95 + SKU858: + constraint: null + cost: 80 + init_stock: 120 + max_stock: 1000 + price: 158 + sale_gamma: 24 + service_level: 0.95 + SKU859: + constraint: G(low_profit -> low_stock_constraint) + cost: 215 + init_stock: 224 + max_stock: 1000 + price: 298 + sale_gamma: 28 + service_level: 0.95 + SKU86: + constraint: null + cost: 386 + init_stock: 595 + max_stock: 1000 + price: 447 + sale_gamma: 85 + service_level: 0.95 + SKU860: + constraint: G(low_profit -> low_stock_constraint) + cost: 313 + init_stock: 105 + max_stock: 1000 + price: 435 + sale_gamma: 15 + service_level: 0.95 + SKU861: + constraint: null + cost: 477 + init_stock: 52 + max_stock: 1000 + price: 944 + sale_gamma: 13 + service_level: 0.95 + SKU862: + constraint: null + cost: 240 + init_stock: 64 + max_stock: 1000 + price: 412 + sale_gamma: 16 + service_level: 0.95 + SKU863: + constraint: null + cost: 470 + init_stock: 372 + max_stock: 1000 + price: 911 + sale_gamma: 93 + service_level: 0.95 + SKU864: + constraint: null + cost: 203 + init_stock: 114 + max_stock: 1000 + price: 341 + sale_gamma: 19 + service_level: 0.95 + SKU865: + constraint: G(low_profit -> low_stock_constraint) + cost: 144 + init_stock: 152 + max_stock: 1000 + price: 253 + sale_gamma: 19 + service_level: 0.95 + SKU866: + constraint: null + cost: 172 + init_stock: 264 + max_stock: 1000 + price: 201 + sale_gamma: 88 + service_level: 0.95 + SKU867: + constraint: G(low_profit -> low_stock_constraint) + cost: 499 + init_stock: 500 + max_stock: 1000 + price: 698 + sale_gamma: 100 + service_level: 0.95 + SKU868: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 41 + init_stock: 150 + max_stock: 1000 + price: 56 + sale_gamma: 50 + service_level: 0.95 + SKU869: + constraint: null + cost: 97 + init_stock: 388 + max_stock: 1000 + price: 111 + sale_gamma: 97 + service_level: 0.95 + SKU87: + constraint: null + cost: 368 + init_stock: 207 + max_stock: 1000 + price: 563 + sale_gamma: 69 + service_level: 0.95 + SKU870: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 281 + init_stock: 335 + max_stock: 1000 + price: 446 + sale_gamma: 67 + service_level: 0.95 + SKU871: + constraint: null + cost: 101 + init_stock: 396 + max_stock: 1000 + price: 112 + sale_gamma: 99 + service_level: 0.95 + SKU872: + constraint: null + cost: 133 + init_stock: 160 + max_stock: 1000 + price: 195 + sale_gamma: 32 + service_level: 0.95 + SKU873: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 423 + init_stock: 155 + max_stock: 1000 + price: 829 + sale_gamma: 31 + service_level: 0.95 + SKU874: + constraint: G(low_profit -> low_stock_constraint) + cost: 469 + init_stock: 360 + max_stock: 1000 + price: 689 + sale_gamma: 60 + service_level: 0.95 + SKU875: + constraint: G(stock_constraint) + cost: 51 + init_stock: 138 + max_stock: 1000 + price: 57 + sale_gamma: 23 + service_level: 0.95 + SKU876: + constraint: null + cost: 303 + init_stock: 427 + max_stock: 1000 + price: 427 + sale_gamma: 61 + service_level: 0.95 + SKU877: + constraint: null + cost: 40 + init_stock: 245 + max_stock: 1000 + price: 70 + sale_gamma: 35 + service_level: 0.95 + SKU878: + constraint: G(low_profit -> low_stock_constraint) + cost: 27 + init_stock: 476 + max_stock: 1000 + price: 32 + sale_gamma: 68 + service_level: 0.95 + SKU879: + constraint: null + cost: 150 + init_stock: 300 + max_stock: 1000 + price: 198 + sale_gamma: 100 + service_level: 0.95 + SKU88: + constraint: G(low_profit -> low_stock_constraint) + cost: 471 + init_stock: 36 + max_stock: 1000 + price: 824 + sale_gamma: 12 + service_level: 0.95 + SKU880: + constraint: null + cost: 433 + init_stock: 155 + max_stock: 1000 + price: 532 + sale_gamma: 31 + service_level: 0.95 + SKU881: + constraint: null + cost: 431 + init_stock: 420 + max_stock: 1000 + price: 560 + sale_gamma: 70 + service_level: 0.95 + SKU882: + constraint: G(stock_constraint) + cost: 194 + init_stock: 80 + max_stock: 1000 + price: 314 + sale_gamma: 16 + service_level: 0.95 + SKU883: + constraint: null + cost: 284 + init_stock: 152 + max_stock: 1000 + price: 479 + sale_gamma: 38 + service_level: 0.95 + SKU884: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 139 + init_stock: 195 + max_stock: 1000 + price: 222 + sale_gamma: 39 + service_level: 0.95 + SKU885: + constraint: G(low_profit -> low_stock_constraint) + cost: 49 + init_stock: 189 + max_stock: 1000 + price: 58 + sale_gamma: 27 + service_level: 0.95 + SKU886: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 584 + max_stock: 1000 + price: 602 + sale_gamma: 73 + service_level: 0.95 + SKU887: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 266 + init_stock: 174 + max_stock: 1000 + price: 494 + sale_gamma: 87 + service_level: 0.95 + SKU888: + constraint: G(low_profit -> low_stock_constraint) + cost: 143 + init_stock: 45 + max_stock: 1000 + price: 220 + sale_gamma: 9 + service_level: 0.95 + SKU889: + constraint: null + cost: 101 + init_stock: 147 + max_stock: 1000 + price: 199 + sale_gamma: 21 + service_level: 0.95 + SKU89: + constraint: null + cost: 138 + init_stock: 744 + max_stock: 1000 + price: 269 + sale_gamma: 93 + service_level: 0.95 + SKU890: + constraint: G(low_profit -> low_stock_constraint) + cost: 161 + init_stock: 600 + max_stock: 1000 + price: 247 + sale_gamma: 100 + service_level: 0.95 + SKU891: + constraint: null + cost: 356 + init_stock: 176 + max_stock: 1000 + price: 615 + sale_gamma: 22 + service_level: 0.95 + SKU892: + constraint: null + cost: 313 + init_stock: 552 + max_stock: 1000 + price: 516 + sale_gamma: 92 + service_level: 0.95 + SKU893: + constraint: null + cost: 229 + init_stock: 594 + max_stock: 1000 + price: 302 + sale_gamma: 99 + service_level: 0.95 + SKU894: + constraint: null + cost: 129 + init_stock: 222 + max_stock: 1000 + price: 176 + sale_gamma: 74 + service_level: 0.95 + SKU895: + constraint: null + cost: 230 + init_stock: 39 + max_stock: 1000 + price: 301 + sale_gamma: 13 + service_level: 0.95 + SKU896: + constraint: null + cost: 289 + init_stock: 400 + max_stock: 1000 + price: 465 + sale_gamma: 80 + service_level: 0.95 + SKU897: + constraint: null + cost: 393 + init_stock: 432 + max_stock: 1000 + price: 640 + sale_gamma: 72 + service_level: 0.95 + SKU898: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 477 + init_stock: 44 + max_stock: 1000 + price: 615 + sale_gamma: 11 + service_level: 0.95 + SKU899: + constraint: null + cost: 233 + init_stock: 240 + max_stock: 1000 + price: 309 + sale_gamma: 48 + service_level: 0.95 + SKU9: + constraint: null + cost: 166 + init_stock: 384 + max_stock: 1000 + price: 247 + sale_gamma: 96 + service_level: 0.95 + SKU90: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 454 + init_stock: 204 + max_stock: 1000 + price: 726 + sale_gamma: 51 + service_level: 0.95 + SKU900: + constraint: null + cost: 158 + init_stock: 165 + max_stock: 1000 + price: 263 + sale_gamma: 55 + service_level: 0.95 + SKU901: + constraint: G(stock_constraint) + cost: 215 + init_stock: 203 + max_stock: 1000 + price: 320 + sale_gamma: 29 + service_level: 0.95 + SKU902: + constraint: G(low_profit -> low_stock_constraint) + cost: 125 + init_stock: 560 + max_stock: 1000 + price: 205 + sale_gamma: 80 + service_level: 0.95 + SKU903: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 357 + init_stock: 266 + max_stock: 1000 + price: 517 + sale_gamma: 38 + service_level: 0.95 + SKU904: + constraint: G(low_profit -> low_stock_constraint) + cost: 496 + init_stock: 255 + max_stock: 1000 + price: 605 + sale_gamma: 51 + service_level: 0.95 + SKU905: + constraint: null + cost: 249 + init_stock: 217 + max_stock: 1000 + price: 383 + sale_gamma: 31 + service_level: 0.95 + SKU906: + constraint: G(low_profit -> low_stock_constraint) + cost: 166 + init_stock: 156 + max_stock: 1000 + price: 273 + sale_gamma: 52 + service_level: 0.95 + SKU907: + constraint: null + cost: 22 + init_stock: 498 + max_stock: 1000 + price: 33 + sale_gamma: 83 + service_level: 0.95 + SKU908: + constraint: null + cost: 408 + init_stock: 546 + max_stock: 1000 + price: 595 + sale_gamma: 78 + service_level: 0.95 + SKU909: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 482 + init_stock: 480 + max_stock: 1000 + price: 703 + sale_gamma: 96 + service_level: 0.95 + SKU91: + constraint: G(low_profit -> low_stock_constraint) + cost: 303 + init_stock: 48 + max_stock: 1000 + price: 463 + sale_gamma: 16 + service_level: 0.95 + SKU910: + constraint: null + cost: 226 + init_stock: 132 + max_stock: 1000 + price: 350 + sale_gamma: 33 + service_level: 0.95 + SKU911: + constraint: null + cost: 461 + init_stock: 210 + max_stock: 1000 + price: 686 + sale_gamma: 70 + service_level: 0.95 + SKU912: + constraint: null + cost: 236 + init_stock: 135 + max_stock: 1000 + price: 328 + sale_gamma: 27 + service_level: 0.95 + SKU913: + constraint: null + cost: 322 + init_stock: 184 + max_stock: 1000 + price: 518 + sale_gamma: 46 + service_level: 0.95 + SKU914: + constraint: null + cost: 272 + init_stock: 434 + max_stock: 1000 + price: 446 + sale_gamma: 62 + service_level: 0.95 + SKU915: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 337 + init_stock: 280 + max_stock: 1000 + price: 545 + sale_gamma: 56 + service_level: 0.95 + SKU916: + constraint: null + cost: 337 + init_stock: 534 + max_stock: 1000 + price: 647 + sale_gamma: 89 + service_level: 0.95 + SKU917: + constraint: G(low_profit -> low_stock_constraint) + cost: 258 + init_stock: 120 + max_stock: 1000 + price: 366 + sale_gamma: 30 + service_level: 0.95 + SKU918: + constraint: G(stock_constraint) + cost: 148 + init_stock: 330 + max_stock: 1000 + price: 224 + sale_gamma: 55 + service_level: 0.95 + SKU919: + constraint: G(stock_constraint) + cost: 393 + init_stock: 125 + max_stock: 1000 + price: 711 + sale_gamma: 25 + service_level: 0.95 + SKU92: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 252 + max_stock: 1000 + price: 466 + sale_gamma: 63 + service_level: 0.95 + SKU920: + constraint: null + cost: 357 + init_stock: 344 + max_stock: 1000 + price: 696 + sale_gamma: 86 + service_level: 0.95 + SKU921: + constraint: G(low_profit -> low_stock_constraint) + cost: 227 + init_stock: 198 + max_stock: 1000 + price: 419 + sale_gamma: 66 + service_level: 0.95 + SKU922: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 112 + init_stock: 201 + max_stock: 1000 + price: 123 + sale_gamma: 67 + service_level: 0.95 + SKU923: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 496 + init_stock: 348 + max_stock: 1000 + price: 828 + sale_gamma: 58 + service_level: 0.95 + SKU924: + constraint: null + cost: 316 + init_stock: 425 + max_stock: 1000 + price: 423 + sale_gamma: 85 + service_level: 0.95 + SKU925: + constraint: null + cost: 360 + init_stock: 90 + max_stock: 1000 + price: 716 + sale_gamma: 15 + service_level: 0.95 + SKU926: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 360 + init_stock: 469 + max_stock: 1000 + price: 475 + sale_gamma: 67 + service_level: 0.95 + SKU927: + constraint: G(stock_constraint) + cost: 260 + init_stock: 147 + max_stock: 1000 + price: 439 + sale_gamma: 21 + service_level: 0.95 + SKU928: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 491 + init_stock: 415 + max_stock: 1000 + price: 869 + sale_gamma: 83 + service_level: 0.95 + SKU929: + constraint: null + cost: 359 + init_stock: 800 + max_stock: 1000 + price: 470 + sale_gamma: 100 + service_level: 0.95 + SKU93: + constraint: null + cost: 404 + init_stock: 268 + max_stock: 1000 + price: 557 + sale_gamma: 67 + service_level: 0.95 + SKU930: + constraint: G(low_profit -> low_stock_constraint) + cost: 198 + init_stock: 196 + max_stock: 1000 + price: 247 + sale_gamma: 28 + service_level: 0.95 + SKU931: + constraint: null + cost: 71 + init_stock: 56 + max_stock: 1000 + price: 101 + sale_gamma: 14 + service_level: 0.95 + SKU932: + constraint: null + cost: 163 + init_stock: 264 + max_stock: 1000 + price: 283 + sale_gamma: 66 + service_level: 0.95 + SKU933: + constraint: null + cost: 113 + init_stock: 156 + max_stock: 1000 + price: 179 + sale_gamma: 78 + service_level: 0.95 + SKU934: + constraint: G(stock_constraint) + cost: 219 + init_stock: 469 + max_stock: 1000 + price: 346 + sale_gamma: 67 + service_level: 0.95 + SKU935: + constraint: null + cost: 364 + init_stock: 376 + max_stock: 1000 + price: 527 + sale_gamma: 94 + service_level: 0.95 + SKU936: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 24 + init_stock: 20 + max_stock: 1000 + price: 41 + sale_gamma: 5 + service_level: 0.95 + SKU937: + constraint: G(stock_constraint) + cost: 135 + init_stock: 85 + max_stock: 1000 + price: 216 + sale_gamma: 17 + service_level: 0.95 + SKU938: + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 63 + max_stock: 1000 + price: 704 + sale_gamma: 21 + service_level: 0.95 + SKU939: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 173 + init_stock: 413 + max_stock: 1000 + price: 219 + sale_gamma: 59 + service_level: 0.95 + SKU94: + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 235 + max_stock: 1000 + price: 261 + sale_gamma: 47 + service_level: 0.95 + SKU940: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 14 + init_stock: 279 + max_stock: 1000 + price: 24 + sale_gamma: 93 + service_level: 0.95 + SKU941: + constraint: G(stock_constraint) + cost: 80 + init_stock: 456 + max_stock: 1000 + price: 132 + sale_gamma: 57 + service_level: 0.95 + SKU942: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 202 + init_stock: 65 + max_stock: 1000 + price: 303 + sale_gamma: 13 + service_level: 0.95 + SKU943: + constraint: null + cost: 138 + init_stock: 245 + max_stock: 1000 + price: 182 + sale_gamma: 49 + service_level: 0.95 + SKU944: + constraint: null + cost: 196 + init_stock: 132 + max_stock: 1000 + price: 356 + sale_gamma: 44 + service_level: 0.95 + SKU945: + constraint: null + cost: 141 + init_stock: 85 + max_stock: 1000 + price: 176 + sale_gamma: 17 + service_level: 0.95 + SKU946: + constraint: null + cost: 325 + init_stock: 294 + max_stock: 1000 + price: 555 + sale_gamma: 49 + service_level: 0.95 + SKU947: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 338 + init_stock: 637 + max_stock: 1000 + price: 547 + sale_gamma: 91 + service_level: 0.95 + SKU948: + constraint: null + cost: 425 + init_stock: 112 + max_stock: 1000 + price: 476 + sale_gamma: 28 + service_level: 0.95 + SKU949: + constraint: G(stock_constraint) + cost: 309 + init_stock: 15 + max_stock: 1000 + price: 522 + sale_gamma: 5 + service_level: 0.95 + SKU95: + constraint: null + cost: 136 + init_stock: 84 + max_stock: 1000 + price: 214 + sale_gamma: 21 + service_level: 0.95 + SKU950: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 102 + init_stock: 324 + max_stock: 1000 + price: 128 + sale_gamma: 54 + service_level: 0.95 + SKU951: + constraint: G(low_profit -> low_stock_constraint) + cost: 75 + init_stock: 90 + max_stock: 1000 + price: 117 + sale_gamma: 18 + service_level: 0.95 + SKU952: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 156 + init_stock: 66 + max_stock: 1000 + price: 276 + sale_gamma: 11 + service_level: 0.95 + SKU953: + constraint: null + cost: 138 + init_stock: 245 + max_stock: 1000 + price: 230 + sale_gamma: 35 + service_level: 0.95 + SKU954: + constraint: null + cost: 296 + init_stock: 430 + max_stock: 1000 + price: 444 + sale_gamma: 86 + service_level: 0.95 + SKU955: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 55 + init_stock: 189 + max_stock: 1000 + price: 85 + sale_gamma: 63 + service_level: 0.95 + SKU956: + constraint: null + cost: 282 + init_stock: 425 + max_stock: 1000 + price: 397 + sale_gamma: 85 + service_level: 0.95 + SKU957: + constraint: null + cost: 305 + init_stock: 306 + max_stock: 1000 + price: 466 + sale_gamma: 51 + service_level: 0.95 + SKU958: + constraint: null + cost: 369 + init_stock: 462 + max_stock: 1000 + price: 704 + sale_gamma: 66 + service_level: 0.95 + SKU959: + constraint: null + cost: 81 + init_stock: 164 + max_stock: 1000 + price: 127 + sale_gamma: 82 + service_level: 0.95 + SKU96: + constraint: G(low_profit -> low_stock_constraint) + cost: 176 + init_stock: 264 + max_stock: 1000 + price: 329 + sale_gamma: 44 + service_level: 0.95 + SKU960: + constraint: null + cost: 147 + init_stock: 42 + max_stock: 1000 + price: 235 + sale_gamma: 6 + service_level: 0.95 + SKU961: + constraint: null + cost: 264 + init_stock: 176 + max_stock: 1000 + price: 290 + sale_gamma: 44 + service_level: 0.95 + SKU962: + constraint: null + cost: 354 + init_stock: 176 + max_stock: 1000 + price: 392 + sale_gamma: 44 + service_level: 0.95 + SKU963: + constraint: null + cost: 349 + init_stock: 63 + max_stock: 1000 + price: 523 + sale_gamma: 21 + service_level: 0.95 + SKU964: + constraint: null + cost: 244 + init_stock: 219 + max_stock: 1000 + price: 480 + sale_gamma: 73 + service_level: 0.95 + SKU965: + constraint: G(stock_constraint) + cost: 124 + init_stock: 255 + max_stock: 1000 + price: 199 + sale_gamma: 51 + service_level: 0.95 + SKU966: + constraint: G(stock_constraint) + cost: 302 + init_stock: 308 + max_stock: 1000 + price: 474 + sale_gamma: 44 + service_level: 0.95 + SKU967: + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 270 + max_stock: 1000 + price: 111 + sale_gamma: 45 + service_level: 0.95 + SKU968: + constraint: G(stock_constraint) + cost: 281 + init_stock: 294 + max_stock: 1000 + price: 528 + sale_gamma: 49 + service_level: 0.95 + SKU969: + constraint: G(low_profit -> low_stock_constraint) + cost: 249 + init_stock: 24 + max_stock: 1000 + price: 328 + sale_gamma: 6 + service_level: 0.95 + SKU97: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 28 + init_stock: 180 + max_stock: 1000 + price: 43 + sale_gamma: 30 + service_level: 0.95 + SKU970: + constraint: null + cost: 244 + init_stock: 30 + max_stock: 1000 + price: 373 + sale_gamma: 5 + service_level: 0.95 + SKU971: + constraint: null + cost: 368 + init_stock: 219 + max_stock: 1000 + price: 426 + sale_gamma: 73 + service_level: 0.95 + SKU972: + constraint: G(stock_constraint) + cost: 209 + init_stock: 345 + max_stock: 1000 + price: 307 + sale_gamma: 69 + service_level: 0.95 + SKU973: + constraint: null + cost: 271 + init_stock: 33 + max_stock: 1000 + price: 447 + sale_gamma: 11 + service_level: 0.95 + SKU974: + constraint: null + cost: 170 + init_stock: 192 + max_stock: 1000 + price: 285 + sale_gamma: 32 + service_level: 0.95 + SKU975: + constraint: G(stock_constraint) + cost: 198 + init_stock: 88 + max_stock: 1000 + price: 334 + sale_gamma: 22 + service_level: 0.95 + SKU976: + constraint: G(stock_constraint) + cost: 178 + init_stock: 75 + max_stock: 1000 + price: 323 + sale_gamma: 15 + service_level: 0.95 + SKU977: + constraint: G(stock_constraint) + cost: 234 + init_stock: 324 + max_stock: 1000 + price: 269 + sale_gamma: 54 + service_level: 0.95 + SKU978: + constraint: G(stock_constraint) + cost: 470 + init_stock: 320 + max_stock: 1000 + price: 921 + sale_gamma: 64 + service_level: 0.95 + SKU979: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 443 + init_stock: 480 + max_stock: 1000 + price: 753 + sale_gamma: 96 + service_level: 0.95 + SKU98: + constraint: G(stock_constraint) + cost: 443 + init_stock: 70 + max_stock: 1000 + price: 527 + sale_gamma: 35 + service_level: 0.95 + SKU980: + constraint: null + cost: 39 + init_stock: 340 + max_stock: 1000 + price: 60 + sale_gamma: 68 + service_level: 0.95 + SKU981: + constraint: null + cost: 482 + init_stock: 360 + max_stock: 1000 + price: 607 + sale_gamma: 72 + service_level: 0.95 + SKU982: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 213 + init_stock: 208 + max_stock: 1000 + price: 374 + sale_gamma: 52 + service_level: 0.95 + SKU983: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 276 + max_stock: 1000 + price: 776 + sale_gamma: 92 + service_level: 0.95 + SKU984: + constraint: G(stock_constraint) + cost: 232 + init_stock: 637 + max_stock: 1000 + price: 327 + sale_gamma: 91 + service_level: 0.95 + SKU985: + constraint: null + cost: 290 + init_stock: 153 + max_stock: 1000 + price: 420 + sale_gamma: 51 + service_level: 0.95 + SKU986: + constraint: null + cost: 275 + init_stock: 136 + max_stock: 1000 + price: 313 + sale_gamma: 17 + service_level: 0.95 + SKU987: + constraint: null + cost: 434 + init_stock: 204 + max_stock: 1000 + price: 516 + sale_gamma: 34 + service_level: 0.95 + SKU988: + constraint: null + cost: 102 + init_stock: 69 + max_stock: 1000 + price: 166 + sale_gamma: 23 + service_level: 0.95 + SKU989: + constraint: null + cost: 484 + init_stock: 558 + max_stock: 1000 + price: 653 + sale_gamma: 93 + service_level: 0.95 + SKU99: + constraint: null + cost: 361 + init_stock: 225 + max_stock: 1000 + price: 581 + sale_gamma: 45 + service_level: 0.95 + SKU990: + constraint: G(low_profit -> low_stock_constraint) + cost: 108 + init_stock: 228 + max_stock: 1000 + price: 174 + sale_gamma: 57 + service_level: 0.95 + SKU991: + constraint: null + cost: 409 + init_stock: 152 + max_stock: 1000 + price: 576 + sale_gamma: 19 + service_level: 0.95 + SKU992: + constraint: null + cost: 434 + init_stock: 651 + max_stock: 1000 + price: 685 + sale_gamma: 93 + service_level: 0.95 + SKU993: + constraint: G(low_profit -> low_stock_constraint) + cost: 145 + init_stock: 602 + max_stock: 1000 + price: 201 + sale_gamma: 86 + service_level: 0.95 + SKU994: + constraint: G(low_profit -> low_stock_constraint) + cost: 470 + init_stock: 300 + max_stock: 1000 + price: 916 + sale_gamma: 50 + service_level: 0.95 + SKU995: + constraint: null + cost: 241 + init_stock: 532 + max_stock: 1000 + price: 310 + sale_gamma: 76 + service_level: 0.95 + SKU996: + constraint: G(stock_constraint) + cost: 260 + init_stock: 438 + max_stock: 1000 + price: 416 + sale_gamma: 73 + service_level: 0.95 + SKU997: + constraint: G(stock_constraint) + cost: 400 + init_stock: 102 + max_stock: 1000 + price: 727 + sale_gamma: 34 + service_level: 0.95 + SKU998: + constraint: G(low_profit -> low_stock_constraint) + cost: 447 + init_stock: 165 + max_stock: 1000 + price: 688 + sale_gamma: 55 + service_level: 0.95 + SKU999: + constraint: null + cost: 79 + init_stock: 161 + max_stock: 1000 + price: 86 + sale_gamma: 23 + service_level: 0.95 + grid: + facilities: + STORE0: + - 9 + - 8 + SUPPLIER0: + - 14 + - 16 + WAREHOUSE0: + - 16 + - 18 + size: + - 20 + - 20 + skus: + - id: 0 + name: SKU0 + - id: 1 + name: SKU1 + - id: 2 + name: SKU2 + - id: 3 + name: SKU3 + - id: 4 + name: SKU4 + - id: 5 + name: SKU5 + - id: 6 + name: SKU6 + - id: 7 + name: SKU7 + - id: 8 + name: SKU8 + - id: 9 + name: SKU9 + - id: 10 + name: SKU10 + - id: 11 + name: SKU11 + - id: 12 + name: SKU12 + - id: 13 + name: SKU13 + - id: 14 + name: SKU14 + - id: 15 + name: SKU15 + - id: 16 + name: SKU16 + - id: 17 + name: SKU17 + - id: 18 + name: SKU18 + - id: 19 + name: SKU19 + - id: 20 + name: SKU20 + - id: 21 + name: SKU21 + - id: 22 + name: SKU22 + - id: 23 + name: SKU23 + - id: 24 + name: SKU24 + - id: 25 + name: SKU25 + - id: 26 + name: SKU26 + - id: 27 + name: SKU27 + - id: 28 + name: SKU28 + - id: 29 + name: SKU29 + - id: 30 + name: SKU30 + - id: 31 + name: SKU31 + - id: 32 + name: SKU32 + - id: 33 + name: SKU33 + - id: 34 + name: SKU34 + - id: 35 + name: SKU35 + - id: 36 + name: SKU36 + - id: 37 + name: SKU37 + - id: 38 + name: SKU38 + - id: 39 + name: SKU39 + - id: 40 + name: SKU40 + - id: 41 + name: SKU41 + - id: 42 + name: SKU42 + - id: 43 + name: SKU43 + - id: 44 + name: SKU44 + - id: 45 + name: SKU45 + - id: 46 + name: SKU46 + - id: 47 + name: SKU47 + - id: 48 + name: SKU48 + - id: 49 + name: SKU49 + - id: 50 + name: SKU50 + - id: 51 + name: SKU51 + - id: 52 + name: SKU52 + - id: 53 + name: SKU53 + - id: 54 + name: SKU54 + - id: 55 + name: SKU55 + - id: 56 + name: SKU56 + - id: 57 + name: SKU57 + - id: 58 + name: SKU58 + - id: 59 + name: SKU59 + - id: 60 + name: SKU60 + - id: 61 + name: SKU61 + - id: 62 + name: SKU62 + - id: 63 + name: SKU63 + - id: 64 + name: SKU64 + - id: 65 + name: SKU65 + - id: 66 + name: SKU66 + - id: 67 + name: SKU67 + - id: 68 + name: SKU68 + - id: 69 + name: SKU69 + - id: 70 + name: SKU70 + - id: 71 + name: SKU71 + - id: 72 + name: SKU72 + - id: 73 + name: SKU73 + - id: 74 + name: SKU74 + - id: 75 + name: SKU75 + - id: 76 + name: SKU76 + - id: 77 + name: SKU77 + - id: 78 + name: SKU78 + - id: 79 + name: SKU79 + - id: 80 + name: SKU80 + - id: 81 + name: SKU81 + - id: 82 + name: SKU82 + - id: 83 + name: SKU83 + - id: 84 + name: SKU84 + - id: 85 + name: SKU85 + - id: 86 + name: SKU86 + - id: 87 + name: SKU87 + - id: 88 + name: SKU88 + - id: 89 + name: SKU89 + - id: 90 + name: SKU90 + - id: 91 + name: SKU91 + - id: 92 + name: SKU92 + - id: 93 + name: SKU93 + - id: 94 + name: SKU94 + - id: 95 + name: SKU95 + - id: 96 + name: SKU96 + - id: 97 + name: SKU97 + - id: 98 + name: SKU98 + - id: 99 + name: SKU99 + - id: 100 + name: SKU100 + - id: 101 + name: SKU101 + - id: 102 + name: SKU102 + - id: 103 + name: SKU103 + - id: 104 + name: SKU104 + - id: 105 + name: SKU105 + - id: 106 + name: SKU106 + - id: 107 + name: SKU107 + - id: 108 + name: SKU108 + - id: 109 + name: SKU109 + - id: 110 + name: SKU110 + - id: 111 + name: SKU111 + - id: 112 + name: SKU112 + - id: 113 + name: SKU113 + - id: 114 + name: SKU114 + - id: 115 + name: SKU115 + - id: 116 + name: SKU116 + - id: 117 + name: SKU117 + - id: 118 + name: SKU118 + - id: 119 + name: SKU119 + - id: 120 + name: SKU120 + - id: 121 + name: SKU121 + - id: 122 + name: SKU122 + - id: 123 + name: SKU123 + - id: 124 + name: SKU124 + - id: 125 + name: SKU125 + - id: 126 + name: SKU126 + - id: 127 + name: SKU127 + - id: 128 + name: SKU128 + - id: 129 + name: SKU129 + - id: 130 + name: SKU130 + - id: 131 + name: SKU131 + - id: 132 + name: SKU132 + - id: 133 + name: SKU133 + - id: 134 + name: SKU134 + - id: 135 + name: SKU135 + - id: 136 + name: SKU136 + - id: 137 + name: SKU137 + - id: 138 + name: SKU138 + - id: 139 + name: SKU139 + - id: 140 + name: SKU140 + - id: 141 + name: SKU141 + - id: 142 + name: SKU142 + - id: 143 + name: SKU143 + - id: 144 + name: SKU144 + - id: 145 + name: SKU145 + - id: 146 + name: SKU146 + - id: 147 + name: SKU147 + - id: 148 + name: SKU148 + - id: 149 + name: SKU149 + - id: 150 + name: SKU150 + - id: 151 + name: SKU151 + - id: 152 + name: SKU152 + - id: 153 + name: SKU153 + - id: 154 + name: SKU154 + - id: 155 + name: SKU155 + - id: 156 + name: SKU156 + - id: 157 + name: SKU157 + - id: 158 + name: SKU158 + - id: 159 + name: SKU159 + - id: 160 + name: SKU160 + - id: 161 + name: SKU161 + - id: 162 + name: SKU162 + - id: 163 + name: SKU163 + - id: 164 + name: SKU164 + - id: 165 + name: SKU165 + - id: 166 + name: SKU166 + - id: 167 + name: SKU167 + - id: 168 + name: SKU168 + - id: 169 + name: SKU169 + - id: 170 + name: SKU170 + - id: 171 + name: SKU171 + - id: 172 + name: SKU172 + - id: 173 + name: SKU173 + - id: 174 + name: SKU174 + - id: 175 + name: SKU175 + - id: 176 + name: SKU176 + - id: 177 + name: SKU177 + - id: 178 + name: SKU178 + - id: 179 + name: SKU179 + - id: 180 + name: SKU180 + - id: 181 + name: SKU181 + - id: 182 + name: SKU182 + - id: 183 + name: SKU183 + - id: 184 + name: SKU184 + - id: 185 + name: SKU185 + - id: 186 + name: SKU186 + - id: 187 + name: SKU187 + - id: 188 + name: SKU188 + - id: 189 + name: SKU189 + - id: 190 + name: SKU190 + - id: 191 + name: SKU191 + - id: 192 + name: SKU192 + - id: 193 + name: SKU193 + - id: 194 + name: SKU194 + - id: 195 + name: SKU195 + - id: 196 + name: SKU196 + - id: 197 + name: SKU197 + - id: 198 + name: SKU198 + - id: 199 + name: SKU199 + - id: 200 + name: SKU200 + - id: 201 + name: SKU201 + - id: 202 + name: SKU202 + - id: 203 + name: SKU203 + - id: 204 + name: SKU204 + - id: 205 + name: SKU205 + - id: 206 + name: SKU206 + - id: 207 + name: SKU207 + - id: 208 + name: SKU208 + - id: 209 + name: SKU209 + - id: 210 + name: SKU210 + - id: 211 + name: SKU211 + - id: 212 + name: SKU212 + - id: 213 + name: SKU213 + - id: 214 + name: SKU214 + - id: 215 + name: SKU215 + - id: 216 + name: SKU216 + - id: 217 + name: SKU217 + - id: 218 + name: SKU218 + - id: 219 + name: SKU219 + - id: 220 + name: SKU220 + - id: 221 + name: SKU221 + - id: 222 + name: SKU222 + - id: 223 + name: SKU223 + - id: 224 + name: SKU224 + - id: 225 + name: SKU225 + - id: 226 + name: SKU226 + - id: 227 + name: SKU227 + - id: 228 + name: SKU228 + - id: 229 + name: SKU229 + - id: 230 + name: SKU230 + - id: 231 + name: SKU231 + - id: 232 + name: SKU232 + - id: 233 + name: SKU233 + - id: 234 + name: SKU234 + - id: 235 + name: SKU235 + - id: 236 + name: SKU236 + - id: 237 + name: SKU237 + - id: 238 + name: SKU238 + - id: 239 + name: SKU239 + - id: 240 + name: SKU240 + - id: 241 + name: SKU241 + - id: 242 + name: SKU242 + - id: 243 + name: SKU243 + - id: 244 + name: SKU244 + - id: 245 + name: SKU245 + - id: 246 + name: SKU246 + - id: 247 + name: SKU247 + - id: 248 + name: SKU248 + - id: 249 + name: SKU249 + - id: 250 + name: SKU250 + - id: 251 + name: SKU251 + - id: 252 + name: SKU252 + - id: 253 + name: SKU253 + - id: 254 + name: SKU254 + - id: 255 + name: SKU255 + - id: 256 + name: SKU256 + - id: 257 + name: SKU257 + - id: 258 + name: SKU258 + - id: 259 + name: SKU259 + - id: 260 + name: SKU260 + - id: 261 + name: SKU261 + - id: 262 + name: SKU262 + - id: 263 + name: SKU263 + - id: 264 + name: SKU264 + - id: 265 + name: SKU265 + - id: 266 + name: SKU266 + - id: 267 + name: SKU267 + - id: 268 + name: SKU268 + - id: 269 + name: SKU269 + - id: 270 + name: SKU270 + - id: 271 + name: SKU271 + - id: 272 + name: SKU272 + - id: 273 + name: SKU273 + - id: 274 + name: SKU274 + - id: 275 + name: SKU275 + - id: 276 + name: SKU276 + - id: 277 + name: SKU277 + - id: 278 + name: SKU278 + - id: 279 + name: SKU279 + - id: 280 + name: SKU280 + - id: 281 + name: SKU281 + - id: 282 + name: SKU282 + - id: 283 + name: SKU283 + - id: 284 + name: SKU284 + - id: 285 + name: SKU285 + - id: 286 + name: SKU286 + - id: 287 + name: SKU287 + - id: 288 + name: SKU288 + - id: 289 + name: SKU289 + - id: 290 + name: SKU290 + - id: 291 + name: SKU291 + - id: 292 + name: SKU292 + - id: 293 + name: SKU293 + - id: 294 + name: SKU294 + - id: 295 + name: SKU295 + - id: 296 + name: SKU296 + - id: 297 + name: SKU297 + - id: 298 + name: SKU298 + - id: 299 + name: SKU299 + - id: 300 + name: SKU300 + - id: 301 + name: SKU301 + - id: 302 + name: SKU302 + - id: 303 + name: SKU303 + - id: 304 + name: SKU304 + - id: 305 + name: SKU305 + - id: 306 + name: SKU306 + - id: 307 + name: SKU307 + - id: 308 + name: SKU308 + - id: 309 + name: SKU309 + - id: 310 + name: SKU310 + - id: 311 + name: SKU311 + - id: 312 + name: SKU312 + - id: 313 + name: SKU313 + - id: 314 + name: SKU314 + - id: 315 + name: SKU315 + - id: 316 + name: SKU316 + - id: 317 + name: SKU317 + - id: 318 + name: SKU318 + - id: 319 + name: SKU319 + - id: 320 + name: SKU320 + - id: 321 + name: SKU321 + - id: 322 + name: SKU322 + - id: 323 + name: SKU323 + - id: 324 + name: SKU324 + - id: 325 + name: SKU325 + - id: 326 + name: SKU326 + - id: 327 + name: SKU327 + - id: 328 + name: SKU328 + - id: 329 + name: SKU329 + - id: 330 + name: SKU330 + - id: 331 + name: SKU331 + - id: 332 + name: SKU332 + - id: 333 + name: SKU333 + - id: 334 + name: SKU334 + - id: 335 + name: SKU335 + - id: 336 + name: SKU336 + - id: 337 + name: SKU337 + - id: 338 + name: SKU338 + - id: 339 + name: SKU339 + - id: 340 + name: SKU340 + - id: 341 + name: SKU341 + - id: 342 + name: SKU342 + - id: 343 + name: SKU343 + - id: 344 + name: SKU344 + - id: 345 + name: SKU345 + - id: 346 + name: SKU346 + - id: 347 + name: SKU347 + - id: 348 + name: SKU348 + - id: 349 + name: SKU349 + - id: 350 + name: SKU350 + - id: 351 + name: SKU351 + - id: 352 + name: SKU352 + - id: 353 + name: SKU353 + - id: 354 + name: SKU354 + - id: 355 + name: SKU355 + - id: 356 + name: SKU356 + - id: 357 + name: SKU357 + - id: 358 + name: SKU358 + - id: 359 + name: SKU359 + - id: 360 + name: SKU360 + - id: 361 + name: SKU361 + - id: 362 + name: SKU362 + - id: 363 + name: SKU363 + - id: 364 + name: SKU364 + - id: 365 + name: SKU365 + - id: 366 + name: SKU366 + - id: 367 + name: SKU367 + - id: 368 + name: SKU368 + - id: 369 + name: SKU369 + - id: 370 + name: SKU370 + - id: 371 + name: SKU371 + - id: 372 + name: SKU372 + - id: 373 + name: SKU373 + - id: 374 + name: SKU374 + - id: 375 + name: SKU375 + - id: 376 + name: SKU376 + - id: 377 + name: SKU377 + - id: 378 + name: SKU378 + - id: 379 + name: SKU379 + - id: 380 + name: SKU380 + - id: 381 + name: SKU381 + - id: 382 + name: SKU382 + - id: 383 + name: SKU383 + - id: 384 + name: SKU384 + - id: 385 + name: SKU385 + - id: 386 + name: SKU386 + - id: 387 + name: SKU387 + - id: 388 + name: SKU388 + - id: 389 + name: SKU389 + - id: 390 + name: SKU390 + - id: 391 + name: SKU391 + - id: 392 + name: SKU392 + - id: 393 + name: SKU393 + - id: 394 + name: SKU394 + - id: 395 + name: SKU395 + - id: 396 + name: SKU396 + - id: 397 + name: SKU397 + - id: 398 + name: SKU398 + - id: 399 + name: SKU399 + - id: 400 + name: SKU400 + - id: 401 + name: SKU401 + - id: 402 + name: SKU402 + - id: 403 + name: SKU403 + - id: 404 + name: SKU404 + - id: 405 + name: SKU405 + - id: 406 + name: SKU406 + - id: 407 + name: SKU407 + - id: 408 + name: SKU408 + - id: 409 + name: SKU409 + - id: 410 + name: SKU410 + - id: 411 + name: SKU411 + - id: 412 + name: SKU412 + - id: 413 + name: SKU413 + - id: 414 + name: SKU414 + - id: 415 + name: SKU415 + - id: 416 + name: SKU416 + - id: 417 + name: SKU417 + - id: 418 + name: SKU418 + - id: 419 + name: SKU419 + - id: 420 + name: SKU420 + - id: 421 + name: SKU421 + - id: 422 + name: SKU422 + - id: 423 + name: SKU423 + - id: 424 + name: SKU424 + - id: 425 + name: SKU425 + - id: 426 + name: SKU426 + - id: 427 + name: SKU427 + - id: 428 + name: SKU428 + - id: 429 + name: SKU429 + - id: 430 + name: SKU430 + - id: 431 + name: SKU431 + - id: 432 + name: SKU432 + - id: 433 + name: SKU433 + - id: 434 + name: SKU434 + - id: 435 + name: SKU435 + - id: 436 + name: SKU436 + - id: 437 + name: SKU437 + - id: 438 + name: SKU438 + - id: 439 + name: SKU439 + - id: 440 + name: SKU440 + - id: 441 + name: SKU441 + - id: 442 + name: SKU442 + - id: 443 + name: SKU443 + - id: 444 + name: SKU444 + - id: 445 + name: SKU445 + - id: 446 + name: SKU446 + - id: 447 + name: SKU447 + - id: 448 + name: SKU448 + - id: 449 + name: SKU449 + - id: 450 + name: SKU450 + - id: 451 + name: SKU451 + - id: 452 + name: SKU452 + - id: 453 + name: SKU453 + - id: 454 + name: SKU454 + - id: 455 + name: SKU455 + - id: 456 + name: SKU456 + - id: 457 + name: SKU457 + - id: 458 + name: SKU458 + - id: 459 + name: SKU459 + - id: 460 + name: SKU460 + - id: 461 + name: SKU461 + - id: 462 + name: SKU462 + - id: 463 + name: SKU463 + - id: 464 + name: SKU464 + - id: 465 + name: SKU465 + - id: 466 + name: SKU466 + - id: 467 + name: SKU467 + - id: 468 + name: SKU468 + - id: 469 + name: SKU469 + - id: 470 + name: SKU470 + - id: 471 + name: SKU471 + - id: 472 + name: SKU472 + - id: 473 + name: SKU473 + - id: 474 + name: SKU474 + - id: 475 + name: SKU475 + - id: 476 + name: SKU476 + - id: 477 + name: SKU477 + - id: 478 + name: SKU478 + - id: 479 + name: SKU479 + - id: 480 + name: SKU480 + - id: 481 + name: SKU481 + - id: 482 + name: SKU482 + - id: 483 + name: SKU483 + - id: 484 + name: SKU484 + - id: 485 + name: SKU485 + - id: 486 + name: SKU486 + - id: 487 + name: SKU487 + - id: 488 + name: SKU488 + - id: 489 + name: SKU489 + - id: 490 + name: SKU490 + - id: 491 + name: SKU491 + - id: 492 + name: SKU492 + - id: 493 + name: SKU493 + - id: 494 + name: SKU494 + - id: 495 + name: SKU495 + - id: 496 + name: SKU496 + - id: 497 + name: SKU497 + - id: 498 + name: SKU498 + - id: 499 + name: SKU499 + - id: 500 + name: SKU500 + - id: 501 + name: SKU501 + - id: 502 + name: SKU502 + - id: 503 + name: SKU503 + - id: 504 + name: SKU504 + - id: 505 + name: SKU505 + - id: 506 + name: SKU506 + - id: 507 + name: SKU507 + - id: 508 + name: SKU508 + - id: 509 + name: SKU509 + - id: 510 + name: SKU510 + - id: 511 + name: SKU511 + - id: 512 + name: SKU512 + - id: 513 + name: SKU513 + - id: 514 + name: SKU514 + - id: 515 + name: SKU515 + - id: 516 + name: SKU516 + - id: 517 + name: SKU517 + - id: 518 + name: SKU518 + - id: 519 + name: SKU519 + - id: 520 + name: SKU520 + - id: 521 + name: SKU521 + - id: 522 + name: SKU522 + - id: 523 + name: SKU523 + - id: 524 + name: SKU524 + - id: 525 + name: SKU525 + - id: 526 + name: SKU526 + - id: 527 + name: SKU527 + - id: 528 + name: SKU528 + - id: 529 + name: SKU529 + - id: 530 + name: SKU530 + - id: 531 + name: SKU531 + - id: 532 + name: SKU532 + - id: 533 + name: SKU533 + - id: 534 + name: SKU534 + - id: 535 + name: SKU535 + - id: 536 + name: SKU536 + - id: 537 + name: SKU537 + - id: 538 + name: SKU538 + - id: 539 + name: SKU539 + - id: 540 + name: SKU540 + - id: 541 + name: SKU541 + - id: 542 + name: SKU542 + - id: 543 + name: SKU543 + - id: 544 + name: SKU544 + - id: 545 + name: SKU545 + - id: 546 + name: SKU546 + - id: 547 + name: SKU547 + - id: 548 + name: SKU548 + - id: 549 + name: SKU549 + - id: 550 + name: SKU550 + - id: 551 + name: SKU551 + - id: 552 + name: SKU552 + - id: 553 + name: SKU553 + - id: 554 + name: SKU554 + - id: 555 + name: SKU555 + - id: 556 + name: SKU556 + - id: 557 + name: SKU557 + - id: 558 + name: SKU558 + - id: 559 + name: SKU559 + - id: 560 + name: SKU560 + - id: 561 + name: SKU561 + - id: 562 + name: SKU562 + - id: 563 + name: SKU563 + - id: 564 + name: SKU564 + - id: 565 + name: SKU565 + - id: 566 + name: SKU566 + - id: 567 + name: SKU567 + - id: 568 + name: SKU568 + - id: 569 + name: SKU569 + - id: 570 + name: SKU570 + - id: 571 + name: SKU571 + - id: 572 + name: SKU572 + - id: 573 + name: SKU573 + - id: 574 + name: SKU574 + - id: 575 + name: SKU575 + - id: 576 + name: SKU576 + - id: 577 + name: SKU577 + - id: 578 + name: SKU578 + - id: 579 + name: SKU579 + - id: 580 + name: SKU580 + - id: 581 + name: SKU581 + - id: 582 + name: SKU582 + - id: 583 + name: SKU583 + - id: 584 + name: SKU584 + - id: 585 + name: SKU585 + - id: 586 + name: SKU586 + - id: 587 + name: SKU587 + - id: 588 + name: SKU588 + - id: 589 + name: SKU589 + - id: 590 + name: SKU590 + - id: 591 + name: SKU591 + - id: 592 + name: SKU592 + - id: 593 + name: SKU593 + - id: 594 + name: SKU594 + - id: 595 + name: SKU595 + - id: 596 + name: SKU596 + - id: 597 + name: SKU597 + - id: 598 + name: SKU598 + - id: 599 + name: SKU599 + - id: 600 + name: SKU600 + - id: 601 + name: SKU601 + - id: 602 + name: SKU602 + - id: 603 + name: SKU603 + - id: 604 + name: SKU604 + - id: 605 + name: SKU605 + - id: 606 + name: SKU606 + - id: 607 + name: SKU607 + - id: 608 + name: SKU608 + - id: 609 + name: SKU609 + - id: 610 + name: SKU610 + - id: 611 + name: SKU611 + - id: 612 + name: SKU612 + - id: 613 + name: SKU613 + - id: 614 + name: SKU614 + - id: 615 + name: SKU615 + - id: 616 + name: SKU616 + - id: 617 + name: SKU617 + - id: 618 + name: SKU618 + - id: 619 + name: SKU619 + - id: 620 + name: SKU620 + - id: 621 + name: SKU621 + - id: 622 + name: SKU622 + - id: 623 + name: SKU623 + - id: 624 + name: SKU624 + - id: 625 + name: SKU625 + - id: 626 + name: SKU626 + - id: 627 + name: SKU627 + - id: 628 + name: SKU628 + - id: 629 + name: SKU629 + - id: 630 + name: SKU630 + - id: 631 + name: SKU631 + - id: 632 + name: SKU632 + - id: 633 + name: SKU633 + - id: 634 + name: SKU634 + - id: 635 + name: SKU635 + - id: 636 + name: SKU636 + - id: 637 + name: SKU637 + - id: 638 + name: SKU638 + - id: 639 + name: SKU639 + - id: 640 + name: SKU640 + - id: 641 + name: SKU641 + - id: 642 + name: SKU642 + - id: 643 + name: SKU643 + - id: 644 + name: SKU644 + - id: 645 + name: SKU645 + - id: 646 + name: SKU646 + - id: 647 + name: SKU647 + - id: 648 + name: SKU648 + - id: 649 + name: SKU649 + - id: 650 + name: SKU650 + - id: 651 + name: SKU651 + - id: 652 + name: SKU652 + - id: 653 + name: SKU653 + - id: 654 + name: SKU654 + - id: 655 + name: SKU655 + - id: 656 + name: SKU656 + - id: 657 + name: SKU657 + - id: 658 + name: SKU658 + - id: 659 + name: SKU659 + - id: 660 + name: SKU660 + - id: 661 + name: SKU661 + - id: 662 + name: SKU662 + - id: 663 + name: SKU663 + - id: 664 + name: SKU664 + - id: 665 + name: SKU665 + - id: 666 + name: SKU666 + - id: 667 + name: SKU667 + - id: 668 + name: SKU668 + - id: 669 + name: SKU669 + - id: 670 + name: SKU670 + - id: 671 + name: SKU671 + - id: 672 + name: SKU672 + - id: 673 + name: SKU673 + - id: 674 + name: SKU674 + - id: 675 + name: SKU675 + - id: 676 + name: SKU676 + - id: 677 + name: SKU677 + - id: 678 + name: SKU678 + - id: 679 + name: SKU679 + - id: 680 + name: SKU680 + - id: 681 + name: SKU681 + - id: 682 + name: SKU682 + - id: 683 + name: SKU683 + - id: 684 + name: SKU684 + - id: 685 + name: SKU685 + - id: 686 + name: SKU686 + - id: 687 + name: SKU687 + - id: 688 + name: SKU688 + - id: 689 + name: SKU689 + - id: 690 + name: SKU690 + - id: 691 + name: SKU691 + - id: 692 + name: SKU692 + - id: 693 + name: SKU693 + - id: 694 + name: SKU694 + - id: 695 + name: SKU695 + - id: 696 + name: SKU696 + - id: 697 + name: SKU697 + - id: 698 + name: SKU698 + - id: 699 + name: SKU699 + - id: 700 + name: SKU700 + - id: 701 + name: SKU701 + - id: 702 + name: SKU702 + - id: 703 + name: SKU703 + - id: 704 + name: SKU704 + - id: 705 + name: SKU705 + - id: 706 + name: SKU706 + - id: 707 + name: SKU707 + - id: 708 + name: SKU708 + - id: 709 + name: SKU709 + - id: 710 + name: SKU710 + - id: 711 + name: SKU711 + - id: 712 + name: SKU712 + - id: 713 + name: SKU713 + - id: 714 + name: SKU714 + - id: 715 + name: SKU715 + - id: 716 + name: SKU716 + - id: 717 + name: SKU717 + - id: 718 + name: SKU718 + - id: 719 + name: SKU719 + - id: 720 + name: SKU720 + - id: 721 + name: SKU721 + - id: 722 + name: SKU722 + - id: 723 + name: SKU723 + - id: 724 + name: SKU724 + - id: 725 + name: SKU725 + - id: 726 + name: SKU726 + - id: 727 + name: SKU727 + - id: 728 + name: SKU728 + - id: 729 + name: SKU729 + - id: 730 + name: SKU730 + - id: 731 + name: SKU731 + - id: 732 + name: SKU732 + - id: 733 + name: SKU733 + - id: 734 + name: SKU734 + - id: 735 + name: SKU735 + - id: 736 + name: SKU736 + - id: 737 + name: SKU737 + - id: 738 + name: SKU738 + - id: 739 + name: SKU739 + - id: 740 + name: SKU740 + - id: 741 + name: SKU741 + - id: 742 + name: SKU742 + - id: 743 + name: SKU743 + - id: 744 + name: SKU744 + - id: 745 + name: SKU745 + - id: 746 + name: SKU746 + - id: 747 + name: SKU747 + - id: 748 + name: SKU748 + - id: 749 + name: SKU749 + - id: 750 + name: SKU750 + - id: 751 + name: SKU751 + - id: 752 + name: SKU752 + - id: 753 + name: SKU753 + - id: 754 + name: SKU754 + - id: 755 + name: SKU755 + - id: 756 + name: SKU756 + - id: 757 + name: SKU757 + - id: 758 + name: SKU758 + - id: 759 + name: SKU759 + - id: 760 + name: SKU760 + - id: 761 + name: SKU761 + - id: 762 + name: SKU762 + - id: 763 + name: SKU763 + - id: 764 + name: SKU764 + - id: 765 + name: SKU765 + - id: 766 + name: SKU766 + - id: 767 + name: SKU767 + - id: 768 + name: SKU768 + - id: 769 + name: SKU769 + - id: 770 + name: SKU770 + - id: 771 + name: SKU771 + - id: 772 + name: SKU772 + - id: 773 + name: SKU773 + - id: 774 + name: SKU774 + - id: 775 + name: SKU775 + - id: 776 + name: SKU776 + - id: 777 + name: SKU777 + - id: 778 + name: SKU778 + - id: 779 + name: SKU779 + - id: 780 + name: SKU780 + - id: 781 + name: SKU781 + - id: 782 + name: SKU782 + - id: 783 + name: SKU783 + - id: 784 + name: SKU784 + - id: 785 + name: SKU785 + - id: 786 + name: SKU786 + - id: 787 + name: SKU787 + - id: 788 + name: SKU788 + - id: 789 + name: SKU789 + - id: 790 + name: SKU790 + - id: 791 + name: SKU791 + - id: 792 + name: SKU792 + - id: 793 + name: SKU793 + - id: 794 + name: SKU794 + - id: 795 + name: SKU795 + - id: 796 + name: SKU796 + - id: 797 + name: SKU797 + - id: 798 + name: SKU798 + - id: 799 + name: SKU799 + - id: 800 + name: SKU800 + - id: 801 + name: SKU801 + - id: 802 + name: SKU802 + - id: 803 + name: SKU803 + - id: 804 + name: SKU804 + - id: 805 + name: SKU805 + - id: 806 + name: SKU806 + - id: 807 + name: SKU807 + - id: 808 + name: SKU808 + - id: 809 + name: SKU809 + - id: 810 + name: SKU810 + - id: 811 + name: SKU811 + - id: 812 + name: SKU812 + - id: 813 + name: SKU813 + - id: 814 + name: SKU814 + - id: 815 + name: SKU815 + - id: 816 + name: SKU816 + - id: 817 + name: SKU817 + - id: 818 + name: SKU818 + - id: 819 + name: SKU819 + - id: 820 + name: SKU820 + - id: 821 + name: SKU821 + - id: 822 + name: SKU822 + - id: 823 + name: SKU823 + - id: 824 + name: SKU824 + - id: 825 + name: SKU825 + - id: 826 + name: SKU826 + - id: 827 + name: SKU827 + - id: 828 + name: SKU828 + - id: 829 + name: SKU829 + - id: 830 + name: SKU830 + - id: 831 + name: SKU831 + - id: 832 + name: SKU832 + - id: 833 + name: SKU833 + - id: 834 + name: SKU834 + - id: 835 + name: SKU835 + - id: 836 + name: SKU836 + - id: 837 + name: SKU837 + - id: 838 + name: SKU838 + - id: 839 + name: SKU839 + - id: 840 + name: SKU840 + - id: 841 + name: SKU841 + - id: 842 + name: SKU842 + - id: 843 + name: SKU843 + - id: 844 + name: SKU844 + - id: 845 + name: SKU845 + - id: 846 + name: SKU846 + - id: 847 + name: SKU847 + - id: 848 + name: SKU848 + - id: 849 + name: SKU849 + - id: 850 + name: SKU850 + - id: 851 + name: SKU851 + - id: 852 + name: SKU852 + - id: 853 + name: SKU853 + - id: 854 + name: SKU854 + - id: 855 + name: SKU855 + - id: 856 + name: SKU856 + - id: 857 + name: SKU857 + - id: 858 + name: SKU858 + - id: 859 + name: SKU859 + - id: 860 + name: SKU860 + - id: 861 + name: SKU861 + - id: 862 + name: SKU862 + - id: 863 + name: SKU863 + - id: 864 + name: SKU864 + - id: 865 + name: SKU865 + - id: 866 + name: SKU866 + - id: 867 + name: SKU867 + - id: 868 + name: SKU868 + - id: 869 + name: SKU869 + - id: 870 + name: SKU870 + - id: 871 + name: SKU871 + - id: 872 + name: SKU872 + - id: 873 + name: SKU873 + - id: 874 + name: SKU874 + - id: 875 + name: SKU875 + - id: 876 + name: SKU876 + - id: 877 + name: SKU877 + - id: 878 + name: SKU878 + - id: 879 + name: SKU879 + - id: 880 + name: SKU880 + - id: 881 + name: SKU881 + - id: 882 + name: SKU882 + - id: 883 + name: SKU883 + - id: 884 + name: SKU884 + - id: 885 + name: SKU885 + - id: 886 + name: SKU886 + - id: 887 + name: SKU887 + - id: 888 + name: SKU888 + - id: 889 + name: SKU889 + - id: 890 + name: SKU890 + - id: 891 + name: SKU891 + - id: 892 + name: SKU892 + - id: 893 + name: SKU893 + - id: 894 + name: SKU894 + - id: 895 + name: SKU895 + - id: 896 + name: SKU896 + - id: 897 + name: SKU897 + - id: 898 + name: SKU898 + - id: 899 + name: SKU899 + - id: 900 + name: SKU900 + - id: 901 + name: SKU901 + - id: 902 + name: SKU902 + - id: 903 + name: SKU903 + - id: 904 + name: SKU904 + - id: 905 + name: SKU905 + - id: 906 + name: SKU906 + - id: 907 + name: SKU907 + - id: 908 + name: SKU908 + - id: 909 + name: SKU909 + - id: 910 + name: SKU910 + - id: 911 + name: SKU911 + - id: 912 + name: SKU912 + - id: 913 + name: SKU913 + - id: 914 + name: SKU914 + - id: 915 + name: SKU915 + - id: 916 + name: SKU916 + - id: 917 + name: SKU917 + - id: 918 + name: SKU918 + - id: 919 + name: SKU919 + - id: 920 + name: SKU920 + - id: 921 + name: SKU921 + - id: 922 + name: SKU922 + - id: 923 + name: SKU923 + - id: 924 + name: SKU924 + - id: 925 + name: SKU925 + - id: 926 + name: SKU926 + - id: 927 + name: SKU927 + - id: 928 + name: SKU928 + - id: 929 + name: SKU929 + - id: 930 + name: SKU930 + - id: 931 + name: SKU931 + - id: 932 + name: SKU932 + - id: 933 + name: SKU933 + - id: 934 + name: SKU934 + - id: 935 + name: SKU935 + - id: 936 + name: SKU936 + - id: 937 + name: SKU937 + - id: 938 + name: SKU938 + - id: 939 + name: SKU939 + - id: 940 + name: SKU940 + - id: 941 + name: SKU941 + - id: 942 + name: SKU942 + - id: 943 + name: SKU943 + - id: 944 + name: SKU944 + - id: 945 + name: SKU945 + - id: 946 + name: SKU946 + - id: 947 + name: SKU947 + - id: 948 + name: SKU948 + - id: 949 + name: SKU949 + - id: 950 + name: SKU950 + - id: 951 + name: SKU951 + - id: 952 + name: SKU952 + - id: 953 + name: SKU953 + - id: 954 + name: SKU954 + - id: 955 + name: SKU955 + - id: 956 + name: SKU956 + - id: 957 + name: SKU957 + - id: 958 + name: SKU958 + - id: 959 + name: SKU959 + - id: 960 + name: SKU960 + - id: 961 + name: SKU961 + - id: 962 + name: SKU962 + - id: 963 + name: SKU963 + - id: 964 + name: SKU964 + - id: 965 + name: SKU965 + - id: 966 + name: SKU966 + - id: 967 + name: SKU967 + - id: 968 + name: SKU968 + - id: 969 + name: SKU969 + - id: 970 + name: SKU970 + - id: 971 + name: SKU971 + - id: 972 + name: SKU972 + - id: 973 + name: SKU973 + - id: 974 + name: SKU974 + - id: 975 + name: SKU975 + - id: 976 + name: SKU976 + - id: 977 + name: SKU977 + - id: 978 + name: SKU978 + - id: 979 + name: SKU979 + - id: 980 + name: SKU980 + - id: 981 + name: SKU981 + - id: 982 + name: SKU982 + - id: 983 + name: SKU983 + - id: 984 + name: SKU984 + - id: 985 + name: SKU985 + - id: 986 + name: SKU986 + - id: 987 + name: SKU987 + - id: 988 + name: SKU988 + - id: 989 + name: SKU989 + - id: 990 + name: SKU990 + - id: 991 + name: SKU991 + - id: 992 + name: SKU992 + - id: 993 + name: SKU993 + - id: 994 + name: SKU994 + - id: 995 + name: SKU995 + - id: 996 + name: SKU996 + - id: 997 + name: SKU997 + - id: 998 + name: SKU998 + - id: 999 + name: SKU999 + topology: + STORE0: + SKU0: + - WAREHOUSE0 + SKU1: + - WAREHOUSE0 + SKU10: + - WAREHOUSE0 + SKU100: + - WAREHOUSE0 + SKU101: + - WAREHOUSE0 + SKU102: + - WAREHOUSE0 + SKU103: + - WAREHOUSE0 + SKU104: + - WAREHOUSE0 + SKU105: + - WAREHOUSE0 + SKU106: + - WAREHOUSE0 + SKU107: + - WAREHOUSE0 + SKU108: + - WAREHOUSE0 + SKU109: + - WAREHOUSE0 + SKU11: + - WAREHOUSE0 + SKU110: + - WAREHOUSE0 + SKU111: + - WAREHOUSE0 + SKU112: + - WAREHOUSE0 + SKU113: + - WAREHOUSE0 + SKU114: + - WAREHOUSE0 + SKU115: + - WAREHOUSE0 + SKU116: + - WAREHOUSE0 + SKU117: + - WAREHOUSE0 + SKU118: + - WAREHOUSE0 + SKU119: + - WAREHOUSE0 + SKU12: + - WAREHOUSE0 + SKU120: + - WAREHOUSE0 + SKU121: + - WAREHOUSE0 + SKU122: + - WAREHOUSE0 + SKU123: + - WAREHOUSE0 + SKU124: + - WAREHOUSE0 + SKU125: + - WAREHOUSE0 + SKU126: + - WAREHOUSE0 + SKU127: + - WAREHOUSE0 + SKU128: + - WAREHOUSE0 + SKU129: + - WAREHOUSE0 + SKU13: + - WAREHOUSE0 + SKU130: + - WAREHOUSE0 + SKU131: + - WAREHOUSE0 + SKU132: + - WAREHOUSE0 + SKU133: + - WAREHOUSE0 + SKU134: + - WAREHOUSE0 + SKU135: + - WAREHOUSE0 + SKU136: + - WAREHOUSE0 + SKU137: + - WAREHOUSE0 + SKU138: + - WAREHOUSE0 + SKU139: + - WAREHOUSE0 + SKU14: + - WAREHOUSE0 + SKU140: + - WAREHOUSE0 + SKU141: + - WAREHOUSE0 + SKU142: + - WAREHOUSE0 + SKU143: + - WAREHOUSE0 + SKU144: + - WAREHOUSE0 + SKU145: + - WAREHOUSE0 + SKU146: + - WAREHOUSE0 + SKU147: + - WAREHOUSE0 + SKU148: + - WAREHOUSE0 + SKU149: + - WAREHOUSE0 + SKU15: + - WAREHOUSE0 + SKU150: + - WAREHOUSE0 + SKU151: + - WAREHOUSE0 + SKU152: + - WAREHOUSE0 + SKU153: + - WAREHOUSE0 + SKU154: + - WAREHOUSE0 + SKU155: + - WAREHOUSE0 + SKU156: + - WAREHOUSE0 + SKU157: + - WAREHOUSE0 + SKU158: + - WAREHOUSE0 + SKU159: + - WAREHOUSE0 + SKU16: + - WAREHOUSE0 + SKU160: + - WAREHOUSE0 + SKU161: + - WAREHOUSE0 + SKU162: + - WAREHOUSE0 + SKU163: + - WAREHOUSE0 + SKU164: + - WAREHOUSE0 + SKU165: + - WAREHOUSE0 + SKU166: + - WAREHOUSE0 + SKU167: + - WAREHOUSE0 + SKU168: + - WAREHOUSE0 + SKU169: + - WAREHOUSE0 + SKU17: + - WAREHOUSE0 + SKU170: + - WAREHOUSE0 + SKU171: + - WAREHOUSE0 + SKU172: + - WAREHOUSE0 + SKU173: + - WAREHOUSE0 + SKU174: + - WAREHOUSE0 + SKU175: + - WAREHOUSE0 + SKU176: + - WAREHOUSE0 + SKU177: + - WAREHOUSE0 + SKU178: + - WAREHOUSE0 + SKU179: + - WAREHOUSE0 + SKU18: + - WAREHOUSE0 + SKU180: + - WAREHOUSE0 + SKU181: + - WAREHOUSE0 + SKU182: + - WAREHOUSE0 + SKU183: + - WAREHOUSE0 + SKU184: + - WAREHOUSE0 + SKU185: + - WAREHOUSE0 + SKU186: + - WAREHOUSE0 + SKU187: + - WAREHOUSE0 + SKU188: + - WAREHOUSE0 + SKU189: + - WAREHOUSE0 + SKU19: + - WAREHOUSE0 + SKU190: + - WAREHOUSE0 + SKU191: + - WAREHOUSE0 + SKU192: + - WAREHOUSE0 + SKU193: + - WAREHOUSE0 + SKU194: + - WAREHOUSE0 + SKU195: + - WAREHOUSE0 + SKU196: + - WAREHOUSE0 + SKU197: + - WAREHOUSE0 + SKU198: + - WAREHOUSE0 + SKU199: + - WAREHOUSE0 + SKU2: + - WAREHOUSE0 + SKU20: + - WAREHOUSE0 + SKU200: + - WAREHOUSE0 + SKU201: + - WAREHOUSE0 + SKU202: + - WAREHOUSE0 + SKU203: + - WAREHOUSE0 + SKU204: + - WAREHOUSE0 + SKU205: + - WAREHOUSE0 + SKU206: + - WAREHOUSE0 + SKU207: + - WAREHOUSE0 + SKU208: + - WAREHOUSE0 + SKU209: + - WAREHOUSE0 + SKU21: + - WAREHOUSE0 + SKU210: + - WAREHOUSE0 + SKU211: + - WAREHOUSE0 + SKU212: + - WAREHOUSE0 + SKU213: + - WAREHOUSE0 + SKU214: + - WAREHOUSE0 + SKU215: + - WAREHOUSE0 + SKU216: + - WAREHOUSE0 + SKU217: + - WAREHOUSE0 + SKU218: + - WAREHOUSE0 + SKU219: + - WAREHOUSE0 + SKU22: + - WAREHOUSE0 + SKU220: + - WAREHOUSE0 + SKU221: + - WAREHOUSE0 + SKU222: + - WAREHOUSE0 + SKU223: + - WAREHOUSE0 + SKU224: + - WAREHOUSE0 + SKU225: + - WAREHOUSE0 + SKU226: + - WAREHOUSE0 + SKU227: + - WAREHOUSE0 + SKU228: + - WAREHOUSE0 + SKU229: + - WAREHOUSE0 + SKU23: + - WAREHOUSE0 + SKU230: + - WAREHOUSE0 + SKU231: + - WAREHOUSE0 + SKU232: + - WAREHOUSE0 + SKU233: + - WAREHOUSE0 + SKU234: + - WAREHOUSE0 + SKU235: + - WAREHOUSE0 + SKU236: + - WAREHOUSE0 + SKU237: + - WAREHOUSE0 + SKU238: + - WAREHOUSE0 + SKU239: + - WAREHOUSE0 + SKU24: + - WAREHOUSE0 + SKU240: + - WAREHOUSE0 + SKU241: + - WAREHOUSE0 + SKU242: + - WAREHOUSE0 + SKU243: + - WAREHOUSE0 + SKU244: + - WAREHOUSE0 + SKU245: + - WAREHOUSE0 + SKU246: + - WAREHOUSE0 + SKU247: + - WAREHOUSE0 + SKU248: + - WAREHOUSE0 + SKU249: + - WAREHOUSE0 + SKU25: + - WAREHOUSE0 + SKU250: + - WAREHOUSE0 + SKU251: + - WAREHOUSE0 + SKU252: + - WAREHOUSE0 + SKU253: + - WAREHOUSE0 + SKU254: + - WAREHOUSE0 + SKU255: + - WAREHOUSE0 + SKU256: + - WAREHOUSE0 + SKU257: + - WAREHOUSE0 + SKU258: + - WAREHOUSE0 + SKU259: + - WAREHOUSE0 + SKU26: + - WAREHOUSE0 + SKU260: + - WAREHOUSE0 + SKU261: + - WAREHOUSE0 + SKU262: + - WAREHOUSE0 + SKU263: + - WAREHOUSE0 + SKU264: + - WAREHOUSE0 + SKU265: + - WAREHOUSE0 + SKU266: + - WAREHOUSE0 + SKU267: + - WAREHOUSE0 + SKU268: + - WAREHOUSE0 + SKU269: + - WAREHOUSE0 + SKU27: + - WAREHOUSE0 + SKU270: + - WAREHOUSE0 + SKU271: + - WAREHOUSE0 + SKU272: + - WAREHOUSE0 + SKU273: + - WAREHOUSE0 + SKU274: + - WAREHOUSE0 + SKU275: + - WAREHOUSE0 + SKU276: + - WAREHOUSE0 + SKU277: + - WAREHOUSE0 + SKU278: + - WAREHOUSE0 + SKU279: + - WAREHOUSE0 + SKU28: + - WAREHOUSE0 + SKU280: + - WAREHOUSE0 + SKU281: + - WAREHOUSE0 + SKU282: + - WAREHOUSE0 + SKU283: + - WAREHOUSE0 + SKU284: + - WAREHOUSE0 + SKU285: + - WAREHOUSE0 + SKU286: + - WAREHOUSE0 + SKU287: + - WAREHOUSE0 + SKU288: + - WAREHOUSE0 + SKU289: + - WAREHOUSE0 + SKU29: + - WAREHOUSE0 + SKU290: + - WAREHOUSE0 + SKU291: + - WAREHOUSE0 + SKU292: + - WAREHOUSE0 + SKU293: + - WAREHOUSE0 + SKU294: + - WAREHOUSE0 + SKU295: + - WAREHOUSE0 + SKU296: + - WAREHOUSE0 + SKU297: + - WAREHOUSE0 + SKU298: + - WAREHOUSE0 + SKU299: + - WAREHOUSE0 + SKU3: + - WAREHOUSE0 + SKU30: + - WAREHOUSE0 + SKU300: + - WAREHOUSE0 + SKU301: + - WAREHOUSE0 + SKU302: + - WAREHOUSE0 + SKU303: + - WAREHOUSE0 + SKU304: + - WAREHOUSE0 + SKU305: + - WAREHOUSE0 + SKU306: + - WAREHOUSE0 + SKU307: + - WAREHOUSE0 + SKU308: + - WAREHOUSE0 + SKU309: + - WAREHOUSE0 + SKU31: + - WAREHOUSE0 + SKU310: + - WAREHOUSE0 + SKU311: + - WAREHOUSE0 + SKU312: + - WAREHOUSE0 + SKU313: + - WAREHOUSE0 + SKU314: + - WAREHOUSE0 + SKU315: + - WAREHOUSE0 + SKU316: + - WAREHOUSE0 + SKU317: + - WAREHOUSE0 + SKU318: + - WAREHOUSE0 + SKU319: + - WAREHOUSE0 + SKU32: + - WAREHOUSE0 + SKU320: + - WAREHOUSE0 + SKU321: + - WAREHOUSE0 + SKU322: + - WAREHOUSE0 + SKU323: + - WAREHOUSE0 + SKU324: + - WAREHOUSE0 + SKU325: + - WAREHOUSE0 + SKU326: + - WAREHOUSE0 + SKU327: + - WAREHOUSE0 + SKU328: + - WAREHOUSE0 + SKU329: + - WAREHOUSE0 + SKU33: + - WAREHOUSE0 + SKU330: + - WAREHOUSE0 + SKU331: + - WAREHOUSE0 + SKU332: + - WAREHOUSE0 + SKU333: + - WAREHOUSE0 + SKU334: + - WAREHOUSE0 + SKU335: + - WAREHOUSE0 + SKU336: + - WAREHOUSE0 + SKU337: + - WAREHOUSE0 + SKU338: + - WAREHOUSE0 + SKU339: + - WAREHOUSE0 + SKU34: + - WAREHOUSE0 + SKU340: + - WAREHOUSE0 + SKU341: + - WAREHOUSE0 + SKU342: + - WAREHOUSE0 + SKU343: + - WAREHOUSE0 + SKU344: + - WAREHOUSE0 + SKU345: + - WAREHOUSE0 + SKU346: + - WAREHOUSE0 + SKU347: + - WAREHOUSE0 + SKU348: + - WAREHOUSE0 + SKU349: + - WAREHOUSE0 + SKU35: + - WAREHOUSE0 + SKU350: + - WAREHOUSE0 + SKU351: + - WAREHOUSE0 + SKU352: + - WAREHOUSE0 + SKU353: + - WAREHOUSE0 + SKU354: + - WAREHOUSE0 + SKU355: + - WAREHOUSE0 + SKU356: + - WAREHOUSE0 + SKU357: + - WAREHOUSE0 + SKU358: + - WAREHOUSE0 + SKU359: + - WAREHOUSE0 + SKU36: + - WAREHOUSE0 + SKU360: + - WAREHOUSE0 + SKU361: + - WAREHOUSE0 + SKU362: + - WAREHOUSE0 + SKU363: + - WAREHOUSE0 + SKU364: + - WAREHOUSE0 + SKU365: + - WAREHOUSE0 + SKU366: + - WAREHOUSE0 + SKU367: + - WAREHOUSE0 + SKU368: + - WAREHOUSE0 + SKU369: + - WAREHOUSE0 + SKU37: + - WAREHOUSE0 + SKU370: + - WAREHOUSE0 + SKU371: + - WAREHOUSE0 + SKU372: + - WAREHOUSE0 + SKU373: + - WAREHOUSE0 + SKU374: + - WAREHOUSE0 + SKU375: + - WAREHOUSE0 + SKU376: + - WAREHOUSE0 + SKU377: + - WAREHOUSE0 + SKU378: + - WAREHOUSE0 + SKU379: + - WAREHOUSE0 + SKU38: + - WAREHOUSE0 + SKU380: + - WAREHOUSE0 + SKU381: + - WAREHOUSE0 + SKU382: + - WAREHOUSE0 + SKU383: + - WAREHOUSE0 + SKU384: + - WAREHOUSE0 + SKU385: + - WAREHOUSE0 + SKU386: + - WAREHOUSE0 + SKU387: + - WAREHOUSE0 + SKU388: + - WAREHOUSE0 + SKU389: + - WAREHOUSE0 + SKU39: + - WAREHOUSE0 + SKU390: + - WAREHOUSE0 + SKU391: + - WAREHOUSE0 + SKU392: + - WAREHOUSE0 + SKU393: + - WAREHOUSE0 + SKU394: + - WAREHOUSE0 + SKU395: + - WAREHOUSE0 + SKU396: + - WAREHOUSE0 + SKU397: + - WAREHOUSE0 + SKU398: + - WAREHOUSE0 + SKU399: + - WAREHOUSE0 + SKU4: + - WAREHOUSE0 + SKU40: + - WAREHOUSE0 + SKU400: + - WAREHOUSE0 + SKU401: + - WAREHOUSE0 + SKU402: + - WAREHOUSE0 + SKU403: + - WAREHOUSE0 + SKU404: + - WAREHOUSE0 + SKU405: + - WAREHOUSE0 + SKU406: + - WAREHOUSE0 + SKU407: + - WAREHOUSE0 + SKU408: + - WAREHOUSE0 + SKU409: + - WAREHOUSE0 + SKU41: + - WAREHOUSE0 + SKU410: + - WAREHOUSE0 + SKU411: + - WAREHOUSE0 + SKU412: + - WAREHOUSE0 + SKU413: + - WAREHOUSE0 + SKU414: + - WAREHOUSE0 + SKU415: + - WAREHOUSE0 + SKU416: + - WAREHOUSE0 + SKU417: + - WAREHOUSE0 + SKU418: + - WAREHOUSE0 + SKU419: + - WAREHOUSE0 + SKU42: + - WAREHOUSE0 + SKU420: + - WAREHOUSE0 + SKU421: + - WAREHOUSE0 + SKU422: + - WAREHOUSE0 + SKU423: + - WAREHOUSE0 + SKU424: + - WAREHOUSE0 + SKU425: + - WAREHOUSE0 + SKU426: + - WAREHOUSE0 + SKU427: + - WAREHOUSE0 + SKU428: + - WAREHOUSE0 + SKU429: + - WAREHOUSE0 + SKU43: + - WAREHOUSE0 + SKU430: + - WAREHOUSE0 + SKU431: + - WAREHOUSE0 + SKU432: + - WAREHOUSE0 + SKU433: + - WAREHOUSE0 + SKU434: + - WAREHOUSE0 + SKU435: + - WAREHOUSE0 + SKU436: + - WAREHOUSE0 + SKU437: + - WAREHOUSE0 + SKU438: + - WAREHOUSE0 + SKU439: + - WAREHOUSE0 + SKU44: + - WAREHOUSE0 + SKU440: + - WAREHOUSE0 + SKU441: + - WAREHOUSE0 + SKU442: + - WAREHOUSE0 + SKU443: + - WAREHOUSE0 + SKU444: + - WAREHOUSE0 + SKU445: + - WAREHOUSE0 + SKU446: + - WAREHOUSE0 + SKU447: + - WAREHOUSE0 + SKU448: + - WAREHOUSE0 + SKU449: + - WAREHOUSE0 + SKU45: + - WAREHOUSE0 + SKU450: + - WAREHOUSE0 + SKU451: + - WAREHOUSE0 + SKU452: + - WAREHOUSE0 + SKU453: + - WAREHOUSE0 + SKU454: + - WAREHOUSE0 + SKU455: + - WAREHOUSE0 + SKU456: + - WAREHOUSE0 + SKU457: + - WAREHOUSE0 + SKU458: + - WAREHOUSE0 + SKU459: + - WAREHOUSE0 + SKU46: + - WAREHOUSE0 + SKU460: + - WAREHOUSE0 + SKU461: + - WAREHOUSE0 + SKU462: + - WAREHOUSE0 + SKU463: + - WAREHOUSE0 + SKU464: + - WAREHOUSE0 + SKU465: + - WAREHOUSE0 + SKU466: + - WAREHOUSE0 + SKU467: + - WAREHOUSE0 + SKU468: + - WAREHOUSE0 + SKU469: + - WAREHOUSE0 + SKU47: + - WAREHOUSE0 + SKU470: + - WAREHOUSE0 + SKU471: + - WAREHOUSE0 + SKU472: + - WAREHOUSE0 + SKU473: + - WAREHOUSE0 + SKU474: + - WAREHOUSE0 + SKU475: + - WAREHOUSE0 + SKU476: + - WAREHOUSE0 + SKU477: + - WAREHOUSE0 + SKU478: + - WAREHOUSE0 + SKU479: + - WAREHOUSE0 + SKU48: + - WAREHOUSE0 + SKU480: + - WAREHOUSE0 + SKU481: + - WAREHOUSE0 + SKU482: + - WAREHOUSE0 + SKU483: + - WAREHOUSE0 + SKU484: + - WAREHOUSE0 + SKU485: + - WAREHOUSE0 + SKU486: + - WAREHOUSE0 + SKU487: + - WAREHOUSE0 + SKU488: + - WAREHOUSE0 + SKU489: + - WAREHOUSE0 + SKU49: + - WAREHOUSE0 + SKU490: + - WAREHOUSE0 + SKU491: + - WAREHOUSE0 + SKU492: + - WAREHOUSE0 + SKU493: + - WAREHOUSE0 + SKU494: + - WAREHOUSE0 + SKU495: + - WAREHOUSE0 + SKU496: + - WAREHOUSE0 + SKU497: + - WAREHOUSE0 + SKU498: + - WAREHOUSE0 + SKU499: + - WAREHOUSE0 + SKU5: + - WAREHOUSE0 + SKU50: + - WAREHOUSE0 + SKU500: + - WAREHOUSE0 + SKU501: + - WAREHOUSE0 + SKU502: + - WAREHOUSE0 + SKU503: + - WAREHOUSE0 + SKU504: + - WAREHOUSE0 + SKU505: + - WAREHOUSE0 + SKU506: + - WAREHOUSE0 + SKU507: + - WAREHOUSE0 + SKU508: + - WAREHOUSE0 + SKU509: + - WAREHOUSE0 + SKU51: + - WAREHOUSE0 + SKU510: + - WAREHOUSE0 + SKU511: + - WAREHOUSE0 + SKU512: + - WAREHOUSE0 + SKU513: + - WAREHOUSE0 + SKU514: + - WAREHOUSE0 + SKU515: + - WAREHOUSE0 + SKU516: + - WAREHOUSE0 + SKU517: + - WAREHOUSE0 + SKU518: + - WAREHOUSE0 + SKU519: + - WAREHOUSE0 + SKU52: + - WAREHOUSE0 + SKU520: + - WAREHOUSE0 + SKU521: + - WAREHOUSE0 + SKU522: + - WAREHOUSE0 + SKU523: + - WAREHOUSE0 + SKU524: + - WAREHOUSE0 + SKU525: + - WAREHOUSE0 + SKU526: + - WAREHOUSE0 + SKU527: + - WAREHOUSE0 + SKU528: + - WAREHOUSE0 + SKU529: + - WAREHOUSE0 + SKU53: + - WAREHOUSE0 + SKU530: + - WAREHOUSE0 + SKU531: + - WAREHOUSE0 + SKU532: + - WAREHOUSE0 + SKU533: + - WAREHOUSE0 + SKU534: + - WAREHOUSE0 + SKU535: + - WAREHOUSE0 + SKU536: + - WAREHOUSE0 + SKU537: + - WAREHOUSE0 + SKU538: + - WAREHOUSE0 + SKU539: + - WAREHOUSE0 + SKU54: + - WAREHOUSE0 + SKU540: + - WAREHOUSE0 + SKU541: + - WAREHOUSE0 + SKU542: + - WAREHOUSE0 + SKU543: + - WAREHOUSE0 + SKU544: + - WAREHOUSE0 + SKU545: + - WAREHOUSE0 + SKU546: + - WAREHOUSE0 + SKU547: + - WAREHOUSE0 + SKU548: + - WAREHOUSE0 + SKU549: + - WAREHOUSE0 + SKU55: + - WAREHOUSE0 + SKU550: + - WAREHOUSE0 + SKU551: + - WAREHOUSE0 + SKU552: + - WAREHOUSE0 + SKU553: + - WAREHOUSE0 + SKU554: + - WAREHOUSE0 + SKU555: + - WAREHOUSE0 + SKU556: + - WAREHOUSE0 + SKU557: + - WAREHOUSE0 + SKU558: + - WAREHOUSE0 + SKU559: + - WAREHOUSE0 + SKU56: + - WAREHOUSE0 + SKU560: + - WAREHOUSE0 + SKU561: + - WAREHOUSE0 + SKU562: + - WAREHOUSE0 + SKU563: + - WAREHOUSE0 + SKU564: + - WAREHOUSE0 + SKU565: + - WAREHOUSE0 + SKU566: + - WAREHOUSE0 + SKU567: + - WAREHOUSE0 + SKU568: + - WAREHOUSE0 + SKU569: + - WAREHOUSE0 + SKU57: + - WAREHOUSE0 + SKU570: + - WAREHOUSE0 + SKU571: + - WAREHOUSE0 + SKU572: + - WAREHOUSE0 + SKU573: + - WAREHOUSE0 + SKU574: + - WAREHOUSE0 + SKU575: + - WAREHOUSE0 + SKU576: + - WAREHOUSE0 + SKU577: + - WAREHOUSE0 + SKU578: + - WAREHOUSE0 + SKU579: + - WAREHOUSE0 + SKU58: + - WAREHOUSE0 + SKU580: + - WAREHOUSE0 + SKU581: + - WAREHOUSE0 + SKU582: + - WAREHOUSE0 + SKU583: + - WAREHOUSE0 + SKU584: + - WAREHOUSE0 + SKU585: + - WAREHOUSE0 + SKU586: + - WAREHOUSE0 + SKU587: + - WAREHOUSE0 + SKU588: + - WAREHOUSE0 + SKU589: + - WAREHOUSE0 + SKU59: + - WAREHOUSE0 + SKU590: + - WAREHOUSE0 + SKU591: + - WAREHOUSE0 + SKU592: + - WAREHOUSE0 + SKU593: + - WAREHOUSE0 + SKU594: + - WAREHOUSE0 + SKU595: + - WAREHOUSE0 + SKU596: + - WAREHOUSE0 + SKU597: + - WAREHOUSE0 + SKU598: + - WAREHOUSE0 + SKU599: + - WAREHOUSE0 + SKU6: + - WAREHOUSE0 + SKU60: + - WAREHOUSE0 + SKU600: + - WAREHOUSE0 + SKU601: + - WAREHOUSE0 + SKU602: + - WAREHOUSE0 + SKU603: + - WAREHOUSE0 + SKU604: + - WAREHOUSE0 + SKU605: + - WAREHOUSE0 + SKU606: + - WAREHOUSE0 + SKU607: + - WAREHOUSE0 + SKU608: + - WAREHOUSE0 + SKU609: + - WAREHOUSE0 + SKU61: + - WAREHOUSE0 + SKU610: + - WAREHOUSE0 + SKU611: + - WAREHOUSE0 + SKU612: + - WAREHOUSE0 + SKU613: + - WAREHOUSE0 + SKU614: + - WAREHOUSE0 + SKU615: + - WAREHOUSE0 + SKU616: + - WAREHOUSE0 + SKU617: + - WAREHOUSE0 + SKU618: + - WAREHOUSE0 + SKU619: + - WAREHOUSE0 + SKU62: + - WAREHOUSE0 + SKU620: + - WAREHOUSE0 + SKU621: + - WAREHOUSE0 + SKU622: + - WAREHOUSE0 + SKU623: + - WAREHOUSE0 + SKU624: + - WAREHOUSE0 + SKU625: + - WAREHOUSE0 + SKU626: + - WAREHOUSE0 + SKU627: + - WAREHOUSE0 + SKU628: + - WAREHOUSE0 + SKU629: + - WAREHOUSE0 + SKU63: + - WAREHOUSE0 + SKU630: + - WAREHOUSE0 + SKU631: + - WAREHOUSE0 + SKU632: + - WAREHOUSE0 + SKU633: + - WAREHOUSE0 + SKU634: + - WAREHOUSE0 + SKU635: + - WAREHOUSE0 + SKU636: + - WAREHOUSE0 + SKU637: + - WAREHOUSE0 + SKU638: + - WAREHOUSE0 + SKU639: + - WAREHOUSE0 + SKU64: + - WAREHOUSE0 + SKU640: + - WAREHOUSE0 + SKU641: + - WAREHOUSE0 + SKU642: + - WAREHOUSE0 + SKU643: + - WAREHOUSE0 + SKU644: + - WAREHOUSE0 + SKU645: + - WAREHOUSE0 + SKU646: + - WAREHOUSE0 + SKU647: + - WAREHOUSE0 + SKU648: + - WAREHOUSE0 + SKU649: + - WAREHOUSE0 + SKU65: + - WAREHOUSE0 + SKU650: + - WAREHOUSE0 + SKU651: + - WAREHOUSE0 + SKU652: + - WAREHOUSE0 + SKU653: + - WAREHOUSE0 + SKU654: + - WAREHOUSE0 + SKU655: + - WAREHOUSE0 + SKU656: + - WAREHOUSE0 + SKU657: + - WAREHOUSE0 + SKU658: + - WAREHOUSE0 + SKU659: + - WAREHOUSE0 + SKU66: + - WAREHOUSE0 + SKU660: + - WAREHOUSE0 + SKU661: + - WAREHOUSE0 + SKU662: + - WAREHOUSE0 + SKU663: + - WAREHOUSE0 + SKU664: + - WAREHOUSE0 + SKU665: + - WAREHOUSE0 + SKU666: + - WAREHOUSE0 + SKU667: + - WAREHOUSE0 + SKU668: + - WAREHOUSE0 + SKU669: + - WAREHOUSE0 + SKU67: + - WAREHOUSE0 + SKU670: + - WAREHOUSE0 + SKU671: + - WAREHOUSE0 + SKU672: + - WAREHOUSE0 + SKU673: + - WAREHOUSE0 + SKU674: + - WAREHOUSE0 + SKU675: + - WAREHOUSE0 + SKU676: + - WAREHOUSE0 + SKU677: + - WAREHOUSE0 + SKU678: + - WAREHOUSE0 + SKU679: + - WAREHOUSE0 + SKU68: + - WAREHOUSE0 + SKU680: + - WAREHOUSE0 + SKU681: + - WAREHOUSE0 + SKU682: + - WAREHOUSE0 + SKU683: + - WAREHOUSE0 + SKU684: + - WAREHOUSE0 + SKU685: + - WAREHOUSE0 + SKU686: + - WAREHOUSE0 + SKU687: + - WAREHOUSE0 + SKU688: + - WAREHOUSE0 + SKU689: + - WAREHOUSE0 + SKU69: + - WAREHOUSE0 + SKU690: + - WAREHOUSE0 + SKU691: + - WAREHOUSE0 + SKU692: + - WAREHOUSE0 + SKU693: + - WAREHOUSE0 + SKU694: + - WAREHOUSE0 + SKU695: + - WAREHOUSE0 + SKU696: + - WAREHOUSE0 + SKU697: + - WAREHOUSE0 + SKU698: + - WAREHOUSE0 + SKU699: + - WAREHOUSE0 + SKU7: + - WAREHOUSE0 + SKU70: + - WAREHOUSE0 + SKU700: + - WAREHOUSE0 + SKU701: + - WAREHOUSE0 + SKU702: + - WAREHOUSE0 + SKU703: + - WAREHOUSE0 + SKU704: + - WAREHOUSE0 + SKU705: + - WAREHOUSE0 + SKU706: + - WAREHOUSE0 + SKU707: + - WAREHOUSE0 + SKU708: + - WAREHOUSE0 + SKU709: + - WAREHOUSE0 + SKU71: + - WAREHOUSE0 + SKU710: + - WAREHOUSE0 + SKU711: + - WAREHOUSE0 + SKU712: + - WAREHOUSE0 + SKU713: + - WAREHOUSE0 + SKU714: + - WAREHOUSE0 + SKU715: + - WAREHOUSE0 + SKU716: + - WAREHOUSE0 + SKU717: + - WAREHOUSE0 + SKU718: + - WAREHOUSE0 + SKU719: + - WAREHOUSE0 + SKU72: + - WAREHOUSE0 + SKU720: + - WAREHOUSE0 + SKU721: + - WAREHOUSE0 + SKU722: + - WAREHOUSE0 + SKU723: + - WAREHOUSE0 + SKU724: + - WAREHOUSE0 + SKU725: + - WAREHOUSE0 + SKU726: + - WAREHOUSE0 + SKU727: + - WAREHOUSE0 + SKU728: + - WAREHOUSE0 + SKU729: + - WAREHOUSE0 + SKU73: + - WAREHOUSE0 + SKU730: + - WAREHOUSE0 + SKU731: + - WAREHOUSE0 + SKU732: + - WAREHOUSE0 + SKU733: + - WAREHOUSE0 + SKU734: + - WAREHOUSE0 + SKU735: + - WAREHOUSE0 + SKU736: + - WAREHOUSE0 + SKU737: + - WAREHOUSE0 + SKU738: + - WAREHOUSE0 + SKU739: + - WAREHOUSE0 + SKU74: + - WAREHOUSE0 + SKU740: + - WAREHOUSE0 + SKU741: + - WAREHOUSE0 + SKU742: + - WAREHOUSE0 + SKU743: + - WAREHOUSE0 + SKU744: + - WAREHOUSE0 + SKU745: + - WAREHOUSE0 + SKU746: + - WAREHOUSE0 + SKU747: + - WAREHOUSE0 + SKU748: + - WAREHOUSE0 + SKU749: + - WAREHOUSE0 + SKU75: + - WAREHOUSE0 + SKU750: + - WAREHOUSE0 + SKU751: + - WAREHOUSE0 + SKU752: + - WAREHOUSE0 + SKU753: + - WAREHOUSE0 + SKU754: + - WAREHOUSE0 + SKU755: + - WAREHOUSE0 + SKU756: + - WAREHOUSE0 + SKU757: + - WAREHOUSE0 + SKU758: + - WAREHOUSE0 + SKU759: + - WAREHOUSE0 + SKU76: + - WAREHOUSE0 + SKU760: + - WAREHOUSE0 + SKU761: + - WAREHOUSE0 + SKU762: + - WAREHOUSE0 + SKU763: + - WAREHOUSE0 + SKU764: + - WAREHOUSE0 + SKU765: + - WAREHOUSE0 + SKU766: + - WAREHOUSE0 + SKU767: + - WAREHOUSE0 + SKU768: + - WAREHOUSE0 + SKU769: + - WAREHOUSE0 + SKU77: + - WAREHOUSE0 + SKU770: + - WAREHOUSE0 + SKU771: + - WAREHOUSE0 + SKU772: + - WAREHOUSE0 + SKU773: + - WAREHOUSE0 + SKU774: + - WAREHOUSE0 + SKU775: + - WAREHOUSE0 + SKU776: + - WAREHOUSE0 + SKU777: + - WAREHOUSE0 + SKU778: + - WAREHOUSE0 + SKU779: + - WAREHOUSE0 + SKU78: + - WAREHOUSE0 + SKU780: + - WAREHOUSE0 + SKU781: + - WAREHOUSE0 + SKU782: + - WAREHOUSE0 + SKU783: + - WAREHOUSE0 + SKU784: + - WAREHOUSE0 + SKU785: + - WAREHOUSE0 + SKU786: + - WAREHOUSE0 + SKU787: + - WAREHOUSE0 + SKU788: + - WAREHOUSE0 + SKU789: + - WAREHOUSE0 + SKU79: + - WAREHOUSE0 + SKU790: + - WAREHOUSE0 + SKU791: + - WAREHOUSE0 + SKU792: + - WAREHOUSE0 + SKU793: + - WAREHOUSE0 + SKU794: + - WAREHOUSE0 + SKU795: + - WAREHOUSE0 + SKU796: + - WAREHOUSE0 + SKU797: + - WAREHOUSE0 + SKU798: + - WAREHOUSE0 + SKU799: + - WAREHOUSE0 + SKU8: + - WAREHOUSE0 + SKU80: + - WAREHOUSE0 + SKU800: + - WAREHOUSE0 + SKU801: + - WAREHOUSE0 + SKU802: + - WAREHOUSE0 + SKU803: + - WAREHOUSE0 + SKU804: + - WAREHOUSE0 + SKU805: + - WAREHOUSE0 + SKU806: + - WAREHOUSE0 + SKU807: + - WAREHOUSE0 + SKU808: + - WAREHOUSE0 + SKU809: + - WAREHOUSE0 + SKU81: + - WAREHOUSE0 + SKU810: + - WAREHOUSE0 + SKU811: + - WAREHOUSE0 + SKU812: + - WAREHOUSE0 + SKU813: + - WAREHOUSE0 + SKU814: + - WAREHOUSE0 + SKU815: + - WAREHOUSE0 + SKU816: + - WAREHOUSE0 + SKU817: + - WAREHOUSE0 + SKU818: + - WAREHOUSE0 + SKU819: + - WAREHOUSE0 + SKU82: + - WAREHOUSE0 + SKU820: + - WAREHOUSE0 + SKU821: + - WAREHOUSE0 + SKU822: + - WAREHOUSE0 + SKU823: + - WAREHOUSE0 + SKU824: + - WAREHOUSE0 + SKU825: + - WAREHOUSE0 + SKU826: + - WAREHOUSE0 + SKU827: + - WAREHOUSE0 + SKU828: + - WAREHOUSE0 + SKU829: + - WAREHOUSE0 + SKU83: + - WAREHOUSE0 + SKU830: + - WAREHOUSE0 + SKU831: + - WAREHOUSE0 + SKU832: + - WAREHOUSE0 + SKU833: + - WAREHOUSE0 + SKU834: + - WAREHOUSE0 + SKU835: + - WAREHOUSE0 + SKU836: + - WAREHOUSE0 + SKU837: + - WAREHOUSE0 + SKU838: + - WAREHOUSE0 + SKU839: + - WAREHOUSE0 + SKU84: + - WAREHOUSE0 + SKU840: + - WAREHOUSE0 + SKU841: + - WAREHOUSE0 + SKU842: + - WAREHOUSE0 + SKU843: + - WAREHOUSE0 + SKU844: + - WAREHOUSE0 + SKU845: + - WAREHOUSE0 + SKU846: + - WAREHOUSE0 + SKU847: + - WAREHOUSE0 + SKU848: + - WAREHOUSE0 + SKU849: + - WAREHOUSE0 + SKU85: + - WAREHOUSE0 + SKU850: + - WAREHOUSE0 + SKU851: + - WAREHOUSE0 + SKU852: + - WAREHOUSE0 + SKU853: + - WAREHOUSE0 + SKU854: + - WAREHOUSE0 + SKU855: + - WAREHOUSE0 + SKU856: + - WAREHOUSE0 + SKU857: + - WAREHOUSE0 + SKU858: + - WAREHOUSE0 + SKU859: + - WAREHOUSE0 + SKU86: + - WAREHOUSE0 + SKU860: + - WAREHOUSE0 + SKU861: + - WAREHOUSE0 + SKU862: + - WAREHOUSE0 + SKU863: + - WAREHOUSE0 + SKU864: + - WAREHOUSE0 + SKU865: + - WAREHOUSE0 + SKU866: + - WAREHOUSE0 + SKU867: + - WAREHOUSE0 + SKU868: + - WAREHOUSE0 + SKU869: + - WAREHOUSE0 + SKU87: + - WAREHOUSE0 + SKU870: + - WAREHOUSE0 + SKU871: + - WAREHOUSE0 + SKU872: + - WAREHOUSE0 + SKU873: + - WAREHOUSE0 + SKU874: + - WAREHOUSE0 + SKU875: + - WAREHOUSE0 + SKU876: + - WAREHOUSE0 + SKU877: + - WAREHOUSE0 + SKU878: + - WAREHOUSE0 + SKU879: + - WAREHOUSE0 + SKU88: + - WAREHOUSE0 + SKU880: + - WAREHOUSE0 + SKU881: + - WAREHOUSE0 + SKU882: + - WAREHOUSE0 + SKU883: + - WAREHOUSE0 + SKU884: + - WAREHOUSE0 + SKU885: + - WAREHOUSE0 + SKU886: + - WAREHOUSE0 + SKU887: + - WAREHOUSE0 + SKU888: + - WAREHOUSE0 + SKU889: + - WAREHOUSE0 + SKU89: + - WAREHOUSE0 + SKU890: + - WAREHOUSE0 + SKU891: + - WAREHOUSE0 + SKU892: + - WAREHOUSE0 + SKU893: + - WAREHOUSE0 + SKU894: + - WAREHOUSE0 + SKU895: + - WAREHOUSE0 + SKU896: + - WAREHOUSE0 + SKU897: + - WAREHOUSE0 + SKU898: + - WAREHOUSE0 + SKU899: + - WAREHOUSE0 + SKU9: + - WAREHOUSE0 + SKU90: + - WAREHOUSE0 + SKU900: + - WAREHOUSE0 + SKU901: + - WAREHOUSE0 + SKU902: + - WAREHOUSE0 + SKU903: + - WAREHOUSE0 + SKU904: + - WAREHOUSE0 + SKU905: + - WAREHOUSE0 + SKU906: + - WAREHOUSE0 + SKU907: + - WAREHOUSE0 + SKU908: + - WAREHOUSE0 + SKU909: + - WAREHOUSE0 + SKU91: + - WAREHOUSE0 + SKU910: + - WAREHOUSE0 + SKU911: + - WAREHOUSE0 + SKU912: + - WAREHOUSE0 + SKU913: + - WAREHOUSE0 + SKU914: + - WAREHOUSE0 + SKU915: + - WAREHOUSE0 + SKU916: + - WAREHOUSE0 + SKU917: + - WAREHOUSE0 + SKU918: + - WAREHOUSE0 + SKU919: + - WAREHOUSE0 + SKU92: + - WAREHOUSE0 + SKU920: + - WAREHOUSE0 + SKU921: + - WAREHOUSE0 + SKU922: + - WAREHOUSE0 + SKU923: + - WAREHOUSE0 + SKU924: + - WAREHOUSE0 + SKU925: + - WAREHOUSE0 + SKU926: + - WAREHOUSE0 + SKU927: + - WAREHOUSE0 + SKU928: + - WAREHOUSE0 + SKU929: + - WAREHOUSE0 + SKU93: + - WAREHOUSE0 + SKU930: + - WAREHOUSE0 + SKU931: + - WAREHOUSE0 + SKU932: + - WAREHOUSE0 + SKU933: + - WAREHOUSE0 + SKU934: + - WAREHOUSE0 + SKU935: + - WAREHOUSE0 + SKU936: + - WAREHOUSE0 + SKU937: + - WAREHOUSE0 + SKU938: + - WAREHOUSE0 + SKU939: + - WAREHOUSE0 + SKU94: + - WAREHOUSE0 + SKU940: + - WAREHOUSE0 + SKU941: + - WAREHOUSE0 + SKU942: + - WAREHOUSE0 + SKU943: + - WAREHOUSE0 + SKU944: + - WAREHOUSE0 + SKU945: + - WAREHOUSE0 + SKU946: + - WAREHOUSE0 + SKU947: + - WAREHOUSE0 + SKU948: + - WAREHOUSE0 + SKU949: + - WAREHOUSE0 + SKU95: + - WAREHOUSE0 + SKU950: + - WAREHOUSE0 + SKU951: + - WAREHOUSE0 + SKU952: + - WAREHOUSE0 + SKU953: + - WAREHOUSE0 + SKU954: + - WAREHOUSE0 + SKU955: + - WAREHOUSE0 + SKU956: + - WAREHOUSE0 + SKU957: + - WAREHOUSE0 + SKU958: + - WAREHOUSE0 + SKU959: + - WAREHOUSE0 + SKU96: + - WAREHOUSE0 + SKU960: + - WAREHOUSE0 + SKU961: + - WAREHOUSE0 + SKU962: + - WAREHOUSE0 + SKU963: + - WAREHOUSE0 + SKU964: + - WAREHOUSE0 + SKU965: + - WAREHOUSE0 + SKU966: + - WAREHOUSE0 + SKU967: + - WAREHOUSE0 + SKU968: + - WAREHOUSE0 + SKU969: + - WAREHOUSE0 + SKU97: + - WAREHOUSE0 + SKU970: + - WAREHOUSE0 + SKU971: + - WAREHOUSE0 + SKU972: + - WAREHOUSE0 + SKU973: + - WAREHOUSE0 + SKU974: + - WAREHOUSE0 + SKU975: + - WAREHOUSE0 + SKU976: + - WAREHOUSE0 + SKU977: + - WAREHOUSE0 + SKU978: + - WAREHOUSE0 + SKU979: + - WAREHOUSE0 + SKU98: + - WAREHOUSE0 + SKU980: + - WAREHOUSE0 + SKU981: + - WAREHOUSE0 + SKU982: + - WAREHOUSE0 + SKU983: + - WAREHOUSE0 + SKU984: + - WAREHOUSE0 + SKU985: + - WAREHOUSE0 + SKU986: + - WAREHOUSE0 + SKU987: + - WAREHOUSE0 + SKU988: + - WAREHOUSE0 + SKU989: + - WAREHOUSE0 + SKU99: + - WAREHOUSE0 + SKU990: + - WAREHOUSE0 + SKU991: + - WAREHOUSE0 + SKU992: + - WAREHOUSE0 + SKU993: + - WAREHOUSE0 + SKU994: + - WAREHOUSE0 + SKU995: + - WAREHOUSE0 + SKU996: + - WAREHOUSE0 + SKU997: + - WAREHOUSE0 + SKU998: + - WAREHOUSE0 + SKU999: + - WAREHOUSE0 + WAREHOUSE0: + SKU0: + - SUPPLIER0 + SKU1: + - SUPPLIER0 + SKU10: + - SUPPLIER0 + SKU100: + - SUPPLIER0 + SKU101: + - SUPPLIER0 + SKU102: + - SUPPLIER0 + SKU103: + - SUPPLIER0 + SKU104: + - SUPPLIER0 + SKU105: + - SUPPLIER0 + SKU106: + - SUPPLIER0 + SKU107: + - SUPPLIER0 + SKU108: + - SUPPLIER0 + SKU109: + - SUPPLIER0 + SKU11: + - SUPPLIER0 + SKU110: + - SUPPLIER0 + SKU111: + - SUPPLIER0 + SKU112: + - SUPPLIER0 + SKU113: + - SUPPLIER0 + SKU114: + - SUPPLIER0 + SKU115: + - SUPPLIER0 + SKU116: + - SUPPLIER0 + SKU117: + - SUPPLIER0 + SKU118: + - SUPPLIER0 + SKU119: + - SUPPLIER0 + SKU12: + - SUPPLIER0 + SKU120: + - SUPPLIER0 + SKU121: + - SUPPLIER0 + SKU122: + - SUPPLIER0 + SKU123: + - SUPPLIER0 + SKU124: + - SUPPLIER0 + SKU125: + - SUPPLIER0 + SKU126: + - SUPPLIER0 + SKU127: + - SUPPLIER0 + SKU128: + - SUPPLIER0 + SKU129: + - SUPPLIER0 + SKU13: + - SUPPLIER0 + SKU130: + - SUPPLIER0 + SKU131: + - SUPPLIER0 + SKU132: + - SUPPLIER0 + SKU133: + - SUPPLIER0 + SKU134: + - SUPPLIER0 + SKU135: + - SUPPLIER0 + SKU136: + - SUPPLIER0 + SKU137: + - SUPPLIER0 + SKU138: + - SUPPLIER0 + SKU139: + - SUPPLIER0 + SKU14: + - SUPPLIER0 + SKU140: + - SUPPLIER0 + SKU141: + - SUPPLIER0 + SKU142: + - SUPPLIER0 + SKU143: + - SUPPLIER0 + SKU144: + - SUPPLIER0 + SKU145: + - SUPPLIER0 + SKU146: + - SUPPLIER0 + SKU147: + - SUPPLIER0 + SKU148: + - SUPPLIER0 + SKU149: + - SUPPLIER0 + SKU15: + - SUPPLIER0 + SKU150: + - SUPPLIER0 + SKU151: + - SUPPLIER0 + SKU152: + - SUPPLIER0 + SKU153: + - SUPPLIER0 + SKU154: + - SUPPLIER0 + SKU155: + - SUPPLIER0 + SKU156: + - SUPPLIER0 + SKU157: + - SUPPLIER0 + SKU158: + - SUPPLIER0 + SKU159: + - SUPPLIER0 + SKU16: + - SUPPLIER0 + SKU160: + - SUPPLIER0 + SKU161: + - SUPPLIER0 + SKU162: + - SUPPLIER0 + SKU163: + - SUPPLIER0 + SKU164: + - SUPPLIER0 + SKU165: + - SUPPLIER0 + SKU166: + - SUPPLIER0 + SKU167: + - SUPPLIER0 + SKU168: + - SUPPLIER0 + SKU169: + - SUPPLIER0 + SKU17: + - SUPPLIER0 + SKU170: + - SUPPLIER0 + SKU171: + - SUPPLIER0 + SKU172: + - SUPPLIER0 + SKU173: + - SUPPLIER0 + SKU174: + - SUPPLIER0 + SKU175: + - SUPPLIER0 + SKU176: + - SUPPLIER0 + SKU177: + - SUPPLIER0 + SKU178: + - SUPPLIER0 + SKU179: + - SUPPLIER0 + SKU18: + - SUPPLIER0 + SKU180: + - SUPPLIER0 + SKU181: + - SUPPLIER0 + SKU182: + - SUPPLIER0 + SKU183: + - SUPPLIER0 + SKU184: + - SUPPLIER0 + SKU185: + - SUPPLIER0 + SKU186: + - SUPPLIER0 + SKU187: + - SUPPLIER0 + SKU188: + - SUPPLIER0 + SKU189: + - SUPPLIER0 + SKU19: + - SUPPLIER0 + SKU190: + - SUPPLIER0 + SKU191: + - SUPPLIER0 + SKU192: + - SUPPLIER0 + SKU193: + - SUPPLIER0 + SKU194: + - SUPPLIER0 + SKU195: + - SUPPLIER0 + SKU196: + - SUPPLIER0 + SKU197: + - SUPPLIER0 + SKU198: + - SUPPLIER0 + SKU199: + - SUPPLIER0 + SKU2: + - SUPPLIER0 + SKU20: + - SUPPLIER0 + SKU200: + - SUPPLIER0 + SKU201: + - SUPPLIER0 + SKU202: + - SUPPLIER0 + SKU203: + - SUPPLIER0 + SKU204: + - SUPPLIER0 + SKU205: + - SUPPLIER0 + SKU206: + - SUPPLIER0 + SKU207: + - SUPPLIER0 + SKU208: + - SUPPLIER0 + SKU209: + - SUPPLIER0 + SKU21: + - SUPPLIER0 + SKU210: + - SUPPLIER0 + SKU211: + - SUPPLIER0 + SKU212: + - SUPPLIER0 + SKU213: + - SUPPLIER0 + SKU214: + - SUPPLIER0 + SKU215: + - SUPPLIER0 + SKU216: + - SUPPLIER0 + SKU217: + - SUPPLIER0 + SKU218: + - SUPPLIER0 + SKU219: + - SUPPLIER0 + SKU22: + - SUPPLIER0 + SKU220: + - SUPPLIER0 + SKU221: + - SUPPLIER0 + SKU222: + - SUPPLIER0 + SKU223: + - SUPPLIER0 + SKU224: + - SUPPLIER0 + SKU225: + - SUPPLIER0 + SKU226: + - SUPPLIER0 + SKU227: + - SUPPLIER0 + SKU228: + - SUPPLIER0 + SKU229: + - SUPPLIER0 + SKU23: + - SUPPLIER0 + SKU230: + - SUPPLIER0 + SKU231: + - SUPPLIER0 + SKU232: + - SUPPLIER0 + SKU233: + - SUPPLIER0 + SKU234: + - SUPPLIER0 + SKU235: + - SUPPLIER0 + SKU236: + - SUPPLIER0 + SKU237: + - SUPPLIER0 + SKU238: + - SUPPLIER0 + SKU239: + - SUPPLIER0 + SKU24: + - SUPPLIER0 + SKU240: + - SUPPLIER0 + SKU241: + - SUPPLIER0 + SKU242: + - SUPPLIER0 + SKU243: + - SUPPLIER0 + SKU244: + - SUPPLIER0 + SKU245: + - SUPPLIER0 + SKU246: + - SUPPLIER0 + SKU247: + - SUPPLIER0 + SKU248: + - SUPPLIER0 + SKU249: + - SUPPLIER0 + SKU25: + - SUPPLIER0 + SKU250: + - SUPPLIER0 + SKU251: + - SUPPLIER0 + SKU252: + - SUPPLIER0 + SKU253: + - SUPPLIER0 + SKU254: + - SUPPLIER0 + SKU255: + - SUPPLIER0 + SKU256: + - SUPPLIER0 + SKU257: + - SUPPLIER0 + SKU258: + - SUPPLIER0 + SKU259: + - SUPPLIER0 + SKU26: + - SUPPLIER0 + SKU260: + - SUPPLIER0 + SKU261: + - SUPPLIER0 + SKU262: + - SUPPLIER0 + SKU263: + - SUPPLIER0 + SKU264: + - SUPPLIER0 + SKU265: + - SUPPLIER0 + SKU266: + - SUPPLIER0 + SKU267: + - SUPPLIER0 + SKU268: + - SUPPLIER0 + SKU269: + - SUPPLIER0 + SKU27: + - SUPPLIER0 + SKU270: + - SUPPLIER0 + SKU271: + - SUPPLIER0 + SKU272: + - SUPPLIER0 + SKU273: + - SUPPLIER0 + SKU274: + - SUPPLIER0 + SKU275: + - SUPPLIER0 + SKU276: + - SUPPLIER0 + SKU277: + - SUPPLIER0 + SKU278: + - SUPPLIER0 + SKU279: + - SUPPLIER0 + SKU28: + - SUPPLIER0 + SKU280: + - SUPPLIER0 + SKU281: + - SUPPLIER0 + SKU282: + - SUPPLIER0 + SKU283: + - SUPPLIER0 + SKU284: + - SUPPLIER0 + SKU285: + - SUPPLIER0 + SKU286: + - SUPPLIER0 + SKU287: + - SUPPLIER0 + SKU288: + - SUPPLIER0 + SKU289: + - SUPPLIER0 + SKU29: + - SUPPLIER0 + SKU290: + - SUPPLIER0 + SKU291: + - SUPPLIER0 + SKU292: + - SUPPLIER0 + SKU293: + - SUPPLIER0 + SKU294: + - SUPPLIER0 + SKU295: + - SUPPLIER0 + SKU296: + - SUPPLIER0 + SKU297: + - SUPPLIER0 + SKU298: + - SUPPLIER0 + SKU299: + - SUPPLIER0 + SKU3: + - SUPPLIER0 + SKU30: + - SUPPLIER0 + SKU300: + - SUPPLIER0 + SKU301: + - SUPPLIER0 + SKU302: + - SUPPLIER0 + SKU303: + - SUPPLIER0 + SKU304: + - SUPPLIER0 + SKU305: + - SUPPLIER0 + SKU306: + - SUPPLIER0 + SKU307: + - SUPPLIER0 + SKU308: + - SUPPLIER0 + SKU309: + - SUPPLIER0 + SKU31: + - SUPPLIER0 + SKU310: + - SUPPLIER0 + SKU311: + - SUPPLIER0 + SKU312: + - SUPPLIER0 + SKU313: + - SUPPLIER0 + SKU314: + - SUPPLIER0 + SKU315: + - SUPPLIER0 + SKU316: + - SUPPLIER0 + SKU317: + - SUPPLIER0 + SKU318: + - SUPPLIER0 + SKU319: + - SUPPLIER0 + SKU32: + - SUPPLIER0 + SKU320: + - SUPPLIER0 + SKU321: + - SUPPLIER0 + SKU322: + - SUPPLIER0 + SKU323: + - SUPPLIER0 + SKU324: + - SUPPLIER0 + SKU325: + - SUPPLIER0 + SKU326: + - SUPPLIER0 + SKU327: + - SUPPLIER0 + SKU328: + - SUPPLIER0 + SKU329: + - SUPPLIER0 + SKU33: + - SUPPLIER0 + SKU330: + - SUPPLIER0 + SKU331: + - SUPPLIER0 + SKU332: + - SUPPLIER0 + SKU333: + - SUPPLIER0 + SKU334: + - SUPPLIER0 + SKU335: + - SUPPLIER0 + SKU336: + - SUPPLIER0 + SKU337: + - SUPPLIER0 + SKU338: + - SUPPLIER0 + SKU339: + - SUPPLIER0 + SKU34: + - SUPPLIER0 + SKU340: + - SUPPLIER0 + SKU341: + - SUPPLIER0 + SKU342: + - SUPPLIER0 + SKU343: + - SUPPLIER0 + SKU344: + - SUPPLIER0 + SKU345: + - SUPPLIER0 + SKU346: + - SUPPLIER0 + SKU347: + - SUPPLIER0 + SKU348: + - SUPPLIER0 + SKU349: + - SUPPLIER0 + SKU35: + - SUPPLIER0 + SKU350: + - SUPPLIER0 + SKU351: + - SUPPLIER0 + SKU352: + - SUPPLIER0 + SKU353: + - SUPPLIER0 + SKU354: + - SUPPLIER0 + SKU355: + - SUPPLIER0 + SKU356: + - SUPPLIER0 + SKU357: + - SUPPLIER0 + SKU358: + - SUPPLIER0 + SKU359: + - SUPPLIER0 + SKU36: + - SUPPLIER0 + SKU360: + - SUPPLIER0 + SKU361: + - SUPPLIER0 + SKU362: + - SUPPLIER0 + SKU363: + - SUPPLIER0 + SKU364: + - SUPPLIER0 + SKU365: + - SUPPLIER0 + SKU366: + - SUPPLIER0 + SKU367: + - SUPPLIER0 + SKU368: + - SUPPLIER0 + SKU369: + - SUPPLIER0 + SKU37: + - SUPPLIER0 + SKU370: + - SUPPLIER0 + SKU371: + - SUPPLIER0 + SKU372: + - SUPPLIER0 + SKU373: + - SUPPLIER0 + SKU374: + - SUPPLIER0 + SKU375: + - SUPPLIER0 + SKU376: + - SUPPLIER0 + SKU377: + - SUPPLIER0 + SKU378: + - SUPPLIER0 + SKU379: + - SUPPLIER0 + SKU38: + - SUPPLIER0 + SKU380: + - SUPPLIER0 + SKU381: + - SUPPLIER0 + SKU382: + - SUPPLIER0 + SKU383: + - SUPPLIER0 + SKU384: + - SUPPLIER0 + SKU385: + - SUPPLIER0 + SKU386: + - SUPPLIER0 + SKU387: + - SUPPLIER0 + SKU388: + - SUPPLIER0 + SKU389: + - SUPPLIER0 + SKU39: + - SUPPLIER0 + SKU390: + - SUPPLIER0 + SKU391: + - SUPPLIER0 + SKU392: + - SUPPLIER0 + SKU393: + - SUPPLIER0 + SKU394: + - SUPPLIER0 + SKU395: + - SUPPLIER0 + SKU396: + - SUPPLIER0 + SKU397: + - SUPPLIER0 + SKU398: + - SUPPLIER0 + SKU399: + - SUPPLIER0 + SKU4: + - SUPPLIER0 + SKU40: + - SUPPLIER0 + SKU400: + - SUPPLIER0 + SKU401: + - SUPPLIER0 + SKU402: + - SUPPLIER0 + SKU403: + - SUPPLIER0 + SKU404: + - SUPPLIER0 + SKU405: + - SUPPLIER0 + SKU406: + - SUPPLIER0 + SKU407: + - SUPPLIER0 + SKU408: + - SUPPLIER0 + SKU409: + - SUPPLIER0 + SKU41: + - SUPPLIER0 + SKU410: + - SUPPLIER0 + SKU411: + - SUPPLIER0 + SKU412: + - SUPPLIER0 + SKU413: + - SUPPLIER0 + SKU414: + - SUPPLIER0 + SKU415: + - SUPPLIER0 + SKU416: + - SUPPLIER0 + SKU417: + - SUPPLIER0 + SKU418: + - SUPPLIER0 + SKU419: + - SUPPLIER0 + SKU42: + - SUPPLIER0 + SKU420: + - SUPPLIER0 + SKU421: + - SUPPLIER0 + SKU422: + - SUPPLIER0 + SKU423: + - SUPPLIER0 + SKU424: + - SUPPLIER0 + SKU425: + - SUPPLIER0 + SKU426: + - SUPPLIER0 + SKU427: + - SUPPLIER0 + SKU428: + - SUPPLIER0 + SKU429: + - SUPPLIER0 + SKU43: + - SUPPLIER0 + SKU430: + - SUPPLIER0 + SKU431: + - SUPPLIER0 + SKU432: + - SUPPLIER0 + SKU433: + - SUPPLIER0 + SKU434: + - SUPPLIER0 + SKU435: + - SUPPLIER0 + SKU436: + - SUPPLIER0 + SKU437: + - SUPPLIER0 + SKU438: + - SUPPLIER0 + SKU439: + - SUPPLIER0 + SKU44: + - SUPPLIER0 + SKU440: + - SUPPLIER0 + SKU441: + - SUPPLIER0 + SKU442: + - SUPPLIER0 + SKU443: + - SUPPLIER0 + SKU444: + - SUPPLIER0 + SKU445: + - SUPPLIER0 + SKU446: + - SUPPLIER0 + SKU447: + - SUPPLIER0 + SKU448: + - SUPPLIER0 + SKU449: + - SUPPLIER0 + SKU45: + - SUPPLIER0 + SKU450: + - SUPPLIER0 + SKU451: + - SUPPLIER0 + SKU452: + - SUPPLIER0 + SKU453: + - SUPPLIER0 + SKU454: + - SUPPLIER0 + SKU455: + - SUPPLIER0 + SKU456: + - SUPPLIER0 + SKU457: + - SUPPLIER0 + SKU458: + - SUPPLIER0 + SKU459: + - SUPPLIER0 + SKU46: + - SUPPLIER0 + SKU460: + - SUPPLIER0 + SKU461: + - SUPPLIER0 + SKU462: + - SUPPLIER0 + SKU463: + - SUPPLIER0 + SKU464: + - SUPPLIER0 + SKU465: + - SUPPLIER0 + SKU466: + - SUPPLIER0 + SKU467: + - SUPPLIER0 + SKU468: + - SUPPLIER0 + SKU469: + - SUPPLIER0 + SKU47: + - SUPPLIER0 + SKU470: + - SUPPLIER0 + SKU471: + - SUPPLIER0 + SKU472: + - SUPPLIER0 + SKU473: + - SUPPLIER0 + SKU474: + - SUPPLIER0 + SKU475: + - SUPPLIER0 + SKU476: + - SUPPLIER0 + SKU477: + - SUPPLIER0 + SKU478: + - SUPPLIER0 + SKU479: + - SUPPLIER0 + SKU48: + - SUPPLIER0 + SKU480: + - SUPPLIER0 + SKU481: + - SUPPLIER0 + SKU482: + - SUPPLIER0 + SKU483: + - SUPPLIER0 + SKU484: + - SUPPLIER0 + SKU485: + - SUPPLIER0 + SKU486: + - SUPPLIER0 + SKU487: + - SUPPLIER0 + SKU488: + - SUPPLIER0 + SKU489: + - SUPPLIER0 + SKU49: + - SUPPLIER0 + SKU490: + - SUPPLIER0 + SKU491: + - SUPPLIER0 + SKU492: + - SUPPLIER0 + SKU493: + - SUPPLIER0 + SKU494: + - SUPPLIER0 + SKU495: + - SUPPLIER0 + SKU496: + - SUPPLIER0 + SKU497: + - SUPPLIER0 + SKU498: + - SUPPLIER0 + SKU499: + - SUPPLIER0 + SKU5: + - SUPPLIER0 + SKU50: + - SUPPLIER0 + SKU500: + - SUPPLIER0 + SKU501: + - SUPPLIER0 + SKU502: + - SUPPLIER0 + SKU503: + - SUPPLIER0 + SKU504: + - SUPPLIER0 + SKU505: + - SUPPLIER0 + SKU506: + - SUPPLIER0 + SKU507: + - SUPPLIER0 + SKU508: + - SUPPLIER0 + SKU509: + - SUPPLIER0 + SKU51: + - SUPPLIER0 + SKU510: + - SUPPLIER0 + SKU511: + - SUPPLIER0 + SKU512: + - SUPPLIER0 + SKU513: + - SUPPLIER0 + SKU514: + - SUPPLIER0 + SKU515: + - SUPPLIER0 + SKU516: + - SUPPLIER0 + SKU517: + - SUPPLIER0 + SKU518: + - SUPPLIER0 + SKU519: + - SUPPLIER0 + SKU52: + - SUPPLIER0 + SKU520: + - SUPPLIER0 + SKU521: + - SUPPLIER0 + SKU522: + - SUPPLIER0 + SKU523: + - SUPPLIER0 + SKU524: + - SUPPLIER0 + SKU525: + - SUPPLIER0 + SKU526: + - SUPPLIER0 + SKU527: + - SUPPLIER0 + SKU528: + - SUPPLIER0 + SKU529: + - SUPPLIER0 + SKU53: + - SUPPLIER0 + SKU530: + - SUPPLIER0 + SKU531: + - SUPPLIER0 + SKU532: + - SUPPLIER0 + SKU533: + - SUPPLIER0 + SKU534: + - SUPPLIER0 + SKU535: + - SUPPLIER0 + SKU536: + - SUPPLIER0 + SKU537: + - SUPPLIER0 + SKU538: + - SUPPLIER0 + SKU539: + - SUPPLIER0 + SKU54: + - SUPPLIER0 + SKU540: + - SUPPLIER0 + SKU541: + - SUPPLIER0 + SKU542: + - SUPPLIER0 + SKU543: + - SUPPLIER0 + SKU544: + - SUPPLIER0 + SKU545: + - SUPPLIER0 + SKU546: + - SUPPLIER0 + SKU547: + - SUPPLIER0 + SKU548: + - SUPPLIER0 + SKU549: + - SUPPLIER0 + SKU55: + - SUPPLIER0 + SKU550: + - SUPPLIER0 + SKU551: + - SUPPLIER0 + SKU552: + - SUPPLIER0 + SKU553: + - SUPPLIER0 + SKU554: + - SUPPLIER0 + SKU555: + - SUPPLIER0 + SKU556: + - SUPPLIER0 + SKU557: + - SUPPLIER0 + SKU558: + - SUPPLIER0 + SKU559: + - SUPPLIER0 + SKU56: + - SUPPLIER0 + SKU560: + - SUPPLIER0 + SKU561: + - SUPPLIER0 + SKU562: + - SUPPLIER0 + SKU563: + - SUPPLIER0 + SKU564: + - SUPPLIER0 + SKU565: + - SUPPLIER0 + SKU566: + - SUPPLIER0 + SKU567: + - SUPPLIER0 + SKU568: + - SUPPLIER0 + SKU569: + - SUPPLIER0 + SKU57: + - SUPPLIER0 + SKU570: + - SUPPLIER0 + SKU571: + - SUPPLIER0 + SKU572: + - SUPPLIER0 + SKU573: + - SUPPLIER0 + SKU574: + - SUPPLIER0 + SKU575: + - SUPPLIER0 + SKU576: + - SUPPLIER0 + SKU577: + - SUPPLIER0 + SKU578: + - SUPPLIER0 + SKU579: + - SUPPLIER0 + SKU58: + - SUPPLIER0 + SKU580: + - SUPPLIER0 + SKU581: + - SUPPLIER0 + SKU582: + - SUPPLIER0 + SKU583: + - SUPPLIER0 + SKU584: + - SUPPLIER0 + SKU585: + - SUPPLIER0 + SKU586: + - SUPPLIER0 + SKU587: + - SUPPLIER0 + SKU588: + - SUPPLIER0 + SKU589: + - SUPPLIER0 + SKU59: + - SUPPLIER0 + SKU590: + - SUPPLIER0 + SKU591: + - SUPPLIER0 + SKU592: + - SUPPLIER0 + SKU593: + - SUPPLIER0 + SKU594: + - SUPPLIER0 + SKU595: + - SUPPLIER0 + SKU596: + - SUPPLIER0 + SKU597: + - SUPPLIER0 + SKU598: + - SUPPLIER0 + SKU599: + - SUPPLIER0 + SKU6: + - SUPPLIER0 + SKU60: + - SUPPLIER0 + SKU600: + - SUPPLIER0 + SKU601: + - SUPPLIER0 + SKU602: + - SUPPLIER0 + SKU603: + - SUPPLIER0 + SKU604: + - SUPPLIER0 + SKU605: + - SUPPLIER0 + SKU606: + - SUPPLIER0 + SKU607: + - SUPPLIER0 + SKU608: + - SUPPLIER0 + SKU609: + - SUPPLIER0 + SKU61: + - SUPPLIER0 + SKU610: + - SUPPLIER0 + SKU611: + - SUPPLIER0 + SKU612: + - SUPPLIER0 + SKU613: + - SUPPLIER0 + SKU614: + - SUPPLIER0 + SKU615: + - SUPPLIER0 + SKU616: + - SUPPLIER0 + SKU617: + - SUPPLIER0 + SKU618: + - SUPPLIER0 + SKU619: + - SUPPLIER0 + SKU62: + - SUPPLIER0 + SKU620: + - SUPPLIER0 + SKU621: + - SUPPLIER0 + SKU622: + - SUPPLIER0 + SKU623: + - SUPPLIER0 + SKU624: + - SUPPLIER0 + SKU625: + - SUPPLIER0 + SKU626: + - SUPPLIER0 + SKU627: + - SUPPLIER0 + SKU628: + - SUPPLIER0 + SKU629: + - SUPPLIER0 + SKU63: + - SUPPLIER0 + SKU630: + - SUPPLIER0 + SKU631: + - SUPPLIER0 + SKU632: + - SUPPLIER0 + SKU633: + - SUPPLIER0 + SKU634: + - SUPPLIER0 + SKU635: + - SUPPLIER0 + SKU636: + - SUPPLIER0 + SKU637: + - SUPPLIER0 + SKU638: + - SUPPLIER0 + SKU639: + - SUPPLIER0 + SKU64: + - SUPPLIER0 + SKU640: + - SUPPLIER0 + SKU641: + - SUPPLIER0 + SKU642: + - SUPPLIER0 + SKU643: + - SUPPLIER0 + SKU644: + - SUPPLIER0 + SKU645: + - SUPPLIER0 + SKU646: + - SUPPLIER0 + SKU647: + - SUPPLIER0 + SKU648: + - SUPPLIER0 + SKU649: + - SUPPLIER0 + SKU65: + - SUPPLIER0 + SKU650: + - SUPPLIER0 + SKU651: + - SUPPLIER0 + SKU652: + - SUPPLIER0 + SKU653: + - SUPPLIER0 + SKU654: + - SUPPLIER0 + SKU655: + - SUPPLIER0 + SKU656: + - SUPPLIER0 + SKU657: + - SUPPLIER0 + SKU658: + - SUPPLIER0 + SKU659: + - SUPPLIER0 + SKU66: + - SUPPLIER0 + SKU660: + - SUPPLIER0 + SKU661: + - SUPPLIER0 + SKU662: + - SUPPLIER0 + SKU663: + - SUPPLIER0 + SKU664: + - SUPPLIER0 + SKU665: + - SUPPLIER0 + SKU666: + - SUPPLIER0 + SKU667: + - SUPPLIER0 + SKU668: + - SUPPLIER0 + SKU669: + - SUPPLIER0 + SKU67: + - SUPPLIER0 + SKU670: + - SUPPLIER0 + SKU671: + - SUPPLIER0 + SKU672: + - SUPPLIER0 + SKU673: + - SUPPLIER0 + SKU674: + - SUPPLIER0 + SKU675: + - SUPPLIER0 + SKU676: + - SUPPLIER0 + SKU677: + - SUPPLIER0 + SKU678: + - SUPPLIER0 + SKU679: + - SUPPLIER0 + SKU68: + - SUPPLIER0 + SKU680: + - SUPPLIER0 + SKU681: + - SUPPLIER0 + SKU682: + - SUPPLIER0 + SKU683: + - SUPPLIER0 + SKU684: + - SUPPLIER0 + SKU685: + - SUPPLIER0 + SKU686: + - SUPPLIER0 + SKU687: + - SUPPLIER0 + SKU688: + - SUPPLIER0 + SKU689: + - SUPPLIER0 + SKU69: + - SUPPLIER0 + SKU690: + - SUPPLIER0 + SKU691: + - SUPPLIER0 + SKU692: + - SUPPLIER0 + SKU693: + - SUPPLIER0 + SKU694: + - SUPPLIER0 + SKU695: + - SUPPLIER0 + SKU696: + - SUPPLIER0 + SKU697: + - SUPPLIER0 + SKU698: + - SUPPLIER0 + SKU699: + - SUPPLIER0 + SKU7: + - SUPPLIER0 + SKU70: + - SUPPLIER0 + SKU700: + - SUPPLIER0 + SKU701: + - SUPPLIER0 + SKU702: + - SUPPLIER0 + SKU703: + - SUPPLIER0 + SKU704: + - SUPPLIER0 + SKU705: + - SUPPLIER0 + SKU706: + - SUPPLIER0 + SKU707: + - SUPPLIER0 + SKU708: + - SUPPLIER0 + SKU709: + - SUPPLIER0 + SKU71: + - SUPPLIER0 + SKU710: + - SUPPLIER0 + SKU711: + - SUPPLIER0 + SKU712: + - SUPPLIER0 + SKU713: + - SUPPLIER0 + SKU714: + - SUPPLIER0 + SKU715: + - SUPPLIER0 + SKU716: + - SUPPLIER0 + SKU717: + - SUPPLIER0 + SKU718: + - SUPPLIER0 + SKU719: + - SUPPLIER0 + SKU72: + - SUPPLIER0 + SKU720: + - SUPPLIER0 + SKU721: + - SUPPLIER0 + SKU722: + - SUPPLIER0 + SKU723: + - SUPPLIER0 + SKU724: + - SUPPLIER0 + SKU725: + - SUPPLIER0 + SKU726: + - SUPPLIER0 + SKU727: + - SUPPLIER0 + SKU728: + - SUPPLIER0 + SKU729: + - SUPPLIER0 + SKU73: + - SUPPLIER0 + SKU730: + - SUPPLIER0 + SKU731: + - SUPPLIER0 + SKU732: + - SUPPLIER0 + SKU733: + - SUPPLIER0 + SKU734: + - SUPPLIER0 + SKU735: + - SUPPLIER0 + SKU736: + - SUPPLIER0 + SKU737: + - SUPPLIER0 + SKU738: + - SUPPLIER0 + SKU739: + - SUPPLIER0 + SKU74: + - SUPPLIER0 + SKU740: + - SUPPLIER0 + SKU741: + - SUPPLIER0 + SKU742: + - SUPPLIER0 + SKU743: + - SUPPLIER0 + SKU744: + - SUPPLIER0 + SKU745: + - SUPPLIER0 + SKU746: + - SUPPLIER0 + SKU747: + - SUPPLIER0 + SKU748: + - SUPPLIER0 + SKU749: + - SUPPLIER0 + SKU75: + - SUPPLIER0 + SKU750: + - SUPPLIER0 + SKU751: + - SUPPLIER0 + SKU752: + - SUPPLIER0 + SKU753: + - SUPPLIER0 + SKU754: + - SUPPLIER0 + SKU755: + - SUPPLIER0 + SKU756: + - SUPPLIER0 + SKU757: + - SUPPLIER0 + SKU758: + - SUPPLIER0 + SKU759: + - SUPPLIER0 + SKU76: + - SUPPLIER0 + SKU760: + - SUPPLIER0 + SKU761: + - SUPPLIER0 + SKU762: + - SUPPLIER0 + SKU763: + - SUPPLIER0 + SKU764: + - SUPPLIER0 + SKU765: + - SUPPLIER0 + SKU766: + - SUPPLIER0 + SKU767: + - SUPPLIER0 + SKU768: + - SUPPLIER0 + SKU769: + - SUPPLIER0 + SKU77: + - SUPPLIER0 + SKU770: + - SUPPLIER0 + SKU771: + - SUPPLIER0 + SKU772: + - SUPPLIER0 + SKU773: + - SUPPLIER0 + SKU774: + - SUPPLIER0 + SKU775: + - SUPPLIER0 + SKU776: + - SUPPLIER0 + SKU777: + - SUPPLIER0 + SKU778: + - SUPPLIER0 + SKU779: + - SUPPLIER0 + SKU78: + - SUPPLIER0 + SKU780: + - SUPPLIER0 + SKU781: + - SUPPLIER0 + SKU782: + - SUPPLIER0 + SKU783: + - SUPPLIER0 + SKU784: + - SUPPLIER0 + SKU785: + - SUPPLIER0 + SKU786: + - SUPPLIER0 + SKU787: + - SUPPLIER0 + SKU788: + - SUPPLIER0 + SKU789: + - SUPPLIER0 + SKU79: + - SUPPLIER0 + SKU790: + - SUPPLIER0 + SKU791: + - SUPPLIER0 + SKU792: + - SUPPLIER0 + SKU793: + - SUPPLIER0 + SKU794: + - SUPPLIER0 + SKU795: + - SUPPLIER0 + SKU796: + - SUPPLIER0 + SKU797: + - SUPPLIER0 + SKU798: + - SUPPLIER0 + SKU799: + - SUPPLIER0 + SKU8: + - SUPPLIER0 + SKU80: + - SUPPLIER0 + SKU800: + - SUPPLIER0 + SKU801: + - SUPPLIER0 + SKU802: + - SUPPLIER0 + SKU803: + - SUPPLIER0 + SKU804: + - SUPPLIER0 + SKU805: + - SUPPLIER0 + SKU806: + - SUPPLIER0 + SKU807: + - SUPPLIER0 + SKU808: + - SUPPLIER0 + SKU809: + - SUPPLIER0 + SKU81: + - SUPPLIER0 + SKU810: + - SUPPLIER0 + SKU811: + - SUPPLIER0 + SKU812: + - SUPPLIER0 + SKU813: + - SUPPLIER0 + SKU814: + - SUPPLIER0 + SKU815: + - SUPPLIER0 + SKU816: + - SUPPLIER0 + SKU817: + - SUPPLIER0 + SKU818: + - SUPPLIER0 + SKU819: + - SUPPLIER0 + SKU82: + - SUPPLIER0 + SKU820: + - SUPPLIER0 + SKU821: + - SUPPLIER0 + SKU822: + - SUPPLIER0 + SKU823: + - SUPPLIER0 + SKU824: + - SUPPLIER0 + SKU825: + - SUPPLIER0 + SKU826: + - SUPPLIER0 + SKU827: + - SUPPLIER0 + SKU828: + - SUPPLIER0 + SKU829: + - SUPPLIER0 + SKU83: + - SUPPLIER0 + SKU830: + - SUPPLIER0 + SKU831: + - SUPPLIER0 + SKU832: + - SUPPLIER0 + SKU833: + - SUPPLIER0 + SKU834: + - SUPPLIER0 + SKU835: + - SUPPLIER0 + SKU836: + - SUPPLIER0 + SKU837: + - SUPPLIER0 + SKU838: + - SUPPLIER0 + SKU839: + - SUPPLIER0 + SKU84: + - SUPPLIER0 + SKU840: + - SUPPLIER0 + SKU841: + - SUPPLIER0 + SKU842: + - SUPPLIER0 + SKU843: + - SUPPLIER0 + SKU844: + - SUPPLIER0 + SKU845: + - SUPPLIER0 + SKU846: + - SUPPLIER0 + SKU847: + - SUPPLIER0 + SKU848: + - SUPPLIER0 + SKU849: + - SUPPLIER0 + SKU85: + - SUPPLIER0 + SKU850: + - SUPPLIER0 + SKU851: + - SUPPLIER0 + SKU852: + - SUPPLIER0 + SKU853: + - SUPPLIER0 + SKU854: + - SUPPLIER0 + SKU855: + - SUPPLIER0 + SKU856: + - SUPPLIER0 + SKU857: + - SUPPLIER0 + SKU858: + - SUPPLIER0 + SKU859: + - SUPPLIER0 + SKU86: + - SUPPLIER0 + SKU860: + - SUPPLIER0 + SKU861: + - SUPPLIER0 + SKU862: + - SUPPLIER0 + SKU863: + - SUPPLIER0 + SKU864: + - SUPPLIER0 + SKU865: + - SUPPLIER0 + SKU866: + - SUPPLIER0 + SKU867: + - SUPPLIER0 + SKU868: + - SUPPLIER0 + SKU869: + - SUPPLIER0 + SKU87: + - SUPPLIER0 + SKU870: + - SUPPLIER0 + SKU871: + - SUPPLIER0 + SKU872: + - SUPPLIER0 + SKU873: + - SUPPLIER0 + SKU874: + - SUPPLIER0 + SKU875: + - SUPPLIER0 + SKU876: + - SUPPLIER0 + SKU877: + - SUPPLIER0 + SKU878: + - SUPPLIER0 + SKU879: + - SUPPLIER0 + SKU88: + - SUPPLIER0 + SKU880: + - SUPPLIER0 + SKU881: + - SUPPLIER0 + SKU882: + - SUPPLIER0 + SKU883: + - SUPPLIER0 + SKU884: + - SUPPLIER0 + SKU885: + - SUPPLIER0 + SKU886: + - SUPPLIER0 + SKU887: + - SUPPLIER0 + SKU888: + - SUPPLIER0 + SKU889: + - SUPPLIER0 + SKU89: + - SUPPLIER0 + SKU890: + - SUPPLIER0 + SKU891: + - SUPPLIER0 + SKU892: + - SUPPLIER0 + SKU893: + - SUPPLIER0 + SKU894: + - SUPPLIER0 + SKU895: + - SUPPLIER0 + SKU896: + - SUPPLIER0 + SKU897: + - SUPPLIER0 + SKU898: + - SUPPLIER0 + SKU899: + - SUPPLIER0 + SKU9: + - SUPPLIER0 + SKU90: + - SUPPLIER0 + SKU900: + - SUPPLIER0 + SKU901: + - SUPPLIER0 + SKU902: + - SUPPLIER0 + SKU903: + - SUPPLIER0 + SKU904: + - SUPPLIER0 + SKU905: + - SUPPLIER0 + SKU906: + - SUPPLIER0 + SKU907: + - SUPPLIER0 + SKU908: + - SUPPLIER0 + SKU909: + - SUPPLIER0 + SKU91: + - SUPPLIER0 + SKU910: + - SUPPLIER0 + SKU911: + - SUPPLIER0 + SKU912: + - SUPPLIER0 + SKU913: + - SUPPLIER0 + SKU914: + - SUPPLIER0 + SKU915: + - SUPPLIER0 + SKU916: + - SUPPLIER0 + SKU917: + - SUPPLIER0 + SKU918: + - SUPPLIER0 + SKU919: + - SUPPLIER0 + SKU92: + - SUPPLIER0 + SKU920: + - SUPPLIER0 + SKU921: + - SUPPLIER0 + SKU922: + - SUPPLIER0 + SKU923: + - SUPPLIER0 + SKU924: + - SUPPLIER0 + SKU925: + - SUPPLIER0 + SKU926: + - SUPPLIER0 + SKU927: + - SUPPLIER0 + SKU928: + - SUPPLIER0 + SKU929: + - SUPPLIER0 + SKU93: + - SUPPLIER0 + SKU930: + - SUPPLIER0 + SKU931: + - SUPPLIER0 + SKU932: + - SUPPLIER0 + SKU933: + - SUPPLIER0 + SKU934: + - SUPPLIER0 + SKU935: + - SUPPLIER0 + SKU936: + - SUPPLIER0 + SKU937: + - SUPPLIER0 + SKU938: + - SUPPLIER0 + SKU939: + - SUPPLIER0 + SKU94: + - SUPPLIER0 + SKU940: + - SUPPLIER0 + SKU941: + - SUPPLIER0 + SKU942: + - SUPPLIER0 + SKU943: + - SUPPLIER0 + SKU944: + - SUPPLIER0 + SKU945: + - SUPPLIER0 + SKU946: + - SUPPLIER0 + SKU947: + - SUPPLIER0 + SKU948: + - SUPPLIER0 + SKU949: + - SUPPLIER0 + SKU95: + - SUPPLIER0 + SKU950: + - SUPPLIER0 + SKU951: + - SUPPLIER0 + SKU952: + - SUPPLIER0 + SKU953: + - SUPPLIER0 + SKU954: + - SUPPLIER0 + SKU955: + - SUPPLIER0 + SKU956: + - SUPPLIER0 + SKU957: + - SUPPLIER0 + SKU958: + - SUPPLIER0 + SKU959: + - SUPPLIER0 + SKU96: + - SUPPLIER0 + SKU960: + - SUPPLIER0 + SKU961: + - SUPPLIER0 + SKU962: + - SUPPLIER0 + SKU963: + - SUPPLIER0 + SKU964: + - SUPPLIER0 + SKU965: + - SUPPLIER0 + SKU966: + - SUPPLIER0 + SKU967: + - SUPPLIER0 + SKU968: + - SUPPLIER0 + SKU969: + - SUPPLIER0 + SKU97: + - SUPPLIER0 + SKU970: + - SUPPLIER0 + SKU971: + - SUPPLIER0 + SKU972: + - SUPPLIER0 + SKU973: + - SUPPLIER0 + SKU974: + - SUPPLIER0 + SKU975: + - SUPPLIER0 + SKU976: + - SUPPLIER0 + SKU977: + - SUPPLIER0 + SKU978: + - SUPPLIER0 + SKU979: + - SUPPLIER0 + SKU98: + - SUPPLIER0 + SKU980: + - SUPPLIER0 + SKU981: + - SUPPLIER0 + SKU982: + - SUPPLIER0 + SKU983: + - SUPPLIER0 + SKU984: + - SUPPLIER0 + SKU985: + - SUPPLIER0 + SKU986: + - SUPPLIER0 + SKU987: + - SUPPLIER0 + SKU988: + - SUPPLIER0 + SKU989: + - SUPPLIER0 + SKU99: + - SUPPLIER0 + SKU990: + - SUPPLIER0 + SKU991: + - SUPPLIER0 + SKU992: + - SUPPLIER0 + SKU993: + - SUPPLIER0 + SKU994: + - SUPPLIER0 + SKU995: + - SUPPLIER0 + SKU996: + - SUPPLIER0 + SKU997: + - SUPPLIER0 + SKU998: + - SUPPLIER0 + SKU999: + - SUPPLIER0 diff --git a/examples/supply_chain/topologies/sample1/config.yml b/examples/supply_chain/topologies/sample1/config.yml new file mode 100644 index 000000000..ffcd92d45 --- /dev/null +++ b/examples/supply_chain/topologies/sample1/config.yml @@ -0,0 +1,335 @@ + +# TODO: which config to inherit +# base: "" + +#core: +# datamodels: "xxx" +# units: "xxx" +# facilities: "xxx" + + +facility_definitions: + # facility definition + WarehouseFacility: &warehouse_facility + class: "WarehouseFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + # if true then will call generate function of class type + is_template: true + # config will be passed to generator as parameters + config: + agent_type: "product" + consumer: + class: "ConsumerUnit" + config: + agent_type: "consumer" + config: + agent_type: "facility" + + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + agent_type: "product" + consumer: + class: "ConsumerUnit" + config: + agent_type: "consumer" + manufacture: + class: "ManufactureUnit" + config: + agent_type: "producer" + config: + agent_type: "facility" + + RetailerFacility: &retailer_facility + class: "RetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "StoreProductUnit" + is_template: true + config: + agent_type: "product" + consumer: + class: "ConsumerUnit" + config: + agent_type: "consumer" + seller: + class: "SellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: "facility" + + + # definition for outer retailer that read demand from csv files. + # NOTE: make sure you provide a valid file path for each retailer instance. + OuterRetailerFacility: &outerretailer_facility + class: "OuterRetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "StoreProductUnit" + is_template: true + config: + agent_type: 5 + consumer: + class: "ConsumerUnit" + seller: + class: "OuterSellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: 2 + seller_sampler_type: data # data, model. What kind of sampler to use for seller. + sku_column: "SKU" # SKU column name + price_column: "Price" # Price column name + sale_column: "Sales" # Sales column name + datetime_column: "DT" # Datetime column name + file_path: "/path/to/data.csv" # full path to data file, override by each store instance + + +# common entity/unit definition as reference to simplify the file. +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 100 + +# a normal distribution definition +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + vehicles: + - *normal_vehicle + - *normal_vehicle + config: + unit_price: 1 + +small_storage: &small_storage + # config of data model of this unit + config: + # other config or storage unit + capacity: 10000 + unit_storage_cost: 1 + +midium_storage: &midium_storage + config: + capacity: 20000 + unit_storage_cost: 1 + +huge_storage: &huge_storage + config: + capacity: 30000 + unit_storage_cost: 1 + +# sku list in this world +# this list do not contains price, cost or other facility related attributes, +# but just base info, like name, id, bom +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 12 + # bill of material that used produce current sku, empty means do not need source material + bom: + # key is the source sku name, value is quantity needed to use per time to produce current sku + sku3: 10 + + - id: 2 + name: "sku2" + output_units_per_lot: 1 + + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + +# world definitions +world: + # here we use reference to make it each to edit. + skus: *sku_definitions + + # facilities in this world + facilities: + - name: "Supplier_001" # name of the facility + # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level + # use the facility definition as base, then we can override configs partially. + definition_ref: "SupplierFacility" + + # sku list of this facility + skus: + sku3: # sku name and attributes needed for this facility + init_stock: 100 + product_unit_cost: 1 + production_rate: 1 + type: "production" # production means this is the output production of this facility + cost: 10 + price: 10 + vlt: 1 + + # configuration of child units. + children: + # config of storage unit + storage: *small_storage + distribution: *normal_distribution + + # products use default config in core.yml + + # config of this facility + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Supplier_002" + definition_ref: "SupplierFacility" + + skus: + sku1: + init_stock: 100 + product_unit_cost: 1 + production_rate: 1 + type: "production" + cost: 10 + price: 100 + vlt: 1 + sku3: + init_stock: 100 + production_rate: 1 + type: "material" + cost: 10 + price: 100 + vlt: 1 + + children: + storage: *small_storage + distribution: *normal_distribution + + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Warehouse_001" + definition_ref: "WarehouseFacility" + + skus: + sku1: + init_stock: 1000 + price: 100 + vlt: 1 + sku2: + init_stock: 1000 + price: 100 + vlt: 1 + sku3: + init_stock: 1000 + price: 100 + vlt: 1 + + children: + storage: *huge_storage + distribution: *normal_distribution + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Retailer_001" + definition_ref: "RetailerFacility" + + skus: + sku1: + price: 300 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 # optional + vlt: 1 + sku3: + price: 200 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + vlt: 1 + sku2: + price: 100 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + vlt: 1 + + children: + storage: *midium_storage + + config: + order_cost: 0 + # topology used to specify the up/downstream for facilities + # we split it from facility, so that we can support configuration inherit to override it + # for a new topology + # TODO: change the name? + topology: + # key is current facility, value if upstream facilities that will provide a certain sku + Supplier_002: + # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", + # or any other facility in the list + sku3: + - "Supplier_001" + Warehouse_001: + sku1: + - "Supplier_002" + sku3: + - "Supplier_001" + Retailer_001: + sku1: + - "Supplier_002" + sku3: + - "Supplier_001" + + # map grid definitions + grid: + size: [20, 20] + + # facility position in grid + facilities: + Supplier_001: [0, 0] + Supplier_002: [3, 3] + Warehouse_001: [6, 6] + Retailer_001: [10, 18] + + # cells that un-traversable + blocks: + railroad: + - [10, 10] + - [10, 11] + - [10, 12] + - [11, 12] + +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e7 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 + start_date_time: "2010-12-01" # start time for data in files, for outer retailer diff --git a/examples/supply_chain/topologies/test/config.yml b/examples/supply_chain/topologies/test/config.yml new file mode 100644 index 000000000..e3bd96bc7 --- /dev/null +++ b/examples/supply_chain/topologies/test/config.yml @@ -0,0 +1,367 @@ + +# TODO: which config to inherit +# base: "" + +#core: +# datamodels: "xxx" +# units: "xxx" +# facilities: "xxx" + + +facility_definitions: + # facility definition + WarehouseFacility: &warehouse_facility + class: "WarehouseFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + # if true then will call generate function of class type + is_template: true + # config will be passed to generator as parameters + config: + agent_type: "product" + consumer: + class: "ConsumerUnit" + config: + agent_type: "consumer" + config: + agent_type: "facility" + + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + agent_type: "product" + manufacture: + class: "SimpleManufactureUnit" + config: + agent_type: "producer" + config: + agent_type: "facility" + + RetailerFacility: &retailer_facility + class: "RetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "StoreProductUnit" + is_template: true + config: + agent_type: "productstore" + consumer: + class: "ConsumerUnit" + config: + agent_type: "consumerstore" + seller: + class: "SellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: "facility" + + + # definition for outer retailer that read demand from csv files. + # NOTE: make sure you provide a valid file path for each retailer instance. + OuterRetailerFacility: &outerretailer_facility + class: "OuterRetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "StoreProductUnit" + is_template: true + config: + agent_type: "consumer" + consumer: + class: "ConsumerUnit" + seller: + class: "OuterSellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: "facility" + seller_sampler_type: data # data, model. What kind of sampler to use for seller. + sku_column: "SKU" # SKU column name + price_column: "Price" # Price column name + sale_column: "Sales" # Sales column name + datetime_column: "DT" # Datetime column name + file_path: "/path/to/data.csv" # full path to data file, override by each store instance + + +# common entity/unit definition as reference to simplify the file. +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 2 + +# a normal distribution definition +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + vehicles: + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + - *normal_vehicle + config: + unit_price: 1 + +huge_storage: &huge_storage + # config of data model of this unit + config: + # other config or storage unit + capacity: 10000 + unit_storage_cost: 1 + +medium_storage: &medium_storage + config: + capacity: 5000 + unit_storage_cost: 1 + +small_storage: &small_storage + config: + capacity: 3000 + unit_storage_cost: 1 + +# sku list in this world +# this list do not contains price, cost or other facility related attributes, +# but just base info, like name, id, bom +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 10 + # bill of material that used produce current sku, empty means do not need source material + bom: + # key is the source sku name, value is quantity needed to use per time to produce current sku + sku1: 1 + + - id: 2 + name: "sku2" + output_units_per_lot: 10 + bom: + sku2: 1 + + - id: 3 + name: "sku3" + output_units_per_lot: 10 + bom: + sku3: 1 + + +# world definitions +world: + # here we use reference to make it each to edit. + skus: *sku_definitions + + # facilities in this world + facilities: + - name: "Supplier_001" # name of the facility + # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level + # use the facility definition as base, then we can override configs partially. + definition_ref: "SupplierFacility" + + # sku list of this facility + skus: + sku1: # sku name and attributes needed for this facility + init_stock: 1000 + product_unit_cost: 90 + production_rate: 1000 + type: "production" # production means this is the output production of this facility + cost: 90 + price: 100 + vlt: 2 + sku2: + init_stock: 1000 + product_unit_cost: 150 + production_rate: 1000 + type: "production" + cost: 150 + price: 200 + vlt: 3 + sku3: + init_stock: 1000 + production_rate: 100 + product_unit_cost: 200 + type: "production" + cost: 200 + price: 300 + vlt: 1 + + # configuration of child units. + children: + # config of storage unit + storage: *huge_storage + distribution: *normal_distribution + + # products use default config in core.yml + + # config of this facility + config: + delay_order_penalty: 10 + order_cost: 0 + + - name: "Warehouse_001" + definition_ref: "WarehouseFacility" + + skus: + sku1: + init_stock: 1000 + price: 200 + vlt: 2 + sku2: + init_stock: 1000 + price: 200 + vlt: 3 + sku3: + init_stock: 1000 + price: 300 + vlt: 1 + + children: + storage: *medium_storage + distribution: *normal_distribution + config: + delay_order_penalty: 1 + order_cost: 10 + + - name: "Retailer_001" + definition_ref: "RetailerFacility" + + skus: + sku1: + price: 200 + cost: 100 + init_stock: 40 + sale_gamma: 20 + backlog_ratio: 0.1 # optional + vlt: 1 + sku2: + price: 350 + cost: 200 + init_stock: 160 + sale_gamma: 80 + backlog_ratio: 0.1 + vlt: 1 + sku3: + price: 650 + cost: 300 + init_stock: 100 + sale_gamma: 50 + backlog_ratio: 0.1 + vlt: 1 + + children: + storage: *small_storage + + config: + order_cost: 0 + + # topology used to specify the up/downstream for facilities + # we split it from facility, so that we can support configuration inherit to override it + # for a new topology + # TODO: change the name? + topology: + # key is current facility, value if upstream facilities that will provide a certain sku + Warehouse_001: + sku1: + - "Supplier_001" + sku2: + - "Supplier_001" + sku3: + - "Supplier_001" + Retailer_001: + sku1: + - "Warehouse_001" + sku2: + - "Warehouse_001" + sku3: + - "Warehouse_001" + + # map grid definitions + grid: + size: [20, 20] + + # facility position in grid + facilities: + Supplier_001: [0, 0] + Warehouse_001: [6, 6] + Retailer_001: [10, 18] + + # cells that un-traversable + blocks: + railroad: + - [10, 10] + - [10, 11] + - [10, 12] + - [11, 12] + +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e6 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 + start_date_time: "2010-12-01" # start time for data in files, for outer retailer diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py new file mode 100644 index 000000000..b6e6162ff --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .actions import ConsumerAction, ManufactureAction +from .datamodels import ( + ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel +) +from .facilities import FacilityBase, RetailerFacility, SupplierFacility, WarehouseFacility +from .units import ConsumerUnit, DistributionUnit, ProductUnit, SellerUnit, ExtendUnitBase, StorageUnit, UnitBase, VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/actions.py b/maro/simulator/scenarios/supply_chain/actions.py new file mode 100644 index 000000000..b6791a851 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/actions.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from collections import namedtuple + +ConsumerAction = namedtuple("ConsumerAction", ("id", "product_id", "source_id", "quantity", "vlt", "reward_discount")) + +ManufactureAction = namedtuple("ManufactureAction", ("id", "production_rate")) diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py new file mode 100644 index 000000000..c17c713b3 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -0,0 +1,174 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +import os +from typing import List, Tuple + +from maro.event_buffer import MaroEvents +from maro.simulator.scenarios import AbsBusinessEngine + +from .parser import ConfigParser, SupplyChainConfiguration +from .units import ProductUnit, UnitBase +from .world import World + + +class SupplyChainBusinessEngine(AbsBusinessEngine): + def __init__(self, **kwargs): + super().__init__(scenario_name="supply_chain", **kwargs) + + self._register_events() + + self._build_world() + + self._product_units = [] + + # Prepare product unit for later using. + for unit in self.world.units.values(): + if issubclass(type(unit), ProductUnit): + self._product_units.append(unit) + + self._frame = self.world.frame + + self._node_mapping = self.world.get_node_mapping() + + # Used to cache the action from outside, then dispatch to units at the beginning of step. + self._action_cache = None + + self._metrics_cache = None + + @property + def frame(self): + return self._frame + + @property + def snapshots(self): + return self._frame.snapshots + + @property + def configs(self) -> SupplyChainConfiguration: + return self.world.configs + + def step(self, tick: int): + # Clear the metrics cache. + self._metrics_cache = None + + # NOTE: we have to dispatch the action here. + self._dispatch_action() + self._step_by_facility(tick) + + # We do not have payload here. + decision_event = self._event_buffer.gen_decision_event(tick, None) + + self._event_buffer.insert_event(decision_event) + + def post_step(self, tick: int): + self._post_step_by_facility(tick) + + return tick + 1 == self._max_tick + + def reset(self): + self._frame.reset() + + if self._frame.snapshots: + self._frame.snapshots.reset() + + self._reset_by_facility() + + self._action_cache = None + + def get_node_mapping(self) -> dict: + return self._node_mapping + + def get_agent_idx_list(self) -> List[Tuple[str, int]]: + """Get a list of agent index. + + Returns: + list: List of agent index. + """ + return self.world.agent_list + + def _step_by_facility(self, tick: int): + """Call step functions by facility. + + Args: + tick (int): Current tick. + """ + # Step first. + for facility in self.world.facilities.values(): + facility.step(tick) + + # Then flush states to frame before generate decision event. + for facility in self.world.facilities.values(): + facility.flush_states() + + def _post_step_by_facility(self, tick: int): + """Call post_step functions by facility.""" + for facility in self.world.facilities.values(): + facility.post_step(tick) + + def _reset_by_facility(self): + """Call reset functions by facility.""" + for facility in self.world.facilities.values(): + facility.reset() + + def _register_events(self): + self._event_buffer.register_event_handler( + MaroEvents.TAKE_ACTION, self._on_action_received) + + def _build_world(self): + self.update_config_root_path(__file__) + + # Core configuration always in topologies folder. + be_root = os.path.split(os.path.realpath(__file__))[0] + core_config = os.path.join(be_root, "topologies", "core.yml") + + config_path = os.path.join(self._config_path, "config.yml") + + parser = ConfigParser(core_config, config_path) + + conf = parser.parse() + + self.world = World() + + self.world.build(conf, self.calc_max_snapshots(), self._max_tick) + + def _on_action_received(self, event): + action = event.payload + + if action is not None and type(action) == list and len(action) > 0: + self._action_cache = action + + def _dispatch_action(self): + if self._action_cache is not None: + # NOTE: we assume that the action_cache is a list of action, and each action has an id field. + for action in self._action_cache: + entity = self.world.get_entity(action.id) + + if entity is not None and issubclass(type(entity), UnitBase): + entity.set_action(action) + + self._action_cache = None + + def get_metrics(self): + if self._metrics_cache is None: + self._metrics_cache = { + "products": { + product.id: { + "sale_mean": product.get_sale_mean(), + "sale_std": product.get_sale_std(), + "selling_price": product.get_selling_price(), + "pending_order_daily": + None if product.consumer is None else product.consumer.pending_order_daily + } for product in self._product_units + }, + "facilities": { + facility.id: { + "in_transit_orders": facility.get_in_transit_orders(), + "pending_order": + None if facility.distribution is None else facility.distribution.get_pending_order() + } for facility in self.world.facilities.values() + } + } + + return self._metrics_cache diff --git a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py new file mode 100644 index 000000000..c1a8c9b9a --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .consumer import ConsumerDataModel +from .distribution import DistributionDataModel +from .facility import FacilityDataModel +from .manufacture import ManufactureDataModel +from .product import ProductDataModel +from .seller import SellerDataModel +from .storage import StorageDataModel +from .vehicle import VehicleDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py new file mode 100644 index 000000000..834864072 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/base.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, NodeBase + + +class DataModelBase(NodeBase): + """Base of all data model of this scenario.""" + # Id of related unit or facility, 0 is invalid by default. + id = NodeAttribute(AttributeType.Int) + + # Id of facility this unit belongs to. + facility_id = NodeAttribute(AttributeType.Int) + + def __init__(self): + self._unit_id = 0 + self._facility_id = 0 + + def initialize(self, **kwargs): + """Initialize the fields with configs, the config should be a dict. + + Args: + kwargs (dict): Configuration of related data model, used to hold value to + reset after frame reset. + """ + # Called from unit after frame is ready. + pass + + def reset(self): + """Reset after each episode.""" + # Called per episode. + self.id = self._unit_id + self.facility_id = self._facility_id + + def set_id(self, unit_id: int, facility_id: int): + """Used to assign id(s), so that it will be assigned after frame rest. + + Args: + unit_id (int): Id of related unit. + facility_id (int): Id of this unit belongs to. + """ + self._unit_id = unit_id + self._facility_id = facility_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py new file mode 100644 index 000000000..5cd9855e5 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, node + +from .extend import ExtendDataModel + + +@node("consumer") +class ConsumerDataModel(ExtendDataModel): + """Data model for consumer unit.""" + total_purchased = NodeAttribute(AttributeType.UInt) + total_received = NodeAttribute(AttributeType.UInt) + + purchased = NodeAttribute(AttributeType.UInt) + received = NodeAttribute(AttributeType.UInt) + order_product_cost = NodeAttribute(AttributeType.UInt) + + latest_consumptions = NodeAttribute(AttributeType.Float) + + order_quantity = NodeAttribute(AttributeType.UInt) + + price = NodeAttribute(AttributeType.Float) + order_cost = NodeAttribute(AttributeType.Float) + + reward_discount = NodeAttribute(AttributeType.Float) + + def __init__(self): + super(ConsumerDataModel, self).__init__() + + self._price = 0 + self._order_cost = 0 + + def initialize(self, price: int, order_cost: int): + self._price = price + self._order_cost = order_cost + + self.reset() + + def reset(self): + super(ConsumerDataModel, self).reset() + + self.price = self._price + self.order_cost = self._order_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py new file mode 100644 index 000000000..ee28e4c7f --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, node + +from .base import DataModelBase + + +@node("distribution") +class DistributionDataModel(DataModelBase): + """Distribution data model for distribution unit.""" + + remaining_order_quantity = NodeAttribute(AttributeType.UInt) + remaining_order_number = NodeAttribute(AttributeType.UInt) + + def __init__(self): + super(DistributionDataModel, self).__init__() + + def reset(self): + super(DistributionDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/extend.py b/maro/simulator/scenarios/supply_chain/datamodels/extend.py new file mode 100644 index 000000000..e16c09c05 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/extend.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute + +from .base import DataModelBase + + +class ExtendDataModel(DataModelBase): + """Data model for sku related unit.""" + # Product id of this consumer belongs to. + product_id = NodeAttribute(AttributeType.UInt) + + # Parent unit id. + product_unit_id = NodeAttribute(AttributeType.UInt) + + def __init__(self): + super(ExtendDataModel, self).__init__() + + self._product_id = 0 + self._product_unit_id = 0 + + def reset(self): + super(ExtendDataModel, self).reset() + + self.product_id = self._product_id + self.product_unit_id = self._product_unit_id + + def set_product_id(self, product_id: int, product_unit_id: int): + self._product_id = product_id + self._product_unit_id = product_unit_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/facility.py b/maro/simulator/scenarios/supply_chain/datamodels/facility.py new file mode 100644 index 000000000..8aae3d146 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/facility.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.frame import node + +from .base import DataModelBase + + +@node("facility") +class FacilityDataModel(DataModelBase): + + def __init__(self): + super(FacilityDataModel, self).__init__() + + def reset(self): + super(FacilityDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py new file mode 100644 index 000000000..f2039c498 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, node + +from .extend import ExtendDataModel + + +@node("manufacture") +class ManufactureDataModel(ExtendDataModel): + """Data model for manufacture unit.""" + # Number per tick, different with original manufacturing cost, we just provide number, and cost + # user can determine how to calculate the cost. + manufacturing_number = NodeAttribute(AttributeType.UInt) + + product_unit_cost = NodeAttribute(AttributeType.Float) + + def __init__(self): + super(ManufactureDataModel, self).__init__() + + self._product_unit_cost = 0 + + def initialize(self, product_unit_cost): + self._product_unit_cost = product_unit_cost + + self.reset() + + def reset(self): + super(ManufactureDataModel, self).reset() + + self.product_unit_cost = self._product_unit_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/product.py b/maro/simulator/scenarios/supply_chain/datamodels/product.py new file mode 100644 index 000000000..3c3d4fa56 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/product.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, node + +from .extend import ExtendDataModel + + +@node("product") +class ProductDataModel(ExtendDataModel): + price = NodeAttribute(AttributeType.Float) + distribution_check_order = NodeAttribute(AttributeType.UInt) + distribution_transport_cost = NodeAttribute(AttributeType.Float) + distribution_delay_order_penalty = NodeAttribute(AttributeType.Float) + + def __init__(self): + super(ProductDataModel, self).__init__() + + self._price = 0 + + def initialize(self, price: float): + self._price = price + + self.reset() + + def reset(self): + super(ProductDataModel, self).reset() + + self.price = self._price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py new file mode 100644 index 000000000..ce4b4b9b9 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/seller.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, node + +from .extend import ExtendDataModel + + +@node("seller") +class SellerDataModel(ExtendDataModel): + """Data model for seller unit.""" + demand = NodeAttribute(AttributeType.UInt) + sold = NodeAttribute(AttributeType.UInt) + total_demand = NodeAttribute(AttributeType.UInt) + total_sold = NodeAttribute(AttributeType.UInt) + price = NodeAttribute(AttributeType.Float) + backlog_ratio = NodeAttribute(AttributeType.Float) + + def __init__(self): + super(SellerDataModel, self).__init__() + self._price = 0 + self._backlog_ratio = 0 + + def initialize(self, price: int, backlog_ratio: float): + self._price = price + self._backlog_ratio = backlog_ratio + + self.reset() + + def reset(self): + super(SellerDataModel, self).reset() + + self.backlog_ratio = self._backlog_ratio + self.price = self._price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py new file mode 100644 index 000000000..2e1280961 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/storage.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, node + +from .base import DataModelBase + + +@node("storage") +class StorageDataModel(DataModelBase): + """Data model for storage unit.""" + remaining_space = NodeAttribute(AttributeType.UInt) + capacity = NodeAttribute(AttributeType.UInt) + + # original is , used to save product and its number + product_list = NodeAttribute(AttributeType.UInt, 1, is_list=True) + product_number = NodeAttribute(AttributeType.UInt, 1, is_list=True) + + def __init__(self): + super(StorageDataModel, self).__init__() + + self._capacity = 0 + self._remaining_space = None + self._product_list = None + self._product_number = None + + def initialize( + self, + capacity: int = 0, + remaining_space: int = None, + product_list: list = None, + product_number: list = None + ): + self._capacity = capacity + self._remaining_space = remaining_space + self._product_list = product_list + self._product_number = product_number + + self.reset() + + def reset(self): + super(StorageDataModel, self).reset() + + self.capacity = self._capacity + + if self._remaining_space is not None: + self.remaining_space = self._remaining_space + else: + self.remaining_space = self._capacity + + if self._product_list is not None: + for id in self._product_list: + self.product_list.append(id) + + if self._product_number is not None: + for n in self._product_number: + self.product_number.append(n) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py new file mode 100644 index 000000000..34cb1df64 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.backends.backend import AttributeType +from maro.backends.frame import NodeAttribute, node + +from .base import DataModelBase + + +@node("vehicle") +class VehicleDataModel(DataModelBase): + # Number of product. + payload = NodeAttribute(AttributeType.UInt) + + unit_transport_cost = NodeAttribute(AttributeType.Float) + + def __init__(self): + super(VehicleDataModel, self).__init__() + + self._unit_transport_cost = 1 + + def initialize(self, unit_transport_cost: int = 1): + self._unit_transport_cost = unit_transport_cost + + self.reset() + + def reset(self): + super(VehicleDataModel, self).reset() + + self.unit_transport_cost = self._unit_transport_cost diff --git a/maro/simulator/scenarios/supply_chain/easy_config.py b/maro/simulator/scenarios/supply_chain/easy_config.py new file mode 100644 index 000000000..83e5f7551 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/easy_config.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +class EasyConfig(dict): + """A wrapper base on dictionary, give it ability to access key as property.""" + + # Default value for not exist keys. + default_values: dict = {} + + def __getattr__(self, key): + if key in self: + return self[key] + + if key in self.default_values: + return self.default_values[key] + + return None + + def __setattr__(self, key, value): + self[key] = value + + +class SkuInfo(EasyConfig): + """Sku information wrapper, with default property value.""" + default_values = { + "price": 0, + "vlt": 1, + "product_unit_cost": 0, + "backlog_ratio": 0, + "service_level": 0.90, + "cost": 0 + } diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py new file mode 100644 index 000000000..7ce1f382d --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .facility import FacilityBase +from .outerretailer import OuterRetailerFacility +from .retailer import RetailerFacility +from .supplier import SupplierFacility +from .warehouse import WarehouseFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py new file mode 100644 index 000000000..c5a3e6781 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -0,0 +1,177 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC +from collections import defaultdict +from typing import Dict, List + +from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo +from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit + + +class FacilityBase(ABC): + """Base of all facilities.""" + + # Id of this facility. + id: int = None + + # Name of this facility. + name: str = None + + # World of this facility belongs to. + world = None + + # Skus in this facility. + skus: Dict[int, SkuInfo] = None + + # Product units for each sku in this facility. + # Key is sku(product) id, value is the instance of product unit. + products: Dict[int, ProductUnit] = None + + # Storage unit in this facility. + storage: StorageUnit = None + + # Distribution unit in this facility. + distribution: DistributionUnit = None + + # Upstream facilities. + # Key is sku id, value is the list of product unit from upstream. + upstreams: Dict[int, List[ProductUnit]] = None + + # Down stream facilities, value same as upstreams. + downstreams: Dict[int, List[ProductUnit]] = None + + # Configuration of this facility. + configs: dict = None + + # Name of data model, from configuration. + data_model_name: str = None + + # Index of the data model node. + data_model_index: int = 0 + + data_model: object = None + + # Children of this facility (valid units). + children: list = None + + def __init__(self): + self.upstreams = {} + self.downstreams = {} + self.children = [] + self.skus = {} + + def parse_skus(self, configs: dict): + """Parse sku information from config. + + Args: + configs (dict): Configuration of skus belongs to this facility. + """ + for sku_name, sku_config in configs.items(): + global_sku = self.world.get_sku_by_name(sku_name) + facility_sku = SkuInfo(sku_config) + facility_sku.id = global_sku.id + + self.skus[global_sku.id] = facility_sku + + def parse_configs(self, configs: dict): + """Parse configuration of this facility. + + Args: + configs (dict): Configuration of this facility. + """ + self.configs = configs + + def get_config(self, key: str, default: object = None) -> object: + """Get specified configuration of facility. + + Args: + key (str): Key of the configuration. + default (object): Default value if key not exist, default is None. + + Returns: + object: value in configuration. + """ + return default if self.configs is None else self.configs.get(key, default) + + def initialize(self): + """Initialize this facility after frame is ready.""" + self.data_model.initialize() + + # Put valid units into the children, used to simplify following usage. + if self.storage is not None: + self.children.append(self.storage) + + if self.distribution is not None: + self.children.append(self.distribution) + + if self.products is not None: + for product in self.products.values(): + self.children.append(product) + + def step(self, tick: int): + """Push facility to next step. + + Args: + tick (int): Current simulator tick. + """ + for unit in self.children: + unit.step(tick) + + def flush_states(self): + """Flush states into frame.""" + for unit in self.children: + unit.flush_states() + + def post_step(self, tick: int): + """Post processing at the end of step.""" + for unit in self.children: + unit.post_step(tick) + + def reset(self): + """Reset facility for new episode.""" + for unit in self.children: + unit.reset() + + if self.data_model is not None: + self.data_model.reset() + + def get_in_transit_orders(self): + in_transit_orders = defaultdict(int) + + for product_id, product in self.products.items(): + if product.consumer is not None: + in_transit_orders[product_id] = product.consumer.get_in_transit_quantity() + + return in_transit_orders + + def set_action(self, action: object): + pass + + def get_node_info(self) -> dict: + products_info = {} + + for product_id, product in self.products.items(): + products_info[product_id] = product.get_unit_info() + + return { + "id": self.id, + "name": self.name, + "class": type(self), + "node_index": self.data_model_index, + "units": { + "storage": self.storage.get_unit_info() if self.storage is not None else None, + "distribution": self.distribution.get_unit_info() if self.distribution is not None else None, + "products": products_info + }, + "configs": self.configs, + "skus": self.skus, + "upstreams": { + product_id: [f.id for f in source_list] + for product_id, source_list in self.upstreams.items() + }, + "downstreams": { + product_id: [f.id for f in source_list] + for product_id, source_list in self.downstreams.items() + } + } diff --git a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py new file mode 100644 index 000000000..1d8e85efe --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from maro.simulator.scenarios.supply_chain.units import DataFileDemandSampler, OuterSellerUnit + +from .retailer import RetailerFacility + +# Mapping for supported sampler. +sampler_mapping = { + "data": DataFileDemandSampler +} + + +class OuterRetailerFacility(RetailerFacility): + """Retailer (store) facility that use outer data as seller demand. + + NOTE: + This require that all product seller is subclass of OuterSellerUnit. + """ + + def initialize(self): + super(OuterRetailerFacility, self).initialize() + + # What kind of sampler we need? + sampler_cls = sampler_mapping[self.configs.get("seller_sampler_type", "data")] + + sampler = sampler_cls(self.configs, self.world) + + # Go though product to find sellers. + for product in self.products.values(): + seller = product.seller + + if seller is not None: + assert issubclass(type(seller), OuterSellerUnit) + + seller.sampler = sampler diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py new file mode 100644 index 000000000..6a08fc64d --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/retailer.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .facility import FacilityBase + + +class RetailerFacility(FacilityBase): + """Retail facility used to generate order from upstream, and sell products by demand.""" + pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py new file mode 100644 index 000000000..7ddface02 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/supplier.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .facility import FacilityBase + + +class SupplierFacility(FacilityBase): + """Supplier facilities used to produce products with material products.""" + pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py new file mode 100644 index 000000000..97725d171 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .facility import FacilityBase + + +class WarehouseFacility(FacilityBase): + """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" + pass diff --git a/maro/simulator/scenarios/supply_chain/frame_builder.py b/maro/simulator/scenarios/supply_chain/frame_builder.py new file mode 100644 index 000000000..9cde1d97a --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/frame_builder.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List, Tuple + +from maro.backends.frame import FrameBase, FrameNode, NodeBase + + +def build_frame(enable_snapshot: bool, total_snapshots: int, nodes: List[Tuple[NodeBase, str, int]]): + class Frame(FrameBase): + def __init__(self): + # Inject the node definition to frame to support add node dynamically. + for node_cls, name, number in nodes: + setattr(Frame, name, FrameNode(node_cls, number)) + + super().__init__(enable_snapshot=enable_snapshot, total_snapshot=total_snapshots, backend_name="dynamic") + + return Frame() diff --git a/maro/simulator/scenarios/supply_chain/parser.py b/maro/simulator/scenarios/supply_chain/parser.py new file mode 100644 index 000000000..16046e07a --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/parser.py @@ -0,0 +1,231 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from collections import namedtuple +from importlib import import_module + +from yaml import safe_load + +DataModelDef = namedtuple("DataModelDef", ("alias", "module_path", "class_name", "class_type", "name_in_frame")) +UnitDef = namedtuple("UnitDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) +FacilityDef = namedtuple("FacilityDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) + + +def find_class_type(module_path: str, class_name: str) -> type: + """Find class type by module path and class name. + + Args: + module_path (str): Full path of the module. + class_name (str): Class name to find. + + Returns: + type: Type of specified class. + """ + target_module = import_module(module_path) + + return getattr(target_module, class_name) + + +def copy_dict(target: dict, source: dict): + """Copy values from source to target dict. + + Args: + target (dict): Target dictionary to copy to. + source (dict): Source dictionary to copy from. + """ + for k, v in source.items(): + if type(v) != dict: + target[k] = v + else: + if k not in target: + target[k] = {} + + copy_dict(target[k], v) + + +class SupplyChainConfiguration: + """Configuration of supply chain scenario.""" + + def __init__(self): + # Data model definitions. + self.data_models = {} + + # Unit definitions. + self.units = {} + + # Facility definitions. + self.facilities = {} + + # World configurations. + self.world = {} + + # Other settings. + self.settings = {} + + def add_data_definition(self, alias: str, class_name: str, module_path: str, name_in_frame: str): + """Add a data model definition. + + Args: + alias (str): Alias of this data model. + class_name (str): Name of class. + module_path (str): Full path of module. + name_in_frame (str): Data model name in frame. + """ + # Check conflicting. + assert alias not in self.data_models + + self.data_models[alias] = DataModelDef( + alias, + module_path, + class_name, + find_class_type(module_path, class_name), + name_in_frame + ) + + def add_unit_definition(self, alias: str, class_name: str, module_path: str, data_model: str): + """Add unit definition. + + Args: + alias (str): Alias of this data model. + class_name (str): Name of class. + module_path (str): Full path of module. + data_model (str): Data model used for this unit. + """ + assert alias not in self.units + + self.units[alias] = UnitDef( + alias, + module_path, + class_name, + find_class_type(module_path, class_name), + data_model + ) + + def add_facility_definition(self, alias: str, class_name: str, module_path: str, data_model_alias: str): + """Add a facility definition. + + Args: + alias (str): Alias of this facility. + class_name (str): Name of this class. + module_path (str): Full path of the module. + """ + assert alias not in self.facilities + + self.facilities[alias] = FacilityDef( + alias, + module_path, + class_name, + find_class_type(module_path, class_name), + data_model_alias + ) + + +class ConfigParser: + """Supply chain configuration parser.""" + + def __init__(self, core_path: str, config_path: str): + self._result = SupplyChainConfiguration() + + self._core_path = core_path + self._config_path = config_path + + def parse(self): + """Parse configuration of current scenario. + + Returns: + SupplyChainConfiguration: Configuration result of this scenario. + """ + self._parse_core() + self._parse_config() + + return self._result + + def _parse_core(self): + """Parse configuration from core.yml.""" + with open(self._core_path, "rt") as fp: + conf = safe_load(fp) + + self._parse_core_conf(conf) + + def _parse_core_conf(self, conf: dict): + # Data models. + if "datamodels" in conf: + for module_conf in conf["datamodels"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + self._result.add_data_definition( + class_alias, + class_def["class"], + module_path, + class_def["name_in_frame"] + ) + + # TODO: dup code + # Units. + if "units" in conf: + for module_conf in conf["units"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + # children not in unit definition + self._result.add_unit_definition( + class_alias, + class_def["class"], + module_path, + class_def.get("datamodel", None) + ) + + # Facilities. + if "facilities" in conf: + for module_conf in conf["facilities"]["modules"]: + module_path = module_conf["path"] + + for class_alias, class_def in module_conf["definitions"].items(): + self._result.add_facility_definition( + class_alias, + class_def["class"], + module_path, + class_def.get("datamodel", None) + ) + + def _parse_config(self): + """Parse configurations.""" + with open(self._config_path, "rt") as fp: + conf = safe_load(fp) + + # Read customized core part. + customized_core_conf = conf.get("core", None) + + if customized_core_conf is not None: + self._parse_core_conf(customized_core_conf) + + # Facility definitions is not required, but it would be much simple to config with it + facility_definitions = conf.get("facility_definitions", {}) + world_def = conf["world"] + + # Go through world configurations to generate a full one. + # . Copy other configurations first + for sub_conf_name in ("skus", "topology", "grid"): + self._result.world[sub_conf_name] = world_def[sub_conf_name] + + # . Copy facilities content different if without definition reference. + # or copy from definition first, then override with current. + self._result.world["facilities"] = [] + + for facility_conf in world_def["facilities"]: + facility_ref = facility_conf.get("definition_ref", None) + + facility = {} + + if facility_ref is not None: + # Copy definition from base. + copy_dict(facility, facility_definitions[facility_ref]) + + # Override with current. + copy_dict(facility, facility_conf) + + self._result.world["facilities"].append(facility) + + self._result.settings = conf.get("settings", {}) diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml new file mode 100644 index 000000000..f650719a2 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/core.yml @@ -0,0 +1,81 @@ + +datamodels: + modules: + - path: "maro.simulator.scenarios.supply_chain.datamodels" + definitions: + StorageDataModel: + class: "StorageDataModel" + name_in_frame: "storage" + VehicleDataModel: + class: "VehicleDataModel" + name_in_frame: "vehicle" + DistributionDataModel: + class: "DistributionDataModel" + name_in_frame: "distribution" + ConsumerDataModel: + class: "ConsumerDataModel" + name_in_frame: "consumer" + SellerDataModel: + class: "SellerDataModel" + name_in_frame: "seller" + ManufactureDataModel: + class: "ManufactureDataModel" + name_in_frame: "manufacture" + ProductDataModel: + class: "ProductDataModel" + name_in_frame: "product" + FacilityDataModel: + class: "FacilityDataModel" + name_in_frame: "facility" + +units: + modules: + - path: "maro.simulator.scenarios.supply_chain.units" + definitions: + StorageUnit: + class: "StorageUnit" + datamodel: "StorageDataModel" + VehicleUnit: + class: "VehicleUnit" + datamodel: "VehicleDataModel" + DistributionUnit: + class: "DistributionUnit" + datamodel: "DistributionDataModel" + ConsumerUnit: + class: "ConsumerUnit" + datamodel: "ConsumerDataModel" + SellerUnit: + class: "SellerUnit" + datamodel: "SellerDataModel" + OuterSellerUnit: + class: "OuterSellerUnit" + datamodel: "SellerDataModel" + ManufactureUnit: + class: "ManufactureUnit" + datamodel: "ManufactureDataModel" + SimpleManufactureUnit: + class: "SimpleManufactureUnit" + datamodel: "ManufactureDataModel" + ProductUnit: + class: "ProductUnit" + datamodel: "ProductDataModel" + StoreProductUnit: + class: "StoreProductUnit" + datamodel: "ProductDataModel" + +facilities: + modules: + - path: "maro.simulator.scenarios.supply_chain.facilities" + definitions: + WarehouseFacility: + class: "WarehouseFacility" + datamodel: "FacilityDataModel" + SupplierFacility: + class: "SupplierFacility" + datamodel: "FacilityDataModel" + RetailerFacility: + class: "RetailerFacility" + datamodel: "FacilityDataModel" + OuterRetailerFacility: + class: "OuterRetailerFacility" + datamodel: "FacilityDataModel" diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml new file mode 100644 index 000000000..40c01fedb --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml @@ -0,0 +1,306 @@ + +# TODO: which config to inherit +# base: "" + +#core: +# datamodels: "xxx" +# units: "xxx" +# facilities: "xxx" + + +facility_definitions: + # facility definition + WarehouseFacility: &warehouse_facility + class: "WarehouseFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + # if true then will call generate function of class type + is_template: true + # config will be passed to generator as parameters + config: + agent_type: "product" + consumer: + class: "ConsumerUnit" + config: + agent_type: "consumer" + config: + # mark current unit/facility as a agent, out side may use this to choose policy. + agent_type: "facility" + + SupplierFacility: &supplier_facility + class: "SupplierFacility" + children: + storage: + class: "StorageUnit" + distribution: + class: "DistributionUnit" + products: + class: "ProductUnit" + is_template: true + config: + agent_type: "product" + consumer: + class: "ConsumerUnit" + config: + agent_type: "consumer" + manufacture: + class: "ManufactureUnit" + config: + agent_type: "producer" + config: + agent_type: "facility" + + RetailerFacility: &retailer_facility + class: "RetailerFacility" + children: + storage: + class: "StorageUnit" + products: + class: "StoreProductUnit" + is_template: true + config: + agent_type: "product" + consumer: + class: "ConsumerUnit" + config: + agent_type: "consumer" + seller: + class: "SellerUnit" + config: + sale_hist_len: 4 + config: + agent_type: "facility" + +# common entity/unit definition as reference to simplify the file. +normal_vehicle: &normal_vehicle + class: "VehicleUnit" + config: + patient: 100 + +# a normal distribution definition +normal_distribution: &normal_distribution + class: "DistributionUnit" + children: + vehicles: + - *normal_vehicle + - *normal_vehicle + config: + unit_price: 1 + +small_storage: &small_storage + # config of data model of this unit + config: + # other config or storage unit + capacity: 10000 + unit_storage_cost: 1 + +midium_storage: &midium_storage + config: + capacity: 20000 + unit_storage_cost: 1 + +huge_storage: &huge_storage + config: + capacity: 30000 + unit_storage_cost: 1 + +# sku list in this world +# this list do not contains price, cost or other facility related attributes, +# but just base info, like name, id, bom +skus: &sku_definitions + - id: 1 + name: "sku1" + output_units_per_lot: 12 + # bill of material that used produce current sku, empty means do not need source material + bom: + # key is the source sku name, value is quantity needed to use per time to produce current sku + sku3: 10 + + - id: 2 + name: "sku2" + output_units_per_lot: 1 + + - id: 3 + name: "sku3" + output_units_per_lot: 1 + + +# world definitions +world: + # here we use reference to make it each to edit. + skus: *sku_definitions + + # facilities in this world + facilities: + - name: "Supplier_001" # name of the facility + # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level + # use the facility definition as base, then we can override configs partially. + definition_ref: "SupplierFacility" + + # sku list of this facility + skus: + sku3: # sku name and attributes needed for this facility + init_stock: 100 + product_unit_cost: 1 + production_rate: 1 + type: "production" # production means this is the output production of this facility + cost: 10 + price: 10 + vlt: 1 + + # configuration of child units. + children: + # config of storage unit + storage: *small_storage + distribution: *normal_distribution + + # products use default config in core.yml + + # config of this facility + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Supplier_002" + definition_ref: "SupplierFacility" + + skus: + sku1: + init_stock: 100 + product_unit_cost: 1 + production_rate: 1 + type: "production" + cost: 10 + price: 100 + vlt: 1 + sku3: + init_stock: 100 + production_rate: 1 + type: "material" + cost: 10 + price: 100 + vlt: 1 + + children: + storage: *small_storage + distribution: *normal_distribution + + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Warehouse_001" + definition_ref: "WarehouseFacility" + + skus: + sku1: + init_stock: 1000 + price: 100 + vlt: 1 + sku2: + init_stock: 1000 + price: 100 + vlt: 1 + sku3: + init_stock: 1000 + price: 100 + vlt: 1 + + children: + storage: *huge_storage + distribution: *normal_distribution + config: + delay_order_penalty: 10 + order_cost: 0 + - name: "Retailer_001" + definition_ref: "RetailerFacility" + + skus: + sku1: + price: 300 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 # optional + vlt: 1 + sku3: + price: 200 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + vlt: 1 + sku2: + price: 100 + cost: 10 + init_stock: 100 + sale_gamma: 100 + backlog_ratio: 0.1 + vlt: 1 + + children: + storage: *midium_storage + + config: + order_cost: 0 + # topology used to specify the up/downstream for facilities + # we split it from facility, so that we can support configuration inherit to override it + # for a new topology + # TODO: change the name? + topology: + # key is current facility, value if upstream facilities that will provide a certain sku + Supplier_002: + # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", + # or any other facility in the list + sku3: + - "Supplier_001" + Warehouse_001: + sku1: + - "Supplier_002" + sku3: + - "Supplier_001" + Retailer_001: + sku1: + - "Supplier_002" + sku3: + - "Supplier_001" + + # map grid definitions + grid: + size: [20, 20] + + # facility position in grid + facilities: + Supplier_001: [0, 0] + Supplier_002: [3, 3] + Warehouse_001: [6, 6] + Retailer_001: [10, 18] + + # cells that un-traversable + blocks: + railroad: + - [10, 10] + - [10, 11] + - [10, 12] + - [11, 12] + +settings: + global_reward_weight_producer: 0.50 + global_reward_weight_consumer: 0.50 + downsampling_rate: 1 + episod_duration: 21 + initial_balance: 100000 + consumption_hist_len: 4 + sale_hist_len: 4 + pending_order_len: 4 + constraint_state_hist_len: 8 + total_echelons: 3 + replenishment_discount: 0.9 + reward_normalization: 1e7 + constraint_violate_reward: -1e6 + gamma: 0.99 + tail_timesteps: 7 + heading_timesteps: 7 diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py new file mode 100644 index 000000000..6fef50d02 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .consumer import ConsumerUnit +from .distribution import DistributionUnit +from .manufacture import ManufactureUnit +from .outerseller import DataFileDemandSampler, OuterSellerUnit, SellerDemandSampler +from .product import ProductUnit +from .seller import SellerUnit +from .simplemanufacture import SimpleManufactureUnit +from .extendunitbase import ExtendUnitBase +from .storage import StorageUnit +from .storeproduct import StoreProductUnit +from .unitbase import UnitBase +from .vehicle import VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py new file mode 100644 index 000000000..097d60c19 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -0,0 +1,174 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from collections import Counter, defaultdict + +from scipy.ndimage.interpolation import shift + +from .order import Order +from .extendunitbase import ExtendUnitBase + + +class ConsumerUnit(ExtendUnitBase): + """Consumer unit used to generate order to purchase from upstream by action.""" + + def __init__(self): + super(ConsumerUnit, self).__init__() + + self.open_orders = defaultdict(Counter) + + # States in python side. + self.received = 0 + self.purchased = 0 + self.sources = [] + self.pending_order_daily = None + self.order_product_cost = 0 + + def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): + """Called after order product is received. + + Args: + source_id (int): Where is the product from (facility id). + product_id (int): What product we received. + quantity (int): How many we received. + original_quantity (int): How many we ordered. + """ + self.received += quantity + + self.update_open_orders(source_id, product_id, -original_quantity) + + def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): + """Update the order states. + + Args: + source_id (int): Where is the product from (facility id). + product_id (int): What product in the order. + qty_delta (int): Number of product to update (sum). + """ + # New order for product. + self.open_orders[source_id][product_id] += qty_delta + + def initialize(self): + super(ConsumerUnit, self).initialize() + + self.pending_order_daily = [0] * self.world.configs.settings["pending_order_len"] + + sku = self.facility.skus[self.product_id] + + order_cost = self.facility.get_config("order_cost") + + self.data_model.initialize(sku.price, order_cost) + + if self.facility.upstreams is not None: + # Construct sources from facility's upstreams. + sources = self.facility.upstreams.get(self.product_id, None) + + if sources is not None: + # Is we are a supplier facility? + is_supplier = self.parent.manufacture is not None + + # Current sku information. + sku = self.world.get_sku_by_id(self.product_id) + + for source_facility in sources: + # We are a supplier unit, then the consumer is used to purchase source materials from upstreams. + # Try to find who will provide this kind of material. + if is_supplier: + if source_facility.products is not None: + for source_sku_id in sku.bom.keys(): + if source_sku_id in source_facility.products: + # This is a valid source facility. + self.sources.append(source_facility.id) + else: + # If we are not a manufacturing, just check if upstream have this sku configuration. + if sku.id in source_facility.skus: + self.sources.append(source_facility.id) + + def step(self, tick: int): + self._update_pending_order() + + # NOTE: id == 0 means invalid,as our id is 1 based. + if not self.action or self.action.quantity <= 0 or self.action.product_id <= 0 or self.action.source_id == 0: + return + + # NOTE: we are using product unit as destination, + # so we expect the action.source_id is and id of product unit + self.update_open_orders(self.action.source_id, self.action.product_id, self.action.quantity) + + order = Order(self.facility, self.action.product_id, self.action.quantity, self.action.vlt) + + source_facility = self.world.get_facility_by_id(self.action.source_id) + + self.order_product_cost = source_facility.distribution.place_order(order) + + self.purchased = self.action.quantity + + if order.vlt < len(self.pending_order_daily): + self.pending_order_daily[order.vlt - 1] += order.quantity + + def flush_states(self): + if self.received > 0: + self.data_model.received = self.received + self.data_model.total_received += self.received + + if self.purchased > 0: + self.data_model.purchased = self.purchased + self.data_model.total_purchased += self.purchased + self.data_model.latest_consumptions = 1.0 + + if self.order_product_cost > 0: + self.data_model.order_product_cost = self.order_product_cost + + if self.action is not None and self.action.quantity > 0: + self.data_model.order_quantity = self.action.quantity + self.data_model.reward_discount = self.action.reward_discount + + def post_step(self, tick: int): + # Clear the action states per step. + if self.action is not None: + self.data_model.latest_consumptions = 0 + self.data_model.reward_discount = 0 + + if self.action.quantity > 0: + self.data_model.order_quantity = 0 + + # This will set action to None. + super(ConsumerUnit, self).post_step(tick) + + if self.received > 0: + self.data_model.received = 0 + self.received = 0 + + if self.purchased > 0: + self.data_model.purchased = 0 + self.purchased = 0 + + if self.order_product_cost > 0: + self.data_model.order_product_cost = 0 + self.order_product_cost = 0 + + def reset(self): + super(ConsumerUnit, self).reset() + + self.pending_order_daily = [0] * self.world.configs.settings["pending_order_len"] + + self.open_orders.clear() + + def get_in_transit_quantity(self): + quantity = 0 + + for source_id, orders in self.open_orders.items(): + quantity += orders.get(self.product_id, 0) + + return quantity + + def _update_pending_order(self): + self.pending_order_daily = shift(self.pending_order_daily, -1, cval=0) + + def get_unit_info(self): + info = super().get_unit_info() + + info["sources"] = self.sources + + return info diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py new file mode 100644 index 000000000..5860e7a5a --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/distribution.py @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from collections import Counter, defaultdict, deque +from typing import Dict, List + +from .order import Order +from .unitbase import UnitBase +from .vehicle import VehicleUnit + + +class DistributionUnit(UnitBase): + """Unit that used to receive and execute orders from downstream facilities. + + One distribution can accept all kind of sku order. + """ + # Vehicle unit list of this distribution unit. + vehicles: List[VehicleUnit] = None + + def __init__(self): + super().__init__() + self.order_queue = deque() + + self.transportation_cost = Counter() + self.delay_order_penalty = Counter() + self.check_in_order = Counter() + + self.base_delay_order_penalty = 0 + + self._is_order_changed = False + + def get_pending_order(self) -> Dict[int, int]: + """Get orders that states is pending. + + Returns: + dict: Dictionary of order that key is product id, value is quantity. + """ + counter = defaultdict(int) + + for order in self.order_queue: + counter[order.product_id] += order.quantity + + for vehicle in self.vehicles: + if vehicle.is_enroute(): + counter[vehicle.product_id] += (vehicle.requested_quantity - vehicle.payload) + + return counter + + def place_order(self, order: Order) -> int: + """Place an order in the pending queue. + + Args: + order (Order): Order to insert. + + Returns: + int: Total price of this order. + """ + if order.quantity > 0: + sku = self.facility.skus[order.product_id] + + if sku is not None: + self._is_order_changed = True + + self.order_queue.append(order) + + order_total_price = sku.price * order.quantity + + self.check_in_order[order.product_id] += order.quantity + + return order_total_price + + return 0 + + def initialize(self): + super(DistributionUnit, self).initialize() + + self.base_delay_order_penalty = self.facility.get_config("delay_order_penalty", 0) + + def step(self, tick: int): + for vehicle in self.vehicles: + # If we have vehicle not on the way and there is any pending order. + if len(self.order_queue) > 0 and vehicle.requested_quantity == 0: + order = self.order_queue.popleft() + + # Schedule a job for available vehicle. + # TODO: why vlt is determined by order? + vehicle.schedule( + order.destination, + order.product_id, + order.quantity, + order.vlt + ) + + self._is_order_changed = True + + # Push vehicle. + vehicle.step(tick) + + self.transportation_cost[vehicle.product_id] += abs(vehicle.cost) + + # Update order's delay penalty per tick. + for order in self.order_queue: + self.delay_order_penalty[order.product_id] += self.base_delay_order_penalty + + def flush_states(self): + super(DistributionUnit, self).flush_states() + + for vehicle in self.vehicles: + vehicle.flush_states() + + if self._is_order_changed: + self._is_order_changed = False + + self.data_model.remaining_order_quantity = sum(order.quantity for order in self.order_queue) + self.data_model.remaining_order_number = len(self.order_queue) + + def reset(self): + super(DistributionUnit, self).reset() + + self.order_queue.clear() + self.transportation_cost.clear() + self.check_in_order.clear() + self.delay_order_penalty.clear() + + # Reset vehicles. + for vehicle in self.vehicles: + vehicle.reset() diff --git a/maro/simulator/scenarios/supply_chain/units/extendunitbase.py b/maro/simulator/scenarios/supply_chain/units/extendunitbase.py new file mode 100644 index 000000000..a8106012d --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/extendunitbase.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .unitbase import UnitBase + + +class ExtendUnitBase(UnitBase): + """A base of sku related unit.""" + + # Product id (sku id), 0 means invalid. + product_id: int = 0 + + def initialize(self): + super(ExtendUnitBase, self).initialize() + + if self.data_model is not None: + self.data_model.set_product_id(self.product_id, self.parent.id) + + def get_unit_info(self) -> dict: + info = super(ExtendUnitBase, self).get_unit_info() + + info["sku_id"] = self.product_id + + return info diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py new file mode 100644 index 000000000..e54da60fa --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .extendunitbase import ExtendUnitBase + + +class ManufactureUnit(ExtendUnitBase): + """Unit that used to produce certain product(sku) with consume specified source skus. + + One manufacture unit per sku. + """ + + # Source material sku and related number per produce cycle. + bom: dict = None + + # How many production unit each produce cycle. + output_units_per_lot: int = None + + # How many unit we will consume each produce cycle. + input_units_per_lot: int = 0 + + # How many we procedure per current step. + manufacture_number: int = 0 + + def initialize(self): + super(ManufactureUnit, self).initialize() + + facility_sku_info = self.facility.skus[self.product_id] + product_unit_cost = facility_sku_info.product_unit_cost + + self.data_model.initialize(product_unit_cost) + + global_sku_info = self.world.get_sku_by_id(self.product_id) + + self.bom = global_sku_info.bom + self.output_units_per_lot = global_sku_info.output_units_per_lot + + if len(self.bom) > 0: + self.input_units_per_lot = sum(self.bom.values()) + + def step(self, tick: int): + # Try to produce production if we have positive rate. + if self.action is not None and self.action.production_rate > 0: + sku_num = len(self.facility.skus) + unit_num_upper_bound = self.facility.storage.capacity // sku_num + + # Compare with avg storage number. + current_product_number = self.facility.storage.get_product_number(self.product_id) + max_number_to_procedure = min( + unit_num_upper_bound - current_product_number, + self.action.production_rate * self.output_units_per_lot, + self.facility.storage.remaining_space + ) + + if max_number_to_procedure > 0: + space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot + + # Consider about the volume, we can produce all if space take per cycle <=1. + if space_taken_per_cycle > 1: + max_number_to_procedure = max_number_to_procedure // space_taken_per_cycle + + source_sku_to_take = {} + # Do we have enough source material? + for source_sku_id, source_sku_cost_number in self.bom.items(): + source_sku_available_number = self.facility.storage.get_product_number(source_sku_id) + + max_number_to_procedure = min( + source_sku_available_number // source_sku_cost_number, + max_number_to_procedure + ) + + if max_number_to_procedure <= 0: + break + + source_sku_to_take[source_sku_id] = max_number_to_procedure * source_sku_cost_number + + if max_number_to_procedure > 0: + self.manufacture_number = max_number_to_procedure + self.facility.storage.try_take_products(source_sku_to_take) + self.facility.storage.try_add_products({self.product_id: self.manufacture_number}) + else: + self.manufacture_number = 0 + + def flush_states(self): + if self.manufacture_number > 0: + self.data_model.manufacturing_number = self.manufacture_number + + def post_step(self, tick: int): + if self.manufacture_number > 0: + self.data_model.manufacturing_number = 0 + self.manufacture_number = 0 + + # NOTE: call super at last, since it will clear the action. + super(ManufactureUnit, self).post_step(tick) diff --git a/maro/simulator/scenarios/supply_chain/units/order.py b/maro/simulator/scenarios/supply_chain/units/order.py new file mode 100644 index 000000000..47a8335d0 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/order.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from collections import namedtuple + +Order = namedtuple("Order", ("destination", "product_id", "quantity", "vlt")) diff --git a/maro/simulator/scenarios/supply_chain/units/outerseller.py b/maro/simulator/scenarios/supply_chain/units/outerseller.py new file mode 100644 index 000000000..c7eb0b3a1 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/outerseller.py @@ -0,0 +1,118 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import warnings +from abc import ABC, abstractmethod +from collections import namedtuple +from csv import DictReader + +from dateutil import parser + +from .seller import SellerUnit + + +class SellerDemandSampler(ABC): + """Base class of seller unit demand sampler, you can inherit from this to read from file or predict from a model. + + Args: + configs (dict): Configuration from retailer facility, contains keys for a special sampler. + world (World): Current world this retail belongs to. + """ + + def __init__(self, configs: dict, world): + self._configs = configs + self._world = world + + @abstractmethod + def sample_demand(self, product_id: int, tick: int) -> int: + """Sample the demand for specified product and tick. + + Args: + product_id (int): Id of product to sample. + tick (int): Tick of environment, NOTE: this tick is start from 0, + you may need to transform it to your time system. + """ + pass + + +class DataFileDemandSampler(SellerDemandSampler): + """Sampler to read sample demand from data files, one store one file. + + NOTE: + This sampler need to configure the start time that to be treated as tick 0 in world.settings, or + it will use first row as start time. + + Args: + configs (dict): Configuration from retail facility, it should contains following keys. + . "file_path", the path to the data file + . "sku_column", column name contains sku name, this must be match with current seller, or will be ignored. + . "price_column", column name that will be treated as price. + . "sale_column", column name that will be treated as sale number (demand). + . "datetime_column", column name that contains datetime, NOTE: we will parse it that ignore the time zone. + """ + + SkuRow = namedtuple("SkuRow", ("price", "sales")) + + def __init__(self, configs: dict, world): + super(DataFileDemandSampler, self).__init__(configs, world) + + self._file_path = configs["file_path"] + + # If start date time is None, then will use first row as start date time (tick 0). + self._start_date_time = self._world.configs.settings["start_date_time"] + + if self._start_date_time is not None: + self._start_date_time = parser.parse(self._start_date_time, ignoretz=True) + + self._sku_column_name = configs.get("sku_column", "SKU") + self._price_column_name = configs.get("price_column", "Price") + self._sale_column_name = configs.get("sale_column", "Sales") + self._datetime_column_name = configs.get("datetime_column", "DT") + + # Tick -> sku -> (sale, price). + self._cache = {} + + self._cache_data() + + def sample_demand(self, product_id: int, tick: int) -> int: + if tick not in self._cache or product_id not in self._cache[tick]: + return 0 + + return self._cache[tick][product_id].sales + + def _cache_data(self): + with open(self._file_path, "rt") as fp: + reader = DictReader(fp) + + for row in reader: + sku_name = row[self._sku_column_name] + + sales = int(row[self._sale_column_name]) + price = float(row[self._price_column_name]) + date = parser.parse(row[self._datetime_column_name], ignoretz=True) + + if self._start_date_time is None: + self._start_date_time = date + + # So one day one tick. + target_tick = (date - self._start_date_time).days + + if target_tick not in self._cache: + self._cache[target_tick] = {} + + sku = self._world.get_sku_by_name(sku_name) + + if sku is not None: + self._cache[target_tick][sku.id] = DataFileDemandSampler.SkuRow(price, sales) + else: + warnings.warn(f"{sku_name} not configured in config file.") + + +class OuterSellerUnit(SellerUnit): + """Seller that demand is from out side sampler, like a data file or data model prediction.""" + + # Sample used to sample demand. + sampler: SellerDemandSampler = None + + def market_demand(self, tick: int) -> int: + return self.sampler.sample_demand(self.product_id, tick) diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py new file mode 100644 index 000000000..917e99275 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -0,0 +1,213 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import numpy as np + +from .consumer import ConsumerUnit +from .distribution import DistributionUnit +from .manufacture import ManufactureUnit +from .seller import SellerUnit +from .extendunitbase import ExtendUnitBase +from .storage import StorageUnit + + +class ProductUnit(ExtendUnitBase): + """Unit that used to group units of one special sku, usually contains consumer, seller and manufacture.""" + + # Consumer unit of current sku. + consumer: ConsumerUnit = None + + # Seller unit of current sku. + seller: SellerUnit = None + + # Manufacture unit of this sku. + manufacture: ManufactureUnit = None + + # Storage of this facility, always a reference of facility.storage. + storage: StorageUnit = None + + # Reference to facility's distribution unit. + distribution: DistributionUnit = None + + # Internal states to track distribution. + _checkin_order = 0 + _transport_cost = 0 + _delay_order_penalty = 0 + + def initialize(self): + super().initialize() + + facility_sku = self.facility.skus[self.product_id] + + self.data_model.initialize(facility_sku.price) + + def step(self, tick: int): + for unit in self.children: + unit.step(tick) + + def flush_states(self): + for unit in self.children: + unit.flush_states() + + if self.distribution is not None: + self._checkin_order = self.distribution.check_in_order[self.product_id] + self._transport_cost = self.distribution.transportation_cost[self.product_id] + self._delay_order_penalty = self.distribution.delay_order_penalty[self.product_id] + + self.distribution.check_in_order[self.product_id] = 0 + self.distribution.transportation_cost[self.product_id] = 0 + self.distribution.delay_order_penalty[self.product_id] = 0 + + if self._checkin_order > 0: + self.data_model.distribution_check_order = self._checkin_order + + if self._transport_cost > 0: + self.data_model.distribution_transport_cost = self._transport_cost + + if self._delay_order_penalty > 0: + self.data_model.distribution_delay_order_penalty = self._delay_order_penalty + + def post_step(self, tick: int): + super().post_step(tick) + + for unit in self.children: + unit.post_step(tick) + + if self._checkin_order > 0: + self.data_model.distribution_check_order = 0 + self._checkin_order = 0 + + if self._transport_cost > 0: + self.data_model.distribution_transport_cost = 0 + self._transport_cost = 0 + + if self._delay_order_penalty > 0: + self.data_model.distribution_delay_order_penalty = 0 + self._delay_order_penalty = 0 + + def reset(self): + super().reset() + + for unit in self.children: + unit.reset() + + def get_unit_info(self) -> dict: + return { + "id": self.id, + "sku_id": self.product_id, + "max_vlt": self._get_max_vlt(), + "node_name": type(self.data_model).__node_name__ if self.data_model is not None else None, + "node_index": self.data_model_index if self.data_model is not None else None, + "class": type(self), + "config": self.config, + "consumer": self.consumer.get_unit_info() if self.consumer is not None else None, + "seller": self.seller.get_unit_info() if self.seller is not None else None, + "manufacture": self.manufacture.get_unit_info() if self.manufacture is not None else None + } + + # TODO: add following field into states. + def get_latest_sale(self): + sale = 0 + downstreams = self.facility.downstreams.get(self.product_id, []) + + for facility in downstreams: + sale += facility.products[self.product_id].get_latest_sale() + + return sale + + def get_sale_mean(self): + sale_mean = 0 + downstreams = self.facility.downstreams.get(self.product_id, []) + + for facility in downstreams: + sale_mean += facility.products[self.product_id].get_sale_mean() + + return sale_mean + + def get_sale_std(self): + sale_std = 0 + + downstreams = self.facility.downstreams.get(self.product_id, []) + + for facility in downstreams: + sale_std += facility.products[self.product_id].get_sale_std() + + return sale_std / np.sqrt(max(1, len(downstreams))) + + def get_selling_price(self): + price = 0.0 + downstreams = self.facility.downstreams.get(self.product_id, []) + + for facility in downstreams: + price = max(price, facility.products[self.product_id].get_selling_price()) + + return price + + def _get_max_vlt(self): + vlt = 1 + + if self.consumer is not None: + for source_facility_id in self.consumer.sources: + source_facility = self.world.get_facility_by_id(source_facility_id) + + source_vlt = source_facility.skus[self.product_id].vlt + + vlt = max(vlt, source_vlt) + + return vlt + + @staticmethod + def generate(facility, config: dict, unit_def: object): + """Generate product unit by sku information. + + Args: + facility (FacilityBase): Facility this product belongs to. + config (dict): Config of children unit. + unit_def (object): Definition of the unit (from config). + + Returns: + dict: Dictionary of product unit, key is the product id, value if ProductUnit. + """ + products_dict = {} + + if facility.skus is not None and len(facility.skus) > 0: + world = facility.world + + for sku_id, sku in facility.skus.items(): + sku_type = sku.type + + product_unit: ProductUnit = world.build_unit_by_type(unit_def, facility, facility) + product_unit.product_id = sku_id + product_unit.children = [] + product_unit.parse_configs(config) + product_unit.storage = product_unit.facility.storage + product_unit.distribution = product_unit.facility.distribution + + # NOTE: BE CAREFUL about the order, product unit will use this order update children, + # the order may affect the states. + # Here we make sure consumer is the first one, so it can place order first. + for child_name in ("consumer", "seller", "manufacture"): + conf = config.get(child_name, None) + + if conf is not None: + # Ignore manufacture unit if it is not for a production, even it is configured in config. + if sku_type != "production" and child_name == "manufacture": + continue + + # We produce the product, so we do not need to purchase it. + if sku_type == "production" and child_name == "consumer": + continue + + child_unit = world.build_unit(facility, product_unit, conf) + child_unit.product_id = sku_id + + setattr(product_unit, child_name, child_unit) + + # Parse config for unit. + child_unit.parse_configs(conf.get("config", {})) + + product_unit.children.append(child_unit) + + products_dict[sku_id] = product_unit + + return products_dict diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py new file mode 100644 index 000000000..3db33b376 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -0,0 +1,92 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +import numpy as np + +from .extendunitbase import ExtendUnitBase + + +class SellerUnit(ExtendUnitBase): + """ + Unit that used to generate product consume demand, and move demand product from current storage. + """ + + def __init__(self): + super(SellerUnit, self).__init__() + + self.gamma = 0 + + # Attribute cache. + self.sold = 0 + self.demand = 0 + self.total_sold = 0 + self.total_demand = 0 + self.price = 0 + + self.sale_hist = [] + + def market_demand(self, tick: int) -> int: + """Generate market demand for current tick. + + Args: + tick (int): Current simulator tick. + + Returns: + int: Demand number. + """ + return int(np.random.gamma(self.gamma)) + + def initialize(self): + super(SellerUnit, self).initialize() + + sku = self.facility.skus[self.product_id] + + self.gamma = sku.sale_gamma + + self.data_model.initialize(sku.price, sku.backlog_ratio) + + self.sale_hist = [self.gamma] * self.config["sale_hist_len"] + + def step(self, tick: int): + demand = self.market_demand(tick) + + # What seller does is just count down the product number. + sold_qty = self.facility.storage.take_available(self.product_id, demand) + + self.total_sold += sold_qty + self.sold = sold_qty + self.demand = demand + self.total_demand += demand + + self.sale_hist.append(demand) + self.sale_hist = self.sale_hist[1:] + + def flush_states(self): + if self.sold > 0: + self.data_model.sold = self.sold + self.data_model.total_sold = self.total_sold + + if self.demand > 0: + self.data_model.demand = self.demand + self.data_model.total_demand = self.total_demand + + def post_step(self, tick: int): + super(SellerUnit, self).post_step(tick) + + if self.sold > 0: + self.data_model.sold = 0 + self.sold = 0 + + if self.demand > 0: + self.data_model.demand = 0 + self.demand = 0 + + def reset(self): + super(SellerUnit, self).reset() + + def sale_mean(self): + return np.mean(self.sale_hist) + + def sale_std(self): + return np.std(self.sale_hist) diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py new file mode 100644 index 000000000..c4a2c81af --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .manufacture import ManufactureUnit + + +class SimpleManufactureUnit(ManufactureUnit): + """This simple manufacture unit will ignore source sku, just generate specified number of product.""" + + def step(self, tick: int): + # Try to produce production if we have positive rate. + self.manufacture_number = 0 + + if self.action is not None and self.action.production_rate > 0: + production_rate = self.action.production_rate + + sku_num = len(self.facility.skus) + unit_num_upper_bound = self.facility.storage.capacity // sku_num + current_product_number = self.facility.storage.get_product_number(self.product_id) + self.manufacture_number = max(0, min(unit_num_upper_bound - current_product_number, production_rate)) + + if self.manufacture_number > 0: + self.facility.storage.try_add_products({self.product_id: self.manufacture_number}) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py new file mode 100644 index 000000000..b0065a9a7 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -0,0 +1,158 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import random +from typing import Dict + +from .unitbase import UnitBase + + +class StorageUnit(UnitBase): + """Unit that used to store skus.""" + + def __init__(self): + super().__init__() + + # Used to map from product id to slot index. + self.capacity = 0 + self.remaining_space = 0 + self.product_level = {} + + # Which product's number has changed. + self._changed_product_cache = {} + + def try_add_products(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: + """Try to add products into storage. + + Args: + product_quantities (Dict[int, int]): Dictionary of product id and quantity need to add to storage. + all_or_nothing (bool): Failed if all product cannot be added, or add as many as it can. Default is True. + + Returns: + dict: Dictionary of product id and quantity success added. + """ + if all_or_nothing and self.remaining_space < sum(product_quantities.values()): + return {} + + unloaded_quantities = {} + + for product_id, quantity in product_quantities.items(): + unload_quantity = min(self.remaining_space, quantity) + + self.product_level[product_id] += unload_quantity + unloaded_quantities[product_id] = unload_quantity + + self._changed_product_cache[product_id] = True + + self.remaining_space -= unload_quantity + + return unloaded_quantities + + def try_take_products(self, product_quantities: Dict[int, int]) -> bool: + """Try to take specified number of product. + + Args: + product_quantities (Dict[int, int]): Dictionary of product id and quantity to take from storage. + + Returns: + bool: Is success to take? + """ + # Check if we can take all kinds of products? + for product_id, quantity in product_quantities.items(): + if self.product_level[product_id] < quantity: + return False + + # Take from storage. + for product_id, quantity in product_quantities.items(): + self.product_level[product_id] -= quantity + self._changed_product_cache[product_id] = True + + self.remaining_space += quantity + + return True + + def take_available(self, product_id: int, quantity: int) -> int: + """Take as much as available specified product from storage. + + Args: + product_id (int): Product to take. + quantity (int): Max quantity to take. + + Returns: + int: Actual quantity taken. + """ + available = self.product_level[product_id] + actual = min(available, quantity) + + self.product_level[product_id] -= actual + self._changed_product_cache[product_id] = True + + self.remaining_space += actual + + return actual + + def get_product_number(self, product_id: int) -> int: + """Get product number in storage. + + Args: + product_id (int): Product to check. + + Returns: + int: Available number of product. + """ + return self.product_level[product_id] + + def initialize(self): + super(StorageUnit, self).initialize() + + self.capacity = self.config.get("capacity", 100) + self.remaining_space = self.capacity + + for sku in self.facility.skus.values(): + self.product_level[sku.id] = sku.init_stock + self._changed_product_cache[sku.id] = False + + self.remaining_space -= sku.init_stock + + self.data_model.initialize( + capacity=self.capacity, + remaining_space=self.remaining_space, + product_list=[id for id in self.product_level.keys()], + product_number=[n for n in self.product_level.values()] + ) + + def flush_states(self): + # Write the changes to frame. + i = 0 + has_changes = False + for product_id, product_number in self.product_level.items(): + if self._changed_product_cache[product_id]: + has_changes = True + self._changed_product_cache[product_id] = False + + self.data_model.product_number[i] = product_number + i += 1 + + if has_changes: + self.data_model.remaining_space = self.remaining_space + + def reset(self): + super(StorageUnit, self).reset() + + self.remaining_space = self.capacity + + for sku in self.facility.skus.values(): + # self.product_level[sku.id] = sku.init_stock + # self.remaining_space -= sku.init_stock + # if hasattr(sku, "sale_gamma") and (sku.sale_gamma is not None): + # init_stock = random.randint(0, 5) * sku.sale_gamma + # sku.init_stock = init_stock + self.product_level[sku.id] = sku.init_stock + self.remaining_space -= sku.init_stock + + def get_unit_info(self): + info = super().get_unit_info() + + info["product_list"] = [i for i in self.product_level.keys()] + + return info diff --git a/maro/simulator/scenarios/supply_chain/units/storeproduct.py b/maro/simulator/scenarios/supply_chain/units/storeproduct.py new file mode 100644 index 000000000..7ce9a5396 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/storeproduct.py @@ -0,0 +1,13 @@ + +from .product import ProductUnit + + +class StoreProductUnit(ProductUnit): + def get_sale_mean(self): + return self.seller.sale_mean() + + def get_sale_std(self): + return self.seller.sale_std() + + def get_selling_price(self): + return self.facility.skus[self.product_id].price diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py new file mode 100644 index 000000000..6d239e52d --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -0,0 +1,116 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +class UnitBase: + """Base of all unit used to contain related logic. + + Typically one unit instance should bind to a data model instance, + that used to update states in frame. + + An unit will have following steps to initializing. + . Create instance with default constructor without parameter, means all unit constructor should not have parameter + or with default value. + . Unit.parse_configs is called with configurations from parent or config file. + . After frame is constructed, Unit.initialize is called to do data model related initializing. + . At the beginning of business_engine.step, Unit.step is called to go through related logic, + then after all the agent completed, Unit.flush_state will be called at the end of business_engine.step, + to tell agents to save their states. + . Unit.post_step is called in business_engine.post_step after step is finished. + . Unit.set_action is called when there is any action from out-side. + + """ + # Id of this unit. + id: int = 0 + + # Which this unit belongs to. + facility = None + + # Which world this unit belongs to. + world = None + + # Parent of this unit, it can be a facility or another unit. + parent: object = None + + # Child units, extended unit can add their own child as property, this is used as a collection. + children: list = None + + # Data model name in the frame, used to query binding data model instance. + data_model_name: str = None + + # Data model instance index in the frame, used to query binding data model instance. + data_model_index: int = None + + # Real data model binding with this unit. + data_model = None + + # Current action. + action: object = None + + # Current unit configurations. + config: dict = None + + def __init__(self): + pass + + def parse_configs(self, config: dict): + """Parse configurations from config. + + Args: + config (dict): Configuration from parent or config file. + """ + self.config = config + + def step(self, tick: int): + """Run related logic for current tick. + + Args: + tick (int): Current simulator tick. + """ + pass + + def flush_states(self): + """Flush states into frame for current tick. + """ + pass + + def post_step(self, tick: int): + """Post-processing for current step. + + Args: + tick (int): Current simulator tick. + """ + self.action = None + + def reset(self): + """Reset this unit for a new episode.""" + if self.data_model is not None: + self.data_model.reset() + + self.action = None + + def initialize(self): + """Initialize this unit after data model is ready to use. + + NOTE: unit.data_model is available from this step. + """ + if self.data_model is not None: + self.data_model.set_id(self.id, self.facility.id) + + def set_action(self, action: object): + """Set action for this agent. + + Args: + action (object): Action from outside. + """ + self.action = action + + def get_unit_info(self) -> dict: + return { + "id": self.id, + "node_name": type(self.data_model).__node_name__, + "node_index": self.data_model_index, + "class": type(self), + "config": self.config, + "children": None if self.children is None else [c.get_unit_info() for c in self.children] + } diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py new file mode 100644 index 000000000..c0c126fb2 --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -0,0 +1,195 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from .unitbase import UnitBase + + +class VehicleUnit(UnitBase): + """Unit used to move production from source to destination by order.""" + + def __init__(self): + super().__init__() + # Max patient of current vehicle. + self.max_patient: int = None + + # Current products' destination. + self.destination = None + + # Path to destination. + self.path: list = None + + # Product to load + self.product_id = 0 + + # Steps to arrive destination. + self.steps = 0 + + # Payload on vehicle. + self.payload = 0 + + # Which product unit current product related to. + self.product = None + + # Current location in the path. + self.location = 0 + + # Velocity. + self.velocity = 0 + self.requested_quantity = 0 + self.patient = 0 + self.cost = 0 + self.unit_transport_cost = 0 + + def schedule(self, destination: object, product_id: int, quantity: int, vlt: int): + """Schedule a job for this vehicle. + + Args: + destination (FacilityBase): Destination facility. + product_id (int): What load from storage. + quantity (int): How many to load. + vlt (int): Velocity of vehicle. + """ + self.product_id = product_id + self.destination = destination + self.requested_quantity = quantity + + # Find the path from current entity to target. + self.path = self.world.find_path( + self.facility.x, + self.facility.y, + destination.x, + destination.y + ) + + if self.path is None: + raise Exception(f"Destination {destination} is unreachable") + + # Steps to destination. + # self.steps = len(self.path) // vlt + self.steps = vlt + + # We are waiting for product loading. + self.location = 0 + + self.velocity = vlt + + self.patient = self.max_patient + + def try_load(self, quantity: int) -> bool: + """Try to load specified number of scheduled product. + + Args: + quantity (int): Number to load. + """ + if self.facility.storage.try_take_products({self.product_id: quantity}): + self.payload = quantity + + # Write to frame, as we do not need to update it per tick. + self.data_model.payload = quantity + + return True + + return False + + def try_unload(self): + """Try unload products into destination's storage.""" + unloaded = self.destination.storage.try_add_products( + {self.product_id: self.payload}, + all_or_nothing=False + ) + + # Update order if we unloaded any. + if len(unloaded) > 0: + unloaded_units = sum(unloaded.values()) + + self.destination.products[self.product_id].consumer.on_order_reception( + self.facility.id, + self.product_id, + unloaded_units, + self.payload + ) + + self.payload -= unloaded_units + self.data_model.payload = self.payload + + def is_enroute(self): + return self.destination is not None + + def initialize(self): + super(VehicleUnit, self).initialize() + + patient = self.config.get("patient", 100) + self.unit_transport_cost = self.config.get("unit_transport_cost", 1) + + self.data_model.initialize(unit_transport_cost=self.unit_transport_cost) + + self.max_patient = patient + + def step(self, tick: int): + # If we have not arrive at destination yet. + if self.steps > 0: + # if we still not loaded enough productions yet. + if self.location == 0 and self.payload == 0: + # then try to load by requested. + + if self.try_load(self.requested_quantity): + # NOTE: here we return to simulate loading + return + else: + self.patient -= 1 + + # Failed to load, check the patient. + if self.patient < 0: + self.destination.products[self.product_id].consumer.update_open_orders( + self.facility.id, + self.product_id, + -self.requested_quantity + ) + + self._reset_internal_states() + self._reset_data_model() + + # Moving to destination + if self.payload > 0: + # Closer to destination until 0. + + self.location += self.velocity + self.steps -= 1 + + if self.location >= len(self.path): + self.location = len(self.path) - 1 + else: + # Avoid update under idle state. + # if self.location > 0: + # Try to unload./////////////////////////////////////////////////////////////////// + if self.payload > 0: + self.try_unload() + + # Back to source if we unload all. + # if self.payload == 0: + self._reset_internal_states() + self._reset_data_model() + + self.cost = self.payload * self.unit_transport_cost + + def reset(self): + super(VehicleUnit, self).reset() + + self._reset_internal_states() + self._reset_data_model() + + def _reset_internal_states(self): + self.destination = None + self.path = None + self.payload = 0 + self.product_id = 0 + self.steps = 0 + self.location = 0 + self.requested_quantity = 0 + self.velocity = 0 + self.patient = self.max_patient + + def _reset_data_model(self): + # Reset data model. + self.data_model.payload = 0 diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py new file mode 100644 index 000000000..8bfe0043b --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -0,0 +1,481 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +from collections import namedtuple +from typing import List, Tuple, Union + +import networkx as nx + +from maro.backends.frame import FrameBase + +from .easy_config import EasyConfig, SkuInfo +from .facilities import FacilityBase +from .frame_builder import build_frame +from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef +from .units import ExtendUnitBase, UnitBase + +AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id", "parent_id")) + + +class World: + """Supply chain world contains facilities and grid base map.""" + + def __init__(self): + # Frame for current world configuration. + self.frame: FrameBase = None + + # Current configuration. + self.configs: SupplyChainConfiguration = None + + # Durations of current simulation. + self.durations = 0 + + # All the entities in the world. + self.units = {} + + # All the facilities in this world. + self.facilities = {} + + # Entity id counter, every unit and facility have unique id. + self._id_counter = 1 + + # Grid of the world + self._graph: nx.Graph = None + + # Sku id to name mapping, used for querying. + self._sku_id2name_mapping = {} + + # All the sku in this world. + self._sku_collection = {} + + # Facility name to id mapping, used for querying. + self._facility_name2id_mapping = {} + + # Data model class collection, used to collection data model class and their number in frame. + self._data_class_collection = {} + + self.agent_list = [] + + self.agent_type_dict = {} + + self.max_sources_per_facility = 0 + self.max_price = 0 + + def get_sku_by_name(self, name: str) -> EasyConfig: + """Get sku information by name. + + Args: + name (str): Sku name to query. + + Returns: + EasyConfig: General information for sku, used as a dict, but support use key as property. + """ + return self._sku_collection.get(name, None) + + def get_sku_by_id(self, sku_id: int) -> EasyConfig: + """Get sku information by sku id. + + Args: + sku_id (int): Id of sku to query. + + Returns: + SkuInfo: General information for sku. + """ + return self._sku_collection[self._sku_id2name_mapping[sku_id]] + + def get_facility_by_id(self, facility_id: int) -> FacilityBase: + """Get facility by id. + + Args: + facility_id (int): Facility id to query. + + Returns: + FacilityBase: Facility instance. + """ + return self.facilities[facility_id] + + def get_facility_by_name(self, name: str): + """Get facility by name. + + Args: + name (str): Facility name to query. + + Returns: + FacilityBase: Facility instance. + """ + return self.facilities[self._facility_name2id_mapping[name]] + + def get_entity(self, id: int) -> Union[FacilityBase, UnitBase]: + """Get an entity(Unit or Facility) by id. + + Args: + id (int): Id to query. + + Returns: + Union[FacilityBase, UnitBase]: Unit or facility instance. + """ + if id in self.units: + return self.units[id] + else: + return self.facilities[id] + + def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: + """Find path to specified cell. + + Args: + start_x (int): Start cell position x. + start_y (int): Start cell position y. + goal_x (int): Destination cell position x. + goal_y (int): Destination cell position y. + + Returns: + List[Tuple[int, int]]: List of (x, y) position to target. + """ + return nx.astar_path(self._graph, source=(start_x, start_y), target=(goal_x, goal_y), weight="cost") + + def build(self, configs: SupplyChainConfiguration, snapshot_number: int, durations: int): + """Build world with configurations. + + Args: + configs (SupplyChainConfiguration): Configuration of current world. + snapshot_number (int): Number of snapshots to keep in memory. + durations (int): Durations of current simulation. + """ + self.durations = durations + self.configs = configs + + world_config = configs.world + + # Grab sku information for this world. + for sku_conf in world_config["skus"]: + sku = SkuInfo(sku_conf) + + self._sku_id2name_mapping[sku.id] = sku.name + self._sku_collection[sku.name] = sku + + # Collect bom info. + for sku_conf in world_config["skus"]: + sku = self._sku_collection[sku_conf["name"]] + sku.bom = {} + + bom = sku_conf.get("bom", {}) + + for material_sku_name, units_per_lot in bom.items(): + sku.bom[self._sku_collection[material_sku_name].id] = units_per_lot + + # Construct facilities. + for facility_conf in world_config["facilities"]: + facility_class_alias = facility_conf["class"] + facility_def: FacilityDef = self.configs.facilities[facility_class_alias] + facility_class_type = facility_def.class_type + + # Instance of facility. + facility = facility_class_type() + + # Normal properties. + facility.id = self._gen_id() + facility.name = facility_conf["name"] + facility.world = self + + # Parse sku info. + facility.parse_skus(facility_conf["skus"]) + + # Parse config for facility. + facility.parse_configs(facility_conf.get("config", {})) + + # Due with data model. + data_model_def: DataModelDef = self.configs.data_models[facility_def.data_model_alias] + + # Register the data model, so that it will help to generate related instance index. + facility.data_model_index = self._register_data_model(data_model_def.alias) + facility.data_model_name = data_model_def.name_in_frame + + # Build children (units). + for child_name, child_conf in facility_conf["children"].items(): + setattr(facility, child_name, self.build_unit(facility, facility, child_conf)) + + self.facilities[facility.id] = facility + + self._facility_name2id_mapping[facility.name] = facility.id + + # Build frame. + self.frame = self._build_frame(snapshot_number) + + # Assign data model instance. + for unit in self.units.values(): + if unit.data_model_name is not None: + unit.data_model = getattr(self.frame, unit.data_model_name)[unit.data_model_index] + + for facility in self.facilities.values(): + if facility.data_model_name is not None: + facility.data_model = getattr(self.frame, facility.data_model_name)[facility.data_model_index] + + # Construct the upstream topology. + topology = world_config["topology"] + + for cur_facility_name, topology_conf in topology.items(): + facility = self.get_facility_by_name(cur_facility_name) + + for sku_name, source_facilities in topology_conf.items(): + sku = self.get_sku_by_name(sku_name) + facility.upstreams[sku.id] = [] + + self.max_sources_per_facility = max(self.max_sources_per_facility, len(source_facilities)) + + for source_name in source_facilities: + source_facility = self.get_facility_by_name(source_name) + + facility.upstreams[sku.id].append(source_facility) + + if sku.id not in source_facility.downstreams: + source_facility.downstreams[sku.id] = [] + + source_facility.downstreams[sku.id].append(facility) + + # Call initialize method for facilities. + for facility in self.facilities.values(): + facility.initialize() + + # Call initialize method for units. + for unit in self.units.values(): + unit.initialize() + + # Construct the map grid. + grid_config = world_config["grid"] + + grid_width, grid_height = grid_config["size"] + + # Build our graph base one settings. + # This will create a full connect graph. + self._graph = nx.grid_2d_graph(grid_width, grid_height) + + # All edge weight will be 1 by default. + edge_weights = {e: 1 for e in self._graph.edges()} + + # Facility to cell will have 1 weight, cell to facility will have 4 cost. + for facility_name, pos in grid_config["facilities"].items(): + facility_id = self._facility_name2id_mapping[facility_name] + facility = self.facilities[facility_id] + facility.x = pos[0] + facility.y = pos[1] + pos = tuple(pos) + + # Neighbors to facility will have high cost. + for npos in ((pos[0] - 1, pos[1]), (pos[0] + 1, pos[1]), (pos[0], pos[1] - 1), (pos[0], pos[1] + 1)): + if 0 <= npos[0] < grid_width and 0 <= npos[1] < grid_height: + edge_weights[(npos, pos)] = 4 + + nx.set_edge_attributes(self._graph, edge_weights, "cost") + + # Collection agent list + for facility in self.facilities.values(): + agent_type = facility.configs.get("agent_type", None) + + if agent_type is not None: + self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id, None)) + + self.agent_type_dict[type(facility).__name__] = agent_type + + for sku in facility.skus.values(): + self.max_price = max(self.max_price, sku.price) + + # Find units that contains agent type to expose as agent. + for unit in self.units.values(): + agent_type = unit.config.get("agent_type", None) + + if agent_type is not None: + # Unit or facility id, agent type, is facility, sku info, facility id, parent id. + self.agent_list.append( + AgentInfo( + unit.id, + agent_type, + False, + unit.facility.skus[unit.product_id], + unit.facility.id, + unit.parent.id + ) + ) + + self.agent_type_dict[type(unit).__name__] = agent_type + + def build_unit_by_type(self, unit_def: UnitDef, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): + """Build an unit by its type. + + Args: + unit_def (UnitDef): Definition of this unit. + parent (Union[FacilityBase, UnitBase]): Parent of this unit. + facility (FacilityBase): Facility this unit belongs to. + + Returns: + UnitBase: Unit instance. + """ + unit = unit_def.class_type() + + unit.id = self._gen_id() + unit.parent = parent + unit.facility = facility + unit.world = self + + if unit_def.data_model_alias is not None: + # Due with data model. + data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] + + # Register the data model, so that it will help to generate related instance index. + unit.data_model_index = self._register_data_model(data_model_def.alias) + unit.data_model_name = data_model_def.name_in_frame + + self.units[unit.id] = unit + + return unit + + def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBase], config: dict) -> UnitBase: + """Build an unit by its configuration. + + Args: + facility (FacilityBase): Facility of this unit belongs to. + parent (Union[FacilityBase, UnitBase]): Parent of this unit belongs to, this may be same with facility, if + this unit is attached to a facility. + config (dict): Configuration of this unit. + + Returns: + UnitBase: Unit instance. + """ + unit_class_alias = config["class"] + unit_def: UnitDef = self.configs.units[unit_class_alias] + + is_template = config.get("is_template", False) + + # If it is not a template, then just use current configuration to generate unit. + if not is_template: + unit_instance = unit_def.class_type() + + # Assign normal properties. + unit_instance.id = self._gen_id() + unit_instance.world = self + unit_instance.facility = facility + unit_instance.parent = parent + + # Record the id. + self.units[unit_instance.id] = unit_instance + + # Due with data model. + data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] + + # Register the data model, so that it will help to generate related instance index. + unit_instance.data_model_index = self._register_data_model(data_model_def.alias) + unit_instance.data_model_name = data_model_def.name_in_frame + + # Parse the config is there is any. + unit_instance.parse_configs(config.get("config", {})) + + # Prepare children. + children_conf = config.get("children", None) + + if children_conf: + unit_instance.children = [] + + for child_name, child_conf in children_conf.items(): + # If child configuration is a dict, then we add it as a property by name (key). + if type(child_conf) == dict: + child_instance = self.build_unit(facility, unit_instance, child_conf) + + setattr(unit_instance, child_name, child_instance) + unit_instance.children.append(child_instance) + elif type(child_conf) == list: + # If child configuration is a list, then will treat it as list property, named same as key. + child_list = [] + for conf in child_conf: + child_list.append(self.build_unit(facility, unit_instance, conf)) + + setattr(unit_instance, child_name, child_list) + unit_instance.children.extend(child_list) + + return unit_instance + else: + # If this is template unit, then will use the class' static method 'generate' to generate sub-units. + children = unit_def.class_type.generate(facility, config.get("config"), unit_def) + + return children + + def get_node_mapping(self): + """Collect all the entities information. + + Returns: + dict: A dictionary contains 'mapping' for id to data model index mapping, + 'detail' for detail of units and facilities. + """ + facility_info_dict = { + facility_id: facility.get_node_info() for facility_id, facility in self.facilities.items() + } + + id2index_mapping = {} + + for unit_id, unit in self.units.items(): + sku = None + + if isinstance(unit, ExtendUnitBase): + sku = unit.facility.skus[unit.product_id] + + if unit.data_model is not None: + id2index_mapping[unit_id] = (unit.data_model_name, unit.data_model_index, unit.facility.id, sku) + else: + id2index_mapping[unit_id] = (None, None, unit.facility.id, sku) + + return { + "agent_types": {k: v for k, v in self.agent_type_dict.items()}, + "unit_mapping": id2index_mapping, + "skus": {sku.id: sku for sku in self._sku_collection.values()}, + "facilities": facility_info_dict, + "max_price": self.max_price, + "max_sources_per_facility": self.max_sources_per_facility, + } + + def _register_data_model(self, alias: str) -> int: + """Register a data model alias, used to collect data model used in frame. + + Args: + alias (str): Class alias defined in core.yml. + + Returns: + int: Specified data model instance index after frame is built. + """ + if alias not in self._data_class_collection: + self._data_class_collection[alias] = 0 + + node_index = self._data_class_collection[alias] + + self._data_class_collection[alias] += 1 + + return node_index + + def _build_frame(self, snapshot_number: int) -> FrameBase: + """Build frame by current world definitions. + + Args: + snapshot_number (int): Number of snapshots to keep in memory. + + Returns: + FrameBase: Frame instance with data model in current configuration. + """ + data_class_in_frame = [] + + for alias, number in self._data_class_collection.items(): + data_model_def: DataModelDef = self.configs.data_models[alias] + data_class_in_frame.append(( + data_model_def.class_type, + data_model_def.name_in_frame, + number + )) + + frame = build_frame(True, snapshot_number, data_class_in_frame) + + return frame + + def _gen_id(self): + """Generate id for entities.""" + nid = self._id_counter + + self._id_counter += 1 + + return nid From f964924dc34df8a6083e7d4b02f63cd4c74764d2 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 30 Jun 2021 07:52:16 +0000 Subject: [PATCH 334/482] simplified cim meta --- examples/cim/agent_wrapper.py | 9 ++++----- examples/cim/dqn.py | 14 +++++++------- examples/cim/meta.py | 5 +++-- examples/general.py | 6 +++--- .../templates/policy_manager/policy_manager.py | 7 +++---- examples/templates/policy_manager/trainer.py | 4 ++-- 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/examples/cim/agent_wrapper.py b/examples/cim/agent_wrapper.py index f28ec9e88..a13bdefb3 100644 --- a/examples/cim/agent_wrapper.py +++ b/examples/cim/agent_wrapper.py @@ -8,9 +8,8 @@ cim_path = os.path.dirname(__file__) sys.path.insert(0, cim_path) -from dqn import get_dqn_policy_for_rollout from env_wrapper import env_config -from meta import CIM_POLICY_NAMES +from meta import CIM_AGENT_IDS, CIM_CREATE_ROLLOUT_POLICY_FUNC exploration_config = { @@ -28,8 +27,8 @@ def get_agent_wrapper(): **exploration_config ) return AgentWrapper( - {name: get_dqn_policy_for_rollout() for name in CIM_POLICY_NAMES}, - {name: name for name in CIM_POLICY_NAMES}, + {name: func() for name, func in CIM_CREATE_ROLLOUT_POLICY_FUNC.items()}, + {name: name for name in CIM_AGENT_IDS}, exploration_dict={f"EpsilonGreedy": epsilon_greedy}, - agent2exploration={name: "EpsilonGreedy" for name in CIM_POLICY_NAMES} + agent2exploration={name: "EpsilonGreedy" for name in CIM_AGENT_IDS} ) diff --git a/examples/cim/dqn.py b/examples/cim/dqn.py index 6487498a5..1b17817da 100644 --- a/examples/cim/dqn.py +++ b/examples/cim/dqn.py @@ -60,14 +60,14 @@ class QNet(DiscreteQNet): - def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): - super().__init__(component, optim_option=optim_option, device=device) + def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): + super().__init__(component, optim_option=optim_option, device=device) - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component(states) + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component(states) def get_dqn_policy_for_training(): diff --git a/examples/cim/meta.py b/examples/cim/meta.py index 344a405cc..fa03dd58c 100644 --- a/examples/cim/meta.py +++ b/examples/cim/meta.py @@ -9,5 +9,6 @@ from dqn import get_dqn_policy_for_rollout, get_dqn_policy_for_training from env_wrapper import CIM_AGENT_IDS -CIM_POLICY_NAMES = CIM_AGENT_IDS # use agent IDs as policy names since each agent uses a separate policy -CIM_CREATE_POLICY_FUNC = {name: get_dqn_policy_for_training for name in CIM_POLICY_NAMES} +# use agent IDs as policy names since each agent uses a separate policy +CIM_CREATE_TRAIN_POLICY_FUNC = {name: get_dqn_policy_for_training for name in CIM_AGENT_IDS} +CIM_CREATE_ROLLOUT_POLICY_FUNC = {name: get_dqn_policy_for_rollout for name in CIM_AGENT_IDS} diff --git a/examples/general.py b/examples/general.py index 405adc675..ed0bfc806 100644 --- a/examples/general.py +++ b/examples/general.py @@ -10,7 +10,7 @@ from cim.env_wrapper import get_cim_env_wrapper from cim.agent_wrapper import get_agent_wrapper -from cim.meta import CIM_AGENT_IDS, CIM_CREATE_POLICY_FUNC, CIM_POLICY_NAMES +from cim.meta import CIM_AGENT_IDS, CIM_CREATE_ROLLOUT_POLICY_FUNC, CIM_CREATE_TRAIN_POLICY_FUNC config_path = os.path.join(example_dir, "config.yml") with open(config_path, "r") as config_file: @@ -22,5 +22,5 @@ get_agent_wrapper_func_index = {"cim": get_agent_wrapper} agent_ids_index = {"cim": CIM_AGENT_IDS} -policy_names_index = {"cim": CIM_POLICY_NAMES} -create_policy_func_index = {"cim": CIM_CREATE_POLICY_FUNC} +create_rollout_policy_func_index = {"cim": CIM_CREATE_ROLLOUT_POLICY_FUNC} +create_train_policy_func_index = {"cim": CIM_CREATE_TRAIN_POLICY_FUNC} diff --git a/examples/templates/policy_manager/policy_manager.py b/examples/templates/policy_manager/policy_manager.py index 437eee471..516be3104 100644 --- a/examples/templates/policy_manager/policy_manager.py +++ b/examples/templates/policy_manager/policy_manager.py @@ -8,21 +8,20 @@ example_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # example directory sys.path.insert(0, example_dir) -from general import config, create_policy_func_index, policy_names_index, log_dir +from general import config, create_train_policy_func_index, log_dir def get_policy_manager(): scenario = config["scenario"] training_mode = config["policy_manager"]["training_mode"] num_trainers = config["policy_manager"]["num_trainers"] - policy_names = policy_names_index[scenario] - policy_dict = {name: create_policy_func_index[scenario][name]() for name in policy_names} + policy_dict = {name: func() for name, func in create_train_policy_func_index[config["scenario"]].items()} if training_mode == "single-process": return LocalPolicyManager(policy_dict, log_dir=log_dir) if training_mode == "multi-process": return MultiProcessPolicyManager( policy_dict, num_trainers, - create_policy_func_index[scenario], + create_train_policy_func_index[scenario], log_dir=log_dir ) if training_mode == "multi-node": diff --git a/examples/templates/policy_manager/trainer.py b/examples/templates/policy_manager/trainer.py index 4f5d55853..d1365b70d 100644 --- a/examples/templates/policy_manager/trainer.py +++ b/examples/templates/policy_manager/trainer.py @@ -9,14 +9,14 @@ example_path = dirname(dirname(dirname(realpath(__file__)))) # DQN async mode directory sys.path.insert(0, example_path) -from general import config, create_policy_func_index, log_dir +from general import config, create_train_policy_func_index, log_dir if __name__ == "__main__": trainer_node( config["policy_manager"]["group"], int(os.environ["TRAINERID"]), - create_policy_func_index[config["scenario"]], + create_train_policy_func_index[config["scenario"]], proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) From 5ad3e54b74055f959e65d953efc5d630146631ad Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 30 Jun 2021 08:48:02 +0000 Subject: [PATCH 335/482] fixed build.sh path bug --- examples/cim/agent_wrapper.py | 2 +- examples/general.py | 4 ++-- examples/scripts/build.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/cim/agent_wrapper.py b/examples/cim/agent_wrapper.py index a13bdefb3..2aed01dc5 100644 --- a/examples/cim/agent_wrapper.py +++ b/examples/cim/agent_wrapper.py @@ -19,7 +19,7 @@ "splits": [(5, 0.32)] } -def get_agent_wrapper(): +def get_cim_agent_wrapper(): epsilon_greedy = EpsilonGreedyExploration(num_actions=env_config["wrapper"]["num_actions"]) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, diff --git a/examples/general.py b/examples/general.py index ed0bfc806..e43577cf7 100644 --- a/examples/general.py +++ b/examples/general.py @@ -9,7 +9,7 @@ sys.path.insert(0, example_dir) from cim.env_wrapper import get_cim_env_wrapper -from cim.agent_wrapper import get_agent_wrapper +from cim.agent_wrapper import get_cim_agent_wrapper from cim.meta import CIM_AGENT_IDS, CIM_CREATE_ROLLOUT_POLICY_FUNC, CIM_CREATE_TRAIN_POLICY_FUNC config_path = os.path.join(example_dir, "config.yml") @@ -19,7 +19,7 @@ log_dir = os.path.join(example_dir, "logs", config["experiment_name"]) get_env_wrapper_func_index = {"cim": get_cim_env_wrapper} -get_agent_wrapper_func_index = {"cim": get_agent_wrapper} +get_agent_wrapper_func_index = {"cim": get_cim_agent_wrapper} agent_ids_index = {"cim": CIM_AGENT_IDS} create_rollout_policy_func_index = {"cim": CIM_CREATE_ROLLOUT_POLICY_FUNC} diff --git a/examples/scripts/build.sh b/examples/scripts/build.sh index 5068c917d..d95ae90ce 100644 --- a/examples/scripts/build.sh +++ b/examples/scripts/build.sh @@ -1,7 +1,7 @@ #!/bin/bash BASEDIR=$(dirname "$0") -ROOTDIR=$BASEDIR/../../../../../ +ROOTDIR=$BASEDIR/../../ # script to build the docker image for running the CIM scenario. docker pull redis:6 From 06c1cd319a37c7e555a3661f11fd59b5c75de104 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 30 Jun 2021 12:41:22 +0000 Subject: [PATCH 336/482] template refinement --- examples/cim/ac.py | 6 +++--- examples/cim/agent_wrapper.py | 14 +++++++------- examples/cim/dqn.py | 9 +++++---- examples/cim/env_wrapper.py | 8 ++++---- examples/cim/meta.py | 11 ++++++----- examples/general.py | 17 +++++++---------- examples/scripts/build.sh | 2 +- examples/scripts/docker_compose_yml.py | 2 +- .../templates/policy_manager/policy_manager.py | 7 +++---- examples/templates/policy_manager/trainer.py | 4 ++-- .../rollout_manager/rollout_manager.py | 8 ++++---- .../templates/rollout_manager/rollout_worker.py | 6 +++--- maro/rl/training/sync_tools/rollout_manager.py | 1 - 13 files changed, 46 insertions(+), 49 deletions(-) diff --git a/examples/cim/ac.py b/examples/cim/ac.py index 0e24cfa1a..8ef829ea9 100644 --- a/examples/cim/ac.py +++ b/examples/cim/ac.py @@ -11,13 +11,13 @@ cim_path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, cim_path) -from env_wrapper import CIM_STATE_DIM, env_config +from env_wrapper import STATE_DIM, env_config config = { "model": { "network": { "actor": { - "input_dim": CIM_STATE_DIM, + "input_dim": STATE_DIM, "hidden_dims": [256, 128, 64], "output_dim": env_config["wrapper"]["num_actions"], "activation": "tanh", @@ -26,7 +26,7 @@ "head": True }, "critic": { - "input_dim": CIM_STATE_DIM, + "input_dim": STATE_DIM, "hidden_dims": [256, 128, 64], "output_dim": env_config["wrapper"]["num_actions"], "activation": "leaky_relu", diff --git a/examples/cim/agent_wrapper.py b/examples/cim/agent_wrapper.py index 2aed01dc5..59bbca829 100644 --- a/examples/cim/agent_wrapper.py +++ b/examples/cim/agent_wrapper.py @@ -6,10 +6,10 @@ from maro.rl import AgentWrapper, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler -cim_path = os.path.dirname(__file__) +cim_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, cim_path) -from env_wrapper import env_config -from meta import CIM_AGENT_IDS, CIM_CREATE_ROLLOUT_POLICY_FUNC +from env_wrapper import AGENT_IDS, env_config +from meta import create_rollout_policy_func exploration_config = { @@ -19,7 +19,7 @@ "splits": [(5, 0.32)] } -def get_cim_agent_wrapper(): +def get_agent_wrapper(): epsilon_greedy = EpsilonGreedyExploration(num_actions=env_config["wrapper"]["num_actions"]) epsilon_greedy.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, @@ -27,8 +27,8 @@ def get_cim_agent_wrapper(): **exploration_config ) return AgentWrapper( - {name: func() for name, func in CIM_CREATE_ROLLOUT_POLICY_FUNC.items()}, - {name: name for name in CIM_AGENT_IDS}, + {name: func() for name, func in create_rollout_policy_func.items()}, + {name: name for name in AGENT_IDS}, exploration_dict={f"EpsilonGreedy": epsilon_greedy}, - agent2exploration={name: "EpsilonGreedy" for name in CIM_AGENT_IDS} + agent2exploration={name: "EpsilonGreedy" for name in AGENT_IDS} ) diff --git a/examples/cim/dqn.py b/examples/cim/dqn.py index 1b17817da..4e9cd39d8 100644 --- a/examples/cim/dqn.py +++ b/examples/cim/dqn.py @@ -9,14 +9,15 @@ import torch.nn as nn from maro.rl import DQN, DQNConfig, DiscreteQNet, ExperienceManager, FullyConnectedBlock, OptimOption -cim_path = os.path.dirname(os.path.dirname(__file__)) -sys.path.insert(0, cim_path) -from env_wrapper import CIM_STATE_DIM, env_config +cim_path = os.path.dirname(os.path.realpath(__file__)) +if cim_path not in sys.path: + sys.path.insert(0, cim_path) +from env_wrapper import STATE_DIM, env_config config = { "model": { "network": { - "input_dim": CIM_STATE_DIM, + "input_dim": STATE_DIM, "hidden_dims": [256, 128, 64, 32], "output_dim": env_config["wrapper"]["num_actions"], "activation": "leaky_relu", diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index 8dabe1466..a1dbc532f 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -132,11 +132,11 @@ def get_reward(self, tick=None): } } -def get_cim_env_wrapper(): +def get_env_wrapper(): return CIMEnvWrapper(Env(**env_config["basic"]), **env_config["wrapper"]) -tmp_env_wrapper = get_cim_env_wrapper() -CIM_AGENT_IDS = tmp_env_wrapper.agent_idx_list -CIM_STATE_DIM = tmp_env_wrapper.state_dim +tmp_env_wrapper = get_env_wrapper() +AGENT_IDS = tmp_env_wrapper.agent_idx_list +STATE_DIM = tmp_env_wrapper.state_dim del tmp_env_wrapper diff --git a/examples/cim/meta.py b/examples/cim/meta.py index fa03dd58c..6dbf69667 100644 --- a/examples/cim/meta.py +++ b/examples/cim/meta.py @@ -4,11 +4,12 @@ import os import sys -cim_path = os.path.dirname(__file__) -sys.path.insert(0, cim_path) +cim_path = os.path.dirname(os.path.realpath(__file__)) +if cim_path not in sys.path: + sys.path.insert(0, cim_path) from dqn import get_dqn_policy_for_rollout, get_dqn_policy_for_training -from env_wrapper import CIM_AGENT_IDS +from env_wrapper import AGENT_IDS # use agent IDs as policy names since each agent uses a separate policy -CIM_CREATE_TRAIN_POLICY_FUNC = {name: get_dqn_policy_for_training for name in CIM_AGENT_IDS} -CIM_CREATE_ROLLOUT_POLICY_FUNC = {name: get_dqn_policy_for_rollout for name in CIM_AGENT_IDS} +create_train_policy_func = {name: get_dqn_policy_for_training for name in AGENT_IDS} +create_rollout_policy_func = {name: get_dqn_policy_for_rollout for name in AGENT_IDS} diff --git a/examples/general.py b/examples/general.py index e43577cf7..43a26a7d9 100644 --- a/examples/general.py +++ b/examples/general.py @@ -8,19 +8,16 @@ example_dir = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, example_dir) -from cim.env_wrapper import get_cim_env_wrapper -from cim.agent_wrapper import get_cim_agent_wrapper -from cim.meta import CIM_AGENT_IDS, CIM_CREATE_ROLLOUT_POLICY_FUNC, CIM_CREATE_TRAIN_POLICY_FUNC - config_path = os.path.join(example_dir, "config.yml") with open(config_path, "r") as config_file: config = yaml.safe_load(config_file) log_dir = os.path.join(example_dir, "logs", config["experiment_name"]) -get_env_wrapper_func_index = {"cim": get_cim_env_wrapper} -get_agent_wrapper_func_index = {"cim": get_cim_agent_wrapper} - -agent_ids_index = {"cim": CIM_AGENT_IDS} -create_rollout_policy_func_index = {"cim": CIM_CREATE_ROLLOUT_POLICY_FUNC} -create_train_policy_func_index = {"cim": CIM_CREATE_TRAIN_POLICY_FUNC} +scenario = config["scenario"] +if scenario == "cim": + from cim.env_wrapper import get_env_wrapper + from cim.agent_wrapper import get_agent_wrapper + from cim.meta import create_rollout_policy_func, create_train_policy_func +else: + raise ValueError(f"Unsupported scenario: {scenario}. Supported scenarios: 'cim'") diff --git a/examples/scripts/build.sh b/examples/scripts/build.sh index d95ae90ce..8f82e9b2e 100644 --- a/examples/scripts/build.sh +++ b/examples/scripts/build.sh @@ -5,4 +5,4 @@ ROOTDIR=$BASEDIR/../../ # script to build the docker image for running the CIM scenario. docker pull redis:6 -docker build -f $ROOTDIR/docker_files/dev.df -t maro-cim:latest $ROOTDIR \ No newline at end of file +docker build -f $ROOTDIR/docker_files/dev.df -t maro:latest $ROOTDIR \ No newline at end of file diff --git a/examples/scripts/docker_compose_yml.py b/examples/scripts/docker_compose_yml.py index 9d13d9464..fc9ea1703 100644 --- a/examples/scripts/docker_compose_yml.py +++ b/examples/scripts/docker_compose_yml.py @@ -22,7 +22,7 @@ docker_compose_manifest = {"version": "3.9", "services": {"redis": {"image": "redis:6", "container_name": redis_host}}} common_spec = { "build": {"context": root_dir, "dockerfile": dockerfile_path}, - "image": "maro-cim", + "image": "maro", "volumes": [ f"{example_dir}:/maro/examples", f"{maro_rl_dir}:/maro/maro/rl", diff --git a/examples/templates/policy_manager/policy_manager.py b/examples/templates/policy_manager/policy_manager.py index 516be3104..8bf53887c 100644 --- a/examples/templates/policy_manager/policy_manager.py +++ b/examples/templates/policy_manager/policy_manager.py @@ -8,20 +8,19 @@ example_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # example directory sys.path.insert(0, example_dir) -from general import config, create_train_policy_func_index, log_dir +from general import config, create_train_policy_func, log_dir def get_policy_manager(): - scenario = config["scenario"] training_mode = config["policy_manager"]["training_mode"] num_trainers = config["policy_manager"]["num_trainers"] - policy_dict = {name: func() for name, func in create_train_policy_func_index[config["scenario"]].items()} + policy_dict = {name: func() for name, func in create_train_policy_func.items()} if training_mode == "single-process": return LocalPolicyManager(policy_dict, log_dir=log_dir) if training_mode == "multi-process": return MultiProcessPolicyManager( policy_dict, num_trainers, - create_train_policy_func_index[scenario], + create_train_policy_func, log_dir=log_dir ) if training_mode == "multi-node": diff --git a/examples/templates/policy_manager/trainer.py b/examples/templates/policy_manager/trainer.py index d1365b70d..91669da05 100644 --- a/examples/templates/policy_manager/trainer.py +++ b/examples/templates/policy_manager/trainer.py @@ -9,14 +9,14 @@ example_path = dirname(dirname(dirname(realpath(__file__)))) # DQN async mode directory sys.path.insert(0, example_path) -from general import config, create_train_policy_func_index, log_dir +from general import config, create_train_policy_func, log_dir if __name__ == "__main__": trainer_node( config["policy_manager"]["group"], int(os.environ["TRAINERID"]), - create_train_policy_func_index[config["scenario"]], + create_train_policy_func, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/examples/templates/rollout_manager/rollout_manager.py b/examples/templates/rollout_manager/rollout_manager.py index bd25d34c7..d82b015ef 100644 --- a/examples/templates/rollout_manager/rollout_manager.py +++ b/examples/templates/rollout_manager/rollout_manager.py @@ -8,22 +8,22 @@ example_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # example directory sys.path.insert(0, example_dir) -from general import config, get_agent_wrapper_func_index, get_env_wrapper_func_index, log_dir +from general import config, get_agent_wrapper, get_env_wrapper, log_dir def get_rollout_manager(): rollout_mode = config["rollout"]["mode"] if rollout_mode == "single-process": return LocalRolloutManager( - get_env_wrapper_func_index[config["scenario"]](), + get_env_wrapper(), num_steps=config["num_steps"], log_dir=log_dir ) if rollout_mode == "multi-process": return MultiProcessRolloutManager( config["rollout"]["num_workers"], - get_env_wrapper_func_index[config["scenario"]], - get_agent_wrapper_func_index[config["scenario"]], + get_env_wrapper, + get_agent_wrapper, num_steps=config["num_steps"], log_dir=log_dir, ) diff --git a/examples/templates/rollout_manager/rollout_worker.py b/examples/templates/rollout_manager/rollout_worker.py index 330f3231c..dc085c26f 100644 --- a/examples/templates/rollout_manager/rollout_worker.py +++ b/examples/templates/rollout_manager/rollout_worker.py @@ -9,15 +9,15 @@ example_path = dirname(dirname(dirname(realpath(__file__)))) # example directory sys.path.insert(0, example_path) -from general import config, get_agent_wrapper_func_index, get_env_wrapper_func_index, log_dir +from general import config, get_agent_wrapper, get_env_wrapper, log_dir if __name__ == "__main__": rollout_worker_node( config["rollout"]["group"], int(environ["WORKERID"]), - get_env_wrapper_func_index[config["scenario"]](), - get_agent_wrapper_func_index[config["scenario"]](), + get_env_wrapper(), + get_agent_wrapper(), proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/maro/rl/training/sync_tools/rollout_manager.py b/maro/rl/training/sync_tools/rollout_manager.py index 2a6645ed7..6a2da46bd 100644 --- a/maro/rl/training/sync_tools/rollout_manager.py +++ b/maro/rl/training/sync_tools/rollout_manager.py @@ -369,7 +369,6 @@ def __init__( peers = {"rollout_worker": num_workers} self._proxy = Proxy(group, "rollout_manager", peers, **proxy_kwargs) self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's - print(self._workers) self._num_steps = num_steps if max_receive_attempts is None: From ff76caad0cd83a79e47dd62f85d13f9b54201546 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 15:05:18 +0800 Subject: [PATCH 337/482] deleted obsolete svgs --- docs/source/images/rl/learner.svg | 3 --- docs/source/images/rl/policy_manager.svg | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 docs/source/images/rl/learner.svg delete mode 100644 docs/source/images/rl/policy_manager.svg diff --git a/docs/source/images/rl/learner.svg b/docs/source/images/rl/learner.svg deleted file mode 100644 index 3c4ee705f..000000000 --- a/docs/source/images/rl/learner.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Learner
Learner
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
experiences
expe...
Policy
Manager
Policy...
updated policies
updated pol...
Learner
Lear...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/policy_manager.svg b/docs/source/images/rl/policy_manager.svg deleted file mode 100644 index 2a725fa7a..000000000 --- a/docs/source/images/rl/policy_manager.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Local Policy Manager
Local Policy Manager
policies
policies
on_experiences
on_experiences
Parallel Policy Manager
Parallel Policy Manager
on_experiences
on_experiences
policy proxy
policy proxy
remote training config
remote training...
Policy Trainer 1
Policy Trainer 1
get_state
get_state
get_state
get_state
subset of policies
subset of pol...
training message
traini...
policy state
policy state
Local Policy Manager
Local Policy Manager
Parallel Policy Manager
Parallel Policy Manager
Viewer does not support full SVG 1.1
\ No newline at end of file From 35e55a73645056ad4c6013264aa80732835ba151 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 15:12:35 +0800 Subject: [PATCH 338/482] updated learner logs --- maro/rl/training/sync_tools/learner.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/maro/rl/training/sync_tools/learner.py b/maro/rl/training/sync_tools/learner.py index c81cffb7e..18cc758fb 100644 --- a/maro/rl/training/sync_tools/learner.py +++ b/maro/rl/training/sync_tools/learner.py @@ -96,7 +96,7 @@ def run(self): self.policy_manager.exit() def _train(self, ep: int): - total_policy_update_time = 0 + collect_time = policy_update_time = 0 num_experiences_collected = segment = 0 self.rollout_manager.reset() while not self.rollout_manager.episode_complete: @@ -105,17 +105,20 @@ def _train(self, ep: int): policy_state_dict = self.policy_manager.get_state() self.policy_manager.reset_update_status() policy_version = self.policy_manager.version + tc0 = time.time() exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) - t0 = time.time() + collect_time += time.time() - tc0 + tu0 = time.time() self.policy_manager.on_experiences(exp_by_policy) - total_policy_update_time += time.time() - t0 + policy_update_time += time.time() - tu0 num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) # performance details - self.logger.debug( + self.logger.info( f"ep {ep} summary - " f"experiences collected: {num_experiences_collected} " - f"total policy update time: {total_policy_update_time}" + f"experience collection time: {collect_time} " + f"policy update time: {policy_update_time}" ) self.end_of_episode(ep, **self._end_of_episode_kwargs) From ae1e93f38c8e0023db236279d15c8265aaab9605 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 15:13:39 +0800 Subject: [PATCH 339/482] minor edits --- maro/rl/training/sync_tools/learner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/rl/training/sync_tools/learner.py b/maro/rl/training/sync_tools/learner.py index 18cc758fb..74b15cc76 100644 --- a/maro/rl/training/sync_tools/learner.py +++ b/maro/rl/training/sync_tools/learner.py @@ -96,8 +96,8 @@ def run(self): self.policy_manager.exit() def _train(self, ep: int): - collect_time = policy_update_time = 0 - num_experiences_collected = segment = 0 + collect_time = policy_update_time = num_experiences_collected = 0 + segment = 0 self.rollout_manager.reset() while not self.rollout_manager.episode_complete: segment += 1 From 04c53e614b4ec3d83743ad0d27373a7659220985 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 16:20:28 +0800 Subject: [PATCH 340/482] refactored templates for easy merge with async PR --- examples/scripts/docker_compose_yml.py | 38 ++++++++++--------- examples/{ => templates}/config.yml | 14 +++---- examples/{ => templates}/general.py | 14 ++++--- examples/templates/learner.py | 28 -------------- .../policy_manager/policy_manager.py | 20 +++++----- examples/templates/policy_manager/trainer.py | 11 +++--- .../learner.py} | 32 ++++++++++++---- .../rollout_worker.py | 7 ++-- 8 files changed, 81 insertions(+), 83 deletions(-) rename examples/{ => templates}/config.yml (54%) rename examples/{ => templates}/general.py (57%) delete mode 100644 examples/templates/learner.py rename examples/templates/{rollout_manager/rollout_manager.py => sync_mode/learner.py} (52%) rename examples/templates/{rollout_manager => sync_mode}/rollout_worker.py (73%) diff --git a/examples/scripts/docker_compose_yml.py b/examples/scripts/docker_compose_yml.py index fc9ea1703..9faa67206 100644 --- a/examples/scripts/docker_compose_yml.py +++ b/examples/scripts/docker_compose_yml.py @@ -9,9 +9,10 @@ script_dir = dirname(path) example_dir = dirname(script_dir) root_dir = dirname(example_dir) +template_dir = join(example_dir, "templates") maro_rl_dir = join(root_dir, "maro", "rl") maro_comm_dir = join(root_dir, "maro", "communication") -config_path = join(example_dir, "config.yml") +config_path = join(template_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") with open(config_path, "r") as fp: @@ -31,7 +32,7 @@ } # trainer spec -if config["policy_manager"]["training_mode"] == "multi-node": +if config["policy_manager"]["train_mode"] == "multi-node": for trainer_id in range(num_trainers): str_id = f"trainer.{trainer_id}" trainer_spec = deepcopy(common_spec) @@ -41,22 +42,25 @@ trainer_spec["environment"] = [f"TRAINERID={trainer_id}"] docker_compose_manifest["services"][str_id] = trainer_spec -# learner_spec -docker_compose_manifest["services"]["learner"] = { - **common_spec, - **{"container_name": "learner", "command": "python3 /maro/examples/templates/learner.py"} -} +if config["mode"] == "sync": + # learner_spec + docker_compose_manifest["services"]["learner"] = { + **common_spec, + **{"container_name": "learner", "command": "python3 /maro/examples/templates/sync_mode/learner.py"} + } + # rollout worker spec + if config["sync"]["rollout_mode"] == "multi-node": + for worker_id in range(config["sync"]["num_rollout_workers"]): + str_id = f"rollout_worker.{worker_id}" + worker_spec = deepcopy(common_spec) + del worker_spec["build"] + worker_spec["command"] = "python3 /maro/examples/templates/sync_mode/rollout_worker.py" + worker_spec["container_name"] = str_id + worker_spec["environment"] = [f"WORKERID={worker_id}"] + docker_compose_manifest["services"][str_id] = worker_spec +else: + raise ValueError("Only sync mode is supported in this version") -# rollout worker spec -if config["rollout"]["mode"] == "multi-node": - for worker_id in range(config["rollout"]["num_workers"]): - str_id = f"rollout_worker.{worker_id}" - worker_spec = deepcopy(common_spec) - del worker_spec["build"] - worker_spec["command"] = "python3 /maro/examples/templates/rollout_manager/rollout_worker.py" - worker_spec["container_name"] = str_id - worker_spec["environment"] = [f"WORKERID={worker_id}"] - docker_compose_manifest["services"][str_id] = worker_spec with open(join(example_dir, "docker-compose.yml"), "w") as fp: yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/config.yml b/examples/templates/config.yml similarity index 54% rename from examples/config.yml rename to examples/templates/config.yml index 0ebd73184..c7566e13a 100644 --- a/examples/config.yml +++ b/examples/templates/config.yml @@ -1,21 +1,21 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -experiment_name: cim-dqn +job_name: cim-dqn scenario: cim mode: sync num_episodes: 10 eval_schedule: 2 num_steps: 50 -rollout: - group: rollout - mode: multi-node # single-process, multi-process, multi-node - num_workers: 3 +sync: + rollout_group: rollout + rollout_mode: multi-node # single-process, multi-process, multi-node + num_rollout_workers: 3 # max_receive_attempts: 2 # receive_timeout: 100 # in milli-seconds policy_manager: - group: policy-manager - training_mode: multi-node # single-process, multi-process, multi-node + train_group: policy-manager + train_mode: multi-node # single-process, multi-process, multi-node num_trainers: 2 redis: host: maro-redis diff --git a/examples/general.py b/examples/templates/general.py similarity index 57% rename from examples/general.py rename to examples/templates/general.py index 43a26a7d9..598959a52 100644 --- a/examples/general.py +++ b/examples/templates/general.py @@ -1,18 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import os import sys import yaml +from os.path import dirname, join, realpath -example_dir = os.path.dirname(os.path.realpath(__file__)) -sys.path.insert(0, example_dir) +template_dir = dirname(dirname(realpath(__file__))) +example_dir = dirname(template_dir) +if template_dir not in sys.path: + sys.path.insert(0, template_dir) +if example_dir not in sys.path: + sys.path.insert(0, example_dir) -config_path = os.path.join(example_dir, "config.yml") +config_path = join(template_dir, "config.yml") with open(config_path, "r") as config_file: config = yaml.safe_load(config_file) -log_dir = os.path.join(example_dir, "logs", config["experiment_name"]) +log_dir = join(example_dir, "logs", config["job_name"]) scenario = config["scenario"] if scenario == "cim": diff --git a/examples/templates/learner.py b/examples/templates/learner.py deleted file mode 100644 index b2c53432f..000000000 --- a/examples/templates/learner.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys -import time - -from maro.rl import Learner - -template_path = os.path.dirname(os.path.realpath(__file__)) -example_path = os.path.dirname(template_path) -sys.path.insert(0, template_path) -sys.path.insert(0, example_path) -from general import config, log_dir -from policy_manager.policy_manager import get_policy_manager -from rollout_manager.rollout_manager import get_rollout_manager - - -if __name__ == "__main__": - learner = Learner( - policy_manager=get_policy_manager(), - rollout_manager=get_rollout_manager(), - num_episodes=config["num_episodes"], - eval_schedule=config["eval_schedule"], - log_dir=log_dir - ) - time.sleep(10) - learner.run() diff --git a/examples/templates/policy_manager/policy_manager.py b/examples/templates/policy_manager/policy_manager.py index 8bf53887c..d3fb1fa46 100644 --- a/examples/templates/policy_manager/policy_manager.py +++ b/examples/templates/policy_manager/policy_manager.py @@ -1,38 +1,38 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import os import sys +from os.path import dirname, realpath from maro.rl import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager -example_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # example directory -sys.path.insert(0, example_dir) +template_dir = dirname(dirname(realpath(__file__))) # template directory +if template_dir not in sys.path: + sys.path.insert(0, template_dir) from general import config, create_train_policy_func, log_dir def get_policy_manager(): - training_mode = config["policy_manager"]["training_mode"] + train_mode = config["policy_manager"]["train_mode"] num_trainers = config["policy_manager"]["num_trainers"] policy_dict = {name: func() for name, func in create_train_policy_func.items()} - if training_mode == "single-process": + if train_mode == "single-process": return LocalPolicyManager(policy_dict, log_dir=log_dir) - if training_mode == "multi-process": + if train_mode == "multi-process": return MultiProcessPolicyManager( policy_dict, num_trainers, create_train_policy_func, log_dir=log_dir ) - if training_mode == "multi-node": + if train_mode == "multi-node": return MultiNodePolicyManager( policy_dict, - config["policy_manager"]["group"], + config["policy_manager"]["train_group"], num_trainers, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) raise ValueError( - f"Unsupported policy training mode: {training_mode}. " - f"Supported modes: single-process, multi-process, multi-node" + f"Unsupported policy training mode: {train_mode}. Supported modes: single-process, multi-process, multi-node" ) diff --git a/examples/templates/policy_manager/trainer.py b/examples/templates/policy_manager/trainer.py index 91669da05..918df7fd0 100644 --- a/examples/templates/policy_manager/trainer.py +++ b/examples/templates/policy_manager/trainer.py @@ -1,21 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import os import sys +from os import environ from os.path import dirname, realpath from maro.rl import trainer_node -example_path = dirname(dirname(dirname(realpath(__file__)))) # DQN async mode directory -sys.path.insert(0, example_path) +template_dir = dirname(dirname(realpath(__file__))) # template directory +if template_dir not in sys.path: + sys.path.insert(0, template_dir) from general import config, create_train_policy_func, log_dir if __name__ == "__main__": trainer_node( - config["policy_manager"]["group"], - int(os.environ["TRAINERID"]), + config["policy_manager"]["train_group"], + int(environ["TRAINERID"]), create_train_policy_func, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir diff --git a/examples/templates/rollout_manager/rollout_manager.py b/examples/templates/sync_mode/learner.py similarity index 52% rename from examples/templates/rollout_manager/rollout_manager.py rename to examples/templates/sync_mode/learner.py index d82b015ef..504dffc59 100644 --- a/examples/templates/rollout_manager/rollout_manager.py +++ b/examples/templates/sync_mode/learner.py @@ -1,18 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import os import sys +import time +from os.path import dirname, realpath -from maro.rl import LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager +from maro.rl import Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager + +template_dir = dirname(dirname((realpath(__file__)))) +if template_dir not in sys.path: + sys.path.insert(0, template_dir) -example_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # example directory -sys.path.insert(0, example_dir) from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from policy_manager.policy_manager import get_policy_manager def get_rollout_manager(): - rollout_mode = config["rollout"]["mode"] + rollout_mode = config["sync"]["rollout_mode"] if rollout_mode == "single-process": return LocalRolloutManager( get_env_wrapper(), @@ -21,7 +25,7 @@ def get_rollout_manager(): ) if rollout_mode == "multi-process": return MultiProcessRolloutManager( - config["rollout"]["num_workers"], + config["sync"]["num_rollout_workers"], get_env_wrapper, get_agent_wrapper, num_steps=config["num_steps"], @@ -29,11 +33,23 @@ def get_rollout_manager(): ) if rollout_mode == "multi-node": return MultiNodeRolloutManager( - config["rollout"]["group"], - config["rollout"]["num_workers"], + config["sync"]["rollout_group"], + config["sync"]["num_rollout_workers"], proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} ) raise ValueError( f"Unsupported roll-out mode: {rollout_mode}. Supported modes: single-process, multi-process, multi-node" ) + + +if __name__ == "__main__": + learner = Learner( + policy_manager=get_policy_manager(), + rollout_manager=get_rollout_manager(), + num_episodes=config["num_episodes"], + eval_schedule=config["eval_schedule"], + log_dir=log_dir + ) + time.sleep(10) + learner.run() diff --git a/examples/templates/rollout_manager/rollout_worker.py b/examples/templates/sync_mode/rollout_worker.py similarity index 73% rename from examples/templates/rollout_manager/rollout_worker.py rename to examples/templates/sync_mode/rollout_worker.py index dc085c26f..a8b782471 100644 --- a/examples/templates/rollout_manager/rollout_worker.py +++ b/examples/templates/sync_mode/rollout_worker.py @@ -7,14 +7,15 @@ from maro.rl import rollout_worker_node -example_path = dirname(dirname(dirname(realpath(__file__)))) # example directory -sys.path.insert(0, example_path) +template_dir = dirname(dirname(realpath(__file__))) # template directory +if template_dir not in sys.path: + sys.path.insert(0, template_dir) from general import config, get_agent_wrapper, get_env_wrapper, log_dir if __name__ == "__main__": rollout_worker_node( - config["rollout"]["group"], + config["sync"]["rollout_group"], int(environ["WORKERID"]), get_env_wrapper(), get_agent_wrapper(), From 1315f04441131dd6977c8597ad920d32e6712ce8 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 16:59:51 +0800 Subject: [PATCH 341/482] added component names for rollout manager and policy manager --- maro/rl/training/policy_manager/policy_manager.py | 4 ++-- maro/rl/training/policy_manager/trainer.py | 6 +++--- maro/rl/training/sync_tools/rollout_manager.py | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py index d74d6d0c1..b3a6de7a4 100644 --- a/maro/rl/training/policy_manager/policy_manager.py +++ b/maro/rl/training/policy_manager/policy_manager.py @@ -178,9 +178,9 @@ def __init__( log_dir: str = getcwd() ): super().__init__(policy_dict) - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) peers = {"trainer": num_trainers} - self._proxy = Proxy(group, "policy_manager", peers, **proxy_kwargs) + self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) + self._logger = Logger(self._proxy.name, dump_folder=log_dir) self._policy2trainer = {} self._trainer2policies = defaultdict(list) diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py index fd50ac16e..7dbf58f26 100644 --- a/maro/rl/training/policy_manager/trainer.py +++ b/maro/rl/training/policy_manager/trainer.py @@ -54,7 +54,7 @@ def trainer_process( def trainer_node( group: str, - trainer_id: int, + trainer_idx: int, create_policy_func_dict: Dict[str, Callable], proxy_kwargs: dict = {}, log_dir: str = getcwd() @@ -64,7 +64,7 @@ def trainer_node( Args: group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. - trainer_id (int): Integer trainer ID. + trainer_idx (int): Integer trainer index. The trainer's ID in the cluster will be "TRAINER.{trainer_idx}". create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. @@ -73,7 +73,7 @@ def trainer_node( log_dir (str): Directory to store logs in. Defaults to the current working directory. """ policy_dict = {} - proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=f"TRAINER.{trainer_id}", **proxy_kwargs) + proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=f"TRAINER.{trainer_idx}", **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) for msg in proxy.receive(): diff --git a/maro/rl/training/sync_tools/rollout_manager.py b/maro/rl/training/sync_tools/rollout_manager.py index 6a2da46bd..3ee73a0a9 100644 --- a/maro/rl/training/sync_tools/rollout_manager.py +++ b/maro/rl/training/sync_tools/rollout_manager.py @@ -364,11 +364,12 @@ def __init__( raise ValueError("num_eval_workers cannot exceed the number of available workers") super().__init__() - self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) self.num_workers = num_workers peers = {"rollout_worker": num_workers} - self._proxy = Proxy(group, "rollout_manager", peers, **proxy_kwargs) + self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's + self._logger = Logger(self._proxy.name, dump_folder=log_dir) + self._num_steps = num_steps if max_receive_attempts is None: From de40647b2829c2cc53bcf468a1c66a9bb85ca777 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 17:10:13 +0800 Subject: [PATCH 342/482] fixed incorrect position to add last episode to eval schedule --- maro/rl/training/sync_tools/learner.py | 6 ++++-- maro/rl/training/sync_tools/local_learner.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/maro/rl/training/sync_tools/learner.py b/maro/rl/training/sync_tools/learner.py index 74b15cc76..0ba71ebfb 100644 --- a/maro/rl/training/sync_tools/learner.py +++ b/maro/rl/training/sync_tools/learner.py @@ -62,8 +62,10 @@ def __init__( else: self._eval_schedule = eval_schedule self._eval_schedule.sort() - if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - self._eval_schedule.append(num_episodes) + + # always evaluate after the last episode + if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: + self._eval_schedule.append(num_episodes) self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") self._eval_point_index = 0 diff --git a/maro/rl/training/sync_tools/local_learner.py b/maro/rl/training/sync_tools/local_learner.py index 0a1c2dae1..d81e7b0c2 100644 --- a/maro/rl/training/sync_tools/local_learner.py +++ b/maro/rl/training/sync_tools/local_learner.py @@ -70,8 +70,10 @@ def __init__( else: self._eval_schedule = eval_schedule self._eval_schedule.sort() - if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - self._eval_schedule.append(num_episodes) + + # always evaluate after the last episode + if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: + self._eval_schedule.append(num_episodes) self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") self._eval_point_index = 0 From 360240f8dad364ef6ca826cb304e97fe1f1f28e1 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 17:17:58 +0800 Subject: [PATCH 343/482] added max_lag option in templates --- examples/templates/config.yml | 1 + examples/templates/sync_mode/learner.py | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/templates/config.yml b/examples/templates/config.yml index c7566e13a..d29df4737 100644 --- a/examples/templates/config.yml +++ b/examples/templates/config.yml @@ -7,6 +7,7 @@ mode: sync num_episodes: 10 eval_schedule: 2 num_steps: 50 +max_lag: 0 sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node diff --git a/examples/templates/sync_mode/learner.py b/examples/templates/sync_mode/learner.py index 504dffc59..9c7eef542 100644 --- a/examples/templates/sync_mode/learner.py +++ b/examples/templates/sync_mode/learner.py @@ -35,6 +35,7 @@ def get_rollout_manager(): return MultiNodeRolloutManager( config["sync"]["rollout_group"], config["sync"]["num_rollout_workers"], + max_lag=config["max_lag"], proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} ) From 315e85faa4a72f2db47ffda0d29bf7a08965213d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 17:21:39 +0800 Subject: [PATCH 344/482] formatting edit in docker_compose_yml script --- examples/scripts/docker_compose_yml.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/scripts/docker_compose_yml.py b/examples/scripts/docker_compose_yml.py index 9faa67206..4ba7040ef 100644 --- a/examples/scripts/docker_compose_yml.py +++ b/examples/scripts/docker_compose_yml.py @@ -42,11 +42,15 @@ trainer_spec["environment"] = [f"TRAINERID={trainer_id}"] docker_compose_manifest["services"][str_id] = trainer_spec -if config["mode"] == "sync": +mode = config["mode"] +if mode == "sync": # learner_spec docker_compose_manifest["services"]["learner"] = { **common_spec, - **{"container_name": "learner", "command": "python3 /maro/examples/templates/sync_mode/learner.py"} + **{ + "container_name": "learner", + "command": "python3 /maro/examples/templates/sync_mode/learner.py" + } } # rollout worker spec if config["sync"]["rollout_mode"] == "multi-node": From 953c8736cad72955754731218efe4b0f88fe5d00 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 19:14:32 +0800 Subject: [PATCH 345/482] moved local learner and early stopper outside sync_tools --- maro/rl/training/__init__.py | 6 ++++-- maro/rl/training/{sync_tools => }/early_stopper.py | 0 maro/rl/training/{sync_tools => }/local_learner.py | 2 +- maro/rl/training/sync_tools/__init__.py | 4 ---- maro/rl/training/sync_tools/learner.py | 4 ++-- 5 files changed, 7 insertions(+), 9 deletions(-) rename maro/rl/training/{sync_tools => }/early_stopper.py (100%) rename maro/rl/training/{sync_tools => }/local_learner.py (99%) diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index bcea443d0..a5c425540 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .early_stopper import AbsEarlyStopper +from .local_learner import LocalLearner from .policy_manager import ( AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, trainer_node, trainer_process ) from .sync_tools import ( - AbsEarlyStopper, AbsRolloutManager, Learner, LocalLearner, LocalRolloutManager, MultiNodeRolloutManager, - MultiProcessRolloutManager, rollout_worker_node, rollout_worker_process + AbsRolloutManager, Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager, + rollout_worker_node, rollout_worker_process ) __all__ = [ diff --git a/maro/rl/training/sync_tools/early_stopper.py b/maro/rl/training/early_stopper.py similarity index 100% rename from maro/rl/training/sync_tools/early_stopper.py rename to maro/rl/training/early_stopper.py diff --git a/maro/rl/training/sync_tools/local_learner.py b/maro/rl/training/local_learner.py similarity index 99% rename from maro/rl/training/sync_tools/local_learner.py rename to maro/rl/training/local_learner.py index d81e7b0c2..a4feb406a 100644 --- a/maro/rl/training/sync_tools/local_learner.py +++ b/maro/rl/training/local_learner.py @@ -8,7 +8,7 @@ from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger -from .early_stopper import AbsEarlyStopper +from early_stopper import AbsEarlyStopper class LocalLearner: diff --git a/maro/rl/training/sync_tools/__init__.py b/maro/rl/training/sync_tools/__init__.py index 0f75e1bd0..f6a00e4a3 100644 --- a/maro/rl/training/sync_tools/__init__.py +++ b/maro/rl/training/sync_tools/__init__.py @@ -1,16 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .early_stopper import AbsEarlyStopper from .learner import Learner -from .local_learner import LocalLearner from .rollout_manager import AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager from .rollout_worker import rollout_worker_node, rollout_worker_process __all__ = [ - "AbsEarlyStopper", "Learner", - "LocalLearner", "AbsRolloutManager", "LocalRolloutManager", "MultiProcessRolloutManager", "MultiNodeRolloutManager", "rollout_worker_node", "rollout_worker_process" ] diff --git a/maro/rl/training/sync_tools/learner.py b/maro/rl/training/sync_tools/learner.py index 0ba71ebfb..4cef5ccef 100644 --- a/maro/rl/training/sync_tools/learner.py +++ b/maro/rl/training/sync_tools/learner.py @@ -7,9 +7,9 @@ from maro.utils import Logger +from ..early_stopper import AbsEarlyStopper from ..policy_manager.policy_manager import AbsPolicyManager -from .early_stopper import AbsEarlyStopper -from .rollout_manager import AbsRolloutManager +from rollout_manager import AbsRolloutManager class Learner: From ed9d44a88aecab0a864df3cfe20f9f63220a8779 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 1 Jul 2021 20:57:24 +0800 Subject: [PATCH 346/482] refactored rl toolkit folder structure --- maro/rl/__init__.py | 32 +- maro/rl/early_stopping/__init__.py | 6 + maro/rl/early_stopping/early_stopper.py | 17 + maro/rl/local/__init__.py | 6 + maro/rl/local/local_learner.py | 149 +++++++ maro/rl/policy/__init__.py | 7 +- maro/rl/policy/policy_manager.py | 219 +++++++++++ maro/rl/policy/trainer.py | 100 +++++ maro/rl/synchronous/__init__.py | 11 + maro/rl/synchronous/learner.py | 130 ++++++ maro/rl/synchronous/rollout_manager.py | 499 ++++++++++++++++++++++++ maro/rl/synchronous/rollout_worker.py | 210 ++++++++++ maro/rl/utils/__init__.py | 5 +- maro/rl/utils/message_enums.py | 38 ++ 14 files changed, 1414 insertions(+), 15 deletions(-) create mode 100644 maro/rl/early_stopping/__init__.py create mode 100644 maro/rl/early_stopping/early_stopper.py create mode 100644 maro/rl/local/__init__.py create mode 100644 maro/rl/local/local_learner.py create mode 100644 maro/rl/policy/policy_manager.py create mode 100644 maro/rl/policy/trainer.py create mode 100644 maro/rl/synchronous/__init__.py create mode 100644 maro/rl/synchronous/learner.py create mode 100644 maro/rl/synchronous/rollout_manager.py create mode 100644 maro/rl/synchronous/rollout_worker.py create mode 100644 maro/rl/utils/message_enums.py diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index 308aed9f8..faf61617a 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -5,42 +5,50 @@ DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls ) +from maro.rl.asynchronous import actor, policy_server +from maro.rl.early_stopping import AbsEarlyStopper from maro.rl.experience import AbsSampler, ExperienceManager, ExperienceSet, PrioritizedSampler from maro.rl.exploration import ( AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, UniformNoiseExploration ) +from maro.rl.local import LocalLearner from maro.rl.model import ( AbsBlock, AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, FullyConnectedBlock, OptimOption ) -from maro.rl.policy import AbsCorePolicy, AbsPolicy, NullPolicy -from maro.rl.training import ( - AbsEarlyStopper, AbsPolicyManager, AbsRolloutManager, Learner, LocalLearner, LocalPolicyManager, - LocalRolloutManager, MultiNodePolicyManager, MultiNodeRolloutManager, MultiProcessPolicyManager, - MultiProcessRolloutManager, rollout_worker_node, rollout_worker_process, trainer_node, trainer_process +from maro.rl.policy import ( + AbsCorePolicy, AbsPolicy, AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, + NullPolicy, trainer_node, trainer_process +) +from maro.rl.synchronous import ( + AbsRolloutManager, Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager, + rollout_worker_node, rollout_worker_process ) from maro.rl.utils import ( - get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, - get_torch_optim_cls, get_truncated_cumulative_reward + MsgKey, MsgTag, get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, + get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward ) from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper __all__ = [ "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", + "actor", "policy_server", + "AbsEarlyStopper", "AbsSampler", "ExperienceManager", "ExperienceSet", "PrioritizedSampler", "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", "UniformNoiseExploration", + "LocalLearner", "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "FullyConnectedBlock", "OptimOption", - "AbsCorePolicy", "AbsPolicy", "NullPolicy", - "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Learner", "LocalLearner", "LocalPolicyManager", - "LocalRolloutManager", "MultiNodePolicyManager", "MultiNodeRolloutManager", "MultiProcessPolicyManager", - "MultiProcessRolloutManager", "rollout_worker_node", "rollout_worker_process", "trainer_node", "trainer_process", - "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", + "AbsCorePolicy", "AbsPolicy", "AbsPolicyManager", "LocalPolicyManager", "MultiNodePolicyManager", + "MultiProcessPolicyManager", "NullPolicy", "trainer_node", "trainer_process", + "AbsRolloutManager", "Learner", "LocalRolloutManager", "MultiNodeRolloutManager", "MultiProcessRolloutManager", + "rollout_worker_node", "rollout_worker_process", + "MsgKey", "MsgTag", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward", "AbsEnvWrapper", "AgentWrapper" ] diff --git a/maro/rl/early_stopping/__init__.py b/maro/rl/early_stopping/__init__.py new file mode 100644 index 000000000..d517f73a1 --- /dev/null +++ b/maro/rl/early_stopping/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .early_stopper import AbsEarlyStopper + +__all__ = ["AbsEarlyStopper"] diff --git a/maro/rl/early_stopping/early_stopper.py b/maro/rl/early_stopping/early_stopper.py new file mode 100644 index 000000000..20cfb1561 --- /dev/null +++ b/maro/rl/early_stopping/early_stopper.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod + + +class AbsEarlyStopper(ABC): + def __init__(self): + super().__init__() + self.metric_history = [] + + def push(self, metric): + self.metric_history.append(metric) + + @abstractmethod + def stop(self) -> bool: + raise NotImplementedError diff --git a/maro/rl/local/__init__.py b/maro/rl/local/__init__.py new file mode 100644 index 000000000..93f6120b9 --- /dev/null +++ b/maro/rl/local/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .local_learner import LocalLearner + +__all__ = ["LocalLearner"] diff --git a/maro/rl/local/local_learner.py b/maro/rl/local/local_learner.py new file mode 100644 index 000000000..275970b37 --- /dev/null +++ b/maro/rl/local/local_learner.py @@ -0,0 +1,149 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from os import getcwd +from typing import List, Union + +from maro.rl.early_stopping import AbsEarlyStopper +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper +from maro.utils import Logger + + +class LocalLearner: + """Controller for single-threaded learning workflows. + + Args: + env_wrapper (AbsEnvWrapper): Environment wrapper instance to interact with a set of agents and collect + experiences for learning. + agent_wrapper (AgentWrapper): Multi-policy wrapper that interacts with the ``env_wrapper`` directly. + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on how the implementation of the roll-out manager. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will + will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated + at the end of the training episodes given in the list. In any case, the policies will be evaluated + at the end of the last training episode. Defaults to None, in which case the policies will only be + evaluated after the last training episode. + eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used + as the evaluation environment. Defaults to None. + early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the + environment metrics are met following an evaluation episode. Default to None. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + + def __init__( + self, + env_wrapper: AbsEnvWrapper, + agent_wrapper: AgentWrapper, + num_episodes: int, + num_steps: int = -1, + eval_schedule: Union[int, List[int]] = None, + eval_env: AbsEnvWrapper = None, + early_stopper: AbsEarlyStopper = None, + log_env_summary: bool = True, + log_dir: str = getcwd(), + ): + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + self.logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) + self.env = env_wrapper + self.eval_env = eval_env if eval_env else self.env + self.agent = agent_wrapper + + self.num_episodes = num_episodes + self._num_steps = num_steps if num_steps > 0 else float("inf") + + # evaluation schedule + if eval_schedule is None: + self._eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + else: + self._eval_schedule = eval_schedule + self._eval_schedule.sort() + + # always evaluate after the last episode + if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: + self._eval_schedule.append(num_episodes) + + self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") + self._eval_point_index = 0 + + self.early_stopper = early_stopper + + self._log_env_summary = log_env_summary + + def run(self): + """Entry point for executing a learning workflow.""" + for ep in range(1, self.num_episodes + 1): + self._train(ep) + if ep == self._eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + self._evaluate() + # early stopping check + if self.early_stopper: + self.early_stopper.push(self.eval_env.summary) + if self.early_stopper.stop(): + return + + def _train(self, ep: int): + """Collect simulation data for training.""" + t0 = time.time() + num_experiences_collected = 0 + + self.agent.explore() + self.env.reset() + self.env.start() # get initial state + segment = 0 + while self.env.state: + segment += 1 + exp_by_agent = self._collect(ep, segment) + self.agent.on_experiences(exp_by_agent) + num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) + # update the exploration parameters if an episode is finished + self.agent.exploration_step() + + # performance details + if self._log_env_summary: + self.logger.info(f"ep {ep}: {self.env.summary}") + + self.logger.info( + f"ep {ep} summary - " + f"running time: {time.time() - t0} " + f"env steps: {self.env.step_index} " + f"experiences collected: {num_experiences_collected}" + ) + + def _evaluate(self): + """Policy evaluation.""" + self.logger.info("Evaluating...") + self.agent.exploit() + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + self.eval_env.step(self.agent.choose_action(self.eval_env.state)) + + # performance details + self.logger.info(f"Evaluation result: {self.eval_env.summary}") + + def _collect(self, ep, segment): + start_step_index = self.env.step_index + 1 + steps_to_go = self._num_steps + while self.env.state and steps_to_go: + self.env.step(self.agent.choose_action(self.env.state)) + steps_to_go -= 1 + + self.logger.info( + f"Roll-out finished for ep {ep}, segment {segment}" + f"(steps {start_step_index} - {self.env.step_index})" + ) + + return self.env.get_experiences() diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 4d97b0289..5bc1e5124 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -2,5 +2,10 @@ # Licensed under the MIT license. from .policy import AbsCorePolicy, AbsPolicy, NullPolicy +from .policy_manager import AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager +from .trainer import trainer_node, trainer_process -__all__ = ["AbsCorePolicy", "AbsPolicy", "NullPolicy"] +__all__ = [ + "AbsCorePolicy", "AbsPolicy", "AbsPolicyManager", "LocalPolicyManager", "MultiNodePolicyManager", + "MultiProcessPolicyManager", "NullPolicy", "trainer_node", "trainer_process" +] diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py new file mode 100644 index 000000000..f9c691ff7 --- /dev/null +++ b/maro/rl/policy/policy_manager.py @@ -0,0 +1,219 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from abc import ABC, abstractmethod +from collections import defaultdict +from multiprocessing import Pipe, Process +from os import getcwd +from typing import Callable, Dict + +from maro.communication import Proxy, SessionMessage, SessionType +from maro.rl.experience import ExperienceSet +from maro.rl.policy import AbsCorePolicy +from maro.rl.utils import MsgKey, MsgTag +from maro.utils import Logger + +from .trainer import trainer_process + + +class AbsPolicyManager(ABC): + """Manage all policies. + + The actual policy instances may reside here or be distributed on a set of processes or remote nodes. + + Args: + policy_dict (Dict[str, AbsCorePolicy]): A list of policies managed by the manager. + """ + def __init__(self, policy_dict: Dict[str, AbsCorePolicy]): + for policy in policy_dict.values(): + if not isinstance(policy, AbsCorePolicy): + raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") + + super().__init__() + self.policy_dict = policy_dict + self.updated = set(self.policy_dict.keys()) + self._version = 0 + + @property + def version(self): + return self._version + + @abstractmethod + def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + """Logic for handling incoming experiences is implemented here.""" + raise NotImplementedError + + def get_state(self): + return {name: self.policy_dict[name].get_state() for name in self.updated} + + def reset_update_status(self): + self.updated.clear() + + +class LocalPolicyManager(AbsPolicyManager): + """Policy manager that contains the actual policy instances. + + Args: + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time + and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + def __init__(self, policy_dict: Dict[str, AbsCorePolicy], log_dir: str = getcwd()): + super().__init__(policy_dict) + self._logger = Logger("LOCAL_TRAINING_MANAGER", dump_folder=log_dir) + self._new_exp_counter = defaultdict(int) + + def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + """Store experiences and update policies if possible. + + The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding + policy's experience manager. Policies whose update conditions have been met will then be updated. + """ + t0 = time.time() + for policy_name, exp in exp_by_policy.items(): + if ( + isinstance(self.policy_dict[policy_name], AbsCorePolicy) and + self.policy_dict[policy_name].on_experiences(exp) + ): + self.updated.add(policy_name) + + if self.updated: + self._logger.info(f"Updated policies {self.updated}") + + self._logger.debug(f"policy update time: {time.time() - t0}") + + +class MultiProcessPolicyManager(AbsPolicyManager): + """Policy manager that spawns a set of trainer processes for parallel training. + + Args: + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. + policy2trainer (dict): Mapping from policy names to trainer IDs. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + def __init__( + self, + policy_dict: Dict[str, AbsCorePolicy], + num_trainers: int, + create_policy_func_dict: Dict[str, Callable], + log_dir: str = getcwd(), + ): + super().__init__(policy_dict) + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) + self._policy2trainer = {} + self._trainer2policies = defaultdict(list) + for i, name in enumerate(self.policy_dict): + trainer_id = i % num_trainers + self._policy2trainer[name] = f"TRAINER.{trainer_id}" + self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) + + self._trainer_processes = [] + self._manager_end = {} + for trainer_id, policy_names in self._trainer2policies.items(): + manager_end, trainer_end = Pipe() + self._manager_end[trainer_id] = manager_end + trainer = Process( + target=trainer_process, + args=( + trainer_id, + trainer_end, + {name: create_policy_func_dict[name] for name in policy_names}, + {name: self.policy_dict[name].get_state() for name in self._trainer2policies[trainer_id]} + ), + kwargs={"log_dir": log_dir} + ) + self._trainer_processes.append(trainer) + trainer.start() + + def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + for trainer_id, conn in self._manager_end.items(): + conn.send({ + "type": "train", + "experiences": {name: exp_by_policy[name] for name in self._trainer2policies[trainer_id]} + }) + + for conn in self._manager_end.values(): + result = conn.recv() + for policy_name, policy_state in result["policy"].items(): + self.policy_dict[policy_name].set_state(policy_state) + self.updated.add(policy_name) + + if self.updated: + self._version += 1 + + def exit(self): + """Tell the trainer processes to exit.""" + for conn in self._manager_end.values(): + conn.send({"type": "quit"}) + + +class MultiNodePolicyManager(AbsPolicyManager): + """Policy manager that communicates with a set of remote nodes for parallel training. + + Args: + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. + group (str): Group name for the training cluster, which includes all trainers and a training manager that + manages them. + num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where + 0 <= i < num_trainers. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + def __init__( + self, + policy_dict: Dict[str, AbsCorePolicy], + group: str, + num_trainers: int, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() + ): + super().__init__(policy_dict) + peers = {"trainer": num_trainers} + self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) + self._logger = Logger(self._proxy.name, dump_folder=log_dir) + + self._policy2trainer = {} + self._trainer2policies = defaultdict(list) + for i, name in enumerate(self.policy_dict): + trainer_id = i % num_trainers + self._policy2trainer[name] = f"TRAINER.{trainer_id}" + self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) + for trainer_name, policy_names in self._trainer2policies.items(): + self._proxy.send( + SessionMessage( + MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, + body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} + ) + ) + + def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + msg_body_by_dest = defaultdict(dict) + for policy_name, exp in exp_by_policy.items(): + trainer_id = self._policy2trainer[policy_name] + if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp + + for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): + for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): + self.policy_dict[policy_name].set_state(policy_state) + self.updated.add(policy_name) + + if self.updated: + self._version += 1 + + def exit(self): + """Tell the remote trainers to exit.""" + self._proxy.ibroadcast("trainer", MsgTag.EXIT, SessionType.NOTIFICATION) + self._proxy.close() + self._logger.info("Exiting...") diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py new file mode 100644 index 000000000..6394a6492 --- /dev/null +++ b/maro/rl/policy/trainer.py @@ -0,0 +1,100 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from multiprocessing.connection import Connection +from os import getcwd +from typing import Callable, Dict + +from maro.communication import Proxy +from maro.rl.utils import MsgKey, MsgTag +from maro.utils import Logger + + +def trainer_process( + trainer_id: int, + conn: Connection, + create_policy_func_dict: Dict[str, Callable], + initial_policy_states: dict, + log_dir: str = getcwd() +): + """Policy trainer process which can be spawned by a ``MultiProcessPolicyManager``. + + Args: + trainer_id (int): Integer trainer ID. + conn (Connection): Connection end for exchanging messages with the manager process. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} + logger = Logger("TRAINER", dump_folder=log_dir) + for name, state in initial_policy_states.items(): + policy_dict[name].set_state(state) + logger.info(f"{trainer_id} initialized policy {name}") + + while True: + msg = conn.recv() + if msg["type"] == "train": + t0 = time.time() + updated = { + name: policy_dict[name].get_state() for name, exp in msg["experiences"].items() + if policy_dict[name].on_experiences(exp) + } + logger.debug(f"total policy update time: {time.time() - t0}") + conn.send({"policy": updated}) + elif msg["type"] == "get_policy_state": + policy_state_dict = {name: policy.get_state() for name, policy in policy_dict.items()} + conn.send({"policy": policy_state_dict}) + elif msg["type"] == "quit": + break + + +def trainer_node( + group: str, + trainer_idx: int, + create_policy_func_dict: Dict[str, Callable], + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + """Policy trainer process that can be launched on separate computation nodes. + + Args: + group (str): Group name for the training cluster, which includes all trainers and a training manager that + manages them. + trainer_idx (int): Integer trainer index. The trainer's ID in the cluster will be "TRAINER.{trainer_idx}". + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + policy_dict = {} + proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=f"TRAINER.{trainer_idx}", **proxy_kwargs) + logger = Logger(proxy.name, dump_folder=log_dir) + + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.INIT_POLICY_STATE: + for name, state in msg.body[MsgKey.POLICY_STATE].items(): + policy_dict[name] = create_policy_func_dict[name]() + policy_dict[name].set_state(state) + logger.info(f"{proxy.name} initialized policy {name}") + proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) + elif msg.tag == MsgTag.TRAIN: + t0 = time.time() + msg_body = { + MsgKey.POLICY_STATE: { + name: policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() + if policy_dict[name].on_experiences(exp) + } + } + logger.info(f"updated policies {list(msg_body[MsgKey.POLICY_STATE].keys())}") + logger.debug(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, body=msg_body) diff --git a/maro/rl/synchronous/__init__.py b/maro/rl/synchronous/__init__.py new file mode 100644 index 000000000..ab4182fae --- /dev/null +++ b/maro/rl/synchronous/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .learner import Learner +from .rollout_manager import AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager +from .rollout_worker import rollout_worker_node, rollout_worker_process + +__all__ = [ + "AbsEarlyStopper", "AbsRolloutManager", "Learner", "LocalLearner", "LocalRolloutManager", "MultiNodeRolloutManager", + "MultiProcessRolloutManager", "rollout_worker_node", "rollout_worker_process" +] diff --git a/maro/rl/synchronous/learner.py b/maro/rl/synchronous/learner.py new file mode 100644 index 000000000..7b1af82ec --- /dev/null +++ b/maro/rl/synchronous/learner.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from os import getcwd +from typing import List, Union + +from maro.rl.early_stopping import AbsEarlyStopper +from maro.rl.policy import AbsPolicyManager +from maro.utils import Logger + +from .rollout_manager import AbsRolloutManager + + +class Learner: + """Main controller for learning. + + This should be used in multi-process or distributed settings where either the policy manager or the roll-out + manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such + as duplicate experience storage. Use ``LocalLearner`` instead. + + Args: + policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. + rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data + collection. + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on the implementation of the roll-out manager. + eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will + will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated + at the end of the training episodes given in the list. In any case, the policies will be evaluated + at the end of the last training episode. Defaults to None, in which case the policies will only be + evaluated after the last training episode. + early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the + environment metric are met following an evaluation episode. Default to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time + and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + end_of_episode_kwargs: Keyword arguments for custom end-of-episode processing. + """ + def __init__( + self, + policy_manager: AbsPolicyManager, + rollout_manager: AbsRolloutManager, + num_episodes: int, + eval_schedule: Union[int, List[int]] = None, + early_stopper: AbsEarlyStopper = None, + log_dir: str = getcwd(), + **end_of_episode_kwargs + ): + self.logger = Logger("LEARNER", dump_folder=log_dir) + self.policy_manager = policy_manager + self.rollout_manager = rollout_manager + + self.num_episodes = num_episodes + + # evaluation schedule + if eval_schedule is None: + self._eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + else: + self._eval_schedule = eval_schedule + self._eval_schedule.sort() + + # always evaluate after the last episode + if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: + self._eval_schedule.append(num_episodes) + + self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") + self._eval_point_index = 0 + + self.early_stopper = early_stopper + + self._end_of_episode_kwargs = end_of_episode_kwargs + self._last_step_set = {} + + def run(self): + """Entry point for executing a learning workflow.""" + for ep in range(1, self.num_episodes + 1): + self._train(ep) + if ep == self._eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) + # performance details + self.logger.info(f"Evaluation result: {env_metric_dict}") + # early stopping check + if self.early_stopper: + for env_metric in env_metric_dict.values(): + self.early_stopper.push(env_metric) + if self.early_stopper.stop(): + return + + if hasattr(self.rollout_manager, "exit"): + self.rollout_manager.exit() + + if hasattr(self.policy_manager, "exit"): + self.policy_manager.exit() + + def _train(self, ep: int): + collect_time = policy_update_time = num_experiences_collected = 0 + segment = 0 + self.rollout_manager.reset() + while not self.rollout_manager.episode_complete: + segment += 1 + # experience collection + policy_state_dict = self.policy_manager.get_state() + self.policy_manager.reset_update_status() + policy_version = self.policy_manager.version + tc0 = time.time() + exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) + collect_time += time.time() - tc0 + tu0 = time.time() + self.policy_manager.on_experiences(exp_by_policy) + policy_update_time += time.time() - tu0 + num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) + + # performance details + self.logger.info( + f"ep {ep} summary - " + f"experiences collected: {num_experiences_collected} " + f"experience collection time: {collect_time} " + f"policy update time: {policy_update_time}" + ) + + self.end_of_episode(ep, **self._end_of_episode_kwargs) + + def end_of_episode(self, ep: int, **kwargs): + """Custom end-of-episode processing is implemented here.""" + pass diff --git a/maro/rl/synchronous/rollout_manager.py b/maro/rl/synchronous/rollout_manager.py new file mode 100644 index 000000000..eaffa2bef --- /dev/null +++ b/maro/rl/synchronous/rollout_manager.py @@ -0,0 +1,499 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from abc import ABC, abstractmethod +from collections import defaultdict +from multiprocessing import Pipe, Process +from os import getcwd +from random import choices +from typing import Callable + +from maro.communication import Proxy, SessionType +from maro.rl.experience import ExperienceSet +from maro.rl.utils import MsgKey, MsgTag +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper +from maro.utils import Logger + +from .rollout_worker import rollout_worker_process + + +class AbsRolloutManager(ABC): + """Controller for simulation data collection.""" + def __init__(self): + super().__init__() + self.episode_complete = False + + @abstractmethod + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): + """Collect simulation data, i.e., experiences for training. + + Args: + ep (int): Current episode index. + segment (int): Current segment index. + policy_state_dict (dict): Policy states to use for simulation. + version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. + + Returns: + Experiences for policy training. + """ + raise NotImplementedError + + @abstractmethod + def evaluate(self, ep: int, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``. + + Args: + ep (int): Current training episode index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Environment summary. + """ + raise NotImplementedError + + def reset(self): + self.episode_complete = False + + +class LocalRolloutManager(AbsRolloutManager): + """Local roll-out controller. + + Args: + env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect + experiences for policy training / update. + agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + eval_env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be + used as the evaluation environment. Defaults to None. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + + def __init__( + self, + env_wrapper: AbsEnvWrapper, + agent_wrapper: AgentWrapper, + num_steps: int = -1, + eval_env_wrapper: AbsEnvWrapper = None, + log_env_summary: bool = True, + log_dir: str = getcwd(), + ): + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + super().__init__() + self._logger = Logger("LOCAL_ROLLOUT_MANAGER", dump_folder=log_dir) + + self.env = env_wrapper + self.eval_env = eval_env_wrapper if eval_env_wrapper else self.env + self.agent = agent_wrapper + + self._num_steps = num_steps if num_steps > 0 else float("inf") + self._log_env_summary = log_env_summary + + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): + """Collect simulation data, i.e., experiences for training. + + Args: + ep (int): Current episode index. + segment (int): Current segment index. + policy_state_dict (dict): Policy states to use for simulation. + version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. + + Returns: + Experiences for policy training. + """ + self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") + t0 = time.time() + learning_time = 0 + num_experiences_collected = 0 + + # start of new episode + if self.env.state is None: + self.env.reset() + self.env.start() # get initial state + self.agent.exploration_step() + + # set policy states + self.agent.set_policy_states(policy_state_dict) + # update exploration parameters + self.agent.explore() + + start_step_index = self.env.step_index + 1 + steps_to_go = self._num_steps + while self.env.state and steps_to_go > 0: + action = self.agent.choose_action(self.env.state) + self.env.step(action) + steps_to_go -= 1 + + self._logger.info( + f"Roll-out finished for ep {ep}, segment {segment}" + f"(steps {start_step_index} - {self.env.step_index})" + ) + + # update the exploration parameters if an episode is finished + if not self.env.state: + self.episode_complete = True + # performance details + if self._log_env_summary: + self._logger.info(f"ep {ep}: {self.env.summary}") + + self._logger.debug( + f"ep {ep} summary - " + f"running time: {time.time() - t0} " + f"env steps: {self.env.step_index} " + f"learning time: {learning_time} " + f"experiences collected: {num_experiences_collected}" + ) + + return self.env.get_experiences() + + def evaluate(self, ep: int, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``. + + Args: + ep (int): Current training episode index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Environment summary. + """ + self._logger.info("Evaluating...") + self.agent.set_policy_states(policy_state_dict) + self.agent.exploit() + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + action = self.agent.choose_action(self.eval_env.state) + self.eval_env.step(action) + + if self._log_env_summary: + self._logger.info(f"Evaluation result: {self.eval_env.summary}") + + return self.eval_env.summary + + +class MultiProcessRolloutManager(AbsRolloutManager): + """Roll-out manager that spawns a set of roll-out worker processes for parallel data collection. + + Args: + num_workers (int): Number of remote roll-out workers. + create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an + environment wrapper for training data collection. The function should take no parameters and return an + environment wrapper instance. + create_agent_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create a + decision generator for interacting with the environment. The function should take no parameters and return + a ``AgentWrapper`` instance. + create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an + environment wrapper for evaluation. The function should take no parameters and return an environment + wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the + worker processes. Defaults to None. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + num_eval_workers (int): Number of workers for evaluation. Defaults to 1. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + def __init__( + self, + num_workers: int, + create_env_wrapper_func: Callable[[], AbsEnvWrapper], + create_agent_wrapper_func: Callable[[], AgentWrapper], + create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, + num_steps: int = -1, + num_eval_workers: int = 1, + log_env_summary: bool = True, + log_dir: str = getcwd(), + ): + super().__init__() + self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) + self._num_workers = num_workers + self._num_steps = num_steps + self._log_env_summary = log_env_summary + self._num_eval_workers = num_eval_workers + self.total_experiences_collected = 0 + self.total_env_steps = 0 + self._exploration_step = False + + self._worker_processes = [] + self._manager_ends = [] + for index in range(self._num_workers): + manager_end, worker_end = Pipe() + self._manager_ends.append(manager_end) + worker = Process( + target=rollout_worker_process, + args=( + index, + worker_end, + create_env_wrapper_func, + create_agent_wrapper_func, + ), + kwargs={ + "create_eval_env_wrapper_func": create_eval_env_wrapper_func, + "log_dir": log_dir + } + ) + self._worker_processes.append(worker) + worker.start() + + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): + """Collect simulation data, i.e., experiences for training. + + Args: + ep (int): Current episode index. + segment (int): Current segment index. + policy_state_dict (dict): Policy states to use for simulation. + version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. + + Returns: + Experiences for policy training. + """ + rollout_req = { + "type": "collect", + "episode": ep, + "segment": segment, + "num_steps": self._num_steps, + "policy": policy_state_dict, + "exploration_step": self._exploration_step + } + + for conn in self._manager_ends: + conn.send(rollout_req) + + self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") + + if self._exploration_step: + self._exploration_step = False + + combined_exp_by_policy = defaultdict(ExperienceSet) + for conn in self._manager_ends: + result = conn.recv() + exp_by_policy = result["experiences"] + self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) + self.total_env_steps += result["num_steps"] + + for policy_name, exp in exp_by_policy.items(): + combined_exp_by_policy[policy_name].extend(exp) + + # log roll-out summary + self.episode_complete = result["episode_end"] + if self.episode_complete and self._log_env_summary: + env_summary = result["env_summary"] + self._logger.info(f"env summary: {env_summary}") + + if self.episode_complete: + self._exploration_step = True + + return combined_exp_by_policy + + def evaluate(self, ep: int, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``. + + Args: + ep (int): Current training episode index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Environment summary. + """ + eval_worker_conns = choices(self._manager_ends, k=self._num_eval_workers) + for conn in eval_worker_conns: + conn.send({"type": "evaluate", "episode": ep, "policy": policy_state_dict}) + + env_summary_dict = {} + for conn in self._manager_ends: + result = conn.recv() + env_summary_dict[result["worker_id"]] = result["env_summary"] + + return env_summary_dict + + def exit(self): + """Tell the worker processes to exit.""" + for conn in self._manager_ends: + conn.send({"type": "quit"}) + + +class MultiNodeRolloutManager(AbsRolloutManager): + """Controller for a set of remote roll-out workers, possibly distributed on different computation nodes. + + Args: + group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager + that manages them. + num_workers (int): Number of remote roll-out workers. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to + None, in which case the number is set to ``num_workers``. + receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the workers. This + This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the + desired amount of data from workers. Defaults to None, in which case each receive attempt is blocking. + max_lag (int): Maximum policy version lag allowed for experiences collected from remote roll-out workers. + Experiences collected using policy versions older than (current_version - max_lag) will be discarded. + Defaults to 0, in which case only experiences collected using the latest policy version will be returned. + num_eval_workers (int): Number of workers for evaluation. Defaults to 1. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + def __init__( + self, + group: str, + num_workers: int, + num_steps: int = -1, + max_receive_attempts: int = None, + receive_timeout: int = None, + max_lag: int = 0, + num_eval_workers: int = 1, + log_env_summary: bool = True, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() + ): + if num_eval_workers > num_workers: + raise ValueError("num_eval_workers cannot exceed the number of available workers") + + super().__init__() + self.num_workers = num_workers + peers = {"rollout_worker": num_workers} + self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) + self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's + self._logger = Logger(self._proxy.name, dump_folder=log_dir) + + self._num_steps = num_steps + + if max_receive_attempts is None: + max_receive_attempts = self.num_workers + self._logger.info(f"Maximum receive attempts is set to {max_receive_attempts}") + + self.max_receive_attempts = max_receive_attempts + self.receive_timeout = receive_timeout + + self._max_lag = max_lag + self.total_experiences_collected = 0 + self.total_env_steps = 0 + self._log_env_summary = log_env_summary + + self._num_eval_workers = num_eval_workers + + self._exploration_step = False + + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): + """Collect simulation data, i.e., experiences for training. + + Args: + ep (int): Current episode index. + segment (int): Current segment index. + policy_state_dict (dict): Policy states to use for simulation. + version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. + + Returns: + Experiences for policy training. + """ + msg_body = { + MsgKey.EPISODE: ep, + MsgKey.SEGMENT: segment, + MsgKey.NUM_STEPS: self._num_steps, + MsgKey.POLICY_STATE: policy_state_dict, + MsgKey.VERSION: version, + MsgKey.EXPLORATION_STEP: self._exploration_step + } + + self._proxy.ibroadcast("rollout_worker", MsgTag.COLLECT, SessionType.TASK, body=msg_body) + self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") + + if self._exploration_step: + self._exploration_step = False + + # Receive roll-out results from remote workers + combined_exp_by_policy = defaultdict(ExperienceSet) + num_finishes = 0 + for _ in range(self.max_receive_attempts): + msg = self._proxy.receive_once(timeout=self.receive_timeout) + if msg.tag != MsgTag.COLLECT_DONE: + self._logger.info( + f"Ignored a message of type {msg.tag} (expected message type {MsgTag.COLLECT_DONE})" + ) + continue + + if version - msg.body[MsgKey.VERSION] > self._max_lag: + self._logger.info( + f"Ignored a message because it contains experiences generated using a stale policy version. " + f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " + f"got {msg.body[MsgKey.VERSION]}" + ) + continue + + exp_by_policy = msg.body[MsgKey.EXPERIENCES] + self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) + self.total_env_steps += msg.body[MsgKey.NUM_STEPS] + + for policy_name, exp in exp_by_policy.items(): + combined_exp_by_policy[policy_name].extend(exp) + + if msg.body[MsgKey.SEGMENT] == segment: + self.episode_complete = msg.body[MsgKey.EPISODE_END] + if self.episode_complete: + # log roll-out summary + if self._log_env_summary: + self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") + num_finishes += 1 + if num_finishes == self.num_workers: + break + + if self.episode_complete: + self._exploration_step = True + + return combined_exp_by_policy + + def evaluate(self, ep: int, policy_state_dict: dict): + """Evaluate the performance of ``policy_state_dict``. + + Args: + ep (int): Current training episode index. + policy_state_dict (dict): Policy states to use for simulation. + + Returns: + Environment summary. + """ + msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY_STATE: policy_state_dict} + + workers = choices(self._workers, k=self._num_eval_workers) + env_summary_dict = {} + self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(worker_id, msg_body) for worker_id in workers]) + self._logger.info(f"Sent evaluation requests to {workers}") + + # Receive roll-out results from remote workers + num_finishes = 0 + for msg in self._proxy.receive(): + if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE] != ep: + self._logger.info( + f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE]} " + f"(expected message type {MsgTag.EVAL_DONE} and episode index {ep})" + ) + continue + + env_summary_dict[msg.source] = msg.body[MsgKey.ENV_SUMMARY] + + if msg.body[MsgKey.EPISODE] == ep: + num_finishes += 1 + if num_finishes == self._num_eval_workers: + break + + return env_summary_dict + + def exit(self): + """Tell the remote workers to exit.""" + self._proxy.ibroadcast("rollout_worker", MsgTag.EXIT, SessionType.NOTIFICATION) + self._proxy.close() + self._logger.info("Exiting...") diff --git a/maro/rl/synchronous/rollout_worker.py b/maro/rl/synchronous/rollout_worker.py new file mode 100644 index 000000000..5ab0baed0 --- /dev/null +++ b/maro/rl/synchronous/rollout_worker.py @@ -0,0 +1,210 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from multiprocessing.connection import Connection +from os import getcwd +from typing import Callable + +from maro.communication import Proxy +from maro.rl.utils import MsgKey, MsgTag +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper +from maro.utils import Logger, set_seeds + + +def rollout_worker_process( + index: int, + conn: Connection, + create_env_wrapper_func: Callable[[], AbsEnvWrapper], + create_agent_wrapper_func: Callable[[], AgentWrapper], + create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, + log_dir: str = getcwd() +): + """Roll-out worker process that can be spawned by a ``MultiProcessRolloutManager``. + + Args: + index (int): Index for the worker process. This is used for bookkeeping by the parent manager process. + conn (Connection): Connection end for exchanging messages with the manager process. + create_env_wrapper_func (Callable): Function to create an environment wrapper for training data collection. + The function should take no parameters and return an environment wrapper instance. + create_agent_wrapper_func (Callable): Function to create a decision generator for interacting with + the environment. The function should take no parameters and return a ``AgentWrapper`` instance. + create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function + should take no parameters and return an environment wrapper instance. If this is None, the training + environment wrapper will be used for evaluation. Defaults to None. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + set_seeds(index) + env_wrapper = create_env_wrapper_func() + eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() + agent_wrapper = create_agent_wrapper_func() + logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) + + def collect(msg): + ep, segment = msg["episode"], msg["segment"] + # set policy states + agent_wrapper.set_policy_states(msg["policy"]) + + # update exploration parameters + agent_wrapper.explore() + if msg["exploration_step"]: + agent_wrapper.exploration_step() + + if env_wrapper.state is None: + logger.info(f"Training episode {ep}") + env_wrapper.reset() + env_wrapper.start() # get initial state + + starting_step_index = env_wrapper.step_index + 1 + steps_to_go = float("inf") if msg["num_steps"] == -1 else msg["num_steps"] + while env_wrapper.state and steps_to_go > 0: + action = agent_wrapper.choose_action(env_wrapper.state) + env_wrapper.step(action) + steps_to_go -= 1 + + logger.info( + f"Roll-out finished (episode {ep}, segment {segment}, " + f"steps {starting_step_index} - {env_wrapper.step_index})" + ) + + policies_with_new_exp = agent_wrapper.on_experiences(env_wrapper.get_experiences()) + ret_exp = agent_wrapper.get_experiences_by_policy(policies_with_new_exp) + + return_info = { + "worker_index": index, + "episode_end": not env_wrapper.state, + "experiences": ret_exp, + "env_summary": env_wrapper.summary, + "num_steps": env_wrapper.step_index - starting_step_index + 1 + } + + conn.send(return_info) + + def evaluate(msg): + logger.info("Evaluating...") + agent_wrapper.set_policy_states(msg["policy"]) + agent_wrapper.exploit() + eval_env_wrapper.reset() + eval_env_wrapper.start() # get initial state + while eval_env_wrapper.state: + action = agent_wrapper.choose_action(eval_env_wrapper.state) + eval_env_wrapper.step(action) + + conn.send({"worker_id": index, "env_summary": eval_env_wrapper.summary}) + + while True: + msg = conn.recv() + if msg["type"] == "collect": + collect(msg) + elif msg["type"] == "evaluate": + evaluate(msg) + elif msg["type"] == "quit": + break + + +def rollout_worker_node( + group: str, + worker_id: int, + env_wrapper: AbsEnvWrapper, + agent_wrapper: AgentWrapper, + eval_env_wrapper: AbsEnvWrapper = None, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + """Roll-out worker process that can be launched on separate computation nodes. + + Args: + group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager + that manages them. + worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". + This is used for bookkeeping by the parent manager. + env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. + agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. + eval_env_wrapper (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training + environment wrapper will be used for evaluation. Defaults to None. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + eval_env_wrapper = env_wrapper if not eval_env_wrapper else eval_env_wrapper + + proxy = Proxy( + group, "rollout_worker", {"rollout_manager": 1}, + component_name=f"ROLLOUT_WORKER.{int(worker_id)}", **proxy_kwargs + ) + logger = Logger(proxy.name, dump_folder=log_dir) + + def collect(msg): + ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] + + # set policy states + agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) + + # set exploration parameters + agent_wrapper.explore() + if msg.body[MsgKey.EXPLORATION_STEP]: + agent_wrapper.exploration_step() + + if env_wrapper.state is None: + logger.info(f"Training episode {msg.body[MsgKey.EPISODE]}") + env_wrapper.reset() + env_wrapper.start() # get initial state + + starting_step_index = env_wrapper.step_index + 1 + steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] + while env_wrapper.state and steps_to_go > 0: + action = agent_wrapper.choose_action(env_wrapper.state) + env_wrapper.step(action) + steps_to_go -= 1 + + logger.info( + f"Roll-out finished (episode {ep}, segment {segment}, " + f"steps {starting_step_index} - {env_wrapper.step_index})" + ) + + policy_names = agent_wrapper.on_experiences(env_wrapper.get_experiences()) + ret_exp = agent_wrapper.get_experiences_by_policy(policy_names) + + return_info = { + MsgKey.EPISODE_END: not env_wrapper.state, + MsgKey.EPISODE: ep, + MsgKey.SEGMENT: segment, + MsgKey.VERSION: msg.body[MsgKey.VERSION], + MsgKey.EXPERIENCES: ret_exp, + MsgKey.ENV_SUMMARY: env_wrapper.summary, + MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1 + } + + proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) + + def evaluate(msg): + logger.info("Evaluating...") + agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) + agent_wrapper.exploit() + eval_env_wrapper.reset() + eval_env_wrapper.start() # get initial state + while eval_env_wrapper.state: + action = agent_wrapper.choose_action(eval_env_wrapper.state) + eval_env_wrapper.step(action) + + return_info = {MsgKey.ENV_SUMMARY: eval_env_wrapper.summary, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} + proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) + + """ + The event loop handles 3 types of messages from the roll-out manager: + 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps + and the collected experiences will be sent back to the roll-out manager; + 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire + duration of the evaluation environment. + 3) EXIT, upon which it will break out of the event loop and the process will terminate. + + """ + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.COLLECT: + collect(msg) + elif msg.tag == MsgTag.EVAL: + evaluate(msg) diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 4797c39c9..b853032d4 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,12 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .message_enums import MsgKey, MsgTag from .torch_cls_index import ( get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls ) from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_truncated_cumulative_reward __all__ = [ - "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", "get_torch_optim_cls", - "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward" + "MsgKey", "MsgTag", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", + "get_torch_optim_cls", "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward" ] diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py new file mode 100644 index 000000000..97fb125d5 --- /dev/null +++ b/maro/rl/utils/message_enums.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from enum import Enum + + +class MsgTag(Enum): + COLLECT = "rollout" + EVAL = "eval" + INIT_POLICY_STATE = "init_policy_state" + INIT_POLICY_STATE_DONE = "init_policy_state_done" + GET_INITIAL_POLICY_STATE = "get_initial_policy_state" + POLICY_STATE = "policy_state" + CHOOSE_ACTION = "choose_action" + ACTION = "action" + TRAIN = "train" + ABORT_ROLLOUT = "abort_rollout" + EVAL_DONE = "eval_done" + COLLECT_DONE = "collect_done" + DONE = "done" + EXIT = "exit" + + +class MsgKey(Enum): + ACTION = "action" + AGENT_ID = "agent_id" + EPISODE = "episode" + SEGMENT = "segment" + STEP = "step" + ENV_SUMMARY = "env_summary" + EXPERIENCES = "experiences" + NUM_EXPERIENCES = "num_experiences" + STATE = "state" + POLICY_STATE = "policy_state" + EXPLORATION_STEP = "exploration_step" + VERSION = "version" + NUM_STEPS = "num_steps" + EPISODE_END = "episode_end" From d2b433e8bbba68fb9bbc45a24386dd8441e3d713 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Jul 2021 13:36:16 +0800 Subject: [PATCH 347/482] refactored rl toolkit folder structure --- maro/rl/__init__.py | 52 -- maro/rl/early_stopping/__init__.py | 6 - maro/rl/{local => learning}/__init__.py | 4 - .../early_stopper.py | 0 .../simple_learner.py} | 4 +- .../sync_tools => learning/sync}/__init__.py | 3 +- .../{synchronous => learning/sync}/learner.py | 4 +- .../sync}/rollout_manager.py | 0 .../sync}/rollout_worker.py | 0 maro/rl/local/local_learner.py | 149 ------ maro/rl/synchronous/__init__.py | 11 - maro/rl/training/__init__.py | 20 - maro/rl/training/early_stopper.py | 17 - maro/rl/training/message_enums.py | 37 -- maro/rl/training/policy_manager/__init__.py | 10 - .../training/policy_manager/policy_manager.py | 219 -------- maro/rl/training/policy_manager/trainer.py | 101 ---- maro/rl/training/sync_tools/learner.py | 130 ----- .../rl/training/sync_tools/rollout_manager.py | 499 ------------------ maro/rl/training/sync_tools/rollout_worker.py | 211 -------- .../rl_formulation.ipynb | 4 +- 21 files changed, 7 insertions(+), 1474 deletions(-) delete mode 100644 maro/rl/early_stopping/__init__.py rename maro/rl/{local => learning}/__init__.py (51%) rename maro/rl/{early_stopping => learning}/early_stopper.py (100%) rename maro/rl/{training/local_learner.py => learning/simple_learner.py} (98%) rename maro/rl/{training/sync_tools => learning/sync}/__init__.py (75%) rename maro/rl/{synchronous => learning/sync}/learner.py (97%) rename maro/rl/{synchronous => learning/sync}/rollout_manager.py (100%) rename maro/rl/{synchronous => learning/sync}/rollout_worker.py (100%) delete mode 100644 maro/rl/local/local_learner.py delete mode 100644 maro/rl/synchronous/__init__.py delete mode 100644 maro/rl/training/__init__.py delete mode 100644 maro/rl/training/early_stopper.py delete mode 100644 maro/rl/training/message_enums.py delete mode 100644 maro/rl/training/policy_manager/__init__.py delete mode 100644 maro/rl/training/policy_manager/policy_manager.py delete mode 100644 maro/rl/training/policy_manager/trainer.py delete mode 100644 maro/rl/training/sync_tools/learner.py delete mode 100644 maro/rl/training/sync_tools/rollout_manager.py delete mode 100644 maro/rl/training/sync_tools/rollout_worker.py diff --git a/maro/rl/__init__.py b/maro/rl/__init__.py index faf61617a..9a0454564 100644 --- a/maro/rl/__init__.py +++ b/maro/rl/__init__.py @@ -1,54 +1,2 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - -from maro.rl.algorithms import ( - DDPG, DQN, ActorCritic, ActorCriticConfig, DDPGConfig, DQNConfig, PolicyGradient, PolicyGradientConfig, - get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls -) -from maro.rl.asynchronous import actor, policy_server -from maro.rl.early_stopping import AbsEarlyStopper -from maro.rl.experience import AbsSampler, ExperienceManager, ExperienceSet, PrioritizedSampler -from maro.rl.exploration import ( - AbsExploration, AbsExplorationScheduler, EpsilonGreedyExploration, GaussianNoiseExploration, - LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler, NoiseExploration, NullExploration, - UniformNoiseExploration -) -from maro.rl.local import LocalLearner -from maro.rl.model import ( - AbsBlock, AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, FullyConnectedBlock, - OptimOption -) -from maro.rl.policy import ( - AbsCorePolicy, AbsPolicy, AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, - NullPolicy, trainer_node, trainer_process -) -from maro.rl.synchronous import ( - AbsRolloutManager, Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager, - rollout_worker_node, rollout_worker_process -) -from maro.rl.utils import ( - MsgKey, MsgTag, get_k_step_returns, get_lambda_returns, get_torch_activation_cls, get_torch_loss_cls, - get_torch_lr_scheduler_cls, get_torch_optim_cls, get_truncated_cumulative_reward -) -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper - -__all__ = [ - "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", - "PolicyGradientConfig", "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls", - "actor", "policy_server", - "AbsEarlyStopper", - "AbsSampler", "ExperienceManager", "ExperienceSet", "PrioritizedSampler", - "AbsExploration", "AbsExplorationScheduler", "EpsilonGreedyExploration", "GaussianNoiseExploration", - "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "NoiseExploration", "NullExploration", - "UniformNoiseExploration", - "LocalLearner", - "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", - "FullyConnectedBlock", "OptimOption", - "AbsCorePolicy", "AbsPolicy", "AbsPolicyManager", "LocalPolicyManager", "MultiNodePolicyManager", - "MultiProcessPolicyManager", "NullPolicy", "trainer_node", "trainer_process", - "AbsRolloutManager", "Learner", "LocalRolloutManager", "MultiNodeRolloutManager", "MultiProcessRolloutManager", - "rollout_worker_node", "rollout_worker_process", - "MsgKey", "MsgTag", "get_k_step_returns", "get_lambda_returns", "get_torch_activation_cls", "get_torch_loss_cls", - "get_torch_lr_scheduler_cls", "get_torch_optim_cls", "get_truncated_cumulative_reward", - "AbsEnvWrapper", "AgentWrapper" -] diff --git a/maro/rl/early_stopping/__init__.py b/maro/rl/early_stopping/__init__.py deleted file mode 100644 index d517f73a1..000000000 --- a/maro/rl/early_stopping/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .early_stopper import AbsEarlyStopper - -__all__ = ["AbsEarlyStopper"] diff --git a/maro/rl/local/__init__.py b/maro/rl/learning/__init__.py similarity index 51% rename from maro/rl/local/__init__.py rename to maro/rl/learning/__init__.py index 93f6120b9..9a0454564 100644 --- a/maro/rl/local/__init__.py +++ b/maro/rl/learning/__init__.py @@ -1,6 +1,2 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - -from .local_learner import LocalLearner - -__all__ = ["LocalLearner"] diff --git a/maro/rl/early_stopping/early_stopper.py b/maro/rl/learning/early_stopper.py similarity index 100% rename from maro/rl/early_stopping/early_stopper.py rename to maro/rl/learning/early_stopper.py diff --git a/maro/rl/training/local_learner.py b/maro/rl/learning/simple_learner.py similarity index 98% rename from maro/rl/training/local_learner.py rename to maro/rl/learning/simple_learner.py index a4feb406a..20626b34d 100644 --- a/maro/rl/training/local_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -8,10 +8,10 @@ from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger -from early_stopper import AbsEarlyStopper +from .early_stopper import AbsEarlyStopper -class LocalLearner: +class SimpleLearner: """Controller for single-threaded learning workflows. Args: diff --git a/maro/rl/training/sync_tools/__init__.py b/maro/rl/learning/sync/__init__.py similarity index 75% rename from maro/rl/training/sync_tools/__init__.py rename to maro/rl/learning/sync/__init__.py index f6a00e4a3..e54f8cea7 100644 --- a/maro/rl/training/sync_tools/__init__.py +++ b/maro/rl/learning/sync/__init__.py @@ -6,7 +6,6 @@ from .rollout_worker import rollout_worker_node, rollout_worker_process __all__ = [ - "Learner", - "AbsRolloutManager", "LocalRolloutManager", "MultiProcessRolloutManager", "MultiNodeRolloutManager", + "AbsRolloutManager", "Learner", "LocalRolloutManager", "MultiNodeRolloutManager", "MultiProcessRolloutManager", "rollout_worker_node", "rollout_worker_process" ] diff --git a/maro/rl/synchronous/learner.py b/maro/rl/learning/sync/learner.py similarity index 97% rename from maro/rl/synchronous/learner.py rename to maro/rl/learning/sync/learner.py index 7b1af82ec..f722061a1 100644 --- a/maro/rl/synchronous/learner.py +++ b/maro/rl/learning/sync/learner.py @@ -5,10 +5,10 @@ from os import getcwd from typing import List, Union -from maro.rl.early_stopping import AbsEarlyStopper from maro.rl.policy import AbsPolicyManager from maro.utils import Logger +from ..early_stopper import AbsEarlyStopper from .rollout_manager import AbsRolloutManager @@ -17,7 +17,7 @@ class Learner: This should be used in multi-process or distributed settings where either the policy manager or the roll-out manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such - as duplicate experience storage. Use ``LocalLearner`` instead. + as duplicate experience storage. Use ``SimpleLearner`` instead. Args: policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. diff --git a/maro/rl/synchronous/rollout_manager.py b/maro/rl/learning/sync/rollout_manager.py similarity index 100% rename from maro/rl/synchronous/rollout_manager.py rename to maro/rl/learning/sync/rollout_manager.py diff --git a/maro/rl/synchronous/rollout_worker.py b/maro/rl/learning/sync/rollout_worker.py similarity index 100% rename from maro/rl/synchronous/rollout_worker.py rename to maro/rl/learning/sync/rollout_worker.py diff --git a/maro/rl/local/local_learner.py b/maro/rl/local/local_learner.py deleted file mode 100644 index 275970b37..000000000 --- a/maro/rl/local/local_learner.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import List, Union - -from maro.rl.early_stopping import AbsEarlyStopper -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper -from maro.utils import Logger - - -class LocalLearner: - """Controller for single-threaded learning workflows. - - Args: - env_wrapper (AbsEnvWrapper): Environment wrapper instance to interact with a set of agents and collect - experiences for learning. - agent_wrapper (AgentWrapper): Multi-policy wrapper that interacts with the ``env_wrapper`` directly. - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on how the implementation of the roll-out manager. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the training episodes given in the list. In any case, the policies will be evaluated - at the end of the last training episode. Defaults to None, in which case the policies will only be - evaluated after the last training episode. - eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used - as the evaluation environment. Defaults to None. - early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metrics are met following an evaluation episode. Default to None. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - - def __init__( - self, - env_wrapper: AbsEnvWrapper, - agent_wrapper: AgentWrapper, - num_episodes: int, - num_steps: int = -1, - eval_schedule: Union[int, List[int]] = None, - eval_env: AbsEnvWrapper = None, - early_stopper: AbsEarlyStopper = None, - log_env_summary: bool = True, - log_dir: str = getcwd(), - ): - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - self.logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) - self.env = env_wrapper - self.eval_env = eval_env if eval_env else self.env - self.agent = agent_wrapper - - self.num_episodes = num_episodes - self._num_steps = num_steps if num_steps > 0 else float("inf") - - # evaluation schedule - if eval_schedule is None: - self._eval_schedule = [] - elif isinstance(eval_schedule, int): - num_eval_schedule = num_episodes // eval_schedule - self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - else: - self._eval_schedule = eval_schedule - self._eval_schedule.sort() - - # always evaluate after the last episode - if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - self._eval_schedule.append(num_episodes) - - self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") - self._eval_point_index = 0 - - self.early_stopper = early_stopper - - self._log_env_summary = log_env_summary - - def run(self): - """Entry point for executing a learning workflow.""" - for ep in range(1, self.num_episodes + 1): - self._train(ep) - if ep == self._eval_schedule[self._eval_point_index]: - self._eval_point_index += 1 - self._evaluate() - # early stopping check - if self.early_stopper: - self.early_stopper.push(self.eval_env.summary) - if self.early_stopper.stop(): - return - - def _train(self, ep: int): - """Collect simulation data for training.""" - t0 = time.time() - num_experiences_collected = 0 - - self.agent.explore() - self.env.reset() - self.env.start() # get initial state - segment = 0 - while self.env.state: - segment += 1 - exp_by_agent = self._collect(ep, segment) - self.agent.on_experiences(exp_by_agent) - num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) - # update the exploration parameters if an episode is finished - self.agent.exploration_step() - - # performance details - if self._log_env_summary: - self.logger.info(f"ep {ep}: {self.env.summary}") - - self.logger.info( - f"ep {ep} summary - " - f"running time: {time.time() - t0} " - f"env steps: {self.env.step_index} " - f"experiences collected: {num_experiences_collected}" - ) - - def _evaluate(self): - """Policy evaluation.""" - self.logger.info("Evaluating...") - self.agent.exploit() - self.eval_env.reset() - self.eval_env.start() # get initial state - while self.eval_env.state: - self.eval_env.step(self.agent.choose_action(self.eval_env.state)) - - # performance details - self.logger.info(f"Evaluation result: {self.eval_env.summary}") - - def _collect(self, ep, segment): - start_step_index = self.env.step_index + 1 - steps_to_go = self._num_steps - while self.env.state and steps_to_go: - self.env.step(self.agent.choose_action(self.env.state)) - steps_to_go -= 1 - - self.logger.info( - f"Roll-out finished for ep {ep}, segment {segment}" - f"(steps {start_step_index} - {self.env.step_index})" - ) - - return self.env.get_experiences() diff --git a/maro/rl/synchronous/__init__.py b/maro/rl/synchronous/__init__.py deleted file mode 100644 index ab4182fae..000000000 --- a/maro/rl/synchronous/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .learner import Learner -from .rollout_manager import AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager -from .rollout_worker import rollout_worker_node, rollout_worker_process - -__all__ = [ - "AbsEarlyStopper", "AbsRolloutManager", "Learner", "LocalLearner", "LocalRolloutManager", "MultiNodeRolloutManager", - "MultiProcessRolloutManager", "rollout_worker_node", "rollout_worker_process" -] diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py deleted file mode 100644 index a5c425540..000000000 --- a/maro/rl/training/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .early_stopper import AbsEarlyStopper -from .local_learner import LocalLearner -from .policy_manager import ( - AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, trainer_node, - trainer_process -) -from .sync_tools import ( - AbsRolloutManager, Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager, - rollout_worker_node, rollout_worker_process -) - -__all__ = [ - "AbsEarlyStopper", "AbsPolicyManager", "AbsRolloutManager", "Learner", "LocalLearner", "LocalPolicyManager", - "LocalRolloutManager", "MultiNodePolicyManager", "MultiNodeRolloutManager", "MultiProcessPolicyManager", - "MultiProcessRolloutManager", "rollout_worker_node", "rollout_worker_process", "trainer_node", - "trainer_process" -] diff --git a/maro/rl/training/early_stopper.py b/maro/rl/training/early_stopper.py deleted file mode 100644 index 20cfb1561..000000000 --- a/maro/rl/training/early_stopper.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - - -class AbsEarlyStopper(ABC): - def __init__(self): - super().__init__() - self.metric_history = [] - - def push(self, metric): - self.metric_history.append(metric) - - @abstractmethod - def stop(self) -> bool: - raise NotImplementedError diff --git a/maro/rl/training/message_enums.py b/maro/rl/training/message_enums.py deleted file mode 100644 index d55676ec2..000000000 --- a/maro/rl/training/message_enums.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from enum import Enum - - -class MsgTag(Enum): - COLLECT = "rollout" - EVAL = "eval" - INIT_POLICY_STATE = "init_policy_state" - INIT_POLICY_STATE_DONE = "init_policy_state_done" - GET_INITIAL_POLICY_STATE = "get_initial_policy_state" - POLICY_STATE = "policy_state" - CHOOSE_ACTION = "choose_action" - ACTION = "action" - TRAIN = "train" - ABORT_ROLLOUT = "abort_rollout" - EVAL_DONE = "eval_done" - COLLECT_DONE = "collect_done" - EXIT = "exit" - - -class MsgKey(Enum): - ACTION = "action" - AGENT_ID = "agent_id" - EPISODE = "episode" - SEGMENT = "segment" - STEP = "step" - ENV_SUMMARY = "env_summary" - EXPERIENCES = "experiences" - NUM_EXPERIENCES = "num_experiences" - STATE = "state" - POLICY_STATE = "policy_state" - EXPLORATION_STEP = "exploration_step" - VERSION = "version" - NUM_STEPS = "num_steps" - EPISODE_END = "episode_end" diff --git a/maro/rl/training/policy_manager/__init__.py b/maro/rl/training/policy_manager/__init__.py deleted file mode 100644 index 9c877a4f9..000000000 --- a/maro/rl/training/policy_manager/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .policy_manager import AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager -from .trainer import trainer_node, trainer_process - -__all__ = [ - "AbsPolicyManager", "LocalPolicyManager", "MultiNodePolicyManager", "MultiProcessPolicyManager", - "trainer_node", "trainer_process", -] diff --git a/maro/rl/training/policy_manager/policy_manager.py b/maro/rl/training/policy_manager/policy_manager.py deleted file mode 100644 index b3a6de7a4..000000000 --- a/maro/rl/training/policy_manager/policy_manager.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from abc import ABC, abstractmethod -from collections import defaultdict -from multiprocessing import Pipe, Process -from os import getcwd -from typing import Callable, Dict - -from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.experience import ExperienceSet -from maro.rl.policy import AbsCorePolicy -from maro.utils import Logger - -from ..message_enums import MsgKey, MsgTag -from .trainer import trainer_process - - -class AbsPolicyManager(ABC): - """Manage all policies. - - The actual policy instances may reside here or be distributed on a set of processes or remote nodes. - - Args: - policy_dict (Dict[str, AbsCorePolicy]): A list of policies managed by the manager. - """ - def __init__(self, policy_dict: Dict[str, AbsCorePolicy]): - for policy in policy_dict.values(): - if not isinstance(policy, AbsCorePolicy): - raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") - - super().__init__() - self.policy_dict = policy_dict - self.updated = set(self.policy_dict.keys()) - self._version = 0 - - @property - def version(self): - return self._version - - @abstractmethod - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): - """Logic for handling incoming experiences is implemented here.""" - raise NotImplementedError - - def get_state(self): - return {name: self.policy_dict[name].get_state() for name in self.updated} - - def reset_update_status(self): - self.updated.clear() - - -class LocalPolicyManager(AbsPolicyManager): - """Policy manager that contains the actual policy instances. - - Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time - and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - def __init__(self, policy_dict: Dict[str, AbsCorePolicy], log_dir: str = getcwd()): - super().__init__(policy_dict) - self._logger = Logger("LOCAL_TRAINING_MANAGER", dump_folder=log_dir) - self._new_exp_counter = defaultdict(int) - - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): - """Store experiences and update policies if possible. - - The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding - policy's experience manager. Policies whose update conditions have been met will then be updated. - """ - t0 = time.time() - for policy_name, exp in exp_by_policy.items(): - if ( - isinstance(self.policy_dict[policy_name], AbsCorePolicy) and - self.policy_dict[policy_name].on_experiences(exp) - ): - self.updated.add(policy_name) - - if self.updated: - self._logger.info(f"Updated policies {self.updated}") - - self._logger.debug(f"policy update time: {time.time() - t0}") - - -class MultiProcessPolicyManager(AbsPolicyManager): - """Policy manager that spawns a set of trainer processes for parallel training. - - Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. - policy2trainer (dict): Mapping from policy names to trainer IDs. - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - policy_dict: Dict[str, AbsCorePolicy], - num_trainers: int, - create_policy_func_dict: Dict[str, Callable], - log_dir: str = getcwd(), - ): - super().__init__(policy_dict) - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - self._policy2trainer = {} - self._trainer2policies = defaultdict(list) - for i, name in enumerate(self.policy_dict): - trainer_id = i % num_trainers - self._policy2trainer[name] = f"TRAINER.{trainer_id}" - self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) - - self._trainer_processes = [] - self._manager_end = {} - for trainer_id, policy_names in self._trainer2policies.items(): - manager_end, trainer_end = Pipe() - self._manager_end[trainer_id] = manager_end - trainer = Process( - target=trainer_process, - args=( - trainer_id, - trainer_end, - {name: create_policy_func_dict[name] for name in policy_names}, - {name: self.policy_dict[name].get_state() for name in self._trainer2policies[trainer_id]} - ), - kwargs={"log_dir": log_dir} - ) - self._trainer_processes.append(trainer) - trainer.start() - - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): - for trainer_id, conn in self._manager_end.items(): - conn.send({ - "type": "train", - "experiences": {name: exp_by_policy[name] for name in self._trainer2policies[trainer_id]} - }) - - for conn in self._manager_end.values(): - result = conn.recv() - for policy_name, policy_state in result["policy"].items(): - self.policy_dict[policy_name].set_state(policy_state) - self.updated.add(policy_name) - - if self.updated: - self._version += 1 - - def exit(self): - """Tell the trainer processes to exit.""" - for conn in self._manager_end.values(): - conn.send({"type": "quit"}) - - -class MultiNodePolicyManager(AbsPolicyManager): - """Policy manager that communicates with a set of remote nodes for parallel training. - - Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. - group (str): Group name for the training cluster, which includes all trainers and a training manager that - manages them. - num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where - 0 <= i < num_trainers. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - policy_dict: Dict[str, AbsCorePolicy], - group: str, - num_trainers: int, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - super().__init__(policy_dict) - peers = {"trainer": num_trainers} - self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) - self._logger = Logger(self._proxy.name, dump_folder=log_dir) - - self._policy2trainer = {} - self._trainer2policies = defaultdict(list) - for i, name in enumerate(self.policy_dict): - trainer_id = i % num_trainers - self._policy2trainer[name] = f"TRAINER.{trainer_id}" - self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) - for trainer_name, policy_names in self._trainer2policies.items(): - self._proxy.send( - SessionMessage( - MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, - body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} - ) - ) - - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): - msg_body_by_dest = defaultdict(dict) - for policy_name, exp in exp_by_policy.items(): - trainer_id = self._policy2trainer[policy_name] - if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp - - for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): - for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): - self.policy_dict[policy_name].set_state(policy_state) - self.updated.add(policy_name) - - if self.updated: - self._version += 1 - - def exit(self): - """Tell the remote trainers to exit.""" - self._proxy.ibroadcast("trainer", MsgTag.EXIT, SessionType.NOTIFICATION) - self._proxy.close() - self._logger.info("Exiting...") diff --git a/maro/rl/training/policy_manager/trainer.py b/maro/rl/training/policy_manager/trainer.py deleted file mode 100644 index 7dbf58f26..000000000 --- a/maro/rl/training/policy_manager/trainer.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from multiprocessing.connection import Connection -from os import getcwd -from typing import Callable, Dict - -from maro.communication import Proxy -from maro.utils import Logger - -from ..message_enums import MsgKey, MsgTag - - -def trainer_process( - trainer_id: int, - conn: Connection, - create_policy_func_dict: Dict[str, Callable], - initial_policy_states: dict, - log_dir: str = getcwd() -): - """Policy trainer process which can be spawned by a ``MultiProcessPolicyManager``. - - Args: - trainer_id (int): Integer trainer ID. - conn (Connection): Connection end for exchanging messages with the manager process. - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} - logger = Logger("TRAINER", dump_folder=log_dir) - for name, state in initial_policy_states.items(): - policy_dict[name].set_state(state) - logger.info(f"{trainer_id} initialized policy {name}") - - while True: - msg = conn.recv() - if msg["type"] == "train": - t0 = time.time() - updated = { - name: policy_dict[name].get_state() for name, exp in msg["experiences"].items() - if policy_dict[name].on_experiences(exp) - } - logger.debug(f"total policy update time: {time.time() - t0}") - conn.send({"policy": updated}) - elif msg["type"] == "get_policy_state": - policy_state_dict = {name: policy.get_state() for name, policy in policy_dict.items()} - conn.send({"policy": policy_state_dict}) - elif msg["type"] == "quit": - break - - -def trainer_node( - group: str, - trainer_idx: int, - create_policy_func_dict: Dict[str, Callable], - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Policy trainer process that can be launched on separate computation nodes. - - Args: - group (str): Group name for the training cluster, which includes all trainers and a training manager that - manages them. - trainer_idx (int): Integer trainer index. The trainer's ID in the cluster will be "TRAINER.{trainer_idx}". - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - policy_dict = {} - proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=f"TRAINER.{trainer_idx}", **proxy_kwargs) - logger = Logger(proxy.name, dump_folder=log_dir) - - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.INIT_POLICY_STATE: - for name, state in msg.body[MsgKey.POLICY_STATE].items(): - policy_dict[name] = create_policy_func_dict[name]() - policy_dict[name].set_state(state) - logger.info(f"{proxy.name} initialized policy {name}") - proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) - elif msg.tag == MsgTag.TRAIN: - t0 = time.time() - msg_body = { - MsgKey.POLICY_STATE: { - name: policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() - if policy_dict[name].on_experiences(exp) - } - } - logger.info(f"updated policies {list(msg_body[MsgKey.POLICY_STATE].keys())}") - logger.debug(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, body=msg_body) diff --git a/maro/rl/training/sync_tools/learner.py b/maro/rl/training/sync_tools/learner.py deleted file mode 100644 index 4cef5ccef..000000000 --- a/maro/rl/training/sync_tools/learner.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import List, Union - -from maro.utils import Logger - -from ..early_stopper import AbsEarlyStopper -from ..policy_manager.policy_manager import AbsPolicyManager -from rollout_manager import AbsRolloutManager - - -class Learner: - """Main controller for learning. - - This should be used in multi-process or distributed settings where either the policy manager or the roll-out - manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such - as duplicate experience storage. Use ``LocalLearner`` instead. - - Args: - policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. - rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data - collection. - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on the implementation of the roll-out manager. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the training episodes given in the list. In any case, the policies will be evaluated - at the end of the last training episode. Defaults to None, in which case the policies will only be - evaluated after the last training episode. - early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metric are met following an evaluation episode. Default to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time - and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - end_of_episode_kwargs: Keyword arguments for custom end-of-episode processing. - """ - def __init__( - self, - policy_manager: AbsPolicyManager, - rollout_manager: AbsRolloutManager, - num_episodes: int, - eval_schedule: Union[int, List[int]] = None, - early_stopper: AbsEarlyStopper = None, - log_dir: str = getcwd(), - **end_of_episode_kwargs - ): - self.logger = Logger("LEARNER", dump_folder=log_dir) - self.policy_manager = policy_manager - self.rollout_manager = rollout_manager - - self.num_episodes = num_episodes - - # evaluation schedule - if eval_schedule is None: - self._eval_schedule = [] - elif isinstance(eval_schedule, int): - num_eval_schedule = num_episodes // eval_schedule - self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - else: - self._eval_schedule = eval_schedule - self._eval_schedule.sort() - - # always evaluate after the last episode - if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - self._eval_schedule.append(num_episodes) - - self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") - self._eval_point_index = 0 - - self.early_stopper = early_stopper - - self._end_of_episode_kwargs = end_of_episode_kwargs - self._last_step_set = {} - - def run(self): - """Entry point for executing a learning workflow.""" - for ep in range(1, self.num_episodes + 1): - self._train(ep) - if ep == self._eval_schedule[self._eval_point_index]: - self._eval_point_index += 1 - env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) - # performance details - self.logger.info(f"Evaluation result: {env_metric_dict}") - # early stopping check - if self.early_stopper: - for env_metric in env_metric_dict.values(): - self.early_stopper.push(env_metric) - if self.early_stopper.stop(): - return - - if hasattr(self.rollout_manager, "exit"): - self.rollout_manager.exit() - - if hasattr(self.policy_manager, "exit"): - self.policy_manager.exit() - - def _train(self, ep: int): - collect_time = policy_update_time = num_experiences_collected = 0 - segment = 0 - self.rollout_manager.reset() - while not self.rollout_manager.episode_complete: - segment += 1 - # experience collection - policy_state_dict = self.policy_manager.get_state() - self.policy_manager.reset_update_status() - policy_version = self.policy_manager.version - tc0 = time.time() - exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) - collect_time += time.time() - tc0 - tu0 = time.time() - self.policy_manager.on_experiences(exp_by_policy) - policy_update_time += time.time() - tu0 - num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) - - # performance details - self.logger.info( - f"ep {ep} summary - " - f"experiences collected: {num_experiences_collected} " - f"experience collection time: {collect_time} " - f"policy update time: {policy_update_time}" - ) - - self.end_of_episode(ep, **self._end_of_episode_kwargs) - - def end_of_episode(self, ep: int, **kwargs): - """Custom end-of-episode processing is implemented here.""" - pass diff --git a/maro/rl/training/sync_tools/rollout_manager.py b/maro/rl/training/sync_tools/rollout_manager.py deleted file mode 100644 index 3ee73a0a9..000000000 --- a/maro/rl/training/sync_tools/rollout_manager.py +++ /dev/null @@ -1,499 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from abc import ABC, abstractmethod -from collections import defaultdict -from multiprocessing import Pipe, Process -from os import getcwd -from random import choices -from typing import Callable - -from maro.communication import Proxy, SessionType -from maro.rl.experience import ExperienceSet -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper -from maro.utils import Logger - -from ..message_enums import MsgKey, MsgTag -from .rollout_worker import rollout_worker_process - - -class AbsRolloutManager(ABC): - """Controller for simulation data collection.""" - def __init__(self): - super().__init__() - self.episode_complete = False - - @abstractmethod - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): - """Collect simulation data, i.e., experiences for training. - - Args: - ep (int): Current episode index. - segment (int): Current segment index. - policy_state_dict (dict): Policy states to use for simulation. - version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. - - Returns: - Experiences for policy training. - """ - raise NotImplementedError - - @abstractmethod - def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``. - - Args: - ep (int): Current training episode index. - policy_state_dict (dict): Policy states to use for simulation. - - Returns: - Environment summary. - """ - raise NotImplementedError - - def reset(self): - self.episode_complete = False - - -class LocalRolloutManager(AbsRolloutManager): - """Local roll-out controller. - - Args: - env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect - experiences for policy training / update. - agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - eval_env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be - used as the evaluation environment. Defaults to None. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - - def __init__( - self, - env_wrapper: AbsEnvWrapper, - agent_wrapper: AgentWrapper, - num_steps: int = -1, - eval_env_wrapper: AbsEnvWrapper = None, - log_env_summary: bool = True, - log_dir: str = getcwd(), - ): - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - super().__init__() - self._logger = Logger("LOCAL_ROLLOUT_MANAGER", dump_folder=log_dir) - - self.env = env_wrapper - self.eval_env = eval_env_wrapper if eval_env_wrapper else self.env - self.agent = agent_wrapper - - self._num_steps = num_steps if num_steps > 0 else float("inf") - self._log_env_summary = log_env_summary - - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): - """Collect simulation data, i.e., experiences for training. - - Args: - ep (int): Current episode index. - segment (int): Current segment index. - policy_state_dict (dict): Policy states to use for simulation. - version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. - - Returns: - Experiences for policy training. - """ - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") - t0 = time.time() - learning_time = 0 - num_experiences_collected = 0 - - # start of new episode - if self.env.state is None: - self.env.reset() - self.env.start() # get initial state - self.agent.exploration_step() - - # set policy states - self.agent.set_policy_states(policy_state_dict) - # update exploration parameters - self.agent.explore() - - start_step_index = self.env.step_index + 1 - steps_to_go = self._num_steps - while self.env.state and steps_to_go > 0: - action = self.agent.choose_action(self.env.state) - self.env.step(action) - steps_to_go -= 1 - - self._logger.info( - f"Roll-out finished for ep {ep}, segment {segment}" - f"(steps {start_step_index} - {self.env.step_index})" - ) - - # update the exploration parameters if an episode is finished - if not self.env.state: - self.episode_complete = True - # performance details - if self._log_env_summary: - self._logger.info(f"ep {ep}: {self.env.summary}") - - self._logger.debug( - f"ep {ep} summary - " - f"running time: {time.time() - t0} " - f"env steps: {self.env.step_index} " - f"learning time: {learning_time} " - f"experiences collected: {num_experiences_collected}" - ) - - return self.env.get_experiences() - - def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``. - - Args: - ep (int): Current training episode index. - policy_state_dict (dict): Policy states to use for simulation. - - Returns: - Environment summary. - """ - self._logger.info("Evaluating...") - self.agent.set_policy_states(policy_state_dict) - self.agent.exploit() - self.eval_env.reset() - self.eval_env.start() # get initial state - while self.eval_env.state: - action = self.agent.choose_action(self.eval_env.state) - self.eval_env.step(action) - - if self._log_env_summary: - self._logger.info(f"Evaluation result: {self.eval_env.summary}") - - return self.eval_env.summary - - -class MultiProcessRolloutManager(AbsRolloutManager): - """Roll-out manager that spawns a set of roll-out worker processes for parallel data collection. - - Args: - num_workers (int): Number of remote roll-out workers. - create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an - environment wrapper for training data collection. The function should take no parameters and return an - environment wrapper instance. - create_agent_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create a - decision generator for interacting with the environment. The function should take no parameters and return - a ``AgentWrapper`` instance. - create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an - environment wrapper for evaluation. The function should take no parameters and return an environment - wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the - worker processes. Defaults to None. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - num_eval_workers (int): Number of workers for evaluation. Defaults to 1. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - num_workers: int, - create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_agent_wrapper_func: Callable[[], AgentWrapper], - create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, - num_steps: int = -1, - num_eval_workers: int = 1, - log_env_summary: bool = True, - log_dir: str = getcwd(), - ): - super().__init__() - self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) - self._num_workers = num_workers - self._num_steps = num_steps - self._log_env_summary = log_env_summary - self._num_eval_workers = num_eval_workers - self.total_experiences_collected = 0 - self.total_env_steps = 0 - self._exploration_step = False - - self._worker_processes = [] - self._manager_ends = [] - for index in range(self._num_workers): - manager_end, worker_end = Pipe() - self._manager_ends.append(manager_end) - worker = Process( - target=rollout_worker_process, - args=( - index, - worker_end, - create_env_wrapper_func, - create_agent_wrapper_func, - ), - kwargs={ - "create_eval_env_wrapper_func": create_eval_env_wrapper_func, - "log_dir": log_dir - } - ) - self._worker_processes.append(worker) - worker.start() - - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): - """Collect simulation data, i.e., experiences for training. - - Args: - ep (int): Current episode index. - segment (int): Current segment index. - policy_state_dict (dict): Policy states to use for simulation. - version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. - - Returns: - Experiences for policy training. - """ - rollout_req = { - "type": "collect", - "episode": ep, - "segment": segment, - "num_steps": self._num_steps, - "policy": policy_state_dict, - "exploration_step": self._exploration_step - } - - for conn in self._manager_ends: - conn.send(rollout_req) - - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") - - if self._exploration_step: - self._exploration_step = False - - combined_exp_by_policy = defaultdict(ExperienceSet) - for conn in self._manager_ends: - result = conn.recv() - exp_by_policy = result["experiences"] - self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) - self.total_env_steps += result["num_steps"] - - for policy_name, exp in exp_by_policy.items(): - combined_exp_by_policy[policy_name].extend(exp) - - # log roll-out summary - self.episode_complete = result["episode_end"] - if self.episode_complete and self._log_env_summary: - env_summary = result["env_summary"] - self._logger.info(f"env summary: {env_summary}") - - if self.episode_complete: - self._exploration_step = True - - return combined_exp_by_policy - - def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``. - - Args: - ep (int): Current training episode index. - policy_state_dict (dict): Policy states to use for simulation. - - Returns: - Environment summary. - """ - eval_worker_conns = choices(self._manager_ends, k=self._num_eval_workers) - for conn in eval_worker_conns: - conn.send({"type": "evaluate", "episode": ep, "policy": policy_state_dict}) - - env_summary_dict = {} - for conn in self._manager_ends: - result = conn.recv() - env_summary_dict[result["worker_id"]] = result["env_summary"] - - return env_summary_dict - - def exit(self): - """Tell the worker processes to exit.""" - for conn in self._manager_ends: - conn.send({"type": "quit"}) - - -class MultiNodeRolloutManager(AbsRolloutManager): - """Controller for a set of remote roll-out workers, possibly distributed on different computation nodes. - - Args: - group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager - that manages them. - num_workers (int): Number of remote roll-out workers. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to - None, in which case the number is set to ``num_workers``. - receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the workers. This - This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the - desired amount of data from workers. Defaults to None, in which case each receive attempt is blocking. - max_lag (int): Maximum policy version lag allowed for experiences collected from remote roll-out workers. - Experiences collected using policy versions older than (current_version - max_lag) will be discarded. - Defaults to 0, in which case only experiences collected using the latest policy version will be returned. - num_eval_workers (int): Number of workers for evaluation. Defaults to 1. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - group: str, - num_workers: int, - num_steps: int = -1, - max_receive_attempts: int = None, - receive_timeout: int = None, - max_lag: int = 0, - num_eval_workers: int = 1, - log_env_summary: bool = True, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - if num_eval_workers > num_workers: - raise ValueError("num_eval_workers cannot exceed the number of available workers") - - super().__init__() - self.num_workers = num_workers - peers = {"rollout_worker": num_workers} - self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) - self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's - self._logger = Logger(self._proxy.name, dump_folder=log_dir) - - self._num_steps = num_steps - - if max_receive_attempts is None: - max_receive_attempts = self.num_workers - self._logger.info(f"Maximum receive attempts is set to {max_receive_attempts}") - - self.max_receive_attempts = max_receive_attempts - self.receive_timeout = receive_timeout - - self._max_lag = max_lag - self.total_experiences_collected = 0 - self.total_env_steps = 0 - self._log_env_summary = log_env_summary - - self._num_eval_workers = num_eval_workers - - self._exploration_step = False - - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): - """Collect simulation data, i.e., experiences for training. - - Args: - ep (int): Current episode index. - segment (int): Current segment index. - policy_state_dict (dict): Policy states to use for simulation. - version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. - - Returns: - Experiences for policy training. - """ - msg_body = { - MsgKey.EPISODE: ep, - MsgKey.SEGMENT: segment, - MsgKey.NUM_STEPS: self._num_steps, - MsgKey.POLICY_STATE: policy_state_dict, - MsgKey.VERSION: version, - MsgKey.EXPLORATION_STEP: self._exploration_step - } - - self._proxy.ibroadcast("rollout_worker", MsgTag.COLLECT, SessionType.TASK, body=msg_body) - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") - - if self._exploration_step: - self._exploration_step = False - - # Receive roll-out results from remote workers - combined_exp_by_policy = defaultdict(ExperienceSet) - num_finishes = 0 - for _ in range(self.max_receive_attempts): - msg = self._proxy.receive_once(timeout=self.receive_timeout) - if msg.tag != MsgTag.COLLECT_DONE: - self._logger.info( - f"Ignored a message of type {msg.tag} (expected message type {MsgTag.COLLECT_DONE})" - ) - continue - - if version - msg.body[MsgKey.VERSION] > self._max_lag: - self._logger.info( - f"Ignored a message because it contains experiences generated using a stale policy version. " - f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " - f"got {msg.body[MsgKey.VERSION]}" - ) - continue - - exp_by_policy = msg.body[MsgKey.EXPERIENCES] - self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) - self.total_env_steps += msg.body[MsgKey.NUM_STEPS] - - for policy_name, exp in exp_by_policy.items(): - combined_exp_by_policy[policy_name].extend(exp) - - if msg.body[MsgKey.SEGMENT] == segment: - self.episode_complete = msg.body[MsgKey.EPISODE_END] - if self.episode_complete: - # log roll-out summary - if self._log_env_summary: - self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") - num_finishes += 1 - if num_finishes == self.num_workers: - break - - if self.episode_complete: - self._exploration_step = True - - return combined_exp_by_policy - - def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``. - - Args: - ep (int): Current training episode index. - policy_state_dict (dict): Policy states to use for simulation. - - Returns: - Environment summary. - """ - msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY_STATE: policy_state_dict} - - workers = choices(self._workers, k=self._num_eval_workers) - env_summary_dict = {} - self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(worker_id, msg_body) for worker_id in workers]) - self._logger.info(f"Sent evaluation requests to {workers}") - - # Receive roll-out results from remote workers - num_finishes = 0 - for msg in self._proxy.receive(): - if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE] != ep: - self._logger.info( - f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE]} " - f"(expected message type {MsgTag.EVAL_DONE} and episode index {ep})" - ) - continue - - env_summary_dict[msg.source] = msg.body[MsgKey.ENV_SUMMARY] - - if msg.body[MsgKey.EPISODE] == ep: - num_finishes += 1 - if num_finishes == self._num_eval_workers: - break - - return env_summary_dict - - def exit(self): - """Tell the remote workers to exit.""" - self._proxy.ibroadcast("rollout_worker", MsgTag.EXIT, SessionType.NOTIFICATION) - self._proxy.close() - self._logger.info("Exiting...") diff --git a/maro/rl/training/sync_tools/rollout_worker.py b/maro/rl/training/sync_tools/rollout_worker.py deleted file mode 100644 index 3d178a3e7..000000000 --- a/maro/rl/training/sync_tools/rollout_worker.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from multiprocessing.connection import Connection -from os import getcwd -from typing import Callable - -from maro.communication import Proxy -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper -from maro.utils import Logger, set_seeds - -from ..message_enums import MsgKey, MsgTag - - -def rollout_worker_process( - index: int, - conn: Connection, - create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_agent_wrapper_func: Callable[[], AgentWrapper], - create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, - log_dir: str = getcwd() -): - """Roll-out worker process that can be spawned by a ``MultiProcessRolloutManager``. - - Args: - index (int): Index for the worker process. This is used for bookkeeping by the parent manager process. - conn (Connection): Connection end for exchanging messages with the manager process. - create_env_wrapper_func (Callable): Function to create an environment wrapper for training data collection. - The function should take no parameters and return an environment wrapper instance. - create_agent_wrapper_func (Callable): Function to create a decision generator for interacting with - the environment. The function should take no parameters and return a ``AgentWrapper`` instance. - create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function - should take no parameters and return an environment wrapper instance. If this is None, the training - environment wrapper will be used for evaluation. Defaults to None. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - set_seeds(index) - env_wrapper = create_env_wrapper_func() - eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() - agent_wrapper = create_agent_wrapper_func() - logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) - - def collect(msg): - ep, segment = msg["episode"], msg["segment"] - # set policy states - agent_wrapper.set_policy_states(msg["policy"]) - - # update exploration parameters - agent_wrapper.explore() - if msg["exploration_step"]: - agent_wrapper.exploration_step() - - if env_wrapper.state is None: - logger.info(f"Training episode {ep}") - env_wrapper.reset() - env_wrapper.start() # get initial state - - starting_step_index = env_wrapper.step_index + 1 - steps_to_go = float("inf") if msg["num_steps"] == -1 else msg["num_steps"] - while env_wrapper.state and steps_to_go > 0: - action = agent_wrapper.choose_action(env_wrapper.state) - env_wrapper.step(action) - steps_to_go -= 1 - - logger.info( - f"Roll-out finished (episode {ep}, segment {segment}, " - f"steps {starting_step_index} - {env_wrapper.step_index})" - ) - - policies_with_new_exp = agent_wrapper.on_experiences(env_wrapper.get_experiences()) - ret_exp = agent_wrapper.get_experiences_by_policy(policies_with_new_exp) - - return_info = { - "worker_index": index, - "episode_end": not env_wrapper.state, - "experiences": ret_exp, - "env_summary": env_wrapper.summary, - "num_steps": env_wrapper.step_index - starting_step_index + 1 - } - - conn.send(return_info) - - def evaluate(msg): - logger.info("Evaluating...") - agent_wrapper.set_policy_states(msg["policy"]) - agent_wrapper.exploit() - eval_env_wrapper.reset() - eval_env_wrapper.start() # get initial state - while eval_env_wrapper.state: - action = agent_wrapper.choose_action(eval_env_wrapper.state) - eval_env_wrapper.step(action) - - conn.send({"worker_id": index, "env_summary": eval_env_wrapper.summary}) - - while True: - msg = conn.recv() - if msg["type"] == "collect": - collect(msg) - elif msg["type"] == "evaluate": - evaluate(msg) - elif msg["type"] == "quit": - break - - -def rollout_worker_node( - group: str, - worker_id: int, - env_wrapper: AbsEnvWrapper, - agent_wrapper: AgentWrapper, - eval_env_wrapper: AbsEnvWrapper = None, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Roll-out worker process that can be launched on separate computation nodes. - - Args: - group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager - that manages them. - worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". - This is used for bookkeeping by the parent manager. - env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. - agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. - eval_env_wrapper (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training - environment wrapper will be used for evaluation. Defaults to None. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - eval_env_wrapper = env_wrapper if not eval_env_wrapper else eval_env_wrapper - - proxy = Proxy( - group, "rollout_worker", {"rollout_manager": 1}, - component_name=f"ROLLOUT_WORKER.{int(worker_id)}", **proxy_kwargs - ) - logger = Logger(proxy.name, dump_folder=log_dir) - - def collect(msg): - ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] - - # set policy states - agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) - - # set exploration parameters - agent_wrapper.explore() - if msg.body[MsgKey.EXPLORATION_STEP]: - agent_wrapper.exploration_step() - - if env_wrapper.state is None: - logger.info(f"Training episode {msg.body[MsgKey.EPISODE]}") - env_wrapper.reset() - env_wrapper.start() # get initial state - - starting_step_index = env_wrapper.step_index + 1 - steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] - while env_wrapper.state and steps_to_go > 0: - action = agent_wrapper.choose_action(env_wrapper.state) - env_wrapper.step(action) - steps_to_go -= 1 - - logger.info( - f"Roll-out finished (episode {ep}, segment {segment}, " - f"steps {starting_step_index} - {env_wrapper.step_index})" - ) - - policy_names = agent_wrapper.on_experiences(env_wrapper.get_experiences()) - ret_exp = agent_wrapper.get_experiences_by_policy(policy_names) - - return_info = { - MsgKey.EPISODE_END: not env_wrapper.state, - MsgKey.EPISODE: ep, - MsgKey.SEGMENT: segment, - MsgKey.VERSION: msg.body[MsgKey.VERSION], - MsgKey.EXPERIENCES: ret_exp, - MsgKey.ENV_SUMMARY: env_wrapper.summary, - MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1 - } - - proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) - - def evaluate(msg): - logger.info("Evaluating...") - agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) - agent_wrapper.exploit() - eval_env_wrapper.reset() - eval_env_wrapper.start() # get initial state - while eval_env_wrapper.state: - action = agent_wrapper.choose_action(eval_env_wrapper.state) - eval_env_wrapper.step(action) - - return_info = {MsgKey.ENV_SUMMARY: eval_env_wrapper.summary, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} - proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) - - """ - The event loop handles 3 types of messages from the roll-out manager: - 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps - and the collected experiences will be sent back to the roll-out manager; - 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire - duration of the evaluation environment. - 3) EXIT, upon which it will break out of the event loop and the process will terminate. - - """ - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.COLLECT: - collect(msg) - elif msg.tag == MsgTag.EVAL: - evaluate(msg) diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 4b61598c3..349f91334 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -241,7 +241,7 @@ "outputs": [], "source": [ "from maro.simulator import Env\n", - "from maro.rl import LocalLearner\n", + "from maro.rl import SimpleLearner\n", "from maro.utils import set_seeds\n", "\n", "set_seeds(1024) # for reproducibility\n", @@ -249,7 +249,7 @@ "env_wrapper = CIMEnvWrapper(env, **common_config)\n", "policies = [get_ac_policy(id_) for id_ in env.agent_idx_list]\n", "agent2policy = {agent_id: agent_id for agent_id in env.agent_idx_list}\n", - "learner = LocalLearner(env_wrapper, policies, agent2policy, 40) # 40 episodes\n", + "learner = SimpleLearner(env_wrapper, policies, agent2policy, 40) # 40 episodes\n", "learner.run()" ] } From 9f799d4f981be480e27d4ab6713a6a616c3a8833 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Jul 2021 06:00:51 +0000 Subject: [PATCH 348/482] moved env_wrapper and agent_wrapper inside rl/learner --- examples/cim/ac.py | 4 +++- examples/cim/agent_wrapper.py | 5 +++-- examples/cim/dqn.py | 4 +++- examples/cim/env_wrapper.py | 2 +- examples/cim/{meta.py => policy_index.py} | 0 examples/scripts/docker_compose_yml.py | 4 ++-- examples/templates/config.yml | 2 +- examples/templates/general.py | 4 ++-- examples/templates/policy_manager/policy_manager.py | 2 +- examples/templates/policy_manager/trainer.py | 2 +- examples/templates/{sync_mode => sync}/learner.py | 3 ++- examples/templates/{sync_mode => sync}/rollout_worker.py | 2 +- maro/rl/learning/__init__.py | 7 +++++++ maro/rl/{wrappers => learning}/agent_wrapper.py | 0 maro/rl/{wrappers => learning}/env_wrapper.py | 0 maro/rl/learning/simple_learner.py | 3 ++- maro/rl/learning/sync/rollout_manager.py | 3 ++- maro/rl/learning/sync/rollout_worker.py | 4 +++- maro/rl/wrappers/__init__.py | 7 ------- 19 files changed, 34 insertions(+), 24 deletions(-) rename examples/cim/{meta.py => policy_index.py} (100%) rename examples/templates/{sync_mode => sync}/learner.py (91%) rename examples/templates/{sync_mode => sync}/rollout_worker.py (92%) rename maro/rl/{wrappers => learning}/agent_wrapper.py (100%) rename maro/rl/{wrappers => learning}/env_wrapper.py (100%) delete mode 100644 maro/rl/wrappers/__init__.py diff --git a/examples/cim/ac.py b/examples/cim/ac.py index 8ef829ea9..3d800fb88 100644 --- a/examples/cim/ac.py +++ b/examples/cim/ac.py @@ -7,7 +7,9 @@ import numpy as np import torch -from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceManager, FullyConnectedBlock, OptimOption +from maro.rl.algorithms import ActorCritic, ActorCriticConfig +from maro.rl.experience import ExperienceManager +from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption cim_path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, cim_path) diff --git a/examples/cim/agent_wrapper.py b/examples/cim/agent_wrapper.py index 59bbca829..a849fc9bf 100644 --- a/examples/cim/agent_wrapper.py +++ b/examples/cim/agent_wrapper.py @@ -4,12 +4,13 @@ import os import sys -from maro.rl import AgentWrapper, EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler +from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler +from maro.rl.learning import AgentWrapper cim_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, cim_path) from env_wrapper import AGENT_IDS, env_config -from meta import create_rollout_policy_func +from policy_index import create_rollout_policy_func exploration_config = { diff --git a/examples/cim/dqn.py b/examples/cim/dqn.py index 4e9cd39d8..4b7ad3c8d 100644 --- a/examples/cim/dqn.py +++ b/examples/cim/dqn.py @@ -7,7 +7,9 @@ import numpy as np import torch import torch.nn as nn -from maro.rl import DQN, DQNConfig, DiscreteQNet, ExperienceManager, FullyConnectedBlock, OptimOption +from maro.rl.algorithms import DQN, DQNConfig +from maro.rl.experience import ExperienceManager +from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: diff --git a/examples/cim/env_wrapper.py b/examples/cim/env_wrapper.py index a1dbc532f..28ab6bb1a 100644 --- a/examples/cim/env_wrapper.py +++ b/examples/cim/env_wrapper.py @@ -3,7 +3,7 @@ import numpy as np -from maro.rl import AbsEnvWrapper +from maro.rl.learning import AbsEnvWrapper from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType diff --git a/examples/cim/meta.py b/examples/cim/policy_index.py similarity index 100% rename from examples/cim/meta.py rename to examples/cim/policy_index.py diff --git a/examples/scripts/docker_compose_yml.py b/examples/scripts/docker_compose_yml.py index 4ba7040ef..874b08cbd 100644 --- a/examples/scripts/docker_compose_yml.py +++ b/examples/scripts/docker_compose_yml.py @@ -49,7 +49,7 @@ **common_spec, **{ "container_name": "learner", - "command": "python3 /maro/examples/templates/sync_mode/learner.py" + "command": "python3 /maro/examples/templates/sync/learner.py" } } # rollout worker spec @@ -58,7 +58,7 @@ str_id = f"rollout_worker.{worker_id}" worker_spec = deepcopy(common_spec) del worker_spec["build"] - worker_spec["command"] = "python3 /maro/examples/templates/sync_mode/rollout_worker.py" + worker_spec["command"] = "python3 /maro/examples/templates/sync/rollout_worker.py" worker_spec["container_name"] = str_id worker_spec["environment"] = [f"WORKERID={worker_id}"] docker_compose_manifest["services"][str_id] = worker_spec diff --git a/examples/templates/config.yml b/examples/templates/config.yml index d29df4737..0678c4c27 100644 --- a/examples/templates/config.yml +++ b/examples/templates/config.yml @@ -10,7 +10,7 @@ num_steps: 50 max_lag: 0 sync: rollout_group: rollout - rollout_mode: multi-node # single-process, multi-process, multi-node + rollout_mode: single-process # single-process, multi-process, multi-node num_rollout_workers: 3 # max_receive_attempts: 2 # receive_timeout: 100 # in milli-seconds diff --git a/examples/templates/general.py b/examples/templates/general.py index 598959a52..cfd991f9f 100644 --- a/examples/templates/general.py +++ b/examples/templates/general.py @@ -5,7 +5,7 @@ import yaml from os.path import dirname, join, realpath -template_dir = dirname(dirname(realpath(__file__))) +template_dir = dirname(realpath(__file__)) example_dir = dirname(template_dir) if template_dir not in sys.path: sys.path.insert(0, template_dir) @@ -22,6 +22,6 @@ if scenario == "cim": from cim.env_wrapper import get_env_wrapper from cim.agent_wrapper import get_agent_wrapper - from cim.meta import create_rollout_policy_func, create_train_policy_func + from cim.policy_index import create_rollout_policy_func, create_train_policy_func else: raise ValueError(f"Unsupported scenario: {scenario}. Supported scenarios: 'cim'") diff --git a/examples/templates/policy_manager/policy_manager.py b/examples/templates/policy_manager/policy_manager.py index d3fb1fa46..0194cc2db 100644 --- a/examples/templates/policy_manager/policy_manager.py +++ b/examples/templates/policy_manager/policy_manager.py @@ -4,7 +4,7 @@ import sys from os.path import dirname, realpath -from maro.rl import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager +from maro.rl.policy import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: diff --git a/examples/templates/policy_manager/trainer.py b/examples/templates/policy_manager/trainer.py index 918df7fd0..1de0636bc 100644 --- a/examples/templates/policy_manager/trainer.py +++ b/examples/templates/policy_manager/trainer.py @@ -5,7 +5,7 @@ from os import environ from os.path import dirname, realpath -from maro.rl import trainer_node +from maro.rl.policy import trainer_node template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: diff --git a/examples/templates/sync_mode/learner.py b/examples/templates/sync/learner.py similarity index 91% rename from examples/templates/sync_mode/learner.py rename to examples/templates/sync/learner.py index 9c7eef542..7ad33f1d0 100644 --- a/examples/templates/sync_mode/learner.py +++ b/examples/templates/sync/learner.py @@ -5,7 +5,7 @@ import time from os.path import dirname, realpath -from maro.rl import Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager +from maro.rl.learning.sync import Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager template_dir = dirname(dirname((realpath(__file__)))) if template_dir not in sys.path: @@ -20,6 +20,7 @@ def get_rollout_manager(): if rollout_mode == "single-process": return LocalRolloutManager( get_env_wrapper(), + get_agent_wrapper(), num_steps=config["num_steps"], log_dir=log_dir ) diff --git a/examples/templates/sync_mode/rollout_worker.py b/examples/templates/sync/rollout_worker.py similarity index 92% rename from examples/templates/sync_mode/rollout_worker.py rename to examples/templates/sync/rollout_worker.py index a8b782471..adbd19b77 100644 --- a/examples/templates/sync_mode/rollout_worker.py +++ b/examples/templates/sync/rollout_worker.py @@ -5,7 +5,7 @@ from os import environ from os.path import dirname, realpath -from maro.rl import rollout_worker_node +from maro.rl.learning.sync import rollout_worker_node template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index 9a0454564..b61977ad3 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -1,2 +1,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. + +from .agent_wrapper import AgentWrapper +from .early_stopper import AbsEarlyStopper +from .env_wrapper import AbsEnvWrapper +from .simple_learner import SimpleLearner + +__all__ = ["AbsEarlyStopper", "AbsEnvWrapper", "AgentWrapper", "SimpleLearner"] diff --git a/maro/rl/wrappers/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py similarity index 100% rename from maro/rl/wrappers/agent_wrapper.py rename to maro/rl/learning/agent_wrapper.py diff --git a/maro/rl/wrappers/env_wrapper.py b/maro/rl/learning/env_wrapper.py similarity index 100% rename from maro/rl/wrappers/env_wrapper.py rename to maro/rl/learning/env_wrapper.py diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py index 20626b34d..0b8eeaf32 100644 --- a/maro/rl/learning/simple_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -5,10 +5,11 @@ from os import getcwd from typing import List, Union -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger +from .agent_wrapper import AgentWrapper from .early_stopper import AbsEarlyStopper +from .env_wrapper import AbsEnvWrapper class SimpleLearner: diff --git a/maro/rl/learning/sync/rollout_manager.py b/maro/rl/learning/sync/rollout_manager.py index eaffa2bef..ef5196cc8 100644 --- a/maro/rl/learning/sync/rollout_manager.py +++ b/maro/rl/learning/sync/rollout_manager.py @@ -12,9 +12,10 @@ from maro.communication import Proxy, SessionType from maro.rl.experience import ExperienceSet from maro.rl.utils import MsgKey, MsgTag -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger +from ..agent_wrapper import AgentWrapper +from ..env_wrapper import AbsEnvWrapper from .rollout_worker import rollout_worker_process diff --git a/maro/rl/learning/sync/rollout_worker.py b/maro/rl/learning/sync/rollout_worker.py index 5ab0baed0..40ca5aaee 100644 --- a/maro/rl/learning/sync/rollout_worker.py +++ b/maro/rl/learning/sync/rollout_worker.py @@ -7,9 +7,11 @@ from maro.communication import Proxy from maro.rl.utils import MsgKey, MsgTag -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger, set_seeds +from ..agent_wrapper import AgentWrapper +from ..env_wrapper import AbsEnvWrapper + def rollout_worker_process( index: int, diff --git a/maro/rl/wrappers/__init__.py b/maro/rl/wrappers/__init__.py deleted file mode 100644 index 9f45fdfc5..000000000 --- a/maro/rl/wrappers/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .agent_wrapper import AgentWrapper -from .env_wrapper import AbsEnvWrapper - -__all__ = ["AbsEnvWrapper", "AgentWrapper"] From f8cccca965b6260c9c96964ca6ec8d56fe7c2089 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Jul 2021 06:30:44 +0000 Subject: [PATCH 349/482] refined scripts --- examples/scripts/{ => docker}/build.sh | 2 +- .../{ => docker}/docker_compose_yml.py | 12 +++------ examples/scripts/{ => docker}/kill.sh | 3 ++- examples/scripts/docker/run.sh | 7 +++++ examples/scripts/run.sh | 5 +--- examples/templates/config.yml | 7 ++--- examples/templates/simple_learner.py | 27 +++++++++++++++++++ examples/templates/sync/learner.py | 4 +++ 8 files changed, 49 insertions(+), 18 deletions(-) rename examples/scripts/{ => docker}/build.sh (86%) rename examples/scripts/{ => docker}/docker_compose_yml.py (88%) rename examples/scripts/{ => docker}/kill.sh (52%) create mode 100644 examples/scripts/docker/run.sh create mode 100644 examples/templates/simple_learner.py diff --git a/examples/scripts/build.sh b/examples/scripts/docker/build.sh similarity index 86% rename from examples/scripts/build.sh rename to examples/scripts/docker/build.sh index 8f82e9b2e..005cbe30a 100644 --- a/examples/scripts/build.sh +++ b/examples/scripts/docker/build.sh @@ -1,7 +1,7 @@ #!/bin/bash BASEDIR=$(dirname "$0") -ROOTDIR=$BASEDIR/../../ +ROOTDIR=$BASEDIR/../../../../ # script to build the docker image for running the CIM scenario. docker pull redis:6 diff --git a/examples/scripts/docker_compose_yml.py b/examples/scripts/docker/docker_compose_yml.py similarity index 88% rename from examples/scripts/docker_compose_yml.py rename to examples/scripts/docker/docker_compose_yml.py index 874b08cbd..8e1b3183a 100644 --- a/examples/scripts/docker_compose_yml.py +++ b/examples/scripts/docker/docker_compose_yml.py @@ -7,11 +7,10 @@ path = realpath(__file__) script_dir = dirname(path) -example_dir = dirname(script_dir) +example_dir = dirname(dirname(script_dir)) root_dir = dirname(example_dir) template_dir = join(example_dir, "templates") maro_rl_dir = join(root_dir, "maro", "rl") -maro_comm_dir = join(root_dir, "maro", "communication") config_path = join(template_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") @@ -24,11 +23,7 @@ common_spec = { "build": {"context": root_dir, "dockerfile": dockerfile_path}, "image": "maro", - "volumes": [ - f"{example_dir}:/maro/examples", - f"{maro_rl_dir}:/maro/maro/rl", - f"{maro_comm_dir}:/maro/maro/communication" - ] + "volumes": [f"{example_dir}:/maro/examples", f"{maro_rl_dir}:/maro/maro/rl"] } # trainer spec @@ -65,6 +60,5 @@ else: raise ValueError("Only sync mode is supported in this version") - -with open(join(example_dir, "docker-compose.yml"), "w") as fp: +with open(join(script_dir, "docker-compose.yml"), "w") as fp: yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/scripts/kill.sh b/examples/scripts/docker/kill.sh similarity index 52% rename from examples/scripts/kill.sh rename to examples/scripts/docker/kill.sh index a081af0b5..c0a63918a 100644 --- a/examples/scripts/kill.sh +++ b/examples/scripts/docker/kill.sh @@ -3,4 +3,5 @@ BASEDIR=$(dirname "$0") # script to kill a previously launched training job. -docker-compose -f $BASEDIR/../docker-compose.yml down \ No newline at end of file +docker-compose -f $BASEDIR/docker-compose.yml down +rm $BASEDIR/docker_compose.yml \ No newline at end of file diff --git a/examples/scripts/docker/run.sh b/examples/scripts/docker/run.sh new file mode 100644 index 000000000..8a51fa1eb --- /dev/null +++ b/examples/scripts/docker/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +BASEDIR=$(dirname "$0") + +# script to run the CIM scenario in single-host multi-container mode. +python3 $BASEDIR/docker_compose_yml.py +docker-compose -f $BASEDIR/docker-compose.yml up \ No newline at end of file diff --git a/examples/scripts/run.sh b/examples/scripts/run.sh index 37ac6143f..e6f685f64 100644 --- a/examples/scripts/run.sh +++ b/examples/scripts/run.sh @@ -1,7 +1,4 @@ #!/bin/bash BASEDIR=$(dirname "$0") - -# script to run the CIM scenario in single-host multi-container mode. -python3 $BASEDIR/docker_compose_yml.py -docker-compose -f $BASEDIR/../docker-compose.yml up \ No newline at end of file +python3 $BASEDIR/../templates/simple_learner.py \ No newline at end of file diff --git a/examples/templates/config.yml b/examples/templates/config.yml index 0678c4c27..753591913 100644 --- a/examples/templates/config.yml +++ b/examples/templates/config.yml @@ -5,12 +5,13 @@ job_name: cim-dqn scenario: cim mode: sync num_episodes: 10 -eval_schedule: 2 -num_steps: 50 +eval_schedule: 5 +num_steps: 100 max_lag: 0 +log_env_summary: true sync: rollout_group: rollout - rollout_mode: single-process # single-process, multi-process, multi-node + rollout_mode: multi-node # single-process, multi-process, multi-node num_rollout_workers: 3 # max_receive_attempts: 2 # receive_timeout: 100 # in milli-seconds diff --git a/examples/templates/simple_learner.py b/examples/templates/simple_learner.py new file mode 100644 index 000000000..842602a35 --- /dev/null +++ b/examples/templates/simple_learner.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +import time +from os.path import dirname, realpath + +from maro.rl.learning import SimpleLearner + +template_dir = dirname(dirname((realpath(__file__)))) +if template_dir not in sys.path: + sys.path.insert(0, template_dir) + +from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from policy_manager.policy_manager import get_policy_manager + + +if __name__ == "__main__": + SimpleLearner( + get_env_wrapper(), + get_agent_wrapper(), + num_episodes=config["num_episodes"], + num_steps=config["num_steps"], + eval_schedule=config["eval_schedule"], + log_env_summary=config["log_env_summary"], + log_dir=log_dir + ).run() diff --git a/examples/templates/sync/learner.py b/examples/templates/sync/learner.py index 7ad33f1d0..f560c7baf 100644 --- a/examples/templates/sync/learner.py +++ b/examples/templates/sync/learner.py @@ -22,6 +22,7 @@ def get_rollout_manager(): get_env_wrapper(), get_agent_wrapper(), num_steps=config["num_steps"], + log_env_summary=config["log_env_summary"], log_dir=log_dir ) if rollout_mode == "multi-process": @@ -30,13 +31,16 @@ def get_rollout_manager(): get_env_wrapper, get_agent_wrapper, num_steps=config["num_steps"], + log_env_summary=config["log_env_summary"], log_dir=log_dir, ) if rollout_mode == "multi-node": return MultiNodeRolloutManager( config["sync"]["rollout_group"], config["sync"]["num_rollout_workers"], + num_steps=config["num_steps"], max_lag=config["max_lag"], + log_env_summary=config["log_env_summary"], proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} ) From 0906577c6a0a63ebeb3a2eaf63c40650db42801c Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Jul 2021 07:04:19 +0000 Subject: [PATCH 350/482] fixed typo in script --- examples/scripts/docker/kill.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/scripts/docker/kill.sh b/examples/scripts/docker/kill.sh index c0a63918a..b5948128c 100644 --- a/examples/scripts/docker/kill.sh +++ b/examples/scripts/docker/kill.sh @@ -4,4 +4,4 @@ BASEDIR=$(dirname "$0") # script to kill a previously launched training job. docker-compose -f $BASEDIR/docker-compose.yml down -rm $BASEDIR/docker_compose.yml \ No newline at end of file +rm $BASEDIR/docker-compose.yml \ No newline at end of file From a13322b045b65a48d9a06eb7fc205ead005be5a4 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Jul 2021 07:31:21 +0000 Subject: [PATCH 351/482] changes needed for running sc --- examples/supply_chain/agent_wrapper.py | 13 +- examples/supply_chain/or_policies.py | 2 +- examples/supply_chain/policy_index.py | 28 + .../supply_chain/topologies/random/config.yml | 3 +- examples/templates/simple_learner.py | 2 - .../supply_chain/topologies/random/config.yml | 29345 ++++++++++++++++ 6 files changed, 29386 insertions(+), 7 deletions(-) create mode 100644 examples/supply_chain/policy_index.py create mode 100644 maro/simulator/scenarios/supply_chain/topologies/random/config.yml diff --git a/examples/supply_chain/agent_wrapper.py b/examples/supply_chain/agent_wrapper.py index 5d62ad724..a1a44d410 100644 --- a/examples/supply_chain/agent_wrapper.py +++ b/examples/supply_chain/agent_wrapper.py @@ -11,10 +11,10 @@ if sc_path not in sys.path: sys.path.insert(0, sc_path) from env_wrapper import NUM_ACTIONS, AGENT_IDS -from policy_index import create_rollout_policy_func +from policy_index import NUM_RL_POLICIES, create_rollout_policy_func exploration_config = { - "last_ep": 800, + "last_ep": 10, "initial_value": 0.8, # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. "final_value": 0.0 } @@ -26,9 +26,16 @@ def get_agent_wrapper(): param_name="epsilon", **exploration_config ) + consumerstores = [agent_id for agent_id in AGENT_IDS if agent_id.startswith("consumerstore")] + agent2policy = { + agent_id: agent_id.split(".")[0] for agent_id in AGENT_IDS if not agent_id.startswith("consumerstore") + } + for i, agent_id in enumerate(consumerstores): + agent2policy[agent_id] = f"consumerstore-{i % NUM_RL_POLICIES}" + return AgentWrapper( {name: func() for name, func in create_rollout_policy_func.items()}, - {agent_id: agent_id.split(".")[0] for agent_id in AGENT_IDS}, + agent2policy, exploration_dict={"consumerstore": epsilon_greedy}, agent2exploration = { agent_id: "consumerstore" for agent_id in AGENT_IDS if agent_id.startswith("consumerstore") diff --git a/examples/supply_chain/or_policies.py b/examples/supply_chain/or_policies.py index 11cb797e2..12cc42b71 100644 --- a/examples/supply_chain/or_policies.py +++ b/examples/supply_chain/or_policies.py @@ -109,7 +109,7 @@ def choose_action(self, state): sale_mean, sale_std = state['sale_mean'], state['sale_std'] service_level = state['service_level'] r = (vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level)) - print(booked_inventory, most_needed_product_id, r) + # print(booked_inventory, most_needed_product_id, r) if booked_inventory[most_needed_product_id] > r: return 0 R = 3*r diff --git a/examples/supply_chain/policy_index.py b/examples/supply_chain/policy_index.py new file mode 100644 index 000000000..a0584bd5a --- /dev/null +++ b/examples/supply_chain/policy_index.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +from maro.rl.policy import NullPolicy + +cim_path = os.path.dirname(__file__) +sys.path.insert(0, cim_path) +from dqn import get_dqn_policy_for_rollout, get_dqn_policy_for_training +from or_policies import ( + get_consumer_baseline_policy, get_consumer_eoq_policy, get_consumer_minmax_policy, get_producer_baseline_policy +) + +NUM_RL_POLICIES = 100 + +create_rollout_policy_func = { + "consumer": get_consumer_minmax_policy, + "producer": get_producer_baseline_policy, + "facility": lambda: NullPolicy(), + "product": lambda: NullPolicy(), + "productstore": lambda: NullPolicy(), + # consumer store policies + **{f"consumerstore-{i}": get_dqn_policy_for_rollout for i in range(NUM_RL_POLICIES)}, +} + +create_train_policy_func = {f"consumerstore-{i}": get_dqn_policy_for_training for i in range(NUM_RL_POLICIES)} diff --git a/examples/supply_chain/topologies/random/config.yml b/examples/supply_chain/topologies/random/config.yml index f4f2af16c..f4e810def 100644 --- a/examples/supply_chain/topologies/random/config.yml +++ b/examples/supply_chain/topologies/random/config.yml @@ -7,7 +7,8 @@ facility_definitions: agent_type: product consumer: class: ConsumerUnit - config: consumer + config: + agent_type: consumer seller: class: SellerUnit config: diff --git a/examples/templates/simple_learner.py b/examples/templates/simple_learner.py index 842602a35..68e2ae471 100644 --- a/examples/templates/simple_learner.py +++ b/examples/templates/simple_learner.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. import sys -import time from os.path import dirname, realpath from maro.rl.learning import SimpleLearner @@ -12,7 +11,6 @@ sys.path.insert(0, template_dir) from general import config, get_agent_wrapper, get_env_wrapper, log_dir -from policy_manager.policy_manager import get_policy_manager if __name__ == "__main__": diff --git a/maro/simulator/scenarios/supply_chain/topologies/random/config.yml b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml new file mode 100644 index 000000000..e103236fe --- /dev/null +++ b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml @@ -0,0 +1,29345 @@ +facility_definitions: + RetailerFacility: + children: + products: + class: StoreProductUnit + config: + agent_type: productstore + consumer: + class: ConsumerUnit + config: + agent_type: consumerstore + seller: + class: SellerUnit + config: + sale_hist_len: 4 + is_template: true + storage: + class: StorageUnit + class: RetailerFacility + config: + agent_type: facility + SupplierFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: product + consumer: + class: ConsumerUnit + config: + agent_type: consumer + manufacture: + class: SimpleManufactureUnit + config: + agent_type: producer + is_template: true + storage: + class: StorageUnit + class: SupplierFacility + config: + agent_type: facility + WarehouseFacility: + children: + distribution: + class: DistributionUnit + products: + class: ProductUnit + config: + agent_type: product + consumer: + class: ConsumerUnit + config: + agent_type: consumer + is_template: true + storage: + class: StorageUnit + class: WarehouseFacility + config: + agent_type: facility +normal_vehicle: &id001 + class: VehicleUnit + config: + patient: 100 + unit_transport_cost: 1 +settings: + constraint_state_hist_len: 4 + constraint_violate_reward: -1000000.0 + consumption_hist_len: 4 + downsampling_rate: 1 + episod_duration: 21 + gamma: 0.99 + global_reward_weight_consumer: 0.5 + global_reward_weight_producer: 0.5 + heading_timesteps: 7 + initial_balance: 100000 + pending_order_len: 4 + replenishment_discount: 0.9 + reward_normalization: 10000000.0 + sale_hist_len: 4 + tail_timesteps: 7 + total_echelons: 3 +world: + facilities: + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5324500 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 200 + definition_ref: SupplierFacility + name: SUPPLIER0 + skus: + SKU0: + cost: 289 + init_stock: 3150 + price: 322 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU1: + cost: 255 + init_stock: 1100 + price: 284 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU10: + cost: 61 + init_stock: 1250 + price: 68 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU100: + cost: 14 + init_stock: 3250 + price: 16 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU101: + cost: 75 + init_stock: 3550 + price: 84 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU102: + cost: 295 + init_stock: 4650 + price: 328 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU103: + cost: 300 + init_stock: 4750 + price: 334 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU104: + cost: 44 + init_stock: 3250 + price: 49 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU105: + cost: 99 + init_stock: 2850 + price: 110 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU106: + cost: 225 + init_stock: 3650 + price: 251 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU107: + cost: 380 + init_stock: 4350 + price: 423 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU108: + cost: 412 + init_stock: 4600 + price: 458 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU109: + cost: 79 + init_stock: 4100 + price: 88 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU11: + cost: 360 + init_stock: 2550 + price: 400 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU110: + cost: 59 + init_stock: 700 + price: 66 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU111: + cost: 234 + init_stock: 3050 + price: 260 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU112: + cost: 54 + init_stock: 4750 + price: 61 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU113: + cost: 313 + init_stock: 1550 + price: 348 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU114: + cost: 350 + init_stock: 1350 + price: 389 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU115: + cost: 257 + init_stock: 4300 + price: 286 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU116: + cost: 446 + init_stock: 3600 + price: 496 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU117: + cost: 288 + init_stock: 4600 + price: 320 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU118: + cost: 164 + init_stock: 1650 + price: 183 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU119: + cost: 188 + init_stock: 1600 + price: 209 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU12: + cost: 100 + init_stock: 4200 + price: 112 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU120: + cost: 108 + init_stock: 4900 + price: 121 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU121: + cost: 36 + init_stock: 4250 + price: 40 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU122: + cost: 393 + init_stock: 350 + price: 437 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU123: + cost: 209 + init_stock: 950 + price: 233 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU124: + cost: 163 + init_stock: 1800 + price: 182 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU125: + cost: 14 + init_stock: 4600 + price: 16 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU126: + cost: 32 + init_stock: 1950 + price: 36 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU127: + cost: 195 + init_stock: 1550 + price: 217 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU128: + cost: 148 + init_stock: 950 + price: 165 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU129: + cost: 128 + init_stock: 2500 + price: 143 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU13: + cost: 285 + init_stock: 2850 + price: 317 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU130: + cost: 313 + init_stock: 4900 + price: 348 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU131: + cost: 57 + init_stock: 2250 + price: 64 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU132: + cost: 384 + init_stock: 1050 + price: 427 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU133: + cost: 201 + init_stock: 1450 + price: 224 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU134: + cost: 302 + init_stock: 3850 + price: 336 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU135: + cost: 137 + init_stock: 5000 + price: 153 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU136: + cost: 179 + init_stock: 3550 + price: 199 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU137: + cost: 83 + init_stock: 3700 + price: 93 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU138: + cost: 205 + init_stock: 1800 + price: 228 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU139: + cost: 186 + init_stock: 1200 + price: 207 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU14: + cost: 241 + init_stock: 3100 + price: 268 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU140: + cost: 234 + init_stock: 1700 + price: 261 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU141: + cost: 171 + init_stock: 2050 + price: 190 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU142: + cost: 288 + init_stock: 1900 + price: 320 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU143: + cost: 286 + init_stock: 1300 + price: 318 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU144: + cost: 360 + init_stock: 600 + price: 400 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU145: + cost: 359 + init_stock: 4100 + price: 399 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU146: + cost: 159 + init_stock: 2400 + price: 177 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU147: + cost: 424 + init_stock: 2800 + price: 472 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU148: + cost: 281 + init_stock: 3850 + price: 313 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU149: + cost: 321 + init_stock: 3850 + price: 357 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU15: + cost: 46 + init_stock: 250 + price: 52 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU150: + cost: 95 + init_stock: 3500 + price: 106 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU151: + cost: 200 + init_stock: 3650 + price: 223 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU152: + cost: 9 + init_stock: 2550 + price: 10 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU153: + cost: 396 + init_stock: 3100 + price: 441 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU154: + cost: 69 + init_stock: 4250 + price: 77 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU155: + cost: 379 + init_stock: 2650 + price: 422 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU156: + cost: 9 + init_stock: 600 + price: 10 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU157: + cost: 369 + init_stock: 3750 + price: 410 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU158: + cost: 130 + init_stock: 4050 + price: 145 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU159: + cost: 173 + init_stock: 1250 + price: 193 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU16: + cost: 157 + init_stock: 2900 + price: 175 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU160: + cost: 413 + init_stock: 4250 + price: 459 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU161: + cost: 215 + init_stock: 2300 + price: 239 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU162: + cost: 142 + init_stock: 250 + price: 158 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU163: + cost: 437 + init_stock: 1950 + price: 486 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU164: + cost: 446 + init_stock: 5000 + price: 496 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU165: + cost: 246 + init_stock: 1650 + price: 274 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU166: + cost: 71 + init_stock: 4450 + price: 79 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU167: + cost: 398 + init_stock: 650 + price: 443 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU168: + cost: 321 + init_stock: 4350 + price: 357 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU169: + cost: 332 + init_stock: 4900 + price: 369 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU17: + cost: 311 + init_stock: 450 + price: 346 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU170: + cost: 61 + init_stock: 2750 + price: 68 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU171: + cost: 358 + init_stock: 3800 + price: 398 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU172: + cost: 180 + init_stock: 3550 + price: 200 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU173: + cost: 157 + init_stock: 4800 + price: 175 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU174: + cost: 261 + init_stock: 3800 + price: 291 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU175: + cost: 75 + init_stock: 3750 + price: 84 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU176: + cost: 366 + init_stock: 3300 + price: 407 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU177: + cost: 231 + init_stock: 1550 + price: 257 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU178: + cost: 380 + init_stock: 250 + price: 423 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU179: + cost: 447 + init_stock: 4150 + price: 497 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU18: + cost: 232 + init_stock: 4050 + price: 258 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU180: + cost: 195 + init_stock: 2750 + price: 217 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU181: + cost: 128 + init_stock: 3000 + price: 143 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU182: + cost: 393 + init_stock: 4950 + price: 437 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU183: + cost: 130 + init_stock: 400 + price: 145 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU184: + cost: 65 + init_stock: 1200 + price: 73 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU185: + cost: 9 + init_stock: 4500 + price: 10 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU186: + cost: 323 + init_stock: 1100 + price: 359 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU187: + cost: 159 + init_stock: 1500 + price: 177 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU188: + cost: 351 + init_stock: 4350 + price: 391 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU189: + cost: 322 + init_stock: 1750 + price: 358 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU19: + cost: 429 + init_stock: 4550 + price: 477 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU190: + cost: 101 + init_stock: 850 + price: 113 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU191: + cost: 425 + init_stock: 2700 + price: 473 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU192: + cost: 373 + init_stock: 3050 + price: 415 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU193: + cost: 186 + init_stock: 1500 + price: 207 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU194: + cost: 388 + init_stock: 250 + price: 432 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU195: + cost: 196 + init_stock: 1550 + price: 218 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU196: + cost: 44 + init_stock: 3400 + price: 49 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU197: + cost: 272 + init_stock: 2850 + price: 303 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU198: + cost: 152 + init_stock: 2700 + price: 169 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU199: + cost: 404 + init_stock: 1150 + price: 449 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU2: + cost: 297 + init_stock: 3500 + price: 331 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU20: + cost: 301 + init_stock: 1250 + price: 335 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU200: + cost: 58 + init_stock: 1250 + price: 65 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU201: + cost: 93 + init_stock: 2950 + price: 104 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU202: + cost: 127 + init_stock: 3650 + price: 142 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU203: + cost: 396 + init_stock: 4100 + price: 440 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU204: + cost: 440 + init_stock: 2350 + price: 489 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU205: + cost: 117 + init_stock: 5000 + price: 130 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU206: + cost: 301 + init_stock: 550 + price: 335 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU207: + cost: 126 + init_stock: 4000 + price: 140 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU208: + cost: 441 + init_stock: 3850 + price: 491 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU209: + cost: 161 + init_stock: 1000 + price: 179 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU21: + cost: 110 + init_stock: 5000 + price: 123 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU210: + cost: 363 + init_stock: 3450 + price: 404 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU211: + cost: 156 + init_stock: 4550 + price: 174 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU212: + cost: 364 + init_stock: 3950 + price: 405 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU213: + cost: 108 + init_stock: 3200 + price: 121 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU214: + cost: 90 + init_stock: 500 + price: 101 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU215: + cost: 377 + init_stock: 2350 + price: 419 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU216: + cost: 297 + init_stock: 1150 + price: 330 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU217: + cost: 255 + init_stock: 3250 + price: 284 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU218: + cost: 184 + init_stock: 2950 + price: 205 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU219: + cost: 82 + init_stock: 2300 + price: 92 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU22: + cost: 443 + init_stock: 3300 + price: 493 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU220: + cost: 348 + init_stock: 4350 + price: 387 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU221: + cost: 35 + init_stock: 3900 + price: 39 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU222: + cost: 103 + init_stock: 1800 + price: 115 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU223: + cost: 176 + init_stock: 600 + price: 196 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU224: + cost: 348 + init_stock: 250 + price: 387 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU225: + cost: 147 + init_stock: 1450 + price: 164 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU226: + cost: 185 + init_stock: 650 + price: 206 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU227: + cost: 431 + init_stock: 1200 + price: 479 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU228: + cost: 12 + init_stock: 4500 + price: 14 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU229: + cost: 424 + init_stock: 2200 + price: 472 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU23: + cost: 348 + init_stock: 2100 + price: 387 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU230: + cost: 216 + init_stock: 1150 + price: 241 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU231: + cost: 43 + init_stock: 4050 + price: 48 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU232: + cost: 201 + init_stock: 4800 + price: 224 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU233: + cost: 324 + init_stock: 3750 + price: 360 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU234: + cost: 258 + init_stock: 250 + price: 287 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU235: + cost: 21 + init_stock: 2850 + price: 24 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU236: + cost: 139 + init_stock: 2750 + price: 155 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU237: + cost: 389 + init_stock: 2250 + price: 433 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU238: + cost: 57 + init_stock: 3300 + price: 64 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU239: + cost: 92 + init_stock: 4900 + price: 103 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU24: + cost: 87 + init_stock: 2350 + price: 97 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU240: + cost: 335 + init_stock: 2350 + price: 373 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU241: + cost: 395 + init_stock: 3550 + price: 439 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU242: + cost: 15 + init_stock: 2200 + price: 17 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU243: + cost: 316 + init_stock: 700 + price: 352 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU244: + cost: 156 + init_stock: 4100 + price: 174 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU245: + cost: 363 + init_stock: 3300 + price: 404 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU246: + cost: 270 + init_stock: 1500 + price: 300 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU247: + cost: 347 + init_stock: 1750 + price: 386 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU248: + cost: 319 + init_stock: 1450 + price: 355 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU249: + cost: 320 + init_stock: 1250 + price: 356 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU25: + cost: 144 + init_stock: 250 + price: 161 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU250: + cost: 114 + init_stock: 2700 + price: 127 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU251: + cost: 309 + init_stock: 3050 + price: 344 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU252: + cost: 162 + init_stock: 4150 + price: 181 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU253: + cost: 403 + init_stock: 800 + price: 448 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU254: + cost: 435 + init_stock: 2300 + price: 484 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU255: + cost: 261 + init_stock: 3350 + price: 290 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU256: + cost: 81 + init_stock: 3600 + price: 91 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU257: + cost: 313 + init_stock: 2850 + price: 348 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU258: + cost: 440 + init_stock: 2150 + price: 489 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU259: + cost: 299 + init_stock: 3450 + price: 333 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU26: + cost: 206 + init_stock: 3150 + price: 229 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU260: + cost: 438 + init_stock: 2600 + price: 487 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU261: + cost: 331 + init_stock: 1100 + price: 368 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU262: + cost: 298 + init_stock: 3900 + price: 332 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU263: + cost: 170 + init_stock: 3700 + price: 189 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU264: + cost: 324 + init_stock: 3950 + price: 361 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU265: + cost: 257 + init_stock: 2950 + price: 286 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU266: + cost: 115 + init_stock: 2350 + price: 128 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU267: + cost: 69 + init_stock: 4000 + price: 77 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU268: + cost: 198 + init_stock: 4450 + price: 221 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU269: + cost: 113 + init_stock: 2200 + price: 126 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU27: + cost: 333 + init_stock: 2800 + price: 370 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU270: + cost: 163 + init_stock: 3700 + price: 182 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU271: + cost: 207 + init_stock: 900 + price: 230 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU272: + cost: 329 + init_stock: 850 + price: 366 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU273: + cost: 378 + init_stock: 900 + price: 421 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU274: + cost: 26 + init_stock: 3850 + price: 29 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU275: + cost: 45 + init_stock: 2400 + price: 50 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU276: + cost: 146 + init_stock: 2700 + price: 163 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU277: + cost: 404 + init_stock: 2050 + price: 449 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU278: + cost: 94 + init_stock: 600 + price: 105 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU279: + cost: 45 + init_stock: 1950 + price: 51 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU28: + cost: 187 + init_stock: 2350 + price: 208 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU280: + cost: 265 + init_stock: 1650 + price: 295 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU281: + cost: 355 + init_stock: 3750 + price: 395 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU282: + cost: 56 + init_stock: 2300 + price: 63 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU283: + cost: 352 + init_stock: 4600 + price: 392 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU284: + cost: 309 + init_stock: 3350 + price: 344 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU285: + cost: 119 + init_stock: 4550 + price: 133 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU286: + cost: 303 + init_stock: 1950 + price: 337 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU287: + cost: 337 + init_stock: 2800 + price: 375 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU288: + cost: 162 + init_stock: 1900 + price: 181 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU289: + cost: 60 + init_stock: 1550 + price: 67 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU29: + cost: 220 + init_stock: 2900 + price: 245 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU290: + cost: 394 + init_stock: 3350 + price: 438 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU291: + cost: 84 + init_stock: 3050 + price: 94 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU292: + cost: 167 + init_stock: 250 + price: 186 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU293: + cost: 145 + init_stock: 2750 + price: 162 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU294: + cost: 368 + init_stock: 450 + price: 409 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU295: + cost: 391 + init_stock: 4650 + price: 435 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU296: + cost: 333 + init_stock: 4600 + price: 370 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU297: + cost: 268 + init_stock: 1900 + price: 298 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU298: + cost: 257 + init_stock: 1750 + price: 286 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU299: + cost: 330 + init_stock: 2550 + price: 367 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU3: + cost: 364 + init_stock: 600 + price: 405 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU30: + cost: 323 + init_stock: 3450 + price: 359 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU300: + cost: 338 + init_stock: 2900 + price: 376 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU301: + cost: 389 + init_stock: 4150 + price: 433 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU302: + cost: 165 + init_stock: 550 + price: 184 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU303: + cost: 221 + init_stock: 4700 + price: 246 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU304: + cost: 377 + init_stock: 1150 + price: 419 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU305: + cost: 445 + init_stock: 5000 + price: 495 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU306: + cost: 431 + init_stock: 2100 + price: 479 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU307: + cost: 189 + init_stock: 3900 + price: 210 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU308: + cost: 187 + init_stock: 250 + price: 208 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU309: + cost: 90 + init_stock: 4600 + price: 101 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU31: + cost: 62 + init_stock: 2800 + price: 69 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU310: + cost: 210 + init_stock: 2200 + price: 234 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU311: + cost: 275 + init_stock: 4000 + price: 306 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU312: + cost: 261 + init_stock: 1250 + price: 291 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU313: + cost: 291 + init_stock: 1900 + price: 324 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU314: + cost: 363 + init_stock: 1450 + price: 404 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU315: + cost: 423 + init_stock: 4200 + price: 471 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU316: + cost: 181 + init_stock: 900 + price: 202 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU317: + cost: 194 + init_stock: 1200 + price: 216 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU318: + cost: 250 + init_stock: 4250 + price: 278 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU319: + cost: 231 + init_stock: 2650 + price: 257 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU32: + cost: 302 + init_stock: 1100 + price: 336 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU320: + cost: 176 + init_stock: 1950 + price: 196 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU321: + cost: 60 + init_stock: 800 + price: 67 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU322: + cost: 216 + init_stock: 5000 + price: 240 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU323: + cost: 59 + init_stock: 1950 + price: 66 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU324: + cost: 397 + init_stock: 4650 + price: 442 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU325: + cost: 407 + init_stock: 3450 + price: 453 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU326: + cost: 310 + init_stock: 1200 + price: 345 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU327: + cost: 411 + init_stock: 700 + price: 457 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU328: + cost: 351 + init_stock: 2250 + price: 390 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU329: + cost: 60 + init_stock: 2100 + price: 67 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU33: + cost: 374 + init_stock: 4600 + price: 416 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU330: + cost: 275 + init_stock: 1950 + price: 306 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU331: + cost: 124 + init_stock: 2050 + price: 138 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU332: + cost: 351 + init_stock: 4800 + price: 390 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU333: + cost: 436 + init_stock: 2650 + price: 485 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU334: + cost: 164 + init_stock: 2850 + price: 183 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU335: + cost: 72 + init_stock: 4050 + price: 80 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU336: + cost: 266 + init_stock: 1400 + price: 296 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU337: + cost: 220 + init_stock: 1450 + price: 245 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU338: + cost: 78 + init_stock: 4050 + price: 87 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU339: + cost: 238 + init_stock: 3150 + price: 265 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU34: + cost: 85 + init_stock: 650 + price: 95 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU340: + cost: 432 + init_stock: 4350 + price: 480 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU341: + cost: 415 + init_stock: 3500 + price: 462 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU342: + cost: 129 + init_stock: 450 + price: 144 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU343: + cost: 279 + init_stock: 750 + price: 310 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU344: + cost: 321 + init_stock: 4350 + price: 357 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU345: + cost: 397 + init_stock: 4450 + price: 442 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU346: + cost: 315 + init_stock: 2100 + price: 350 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU347: + cost: 447 + init_stock: 4100 + price: 497 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU348: + cost: 132 + init_stock: 1000 + price: 147 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU349: + cost: 60 + init_stock: 3350 + price: 67 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU35: + cost: 240 + init_stock: 4300 + price: 267 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU350: + cost: 251 + init_stock: 4600 + price: 279 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU351: + cost: 139 + init_stock: 3350 + price: 155 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU352: + cost: 63 + init_stock: 900 + price: 71 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU353: + cost: 227 + init_stock: 4650 + price: 253 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU354: + cost: 356 + init_stock: 3950 + price: 396 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU355: + cost: 56 + init_stock: 500 + price: 63 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU356: + cost: 313 + init_stock: 1450 + price: 348 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU357: + cost: 449 + init_stock: 4600 + price: 499 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU358: + cost: 242 + init_stock: 3450 + price: 269 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU359: + cost: 73 + init_stock: 3750 + price: 82 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU36: + cost: 94 + init_stock: 500 + price: 105 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU360: + cost: 435 + init_stock: 1250 + price: 484 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU361: + cost: 146 + init_stock: 4500 + price: 163 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU362: + cost: 417 + init_stock: 4350 + price: 464 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU363: + cost: 46 + init_stock: 3900 + price: 52 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU364: + cost: 348 + init_stock: 1650 + price: 387 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU365: + cost: 142 + init_stock: 4150 + price: 158 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU366: + cost: 399 + init_stock: 4300 + price: 444 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU367: + cost: 244 + init_stock: 2300 + price: 272 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU368: + cost: 424 + init_stock: 1650 + price: 472 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU369: + cost: 86 + init_stock: 3750 + price: 96 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU37: + cost: 59 + init_stock: 2950 + price: 66 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU370: + cost: 100 + init_stock: 750 + price: 112 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU371: + cost: 295 + init_stock: 250 + price: 328 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU372: + cost: 60 + init_stock: 1600 + price: 67 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU373: + cost: 52 + init_stock: 3350 + price: 58 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU374: + cost: 34 + init_stock: 2700 + price: 38 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU375: + cost: 254 + init_stock: 3600 + price: 283 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU376: + cost: 140 + init_stock: 4100 + price: 156 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU377: + cost: 70 + init_stock: 3350 + price: 78 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU378: + cost: 381 + init_stock: 1750 + price: 424 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU379: + cost: 9 + init_stock: 2450 + price: 11 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU38: + cost: 309 + init_stock: 2150 + price: 344 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU380: + cost: 353 + init_stock: 2650 + price: 393 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU381: + cost: 428 + init_stock: 300 + price: 476 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU382: + cost: 112 + init_stock: 3550 + price: 125 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU383: + cost: 273 + init_stock: 4600 + price: 304 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU384: + cost: 288 + init_stock: 450 + price: 320 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU385: + cost: 108 + init_stock: 650 + price: 120 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU386: + cost: 265 + init_stock: 1550 + price: 295 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU387: + cost: 100 + init_stock: 4850 + price: 112 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU388: + cost: 364 + init_stock: 2200 + price: 405 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU389: + cost: 338 + init_stock: 3500 + price: 376 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU39: + cost: 22 + init_stock: 2550 + price: 25 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU390: + cost: 129 + init_stock: 1950 + price: 144 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU391: + cost: 209 + init_stock: 3350 + price: 233 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU392: + cost: 146 + init_stock: 3700 + price: 163 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU393: + cost: 438 + init_stock: 3350 + price: 487 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU394: + cost: 138 + init_stock: 2650 + price: 154 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU395: + cost: 439 + init_stock: 1650 + price: 488 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU396: + cost: 299 + init_stock: 3050 + price: 333 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU397: + cost: 414 + init_stock: 2550 + price: 460 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU398: + cost: 210 + init_stock: 2900 + price: 234 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU399: + cost: 338 + init_stock: 1850 + price: 376 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU4: + cost: 395 + init_stock: 350 + price: 439 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU40: + cost: 441 + init_stock: 4950 + price: 490 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU400: + cost: 400 + init_stock: 4200 + price: 445 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU401: + cost: 369 + init_stock: 3900 + price: 410 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU402: + cost: 77 + init_stock: 350 + price: 86 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU403: + cost: 80 + init_stock: 4950 + price: 89 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU404: + cost: 258 + init_stock: 3050 + price: 287 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU405: + cost: 414 + init_stock: 950 + price: 460 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU406: + cost: 294 + init_stock: 5000 + price: 327 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU407: + cost: 23 + init_stock: 2300 + price: 26 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU408: + cost: 399 + init_stock: 400 + price: 444 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU409: + cost: 231 + init_stock: 4550 + price: 257 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU41: + cost: 126 + init_stock: 3950 + price: 141 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU410: + cost: 63 + init_stock: 800 + price: 70 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU411: + cost: 189 + init_stock: 4750 + price: 210 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU412: + cost: 257 + init_stock: 3100 + price: 286 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU413: + cost: 362 + init_stock: 4150 + price: 403 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU414: + cost: 148 + init_stock: 4350 + price: 165 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU415: + cost: 261 + init_stock: 1150 + price: 291 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU416: + cost: 205 + init_stock: 450 + price: 228 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU417: + cost: 398 + init_stock: 3600 + price: 443 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU418: + cost: 412 + init_stock: 650 + price: 458 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU419: + cost: 272 + init_stock: 4450 + price: 303 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU42: + cost: 415 + init_stock: 1300 + price: 462 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU420: + cost: 241 + init_stock: 2100 + price: 268 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU421: + cost: 192 + init_stock: 2300 + price: 214 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU422: + cost: 46 + init_stock: 2700 + price: 52 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU423: + cost: 169 + init_stock: 3300 + price: 188 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU424: + cost: 170 + init_stock: 550 + price: 189 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU425: + cost: 145 + init_stock: 600 + price: 162 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU426: + cost: 112 + init_stock: 4900 + price: 125 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU427: + cost: 371 + init_stock: 4700 + price: 413 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU428: + cost: 351 + init_stock: 3150 + price: 391 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU429: + cost: 35 + init_stock: 2050 + price: 39 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU43: + cost: 195 + init_stock: 3400 + price: 217 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU430: + cost: 194 + init_stock: 3650 + price: 216 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU431: + cost: 295 + init_stock: 1050 + price: 328 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU432: + cost: 22 + init_stock: 2300 + price: 25 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU433: + cost: 313 + init_stock: 2250 + price: 348 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU434: + cost: 33 + init_stock: 1500 + price: 37 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU435: + cost: 244 + init_stock: 1500 + price: 272 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU436: + cost: 64 + init_stock: 4050 + price: 72 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU437: + cost: 103 + init_stock: 700 + price: 115 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU438: + cost: 128 + init_stock: 500 + price: 143 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU439: + cost: 230 + init_stock: 1900 + price: 256 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU44: + cost: 324 + init_stock: 2400 + price: 360 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU440: + cost: 223 + init_stock: 4100 + price: 248 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU441: + cost: 227 + init_stock: 2800 + price: 253 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU442: + cost: 423 + init_stock: 4400 + price: 470 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU443: + cost: 196 + init_stock: 3650 + price: 218 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU444: + cost: 140 + init_stock: 300 + price: 156 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU445: + cost: 234 + init_stock: 4300 + price: 260 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU446: + cost: 447 + init_stock: 2450 + price: 497 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU447: + cost: 176 + init_stock: 250 + price: 196 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU448: + cost: 436 + init_stock: 3550 + price: 485 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU449: + cost: 253 + init_stock: 1900 + price: 282 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU45: + cost: 227 + init_stock: 500 + price: 253 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU450: + cost: 52 + init_stock: 4900 + price: 58 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU451: + cost: 421 + init_stock: 3450 + price: 468 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU452: + cost: 56 + init_stock: 3950 + price: 63 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU453: + cost: 33 + init_stock: 1750 + price: 37 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU454: + cost: 444 + init_stock: 4250 + price: 494 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU455: + cost: 122 + init_stock: 1300 + price: 136 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU456: + cost: 304 + init_stock: 1250 + price: 338 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU457: + cost: 152 + init_stock: 3200 + price: 169 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU458: + cost: 362 + init_stock: 350 + price: 403 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU459: + cost: 382 + init_stock: 1700 + price: 425 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU46: + cost: 269 + init_stock: 2500 + price: 299 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU460: + cost: 364 + init_stock: 3650 + price: 405 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU461: + cost: 225 + init_stock: 2400 + price: 251 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU462: + cost: 403 + init_stock: 450 + price: 448 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU463: + cost: 168 + init_stock: 4100 + price: 187 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU464: + cost: 251 + init_stock: 1700 + price: 279 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU465: + cost: 132 + init_stock: 5000 + price: 147 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU466: + cost: 87 + init_stock: 2100 + price: 97 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU467: + cost: 127 + init_stock: 1800 + price: 142 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU468: + cost: 49 + init_stock: 2500 + price: 55 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU469: + cost: 160 + init_stock: 750 + price: 178 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU47: + cost: 108 + init_stock: 1950 + price: 121 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU470: + cost: 335 + init_stock: 1600 + price: 373 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU471: + cost: 283 + init_stock: 3550 + price: 315 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU472: + cost: 378 + init_stock: 2950 + price: 421 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU473: + cost: 175 + init_stock: 1050 + price: 195 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU474: + cost: 403 + init_stock: 4300 + price: 448 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU475: + cost: 30 + init_stock: 4700 + price: 34 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU476: + cost: 225 + init_stock: 4500 + price: 251 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU477: + cost: 260 + init_stock: 2350 + price: 289 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU478: + cost: 431 + init_stock: 4400 + price: 479 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU479: + cost: 329 + init_stock: 1900 + price: 366 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU48: + cost: 306 + init_stock: 2900 + price: 340 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU480: + cost: 16 + init_stock: 1500 + price: 18 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU481: + cost: 297 + init_stock: 4400 + price: 330 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU482: + cost: 84 + init_stock: 4350 + price: 94 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU483: + cost: 268 + init_stock: 1700 + price: 298 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU484: + cost: 75 + init_stock: 3650 + price: 84 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU485: + cost: 286 + init_stock: 3350 + price: 318 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU486: + cost: 109 + init_stock: 2600 + price: 122 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU487: + cost: 249 + init_stock: 3300 + price: 277 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU488: + cost: 32 + init_stock: 1350 + price: 36 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU489: + cost: 53 + init_stock: 1550 + price: 59 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU49: + cost: 236 + init_stock: 4750 + price: 263 + product_unit_cost: 1 + production_rate: 4750 + service_level: 0.95 + type: production + vlt: 3 + SKU490: + cost: 144 + init_stock: 4050 + price: 161 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU491: + cost: 399 + init_stock: 3750 + price: 444 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU492: + cost: 47 + init_stock: 4000 + price: 53 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU493: + cost: 414 + init_stock: 1350 + price: 461 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU494: + cost: 130 + init_stock: 4700 + price: 145 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU495: + cost: 134 + init_stock: 3100 + price: 149 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU496: + cost: 307 + init_stock: 2950 + price: 342 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU497: + cost: 29 + init_stock: 450 + price: 33 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU498: + cost: 274 + init_stock: 3250 + price: 305 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU499: + cost: 276 + init_stock: 1450 + price: 307 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU5: + cost: 419 + init_stock: 3550 + price: 466 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU50: + cost: 253 + init_stock: 1850 + price: 282 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU500: + cost: 187 + init_stock: 2100 + price: 208 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU501: + cost: 174 + init_stock: 3800 + price: 194 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU502: + cost: 62 + init_stock: 3250 + price: 69 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU503: + cost: 187 + init_stock: 2850 + price: 208 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU504: + cost: 225 + init_stock: 1500 + price: 251 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU505: + cost: 383 + init_stock: 2950 + price: 426 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU506: + cost: 134 + init_stock: 4650 + price: 149 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU507: + cost: 168 + init_stock: 750 + price: 187 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU508: + cost: 244 + init_stock: 4800 + price: 272 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU509: + cost: 267 + init_stock: 4050 + price: 297 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU51: + cost: 265 + init_stock: 3350 + price: 295 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU510: + cost: 84 + init_stock: 450 + price: 94 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU511: + cost: 324 + init_stock: 3750 + price: 361 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU512: + cost: 420 + init_stock: 1100 + price: 467 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU513: + cost: 308 + init_stock: 350 + price: 343 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU514: + cost: 167 + init_stock: 3150 + price: 186 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU515: + cost: 130 + init_stock: 2700 + price: 145 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU516: + cost: 57 + init_stock: 1900 + price: 64 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU517: + cost: 105 + init_stock: 450 + price: 117 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU518: + cost: 23 + init_stock: 1050 + price: 26 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU519: + cost: 209 + init_stock: 2500 + price: 233 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU52: + cost: 19 + init_stock: 3350 + price: 22 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU520: + cost: 188 + init_stock: 1100 + price: 209 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU521: + cost: 198 + init_stock: 2500 + price: 220 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU522: + cost: 140 + init_stock: 4350 + price: 156 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU523: + cost: 58 + init_stock: 5000 + price: 65 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU524: + cost: 264 + init_stock: 4700 + price: 294 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU525: + cost: 388 + init_stock: 2100 + price: 432 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU526: + cost: 377 + init_stock: 1200 + price: 419 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU527: + cost: 117 + init_stock: 3500 + price: 131 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU528: + cost: 284 + init_stock: 1050 + price: 316 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU529: + cost: 268 + init_stock: 1550 + price: 298 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU53: + cost: 135 + init_stock: 3500 + price: 151 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU530: + cost: 117 + init_stock: 2250 + price: 131 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU531: + cost: 92 + init_stock: 3000 + price: 103 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU532: + cost: 315 + init_stock: 3850 + price: 351 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU533: + cost: 266 + init_stock: 2100 + price: 296 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU534: + cost: 382 + init_stock: 2050 + price: 425 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU535: + cost: 273 + init_stock: 4300 + price: 304 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU536: + cost: 322 + init_stock: 2600 + price: 358 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU537: + cost: 288 + init_stock: 450 + price: 321 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU538: + cost: 411 + init_stock: 2500 + price: 457 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU539: + cost: 148 + init_stock: 3900 + price: 165 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU54: + cost: 142 + init_stock: 2300 + price: 158 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU540: + cost: 349 + init_stock: 2100 + price: 388 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU541: + cost: 117 + init_stock: 1450 + price: 131 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU542: + cost: 34 + init_stock: 1300 + price: 38 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU543: + cost: 387 + init_stock: 4250 + price: 430 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU544: + cost: 311 + init_stock: 4850 + price: 346 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU545: + cost: 157 + init_stock: 750 + price: 175 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU546: + cost: 441 + init_stock: 4800 + price: 491 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU547: + cost: 144 + init_stock: 2200 + price: 161 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU548: + cost: 99 + init_stock: 300 + price: 111 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU549: + cost: 348 + init_stock: 3950 + price: 387 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU55: + cost: 433 + init_stock: 3250 + price: 482 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU550: + cost: 233 + init_stock: 4700 + price: 259 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU551: + cost: 41 + init_stock: 1550 + price: 46 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU552: + cost: 171 + init_stock: 4500 + price: 191 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU553: + cost: 187 + init_stock: 1500 + price: 208 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU554: + cost: 306 + init_stock: 2050 + price: 340 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU555: + cost: 440 + init_stock: 4200 + price: 489 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU556: + cost: 99 + init_stock: 1500 + price: 110 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU557: + cost: 300 + init_stock: 3650 + price: 334 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU558: + cost: 359 + init_stock: 850 + price: 399 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU559: + cost: 281 + init_stock: 2700 + price: 313 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU56: + cost: 339 + init_stock: 3700 + price: 377 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU560: + cost: 254 + init_stock: 2050 + price: 283 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU561: + cost: 181 + init_stock: 1950 + price: 202 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU562: + cost: 312 + init_stock: 4450 + price: 347 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU563: + cost: 360 + init_stock: 250 + price: 401 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU564: + cost: 98 + init_stock: 450 + price: 109 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU565: + cost: 51 + init_stock: 3750 + price: 57 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU566: + cost: 117 + init_stock: 550 + price: 131 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU567: + cost: 324 + init_stock: 450 + price: 361 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU568: + cost: 67 + init_stock: 3900 + price: 75 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU569: + cost: 425 + init_stock: 2400 + price: 473 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU57: + cost: 323 + init_stock: 2500 + price: 359 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU570: + cost: 349 + init_stock: 4150 + price: 388 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU571: + cost: 151 + init_stock: 1950 + price: 168 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU572: + cost: 23 + init_stock: 2250 + price: 26 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU573: + cost: 221 + init_stock: 2050 + price: 246 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU574: + cost: 84 + init_stock: 2500 + price: 94 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU575: + cost: 213 + init_stock: 3950 + price: 237 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU576: + cost: 238 + init_stock: 3900 + price: 265 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU577: + cost: 16 + init_stock: 4350 + price: 18 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU578: + cost: 90 + init_stock: 3550 + price: 100 + product_unit_cost: 1 + production_rate: 3550 + service_level: 0.95 + type: production + vlt: 3 + SKU579: + cost: 373 + init_stock: 450 + price: 415 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU58: + cost: 325 + init_stock: 2400 + price: 362 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU580: + cost: 182 + init_stock: 250 + price: 203 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU581: + cost: 136 + init_stock: 4300 + price: 152 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU582: + cost: 323 + init_stock: 4800 + price: 359 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU583: + cost: 77 + init_stock: 4350 + price: 86 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU584: + cost: 29 + init_stock: 2250 + price: 33 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU585: + cost: 27 + init_stock: 1000 + price: 31 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU586: + cost: 54 + init_stock: 2200 + price: 61 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU587: + cost: 261 + init_stock: 3900 + price: 290 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU588: + cost: 428 + init_stock: 2200 + price: 476 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU589: + cost: 219 + init_stock: 1700 + price: 244 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU59: + cost: 130 + init_stock: 2550 + price: 145 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU590: + cost: 63 + init_stock: 1550 + price: 70 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU591: + cost: 185 + init_stock: 4200 + price: 206 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU592: + cost: 196 + init_stock: 2300 + price: 218 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU593: + cost: 111 + init_stock: 2200 + price: 124 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU594: + cost: 214 + init_stock: 2500 + price: 238 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU595: + cost: 65 + init_stock: 2150 + price: 73 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU596: + cost: 388 + init_stock: 350 + price: 432 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU597: + cost: 226 + init_stock: 4350 + price: 252 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU598: + cost: 313 + init_stock: 700 + price: 348 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU599: + cost: 48 + init_stock: 3400 + price: 54 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU6: + cost: 109 + init_stock: 4400 + price: 122 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU60: + cost: 180 + init_stock: 1950 + price: 200 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU600: + cost: 347 + init_stock: 3250 + price: 386 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU601: + cost: 124 + init_stock: 1950 + price: 138 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU602: + cost: 165 + init_stock: 4900 + price: 184 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU603: + cost: 250 + init_stock: 2100 + price: 278 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU604: + cost: 243 + init_stock: 2500 + price: 270 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU605: + cost: 259 + init_stock: 1200 + price: 288 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU606: + cost: 102 + init_stock: 550 + price: 114 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU607: + cost: 187 + init_stock: 3950 + price: 208 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU608: + cost: 35 + init_stock: 3650 + price: 39 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU609: + cost: 91 + init_stock: 950 + price: 102 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU61: + cost: 414 + init_stock: 3750 + price: 461 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU610: + cost: 404 + init_stock: 3400 + price: 449 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU611: + cost: 275 + init_stock: 3850 + price: 306 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU612: + cost: 351 + init_stock: 4400 + price: 391 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU613: + cost: 156 + init_stock: 350 + price: 174 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU614: + cost: 155 + init_stock: 750 + price: 173 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU615: + cost: 297 + init_stock: 4550 + price: 330 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU616: + cost: 208 + init_stock: 3650 + price: 232 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU617: + cost: 182 + init_stock: 3000 + price: 203 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU618: + cost: 69 + init_stock: 1150 + price: 77 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU619: + cost: 170 + init_stock: 4300 + price: 189 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU62: + cost: 111 + init_stock: 450 + price: 124 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU620: + cost: 207 + init_stock: 1950 + price: 231 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU621: + cost: 179 + init_stock: 3600 + price: 199 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU622: + cost: 227 + init_stock: 3250 + price: 253 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU623: + cost: 301 + init_stock: 2750 + price: 335 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU624: + cost: 433 + init_stock: 2700 + price: 482 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU625: + cost: 41 + init_stock: 4450 + price: 46 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU626: + cost: 405 + init_stock: 4450 + price: 451 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU627: + cost: 198 + init_stock: 4100 + price: 220 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU628: + cost: 111 + init_stock: 1300 + price: 124 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU629: + cost: 317 + init_stock: 4600 + price: 353 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU63: + cost: 61 + init_stock: 700 + price: 68 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU630: + cost: 109 + init_stock: 850 + price: 122 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU631: + cost: 266 + init_stock: 2450 + price: 296 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU632: + cost: 76 + init_stock: 4700 + price: 85 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU633: + cost: 165 + init_stock: 3350 + price: 184 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU634: + cost: 15 + init_stock: 3650 + price: 17 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU635: + cost: 306 + init_stock: 4650 + price: 341 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU636: + cost: 346 + init_stock: 1850 + price: 385 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU637: + cost: 74 + init_stock: 3050 + price: 83 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU638: + cost: 337 + init_stock: 400 + price: 375 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU639: + cost: 294 + init_stock: 2000 + price: 327 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU64: + cost: 32 + init_stock: 950 + price: 36 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU640: + cost: 247 + init_stock: 4550 + price: 275 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU641: + cost: 328 + init_stock: 4050 + price: 365 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU642: + cost: 372 + init_stock: 1250 + price: 414 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU643: + cost: 322 + init_stock: 2300 + price: 358 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU644: + cost: 41 + init_stock: 4100 + price: 46 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU645: + cost: 420 + init_stock: 4950 + price: 467 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU646: + cost: 170 + init_stock: 650 + price: 189 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU647: + cost: 272 + init_stock: 3850 + price: 303 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU648: + cost: 203 + init_stock: 2750 + price: 226 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU649: + cost: 178 + init_stock: 1250 + price: 198 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU65: + cost: 13 + init_stock: 4700 + price: 15 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU650: + cost: 315 + init_stock: 2800 + price: 351 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU651: + cost: 342 + init_stock: 4800 + price: 380 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU652: + cost: 110 + init_stock: 2000 + price: 123 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU653: + cost: 431 + init_stock: 4800 + price: 479 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU654: + cost: 59 + init_stock: 400 + price: 66 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU655: + cost: 299 + init_stock: 2100 + price: 333 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU656: + cost: 47 + init_stock: 3600 + price: 53 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU657: + cost: 65 + init_stock: 4050 + price: 73 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU658: + cost: 63 + init_stock: 1800 + price: 70 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU659: + cost: 18 + init_stock: 2800 + price: 21 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU66: + cost: 128 + init_stock: 4500 + price: 143 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU660: + cost: 438 + init_stock: 4150 + price: 487 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU661: + cost: 309 + init_stock: 3700 + price: 344 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU662: + cost: 334 + init_stock: 1900 + price: 372 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU663: + cost: 376 + init_stock: 2000 + price: 418 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU664: + cost: 315 + init_stock: 4200 + price: 351 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU665: + cost: 443 + init_stock: 2150 + price: 493 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU666: + cost: 306 + init_stock: 4400 + price: 341 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU667: + cost: 292 + init_stock: 650 + price: 325 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU668: + cost: 257 + init_stock: 3000 + price: 286 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU669: + cost: 66 + init_stock: 800 + price: 74 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU67: + cost: 222 + init_stock: 2650 + price: 247 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU670: + cost: 260 + init_stock: 4300 + price: 289 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU671: + cost: 240 + init_stock: 4150 + price: 267 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU672: + cost: 332 + init_stock: 650 + price: 369 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU673: + cost: 289 + init_stock: 2050 + price: 322 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU674: + cost: 200 + init_stock: 1100 + price: 223 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU675: + cost: 394 + init_stock: 1650 + price: 438 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU676: + cost: 219 + init_stock: 3050 + price: 244 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU677: + cost: 382 + init_stock: 2200 + price: 425 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU678: + cost: 151 + init_stock: 1800 + price: 168 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU679: + cost: 355 + init_stock: 2200 + price: 395 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU68: + cost: 152 + init_stock: 700 + price: 169 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU680: + cost: 234 + init_stock: 1500 + price: 260 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU681: + cost: 417 + init_stock: 4850 + price: 464 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU682: + cost: 333 + init_stock: 2650 + price: 370 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU683: + cost: 294 + init_stock: 3350 + price: 327 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU684: + cost: 319 + init_stock: 3950 + price: 355 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU685: + cost: 379 + init_stock: 2250 + price: 422 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU686: + cost: 56 + init_stock: 3150 + price: 63 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU687: + cost: 30 + init_stock: 3800 + price: 34 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU688: + cost: 435 + init_stock: 3850 + price: 484 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU689: + cost: 449 + init_stock: 750 + price: 499 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU69: + cost: 158 + init_stock: 3350 + price: 176 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU690: + cost: 393 + init_stock: 4150 + price: 437 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU691: + cost: 15 + init_stock: 3950 + price: 17 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU692: + cost: 202 + init_stock: 3250 + price: 225 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU693: + cost: 17 + init_stock: 3050 + price: 19 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU694: + cost: 187 + init_stock: 3750 + price: 208 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU695: + cost: 171 + init_stock: 350 + price: 190 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU696: + cost: 313 + init_stock: 2900 + price: 348 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU697: + cost: 409 + init_stock: 4000 + price: 455 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU698: + cost: 178 + init_stock: 3050 + price: 198 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU699: + cost: 27 + init_stock: 1000 + price: 31 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU7: + cost: 90 + init_stock: 4800 + price: 101 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU70: + cost: 167 + init_stock: 1750 + price: 186 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU700: + cost: 66 + init_stock: 450 + price: 74 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU701: + cost: 359 + init_stock: 3850 + price: 399 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU702: + cost: 73 + init_stock: 1700 + price: 82 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU703: + cost: 326 + init_stock: 1900 + price: 363 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU704: + cost: 345 + init_stock: 4350 + price: 384 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU705: + cost: 115 + init_stock: 3150 + price: 128 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU706: + cost: 163 + init_stock: 4550 + price: 182 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU707: + cost: 16 + init_stock: 3450 + price: 18 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU708: + cost: 160 + init_stock: 1400 + price: 178 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU709: + cost: 91 + init_stock: 2650 + price: 102 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU71: + cost: 283 + init_stock: 2250 + price: 315 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU710: + cost: 226 + init_stock: 950 + price: 252 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU711: + cost: 252 + init_stock: 3450 + price: 281 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU712: + cost: 208 + init_stock: 550 + price: 232 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU713: + cost: 256 + init_stock: 2150 + price: 285 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU714: + cost: 71 + init_stock: 2050 + price: 79 + product_unit_cost: 1 + production_rate: 2050 + service_level: 0.95 + type: production + vlt: 3 + SKU715: + cost: 210 + init_stock: 850 + price: 234 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU716: + cost: 63 + init_stock: 3750 + price: 70 + product_unit_cost: 1 + production_rate: 3750 + service_level: 0.95 + type: production + vlt: 3 + SKU717: + cost: 310 + init_stock: 400 + price: 345 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU718: + cost: 321 + init_stock: 2900 + price: 357 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU719: + cost: 306 + init_stock: 3250 + price: 340 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU72: + cost: 412 + init_stock: 4250 + price: 458 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU720: + cost: 292 + init_stock: 2100 + price: 325 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU721: + cost: 65 + init_stock: 550 + price: 73 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU722: + cost: 352 + init_stock: 4850 + price: 392 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU723: + cost: 286 + init_stock: 4450 + price: 318 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU724: + cost: 360 + init_stock: 1650 + price: 400 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU725: + cost: 157 + init_stock: 1850 + price: 175 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU726: + cost: 412 + init_stock: 4450 + price: 458 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU727: + cost: 376 + init_stock: 2850 + price: 418 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU728: + cost: 427 + init_stock: 1850 + price: 475 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU729: + cost: 291 + init_stock: 1800 + price: 324 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU73: + cost: 190 + init_stock: 2700 + price: 212 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU730: + cost: 14 + init_stock: 650 + price: 16 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU731: + cost: 79 + init_stock: 4100 + price: 88 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU732: + cost: 36 + init_stock: 3100 + price: 41 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU733: + cost: 283 + init_stock: 1300 + price: 315 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU734: + cost: 33 + init_stock: 2550 + price: 37 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU735: + cost: 239 + init_stock: 4950 + price: 266 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU736: + cost: 331 + init_stock: 1250 + price: 368 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU737: + cost: 427 + init_stock: 1550 + price: 475 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU738: + cost: 166 + init_stock: 2400 + price: 185 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU739: + cost: 427 + init_stock: 3700 + price: 475 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU74: + cost: 143 + init_stock: 2100 + price: 159 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU740: + cost: 351 + init_stock: 3200 + price: 390 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU741: + cost: 81 + init_stock: 2800 + price: 91 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU742: + cost: 169 + init_stock: 1100 + price: 188 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU743: + cost: 195 + init_stock: 2150 + price: 217 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU744: + cost: 341 + init_stock: 2900 + price: 379 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU745: + cost: 284 + init_stock: 4600 + price: 316 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU746: + cost: 393 + init_stock: 2000 + price: 437 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU747: + cost: 335 + init_stock: 3950 + price: 373 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU748: + cost: 247 + init_stock: 3300 + price: 275 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU749: + cost: 354 + init_stock: 2650 + price: 394 + product_unit_cost: 1 + production_rate: 2650 + service_level: 0.95 + type: production + vlt: 3 + SKU75: + cost: 117 + init_stock: 950 + price: 131 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU750: + cost: 230 + init_stock: 3100 + price: 256 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU751: + cost: 332 + init_stock: 3250 + price: 369 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU752: + cost: 233 + init_stock: 3900 + price: 259 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU753: + cost: 69 + init_stock: 3250 + price: 77 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU754: + cost: 348 + init_stock: 650 + price: 387 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU755: + cost: 318 + init_stock: 4000 + price: 354 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU756: + cost: 221 + init_stock: 4500 + price: 246 + product_unit_cost: 1 + production_rate: 4500 + service_level: 0.95 + type: production + vlt: 3 + SKU757: + cost: 36 + init_stock: 2850 + price: 40 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU758: + cost: 216 + init_stock: 2000 + price: 241 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU759: + cost: 391 + init_stock: 1900 + price: 435 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU76: + cost: 132 + init_stock: 3200 + price: 147 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU760: + cost: 158 + init_stock: 2550 + price: 176 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU761: + cost: 201 + init_stock: 2750 + price: 224 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU762: + cost: 237 + init_stock: 2550 + price: 264 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU763: + cost: 346 + init_stock: 3950 + price: 385 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU764: + cost: 314 + init_stock: 1700 + price: 349 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU765: + cost: 310 + init_stock: 650 + price: 345 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU766: + cost: 430 + init_stock: 1000 + price: 478 + product_unit_cost: 1 + production_rate: 1000 + service_level: 0.95 + type: production + vlt: 3 + SKU767: + cost: 85 + init_stock: 3800 + price: 95 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU768: + cost: 162 + init_stock: 4600 + price: 181 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU769: + cost: 21 + init_stock: 1450 + price: 24 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU77: + cost: 368 + init_stock: 3600 + price: 409 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU770: + cost: 135 + init_stock: 3100 + price: 150 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU771: + cost: 90 + init_stock: 350 + price: 101 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU772: + cost: 230 + init_stock: 300 + price: 256 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU773: + cost: 75 + init_stock: 4600 + price: 84 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU774: + cost: 402 + init_stock: 2950 + price: 447 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU775: + cost: 157 + init_stock: 4300 + price: 175 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU776: + cost: 92 + init_stock: 4250 + price: 103 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU777: + cost: 262 + init_stock: 2850 + price: 292 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU778: + cost: 182 + init_stock: 400 + price: 203 + product_unit_cost: 1 + production_rate: 400 + service_level: 0.95 + type: production + vlt: 3 + SKU779: + cost: 105 + init_stock: 2150 + price: 117 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU78: + cost: 210 + init_stock: 650 + price: 234 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU780: + cost: 62 + init_stock: 4100 + price: 69 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU781: + cost: 334 + init_stock: 1800 + price: 372 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU782: + cost: 24 + init_stock: 500 + price: 27 + product_unit_cost: 1 + production_rate: 500 + service_level: 0.95 + type: production + vlt: 3 + SKU783: + cost: 78 + init_stock: 4050 + price: 87 + product_unit_cost: 1 + production_rate: 4050 + service_level: 0.95 + type: production + vlt: 3 + SKU784: + cost: 278 + init_stock: 2600 + price: 309 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU785: + cost: 171 + init_stock: 3500 + price: 191 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU786: + cost: 81 + init_stock: 1100 + price: 91 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU787: + cost: 324 + init_stock: 3050 + price: 360 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU788: + cost: 315 + init_stock: 1400 + price: 351 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU789: + cost: 137 + init_stock: 2900 + price: 153 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU79: + cost: 220 + init_stock: 550 + price: 245 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU790: + cost: 375 + init_stock: 3300 + price: 417 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU791: + cost: 120 + init_stock: 1400 + price: 134 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU792: + cost: 281 + init_stock: 1050 + price: 313 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU793: + cost: 175 + init_stock: 3800 + price: 195 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU794: + cost: 292 + init_stock: 4000 + price: 325 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU795: + cost: 248 + init_stock: 2400 + price: 276 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU796: + cost: 402 + init_stock: 2450 + price: 447 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU797: + cost: 90 + init_stock: 1950 + price: 100 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU798: + cost: 383 + init_stock: 1500 + price: 426 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU799: + cost: 56 + init_stock: 350 + price: 63 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU8: + cost: 112 + init_stock: 3150 + price: 125 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU80: + cost: 146 + init_stock: 3500 + price: 163 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU800: + cost: 268 + init_stock: 4700 + price: 298 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU801: + cost: 350 + init_stock: 1800 + price: 389 + product_unit_cost: 1 + production_rate: 1800 + service_level: 0.95 + type: production + vlt: 3 + SKU802: + cost: 155 + init_stock: 3100 + price: 173 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU803: + cost: 244 + init_stock: 950 + price: 272 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU804: + cost: 351 + init_stock: 2350 + price: 390 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU805: + cost: 54 + init_stock: 1650 + price: 61 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU806: + cost: 142 + init_stock: 2600 + price: 158 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU807: + cost: 407 + init_stock: 1300 + price: 453 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU808: + cost: 224 + init_stock: 4550 + price: 249 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU809: + cost: 49 + init_stock: 3250 + price: 55 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU81: + cost: 163 + init_stock: 3300 + price: 182 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU810: + cost: 248 + init_stock: 300 + price: 276 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU811: + cost: 228 + init_stock: 1850 + price: 254 + product_unit_cost: 1 + production_rate: 1850 + service_level: 0.95 + type: production + vlt: 3 + SKU812: + cost: 226 + init_stock: 4000 + price: 252 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU813: + cost: 227 + init_stock: 350 + price: 253 + product_unit_cost: 1 + production_rate: 350 + service_level: 0.95 + type: production + vlt: 3 + SKU814: + cost: 436 + init_stock: 4850 + price: 485 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU815: + cost: 351 + init_stock: 1200 + price: 390 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU816: + cost: 136 + init_stock: 4550 + price: 152 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU817: + cost: 204 + init_stock: 250 + price: 227 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU818: + cost: 318 + init_stock: 2150 + price: 354 + product_unit_cost: 1 + production_rate: 2150 + service_level: 0.95 + type: production + vlt: 3 + SKU819: + cost: 271 + init_stock: 1350 + price: 302 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU82: + cost: 261 + init_stock: 1400 + price: 290 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU820: + cost: 237 + init_stock: 4100 + price: 264 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU821: + cost: 89 + init_stock: 3450 + price: 99 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU822: + cost: 122 + init_stock: 3500 + price: 136 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU823: + cost: 67 + init_stock: 1400 + price: 75 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU824: + cost: 153 + init_stock: 3500 + price: 170 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU825: + cost: 192 + init_stock: 750 + price: 214 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU826: + cost: 347 + init_stock: 2750 + price: 386 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU827: + cost: 133 + init_stock: 3200 + price: 148 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU828: + cost: 360 + init_stock: 3450 + price: 400 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU829: + cost: 54 + init_stock: 3700 + price: 61 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU83: + cost: 266 + init_stock: 850 + price: 296 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU830: + cost: 150 + init_stock: 1200 + price: 167 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU831: + cost: 235 + init_stock: 1450 + price: 262 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU832: + cost: 29 + init_stock: 4100 + price: 33 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU833: + cost: 360 + init_stock: 4900 + price: 400 + product_unit_cost: 1 + production_rate: 4900 + service_level: 0.95 + type: production + vlt: 3 + SKU834: + cost: 379 + init_stock: 250 + price: 422 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU835: + cost: 396 + init_stock: 2600 + price: 440 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU836: + cost: 290 + init_stock: 4800 + price: 323 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU837: + cost: 335 + init_stock: 1300 + price: 373 + product_unit_cost: 1 + production_rate: 1300 + service_level: 0.95 + type: production + vlt: 3 + SKU838: + cost: 410 + init_stock: 3850 + price: 456 + product_unit_cost: 1 + production_rate: 3850 + service_level: 0.95 + type: production + vlt: 3 + SKU839: + cost: 425 + init_stock: 3000 + price: 473 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU84: + cost: 286 + init_stock: 2100 + price: 318 + product_unit_cost: 1 + production_rate: 2100 + service_level: 0.95 + type: production + vlt: 3 + SKU840: + cost: 239 + init_stock: 2500 + price: 266 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU841: + cost: 256 + init_stock: 4250 + price: 285 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU842: + cost: 36 + init_stock: 4200 + price: 41 + product_unit_cost: 1 + production_rate: 4200 + service_level: 0.95 + type: production + vlt: 3 + SKU843: + cost: 324 + init_stock: 3600 + price: 360 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU844: + cost: 45 + init_stock: 3950 + price: 51 + product_unit_cost: 1 + production_rate: 3950 + service_level: 0.95 + type: production + vlt: 3 + SKU845: + cost: 259 + init_stock: 1100 + price: 288 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU846: + cost: 436 + init_stock: 1950 + price: 485 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU847: + cost: 349 + init_stock: 4550 + price: 388 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU848: + cost: 275 + init_stock: 2300 + price: 306 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU849: + cost: 288 + init_stock: 2000 + price: 320 + product_unit_cost: 1 + production_rate: 2000 + service_level: 0.95 + type: production + vlt: 3 + SKU85: + cost: 397 + init_stock: 5000 + price: 442 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU850: + cost: 164 + init_stock: 2850 + price: 183 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU851: + cost: 47 + init_stock: 3200 + price: 53 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU852: + cost: 275 + init_stock: 2700 + price: 306 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU853: + cost: 347 + init_stock: 2800 + price: 386 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU854: + cost: 190 + init_stock: 3250 + price: 212 + product_unit_cost: 1 + production_rate: 3250 + service_level: 0.95 + type: production + vlt: 3 + SKU855: + cost: 68 + init_stock: 3600 + price: 76 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU856: + cost: 342 + init_stock: 1600 + price: 380 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU857: + cost: 415 + init_stock: 5000 + price: 462 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU858: + cost: 72 + init_stock: 1200 + price: 80 + product_unit_cost: 1 + production_rate: 1200 + service_level: 0.95 + type: production + vlt: 3 + SKU859: + cost: 193 + init_stock: 1400 + price: 215 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU86: + cost: 347 + init_stock: 4250 + price: 386 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU860: + cost: 281 + init_stock: 750 + price: 313 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU861: + cost: 429 + init_stock: 650 + price: 477 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU862: + cost: 216 + init_stock: 800 + price: 240 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU863: + cost: 423 + init_stock: 4650 + price: 470 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU864: + cost: 182 + init_stock: 950 + price: 203 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU865: + cost: 129 + init_stock: 950 + price: 144 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU866: + cost: 154 + init_stock: 4400 + price: 172 + product_unit_cost: 1 + production_rate: 4400 + service_level: 0.95 + type: production + vlt: 3 + SKU867: + cost: 449 + init_stock: 5000 + price: 499 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU868: + cost: 36 + init_stock: 2500 + price: 41 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU869: + cost: 87 + init_stock: 4850 + price: 97 + product_unit_cost: 1 + production_rate: 4850 + service_level: 0.95 + type: production + vlt: 3 + SKU87: + cost: 331 + init_stock: 3450 + price: 368 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU870: + cost: 252 + init_stock: 3350 + price: 281 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU871: + cost: 90 + init_stock: 4950 + price: 101 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU872: + cost: 119 + init_stock: 1600 + price: 133 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU873: + cost: 380 + init_stock: 1550 + price: 423 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU874: + cost: 422 + init_stock: 3000 + price: 469 + product_unit_cost: 1 + production_rate: 3000 + service_level: 0.95 + type: production + vlt: 3 + SKU875: + cost: 45 + init_stock: 1150 + price: 51 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU876: + cost: 272 + init_stock: 3050 + price: 303 + product_unit_cost: 1 + production_rate: 3050 + service_level: 0.95 + type: production + vlt: 3 + SKU877: + cost: 36 + init_stock: 1750 + price: 40 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU878: + cost: 24 + init_stock: 3400 + price: 27 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU879: + cost: 135 + init_stock: 5000 + price: 150 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU88: + cost: 423 + init_stock: 600 + price: 471 + product_unit_cost: 1 + production_rate: 600 + service_level: 0.95 + type: production + vlt: 3 + SKU880: + cost: 389 + init_stock: 1550 + price: 433 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU881: + cost: 387 + init_stock: 3500 + price: 431 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU882: + cost: 174 + init_stock: 800 + price: 194 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU883: + cost: 255 + init_stock: 1900 + price: 284 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU884: + cost: 125 + init_stock: 1950 + price: 139 + product_unit_cost: 1 + production_rate: 1950 + service_level: 0.95 + type: production + vlt: 3 + SKU885: + cost: 44 + init_stock: 1350 + price: 49 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU886: + cost: 334 + init_stock: 3650 + price: 372 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU887: + cost: 239 + init_stock: 4350 + price: 266 + product_unit_cost: 1 + production_rate: 4350 + service_level: 0.95 + type: production + vlt: 3 + SKU888: + cost: 128 + init_stock: 450 + price: 143 + product_unit_cost: 1 + production_rate: 450 + service_level: 0.95 + type: production + vlt: 3 + SKU889: + cost: 90 + init_stock: 1050 + price: 101 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU89: + cost: 124 + init_stock: 4650 + price: 138 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU890: + cost: 144 + init_stock: 5000 + price: 161 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU891: + cost: 320 + init_stock: 1100 + price: 356 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU892: + cost: 281 + init_stock: 4600 + price: 313 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU893: + cost: 206 + init_stock: 4950 + price: 229 + product_unit_cost: 1 + production_rate: 4950 + service_level: 0.95 + type: production + vlt: 3 + SKU894: + cost: 116 + init_stock: 3700 + price: 129 + product_unit_cost: 1 + production_rate: 3700 + service_level: 0.95 + type: production + vlt: 3 + SKU895: + cost: 207 + init_stock: 650 + price: 230 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU896: + cost: 260 + init_stock: 4000 + price: 289 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU897: + cost: 353 + init_stock: 3600 + price: 393 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU898: + cost: 429 + init_stock: 550 + price: 477 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU899: + cost: 209 + init_stock: 2400 + price: 233 + product_unit_cost: 1 + production_rate: 2400 + service_level: 0.95 + type: production + vlt: 3 + SKU9: + cost: 149 + init_stock: 4800 + price: 166 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU90: + cost: 408 + init_stock: 2550 + price: 454 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU900: + cost: 142 + init_stock: 2750 + price: 158 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU901: + cost: 193 + init_stock: 1450 + price: 215 + product_unit_cost: 1 + production_rate: 1450 + service_level: 0.95 + type: production + vlt: 3 + SKU902: + cost: 112 + init_stock: 4000 + price: 125 + product_unit_cost: 1 + production_rate: 4000 + service_level: 0.95 + type: production + vlt: 3 + SKU903: + cost: 321 + init_stock: 1900 + price: 357 + product_unit_cost: 1 + production_rate: 1900 + service_level: 0.95 + type: production + vlt: 3 + SKU904: + cost: 446 + init_stock: 2550 + price: 496 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU905: + cost: 224 + init_stock: 1550 + price: 249 + product_unit_cost: 1 + production_rate: 1550 + service_level: 0.95 + type: production + vlt: 3 + SKU906: + cost: 149 + init_stock: 2600 + price: 166 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU907: + cost: 19 + init_stock: 4150 + price: 22 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU908: + cost: 367 + init_stock: 3900 + price: 408 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU909: + cost: 433 + init_stock: 4800 + price: 482 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU91: + cost: 272 + init_stock: 800 + price: 303 + product_unit_cost: 1 + production_rate: 800 + service_level: 0.95 + type: production + vlt: 3 + SKU910: + cost: 203 + init_stock: 1650 + price: 226 + product_unit_cost: 1 + production_rate: 1650 + service_level: 0.95 + type: production + vlt: 3 + SKU911: + cost: 414 + init_stock: 3500 + price: 461 + product_unit_cost: 1 + production_rate: 3500 + service_level: 0.95 + type: production + vlt: 3 + SKU912: + cost: 212 + init_stock: 1350 + price: 236 + product_unit_cost: 1 + production_rate: 1350 + service_level: 0.95 + type: production + vlt: 3 + SKU913: + cost: 289 + init_stock: 2300 + price: 322 + product_unit_cost: 1 + production_rate: 2300 + service_level: 0.95 + type: production + vlt: 3 + SKU914: + cost: 244 + init_stock: 3100 + price: 272 + product_unit_cost: 1 + production_rate: 3100 + service_level: 0.95 + type: production + vlt: 3 + SKU915: + cost: 303 + init_stock: 2800 + price: 337 + product_unit_cost: 1 + production_rate: 2800 + service_level: 0.95 + type: production + vlt: 3 + SKU916: + cost: 303 + init_stock: 4450 + price: 337 + product_unit_cost: 1 + production_rate: 4450 + service_level: 0.95 + type: production + vlt: 3 + SKU917: + cost: 232 + init_stock: 1500 + price: 258 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU918: + cost: 133 + init_stock: 2750 + price: 148 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU919: + cost: 353 + init_stock: 1250 + price: 393 + product_unit_cost: 1 + production_rate: 1250 + service_level: 0.95 + type: production + vlt: 3 + SKU92: + cost: 235 + init_stock: 3150 + price: 262 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU920: + cost: 321 + init_stock: 4300 + price: 357 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU921: + cost: 204 + init_stock: 3300 + price: 227 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU922: + cost: 100 + init_stock: 3350 + price: 112 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU923: + cost: 446 + init_stock: 2900 + price: 496 + product_unit_cost: 1 + production_rate: 2900 + service_level: 0.95 + type: production + vlt: 3 + SKU924: + cost: 284 + init_stock: 4250 + price: 316 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU925: + cost: 324 + init_stock: 750 + price: 360 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU926: + cost: 324 + init_stock: 3350 + price: 360 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU927: + cost: 234 + init_stock: 1050 + price: 260 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU928: + cost: 441 + init_stock: 4150 + price: 491 + product_unit_cost: 1 + production_rate: 4150 + service_level: 0.95 + type: production + vlt: 3 + SKU929: + cost: 323 + init_stock: 5000 + price: 359 + product_unit_cost: 1 + production_rate: 5000 + service_level: 0.95 + type: production + vlt: 3 + SKU93: + cost: 363 + init_stock: 3350 + price: 404 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU930: + cost: 178 + init_stock: 1400 + price: 198 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU931: + cost: 63 + init_stock: 700 + price: 71 + product_unit_cost: 1 + production_rate: 700 + service_level: 0.95 + type: production + vlt: 3 + SKU932: + cost: 146 + init_stock: 3300 + price: 163 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU933: + cost: 101 + init_stock: 3900 + price: 113 + product_unit_cost: 1 + production_rate: 3900 + service_level: 0.95 + type: production + vlt: 3 + SKU934: + cost: 197 + init_stock: 3350 + price: 219 + product_unit_cost: 1 + production_rate: 3350 + service_level: 0.95 + type: production + vlt: 3 + SKU935: + cost: 327 + init_stock: 4700 + price: 364 + product_unit_cost: 1 + production_rate: 4700 + service_level: 0.95 + type: production + vlt: 3 + SKU936: + cost: 21 + init_stock: 250 + price: 24 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU937: + cost: 121 + init_stock: 850 + price: 135 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU938: + cost: 388 + init_stock: 1050 + price: 432 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU939: + cost: 155 + init_stock: 2950 + price: 173 + product_unit_cost: 1 + production_rate: 2950 + service_level: 0.95 + type: production + vlt: 3 + SKU94: + cost: 165 + init_stock: 2350 + price: 184 + product_unit_cost: 1 + production_rate: 2350 + service_level: 0.95 + type: production + vlt: 3 + SKU940: + cost: 12 + init_stock: 4650 + price: 14 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU941: + cost: 72 + init_stock: 2850 + price: 80 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU942: + cost: 181 + init_stock: 650 + price: 202 + product_unit_cost: 1 + production_rate: 650 + service_level: 0.95 + type: production + vlt: 3 + SKU943: + cost: 124 + init_stock: 2450 + price: 138 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU944: + cost: 176 + init_stock: 2200 + price: 196 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU945: + cost: 126 + init_stock: 850 + price: 141 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU946: + cost: 292 + init_stock: 2450 + price: 325 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU947: + cost: 304 + init_stock: 4550 + price: 338 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU948: + cost: 382 + init_stock: 1400 + price: 425 + product_unit_cost: 1 + production_rate: 1400 + service_level: 0.95 + type: production + vlt: 3 + SKU949: + cost: 278 + init_stock: 250 + price: 309 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU95: + cost: 122 + init_stock: 1050 + price: 136 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU950: + cost: 91 + init_stock: 2700 + price: 102 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU951: + cost: 67 + init_stock: 900 + price: 75 + product_unit_cost: 1 + production_rate: 900 + service_level: 0.95 + type: production + vlt: 3 + SKU952: + cost: 140 + init_stock: 550 + price: 156 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU953: + cost: 124 + init_stock: 1750 + price: 138 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU954: + cost: 266 + init_stock: 4300 + price: 296 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU955: + cost: 49 + init_stock: 3150 + price: 55 + product_unit_cost: 1 + production_rate: 3150 + service_level: 0.95 + type: production + vlt: 3 + SKU956: + cost: 253 + init_stock: 4250 + price: 282 + product_unit_cost: 1 + production_rate: 4250 + service_level: 0.95 + type: production + vlt: 3 + SKU957: + cost: 274 + init_stock: 2550 + price: 305 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU958: + cost: 332 + init_stock: 3300 + price: 369 + product_unit_cost: 1 + production_rate: 3300 + service_level: 0.95 + type: production + vlt: 3 + SKU959: + cost: 72 + init_stock: 4100 + price: 81 + product_unit_cost: 1 + production_rate: 4100 + service_level: 0.95 + type: production + vlt: 3 + SKU96: + cost: 158 + init_stock: 2200 + price: 176 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU960: + cost: 132 + init_stock: 300 + price: 147 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU961: + cost: 237 + init_stock: 2200 + price: 264 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU962: + cost: 318 + init_stock: 2200 + price: 354 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU963: + cost: 314 + init_stock: 1050 + price: 349 + product_unit_cost: 1 + production_rate: 1050 + service_level: 0.95 + type: production + vlt: 3 + SKU964: + cost: 219 + init_stock: 3650 + price: 244 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU965: + cost: 111 + init_stock: 2550 + price: 124 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU966: + cost: 271 + init_stock: 2200 + price: 302 + product_unit_cost: 1 + production_rate: 2200 + service_level: 0.95 + type: production + vlt: 3 + SKU967: + cost: 60 + init_stock: 2250 + price: 67 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU968: + cost: 252 + init_stock: 2450 + price: 281 + product_unit_cost: 1 + production_rate: 2450 + service_level: 0.95 + type: production + vlt: 3 + SKU969: + cost: 224 + init_stock: 300 + price: 249 + product_unit_cost: 1 + production_rate: 300 + service_level: 0.95 + type: production + vlt: 3 + SKU97: + cost: 25 + init_stock: 1500 + price: 28 + product_unit_cost: 1 + production_rate: 1500 + service_level: 0.95 + type: production + vlt: 3 + SKU970: + cost: 219 + init_stock: 250 + price: 244 + product_unit_cost: 1 + production_rate: 250 + service_level: 0.95 + type: production + vlt: 3 + SKU971: + cost: 331 + init_stock: 3650 + price: 368 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU972: + cost: 188 + init_stock: 3450 + price: 209 + product_unit_cost: 1 + production_rate: 3450 + service_level: 0.95 + type: production + vlt: 3 + SKU973: + cost: 243 + init_stock: 550 + price: 271 + product_unit_cost: 1 + production_rate: 550 + service_level: 0.95 + type: production + vlt: 3 + SKU974: + cost: 153 + init_stock: 1600 + price: 170 + product_unit_cost: 1 + production_rate: 1600 + service_level: 0.95 + type: production + vlt: 3 + SKU975: + cost: 178 + init_stock: 1100 + price: 198 + product_unit_cost: 1 + production_rate: 1100 + service_level: 0.95 + type: production + vlt: 3 + SKU976: + cost: 160 + init_stock: 750 + price: 178 + product_unit_cost: 1 + production_rate: 750 + service_level: 0.95 + type: production + vlt: 3 + SKU977: + cost: 210 + init_stock: 2700 + price: 234 + product_unit_cost: 1 + production_rate: 2700 + service_level: 0.95 + type: production + vlt: 3 + SKU978: + cost: 423 + init_stock: 3200 + price: 470 + product_unit_cost: 1 + production_rate: 3200 + service_level: 0.95 + type: production + vlt: 3 + SKU979: + cost: 398 + init_stock: 4800 + price: 443 + product_unit_cost: 1 + production_rate: 4800 + service_level: 0.95 + type: production + vlt: 3 + SKU98: + cost: 398 + init_stock: 1750 + price: 443 + product_unit_cost: 1 + production_rate: 1750 + service_level: 0.95 + type: production + vlt: 3 + SKU980: + cost: 35 + init_stock: 3400 + price: 39 + product_unit_cost: 1 + production_rate: 3400 + service_level: 0.95 + type: production + vlt: 3 + SKU981: + cost: 433 + init_stock: 3600 + price: 482 + product_unit_cost: 1 + production_rate: 3600 + service_level: 0.95 + type: production + vlt: 3 + SKU982: + cost: 191 + init_stock: 2600 + price: 213 + product_unit_cost: 1 + production_rate: 2600 + service_level: 0.95 + type: production + vlt: 3 + SKU983: + cost: 404 + init_stock: 4600 + price: 449 + product_unit_cost: 1 + production_rate: 4600 + service_level: 0.95 + type: production + vlt: 3 + SKU984: + cost: 208 + init_stock: 4550 + price: 232 + product_unit_cost: 1 + production_rate: 4550 + service_level: 0.95 + type: production + vlt: 3 + SKU985: + cost: 261 + init_stock: 2550 + price: 290 + product_unit_cost: 1 + production_rate: 2550 + service_level: 0.95 + type: production + vlt: 3 + SKU986: + cost: 247 + init_stock: 850 + price: 275 + product_unit_cost: 1 + production_rate: 850 + service_level: 0.95 + type: production + vlt: 3 + SKU987: + cost: 390 + init_stock: 1700 + price: 434 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU988: + cost: 91 + init_stock: 1150 + price: 102 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + SKU989: + cost: 435 + init_stock: 4650 + price: 484 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU99: + cost: 324 + init_stock: 2250 + price: 361 + product_unit_cost: 1 + production_rate: 2250 + service_level: 0.95 + type: production + vlt: 3 + SKU990: + cost: 97 + init_stock: 2850 + price: 108 + product_unit_cost: 1 + production_rate: 2850 + service_level: 0.95 + type: production + vlt: 3 + SKU991: + cost: 368 + init_stock: 950 + price: 409 + product_unit_cost: 1 + production_rate: 950 + service_level: 0.95 + type: production + vlt: 3 + SKU992: + cost: 390 + init_stock: 4650 + price: 434 + product_unit_cost: 1 + production_rate: 4650 + service_level: 0.95 + type: production + vlt: 3 + SKU993: + cost: 130 + init_stock: 4300 + price: 145 + product_unit_cost: 1 + production_rate: 4300 + service_level: 0.95 + type: production + vlt: 3 + SKU994: + cost: 423 + init_stock: 2500 + price: 470 + product_unit_cost: 1 + production_rate: 2500 + service_level: 0.95 + type: production + vlt: 3 + SKU995: + cost: 216 + init_stock: 3800 + price: 241 + product_unit_cost: 1 + production_rate: 3800 + service_level: 0.95 + type: production + vlt: 3 + SKU996: + cost: 234 + init_stock: 3650 + price: 260 + product_unit_cost: 1 + production_rate: 3650 + service_level: 0.95 + type: production + vlt: 3 + SKU997: + cost: 360 + init_stock: 1700 + price: 400 + product_unit_cost: 1 + production_rate: 1700 + service_level: 0.95 + type: production + vlt: 3 + SKU998: + cost: 402 + init_stock: 2750 + price: 447 + product_unit_cost: 1 + production_rate: 2750 + service_level: 0.95 + type: production + vlt: 3 + SKU999: + cost: 71 + init_stock: 1150 + price: 79 + product_unit_cost: 1 + production_rate: 1150 + service_level: 0.95 + type: production + vlt: 3 + - children: + distribution: + children: + vehicles: + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + - *id001 + config: + unit_price: 1 + storage: + config: + capacity: 5324500 + unit_storage_cost: 1 + config: + delay_order_penalty: 1000 + order_cost: 500 + definition_ref: WarehouseFacility + name: WAREHOUSE0 + skus: + SKU0: + cost: 322 + init_stock: 1260 + price: 322 + service_level: 0.96 + vlt: 1 + SKU1: + cost: 284 + init_stock: 440 + price: 284 + service_level: 0.96 + vlt: 3 + SKU10: + cost: 68 + init_stock: 500 + price: 68 + service_level: 0.96 + vlt: 3 + SKU100: + cost: 16 + init_stock: 1300 + price: 16 + service_level: 0.96 + vlt: 3 + SKU101: + cost: 84 + init_stock: 1420 + price: 84 + service_level: 0.96 + vlt: 3 + SKU102: + cost: 328 + init_stock: 1860 + price: 328 + service_level: 0.96 + vlt: 2 + SKU103: + cost: 334 + init_stock: 1900 + price: 334 + service_level: 0.96 + vlt: 1 + SKU104: + cost: 49 + init_stock: 1300 + price: 49 + service_level: 0.96 + vlt: 3 + SKU105: + cost: 110 + init_stock: 1140 + price: 110 + service_level: 0.96 + vlt: 2 + SKU106: + cost: 251 + init_stock: 1460 + price: 251 + service_level: 0.96 + vlt: 3 + SKU107: + cost: 423 + init_stock: 1740 + price: 423 + service_level: 0.96 + vlt: 3 + SKU108: + cost: 458 + init_stock: 1840 + price: 458 + service_level: 0.96 + vlt: 3 + SKU109: + cost: 88 + init_stock: 1640 + price: 88 + service_level: 0.96 + vlt: 2 + SKU11: + cost: 400 + init_stock: 1020 + price: 400 + service_level: 0.96 + vlt: 2 + SKU110: + cost: 66 + init_stock: 280 + price: 66 + service_level: 0.96 + vlt: 3 + SKU111: + cost: 260 + init_stock: 1220 + price: 260 + service_level: 0.96 + vlt: 1 + SKU112: + cost: 61 + init_stock: 1900 + price: 61 + service_level: 0.96 + vlt: 2 + SKU113: + cost: 348 + init_stock: 620 + price: 348 + service_level: 0.96 + vlt: 3 + SKU114: + cost: 389 + init_stock: 540 + price: 389 + service_level: 0.96 + vlt: 1 + SKU115: + cost: 286 + init_stock: 1720 + price: 286 + service_level: 0.96 + vlt: 2 + SKU116: + cost: 496 + init_stock: 1440 + price: 496 + service_level: 0.96 + vlt: 3 + SKU117: + cost: 320 + init_stock: 1840 + price: 320 + service_level: 0.96 + vlt: 3 + SKU118: + cost: 183 + init_stock: 660 + price: 183 + service_level: 0.96 + vlt: 1 + SKU119: + cost: 209 + init_stock: 640 + price: 209 + service_level: 0.96 + vlt: 3 + SKU12: + cost: 112 + init_stock: 1680 + price: 112 + service_level: 0.96 + vlt: 1 + SKU120: + cost: 121 + init_stock: 1960 + price: 121 + service_level: 0.96 + vlt: 1 + SKU121: + cost: 40 + init_stock: 1700 + price: 40 + service_level: 0.96 + vlt: 1 + SKU122: + cost: 437 + init_stock: 140 + price: 437 + service_level: 0.96 + vlt: 3 + SKU123: + cost: 233 + init_stock: 380 + price: 233 + service_level: 0.96 + vlt: 3 + SKU124: + cost: 182 + init_stock: 720 + price: 182 + service_level: 0.96 + vlt: 2 + SKU125: + cost: 16 + init_stock: 1840 + price: 16 + service_level: 0.96 + vlt: 2 + SKU126: + cost: 36 + init_stock: 780 + price: 36 + service_level: 0.96 + vlt: 3 + SKU127: + cost: 217 + init_stock: 620 + price: 217 + service_level: 0.96 + vlt: 2 + SKU128: + cost: 165 + init_stock: 380 + price: 165 + service_level: 0.96 + vlt: 1 + SKU129: + cost: 143 + init_stock: 1000 + price: 143 + service_level: 0.96 + vlt: 2 + SKU13: + cost: 317 + init_stock: 1140 + price: 317 + service_level: 0.96 + vlt: 3 + SKU130: + cost: 348 + init_stock: 1960 + price: 348 + service_level: 0.96 + vlt: 3 + SKU131: + cost: 64 + init_stock: 900 + price: 64 + service_level: 0.96 + vlt: 1 + SKU132: + cost: 427 + init_stock: 420 + price: 427 + service_level: 0.96 + vlt: 2 + SKU133: + cost: 224 + init_stock: 580 + price: 224 + service_level: 0.96 + vlt: 2 + SKU134: + cost: 336 + init_stock: 1540 + price: 336 + service_level: 0.96 + vlt: 3 + SKU135: + cost: 153 + init_stock: 2000 + price: 153 + service_level: 0.96 + vlt: 1 + SKU136: + cost: 199 + init_stock: 1420 + price: 199 + service_level: 0.96 + vlt: 3 + SKU137: + cost: 93 + init_stock: 1480 + price: 93 + service_level: 0.96 + vlt: 2 + SKU138: + cost: 228 + init_stock: 720 + price: 228 + service_level: 0.96 + vlt: 1 + SKU139: + cost: 207 + init_stock: 480 + price: 207 + service_level: 0.96 + vlt: 1 + SKU14: + cost: 268 + init_stock: 1240 + price: 268 + service_level: 0.96 + vlt: 1 + SKU140: + cost: 261 + init_stock: 680 + price: 261 + service_level: 0.96 + vlt: 3 + SKU141: + cost: 190 + init_stock: 820 + price: 190 + service_level: 0.96 + vlt: 1 + SKU142: + cost: 320 + init_stock: 760 + price: 320 + service_level: 0.96 + vlt: 3 + SKU143: + cost: 318 + init_stock: 520 + price: 318 + service_level: 0.96 + vlt: 3 + SKU144: + cost: 400 + init_stock: 240 + price: 400 + service_level: 0.96 + vlt: 1 + SKU145: + cost: 399 + init_stock: 1640 + price: 399 + service_level: 0.96 + vlt: 1 + SKU146: + cost: 177 + init_stock: 960 + price: 177 + service_level: 0.96 + vlt: 3 + SKU147: + cost: 472 + init_stock: 1120 + price: 472 + service_level: 0.96 + vlt: 3 + SKU148: + cost: 313 + init_stock: 1540 + price: 313 + service_level: 0.96 + vlt: 3 + SKU149: + cost: 357 + init_stock: 1540 + price: 357 + service_level: 0.96 + vlt: 3 + SKU15: + cost: 52 + init_stock: 100 + price: 52 + service_level: 0.96 + vlt: 2 + SKU150: + cost: 106 + init_stock: 1400 + price: 106 + service_level: 0.96 + vlt: 2 + SKU151: + cost: 223 + init_stock: 1460 + price: 223 + service_level: 0.96 + vlt: 1 + SKU152: + cost: 10 + init_stock: 1020 + price: 10 + service_level: 0.96 + vlt: 3 + SKU153: + cost: 441 + init_stock: 1240 + price: 441 + service_level: 0.96 + vlt: 1 + SKU154: + cost: 77 + init_stock: 1700 + price: 77 + service_level: 0.96 + vlt: 3 + SKU155: + cost: 422 + init_stock: 1060 + price: 422 + service_level: 0.96 + vlt: 1 + SKU156: + cost: 10 + init_stock: 240 + price: 10 + service_level: 0.96 + vlt: 1 + SKU157: + cost: 410 + init_stock: 1500 + price: 410 + service_level: 0.96 + vlt: 2 + SKU158: + cost: 145 + init_stock: 1620 + price: 145 + service_level: 0.96 + vlt: 3 + SKU159: + cost: 193 + init_stock: 500 + price: 193 + service_level: 0.96 + vlt: 2 + SKU16: + cost: 175 + init_stock: 1160 + price: 175 + service_level: 0.96 + vlt: 3 + SKU160: + cost: 459 + init_stock: 1700 + price: 459 + service_level: 0.96 + vlt: 1 + SKU161: + cost: 239 + init_stock: 920 + price: 239 + service_level: 0.96 + vlt: 2 + SKU162: + cost: 158 + init_stock: 100 + price: 158 + service_level: 0.96 + vlt: 2 + SKU163: + cost: 486 + init_stock: 780 + price: 486 + service_level: 0.96 + vlt: 2 + SKU164: + cost: 496 + init_stock: 2000 + price: 496 + service_level: 0.96 + vlt: 1 + SKU165: + cost: 274 + init_stock: 660 + price: 274 + service_level: 0.96 + vlt: 3 + SKU166: + cost: 79 + init_stock: 1780 + price: 79 + service_level: 0.96 + vlt: 1 + SKU167: + cost: 443 + init_stock: 260 + price: 443 + service_level: 0.96 + vlt: 1 + SKU168: + cost: 357 + init_stock: 1740 + price: 357 + service_level: 0.96 + vlt: 2 + SKU169: + cost: 369 + init_stock: 1960 + price: 369 + service_level: 0.96 + vlt: 3 + SKU17: + cost: 346 + init_stock: 180 + price: 346 + service_level: 0.96 + vlt: 1 + SKU170: + cost: 68 + init_stock: 1100 + price: 68 + service_level: 0.96 + vlt: 2 + SKU171: + cost: 398 + init_stock: 1520 + price: 398 + service_level: 0.96 + vlt: 1 + SKU172: + cost: 200 + init_stock: 1420 + price: 200 + service_level: 0.96 + vlt: 2 + SKU173: + cost: 175 + init_stock: 1920 + price: 175 + service_level: 0.96 + vlt: 3 + SKU174: + cost: 291 + init_stock: 1520 + price: 291 + service_level: 0.96 + vlt: 2 + SKU175: + cost: 84 + init_stock: 1500 + price: 84 + service_level: 0.96 + vlt: 2 + SKU176: + cost: 407 + init_stock: 1320 + price: 407 + service_level: 0.96 + vlt: 1 + SKU177: + cost: 257 + init_stock: 620 + price: 257 + service_level: 0.96 + vlt: 1 + SKU178: + cost: 423 + init_stock: 100 + price: 423 + service_level: 0.96 + vlt: 2 + SKU179: + cost: 497 + init_stock: 1660 + price: 497 + service_level: 0.96 + vlt: 1 + SKU18: + cost: 258 + init_stock: 1620 + price: 258 + service_level: 0.96 + vlt: 1 + SKU180: + cost: 217 + init_stock: 1100 + price: 217 + service_level: 0.96 + vlt: 2 + SKU181: + cost: 143 + init_stock: 1200 + price: 143 + service_level: 0.96 + vlt: 1 + SKU182: + cost: 437 + init_stock: 1980 + price: 437 + service_level: 0.96 + vlt: 3 + SKU183: + cost: 145 + init_stock: 160 + price: 145 + service_level: 0.96 + vlt: 3 + SKU184: + cost: 73 + init_stock: 480 + price: 73 + service_level: 0.96 + vlt: 2 + SKU185: + cost: 10 + init_stock: 1800 + price: 10 + service_level: 0.96 + vlt: 2 + SKU186: + cost: 359 + init_stock: 440 + price: 359 + service_level: 0.96 + vlt: 1 + SKU187: + cost: 177 + init_stock: 600 + price: 177 + service_level: 0.96 + vlt: 3 + SKU188: + cost: 391 + init_stock: 1740 + price: 391 + service_level: 0.96 + vlt: 3 + SKU189: + cost: 358 + init_stock: 700 + price: 358 + service_level: 0.96 + vlt: 2 + SKU19: + cost: 477 + init_stock: 1820 + price: 477 + service_level: 0.96 + vlt: 3 + SKU190: + cost: 113 + init_stock: 340 + price: 113 + service_level: 0.96 + vlt: 3 + SKU191: + cost: 473 + init_stock: 1080 + price: 473 + service_level: 0.96 + vlt: 2 + SKU192: + cost: 415 + init_stock: 1220 + price: 415 + service_level: 0.96 + vlt: 2 + SKU193: + cost: 207 + init_stock: 600 + price: 207 + service_level: 0.96 + vlt: 2 + SKU194: + cost: 432 + init_stock: 100 + price: 432 + service_level: 0.96 + vlt: 2 + SKU195: + cost: 218 + init_stock: 620 + price: 218 + service_level: 0.96 + vlt: 2 + SKU196: + cost: 49 + init_stock: 1360 + price: 49 + service_level: 0.96 + vlt: 3 + SKU197: + cost: 303 + init_stock: 1140 + price: 303 + service_level: 0.96 + vlt: 1 + SKU198: + cost: 169 + init_stock: 1080 + price: 169 + service_level: 0.96 + vlt: 2 + SKU199: + cost: 449 + init_stock: 460 + price: 449 + service_level: 0.96 + vlt: 1 + SKU2: + cost: 331 + init_stock: 1400 + price: 331 + service_level: 0.96 + vlt: 3 + SKU20: + cost: 335 + init_stock: 500 + price: 335 + service_level: 0.96 + vlt: 3 + SKU200: + cost: 65 + init_stock: 500 + price: 65 + service_level: 0.96 + vlt: 1 + SKU201: + cost: 104 + init_stock: 1180 + price: 104 + service_level: 0.96 + vlt: 1 + SKU202: + cost: 142 + init_stock: 1460 + price: 142 + service_level: 0.96 + vlt: 1 + SKU203: + cost: 440 + init_stock: 1640 + price: 440 + service_level: 0.96 + vlt: 2 + SKU204: + cost: 489 + init_stock: 940 + price: 489 + service_level: 0.96 + vlt: 2 + SKU205: + cost: 130 + init_stock: 2000 + price: 130 + service_level: 0.96 + vlt: 3 + SKU206: + cost: 335 + init_stock: 220 + price: 335 + service_level: 0.96 + vlt: 2 + SKU207: + cost: 140 + init_stock: 1600 + price: 140 + service_level: 0.96 + vlt: 1 + SKU208: + cost: 491 + init_stock: 1540 + price: 491 + service_level: 0.96 + vlt: 1 + SKU209: + cost: 179 + init_stock: 400 + price: 179 + service_level: 0.96 + vlt: 3 + SKU21: + cost: 123 + init_stock: 2000 + price: 123 + service_level: 0.96 + vlt: 2 + SKU210: + cost: 404 + init_stock: 1380 + price: 404 + service_level: 0.96 + vlt: 3 + SKU211: + cost: 174 + init_stock: 1820 + price: 174 + service_level: 0.96 + vlt: 2 + SKU212: + cost: 405 + init_stock: 1580 + price: 405 + service_level: 0.96 + vlt: 3 + SKU213: + cost: 121 + init_stock: 1280 + price: 121 + service_level: 0.96 + vlt: 2 + SKU214: + cost: 101 + init_stock: 200 + price: 101 + service_level: 0.96 + vlt: 2 + SKU215: + cost: 419 + init_stock: 940 + price: 419 + service_level: 0.96 + vlt: 1 + SKU216: + cost: 330 + init_stock: 460 + price: 330 + service_level: 0.96 + vlt: 1 + SKU217: + cost: 284 + init_stock: 1300 + price: 284 + service_level: 0.96 + vlt: 2 + SKU218: + cost: 205 + init_stock: 1180 + price: 205 + service_level: 0.96 + vlt: 1 + SKU219: + cost: 92 + init_stock: 920 + price: 92 + service_level: 0.96 + vlt: 3 + SKU22: + cost: 493 + init_stock: 1320 + price: 493 + service_level: 0.96 + vlt: 1 + SKU220: + cost: 387 + init_stock: 1740 + price: 387 + service_level: 0.96 + vlt: 2 + SKU221: + cost: 39 + init_stock: 1560 + price: 39 + service_level: 0.96 + vlt: 2 + SKU222: + cost: 115 + init_stock: 720 + price: 115 + service_level: 0.96 + vlt: 2 + SKU223: + cost: 196 + init_stock: 240 + price: 196 + service_level: 0.96 + vlt: 2 + SKU224: + cost: 387 + init_stock: 100 + price: 387 + service_level: 0.96 + vlt: 2 + SKU225: + cost: 164 + init_stock: 580 + price: 164 + service_level: 0.96 + vlt: 1 + SKU226: + cost: 206 + init_stock: 260 + price: 206 + service_level: 0.96 + vlt: 2 + SKU227: + cost: 479 + init_stock: 480 + price: 479 + service_level: 0.96 + vlt: 3 + SKU228: + cost: 14 + init_stock: 1800 + price: 14 + service_level: 0.96 + vlt: 1 + SKU229: + cost: 472 + init_stock: 880 + price: 472 + service_level: 0.96 + vlt: 1 + SKU23: + cost: 387 + init_stock: 840 + price: 387 + service_level: 0.96 + vlt: 1 + SKU230: + cost: 241 + init_stock: 460 + price: 241 + service_level: 0.96 + vlt: 3 + SKU231: + cost: 48 + init_stock: 1620 + price: 48 + service_level: 0.96 + vlt: 3 + SKU232: + cost: 224 + init_stock: 1920 + price: 224 + service_level: 0.96 + vlt: 1 + SKU233: + cost: 360 + init_stock: 1500 + price: 360 + service_level: 0.96 + vlt: 2 + SKU234: + cost: 287 + init_stock: 100 + price: 287 + service_level: 0.96 + vlt: 1 + SKU235: + cost: 24 + init_stock: 1140 + price: 24 + service_level: 0.96 + vlt: 3 + SKU236: + cost: 155 + init_stock: 1100 + price: 155 + service_level: 0.96 + vlt: 2 + SKU237: + cost: 433 + init_stock: 900 + price: 433 + service_level: 0.96 + vlt: 3 + SKU238: + cost: 64 + init_stock: 1320 + price: 64 + service_level: 0.96 + vlt: 3 + SKU239: + cost: 103 + init_stock: 1960 + price: 103 + service_level: 0.96 + vlt: 1 + SKU24: + cost: 97 + init_stock: 940 + price: 97 + service_level: 0.96 + vlt: 2 + SKU240: + cost: 373 + init_stock: 940 + price: 373 + service_level: 0.96 + vlt: 2 + SKU241: + cost: 439 + init_stock: 1420 + price: 439 + service_level: 0.96 + vlt: 2 + SKU242: + cost: 17 + init_stock: 880 + price: 17 + service_level: 0.96 + vlt: 1 + SKU243: + cost: 352 + init_stock: 280 + price: 352 + service_level: 0.96 + vlt: 1 + SKU244: + cost: 174 + init_stock: 1640 + price: 174 + service_level: 0.96 + vlt: 2 + SKU245: + cost: 404 + init_stock: 1320 + price: 404 + service_level: 0.96 + vlt: 2 + SKU246: + cost: 300 + init_stock: 600 + price: 300 + service_level: 0.96 + vlt: 2 + SKU247: + cost: 386 + init_stock: 700 + price: 386 + service_level: 0.96 + vlt: 2 + SKU248: + cost: 355 + init_stock: 580 + price: 355 + service_level: 0.96 + vlt: 3 + SKU249: + cost: 356 + init_stock: 500 + price: 356 + service_level: 0.96 + vlt: 1 + SKU25: + cost: 161 + init_stock: 100 + price: 161 + service_level: 0.96 + vlt: 2 + SKU250: + cost: 127 + init_stock: 1080 + price: 127 + service_level: 0.96 + vlt: 3 + SKU251: + cost: 344 + init_stock: 1220 + price: 344 + service_level: 0.96 + vlt: 1 + SKU252: + cost: 181 + init_stock: 1660 + price: 181 + service_level: 0.96 + vlt: 1 + SKU253: + cost: 448 + init_stock: 320 + price: 448 + service_level: 0.96 + vlt: 1 + SKU254: + cost: 484 + init_stock: 920 + price: 484 + service_level: 0.96 + vlt: 3 + SKU255: + cost: 290 + init_stock: 1340 + price: 290 + service_level: 0.96 + vlt: 2 + SKU256: + cost: 91 + init_stock: 1440 + price: 91 + service_level: 0.96 + vlt: 3 + SKU257: + cost: 348 + init_stock: 1140 + price: 348 + service_level: 0.96 + vlt: 1 + SKU258: + cost: 489 + init_stock: 860 + price: 489 + service_level: 0.96 + vlt: 1 + SKU259: + cost: 333 + init_stock: 1380 + price: 333 + service_level: 0.96 + vlt: 1 + SKU26: + cost: 229 + init_stock: 1260 + price: 229 + service_level: 0.96 + vlt: 1 + SKU260: + cost: 487 + init_stock: 1040 + price: 487 + service_level: 0.96 + vlt: 3 + SKU261: + cost: 368 + init_stock: 440 + price: 368 + service_level: 0.96 + vlt: 1 + SKU262: + cost: 332 + init_stock: 1560 + price: 332 + service_level: 0.96 + vlt: 3 + SKU263: + cost: 189 + init_stock: 1480 + price: 189 + service_level: 0.96 + vlt: 3 + SKU264: + cost: 361 + init_stock: 1580 + price: 361 + service_level: 0.96 + vlt: 1 + SKU265: + cost: 286 + init_stock: 1180 + price: 286 + service_level: 0.96 + vlt: 3 + SKU266: + cost: 128 + init_stock: 940 + price: 128 + service_level: 0.96 + vlt: 2 + SKU267: + cost: 77 + init_stock: 1600 + price: 77 + service_level: 0.96 + vlt: 1 + SKU268: + cost: 221 + init_stock: 1780 + price: 221 + service_level: 0.96 + vlt: 2 + SKU269: + cost: 126 + init_stock: 880 + price: 126 + service_level: 0.96 + vlt: 2 + SKU27: + cost: 370 + init_stock: 1120 + price: 370 + service_level: 0.96 + vlt: 3 + SKU270: + cost: 182 + init_stock: 1480 + price: 182 + service_level: 0.96 + vlt: 3 + SKU271: + cost: 230 + init_stock: 360 + price: 230 + service_level: 0.96 + vlt: 1 + SKU272: + cost: 366 + init_stock: 340 + price: 366 + service_level: 0.96 + vlt: 2 + SKU273: + cost: 421 + init_stock: 360 + price: 421 + service_level: 0.96 + vlt: 2 + SKU274: + cost: 29 + init_stock: 1540 + price: 29 + service_level: 0.96 + vlt: 2 + SKU275: + cost: 50 + init_stock: 960 + price: 50 + service_level: 0.96 + vlt: 1 + SKU276: + cost: 163 + init_stock: 1080 + price: 163 + service_level: 0.96 + vlt: 3 + SKU277: + cost: 449 + init_stock: 820 + price: 449 + service_level: 0.96 + vlt: 1 + SKU278: + cost: 105 + init_stock: 240 + price: 105 + service_level: 0.96 + vlt: 3 + SKU279: + cost: 51 + init_stock: 780 + price: 51 + service_level: 0.96 + vlt: 2 + SKU28: + cost: 208 + init_stock: 940 + price: 208 + service_level: 0.96 + vlt: 2 + SKU280: + cost: 295 + init_stock: 660 + price: 295 + service_level: 0.96 + vlt: 2 + SKU281: + cost: 395 + init_stock: 1500 + price: 395 + service_level: 0.96 + vlt: 1 + SKU282: + cost: 63 + init_stock: 920 + price: 63 + service_level: 0.96 + vlt: 2 + SKU283: + cost: 392 + init_stock: 1840 + price: 392 + service_level: 0.96 + vlt: 3 + SKU284: + cost: 344 + init_stock: 1340 + price: 344 + service_level: 0.96 + vlt: 3 + SKU285: + cost: 133 + init_stock: 1820 + price: 133 + service_level: 0.96 + vlt: 2 + SKU286: + cost: 337 + init_stock: 780 + price: 337 + service_level: 0.96 + vlt: 1 + SKU287: + cost: 375 + init_stock: 1120 + price: 375 + service_level: 0.96 + vlt: 3 + SKU288: + cost: 181 + init_stock: 760 + price: 181 + service_level: 0.96 + vlt: 1 + SKU289: + cost: 67 + init_stock: 620 + price: 67 + service_level: 0.96 + vlt: 1 + SKU29: + cost: 245 + init_stock: 1160 + price: 245 + service_level: 0.96 + vlt: 1 + SKU290: + cost: 438 + init_stock: 1340 + price: 438 + service_level: 0.96 + vlt: 1 + SKU291: + cost: 94 + init_stock: 1220 + price: 94 + service_level: 0.96 + vlt: 1 + SKU292: + cost: 186 + init_stock: 100 + price: 186 + service_level: 0.96 + vlt: 1 + SKU293: + cost: 162 + init_stock: 1100 + price: 162 + service_level: 0.96 + vlt: 2 + SKU294: + cost: 409 + init_stock: 180 + price: 409 + service_level: 0.96 + vlt: 2 + SKU295: + cost: 435 + init_stock: 1860 + price: 435 + service_level: 0.96 + vlt: 3 + SKU296: + cost: 370 + init_stock: 1840 + price: 370 + service_level: 0.96 + vlt: 3 + SKU297: + cost: 298 + init_stock: 760 + price: 298 + service_level: 0.96 + vlt: 1 + SKU298: + cost: 286 + init_stock: 700 + price: 286 + service_level: 0.96 + vlt: 2 + SKU299: + cost: 367 + init_stock: 1020 + price: 367 + service_level: 0.96 + vlt: 3 + SKU3: + cost: 405 + init_stock: 240 + price: 405 + service_level: 0.96 + vlt: 3 + SKU30: + cost: 359 + init_stock: 1380 + price: 359 + service_level: 0.96 + vlt: 2 + SKU300: + cost: 376 + init_stock: 1160 + price: 376 + service_level: 0.96 + vlt: 1 + SKU301: + cost: 433 + init_stock: 1660 + price: 433 + service_level: 0.96 + vlt: 2 + SKU302: + cost: 184 + init_stock: 220 + price: 184 + service_level: 0.96 + vlt: 2 + SKU303: + cost: 246 + init_stock: 1880 + price: 246 + service_level: 0.96 + vlt: 1 + SKU304: + cost: 419 + init_stock: 460 + price: 419 + service_level: 0.96 + vlt: 3 + SKU305: + cost: 495 + init_stock: 2000 + price: 495 + service_level: 0.96 + vlt: 1 + SKU306: + cost: 479 + init_stock: 840 + price: 479 + service_level: 0.96 + vlt: 2 + SKU307: + cost: 210 + init_stock: 1560 + price: 210 + service_level: 0.96 + vlt: 1 + SKU308: + cost: 208 + init_stock: 100 + price: 208 + service_level: 0.96 + vlt: 2 + SKU309: + cost: 101 + init_stock: 1840 + price: 101 + service_level: 0.96 + vlt: 2 + SKU31: + cost: 69 + init_stock: 1120 + price: 69 + service_level: 0.96 + vlt: 3 + SKU310: + cost: 234 + init_stock: 880 + price: 234 + service_level: 0.96 + vlt: 3 + SKU311: + cost: 306 + init_stock: 1600 + price: 306 + service_level: 0.96 + vlt: 3 + SKU312: + cost: 291 + init_stock: 500 + price: 291 + service_level: 0.96 + vlt: 1 + SKU313: + cost: 324 + init_stock: 760 + price: 324 + service_level: 0.96 + vlt: 1 + SKU314: + cost: 404 + init_stock: 580 + price: 404 + service_level: 0.96 + vlt: 1 + SKU315: + cost: 471 + init_stock: 1680 + price: 471 + service_level: 0.96 + vlt: 2 + SKU316: + cost: 202 + init_stock: 360 + price: 202 + service_level: 0.96 + vlt: 1 + SKU317: + cost: 216 + init_stock: 480 + price: 216 + service_level: 0.96 + vlt: 2 + SKU318: + cost: 278 + init_stock: 1700 + price: 278 + service_level: 0.96 + vlt: 1 + SKU319: + cost: 257 + init_stock: 1060 + price: 257 + service_level: 0.96 + vlt: 3 + SKU32: + cost: 336 + init_stock: 440 + price: 336 + service_level: 0.96 + vlt: 1 + SKU320: + cost: 196 + init_stock: 780 + price: 196 + service_level: 0.96 + vlt: 3 + SKU321: + cost: 67 + init_stock: 320 + price: 67 + service_level: 0.96 + vlt: 3 + SKU322: + cost: 240 + init_stock: 2000 + price: 240 + service_level: 0.96 + vlt: 1 + SKU323: + cost: 66 + init_stock: 780 + price: 66 + service_level: 0.96 + vlt: 2 + SKU324: + cost: 442 + init_stock: 1860 + price: 442 + service_level: 0.96 + vlt: 3 + SKU325: + cost: 453 + init_stock: 1380 + price: 453 + service_level: 0.96 + vlt: 2 + SKU326: + cost: 345 + init_stock: 480 + price: 345 + service_level: 0.96 + vlt: 2 + SKU327: + cost: 457 + init_stock: 280 + price: 457 + service_level: 0.96 + vlt: 2 + SKU328: + cost: 390 + init_stock: 900 + price: 390 + service_level: 0.96 + vlt: 1 + SKU329: + cost: 67 + init_stock: 840 + price: 67 + service_level: 0.96 + vlt: 1 + SKU33: + cost: 416 + init_stock: 1840 + price: 416 + service_level: 0.96 + vlt: 2 + SKU330: + cost: 306 + init_stock: 780 + price: 306 + service_level: 0.96 + vlt: 2 + SKU331: + cost: 138 + init_stock: 820 + price: 138 + service_level: 0.96 + vlt: 3 + SKU332: + cost: 390 + init_stock: 1920 + price: 390 + service_level: 0.96 + vlt: 2 + SKU333: + cost: 485 + init_stock: 1060 + price: 485 + service_level: 0.96 + vlt: 2 + SKU334: + cost: 183 + init_stock: 1140 + price: 183 + service_level: 0.96 + vlt: 1 + SKU335: + cost: 80 + init_stock: 1620 + price: 80 + service_level: 0.96 + vlt: 1 + SKU336: + cost: 296 + init_stock: 560 + price: 296 + service_level: 0.96 + vlt: 1 + SKU337: + cost: 245 + init_stock: 580 + price: 245 + service_level: 0.96 + vlt: 2 + SKU338: + cost: 87 + init_stock: 1620 + price: 87 + service_level: 0.96 + vlt: 3 + SKU339: + cost: 265 + init_stock: 1260 + price: 265 + service_level: 0.96 + vlt: 2 + SKU34: + cost: 95 + init_stock: 260 + price: 95 + service_level: 0.96 + vlt: 3 + SKU340: + cost: 480 + init_stock: 1740 + price: 480 + service_level: 0.96 + vlt: 2 + SKU341: + cost: 462 + init_stock: 1400 + price: 462 + service_level: 0.96 + vlt: 1 + SKU342: + cost: 144 + init_stock: 180 + price: 144 + service_level: 0.96 + vlt: 3 + SKU343: + cost: 310 + init_stock: 300 + price: 310 + service_level: 0.96 + vlt: 2 + SKU344: + cost: 357 + init_stock: 1740 + price: 357 + service_level: 0.96 + vlt: 2 + SKU345: + cost: 442 + init_stock: 1780 + price: 442 + service_level: 0.96 + vlt: 2 + SKU346: + cost: 350 + init_stock: 840 + price: 350 + service_level: 0.96 + vlt: 3 + SKU347: + cost: 497 + init_stock: 1640 + price: 497 + service_level: 0.96 + vlt: 1 + SKU348: + cost: 147 + init_stock: 400 + price: 147 + service_level: 0.96 + vlt: 1 + SKU349: + cost: 67 + init_stock: 1340 + price: 67 + service_level: 0.96 + vlt: 3 + SKU35: + cost: 267 + init_stock: 1720 + price: 267 + service_level: 0.96 + vlt: 3 + SKU350: + cost: 279 + init_stock: 1840 + price: 279 + service_level: 0.96 + vlt: 3 + SKU351: + cost: 155 + init_stock: 1340 + price: 155 + service_level: 0.96 + vlt: 1 + SKU352: + cost: 71 + init_stock: 360 + price: 71 + service_level: 0.96 + vlt: 2 + SKU353: + cost: 253 + init_stock: 1860 + price: 253 + service_level: 0.96 + vlt: 2 + SKU354: + cost: 396 + init_stock: 1580 + price: 396 + service_level: 0.96 + vlt: 3 + SKU355: + cost: 63 + init_stock: 200 + price: 63 + service_level: 0.96 + vlt: 1 + SKU356: + cost: 348 + init_stock: 580 + price: 348 + service_level: 0.96 + vlt: 1 + SKU357: + cost: 499 + init_stock: 1840 + price: 499 + service_level: 0.96 + vlt: 3 + SKU358: + cost: 269 + init_stock: 1380 + price: 269 + service_level: 0.96 + vlt: 2 + SKU359: + cost: 82 + init_stock: 1500 + price: 82 + service_level: 0.96 + vlt: 3 + SKU36: + cost: 105 + init_stock: 200 + price: 105 + service_level: 0.96 + vlt: 2 + SKU360: + cost: 484 + init_stock: 500 + price: 484 + service_level: 0.96 + vlt: 1 + SKU361: + cost: 163 + init_stock: 1800 + price: 163 + service_level: 0.96 + vlt: 1 + SKU362: + cost: 464 + init_stock: 1740 + price: 464 + service_level: 0.96 + vlt: 2 + SKU363: + cost: 52 + init_stock: 1560 + price: 52 + service_level: 0.96 + vlt: 2 + SKU364: + cost: 387 + init_stock: 660 + price: 387 + service_level: 0.96 + vlt: 1 + SKU365: + cost: 158 + init_stock: 1660 + price: 158 + service_level: 0.96 + vlt: 3 + SKU366: + cost: 444 + init_stock: 1720 + price: 444 + service_level: 0.96 + vlt: 3 + SKU367: + cost: 272 + init_stock: 920 + price: 272 + service_level: 0.96 + vlt: 3 + SKU368: + cost: 472 + init_stock: 660 + price: 472 + service_level: 0.96 + vlt: 1 + SKU369: + cost: 96 + init_stock: 1500 + price: 96 + service_level: 0.96 + vlt: 2 + SKU37: + cost: 66 + init_stock: 1180 + price: 66 + service_level: 0.96 + vlt: 2 + SKU370: + cost: 112 + init_stock: 300 + price: 112 + service_level: 0.96 + vlt: 2 + SKU371: + cost: 328 + init_stock: 100 + price: 328 + service_level: 0.96 + vlt: 3 + SKU372: + cost: 67 + init_stock: 640 + price: 67 + service_level: 0.96 + vlt: 1 + SKU373: + cost: 58 + init_stock: 1340 + price: 58 + service_level: 0.96 + vlt: 3 + SKU374: + cost: 38 + init_stock: 1080 + price: 38 + service_level: 0.96 + vlt: 2 + SKU375: + cost: 283 + init_stock: 1440 + price: 283 + service_level: 0.96 + vlt: 1 + SKU376: + cost: 156 + init_stock: 1640 + price: 156 + service_level: 0.96 + vlt: 1 + SKU377: + cost: 78 + init_stock: 1340 + price: 78 + service_level: 0.96 + vlt: 3 + SKU378: + cost: 424 + init_stock: 700 + price: 424 + service_level: 0.96 + vlt: 1 + SKU379: + cost: 11 + init_stock: 980 + price: 11 + service_level: 0.96 + vlt: 1 + SKU38: + cost: 344 + init_stock: 860 + price: 344 + service_level: 0.96 + vlt: 2 + SKU380: + cost: 393 + init_stock: 1060 + price: 393 + service_level: 0.96 + vlt: 1 + SKU381: + cost: 476 + init_stock: 120 + price: 476 + service_level: 0.96 + vlt: 1 + SKU382: + cost: 125 + init_stock: 1420 + price: 125 + service_level: 0.96 + vlt: 2 + SKU383: + cost: 304 + init_stock: 1840 + price: 304 + service_level: 0.96 + vlt: 3 + SKU384: + cost: 320 + init_stock: 180 + price: 320 + service_level: 0.96 + vlt: 2 + SKU385: + cost: 120 + init_stock: 260 + price: 120 + service_level: 0.96 + vlt: 2 + SKU386: + cost: 295 + init_stock: 620 + price: 295 + service_level: 0.96 + vlt: 1 + SKU387: + cost: 112 + init_stock: 1940 + price: 112 + service_level: 0.96 + vlt: 3 + SKU388: + cost: 405 + init_stock: 880 + price: 405 + service_level: 0.96 + vlt: 2 + SKU389: + cost: 376 + init_stock: 1400 + price: 376 + service_level: 0.96 + vlt: 2 + SKU39: + cost: 25 + init_stock: 1020 + price: 25 + service_level: 0.96 + vlt: 3 + SKU390: + cost: 144 + init_stock: 780 + price: 144 + service_level: 0.96 + vlt: 2 + SKU391: + cost: 233 + init_stock: 1340 + price: 233 + service_level: 0.96 + vlt: 2 + SKU392: + cost: 163 + init_stock: 1480 + price: 163 + service_level: 0.96 + vlt: 2 + SKU393: + cost: 487 + init_stock: 1340 + price: 487 + service_level: 0.96 + vlt: 1 + SKU394: + cost: 154 + init_stock: 1060 + price: 154 + service_level: 0.96 + vlt: 2 + SKU395: + cost: 488 + init_stock: 660 + price: 488 + service_level: 0.96 + vlt: 3 + SKU396: + cost: 333 + init_stock: 1220 + price: 333 + service_level: 0.96 + vlt: 3 + SKU397: + cost: 460 + init_stock: 1020 + price: 460 + service_level: 0.96 + vlt: 1 + SKU398: + cost: 234 + init_stock: 1160 + price: 234 + service_level: 0.96 + vlt: 2 + SKU399: + cost: 376 + init_stock: 740 + price: 376 + service_level: 0.96 + vlt: 2 + SKU4: + cost: 439 + init_stock: 140 + price: 439 + service_level: 0.96 + vlt: 2 + SKU40: + cost: 490 + init_stock: 1980 + price: 490 + service_level: 0.96 + vlt: 3 + SKU400: + cost: 445 + init_stock: 1680 + price: 445 + service_level: 0.96 + vlt: 2 + SKU401: + cost: 410 + init_stock: 1560 + price: 410 + service_level: 0.96 + vlt: 1 + SKU402: + cost: 86 + init_stock: 140 + price: 86 + service_level: 0.96 + vlt: 3 + SKU403: + cost: 89 + init_stock: 1980 + price: 89 + service_level: 0.96 + vlt: 3 + SKU404: + cost: 287 + init_stock: 1220 + price: 287 + service_level: 0.96 + vlt: 1 + SKU405: + cost: 460 + init_stock: 380 + price: 460 + service_level: 0.96 + vlt: 1 + SKU406: + cost: 327 + init_stock: 2000 + price: 327 + service_level: 0.96 + vlt: 1 + SKU407: + cost: 26 + init_stock: 920 + price: 26 + service_level: 0.96 + vlt: 2 + SKU408: + cost: 444 + init_stock: 160 + price: 444 + service_level: 0.96 + vlt: 2 + SKU409: + cost: 257 + init_stock: 1820 + price: 257 + service_level: 0.96 + vlt: 2 + SKU41: + cost: 141 + init_stock: 1580 + price: 141 + service_level: 0.96 + vlt: 2 + SKU410: + cost: 70 + init_stock: 320 + price: 70 + service_level: 0.96 + vlt: 1 + SKU411: + cost: 210 + init_stock: 1900 + price: 210 + service_level: 0.96 + vlt: 3 + SKU412: + cost: 286 + init_stock: 1240 + price: 286 + service_level: 0.96 + vlt: 2 + SKU413: + cost: 403 + init_stock: 1660 + price: 403 + service_level: 0.96 + vlt: 3 + SKU414: + cost: 165 + init_stock: 1740 + price: 165 + service_level: 0.96 + vlt: 1 + SKU415: + cost: 291 + init_stock: 460 + price: 291 + service_level: 0.96 + vlt: 3 + SKU416: + cost: 228 + init_stock: 180 + price: 228 + service_level: 0.96 + vlt: 3 + SKU417: + cost: 443 + init_stock: 1440 + price: 443 + service_level: 0.96 + vlt: 1 + SKU418: + cost: 458 + init_stock: 260 + price: 458 + service_level: 0.96 + vlt: 3 + SKU419: + cost: 303 + init_stock: 1780 + price: 303 + service_level: 0.96 + vlt: 3 + SKU42: + cost: 462 + init_stock: 520 + price: 462 + service_level: 0.96 + vlt: 3 + SKU420: + cost: 268 + init_stock: 840 + price: 268 + service_level: 0.96 + vlt: 1 + SKU421: + cost: 214 + init_stock: 920 + price: 214 + service_level: 0.96 + vlt: 2 + SKU422: + cost: 52 + init_stock: 1080 + price: 52 + service_level: 0.96 + vlt: 3 + SKU423: + cost: 188 + init_stock: 1320 + price: 188 + service_level: 0.96 + vlt: 1 + SKU424: + cost: 189 + init_stock: 220 + price: 189 + service_level: 0.96 + vlt: 2 + SKU425: + cost: 162 + init_stock: 240 + price: 162 + service_level: 0.96 + vlt: 3 + SKU426: + cost: 125 + init_stock: 1960 + price: 125 + service_level: 0.96 + vlt: 3 + SKU427: + cost: 413 + init_stock: 1880 + price: 413 + service_level: 0.96 + vlt: 1 + SKU428: + cost: 391 + init_stock: 1260 + price: 391 + service_level: 0.96 + vlt: 3 + SKU429: + cost: 39 + init_stock: 820 + price: 39 + service_level: 0.96 + vlt: 1 + SKU43: + cost: 217 + init_stock: 1360 + price: 217 + service_level: 0.96 + vlt: 1 + SKU430: + cost: 216 + init_stock: 1460 + price: 216 + service_level: 0.96 + vlt: 2 + SKU431: + cost: 328 + init_stock: 420 + price: 328 + service_level: 0.96 + vlt: 1 + SKU432: + cost: 25 + init_stock: 920 + price: 25 + service_level: 0.96 + vlt: 3 + SKU433: + cost: 348 + init_stock: 900 + price: 348 + service_level: 0.96 + vlt: 2 + SKU434: + cost: 37 + init_stock: 600 + price: 37 + service_level: 0.96 + vlt: 1 + SKU435: + cost: 272 + init_stock: 600 + price: 272 + service_level: 0.96 + vlt: 3 + SKU436: + cost: 72 + init_stock: 1620 + price: 72 + service_level: 0.96 + vlt: 1 + SKU437: + cost: 115 + init_stock: 280 + price: 115 + service_level: 0.96 + vlt: 2 + SKU438: + cost: 143 + init_stock: 200 + price: 143 + service_level: 0.96 + vlt: 3 + SKU439: + cost: 256 + init_stock: 760 + price: 256 + service_level: 0.96 + vlt: 1 + SKU44: + cost: 360 + init_stock: 960 + price: 360 + service_level: 0.96 + vlt: 1 + SKU440: + cost: 248 + init_stock: 1640 + price: 248 + service_level: 0.96 + vlt: 2 + SKU441: + cost: 253 + init_stock: 1120 + price: 253 + service_level: 0.96 + vlt: 2 + SKU442: + cost: 470 + init_stock: 1760 + price: 470 + service_level: 0.96 + vlt: 2 + SKU443: + cost: 218 + init_stock: 1460 + price: 218 + service_level: 0.96 + vlt: 1 + SKU444: + cost: 156 + init_stock: 120 + price: 156 + service_level: 0.96 + vlt: 2 + SKU445: + cost: 260 + init_stock: 1720 + price: 260 + service_level: 0.96 + vlt: 2 + SKU446: + cost: 497 + init_stock: 980 + price: 497 + service_level: 0.96 + vlt: 1 + SKU447: + cost: 196 + init_stock: 100 + price: 196 + service_level: 0.96 + vlt: 1 + SKU448: + cost: 485 + init_stock: 1420 + price: 485 + service_level: 0.96 + vlt: 3 + SKU449: + cost: 282 + init_stock: 760 + price: 282 + service_level: 0.96 + vlt: 2 + SKU45: + cost: 253 + init_stock: 200 + price: 253 + service_level: 0.96 + vlt: 1 + SKU450: + cost: 58 + init_stock: 1960 + price: 58 + service_level: 0.96 + vlt: 2 + SKU451: + cost: 468 + init_stock: 1380 + price: 468 + service_level: 0.96 + vlt: 3 + SKU452: + cost: 63 + init_stock: 1580 + price: 63 + service_level: 0.96 + vlt: 1 + SKU453: + cost: 37 + init_stock: 700 + price: 37 + service_level: 0.96 + vlt: 3 + SKU454: + cost: 494 + init_stock: 1700 + price: 494 + service_level: 0.96 + vlt: 1 + SKU455: + cost: 136 + init_stock: 520 + price: 136 + service_level: 0.96 + vlt: 2 + SKU456: + cost: 338 + init_stock: 500 + price: 338 + service_level: 0.96 + vlt: 2 + SKU457: + cost: 169 + init_stock: 1280 + price: 169 + service_level: 0.96 + vlt: 2 + SKU458: + cost: 403 + init_stock: 140 + price: 403 + service_level: 0.96 + vlt: 3 + SKU459: + cost: 425 + init_stock: 680 + price: 425 + service_level: 0.96 + vlt: 3 + SKU46: + cost: 299 + init_stock: 1000 + price: 299 + service_level: 0.96 + vlt: 3 + SKU460: + cost: 405 + init_stock: 1460 + price: 405 + service_level: 0.96 + vlt: 2 + SKU461: + cost: 251 + init_stock: 960 + price: 251 + service_level: 0.96 + vlt: 1 + SKU462: + cost: 448 + init_stock: 180 + price: 448 + service_level: 0.96 + vlt: 1 + SKU463: + cost: 187 + init_stock: 1640 + price: 187 + service_level: 0.96 + vlt: 3 + SKU464: + cost: 279 + init_stock: 680 + price: 279 + service_level: 0.96 + vlt: 3 + SKU465: + cost: 147 + init_stock: 2000 + price: 147 + service_level: 0.96 + vlt: 2 + SKU466: + cost: 97 + init_stock: 840 + price: 97 + service_level: 0.96 + vlt: 1 + SKU467: + cost: 142 + init_stock: 720 + price: 142 + service_level: 0.96 + vlt: 2 + SKU468: + cost: 55 + init_stock: 1000 + price: 55 + service_level: 0.96 + vlt: 2 + SKU469: + cost: 178 + init_stock: 300 + price: 178 + service_level: 0.96 + vlt: 3 + SKU47: + cost: 121 + init_stock: 780 + price: 121 + service_level: 0.96 + vlt: 1 + SKU470: + cost: 373 + init_stock: 640 + price: 373 + service_level: 0.96 + vlt: 2 + SKU471: + cost: 315 + init_stock: 1420 + price: 315 + service_level: 0.96 + vlt: 3 + SKU472: + cost: 421 + init_stock: 1180 + price: 421 + service_level: 0.96 + vlt: 2 + SKU473: + cost: 195 + init_stock: 420 + price: 195 + service_level: 0.96 + vlt: 1 + SKU474: + cost: 448 + init_stock: 1720 + price: 448 + service_level: 0.96 + vlt: 2 + SKU475: + cost: 34 + init_stock: 1880 + price: 34 + service_level: 0.96 + vlt: 1 + SKU476: + cost: 251 + init_stock: 1800 + price: 251 + service_level: 0.96 + vlt: 1 + SKU477: + cost: 289 + init_stock: 940 + price: 289 + service_level: 0.96 + vlt: 2 + SKU478: + cost: 479 + init_stock: 1760 + price: 479 + service_level: 0.96 + vlt: 2 + SKU479: + cost: 366 + init_stock: 760 + price: 366 + service_level: 0.96 + vlt: 1 + SKU48: + cost: 340 + init_stock: 1160 + price: 340 + service_level: 0.96 + vlt: 2 + SKU480: + cost: 18 + init_stock: 600 + price: 18 + service_level: 0.96 + vlt: 2 + SKU481: + cost: 330 + init_stock: 1760 + price: 330 + service_level: 0.96 + vlt: 1 + SKU482: + cost: 94 + init_stock: 1740 + price: 94 + service_level: 0.96 + vlt: 3 + SKU483: + cost: 298 + init_stock: 680 + price: 298 + service_level: 0.96 + vlt: 2 + SKU484: + cost: 84 + init_stock: 1460 + price: 84 + service_level: 0.96 + vlt: 3 + SKU485: + cost: 318 + init_stock: 1340 + price: 318 + service_level: 0.96 + vlt: 3 + SKU486: + cost: 122 + init_stock: 1040 + price: 122 + service_level: 0.96 + vlt: 1 + SKU487: + cost: 277 + init_stock: 1320 + price: 277 + service_level: 0.96 + vlt: 3 + SKU488: + cost: 36 + init_stock: 540 + price: 36 + service_level: 0.96 + vlt: 3 + SKU489: + cost: 59 + init_stock: 620 + price: 59 + service_level: 0.96 + vlt: 3 + SKU49: + cost: 263 + init_stock: 1900 + price: 263 + service_level: 0.96 + vlt: 3 + SKU490: + cost: 161 + init_stock: 1620 + price: 161 + service_level: 0.96 + vlt: 2 + SKU491: + cost: 444 + init_stock: 1500 + price: 444 + service_level: 0.96 + vlt: 2 + SKU492: + cost: 53 + init_stock: 1600 + price: 53 + service_level: 0.96 + vlt: 2 + SKU493: + cost: 461 + init_stock: 540 + price: 461 + service_level: 0.96 + vlt: 1 + SKU494: + cost: 145 + init_stock: 1880 + price: 145 + service_level: 0.96 + vlt: 1 + SKU495: + cost: 149 + init_stock: 1240 + price: 149 + service_level: 0.96 + vlt: 3 + SKU496: + cost: 342 + init_stock: 1180 + price: 342 + service_level: 0.96 + vlt: 1 + SKU497: + cost: 33 + init_stock: 180 + price: 33 + service_level: 0.96 + vlt: 3 + SKU498: + cost: 305 + init_stock: 1300 + price: 305 + service_level: 0.96 + vlt: 3 + SKU499: + cost: 307 + init_stock: 580 + price: 307 + service_level: 0.96 + vlt: 2 + SKU5: + cost: 466 + init_stock: 1420 + price: 466 + service_level: 0.96 + vlt: 2 + SKU50: + cost: 282 + init_stock: 740 + price: 282 + service_level: 0.96 + vlt: 3 + SKU500: + cost: 208 + init_stock: 840 + price: 208 + service_level: 0.96 + vlt: 3 + SKU501: + cost: 194 + init_stock: 1520 + price: 194 + service_level: 0.96 + vlt: 1 + SKU502: + cost: 69 + init_stock: 1300 + price: 69 + service_level: 0.96 + vlt: 2 + SKU503: + cost: 208 + init_stock: 1140 + price: 208 + service_level: 0.96 + vlt: 1 + SKU504: + cost: 251 + init_stock: 600 + price: 251 + service_level: 0.96 + vlt: 2 + SKU505: + cost: 426 + init_stock: 1180 + price: 426 + service_level: 0.96 + vlt: 1 + SKU506: + cost: 149 + init_stock: 1860 + price: 149 + service_level: 0.96 + vlt: 3 + SKU507: + cost: 187 + init_stock: 300 + price: 187 + service_level: 0.96 + vlt: 1 + SKU508: + cost: 272 + init_stock: 1920 + price: 272 + service_level: 0.96 + vlt: 1 + SKU509: + cost: 297 + init_stock: 1620 + price: 297 + service_level: 0.96 + vlt: 1 + SKU51: + cost: 295 + init_stock: 1340 + price: 295 + service_level: 0.96 + vlt: 3 + SKU510: + cost: 94 + init_stock: 180 + price: 94 + service_level: 0.96 + vlt: 1 + SKU511: + cost: 361 + init_stock: 1500 + price: 361 + service_level: 0.96 + vlt: 1 + SKU512: + cost: 467 + init_stock: 440 + price: 467 + service_level: 0.96 + vlt: 2 + SKU513: + cost: 343 + init_stock: 140 + price: 343 + service_level: 0.96 + vlt: 3 + SKU514: + cost: 186 + init_stock: 1260 + price: 186 + service_level: 0.96 + vlt: 3 + SKU515: + cost: 145 + init_stock: 1080 + price: 145 + service_level: 0.96 + vlt: 3 + SKU516: + cost: 64 + init_stock: 760 + price: 64 + service_level: 0.96 + vlt: 1 + SKU517: + cost: 117 + init_stock: 180 + price: 117 + service_level: 0.96 + vlt: 3 + SKU518: + cost: 26 + init_stock: 420 + price: 26 + service_level: 0.96 + vlt: 3 + SKU519: + cost: 233 + init_stock: 1000 + price: 233 + service_level: 0.96 + vlt: 2 + SKU52: + cost: 22 + init_stock: 1340 + price: 22 + service_level: 0.96 + vlt: 1 + SKU520: + cost: 209 + init_stock: 440 + price: 209 + service_level: 0.96 + vlt: 1 + SKU521: + cost: 220 + init_stock: 1000 + price: 220 + service_level: 0.96 + vlt: 3 + SKU522: + cost: 156 + init_stock: 1740 + price: 156 + service_level: 0.96 + vlt: 1 + SKU523: + cost: 65 + init_stock: 2000 + price: 65 + service_level: 0.96 + vlt: 3 + SKU524: + cost: 294 + init_stock: 1880 + price: 294 + service_level: 0.96 + vlt: 1 + SKU525: + cost: 432 + init_stock: 840 + price: 432 + service_level: 0.96 + vlt: 2 + SKU526: + cost: 419 + init_stock: 480 + price: 419 + service_level: 0.96 + vlt: 3 + SKU527: + cost: 131 + init_stock: 1400 + price: 131 + service_level: 0.96 + vlt: 1 + SKU528: + cost: 316 + init_stock: 420 + price: 316 + service_level: 0.96 + vlt: 3 + SKU529: + cost: 298 + init_stock: 620 + price: 298 + service_level: 0.96 + vlt: 3 + SKU53: + cost: 151 + init_stock: 1400 + price: 151 + service_level: 0.96 + vlt: 2 + SKU530: + cost: 131 + init_stock: 900 + price: 131 + service_level: 0.96 + vlt: 1 + SKU531: + cost: 103 + init_stock: 1200 + price: 103 + service_level: 0.96 + vlt: 3 + SKU532: + cost: 351 + init_stock: 1540 + price: 351 + service_level: 0.96 + vlt: 3 + SKU533: + cost: 296 + init_stock: 840 + price: 296 + service_level: 0.96 + vlt: 3 + SKU534: + cost: 425 + init_stock: 820 + price: 425 + service_level: 0.96 + vlt: 1 + SKU535: + cost: 304 + init_stock: 1720 + price: 304 + service_level: 0.96 + vlt: 2 + SKU536: + cost: 358 + init_stock: 1040 + price: 358 + service_level: 0.96 + vlt: 2 + SKU537: + cost: 321 + init_stock: 180 + price: 321 + service_level: 0.96 + vlt: 3 + SKU538: + cost: 457 + init_stock: 1000 + price: 457 + service_level: 0.96 + vlt: 1 + SKU539: + cost: 165 + init_stock: 1560 + price: 165 + service_level: 0.96 + vlt: 2 + SKU54: + cost: 158 + init_stock: 920 + price: 158 + service_level: 0.96 + vlt: 1 + SKU540: + cost: 388 + init_stock: 840 + price: 388 + service_level: 0.96 + vlt: 3 + SKU541: + cost: 131 + init_stock: 580 + price: 131 + service_level: 0.96 + vlt: 2 + SKU542: + cost: 38 + init_stock: 520 + price: 38 + service_level: 0.96 + vlt: 3 + SKU543: + cost: 430 + init_stock: 1700 + price: 430 + service_level: 0.96 + vlt: 2 + SKU544: + cost: 346 + init_stock: 1940 + price: 346 + service_level: 0.96 + vlt: 1 + SKU545: + cost: 175 + init_stock: 300 + price: 175 + service_level: 0.96 + vlt: 3 + SKU546: + cost: 491 + init_stock: 1920 + price: 491 + service_level: 0.96 + vlt: 3 + SKU547: + cost: 161 + init_stock: 880 + price: 161 + service_level: 0.96 + vlt: 2 + SKU548: + cost: 111 + init_stock: 120 + price: 111 + service_level: 0.96 + vlt: 1 + SKU549: + cost: 387 + init_stock: 1580 + price: 387 + service_level: 0.96 + vlt: 1 + SKU55: + cost: 482 + init_stock: 1300 + price: 482 + service_level: 0.96 + vlt: 3 + SKU550: + cost: 259 + init_stock: 1880 + price: 259 + service_level: 0.96 + vlt: 1 + SKU551: + cost: 46 + init_stock: 620 + price: 46 + service_level: 0.96 + vlt: 2 + SKU552: + cost: 191 + init_stock: 1800 + price: 191 + service_level: 0.96 + vlt: 3 + SKU553: + cost: 208 + init_stock: 600 + price: 208 + service_level: 0.96 + vlt: 1 + SKU554: + cost: 340 + init_stock: 820 + price: 340 + service_level: 0.96 + vlt: 1 + SKU555: + cost: 489 + init_stock: 1680 + price: 489 + service_level: 0.96 + vlt: 1 + SKU556: + cost: 110 + init_stock: 600 + price: 110 + service_level: 0.96 + vlt: 2 + SKU557: + cost: 334 + init_stock: 1460 + price: 334 + service_level: 0.96 + vlt: 2 + SKU558: + cost: 399 + init_stock: 340 + price: 399 + service_level: 0.96 + vlt: 1 + SKU559: + cost: 313 + init_stock: 1080 + price: 313 + service_level: 0.96 + vlt: 1 + SKU56: + cost: 377 + init_stock: 1480 + price: 377 + service_level: 0.96 + vlt: 1 + SKU560: + cost: 283 + init_stock: 820 + price: 283 + service_level: 0.96 + vlt: 1 + SKU561: + cost: 202 + init_stock: 780 + price: 202 + service_level: 0.96 + vlt: 3 + SKU562: + cost: 347 + init_stock: 1780 + price: 347 + service_level: 0.96 + vlt: 2 + SKU563: + cost: 401 + init_stock: 100 + price: 401 + service_level: 0.96 + vlt: 3 + SKU564: + cost: 109 + init_stock: 180 + price: 109 + service_level: 0.96 + vlt: 2 + SKU565: + cost: 57 + init_stock: 1500 + price: 57 + service_level: 0.96 + vlt: 3 + SKU566: + cost: 131 + init_stock: 220 + price: 131 + service_level: 0.96 + vlt: 2 + SKU567: + cost: 361 + init_stock: 180 + price: 361 + service_level: 0.96 + vlt: 1 + SKU568: + cost: 75 + init_stock: 1560 + price: 75 + service_level: 0.96 + vlt: 2 + SKU569: + cost: 473 + init_stock: 960 + price: 473 + service_level: 0.96 + vlt: 2 + SKU57: + cost: 359 + init_stock: 1000 + price: 359 + service_level: 0.96 + vlt: 2 + SKU570: + cost: 388 + init_stock: 1660 + price: 388 + service_level: 0.96 + vlt: 2 + SKU571: + cost: 168 + init_stock: 780 + price: 168 + service_level: 0.96 + vlt: 1 + SKU572: + cost: 26 + init_stock: 900 + price: 26 + service_level: 0.96 + vlt: 3 + SKU573: + cost: 246 + init_stock: 820 + price: 246 + service_level: 0.96 + vlt: 2 + SKU574: + cost: 94 + init_stock: 1000 + price: 94 + service_level: 0.96 + vlt: 2 + SKU575: + cost: 237 + init_stock: 1580 + price: 237 + service_level: 0.96 + vlt: 1 + SKU576: + cost: 265 + init_stock: 1560 + price: 265 + service_level: 0.96 + vlt: 3 + SKU577: + cost: 18 + init_stock: 1740 + price: 18 + service_level: 0.96 + vlt: 3 + SKU578: + cost: 100 + init_stock: 1420 + price: 100 + service_level: 0.96 + vlt: 2 + SKU579: + cost: 415 + init_stock: 180 + price: 415 + service_level: 0.96 + vlt: 1 + SKU58: + cost: 362 + init_stock: 960 + price: 362 + service_level: 0.96 + vlt: 2 + SKU580: + cost: 203 + init_stock: 100 + price: 203 + service_level: 0.96 + vlt: 1 + SKU581: + cost: 152 + init_stock: 1720 + price: 152 + service_level: 0.96 + vlt: 2 + SKU582: + cost: 359 + init_stock: 1920 + price: 359 + service_level: 0.96 + vlt: 2 + SKU583: + cost: 86 + init_stock: 1740 + price: 86 + service_level: 0.96 + vlt: 1 + SKU584: + cost: 33 + init_stock: 900 + price: 33 + service_level: 0.96 + vlt: 2 + SKU585: + cost: 31 + init_stock: 400 + price: 31 + service_level: 0.96 + vlt: 2 + SKU586: + cost: 61 + init_stock: 880 + price: 61 + service_level: 0.96 + vlt: 2 + SKU587: + cost: 290 + init_stock: 1560 + price: 290 + service_level: 0.96 + vlt: 2 + SKU588: + cost: 476 + init_stock: 880 + price: 476 + service_level: 0.96 + vlt: 2 + SKU589: + cost: 244 + init_stock: 680 + price: 244 + service_level: 0.96 + vlt: 3 + SKU59: + cost: 145 + init_stock: 1020 + price: 145 + service_level: 0.96 + vlt: 1 + SKU590: + cost: 70 + init_stock: 620 + price: 70 + service_level: 0.96 + vlt: 2 + SKU591: + cost: 206 + init_stock: 1680 + price: 206 + service_level: 0.96 + vlt: 2 + SKU592: + cost: 218 + init_stock: 920 + price: 218 + service_level: 0.96 + vlt: 1 + SKU593: + cost: 124 + init_stock: 880 + price: 124 + service_level: 0.96 + vlt: 1 + SKU594: + cost: 238 + init_stock: 1000 + price: 238 + service_level: 0.96 + vlt: 1 + SKU595: + cost: 73 + init_stock: 860 + price: 73 + service_level: 0.96 + vlt: 3 + SKU596: + cost: 432 + init_stock: 140 + price: 432 + service_level: 0.96 + vlt: 2 + SKU597: + cost: 252 + init_stock: 1740 + price: 252 + service_level: 0.96 + vlt: 2 + SKU598: + cost: 348 + init_stock: 280 + price: 348 + service_level: 0.96 + vlt: 1 + SKU599: + cost: 54 + init_stock: 1360 + price: 54 + service_level: 0.96 + vlt: 2 + SKU6: + cost: 122 + init_stock: 1760 + price: 122 + service_level: 0.96 + vlt: 2 + SKU60: + cost: 200 + init_stock: 780 + price: 200 + service_level: 0.96 + vlt: 1 + SKU600: + cost: 386 + init_stock: 1300 + price: 386 + service_level: 0.96 + vlt: 1 + SKU601: + cost: 138 + init_stock: 780 + price: 138 + service_level: 0.96 + vlt: 3 + SKU602: + cost: 184 + init_stock: 1960 + price: 184 + service_level: 0.96 + vlt: 1 + SKU603: + cost: 278 + init_stock: 840 + price: 278 + service_level: 0.96 + vlt: 3 + SKU604: + cost: 270 + init_stock: 1000 + price: 270 + service_level: 0.96 + vlt: 3 + SKU605: + cost: 288 + init_stock: 480 + price: 288 + service_level: 0.96 + vlt: 2 + SKU606: + cost: 114 + init_stock: 220 + price: 114 + service_level: 0.96 + vlt: 3 + SKU607: + cost: 208 + init_stock: 1580 + price: 208 + service_level: 0.96 + vlt: 2 + SKU608: + cost: 39 + init_stock: 1460 + price: 39 + service_level: 0.96 + vlt: 1 + SKU609: + cost: 102 + init_stock: 380 + price: 102 + service_level: 0.96 + vlt: 2 + SKU61: + cost: 461 + init_stock: 1500 + price: 461 + service_level: 0.96 + vlt: 1 + SKU610: + cost: 449 + init_stock: 1360 + price: 449 + service_level: 0.96 + vlt: 1 + SKU611: + cost: 306 + init_stock: 1540 + price: 306 + service_level: 0.96 + vlt: 3 + SKU612: + cost: 391 + init_stock: 1760 + price: 391 + service_level: 0.96 + vlt: 2 + SKU613: + cost: 174 + init_stock: 140 + price: 174 + service_level: 0.96 + vlt: 3 + SKU614: + cost: 173 + init_stock: 300 + price: 173 + service_level: 0.96 + vlt: 1 + SKU615: + cost: 330 + init_stock: 1820 + price: 330 + service_level: 0.96 + vlt: 2 + SKU616: + cost: 232 + init_stock: 1460 + price: 232 + service_level: 0.96 + vlt: 1 + SKU617: + cost: 203 + init_stock: 1200 + price: 203 + service_level: 0.96 + vlt: 3 + SKU618: + cost: 77 + init_stock: 460 + price: 77 + service_level: 0.96 + vlt: 1 + SKU619: + cost: 189 + init_stock: 1720 + price: 189 + service_level: 0.96 + vlt: 2 + SKU62: + cost: 124 + init_stock: 180 + price: 124 + service_level: 0.96 + vlt: 2 + SKU620: + cost: 231 + init_stock: 780 + price: 231 + service_level: 0.96 + vlt: 3 + SKU621: + cost: 199 + init_stock: 1440 + price: 199 + service_level: 0.96 + vlt: 2 + SKU622: + cost: 253 + init_stock: 1300 + price: 253 + service_level: 0.96 + vlt: 3 + SKU623: + cost: 335 + init_stock: 1100 + price: 335 + service_level: 0.96 + vlt: 1 + SKU624: + cost: 482 + init_stock: 1080 + price: 482 + service_level: 0.96 + vlt: 2 + SKU625: + cost: 46 + init_stock: 1780 + price: 46 + service_level: 0.96 + vlt: 1 + SKU626: + cost: 451 + init_stock: 1780 + price: 451 + service_level: 0.96 + vlt: 3 + SKU627: + cost: 220 + init_stock: 1640 + price: 220 + service_level: 0.96 + vlt: 3 + SKU628: + cost: 124 + init_stock: 520 + price: 124 + service_level: 0.96 + vlt: 2 + SKU629: + cost: 353 + init_stock: 1840 + price: 353 + service_level: 0.96 + vlt: 1 + SKU63: + cost: 68 + init_stock: 280 + price: 68 + service_level: 0.96 + vlt: 3 + SKU630: + cost: 122 + init_stock: 340 + price: 122 + service_level: 0.96 + vlt: 1 + SKU631: + cost: 296 + init_stock: 980 + price: 296 + service_level: 0.96 + vlt: 2 + SKU632: + cost: 85 + init_stock: 1880 + price: 85 + service_level: 0.96 + vlt: 2 + SKU633: + cost: 184 + init_stock: 1340 + price: 184 + service_level: 0.96 + vlt: 2 + SKU634: + cost: 17 + init_stock: 1460 + price: 17 + service_level: 0.96 + vlt: 1 + SKU635: + cost: 341 + init_stock: 1860 + price: 341 + service_level: 0.96 + vlt: 1 + SKU636: + cost: 385 + init_stock: 740 + price: 385 + service_level: 0.96 + vlt: 1 + SKU637: + cost: 83 + init_stock: 1220 + price: 83 + service_level: 0.96 + vlt: 3 + SKU638: + cost: 375 + init_stock: 160 + price: 375 + service_level: 0.96 + vlt: 1 + SKU639: + cost: 327 + init_stock: 800 + price: 327 + service_level: 0.96 + vlt: 2 + SKU64: + cost: 36 + init_stock: 380 + price: 36 + service_level: 0.96 + vlt: 3 + SKU640: + cost: 275 + init_stock: 1820 + price: 275 + service_level: 0.96 + vlt: 1 + SKU641: + cost: 365 + init_stock: 1620 + price: 365 + service_level: 0.96 + vlt: 1 + SKU642: + cost: 414 + init_stock: 500 + price: 414 + service_level: 0.96 + vlt: 2 + SKU643: + cost: 358 + init_stock: 920 + price: 358 + service_level: 0.96 + vlt: 2 + SKU644: + cost: 46 + init_stock: 1640 + price: 46 + service_level: 0.96 + vlt: 2 + SKU645: + cost: 467 + init_stock: 1980 + price: 467 + service_level: 0.96 + vlt: 3 + SKU646: + cost: 189 + init_stock: 260 + price: 189 + service_level: 0.96 + vlt: 3 + SKU647: + cost: 303 + init_stock: 1540 + price: 303 + service_level: 0.96 + vlt: 3 + SKU648: + cost: 226 + init_stock: 1100 + price: 226 + service_level: 0.96 + vlt: 1 + SKU649: + cost: 198 + init_stock: 500 + price: 198 + service_level: 0.96 + vlt: 3 + SKU65: + cost: 15 + init_stock: 1880 + price: 15 + service_level: 0.96 + vlt: 2 + SKU650: + cost: 351 + init_stock: 1120 + price: 351 + service_level: 0.96 + vlt: 2 + SKU651: + cost: 380 + init_stock: 1920 + price: 380 + service_level: 0.96 + vlt: 3 + SKU652: + cost: 123 + init_stock: 800 + price: 123 + service_level: 0.96 + vlt: 2 + SKU653: + cost: 479 + init_stock: 1920 + price: 479 + service_level: 0.96 + vlt: 2 + SKU654: + cost: 66 + init_stock: 160 + price: 66 + service_level: 0.96 + vlt: 3 + SKU655: + cost: 333 + init_stock: 840 + price: 333 + service_level: 0.96 + vlt: 1 + SKU656: + cost: 53 + init_stock: 1440 + price: 53 + service_level: 0.96 + vlt: 2 + SKU657: + cost: 73 + init_stock: 1620 + price: 73 + service_level: 0.96 + vlt: 2 + SKU658: + cost: 70 + init_stock: 720 + price: 70 + service_level: 0.96 + vlt: 3 + SKU659: + cost: 21 + init_stock: 1120 + price: 21 + service_level: 0.96 + vlt: 1 + SKU66: + cost: 143 + init_stock: 1800 + price: 143 + service_level: 0.96 + vlt: 1 + SKU660: + cost: 487 + init_stock: 1660 + price: 487 + service_level: 0.96 + vlt: 1 + SKU661: + cost: 344 + init_stock: 1480 + price: 344 + service_level: 0.96 + vlt: 2 + SKU662: + cost: 372 + init_stock: 760 + price: 372 + service_level: 0.96 + vlt: 3 + SKU663: + cost: 418 + init_stock: 800 + price: 418 + service_level: 0.96 + vlt: 3 + SKU664: + cost: 351 + init_stock: 1680 + price: 351 + service_level: 0.96 + vlt: 1 + SKU665: + cost: 493 + init_stock: 860 + price: 493 + service_level: 0.96 + vlt: 3 + SKU666: + cost: 341 + init_stock: 1760 + price: 341 + service_level: 0.96 + vlt: 1 + SKU667: + cost: 325 + init_stock: 260 + price: 325 + service_level: 0.96 + vlt: 3 + SKU668: + cost: 286 + init_stock: 1200 + price: 286 + service_level: 0.96 + vlt: 3 + SKU669: + cost: 74 + init_stock: 320 + price: 74 + service_level: 0.96 + vlt: 1 + SKU67: + cost: 247 + init_stock: 1060 + price: 247 + service_level: 0.96 + vlt: 1 + SKU670: + cost: 289 + init_stock: 1720 + price: 289 + service_level: 0.96 + vlt: 1 + SKU671: + cost: 267 + init_stock: 1660 + price: 267 + service_level: 0.96 + vlt: 3 + SKU672: + cost: 369 + init_stock: 260 + price: 369 + service_level: 0.96 + vlt: 2 + SKU673: + cost: 322 + init_stock: 820 + price: 322 + service_level: 0.96 + vlt: 2 + SKU674: + cost: 223 + init_stock: 440 + price: 223 + service_level: 0.96 + vlt: 1 + SKU675: + cost: 438 + init_stock: 660 + price: 438 + service_level: 0.96 + vlt: 1 + SKU676: + cost: 244 + init_stock: 1220 + price: 244 + service_level: 0.96 + vlt: 1 + SKU677: + cost: 425 + init_stock: 880 + price: 425 + service_level: 0.96 + vlt: 2 + SKU678: + cost: 168 + init_stock: 720 + price: 168 + service_level: 0.96 + vlt: 1 + SKU679: + cost: 395 + init_stock: 880 + price: 395 + service_level: 0.96 + vlt: 2 + SKU68: + cost: 169 + init_stock: 280 + price: 169 + service_level: 0.96 + vlt: 2 + SKU680: + cost: 260 + init_stock: 600 + price: 260 + service_level: 0.96 + vlt: 1 + SKU681: + cost: 464 + init_stock: 1940 + price: 464 + service_level: 0.96 + vlt: 3 + SKU682: + cost: 370 + init_stock: 1060 + price: 370 + service_level: 0.96 + vlt: 2 + SKU683: + cost: 327 + init_stock: 1340 + price: 327 + service_level: 0.96 + vlt: 3 + SKU684: + cost: 355 + init_stock: 1580 + price: 355 + service_level: 0.96 + vlt: 1 + SKU685: + cost: 422 + init_stock: 900 + price: 422 + service_level: 0.96 + vlt: 2 + SKU686: + cost: 63 + init_stock: 1260 + price: 63 + service_level: 0.96 + vlt: 1 + SKU687: + cost: 34 + init_stock: 1520 + price: 34 + service_level: 0.96 + vlt: 2 + SKU688: + cost: 484 + init_stock: 1540 + price: 484 + service_level: 0.96 + vlt: 3 + SKU689: + cost: 499 + init_stock: 300 + price: 499 + service_level: 0.96 + vlt: 1 + SKU69: + cost: 176 + init_stock: 1340 + price: 176 + service_level: 0.96 + vlt: 1 + SKU690: + cost: 437 + init_stock: 1660 + price: 437 + service_level: 0.96 + vlt: 2 + SKU691: + cost: 17 + init_stock: 1580 + price: 17 + service_level: 0.96 + vlt: 3 + SKU692: + cost: 225 + init_stock: 1300 + price: 225 + service_level: 0.96 + vlt: 1 + SKU693: + cost: 19 + init_stock: 1220 + price: 19 + service_level: 0.96 + vlt: 2 + SKU694: + cost: 208 + init_stock: 1500 + price: 208 + service_level: 0.96 + vlt: 3 + SKU695: + cost: 190 + init_stock: 140 + price: 190 + service_level: 0.96 + vlt: 2 + SKU696: + cost: 348 + init_stock: 1160 + price: 348 + service_level: 0.96 + vlt: 1 + SKU697: + cost: 455 + init_stock: 1600 + price: 455 + service_level: 0.96 + vlt: 1 + SKU698: + cost: 198 + init_stock: 1220 + price: 198 + service_level: 0.96 + vlt: 3 + SKU699: + cost: 31 + init_stock: 400 + price: 31 + service_level: 0.96 + vlt: 3 + SKU7: + cost: 101 + init_stock: 1920 + price: 101 + service_level: 0.96 + vlt: 2 + SKU70: + cost: 186 + init_stock: 700 + price: 186 + service_level: 0.96 + vlt: 1 + SKU700: + cost: 74 + init_stock: 180 + price: 74 + service_level: 0.96 + vlt: 2 + SKU701: + cost: 399 + init_stock: 1540 + price: 399 + service_level: 0.96 + vlt: 1 + SKU702: + cost: 82 + init_stock: 680 + price: 82 + service_level: 0.96 + vlt: 1 + SKU703: + cost: 363 + init_stock: 760 + price: 363 + service_level: 0.96 + vlt: 1 + SKU704: + cost: 384 + init_stock: 1740 + price: 384 + service_level: 0.96 + vlt: 2 + SKU705: + cost: 128 + init_stock: 1260 + price: 128 + service_level: 0.96 + vlt: 2 + SKU706: + cost: 182 + init_stock: 1820 + price: 182 + service_level: 0.96 + vlt: 2 + SKU707: + cost: 18 + init_stock: 1380 + price: 18 + service_level: 0.96 + vlt: 1 + SKU708: + cost: 178 + init_stock: 560 + price: 178 + service_level: 0.96 + vlt: 3 + SKU709: + cost: 102 + init_stock: 1060 + price: 102 + service_level: 0.96 + vlt: 3 + SKU71: + cost: 315 + init_stock: 900 + price: 315 + service_level: 0.96 + vlt: 2 + SKU710: + cost: 252 + init_stock: 380 + price: 252 + service_level: 0.96 + vlt: 2 + SKU711: + cost: 281 + init_stock: 1380 + price: 281 + service_level: 0.96 + vlt: 1 + SKU712: + cost: 232 + init_stock: 220 + price: 232 + service_level: 0.96 + vlt: 2 + SKU713: + cost: 285 + init_stock: 860 + price: 285 + service_level: 0.96 + vlt: 2 + SKU714: + cost: 79 + init_stock: 820 + price: 79 + service_level: 0.96 + vlt: 3 + SKU715: + cost: 234 + init_stock: 340 + price: 234 + service_level: 0.96 + vlt: 1 + SKU716: + cost: 70 + init_stock: 1500 + price: 70 + service_level: 0.96 + vlt: 2 + SKU717: + cost: 345 + init_stock: 160 + price: 345 + service_level: 0.96 + vlt: 2 + SKU718: + cost: 357 + init_stock: 1160 + price: 357 + service_level: 0.96 + vlt: 2 + SKU719: + cost: 340 + init_stock: 1300 + price: 340 + service_level: 0.96 + vlt: 3 + SKU72: + cost: 458 + init_stock: 1700 + price: 458 + service_level: 0.96 + vlt: 2 + SKU720: + cost: 325 + init_stock: 840 + price: 325 + service_level: 0.96 + vlt: 3 + SKU721: + cost: 73 + init_stock: 220 + price: 73 + service_level: 0.96 + vlt: 2 + SKU722: + cost: 392 + init_stock: 1940 + price: 392 + service_level: 0.96 + vlt: 1 + SKU723: + cost: 318 + init_stock: 1780 + price: 318 + service_level: 0.96 + vlt: 3 + SKU724: + cost: 400 + init_stock: 660 + price: 400 + service_level: 0.96 + vlt: 1 + SKU725: + cost: 175 + init_stock: 740 + price: 175 + service_level: 0.96 + vlt: 1 + SKU726: + cost: 458 + init_stock: 1780 + price: 458 + service_level: 0.96 + vlt: 3 + SKU727: + cost: 418 + init_stock: 1140 + price: 418 + service_level: 0.96 + vlt: 1 + SKU728: + cost: 475 + init_stock: 740 + price: 475 + service_level: 0.96 + vlt: 3 + SKU729: + cost: 324 + init_stock: 720 + price: 324 + service_level: 0.96 + vlt: 3 + SKU73: + cost: 212 + init_stock: 1080 + price: 212 + service_level: 0.96 + vlt: 2 + SKU730: + cost: 16 + init_stock: 260 + price: 16 + service_level: 0.96 + vlt: 2 + SKU731: + cost: 88 + init_stock: 1640 + price: 88 + service_level: 0.96 + vlt: 2 + SKU732: + cost: 41 + init_stock: 1240 + price: 41 + service_level: 0.96 + vlt: 3 + SKU733: + cost: 315 + init_stock: 520 + price: 315 + service_level: 0.96 + vlt: 3 + SKU734: + cost: 37 + init_stock: 1020 + price: 37 + service_level: 0.96 + vlt: 3 + SKU735: + cost: 266 + init_stock: 1980 + price: 266 + service_level: 0.96 + vlt: 3 + SKU736: + cost: 368 + init_stock: 500 + price: 368 + service_level: 0.96 + vlt: 2 + SKU737: + cost: 475 + init_stock: 620 + price: 475 + service_level: 0.96 + vlt: 2 + SKU738: + cost: 185 + init_stock: 960 + price: 185 + service_level: 0.96 + vlt: 3 + SKU739: + cost: 475 + init_stock: 1480 + price: 475 + service_level: 0.96 + vlt: 2 + SKU74: + cost: 159 + init_stock: 840 + price: 159 + service_level: 0.96 + vlt: 1 + SKU740: + cost: 390 + init_stock: 1280 + price: 390 + service_level: 0.96 + vlt: 1 + SKU741: + cost: 91 + init_stock: 1120 + price: 91 + service_level: 0.96 + vlt: 1 + SKU742: + cost: 188 + init_stock: 440 + price: 188 + service_level: 0.96 + vlt: 1 + SKU743: + cost: 217 + init_stock: 860 + price: 217 + service_level: 0.96 + vlt: 3 + SKU744: + cost: 379 + init_stock: 1160 + price: 379 + service_level: 0.96 + vlt: 1 + SKU745: + cost: 316 + init_stock: 1840 + price: 316 + service_level: 0.96 + vlt: 2 + SKU746: + cost: 437 + init_stock: 800 + price: 437 + service_level: 0.96 + vlt: 2 + SKU747: + cost: 373 + init_stock: 1580 + price: 373 + service_level: 0.96 + vlt: 2 + SKU748: + cost: 275 + init_stock: 1320 + price: 275 + service_level: 0.96 + vlt: 2 + SKU749: + cost: 394 + init_stock: 1060 + price: 394 + service_level: 0.96 + vlt: 1 + SKU75: + cost: 131 + init_stock: 380 + price: 131 + service_level: 0.96 + vlt: 1 + SKU750: + cost: 256 + init_stock: 1240 + price: 256 + service_level: 0.96 + vlt: 3 + SKU751: + cost: 369 + init_stock: 1300 + price: 369 + service_level: 0.96 + vlt: 3 + SKU752: + cost: 259 + init_stock: 1560 + price: 259 + service_level: 0.96 + vlt: 3 + SKU753: + cost: 77 + init_stock: 1300 + price: 77 + service_level: 0.96 + vlt: 3 + SKU754: + cost: 387 + init_stock: 260 + price: 387 + service_level: 0.96 + vlt: 2 + SKU755: + cost: 354 + init_stock: 1600 + price: 354 + service_level: 0.96 + vlt: 3 + SKU756: + cost: 246 + init_stock: 1800 + price: 246 + service_level: 0.96 + vlt: 1 + SKU757: + cost: 40 + init_stock: 1140 + price: 40 + service_level: 0.96 + vlt: 3 + SKU758: + cost: 241 + init_stock: 800 + price: 241 + service_level: 0.96 + vlt: 2 + SKU759: + cost: 435 + init_stock: 760 + price: 435 + service_level: 0.96 + vlt: 1 + SKU76: + cost: 147 + init_stock: 1280 + price: 147 + service_level: 0.96 + vlt: 2 + SKU760: + cost: 176 + init_stock: 1020 + price: 176 + service_level: 0.96 + vlt: 3 + SKU761: + cost: 224 + init_stock: 1100 + price: 224 + service_level: 0.96 + vlt: 1 + SKU762: + cost: 264 + init_stock: 1020 + price: 264 + service_level: 0.96 + vlt: 1 + SKU763: + cost: 385 + init_stock: 1580 + price: 385 + service_level: 0.96 + vlt: 2 + SKU764: + cost: 349 + init_stock: 680 + price: 349 + service_level: 0.96 + vlt: 1 + SKU765: + cost: 345 + init_stock: 260 + price: 345 + service_level: 0.96 + vlt: 1 + SKU766: + cost: 478 + init_stock: 400 + price: 478 + service_level: 0.96 + vlt: 2 + SKU767: + cost: 95 + init_stock: 1520 + price: 95 + service_level: 0.96 + vlt: 1 + SKU768: + cost: 181 + init_stock: 1840 + price: 181 + service_level: 0.96 + vlt: 2 + SKU769: + cost: 24 + init_stock: 580 + price: 24 + service_level: 0.96 + vlt: 2 + SKU77: + cost: 409 + init_stock: 1440 + price: 409 + service_level: 0.96 + vlt: 1 + SKU770: + cost: 150 + init_stock: 1240 + price: 150 + service_level: 0.96 + vlt: 2 + SKU771: + cost: 101 + init_stock: 140 + price: 101 + service_level: 0.96 + vlt: 1 + SKU772: + cost: 256 + init_stock: 120 + price: 256 + service_level: 0.96 + vlt: 3 + SKU773: + cost: 84 + init_stock: 1840 + price: 84 + service_level: 0.96 + vlt: 1 + SKU774: + cost: 447 + init_stock: 1180 + price: 447 + service_level: 0.96 + vlt: 1 + SKU775: + cost: 175 + init_stock: 1720 + price: 175 + service_level: 0.96 + vlt: 3 + SKU776: + cost: 103 + init_stock: 1700 + price: 103 + service_level: 0.96 + vlt: 2 + SKU777: + cost: 292 + init_stock: 1140 + price: 292 + service_level: 0.96 + vlt: 2 + SKU778: + cost: 203 + init_stock: 160 + price: 203 + service_level: 0.96 + vlt: 3 + SKU779: + cost: 117 + init_stock: 860 + price: 117 + service_level: 0.96 + vlt: 1 + SKU78: + cost: 234 + init_stock: 260 + price: 234 + service_level: 0.96 + vlt: 3 + SKU780: + cost: 69 + init_stock: 1640 + price: 69 + service_level: 0.96 + vlt: 3 + SKU781: + cost: 372 + init_stock: 720 + price: 372 + service_level: 0.96 + vlt: 3 + SKU782: + cost: 27 + init_stock: 200 + price: 27 + service_level: 0.96 + vlt: 3 + SKU783: + cost: 87 + init_stock: 1620 + price: 87 + service_level: 0.96 + vlt: 2 + SKU784: + cost: 309 + init_stock: 1040 + price: 309 + service_level: 0.96 + vlt: 3 + SKU785: + cost: 191 + init_stock: 1400 + price: 191 + service_level: 0.96 + vlt: 2 + SKU786: + cost: 91 + init_stock: 440 + price: 91 + service_level: 0.96 + vlt: 3 + SKU787: + cost: 360 + init_stock: 1220 + price: 360 + service_level: 0.96 + vlt: 1 + SKU788: + cost: 351 + init_stock: 560 + price: 351 + service_level: 0.96 + vlt: 1 + SKU789: + cost: 153 + init_stock: 1160 + price: 153 + service_level: 0.96 + vlt: 2 + SKU79: + cost: 245 + init_stock: 220 + price: 245 + service_level: 0.96 + vlt: 1 + SKU790: + cost: 417 + init_stock: 1320 + price: 417 + service_level: 0.96 + vlt: 2 + SKU791: + cost: 134 + init_stock: 560 + price: 134 + service_level: 0.96 + vlt: 1 + SKU792: + cost: 313 + init_stock: 420 + price: 313 + service_level: 0.96 + vlt: 2 + SKU793: + cost: 195 + init_stock: 1520 + price: 195 + service_level: 0.96 + vlt: 2 + SKU794: + cost: 325 + init_stock: 1600 + price: 325 + service_level: 0.96 + vlt: 3 + SKU795: + cost: 276 + init_stock: 960 + price: 276 + service_level: 0.96 + vlt: 3 + SKU796: + cost: 447 + init_stock: 980 + price: 447 + service_level: 0.96 + vlt: 1 + SKU797: + cost: 100 + init_stock: 780 + price: 100 + service_level: 0.96 + vlt: 3 + SKU798: + cost: 426 + init_stock: 600 + price: 426 + service_level: 0.96 + vlt: 2 + SKU799: + cost: 63 + init_stock: 140 + price: 63 + service_level: 0.96 + vlt: 2 + SKU8: + cost: 125 + init_stock: 1260 + price: 125 + service_level: 0.96 + vlt: 3 + SKU80: + cost: 163 + init_stock: 1400 + price: 163 + service_level: 0.96 + vlt: 1 + SKU800: + cost: 298 + init_stock: 1880 + price: 298 + service_level: 0.96 + vlt: 2 + SKU801: + cost: 389 + init_stock: 720 + price: 389 + service_level: 0.96 + vlt: 2 + SKU802: + cost: 173 + init_stock: 1240 + price: 173 + service_level: 0.96 + vlt: 2 + SKU803: + cost: 272 + init_stock: 380 + price: 272 + service_level: 0.96 + vlt: 3 + SKU804: + cost: 390 + init_stock: 940 + price: 390 + service_level: 0.96 + vlt: 3 + SKU805: + cost: 61 + init_stock: 660 + price: 61 + service_level: 0.96 + vlt: 3 + SKU806: + cost: 158 + init_stock: 1040 + price: 158 + service_level: 0.96 + vlt: 1 + SKU807: + cost: 453 + init_stock: 520 + price: 453 + service_level: 0.96 + vlt: 3 + SKU808: + cost: 249 + init_stock: 1820 + price: 249 + service_level: 0.96 + vlt: 1 + SKU809: + cost: 55 + init_stock: 1300 + price: 55 + service_level: 0.96 + vlt: 1 + SKU81: + cost: 182 + init_stock: 1320 + price: 182 + service_level: 0.96 + vlt: 3 + SKU810: + cost: 276 + init_stock: 120 + price: 276 + service_level: 0.96 + vlt: 2 + SKU811: + cost: 254 + init_stock: 740 + price: 254 + service_level: 0.96 + vlt: 3 + SKU812: + cost: 252 + init_stock: 1600 + price: 252 + service_level: 0.96 + vlt: 1 + SKU813: + cost: 253 + init_stock: 140 + price: 253 + service_level: 0.96 + vlt: 3 + SKU814: + cost: 485 + init_stock: 1940 + price: 485 + service_level: 0.96 + vlt: 1 + SKU815: + cost: 390 + init_stock: 480 + price: 390 + service_level: 0.96 + vlt: 2 + SKU816: + cost: 152 + init_stock: 1820 + price: 152 + service_level: 0.96 + vlt: 3 + SKU817: + cost: 227 + init_stock: 100 + price: 227 + service_level: 0.96 + vlt: 2 + SKU818: + cost: 354 + init_stock: 860 + price: 354 + service_level: 0.96 + vlt: 2 + SKU819: + cost: 302 + init_stock: 540 + price: 302 + service_level: 0.96 + vlt: 1 + SKU82: + cost: 290 + init_stock: 560 + price: 290 + service_level: 0.96 + vlt: 1 + SKU820: + cost: 264 + init_stock: 1640 + price: 264 + service_level: 0.96 + vlt: 3 + SKU821: + cost: 99 + init_stock: 1380 + price: 99 + service_level: 0.96 + vlt: 1 + SKU822: + cost: 136 + init_stock: 1400 + price: 136 + service_level: 0.96 + vlt: 3 + SKU823: + cost: 75 + init_stock: 560 + price: 75 + service_level: 0.96 + vlt: 2 + SKU824: + cost: 170 + init_stock: 1400 + price: 170 + service_level: 0.96 + vlt: 1 + SKU825: + cost: 214 + init_stock: 300 + price: 214 + service_level: 0.96 + vlt: 1 + SKU826: + cost: 386 + init_stock: 1100 + price: 386 + service_level: 0.96 + vlt: 2 + SKU827: + cost: 148 + init_stock: 1280 + price: 148 + service_level: 0.96 + vlt: 2 + SKU828: + cost: 400 + init_stock: 1380 + price: 400 + service_level: 0.96 + vlt: 1 + SKU829: + cost: 61 + init_stock: 1480 + price: 61 + service_level: 0.96 + vlt: 1 + SKU83: + cost: 296 + init_stock: 340 + price: 296 + service_level: 0.96 + vlt: 2 + SKU830: + cost: 167 + init_stock: 480 + price: 167 + service_level: 0.96 + vlt: 3 + SKU831: + cost: 262 + init_stock: 580 + price: 262 + service_level: 0.96 + vlt: 1 + SKU832: + cost: 33 + init_stock: 1640 + price: 33 + service_level: 0.96 + vlt: 1 + SKU833: + cost: 400 + init_stock: 1960 + price: 400 + service_level: 0.96 + vlt: 3 + SKU834: + cost: 422 + init_stock: 100 + price: 422 + service_level: 0.96 + vlt: 1 + SKU835: + cost: 440 + init_stock: 1040 + price: 440 + service_level: 0.96 + vlt: 2 + SKU836: + cost: 323 + init_stock: 1920 + price: 323 + service_level: 0.96 + vlt: 1 + SKU837: + cost: 373 + init_stock: 520 + price: 373 + service_level: 0.96 + vlt: 3 + SKU838: + cost: 456 + init_stock: 1540 + price: 456 + service_level: 0.96 + vlt: 1 + SKU839: + cost: 473 + init_stock: 1200 + price: 473 + service_level: 0.96 + vlt: 1 + SKU84: + cost: 318 + init_stock: 840 + price: 318 + service_level: 0.96 + vlt: 3 + SKU840: + cost: 266 + init_stock: 1000 + price: 266 + service_level: 0.96 + vlt: 1 + SKU841: + cost: 285 + init_stock: 1700 + price: 285 + service_level: 0.96 + vlt: 3 + SKU842: + cost: 41 + init_stock: 1680 + price: 41 + service_level: 0.96 + vlt: 1 + SKU843: + cost: 360 + init_stock: 1440 + price: 360 + service_level: 0.96 + vlt: 2 + SKU844: + cost: 51 + init_stock: 1580 + price: 51 + service_level: 0.96 + vlt: 3 + SKU845: + cost: 288 + init_stock: 440 + price: 288 + service_level: 0.96 + vlt: 3 + SKU846: + cost: 485 + init_stock: 780 + price: 485 + service_level: 0.96 + vlt: 1 + SKU847: + cost: 388 + init_stock: 1820 + price: 388 + service_level: 0.96 + vlt: 1 + SKU848: + cost: 306 + init_stock: 920 + price: 306 + service_level: 0.96 + vlt: 2 + SKU849: + cost: 320 + init_stock: 800 + price: 320 + service_level: 0.96 + vlt: 2 + SKU85: + cost: 442 + init_stock: 2000 + price: 442 + service_level: 0.96 + vlt: 3 + SKU850: + cost: 183 + init_stock: 1140 + price: 183 + service_level: 0.96 + vlt: 2 + SKU851: + cost: 53 + init_stock: 1280 + price: 53 + service_level: 0.96 + vlt: 1 + SKU852: + cost: 306 + init_stock: 1080 + price: 306 + service_level: 0.96 + vlt: 2 + SKU853: + cost: 386 + init_stock: 1120 + price: 386 + service_level: 0.96 + vlt: 3 + SKU854: + cost: 212 + init_stock: 1300 + price: 212 + service_level: 0.96 + vlt: 3 + SKU855: + cost: 76 + init_stock: 1440 + price: 76 + service_level: 0.96 + vlt: 3 + SKU856: + cost: 380 + init_stock: 640 + price: 380 + service_level: 0.96 + vlt: 2 + SKU857: + cost: 462 + init_stock: 2000 + price: 462 + service_level: 0.96 + vlt: 1 + SKU858: + cost: 80 + init_stock: 480 + price: 80 + service_level: 0.96 + vlt: 3 + SKU859: + cost: 215 + init_stock: 560 + price: 215 + service_level: 0.96 + vlt: 3 + SKU86: + cost: 386 + init_stock: 1700 + price: 386 + service_level: 0.96 + vlt: 2 + SKU860: + cost: 313 + init_stock: 300 + price: 313 + service_level: 0.96 + vlt: 2 + SKU861: + cost: 477 + init_stock: 260 + price: 477 + service_level: 0.96 + vlt: 2 + SKU862: + cost: 240 + init_stock: 320 + price: 240 + service_level: 0.96 + vlt: 2 + SKU863: + cost: 470 + init_stock: 1860 + price: 470 + service_level: 0.96 + vlt: 2 + SKU864: + cost: 203 + init_stock: 380 + price: 203 + service_level: 0.96 + vlt: 1 + SKU865: + cost: 144 + init_stock: 380 + price: 144 + service_level: 0.96 + vlt: 3 + SKU866: + cost: 172 + init_stock: 1760 + price: 172 + service_level: 0.96 + vlt: 1 + SKU867: + cost: 499 + init_stock: 2000 + price: 499 + service_level: 0.96 + vlt: 2 + SKU868: + cost: 41 + init_stock: 1000 + price: 41 + service_level: 0.96 + vlt: 2 + SKU869: + cost: 97 + init_stock: 1940 + price: 97 + service_level: 0.96 + vlt: 2 + SKU87: + cost: 368 + init_stock: 1380 + price: 368 + service_level: 0.96 + vlt: 2 + SKU870: + cost: 281 + init_stock: 1340 + price: 281 + service_level: 0.96 + vlt: 1 + SKU871: + cost: 101 + init_stock: 1980 + price: 101 + service_level: 0.96 + vlt: 1 + SKU872: + cost: 133 + init_stock: 640 + price: 133 + service_level: 0.96 + vlt: 3 + SKU873: + cost: 423 + init_stock: 620 + price: 423 + service_level: 0.96 + vlt: 2 + SKU874: + cost: 469 + init_stock: 1200 + price: 469 + service_level: 0.96 + vlt: 1 + SKU875: + cost: 51 + init_stock: 460 + price: 51 + service_level: 0.96 + vlt: 2 + SKU876: + cost: 303 + init_stock: 1220 + price: 303 + service_level: 0.96 + vlt: 3 + SKU877: + cost: 40 + init_stock: 700 + price: 40 + service_level: 0.96 + vlt: 3 + SKU878: + cost: 27 + init_stock: 1360 + price: 27 + service_level: 0.96 + vlt: 3 + SKU879: + cost: 150 + init_stock: 2000 + price: 150 + service_level: 0.96 + vlt: 1 + SKU88: + cost: 471 + init_stock: 240 + price: 471 + service_level: 0.96 + vlt: 1 + SKU880: + cost: 433 + init_stock: 620 + price: 433 + service_level: 0.96 + vlt: 3 + SKU881: + cost: 431 + init_stock: 1400 + price: 431 + service_level: 0.96 + vlt: 3 + SKU882: + cost: 194 + init_stock: 320 + price: 194 + service_level: 0.96 + vlt: 1 + SKU883: + cost: 284 + init_stock: 760 + price: 284 + service_level: 0.96 + vlt: 1 + SKU884: + cost: 139 + init_stock: 780 + price: 139 + service_level: 0.96 + vlt: 2 + SKU885: + cost: 49 + init_stock: 540 + price: 49 + service_level: 0.96 + vlt: 3 + SKU886: + cost: 372 + init_stock: 1460 + price: 372 + service_level: 0.96 + vlt: 3 + SKU887: + cost: 266 + init_stock: 1740 + price: 266 + service_level: 0.96 + vlt: 1 + SKU888: + cost: 143 + init_stock: 180 + price: 143 + service_level: 0.96 + vlt: 2 + SKU889: + cost: 101 + init_stock: 420 + price: 101 + service_level: 0.96 + vlt: 2 + SKU89: + cost: 138 + init_stock: 1860 + price: 138 + service_level: 0.96 + vlt: 3 + SKU890: + cost: 161 + init_stock: 2000 + price: 161 + service_level: 0.96 + vlt: 3 + SKU891: + cost: 356 + init_stock: 440 + price: 356 + service_level: 0.96 + vlt: 3 + SKU892: + cost: 313 + init_stock: 1840 + price: 313 + service_level: 0.96 + vlt: 2 + SKU893: + cost: 229 + init_stock: 1980 + price: 229 + service_level: 0.96 + vlt: 1 + SKU894: + cost: 129 + init_stock: 1480 + price: 129 + service_level: 0.96 + vlt: 1 + SKU895: + cost: 230 + init_stock: 260 + price: 230 + service_level: 0.96 + vlt: 2 + SKU896: + cost: 289 + init_stock: 1600 + price: 289 + service_level: 0.96 + vlt: 3 + SKU897: + cost: 393 + init_stock: 1440 + price: 393 + service_level: 0.96 + vlt: 3 + SKU898: + cost: 477 + init_stock: 220 + price: 477 + service_level: 0.96 + vlt: 2 + SKU899: + cost: 233 + init_stock: 960 + price: 233 + service_level: 0.96 + vlt: 2 + SKU9: + cost: 166 + init_stock: 1920 + price: 166 + service_level: 0.96 + vlt: 2 + SKU90: + cost: 454 + init_stock: 1020 + price: 454 + service_level: 0.96 + vlt: 2 + SKU900: + cost: 158 + init_stock: 1100 + price: 158 + service_level: 0.96 + vlt: 1 + SKU901: + cost: 215 + init_stock: 580 + price: 215 + service_level: 0.96 + vlt: 3 + SKU902: + cost: 125 + init_stock: 1600 + price: 125 + service_level: 0.96 + vlt: 2 + SKU903: + cost: 357 + init_stock: 760 + price: 357 + service_level: 0.96 + vlt: 2 + SKU904: + cost: 496 + init_stock: 1020 + price: 496 + service_level: 0.96 + vlt: 1 + SKU905: + cost: 249 + init_stock: 620 + price: 249 + service_level: 0.96 + vlt: 3 + SKU906: + cost: 166 + init_stock: 1040 + price: 166 + service_level: 0.96 + vlt: 2 + SKU907: + cost: 22 + init_stock: 1660 + price: 22 + service_level: 0.96 + vlt: 3 + SKU908: + cost: 408 + init_stock: 1560 + price: 408 + service_level: 0.96 + vlt: 2 + SKU909: + cost: 482 + init_stock: 1920 + price: 482 + service_level: 0.96 + vlt: 1 + SKU91: + cost: 303 + init_stock: 320 + price: 303 + service_level: 0.96 + vlt: 2 + SKU910: + cost: 226 + init_stock: 660 + price: 226 + service_level: 0.96 + vlt: 2 + SKU911: + cost: 461 + init_stock: 1400 + price: 461 + service_level: 0.96 + vlt: 2 + SKU912: + cost: 236 + init_stock: 540 + price: 236 + service_level: 0.96 + vlt: 3 + SKU913: + cost: 322 + init_stock: 920 + price: 322 + service_level: 0.96 + vlt: 2 + SKU914: + cost: 272 + init_stock: 1240 + price: 272 + service_level: 0.96 + vlt: 3 + SKU915: + cost: 337 + init_stock: 1120 + price: 337 + service_level: 0.96 + vlt: 3 + SKU916: + cost: 337 + init_stock: 1780 + price: 337 + service_level: 0.96 + vlt: 3 + SKU917: + cost: 258 + init_stock: 600 + price: 258 + service_level: 0.96 + vlt: 3 + SKU918: + cost: 148 + init_stock: 1100 + price: 148 + service_level: 0.96 + vlt: 1 + SKU919: + cost: 393 + init_stock: 500 + price: 393 + service_level: 0.96 + vlt: 3 + SKU92: + cost: 262 + init_stock: 1260 + price: 262 + service_level: 0.96 + vlt: 2 + SKU920: + cost: 357 + init_stock: 1720 + price: 357 + service_level: 0.96 + vlt: 3 + SKU921: + cost: 227 + init_stock: 1320 + price: 227 + service_level: 0.96 + vlt: 2 + SKU922: + cost: 112 + init_stock: 1340 + price: 112 + service_level: 0.96 + vlt: 2 + SKU923: + cost: 496 + init_stock: 1160 + price: 496 + service_level: 0.96 + vlt: 2 + SKU924: + cost: 316 + init_stock: 1700 + price: 316 + service_level: 0.96 + vlt: 3 + SKU925: + cost: 360 + init_stock: 300 + price: 360 + service_level: 0.96 + vlt: 1 + SKU926: + cost: 360 + init_stock: 1340 + price: 360 + service_level: 0.96 + vlt: 2 + SKU927: + cost: 260 + init_stock: 420 + price: 260 + service_level: 0.96 + vlt: 3 + SKU928: + cost: 491 + init_stock: 1660 + price: 491 + service_level: 0.96 + vlt: 1 + SKU929: + cost: 359 + init_stock: 2000 + price: 359 + service_level: 0.96 + vlt: 3 + SKU93: + cost: 404 + init_stock: 1340 + price: 404 + service_level: 0.96 + vlt: 2 + SKU930: + cost: 198 + init_stock: 560 + price: 198 + service_level: 0.96 + vlt: 2 + SKU931: + cost: 71 + init_stock: 280 + price: 71 + service_level: 0.96 + vlt: 3 + SKU932: + cost: 163 + init_stock: 1320 + price: 163 + service_level: 0.96 + vlt: 2 + SKU933: + cost: 113 + init_stock: 1560 + price: 113 + service_level: 0.96 + vlt: 1 + SKU934: + cost: 219 + init_stock: 1340 + price: 219 + service_level: 0.96 + vlt: 3 + SKU935: + cost: 364 + init_stock: 1880 + price: 364 + service_level: 0.96 + vlt: 1 + SKU936: + cost: 24 + init_stock: 100 + price: 24 + service_level: 0.96 + vlt: 3 + SKU937: + cost: 135 + init_stock: 340 + price: 135 + service_level: 0.96 + vlt: 1 + SKU938: + cost: 432 + init_stock: 420 + price: 432 + service_level: 0.96 + vlt: 2 + SKU939: + cost: 173 + init_stock: 1180 + price: 173 + service_level: 0.96 + vlt: 3 + SKU94: + cost: 184 + init_stock: 940 + price: 184 + service_level: 0.96 + vlt: 3 + SKU940: + cost: 14 + init_stock: 1860 + price: 14 + service_level: 0.96 + vlt: 1 + SKU941: + cost: 80 + init_stock: 1140 + price: 80 + service_level: 0.96 + vlt: 3 + SKU942: + cost: 202 + init_stock: 260 + price: 202 + service_level: 0.96 + vlt: 3 + SKU943: + cost: 138 + init_stock: 980 + price: 138 + service_level: 0.96 + vlt: 1 + SKU944: + cost: 196 + init_stock: 880 + price: 196 + service_level: 0.96 + vlt: 1 + SKU945: + cost: 141 + init_stock: 340 + price: 141 + service_level: 0.96 + vlt: 2 + SKU946: + cost: 325 + init_stock: 980 + price: 325 + service_level: 0.96 + vlt: 1 + SKU947: + cost: 338 + init_stock: 1820 + price: 338 + service_level: 0.96 + vlt: 3 + SKU948: + cost: 425 + init_stock: 560 + price: 425 + service_level: 0.96 + vlt: 3 + SKU949: + cost: 309 + init_stock: 100 + price: 309 + service_level: 0.96 + vlt: 2 + SKU95: + cost: 136 + init_stock: 420 + price: 136 + service_level: 0.96 + vlt: 3 + SKU950: + cost: 102 + init_stock: 1080 + price: 102 + service_level: 0.96 + vlt: 2 + SKU951: + cost: 75 + init_stock: 360 + price: 75 + service_level: 0.96 + vlt: 2 + SKU952: + cost: 156 + init_stock: 220 + price: 156 + service_level: 0.96 + vlt: 3 + SKU953: + cost: 138 + init_stock: 700 + price: 138 + service_level: 0.96 + vlt: 3 + SKU954: + cost: 296 + init_stock: 1720 + price: 296 + service_level: 0.96 + vlt: 1 + SKU955: + cost: 55 + init_stock: 1260 + price: 55 + service_level: 0.96 + vlt: 1 + SKU956: + cost: 282 + init_stock: 1700 + price: 282 + service_level: 0.96 + vlt: 1 + SKU957: + cost: 305 + init_stock: 1020 + price: 305 + service_level: 0.96 + vlt: 2 + SKU958: + cost: 369 + init_stock: 1320 + price: 369 + service_level: 0.96 + vlt: 2 + SKU959: + cost: 81 + init_stock: 1640 + price: 81 + service_level: 0.96 + vlt: 1 + SKU96: + cost: 176 + init_stock: 880 + price: 176 + service_level: 0.96 + vlt: 3 + SKU960: + cost: 147 + init_stock: 120 + price: 147 + service_level: 0.96 + vlt: 3 + SKU961: + cost: 264 + init_stock: 880 + price: 264 + service_level: 0.96 + vlt: 1 + SKU962: + cost: 354 + init_stock: 880 + price: 354 + service_level: 0.96 + vlt: 1 + SKU963: + cost: 349 + init_stock: 420 + price: 349 + service_level: 0.96 + vlt: 1 + SKU964: + cost: 244 + init_stock: 1460 + price: 244 + service_level: 0.96 + vlt: 1 + SKU965: + cost: 124 + init_stock: 1020 + price: 124 + service_level: 0.96 + vlt: 1 + SKU966: + cost: 302 + init_stock: 880 + price: 302 + service_level: 0.96 + vlt: 3 + SKU967: + cost: 67 + init_stock: 900 + price: 67 + service_level: 0.96 + vlt: 3 + SKU968: + cost: 281 + init_stock: 980 + price: 281 + service_level: 0.96 + vlt: 2 + SKU969: + cost: 249 + init_stock: 120 + price: 249 + service_level: 0.96 + vlt: 1 + SKU97: + cost: 28 + init_stock: 600 + price: 28 + service_level: 0.96 + vlt: 3 + SKU970: + cost: 244 + init_stock: 100 + price: 244 + service_level: 0.96 + vlt: 2 + SKU971: + cost: 368 + init_stock: 1460 + price: 368 + service_level: 0.96 + vlt: 1 + SKU972: + cost: 209 + init_stock: 1380 + price: 209 + service_level: 0.96 + vlt: 1 + SKU973: + cost: 271 + init_stock: 220 + price: 271 + service_level: 0.96 + vlt: 2 + SKU974: + cost: 170 + init_stock: 640 + price: 170 + service_level: 0.96 + vlt: 1 + SKU975: + cost: 198 + init_stock: 440 + price: 198 + service_level: 0.96 + vlt: 3 + SKU976: + cost: 178 + init_stock: 300 + price: 178 + service_level: 0.96 + vlt: 2 + SKU977: + cost: 234 + init_stock: 1080 + price: 234 + service_level: 0.96 + vlt: 3 + SKU978: + cost: 470 + init_stock: 1280 + price: 470 + service_level: 0.96 + vlt: 3 + SKU979: + cost: 443 + init_stock: 1920 + price: 443 + service_level: 0.96 + vlt: 2 + SKU98: + cost: 443 + init_stock: 700 + price: 443 + service_level: 0.96 + vlt: 1 + SKU980: + cost: 39 + init_stock: 1360 + price: 39 + service_level: 0.96 + vlt: 3 + SKU981: + cost: 482 + init_stock: 1440 + price: 482 + service_level: 0.96 + vlt: 2 + SKU982: + cost: 213 + init_stock: 1040 + price: 213 + service_level: 0.96 + vlt: 3 + SKU983: + cost: 449 + init_stock: 1840 + price: 449 + service_level: 0.96 + vlt: 1 + SKU984: + cost: 232 + init_stock: 1820 + price: 232 + service_level: 0.96 + vlt: 3 + SKU985: + cost: 290 + init_stock: 1020 + price: 290 + service_level: 0.96 + vlt: 1 + SKU986: + cost: 275 + init_stock: 340 + price: 275 + service_level: 0.96 + vlt: 3 + SKU987: + cost: 434 + init_stock: 680 + price: 434 + service_level: 0.96 + vlt: 1 + SKU988: + cost: 102 + init_stock: 460 + price: 102 + service_level: 0.96 + vlt: 1 + SKU989: + cost: 484 + init_stock: 1860 + price: 484 + service_level: 0.96 + vlt: 1 + SKU99: + cost: 361 + init_stock: 900 + price: 361 + service_level: 0.96 + vlt: 3 + SKU990: + cost: 108 + init_stock: 1140 + price: 108 + service_level: 0.96 + vlt: 3 + SKU991: + cost: 409 + init_stock: 380 + price: 409 + service_level: 0.96 + vlt: 3 + SKU992: + cost: 434 + init_stock: 1860 + price: 434 + service_level: 0.96 + vlt: 3 + SKU993: + cost: 145 + init_stock: 1720 + price: 145 + service_level: 0.96 + vlt: 2 + SKU994: + cost: 470 + init_stock: 1000 + price: 470 + service_level: 0.96 + vlt: 3 + SKU995: + cost: 241 + init_stock: 1520 + price: 241 + service_level: 0.96 + vlt: 2 + SKU996: + cost: 260 + init_stock: 1460 + price: 260 + service_level: 0.96 + vlt: 3 + SKU997: + cost: 400 + init_stock: 680 + price: 400 + service_level: 0.96 + vlt: 1 + SKU998: + cost: 447 + init_stock: 1100 + price: 447 + service_level: 0.96 + vlt: 2 + SKU999: + cost: 79 + init_stock: 460 + price: 79 + service_level: 0.96 + vlt: 2 + - children: + storage: + config: + capacity: 1064900 + unit_storage_cost: 1 + config: + order_cost: 500 + definition_ref: RetailerFacility + name: STORE0 + skus: + SKU0: + constraint: null + cost: 322 + init_stock: 126 + max_stock: 1000 + price: 631 + sale_gamma: 63 + service_level: 0.95 + SKU1: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 284 + init_stock: 176 + max_stock: 1000 + price: 448 + sale_gamma: 22 + service_level: 0.95 + SKU10: + constraint: null + cost: 68 + init_stock: 150 + max_stock: 1000 + price: 116 + sale_gamma: 25 + service_level: 0.95 + SKU100: + constraint: G(low_profit -> low_stock_constraint) + cost: 16 + init_stock: 390 + max_stock: 1000 + price: 23 + sale_gamma: 65 + service_level: 0.95 + SKU101: + constraint: null + cost: 84 + init_stock: 497 + max_stock: 1000 + price: 149 + sale_gamma: 71 + service_level: 0.95 + SKU102: + constraint: null + cost: 328 + init_stock: 558 + max_stock: 1000 + price: 505 + sale_gamma: 93 + service_level: 0.95 + SKU103: + constraint: null + cost: 334 + init_stock: 285 + max_stock: 1000 + price: 601 + sale_gamma: 95 + service_level: 0.95 + SKU104: + constraint: null + cost: 49 + init_stock: 325 + max_stock: 1000 + price: 57 + sale_gamma: 65 + service_level: 0.95 + SKU105: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 110 + init_stock: 285 + max_stock: 1000 + price: 171 + sale_gamma: 57 + service_level: 0.95 + SKU106: + constraint: G(low_profit -> low_stock_constraint) + cost: 251 + init_stock: 584 + max_stock: 1000 + price: 454 + sale_gamma: 73 + service_level: 0.95 + SKU107: + constraint: G(stock_constraint) + cost: 423 + init_stock: 348 + max_stock: 1000 + price: 706 + sale_gamma: 87 + service_level: 0.95 + SKU108: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 458 + init_stock: 368 + max_stock: 1000 + price: 801 + sale_gamma: 92 + service_level: 0.95 + SKU109: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 88 + init_stock: 328 + max_stock: 1000 + price: 98 + sale_gamma: 82 + service_level: 0.95 + SKU11: + constraint: null + cost: 400 + init_stock: 306 + max_stock: 1000 + price: 680 + sale_gamma: 51 + service_level: 0.95 + SKU110: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 66 + init_stock: 112 + max_stock: 1000 + price: 100 + sale_gamma: 14 + service_level: 0.95 + SKU111: + constraint: G(stock_constraint) + cost: 260 + init_stock: 183 + max_stock: 1000 + price: 364 + sale_gamma: 61 + service_level: 0.95 + SKU112: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 61 + init_stock: 475 + max_stock: 1000 + price: 119 + sale_gamma: 95 + service_level: 0.95 + SKU113: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 155 + max_stock: 1000 + price: 678 + sale_gamma: 31 + service_level: 0.95 + SKU114: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 389 + init_stock: 162 + max_stock: 1000 + price: 564 + sale_gamma: 27 + service_level: 0.95 + SKU115: + constraint: G(low_profit -> low_stock_constraint) + cost: 286 + init_stock: 258 + max_stock: 1000 + price: 557 + sale_gamma: 86 + service_level: 0.95 + SKU116: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 496 + init_stock: 288 + max_stock: 1000 + price: 768 + sale_gamma: 72 + service_level: 0.95 + SKU117: + constraint: null + cost: 320 + init_stock: 644 + max_stock: 1000 + price: 374 + sale_gamma: 92 + service_level: 0.95 + SKU118: + constraint: null + cost: 183 + init_stock: 66 + max_stock: 1000 + price: 208 + sale_gamma: 33 + service_level: 0.95 + SKU119: + constraint: null + cost: 209 + init_stock: 256 + max_stock: 1000 + price: 317 + sale_gamma: 32 + service_level: 0.95 + SKU12: + constraint: null + cost: 112 + init_stock: 336 + max_stock: 1000 + price: 210 + sale_gamma: 84 + service_level: 0.95 + SKU120: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 121 + init_stock: 392 + max_stock: 1000 + price: 151 + sale_gamma: 98 + service_level: 0.95 + SKU121: + constraint: null + cost: 40 + init_stock: 510 + max_stock: 1000 + price: 54 + sale_gamma: 85 + service_level: 0.95 + SKU122: + constraint: G(stock_constraint) + cost: 437 + init_stock: 35 + max_stock: 1000 + price: 589 + sale_gamma: 7 + service_level: 0.95 + SKU123: + constraint: G(stock_constraint) + cost: 233 + init_stock: 114 + max_stock: 1000 + price: 326 + sale_gamma: 19 + service_level: 0.95 + SKU124: + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 108 + max_stock: 1000 + price: 298 + sale_gamma: 36 + service_level: 0.95 + SKU125: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 16 + init_stock: 368 + max_stock: 1000 + price: 32 + sale_gamma: 92 + service_level: 0.95 + SKU126: + constraint: null + cost: 36 + init_stock: 156 + max_stock: 1000 + price: 40 + sale_gamma: 39 + service_level: 0.95 + SKU127: + constraint: G(stock_constraint) + cost: 217 + init_stock: 155 + max_stock: 1000 + price: 366 + sale_gamma: 31 + service_level: 0.95 + SKU128: + constraint: null + cost: 165 + init_stock: 95 + max_stock: 1000 + price: 283 + sale_gamma: 19 + service_level: 0.95 + SKU129: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 300 + max_stock: 1000 + price: 203 + sale_gamma: 50 + service_level: 0.95 + SKU13: + constraint: null + cost: 317 + init_stock: 456 + max_stock: 1000 + price: 573 + sale_gamma: 57 + service_level: 0.95 + SKU130: + constraint: null + cost: 348 + init_stock: 784 + max_stock: 1000 + price: 396 + sale_gamma: 98 + service_level: 0.95 + SKU131: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 64 + init_stock: 270 + max_stock: 1000 + price: 110 + sale_gamma: 45 + service_level: 0.95 + SKU132: + constraint: null + cost: 427 + init_stock: 105 + max_stock: 1000 + price: 678 + sale_gamma: 21 + service_level: 0.95 + SKU133: + constraint: null + cost: 224 + init_stock: 145 + max_stock: 1000 + price: 448 + sale_gamma: 29 + service_level: 0.95 + SKU134: + constraint: G(stock_constraint) + cost: 336 + init_stock: 539 + max_stock: 1000 + price: 470 + sale_gamma: 77 + service_level: 0.95 + SKU135: + constraint: null + cost: 153 + init_stock: 500 + max_stock: 1000 + price: 243 + sale_gamma: 100 + service_level: 0.95 + SKU136: + constraint: G(low_profit -> low_stock_constraint) + cost: 199 + init_stock: 497 + max_stock: 1000 + price: 370 + sale_gamma: 71 + service_level: 0.95 + SKU137: + constraint: G(stock_constraint) + cost: 93 + init_stock: 444 + max_stock: 1000 + price: 165 + sale_gamma: 74 + service_level: 0.95 + SKU138: + constraint: G(stock_constraint) + cost: 228 + init_stock: 180 + max_stock: 1000 + price: 253 + sale_gamma: 36 + service_level: 0.95 + SKU139: + constraint: G(stock_constraint) + cost: 207 + init_stock: 144 + max_stock: 1000 + price: 368 + sale_gamma: 24 + service_level: 0.95 + SKU14: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 268 + init_stock: 372 + max_stock: 1000 + price: 305 + sale_gamma: 62 + service_level: 0.95 + SKU140: + constraint: null + cost: 261 + init_stock: 238 + max_stock: 1000 + price: 357 + sale_gamma: 34 + service_level: 0.95 + SKU141: + constraint: null + cost: 190 + init_stock: 164 + max_stock: 1000 + price: 370 + sale_gamma: 41 + service_level: 0.95 + SKU142: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 320 + init_stock: 152 + max_stock: 1000 + price: 428 + sale_gamma: 38 + service_level: 0.95 + SKU143: + constraint: G(stock_constraint) + cost: 318 + init_stock: 182 + max_stock: 1000 + price: 566 + sale_gamma: 26 + service_level: 0.95 + SKU144: + constraint: null + cost: 400 + init_stock: 24 + max_stock: 1000 + price: 716 + sale_gamma: 12 + service_level: 0.95 + SKU145: + constraint: null + cost: 399 + init_stock: 328 + max_stock: 1000 + price: 774 + sale_gamma: 82 + service_level: 0.95 + SKU146: + constraint: null + cost: 177 + init_stock: 192 + max_stock: 1000 + price: 194 + sale_gamma: 48 + service_level: 0.95 + SKU147: + constraint: G(low_profit -> low_stock_constraint) + cost: 472 + init_stock: 392 + max_stock: 1000 + price: 840 + sale_gamma: 56 + service_level: 0.95 + SKU148: + constraint: G(stock_constraint) + cost: 313 + init_stock: 308 + max_stock: 1000 + price: 566 + sale_gamma: 77 + service_level: 0.95 + SKU149: + constraint: G(low_profit -> low_stock_constraint) + cost: 357 + init_stock: 539 + max_stock: 1000 + price: 549 + sale_gamma: 77 + service_level: 0.95 + SKU15: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 25 + max_stock: 1000 + price: 58 + sale_gamma: 5 + service_level: 0.95 + SKU150: + constraint: null + cost: 106 + init_stock: 210 + max_stock: 1000 + price: 159 + sale_gamma: 70 + service_level: 0.95 + SKU151: + constraint: G(low_profit -> low_stock_constraint) + cost: 223 + init_stock: 146 + max_stock: 1000 + price: 370 + sale_gamma: 73 + service_level: 0.95 + SKU152: + constraint: null + cost: 10 + init_stock: 408 + max_stock: 1000 + price: 14 + sale_gamma: 51 + service_level: 0.95 + SKU153: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 441 + init_stock: 372 + max_stock: 1000 + price: 538 + sale_gamma: 62 + service_level: 0.95 + SKU154: + constraint: null + cost: 77 + init_stock: 680 + max_stock: 1000 + price: 135 + sale_gamma: 85 + service_level: 0.95 + SKU155: + constraint: G(low_profit -> low_stock_constraint) + cost: 422 + init_stock: 159 + max_stock: 1000 + price: 641 + sale_gamma: 53 + service_level: 0.95 + SKU156: + constraint: null + cost: 10 + init_stock: 36 + max_stock: 1000 + price: 16 + sale_gamma: 12 + service_level: 0.95 + SKU157: + constraint: null + cost: 410 + init_stock: 525 + max_stock: 1000 + price: 594 + sale_gamma: 75 + service_level: 0.95 + SKU158: + constraint: null + cost: 145 + init_stock: 486 + max_stock: 1000 + price: 275 + sale_gamma: 81 + service_level: 0.95 + SKU159: + constraint: G(stock_constraint) + cost: 193 + init_stock: 100 + max_stock: 1000 + price: 368 + sale_gamma: 25 + service_level: 0.95 + SKU16: + constraint: null + cost: 175 + init_stock: 464 + max_stock: 1000 + price: 344 + sale_gamma: 58 + service_level: 0.95 + SKU160: + constraint: G(low_profit -> low_stock_constraint) + cost: 459 + init_stock: 510 + max_stock: 1000 + price: 876 + sale_gamma: 85 + service_level: 0.95 + SKU161: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 239 + init_stock: 230 + max_stock: 1000 + price: 478 + sale_gamma: 46 + service_level: 0.95 + SKU162: + constraint: null + cost: 158 + init_stock: 30 + max_stock: 1000 + price: 290 + sale_gamma: 5 + service_level: 0.95 + SKU163: + constraint: G(stock_constraint) + cost: 486 + init_stock: 273 + max_stock: 1000 + price: 704 + sale_gamma: 39 + service_level: 0.95 + SKU164: + constraint: null + cost: 496 + init_stock: 500 + max_stock: 1000 + price: 615 + sale_gamma: 100 + service_level: 0.95 + SKU165: + constraint: G(stock_constraint) + cost: 274 + init_stock: 231 + max_stock: 1000 + price: 548 + sale_gamma: 33 + service_level: 0.95 + SKU166: + constraint: null + cost: 79 + init_stock: 178 + max_stock: 1000 + price: 137 + sale_gamma: 89 + service_level: 0.95 + SKU167: + constraint: G(stock_constraint) + cost: 443 + init_stock: 52 + max_stock: 1000 + price: 854 + sale_gamma: 13 + service_level: 0.95 + SKU168: + constraint: null + cost: 357 + init_stock: 522 + max_stock: 1000 + price: 546 + sale_gamma: 87 + service_level: 0.95 + SKU169: + constraint: null + cost: 369 + init_stock: 686 + max_stock: 1000 + price: 505 + sale_gamma: 98 + service_level: 0.95 + SKU17: + constraint: null + cost: 346 + init_stock: 18 + max_stock: 1000 + price: 553 + sale_gamma: 9 + service_level: 0.95 + SKU170: + constraint: null + cost: 68 + init_stock: 275 + max_stock: 1000 + price: 113 + sale_gamma: 55 + service_level: 0.95 + SKU171: + constraint: G(low_profit -> low_stock_constraint) + cost: 398 + init_stock: 380 + max_stock: 1000 + price: 704 + sale_gamma: 76 + service_level: 0.95 + SKU172: + constraint: null + cost: 200 + init_stock: 426 + max_stock: 1000 + price: 222 + sale_gamma: 71 + service_level: 0.95 + SKU173: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 175 + init_stock: 480 + max_stock: 1000 + price: 264 + sale_gamma: 96 + service_level: 0.95 + SKU174: + constraint: null + cost: 291 + init_stock: 380 + max_stock: 1000 + price: 582 + sale_gamma: 76 + service_level: 0.95 + SKU175: + constraint: null + cost: 84 + init_stock: 225 + max_stock: 1000 + price: 140 + sale_gamma: 75 + service_level: 0.95 + SKU176: + constraint: G(stock_constraint) + cost: 407 + init_stock: 330 + max_stock: 1000 + price: 610 + sale_gamma: 66 + service_level: 0.95 + SKU177: + constraint: null + cost: 257 + init_stock: 155 + max_stock: 1000 + price: 346 + sale_gamma: 31 + service_level: 0.95 + SKU178: + constraint: null + cost: 423 + init_stock: 20 + max_stock: 1000 + price: 499 + sale_gamma: 5 + service_level: 0.95 + SKU179: + constraint: null + cost: 497 + init_stock: 415 + max_stock: 1000 + price: 690 + sale_gamma: 83 + service_level: 0.95 + SKU18: + constraint: null + cost: 258 + init_stock: 405 + max_stock: 1000 + price: 387 + sale_gamma: 81 + service_level: 0.95 + SKU180: + constraint: null + cost: 217 + init_stock: 165 + max_stock: 1000 + price: 297 + sale_gamma: 55 + service_level: 0.95 + SKU181: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 300 + max_stock: 1000 + price: 214 + sale_gamma: 60 + service_level: 0.95 + SKU182: + constraint: null + cost: 437 + init_stock: 693 + max_stock: 1000 + price: 764 + sale_gamma: 99 + service_level: 0.95 + SKU183: + constraint: null + cost: 145 + init_stock: 56 + max_stock: 1000 + price: 178 + sale_gamma: 8 + service_level: 0.95 + SKU184: + constraint: null + cost: 73 + init_stock: 72 + max_stock: 1000 + price: 100 + sale_gamma: 24 + service_level: 0.95 + SKU185: + constraint: null + cost: 10 + init_stock: 360 + max_stock: 1000 + price: 14 + sale_gamma: 90 + service_level: 0.95 + SKU186: + constraint: null + cost: 359 + init_stock: 66 + max_stock: 1000 + price: 649 + sale_gamma: 22 + service_level: 0.95 + SKU187: + constraint: G(low_profit -> low_stock_constraint) + cost: 177 + init_stock: 120 + max_stock: 1000 + price: 249 + sale_gamma: 30 + service_level: 0.95 + SKU188: + constraint: null + cost: 391 + init_stock: 348 + max_stock: 1000 + price: 586 + sale_gamma: 87 + service_level: 0.95 + SKU189: + constraint: G(low_profit -> low_stock_constraint) + cost: 358 + init_stock: 210 + max_stock: 1000 + price: 400 + sale_gamma: 35 + service_level: 0.95 + SKU19: + constraint: G(stock_constraint) + cost: 477 + init_stock: 364 + max_stock: 1000 + price: 791 + sale_gamma: 91 + service_level: 0.95 + SKU190: + constraint: null + cost: 113 + init_stock: 102 + max_stock: 1000 + price: 153 + sale_gamma: 17 + service_level: 0.95 + SKU191: + constraint: G(stock_constraint) + cost: 473 + init_stock: 324 + max_stock: 1000 + price: 638 + sale_gamma: 54 + service_level: 0.95 + SKU192: + constraint: null + cost: 415 + init_stock: 244 + max_stock: 1000 + price: 539 + sale_gamma: 61 + service_level: 0.95 + SKU193: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 207 + init_stock: 120 + max_stock: 1000 + price: 353 + sale_gamma: 30 + service_level: 0.95 + SKU194: + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 30 + max_stock: 1000 + price: 803 + sale_gamma: 5 + service_level: 0.95 + SKU195: + constraint: G(low_profit -> low_stock_constraint) + cost: 218 + init_stock: 93 + max_stock: 1000 + price: 248 + sale_gamma: 31 + service_level: 0.95 + SKU196: + constraint: null + cost: 49 + init_stock: 544 + max_stock: 1000 + price: 62 + sale_gamma: 68 + service_level: 0.95 + SKU197: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 303 + init_stock: 285 + max_stock: 1000 + price: 509 + sale_gamma: 57 + service_level: 0.95 + SKU198: + constraint: G(low_profit -> low_stock_constraint) + cost: 169 + init_stock: 378 + max_stock: 1000 + price: 307 + sale_gamma: 54 + service_level: 0.95 + SKU199: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 138 + max_stock: 1000 + price: 794 + sale_gamma: 23 + service_level: 0.95 + SKU2: + constraint: null + cost: 331 + init_stock: 350 + max_stock: 1000 + price: 499 + sale_gamma: 70 + service_level: 0.95 + SKU20: + constraint: G(stock_constraint) + cost: 335 + init_stock: 200 + max_stock: 1000 + price: 649 + sale_gamma: 25 + service_level: 0.95 + SKU200: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 65 + init_stock: 150 + max_stock: 1000 + price: 85 + sale_gamma: 25 + service_level: 0.95 + SKU201: + constraint: G(stock_constraint) + cost: 104 + init_stock: 354 + max_stock: 1000 + price: 161 + sale_gamma: 59 + service_level: 0.95 + SKU202: + constraint: null + cost: 142 + init_stock: 365 + max_stock: 1000 + price: 222 + sale_gamma: 73 + service_level: 0.95 + SKU203: + constraint: null + cost: 440 + init_stock: 328 + max_stock: 1000 + price: 677 + sale_gamma: 82 + service_level: 0.95 + SKU204: + constraint: null + cost: 489 + init_stock: 188 + max_stock: 1000 + price: 831 + sale_gamma: 47 + service_level: 0.95 + SKU205: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 130 + init_stock: 500 + max_stock: 1000 + price: 175 + sale_gamma: 100 + service_level: 0.95 + SKU206: + constraint: null + cost: 335 + init_stock: 55 + max_stock: 1000 + price: 552 + sale_gamma: 11 + service_level: 0.95 + SKU207: + constraint: G(low_profit -> low_stock_constraint) + cost: 140 + init_stock: 240 + max_stock: 1000 + price: 170 + sale_gamma: 80 + service_level: 0.95 + SKU208: + constraint: null + cost: 491 + init_stock: 308 + max_stock: 1000 + price: 927 + sale_gamma: 77 + service_level: 0.95 + SKU209: + constraint: G(stock_constraint) + cost: 179 + init_stock: 120 + max_stock: 1000 + price: 322 + sale_gamma: 20 + service_level: 0.95 + SKU21: + constraint: null + cost: 123 + init_stock: 400 + max_stock: 1000 + price: 225 + sale_gamma: 100 + service_level: 0.95 + SKU210: + constraint: null + cost: 404 + init_stock: 345 + max_stock: 1000 + price: 468 + sale_gamma: 69 + service_level: 0.95 + SKU211: + constraint: null + cost: 174 + init_stock: 364 + max_stock: 1000 + price: 226 + sale_gamma: 91 + service_level: 0.95 + SKU212: + constraint: null + cost: 405 + init_stock: 632 + max_stock: 1000 + price: 534 + sale_gamma: 79 + service_level: 0.95 + SKU213: + constraint: null + cost: 121 + init_stock: 192 + max_stock: 1000 + price: 229 + sale_gamma: 64 + service_level: 0.95 + SKU214: + constraint: G(stock_constraint) + cost: 101 + init_stock: 60 + max_stock: 1000 + price: 144 + sale_gamma: 10 + service_level: 0.95 + SKU215: + constraint: null + cost: 419 + init_stock: 94 + max_stock: 1000 + price: 469 + sale_gamma: 47 + service_level: 0.95 + SKU216: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 330 + init_stock: 92 + max_stock: 1000 + price: 432 + sale_gamma: 23 + service_level: 0.95 + SKU217: + constraint: null + cost: 284 + init_stock: 455 + max_stock: 1000 + price: 312 + sale_gamma: 65 + service_level: 0.95 + SKU218: + constraint: G(low_profit -> low_stock_constraint) + cost: 205 + init_stock: 354 + max_stock: 1000 + price: 397 + sale_gamma: 59 + service_level: 0.95 + SKU219: + constraint: G(stock_constraint) + cost: 92 + init_stock: 368 + max_stock: 1000 + price: 135 + sale_gamma: 46 + service_level: 0.95 + SKU22: + constraint: null + cost: 493 + init_stock: 264 + max_stock: 1000 + price: 986 + sale_gamma: 66 + service_level: 0.95 + SKU220: + constraint: null + cost: 387 + init_stock: 435 + max_stock: 1000 + price: 572 + sale_gamma: 87 + service_level: 0.95 + SKU221: + constraint: null + cost: 39 + init_stock: 468 + max_stock: 1000 + price: 48 + sale_gamma: 78 + service_level: 0.95 + SKU222: + constraint: G(low_profit -> low_stock_constraint) + cost: 115 + init_stock: 180 + max_stock: 1000 + price: 210 + sale_gamma: 36 + service_level: 0.95 + SKU223: + constraint: G(low_profit -> low_stock_constraint) + cost: 196 + init_stock: 36 + max_stock: 1000 + price: 286 + sale_gamma: 12 + service_level: 0.95 + SKU224: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 387 + init_stock: 25 + max_stock: 1000 + price: 661 + sale_gamma: 5 + service_level: 0.95 + SKU225: + constraint: null + cost: 164 + init_stock: 116 + max_stock: 1000 + price: 216 + sale_gamma: 29 + service_level: 0.95 + SKU226: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 206 + init_stock: 65 + max_stock: 1000 + price: 350 + sale_gamma: 13 + service_level: 0.95 + SKU227: + constraint: G(low_profit -> low_stock_constraint) + cost: 479 + init_stock: 144 + max_stock: 1000 + price: 895 + sale_gamma: 24 + service_level: 0.95 + SKU228: + constraint: null + cost: 14 + init_stock: 180 + max_stock: 1000 + price: 21 + sale_gamma: 90 + service_level: 0.95 + SKU229: + constraint: null + cost: 472 + init_stock: 176 + max_stock: 1000 + price: 708 + sale_gamma: 44 + service_level: 0.95 + SKU23: + constraint: null + cost: 387 + init_stock: 210 + max_stock: 1000 + price: 700 + sale_gamma: 42 + service_level: 0.95 + SKU230: + constraint: null + cost: 241 + init_stock: 138 + max_stock: 1000 + price: 436 + sale_gamma: 23 + service_level: 0.95 + SKU231: + constraint: G(stock_constraint) + cost: 48 + init_stock: 486 + max_stock: 1000 + price: 96 + sale_gamma: 81 + service_level: 0.95 + SKU232: + constraint: G(low_profit -> low_stock_constraint) + cost: 224 + init_stock: 480 + max_stock: 1000 + price: 338 + sale_gamma: 96 + service_level: 0.95 + SKU233: + constraint: G(low_profit -> low_stock_constraint) + cost: 360 + init_stock: 300 + max_stock: 1000 + price: 428 + sale_gamma: 75 + service_level: 0.95 + SKU234: + constraint: null + cost: 287 + init_stock: 25 + max_stock: 1000 + price: 387 + sale_gamma: 5 + service_level: 0.95 + SKU235: + constraint: G(low_profit -> low_stock_constraint) + cost: 24 + init_stock: 228 + max_stock: 1000 + price: 32 + sale_gamma: 57 + service_level: 0.95 + SKU236: + constraint: G(low_profit -> low_stock_constraint) + cost: 155 + init_stock: 165 + max_stock: 1000 + price: 289 + sale_gamma: 55 + service_level: 0.95 + SKU237: + constraint: null + cost: 433 + init_stock: 270 + max_stock: 1000 + price: 779 + sale_gamma: 45 + service_level: 0.95 + SKU238: + constraint: G(stock_constraint) + cost: 64 + init_stock: 264 + max_stock: 1000 + price: 112 + sale_gamma: 66 + service_level: 0.95 + SKU239: + constraint: null + cost: 103 + init_stock: 490 + max_stock: 1000 + price: 139 + sale_gamma: 98 + service_level: 0.95 + SKU24: + constraint: null + cost: 97 + init_stock: 329 + max_stock: 1000 + price: 114 + sale_gamma: 47 + service_level: 0.95 + SKU240: + constraint: null + cost: 373 + init_stock: 188 + max_stock: 1000 + price: 738 + sale_gamma: 47 + service_level: 0.95 + SKU241: + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 213 + max_stock: 1000 + price: 860 + sale_gamma: 71 + service_level: 0.95 + SKU242: + constraint: null + cost: 17 + init_stock: 88 + max_stock: 1000 + price: 29 + sale_gamma: 44 + service_level: 0.95 + SKU243: + constraint: null + cost: 352 + init_stock: 84 + max_stock: 1000 + price: 394 + sale_gamma: 14 + service_level: 0.95 + SKU244: + constraint: null + cost: 174 + init_stock: 410 + max_stock: 1000 + price: 226 + sale_gamma: 82 + service_level: 0.95 + SKU245: + constraint: G(low_profit -> low_stock_constraint) + cost: 404 + init_stock: 198 + max_stock: 1000 + price: 525 + sale_gamma: 66 + service_level: 0.95 + SKU246: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 300 + init_stock: 210 + max_stock: 1000 + price: 474 + sale_gamma: 30 + service_level: 0.95 + SKU247: + constraint: null + cost: 386 + init_stock: 210 + max_stock: 1000 + price: 497 + sale_gamma: 35 + service_level: 0.95 + SKU248: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 355 + init_stock: 174 + max_stock: 1000 + price: 539 + sale_gamma: 29 + service_level: 0.95 + SKU249: + constraint: null + cost: 356 + init_stock: 100 + max_stock: 1000 + price: 544 + sale_gamma: 25 + service_level: 0.95 + SKU25: + constraint: G(stock_constraint) + cost: 161 + init_stock: 35 + max_stock: 1000 + price: 249 + sale_gamma: 5 + service_level: 0.95 + SKU250: + constraint: null + cost: 127 + init_stock: 324 + max_stock: 1000 + price: 186 + sale_gamma: 54 + service_level: 0.95 + SKU251: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 344 + init_stock: 244 + max_stock: 1000 + price: 543 + sale_gamma: 61 + service_level: 0.95 + SKU252: + constraint: null + cost: 181 + init_stock: 415 + max_stock: 1000 + price: 334 + sale_gamma: 83 + service_level: 0.95 + SKU253: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 448 + init_stock: 32 + max_stock: 1000 + price: 864 + sale_gamma: 16 + service_level: 0.95 + SKU254: + constraint: null + cost: 484 + init_stock: 184 + max_stock: 1000 + price: 696 + sale_gamma: 46 + service_level: 0.95 + SKU255: + constraint: null + cost: 290 + init_stock: 335 + max_stock: 1000 + price: 568 + sale_gamma: 67 + service_level: 0.95 + SKU256: + constraint: null + cost: 91 + init_stock: 360 + max_stock: 1000 + price: 167 + sale_gamma: 72 + service_level: 0.95 + SKU257: + constraint: null + cost: 348 + init_stock: 171 + max_stock: 1000 + price: 497 + sale_gamma: 57 + service_level: 0.95 + SKU258: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 258 + max_stock: 1000 + price: 689 + sale_gamma: 43 + service_level: 0.95 + SKU259: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 207 + max_stock: 1000 + price: 559 + sale_gamma: 69 + service_level: 0.95 + SKU26: + constraint: null + cost: 229 + init_stock: 126 + max_stock: 1000 + price: 357 + sale_gamma: 63 + service_level: 0.95 + SKU260: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 364 + max_stock: 1000 + price: 866 + sale_gamma: 52 + service_level: 0.95 + SKU261: + constraint: null + cost: 368 + init_stock: 66 + max_stock: 1000 + price: 688 + sale_gamma: 22 + service_level: 0.95 + SKU262: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 332 + init_stock: 390 + max_stock: 1000 + price: 385 + sale_gamma: 78 + service_level: 0.95 + SKU263: + constraint: G(stock_constraint) + cost: 189 + init_stock: 444 + max_stock: 1000 + price: 372 + sale_gamma: 74 + service_level: 0.95 + SKU264: + constraint: null + cost: 361 + init_stock: 158 + max_stock: 1000 + price: 566 + sale_gamma: 79 + service_level: 0.95 + SKU265: + constraint: null + cost: 286 + init_stock: 236 + max_stock: 1000 + price: 434 + sale_gamma: 59 + service_level: 0.95 + SKU266: + constraint: null + cost: 128 + init_stock: 282 + max_stock: 1000 + price: 172 + sale_gamma: 47 + service_level: 0.95 + SKU267: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 77 + init_stock: 320 + max_stock: 1000 + price: 130 + sale_gamma: 80 + service_level: 0.95 + SKU268: + constraint: null + cost: 221 + init_stock: 356 + max_stock: 1000 + price: 362 + sale_gamma: 89 + service_level: 0.95 + SKU269: + constraint: G(low_profit -> low_stock_constraint) + cost: 126 + init_stock: 132 + max_stock: 1000 + price: 175 + sale_gamma: 44 + service_level: 0.95 + SKU27: + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 448 + max_stock: 1000 + price: 728 + sale_gamma: 56 + service_level: 0.95 + SKU270: + constraint: null + cost: 182 + init_stock: 370 + max_stock: 1000 + price: 263 + sale_gamma: 74 + service_level: 0.95 + SKU271: + constraint: G(stock_constraint) + cost: 230 + init_stock: 54 + max_stock: 1000 + price: 266 + sale_gamma: 18 + service_level: 0.95 + SKU272: + constraint: null + cost: 366 + init_stock: 51 + max_stock: 1000 + price: 625 + sale_gamma: 17 + service_level: 0.95 + SKU273: + constraint: null + cost: 421 + init_stock: 72 + max_stock: 1000 + price: 614 + sale_gamma: 18 + service_level: 0.95 + SKU274: + constraint: null + cost: 29 + init_stock: 308 + max_stock: 1000 + price: 42 + sale_gamma: 77 + service_level: 0.95 + SKU275: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 50 + init_stock: 144 + max_stock: 1000 + price: 76 + sale_gamma: 48 + service_level: 0.95 + SKU276: + constraint: G(stock_constraint) + cost: 163 + init_stock: 378 + max_stock: 1000 + price: 299 + sale_gamma: 54 + service_level: 0.95 + SKU277: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 82 + max_stock: 1000 + price: 507 + sale_gamma: 41 + service_level: 0.95 + SKU278: + constraint: null + cost: 105 + init_stock: 84 + max_stock: 1000 + price: 140 + sale_gamma: 12 + service_level: 0.95 + SKU279: + constraint: G(stock_constraint) + cost: 51 + init_stock: 273 + max_stock: 1000 + price: 61 + sale_gamma: 39 + service_level: 0.95 + SKU28: + constraint: G(stock_constraint) + cost: 208 + init_stock: 235 + max_stock: 1000 + price: 295 + sale_gamma: 47 + service_level: 0.95 + SKU280: + constraint: G(low_profit -> low_stock_constraint) + cost: 295 + init_stock: 198 + max_stock: 1000 + price: 436 + sale_gamma: 33 + service_level: 0.95 + SKU281: + constraint: null + cost: 395 + init_stock: 375 + max_stock: 1000 + price: 592 + sale_gamma: 75 + service_level: 0.95 + SKU282: + constraint: null + cost: 63 + init_stock: 184 + max_stock: 1000 + price: 112 + sale_gamma: 46 + service_level: 0.95 + SKU283: + constraint: null + cost: 392 + init_stock: 736 + max_stock: 1000 + price: 490 + sale_gamma: 92 + service_level: 0.95 + SKU284: + constraint: G(stock_constraint) + cost: 344 + init_stock: 268 + max_stock: 1000 + price: 643 + sale_gamma: 67 + service_level: 0.95 + SKU285: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 133 + init_stock: 546 + max_stock: 1000 + price: 148 + sale_gamma: 91 + service_level: 0.95 + SKU286: + constraint: null + cost: 337 + init_stock: 156 + max_stock: 1000 + price: 626 + sale_gamma: 39 + service_level: 0.95 + SKU287: + constraint: null + cost: 375 + init_stock: 224 + max_stock: 1000 + price: 660 + sale_gamma: 56 + service_level: 0.95 + SKU288: + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 190 + max_stock: 1000 + price: 352 + sale_gamma: 38 + service_level: 0.95 + SKU289: + constraint: G(stock_constraint) + cost: 67 + init_stock: 62 + max_stock: 1000 + price: 91 + sale_gamma: 31 + service_level: 0.95 + SKU29: + constraint: null + cost: 245 + init_stock: 174 + max_stock: 1000 + price: 475 + sale_gamma: 58 + service_level: 0.95 + SKU290: + constraint: null + cost: 438 + init_stock: 268 + max_stock: 1000 + price: 779 + sale_gamma: 67 + service_level: 0.95 + SKU291: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 122 + max_stock: 1000 + price: 126 + sale_gamma: 61 + service_level: 0.95 + SKU292: + constraint: null + cost: 186 + init_stock: 15 + max_stock: 1000 + price: 344 + sale_gamma: 5 + service_level: 0.95 + SKU293: + constraint: G(stock_constraint) + cost: 162 + init_stock: 385 + max_stock: 1000 + price: 288 + sale_gamma: 55 + service_level: 0.95 + SKU294: + constraint: null + cost: 409 + init_stock: 45 + max_stock: 1000 + price: 605 + sale_gamma: 9 + service_level: 0.95 + SKU295: + constraint: null + cost: 435 + init_stock: 651 + max_stock: 1000 + price: 674 + sale_gamma: 93 + service_level: 0.95 + SKU296: + constraint: G(low_profit -> low_stock_constraint) + cost: 370 + init_stock: 552 + max_stock: 1000 + price: 580 + sale_gamma: 92 + service_level: 0.95 + SKU297: + constraint: null + cost: 298 + init_stock: 228 + max_stock: 1000 + price: 333 + sale_gamma: 38 + service_level: 0.95 + SKU298: + constraint: null + cost: 286 + init_stock: 140 + max_stock: 1000 + price: 500 + sale_gamma: 35 + service_level: 0.95 + SKU299: + constraint: G(stock_constraint) + cost: 367 + init_stock: 255 + max_stock: 1000 + price: 451 + sale_gamma: 51 + service_level: 0.95 + SKU3: + constraint: G(stock_constraint) + cost: 405 + init_stock: 48 + max_stock: 1000 + price: 733 + sale_gamma: 12 + service_level: 0.95 + SKU30: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 359 + init_stock: 276 + max_stock: 1000 + price: 581 + sale_gamma: 69 + service_level: 0.95 + SKU300: + constraint: G(low_profit -> low_stock_constraint) + cost: 376 + init_stock: 290 + max_stock: 1000 + price: 428 + sale_gamma: 58 + service_level: 0.95 + SKU301: + constraint: null + cost: 433 + init_stock: 581 + max_stock: 1000 + price: 857 + sale_gamma: 83 + service_level: 0.95 + SKU302: + constraint: null + cost: 184 + init_stock: 44 + max_stock: 1000 + price: 322 + sale_gamma: 11 + service_level: 0.95 + SKU303: + constraint: null + cost: 246 + init_stock: 188 + max_stock: 1000 + price: 487 + sale_gamma: 94 + service_level: 0.95 + SKU304: + constraint: G(low_profit -> low_stock_constraint) + cost: 419 + init_stock: 138 + max_stock: 1000 + price: 632 + sale_gamma: 23 + service_level: 0.95 + SKU305: + constraint: null + cost: 495 + init_stock: 500 + max_stock: 1000 + price: 772 + sale_gamma: 100 + service_level: 0.95 + SKU306: + constraint: null + cost: 479 + init_stock: 126 + max_stock: 1000 + price: 852 + sale_gamma: 42 + service_level: 0.95 + SKU307: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 210 + init_stock: 156 + max_stock: 1000 + price: 371 + sale_gamma: 78 + service_level: 0.95 + SKU308: + constraint: null + cost: 208 + init_stock: 25 + max_stock: 1000 + price: 262 + sale_gamma: 5 + service_level: 0.95 + SKU309: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 101 + init_stock: 460 + max_stock: 1000 + price: 156 + sale_gamma: 92 + service_level: 0.95 + SKU31: + constraint: null + cost: 69 + init_stock: 280 + max_stock: 1000 + price: 120 + sale_gamma: 56 + service_level: 0.95 + SKU310: + constraint: null + cost: 234 + init_stock: 352 + max_stock: 1000 + price: 294 + sale_gamma: 44 + service_level: 0.95 + SKU311: + constraint: null + cost: 306 + init_stock: 400 + max_stock: 1000 + price: 437 + sale_gamma: 80 + service_level: 0.95 + SKU312: + constraint: G(stock_constraint) + cost: 291 + init_stock: 75 + max_stock: 1000 + price: 392 + sale_gamma: 25 + service_level: 0.95 + SKU313: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 324 + init_stock: 190 + max_stock: 1000 + price: 492 + sale_gamma: 38 + service_level: 0.95 + SKU314: + constraint: G(stock_constraint) + cost: 404 + init_stock: 116 + max_stock: 1000 + price: 456 + sale_gamma: 29 + service_level: 0.95 + SKU315: + constraint: G(stock_constraint) + cost: 471 + init_stock: 504 + max_stock: 1000 + price: 885 + sale_gamma: 84 + service_level: 0.95 + SKU316: + constraint: null + cost: 202 + init_stock: 90 + max_stock: 1000 + price: 282 + sale_gamma: 18 + service_level: 0.95 + SKU317: + constraint: null + cost: 216 + init_stock: 168 + max_stock: 1000 + price: 352 + sale_gamma: 24 + service_level: 0.95 + SKU318: + constraint: null + cost: 278 + init_stock: 255 + max_stock: 1000 + price: 350 + sale_gamma: 85 + service_level: 0.95 + SKU319: + constraint: null + cost: 257 + init_stock: 371 + max_stock: 1000 + price: 454 + sale_gamma: 53 + service_level: 0.95 + SKU32: + constraint: null + cost: 336 + init_stock: 110 + max_stock: 1000 + price: 581 + sale_gamma: 22 + service_level: 0.95 + SKU320: + constraint: null + cost: 196 + init_stock: 156 + max_stock: 1000 + price: 258 + sale_gamma: 39 + service_level: 0.95 + SKU321: + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 96 + max_stock: 1000 + price: 105 + sale_gamma: 16 + service_level: 0.95 + SKU322: + constraint: null + cost: 240 + init_stock: 600 + max_stock: 1000 + price: 453 + sale_gamma: 100 + service_level: 0.95 + SKU323: + constraint: G(stock_constraint) + cost: 66 + init_stock: 195 + max_stock: 1000 + price: 74 + sale_gamma: 39 + service_level: 0.95 + SKU324: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 442 + init_stock: 465 + max_stock: 1000 + price: 804 + sale_gamma: 93 + service_level: 0.95 + SKU325: + constraint: null + cost: 453 + init_stock: 345 + max_stock: 1000 + price: 579 + sale_gamma: 69 + service_level: 0.95 + SKU326: + constraint: null + cost: 345 + init_stock: 72 + max_stock: 1000 + price: 538 + sale_gamma: 24 + service_level: 0.95 + SKU327: + constraint: G(low_profit -> low_stock_constraint) + cost: 457 + init_stock: 84 + max_stock: 1000 + price: 749 + sale_gamma: 14 + service_level: 0.95 + SKU328: + constraint: null + cost: 390 + init_stock: 90 + max_stock: 1000 + price: 487 + sale_gamma: 45 + service_level: 0.95 + SKU329: + constraint: null + cost: 67 + init_stock: 84 + max_stock: 1000 + price: 126 + sale_gamma: 42 + service_level: 0.95 + SKU33: + constraint: G(low_profit -> low_stock_constraint) + cost: 416 + init_stock: 552 + max_stock: 1000 + price: 465 + sale_gamma: 92 + service_level: 0.95 + SKU330: + constraint: null + cost: 306 + init_stock: 273 + max_stock: 1000 + price: 385 + sale_gamma: 39 + service_level: 0.95 + SKU331: + constraint: G(low_profit -> low_stock_constraint) + cost: 138 + init_stock: 164 + max_stock: 1000 + price: 180 + sale_gamma: 41 + service_level: 0.95 + SKU332: + constraint: null + cost: 390 + init_stock: 288 + max_stock: 1000 + price: 670 + sale_gamma: 96 + service_level: 0.95 + SKU333: + constraint: G(stock_constraint) + cost: 485 + init_stock: 318 + max_stock: 1000 + price: 800 + sale_gamma: 53 + service_level: 0.95 + SKU334: + constraint: null + cost: 183 + init_stock: 114 + max_stock: 1000 + price: 245 + sale_gamma: 57 + service_level: 0.95 + SKU335: + constraint: null + cost: 80 + init_stock: 243 + max_stock: 1000 + price: 141 + sale_gamma: 81 + service_level: 0.95 + SKU336: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 56 + max_stock: 1000 + price: 565 + sale_gamma: 28 + service_level: 0.95 + SKU337: + constraint: null + cost: 245 + init_stock: 174 + max_stock: 1000 + price: 394 + sale_gamma: 29 + service_level: 0.95 + SKU338: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 87 + init_stock: 324 + max_stock: 1000 + price: 150 + sale_gamma: 81 + service_level: 0.95 + SKU339: + constraint: G(stock_constraint) + cost: 265 + init_stock: 441 + max_stock: 1000 + price: 368 + sale_gamma: 63 + service_level: 0.95 + SKU34: + constraint: null + cost: 95 + init_stock: 91 + max_stock: 1000 + price: 135 + sale_gamma: 13 + service_level: 0.95 + SKU340: + constraint: null + cost: 480 + init_stock: 435 + max_stock: 1000 + price: 633 + sale_gamma: 87 + service_level: 0.95 + SKU341: + constraint: G(low_profit -> low_stock_constraint) + cost: 462 + init_stock: 280 + max_stock: 1000 + price: 924 + sale_gamma: 70 + service_level: 0.95 + SKU342: + constraint: null + cost: 144 + init_stock: 72 + max_stock: 1000 + price: 216 + sale_gamma: 9 + service_level: 0.95 + SKU343: + constraint: null + cost: 310 + init_stock: 90 + max_stock: 1000 + price: 589 + sale_gamma: 15 + service_level: 0.95 + SKU344: + constraint: null + cost: 357 + init_stock: 348 + max_stock: 1000 + price: 442 + sale_gamma: 87 + service_level: 0.95 + SKU345: + constraint: null + cost: 442 + init_stock: 267 + max_stock: 1000 + price: 857 + sale_gamma: 89 + service_level: 0.95 + SKU346: + constraint: G(stock_constraint) + cost: 350 + init_stock: 336 + max_stock: 1000 + price: 549 + sale_gamma: 42 + service_level: 0.95 + SKU347: + constraint: G(low_profit -> low_stock_constraint) + cost: 497 + init_stock: 328 + max_stock: 1000 + price: 810 + sale_gamma: 82 + service_level: 0.95 + SKU348: + constraint: G(stock_constraint) + cost: 147 + init_stock: 100 + max_stock: 1000 + price: 277 + sale_gamma: 20 + service_level: 0.95 + SKU349: + constraint: null + cost: 67 + init_stock: 335 + max_stock: 1000 + price: 116 + sale_gamma: 67 + service_level: 0.95 + SKU35: + constraint: null + cost: 267 + init_stock: 430 + max_stock: 1000 + price: 373 + sale_gamma: 86 + service_level: 0.95 + SKU350: + constraint: G(stock_constraint) + cost: 279 + init_stock: 552 + max_stock: 1000 + price: 460 + sale_gamma: 92 + service_level: 0.95 + SKU351: + constraint: null + cost: 155 + init_stock: 335 + max_stock: 1000 + price: 294 + sale_gamma: 67 + service_level: 0.95 + SKU352: + constraint: null + cost: 71 + init_stock: 54 + max_stock: 1000 + price: 100 + sale_gamma: 18 + service_level: 0.95 + SKU353: + constraint: G(stock_constraint) + cost: 253 + init_stock: 465 + max_stock: 1000 + price: 437 + sale_gamma: 93 + service_level: 0.95 + SKU354: + constraint: G(low_profit -> low_stock_constraint) + cost: 396 + init_stock: 395 + max_stock: 1000 + price: 566 + sale_gamma: 79 + service_level: 0.95 + SKU355: + constraint: G(stock_constraint) + cost: 63 + init_stock: 30 + max_stock: 1000 + price: 74 + sale_gamma: 10 + service_level: 0.95 + SKU356: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 348 + init_stock: 87 + max_stock: 1000 + price: 441 + sale_gamma: 29 + service_level: 0.95 + SKU357: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 499 + init_stock: 460 + max_stock: 1000 + price: 868 + sale_gamma: 92 + service_level: 0.95 + SKU358: + constraint: null + cost: 269 + init_stock: 414 + max_stock: 1000 + price: 408 + sale_gamma: 69 + service_level: 0.95 + SKU359: + constraint: G(low_profit -> low_stock_constraint) + cost: 82 + init_stock: 600 + max_stock: 1000 + price: 110 + sale_gamma: 75 + service_level: 0.95 + SKU36: + constraint: null + cost: 105 + init_stock: 30 + max_stock: 1000 + price: 165 + sale_gamma: 10 + service_level: 0.95 + SKU360: + constraint: G(low_profit -> low_stock_constraint) + cost: 484 + init_stock: 75 + max_stock: 1000 + price: 682 + sale_gamma: 25 + service_level: 0.95 + SKU361: + constraint: null + cost: 163 + init_stock: 360 + max_stock: 1000 + price: 288 + sale_gamma: 90 + service_level: 0.95 + SKU362: + constraint: null + cost: 464 + init_stock: 435 + max_stock: 1000 + price: 867 + sale_gamma: 87 + service_level: 0.95 + SKU363: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 390 + max_stock: 1000 + price: 93 + sale_gamma: 78 + service_level: 0.95 + SKU364: + constraint: G(stock_constraint) + cost: 387 + init_stock: 66 + max_stock: 1000 + price: 472 + sale_gamma: 33 + service_level: 0.95 + SKU365: + constraint: null + cost: 158 + init_stock: 581 + max_stock: 1000 + price: 241 + sale_gamma: 83 + service_level: 0.95 + SKU366: + constraint: null + cost: 444 + init_stock: 344 + max_stock: 1000 + price: 639 + sale_gamma: 86 + service_level: 0.95 + SKU367: + constraint: G(low_profit -> low_stock_constraint) + cost: 272 + init_stock: 322 + max_stock: 1000 + price: 304 + sale_gamma: 46 + service_level: 0.95 + SKU368: + constraint: G(stock_constraint) + cost: 472 + init_stock: 198 + max_stock: 1000 + price: 613 + sale_gamma: 33 + service_level: 0.95 + SKU369: + constraint: null + cost: 96 + init_stock: 375 + max_stock: 1000 + price: 155 + sale_gamma: 75 + service_level: 0.95 + SKU37: + constraint: G(low_profit -> low_stock_constraint) + cost: 66 + init_stock: 177 + max_stock: 1000 + price: 110 + sale_gamma: 59 + service_level: 0.95 + SKU370: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 112 + init_stock: 90 + max_stock: 1000 + price: 141 + sale_gamma: 15 + service_level: 0.95 + SKU371: + constraint: null + cost: 328 + init_stock: 25 + max_stock: 1000 + price: 511 + sale_gamma: 5 + service_level: 0.95 + SKU372: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 67 + init_stock: 192 + max_stock: 1000 + price: 134 + sale_gamma: 32 + service_level: 0.95 + SKU373: + constraint: null + cost: 58 + init_stock: 268 + max_stock: 1000 + price: 91 + sale_gamma: 67 + service_level: 0.95 + SKU374: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 38 + init_stock: 216 + max_stock: 1000 + price: 73 + sale_gamma: 54 + service_level: 0.95 + SKU375: + constraint: G(low_profit -> low_stock_constraint) + cost: 283 + init_stock: 432 + max_stock: 1000 + price: 416 + sale_gamma: 72 + service_level: 0.95 + SKU376: + constraint: null + cost: 156 + init_stock: 164 + max_stock: 1000 + price: 291 + sale_gamma: 82 + service_level: 0.95 + SKU377: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 78 + init_stock: 536 + max_stock: 1000 + price: 134 + sale_gamma: 67 + service_level: 0.95 + SKU378: + constraint: G(low_profit -> low_stock_constraint) + cost: 424 + init_stock: 175 + max_stock: 1000 + price: 546 + sale_gamma: 35 + service_level: 0.95 + SKU379: + constraint: null + cost: 11 + init_stock: 196 + max_stock: 1000 + price: 20 + sale_gamma: 49 + service_level: 0.95 + SKU38: + constraint: null + cost: 344 + init_stock: 258 + max_stock: 1000 + price: 567 + sale_gamma: 43 + service_level: 0.95 + SKU380: + constraint: null + cost: 393 + init_stock: 212 + max_stock: 1000 + price: 605 + sale_gamma: 53 + service_level: 0.95 + SKU381: + constraint: null + cost: 476 + init_stock: 18 + max_stock: 1000 + price: 609 + sale_gamma: 6 + service_level: 0.95 + SKU382: + constraint: null + cost: 125 + init_stock: 426 + max_stock: 1000 + price: 177 + sale_gamma: 71 + service_level: 0.95 + SKU383: + constraint: null + cost: 304 + init_stock: 368 + max_stock: 1000 + price: 425 + sale_gamma: 92 + service_level: 0.95 + SKU384: + constraint: null + cost: 320 + init_stock: 54 + max_stock: 1000 + price: 460 + sale_gamma: 9 + service_level: 0.95 + SKU385: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 120 + init_stock: 91 + max_stock: 1000 + price: 184 + sale_gamma: 13 + service_level: 0.95 + SKU386: + constraint: G(stock_constraint) + cost: 295 + init_stock: 124 + max_stock: 1000 + price: 536 + sale_gamma: 31 + service_level: 0.95 + SKU387: + constraint: null + cost: 112 + init_stock: 582 + max_stock: 1000 + price: 154 + sale_gamma: 97 + service_level: 0.95 + SKU388: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 405 + init_stock: 264 + max_stock: 1000 + price: 635 + sale_gamma: 44 + service_level: 0.95 + SKU389: + constraint: G(stock_constraint) + cost: 376 + init_stock: 490 + max_stock: 1000 + price: 699 + sale_gamma: 70 + service_level: 0.95 + SKU39: + constraint: G(low_profit -> low_stock_constraint) + cost: 25 + init_stock: 204 + max_stock: 1000 + price: 45 + sale_gamma: 51 + service_level: 0.95 + SKU390: + constraint: null + cost: 144 + init_stock: 156 + max_stock: 1000 + price: 223 + sale_gamma: 39 + service_level: 0.95 + SKU391: + constraint: G(stock_constraint) + cost: 233 + init_stock: 402 + max_stock: 1000 + price: 403 + sale_gamma: 67 + service_level: 0.95 + SKU392: + constraint: null + cost: 163 + init_stock: 370 + max_stock: 1000 + price: 205 + sale_gamma: 74 + service_level: 0.95 + SKU393: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 487 + init_stock: 402 + max_stock: 1000 + price: 701 + sale_gamma: 67 + service_level: 0.95 + SKU394: + constraint: null + cost: 154 + init_stock: 318 + max_stock: 1000 + price: 252 + sale_gamma: 53 + service_level: 0.95 + SKU395: + constraint: null + cost: 488 + init_stock: 198 + max_stock: 1000 + price: 854 + sale_gamma: 33 + service_level: 0.95 + SKU396: + constraint: null + cost: 333 + init_stock: 427 + max_stock: 1000 + price: 469 + sale_gamma: 61 + service_level: 0.95 + SKU397: + constraint: null + cost: 460 + init_stock: 153 + max_stock: 1000 + price: 791 + sale_gamma: 51 + service_level: 0.95 + SKU398: + constraint: G(stock_constraint) + cost: 234 + init_stock: 174 + max_stock: 1000 + price: 414 + sale_gamma: 58 + service_level: 0.95 + SKU399: + constraint: G(stock_constraint) + cost: 376 + init_stock: 148 + max_stock: 1000 + price: 612 + sale_gamma: 37 + service_level: 0.95 + SKU4: + constraint: G(low_profit -> low_stock_constraint) + cost: 439 + init_stock: 21 + max_stock: 1000 + price: 798 + sale_gamma: 7 + service_level: 0.95 + SKU40: + constraint: null + cost: 490 + init_stock: 594 + max_stock: 1000 + price: 563 + sale_gamma: 99 + service_level: 0.95 + SKU400: + constraint: G(low_profit -> low_stock_constraint) + cost: 445 + init_stock: 420 + max_stock: 1000 + price: 729 + sale_gamma: 84 + service_level: 0.95 + SKU401: + constraint: G(low_profit -> low_stock_constraint) + cost: 410 + init_stock: 312 + max_stock: 1000 + price: 774 + sale_gamma: 78 + service_level: 0.95 + SKU402: + constraint: G(low_profit -> low_stock_constraint) + cost: 86 + init_stock: 35 + max_stock: 1000 + price: 153 + sale_gamma: 7 + service_level: 0.95 + SKU403: + constraint: null + cost: 89 + init_stock: 594 + max_stock: 1000 + price: 133 + sale_gamma: 99 + service_level: 0.95 + SKU404: + constraint: null + cost: 287 + init_stock: 305 + max_stock: 1000 + price: 522 + sale_gamma: 61 + service_level: 0.95 + SKU405: + constraint: G(stock_constraint) + cost: 460 + init_stock: 114 + max_stock: 1000 + price: 584 + sale_gamma: 19 + service_level: 0.95 + SKU406: + constraint: null + cost: 327 + init_stock: 400 + max_stock: 1000 + price: 405 + sale_gamma: 100 + service_level: 0.95 + SKU407: + constraint: null + cost: 26 + init_stock: 184 + max_stock: 1000 + price: 41 + sale_gamma: 46 + service_level: 0.95 + SKU408: + constraint: null + cost: 444 + init_stock: 48 + max_stock: 1000 + price: 754 + sale_gamma: 8 + service_level: 0.95 + SKU409: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 257 + init_stock: 273 + max_stock: 1000 + price: 449 + sale_gamma: 91 + service_level: 0.95 + SKU41: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 141 + init_stock: 553 + max_stock: 1000 + price: 162 + sale_gamma: 79 + service_level: 0.95 + SKU410: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 70 + init_stock: 32 + max_stock: 1000 + price: 88 + sale_gamma: 16 + service_level: 0.95 + SKU411: + constraint: G(stock_constraint) + cost: 210 + init_stock: 380 + max_stock: 1000 + price: 405 + sale_gamma: 95 + service_level: 0.95 + SKU412: + constraint: null + cost: 286 + init_stock: 248 + max_stock: 1000 + price: 414 + sale_gamma: 62 + service_level: 0.95 + SKU413: + constraint: null + cost: 403 + init_stock: 581 + max_stock: 1000 + price: 801 + sale_gamma: 83 + service_level: 0.95 + SKU414: + constraint: G(stock_constraint) + cost: 165 + init_stock: 435 + max_stock: 1000 + price: 229 + sale_gamma: 87 + service_level: 0.95 + SKU415: + constraint: G(stock_constraint) + cost: 291 + init_stock: 184 + max_stock: 1000 + price: 372 + sale_gamma: 23 + service_level: 0.95 + SKU416: + constraint: null + cost: 228 + init_stock: 36 + max_stock: 1000 + price: 373 + sale_gamma: 9 + service_level: 0.95 + SKU417: + constraint: G(low_profit -> low_stock_constraint) + cost: 443 + init_stock: 288 + max_stock: 1000 + price: 872 + sale_gamma: 72 + service_level: 0.95 + SKU418: + constraint: null + cost: 458 + init_stock: 52 + max_stock: 1000 + price: 838 + sale_gamma: 13 + service_level: 0.95 + SKU419: + constraint: null + cost: 303 + init_stock: 712 + max_stock: 1000 + price: 448 + sale_gamma: 89 + service_level: 0.95 + SKU42: + constraint: null + cost: 462 + init_stock: 156 + max_stock: 1000 + price: 688 + sale_gamma: 26 + service_level: 0.95 + SKU420: + constraint: null + cost: 268 + init_stock: 84 + max_stock: 1000 + price: 506 + sale_gamma: 42 + service_level: 0.95 + SKU421: + constraint: G(stock_constraint) + cost: 214 + init_stock: 138 + max_stock: 1000 + price: 314 + sale_gamma: 46 + service_level: 0.95 + SKU422: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 52 + init_stock: 432 + max_stock: 1000 + price: 97 + sale_gamma: 54 + service_level: 0.95 + SKU423: + constraint: G(low_profit -> low_stock_constraint) + cost: 188 + init_stock: 396 + max_stock: 1000 + price: 265 + sale_gamma: 66 + service_level: 0.95 + SKU424: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 66 + max_stock: 1000 + price: 317 + sale_gamma: 11 + service_level: 0.95 + SKU425: + constraint: G(stock_constraint) + cost: 162 + init_stock: 72 + max_stock: 1000 + price: 277 + sale_gamma: 12 + service_level: 0.95 + SKU426: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 125 + init_stock: 588 + max_stock: 1000 + price: 246 + sale_gamma: 98 + service_level: 0.95 + SKU427: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 413 + init_stock: 282 + max_stock: 1000 + price: 710 + sale_gamma: 94 + service_level: 0.95 + SKU428: + constraint: G(stock_constraint) + cost: 391 + init_stock: 378 + max_stock: 1000 + price: 629 + sale_gamma: 63 + service_level: 0.95 + SKU429: + constraint: G(stock_constraint) + cost: 39 + init_stock: 246 + max_stock: 1000 + price: 73 + sale_gamma: 41 + service_level: 0.95 + SKU43: + constraint: G(stock_constraint) + cost: 217 + init_stock: 272 + max_stock: 1000 + price: 353 + sale_gamma: 68 + service_level: 0.95 + SKU430: + constraint: null + cost: 216 + init_stock: 511 + max_stock: 1000 + price: 313 + sale_gamma: 73 + service_level: 0.95 + SKU431: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 328 + init_stock: 126 + max_stock: 1000 + price: 646 + sale_gamma: 21 + service_level: 0.95 + SKU432: + constraint: G(stock_constraint) + cost: 25 + init_stock: 368 + max_stock: 1000 + price: 35 + sale_gamma: 46 + service_level: 0.95 + SKU433: + constraint: null + cost: 348 + init_stock: 270 + max_stock: 1000 + price: 396 + sale_gamma: 45 + service_level: 0.95 + SKU434: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 90 + max_stock: 1000 + price: 40 + sale_gamma: 30 + service_level: 0.95 + SKU435: + constraint: G(stock_constraint) + cost: 272 + init_stock: 210 + max_stock: 1000 + price: 320 + sale_gamma: 30 + service_level: 0.95 + SKU436: + constraint: null + cost: 72 + init_stock: 405 + max_stock: 1000 + price: 105 + sale_gamma: 81 + service_level: 0.95 + SKU437: + constraint: G(stock_constraint) + cost: 115 + init_stock: 84 + max_stock: 1000 + price: 223 + sale_gamma: 14 + service_level: 0.95 + SKU438: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 143 + init_stock: 40 + max_stock: 1000 + price: 190 + sale_gamma: 10 + service_level: 0.95 + SKU439: + constraint: G(stock_constraint) + cost: 256 + init_stock: 190 + max_stock: 1000 + price: 304 + sale_gamma: 38 + service_level: 0.95 + SKU44: + constraint: null + cost: 360 + init_stock: 240 + max_stock: 1000 + price: 669 + sale_gamma: 48 + service_level: 0.95 + SKU440: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 248 + init_stock: 246 + max_stock: 1000 + price: 357 + sale_gamma: 82 + service_level: 0.95 + SKU441: + constraint: null + cost: 253 + init_stock: 224 + max_stock: 1000 + price: 374 + sale_gamma: 56 + service_level: 0.95 + SKU442: + constraint: null + cost: 470 + init_stock: 352 + max_stock: 1000 + price: 629 + sale_gamma: 88 + service_level: 0.95 + SKU443: + constraint: null + cost: 218 + init_stock: 365 + max_stock: 1000 + price: 361 + sale_gamma: 73 + service_level: 0.95 + SKU444: + constraint: null + cost: 156 + init_stock: 18 + max_stock: 1000 + price: 182 + sale_gamma: 6 + service_level: 0.95 + SKU445: + constraint: null + cost: 260 + init_stock: 602 + max_stock: 1000 + price: 296 + sale_gamma: 86 + service_level: 0.95 + SKU446: + constraint: null + cost: 497 + init_stock: 147 + max_stock: 1000 + price: 690 + sale_gamma: 49 + service_level: 0.95 + SKU447: + constraint: null + cost: 196 + init_stock: 30 + max_stock: 1000 + price: 280 + sale_gamma: 5 + service_level: 0.95 + SKU448: + constraint: null + cost: 485 + init_stock: 497 + max_stock: 1000 + price: 742 + sale_gamma: 71 + service_level: 0.95 + SKU449: + constraint: null + cost: 282 + init_stock: 114 + max_stock: 1000 + price: 360 + sale_gamma: 38 + service_level: 0.95 + SKU45: + constraint: G(stock_constraint) + cost: 253 + init_stock: 30 + max_stock: 1000 + price: 351 + sale_gamma: 10 + service_level: 0.95 + SKU450: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 58 + init_stock: 294 + max_stock: 1000 + price: 77 + sale_gamma: 98 + service_level: 0.95 + SKU451: + constraint: null + cost: 468 + init_stock: 483 + max_stock: 1000 + price: 851 + sale_gamma: 69 + service_level: 0.95 + SKU452: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 63 + init_stock: 395 + max_stock: 1000 + price: 97 + sale_gamma: 79 + service_level: 0.95 + SKU453: + constraint: null + cost: 37 + init_stock: 140 + max_stock: 1000 + price: 65 + sale_gamma: 35 + service_level: 0.95 + SKU454: + constraint: G(low_profit -> low_stock_constraint) + cost: 494 + init_stock: 340 + max_stock: 1000 + price: 899 + sale_gamma: 85 + service_level: 0.95 + SKU455: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 136 + init_stock: 182 + max_stock: 1000 + price: 195 + sale_gamma: 26 + service_level: 0.95 + SKU456: + constraint: G(stock_constraint) + cost: 338 + init_stock: 75 + max_stock: 1000 + price: 659 + sale_gamma: 25 + service_level: 0.95 + SKU457: + constraint: null + cost: 169 + init_stock: 256 + max_stock: 1000 + price: 190 + sale_gamma: 64 + service_level: 0.95 + SKU458: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 403 + init_stock: 28 + max_stock: 1000 + price: 685 + sale_gamma: 7 + service_level: 0.95 + SKU459: + constraint: null + cost: 425 + init_stock: 272 + max_stock: 1000 + price: 731 + sale_gamma: 34 + service_level: 0.95 + SKU46: + constraint: G(low_profit -> low_stock_constraint) + cost: 299 + init_stock: 200 + max_stock: 1000 + price: 409 + sale_gamma: 50 + service_level: 0.95 + SKU460: + constraint: G(low_profit -> low_stock_constraint) + cost: 405 + init_stock: 292 + max_stock: 1000 + price: 558 + sale_gamma: 73 + service_level: 0.95 + SKU461: + constraint: G(stock_constraint) + cost: 251 + init_stock: 240 + max_stock: 1000 + price: 326 + sale_gamma: 48 + service_level: 0.95 + SKU462: + constraint: G(low_profit -> low_stock_constraint) + cost: 448 + init_stock: 36 + max_stock: 1000 + price: 757 + sale_gamma: 9 + service_level: 0.95 + SKU463: + constraint: null + cost: 187 + init_stock: 574 + max_stock: 1000 + price: 213 + sale_gamma: 82 + service_level: 0.95 + SKU464: + constraint: null + cost: 279 + init_stock: 272 + max_stock: 1000 + price: 438 + sale_gamma: 34 + service_level: 0.95 + SKU465: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 147 + init_stock: 400 + max_stock: 1000 + price: 288 + sale_gamma: 100 + service_level: 0.95 + SKU466: + constraint: G(stock_constraint) + cost: 97 + init_stock: 126 + max_stock: 1000 + price: 167 + sale_gamma: 42 + service_level: 0.95 + SKU467: + constraint: G(stock_constraint) + cost: 142 + init_stock: 252 + max_stock: 1000 + price: 230 + sale_gamma: 36 + service_level: 0.95 + SKU468: + constraint: G(stock_constraint) + cost: 55 + init_stock: 250 + max_stock: 1000 + price: 104 + sale_gamma: 50 + service_level: 0.95 + SKU469: + constraint: G(stock_constraint) + cost: 178 + init_stock: 105 + max_stock: 1000 + price: 331 + sale_gamma: 15 + service_level: 0.95 + SKU47: + constraint: G(stock_constraint) + cost: 121 + init_stock: 117 + max_stock: 1000 + price: 240 + sale_gamma: 39 + service_level: 0.95 + SKU470: + constraint: G(low_profit -> low_stock_constraint) + cost: 373 + init_stock: 128 + max_stock: 1000 + price: 548 + sale_gamma: 32 + service_level: 0.95 + SKU471: + constraint: G(low_profit -> low_stock_constraint) + cost: 315 + init_stock: 568 + max_stock: 1000 + price: 387 + sale_gamma: 71 + service_level: 0.95 + SKU472: + constraint: null + cost: 421 + init_stock: 177 + max_stock: 1000 + price: 627 + sale_gamma: 59 + service_level: 0.95 + SKU473: + constraint: G(low_profit -> low_stock_constraint) + cost: 195 + init_stock: 105 + max_stock: 1000 + price: 323 + sale_gamma: 21 + service_level: 0.95 + SKU474: + constraint: null + cost: 448 + init_stock: 602 + max_stock: 1000 + price: 775 + sale_gamma: 86 + service_level: 0.95 + SKU475: + constraint: null + cost: 34 + init_stock: 282 + max_stock: 1000 + price: 62 + sale_gamma: 94 + service_level: 0.95 + SKU476: + constraint: null + cost: 251 + init_stock: 180 + max_stock: 1000 + price: 361 + sale_gamma: 90 + service_level: 0.95 + SKU477: + constraint: null + cost: 289 + init_stock: 329 + max_stock: 1000 + price: 338 + sale_gamma: 47 + service_level: 0.95 + SKU478: + constraint: null + cost: 479 + init_stock: 352 + max_stock: 1000 + price: 723 + sale_gamma: 88 + service_level: 0.95 + SKU479: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 366 + init_stock: 152 + max_stock: 1000 + price: 552 + sale_gamma: 38 + service_level: 0.95 + SKU48: + constraint: null + cost: 340 + init_stock: 232 + max_stock: 1000 + price: 387 + sale_gamma: 58 + service_level: 0.95 + SKU480: + constraint: G(stock_constraint) + cost: 18 + init_stock: 180 + max_stock: 1000 + price: 21 + sale_gamma: 30 + service_level: 0.95 + SKU481: + constraint: null + cost: 330 + init_stock: 352 + max_stock: 1000 + price: 422 + sale_gamma: 88 + service_level: 0.95 + SKU482: + constraint: G(stock_constraint) + cost: 94 + init_stock: 696 + max_stock: 1000 + price: 108 + sale_gamma: 87 + service_level: 0.95 + SKU483: + constraint: null + cost: 298 + init_stock: 170 + max_stock: 1000 + price: 533 + sale_gamma: 34 + service_level: 0.95 + SKU484: + constraint: null + cost: 84 + init_stock: 365 + max_stock: 1000 + price: 117 + sale_gamma: 73 + service_level: 0.95 + SKU485: + constraint: null + cost: 318 + init_stock: 536 + max_stock: 1000 + price: 543 + sale_gamma: 67 + service_level: 0.95 + SKU486: + constraint: G(low_profit -> low_stock_constraint) + cost: 122 + init_stock: 208 + max_stock: 1000 + price: 161 + sale_gamma: 52 + service_level: 0.95 + SKU487: + constraint: null + cost: 277 + init_stock: 264 + max_stock: 1000 + price: 362 + sale_gamma: 66 + service_level: 0.95 + SKU488: + constraint: G(low_profit -> low_stock_constraint) + cost: 36 + init_stock: 189 + max_stock: 1000 + price: 42 + sale_gamma: 27 + service_level: 0.95 + SKU489: + constraint: null + cost: 59 + init_stock: 124 + max_stock: 1000 + price: 97 + sale_gamma: 31 + service_level: 0.95 + SKU49: + constraint: null + cost: 263 + init_stock: 760 + max_stock: 1000 + price: 515 + sale_gamma: 95 + service_level: 0.95 + SKU490: + constraint: null + cost: 161 + init_stock: 486 + max_stock: 1000 + price: 264 + sale_gamma: 81 + service_level: 0.95 + SKU491: + constraint: null + cost: 444 + init_stock: 450 + max_stock: 1000 + price: 834 + sale_gamma: 75 + service_level: 0.95 + SKU492: + constraint: null + cost: 53 + init_stock: 240 + max_stock: 1000 + price: 63 + sale_gamma: 80 + service_level: 0.95 + SKU493: + constraint: null + cost: 461 + init_stock: 81 + max_stock: 1000 + price: 567 + sale_gamma: 27 + service_level: 0.95 + SKU494: + constraint: null + cost: 145 + init_stock: 282 + max_stock: 1000 + price: 242 + sale_gamma: 94 + service_level: 0.95 + SKU495: + constraint: G(stock_constraint) + cost: 149 + init_stock: 372 + max_stock: 1000 + price: 217 + sale_gamma: 62 + service_level: 0.95 + SKU496: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 342 + init_stock: 295 + max_stock: 1000 + price: 492 + sale_gamma: 59 + service_level: 0.95 + SKU497: + constraint: G(low_profit -> low_stock_constraint) + cost: 33 + init_stock: 45 + max_stock: 1000 + price: 42 + sale_gamma: 9 + service_level: 0.95 + SKU498: + constraint: null + cost: 305 + init_stock: 325 + max_stock: 1000 + price: 439 + sale_gamma: 65 + service_level: 0.95 + SKU499: + constraint: G(stock_constraint) + cost: 307 + init_stock: 174 + max_stock: 1000 + price: 472 + sale_gamma: 29 + service_level: 0.95 + SKU5: + constraint: G(stock_constraint) + cost: 466 + init_stock: 426 + max_stock: 1000 + price: 852 + sale_gamma: 71 + service_level: 0.95 + SKU50: + constraint: null + cost: 282 + init_stock: 185 + max_stock: 1000 + price: 516 + sale_gamma: 37 + service_level: 0.95 + SKU500: + constraint: null + cost: 208 + init_stock: 336 + max_stock: 1000 + price: 255 + sale_gamma: 42 + service_level: 0.95 + SKU501: + constraint: null + cost: 194 + init_stock: 152 + max_stock: 1000 + price: 265 + sale_gamma: 76 + service_level: 0.95 + SKU502: + constraint: null + cost: 69 + init_stock: 390 + max_stock: 1000 + price: 101 + sale_gamma: 65 + service_level: 0.95 + SKU503: + constraint: G(stock_constraint) + cost: 208 + init_stock: 228 + max_stock: 1000 + price: 280 + sale_gamma: 57 + service_level: 0.95 + SKU504: + constraint: null + cost: 251 + init_stock: 210 + max_stock: 1000 + price: 426 + sale_gamma: 30 + service_level: 0.95 + SKU505: + constraint: null + cost: 426 + init_stock: 295 + max_stock: 1000 + price: 515 + sale_gamma: 59 + service_level: 0.95 + SKU506: + constraint: null + cost: 149 + init_stock: 465 + max_stock: 1000 + price: 220 + sale_gamma: 93 + service_level: 0.95 + SKU507: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 187 + init_stock: 60 + max_stock: 1000 + price: 293 + sale_gamma: 15 + service_level: 0.95 + SKU508: + constraint: G(low_profit -> low_stock_constraint) + cost: 272 + init_stock: 576 + max_stock: 1000 + price: 432 + sale_gamma: 96 + service_level: 0.95 + SKU509: + constraint: G(low_profit -> low_stock_constraint) + cost: 297 + init_stock: 486 + max_stock: 1000 + price: 397 + sale_gamma: 81 + service_level: 0.95 + SKU51: + constraint: G(low_profit -> low_stock_constraint) + cost: 295 + init_stock: 469 + max_stock: 1000 + price: 477 + sale_gamma: 67 + service_level: 0.95 + SKU510: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 36 + max_stock: 1000 + price: 156 + sale_gamma: 9 + service_level: 0.95 + SKU511: + constraint: G(stock_constraint) + cost: 361 + init_stock: 450 + max_stock: 1000 + price: 480 + sale_gamma: 75 + service_level: 0.95 + SKU512: + constraint: null + cost: 467 + init_stock: 132 + max_stock: 1000 + price: 854 + sale_gamma: 22 + service_level: 0.95 + SKU513: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 343 + init_stock: 42 + max_stock: 1000 + price: 631 + sale_gamma: 7 + service_level: 0.95 + SKU514: + constraint: null + cost: 186 + init_stock: 504 + max_stock: 1000 + price: 332 + sale_gamma: 63 + service_level: 0.95 + SKU515: + constraint: null + cost: 145 + init_stock: 216 + max_stock: 1000 + price: 227 + sale_gamma: 54 + service_level: 0.95 + SKU516: + constraint: G(stock_constraint) + cost: 64 + init_stock: 114 + max_stock: 1000 + price: 96 + sale_gamma: 38 + service_level: 0.95 + SKU517: + constraint: null + cost: 117 + init_stock: 45 + max_stock: 1000 + price: 168 + sale_gamma: 9 + service_level: 0.95 + SKU518: + constraint: G(stock_constraint) + cost: 26 + init_stock: 147 + max_stock: 1000 + price: 30 + sale_gamma: 21 + service_level: 0.95 + SKU519: + constraint: null + cost: 233 + init_stock: 250 + max_stock: 1000 + price: 410 + sale_gamma: 50 + service_level: 0.95 + SKU52: + constraint: null + cost: 22 + init_stock: 402 + max_stock: 1000 + price: 28 + sale_gamma: 67 + service_level: 0.95 + SKU520: + constraint: G(low_profit -> low_stock_constraint) + cost: 209 + init_stock: 44 + max_stock: 1000 + price: 248 + sale_gamma: 22 + service_level: 0.95 + SKU521: + constraint: G(low_profit -> low_stock_constraint) + cost: 220 + init_stock: 350 + max_stock: 1000 + price: 330 + sale_gamma: 50 + service_level: 0.95 + SKU522: + constraint: null + cost: 156 + init_stock: 522 + max_stock: 1000 + price: 277 + sale_gamma: 87 + service_level: 0.95 + SKU523: + constraint: null + cost: 65 + init_stock: 500 + max_stock: 1000 + price: 107 + sale_gamma: 100 + service_level: 0.95 + SKU524: + constraint: null + cost: 294 + init_stock: 188 + max_stock: 1000 + price: 464 + sale_gamma: 94 + service_level: 0.95 + SKU525: + constraint: G(stock_constraint) + cost: 432 + init_stock: 126 + max_stock: 1000 + price: 751 + sale_gamma: 42 + service_level: 0.95 + SKU526: + constraint: null + cost: 419 + init_stock: 168 + max_stock: 1000 + price: 716 + sale_gamma: 24 + service_level: 0.95 + SKU527: + constraint: null + cost: 131 + init_stock: 350 + max_stock: 1000 + price: 262 + sale_gamma: 70 + service_level: 0.95 + SKU528: + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 84 + max_stock: 1000 + price: 581 + sale_gamma: 21 + service_level: 0.95 + SKU529: + constraint: G(low_profit -> low_stock_constraint) + cost: 298 + init_stock: 248 + max_stock: 1000 + price: 375 + sale_gamma: 31 + service_level: 0.95 + SKU53: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 151 + init_stock: 490 + max_stock: 1000 + price: 191 + sale_gamma: 70 + service_level: 0.95 + SKU530: + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 135 + max_stock: 1000 + price: 205 + sale_gamma: 45 + service_level: 0.95 + SKU531: + constraint: null + cost: 103 + init_stock: 300 + max_stock: 1000 + price: 153 + sale_gamma: 60 + service_level: 0.95 + SKU532: + constraint: null + cost: 351 + init_stock: 539 + max_stock: 1000 + price: 431 + sale_gamma: 77 + service_level: 0.95 + SKU533: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 168 + max_stock: 1000 + price: 340 + sale_gamma: 42 + service_level: 0.95 + SKU534: + constraint: null + cost: 425 + init_stock: 82 + max_stock: 1000 + price: 548 + sale_gamma: 41 + service_level: 0.95 + SKU535: + constraint: null + cost: 304 + init_stock: 430 + max_stock: 1000 + price: 465 + sale_gamma: 86 + service_level: 0.95 + SKU536: + constraint: null + cost: 358 + init_stock: 208 + max_stock: 1000 + price: 415 + sale_gamma: 52 + service_level: 0.95 + SKU537: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 321 + init_stock: 45 + max_stock: 1000 + price: 455 + sale_gamma: 9 + service_level: 0.95 + SKU538: + constraint: null + cost: 457 + init_stock: 200 + max_stock: 1000 + price: 868 + sale_gamma: 50 + service_level: 0.95 + SKU539: + constraint: G(stock_constraint) + cost: 165 + init_stock: 234 + max_stock: 1000 + price: 229 + sale_gamma: 78 + service_level: 0.95 + SKU54: + constraint: G(low_profit -> low_stock_constraint) + cost: 158 + init_stock: 138 + max_stock: 1000 + price: 241 + sale_gamma: 46 + service_level: 0.95 + SKU540: + constraint: null + cost: 388 + init_stock: 294 + max_stock: 1000 + price: 496 + sale_gamma: 42 + service_level: 0.95 + SKU541: + constraint: null + cost: 131 + init_stock: 203 + max_stock: 1000 + price: 213 + sale_gamma: 29 + service_level: 0.95 + SKU542: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 38 + init_stock: 130 + max_stock: 1000 + price: 42 + sale_gamma: 26 + service_level: 0.95 + SKU543: + constraint: G(stock_constraint) + cost: 430 + init_stock: 510 + max_stock: 1000 + price: 718 + sale_gamma: 85 + service_level: 0.95 + SKU544: + constraint: G(stock_constraint) + cost: 346 + init_stock: 194 + max_stock: 1000 + price: 474 + sale_gamma: 97 + service_level: 0.95 + SKU545: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 175 + init_stock: 75 + max_stock: 1000 + price: 323 + sale_gamma: 15 + service_level: 0.95 + SKU546: + constraint: null + cost: 491 + init_stock: 576 + max_stock: 1000 + price: 765 + sale_gamma: 96 + service_level: 0.95 + SKU547: + constraint: G(stock_constraint) + cost: 161 + init_stock: 264 + max_stock: 1000 + price: 251 + sale_gamma: 44 + service_level: 0.95 + SKU548: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 111 + init_stock: 36 + max_stock: 1000 + price: 217 + sale_gamma: 6 + service_level: 0.95 + SKU549: + constraint: G(stock_constraint) + cost: 387 + init_stock: 316 + max_stock: 1000 + price: 510 + sale_gamma: 79 + service_level: 0.95 + SKU55: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 482 + init_stock: 455 + max_stock: 1000 + price: 896 + sale_gamma: 65 + service_level: 0.95 + SKU550: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 259 + init_stock: 282 + max_stock: 1000 + price: 502 + sale_gamma: 94 + service_level: 0.95 + SKU551: + constraint: null + cost: 46 + init_stock: 186 + max_stock: 1000 + price: 60 + sale_gamma: 31 + service_level: 0.95 + SKU552: + constraint: null + cost: 191 + init_stock: 450 + max_stock: 1000 + price: 315 + sale_gamma: 90 + service_level: 0.95 + SKU553: + constraint: G(low_profit -> low_stock_constraint) + cost: 208 + init_stock: 60 + max_stock: 1000 + price: 251 + sale_gamma: 30 + service_level: 0.95 + SKU554: + constraint: null + cost: 340 + init_stock: 82 + max_stock: 1000 + price: 387 + sale_gamma: 41 + service_level: 0.95 + SKU555: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 489 + init_stock: 252 + max_stock: 1000 + price: 684 + sale_gamma: 84 + service_level: 0.95 + SKU556: + constraint: G(stock_constraint) + cost: 110 + init_stock: 90 + max_stock: 1000 + price: 213 + sale_gamma: 30 + service_level: 0.95 + SKU557: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 334 + init_stock: 365 + max_stock: 1000 + price: 661 + sale_gamma: 73 + service_level: 0.95 + SKU558: + constraint: null + cost: 399 + init_stock: 85 + max_stock: 1000 + price: 490 + sale_gamma: 17 + service_level: 0.95 + SKU559: + constraint: null + cost: 313 + init_stock: 270 + max_stock: 1000 + price: 591 + sale_gamma: 54 + service_level: 0.95 + SKU56: + constraint: null + cost: 377 + init_stock: 222 + max_stock: 1000 + price: 671 + sale_gamma: 74 + service_level: 0.95 + SKU560: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 283 + init_stock: 246 + max_stock: 1000 + price: 458 + sale_gamma: 41 + service_level: 0.95 + SKU561: + constraint: null + cost: 202 + init_stock: 312 + max_stock: 1000 + price: 385 + sale_gamma: 39 + service_level: 0.95 + SKU562: + constraint: null + cost: 347 + init_stock: 356 + max_stock: 1000 + price: 666 + sale_gamma: 89 + service_level: 0.95 + SKU563: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 401 + init_stock: 30 + max_stock: 1000 + price: 509 + sale_gamma: 5 + service_level: 0.95 + SKU564: + constraint: null + cost: 109 + init_stock: 63 + max_stock: 1000 + price: 183 + sale_gamma: 9 + service_level: 0.95 + SKU565: + constraint: null + cost: 57 + init_stock: 525 + max_stock: 1000 + price: 90 + sale_gamma: 75 + service_level: 0.95 + SKU566: + constraint: null + cost: 131 + init_stock: 44 + max_stock: 1000 + price: 165 + sale_gamma: 11 + service_level: 0.95 + SKU567: + constraint: G(stock_constraint) + cost: 361 + init_stock: 27 + max_stock: 1000 + price: 465 + sale_gamma: 9 + service_level: 0.95 + SKU568: + constraint: null + cost: 75 + init_stock: 234 + max_stock: 1000 + price: 102 + sale_gamma: 78 + service_level: 0.95 + SKU569: + constraint: null + cost: 473 + init_stock: 192 + max_stock: 1000 + price: 591 + sale_gamma: 48 + service_level: 0.95 + SKU57: + constraint: G(stock_constraint) + cost: 359 + init_stock: 350 + max_stock: 1000 + price: 682 + sale_gamma: 50 + service_level: 0.95 + SKU570: + constraint: null + cost: 388 + init_stock: 581 + max_stock: 1000 + price: 686 + sale_gamma: 83 + service_level: 0.95 + SKU571: + constraint: G(stock_constraint) + cost: 168 + init_stock: 78 + max_stock: 1000 + price: 208 + sale_gamma: 39 + service_level: 0.95 + SKU572: + constraint: null + cost: 26 + init_stock: 225 + max_stock: 1000 + price: 41 + sale_gamma: 45 + service_level: 0.95 + SKU573: + constraint: null + cost: 246 + init_stock: 164 + max_stock: 1000 + price: 391 + sale_gamma: 41 + service_level: 0.95 + SKU574: + constraint: G(low_profit -> low_stock_constraint) + cost: 94 + init_stock: 150 + max_stock: 1000 + price: 166 + sale_gamma: 50 + service_level: 0.95 + SKU575: + constraint: null + cost: 237 + init_stock: 237 + max_stock: 1000 + price: 438 + sale_gamma: 79 + service_level: 0.95 + SKU576: + constraint: G(stock_constraint) + cost: 265 + init_stock: 468 + max_stock: 1000 + price: 416 + sale_gamma: 78 + service_level: 0.95 + SKU577: + constraint: G(low_profit -> low_stock_constraint) + cost: 18 + init_stock: 348 + max_stock: 1000 + price: 24 + sale_gamma: 87 + service_level: 0.95 + SKU578: + constraint: null + cost: 100 + init_stock: 284 + max_stock: 1000 + price: 148 + sale_gamma: 71 + service_level: 0.95 + SKU579: + constraint: null + cost: 415 + init_stock: 27 + max_stock: 1000 + price: 771 + sale_gamma: 9 + service_level: 0.95 + SKU58: + constraint: null + cost: 362 + init_stock: 240 + max_stock: 1000 + price: 521 + sale_gamma: 48 + service_level: 0.95 + SKU580: + constraint: null + cost: 203 + init_stock: 15 + max_stock: 1000 + price: 261 + sale_gamma: 5 + service_level: 0.95 + SKU581: + constraint: null + cost: 152 + init_stock: 516 + max_stock: 1000 + price: 185 + sale_gamma: 86 + service_level: 0.95 + SKU582: + constraint: G(stock_constraint) + cost: 359 + init_stock: 480 + max_stock: 1000 + price: 567 + sale_gamma: 96 + service_level: 0.95 + SKU583: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 86 + init_stock: 261 + max_stock: 1000 + price: 98 + sale_gamma: 87 + service_level: 0.95 + SKU584: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 33 + init_stock: 270 + max_stock: 1000 + price: 36 + sale_gamma: 45 + service_level: 0.95 + SKU585: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 100 + max_stock: 1000 + price: 53 + sale_gamma: 20 + service_level: 0.95 + SKU586: + constraint: G(stock_constraint) + cost: 61 + init_stock: 264 + max_stock: 1000 + price: 94 + sale_gamma: 44 + service_level: 0.95 + SKU587: + constraint: null + cost: 290 + init_stock: 468 + max_stock: 1000 + price: 423 + sale_gamma: 78 + service_level: 0.95 + SKU588: + constraint: G(stock_constraint) + cost: 476 + init_stock: 176 + max_stock: 1000 + price: 885 + sale_gamma: 44 + service_level: 0.95 + SKU589: + constraint: null + cost: 244 + init_stock: 170 + max_stock: 1000 + price: 380 + sale_gamma: 34 + service_level: 0.95 + SKU59: + constraint: G(low_profit -> low_stock_constraint) + cost: 145 + init_stock: 255 + max_stock: 1000 + price: 275 + sale_gamma: 51 + service_level: 0.95 + SKU590: + constraint: null + cost: 70 + init_stock: 93 + max_stock: 1000 + price: 103 + sale_gamma: 31 + service_level: 0.95 + SKU591: + constraint: G(stock_constraint) + cost: 206 + init_stock: 504 + max_stock: 1000 + price: 298 + sale_gamma: 84 + service_level: 0.95 + SKU592: + constraint: null + cost: 218 + init_stock: 276 + max_stock: 1000 + price: 418 + sale_gamma: 46 + service_level: 0.95 + SKU593: + constraint: null + cost: 124 + init_stock: 88 + max_stock: 1000 + price: 221 + sale_gamma: 44 + service_level: 0.95 + SKU594: + constraint: null + cost: 238 + init_stock: 150 + max_stock: 1000 + price: 459 + sale_gamma: 50 + service_level: 0.95 + SKU595: + constraint: null + cost: 73 + init_stock: 172 + max_stock: 1000 + price: 107 + sale_gamma: 43 + service_level: 0.95 + SKU596: + constraint: null + cost: 432 + init_stock: 35 + max_stock: 1000 + price: 540 + sale_gamma: 7 + service_level: 0.95 + SKU597: + constraint: G(stock_constraint) + cost: 252 + init_stock: 435 + max_stock: 1000 + price: 451 + sale_gamma: 87 + service_level: 0.95 + SKU598: + constraint: null + cost: 348 + init_stock: 28 + max_stock: 1000 + price: 612 + sale_gamma: 14 + service_level: 0.95 + SKU599: + constraint: null + cost: 54 + init_stock: 204 + max_stock: 1000 + price: 66 + sale_gamma: 68 + service_level: 0.95 + SKU6: + constraint: null + cost: 122 + init_stock: 616 + max_stock: 1000 + price: 195 + sale_gamma: 88 + service_level: 0.95 + SKU60: + constraint: G(stock_constraint) + cost: 200 + init_stock: 234 + max_stock: 1000 + price: 346 + sale_gamma: 39 + service_level: 0.95 + SKU600: + constraint: null + cost: 386 + init_stock: 325 + max_stock: 1000 + price: 505 + sale_gamma: 65 + service_level: 0.95 + SKU601: + constraint: null + cost: 138 + init_stock: 273 + max_stock: 1000 + price: 198 + sale_gamma: 39 + service_level: 0.95 + SKU602: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 184 + init_stock: 588 + max_stock: 1000 + price: 318 + sale_gamma: 98 + service_level: 0.95 + SKU603: + constraint: G(low_profit -> low_stock_constraint) + cost: 278 + init_stock: 252 + max_stock: 1000 + price: 550 + sale_gamma: 42 + service_level: 0.95 + SKU604: + constraint: G(stock_constraint) + cost: 270 + init_stock: 400 + max_stock: 1000 + price: 378 + sale_gamma: 50 + service_level: 0.95 + SKU605: + constraint: null + cost: 288 + init_stock: 120 + max_stock: 1000 + price: 443 + sale_gamma: 24 + service_level: 0.95 + SKU606: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 114 + init_stock: 66 + max_stock: 1000 + price: 161 + sale_gamma: 11 + service_level: 0.95 + SKU607: + constraint: null + cost: 208 + init_stock: 395 + max_stock: 1000 + price: 359 + sale_gamma: 79 + service_level: 0.95 + SKU608: + constraint: null + cost: 39 + init_stock: 365 + max_stock: 1000 + price: 49 + sale_gamma: 73 + service_level: 0.95 + SKU609: + constraint: null + cost: 102 + init_stock: 114 + max_stock: 1000 + price: 171 + sale_gamma: 19 + service_level: 0.95 + SKU61: + constraint: null + cost: 461 + init_stock: 150 + max_stock: 1000 + price: 645 + sale_gamma: 75 + service_level: 0.95 + SKU610: + constraint: null + cost: 449 + init_stock: 204 + max_stock: 1000 + price: 749 + sale_gamma: 68 + service_level: 0.95 + SKU611: + constraint: G(low_profit -> low_stock_constraint) + cost: 306 + init_stock: 539 + max_stock: 1000 + price: 489 + sale_gamma: 77 + service_level: 0.95 + SKU612: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 391 + init_stock: 616 + max_stock: 1000 + price: 613 + sale_gamma: 88 + service_level: 0.95 + SKU613: + constraint: G(stock_constraint) + cost: 174 + init_stock: 56 + max_stock: 1000 + price: 254 + sale_gamma: 7 + service_level: 0.95 + SKU614: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 173 + init_stock: 45 + max_stock: 1000 + price: 318 + sale_gamma: 15 + service_level: 0.95 + SKU615: + constraint: G(stock_constraint) + cost: 330 + init_stock: 364 + max_stock: 1000 + price: 432 + sale_gamma: 91 + service_level: 0.95 + SKU616: + constraint: G(low_profit -> low_stock_constraint) + cost: 232 + init_stock: 219 + max_stock: 1000 + price: 382 + sale_gamma: 73 + service_level: 0.95 + SKU617: + constraint: null + cost: 203 + init_stock: 420 + max_stock: 1000 + price: 227 + sale_gamma: 60 + service_level: 0.95 + SKU618: + constraint: G(stock_constraint) + cost: 77 + init_stock: 138 + max_stock: 1000 + price: 97 + sale_gamma: 23 + service_level: 0.95 + SKU619: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 189 + init_stock: 344 + max_stock: 1000 + price: 359 + sale_gamma: 86 + service_level: 0.95 + SKU62: + constraint: null + cost: 124 + init_stock: 36 + max_stock: 1000 + price: 168 + sale_gamma: 9 + service_level: 0.95 + SKU620: + constraint: null + cost: 231 + init_stock: 156 + max_stock: 1000 + price: 267 + sale_gamma: 39 + service_level: 0.95 + SKU621: + constraint: null + cost: 199 + init_stock: 216 + max_stock: 1000 + price: 332 + sale_gamma: 72 + service_level: 0.95 + SKU622: + constraint: null + cost: 253 + init_stock: 455 + max_stock: 1000 + price: 468 + sale_gamma: 65 + service_level: 0.95 + SKU623: + constraint: null + cost: 335 + init_stock: 220 + max_stock: 1000 + price: 368 + sale_gamma: 55 + service_level: 0.95 + SKU624: + constraint: null + cost: 482 + init_stock: 270 + max_stock: 1000 + price: 824 + sale_gamma: 54 + service_level: 0.95 + SKU625: + constraint: null + cost: 46 + init_stock: 445 + max_stock: 1000 + price: 60 + sale_gamma: 89 + service_level: 0.95 + SKU626: + constraint: null + cost: 451 + init_stock: 534 + max_stock: 1000 + price: 694 + sale_gamma: 89 + service_level: 0.95 + SKU627: + constraint: null + cost: 220 + init_stock: 656 + max_stock: 1000 + price: 264 + sale_gamma: 82 + service_level: 0.95 + SKU628: + constraint: null + cost: 124 + init_stock: 156 + max_stock: 1000 + price: 229 + sale_gamma: 26 + service_level: 0.95 + SKU629: + constraint: G(stock_constraint) + cost: 353 + init_stock: 460 + max_stock: 1000 + price: 515 + sale_gamma: 92 + service_level: 0.95 + SKU63: + constraint: null + cost: 68 + init_stock: 70 + max_stock: 1000 + price: 86 + sale_gamma: 14 + service_level: 0.95 + SKU630: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 122 + init_stock: 34 + max_stock: 1000 + price: 137 + sale_gamma: 17 + service_level: 0.95 + SKU631: + constraint: null + cost: 296 + init_stock: 196 + max_stock: 1000 + price: 399 + sale_gamma: 49 + service_level: 0.95 + SKU632: + constraint: null + cost: 85 + init_stock: 470 + max_stock: 1000 + price: 141 + sale_gamma: 94 + service_level: 0.95 + SKU633: + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 402 + max_stock: 1000 + price: 228 + sale_gamma: 67 + service_level: 0.95 + SKU634: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 17 + init_stock: 438 + max_stock: 1000 + price: 30 + sale_gamma: 73 + service_level: 0.95 + SKU635: + constraint: null + cost: 341 + init_stock: 372 + max_stock: 1000 + price: 654 + sale_gamma: 93 + service_level: 0.95 + SKU636: + constraint: G(stock_constraint) + cost: 385 + init_stock: 111 + max_stock: 1000 + price: 739 + sale_gamma: 37 + service_level: 0.95 + SKU637: + constraint: G(low_profit -> low_stock_constraint) + cost: 83 + init_stock: 305 + max_stock: 1000 + price: 118 + sale_gamma: 61 + service_level: 0.95 + SKU638: + constraint: G(stock_constraint) + cost: 375 + init_stock: 16 + max_stock: 1000 + price: 536 + sale_gamma: 8 + service_level: 0.95 + SKU639: + constraint: null + cost: 327 + init_stock: 160 + max_stock: 1000 + price: 487 + sale_gamma: 40 + service_level: 0.95 + SKU64: + constraint: G(low_profit -> low_stock_constraint) + cost: 36 + init_stock: 133 + max_stock: 1000 + price: 42 + sale_gamma: 19 + service_level: 0.95 + SKU640: + constraint: G(low_profit -> low_stock_constraint) + cost: 275 + init_stock: 546 + max_stock: 1000 + price: 382 + sale_gamma: 91 + service_level: 0.95 + SKU641: + constraint: null + cost: 365 + init_stock: 486 + max_stock: 1000 + price: 445 + sale_gamma: 81 + service_level: 0.95 + SKU642: + constraint: null + cost: 414 + init_stock: 150 + max_stock: 1000 + price: 554 + sale_gamma: 25 + service_level: 0.95 + SKU643: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 358 + init_stock: 322 + max_stock: 1000 + price: 461 + sale_gamma: 46 + service_level: 0.95 + SKU644: + constraint: G(low_profit -> low_stock_constraint) + cost: 46 + init_stock: 410 + max_stock: 1000 + price: 61 + sale_gamma: 82 + service_level: 0.95 + SKU645: + constraint: null + cost: 467 + init_stock: 594 + max_stock: 1000 + price: 742 + sale_gamma: 99 + service_level: 0.95 + SKU646: + constraint: G(low_profit -> low_stock_constraint) + cost: 189 + init_stock: 91 + max_stock: 1000 + price: 268 + sale_gamma: 13 + service_level: 0.95 + SKU647: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 303 + init_stock: 462 + max_stock: 1000 + price: 369 + sale_gamma: 77 + service_level: 0.95 + SKU648: + constraint: G(stock_constraint) + cost: 226 + init_stock: 110 + max_stock: 1000 + price: 336 + sale_gamma: 55 + service_level: 0.95 + SKU649: + constraint: null + cost: 198 + init_stock: 175 + max_stock: 1000 + price: 229 + sale_gamma: 25 + service_level: 0.95 + SKU65: + constraint: null + cost: 15 + init_stock: 658 + max_stock: 1000 + price: 25 + sale_gamma: 94 + service_level: 0.95 + SKU650: + constraint: null + cost: 351 + init_stock: 224 + max_stock: 1000 + price: 498 + sale_gamma: 56 + service_level: 0.95 + SKU651: + constraint: null + cost: 380 + init_stock: 672 + max_stock: 1000 + price: 596 + sale_gamma: 96 + service_level: 0.95 + SKU652: + constraint: null + cost: 123 + init_stock: 280 + max_stock: 1000 + price: 168 + sale_gamma: 40 + service_level: 0.95 + SKU653: + constraint: G(low_profit -> low_stock_constraint) + cost: 479 + init_stock: 384 + max_stock: 1000 + price: 661 + sale_gamma: 96 + service_level: 0.95 + SKU654: + constraint: null + cost: 66 + init_stock: 32 + max_stock: 1000 + price: 110 + sale_gamma: 8 + service_level: 0.95 + SKU655: + constraint: G(low_profit -> low_stock_constraint) + cost: 333 + init_stock: 126 + max_stock: 1000 + price: 479 + sale_gamma: 42 + service_level: 0.95 + SKU656: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 53 + init_stock: 504 + max_stock: 1000 + price: 81 + sale_gamma: 72 + service_level: 0.95 + SKU657: + constraint: G(low_profit -> low_stock_constraint) + cost: 73 + init_stock: 567 + max_stock: 1000 + price: 110 + sale_gamma: 81 + service_level: 0.95 + SKU658: + constraint: null + cost: 70 + init_stock: 252 + max_stock: 1000 + price: 107 + sale_gamma: 36 + service_level: 0.95 + SKU659: + constraint: null + cost: 21 + init_stock: 336 + max_stock: 1000 + price: 27 + sale_gamma: 56 + service_level: 0.95 + SKU66: + constraint: null + cost: 143 + init_stock: 360 + max_stock: 1000 + price: 230 + sale_gamma: 90 + service_level: 0.95 + SKU660: + constraint: null + cost: 487 + init_stock: 415 + max_stock: 1000 + price: 560 + sale_gamma: 83 + service_level: 0.95 + SKU661: + constraint: null + cost: 344 + init_stock: 296 + max_stock: 1000 + price: 581 + sale_gamma: 74 + service_level: 0.95 + SKU662: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 266 + max_stock: 1000 + price: 628 + sale_gamma: 38 + service_level: 0.95 + SKU663: + constraint: G(stock_constraint) + cost: 418 + init_stock: 320 + max_stock: 1000 + price: 518 + sale_gamma: 40 + service_level: 0.95 + SKU664: + constraint: G(stock_constraint) + cost: 351 + init_stock: 420 + max_stock: 1000 + price: 494 + sale_gamma: 84 + service_level: 0.95 + SKU665: + constraint: null + cost: 493 + init_stock: 344 + max_stock: 1000 + price: 734 + sale_gamma: 43 + service_level: 0.95 + SKU666: + constraint: G(stock_constraint) + cost: 341 + init_stock: 176 + max_stock: 1000 + price: 572 + sale_gamma: 88 + service_level: 0.95 + SKU667: + constraint: null + cost: 325 + init_stock: 52 + max_stock: 1000 + price: 562 + sale_gamma: 13 + service_level: 0.95 + SKU668: + constraint: G(low_profit -> low_stock_constraint) + cost: 286 + init_stock: 300 + max_stock: 1000 + price: 463 + sale_gamma: 60 + service_level: 0.95 + SKU669: + constraint: null + cost: 74 + init_stock: 96 + max_stock: 1000 + price: 83 + sale_gamma: 16 + service_level: 0.95 + SKU67: + constraint: G(low_profit -> low_stock_constraint) + cost: 247 + init_stock: 159 + max_stock: 1000 + price: 454 + sale_gamma: 53 + service_level: 0.95 + SKU670: + constraint: G(stock_constraint) + cost: 289 + init_stock: 258 + max_stock: 1000 + price: 430 + sale_gamma: 86 + service_level: 0.95 + SKU671: + constraint: G(stock_constraint) + cost: 267 + init_stock: 332 + max_stock: 1000 + price: 296 + sale_gamma: 83 + service_level: 0.95 + SKU672: + constraint: null + cost: 369 + init_stock: 78 + max_stock: 1000 + price: 420 + sale_gamma: 13 + service_level: 0.95 + SKU673: + constraint: G(low_profit -> low_stock_constraint) + cost: 322 + init_stock: 123 + max_stock: 1000 + price: 418 + sale_gamma: 41 + service_level: 0.95 + SKU674: + constraint: null + cost: 223 + init_stock: 66 + max_stock: 1000 + price: 263 + sale_gamma: 22 + service_level: 0.95 + SKU675: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 438 + init_stock: 99 + max_stock: 1000 + price: 529 + sale_gamma: 33 + service_level: 0.95 + SKU676: + constraint: null + cost: 244 + init_stock: 366 + max_stock: 1000 + price: 275 + sale_gamma: 61 + service_level: 0.95 + SKU677: + constraint: null + cost: 425 + init_stock: 176 + max_stock: 1000 + price: 658 + sale_gamma: 44 + service_level: 0.95 + SKU678: + constraint: null + cost: 168 + init_stock: 216 + max_stock: 1000 + price: 199 + sale_gamma: 36 + service_level: 0.95 + SKU679: + constraint: null + cost: 395 + init_stock: 132 + max_stock: 1000 + price: 734 + sale_gamma: 44 + service_level: 0.95 + SKU68: + constraint: G(stock_constraint) + cost: 169 + init_stock: 56 + max_stock: 1000 + price: 239 + sale_gamma: 14 + service_level: 0.95 + SKU680: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 260 + init_stock: 180 + max_stock: 1000 + price: 327 + sale_gamma: 30 + service_level: 0.95 + SKU681: + constraint: G(low_profit -> low_stock_constraint) + cost: 464 + init_stock: 776 + max_stock: 1000 + price: 510 + sale_gamma: 97 + service_level: 0.95 + SKU682: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 370 + init_stock: 318 + max_stock: 1000 + price: 407 + sale_gamma: 53 + service_level: 0.95 + SKU683: + constraint: G(low_profit -> low_stock_constraint) + cost: 327 + init_stock: 536 + max_stock: 1000 + price: 608 + sale_gamma: 67 + service_level: 0.95 + SKU684: + constraint: G(stock_constraint) + cost: 355 + init_stock: 158 + max_stock: 1000 + price: 401 + sale_gamma: 79 + service_level: 0.95 + SKU685: + constraint: null + cost: 422 + init_stock: 270 + max_stock: 1000 + price: 493 + sale_gamma: 45 + service_level: 0.95 + SKU686: + constraint: G(low_profit -> low_stock_constraint) + cost: 63 + init_stock: 378 + max_stock: 1000 + price: 122 + sale_gamma: 63 + service_level: 0.95 + SKU687: + constraint: G(low_profit -> low_stock_constraint) + cost: 34 + init_stock: 456 + max_stock: 1000 + price: 43 + sale_gamma: 76 + service_level: 0.95 + SKU688: + constraint: G(low_profit -> low_stock_constraint) + cost: 484 + init_stock: 462 + max_stock: 1000 + price: 759 + sale_gamma: 77 + service_level: 0.95 + SKU689: + constraint: G(stock_constraint) + cost: 499 + init_stock: 75 + max_stock: 1000 + price: 808 + sale_gamma: 15 + service_level: 0.95 + SKU69: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 176 + init_stock: 402 + max_stock: 1000 + price: 197 + sale_gamma: 67 + service_level: 0.95 + SKU690: + constraint: null + cost: 437 + init_stock: 415 + max_stock: 1000 + price: 498 + sale_gamma: 83 + service_level: 0.95 + SKU691: + constraint: null + cost: 17 + init_stock: 632 + max_stock: 1000 + price: 18 + sale_gamma: 79 + service_level: 0.95 + SKU692: + constraint: G(stock_constraint) + cost: 225 + init_stock: 195 + max_stock: 1000 + price: 258 + sale_gamma: 65 + service_level: 0.95 + SKU693: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 19 + init_stock: 244 + max_stock: 1000 + price: 21 + sale_gamma: 61 + service_level: 0.95 + SKU694: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 208 + init_stock: 300 + max_stock: 1000 + price: 386 + sale_gamma: 75 + service_level: 0.95 + SKU695: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 190 + init_stock: 49 + max_stock: 1000 + price: 361 + sale_gamma: 7 + service_level: 0.95 + SKU696: + constraint: G(low_profit -> low_stock_constraint) + cost: 348 + init_stock: 290 + max_stock: 1000 + price: 643 + sale_gamma: 58 + service_level: 0.95 + SKU697: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 455 + init_stock: 320 + max_stock: 1000 + price: 641 + sale_gamma: 80 + service_level: 0.95 + SKU698: + constraint: null + cost: 198 + init_stock: 488 + max_stock: 1000 + price: 316 + sale_gamma: 61 + service_level: 0.95 + SKU699: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 31 + init_stock: 100 + max_stock: 1000 + price: 58 + sale_gamma: 20 + service_level: 0.95 + SKU7: + constraint: null + cost: 101 + init_stock: 480 + max_stock: 1000 + price: 131 + sale_gamma: 96 + service_level: 0.95 + SKU70: + constraint: G(stock_constraint) + cost: 186 + init_stock: 140 + max_stock: 1000 + price: 288 + sale_gamma: 35 + service_level: 0.95 + SKU700: + constraint: G(low_profit -> low_stock_constraint) + cost: 74 + init_stock: 45 + max_stock: 1000 + price: 130 + sale_gamma: 9 + service_level: 0.95 + SKU701: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 399 + init_stock: 462 + max_stock: 1000 + price: 770 + sale_gamma: 77 + service_level: 0.95 + SKU702: + constraint: G(stock_constraint) + cost: 82 + init_stock: 204 + max_stock: 1000 + price: 163 + sale_gamma: 34 + service_level: 0.95 + SKU703: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 363 + init_stock: 76 + max_stock: 1000 + price: 602 + sale_gamma: 38 + service_level: 0.95 + SKU704: + constraint: G(stock_constraint) + cost: 384 + init_stock: 261 + max_stock: 1000 + price: 518 + sale_gamma: 87 + service_level: 0.95 + SKU705: + constraint: null + cost: 128 + init_stock: 378 + max_stock: 1000 + price: 236 + sale_gamma: 63 + service_level: 0.95 + SKU706: + constraint: null + cost: 182 + init_stock: 455 + max_stock: 1000 + price: 242 + sale_gamma: 91 + service_level: 0.95 + SKU707: + constraint: null + cost: 18 + init_stock: 276 + max_stock: 1000 + price: 35 + sale_gamma: 69 + service_level: 0.95 + SKU708: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 178 + init_stock: 112 + max_stock: 1000 + price: 334 + sale_gamma: 28 + service_level: 0.95 + SKU709: + constraint: G(stock_constraint) + cost: 102 + init_stock: 212 + max_stock: 1000 + price: 173 + sale_gamma: 53 + service_level: 0.95 + SKU71: + constraint: null + cost: 315 + init_stock: 225 + max_stock: 1000 + price: 589 + sale_gamma: 45 + service_level: 0.95 + SKU710: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 252 + init_stock: 95 + max_stock: 1000 + price: 337 + sale_gamma: 19 + service_level: 0.95 + SKU711: + constraint: null + cost: 281 + init_stock: 414 + max_stock: 1000 + price: 320 + sale_gamma: 69 + service_level: 0.95 + SKU712: + constraint: null + cost: 232 + init_stock: 77 + max_stock: 1000 + price: 438 + sale_gamma: 11 + service_level: 0.95 + SKU713: + constraint: null + cost: 285 + init_stock: 129 + max_stock: 1000 + price: 413 + sale_gamma: 43 + service_level: 0.95 + SKU714: + constraint: null + cost: 79 + init_stock: 287 + max_stock: 1000 + price: 106 + sale_gamma: 41 + service_level: 0.95 + SKU715: + constraint: G(stock_constraint) + cost: 234 + init_stock: 34 + max_stock: 1000 + price: 271 + sale_gamma: 17 + service_level: 0.95 + SKU716: + constraint: null + cost: 70 + init_stock: 450 + max_stock: 1000 + price: 123 + sale_gamma: 75 + service_level: 0.95 + SKU717: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 345 + init_stock: 56 + max_stock: 1000 + price: 382 + sale_gamma: 8 + service_level: 0.95 + SKU718: + constraint: null + cost: 357 + init_stock: 174 + max_stock: 1000 + price: 692 + sale_gamma: 58 + service_level: 0.95 + SKU719: + constraint: null + cost: 340 + init_stock: 455 + max_stock: 1000 + price: 384 + sale_gamma: 65 + service_level: 0.95 + SKU72: + constraint: null + cost: 458 + init_stock: 595 + max_stock: 1000 + price: 870 + sale_gamma: 85 + service_level: 0.95 + SKU720: + constraint: null + cost: 325 + init_stock: 294 + max_stock: 1000 + price: 503 + sale_gamma: 42 + service_level: 0.95 + SKU721: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 73 + init_stock: 44 + max_stock: 1000 + price: 143 + sale_gamma: 11 + service_level: 0.95 + SKU722: + constraint: G(stock_constraint) + cost: 392 + init_stock: 194 + max_stock: 1000 + price: 572 + sale_gamma: 97 + service_level: 0.95 + SKU723: + constraint: null + cost: 318 + init_stock: 356 + max_stock: 1000 + price: 442 + sale_gamma: 89 + service_level: 0.95 + SKU724: + constraint: G(low_profit -> low_stock_constraint) + cost: 400 + init_stock: 198 + max_stock: 1000 + price: 520 + sale_gamma: 33 + service_level: 0.95 + SKU725: + constraint: null + cost: 175 + init_stock: 148 + max_stock: 1000 + price: 309 + sale_gamma: 37 + service_level: 0.95 + SKU726: + constraint: G(low_profit -> low_stock_constraint) + cost: 458 + init_stock: 356 + max_stock: 1000 + price: 567 + sale_gamma: 89 + service_level: 0.95 + SKU727: + constraint: null + cost: 418 + init_stock: 285 + max_stock: 1000 + price: 526 + sale_gamma: 57 + service_level: 0.95 + SKU728: + constraint: null + cost: 475 + init_stock: 185 + max_stock: 1000 + price: 864 + sale_gamma: 37 + service_level: 0.95 + SKU729: + constraint: null + cost: 324 + init_stock: 252 + max_stock: 1000 + price: 476 + sale_gamma: 36 + service_level: 0.95 + SKU73: + constraint: null + cost: 212 + init_stock: 216 + max_stock: 1000 + price: 248 + sale_gamma: 54 + service_level: 0.95 + SKU730: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 16 + init_stock: 65 + max_stock: 1000 + price: 19 + sale_gamma: 13 + service_level: 0.95 + SKU731: + constraint: G(stock_constraint) + cost: 88 + init_stock: 410 + max_stock: 1000 + price: 155 + sale_gamma: 82 + service_level: 0.95 + SKU732: + constraint: null + cost: 41 + init_stock: 434 + max_stock: 1000 + price: 73 + sale_gamma: 62 + service_level: 0.95 + SKU733: + constraint: G(stock_constraint) + cost: 315 + init_stock: 104 + max_stock: 1000 + price: 541 + sale_gamma: 26 + service_level: 0.95 + SKU734: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 37 + init_stock: 255 + max_stock: 1000 + price: 71 + sale_gamma: 51 + service_level: 0.95 + SKU735: + constraint: null + cost: 266 + init_stock: 594 + max_stock: 1000 + price: 409 + sale_gamma: 99 + service_level: 0.95 + SKU736: + constraint: G(stock_constraint) + cost: 368 + init_stock: 75 + max_stock: 1000 + price: 632 + sale_gamma: 25 + service_level: 0.95 + SKU737: + constraint: null + cost: 475 + init_stock: 93 + max_stock: 1000 + price: 655 + sale_gamma: 31 + service_level: 0.95 + SKU738: + constraint: null + cost: 185 + init_stock: 192 + max_stock: 1000 + price: 246 + sale_gamma: 48 + service_level: 0.95 + SKU739: + constraint: G(low_profit -> low_stock_constraint) + cost: 475 + init_stock: 296 + max_stock: 1000 + price: 612 + sale_gamma: 74 + service_level: 0.95 + SKU74: + constraint: null + cost: 159 + init_stock: 168 + max_stock: 1000 + price: 189 + sale_gamma: 42 + service_level: 0.95 + SKU740: + constraint: null + cost: 390 + init_stock: 256 + max_stock: 1000 + price: 549 + sale_gamma: 64 + service_level: 0.95 + SKU741: + constraint: G(stock_constraint) + cost: 91 + init_stock: 280 + max_stock: 1000 + price: 112 + sale_gamma: 56 + service_level: 0.95 + SKU742: + constraint: G(stock_constraint) + cost: 188 + init_stock: 110 + max_stock: 1000 + price: 374 + sale_gamma: 22 + service_level: 0.95 + SKU743: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 217 + init_stock: 301 + max_stock: 1000 + price: 431 + sale_gamma: 43 + service_level: 0.95 + SKU744: + constraint: G(low_profit -> low_stock_constraint) + cost: 379 + init_stock: 290 + max_stock: 1000 + price: 579 + sale_gamma: 58 + service_level: 0.95 + SKU745: + constraint: G(low_profit -> low_stock_constraint) + cost: 316 + init_stock: 368 + max_stock: 1000 + price: 376 + sale_gamma: 92 + service_level: 0.95 + SKU746: + constraint: null + cost: 437 + init_stock: 160 + max_stock: 1000 + price: 624 + sale_gamma: 40 + service_level: 0.95 + SKU747: + constraint: null + cost: 373 + init_stock: 474 + max_stock: 1000 + price: 589 + sale_gamma: 79 + service_level: 0.95 + SKU748: + constraint: null + cost: 275 + init_stock: 330 + max_stock: 1000 + price: 464 + sale_gamma: 66 + service_level: 0.95 + SKU749: + constraint: null + cost: 394 + init_stock: 106 + max_stock: 1000 + price: 705 + sale_gamma: 53 + service_level: 0.95 + SKU75: + constraint: G(low_profit -> low_stock_constraint) + cost: 131 + init_stock: 76 + max_stock: 1000 + price: 217 + sale_gamma: 19 + service_level: 0.95 + SKU750: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 310 + max_stock: 1000 + price: 463 + sale_gamma: 62 + service_level: 0.95 + SKU751: + constraint: null + cost: 369 + init_stock: 325 + max_stock: 1000 + price: 428 + sale_gamma: 65 + service_level: 0.95 + SKU752: + constraint: null + cost: 259 + init_stock: 546 + max_stock: 1000 + price: 435 + sale_gamma: 78 + service_level: 0.95 + SKU753: + constraint: G(stock_constraint) + cost: 77 + init_stock: 325 + max_stock: 1000 + price: 128 + sale_gamma: 65 + service_level: 0.95 + SKU754: + constraint: G(stock_constraint) + cost: 387 + init_stock: 91 + max_stock: 1000 + price: 545 + sale_gamma: 13 + service_level: 0.95 + SKU755: + constraint: null + cost: 354 + init_stock: 560 + max_stock: 1000 + price: 523 + sale_gamma: 80 + service_level: 0.95 + SKU756: + constraint: G(stock_constraint) + cost: 246 + init_stock: 360 + max_stock: 1000 + price: 361 + sale_gamma: 90 + service_level: 0.95 + SKU757: + constraint: null + cost: 40 + init_stock: 399 + max_stock: 1000 + price: 76 + sale_gamma: 57 + service_level: 0.95 + SKU758: + constraint: null + cost: 241 + init_stock: 160 + max_stock: 1000 + price: 457 + sale_gamma: 40 + service_level: 0.95 + SKU759: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 435 + init_stock: 76 + max_stock: 1000 + price: 648 + sale_gamma: 38 + service_level: 0.95 + SKU76: + constraint: null + cost: 147 + init_stock: 256 + max_stock: 1000 + price: 191 + sale_gamma: 64 + service_level: 0.95 + SKU760: + constraint: null + cost: 176 + init_stock: 255 + max_stock: 1000 + price: 279 + sale_gamma: 51 + service_level: 0.95 + SKU761: + constraint: G(stock_constraint) + cost: 224 + init_stock: 275 + max_stock: 1000 + price: 250 + sale_gamma: 55 + service_level: 0.95 + SKU762: + constraint: G(stock_constraint) + cost: 264 + init_stock: 153 + max_stock: 1000 + price: 351 + sale_gamma: 51 + service_level: 0.95 + SKU763: + constraint: null + cost: 385 + init_stock: 316 + max_stock: 1000 + price: 677 + sale_gamma: 79 + service_level: 0.95 + SKU764: + constraint: null + cost: 349 + init_stock: 68 + max_stock: 1000 + price: 596 + sale_gamma: 34 + service_level: 0.95 + SKU765: + constraint: null + cost: 345 + init_stock: 52 + max_stock: 1000 + price: 472 + sale_gamma: 13 + service_level: 0.95 + SKU766: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 478 + init_stock: 140 + max_stock: 1000 + price: 855 + sale_gamma: 20 + service_level: 0.95 + SKU767: + constraint: null + cost: 95 + init_stock: 456 + max_stock: 1000 + price: 160 + sale_gamma: 76 + service_level: 0.95 + SKU768: + constraint: G(low_profit -> low_stock_constraint) + cost: 181 + init_stock: 644 + max_stock: 1000 + price: 244 + sale_gamma: 92 + service_level: 0.95 + SKU769: + constraint: null + cost: 24 + init_stock: 87 + max_stock: 1000 + price: 43 + sale_gamma: 29 + service_level: 0.95 + SKU77: + constraint: null + cost: 409 + init_stock: 144 + max_stock: 1000 + price: 687 + sale_gamma: 72 + service_level: 0.95 + SKU770: + constraint: null + cost: 150 + init_stock: 186 + max_stock: 1000 + price: 166 + sale_gamma: 62 + service_level: 0.95 + SKU771: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 101 + init_stock: 35 + max_stock: 1000 + price: 182 + sale_gamma: 7 + service_level: 0.95 + SKU772: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 256 + init_stock: 36 + max_stock: 1000 + price: 281 + sale_gamma: 6 + service_level: 0.95 + SKU773: + constraint: null + cost: 84 + init_stock: 368 + max_stock: 1000 + price: 117 + sale_gamma: 92 + service_level: 0.95 + SKU774: + constraint: null + cost: 447 + init_stock: 118 + max_stock: 1000 + price: 746 + sale_gamma: 59 + service_level: 0.95 + SKU775: + constraint: null + cost: 175 + init_stock: 688 + max_stock: 1000 + price: 192 + sale_gamma: 86 + service_level: 0.95 + SKU776: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 103 + init_stock: 425 + max_stock: 1000 + price: 172 + sale_gamma: 85 + service_level: 0.95 + SKU777: + constraint: null + cost: 292 + init_stock: 171 + max_stock: 1000 + price: 347 + sale_gamma: 57 + service_level: 0.95 + SKU778: + constraint: null + cost: 203 + init_stock: 40 + max_stock: 1000 + price: 288 + sale_gamma: 8 + service_level: 0.95 + SKU779: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 117 + init_stock: 215 + max_stock: 1000 + price: 186 + sale_gamma: 43 + service_level: 0.95 + SKU78: + constraint: null + cost: 234 + init_stock: 91 + max_stock: 1000 + price: 400 + sale_gamma: 13 + service_level: 0.95 + SKU780: + constraint: G(low_profit -> low_stock_constraint) + cost: 69 + init_stock: 410 + max_stock: 1000 + price: 102 + sale_gamma: 82 + service_level: 0.95 + SKU781: + constraint: null + cost: 372 + init_stock: 144 + max_stock: 1000 + price: 528 + sale_gamma: 36 + service_level: 0.95 + SKU782: + constraint: G(low_profit -> low_stock_constraint) + cost: 27 + init_stock: 70 + max_stock: 1000 + price: 35 + sale_gamma: 10 + service_level: 0.95 + SKU783: + constraint: null + cost: 87 + init_stock: 324 + max_stock: 1000 + price: 139 + sale_gamma: 81 + service_level: 0.95 + SKU784: + constraint: null + cost: 309 + init_stock: 312 + max_stock: 1000 + price: 577 + sale_gamma: 52 + service_level: 0.95 + SKU785: + constraint: null + cost: 191 + init_stock: 210 + max_stock: 1000 + price: 255 + sale_gamma: 70 + service_level: 0.95 + SKU786: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 91 + init_stock: 132 + max_stock: 1000 + price: 102 + sale_gamma: 22 + service_level: 0.95 + SKU787: + constraint: null + cost: 360 + init_stock: 366 + max_stock: 1000 + price: 658 + sale_gamma: 61 + service_level: 0.95 + SKU788: + constraint: G(stock_constraint) + cost: 351 + init_stock: 112 + max_stock: 1000 + price: 417 + sale_gamma: 28 + service_level: 0.95 + SKU789: + constraint: G(stock_constraint) + cost: 153 + init_stock: 232 + max_stock: 1000 + price: 298 + sale_gamma: 58 + service_level: 0.95 + SKU79: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 245 + init_stock: 66 + max_stock: 1000 + price: 276 + sale_gamma: 11 + service_level: 0.95 + SKU790: + constraint: null + cost: 417 + init_stock: 330 + max_stock: 1000 + price: 704 + sale_gamma: 66 + service_level: 0.95 + SKU791: + constraint: G(stock_constraint) + cost: 134 + init_stock: 56 + max_stock: 1000 + price: 155 + sale_gamma: 28 + service_level: 0.95 + SKU792: + constraint: G(low_profit -> low_stock_constraint) + cost: 313 + init_stock: 84 + max_stock: 1000 + price: 494 + sale_gamma: 21 + service_level: 0.95 + SKU793: + constraint: null + cost: 195 + init_stock: 532 + max_stock: 1000 + price: 282 + sale_gamma: 76 + service_level: 0.95 + SKU794: + constraint: null + cost: 325 + init_stock: 400 + max_stock: 1000 + price: 454 + sale_gamma: 80 + service_level: 0.95 + SKU795: + constraint: null + cost: 276 + init_stock: 288 + max_stock: 1000 + price: 549 + sale_gamma: 48 + service_level: 0.95 + SKU796: + constraint: null + cost: 447 + init_stock: 294 + max_stock: 1000 + price: 885 + sale_gamma: 49 + service_level: 0.95 + SKU797: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 100 + init_stock: 234 + max_stock: 1000 + price: 119 + sale_gamma: 39 + service_level: 0.95 + SKU798: + constraint: null + cost: 426 + init_stock: 180 + max_stock: 1000 + price: 630 + sale_gamma: 30 + service_level: 0.95 + SKU799: + constraint: null + cost: 63 + init_stock: 21 + max_stock: 1000 + price: 78 + sale_gamma: 7 + service_level: 0.95 + SKU8: + constraint: G(stock_constraint) + cost: 125 + init_stock: 252 + max_stock: 1000 + price: 196 + sale_gamma: 63 + service_level: 0.95 + SKU80: + constraint: G(stock_constraint) + cost: 163 + init_stock: 420 + max_stock: 1000 + price: 270 + sale_gamma: 70 + service_level: 0.95 + SKU800: + constraint: G(low_profit -> low_stock_constraint) + cost: 298 + init_stock: 470 + max_stock: 1000 + price: 455 + sale_gamma: 94 + service_level: 0.95 + SKU801: + constraint: G(low_profit -> low_stock_constraint) + cost: 389 + init_stock: 216 + max_stock: 1000 + price: 672 + sale_gamma: 36 + service_level: 0.95 + SKU802: + constraint: G(stock_constraint) + cost: 173 + init_stock: 310 + max_stock: 1000 + price: 281 + sale_gamma: 62 + service_level: 0.95 + SKU803: + constraint: null + cost: 272 + init_stock: 95 + max_stock: 1000 + price: 544 + sale_gamma: 19 + service_level: 0.95 + SKU804: + constraint: null + cost: 390 + init_stock: 188 + max_stock: 1000 + price: 491 + sale_gamma: 47 + service_level: 0.95 + SKU805: + constraint: null + cost: 61 + init_stock: 132 + max_stock: 1000 + price: 118 + sale_gamma: 33 + service_level: 0.95 + SKU806: + constraint: null + cost: 158 + init_stock: 156 + max_stock: 1000 + price: 186 + sale_gamma: 52 + service_level: 0.95 + SKU807: + constraint: null + cost: 453 + init_stock: 182 + max_stock: 1000 + price: 656 + sale_gamma: 26 + service_level: 0.95 + SKU808: + constraint: G(stock_constraint) + cost: 249 + init_stock: 182 + max_stock: 1000 + price: 435 + sale_gamma: 91 + service_level: 0.95 + SKU809: + constraint: null + cost: 55 + init_stock: 390 + max_stock: 1000 + price: 69 + sale_gamma: 65 + service_level: 0.95 + SKU81: + constraint: G(low_profit -> low_stock_constraint) + cost: 182 + init_stock: 528 + max_stock: 1000 + price: 223 + sale_gamma: 66 + service_level: 0.95 + SKU810: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 276 + init_stock: 42 + max_stock: 1000 + price: 322 + sale_gamma: 6 + service_level: 0.95 + SKU811: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 254 + init_stock: 148 + max_stock: 1000 + price: 322 + sale_gamma: 37 + service_level: 0.95 + SKU812: + constraint: null + cost: 252 + init_stock: 320 + max_stock: 1000 + price: 337 + sale_gamma: 80 + service_level: 0.95 + SKU813: + constraint: G(stock_constraint) + cost: 253 + init_stock: 49 + max_stock: 1000 + price: 333 + sale_gamma: 7 + service_level: 0.95 + SKU814: + constraint: null + cost: 485 + init_stock: 194 + max_stock: 1000 + price: 921 + sale_gamma: 97 + service_level: 0.95 + SKU815: + constraint: null + cost: 390 + init_stock: 168 + max_stock: 1000 + price: 429 + sale_gamma: 24 + service_level: 0.95 + SKU816: + constraint: null + cost: 152 + init_stock: 455 + max_stock: 1000 + price: 174 + sale_gamma: 91 + service_level: 0.95 + SKU817: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 227 + init_stock: 25 + max_stock: 1000 + price: 401 + sale_gamma: 5 + service_level: 0.95 + SKU818: + constraint: null + cost: 354 + init_stock: 215 + max_stock: 1000 + price: 534 + sale_gamma: 43 + service_level: 0.95 + SKU819: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 302 + init_stock: 135 + max_stock: 1000 + price: 480 + sale_gamma: 27 + service_level: 0.95 + SKU82: + constraint: null + cost: 290 + init_stock: 140 + max_stock: 1000 + price: 469 + sale_gamma: 28 + service_level: 0.95 + SKU820: + constraint: null + cost: 264 + init_stock: 410 + max_stock: 1000 + price: 464 + sale_gamma: 82 + service_level: 0.95 + SKU821: + constraint: G(stock_constraint) + cost: 99 + init_stock: 207 + max_stock: 1000 + price: 151 + sale_gamma: 69 + service_level: 0.95 + SKU822: + constraint: null + cost: 136 + init_stock: 490 + max_stock: 1000 + price: 266 + sale_gamma: 70 + service_level: 0.95 + SKU823: + constraint: null + cost: 75 + init_stock: 84 + max_stock: 1000 + price: 116 + sale_gamma: 28 + service_level: 0.95 + SKU824: + constraint: G(low_profit -> low_stock_constraint) + cost: 170 + init_stock: 420 + max_stock: 1000 + price: 200 + sale_gamma: 70 + service_level: 0.95 + SKU825: + constraint: null + cost: 214 + init_stock: 45 + max_stock: 1000 + price: 359 + sale_gamma: 15 + service_level: 0.95 + SKU826: + constraint: G(stock_constraint) + cost: 386 + init_stock: 330 + max_stock: 1000 + price: 428 + sale_gamma: 55 + service_level: 0.95 + SKU827: + constraint: G(stock_constraint) + cost: 148 + init_stock: 448 + max_stock: 1000 + price: 208 + sale_gamma: 64 + service_level: 0.95 + SKU828: + constraint: null + cost: 400 + init_stock: 276 + max_stock: 1000 + price: 627 + sale_gamma: 69 + service_level: 0.95 + SKU829: + constraint: null + cost: 61 + init_stock: 370 + max_stock: 1000 + price: 83 + sale_gamma: 74 + service_level: 0.95 + SKU83: + constraint: G(low_profit -> low_stock_constraint) + cost: 296 + init_stock: 68 + max_stock: 1000 + price: 535 + sale_gamma: 17 + service_level: 0.95 + SKU830: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 167 + init_stock: 144 + max_stock: 1000 + price: 252 + sale_gamma: 24 + service_level: 0.95 + SKU831: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 116 + max_stock: 1000 + price: 353 + sale_gamma: 29 + service_level: 0.95 + SKU832: + constraint: null + cost: 33 + init_stock: 164 + max_stock: 1000 + price: 48 + sale_gamma: 82 + service_level: 0.95 + SKU833: + constraint: G(low_profit -> low_stock_constraint) + cost: 400 + init_stock: 392 + max_stock: 1000 + price: 756 + sale_gamma: 98 + service_level: 0.95 + SKU834: + constraint: G(low_profit -> low_stock_constraint) + cost: 422 + init_stock: 10 + max_stock: 1000 + price: 662 + sale_gamma: 5 + service_level: 0.95 + SKU835: + constraint: null + cost: 440 + init_stock: 156 + max_stock: 1000 + price: 532 + sale_gamma: 52 + service_level: 0.95 + SKU836: + constraint: G(stock_constraint) + cost: 323 + init_stock: 192 + max_stock: 1000 + price: 552 + sale_gamma: 96 + service_level: 0.95 + SKU837: + constraint: null + cost: 373 + init_stock: 156 + max_stock: 1000 + price: 607 + sale_gamma: 26 + service_level: 0.95 + SKU838: + constraint: null + cost: 456 + init_stock: 308 + max_stock: 1000 + price: 711 + sale_gamma: 77 + service_level: 0.95 + SKU839: + constraint: null + cost: 473 + init_stock: 360 + max_stock: 1000 + price: 695 + sale_gamma: 60 + service_level: 0.95 + SKU84: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 318 + init_stock: 294 + max_stock: 1000 + price: 594 + sale_gamma: 42 + service_level: 0.95 + SKU840: + constraint: G(low_profit -> low_stock_constraint) + cost: 266 + init_stock: 150 + max_stock: 1000 + price: 436 + sale_gamma: 50 + service_level: 0.95 + SKU841: + constraint: G(stock_constraint) + cost: 285 + init_stock: 595 + max_stock: 1000 + price: 538 + sale_gamma: 85 + service_level: 0.95 + SKU842: + constraint: G(low_profit -> low_stock_constraint) + cost: 41 + init_stock: 252 + max_stock: 1000 + price: 70 + sale_gamma: 84 + service_level: 0.95 + SKU843: + constraint: null + cost: 360 + init_stock: 432 + max_stock: 1000 + price: 694 + sale_gamma: 72 + service_level: 0.95 + SKU844: + constraint: G(low_profit -> low_stock_constraint) + cost: 51 + init_stock: 474 + max_stock: 1000 + price: 63 + sale_gamma: 79 + service_level: 0.95 + SKU845: + constraint: G(low_profit -> low_stock_constraint) + cost: 288 + init_stock: 176 + max_stock: 1000 + price: 558 + sale_gamma: 22 + service_level: 0.95 + SKU846: + constraint: null + cost: 485 + init_stock: 234 + max_stock: 1000 + price: 940 + sale_gamma: 39 + service_level: 0.95 + SKU847: + constraint: G(low_profit -> low_stock_constraint) + cost: 388 + init_stock: 546 + max_stock: 1000 + price: 519 + sale_gamma: 91 + service_level: 0.95 + SKU848: + constraint: null + cost: 306 + init_stock: 184 + max_stock: 1000 + price: 358 + sale_gamma: 46 + service_level: 0.95 + SKU849: + constraint: G(low_profit -> low_stock_constraint) + cost: 320 + init_stock: 160 + max_stock: 1000 + price: 425 + sale_gamma: 40 + service_level: 0.95 + SKU85: + constraint: G(low_profit -> low_stock_constraint) + cost: 442 + init_stock: 400 + max_stock: 1000 + price: 583 + sale_gamma: 100 + service_level: 0.95 + SKU850: + constraint: G(stock_constraint) + cost: 183 + init_stock: 342 + max_stock: 1000 + price: 206 + sale_gamma: 57 + service_level: 0.95 + SKU851: + constraint: null + cost: 53 + init_stock: 384 + max_stock: 1000 + price: 96 + sale_gamma: 64 + service_level: 0.95 + SKU852: + constraint: null + cost: 306 + init_stock: 162 + max_stock: 1000 + price: 483 + sale_gamma: 54 + service_level: 0.95 + SKU853: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 386 + init_stock: 280 + max_stock: 1000 + price: 443 + sale_gamma: 56 + service_level: 0.95 + SKU854: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 212 + init_stock: 390 + max_stock: 1000 + price: 290 + sale_gamma: 65 + service_level: 0.95 + SKU855: + constraint: null + cost: 76 + init_stock: 432 + max_stock: 1000 + price: 114 + sale_gamma: 72 + service_level: 0.95 + SKU856: + constraint: G(low_profit -> low_stock_constraint) + cost: 380 + init_stock: 224 + max_stock: 1000 + price: 627 + sale_gamma: 32 + service_level: 0.95 + SKU857: + constraint: G(stock_constraint) + cost: 462 + init_stock: 500 + max_stock: 1000 + price: 646 + sale_gamma: 100 + service_level: 0.95 + SKU858: + constraint: null + cost: 80 + init_stock: 120 + max_stock: 1000 + price: 158 + sale_gamma: 24 + service_level: 0.95 + SKU859: + constraint: G(low_profit -> low_stock_constraint) + cost: 215 + init_stock: 224 + max_stock: 1000 + price: 298 + sale_gamma: 28 + service_level: 0.95 + SKU86: + constraint: null + cost: 386 + init_stock: 595 + max_stock: 1000 + price: 447 + sale_gamma: 85 + service_level: 0.95 + SKU860: + constraint: G(low_profit -> low_stock_constraint) + cost: 313 + init_stock: 105 + max_stock: 1000 + price: 435 + sale_gamma: 15 + service_level: 0.95 + SKU861: + constraint: null + cost: 477 + init_stock: 52 + max_stock: 1000 + price: 944 + sale_gamma: 13 + service_level: 0.95 + SKU862: + constraint: null + cost: 240 + init_stock: 64 + max_stock: 1000 + price: 412 + sale_gamma: 16 + service_level: 0.95 + SKU863: + constraint: null + cost: 470 + init_stock: 372 + max_stock: 1000 + price: 911 + sale_gamma: 93 + service_level: 0.95 + SKU864: + constraint: null + cost: 203 + init_stock: 114 + max_stock: 1000 + price: 341 + sale_gamma: 19 + service_level: 0.95 + SKU865: + constraint: G(low_profit -> low_stock_constraint) + cost: 144 + init_stock: 152 + max_stock: 1000 + price: 253 + sale_gamma: 19 + service_level: 0.95 + SKU866: + constraint: null + cost: 172 + init_stock: 264 + max_stock: 1000 + price: 201 + sale_gamma: 88 + service_level: 0.95 + SKU867: + constraint: G(low_profit -> low_stock_constraint) + cost: 499 + init_stock: 500 + max_stock: 1000 + price: 698 + sale_gamma: 100 + service_level: 0.95 + SKU868: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 41 + init_stock: 150 + max_stock: 1000 + price: 56 + sale_gamma: 50 + service_level: 0.95 + SKU869: + constraint: null + cost: 97 + init_stock: 388 + max_stock: 1000 + price: 111 + sale_gamma: 97 + service_level: 0.95 + SKU87: + constraint: null + cost: 368 + init_stock: 207 + max_stock: 1000 + price: 563 + sale_gamma: 69 + service_level: 0.95 + SKU870: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 281 + init_stock: 335 + max_stock: 1000 + price: 446 + sale_gamma: 67 + service_level: 0.95 + SKU871: + constraint: null + cost: 101 + init_stock: 396 + max_stock: 1000 + price: 112 + sale_gamma: 99 + service_level: 0.95 + SKU872: + constraint: null + cost: 133 + init_stock: 160 + max_stock: 1000 + price: 195 + sale_gamma: 32 + service_level: 0.95 + SKU873: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 423 + init_stock: 155 + max_stock: 1000 + price: 829 + sale_gamma: 31 + service_level: 0.95 + SKU874: + constraint: G(low_profit -> low_stock_constraint) + cost: 469 + init_stock: 360 + max_stock: 1000 + price: 689 + sale_gamma: 60 + service_level: 0.95 + SKU875: + constraint: G(stock_constraint) + cost: 51 + init_stock: 138 + max_stock: 1000 + price: 57 + sale_gamma: 23 + service_level: 0.95 + SKU876: + constraint: null + cost: 303 + init_stock: 427 + max_stock: 1000 + price: 427 + sale_gamma: 61 + service_level: 0.95 + SKU877: + constraint: null + cost: 40 + init_stock: 245 + max_stock: 1000 + price: 70 + sale_gamma: 35 + service_level: 0.95 + SKU878: + constraint: G(low_profit -> low_stock_constraint) + cost: 27 + init_stock: 476 + max_stock: 1000 + price: 32 + sale_gamma: 68 + service_level: 0.95 + SKU879: + constraint: null + cost: 150 + init_stock: 300 + max_stock: 1000 + price: 198 + sale_gamma: 100 + service_level: 0.95 + SKU88: + constraint: G(low_profit -> low_stock_constraint) + cost: 471 + init_stock: 36 + max_stock: 1000 + price: 824 + sale_gamma: 12 + service_level: 0.95 + SKU880: + constraint: null + cost: 433 + init_stock: 155 + max_stock: 1000 + price: 532 + sale_gamma: 31 + service_level: 0.95 + SKU881: + constraint: null + cost: 431 + init_stock: 420 + max_stock: 1000 + price: 560 + sale_gamma: 70 + service_level: 0.95 + SKU882: + constraint: G(stock_constraint) + cost: 194 + init_stock: 80 + max_stock: 1000 + price: 314 + sale_gamma: 16 + service_level: 0.95 + SKU883: + constraint: null + cost: 284 + init_stock: 152 + max_stock: 1000 + price: 479 + sale_gamma: 38 + service_level: 0.95 + SKU884: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 139 + init_stock: 195 + max_stock: 1000 + price: 222 + sale_gamma: 39 + service_level: 0.95 + SKU885: + constraint: G(low_profit -> low_stock_constraint) + cost: 49 + init_stock: 189 + max_stock: 1000 + price: 58 + sale_gamma: 27 + service_level: 0.95 + SKU886: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 372 + init_stock: 584 + max_stock: 1000 + price: 602 + sale_gamma: 73 + service_level: 0.95 + SKU887: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 266 + init_stock: 174 + max_stock: 1000 + price: 494 + sale_gamma: 87 + service_level: 0.95 + SKU888: + constraint: G(low_profit -> low_stock_constraint) + cost: 143 + init_stock: 45 + max_stock: 1000 + price: 220 + sale_gamma: 9 + service_level: 0.95 + SKU889: + constraint: null + cost: 101 + init_stock: 147 + max_stock: 1000 + price: 199 + sale_gamma: 21 + service_level: 0.95 + SKU89: + constraint: null + cost: 138 + init_stock: 744 + max_stock: 1000 + price: 269 + sale_gamma: 93 + service_level: 0.95 + SKU890: + constraint: G(low_profit -> low_stock_constraint) + cost: 161 + init_stock: 600 + max_stock: 1000 + price: 247 + sale_gamma: 100 + service_level: 0.95 + SKU891: + constraint: null + cost: 356 + init_stock: 176 + max_stock: 1000 + price: 615 + sale_gamma: 22 + service_level: 0.95 + SKU892: + constraint: null + cost: 313 + init_stock: 552 + max_stock: 1000 + price: 516 + sale_gamma: 92 + service_level: 0.95 + SKU893: + constraint: null + cost: 229 + init_stock: 594 + max_stock: 1000 + price: 302 + sale_gamma: 99 + service_level: 0.95 + SKU894: + constraint: null + cost: 129 + init_stock: 222 + max_stock: 1000 + price: 176 + sale_gamma: 74 + service_level: 0.95 + SKU895: + constraint: null + cost: 230 + init_stock: 39 + max_stock: 1000 + price: 301 + sale_gamma: 13 + service_level: 0.95 + SKU896: + constraint: null + cost: 289 + init_stock: 400 + max_stock: 1000 + price: 465 + sale_gamma: 80 + service_level: 0.95 + SKU897: + constraint: null + cost: 393 + init_stock: 432 + max_stock: 1000 + price: 640 + sale_gamma: 72 + service_level: 0.95 + SKU898: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 477 + init_stock: 44 + max_stock: 1000 + price: 615 + sale_gamma: 11 + service_level: 0.95 + SKU899: + constraint: null + cost: 233 + init_stock: 240 + max_stock: 1000 + price: 309 + sale_gamma: 48 + service_level: 0.95 + SKU9: + constraint: null + cost: 166 + init_stock: 384 + max_stock: 1000 + price: 247 + sale_gamma: 96 + service_level: 0.95 + SKU90: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 454 + init_stock: 204 + max_stock: 1000 + price: 726 + sale_gamma: 51 + service_level: 0.95 + SKU900: + constraint: null + cost: 158 + init_stock: 165 + max_stock: 1000 + price: 263 + sale_gamma: 55 + service_level: 0.95 + SKU901: + constraint: G(stock_constraint) + cost: 215 + init_stock: 203 + max_stock: 1000 + price: 320 + sale_gamma: 29 + service_level: 0.95 + SKU902: + constraint: G(low_profit -> low_stock_constraint) + cost: 125 + init_stock: 560 + max_stock: 1000 + price: 205 + sale_gamma: 80 + service_level: 0.95 + SKU903: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 357 + init_stock: 266 + max_stock: 1000 + price: 517 + sale_gamma: 38 + service_level: 0.95 + SKU904: + constraint: G(low_profit -> low_stock_constraint) + cost: 496 + init_stock: 255 + max_stock: 1000 + price: 605 + sale_gamma: 51 + service_level: 0.95 + SKU905: + constraint: null + cost: 249 + init_stock: 217 + max_stock: 1000 + price: 383 + sale_gamma: 31 + service_level: 0.95 + SKU906: + constraint: G(low_profit -> low_stock_constraint) + cost: 166 + init_stock: 156 + max_stock: 1000 + price: 273 + sale_gamma: 52 + service_level: 0.95 + SKU907: + constraint: null + cost: 22 + init_stock: 498 + max_stock: 1000 + price: 33 + sale_gamma: 83 + service_level: 0.95 + SKU908: + constraint: null + cost: 408 + init_stock: 546 + max_stock: 1000 + price: 595 + sale_gamma: 78 + service_level: 0.95 + SKU909: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 482 + init_stock: 480 + max_stock: 1000 + price: 703 + sale_gamma: 96 + service_level: 0.95 + SKU91: + constraint: G(low_profit -> low_stock_constraint) + cost: 303 + init_stock: 48 + max_stock: 1000 + price: 463 + sale_gamma: 16 + service_level: 0.95 + SKU910: + constraint: null + cost: 226 + init_stock: 132 + max_stock: 1000 + price: 350 + sale_gamma: 33 + service_level: 0.95 + SKU911: + constraint: null + cost: 461 + init_stock: 210 + max_stock: 1000 + price: 686 + sale_gamma: 70 + service_level: 0.95 + SKU912: + constraint: null + cost: 236 + init_stock: 135 + max_stock: 1000 + price: 328 + sale_gamma: 27 + service_level: 0.95 + SKU913: + constraint: null + cost: 322 + init_stock: 184 + max_stock: 1000 + price: 518 + sale_gamma: 46 + service_level: 0.95 + SKU914: + constraint: null + cost: 272 + init_stock: 434 + max_stock: 1000 + price: 446 + sale_gamma: 62 + service_level: 0.95 + SKU915: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 337 + init_stock: 280 + max_stock: 1000 + price: 545 + sale_gamma: 56 + service_level: 0.95 + SKU916: + constraint: null + cost: 337 + init_stock: 534 + max_stock: 1000 + price: 647 + sale_gamma: 89 + service_level: 0.95 + SKU917: + constraint: G(low_profit -> low_stock_constraint) + cost: 258 + init_stock: 120 + max_stock: 1000 + price: 366 + sale_gamma: 30 + service_level: 0.95 + SKU918: + constraint: G(stock_constraint) + cost: 148 + init_stock: 330 + max_stock: 1000 + price: 224 + sale_gamma: 55 + service_level: 0.95 + SKU919: + constraint: G(stock_constraint) + cost: 393 + init_stock: 125 + max_stock: 1000 + price: 711 + sale_gamma: 25 + service_level: 0.95 + SKU92: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 262 + init_stock: 252 + max_stock: 1000 + price: 466 + sale_gamma: 63 + service_level: 0.95 + SKU920: + constraint: null + cost: 357 + init_stock: 344 + max_stock: 1000 + price: 696 + sale_gamma: 86 + service_level: 0.95 + SKU921: + constraint: G(low_profit -> low_stock_constraint) + cost: 227 + init_stock: 198 + max_stock: 1000 + price: 419 + sale_gamma: 66 + service_level: 0.95 + SKU922: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 112 + init_stock: 201 + max_stock: 1000 + price: 123 + sale_gamma: 67 + service_level: 0.95 + SKU923: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 496 + init_stock: 348 + max_stock: 1000 + price: 828 + sale_gamma: 58 + service_level: 0.95 + SKU924: + constraint: null + cost: 316 + init_stock: 425 + max_stock: 1000 + price: 423 + sale_gamma: 85 + service_level: 0.95 + SKU925: + constraint: null + cost: 360 + init_stock: 90 + max_stock: 1000 + price: 716 + sale_gamma: 15 + service_level: 0.95 + SKU926: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 360 + init_stock: 469 + max_stock: 1000 + price: 475 + sale_gamma: 67 + service_level: 0.95 + SKU927: + constraint: G(stock_constraint) + cost: 260 + init_stock: 147 + max_stock: 1000 + price: 439 + sale_gamma: 21 + service_level: 0.95 + SKU928: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 491 + init_stock: 415 + max_stock: 1000 + price: 869 + sale_gamma: 83 + service_level: 0.95 + SKU929: + constraint: null + cost: 359 + init_stock: 800 + max_stock: 1000 + price: 470 + sale_gamma: 100 + service_level: 0.95 + SKU93: + constraint: null + cost: 404 + init_stock: 268 + max_stock: 1000 + price: 557 + sale_gamma: 67 + service_level: 0.95 + SKU930: + constraint: G(low_profit -> low_stock_constraint) + cost: 198 + init_stock: 196 + max_stock: 1000 + price: 247 + sale_gamma: 28 + service_level: 0.95 + SKU931: + constraint: null + cost: 71 + init_stock: 56 + max_stock: 1000 + price: 101 + sale_gamma: 14 + service_level: 0.95 + SKU932: + constraint: null + cost: 163 + init_stock: 264 + max_stock: 1000 + price: 283 + sale_gamma: 66 + service_level: 0.95 + SKU933: + constraint: null + cost: 113 + init_stock: 156 + max_stock: 1000 + price: 179 + sale_gamma: 78 + service_level: 0.95 + SKU934: + constraint: G(stock_constraint) + cost: 219 + init_stock: 469 + max_stock: 1000 + price: 346 + sale_gamma: 67 + service_level: 0.95 + SKU935: + constraint: null + cost: 364 + init_stock: 376 + max_stock: 1000 + price: 527 + sale_gamma: 94 + service_level: 0.95 + SKU936: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 24 + init_stock: 20 + max_stock: 1000 + price: 41 + sale_gamma: 5 + service_level: 0.95 + SKU937: + constraint: G(stock_constraint) + cost: 135 + init_stock: 85 + max_stock: 1000 + price: 216 + sale_gamma: 17 + service_level: 0.95 + SKU938: + constraint: G(low_profit -> low_stock_constraint) + cost: 432 + init_stock: 63 + max_stock: 1000 + price: 704 + sale_gamma: 21 + service_level: 0.95 + SKU939: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 173 + init_stock: 413 + max_stock: 1000 + price: 219 + sale_gamma: 59 + service_level: 0.95 + SKU94: + constraint: G(low_profit -> low_stock_constraint) + cost: 184 + init_stock: 235 + max_stock: 1000 + price: 261 + sale_gamma: 47 + service_level: 0.95 + SKU940: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 14 + init_stock: 279 + max_stock: 1000 + price: 24 + sale_gamma: 93 + service_level: 0.95 + SKU941: + constraint: G(stock_constraint) + cost: 80 + init_stock: 456 + max_stock: 1000 + price: 132 + sale_gamma: 57 + service_level: 0.95 + SKU942: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 202 + init_stock: 65 + max_stock: 1000 + price: 303 + sale_gamma: 13 + service_level: 0.95 + SKU943: + constraint: null + cost: 138 + init_stock: 245 + max_stock: 1000 + price: 182 + sale_gamma: 49 + service_level: 0.95 + SKU944: + constraint: null + cost: 196 + init_stock: 132 + max_stock: 1000 + price: 356 + sale_gamma: 44 + service_level: 0.95 + SKU945: + constraint: null + cost: 141 + init_stock: 85 + max_stock: 1000 + price: 176 + sale_gamma: 17 + service_level: 0.95 + SKU946: + constraint: null + cost: 325 + init_stock: 294 + max_stock: 1000 + price: 555 + sale_gamma: 49 + service_level: 0.95 + SKU947: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 338 + init_stock: 637 + max_stock: 1000 + price: 547 + sale_gamma: 91 + service_level: 0.95 + SKU948: + constraint: null + cost: 425 + init_stock: 112 + max_stock: 1000 + price: 476 + sale_gamma: 28 + service_level: 0.95 + SKU949: + constraint: G(stock_constraint) + cost: 309 + init_stock: 15 + max_stock: 1000 + price: 522 + sale_gamma: 5 + service_level: 0.95 + SKU95: + constraint: null + cost: 136 + init_stock: 84 + max_stock: 1000 + price: 214 + sale_gamma: 21 + service_level: 0.95 + SKU950: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 102 + init_stock: 324 + max_stock: 1000 + price: 128 + sale_gamma: 54 + service_level: 0.95 + SKU951: + constraint: G(low_profit -> low_stock_constraint) + cost: 75 + init_stock: 90 + max_stock: 1000 + price: 117 + sale_gamma: 18 + service_level: 0.95 + SKU952: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 156 + init_stock: 66 + max_stock: 1000 + price: 276 + sale_gamma: 11 + service_level: 0.95 + SKU953: + constraint: null + cost: 138 + init_stock: 245 + max_stock: 1000 + price: 230 + sale_gamma: 35 + service_level: 0.95 + SKU954: + constraint: null + cost: 296 + init_stock: 430 + max_stock: 1000 + price: 444 + sale_gamma: 86 + service_level: 0.95 + SKU955: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 55 + init_stock: 189 + max_stock: 1000 + price: 85 + sale_gamma: 63 + service_level: 0.95 + SKU956: + constraint: null + cost: 282 + init_stock: 425 + max_stock: 1000 + price: 397 + sale_gamma: 85 + service_level: 0.95 + SKU957: + constraint: null + cost: 305 + init_stock: 306 + max_stock: 1000 + price: 466 + sale_gamma: 51 + service_level: 0.95 + SKU958: + constraint: null + cost: 369 + init_stock: 462 + max_stock: 1000 + price: 704 + sale_gamma: 66 + service_level: 0.95 + SKU959: + constraint: null + cost: 81 + init_stock: 164 + max_stock: 1000 + price: 127 + sale_gamma: 82 + service_level: 0.95 + SKU96: + constraint: G(low_profit -> low_stock_constraint) + cost: 176 + init_stock: 264 + max_stock: 1000 + price: 329 + sale_gamma: 44 + service_level: 0.95 + SKU960: + constraint: null + cost: 147 + init_stock: 42 + max_stock: 1000 + price: 235 + sale_gamma: 6 + service_level: 0.95 + SKU961: + constraint: null + cost: 264 + init_stock: 176 + max_stock: 1000 + price: 290 + sale_gamma: 44 + service_level: 0.95 + SKU962: + constraint: null + cost: 354 + init_stock: 176 + max_stock: 1000 + price: 392 + sale_gamma: 44 + service_level: 0.95 + SKU963: + constraint: null + cost: 349 + init_stock: 63 + max_stock: 1000 + price: 523 + sale_gamma: 21 + service_level: 0.95 + SKU964: + constraint: null + cost: 244 + init_stock: 219 + max_stock: 1000 + price: 480 + sale_gamma: 73 + service_level: 0.95 + SKU965: + constraint: G(stock_constraint) + cost: 124 + init_stock: 255 + max_stock: 1000 + price: 199 + sale_gamma: 51 + service_level: 0.95 + SKU966: + constraint: G(stock_constraint) + cost: 302 + init_stock: 308 + max_stock: 1000 + price: 474 + sale_gamma: 44 + service_level: 0.95 + SKU967: + constraint: G(low_profit -> low_stock_constraint) + cost: 67 + init_stock: 270 + max_stock: 1000 + price: 111 + sale_gamma: 45 + service_level: 0.95 + SKU968: + constraint: G(stock_constraint) + cost: 281 + init_stock: 294 + max_stock: 1000 + price: 528 + sale_gamma: 49 + service_level: 0.95 + SKU969: + constraint: G(low_profit -> low_stock_constraint) + cost: 249 + init_stock: 24 + max_stock: 1000 + price: 328 + sale_gamma: 6 + service_level: 0.95 + SKU97: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 28 + init_stock: 180 + max_stock: 1000 + price: 43 + sale_gamma: 30 + service_level: 0.95 + SKU970: + constraint: null + cost: 244 + init_stock: 30 + max_stock: 1000 + price: 373 + sale_gamma: 5 + service_level: 0.95 + SKU971: + constraint: null + cost: 368 + init_stock: 219 + max_stock: 1000 + price: 426 + sale_gamma: 73 + service_level: 0.95 + SKU972: + constraint: G(stock_constraint) + cost: 209 + init_stock: 345 + max_stock: 1000 + price: 307 + sale_gamma: 69 + service_level: 0.95 + SKU973: + constraint: null + cost: 271 + init_stock: 33 + max_stock: 1000 + price: 447 + sale_gamma: 11 + service_level: 0.95 + SKU974: + constraint: null + cost: 170 + init_stock: 192 + max_stock: 1000 + price: 285 + sale_gamma: 32 + service_level: 0.95 + SKU975: + constraint: G(stock_constraint) + cost: 198 + init_stock: 88 + max_stock: 1000 + price: 334 + sale_gamma: 22 + service_level: 0.95 + SKU976: + constraint: G(stock_constraint) + cost: 178 + init_stock: 75 + max_stock: 1000 + price: 323 + sale_gamma: 15 + service_level: 0.95 + SKU977: + constraint: G(stock_constraint) + cost: 234 + init_stock: 324 + max_stock: 1000 + price: 269 + sale_gamma: 54 + service_level: 0.95 + SKU978: + constraint: G(stock_constraint) + cost: 470 + init_stock: 320 + max_stock: 1000 + price: 921 + sale_gamma: 64 + service_level: 0.95 + SKU979: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 443 + init_stock: 480 + max_stock: 1000 + price: 753 + sale_gamma: 96 + service_level: 0.95 + SKU98: + constraint: G(stock_constraint) + cost: 443 + init_stock: 70 + max_stock: 1000 + price: 527 + sale_gamma: 35 + service_level: 0.95 + SKU980: + constraint: null + cost: 39 + init_stock: 340 + max_stock: 1000 + price: 60 + sale_gamma: 68 + service_level: 0.95 + SKU981: + constraint: null + cost: 482 + init_stock: 360 + max_stock: 1000 + price: 607 + sale_gamma: 72 + service_level: 0.95 + SKU982: + constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) + cost: 213 + init_stock: 208 + max_stock: 1000 + price: 374 + sale_gamma: 52 + service_level: 0.95 + SKU983: + constraint: G(low_profit -> low_stock_constraint) + cost: 449 + init_stock: 276 + max_stock: 1000 + price: 776 + sale_gamma: 92 + service_level: 0.95 + SKU984: + constraint: G(stock_constraint) + cost: 232 + init_stock: 637 + max_stock: 1000 + price: 327 + sale_gamma: 91 + service_level: 0.95 + SKU985: + constraint: null + cost: 290 + init_stock: 153 + max_stock: 1000 + price: 420 + sale_gamma: 51 + service_level: 0.95 + SKU986: + constraint: null + cost: 275 + init_stock: 136 + max_stock: 1000 + price: 313 + sale_gamma: 17 + service_level: 0.95 + SKU987: + constraint: null + cost: 434 + init_stock: 204 + max_stock: 1000 + price: 516 + sale_gamma: 34 + service_level: 0.95 + SKU988: + constraint: null + cost: 102 + init_stock: 69 + max_stock: 1000 + price: 166 + sale_gamma: 23 + service_level: 0.95 + SKU989: + constraint: null + cost: 484 + init_stock: 558 + max_stock: 1000 + price: 653 + sale_gamma: 93 + service_level: 0.95 + SKU99: + constraint: null + cost: 361 + init_stock: 225 + max_stock: 1000 + price: 581 + sale_gamma: 45 + service_level: 0.95 + SKU990: + constraint: G(low_profit -> low_stock_constraint) + cost: 108 + init_stock: 228 + max_stock: 1000 + price: 174 + sale_gamma: 57 + service_level: 0.95 + SKU991: + constraint: null + cost: 409 + init_stock: 152 + max_stock: 1000 + price: 576 + sale_gamma: 19 + service_level: 0.95 + SKU992: + constraint: null + cost: 434 + init_stock: 651 + max_stock: 1000 + price: 685 + sale_gamma: 93 + service_level: 0.95 + SKU993: + constraint: G(low_profit -> low_stock_constraint) + cost: 145 + init_stock: 602 + max_stock: 1000 + price: 201 + sale_gamma: 86 + service_level: 0.95 + SKU994: + constraint: G(low_profit -> low_stock_constraint) + cost: 470 + init_stock: 300 + max_stock: 1000 + price: 916 + sale_gamma: 50 + service_level: 0.95 + SKU995: + constraint: null + cost: 241 + init_stock: 532 + max_stock: 1000 + price: 310 + sale_gamma: 76 + service_level: 0.95 + SKU996: + constraint: G(stock_constraint) + cost: 260 + init_stock: 438 + max_stock: 1000 + price: 416 + sale_gamma: 73 + service_level: 0.95 + SKU997: + constraint: G(stock_constraint) + cost: 400 + init_stock: 102 + max_stock: 1000 + price: 727 + sale_gamma: 34 + service_level: 0.95 + SKU998: + constraint: G(low_profit -> low_stock_constraint) + cost: 447 + init_stock: 165 + max_stock: 1000 + price: 688 + sale_gamma: 55 + service_level: 0.95 + SKU999: + constraint: null + cost: 79 + init_stock: 161 + max_stock: 1000 + price: 86 + sale_gamma: 23 + service_level: 0.95 + grid: + facilities: + STORE0: + - 9 + - 8 + SUPPLIER0: + - 14 + - 16 + WAREHOUSE0: + - 16 + - 18 + size: + - 20 + - 20 + skus: + - id: 0 + name: SKU0 + - id: 1 + name: SKU1 + - id: 2 + name: SKU2 + - id: 3 + name: SKU3 + - id: 4 + name: SKU4 + - id: 5 + name: SKU5 + - id: 6 + name: SKU6 + - id: 7 + name: SKU7 + - id: 8 + name: SKU8 + - id: 9 + name: SKU9 + - id: 10 + name: SKU10 + - id: 11 + name: SKU11 + - id: 12 + name: SKU12 + - id: 13 + name: SKU13 + - id: 14 + name: SKU14 + - id: 15 + name: SKU15 + - id: 16 + name: SKU16 + - id: 17 + name: SKU17 + - id: 18 + name: SKU18 + - id: 19 + name: SKU19 + - id: 20 + name: SKU20 + - id: 21 + name: SKU21 + - id: 22 + name: SKU22 + - id: 23 + name: SKU23 + - id: 24 + name: SKU24 + - id: 25 + name: SKU25 + - id: 26 + name: SKU26 + - id: 27 + name: SKU27 + - id: 28 + name: SKU28 + - id: 29 + name: SKU29 + - id: 30 + name: SKU30 + - id: 31 + name: SKU31 + - id: 32 + name: SKU32 + - id: 33 + name: SKU33 + - id: 34 + name: SKU34 + - id: 35 + name: SKU35 + - id: 36 + name: SKU36 + - id: 37 + name: SKU37 + - id: 38 + name: SKU38 + - id: 39 + name: SKU39 + - id: 40 + name: SKU40 + - id: 41 + name: SKU41 + - id: 42 + name: SKU42 + - id: 43 + name: SKU43 + - id: 44 + name: SKU44 + - id: 45 + name: SKU45 + - id: 46 + name: SKU46 + - id: 47 + name: SKU47 + - id: 48 + name: SKU48 + - id: 49 + name: SKU49 + - id: 50 + name: SKU50 + - id: 51 + name: SKU51 + - id: 52 + name: SKU52 + - id: 53 + name: SKU53 + - id: 54 + name: SKU54 + - id: 55 + name: SKU55 + - id: 56 + name: SKU56 + - id: 57 + name: SKU57 + - id: 58 + name: SKU58 + - id: 59 + name: SKU59 + - id: 60 + name: SKU60 + - id: 61 + name: SKU61 + - id: 62 + name: SKU62 + - id: 63 + name: SKU63 + - id: 64 + name: SKU64 + - id: 65 + name: SKU65 + - id: 66 + name: SKU66 + - id: 67 + name: SKU67 + - id: 68 + name: SKU68 + - id: 69 + name: SKU69 + - id: 70 + name: SKU70 + - id: 71 + name: SKU71 + - id: 72 + name: SKU72 + - id: 73 + name: SKU73 + - id: 74 + name: SKU74 + - id: 75 + name: SKU75 + - id: 76 + name: SKU76 + - id: 77 + name: SKU77 + - id: 78 + name: SKU78 + - id: 79 + name: SKU79 + - id: 80 + name: SKU80 + - id: 81 + name: SKU81 + - id: 82 + name: SKU82 + - id: 83 + name: SKU83 + - id: 84 + name: SKU84 + - id: 85 + name: SKU85 + - id: 86 + name: SKU86 + - id: 87 + name: SKU87 + - id: 88 + name: SKU88 + - id: 89 + name: SKU89 + - id: 90 + name: SKU90 + - id: 91 + name: SKU91 + - id: 92 + name: SKU92 + - id: 93 + name: SKU93 + - id: 94 + name: SKU94 + - id: 95 + name: SKU95 + - id: 96 + name: SKU96 + - id: 97 + name: SKU97 + - id: 98 + name: SKU98 + - id: 99 + name: SKU99 + - id: 100 + name: SKU100 + - id: 101 + name: SKU101 + - id: 102 + name: SKU102 + - id: 103 + name: SKU103 + - id: 104 + name: SKU104 + - id: 105 + name: SKU105 + - id: 106 + name: SKU106 + - id: 107 + name: SKU107 + - id: 108 + name: SKU108 + - id: 109 + name: SKU109 + - id: 110 + name: SKU110 + - id: 111 + name: SKU111 + - id: 112 + name: SKU112 + - id: 113 + name: SKU113 + - id: 114 + name: SKU114 + - id: 115 + name: SKU115 + - id: 116 + name: SKU116 + - id: 117 + name: SKU117 + - id: 118 + name: SKU118 + - id: 119 + name: SKU119 + - id: 120 + name: SKU120 + - id: 121 + name: SKU121 + - id: 122 + name: SKU122 + - id: 123 + name: SKU123 + - id: 124 + name: SKU124 + - id: 125 + name: SKU125 + - id: 126 + name: SKU126 + - id: 127 + name: SKU127 + - id: 128 + name: SKU128 + - id: 129 + name: SKU129 + - id: 130 + name: SKU130 + - id: 131 + name: SKU131 + - id: 132 + name: SKU132 + - id: 133 + name: SKU133 + - id: 134 + name: SKU134 + - id: 135 + name: SKU135 + - id: 136 + name: SKU136 + - id: 137 + name: SKU137 + - id: 138 + name: SKU138 + - id: 139 + name: SKU139 + - id: 140 + name: SKU140 + - id: 141 + name: SKU141 + - id: 142 + name: SKU142 + - id: 143 + name: SKU143 + - id: 144 + name: SKU144 + - id: 145 + name: SKU145 + - id: 146 + name: SKU146 + - id: 147 + name: SKU147 + - id: 148 + name: SKU148 + - id: 149 + name: SKU149 + - id: 150 + name: SKU150 + - id: 151 + name: SKU151 + - id: 152 + name: SKU152 + - id: 153 + name: SKU153 + - id: 154 + name: SKU154 + - id: 155 + name: SKU155 + - id: 156 + name: SKU156 + - id: 157 + name: SKU157 + - id: 158 + name: SKU158 + - id: 159 + name: SKU159 + - id: 160 + name: SKU160 + - id: 161 + name: SKU161 + - id: 162 + name: SKU162 + - id: 163 + name: SKU163 + - id: 164 + name: SKU164 + - id: 165 + name: SKU165 + - id: 166 + name: SKU166 + - id: 167 + name: SKU167 + - id: 168 + name: SKU168 + - id: 169 + name: SKU169 + - id: 170 + name: SKU170 + - id: 171 + name: SKU171 + - id: 172 + name: SKU172 + - id: 173 + name: SKU173 + - id: 174 + name: SKU174 + - id: 175 + name: SKU175 + - id: 176 + name: SKU176 + - id: 177 + name: SKU177 + - id: 178 + name: SKU178 + - id: 179 + name: SKU179 + - id: 180 + name: SKU180 + - id: 181 + name: SKU181 + - id: 182 + name: SKU182 + - id: 183 + name: SKU183 + - id: 184 + name: SKU184 + - id: 185 + name: SKU185 + - id: 186 + name: SKU186 + - id: 187 + name: SKU187 + - id: 188 + name: SKU188 + - id: 189 + name: SKU189 + - id: 190 + name: SKU190 + - id: 191 + name: SKU191 + - id: 192 + name: SKU192 + - id: 193 + name: SKU193 + - id: 194 + name: SKU194 + - id: 195 + name: SKU195 + - id: 196 + name: SKU196 + - id: 197 + name: SKU197 + - id: 198 + name: SKU198 + - id: 199 + name: SKU199 + - id: 200 + name: SKU200 + - id: 201 + name: SKU201 + - id: 202 + name: SKU202 + - id: 203 + name: SKU203 + - id: 204 + name: SKU204 + - id: 205 + name: SKU205 + - id: 206 + name: SKU206 + - id: 207 + name: SKU207 + - id: 208 + name: SKU208 + - id: 209 + name: SKU209 + - id: 210 + name: SKU210 + - id: 211 + name: SKU211 + - id: 212 + name: SKU212 + - id: 213 + name: SKU213 + - id: 214 + name: SKU214 + - id: 215 + name: SKU215 + - id: 216 + name: SKU216 + - id: 217 + name: SKU217 + - id: 218 + name: SKU218 + - id: 219 + name: SKU219 + - id: 220 + name: SKU220 + - id: 221 + name: SKU221 + - id: 222 + name: SKU222 + - id: 223 + name: SKU223 + - id: 224 + name: SKU224 + - id: 225 + name: SKU225 + - id: 226 + name: SKU226 + - id: 227 + name: SKU227 + - id: 228 + name: SKU228 + - id: 229 + name: SKU229 + - id: 230 + name: SKU230 + - id: 231 + name: SKU231 + - id: 232 + name: SKU232 + - id: 233 + name: SKU233 + - id: 234 + name: SKU234 + - id: 235 + name: SKU235 + - id: 236 + name: SKU236 + - id: 237 + name: SKU237 + - id: 238 + name: SKU238 + - id: 239 + name: SKU239 + - id: 240 + name: SKU240 + - id: 241 + name: SKU241 + - id: 242 + name: SKU242 + - id: 243 + name: SKU243 + - id: 244 + name: SKU244 + - id: 245 + name: SKU245 + - id: 246 + name: SKU246 + - id: 247 + name: SKU247 + - id: 248 + name: SKU248 + - id: 249 + name: SKU249 + - id: 250 + name: SKU250 + - id: 251 + name: SKU251 + - id: 252 + name: SKU252 + - id: 253 + name: SKU253 + - id: 254 + name: SKU254 + - id: 255 + name: SKU255 + - id: 256 + name: SKU256 + - id: 257 + name: SKU257 + - id: 258 + name: SKU258 + - id: 259 + name: SKU259 + - id: 260 + name: SKU260 + - id: 261 + name: SKU261 + - id: 262 + name: SKU262 + - id: 263 + name: SKU263 + - id: 264 + name: SKU264 + - id: 265 + name: SKU265 + - id: 266 + name: SKU266 + - id: 267 + name: SKU267 + - id: 268 + name: SKU268 + - id: 269 + name: SKU269 + - id: 270 + name: SKU270 + - id: 271 + name: SKU271 + - id: 272 + name: SKU272 + - id: 273 + name: SKU273 + - id: 274 + name: SKU274 + - id: 275 + name: SKU275 + - id: 276 + name: SKU276 + - id: 277 + name: SKU277 + - id: 278 + name: SKU278 + - id: 279 + name: SKU279 + - id: 280 + name: SKU280 + - id: 281 + name: SKU281 + - id: 282 + name: SKU282 + - id: 283 + name: SKU283 + - id: 284 + name: SKU284 + - id: 285 + name: SKU285 + - id: 286 + name: SKU286 + - id: 287 + name: SKU287 + - id: 288 + name: SKU288 + - id: 289 + name: SKU289 + - id: 290 + name: SKU290 + - id: 291 + name: SKU291 + - id: 292 + name: SKU292 + - id: 293 + name: SKU293 + - id: 294 + name: SKU294 + - id: 295 + name: SKU295 + - id: 296 + name: SKU296 + - id: 297 + name: SKU297 + - id: 298 + name: SKU298 + - id: 299 + name: SKU299 + - id: 300 + name: SKU300 + - id: 301 + name: SKU301 + - id: 302 + name: SKU302 + - id: 303 + name: SKU303 + - id: 304 + name: SKU304 + - id: 305 + name: SKU305 + - id: 306 + name: SKU306 + - id: 307 + name: SKU307 + - id: 308 + name: SKU308 + - id: 309 + name: SKU309 + - id: 310 + name: SKU310 + - id: 311 + name: SKU311 + - id: 312 + name: SKU312 + - id: 313 + name: SKU313 + - id: 314 + name: SKU314 + - id: 315 + name: SKU315 + - id: 316 + name: SKU316 + - id: 317 + name: SKU317 + - id: 318 + name: SKU318 + - id: 319 + name: SKU319 + - id: 320 + name: SKU320 + - id: 321 + name: SKU321 + - id: 322 + name: SKU322 + - id: 323 + name: SKU323 + - id: 324 + name: SKU324 + - id: 325 + name: SKU325 + - id: 326 + name: SKU326 + - id: 327 + name: SKU327 + - id: 328 + name: SKU328 + - id: 329 + name: SKU329 + - id: 330 + name: SKU330 + - id: 331 + name: SKU331 + - id: 332 + name: SKU332 + - id: 333 + name: SKU333 + - id: 334 + name: SKU334 + - id: 335 + name: SKU335 + - id: 336 + name: SKU336 + - id: 337 + name: SKU337 + - id: 338 + name: SKU338 + - id: 339 + name: SKU339 + - id: 340 + name: SKU340 + - id: 341 + name: SKU341 + - id: 342 + name: SKU342 + - id: 343 + name: SKU343 + - id: 344 + name: SKU344 + - id: 345 + name: SKU345 + - id: 346 + name: SKU346 + - id: 347 + name: SKU347 + - id: 348 + name: SKU348 + - id: 349 + name: SKU349 + - id: 350 + name: SKU350 + - id: 351 + name: SKU351 + - id: 352 + name: SKU352 + - id: 353 + name: SKU353 + - id: 354 + name: SKU354 + - id: 355 + name: SKU355 + - id: 356 + name: SKU356 + - id: 357 + name: SKU357 + - id: 358 + name: SKU358 + - id: 359 + name: SKU359 + - id: 360 + name: SKU360 + - id: 361 + name: SKU361 + - id: 362 + name: SKU362 + - id: 363 + name: SKU363 + - id: 364 + name: SKU364 + - id: 365 + name: SKU365 + - id: 366 + name: SKU366 + - id: 367 + name: SKU367 + - id: 368 + name: SKU368 + - id: 369 + name: SKU369 + - id: 370 + name: SKU370 + - id: 371 + name: SKU371 + - id: 372 + name: SKU372 + - id: 373 + name: SKU373 + - id: 374 + name: SKU374 + - id: 375 + name: SKU375 + - id: 376 + name: SKU376 + - id: 377 + name: SKU377 + - id: 378 + name: SKU378 + - id: 379 + name: SKU379 + - id: 380 + name: SKU380 + - id: 381 + name: SKU381 + - id: 382 + name: SKU382 + - id: 383 + name: SKU383 + - id: 384 + name: SKU384 + - id: 385 + name: SKU385 + - id: 386 + name: SKU386 + - id: 387 + name: SKU387 + - id: 388 + name: SKU388 + - id: 389 + name: SKU389 + - id: 390 + name: SKU390 + - id: 391 + name: SKU391 + - id: 392 + name: SKU392 + - id: 393 + name: SKU393 + - id: 394 + name: SKU394 + - id: 395 + name: SKU395 + - id: 396 + name: SKU396 + - id: 397 + name: SKU397 + - id: 398 + name: SKU398 + - id: 399 + name: SKU399 + - id: 400 + name: SKU400 + - id: 401 + name: SKU401 + - id: 402 + name: SKU402 + - id: 403 + name: SKU403 + - id: 404 + name: SKU404 + - id: 405 + name: SKU405 + - id: 406 + name: SKU406 + - id: 407 + name: SKU407 + - id: 408 + name: SKU408 + - id: 409 + name: SKU409 + - id: 410 + name: SKU410 + - id: 411 + name: SKU411 + - id: 412 + name: SKU412 + - id: 413 + name: SKU413 + - id: 414 + name: SKU414 + - id: 415 + name: SKU415 + - id: 416 + name: SKU416 + - id: 417 + name: SKU417 + - id: 418 + name: SKU418 + - id: 419 + name: SKU419 + - id: 420 + name: SKU420 + - id: 421 + name: SKU421 + - id: 422 + name: SKU422 + - id: 423 + name: SKU423 + - id: 424 + name: SKU424 + - id: 425 + name: SKU425 + - id: 426 + name: SKU426 + - id: 427 + name: SKU427 + - id: 428 + name: SKU428 + - id: 429 + name: SKU429 + - id: 430 + name: SKU430 + - id: 431 + name: SKU431 + - id: 432 + name: SKU432 + - id: 433 + name: SKU433 + - id: 434 + name: SKU434 + - id: 435 + name: SKU435 + - id: 436 + name: SKU436 + - id: 437 + name: SKU437 + - id: 438 + name: SKU438 + - id: 439 + name: SKU439 + - id: 440 + name: SKU440 + - id: 441 + name: SKU441 + - id: 442 + name: SKU442 + - id: 443 + name: SKU443 + - id: 444 + name: SKU444 + - id: 445 + name: SKU445 + - id: 446 + name: SKU446 + - id: 447 + name: SKU447 + - id: 448 + name: SKU448 + - id: 449 + name: SKU449 + - id: 450 + name: SKU450 + - id: 451 + name: SKU451 + - id: 452 + name: SKU452 + - id: 453 + name: SKU453 + - id: 454 + name: SKU454 + - id: 455 + name: SKU455 + - id: 456 + name: SKU456 + - id: 457 + name: SKU457 + - id: 458 + name: SKU458 + - id: 459 + name: SKU459 + - id: 460 + name: SKU460 + - id: 461 + name: SKU461 + - id: 462 + name: SKU462 + - id: 463 + name: SKU463 + - id: 464 + name: SKU464 + - id: 465 + name: SKU465 + - id: 466 + name: SKU466 + - id: 467 + name: SKU467 + - id: 468 + name: SKU468 + - id: 469 + name: SKU469 + - id: 470 + name: SKU470 + - id: 471 + name: SKU471 + - id: 472 + name: SKU472 + - id: 473 + name: SKU473 + - id: 474 + name: SKU474 + - id: 475 + name: SKU475 + - id: 476 + name: SKU476 + - id: 477 + name: SKU477 + - id: 478 + name: SKU478 + - id: 479 + name: SKU479 + - id: 480 + name: SKU480 + - id: 481 + name: SKU481 + - id: 482 + name: SKU482 + - id: 483 + name: SKU483 + - id: 484 + name: SKU484 + - id: 485 + name: SKU485 + - id: 486 + name: SKU486 + - id: 487 + name: SKU487 + - id: 488 + name: SKU488 + - id: 489 + name: SKU489 + - id: 490 + name: SKU490 + - id: 491 + name: SKU491 + - id: 492 + name: SKU492 + - id: 493 + name: SKU493 + - id: 494 + name: SKU494 + - id: 495 + name: SKU495 + - id: 496 + name: SKU496 + - id: 497 + name: SKU497 + - id: 498 + name: SKU498 + - id: 499 + name: SKU499 + - id: 500 + name: SKU500 + - id: 501 + name: SKU501 + - id: 502 + name: SKU502 + - id: 503 + name: SKU503 + - id: 504 + name: SKU504 + - id: 505 + name: SKU505 + - id: 506 + name: SKU506 + - id: 507 + name: SKU507 + - id: 508 + name: SKU508 + - id: 509 + name: SKU509 + - id: 510 + name: SKU510 + - id: 511 + name: SKU511 + - id: 512 + name: SKU512 + - id: 513 + name: SKU513 + - id: 514 + name: SKU514 + - id: 515 + name: SKU515 + - id: 516 + name: SKU516 + - id: 517 + name: SKU517 + - id: 518 + name: SKU518 + - id: 519 + name: SKU519 + - id: 520 + name: SKU520 + - id: 521 + name: SKU521 + - id: 522 + name: SKU522 + - id: 523 + name: SKU523 + - id: 524 + name: SKU524 + - id: 525 + name: SKU525 + - id: 526 + name: SKU526 + - id: 527 + name: SKU527 + - id: 528 + name: SKU528 + - id: 529 + name: SKU529 + - id: 530 + name: SKU530 + - id: 531 + name: SKU531 + - id: 532 + name: SKU532 + - id: 533 + name: SKU533 + - id: 534 + name: SKU534 + - id: 535 + name: SKU535 + - id: 536 + name: SKU536 + - id: 537 + name: SKU537 + - id: 538 + name: SKU538 + - id: 539 + name: SKU539 + - id: 540 + name: SKU540 + - id: 541 + name: SKU541 + - id: 542 + name: SKU542 + - id: 543 + name: SKU543 + - id: 544 + name: SKU544 + - id: 545 + name: SKU545 + - id: 546 + name: SKU546 + - id: 547 + name: SKU547 + - id: 548 + name: SKU548 + - id: 549 + name: SKU549 + - id: 550 + name: SKU550 + - id: 551 + name: SKU551 + - id: 552 + name: SKU552 + - id: 553 + name: SKU553 + - id: 554 + name: SKU554 + - id: 555 + name: SKU555 + - id: 556 + name: SKU556 + - id: 557 + name: SKU557 + - id: 558 + name: SKU558 + - id: 559 + name: SKU559 + - id: 560 + name: SKU560 + - id: 561 + name: SKU561 + - id: 562 + name: SKU562 + - id: 563 + name: SKU563 + - id: 564 + name: SKU564 + - id: 565 + name: SKU565 + - id: 566 + name: SKU566 + - id: 567 + name: SKU567 + - id: 568 + name: SKU568 + - id: 569 + name: SKU569 + - id: 570 + name: SKU570 + - id: 571 + name: SKU571 + - id: 572 + name: SKU572 + - id: 573 + name: SKU573 + - id: 574 + name: SKU574 + - id: 575 + name: SKU575 + - id: 576 + name: SKU576 + - id: 577 + name: SKU577 + - id: 578 + name: SKU578 + - id: 579 + name: SKU579 + - id: 580 + name: SKU580 + - id: 581 + name: SKU581 + - id: 582 + name: SKU582 + - id: 583 + name: SKU583 + - id: 584 + name: SKU584 + - id: 585 + name: SKU585 + - id: 586 + name: SKU586 + - id: 587 + name: SKU587 + - id: 588 + name: SKU588 + - id: 589 + name: SKU589 + - id: 590 + name: SKU590 + - id: 591 + name: SKU591 + - id: 592 + name: SKU592 + - id: 593 + name: SKU593 + - id: 594 + name: SKU594 + - id: 595 + name: SKU595 + - id: 596 + name: SKU596 + - id: 597 + name: SKU597 + - id: 598 + name: SKU598 + - id: 599 + name: SKU599 + - id: 600 + name: SKU600 + - id: 601 + name: SKU601 + - id: 602 + name: SKU602 + - id: 603 + name: SKU603 + - id: 604 + name: SKU604 + - id: 605 + name: SKU605 + - id: 606 + name: SKU606 + - id: 607 + name: SKU607 + - id: 608 + name: SKU608 + - id: 609 + name: SKU609 + - id: 610 + name: SKU610 + - id: 611 + name: SKU611 + - id: 612 + name: SKU612 + - id: 613 + name: SKU613 + - id: 614 + name: SKU614 + - id: 615 + name: SKU615 + - id: 616 + name: SKU616 + - id: 617 + name: SKU617 + - id: 618 + name: SKU618 + - id: 619 + name: SKU619 + - id: 620 + name: SKU620 + - id: 621 + name: SKU621 + - id: 622 + name: SKU622 + - id: 623 + name: SKU623 + - id: 624 + name: SKU624 + - id: 625 + name: SKU625 + - id: 626 + name: SKU626 + - id: 627 + name: SKU627 + - id: 628 + name: SKU628 + - id: 629 + name: SKU629 + - id: 630 + name: SKU630 + - id: 631 + name: SKU631 + - id: 632 + name: SKU632 + - id: 633 + name: SKU633 + - id: 634 + name: SKU634 + - id: 635 + name: SKU635 + - id: 636 + name: SKU636 + - id: 637 + name: SKU637 + - id: 638 + name: SKU638 + - id: 639 + name: SKU639 + - id: 640 + name: SKU640 + - id: 641 + name: SKU641 + - id: 642 + name: SKU642 + - id: 643 + name: SKU643 + - id: 644 + name: SKU644 + - id: 645 + name: SKU645 + - id: 646 + name: SKU646 + - id: 647 + name: SKU647 + - id: 648 + name: SKU648 + - id: 649 + name: SKU649 + - id: 650 + name: SKU650 + - id: 651 + name: SKU651 + - id: 652 + name: SKU652 + - id: 653 + name: SKU653 + - id: 654 + name: SKU654 + - id: 655 + name: SKU655 + - id: 656 + name: SKU656 + - id: 657 + name: SKU657 + - id: 658 + name: SKU658 + - id: 659 + name: SKU659 + - id: 660 + name: SKU660 + - id: 661 + name: SKU661 + - id: 662 + name: SKU662 + - id: 663 + name: SKU663 + - id: 664 + name: SKU664 + - id: 665 + name: SKU665 + - id: 666 + name: SKU666 + - id: 667 + name: SKU667 + - id: 668 + name: SKU668 + - id: 669 + name: SKU669 + - id: 670 + name: SKU670 + - id: 671 + name: SKU671 + - id: 672 + name: SKU672 + - id: 673 + name: SKU673 + - id: 674 + name: SKU674 + - id: 675 + name: SKU675 + - id: 676 + name: SKU676 + - id: 677 + name: SKU677 + - id: 678 + name: SKU678 + - id: 679 + name: SKU679 + - id: 680 + name: SKU680 + - id: 681 + name: SKU681 + - id: 682 + name: SKU682 + - id: 683 + name: SKU683 + - id: 684 + name: SKU684 + - id: 685 + name: SKU685 + - id: 686 + name: SKU686 + - id: 687 + name: SKU687 + - id: 688 + name: SKU688 + - id: 689 + name: SKU689 + - id: 690 + name: SKU690 + - id: 691 + name: SKU691 + - id: 692 + name: SKU692 + - id: 693 + name: SKU693 + - id: 694 + name: SKU694 + - id: 695 + name: SKU695 + - id: 696 + name: SKU696 + - id: 697 + name: SKU697 + - id: 698 + name: SKU698 + - id: 699 + name: SKU699 + - id: 700 + name: SKU700 + - id: 701 + name: SKU701 + - id: 702 + name: SKU702 + - id: 703 + name: SKU703 + - id: 704 + name: SKU704 + - id: 705 + name: SKU705 + - id: 706 + name: SKU706 + - id: 707 + name: SKU707 + - id: 708 + name: SKU708 + - id: 709 + name: SKU709 + - id: 710 + name: SKU710 + - id: 711 + name: SKU711 + - id: 712 + name: SKU712 + - id: 713 + name: SKU713 + - id: 714 + name: SKU714 + - id: 715 + name: SKU715 + - id: 716 + name: SKU716 + - id: 717 + name: SKU717 + - id: 718 + name: SKU718 + - id: 719 + name: SKU719 + - id: 720 + name: SKU720 + - id: 721 + name: SKU721 + - id: 722 + name: SKU722 + - id: 723 + name: SKU723 + - id: 724 + name: SKU724 + - id: 725 + name: SKU725 + - id: 726 + name: SKU726 + - id: 727 + name: SKU727 + - id: 728 + name: SKU728 + - id: 729 + name: SKU729 + - id: 730 + name: SKU730 + - id: 731 + name: SKU731 + - id: 732 + name: SKU732 + - id: 733 + name: SKU733 + - id: 734 + name: SKU734 + - id: 735 + name: SKU735 + - id: 736 + name: SKU736 + - id: 737 + name: SKU737 + - id: 738 + name: SKU738 + - id: 739 + name: SKU739 + - id: 740 + name: SKU740 + - id: 741 + name: SKU741 + - id: 742 + name: SKU742 + - id: 743 + name: SKU743 + - id: 744 + name: SKU744 + - id: 745 + name: SKU745 + - id: 746 + name: SKU746 + - id: 747 + name: SKU747 + - id: 748 + name: SKU748 + - id: 749 + name: SKU749 + - id: 750 + name: SKU750 + - id: 751 + name: SKU751 + - id: 752 + name: SKU752 + - id: 753 + name: SKU753 + - id: 754 + name: SKU754 + - id: 755 + name: SKU755 + - id: 756 + name: SKU756 + - id: 757 + name: SKU757 + - id: 758 + name: SKU758 + - id: 759 + name: SKU759 + - id: 760 + name: SKU760 + - id: 761 + name: SKU761 + - id: 762 + name: SKU762 + - id: 763 + name: SKU763 + - id: 764 + name: SKU764 + - id: 765 + name: SKU765 + - id: 766 + name: SKU766 + - id: 767 + name: SKU767 + - id: 768 + name: SKU768 + - id: 769 + name: SKU769 + - id: 770 + name: SKU770 + - id: 771 + name: SKU771 + - id: 772 + name: SKU772 + - id: 773 + name: SKU773 + - id: 774 + name: SKU774 + - id: 775 + name: SKU775 + - id: 776 + name: SKU776 + - id: 777 + name: SKU777 + - id: 778 + name: SKU778 + - id: 779 + name: SKU779 + - id: 780 + name: SKU780 + - id: 781 + name: SKU781 + - id: 782 + name: SKU782 + - id: 783 + name: SKU783 + - id: 784 + name: SKU784 + - id: 785 + name: SKU785 + - id: 786 + name: SKU786 + - id: 787 + name: SKU787 + - id: 788 + name: SKU788 + - id: 789 + name: SKU789 + - id: 790 + name: SKU790 + - id: 791 + name: SKU791 + - id: 792 + name: SKU792 + - id: 793 + name: SKU793 + - id: 794 + name: SKU794 + - id: 795 + name: SKU795 + - id: 796 + name: SKU796 + - id: 797 + name: SKU797 + - id: 798 + name: SKU798 + - id: 799 + name: SKU799 + - id: 800 + name: SKU800 + - id: 801 + name: SKU801 + - id: 802 + name: SKU802 + - id: 803 + name: SKU803 + - id: 804 + name: SKU804 + - id: 805 + name: SKU805 + - id: 806 + name: SKU806 + - id: 807 + name: SKU807 + - id: 808 + name: SKU808 + - id: 809 + name: SKU809 + - id: 810 + name: SKU810 + - id: 811 + name: SKU811 + - id: 812 + name: SKU812 + - id: 813 + name: SKU813 + - id: 814 + name: SKU814 + - id: 815 + name: SKU815 + - id: 816 + name: SKU816 + - id: 817 + name: SKU817 + - id: 818 + name: SKU818 + - id: 819 + name: SKU819 + - id: 820 + name: SKU820 + - id: 821 + name: SKU821 + - id: 822 + name: SKU822 + - id: 823 + name: SKU823 + - id: 824 + name: SKU824 + - id: 825 + name: SKU825 + - id: 826 + name: SKU826 + - id: 827 + name: SKU827 + - id: 828 + name: SKU828 + - id: 829 + name: SKU829 + - id: 830 + name: SKU830 + - id: 831 + name: SKU831 + - id: 832 + name: SKU832 + - id: 833 + name: SKU833 + - id: 834 + name: SKU834 + - id: 835 + name: SKU835 + - id: 836 + name: SKU836 + - id: 837 + name: SKU837 + - id: 838 + name: SKU838 + - id: 839 + name: SKU839 + - id: 840 + name: SKU840 + - id: 841 + name: SKU841 + - id: 842 + name: SKU842 + - id: 843 + name: SKU843 + - id: 844 + name: SKU844 + - id: 845 + name: SKU845 + - id: 846 + name: SKU846 + - id: 847 + name: SKU847 + - id: 848 + name: SKU848 + - id: 849 + name: SKU849 + - id: 850 + name: SKU850 + - id: 851 + name: SKU851 + - id: 852 + name: SKU852 + - id: 853 + name: SKU853 + - id: 854 + name: SKU854 + - id: 855 + name: SKU855 + - id: 856 + name: SKU856 + - id: 857 + name: SKU857 + - id: 858 + name: SKU858 + - id: 859 + name: SKU859 + - id: 860 + name: SKU860 + - id: 861 + name: SKU861 + - id: 862 + name: SKU862 + - id: 863 + name: SKU863 + - id: 864 + name: SKU864 + - id: 865 + name: SKU865 + - id: 866 + name: SKU866 + - id: 867 + name: SKU867 + - id: 868 + name: SKU868 + - id: 869 + name: SKU869 + - id: 870 + name: SKU870 + - id: 871 + name: SKU871 + - id: 872 + name: SKU872 + - id: 873 + name: SKU873 + - id: 874 + name: SKU874 + - id: 875 + name: SKU875 + - id: 876 + name: SKU876 + - id: 877 + name: SKU877 + - id: 878 + name: SKU878 + - id: 879 + name: SKU879 + - id: 880 + name: SKU880 + - id: 881 + name: SKU881 + - id: 882 + name: SKU882 + - id: 883 + name: SKU883 + - id: 884 + name: SKU884 + - id: 885 + name: SKU885 + - id: 886 + name: SKU886 + - id: 887 + name: SKU887 + - id: 888 + name: SKU888 + - id: 889 + name: SKU889 + - id: 890 + name: SKU890 + - id: 891 + name: SKU891 + - id: 892 + name: SKU892 + - id: 893 + name: SKU893 + - id: 894 + name: SKU894 + - id: 895 + name: SKU895 + - id: 896 + name: SKU896 + - id: 897 + name: SKU897 + - id: 898 + name: SKU898 + - id: 899 + name: SKU899 + - id: 900 + name: SKU900 + - id: 901 + name: SKU901 + - id: 902 + name: SKU902 + - id: 903 + name: SKU903 + - id: 904 + name: SKU904 + - id: 905 + name: SKU905 + - id: 906 + name: SKU906 + - id: 907 + name: SKU907 + - id: 908 + name: SKU908 + - id: 909 + name: SKU909 + - id: 910 + name: SKU910 + - id: 911 + name: SKU911 + - id: 912 + name: SKU912 + - id: 913 + name: SKU913 + - id: 914 + name: SKU914 + - id: 915 + name: SKU915 + - id: 916 + name: SKU916 + - id: 917 + name: SKU917 + - id: 918 + name: SKU918 + - id: 919 + name: SKU919 + - id: 920 + name: SKU920 + - id: 921 + name: SKU921 + - id: 922 + name: SKU922 + - id: 923 + name: SKU923 + - id: 924 + name: SKU924 + - id: 925 + name: SKU925 + - id: 926 + name: SKU926 + - id: 927 + name: SKU927 + - id: 928 + name: SKU928 + - id: 929 + name: SKU929 + - id: 930 + name: SKU930 + - id: 931 + name: SKU931 + - id: 932 + name: SKU932 + - id: 933 + name: SKU933 + - id: 934 + name: SKU934 + - id: 935 + name: SKU935 + - id: 936 + name: SKU936 + - id: 937 + name: SKU937 + - id: 938 + name: SKU938 + - id: 939 + name: SKU939 + - id: 940 + name: SKU940 + - id: 941 + name: SKU941 + - id: 942 + name: SKU942 + - id: 943 + name: SKU943 + - id: 944 + name: SKU944 + - id: 945 + name: SKU945 + - id: 946 + name: SKU946 + - id: 947 + name: SKU947 + - id: 948 + name: SKU948 + - id: 949 + name: SKU949 + - id: 950 + name: SKU950 + - id: 951 + name: SKU951 + - id: 952 + name: SKU952 + - id: 953 + name: SKU953 + - id: 954 + name: SKU954 + - id: 955 + name: SKU955 + - id: 956 + name: SKU956 + - id: 957 + name: SKU957 + - id: 958 + name: SKU958 + - id: 959 + name: SKU959 + - id: 960 + name: SKU960 + - id: 961 + name: SKU961 + - id: 962 + name: SKU962 + - id: 963 + name: SKU963 + - id: 964 + name: SKU964 + - id: 965 + name: SKU965 + - id: 966 + name: SKU966 + - id: 967 + name: SKU967 + - id: 968 + name: SKU968 + - id: 969 + name: SKU969 + - id: 970 + name: SKU970 + - id: 971 + name: SKU971 + - id: 972 + name: SKU972 + - id: 973 + name: SKU973 + - id: 974 + name: SKU974 + - id: 975 + name: SKU975 + - id: 976 + name: SKU976 + - id: 977 + name: SKU977 + - id: 978 + name: SKU978 + - id: 979 + name: SKU979 + - id: 980 + name: SKU980 + - id: 981 + name: SKU981 + - id: 982 + name: SKU982 + - id: 983 + name: SKU983 + - id: 984 + name: SKU984 + - id: 985 + name: SKU985 + - id: 986 + name: SKU986 + - id: 987 + name: SKU987 + - id: 988 + name: SKU988 + - id: 989 + name: SKU989 + - id: 990 + name: SKU990 + - id: 991 + name: SKU991 + - id: 992 + name: SKU992 + - id: 993 + name: SKU993 + - id: 994 + name: SKU994 + - id: 995 + name: SKU995 + - id: 996 + name: SKU996 + - id: 997 + name: SKU997 + - id: 998 + name: SKU998 + - id: 999 + name: SKU999 + topology: + STORE0: + SKU0: + - WAREHOUSE0 + SKU1: + - WAREHOUSE0 + SKU10: + - WAREHOUSE0 + SKU100: + - WAREHOUSE0 + SKU101: + - WAREHOUSE0 + SKU102: + - WAREHOUSE0 + SKU103: + - WAREHOUSE0 + SKU104: + - WAREHOUSE0 + SKU105: + - WAREHOUSE0 + SKU106: + - WAREHOUSE0 + SKU107: + - WAREHOUSE0 + SKU108: + - WAREHOUSE0 + SKU109: + - WAREHOUSE0 + SKU11: + - WAREHOUSE0 + SKU110: + - WAREHOUSE0 + SKU111: + - WAREHOUSE0 + SKU112: + - WAREHOUSE0 + SKU113: + - WAREHOUSE0 + SKU114: + - WAREHOUSE0 + SKU115: + - WAREHOUSE0 + SKU116: + - WAREHOUSE0 + SKU117: + - WAREHOUSE0 + SKU118: + - WAREHOUSE0 + SKU119: + - WAREHOUSE0 + SKU12: + - WAREHOUSE0 + SKU120: + - WAREHOUSE0 + SKU121: + - WAREHOUSE0 + SKU122: + - WAREHOUSE0 + SKU123: + - WAREHOUSE0 + SKU124: + - WAREHOUSE0 + SKU125: + - WAREHOUSE0 + SKU126: + - WAREHOUSE0 + SKU127: + - WAREHOUSE0 + SKU128: + - WAREHOUSE0 + SKU129: + - WAREHOUSE0 + SKU13: + - WAREHOUSE0 + SKU130: + - WAREHOUSE0 + SKU131: + - WAREHOUSE0 + SKU132: + - WAREHOUSE0 + SKU133: + - WAREHOUSE0 + SKU134: + - WAREHOUSE0 + SKU135: + - WAREHOUSE0 + SKU136: + - WAREHOUSE0 + SKU137: + - WAREHOUSE0 + SKU138: + - WAREHOUSE0 + SKU139: + - WAREHOUSE0 + SKU14: + - WAREHOUSE0 + SKU140: + - WAREHOUSE0 + SKU141: + - WAREHOUSE0 + SKU142: + - WAREHOUSE0 + SKU143: + - WAREHOUSE0 + SKU144: + - WAREHOUSE0 + SKU145: + - WAREHOUSE0 + SKU146: + - WAREHOUSE0 + SKU147: + - WAREHOUSE0 + SKU148: + - WAREHOUSE0 + SKU149: + - WAREHOUSE0 + SKU15: + - WAREHOUSE0 + SKU150: + - WAREHOUSE0 + SKU151: + - WAREHOUSE0 + SKU152: + - WAREHOUSE0 + SKU153: + - WAREHOUSE0 + SKU154: + - WAREHOUSE0 + SKU155: + - WAREHOUSE0 + SKU156: + - WAREHOUSE0 + SKU157: + - WAREHOUSE0 + SKU158: + - WAREHOUSE0 + SKU159: + - WAREHOUSE0 + SKU16: + - WAREHOUSE0 + SKU160: + - WAREHOUSE0 + SKU161: + - WAREHOUSE0 + SKU162: + - WAREHOUSE0 + SKU163: + - WAREHOUSE0 + SKU164: + - WAREHOUSE0 + SKU165: + - WAREHOUSE0 + SKU166: + - WAREHOUSE0 + SKU167: + - WAREHOUSE0 + SKU168: + - WAREHOUSE0 + SKU169: + - WAREHOUSE0 + SKU17: + - WAREHOUSE0 + SKU170: + - WAREHOUSE0 + SKU171: + - WAREHOUSE0 + SKU172: + - WAREHOUSE0 + SKU173: + - WAREHOUSE0 + SKU174: + - WAREHOUSE0 + SKU175: + - WAREHOUSE0 + SKU176: + - WAREHOUSE0 + SKU177: + - WAREHOUSE0 + SKU178: + - WAREHOUSE0 + SKU179: + - WAREHOUSE0 + SKU18: + - WAREHOUSE0 + SKU180: + - WAREHOUSE0 + SKU181: + - WAREHOUSE0 + SKU182: + - WAREHOUSE0 + SKU183: + - WAREHOUSE0 + SKU184: + - WAREHOUSE0 + SKU185: + - WAREHOUSE0 + SKU186: + - WAREHOUSE0 + SKU187: + - WAREHOUSE0 + SKU188: + - WAREHOUSE0 + SKU189: + - WAREHOUSE0 + SKU19: + - WAREHOUSE0 + SKU190: + - WAREHOUSE0 + SKU191: + - WAREHOUSE0 + SKU192: + - WAREHOUSE0 + SKU193: + - WAREHOUSE0 + SKU194: + - WAREHOUSE0 + SKU195: + - WAREHOUSE0 + SKU196: + - WAREHOUSE0 + SKU197: + - WAREHOUSE0 + SKU198: + - WAREHOUSE0 + SKU199: + - WAREHOUSE0 + SKU2: + - WAREHOUSE0 + SKU20: + - WAREHOUSE0 + SKU200: + - WAREHOUSE0 + SKU201: + - WAREHOUSE0 + SKU202: + - WAREHOUSE0 + SKU203: + - WAREHOUSE0 + SKU204: + - WAREHOUSE0 + SKU205: + - WAREHOUSE0 + SKU206: + - WAREHOUSE0 + SKU207: + - WAREHOUSE0 + SKU208: + - WAREHOUSE0 + SKU209: + - WAREHOUSE0 + SKU21: + - WAREHOUSE0 + SKU210: + - WAREHOUSE0 + SKU211: + - WAREHOUSE0 + SKU212: + - WAREHOUSE0 + SKU213: + - WAREHOUSE0 + SKU214: + - WAREHOUSE0 + SKU215: + - WAREHOUSE0 + SKU216: + - WAREHOUSE0 + SKU217: + - WAREHOUSE0 + SKU218: + - WAREHOUSE0 + SKU219: + - WAREHOUSE0 + SKU22: + - WAREHOUSE0 + SKU220: + - WAREHOUSE0 + SKU221: + - WAREHOUSE0 + SKU222: + - WAREHOUSE0 + SKU223: + - WAREHOUSE0 + SKU224: + - WAREHOUSE0 + SKU225: + - WAREHOUSE0 + SKU226: + - WAREHOUSE0 + SKU227: + - WAREHOUSE0 + SKU228: + - WAREHOUSE0 + SKU229: + - WAREHOUSE0 + SKU23: + - WAREHOUSE0 + SKU230: + - WAREHOUSE0 + SKU231: + - WAREHOUSE0 + SKU232: + - WAREHOUSE0 + SKU233: + - WAREHOUSE0 + SKU234: + - WAREHOUSE0 + SKU235: + - WAREHOUSE0 + SKU236: + - WAREHOUSE0 + SKU237: + - WAREHOUSE0 + SKU238: + - WAREHOUSE0 + SKU239: + - WAREHOUSE0 + SKU24: + - WAREHOUSE0 + SKU240: + - WAREHOUSE0 + SKU241: + - WAREHOUSE0 + SKU242: + - WAREHOUSE0 + SKU243: + - WAREHOUSE0 + SKU244: + - WAREHOUSE0 + SKU245: + - WAREHOUSE0 + SKU246: + - WAREHOUSE0 + SKU247: + - WAREHOUSE0 + SKU248: + - WAREHOUSE0 + SKU249: + - WAREHOUSE0 + SKU25: + - WAREHOUSE0 + SKU250: + - WAREHOUSE0 + SKU251: + - WAREHOUSE0 + SKU252: + - WAREHOUSE0 + SKU253: + - WAREHOUSE0 + SKU254: + - WAREHOUSE0 + SKU255: + - WAREHOUSE0 + SKU256: + - WAREHOUSE0 + SKU257: + - WAREHOUSE0 + SKU258: + - WAREHOUSE0 + SKU259: + - WAREHOUSE0 + SKU26: + - WAREHOUSE0 + SKU260: + - WAREHOUSE0 + SKU261: + - WAREHOUSE0 + SKU262: + - WAREHOUSE0 + SKU263: + - WAREHOUSE0 + SKU264: + - WAREHOUSE0 + SKU265: + - WAREHOUSE0 + SKU266: + - WAREHOUSE0 + SKU267: + - WAREHOUSE0 + SKU268: + - WAREHOUSE0 + SKU269: + - WAREHOUSE0 + SKU27: + - WAREHOUSE0 + SKU270: + - WAREHOUSE0 + SKU271: + - WAREHOUSE0 + SKU272: + - WAREHOUSE0 + SKU273: + - WAREHOUSE0 + SKU274: + - WAREHOUSE0 + SKU275: + - WAREHOUSE0 + SKU276: + - WAREHOUSE0 + SKU277: + - WAREHOUSE0 + SKU278: + - WAREHOUSE0 + SKU279: + - WAREHOUSE0 + SKU28: + - WAREHOUSE0 + SKU280: + - WAREHOUSE0 + SKU281: + - WAREHOUSE0 + SKU282: + - WAREHOUSE0 + SKU283: + - WAREHOUSE0 + SKU284: + - WAREHOUSE0 + SKU285: + - WAREHOUSE0 + SKU286: + - WAREHOUSE0 + SKU287: + - WAREHOUSE0 + SKU288: + - WAREHOUSE0 + SKU289: + - WAREHOUSE0 + SKU29: + - WAREHOUSE0 + SKU290: + - WAREHOUSE0 + SKU291: + - WAREHOUSE0 + SKU292: + - WAREHOUSE0 + SKU293: + - WAREHOUSE0 + SKU294: + - WAREHOUSE0 + SKU295: + - WAREHOUSE0 + SKU296: + - WAREHOUSE0 + SKU297: + - WAREHOUSE0 + SKU298: + - WAREHOUSE0 + SKU299: + - WAREHOUSE0 + SKU3: + - WAREHOUSE0 + SKU30: + - WAREHOUSE0 + SKU300: + - WAREHOUSE0 + SKU301: + - WAREHOUSE0 + SKU302: + - WAREHOUSE0 + SKU303: + - WAREHOUSE0 + SKU304: + - WAREHOUSE0 + SKU305: + - WAREHOUSE0 + SKU306: + - WAREHOUSE0 + SKU307: + - WAREHOUSE0 + SKU308: + - WAREHOUSE0 + SKU309: + - WAREHOUSE0 + SKU31: + - WAREHOUSE0 + SKU310: + - WAREHOUSE0 + SKU311: + - WAREHOUSE0 + SKU312: + - WAREHOUSE0 + SKU313: + - WAREHOUSE0 + SKU314: + - WAREHOUSE0 + SKU315: + - WAREHOUSE0 + SKU316: + - WAREHOUSE0 + SKU317: + - WAREHOUSE0 + SKU318: + - WAREHOUSE0 + SKU319: + - WAREHOUSE0 + SKU32: + - WAREHOUSE0 + SKU320: + - WAREHOUSE0 + SKU321: + - WAREHOUSE0 + SKU322: + - WAREHOUSE0 + SKU323: + - WAREHOUSE0 + SKU324: + - WAREHOUSE0 + SKU325: + - WAREHOUSE0 + SKU326: + - WAREHOUSE0 + SKU327: + - WAREHOUSE0 + SKU328: + - WAREHOUSE0 + SKU329: + - WAREHOUSE0 + SKU33: + - WAREHOUSE0 + SKU330: + - WAREHOUSE0 + SKU331: + - WAREHOUSE0 + SKU332: + - WAREHOUSE0 + SKU333: + - WAREHOUSE0 + SKU334: + - WAREHOUSE0 + SKU335: + - WAREHOUSE0 + SKU336: + - WAREHOUSE0 + SKU337: + - WAREHOUSE0 + SKU338: + - WAREHOUSE0 + SKU339: + - WAREHOUSE0 + SKU34: + - WAREHOUSE0 + SKU340: + - WAREHOUSE0 + SKU341: + - WAREHOUSE0 + SKU342: + - WAREHOUSE0 + SKU343: + - WAREHOUSE0 + SKU344: + - WAREHOUSE0 + SKU345: + - WAREHOUSE0 + SKU346: + - WAREHOUSE0 + SKU347: + - WAREHOUSE0 + SKU348: + - WAREHOUSE0 + SKU349: + - WAREHOUSE0 + SKU35: + - WAREHOUSE0 + SKU350: + - WAREHOUSE0 + SKU351: + - WAREHOUSE0 + SKU352: + - WAREHOUSE0 + SKU353: + - WAREHOUSE0 + SKU354: + - WAREHOUSE0 + SKU355: + - WAREHOUSE0 + SKU356: + - WAREHOUSE0 + SKU357: + - WAREHOUSE0 + SKU358: + - WAREHOUSE0 + SKU359: + - WAREHOUSE0 + SKU36: + - WAREHOUSE0 + SKU360: + - WAREHOUSE0 + SKU361: + - WAREHOUSE0 + SKU362: + - WAREHOUSE0 + SKU363: + - WAREHOUSE0 + SKU364: + - WAREHOUSE0 + SKU365: + - WAREHOUSE0 + SKU366: + - WAREHOUSE0 + SKU367: + - WAREHOUSE0 + SKU368: + - WAREHOUSE0 + SKU369: + - WAREHOUSE0 + SKU37: + - WAREHOUSE0 + SKU370: + - WAREHOUSE0 + SKU371: + - WAREHOUSE0 + SKU372: + - WAREHOUSE0 + SKU373: + - WAREHOUSE0 + SKU374: + - WAREHOUSE0 + SKU375: + - WAREHOUSE0 + SKU376: + - WAREHOUSE0 + SKU377: + - WAREHOUSE0 + SKU378: + - WAREHOUSE0 + SKU379: + - WAREHOUSE0 + SKU38: + - WAREHOUSE0 + SKU380: + - WAREHOUSE0 + SKU381: + - WAREHOUSE0 + SKU382: + - WAREHOUSE0 + SKU383: + - WAREHOUSE0 + SKU384: + - WAREHOUSE0 + SKU385: + - WAREHOUSE0 + SKU386: + - WAREHOUSE0 + SKU387: + - WAREHOUSE0 + SKU388: + - WAREHOUSE0 + SKU389: + - WAREHOUSE0 + SKU39: + - WAREHOUSE0 + SKU390: + - WAREHOUSE0 + SKU391: + - WAREHOUSE0 + SKU392: + - WAREHOUSE0 + SKU393: + - WAREHOUSE0 + SKU394: + - WAREHOUSE0 + SKU395: + - WAREHOUSE0 + SKU396: + - WAREHOUSE0 + SKU397: + - WAREHOUSE0 + SKU398: + - WAREHOUSE0 + SKU399: + - WAREHOUSE0 + SKU4: + - WAREHOUSE0 + SKU40: + - WAREHOUSE0 + SKU400: + - WAREHOUSE0 + SKU401: + - WAREHOUSE0 + SKU402: + - WAREHOUSE0 + SKU403: + - WAREHOUSE0 + SKU404: + - WAREHOUSE0 + SKU405: + - WAREHOUSE0 + SKU406: + - WAREHOUSE0 + SKU407: + - WAREHOUSE0 + SKU408: + - WAREHOUSE0 + SKU409: + - WAREHOUSE0 + SKU41: + - WAREHOUSE0 + SKU410: + - WAREHOUSE0 + SKU411: + - WAREHOUSE0 + SKU412: + - WAREHOUSE0 + SKU413: + - WAREHOUSE0 + SKU414: + - WAREHOUSE0 + SKU415: + - WAREHOUSE0 + SKU416: + - WAREHOUSE0 + SKU417: + - WAREHOUSE0 + SKU418: + - WAREHOUSE0 + SKU419: + - WAREHOUSE0 + SKU42: + - WAREHOUSE0 + SKU420: + - WAREHOUSE0 + SKU421: + - WAREHOUSE0 + SKU422: + - WAREHOUSE0 + SKU423: + - WAREHOUSE0 + SKU424: + - WAREHOUSE0 + SKU425: + - WAREHOUSE0 + SKU426: + - WAREHOUSE0 + SKU427: + - WAREHOUSE0 + SKU428: + - WAREHOUSE0 + SKU429: + - WAREHOUSE0 + SKU43: + - WAREHOUSE0 + SKU430: + - WAREHOUSE0 + SKU431: + - WAREHOUSE0 + SKU432: + - WAREHOUSE0 + SKU433: + - WAREHOUSE0 + SKU434: + - WAREHOUSE0 + SKU435: + - WAREHOUSE0 + SKU436: + - WAREHOUSE0 + SKU437: + - WAREHOUSE0 + SKU438: + - WAREHOUSE0 + SKU439: + - WAREHOUSE0 + SKU44: + - WAREHOUSE0 + SKU440: + - WAREHOUSE0 + SKU441: + - WAREHOUSE0 + SKU442: + - WAREHOUSE0 + SKU443: + - WAREHOUSE0 + SKU444: + - WAREHOUSE0 + SKU445: + - WAREHOUSE0 + SKU446: + - WAREHOUSE0 + SKU447: + - WAREHOUSE0 + SKU448: + - WAREHOUSE0 + SKU449: + - WAREHOUSE0 + SKU45: + - WAREHOUSE0 + SKU450: + - WAREHOUSE0 + SKU451: + - WAREHOUSE0 + SKU452: + - WAREHOUSE0 + SKU453: + - WAREHOUSE0 + SKU454: + - WAREHOUSE0 + SKU455: + - WAREHOUSE0 + SKU456: + - WAREHOUSE0 + SKU457: + - WAREHOUSE0 + SKU458: + - WAREHOUSE0 + SKU459: + - WAREHOUSE0 + SKU46: + - WAREHOUSE0 + SKU460: + - WAREHOUSE0 + SKU461: + - WAREHOUSE0 + SKU462: + - WAREHOUSE0 + SKU463: + - WAREHOUSE0 + SKU464: + - WAREHOUSE0 + SKU465: + - WAREHOUSE0 + SKU466: + - WAREHOUSE0 + SKU467: + - WAREHOUSE0 + SKU468: + - WAREHOUSE0 + SKU469: + - WAREHOUSE0 + SKU47: + - WAREHOUSE0 + SKU470: + - WAREHOUSE0 + SKU471: + - WAREHOUSE0 + SKU472: + - WAREHOUSE0 + SKU473: + - WAREHOUSE0 + SKU474: + - WAREHOUSE0 + SKU475: + - WAREHOUSE0 + SKU476: + - WAREHOUSE0 + SKU477: + - WAREHOUSE0 + SKU478: + - WAREHOUSE0 + SKU479: + - WAREHOUSE0 + SKU48: + - WAREHOUSE0 + SKU480: + - WAREHOUSE0 + SKU481: + - WAREHOUSE0 + SKU482: + - WAREHOUSE0 + SKU483: + - WAREHOUSE0 + SKU484: + - WAREHOUSE0 + SKU485: + - WAREHOUSE0 + SKU486: + - WAREHOUSE0 + SKU487: + - WAREHOUSE0 + SKU488: + - WAREHOUSE0 + SKU489: + - WAREHOUSE0 + SKU49: + - WAREHOUSE0 + SKU490: + - WAREHOUSE0 + SKU491: + - WAREHOUSE0 + SKU492: + - WAREHOUSE0 + SKU493: + - WAREHOUSE0 + SKU494: + - WAREHOUSE0 + SKU495: + - WAREHOUSE0 + SKU496: + - WAREHOUSE0 + SKU497: + - WAREHOUSE0 + SKU498: + - WAREHOUSE0 + SKU499: + - WAREHOUSE0 + SKU5: + - WAREHOUSE0 + SKU50: + - WAREHOUSE0 + SKU500: + - WAREHOUSE0 + SKU501: + - WAREHOUSE0 + SKU502: + - WAREHOUSE0 + SKU503: + - WAREHOUSE0 + SKU504: + - WAREHOUSE0 + SKU505: + - WAREHOUSE0 + SKU506: + - WAREHOUSE0 + SKU507: + - WAREHOUSE0 + SKU508: + - WAREHOUSE0 + SKU509: + - WAREHOUSE0 + SKU51: + - WAREHOUSE0 + SKU510: + - WAREHOUSE0 + SKU511: + - WAREHOUSE0 + SKU512: + - WAREHOUSE0 + SKU513: + - WAREHOUSE0 + SKU514: + - WAREHOUSE0 + SKU515: + - WAREHOUSE0 + SKU516: + - WAREHOUSE0 + SKU517: + - WAREHOUSE0 + SKU518: + - WAREHOUSE0 + SKU519: + - WAREHOUSE0 + SKU52: + - WAREHOUSE0 + SKU520: + - WAREHOUSE0 + SKU521: + - WAREHOUSE0 + SKU522: + - WAREHOUSE0 + SKU523: + - WAREHOUSE0 + SKU524: + - WAREHOUSE0 + SKU525: + - WAREHOUSE0 + SKU526: + - WAREHOUSE0 + SKU527: + - WAREHOUSE0 + SKU528: + - WAREHOUSE0 + SKU529: + - WAREHOUSE0 + SKU53: + - WAREHOUSE0 + SKU530: + - WAREHOUSE0 + SKU531: + - WAREHOUSE0 + SKU532: + - WAREHOUSE0 + SKU533: + - WAREHOUSE0 + SKU534: + - WAREHOUSE0 + SKU535: + - WAREHOUSE0 + SKU536: + - WAREHOUSE0 + SKU537: + - WAREHOUSE0 + SKU538: + - WAREHOUSE0 + SKU539: + - WAREHOUSE0 + SKU54: + - WAREHOUSE0 + SKU540: + - WAREHOUSE0 + SKU541: + - WAREHOUSE0 + SKU542: + - WAREHOUSE0 + SKU543: + - WAREHOUSE0 + SKU544: + - WAREHOUSE0 + SKU545: + - WAREHOUSE0 + SKU546: + - WAREHOUSE0 + SKU547: + - WAREHOUSE0 + SKU548: + - WAREHOUSE0 + SKU549: + - WAREHOUSE0 + SKU55: + - WAREHOUSE0 + SKU550: + - WAREHOUSE0 + SKU551: + - WAREHOUSE0 + SKU552: + - WAREHOUSE0 + SKU553: + - WAREHOUSE0 + SKU554: + - WAREHOUSE0 + SKU555: + - WAREHOUSE0 + SKU556: + - WAREHOUSE0 + SKU557: + - WAREHOUSE0 + SKU558: + - WAREHOUSE0 + SKU559: + - WAREHOUSE0 + SKU56: + - WAREHOUSE0 + SKU560: + - WAREHOUSE0 + SKU561: + - WAREHOUSE0 + SKU562: + - WAREHOUSE0 + SKU563: + - WAREHOUSE0 + SKU564: + - WAREHOUSE0 + SKU565: + - WAREHOUSE0 + SKU566: + - WAREHOUSE0 + SKU567: + - WAREHOUSE0 + SKU568: + - WAREHOUSE0 + SKU569: + - WAREHOUSE0 + SKU57: + - WAREHOUSE0 + SKU570: + - WAREHOUSE0 + SKU571: + - WAREHOUSE0 + SKU572: + - WAREHOUSE0 + SKU573: + - WAREHOUSE0 + SKU574: + - WAREHOUSE0 + SKU575: + - WAREHOUSE0 + SKU576: + - WAREHOUSE0 + SKU577: + - WAREHOUSE0 + SKU578: + - WAREHOUSE0 + SKU579: + - WAREHOUSE0 + SKU58: + - WAREHOUSE0 + SKU580: + - WAREHOUSE0 + SKU581: + - WAREHOUSE0 + SKU582: + - WAREHOUSE0 + SKU583: + - WAREHOUSE0 + SKU584: + - WAREHOUSE0 + SKU585: + - WAREHOUSE0 + SKU586: + - WAREHOUSE0 + SKU587: + - WAREHOUSE0 + SKU588: + - WAREHOUSE0 + SKU589: + - WAREHOUSE0 + SKU59: + - WAREHOUSE0 + SKU590: + - WAREHOUSE0 + SKU591: + - WAREHOUSE0 + SKU592: + - WAREHOUSE0 + SKU593: + - WAREHOUSE0 + SKU594: + - WAREHOUSE0 + SKU595: + - WAREHOUSE0 + SKU596: + - WAREHOUSE0 + SKU597: + - WAREHOUSE0 + SKU598: + - WAREHOUSE0 + SKU599: + - WAREHOUSE0 + SKU6: + - WAREHOUSE0 + SKU60: + - WAREHOUSE0 + SKU600: + - WAREHOUSE0 + SKU601: + - WAREHOUSE0 + SKU602: + - WAREHOUSE0 + SKU603: + - WAREHOUSE0 + SKU604: + - WAREHOUSE0 + SKU605: + - WAREHOUSE0 + SKU606: + - WAREHOUSE0 + SKU607: + - WAREHOUSE0 + SKU608: + - WAREHOUSE0 + SKU609: + - WAREHOUSE0 + SKU61: + - WAREHOUSE0 + SKU610: + - WAREHOUSE0 + SKU611: + - WAREHOUSE0 + SKU612: + - WAREHOUSE0 + SKU613: + - WAREHOUSE0 + SKU614: + - WAREHOUSE0 + SKU615: + - WAREHOUSE0 + SKU616: + - WAREHOUSE0 + SKU617: + - WAREHOUSE0 + SKU618: + - WAREHOUSE0 + SKU619: + - WAREHOUSE0 + SKU62: + - WAREHOUSE0 + SKU620: + - WAREHOUSE0 + SKU621: + - WAREHOUSE0 + SKU622: + - WAREHOUSE0 + SKU623: + - WAREHOUSE0 + SKU624: + - WAREHOUSE0 + SKU625: + - WAREHOUSE0 + SKU626: + - WAREHOUSE0 + SKU627: + - WAREHOUSE0 + SKU628: + - WAREHOUSE0 + SKU629: + - WAREHOUSE0 + SKU63: + - WAREHOUSE0 + SKU630: + - WAREHOUSE0 + SKU631: + - WAREHOUSE0 + SKU632: + - WAREHOUSE0 + SKU633: + - WAREHOUSE0 + SKU634: + - WAREHOUSE0 + SKU635: + - WAREHOUSE0 + SKU636: + - WAREHOUSE0 + SKU637: + - WAREHOUSE0 + SKU638: + - WAREHOUSE0 + SKU639: + - WAREHOUSE0 + SKU64: + - WAREHOUSE0 + SKU640: + - WAREHOUSE0 + SKU641: + - WAREHOUSE0 + SKU642: + - WAREHOUSE0 + SKU643: + - WAREHOUSE0 + SKU644: + - WAREHOUSE0 + SKU645: + - WAREHOUSE0 + SKU646: + - WAREHOUSE0 + SKU647: + - WAREHOUSE0 + SKU648: + - WAREHOUSE0 + SKU649: + - WAREHOUSE0 + SKU65: + - WAREHOUSE0 + SKU650: + - WAREHOUSE0 + SKU651: + - WAREHOUSE0 + SKU652: + - WAREHOUSE0 + SKU653: + - WAREHOUSE0 + SKU654: + - WAREHOUSE0 + SKU655: + - WAREHOUSE0 + SKU656: + - WAREHOUSE0 + SKU657: + - WAREHOUSE0 + SKU658: + - WAREHOUSE0 + SKU659: + - WAREHOUSE0 + SKU66: + - WAREHOUSE0 + SKU660: + - WAREHOUSE0 + SKU661: + - WAREHOUSE0 + SKU662: + - WAREHOUSE0 + SKU663: + - WAREHOUSE0 + SKU664: + - WAREHOUSE0 + SKU665: + - WAREHOUSE0 + SKU666: + - WAREHOUSE0 + SKU667: + - WAREHOUSE0 + SKU668: + - WAREHOUSE0 + SKU669: + - WAREHOUSE0 + SKU67: + - WAREHOUSE0 + SKU670: + - WAREHOUSE0 + SKU671: + - WAREHOUSE0 + SKU672: + - WAREHOUSE0 + SKU673: + - WAREHOUSE0 + SKU674: + - WAREHOUSE0 + SKU675: + - WAREHOUSE0 + SKU676: + - WAREHOUSE0 + SKU677: + - WAREHOUSE0 + SKU678: + - WAREHOUSE0 + SKU679: + - WAREHOUSE0 + SKU68: + - WAREHOUSE0 + SKU680: + - WAREHOUSE0 + SKU681: + - WAREHOUSE0 + SKU682: + - WAREHOUSE0 + SKU683: + - WAREHOUSE0 + SKU684: + - WAREHOUSE0 + SKU685: + - WAREHOUSE0 + SKU686: + - WAREHOUSE0 + SKU687: + - WAREHOUSE0 + SKU688: + - WAREHOUSE0 + SKU689: + - WAREHOUSE0 + SKU69: + - WAREHOUSE0 + SKU690: + - WAREHOUSE0 + SKU691: + - WAREHOUSE0 + SKU692: + - WAREHOUSE0 + SKU693: + - WAREHOUSE0 + SKU694: + - WAREHOUSE0 + SKU695: + - WAREHOUSE0 + SKU696: + - WAREHOUSE0 + SKU697: + - WAREHOUSE0 + SKU698: + - WAREHOUSE0 + SKU699: + - WAREHOUSE0 + SKU7: + - WAREHOUSE0 + SKU70: + - WAREHOUSE0 + SKU700: + - WAREHOUSE0 + SKU701: + - WAREHOUSE0 + SKU702: + - WAREHOUSE0 + SKU703: + - WAREHOUSE0 + SKU704: + - WAREHOUSE0 + SKU705: + - WAREHOUSE0 + SKU706: + - WAREHOUSE0 + SKU707: + - WAREHOUSE0 + SKU708: + - WAREHOUSE0 + SKU709: + - WAREHOUSE0 + SKU71: + - WAREHOUSE0 + SKU710: + - WAREHOUSE0 + SKU711: + - WAREHOUSE0 + SKU712: + - WAREHOUSE0 + SKU713: + - WAREHOUSE0 + SKU714: + - WAREHOUSE0 + SKU715: + - WAREHOUSE0 + SKU716: + - WAREHOUSE0 + SKU717: + - WAREHOUSE0 + SKU718: + - WAREHOUSE0 + SKU719: + - WAREHOUSE0 + SKU72: + - WAREHOUSE0 + SKU720: + - WAREHOUSE0 + SKU721: + - WAREHOUSE0 + SKU722: + - WAREHOUSE0 + SKU723: + - WAREHOUSE0 + SKU724: + - WAREHOUSE0 + SKU725: + - WAREHOUSE0 + SKU726: + - WAREHOUSE0 + SKU727: + - WAREHOUSE0 + SKU728: + - WAREHOUSE0 + SKU729: + - WAREHOUSE0 + SKU73: + - WAREHOUSE0 + SKU730: + - WAREHOUSE0 + SKU731: + - WAREHOUSE0 + SKU732: + - WAREHOUSE0 + SKU733: + - WAREHOUSE0 + SKU734: + - WAREHOUSE0 + SKU735: + - WAREHOUSE0 + SKU736: + - WAREHOUSE0 + SKU737: + - WAREHOUSE0 + SKU738: + - WAREHOUSE0 + SKU739: + - WAREHOUSE0 + SKU74: + - WAREHOUSE0 + SKU740: + - WAREHOUSE0 + SKU741: + - WAREHOUSE0 + SKU742: + - WAREHOUSE0 + SKU743: + - WAREHOUSE0 + SKU744: + - WAREHOUSE0 + SKU745: + - WAREHOUSE0 + SKU746: + - WAREHOUSE0 + SKU747: + - WAREHOUSE0 + SKU748: + - WAREHOUSE0 + SKU749: + - WAREHOUSE0 + SKU75: + - WAREHOUSE0 + SKU750: + - WAREHOUSE0 + SKU751: + - WAREHOUSE0 + SKU752: + - WAREHOUSE0 + SKU753: + - WAREHOUSE0 + SKU754: + - WAREHOUSE0 + SKU755: + - WAREHOUSE0 + SKU756: + - WAREHOUSE0 + SKU757: + - WAREHOUSE0 + SKU758: + - WAREHOUSE0 + SKU759: + - WAREHOUSE0 + SKU76: + - WAREHOUSE0 + SKU760: + - WAREHOUSE0 + SKU761: + - WAREHOUSE0 + SKU762: + - WAREHOUSE0 + SKU763: + - WAREHOUSE0 + SKU764: + - WAREHOUSE0 + SKU765: + - WAREHOUSE0 + SKU766: + - WAREHOUSE0 + SKU767: + - WAREHOUSE0 + SKU768: + - WAREHOUSE0 + SKU769: + - WAREHOUSE0 + SKU77: + - WAREHOUSE0 + SKU770: + - WAREHOUSE0 + SKU771: + - WAREHOUSE0 + SKU772: + - WAREHOUSE0 + SKU773: + - WAREHOUSE0 + SKU774: + - WAREHOUSE0 + SKU775: + - WAREHOUSE0 + SKU776: + - WAREHOUSE0 + SKU777: + - WAREHOUSE0 + SKU778: + - WAREHOUSE0 + SKU779: + - WAREHOUSE0 + SKU78: + - WAREHOUSE0 + SKU780: + - WAREHOUSE0 + SKU781: + - WAREHOUSE0 + SKU782: + - WAREHOUSE0 + SKU783: + - WAREHOUSE0 + SKU784: + - WAREHOUSE0 + SKU785: + - WAREHOUSE0 + SKU786: + - WAREHOUSE0 + SKU787: + - WAREHOUSE0 + SKU788: + - WAREHOUSE0 + SKU789: + - WAREHOUSE0 + SKU79: + - WAREHOUSE0 + SKU790: + - WAREHOUSE0 + SKU791: + - WAREHOUSE0 + SKU792: + - WAREHOUSE0 + SKU793: + - WAREHOUSE0 + SKU794: + - WAREHOUSE0 + SKU795: + - WAREHOUSE0 + SKU796: + - WAREHOUSE0 + SKU797: + - WAREHOUSE0 + SKU798: + - WAREHOUSE0 + SKU799: + - WAREHOUSE0 + SKU8: + - WAREHOUSE0 + SKU80: + - WAREHOUSE0 + SKU800: + - WAREHOUSE0 + SKU801: + - WAREHOUSE0 + SKU802: + - WAREHOUSE0 + SKU803: + - WAREHOUSE0 + SKU804: + - WAREHOUSE0 + SKU805: + - WAREHOUSE0 + SKU806: + - WAREHOUSE0 + SKU807: + - WAREHOUSE0 + SKU808: + - WAREHOUSE0 + SKU809: + - WAREHOUSE0 + SKU81: + - WAREHOUSE0 + SKU810: + - WAREHOUSE0 + SKU811: + - WAREHOUSE0 + SKU812: + - WAREHOUSE0 + SKU813: + - WAREHOUSE0 + SKU814: + - WAREHOUSE0 + SKU815: + - WAREHOUSE0 + SKU816: + - WAREHOUSE0 + SKU817: + - WAREHOUSE0 + SKU818: + - WAREHOUSE0 + SKU819: + - WAREHOUSE0 + SKU82: + - WAREHOUSE0 + SKU820: + - WAREHOUSE0 + SKU821: + - WAREHOUSE0 + SKU822: + - WAREHOUSE0 + SKU823: + - WAREHOUSE0 + SKU824: + - WAREHOUSE0 + SKU825: + - WAREHOUSE0 + SKU826: + - WAREHOUSE0 + SKU827: + - WAREHOUSE0 + SKU828: + - WAREHOUSE0 + SKU829: + - WAREHOUSE0 + SKU83: + - WAREHOUSE0 + SKU830: + - WAREHOUSE0 + SKU831: + - WAREHOUSE0 + SKU832: + - WAREHOUSE0 + SKU833: + - WAREHOUSE0 + SKU834: + - WAREHOUSE0 + SKU835: + - WAREHOUSE0 + SKU836: + - WAREHOUSE0 + SKU837: + - WAREHOUSE0 + SKU838: + - WAREHOUSE0 + SKU839: + - WAREHOUSE0 + SKU84: + - WAREHOUSE0 + SKU840: + - WAREHOUSE0 + SKU841: + - WAREHOUSE0 + SKU842: + - WAREHOUSE0 + SKU843: + - WAREHOUSE0 + SKU844: + - WAREHOUSE0 + SKU845: + - WAREHOUSE0 + SKU846: + - WAREHOUSE0 + SKU847: + - WAREHOUSE0 + SKU848: + - WAREHOUSE0 + SKU849: + - WAREHOUSE0 + SKU85: + - WAREHOUSE0 + SKU850: + - WAREHOUSE0 + SKU851: + - WAREHOUSE0 + SKU852: + - WAREHOUSE0 + SKU853: + - WAREHOUSE0 + SKU854: + - WAREHOUSE0 + SKU855: + - WAREHOUSE0 + SKU856: + - WAREHOUSE0 + SKU857: + - WAREHOUSE0 + SKU858: + - WAREHOUSE0 + SKU859: + - WAREHOUSE0 + SKU86: + - WAREHOUSE0 + SKU860: + - WAREHOUSE0 + SKU861: + - WAREHOUSE0 + SKU862: + - WAREHOUSE0 + SKU863: + - WAREHOUSE0 + SKU864: + - WAREHOUSE0 + SKU865: + - WAREHOUSE0 + SKU866: + - WAREHOUSE0 + SKU867: + - WAREHOUSE0 + SKU868: + - WAREHOUSE0 + SKU869: + - WAREHOUSE0 + SKU87: + - WAREHOUSE0 + SKU870: + - WAREHOUSE0 + SKU871: + - WAREHOUSE0 + SKU872: + - WAREHOUSE0 + SKU873: + - WAREHOUSE0 + SKU874: + - WAREHOUSE0 + SKU875: + - WAREHOUSE0 + SKU876: + - WAREHOUSE0 + SKU877: + - WAREHOUSE0 + SKU878: + - WAREHOUSE0 + SKU879: + - WAREHOUSE0 + SKU88: + - WAREHOUSE0 + SKU880: + - WAREHOUSE0 + SKU881: + - WAREHOUSE0 + SKU882: + - WAREHOUSE0 + SKU883: + - WAREHOUSE0 + SKU884: + - WAREHOUSE0 + SKU885: + - WAREHOUSE0 + SKU886: + - WAREHOUSE0 + SKU887: + - WAREHOUSE0 + SKU888: + - WAREHOUSE0 + SKU889: + - WAREHOUSE0 + SKU89: + - WAREHOUSE0 + SKU890: + - WAREHOUSE0 + SKU891: + - WAREHOUSE0 + SKU892: + - WAREHOUSE0 + SKU893: + - WAREHOUSE0 + SKU894: + - WAREHOUSE0 + SKU895: + - WAREHOUSE0 + SKU896: + - WAREHOUSE0 + SKU897: + - WAREHOUSE0 + SKU898: + - WAREHOUSE0 + SKU899: + - WAREHOUSE0 + SKU9: + - WAREHOUSE0 + SKU90: + - WAREHOUSE0 + SKU900: + - WAREHOUSE0 + SKU901: + - WAREHOUSE0 + SKU902: + - WAREHOUSE0 + SKU903: + - WAREHOUSE0 + SKU904: + - WAREHOUSE0 + SKU905: + - WAREHOUSE0 + SKU906: + - WAREHOUSE0 + SKU907: + - WAREHOUSE0 + SKU908: + - WAREHOUSE0 + SKU909: + - WAREHOUSE0 + SKU91: + - WAREHOUSE0 + SKU910: + - WAREHOUSE0 + SKU911: + - WAREHOUSE0 + SKU912: + - WAREHOUSE0 + SKU913: + - WAREHOUSE0 + SKU914: + - WAREHOUSE0 + SKU915: + - WAREHOUSE0 + SKU916: + - WAREHOUSE0 + SKU917: + - WAREHOUSE0 + SKU918: + - WAREHOUSE0 + SKU919: + - WAREHOUSE0 + SKU92: + - WAREHOUSE0 + SKU920: + - WAREHOUSE0 + SKU921: + - WAREHOUSE0 + SKU922: + - WAREHOUSE0 + SKU923: + - WAREHOUSE0 + SKU924: + - WAREHOUSE0 + SKU925: + - WAREHOUSE0 + SKU926: + - WAREHOUSE0 + SKU927: + - WAREHOUSE0 + SKU928: + - WAREHOUSE0 + SKU929: + - WAREHOUSE0 + SKU93: + - WAREHOUSE0 + SKU930: + - WAREHOUSE0 + SKU931: + - WAREHOUSE0 + SKU932: + - WAREHOUSE0 + SKU933: + - WAREHOUSE0 + SKU934: + - WAREHOUSE0 + SKU935: + - WAREHOUSE0 + SKU936: + - WAREHOUSE0 + SKU937: + - WAREHOUSE0 + SKU938: + - WAREHOUSE0 + SKU939: + - WAREHOUSE0 + SKU94: + - WAREHOUSE0 + SKU940: + - WAREHOUSE0 + SKU941: + - WAREHOUSE0 + SKU942: + - WAREHOUSE0 + SKU943: + - WAREHOUSE0 + SKU944: + - WAREHOUSE0 + SKU945: + - WAREHOUSE0 + SKU946: + - WAREHOUSE0 + SKU947: + - WAREHOUSE0 + SKU948: + - WAREHOUSE0 + SKU949: + - WAREHOUSE0 + SKU95: + - WAREHOUSE0 + SKU950: + - WAREHOUSE0 + SKU951: + - WAREHOUSE0 + SKU952: + - WAREHOUSE0 + SKU953: + - WAREHOUSE0 + SKU954: + - WAREHOUSE0 + SKU955: + - WAREHOUSE0 + SKU956: + - WAREHOUSE0 + SKU957: + - WAREHOUSE0 + SKU958: + - WAREHOUSE0 + SKU959: + - WAREHOUSE0 + SKU96: + - WAREHOUSE0 + SKU960: + - WAREHOUSE0 + SKU961: + - WAREHOUSE0 + SKU962: + - WAREHOUSE0 + SKU963: + - WAREHOUSE0 + SKU964: + - WAREHOUSE0 + SKU965: + - WAREHOUSE0 + SKU966: + - WAREHOUSE0 + SKU967: + - WAREHOUSE0 + SKU968: + - WAREHOUSE0 + SKU969: + - WAREHOUSE0 + SKU97: + - WAREHOUSE0 + SKU970: + - WAREHOUSE0 + SKU971: + - WAREHOUSE0 + SKU972: + - WAREHOUSE0 + SKU973: + - WAREHOUSE0 + SKU974: + - WAREHOUSE0 + SKU975: + - WAREHOUSE0 + SKU976: + - WAREHOUSE0 + SKU977: + - WAREHOUSE0 + SKU978: + - WAREHOUSE0 + SKU979: + - WAREHOUSE0 + SKU98: + - WAREHOUSE0 + SKU980: + - WAREHOUSE0 + SKU981: + - WAREHOUSE0 + SKU982: + - WAREHOUSE0 + SKU983: + - WAREHOUSE0 + SKU984: + - WAREHOUSE0 + SKU985: + - WAREHOUSE0 + SKU986: + - WAREHOUSE0 + SKU987: + - WAREHOUSE0 + SKU988: + - WAREHOUSE0 + SKU989: + - WAREHOUSE0 + SKU99: + - WAREHOUSE0 + SKU990: + - WAREHOUSE0 + SKU991: + - WAREHOUSE0 + SKU992: + - WAREHOUSE0 + SKU993: + - WAREHOUSE0 + SKU994: + - WAREHOUSE0 + SKU995: + - WAREHOUSE0 + SKU996: + - WAREHOUSE0 + SKU997: + - WAREHOUSE0 + SKU998: + - WAREHOUSE0 + SKU999: + - WAREHOUSE0 + WAREHOUSE0: + SKU0: + - SUPPLIER0 + SKU1: + - SUPPLIER0 + SKU10: + - SUPPLIER0 + SKU100: + - SUPPLIER0 + SKU101: + - SUPPLIER0 + SKU102: + - SUPPLIER0 + SKU103: + - SUPPLIER0 + SKU104: + - SUPPLIER0 + SKU105: + - SUPPLIER0 + SKU106: + - SUPPLIER0 + SKU107: + - SUPPLIER0 + SKU108: + - SUPPLIER0 + SKU109: + - SUPPLIER0 + SKU11: + - SUPPLIER0 + SKU110: + - SUPPLIER0 + SKU111: + - SUPPLIER0 + SKU112: + - SUPPLIER0 + SKU113: + - SUPPLIER0 + SKU114: + - SUPPLIER0 + SKU115: + - SUPPLIER0 + SKU116: + - SUPPLIER0 + SKU117: + - SUPPLIER0 + SKU118: + - SUPPLIER0 + SKU119: + - SUPPLIER0 + SKU12: + - SUPPLIER0 + SKU120: + - SUPPLIER0 + SKU121: + - SUPPLIER0 + SKU122: + - SUPPLIER0 + SKU123: + - SUPPLIER0 + SKU124: + - SUPPLIER0 + SKU125: + - SUPPLIER0 + SKU126: + - SUPPLIER0 + SKU127: + - SUPPLIER0 + SKU128: + - SUPPLIER0 + SKU129: + - SUPPLIER0 + SKU13: + - SUPPLIER0 + SKU130: + - SUPPLIER0 + SKU131: + - SUPPLIER0 + SKU132: + - SUPPLIER0 + SKU133: + - SUPPLIER0 + SKU134: + - SUPPLIER0 + SKU135: + - SUPPLIER0 + SKU136: + - SUPPLIER0 + SKU137: + - SUPPLIER0 + SKU138: + - SUPPLIER0 + SKU139: + - SUPPLIER0 + SKU14: + - SUPPLIER0 + SKU140: + - SUPPLIER0 + SKU141: + - SUPPLIER0 + SKU142: + - SUPPLIER0 + SKU143: + - SUPPLIER0 + SKU144: + - SUPPLIER0 + SKU145: + - SUPPLIER0 + SKU146: + - SUPPLIER0 + SKU147: + - SUPPLIER0 + SKU148: + - SUPPLIER0 + SKU149: + - SUPPLIER0 + SKU15: + - SUPPLIER0 + SKU150: + - SUPPLIER0 + SKU151: + - SUPPLIER0 + SKU152: + - SUPPLIER0 + SKU153: + - SUPPLIER0 + SKU154: + - SUPPLIER0 + SKU155: + - SUPPLIER0 + SKU156: + - SUPPLIER0 + SKU157: + - SUPPLIER0 + SKU158: + - SUPPLIER0 + SKU159: + - SUPPLIER0 + SKU16: + - SUPPLIER0 + SKU160: + - SUPPLIER0 + SKU161: + - SUPPLIER0 + SKU162: + - SUPPLIER0 + SKU163: + - SUPPLIER0 + SKU164: + - SUPPLIER0 + SKU165: + - SUPPLIER0 + SKU166: + - SUPPLIER0 + SKU167: + - SUPPLIER0 + SKU168: + - SUPPLIER0 + SKU169: + - SUPPLIER0 + SKU17: + - SUPPLIER0 + SKU170: + - SUPPLIER0 + SKU171: + - SUPPLIER0 + SKU172: + - SUPPLIER0 + SKU173: + - SUPPLIER0 + SKU174: + - SUPPLIER0 + SKU175: + - SUPPLIER0 + SKU176: + - SUPPLIER0 + SKU177: + - SUPPLIER0 + SKU178: + - SUPPLIER0 + SKU179: + - SUPPLIER0 + SKU18: + - SUPPLIER0 + SKU180: + - SUPPLIER0 + SKU181: + - SUPPLIER0 + SKU182: + - SUPPLIER0 + SKU183: + - SUPPLIER0 + SKU184: + - SUPPLIER0 + SKU185: + - SUPPLIER0 + SKU186: + - SUPPLIER0 + SKU187: + - SUPPLIER0 + SKU188: + - SUPPLIER0 + SKU189: + - SUPPLIER0 + SKU19: + - SUPPLIER0 + SKU190: + - SUPPLIER0 + SKU191: + - SUPPLIER0 + SKU192: + - SUPPLIER0 + SKU193: + - SUPPLIER0 + SKU194: + - SUPPLIER0 + SKU195: + - SUPPLIER0 + SKU196: + - SUPPLIER0 + SKU197: + - SUPPLIER0 + SKU198: + - SUPPLIER0 + SKU199: + - SUPPLIER0 + SKU2: + - SUPPLIER0 + SKU20: + - SUPPLIER0 + SKU200: + - SUPPLIER0 + SKU201: + - SUPPLIER0 + SKU202: + - SUPPLIER0 + SKU203: + - SUPPLIER0 + SKU204: + - SUPPLIER0 + SKU205: + - SUPPLIER0 + SKU206: + - SUPPLIER0 + SKU207: + - SUPPLIER0 + SKU208: + - SUPPLIER0 + SKU209: + - SUPPLIER0 + SKU21: + - SUPPLIER0 + SKU210: + - SUPPLIER0 + SKU211: + - SUPPLIER0 + SKU212: + - SUPPLIER0 + SKU213: + - SUPPLIER0 + SKU214: + - SUPPLIER0 + SKU215: + - SUPPLIER0 + SKU216: + - SUPPLIER0 + SKU217: + - SUPPLIER0 + SKU218: + - SUPPLIER0 + SKU219: + - SUPPLIER0 + SKU22: + - SUPPLIER0 + SKU220: + - SUPPLIER0 + SKU221: + - SUPPLIER0 + SKU222: + - SUPPLIER0 + SKU223: + - SUPPLIER0 + SKU224: + - SUPPLIER0 + SKU225: + - SUPPLIER0 + SKU226: + - SUPPLIER0 + SKU227: + - SUPPLIER0 + SKU228: + - SUPPLIER0 + SKU229: + - SUPPLIER0 + SKU23: + - SUPPLIER0 + SKU230: + - SUPPLIER0 + SKU231: + - SUPPLIER0 + SKU232: + - SUPPLIER0 + SKU233: + - SUPPLIER0 + SKU234: + - SUPPLIER0 + SKU235: + - SUPPLIER0 + SKU236: + - SUPPLIER0 + SKU237: + - SUPPLIER0 + SKU238: + - SUPPLIER0 + SKU239: + - SUPPLIER0 + SKU24: + - SUPPLIER0 + SKU240: + - SUPPLIER0 + SKU241: + - SUPPLIER0 + SKU242: + - SUPPLIER0 + SKU243: + - SUPPLIER0 + SKU244: + - SUPPLIER0 + SKU245: + - SUPPLIER0 + SKU246: + - SUPPLIER0 + SKU247: + - SUPPLIER0 + SKU248: + - SUPPLIER0 + SKU249: + - SUPPLIER0 + SKU25: + - SUPPLIER0 + SKU250: + - SUPPLIER0 + SKU251: + - SUPPLIER0 + SKU252: + - SUPPLIER0 + SKU253: + - SUPPLIER0 + SKU254: + - SUPPLIER0 + SKU255: + - SUPPLIER0 + SKU256: + - SUPPLIER0 + SKU257: + - SUPPLIER0 + SKU258: + - SUPPLIER0 + SKU259: + - SUPPLIER0 + SKU26: + - SUPPLIER0 + SKU260: + - SUPPLIER0 + SKU261: + - SUPPLIER0 + SKU262: + - SUPPLIER0 + SKU263: + - SUPPLIER0 + SKU264: + - SUPPLIER0 + SKU265: + - SUPPLIER0 + SKU266: + - SUPPLIER0 + SKU267: + - SUPPLIER0 + SKU268: + - SUPPLIER0 + SKU269: + - SUPPLIER0 + SKU27: + - SUPPLIER0 + SKU270: + - SUPPLIER0 + SKU271: + - SUPPLIER0 + SKU272: + - SUPPLIER0 + SKU273: + - SUPPLIER0 + SKU274: + - SUPPLIER0 + SKU275: + - SUPPLIER0 + SKU276: + - SUPPLIER0 + SKU277: + - SUPPLIER0 + SKU278: + - SUPPLIER0 + SKU279: + - SUPPLIER0 + SKU28: + - SUPPLIER0 + SKU280: + - SUPPLIER0 + SKU281: + - SUPPLIER0 + SKU282: + - SUPPLIER0 + SKU283: + - SUPPLIER0 + SKU284: + - SUPPLIER0 + SKU285: + - SUPPLIER0 + SKU286: + - SUPPLIER0 + SKU287: + - SUPPLIER0 + SKU288: + - SUPPLIER0 + SKU289: + - SUPPLIER0 + SKU29: + - SUPPLIER0 + SKU290: + - SUPPLIER0 + SKU291: + - SUPPLIER0 + SKU292: + - SUPPLIER0 + SKU293: + - SUPPLIER0 + SKU294: + - SUPPLIER0 + SKU295: + - SUPPLIER0 + SKU296: + - SUPPLIER0 + SKU297: + - SUPPLIER0 + SKU298: + - SUPPLIER0 + SKU299: + - SUPPLIER0 + SKU3: + - SUPPLIER0 + SKU30: + - SUPPLIER0 + SKU300: + - SUPPLIER0 + SKU301: + - SUPPLIER0 + SKU302: + - SUPPLIER0 + SKU303: + - SUPPLIER0 + SKU304: + - SUPPLIER0 + SKU305: + - SUPPLIER0 + SKU306: + - SUPPLIER0 + SKU307: + - SUPPLIER0 + SKU308: + - SUPPLIER0 + SKU309: + - SUPPLIER0 + SKU31: + - SUPPLIER0 + SKU310: + - SUPPLIER0 + SKU311: + - SUPPLIER0 + SKU312: + - SUPPLIER0 + SKU313: + - SUPPLIER0 + SKU314: + - SUPPLIER0 + SKU315: + - SUPPLIER0 + SKU316: + - SUPPLIER0 + SKU317: + - SUPPLIER0 + SKU318: + - SUPPLIER0 + SKU319: + - SUPPLIER0 + SKU32: + - SUPPLIER0 + SKU320: + - SUPPLIER0 + SKU321: + - SUPPLIER0 + SKU322: + - SUPPLIER0 + SKU323: + - SUPPLIER0 + SKU324: + - SUPPLIER0 + SKU325: + - SUPPLIER0 + SKU326: + - SUPPLIER0 + SKU327: + - SUPPLIER0 + SKU328: + - SUPPLIER0 + SKU329: + - SUPPLIER0 + SKU33: + - SUPPLIER0 + SKU330: + - SUPPLIER0 + SKU331: + - SUPPLIER0 + SKU332: + - SUPPLIER0 + SKU333: + - SUPPLIER0 + SKU334: + - SUPPLIER0 + SKU335: + - SUPPLIER0 + SKU336: + - SUPPLIER0 + SKU337: + - SUPPLIER0 + SKU338: + - SUPPLIER0 + SKU339: + - SUPPLIER0 + SKU34: + - SUPPLIER0 + SKU340: + - SUPPLIER0 + SKU341: + - SUPPLIER0 + SKU342: + - SUPPLIER0 + SKU343: + - SUPPLIER0 + SKU344: + - SUPPLIER0 + SKU345: + - SUPPLIER0 + SKU346: + - SUPPLIER0 + SKU347: + - SUPPLIER0 + SKU348: + - SUPPLIER0 + SKU349: + - SUPPLIER0 + SKU35: + - SUPPLIER0 + SKU350: + - SUPPLIER0 + SKU351: + - SUPPLIER0 + SKU352: + - SUPPLIER0 + SKU353: + - SUPPLIER0 + SKU354: + - SUPPLIER0 + SKU355: + - SUPPLIER0 + SKU356: + - SUPPLIER0 + SKU357: + - SUPPLIER0 + SKU358: + - SUPPLIER0 + SKU359: + - SUPPLIER0 + SKU36: + - SUPPLIER0 + SKU360: + - SUPPLIER0 + SKU361: + - SUPPLIER0 + SKU362: + - SUPPLIER0 + SKU363: + - SUPPLIER0 + SKU364: + - SUPPLIER0 + SKU365: + - SUPPLIER0 + SKU366: + - SUPPLIER0 + SKU367: + - SUPPLIER0 + SKU368: + - SUPPLIER0 + SKU369: + - SUPPLIER0 + SKU37: + - SUPPLIER0 + SKU370: + - SUPPLIER0 + SKU371: + - SUPPLIER0 + SKU372: + - SUPPLIER0 + SKU373: + - SUPPLIER0 + SKU374: + - SUPPLIER0 + SKU375: + - SUPPLIER0 + SKU376: + - SUPPLIER0 + SKU377: + - SUPPLIER0 + SKU378: + - SUPPLIER0 + SKU379: + - SUPPLIER0 + SKU38: + - SUPPLIER0 + SKU380: + - SUPPLIER0 + SKU381: + - SUPPLIER0 + SKU382: + - SUPPLIER0 + SKU383: + - SUPPLIER0 + SKU384: + - SUPPLIER0 + SKU385: + - SUPPLIER0 + SKU386: + - SUPPLIER0 + SKU387: + - SUPPLIER0 + SKU388: + - SUPPLIER0 + SKU389: + - SUPPLIER0 + SKU39: + - SUPPLIER0 + SKU390: + - SUPPLIER0 + SKU391: + - SUPPLIER0 + SKU392: + - SUPPLIER0 + SKU393: + - SUPPLIER0 + SKU394: + - SUPPLIER0 + SKU395: + - SUPPLIER0 + SKU396: + - SUPPLIER0 + SKU397: + - SUPPLIER0 + SKU398: + - SUPPLIER0 + SKU399: + - SUPPLIER0 + SKU4: + - SUPPLIER0 + SKU40: + - SUPPLIER0 + SKU400: + - SUPPLIER0 + SKU401: + - SUPPLIER0 + SKU402: + - SUPPLIER0 + SKU403: + - SUPPLIER0 + SKU404: + - SUPPLIER0 + SKU405: + - SUPPLIER0 + SKU406: + - SUPPLIER0 + SKU407: + - SUPPLIER0 + SKU408: + - SUPPLIER0 + SKU409: + - SUPPLIER0 + SKU41: + - SUPPLIER0 + SKU410: + - SUPPLIER0 + SKU411: + - SUPPLIER0 + SKU412: + - SUPPLIER0 + SKU413: + - SUPPLIER0 + SKU414: + - SUPPLIER0 + SKU415: + - SUPPLIER0 + SKU416: + - SUPPLIER0 + SKU417: + - SUPPLIER0 + SKU418: + - SUPPLIER0 + SKU419: + - SUPPLIER0 + SKU42: + - SUPPLIER0 + SKU420: + - SUPPLIER0 + SKU421: + - SUPPLIER0 + SKU422: + - SUPPLIER0 + SKU423: + - SUPPLIER0 + SKU424: + - SUPPLIER0 + SKU425: + - SUPPLIER0 + SKU426: + - SUPPLIER0 + SKU427: + - SUPPLIER0 + SKU428: + - SUPPLIER0 + SKU429: + - SUPPLIER0 + SKU43: + - SUPPLIER0 + SKU430: + - SUPPLIER0 + SKU431: + - SUPPLIER0 + SKU432: + - SUPPLIER0 + SKU433: + - SUPPLIER0 + SKU434: + - SUPPLIER0 + SKU435: + - SUPPLIER0 + SKU436: + - SUPPLIER0 + SKU437: + - SUPPLIER0 + SKU438: + - SUPPLIER0 + SKU439: + - SUPPLIER0 + SKU44: + - SUPPLIER0 + SKU440: + - SUPPLIER0 + SKU441: + - SUPPLIER0 + SKU442: + - SUPPLIER0 + SKU443: + - SUPPLIER0 + SKU444: + - SUPPLIER0 + SKU445: + - SUPPLIER0 + SKU446: + - SUPPLIER0 + SKU447: + - SUPPLIER0 + SKU448: + - SUPPLIER0 + SKU449: + - SUPPLIER0 + SKU45: + - SUPPLIER0 + SKU450: + - SUPPLIER0 + SKU451: + - SUPPLIER0 + SKU452: + - SUPPLIER0 + SKU453: + - SUPPLIER0 + SKU454: + - SUPPLIER0 + SKU455: + - SUPPLIER0 + SKU456: + - SUPPLIER0 + SKU457: + - SUPPLIER0 + SKU458: + - SUPPLIER0 + SKU459: + - SUPPLIER0 + SKU46: + - SUPPLIER0 + SKU460: + - SUPPLIER0 + SKU461: + - SUPPLIER0 + SKU462: + - SUPPLIER0 + SKU463: + - SUPPLIER0 + SKU464: + - SUPPLIER0 + SKU465: + - SUPPLIER0 + SKU466: + - SUPPLIER0 + SKU467: + - SUPPLIER0 + SKU468: + - SUPPLIER0 + SKU469: + - SUPPLIER0 + SKU47: + - SUPPLIER0 + SKU470: + - SUPPLIER0 + SKU471: + - SUPPLIER0 + SKU472: + - SUPPLIER0 + SKU473: + - SUPPLIER0 + SKU474: + - SUPPLIER0 + SKU475: + - SUPPLIER0 + SKU476: + - SUPPLIER0 + SKU477: + - SUPPLIER0 + SKU478: + - SUPPLIER0 + SKU479: + - SUPPLIER0 + SKU48: + - SUPPLIER0 + SKU480: + - SUPPLIER0 + SKU481: + - SUPPLIER0 + SKU482: + - SUPPLIER0 + SKU483: + - SUPPLIER0 + SKU484: + - SUPPLIER0 + SKU485: + - SUPPLIER0 + SKU486: + - SUPPLIER0 + SKU487: + - SUPPLIER0 + SKU488: + - SUPPLIER0 + SKU489: + - SUPPLIER0 + SKU49: + - SUPPLIER0 + SKU490: + - SUPPLIER0 + SKU491: + - SUPPLIER0 + SKU492: + - SUPPLIER0 + SKU493: + - SUPPLIER0 + SKU494: + - SUPPLIER0 + SKU495: + - SUPPLIER0 + SKU496: + - SUPPLIER0 + SKU497: + - SUPPLIER0 + SKU498: + - SUPPLIER0 + SKU499: + - SUPPLIER0 + SKU5: + - SUPPLIER0 + SKU50: + - SUPPLIER0 + SKU500: + - SUPPLIER0 + SKU501: + - SUPPLIER0 + SKU502: + - SUPPLIER0 + SKU503: + - SUPPLIER0 + SKU504: + - SUPPLIER0 + SKU505: + - SUPPLIER0 + SKU506: + - SUPPLIER0 + SKU507: + - SUPPLIER0 + SKU508: + - SUPPLIER0 + SKU509: + - SUPPLIER0 + SKU51: + - SUPPLIER0 + SKU510: + - SUPPLIER0 + SKU511: + - SUPPLIER0 + SKU512: + - SUPPLIER0 + SKU513: + - SUPPLIER0 + SKU514: + - SUPPLIER0 + SKU515: + - SUPPLIER0 + SKU516: + - SUPPLIER0 + SKU517: + - SUPPLIER0 + SKU518: + - SUPPLIER0 + SKU519: + - SUPPLIER0 + SKU52: + - SUPPLIER0 + SKU520: + - SUPPLIER0 + SKU521: + - SUPPLIER0 + SKU522: + - SUPPLIER0 + SKU523: + - SUPPLIER0 + SKU524: + - SUPPLIER0 + SKU525: + - SUPPLIER0 + SKU526: + - SUPPLIER0 + SKU527: + - SUPPLIER0 + SKU528: + - SUPPLIER0 + SKU529: + - SUPPLIER0 + SKU53: + - SUPPLIER0 + SKU530: + - SUPPLIER0 + SKU531: + - SUPPLIER0 + SKU532: + - SUPPLIER0 + SKU533: + - SUPPLIER0 + SKU534: + - SUPPLIER0 + SKU535: + - SUPPLIER0 + SKU536: + - SUPPLIER0 + SKU537: + - SUPPLIER0 + SKU538: + - SUPPLIER0 + SKU539: + - SUPPLIER0 + SKU54: + - SUPPLIER0 + SKU540: + - SUPPLIER0 + SKU541: + - SUPPLIER0 + SKU542: + - SUPPLIER0 + SKU543: + - SUPPLIER0 + SKU544: + - SUPPLIER0 + SKU545: + - SUPPLIER0 + SKU546: + - SUPPLIER0 + SKU547: + - SUPPLIER0 + SKU548: + - SUPPLIER0 + SKU549: + - SUPPLIER0 + SKU55: + - SUPPLIER0 + SKU550: + - SUPPLIER0 + SKU551: + - SUPPLIER0 + SKU552: + - SUPPLIER0 + SKU553: + - SUPPLIER0 + SKU554: + - SUPPLIER0 + SKU555: + - SUPPLIER0 + SKU556: + - SUPPLIER0 + SKU557: + - SUPPLIER0 + SKU558: + - SUPPLIER0 + SKU559: + - SUPPLIER0 + SKU56: + - SUPPLIER0 + SKU560: + - SUPPLIER0 + SKU561: + - SUPPLIER0 + SKU562: + - SUPPLIER0 + SKU563: + - SUPPLIER0 + SKU564: + - SUPPLIER0 + SKU565: + - SUPPLIER0 + SKU566: + - SUPPLIER0 + SKU567: + - SUPPLIER0 + SKU568: + - SUPPLIER0 + SKU569: + - SUPPLIER0 + SKU57: + - SUPPLIER0 + SKU570: + - SUPPLIER0 + SKU571: + - SUPPLIER0 + SKU572: + - SUPPLIER0 + SKU573: + - SUPPLIER0 + SKU574: + - SUPPLIER0 + SKU575: + - SUPPLIER0 + SKU576: + - SUPPLIER0 + SKU577: + - SUPPLIER0 + SKU578: + - SUPPLIER0 + SKU579: + - SUPPLIER0 + SKU58: + - SUPPLIER0 + SKU580: + - SUPPLIER0 + SKU581: + - SUPPLIER0 + SKU582: + - SUPPLIER0 + SKU583: + - SUPPLIER0 + SKU584: + - SUPPLIER0 + SKU585: + - SUPPLIER0 + SKU586: + - SUPPLIER0 + SKU587: + - SUPPLIER0 + SKU588: + - SUPPLIER0 + SKU589: + - SUPPLIER0 + SKU59: + - SUPPLIER0 + SKU590: + - SUPPLIER0 + SKU591: + - SUPPLIER0 + SKU592: + - SUPPLIER0 + SKU593: + - SUPPLIER0 + SKU594: + - SUPPLIER0 + SKU595: + - SUPPLIER0 + SKU596: + - SUPPLIER0 + SKU597: + - SUPPLIER0 + SKU598: + - SUPPLIER0 + SKU599: + - SUPPLIER0 + SKU6: + - SUPPLIER0 + SKU60: + - SUPPLIER0 + SKU600: + - SUPPLIER0 + SKU601: + - SUPPLIER0 + SKU602: + - SUPPLIER0 + SKU603: + - SUPPLIER0 + SKU604: + - SUPPLIER0 + SKU605: + - SUPPLIER0 + SKU606: + - SUPPLIER0 + SKU607: + - SUPPLIER0 + SKU608: + - SUPPLIER0 + SKU609: + - SUPPLIER0 + SKU61: + - SUPPLIER0 + SKU610: + - SUPPLIER0 + SKU611: + - SUPPLIER0 + SKU612: + - SUPPLIER0 + SKU613: + - SUPPLIER0 + SKU614: + - SUPPLIER0 + SKU615: + - SUPPLIER0 + SKU616: + - SUPPLIER0 + SKU617: + - SUPPLIER0 + SKU618: + - SUPPLIER0 + SKU619: + - SUPPLIER0 + SKU62: + - SUPPLIER0 + SKU620: + - SUPPLIER0 + SKU621: + - SUPPLIER0 + SKU622: + - SUPPLIER0 + SKU623: + - SUPPLIER0 + SKU624: + - SUPPLIER0 + SKU625: + - SUPPLIER0 + SKU626: + - SUPPLIER0 + SKU627: + - SUPPLIER0 + SKU628: + - SUPPLIER0 + SKU629: + - SUPPLIER0 + SKU63: + - SUPPLIER0 + SKU630: + - SUPPLIER0 + SKU631: + - SUPPLIER0 + SKU632: + - SUPPLIER0 + SKU633: + - SUPPLIER0 + SKU634: + - SUPPLIER0 + SKU635: + - SUPPLIER0 + SKU636: + - SUPPLIER0 + SKU637: + - SUPPLIER0 + SKU638: + - SUPPLIER0 + SKU639: + - SUPPLIER0 + SKU64: + - SUPPLIER0 + SKU640: + - SUPPLIER0 + SKU641: + - SUPPLIER0 + SKU642: + - SUPPLIER0 + SKU643: + - SUPPLIER0 + SKU644: + - SUPPLIER0 + SKU645: + - SUPPLIER0 + SKU646: + - SUPPLIER0 + SKU647: + - SUPPLIER0 + SKU648: + - SUPPLIER0 + SKU649: + - SUPPLIER0 + SKU65: + - SUPPLIER0 + SKU650: + - SUPPLIER0 + SKU651: + - SUPPLIER0 + SKU652: + - SUPPLIER0 + SKU653: + - SUPPLIER0 + SKU654: + - SUPPLIER0 + SKU655: + - SUPPLIER0 + SKU656: + - SUPPLIER0 + SKU657: + - SUPPLIER0 + SKU658: + - SUPPLIER0 + SKU659: + - SUPPLIER0 + SKU66: + - SUPPLIER0 + SKU660: + - SUPPLIER0 + SKU661: + - SUPPLIER0 + SKU662: + - SUPPLIER0 + SKU663: + - SUPPLIER0 + SKU664: + - SUPPLIER0 + SKU665: + - SUPPLIER0 + SKU666: + - SUPPLIER0 + SKU667: + - SUPPLIER0 + SKU668: + - SUPPLIER0 + SKU669: + - SUPPLIER0 + SKU67: + - SUPPLIER0 + SKU670: + - SUPPLIER0 + SKU671: + - SUPPLIER0 + SKU672: + - SUPPLIER0 + SKU673: + - SUPPLIER0 + SKU674: + - SUPPLIER0 + SKU675: + - SUPPLIER0 + SKU676: + - SUPPLIER0 + SKU677: + - SUPPLIER0 + SKU678: + - SUPPLIER0 + SKU679: + - SUPPLIER0 + SKU68: + - SUPPLIER0 + SKU680: + - SUPPLIER0 + SKU681: + - SUPPLIER0 + SKU682: + - SUPPLIER0 + SKU683: + - SUPPLIER0 + SKU684: + - SUPPLIER0 + SKU685: + - SUPPLIER0 + SKU686: + - SUPPLIER0 + SKU687: + - SUPPLIER0 + SKU688: + - SUPPLIER0 + SKU689: + - SUPPLIER0 + SKU69: + - SUPPLIER0 + SKU690: + - SUPPLIER0 + SKU691: + - SUPPLIER0 + SKU692: + - SUPPLIER0 + SKU693: + - SUPPLIER0 + SKU694: + - SUPPLIER0 + SKU695: + - SUPPLIER0 + SKU696: + - SUPPLIER0 + SKU697: + - SUPPLIER0 + SKU698: + - SUPPLIER0 + SKU699: + - SUPPLIER0 + SKU7: + - SUPPLIER0 + SKU70: + - SUPPLIER0 + SKU700: + - SUPPLIER0 + SKU701: + - SUPPLIER0 + SKU702: + - SUPPLIER0 + SKU703: + - SUPPLIER0 + SKU704: + - SUPPLIER0 + SKU705: + - SUPPLIER0 + SKU706: + - SUPPLIER0 + SKU707: + - SUPPLIER0 + SKU708: + - SUPPLIER0 + SKU709: + - SUPPLIER0 + SKU71: + - SUPPLIER0 + SKU710: + - SUPPLIER0 + SKU711: + - SUPPLIER0 + SKU712: + - SUPPLIER0 + SKU713: + - SUPPLIER0 + SKU714: + - SUPPLIER0 + SKU715: + - SUPPLIER0 + SKU716: + - SUPPLIER0 + SKU717: + - SUPPLIER0 + SKU718: + - SUPPLIER0 + SKU719: + - SUPPLIER0 + SKU72: + - SUPPLIER0 + SKU720: + - SUPPLIER0 + SKU721: + - SUPPLIER0 + SKU722: + - SUPPLIER0 + SKU723: + - SUPPLIER0 + SKU724: + - SUPPLIER0 + SKU725: + - SUPPLIER0 + SKU726: + - SUPPLIER0 + SKU727: + - SUPPLIER0 + SKU728: + - SUPPLIER0 + SKU729: + - SUPPLIER0 + SKU73: + - SUPPLIER0 + SKU730: + - SUPPLIER0 + SKU731: + - SUPPLIER0 + SKU732: + - SUPPLIER0 + SKU733: + - SUPPLIER0 + SKU734: + - SUPPLIER0 + SKU735: + - SUPPLIER0 + SKU736: + - SUPPLIER0 + SKU737: + - SUPPLIER0 + SKU738: + - SUPPLIER0 + SKU739: + - SUPPLIER0 + SKU74: + - SUPPLIER0 + SKU740: + - SUPPLIER0 + SKU741: + - SUPPLIER0 + SKU742: + - SUPPLIER0 + SKU743: + - SUPPLIER0 + SKU744: + - SUPPLIER0 + SKU745: + - SUPPLIER0 + SKU746: + - SUPPLIER0 + SKU747: + - SUPPLIER0 + SKU748: + - SUPPLIER0 + SKU749: + - SUPPLIER0 + SKU75: + - SUPPLIER0 + SKU750: + - SUPPLIER0 + SKU751: + - SUPPLIER0 + SKU752: + - SUPPLIER0 + SKU753: + - SUPPLIER0 + SKU754: + - SUPPLIER0 + SKU755: + - SUPPLIER0 + SKU756: + - SUPPLIER0 + SKU757: + - SUPPLIER0 + SKU758: + - SUPPLIER0 + SKU759: + - SUPPLIER0 + SKU76: + - SUPPLIER0 + SKU760: + - SUPPLIER0 + SKU761: + - SUPPLIER0 + SKU762: + - SUPPLIER0 + SKU763: + - SUPPLIER0 + SKU764: + - SUPPLIER0 + SKU765: + - SUPPLIER0 + SKU766: + - SUPPLIER0 + SKU767: + - SUPPLIER0 + SKU768: + - SUPPLIER0 + SKU769: + - SUPPLIER0 + SKU77: + - SUPPLIER0 + SKU770: + - SUPPLIER0 + SKU771: + - SUPPLIER0 + SKU772: + - SUPPLIER0 + SKU773: + - SUPPLIER0 + SKU774: + - SUPPLIER0 + SKU775: + - SUPPLIER0 + SKU776: + - SUPPLIER0 + SKU777: + - SUPPLIER0 + SKU778: + - SUPPLIER0 + SKU779: + - SUPPLIER0 + SKU78: + - SUPPLIER0 + SKU780: + - SUPPLIER0 + SKU781: + - SUPPLIER0 + SKU782: + - SUPPLIER0 + SKU783: + - SUPPLIER0 + SKU784: + - SUPPLIER0 + SKU785: + - SUPPLIER0 + SKU786: + - SUPPLIER0 + SKU787: + - SUPPLIER0 + SKU788: + - SUPPLIER0 + SKU789: + - SUPPLIER0 + SKU79: + - SUPPLIER0 + SKU790: + - SUPPLIER0 + SKU791: + - SUPPLIER0 + SKU792: + - SUPPLIER0 + SKU793: + - SUPPLIER0 + SKU794: + - SUPPLIER0 + SKU795: + - SUPPLIER0 + SKU796: + - SUPPLIER0 + SKU797: + - SUPPLIER0 + SKU798: + - SUPPLIER0 + SKU799: + - SUPPLIER0 + SKU8: + - SUPPLIER0 + SKU80: + - SUPPLIER0 + SKU800: + - SUPPLIER0 + SKU801: + - SUPPLIER0 + SKU802: + - SUPPLIER0 + SKU803: + - SUPPLIER0 + SKU804: + - SUPPLIER0 + SKU805: + - SUPPLIER0 + SKU806: + - SUPPLIER0 + SKU807: + - SUPPLIER0 + SKU808: + - SUPPLIER0 + SKU809: + - SUPPLIER0 + SKU81: + - SUPPLIER0 + SKU810: + - SUPPLIER0 + SKU811: + - SUPPLIER0 + SKU812: + - SUPPLIER0 + SKU813: + - SUPPLIER0 + SKU814: + - SUPPLIER0 + SKU815: + - SUPPLIER0 + SKU816: + - SUPPLIER0 + SKU817: + - SUPPLIER0 + SKU818: + - SUPPLIER0 + SKU819: + - SUPPLIER0 + SKU82: + - SUPPLIER0 + SKU820: + - SUPPLIER0 + SKU821: + - SUPPLIER0 + SKU822: + - SUPPLIER0 + SKU823: + - SUPPLIER0 + SKU824: + - SUPPLIER0 + SKU825: + - SUPPLIER0 + SKU826: + - SUPPLIER0 + SKU827: + - SUPPLIER0 + SKU828: + - SUPPLIER0 + SKU829: + - SUPPLIER0 + SKU83: + - SUPPLIER0 + SKU830: + - SUPPLIER0 + SKU831: + - SUPPLIER0 + SKU832: + - SUPPLIER0 + SKU833: + - SUPPLIER0 + SKU834: + - SUPPLIER0 + SKU835: + - SUPPLIER0 + SKU836: + - SUPPLIER0 + SKU837: + - SUPPLIER0 + SKU838: + - SUPPLIER0 + SKU839: + - SUPPLIER0 + SKU84: + - SUPPLIER0 + SKU840: + - SUPPLIER0 + SKU841: + - SUPPLIER0 + SKU842: + - SUPPLIER0 + SKU843: + - SUPPLIER0 + SKU844: + - SUPPLIER0 + SKU845: + - SUPPLIER0 + SKU846: + - SUPPLIER0 + SKU847: + - SUPPLIER0 + SKU848: + - SUPPLIER0 + SKU849: + - SUPPLIER0 + SKU85: + - SUPPLIER0 + SKU850: + - SUPPLIER0 + SKU851: + - SUPPLIER0 + SKU852: + - SUPPLIER0 + SKU853: + - SUPPLIER0 + SKU854: + - SUPPLIER0 + SKU855: + - SUPPLIER0 + SKU856: + - SUPPLIER0 + SKU857: + - SUPPLIER0 + SKU858: + - SUPPLIER0 + SKU859: + - SUPPLIER0 + SKU86: + - SUPPLIER0 + SKU860: + - SUPPLIER0 + SKU861: + - SUPPLIER0 + SKU862: + - SUPPLIER0 + SKU863: + - SUPPLIER0 + SKU864: + - SUPPLIER0 + SKU865: + - SUPPLIER0 + SKU866: + - SUPPLIER0 + SKU867: + - SUPPLIER0 + SKU868: + - SUPPLIER0 + SKU869: + - SUPPLIER0 + SKU87: + - SUPPLIER0 + SKU870: + - SUPPLIER0 + SKU871: + - SUPPLIER0 + SKU872: + - SUPPLIER0 + SKU873: + - SUPPLIER0 + SKU874: + - SUPPLIER0 + SKU875: + - SUPPLIER0 + SKU876: + - SUPPLIER0 + SKU877: + - SUPPLIER0 + SKU878: + - SUPPLIER0 + SKU879: + - SUPPLIER0 + SKU88: + - SUPPLIER0 + SKU880: + - SUPPLIER0 + SKU881: + - SUPPLIER0 + SKU882: + - SUPPLIER0 + SKU883: + - SUPPLIER0 + SKU884: + - SUPPLIER0 + SKU885: + - SUPPLIER0 + SKU886: + - SUPPLIER0 + SKU887: + - SUPPLIER0 + SKU888: + - SUPPLIER0 + SKU889: + - SUPPLIER0 + SKU89: + - SUPPLIER0 + SKU890: + - SUPPLIER0 + SKU891: + - SUPPLIER0 + SKU892: + - SUPPLIER0 + SKU893: + - SUPPLIER0 + SKU894: + - SUPPLIER0 + SKU895: + - SUPPLIER0 + SKU896: + - SUPPLIER0 + SKU897: + - SUPPLIER0 + SKU898: + - SUPPLIER0 + SKU899: + - SUPPLIER0 + SKU9: + - SUPPLIER0 + SKU90: + - SUPPLIER0 + SKU900: + - SUPPLIER0 + SKU901: + - SUPPLIER0 + SKU902: + - SUPPLIER0 + SKU903: + - SUPPLIER0 + SKU904: + - SUPPLIER0 + SKU905: + - SUPPLIER0 + SKU906: + - SUPPLIER0 + SKU907: + - SUPPLIER0 + SKU908: + - SUPPLIER0 + SKU909: + - SUPPLIER0 + SKU91: + - SUPPLIER0 + SKU910: + - SUPPLIER0 + SKU911: + - SUPPLIER0 + SKU912: + - SUPPLIER0 + SKU913: + - SUPPLIER0 + SKU914: + - SUPPLIER0 + SKU915: + - SUPPLIER0 + SKU916: + - SUPPLIER0 + SKU917: + - SUPPLIER0 + SKU918: + - SUPPLIER0 + SKU919: + - SUPPLIER0 + SKU92: + - SUPPLIER0 + SKU920: + - SUPPLIER0 + SKU921: + - SUPPLIER0 + SKU922: + - SUPPLIER0 + SKU923: + - SUPPLIER0 + SKU924: + - SUPPLIER0 + SKU925: + - SUPPLIER0 + SKU926: + - SUPPLIER0 + SKU927: + - SUPPLIER0 + SKU928: + - SUPPLIER0 + SKU929: + - SUPPLIER0 + SKU93: + - SUPPLIER0 + SKU930: + - SUPPLIER0 + SKU931: + - SUPPLIER0 + SKU932: + - SUPPLIER0 + SKU933: + - SUPPLIER0 + SKU934: + - SUPPLIER0 + SKU935: + - SUPPLIER0 + SKU936: + - SUPPLIER0 + SKU937: + - SUPPLIER0 + SKU938: + - SUPPLIER0 + SKU939: + - SUPPLIER0 + SKU94: + - SUPPLIER0 + SKU940: + - SUPPLIER0 + SKU941: + - SUPPLIER0 + SKU942: + - SUPPLIER0 + SKU943: + - SUPPLIER0 + SKU944: + - SUPPLIER0 + SKU945: + - SUPPLIER0 + SKU946: + - SUPPLIER0 + SKU947: + - SUPPLIER0 + SKU948: + - SUPPLIER0 + SKU949: + - SUPPLIER0 + SKU95: + - SUPPLIER0 + SKU950: + - SUPPLIER0 + SKU951: + - SUPPLIER0 + SKU952: + - SUPPLIER0 + SKU953: + - SUPPLIER0 + SKU954: + - SUPPLIER0 + SKU955: + - SUPPLIER0 + SKU956: + - SUPPLIER0 + SKU957: + - SUPPLIER0 + SKU958: + - SUPPLIER0 + SKU959: + - SUPPLIER0 + SKU96: + - SUPPLIER0 + SKU960: + - SUPPLIER0 + SKU961: + - SUPPLIER0 + SKU962: + - SUPPLIER0 + SKU963: + - SUPPLIER0 + SKU964: + - SUPPLIER0 + SKU965: + - SUPPLIER0 + SKU966: + - SUPPLIER0 + SKU967: + - SUPPLIER0 + SKU968: + - SUPPLIER0 + SKU969: + - SUPPLIER0 + SKU97: + - SUPPLIER0 + SKU970: + - SUPPLIER0 + SKU971: + - SUPPLIER0 + SKU972: + - SUPPLIER0 + SKU973: + - SUPPLIER0 + SKU974: + - SUPPLIER0 + SKU975: + - SUPPLIER0 + SKU976: + - SUPPLIER0 + SKU977: + - SUPPLIER0 + SKU978: + - SUPPLIER0 + SKU979: + - SUPPLIER0 + SKU98: + - SUPPLIER0 + SKU980: + - SUPPLIER0 + SKU981: + - SUPPLIER0 + SKU982: + - SUPPLIER0 + SKU983: + - SUPPLIER0 + SKU984: + - SUPPLIER0 + SKU985: + - SUPPLIER0 + SKU986: + - SUPPLIER0 + SKU987: + - SUPPLIER0 + SKU988: + - SUPPLIER0 + SKU989: + - SUPPLIER0 + SKU99: + - SUPPLIER0 + SKU990: + - SUPPLIER0 + SKU991: + - SUPPLIER0 + SKU992: + - SUPPLIER0 + SKU993: + - SUPPLIER0 + SKU994: + - SUPPLIER0 + SKU995: + - SUPPLIER0 + SKU996: + - SUPPLIER0 + SKU997: + - SUPPLIER0 + SKU998: + - SUPPLIER0 + SKU999: + - SUPPLIER0 From 8ec0282b82b36f32f6a25b4eec365c577e9029e8 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Jul 2021 07:32:07 +0000 Subject: [PATCH 352/482] removed unwanted imports --- examples/scripts/docker/kill.sh | 1 + examples/templates/simple_learner.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/scripts/docker/kill.sh b/examples/scripts/docker/kill.sh index b5948128c..b67e3f298 100644 --- a/examples/scripts/docker/kill.sh +++ b/examples/scripts/docker/kill.sh @@ -4,4 +4,5 @@ BASEDIR=$(dirname "$0") # script to kill a previously launched training job. docker-compose -f $BASEDIR/docker-compose.yml down + rm $BASEDIR/docker-compose.yml \ No newline at end of file diff --git a/examples/templates/simple_learner.py b/examples/templates/simple_learner.py index 842602a35..68e2ae471 100644 --- a/examples/templates/simple_learner.py +++ b/examples/templates/simple_learner.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. import sys -import time from os.path import dirname, realpath from maro.rl.learning import SimpleLearner @@ -12,7 +11,6 @@ sys.path.insert(0, template_dir) from general import config, get_agent_wrapper, get_env_wrapper, log_dir -from policy_manager.policy_manager import get_policy_manager if __name__ == "__main__": From 894c376b16ab6222f37fb1c6e0f7370e00e754a8 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 2 Jul 2021 08:56:31 +0000 Subject: [PATCH 353/482] config change for testing sc scenario --- examples/supply_chain/env_wrapper.py | 4 ++-- examples/templates/config.yml | 12 ++++++------ examples/templates/general.py | 4 ++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index 388d30b62..fde731178 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -1160,8 +1160,8 @@ def calc(self): "scenario": "supply_chain", # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder # that contains a single config.yml and should be placed under examples/supply_chain/envs/ - "topology": "test", - "durations": 64 # number of ticks per episode + "topology": "random", + "durations": 5 # number of ticks per episode } def get_env_wrapper(): diff --git a/examples/templates/config.yml b/examples/templates/config.yml index 753591913..271d33d0a 100644 --- a/examples/templates/config.yml +++ b/examples/templates/config.yml @@ -1,14 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -job_name: cim-dqn -scenario: cim +job_name: sc +scenario: sc mode: sync -num_episodes: 10 -eval_schedule: 5 -num_steps: 100 +num_episodes: 1 +eval_schedule: 1 +num_steps: -1 max_lag: 0 -log_env_summary: true +log_env_summary: false sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node diff --git a/examples/templates/general.py b/examples/templates/general.py index cfd991f9f..0858c7a26 100644 --- a/examples/templates/general.py +++ b/examples/templates/general.py @@ -23,5 +23,9 @@ from cim.env_wrapper import get_env_wrapper from cim.agent_wrapper import get_agent_wrapper from cim.policy_index import create_rollout_policy_func, create_train_policy_func +if scenario == "sc": + from supply_chain.env_wrapper import get_env_wrapper + from supply_chain.agent_wrapper import get_agent_wrapper + from supply_chain.policy_index import create_rollout_policy_func, create_train_policy_func else: raise ValueError(f"Unsupported scenario: {scenario}. Supported scenarios: 'cim'") From 743e9f38657e248c14a3c9bfac371afd8577411f Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 4 Jul 2021 07:51:44 +0000 Subject: [PATCH 354/482] changes for perf testing --- examples/scripts/docker/build.sh | 2 +- examples/supply_chain/dqn.py | 6 +++--- examples/supply_chain/env_wrapper.py | 6 +++--- examples/templates/config.yml | 8 ++++---- maro/rl/policy/policy.py | 10 +++++----- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/scripts/docker/build.sh b/examples/scripts/docker/build.sh index 005cbe30a..d9c2895c9 100644 --- a/examples/scripts/docker/build.sh +++ b/examples/scripts/docker/build.sh @@ -1,7 +1,7 @@ #!/bin/bash BASEDIR=$(dirname "$0") -ROOTDIR=$BASEDIR/../../../../ +ROOTDIR=$BASEDIR/../../../ # script to build the docker image for running the CIM scenario. docker pull redis:6 diff --git a/examples/supply_chain/dqn.py b/examples/supply_chain/dqn.py index d0388c5f8..b7791339f 100644 --- a/examples/supply_chain/dqn.py +++ b/examples/supply_chain/dqn.py @@ -44,7 +44,7 @@ }, "experience_manager": { "rollout": { # for experience managers in actor processes - "capacity": 1000, + "capacity": 10000, # This determines how existing experiences are replaced when adding new experiences to a full experience # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. @@ -53,9 +53,9 @@ "replace": False }, "training": { # for experience managers in the learner process - "capacity": 128000, + "capacity": 100000, "overwrite_type": "rolling", - "batch_size": 2560, + "batch_size": 256, "replace": True } }, diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py index fde731178..8eb6b0a66 100644 --- a/examples/supply_chain/env_wrapper.py +++ b/examples/supply_chain/env_wrapper.py @@ -237,8 +237,8 @@ def get_rl_policy_state(self, state, agent_info): self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] - self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} - self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} + #self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} + #self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} np_state = self._serialize_state(state) return np_state @@ -1161,7 +1161,7 @@ def calc(self): # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder # that contains a single config.yml and should be placed under examples/supply_chain/envs/ "topology": "random", - "durations": 5 # number of ticks per episode + "durations": 200 # number of ticks per episode } def get_env_wrapper(): diff --git a/examples/templates/config.yml b/examples/templates/config.yml index 271d33d0a..3388623b8 100644 --- a/examples/templates/config.yml +++ b/examples/templates/config.yml @@ -4,21 +4,21 @@ job_name: sc scenario: sc mode: sync -num_episodes: 1 -eval_schedule: 1 +num_episodes: 5 +eval_schedule: 3 num_steps: -1 max_lag: 0 log_env_summary: false sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 3 + num_rollout_workers: 2 # max_receive_attempts: 2 # receive_timeout: 100 # in milli-seconds policy_manager: train_group: policy-manager train_mode: multi-node # single-process, multi-process, multi-node - num_trainers: 2 + num_trainers: 4 redis: host: maro-redis port: 6379 \ No newline at end of file diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 377ddc2dd..891bd2ba2 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -89,13 +89,13 @@ def on_experiences(self, exp: ExperienceSet) -> bool: """ self.experience_manager.put(exp) self._new_exp_counter += exp.size - print( - f"exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" - ) + # print( + # f"exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" + # ) if self.experience_manager.size >= self.warmup and self._new_exp_counter >= self.update_trigger: - t0 = time.time() + # t0 = time.time() self.update() - print(f"policy update time: {time.time() - t0}") + # print(f"policy update time: {time.time() - t0}") self._new_exp_counter = 0 return True From 8e97adc22e15b450bc6ac5f253d066c45893ab24 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 5 Jul 2021 11:29:28 +0800 Subject: [PATCH 355/482] Asynchronous Training (#364) * remote inference code draft * changed actor to rollout_worker and updated init files * removed unwanted import * updated inits * more async code * added async scripts * added async training code & scripts for CIM-dqn * changed async to async_tools to avoid conflict with python keyword * reverted unwanted change to dockerfile * added doc for policy server * addressed PR comments and fixed a bug in docker_compose_yml.py * fixed lint issue * resolved PR comment * resolved merge conflicts * added async templates * added proxy.close() for actor and policy_server * fixed incorrect position to add last episode to eval schedule * reverted unwanted changes * added missing async files * rm unwanted echo in kill.sh Co-authored-by: ysqyang --- examples/scripts/docker/docker_compose_yml.py | 22 ++- examples/templates/async/actor.py | 26 +++ examples/templates/async/policy_server.py | 24 +++ examples/templates/config.yml | 5 +- maro/rl/learning/async/__init__.py | 7 + maro/rl/learning/async/actor.py | 152 ++++++++++++++++++ maro/rl/learning/async/policy_server.py | 67 ++++++++ 7 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 examples/templates/async/actor.py create mode 100644 examples/templates/async/policy_server.py create mode 100644 maro/rl/learning/async/__init__.py create mode 100644 maro/rl/learning/async/actor.py create mode 100644 maro/rl/learning/async/policy_server.py diff --git a/examples/scripts/docker/docker_compose_yml.py b/examples/scripts/docker/docker_compose_yml.py index 8e1b3183a..5f58adaa1 100644 --- a/examples/scripts/docker/docker_compose_yml.py +++ b/examples/scripts/docker/docker_compose_yml.py @@ -57,8 +57,26 @@ worker_spec["container_name"] = str_id worker_spec["environment"] = [f"WORKERID={worker_id}"] docker_compose_manifest["services"][str_id] = worker_spec -else: - raise ValueError("Only sync mode is supported in this version") +elif mode == "async": + # policy server spec + docker_compose_manifest["services"]["policy_server"] = { + **common_spec, + **{ + "container_name": "policy_server", + "command": "python3 /maro/examples/templates/async/policy_server.py" + } + } + # actor spec + for actor_id in range(config["async"]["num_actors"]): + str_id = f"actor.{actor_id}" + actor_spec = deepcopy(common_spec) + del actor_spec["build"] + actor_spec["command"] = "python3 /maro/examples/templates/async/actor.py" + actor_spec["container_name"] = str_id + actor_spec["environment"] = [f"ACTORID={actor_id}"] + docker_compose_manifest["services"][str_id] = actor_spec +else: + raise ValueError(f"mode must be 'sync' or 'async', got {mode}") with open(join(script_dir, "docker-compose.yml"), "w") as fp: yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/templates/async/actor.py b/examples/templates/async/actor.py new file mode 100644 index 000000000..4981cada3 --- /dev/null +++ b/examples/templates/async/actor.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os import environ +from os.path import dirname, realpath + +from maro.rl.learning.async import actor + +template_dir = dirname(dirname(realpath(__file__))) # DQN directory +if template_dir not in sys.path: + sys.path.insert(0, template_dir) +from general import config, get_agent_wrapper, get_env_wrapper, log_dir + + +if __name__ == "__main__": + actor( + config["async"]["group"], + environ["ACTORID"], + get_env_wrapper(), + get_agent_wrapper(), + config["num_episodes"], + num_steps=config["num_steps"], + proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + log_dir=log_dir, + ) diff --git a/examples/templates/async/policy_server.py b/examples/templates/async/policy_server.py new file mode 100644 index 000000000..c61e6e3aa --- /dev/null +++ b/examples/templates/async/policy_server.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os.path import dirname, realpath + +from maro.rl.learning.async import policy_server + +template_dir = dirname(dirname(realpath(__file__))) # DQN directory +if template_dir not in sys.path: + sys.path.insert(0, template_dir) +from general import config, log_dir +from policy_manager.policy_manager import get_policy_manager + + +if __name__ == "__main__": + policy_server( + config["async"]["group"], + get_policy_manager(), + config["async"]["num_actors"], + max_lag=config["max_lag"], + proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + log_dir=log_dir + ) diff --git a/examples/templates/config.yml b/examples/templates/config.yml index 753591913..56a67c6e2 100644 --- a/examples/templates/config.yml +++ b/examples/templates/config.yml @@ -3,7 +3,7 @@ job_name: cim-dqn scenario: cim -mode: sync +mode: async num_episodes: 10 eval_schedule: 5 num_steps: 100 @@ -15,6 +15,9 @@ sync: num_rollout_workers: 3 # max_receive_attempts: 2 # receive_timeout: 100 # in milli-seconds +async: + group: async + num_actors: 3 policy_manager: train_group: policy-manager train_mode: multi-node # single-process, multi-process, multi-node diff --git a/maro/rl/learning/async/__init__.py b/maro/rl/learning/async/__init__.py new file mode 100644 index 000000000..6265ea7eb --- /dev/null +++ b/maro/rl/learning/async/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .actor import actor +from .policy_server import policy_server + +__all__ = ["actor", "policy_server"] diff --git a/maro/rl/learning/async/actor.py b/maro/rl/learning/async/actor.py new file mode 100644 index 000000000..c47b6df41 --- /dev/null +++ b/maro/rl/learning/async/actor.py @@ -0,0 +1,152 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from os import getcwd +from typing import List, Union + +from maro.communication import Proxy, SessionMessage, SessionType +from maro.rl.utils import MsgKey, MsgTag +from maro.utils import Logger + +from ..agent_wrapper import AgentWrapper +from ..env_wrapper import AbsEnvWrapper + + +def actor( + group: str, + actor_idx: int, + env_wrapper: AbsEnvWrapper, + agent_wrapper: AgentWrapper, + num_episodes: int, + num_steps: int = -1, + eval_env_wrapper: AbsEnvWrapper = None, + eval_schedule: Union[int, List[int]] = None, + log_env_summary: bool = True, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + """Controller for single-threaded learning workflows. + + Args: + group (str): Group name for the cluster that includes the server and all actors. + actor_idx (int): Integer actor index. The actor's ID in the cluster will be "ACTOR.{actor_idx}". + env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. + agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on how the implementation of the roll-out manager. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in + which case the roll-out will be executed until the end of the environment. + eval_env_wrapper_func (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training + environment wrapper will be used for evaluation. Defaults to None. + eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will + will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated + at the end of the training episodes given in the list. In any case, the policies will be evaluated + at the end of the last training episode. Defaults to None, in which case the policies will only be + evaluated after the last training episode. + log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end + of each episode. Defaults to True. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + eval_env_wrapper = env_wrapper if not eval_env_wrapper else eval_env_wrapper + peers = {"policy_server": 1} + proxy = Proxy(group, "actor", peers, component_name=f"ACTOR.{actor_idx}", **proxy_kwargs) + policy_server_address = proxy.peers["policy_server"][0] + logger = Logger(proxy.name, dump_folder=log_dir) + policy_version = None + + # evaluation schedule + if eval_schedule is None: + eval_schedule = [] + elif isinstance(eval_schedule, int): + num_eval_schedule = num_episodes // eval_schedule + eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] + else: + eval_schedule.sort() + + # always evaluate after the last episode + if not eval_schedule or num_episodes != eval_schedule[-1]: + eval_schedule.append(num_episodes) + + eval_point_index = 0 + + # get initial policy states from the policy manager + msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, policy_server_address) + reply = proxy.send(msg)[0] + policy_version = reply.body[MsgKey.VERSION] + agent_wrapper.set_policy_states(reply.body[MsgKey.POLICY_STATE]) + + # main loop + for ep in range(1, num_episodes + 1): + t0 = time.time() + num_experiences_collected = 0 + agent_wrapper.explore() + env_wrapper.reset() + env_wrapper.start() # get initial state + segment = 0 + while env_wrapper.state: + segment += 1 + logger.info( + f"Collecting simulation data (episode {ep}, segment {segment}, policy version {policy_version})" + ) + start_step_index = env_wrapper.step_index + 1 + steps_to_go = num_steps + while env_wrapper.state and steps_to_go: + env_wrapper.step(agent_wrapper.choose_action(env_wrapper.state)) + steps_to_go -= 1 + + logger.info( + f"Roll-out finished (episode {ep}, segment {segment}, " + f"steps {start_step_index} - {env_wrapper.step_index})" + ) + + exp_by_agent = env_wrapper.get_experiences() + policies_with_new_exp = agent_wrapper.on_experiences(exp_by_agent) + num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) + exp_by_policy = agent_wrapper.get_experiences_by_policy(policies_with_new_exp) + reply = proxy.send( + SessionMessage( + MsgTag.COLLECT_DONE, proxy.name, policy_server_address, + body={MsgKey.EXPERIENCES: exp_by_policy, MsgKey.VERSION: policy_version} + ) + )[0] + policy_version = reply.body[MsgKey.VERSION] + agent_wrapper.set_policy_states(reply.body[MsgKey.POLICY_STATE]) + + # update the exploration parameters + agent_wrapper.exploration_step() + + # performance details + if log_env_summary: + logger.info(f"ep {ep}: {env_wrapper.summary}") + + logger.info( + f"ep {ep} summary - " + f"running time: {time.time() - t0} " + f"env steps: {env_wrapper.step_index} " + f"experiences collected: {num_experiences_collected}" + ) + if ep == eval_schedule[eval_point_index]: + # evaluation + eval_point_index += 1 + logger.info("Evaluating...") + agent_wrapper.exploit() + eval_env_wrapper.reset() + eval_env_wrapper.start() # get initial state + while eval_env_wrapper.state: + action = agent_wrapper.choose_action(eval_env_wrapper.state) + eval_env_wrapper.step(action) + + # performance details + logger.info(f"Evaluation result: {eval_env_wrapper.summary}") + + # tell the policy server I'm all done. + proxy.isend(SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION)) + proxy.close() diff --git a/maro/rl/learning/async/policy_server.py b/maro/rl/learning/async/policy_server.py new file mode 100644 index 000000000..9af0a92ca --- /dev/null +++ b/maro/rl/learning/async/policy_server.py @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from os import getcwd + +from maro.communication import Proxy +from maro.rl.policy import AbsPolicyManager +from maro.rl.utils import MsgKey, MsgTag +from maro.utils import Logger + + +def policy_server( + group: str, + policy_manager: AbsPolicyManager, + num_actors: int, + max_lag: int = 0, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + """Policy server process. + + The process serves the latest policy states to a set of remote actors and receives simulated experiences from them. + + Args: + group (str): Group name for the cluster that includes the server and all actors. + policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that hosts all policies and updates + them using experiences collected by the actors. + num_actors (int): Number of remote actors to collect simulation experiences. + max_lag (int): Maximum policy version lag allowed for experiences collected from remote roll-out workers. + Experiences collected using policy versions older than (current_version - max_lag) will be discarded. + Defaults to 0, in which case only experiences collected using the latest policy version will be returned. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + peers = {"actor": num_actors} + name = "POLICY_SERVER" + proxy = Proxy(group, "policy_server", peers, component_name=name, **proxy_kwargs) + logger = Logger(name, dump_folder=log_dir) + + num_active_actors = num_actors + for msg in proxy.receive(): + if msg.tag == MsgTag.GET_INITIAL_POLICY_STATE: + proxy.reply( + msg, tag=MsgTag.POLICY_STATE, + body={MsgKey.POLICY_STATE: policy_manager.get_state(), MsgKey.VERSION: policy_manager.version} + ) + policy_manager.reset_update_status() + elif msg.tag == MsgTag.COLLECT_DONE: + if policy_manager.version - msg.body[MsgKey.VERSION] > max_lag: + logger.info( + f"Ignored a message because it contains experiences generated using a stale policy version. " + f"Expected experiences generated using policy versions no earlier than " + f"{policy_manager.version - max_lag}, got {msg.body[MsgKey.VERSION]}" + ) + else: + policy_manager.on_experiences(msg.body[MsgKey.EXPERIENCES]) + proxy.reply( + msg, tag=MsgTag.POLICY_STATE, + body={MsgKey.POLICY_STATE: policy_manager.get_state(), MsgKey.VERSION: policy_manager.version} + ) + policy_manager.reset_update_status() + elif msg.tag == MsgTag.DONE: + num_active_actors -= 1 + if num_active_actors == 0: + proxy.close() + return From fac6006959fbab7468c0f205e0c2d8022a64f959 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 5 Jul 2021 04:06:30 +0000 Subject: [PATCH 356/482] renamed sync to synchronous and async to asynchronous to avoid conflict with keyword --- examples/cim/agent_wrapper.py | 4 ++-- examples/cim/policy_index.py | 4 ++-- examples/scripts/docker/docker_compose_yml.py | 8 ++++---- examples/templates/{async => asynchronous}/actor.py | 2 +- .../templates/{async => asynchronous}/policy_server.py | 2 +- examples/templates/general.py | 2 +- examples/templates/policy_manager/policy_manager.py | 6 +++--- examples/templates/policy_manager/trainer.py | 4 ++-- examples/templates/{sync => synchronous}/learner.py | 4 +++- .../templates/{sync => synchronous}/rollout_worker.py | 2 +- maro/rl/learning/{async => asynchronous}/__init__.py | 0 maro/rl/learning/{async => asynchronous}/actor.py | 0 maro/rl/learning/{async => asynchronous}/policy_server.py | 0 maro/rl/learning/{sync => synchronous}/__init__.py | 0 maro/rl/learning/{sync => synchronous}/learner.py | 0 maro/rl/learning/{sync => synchronous}/rollout_manager.py | 0 maro/rl/learning/{sync => synchronous}/rollout_worker.py | 0 17 files changed, 20 insertions(+), 18 deletions(-) rename examples/templates/{async => asynchronous}/actor.py (93%) rename examples/templates/{async => asynchronous}/policy_server.py (92%) rename examples/templates/{sync => synchronous}/learner.py (93%) rename examples/templates/{sync => synchronous}/rollout_worker.py (91%) rename maro/rl/learning/{async => asynchronous}/__init__.py (100%) rename maro/rl/learning/{async => asynchronous}/actor.py (100%) rename maro/rl/learning/{async => asynchronous}/policy_server.py (100%) rename maro/rl/learning/{sync => synchronous}/__init__.py (100%) rename maro/rl/learning/{sync => synchronous}/learner.py (100%) rename maro/rl/learning/{sync => synchronous}/rollout_manager.py (100%) rename maro/rl/learning/{sync => synchronous}/rollout_worker.py (100%) diff --git a/examples/cim/agent_wrapper.py b/examples/cim/agent_wrapper.py index a849fc9bf..867f59d88 100644 --- a/examples/cim/agent_wrapper.py +++ b/examples/cim/agent_wrapper.py @@ -10,7 +10,7 @@ cim_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, cim_path) from env_wrapper import AGENT_IDS, env_config -from policy_index import create_rollout_policy_func +from policy_index import rollout_policy_func_index exploration_config = { @@ -28,7 +28,7 @@ def get_agent_wrapper(): **exploration_config ) return AgentWrapper( - {name: func() for name, func in create_rollout_policy_func.items()}, + {name: func() for name, func in rollout_policy_func_index.items()}, {name: name for name in AGENT_IDS}, exploration_dict={f"EpsilonGreedy": epsilon_greedy}, agent2exploration={name: "EpsilonGreedy" for name in AGENT_IDS} diff --git a/examples/cim/policy_index.py b/examples/cim/policy_index.py index 6dbf69667..ea80d03ca 100644 --- a/examples/cim/policy_index.py +++ b/examples/cim/policy_index.py @@ -11,5 +11,5 @@ from env_wrapper import AGENT_IDS # use agent IDs as policy names since each agent uses a separate policy -create_train_policy_func = {name: get_dqn_policy_for_training for name in AGENT_IDS} -create_rollout_policy_func = {name: get_dqn_policy_for_rollout for name in AGENT_IDS} +train_policy_func_index = {name: get_dqn_policy_for_training for name in AGENT_IDS} +rollout_policy_func_index = {name: get_dqn_policy_for_rollout for name in AGENT_IDS} diff --git a/examples/scripts/docker/docker_compose_yml.py b/examples/scripts/docker/docker_compose_yml.py index 5f58adaa1..0db5e8291 100644 --- a/examples/scripts/docker/docker_compose_yml.py +++ b/examples/scripts/docker/docker_compose_yml.py @@ -44,7 +44,7 @@ **common_spec, **{ "container_name": "learner", - "command": "python3 /maro/examples/templates/sync/learner.py" + "command": "python3 /maro/examples/templates/synchronous/learner.py" } } # rollout worker spec @@ -53,7 +53,7 @@ str_id = f"rollout_worker.{worker_id}" worker_spec = deepcopy(common_spec) del worker_spec["build"] - worker_spec["command"] = "python3 /maro/examples/templates/sync/rollout_worker.py" + worker_spec["command"] = "python3 /maro/examples/templates/synchronous/rollout_worker.py" worker_spec["container_name"] = str_id worker_spec["environment"] = [f"WORKERID={worker_id}"] docker_compose_manifest["services"][str_id] = worker_spec @@ -63,7 +63,7 @@ **common_spec, **{ "container_name": "policy_server", - "command": "python3 /maro/examples/templates/async/policy_server.py" + "command": "python3 /maro/examples/templates/asynchronous/policy_server.py" } } # actor spec @@ -71,7 +71,7 @@ str_id = f"actor.{actor_id}" actor_spec = deepcopy(common_spec) del actor_spec["build"] - actor_spec["command"] = "python3 /maro/examples/templates/async/actor.py" + actor_spec["command"] = "python3 /maro/examples/templates/asynchronous/actor.py" actor_spec["container_name"] = str_id actor_spec["environment"] = [f"ACTORID={actor_id}"] docker_compose_manifest["services"][str_id] = actor_spec diff --git a/examples/templates/async/actor.py b/examples/templates/asynchronous/actor.py similarity index 93% rename from examples/templates/async/actor.py rename to examples/templates/asynchronous/actor.py index 4981cada3..82df2e99a 100644 --- a/examples/templates/async/actor.py +++ b/examples/templates/asynchronous/actor.py @@ -5,7 +5,7 @@ from os import environ from os.path import dirname, realpath -from maro.rl.learning.async import actor +from maro.rl.learning.asynchronous import actor template_dir = dirname(dirname(realpath(__file__))) # DQN directory if template_dir not in sys.path: diff --git a/examples/templates/async/policy_server.py b/examples/templates/asynchronous/policy_server.py similarity index 92% rename from examples/templates/async/policy_server.py rename to examples/templates/asynchronous/policy_server.py index c61e6e3aa..18dc06fba 100644 --- a/examples/templates/async/policy_server.py +++ b/examples/templates/asynchronous/policy_server.py @@ -4,7 +4,7 @@ import sys from os.path import dirname, realpath -from maro.rl.learning.async import policy_server +from maro.rl.learning.asynchronous import policy_server template_dir = dirname(dirname(realpath(__file__))) # DQN directory if template_dir not in sys.path: diff --git a/examples/templates/general.py b/examples/templates/general.py index cfd991f9f..20eace23d 100644 --- a/examples/templates/general.py +++ b/examples/templates/general.py @@ -22,6 +22,6 @@ if scenario == "cim": from cim.env_wrapper import get_env_wrapper from cim.agent_wrapper import get_agent_wrapper - from cim.policy_index import create_rollout_policy_func, create_train_policy_func + from cim.policy_index import rollout_policy_func_index, train_policy_func_index else: raise ValueError(f"Unsupported scenario: {scenario}. Supported scenarios: 'cim'") diff --git a/examples/templates/policy_manager/policy_manager.py b/examples/templates/policy_manager/policy_manager.py index 0194cc2db..385f3c4ec 100644 --- a/examples/templates/policy_manager/policy_manager.py +++ b/examples/templates/policy_manager/policy_manager.py @@ -9,19 +9,19 @@ template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: sys.path.insert(0, template_dir) -from general import config, create_train_policy_func, log_dir +from general import config, train_policy_func_index, log_dir def get_policy_manager(): train_mode = config["policy_manager"]["train_mode"] num_trainers = config["policy_manager"]["num_trainers"] - policy_dict = {name: func() for name, func in create_train_policy_func.items()} + policy_dict = {name: func() for name, func in train_policy_func_index.items()} if train_mode == "single-process": return LocalPolicyManager(policy_dict, log_dir=log_dir) if train_mode == "multi-process": return MultiProcessPolicyManager( policy_dict, num_trainers, - create_train_policy_func, + train_policy_func_index, log_dir=log_dir ) if train_mode == "multi-node": diff --git a/examples/templates/policy_manager/trainer.py b/examples/templates/policy_manager/trainer.py index 1de0636bc..199ce80b6 100644 --- a/examples/templates/policy_manager/trainer.py +++ b/examples/templates/policy_manager/trainer.py @@ -10,14 +10,14 @@ template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: sys.path.insert(0, template_dir) -from general import config, create_train_policy_func, log_dir +from general import config, train_policy_func_index, log_dir if __name__ == "__main__": trainer_node( config["policy_manager"]["train_group"], int(environ["TRAINERID"]), - create_train_policy_func, + train_policy_func_index, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/examples/templates/sync/learner.py b/examples/templates/synchronous/learner.py similarity index 93% rename from examples/templates/sync/learner.py rename to examples/templates/synchronous/learner.py index f560c7baf..dd8d74edb 100644 --- a/examples/templates/sync/learner.py +++ b/examples/templates/synchronous/learner.py @@ -5,7 +5,9 @@ import time from os.path import dirname, realpath -from maro.rl.learning.sync import Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager +from maro.rl.learning.synchronous import ( + Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager +) template_dir = dirname(dirname((realpath(__file__)))) if template_dir not in sys.path: diff --git a/examples/templates/sync/rollout_worker.py b/examples/templates/synchronous/rollout_worker.py similarity index 91% rename from examples/templates/sync/rollout_worker.py rename to examples/templates/synchronous/rollout_worker.py index adbd19b77..401e83580 100644 --- a/examples/templates/sync/rollout_worker.py +++ b/examples/templates/synchronous/rollout_worker.py @@ -5,7 +5,7 @@ from os import environ from os.path import dirname, realpath -from maro.rl.learning.sync import rollout_worker_node +from maro.rl.learning.synchronous import rollout_worker_node template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: diff --git a/maro/rl/learning/async/__init__.py b/maro/rl/learning/asynchronous/__init__.py similarity index 100% rename from maro/rl/learning/async/__init__.py rename to maro/rl/learning/asynchronous/__init__.py diff --git a/maro/rl/learning/async/actor.py b/maro/rl/learning/asynchronous/actor.py similarity index 100% rename from maro/rl/learning/async/actor.py rename to maro/rl/learning/asynchronous/actor.py diff --git a/maro/rl/learning/async/policy_server.py b/maro/rl/learning/asynchronous/policy_server.py similarity index 100% rename from maro/rl/learning/async/policy_server.py rename to maro/rl/learning/asynchronous/policy_server.py diff --git a/maro/rl/learning/sync/__init__.py b/maro/rl/learning/synchronous/__init__.py similarity index 100% rename from maro/rl/learning/sync/__init__.py rename to maro/rl/learning/synchronous/__init__.py diff --git a/maro/rl/learning/sync/learner.py b/maro/rl/learning/synchronous/learner.py similarity index 100% rename from maro/rl/learning/sync/learner.py rename to maro/rl/learning/synchronous/learner.py diff --git a/maro/rl/learning/sync/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py similarity index 100% rename from maro/rl/learning/sync/rollout_manager.py rename to maro/rl/learning/synchronous/rollout_manager.py diff --git a/maro/rl/learning/sync/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py similarity index 100% rename from maro/rl/learning/sync/rollout_worker.py rename to maro/rl/learning/synchronous/rollout_worker.py From 60a74239d04e615d6a8d23b4b2eb6509ab118f59 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 5 Jul 2021 12:31:52 +0800 Subject: [PATCH 357/482] added missing policy version increment in LocalPolicyManager --- examples/templates/synchronous/learner.py | 2 -- maro/rl/learning/synchronous/rollout_manager.py | 2 +- maro/rl/policy/policy_manager.py | 3 +++ maro/rl/policy/trainer.py | 1 - 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/templates/synchronous/learner.py b/examples/templates/synchronous/learner.py index dd8d74edb..dcddca535 100644 --- a/examples/templates/synchronous/learner.py +++ b/examples/templates/synchronous/learner.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. import sys -import time from os.path import dirname, realpath from maro.rl.learning.synchronous import ( @@ -59,5 +58,4 @@ def get_rollout_manager(): eval_schedule=config["eval_schedule"], log_dir=log_dir ) - time.sleep(10) learner.run() diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index ef5196cc8..df72f498d 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -410,7 +410,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): MsgKey.EXPLORATION_STEP: self._exploration_step } - self._proxy.ibroadcast("rollout_worker", MsgTag.COLLECT, SessionType.TASK, body=msg_body) + self._proxy.iscatter(MsgTag.COLLECT, SessionType.TASK, [(worker_id, msg_body) for worker_id in self._workers]) self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") if self._exploration_step: diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index f9c691ff7..691f0606a 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -80,6 +80,7 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): self.updated.add(policy_name) if self.updated: + self._version += 1 self._logger.info(f"Updated policies {self.updated}") self._logger.debug(f"policy update time: {time.time() - t0}") @@ -147,6 +148,7 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): if self.updated: self._version += 1 + self._logger.info(f"Updated policies {self.updated}") def exit(self): """Tell the trainer processes to exit.""" @@ -211,6 +213,7 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): if self.updated: self._version += 1 + self._logger.info(f"Updated policies {self.updated}") def exit(self): """Tell the remote trainers to exit.""" diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index 6394a6492..5eafbbc45 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -95,6 +95,5 @@ def trainer_node( if policy_dict[name].on_experiences(exp) } } - logger.info(f"updated policies {list(msg_body[MsgKey.POLICY_STATE].keys())}") logger.debug(f"total policy update time: {time.time() - t0}") proxy.reply(msg, body=msg_body) From a16355454c1c85ff9edfaecf7a8808e096a19e9e Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 5 Jul 2021 08:24:57 +0000 Subject: [PATCH 358/482] refined rollout manager recv logic --- .../learning/synchronous/rollout_manager.py | 127 +++++++++--------- .../rl/learning/synchronous/rollout_worker.py | 5 +- maro/rl/utils/message_enums.py | 1 - 3 files changed, 67 insertions(+), 66 deletions(-) diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index df72f498d..4857baa68 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import time from abc import ABC, abstractmethod from collections import defaultdict from multiprocessing import Pipe, Process @@ -110,9 +109,6 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): Experiences for policy training. """ self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") - t0 = time.time() - learning_time = 0 - num_experiences_collected = 0 # start of new episode if self.env.state is None: @@ -144,14 +140,6 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if self._log_env_summary: self._logger.info(f"ep {ep}: {self.env.summary}") - self._logger.debug( - f"ep {ep} summary - " - f"running time: {time.time() - t0} " - f"env steps: {self.env.step_index} " - f"learning time: {learning_time} " - f"experiences collected: {num_experiences_collected}" - ) - return self.env.get_experiences() def evaluate(self, ep: int, policy_state_dict: dict): @@ -220,8 +208,6 @@ def __init__( self._num_steps = num_steps self._log_env_summary = log_env_summary self._num_eval_workers = num_eval_workers - self.total_experiences_collected = 0 - self.total_env_steps = 0 self._exploration_step = False self._worker_processes = [] @@ -278,8 +264,6 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): for conn in self._manager_ends: result = conn.recv() exp_by_policy = result["experiences"] - self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) - self.total_env_steps += result["num_steps"] for policy_name, exp in exp_by_policy.items(): combined_exp_by_policy[policy_name].extend(exp) @@ -331,11 +315,13 @@ class MultiNodeRolloutManager(AbsRolloutManager): num_workers (int): Number of remote roll-out workers. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - max_receive_attempts (int): Maximum number of attempts to receive results in ``collect``. Defaults to - None, in which case the number is set to ``num_workers``. - receive_timeout (int): Maximum wait time (in milliseconds) for each attempt to receive from the workers. This - This multiplied by ``max_receive_attempts`` give the upperbound for the amount of time to receive the - desired amount of data from workers. Defaults to None, in which case each receive attempt is blocking. + min_finished_workers (int): Minimum number of finished workers required for a ``collect`` call. Defaults to + None, in which case it will be set to ``num_workers``. + max_extra_recv_tries (int): Maximum number of attempts to receive worker results after ``min_finished_workers`` + have been received in ``collect``. Defaults to None, in which case it is set to ``num_workers`` - + ``min_finished_workers``. + extra_recv_timeout (int): Timeout (in milliseconds) for each attempt to receive from a worker after + ``min_finished_workers`` have been received in ``collect``. Defaults to 100 (milliseconds). max_lag (int): Maximum policy version lag allowed for experiences collected from remote roll-out workers. Experiences collected using policy versions older than (current_version - max_lag) will be discarded. Defaults to 0, in which case only experiences collected using the latest policy version will be returned. @@ -353,8 +339,9 @@ def __init__( group: str, num_workers: int, num_steps: int = -1, - max_receive_attempts: int = None, - receive_timeout: int = None, + min_finished_workers: int = None, + max_extra_recv_tries: int = None, + extra_recv_timeout: int = None, max_lag: int = 0, num_eval_workers: int = 1, log_env_summary: bool = True, @@ -365,28 +352,29 @@ def __init__( raise ValueError("num_eval_workers cannot exceed the number of available workers") super().__init__() - self.num_workers = num_workers + self._num_workers = num_workers peers = {"rollout_worker": num_workers} self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's self._logger = Logger(self._proxy.name, dump_folder=log_dir) self._num_steps = num_steps + if min_finished_workers is None: + min_finished_workers = self._num_workers + self._logger.info(f"Minimum number of finished workers is set to {min_finished_workers}") + + self._min_finished_workers = min_finished_workers - if max_receive_attempts is None: - max_receive_attempts = self.num_workers - self._logger.info(f"Maximum receive attempts is set to {max_receive_attempts}") + if max_extra_recv_tries is None: + max_extra_recv_tries = self._num_workers - self._min_finished_workers + self._logger.info(f"Maximum number of extra receive tries is set to {max_extra_recv_tries}") - self.max_receive_attempts = max_receive_attempts - self.receive_timeout = receive_timeout + self._max_extra_recv_tries = max_extra_recv_tries + self._extra_recv_timeout = extra_recv_timeout self._max_lag = max_lag - self.total_experiences_collected = 0 - self.total_env_steps = 0 self._log_env_summary = log_env_summary - self._num_eval_workers = num_eval_workers - self._exploration_step = False def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): @@ -419,37 +407,21 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): # Receive roll-out results from remote workers combined_exp_by_policy = defaultdict(ExperienceSet) num_finishes = 0 - for _ in range(self.max_receive_attempts): - msg = self._proxy.receive_once(timeout=self.receive_timeout) - if msg.tag != MsgTag.COLLECT_DONE: - self._logger.info( - f"Ignored a message of type {msg.tag} (expected message type {MsgTag.COLLECT_DONE})" - ) - continue - - if version - msg.body[MsgKey.VERSION] > self._max_lag: - self._logger.info( - f"Ignored a message because it contains experiences generated using a stale policy version. " - f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " - f"got {msg.body[MsgKey.VERSION]}" - ) - continue - exp_by_policy = msg.body[MsgKey.EXPERIENCES] - self.total_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) - self.total_env_steps += msg.body[MsgKey.NUM_STEPS] - - for policy_name, exp in exp_by_policy.items(): - combined_exp_by_policy[policy_name].extend(exp) - - if msg.body[MsgKey.SEGMENT] == segment: - self.episode_complete = msg.body[MsgKey.EPISODE_END] - if self.episode_complete: - # log roll-out summary - if self._log_env_summary: - self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") - num_finishes += 1 - if num_finishes == self.num_workers: + # Ensure the minimum number of worker results are received. + for msg in self._proxy.receive(): + num_finishes += self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) + if num_finishes == self._min_finished_workers: + break + + # Keep trying to receive from workers, but with timeout + for _ in range(self._max_extra_recv_tries): + msg = self._proxy.receive_once(timeout=self._extra_recv_timeout) + if not msg: + self._logger.info("No message received") + else: + num_finishes += self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) + if num_finishes == self._num_workers: break if self.episode_complete: @@ -457,6 +429,35 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): return combined_exp_by_policy + def _handle_worker_result(self, msg, ep, segment, version, combined_exp): + if msg.tag != MsgTag.COLLECT_DONE: + self._logger.info( + f"Ignored a message of type {msg.tag} (expected message type {MsgTag.COLLECT_DONE})" + ) + return 0 + + if version - msg.body[MsgKey.VERSION] > self._max_lag: + self._logger.info( + f"Ignored a message because it contains experiences generated using a stale policy version. " + f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " + f"got {msg.body[MsgKey.VERSION]}" + ) + return 0 + + for policy_name, exp in msg.body[MsgKey.EXPERIENCES].items(): + combined_exp[policy_name].extend(exp) + + # The message is what we expect + if msg.body[MsgKey.EPISODE] == ep and msg.body[MsgKey.SEGMENT] == segment: + if MsgKey.ENV_SUMMARY in msg.body: + # log roll-out summary + if self._log_env_summary: + self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") + self.episode_complete = True + return 1 + + return 0 + def evaluate(self, ep: int, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``. diff --git a/maro/rl/learning/synchronous/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py index 40ca5aaee..a21bed2ce 100644 --- a/maro/rl/learning/synchronous/rollout_worker.py +++ b/maro/rl/learning/synchronous/rollout_worker.py @@ -167,15 +167,16 @@ def collect(msg): ret_exp = agent_wrapper.get_experiences_by_policy(policy_names) return_info = { - MsgKey.EPISODE_END: not env_wrapper.state, MsgKey.EPISODE: ep, MsgKey.SEGMENT: segment, MsgKey.VERSION: msg.body[MsgKey.VERSION], MsgKey.EXPERIENCES: ret_exp, - MsgKey.ENV_SUMMARY: env_wrapper.summary, MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1 } + if not env_wrapper.state: + return_info[MsgKey.ENV_SUMMARY] = env_wrapper.summary + proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) def evaluate(msg): diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 97fb125d5..7024f254a 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -35,4 +35,3 @@ class MsgKey(Enum): EXPLORATION_STEP = "exploration_step" VERSION = "version" NUM_STEPS = "num_steps" - EPISODE_END = "episode_end" From 803faad0778f19990601c6031db299650689aed1 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 5 Jul 2021 11:21:14 +0000 Subject: [PATCH 359/482] removed a debugging print --- examples/rl/README.md | 5 +++++ examples/{ => rl}/cim/README.md | 0 examples/{ => rl}/cim/__init__.py | 0 examples/{ => rl}/cim/ac.py | 0 examples/{ => rl}/cim/agent_wrapper.py | 0 examples/{ => rl}/cim/dqn.py | 0 examples/{ => rl}/cim/env_wrapper.py | 0 examples/{ => rl}/cim/policy_index.py | 0 examples/{ => rl}/scripts/docker/build.sh | 0 .../scripts/docker/docker_compose_yml.py | 18 +++++++++--------- examples/{ => rl}/scripts/docker/kill.sh | 0 examples/{ => rl}/scripts/docker/run.sh | 0 examples/{ => rl}/scripts/run.sh | 0 .../{ => rl}/templates/asynchronous/actor.py | 2 +- .../templates/asynchronous/policy_server.py | 0 examples/{ => rl}/templates/config.yml | 13 +++++++------ .../templates/policy_manager/policy_manager.py | 2 +- .../templates/policy_manager/trainer.py | 2 +- .../templates/scenario_index.py} | 10 +++++----- examples/{ => rl}/templates/simple_learner.py | 4 ++-- .../{ => rl}/templates/synchronous/learner.py | 5 ++++- .../templates/synchronous/rollout_worker.py | 2 +- maro/rl/learning/agent_wrapper.py | 1 - .../rl/learning/synchronous/rollout_manager.py | 4 ++-- 24 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 examples/rl/README.md rename examples/{ => rl}/cim/README.md (100%) rename examples/{ => rl}/cim/__init__.py (100%) rename examples/{ => rl}/cim/ac.py (100%) rename examples/{ => rl}/cim/agent_wrapper.py (100%) rename examples/{ => rl}/cim/dqn.py (100%) rename examples/{ => rl}/cim/env_wrapper.py (100%) rename examples/{ => rl}/cim/policy_index.py (100%) rename examples/{ => rl}/scripts/docker/build.sh (100%) rename examples/{ => rl}/scripts/docker/docker_compose_yml.py (78%) rename examples/{ => rl}/scripts/docker/kill.sh (100%) rename examples/{ => rl}/scripts/docker/run.sh (100%) rename examples/{ => rl}/scripts/run.sh (100%) rename examples/{ => rl}/templates/asynchronous/actor.py (89%) rename examples/{ => rl}/templates/asynchronous/policy_server.py (100%) rename examples/{ => rl}/templates/config.yml (76%) rename examples/{ => rl}/templates/policy_manager/policy_manager.py (95%) rename examples/{ => rl}/templates/policy_manager/trainer.py (90%) rename examples/{templates/general.py => rl/templates/scenario_index.py} (75%) rename examples/{ => rl}/templates/simple_learner.py (81%) rename examples/{ => rl}/templates/synchronous/learner.py (86%) rename examples/{ => rl}/templates/synchronous/rollout_worker.py (89%) diff --git a/examples/rl/README.md b/examples/rl/README.md new file mode 100644 index 000000000..deed62268 --- /dev/null +++ b/examples/rl/README.md @@ -0,0 +1,5 @@ +# Reinforcement Learning (RL) Examples + +This folder contains scenarios that employ reinforcement learning. It is possible to apply a common workflow to different scenarios, thanks to the abstractions from MARO's RL toolkit. The workflow can be found under ``templates``, which contains Python scripts for running the necessary components in single-threaded, synchronous and asynchronous modes. The scenario can be set + +To run single-threaded mode, \ No newline at end of file diff --git a/examples/cim/README.md b/examples/rl/cim/README.md similarity index 100% rename from examples/cim/README.md rename to examples/rl/cim/README.md diff --git a/examples/cim/__init__.py b/examples/rl/cim/__init__.py similarity index 100% rename from examples/cim/__init__.py rename to examples/rl/cim/__init__.py diff --git a/examples/cim/ac.py b/examples/rl/cim/ac.py similarity index 100% rename from examples/cim/ac.py rename to examples/rl/cim/ac.py diff --git a/examples/cim/agent_wrapper.py b/examples/rl/cim/agent_wrapper.py similarity index 100% rename from examples/cim/agent_wrapper.py rename to examples/rl/cim/agent_wrapper.py diff --git a/examples/cim/dqn.py b/examples/rl/cim/dqn.py similarity index 100% rename from examples/cim/dqn.py rename to examples/rl/cim/dqn.py diff --git a/examples/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py similarity index 100% rename from examples/cim/env_wrapper.py rename to examples/rl/cim/env_wrapper.py diff --git a/examples/cim/policy_index.py b/examples/rl/cim/policy_index.py similarity index 100% rename from examples/cim/policy_index.py rename to examples/rl/cim/policy_index.py diff --git a/examples/scripts/docker/build.sh b/examples/rl/scripts/docker/build.sh similarity index 100% rename from examples/scripts/docker/build.sh rename to examples/rl/scripts/docker/build.sh diff --git a/examples/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py similarity index 78% rename from examples/scripts/docker/docker_compose_yml.py rename to examples/rl/scripts/docker/docker_compose_yml.py index 0db5e8291..4d88d375a 100644 --- a/examples/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -7,9 +7,9 @@ path = realpath(__file__) script_dir = dirname(path) -example_dir = dirname(dirname(script_dir)) -root_dir = dirname(example_dir) -template_dir = join(example_dir, "templates") +rl_example_dir = dirname(dirname(script_dir)) +root_dir = dirname(dirname(rl_example_dir)) +template_dir = join(rl_example_dir, "templates") maro_rl_dir = join(root_dir, "maro", "rl") config_path = join(template_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") @@ -23,7 +23,7 @@ common_spec = { "build": {"context": root_dir, "dockerfile": dockerfile_path}, "image": "maro", - "volumes": [f"{example_dir}:/maro/examples", f"{maro_rl_dir}:/maro/maro/rl"] + "volumes": [f"{rl_example_dir}:/maro/rl_examples", f"{maro_rl_dir}:/maro/maro/rl"] } # trainer spec @@ -32,7 +32,7 @@ str_id = f"trainer.{trainer_id}" trainer_spec = deepcopy(common_spec) del trainer_spec["build"] - trainer_spec["command"] = "python3 /maro/examples/templates/policy_manager/trainer.py" + trainer_spec["command"] = "python3 /maro/rl_examples/templates/policy_manager/trainer.py" trainer_spec["container_name"] = str_id trainer_spec["environment"] = [f"TRAINERID={trainer_id}"] docker_compose_manifest["services"][str_id] = trainer_spec @@ -44,7 +44,7 @@ **common_spec, **{ "container_name": "learner", - "command": "python3 /maro/examples/templates/synchronous/learner.py" + "command": "python3 /maro/rl_examples/templates/synchronous/learner.py" } } # rollout worker spec @@ -53,7 +53,7 @@ str_id = f"rollout_worker.{worker_id}" worker_spec = deepcopy(common_spec) del worker_spec["build"] - worker_spec["command"] = "python3 /maro/examples/templates/synchronous/rollout_worker.py" + worker_spec["command"] = "python3 /maro/rl_examples/templates/synchronous/rollout_worker.py" worker_spec["container_name"] = str_id worker_spec["environment"] = [f"WORKERID={worker_id}"] docker_compose_manifest["services"][str_id] = worker_spec @@ -63,7 +63,7 @@ **common_spec, **{ "container_name": "policy_server", - "command": "python3 /maro/examples/templates/asynchronous/policy_server.py" + "command": "python3 /maro/rl_examples/templates/asynchronous/policy_server.py" } } # actor spec @@ -71,7 +71,7 @@ str_id = f"actor.{actor_id}" actor_spec = deepcopy(common_spec) del actor_spec["build"] - actor_spec["command"] = "python3 /maro/examples/templates/asynchronous/actor.py" + actor_spec["command"] = "python3 /maro/rl_examples/templates/asynchronous/actor.py" actor_spec["container_name"] = str_id actor_spec["environment"] = [f"ACTORID={actor_id}"] docker_compose_manifest["services"][str_id] = actor_spec diff --git a/examples/scripts/docker/kill.sh b/examples/rl/scripts/docker/kill.sh similarity index 100% rename from examples/scripts/docker/kill.sh rename to examples/rl/scripts/docker/kill.sh diff --git a/examples/scripts/docker/run.sh b/examples/rl/scripts/docker/run.sh similarity index 100% rename from examples/scripts/docker/run.sh rename to examples/rl/scripts/docker/run.sh diff --git a/examples/scripts/run.sh b/examples/rl/scripts/run.sh similarity index 100% rename from examples/scripts/run.sh rename to examples/rl/scripts/run.sh diff --git a/examples/templates/asynchronous/actor.py b/examples/rl/templates/asynchronous/actor.py similarity index 89% rename from examples/templates/asynchronous/actor.py rename to examples/rl/templates/asynchronous/actor.py index 82df2e99a..b9c7455d7 100644 --- a/examples/templates/asynchronous/actor.py +++ b/examples/rl/templates/asynchronous/actor.py @@ -10,7 +10,7 @@ template_dir = dirname(dirname(realpath(__file__))) # DQN directory if template_dir not in sys.path: sys.path.insert(0, template_dir) -from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from scenario_index import config, get_agent_wrapper, get_env_wrapper, log_dir if __name__ == "__main__": diff --git a/examples/templates/asynchronous/policy_server.py b/examples/rl/templates/asynchronous/policy_server.py similarity index 100% rename from examples/templates/asynchronous/policy_server.py rename to examples/rl/templates/asynchronous/policy_server.py diff --git a/examples/templates/config.yml b/examples/rl/templates/config.yml similarity index 76% rename from examples/templates/config.yml rename to examples/rl/templates/config.yml index 56a67c6e2..30e276f5d 100644 --- a/examples/templates/config.yml +++ b/examples/rl/templates/config.yml @@ -3,18 +3,19 @@ job_name: cim-dqn scenario: cim -mode: async -num_episodes: 10 +mode: sync +num_episodes: 5 eval_schedule: 5 -num_steps: 100 +num_steps: -1 max_lag: 0 log_env_summary: true sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 3 - # max_receive_attempts: 2 - # receive_timeout: 100 # in milli-seconds + num_rollout_workers: 6 + min_finished_workers: 4 + # max_extra_recv_tries: 3 + extra_recv_timeout: 100 async: group: async num_actors: 3 diff --git a/examples/templates/policy_manager/policy_manager.py b/examples/rl/templates/policy_manager/policy_manager.py similarity index 95% rename from examples/templates/policy_manager/policy_manager.py rename to examples/rl/templates/policy_manager/policy_manager.py index 385f3c4ec..3778d6029 100644 --- a/examples/templates/policy_manager/policy_manager.py +++ b/examples/rl/templates/policy_manager/policy_manager.py @@ -9,7 +9,7 @@ template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: sys.path.insert(0, template_dir) -from general import config, train_policy_func_index, log_dir +from scenario_index import config, train_policy_func_index, log_dir def get_policy_manager(): train_mode = config["policy_manager"]["train_mode"] diff --git a/examples/templates/policy_manager/trainer.py b/examples/rl/templates/policy_manager/trainer.py similarity index 90% rename from examples/templates/policy_manager/trainer.py rename to examples/rl/templates/policy_manager/trainer.py index 199ce80b6..061edb2e9 100644 --- a/examples/templates/policy_manager/trainer.py +++ b/examples/rl/templates/policy_manager/trainer.py @@ -10,7 +10,7 @@ template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: sys.path.insert(0, template_dir) -from general import config, train_policy_func_index, log_dir +from scenario_index import config, train_policy_func_index, log_dir if __name__ == "__main__": diff --git a/examples/templates/general.py b/examples/rl/templates/scenario_index.py similarity index 75% rename from examples/templates/general.py rename to examples/rl/templates/scenario_index.py index 20eace23d..1f1139cf0 100644 --- a/examples/templates/general.py +++ b/examples/rl/templates/scenario_index.py @@ -5,18 +5,18 @@ import yaml from os.path import dirname, join, realpath -template_dir = dirname(realpath(__file__)) -example_dir = dirname(template_dir) +template_dir = dirname(realpath(__file__)) +rl_example_dir = dirname(template_dir) if template_dir not in sys.path: sys.path.insert(0, template_dir) -if example_dir not in sys.path: - sys.path.insert(0, example_dir) +if rl_example_dir not in sys.path: + sys.path.insert(0, rl_example_dir) config_path = join(template_dir, "config.yml") with open(config_path, "r") as config_file: config = yaml.safe_load(config_file) -log_dir = join(example_dir, "logs", config["job_name"]) +log_dir = join(rl_example_dir, "logs", config["job_name"]) scenario = config["scenario"] if scenario == "cim": diff --git a/examples/templates/simple_learner.py b/examples/rl/templates/simple_learner.py similarity index 81% rename from examples/templates/simple_learner.py rename to examples/rl/templates/simple_learner.py index 68e2ae471..f38104f02 100644 --- a/examples/templates/simple_learner.py +++ b/examples/rl/templates/simple_learner.py @@ -6,11 +6,11 @@ from maro.rl.learning import SimpleLearner -template_dir = dirname(dirname((realpath(__file__)))) +template_dir = dirname((realpath(__file__))) if template_dir not in sys.path: sys.path.insert(0, template_dir) -from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from scenario_index import config, get_agent_wrapper, get_env_wrapper, log_dir if __name__ == "__main__": diff --git a/examples/templates/synchronous/learner.py b/examples/rl/templates/synchronous/learner.py similarity index 86% rename from examples/templates/synchronous/learner.py rename to examples/rl/templates/synchronous/learner.py index dcddca535..1dc5f6e69 100644 --- a/examples/templates/synchronous/learner.py +++ b/examples/rl/templates/synchronous/learner.py @@ -12,8 +12,8 @@ if template_dir not in sys.path: sys.path.insert(0, template_dir) -from general import config, get_agent_wrapper, get_env_wrapper, log_dir from policy_manager.policy_manager import get_policy_manager +from scenario_index import config, get_agent_wrapper, get_env_wrapper, log_dir def get_rollout_manager(): @@ -41,6 +41,9 @@ def get_rollout_manager(): config["sync"]["num_rollout_workers"], num_steps=config["num_steps"], max_lag=config["max_lag"], + min_finished_workers=config["sync"]["min_finished_workers"], + # max_extra_recv_tries=config["sync"]["max_extra_recv_tries"], + extra_recv_timeout=config["sync"]["extra_recv_timeout"], log_env_summary=config["log_env_summary"], proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} ) diff --git a/examples/templates/synchronous/rollout_worker.py b/examples/rl/templates/synchronous/rollout_worker.py similarity index 89% rename from examples/templates/synchronous/rollout_worker.py rename to examples/rl/templates/synchronous/rollout_worker.py index 401e83580..ae7e1441b 100644 --- a/examples/templates/synchronous/rollout_worker.py +++ b/examples/rl/templates/synchronous/rollout_worker.py @@ -10,7 +10,7 @@ template_dir = dirname(dirname(realpath(__file__))) # template directory if template_dir not in sys.path: sys.path.insert(0, template_dir) -from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from scenario_index import config, get_agent_wrapper, get_env_wrapper, log_dir if __name__ == "__main__": diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index 4821a9fd4..aa93eb427 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -74,7 +74,6 @@ def exploration_step(self): if self.exploration_dict: for exploration in self.exploration_dict.values(): exploration.step() - # print(f"epsilon: {exploration.epsilon}") def exploit(self): self.exploring = False diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 4857baa68..c0524711c 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -415,10 +415,10 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): break # Keep trying to receive from workers, but with timeout - for _ in range(self._max_extra_recv_tries): + for i in range(self._max_extra_recv_tries): msg = self._proxy.receive_once(timeout=self._extra_recv_timeout) if not msg: - self._logger.info("No message received") + self._logger.info(f"Receive timeout, {self._max_extra_recv_tries - i - 1} attempts left") else: num_finishes += self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) if num_finishes == self._num_workers: From 34b47a5ded0e54137747c9233edadadcf95aeb56 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 6 Jul 2021 03:26:03 +0000 Subject: [PATCH 360/482] added sleep in distributed launcher to avoid hanging --- examples/supply_chain/distributed_launcher.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/supply_chain/distributed_launcher.py b/examples/supply_chain/distributed_launcher.py index fec5d0e05..026850b49 100644 --- a/examples/supply_chain/distributed_launcher.py +++ b/examples/supply_chain/distributed_launcher.py @@ -3,6 +3,7 @@ import argparse import sys +import time from multiprocessing import Process from os import getenv, makedirs from os.path import dirname, join, realpath @@ -48,6 +49,7 @@ def sc_learner(): log_env_metrics=config["log_env_metrics"], log_dir=log_dir ) + learner.run() @@ -61,7 +63,7 @@ def sc_actor(name: str): redis_address=(REDIS_HOST, REDIS_PORT), log_dir=log_dir ) - + time.sleep(10) actor.run() From c41ca357dd0c54bef9ef2059f78d4ea223c9883c Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 7 Jul 2021 16:48:11 +0800 Subject: [PATCH 361/482] updated api doc and rl toolkit doc --- docs/source/apidoc/maro.rl.rst | 70 +++++++++++-------- docs/source/key_components/rl_toolkit.rst | 44 ++++++++++-- examples/rl/README.md | 17 ++++- examples/rl/cim/README.md | 15 ++-- examples/rl/cim/__init__.py | 6 ++ examples/rl/cim/agent_wrapper.py | 4 +- examples/rl/cim/dqn.py | 27 +++---- examples/rl/cim/policy_index.py | 5 +- .../rl/scripts/docker/docker_compose_yml.py | 4 +- examples/rl/templates/scenario_index.py | 27 ------- .../asynchronous/actor.py | 9 +-- .../asynchronous/policy_server.py | 8 +-- .../rl/{templates => workflows}/config.yml | 0 examples/rl/workflows/general.py | 21 ++++++ .../policy_manager/policy_manager.py | 13 ++-- .../policy_manager/trainer.py | 11 +-- .../simple_learner.py | 8 +-- .../synchronous/learner.py | 8 +-- .../synchronous/rollout_worker.py | 9 +-- .../rl/learning/asynchronous/policy_server.py | 4 +- 20 files changed, 183 insertions(+), 127 deletions(-) delete mode 100644 examples/rl/templates/scenario_index.py rename examples/rl/{templates => workflows}/asynchronous/actor.py (72%) rename examples/rl/{templates => workflows}/asynchronous/policy_server.py (81%) rename examples/rl/{templates => workflows}/config.yml (100%) create mode 100644 examples/rl/workflows/general.py rename examples/rl/{templates => workflows}/policy_manager/policy_manager.py (77%) rename examples/rl/{templates => workflows}/policy_manager/trainer.py (65%) rename examples/rl/{templates => workflows}/simple_learner.py (71%) rename examples/rl/{templates => workflows}/synchronous/learner.py (91%) rename examples/rl/{templates => workflows}/synchronous/rollout_worker.py (71%) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index e93061ffe..9ade723bc 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -34,18 +34,6 @@ maro.rl.algorithms.pg :show-inheritance: -Environment Wrapper -================================================================================ - -maro.rl.env_wrapper.env_wrapper --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.env_wrapper.env_wrapper - :members: - :undoc-members: - :show-inheritance: - - Experience ================================================================================ @@ -141,70 +129,94 @@ maro.rl.policy.policy :undoc-members: :show-inheritance: +maro.rl.policy.policy_manager +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.policy_manager + :members: + :undoc-members: + :show-inheritance: -Training +maro.rl.policy.trainer +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.trainer + :members: + :undoc-members: + :show-inheritance: + + +Learning ================================================================================ -maro.rl.training.agent_wrapper +maro.rl.learning.env_wrapper +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.learning.env_wrapper + :members: + :undoc-members: + :show-inheritance: + +maro.rl.learning.agent_wrapper -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.agent_wrapper +.. automodule:: maro.rl.learning.agent_wrapper :members: :undoc-members: :show-inheritance: -maro.rl.training.early_stopper +maro.rl.learning.early_stopper -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.early_stopper +.. automodule:: maro.rl.learning.early_stopper :members: :undoc-members: :show-inheritance: -maro.rl.training.learner +maro.rl.learning.simple_learner -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.learner +.. automodule:: maro.rl.learning.simple_learner :members: :undoc-members: :show-inheritance: -maro.rl.training.local_learner +maro.rl.learning.synchronous.learner -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.local_learner +.. automodule:: maro.rl.learning.synchronous.learner :members: :undoc-members: :show-inheritance: -maro.rl.training.trainer +maro.rl.learning.synchronous.rollout_manager -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.trainer +.. automodule:: maro.rl.learning.synchronous.rollout_manager :members: :undoc-members: :show-inheritance: -maro.rl.training.rollout_manager +maro.rl.learning.synchronous.rollout_worker -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.rollout_manager +.. automodule:: maro.rl.learning.synchronous.rollout_worker :members: :undoc-members: :show-inheritance: -maro.rl.training.rollout_worker +maro.rl.learning.asynchronous.actor -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.rollout_worker +.. automodule:: maro.rl.learning.asynchronous.actor :members: :undoc-members: :show-inheritance: -maro.rl.training.policy_manager +maro.rl.learning.asynchronous.policy_server -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.policy_manager +.. automodule:: maro.rl.learning.asynchronous.policy_server :members: :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 5d3188405..5c06efa50 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -2,11 +2,31 @@ RL Toolkit ========== MARO provides a full-stack abstraction for reinforcement learning (RL) which includes various customizable -components. At the top level of a training workflow are: +components. In order to provide a gentle introduction for the RL toolkit, we cover the components in top-down +fashion, starting from the outermost layer, the learning workflows. The RL toolkit supports single-threaded and +distributed learning workflows. The distributed workflow can be synchronous or asynchronous. -* Learner, which consists of a roll-out manager and a training manager, is the controller for a learning - process. The learner process executes training cycles that alternate between data collection and policy - updates. + +Synchronous Learning +==================== + +In synchronous mode, a central controler executes learning cycles that consist of simulation data collection and +policy update. In a strictly synchronous learning process, the coordinator would wait for all data collectors, +a.k.a. "roll-out workers", to return their results before moving on to the policy update phase. So what if a slow +worker drags the whole process down? We provide users the flexibility to loosen the restriction by specifying the +minimum number of workers required to report back before proceeding to the next phase. If one is concerned about +losing precious data in the case of all workers reporting back at roughly the same time, we also provide the option +to continue to receive after receiving the minimum number of results, but with timeouts to keep the wait time +upperbounded. Note that the transition from the policy update phase to the data collection phase is still strictly +synchronous. This means that in the case of the policy instances distributed amongst a set of trainer nodes, the +central controller waits until all trainers report back with the latest policy states before starting the next +cycle. + + +The components required for synchronous learning include: + +* Learner, which is the central coordinator for a learning process. The learner consists of a roll-out manager and +a training manager and executes learning cycles that alternate between data collection and policy update. * Rollout manager, which is responsible for collecting simulation data, in local or distributed fashion. * Policy manager, which manages a set of policies and controls their updates. The policy instances may reside with the manager or be distributed amongst a set of processes or remote nodes for parallelized training. @@ -27,6 +47,22 @@ components. At the top level of a training workflow are: :alt: RL Overview +Asynchronous Learning +===================== + +The asynchronous mode consists of a policy server and multiple actors. No central controller is present. Each data collector, +a.k.a., actor, operates independently. As soon as the server receives data from an actor, it feeds the data to the policy +manager for perform updates. The server then returns the updated policy states to the actor for the next round of data collection. +As the actors may differ significantly in speed, the policy server only uses data generated by policies sufficiently up-to-date. +but always sends the latest policy states to every single actor. + +The components required for asynchronous learning include: + +* actor, which alternates between sending collected simulation data to the policy server and receiving updated +policy states from it. +* policy server, which receives data from the actors and update the policy states when necessary. + + Environment Wrapper ------------------- diff --git a/examples/rl/README.md b/examples/rl/README.md index deed62268..e84fa1cdb 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -1,5 +1,18 @@ # Reinforcement Learning (RL) Examples -This folder contains scenarios that employ reinforcement learning. It is possible to apply a common workflow to different scenarios, thanks to the abstractions from MARO's RL toolkit. The workflow can be found under ``templates``, which contains Python scripts for running the necessary components in single-threaded, synchronous and asynchronous modes. The scenario can be set +This folder contains scenarios that employ reinforcement learning. It is possible to apply a common workflow to different scenarios, thanks to the abstractions from MARO's RL toolkit. The workflow can be found under ``workflows``, which contains Python scripts for running the necessary components in single-threaded and distributed modes. The scenario can be chosen by setting the ``scenario`` field in ``config.yml``. -To run single-threaded mode, \ No newline at end of file +## How to Run + +To run the single-threaded workflow, go to ``workflows`` and run ``python3 simple_learner.py``. Alternatively, go to ``scripts`` and run ``bash run.sh``. + +To run the distributed workflow, start by choosing "sync" or "async" for the ``mode`` field in ``config.yml``. The ``scrpts/docker`` folder provides bash scripts for simulating the distributed workflow using multiple docker containers on a single host. Go to this folder and execute ``bash run.sh`` to launch the program and Docker Compose will take care of starting the necessary containers. Note that the script will build the docker image first if it has not already been built by running ``bash build.sh``. When the program is finished, be sure to run ``bash kill.sh`` to clean up the containers and remove the network. + +## Write Your Own Scenarios + +To use the workflow provided under ``workflows``, the following ingredients are required: +* ``get_env_wrapper``, a function that takes no parameters and returns an environment wrapper instance. +* ``get_agent_wrapper``, a function that takes no parameters and returns an agent wrapper instance. +* ``policy_func_index``, a dictionary mapping policy names to functions that create them. +The policy-creating functions should take as its sole parameter a flag indicating whether the created policy is for roll-out or training. +We recommend that you follow the ``cim`` example to write your own scenario. diff --git a/examples/rl/cim/README.md b/examples/rl/cim/README.md index 44b657494..2d280f476 100644 --- a/examples/rl/cim/README.md +++ b/examples/rl/cim/README.md @@ -1,11 +1,10 @@ # Container Inventory Management -Container inventory management (CIM) is a scenario where reinforcement learning (RL) can potentially prove useful. Three algorithms are used to learn the multi-agent policy in given environments. Each algorithm has a ``config`` folder which contains ``agent_config.py`` and ``training_config.py``. The former contains parameters for the underlying models and algorithm specific hyper-parameters. The latter contains parameters for the environment and the main training loop. The file ``common.py`` contains parameters and utility functions shared by some or all of these algorithms. +Container inventory management (CIM) is a scenario where reinforcement learning (RL) can potentially prove useful. In this folder you can find: +* ``env_wrapper.py``, which contains a function to generate an environment wrapper to interact +with our "agent" (see below); +* ``agent_wrapper.py``, which contains a function to generate an agent wrapper to interact +with the environment wrapper; +* ``policy_index``, which maps policy names to functions that create them; the functions to create DQN and Actor-Critic policies are defined in ``dqn.py`` and ``ac.py``, respectively. -In the ``ac`` folder, , the policy is trained using the Actor-Critc algorithm in single-threaded fashion. The example can be run by simply executing ``python3 main.py``. Logs will be saved in a file named ``cim-ac.CURRENT_TIME_STAMP.log`` under the ``ac/logs`` folder, where ``CURRENT_TIME_STAMP`` is the time of executing the script. - -In the ``dqn`` folder, the policy is trained using the DQN algorithm in multi-process / distributed mode. This example can be run in three ways. -* ``python3 main.py`` or ``python3 main.py -w 0`` runs the example in multi-process mode, in which a main process spawns one learner process and a number of actor processes as specified in ``config/training_config.py``. -* ``python3 main.py -w 1`` launches the learner process only. This is for distributed training and expects a number of actor processes (as specified in ``config/training_config.py``) running on some other node(s). -* ``python3 main.py -w 2`` launches the actor process only. This is for distributed training and expects a learner process running on some other node. -Logs will be saved in a file named ``GROUP_NAME.log`` under the ``{ac_gnn, dqn}/logs`` folder, where ``GROUP_NAME`` is specified in the "group" field in ``config/training_config.py``. +The code for the actual learning workflows (e.g., learner, roll-out worker and trainer) can be found under ``examples/rl/workflows``. The reason for putting it in a separate folder is that these workflows apply to any scenario, so long as the necessary component generators, such as the ones listed above, are provided. See ``README`` under ``examples/rl`` for details. We recommend that you follow this example to write your own scenarios. \ No newline at end of file diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index b14b47650..5d2e7c317 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -1,2 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. + +from .agent_wrapper import get_agent_wrapper +from .env_wrapper import get_env_wrapper +from .policy_index import policy_func_index + +__all__ = ["get_agent_wrapper", "get_env_wrapper", "policy_func_index"] diff --git a/examples/rl/cim/agent_wrapper.py b/examples/rl/cim/agent_wrapper.py index 867f59d88..1f135756f 100644 --- a/examples/rl/cim/agent_wrapper.py +++ b/examples/rl/cim/agent_wrapper.py @@ -10,7 +10,7 @@ cim_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, cim_path) from env_wrapper import AGENT_IDS, env_config -from policy_index import rollout_policy_func_index +from policy_index import policy_func_index exploration_config = { @@ -28,7 +28,7 @@ def get_agent_wrapper(): **exploration_config ) return AgentWrapper( - {name: func() for name, func in rollout_policy_func_index.items()}, + {name: func(training=False) for name, func in policy_func_index.items()}, {name: name for name in AGENT_IDS}, exploration_dict={f"EpsilonGreedy": epsilon_greedy}, agent2exploration={name: "EpsilonGreedy" for name in AGENT_IDS} diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index 4b7ad3c8d..a8ce8d710 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -73,25 +73,18 @@ def forward(self, states): return self.component(states) -def get_dqn_policy_for_training(): +def get_dqn_policy(training: bool = True): qnet = QNet( FullyConnectedBlock(**config["model"]["network"]), - optim_option=OptimOption(**config["model"]["optimization"]) + optim_option=OptimOption(**config["model"]["optimization"]) if training else None ) + if training: + exp_manager = ExperienceManager(**config["experience_manager"]["training"]) + else: + exp_manager = ExperienceManager(**config["experience_manager"]["rollout"]) return DQN( - qnet, - ExperienceManager(**config["experience_manager"]["training"]), - DQNConfig(**config["algorithm"]), - update_trigger=config["update_trigger"], - warmup=config["warmup"] - ) - - -def get_dqn_policy_for_rollout(): - qnet = QNet(FullyConnectedBlock(**config["model"]["network"])) - return DQN( - qnet, - ExperienceManager(**config["experience_manager"]["rollout"]), - DQNConfig(**config["algorithm"]), - update_trigger=1e8 # set to a large number to ensure that the roll-out workers don't update policies + qnet, exp_manager, DQNConfig(**config["algorithm"]), + # set these to a large number to ensure that the roll-out workers don't update policies + update_trigger=config["update_trigger"] if training else 1e8, + warmup=config["warmup"] if training else 1e8 ) diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index ea80d03ca..585d7a3ab 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -7,9 +7,8 @@ cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) -from dqn import get_dqn_policy_for_rollout, get_dqn_policy_for_training +from dqn import get_dqn_policy from env_wrapper import AGENT_IDS # use agent IDs as policy names since each agent uses a separate policy -train_policy_func_index = {name: get_dqn_policy_for_training for name in AGENT_IDS} -rollout_policy_func_index = {name: get_dqn_policy_for_rollout for name in AGENT_IDS} +policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 4d88d375a..3fdaa25c1 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -9,9 +9,9 @@ script_dir = dirname(path) rl_example_dir = dirname(dirname(script_dir)) root_dir = dirname(dirname(rl_example_dir)) -template_dir = join(rl_example_dir, "templates") +workflow_dir = join(rl_example_dir, "templates") maro_rl_dir = join(root_dir, "maro", "rl") -config_path = join(template_dir, "config.yml") +config_path = join(workflow_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") with open(config_path, "r") as fp: diff --git a/examples/rl/templates/scenario_index.py b/examples/rl/templates/scenario_index.py deleted file mode 100644 index 1f1139cf0..000000000 --- a/examples/rl/templates/scenario_index.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -import yaml -from os.path import dirname, join, realpath - -template_dir = dirname(realpath(__file__)) -rl_example_dir = dirname(template_dir) -if template_dir not in sys.path: - sys.path.insert(0, template_dir) -if rl_example_dir not in sys.path: - sys.path.insert(0, rl_example_dir) - -config_path = join(template_dir, "config.yml") -with open(config_path, "r") as config_file: - config = yaml.safe_load(config_file) - -log_dir = join(rl_example_dir, "logs", config["job_name"]) - -scenario = config["scenario"] -if scenario == "cim": - from cim.env_wrapper import get_env_wrapper - from cim.agent_wrapper import get_agent_wrapper - from cim.policy_index import rollout_policy_func_index, train_policy_func_index -else: - raise ValueError(f"Unsupported scenario: {scenario}. Supported scenarios: 'cim'") diff --git a/examples/rl/templates/asynchronous/actor.py b/examples/rl/workflows/asynchronous/actor.py similarity index 72% rename from examples/rl/templates/asynchronous/actor.py rename to examples/rl/workflows/asynchronous/actor.py index b9c7455d7..4b9c9d225 100644 --- a/examples/rl/templates/asynchronous/actor.py +++ b/examples/rl/workflows/asynchronous/actor.py @@ -7,10 +7,11 @@ from maro.rl.learning.asynchronous import actor -template_dir = dirname(dirname(realpath(__file__))) # DQN directory -if template_dir not in sys.path: - sys.path.insert(0, template_dir) -from scenario_index import config, get_agent_wrapper, get_env_wrapper, log_dir +workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) + +from general import config, get_agent_wrapper, get_env_wrapper, log_dir if __name__ == "__main__": diff --git a/examples/rl/templates/asynchronous/policy_server.py b/examples/rl/workflows/asynchronous/policy_server.py similarity index 81% rename from examples/rl/templates/asynchronous/policy_server.py rename to examples/rl/workflows/asynchronous/policy_server.py index 18dc06fba..7e06fcb16 100644 --- a/examples/rl/templates/asynchronous/policy_server.py +++ b/examples/rl/workflows/asynchronous/policy_server.py @@ -6,11 +6,11 @@ from maro.rl.learning.asynchronous import policy_server -template_dir = dirname(dirname(realpath(__file__))) # DQN directory -if template_dir not in sys.path: - sys.path.insert(0, template_dir) -from general import config, log_dir +workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) from policy_manager.policy_manager import get_policy_manager +from general import config, log_dir if __name__ == "__main__": diff --git a/examples/rl/templates/config.yml b/examples/rl/workflows/config.yml similarity index 100% rename from examples/rl/templates/config.yml rename to examples/rl/workflows/config.yml diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py new file mode 100644 index 000000000..e307d4b10 --- /dev/null +++ b/examples/rl/workflows/general.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +import yaml +from os.path import dirname, join, realpath + +workflow_dir = dirname(realpath(__file__)) +rl_example_dir = dirname(workflow_dir) + +config_path = join(workflow_dir, "config.yml") +with open(config_path, "r") as config_file: + config = yaml.safe_load(config_file) + +log_dir = join(rl_example_dir, "logs", config["job_name"]) + +scenario_dir = join(rl_example_dir, config["scenario"]) +if scenario_dir not in sys.path: + sys.path.insert(0, scenario_dir) + +import get_env_wrapper, get_agent_wrapper, policy_func_index diff --git a/examples/rl/templates/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py similarity index 77% rename from examples/rl/templates/policy_manager/policy_manager.py rename to examples/rl/workflows/policy_manager/policy_manager.py index 3778d6029..2756a1683 100644 --- a/examples/rl/templates/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -6,22 +6,23 @@ from maro.rl.policy import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager -template_dir = dirname(dirname(realpath(__file__))) # template directory -if template_dir not in sys.path: - sys.path.insert(0, template_dir) -from scenario_index import config, train_policy_func_index, log_dir +workflow_dir = dirname(dirname(realpath(__file__))) # template directory +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) + +from general import config, log_dir, policy_func_index def get_policy_manager(): train_mode = config["policy_manager"]["train_mode"] num_trainers = config["policy_manager"]["num_trainers"] - policy_dict = {name: func() for name, func in train_policy_func_index.items()} + policy_dict = {name: func() for name, func in policy_func_index.items()} if train_mode == "single-process": return LocalPolicyManager(policy_dict, log_dir=log_dir) if train_mode == "multi-process": return MultiProcessPolicyManager( policy_dict, num_trainers, - train_policy_func_index, + policy_func_index, log_dir=log_dir ) if train_mode == "multi-node": diff --git a/examples/rl/templates/policy_manager/trainer.py b/examples/rl/workflows/policy_manager/trainer.py similarity index 65% rename from examples/rl/templates/policy_manager/trainer.py rename to examples/rl/workflows/policy_manager/trainer.py index 061edb2e9..26ccad5d2 100644 --- a/examples/rl/templates/policy_manager/trainer.py +++ b/examples/rl/workflows/policy_manager/trainer.py @@ -7,17 +7,18 @@ from maro.rl.policy import trainer_node -template_dir = dirname(dirname(realpath(__file__))) # template directory -if template_dir not in sys.path: - sys.path.insert(0, template_dir) -from scenario_index import config, train_policy_func_index, log_dir +workflow_dir = dirname(dirname(realpath(__file__))) # template directory +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) + +from general import config, log_dir, policy_func_index if __name__ == "__main__": trainer_node( config["policy_manager"]["train_group"], int(environ["TRAINERID"]), - train_policy_func_index, + policy_func_index, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/examples/rl/templates/simple_learner.py b/examples/rl/workflows/simple_learner.py similarity index 71% rename from examples/rl/templates/simple_learner.py rename to examples/rl/workflows/simple_learner.py index f38104f02..1b7458f86 100644 --- a/examples/rl/templates/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -6,11 +6,11 @@ from maro.rl.learning import SimpleLearner -template_dir = dirname((realpath(__file__))) -if template_dir not in sys.path: - sys.path.insert(0, template_dir) +workflow_dir = dirname((realpath(__file__))) +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) -from scenario_index import config, get_agent_wrapper, get_env_wrapper, log_dir +from general import config, get_agent_wrapper, get_env_wrapper, log_dir if __name__ == "__main__": diff --git a/examples/rl/templates/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py similarity index 91% rename from examples/rl/templates/synchronous/learner.py rename to examples/rl/workflows/synchronous/learner.py index 1dc5f6e69..edc9ae574 100644 --- a/examples/rl/templates/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -8,12 +8,12 @@ Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager ) -template_dir = dirname(dirname((realpath(__file__)))) -if template_dir not in sys.path: - sys.path.insert(0, template_dir) +workflow_dir = dirname(dirname((realpath(__file__)))) +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) from policy_manager.policy_manager import get_policy_manager -from scenario_index import config, get_agent_wrapper, get_env_wrapper, log_dir +from general import config, get_agent_wrapper, get_env_wrapper, log_dir def get_rollout_manager(): diff --git a/examples/rl/templates/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py similarity index 71% rename from examples/rl/templates/synchronous/rollout_worker.py rename to examples/rl/workflows/synchronous/rollout_worker.py index ae7e1441b..25daeee71 100644 --- a/examples/rl/templates/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -7,10 +7,11 @@ from maro.rl.learning.synchronous import rollout_worker_node -template_dir = dirname(dirname(realpath(__file__))) # template directory -if template_dir not in sys.path: - sys.path.insert(0, template_dir) -from scenario_index import config, get_agent_wrapper, get_env_wrapper, log_dir +workflow_dir = dirname(dirname(realpath(__file__))) # template directory +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) + +from general import config, get_agent_wrapper, get_env_wrapper, log_dir if __name__ == "__main__": diff --git a/maro/rl/learning/asynchronous/policy_server.py b/maro/rl/learning/asynchronous/policy_server.py index 9af0a92ca..17bab78fb 100644 --- a/maro/rl/learning/asynchronous/policy_server.py +++ b/maro/rl/learning/asynchronous/policy_server.py @@ -26,8 +26,8 @@ def policy_server( policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that hosts all policies and updates them using experiences collected by the actors. num_actors (int): Number of remote actors to collect simulation experiences. - max_lag (int): Maximum policy version lag allowed for experiences collected from remote roll-out workers. - Experiences collected using policy versions older than (current_version - max_lag) will be discarded. + max_lag (int): Maximum policy version lag allowed for experiences collected from remote actors. Experiences + collected using policy versions older than (current_version - max_lag) will be discarded. Defaults to 0, in which case only experiences collected using the latest policy version will be returned. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. From a2244b596aee4c4b6c44919d8112bd5911af336a Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 7 Jul 2021 09:45:48 +0000 Subject: [PATCH 362/482] refined dynamic imports using importlib --- .../rl/scripts/docker/docker_compose_yml.py | 18 +++++++++--------- .../rl/workflows/asynchronous/policy_server.py | 1 + examples/rl/workflows/general.py | 12 ++++++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 3fdaa25c1..fd0a50e56 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -6,10 +6,10 @@ from os.path import dirname, join, realpath path = realpath(__file__) -script_dir = dirname(path) -rl_example_dir = dirname(dirname(script_dir)) +docker_script_dir = dirname(path) +rl_example_dir = dirname(dirname(docker_script_dir)) root_dir = dirname(dirname(rl_example_dir)) -workflow_dir = join(rl_example_dir, "templates") +workflow_dir = join(rl_example_dir, "workflows") maro_rl_dir = join(root_dir, "maro", "rl") config_path = join(workflow_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") @@ -32,7 +32,7 @@ str_id = f"trainer.{trainer_id}" trainer_spec = deepcopy(common_spec) del trainer_spec["build"] - trainer_spec["command"] = "python3 /maro/rl_examples/templates/policy_manager/trainer.py" + trainer_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/trainer.py" trainer_spec["container_name"] = str_id trainer_spec["environment"] = [f"TRAINERID={trainer_id}"] docker_compose_manifest["services"][str_id] = trainer_spec @@ -44,7 +44,7 @@ **common_spec, **{ "container_name": "learner", - "command": "python3 /maro/rl_examples/templates/synchronous/learner.py" + "command": "python3 /maro/rl_examples/workflows/synchronous/learner.py" } } # rollout worker spec @@ -53,7 +53,7 @@ str_id = f"rollout_worker.{worker_id}" worker_spec = deepcopy(common_spec) del worker_spec["build"] - worker_spec["command"] = "python3 /maro/rl_examples/templates/synchronous/rollout_worker.py" + worker_spec["command"] = "python3 /maro/rl_examples/workflows/synchronous/rollout_worker.py" worker_spec["container_name"] = str_id worker_spec["environment"] = [f"WORKERID={worker_id}"] docker_compose_manifest["services"][str_id] = worker_spec @@ -63,7 +63,7 @@ **common_spec, **{ "container_name": "policy_server", - "command": "python3 /maro/rl_examples/templates/asynchronous/policy_server.py" + "command": "python3 /maro/rl_examples/workflows/asynchronous/policy_server.py" } } # actor spec @@ -71,12 +71,12 @@ str_id = f"actor.{actor_id}" actor_spec = deepcopy(common_spec) del actor_spec["build"] - actor_spec["command"] = "python3 /maro/rl_examples/templates/asynchronous/actor.py" + actor_spec["command"] = "python3 /maro/rl_examples/workflows/asynchronous/actor.py" actor_spec["container_name"] = str_id actor_spec["environment"] = [f"ACTORID={actor_id}"] docker_compose_manifest["services"][str_id] = actor_spec else: raise ValueError(f"mode must be 'sync' or 'async', got {mode}") -with open(join(script_dir, "docker-compose.yml"), "w") as fp: +with open(join(docker_script_dir, "docker-compose.yml"), "w") as fp: yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/rl/workflows/asynchronous/policy_server.py b/examples/rl/workflows/asynchronous/policy_server.py index 7e06fcb16..8cc6e43d8 100644 --- a/examples/rl/workflows/asynchronous/policy_server.py +++ b/examples/rl/workflows/asynchronous/policy_server.py @@ -9,6 +9,7 @@ workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) + from policy_manager.policy_manager import get_policy_manager from general import config, log_dir diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index e307d4b10..47fe9f7bd 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import importlib import sys import yaml from os.path import dirname, join, realpath @@ -8,14 +9,17 @@ workflow_dir = dirname(realpath(__file__)) rl_example_dir = dirname(workflow_dir) +if rl_example_dir not in sys.path: + sys.path.insert(0, rl_example_dir) + config_path = join(workflow_dir, "config.yml") with open(config_path, "r") as config_file: config = yaml.safe_load(config_file) log_dir = join(rl_example_dir, "logs", config["job_name"]) -scenario_dir = join(rl_example_dir, config["scenario"]) -if scenario_dir not in sys.path: - sys.path.insert(0, scenario_dir) +module = importlib.import_module(f"{config['scenario']}") -import get_env_wrapper, get_agent_wrapper, policy_func_index +get_env_wrapper = getattr(module, "get_env_wrapper") +get_agent_wrapper = getattr(module, "get_agent_wrapper") +policy_func_index = getattr(module, "policy_func_index") From 740efa701cc8a3d06a7294333b9ae048991abb0b Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 8 Jul 2021 09:34:42 +0000 Subject: [PATCH 363/482] 1. moved policy update triggers to policy manager; 2. added version control in policy manager --- examples/rl/cim/ac.py | 2 +- examples/rl/cim/dqn.py | 12 +- examples/rl/workflows/general.py | 2 +- maro/rl/learning/agent_wrapper.py | 8 +- maro/rl/learning/asynchronous/actor.py | 2 +- .../rl/learning/asynchronous/policy_server.py | 9 +- maro/rl/learning/simple_learner.py | 6 +- maro/rl/learning/synchronous/learner.py | 9 +- .../rl/learning/synchronous/rollout_worker.py | 4 +- maro/rl/{ => policy}/algorithms/__init__.py | 0 maro/rl/{ => policy}/algorithms/ac.py | 16 +-- maro/rl/{ => policy}/algorithms/ddpg.py | 16 +-- maro/rl/{ => policy}/algorithms/dqn.py | 16 +-- maro/rl/{ => policy}/algorithms/pg.py | 9 +- .../algorithms/rl_policy_index.py | 0 maro/rl/policy/policy.py | 40 +++---- maro/rl/policy/policy_manager.py | 104 +++++++++++++----- maro/rl/policy/trainer.py | 21 ++-- maro/rl/utils/message_enums.py | 2 +- 19 files changed, 136 insertions(+), 142 deletions(-) rename maro/rl/{ => policy}/algorithms/__init__.py (100%) rename maro/rl/{ => policy}/algorithms/ac.py (90%) rename maro/rl/{ => policy}/algorithms/ddpg.py (89%) rename maro/rl/{ => policy}/algorithms/dqn.py (90%) rename maro/rl/{ => policy}/algorithms/pg.py (87%) rename maro/rl/{ => policy}/algorithms/rl_policy_index.py (100%) diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 3d800fb88..14b478116 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -7,9 +7,9 @@ import numpy as np import torch -from maro.rl.algorithms import ActorCritic, ActorCriticConfig from maro.rl.experience import ExperienceManager from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption +from maro.rl.policy.algorithms import ActorCritic, ActorCriticConfig cim_path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, cim_path) diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index a8ce8d710..85e0a7bbb 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -7,9 +7,9 @@ import numpy as np import torch import torch.nn as nn -from maro.rl.algorithms import DQN, DQNConfig from maro.rl.experience import ExperienceManager from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption +from maro.rl.policy.algorithms import DQN, DQNConfig cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: @@ -73,18 +73,18 @@ def forward(self, states): return self.component(states) -def get_dqn_policy(training: bool = True): +def get_dqn_policy(learning: bool = True): qnet = QNet( FullyConnectedBlock(**config["model"]["network"]), - optim_option=OptimOption(**config["model"]["optimization"]) if training else None + optim_option=OptimOption(**config["model"]["optimization"]) if learning else None ) - if training: + if learning: exp_manager = ExperienceManager(**config["experience_manager"]["training"]) else: exp_manager = ExperienceManager(**config["experience_manager"]["rollout"]) return DQN( qnet, exp_manager, DQNConfig(**config["algorithm"]), # set these to a large number to ensure that the roll-out workers don't update policies - update_trigger=config["update_trigger"] if training else 1e8, - warmup=config["warmup"] if training else 1e8 + update_trigger=config["update_trigger"] if learning else float("inf"), + warmup=config["warmup"] if learning else float("inf") ) diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 47fe9f7bd..8d55a997a 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -16,7 +16,7 @@ with open(config_path, "r") as config_file: config = yaml.safe_load(config_file) -log_dir = join(rl_example_dir, "logs", config["job_name"]) +log_dir = join(rl_example_dir, "log", config["job_name"]) module = importlib.import_module(f"{config['scenario']}") diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index aa93eb427..8c24d7d5c 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -17,8 +17,6 @@ class AgentWrapper: exploration_dict (Dict[str, AbsExploration]): A dictionary of named ``AbsExploration`` instances. Defaults to None. agent2exploration (Dict[str, str]): Mapping from agent names to exploration instance names. Defaults to None. - log_dir (str): Directory to store logs in. A ``Logger`` will be created at init time and this directory - will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( self, @@ -51,12 +49,12 @@ def choose_action(self, state: dict) -> dict: return action_by_agent - def on_experiences(self, exp_by_agent: dict) -> set: + def store_experiences(self, exp_by_agent: dict) -> set: """Store agent experiences in the policies' experience managers.""" policies_with_new_exp = set() for agent_id, exp in exp_by_agent.items(): - if hasattr(self.policy[agent_id], "on_experiences"): - self.policy[agent_id].on_experiences(exp) + if hasattr(self.policy[agent_id], "update"): + self.policy[agent_id].update(exp) policies_with_new_exp.add(self.agent2policy[agent_id]) return policies_with_new_exp diff --git a/maro/rl/learning/asynchronous/actor.py b/maro/rl/learning/asynchronous/actor.py index c47b6df41..9f6526fa6 100644 --- a/maro/rl/learning/asynchronous/actor.py +++ b/maro/rl/learning/asynchronous/actor.py @@ -108,7 +108,7 @@ def actor( ) exp_by_agent = env_wrapper.get_experiences() - policies_with_new_exp = agent_wrapper.on_experiences(exp_by_agent) + policies_with_new_exp = agent_wrapper.store_experiences(exp_by_agent) num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) exp_by_policy = agent_wrapper.get_experiences_by_policy(policies_with_new_exp) reply = proxy.send( diff --git a/maro/rl/learning/asynchronous/policy_server.py b/maro/rl/learning/asynchronous/policy_server.py index 17bab78fb..51a3a47a8 100644 --- a/maro/rl/learning/asynchronous/policy_server.py +++ b/maro/rl/learning/asynchronous/policy_server.py @@ -45,7 +45,6 @@ def policy_server( msg, tag=MsgTag.POLICY_STATE, body={MsgKey.POLICY_STATE: policy_manager.get_state(), MsgKey.VERSION: policy_manager.version} ) - policy_manager.reset_update_status() elif msg.tag == MsgTag.COLLECT_DONE: if policy_manager.version - msg.body[MsgKey.VERSION] > max_lag: logger.info( @@ -54,12 +53,14 @@ def policy_server( f"{policy_manager.version - max_lag}, got {msg.body[MsgKey.VERSION]}" ) else: - policy_manager.on_experiences(msg.body[MsgKey.EXPERIENCES]) + policy_manager.update(msg.body[MsgKey.EXPERIENCES]) proxy.reply( msg, tag=MsgTag.POLICY_STATE, - body={MsgKey.POLICY_STATE: policy_manager.get_state(), MsgKey.VERSION: policy_manager.version} + body={ + MsgKey.POLICY_STATE: policy_manager.get_state(version=msg.body[MsgKey.VERSION]), + MsgKey.VERSION: policy_manager.version + } ) - policy_manager.reset_update_status() elif msg.tag == MsgTag.DONE: num_active_actors -= 1 if num_active_actors == 0: diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py index 0b8eeaf32..7348803cd 100644 --- a/maro/rl/learning/simple_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -86,7 +86,7 @@ def __init__( def run(self): """Entry point for executing a learning workflow.""" for ep in range(1, self.num_episodes + 1): - self._train(ep) + self._collect_and_update(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 self._evaluate() @@ -96,7 +96,7 @@ def run(self): if self.early_stopper.stop(): return - def _train(self, ep: int): + def _collect_and_update(self, ep: int): """Collect simulation data for training.""" t0 = time.time() num_experiences_collected = 0 @@ -108,7 +108,7 @@ def _train(self, ep: int): while self.env.state: segment += 1 exp_by_agent = self._collect(ep, segment) - self.agent.on_experiences(exp_by_agent) + self.agent.update(exp_by_agent) num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) # update the exploration parameters if an episode is finished self.agent.exploration_step() diff --git a/maro/rl/learning/synchronous/learner.py b/maro/rl/learning/synchronous/learner.py index f722061a1..87175f596 100644 --- a/maro/rl/learning/synchronous/learner.py +++ b/maro/rl/learning/synchronous/learner.py @@ -73,12 +73,12 @@ def __init__( self.early_stopper = early_stopper self._end_of_episode_kwargs = end_of_episode_kwargs - self._last_step_set = {} + self._last_policy_version = 0 def run(self): """Entry point for executing a learning workflow.""" for ep in range(1, self.num_episodes + 1): - self._train(ep) + self._collect_and_update(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) @@ -97,7 +97,7 @@ def run(self): if hasattr(self.policy_manager, "exit"): self.policy_manager.exit() - def _train(self, ep: int): + def _collect_and_update(self, ep: int): collect_time = policy_update_time = num_experiences_collected = 0 segment = 0 self.rollout_manager.reset() @@ -105,13 +105,12 @@ def _train(self, ep: int): segment += 1 # experience collection policy_state_dict = self.policy_manager.get_state() - self.policy_manager.reset_update_status() policy_version = self.policy_manager.version tc0 = time.time() exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) collect_time += time.time() - tc0 tu0 = time.time() - self.policy_manager.on_experiences(exp_by_policy) + self.policy_manager.update(exp_by_policy) policy_update_time += time.time() - tu0 num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) diff --git a/maro/rl/learning/synchronous/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py index a21bed2ce..a42e1be65 100644 --- a/maro/rl/learning/synchronous/rollout_worker.py +++ b/maro/rl/learning/synchronous/rollout_worker.py @@ -68,7 +68,7 @@ def collect(msg): f"steps {starting_step_index} - {env_wrapper.step_index})" ) - policies_with_new_exp = agent_wrapper.on_experiences(env_wrapper.get_experiences()) + policies_with_new_exp = agent_wrapper.store_experiences(env_wrapper.get_experiences()) ret_exp = agent_wrapper.get_experiences_by_policy(policies_with_new_exp) return_info = { @@ -163,7 +163,7 @@ def collect(msg): f"steps {starting_step_index} - {env_wrapper.step_index})" ) - policy_names = agent_wrapper.on_experiences(env_wrapper.get_experiences()) + policy_names = agent_wrapper.store_experiences(env_wrapper.get_experiences()) ret_exp = agent_wrapper.get_experiences_by_policy(policy_names) return_info = { diff --git a/maro/rl/algorithms/__init__.py b/maro/rl/policy/algorithms/__init__.py similarity index 100% rename from maro/rl/algorithms/__init__.py rename to maro/rl/policy/algorithms/__init__.py diff --git a/maro/rl/algorithms/ac.py b/maro/rl/policy/algorithms/ac.py similarity index 90% rename from maro/rl/algorithms/ac.py rename to maro/rl/policy/algorithms/ac.py index f3bd4df40..6609369fc 100644 --- a/maro/rl/algorithms/ac.py +++ b/maro/rl/policy/algorithms/ac.py @@ -61,23 +61,13 @@ class ActorCritic(AbsCorePolicy): experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config: Configuration for the AC algorithm. - update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. - warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. - Defaults to 1. """ - def __init__( - self, - ac_net: DiscreteACNet, - experience_manager: ExperienceManager, - config: ActorCriticConfig, - update_trigger: int = 1, - warmup: int = 1 - ): + def __init__(self, ac_net: DiscreteACNet, experience_manager: ExperienceManager, config: ActorCriticConfig): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") - super().__init__(experience_manager, update_trigger=update_trigger, warmup=warmup) + super().__init__(experience_manager) self.ac_net = ac_net self.config = config self.device = self.ac_net.device @@ -89,7 +79,7 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else (actions, log_p) - def update(self): + def learn(self): self.ac_net.train() for _ in range(self.config.train_epochs): experience_set = self.experience_manager.get() diff --git a/maro/rl/algorithms/ddpg.py b/maro/rl/policy/algorithms/ddpg.py similarity index 89% rename from maro/rl/algorithms/ddpg.py rename to maro/rl/policy/algorithms/ddpg.py index cb7844689..704fc8a7e 100644 --- a/maro/rl/algorithms/ddpg.py +++ b/maro/rl/policy/algorithms/ddpg.py @@ -64,22 +64,12 @@ class DDPG(AbsCorePolicy): experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config (DDPGConfig): Configuration for DDPG algorithm. - update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. - warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. - Defaults to 1. """ - def __init__( - self, - ac_net: ContinuousACNet, - experience_manager: ExperienceManager, - config: DDPGConfig, - update_trigger: int = 1, - warmup: int = 1, - ): + def __init__(self, ac_net: ContinuousACNet, experience_manager: ExperienceManager, config: DDPGConfig): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") - super().__init__(experience_manager, update_trigger=update_trigger, warmup=warmup) + super().__init__(experience_manager) self.ac_net = ac_net if self.ac_net.trainable: self.target_ac_net = ac_net.copy() @@ -96,7 +86,7 @@ def choose_action(self, states) -> Union[float, np.ndarray]: return actions[0] if len(actions) == 1 else actions - def update(self): + def learn(self): self.ac_net.train() for _ in range(self.config.train_epochs): experience_set = self.experience_manager.get() diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/policy/algorithms/dqn.py similarity index 90% rename from maro/rl/algorithms/dqn.py rename to maro/rl/policy/algorithms/dqn.py index aafffc717..0cce2d099 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/policy/algorithms/dqn.py @@ -55,22 +55,12 @@ class DQN(AbsCorePolicy): experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config (DQNConfig): Configuration for DQN algorithm. - update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. - warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. - Defaults to 1. """ - def __init__( - self, - q_net: DiscreteQNet, - experience_manager: ExperienceManager, - config: DQNConfig, - update_trigger: int = 1, - warmup: int = 1, - ): + def __init__(self, q_net: DiscreteQNet, experience_manager: ExperienceManager, config: DQNConfig): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(experience_manager, update_trigger=update_trigger, warmup=warmup) + super().__init__(experience_manager) self.q_net = q_net if self.q_net.trainable: self.target_q_net = q_net.copy() @@ -92,7 +82,7 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = actions.cpu().numpy() return actions[0] if len(actions) == 1 else actions - def update(self): + def learn(self): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() for _ in range(self.config.train_epochs): diff --git a/maro/rl/algorithms/pg.py b/maro/rl/policy/algorithms/pg.py similarity index 87% rename from maro/rl/algorithms/pg.py rename to maro/rl/policy/algorithms/pg.py index 6792d784f..bba4729df 100644 --- a/maro/rl/algorithms/pg.py +++ b/maro/rl/policy/algorithms/pg.py @@ -35,21 +35,16 @@ class PolicyGradient(AbsCorePolicy): experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config (PolicyGradientConfig): Configuration for the PG algorithm. - update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. - warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. - Defaults to 1. """ def __init__( self, policy_net: DiscretePolicyNet, experience_manager: ExperienceManager, config: PolicyGradientConfig, - update_trigger: int = 1, - warmup: int = 1 ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__(experience_manager, update_trigger=update_trigger, warmup=warmup) + super().__init__(experience_manager) self.policy_net = policy_net self.config = config self.device = self.policy_net.device @@ -61,7 +56,7 @@ def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def update(self): + def learn(self): """ This should be called at the end of a simulation episode and the experiences obtained from the experience manager's ``get`` method should be a sequential set, i.e., in the order in diff --git a/maro/rl/algorithms/rl_policy_index.py b/maro/rl/policy/algorithms/rl_policy_index.py similarity index 100% rename from maro/rl/algorithms/rl_policy_index.py rename to maro/rl/policy/algorithms/rl_policy_index.py diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 377ddc2dd..5ab76167c 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -34,28 +34,17 @@ class AbsCorePolicy(AbsPolicy): Args: experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. - update_trigger (int): Minimum number of new experiences required to trigger an ``update`` call. Defaults to 1. - warmup (int): Minimum number of experiences in the experience memory required to trigger an ``update`` call. - Defaults to 1. """ - def __init__( - self, - experience_manager: ExperienceManager, - update_trigger: int = 1, - warmup: int = 1 - ): + def __init__(self, experience_manager: ExperienceManager): super().__init__() self.experience_manager = experience_manager - self.update_trigger = update_trigger - self.warmup = warmup - self._new_exp_counter = 0 @abstractmethod def choose_action(self, state): raise NotImplementedError @abstractmethod - def update(self): + def learn(self): """Policy update logic is implemented here. This usually includes retrieving experiences as training samples from the experience manager and @@ -83,23 +72,22 @@ def set_state(self, policy_state): """ pass - def on_experiences(self, exp: ExperienceSet) -> bool: + def freeze(self): + """Freeze the policy instance so that incoming experiences will not trigger calls to ``learn``.""" + self.learning = False + + def unfreeze(self): + """Unfreeze the policy to allow incoming experiences to trigger calls to ``learn``.""" + self.learning = True + + def store_experiences(self, exp: ExperienceSet) -> bool: """ Store incoming experiences and update if necessary. """ self.experience_manager.put(exp) - self._new_exp_counter += exp.size - print( - f"exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" - ) - if self.experience_manager.size >= self.warmup and self._new_exp_counter >= self.update_trigger: - t0 = time.time() - self.update() - print(f"policy update time: {time.time() - t0}") - self._new_exp_counter = 0 - return True - - return False + # print( + # f"exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" + # ) def load(self, path: str): """Load the policy state from disk.""" diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 691f0606a..acc523cbf 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -24,31 +24,45 @@ class AbsPolicyManager(ABC): Args: policy_dict (Dict[str, AbsCorePolicy]): A list of policies managed by the manager. + update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the + required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, + all triggers will be set to 1. + warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the + minimum number of experiences in the experience memory required to trigger a call to ``learn`` for + each policy. Defaults to None, in which case all warm-up sizes will be set to 1. """ - def __init__(self, policy_dict: Dict[str, AbsCorePolicy]): + def __init__( + self, + policy_dict: Dict[str, AbsCorePolicy], + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None + ): for policy in policy_dict.values(): if not isinstance(policy, AbsCorePolicy): raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") super().__init__() self.policy_dict = policy_dict - self.updated = set(self.policy_dict.keys()) - self._version = 0 + self.update_trigger = update_trigger + self.warmup = warmup + self._update_history = [set(policy_dict.keys())] @property def version(self): - return self._version + return len(self._update_history) - 1 @abstractmethod - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + def update(self, exp_by_policy: Dict[str, ExperienceSet]): """Logic for handling incoming experiences is implemented here.""" raise NotImplementedError - def get_state(self): - return {name: self.policy_dict[name].get_state() for name in self.updated} - - def reset_update_status(self): - self.updated.clear() + def get_state(self, cur_version: int = None): + if cur_version is None: + cur_version = self.version - 1 + updated = set() + for version in range(cur_version + 1, len(self._update_history)): + updated |= self._update_history[version] + return {name: self.policy_dict[name].get_state() for name in updated} class LocalPolicyManager(AbsPolicyManager): @@ -65,23 +79,29 @@ def __init__(self, policy_dict: Dict[str, AbsCorePolicy], log_dir: str = getcwd( self._logger = Logger("LOCAL_TRAINING_MANAGER", dump_folder=log_dir) self._new_exp_counter = defaultdict(int) - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + def update(self, exp_by_policy: Dict[str, ExperienceSet]): """Store experiences and update policies if possible. The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding policy's experience manager. Policies whose update conditions have been met will then be updated. """ t0 = time.time() + updated = set() for policy_name, exp in exp_by_policy.items(): + policy = self.policy_dict[policy_name] + policy.experience_manager.put(exp) + self._new_exp_counter[policy_name] += exp.size if ( - isinstance(self.policy_dict[policy_name], AbsCorePolicy) and - self.policy_dict[policy_name].on_experiences(exp) + self._new_exp_counter[policy_name] >= self.update_trigger[policy_name] and + policy.experience_manager.size >= self.warmup[policy_name] ): - self.updated.add(policy_name) + policy.learn() + updated.add(policy_name) + self._new_exp_counter[policy_name] = 0 - if self.updated: - self._version += 1 - self._logger.info(f"Updated policies {self.updated}") + if updated: + self._update_history.append(updated) + self._logger.info(f"Updated policies {updated}") self._logger.debug(f"policy update time: {time.time() - t0}") @@ -110,6 +130,9 @@ def __init__( self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._policy2trainer = {} self._trainer2policies = defaultdict(list) + self._exp_cache = defaultdict(ExperienceSet) + self._num_experiences_by_policy = defaultdict(int) + for i, name in enumerate(self.policy_dict): trainer_id = i % num_trainers self._policy2trainer[name] = f"TRAINER.{trainer_id}" @@ -133,22 +156,32 @@ def __init__( self._trainer_processes.append(trainer) trainer.start() - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): + def update(self, exp_by_policy: Dict[str, ExperienceSet]): + exp_to_send, updated = {}, set() + for policy_name, exp in exp_by_policy.items(): + self._num_experiences_by_policy[policy_name] += exp.size + self._exp_cache[policy_name].extend(exp) + if ( + self._exp_cache[policy_name].size >= self.update_trigger[policy_name] and + self._num_experiences_by_policy[policy_name] >= self.warmup[policy_name] + ): + exp_to_send[policy_name] = self._exp_cache.pop(policy_name) + updated.add(policy_name) + for trainer_id, conn in self._manager_end.items(): conn.send({ "type": "train", - "experiences": {name: exp_by_policy[name] for name in self._trainer2policies[trainer_id]} + "experiences": {name: exp_to_send[name] for name in self._trainer2policies[trainer_id]} }) for conn in self._manager_end.values(): result = conn.recv() for policy_name, policy_state in result["policy"].items(): self.policy_dict[policy_name].set_state(policy_state) - self.updated.add(policy_name) - if self.updated: - self._version += 1 - self._logger.info(f"Updated policies {self.updated}") + if updated: + self._update_history.append(updated) + self._logger.info(f"Updated policies {updated}") def exit(self): """Tell the trainer processes to exit.""" @@ -186,6 +219,9 @@ def __init__( self._policy2trainer = {} self._trainer2policies = defaultdict(list) + self._exp_cache = defaultdict(ExperienceSet) + self._num_experiences_by_policy = defaultdict(int) + for i, name in enumerate(self.policy_dict): trainer_id = i % num_trainers self._policy2trainer[name] = f"TRAINER.{trainer_id}" @@ -198,9 +234,20 @@ def __init__( ) ) - def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): - msg_body_by_dest = defaultdict(dict) + def update(self, exp_by_policy: Dict[str, ExperienceSet]): + exp_to_send, updated = {}, set() for policy_name, exp in exp_by_policy.items(): + self._num_experiences_by_policy[policy_name] += exp.size + self._exp_cache[policy_name].extend(exp) + if ( + self._exp_cache[policy_name].size >= self.update_trigger[policy_name] and + self._num_experiences_by_policy[policy_name] >= self.warmup[policy_name] + ): + exp_to_send[policy_name] = self._exp_cache.pop(policy_name) + updated.add(policy_name) + + msg_body_by_dest = defaultdict(dict) + for policy_name, exp in exp_to_send.items(): trainer_id = self._policy2trainer[policy_name] if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} @@ -209,11 +256,10 @@ def on_experiences(self, exp_by_policy: Dict[str, ExperienceSet]): for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): self.policy_dict[policy_name].set_state(policy_state) - self.updated.add(policy_name) - if self.updated: - self._version += 1 - self._logger.info(f"Updated policies {self.updated}") + if updated: + self._update_history.append(updated) + self._logger.info(f"Updated policies {updated}") def exit(self): """Tell the remote trainers to exit.""" diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index 5eafbbc45..27216a141 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -38,15 +38,11 @@ def trainer_process( msg = conn.recv() if msg["type"] == "train": t0 = time.time() - updated = { - name: policy_dict[name].get_state() for name, exp in msg["experiences"].items() - if policy_dict[name].on_experiences(exp) - } + for name, exp in msg["experiences"].items(): + policy_dict[name].store_experiences(exp) + policy_dict[name].learn() logger.debug(f"total policy update time: {time.time() - t0}") - conn.send({"policy": updated}) - elif msg["type"] == "get_policy_state": - policy_state_dict = {name: policy.get_state() for name, policy in policy_dict.items()} - conn.send({"policy": policy_state_dict}) + conn.send({"policy": {name: policy_dict[name].get_state() for name in msg["experiences"]}}) elif msg["type"] == "quit": break @@ -89,11 +85,12 @@ def trainer_node( proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) elif msg.tag == MsgTag.TRAIN: t0 = time.time() + for name, exp in msg.body[MsgKey.EXPERIENCES].items(): + policy_dict[name].store_experiences(exp) + policy_dict[name].learn() + msg_body = { - MsgKey.POLICY_STATE: { - name: policy_dict[name].get_state() for name, exp in msg.body[MsgKey.EXPERIENCES].items() - if policy_dict[name].on_experiences(exp) - } + MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.EXPERIENCES]} } logger.debug(f"total policy update time: {time.time() - t0}") proxy.reply(msg, body=msg_body) diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 7024f254a..6e6c3b076 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -13,7 +13,7 @@ class MsgTag(Enum): POLICY_STATE = "policy_state" CHOOSE_ACTION = "choose_action" ACTION = "action" - TRAIN = "train" + LEARN = "LEARN" ABORT_ROLLOUT = "abort_rollout" EVAL_DONE = "eval_done" COLLECT_DONE = "collect_done" From c278693f2f8c76ba4e1fed3a446e6143c79729b9 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 8 Jul 2021 10:59:03 +0000 Subject: [PATCH 364/482] fixed a few bugs and updated cim RL example --- examples/rl/cim/__init__.py | 4 +- examples/rl/cim/ac.py | 4 +- examples/rl/cim/dqn.py | 15 ++---- examples/rl/cim/policy_index.py | 2 + examples/rl/workflows/general.py | 2 + .../policy_manager/policy_manager.py | 13 ++++- maro/rl/learning/agent_wrapper.py | 22 ++++---- maro/rl/learning/asynchronous/actor.py | 10 ++-- .../rl/learning/synchronous/rollout_worker.py | 10 +--- maro/rl/policy/policy.py | 10 +--- maro/rl/policy/policy_manager.py | 51 ++++++++++++++++--- maro/rl/policy/trainer.py | 4 +- 12 files changed, 84 insertions(+), 63 deletions(-) diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index 5d2e7c317..cddfbf182 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -3,6 +3,6 @@ from .agent_wrapper import get_agent_wrapper from .env_wrapper import get_env_wrapper -from .policy_index import policy_func_index +from .policy_index import policy_func_index, update_trigger, warmup -__all__ = ["get_agent_wrapper", "get_env_wrapper", "policy_func_index"] +__all__ = ["get_agent_wrapper", "get_env_wrapper", "policy_func_index", "update_trigger", "warmup"] diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 14b478116..1b27ca6e4 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -70,9 +70,7 @@ "beta": 0.4, "beta_step": 0.001 } - }, - "update_trigger": 1, - "warmup": 1 + } } diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index 85e0a7bbb..21f17264e 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -48,7 +48,7 @@ "batch_size": -1, "replace": False }, - "training": { # for experience managers in the learner process + "learning": { # for experience managers in the learner process "capacity": 100000, "overwrite_type": "rolling", "batch_size": 128, @@ -56,9 +56,7 @@ "beta": 0.4, "beta_step": 0.001 } - }, - "update_trigger": 16, - "warmup": 1 + } } @@ -79,12 +77,7 @@ def get_dqn_policy(learning: bool = True): optim_option=OptimOption(**config["model"]["optimization"]) if learning else None ) if learning: - exp_manager = ExperienceManager(**config["experience_manager"]["training"]) + exp_manager = ExperienceManager(**config["experience_manager"]["learning"]) else: exp_manager = ExperienceManager(**config["experience_manager"]["rollout"]) - return DQN( - qnet, exp_manager, DQNConfig(**config["algorithm"]), - # set these to a large number to ensure that the roll-out workers don't update policies - update_trigger=config["update_trigger"] if learning else float("inf"), - warmup=config["warmup"] if learning else float("inf") - ) + return DQN(qnet, exp_manager, DQNConfig(**config["algorithm"])) diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index 585d7a3ab..f85c494a7 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -11,4 +11,6 @@ from env_wrapper import AGENT_IDS # use agent IDs as policy names since each agent uses a separate policy +update_trigger = {name: 16 for name in AGENT_IDS} +warmup = {name: 1 for name in AGENT_IDS} policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 8d55a997a..57c498a86 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -23,3 +23,5 @@ get_env_wrapper = getattr(module, "get_env_wrapper") get_agent_wrapper = getattr(module, "get_agent_wrapper") policy_func_index = getattr(module, "policy_func_index") +update_trigger = getattr(module, "update_trigger") +warmup = getattr(module, "warmup") diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py index 2756a1683..e6f66deb7 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -10,19 +10,26 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import config, log_dir, policy_func_index +from general import config, log_dir, policy_func_index, update_trigger, warmup def get_policy_manager(): train_mode = config["policy_manager"]["train_mode"] num_trainers = config["policy_manager"]["num_trainers"] policy_dict = {name: func() for name, func in policy_func_index.items()} if train_mode == "single-process": - return LocalPolicyManager(policy_dict, log_dir=log_dir) + return LocalPolicyManager( + policy_dict, + update_trigger=update_trigger, + warmup=warmup, + log_dir=log_dir + ) if train_mode == "multi-process": return MultiProcessPolicyManager( policy_dict, num_trainers, policy_func_index, + update_trigger=update_trigger, + warmup=warmup, log_dir=log_dir ) if train_mode == "multi-node": @@ -30,6 +37,8 @@ def get_policy_manager(): policy_dict, config["policy_manager"]["train_group"], num_trainers, + update_trigger=update_trigger, + warmup=warmup, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index 8c24d7d5c..b9c94e5bb 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -1,11 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Dict, List +from typing import Dict from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsPolicy +from .env_wrapper import AbsEnvWrapper + class AgentWrapper: """Multi-agent wrapper that interacts with an ``EnvWrapper`` with a unified inferface. @@ -49,19 +51,15 @@ def choose_action(self, state: dict) -> dict: return action_by_agent - def store_experiences(self, exp_by_agent: dict) -> set: - """Store agent experiences in the policies' experience managers.""" - policies_with_new_exp = set() - for agent_id, exp in exp_by_agent.items(): + def get_batch(self, env: AbsEnvWrapper): + """Get experiences by policy names.""" + names = set() + for agent_id, exp in env.get_experiences().items(): if hasattr(self.policy[agent_id], "update"): - self.policy[agent_id].update(exp) - policies_with_new_exp.add(self.agent2policy[agent_id]) + self.policy[agent_id].store(exp) + names.add(self.agent2policy[agent_id]) - return policies_with_new_exp - - def get_experiences_by_policy(self, policy_names: List[str]): - """Get experiences by policy names.""" - return {name: self.policy_dict[name].experience_manager.get() for name in policy_names} + return {name: self.policy_dict[name].experience_manager.get() for name in names} def set_policy_states(self, policy_state_dict: dict): """Update policy states.""" diff --git a/maro/rl/learning/asynchronous/actor.py b/maro/rl/learning/asynchronous/actor.py index 9f6526fa6..b3b4f2b07 100644 --- a/maro/rl/learning/asynchronous/actor.py +++ b/maro/rl/learning/asynchronous/actor.py @@ -86,7 +86,7 @@ def actor( # main loop for ep in range(1, num_episodes + 1): t0 = time.time() - num_experiences_collected = 0 + num_training_experiences = 0 agent_wrapper.explore() env_wrapper.reset() env_wrapper.start() # get initial state @@ -107,10 +107,8 @@ def actor( f"steps {start_step_index} - {env_wrapper.step_index})" ) - exp_by_agent = env_wrapper.get_experiences() - policies_with_new_exp = agent_wrapper.store_experiences(exp_by_agent) - num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) - exp_by_policy = agent_wrapper.get_experiences_by_policy(policies_with_new_exp) + exp_by_policy = agent_wrapper.get_batch(env_wrapper) + num_training_experiences += sum(exp.size for exp in exp_by_policy.values()) reply = proxy.send( SessionMessage( MsgTag.COLLECT_DONE, proxy.name, policy_server_address, @@ -131,7 +129,7 @@ def actor( f"ep {ep} summary - " f"running time: {time.time() - t0} " f"env steps: {env_wrapper.step_index} " - f"experiences collected: {num_experiences_collected}" + f"experiences collected: {num_training_experiences}" ) if ep == eval_schedule[eval_point_index]: # evaluation diff --git a/maro/rl/learning/synchronous/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py index a42e1be65..453fcbfb4 100644 --- a/maro/rl/learning/synchronous/rollout_worker.py +++ b/maro/rl/learning/synchronous/rollout_worker.py @@ -68,13 +68,10 @@ def collect(msg): f"steps {starting_step_index} - {env_wrapper.step_index})" ) - policies_with_new_exp = agent_wrapper.store_experiences(env_wrapper.get_experiences()) - ret_exp = agent_wrapper.get_experiences_by_policy(policies_with_new_exp) - return_info = { "worker_index": index, "episode_end": not env_wrapper.state, - "experiences": ret_exp, + "experiences": agent_wrapper.get_batch(env_wrapper), "env_summary": env_wrapper.summary, "num_steps": env_wrapper.step_index - starting_step_index + 1 } @@ -163,14 +160,11 @@ def collect(msg): f"steps {starting_step_index} - {env_wrapper.step_index})" ) - policy_names = agent_wrapper.store_experiences(env_wrapper.get_experiences()) - ret_exp = agent_wrapper.get_experiences_by_policy(policy_names) - return_info = { MsgKey.EPISODE: ep, MsgKey.SEGMENT: segment, MsgKey.VERSION: msg.body[MsgKey.VERSION], - MsgKey.EXPERIENCES: ret_exp, + MsgKey.EXPERIENCES: agent_wrapper.get_batch(env_wrapper), MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1 } diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 5ab76167c..715f26505 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -72,15 +72,7 @@ def set_state(self, policy_state): """ pass - def freeze(self): - """Freeze the policy instance so that incoming experiences will not trigger calls to ``learn``.""" - self.learning = False - - def unfreeze(self): - """Unfreeze the policy to allow incoming experiences to trigger calls to ``learn``.""" - self.learning = True - - def store_experiences(self, exp: ExperienceSet) -> bool: + def store(self, exp: ExperienceSet) -> bool: """ Store incoming experiences and update if necessary. """ diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index acc523cbf..4ff705d9c 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -29,7 +29,7 @@ class AbsPolicyManager(ABC): all triggers will be set to 1. warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for - each policy. Defaults to None, in which case all warm-up sizes will be set to 1. + each policy. Defaults to None, in which case all warm-up sizes will be set to 1. """ def __init__( self, @@ -43,8 +43,15 @@ def __init__( super().__init__() self.policy_dict = policy_dict - self.update_trigger = update_trigger - self.warmup = warmup + if not update_trigger: + self.update_trigger = {name: 1 for name in self.policy_dict} + else: + self.update_trigger = update_trigger + if not warmup: + self.warmup = {name: 1 for name in self.policy_dict} + else: + self.warmup = warmup + self._update_history = [set(policy_dict.keys())] @property @@ -70,12 +77,24 @@ class LocalPolicyManager(AbsPolicyManager): Args: policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. + update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the + required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, + all triggers will be set to 1. + warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the + minimum number of experiences in the experience memory required to trigger a call to ``learn`` for + each policy. Defaults to None, in which case all warm-up sizes will be set to 1. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ - def __init__(self, policy_dict: Dict[str, AbsCorePolicy], log_dir: str = getcwd()): - super().__init__(policy_dict) + def __init__( + self, + policy_dict: Dict[str, AbsCorePolicy], + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None, + log_dir: str = getcwd() + ): + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup) self._logger = Logger("LOCAL_TRAINING_MANAGER", dump_folder=log_dir) self._new_exp_counter = defaultdict(int) @@ -111,10 +130,16 @@ class MultiProcessPolicyManager(AbsPolicyManager): Args: policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. - policy2trainer (dict): Mapping from policy names to trainer IDs. + num_trainers (int): Number of trainer processes to be forked. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. + update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the + required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, + all triggers will be set to 1. + warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the + minimum number of experiences in the experience memory required to trigger a call to ``learn`` for + each policy. Defaults to None, in which case all warm-up sizes will be set to 1. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -124,9 +149,11 @@ def __init__( policy_dict: Dict[str, AbsCorePolicy], num_trainers: int, create_policy_func_dict: Dict[str, Callable], + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None, log_dir: str = getcwd(), ): - super().__init__(policy_dict) + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._policy2trainer = {} self._trainer2policies = defaultdict(list) @@ -198,6 +225,12 @@ class MultiNodePolicyManager(AbsPolicyManager): manages them. num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where 0 <= i < num_trainers. + update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the + required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, + all triggers will be set to 1. + warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the + minimum number of experiences in the experience memory required to trigger a call to ``learn`` for + each policy. Defaults to None, in which case all warm-up sizes will be set to 1. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at @@ -209,10 +242,12 @@ def __init__( policy_dict: Dict[str, AbsCorePolicy], group: str, num_trainers: int, + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): - super().__init__(policy_dict) + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup) peers = {"trainer": num_trainers} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) self._logger = Logger(self._proxy.name, dump_folder=log_dir) diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index 27216a141..980ad3018 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -39,7 +39,7 @@ def trainer_process( if msg["type"] == "train": t0 = time.time() for name, exp in msg["experiences"].items(): - policy_dict[name].store_experiences(exp) + policy_dict[name].store(exp) policy_dict[name].learn() logger.debug(f"total policy update time: {time.time() - t0}") conn.send({"policy": {name: policy_dict[name].get_state() for name in msg["experiences"]}}) @@ -86,7 +86,7 @@ def trainer_node( elif msg.tag == MsgTag.TRAIN: t0 = time.time() for name, exp in msg.body[MsgKey.EXPERIENCES].items(): - policy_dict[name].store_experiences(exp) + policy_dict[name].store(exp) policy_dict[name].learn() msg_body = { From 455751af3d9f6927b7c9a91fc0921891b3c3c1e7 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 8 Jul 2021 11:23:39 +0000 Subject: [PATCH 365/482] fixed a few more bugs --- examples/rl/cim/agent_wrapper.py | 2 +- examples/rl/cim/policy_index.py | 2 +- maro/rl/learning/agent_wrapper.py | 2 +- maro/rl/policy/policy_manager.py | 2 +- maro/rl/policy/trainer.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/rl/cim/agent_wrapper.py b/examples/rl/cim/agent_wrapper.py index 1f135756f..27e23788a 100644 --- a/examples/rl/cim/agent_wrapper.py +++ b/examples/rl/cim/agent_wrapper.py @@ -28,7 +28,7 @@ def get_agent_wrapper(): **exploration_config ) return AgentWrapper( - {name: func(training=False) for name, func in policy_func_index.items()}, + {name: func(learning=False) for name, func in policy_func_index.items()}, {name: name for name in AGENT_IDS}, exploration_dict={f"EpsilonGreedy": epsilon_greedy}, agent2exploration={name: "EpsilonGreedy" for name in AGENT_IDS} diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index f85c494a7..b824905dd 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -11,6 +11,6 @@ from env_wrapper import AGENT_IDS # use agent IDs as policy names since each agent uses a separate policy -update_trigger = {name: 16 for name in AGENT_IDS} +update_trigger = {name: 128 for name in AGENT_IDS} warmup = {name: 1 for name in AGENT_IDS} policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index b9c94e5bb..e49fd9c4f 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -55,7 +55,7 @@ def get_batch(self, env: AbsEnvWrapper): """Get experiences by policy names.""" names = set() for agent_id, exp in env.get_experiences().items(): - if hasattr(self.policy[agent_id], "update"): + if hasattr(self.policy[agent_id], "store"): self.policy[agent_id].store(exp) names.add(self.agent2policy[agent_id]) diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 4ff705d9c..26705334b 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -288,7 +288,7 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp - for reply in self._proxy.scatter(MsgTag.TRAIN, SessionType.TASK, list(msg_body_by_dest.items())): + for reply in self._proxy.scatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())): for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): self.policy_dict[policy_name].set_state(policy_state) diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index 980ad3018..70eb19dc5 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -83,7 +83,7 @@ def trainer_node( policy_dict[name].set_state(state) logger.info(f"{proxy.name} initialized policy {name}") proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) - elif msg.tag == MsgTag.TRAIN: + elif msg.tag == MsgTag.LEARN: t0 = time.time() for name, exp in msg.body[MsgKey.EXPERIENCES].items(): policy_dict[name].store(exp) From 746f0f9dc4ccc56b0b727548ae27cecef0183cc3 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 9 Jul 2021 09:47:27 +0000 Subject: [PATCH 366/482] added agent wrapper instantiation to workflows --- examples/rl/cim/__init__.py | 10 ++++-- examples/rl/cim/agent_wrapper.py | 35 ------------------- examples/rl/cim/env_wrapper.py | 5 +-- examples/rl/cim/policy_index.py | 29 +++++++++++++-- examples/rl/workflows/asynchronous/actor.py | 12 +++++-- examples/rl/workflows/general.py | 4 ++- examples/rl/workflows/simple_learner.py | 14 ++++++-- .../workflows/synchronous/rollout_worker.py | 13 +++++-- maro/rl/learning/env_wrapper.py | 2 -- 9 files changed, 72 insertions(+), 52 deletions(-) delete mode 100644 examples/rl/cim/agent_wrapper.py diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index cddfbf182..b3d9785e3 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -1,8 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .agent_wrapper import get_agent_wrapper from .env_wrapper import get_env_wrapper -from .policy_index import policy_func_index, update_trigger, warmup +from .policy_index import ( + agent2exploration, agent2policy, exploration_func_index, policy_func_index, update_trigger, warmup +) -__all__ = ["get_agent_wrapper", "get_env_wrapper", "policy_func_index", "update_trigger", "warmup"] +__all__ = [ + "agent2exploration", "agent2policy", "exploration_func_index", "get_env_wrapper", "policy_func_index", + "update_trigger", "warmup" +] diff --git a/examples/rl/cim/agent_wrapper.py b/examples/rl/cim/agent_wrapper.py deleted file mode 100644 index 27e23788a..000000000 --- a/examples/rl/cim/agent_wrapper.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler -from maro.rl.learning import AgentWrapper - -cim_path = os.path.dirname(os.path.realpath(__file__)) -sys.path.insert(0, cim_path) -from env_wrapper import AGENT_IDS, env_config -from policy_index import policy_func_index - - -exploration_config = { - "last_ep": 10, - "initial_value": 0.4, - "final_value": 0.0, - "splits": [(5, 0.32)] -} - -def get_agent_wrapper(): - epsilon_greedy = EpsilonGreedyExploration(num_actions=env_config["wrapper"]["num_actions"]) - epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - **exploration_config - ) - return AgentWrapper( - {name: func(learning=False) for name, func in policy_func_index.items()}, - {name: name for name in AGENT_IDS}, - exploration_dict={f"EpsilonGreedy": epsilon_greedy}, - agent2exploration={name: "EpsilonGreedy" for name in AGENT_IDS} - ) diff --git a/examples/rl/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py index 28ab6bb1a..68e3cebb5 100644 --- a/examples/rl/cim/env_wrapper.py +++ b/examples/rl/cim/env_wrapper.py @@ -30,6 +30,7 @@ def __init__( (self.look_back + 1) * (self.max_ports_downstream + 1) * len(self.port_attributes) + len(self.vessel_attributes) ) + self._state_info = None @property def state_dim(self): @@ -44,7 +45,7 @@ def get_state(self, tick=None): future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] - self.state_info = { + self._state_info = { port_idx: { "tick": tick, "action_scope": self.event.action_scope, @@ -59,7 +60,7 @@ def get_state(self, tick=None): def to_env_action(self, action_by_agent: dict): env_action = {} for agent_id, action_info in action_by_agent.items(): - state_info = self.state_info[agent_id] + state_info = self._state_info[agent_id] tick, port, vessel, action_scope = ( state_info["tick"], state_info["port_idx"], state_info["vessel_idx"], state_info["action_scope"] ) diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index b824905dd..3ea446272 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -4,13 +4,38 @@ import os import sys +from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler + cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) from dqn import get_dqn_policy -from env_wrapper import AGENT_IDS +from env_wrapper import AGENT_IDS, env_config -# use agent IDs as policy names since each agent uses a separate policy update_trigger = {name: 128 for name in AGENT_IDS} warmup = {name: 1 for name in AGENT_IDS} + +# use agent IDs as policy names since each agent uses a separate policy policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} +agent2policy = {name: name for name in AGENT_IDS} + +# exploration creators and mappings +exploration_config = { + "last_ep": 10, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(5, 0.32)] +} + +def get_exploration(): + epsilon_greedy = EpsilonGreedyExploration(num_actions=env_config["wrapper"]["num_actions"]) + epsilon_greedy.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **exploration_config + ) + return epsilon_greedy + + +exploration_func_index = {f"EpsilonGreedy": get_exploration} +agent2exploration = {name: "EpsilonGreedy" for name in AGENT_IDS} diff --git a/examples/rl/workflows/asynchronous/actor.py b/examples/rl/workflows/asynchronous/actor.py index 4b9c9d225..302dd56d1 100644 --- a/examples/rl/workflows/asynchronous/actor.py +++ b/examples/rl/workflows/asynchronous/actor.py @@ -5,13 +5,16 @@ from os import environ from os.path import dirname, realpath +from maro.rl.learning import AgentWrapper from maro.rl.learning.asynchronous import actor workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from general import ( + agent2exploration, agent2policy, config, policy_func_index, exploration_func_index, get_env_wrapper, log_dir +) if __name__ == "__main__": @@ -19,7 +22,12 @@ config["async"]["group"], environ["ACTORID"], get_env_wrapper(), - get_agent_wrapper(), + AgentWrapper( + {name: func(learning=False) for name, func in policy_func_index.items()}, + agent2policy, + exploration_dict={name: func() for name, func in exploration_func_index.items()}, + agent2exploration=agent2exploration + ), config["num_episodes"], num_steps=config["num_steps"], proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 57c498a86..d8df70136 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -20,8 +20,10 @@ module = importlib.import_module(f"{config['scenario']}") +agent2exploration = getattr(module, "agent2exploration", None) +agent2policy = getattr(module, "agent2policy") +exploration_func_index = getattr(module, "exploration_fun_index", None) get_env_wrapper = getattr(module, "get_env_wrapper") -get_agent_wrapper = getattr(module, "get_agent_wrapper") policy_func_index = getattr(module, "policy_func_index") update_trigger = getattr(module, "update_trigger") warmup = getattr(module, "warmup") diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index 1b7458f86..594e78e61 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -4,19 +4,27 @@ import sys from os.path import dirname, realpath -from maro.rl.learning import SimpleLearner +from maro.rl.learning import AgentWrapper, SimpleLearner + workflow_dir = dirname((realpath(__file__))) if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from general import ( + agent2exploration, agent2policy, config, policy_func_index, exploration_func_index, get_env_wrapper, log_dir +) if __name__ == "__main__": SimpleLearner( get_env_wrapper(), - get_agent_wrapper(), + AgentWrapper( + {name: func(learning=False) for name, func in policy_func_index.items()}, + agent2policy, + exploration_dict={name: func() for name, func in exploration_func_index.items()}, + agent2exploration=agent2exploration + ), num_episodes=config["num_episodes"], num_steps=config["num_steps"], eval_schedule=config["eval_schedule"], diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 25daeee71..b89763ac7 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -5,13 +5,17 @@ from os import environ from os.path import dirname, realpath +from maro.rl.learning import AgentWrapper from maro.rl.learning.synchronous import rollout_worker_node + workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from general import ( + agent2exploration, agent2policy, config, policy_func_index, exploration_func_index, get_env_wrapper, log_dir +) if __name__ == "__main__": @@ -19,7 +23,12 @@ config["sync"]["rollout_group"], int(environ["WORKERID"]), get_env_wrapper(), - get_agent_wrapper(), + AgentWrapper( + {name: func(learning=False) for name, func in policy_func_index.items()}, + agent2policy, + exploration_dict={name: func() for name, func in exploration_func_index.items()}, + agent2exploration=agent2exploration + ), proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index 22edd4b69..74cfcd2f6 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -23,7 +23,6 @@ class AbsEnvWrapper(ABC): """ def __init__(self, env: Env, reward_eval_delay: int = 0, save_replay: bool = True, replay_agent_ids: list = None): self.env = env - self.state_info = None # context for converting model output to actions that can be executed by the env self.reward_eval_delay = reward_eval_delay self.action_history = defaultdict(dict) self.save_replay = save_replay @@ -176,7 +175,6 @@ def get_experiences(self): def reset(self): self.env.reset() - self.state_info = None self._total_reward.clear() self._state = None self._pending_reward_cache.clear() From 18cd676f2f46247586b32631b703d7cc75de51ba Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 9 Jul 2021 10:05:46 +0000 Subject: [PATCH 367/482] added agent wrapper instantiation to workflows --- examples/rl/scripts/run.sh | 2 +- examples/rl/workflows/agent_wrapper.py | 27 +++++++++++++++++++ examples/rl/workflows/asynchronous/actor.py | 13 +++------ examples/rl/workflows/general.py | 2 +- examples/rl/workflows/synchronous/learner.py | 3 ++- .../workflows/synchronous/rollout_worker.py | 13 +++------ 6 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 examples/rl/workflows/agent_wrapper.py diff --git a/examples/rl/scripts/run.sh b/examples/rl/scripts/run.sh index e6f685f64..b241ba0e7 100644 --- a/examples/rl/scripts/run.sh +++ b/examples/rl/scripts/run.sh @@ -1,4 +1,4 @@ #!/bin/bash BASEDIR=$(dirname "$0") -python3 $BASEDIR/../templates/simple_learner.py \ No newline at end of file +python3 $BASEDIR/../workflows/simple_learner.py \ No newline at end of file diff --git a/examples/rl/workflows/agent_wrapper.py b/examples/rl/workflows/agent_wrapper.py new file mode 100644 index 000000000..6fa74ea06 --- /dev/null +++ b/examples/rl/workflows/agent_wrapper.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os.path import dirname, realpath + +from maro.rl.learning import AgentWrapper + +workflow_dir = dirname(dirname(realpath(__file__))) # template directory +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) + +from general import agent2exploration, agent2policy, policy_func_index, exploration_func_index + + +def get_agent_wrapper(): + if exploration_func_index: + exploration_dict = {name: func() for name, func in exploration_func_index.items()} + else: + exploration_dict = None + + return AgentWrapper( + {name: func(learning=False) for name, func in policy_func_index.items()}, + agent2policy, + exploration_dict=exploration_dict, + agent2exploration=agent2exploration + ) diff --git a/examples/rl/workflows/asynchronous/actor.py b/examples/rl/workflows/asynchronous/actor.py index 302dd56d1..f3ce5e373 100644 --- a/examples/rl/workflows/asynchronous/actor.py +++ b/examples/rl/workflows/asynchronous/actor.py @@ -5,16 +5,14 @@ from os import environ from os.path import dirname, realpath -from maro.rl.learning import AgentWrapper from maro.rl.learning.asynchronous import actor workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import ( - agent2exploration, agent2policy, config, policy_func_index, exploration_func_index, get_env_wrapper, log_dir -) +from agent_wrapper import get_agent_wrapper +from general import config, get_env_wrapper, log_dir if __name__ == "__main__": @@ -22,12 +20,7 @@ config["async"]["group"], environ["ACTORID"], get_env_wrapper(), - AgentWrapper( - {name: func(learning=False) for name, func in policy_func_index.items()}, - agent2policy, - exploration_dict={name: func() for name, func in exploration_func_index.items()}, - agent2exploration=agent2exploration - ), + get_agent_wrapper(), config["num_episodes"], num_steps=config["num_steps"], proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index d8df70136..799af6717 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -22,7 +22,7 @@ agent2exploration = getattr(module, "agent2exploration", None) agent2policy = getattr(module, "agent2policy") -exploration_func_index = getattr(module, "exploration_fun_index", None) +exploration_func_index = getattr(module, "exploration_func_index", None) get_env_wrapper = getattr(module, "get_env_wrapper") policy_func_index = getattr(module, "policy_func_index") update_trigger = getattr(module, "update_trigger") diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py index edc9ae574..00b10d8cb 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -12,8 +12,9 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) +from agent_wrapper import get_agent_wrapper from policy_manager.policy_manager import get_policy_manager -from general import config, get_agent_wrapper, get_env_wrapper, log_dir +from general import config, get_env_wrapper, log_dir def get_rollout_manager(): diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index b89763ac7..d35068d75 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -5,7 +5,6 @@ from os import environ from os.path import dirname, realpath -from maro.rl.learning import AgentWrapper from maro.rl.learning.synchronous import rollout_worker_node @@ -13,9 +12,8 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import ( - agent2exploration, agent2policy, config, policy_func_index, exploration_func_index, get_env_wrapper, log_dir -) +from agent_wrapper import get_agent_wrapper +from general import config, get_env_wrapper, log_dir if __name__ == "__main__": @@ -23,12 +21,7 @@ config["sync"]["rollout_group"], int(environ["WORKERID"]), get_env_wrapper(), - AgentWrapper( - {name: func(learning=False) for name, func in policy_func_index.items()}, - agent2policy, - exploration_dict={name: func() for name, func in exploration_func_index.items()}, - agent2exploration=agent2exploration - ), + get_agent_wrapper(), proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) From c5cf9df6ac5970df408cba98d9f4969619c23bd6 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 9 Jul 2021 11:25:10 +0000 Subject: [PATCH 368/482] removed abs_block and added max_prob option for DiscretePolicyNet and DiscreteACNet --- docs/source/key_components/rl_toolkit.rst | 9 +++---- maro/rl/model/__init__.py | 2 -- maro/rl/model/abs_block.py | 14 ----------- maro/rl/model/core_model.py | 30 +++++++++++++++-------- maro/rl/model/fc_block.py | 4 +-- maro/rl/policy/policy.py | 1 - 6 files changed, 24 insertions(+), 36 deletions(-) delete mode 100644 maro/rl/model/abs_block.py diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 5c06efa50..e35451415 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -117,12 +117,9 @@ In the deep reinforcement learning (DRL) world, a core policy usually includes o which may be used to compute action preferences or estimate state / action values. The core model abstraction is designed to decouple the the inner workings of these models from the algorithmic aspects of the policy that uses them. For example, the actor-critic algorithm does not need to concern itself with the structures and optimizing schemes of the actor and -critic models. The abstraction consists of ``AbsBlock`` and ``AbsCoreModel``, both of which subclass torch's nn.Module. -The ``AbsBlock`` represents the smallest structural unit of an NN-based model. For instance, the ``FullyConnectedBlock`` -is a stack of fully connected layers with features like batch normalization, drop-out and skip connection. The ``AbsCoreModel`` -is a collection of network components with embedded optimizers. Subclasses of ``AbsCoreModel`` provided for use with specific -RL algorithms include ``DiscreteQNet`` for DQN, ``DiscretePolicyNet`` for Policy Gradient, ``DiscreteACNet`` for Actor-Critic -and ``ContinuousACNet`` for DDPG. +critic models. The ``AbsCoreModel`` abstraction represents a collection of network components with embedded optimizers. +Subclasses of ``AbsCoreModel`` provided for use with specific RL algorithms include ``DiscreteQNet`` for DQN, ``DiscretePolicyNet`` +for Policy Gradient, ``DiscreteACNet`` for Actor-Critic and ``ContinuousACNet`` for DDPG. The code snippet below shows how to create a model for the actor-critic algorithm with a shared bottom stack: diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 8b1bb5af4..4f714f1ea 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -1,12 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .abs_block import AbsBlock from .core_model import AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, OptimOption from .fc_block import FullyConnectedBlock __all__ = [ - "AbsBlock", "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "OptimOption", "FullyConnectedBlock", ] diff --git a/maro/rl/model/abs_block.py b/maro/rl/model/abs_block.py deleted file mode 100644 index 2f5a1e850..000000000 --- a/maro/rl/model/abs_block.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import torch.nn as nn - - -class AbsBlock(nn.Module): - @property - def input_dim(self): - raise NotImplementedError - - @property - def output_dim(self): - raise NotImplementedError diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index d6f851971..e8d03af74 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -244,15 +244,20 @@ def forward(self, states) -> torch.tensor: """ raise NotImplementedError - def get_action(self, states): + def get_action(self, states, max_prob: bool = False): """ Given a batch of states, return actions selected based on the probabilities computed by ``forward`` and the corresponding log probabilities. """ - action_prob = Categorical(self.forward(states)) # (batch_size, num_actions) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - return action, log_p + action_prob = self.forward(states) # (batch_size, num_actions) + if max_prob: + prob, action = action_prob.max(dim=1) + return action, torch.log(prob) + else: + action_prob = Categorical(action_prob) # (batch_size, action_space_size) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + return action, log_p class DiscreteACNet(AbsCoreModel): @@ -294,15 +299,20 @@ def forward(self, states, actor: bool = True, critic: bool = True) -> tuple: """ raise NotImplementedError - def get_action(self, states): + def get_action(self, states, max_prob: bool = False): """ Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value for each state. """ - action_prob = Categorical(self.forward(states, critic=False)[0]) # (batch_size, action_space_size) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - return action, log_p + action_prob = self.forward(states, critic=False)[0] + if max_prob: + prob, action = action_prob.max(dim=1) + return action, torch.log(prob) + else: + action_prob = Categorical(action_prob) # (batch_size, action_space_size) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + return action, log_p class ContinuousACNet(AbsCoreModel): diff --git a/maro/rl/model/fc_block.py b/maro/rl/model/fc_block.py index 811d2cf43..b2f03af5d 100644 --- a/maro/rl/model/fc_block.py +++ b/maro/rl/model/fc_block.py @@ -9,10 +9,8 @@ from maro.rl.utils import get_torch_activation_cls -from .abs_block import AbsBlock - -class FullyConnectedBlock(AbsBlock): +class FullyConnectedBlock(nn.Module): """Fully connected network with optional batch normalization, activation and dropout components. Args: diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 715f26505..e91ec1f81 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import time from abc import ABC, abstractmethod from maro.rl.experience import ExperienceManager, ExperienceSet From 1f3b59085edef43390e05a49cb2889e449b8ff03 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 9 Jul 2021 12:44:19 +0000 Subject: [PATCH 369/482] fixed incorrect get_ac_policy signature for CIM --- examples/rl/cim/ac.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 1b27ca6e4..c19a3171d 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -74,7 +74,7 @@ } -def get_ac_policy(name): +def get_ac_policy(learning: bool = True): class MyACNET(DiscreteACNet): def forward(self, states, actor: bool = True, critic: bool = True): states = torch.from_numpy(np.asarray(states)) @@ -96,7 +96,12 @@ def forward(self, states, actor: bool = True, critic: bool = True): optim_option={ "actor": OptimOption(**cfg["model"]["optimization"]["actor"]), "critic": OptimOption(**cfg["model"]["optimization"]["critic"]) - } + } if learning else None ) - experience_manager = ExperienceManager(**cfg["experience_manager"]) - return ActorCritic(name, ac_net, experience_manager, ActorCriticConfig(**cfg["algorithm_config"])) + + if learning: + exp_manager = ExperienceManager(**config["experience_manager"]["learning"]) + else: + exp_manager = ExperienceManager(**config["experience_manager"]["rollout"]) + + return ActorCritic(ac_net, exp_manager, ActorCriticConfig(**cfg["algorithm_config"])) From dd017d3e0241532223901e494070f18bcb29dc60 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 9 Jul 2021 16:38:38 +0000 Subject: [PATCH 370/482] moved exploration inside core policy --- examples/rl/cim/__init__.py | 9 +--- examples/rl/cim/dqn.py | 20 ++++++++- examples/rl/cim/policy_index.py | 24 +---------- examples/rl/workflows/agent_wrapper.py | 11 +---- examples/rl/workflows/config.yml | 4 +- examples/rl/workflows/general.py | 2 - maro/rl/exploration/__init__.py | 4 +- .../exploration/discrete_space_exploration.py | 43 +++++++++++++++++++ .../exploration/epsilon_greedy_exploration.py | 30 ------------- maro/rl/exploration/exploration_scheduler.py | 5 +-- maro/rl/learning/agent_wrapper.py | 40 +++++------------ maro/rl/model/core_model.py | 2 +- maro/rl/policy/algorithms/ddpg.py | 7 ++- maro/rl/policy/algorithms/dqn.py | 18 ++++++-- maro/rl/policy/policy.py | 16 ++++++- 15 files changed, 120 insertions(+), 115 deletions(-) create mode 100644 maro/rl/exploration/discrete_space_exploration.py delete mode 100644 maro/rl/exploration/epsilon_greedy_exploration.py diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index b3d9785e3..2a2cf93a6 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -2,11 +2,6 @@ # Licensed under the MIT license. from .env_wrapper import get_env_wrapper -from .policy_index import ( - agent2exploration, agent2policy, exploration_func_index, policy_func_index, update_trigger, warmup -) +from .policy_index import agent2policy, policy_func_index, update_trigger, warmup -__all__ = [ - "agent2exploration", "agent2policy", "exploration_func_index", "get_env_wrapper", "policy_func_index", - "update_trigger", "warmup" -] +__all__ = ["agent2policy", "get_env_wrapper", "policy_func_index", "update_trigger", "warmup"] diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index 21f17264e..a2bd17408 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -8,6 +8,7 @@ import torch import torch.nn as nn from maro.rl.experience import ExperienceManager +from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption from maro.rl.policy.algorithms import DQN, DQNConfig @@ -56,7 +57,13 @@ "beta": 0.4, "beta_step": 0.001 } - } + }, + "exploration": { + "last_ep": 10, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(5, 0.32)] + } } @@ -78,6 +85,15 @@ def get_dqn_policy(learning: bool = True): ) if learning: exp_manager = ExperienceManager(**config["experience_manager"]["learning"]) + exploration = None else: exp_manager = ExperienceManager(**config["experience_manager"]["rollout"]) - return DQN(qnet, exp_manager, DQNConfig(**config["algorithm"])) + exploration = EpsilonGreedyExploration() + print("expl ID: ", id(exploration)) + exploration.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **config["exploration"] + ) + + return DQN(qnet, exp_manager, DQNConfig(**config["algorithm"]), exploration=exploration) diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index 3ea446272..4bfb44ae5 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -4,13 +4,12 @@ import os import sys -from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) from dqn import get_dqn_policy -from env_wrapper import AGENT_IDS, env_config +from env_wrapper import AGENT_IDS update_trigger = {name: 128 for name in AGENT_IDS} warmup = {name: 1 for name in AGENT_IDS} @@ -18,24 +17,3 @@ # use agent IDs as policy names since each agent uses a separate policy policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} agent2policy = {name: name for name in AGENT_IDS} - -# exploration creators and mappings -exploration_config = { - "last_ep": 10, - "initial_value": 0.4, - "final_value": 0.0, - "splits": [(5, 0.32)] -} - -def get_exploration(): - epsilon_greedy = EpsilonGreedyExploration(num_actions=env_config["wrapper"]["num_actions"]) - epsilon_greedy.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - **exploration_config - ) - return epsilon_greedy - - -exploration_func_index = {f"EpsilonGreedy": get_exploration} -agent2exploration = {name: "EpsilonGreedy" for name in AGENT_IDS} diff --git a/examples/rl/workflows/agent_wrapper.py b/examples/rl/workflows/agent_wrapper.py index 6fa74ea06..193743656 100644 --- a/examples/rl/workflows/agent_wrapper.py +++ b/examples/rl/workflows/agent_wrapper.py @@ -10,18 +10,11 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import agent2exploration, agent2policy, policy_func_index, exploration_func_index +from general import agent2policy, policy_func_index def get_agent_wrapper(): - if exploration_func_index: - exploration_dict = {name: func() for name, func in exploration_func_index.items()} - else: - exploration_dict = None - return AgentWrapper( {name: func(learning=False) for name, func in policy_func_index.items()}, - agent2policy, - exploration_dict=exploration_dict, - agent2exploration=agent2exploration + agent2policy ) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 30e276f5d..5b6a7d3bd 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -12,8 +12,8 @@ log_env_summary: true sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 6 - min_finished_workers: 4 + num_rollout_workers: 3 + min_finished_workers: 2 # max_extra_recv_tries: 3 extra_recv_timeout: 100 async: diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 799af6717..3678064b1 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -20,9 +20,7 @@ module = importlib.import_module(f"{config['scenario']}") -agent2exploration = getattr(module, "agent2exploration", None) agent2policy = getattr(module, "agent2policy") -exploration_func_index = getattr(module, "exploration_func_index", None) get_env_wrapper = getattr(module, "get_env_wrapper") policy_func_index = getattr(module, "policy_func_index") update_trigger = getattr(module, "update_trigger") diff --git a/maro/rl/exploration/__init__.py b/maro/rl/exploration/__init__.py index 0c01452aa..0edfd93bc 100644 --- a/maro/rl/exploration/__init__.py +++ b/maro/rl/exploration/__init__.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. from .abs_exploration import AbsExploration, NullExploration -from .epsilon_greedy_exploration import EpsilonGreedyExploration +from .discrete_space_exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from .exploration_scheduler import ( AbsExplorationScheduler, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler ) @@ -10,7 +10,7 @@ __all__ = [ "AbsExploration", "NullExploration", - "EpsilonGreedyExploration", + "DiscreteSpaceExploration", "EpsilonGreedyExploration", "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", "GaussianNoiseExploration", "NoiseExploration", "UniformNoiseExploration" ] diff --git a/maro/rl/exploration/discrete_space_exploration.py b/maro/rl/exploration/discrete_space_exploration.py new file mode 100644 index 000000000..823cc4e70 --- /dev/null +++ b/maro/rl/exploration/discrete_space_exploration.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import abstractmethod +from typing import Union + +import numpy as np + +from .abs_exploration import AbsExploration + + +class DiscreteSpaceExploration(AbsExploration): + """Exploration for discrete action spaces.""" + def __init__(self): + super().__init__() + self._action_space = None + + def set_action_space(self, action_space): + self._action_space = action_space + + @abstractmethod + def __call__(self, action_index): + raise NotImplementedError + + +class EpsilonGreedyExploration(DiscreteSpaceExploration): + """Epsilon greedy exploration for discrete action spaces. + + Args: + num_actions (int): Number of all possible actions. + """ + def __init__(self, epsilon: float = .0): + super().__init__() + self.epsilon = epsilon + + def __call__(self, action: Union[int, np.ndarray]): + if isinstance(action, np.ndarray): + return np.array([self._get_exploration_action(act) for act in action]) + else: + return self._get_exploration_action(action) + + def _get_exploration_action(self, action): + return action if np.random.random() > self.epsilon else np.random.choice(self._action_space) diff --git a/maro/rl/exploration/epsilon_greedy_exploration.py b/maro/rl/exploration/epsilon_greedy_exploration.py deleted file mode 100644 index 036c3e0b5..000000000 --- a/maro/rl/exploration/epsilon_greedy_exploration.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Union - -import numpy as np - -from .abs_exploration import AbsExploration - - -class EpsilonGreedyExploration(AbsExploration): - """Epsilon greedy exploration for discrete action spaces. - - Args: - num_actions (int): Number of all possible actions. - """ - def __init__(self, num_actions: int, epsilon: float = .0): - super().__init__() - self._num_actions = num_actions - self.epsilon = epsilon - - def __call__(self, action_index: Union[int, np.ndarray]): - if isinstance(action_index, np.ndarray): - return np.array([self._get_exploration_action(act) for act in action_index]) - else: - return self._get_exploration_action(action_index) - - def _get_exploration_action(self, action_index): - assert (action_index < self._num_actions), f"Invalid action: {action_index}" - return action_index if np.random.random() > self.epsilon else np.random.choice(self._num_actions) diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/exploration_scheduler.py index 78448e748..1d0c285d9 100644 --- a/maro/rl/exploration/exploration_scheduler.py +++ b/maro/rl/exploration/exploration_scheduler.py @@ -104,8 +104,7 @@ def __init__( initial_value: float = None ): # validate splits - splits.append((1, initial_value)) - splits.append((last_ep, final_value)) + splits = [(1, initial_value)] + splits + [(last_ep, final_value)] splits.sort() for (ep, _), (ep2, _) in zip(splits, splits[1:]): if ep == ep2: @@ -134,7 +133,7 @@ def step(self): if __name__ == "__main__": - from maro.rl.exploration.epsilon_greedy_exploration import EpsilonGreedyExploration + from maro.rl.exploration.discrete_space_exploration import EpsilonGreedyExploration exploration = EpsilonGreedyExploration(5, epsilon=0.6) scheduler = MultiPhaseLinearExplorationScheduler( exploration, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0 diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index e49fd9c4f..14d6d3652 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -3,7 +3,6 @@ from typing import Dict -from maro.rl.exploration import AbsExploration from maro.rl.policy import AbsPolicy from .env_wrapper import AbsEnvWrapper @@ -16,26 +15,11 @@ class AgentWrapper: policy_dict (Dict[str, AbsPolicy]): Policies for inference. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. - exploration_dict (Dict[str, AbsExploration]): A dictionary of named ``AbsExploration`` instances. Defaults - to None. - agent2exploration (Dict[str, str]): Mapping from agent names to exploration instance names. Defaults to None. """ - def __init__( - self, - policy_dict: Dict[str, AbsPolicy], - agent2policy: Dict[str, str], - exploration_dict: Dict[str, AbsExploration] = None, - agent2exploration: Dict[str, str] = None - ): + def __init__(self, policy_dict: Dict[str, AbsPolicy], agent2policy: Dict[str, str]): self.policy_dict = policy_dict self.agent2policy = agent2policy self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} - self.exploration_dict = exploration_dict - if self.exploration_dict: - self.exploration_by_agent = { - agent_id: exploration_dict[exploration_id] for agent_id, exploration_id in agent2exploration.items() - } - self.exploring = True # Flag indicating that exploration is turned on. def choose_action(self, state: dict) -> dict: """Generate an action based on the given state. @@ -43,13 +27,7 @@ def choose_action(self, state: dict) -> dict: Args: state (dict): Dicitionary of agents' states based on which action decisions will be made. """ - action_by_agent = {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} - if self.exploring and self.exploration_dict: - for agent_id in action_by_agent: - if agent_id in self.exploration_by_agent: - action_by_agent[agent_id] = self.exploration_by_agent[agent_id](action_by_agent[agent_id]) - - return action_by_agent + return {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} def get_batch(self, env: AbsEnvWrapper): """Get experiences by policy names.""" @@ -67,12 +45,16 @@ def set_policy_states(self, policy_state_dict: dict): self.policy_dict[policy_id].set_state(policy_state) def exploration_step(self): - if self.exploration_dict: - for exploration in self.exploration_dict.values(): - exploration.step() + for policy in self.policy_dict.values(): + if hasattr(policy, "exploration_step"): + policy.exploration_step() def exploit(self): - self.exploring = False + for policy in self.policy_dict.values(): + if hasattr(policy, "exploit"): + policy.exploit() def explore(self): - self.exploring = True + for policy in self.policy_dict.values(): + if hasattr(policy, "explore"): + policy.explore() diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index e8d03af74..82f1fde2d 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -199,7 +199,7 @@ def get_action(self, states): """ q_for_all_actions = self.forward(states) # (batch_size, num_actions) greedy_q, actions = q_for_all_actions.max(dim=1) - return actions.detach(), greedy_q.detach() + return actions.detach(), greedy_q.detach(), q_for_all_actions.shape[1] def q_values(self, states, actions: torch.tensor): """Return the Q-values for a batch of states and actions.""" diff --git a/maro/rl/policy/algorithms/ddpg.py b/maro/rl/policy/algorithms/ddpg.py index 704fc8a7e..5d169f8bf 100644 --- a/maro/rl/policy/algorithms/ddpg.py +++ b/maro/rl/policy/algorithms/ddpg.py @@ -65,7 +65,12 @@ class DDPG(AbsCorePolicy): for training. config (DDPGConfig): Configuration for DDPG algorithm. """ - def __init__(self, ac_net: ContinuousACNet, experience_manager: ExperienceManager, config: DDPGConfig): + def __init__( + self, + ac_net: ContinuousACNet, + experience_manager: ExperienceManager, + config: DDPGConfig + ): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") diff --git a/maro/rl/policy/algorithms/dqn.py b/maro/rl/policy/algorithms/dqn.py index 0cce2d099..add59c7a3 100644 --- a/maro/rl/policy/algorithms/dqn.py +++ b/maro/rl/policy/algorithms/dqn.py @@ -7,6 +7,7 @@ import torch from maro.rl.experience import ExperienceManager, PrioritizedSampler +from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.model import DiscreteQNet from maro.rl.policy import AbsCorePolicy @@ -55,12 +56,20 @@ class DQN(AbsCorePolicy): experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. config (DQNConfig): Configuration for DQN algorithm. + exploration (DiscreteSpaceExploration): Exploration strategy for generating exploratory actions. Defaults to + an ``EpsilonGreedyExploration`` instance. """ - def __init__(self, q_net: DiscreteQNet, experience_manager: ExperienceManager, config: DQNConfig): + def __init__( + self, + q_net: DiscreteQNet, + experience_manager: ExperienceManager, + config: DQNConfig, + exploration: DiscreteSpaceExploration = EpsilonGreedyExploration() + ): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(experience_manager) + super().__init__(experience_manager, exploration=exploration) self.q_net = q_net if self.q_net.trainable: self.target_q_net = q_net.copy() @@ -77,9 +86,12 @@ def __init__(self, q_net: DiscreteQNet, experience_manager: ExperienceManager, c def choose_action(self, states) -> Union[int, np.ndarray]: with torch.no_grad(): self.q_net.eval() - actions, _ = self.q_net.get_action(states) + actions, _, num_actions = self.q_net.get_action(states) actions = actions.cpu().numpy() + self.exploration.set_action_space(np.arange(num_actions)) + if self.exploring: + actions = self.exploration(actions) return actions[0] if len(actions) == 1 else actions def learn(self): diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index e91ec1f81..8465434f1 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from maro.rl.experience import ExperienceManager, ExperienceSet +from maro.rl.exploration import AbsExploration class AbsPolicy(ABC): @@ -33,10 +34,13 @@ class AbsCorePolicy(AbsPolicy): Args: experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences for training. + exploration (AbsExploration): Exploration strategy for generating exploratory actions. Defaults to None. """ - def __init__(self, experience_manager: ExperienceManager): + def __init__(self, experience_manager: ExperienceManager, exploration: AbsExploration = None): super().__init__() self.experience_manager = experience_manager + self.exploration = exploration + self.exploring = True @abstractmethod def choose_action(self, state): @@ -80,6 +84,16 @@ def store(self, exp: ExperienceSet) -> bool: # f"exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" # ) + def exploit(self): + self.exploring = False + + def explore(self): + self.exploring = True + + def exploration_step(self): + if self.exploration: + self.exploration.step() + def load(self, path: str): """Load the policy state from disk.""" pass From 98d0961e04b953c5bf2bc4b0ca7c744c59385485 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 11 Jul 2021 02:43:24 +0000 Subject: [PATCH 371/482] added state to exploration call to support context-dependent exploration --- examples/rl/cim/dqn.py | 1 - .../exploration/discrete_space_exploration.py | 24 +++++++++++++++---- maro/rl/policy/algorithms/dqn.py | 8 ++++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index a2bd17408..827aeb64c 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -89,7 +89,6 @@ def get_dqn_policy(learning: bool = True): else: exp_manager = ExperienceManager(**config["experience_manager"]["rollout"]) exploration = EpsilonGreedyExploration() - print("expl ID: ", id(exploration)) exploration.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, param_name="epsilon", diff --git a/maro/rl/exploration/discrete_space_exploration.py b/maro/rl/exploration/discrete_space_exploration.py index 823cc4e70..eb52e2b63 100644 --- a/maro/rl/exploration/discrete_space_exploration.py +++ b/maro/rl/exploration/discrete_space_exploration.py @@ -13,13 +13,20 @@ class DiscreteSpaceExploration(AbsExploration): """Exploration for discrete action spaces.""" def __init__(self): super().__init__() - self._action_space = None + self.action_space = None def set_action_space(self, action_space): - self._action_space = action_space + self.action_space = action_space @abstractmethod - def __call__(self, action_index): + def __call__(self, action, state=None): + """Generate an exploratory action. + + Args: + action: Optimal action according to the policy to which this exploration instance belongs. + state: State information which might be needed as context to generate an exploratory action. + Defaults to None. + """ raise NotImplementedError @@ -33,11 +40,18 @@ def __init__(self, epsilon: float = .0): super().__init__() self.epsilon = epsilon - def __call__(self, action: Union[int, np.ndarray]): + def __call__(self, action: Union[int, np.ndarray], state=None): + """Generate an exploratory action. + + Args: + action: Optimal action according to the policy to which this exploration instance belongs. + state: State information which might be needed as context to generate an exploratory action. + In this simple epsilon-greedy scheme, it is not used. Defaults to None. + """ if isinstance(action, np.ndarray): return np.array([self._get_exploration_action(act) for act in action]) else: return self._get_exploration_action(action) def _get_exploration_action(self, action): - return action if np.random.random() > self.epsilon else np.random.choice(self._action_space) + return action if np.random.random() > self.epsilon else np.random.choice(self.action_space) diff --git a/maro/rl/policy/algorithms/dqn.py b/maro/rl/policy/algorithms/dqn.py index add59c7a3..6ed240433 100644 --- a/maro/rl/policy/algorithms/dqn.py +++ b/maro/rl/policy/algorithms/dqn.py @@ -86,12 +86,14 @@ def __init__( def choose_action(self, states) -> Union[int, np.ndarray]: with torch.no_grad(): self.q_net.eval() - actions, _, num_actions = self.q_net.get_action(states) + q_for_all_actions = self.q_net(states) # (batch_size, num_actions) + _, actions = q_for_all_actions.max(dim=1) actions = actions.cpu().numpy() - self.exploration.set_action_space(np.arange(num_actions)) + if self.exploration.action_space is None: + self.exploration.set_action_space(np.arange(q_for_all_actions.shape[1])) if self.exploring: - actions = self.exploration(actions) + actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions def learn(self): From bbb6ba731ff0692ecec8df29000c17f5afb64f3c Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 11 Jul 2021 08:06:38 +0000 Subject: [PATCH 372/482] separated non_rl_policy_index and rl_policy_index in workflows --- examples/rl/cim/__init__.py | 4 ++-- examples/rl/cim/policy_index.py | 2 +- examples/rl/workflows/agent_wrapper.py | 7 +++++-- examples/rl/workflows/general.py | 3 ++- examples/rl/workflows/policy_manager/policy_manager.py | 6 +++--- examples/rl/workflows/policy_manager/trainer.py | 4 ++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index 2a2cf93a6..ace955ed0 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -2,6 +2,6 @@ # Licensed under the MIT license. from .env_wrapper import get_env_wrapper -from .policy_index import agent2policy, policy_func_index, update_trigger, warmup +from .policy_index import agent2policy, rl_policy_func_index, update_trigger, warmup -__all__ = ["agent2policy", "get_env_wrapper", "policy_func_index", "update_trigger", "warmup"] +__all__ = ["agent2policy", "get_env_wrapper", "rl_policy_func_index", "update_trigger", "warmup"] diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index 4bfb44ae5..a3ec1c1d0 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -15,5 +15,5 @@ warmup = {name: 1 for name in AGENT_IDS} # use agent IDs as policy names since each agent uses a separate policy -policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} +rl_policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/workflows/agent_wrapper.py b/examples/rl/workflows/agent_wrapper.py index 193743656..2d3145cdd 100644 --- a/examples/rl/workflows/agent_wrapper.py +++ b/examples/rl/workflows/agent_wrapper.py @@ -10,11 +10,14 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import agent2policy, policy_func_index +from general import agent2policy, non_rl_policy_func_index, rl_policy_func_index def get_agent_wrapper(): return AgentWrapper( - {name: func(learning=False) for name, func in policy_func_index.items()}, + { + **{name: func() for name, func in non_rl_policy_func_index.items()}, + **{name: func(learning=False) for name, func in rl_policy_func_index.items()} + }, agent2policy ) diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 3678064b1..979d1d81f 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -22,6 +22,7 @@ agent2policy = getattr(module, "agent2policy") get_env_wrapper = getattr(module, "get_env_wrapper") -policy_func_index = getattr(module, "policy_func_index") +non_rl_policy_func_index = getattr(module, "non_rl_policy_func_index", {}) +rl_policy_func_index = getattr(module, "rl_policy_func_index") update_trigger = getattr(module, "update_trigger") warmup = getattr(module, "warmup") diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py index e6f66deb7..2352045f4 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -10,12 +10,12 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import config, log_dir, policy_func_index, update_trigger, warmup +from general import config, log_dir, rl_policy_func_index, update_trigger, warmup def get_policy_manager(): train_mode = config["policy_manager"]["train_mode"] num_trainers = config["policy_manager"]["num_trainers"] - policy_dict = {name: func() for name, func in policy_func_index.items()} + policy_dict = {name: func() for name, func in rl_policy_func_index.items()} if train_mode == "single-process": return LocalPolicyManager( policy_dict, @@ -27,7 +27,7 @@ def get_policy_manager(): return MultiProcessPolicyManager( policy_dict, num_trainers, - policy_func_index, + rl_policy_func_index, update_trigger=update_trigger, warmup=warmup, log_dir=log_dir diff --git a/examples/rl/workflows/policy_manager/trainer.py b/examples/rl/workflows/policy_manager/trainer.py index 26ccad5d2..628955b28 100644 --- a/examples/rl/workflows/policy_manager/trainer.py +++ b/examples/rl/workflows/policy_manager/trainer.py @@ -11,14 +11,14 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import config, log_dir, policy_func_index +from general import config, log_dir, rl_policy_func_index if __name__ == "__main__": trainer_node( config["policy_manager"]["train_group"], int(environ["TRAINERID"]), - policy_func_index, + rl_policy_func_index, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) From f004fbadb77e16d0a9b4a0dba18d35dd33452b33 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 11 Jul 2021 08:52:42 +0000 Subject: [PATCH 373/482] modified sc example code according to workflow changes --- examples/rl/supply_chain/__init__.py | 7 +++++ examples/rl/supply_chain/agent_wrapper.py | 34 ----------------------- examples/rl/supply_chain/dqn.py | 4 +-- examples/rl/supply_chain/env_wrapper.py | 6 ++-- examples/rl/supply_chain/policy_index.py | 20 +++++++++---- 5 files changed, 27 insertions(+), 44 deletions(-) delete mode 100644 examples/rl/supply_chain/agent_wrapper.py diff --git a/examples/rl/supply_chain/__init__.py b/examples/rl/supply_chain/__init__.py index b14b47650..71c206463 100644 --- a/examples/rl/supply_chain/__init__.py +++ b/examples/rl/supply_chain/__init__.py @@ -1,2 +1,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. + +from .env_wrapper import get_env_wrapper +from .policy_index import agent2policy, non_rl_policy_func_index, rl_policy_func_index, update_trigger, warmup + +__all__ = [ + "agent2policy", "get_env_wrapper", "non_rl_policy_func_index", "rl_policy_func_index", "update_trigger", "warmup" +] diff --git a/examples/rl/supply_chain/agent_wrapper.py b/examples/rl/supply_chain/agent_wrapper.py deleted file mode 100644 index 7213973dd..000000000 --- a/examples/rl/supply_chain/agent_wrapper.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl.exploration import EpsilonGreedyExploration, LinearExplorationScheduler -from maro.rl.learning import AgentWrapper - -sc_path = os.path.dirname(os.path.realpath(__file__)) -if sc_path not in sys.path: - sys.path.insert(0, sc_path) -from env_wrapper import NUM_ACTIONS, AGENT_IDS -from policy_index import NUM_RL_POLICIES, rollout_policy_func_index - - - -def get_agent_wrapper(): - - consumerstores = [agent_id for agent_id in AGENT_IDS if agent_id.startswith("consumerstore")] - agent2policy = { - agent_id: agent_id.split(".")[0] for agent_id in AGENT_IDS if not agent_id.startswith("consumerstore") - } - for i, agent_id in enumerate(consumerstores): - agent2policy[agent_id] = f"consumerstore-{i % NUM_RL_POLICIES}" - - return AgentWrapper( - {name: func() for name, func in rollout_policy_func_index.items()}, - agent2policy, - exploration_dict={"consumerstore": epsilon_greedy}, - agent2exploration = { - agent_id: "consumerstore" for agent_id in AGENT_IDS if agent_id.startswith("consumerstore") - } - ) diff --git a/examples/rl/supply_chain/dqn.py b/examples/rl/supply_chain/dqn.py index be6d12a13..6e883ac1e 100644 --- a/examples/rl/supply_chain/dqn.py +++ b/examples/rl/supply_chain/dqn.py @@ -53,14 +53,14 @@ "batch_size": 128, "replace": False }, - "training": { # for experience managers in the learner process + "learning": { # for experience managers in the learner process "capacity": 100000, "overwrite_type": "rolling", "batch_size": 256, "replace": True } }, - "exploration_config": { + "exploration": { "last_ep": 10, "initial_value": 0.8, # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. "final_value": 0.0 diff --git a/examples/rl/supply_chain/env_wrapper.py b/examples/rl/supply_chain/env_wrapper.py index 6b49bbe49..33d0d34fd 100644 --- a/examples/rl/supply_chain/env_wrapper.py +++ b/examples/rl/supply_chain/env_wrapper.py @@ -6,7 +6,7 @@ import scipy.stats as st import numpy as np -from maro.rl import AbsEnvWrapper +from maro.rl.learning import AbsEnvWrapper from maro.simulator import Env from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction @@ -238,8 +238,8 @@ def get_rl_policy_state(self, state, agent_info): self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] - self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} - self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} + #self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} + #self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} np_state = self._serialize_state(state) return np_state diff --git a/examples/rl/supply_chain/policy_index.py b/examples/rl/supply_chain/policy_index.py index a3d394f03..eb968bc95 100644 --- a/examples/rl/supply_chain/policy_index.py +++ b/examples/rl/supply_chain/policy_index.py @@ -9,20 +9,30 @@ cim_path = os.path.dirname(__file__) sys.path.insert(0, cim_path) from dqn import get_dqn_policy +from env_wrapper import AGENT_IDS from or_policies import ( get_consumer_baseline_policy, get_consumer_eoq_policy, get_consumer_minmax_policy, get_producer_baseline_policy ) NUM_RL_POLICIES = 100 -rollout_policy_func_index = { +non_rl_policy_func_index = { "consumer": get_consumer_minmax_policy, "producer": get_producer_baseline_policy, "facility": lambda: NullPolicy(), "product": lambda: NullPolicy(), - "productstore": lambda: NullPolicy(), - # consumer store policies - **{f"consumerstore-{i}": get_dqn_policy_for_rollout for i in range(NUM_RL_POLICIES)}, + "productstore": lambda: NullPolicy() } -policy_func_index = {f"consumerstore-{i}": get_dqn_policy for i in range(NUM_RL_POLICIES)} +rl_policy_func_index = {f"consumerstore-{i}": get_dqn_policy for i in range(NUM_RL_POLICIES)} +consumerstores = [agent_id for agent_id in AGENT_IDS if agent_id.startswith("consumerstore")] + +agent2policy = { + agent_id: agent_id.split(".")[0] for agent_id in AGENT_IDS if not agent_id.startswith("consumerstore") +} + +for i, agent_id in enumerate(consumerstores): + agent2policy[agent_id] = f"consumerstore-{i % NUM_RL_POLICIES}" + +update_trigger = {name: 128 for name in rl_policy_func_index} +warmup = {name: 1 for name in rl_policy_func_index} From 2be91141ba374337bddc95e832587259344f7570 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 11 Jul 2021 09:25:36 +0000 Subject: [PATCH 374/482] modified sc example code according to workflow changes --- examples/rl/supply_chain/env_wrapper.py | 474 ++++++++++++++---------- 1 file changed, 287 insertions(+), 187 deletions(-) diff --git a/examples/rl/supply_chain/env_wrapper.py b/examples/rl/supply_chain/env_wrapper.py index 33d0d34fd..af8be94eb 100644 --- a/examples/rl/supply_chain/env_wrapper.py +++ b/examples/rl/supply_chain/env_wrapper.py @@ -2,16 +2,15 @@ # Licensed under the MIT license. from collections import defaultdict, namedtuple +from typing import List import scipy.stats as st import numpy as np from maro.rl.learning import AbsEnvWrapper from maro.simulator import Env -from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction +from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction -# from exploration import exploration_dict, agent2exploration -# from learner import SCLearner def stock_constraint(f_state): return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean'] @@ -238,24 +237,22 @@ def get_rl_policy_state(self, state, agent_info): self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] - #self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} - #self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} - np_state = self._serialize_state(state) return np_state - def get_state(self, event): - cur_tick = self.env.tick + def get_state(self, tick=None): + if tick is None: + tick = self.env.tick settings: dict = self.env.configs.settings consumption_hist_len = settings['consumption_hist_len'] hist_len = settings['sale_hist_len'] - consumption_ticks = [cur_tick - i for i in range(consumption_hist_len-1, -1, -1)] - hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] + consumption_ticks = [tick - i for i in range(consumption_hist_len-1, -1, -1)] + hist_ticks = [tick - i for i in range(hist_len-1, -1, -1)] self.cur_balance_sheet_reward = self.balance_cal.calc() self._cur_metrics = self.env.metrics - self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features] \ + self._cur_distribution_states = self.distribution_ss[tick::distribution_features] \ .flatten() \ .reshape(-1, len(distribution_features)) \ .astype(np.int) @@ -284,7 +281,7 @@ def get_state(self, event): # calculate storage info first, then use it later to speed up. for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"] \ + product_numbers = self.storage_ss[tick:storage_index:"product_number"] \ .flatten() \ .astype(np.int) @@ -306,6 +303,9 @@ def get_state(self, event): # agent_info.agent_type -> policy final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state + #self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} + #self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} + return final_state def get_reward(self, tick=None): @@ -727,28 +727,41 @@ def _build_init_state(self): self._states[agent_info.id] = state -class BalanceSheetCalculator: - consumer_features = ("id", "order_quantity", "price", - "order_cost", "order_product_cost", "reward_discount") - seller_features = ("id", "sold", "demand", "price", "backlog_ratio") - manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - product_features = ( - "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") - storage_features = ("capacity", "remaining_space") - vehicle_features = ("id", "payload", "unit_transport_cost") +ProductInfo = namedtuple( + "ProductInfo", + ( + "unit_id", + "sku_id", + "node_index", + "storage_index", + "unit_storage_cost", + "distribution_index", + "downstream_product_units", + "consumer_id_index_tuple", + "seller_id_index_tuple", + "manufacture_id_index_tuple" + ) +) + +FacilityLevelInfo = namedtuple( + "FacilityLevelInfo", + ( + "unit_id", + "product_unit_id_list", + "storage_index", + "unit_storage_cost", + "distribution_index", + "vehicle_index_list" + ) +) + +class BalanceSheetCalculator: def __init__(self, env: Env): self.env = env - self.consumer_ss = env.snapshot_list["consumer"] - self.seller_ss = env.snapshot_list["seller"] - self.manufacture_ss = env.snapshot_list["manufacture"] - self.storage_ss = env.snapshot_list["storage"] - self.distribution_ss = env.snapshot_list["distribution"] - self.vehicle_ss = env.snapshot_list["vehicle"] - self.product_ss = env.snapshot_list["product"] - self.products = [] + self.products: List[ProductInfo] = [] self.product_id2index_dict = {} - self.facility_levels = [] + self.facility_levels: List[FacilityLevelInfo] = [] self.consumer_id2product = {} self.facilities = env.summary["node_mapping"]["facilities"] @@ -776,33 +789,39 @@ def __init__(self, env: Env): downstream_product_units.append(dproducts[product_id]["id"]) - self.products.append(( - product["id"], - product_id, - product["node_index"], - facility["units"]["storage"]["node_index"], - facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution["node_index"] if distribution is not None else None, - downstream_product_units, - None if consumer is None else (consumer["id"], consumer["node_index"]), - None if seller is None else (seller["id"], seller["node_index"]), - None if manufacture is None else (manufacture["id"], manufacture["node_index"]), - )) - - self.facility_levels.append(( - facility_id, - pid_list, - facility["units"]["storage"]["node_index"], - facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution["node_index"] if distribution is not None else None, - [v["node_index"] for v in distribution["children"]] if distribution is not None else [] - )) + self.products.append( + ProductInfo( + unit_id=product["id"], + sku_id=product_id, + node_index=product["node_index"], + storage_index=facility["units"]["storage"]["node_index"], + unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution_index=distribution["node_index"] if distribution is not None else None, + downstream_product_units=downstream_product_units, + consumer_id_index_tuple=None if consumer is None else (consumer["id"], consumer["node_index"]), + seller_id_index_tuple=None if seller is None else (seller["id"], seller["node_index"]), + manufacture_id_index_tuple=None if manufacture is None else (manufacture["id"], manufacture["node_index"]) + ) + ) + + self.facility_levels.append( + FacilityLevelInfo( + unit_id=facility_id, + product_unit_id_list=pid_list, + storage_index=facility["units"]["storage"]["node_index"], + unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], + distribution_index=distribution["node_index"] if distribution is not None else None, + vehicle_index_list=[ + v["node_index"] for v in distribution["children"] + ] if distribution is not None else [] + ) + ) # TODO: order products make sure calculate reward from downstream to upstream tmp_product_unit_dict = {} for product in self.products: - tmp_product_unit_dict[product[0]] = product + tmp_product_unit_dict[product.unit_id] = product self._ordered_products = [] @@ -810,17 +829,17 @@ def __init__(self, env: Env): for product in self.products: # skip if already being processed - if tmp_product_unit_dict[product[0]] is None: + if tmp_product_unit_dict[product.unit_id] is None: continue - for dproduct in product[6]: + for dproduct in product.downstream_product_units: # push downstream id to stack tmp_stack.append(dproduct) # insert current product to list head self._ordered_products.insert(0, product) # mark it as processed - tmp_product_unit_dict[product[0]] = None + tmp_product_unit_dict[product.unit_id] = None while len(tmp_stack) > 0: # process downstream of product unit in stack @@ -833,7 +852,7 @@ def __init__(self, env: Env): # or extract it downstreams dproduct_unit = tmp_product_unit_dict[dproduct_unit_id] - dproduct_downstreams = dproduct_unit[6] + dproduct_downstreams = dproduct_unit.downstream_product_units for dproduct in dproduct_downstreams: tmp_stack.append(dproduct) @@ -847,206 +866,287 @@ def __init__(self, env: Env): # tick -> (product unit id, sku id, manufacture number, manufacture cost, checkin order, delay penaty) self._supplier_reward_factors = {} - def calc(self): - tick = self.env.tick + def _check_attribute_keys(self, target_type: str, attribute: str): + valid_target_types = list(self.env.summary["node_detail"].keys()) + assert target_type in valid_target_types, f"Target_type {target_type} not in {valid_target_types}!" - ## consumer - # consumer_features = ( - # "id", "order_quantity", "price", "order_cost", "order_product_cost", "reward_discount" - # ) - consumer_bs_states = self.consumer_ss[tick::self.consumer_features]\ - .flatten()\ - .reshape(-1, len(self.consumer_features)) + valid_attributes = list(self.env.summary["node_detail"][target_type]["attributes"].keys()) + assert attribute in valid_attributes, ( + f"Attribute {attribute} not valid for {target_type}. " + f"Valid attributes: {valid_attributes}" + ) + return + + def _get_attributes(self, target_type: str, attribute: str, tick: int=None) -> np.ndarray: + self._check_attribute_keys(target_type, attribute) + + if tick == None: + tick = self.env.tick + + return self.env.snapshot_list[target_type][tick::attribute].flatten() + + def _get_list_attributes(self, target_type: str, attribute: str, tick: int=None) -> List[np.ndarray]: + self._check_attribute_keys(target_type, attribute) + + if tick == None: + tick = self.env.tick + + indexes = list(range(len(self.env.snapshot_list[target_type]))) + return [self.env.snapshot_list[target_type][tick:index:attribute].flatten() for index in indexes] + + def _calc_consumer(self): + #### Consumer + consumer_ids = self._get_attributes("consumer", "id").astype(np.int) # quantity * price - order_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] + order_profit = ( + self._get_attributes("consumer", "order_quantity") + * self._get_attributes("consumer", "price") + ) - # balance_sheet_profit = 0 # order_cost + order_product_cost - consumer_step_balance_sheet_loss = -1 * (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) + consumer_step_balance_sheet_loss = -1 * ( + self._get_attributes("consumer", "order_cost") + + self._get_attributes("consumer", "order_product_cost") + ) # consumer step reward: balance sheet los + profile * discount - # consumer_step_reward = consumer_step_balance_sheet_loss + \ - # order_profit * consumer_bs_states[:, 5] + # consumer_step_reward = ( + # consumer_step_balance_sheet_loss + # + order_profit * self._get_attributes("consumer", "reward_discount") + # ) consumer_step_reward = consumer_step_balance_sheet_loss - ######################################################################## + consumer_step_balance_sheet = order_profit + consumer_step_balance_sheet_loss - ## seller - # seller_features = ("id", "sold", "demand", "price", "backlog_ratio") - seller_bs_states = self.seller_ss[tick::self.seller_features] \ - .flatten()\ - .reshape(-1, len(self.seller_features)) + return consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet + def _calc_seller(self): + #### Seller # profit = sold * price - seller_balance_sheet_profit = seller_bs_states[:, 1] * seller_bs_states[:, 3] + seller_balance_sheet_profit = ( + self._get_attributes("seller", "sold") + * self._get_attributes("seller", "price") + ) # loss = demand * price * backlog_ratio - seller_balance_sheet_loss = -1 * seller_bs_states[:, 2] * seller_bs_states[:, 3] * seller_bs_states[:, 4] + seller_balance_sheet_loss = -1 * ( + self._get_attributes("seller", "demand") + * self._get_attributes("seller", "price") + * self._get_attributes("seller", "backlog_ratio") + ) # step reward = loss + profit seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit - ######################################################################## + return seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward - ## manufacture - # manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - man_bs_states = self.manufacture_ss[tick::self.manufacture_features] \ - .flatten()\ - .reshape(-1, len(self.manufacture_features)) + def _calc_manufacture(self): + #### manufacture + manufacture_ids = self._get_attributes("manufacture", "id").astype(np.int) # loss = manufacture number * cost - man_balance_sheet_profit_loss = -1 * man_bs_states[:, 1] * man_bs_states[:, 2] + manufacture_balance_sheet_loss = -1 * ( + self._get_attributes("manufacture", "manufacturing_number") + * self._get_attributes("manufacture", "product_unit_cost") + ) # step reward = loss - man_step_reward = man_balance_sheet_profit_loss - - ######################################################################## - - ## product - # product_features = ( - # "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty" - # ) - product_bs_states = self.product_ss[tick::self.product_features] \ - .flatten()\ - .reshape(-1, len(self.product_features)) + manufacture_step_reward = manufacture_balance_sheet_loss + manufacture_step_balance_sheet = manufacture_balance_sheet_loss - # product distribution loss = transportation cost + delay order penalty - product_distribution_balance_sheet_loss = -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) - - # product distribution profit = check order * price - product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] + return manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet - # result we need - product_step_reward = np.zeros((len(self.products, ))) - product_balance_sheet_profit = np.zeros((len(self.products, ))) - product_balance_sheet_loss = np.zeros((len(self.products, ))) + def _calc_storage(self): + #### storage + # loss = (capacity-remaining space) * cost + storage_balance_sheet_loss = -1 * ( + self._get_attributes("storage", "capacity") + - self._get_attributes("storage", "remaining_space") + ) # create product number mapping for storages - storages_product_map = {} - for storage_index in range(len(self.storage_ss)): - product_list = self.storage_ss[tick:storage_index:"product_list"] \ - .flatten()\ - .astype(np.int) + product_list = self._get_list_attributes("storage", "product_list") + product_number = self._get_list_attributes("storage", "product_number") + storages_product_map = { + idx: { + id: num + for id, num in zip(id_list.astype(np.int), num_list.astype(np.int)) + } + for idx, (id_list, num_list) in enumerate(zip(product_list, product_number)) + } - product_number = self.storage_ss[tick:storage_index:"product_number"] \ - .flatten()\ - .astype(np.int) + return storage_balance_sheet_loss, storages_product_map - storages_product_map[storage_index] = { - pid: pnum for pid, pnum in zip(product_list, product_number) - } + def _calc_vehicle(self): + ## vehicles + # loss = cost * payload + vehicle_balance_sheet_loss = -1 * ( + self._get_attributes("vehicle", "payload") + * self._get_attributes("vehicle", "unit_transport_cost") + ) + vehicle_step_reward = vehicle_balance_sheet_loss + return vehicle_balance_sheet_loss, vehicle_step_reward - # product balance sheet and reward - # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss - # profit = same as above - # reward = same as above + def _calc_product_distribution(self): + #### product + # product distribution profit = check order * price + product_distribution_balance_sheet_profit = ( + self._get_attributes("product", "distribution_check_order") + * self._get_attributes("product", "price") + ) + # product distribution loss = transportation cost + delay order penalty + product_distribution_balance_sheet_loss = -1 * ( + self._get_attributes("product", "distribution_transport_cost") + + self._get_attributes("product", "distribution_delay_order_penalty") + ) + return product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss + + def _calc_product( + self, + consumer_step_balance_sheet_loss, + consumer_step_reward, + seller_balance_sheet_profit, + seller_balance_sheet_loss, + seller_step_reward, + manufacture_balance_sheet_loss, + manufacture_step_reward, + storages_product_map, + product_distribution_balance_sheet_profit, + product_distribution_balance_sheet_loss, + ): + num_products = len(self.products) + product_step_reward = np.zeros(num_products) + product_balance_sheet_profit = np.zeros(num_products) + product_balance_sheet_loss = np.zeros(num_products) + + # product = consumer + seller + manufacture + storage + distribution + downstreams for product in self._ordered_products: - id, product_id, i, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = product - - if consumer: - product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer[1]] - product_step_reward[i] += consumer_step_reward[consumer[1]] + i = product.node_index - if seller: - product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller[1]] - product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller[1]] - product_step_reward[i] += seller_step_reward[seller[1]] + if product.consumer_id_index_tuple: + consumer_index = product.consumer_id_index_tuple[1] + product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer_index] + product_step_reward[i] += consumer_step_reward[consumer_index] - if manufacture: - product_balance_sheet_loss[i] += man_balance_sheet_profit_loss[manufacture[1]] - product_step_reward[i] += man_step_reward[manufacture[1]] + if product.seller_id_index_tuple: + seller_index = product.seller_id_index_tuple[1] + product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller_index] + product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller_index] + product_step_reward[i] += seller_step_reward[seller_index] - storage_reward = -1 * storages_product_map[storage_index][product_id] * unit_storage_cost + if product.manufacture_id_index_tuple: + manufacture_index = product.manufacture_id_index_tuple[1] + product_balance_sheet_loss[i] += manufacture_balance_sheet_loss[manufacture_index] + product_step_reward[i] += manufacture_step_reward[manufacture_index] + storage_reward = -1 * storages_product_map[product.storage_index][product.sku_id] * product.unit_storage_cost product_step_reward[i] += storage_reward - product_balance_sheet_loss[i] += storage_reward - if distribution_index is not None: - product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[i] + if product.distribution_index is not None: product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[i] + product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[i] + product_step_reward[i] += product_distribution_balance_sheet_loss[i] + product_distribution_balance_sheet_profit[i] - product_step_reward[i] += product_distribution_balance_sheet_loss[i] + \ - product_distribution_balance_sheet_profit[i] - - if len(downstreams) > 0: - for did in downstreams: - product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] + if len(product.downstream_product_units) > 0: + for did in product.downstream_product_units: product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] + product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss - # storage - storage_states = self.storage_ss[tick::self.storage_features] \ - .flatten()\ - .reshape(-1, len(self.storage_features)) - - # loss = (capacity-remaining space) * cost - storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) - - # vehicles - vehicle_states = self.vehicle_ss[tick::self.vehicle_features] \ - .flatten()\ - .reshape(-1, len(self.vehicle_features)) - - # loss = cost * payload - vehicle_balance_sheet_loss = -1 * vehicle_states[:, 1] * vehicle_states[:, 2] - - vehicle_step_reward = vehicle_balance_sheet_loss - - facility_balance_sheet_loss = np.zeros((len(self.facility_levels),)) - facility_balance_sheet_profit = np.zeros((len(self.facility_levels),)) - facility_step_reward = np.zeros((len(self.facility_levels),)) + return product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet + + def _calc_facility( + self, + storage_balance_sheet_loss, + vehicle_balance_sheet_loss, + product_balance_sheet_profit, + product_balance_sheet_loss, + product_step_reward + ): + num_facilities = len(self.facility_levels) + facility_balance_sheet_loss = np.zeros(num_facilities) + facility_balance_sheet_profit = np.zeros(num_facilities) + facility_step_reward = np.zeros(num_facilities) # for facilities for i, facility in enumerate(self.facility_levels): - id, pid_list, storage_index, unit_storage_cost, distribution_index, vehicle_indices = facility - # storage balance sheet # profit=0 - facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * unit_storage_cost + facility_balance_sheet_loss[i] += storage_balance_sheet_loss[facility.storage_index] * facility.unit_storage_cost # distribution balance sheet - if distribution_index is not None: - for vindex in vehicle_indices: - facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vindex] + if facility.distribution_index is not None: + for vidx in facility.vehicle_index_list: + facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vidx] # distribution unit do not provide reward # sku product unit balance sheet - for pid in pid_list: - facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] + for pid in facility.product_unit_id_list: facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] + facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] + facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit + + return facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet + + def calc(self): + #### Basic Units: Loss, Profit, Reward + consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet = self._calc_consumer() + seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward = self._calc_seller() + manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet = self._calc_manufacture() + storage_balance_sheet_loss, storages_product_map = self._calc_storage() + vehicle_balance_sheet_loss, vehicle_step_reward = self._calc_vehicle() + product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss = self._calc_product_distribution() + ######################################################################## + + #### Loss, profit, reward for each product + product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet = self._calc_product( + consumer_step_balance_sheet_loss, + consumer_step_reward, + seller_balance_sheet_profit, + seller_balance_sheet_loss, + seller_step_reward, + manufacture_balance_sheet_loss, + manufacture_step_reward, + storages_product_map, + product_distribution_balance_sheet_profit, + product_distribution_balance_sheet_loss + ) + ######################################################################## + + #### Loss, profit, reward for each facility + # facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet = self._calc_facility( + # storage_balance_sheet_loss, + # vehicle_balance_sheet_loss, + # product_balance_sheet_profit, + # product_balance_sheet_loss, + # product_step_reward + # ) + ######################################################################## + # Final result for current tick, key is the facility/unit id, value is tuple of balance sheet and reward. result = {} # For product units. - for id, bs, rw in zip([item[0] for item in self.products], product_balance_sheet, product_step_reward): + for id, bs, rw in zip([product.unit_id for product in self.products], product_balance_sheet, product_step_reward): result[id] = (bs, rw) - self.total_balance_sheet[id] += bs - facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit - - # For consumers. - consumer_step_balance_sheet = order_profit + consumer_step_balance_sheet_loss - - for id, bs, rw in zip(consumer_bs_states[:, 0], consumer_step_balance_sheet, consumer_step_reward): - # result[int(id)] = (bs, rw) + for id, bs, rw in zip(consumer_ids, consumer_step_balance_sheet, consumer_step_reward): + # result[id] = (bs, rw) # let reward of a consumer equate its parent product - result[int(id)] = result[self.consumer_id2product[int(id)]] - self.total_balance_sheet[id] += result[int(id)][0] + result[id] = result[self.consumer_id2product[id]] + self.total_balance_sheet[id] += result[id][0] # For producers. - man_step_balance_sheet = man_balance_sheet_profit_loss - - for id, bs, rw in zip(man_bs_states[:, 0], man_step_balance_sheet, man_step_reward): - result[int(id)] = (bs, rw) - + for id, bs, rw in zip(manufacture_ids, manufacture_step_balance_sheet, manufacture_step_reward): + result[id] = (bs, rw) self.total_balance_sheet[id] += bs # NOTE: add followings if you need. From 9b04ad5ce0176e49b3212e2ffa95c3b9fe88f09a Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 12 Jul 2021 05:46:26 +0000 Subject: [PATCH 375/482] added replay_agent_ids parameter to get_env_func for RL examples --- examples/rl/cim/env_wrapper.py | 10 +-- examples/rl/workflows/asynchronous/actor.py | 7 +- examples/rl/workflows/general.py | 5 ++ .../workflows/synchronous/rollout_worker.py | 7 +- maro/rl/learning/agent_wrapper.py | 3 +- .../rl/learning/asynchronous/policy_server.py | 2 +- maro/rl/learning/env_wrapper.py | 77 +++++++++++-------- 7 files changed, 64 insertions(+), 47 deletions(-) diff --git a/examples/rl/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py index 68e3cebb5..dbbbe671b 100644 --- a/examples/rl/cim/env_wrapper.py +++ b/examples/rl/cim/env_wrapper.py @@ -10,11 +10,11 @@ class CIMEnvWrapper(AbsEnvWrapper): def __init__( - self, env, *, port_attributes, vessel_attributes, num_actions, look_back, max_ports_downstream, - reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, + self, env, replay_agent_ids=None, *, port_attributes, vessel_attributes, num_actions, look_back, + max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, finite_vessel_space=True, has_early_discharge=True ): - super().__init__(env, reward_eval_delay=reward_eval_delay) + super().__init__(env, replay_agent_ids=replay_agent_ids, reward_eval_delay=reward_eval_delay) self.port_attributes = port_attributes self.vessel_attributes = vessel_attributes self.action_space = list(np.linspace(-1.0, 1.0, num_actions)) @@ -133,8 +133,8 @@ def get_reward(self, tick=None): } } -def get_env_wrapper(): - return CIMEnvWrapper(Env(**env_config["basic"]), **env_config["wrapper"]) +def get_env_wrapper(replay_agent_ids=None): + return CIMEnvWrapper(Env(**env_config["basic"]), replay_agent_ids=replay_agent_ids, **env_config["wrapper"]) tmp_env_wrapper = get_env_wrapper() diff --git a/examples/rl/workflows/asynchronous/actor.py b/examples/rl/workflows/asynchronous/actor.py index f3ce5e373..1237f72a9 100644 --- a/examples/rl/workflows/asynchronous/actor.py +++ b/examples/rl/workflows/asynchronous/actor.py @@ -12,14 +12,15 @@ sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import config, get_env_wrapper, log_dir +from general import config, get_env_wrapper, log_dir, replay_agents if __name__ == "__main__": + actor_id = int(environ["ACTORID"]) actor( config["async"]["group"], - environ["ACTORID"], - get_env_wrapper(), + actor_id, + get_env_wrapper(replay_agent_ids=replay_agents[actor_id]), get_agent_wrapper(), config["num_episodes"], num_steps=config["num_steps"], diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 979d1d81f..442e2ce9d 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -26,3 +26,8 @@ rl_policy_func_index = getattr(module, "rl_policy_func_index") update_trigger = getattr(module, "update_trigger") warmup = getattr(module, "warmup") + +num_rollouts = config["sync"]["num_rollout_workers"] if config["mode"] == "sync" else config["async"]["num_actors"] +replay_agents = [[] for _ in range(num_rollouts)] +for i, agent in enumerate(list(agent2policy.keys())): + replay_agents[i % num_rollouts].append(agent) diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index d35068d75..6ce7fc59f 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -13,14 +13,15 @@ sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import config, get_env_wrapper, log_dir +from general import config, get_env_wrapper, log_dir, replay_agents if __name__ == "__main__": + worker_id = int(environ["WORKERID"]) rollout_worker_node( config["sync"]["rollout_group"], - int(environ["WORKERID"]), - get_env_wrapper(), + worker_id, + get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), get_agent_wrapper(), proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index 14d6d3652..03a1e4f57 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -32,7 +32,8 @@ def choose_action(self, state: dict) -> dict: def get_batch(self, env: AbsEnvWrapper): """Get experiences by policy names.""" names = set() - for agent_id, exp in env.get_experiences().items(): + exp_by_agent = env.get_experiences() + for agent_id, exp in exp_by_agent.items(): if hasattr(self.policy[agent_id], "store"): self.policy[agent_id].store(exp) names.add(self.agent2policy[agent_id]) diff --git a/maro/rl/learning/asynchronous/policy_server.py b/maro/rl/learning/asynchronous/policy_server.py index 51a3a47a8..5a24debcc 100644 --- a/maro/rl/learning/asynchronous/policy_server.py +++ b/maro/rl/learning/asynchronous/policy_server.py @@ -57,7 +57,7 @@ def policy_server( proxy.reply( msg, tag=MsgTag.POLICY_STATE, body={ - MsgKey.POLICY_STATE: policy_manager.get_state(version=msg.body[MsgKey.VERSION]), + MsgKey.POLICY_STATE: policy_manager.get_state(cur_version=msg.body[MsgKey.VERSION]), MsgKey.VERSION: policy_manager.version } ) diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index 74cfcd2f6..f0c477e57 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod from collections import defaultdict, deque +from typing import Callable from maro.rl.experience import ExperienceSet from maro.simulator import Env @@ -16,18 +17,26 @@ class AbsEnvWrapper(ABC): reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. - save_replay (bool): If True, transitions for some or all agents will in stored in internal replay buffers. replay_agent_ids (list): List of agent IDs whose transitions will be stored in internal replay buffers. - If ``save_replay`` is False, this is ignored. Otherwise, if it is None, it will be set to all agents in - Defaults to None. + If it is None, it will be set to all agents in the environment (i.e., env.agent_idx_list). Defaults + to None. + get_experience_func (Callable): Custom function to convert the replay buffer to training experiences. Defaults + to None, in which case the replay buffer will be converted directly to SARS experiences for each agent. """ - def __init__(self, env: Env, reward_eval_delay: int = 0, save_replay: bool = True, replay_agent_ids: list = None): + def __init__( + self, + env: Env, + reward_eval_delay: int = 0, + replay_agent_ids: list = None, + get_experience_func: Callable = None + ): self.env = env self.reward_eval_delay = reward_eval_delay self.action_history = defaultdict(dict) - self.save_replay = save_replay - self.replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids - self._replay_buffer = {agent_id: defaultdict(list) for agent_id in self.replay_agent_ids} + self.get_experience_func = get_experience_func + + replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids + self._replay_buffer = {agent_id: defaultdict(list) for agent_id in replay_agent_ids} self._pending_reward_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None self._total_reward = defaultdict(int) @@ -138,16 +147,14 @@ def step(self, action_by_agent: dict): ): state, action, info, tick = self._pending_reward_cache.popleft() reward = self.get_reward(tick=tick) - # assign rewards to the agents that took action at that tick - if self.save_replay: - for agent_id, st in state.items(): - self._total_reward[agent_id] += reward[agent_id] - if agent_id in self._replay_buffer: - buf = self._replay_buffer[agent_id] - buf["states"].append(st) - buf["actions"].append(action[agent_id]) - buf["rewards"].append(reward[agent_id]) - buf["info"].append(info[agent_id] if info else None) + for agent_id, st in state.items(): + self._total_reward[agent_id] += reward[agent_id] + if agent_id in self._replay_buffer: + buf = self._replay_buffer[agent_id] + buf["states"].append(st) + buf["actions"].append(action[agent_id]) + buf["rewards"].append(reward[agent_id]) + buf["info"].append(info[agent_id] if info else None) def end_of_episode(self): """Custom processing logic at the end of an episode.""" @@ -155,23 +162,25 @@ def end_of_episode(self): def get_experiences(self): """Get per-agent experiences from the replay buffer.""" - exp_by_agent = {} - for agent_id in self.replay_agent_ids: - buf = self._replay_buffer[agent_id] - exp_set = ExperienceSet( - buf["states"][:-1], - buf["actions"][:-1], - buf["rewards"][:-1], - buf["states"][1:], - buf["info"][:-1], - ) - del buf["states"][:-1] - del buf["actions"][:-1] - del buf["rewards"][:-1] - del buf["info"][:-1] - exp_by_agent[agent_id] = exp_set - - return exp_by_agent + if not self.get_experience_func: + exp_by_agent = {} + for agent_id, buf in self._replay_buffer.items(): + exp_set = ExperienceSet( + buf["states"][:-1], + buf["actions"][:-1], + buf["rewards"][:-1], + buf["states"][1:], + buf["info"][:-1], + ) + del buf["states"][:-1] + del buf["actions"][:-1] + del buf["rewards"][:-1] + del buf["info"][:-1] + exp_by_agent[agent_id] = exp_set + + return exp_by_agent + else: + return self.get_experience_func(self._replay_buffer) def reset(self): self.env.reset() From 700b1497223023f83da2428a3d2a533be86e26ea Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 12 Jul 2021 06:37:17 +0000 Subject: [PATCH 376/482] fixed a few bugs --- examples/rl/supply_chain/env_wrapper.py | 16 ++++++---------- examples/rl/workflows/config.yml | 4 ++-- examples/rl/workflows/general.py | 3 ++- .../rl/workflows/synchronous/rollout_worker.py | 1 + maro/rl/learning/env_wrapper.py | 2 +- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/examples/rl/supply_chain/env_wrapper.py b/examples/rl/supply_chain/env_wrapper.py index af8be94eb..f4f1594e7 100644 --- a/examples/rl/supply_chain/env_wrapper.py +++ b/examples/rl/supply_chain/env_wrapper.py @@ -80,8 +80,8 @@ def __getitem__(self, key, default=None): class SCEnvWrapper(AbsEnvWrapper): - def __init__(self, env: Env, reward_eval_delay: int=0, save_replay: bool=True, replay_agent_ids: list=None): - super().__init__(env, reward_eval_delay, save_replay, replay_agent_ids) + def __init__(self, env: Env, reward_eval_delay: int=0, replay_agent_ids: list=None): + super().__init__(env, reward_eval_delay, replay_agent_ids=replay_agent_ids) self.balance_cal = BalanceSheetCalculator(env) self.cur_balance_sheet_reward = None self.storage_ss = env.snapshot_list["storage"] @@ -1162,18 +1162,14 @@ def calc(self): # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder # that contains a single config.yml and should be placed under examples/supply_chain/envs/ "topology": "random", - "durations": 200 # number of ticks per episode + "durations": 100 # number of ticks per episode } -def get_env_wrapper(): - env = Env(**env_config) - replay_agent_ids = [ - f"{info.agent_type}.{info.id}" for info in env.agent_idx_list if info.agent_type == "consumerstore" - ] - return SCEnvWrapper(env, replay_agent_ids=replay_agent_ids) +def get_env_wrapper(replay_agent_ids=None): + return SCEnvWrapper(env=Env(**env_config), replay_agent_ids=replay_agent_ids) -tmp_env_wrapper = get_env_wrapper() +tmp_env_wrapper = get_env_wrapper(replay_agent_ids=[]) AGENT_IDS = [f"{info.agent_type}.{info.id}" for info in tmp_env_wrapper.agent_idx_list] STATE_DIM = tmp_env_wrapper.dim NUM_ACTIONS = 10 diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 9b65f24c3..f47c2fc68 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -21,8 +21,8 @@ async: num_actors: 3 policy_manager: train_group: policy-manager - train_mode: single-process # single-process, multi-process, multi-node + train_mode: multi-node # single-process, multi-process, multi-node num_trainers: 4 redis: host: maro-redis - port: 6379 \ No newline at end of file + port: 6379 diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 442e2ce9d..16c0cb448 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -30,4 +30,5 @@ num_rollouts = config["sync"]["num_rollout_workers"] if config["mode"] == "sync" else config["async"]["num_actors"] replay_agents = [[] for _ in range(num_rollouts)] for i, agent in enumerate(list(agent2policy.keys())): - replay_agents[i % num_rollouts].append(agent) + if agent2policy[agent] in rl_policy_func_index: + replay_agents[i % num_rollouts].append(agent) diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 6ce7fc59f..b6565b88d 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -18,6 +18,7 @@ if __name__ == "__main__": worker_id = int(environ["WORKERID"]) + print(f"replay agents: {replay_agents[worker_id]}") rollout_worker_node( config["sync"]["rollout_group"], worker_id, diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index f0c477e57..15e0afb36 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -35,7 +35,7 @@ def __init__( self.action_history = defaultdict(dict) self.get_experience_func = get_experience_func - replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids + replay_agent_ids = self.env.agent_idx_list if replay_agent_ids is None else replay_agent_ids self._replay_buffer = {agent_id: defaultdict(list) for agent_id in replay_agent_ids} self._pending_reward_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None From b9afaef2c029769e21467375a487dd320647158a Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 12 Jul 2021 08:27:59 +0000 Subject: [PATCH 377/482] added maro/simulator/scenarios/supply_chain as bind mount --- .../rl/scripts/docker/docker_compose_yml.py | 7 +- examples/rl/supply_chain/env_wrapper.py | 5 +- examples/rl/supply_chain/policy_index.py | 4 +- .../supply_chain/topologies/random/config.yml | 29345 ---------------- .../topologies/sample1/config.yml | 335 - examples/rl/workflows/config.yml | 2 +- .../workflows/synchronous/rollout_worker.py | 1 - 7 files changed, 12 insertions(+), 29687 deletions(-) delete mode 100644 examples/rl/supply_chain/topologies/random/config.yml delete mode 100644 examples/rl/supply_chain/topologies/sample1/config.yml diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index fd0a50e56..2d6eb296b 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -11,6 +11,7 @@ root_dir = dirname(dirname(rl_example_dir)) workflow_dir = join(rl_example_dir, "workflows") maro_rl_dir = join(root_dir, "maro", "rl") +maro_sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") config_path = join(workflow_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") @@ -23,7 +24,11 @@ common_spec = { "build": {"context": root_dir, "dockerfile": dockerfile_path}, "image": "maro", - "volumes": [f"{rl_example_dir}:/maro/rl_examples", f"{maro_rl_dir}:/maro/maro/rl"] + "volumes": [ + f"{rl_example_dir}:/maro/rl_examples", + f"{maro_rl_dir}:/maro/maro/rl", + f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" + ] } # trainer spec diff --git a/examples/rl/supply_chain/env_wrapper.py b/examples/rl/supply_chain/env_wrapper.py index f4f1594e7..8a616ea51 100644 --- a/examples/rl/supply_chain/env_wrapper.py +++ b/examples/rl/supply_chain/env_wrapper.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from collections import defaultdict, namedtuple +from os.path import dirname, join, realpath from typing import List import scipy.stats as st @@ -1159,8 +1160,8 @@ def calc(self): env_config = { "scenario": "supply_chain", - # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder - # that contains a single config.yml and should be placed under examples/supply_chain/envs/ + # Currently available topologies are "sample" or "random". New topologies must consist of a single folder + # that contains a single config.yml and should be placed under /maro/simulator/scenarios/supply_chain/topologies "topology": "random", "durations": 100 # number of ticks per episode } diff --git a/examples/rl/supply_chain/policy_index.py b/examples/rl/supply_chain/policy_index.py index eb968bc95..430282eab 100644 --- a/examples/rl/supply_chain/policy_index.py +++ b/examples/rl/supply_chain/policy_index.py @@ -6,8 +6,8 @@ from maro.rl.policy import NullPolicy -cim_path = os.path.dirname(__file__) -sys.path.insert(0, cim_path) +sc_path = os.path.dirname(__file__) +sys.path.insert(0, sc_path) from dqn import get_dqn_policy from env_wrapper import AGENT_IDS from or_policies import ( diff --git a/examples/rl/supply_chain/topologies/random/config.yml b/examples/rl/supply_chain/topologies/random/config.yml deleted file mode 100644 index f4e810def..000000000 --- a/examples/rl/supply_chain/topologies/random/config.yml +++ /dev/null @@ -1,29345 +0,0 @@ -facility_definitions: - RetailerFacility: - children: - products: - class: StoreProductUnit - config: - agent_type: product - consumer: - class: ConsumerUnit - config: - agent_type: consumer - seller: - class: SellerUnit - config: - sale_hist_len: 4 - is_template: true - storage: - class: StorageUnit - class: RetailerFacility - config: - agent_type: facility - SupplierFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: product - consumer: - class: ConsumerUnit - config: - agent_type: consumer - manufacture: - class: ManufactureUnit - config: - agent_type: producer - is_template: true - storage: - class: StorageUnit - class: SupplierFacility - config: - agent_type: facility - WarehouseFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: product - consumer: - class: ConsumerUnit - config: - agent_type: consumer - is_template: true - storage: - class: StorageUnit - class: WarehouseFacility - config: - agent_type: facility -normal_vehicle: &id001 - class: VehicleUnit - config: - patient: 100 - unit_transport_cost: 1 -settings: - constraint_state_hist_len: 4 - constraint_violate_reward: -1000000.0 - consumption_hist_len: 4 - downsampling_rate: 1 - episod_duration: 21 - gamma: 0.99 - global_reward_weight_consumer: 0.5 - global_reward_weight_producer: 0.5 - heading_timesteps: 7 - initial_balance: 100000 - pending_order_len: 4 - replenishment_discount: 0.9 - reward_normalization: 10000000.0 - sale_hist_len: 4 - tail_timesteps: 7 - total_echelons: 3 -world: - facilities: - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 200 - definition_ref: SupplierFacility - name: SUPPLIER0 - skus: - SKU0: - cost: 289 - init_stock: 3150 - price: 322 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU1: - cost: 255 - init_stock: 1100 - price: 284 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU10: - cost: 61 - init_stock: 1250 - price: 68 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU100: - cost: 14 - init_stock: 3250 - price: 16 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU101: - cost: 75 - init_stock: 3550 - price: 84 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU102: - cost: 295 - init_stock: 4650 - price: 328 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU103: - cost: 300 - init_stock: 4750 - price: 334 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU104: - cost: 44 - init_stock: 3250 - price: 49 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU105: - cost: 99 - init_stock: 2850 - price: 110 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU106: - cost: 225 - init_stock: 3650 - price: 251 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU107: - cost: 380 - init_stock: 4350 - price: 423 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU108: - cost: 412 - init_stock: 4600 - price: 458 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU109: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU11: - cost: 360 - init_stock: 2550 - price: 400 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU110: - cost: 59 - init_stock: 700 - price: 66 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU111: - cost: 234 - init_stock: 3050 - price: 260 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU112: - cost: 54 - init_stock: 4750 - price: 61 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU113: - cost: 313 - init_stock: 1550 - price: 348 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU114: - cost: 350 - init_stock: 1350 - price: 389 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU115: - cost: 257 - init_stock: 4300 - price: 286 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU116: - cost: 446 - init_stock: 3600 - price: 496 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU117: - cost: 288 - init_stock: 4600 - price: 320 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU118: - cost: 164 - init_stock: 1650 - price: 183 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU119: - cost: 188 - init_stock: 1600 - price: 209 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU12: - cost: 100 - init_stock: 4200 - price: 112 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU120: - cost: 108 - init_stock: 4900 - price: 121 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU121: - cost: 36 - init_stock: 4250 - price: 40 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU122: - cost: 393 - init_stock: 350 - price: 437 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU123: - cost: 209 - init_stock: 950 - price: 233 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU124: - cost: 163 - init_stock: 1800 - price: 182 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU125: - cost: 14 - init_stock: 4600 - price: 16 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU126: - cost: 32 - init_stock: 1950 - price: 36 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU127: - cost: 195 - init_stock: 1550 - price: 217 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU128: - cost: 148 - init_stock: 950 - price: 165 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU129: - cost: 128 - init_stock: 2500 - price: 143 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU13: - cost: 285 - init_stock: 2850 - price: 317 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU130: - cost: 313 - init_stock: 4900 - price: 348 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU131: - cost: 57 - init_stock: 2250 - price: 64 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU132: - cost: 384 - init_stock: 1050 - price: 427 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU133: - cost: 201 - init_stock: 1450 - price: 224 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU134: - cost: 302 - init_stock: 3850 - price: 336 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU135: - cost: 137 - init_stock: 5000 - price: 153 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU136: - cost: 179 - init_stock: 3550 - price: 199 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU137: - cost: 83 - init_stock: 3700 - price: 93 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU138: - cost: 205 - init_stock: 1800 - price: 228 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU139: - cost: 186 - init_stock: 1200 - price: 207 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU14: - cost: 241 - init_stock: 3100 - price: 268 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU140: - cost: 234 - init_stock: 1700 - price: 261 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU141: - cost: 171 - init_stock: 2050 - price: 190 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU142: - cost: 288 - init_stock: 1900 - price: 320 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU143: - cost: 286 - init_stock: 1300 - price: 318 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU144: - cost: 360 - init_stock: 600 - price: 400 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU145: - cost: 359 - init_stock: 4100 - price: 399 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU146: - cost: 159 - init_stock: 2400 - price: 177 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU147: - cost: 424 - init_stock: 2800 - price: 472 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU148: - cost: 281 - init_stock: 3850 - price: 313 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU149: - cost: 321 - init_stock: 3850 - price: 357 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU15: - cost: 46 - init_stock: 250 - price: 52 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU150: - cost: 95 - init_stock: 3500 - price: 106 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU151: - cost: 200 - init_stock: 3650 - price: 223 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU152: - cost: 9 - init_stock: 2550 - price: 10 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU153: - cost: 396 - init_stock: 3100 - price: 441 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU154: - cost: 69 - init_stock: 4250 - price: 77 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU155: - cost: 379 - init_stock: 2650 - price: 422 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU156: - cost: 9 - init_stock: 600 - price: 10 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU157: - cost: 369 - init_stock: 3750 - price: 410 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU158: - cost: 130 - init_stock: 4050 - price: 145 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU159: - cost: 173 - init_stock: 1250 - price: 193 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU16: - cost: 157 - init_stock: 2900 - price: 175 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU160: - cost: 413 - init_stock: 4250 - price: 459 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU161: - cost: 215 - init_stock: 2300 - price: 239 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU162: - cost: 142 - init_stock: 250 - price: 158 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU163: - cost: 437 - init_stock: 1950 - price: 486 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU164: - cost: 446 - init_stock: 5000 - price: 496 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU165: - cost: 246 - init_stock: 1650 - price: 274 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU166: - cost: 71 - init_stock: 4450 - price: 79 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU167: - cost: 398 - init_stock: 650 - price: 443 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU168: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU169: - cost: 332 - init_stock: 4900 - price: 369 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU17: - cost: 311 - init_stock: 450 - price: 346 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU170: - cost: 61 - init_stock: 2750 - price: 68 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU171: - cost: 358 - init_stock: 3800 - price: 398 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU172: - cost: 180 - init_stock: 3550 - price: 200 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU173: - cost: 157 - init_stock: 4800 - price: 175 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU174: - cost: 261 - init_stock: 3800 - price: 291 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU175: - cost: 75 - init_stock: 3750 - price: 84 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU176: - cost: 366 - init_stock: 3300 - price: 407 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU177: - cost: 231 - init_stock: 1550 - price: 257 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU178: - cost: 380 - init_stock: 250 - price: 423 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU179: - cost: 447 - init_stock: 4150 - price: 497 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU18: - cost: 232 - init_stock: 4050 - price: 258 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU180: - cost: 195 - init_stock: 2750 - price: 217 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU181: - cost: 128 - init_stock: 3000 - price: 143 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU182: - cost: 393 - init_stock: 4950 - price: 437 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU183: - cost: 130 - init_stock: 400 - price: 145 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU184: - cost: 65 - init_stock: 1200 - price: 73 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU185: - cost: 9 - init_stock: 4500 - price: 10 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU186: - cost: 323 - init_stock: 1100 - price: 359 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU187: - cost: 159 - init_stock: 1500 - price: 177 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU188: - cost: 351 - init_stock: 4350 - price: 391 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU189: - cost: 322 - init_stock: 1750 - price: 358 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU19: - cost: 429 - init_stock: 4550 - price: 477 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU190: - cost: 101 - init_stock: 850 - price: 113 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU191: - cost: 425 - init_stock: 2700 - price: 473 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU192: - cost: 373 - init_stock: 3050 - price: 415 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU193: - cost: 186 - init_stock: 1500 - price: 207 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU194: - cost: 388 - init_stock: 250 - price: 432 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU195: - cost: 196 - init_stock: 1550 - price: 218 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU196: - cost: 44 - init_stock: 3400 - price: 49 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU197: - cost: 272 - init_stock: 2850 - price: 303 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU198: - cost: 152 - init_stock: 2700 - price: 169 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU199: - cost: 404 - init_stock: 1150 - price: 449 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU2: - cost: 297 - init_stock: 3500 - price: 331 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU20: - cost: 301 - init_stock: 1250 - price: 335 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU200: - cost: 58 - init_stock: 1250 - price: 65 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU201: - cost: 93 - init_stock: 2950 - price: 104 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU202: - cost: 127 - init_stock: 3650 - price: 142 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU203: - cost: 396 - init_stock: 4100 - price: 440 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU204: - cost: 440 - init_stock: 2350 - price: 489 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU205: - cost: 117 - init_stock: 5000 - price: 130 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU206: - cost: 301 - init_stock: 550 - price: 335 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU207: - cost: 126 - init_stock: 4000 - price: 140 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU208: - cost: 441 - init_stock: 3850 - price: 491 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU209: - cost: 161 - init_stock: 1000 - price: 179 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU21: - cost: 110 - init_stock: 5000 - price: 123 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU210: - cost: 363 - init_stock: 3450 - price: 404 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU211: - cost: 156 - init_stock: 4550 - price: 174 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU212: - cost: 364 - init_stock: 3950 - price: 405 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU213: - cost: 108 - init_stock: 3200 - price: 121 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU214: - cost: 90 - init_stock: 500 - price: 101 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU215: - cost: 377 - init_stock: 2350 - price: 419 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU216: - cost: 297 - init_stock: 1150 - price: 330 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU217: - cost: 255 - init_stock: 3250 - price: 284 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU218: - cost: 184 - init_stock: 2950 - price: 205 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU219: - cost: 82 - init_stock: 2300 - price: 92 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU22: - cost: 443 - init_stock: 3300 - price: 493 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU220: - cost: 348 - init_stock: 4350 - price: 387 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU221: - cost: 35 - init_stock: 3900 - price: 39 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU222: - cost: 103 - init_stock: 1800 - price: 115 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU223: - cost: 176 - init_stock: 600 - price: 196 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU224: - cost: 348 - init_stock: 250 - price: 387 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU225: - cost: 147 - init_stock: 1450 - price: 164 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU226: - cost: 185 - init_stock: 650 - price: 206 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU227: - cost: 431 - init_stock: 1200 - price: 479 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU228: - cost: 12 - init_stock: 4500 - price: 14 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU229: - cost: 424 - init_stock: 2200 - price: 472 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU23: - cost: 348 - init_stock: 2100 - price: 387 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU230: - cost: 216 - init_stock: 1150 - price: 241 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU231: - cost: 43 - init_stock: 4050 - price: 48 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU232: - cost: 201 - init_stock: 4800 - price: 224 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU233: - cost: 324 - init_stock: 3750 - price: 360 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU234: - cost: 258 - init_stock: 250 - price: 287 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU235: - cost: 21 - init_stock: 2850 - price: 24 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU236: - cost: 139 - init_stock: 2750 - price: 155 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU237: - cost: 389 - init_stock: 2250 - price: 433 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU238: - cost: 57 - init_stock: 3300 - price: 64 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU239: - cost: 92 - init_stock: 4900 - price: 103 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU24: - cost: 87 - init_stock: 2350 - price: 97 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU240: - cost: 335 - init_stock: 2350 - price: 373 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU241: - cost: 395 - init_stock: 3550 - price: 439 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU242: - cost: 15 - init_stock: 2200 - price: 17 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU243: - cost: 316 - init_stock: 700 - price: 352 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU244: - cost: 156 - init_stock: 4100 - price: 174 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU245: - cost: 363 - init_stock: 3300 - price: 404 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU246: - cost: 270 - init_stock: 1500 - price: 300 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU247: - cost: 347 - init_stock: 1750 - price: 386 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU248: - cost: 319 - init_stock: 1450 - price: 355 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU249: - cost: 320 - init_stock: 1250 - price: 356 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU25: - cost: 144 - init_stock: 250 - price: 161 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU250: - cost: 114 - init_stock: 2700 - price: 127 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU251: - cost: 309 - init_stock: 3050 - price: 344 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU252: - cost: 162 - init_stock: 4150 - price: 181 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU253: - cost: 403 - init_stock: 800 - price: 448 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU254: - cost: 435 - init_stock: 2300 - price: 484 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU255: - cost: 261 - init_stock: 3350 - price: 290 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU256: - cost: 81 - init_stock: 3600 - price: 91 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU257: - cost: 313 - init_stock: 2850 - price: 348 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU258: - cost: 440 - init_stock: 2150 - price: 489 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU259: - cost: 299 - init_stock: 3450 - price: 333 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU26: - cost: 206 - init_stock: 3150 - price: 229 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU260: - cost: 438 - init_stock: 2600 - price: 487 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU261: - cost: 331 - init_stock: 1100 - price: 368 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU262: - cost: 298 - init_stock: 3900 - price: 332 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU263: - cost: 170 - init_stock: 3700 - price: 189 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU264: - cost: 324 - init_stock: 3950 - price: 361 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU265: - cost: 257 - init_stock: 2950 - price: 286 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU266: - cost: 115 - init_stock: 2350 - price: 128 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU267: - cost: 69 - init_stock: 4000 - price: 77 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU268: - cost: 198 - init_stock: 4450 - price: 221 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU269: - cost: 113 - init_stock: 2200 - price: 126 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU27: - cost: 333 - init_stock: 2800 - price: 370 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU270: - cost: 163 - init_stock: 3700 - price: 182 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU271: - cost: 207 - init_stock: 900 - price: 230 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU272: - cost: 329 - init_stock: 850 - price: 366 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU273: - cost: 378 - init_stock: 900 - price: 421 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU274: - cost: 26 - init_stock: 3850 - price: 29 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU275: - cost: 45 - init_stock: 2400 - price: 50 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU276: - cost: 146 - init_stock: 2700 - price: 163 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU277: - cost: 404 - init_stock: 2050 - price: 449 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU278: - cost: 94 - init_stock: 600 - price: 105 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU279: - cost: 45 - init_stock: 1950 - price: 51 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU28: - cost: 187 - init_stock: 2350 - price: 208 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU280: - cost: 265 - init_stock: 1650 - price: 295 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU281: - cost: 355 - init_stock: 3750 - price: 395 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU282: - cost: 56 - init_stock: 2300 - price: 63 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU283: - cost: 352 - init_stock: 4600 - price: 392 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU284: - cost: 309 - init_stock: 3350 - price: 344 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU285: - cost: 119 - init_stock: 4550 - price: 133 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU286: - cost: 303 - init_stock: 1950 - price: 337 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU287: - cost: 337 - init_stock: 2800 - price: 375 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU288: - cost: 162 - init_stock: 1900 - price: 181 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU289: - cost: 60 - init_stock: 1550 - price: 67 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU29: - cost: 220 - init_stock: 2900 - price: 245 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU290: - cost: 394 - init_stock: 3350 - price: 438 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU291: - cost: 84 - init_stock: 3050 - price: 94 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU292: - cost: 167 - init_stock: 250 - price: 186 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU293: - cost: 145 - init_stock: 2750 - price: 162 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU294: - cost: 368 - init_stock: 450 - price: 409 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU295: - cost: 391 - init_stock: 4650 - price: 435 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU296: - cost: 333 - init_stock: 4600 - price: 370 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU297: - cost: 268 - init_stock: 1900 - price: 298 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU298: - cost: 257 - init_stock: 1750 - price: 286 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU299: - cost: 330 - init_stock: 2550 - price: 367 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU3: - cost: 364 - init_stock: 600 - price: 405 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU30: - cost: 323 - init_stock: 3450 - price: 359 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU300: - cost: 338 - init_stock: 2900 - price: 376 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU301: - cost: 389 - init_stock: 4150 - price: 433 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU302: - cost: 165 - init_stock: 550 - price: 184 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU303: - cost: 221 - init_stock: 4700 - price: 246 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU304: - cost: 377 - init_stock: 1150 - price: 419 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU305: - cost: 445 - init_stock: 5000 - price: 495 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU306: - cost: 431 - init_stock: 2100 - price: 479 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU307: - cost: 189 - init_stock: 3900 - price: 210 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU308: - cost: 187 - init_stock: 250 - price: 208 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU309: - cost: 90 - init_stock: 4600 - price: 101 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU31: - cost: 62 - init_stock: 2800 - price: 69 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU310: - cost: 210 - init_stock: 2200 - price: 234 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU311: - cost: 275 - init_stock: 4000 - price: 306 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU312: - cost: 261 - init_stock: 1250 - price: 291 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU313: - cost: 291 - init_stock: 1900 - price: 324 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU314: - cost: 363 - init_stock: 1450 - price: 404 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU315: - cost: 423 - init_stock: 4200 - price: 471 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU316: - cost: 181 - init_stock: 900 - price: 202 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU317: - cost: 194 - init_stock: 1200 - price: 216 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU318: - cost: 250 - init_stock: 4250 - price: 278 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU319: - cost: 231 - init_stock: 2650 - price: 257 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU32: - cost: 302 - init_stock: 1100 - price: 336 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU320: - cost: 176 - init_stock: 1950 - price: 196 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU321: - cost: 60 - init_stock: 800 - price: 67 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU322: - cost: 216 - init_stock: 5000 - price: 240 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU323: - cost: 59 - init_stock: 1950 - price: 66 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU324: - cost: 397 - init_stock: 4650 - price: 442 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU325: - cost: 407 - init_stock: 3450 - price: 453 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU326: - cost: 310 - init_stock: 1200 - price: 345 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU327: - cost: 411 - init_stock: 700 - price: 457 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU328: - cost: 351 - init_stock: 2250 - price: 390 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU329: - cost: 60 - init_stock: 2100 - price: 67 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU33: - cost: 374 - init_stock: 4600 - price: 416 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU330: - cost: 275 - init_stock: 1950 - price: 306 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU331: - cost: 124 - init_stock: 2050 - price: 138 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU332: - cost: 351 - init_stock: 4800 - price: 390 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU333: - cost: 436 - init_stock: 2650 - price: 485 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU334: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU335: - cost: 72 - init_stock: 4050 - price: 80 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU336: - cost: 266 - init_stock: 1400 - price: 296 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU337: - cost: 220 - init_stock: 1450 - price: 245 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU338: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU339: - cost: 238 - init_stock: 3150 - price: 265 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU34: - cost: 85 - init_stock: 650 - price: 95 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU340: - cost: 432 - init_stock: 4350 - price: 480 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU341: - cost: 415 - init_stock: 3500 - price: 462 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU342: - cost: 129 - init_stock: 450 - price: 144 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU343: - cost: 279 - init_stock: 750 - price: 310 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU344: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU345: - cost: 397 - init_stock: 4450 - price: 442 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU346: - cost: 315 - init_stock: 2100 - price: 350 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU347: - cost: 447 - init_stock: 4100 - price: 497 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU348: - cost: 132 - init_stock: 1000 - price: 147 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU349: - cost: 60 - init_stock: 3350 - price: 67 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU35: - cost: 240 - init_stock: 4300 - price: 267 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU350: - cost: 251 - init_stock: 4600 - price: 279 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU351: - cost: 139 - init_stock: 3350 - price: 155 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU352: - cost: 63 - init_stock: 900 - price: 71 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU353: - cost: 227 - init_stock: 4650 - price: 253 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU354: - cost: 356 - init_stock: 3950 - price: 396 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU355: - cost: 56 - init_stock: 500 - price: 63 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU356: - cost: 313 - init_stock: 1450 - price: 348 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU357: - cost: 449 - init_stock: 4600 - price: 499 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU358: - cost: 242 - init_stock: 3450 - price: 269 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU359: - cost: 73 - init_stock: 3750 - price: 82 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU36: - cost: 94 - init_stock: 500 - price: 105 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU360: - cost: 435 - init_stock: 1250 - price: 484 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU361: - cost: 146 - init_stock: 4500 - price: 163 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU362: - cost: 417 - init_stock: 4350 - price: 464 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU363: - cost: 46 - init_stock: 3900 - price: 52 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU364: - cost: 348 - init_stock: 1650 - price: 387 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU365: - cost: 142 - init_stock: 4150 - price: 158 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU366: - cost: 399 - init_stock: 4300 - price: 444 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU367: - cost: 244 - init_stock: 2300 - price: 272 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU368: - cost: 424 - init_stock: 1650 - price: 472 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU369: - cost: 86 - init_stock: 3750 - price: 96 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU37: - cost: 59 - init_stock: 2950 - price: 66 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU370: - cost: 100 - init_stock: 750 - price: 112 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU371: - cost: 295 - init_stock: 250 - price: 328 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU372: - cost: 60 - init_stock: 1600 - price: 67 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU373: - cost: 52 - init_stock: 3350 - price: 58 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU374: - cost: 34 - init_stock: 2700 - price: 38 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU375: - cost: 254 - init_stock: 3600 - price: 283 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU376: - cost: 140 - init_stock: 4100 - price: 156 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU377: - cost: 70 - init_stock: 3350 - price: 78 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU378: - cost: 381 - init_stock: 1750 - price: 424 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU379: - cost: 9 - init_stock: 2450 - price: 11 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU38: - cost: 309 - init_stock: 2150 - price: 344 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU380: - cost: 353 - init_stock: 2650 - price: 393 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU381: - cost: 428 - init_stock: 300 - price: 476 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU382: - cost: 112 - init_stock: 3550 - price: 125 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU383: - cost: 273 - init_stock: 4600 - price: 304 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU384: - cost: 288 - init_stock: 450 - price: 320 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU385: - cost: 108 - init_stock: 650 - price: 120 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU386: - cost: 265 - init_stock: 1550 - price: 295 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU387: - cost: 100 - init_stock: 4850 - price: 112 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU388: - cost: 364 - init_stock: 2200 - price: 405 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU389: - cost: 338 - init_stock: 3500 - price: 376 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU39: - cost: 22 - init_stock: 2550 - price: 25 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU390: - cost: 129 - init_stock: 1950 - price: 144 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU391: - cost: 209 - init_stock: 3350 - price: 233 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU392: - cost: 146 - init_stock: 3700 - price: 163 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU393: - cost: 438 - init_stock: 3350 - price: 487 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU394: - cost: 138 - init_stock: 2650 - price: 154 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU395: - cost: 439 - init_stock: 1650 - price: 488 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU396: - cost: 299 - init_stock: 3050 - price: 333 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU397: - cost: 414 - init_stock: 2550 - price: 460 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU398: - cost: 210 - init_stock: 2900 - price: 234 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU399: - cost: 338 - init_stock: 1850 - price: 376 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU4: - cost: 395 - init_stock: 350 - price: 439 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU40: - cost: 441 - init_stock: 4950 - price: 490 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU400: - cost: 400 - init_stock: 4200 - price: 445 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU401: - cost: 369 - init_stock: 3900 - price: 410 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU402: - cost: 77 - init_stock: 350 - price: 86 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU403: - cost: 80 - init_stock: 4950 - price: 89 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU404: - cost: 258 - init_stock: 3050 - price: 287 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU405: - cost: 414 - init_stock: 950 - price: 460 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU406: - cost: 294 - init_stock: 5000 - price: 327 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU407: - cost: 23 - init_stock: 2300 - price: 26 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU408: - cost: 399 - init_stock: 400 - price: 444 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU409: - cost: 231 - init_stock: 4550 - price: 257 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU41: - cost: 126 - init_stock: 3950 - price: 141 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU410: - cost: 63 - init_stock: 800 - price: 70 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU411: - cost: 189 - init_stock: 4750 - price: 210 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU412: - cost: 257 - init_stock: 3100 - price: 286 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU413: - cost: 362 - init_stock: 4150 - price: 403 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU414: - cost: 148 - init_stock: 4350 - price: 165 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU415: - cost: 261 - init_stock: 1150 - price: 291 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU416: - cost: 205 - init_stock: 450 - price: 228 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU417: - cost: 398 - init_stock: 3600 - price: 443 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU418: - cost: 412 - init_stock: 650 - price: 458 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU419: - cost: 272 - init_stock: 4450 - price: 303 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU42: - cost: 415 - init_stock: 1300 - price: 462 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU420: - cost: 241 - init_stock: 2100 - price: 268 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU421: - cost: 192 - init_stock: 2300 - price: 214 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU422: - cost: 46 - init_stock: 2700 - price: 52 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU423: - cost: 169 - init_stock: 3300 - price: 188 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU424: - cost: 170 - init_stock: 550 - price: 189 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU425: - cost: 145 - init_stock: 600 - price: 162 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU426: - cost: 112 - init_stock: 4900 - price: 125 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU427: - cost: 371 - init_stock: 4700 - price: 413 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU428: - cost: 351 - init_stock: 3150 - price: 391 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU429: - cost: 35 - init_stock: 2050 - price: 39 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU43: - cost: 195 - init_stock: 3400 - price: 217 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU430: - cost: 194 - init_stock: 3650 - price: 216 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU431: - cost: 295 - init_stock: 1050 - price: 328 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU432: - cost: 22 - init_stock: 2300 - price: 25 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU433: - cost: 313 - init_stock: 2250 - price: 348 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU434: - cost: 33 - init_stock: 1500 - price: 37 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU435: - cost: 244 - init_stock: 1500 - price: 272 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU436: - cost: 64 - init_stock: 4050 - price: 72 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU437: - cost: 103 - init_stock: 700 - price: 115 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU438: - cost: 128 - init_stock: 500 - price: 143 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU439: - cost: 230 - init_stock: 1900 - price: 256 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU44: - cost: 324 - init_stock: 2400 - price: 360 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU440: - cost: 223 - init_stock: 4100 - price: 248 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU441: - cost: 227 - init_stock: 2800 - price: 253 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU442: - cost: 423 - init_stock: 4400 - price: 470 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU443: - cost: 196 - init_stock: 3650 - price: 218 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU444: - cost: 140 - init_stock: 300 - price: 156 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU445: - cost: 234 - init_stock: 4300 - price: 260 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU446: - cost: 447 - init_stock: 2450 - price: 497 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU447: - cost: 176 - init_stock: 250 - price: 196 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU448: - cost: 436 - init_stock: 3550 - price: 485 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU449: - cost: 253 - init_stock: 1900 - price: 282 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU45: - cost: 227 - init_stock: 500 - price: 253 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU450: - cost: 52 - init_stock: 4900 - price: 58 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU451: - cost: 421 - init_stock: 3450 - price: 468 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU452: - cost: 56 - init_stock: 3950 - price: 63 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU453: - cost: 33 - init_stock: 1750 - price: 37 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU454: - cost: 444 - init_stock: 4250 - price: 494 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU455: - cost: 122 - init_stock: 1300 - price: 136 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU456: - cost: 304 - init_stock: 1250 - price: 338 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU457: - cost: 152 - init_stock: 3200 - price: 169 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU458: - cost: 362 - init_stock: 350 - price: 403 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU459: - cost: 382 - init_stock: 1700 - price: 425 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU46: - cost: 269 - init_stock: 2500 - price: 299 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU460: - cost: 364 - init_stock: 3650 - price: 405 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU461: - cost: 225 - init_stock: 2400 - price: 251 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU462: - cost: 403 - init_stock: 450 - price: 448 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU463: - cost: 168 - init_stock: 4100 - price: 187 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU464: - cost: 251 - init_stock: 1700 - price: 279 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU465: - cost: 132 - init_stock: 5000 - price: 147 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU466: - cost: 87 - init_stock: 2100 - price: 97 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU467: - cost: 127 - init_stock: 1800 - price: 142 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU468: - cost: 49 - init_stock: 2500 - price: 55 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU469: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU47: - cost: 108 - init_stock: 1950 - price: 121 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU470: - cost: 335 - init_stock: 1600 - price: 373 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU471: - cost: 283 - init_stock: 3550 - price: 315 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU472: - cost: 378 - init_stock: 2950 - price: 421 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU473: - cost: 175 - init_stock: 1050 - price: 195 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU474: - cost: 403 - init_stock: 4300 - price: 448 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU475: - cost: 30 - init_stock: 4700 - price: 34 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU476: - cost: 225 - init_stock: 4500 - price: 251 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU477: - cost: 260 - init_stock: 2350 - price: 289 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU478: - cost: 431 - init_stock: 4400 - price: 479 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU479: - cost: 329 - init_stock: 1900 - price: 366 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU48: - cost: 306 - init_stock: 2900 - price: 340 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU480: - cost: 16 - init_stock: 1500 - price: 18 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU481: - cost: 297 - init_stock: 4400 - price: 330 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU482: - cost: 84 - init_stock: 4350 - price: 94 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU483: - cost: 268 - init_stock: 1700 - price: 298 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU484: - cost: 75 - init_stock: 3650 - price: 84 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU485: - cost: 286 - init_stock: 3350 - price: 318 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU486: - cost: 109 - init_stock: 2600 - price: 122 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU487: - cost: 249 - init_stock: 3300 - price: 277 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU488: - cost: 32 - init_stock: 1350 - price: 36 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU489: - cost: 53 - init_stock: 1550 - price: 59 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU49: - cost: 236 - init_stock: 4750 - price: 263 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU490: - cost: 144 - init_stock: 4050 - price: 161 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU491: - cost: 399 - init_stock: 3750 - price: 444 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU492: - cost: 47 - init_stock: 4000 - price: 53 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU493: - cost: 414 - init_stock: 1350 - price: 461 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU494: - cost: 130 - init_stock: 4700 - price: 145 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU495: - cost: 134 - init_stock: 3100 - price: 149 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU496: - cost: 307 - init_stock: 2950 - price: 342 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU497: - cost: 29 - init_stock: 450 - price: 33 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU498: - cost: 274 - init_stock: 3250 - price: 305 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU499: - cost: 276 - init_stock: 1450 - price: 307 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU5: - cost: 419 - init_stock: 3550 - price: 466 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU50: - cost: 253 - init_stock: 1850 - price: 282 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU500: - cost: 187 - init_stock: 2100 - price: 208 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU501: - cost: 174 - init_stock: 3800 - price: 194 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU502: - cost: 62 - init_stock: 3250 - price: 69 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU503: - cost: 187 - init_stock: 2850 - price: 208 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU504: - cost: 225 - init_stock: 1500 - price: 251 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU505: - cost: 383 - init_stock: 2950 - price: 426 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU506: - cost: 134 - init_stock: 4650 - price: 149 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU507: - cost: 168 - init_stock: 750 - price: 187 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU508: - cost: 244 - init_stock: 4800 - price: 272 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU509: - cost: 267 - init_stock: 4050 - price: 297 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU51: - cost: 265 - init_stock: 3350 - price: 295 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU510: - cost: 84 - init_stock: 450 - price: 94 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU511: - cost: 324 - init_stock: 3750 - price: 361 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU512: - cost: 420 - init_stock: 1100 - price: 467 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU513: - cost: 308 - init_stock: 350 - price: 343 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU514: - cost: 167 - init_stock: 3150 - price: 186 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU515: - cost: 130 - init_stock: 2700 - price: 145 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU516: - cost: 57 - init_stock: 1900 - price: 64 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU517: - cost: 105 - init_stock: 450 - price: 117 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU518: - cost: 23 - init_stock: 1050 - price: 26 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU519: - cost: 209 - init_stock: 2500 - price: 233 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU52: - cost: 19 - init_stock: 3350 - price: 22 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU520: - cost: 188 - init_stock: 1100 - price: 209 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU521: - cost: 198 - init_stock: 2500 - price: 220 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU522: - cost: 140 - init_stock: 4350 - price: 156 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU523: - cost: 58 - init_stock: 5000 - price: 65 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU524: - cost: 264 - init_stock: 4700 - price: 294 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU525: - cost: 388 - init_stock: 2100 - price: 432 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU526: - cost: 377 - init_stock: 1200 - price: 419 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU527: - cost: 117 - init_stock: 3500 - price: 131 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU528: - cost: 284 - init_stock: 1050 - price: 316 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU529: - cost: 268 - init_stock: 1550 - price: 298 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU53: - cost: 135 - init_stock: 3500 - price: 151 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU530: - cost: 117 - init_stock: 2250 - price: 131 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU531: - cost: 92 - init_stock: 3000 - price: 103 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU532: - cost: 315 - init_stock: 3850 - price: 351 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU533: - cost: 266 - init_stock: 2100 - price: 296 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU534: - cost: 382 - init_stock: 2050 - price: 425 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU535: - cost: 273 - init_stock: 4300 - price: 304 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU536: - cost: 322 - init_stock: 2600 - price: 358 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU537: - cost: 288 - init_stock: 450 - price: 321 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU538: - cost: 411 - init_stock: 2500 - price: 457 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU539: - cost: 148 - init_stock: 3900 - price: 165 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU54: - cost: 142 - init_stock: 2300 - price: 158 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU540: - cost: 349 - init_stock: 2100 - price: 388 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU541: - cost: 117 - init_stock: 1450 - price: 131 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU542: - cost: 34 - init_stock: 1300 - price: 38 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU543: - cost: 387 - init_stock: 4250 - price: 430 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU544: - cost: 311 - init_stock: 4850 - price: 346 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU545: - cost: 157 - init_stock: 750 - price: 175 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU546: - cost: 441 - init_stock: 4800 - price: 491 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU547: - cost: 144 - init_stock: 2200 - price: 161 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU548: - cost: 99 - init_stock: 300 - price: 111 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU549: - cost: 348 - init_stock: 3950 - price: 387 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU55: - cost: 433 - init_stock: 3250 - price: 482 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU550: - cost: 233 - init_stock: 4700 - price: 259 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU551: - cost: 41 - init_stock: 1550 - price: 46 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU552: - cost: 171 - init_stock: 4500 - price: 191 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU553: - cost: 187 - init_stock: 1500 - price: 208 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU554: - cost: 306 - init_stock: 2050 - price: 340 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU555: - cost: 440 - init_stock: 4200 - price: 489 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU556: - cost: 99 - init_stock: 1500 - price: 110 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU557: - cost: 300 - init_stock: 3650 - price: 334 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU558: - cost: 359 - init_stock: 850 - price: 399 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU559: - cost: 281 - init_stock: 2700 - price: 313 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU56: - cost: 339 - init_stock: 3700 - price: 377 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU560: - cost: 254 - init_stock: 2050 - price: 283 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU561: - cost: 181 - init_stock: 1950 - price: 202 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU562: - cost: 312 - init_stock: 4450 - price: 347 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU563: - cost: 360 - init_stock: 250 - price: 401 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU564: - cost: 98 - init_stock: 450 - price: 109 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU565: - cost: 51 - init_stock: 3750 - price: 57 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU566: - cost: 117 - init_stock: 550 - price: 131 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU567: - cost: 324 - init_stock: 450 - price: 361 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU568: - cost: 67 - init_stock: 3900 - price: 75 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU569: - cost: 425 - init_stock: 2400 - price: 473 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU57: - cost: 323 - init_stock: 2500 - price: 359 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU570: - cost: 349 - init_stock: 4150 - price: 388 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU571: - cost: 151 - init_stock: 1950 - price: 168 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU572: - cost: 23 - init_stock: 2250 - price: 26 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU573: - cost: 221 - init_stock: 2050 - price: 246 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU574: - cost: 84 - init_stock: 2500 - price: 94 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU575: - cost: 213 - init_stock: 3950 - price: 237 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU576: - cost: 238 - init_stock: 3900 - price: 265 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU577: - cost: 16 - init_stock: 4350 - price: 18 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU578: - cost: 90 - init_stock: 3550 - price: 100 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU579: - cost: 373 - init_stock: 450 - price: 415 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU58: - cost: 325 - init_stock: 2400 - price: 362 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU580: - cost: 182 - init_stock: 250 - price: 203 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU581: - cost: 136 - init_stock: 4300 - price: 152 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU582: - cost: 323 - init_stock: 4800 - price: 359 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU583: - cost: 77 - init_stock: 4350 - price: 86 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU584: - cost: 29 - init_stock: 2250 - price: 33 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU585: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU586: - cost: 54 - init_stock: 2200 - price: 61 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU587: - cost: 261 - init_stock: 3900 - price: 290 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU588: - cost: 428 - init_stock: 2200 - price: 476 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU589: - cost: 219 - init_stock: 1700 - price: 244 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU59: - cost: 130 - init_stock: 2550 - price: 145 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU590: - cost: 63 - init_stock: 1550 - price: 70 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU591: - cost: 185 - init_stock: 4200 - price: 206 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU592: - cost: 196 - init_stock: 2300 - price: 218 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU593: - cost: 111 - init_stock: 2200 - price: 124 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU594: - cost: 214 - init_stock: 2500 - price: 238 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU595: - cost: 65 - init_stock: 2150 - price: 73 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU596: - cost: 388 - init_stock: 350 - price: 432 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU597: - cost: 226 - init_stock: 4350 - price: 252 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU598: - cost: 313 - init_stock: 700 - price: 348 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU599: - cost: 48 - init_stock: 3400 - price: 54 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU6: - cost: 109 - init_stock: 4400 - price: 122 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU60: - cost: 180 - init_stock: 1950 - price: 200 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU600: - cost: 347 - init_stock: 3250 - price: 386 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU601: - cost: 124 - init_stock: 1950 - price: 138 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU602: - cost: 165 - init_stock: 4900 - price: 184 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU603: - cost: 250 - init_stock: 2100 - price: 278 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU604: - cost: 243 - init_stock: 2500 - price: 270 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU605: - cost: 259 - init_stock: 1200 - price: 288 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU606: - cost: 102 - init_stock: 550 - price: 114 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU607: - cost: 187 - init_stock: 3950 - price: 208 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU608: - cost: 35 - init_stock: 3650 - price: 39 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU609: - cost: 91 - init_stock: 950 - price: 102 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU61: - cost: 414 - init_stock: 3750 - price: 461 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU610: - cost: 404 - init_stock: 3400 - price: 449 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU611: - cost: 275 - init_stock: 3850 - price: 306 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU612: - cost: 351 - init_stock: 4400 - price: 391 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU613: - cost: 156 - init_stock: 350 - price: 174 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU614: - cost: 155 - init_stock: 750 - price: 173 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU615: - cost: 297 - init_stock: 4550 - price: 330 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU616: - cost: 208 - init_stock: 3650 - price: 232 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU617: - cost: 182 - init_stock: 3000 - price: 203 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU618: - cost: 69 - init_stock: 1150 - price: 77 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU619: - cost: 170 - init_stock: 4300 - price: 189 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU62: - cost: 111 - init_stock: 450 - price: 124 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU620: - cost: 207 - init_stock: 1950 - price: 231 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU621: - cost: 179 - init_stock: 3600 - price: 199 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU622: - cost: 227 - init_stock: 3250 - price: 253 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU623: - cost: 301 - init_stock: 2750 - price: 335 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU624: - cost: 433 - init_stock: 2700 - price: 482 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU625: - cost: 41 - init_stock: 4450 - price: 46 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU626: - cost: 405 - init_stock: 4450 - price: 451 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU627: - cost: 198 - init_stock: 4100 - price: 220 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU628: - cost: 111 - init_stock: 1300 - price: 124 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU629: - cost: 317 - init_stock: 4600 - price: 353 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU63: - cost: 61 - init_stock: 700 - price: 68 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU630: - cost: 109 - init_stock: 850 - price: 122 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU631: - cost: 266 - init_stock: 2450 - price: 296 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU632: - cost: 76 - init_stock: 4700 - price: 85 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU633: - cost: 165 - init_stock: 3350 - price: 184 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU634: - cost: 15 - init_stock: 3650 - price: 17 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU635: - cost: 306 - init_stock: 4650 - price: 341 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU636: - cost: 346 - init_stock: 1850 - price: 385 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU637: - cost: 74 - init_stock: 3050 - price: 83 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU638: - cost: 337 - init_stock: 400 - price: 375 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU639: - cost: 294 - init_stock: 2000 - price: 327 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU64: - cost: 32 - init_stock: 950 - price: 36 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU640: - cost: 247 - init_stock: 4550 - price: 275 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU641: - cost: 328 - init_stock: 4050 - price: 365 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU642: - cost: 372 - init_stock: 1250 - price: 414 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU643: - cost: 322 - init_stock: 2300 - price: 358 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU644: - cost: 41 - init_stock: 4100 - price: 46 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU645: - cost: 420 - init_stock: 4950 - price: 467 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU646: - cost: 170 - init_stock: 650 - price: 189 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU647: - cost: 272 - init_stock: 3850 - price: 303 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU648: - cost: 203 - init_stock: 2750 - price: 226 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU649: - cost: 178 - init_stock: 1250 - price: 198 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU65: - cost: 13 - init_stock: 4700 - price: 15 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU650: - cost: 315 - init_stock: 2800 - price: 351 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU651: - cost: 342 - init_stock: 4800 - price: 380 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU652: - cost: 110 - init_stock: 2000 - price: 123 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU653: - cost: 431 - init_stock: 4800 - price: 479 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU654: - cost: 59 - init_stock: 400 - price: 66 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU655: - cost: 299 - init_stock: 2100 - price: 333 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU656: - cost: 47 - init_stock: 3600 - price: 53 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU657: - cost: 65 - init_stock: 4050 - price: 73 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU658: - cost: 63 - init_stock: 1800 - price: 70 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU659: - cost: 18 - init_stock: 2800 - price: 21 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU66: - cost: 128 - init_stock: 4500 - price: 143 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU660: - cost: 438 - init_stock: 4150 - price: 487 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU661: - cost: 309 - init_stock: 3700 - price: 344 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU662: - cost: 334 - init_stock: 1900 - price: 372 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU663: - cost: 376 - init_stock: 2000 - price: 418 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU664: - cost: 315 - init_stock: 4200 - price: 351 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU665: - cost: 443 - init_stock: 2150 - price: 493 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU666: - cost: 306 - init_stock: 4400 - price: 341 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU667: - cost: 292 - init_stock: 650 - price: 325 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU668: - cost: 257 - init_stock: 3000 - price: 286 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU669: - cost: 66 - init_stock: 800 - price: 74 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU67: - cost: 222 - init_stock: 2650 - price: 247 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU670: - cost: 260 - init_stock: 4300 - price: 289 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU671: - cost: 240 - init_stock: 4150 - price: 267 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU672: - cost: 332 - init_stock: 650 - price: 369 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU673: - cost: 289 - init_stock: 2050 - price: 322 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU674: - cost: 200 - init_stock: 1100 - price: 223 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU675: - cost: 394 - init_stock: 1650 - price: 438 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU676: - cost: 219 - init_stock: 3050 - price: 244 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU677: - cost: 382 - init_stock: 2200 - price: 425 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU678: - cost: 151 - init_stock: 1800 - price: 168 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU679: - cost: 355 - init_stock: 2200 - price: 395 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU68: - cost: 152 - init_stock: 700 - price: 169 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU680: - cost: 234 - init_stock: 1500 - price: 260 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU681: - cost: 417 - init_stock: 4850 - price: 464 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU682: - cost: 333 - init_stock: 2650 - price: 370 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU683: - cost: 294 - init_stock: 3350 - price: 327 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU684: - cost: 319 - init_stock: 3950 - price: 355 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU685: - cost: 379 - init_stock: 2250 - price: 422 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU686: - cost: 56 - init_stock: 3150 - price: 63 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU687: - cost: 30 - init_stock: 3800 - price: 34 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU688: - cost: 435 - init_stock: 3850 - price: 484 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU689: - cost: 449 - init_stock: 750 - price: 499 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU69: - cost: 158 - init_stock: 3350 - price: 176 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU690: - cost: 393 - init_stock: 4150 - price: 437 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU691: - cost: 15 - init_stock: 3950 - price: 17 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU692: - cost: 202 - init_stock: 3250 - price: 225 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU693: - cost: 17 - init_stock: 3050 - price: 19 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU694: - cost: 187 - init_stock: 3750 - price: 208 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU695: - cost: 171 - init_stock: 350 - price: 190 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU696: - cost: 313 - init_stock: 2900 - price: 348 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU697: - cost: 409 - init_stock: 4000 - price: 455 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU698: - cost: 178 - init_stock: 3050 - price: 198 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU699: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU7: - cost: 90 - init_stock: 4800 - price: 101 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU70: - cost: 167 - init_stock: 1750 - price: 186 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU700: - cost: 66 - init_stock: 450 - price: 74 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU701: - cost: 359 - init_stock: 3850 - price: 399 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU702: - cost: 73 - init_stock: 1700 - price: 82 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU703: - cost: 326 - init_stock: 1900 - price: 363 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU704: - cost: 345 - init_stock: 4350 - price: 384 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU705: - cost: 115 - init_stock: 3150 - price: 128 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU706: - cost: 163 - init_stock: 4550 - price: 182 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU707: - cost: 16 - init_stock: 3450 - price: 18 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU708: - cost: 160 - init_stock: 1400 - price: 178 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU709: - cost: 91 - init_stock: 2650 - price: 102 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU71: - cost: 283 - init_stock: 2250 - price: 315 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU710: - cost: 226 - init_stock: 950 - price: 252 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU711: - cost: 252 - init_stock: 3450 - price: 281 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU712: - cost: 208 - init_stock: 550 - price: 232 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU713: - cost: 256 - init_stock: 2150 - price: 285 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU714: - cost: 71 - init_stock: 2050 - price: 79 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU715: - cost: 210 - init_stock: 850 - price: 234 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU716: - cost: 63 - init_stock: 3750 - price: 70 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU717: - cost: 310 - init_stock: 400 - price: 345 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU718: - cost: 321 - init_stock: 2900 - price: 357 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU719: - cost: 306 - init_stock: 3250 - price: 340 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU72: - cost: 412 - init_stock: 4250 - price: 458 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU720: - cost: 292 - init_stock: 2100 - price: 325 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU721: - cost: 65 - init_stock: 550 - price: 73 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU722: - cost: 352 - init_stock: 4850 - price: 392 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU723: - cost: 286 - init_stock: 4450 - price: 318 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU724: - cost: 360 - init_stock: 1650 - price: 400 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU725: - cost: 157 - init_stock: 1850 - price: 175 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU726: - cost: 412 - init_stock: 4450 - price: 458 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU727: - cost: 376 - init_stock: 2850 - price: 418 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU728: - cost: 427 - init_stock: 1850 - price: 475 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU729: - cost: 291 - init_stock: 1800 - price: 324 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU73: - cost: 190 - init_stock: 2700 - price: 212 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU730: - cost: 14 - init_stock: 650 - price: 16 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU731: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU732: - cost: 36 - init_stock: 3100 - price: 41 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU733: - cost: 283 - init_stock: 1300 - price: 315 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU734: - cost: 33 - init_stock: 2550 - price: 37 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU735: - cost: 239 - init_stock: 4950 - price: 266 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU736: - cost: 331 - init_stock: 1250 - price: 368 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU737: - cost: 427 - init_stock: 1550 - price: 475 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU738: - cost: 166 - init_stock: 2400 - price: 185 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU739: - cost: 427 - init_stock: 3700 - price: 475 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU74: - cost: 143 - init_stock: 2100 - price: 159 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU740: - cost: 351 - init_stock: 3200 - price: 390 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU741: - cost: 81 - init_stock: 2800 - price: 91 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU742: - cost: 169 - init_stock: 1100 - price: 188 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU743: - cost: 195 - init_stock: 2150 - price: 217 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU744: - cost: 341 - init_stock: 2900 - price: 379 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU745: - cost: 284 - init_stock: 4600 - price: 316 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU746: - cost: 393 - init_stock: 2000 - price: 437 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU747: - cost: 335 - init_stock: 3950 - price: 373 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU748: - cost: 247 - init_stock: 3300 - price: 275 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU749: - cost: 354 - init_stock: 2650 - price: 394 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU75: - cost: 117 - init_stock: 950 - price: 131 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU750: - cost: 230 - init_stock: 3100 - price: 256 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU751: - cost: 332 - init_stock: 3250 - price: 369 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU752: - cost: 233 - init_stock: 3900 - price: 259 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU753: - cost: 69 - init_stock: 3250 - price: 77 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU754: - cost: 348 - init_stock: 650 - price: 387 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU755: - cost: 318 - init_stock: 4000 - price: 354 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU756: - cost: 221 - init_stock: 4500 - price: 246 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU757: - cost: 36 - init_stock: 2850 - price: 40 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU758: - cost: 216 - init_stock: 2000 - price: 241 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU759: - cost: 391 - init_stock: 1900 - price: 435 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU76: - cost: 132 - init_stock: 3200 - price: 147 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU760: - cost: 158 - init_stock: 2550 - price: 176 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU761: - cost: 201 - init_stock: 2750 - price: 224 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU762: - cost: 237 - init_stock: 2550 - price: 264 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU763: - cost: 346 - init_stock: 3950 - price: 385 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU764: - cost: 314 - init_stock: 1700 - price: 349 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU765: - cost: 310 - init_stock: 650 - price: 345 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU766: - cost: 430 - init_stock: 1000 - price: 478 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU767: - cost: 85 - init_stock: 3800 - price: 95 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU768: - cost: 162 - init_stock: 4600 - price: 181 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU769: - cost: 21 - init_stock: 1450 - price: 24 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU77: - cost: 368 - init_stock: 3600 - price: 409 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU770: - cost: 135 - init_stock: 3100 - price: 150 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU771: - cost: 90 - init_stock: 350 - price: 101 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU772: - cost: 230 - init_stock: 300 - price: 256 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU773: - cost: 75 - init_stock: 4600 - price: 84 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU774: - cost: 402 - init_stock: 2950 - price: 447 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU775: - cost: 157 - init_stock: 4300 - price: 175 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU776: - cost: 92 - init_stock: 4250 - price: 103 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU777: - cost: 262 - init_stock: 2850 - price: 292 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU778: - cost: 182 - init_stock: 400 - price: 203 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU779: - cost: 105 - init_stock: 2150 - price: 117 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU78: - cost: 210 - init_stock: 650 - price: 234 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU780: - cost: 62 - init_stock: 4100 - price: 69 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU781: - cost: 334 - init_stock: 1800 - price: 372 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU782: - cost: 24 - init_stock: 500 - price: 27 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU783: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU784: - cost: 278 - init_stock: 2600 - price: 309 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU785: - cost: 171 - init_stock: 3500 - price: 191 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU786: - cost: 81 - init_stock: 1100 - price: 91 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU787: - cost: 324 - init_stock: 3050 - price: 360 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU788: - cost: 315 - init_stock: 1400 - price: 351 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU789: - cost: 137 - init_stock: 2900 - price: 153 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU79: - cost: 220 - init_stock: 550 - price: 245 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU790: - cost: 375 - init_stock: 3300 - price: 417 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU791: - cost: 120 - init_stock: 1400 - price: 134 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU792: - cost: 281 - init_stock: 1050 - price: 313 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU793: - cost: 175 - init_stock: 3800 - price: 195 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU794: - cost: 292 - init_stock: 4000 - price: 325 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU795: - cost: 248 - init_stock: 2400 - price: 276 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU796: - cost: 402 - init_stock: 2450 - price: 447 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU797: - cost: 90 - init_stock: 1950 - price: 100 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU798: - cost: 383 - init_stock: 1500 - price: 426 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU799: - cost: 56 - init_stock: 350 - price: 63 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU8: - cost: 112 - init_stock: 3150 - price: 125 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU80: - cost: 146 - init_stock: 3500 - price: 163 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU800: - cost: 268 - init_stock: 4700 - price: 298 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU801: - cost: 350 - init_stock: 1800 - price: 389 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU802: - cost: 155 - init_stock: 3100 - price: 173 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU803: - cost: 244 - init_stock: 950 - price: 272 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU804: - cost: 351 - init_stock: 2350 - price: 390 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU805: - cost: 54 - init_stock: 1650 - price: 61 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU806: - cost: 142 - init_stock: 2600 - price: 158 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU807: - cost: 407 - init_stock: 1300 - price: 453 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU808: - cost: 224 - init_stock: 4550 - price: 249 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU809: - cost: 49 - init_stock: 3250 - price: 55 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU81: - cost: 163 - init_stock: 3300 - price: 182 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU810: - cost: 248 - init_stock: 300 - price: 276 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU811: - cost: 228 - init_stock: 1850 - price: 254 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU812: - cost: 226 - init_stock: 4000 - price: 252 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU813: - cost: 227 - init_stock: 350 - price: 253 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU814: - cost: 436 - init_stock: 4850 - price: 485 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU815: - cost: 351 - init_stock: 1200 - price: 390 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU816: - cost: 136 - init_stock: 4550 - price: 152 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU817: - cost: 204 - init_stock: 250 - price: 227 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU818: - cost: 318 - init_stock: 2150 - price: 354 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU819: - cost: 271 - init_stock: 1350 - price: 302 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU82: - cost: 261 - init_stock: 1400 - price: 290 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU820: - cost: 237 - init_stock: 4100 - price: 264 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU821: - cost: 89 - init_stock: 3450 - price: 99 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU822: - cost: 122 - init_stock: 3500 - price: 136 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU823: - cost: 67 - init_stock: 1400 - price: 75 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU824: - cost: 153 - init_stock: 3500 - price: 170 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU825: - cost: 192 - init_stock: 750 - price: 214 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU826: - cost: 347 - init_stock: 2750 - price: 386 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU827: - cost: 133 - init_stock: 3200 - price: 148 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU828: - cost: 360 - init_stock: 3450 - price: 400 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU829: - cost: 54 - init_stock: 3700 - price: 61 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU83: - cost: 266 - init_stock: 850 - price: 296 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU830: - cost: 150 - init_stock: 1200 - price: 167 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU831: - cost: 235 - init_stock: 1450 - price: 262 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU832: - cost: 29 - init_stock: 4100 - price: 33 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU833: - cost: 360 - init_stock: 4900 - price: 400 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU834: - cost: 379 - init_stock: 250 - price: 422 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU835: - cost: 396 - init_stock: 2600 - price: 440 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU836: - cost: 290 - init_stock: 4800 - price: 323 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU837: - cost: 335 - init_stock: 1300 - price: 373 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU838: - cost: 410 - init_stock: 3850 - price: 456 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU839: - cost: 425 - init_stock: 3000 - price: 473 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU84: - cost: 286 - init_stock: 2100 - price: 318 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU840: - cost: 239 - init_stock: 2500 - price: 266 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU841: - cost: 256 - init_stock: 4250 - price: 285 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU842: - cost: 36 - init_stock: 4200 - price: 41 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU843: - cost: 324 - init_stock: 3600 - price: 360 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU844: - cost: 45 - init_stock: 3950 - price: 51 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU845: - cost: 259 - init_stock: 1100 - price: 288 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU846: - cost: 436 - init_stock: 1950 - price: 485 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU847: - cost: 349 - init_stock: 4550 - price: 388 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU848: - cost: 275 - init_stock: 2300 - price: 306 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU849: - cost: 288 - init_stock: 2000 - price: 320 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU85: - cost: 397 - init_stock: 5000 - price: 442 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU850: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU851: - cost: 47 - init_stock: 3200 - price: 53 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU852: - cost: 275 - init_stock: 2700 - price: 306 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU853: - cost: 347 - init_stock: 2800 - price: 386 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU854: - cost: 190 - init_stock: 3250 - price: 212 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU855: - cost: 68 - init_stock: 3600 - price: 76 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU856: - cost: 342 - init_stock: 1600 - price: 380 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU857: - cost: 415 - init_stock: 5000 - price: 462 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU858: - cost: 72 - init_stock: 1200 - price: 80 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU859: - cost: 193 - init_stock: 1400 - price: 215 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU86: - cost: 347 - init_stock: 4250 - price: 386 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU860: - cost: 281 - init_stock: 750 - price: 313 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU861: - cost: 429 - init_stock: 650 - price: 477 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU862: - cost: 216 - init_stock: 800 - price: 240 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU863: - cost: 423 - init_stock: 4650 - price: 470 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU864: - cost: 182 - init_stock: 950 - price: 203 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU865: - cost: 129 - init_stock: 950 - price: 144 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU866: - cost: 154 - init_stock: 4400 - price: 172 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU867: - cost: 449 - init_stock: 5000 - price: 499 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU868: - cost: 36 - init_stock: 2500 - price: 41 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU869: - cost: 87 - init_stock: 4850 - price: 97 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU87: - cost: 331 - init_stock: 3450 - price: 368 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU870: - cost: 252 - init_stock: 3350 - price: 281 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU871: - cost: 90 - init_stock: 4950 - price: 101 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU872: - cost: 119 - init_stock: 1600 - price: 133 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU873: - cost: 380 - init_stock: 1550 - price: 423 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU874: - cost: 422 - init_stock: 3000 - price: 469 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU875: - cost: 45 - init_stock: 1150 - price: 51 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU876: - cost: 272 - init_stock: 3050 - price: 303 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU877: - cost: 36 - init_stock: 1750 - price: 40 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU878: - cost: 24 - init_stock: 3400 - price: 27 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU879: - cost: 135 - init_stock: 5000 - price: 150 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU88: - cost: 423 - init_stock: 600 - price: 471 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU880: - cost: 389 - init_stock: 1550 - price: 433 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU881: - cost: 387 - init_stock: 3500 - price: 431 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU882: - cost: 174 - init_stock: 800 - price: 194 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU883: - cost: 255 - init_stock: 1900 - price: 284 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU884: - cost: 125 - init_stock: 1950 - price: 139 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU885: - cost: 44 - init_stock: 1350 - price: 49 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU886: - cost: 334 - init_stock: 3650 - price: 372 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU887: - cost: 239 - init_stock: 4350 - price: 266 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU888: - cost: 128 - init_stock: 450 - price: 143 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU889: - cost: 90 - init_stock: 1050 - price: 101 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU89: - cost: 124 - init_stock: 4650 - price: 138 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU890: - cost: 144 - init_stock: 5000 - price: 161 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU891: - cost: 320 - init_stock: 1100 - price: 356 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU892: - cost: 281 - init_stock: 4600 - price: 313 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU893: - cost: 206 - init_stock: 4950 - price: 229 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU894: - cost: 116 - init_stock: 3700 - price: 129 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU895: - cost: 207 - init_stock: 650 - price: 230 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU896: - cost: 260 - init_stock: 4000 - price: 289 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU897: - cost: 353 - init_stock: 3600 - price: 393 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU898: - cost: 429 - init_stock: 550 - price: 477 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU899: - cost: 209 - init_stock: 2400 - price: 233 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU9: - cost: 149 - init_stock: 4800 - price: 166 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU90: - cost: 408 - init_stock: 2550 - price: 454 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU900: - cost: 142 - init_stock: 2750 - price: 158 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU901: - cost: 193 - init_stock: 1450 - price: 215 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU902: - cost: 112 - init_stock: 4000 - price: 125 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU903: - cost: 321 - init_stock: 1900 - price: 357 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU904: - cost: 446 - init_stock: 2550 - price: 496 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU905: - cost: 224 - init_stock: 1550 - price: 249 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU906: - cost: 149 - init_stock: 2600 - price: 166 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU907: - cost: 19 - init_stock: 4150 - price: 22 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU908: - cost: 367 - init_stock: 3900 - price: 408 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU909: - cost: 433 - init_stock: 4800 - price: 482 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU91: - cost: 272 - init_stock: 800 - price: 303 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU910: - cost: 203 - init_stock: 1650 - price: 226 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU911: - cost: 414 - init_stock: 3500 - price: 461 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU912: - cost: 212 - init_stock: 1350 - price: 236 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU913: - cost: 289 - init_stock: 2300 - price: 322 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU914: - cost: 244 - init_stock: 3100 - price: 272 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU915: - cost: 303 - init_stock: 2800 - price: 337 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU916: - cost: 303 - init_stock: 4450 - price: 337 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU917: - cost: 232 - init_stock: 1500 - price: 258 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU918: - cost: 133 - init_stock: 2750 - price: 148 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU919: - cost: 353 - init_stock: 1250 - price: 393 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU92: - cost: 235 - init_stock: 3150 - price: 262 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU920: - cost: 321 - init_stock: 4300 - price: 357 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU921: - cost: 204 - init_stock: 3300 - price: 227 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU922: - cost: 100 - init_stock: 3350 - price: 112 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU923: - cost: 446 - init_stock: 2900 - price: 496 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU924: - cost: 284 - init_stock: 4250 - price: 316 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU925: - cost: 324 - init_stock: 750 - price: 360 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU926: - cost: 324 - init_stock: 3350 - price: 360 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU927: - cost: 234 - init_stock: 1050 - price: 260 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU928: - cost: 441 - init_stock: 4150 - price: 491 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU929: - cost: 323 - init_stock: 5000 - price: 359 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU93: - cost: 363 - init_stock: 3350 - price: 404 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU930: - cost: 178 - init_stock: 1400 - price: 198 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU931: - cost: 63 - init_stock: 700 - price: 71 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU932: - cost: 146 - init_stock: 3300 - price: 163 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU933: - cost: 101 - init_stock: 3900 - price: 113 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU934: - cost: 197 - init_stock: 3350 - price: 219 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU935: - cost: 327 - init_stock: 4700 - price: 364 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU936: - cost: 21 - init_stock: 250 - price: 24 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU937: - cost: 121 - init_stock: 850 - price: 135 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU938: - cost: 388 - init_stock: 1050 - price: 432 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU939: - cost: 155 - init_stock: 2950 - price: 173 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU94: - cost: 165 - init_stock: 2350 - price: 184 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU940: - cost: 12 - init_stock: 4650 - price: 14 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU941: - cost: 72 - init_stock: 2850 - price: 80 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU942: - cost: 181 - init_stock: 650 - price: 202 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU943: - cost: 124 - init_stock: 2450 - price: 138 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU944: - cost: 176 - init_stock: 2200 - price: 196 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU945: - cost: 126 - init_stock: 850 - price: 141 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU946: - cost: 292 - init_stock: 2450 - price: 325 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU947: - cost: 304 - init_stock: 4550 - price: 338 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU948: - cost: 382 - init_stock: 1400 - price: 425 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU949: - cost: 278 - init_stock: 250 - price: 309 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU95: - cost: 122 - init_stock: 1050 - price: 136 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU950: - cost: 91 - init_stock: 2700 - price: 102 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU951: - cost: 67 - init_stock: 900 - price: 75 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU952: - cost: 140 - init_stock: 550 - price: 156 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU953: - cost: 124 - init_stock: 1750 - price: 138 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU954: - cost: 266 - init_stock: 4300 - price: 296 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU955: - cost: 49 - init_stock: 3150 - price: 55 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU956: - cost: 253 - init_stock: 4250 - price: 282 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU957: - cost: 274 - init_stock: 2550 - price: 305 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU958: - cost: 332 - init_stock: 3300 - price: 369 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU959: - cost: 72 - init_stock: 4100 - price: 81 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU96: - cost: 158 - init_stock: 2200 - price: 176 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU960: - cost: 132 - init_stock: 300 - price: 147 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU961: - cost: 237 - init_stock: 2200 - price: 264 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU962: - cost: 318 - init_stock: 2200 - price: 354 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU963: - cost: 314 - init_stock: 1050 - price: 349 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU964: - cost: 219 - init_stock: 3650 - price: 244 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU965: - cost: 111 - init_stock: 2550 - price: 124 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU966: - cost: 271 - init_stock: 2200 - price: 302 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU967: - cost: 60 - init_stock: 2250 - price: 67 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU968: - cost: 252 - init_stock: 2450 - price: 281 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU969: - cost: 224 - init_stock: 300 - price: 249 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU97: - cost: 25 - init_stock: 1500 - price: 28 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU970: - cost: 219 - init_stock: 250 - price: 244 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU971: - cost: 331 - init_stock: 3650 - price: 368 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU972: - cost: 188 - init_stock: 3450 - price: 209 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU973: - cost: 243 - init_stock: 550 - price: 271 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU974: - cost: 153 - init_stock: 1600 - price: 170 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU975: - cost: 178 - init_stock: 1100 - price: 198 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU976: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU977: - cost: 210 - init_stock: 2700 - price: 234 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU978: - cost: 423 - init_stock: 3200 - price: 470 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU979: - cost: 398 - init_stock: 4800 - price: 443 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU98: - cost: 398 - init_stock: 1750 - price: 443 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU980: - cost: 35 - init_stock: 3400 - price: 39 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU981: - cost: 433 - init_stock: 3600 - price: 482 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU982: - cost: 191 - init_stock: 2600 - price: 213 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU983: - cost: 404 - init_stock: 4600 - price: 449 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU984: - cost: 208 - init_stock: 4550 - price: 232 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU985: - cost: 261 - init_stock: 2550 - price: 290 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU986: - cost: 247 - init_stock: 850 - price: 275 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU987: - cost: 390 - init_stock: 1700 - price: 434 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU988: - cost: 91 - init_stock: 1150 - price: 102 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU989: - cost: 435 - init_stock: 4650 - price: 484 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU99: - cost: 324 - init_stock: 2250 - price: 361 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU990: - cost: 97 - init_stock: 2850 - price: 108 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU991: - cost: 368 - init_stock: 950 - price: 409 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU992: - cost: 390 - init_stock: 4650 - price: 434 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU993: - cost: 130 - init_stock: 4300 - price: 145 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU994: - cost: 423 - init_stock: 2500 - price: 470 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU995: - cost: 216 - init_stock: 3800 - price: 241 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU996: - cost: 234 - init_stock: 3650 - price: 260 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU997: - cost: 360 - init_stock: 1700 - price: 400 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU998: - cost: 402 - init_stock: 2750 - price: 447 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU999: - cost: 71 - init_stock: 1150 - price: 79 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 500 - definition_ref: WarehouseFacility - name: WAREHOUSE0 - skus: - SKU0: - cost: 322 - init_stock: 1260 - price: 322 - service_level: 0.96 - vlt: 1 - SKU1: - cost: 284 - init_stock: 440 - price: 284 - service_level: 0.96 - vlt: 3 - SKU10: - cost: 68 - init_stock: 500 - price: 68 - service_level: 0.96 - vlt: 3 - SKU100: - cost: 16 - init_stock: 1300 - price: 16 - service_level: 0.96 - vlt: 3 - SKU101: - cost: 84 - init_stock: 1420 - price: 84 - service_level: 0.96 - vlt: 3 - SKU102: - cost: 328 - init_stock: 1860 - price: 328 - service_level: 0.96 - vlt: 2 - SKU103: - cost: 334 - init_stock: 1900 - price: 334 - service_level: 0.96 - vlt: 1 - SKU104: - cost: 49 - init_stock: 1300 - price: 49 - service_level: 0.96 - vlt: 3 - SKU105: - cost: 110 - init_stock: 1140 - price: 110 - service_level: 0.96 - vlt: 2 - SKU106: - cost: 251 - init_stock: 1460 - price: 251 - service_level: 0.96 - vlt: 3 - SKU107: - cost: 423 - init_stock: 1740 - price: 423 - service_level: 0.96 - vlt: 3 - SKU108: - cost: 458 - init_stock: 1840 - price: 458 - service_level: 0.96 - vlt: 3 - SKU109: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU11: - cost: 400 - init_stock: 1020 - price: 400 - service_level: 0.96 - vlt: 2 - SKU110: - cost: 66 - init_stock: 280 - price: 66 - service_level: 0.96 - vlt: 3 - SKU111: - cost: 260 - init_stock: 1220 - price: 260 - service_level: 0.96 - vlt: 1 - SKU112: - cost: 61 - init_stock: 1900 - price: 61 - service_level: 0.96 - vlt: 2 - SKU113: - cost: 348 - init_stock: 620 - price: 348 - service_level: 0.96 - vlt: 3 - SKU114: - cost: 389 - init_stock: 540 - price: 389 - service_level: 0.96 - vlt: 1 - SKU115: - cost: 286 - init_stock: 1720 - price: 286 - service_level: 0.96 - vlt: 2 - SKU116: - cost: 496 - init_stock: 1440 - price: 496 - service_level: 0.96 - vlt: 3 - SKU117: - cost: 320 - init_stock: 1840 - price: 320 - service_level: 0.96 - vlt: 3 - SKU118: - cost: 183 - init_stock: 660 - price: 183 - service_level: 0.96 - vlt: 1 - SKU119: - cost: 209 - init_stock: 640 - price: 209 - service_level: 0.96 - vlt: 3 - SKU12: - cost: 112 - init_stock: 1680 - price: 112 - service_level: 0.96 - vlt: 1 - SKU120: - cost: 121 - init_stock: 1960 - price: 121 - service_level: 0.96 - vlt: 1 - SKU121: - cost: 40 - init_stock: 1700 - price: 40 - service_level: 0.96 - vlt: 1 - SKU122: - cost: 437 - init_stock: 140 - price: 437 - service_level: 0.96 - vlt: 3 - SKU123: - cost: 233 - init_stock: 380 - price: 233 - service_level: 0.96 - vlt: 3 - SKU124: - cost: 182 - init_stock: 720 - price: 182 - service_level: 0.96 - vlt: 2 - SKU125: - cost: 16 - init_stock: 1840 - price: 16 - service_level: 0.96 - vlt: 2 - SKU126: - cost: 36 - init_stock: 780 - price: 36 - service_level: 0.96 - vlt: 3 - SKU127: - cost: 217 - init_stock: 620 - price: 217 - service_level: 0.96 - vlt: 2 - SKU128: - cost: 165 - init_stock: 380 - price: 165 - service_level: 0.96 - vlt: 1 - SKU129: - cost: 143 - init_stock: 1000 - price: 143 - service_level: 0.96 - vlt: 2 - SKU13: - cost: 317 - init_stock: 1140 - price: 317 - service_level: 0.96 - vlt: 3 - SKU130: - cost: 348 - init_stock: 1960 - price: 348 - service_level: 0.96 - vlt: 3 - SKU131: - cost: 64 - init_stock: 900 - price: 64 - service_level: 0.96 - vlt: 1 - SKU132: - cost: 427 - init_stock: 420 - price: 427 - service_level: 0.96 - vlt: 2 - SKU133: - cost: 224 - init_stock: 580 - price: 224 - service_level: 0.96 - vlt: 2 - SKU134: - cost: 336 - init_stock: 1540 - price: 336 - service_level: 0.96 - vlt: 3 - SKU135: - cost: 153 - init_stock: 2000 - price: 153 - service_level: 0.96 - vlt: 1 - SKU136: - cost: 199 - init_stock: 1420 - price: 199 - service_level: 0.96 - vlt: 3 - SKU137: - cost: 93 - init_stock: 1480 - price: 93 - service_level: 0.96 - vlt: 2 - SKU138: - cost: 228 - init_stock: 720 - price: 228 - service_level: 0.96 - vlt: 1 - SKU139: - cost: 207 - init_stock: 480 - price: 207 - service_level: 0.96 - vlt: 1 - SKU14: - cost: 268 - init_stock: 1240 - price: 268 - service_level: 0.96 - vlt: 1 - SKU140: - cost: 261 - init_stock: 680 - price: 261 - service_level: 0.96 - vlt: 3 - SKU141: - cost: 190 - init_stock: 820 - price: 190 - service_level: 0.96 - vlt: 1 - SKU142: - cost: 320 - init_stock: 760 - price: 320 - service_level: 0.96 - vlt: 3 - SKU143: - cost: 318 - init_stock: 520 - price: 318 - service_level: 0.96 - vlt: 3 - SKU144: - cost: 400 - init_stock: 240 - price: 400 - service_level: 0.96 - vlt: 1 - SKU145: - cost: 399 - init_stock: 1640 - price: 399 - service_level: 0.96 - vlt: 1 - SKU146: - cost: 177 - init_stock: 960 - price: 177 - service_level: 0.96 - vlt: 3 - SKU147: - cost: 472 - init_stock: 1120 - price: 472 - service_level: 0.96 - vlt: 3 - SKU148: - cost: 313 - init_stock: 1540 - price: 313 - service_level: 0.96 - vlt: 3 - SKU149: - cost: 357 - init_stock: 1540 - price: 357 - service_level: 0.96 - vlt: 3 - SKU15: - cost: 52 - init_stock: 100 - price: 52 - service_level: 0.96 - vlt: 2 - SKU150: - cost: 106 - init_stock: 1400 - price: 106 - service_level: 0.96 - vlt: 2 - SKU151: - cost: 223 - init_stock: 1460 - price: 223 - service_level: 0.96 - vlt: 1 - SKU152: - cost: 10 - init_stock: 1020 - price: 10 - service_level: 0.96 - vlt: 3 - SKU153: - cost: 441 - init_stock: 1240 - price: 441 - service_level: 0.96 - vlt: 1 - SKU154: - cost: 77 - init_stock: 1700 - price: 77 - service_level: 0.96 - vlt: 3 - SKU155: - cost: 422 - init_stock: 1060 - price: 422 - service_level: 0.96 - vlt: 1 - SKU156: - cost: 10 - init_stock: 240 - price: 10 - service_level: 0.96 - vlt: 1 - SKU157: - cost: 410 - init_stock: 1500 - price: 410 - service_level: 0.96 - vlt: 2 - SKU158: - cost: 145 - init_stock: 1620 - price: 145 - service_level: 0.96 - vlt: 3 - SKU159: - cost: 193 - init_stock: 500 - price: 193 - service_level: 0.96 - vlt: 2 - SKU16: - cost: 175 - init_stock: 1160 - price: 175 - service_level: 0.96 - vlt: 3 - SKU160: - cost: 459 - init_stock: 1700 - price: 459 - service_level: 0.96 - vlt: 1 - SKU161: - cost: 239 - init_stock: 920 - price: 239 - service_level: 0.96 - vlt: 2 - SKU162: - cost: 158 - init_stock: 100 - price: 158 - service_level: 0.96 - vlt: 2 - SKU163: - cost: 486 - init_stock: 780 - price: 486 - service_level: 0.96 - vlt: 2 - SKU164: - cost: 496 - init_stock: 2000 - price: 496 - service_level: 0.96 - vlt: 1 - SKU165: - cost: 274 - init_stock: 660 - price: 274 - service_level: 0.96 - vlt: 3 - SKU166: - cost: 79 - init_stock: 1780 - price: 79 - service_level: 0.96 - vlt: 1 - SKU167: - cost: 443 - init_stock: 260 - price: 443 - service_level: 0.96 - vlt: 1 - SKU168: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU169: - cost: 369 - init_stock: 1960 - price: 369 - service_level: 0.96 - vlt: 3 - SKU17: - cost: 346 - init_stock: 180 - price: 346 - service_level: 0.96 - vlt: 1 - SKU170: - cost: 68 - init_stock: 1100 - price: 68 - service_level: 0.96 - vlt: 2 - SKU171: - cost: 398 - init_stock: 1520 - price: 398 - service_level: 0.96 - vlt: 1 - SKU172: - cost: 200 - init_stock: 1420 - price: 200 - service_level: 0.96 - vlt: 2 - SKU173: - cost: 175 - init_stock: 1920 - price: 175 - service_level: 0.96 - vlt: 3 - SKU174: - cost: 291 - init_stock: 1520 - price: 291 - service_level: 0.96 - vlt: 2 - SKU175: - cost: 84 - init_stock: 1500 - price: 84 - service_level: 0.96 - vlt: 2 - SKU176: - cost: 407 - init_stock: 1320 - price: 407 - service_level: 0.96 - vlt: 1 - SKU177: - cost: 257 - init_stock: 620 - price: 257 - service_level: 0.96 - vlt: 1 - SKU178: - cost: 423 - init_stock: 100 - price: 423 - service_level: 0.96 - vlt: 2 - SKU179: - cost: 497 - init_stock: 1660 - price: 497 - service_level: 0.96 - vlt: 1 - SKU18: - cost: 258 - init_stock: 1620 - price: 258 - service_level: 0.96 - vlt: 1 - SKU180: - cost: 217 - init_stock: 1100 - price: 217 - service_level: 0.96 - vlt: 2 - SKU181: - cost: 143 - init_stock: 1200 - price: 143 - service_level: 0.96 - vlt: 1 - SKU182: - cost: 437 - init_stock: 1980 - price: 437 - service_level: 0.96 - vlt: 3 - SKU183: - cost: 145 - init_stock: 160 - price: 145 - service_level: 0.96 - vlt: 3 - SKU184: - cost: 73 - init_stock: 480 - price: 73 - service_level: 0.96 - vlt: 2 - SKU185: - cost: 10 - init_stock: 1800 - price: 10 - service_level: 0.96 - vlt: 2 - SKU186: - cost: 359 - init_stock: 440 - price: 359 - service_level: 0.96 - vlt: 1 - SKU187: - cost: 177 - init_stock: 600 - price: 177 - service_level: 0.96 - vlt: 3 - SKU188: - cost: 391 - init_stock: 1740 - price: 391 - service_level: 0.96 - vlt: 3 - SKU189: - cost: 358 - init_stock: 700 - price: 358 - service_level: 0.96 - vlt: 2 - SKU19: - cost: 477 - init_stock: 1820 - price: 477 - service_level: 0.96 - vlt: 3 - SKU190: - cost: 113 - init_stock: 340 - price: 113 - service_level: 0.96 - vlt: 3 - SKU191: - cost: 473 - init_stock: 1080 - price: 473 - service_level: 0.96 - vlt: 2 - SKU192: - cost: 415 - init_stock: 1220 - price: 415 - service_level: 0.96 - vlt: 2 - SKU193: - cost: 207 - init_stock: 600 - price: 207 - service_level: 0.96 - vlt: 2 - SKU194: - cost: 432 - init_stock: 100 - price: 432 - service_level: 0.96 - vlt: 2 - SKU195: - cost: 218 - init_stock: 620 - price: 218 - service_level: 0.96 - vlt: 2 - SKU196: - cost: 49 - init_stock: 1360 - price: 49 - service_level: 0.96 - vlt: 3 - SKU197: - cost: 303 - init_stock: 1140 - price: 303 - service_level: 0.96 - vlt: 1 - SKU198: - cost: 169 - init_stock: 1080 - price: 169 - service_level: 0.96 - vlt: 2 - SKU199: - cost: 449 - init_stock: 460 - price: 449 - service_level: 0.96 - vlt: 1 - SKU2: - cost: 331 - init_stock: 1400 - price: 331 - service_level: 0.96 - vlt: 3 - SKU20: - cost: 335 - init_stock: 500 - price: 335 - service_level: 0.96 - vlt: 3 - SKU200: - cost: 65 - init_stock: 500 - price: 65 - service_level: 0.96 - vlt: 1 - SKU201: - cost: 104 - init_stock: 1180 - price: 104 - service_level: 0.96 - vlt: 1 - SKU202: - cost: 142 - init_stock: 1460 - price: 142 - service_level: 0.96 - vlt: 1 - SKU203: - cost: 440 - init_stock: 1640 - price: 440 - service_level: 0.96 - vlt: 2 - SKU204: - cost: 489 - init_stock: 940 - price: 489 - service_level: 0.96 - vlt: 2 - SKU205: - cost: 130 - init_stock: 2000 - price: 130 - service_level: 0.96 - vlt: 3 - SKU206: - cost: 335 - init_stock: 220 - price: 335 - service_level: 0.96 - vlt: 2 - SKU207: - cost: 140 - init_stock: 1600 - price: 140 - service_level: 0.96 - vlt: 1 - SKU208: - cost: 491 - init_stock: 1540 - price: 491 - service_level: 0.96 - vlt: 1 - SKU209: - cost: 179 - init_stock: 400 - price: 179 - service_level: 0.96 - vlt: 3 - SKU21: - cost: 123 - init_stock: 2000 - price: 123 - service_level: 0.96 - vlt: 2 - SKU210: - cost: 404 - init_stock: 1380 - price: 404 - service_level: 0.96 - vlt: 3 - SKU211: - cost: 174 - init_stock: 1820 - price: 174 - service_level: 0.96 - vlt: 2 - SKU212: - cost: 405 - init_stock: 1580 - price: 405 - service_level: 0.96 - vlt: 3 - SKU213: - cost: 121 - init_stock: 1280 - price: 121 - service_level: 0.96 - vlt: 2 - SKU214: - cost: 101 - init_stock: 200 - price: 101 - service_level: 0.96 - vlt: 2 - SKU215: - cost: 419 - init_stock: 940 - price: 419 - service_level: 0.96 - vlt: 1 - SKU216: - cost: 330 - init_stock: 460 - price: 330 - service_level: 0.96 - vlt: 1 - SKU217: - cost: 284 - init_stock: 1300 - price: 284 - service_level: 0.96 - vlt: 2 - SKU218: - cost: 205 - init_stock: 1180 - price: 205 - service_level: 0.96 - vlt: 1 - SKU219: - cost: 92 - init_stock: 920 - price: 92 - service_level: 0.96 - vlt: 3 - SKU22: - cost: 493 - init_stock: 1320 - price: 493 - service_level: 0.96 - vlt: 1 - SKU220: - cost: 387 - init_stock: 1740 - price: 387 - service_level: 0.96 - vlt: 2 - SKU221: - cost: 39 - init_stock: 1560 - price: 39 - service_level: 0.96 - vlt: 2 - SKU222: - cost: 115 - init_stock: 720 - price: 115 - service_level: 0.96 - vlt: 2 - SKU223: - cost: 196 - init_stock: 240 - price: 196 - service_level: 0.96 - vlt: 2 - SKU224: - cost: 387 - init_stock: 100 - price: 387 - service_level: 0.96 - vlt: 2 - SKU225: - cost: 164 - init_stock: 580 - price: 164 - service_level: 0.96 - vlt: 1 - SKU226: - cost: 206 - init_stock: 260 - price: 206 - service_level: 0.96 - vlt: 2 - SKU227: - cost: 479 - init_stock: 480 - price: 479 - service_level: 0.96 - vlt: 3 - SKU228: - cost: 14 - init_stock: 1800 - price: 14 - service_level: 0.96 - vlt: 1 - SKU229: - cost: 472 - init_stock: 880 - price: 472 - service_level: 0.96 - vlt: 1 - SKU23: - cost: 387 - init_stock: 840 - price: 387 - service_level: 0.96 - vlt: 1 - SKU230: - cost: 241 - init_stock: 460 - price: 241 - service_level: 0.96 - vlt: 3 - SKU231: - cost: 48 - init_stock: 1620 - price: 48 - service_level: 0.96 - vlt: 3 - SKU232: - cost: 224 - init_stock: 1920 - price: 224 - service_level: 0.96 - vlt: 1 - SKU233: - cost: 360 - init_stock: 1500 - price: 360 - service_level: 0.96 - vlt: 2 - SKU234: - cost: 287 - init_stock: 100 - price: 287 - service_level: 0.96 - vlt: 1 - SKU235: - cost: 24 - init_stock: 1140 - price: 24 - service_level: 0.96 - vlt: 3 - SKU236: - cost: 155 - init_stock: 1100 - price: 155 - service_level: 0.96 - vlt: 2 - SKU237: - cost: 433 - init_stock: 900 - price: 433 - service_level: 0.96 - vlt: 3 - SKU238: - cost: 64 - init_stock: 1320 - price: 64 - service_level: 0.96 - vlt: 3 - SKU239: - cost: 103 - init_stock: 1960 - price: 103 - service_level: 0.96 - vlt: 1 - SKU24: - cost: 97 - init_stock: 940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU240: - cost: 373 - init_stock: 940 - price: 373 - service_level: 0.96 - vlt: 2 - SKU241: - cost: 439 - init_stock: 1420 - price: 439 - service_level: 0.96 - vlt: 2 - SKU242: - cost: 17 - init_stock: 880 - price: 17 - service_level: 0.96 - vlt: 1 - SKU243: - cost: 352 - init_stock: 280 - price: 352 - service_level: 0.96 - vlt: 1 - SKU244: - cost: 174 - init_stock: 1640 - price: 174 - service_level: 0.96 - vlt: 2 - SKU245: - cost: 404 - init_stock: 1320 - price: 404 - service_level: 0.96 - vlt: 2 - SKU246: - cost: 300 - init_stock: 600 - price: 300 - service_level: 0.96 - vlt: 2 - SKU247: - cost: 386 - init_stock: 700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU248: - cost: 355 - init_stock: 580 - price: 355 - service_level: 0.96 - vlt: 3 - SKU249: - cost: 356 - init_stock: 500 - price: 356 - service_level: 0.96 - vlt: 1 - SKU25: - cost: 161 - init_stock: 100 - price: 161 - service_level: 0.96 - vlt: 2 - SKU250: - cost: 127 - init_stock: 1080 - price: 127 - service_level: 0.96 - vlt: 3 - SKU251: - cost: 344 - init_stock: 1220 - price: 344 - service_level: 0.96 - vlt: 1 - SKU252: - cost: 181 - init_stock: 1660 - price: 181 - service_level: 0.96 - vlt: 1 - SKU253: - cost: 448 - init_stock: 320 - price: 448 - service_level: 0.96 - vlt: 1 - SKU254: - cost: 484 - init_stock: 920 - price: 484 - service_level: 0.96 - vlt: 3 - SKU255: - cost: 290 - init_stock: 1340 - price: 290 - service_level: 0.96 - vlt: 2 - SKU256: - cost: 91 - init_stock: 1440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU257: - cost: 348 - init_stock: 1140 - price: 348 - service_level: 0.96 - vlt: 1 - SKU258: - cost: 489 - init_stock: 860 - price: 489 - service_level: 0.96 - vlt: 1 - SKU259: - cost: 333 - init_stock: 1380 - price: 333 - service_level: 0.96 - vlt: 1 - SKU26: - cost: 229 - init_stock: 1260 - price: 229 - service_level: 0.96 - vlt: 1 - SKU260: - cost: 487 - init_stock: 1040 - price: 487 - service_level: 0.96 - vlt: 3 - SKU261: - cost: 368 - init_stock: 440 - price: 368 - service_level: 0.96 - vlt: 1 - SKU262: - cost: 332 - init_stock: 1560 - price: 332 - service_level: 0.96 - vlt: 3 - SKU263: - cost: 189 - init_stock: 1480 - price: 189 - service_level: 0.96 - vlt: 3 - SKU264: - cost: 361 - init_stock: 1580 - price: 361 - service_level: 0.96 - vlt: 1 - SKU265: - cost: 286 - init_stock: 1180 - price: 286 - service_level: 0.96 - vlt: 3 - SKU266: - cost: 128 - init_stock: 940 - price: 128 - service_level: 0.96 - vlt: 2 - SKU267: - cost: 77 - init_stock: 1600 - price: 77 - service_level: 0.96 - vlt: 1 - SKU268: - cost: 221 - init_stock: 1780 - price: 221 - service_level: 0.96 - vlt: 2 - SKU269: - cost: 126 - init_stock: 880 - price: 126 - service_level: 0.96 - vlt: 2 - SKU27: - cost: 370 - init_stock: 1120 - price: 370 - service_level: 0.96 - vlt: 3 - SKU270: - cost: 182 - init_stock: 1480 - price: 182 - service_level: 0.96 - vlt: 3 - SKU271: - cost: 230 - init_stock: 360 - price: 230 - service_level: 0.96 - vlt: 1 - SKU272: - cost: 366 - init_stock: 340 - price: 366 - service_level: 0.96 - vlt: 2 - SKU273: - cost: 421 - init_stock: 360 - price: 421 - service_level: 0.96 - vlt: 2 - SKU274: - cost: 29 - init_stock: 1540 - price: 29 - service_level: 0.96 - vlt: 2 - SKU275: - cost: 50 - init_stock: 960 - price: 50 - service_level: 0.96 - vlt: 1 - SKU276: - cost: 163 - init_stock: 1080 - price: 163 - service_level: 0.96 - vlt: 3 - SKU277: - cost: 449 - init_stock: 820 - price: 449 - service_level: 0.96 - vlt: 1 - SKU278: - cost: 105 - init_stock: 240 - price: 105 - service_level: 0.96 - vlt: 3 - SKU279: - cost: 51 - init_stock: 780 - price: 51 - service_level: 0.96 - vlt: 2 - SKU28: - cost: 208 - init_stock: 940 - price: 208 - service_level: 0.96 - vlt: 2 - SKU280: - cost: 295 - init_stock: 660 - price: 295 - service_level: 0.96 - vlt: 2 - SKU281: - cost: 395 - init_stock: 1500 - price: 395 - service_level: 0.96 - vlt: 1 - SKU282: - cost: 63 - init_stock: 920 - price: 63 - service_level: 0.96 - vlt: 2 - SKU283: - cost: 392 - init_stock: 1840 - price: 392 - service_level: 0.96 - vlt: 3 - SKU284: - cost: 344 - init_stock: 1340 - price: 344 - service_level: 0.96 - vlt: 3 - SKU285: - cost: 133 - init_stock: 1820 - price: 133 - service_level: 0.96 - vlt: 2 - SKU286: - cost: 337 - init_stock: 780 - price: 337 - service_level: 0.96 - vlt: 1 - SKU287: - cost: 375 - init_stock: 1120 - price: 375 - service_level: 0.96 - vlt: 3 - SKU288: - cost: 181 - init_stock: 760 - price: 181 - service_level: 0.96 - vlt: 1 - SKU289: - cost: 67 - init_stock: 620 - price: 67 - service_level: 0.96 - vlt: 1 - SKU29: - cost: 245 - init_stock: 1160 - price: 245 - service_level: 0.96 - vlt: 1 - SKU290: - cost: 438 - init_stock: 1340 - price: 438 - service_level: 0.96 - vlt: 1 - SKU291: - cost: 94 - init_stock: 1220 - price: 94 - service_level: 0.96 - vlt: 1 - SKU292: - cost: 186 - init_stock: 100 - price: 186 - service_level: 0.96 - vlt: 1 - SKU293: - cost: 162 - init_stock: 1100 - price: 162 - service_level: 0.96 - vlt: 2 - SKU294: - cost: 409 - init_stock: 180 - price: 409 - service_level: 0.96 - vlt: 2 - SKU295: - cost: 435 - init_stock: 1860 - price: 435 - service_level: 0.96 - vlt: 3 - SKU296: - cost: 370 - init_stock: 1840 - price: 370 - service_level: 0.96 - vlt: 3 - SKU297: - cost: 298 - init_stock: 760 - price: 298 - service_level: 0.96 - vlt: 1 - SKU298: - cost: 286 - init_stock: 700 - price: 286 - service_level: 0.96 - vlt: 2 - SKU299: - cost: 367 - init_stock: 1020 - price: 367 - service_level: 0.96 - vlt: 3 - SKU3: - cost: 405 - init_stock: 240 - price: 405 - service_level: 0.96 - vlt: 3 - SKU30: - cost: 359 - init_stock: 1380 - price: 359 - service_level: 0.96 - vlt: 2 - SKU300: - cost: 376 - init_stock: 1160 - price: 376 - service_level: 0.96 - vlt: 1 - SKU301: - cost: 433 - init_stock: 1660 - price: 433 - service_level: 0.96 - vlt: 2 - SKU302: - cost: 184 - init_stock: 220 - price: 184 - service_level: 0.96 - vlt: 2 - SKU303: - cost: 246 - init_stock: 1880 - price: 246 - service_level: 0.96 - vlt: 1 - SKU304: - cost: 419 - init_stock: 460 - price: 419 - service_level: 0.96 - vlt: 3 - SKU305: - cost: 495 - init_stock: 2000 - price: 495 - service_level: 0.96 - vlt: 1 - SKU306: - cost: 479 - init_stock: 840 - price: 479 - service_level: 0.96 - vlt: 2 - SKU307: - cost: 210 - init_stock: 1560 - price: 210 - service_level: 0.96 - vlt: 1 - SKU308: - cost: 208 - init_stock: 100 - price: 208 - service_level: 0.96 - vlt: 2 - SKU309: - cost: 101 - init_stock: 1840 - price: 101 - service_level: 0.96 - vlt: 2 - SKU31: - cost: 69 - init_stock: 1120 - price: 69 - service_level: 0.96 - vlt: 3 - SKU310: - cost: 234 - init_stock: 880 - price: 234 - service_level: 0.96 - vlt: 3 - SKU311: - cost: 306 - init_stock: 1600 - price: 306 - service_level: 0.96 - vlt: 3 - SKU312: - cost: 291 - init_stock: 500 - price: 291 - service_level: 0.96 - vlt: 1 - SKU313: - cost: 324 - init_stock: 760 - price: 324 - service_level: 0.96 - vlt: 1 - SKU314: - cost: 404 - init_stock: 580 - price: 404 - service_level: 0.96 - vlt: 1 - SKU315: - cost: 471 - init_stock: 1680 - price: 471 - service_level: 0.96 - vlt: 2 - SKU316: - cost: 202 - init_stock: 360 - price: 202 - service_level: 0.96 - vlt: 1 - SKU317: - cost: 216 - init_stock: 480 - price: 216 - service_level: 0.96 - vlt: 2 - SKU318: - cost: 278 - init_stock: 1700 - price: 278 - service_level: 0.96 - vlt: 1 - SKU319: - cost: 257 - init_stock: 1060 - price: 257 - service_level: 0.96 - vlt: 3 - SKU32: - cost: 336 - init_stock: 440 - price: 336 - service_level: 0.96 - vlt: 1 - SKU320: - cost: 196 - init_stock: 780 - price: 196 - service_level: 0.96 - vlt: 3 - SKU321: - cost: 67 - init_stock: 320 - price: 67 - service_level: 0.96 - vlt: 3 - SKU322: - cost: 240 - init_stock: 2000 - price: 240 - service_level: 0.96 - vlt: 1 - SKU323: - cost: 66 - init_stock: 780 - price: 66 - service_level: 0.96 - vlt: 2 - SKU324: - cost: 442 - init_stock: 1860 - price: 442 - service_level: 0.96 - vlt: 3 - SKU325: - cost: 453 - init_stock: 1380 - price: 453 - service_level: 0.96 - vlt: 2 - SKU326: - cost: 345 - init_stock: 480 - price: 345 - service_level: 0.96 - vlt: 2 - SKU327: - cost: 457 - init_stock: 280 - price: 457 - service_level: 0.96 - vlt: 2 - SKU328: - cost: 390 - init_stock: 900 - price: 390 - service_level: 0.96 - vlt: 1 - SKU329: - cost: 67 - init_stock: 840 - price: 67 - service_level: 0.96 - vlt: 1 - SKU33: - cost: 416 - init_stock: 1840 - price: 416 - service_level: 0.96 - vlt: 2 - SKU330: - cost: 306 - init_stock: 780 - price: 306 - service_level: 0.96 - vlt: 2 - SKU331: - cost: 138 - init_stock: 820 - price: 138 - service_level: 0.96 - vlt: 3 - SKU332: - cost: 390 - init_stock: 1920 - price: 390 - service_level: 0.96 - vlt: 2 - SKU333: - cost: 485 - init_stock: 1060 - price: 485 - service_level: 0.96 - vlt: 2 - SKU334: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 1 - SKU335: - cost: 80 - init_stock: 1620 - price: 80 - service_level: 0.96 - vlt: 1 - SKU336: - cost: 296 - init_stock: 560 - price: 296 - service_level: 0.96 - vlt: 1 - SKU337: - cost: 245 - init_stock: 580 - price: 245 - service_level: 0.96 - vlt: 2 - SKU338: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 3 - SKU339: - cost: 265 - init_stock: 1260 - price: 265 - service_level: 0.96 - vlt: 2 - SKU34: - cost: 95 - init_stock: 260 - price: 95 - service_level: 0.96 - vlt: 3 - SKU340: - cost: 480 - init_stock: 1740 - price: 480 - service_level: 0.96 - vlt: 2 - SKU341: - cost: 462 - init_stock: 1400 - price: 462 - service_level: 0.96 - vlt: 1 - SKU342: - cost: 144 - init_stock: 180 - price: 144 - service_level: 0.96 - vlt: 3 - SKU343: - cost: 310 - init_stock: 300 - price: 310 - service_level: 0.96 - vlt: 2 - SKU344: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU345: - cost: 442 - init_stock: 1780 - price: 442 - service_level: 0.96 - vlt: 2 - SKU346: - cost: 350 - init_stock: 840 - price: 350 - service_level: 0.96 - vlt: 3 - SKU347: - cost: 497 - init_stock: 1640 - price: 497 - service_level: 0.96 - vlt: 1 - SKU348: - cost: 147 - init_stock: 400 - price: 147 - service_level: 0.96 - vlt: 1 - SKU349: - cost: 67 - init_stock: 1340 - price: 67 - service_level: 0.96 - vlt: 3 - SKU35: - cost: 267 - init_stock: 1720 - price: 267 - service_level: 0.96 - vlt: 3 - SKU350: - cost: 279 - init_stock: 1840 - price: 279 - service_level: 0.96 - vlt: 3 - SKU351: - cost: 155 - init_stock: 1340 - price: 155 - service_level: 0.96 - vlt: 1 - SKU352: - cost: 71 - init_stock: 360 - price: 71 - service_level: 0.96 - vlt: 2 - SKU353: - cost: 253 - init_stock: 1860 - price: 253 - service_level: 0.96 - vlt: 2 - SKU354: - cost: 396 - init_stock: 1580 - price: 396 - service_level: 0.96 - vlt: 3 - SKU355: - cost: 63 - init_stock: 200 - price: 63 - service_level: 0.96 - vlt: 1 - SKU356: - cost: 348 - init_stock: 580 - price: 348 - service_level: 0.96 - vlt: 1 - SKU357: - cost: 499 - init_stock: 1840 - price: 499 - service_level: 0.96 - vlt: 3 - SKU358: - cost: 269 - init_stock: 1380 - price: 269 - service_level: 0.96 - vlt: 2 - SKU359: - cost: 82 - init_stock: 1500 - price: 82 - service_level: 0.96 - vlt: 3 - SKU36: - cost: 105 - init_stock: 200 - price: 105 - service_level: 0.96 - vlt: 2 - SKU360: - cost: 484 - init_stock: 500 - price: 484 - service_level: 0.96 - vlt: 1 - SKU361: - cost: 163 - init_stock: 1800 - price: 163 - service_level: 0.96 - vlt: 1 - SKU362: - cost: 464 - init_stock: 1740 - price: 464 - service_level: 0.96 - vlt: 2 - SKU363: - cost: 52 - init_stock: 1560 - price: 52 - service_level: 0.96 - vlt: 2 - SKU364: - cost: 387 - init_stock: 660 - price: 387 - service_level: 0.96 - vlt: 1 - SKU365: - cost: 158 - init_stock: 1660 - price: 158 - service_level: 0.96 - vlt: 3 - SKU366: - cost: 444 - init_stock: 1720 - price: 444 - service_level: 0.96 - vlt: 3 - SKU367: - cost: 272 - init_stock: 920 - price: 272 - service_level: 0.96 - vlt: 3 - SKU368: - cost: 472 - init_stock: 660 - price: 472 - service_level: 0.96 - vlt: 1 - SKU369: - cost: 96 - init_stock: 1500 - price: 96 - service_level: 0.96 - vlt: 2 - SKU37: - cost: 66 - init_stock: 1180 - price: 66 - service_level: 0.96 - vlt: 2 - SKU370: - cost: 112 - init_stock: 300 - price: 112 - service_level: 0.96 - vlt: 2 - SKU371: - cost: 328 - init_stock: 100 - price: 328 - service_level: 0.96 - vlt: 3 - SKU372: - cost: 67 - init_stock: 640 - price: 67 - service_level: 0.96 - vlt: 1 - SKU373: - cost: 58 - init_stock: 1340 - price: 58 - service_level: 0.96 - vlt: 3 - SKU374: - cost: 38 - init_stock: 1080 - price: 38 - service_level: 0.96 - vlt: 2 - SKU375: - cost: 283 - init_stock: 1440 - price: 283 - service_level: 0.96 - vlt: 1 - SKU376: - cost: 156 - init_stock: 1640 - price: 156 - service_level: 0.96 - vlt: 1 - SKU377: - cost: 78 - init_stock: 1340 - price: 78 - service_level: 0.96 - vlt: 3 - SKU378: - cost: 424 - init_stock: 700 - price: 424 - service_level: 0.96 - vlt: 1 - SKU379: - cost: 11 - init_stock: 980 - price: 11 - service_level: 0.96 - vlt: 1 - SKU38: - cost: 344 - init_stock: 860 - price: 344 - service_level: 0.96 - vlt: 2 - SKU380: - cost: 393 - init_stock: 1060 - price: 393 - service_level: 0.96 - vlt: 1 - SKU381: - cost: 476 - init_stock: 120 - price: 476 - service_level: 0.96 - vlt: 1 - SKU382: - cost: 125 - init_stock: 1420 - price: 125 - service_level: 0.96 - vlt: 2 - SKU383: - cost: 304 - init_stock: 1840 - price: 304 - service_level: 0.96 - vlt: 3 - SKU384: - cost: 320 - init_stock: 180 - price: 320 - service_level: 0.96 - vlt: 2 - SKU385: - cost: 120 - init_stock: 260 - price: 120 - service_level: 0.96 - vlt: 2 - SKU386: - cost: 295 - init_stock: 620 - price: 295 - service_level: 0.96 - vlt: 1 - SKU387: - cost: 112 - init_stock: 1940 - price: 112 - service_level: 0.96 - vlt: 3 - SKU388: - cost: 405 - init_stock: 880 - price: 405 - service_level: 0.96 - vlt: 2 - SKU389: - cost: 376 - init_stock: 1400 - price: 376 - service_level: 0.96 - vlt: 2 - SKU39: - cost: 25 - init_stock: 1020 - price: 25 - service_level: 0.96 - vlt: 3 - SKU390: - cost: 144 - init_stock: 780 - price: 144 - service_level: 0.96 - vlt: 2 - SKU391: - cost: 233 - init_stock: 1340 - price: 233 - service_level: 0.96 - vlt: 2 - SKU392: - cost: 163 - init_stock: 1480 - price: 163 - service_level: 0.96 - vlt: 2 - SKU393: - cost: 487 - init_stock: 1340 - price: 487 - service_level: 0.96 - vlt: 1 - SKU394: - cost: 154 - init_stock: 1060 - price: 154 - service_level: 0.96 - vlt: 2 - SKU395: - cost: 488 - init_stock: 660 - price: 488 - service_level: 0.96 - vlt: 3 - SKU396: - cost: 333 - init_stock: 1220 - price: 333 - service_level: 0.96 - vlt: 3 - SKU397: - cost: 460 - init_stock: 1020 - price: 460 - service_level: 0.96 - vlt: 1 - SKU398: - cost: 234 - init_stock: 1160 - price: 234 - service_level: 0.96 - vlt: 2 - SKU399: - cost: 376 - init_stock: 740 - price: 376 - service_level: 0.96 - vlt: 2 - SKU4: - cost: 439 - init_stock: 140 - price: 439 - service_level: 0.96 - vlt: 2 - SKU40: - cost: 490 - init_stock: 1980 - price: 490 - service_level: 0.96 - vlt: 3 - SKU400: - cost: 445 - init_stock: 1680 - price: 445 - service_level: 0.96 - vlt: 2 - SKU401: - cost: 410 - init_stock: 1560 - price: 410 - service_level: 0.96 - vlt: 1 - SKU402: - cost: 86 - init_stock: 140 - price: 86 - service_level: 0.96 - vlt: 3 - SKU403: - cost: 89 - init_stock: 1980 - price: 89 - service_level: 0.96 - vlt: 3 - SKU404: - cost: 287 - init_stock: 1220 - price: 287 - service_level: 0.96 - vlt: 1 - SKU405: - cost: 460 - init_stock: 380 - price: 460 - service_level: 0.96 - vlt: 1 - SKU406: - cost: 327 - init_stock: 2000 - price: 327 - service_level: 0.96 - vlt: 1 - SKU407: - cost: 26 - init_stock: 920 - price: 26 - service_level: 0.96 - vlt: 2 - SKU408: - cost: 444 - init_stock: 160 - price: 444 - service_level: 0.96 - vlt: 2 - SKU409: - cost: 257 - init_stock: 1820 - price: 257 - service_level: 0.96 - vlt: 2 - SKU41: - cost: 141 - init_stock: 1580 - price: 141 - service_level: 0.96 - vlt: 2 - SKU410: - cost: 70 - init_stock: 320 - price: 70 - service_level: 0.96 - vlt: 1 - SKU411: - cost: 210 - init_stock: 1900 - price: 210 - service_level: 0.96 - vlt: 3 - SKU412: - cost: 286 - init_stock: 1240 - price: 286 - service_level: 0.96 - vlt: 2 - SKU413: - cost: 403 - init_stock: 1660 - price: 403 - service_level: 0.96 - vlt: 3 - SKU414: - cost: 165 - init_stock: 1740 - price: 165 - service_level: 0.96 - vlt: 1 - SKU415: - cost: 291 - init_stock: 460 - price: 291 - service_level: 0.96 - vlt: 3 - SKU416: - cost: 228 - init_stock: 180 - price: 228 - service_level: 0.96 - vlt: 3 - SKU417: - cost: 443 - init_stock: 1440 - price: 443 - service_level: 0.96 - vlt: 1 - SKU418: - cost: 458 - init_stock: 260 - price: 458 - service_level: 0.96 - vlt: 3 - SKU419: - cost: 303 - init_stock: 1780 - price: 303 - service_level: 0.96 - vlt: 3 - SKU42: - cost: 462 - init_stock: 520 - price: 462 - service_level: 0.96 - vlt: 3 - SKU420: - cost: 268 - init_stock: 840 - price: 268 - service_level: 0.96 - vlt: 1 - SKU421: - cost: 214 - init_stock: 920 - price: 214 - service_level: 0.96 - vlt: 2 - SKU422: - cost: 52 - init_stock: 1080 - price: 52 - service_level: 0.96 - vlt: 3 - SKU423: - cost: 188 - init_stock: 1320 - price: 188 - service_level: 0.96 - vlt: 1 - SKU424: - cost: 189 - init_stock: 220 - price: 189 - service_level: 0.96 - vlt: 2 - SKU425: - cost: 162 - init_stock: 240 - price: 162 - service_level: 0.96 - vlt: 3 - SKU426: - cost: 125 - init_stock: 1960 - price: 125 - service_level: 0.96 - vlt: 3 - SKU427: - cost: 413 - init_stock: 1880 - price: 413 - service_level: 0.96 - vlt: 1 - SKU428: - cost: 391 - init_stock: 1260 - price: 391 - service_level: 0.96 - vlt: 3 - SKU429: - cost: 39 - init_stock: 820 - price: 39 - service_level: 0.96 - vlt: 1 - SKU43: - cost: 217 - init_stock: 1360 - price: 217 - service_level: 0.96 - vlt: 1 - SKU430: - cost: 216 - init_stock: 1460 - price: 216 - service_level: 0.96 - vlt: 2 - SKU431: - cost: 328 - init_stock: 420 - price: 328 - service_level: 0.96 - vlt: 1 - SKU432: - cost: 25 - init_stock: 920 - price: 25 - service_level: 0.96 - vlt: 3 - SKU433: - cost: 348 - init_stock: 900 - price: 348 - service_level: 0.96 - vlt: 2 - SKU434: - cost: 37 - init_stock: 600 - price: 37 - service_level: 0.96 - vlt: 1 - SKU435: - cost: 272 - init_stock: 600 - price: 272 - service_level: 0.96 - vlt: 3 - SKU436: - cost: 72 - init_stock: 1620 - price: 72 - service_level: 0.96 - vlt: 1 - SKU437: - cost: 115 - init_stock: 280 - price: 115 - service_level: 0.96 - vlt: 2 - SKU438: - cost: 143 - init_stock: 200 - price: 143 - service_level: 0.96 - vlt: 3 - SKU439: - cost: 256 - init_stock: 760 - price: 256 - service_level: 0.96 - vlt: 1 - SKU44: - cost: 360 - init_stock: 960 - price: 360 - service_level: 0.96 - vlt: 1 - SKU440: - cost: 248 - init_stock: 1640 - price: 248 - service_level: 0.96 - vlt: 2 - SKU441: - cost: 253 - init_stock: 1120 - price: 253 - service_level: 0.96 - vlt: 2 - SKU442: - cost: 470 - init_stock: 1760 - price: 470 - service_level: 0.96 - vlt: 2 - SKU443: - cost: 218 - init_stock: 1460 - price: 218 - service_level: 0.96 - vlt: 1 - SKU444: - cost: 156 - init_stock: 120 - price: 156 - service_level: 0.96 - vlt: 2 - SKU445: - cost: 260 - init_stock: 1720 - price: 260 - service_level: 0.96 - vlt: 2 - SKU446: - cost: 497 - init_stock: 980 - price: 497 - service_level: 0.96 - vlt: 1 - SKU447: - cost: 196 - init_stock: 100 - price: 196 - service_level: 0.96 - vlt: 1 - SKU448: - cost: 485 - init_stock: 1420 - price: 485 - service_level: 0.96 - vlt: 3 - SKU449: - cost: 282 - init_stock: 760 - price: 282 - service_level: 0.96 - vlt: 2 - SKU45: - cost: 253 - init_stock: 200 - price: 253 - service_level: 0.96 - vlt: 1 - SKU450: - cost: 58 - init_stock: 1960 - price: 58 - service_level: 0.96 - vlt: 2 - SKU451: - cost: 468 - init_stock: 1380 - price: 468 - service_level: 0.96 - vlt: 3 - SKU452: - cost: 63 - init_stock: 1580 - price: 63 - service_level: 0.96 - vlt: 1 - SKU453: - cost: 37 - init_stock: 700 - price: 37 - service_level: 0.96 - vlt: 3 - SKU454: - cost: 494 - init_stock: 1700 - price: 494 - service_level: 0.96 - vlt: 1 - SKU455: - cost: 136 - init_stock: 520 - price: 136 - service_level: 0.96 - vlt: 2 - SKU456: - cost: 338 - init_stock: 500 - price: 338 - service_level: 0.96 - vlt: 2 - SKU457: - cost: 169 - init_stock: 1280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU458: - cost: 403 - init_stock: 140 - price: 403 - service_level: 0.96 - vlt: 3 - SKU459: - cost: 425 - init_stock: 680 - price: 425 - service_level: 0.96 - vlt: 3 - SKU46: - cost: 299 - init_stock: 1000 - price: 299 - service_level: 0.96 - vlt: 3 - SKU460: - cost: 405 - init_stock: 1460 - price: 405 - service_level: 0.96 - vlt: 2 - SKU461: - cost: 251 - init_stock: 960 - price: 251 - service_level: 0.96 - vlt: 1 - SKU462: - cost: 448 - init_stock: 180 - price: 448 - service_level: 0.96 - vlt: 1 - SKU463: - cost: 187 - init_stock: 1640 - price: 187 - service_level: 0.96 - vlt: 3 - SKU464: - cost: 279 - init_stock: 680 - price: 279 - service_level: 0.96 - vlt: 3 - SKU465: - cost: 147 - init_stock: 2000 - price: 147 - service_level: 0.96 - vlt: 2 - SKU466: - cost: 97 - init_stock: 840 - price: 97 - service_level: 0.96 - vlt: 1 - SKU467: - cost: 142 - init_stock: 720 - price: 142 - service_level: 0.96 - vlt: 2 - SKU468: - cost: 55 - init_stock: 1000 - price: 55 - service_level: 0.96 - vlt: 2 - SKU469: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 3 - SKU47: - cost: 121 - init_stock: 780 - price: 121 - service_level: 0.96 - vlt: 1 - SKU470: - cost: 373 - init_stock: 640 - price: 373 - service_level: 0.96 - vlt: 2 - SKU471: - cost: 315 - init_stock: 1420 - price: 315 - service_level: 0.96 - vlt: 3 - SKU472: - cost: 421 - init_stock: 1180 - price: 421 - service_level: 0.96 - vlt: 2 - SKU473: - cost: 195 - init_stock: 420 - price: 195 - service_level: 0.96 - vlt: 1 - SKU474: - cost: 448 - init_stock: 1720 - price: 448 - service_level: 0.96 - vlt: 2 - SKU475: - cost: 34 - init_stock: 1880 - price: 34 - service_level: 0.96 - vlt: 1 - SKU476: - cost: 251 - init_stock: 1800 - price: 251 - service_level: 0.96 - vlt: 1 - SKU477: - cost: 289 - init_stock: 940 - price: 289 - service_level: 0.96 - vlt: 2 - SKU478: - cost: 479 - init_stock: 1760 - price: 479 - service_level: 0.96 - vlt: 2 - SKU479: - cost: 366 - init_stock: 760 - price: 366 - service_level: 0.96 - vlt: 1 - SKU48: - cost: 340 - init_stock: 1160 - price: 340 - service_level: 0.96 - vlt: 2 - SKU480: - cost: 18 - init_stock: 600 - price: 18 - service_level: 0.96 - vlt: 2 - SKU481: - cost: 330 - init_stock: 1760 - price: 330 - service_level: 0.96 - vlt: 1 - SKU482: - cost: 94 - init_stock: 1740 - price: 94 - service_level: 0.96 - vlt: 3 - SKU483: - cost: 298 - init_stock: 680 - price: 298 - service_level: 0.96 - vlt: 2 - SKU484: - cost: 84 - init_stock: 1460 - price: 84 - service_level: 0.96 - vlt: 3 - SKU485: - cost: 318 - init_stock: 1340 - price: 318 - service_level: 0.96 - vlt: 3 - SKU486: - cost: 122 - init_stock: 1040 - price: 122 - service_level: 0.96 - vlt: 1 - SKU487: - cost: 277 - init_stock: 1320 - price: 277 - service_level: 0.96 - vlt: 3 - SKU488: - cost: 36 - init_stock: 540 - price: 36 - service_level: 0.96 - vlt: 3 - SKU489: - cost: 59 - init_stock: 620 - price: 59 - service_level: 0.96 - vlt: 3 - SKU49: - cost: 263 - init_stock: 1900 - price: 263 - service_level: 0.96 - vlt: 3 - SKU490: - cost: 161 - init_stock: 1620 - price: 161 - service_level: 0.96 - vlt: 2 - SKU491: - cost: 444 - init_stock: 1500 - price: 444 - service_level: 0.96 - vlt: 2 - SKU492: - cost: 53 - init_stock: 1600 - price: 53 - service_level: 0.96 - vlt: 2 - SKU493: - cost: 461 - init_stock: 540 - price: 461 - service_level: 0.96 - vlt: 1 - SKU494: - cost: 145 - init_stock: 1880 - price: 145 - service_level: 0.96 - vlt: 1 - SKU495: - cost: 149 - init_stock: 1240 - price: 149 - service_level: 0.96 - vlt: 3 - SKU496: - cost: 342 - init_stock: 1180 - price: 342 - service_level: 0.96 - vlt: 1 - SKU497: - cost: 33 - init_stock: 180 - price: 33 - service_level: 0.96 - vlt: 3 - SKU498: - cost: 305 - init_stock: 1300 - price: 305 - service_level: 0.96 - vlt: 3 - SKU499: - cost: 307 - init_stock: 580 - price: 307 - service_level: 0.96 - vlt: 2 - SKU5: - cost: 466 - init_stock: 1420 - price: 466 - service_level: 0.96 - vlt: 2 - SKU50: - cost: 282 - init_stock: 740 - price: 282 - service_level: 0.96 - vlt: 3 - SKU500: - cost: 208 - init_stock: 840 - price: 208 - service_level: 0.96 - vlt: 3 - SKU501: - cost: 194 - init_stock: 1520 - price: 194 - service_level: 0.96 - vlt: 1 - SKU502: - cost: 69 - init_stock: 1300 - price: 69 - service_level: 0.96 - vlt: 2 - SKU503: - cost: 208 - init_stock: 1140 - price: 208 - service_level: 0.96 - vlt: 1 - SKU504: - cost: 251 - init_stock: 600 - price: 251 - service_level: 0.96 - vlt: 2 - SKU505: - cost: 426 - init_stock: 1180 - price: 426 - service_level: 0.96 - vlt: 1 - SKU506: - cost: 149 - init_stock: 1860 - price: 149 - service_level: 0.96 - vlt: 3 - SKU507: - cost: 187 - init_stock: 300 - price: 187 - service_level: 0.96 - vlt: 1 - SKU508: - cost: 272 - init_stock: 1920 - price: 272 - service_level: 0.96 - vlt: 1 - SKU509: - cost: 297 - init_stock: 1620 - price: 297 - service_level: 0.96 - vlt: 1 - SKU51: - cost: 295 - init_stock: 1340 - price: 295 - service_level: 0.96 - vlt: 3 - SKU510: - cost: 94 - init_stock: 180 - price: 94 - service_level: 0.96 - vlt: 1 - SKU511: - cost: 361 - init_stock: 1500 - price: 361 - service_level: 0.96 - vlt: 1 - SKU512: - cost: 467 - init_stock: 440 - price: 467 - service_level: 0.96 - vlt: 2 - SKU513: - cost: 343 - init_stock: 140 - price: 343 - service_level: 0.96 - vlt: 3 - SKU514: - cost: 186 - init_stock: 1260 - price: 186 - service_level: 0.96 - vlt: 3 - SKU515: - cost: 145 - init_stock: 1080 - price: 145 - service_level: 0.96 - vlt: 3 - SKU516: - cost: 64 - init_stock: 760 - price: 64 - service_level: 0.96 - vlt: 1 - SKU517: - cost: 117 - init_stock: 180 - price: 117 - service_level: 0.96 - vlt: 3 - SKU518: - cost: 26 - init_stock: 420 - price: 26 - service_level: 0.96 - vlt: 3 - SKU519: - cost: 233 - init_stock: 1000 - price: 233 - service_level: 0.96 - vlt: 2 - SKU52: - cost: 22 - init_stock: 1340 - price: 22 - service_level: 0.96 - vlt: 1 - SKU520: - cost: 209 - init_stock: 440 - price: 209 - service_level: 0.96 - vlt: 1 - SKU521: - cost: 220 - init_stock: 1000 - price: 220 - service_level: 0.96 - vlt: 3 - SKU522: - cost: 156 - init_stock: 1740 - price: 156 - service_level: 0.96 - vlt: 1 - SKU523: - cost: 65 - init_stock: 2000 - price: 65 - service_level: 0.96 - vlt: 3 - SKU524: - cost: 294 - init_stock: 1880 - price: 294 - service_level: 0.96 - vlt: 1 - SKU525: - cost: 432 - init_stock: 840 - price: 432 - service_level: 0.96 - vlt: 2 - SKU526: - cost: 419 - init_stock: 480 - price: 419 - service_level: 0.96 - vlt: 3 - SKU527: - cost: 131 - init_stock: 1400 - price: 131 - service_level: 0.96 - vlt: 1 - SKU528: - cost: 316 - init_stock: 420 - price: 316 - service_level: 0.96 - vlt: 3 - SKU529: - cost: 298 - init_stock: 620 - price: 298 - service_level: 0.96 - vlt: 3 - SKU53: - cost: 151 - init_stock: 1400 - price: 151 - service_level: 0.96 - vlt: 2 - SKU530: - cost: 131 - init_stock: 900 - price: 131 - service_level: 0.96 - vlt: 1 - SKU531: - cost: 103 - init_stock: 1200 - price: 103 - service_level: 0.96 - vlt: 3 - SKU532: - cost: 351 - init_stock: 1540 - price: 351 - service_level: 0.96 - vlt: 3 - SKU533: - cost: 296 - init_stock: 840 - price: 296 - service_level: 0.96 - vlt: 3 - SKU534: - cost: 425 - init_stock: 820 - price: 425 - service_level: 0.96 - vlt: 1 - SKU535: - cost: 304 - init_stock: 1720 - price: 304 - service_level: 0.96 - vlt: 2 - SKU536: - cost: 358 - init_stock: 1040 - price: 358 - service_level: 0.96 - vlt: 2 - SKU537: - cost: 321 - init_stock: 180 - price: 321 - service_level: 0.96 - vlt: 3 - SKU538: - cost: 457 - init_stock: 1000 - price: 457 - service_level: 0.96 - vlt: 1 - SKU539: - cost: 165 - init_stock: 1560 - price: 165 - service_level: 0.96 - vlt: 2 - SKU54: - cost: 158 - init_stock: 920 - price: 158 - service_level: 0.96 - vlt: 1 - SKU540: - cost: 388 - init_stock: 840 - price: 388 - service_level: 0.96 - vlt: 3 - SKU541: - cost: 131 - init_stock: 580 - price: 131 - service_level: 0.96 - vlt: 2 - SKU542: - cost: 38 - init_stock: 520 - price: 38 - service_level: 0.96 - vlt: 3 - SKU543: - cost: 430 - init_stock: 1700 - price: 430 - service_level: 0.96 - vlt: 2 - SKU544: - cost: 346 - init_stock: 1940 - price: 346 - service_level: 0.96 - vlt: 1 - SKU545: - cost: 175 - init_stock: 300 - price: 175 - service_level: 0.96 - vlt: 3 - SKU546: - cost: 491 - init_stock: 1920 - price: 491 - service_level: 0.96 - vlt: 3 - SKU547: - cost: 161 - init_stock: 880 - price: 161 - service_level: 0.96 - vlt: 2 - SKU548: - cost: 111 - init_stock: 120 - price: 111 - service_level: 0.96 - vlt: 1 - SKU549: - cost: 387 - init_stock: 1580 - price: 387 - service_level: 0.96 - vlt: 1 - SKU55: - cost: 482 - init_stock: 1300 - price: 482 - service_level: 0.96 - vlt: 3 - SKU550: - cost: 259 - init_stock: 1880 - price: 259 - service_level: 0.96 - vlt: 1 - SKU551: - cost: 46 - init_stock: 620 - price: 46 - service_level: 0.96 - vlt: 2 - SKU552: - cost: 191 - init_stock: 1800 - price: 191 - service_level: 0.96 - vlt: 3 - SKU553: - cost: 208 - init_stock: 600 - price: 208 - service_level: 0.96 - vlt: 1 - SKU554: - cost: 340 - init_stock: 820 - price: 340 - service_level: 0.96 - vlt: 1 - SKU555: - cost: 489 - init_stock: 1680 - price: 489 - service_level: 0.96 - vlt: 1 - SKU556: - cost: 110 - init_stock: 600 - price: 110 - service_level: 0.96 - vlt: 2 - SKU557: - cost: 334 - init_stock: 1460 - price: 334 - service_level: 0.96 - vlt: 2 - SKU558: - cost: 399 - init_stock: 340 - price: 399 - service_level: 0.96 - vlt: 1 - SKU559: - cost: 313 - init_stock: 1080 - price: 313 - service_level: 0.96 - vlt: 1 - SKU56: - cost: 377 - init_stock: 1480 - price: 377 - service_level: 0.96 - vlt: 1 - SKU560: - cost: 283 - init_stock: 820 - price: 283 - service_level: 0.96 - vlt: 1 - SKU561: - cost: 202 - init_stock: 780 - price: 202 - service_level: 0.96 - vlt: 3 - SKU562: - cost: 347 - init_stock: 1780 - price: 347 - service_level: 0.96 - vlt: 2 - SKU563: - cost: 401 - init_stock: 100 - price: 401 - service_level: 0.96 - vlt: 3 - SKU564: - cost: 109 - init_stock: 180 - price: 109 - service_level: 0.96 - vlt: 2 - SKU565: - cost: 57 - init_stock: 1500 - price: 57 - service_level: 0.96 - vlt: 3 - SKU566: - cost: 131 - init_stock: 220 - price: 131 - service_level: 0.96 - vlt: 2 - SKU567: - cost: 361 - init_stock: 180 - price: 361 - service_level: 0.96 - vlt: 1 - SKU568: - cost: 75 - init_stock: 1560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU569: - cost: 473 - init_stock: 960 - price: 473 - service_level: 0.96 - vlt: 2 - SKU57: - cost: 359 - init_stock: 1000 - price: 359 - service_level: 0.96 - vlt: 2 - SKU570: - cost: 388 - init_stock: 1660 - price: 388 - service_level: 0.96 - vlt: 2 - SKU571: - cost: 168 - init_stock: 780 - price: 168 - service_level: 0.96 - vlt: 1 - SKU572: - cost: 26 - init_stock: 900 - price: 26 - service_level: 0.96 - vlt: 3 - SKU573: - cost: 246 - init_stock: 820 - price: 246 - service_level: 0.96 - vlt: 2 - SKU574: - cost: 94 - init_stock: 1000 - price: 94 - service_level: 0.96 - vlt: 2 - SKU575: - cost: 237 - init_stock: 1580 - price: 237 - service_level: 0.96 - vlt: 1 - SKU576: - cost: 265 - init_stock: 1560 - price: 265 - service_level: 0.96 - vlt: 3 - SKU577: - cost: 18 - init_stock: 1740 - price: 18 - service_level: 0.96 - vlt: 3 - SKU578: - cost: 100 - init_stock: 1420 - price: 100 - service_level: 0.96 - vlt: 2 - SKU579: - cost: 415 - init_stock: 180 - price: 415 - service_level: 0.96 - vlt: 1 - SKU58: - cost: 362 - init_stock: 960 - price: 362 - service_level: 0.96 - vlt: 2 - SKU580: - cost: 203 - init_stock: 100 - price: 203 - service_level: 0.96 - vlt: 1 - SKU581: - cost: 152 - init_stock: 1720 - price: 152 - service_level: 0.96 - vlt: 2 - SKU582: - cost: 359 - init_stock: 1920 - price: 359 - service_level: 0.96 - vlt: 2 - SKU583: - cost: 86 - init_stock: 1740 - price: 86 - service_level: 0.96 - vlt: 1 - SKU584: - cost: 33 - init_stock: 900 - price: 33 - service_level: 0.96 - vlt: 2 - SKU585: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 2 - SKU586: - cost: 61 - init_stock: 880 - price: 61 - service_level: 0.96 - vlt: 2 - SKU587: - cost: 290 - init_stock: 1560 - price: 290 - service_level: 0.96 - vlt: 2 - SKU588: - cost: 476 - init_stock: 880 - price: 476 - service_level: 0.96 - vlt: 2 - SKU589: - cost: 244 - init_stock: 680 - price: 244 - service_level: 0.96 - vlt: 3 - SKU59: - cost: 145 - init_stock: 1020 - price: 145 - service_level: 0.96 - vlt: 1 - SKU590: - cost: 70 - init_stock: 620 - price: 70 - service_level: 0.96 - vlt: 2 - SKU591: - cost: 206 - init_stock: 1680 - price: 206 - service_level: 0.96 - vlt: 2 - SKU592: - cost: 218 - init_stock: 920 - price: 218 - service_level: 0.96 - vlt: 1 - SKU593: - cost: 124 - init_stock: 880 - price: 124 - service_level: 0.96 - vlt: 1 - SKU594: - cost: 238 - init_stock: 1000 - price: 238 - service_level: 0.96 - vlt: 1 - SKU595: - cost: 73 - init_stock: 860 - price: 73 - service_level: 0.96 - vlt: 3 - SKU596: - cost: 432 - init_stock: 140 - price: 432 - service_level: 0.96 - vlt: 2 - SKU597: - cost: 252 - init_stock: 1740 - price: 252 - service_level: 0.96 - vlt: 2 - SKU598: - cost: 348 - init_stock: 280 - price: 348 - service_level: 0.96 - vlt: 1 - SKU599: - cost: 54 - init_stock: 1360 - price: 54 - service_level: 0.96 - vlt: 2 - SKU6: - cost: 122 - init_stock: 1760 - price: 122 - service_level: 0.96 - vlt: 2 - SKU60: - cost: 200 - init_stock: 780 - price: 200 - service_level: 0.96 - vlt: 1 - SKU600: - cost: 386 - init_stock: 1300 - price: 386 - service_level: 0.96 - vlt: 1 - SKU601: - cost: 138 - init_stock: 780 - price: 138 - service_level: 0.96 - vlt: 3 - SKU602: - cost: 184 - init_stock: 1960 - price: 184 - service_level: 0.96 - vlt: 1 - SKU603: - cost: 278 - init_stock: 840 - price: 278 - service_level: 0.96 - vlt: 3 - SKU604: - cost: 270 - init_stock: 1000 - price: 270 - service_level: 0.96 - vlt: 3 - SKU605: - cost: 288 - init_stock: 480 - price: 288 - service_level: 0.96 - vlt: 2 - SKU606: - cost: 114 - init_stock: 220 - price: 114 - service_level: 0.96 - vlt: 3 - SKU607: - cost: 208 - init_stock: 1580 - price: 208 - service_level: 0.96 - vlt: 2 - SKU608: - cost: 39 - init_stock: 1460 - price: 39 - service_level: 0.96 - vlt: 1 - SKU609: - cost: 102 - init_stock: 380 - price: 102 - service_level: 0.96 - vlt: 2 - SKU61: - cost: 461 - init_stock: 1500 - price: 461 - service_level: 0.96 - vlt: 1 - SKU610: - cost: 449 - init_stock: 1360 - price: 449 - service_level: 0.96 - vlt: 1 - SKU611: - cost: 306 - init_stock: 1540 - price: 306 - service_level: 0.96 - vlt: 3 - SKU612: - cost: 391 - init_stock: 1760 - price: 391 - service_level: 0.96 - vlt: 2 - SKU613: - cost: 174 - init_stock: 140 - price: 174 - service_level: 0.96 - vlt: 3 - SKU614: - cost: 173 - init_stock: 300 - price: 173 - service_level: 0.96 - vlt: 1 - SKU615: - cost: 330 - init_stock: 1820 - price: 330 - service_level: 0.96 - vlt: 2 - SKU616: - cost: 232 - init_stock: 1460 - price: 232 - service_level: 0.96 - vlt: 1 - SKU617: - cost: 203 - init_stock: 1200 - price: 203 - service_level: 0.96 - vlt: 3 - SKU618: - cost: 77 - init_stock: 460 - price: 77 - service_level: 0.96 - vlt: 1 - SKU619: - cost: 189 - init_stock: 1720 - price: 189 - service_level: 0.96 - vlt: 2 - SKU62: - cost: 124 - init_stock: 180 - price: 124 - service_level: 0.96 - vlt: 2 - SKU620: - cost: 231 - init_stock: 780 - price: 231 - service_level: 0.96 - vlt: 3 - SKU621: - cost: 199 - init_stock: 1440 - price: 199 - service_level: 0.96 - vlt: 2 - SKU622: - cost: 253 - init_stock: 1300 - price: 253 - service_level: 0.96 - vlt: 3 - SKU623: - cost: 335 - init_stock: 1100 - price: 335 - service_level: 0.96 - vlt: 1 - SKU624: - cost: 482 - init_stock: 1080 - price: 482 - service_level: 0.96 - vlt: 2 - SKU625: - cost: 46 - init_stock: 1780 - price: 46 - service_level: 0.96 - vlt: 1 - SKU626: - cost: 451 - init_stock: 1780 - price: 451 - service_level: 0.96 - vlt: 3 - SKU627: - cost: 220 - init_stock: 1640 - price: 220 - service_level: 0.96 - vlt: 3 - SKU628: - cost: 124 - init_stock: 520 - price: 124 - service_level: 0.96 - vlt: 2 - SKU629: - cost: 353 - init_stock: 1840 - price: 353 - service_level: 0.96 - vlt: 1 - SKU63: - cost: 68 - init_stock: 280 - price: 68 - service_level: 0.96 - vlt: 3 - SKU630: - cost: 122 - init_stock: 340 - price: 122 - service_level: 0.96 - vlt: 1 - SKU631: - cost: 296 - init_stock: 980 - price: 296 - service_level: 0.96 - vlt: 2 - SKU632: - cost: 85 - init_stock: 1880 - price: 85 - service_level: 0.96 - vlt: 2 - SKU633: - cost: 184 - init_stock: 1340 - price: 184 - service_level: 0.96 - vlt: 2 - SKU634: - cost: 17 - init_stock: 1460 - price: 17 - service_level: 0.96 - vlt: 1 - SKU635: - cost: 341 - init_stock: 1860 - price: 341 - service_level: 0.96 - vlt: 1 - SKU636: - cost: 385 - init_stock: 740 - price: 385 - service_level: 0.96 - vlt: 1 - SKU637: - cost: 83 - init_stock: 1220 - price: 83 - service_level: 0.96 - vlt: 3 - SKU638: - cost: 375 - init_stock: 160 - price: 375 - service_level: 0.96 - vlt: 1 - SKU639: - cost: 327 - init_stock: 800 - price: 327 - service_level: 0.96 - vlt: 2 - SKU64: - cost: 36 - init_stock: 380 - price: 36 - service_level: 0.96 - vlt: 3 - SKU640: - cost: 275 - init_stock: 1820 - price: 275 - service_level: 0.96 - vlt: 1 - SKU641: - cost: 365 - init_stock: 1620 - price: 365 - service_level: 0.96 - vlt: 1 - SKU642: - cost: 414 - init_stock: 500 - price: 414 - service_level: 0.96 - vlt: 2 - SKU643: - cost: 358 - init_stock: 920 - price: 358 - service_level: 0.96 - vlt: 2 - SKU644: - cost: 46 - init_stock: 1640 - price: 46 - service_level: 0.96 - vlt: 2 - SKU645: - cost: 467 - init_stock: 1980 - price: 467 - service_level: 0.96 - vlt: 3 - SKU646: - cost: 189 - init_stock: 260 - price: 189 - service_level: 0.96 - vlt: 3 - SKU647: - cost: 303 - init_stock: 1540 - price: 303 - service_level: 0.96 - vlt: 3 - SKU648: - cost: 226 - init_stock: 1100 - price: 226 - service_level: 0.96 - vlt: 1 - SKU649: - cost: 198 - init_stock: 500 - price: 198 - service_level: 0.96 - vlt: 3 - SKU65: - cost: 15 - init_stock: 1880 - price: 15 - service_level: 0.96 - vlt: 2 - SKU650: - cost: 351 - init_stock: 1120 - price: 351 - service_level: 0.96 - vlt: 2 - SKU651: - cost: 380 - init_stock: 1920 - price: 380 - service_level: 0.96 - vlt: 3 - SKU652: - cost: 123 - init_stock: 800 - price: 123 - service_level: 0.96 - vlt: 2 - SKU653: - cost: 479 - init_stock: 1920 - price: 479 - service_level: 0.96 - vlt: 2 - SKU654: - cost: 66 - init_stock: 160 - price: 66 - service_level: 0.96 - vlt: 3 - SKU655: - cost: 333 - init_stock: 840 - price: 333 - service_level: 0.96 - vlt: 1 - SKU656: - cost: 53 - init_stock: 1440 - price: 53 - service_level: 0.96 - vlt: 2 - SKU657: - cost: 73 - init_stock: 1620 - price: 73 - service_level: 0.96 - vlt: 2 - SKU658: - cost: 70 - init_stock: 720 - price: 70 - service_level: 0.96 - vlt: 3 - SKU659: - cost: 21 - init_stock: 1120 - price: 21 - service_level: 0.96 - vlt: 1 - SKU66: - cost: 143 - init_stock: 1800 - price: 143 - service_level: 0.96 - vlt: 1 - SKU660: - cost: 487 - init_stock: 1660 - price: 487 - service_level: 0.96 - vlt: 1 - SKU661: - cost: 344 - init_stock: 1480 - price: 344 - service_level: 0.96 - vlt: 2 - SKU662: - cost: 372 - init_stock: 760 - price: 372 - service_level: 0.96 - vlt: 3 - SKU663: - cost: 418 - init_stock: 800 - price: 418 - service_level: 0.96 - vlt: 3 - SKU664: - cost: 351 - init_stock: 1680 - price: 351 - service_level: 0.96 - vlt: 1 - SKU665: - cost: 493 - init_stock: 860 - price: 493 - service_level: 0.96 - vlt: 3 - SKU666: - cost: 341 - init_stock: 1760 - price: 341 - service_level: 0.96 - vlt: 1 - SKU667: - cost: 325 - init_stock: 260 - price: 325 - service_level: 0.96 - vlt: 3 - SKU668: - cost: 286 - init_stock: 1200 - price: 286 - service_level: 0.96 - vlt: 3 - SKU669: - cost: 74 - init_stock: 320 - price: 74 - service_level: 0.96 - vlt: 1 - SKU67: - cost: 247 - init_stock: 1060 - price: 247 - service_level: 0.96 - vlt: 1 - SKU670: - cost: 289 - init_stock: 1720 - price: 289 - service_level: 0.96 - vlt: 1 - SKU671: - cost: 267 - init_stock: 1660 - price: 267 - service_level: 0.96 - vlt: 3 - SKU672: - cost: 369 - init_stock: 260 - price: 369 - service_level: 0.96 - vlt: 2 - SKU673: - cost: 322 - init_stock: 820 - price: 322 - service_level: 0.96 - vlt: 2 - SKU674: - cost: 223 - init_stock: 440 - price: 223 - service_level: 0.96 - vlt: 1 - SKU675: - cost: 438 - init_stock: 660 - price: 438 - service_level: 0.96 - vlt: 1 - SKU676: - cost: 244 - init_stock: 1220 - price: 244 - service_level: 0.96 - vlt: 1 - SKU677: - cost: 425 - init_stock: 880 - price: 425 - service_level: 0.96 - vlt: 2 - SKU678: - cost: 168 - init_stock: 720 - price: 168 - service_level: 0.96 - vlt: 1 - SKU679: - cost: 395 - init_stock: 880 - price: 395 - service_level: 0.96 - vlt: 2 - SKU68: - cost: 169 - init_stock: 280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU680: - cost: 260 - init_stock: 600 - price: 260 - service_level: 0.96 - vlt: 1 - SKU681: - cost: 464 - init_stock: 1940 - price: 464 - service_level: 0.96 - vlt: 3 - SKU682: - cost: 370 - init_stock: 1060 - price: 370 - service_level: 0.96 - vlt: 2 - SKU683: - cost: 327 - init_stock: 1340 - price: 327 - service_level: 0.96 - vlt: 3 - SKU684: - cost: 355 - init_stock: 1580 - price: 355 - service_level: 0.96 - vlt: 1 - SKU685: - cost: 422 - init_stock: 900 - price: 422 - service_level: 0.96 - vlt: 2 - SKU686: - cost: 63 - init_stock: 1260 - price: 63 - service_level: 0.96 - vlt: 1 - SKU687: - cost: 34 - init_stock: 1520 - price: 34 - service_level: 0.96 - vlt: 2 - SKU688: - cost: 484 - init_stock: 1540 - price: 484 - service_level: 0.96 - vlt: 3 - SKU689: - cost: 499 - init_stock: 300 - price: 499 - service_level: 0.96 - vlt: 1 - SKU69: - cost: 176 - init_stock: 1340 - price: 176 - service_level: 0.96 - vlt: 1 - SKU690: - cost: 437 - init_stock: 1660 - price: 437 - service_level: 0.96 - vlt: 2 - SKU691: - cost: 17 - init_stock: 1580 - price: 17 - service_level: 0.96 - vlt: 3 - SKU692: - cost: 225 - init_stock: 1300 - price: 225 - service_level: 0.96 - vlt: 1 - SKU693: - cost: 19 - init_stock: 1220 - price: 19 - service_level: 0.96 - vlt: 2 - SKU694: - cost: 208 - init_stock: 1500 - price: 208 - service_level: 0.96 - vlt: 3 - SKU695: - cost: 190 - init_stock: 140 - price: 190 - service_level: 0.96 - vlt: 2 - SKU696: - cost: 348 - init_stock: 1160 - price: 348 - service_level: 0.96 - vlt: 1 - SKU697: - cost: 455 - init_stock: 1600 - price: 455 - service_level: 0.96 - vlt: 1 - SKU698: - cost: 198 - init_stock: 1220 - price: 198 - service_level: 0.96 - vlt: 3 - SKU699: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 3 - SKU7: - cost: 101 - init_stock: 1920 - price: 101 - service_level: 0.96 - vlt: 2 - SKU70: - cost: 186 - init_stock: 700 - price: 186 - service_level: 0.96 - vlt: 1 - SKU700: - cost: 74 - init_stock: 180 - price: 74 - service_level: 0.96 - vlt: 2 - SKU701: - cost: 399 - init_stock: 1540 - price: 399 - service_level: 0.96 - vlt: 1 - SKU702: - cost: 82 - init_stock: 680 - price: 82 - service_level: 0.96 - vlt: 1 - SKU703: - cost: 363 - init_stock: 760 - price: 363 - service_level: 0.96 - vlt: 1 - SKU704: - cost: 384 - init_stock: 1740 - price: 384 - service_level: 0.96 - vlt: 2 - SKU705: - cost: 128 - init_stock: 1260 - price: 128 - service_level: 0.96 - vlt: 2 - SKU706: - cost: 182 - init_stock: 1820 - price: 182 - service_level: 0.96 - vlt: 2 - SKU707: - cost: 18 - init_stock: 1380 - price: 18 - service_level: 0.96 - vlt: 1 - SKU708: - cost: 178 - init_stock: 560 - price: 178 - service_level: 0.96 - vlt: 3 - SKU709: - cost: 102 - init_stock: 1060 - price: 102 - service_level: 0.96 - vlt: 3 - SKU71: - cost: 315 - init_stock: 900 - price: 315 - service_level: 0.96 - vlt: 2 - SKU710: - cost: 252 - init_stock: 380 - price: 252 - service_level: 0.96 - vlt: 2 - SKU711: - cost: 281 - init_stock: 1380 - price: 281 - service_level: 0.96 - vlt: 1 - SKU712: - cost: 232 - init_stock: 220 - price: 232 - service_level: 0.96 - vlt: 2 - SKU713: - cost: 285 - init_stock: 860 - price: 285 - service_level: 0.96 - vlt: 2 - SKU714: - cost: 79 - init_stock: 820 - price: 79 - service_level: 0.96 - vlt: 3 - SKU715: - cost: 234 - init_stock: 340 - price: 234 - service_level: 0.96 - vlt: 1 - SKU716: - cost: 70 - init_stock: 1500 - price: 70 - service_level: 0.96 - vlt: 2 - SKU717: - cost: 345 - init_stock: 160 - price: 345 - service_level: 0.96 - vlt: 2 - SKU718: - cost: 357 - init_stock: 1160 - price: 357 - service_level: 0.96 - vlt: 2 - SKU719: - cost: 340 - init_stock: 1300 - price: 340 - service_level: 0.96 - vlt: 3 - SKU72: - cost: 458 - init_stock: 1700 - price: 458 - service_level: 0.96 - vlt: 2 - SKU720: - cost: 325 - init_stock: 840 - price: 325 - service_level: 0.96 - vlt: 3 - SKU721: - cost: 73 - init_stock: 220 - price: 73 - service_level: 0.96 - vlt: 2 - SKU722: - cost: 392 - init_stock: 1940 - price: 392 - service_level: 0.96 - vlt: 1 - SKU723: - cost: 318 - init_stock: 1780 - price: 318 - service_level: 0.96 - vlt: 3 - SKU724: - cost: 400 - init_stock: 660 - price: 400 - service_level: 0.96 - vlt: 1 - SKU725: - cost: 175 - init_stock: 740 - price: 175 - service_level: 0.96 - vlt: 1 - SKU726: - cost: 458 - init_stock: 1780 - price: 458 - service_level: 0.96 - vlt: 3 - SKU727: - cost: 418 - init_stock: 1140 - price: 418 - service_level: 0.96 - vlt: 1 - SKU728: - cost: 475 - init_stock: 740 - price: 475 - service_level: 0.96 - vlt: 3 - SKU729: - cost: 324 - init_stock: 720 - price: 324 - service_level: 0.96 - vlt: 3 - SKU73: - cost: 212 - init_stock: 1080 - price: 212 - service_level: 0.96 - vlt: 2 - SKU730: - cost: 16 - init_stock: 260 - price: 16 - service_level: 0.96 - vlt: 2 - SKU731: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU732: - cost: 41 - init_stock: 1240 - price: 41 - service_level: 0.96 - vlt: 3 - SKU733: - cost: 315 - init_stock: 520 - price: 315 - service_level: 0.96 - vlt: 3 - SKU734: - cost: 37 - init_stock: 1020 - price: 37 - service_level: 0.96 - vlt: 3 - SKU735: - cost: 266 - init_stock: 1980 - price: 266 - service_level: 0.96 - vlt: 3 - SKU736: - cost: 368 - init_stock: 500 - price: 368 - service_level: 0.96 - vlt: 2 - SKU737: - cost: 475 - init_stock: 620 - price: 475 - service_level: 0.96 - vlt: 2 - SKU738: - cost: 185 - init_stock: 960 - price: 185 - service_level: 0.96 - vlt: 3 - SKU739: - cost: 475 - init_stock: 1480 - price: 475 - service_level: 0.96 - vlt: 2 - SKU74: - cost: 159 - init_stock: 840 - price: 159 - service_level: 0.96 - vlt: 1 - SKU740: - cost: 390 - init_stock: 1280 - price: 390 - service_level: 0.96 - vlt: 1 - SKU741: - cost: 91 - init_stock: 1120 - price: 91 - service_level: 0.96 - vlt: 1 - SKU742: - cost: 188 - init_stock: 440 - price: 188 - service_level: 0.96 - vlt: 1 - SKU743: - cost: 217 - init_stock: 860 - price: 217 - service_level: 0.96 - vlt: 3 - SKU744: - cost: 379 - init_stock: 1160 - price: 379 - service_level: 0.96 - vlt: 1 - SKU745: - cost: 316 - init_stock: 1840 - price: 316 - service_level: 0.96 - vlt: 2 - SKU746: - cost: 437 - init_stock: 800 - price: 437 - service_level: 0.96 - vlt: 2 - SKU747: - cost: 373 - init_stock: 1580 - price: 373 - service_level: 0.96 - vlt: 2 - SKU748: - cost: 275 - init_stock: 1320 - price: 275 - service_level: 0.96 - vlt: 2 - SKU749: - cost: 394 - init_stock: 1060 - price: 394 - service_level: 0.96 - vlt: 1 - SKU75: - cost: 131 - init_stock: 380 - price: 131 - service_level: 0.96 - vlt: 1 - SKU750: - cost: 256 - init_stock: 1240 - price: 256 - service_level: 0.96 - vlt: 3 - SKU751: - cost: 369 - init_stock: 1300 - price: 369 - service_level: 0.96 - vlt: 3 - SKU752: - cost: 259 - init_stock: 1560 - price: 259 - service_level: 0.96 - vlt: 3 - SKU753: - cost: 77 - init_stock: 1300 - price: 77 - service_level: 0.96 - vlt: 3 - SKU754: - cost: 387 - init_stock: 260 - price: 387 - service_level: 0.96 - vlt: 2 - SKU755: - cost: 354 - init_stock: 1600 - price: 354 - service_level: 0.96 - vlt: 3 - SKU756: - cost: 246 - init_stock: 1800 - price: 246 - service_level: 0.96 - vlt: 1 - SKU757: - cost: 40 - init_stock: 1140 - price: 40 - service_level: 0.96 - vlt: 3 - SKU758: - cost: 241 - init_stock: 800 - price: 241 - service_level: 0.96 - vlt: 2 - SKU759: - cost: 435 - init_stock: 760 - price: 435 - service_level: 0.96 - vlt: 1 - SKU76: - cost: 147 - init_stock: 1280 - price: 147 - service_level: 0.96 - vlt: 2 - SKU760: - cost: 176 - init_stock: 1020 - price: 176 - service_level: 0.96 - vlt: 3 - SKU761: - cost: 224 - init_stock: 1100 - price: 224 - service_level: 0.96 - vlt: 1 - SKU762: - cost: 264 - init_stock: 1020 - price: 264 - service_level: 0.96 - vlt: 1 - SKU763: - cost: 385 - init_stock: 1580 - price: 385 - service_level: 0.96 - vlt: 2 - SKU764: - cost: 349 - init_stock: 680 - price: 349 - service_level: 0.96 - vlt: 1 - SKU765: - cost: 345 - init_stock: 260 - price: 345 - service_level: 0.96 - vlt: 1 - SKU766: - cost: 478 - init_stock: 400 - price: 478 - service_level: 0.96 - vlt: 2 - SKU767: - cost: 95 - init_stock: 1520 - price: 95 - service_level: 0.96 - vlt: 1 - SKU768: - cost: 181 - init_stock: 1840 - price: 181 - service_level: 0.96 - vlt: 2 - SKU769: - cost: 24 - init_stock: 580 - price: 24 - service_level: 0.96 - vlt: 2 - SKU77: - cost: 409 - init_stock: 1440 - price: 409 - service_level: 0.96 - vlt: 1 - SKU770: - cost: 150 - init_stock: 1240 - price: 150 - service_level: 0.96 - vlt: 2 - SKU771: - cost: 101 - init_stock: 140 - price: 101 - service_level: 0.96 - vlt: 1 - SKU772: - cost: 256 - init_stock: 120 - price: 256 - service_level: 0.96 - vlt: 3 - SKU773: - cost: 84 - init_stock: 1840 - price: 84 - service_level: 0.96 - vlt: 1 - SKU774: - cost: 447 - init_stock: 1180 - price: 447 - service_level: 0.96 - vlt: 1 - SKU775: - cost: 175 - init_stock: 1720 - price: 175 - service_level: 0.96 - vlt: 3 - SKU776: - cost: 103 - init_stock: 1700 - price: 103 - service_level: 0.96 - vlt: 2 - SKU777: - cost: 292 - init_stock: 1140 - price: 292 - service_level: 0.96 - vlt: 2 - SKU778: - cost: 203 - init_stock: 160 - price: 203 - service_level: 0.96 - vlt: 3 - SKU779: - cost: 117 - init_stock: 860 - price: 117 - service_level: 0.96 - vlt: 1 - SKU78: - cost: 234 - init_stock: 260 - price: 234 - service_level: 0.96 - vlt: 3 - SKU780: - cost: 69 - init_stock: 1640 - price: 69 - service_level: 0.96 - vlt: 3 - SKU781: - cost: 372 - init_stock: 720 - price: 372 - service_level: 0.96 - vlt: 3 - SKU782: - cost: 27 - init_stock: 200 - price: 27 - service_level: 0.96 - vlt: 3 - SKU783: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 2 - SKU784: - cost: 309 - init_stock: 1040 - price: 309 - service_level: 0.96 - vlt: 3 - SKU785: - cost: 191 - init_stock: 1400 - price: 191 - service_level: 0.96 - vlt: 2 - SKU786: - cost: 91 - init_stock: 440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU787: - cost: 360 - init_stock: 1220 - price: 360 - service_level: 0.96 - vlt: 1 - SKU788: - cost: 351 - init_stock: 560 - price: 351 - service_level: 0.96 - vlt: 1 - SKU789: - cost: 153 - init_stock: 1160 - price: 153 - service_level: 0.96 - vlt: 2 - SKU79: - cost: 245 - init_stock: 220 - price: 245 - service_level: 0.96 - vlt: 1 - SKU790: - cost: 417 - init_stock: 1320 - price: 417 - service_level: 0.96 - vlt: 2 - SKU791: - cost: 134 - init_stock: 560 - price: 134 - service_level: 0.96 - vlt: 1 - SKU792: - cost: 313 - init_stock: 420 - price: 313 - service_level: 0.96 - vlt: 2 - SKU793: - cost: 195 - init_stock: 1520 - price: 195 - service_level: 0.96 - vlt: 2 - SKU794: - cost: 325 - init_stock: 1600 - price: 325 - service_level: 0.96 - vlt: 3 - SKU795: - cost: 276 - init_stock: 960 - price: 276 - service_level: 0.96 - vlt: 3 - SKU796: - cost: 447 - init_stock: 980 - price: 447 - service_level: 0.96 - vlt: 1 - SKU797: - cost: 100 - init_stock: 780 - price: 100 - service_level: 0.96 - vlt: 3 - SKU798: - cost: 426 - init_stock: 600 - price: 426 - service_level: 0.96 - vlt: 2 - SKU799: - cost: 63 - init_stock: 140 - price: 63 - service_level: 0.96 - vlt: 2 - SKU8: - cost: 125 - init_stock: 1260 - price: 125 - service_level: 0.96 - vlt: 3 - SKU80: - cost: 163 - init_stock: 1400 - price: 163 - service_level: 0.96 - vlt: 1 - SKU800: - cost: 298 - init_stock: 1880 - price: 298 - service_level: 0.96 - vlt: 2 - SKU801: - cost: 389 - init_stock: 720 - price: 389 - service_level: 0.96 - vlt: 2 - SKU802: - cost: 173 - init_stock: 1240 - price: 173 - service_level: 0.96 - vlt: 2 - SKU803: - cost: 272 - init_stock: 380 - price: 272 - service_level: 0.96 - vlt: 3 - SKU804: - cost: 390 - init_stock: 940 - price: 390 - service_level: 0.96 - vlt: 3 - SKU805: - cost: 61 - init_stock: 660 - price: 61 - service_level: 0.96 - vlt: 3 - SKU806: - cost: 158 - init_stock: 1040 - price: 158 - service_level: 0.96 - vlt: 1 - SKU807: - cost: 453 - init_stock: 520 - price: 453 - service_level: 0.96 - vlt: 3 - SKU808: - cost: 249 - init_stock: 1820 - price: 249 - service_level: 0.96 - vlt: 1 - SKU809: - cost: 55 - init_stock: 1300 - price: 55 - service_level: 0.96 - vlt: 1 - SKU81: - cost: 182 - init_stock: 1320 - price: 182 - service_level: 0.96 - vlt: 3 - SKU810: - cost: 276 - init_stock: 120 - price: 276 - service_level: 0.96 - vlt: 2 - SKU811: - cost: 254 - init_stock: 740 - price: 254 - service_level: 0.96 - vlt: 3 - SKU812: - cost: 252 - init_stock: 1600 - price: 252 - service_level: 0.96 - vlt: 1 - SKU813: - cost: 253 - init_stock: 140 - price: 253 - service_level: 0.96 - vlt: 3 - SKU814: - cost: 485 - init_stock: 1940 - price: 485 - service_level: 0.96 - vlt: 1 - SKU815: - cost: 390 - init_stock: 480 - price: 390 - service_level: 0.96 - vlt: 2 - SKU816: - cost: 152 - init_stock: 1820 - price: 152 - service_level: 0.96 - vlt: 3 - SKU817: - cost: 227 - init_stock: 100 - price: 227 - service_level: 0.96 - vlt: 2 - SKU818: - cost: 354 - init_stock: 860 - price: 354 - service_level: 0.96 - vlt: 2 - SKU819: - cost: 302 - init_stock: 540 - price: 302 - service_level: 0.96 - vlt: 1 - SKU82: - cost: 290 - init_stock: 560 - price: 290 - service_level: 0.96 - vlt: 1 - SKU820: - cost: 264 - init_stock: 1640 - price: 264 - service_level: 0.96 - vlt: 3 - SKU821: - cost: 99 - init_stock: 1380 - price: 99 - service_level: 0.96 - vlt: 1 - SKU822: - cost: 136 - init_stock: 1400 - price: 136 - service_level: 0.96 - vlt: 3 - SKU823: - cost: 75 - init_stock: 560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU824: - cost: 170 - init_stock: 1400 - price: 170 - service_level: 0.96 - vlt: 1 - SKU825: - cost: 214 - init_stock: 300 - price: 214 - service_level: 0.96 - vlt: 1 - SKU826: - cost: 386 - init_stock: 1100 - price: 386 - service_level: 0.96 - vlt: 2 - SKU827: - cost: 148 - init_stock: 1280 - price: 148 - service_level: 0.96 - vlt: 2 - SKU828: - cost: 400 - init_stock: 1380 - price: 400 - service_level: 0.96 - vlt: 1 - SKU829: - cost: 61 - init_stock: 1480 - price: 61 - service_level: 0.96 - vlt: 1 - SKU83: - cost: 296 - init_stock: 340 - price: 296 - service_level: 0.96 - vlt: 2 - SKU830: - cost: 167 - init_stock: 480 - price: 167 - service_level: 0.96 - vlt: 3 - SKU831: - cost: 262 - init_stock: 580 - price: 262 - service_level: 0.96 - vlt: 1 - SKU832: - cost: 33 - init_stock: 1640 - price: 33 - service_level: 0.96 - vlt: 1 - SKU833: - cost: 400 - init_stock: 1960 - price: 400 - service_level: 0.96 - vlt: 3 - SKU834: - cost: 422 - init_stock: 100 - price: 422 - service_level: 0.96 - vlt: 1 - SKU835: - cost: 440 - init_stock: 1040 - price: 440 - service_level: 0.96 - vlt: 2 - SKU836: - cost: 323 - init_stock: 1920 - price: 323 - service_level: 0.96 - vlt: 1 - SKU837: - cost: 373 - init_stock: 520 - price: 373 - service_level: 0.96 - vlt: 3 - SKU838: - cost: 456 - init_stock: 1540 - price: 456 - service_level: 0.96 - vlt: 1 - SKU839: - cost: 473 - init_stock: 1200 - price: 473 - service_level: 0.96 - vlt: 1 - SKU84: - cost: 318 - init_stock: 840 - price: 318 - service_level: 0.96 - vlt: 3 - SKU840: - cost: 266 - init_stock: 1000 - price: 266 - service_level: 0.96 - vlt: 1 - SKU841: - cost: 285 - init_stock: 1700 - price: 285 - service_level: 0.96 - vlt: 3 - SKU842: - cost: 41 - init_stock: 1680 - price: 41 - service_level: 0.96 - vlt: 1 - SKU843: - cost: 360 - init_stock: 1440 - price: 360 - service_level: 0.96 - vlt: 2 - SKU844: - cost: 51 - init_stock: 1580 - price: 51 - service_level: 0.96 - vlt: 3 - SKU845: - cost: 288 - init_stock: 440 - price: 288 - service_level: 0.96 - vlt: 3 - SKU846: - cost: 485 - init_stock: 780 - price: 485 - service_level: 0.96 - vlt: 1 - SKU847: - cost: 388 - init_stock: 1820 - price: 388 - service_level: 0.96 - vlt: 1 - SKU848: - cost: 306 - init_stock: 920 - price: 306 - service_level: 0.96 - vlt: 2 - SKU849: - cost: 320 - init_stock: 800 - price: 320 - service_level: 0.96 - vlt: 2 - SKU85: - cost: 442 - init_stock: 2000 - price: 442 - service_level: 0.96 - vlt: 3 - SKU850: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 2 - SKU851: - cost: 53 - init_stock: 1280 - price: 53 - service_level: 0.96 - vlt: 1 - SKU852: - cost: 306 - init_stock: 1080 - price: 306 - service_level: 0.96 - vlt: 2 - SKU853: - cost: 386 - init_stock: 1120 - price: 386 - service_level: 0.96 - vlt: 3 - SKU854: - cost: 212 - init_stock: 1300 - price: 212 - service_level: 0.96 - vlt: 3 - SKU855: - cost: 76 - init_stock: 1440 - price: 76 - service_level: 0.96 - vlt: 3 - SKU856: - cost: 380 - init_stock: 640 - price: 380 - service_level: 0.96 - vlt: 2 - SKU857: - cost: 462 - init_stock: 2000 - price: 462 - service_level: 0.96 - vlt: 1 - SKU858: - cost: 80 - init_stock: 480 - price: 80 - service_level: 0.96 - vlt: 3 - SKU859: - cost: 215 - init_stock: 560 - price: 215 - service_level: 0.96 - vlt: 3 - SKU86: - cost: 386 - init_stock: 1700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU860: - cost: 313 - init_stock: 300 - price: 313 - service_level: 0.96 - vlt: 2 - SKU861: - cost: 477 - init_stock: 260 - price: 477 - service_level: 0.96 - vlt: 2 - SKU862: - cost: 240 - init_stock: 320 - price: 240 - service_level: 0.96 - vlt: 2 - SKU863: - cost: 470 - init_stock: 1860 - price: 470 - service_level: 0.96 - vlt: 2 - SKU864: - cost: 203 - init_stock: 380 - price: 203 - service_level: 0.96 - vlt: 1 - SKU865: - cost: 144 - init_stock: 380 - price: 144 - service_level: 0.96 - vlt: 3 - SKU866: - cost: 172 - init_stock: 1760 - price: 172 - service_level: 0.96 - vlt: 1 - SKU867: - cost: 499 - init_stock: 2000 - price: 499 - service_level: 0.96 - vlt: 2 - SKU868: - cost: 41 - init_stock: 1000 - price: 41 - service_level: 0.96 - vlt: 2 - SKU869: - cost: 97 - init_stock: 1940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU87: - cost: 368 - init_stock: 1380 - price: 368 - service_level: 0.96 - vlt: 2 - SKU870: - cost: 281 - init_stock: 1340 - price: 281 - service_level: 0.96 - vlt: 1 - SKU871: - cost: 101 - init_stock: 1980 - price: 101 - service_level: 0.96 - vlt: 1 - SKU872: - cost: 133 - init_stock: 640 - price: 133 - service_level: 0.96 - vlt: 3 - SKU873: - cost: 423 - init_stock: 620 - price: 423 - service_level: 0.96 - vlt: 2 - SKU874: - cost: 469 - init_stock: 1200 - price: 469 - service_level: 0.96 - vlt: 1 - SKU875: - cost: 51 - init_stock: 460 - price: 51 - service_level: 0.96 - vlt: 2 - SKU876: - cost: 303 - init_stock: 1220 - price: 303 - service_level: 0.96 - vlt: 3 - SKU877: - cost: 40 - init_stock: 700 - price: 40 - service_level: 0.96 - vlt: 3 - SKU878: - cost: 27 - init_stock: 1360 - price: 27 - service_level: 0.96 - vlt: 3 - SKU879: - cost: 150 - init_stock: 2000 - price: 150 - service_level: 0.96 - vlt: 1 - SKU88: - cost: 471 - init_stock: 240 - price: 471 - service_level: 0.96 - vlt: 1 - SKU880: - cost: 433 - init_stock: 620 - price: 433 - service_level: 0.96 - vlt: 3 - SKU881: - cost: 431 - init_stock: 1400 - price: 431 - service_level: 0.96 - vlt: 3 - SKU882: - cost: 194 - init_stock: 320 - price: 194 - service_level: 0.96 - vlt: 1 - SKU883: - cost: 284 - init_stock: 760 - price: 284 - service_level: 0.96 - vlt: 1 - SKU884: - cost: 139 - init_stock: 780 - price: 139 - service_level: 0.96 - vlt: 2 - SKU885: - cost: 49 - init_stock: 540 - price: 49 - service_level: 0.96 - vlt: 3 - SKU886: - cost: 372 - init_stock: 1460 - price: 372 - service_level: 0.96 - vlt: 3 - SKU887: - cost: 266 - init_stock: 1740 - price: 266 - service_level: 0.96 - vlt: 1 - SKU888: - cost: 143 - init_stock: 180 - price: 143 - service_level: 0.96 - vlt: 2 - SKU889: - cost: 101 - init_stock: 420 - price: 101 - service_level: 0.96 - vlt: 2 - SKU89: - cost: 138 - init_stock: 1860 - price: 138 - service_level: 0.96 - vlt: 3 - SKU890: - cost: 161 - init_stock: 2000 - price: 161 - service_level: 0.96 - vlt: 3 - SKU891: - cost: 356 - init_stock: 440 - price: 356 - service_level: 0.96 - vlt: 3 - SKU892: - cost: 313 - init_stock: 1840 - price: 313 - service_level: 0.96 - vlt: 2 - SKU893: - cost: 229 - init_stock: 1980 - price: 229 - service_level: 0.96 - vlt: 1 - SKU894: - cost: 129 - init_stock: 1480 - price: 129 - service_level: 0.96 - vlt: 1 - SKU895: - cost: 230 - init_stock: 260 - price: 230 - service_level: 0.96 - vlt: 2 - SKU896: - cost: 289 - init_stock: 1600 - price: 289 - service_level: 0.96 - vlt: 3 - SKU897: - cost: 393 - init_stock: 1440 - price: 393 - service_level: 0.96 - vlt: 3 - SKU898: - cost: 477 - init_stock: 220 - price: 477 - service_level: 0.96 - vlt: 2 - SKU899: - cost: 233 - init_stock: 960 - price: 233 - service_level: 0.96 - vlt: 2 - SKU9: - cost: 166 - init_stock: 1920 - price: 166 - service_level: 0.96 - vlt: 2 - SKU90: - cost: 454 - init_stock: 1020 - price: 454 - service_level: 0.96 - vlt: 2 - SKU900: - cost: 158 - init_stock: 1100 - price: 158 - service_level: 0.96 - vlt: 1 - SKU901: - cost: 215 - init_stock: 580 - price: 215 - service_level: 0.96 - vlt: 3 - SKU902: - cost: 125 - init_stock: 1600 - price: 125 - service_level: 0.96 - vlt: 2 - SKU903: - cost: 357 - init_stock: 760 - price: 357 - service_level: 0.96 - vlt: 2 - SKU904: - cost: 496 - init_stock: 1020 - price: 496 - service_level: 0.96 - vlt: 1 - SKU905: - cost: 249 - init_stock: 620 - price: 249 - service_level: 0.96 - vlt: 3 - SKU906: - cost: 166 - init_stock: 1040 - price: 166 - service_level: 0.96 - vlt: 2 - SKU907: - cost: 22 - init_stock: 1660 - price: 22 - service_level: 0.96 - vlt: 3 - SKU908: - cost: 408 - init_stock: 1560 - price: 408 - service_level: 0.96 - vlt: 2 - SKU909: - cost: 482 - init_stock: 1920 - price: 482 - service_level: 0.96 - vlt: 1 - SKU91: - cost: 303 - init_stock: 320 - price: 303 - service_level: 0.96 - vlt: 2 - SKU910: - cost: 226 - init_stock: 660 - price: 226 - service_level: 0.96 - vlt: 2 - SKU911: - cost: 461 - init_stock: 1400 - price: 461 - service_level: 0.96 - vlt: 2 - SKU912: - cost: 236 - init_stock: 540 - price: 236 - service_level: 0.96 - vlt: 3 - SKU913: - cost: 322 - init_stock: 920 - price: 322 - service_level: 0.96 - vlt: 2 - SKU914: - cost: 272 - init_stock: 1240 - price: 272 - service_level: 0.96 - vlt: 3 - SKU915: - cost: 337 - init_stock: 1120 - price: 337 - service_level: 0.96 - vlt: 3 - SKU916: - cost: 337 - init_stock: 1780 - price: 337 - service_level: 0.96 - vlt: 3 - SKU917: - cost: 258 - init_stock: 600 - price: 258 - service_level: 0.96 - vlt: 3 - SKU918: - cost: 148 - init_stock: 1100 - price: 148 - service_level: 0.96 - vlt: 1 - SKU919: - cost: 393 - init_stock: 500 - price: 393 - service_level: 0.96 - vlt: 3 - SKU92: - cost: 262 - init_stock: 1260 - price: 262 - service_level: 0.96 - vlt: 2 - SKU920: - cost: 357 - init_stock: 1720 - price: 357 - service_level: 0.96 - vlt: 3 - SKU921: - cost: 227 - init_stock: 1320 - price: 227 - service_level: 0.96 - vlt: 2 - SKU922: - cost: 112 - init_stock: 1340 - price: 112 - service_level: 0.96 - vlt: 2 - SKU923: - cost: 496 - init_stock: 1160 - price: 496 - service_level: 0.96 - vlt: 2 - SKU924: - cost: 316 - init_stock: 1700 - price: 316 - service_level: 0.96 - vlt: 3 - SKU925: - cost: 360 - init_stock: 300 - price: 360 - service_level: 0.96 - vlt: 1 - SKU926: - cost: 360 - init_stock: 1340 - price: 360 - service_level: 0.96 - vlt: 2 - SKU927: - cost: 260 - init_stock: 420 - price: 260 - service_level: 0.96 - vlt: 3 - SKU928: - cost: 491 - init_stock: 1660 - price: 491 - service_level: 0.96 - vlt: 1 - SKU929: - cost: 359 - init_stock: 2000 - price: 359 - service_level: 0.96 - vlt: 3 - SKU93: - cost: 404 - init_stock: 1340 - price: 404 - service_level: 0.96 - vlt: 2 - SKU930: - cost: 198 - init_stock: 560 - price: 198 - service_level: 0.96 - vlt: 2 - SKU931: - cost: 71 - init_stock: 280 - price: 71 - service_level: 0.96 - vlt: 3 - SKU932: - cost: 163 - init_stock: 1320 - price: 163 - service_level: 0.96 - vlt: 2 - SKU933: - cost: 113 - init_stock: 1560 - price: 113 - service_level: 0.96 - vlt: 1 - SKU934: - cost: 219 - init_stock: 1340 - price: 219 - service_level: 0.96 - vlt: 3 - SKU935: - cost: 364 - init_stock: 1880 - price: 364 - service_level: 0.96 - vlt: 1 - SKU936: - cost: 24 - init_stock: 100 - price: 24 - service_level: 0.96 - vlt: 3 - SKU937: - cost: 135 - init_stock: 340 - price: 135 - service_level: 0.96 - vlt: 1 - SKU938: - cost: 432 - init_stock: 420 - price: 432 - service_level: 0.96 - vlt: 2 - SKU939: - cost: 173 - init_stock: 1180 - price: 173 - service_level: 0.96 - vlt: 3 - SKU94: - cost: 184 - init_stock: 940 - price: 184 - service_level: 0.96 - vlt: 3 - SKU940: - cost: 14 - init_stock: 1860 - price: 14 - service_level: 0.96 - vlt: 1 - SKU941: - cost: 80 - init_stock: 1140 - price: 80 - service_level: 0.96 - vlt: 3 - SKU942: - cost: 202 - init_stock: 260 - price: 202 - service_level: 0.96 - vlt: 3 - SKU943: - cost: 138 - init_stock: 980 - price: 138 - service_level: 0.96 - vlt: 1 - SKU944: - cost: 196 - init_stock: 880 - price: 196 - service_level: 0.96 - vlt: 1 - SKU945: - cost: 141 - init_stock: 340 - price: 141 - service_level: 0.96 - vlt: 2 - SKU946: - cost: 325 - init_stock: 980 - price: 325 - service_level: 0.96 - vlt: 1 - SKU947: - cost: 338 - init_stock: 1820 - price: 338 - service_level: 0.96 - vlt: 3 - SKU948: - cost: 425 - init_stock: 560 - price: 425 - service_level: 0.96 - vlt: 3 - SKU949: - cost: 309 - init_stock: 100 - price: 309 - service_level: 0.96 - vlt: 2 - SKU95: - cost: 136 - init_stock: 420 - price: 136 - service_level: 0.96 - vlt: 3 - SKU950: - cost: 102 - init_stock: 1080 - price: 102 - service_level: 0.96 - vlt: 2 - SKU951: - cost: 75 - init_stock: 360 - price: 75 - service_level: 0.96 - vlt: 2 - SKU952: - cost: 156 - init_stock: 220 - price: 156 - service_level: 0.96 - vlt: 3 - SKU953: - cost: 138 - init_stock: 700 - price: 138 - service_level: 0.96 - vlt: 3 - SKU954: - cost: 296 - init_stock: 1720 - price: 296 - service_level: 0.96 - vlt: 1 - SKU955: - cost: 55 - init_stock: 1260 - price: 55 - service_level: 0.96 - vlt: 1 - SKU956: - cost: 282 - init_stock: 1700 - price: 282 - service_level: 0.96 - vlt: 1 - SKU957: - cost: 305 - init_stock: 1020 - price: 305 - service_level: 0.96 - vlt: 2 - SKU958: - cost: 369 - init_stock: 1320 - price: 369 - service_level: 0.96 - vlt: 2 - SKU959: - cost: 81 - init_stock: 1640 - price: 81 - service_level: 0.96 - vlt: 1 - SKU96: - cost: 176 - init_stock: 880 - price: 176 - service_level: 0.96 - vlt: 3 - SKU960: - cost: 147 - init_stock: 120 - price: 147 - service_level: 0.96 - vlt: 3 - SKU961: - cost: 264 - init_stock: 880 - price: 264 - service_level: 0.96 - vlt: 1 - SKU962: - cost: 354 - init_stock: 880 - price: 354 - service_level: 0.96 - vlt: 1 - SKU963: - cost: 349 - init_stock: 420 - price: 349 - service_level: 0.96 - vlt: 1 - SKU964: - cost: 244 - init_stock: 1460 - price: 244 - service_level: 0.96 - vlt: 1 - SKU965: - cost: 124 - init_stock: 1020 - price: 124 - service_level: 0.96 - vlt: 1 - SKU966: - cost: 302 - init_stock: 880 - price: 302 - service_level: 0.96 - vlt: 3 - SKU967: - cost: 67 - init_stock: 900 - price: 67 - service_level: 0.96 - vlt: 3 - SKU968: - cost: 281 - init_stock: 980 - price: 281 - service_level: 0.96 - vlt: 2 - SKU969: - cost: 249 - init_stock: 120 - price: 249 - service_level: 0.96 - vlt: 1 - SKU97: - cost: 28 - init_stock: 600 - price: 28 - service_level: 0.96 - vlt: 3 - SKU970: - cost: 244 - init_stock: 100 - price: 244 - service_level: 0.96 - vlt: 2 - SKU971: - cost: 368 - init_stock: 1460 - price: 368 - service_level: 0.96 - vlt: 1 - SKU972: - cost: 209 - init_stock: 1380 - price: 209 - service_level: 0.96 - vlt: 1 - SKU973: - cost: 271 - init_stock: 220 - price: 271 - service_level: 0.96 - vlt: 2 - SKU974: - cost: 170 - init_stock: 640 - price: 170 - service_level: 0.96 - vlt: 1 - SKU975: - cost: 198 - init_stock: 440 - price: 198 - service_level: 0.96 - vlt: 3 - SKU976: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 2 - SKU977: - cost: 234 - init_stock: 1080 - price: 234 - service_level: 0.96 - vlt: 3 - SKU978: - cost: 470 - init_stock: 1280 - price: 470 - service_level: 0.96 - vlt: 3 - SKU979: - cost: 443 - init_stock: 1920 - price: 443 - service_level: 0.96 - vlt: 2 - SKU98: - cost: 443 - init_stock: 700 - price: 443 - service_level: 0.96 - vlt: 1 - SKU980: - cost: 39 - init_stock: 1360 - price: 39 - service_level: 0.96 - vlt: 3 - SKU981: - cost: 482 - init_stock: 1440 - price: 482 - service_level: 0.96 - vlt: 2 - SKU982: - cost: 213 - init_stock: 1040 - price: 213 - service_level: 0.96 - vlt: 3 - SKU983: - cost: 449 - init_stock: 1840 - price: 449 - service_level: 0.96 - vlt: 1 - SKU984: - cost: 232 - init_stock: 1820 - price: 232 - service_level: 0.96 - vlt: 3 - SKU985: - cost: 290 - init_stock: 1020 - price: 290 - service_level: 0.96 - vlt: 1 - SKU986: - cost: 275 - init_stock: 340 - price: 275 - service_level: 0.96 - vlt: 3 - SKU987: - cost: 434 - init_stock: 680 - price: 434 - service_level: 0.96 - vlt: 1 - SKU988: - cost: 102 - init_stock: 460 - price: 102 - service_level: 0.96 - vlt: 1 - SKU989: - cost: 484 - init_stock: 1860 - price: 484 - service_level: 0.96 - vlt: 1 - SKU99: - cost: 361 - init_stock: 900 - price: 361 - service_level: 0.96 - vlt: 3 - SKU990: - cost: 108 - init_stock: 1140 - price: 108 - service_level: 0.96 - vlt: 3 - SKU991: - cost: 409 - init_stock: 380 - price: 409 - service_level: 0.96 - vlt: 3 - SKU992: - cost: 434 - init_stock: 1860 - price: 434 - service_level: 0.96 - vlt: 3 - SKU993: - cost: 145 - init_stock: 1720 - price: 145 - service_level: 0.96 - vlt: 2 - SKU994: - cost: 470 - init_stock: 1000 - price: 470 - service_level: 0.96 - vlt: 3 - SKU995: - cost: 241 - init_stock: 1520 - price: 241 - service_level: 0.96 - vlt: 2 - SKU996: - cost: 260 - init_stock: 1460 - price: 260 - service_level: 0.96 - vlt: 3 - SKU997: - cost: 400 - init_stock: 680 - price: 400 - service_level: 0.96 - vlt: 1 - SKU998: - cost: 447 - init_stock: 1100 - price: 447 - service_level: 0.96 - vlt: 2 - SKU999: - cost: 79 - init_stock: 460 - price: 79 - service_level: 0.96 - vlt: 2 - - children: - storage: - config: - capacity: 1064900 - unit_storage_cost: 1 - config: - order_cost: 500 - definition_ref: RetailerFacility - name: STORE0 - skus: - SKU0: - constraint: null - cost: 322 - init_stock: 126 - max_stock: 1000 - price: 631 - sale_gamma: 63 - service_level: 0.95 - SKU1: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 284 - init_stock: 176 - max_stock: 1000 - price: 448 - sale_gamma: 22 - service_level: 0.95 - SKU10: - constraint: null - cost: 68 - init_stock: 150 - max_stock: 1000 - price: 116 - sale_gamma: 25 - service_level: 0.95 - SKU100: - constraint: G(low_profit -> low_stock_constraint) - cost: 16 - init_stock: 390 - max_stock: 1000 - price: 23 - sale_gamma: 65 - service_level: 0.95 - SKU101: - constraint: null - cost: 84 - init_stock: 497 - max_stock: 1000 - price: 149 - sale_gamma: 71 - service_level: 0.95 - SKU102: - constraint: null - cost: 328 - init_stock: 558 - max_stock: 1000 - price: 505 - sale_gamma: 93 - service_level: 0.95 - SKU103: - constraint: null - cost: 334 - init_stock: 285 - max_stock: 1000 - price: 601 - sale_gamma: 95 - service_level: 0.95 - SKU104: - constraint: null - cost: 49 - init_stock: 325 - max_stock: 1000 - price: 57 - sale_gamma: 65 - service_level: 0.95 - SKU105: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 110 - init_stock: 285 - max_stock: 1000 - price: 171 - sale_gamma: 57 - service_level: 0.95 - SKU106: - constraint: G(low_profit -> low_stock_constraint) - cost: 251 - init_stock: 584 - max_stock: 1000 - price: 454 - sale_gamma: 73 - service_level: 0.95 - SKU107: - constraint: G(stock_constraint) - cost: 423 - init_stock: 348 - max_stock: 1000 - price: 706 - sale_gamma: 87 - service_level: 0.95 - SKU108: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 458 - init_stock: 368 - max_stock: 1000 - price: 801 - sale_gamma: 92 - service_level: 0.95 - SKU109: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 88 - init_stock: 328 - max_stock: 1000 - price: 98 - sale_gamma: 82 - service_level: 0.95 - SKU11: - constraint: null - cost: 400 - init_stock: 306 - max_stock: 1000 - price: 680 - sale_gamma: 51 - service_level: 0.95 - SKU110: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 66 - init_stock: 112 - max_stock: 1000 - price: 100 - sale_gamma: 14 - service_level: 0.95 - SKU111: - constraint: G(stock_constraint) - cost: 260 - init_stock: 183 - max_stock: 1000 - price: 364 - sale_gamma: 61 - service_level: 0.95 - SKU112: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 61 - init_stock: 475 - max_stock: 1000 - price: 119 - sale_gamma: 95 - service_level: 0.95 - SKU113: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 155 - max_stock: 1000 - price: 678 - sale_gamma: 31 - service_level: 0.95 - SKU114: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 389 - init_stock: 162 - max_stock: 1000 - price: 564 - sale_gamma: 27 - service_level: 0.95 - SKU115: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 258 - max_stock: 1000 - price: 557 - sale_gamma: 86 - service_level: 0.95 - SKU116: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 288 - max_stock: 1000 - price: 768 - sale_gamma: 72 - service_level: 0.95 - SKU117: - constraint: null - cost: 320 - init_stock: 644 - max_stock: 1000 - price: 374 - sale_gamma: 92 - service_level: 0.95 - SKU118: - constraint: null - cost: 183 - init_stock: 66 - max_stock: 1000 - price: 208 - sale_gamma: 33 - service_level: 0.95 - SKU119: - constraint: null - cost: 209 - init_stock: 256 - max_stock: 1000 - price: 317 - sale_gamma: 32 - service_level: 0.95 - SKU12: - constraint: null - cost: 112 - init_stock: 336 - max_stock: 1000 - price: 210 - sale_gamma: 84 - service_level: 0.95 - SKU120: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 121 - init_stock: 392 - max_stock: 1000 - price: 151 - sale_gamma: 98 - service_level: 0.95 - SKU121: - constraint: null - cost: 40 - init_stock: 510 - max_stock: 1000 - price: 54 - sale_gamma: 85 - service_level: 0.95 - SKU122: - constraint: G(stock_constraint) - cost: 437 - init_stock: 35 - max_stock: 1000 - price: 589 - sale_gamma: 7 - service_level: 0.95 - SKU123: - constraint: G(stock_constraint) - cost: 233 - init_stock: 114 - max_stock: 1000 - price: 326 - sale_gamma: 19 - service_level: 0.95 - SKU124: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 108 - max_stock: 1000 - price: 298 - sale_gamma: 36 - service_level: 0.95 - SKU125: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 368 - max_stock: 1000 - price: 32 - sale_gamma: 92 - service_level: 0.95 - SKU126: - constraint: null - cost: 36 - init_stock: 156 - max_stock: 1000 - price: 40 - sale_gamma: 39 - service_level: 0.95 - SKU127: - constraint: G(stock_constraint) - cost: 217 - init_stock: 155 - max_stock: 1000 - price: 366 - sale_gamma: 31 - service_level: 0.95 - SKU128: - constraint: null - cost: 165 - init_stock: 95 - max_stock: 1000 - price: 283 - sale_gamma: 19 - service_level: 0.95 - SKU129: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 203 - sale_gamma: 50 - service_level: 0.95 - SKU13: - constraint: null - cost: 317 - init_stock: 456 - max_stock: 1000 - price: 573 - sale_gamma: 57 - service_level: 0.95 - SKU130: - constraint: null - cost: 348 - init_stock: 784 - max_stock: 1000 - price: 396 - sale_gamma: 98 - service_level: 0.95 - SKU131: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 64 - init_stock: 270 - max_stock: 1000 - price: 110 - sale_gamma: 45 - service_level: 0.95 - SKU132: - constraint: null - cost: 427 - init_stock: 105 - max_stock: 1000 - price: 678 - sale_gamma: 21 - service_level: 0.95 - SKU133: - constraint: null - cost: 224 - init_stock: 145 - max_stock: 1000 - price: 448 - sale_gamma: 29 - service_level: 0.95 - SKU134: - constraint: G(stock_constraint) - cost: 336 - init_stock: 539 - max_stock: 1000 - price: 470 - sale_gamma: 77 - service_level: 0.95 - SKU135: - constraint: null - cost: 153 - init_stock: 500 - max_stock: 1000 - price: 243 - sale_gamma: 100 - service_level: 0.95 - SKU136: - constraint: G(low_profit -> low_stock_constraint) - cost: 199 - init_stock: 497 - max_stock: 1000 - price: 370 - sale_gamma: 71 - service_level: 0.95 - SKU137: - constraint: G(stock_constraint) - cost: 93 - init_stock: 444 - max_stock: 1000 - price: 165 - sale_gamma: 74 - service_level: 0.95 - SKU138: - constraint: G(stock_constraint) - cost: 228 - init_stock: 180 - max_stock: 1000 - price: 253 - sale_gamma: 36 - service_level: 0.95 - SKU139: - constraint: G(stock_constraint) - cost: 207 - init_stock: 144 - max_stock: 1000 - price: 368 - sale_gamma: 24 - service_level: 0.95 - SKU14: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 268 - init_stock: 372 - max_stock: 1000 - price: 305 - sale_gamma: 62 - service_level: 0.95 - SKU140: - constraint: null - cost: 261 - init_stock: 238 - max_stock: 1000 - price: 357 - sale_gamma: 34 - service_level: 0.95 - SKU141: - constraint: null - cost: 190 - init_stock: 164 - max_stock: 1000 - price: 370 - sale_gamma: 41 - service_level: 0.95 - SKU142: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 320 - init_stock: 152 - max_stock: 1000 - price: 428 - sale_gamma: 38 - service_level: 0.95 - SKU143: - constraint: G(stock_constraint) - cost: 318 - init_stock: 182 - max_stock: 1000 - price: 566 - sale_gamma: 26 - service_level: 0.95 - SKU144: - constraint: null - cost: 400 - init_stock: 24 - max_stock: 1000 - price: 716 - sale_gamma: 12 - service_level: 0.95 - SKU145: - constraint: null - cost: 399 - init_stock: 328 - max_stock: 1000 - price: 774 - sale_gamma: 82 - service_level: 0.95 - SKU146: - constraint: null - cost: 177 - init_stock: 192 - max_stock: 1000 - price: 194 - sale_gamma: 48 - service_level: 0.95 - SKU147: - constraint: G(low_profit -> low_stock_constraint) - cost: 472 - init_stock: 392 - max_stock: 1000 - price: 840 - sale_gamma: 56 - service_level: 0.95 - SKU148: - constraint: G(stock_constraint) - cost: 313 - init_stock: 308 - max_stock: 1000 - price: 566 - sale_gamma: 77 - service_level: 0.95 - SKU149: - constraint: G(low_profit -> low_stock_constraint) - cost: 357 - init_stock: 539 - max_stock: 1000 - price: 549 - sale_gamma: 77 - service_level: 0.95 - SKU15: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 25 - max_stock: 1000 - price: 58 - sale_gamma: 5 - service_level: 0.95 - SKU150: - constraint: null - cost: 106 - init_stock: 210 - max_stock: 1000 - price: 159 - sale_gamma: 70 - service_level: 0.95 - SKU151: - constraint: G(low_profit -> low_stock_constraint) - cost: 223 - init_stock: 146 - max_stock: 1000 - price: 370 - sale_gamma: 73 - service_level: 0.95 - SKU152: - constraint: null - cost: 10 - init_stock: 408 - max_stock: 1000 - price: 14 - sale_gamma: 51 - service_level: 0.95 - SKU153: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 441 - init_stock: 372 - max_stock: 1000 - price: 538 - sale_gamma: 62 - service_level: 0.95 - SKU154: - constraint: null - cost: 77 - init_stock: 680 - max_stock: 1000 - price: 135 - sale_gamma: 85 - service_level: 0.95 - SKU155: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 159 - max_stock: 1000 - price: 641 - sale_gamma: 53 - service_level: 0.95 - SKU156: - constraint: null - cost: 10 - init_stock: 36 - max_stock: 1000 - price: 16 - sale_gamma: 12 - service_level: 0.95 - SKU157: - constraint: null - cost: 410 - init_stock: 525 - max_stock: 1000 - price: 594 - sale_gamma: 75 - service_level: 0.95 - SKU158: - constraint: null - cost: 145 - init_stock: 486 - max_stock: 1000 - price: 275 - sale_gamma: 81 - service_level: 0.95 - SKU159: - constraint: G(stock_constraint) - cost: 193 - init_stock: 100 - max_stock: 1000 - price: 368 - sale_gamma: 25 - service_level: 0.95 - SKU16: - constraint: null - cost: 175 - init_stock: 464 - max_stock: 1000 - price: 344 - sale_gamma: 58 - service_level: 0.95 - SKU160: - constraint: G(low_profit -> low_stock_constraint) - cost: 459 - init_stock: 510 - max_stock: 1000 - price: 876 - sale_gamma: 85 - service_level: 0.95 - SKU161: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 239 - init_stock: 230 - max_stock: 1000 - price: 478 - sale_gamma: 46 - service_level: 0.95 - SKU162: - constraint: null - cost: 158 - init_stock: 30 - max_stock: 1000 - price: 290 - sale_gamma: 5 - service_level: 0.95 - SKU163: - constraint: G(stock_constraint) - cost: 486 - init_stock: 273 - max_stock: 1000 - price: 704 - sale_gamma: 39 - service_level: 0.95 - SKU164: - constraint: null - cost: 496 - init_stock: 500 - max_stock: 1000 - price: 615 - sale_gamma: 100 - service_level: 0.95 - SKU165: - constraint: G(stock_constraint) - cost: 274 - init_stock: 231 - max_stock: 1000 - price: 548 - sale_gamma: 33 - service_level: 0.95 - SKU166: - constraint: null - cost: 79 - init_stock: 178 - max_stock: 1000 - price: 137 - sale_gamma: 89 - service_level: 0.95 - SKU167: - constraint: G(stock_constraint) - cost: 443 - init_stock: 52 - max_stock: 1000 - price: 854 - sale_gamma: 13 - service_level: 0.95 - SKU168: - constraint: null - cost: 357 - init_stock: 522 - max_stock: 1000 - price: 546 - sale_gamma: 87 - service_level: 0.95 - SKU169: - constraint: null - cost: 369 - init_stock: 686 - max_stock: 1000 - price: 505 - sale_gamma: 98 - service_level: 0.95 - SKU17: - constraint: null - cost: 346 - init_stock: 18 - max_stock: 1000 - price: 553 - sale_gamma: 9 - service_level: 0.95 - SKU170: - constraint: null - cost: 68 - init_stock: 275 - max_stock: 1000 - price: 113 - sale_gamma: 55 - service_level: 0.95 - SKU171: - constraint: G(low_profit -> low_stock_constraint) - cost: 398 - init_stock: 380 - max_stock: 1000 - price: 704 - sale_gamma: 76 - service_level: 0.95 - SKU172: - constraint: null - cost: 200 - init_stock: 426 - max_stock: 1000 - price: 222 - sale_gamma: 71 - service_level: 0.95 - SKU173: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 480 - max_stock: 1000 - price: 264 - sale_gamma: 96 - service_level: 0.95 - SKU174: - constraint: null - cost: 291 - init_stock: 380 - max_stock: 1000 - price: 582 - sale_gamma: 76 - service_level: 0.95 - SKU175: - constraint: null - cost: 84 - init_stock: 225 - max_stock: 1000 - price: 140 - sale_gamma: 75 - service_level: 0.95 - SKU176: - constraint: G(stock_constraint) - cost: 407 - init_stock: 330 - max_stock: 1000 - price: 610 - sale_gamma: 66 - service_level: 0.95 - SKU177: - constraint: null - cost: 257 - init_stock: 155 - max_stock: 1000 - price: 346 - sale_gamma: 31 - service_level: 0.95 - SKU178: - constraint: null - cost: 423 - init_stock: 20 - max_stock: 1000 - price: 499 - sale_gamma: 5 - service_level: 0.95 - SKU179: - constraint: null - cost: 497 - init_stock: 415 - max_stock: 1000 - price: 690 - sale_gamma: 83 - service_level: 0.95 - SKU18: - constraint: null - cost: 258 - init_stock: 405 - max_stock: 1000 - price: 387 - sale_gamma: 81 - service_level: 0.95 - SKU180: - constraint: null - cost: 217 - init_stock: 165 - max_stock: 1000 - price: 297 - sale_gamma: 55 - service_level: 0.95 - SKU181: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 214 - sale_gamma: 60 - service_level: 0.95 - SKU182: - constraint: null - cost: 437 - init_stock: 693 - max_stock: 1000 - price: 764 - sale_gamma: 99 - service_level: 0.95 - SKU183: - constraint: null - cost: 145 - init_stock: 56 - max_stock: 1000 - price: 178 - sale_gamma: 8 - service_level: 0.95 - SKU184: - constraint: null - cost: 73 - init_stock: 72 - max_stock: 1000 - price: 100 - sale_gamma: 24 - service_level: 0.95 - SKU185: - constraint: null - cost: 10 - init_stock: 360 - max_stock: 1000 - price: 14 - sale_gamma: 90 - service_level: 0.95 - SKU186: - constraint: null - cost: 359 - init_stock: 66 - max_stock: 1000 - price: 649 - sale_gamma: 22 - service_level: 0.95 - SKU187: - constraint: G(low_profit -> low_stock_constraint) - cost: 177 - init_stock: 120 - max_stock: 1000 - price: 249 - sale_gamma: 30 - service_level: 0.95 - SKU188: - constraint: null - cost: 391 - init_stock: 348 - max_stock: 1000 - price: 586 - sale_gamma: 87 - service_level: 0.95 - SKU189: - constraint: G(low_profit -> low_stock_constraint) - cost: 358 - init_stock: 210 - max_stock: 1000 - price: 400 - sale_gamma: 35 - service_level: 0.95 - SKU19: - constraint: G(stock_constraint) - cost: 477 - init_stock: 364 - max_stock: 1000 - price: 791 - sale_gamma: 91 - service_level: 0.95 - SKU190: - constraint: null - cost: 113 - init_stock: 102 - max_stock: 1000 - price: 153 - sale_gamma: 17 - service_level: 0.95 - SKU191: - constraint: G(stock_constraint) - cost: 473 - init_stock: 324 - max_stock: 1000 - price: 638 - sale_gamma: 54 - service_level: 0.95 - SKU192: - constraint: null - cost: 415 - init_stock: 244 - max_stock: 1000 - price: 539 - sale_gamma: 61 - service_level: 0.95 - SKU193: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 207 - init_stock: 120 - max_stock: 1000 - price: 353 - sale_gamma: 30 - service_level: 0.95 - SKU194: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 30 - max_stock: 1000 - price: 803 - sale_gamma: 5 - service_level: 0.95 - SKU195: - constraint: G(low_profit -> low_stock_constraint) - cost: 218 - init_stock: 93 - max_stock: 1000 - price: 248 - sale_gamma: 31 - service_level: 0.95 - SKU196: - constraint: null - cost: 49 - init_stock: 544 - max_stock: 1000 - price: 62 - sale_gamma: 68 - service_level: 0.95 - SKU197: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 285 - max_stock: 1000 - price: 509 - sale_gamma: 57 - service_level: 0.95 - SKU198: - constraint: G(low_profit -> low_stock_constraint) - cost: 169 - init_stock: 378 - max_stock: 1000 - price: 307 - sale_gamma: 54 - service_level: 0.95 - SKU199: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 138 - max_stock: 1000 - price: 794 - sale_gamma: 23 - service_level: 0.95 - SKU2: - constraint: null - cost: 331 - init_stock: 350 - max_stock: 1000 - price: 499 - sale_gamma: 70 - service_level: 0.95 - SKU20: - constraint: G(stock_constraint) - cost: 335 - init_stock: 200 - max_stock: 1000 - price: 649 - sale_gamma: 25 - service_level: 0.95 - SKU200: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 65 - init_stock: 150 - max_stock: 1000 - price: 85 - sale_gamma: 25 - service_level: 0.95 - SKU201: - constraint: G(stock_constraint) - cost: 104 - init_stock: 354 - max_stock: 1000 - price: 161 - sale_gamma: 59 - service_level: 0.95 - SKU202: - constraint: null - cost: 142 - init_stock: 365 - max_stock: 1000 - price: 222 - sale_gamma: 73 - service_level: 0.95 - SKU203: - constraint: null - cost: 440 - init_stock: 328 - max_stock: 1000 - price: 677 - sale_gamma: 82 - service_level: 0.95 - SKU204: - constraint: null - cost: 489 - init_stock: 188 - max_stock: 1000 - price: 831 - sale_gamma: 47 - service_level: 0.95 - SKU205: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 130 - init_stock: 500 - max_stock: 1000 - price: 175 - sale_gamma: 100 - service_level: 0.95 - SKU206: - constraint: null - cost: 335 - init_stock: 55 - max_stock: 1000 - price: 552 - sale_gamma: 11 - service_level: 0.95 - SKU207: - constraint: G(low_profit -> low_stock_constraint) - cost: 140 - init_stock: 240 - max_stock: 1000 - price: 170 - sale_gamma: 80 - service_level: 0.95 - SKU208: - constraint: null - cost: 491 - init_stock: 308 - max_stock: 1000 - price: 927 - sale_gamma: 77 - service_level: 0.95 - SKU209: - constraint: G(stock_constraint) - cost: 179 - init_stock: 120 - max_stock: 1000 - price: 322 - sale_gamma: 20 - service_level: 0.95 - SKU21: - constraint: null - cost: 123 - init_stock: 400 - max_stock: 1000 - price: 225 - sale_gamma: 100 - service_level: 0.95 - SKU210: - constraint: null - cost: 404 - init_stock: 345 - max_stock: 1000 - price: 468 - sale_gamma: 69 - service_level: 0.95 - SKU211: - constraint: null - cost: 174 - init_stock: 364 - max_stock: 1000 - price: 226 - sale_gamma: 91 - service_level: 0.95 - SKU212: - constraint: null - cost: 405 - init_stock: 632 - max_stock: 1000 - price: 534 - sale_gamma: 79 - service_level: 0.95 - SKU213: - constraint: null - cost: 121 - init_stock: 192 - max_stock: 1000 - price: 229 - sale_gamma: 64 - service_level: 0.95 - SKU214: - constraint: G(stock_constraint) - cost: 101 - init_stock: 60 - max_stock: 1000 - price: 144 - sale_gamma: 10 - service_level: 0.95 - SKU215: - constraint: null - cost: 419 - init_stock: 94 - max_stock: 1000 - price: 469 - sale_gamma: 47 - service_level: 0.95 - SKU216: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 330 - init_stock: 92 - max_stock: 1000 - price: 432 - sale_gamma: 23 - service_level: 0.95 - SKU217: - constraint: null - cost: 284 - init_stock: 455 - max_stock: 1000 - price: 312 - sale_gamma: 65 - service_level: 0.95 - SKU218: - constraint: G(low_profit -> low_stock_constraint) - cost: 205 - init_stock: 354 - max_stock: 1000 - price: 397 - sale_gamma: 59 - service_level: 0.95 - SKU219: - constraint: G(stock_constraint) - cost: 92 - init_stock: 368 - max_stock: 1000 - price: 135 - sale_gamma: 46 - service_level: 0.95 - SKU22: - constraint: null - cost: 493 - init_stock: 264 - max_stock: 1000 - price: 986 - sale_gamma: 66 - service_level: 0.95 - SKU220: - constraint: null - cost: 387 - init_stock: 435 - max_stock: 1000 - price: 572 - sale_gamma: 87 - service_level: 0.95 - SKU221: - constraint: null - cost: 39 - init_stock: 468 - max_stock: 1000 - price: 48 - sale_gamma: 78 - service_level: 0.95 - SKU222: - constraint: G(low_profit -> low_stock_constraint) - cost: 115 - init_stock: 180 - max_stock: 1000 - price: 210 - sale_gamma: 36 - service_level: 0.95 - SKU223: - constraint: G(low_profit -> low_stock_constraint) - cost: 196 - init_stock: 36 - max_stock: 1000 - price: 286 - sale_gamma: 12 - service_level: 0.95 - SKU224: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 387 - init_stock: 25 - max_stock: 1000 - price: 661 - sale_gamma: 5 - service_level: 0.95 - SKU225: - constraint: null - cost: 164 - init_stock: 116 - max_stock: 1000 - price: 216 - sale_gamma: 29 - service_level: 0.95 - SKU226: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 206 - init_stock: 65 - max_stock: 1000 - price: 350 - sale_gamma: 13 - service_level: 0.95 - SKU227: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 144 - max_stock: 1000 - price: 895 - sale_gamma: 24 - service_level: 0.95 - SKU228: - constraint: null - cost: 14 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 90 - service_level: 0.95 - SKU229: - constraint: null - cost: 472 - init_stock: 176 - max_stock: 1000 - price: 708 - sale_gamma: 44 - service_level: 0.95 - SKU23: - constraint: null - cost: 387 - init_stock: 210 - max_stock: 1000 - price: 700 - sale_gamma: 42 - service_level: 0.95 - SKU230: - constraint: null - cost: 241 - init_stock: 138 - max_stock: 1000 - price: 436 - sale_gamma: 23 - service_level: 0.95 - SKU231: - constraint: G(stock_constraint) - cost: 48 - init_stock: 486 - max_stock: 1000 - price: 96 - sale_gamma: 81 - service_level: 0.95 - SKU232: - constraint: G(low_profit -> low_stock_constraint) - cost: 224 - init_stock: 480 - max_stock: 1000 - price: 338 - sale_gamma: 96 - service_level: 0.95 - SKU233: - constraint: G(low_profit -> low_stock_constraint) - cost: 360 - init_stock: 300 - max_stock: 1000 - price: 428 - sale_gamma: 75 - service_level: 0.95 - SKU234: - constraint: null - cost: 287 - init_stock: 25 - max_stock: 1000 - price: 387 - sale_gamma: 5 - service_level: 0.95 - SKU235: - constraint: G(low_profit -> low_stock_constraint) - cost: 24 - init_stock: 228 - max_stock: 1000 - price: 32 - sale_gamma: 57 - service_level: 0.95 - SKU236: - constraint: G(low_profit -> low_stock_constraint) - cost: 155 - init_stock: 165 - max_stock: 1000 - price: 289 - sale_gamma: 55 - service_level: 0.95 - SKU237: - constraint: null - cost: 433 - init_stock: 270 - max_stock: 1000 - price: 779 - sale_gamma: 45 - service_level: 0.95 - SKU238: - constraint: G(stock_constraint) - cost: 64 - init_stock: 264 - max_stock: 1000 - price: 112 - sale_gamma: 66 - service_level: 0.95 - SKU239: - constraint: null - cost: 103 - init_stock: 490 - max_stock: 1000 - price: 139 - sale_gamma: 98 - service_level: 0.95 - SKU24: - constraint: null - cost: 97 - init_stock: 329 - max_stock: 1000 - price: 114 - sale_gamma: 47 - service_level: 0.95 - SKU240: - constraint: null - cost: 373 - init_stock: 188 - max_stock: 1000 - price: 738 - sale_gamma: 47 - service_level: 0.95 - SKU241: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 213 - max_stock: 1000 - price: 860 - sale_gamma: 71 - service_level: 0.95 - SKU242: - constraint: null - cost: 17 - init_stock: 88 - max_stock: 1000 - price: 29 - sale_gamma: 44 - service_level: 0.95 - SKU243: - constraint: null - cost: 352 - init_stock: 84 - max_stock: 1000 - price: 394 - sale_gamma: 14 - service_level: 0.95 - SKU244: - constraint: null - cost: 174 - init_stock: 410 - max_stock: 1000 - price: 226 - sale_gamma: 82 - service_level: 0.95 - SKU245: - constraint: G(low_profit -> low_stock_constraint) - cost: 404 - init_stock: 198 - max_stock: 1000 - price: 525 - sale_gamma: 66 - service_level: 0.95 - SKU246: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 300 - init_stock: 210 - max_stock: 1000 - price: 474 - sale_gamma: 30 - service_level: 0.95 - SKU247: - constraint: null - cost: 386 - init_stock: 210 - max_stock: 1000 - price: 497 - sale_gamma: 35 - service_level: 0.95 - SKU248: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 355 - init_stock: 174 - max_stock: 1000 - price: 539 - sale_gamma: 29 - service_level: 0.95 - SKU249: - constraint: null - cost: 356 - init_stock: 100 - max_stock: 1000 - price: 544 - sale_gamma: 25 - service_level: 0.95 - SKU25: - constraint: G(stock_constraint) - cost: 161 - init_stock: 35 - max_stock: 1000 - price: 249 - sale_gamma: 5 - service_level: 0.95 - SKU250: - constraint: null - cost: 127 - init_stock: 324 - max_stock: 1000 - price: 186 - sale_gamma: 54 - service_level: 0.95 - SKU251: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 344 - init_stock: 244 - max_stock: 1000 - price: 543 - sale_gamma: 61 - service_level: 0.95 - SKU252: - constraint: null - cost: 181 - init_stock: 415 - max_stock: 1000 - price: 334 - sale_gamma: 83 - service_level: 0.95 - SKU253: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 448 - init_stock: 32 - max_stock: 1000 - price: 864 - sale_gamma: 16 - service_level: 0.95 - SKU254: - constraint: null - cost: 484 - init_stock: 184 - max_stock: 1000 - price: 696 - sale_gamma: 46 - service_level: 0.95 - SKU255: - constraint: null - cost: 290 - init_stock: 335 - max_stock: 1000 - price: 568 - sale_gamma: 67 - service_level: 0.95 - SKU256: - constraint: null - cost: 91 - init_stock: 360 - max_stock: 1000 - price: 167 - sale_gamma: 72 - service_level: 0.95 - SKU257: - constraint: null - cost: 348 - init_stock: 171 - max_stock: 1000 - price: 497 - sale_gamma: 57 - service_level: 0.95 - SKU258: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 258 - max_stock: 1000 - price: 689 - sale_gamma: 43 - service_level: 0.95 - SKU259: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 207 - max_stock: 1000 - price: 559 - sale_gamma: 69 - service_level: 0.95 - SKU26: - constraint: null - cost: 229 - init_stock: 126 - max_stock: 1000 - price: 357 - sale_gamma: 63 - service_level: 0.95 - SKU260: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 364 - max_stock: 1000 - price: 866 - sale_gamma: 52 - service_level: 0.95 - SKU261: - constraint: null - cost: 368 - init_stock: 66 - max_stock: 1000 - price: 688 - sale_gamma: 22 - service_level: 0.95 - SKU262: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 332 - init_stock: 390 - max_stock: 1000 - price: 385 - sale_gamma: 78 - service_level: 0.95 - SKU263: - constraint: G(stock_constraint) - cost: 189 - init_stock: 444 - max_stock: 1000 - price: 372 - sale_gamma: 74 - service_level: 0.95 - SKU264: - constraint: null - cost: 361 - init_stock: 158 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU265: - constraint: null - cost: 286 - init_stock: 236 - max_stock: 1000 - price: 434 - sale_gamma: 59 - service_level: 0.95 - SKU266: - constraint: null - cost: 128 - init_stock: 282 - max_stock: 1000 - price: 172 - sale_gamma: 47 - service_level: 0.95 - SKU267: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 77 - init_stock: 320 - max_stock: 1000 - price: 130 - sale_gamma: 80 - service_level: 0.95 - SKU268: - constraint: null - cost: 221 - init_stock: 356 - max_stock: 1000 - price: 362 - sale_gamma: 89 - service_level: 0.95 - SKU269: - constraint: G(low_profit -> low_stock_constraint) - cost: 126 - init_stock: 132 - max_stock: 1000 - price: 175 - sale_gamma: 44 - service_level: 0.95 - SKU27: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 448 - max_stock: 1000 - price: 728 - sale_gamma: 56 - service_level: 0.95 - SKU270: - constraint: null - cost: 182 - init_stock: 370 - max_stock: 1000 - price: 263 - sale_gamma: 74 - service_level: 0.95 - SKU271: - constraint: G(stock_constraint) - cost: 230 - init_stock: 54 - max_stock: 1000 - price: 266 - sale_gamma: 18 - service_level: 0.95 - SKU272: - constraint: null - cost: 366 - init_stock: 51 - max_stock: 1000 - price: 625 - sale_gamma: 17 - service_level: 0.95 - SKU273: - constraint: null - cost: 421 - init_stock: 72 - max_stock: 1000 - price: 614 - sale_gamma: 18 - service_level: 0.95 - SKU274: - constraint: null - cost: 29 - init_stock: 308 - max_stock: 1000 - price: 42 - sale_gamma: 77 - service_level: 0.95 - SKU275: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 50 - init_stock: 144 - max_stock: 1000 - price: 76 - sale_gamma: 48 - service_level: 0.95 - SKU276: - constraint: G(stock_constraint) - cost: 163 - init_stock: 378 - max_stock: 1000 - price: 299 - sale_gamma: 54 - service_level: 0.95 - SKU277: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 82 - max_stock: 1000 - price: 507 - sale_gamma: 41 - service_level: 0.95 - SKU278: - constraint: null - cost: 105 - init_stock: 84 - max_stock: 1000 - price: 140 - sale_gamma: 12 - service_level: 0.95 - SKU279: - constraint: G(stock_constraint) - cost: 51 - init_stock: 273 - max_stock: 1000 - price: 61 - sale_gamma: 39 - service_level: 0.95 - SKU28: - constraint: G(stock_constraint) - cost: 208 - init_stock: 235 - max_stock: 1000 - price: 295 - sale_gamma: 47 - service_level: 0.95 - SKU280: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 198 - max_stock: 1000 - price: 436 - sale_gamma: 33 - service_level: 0.95 - SKU281: - constraint: null - cost: 395 - init_stock: 375 - max_stock: 1000 - price: 592 - sale_gamma: 75 - service_level: 0.95 - SKU282: - constraint: null - cost: 63 - init_stock: 184 - max_stock: 1000 - price: 112 - sale_gamma: 46 - service_level: 0.95 - SKU283: - constraint: null - cost: 392 - init_stock: 736 - max_stock: 1000 - price: 490 - sale_gamma: 92 - service_level: 0.95 - SKU284: - constraint: G(stock_constraint) - cost: 344 - init_stock: 268 - max_stock: 1000 - price: 643 - sale_gamma: 67 - service_level: 0.95 - SKU285: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 133 - init_stock: 546 - max_stock: 1000 - price: 148 - sale_gamma: 91 - service_level: 0.95 - SKU286: - constraint: null - cost: 337 - init_stock: 156 - max_stock: 1000 - price: 626 - sale_gamma: 39 - service_level: 0.95 - SKU287: - constraint: null - cost: 375 - init_stock: 224 - max_stock: 1000 - price: 660 - sale_gamma: 56 - service_level: 0.95 - SKU288: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 190 - max_stock: 1000 - price: 352 - sale_gamma: 38 - service_level: 0.95 - SKU289: - constraint: G(stock_constraint) - cost: 67 - init_stock: 62 - max_stock: 1000 - price: 91 - sale_gamma: 31 - service_level: 0.95 - SKU29: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 475 - sale_gamma: 58 - service_level: 0.95 - SKU290: - constraint: null - cost: 438 - init_stock: 268 - max_stock: 1000 - price: 779 - sale_gamma: 67 - service_level: 0.95 - SKU291: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 122 - max_stock: 1000 - price: 126 - sale_gamma: 61 - service_level: 0.95 - SKU292: - constraint: null - cost: 186 - init_stock: 15 - max_stock: 1000 - price: 344 - sale_gamma: 5 - service_level: 0.95 - SKU293: - constraint: G(stock_constraint) - cost: 162 - init_stock: 385 - max_stock: 1000 - price: 288 - sale_gamma: 55 - service_level: 0.95 - SKU294: - constraint: null - cost: 409 - init_stock: 45 - max_stock: 1000 - price: 605 - sale_gamma: 9 - service_level: 0.95 - SKU295: - constraint: null - cost: 435 - init_stock: 651 - max_stock: 1000 - price: 674 - sale_gamma: 93 - service_level: 0.95 - SKU296: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 552 - max_stock: 1000 - price: 580 - sale_gamma: 92 - service_level: 0.95 - SKU297: - constraint: null - cost: 298 - init_stock: 228 - max_stock: 1000 - price: 333 - sale_gamma: 38 - service_level: 0.95 - SKU298: - constraint: null - cost: 286 - init_stock: 140 - max_stock: 1000 - price: 500 - sale_gamma: 35 - service_level: 0.95 - SKU299: - constraint: G(stock_constraint) - cost: 367 - init_stock: 255 - max_stock: 1000 - price: 451 - sale_gamma: 51 - service_level: 0.95 - SKU3: - constraint: G(stock_constraint) - cost: 405 - init_stock: 48 - max_stock: 1000 - price: 733 - sale_gamma: 12 - service_level: 0.95 - SKU30: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 359 - init_stock: 276 - max_stock: 1000 - price: 581 - sale_gamma: 69 - service_level: 0.95 - SKU300: - constraint: G(low_profit -> low_stock_constraint) - cost: 376 - init_stock: 290 - max_stock: 1000 - price: 428 - sale_gamma: 58 - service_level: 0.95 - SKU301: - constraint: null - cost: 433 - init_stock: 581 - max_stock: 1000 - price: 857 - sale_gamma: 83 - service_level: 0.95 - SKU302: - constraint: null - cost: 184 - init_stock: 44 - max_stock: 1000 - price: 322 - sale_gamma: 11 - service_level: 0.95 - SKU303: - constraint: null - cost: 246 - init_stock: 188 - max_stock: 1000 - price: 487 - sale_gamma: 94 - service_level: 0.95 - SKU304: - constraint: G(low_profit -> low_stock_constraint) - cost: 419 - init_stock: 138 - max_stock: 1000 - price: 632 - sale_gamma: 23 - service_level: 0.95 - SKU305: - constraint: null - cost: 495 - init_stock: 500 - max_stock: 1000 - price: 772 - sale_gamma: 100 - service_level: 0.95 - SKU306: - constraint: null - cost: 479 - init_stock: 126 - max_stock: 1000 - price: 852 - sale_gamma: 42 - service_level: 0.95 - SKU307: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 210 - init_stock: 156 - max_stock: 1000 - price: 371 - sale_gamma: 78 - service_level: 0.95 - SKU308: - constraint: null - cost: 208 - init_stock: 25 - max_stock: 1000 - price: 262 - sale_gamma: 5 - service_level: 0.95 - SKU309: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 460 - max_stock: 1000 - price: 156 - sale_gamma: 92 - service_level: 0.95 - SKU31: - constraint: null - cost: 69 - init_stock: 280 - max_stock: 1000 - price: 120 - sale_gamma: 56 - service_level: 0.95 - SKU310: - constraint: null - cost: 234 - init_stock: 352 - max_stock: 1000 - price: 294 - sale_gamma: 44 - service_level: 0.95 - SKU311: - constraint: null - cost: 306 - init_stock: 400 - max_stock: 1000 - price: 437 - sale_gamma: 80 - service_level: 0.95 - SKU312: - constraint: G(stock_constraint) - cost: 291 - init_stock: 75 - max_stock: 1000 - price: 392 - sale_gamma: 25 - service_level: 0.95 - SKU313: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 324 - init_stock: 190 - max_stock: 1000 - price: 492 - sale_gamma: 38 - service_level: 0.95 - SKU314: - constraint: G(stock_constraint) - cost: 404 - init_stock: 116 - max_stock: 1000 - price: 456 - sale_gamma: 29 - service_level: 0.95 - SKU315: - constraint: G(stock_constraint) - cost: 471 - init_stock: 504 - max_stock: 1000 - price: 885 - sale_gamma: 84 - service_level: 0.95 - SKU316: - constraint: null - cost: 202 - init_stock: 90 - max_stock: 1000 - price: 282 - sale_gamma: 18 - service_level: 0.95 - SKU317: - constraint: null - cost: 216 - init_stock: 168 - max_stock: 1000 - price: 352 - sale_gamma: 24 - service_level: 0.95 - SKU318: - constraint: null - cost: 278 - init_stock: 255 - max_stock: 1000 - price: 350 - sale_gamma: 85 - service_level: 0.95 - SKU319: - constraint: null - cost: 257 - init_stock: 371 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU32: - constraint: null - cost: 336 - init_stock: 110 - max_stock: 1000 - price: 581 - sale_gamma: 22 - service_level: 0.95 - SKU320: - constraint: null - cost: 196 - init_stock: 156 - max_stock: 1000 - price: 258 - sale_gamma: 39 - service_level: 0.95 - SKU321: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 96 - max_stock: 1000 - price: 105 - sale_gamma: 16 - service_level: 0.95 - SKU322: - constraint: null - cost: 240 - init_stock: 600 - max_stock: 1000 - price: 453 - sale_gamma: 100 - service_level: 0.95 - SKU323: - constraint: G(stock_constraint) - cost: 66 - init_stock: 195 - max_stock: 1000 - price: 74 - sale_gamma: 39 - service_level: 0.95 - SKU324: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 442 - init_stock: 465 - max_stock: 1000 - price: 804 - sale_gamma: 93 - service_level: 0.95 - SKU325: - constraint: null - cost: 453 - init_stock: 345 - max_stock: 1000 - price: 579 - sale_gamma: 69 - service_level: 0.95 - SKU326: - constraint: null - cost: 345 - init_stock: 72 - max_stock: 1000 - price: 538 - sale_gamma: 24 - service_level: 0.95 - SKU327: - constraint: G(low_profit -> low_stock_constraint) - cost: 457 - init_stock: 84 - max_stock: 1000 - price: 749 - sale_gamma: 14 - service_level: 0.95 - SKU328: - constraint: null - cost: 390 - init_stock: 90 - max_stock: 1000 - price: 487 - sale_gamma: 45 - service_level: 0.95 - SKU329: - constraint: null - cost: 67 - init_stock: 84 - max_stock: 1000 - price: 126 - sale_gamma: 42 - service_level: 0.95 - SKU33: - constraint: G(low_profit -> low_stock_constraint) - cost: 416 - init_stock: 552 - max_stock: 1000 - price: 465 - sale_gamma: 92 - service_level: 0.95 - SKU330: - constraint: null - cost: 306 - init_stock: 273 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU331: - constraint: G(low_profit -> low_stock_constraint) - cost: 138 - init_stock: 164 - max_stock: 1000 - price: 180 - sale_gamma: 41 - service_level: 0.95 - SKU332: - constraint: null - cost: 390 - init_stock: 288 - max_stock: 1000 - price: 670 - sale_gamma: 96 - service_level: 0.95 - SKU333: - constraint: G(stock_constraint) - cost: 485 - init_stock: 318 - max_stock: 1000 - price: 800 - sale_gamma: 53 - service_level: 0.95 - SKU334: - constraint: null - cost: 183 - init_stock: 114 - max_stock: 1000 - price: 245 - sale_gamma: 57 - service_level: 0.95 - SKU335: - constraint: null - cost: 80 - init_stock: 243 - max_stock: 1000 - price: 141 - sale_gamma: 81 - service_level: 0.95 - SKU336: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 56 - max_stock: 1000 - price: 565 - sale_gamma: 28 - service_level: 0.95 - SKU337: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 394 - sale_gamma: 29 - service_level: 0.95 - SKU338: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 150 - sale_gamma: 81 - service_level: 0.95 - SKU339: - constraint: G(stock_constraint) - cost: 265 - init_stock: 441 - max_stock: 1000 - price: 368 - sale_gamma: 63 - service_level: 0.95 - SKU34: - constraint: null - cost: 95 - init_stock: 91 - max_stock: 1000 - price: 135 - sale_gamma: 13 - service_level: 0.95 - SKU340: - constraint: null - cost: 480 - init_stock: 435 - max_stock: 1000 - price: 633 - sale_gamma: 87 - service_level: 0.95 - SKU341: - constraint: G(low_profit -> low_stock_constraint) - cost: 462 - init_stock: 280 - max_stock: 1000 - price: 924 - sale_gamma: 70 - service_level: 0.95 - SKU342: - constraint: null - cost: 144 - init_stock: 72 - max_stock: 1000 - price: 216 - sale_gamma: 9 - service_level: 0.95 - SKU343: - constraint: null - cost: 310 - init_stock: 90 - max_stock: 1000 - price: 589 - sale_gamma: 15 - service_level: 0.95 - SKU344: - constraint: null - cost: 357 - init_stock: 348 - max_stock: 1000 - price: 442 - sale_gamma: 87 - service_level: 0.95 - SKU345: - constraint: null - cost: 442 - init_stock: 267 - max_stock: 1000 - price: 857 - sale_gamma: 89 - service_level: 0.95 - SKU346: - constraint: G(stock_constraint) - cost: 350 - init_stock: 336 - max_stock: 1000 - price: 549 - sale_gamma: 42 - service_level: 0.95 - SKU347: - constraint: G(low_profit -> low_stock_constraint) - cost: 497 - init_stock: 328 - max_stock: 1000 - price: 810 - sale_gamma: 82 - service_level: 0.95 - SKU348: - constraint: G(stock_constraint) - cost: 147 - init_stock: 100 - max_stock: 1000 - price: 277 - sale_gamma: 20 - service_level: 0.95 - SKU349: - constraint: null - cost: 67 - init_stock: 335 - max_stock: 1000 - price: 116 - sale_gamma: 67 - service_level: 0.95 - SKU35: - constraint: null - cost: 267 - init_stock: 430 - max_stock: 1000 - price: 373 - sale_gamma: 86 - service_level: 0.95 - SKU350: - constraint: G(stock_constraint) - cost: 279 - init_stock: 552 - max_stock: 1000 - price: 460 - sale_gamma: 92 - service_level: 0.95 - SKU351: - constraint: null - cost: 155 - init_stock: 335 - max_stock: 1000 - price: 294 - sale_gamma: 67 - service_level: 0.95 - SKU352: - constraint: null - cost: 71 - init_stock: 54 - max_stock: 1000 - price: 100 - sale_gamma: 18 - service_level: 0.95 - SKU353: - constraint: G(stock_constraint) - cost: 253 - init_stock: 465 - max_stock: 1000 - price: 437 - sale_gamma: 93 - service_level: 0.95 - SKU354: - constraint: G(low_profit -> low_stock_constraint) - cost: 396 - init_stock: 395 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU355: - constraint: G(stock_constraint) - cost: 63 - init_stock: 30 - max_stock: 1000 - price: 74 - sale_gamma: 10 - service_level: 0.95 - SKU356: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 87 - max_stock: 1000 - price: 441 - sale_gamma: 29 - service_level: 0.95 - SKU357: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 499 - init_stock: 460 - max_stock: 1000 - price: 868 - sale_gamma: 92 - service_level: 0.95 - SKU358: - constraint: null - cost: 269 - init_stock: 414 - max_stock: 1000 - price: 408 - sale_gamma: 69 - service_level: 0.95 - SKU359: - constraint: G(low_profit -> low_stock_constraint) - cost: 82 - init_stock: 600 - max_stock: 1000 - price: 110 - sale_gamma: 75 - service_level: 0.95 - SKU36: - constraint: null - cost: 105 - init_stock: 30 - max_stock: 1000 - price: 165 - sale_gamma: 10 - service_level: 0.95 - SKU360: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 75 - max_stock: 1000 - price: 682 - sale_gamma: 25 - service_level: 0.95 - SKU361: - constraint: null - cost: 163 - init_stock: 360 - max_stock: 1000 - price: 288 - sale_gamma: 90 - service_level: 0.95 - SKU362: - constraint: null - cost: 464 - init_stock: 435 - max_stock: 1000 - price: 867 - sale_gamma: 87 - service_level: 0.95 - SKU363: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 390 - max_stock: 1000 - price: 93 - sale_gamma: 78 - service_level: 0.95 - SKU364: - constraint: G(stock_constraint) - cost: 387 - init_stock: 66 - max_stock: 1000 - price: 472 - sale_gamma: 33 - service_level: 0.95 - SKU365: - constraint: null - cost: 158 - init_stock: 581 - max_stock: 1000 - price: 241 - sale_gamma: 83 - service_level: 0.95 - SKU366: - constraint: null - cost: 444 - init_stock: 344 - max_stock: 1000 - price: 639 - sale_gamma: 86 - service_level: 0.95 - SKU367: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 322 - max_stock: 1000 - price: 304 - sale_gamma: 46 - service_level: 0.95 - SKU368: - constraint: G(stock_constraint) - cost: 472 - init_stock: 198 - max_stock: 1000 - price: 613 - sale_gamma: 33 - service_level: 0.95 - SKU369: - constraint: null - cost: 96 - init_stock: 375 - max_stock: 1000 - price: 155 - sale_gamma: 75 - service_level: 0.95 - SKU37: - constraint: G(low_profit -> low_stock_constraint) - cost: 66 - init_stock: 177 - max_stock: 1000 - price: 110 - sale_gamma: 59 - service_level: 0.95 - SKU370: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 90 - max_stock: 1000 - price: 141 - sale_gamma: 15 - service_level: 0.95 - SKU371: - constraint: null - cost: 328 - init_stock: 25 - max_stock: 1000 - price: 511 - sale_gamma: 5 - service_level: 0.95 - SKU372: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 67 - init_stock: 192 - max_stock: 1000 - price: 134 - sale_gamma: 32 - service_level: 0.95 - SKU373: - constraint: null - cost: 58 - init_stock: 268 - max_stock: 1000 - price: 91 - sale_gamma: 67 - service_level: 0.95 - SKU374: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 216 - max_stock: 1000 - price: 73 - sale_gamma: 54 - service_level: 0.95 - SKU375: - constraint: G(low_profit -> low_stock_constraint) - cost: 283 - init_stock: 432 - max_stock: 1000 - price: 416 - sale_gamma: 72 - service_level: 0.95 - SKU376: - constraint: null - cost: 156 - init_stock: 164 - max_stock: 1000 - price: 291 - sale_gamma: 82 - service_level: 0.95 - SKU377: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 78 - init_stock: 536 - max_stock: 1000 - price: 134 - sale_gamma: 67 - service_level: 0.95 - SKU378: - constraint: G(low_profit -> low_stock_constraint) - cost: 424 - init_stock: 175 - max_stock: 1000 - price: 546 - sale_gamma: 35 - service_level: 0.95 - SKU379: - constraint: null - cost: 11 - init_stock: 196 - max_stock: 1000 - price: 20 - sale_gamma: 49 - service_level: 0.95 - SKU38: - constraint: null - cost: 344 - init_stock: 258 - max_stock: 1000 - price: 567 - sale_gamma: 43 - service_level: 0.95 - SKU380: - constraint: null - cost: 393 - init_stock: 212 - max_stock: 1000 - price: 605 - sale_gamma: 53 - service_level: 0.95 - SKU381: - constraint: null - cost: 476 - init_stock: 18 - max_stock: 1000 - price: 609 - sale_gamma: 6 - service_level: 0.95 - SKU382: - constraint: null - cost: 125 - init_stock: 426 - max_stock: 1000 - price: 177 - sale_gamma: 71 - service_level: 0.95 - SKU383: - constraint: null - cost: 304 - init_stock: 368 - max_stock: 1000 - price: 425 - sale_gamma: 92 - service_level: 0.95 - SKU384: - constraint: null - cost: 320 - init_stock: 54 - max_stock: 1000 - price: 460 - sale_gamma: 9 - service_level: 0.95 - SKU385: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 120 - init_stock: 91 - max_stock: 1000 - price: 184 - sale_gamma: 13 - service_level: 0.95 - SKU386: - constraint: G(stock_constraint) - cost: 295 - init_stock: 124 - max_stock: 1000 - price: 536 - sale_gamma: 31 - service_level: 0.95 - SKU387: - constraint: null - cost: 112 - init_stock: 582 - max_stock: 1000 - price: 154 - sale_gamma: 97 - service_level: 0.95 - SKU388: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 405 - init_stock: 264 - max_stock: 1000 - price: 635 - sale_gamma: 44 - service_level: 0.95 - SKU389: - constraint: G(stock_constraint) - cost: 376 - init_stock: 490 - max_stock: 1000 - price: 699 - sale_gamma: 70 - service_level: 0.95 - SKU39: - constraint: G(low_profit -> low_stock_constraint) - cost: 25 - init_stock: 204 - max_stock: 1000 - price: 45 - sale_gamma: 51 - service_level: 0.95 - SKU390: - constraint: null - cost: 144 - init_stock: 156 - max_stock: 1000 - price: 223 - sale_gamma: 39 - service_level: 0.95 - SKU391: - constraint: G(stock_constraint) - cost: 233 - init_stock: 402 - max_stock: 1000 - price: 403 - sale_gamma: 67 - service_level: 0.95 - SKU392: - constraint: null - cost: 163 - init_stock: 370 - max_stock: 1000 - price: 205 - sale_gamma: 74 - service_level: 0.95 - SKU393: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 402 - max_stock: 1000 - price: 701 - sale_gamma: 67 - service_level: 0.95 - SKU394: - constraint: null - cost: 154 - init_stock: 318 - max_stock: 1000 - price: 252 - sale_gamma: 53 - service_level: 0.95 - SKU395: - constraint: null - cost: 488 - init_stock: 198 - max_stock: 1000 - price: 854 - sale_gamma: 33 - service_level: 0.95 - SKU396: - constraint: null - cost: 333 - init_stock: 427 - max_stock: 1000 - price: 469 - sale_gamma: 61 - service_level: 0.95 - SKU397: - constraint: null - cost: 460 - init_stock: 153 - max_stock: 1000 - price: 791 - sale_gamma: 51 - service_level: 0.95 - SKU398: - constraint: G(stock_constraint) - cost: 234 - init_stock: 174 - max_stock: 1000 - price: 414 - sale_gamma: 58 - service_level: 0.95 - SKU399: - constraint: G(stock_constraint) - cost: 376 - init_stock: 148 - max_stock: 1000 - price: 612 - sale_gamma: 37 - service_level: 0.95 - SKU4: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 21 - max_stock: 1000 - price: 798 - sale_gamma: 7 - service_level: 0.95 - SKU40: - constraint: null - cost: 490 - init_stock: 594 - max_stock: 1000 - price: 563 - sale_gamma: 99 - service_level: 0.95 - SKU400: - constraint: G(low_profit -> low_stock_constraint) - cost: 445 - init_stock: 420 - max_stock: 1000 - price: 729 - sale_gamma: 84 - service_level: 0.95 - SKU401: - constraint: G(low_profit -> low_stock_constraint) - cost: 410 - init_stock: 312 - max_stock: 1000 - price: 774 - sale_gamma: 78 - service_level: 0.95 - SKU402: - constraint: G(low_profit -> low_stock_constraint) - cost: 86 - init_stock: 35 - max_stock: 1000 - price: 153 - sale_gamma: 7 - service_level: 0.95 - SKU403: - constraint: null - cost: 89 - init_stock: 594 - max_stock: 1000 - price: 133 - sale_gamma: 99 - service_level: 0.95 - SKU404: - constraint: null - cost: 287 - init_stock: 305 - max_stock: 1000 - price: 522 - sale_gamma: 61 - service_level: 0.95 - SKU405: - constraint: G(stock_constraint) - cost: 460 - init_stock: 114 - max_stock: 1000 - price: 584 - sale_gamma: 19 - service_level: 0.95 - SKU406: - constraint: null - cost: 327 - init_stock: 400 - max_stock: 1000 - price: 405 - sale_gamma: 100 - service_level: 0.95 - SKU407: - constraint: null - cost: 26 - init_stock: 184 - max_stock: 1000 - price: 41 - sale_gamma: 46 - service_level: 0.95 - SKU408: - constraint: null - cost: 444 - init_stock: 48 - max_stock: 1000 - price: 754 - sale_gamma: 8 - service_level: 0.95 - SKU409: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 257 - init_stock: 273 - max_stock: 1000 - price: 449 - sale_gamma: 91 - service_level: 0.95 - SKU41: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 141 - init_stock: 553 - max_stock: 1000 - price: 162 - sale_gamma: 79 - service_level: 0.95 - SKU410: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 70 - init_stock: 32 - max_stock: 1000 - price: 88 - sale_gamma: 16 - service_level: 0.95 - SKU411: - constraint: G(stock_constraint) - cost: 210 - init_stock: 380 - max_stock: 1000 - price: 405 - sale_gamma: 95 - service_level: 0.95 - SKU412: - constraint: null - cost: 286 - init_stock: 248 - max_stock: 1000 - price: 414 - sale_gamma: 62 - service_level: 0.95 - SKU413: - constraint: null - cost: 403 - init_stock: 581 - max_stock: 1000 - price: 801 - sale_gamma: 83 - service_level: 0.95 - SKU414: - constraint: G(stock_constraint) - cost: 165 - init_stock: 435 - max_stock: 1000 - price: 229 - sale_gamma: 87 - service_level: 0.95 - SKU415: - constraint: G(stock_constraint) - cost: 291 - init_stock: 184 - max_stock: 1000 - price: 372 - sale_gamma: 23 - service_level: 0.95 - SKU416: - constraint: null - cost: 228 - init_stock: 36 - max_stock: 1000 - price: 373 - sale_gamma: 9 - service_level: 0.95 - SKU417: - constraint: G(low_profit -> low_stock_constraint) - cost: 443 - init_stock: 288 - max_stock: 1000 - price: 872 - sale_gamma: 72 - service_level: 0.95 - SKU418: - constraint: null - cost: 458 - init_stock: 52 - max_stock: 1000 - price: 838 - sale_gamma: 13 - service_level: 0.95 - SKU419: - constraint: null - cost: 303 - init_stock: 712 - max_stock: 1000 - price: 448 - sale_gamma: 89 - service_level: 0.95 - SKU42: - constraint: null - cost: 462 - init_stock: 156 - max_stock: 1000 - price: 688 - sale_gamma: 26 - service_level: 0.95 - SKU420: - constraint: null - cost: 268 - init_stock: 84 - max_stock: 1000 - price: 506 - sale_gamma: 42 - service_level: 0.95 - SKU421: - constraint: G(stock_constraint) - cost: 214 - init_stock: 138 - max_stock: 1000 - price: 314 - sale_gamma: 46 - service_level: 0.95 - SKU422: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 432 - max_stock: 1000 - price: 97 - sale_gamma: 54 - service_level: 0.95 - SKU423: - constraint: G(low_profit -> low_stock_constraint) - cost: 188 - init_stock: 396 - max_stock: 1000 - price: 265 - sale_gamma: 66 - service_level: 0.95 - SKU424: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 66 - max_stock: 1000 - price: 317 - sale_gamma: 11 - service_level: 0.95 - SKU425: - constraint: G(stock_constraint) - cost: 162 - init_stock: 72 - max_stock: 1000 - price: 277 - sale_gamma: 12 - service_level: 0.95 - SKU426: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 125 - init_stock: 588 - max_stock: 1000 - price: 246 - sale_gamma: 98 - service_level: 0.95 - SKU427: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 413 - init_stock: 282 - max_stock: 1000 - price: 710 - sale_gamma: 94 - service_level: 0.95 - SKU428: - constraint: G(stock_constraint) - cost: 391 - init_stock: 378 - max_stock: 1000 - price: 629 - sale_gamma: 63 - service_level: 0.95 - SKU429: - constraint: G(stock_constraint) - cost: 39 - init_stock: 246 - max_stock: 1000 - price: 73 - sale_gamma: 41 - service_level: 0.95 - SKU43: - constraint: G(stock_constraint) - cost: 217 - init_stock: 272 - max_stock: 1000 - price: 353 - sale_gamma: 68 - service_level: 0.95 - SKU430: - constraint: null - cost: 216 - init_stock: 511 - max_stock: 1000 - price: 313 - sale_gamma: 73 - service_level: 0.95 - SKU431: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 328 - init_stock: 126 - max_stock: 1000 - price: 646 - sale_gamma: 21 - service_level: 0.95 - SKU432: - constraint: G(stock_constraint) - cost: 25 - init_stock: 368 - max_stock: 1000 - price: 35 - sale_gamma: 46 - service_level: 0.95 - SKU433: - constraint: null - cost: 348 - init_stock: 270 - max_stock: 1000 - price: 396 - sale_gamma: 45 - service_level: 0.95 - SKU434: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 90 - max_stock: 1000 - price: 40 - sale_gamma: 30 - service_level: 0.95 - SKU435: - constraint: G(stock_constraint) - cost: 272 - init_stock: 210 - max_stock: 1000 - price: 320 - sale_gamma: 30 - service_level: 0.95 - SKU436: - constraint: null - cost: 72 - init_stock: 405 - max_stock: 1000 - price: 105 - sale_gamma: 81 - service_level: 0.95 - SKU437: - constraint: G(stock_constraint) - cost: 115 - init_stock: 84 - max_stock: 1000 - price: 223 - sale_gamma: 14 - service_level: 0.95 - SKU438: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 40 - max_stock: 1000 - price: 190 - sale_gamma: 10 - service_level: 0.95 - SKU439: - constraint: G(stock_constraint) - cost: 256 - init_stock: 190 - max_stock: 1000 - price: 304 - sale_gamma: 38 - service_level: 0.95 - SKU44: - constraint: null - cost: 360 - init_stock: 240 - max_stock: 1000 - price: 669 - sale_gamma: 48 - service_level: 0.95 - SKU440: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 248 - init_stock: 246 - max_stock: 1000 - price: 357 - sale_gamma: 82 - service_level: 0.95 - SKU441: - constraint: null - cost: 253 - init_stock: 224 - max_stock: 1000 - price: 374 - sale_gamma: 56 - service_level: 0.95 - SKU442: - constraint: null - cost: 470 - init_stock: 352 - max_stock: 1000 - price: 629 - sale_gamma: 88 - service_level: 0.95 - SKU443: - constraint: null - cost: 218 - init_stock: 365 - max_stock: 1000 - price: 361 - sale_gamma: 73 - service_level: 0.95 - SKU444: - constraint: null - cost: 156 - init_stock: 18 - max_stock: 1000 - price: 182 - sale_gamma: 6 - service_level: 0.95 - SKU445: - constraint: null - cost: 260 - init_stock: 602 - max_stock: 1000 - price: 296 - sale_gamma: 86 - service_level: 0.95 - SKU446: - constraint: null - cost: 497 - init_stock: 147 - max_stock: 1000 - price: 690 - sale_gamma: 49 - service_level: 0.95 - SKU447: - constraint: null - cost: 196 - init_stock: 30 - max_stock: 1000 - price: 280 - sale_gamma: 5 - service_level: 0.95 - SKU448: - constraint: null - cost: 485 - init_stock: 497 - max_stock: 1000 - price: 742 - sale_gamma: 71 - service_level: 0.95 - SKU449: - constraint: null - cost: 282 - init_stock: 114 - max_stock: 1000 - price: 360 - sale_gamma: 38 - service_level: 0.95 - SKU45: - constraint: G(stock_constraint) - cost: 253 - init_stock: 30 - max_stock: 1000 - price: 351 - sale_gamma: 10 - service_level: 0.95 - SKU450: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 58 - init_stock: 294 - max_stock: 1000 - price: 77 - sale_gamma: 98 - service_level: 0.95 - SKU451: - constraint: null - cost: 468 - init_stock: 483 - max_stock: 1000 - price: 851 - sale_gamma: 69 - service_level: 0.95 - SKU452: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 63 - init_stock: 395 - max_stock: 1000 - price: 97 - sale_gamma: 79 - service_level: 0.95 - SKU453: - constraint: null - cost: 37 - init_stock: 140 - max_stock: 1000 - price: 65 - sale_gamma: 35 - service_level: 0.95 - SKU454: - constraint: G(low_profit -> low_stock_constraint) - cost: 494 - init_stock: 340 - max_stock: 1000 - price: 899 - sale_gamma: 85 - service_level: 0.95 - SKU455: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 136 - init_stock: 182 - max_stock: 1000 - price: 195 - sale_gamma: 26 - service_level: 0.95 - SKU456: - constraint: G(stock_constraint) - cost: 338 - init_stock: 75 - max_stock: 1000 - price: 659 - sale_gamma: 25 - service_level: 0.95 - SKU457: - constraint: null - cost: 169 - init_stock: 256 - max_stock: 1000 - price: 190 - sale_gamma: 64 - service_level: 0.95 - SKU458: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 403 - init_stock: 28 - max_stock: 1000 - price: 685 - sale_gamma: 7 - service_level: 0.95 - SKU459: - constraint: null - cost: 425 - init_stock: 272 - max_stock: 1000 - price: 731 - sale_gamma: 34 - service_level: 0.95 - SKU46: - constraint: G(low_profit -> low_stock_constraint) - cost: 299 - init_stock: 200 - max_stock: 1000 - price: 409 - sale_gamma: 50 - service_level: 0.95 - SKU460: - constraint: G(low_profit -> low_stock_constraint) - cost: 405 - init_stock: 292 - max_stock: 1000 - price: 558 - sale_gamma: 73 - service_level: 0.95 - SKU461: - constraint: G(stock_constraint) - cost: 251 - init_stock: 240 - max_stock: 1000 - price: 326 - sale_gamma: 48 - service_level: 0.95 - SKU462: - constraint: G(low_profit -> low_stock_constraint) - cost: 448 - init_stock: 36 - max_stock: 1000 - price: 757 - sale_gamma: 9 - service_level: 0.95 - SKU463: - constraint: null - cost: 187 - init_stock: 574 - max_stock: 1000 - price: 213 - sale_gamma: 82 - service_level: 0.95 - SKU464: - constraint: null - cost: 279 - init_stock: 272 - max_stock: 1000 - price: 438 - sale_gamma: 34 - service_level: 0.95 - SKU465: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 147 - init_stock: 400 - max_stock: 1000 - price: 288 - sale_gamma: 100 - service_level: 0.95 - SKU466: - constraint: G(stock_constraint) - cost: 97 - init_stock: 126 - max_stock: 1000 - price: 167 - sale_gamma: 42 - service_level: 0.95 - SKU467: - constraint: G(stock_constraint) - cost: 142 - init_stock: 252 - max_stock: 1000 - price: 230 - sale_gamma: 36 - service_level: 0.95 - SKU468: - constraint: G(stock_constraint) - cost: 55 - init_stock: 250 - max_stock: 1000 - price: 104 - sale_gamma: 50 - service_level: 0.95 - SKU469: - constraint: G(stock_constraint) - cost: 178 - init_stock: 105 - max_stock: 1000 - price: 331 - sale_gamma: 15 - service_level: 0.95 - SKU47: - constraint: G(stock_constraint) - cost: 121 - init_stock: 117 - max_stock: 1000 - price: 240 - sale_gamma: 39 - service_level: 0.95 - SKU470: - constraint: G(low_profit -> low_stock_constraint) - cost: 373 - init_stock: 128 - max_stock: 1000 - price: 548 - sale_gamma: 32 - service_level: 0.95 - SKU471: - constraint: G(low_profit -> low_stock_constraint) - cost: 315 - init_stock: 568 - max_stock: 1000 - price: 387 - sale_gamma: 71 - service_level: 0.95 - SKU472: - constraint: null - cost: 421 - init_stock: 177 - max_stock: 1000 - price: 627 - sale_gamma: 59 - service_level: 0.95 - SKU473: - constraint: G(low_profit -> low_stock_constraint) - cost: 195 - init_stock: 105 - max_stock: 1000 - price: 323 - sale_gamma: 21 - service_level: 0.95 - SKU474: - constraint: null - cost: 448 - init_stock: 602 - max_stock: 1000 - price: 775 - sale_gamma: 86 - service_level: 0.95 - SKU475: - constraint: null - cost: 34 - init_stock: 282 - max_stock: 1000 - price: 62 - sale_gamma: 94 - service_level: 0.95 - SKU476: - constraint: null - cost: 251 - init_stock: 180 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU477: - constraint: null - cost: 289 - init_stock: 329 - max_stock: 1000 - price: 338 - sale_gamma: 47 - service_level: 0.95 - SKU478: - constraint: null - cost: 479 - init_stock: 352 - max_stock: 1000 - price: 723 - sale_gamma: 88 - service_level: 0.95 - SKU479: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 366 - init_stock: 152 - max_stock: 1000 - price: 552 - sale_gamma: 38 - service_level: 0.95 - SKU48: - constraint: null - cost: 340 - init_stock: 232 - max_stock: 1000 - price: 387 - sale_gamma: 58 - service_level: 0.95 - SKU480: - constraint: G(stock_constraint) - cost: 18 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 30 - service_level: 0.95 - SKU481: - constraint: null - cost: 330 - init_stock: 352 - max_stock: 1000 - price: 422 - sale_gamma: 88 - service_level: 0.95 - SKU482: - constraint: G(stock_constraint) - cost: 94 - init_stock: 696 - max_stock: 1000 - price: 108 - sale_gamma: 87 - service_level: 0.95 - SKU483: - constraint: null - cost: 298 - init_stock: 170 - max_stock: 1000 - price: 533 - sale_gamma: 34 - service_level: 0.95 - SKU484: - constraint: null - cost: 84 - init_stock: 365 - max_stock: 1000 - price: 117 - sale_gamma: 73 - service_level: 0.95 - SKU485: - constraint: null - cost: 318 - init_stock: 536 - max_stock: 1000 - price: 543 - sale_gamma: 67 - service_level: 0.95 - SKU486: - constraint: G(low_profit -> low_stock_constraint) - cost: 122 - init_stock: 208 - max_stock: 1000 - price: 161 - sale_gamma: 52 - service_level: 0.95 - SKU487: - constraint: null - cost: 277 - init_stock: 264 - max_stock: 1000 - price: 362 - sale_gamma: 66 - service_level: 0.95 - SKU488: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 189 - max_stock: 1000 - price: 42 - sale_gamma: 27 - service_level: 0.95 - SKU489: - constraint: null - cost: 59 - init_stock: 124 - max_stock: 1000 - price: 97 - sale_gamma: 31 - service_level: 0.95 - SKU49: - constraint: null - cost: 263 - init_stock: 760 - max_stock: 1000 - price: 515 - sale_gamma: 95 - service_level: 0.95 - SKU490: - constraint: null - cost: 161 - init_stock: 486 - max_stock: 1000 - price: 264 - sale_gamma: 81 - service_level: 0.95 - SKU491: - constraint: null - cost: 444 - init_stock: 450 - max_stock: 1000 - price: 834 - sale_gamma: 75 - service_level: 0.95 - SKU492: - constraint: null - cost: 53 - init_stock: 240 - max_stock: 1000 - price: 63 - sale_gamma: 80 - service_level: 0.95 - SKU493: - constraint: null - cost: 461 - init_stock: 81 - max_stock: 1000 - price: 567 - sale_gamma: 27 - service_level: 0.95 - SKU494: - constraint: null - cost: 145 - init_stock: 282 - max_stock: 1000 - price: 242 - sale_gamma: 94 - service_level: 0.95 - SKU495: - constraint: G(stock_constraint) - cost: 149 - init_stock: 372 - max_stock: 1000 - price: 217 - sale_gamma: 62 - service_level: 0.95 - SKU496: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 342 - init_stock: 295 - max_stock: 1000 - price: 492 - sale_gamma: 59 - service_level: 0.95 - SKU497: - constraint: G(low_profit -> low_stock_constraint) - cost: 33 - init_stock: 45 - max_stock: 1000 - price: 42 - sale_gamma: 9 - service_level: 0.95 - SKU498: - constraint: null - cost: 305 - init_stock: 325 - max_stock: 1000 - price: 439 - sale_gamma: 65 - service_level: 0.95 - SKU499: - constraint: G(stock_constraint) - cost: 307 - init_stock: 174 - max_stock: 1000 - price: 472 - sale_gamma: 29 - service_level: 0.95 - SKU5: - constraint: G(stock_constraint) - cost: 466 - init_stock: 426 - max_stock: 1000 - price: 852 - sale_gamma: 71 - service_level: 0.95 - SKU50: - constraint: null - cost: 282 - init_stock: 185 - max_stock: 1000 - price: 516 - sale_gamma: 37 - service_level: 0.95 - SKU500: - constraint: null - cost: 208 - init_stock: 336 - max_stock: 1000 - price: 255 - sale_gamma: 42 - service_level: 0.95 - SKU501: - constraint: null - cost: 194 - init_stock: 152 - max_stock: 1000 - price: 265 - sale_gamma: 76 - service_level: 0.95 - SKU502: - constraint: null - cost: 69 - init_stock: 390 - max_stock: 1000 - price: 101 - sale_gamma: 65 - service_level: 0.95 - SKU503: - constraint: G(stock_constraint) - cost: 208 - init_stock: 228 - max_stock: 1000 - price: 280 - sale_gamma: 57 - service_level: 0.95 - SKU504: - constraint: null - cost: 251 - init_stock: 210 - max_stock: 1000 - price: 426 - sale_gamma: 30 - service_level: 0.95 - SKU505: - constraint: null - cost: 426 - init_stock: 295 - max_stock: 1000 - price: 515 - sale_gamma: 59 - service_level: 0.95 - SKU506: - constraint: null - cost: 149 - init_stock: 465 - max_stock: 1000 - price: 220 - sale_gamma: 93 - service_level: 0.95 - SKU507: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 187 - init_stock: 60 - max_stock: 1000 - price: 293 - sale_gamma: 15 - service_level: 0.95 - SKU508: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 576 - max_stock: 1000 - price: 432 - sale_gamma: 96 - service_level: 0.95 - SKU509: - constraint: G(low_profit -> low_stock_constraint) - cost: 297 - init_stock: 486 - max_stock: 1000 - price: 397 - sale_gamma: 81 - service_level: 0.95 - SKU51: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 469 - max_stock: 1000 - price: 477 - sale_gamma: 67 - service_level: 0.95 - SKU510: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 36 - max_stock: 1000 - price: 156 - sale_gamma: 9 - service_level: 0.95 - SKU511: - constraint: G(stock_constraint) - cost: 361 - init_stock: 450 - max_stock: 1000 - price: 480 - sale_gamma: 75 - service_level: 0.95 - SKU512: - constraint: null - cost: 467 - init_stock: 132 - max_stock: 1000 - price: 854 - sale_gamma: 22 - service_level: 0.95 - SKU513: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 343 - init_stock: 42 - max_stock: 1000 - price: 631 - sale_gamma: 7 - service_level: 0.95 - SKU514: - constraint: null - cost: 186 - init_stock: 504 - max_stock: 1000 - price: 332 - sale_gamma: 63 - service_level: 0.95 - SKU515: - constraint: null - cost: 145 - init_stock: 216 - max_stock: 1000 - price: 227 - sale_gamma: 54 - service_level: 0.95 - SKU516: - constraint: G(stock_constraint) - cost: 64 - init_stock: 114 - max_stock: 1000 - price: 96 - sale_gamma: 38 - service_level: 0.95 - SKU517: - constraint: null - cost: 117 - init_stock: 45 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU518: - constraint: G(stock_constraint) - cost: 26 - init_stock: 147 - max_stock: 1000 - price: 30 - sale_gamma: 21 - service_level: 0.95 - SKU519: - constraint: null - cost: 233 - init_stock: 250 - max_stock: 1000 - price: 410 - sale_gamma: 50 - service_level: 0.95 - SKU52: - constraint: null - cost: 22 - init_stock: 402 - max_stock: 1000 - price: 28 - sale_gamma: 67 - service_level: 0.95 - SKU520: - constraint: G(low_profit -> low_stock_constraint) - cost: 209 - init_stock: 44 - max_stock: 1000 - price: 248 - sale_gamma: 22 - service_level: 0.95 - SKU521: - constraint: G(low_profit -> low_stock_constraint) - cost: 220 - init_stock: 350 - max_stock: 1000 - price: 330 - sale_gamma: 50 - service_level: 0.95 - SKU522: - constraint: null - cost: 156 - init_stock: 522 - max_stock: 1000 - price: 277 - sale_gamma: 87 - service_level: 0.95 - SKU523: - constraint: null - cost: 65 - init_stock: 500 - max_stock: 1000 - price: 107 - sale_gamma: 100 - service_level: 0.95 - SKU524: - constraint: null - cost: 294 - init_stock: 188 - max_stock: 1000 - price: 464 - sale_gamma: 94 - service_level: 0.95 - SKU525: - constraint: G(stock_constraint) - cost: 432 - init_stock: 126 - max_stock: 1000 - price: 751 - sale_gamma: 42 - service_level: 0.95 - SKU526: - constraint: null - cost: 419 - init_stock: 168 - max_stock: 1000 - price: 716 - sale_gamma: 24 - service_level: 0.95 - SKU527: - constraint: null - cost: 131 - init_stock: 350 - max_stock: 1000 - price: 262 - sale_gamma: 70 - service_level: 0.95 - SKU528: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 84 - max_stock: 1000 - price: 581 - sale_gamma: 21 - service_level: 0.95 - SKU529: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 248 - max_stock: 1000 - price: 375 - sale_gamma: 31 - service_level: 0.95 - SKU53: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 151 - init_stock: 490 - max_stock: 1000 - price: 191 - sale_gamma: 70 - service_level: 0.95 - SKU530: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 135 - max_stock: 1000 - price: 205 - sale_gamma: 45 - service_level: 0.95 - SKU531: - constraint: null - cost: 103 - init_stock: 300 - max_stock: 1000 - price: 153 - sale_gamma: 60 - service_level: 0.95 - SKU532: - constraint: null - cost: 351 - init_stock: 539 - max_stock: 1000 - price: 431 - sale_gamma: 77 - service_level: 0.95 - SKU533: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 168 - max_stock: 1000 - price: 340 - sale_gamma: 42 - service_level: 0.95 - SKU534: - constraint: null - cost: 425 - init_stock: 82 - max_stock: 1000 - price: 548 - sale_gamma: 41 - service_level: 0.95 - SKU535: - constraint: null - cost: 304 - init_stock: 430 - max_stock: 1000 - price: 465 - sale_gamma: 86 - service_level: 0.95 - SKU536: - constraint: null - cost: 358 - init_stock: 208 - max_stock: 1000 - price: 415 - sale_gamma: 52 - service_level: 0.95 - SKU537: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 321 - init_stock: 45 - max_stock: 1000 - price: 455 - sale_gamma: 9 - service_level: 0.95 - SKU538: - constraint: null - cost: 457 - init_stock: 200 - max_stock: 1000 - price: 868 - sale_gamma: 50 - service_level: 0.95 - SKU539: - constraint: G(stock_constraint) - cost: 165 - init_stock: 234 - max_stock: 1000 - price: 229 - sale_gamma: 78 - service_level: 0.95 - SKU54: - constraint: G(low_profit -> low_stock_constraint) - cost: 158 - init_stock: 138 - max_stock: 1000 - price: 241 - sale_gamma: 46 - service_level: 0.95 - SKU540: - constraint: null - cost: 388 - init_stock: 294 - max_stock: 1000 - price: 496 - sale_gamma: 42 - service_level: 0.95 - SKU541: - constraint: null - cost: 131 - init_stock: 203 - max_stock: 1000 - price: 213 - sale_gamma: 29 - service_level: 0.95 - SKU542: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 130 - max_stock: 1000 - price: 42 - sale_gamma: 26 - service_level: 0.95 - SKU543: - constraint: G(stock_constraint) - cost: 430 - init_stock: 510 - max_stock: 1000 - price: 718 - sale_gamma: 85 - service_level: 0.95 - SKU544: - constraint: G(stock_constraint) - cost: 346 - init_stock: 194 - max_stock: 1000 - price: 474 - sale_gamma: 97 - service_level: 0.95 - SKU545: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU546: - constraint: null - cost: 491 - init_stock: 576 - max_stock: 1000 - price: 765 - sale_gamma: 96 - service_level: 0.95 - SKU547: - constraint: G(stock_constraint) - cost: 161 - init_stock: 264 - max_stock: 1000 - price: 251 - sale_gamma: 44 - service_level: 0.95 - SKU548: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 111 - init_stock: 36 - max_stock: 1000 - price: 217 - sale_gamma: 6 - service_level: 0.95 - SKU549: - constraint: G(stock_constraint) - cost: 387 - init_stock: 316 - max_stock: 1000 - price: 510 - sale_gamma: 79 - service_level: 0.95 - SKU55: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 455 - max_stock: 1000 - price: 896 - sale_gamma: 65 - service_level: 0.95 - SKU550: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 259 - init_stock: 282 - max_stock: 1000 - price: 502 - sale_gamma: 94 - service_level: 0.95 - SKU551: - constraint: null - cost: 46 - init_stock: 186 - max_stock: 1000 - price: 60 - sale_gamma: 31 - service_level: 0.95 - SKU552: - constraint: null - cost: 191 - init_stock: 450 - max_stock: 1000 - price: 315 - sale_gamma: 90 - service_level: 0.95 - SKU553: - constraint: G(low_profit -> low_stock_constraint) - cost: 208 - init_stock: 60 - max_stock: 1000 - price: 251 - sale_gamma: 30 - service_level: 0.95 - SKU554: - constraint: null - cost: 340 - init_stock: 82 - max_stock: 1000 - price: 387 - sale_gamma: 41 - service_level: 0.95 - SKU555: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 252 - max_stock: 1000 - price: 684 - sale_gamma: 84 - service_level: 0.95 - SKU556: - constraint: G(stock_constraint) - cost: 110 - init_stock: 90 - max_stock: 1000 - price: 213 - sale_gamma: 30 - service_level: 0.95 - SKU557: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 334 - init_stock: 365 - max_stock: 1000 - price: 661 - sale_gamma: 73 - service_level: 0.95 - SKU558: - constraint: null - cost: 399 - init_stock: 85 - max_stock: 1000 - price: 490 - sale_gamma: 17 - service_level: 0.95 - SKU559: - constraint: null - cost: 313 - init_stock: 270 - max_stock: 1000 - price: 591 - sale_gamma: 54 - service_level: 0.95 - SKU56: - constraint: null - cost: 377 - init_stock: 222 - max_stock: 1000 - price: 671 - sale_gamma: 74 - service_level: 0.95 - SKU560: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 283 - init_stock: 246 - max_stock: 1000 - price: 458 - sale_gamma: 41 - service_level: 0.95 - SKU561: - constraint: null - cost: 202 - init_stock: 312 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU562: - constraint: null - cost: 347 - init_stock: 356 - max_stock: 1000 - price: 666 - sale_gamma: 89 - service_level: 0.95 - SKU563: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 401 - init_stock: 30 - max_stock: 1000 - price: 509 - sale_gamma: 5 - service_level: 0.95 - SKU564: - constraint: null - cost: 109 - init_stock: 63 - max_stock: 1000 - price: 183 - sale_gamma: 9 - service_level: 0.95 - SKU565: - constraint: null - cost: 57 - init_stock: 525 - max_stock: 1000 - price: 90 - sale_gamma: 75 - service_level: 0.95 - SKU566: - constraint: null - cost: 131 - init_stock: 44 - max_stock: 1000 - price: 165 - sale_gamma: 11 - service_level: 0.95 - SKU567: - constraint: G(stock_constraint) - cost: 361 - init_stock: 27 - max_stock: 1000 - price: 465 - sale_gamma: 9 - service_level: 0.95 - SKU568: - constraint: null - cost: 75 - init_stock: 234 - max_stock: 1000 - price: 102 - sale_gamma: 78 - service_level: 0.95 - SKU569: - constraint: null - cost: 473 - init_stock: 192 - max_stock: 1000 - price: 591 - sale_gamma: 48 - service_level: 0.95 - SKU57: - constraint: G(stock_constraint) - cost: 359 - init_stock: 350 - max_stock: 1000 - price: 682 - sale_gamma: 50 - service_level: 0.95 - SKU570: - constraint: null - cost: 388 - init_stock: 581 - max_stock: 1000 - price: 686 - sale_gamma: 83 - service_level: 0.95 - SKU571: - constraint: G(stock_constraint) - cost: 168 - init_stock: 78 - max_stock: 1000 - price: 208 - sale_gamma: 39 - service_level: 0.95 - SKU572: - constraint: null - cost: 26 - init_stock: 225 - max_stock: 1000 - price: 41 - sale_gamma: 45 - service_level: 0.95 - SKU573: - constraint: null - cost: 246 - init_stock: 164 - max_stock: 1000 - price: 391 - sale_gamma: 41 - service_level: 0.95 - SKU574: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 150 - max_stock: 1000 - price: 166 - sale_gamma: 50 - service_level: 0.95 - SKU575: - constraint: null - cost: 237 - init_stock: 237 - max_stock: 1000 - price: 438 - sale_gamma: 79 - service_level: 0.95 - SKU576: - constraint: G(stock_constraint) - cost: 265 - init_stock: 468 - max_stock: 1000 - price: 416 - sale_gamma: 78 - service_level: 0.95 - SKU577: - constraint: G(low_profit -> low_stock_constraint) - cost: 18 - init_stock: 348 - max_stock: 1000 - price: 24 - sale_gamma: 87 - service_level: 0.95 - SKU578: - constraint: null - cost: 100 - init_stock: 284 - max_stock: 1000 - price: 148 - sale_gamma: 71 - service_level: 0.95 - SKU579: - constraint: null - cost: 415 - init_stock: 27 - max_stock: 1000 - price: 771 - sale_gamma: 9 - service_level: 0.95 - SKU58: - constraint: null - cost: 362 - init_stock: 240 - max_stock: 1000 - price: 521 - sale_gamma: 48 - service_level: 0.95 - SKU580: - constraint: null - cost: 203 - init_stock: 15 - max_stock: 1000 - price: 261 - sale_gamma: 5 - service_level: 0.95 - SKU581: - constraint: null - cost: 152 - init_stock: 516 - max_stock: 1000 - price: 185 - sale_gamma: 86 - service_level: 0.95 - SKU582: - constraint: G(stock_constraint) - cost: 359 - init_stock: 480 - max_stock: 1000 - price: 567 - sale_gamma: 96 - service_level: 0.95 - SKU583: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 86 - init_stock: 261 - max_stock: 1000 - price: 98 - sale_gamma: 87 - service_level: 0.95 - SKU584: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 33 - init_stock: 270 - max_stock: 1000 - price: 36 - sale_gamma: 45 - service_level: 0.95 - SKU585: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 53 - sale_gamma: 20 - service_level: 0.95 - SKU586: - constraint: G(stock_constraint) - cost: 61 - init_stock: 264 - max_stock: 1000 - price: 94 - sale_gamma: 44 - service_level: 0.95 - SKU587: - constraint: null - cost: 290 - init_stock: 468 - max_stock: 1000 - price: 423 - sale_gamma: 78 - service_level: 0.95 - SKU588: - constraint: G(stock_constraint) - cost: 476 - init_stock: 176 - max_stock: 1000 - price: 885 - sale_gamma: 44 - service_level: 0.95 - SKU589: - constraint: null - cost: 244 - init_stock: 170 - max_stock: 1000 - price: 380 - sale_gamma: 34 - service_level: 0.95 - SKU59: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 255 - max_stock: 1000 - price: 275 - sale_gamma: 51 - service_level: 0.95 - SKU590: - constraint: null - cost: 70 - init_stock: 93 - max_stock: 1000 - price: 103 - sale_gamma: 31 - service_level: 0.95 - SKU591: - constraint: G(stock_constraint) - cost: 206 - init_stock: 504 - max_stock: 1000 - price: 298 - sale_gamma: 84 - service_level: 0.95 - SKU592: - constraint: null - cost: 218 - init_stock: 276 - max_stock: 1000 - price: 418 - sale_gamma: 46 - service_level: 0.95 - SKU593: - constraint: null - cost: 124 - init_stock: 88 - max_stock: 1000 - price: 221 - sale_gamma: 44 - service_level: 0.95 - SKU594: - constraint: null - cost: 238 - init_stock: 150 - max_stock: 1000 - price: 459 - sale_gamma: 50 - service_level: 0.95 - SKU595: - constraint: null - cost: 73 - init_stock: 172 - max_stock: 1000 - price: 107 - sale_gamma: 43 - service_level: 0.95 - SKU596: - constraint: null - cost: 432 - init_stock: 35 - max_stock: 1000 - price: 540 - sale_gamma: 7 - service_level: 0.95 - SKU597: - constraint: G(stock_constraint) - cost: 252 - init_stock: 435 - max_stock: 1000 - price: 451 - sale_gamma: 87 - service_level: 0.95 - SKU598: - constraint: null - cost: 348 - init_stock: 28 - max_stock: 1000 - price: 612 - sale_gamma: 14 - service_level: 0.95 - SKU599: - constraint: null - cost: 54 - init_stock: 204 - max_stock: 1000 - price: 66 - sale_gamma: 68 - service_level: 0.95 - SKU6: - constraint: null - cost: 122 - init_stock: 616 - max_stock: 1000 - price: 195 - sale_gamma: 88 - service_level: 0.95 - SKU60: - constraint: G(stock_constraint) - cost: 200 - init_stock: 234 - max_stock: 1000 - price: 346 - sale_gamma: 39 - service_level: 0.95 - SKU600: - constraint: null - cost: 386 - init_stock: 325 - max_stock: 1000 - price: 505 - sale_gamma: 65 - service_level: 0.95 - SKU601: - constraint: null - cost: 138 - init_stock: 273 - max_stock: 1000 - price: 198 - sale_gamma: 39 - service_level: 0.95 - SKU602: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 184 - init_stock: 588 - max_stock: 1000 - price: 318 - sale_gamma: 98 - service_level: 0.95 - SKU603: - constraint: G(low_profit -> low_stock_constraint) - cost: 278 - init_stock: 252 - max_stock: 1000 - price: 550 - sale_gamma: 42 - service_level: 0.95 - SKU604: - constraint: G(stock_constraint) - cost: 270 - init_stock: 400 - max_stock: 1000 - price: 378 - sale_gamma: 50 - service_level: 0.95 - SKU605: - constraint: null - cost: 288 - init_stock: 120 - max_stock: 1000 - price: 443 - sale_gamma: 24 - service_level: 0.95 - SKU606: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 114 - init_stock: 66 - max_stock: 1000 - price: 161 - sale_gamma: 11 - service_level: 0.95 - SKU607: - constraint: null - cost: 208 - init_stock: 395 - max_stock: 1000 - price: 359 - sale_gamma: 79 - service_level: 0.95 - SKU608: - constraint: null - cost: 39 - init_stock: 365 - max_stock: 1000 - price: 49 - sale_gamma: 73 - service_level: 0.95 - SKU609: - constraint: null - cost: 102 - init_stock: 114 - max_stock: 1000 - price: 171 - sale_gamma: 19 - service_level: 0.95 - SKU61: - constraint: null - cost: 461 - init_stock: 150 - max_stock: 1000 - price: 645 - sale_gamma: 75 - service_level: 0.95 - SKU610: - constraint: null - cost: 449 - init_stock: 204 - max_stock: 1000 - price: 749 - sale_gamma: 68 - service_level: 0.95 - SKU611: - constraint: G(low_profit -> low_stock_constraint) - cost: 306 - init_stock: 539 - max_stock: 1000 - price: 489 - sale_gamma: 77 - service_level: 0.95 - SKU612: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 391 - init_stock: 616 - max_stock: 1000 - price: 613 - sale_gamma: 88 - service_level: 0.95 - SKU613: - constraint: G(stock_constraint) - cost: 174 - init_stock: 56 - max_stock: 1000 - price: 254 - sale_gamma: 7 - service_level: 0.95 - SKU614: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 45 - max_stock: 1000 - price: 318 - sale_gamma: 15 - service_level: 0.95 - SKU615: - constraint: G(stock_constraint) - cost: 330 - init_stock: 364 - max_stock: 1000 - price: 432 - sale_gamma: 91 - service_level: 0.95 - SKU616: - constraint: G(low_profit -> low_stock_constraint) - cost: 232 - init_stock: 219 - max_stock: 1000 - price: 382 - sale_gamma: 73 - service_level: 0.95 - SKU617: - constraint: null - cost: 203 - init_stock: 420 - max_stock: 1000 - price: 227 - sale_gamma: 60 - service_level: 0.95 - SKU618: - constraint: G(stock_constraint) - cost: 77 - init_stock: 138 - max_stock: 1000 - price: 97 - sale_gamma: 23 - service_level: 0.95 - SKU619: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 344 - max_stock: 1000 - price: 359 - sale_gamma: 86 - service_level: 0.95 - SKU62: - constraint: null - cost: 124 - init_stock: 36 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU620: - constraint: null - cost: 231 - init_stock: 156 - max_stock: 1000 - price: 267 - sale_gamma: 39 - service_level: 0.95 - SKU621: - constraint: null - cost: 199 - init_stock: 216 - max_stock: 1000 - price: 332 - sale_gamma: 72 - service_level: 0.95 - SKU622: - constraint: null - cost: 253 - init_stock: 455 - max_stock: 1000 - price: 468 - sale_gamma: 65 - service_level: 0.95 - SKU623: - constraint: null - cost: 335 - init_stock: 220 - max_stock: 1000 - price: 368 - sale_gamma: 55 - service_level: 0.95 - SKU624: - constraint: null - cost: 482 - init_stock: 270 - max_stock: 1000 - price: 824 - sale_gamma: 54 - service_level: 0.95 - SKU625: - constraint: null - cost: 46 - init_stock: 445 - max_stock: 1000 - price: 60 - sale_gamma: 89 - service_level: 0.95 - SKU626: - constraint: null - cost: 451 - init_stock: 534 - max_stock: 1000 - price: 694 - sale_gamma: 89 - service_level: 0.95 - SKU627: - constraint: null - cost: 220 - init_stock: 656 - max_stock: 1000 - price: 264 - sale_gamma: 82 - service_level: 0.95 - SKU628: - constraint: null - cost: 124 - init_stock: 156 - max_stock: 1000 - price: 229 - sale_gamma: 26 - service_level: 0.95 - SKU629: - constraint: G(stock_constraint) - cost: 353 - init_stock: 460 - max_stock: 1000 - price: 515 - sale_gamma: 92 - service_level: 0.95 - SKU63: - constraint: null - cost: 68 - init_stock: 70 - max_stock: 1000 - price: 86 - sale_gamma: 14 - service_level: 0.95 - SKU630: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 122 - init_stock: 34 - max_stock: 1000 - price: 137 - sale_gamma: 17 - service_level: 0.95 - SKU631: - constraint: null - cost: 296 - init_stock: 196 - max_stock: 1000 - price: 399 - sale_gamma: 49 - service_level: 0.95 - SKU632: - constraint: null - cost: 85 - init_stock: 470 - max_stock: 1000 - price: 141 - sale_gamma: 94 - service_level: 0.95 - SKU633: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 402 - max_stock: 1000 - price: 228 - sale_gamma: 67 - service_level: 0.95 - SKU634: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 17 - init_stock: 438 - max_stock: 1000 - price: 30 - sale_gamma: 73 - service_level: 0.95 - SKU635: - constraint: null - cost: 341 - init_stock: 372 - max_stock: 1000 - price: 654 - sale_gamma: 93 - service_level: 0.95 - SKU636: - constraint: G(stock_constraint) - cost: 385 - init_stock: 111 - max_stock: 1000 - price: 739 - sale_gamma: 37 - service_level: 0.95 - SKU637: - constraint: G(low_profit -> low_stock_constraint) - cost: 83 - init_stock: 305 - max_stock: 1000 - price: 118 - sale_gamma: 61 - service_level: 0.95 - SKU638: - constraint: G(stock_constraint) - cost: 375 - init_stock: 16 - max_stock: 1000 - price: 536 - sale_gamma: 8 - service_level: 0.95 - SKU639: - constraint: null - cost: 327 - init_stock: 160 - max_stock: 1000 - price: 487 - sale_gamma: 40 - service_level: 0.95 - SKU64: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 133 - max_stock: 1000 - price: 42 - sale_gamma: 19 - service_level: 0.95 - SKU640: - constraint: G(low_profit -> low_stock_constraint) - cost: 275 - init_stock: 546 - max_stock: 1000 - price: 382 - sale_gamma: 91 - service_level: 0.95 - SKU641: - constraint: null - cost: 365 - init_stock: 486 - max_stock: 1000 - price: 445 - sale_gamma: 81 - service_level: 0.95 - SKU642: - constraint: null - cost: 414 - init_stock: 150 - max_stock: 1000 - price: 554 - sale_gamma: 25 - service_level: 0.95 - SKU643: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 358 - init_stock: 322 - max_stock: 1000 - price: 461 - sale_gamma: 46 - service_level: 0.95 - SKU644: - constraint: G(low_profit -> low_stock_constraint) - cost: 46 - init_stock: 410 - max_stock: 1000 - price: 61 - sale_gamma: 82 - service_level: 0.95 - SKU645: - constraint: null - cost: 467 - init_stock: 594 - max_stock: 1000 - price: 742 - sale_gamma: 99 - service_level: 0.95 - SKU646: - constraint: G(low_profit -> low_stock_constraint) - cost: 189 - init_stock: 91 - max_stock: 1000 - price: 268 - sale_gamma: 13 - service_level: 0.95 - SKU647: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 462 - max_stock: 1000 - price: 369 - sale_gamma: 77 - service_level: 0.95 - SKU648: - constraint: G(stock_constraint) - cost: 226 - init_stock: 110 - max_stock: 1000 - price: 336 - sale_gamma: 55 - service_level: 0.95 - SKU649: - constraint: null - cost: 198 - init_stock: 175 - max_stock: 1000 - price: 229 - sale_gamma: 25 - service_level: 0.95 - SKU65: - constraint: null - cost: 15 - init_stock: 658 - max_stock: 1000 - price: 25 - sale_gamma: 94 - service_level: 0.95 - SKU650: - constraint: null - cost: 351 - init_stock: 224 - max_stock: 1000 - price: 498 - sale_gamma: 56 - service_level: 0.95 - SKU651: - constraint: null - cost: 380 - init_stock: 672 - max_stock: 1000 - price: 596 - sale_gamma: 96 - service_level: 0.95 - SKU652: - constraint: null - cost: 123 - init_stock: 280 - max_stock: 1000 - price: 168 - sale_gamma: 40 - service_level: 0.95 - SKU653: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 384 - max_stock: 1000 - price: 661 - sale_gamma: 96 - service_level: 0.95 - SKU654: - constraint: null - cost: 66 - init_stock: 32 - max_stock: 1000 - price: 110 - sale_gamma: 8 - service_level: 0.95 - SKU655: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 126 - max_stock: 1000 - price: 479 - sale_gamma: 42 - service_level: 0.95 - SKU656: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 53 - init_stock: 504 - max_stock: 1000 - price: 81 - sale_gamma: 72 - service_level: 0.95 - SKU657: - constraint: G(low_profit -> low_stock_constraint) - cost: 73 - init_stock: 567 - max_stock: 1000 - price: 110 - sale_gamma: 81 - service_level: 0.95 - SKU658: - constraint: null - cost: 70 - init_stock: 252 - max_stock: 1000 - price: 107 - sale_gamma: 36 - service_level: 0.95 - SKU659: - constraint: null - cost: 21 - init_stock: 336 - max_stock: 1000 - price: 27 - sale_gamma: 56 - service_level: 0.95 - SKU66: - constraint: null - cost: 143 - init_stock: 360 - max_stock: 1000 - price: 230 - sale_gamma: 90 - service_level: 0.95 - SKU660: - constraint: null - cost: 487 - init_stock: 415 - max_stock: 1000 - price: 560 - sale_gamma: 83 - service_level: 0.95 - SKU661: - constraint: null - cost: 344 - init_stock: 296 - max_stock: 1000 - price: 581 - sale_gamma: 74 - service_level: 0.95 - SKU662: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 266 - max_stock: 1000 - price: 628 - sale_gamma: 38 - service_level: 0.95 - SKU663: - constraint: G(stock_constraint) - cost: 418 - init_stock: 320 - max_stock: 1000 - price: 518 - sale_gamma: 40 - service_level: 0.95 - SKU664: - constraint: G(stock_constraint) - cost: 351 - init_stock: 420 - max_stock: 1000 - price: 494 - sale_gamma: 84 - service_level: 0.95 - SKU665: - constraint: null - cost: 493 - init_stock: 344 - max_stock: 1000 - price: 734 - sale_gamma: 43 - service_level: 0.95 - SKU666: - constraint: G(stock_constraint) - cost: 341 - init_stock: 176 - max_stock: 1000 - price: 572 - sale_gamma: 88 - service_level: 0.95 - SKU667: - constraint: null - cost: 325 - init_stock: 52 - max_stock: 1000 - price: 562 - sale_gamma: 13 - service_level: 0.95 - SKU668: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 300 - max_stock: 1000 - price: 463 - sale_gamma: 60 - service_level: 0.95 - SKU669: - constraint: null - cost: 74 - init_stock: 96 - max_stock: 1000 - price: 83 - sale_gamma: 16 - service_level: 0.95 - SKU67: - constraint: G(low_profit -> low_stock_constraint) - cost: 247 - init_stock: 159 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU670: - constraint: G(stock_constraint) - cost: 289 - init_stock: 258 - max_stock: 1000 - price: 430 - sale_gamma: 86 - service_level: 0.95 - SKU671: - constraint: G(stock_constraint) - cost: 267 - init_stock: 332 - max_stock: 1000 - price: 296 - sale_gamma: 83 - service_level: 0.95 - SKU672: - constraint: null - cost: 369 - init_stock: 78 - max_stock: 1000 - price: 420 - sale_gamma: 13 - service_level: 0.95 - SKU673: - constraint: G(low_profit -> low_stock_constraint) - cost: 322 - init_stock: 123 - max_stock: 1000 - price: 418 - sale_gamma: 41 - service_level: 0.95 - SKU674: - constraint: null - cost: 223 - init_stock: 66 - max_stock: 1000 - price: 263 - sale_gamma: 22 - service_level: 0.95 - SKU675: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 438 - init_stock: 99 - max_stock: 1000 - price: 529 - sale_gamma: 33 - service_level: 0.95 - SKU676: - constraint: null - cost: 244 - init_stock: 366 - max_stock: 1000 - price: 275 - sale_gamma: 61 - service_level: 0.95 - SKU677: - constraint: null - cost: 425 - init_stock: 176 - max_stock: 1000 - price: 658 - sale_gamma: 44 - service_level: 0.95 - SKU678: - constraint: null - cost: 168 - init_stock: 216 - max_stock: 1000 - price: 199 - sale_gamma: 36 - service_level: 0.95 - SKU679: - constraint: null - cost: 395 - init_stock: 132 - max_stock: 1000 - price: 734 - sale_gamma: 44 - service_level: 0.95 - SKU68: - constraint: G(stock_constraint) - cost: 169 - init_stock: 56 - max_stock: 1000 - price: 239 - sale_gamma: 14 - service_level: 0.95 - SKU680: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 260 - init_stock: 180 - max_stock: 1000 - price: 327 - sale_gamma: 30 - service_level: 0.95 - SKU681: - constraint: G(low_profit -> low_stock_constraint) - cost: 464 - init_stock: 776 - max_stock: 1000 - price: 510 - sale_gamma: 97 - service_level: 0.95 - SKU682: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 370 - init_stock: 318 - max_stock: 1000 - price: 407 - sale_gamma: 53 - service_level: 0.95 - SKU683: - constraint: G(low_profit -> low_stock_constraint) - cost: 327 - init_stock: 536 - max_stock: 1000 - price: 608 - sale_gamma: 67 - service_level: 0.95 - SKU684: - constraint: G(stock_constraint) - cost: 355 - init_stock: 158 - max_stock: 1000 - price: 401 - sale_gamma: 79 - service_level: 0.95 - SKU685: - constraint: null - cost: 422 - init_stock: 270 - max_stock: 1000 - price: 493 - sale_gamma: 45 - service_level: 0.95 - SKU686: - constraint: G(low_profit -> low_stock_constraint) - cost: 63 - init_stock: 378 - max_stock: 1000 - price: 122 - sale_gamma: 63 - service_level: 0.95 - SKU687: - constraint: G(low_profit -> low_stock_constraint) - cost: 34 - init_stock: 456 - max_stock: 1000 - price: 43 - sale_gamma: 76 - service_level: 0.95 - SKU688: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 462 - max_stock: 1000 - price: 759 - sale_gamma: 77 - service_level: 0.95 - SKU689: - constraint: G(stock_constraint) - cost: 499 - init_stock: 75 - max_stock: 1000 - price: 808 - sale_gamma: 15 - service_level: 0.95 - SKU69: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 176 - init_stock: 402 - max_stock: 1000 - price: 197 - sale_gamma: 67 - service_level: 0.95 - SKU690: - constraint: null - cost: 437 - init_stock: 415 - max_stock: 1000 - price: 498 - sale_gamma: 83 - service_level: 0.95 - SKU691: - constraint: null - cost: 17 - init_stock: 632 - max_stock: 1000 - price: 18 - sale_gamma: 79 - service_level: 0.95 - SKU692: - constraint: G(stock_constraint) - cost: 225 - init_stock: 195 - max_stock: 1000 - price: 258 - sale_gamma: 65 - service_level: 0.95 - SKU693: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 19 - init_stock: 244 - max_stock: 1000 - price: 21 - sale_gamma: 61 - service_level: 0.95 - SKU694: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 208 - init_stock: 300 - max_stock: 1000 - price: 386 - sale_gamma: 75 - service_level: 0.95 - SKU695: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 190 - init_stock: 49 - max_stock: 1000 - price: 361 - sale_gamma: 7 - service_level: 0.95 - SKU696: - constraint: G(low_profit -> low_stock_constraint) - cost: 348 - init_stock: 290 - max_stock: 1000 - price: 643 - sale_gamma: 58 - service_level: 0.95 - SKU697: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 455 - init_stock: 320 - max_stock: 1000 - price: 641 - sale_gamma: 80 - service_level: 0.95 - SKU698: - constraint: null - cost: 198 - init_stock: 488 - max_stock: 1000 - price: 316 - sale_gamma: 61 - service_level: 0.95 - SKU699: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 58 - sale_gamma: 20 - service_level: 0.95 - SKU7: - constraint: null - cost: 101 - init_stock: 480 - max_stock: 1000 - price: 131 - sale_gamma: 96 - service_level: 0.95 - SKU70: - constraint: G(stock_constraint) - cost: 186 - init_stock: 140 - max_stock: 1000 - price: 288 - sale_gamma: 35 - service_level: 0.95 - SKU700: - constraint: G(low_profit -> low_stock_constraint) - cost: 74 - init_stock: 45 - max_stock: 1000 - price: 130 - sale_gamma: 9 - service_level: 0.95 - SKU701: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 399 - init_stock: 462 - max_stock: 1000 - price: 770 - sale_gamma: 77 - service_level: 0.95 - SKU702: - constraint: G(stock_constraint) - cost: 82 - init_stock: 204 - max_stock: 1000 - price: 163 - sale_gamma: 34 - service_level: 0.95 - SKU703: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 363 - init_stock: 76 - max_stock: 1000 - price: 602 - sale_gamma: 38 - service_level: 0.95 - SKU704: - constraint: G(stock_constraint) - cost: 384 - init_stock: 261 - max_stock: 1000 - price: 518 - sale_gamma: 87 - service_level: 0.95 - SKU705: - constraint: null - cost: 128 - init_stock: 378 - max_stock: 1000 - price: 236 - sale_gamma: 63 - service_level: 0.95 - SKU706: - constraint: null - cost: 182 - init_stock: 455 - max_stock: 1000 - price: 242 - sale_gamma: 91 - service_level: 0.95 - SKU707: - constraint: null - cost: 18 - init_stock: 276 - max_stock: 1000 - price: 35 - sale_gamma: 69 - service_level: 0.95 - SKU708: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 178 - init_stock: 112 - max_stock: 1000 - price: 334 - sale_gamma: 28 - service_level: 0.95 - SKU709: - constraint: G(stock_constraint) - cost: 102 - init_stock: 212 - max_stock: 1000 - price: 173 - sale_gamma: 53 - service_level: 0.95 - SKU71: - constraint: null - cost: 315 - init_stock: 225 - max_stock: 1000 - price: 589 - sale_gamma: 45 - service_level: 0.95 - SKU710: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 252 - init_stock: 95 - max_stock: 1000 - price: 337 - sale_gamma: 19 - service_level: 0.95 - SKU711: - constraint: null - cost: 281 - init_stock: 414 - max_stock: 1000 - price: 320 - sale_gamma: 69 - service_level: 0.95 - SKU712: - constraint: null - cost: 232 - init_stock: 77 - max_stock: 1000 - price: 438 - sale_gamma: 11 - service_level: 0.95 - SKU713: - constraint: null - cost: 285 - init_stock: 129 - max_stock: 1000 - price: 413 - sale_gamma: 43 - service_level: 0.95 - SKU714: - constraint: null - cost: 79 - init_stock: 287 - max_stock: 1000 - price: 106 - sale_gamma: 41 - service_level: 0.95 - SKU715: - constraint: G(stock_constraint) - cost: 234 - init_stock: 34 - max_stock: 1000 - price: 271 - sale_gamma: 17 - service_level: 0.95 - SKU716: - constraint: null - cost: 70 - init_stock: 450 - max_stock: 1000 - price: 123 - sale_gamma: 75 - service_level: 0.95 - SKU717: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 345 - init_stock: 56 - max_stock: 1000 - price: 382 - sale_gamma: 8 - service_level: 0.95 - SKU718: - constraint: null - cost: 357 - init_stock: 174 - max_stock: 1000 - price: 692 - sale_gamma: 58 - service_level: 0.95 - SKU719: - constraint: null - cost: 340 - init_stock: 455 - max_stock: 1000 - price: 384 - sale_gamma: 65 - service_level: 0.95 - SKU72: - constraint: null - cost: 458 - init_stock: 595 - max_stock: 1000 - price: 870 - sale_gamma: 85 - service_level: 0.95 - SKU720: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 503 - sale_gamma: 42 - service_level: 0.95 - SKU721: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 73 - init_stock: 44 - max_stock: 1000 - price: 143 - sale_gamma: 11 - service_level: 0.95 - SKU722: - constraint: G(stock_constraint) - cost: 392 - init_stock: 194 - max_stock: 1000 - price: 572 - sale_gamma: 97 - service_level: 0.95 - SKU723: - constraint: null - cost: 318 - init_stock: 356 - max_stock: 1000 - price: 442 - sale_gamma: 89 - service_level: 0.95 - SKU724: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 198 - max_stock: 1000 - price: 520 - sale_gamma: 33 - service_level: 0.95 - SKU725: - constraint: null - cost: 175 - init_stock: 148 - max_stock: 1000 - price: 309 - sale_gamma: 37 - service_level: 0.95 - SKU726: - constraint: G(low_profit -> low_stock_constraint) - cost: 458 - init_stock: 356 - max_stock: 1000 - price: 567 - sale_gamma: 89 - service_level: 0.95 - SKU727: - constraint: null - cost: 418 - init_stock: 285 - max_stock: 1000 - price: 526 - sale_gamma: 57 - service_level: 0.95 - SKU728: - constraint: null - cost: 475 - init_stock: 185 - max_stock: 1000 - price: 864 - sale_gamma: 37 - service_level: 0.95 - SKU729: - constraint: null - cost: 324 - init_stock: 252 - max_stock: 1000 - price: 476 - sale_gamma: 36 - service_level: 0.95 - SKU73: - constraint: null - cost: 212 - init_stock: 216 - max_stock: 1000 - price: 248 - sale_gamma: 54 - service_level: 0.95 - SKU730: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 65 - max_stock: 1000 - price: 19 - sale_gamma: 13 - service_level: 0.95 - SKU731: - constraint: G(stock_constraint) - cost: 88 - init_stock: 410 - max_stock: 1000 - price: 155 - sale_gamma: 82 - service_level: 0.95 - SKU732: - constraint: null - cost: 41 - init_stock: 434 - max_stock: 1000 - price: 73 - sale_gamma: 62 - service_level: 0.95 - SKU733: - constraint: G(stock_constraint) - cost: 315 - init_stock: 104 - max_stock: 1000 - price: 541 - sale_gamma: 26 - service_level: 0.95 - SKU734: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 255 - max_stock: 1000 - price: 71 - sale_gamma: 51 - service_level: 0.95 - SKU735: - constraint: null - cost: 266 - init_stock: 594 - max_stock: 1000 - price: 409 - sale_gamma: 99 - service_level: 0.95 - SKU736: - constraint: G(stock_constraint) - cost: 368 - init_stock: 75 - max_stock: 1000 - price: 632 - sale_gamma: 25 - service_level: 0.95 - SKU737: - constraint: null - cost: 475 - init_stock: 93 - max_stock: 1000 - price: 655 - sale_gamma: 31 - service_level: 0.95 - SKU738: - constraint: null - cost: 185 - init_stock: 192 - max_stock: 1000 - price: 246 - sale_gamma: 48 - service_level: 0.95 - SKU739: - constraint: G(low_profit -> low_stock_constraint) - cost: 475 - init_stock: 296 - max_stock: 1000 - price: 612 - sale_gamma: 74 - service_level: 0.95 - SKU74: - constraint: null - cost: 159 - init_stock: 168 - max_stock: 1000 - price: 189 - sale_gamma: 42 - service_level: 0.95 - SKU740: - constraint: null - cost: 390 - init_stock: 256 - max_stock: 1000 - price: 549 - sale_gamma: 64 - service_level: 0.95 - SKU741: - constraint: G(stock_constraint) - cost: 91 - init_stock: 280 - max_stock: 1000 - price: 112 - sale_gamma: 56 - service_level: 0.95 - SKU742: - constraint: G(stock_constraint) - cost: 188 - init_stock: 110 - max_stock: 1000 - price: 374 - sale_gamma: 22 - service_level: 0.95 - SKU743: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 217 - init_stock: 301 - max_stock: 1000 - price: 431 - sale_gamma: 43 - service_level: 0.95 - SKU744: - constraint: G(low_profit -> low_stock_constraint) - cost: 379 - init_stock: 290 - max_stock: 1000 - price: 579 - sale_gamma: 58 - service_level: 0.95 - SKU745: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 368 - max_stock: 1000 - price: 376 - sale_gamma: 92 - service_level: 0.95 - SKU746: - constraint: null - cost: 437 - init_stock: 160 - max_stock: 1000 - price: 624 - sale_gamma: 40 - service_level: 0.95 - SKU747: - constraint: null - cost: 373 - init_stock: 474 - max_stock: 1000 - price: 589 - sale_gamma: 79 - service_level: 0.95 - SKU748: - constraint: null - cost: 275 - init_stock: 330 - max_stock: 1000 - price: 464 - sale_gamma: 66 - service_level: 0.95 - SKU749: - constraint: null - cost: 394 - init_stock: 106 - max_stock: 1000 - price: 705 - sale_gamma: 53 - service_level: 0.95 - SKU75: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 76 - max_stock: 1000 - price: 217 - sale_gamma: 19 - service_level: 0.95 - SKU750: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 310 - max_stock: 1000 - price: 463 - sale_gamma: 62 - service_level: 0.95 - SKU751: - constraint: null - cost: 369 - init_stock: 325 - max_stock: 1000 - price: 428 - sale_gamma: 65 - service_level: 0.95 - SKU752: - constraint: null - cost: 259 - init_stock: 546 - max_stock: 1000 - price: 435 - sale_gamma: 78 - service_level: 0.95 - SKU753: - constraint: G(stock_constraint) - cost: 77 - init_stock: 325 - max_stock: 1000 - price: 128 - sale_gamma: 65 - service_level: 0.95 - SKU754: - constraint: G(stock_constraint) - cost: 387 - init_stock: 91 - max_stock: 1000 - price: 545 - sale_gamma: 13 - service_level: 0.95 - SKU755: - constraint: null - cost: 354 - init_stock: 560 - max_stock: 1000 - price: 523 - sale_gamma: 80 - service_level: 0.95 - SKU756: - constraint: G(stock_constraint) - cost: 246 - init_stock: 360 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU757: - constraint: null - cost: 40 - init_stock: 399 - max_stock: 1000 - price: 76 - sale_gamma: 57 - service_level: 0.95 - SKU758: - constraint: null - cost: 241 - init_stock: 160 - max_stock: 1000 - price: 457 - sale_gamma: 40 - service_level: 0.95 - SKU759: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 435 - init_stock: 76 - max_stock: 1000 - price: 648 - sale_gamma: 38 - service_level: 0.95 - SKU76: - constraint: null - cost: 147 - init_stock: 256 - max_stock: 1000 - price: 191 - sale_gamma: 64 - service_level: 0.95 - SKU760: - constraint: null - cost: 176 - init_stock: 255 - max_stock: 1000 - price: 279 - sale_gamma: 51 - service_level: 0.95 - SKU761: - constraint: G(stock_constraint) - cost: 224 - init_stock: 275 - max_stock: 1000 - price: 250 - sale_gamma: 55 - service_level: 0.95 - SKU762: - constraint: G(stock_constraint) - cost: 264 - init_stock: 153 - max_stock: 1000 - price: 351 - sale_gamma: 51 - service_level: 0.95 - SKU763: - constraint: null - cost: 385 - init_stock: 316 - max_stock: 1000 - price: 677 - sale_gamma: 79 - service_level: 0.95 - SKU764: - constraint: null - cost: 349 - init_stock: 68 - max_stock: 1000 - price: 596 - sale_gamma: 34 - service_level: 0.95 - SKU765: - constraint: null - cost: 345 - init_stock: 52 - max_stock: 1000 - price: 472 - sale_gamma: 13 - service_level: 0.95 - SKU766: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 478 - init_stock: 140 - max_stock: 1000 - price: 855 - sale_gamma: 20 - service_level: 0.95 - SKU767: - constraint: null - cost: 95 - init_stock: 456 - max_stock: 1000 - price: 160 - sale_gamma: 76 - service_level: 0.95 - SKU768: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 644 - max_stock: 1000 - price: 244 - sale_gamma: 92 - service_level: 0.95 - SKU769: - constraint: null - cost: 24 - init_stock: 87 - max_stock: 1000 - price: 43 - sale_gamma: 29 - service_level: 0.95 - SKU77: - constraint: null - cost: 409 - init_stock: 144 - max_stock: 1000 - price: 687 - sale_gamma: 72 - service_level: 0.95 - SKU770: - constraint: null - cost: 150 - init_stock: 186 - max_stock: 1000 - price: 166 - sale_gamma: 62 - service_level: 0.95 - SKU771: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 35 - max_stock: 1000 - price: 182 - sale_gamma: 7 - service_level: 0.95 - SKU772: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 36 - max_stock: 1000 - price: 281 - sale_gamma: 6 - service_level: 0.95 - SKU773: - constraint: null - cost: 84 - init_stock: 368 - max_stock: 1000 - price: 117 - sale_gamma: 92 - service_level: 0.95 - SKU774: - constraint: null - cost: 447 - init_stock: 118 - max_stock: 1000 - price: 746 - sale_gamma: 59 - service_level: 0.95 - SKU775: - constraint: null - cost: 175 - init_stock: 688 - max_stock: 1000 - price: 192 - sale_gamma: 86 - service_level: 0.95 - SKU776: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 103 - init_stock: 425 - max_stock: 1000 - price: 172 - sale_gamma: 85 - service_level: 0.95 - SKU777: - constraint: null - cost: 292 - init_stock: 171 - max_stock: 1000 - price: 347 - sale_gamma: 57 - service_level: 0.95 - SKU778: - constraint: null - cost: 203 - init_stock: 40 - max_stock: 1000 - price: 288 - sale_gamma: 8 - service_level: 0.95 - SKU779: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 117 - init_stock: 215 - max_stock: 1000 - price: 186 - sale_gamma: 43 - service_level: 0.95 - SKU78: - constraint: null - cost: 234 - init_stock: 91 - max_stock: 1000 - price: 400 - sale_gamma: 13 - service_level: 0.95 - SKU780: - constraint: G(low_profit -> low_stock_constraint) - cost: 69 - init_stock: 410 - max_stock: 1000 - price: 102 - sale_gamma: 82 - service_level: 0.95 - SKU781: - constraint: null - cost: 372 - init_stock: 144 - max_stock: 1000 - price: 528 - sale_gamma: 36 - service_level: 0.95 - SKU782: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 70 - max_stock: 1000 - price: 35 - sale_gamma: 10 - service_level: 0.95 - SKU783: - constraint: null - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 139 - sale_gamma: 81 - service_level: 0.95 - SKU784: - constraint: null - cost: 309 - init_stock: 312 - max_stock: 1000 - price: 577 - sale_gamma: 52 - service_level: 0.95 - SKU785: - constraint: null - cost: 191 - init_stock: 210 - max_stock: 1000 - price: 255 - sale_gamma: 70 - service_level: 0.95 - SKU786: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 91 - init_stock: 132 - max_stock: 1000 - price: 102 - sale_gamma: 22 - service_level: 0.95 - SKU787: - constraint: null - cost: 360 - init_stock: 366 - max_stock: 1000 - price: 658 - sale_gamma: 61 - service_level: 0.95 - SKU788: - constraint: G(stock_constraint) - cost: 351 - init_stock: 112 - max_stock: 1000 - price: 417 - sale_gamma: 28 - service_level: 0.95 - SKU789: - constraint: G(stock_constraint) - cost: 153 - init_stock: 232 - max_stock: 1000 - price: 298 - sale_gamma: 58 - service_level: 0.95 - SKU79: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 245 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU790: - constraint: null - cost: 417 - init_stock: 330 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU791: - constraint: G(stock_constraint) - cost: 134 - init_stock: 56 - max_stock: 1000 - price: 155 - sale_gamma: 28 - service_level: 0.95 - SKU792: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 84 - max_stock: 1000 - price: 494 - sale_gamma: 21 - service_level: 0.95 - SKU793: - constraint: null - cost: 195 - init_stock: 532 - max_stock: 1000 - price: 282 - sale_gamma: 76 - service_level: 0.95 - SKU794: - constraint: null - cost: 325 - init_stock: 400 - max_stock: 1000 - price: 454 - sale_gamma: 80 - service_level: 0.95 - SKU795: - constraint: null - cost: 276 - init_stock: 288 - max_stock: 1000 - price: 549 - sale_gamma: 48 - service_level: 0.95 - SKU796: - constraint: null - cost: 447 - init_stock: 294 - max_stock: 1000 - price: 885 - sale_gamma: 49 - service_level: 0.95 - SKU797: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 100 - init_stock: 234 - max_stock: 1000 - price: 119 - sale_gamma: 39 - service_level: 0.95 - SKU798: - constraint: null - cost: 426 - init_stock: 180 - max_stock: 1000 - price: 630 - sale_gamma: 30 - service_level: 0.95 - SKU799: - constraint: null - cost: 63 - init_stock: 21 - max_stock: 1000 - price: 78 - sale_gamma: 7 - service_level: 0.95 - SKU8: - constraint: G(stock_constraint) - cost: 125 - init_stock: 252 - max_stock: 1000 - price: 196 - sale_gamma: 63 - service_level: 0.95 - SKU80: - constraint: G(stock_constraint) - cost: 163 - init_stock: 420 - max_stock: 1000 - price: 270 - sale_gamma: 70 - service_level: 0.95 - SKU800: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 470 - max_stock: 1000 - price: 455 - sale_gamma: 94 - service_level: 0.95 - SKU801: - constraint: G(low_profit -> low_stock_constraint) - cost: 389 - init_stock: 216 - max_stock: 1000 - price: 672 - sale_gamma: 36 - service_level: 0.95 - SKU802: - constraint: G(stock_constraint) - cost: 173 - init_stock: 310 - max_stock: 1000 - price: 281 - sale_gamma: 62 - service_level: 0.95 - SKU803: - constraint: null - cost: 272 - init_stock: 95 - max_stock: 1000 - price: 544 - sale_gamma: 19 - service_level: 0.95 - SKU804: - constraint: null - cost: 390 - init_stock: 188 - max_stock: 1000 - price: 491 - sale_gamma: 47 - service_level: 0.95 - SKU805: - constraint: null - cost: 61 - init_stock: 132 - max_stock: 1000 - price: 118 - sale_gamma: 33 - service_level: 0.95 - SKU806: - constraint: null - cost: 158 - init_stock: 156 - max_stock: 1000 - price: 186 - sale_gamma: 52 - service_level: 0.95 - SKU807: - constraint: null - cost: 453 - init_stock: 182 - max_stock: 1000 - price: 656 - sale_gamma: 26 - service_level: 0.95 - SKU808: - constraint: G(stock_constraint) - cost: 249 - init_stock: 182 - max_stock: 1000 - price: 435 - sale_gamma: 91 - service_level: 0.95 - SKU809: - constraint: null - cost: 55 - init_stock: 390 - max_stock: 1000 - price: 69 - sale_gamma: 65 - service_level: 0.95 - SKU81: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 528 - max_stock: 1000 - price: 223 - sale_gamma: 66 - service_level: 0.95 - SKU810: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 276 - init_stock: 42 - max_stock: 1000 - price: 322 - sale_gamma: 6 - service_level: 0.95 - SKU811: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 254 - init_stock: 148 - max_stock: 1000 - price: 322 - sale_gamma: 37 - service_level: 0.95 - SKU812: - constraint: null - cost: 252 - init_stock: 320 - max_stock: 1000 - price: 337 - sale_gamma: 80 - service_level: 0.95 - SKU813: - constraint: G(stock_constraint) - cost: 253 - init_stock: 49 - max_stock: 1000 - price: 333 - sale_gamma: 7 - service_level: 0.95 - SKU814: - constraint: null - cost: 485 - init_stock: 194 - max_stock: 1000 - price: 921 - sale_gamma: 97 - service_level: 0.95 - SKU815: - constraint: null - cost: 390 - init_stock: 168 - max_stock: 1000 - price: 429 - sale_gamma: 24 - service_level: 0.95 - SKU816: - constraint: null - cost: 152 - init_stock: 455 - max_stock: 1000 - price: 174 - sale_gamma: 91 - service_level: 0.95 - SKU817: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 227 - init_stock: 25 - max_stock: 1000 - price: 401 - sale_gamma: 5 - service_level: 0.95 - SKU818: - constraint: null - cost: 354 - init_stock: 215 - max_stock: 1000 - price: 534 - sale_gamma: 43 - service_level: 0.95 - SKU819: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 302 - init_stock: 135 - max_stock: 1000 - price: 480 - sale_gamma: 27 - service_level: 0.95 - SKU82: - constraint: null - cost: 290 - init_stock: 140 - max_stock: 1000 - price: 469 - sale_gamma: 28 - service_level: 0.95 - SKU820: - constraint: null - cost: 264 - init_stock: 410 - max_stock: 1000 - price: 464 - sale_gamma: 82 - service_level: 0.95 - SKU821: - constraint: G(stock_constraint) - cost: 99 - init_stock: 207 - max_stock: 1000 - price: 151 - sale_gamma: 69 - service_level: 0.95 - SKU822: - constraint: null - cost: 136 - init_stock: 490 - max_stock: 1000 - price: 266 - sale_gamma: 70 - service_level: 0.95 - SKU823: - constraint: null - cost: 75 - init_stock: 84 - max_stock: 1000 - price: 116 - sale_gamma: 28 - service_level: 0.95 - SKU824: - constraint: G(low_profit -> low_stock_constraint) - cost: 170 - init_stock: 420 - max_stock: 1000 - price: 200 - sale_gamma: 70 - service_level: 0.95 - SKU825: - constraint: null - cost: 214 - init_stock: 45 - max_stock: 1000 - price: 359 - sale_gamma: 15 - service_level: 0.95 - SKU826: - constraint: G(stock_constraint) - cost: 386 - init_stock: 330 - max_stock: 1000 - price: 428 - sale_gamma: 55 - service_level: 0.95 - SKU827: - constraint: G(stock_constraint) - cost: 148 - init_stock: 448 - max_stock: 1000 - price: 208 - sale_gamma: 64 - service_level: 0.95 - SKU828: - constraint: null - cost: 400 - init_stock: 276 - max_stock: 1000 - price: 627 - sale_gamma: 69 - service_level: 0.95 - SKU829: - constraint: null - cost: 61 - init_stock: 370 - max_stock: 1000 - price: 83 - sale_gamma: 74 - service_level: 0.95 - SKU83: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 68 - max_stock: 1000 - price: 535 - sale_gamma: 17 - service_level: 0.95 - SKU830: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 167 - init_stock: 144 - max_stock: 1000 - price: 252 - sale_gamma: 24 - service_level: 0.95 - SKU831: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 116 - max_stock: 1000 - price: 353 - sale_gamma: 29 - service_level: 0.95 - SKU832: - constraint: null - cost: 33 - init_stock: 164 - max_stock: 1000 - price: 48 - sale_gamma: 82 - service_level: 0.95 - SKU833: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 392 - max_stock: 1000 - price: 756 - sale_gamma: 98 - service_level: 0.95 - SKU834: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 10 - max_stock: 1000 - price: 662 - sale_gamma: 5 - service_level: 0.95 - SKU835: - constraint: null - cost: 440 - init_stock: 156 - max_stock: 1000 - price: 532 - sale_gamma: 52 - service_level: 0.95 - SKU836: - constraint: G(stock_constraint) - cost: 323 - init_stock: 192 - max_stock: 1000 - price: 552 - sale_gamma: 96 - service_level: 0.95 - SKU837: - constraint: null - cost: 373 - init_stock: 156 - max_stock: 1000 - price: 607 - sale_gamma: 26 - service_level: 0.95 - SKU838: - constraint: null - cost: 456 - init_stock: 308 - max_stock: 1000 - price: 711 - sale_gamma: 77 - service_level: 0.95 - SKU839: - constraint: null - cost: 473 - init_stock: 360 - max_stock: 1000 - price: 695 - sale_gamma: 60 - service_level: 0.95 - SKU84: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 318 - init_stock: 294 - max_stock: 1000 - price: 594 - sale_gamma: 42 - service_level: 0.95 - SKU840: - constraint: G(low_profit -> low_stock_constraint) - cost: 266 - init_stock: 150 - max_stock: 1000 - price: 436 - sale_gamma: 50 - service_level: 0.95 - SKU841: - constraint: G(stock_constraint) - cost: 285 - init_stock: 595 - max_stock: 1000 - price: 538 - sale_gamma: 85 - service_level: 0.95 - SKU842: - constraint: G(low_profit -> low_stock_constraint) - cost: 41 - init_stock: 252 - max_stock: 1000 - price: 70 - sale_gamma: 84 - service_level: 0.95 - SKU843: - constraint: null - cost: 360 - init_stock: 432 - max_stock: 1000 - price: 694 - sale_gamma: 72 - service_level: 0.95 - SKU844: - constraint: G(low_profit -> low_stock_constraint) - cost: 51 - init_stock: 474 - max_stock: 1000 - price: 63 - sale_gamma: 79 - service_level: 0.95 - SKU845: - constraint: G(low_profit -> low_stock_constraint) - cost: 288 - init_stock: 176 - max_stock: 1000 - price: 558 - sale_gamma: 22 - service_level: 0.95 - SKU846: - constraint: null - cost: 485 - init_stock: 234 - max_stock: 1000 - price: 940 - sale_gamma: 39 - service_level: 0.95 - SKU847: - constraint: G(low_profit -> low_stock_constraint) - cost: 388 - init_stock: 546 - max_stock: 1000 - price: 519 - sale_gamma: 91 - service_level: 0.95 - SKU848: - constraint: null - cost: 306 - init_stock: 184 - max_stock: 1000 - price: 358 - sale_gamma: 46 - service_level: 0.95 - SKU849: - constraint: G(low_profit -> low_stock_constraint) - cost: 320 - init_stock: 160 - max_stock: 1000 - price: 425 - sale_gamma: 40 - service_level: 0.95 - SKU85: - constraint: G(low_profit -> low_stock_constraint) - cost: 442 - init_stock: 400 - max_stock: 1000 - price: 583 - sale_gamma: 100 - service_level: 0.95 - SKU850: - constraint: G(stock_constraint) - cost: 183 - init_stock: 342 - max_stock: 1000 - price: 206 - sale_gamma: 57 - service_level: 0.95 - SKU851: - constraint: null - cost: 53 - init_stock: 384 - max_stock: 1000 - price: 96 - sale_gamma: 64 - service_level: 0.95 - SKU852: - constraint: null - cost: 306 - init_stock: 162 - max_stock: 1000 - price: 483 - sale_gamma: 54 - service_level: 0.95 - SKU853: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 386 - init_stock: 280 - max_stock: 1000 - price: 443 - sale_gamma: 56 - service_level: 0.95 - SKU854: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 212 - init_stock: 390 - max_stock: 1000 - price: 290 - sale_gamma: 65 - service_level: 0.95 - SKU855: - constraint: null - cost: 76 - init_stock: 432 - max_stock: 1000 - price: 114 - sale_gamma: 72 - service_level: 0.95 - SKU856: - constraint: G(low_profit -> low_stock_constraint) - cost: 380 - init_stock: 224 - max_stock: 1000 - price: 627 - sale_gamma: 32 - service_level: 0.95 - SKU857: - constraint: G(stock_constraint) - cost: 462 - init_stock: 500 - max_stock: 1000 - price: 646 - sale_gamma: 100 - service_level: 0.95 - SKU858: - constraint: null - cost: 80 - init_stock: 120 - max_stock: 1000 - price: 158 - sale_gamma: 24 - service_level: 0.95 - SKU859: - constraint: G(low_profit -> low_stock_constraint) - cost: 215 - init_stock: 224 - max_stock: 1000 - price: 298 - sale_gamma: 28 - service_level: 0.95 - SKU86: - constraint: null - cost: 386 - init_stock: 595 - max_stock: 1000 - price: 447 - sale_gamma: 85 - service_level: 0.95 - SKU860: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 105 - max_stock: 1000 - price: 435 - sale_gamma: 15 - service_level: 0.95 - SKU861: - constraint: null - cost: 477 - init_stock: 52 - max_stock: 1000 - price: 944 - sale_gamma: 13 - service_level: 0.95 - SKU862: - constraint: null - cost: 240 - init_stock: 64 - max_stock: 1000 - price: 412 - sale_gamma: 16 - service_level: 0.95 - SKU863: - constraint: null - cost: 470 - init_stock: 372 - max_stock: 1000 - price: 911 - sale_gamma: 93 - service_level: 0.95 - SKU864: - constraint: null - cost: 203 - init_stock: 114 - max_stock: 1000 - price: 341 - sale_gamma: 19 - service_level: 0.95 - SKU865: - constraint: G(low_profit -> low_stock_constraint) - cost: 144 - init_stock: 152 - max_stock: 1000 - price: 253 - sale_gamma: 19 - service_level: 0.95 - SKU866: - constraint: null - cost: 172 - init_stock: 264 - max_stock: 1000 - price: 201 - sale_gamma: 88 - service_level: 0.95 - SKU867: - constraint: G(low_profit -> low_stock_constraint) - cost: 499 - init_stock: 500 - max_stock: 1000 - price: 698 - sale_gamma: 100 - service_level: 0.95 - SKU868: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 41 - init_stock: 150 - max_stock: 1000 - price: 56 - sale_gamma: 50 - service_level: 0.95 - SKU869: - constraint: null - cost: 97 - init_stock: 388 - max_stock: 1000 - price: 111 - sale_gamma: 97 - service_level: 0.95 - SKU87: - constraint: null - cost: 368 - init_stock: 207 - max_stock: 1000 - price: 563 - sale_gamma: 69 - service_level: 0.95 - SKU870: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 281 - init_stock: 335 - max_stock: 1000 - price: 446 - sale_gamma: 67 - service_level: 0.95 - SKU871: - constraint: null - cost: 101 - init_stock: 396 - max_stock: 1000 - price: 112 - sale_gamma: 99 - service_level: 0.95 - SKU872: - constraint: null - cost: 133 - init_stock: 160 - max_stock: 1000 - price: 195 - sale_gamma: 32 - service_level: 0.95 - SKU873: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 423 - init_stock: 155 - max_stock: 1000 - price: 829 - sale_gamma: 31 - service_level: 0.95 - SKU874: - constraint: G(low_profit -> low_stock_constraint) - cost: 469 - init_stock: 360 - max_stock: 1000 - price: 689 - sale_gamma: 60 - service_level: 0.95 - SKU875: - constraint: G(stock_constraint) - cost: 51 - init_stock: 138 - max_stock: 1000 - price: 57 - sale_gamma: 23 - service_level: 0.95 - SKU876: - constraint: null - cost: 303 - init_stock: 427 - max_stock: 1000 - price: 427 - sale_gamma: 61 - service_level: 0.95 - SKU877: - constraint: null - cost: 40 - init_stock: 245 - max_stock: 1000 - price: 70 - sale_gamma: 35 - service_level: 0.95 - SKU878: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 476 - max_stock: 1000 - price: 32 - sale_gamma: 68 - service_level: 0.95 - SKU879: - constraint: null - cost: 150 - init_stock: 300 - max_stock: 1000 - price: 198 - sale_gamma: 100 - service_level: 0.95 - SKU88: - constraint: G(low_profit -> low_stock_constraint) - cost: 471 - init_stock: 36 - max_stock: 1000 - price: 824 - sale_gamma: 12 - service_level: 0.95 - SKU880: - constraint: null - cost: 433 - init_stock: 155 - max_stock: 1000 - price: 532 - sale_gamma: 31 - service_level: 0.95 - SKU881: - constraint: null - cost: 431 - init_stock: 420 - max_stock: 1000 - price: 560 - sale_gamma: 70 - service_level: 0.95 - SKU882: - constraint: G(stock_constraint) - cost: 194 - init_stock: 80 - max_stock: 1000 - price: 314 - sale_gamma: 16 - service_level: 0.95 - SKU883: - constraint: null - cost: 284 - init_stock: 152 - max_stock: 1000 - price: 479 - sale_gamma: 38 - service_level: 0.95 - SKU884: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 139 - init_stock: 195 - max_stock: 1000 - price: 222 - sale_gamma: 39 - service_level: 0.95 - SKU885: - constraint: G(low_profit -> low_stock_constraint) - cost: 49 - init_stock: 189 - max_stock: 1000 - price: 58 - sale_gamma: 27 - service_level: 0.95 - SKU886: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 584 - max_stock: 1000 - price: 602 - sale_gamma: 73 - service_level: 0.95 - SKU887: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 266 - init_stock: 174 - max_stock: 1000 - price: 494 - sale_gamma: 87 - service_level: 0.95 - SKU888: - constraint: G(low_profit -> low_stock_constraint) - cost: 143 - init_stock: 45 - max_stock: 1000 - price: 220 - sale_gamma: 9 - service_level: 0.95 - SKU889: - constraint: null - cost: 101 - init_stock: 147 - max_stock: 1000 - price: 199 - sale_gamma: 21 - service_level: 0.95 - SKU89: - constraint: null - cost: 138 - init_stock: 744 - max_stock: 1000 - price: 269 - sale_gamma: 93 - service_level: 0.95 - SKU890: - constraint: G(low_profit -> low_stock_constraint) - cost: 161 - init_stock: 600 - max_stock: 1000 - price: 247 - sale_gamma: 100 - service_level: 0.95 - SKU891: - constraint: null - cost: 356 - init_stock: 176 - max_stock: 1000 - price: 615 - sale_gamma: 22 - service_level: 0.95 - SKU892: - constraint: null - cost: 313 - init_stock: 552 - max_stock: 1000 - price: 516 - sale_gamma: 92 - service_level: 0.95 - SKU893: - constraint: null - cost: 229 - init_stock: 594 - max_stock: 1000 - price: 302 - sale_gamma: 99 - service_level: 0.95 - SKU894: - constraint: null - cost: 129 - init_stock: 222 - max_stock: 1000 - price: 176 - sale_gamma: 74 - service_level: 0.95 - SKU895: - constraint: null - cost: 230 - init_stock: 39 - max_stock: 1000 - price: 301 - sale_gamma: 13 - service_level: 0.95 - SKU896: - constraint: null - cost: 289 - init_stock: 400 - max_stock: 1000 - price: 465 - sale_gamma: 80 - service_level: 0.95 - SKU897: - constraint: null - cost: 393 - init_stock: 432 - max_stock: 1000 - price: 640 - sale_gamma: 72 - service_level: 0.95 - SKU898: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 477 - init_stock: 44 - max_stock: 1000 - price: 615 - sale_gamma: 11 - service_level: 0.95 - SKU899: - constraint: null - cost: 233 - init_stock: 240 - max_stock: 1000 - price: 309 - sale_gamma: 48 - service_level: 0.95 - SKU9: - constraint: null - cost: 166 - init_stock: 384 - max_stock: 1000 - price: 247 - sale_gamma: 96 - service_level: 0.95 - SKU90: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 454 - init_stock: 204 - max_stock: 1000 - price: 726 - sale_gamma: 51 - service_level: 0.95 - SKU900: - constraint: null - cost: 158 - init_stock: 165 - max_stock: 1000 - price: 263 - sale_gamma: 55 - service_level: 0.95 - SKU901: - constraint: G(stock_constraint) - cost: 215 - init_stock: 203 - max_stock: 1000 - price: 320 - sale_gamma: 29 - service_level: 0.95 - SKU902: - constraint: G(low_profit -> low_stock_constraint) - cost: 125 - init_stock: 560 - max_stock: 1000 - price: 205 - sale_gamma: 80 - service_level: 0.95 - SKU903: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 357 - init_stock: 266 - max_stock: 1000 - price: 517 - sale_gamma: 38 - service_level: 0.95 - SKU904: - constraint: G(low_profit -> low_stock_constraint) - cost: 496 - init_stock: 255 - max_stock: 1000 - price: 605 - sale_gamma: 51 - service_level: 0.95 - SKU905: - constraint: null - cost: 249 - init_stock: 217 - max_stock: 1000 - price: 383 - sale_gamma: 31 - service_level: 0.95 - SKU906: - constraint: G(low_profit -> low_stock_constraint) - cost: 166 - init_stock: 156 - max_stock: 1000 - price: 273 - sale_gamma: 52 - service_level: 0.95 - SKU907: - constraint: null - cost: 22 - init_stock: 498 - max_stock: 1000 - price: 33 - sale_gamma: 83 - service_level: 0.95 - SKU908: - constraint: null - cost: 408 - init_stock: 546 - max_stock: 1000 - price: 595 - sale_gamma: 78 - service_level: 0.95 - SKU909: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 480 - max_stock: 1000 - price: 703 - sale_gamma: 96 - service_level: 0.95 - SKU91: - constraint: G(low_profit -> low_stock_constraint) - cost: 303 - init_stock: 48 - max_stock: 1000 - price: 463 - sale_gamma: 16 - service_level: 0.95 - SKU910: - constraint: null - cost: 226 - init_stock: 132 - max_stock: 1000 - price: 350 - sale_gamma: 33 - service_level: 0.95 - SKU911: - constraint: null - cost: 461 - init_stock: 210 - max_stock: 1000 - price: 686 - sale_gamma: 70 - service_level: 0.95 - SKU912: - constraint: null - cost: 236 - init_stock: 135 - max_stock: 1000 - price: 328 - sale_gamma: 27 - service_level: 0.95 - SKU913: - constraint: null - cost: 322 - init_stock: 184 - max_stock: 1000 - price: 518 - sale_gamma: 46 - service_level: 0.95 - SKU914: - constraint: null - cost: 272 - init_stock: 434 - max_stock: 1000 - price: 446 - sale_gamma: 62 - service_level: 0.95 - SKU915: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 337 - init_stock: 280 - max_stock: 1000 - price: 545 - sale_gamma: 56 - service_level: 0.95 - SKU916: - constraint: null - cost: 337 - init_stock: 534 - max_stock: 1000 - price: 647 - sale_gamma: 89 - service_level: 0.95 - SKU917: - constraint: G(low_profit -> low_stock_constraint) - cost: 258 - init_stock: 120 - max_stock: 1000 - price: 366 - sale_gamma: 30 - service_level: 0.95 - SKU918: - constraint: G(stock_constraint) - cost: 148 - init_stock: 330 - max_stock: 1000 - price: 224 - sale_gamma: 55 - service_level: 0.95 - SKU919: - constraint: G(stock_constraint) - cost: 393 - init_stock: 125 - max_stock: 1000 - price: 711 - sale_gamma: 25 - service_level: 0.95 - SKU92: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 252 - max_stock: 1000 - price: 466 - sale_gamma: 63 - service_level: 0.95 - SKU920: - constraint: null - cost: 357 - init_stock: 344 - max_stock: 1000 - price: 696 - sale_gamma: 86 - service_level: 0.95 - SKU921: - constraint: G(low_profit -> low_stock_constraint) - cost: 227 - init_stock: 198 - max_stock: 1000 - price: 419 - sale_gamma: 66 - service_level: 0.95 - SKU922: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 201 - max_stock: 1000 - price: 123 - sale_gamma: 67 - service_level: 0.95 - SKU923: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 348 - max_stock: 1000 - price: 828 - sale_gamma: 58 - service_level: 0.95 - SKU924: - constraint: null - cost: 316 - init_stock: 425 - max_stock: 1000 - price: 423 - sale_gamma: 85 - service_level: 0.95 - SKU925: - constraint: null - cost: 360 - init_stock: 90 - max_stock: 1000 - price: 716 - sale_gamma: 15 - service_level: 0.95 - SKU926: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 360 - init_stock: 469 - max_stock: 1000 - price: 475 - sale_gamma: 67 - service_level: 0.95 - SKU927: - constraint: G(stock_constraint) - cost: 260 - init_stock: 147 - max_stock: 1000 - price: 439 - sale_gamma: 21 - service_level: 0.95 - SKU928: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 491 - init_stock: 415 - max_stock: 1000 - price: 869 - sale_gamma: 83 - service_level: 0.95 - SKU929: - constraint: null - cost: 359 - init_stock: 800 - max_stock: 1000 - price: 470 - sale_gamma: 100 - service_level: 0.95 - SKU93: - constraint: null - cost: 404 - init_stock: 268 - max_stock: 1000 - price: 557 - sale_gamma: 67 - service_level: 0.95 - SKU930: - constraint: G(low_profit -> low_stock_constraint) - cost: 198 - init_stock: 196 - max_stock: 1000 - price: 247 - sale_gamma: 28 - service_level: 0.95 - SKU931: - constraint: null - cost: 71 - init_stock: 56 - max_stock: 1000 - price: 101 - sale_gamma: 14 - service_level: 0.95 - SKU932: - constraint: null - cost: 163 - init_stock: 264 - max_stock: 1000 - price: 283 - sale_gamma: 66 - service_level: 0.95 - SKU933: - constraint: null - cost: 113 - init_stock: 156 - max_stock: 1000 - price: 179 - sale_gamma: 78 - service_level: 0.95 - SKU934: - constraint: G(stock_constraint) - cost: 219 - init_stock: 469 - max_stock: 1000 - price: 346 - sale_gamma: 67 - service_level: 0.95 - SKU935: - constraint: null - cost: 364 - init_stock: 376 - max_stock: 1000 - price: 527 - sale_gamma: 94 - service_level: 0.95 - SKU936: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 24 - init_stock: 20 - max_stock: 1000 - price: 41 - sale_gamma: 5 - service_level: 0.95 - SKU937: - constraint: G(stock_constraint) - cost: 135 - init_stock: 85 - max_stock: 1000 - price: 216 - sale_gamma: 17 - service_level: 0.95 - SKU938: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 63 - max_stock: 1000 - price: 704 - sale_gamma: 21 - service_level: 0.95 - SKU939: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 413 - max_stock: 1000 - price: 219 - sale_gamma: 59 - service_level: 0.95 - SKU94: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 235 - max_stock: 1000 - price: 261 - sale_gamma: 47 - service_level: 0.95 - SKU940: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 14 - init_stock: 279 - max_stock: 1000 - price: 24 - sale_gamma: 93 - service_level: 0.95 - SKU941: - constraint: G(stock_constraint) - cost: 80 - init_stock: 456 - max_stock: 1000 - price: 132 - sale_gamma: 57 - service_level: 0.95 - SKU942: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 202 - init_stock: 65 - max_stock: 1000 - price: 303 - sale_gamma: 13 - service_level: 0.95 - SKU943: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 182 - sale_gamma: 49 - service_level: 0.95 - SKU944: - constraint: null - cost: 196 - init_stock: 132 - max_stock: 1000 - price: 356 - sale_gamma: 44 - service_level: 0.95 - SKU945: - constraint: null - cost: 141 - init_stock: 85 - max_stock: 1000 - price: 176 - sale_gamma: 17 - service_level: 0.95 - SKU946: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 555 - sale_gamma: 49 - service_level: 0.95 - SKU947: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 338 - init_stock: 637 - max_stock: 1000 - price: 547 - sale_gamma: 91 - service_level: 0.95 - SKU948: - constraint: null - cost: 425 - init_stock: 112 - max_stock: 1000 - price: 476 - sale_gamma: 28 - service_level: 0.95 - SKU949: - constraint: G(stock_constraint) - cost: 309 - init_stock: 15 - max_stock: 1000 - price: 522 - sale_gamma: 5 - service_level: 0.95 - SKU95: - constraint: null - cost: 136 - init_stock: 84 - max_stock: 1000 - price: 214 - sale_gamma: 21 - service_level: 0.95 - SKU950: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 102 - init_stock: 324 - max_stock: 1000 - price: 128 - sale_gamma: 54 - service_level: 0.95 - SKU951: - constraint: G(low_profit -> low_stock_constraint) - cost: 75 - init_stock: 90 - max_stock: 1000 - price: 117 - sale_gamma: 18 - service_level: 0.95 - SKU952: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 156 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU953: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 230 - sale_gamma: 35 - service_level: 0.95 - SKU954: - constraint: null - cost: 296 - init_stock: 430 - max_stock: 1000 - price: 444 - sale_gamma: 86 - service_level: 0.95 - SKU955: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 55 - init_stock: 189 - max_stock: 1000 - price: 85 - sale_gamma: 63 - service_level: 0.95 - SKU956: - constraint: null - cost: 282 - init_stock: 425 - max_stock: 1000 - price: 397 - sale_gamma: 85 - service_level: 0.95 - SKU957: - constraint: null - cost: 305 - init_stock: 306 - max_stock: 1000 - price: 466 - sale_gamma: 51 - service_level: 0.95 - SKU958: - constraint: null - cost: 369 - init_stock: 462 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU959: - constraint: null - cost: 81 - init_stock: 164 - max_stock: 1000 - price: 127 - sale_gamma: 82 - service_level: 0.95 - SKU96: - constraint: G(low_profit -> low_stock_constraint) - cost: 176 - init_stock: 264 - max_stock: 1000 - price: 329 - sale_gamma: 44 - service_level: 0.95 - SKU960: - constraint: null - cost: 147 - init_stock: 42 - max_stock: 1000 - price: 235 - sale_gamma: 6 - service_level: 0.95 - SKU961: - constraint: null - cost: 264 - init_stock: 176 - max_stock: 1000 - price: 290 - sale_gamma: 44 - service_level: 0.95 - SKU962: - constraint: null - cost: 354 - init_stock: 176 - max_stock: 1000 - price: 392 - sale_gamma: 44 - service_level: 0.95 - SKU963: - constraint: null - cost: 349 - init_stock: 63 - max_stock: 1000 - price: 523 - sale_gamma: 21 - service_level: 0.95 - SKU964: - constraint: null - cost: 244 - init_stock: 219 - max_stock: 1000 - price: 480 - sale_gamma: 73 - service_level: 0.95 - SKU965: - constraint: G(stock_constraint) - cost: 124 - init_stock: 255 - max_stock: 1000 - price: 199 - sale_gamma: 51 - service_level: 0.95 - SKU966: - constraint: G(stock_constraint) - cost: 302 - init_stock: 308 - max_stock: 1000 - price: 474 - sale_gamma: 44 - service_level: 0.95 - SKU967: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 270 - max_stock: 1000 - price: 111 - sale_gamma: 45 - service_level: 0.95 - SKU968: - constraint: G(stock_constraint) - cost: 281 - init_stock: 294 - max_stock: 1000 - price: 528 - sale_gamma: 49 - service_level: 0.95 - SKU969: - constraint: G(low_profit -> low_stock_constraint) - cost: 249 - init_stock: 24 - max_stock: 1000 - price: 328 - sale_gamma: 6 - service_level: 0.95 - SKU97: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 28 - init_stock: 180 - max_stock: 1000 - price: 43 - sale_gamma: 30 - service_level: 0.95 - SKU970: - constraint: null - cost: 244 - init_stock: 30 - max_stock: 1000 - price: 373 - sale_gamma: 5 - service_level: 0.95 - SKU971: - constraint: null - cost: 368 - init_stock: 219 - max_stock: 1000 - price: 426 - sale_gamma: 73 - service_level: 0.95 - SKU972: - constraint: G(stock_constraint) - cost: 209 - init_stock: 345 - max_stock: 1000 - price: 307 - sale_gamma: 69 - service_level: 0.95 - SKU973: - constraint: null - cost: 271 - init_stock: 33 - max_stock: 1000 - price: 447 - sale_gamma: 11 - service_level: 0.95 - SKU974: - constraint: null - cost: 170 - init_stock: 192 - max_stock: 1000 - price: 285 - sale_gamma: 32 - service_level: 0.95 - SKU975: - constraint: G(stock_constraint) - cost: 198 - init_stock: 88 - max_stock: 1000 - price: 334 - sale_gamma: 22 - service_level: 0.95 - SKU976: - constraint: G(stock_constraint) - cost: 178 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU977: - constraint: G(stock_constraint) - cost: 234 - init_stock: 324 - max_stock: 1000 - price: 269 - sale_gamma: 54 - service_level: 0.95 - SKU978: - constraint: G(stock_constraint) - cost: 470 - init_stock: 320 - max_stock: 1000 - price: 921 - sale_gamma: 64 - service_level: 0.95 - SKU979: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 443 - init_stock: 480 - max_stock: 1000 - price: 753 - sale_gamma: 96 - service_level: 0.95 - SKU98: - constraint: G(stock_constraint) - cost: 443 - init_stock: 70 - max_stock: 1000 - price: 527 - sale_gamma: 35 - service_level: 0.95 - SKU980: - constraint: null - cost: 39 - init_stock: 340 - max_stock: 1000 - price: 60 - sale_gamma: 68 - service_level: 0.95 - SKU981: - constraint: null - cost: 482 - init_stock: 360 - max_stock: 1000 - price: 607 - sale_gamma: 72 - service_level: 0.95 - SKU982: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 213 - init_stock: 208 - max_stock: 1000 - price: 374 - sale_gamma: 52 - service_level: 0.95 - SKU983: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 276 - max_stock: 1000 - price: 776 - sale_gamma: 92 - service_level: 0.95 - SKU984: - constraint: G(stock_constraint) - cost: 232 - init_stock: 637 - max_stock: 1000 - price: 327 - sale_gamma: 91 - service_level: 0.95 - SKU985: - constraint: null - cost: 290 - init_stock: 153 - max_stock: 1000 - price: 420 - sale_gamma: 51 - service_level: 0.95 - SKU986: - constraint: null - cost: 275 - init_stock: 136 - max_stock: 1000 - price: 313 - sale_gamma: 17 - service_level: 0.95 - SKU987: - constraint: null - cost: 434 - init_stock: 204 - max_stock: 1000 - price: 516 - sale_gamma: 34 - service_level: 0.95 - SKU988: - constraint: null - cost: 102 - init_stock: 69 - max_stock: 1000 - price: 166 - sale_gamma: 23 - service_level: 0.95 - SKU989: - constraint: null - cost: 484 - init_stock: 558 - max_stock: 1000 - price: 653 - sale_gamma: 93 - service_level: 0.95 - SKU99: - constraint: null - cost: 361 - init_stock: 225 - max_stock: 1000 - price: 581 - sale_gamma: 45 - service_level: 0.95 - SKU990: - constraint: G(low_profit -> low_stock_constraint) - cost: 108 - init_stock: 228 - max_stock: 1000 - price: 174 - sale_gamma: 57 - service_level: 0.95 - SKU991: - constraint: null - cost: 409 - init_stock: 152 - max_stock: 1000 - price: 576 - sale_gamma: 19 - service_level: 0.95 - SKU992: - constraint: null - cost: 434 - init_stock: 651 - max_stock: 1000 - price: 685 - sale_gamma: 93 - service_level: 0.95 - SKU993: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 602 - max_stock: 1000 - price: 201 - sale_gamma: 86 - service_level: 0.95 - SKU994: - constraint: G(low_profit -> low_stock_constraint) - cost: 470 - init_stock: 300 - max_stock: 1000 - price: 916 - sale_gamma: 50 - service_level: 0.95 - SKU995: - constraint: null - cost: 241 - init_stock: 532 - max_stock: 1000 - price: 310 - sale_gamma: 76 - service_level: 0.95 - SKU996: - constraint: G(stock_constraint) - cost: 260 - init_stock: 438 - max_stock: 1000 - price: 416 - sale_gamma: 73 - service_level: 0.95 - SKU997: - constraint: G(stock_constraint) - cost: 400 - init_stock: 102 - max_stock: 1000 - price: 727 - sale_gamma: 34 - service_level: 0.95 - SKU998: - constraint: G(low_profit -> low_stock_constraint) - cost: 447 - init_stock: 165 - max_stock: 1000 - price: 688 - sale_gamma: 55 - service_level: 0.95 - SKU999: - constraint: null - cost: 79 - init_stock: 161 - max_stock: 1000 - price: 86 - sale_gamma: 23 - service_level: 0.95 - grid: - facilities: - STORE0: - - 9 - - 8 - SUPPLIER0: - - 14 - - 16 - WAREHOUSE0: - - 16 - - 18 - size: - - 20 - - 20 - skus: - - id: 0 - name: SKU0 - - id: 1 - name: SKU1 - - id: 2 - name: SKU2 - - id: 3 - name: SKU3 - - id: 4 - name: SKU4 - - id: 5 - name: SKU5 - - id: 6 - name: SKU6 - - id: 7 - name: SKU7 - - id: 8 - name: SKU8 - - id: 9 - name: SKU9 - - id: 10 - name: SKU10 - - id: 11 - name: SKU11 - - id: 12 - name: SKU12 - - id: 13 - name: SKU13 - - id: 14 - name: SKU14 - - id: 15 - name: SKU15 - - id: 16 - name: SKU16 - - id: 17 - name: SKU17 - - id: 18 - name: SKU18 - - id: 19 - name: SKU19 - - id: 20 - name: SKU20 - - id: 21 - name: SKU21 - - id: 22 - name: SKU22 - - id: 23 - name: SKU23 - - id: 24 - name: SKU24 - - id: 25 - name: SKU25 - - id: 26 - name: SKU26 - - id: 27 - name: SKU27 - - id: 28 - name: SKU28 - - id: 29 - name: SKU29 - - id: 30 - name: SKU30 - - id: 31 - name: SKU31 - - id: 32 - name: SKU32 - - id: 33 - name: SKU33 - - id: 34 - name: SKU34 - - id: 35 - name: SKU35 - - id: 36 - name: SKU36 - - id: 37 - name: SKU37 - - id: 38 - name: SKU38 - - id: 39 - name: SKU39 - - id: 40 - name: SKU40 - - id: 41 - name: SKU41 - - id: 42 - name: SKU42 - - id: 43 - name: SKU43 - - id: 44 - name: SKU44 - - id: 45 - name: SKU45 - - id: 46 - name: SKU46 - - id: 47 - name: SKU47 - - id: 48 - name: SKU48 - - id: 49 - name: SKU49 - - id: 50 - name: SKU50 - - id: 51 - name: SKU51 - - id: 52 - name: SKU52 - - id: 53 - name: SKU53 - - id: 54 - name: SKU54 - - id: 55 - name: SKU55 - - id: 56 - name: SKU56 - - id: 57 - name: SKU57 - - id: 58 - name: SKU58 - - id: 59 - name: SKU59 - - id: 60 - name: SKU60 - - id: 61 - name: SKU61 - - id: 62 - name: SKU62 - - id: 63 - name: SKU63 - - id: 64 - name: SKU64 - - id: 65 - name: SKU65 - - id: 66 - name: SKU66 - - id: 67 - name: SKU67 - - id: 68 - name: SKU68 - - id: 69 - name: SKU69 - - id: 70 - name: SKU70 - - id: 71 - name: SKU71 - - id: 72 - name: SKU72 - - id: 73 - name: SKU73 - - id: 74 - name: SKU74 - - id: 75 - name: SKU75 - - id: 76 - name: SKU76 - - id: 77 - name: SKU77 - - id: 78 - name: SKU78 - - id: 79 - name: SKU79 - - id: 80 - name: SKU80 - - id: 81 - name: SKU81 - - id: 82 - name: SKU82 - - id: 83 - name: SKU83 - - id: 84 - name: SKU84 - - id: 85 - name: SKU85 - - id: 86 - name: SKU86 - - id: 87 - name: SKU87 - - id: 88 - name: SKU88 - - id: 89 - name: SKU89 - - id: 90 - name: SKU90 - - id: 91 - name: SKU91 - - id: 92 - name: SKU92 - - id: 93 - name: SKU93 - - id: 94 - name: SKU94 - - id: 95 - name: SKU95 - - id: 96 - name: SKU96 - - id: 97 - name: SKU97 - - id: 98 - name: SKU98 - - id: 99 - name: SKU99 - - id: 100 - name: SKU100 - - id: 101 - name: SKU101 - - id: 102 - name: SKU102 - - id: 103 - name: SKU103 - - id: 104 - name: SKU104 - - id: 105 - name: SKU105 - - id: 106 - name: SKU106 - - id: 107 - name: SKU107 - - id: 108 - name: SKU108 - - id: 109 - name: SKU109 - - id: 110 - name: SKU110 - - id: 111 - name: SKU111 - - id: 112 - name: SKU112 - - id: 113 - name: SKU113 - - id: 114 - name: SKU114 - - id: 115 - name: SKU115 - - id: 116 - name: SKU116 - - id: 117 - name: SKU117 - - id: 118 - name: SKU118 - - id: 119 - name: SKU119 - - id: 120 - name: SKU120 - - id: 121 - name: SKU121 - - id: 122 - name: SKU122 - - id: 123 - name: SKU123 - - id: 124 - name: SKU124 - - id: 125 - name: SKU125 - - id: 126 - name: SKU126 - - id: 127 - name: SKU127 - - id: 128 - name: SKU128 - - id: 129 - name: SKU129 - - id: 130 - name: SKU130 - - id: 131 - name: SKU131 - - id: 132 - name: SKU132 - - id: 133 - name: SKU133 - - id: 134 - name: SKU134 - - id: 135 - name: SKU135 - - id: 136 - name: SKU136 - - id: 137 - name: SKU137 - - id: 138 - name: SKU138 - - id: 139 - name: SKU139 - - id: 140 - name: SKU140 - - id: 141 - name: SKU141 - - id: 142 - name: SKU142 - - id: 143 - name: SKU143 - - id: 144 - name: SKU144 - - id: 145 - name: SKU145 - - id: 146 - name: SKU146 - - id: 147 - name: SKU147 - - id: 148 - name: SKU148 - - id: 149 - name: SKU149 - - id: 150 - name: SKU150 - - id: 151 - name: SKU151 - - id: 152 - name: SKU152 - - id: 153 - name: SKU153 - - id: 154 - name: SKU154 - - id: 155 - name: SKU155 - - id: 156 - name: SKU156 - - id: 157 - name: SKU157 - - id: 158 - name: SKU158 - - id: 159 - name: SKU159 - - id: 160 - name: SKU160 - - id: 161 - name: SKU161 - - id: 162 - name: SKU162 - - id: 163 - name: SKU163 - - id: 164 - name: SKU164 - - id: 165 - name: SKU165 - - id: 166 - name: SKU166 - - id: 167 - name: SKU167 - - id: 168 - name: SKU168 - - id: 169 - name: SKU169 - - id: 170 - name: SKU170 - - id: 171 - name: SKU171 - - id: 172 - name: SKU172 - - id: 173 - name: SKU173 - - id: 174 - name: SKU174 - - id: 175 - name: SKU175 - - id: 176 - name: SKU176 - - id: 177 - name: SKU177 - - id: 178 - name: SKU178 - - id: 179 - name: SKU179 - - id: 180 - name: SKU180 - - id: 181 - name: SKU181 - - id: 182 - name: SKU182 - - id: 183 - name: SKU183 - - id: 184 - name: SKU184 - - id: 185 - name: SKU185 - - id: 186 - name: SKU186 - - id: 187 - name: SKU187 - - id: 188 - name: SKU188 - - id: 189 - name: SKU189 - - id: 190 - name: SKU190 - - id: 191 - name: SKU191 - - id: 192 - name: SKU192 - - id: 193 - name: SKU193 - - id: 194 - name: SKU194 - - id: 195 - name: SKU195 - - id: 196 - name: SKU196 - - id: 197 - name: SKU197 - - id: 198 - name: SKU198 - - id: 199 - name: SKU199 - - id: 200 - name: SKU200 - - id: 201 - name: SKU201 - - id: 202 - name: SKU202 - - id: 203 - name: SKU203 - - id: 204 - name: SKU204 - - id: 205 - name: SKU205 - - id: 206 - name: SKU206 - - id: 207 - name: SKU207 - - id: 208 - name: SKU208 - - id: 209 - name: SKU209 - - id: 210 - name: SKU210 - - id: 211 - name: SKU211 - - id: 212 - name: SKU212 - - id: 213 - name: SKU213 - - id: 214 - name: SKU214 - - id: 215 - name: SKU215 - - id: 216 - name: SKU216 - - id: 217 - name: SKU217 - - id: 218 - name: SKU218 - - id: 219 - name: SKU219 - - id: 220 - name: SKU220 - - id: 221 - name: SKU221 - - id: 222 - name: SKU222 - - id: 223 - name: SKU223 - - id: 224 - name: SKU224 - - id: 225 - name: SKU225 - - id: 226 - name: SKU226 - - id: 227 - name: SKU227 - - id: 228 - name: SKU228 - - id: 229 - name: SKU229 - - id: 230 - name: SKU230 - - id: 231 - name: SKU231 - - id: 232 - name: SKU232 - - id: 233 - name: SKU233 - - id: 234 - name: SKU234 - - id: 235 - name: SKU235 - - id: 236 - name: SKU236 - - id: 237 - name: SKU237 - - id: 238 - name: SKU238 - - id: 239 - name: SKU239 - - id: 240 - name: SKU240 - - id: 241 - name: SKU241 - - id: 242 - name: SKU242 - - id: 243 - name: SKU243 - - id: 244 - name: SKU244 - - id: 245 - name: SKU245 - - id: 246 - name: SKU246 - - id: 247 - name: SKU247 - - id: 248 - name: SKU248 - - id: 249 - name: SKU249 - - id: 250 - name: SKU250 - - id: 251 - name: SKU251 - - id: 252 - name: SKU252 - - id: 253 - name: SKU253 - - id: 254 - name: SKU254 - - id: 255 - name: SKU255 - - id: 256 - name: SKU256 - - id: 257 - name: SKU257 - - id: 258 - name: SKU258 - - id: 259 - name: SKU259 - - id: 260 - name: SKU260 - - id: 261 - name: SKU261 - - id: 262 - name: SKU262 - - id: 263 - name: SKU263 - - id: 264 - name: SKU264 - - id: 265 - name: SKU265 - - id: 266 - name: SKU266 - - id: 267 - name: SKU267 - - id: 268 - name: SKU268 - - id: 269 - name: SKU269 - - id: 270 - name: SKU270 - - id: 271 - name: SKU271 - - id: 272 - name: SKU272 - - id: 273 - name: SKU273 - - id: 274 - name: SKU274 - - id: 275 - name: SKU275 - - id: 276 - name: SKU276 - - id: 277 - name: SKU277 - - id: 278 - name: SKU278 - - id: 279 - name: SKU279 - - id: 280 - name: SKU280 - - id: 281 - name: SKU281 - - id: 282 - name: SKU282 - - id: 283 - name: SKU283 - - id: 284 - name: SKU284 - - id: 285 - name: SKU285 - - id: 286 - name: SKU286 - - id: 287 - name: SKU287 - - id: 288 - name: SKU288 - - id: 289 - name: SKU289 - - id: 290 - name: SKU290 - - id: 291 - name: SKU291 - - id: 292 - name: SKU292 - - id: 293 - name: SKU293 - - id: 294 - name: SKU294 - - id: 295 - name: SKU295 - - id: 296 - name: SKU296 - - id: 297 - name: SKU297 - - id: 298 - name: SKU298 - - id: 299 - name: SKU299 - - id: 300 - name: SKU300 - - id: 301 - name: SKU301 - - id: 302 - name: SKU302 - - id: 303 - name: SKU303 - - id: 304 - name: SKU304 - - id: 305 - name: SKU305 - - id: 306 - name: SKU306 - - id: 307 - name: SKU307 - - id: 308 - name: SKU308 - - id: 309 - name: SKU309 - - id: 310 - name: SKU310 - - id: 311 - name: SKU311 - - id: 312 - name: SKU312 - - id: 313 - name: SKU313 - - id: 314 - name: SKU314 - - id: 315 - name: SKU315 - - id: 316 - name: SKU316 - - id: 317 - name: SKU317 - - id: 318 - name: SKU318 - - id: 319 - name: SKU319 - - id: 320 - name: SKU320 - - id: 321 - name: SKU321 - - id: 322 - name: SKU322 - - id: 323 - name: SKU323 - - id: 324 - name: SKU324 - - id: 325 - name: SKU325 - - id: 326 - name: SKU326 - - id: 327 - name: SKU327 - - id: 328 - name: SKU328 - - id: 329 - name: SKU329 - - id: 330 - name: SKU330 - - id: 331 - name: SKU331 - - id: 332 - name: SKU332 - - id: 333 - name: SKU333 - - id: 334 - name: SKU334 - - id: 335 - name: SKU335 - - id: 336 - name: SKU336 - - id: 337 - name: SKU337 - - id: 338 - name: SKU338 - - id: 339 - name: SKU339 - - id: 340 - name: SKU340 - - id: 341 - name: SKU341 - - id: 342 - name: SKU342 - - id: 343 - name: SKU343 - - id: 344 - name: SKU344 - - id: 345 - name: SKU345 - - id: 346 - name: SKU346 - - id: 347 - name: SKU347 - - id: 348 - name: SKU348 - - id: 349 - name: SKU349 - - id: 350 - name: SKU350 - - id: 351 - name: SKU351 - - id: 352 - name: SKU352 - - id: 353 - name: SKU353 - - id: 354 - name: SKU354 - - id: 355 - name: SKU355 - - id: 356 - name: SKU356 - - id: 357 - name: SKU357 - - id: 358 - name: SKU358 - - id: 359 - name: SKU359 - - id: 360 - name: SKU360 - - id: 361 - name: SKU361 - - id: 362 - name: SKU362 - - id: 363 - name: SKU363 - - id: 364 - name: SKU364 - - id: 365 - name: SKU365 - - id: 366 - name: SKU366 - - id: 367 - name: SKU367 - - id: 368 - name: SKU368 - - id: 369 - name: SKU369 - - id: 370 - name: SKU370 - - id: 371 - name: SKU371 - - id: 372 - name: SKU372 - - id: 373 - name: SKU373 - - id: 374 - name: SKU374 - - id: 375 - name: SKU375 - - id: 376 - name: SKU376 - - id: 377 - name: SKU377 - - id: 378 - name: SKU378 - - id: 379 - name: SKU379 - - id: 380 - name: SKU380 - - id: 381 - name: SKU381 - - id: 382 - name: SKU382 - - id: 383 - name: SKU383 - - id: 384 - name: SKU384 - - id: 385 - name: SKU385 - - id: 386 - name: SKU386 - - id: 387 - name: SKU387 - - id: 388 - name: SKU388 - - id: 389 - name: SKU389 - - id: 390 - name: SKU390 - - id: 391 - name: SKU391 - - id: 392 - name: SKU392 - - id: 393 - name: SKU393 - - id: 394 - name: SKU394 - - id: 395 - name: SKU395 - - id: 396 - name: SKU396 - - id: 397 - name: SKU397 - - id: 398 - name: SKU398 - - id: 399 - name: SKU399 - - id: 400 - name: SKU400 - - id: 401 - name: SKU401 - - id: 402 - name: SKU402 - - id: 403 - name: SKU403 - - id: 404 - name: SKU404 - - id: 405 - name: SKU405 - - id: 406 - name: SKU406 - - id: 407 - name: SKU407 - - id: 408 - name: SKU408 - - id: 409 - name: SKU409 - - id: 410 - name: SKU410 - - id: 411 - name: SKU411 - - id: 412 - name: SKU412 - - id: 413 - name: SKU413 - - id: 414 - name: SKU414 - - id: 415 - name: SKU415 - - id: 416 - name: SKU416 - - id: 417 - name: SKU417 - - id: 418 - name: SKU418 - - id: 419 - name: SKU419 - - id: 420 - name: SKU420 - - id: 421 - name: SKU421 - - id: 422 - name: SKU422 - - id: 423 - name: SKU423 - - id: 424 - name: SKU424 - - id: 425 - name: SKU425 - - id: 426 - name: SKU426 - - id: 427 - name: SKU427 - - id: 428 - name: SKU428 - - id: 429 - name: SKU429 - - id: 430 - name: SKU430 - - id: 431 - name: SKU431 - - id: 432 - name: SKU432 - - id: 433 - name: SKU433 - - id: 434 - name: SKU434 - - id: 435 - name: SKU435 - - id: 436 - name: SKU436 - - id: 437 - name: SKU437 - - id: 438 - name: SKU438 - - id: 439 - name: SKU439 - - id: 440 - name: SKU440 - - id: 441 - name: SKU441 - - id: 442 - name: SKU442 - - id: 443 - name: SKU443 - - id: 444 - name: SKU444 - - id: 445 - name: SKU445 - - id: 446 - name: SKU446 - - id: 447 - name: SKU447 - - id: 448 - name: SKU448 - - id: 449 - name: SKU449 - - id: 450 - name: SKU450 - - id: 451 - name: SKU451 - - id: 452 - name: SKU452 - - id: 453 - name: SKU453 - - id: 454 - name: SKU454 - - id: 455 - name: SKU455 - - id: 456 - name: SKU456 - - id: 457 - name: SKU457 - - id: 458 - name: SKU458 - - id: 459 - name: SKU459 - - id: 460 - name: SKU460 - - id: 461 - name: SKU461 - - id: 462 - name: SKU462 - - id: 463 - name: SKU463 - - id: 464 - name: SKU464 - - id: 465 - name: SKU465 - - id: 466 - name: SKU466 - - id: 467 - name: SKU467 - - id: 468 - name: SKU468 - - id: 469 - name: SKU469 - - id: 470 - name: SKU470 - - id: 471 - name: SKU471 - - id: 472 - name: SKU472 - - id: 473 - name: SKU473 - - id: 474 - name: SKU474 - - id: 475 - name: SKU475 - - id: 476 - name: SKU476 - - id: 477 - name: SKU477 - - id: 478 - name: SKU478 - - id: 479 - name: SKU479 - - id: 480 - name: SKU480 - - id: 481 - name: SKU481 - - id: 482 - name: SKU482 - - id: 483 - name: SKU483 - - id: 484 - name: SKU484 - - id: 485 - name: SKU485 - - id: 486 - name: SKU486 - - id: 487 - name: SKU487 - - id: 488 - name: SKU488 - - id: 489 - name: SKU489 - - id: 490 - name: SKU490 - - id: 491 - name: SKU491 - - id: 492 - name: SKU492 - - id: 493 - name: SKU493 - - id: 494 - name: SKU494 - - id: 495 - name: SKU495 - - id: 496 - name: SKU496 - - id: 497 - name: SKU497 - - id: 498 - name: SKU498 - - id: 499 - name: SKU499 - - id: 500 - name: SKU500 - - id: 501 - name: SKU501 - - id: 502 - name: SKU502 - - id: 503 - name: SKU503 - - id: 504 - name: SKU504 - - id: 505 - name: SKU505 - - id: 506 - name: SKU506 - - id: 507 - name: SKU507 - - id: 508 - name: SKU508 - - id: 509 - name: SKU509 - - id: 510 - name: SKU510 - - id: 511 - name: SKU511 - - id: 512 - name: SKU512 - - id: 513 - name: SKU513 - - id: 514 - name: SKU514 - - id: 515 - name: SKU515 - - id: 516 - name: SKU516 - - id: 517 - name: SKU517 - - id: 518 - name: SKU518 - - id: 519 - name: SKU519 - - id: 520 - name: SKU520 - - id: 521 - name: SKU521 - - id: 522 - name: SKU522 - - id: 523 - name: SKU523 - - id: 524 - name: SKU524 - - id: 525 - name: SKU525 - - id: 526 - name: SKU526 - - id: 527 - name: SKU527 - - id: 528 - name: SKU528 - - id: 529 - name: SKU529 - - id: 530 - name: SKU530 - - id: 531 - name: SKU531 - - id: 532 - name: SKU532 - - id: 533 - name: SKU533 - - id: 534 - name: SKU534 - - id: 535 - name: SKU535 - - id: 536 - name: SKU536 - - id: 537 - name: SKU537 - - id: 538 - name: SKU538 - - id: 539 - name: SKU539 - - id: 540 - name: SKU540 - - id: 541 - name: SKU541 - - id: 542 - name: SKU542 - - id: 543 - name: SKU543 - - id: 544 - name: SKU544 - - id: 545 - name: SKU545 - - id: 546 - name: SKU546 - - id: 547 - name: SKU547 - - id: 548 - name: SKU548 - - id: 549 - name: SKU549 - - id: 550 - name: SKU550 - - id: 551 - name: SKU551 - - id: 552 - name: SKU552 - - id: 553 - name: SKU553 - - id: 554 - name: SKU554 - - id: 555 - name: SKU555 - - id: 556 - name: SKU556 - - id: 557 - name: SKU557 - - id: 558 - name: SKU558 - - id: 559 - name: SKU559 - - id: 560 - name: SKU560 - - id: 561 - name: SKU561 - - id: 562 - name: SKU562 - - id: 563 - name: SKU563 - - id: 564 - name: SKU564 - - id: 565 - name: SKU565 - - id: 566 - name: SKU566 - - id: 567 - name: SKU567 - - id: 568 - name: SKU568 - - id: 569 - name: SKU569 - - id: 570 - name: SKU570 - - id: 571 - name: SKU571 - - id: 572 - name: SKU572 - - id: 573 - name: SKU573 - - id: 574 - name: SKU574 - - id: 575 - name: SKU575 - - id: 576 - name: SKU576 - - id: 577 - name: SKU577 - - id: 578 - name: SKU578 - - id: 579 - name: SKU579 - - id: 580 - name: SKU580 - - id: 581 - name: SKU581 - - id: 582 - name: SKU582 - - id: 583 - name: SKU583 - - id: 584 - name: SKU584 - - id: 585 - name: SKU585 - - id: 586 - name: SKU586 - - id: 587 - name: SKU587 - - id: 588 - name: SKU588 - - id: 589 - name: SKU589 - - id: 590 - name: SKU590 - - id: 591 - name: SKU591 - - id: 592 - name: SKU592 - - id: 593 - name: SKU593 - - id: 594 - name: SKU594 - - id: 595 - name: SKU595 - - id: 596 - name: SKU596 - - id: 597 - name: SKU597 - - id: 598 - name: SKU598 - - id: 599 - name: SKU599 - - id: 600 - name: SKU600 - - id: 601 - name: SKU601 - - id: 602 - name: SKU602 - - id: 603 - name: SKU603 - - id: 604 - name: SKU604 - - id: 605 - name: SKU605 - - id: 606 - name: SKU606 - - id: 607 - name: SKU607 - - id: 608 - name: SKU608 - - id: 609 - name: SKU609 - - id: 610 - name: SKU610 - - id: 611 - name: SKU611 - - id: 612 - name: SKU612 - - id: 613 - name: SKU613 - - id: 614 - name: SKU614 - - id: 615 - name: SKU615 - - id: 616 - name: SKU616 - - id: 617 - name: SKU617 - - id: 618 - name: SKU618 - - id: 619 - name: SKU619 - - id: 620 - name: SKU620 - - id: 621 - name: SKU621 - - id: 622 - name: SKU622 - - id: 623 - name: SKU623 - - id: 624 - name: SKU624 - - id: 625 - name: SKU625 - - id: 626 - name: SKU626 - - id: 627 - name: SKU627 - - id: 628 - name: SKU628 - - id: 629 - name: SKU629 - - id: 630 - name: SKU630 - - id: 631 - name: SKU631 - - id: 632 - name: SKU632 - - id: 633 - name: SKU633 - - id: 634 - name: SKU634 - - id: 635 - name: SKU635 - - id: 636 - name: SKU636 - - id: 637 - name: SKU637 - - id: 638 - name: SKU638 - - id: 639 - name: SKU639 - - id: 640 - name: SKU640 - - id: 641 - name: SKU641 - - id: 642 - name: SKU642 - - id: 643 - name: SKU643 - - id: 644 - name: SKU644 - - id: 645 - name: SKU645 - - id: 646 - name: SKU646 - - id: 647 - name: SKU647 - - id: 648 - name: SKU648 - - id: 649 - name: SKU649 - - id: 650 - name: SKU650 - - id: 651 - name: SKU651 - - id: 652 - name: SKU652 - - id: 653 - name: SKU653 - - id: 654 - name: SKU654 - - id: 655 - name: SKU655 - - id: 656 - name: SKU656 - - id: 657 - name: SKU657 - - id: 658 - name: SKU658 - - id: 659 - name: SKU659 - - id: 660 - name: SKU660 - - id: 661 - name: SKU661 - - id: 662 - name: SKU662 - - id: 663 - name: SKU663 - - id: 664 - name: SKU664 - - id: 665 - name: SKU665 - - id: 666 - name: SKU666 - - id: 667 - name: SKU667 - - id: 668 - name: SKU668 - - id: 669 - name: SKU669 - - id: 670 - name: SKU670 - - id: 671 - name: SKU671 - - id: 672 - name: SKU672 - - id: 673 - name: SKU673 - - id: 674 - name: SKU674 - - id: 675 - name: SKU675 - - id: 676 - name: SKU676 - - id: 677 - name: SKU677 - - id: 678 - name: SKU678 - - id: 679 - name: SKU679 - - id: 680 - name: SKU680 - - id: 681 - name: SKU681 - - id: 682 - name: SKU682 - - id: 683 - name: SKU683 - - id: 684 - name: SKU684 - - id: 685 - name: SKU685 - - id: 686 - name: SKU686 - - id: 687 - name: SKU687 - - id: 688 - name: SKU688 - - id: 689 - name: SKU689 - - id: 690 - name: SKU690 - - id: 691 - name: SKU691 - - id: 692 - name: SKU692 - - id: 693 - name: SKU693 - - id: 694 - name: SKU694 - - id: 695 - name: SKU695 - - id: 696 - name: SKU696 - - id: 697 - name: SKU697 - - id: 698 - name: SKU698 - - id: 699 - name: SKU699 - - id: 700 - name: SKU700 - - id: 701 - name: SKU701 - - id: 702 - name: SKU702 - - id: 703 - name: SKU703 - - id: 704 - name: SKU704 - - id: 705 - name: SKU705 - - id: 706 - name: SKU706 - - id: 707 - name: SKU707 - - id: 708 - name: SKU708 - - id: 709 - name: SKU709 - - id: 710 - name: SKU710 - - id: 711 - name: SKU711 - - id: 712 - name: SKU712 - - id: 713 - name: SKU713 - - id: 714 - name: SKU714 - - id: 715 - name: SKU715 - - id: 716 - name: SKU716 - - id: 717 - name: SKU717 - - id: 718 - name: SKU718 - - id: 719 - name: SKU719 - - id: 720 - name: SKU720 - - id: 721 - name: SKU721 - - id: 722 - name: SKU722 - - id: 723 - name: SKU723 - - id: 724 - name: SKU724 - - id: 725 - name: SKU725 - - id: 726 - name: SKU726 - - id: 727 - name: SKU727 - - id: 728 - name: SKU728 - - id: 729 - name: SKU729 - - id: 730 - name: SKU730 - - id: 731 - name: SKU731 - - id: 732 - name: SKU732 - - id: 733 - name: SKU733 - - id: 734 - name: SKU734 - - id: 735 - name: SKU735 - - id: 736 - name: SKU736 - - id: 737 - name: SKU737 - - id: 738 - name: SKU738 - - id: 739 - name: SKU739 - - id: 740 - name: SKU740 - - id: 741 - name: SKU741 - - id: 742 - name: SKU742 - - id: 743 - name: SKU743 - - id: 744 - name: SKU744 - - id: 745 - name: SKU745 - - id: 746 - name: SKU746 - - id: 747 - name: SKU747 - - id: 748 - name: SKU748 - - id: 749 - name: SKU749 - - id: 750 - name: SKU750 - - id: 751 - name: SKU751 - - id: 752 - name: SKU752 - - id: 753 - name: SKU753 - - id: 754 - name: SKU754 - - id: 755 - name: SKU755 - - id: 756 - name: SKU756 - - id: 757 - name: SKU757 - - id: 758 - name: SKU758 - - id: 759 - name: SKU759 - - id: 760 - name: SKU760 - - id: 761 - name: SKU761 - - id: 762 - name: SKU762 - - id: 763 - name: SKU763 - - id: 764 - name: SKU764 - - id: 765 - name: SKU765 - - id: 766 - name: SKU766 - - id: 767 - name: SKU767 - - id: 768 - name: SKU768 - - id: 769 - name: SKU769 - - id: 770 - name: SKU770 - - id: 771 - name: SKU771 - - id: 772 - name: SKU772 - - id: 773 - name: SKU773 - - id: 774 - name: SKU774 - - id: 775 - name: SKU775 - - id: 776 - name: SKU776 - - id: 777 - name: SKU777 - - id: 778 - name: SKU778 - - id: 779 - name: SKU779 - - id: 780 - name: SKU780 - - id: 781 - name: SKU781 - - id: 782 - name: SKU782 - - id: 783 - name: SKU783 - - id: 784 - name: SKU784 - - id: 785 - name: SKU785 - - id: 786 - name: SKU786 - - id: 787 - name: SKU787 - - id: 788 - name: SKU788 - - id: 789 - name: SKU789 - - id: 790 - name: SKU790 - - id: 791 - name: SKU791 - - id: 792 - name: SKU792 - - id: 793 - name: SKU793 - - id: 794 - name: SKU794 - - id: 795 - name: SKU795 - - id: 796 - name: SKU796 - - id: 797 - name: SKU797 - - id: 798 - name: SKU798 - - id: 799 - name: SKU799 - - id: 800 - name: SKU800 - - id: 801 - name: SKU801 - - id: 802 - name: SKU802 - - id: 803 - name: SKU803 - - id: 804 - name: SKU804 - - id: 805 - name: SKU805 - - id: 806 - name: SKU806 - - id: 807 - name: SKU807 - - id: 808 - name: SKU808 - - id: 809 - name: SKU809 - - id: 810 - name: SKU810 - - id: 811 - name: SKU811 - - id: 812 - name: SKU812 - - id: 813 - name: SKU813 - - id: 814 - name: SKU814 - - id: 815 - name: SKU815 - - id: 816 - name: SKU816 - - id: 817 - name: SKU817 - - id: 818 - name: SKU818 - - id: 819 - name: SKU819 - - id: 820 - name: SKU820 - - id: 821 - name: SKU821 - - id: 822 - name: SKU822 - - id: 823 - name: SKU823 - - id: 824 - name: SKU824 - - id: 825 - name: SKU825 - - id: 826 - name: SKU826 - - id: 827 - name: SKU827 - - id: 828 - name: SKU828 - - id: 829 - name: SKU829 - - id: 830 - name: SKU830 - - id: 831 - name: SKU831 - - id: 832 - name: SKU832 - - id: 833 - name: SKU833 - - id: 834 - name: SKU834 - - id: 835 - name: SKU835 - - id: 836 - name: SKU836 - - id: 837 - name: SKU837 - - id: 838 - name: SKU838 - - id: 839 - name: SKU839 - - id: 840 - name: SKU840 - - id: 841 - name: SKU841 - - id: 842 - name: SKU842 - - id: 843 - name: SKU843 - - id: 844 - name: SKU844 - - id: 845 - name: SKU845 - - id: 846 - name: SKU846 - - id: 847 - name: SKU847 - - id: 848 - name: SKU848 - - id: 849 - name: SKU849 - - id: 850 - name: SKU850 - - id: 851 - name: SKU851 - - id: 852 - name: SKU852 - - id: 853 - name: SKU853 - - id: 854 - name: SKU854 - - id: 855 - name: SKU855 - - id: 856 - name: SKU856 - - id: 857 - name: SKU857 - - id: 858 - name: SKU858 - - id: 859 - name: SKU859 - - id: 860 - name: SKU860 - - id: 861 - name: SKU861 - - id: 862 - name: SKU862 - - id: 863 - name: SKU863 - - id: 864 - name: SKU864 - - id: 865 - name: SKU865 - - id: 866 - name: SKU866 - - id: 867 - name: SKU867 - - id: 868 - name: SKU868 - - id: 869 - name: SKU869 - - id: 870 - name: SKU870 - - id: 871 - name: SKU871 - - id: 872 - name: SKU872 - - id: 873 - name: SKU873 - - id: 874 - name: SKU874 - - id: 875 - name: SKU875 - - id: 876 - name: SKU876 - - id: 877 - name: SKU877 - - id: 878 - name: SKU878 - - id: 879 - name: SKU879 - - id: 880 - name: SKU880 - - id: 881 - name: SKU881 - - id: 882 - name: SKU882 - - id: 883 - name: SKU883 - - id: 884 - name: SKU884 - - id: 885 - name: SKU885 - - id: 886 - name: SKU886 - - id: 887 - name: SKU887 - - id: 888 - name: SKU888 - - id: 889 - name: SKU889 - - id: 890 - name: SKU890 - - id: 891 - name: SKU891 - - id: 892 - name: SKU892 - - id: 893 - name: SKU893 - - id: 894 - name: SKU894 - - id: 895 - name: SKU895 - - id: 896 - name: SKU896 - - id: 897 - name: SKU897 - - id: 898 - name: SKU898 - - id: 899 - name: SKU899 - - id: 900 - name: SKU900 - - id: 901 - name: SKU901 - - id: 902 - name: SKU902 - - id: 903 - name: SKU903 - - id: 904 - name: SKU904 - - id: 905 - name: SKU905 - - id: 906 - name: SKU906 - - id: 907 - name: SKU907 - - id: 908 - name: SKU908 - - id: 909 - name: SKU909 - - id: 910 - name: SKU910 - - id: 911 - name: SKU911 - - id: 912 - name: SKU912 - - id: 913 - name: SKU913 - - id: 914 - name: SKU914 - - id: 915 - name: SKU915 - - id: 916 - name: SKU916 - - id: 917 - name: SKU917 - - id: 918 - name: SKU918 - - id: 919 - name: SKU919 - - id: 920 - name: SKU920 - - id: 921 - name: SKU921 - - id: 922 - name: SKU922 - - id: 923 - name: SKU923 - - id: 924 - name: SKU924 - - id: 925 - name: SKU925 - - id: 926 - name: SKU926 - - id: 927 - name: SKU927 - - id: 928 - name: SKU928 - - id: 929 - name: SKU929 - - id: 930 - name: SKU930 - - id: 931 - name: SKU931 - - id: 932 - name: SKU932 - - id: 933 - name: SKU933 - - id: 934 - name: SKU934 - - id: 935 - name: SKU935 - - id: 936 - name: SKU936 - - id: 937 - name: SKU937 - - id: 938 - name: SKU938 - - id: 939 - name: SKU939 - - id: 940 - name: SKU940 - - id: 941 - name: SKU941 - - id: 942 - name: SKU942 - - id: 943 - name: SKU943 - - id: 944 - name: SKU944 - - id: 945 - name: SKU945 - - id: 946 - name: SKU946 - - id: 947 - name: SKU947 - - id: 948 - name: SKU948 - - id: 949 - name: SKU949 - - id: 950 - name: SKU950 - - id: 951 - name: SKU951 - - id: 952 - name: SKU952 - - id: 953 - name: SKU953 - - id: 954 - name: SKU954 - - id: 955 - name: SKU955 - - id: 956 - name: SKU956 - - id: 957 - name: SKU957 - - id: 958 - name: SKU958 - - id: 959 - name: SKU959 - - id: 960 - name: SKU960 - - id: 961 - name: SKU961 - - id: 962 - name: SKU962 - - id: 963 - name: SKU963 - - id: 964 - name: SKU964 - - id: 965 - name: SKU965 - - id: 966 - name: SKU966 - - id: 967 - name: SKU967 - - id: 968 - name: SKU968 - - id: 969 - name: SKU969 - - id: 970 - name: SKU970 - - id: 971 - name: SKU971 - - id: 972 - name: SKU972 - - id: 973 - name: SKU973 - - id: 974 - name: SKU974 - - id: 975 - name: SKU975 - - id: 976 - name: SKU976 - - id: 977 - name: SKU977 - - id: 978 - name: SKU978 - - id: 979 - name: SKU979 - - id: 980 - name: SKU980 - - id: 981 - name: SKU981 - - id: 982 - name: SKU982 - - id: 983 - name: SKU983 - - id: 984 - name: SKU984 - - id: 985 - name: SKU985 - - id: 986 - name: SKU986 - - id: 987 - name: SKU987 - - id: 988 - name: SKU988 - - id: 989 - name: SKU989 - - id: 990 - name: SKU990 - - id: 991 - name: SKU991 - - id: 992 - name: SKU992 - - id: 993 - name: SKU993 - - id: 994 - name: SKU994 - - id: 995 - name: SKU995 - - id: 996 - name: SKU996 - - id: 997 - name: SKU997 - - id: 998 - name: SKU998 - - id: 999 - name: SKU999 - topology: - STORE0: - SKU0: - - WAREHOUSE0 - SKU1: - - WAREHOUSE0 - SKU10: - - WAREHOUSE0 - SKU100: - - WAREHOUSE0 - SKU101: - - WAREHOUSE0 - SKU102: - - WAREHOUSE0 - SKU103: - - WAREHOUSE0 - SKU104: - - WAREHOUSE0 - SKU105: - - WAREHOUSE0 - SKU106: - - WAREHOUSE0 - SKU107: - - WAREHOUSE0 - SKU108: - - WAREHOUSE0 - SKU109: - - WAREHOUSE0 - SKU11: - - WAREHOUSE0 - SKU110: - - WAREHOUSE0 - SKU111: - - WAREHOUSE0 - SKU112: - - WAREHOUSE0 - SKU113: - - WAREHOUSE0 - SKU114: - - WAREHOUSE0 - SKU115: - - WAREHOUSE0 - SKU116: - - WAREHOUSE0 - SKU117: - - WAREHOUSE0 - SKU118: - - WAREHOUSE0 - SKU119: - - WAREHOUSE0 - SKU12: - - WAREHOUSE0 - SKU120: - - WAREHOUSE0 - SKU121: - - WAREHOUSE0 - SKU122: - - WAREHOUSE0 - SKU123: - - WAREHOUSE0 - SKU124: - - WAREHOUSE0 - SKU125: - - WAREHOUSE0 - SKU126: - - WAREHOUSE0 - SKU127: - - WAREHOUSE0 - SKU128: - - WAREHOUSE0 - SKU129: - - WAREHOUSE0 - SKU13: - - WAREHOUSE0 - SKU130: - - WAREHOUSE0 - SKU131: - - WAREHOUSE0 - SKU132: - - WAREHOUSE0 - SKU133: - - WAREHOUSE0 - SKU134: - - WAREHOUSE0 - SKU135: - - WAREHOUSE0 - SKU136: - - WAREHOUSE0 - SKU137: - - WAREHOUSE0 - SKU138: - - WAREHOUSE0 - SKU139: - - WAREHOUSE0 - SKU14: - - WAREHOUSE0 - SKU140: - - WAREHOUSE0 - SKU141: - - WAREHOUSE0 - SKU142: - - WAREHOUSE0 - SKU143: - - WAREHOUSE0 - SKU144: - - WAREHOUSE0 - SKU145: - - WAREHOUSE0 - SKU146: - - WAREHOUSE0 - SKU147: - - WAREHOUSE0 - SKU148: - - WAREHOUSE0 - SKU149: - - WAREHOUSE0 - SKU15: - - WAREHOUSE0 - SKU150: - - WAREHOUSE0 - SKU151: - - WAREHOUSE0 - SKU152: - - WAREHOUSE0 - SKU153: - - WAREHOUSE0 - SKU154: - - WAREHOUSE0 - SKU155: - - WAREHOUSE0 - SKU156: - - WAREHOUSE0 - SKU157: - - WAREHOUSE0 - SKU158: - - WAREHOUSE0 - SKU159: - - WAREHOUSE0 - SKU16: - - WAREHOUSE0 - SKU160: - - WAREHOUSE0 - SKU161: - - WAREHOUSE0 - SKU162: - - WAREHOUSE0 - SKU163: - - WAREHOUSE0 - SKU164: - - WAREHOUSE0 - SKU165: - - WAREHOUSE0 - SKU166: - - WAREHOUSE0 - SKU167: - - WAREHOUSE0 - SKU168: - - WAREHOUSE0 - SKU169: - - WAREHOUSE0 - SKU17: - - WAREHOUSE0 - SKU170: - - WAREHOUSE0 - SKU171: - - WAREHOUSE0 - SKU172: - - WAREHOUSE0 - SKU173: - - WAREHOUSE0 - SKU174: - - WAREHOUSE0 - SKU175: - - WAREHOUSE0 - SKU176: - - WAREHOUSE0 - SKU177: - - WAREHOUSE0 - SKU178: - - WAREHOUSE0 - SKU179: - - WAREHOUSE0 - SKU18: - - WAREHOUSE0 - SKU180: - - WAREHOUSE0 - SKU181: - - WAREHOUSE0 - SKU182: - - WAREHOUSE0 - SKU183: - - WAREHOUSE0 - SKU184: - - WAREHOUSE0 - SKU185: - - WAREHOUSE0 - SKU186: - - WAREHOUSE0 - SKU187: - - WAREHOUSE0 - SKU188: - - WAREHOUSE0 - SKU189: - - WAREHOUSE0 - SKU19: - - WAREHOUSE0 - SKU190: - - WAREHOUSE0 - SKU191: - - WAREHOUSE0 - SKU192: - - WAREHOUSE0 - SKU193: - - WAREHOUSE0 - SKU194: - - WAREHOUSE0 - SKU195: - - WAREHOUSE0 - SKU196: - - WAREHOUSE0 - SKU197: - - WAREHOUSE0 - SKU198: - - WAREHOUSE0 - SKU199: - - WAREHOUSE0 - SKU2: - - WAREHOUSE0 - SKU20: - - WAREHOUSE0 - SKU200: - - WAREHOUSE0 - SKU201: - - WAREHOUSE0 - SKU202: - - WAREHOUSE0 - SKU203: - - WAREHOUSE0 - SKU204: - - WAREHOUSE0 - SKU205: - - WAREHOUSE0 - SKU206: - - WAREHOUSE0 - SKU207: - - WAREHOUSE0 - SKU208: - - WAREHOUSE0 - SKU209: - - WAREHOUSE0 - SKU21: - - WAREHOUSE0 - SKU210: - - WAREHOUSE0 - SKU211: - - WAREHOUSE0 - SKU212: - - WAREHOUSE0 - SKU213: - - WAREHOUSE0 - SKU214: - - WAREHOUSE0 - SKU215: - - WAREHOUSE0 - SKU216: - - WAREHOUSE0 - SKU217: - - WAREHOUSE0 - SKU218: - - WAREHOUSE0 - SKU219: - - WAREHOUSE0 - SKU22: - - WAREHOUSE0 - SKU220: - - WAREHOUSE0 - SKU221: - - WAREHOUSE0 - SKU222: - - WAREHOUSE0 - SKU223: - - WAREHOUSE0 - SKU224: - - WAREHOUSE0 - SKU225: - - WAREHOUSE0 - SKU226: - - WAREHOUSE0 - SKU227: - - WAREHOUSE0 - SKU228: - - WAREHOUSE0 - SKU229: - - WAREHOUSE0 - SKU23: - - WAREHOUSE0 - SKU230: - - WAREHOUSE0 - SKU231: - - WAREHOUSE0 - SKU232: - - WAREHOUSE0 - SKU233: - - WAREHOUSE0 - SKU234: - - WAREHOUSE0 - SKU235: - - WAREHOUSE0 - SKU236: - - WAREHOUSE0 - SKU237: - - WAREHOUSE0 - SKU238: - - WAREHOUSE0 - SKU239: - - WAREHOUSE0 - SKU24: - - WAREHOUSE0 - SKU240: - - WAREHOUSE0 - SKU241: - - WAREHOUSE0 - SKU242: - - WAREHOUSE0 - SKU243: - - WAREHOUSE0 - SKU244: - - WAREHOUSE0 - SKU245: - - WAREHOUSE0 - SKU246: - - WAREHOUSE0 - SKU247: - - WAREHOUSE0 - SKU248: - - WAREHOUSE0 - SKU249: - - WAREHOUSE0 - SKU25: - - WAREHOUSE0 - SKU250: - - WAREHOUSE0 - SKU251: - - WAREHOUSE0 - SKU252: - - WAREHOUSE0 - SKU253: - - WAREHOUSE0 - SKU254: - - WAREHOUSE0 - SKU255: - - WAREHOUSE0 - SKU256: - - WAREHOUSE0 - SKU257: - - WAREHOUSE0 - SKU258: - - WAREHOUSE0 - SKU259: - - WAREHOUSE0 - SKU26: - - WAREHOUSE0 - SKU260: - - WAREHOUSE0 - SKU261: - - WAREHOUSE0 - SKU262: - - WAREHOUSE0 - SKU263: - - WAREHOUSE0 - SKU264: - - WAREHOUSE0 - SKU265: - - WAREHOUSE0 - SKU266: - - WAREHOUSE0 - SKU267: - - WAREHOUSE0 - SKU268: - - WAREHOUSE0 - SKU269: - - WAREHOUSE0 - SKU27: - - WAREHOUSE0 - SKU270: - - WAREHOUSE0 - SKU271: - - WAREHOUSE0 - SKU272: - - WAREHOUSE0 - SKU273: - - WAREHOUSE0 - SKU274: - - WAREHOUSE0 - SKU275: - - WAREHOUSE0 - SKU276: - - WAREHOUSE0 - SKU277: - - WAREHOUSE0 - SKU278: - - WAREHOUSE0 - SKU279: - - WAREHOUSE0 - SKU28: - - WAREHOUSE0 - SKU280: - - WAREHOUSE0 - SKU281: - - WAREHOUSE0 - SKU282: - - WAREHOUSE0 - SKU283: - - WAREHOUSE0 - SKU284: - - WAREHOUSE0 - SKU285: - - WAREHOUSE0 - SKU286: - - WAREHOUSE0 - SKU287: - - WAREHOUSE0 - SKU288: - - WAREHOUSE0 - SKU289: - - WAREHOUSE0 - SKU29: - - WAREHOUSE0 - SKU290: - - WAREHOUSE0 - SKU291: - - WAREHOUSE0 - SKU292: - - WAREHOUSE0 - SKU293: - - WAREHOUSE0 - SKU294: - - WAREHOUSE0 - SKU295: - - WAREHOUSE0 - SKU296: - - WAREHOUSE0 - SKU297: - - WAREHOUSE0 - SKU298: - - WAREHOUSE0 - SKU299: - - WAREHOUSE0 - SKU3: - - WAREHOUSE0 - SKU30: - - WAREHOUSE0 - SKU300: - - WAREHOUSE0 - SKU301: - - WAREHOUSE0 - SKU302: - - WAREHOUSE0 - SKU303: - - WAREHOUSE0 - SKU304: - - WAREHOUSE0 - SKU305: - - WAREHOUSE0 - SKU306: - - WAREHOUSE0 - SKU307: - - WAREHOUSE0 - SKU308: - - WAREHOUSE0 - SKU309: - - WAREHOUSE0 - SKU31: - - WAREHOUSE0 - SKU310: - - WAREHOUSE0 - SKU311: - - WAREHOUSE0 - SKU312: - - WAREHOUSE0 - SKU313: - - WAREHOUSE0 - SKU314: - - WAREHOUSE0 - SKU315: - - WAREHOUSE0 - SKU316: - - WAREHOUSE0 - SKU317: - - WAREHOUSE0 - SKU318: - - WAREHOUSE0 - SKU319: - - WAREHOUSE0 - SKU32: - - WAREHOUSE0 - SKU320: - - WAREHOUSE0 - SKU321: - - WAREHOUSE0 - SKU322: - - WAREHOUSE0 - SKU323: - - WAREHOUSE0 - SKU324: - - WAREHOUSE0 - SKU325: - - WAREHOUSE0 - SKU326: - - WAREHOUSE0 - SKU327: - - WAREHOUSE0 - SKU328: - - WAREHOUSE0 - SKU329: - - WAREHOUSE0 - SKU33: - - WAREHOUSE0 - SKU330: - - WAREHOUSE0 - SKU331: - - WAREHOUSE0 - SKU332: - - WAREHOUSE0 - SKU333: - - WAREHOUSE0 - SKU334: - - WAREHOUSE0 - SKU335: - - WAREHOUSE0 - SKU336: - - WAREHOUSE0 - SKU337: - - WAREHOUSE0 - SKU338: - - WAREHOUSE0 - SKU339: - - WAREHOUSE0 - SKU34: - - WAREHOUSE0 - SKU340: - - WAREHOUSE0 - SKU341: - - WAREHOUSE0 - SKU342: - - WAREHOUSE0 - SKU343: - - WAREHOUSE0 - SKU344: - - WAREHOUSE0 - SKU345: - - WAREHOUSE0 - SKU346: - - WAREHOUSE0 - SKU347: - - WAREHOUSE0 - SKU348: - - WAREHOUSE0 - SKU349: - - WAREHOUSE0 - SKU35: - - WAREHOUSE0 - SKU350: - - WAREHOUSE0 - SKU351: - - WAREHOUSE0 - SKU352: - - WAREHOUSE0 - SKU353: - - WAREHOUSE0 - SKU354: - - WAREHOUSE0 - SKU355: - - WAREHOUSE0 - SKU356: - - WAREHOUSE0 - SKU357: - - WAREHOUSE0 - SKU358: - - WAREHOUSE0 - SKU359: - - WAREHOUSE0 - SKU36: - - WAREHOUSE0 - SKU360: - - WAREHOUSE0 - SKU361: - - WAREHOUSE0 - SKU362: - - WAREHOUSE0 - SKU363: - - WAREHOUSE0 - SKU364: - - WAREHOUSE0 - SKU365: - - WAREHOUSE0 - SKU366: - - WAREHOUSE0 - SKU367: - - WAREHOUSE0 - SKU368: - - WAREHOUSE0 - SKU369: - - WAREHOUSE0 - SKU37: - - WAREHOUSE0 - SKU370: - - WAREHOUSE0 - SKU371: - - WAREHOUSE0 - SKU372: - - WAREHOUSE0 - SKU373: - - WAREHOUSE0 - SKU374: - - WAREHOUSE0 - SKU375: - - WAREHOUSE0 - SKU376: - - WAREHOUSE0 - SKU377: - - WAREHOUSE0 - SKU378: - - WAREHOUSE0 - SKU379: - - WAREHOUSE0 - SKU38: - - WAREHOUSE0 - SKU380: - - WAREHOUSE0 - SKU381: - - WAREHOUSE0 - SKU382: - - WAREHOUSE0 - SKU383: - - WAREHOUSE0 - SKU384: - - WAREHOUSE0 - SKU385: - - WAREHOUSE0 - SKU386: - - WAREHOUSE0 - SKU387: - - WAREHOUSE0 - SKU388: - - WAREHOUSE0 - SKU389: - - WAREHOUSE0 - SKU39: - - WAREHOUSE0 - SKU390: - - WAREHOUSE0 - SKU391: - - WAREHOUSE0 - SKU392: - - WAREHOUSE0 - SKU393: - - WAREHOUSE0 - SKU394: - - WAREHOUSE0 - SKU395: - - WAREHOUSE0 - SKU396: - - WAREHOUSE0 - SKU397: - - WAREHOUSE0 - SKU398: - - WAREHOUSE0 - SKU399: - - WAREHOUSE0 - SKU4: - - WAREHOUSE0 - SKU40: - - WAREHOUSE0 - SKU400: - - WAREHOUSE0 - SKU401: - - WAREHOUSE0 - SKU402: - - WAREHOUSE0 - SKU403: - - WAREHOUSE0 - SKU404: - - WAREHOUSE0 - SKU405: - - WAREHOUSE0 - SKU406: - - WAREHOUSE0 - SKU407: - - WAREHOUSE0 - SKU408: - - WAREHOUSE0 - SKU409: - - WAREHOUSE0 - SKU41: - - WAREHOUSE0 - SKU410: - - WAREHOUSE0 - SKU411: - - WAREHOUSE0 - SKU412: - - WAREHOUSE0 - SKU413: - - WAREHOUSE0 - SKU414: - - WAREHOUSE0 - SKU415: - - WAREHOUSE0 - SKU416: - - WAREHOUSE0 - SKU417: - - WAREHOUSE0 - SKU418: - - WAREHOUSE0 - SKU419: - - WAREHOUSE0 - SKU42: - - WAREHOUSE0 - SKU420: - - WAREHOUSE0 - SKU421: - - WAREHOUSE0 - SKU422: - - WAREHOUSE0 - SKU423: - - WAREHOUSE0 - SKU424: - - WAREHOUSE0 - SKU425: - - WAREHOUSE0 - SKU426: - - WAREHOUSE0 - SKU427: - - WAREHOUSE0 - SKU428: - - WAREHOUSE0 - SKU429: - - WAREHOUSE0 - SKU43: - - WAREHOUSE0 - SKU430: - - WAREHOUSE0 - SKU431: - - WAREHOUSE0 - SKU432: - - WAREHOUSE0 - SKU433: - - WAREHOUSE0 - SKU434: - - WAREHOUSE0 - SKU435: - - WAREHOUSE0 - SKU436: - - WAREHOUSE0 - SKU437: - - WAREHOUSE0 - SKU438: - - WAREHOUSE0 - SKU439: - - WAREHOUSE0 - SKU44: - - WAREHOUSE0 - SKU440: - - WAREHOUSE0 - SKU441: - - WAREHOUSE0 - SKU442: - - WAREHOUSE0 - SKU443: - - WAREHOUSE0 - SKU444: - - WAREHOUSE0 - SKU445: - - WAREHOUSE0 - SKU446: - - WAREHOUSE0 - SKU447: - - WAREHOUSE0 - SKU448: - - WAREHOUSE0 - SKU449: - - WAREHOUSE0 - SKU45: - - WAREHOUSE0 - SKU450: - - WAREHOUSE0 - SKU451: - - WAREHOUSE0 - SKU452: - - WAREHOUSE0 - SKU453: - - WAREHOUSE0 - SKU454: - - WAREHOUSE0 - SKU455: - - WAREHOUSE0 - SKU456: - - WAREHOUSE0 - SKU457: - - WAREHOUSE0 - SKU458: - - WAREHOUSE0 - SKU459: - - WAREHOUSE0 - SKU46: - - WAREHOUSE0 - SKU460: - - WAREHOUSE0 - SKU461: - - WAREHOUSE0 - SKU462: - - WAREHOUSE0 - SKU463: - - WAREHOUSE0 - SKU464: - - WAREHOUSE0 - SKU465: - - WAREHOUSE0 - SKU466: - - WAREHOUSE0 - SKU467: - - WAREHOUSE0 - SKU468: - - WAREHOUSE0 - SKU469: - - WAREHOUSE0 - SKU47: - - WAREHOUSE0 - SKU470: - - WAREHOUSE0 - SKU471: - - WAREHOUSE0 - SKU472: - - WAREHOUSE0 - SKU473: - - WAREHOUSE0 - SKU474: - - WAREHOUSE0 - SKU475: - - WAREHOUSE0 - SKU476: - - WAREHOUSE0 - SKU477: - - WAREHOUSE0 - SKU478: - - WAREHOUSE0 - SKU479: - - WAREHOUSE0 - SKU48: - - WAREHOUSE0 - SKU480: - - WAREHOUSE0 - SKU481: - - WAREHOUSE0 - SKU482: - - WAREHOUSE0 - SKU483: - - WAREHOUSE0 - SKU484: - - WAREHOUSE0 - SKU485: - - WAREHOUSE0 - SKU486: - - WAREHOUSE0 - SKU487: - - WAREHOUSE0 - SKU488: - - WAREHOUSE0 - SKU489: - - WAREHOUSE0 - SKU49: - - WAREHOUSE0 - SKU490: - - WAREHOUSE0 - SKU491: - - WAREHOUSE0 - SKU492: - - WAREHOUSE0 - SKU493: - - WAREHOUSE0 - SKU494: - - WAREHOUSE0 - SKU495: - - WAREHOUSE0 - SKU496: - - WAREHOUSE0 - SKU497: - - WAREHOUSE0 - SKU498: - - WAREHOUSE0 - SKU499: - - WAREHOUSE0 - SKU5: - - WAREHOUSE0 - SKU50: - - WAREHOUSE0 - SKU500: - - WAREHOUSE0 - SKU501: - - WAREHOUSE0 - SKU502: - - WAREHOUSE0 - SKU503: - - WAREHOUSE0 - SKU504: - - WAREHOUSE0 - SKU505: - - WAREHOUSE0 - SKU506: - - WAREHOUSE0 - SKU507: - - WAREHOUSE0 - SKU508: - - WAREHOUSE0 - SKU509: - - WAREHOUSE0 - SKU51: - - WAREHOUSE0 - SKU510: - - WAREHOUSE0 - SKU511: - - WAREHOUSE0 - SKU512: - - WAREHOUSE0 - SKU513: - - WAREHOUSE0 - SKU514: - - WAREHOUSE0 - SKU515: - - WAREHOUSE0 - SKU516: - - WAREHOUSE0 - SKU517: - - WAREHOUSE0 - SKU518: - - WAREHOUSE0 - SKU519: - - WAREHOUSE0 - SKU52: - - WAREHOUSE0 - SKU520: - - WAREHOUSE0 - SKU521: - - WAREHOUSE0 - SKU522: - - WAREHOUSE0 - SKU523: - - WAREHOUSE0 - SKU524: - - WAREHOUSE0 - SKU525: - - WAREHOUSE0 - SKU526: - - WAREHOUSE0 - SKU527: - - WAREHOUSE0 - SKU528: - - WAREHOUSE0 - SKU529: - - WAREHOUSE0 - SKU53: - - WAREHOUSE0 - SKU530: - - WAREHOUSE0 - SKU531: - - WAREHOUSE0 - SKU532: - - WAREHOUSE0 - SKU533: - - WAREHOUSE0 - SKU534: - - WAREHOUSE0 - SKU535: - - WAREHOUSE0 - SKU536: - - WAREHOUSE0 - SKU537: - - WAREHOUSE0 - SKU538: - - WAREHOUSE0 - SKU539: - - WAREHOUSE0 - SKU54: - - WAREHOUSE0 - SKU540: - - WAREHOUSE0 - SKU541: - - WAREHOUSE0 - SKU542: - - WAREHOUSE0 - SKU543: - - WAREHOUSE0 - SKU544: - - WAREHOUSE0 - SKU545: - - WAREHOUSE0 - SKU546: - - WAREHOUSE0 - SKU547: - - WAREHOUSE0 - SKU548: - - WAREHOUSE0 - SKU549: - - WAREHOUSE0 - SKU55: - - WAREHOUSE0 - SKU550: - - WAREHOUSE0 - SKU551: - - WAREHOUSE0 - SKU552: - - WAREHOUSE0 - SKU553: - - WAREHOUSE0 - SKU554: - - WAREHOUSE0 - SKU555: - - WAREHOUSE0 - SKU556: - - WAREHOUSE0 - SKU557: - - WAREHOUSE0 - SKU558: - - WAREHOUSE0 - SKU559: - - WAREHOUSE0 - SKU56: - - WAREHOUSE0 - SKU560: - - WAREHOUSE0 - SKU561: - - WAREHOUSE0 - SKU562: - - WAREHOUSE0 - SKU563: - - WAREHOUSE0 - SKU564: - - WAREHOUSE0 - SKU565: - - WAREHOUSE0 - SKU566: - - WAREHOUSE0 - SKU567: - - WAREHOUSE0 - SKU568: - - WAREHOUSE0 - SKU569: - - WAREHOUSE0 - SKU57: - - WAREHOUSE0 - SKU570: - - WAREHOUSE0 - SKU571: - - WAREHOUSE0 - SKU572: - - WAREHOUSE0 - SKU573: - - WAREHOUSE0 - SKU574: - - WAREHOUSE0 - SKU575: - - WAREHOUSE0 - SKU576: - - WAREHOUSE0 - SKU577: - - WAREHOUSE0 - SKU578: - - WAREHOUSE0 - SKU579: - - WAREHOUSE0 - SKU58: - - WAREHOUSE0 - SKU580: - - WAREHOUSE0 - SKU581: - - WAREHOUSE0 - SKU582: - - WAREHOUSE0 - SKU583: - - WAREHOUSE0 - SKU584: - - WAREHOUSE0 - SKU585: - - WAREHOUSE0 - SKU586: - - WAREHOUSE0 - SKU587: - - WAREHOUSE0 - SKU588: - - WAREHOUSE0 - SKU589: - - WAREHOUSE0 - SKU59: - - WAREHOUSE0 - SKU590: - - WAREHOUSE0 - SKU591: - - WAREHOUSE0 - SKU592: - - WAREHOUSE0 - SKU593: - - WAREHOUSE0 - SKU594: - - WAREHOUSE0 - SKU595: - - WAREHOUSE0 - SKU596: - - WAREHOUSE0 - SKU597: - - WAREHOUSE0 - SKU598: - - WAREHOUSE0 - SKU599: - - WAREHOUSE0 - SKU6: - - WAREHOUSE0 - SKU60: - - WAREHOUSE0 - SKU600: - - WAREHOUSE0 - SKU601: - - WAREHOUSE0 - SKU602: - - WAREHOUSE0 - SKU603: - - WAREHOUSE0 - SKU604: - - WAREHOUSE0 - SKU605: - - WAREHOUSE0 - SKU606: - - WAREHOUSE0 - SKU607: - - WAREHOUSE0 - SKU608: - - WAREHOUSE0 - SKU609: - - WAREHOUSE0 - SKU61: - - WAREHOUSE0 - SKU610: - - WAREHOUSE0 - SKU611: - - WAREHOUSE0 - SKU612: - - WAREHOUSE0 - SKU613: - - WAREHOUSE0 - SKU614: - - WAREHOUSE0 - SKU615: - - WAREHOUSE0 - SKU616: - - WAREHOUSE0 - SKU617: - - WAREHOUSE0 - SKU618: - - WAREHOUSE0 - SKU619: - - WAREHOUSE0 - SKU62: - - WAREHOUSE0 - SKU620: - - WAREHOUSE0 - SKU621: - - WAREHOUSE0 - SKU622: - - WAREHOUSE0 - SKU623: - - WAREHOUSE0 - SKU624: - - WAREHOUSE0 - SKU625: - - WAREHOUSE0 - SKU626: - - WAREHOUSE0 - SKU627: - - WAREHOUSE0 - SKU628: - - WAREHOUSE0 - SKU629: - - WAREHOUSE0 - SKU63: - - WAREHOUSE0 - SKU630: - - WAREHOUSE0 - SKU631: - - WAREHOUSE0 - SKU632: - - WAREHOUSE0 - SKU633: - - WAREHOUSE0 - SKU634: - - WAREHOUSE0 - SKU635: - - WAREHOUSE0 - SKU636: - - WAREHOUSE0 - SKU637: - - WAREHOUSE0 - SKU638: - - WAREHOUSE0 - SKU639: - - WAREHOUSE0 - SKU64: - - WAREHOUSE0 - SKU640: - - WAREHOUSE0 - SKU641: - - WAREHOUSE0 - SKU642: - - WAREHOUSE0 - SKU643: - - WAREHOUSE0 - SKU644: - - WAREHOUSE0 - SKU645: - - WAREHOUSE0 - SKU646: - - WAREHOUSE0 - SKU647: - - WAREHOUSE0 - SKU648: - - WAREHOUSE0 - SKU649: - - WAREHOUSE0 - SKU65: - - WAREHOUSE0 - SKU650: - - WAREHOUSE0 - SKU651: - - WAREHOUSE0 - SKU652: - - WAREHOUSE0 - SKU653: - - WAREHOUSE0 - SKU654: - - WAREHOUSE0 - SKU655: - - WAREHOUSE0 - SKU656: - - WAREHOUSE0 - SKU657: - - WAREHOUSE0 - SKU658: - - WAREHOUSE0 - SKU659: - - WAREHOUSE0 - SKU66: - - WAREHOUSE0 - SKU660: - - WAREHOUSE0 - SKU661: - - WAREHOUSE0 - SKU662: - - WAREHOUSE0 - SKU663: - - WAREHOUSE0 - SKU664: - - WAREHOUSE0 - SKU665: - - WAREHOUSE0 - SKU666: - - WAREHOUSE0 - SKU667: - - WAREHOUSE0 - SKU668: - - WAREHOUSE0 - SKU669: - - WAREHOUSE0 - SKU67: - - WAREHOUSE0 - SKU670: - - WAREHOUSE0 - SKU671: - - WAREHOUSE0 - SKU672: - - WAREHOUSE0 - SKU673: - - WAREHOUSE0 - SKU674: - - WAREHOUSE0 - SKU675: - - WAREHOUSE0 - SKU676: - - WAREHOUSE0 - SKU677: - - WAREHOUSE0 - SKU678: - - WAREHOUSE0 - SKU679: - - WAREHOUSE0 - SKU68: - - WAREHOUSE0 - SKU680: - - WAREHOUSE0 - SKU681: - - WAREHOUSE0 - SKU682: - - WAREHOUSE0 - SKU683: - - WAREHOUSE0 - SKU684: - - WAREHOUSE0 - SKU685: - - WAREHOUSE0 - SKU686: - - WAREHOUSE0 - SKU687: - - WAREHOUSE0 - SKU688: - - WAREHOUSE0 - SKU689: - - WAREHOUSE0 - SKU69: - - WAREHOUSE0 - SKU690: - - WAREHOUSE0 - SKU691: - - WAREHOUSE0 - SKU692: - - WAREHOUSE0 - SKU693: - - WAREHOUSE0 - SKU694: - - WAREHOUSE0 - SKU695: - - WAREHOUSE0 - SKU696: - - WAREHOUSE0 - SKU697: - - WAREHOUSE0 - SKU698: - - WAREHOUSE0 - SKU699: - - WAREHOUSE0 - SKU7: - - WAREHOUSE0 - SKU70: - - WAREHOUSE0 - SKU700: - - WAREHOUSE0 - SKU701: - - WAREHOUSE0 - SKU702: - - WAREHOUSE0 - SKU703: - - WAREHOUSE0 - SKU704: - - WAREHOUSE0 - SKU705: - - WAREHOUSE0 - SKU706: - - WAREHOUSE0 - SKU707: - - WAREHOUSE0 - SKU708: - - WAREHOUSE0 - SKU709: - - WAREHOUSE0 - SKU71: - - WAREHOUSE0 - SKU710: - - WAREHOUSE0 - SKU711: - - WAREHOUSE0 - SKU712: - - WAREHOUSE0 - SKU713: - - WAREHOUSE0 - SKU714: - - WAREHOUSE0 - SKU715: - - WAREHOUSE0 - SKU716: - - WAREHOUSE0 - SKU717: - - WAREHOUSE0 - SKU718: - - WAREHOUSE0 - SKU719: - - WAREHOUSE0 - SKU72: - - WAREHOUSE0 - SKU720: - - WAREHOUSE0 - SKU721: - - WAREHOUSE0 - SKU722: - - WAREHOUSE0 - SKU723: - - WAREHOUSE0 - SKU724: - - WAREHOUSE0 - SKU725: - - WAREHOUSE0 - SKU726: - - WAREHOUSE0 - SKU727: - - WAREHOUSE0 - SKU728: - - WAREHOUSE0 - SKU729: - - WAREHOUSE0 - SKU73: - - WAREHOUSE0 - SKU730: - - WAREHOUSE0 - SKU731: - - WAREHOUSE0 - SKU732: - - WAREHOUSE0 - SKU733: - - WAREHOUSE0 - SKU734: - - WAREHOUSE0 - SKU735: - - WAREHOUSE0 - SKU736: - - WAREHOUSE0 - SKU737: - - WAREHOUSE0 - SKU738: - - WAREHOUSE0 - SKU739: - - WAREHOUSE0 - SKU74: - - WAREHOUSE0 - SKU740: - - WAREHOUSE0 - SKU741: - - WAREHOUSE0 - SKU742: - - WAREHOUSE0 - SKU743: - - WAREHOUSE0 - SKU744: - - WAREHOUSE0 - SKU745: - - WAREHOUSE0 - SKU746: - - WAREHOUSE0 - SKU747: - - WAREHOUSE0 - SKU748: - - WAREHOUSE0 - SKU749: - - WAREHOUSE0 - SKU75: - - WAREHOUSE0 - SKU750: - - WAREHOUSE0 - SKU751: - - WAREHOUSE0 - SKU752: - - WAREHOUSE0 - SKU753: - - WAREHOUSE0 - SKU754: - - WAREHOUSE0 - SKU755: - - WAREHOUSE0 - SKU756: - - WAREHOUSE0 - SKU757: - - WAREHOUSE0 - SKU758: - - WAREHOUSE0 - SKU759: - - WAREHOUSE0 - SKU76: - - WAREHOUSE0 - SKU760: - - WAREHOUSE0 - SKU761: - - WAREHOUSE0 - SKU762: - - WAREHOUSE0 - SKU763: - - WAREHOUSE0 - SKU764: - - WAREHOUSE0 - SKU765: - - WAREHOUSE0 - SKU766: - - WAREHOUSE0 - SKU767: - - WAREHOUSE0 - SKU768: - - WAREHOUSE0 - SKU769: - - WAREHOUSE0 - SKU77: - - WAREHOUSE0 - SKU770: - - WAREHOUSE0 - SKU771: - - WAREHOUSE0 - SKU772: - - WAREHOUSE0 - SKU773: - - WAREHOUSE0 - SKU774: - - WAREHOUSE0 - SKU775: - - WAREHOUSE0 - SKU776: - - WAREHOUSE0 - SKU777: - - WAREHOUSE0 - SKU778: - - WAREHOUSE0 - SKU779: - - WAREHOUSE0 - SKU78: - - WAREHOUSE0 - SKU780: - - WAREHOUSE0 - SKU781: - - WAREHOUSE0 - SKU782: - - WAREHOUSE0 - SKU783: - - WAREHOUSE0 - SKU784: - - WAREHOUSE0 - SKU785: - - WAREHOUSE0 - SKU786: - - WAREHOUSE0 - SKU787: - - WAREHOUSE0 - SKU788: - - WAREHOUSE0 - SKU789: - - WAREHOUSE0 - SKU79: - - WAREHOUSE0 - SKU790: - - WAREHOUSE0 - SKU791: - - WAREHOUSE0 - SKU792: - - WAREHOUSE0 - SKU793: - - WAREHOUSE0 - SKU794: - - WAREHOUSE0 - SKU795: - - WAREHOUSE0 - SKU796: - - WAREHOUSE0 - SKU797: - - WAREHOUSE0 - SKU798: - - WAREHOUSE0 - SKU799: - - WAREHOUSE0 - SKU8: - - WAREHOUSE0 - SKU80: - - WAREHOUSE0 - SKU800: - - WAREHOUSE0 - SKU801: - - WAREHOUSE0 - SKU802: - - WAREHOUSE0 - SKU803: - - WAREHOUSE0 - SKU804: - - WAREHOUSE0 - SKU805: - - WAREHOUSE0 - SKU806: - - WAREHOUSE0 - SKU807: - - WAREHOUSE0 - SKU808: - - WAREHOUSE0 - SKU809: - - WAREHOUSE0 - SKU81: - - WAREHOUSE0 - SKU810: - - WAREHOUSE0 - SKU811: - - WAREHOUSE0 - SKU812: - - WAREHOUSE0 - SKU813: - - WAREHOUSE0 - SKU814: - - WAREHOUSE0 - SKU815: - - WAREHOUSE0 - SKU816: - - WAREHOUSE0 - SKU817: - - WAREHOUSE0 - SKU818: - - WAREHOUSE0 - SKU819: - - WAREHOUSE0 - SKU82: - - WAREHOUSE0 - SKU820: - - WAREHOUSE0 - SKU821: - - WAREHOUSE0 - SKU822: - - WAREHOUSE0 - SKU823: - - WAREHOUSE0 - SKU824: - - WAREHOUSE0 - SKU825: - - WAREHOUSE0 - SKU826: - - WAREHOUSE0 - SKU827: - - WAREHOUSE0 - SKU828: - - WAREHOUSE0 - SKU829: - - WAREHOUSE0 - SKU83: - - WAREHOUSE0 - SKU830: - - WAREHOUSE0 - SKU831: - - WAREHOUSE0 - SKU832: - - WAREHOUSE0 - SKU833: - - WAREHOUSE0 - SKU834: - - WAREHOUSE0 - SKU835: - - WAREHOUSE0 - SKU836: - - WAREHOUSE0 - SKU837: - - WAREHOUSE0 - SKU838: - - WAREHOUSE0 - SKU839: - - WAREHOUSE0 - SKU84: - - WAREHOUSE0 - SKU840: - - WAREHOUSE0 - SKU841: - - WAREHOUSE0 - SKU842: - - WAREHOUSE0 - SKU843: - - WAREHOUSE0 - SKU844: - - WAREHOUSE0 - SKU845: - - WAREHOUSE0 - SKU846: - - WAREHOUSE0 - SKU847: - - WAREHOUSE0 - SKU848: - - WAREHOUSE0 - SKU849: - - WAREHOUSE0 - SKU85: - - WAREHOUSE0 - SKU850: - - WAREHOUSE0 - SKU851: - - WAREHOUSE0 - SKU852: - - WAREHOUSE0 - SKU853: - - WAREHOUSE0 - SKU854: - - WAREHOUSE0 - SKU855: - - WAREHOUSE0 - SKU856: - - WAREHOUSE0 - SKU857: - - WAREHOUSE0 - SKU858: - - WAREHOUSE0 - SKU859: - - WAREHOUSE0 - SKU86: - - WAREHOUSE0 - SKU860: - - WAREHOUSE0 - SKU861: - - WAREHOUSE0 - SKU862: - - WAREHOUSE0 - SKU863: - - WAREHOUSE0 - SKU864: - - WAREHOUSE0 - SKU865: - - WAREHOUSE0 - SKU866: - - WAREHOUSE0 - SKU867: - - WAREHOUSE0 - SKU868: - - WAREHOUSE0 - SKU869: - - WAREHOUSE0 - SKU87: - - WAREHOUSE0 - SKU870: - - WAREHOUSE0 - SKU871: - - WAREHOUSE0 - SKU872: - - WAREHOUSE0 - SKU873: - - WAREHOUSE0 - SKU874: - - WAREHOUSE0 - SKU875: - - WAREHOUSE0 - SKU876: - - WAREHOUSE0 - SKU877: - - WAREHOUSE0 - SKU878: - - WAREHOUSE0 - SKU879: - - WAREHOUSE0 - SKU88: - - WAREHOUSE0 - SKU880: - - WAREHOUSE0 - SKU881: - - WAREHOUSE0 - SKU882: - - WAREHOUSE0 - SKU883: - - WAREHOUSE0 - SKU884: - - WAREHOUSE0 - SKU885: - - WAREHOUSE0 - SKU886: - - WAREHOUSE0 - SKU887: - - WAREHOUSE0 - SKU888: - - WAREHOUSE0 - SKU889: - - WAREHOUSE0 - SKU89: - - WAREHOUSE0 - SKU890: - - WAREHOUSE0 - SKU891: - - WAREHOUSE0 - SKU892: - - WAREHOUSE0 - SKU893: - - WAREHOUSE0 - SKU894: - - WAREHOUSE0 - SKU895: - - WAREHOUSE0 - SKU896: - - WAREHOUSE0 - SKU897: - - WAREHOUSE0 - SKU898: - - WAREHOUSE0 - SKU899: - - WAREHOUSE0 - SKU9: - - WAREHOUSE0 - SKU90: - - WAREHOUSE0 - SKU900: - - WAREHOUSE0 - SKU901: - - WAREHOUSE0 - SKU902: - - WAREHOUSE0 - SKU903: - - WAREHOUSE0 - SKU904: - - WAREHOUSE0 - SKU905: - - WAREHOUSE0 - SKU906: - - WAREHOUSE0 - SKU907: - - WAREHOUSE0 - SKU908: - - WAREHOUSE0 - SKU909: - - WAREHOUSE0 - SKU91: - - WAREHOUSE0 - SKU910: - - WAREHOUSE0 - SKU911: - - WAREHOUSE0 - SKU912: - - WAREHOUSE0 - SKU913: - - WAREHOUSE0 - SKU914: - - WAREHOUSE0 - SKU915: - - WAREHOUSE0 - SKU916: - - WAREHOUSE0 - SKU917: - - WAREHOUSE0 - SKU918: - - WAREHOUSE0 - SKU919: - - WAREHOUSE0 - SKU92: - - WAREHOUSE0 - SKU920: - - WAREHOUSE0 - SKU921: - - WAREHOUSE0 - SKU922: - - WAREHOUSE0 - SKU923: - - WAREHOUSE0 - SKU924: - - WAREHOUSE0 - SKU925: - - WAREHOUSE0 - SKU926: - - WAREHOUSE0 - SKU927: - - WAREHOUSE0 - SKU928: - - WAREHOUSE0 - SKU929: - - WAREHOUSE0 - SKU93: - - WAREHOUSE0 - SKU930: - - WAREHOUSE0 - SKU931: - - WAREHOUSE0 - SKU932: - - WAREHOUSE0 - SKU933: - - WAREHOUSE0 - SKU934: - - WAREHOUSE0 - SKU935: - - WAREHOUSE0 - SKU936: - - WAREHOUSE0 - SKU937: - - WAREHOUSE0 - SKU938: - - WAREHOUSE0 - SKU939: - - WAREHOUSE0 - SKU94: - - WAREHOUSE0 - SKU940: - - WAREHOUSE0 - SKU941: - - WAREHOUSE0 - SKU942: - - WAREHOUSE0 - SKU943: - - WAREHOUSE0 - SKU944: - - WAREHOUSE0 - SKU945: - - WAREHOUSE0 - SKU946: - - WAREHOUSE0 - SKU947: - - WAREHOUSE0 - SKU948: - - WAREHOUSE0 - SKU949: - - WAREHOUSE0 - SKU95: - - WAREHOUSE0 - SKU950: - - WAREHOUSE0 - SKU951: - - WAREHOUSE0 - SKU952: - - WAREHOUSE0 - SKU953: - - WAREHOUSE0 - SKU954: - - WAREHOUSE0 - SKU955: - - WAREHOUSE0 - SKU956: - - WAREHOUSE0 - SKU957: - - WAREHOUSE0 - SKU958: - - WAREHOUSE0 - SKU959: - - WAREHOUSE0 - SKU96: - - WAREHOUSE0 - SKU960: - - WAREHOUSE0 - SKU961: - - WAREHOUSE0 - SKU962: - - WAREHOUSE0 - SKU963: - - WAREHOUSE0 - SKU964: - - WAREHOUSE0 - SKU965: - - WAREHOUSE0 - SKU966: - - WAREHOUSE0 - SKU967: - - WAREHOUSE0 - SKU968: - - WAREHOUSE0 - SKU969: - - WAREHOUSE0 - SKU97: - - WAREHOUSE0 - SKU970: - - WAREHOUSE0 - SKU971: - - WAREHOUSE0 - SKU972: - - WAREHOUSE0 - SKU973: - - WAREHOUSE0 - SKU974: - - WAREHOUSE0 - SKU975: - - WAREHOUSE0 - SKU976: - - WAREHOUSE0 - SKU977: - - WAREHOUSE0 - SKU978: - - WAREHOUSE0 - SKU979: - - WAREHOUSE0 - SKU98: - - WAREHOUSE0 - SKU980: - - WAREHOUSE0 - SKU981: - - WAREHOUSE0 - SKU982: - - WAREHOUSE0 - SKU983: - - WAREHOUSE0 - SKU984: - - WAREHOUSE0 - SKU985: - - WAREHOUSE0 - SKU986: - - WAREHOUSE0 - SKU987: - - WAREHOUSE0 - SKU988: - - WAREHOUSE0 - SKU989: - - WAREHOUSE0 - SKU99: - - WAREHOUSE0 - SKU990: - - WAREHOUSE0 - SKU991: - - WAREHOUSE0 - SKU992: - - WAREHOUSE0 - SKU993: - - WAREHOUSE0 - SKU994: - - WAREHOUSE0 - SKU995: - - WAREHOUSE0 - SKU996: - - WAREHOUSE0 - SKU997: - - WAREHOUSE0 - SKU998: - - WAREHOUSE0 - SKU999: - - WAREHOUSE0 - WAREHOUSE0: - SKU0: - - SUPPLIER0 - SKU1: - - SUPPLIER0 - SKU10: - - SUPPLIER0 - SKU100: - - SUPPLIER0 - SKU101: - - SUPPLIER0 - SKU102: - - SUPPLIER0 - SKU103: - - SUPPLIER0 - SKU104: - - SUPPLIER0 - SKU105: - - SUPPLIER0 - SKU106: - - SUPPLIER0 - SKU107: - - SUPPLIER0 - SKU108: - - SUPPLIER0 - SKU109: - - SUPPLIER0 - SKU11: - - SUPPLIER0 - SKU110: - - SUPPLIER0 - SKU111: - - SUPPLIER0 - SKU112: - - SUPPLIER0 - SKU113: - - SUPPLIER0 - SKU114: - - SUPPLIER0 - SKU115: - - SUPPLIER0 - SKU116: - - SUPPLIER0 - SKU117: - - SUPPLIER0 - SKU118: - - SUPPLIER0 - SKU119: - - SUPPLIER0 - SKU12: - - SUPPLIER0 - SKU120: - - SUPPLIER0 - SKU121: - - SUPPLIER0 - SKU122: - - SUPPLIER0 - SKU123: - - SUPPLIER0 - SKU124: - - SUPPLIER0 - SKU125: - - SUPPLIER0 - SKU126: - - SUPPLIER0 - SKU127: - - SUPPLIER0 - SKU128: - - SUPPLIER0 - SKU129: - - SUPPLIER0 - SKU13: - - SUPPLIER0 - SKU130: - - SUPPLIER0 - SKU131: - - SUPPLIER0 - SKU132: - - SUPPLIER0 - SKU133: - - SUPPLIER0 - SKU134: - - SUPPLIER0 - SKU135: - - SUPPLIER0 - SKU136: - - SUPPLIER0 - SKU137: - - SUPPLIER0 - SKU138: - - SUPPLIER0 - SKU139: - - SUPPLIER0 - SKU14: - - SUPPLIER0 - SKU140: - - SUPPLIER0 - SKU141: - - SUPPLIER0 - SKU142: - - SUPPLIER0 - SKU143: - - SUPPLIER0 - SKU144: - - SUPPLIER0 - SKU145: - - SUPPLIER0 - SKU146: - - SUPPLIER0 - SKU147: - - SUPPLIER0 - SKU148: - - SUPPLIER0 - SKU149: - - SUPPLIER0 - SKU15: - - SUPPLIER0 - SKU150: - - SUPPLIER0 - SKU151: - - SUPPLIER0 - SKU152: - - SUPPLIER0 - SKU153: - - SUPPLIER0 - SKU154: - - SUPPLIER0 - SKU155: - - SUPPLIER0 - SKU156: - - SUPPLIER0 - SKU157: - - SUPPLIER0 - SKU158: - - SUPPLIER0 - SKU159: - - SUPPLIER0 - SKU16: - - SUPPLIER0 - SKU160: - - SUPPLIER0 - SKU161: - - SUPPLIER0 - SKU162: - - SUPPLIER0 - SKU163: - - SUPPLIER0 - SKU164: - - SUPPLIER0 - SKU165: - - SUPPLIER0 - SKU166: - - SUPPLIER0 - SKU167: - - SUPPLIER0 - SKU168: - - SUPPLIER0 - SKU169: - - SUPPLIER0 - SKU17: - - SUPPLIER0 - SKU170: - - SUPPLIER0 - SKU171: - - SUPPLIER0 - SKU172: - - SUPPLIER0 - SKU173: - - SUPPLIER0 - SKU174: - - SUPPLIER0 - SKU175: - - SUPPLIER0 - SKU176: - - SUPPLIER0 - SKU177: - - SUPPLIER0 - SKU178: - - SUPPLIER0 - SKU179: - - SUPPLIER0 - SKU18: - - SUPPLIER0 - SKU180: - - SUPPLIER0 - SKU181: - - SUPPLIER0 - SKU182: - - SUPPLIER0 - SKU183: - - SUPPLIER0 - SKU184: - - SUPPLIER0 - SKU185: - - SUPPLIER0 - SKU186: - - SUPPLIER0 - SKU187: - - SUPPLIER0 - SKU188: - - SUPPLIER0 - SKU189: - - SUPPLIER0 - SKU19: - - SUPPLIER0 - SKU190: - - SUPPLIER0 - SKU191: - - SUPPLIER0 - SKU192: - - SUPPLIER0 - SKU193: - - SUPPLIER0 - SKU194: - - SUPPLIER0 - SKU195: - - SUPPLIER0 - SKU196: - - SUPPLIER0 - SKU197: - - SUPPLIER0 - SKU198: - - SUPPLIER0 - SKU199: - - SUPPLIER0 - SKU2: - - SUPPLIER0 - SKU20: - - SUPPLIER0 - SKU200: - - SUPPLIER0 - SKU201: - - SUPPLIER0 - SKU202: - - SUPPLIER0 - SKU203: - - SUPPLIER0 - SKU204: - - SUPPLIER0 - SKU205: - - SUPPLIER0 - SKU206: - - SUPPLIER0 - SKU207: - - SUPPLIER0 - SKU208: - - SUPPLIER0 - SKU209: - - SUPPLIER0 - SKU21: - - SUPPLIER0 - SKU210: - - SUPPLIER0 - SKU211: - - SUPPLIER0 - SKU212: - - SUPPLIER0 - SKU213: - - SUPPLIER0 - SKU214: - - SUPPLIER0 - SKU215: - - SUPPLIER0 - SKU216: - - SUPPLIER0 - SKU217: - - SUPPLIER0 - SKU218: - - SUPPLIER0 - SKU219: - - SUPPLIER0 - SKU22: - - SUPPLIER0 - SKU220: - - SUPPLIER0 - SKU221: - - SUPPLIER0 - SKU222: - - SUPPLIER0 - SKU223: - - SUPPLIER0 - SKU224: - - SUPPLIER0 - SKU225: - - SUPPLIER0 - SKU226: - - SUPPLIER0 - SKU227: - - SUPPLIER0 - SKU228: - - SUPPLIER0 - SKU229: - - SUPPLIER0 - SKU23: - - SUPPLIER0 - SKU230: - - SUPPLIER0 - SKU231: - - SUPPLIER0 - SKU232: - - SUPPLIER0 - SKU233: - - SUPPLIER0 - SKU234: - - SUPPLIER0 - SKU235: - - SUPPLIER0 - SKU236: - - SUPPLIER0 - SKU237: - - SUPPLIER0 - SKU238: - - SUPPLIER0 - SKU239: - - SUPPLIER0 - SKU24: - - SUPPLIER0 - SKU240: - - SUPPLIER0 - SKU241: - - SUPPLIER0 - SKU242: - - SUPPLIER0 - SKU243: - - SUPPLIER0 - SKU244: - - SUPPLIER0 - SKU245: - - SUPPLIER0 - SKU246: - - SUPPLIER0 - SKU247: - - SUPPLIER0 - SKU248: - - SUPPLIER0 - SKU249: - - SUPPLIER0 - SKU25: - - SUPPLIER0 - SKU250: - - SUPPLIER0 - SKU251: - - SUPPLIER0 - SKU252: - - SUPPLIER0 - SKU253: - - SUPPLIER0 - SKU254: - - SUPPLIER0 - SKU255: - - SUPPLIER0 - SKU256: - - SUPPLIER0 - SKU257: - - SUPPLIER0 - SKU258: - - SUPPLIER0 - SKU259: - - SUPPLIER0 - SKU26: - - SUPPLIER0 - SKU260: - - SUPPLIER0 - SKU261: - - SUPPLIER0 - SKU262: - - SUPPLIER0 - SKU263: - - SUPPLIER0 - SKU264: - - SUPPLIER0 - SKU265: - - SUPPLIER0 - SKU266: - - SUPPLIER0 - SKU267: - - SUPPLIER0 - SKU268: - - SUPPLIER0 - SKU269: - - SUPPLIER0 - SKU27: - - SUPPLIER0 - SKU270: - - SUPPLIER0 - SKU271: - - SUPPLIER0 - SKU272: - - SUPPLIER0 - SKU273: - - SUPPLIER0 - SKU274: - - SUPPLIER0 - SKU275: - - SUPPLIER0 - SKU276: - - SUPPLIER0 - SKU277: - - SUPPLIER0 - SKU278: - - SUPPLIER0 - SKU279: - - SUPPLIER0 - SKU28: - - SUPPLIER0 - SKU280: - - SUPPLIER0 - SKU281: - - SUPPLIER0 - SKU282: - - SUPPLIER0 - SKU283: - - SUPPLIER0 - SKU284: - - SUPPLIER0 - SKU285: - - SUPPLIER0 - SKU286: - - SUPPLIER0 - SKU287: - - SUPPLIER0 - SKU288: - - SUPPLIER0 - SKU289: - - SUPPLIER0 - SKU29: - - SUPPLIER0 - SKU290: - - SUPPLIER0 - SKU291: - - SUPPLIER0 - SKU292: - - SUPPLIER0 - SKU293: - - SUPPLIER0 - SKU294: - - SUPPLIER0 - SKU295: - - SUPPLIER0 - SKU296: - - SUPPLIER0 - SKU297: - - SUPPLIER0 - SKU298: - - SUPPLIER0 - SKU299: - - SUPPLIER0 - SKU3: - - SUPPLIER0 - SKU30: - - SUPPLIER0 - SKU300: - - SUPPLIER0 - SKU301: - - SUPPLIER0 - SKU302: - - SUPPLIER0 - SKU303: - - SUPPLIER0 - SKU304: - - SUPPLIER0 - SKU305: - - SUPPLIER0 - SKU306: - - SUPPLIER0 - SKU307: - - SUPPLIER0 - SKU308: - - SUPPLIER0 - SKU309: - - SUPPLIER0 - SKU31: - - SUPPLIER0 - SKU310: - - SUPPLIER0 - SKU311: - - SUPPLIER0 - SKU312: - - SUPPLIER0 - SKU313: - - SUPPLIER0 - SKU314: - - SUPPLIER0 - SKU315: - - SUPPLIER0 - SKU316: - - SUPPLIER0 - SKU317: - - SUPPLIER0 - SKU318: - - SUPPLIER0 - SKU319: - - SUPPLIER0 - SKU32: - - SUPPLIER0 - SKU320: - - SUPPLIER0 - SKU321: - - SUPPLIER0 - SKU322: - - SUPPLIER0 - SKU323: - - SUPPLIER0 - SKU324: - - SUPPLIER0 - SKU325: - - SUPPLIER0 - SKU326: - - SUPPLIER0 - SKU327: - - SUPPLIER0 - SKU328: - - SUPPLIER0 - SKU329: - - SUPPLIER0 - SKU33: - - SUPPLIER0 - SKU330: - - SUPPLIER0 - SKU331: - - SUPPLIER0 - SKU332: - - SUPPLIER0 - SKU333: - - SUPPLIER0 - SKU334: - - SUPPLIER0 - SKU335: - - SUPPLIER0 - SKU336: - - SUPPLIER0 - SKU337: - - SUPPLIER0 - SKU338: - - SUPPLIER0 - SKU339: - - SUPPLIER0 - SKU34: - - SUPPLIER0 - SKU340: - - SUPPLIER0 - SKU341: - - SUPPLIER0 - SKU342: - - SUPPLIER0 - SKU343: - - SUPPLIER0 - SKU344: - - SUPPLIER0 - SKU345: - - SUPPLIER0 - SKU346: - - SUPPLIER0 - SKU347: - - SUPPLIER0 - SKU348: - - SUPPLIER0 - SKU349: - - SUPPLIER0 - SKU35: - - SUPPLIER0 - SKU350: - - SUPPLIER0 - SKU351: - - SUPPLIER0 - SKU352: - - SUPPLIER0 - SKU353: - - SUPPLIER0 - SKU354: - - SUPPLIER0 - SKU355: - - SUPPLIER0 - SKU356: - - SUPPLIER0 - SKU357: - - SUPPLIER0 - SKU358: - - SUPPLIER0 - SKU359: - - SUPPLIER0 - SKU36: - - SUPPLIER0 - SKU360: - - SUPPLIER0 - SKU361: - - SUPPLIER0 - SKU362: - - SUPPLIER0 - SKU363: - - SUPPLIER0 - SKU364: - - SUPPLIER0 - SKU365: - - SUPPLIER0 - SKU366: - - SUPPLIER0 - SKU367: - - SUPPLIER0 - SKU368: - - SUPPLIER0 - SKU369: - - SUPPLIER0 - SKU37: - - SUPPLIER0 - SKU370: - - SUPPLIER0 - SKU371: - - SUPPLIER0 - SKU372: - - SUPPLIER0 - SKU373: - - SUPPLIER0 - SKU374: - - SUPPLIER0 - SKU375: - - SUPPLIER0 - SKU376: - - SUPPLIER0 - SKU377: - - SUPPLIER0 - SKU378: - - SUPPLIER0 - SKU379: - - SUPPLIER0 - SKU38: - - SUPPLIER0 - SKU380: - - SUPPLIER0 - SKU381: - - SUPPLIER0 - SKU382: - - SUPPLIER0 - SKU383: - - SUPPLIER0 - SKU384: - - SUPPLIER0 - SKU385: - - SUPPLIER0 - SKU386: - - SUPPLIER0 - SKU387: - - SUPPLIER0 - SKU388: - - SUPPLIER0 - SKU389: - - SUPPLIER0 - SKU39: - - SUPPLIER0 - SKU390: - - SUPPLIER0 - SKU391: - - SUPPLIER0 - SKU392: - - SUPPLIER0 - SKU393: - - SUPPLIER0 - SKU394: - - SUPPLIER0 - SKU395: - - SUPPLIER0 - SKU396: - - SUPPLIER0 - SKU397: - - SUPPLIER0 - SKU398: - - SUPPLIER0 - SKU399: - - SUPPLIER0 - SKU4: - - SUPPLIER0 - SKU40: - - SUPPLIER0 - SKU400: - - SUPPLIER0 - SKU401: - - SUPPLIER0 - SKU402: - - SUPPLIER0 - SKU403: - - SUPPLIER0 - SKU404: - - SUPPLIER0 - SKU405: - - SUPPLIER0 - SKU406: - - SUPPLIER0 - SKU407: - - SUPPLIER0 - SKU408: - - SUPPLIER0 - SKU409: - - SUPPLIER0 - SKU41: - - SUPPLIER0 - SKU410: - - SUPPLIER0 - SKU411: - - SUPPLIER0 - SKU412: - - SUPPLIER0 - SKU413: - - SUPPLIER0 - SKU414: - - SUPPLIER0 - SKU415: - - SUPPLIER0 - SKU416: - - SUPPLIER0 - SKU417: - - SUPPLIER0 - SKU418: - - SUPPLIER0 - SKU419: - - SUPPLIER0 - SKU42: - - SUPPLIER0 - SKU420: - - SUPPLIER0 - SKU421: - - SUPPLIER0 - SKU422: - - SUPPLIER0 - SKU423: - - SUPPLIER0 - SKU424: - - SUPPLIER0 - SKU425: - - SUPPLIER0 - SKU426: - - SUPPLIER0 - SKU427: - - SUPPLIER0 - SKU428: - - SUPPLIER0 - SKU429: - - SUPPLIER0 - SKU43: - - SUPPLIER0 - SKU430: - - SUPPLIER0 - SKU431: - - SUPPLIER0 - SKU432: - - SUPPLIER0 - SKU433: - - SUPPLIER0 - SKU434: - - SUPPLIER0 - SKU435: - - SUPPLIER0 - SKU436: - - SUPPLIER0 - SKU437: - - SUPPLIER0 - SKU438: - - SUPPLIER0 - SKU439: - - SUPPLIER0 - SKU44: - - SUPPLIER0 - SKU440: - - SUPPLIER0 - SKU441: - - SUPPLIER0 - SKU442: - - SUPPLIER0 - SKU443: - - SUPPLIER0 - SKU444: - - SUPPLIER0 - SKU445: - - SUPPLIER0 - SKU446: - - SUPPLIER0 - SKU447: - - SUPPLIER0 - SKU448: - - SUPPLIER0 - SKU449: - - SUPPLIER0 - SKU45: - - SUPPLIER0 - SKU450: - - SUPPLIER0 - SKU451: - - SUPPLIER0 - SKU452: - - SUPPLIER0 - SKU453: - - SUPPLIER0 - SKU454: - - SUPPLIER0 - SKU455: - - SUPPLIER0 - SKU456: - - SUPPLIER0 - SKU457: - - SUPPLIER0 - SKU458: - - SUPPLIER0 - SKU459: - - SUPPLIER0 - SKU46: - - SUPPLIER0 - SKU460: - - SUPPLIER0 - SKU461: - - SUPPLIER0 - SKU462: - - SUPPLIER0 - SKU463: - - SUPPLIER0 - SKU464: - - SUPPLIER0 - SKU465: - - SUPPLIER0 - SKU466: - - SUPPLIER0 - SKU467: - - SUPPLIER0 - SKU468: - - SUPPLIER0 - SKU469: - - SUPPLIER0 - SKU47: - - SUPPLIER0 - SKU470: - - SUPPLIER0 - SKU471: - - SUPPLIER0 - SKU472: - - SUPPLIER0 - SKU473: - - SUPPLIER0 - SKU474: - - SUPPLIER0 - SKU475: - - SUPPLIER0 - SKU476: - - SUPPLIER0 - SKU477: - - SUPPLIER0 - SKU478: - - SUPPLIER0 - SKU479: - - SUPPLIER0 - SKU48: - - SUPPLIER0 - SKU480: - - SUPPLIER0 - SKU481: - - SUPPLIER0 - SKU482: - - SUPPLIER0 - SKU483: - - SUPPLIER0 - SKU484: - - SUPPLIER0 - SKU485: - - SUPPLIER0 - SKU486: - - SUPPLIER0 - SKU487: - - SUPPLIER0 - SKU488: - - SUPPLIER0 - SKU489: - - SUPPLIER0 - SKU49: - - SUPPLIER0 - SKU490: - - SUPPLIER0 - SKU491: - - SUPPLIER0 - SKU492: - - SUPPLIER0 - SKU493: - - SUPPLIER0 - SKU494: - - SUPPLIER0 - SKU495: - - SUPPLIER0 - SKU496: - - SUPPLIER0 - SKU497: - - SUPPLIER0 - SKU498: - - SUPPLIER0 - SKU499: - - SUPPLIER0 - SKU5: - - SUPPLIER0 - SKU50: - - SUPPLIER0 - SKU500: - - SUPPLIER0 - SKU501: - - SUPPLIER0 - SKU502: - - SUPPLIER0 - SKU503: - - SUPPLIER0 - SKU504: - - SUPPLIER0 - SKU505: - - SUPPLIER0 - SKU506: - - SUPPLIER0 - SKU507: - - SUPPLIER0 - SKU508: - - SUPPLIER0 - SKU509: - - SUPPLIER0 - SKU51: - - SUPPLIER0 - SKU510: - - SUPPLIER0 - SKU511: - - SUPPLIER0 - SKU512: - - SUPPLIER0 - SKU513: - - SUPPLIER0 - SKU514: - - SUPPLIER0 - SKU515: - - SUPPLIER0 - SKU516: - - SUPPLIER0 - SKU517: - - SUPPLIER0 - SKU518: - - SUPPLIER0 - SKU519: - - SUPPLIER0 - SKU52: - - SUPPLIER0 - SKU520: - - SUPPLIER0 - SKU521: - - SUPPLIER0 - SKU522: - - SUPPLIER0 - SKU523: - - SUPPLIER0 - SKU524: - - SUPPLIER0 - SKU525: - - SUPPLIER0 - SKU526: - - SUPPLIER0 - SKU527: - - SUPPLIER0 - SKU528: - - SUPPLIER0 - SKU529: - - SUPPLIER0 - SKU53: - - SUPPLIER0 - SKU530: - - SUPPLIER0 - SKU531: - - SUPPLIER0 - SKU532: - - SUPPLIER0 - SKU533: - - SUPPLIER0 - SKU534: - - SUPPLIER0 - SKU535: - - SUPPLIER0 - SKU536: - - SUPPLIER0 - SKU537: - - SUPPLIER0 - SKU538: - - SUPPLIER0 - SKU539: - - SUPPLIER0 - SKU54: - - SUPPLIER0 - SKU540: - - SUPPLIER0 - SKU541: - - SUPPLIER0 - SKU542: - - SUPPLIER0 - SKU543: - - SUPPLIER0 - SKU544: - - SUPPLIER0 - SKU545: - - SUPPLIER0 - SKU546: - - SUPPLIER0 - SKU547: - - SUPPLIER0 - SKU548: - - SUPPLIER0 - SKU549: - - SUPPLIER0 - SKU55: - - SUPPLIER0 - SKU550: - - SUPPLIER0 - SKU551: - - SUPPLIER0 - SKU552: - - SUPPLIER0 - SKU553: - - SUPPLIER0 - SKU554: - - SUPPLIER0 - SKU555: - - SUPPLIER0 - SKU556: - - SUPPLIER0 - SKU557: - - SUPPLIER0 - SKU558: - - SUPPLIER0 - SKU559: - - SUPPLIER0 - SKU56: - - SUPPLIER0 - SKU560: - - SUPPLIER0 - SKU561: - - SUPPLIER0 - SKU562: - - SUPPLIER0 - SKU563: - - SUPPLIER0 - SKU564: - - SUPPLIER0 - SKU565: - - SUPPLIER0 - SKU566: - - SUPPLIER0 - SKU567: - - SUPPLIER0 - SKU568: - - SUPPLIER0 - SKU569: - - SUPPLIER0 - SKU57: - - SUPPLIER0 - SKU570: - - SUPPLIER0 - SKU571: - - SUPPLIER0 - SKU572: - - SUPPLIER0 - SKU573: - - SUPPLIER0 - SKU574: - - SUPPLIER0 - SKU575: - - SUPPLIER0 - SKU576: - - SUPPLIER0 - SKU577: - - SUPPLIER0 - SKU578: - - SUPPLIER0 - SKU579: - - SUPPLIER0 - SKU58: - - SUPPLIER0 - SKU580: - - SUPPLIER0 - SKU581: - - SUPPLIER0 - SKU582: - - SUPPLIER0 - SKU583: - - SUPPLIER0 - SKU584: - - SUPPLIER0 - SKU585: - - SUPPLIER0 - SKU586: - - SUPPLIER0 - SKU587: - - SUPPLIER0 - SKU588: - - SUPPLIER0 - SKU589: - - SUPPLIER0 - SKU59: - - SUPPLIER0 - SKU590: - - SUPPLIER0 - SKU591: - - SUPPLIER0 - SKU592: - - SUPPLIER0 - SKU593: - - SUPPLIER0 - SKU594: - - SUPPLIER0 - SKU595: - - SUPPLIER0 - SKU596: - - SUPPLIER0 - SKU597: - - SUPPLIER0 - SKU598: - - SUPPLIER0 - SKU599: - - SUPPLIER0 - SKU6: - - SUPPLIER0 - SKU60: - - SUPPLIER0 - SKU600: - - SUPPLIER0 - SKU601: - - SUPPLIER0 - SKU602: - - SUPPLIER0 - SKU603: - - SUPPLIER0 - SKU604: - - SUPPLIER0 - SKU605: - - SUPPLIER0 - SKU606: - - SUPPLIER0 - SKU607: - - SUPPLIER0 - SKU608: - - SUPPLIER0 - SKU609: - - SUPPLIER0 - SKU61: - - SUPPLIER0 - SKU610: - - SUPPLIER0 - SKU611: - - SUPPLIER0 - SKU612: - - SUPPLIER0 - SKU613: - - SUPPLIER0 - SKU614: - - SUPPLIER0 - SKU615: - - SUPPLIER0 - SKU616: - - SUPPLIER0 - SKU617: - - SUPPLIER0 - SKU618: - - SUPPLIER0 - SKU619: - - SUPPLIER0 - SKU62: - - SUPPLIER0 - SKU620: - - SUPPLIER0 - SKU621: - - SUPPLIER0 - SKU622: - - SUPPLIER0 - SKU623: - - SUPPLIER0 - SKU624: - - SUPPLIER0 - SKU625: - - SUPPLIER0 - SKU626: - - SUPPLIER0 - SKU627: - - SUPPLIER0 - SKU628: - - SUPPLIER0 - SKU629: - - SUPPLIER0 - SKU63: - - SUPPLIER0 - SKU630: - - SUPPLIER0 - SKU631: - - SUPPLIER0 - SKU632: - - SUPPLIER0 - SKU633: - - SUPPLIER0 - SKU634: - - SUPPLIER0 - SKU635: - - SUPPLIER0 - SKU636: - - SUPPLIER0 - SKU637: - - SUPPLIER0 - SKU638: - - SUPPLIER0 - SKU639: - - SUPPLIER0 - SKU64: - - SUPPLIER0 - SKU640: - - SUPPLIER0 - SKU641: - - SUPPLIER0 - SKU642: - - SUPPLIER0 - SKU643: - - SUPPLIER0 - SKU644: - - SUPPLIER0 - SKU645: - - SUPPLIER0 - SKU646: - - SUPPLIER0 - SKU647: - - SUPPLIER0 - SKU648: - - SUPPLIER0 - SKU649: - - SUPPLIER0 - SKU65: - - SUPPLIER0 - SKU650: - - SUPPLIER0 - SKU651: - - SUPPLIER0 - SKU652: - - SUPPLIER0 - SKU653: - - SUPPLIER0 - SKU654: - - SUPPLIER0 - SKU655: - - SUPPLIER0 - SKU656: - - SUPPLIER0 - SKU657: - - SUPPLIER0 - SKU658: - - SUPPLIER0 - SKU659: - - SUPPLIER0 - SKU66: - - SUPPLIER0 - SKU660: - - SUPPLIER0 - SKU661: - - SUPPLIER0 - SKU662: - - SUPPLIER0 - SKU663: - - SUPPLIER0 - SKU664: - - SUPPLIER0 - SKU665: - - SUPPLIER0 - SKU666: - - SUPPLIER0 - SKU667: - - SUPPLIER0 - SKU668: - - SUPPLIER0 - SKU669: - - SUPPLIER0 - SKU67: - - SUPPLIER0 - SKU670: - - SUPPLIER0 - SKU671: - - SUPPLIER0 - SKU672: - - SUPPLIER0 - SKU673: - - SUPPLIER0 - SKU674: - - SUPPLIER0 - SKU675: - - SUPPLIER0 - SKU676: - - SUPPLIER0 - SKU677: - - SUPPLIER0 - SKU678: - - SUPPLIER0 - SKU679: - - SUPPLIER0 - SKU68: - - SUPPLIER0 - SKU680: - - SUPPLIER0 - SKU681: - - SUPPLIER0 - SKU682: - - SUPPLIER0 - SKU683: - - SUPPLIER0 - SKU684: - - SUPPLIER0 - SKU685: - - SUPPLIER0 - SKU686: - - SUPPLIER0 - SKU687: - - SUPPLIER0 - SKU688: - - SUPPLIER0 - SKU689: - - SUPPLIER0 - SKU69: - - SUPPLIER0 - SKU690: - - SUPPLIER0 - SKU691: - - SUPPLIER0 - SKU692: - - SUPPLIER0 - SKU693: - - SUPPLIER0 - SKU694: - - SUPPLIER0 - SKU695: - - SUPPLIER0 - SKU696: - - SUPPLIER0 - SKU697: - - SUPPLIER0 - SKU698: - - SUPPLIER0 - SKU699: - - SUPPLIER0 - SKU7: - - SUPPLIER0 - SKU70: - - SUPPLIER0 - SKU700: - - SUPPLIER0 - SKU701: - - SUPPLIER0 - SKU702: - - SUPPLIER0 - SKU703: - - SUPPLIER0 - SKU704: - - SUPPLIER0 - SKU705: - - SUPPLIER0 - SKU706: - - SUPPLIER0 - SKU707: - - SUPPLIER0 - SKU708: - - SUPPLIER0 - SKU709: - - SUPPLIER0 - SKU71: - - SUPPLIER0 - SKU710: - - SUPPLIER0 - SKU711: - - SUPPLIER0 - SKU712: - - SUPPLIER0 - SKU713: - - SUPPLIER0 - SKU714: - - SUPPLIER0 - SKU715: - - SUPPLIER0 - SKU716: - - SUPPLIER0 - SKU717: - - SUPPLIER0 - SKU718: - - SUPPLIER0 - SKU719: - - SUPPLIER0 - SKU72: - - SUPPLIER0 - SKU720: - - SUPPLIER0 - SKU721: - - SUPPLIER0 - SKU722: - - SUPPLIER0 - SKU723: - - SUPPLIER0 - SKU724: - - SUPPLIER0 - SKU725: - - SUPPLIER0 - SKU726: - - SUPPLIER0 - SKU727: - - SUPPLIER0 - SKU728: - - SUPPLIER0 - SKU729: - - SUPPLIER0 - SKU73: - - SUPPLIER0 - SKU730: - - SUPPLIER0 - SKU731: - - SUPPLIER0 - SKU732: - - SUPPLIER0 - SKU733: - - SUPPLIER0 - SKU734: - - SUPPLIER0 - SKU735: - - SUPPLIER0 - SKU736: - - SUPPLIER0 - SKU737: - - SUPPLIER0 - SKU738: - - SUPPLIER0 - SKU739: - - SUPPLIER0 - SKU74: - - SUPPLIER0 - SKU740: - - SUPPLIER0 - SKU741: - - SUPPLIER0 - SKU742: - - SUPPLIER0 - SKU743: - - SUPPLIER0 - SKU744: - - SUPPLIER0 - SKU745: - - SUPPLIER0 - SKU746: - - SUPPLIER0 - SKU747: - - SUPPLIER0 - SKU748: - - SUPPLIER0 - SKU749: - - SUPPLIER0 - SKU75: - - SUPPLIER0 - SKU750: - - SUPPLIER0 - SKU751: - - SUPPLIER0 - SKU752: - - SUPPLIER0 - SKU753: - - SUPPLIER0 - SKU754: - - SUPPLIER0 - SKU755: - - SUPPLIER0 - SKU756: - - SUPPLIER0 - SKU757: - - SUPPLIER0 - SKU758: - - SUPPLIER0 - SKU759: - - SUPPLIER0 - SKU76: - - SUPPLIER0 - SKU760: - - SUPPLIER0 - SKU761: - - SUPPLIER0 - SKU762: - - SUPPLIER0 - SKU763: - - SUPPLIER0 - SKU764: - - SUPPLIER0 - SKU765: - - SUPPLIER0 - SKU766: - - SUPPLIER0 - SKU767: - - SUPPLIER0 - SKU768: - - SUPPLIER0 - SKU769: - - SUPPLIER0 - SKU77: - - SUPPLIER0 - SKU770: - - SUPPLIER0 - SKU771: - - SUPPLIER0 - SKU772: - - SUPPLIER0 - SKU773: - - SUPPLIER0 - SKU774: - - SUPPLIER0 - SKU775: - - SUPPLIER0 - SKU776: - - SUPPLIER0 - SKU777: - - SUPPLIER0 - SKU778: - - SUPPLIER0 - SKU779: - - SUPPLIER0 - SKU78: - - SUPPLIER0 - SKU780: - - SUPPLIER0 - SKU781: - - SUPPLIER0 - SKU782: - - SUPPLIER0 - SKU783: - - SUPPLIER0 - SKU784: - - SUPPLIER0 - SKU785: - - SUPPLIER0 - SKU786: - - SUPPLIER0 - SKU787: - - SUPPLIER0 - SKU788: - - SUPPLIER0 - SKU789: - - SUPPLIER0 - SKU79: - - SUPPLIER0 - SKU790: - - SUPPLIER0 - SKU791: - - SUPPLIER0 - SKU792: - - SUPPLIER0 - SKU793: - - SUPPLIER0 - SKU794: - - SUPPLIER0 - SKU795: - - SUPPLIER0 - SKU796: - - SUPPLIER0 - SKU797: - - SUPPLIER0 - SKU798: - - SUPPLIER0 - SKU799: - - SUPPLIER0 - SKU8: - - SUPPLIER0 - SKU80: - - SUPPLIER0 - SKU800: - - SUPPLIER0 - SKU801: - - SUPPLIER0 - SKU802: - - SUPPLIER0 - SKU803: - - SUPPLIER0 - SKU804: - - SUPPLIER0 - SKU805: - - SUPPLIER0 - SKU806: - - SUPPLIER0 - SKU807: - - SUPPLIER0 - SKU808: - - SUPPLIER0 - SKU809: - - SUPPLIER0 - SKU81: - - SUPPLIER0 - SKU810: - - SUPPLIER0 - SKU811: - - SUPPLIER0 - SKU812: - - SUPPLIER0 - SKU813: - - SUPPLIER0 - SKU814: - - SUPPLIER0 - SKU815: - - SUPPLIER0 - SKU816: - - SUPPLIER0 - SKU817: - - SUPPLIER0 - SKU818: - - SUPPLIER0 - SKU819: - - SUPPLIER0 - SKU82: - - SUPPLIER0 - SKU820: - - SUPPLIER0 - SKU821: - - SUPPLIER0 - SKU822: - - SUPPLIER0 - SKU823: - - SUPPLIER0 - SKU824: - - SUPPLIER0 - SKU825: - - SUPPLIER0 - SKU826: - - SUPPLIER0 - SKU827: - - SUPPLIER0 - SKU828: - - SUPPLIER0 - SKU829: - - SUPPLIER0 - SKU83: - - SUPPLIER0 - SKU830: - - SUPPLIER0 - SKU831: - - SUPPLIER0 - SKU832: - - SUPPLIER0 - SKU833: - - SUPPLIER0 - SKU834: - - SUPPLIER0 - SKU835: - - SUPPLIER0 - SKU836: - - SUPPLIER0 - SKU837: - - SUPPLIER0 - SKU838: - - SUPPLIER0 - SKU839: - - SUPPLIER0 - SKU84: - - SUPPLIER0 - SKU840: - - SUPPLIER0 - SKU841: - - SUPPLIER0 - SKU842: - - SUPPLIER0 - SKU843: - - SUPPLIER0 - SKU844: - - SUPPLIER0 - SKU845: - - SUPPLIER0 - SKU846: - - SUPPLIER0 - SKU847: - - SUPPLIER0 - SKU848: - - SUPPLIER0 - SKU849: - - SUPPLIER0 - SKU85: - - SUPPLIER0 - SKU850: - - SUPPLIER0 - SKU851: - - SUPPLIER0 - SKU852: - - SUPPLIER0 - SKU853: - - SUPPLIER0 - SKU854: - - SUPPLIER0 - SKU855: - - SUPPLIER0 - SKU856: - - SUPPLIER0 - SKU857: - - SUPPLIER0 - SKU858: - - SUPPLIER0 - SKU859: - - SUPPLIER0 - SKU86: - - SUPPLIER0 - SKU860: - - SUPPLIER0 - SKU861: - - SUPPLIER0 - SKU862: - - SUPPLIER0 - SKU863: - - SUPPLIER0 - SKU864: - - SUPPLIER0 - SKU865: - - SUPPLIER0 - SKU866: - - SUPPLIER0 - SKU867: - - SUPPLIER0 - SKU868: - - SUPPLIER0 - SKU869: - - SUPPLIER0 - SKU87: - - SUPPLIER0 - SKU870: - - SUPPLIER0 - SKU871: - - SUPPLIER0 - SKU872: - - SUPPLIER0 - SKU873: - - SUPPLIER0 - SKU874: - - SUPPLIER0 - SKU875: - - SUPPLIER0 - SKU876: - - SUPPLIER0 - SKU877: - - SUPPLIER0 - SKU878: - - SUPPLIER0 - SKU879: - - SUPPLIER0 - SKU88: - - SUPPLIER0 - SKU880: - - SUPPLIER0 - SKU881: - - SUPPLIER0 - SKU882: - - SUPPLIER0 - SKU883: - - SUPPLIER0 - SKU884: - - SUPPLIER0 - SKU885: - - SUPPLIER0 - SKU886: - - SUPPLIER0 - SKU887: - - SUPPLIER0 - SKU888: - - SUPPLIER0 - SKU889: - - SUPPLIER0 - SKU89: - - SUPPLIER0 - SKU890: - - SUPPLIER0 - SKU891: - - SUPPLIER0 - SKU892: - - SUPPLIER0 - SKU893: - - SUPPLIER0 - SKU894: - - SUPPLIER0 - SKU895: - - SUPPLIER0 - SKU896: - - SUPPLIER0 - SKU897: - - SUPPLIER0 - SKU898: - - SUPPLIER0 - SKU899: - - SUPPLIER0 - SKU9: - - SUPPLIER0 - SKU90: - - SUPPLIER0 - SKU900: - - SUPPLIER0 - SKU901: - - SUPPLIER0 - SKU902: - - SUPPLIER0 - SKU903: - - SUPPLIER0 - SKU904: - - SUPPLIER0 - SKU905: - - SUPPLIER0 - SKU906: - - SUPPLIER0 - SKU907: - - SUPPLIER0 - SKU908: - - SUPPLIER0 - SKU909: - - SUPPLIER0 - SKU91: - - SUPPLIER0 - SKU910: - - SUPPLIER0 - SKU911: - - SUPPLIER0 - SKU912: - - SUPPLIER0 - SKU913: - - SUPPLIER0 - SKU914: - - SUPPLIER0 - SKU915: - - SUPPLIER0 - SKU916: - - SUPPLIER0 - SKU917: - - SUPPLIER0 - SKU918: - - SUPPLIER0 - SKU919: - - SUPPLIER0 - SKU92: - - SUPPLIER0 - SKU920: - - SUPPLIER0 - SKU921: - - SUPPLIER0 - SKU922: - - SUPPLIER0 - SKU923: - - SUPPLIER0 - SKU924: - - SUPPLIER0 - SKU925: - - SUPPLIER0 - SKU926: - - SUPPLIER0 - SKU927: - - SUPPLIER0 - SKU928: - - SUPPLIER0 - SKU929: - - SUPPLIER0 - SKU93: - - SUPPLIER0 - SKU930: - - SUPPLIER0 - SKU931: - - SUPPLIER0 - SKU932: - - SUPPLIER0 - SKU933: - - SUPPLIER0 - SKU934: - - SUPPLIER0 - SKU935: - - SUPPLIER0 - SKU936: - - SUPPLIER0 - SKU937: - - SUPPLIER0 - SKU938: - - SUPPLIER0 - SKU939: - - SUPPLIER0 - SKU94: - - SUPPLIER0 - SKU940: - - SUPPLIER0 - SKU941: - - SUPPLIER0 - SKU942: - - SUPPLIER0 - SKU943: - - SUPPLIER0 - SKU944: - - SUPPLIER0 - SKU945: - - SUPPLIER0 - SKU946: - - SUPPLIER0 - SKU947: - - SUPPLIER0 - SKU948: - - SUPPLIER0 - SKU949: - - SUPPLIER0 - SKU95: - - SUPPLIER0 - SKU950: - - SUPPLIER0 - SKU951: - - SUPPLIER0 - SKU952: - - SUPPLIER0 - SKU953: - - SUPPLIER0 - SKU954: - - SUPPLIER0 - SKU955: - - SUPPLIER0 - SKU956: - - SUPPLIER0 - SKU957: - - SUPPLIER0 - SKU958: - - SUPPLIER0 - SKU959: - - SUPPLIER0 - SKU96: - - SUPPLIER0 - SKU960: - - SUPPLIER0 - SKU961: - - SUPPLIER0 - SKU962: - - SUPPLIER0 - SKU963: - - SUPPLIER0 - SKU964: - - SUPPLIER0 - SKU965: - - SUPPLIER0 - SKU966: - - SUPPLIER0 - SKU967: - - SUPPLIER0 - SKU968: - - SUPPLIER0 - SKU969: - - SUPPLIER0 - SKU97: - - SUPPLIER0 - SKU970: - - SUPPLIER0 - SKU971: - - SUPPLIER0 - SKU972: - - SUPPLIER0 - SKU973: - - SUPPLIER0 - SKU974: - - SUPPLIER0 - SKU975: - - SUPPLIER0 - SKU976: - - SUPPLIER0 - SKU977: - - SUPPLIER0 - SKU978: - - SUPPLIER0 - SKU979: - - SUPPLIER0 - SKU98: - - SUPPLIER0 - SKU980: - - SUPPLIER0 - SKU981: - - SUPPLIER0 - SKU982: - - SUPPLIER0 - SKU983: - - SUPPLIER0 - SKU984: - - SUPPLIER0 - SKU985: - - SUPPLIER0 - SKU986: - - SUPPLIER0 - SKU987: - - SUPPLIER0 - SKU988: - - SUPPLIER0 - SKU989: - - SUPPLIER0 - SKU99: - - SUPPLIER0 - SKU990: - - SUPPLIER0 - SKU991: - - SUPPLIER0 - SKU992: - - SUPPLIER0 - SKU993: - - SUPPLIER0 - SKU994: - - SUPPLIER0 - SKU995: - - SUPPLIER0 - SKU996: - - SUPPLIER0 - SKU997: - - SUPPLIER0 - SKU998: - - SUPPLIER0 - SKU999: - - SUPPLIER0 diff --git a/examples/rl/supply_chain/topologies/sample1/config.yml b/examples/rl/supply_chain/topologies/sample1/config.yml deleted file mode 100644 index ffcd92d45..000000000 --- a/examples/rl/supply_chain/topologies/sample1/config.yml +++ /dev/null @@ -1,335 +0,0 @@ - -# TODO: which config to inherit -# base: "" - -#core: -# datamodels: "xxx" -# units: "xxx" -# facilities: "xxx" - - -facility_definitions: - # facility definition - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - # if true then will call generate function of class type - is_template: true - # config will be passed to generator as parameters - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - config: - agent_type: "facility" - - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - manufacture: - class: "ManufactureUnit" - config: - agent_type: "producer" - config: - agent_type: "facility" - - RetailerFacility: &retailer_facility - class: "RetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - seller: - class: "SellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: "facility" - - - # definition for outer retailer that read demand from csv files. - # NOTE: make sure you provide a valid file path for each retailer instance. - OuterRetailerFacility: &outerretailer_facility - class: "OuterRetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "OuterSellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: 2 - seller_sampler_type: data # data, model. What kind of sampler to use for seller. - sku_column: "SKU" # SKU column name - price_column: "Price" # Price column name - sale_column: "Sales" # Sales column name - datetime_column: "DT" # Datetime column name - file_path: "/path/to/data.csv" # full path to data file, override by each store instance - - -# common entity/unit definition as reference to simplify the file. -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 100 - -# a normal distribution definition -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -small_storage: &small_storage - # config of data model of this unit - config: - # other config or storage unit - capacity: 10000 - unit_storage_cost: 1 - -midium_storage: &midium_storage - config: - capacity: 20000 - unit_storage_cost: 1 - -huge_storage: &huge_storage - config: - capacity: 30000 - unit_storage_cost: 1 - -# sku list in this world -# this list do not contains price, cost or other facility related attributes, -# but just base info, like name, id, bom -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 12 - # bill of material that used produce current sku, empty means do not need source material - bom: - # key is the source sku name, value is quantity needed to use per time to produce current sku - sku3: 10 - - - id: 2 - name: "sku2" - output_units_per_lot: 1 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - -# world definitions -world: - # here we use reference to make it each to edit. - skus: *sku_definitions - - # facilities in this world - facilities: - - name: "Supplier_001" # name of the facility - # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level - # use the facility definition as base, then we can override configs partially. - definition_ref: "SupplierFacility" - - # sku list of this facility - skus: - sku3: # sku name and attributes needed for this facility - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" # production means this is the output production of this facility - cost: 10 - price: 10 - vlt: 1 - - # configuration of child units. - children: - # config of storage unit - storage: *small_storage - distribution: *normal_distribution - - # products use default config in core.yml - - # config of this facility - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Supplier_002" - definition_ref: "SupplierFacility" - - skus: - sku1: - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" - cost: 10 - price: 100 - vlt: 1 - sku3: - init_stock: 100 - production_rate: 1 - type: "material" - cost: 10 - price: 100 - vlt: 1 - - children: - storage: *small_storage - distribution: *normal_distribution - - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - - skus: - sku1: - init_stock: 1000 - price: 100 - vlt: 1 - sku2: - init_stock: 1000 - price: 100 - vlt: 1 - sku3: - init_stock: 1000 - price: 100 - vlt: 1 - - children: - storage: *huge_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Retailer_001" - definition_ref: "RetailerFacility" - - skus: - sku1: - price: 300 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 # optional - vlt: 1 - sku3: - price: 200 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - sku2: - price: 100 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - - children: - storage: *midium_storage - - config: - order_cost: 0 - # topology used to specify the up/downstream for facilities - # we split it from facility, so that we can support configuration inherit to override it - # for a new topology - # TODO: change the name? - topology: - # key is current facility, value if upstream facilities that will provide a certain sku - Supplier_002: - # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", - # or any other facility in the list - sku3: - - "Supplier_001" - Warehouse_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - Retailer_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - - # map grid definitions - grid: - size: [20, 20] - - # facility position in grid - facilities: - Supplier_001: [0, 0] - Supplier_002: [3, 3] - Warehouse_001: [6, 6] - Retailer_001: [10, 18] - - # cells that un-traversable - blocks: - railroad: - - [10, 10] - - [10, 11] - - [10, 12] - - [11, 12] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 - start_date_time: "2010-12-01" # start time for data in files, for outer retailer diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index f47c2fc68..06d355ca3 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -22,7 +22,7 @@ async: policy_manager: train_group: policy-manager train_mode: multi-node # single-process, multi-process, multi-node - num_trainers: 4 + num_trainers: 3 redis: host: maro-redis port: 6379 diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index b6565b88d..6ce7fc59f 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -18,7 +18,6 @@ if __name__ == "__main__": worker_id = int(environ["WORKERID"]) - print(f"replay agents: {replay_agents[worker_id]}") rollout_worker_node( config["sync"]["rollout_group"], worker_id, From 87066c9a5ff3555507fdeb46f6938caebce3e9e3 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 14 Jul 2021 15:28:56 +0000 Subject: [PATCH 378/482] added post-step, post-collect, post-eval and post-update callbacks --- docs/source/apidoc/maro.rl.rst | 4 +- docs/source/key_components/rl_toolkit.rst | 6 +- examples/rl/cim/__init__.py | 6 +- examples/rl/cim/ac.py | 56 +++--- examples/rl/cim/dqn.py | 43 +++-- examples/rl/cim/env_wrapper.py | 38 ++-- examples/rl/cim/policy_index.py | 3 +- examples/rl/workflows/agent_wrapper.py | 20 +- examples/rl/workflows/config.yml | 1 - examples/rl/workflows/general.py | 3 + examples/rl/workflows/simple_learner.py | 17 +- examples/rl/workflows/synchronous/learner.py | 10 +- .../workflows/synchronous/rollout_worker.py | 2 +- maro/rl/experience/__init__.py | 6 +- ...erience_manager.py => experience_store.py} | 45 +---- maro/rl/experience/sampler.py | 59 ++++-- maro/rl/exploration/noise_exploration.py | 4 - maro/rl/learning/__init__.py | 4 +- maro/rl/learning/agent_wrapper.py | 22 ++- maro/rl/learning/env_wrapper.py | 152 ++++++++++----- maro/rl/learning/simple_learner.py | 47 +++-- maro/rl/learning/synchronous/learner.py | 13 +- .../learning/synchronous/rollout_manager.py | 180 ++++++++++++------ .../rl/learning/synchronous/rollout_worker.py | 21 +- maro/rl/policy/algorithms/ac.py | 101 ++++++---- maro/rl/policy/algorithms/ddpg.py | 59 +++--- maro/rl/policy/algorithms/dqn.py | 55 ++++-- maro/rl/policy/algorithms/pg.py | 44 ++++- maro/rl/policy/policy.py | 24 ++- maro/rl/policy/policy_manager.py | 92 +++++++-- maro/rl/policy/trainer.py | 8 +- maro/rl/utils/message_enums.py | 4 +- .../rl_formulation.ipynb | 8 +- 33 files changed, 711 insertions(+), 446 deletions(-) rename maro/rl/experience/{experience_manager.py => experience_store.py} (68%) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index 9ade723bc..6aa78ce1f 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -37,10 +37,10 @@ maro.rl.algorithms.pg Experience ================================================================================ -maro.rl.experience.experience_manager +maro.rl.experience.experience_store -------------------------------------------------------------------------------- -.. automodule:: maro.rl.experience.experience_manager +.. automodule:: maro.rl.experience.experience_store :members: :undoc-members: :show-inheritance: diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index e35451415..ae677db81 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -101,9 +101,9 @@ based on which updates can be made. class AbsCorePolicy(AbsPolicy): - def __init__(self, experience_manager: ExperienceManager): + def __init__(self, experience_store: ExperienceStore): super().__init__() - self.experience_manager = experience_manager + self.experience_store = experience_store @abstractmethod def update(self): @@ -168,7 +168,7 @@ Experience An ``ExperienceSet`` is a synonym for training data for RL policies. The data originate from the simulator and get processed and organized into a set of transitions in the form of (state, action, reward, next_state, info), where ''info'' contains information about the transition that is not encoded in the state but may be necessary -for sampling purposes. An ``ExperienceManager`` is a storage facility for experience sets and is maintained by +for sampling purposes. An ``ExperienceStore`` is a storage facility for experience sets and is maintained by a policy for storing and retrieving training data. Sampling from the experience memory can be customized by registering a user-defined sampler to it. diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index ace955ed0..7af3aff3d 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -1,7 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .callbacks import post_collect, post_evaluate from .env_wrapper import get_env_wrapper from .policy_index import agent2policy, rl_policy_func_index, update_trigger, warmup -__all__ = ["agent2policy", "get_env_wrapper", "rl_policy_func_index", "update_trigger", "warmup"] +__all__ = [ + "agent2policy", "post_collect", "post_evaluate", "get_env_wrapper", "rl_policy_func_index", + "update_trigger", "warmup" +] diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index c19a3171d..949a6dffc 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -7,7 +7,7 @@ import numpy as np import torch -from maro.rl.experience import ExperienceManager +from maro.rl.experience import ExperienceStore, UniformSampler from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption from maro.rl.policy.algorithms import ActorCritic, ActorCriticConfig @@ -30,7 +30,7 @@ "critic": { "input_dim": STATE_DIM, "hidden_dims": [256, 128, 64], - "output_dim": env_config["wrapper"]["num_actions"], + "output_dim": 1, "activation": "leaky_relu", "softmax": False, "batch_norm": True, @@ -55,26 +55,19 @@ "actor_loss_coefficient": 0.1, # "clip_ratio": 0.8 # for PPO }, - "experience_manager": { - "rollout": { # for experience managers in actor processes - "capacity": 1000, - "overwrite_type": "rolling", - "batch_size": -1, - "replace": False - }, - "training": { # for experience managers in the learner process - "capacity": 100000, - "overwrite_type": "rolling", - "batch_size": 128, - "alpha": 0.6, - "beta": 0.4, - "beta_step": 0.001 - } - } + "experience_store": { + "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, + "update": {"capacity": 100000, "overwrite_type": "rolling"} + }, + "sampler": { + "rollout": {"batch_size": -1, "replace": False}, + "update": {"batch_size": 128, "replace": True} + } } -def get_ac_policy(learning: bool = True): +def get_ac_policy(mode="update"): + assert mode in {"inference", "update", "inference-update"} class MyACNET(DiscreteACNet): def forward(self, states, actor: bool = True, critic: bool = True): states = torch.from_numpy(np.asarray(states)) @@ -87,21 +80,26 @@ def forward(self, states, actor: bool = True, critic: bool = True): self.component["critic"](states) if critic else None ) - cfg = config["policy"] ac_net = MyACNET( component={ - "actor": FullyConnectedBlock(**cfg["model"]["network"]["actor"]), - "critic": FullyConnectedBlock(**cfg["model"]["network"]["critic"]) + "actor": FullyConnectedBlock(**config["model"]["network"]["actor"]), + "critic": FullyConnectedBlock(**config["model"]["network"]["critic"]) }, optim_option={ - "actor": OptimOption(**cfg["model"]["optimization"]["actor"]), - "critic": OptimOption(**cfg["model"]["optimization"]["critic"]) - } if learning else None + "actor": OptimOption(**config["model"]["optimization"]["actor"]), + "critic": OptimOption(**config["model"]["optimization"]["critic"]) + } if mode != "inference" else None ) - if learning: - exp_manager = ExperienceManager(**config["experience_manager"]["learning"]) + if mode == "update": + exp_manager = ExperienceStore(**config["experience_store"]["update"]) + experience_sampler_kwargs = config["sampler"]["update"] else: - exp_manager = ExperienceManager(**config["experience_manager"]["rollout"]) + exp_manager = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) + experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - return ActorCritic(ac_net, exp_manager, ActorCriticConfig(**cfg["algorithm_config"])) + return ActorCritic( + ac_net, ActorCriticConfig(**config["algorithm"]), exp_manager, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs=experience_sampler_kwargs + ) diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index 827aeb64c..c55eaf357 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -7,7 +7,8 @@ import numpy as np import torch import torch.nn as nn -from maro.rl.experience import ExperienceManager + +from maro.rl.experience import ExperienceStore, UniformSampler from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption from maro.rl.policy.algorithms import DQN, DQNConfig @@ -37,25 +38,23 @@ }, "algorithm": { "reward_discount": .0, - "target_update_freq": 5, + "update_target_every": 5, "train_epochs": 10, "soft_update_coefficient": 0.1, "double": False }, - "experience_manager": { - "rollout": { # for experience managers in actor processes - "capacity": 1000, - "overwrite_type": "rolling", + "experience_store": { + "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, + "update": {"capacity": 100000, "overwrite_type": "rolling"} + }, + "sampler": { + "rollout": { "batch_size": -1, "replace": False }, - "learning": { # for experience managers in the learner process - "capacity": 100000, - "overwrite_type": "rolling", + "update": { "batch_size": 128, - "alpha": 0.6, - "beta": 0.4, - "beta_step": 0.001 + "replace": True } }, "exploration": { @@ -78,21 +77,29 @@ def forward(self, states): return self.component(states) -def get_dqn_policy(learning: bool = True): +def get_dqn_policy(mode="update"): + assert mode in {"inference", "update", "inference-update"} qnet = QNet( FullyConnectedBlock(**config["model"]["network"]), - optim_option=OptimOption(**config["model"]["optimization"]) if learning else None + optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None ) - if learning: - exp_manager = ExperienceManager(**config["experience_manager"]["learning"]) + if mode == "update": + exp_manager = ExperienceStore(**config["experience_store"]["update"]) exploration = None + experience_sampler_kwargs = config["sampler"]["update"] else: - exp_manager = ExperienceManager(**config["experience_manager"]["rollout"]) exploration = EpsilonGreedyExploration() exploration.register_schedule( scheduler_cls=MultiPhaseLinearExplorationScheduler, param_name="epsilon", **config["exploration"] ) + exp_manager = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) + experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - return DQN(qnet, exp_manager, DQNConfig(**config["algorithm"]), exploration=exploration) + return DQN( + qnet, DQNConfig(**config["algorithm"]), exp_manager, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs=experience_sampler_kwargs, + exploration=exploration + ) diff --git a/examples/rl/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py index dbbbe671b..f9ad93f6f 100644 --- a/examples/rl/cim/env_wrapper.py +++ b/examples/rl/cim/env_wrapper.py @@ -8,13 +8,22 @@ from maro.simulator.scenarios.cim.common import Action, ActionType +def step_callback(env, tracker, transition): + tracker["env_metric"] = env.metrics + + class CIMEnvWrapper(AbsEnvWrapper): def __init__( self, env, replay_agent_ids=None, *, port_attributes, vessel_attributes, num_actions, look_back, max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, finite_vessel_space=True, has_early_discharge=True ): - super().__init__(env, replay_agent_ids=replay_agent_ids, reward_eval_delay=reward_eval_delay) + super().__init__( + env, + replay_agent_ids=replay_agent_ids, + reward_eval_delay=reward_eval_delay, + step_callback=step_callback + ) self.port_attributes = port_attributes self.vessel_attributes = vessel_attributes self.action_space = list(np.linspace(-1.0, 1.0, num_actions)) @@ -46,24 +55,18 @@ def get_state(self, tick=None): port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] self._state_info = { - port_idx: { - "tick": tick, - "action_scope": self.event.action_scope, - "port_idx": port_idx, - "vessel_idx": vessel_idx - } + port_idx: {"tick": tick, "action_scope": self.event.action_scope, "vessel_idx": vessel_idx} } state = np.concatenate((port_features, vessel_features)) self._last_action_tick = tick return {port_idx: state} def to_env_action(self, action_by_agent: dict): - env_action = {} + env_action = [] for agent_id, action_info in action_by_agent.items(): - state_info = self._state_info[agent_id] - tick, port, vessel, action_scope = ( - state_info["tick"], state_info["port_idx"], state_info["vessel_idx"], state_info["action_scope"] - ) + tick = self._state_info[agent_id]["tick"] + vessel = self._state_info[agent_id]["vessel_idx"] + action_scope = self._state_info[agent_id]["action_scope"] vessel_snapshots = self.env.snapshot_list["vessels"] vessel_space = ( vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") @@ -83,20 +86,21 @@ def to_env_action(self, action_by_agent: dict): else: actual_action, action_type = 0, None - env_action[agent_id] = Action( - vessel_idx=vessel, port_idx=port, quantity=actual_action, action_type=action_type + env_action.append( + Action(port_idx=agent_id, vessel_idx=vessel, quantity=actual_action, action_type=action_type) ) + return env_action - def get_reward(self, tick=None): + def get_reward(self, actions, tick=None): """Delayed reward evaluation.""" if tick is None: tick = self._last_action_tick start_tick = tick + 1 ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) - ports = list(self.action_history[tick].keys()) - + # Get the ports that took actions at the given tick + ports = [action.port_idx for action in actions] port_snapshots = self.env.snapshot_list["ports"] future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index a3ec1c1d0..2bc657351 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -8,6 +8,7 @@ cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) +from ac import get_ac_policy from dqn import get_dqn_policy from env_wrapper import AGENT_IDS @@ -15,5 +16,5 @@ warmup = {name: 1 for name in AGENT_IDS} # use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} +rl_policy_func_index = {name: get_ac_policy for name in AGENT_IDS} agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/workflows/agent_wrapper.py b/examples/rl/workflows/agent_wrapper.py index 2d3145cdd..d98981387 100644 --- a/examples/rl/workflows/agent_wrapper.py +++ b/examples/rl/workflows/agent_wrapper.py @@ -5,19 +5,27 @@ from os.path import dirname, realpath from maro.rl.learning import AgentWrapper +from maro.rl.policy import LocalPolicyManager workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import agent2policy, non_rl_policy_func_index, rl_policy_func_index +from general import agent2policy, log_dir, non_rl_policy_func_index, rl_policy_func_index, update_trigger, warmup -def get_agent_wrapper(): +def get_agent_wrapper(mode: str = "inference-update"): + assert mode in {"inference", "inference-update"}, f"mode must be 'inference' or 'inference-update', got {mode}" + policy_dict = { + **{name: func() for name, func in non_rl_policy_func_index.items()}, + **{name: func(mode=mode) for name, func in rl_policy_func_index.items()} + } return AgentWrapper( - { - **{name: func() for name, func in non_rl_policy_func_index.items()}, - **{name: func(learning=False) for name, func in rl_policy_func_index.items()} - }, + LocalPolicyManager( + policy_dict, + update_trigger=update_trigger, + warmup=warmup, + log_dir=log_dir + ), agent2policy ) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 5b6a7d3bd..a59531f54 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -8,7 +8,6 @@ num_episodes: 5 eval_schedule: 5 num_steps: -1 max_lag: 0 -log_env_summary: true sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 442e2ce9d..9b937fedb 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -26,6 +26,9 @@ rl_policy_func_index = getattr(module, "rl_policy_func_index") update_trigger = getattr(module, "update_trigger") warmup = getattr(module, "warmup") +post_collect = getattr(module, "post_collect", None) +post_evaluate = getattr(module, "end_of_evaluate", None) +post_update = getattr(module, "post_update", None) num_rollouts = config["sync"]["num_rollout_workers"] if config["mode"] == "sync" else config["async"]["num_actors"] replay_agents = [[] for _ in range(num_rollouts)] diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index 594e78e61..5c48f4469 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -4,30 +4,25 @@ import sys from os.path import dirname, realpath -from maro.rl.learning import AgentWrapper, SimpleLearner +from maro.rl.learning import SimpleLearner workflow_dir = dirname((realpath(__file__))) if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import ( - agent2exploration, agent2policy, config, policy_func_index, exploration_func_index, get_env_wrapper, log_dir -) +from agent_wrapper import get_agent_wrapper +from general import config, get_env_wrapper, log_dir, post_collect, post_evaluate if __name__ == "__main__": SimpleLearner( get_env_wrapper(), - AgentWrapper( - {name: func(learning=False) for name, func in policy_func_index.items()}, - agent2policy, - exploration_dict={name: func() for name, func in exploration_func_index.items()}, - agent2exploration=agent2exploration - ), + get_agent_wrapper(), num_episodes=config["num_episodes"], num_steps=config["num_steps"], eval_schedule=config["eval_schedule"], - log_env_summary=config["log_env_summary"], + post_collect=post_collect, + post_evaluate=post_evaluate, log_dir=log_dir ).run() diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py index 00b10d8cb..14820e684 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -14,7 +14,7 @@ from agent_wrapper import get_agent_wrapper from policy_manager.policy_manager import get_policy_manager -from general import config, get_env_wrapper, log_dir +from general import config, post_collect, post_evaluate, get_env_wrapper, log_dir def get_rollout_manager(): @@ -24,7 +24,8 @@ def get_rollout_manager(): get_env_wrapper(), get_agent_wrapper(), num_steps=config["num_steps"], - log_env_summary=config["log_env_summary"], + post_collect=post_collect, + post_evaluate=post_evaluate, log_dir=log_dir ) if rollout_mode == "multi-process": @@ -33,7 +34,8 @@ def get_rollout_manager(): get_env_wrapper, get_agent_wrapper, num_steps=config["num_steps"], - log_env_summary=config["log_env_summary"], + post_collect=post_collect, + post_evaluate=post_evaluate, log_dir=log_dir, ) if rollout_mode == "multi-node": @@ -45,7 +47,7 @@ def get_rollout_manager(): min_finished_workers=config["sync"]["min_finished_workers"], # max_extra_recv_tries=config["sync"]["max_extra_recv_tries"], extra_recv_timeout=config["sync"]["extra_recv_timeout"], - log_env_summary=config["log_env_summary"], + post_collect=post_collect, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} ) diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 6ce7fc59f..dfd4d5cbf 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -22,7 +22,7 @@ config["sync"]["rollout_group"], worker_id, get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), - get_agent_wrapper(), + get_agent_wrapper(mode="inference"), proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index da9097898..6952fb274 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .experience_manager import ExperienceManager, ExperienceSet -from .sampler import AbsSampler, PrioritizedSampler +from .experience_store import ExperienceStore, ExperienceSet +from .sampler import AbsSampler, PrioritizedSampler, UniformSampler -__all__ = ["AbsSampler", "ExperienceManager", "ExperienceSet", "PrioritizedSampler"] +__all__ = ["AbsSampler", "ExperienceStore", "ExperienceSet", "PrioritizedSampler", "UniformSampler"] diff --git a/maro/rl/experience/experience_manager.py b/maro/rl/experience/experience_store.py similarity index 68% rename from maro/rl/experience/experience_manager.py rename to maro/rl/experience/experience_store.py index b17392d06..9ec4cedc5 100644 --- a/maro/rl/experience/experience_manager.py +++ b/maro/rl/experience/experience_store.py @@ -45,7 +45,7 @@ def extend(self, other): self.info += other.info -class ExperienceManager: +class ExperienceStore: """Storage facility for simulation experiences. This implementation uses a dictionary of lists as the internal data structure. The objects for each key @@ -57,37 +57,16 @@ class ExperienceManager: are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - "rolling", where overwrite occurs sequentially with wrap-around. - "random", where overwrite occurs randomly among filled positions. - batch_size (int): Batch size for the default uniform sampling. This can be set to -1 if the required - batch size is the number of items stored. To get the whole data, set this to -1 and ``replace`` to - False. If a sampler is registered, this is ignored, and the batch size will be determined by the - sampler. Defaults to 32. - replace (bool): A flag indicating whether the default uniform sampling is with replacement or without. - Ignored if a sampler is registered. Defaults to True. - sampler_cls: Type of sampler to be registered. Must be a subclass of ``AbsSampler``. - Defaults to UnifromSampler. - sampler_params (dict): Keyword parameters for ``sampler_cls``. """ - def __init__( - self, - capacity: int, - overwrite_type: str = "rolling", - batch_size: int = 32, - replace: bool = True, - sampler_cls=None, - **sampler_params - ): - super().__init__() + def __init__(self, capacity: int, overwrite_type: str = "rolling"): if overwrite_type not in {"rolling", "random"}: raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") - if batch_size <= 0 and batch_size != -1: - raise ValueError("batch_size must be -1 or a positive integer") + + super().__init__() self._capacity = capacity self._overwrite_type = overwrite_type self._keys = ExperienceSet.__slots__ self.data = {key: [None] * self._capacity for key in self._keys} - self.batch_size = batch_size - self.replace = replace - self.sampler = None if sampler_cls is None else sampler_cls(self, **sampler_params) self._size = 0 self._index = 0 @@ -141,22 +120,6 @@ def put(self, experience_set: ExperienceSet): self.data[key][idx] = val self._size = min(self._capacity, num_experiences) - if self.sampler: - self.sampler.on_put(experience_set, indexes) - - def get(self): - """Retrieve an experience set from the memory. - - If not sampler has been registered, a uniformly random sample of the stored data will be returned - in the form of an ``ExperienceSet`` and the memory will be cleared. Otherwise, a sample from the - memory will be returned according to the sampling logic defined by the registered sampler. - """ - batch_size = self._size if self.batch_size == -1 else self.batch_size - if not self.sampler: - indexes = np.random.choice(self._size, size=batch_size, replace=self.replace) - return ExperienceSet(*[[self.data[key][idx] for idx in indexes] for key in self._keys]) - else: - return self.sampler.get() def clear(self): """Empty the memory.""" diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index 1a2fb510d..bf117a272 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -6,18 +6,18 @@ import numpy as np -from .experience_manager import ExperienceManager, ExperienceSet +from .experience_store import ExperienceStore, ExperienceSet class AbsSampler(ABC): """Sampler class. Args: - experience_manager (ExperienceManager): experience manager the sampler is associated with. + experience_store (ExperienceStore): experience manager the sampler is associated with. """ - def __init__(self, experience_manager: ExperienceManager): + def __init__(self, experience_store: ExperienceStore): super().__init__() - self.experience_manager = experience_manager + self.experience_store = experience_store @abstractmethod def get(self) -> ExperienceSet: @@ -25,10 +25,37 @@ def get(self) -> ExperienceSet: raise NotImplementedError def on_put(self, experience_set: ExperienceSet, indexes: List[int]): - """Callback to be executed after calling experience_manager.put().""" + """Callback to be executed after calling experience_store.put().""" pass +class UniformSampler(ABC): + """Uniform sampler class. + + Args: + experience_store (ExperienceStore): experience manager the sampler is associated with. + batch_size (int): Batch size for the default uniform sampling. This can be set to -1 if the required + batch size is the number of items stored. To get the whole data, set this to -1 and ``replace`` to + False. Defaults to 32. + replace (bool): A flag indicating whether the default uniform sampling is with replacement or without. + Defaults to True. + """ + def __init__(self, experience_store: ExperienceStore, batch_size: int = 32, replace: bool = True): + if batch_size <= 0 and batch_size != -1: + raise ValueError("batch_size must be -1 or a positive integer") + super().__init__() + self.experience_store = experience_store + self.batch_size = batch_size + self.replace = replace + + def get(self) -> ExperienceSet: + batch_size = self.experience_store.size if self.batch_size == -1 else self.batch_size + indexes = np.random.choice(self.experience_store.size, size=batch_size, replace=self.replace) + return ExperienceSet( + *[[self.experience_store.data[key][idx] for idx in indexes] for key in self.experience_store.keys] + ) + + class PrioritizedSampler(AbsSampler): """Sampler for Prioritized Experience Replay (PER). @@ -40,7 +67,7 @@ class PrioritizedSampler(AbsSampler): The rank-based variant is not implemented here. Args: - experience_manager (ExperienceManager): experience manager the sampler is associated with. + experience_store (ExperienceStore): experience manager the sampler is associated with. batch_size (int): mini-batch size. Defaults to 32. alpha (float): Prioritization strength. Sampling probabilities are calculated according to P = p_i^alpha / sum(p_k^alpha). Defaults to 0.6. @@ -52,7 +79,7 @@ class PrioritizedSampler(AbsSampler): """ def __init__( self, - experience_manager: ExperienceManager, + experience_store: ExperienceStore, batch_size: int = 32, alpha: float = 0.6, beta: float = 0.4, @@ -60,8 +87,8 @@ def __init__( ): if beta > 1.0: raise ValueError("beta should be between 0.0 and 1.0") - super().__init__(experience_manager) - self._sum_tree = np.zeros(2 * self.experience_manager.capacity - 1) + super().__init__(experience_store) + self._sum_tree = np.zeros(2 * self.experience_store.capacity - 1) self.batch_size = batch_size self.alpha = alpha self.beta = beta @@ -81,7 +108,7 @@ def update(self, indexes, td_errors): """Update priority values at given indexes.""" for idx, err in zip(indexes, td_errors): priority = self._get_priority(err) - tree_idx = idx + self.experience_manager.capacity - 1 + tree_idx = idx + self.experience_store.capacity - 1 delta = priority - self._sum_tree[tree_idx] self._sum_tree[tree_idx] = priority self._update(tree_idx, delta) @@ -94,20 +121,20 @@ def get(self): low, high = segment_len * i, segment_len * (i + 1) sampled_val = np.random.uniform(low=low, high=high) idx = self._get(0, sampled_val) - data_idx = idx - self.experience_manager.capacity + 1 + data_idx = idx - self.experience_store.capacity + 1 indexes.append(data_idx) priorities.append(self._sum_tree[idx]) self.beta = min(1., self.beta + self.beta_step) sampling_probabilities = priorities / self.total() - is_weights = np.power(self.experience_manager.size * sampling_probabilities, -self.beta) + is_weights = np.power(self.experience_store.size * sampling_probabilities, -self.beta) is_weights /= is_weights.max() return ExperienceSet( - states=[self.experience_manager.data["states"][idx] for idx in indexes], - actions=[self.experience_manager.data["actions"][idx] for idx in indexes], - rewards=[self.experience_manager.data["rewards"][idx] for idx in indexes], - next_states=[self.experience_manager.data["next_states"][idx] for idx in indexes], + states=[self.experience_store.data["states"][idx] for idx in indexes], + actions=[self.experience_store.data["actions"][idx] for idx in indexes], + rewards=[self.experience_store.data["rewards"][idx] for idx in indexes], + next_states=[self.experience_store.data["next_states"][idx] for idx in indexes], info=[{"index": idx, "is_weight": wt} for idx, wt in zip(indexes, is_weights)] ) diff --git a/maro/rl/exploration/noise_exploration.py b/maro/rl/exploration/noise_exploration.py index 560c0c5d2..45f14ce6a 100644 --- a/maro/rl/exploration/noise_exploration.py +++ b/maro/rl/exploration/noise_exploration.py @@ -22,10 +22,6 @@ def __init__( self.min_action = min_action self.max_action = max_action - @abstractmethod - def set_params(self, **parameters): - raise NotImplementedError - @abstractmethod def __call__(self, action) -> np.ndarray: raise NotImplementedError diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index b61977ad3..1fff5614b 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -3,7 +3,7 @@ from .agent_wrapper import AgentWrapper from .early_stopper import AbsEarlyStopper -from .env_wrapper import AbsEnvWrapper +from .env_wrapper import AbsEnvWrapper, Transition from .simple_learner import SimpleLearner -__all__ = ["AbsEarlyStopper", "AbsEnvWrapper", "AgentWrapper", "SimpleLearner"] +__all__ = ["AbsEarlyStopper", "AbsEnvWrapper", "AgentWrapper", "SimpleLearner", "Transition"] diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index 03a1e4f57..752e87b0d 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -3,7 +3,7 @@ from typing import Dict -from maro.rl.policy import AbsPolicy +from maro.rl.policy import LocalPolicyManager from .env_wrapper import AbsEnvWrapper @@ -12,14 +12,16 @@ class AgentWrapper: """Multi-agent wrapper that interacts with an ``EnvWrapper`` with a unified inferface. Args: - policy_dict (Dict[str, AbsPolicy]): Policies for inference. + policy_manager (LocalPolicyManager): ``LocalPolicyManager`` instance. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. """ - def __init__(self, policy_dict: Dict[str, AbsPolicy], agent2policy: Dict[str, str]): - self.policy_dict = policy_dict + def __init__(self, policy_manager: LocalPolicyManager, agent2policy: Dict[str, str]): + self.policy_manager = policy_manager self.agent2policy = agent2policy - self.policy = {agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items()} + self.policy = { + agent_id: self.policy_manager.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() + } def choose_action(self, state: dict) -> dict: """Generate an action based on the given state. @@ -38,24 +40,24 @@ def get_batch(self, env: AbsEnvWrapper): self.policy[agent_id].store(exp) names.add(self.agent2policy[agent_id]) - return {name: self.policy_dict[name].experience_manager.get() for name in names} + return {name: self.policy_manager.policy_dict[name].sampler.get() for name in names} def set_policy_states(self, policy_state_dict: dict): """Update policy states.""" for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].set_state(policy_state) + self.policy_manager.policy_dict[policy_id].set_state(policy_state) def exploration_step(self): - for policy in self.policy_dict.values(): + for policy in self.policy_manager.policy_dict.values(): if hasattr(policy, "exploration_step"): policy.exploration_step() def exploit(self): - for policy in self.policy_dict.values(): + for policy in self.policy_manager.policy_dict.values(): if hasattr(policy, "exploit"): policy.exploit() def explore(self): - for policy in self.policy_dict.values(): + for policy in self.policy_manager.policy_dict.values(): if hasattr(policy, "explore"): policy.explore() diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index f0c477e57..4e1d4570e 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -9,6 +9,29 @@ from maro.simulator import Env +class Transition: + """Convenience class to be used in an environment wrapper's post-step processing function. + + Args: + state: Output of the environment wrapper's ``get_state``. + action: Output of the ``AgentWrapper`` that interacts with environment wrapper. + env_action: Output of the environment wrapper's ``to_env_action``. + reward: Output of the environmet wrapper's ``get_reward``. + next_state: The state immediately following ``state``. + info: Output of the environment wrapper's ``get_transition_info``. + """ + + __slots__ = ["state", "action", "env_action", "reward", "next_state", "info"] + + def __init__(self, state, action, env_action, reward, next_state, info): + self.state = state + self.action = action + self.env_action = env_action + self.reward = reward + self.next_state = next_state + self.info = info + + class AbsEnvWrapper(ABC): """Environment wrapper that performs scenario-specific processing, transition caching and experience generation. @@ -22,27 +45,40 @@ class AbsEnvWrapper(ABC): to None. get_experience_func (Callable): Custom function to convert the replay buffer to training experiences. Defaults to None, in which case the replay buffer will be converted directly to SARS experiences for each agent. + get_experience_func_kwargs (dict): Keyword arguments for the user-defined ``get_experience_func``. Defaults to + an empty dictionary. + step_callback (Callable): Custom function to gather information about a transition and the evolvement of the + environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` + instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition + is a ``Transition`` object. For example, this callback can be used to collect various statistics on the + simulation. Defaults to None. """ def __init__( self, env: Env, reward_eval_delay: int = 0, replay_agent_ids: list = None, - get_experience_func: Callable = None + get_experience_func: Callable = None, + get_experience_func_kwargs: dict = {}, + step_callback: Callable = None ): self.env = env self.reward_eval_delay = reward_eval_delay - self.action_history = defaultdict(dict) - self.get_experience_func = get_experience_func + + self._get_experience_func = get_experience_func + self._get_experience_func_kwargs = get_experience_func_kwargs + self._step_callback = step_callback replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids self._replay_buffer = {agent_id: defaultdict(list) for agent_id in replay_agent_ids} - self._pending_reward_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated + self._transition_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None - self._total_reward = defaultdict(int) self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. self._state = None # the latest extracted state is kept here + self.tracker = {} # User-defined tracking information is placed here. + self._replay = True + @property def step_index(self): """Number of environmental steps taken so far.""" @@ -54,7 +90,7 @@ def agent_idx_list(self): @property def summary(self): - return self.env.metrics, self._total_reward + return self.env.metrics @property def state(self): @@ -65,6 +101,12 @@ def state(self): def event(self): return self._event + def collect(self): + self._replay = True + + def evaluate(self): + self._replay = False + def start(self): """Generate the initial environmental state at the beginning of a simulation episode.""" self._step_index = 0 @@ -90,25 +132,29 @@ def to_env_action(self, action) -> dict: raise NotImplementedError @abstractmethod - def get_reward(self, tick: int = None): + def get_reward(self, actions: list, tick: int = None): """Evaluate the reward for an action. Args: - tick (int): Evaluate the reward for the action that occured at the given tick. The tick may be - None, in which case the reward is evaluated for the latest action (i.e., immediate reward). - Otherwise, it must be a key in the ``action_history`` attribute (i.e., there must be an action - at that tick). Defaults to None. + tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in + ``actions`` must be an Action object defined for the environment in question. The tick may + be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). + Defaults to None. Returns: A dictionary with (agent ID, reward) as key-value pairs. """ raise NotImplementedError - def get_transition_info(self): + def get_transition_info(self, tick: int = None): """Get additional info for a transition. The returned transition info will be stored in the experience manager alongside states, actions, rewards. + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + Returns: A dictionary with (agent ID, transition_info) as key-value pairs. @@ -122,70 +168,76 @@ def step(self, action_by_agent: dict): reward cannot be determined yet due to a non-zero ``reward_eval_delay``. """ self._step_index += 1 - env_action_dict = self.to_env_action(action_by_agent) - for agent_id, action in env_action_dict.items(): - self.action_history[self.env.tick][agent_id] = action - transition_info = self.get_transition_info() - self._pending_reward_cache.append((self._state, action_by_agent, transition_info, self.env.tick)) + env_action = self.to_env_action(action_by_agent) + + self._transition_cache.append(( + self._state, + action_by_agent, + env_action, + self.get_transition_info(), + self.env.tick + )) - env_action = list(env_action_dict.values()) _, self._event, done = self.env.step(env_action) if not done: self._state = self.get_state(self.env.tick) # current env state else: self._state = None - self.end_of_episode() """ If this is the final step, evaluate rewards for all remaining events except the last. Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ while ( - self._pending_reward_cache and - (done or self.env.tick - self._pending_reward_cache[0][-1] >= self.reward_eval_delay) + self._transition_cache and + (done or self.env.tick - self._transition_cache[0][-1] >= self.reward_eval_delay) ): - state, action, info, tick = self._pending_reward_cache.popleft() - reward = self.get_reward(tick=tick) - for agent_id, st in state.items(): - self._total_reward[agent_id] += reward[agent_id] - if agent_id in self._replay_buffer: - buf = self._replay_buffer[agent_id] - buf["states"].append(st) - buf["actions"].append(action[agent_id]) - buf["rewards"].append(reward[agent_id]) - buf["info"].append(info[agent_id] if info else None) - - def end_of_episode(self): - """Custom processing logic at the end of an episode.""" - pass + state, action, env_action, info, tick = self._transition_cache.popleft() + reward = self.get_reward(env_action, tick=tick) + if self._step_callback: + next_state = self._transition_cache[0][0] if self._transition_cache else None + transition = Transition(state, action, env_action, reward, next_state, info) + # put things you want to track in the tracker attribute + self._step_callback(self.env, self.tracker, transition) + + if self._replay: + for agent_id, agent_state in state.items(): + if agent_id in self._replay_buffer: + buf = self._replay_buffer[agent_id] + buf["states"].append(agent_state) + buf["actions"].append(action[agent_id]) + buf["rewards"].append(reward[agent_id]) + buf["info"].append(info[agent_id] if info else None) def get_experiences(self): """Get per-agent experiences from the replay buffer.""" - if not self.get_experience_func: - exp_by_agent = {} - for agent_id, buf in self._replay_buffer.items(): - exp_set = ExperienceSet( + if not self._get_experience_func: + exp_by_agent = { + agent_id: ExperienceSet( buf["states"][:-1], buf["actions"][:-1], buf["rewards"][:-1], buf["states"][1:], buf["info"][:-1], - ) - del buf["states"][:-1] - del buf["actions"][:-1] - del buf["rewards"][:-1] - del buf["info"][:-1] - exp_by_agent[agent_id] = exp_set - - return exp_by_agent + ) for agent_id, buf in self._replay_buffer.items() + } else: - return self.get_experience_func(self._replay_buffer) + exp_by_agent = self._get_experience_func(self._replay_buffer, **self._get_experience_func_kwargs) + + # clear the replay buffer of transitions that have already been converted to experiences. + for buf in self._replay_buffer.values(): + del buf["states"][:-1] + del buf["actions"][:-1] + del buf["rewards"][:-1] + del buf["info"][:-1] + + return exp_by_agent def reset(self): self.env.reset() - self._total_reward.clear() self._state = None - self._pending_reward_cache.clear() + self._transition_cache.clear() + self.tracker.clear() for replay in self._replay_buffer.values(): replay.clear() diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py index 7348803cd..6c2e72262 100644 --- a/maro/rl/learning/simple_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -3,7 +3,7 @@ import time from os import getcwd -from typing import List, Union +from typing import Callable, List, Union from maro.utils import Logger @@ -32,8 +32,14 @@ class SimpleLearner: as the evaluation environment. Defaults to None. early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the environment metrics are met following an evaluation episode. Default to None. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. + post_collect (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + post_evaluate (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -48,13 +54,14 @@ def __init__( eval_schedule: Union[int, List[int]] = None, eval_env: AbsEnvWrapper = None, early_stopper: AbsEarlyStopper = None, - log_env_summary: bool = True, + post_collect: Callable = None, + post_evaluate: Callable = None, log_dir: str = getcwd(), ): if num_steps == 0 or num_steps < -1: raise ValueError("num_steps must be a positive integer or -1") - self.logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) + self._logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) self.env = env_wrapper self.eval_env = eval_env if eval_env else self.env self.agent = agent_wrapper @@ -76,12 +83,12 @@ def __init__( if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: self._eval_schedule.append(num_episodes) - self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") + self._logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") self._eval_point_index = 0 self.early_stopper = early_stopper - - self._log_env_summary = log_env_summary + self._post_collect = post_collect + self._post_evaluate = post_evaluate def run(self): """Entry point for executing a learning workflow.""" @@ -107,17 +114,14 @@ def _collect_and_update(self, ep: int): segment = 0 while self.env.state: segment += 1 - exp_by_agent = self._collect(ep, segment) - self.agent.update(exp_by_agent) - num_experiences_collected += sum(exp.size for exp in exp_by_agent.values()) + self._collect(ep, segment) + exp_by_policy = self.agent.get_batch(self.env) + self.agent.policy_manager.update(exp_by_policy) + num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) # update the exploration parameters if an episode is finished self.agent.exploration_step() - # performance details - if self._log_env_summary: - self.logger.info(f"ep {ep}: {self.env.summary}") - - self.logger.info( + self._logger.info( f"ep {ep} summary - " f"running time: {time.time() - t0} " f"env steps: {self.env.step_index} " @@ -126,15 +130,15 @@ def _collect_and_update(self, ep: int): def _evaluate(self): """Policy evaluation.""" - self.logger.info("Evaluating...") + self._logger.info("Evaluating...") self.agent.exploit() self.eval_env.reset() self.eval_env.start() # get initial state while self.eval_env.state: self.eval_env.step(self.agent.choose_action(self.eval_env.state)) - # performance details - self.logger.info(f"Evaluation result: {self.eval_env.summary}") + if self._post_evaluate: + self._post_evaluate([self.env.tracker]) def _collect(self, ep, segment): start_step_index = self.env.step_index + 1 @@ -143,9 +147,10 @@ def _collect(self, ep, segment): self.env.step(self.agent.choose_action(self.env.state)) steps_to_go -= 1 - self.logger.info( + self._logger.info( f"Roll-out finished for ep {ep}, segment {segment}" f"(steps {start_step_index} - {self.env.step_index})" ) - return self.env.get_experiences() + if self._post_collect: + self._post_collect([self.env.tracker]) diff --git a/maro/rl/learning/synchronous/learner.py b/maro/rl/learning/synchronous/learner.py index 87175f596..c1b4e6fba 100644 --- a/maro/rl/learning/synchronous/learner.py +++ b/maro/rl/learning/synchronous/learner.py @@ -35,7 +35,6 @@ class Learner: log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. - end_of_episode_kwargs: Keyword arguments for custom end-of-episode processing. """ def __init__( self, @@ -44,8 +43,7 @@ def __init__( num_episodes: int, eval_schedule: Union[int, List[int]] = None, early_stopper: AbsEarlyStopper = None, - log_dir: str = getcwd(), - **end_of_episode_kwargs + log_dir: str = getcwd() ): self.logger = Logger("LEARNER", dump_folder=log_dir) self.policy_manager = policy_manager @@ -72,9 +70,6 @@ def __init__( self.early_stopper = early_stopper - self._end_of_episode_kwargs = end_of_episode_kwargs - self._last_policy_version = 0 - def run(self): """Entry point for executing a learning workflow.""" for ep in range(1, self.num_episodes + 1): @@ -121,9 +116,3 @@ def _collect_and_update(self, ep: int): f"experience collection time: {collect_time} " f"policy update time: {policy_update_time}" ) - - self.end_of_episode(ep, **self._end_of_episode_kwargs) - - def end_of_episode(self, ep: int, **kwargs): - """Custom end-of-episode processing is implemented here.""" - pass diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index c0524711c..42945a4c1 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -19,9 +19,31 @@ class AbsRolloutManager(ABC): - """Controller for simulation data collection.""" - def __init__(self): + """Controller for simulation data collection. + + Args: + post_collect (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + post_evaluate (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + def __init__( + self, + post_collect: Callable = None, + post_evaluate: Callable = None, + log_dir: str = getcwd() + ): super().__init__() + self._post_collect = post_collect + self._post_evaluate = post_evaluate + self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) self.episode_complete = False @abstractmethod @@ -65,36 +87,44 @@ class LocalRolloutManager(AbsRolloutManager): agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - eval_env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be - used as the evaluation environment. Defaults to None. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current + eval_env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will + be used as the evaluation environment. Defaults to None. + post_collect (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + post_evaluate (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ - def __init__( self, env_wrapper: AbsEnvWrapper, agent_wrapper: AgentWrapper, num_steps: int = -1, eval_env_wrapper: AbsEnvWrapper = None, - log_env_summary: bool = True, - log_dir: str = getcwd(), + post_collect: Callable = None, + post_evaluate: Callable = None, + log_dir: str = getcwd() ): if num_steps == 0 or num_steps < -1: raise ValueError("num_steps must be a positive integer or -1") - super().__init__() - self._logger = Logger("LOCAL_ROLLOUT_MANAGER", dump_folder=log_dir) + super().__init__( + post_collect=post_collect, + post_evaluate=post_evaluate, + log_dir=log_dir + ) self.env = env_wrapper self.eval_env = eval_env_wrapper if eval_env_wrapper else self.env self.agent = agent_wrapper self._num_steps = num_steps if num_steps > 0 else float("inf") - self._log_env_summary = log_env_summary def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): """Collect simulation data, i.e., experiences for training. @@ -113,6 +143,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): # start of new episode if self.env.state is None: self.env.reset() + self.env.collect() self.env.start() # get initial state self.agent.exploration_step() @@ -136,9 +167,9 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): # update the exploration parameters if an episode is finished if not self.env.state: self.episode_complete = True - # performance details - if self._log_env_summary: - self._logger.info(f"ep {ep}: {self.env.summary}") + + if self._post_collect: + self._post_collect([self.env.tracker]) return self.env.get_experiences() @@ -156,15 +187,16 @@ def evaluate(self, ep: int, policy_state_dict: dict): self.agent.set_policy_states(policy_state_dict) self.agent.exploit() self.eval_env.reset() + self.eval_env.evaluate() self.eval_env.start() # get initial state while self.eval_env.state: action = self.agent.choose_action(self.eval_env.state) self.eval_env.step(action) - if self._log_env_summary: - self._logger.info(f"Evaluation result: {self.eval_env.summary}") + if self._post_evaluate: + self._post_evaluate([self.eval_env.tracker]) - return self.eval_env.summary + return self.eval_env.tracker class MultiProcessRolloutManager(AbsRolloutManager): @@ -185,10 +217,16 @@ class MultiProcessRolloutManager(AbsRolloutManager): num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. num_eval_workers (int): Number of workers for evaluation. Defaults to 1. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current + post_collect (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + post_evaluate (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( @@ -199,14 +237,17 @@ def __init__( create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, num_steps: int = -1, num_eval_workers: int = 1, - log_env_summary: bool = True, - log_dir: str = getcwd(), + post_collect: Callable = None, + post_evaluate: Callable = None, + log_dir: str = getcwd() ): - super().__init__() - self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) + super().__init__( + post_collect=post_collect, + post_evaluate=post_evaluate, + log_dir=log_dir + ) self._num_workers = num_workers self._num_steps = num_steps - self._log_env_summary = log_env_summary self._num_eval_workers = num_eval_workers self._exploration_step = False @@ -261,22 +302,22 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): self._exploration_step = False combined_exp_by_policy = defaultdict(ExperienceSet) + trackers = [] for conn in self._manager_ends: result = conn.recv() exp_by_policy = result["experiences"] - + trackers.append(result["tracker"]) for policy_name, exp in exp_by_policy.items(): combined_exp_by_policy[policy_name].extend(exp) - # log roll-out summary self.episode_complete = result["episode_end"] - if self.episode_complete and self._log_env_summary: - env_summary = result["env_summary"] - self._logger.info(f"env summary: {env_summary}") if self.episode_complete: self._exploration_step = True + if self._post_collect: + self._post_collect(trackers) + return combined_exp_by_policy def evaluate(self, ep: int, policy_state_dict: dict): @@ -293,12 +334,15 @@ def evaluate(self, ep: int, policy_state_dict: dict): for conn in eval_worker_conns: conn.send({"type": "evaluate", "episode": ep, "policy": policy_state_dict}) - env_summary_dict = {} + trackers = [] for conn in self._manager_ends: result = conn.recv() - env_summary_dict[result["worker_id"]] = result["env_summary"] + trackers.append(result["tracker"]) - return env_summary_dict + if self._post_evaluate: + self._post_evaluate(trackers) + + return trackers def exit(self): """Tell the worker processes to exit.""" @@ -326,12 +370,18 @@ class MultiNodeRolloutManager(AbsRolloutManager): Experiences collected using policy versions older than (current_version - max_lag) will be discarded. Defaults to 0, in which case only experiences collected using the latest policy version will be returned. num_eval_workers (int): Number of workers for evaluation. Defaults to 1. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. + post_collect (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + post_evaluate (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current + log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( @@ -344,19 +394,23 @@ def __init__( extra_recv_timeout: int = None, max_lag: int = 0, num_eval_workers: int = 1, - log_env_summary: bool = True, + post_collect: Callable = None, + post_evaluate: Callable = None, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): if num_eval_workers > num_workers: raise ValueError("num_eval_workers cannot exceed the number of available workers") - super().__init__() + super().__init__( + post_collect=post_collect, + post_evaluate=post_evaluate, + log_dir=log_dir + ) self._num_workers = num_workers peers = {"rollout_worker": num_workers} self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's - self._logger = Logger(self._proxy.name, dump_folder=log_dir) self._num_steps = num_steps if min_finished_workers is None: @@ -373,7 +427,6 @@ def __init__( self._extra_recv_timeout = extra_recv_timeout self._max_lag = max_lag - self._log_env_summary = log_env_summary self._num_eval_workers = num_eval_workers self._exploration_step = False @@ -406,11 +459,15 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): # Receive roll-out results from remote workers combined_exp_by_policy = defaultdict(ExperienceSet) + trackers = [] num_finishes = 0 # Ensure the minimum number of worker results are received. for msg in self._proxy.receive(): - num_finishes += self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) + tracker = self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) + if tracker is not None: + num_finishes += 1 + trackers.append(tracker) if num_finishes == self._min_finished_workers: break @@ -420,13 +477,19 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if not msg: self._logger.info(f"Receive timeout, {self._max_extra_recv_tries - i - 1} attempts left") else: - num_finishes += self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) + tracker = self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) + if tracker is not None: + num_finishes += 1 + trackers.append(tracker) if num_finishes == self._num_workers: break if self.episode_complete: self._exploration_step = True + if self._post_collect: + self._post_collect(trackers) + return combined_exp_by_policy def _handle_worker_result(self, msg, ep, segment, version, combined_exp): @@ -434,7 +497,7 @@ def _handle_worker_result(self, msg, ep, segment, version, combined_exp): self._logger.info( f"Ignored a message of type {msg.tag} (expected message type {MsgTag.COLLECT_DONE})" ) - return 0 + return if version - msg.body[MsgKey.VERSION] > self._max_lag: self._logger.info( @@ -442,21 +505,15 @@ def _handle_worker_result(self, msg, ep, segment, version, combined_exp): f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " f"got {msg.body[MsgKey.VERSION]}" ) - return 0 + return for policy_name, exp in msg.body[MsgKey.EXPERIENCES].items(): combined_exp[policy_name].extend(exp) # The message is what we expect if msg.body[MsgKey.EPISODE] == ep and msg.body[MsgKey.SEGMENT] == segment: - if MsgKey.ENV_SUMMARY in msg.body: - # log roll-out summary - if self._log_env_summary: - self._logger.info(f"env summary: {msg.body[MsgKey.ENV_SUMMARY]}") - self.episode_complete = True - return 1 - - return 0 + self.episode_complete = msg.body[MsgKey.END_OF_EPISODE] + return msg.body[MsgKey.TRACKER] def evaluate(self, ep: int, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``. @@ -471,12 +528,12 @@ def evaluate(self, ep: int, policy_state_dict: dict): msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY_STATE: policy_state_dict} workers = choices(self._workers, k=self._num_eval_workers) - env_summary_dict = {} self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(worker_id, msg_body) for worker_id in workers]) self._logger.info(f"Sent evaluation requests to {workers}") # Receive roll-out results from remote workers num_finishes = 0 + trackers = [] for msg in self._proxy.receive(): if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE] != ep: self._logger.info( @@ -485,14 +542,17 @@ def evaluate(self, ep: int, policy_state_dict: dict): ) continue - env_summary_dict[msg.source] = msg.body[MsgKey.ENV_SUMMARY] + trackers.append(msg.body[MsgKey.TRACKER]) if msg.body[MsgKey.EPISODE] == ep: num_finishes += 1 if num_finishes == self._num_eval_workers: break + + if self._post_evaluate: + self._post_evaluate(trackers) - return env_summary_dict + return trackers def exit(self): """Tell the remote workers to exit.""" diff --git a/maro/rl/learning/synchronous/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py index 453fcbfb4..e834fedfa 100644 --- a/maro/rl/learning/synchronous/rollout_worker.py +++ b/maro/rl/learning/synchronous/rollout_worker.py @@ -52,8 +52,9 @@ def collect(msg): agent_wrapper.exploration_step() if env_wrapper.state is None: - logger.info(f"Training episode {ep}") + logger.info(f"Roll-out episode {ep}") env_wrapper.reset() + env_wrapper.collect() env_wrapper.start() # get initial state starting_step_index = env_wrapper.step_index + 1 @@ -72,7 +73,7 @@ def collect(msg): "worker_index": index, "episode_end": not env_wrapper.state, "experiences": agent_wrapper.get_batch(env_wrapper), - "env_summary": env_wrapper.summary, + "tracker": env_wrapper.tracker, "num_steps": env_wrapper.step_index - starting_step_index + 1 } @@ -83,12 +84,13 @@ def evaluate(msg): agent_wrapper.set_policy_states(msg["policy"]) agent_wrapper.exploit() eval_env_wrapper.reset() + eval_env_wrapper.evaluate() eval_env_wrapper.start() # get initial state while eval_env_wrapper.state: action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) - conn.send({"worker_id": index, "env_summary": eval_env_wrapper.summary}) + conn.send({"worker_id": index, "tracker": eval_env_wrapper.tracker}) while True: msg = conn.recv() @@ -144,8 +146,9 @@ def collect(msg): agent_wrapper.exploration_step() if env_wrapper.state is None: - logger.info(f"Training episode {msg.body[MsgKey.EPISODE]}") + logger.info(f"Rollout episode {msg.body[MsgKey.EPISODE]}") env_wrapper.reset() + env_wrapper.collect() env_wrapper.start() # get initial state starting_step_index = env_wrapper.step_index + 1 @@ -165,12 +168,11 @@ def collect(msg): MsgKey.SEGMENT: segment, MsgKey.VERSION: msg.body[MsgKey.VERSION], MsgKey.EXPERIENCES: agent_wrapper.get_batch(env_wrapper), - MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1 + MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1, + MsgKey.TRACKER: env_wrapper.tracker, + MsgKey.END_OF_EPISODE: not env_wrapper.state } - if not env_wrapper.state: - return_info[MsgKey.ENV_SUMMARY] = env_wrapper.summary - proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) def evaluate(msg): @@ -178,12 +180,13 @@ def evaluate(msg): agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) agent_wrapper.exploit() eval_env_wrapper.reset() + eval_env_wrapper.evaluate() eval_env_wrapper.start() # get initial state while eval_env_wrapper.state: action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) - return_info = {MsgKey.ENV_SUMMARY: eval_env_wrapper.summary, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} + return_info = {MsgKey.TRACKER: eval_env_wrapper.tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) """ diff --git a/maro/rl/policy/algorithms/ac.py b/maro/rl/policy/algorithms/ac.py index 6609369fc..b9c52236f 100644 --- a/maro/rl/policy/algorithms/ac.py +++ b/maro/rl/policy/algorithms/ac.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Tuple +from typing import Callable, Tuple import numpy as np import torch -from maro.rl.experience import ExperienceManager +from maro.rl.experience import ExperienceStore, UniformSampler from maro.rl.model import DiscreteACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -18,58 +18,77 @@ class ActorCriticConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. - gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. + clear_experience_memory_every (int): Number of ``ActorCritic.learn`` calls between experience memory clearances. + Defaults to 1. """ __slots__ = [ - "reward_discount", "train_epochs", "gradient_iters", "critic_loss_func", "actor_loss_coefficient", "clip_ratio" + "reward_discount", "train_epochs", "critic_loss_func", "actor_loss_coefficient", "clip_ratio", + "clear_experience_memory_every" ] def __init__( self, reward_discount: float, train_epochs: int = 1, - gradient_iters: int = 1, critic_loss_cls="mse", actor_loss_coefficient: float = 1.0, - clip_ratio: float = None + clip_ratio: float = None, + clear_experience_memory_every: int = 1 ): self.reward_discount = reward_discount self.train_epochs = train_epochs - self.gradient_iters = gradient_iters self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.actor_loss_coefficient = actor_loss_coefficient self.clip_ratio = clip_ratio + self.clear_experience_memory_every = clear_experience_memory_every class ActorCritic(AbsCorePolicy): """Actor Critic algorithm with separate policy and value models. - In accordance with its on-policy nature, the experiences manager is emptied at the end of each ``update()`` call. - References: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. - experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences - for training. config: Configuration for the AC algorithm. + experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences + generated by the policy. + experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to + ``UnifromSampler``. + experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. + post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking + the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ - def __init__(self, ac_net: DiscreteACNet, experience_manager: ExperienceManager, config: ActorCriticConfig): + def __init__( + self, + ac_net: DiscreteACNet, + config: ActorCriticConfig, + experience_store: ExperienceStore, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs: dict = {}, + post_step: Callable = None + ): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") - super().__init__(experience_manager) + super().__init__( + experience_store, + experience_sampler_cls=experience_sampler_cls, + experience_sampler_kwargs=experience_sampler_kwargs + ) self.ac_net = ac_net self.config = config + self._post_step = post_step + self._num_learn_calls = 0 self.device = self.ac_net.device def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: @@ -82,37 +101,41 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: def learn(self): self.ac_net.train() for _ in range(self.config.train_epochs): - experience_set = self.experience_manager.get() + experience_set = self.sampler.get() states, next_states = experience_set.states, experience_set.next_states actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.device) log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) - for _ in range(self.config.gradient_iters): - action_probs, state_values = self.ac_net(states) - state_values = state_values.squeeze() - with torch.no_grad(): - next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() - return_est = rewards + self.config.reward_discount * next_state_values - advantages = return_est - state_values - - # actor loss - log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - if self.config.clip_ratio is not None: - ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() - else: - actor_loss = -(log_p_new * advantages).mean() - - # critic_loss - critic_loss = self.config.critic_loss_func(state_values, return_est) - loss = critic_loss + self.config.actor_loss_coefficient * actor_loss - - self.ac_net.step(loss) - - # Empty the experience manager due to the on-policy nature of the algorithm. - self.experience_manager.clear() + action_probs, state_values = self.ac_net(states) + state_values = state_values.squeeze() + with torch.no_grad(): + next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() + return_est = rewards + self.config.reward_discount * next_state_values + advantages = return_est - state_values + + # actor loss + log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) + if self.config.clip_ratio is not None: + ratio = torch.exp(log_p_new - log_p) + clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() + else: + actor_loss = -(log_p_new * advantages).mean() + + # critic_loss + critic_loss = self.config.critic_loss_func(state_values, return_est) + loss = critic_loss + self.config.actor_loss_coefficient * actor_loss + + self.ac_net.step(loss) + + if self._post_step: + self._post_step(loss.detach().cpu().numpy(), self.tracker) + + # Empty the experience store due to the on-policy nature of the algorithm. + self._num_learn_calls += 1 + if self._num_learn_calls % self.config.clear_experience_memory_every == 0: + self.experience_store.clear() def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/algorithms/ddpg.py b/maro/rl/policy/algorithms/ddpg.py index 5d169f8bf..3a8426be1 100644 --- a/maro/rl/policy/algorithms/ddpg.py +++ b/maro/rl/policy/algorithms/ddpg.py @@ -6,7 +6,8 @@ import numpy as np import torch -from maro.rl.experience import ExperienceManager +from maro.rl.experience import ExperienceStore, UniformSampler +from maro.rl.exploration import GaussianNoiseExploration from maro.rl.model import ContinuousACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -19,7 +20,6 @@ class DDPGConfig: reward_discount (float): Reward decay as defined in standard RL terminology. target_update_freq (int): Number of training rounds between policy target model updates. train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. - gradient_iters (int): Number of gradient steps for each mini-batch. Defaults to 1. q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., @@ -29,8 +29,8 @@ class DDPGConfig: Defaults to 1.0. """ __slots__ = [ - "reward_discount", "target_update_freq", "train_epochs", "gradient_iters", "q_value_loss_func", - "policy_loss_coefficient", "soft_update_coefficient" + "reward_discount", "target_update_freq", "train_epochs", "q_value_loss_func", "policy_loss_coefficient", + "soft_update_coefficient" ] def __init__( @@ -38,7 +38,6 @@ def __init__( reward_discount: float, target_update_freq: int, train_epochs: int = 1, - gradient_iters: int = 1, q_value_loss_cls="mse", policy_loss_coefficient: float = 1.0, soft_update_coefficient: float = 1.0, @@ -46,7 +45,6 @@ def __init__( self.reward_discount = reward_discount self.target_update_freq = target_update_freq self.train_epochs = train_epochs - self.gradient_iters = gradient_iters self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() self.policy_loss_coefficient = policy_loss_coefficient self.soft_update_coefficient = soft_update_coefficient @@ -61,20 +59,34 @@ class DDPG(AbsCorePolicy): Args: ac_net (ContinuousACNet): DDPG policy and q-value models. - experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences - for training. config (DDPGConfig): Configuration for DDPG algorithm. + experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences + generated by the policy. + experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to + ``UnifromSampler``. + experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. + post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking + the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ def __init__( self, ac_net: ContinuousACNet, - experience_manager: ExperienceManager, - config: DDPGConfig + config: DDPGConfig, + experience_store: ExperienceStore, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs: dict = {}, + exploration=GaussianNoiseExploration(), + post_step=None ): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") - super().__init__(experience_manager) + super().__init__( + experience_store, + experience_sampler_cls=experience_sampler_cls, + experience_sampler_kwargs=experience_sampler_kwargs, + exploration=exploration + ) self.ac_net = ac_net if self.ac_net.trainable: self.target_ac_net = ac_net.copy() @@ -82,8 +94,9 @@ def __init__( else: self.target_ac_net = None self.config = config + self._post_step = post_step self.device = self.ac_net.device - self._train_cnt = 0 + self._num_steps = 0 def choose_action(self, states) -> Union[float, np.ndarray]: with torch.no_grad(): @@ -94,7 +107,7 @@ def choose_action(self, states) -> Union[float, np.ndarray]: def learn(self): self.ac_net.train() for _ in range(self.config.train_epochs): - experience_set = self.experience_manager.get() + experience_set = self.sampler.get() states, next_states = experience_set.states, experience_set.next_states actual_actions = torch.from_numpy(experience_set.actions).to(self.device) rewards = torch.from_numpy(experience_set.rewards).to(self.device) @@ -105,15 +118,17 @@ def learn(self): next_q_values = self.target_ac_net.value(next_states) target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) - for _ in range(self.config.gradient_iters): - q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) - q_value_loss = self.config.q_value_loss_func(q_values, target_q_values) - policy_loss = -self.ac_net.value(states).mean() - loss = q_value_loss + self.config.policy_loss_coefficient * policy_loss - self.ac_net.step(loss) - self._train_cnt += 1 - if self._train_cnt % self.config.target_update_freq == 0: - self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coefficient) + q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) + q_value_loss = self.config.q_value_loss_func(q_values, target_q_values) + policy_loss = -self.ac_net.value(states).mean() + loss = q_value_loss + self.config.policy_loss_coefficient * policy_loss + self.ac_net.step(loss) + if self._post_step: + self._post_step(loss.detach().cpu().numpy(), self.tracker) + + self._num_steps += 1 + if self._num_steps % self.config.target_update_freq == 0: + self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coefficient) def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/algorithms/dqn.py b/maro/rl/policy/algorithms/dqn.py index 6ed240433..4f27080fa 100644 --- a/maro/rl/policy/algorithms/dqn.py +++ b/maro/rl/policy/algorithms/dqn.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Union +from typing import Callable, Union import numpy as np import torch -from maro.rl.experience import ExperienceManager, PrioritizedSampler +from maro.rl.experience import ExperienceStore, PrioritizedSampler, UniformSampler from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.model import DiscreteQNet from maro.rl.policy import AbsCorePolicy @@ -17,7 +17,7 @@ class DQNConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. - target_update_freq (int): Number of training rounds between target model updates. + update_target_every (int): Number of gradient steps between target model updates. train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. soft_update_coefficient (float): Soft update coefficient, e.g., target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. @@ -26,21 +26,18 @@ class DQNConfig: i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. """ - __slots__ = [ - "reward_discount", "target_update_freq", "train_epochs", "gradient_iters", "soft_update_coefficient", - "double", - ] + __slots__ = ["reward_discount", "update_target_every", "train_epochs", "soft_update_coefficient", "double"] def __init__( self, reward_discount: float, - target_update_freq: int, + update_target_every: int, train_epochs: int = 1, soft_update_coefficient: float = 0.1, double: bool = True ): self.reward_discount = reward_discount - self.target_update_freq = target_update_freq + self.update_target_every = update_target_every self.train_epochs = train_epochs self.soft_update_coefficient = soft_update_coefficient self.double = double @@ -53,23 +50,36 @@ class DQN(AbsCorePolicy): Args: q_net (DiscreteQNet): Q-value model. - experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences - for training. config (DQNConfig): Configuration for DQN algorithm. + experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences + generated by the policy. + experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to + ``UnifromSampler``. + experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. exploration (DiscreteSpaceExploration): Exploration strategy for generating exploratory actions. Defaults to an ``EpsilonGreedyExploration`` instance. + post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking + the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ def __init__( self, q_net: DiscreteQNet, - experience_manager: ExperienceManager, config: DQNConfig, - exploration: DiscreteSpaceExploration = EpsilonGreedyExploration() + experience_store: ExperienceStore, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs: dict = {}, + exploration: DiscreteSpaceExploration = EpsilonGreedyExploration(), + post_step: Callable = None ): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(experience_manager, exploration=exploration) + super().__init__( + experience_store, + experience_sampler_cls=experience_sampler_cls, + experience_sampler_kwargs=experience_sampler_kwargs, + exploration=exploration + ) self.q_net = q_net if self.q_net.trainable: self.target_q_net = q_net.copy() @@ -77,9 +87,11 @@ def __init__( else: self.target_q_net = None self.config = config + self._post_step = post_step self.device = self.q_net.device - self._training_counter = 0 - self.prioritized_experience_replay = isinstance(self.experience_manager.sampler, PrioritizedSampler) + self._num_steps = 0 + + self.prioritized_experience_replay = isinstance(self.sampler, PrioritizedSampler) if not self.prioritized_experience_replay: self._loss_func = torch.nn.MSELoss() @@ -101,7 +113,7 @@ def learn(self): self.q_net.train() for _ in range(self.config.train_epochs): # sample from the replay memory - experience_set = self.experience_manager.get() + experience_set = self.sampler.get() states, next_states = experience_set.states, experience_set.next_states actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) @@ -124,14 +136,17 @@ def learn(self): if self.prioritized_experience_replay: td_errors = target_q_values - q_values loss = (td_errors * is_weights).mean() - self.experience_manager.sampler.update(indexes, td_errors.detach().cpu().numpy()) + self.sampler.update(indexes, td_errors.detach().cpu().numpy()) else: loss = self._loss_func(q_values, target_q_values) self.q_net.step(loss) + if self._post_step: + self._post_step(loss.detach().cpu().numpy(), self.tracker) + # soft-update target network - self._training_counter += 1 - if self._training_counter % self.config.target_update_freq == 0: + self._num_steps += 1 + if self._num_steps % self.config.update_target_every == 0: self.target_q_net.soft_update(self.q_net, self.config.soft_update_coefficient) def set_state(self, policy_state): diff --git a/maro/rl/policy/algorithms/pg.py b/maro/rl/policy/algorithms/pg.py index bba4729df..a3f5aef09 100644 --- a/maro/rl/policy/algorithms/pg.py +++ b/maro/rl/policy/algorithms/pg.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Tuple +from typing import Callable, Tuple import numpy as np import torch -from maro.rl.experience.experience_manager import ExperienceManager +from maro.rl.experience import ExperienceStore, UniformSampler from maro.rl.model import DiscretePolicyNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_truncated_cumulative_reward @@ -17,11 +17,14 @@ class PolicyGradientConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. + clear_experience_memory_every (int): Number of ``ActorCritic.learn`` calls between experience memory clearances. + Defaults to 1. """ - __slots__ = ["reward_discount"] + __slots__ = ["reward_discount", "clear_experience_memory_every"] - def __init__(self, reward_discount: float): + def __init__(self, reward_discount: float, clear_experience_memory_every: int = 1): self.reward_discount = reward_discount + self.clear_experience_memory_every = clear_experience_memory_every class PolicyGradient(AbsCorePolicy): @@ -32,21 +35,35 @@ class PolicyGradient(AbsCorePolicy): Args: policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. - experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences - for training. config (PolicyGradientConfig): Configuration for the PG algorithm. + experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences + generated by the policy. + experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to + ``UnifromSampler``. + experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. + post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking + the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ def __init__( self, policy_net: DiscretePolicyNet, - experience_manager: ExperienceManager, config: PolicyGradientConfig, + experience_store: ExperienceStore, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs: dict = {}, + post_step: Callable = None ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__(experience_manager) + super().__init__( + experience_store, + experience_sampler_cls=experience_sampler_cls, + experience_sampler_kwargs=experience_sampler_kwargs + ) self.policy_net = policy_net self.config = config + self._post_step = post_step + self._num_learn_calls = 0 self.device = self.policy_net.device def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: @@ -59,11 +76,11 @@ def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: def learn(self): """ This should be called at the end of a simulation episode and the experiences obtained from - the experience manager's ``get`` method should be a sequential set, i.e., in the order in + the experience store's ``get`` method should be a sequential set, i.e., in the order in which they are generated during the simulation. Otherwise, the return values may be meaningless. """ self.policy_net.train() - experience_set = self.experience_manager.get() + experience_set = self.sampler.get() log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) @@ -71,6 +88,13 @@ def learn(self): loss = -(log_p * returns).mean() self.policy_net.step(loss) + if self._post_step: + self._post_step(loss.detach().cpu().numpy(), self.tracker) + + self._num_learn_calls += 1 + if self._num_learn_calls % self.config.clear_experience_memory_every == 0: + self.experience_store.clear() + def set_state(self, policy_state): self.policy_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 8465434f1..4d2c3ec6b 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -2,8 +2,9 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod +from typing import Callable -from maro.rl.experience import ExperienceManager, ExperienceSet +from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler from maro.rl.exploration import AbsExploration @@ -32,14 +33,25 @@ class AbsCorePolicy(AbsPolicy): Reinforcement learning (RL) policies should inherit from this. Args: - experience_manager (ExperienceManager): An experience manager for storing and retrieving experiences - for training. + experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences + generated by the policy. + experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to + ``UnifromSampler``. + experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. exploration (AbsExploration): Exploration strategy for generating exploratory actions. Defaults to None. """ - def __init__(self, experience_manager: ExperienceManager, exploration: AbsExploration = None): + def __init__( + self, + experience_store: ExperienceStore, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs: dict = {}, + exploration: AbsExploration = None + ): super().__init__() - self.experience_manager = experience_manager + self.experience_store = experience_store + self.sampler = experience_sampler_cls(self.experience_store, **experience_sampler_kwargs) self.exploration = exploration + self.tracker = {} self.exploring = True @abstractmethod @@ -79,7 +91,7 @@ def store(self, exp: ExperienceSet) -> bool: """ Store incoming experiences and update if necessary. """ - self.experience_manager.put(exp) + self.experience_store.put(exp) # print( # f"exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" # ) diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 26705334b..68fdf7e2f 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -30,18 +30,28 @@ class AbsPolicyManager(ABC): warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, + ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to + None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. """ def __init__( self, policy_dict: Dict[str, AbsCorePolicy], update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None + warmup: Dict[str, int] = None, + post_update: Callable = None, + log_dir: str = getcwd() ): for policy in policy_dict.values(): if not isinstance(policy, AbsCorePolicy): raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") super().__init__() + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self.policy_dict = policy_dict if not update_trigger: self.update_trigger = {name: 1 for name in self.policy_dict} @@ -52,7 +62,10 @@ def __init__( else: self.warmup = warmup + self._post_update = post_update + self._update_history = [set(policy_dict.keys())] + self.tracker = {} @property def version(self): @@ -83,19 +96,29 @@ class LocalPolicyManager(AbsPolicyManager): warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time - and this directory will be used to save the log files generated by it. Defaults to the current working - directory. + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, + ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to + None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. """ def __init__( self, policy_dict: Dict[str, AbsCorePolicy], update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, + post_update: Callable = None, log_dir: str = getcwd() ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup) - self._logger = Logger("LOCAL_TRAINING_MANAGER", dump_folder=log_dir) + super().__init__( + policy_dict, + update_trigger=update_trigger, + warmup=warmup, + post_update=post_update, + log_dir=log_dir + ) self._new_exp_counter = defaultdict(int) def update(self, exp_by_policy: Dict[str, ExperienceSet]): @@ -108,11 +131,11 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): updated = set() for policy_name, exp in exp_by_policy.items(): policy = self.policy_dict[policy_name] - policy.experience_manager.put(exp) + policy.experience_store.put(exp) self._new_exp_counter[policy_name] += exp.size if ( self._new_exp_counter[policy_name] >= self.update_trigger[policy_name] and - policy.experience_manager.size >= self.warmup[policy_name] + policy.experience_store.size >= self.warmup[policy_name] ): policy.learn() updated.add(policy_name) @@ -122,6 +145,9 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") + if self._post_update: + self._post_update([policy.tracker for policy in self.policy_dict.values()]) + self._logger.debug(f"policy update time: {time.time() - t0}") @@ -140,8 +166,11 @@ class MultiProcessPolicyManager(AbsPolicyManager): warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers,) + -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( @@ -151,10 +180,16 @@ def __init__( create_policy_func_dict: Dict[str, Callable], update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, + post_update: Callable = None, log_dir: str = getcwd(), ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup) - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) + super().__init__( + policy_dict, + update_trigger=update_trigger, + warmup=warmup, + post_update=post_update, + log_dir=log_dir + ) self._policy2trainer = {} self._trainer2policies = defaultdict(list) self._exp_cache = defaultdict(ExperienceSet) @@ -201,8 +236,10 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): "experiences": {name: exp_to_send[name] for name in self._trainer2policies[trainer_id]} }) + trackers = [] for conn in self._manager_end.values(): result = conn.recv() + trackers.append(result["tracker"]) for policy_name, policy_state in result["policy"].items(): self.policy_dict[policy_name].set_state(policy_state) @@ -210,6 +247,9 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") + if self._post_update: + self._post_update(trackers) + def exit(self): """Tell the trainer processes to exit.""" for conn in self._manager_end.values(): @@ -231,11 +271,14 @@ class MultiNodePolicyManager(AbsPolicyManager): warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers,) + -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. """ def __init__( self, @@ -244,13 +287,19 @@ def __init__( num_trainers: int, update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() + post_update: Callable = None, + log_dir: str = getcwd(), + proxy_kwargs: dict = {} ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup) + super().__init__( + policy_dict, + update_trigger=update_trigger, + warmup=warmup, + post_update=post_update, + log_dir=log_dir + ) peers = {"trainer": num_trainers} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) - self._logger = Logger(self._proxy.name, dump_folder=log_dir) self._policy2trainer = {} self._trainer2policies = defaultdict(list) @@ -288,7 +337,9 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp + trackers = [] for reply in self._proxy.scatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())): + trackers.append(reply.body[MsgKey.TRACKER]) for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): self.policy_dict[policy_name].set_state(policy_state) @@ -296,6 +347,9 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") + if self._post_update: + self._post_update(trackers) + def exit(self): """Tell the remote trainers to exit.""" self._proxy.ibroadcast("trainer", MsgTag.EXIT, SessionType.NOTIFICATION) diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index 70eb19dc5..4b025dfaa 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -42,7 +42,10 @@ def trainer_process( policy_dict[name].store(exp) policy_dict[name].learn() logger.debug(f"total policy update time: {time.time() - t0}") - conn.send({"policy": {name: policy_dict[name].get_state() for name in msg["experiences"]}}) + conn.send({ + "policy": {name: policy_dict[name].get_state() for name in msg["experiences"]}, + "tracker": {name: policy_dict[name].tracker for name in msg["experiences"]} + }) elif msg["type"] == "quit": break @@ -90,7 +93,8 @@ def trainer_node( policy_dict[name].learn() msg_body = { - MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.EXPERIENCES]} + MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.EXPERIENCES]}, + MsgKey.TRACKER: {name: policy_dict[name].tracker for name in msg.body[MsgKey.EXPERIENCES]} } logger.debug(f"total policy update time: {time.time() - t0}") proxy.reply(msg, body=msg_body) diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 6e6c3b076..4a76fd279 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -27,11 +27,11 @@ class MsgKey(Enum): EPISODE = "episode" SEGMENT = "segment" STEP = "step" - ENV_SUMMARY = "env_summary" EXPERIENCES = "experiences" - NUM_EXPERIENCES = "num_experiences" + TRACKER = "tracker" STATE = "state" POLICY_STATE = "policy_state" EXPLORATION_STEP = "exploration_step" VERSION = "version" NUM_STEPS = "num_steps" + END_OF_EPISODE = "end_of_episode" diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 349f91334..63cae9f14 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -153,7 +153,7 @@ "source": [ "import torch\n", "\n", - "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceManager, FullyConnectedBlock, OptimOption\n", + "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceStore, FullyConnectedBlock, OptimOption\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", @@ -191,7 +191,7 @@ " \"critic\": OptimOption(optim_cls=\"rmsprop\", optim_params={\"lr\": 0.001})\n", " }\n", " },\n", - " \"experience_manager\": {\n", + " \"experience_store\": {\n", " \"capacity\": 10000\n", " },\n", " \"algorithm_config\": {\n", @@ -221,8 +221,8 @@ " actor = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"actor\"])\n", " critic = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"critic\"])\n", " ac_net = MyACNet({\"actor\": actor, \"critic\": critic}, optim_option=policy_config[\"model\"][\"optimization\"])\n", - " experience_manager = ExperienceManager(policy_config[\"experience_manager\"][\"capacity\"])\n", - " return ActorCritic(name, ac_net, experience_manager, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" + " experience_store = ExperienceStore(policy_config[\"experience_store\"][\"capacity\"])\n", + " return ActorCritic(name, ac_net, experience_store, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" ] }, { From f0a29ef9dca151cea4964092681b148aa1d974fc Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 14 Jul 2021 15:34:43 +0000 Subject: [PATCH 379/482] fixed lint issues --- maro/rl/experience/__init__.py | 4 ++-- maro/rl/learning/env_wrapper.py | 14 +++++++------- maro/rl/learning/synchronous/rollout_manager.py | 2 +- maro/rl/policy/policy.py | 1 - 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 6952fb274..369546dd5 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .experience_store import ExperienceStore, ExperienceSet +from .experience_store import ExperienceSet, ExperienceStore from .sampler import AbsSampler, PrioritizedSampler, UniformSampler -__all__ = ["AbsSampler", "ExperienceStore", "ExperienceSet", "PrioritizedSampler", "UniformSampler"] +__all__ = ["AbsSampler", "ExperienceSet", "ExperienceStore", "PrioritizedSampler", "UniformSampler"] diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index 4e1d4570e..4ae8e3dba 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -14,7 +14,7 @@ class Transition: Args: state: Output of the environment wrapper's ``get_state``. - action: Output of the ``AgentWrapper`` that interacts with environment wrapper. + action: Output of the ``AgentWrapper`` that interacts with environment wrapper. env_action: Output of the environment wrapper's ``to_env_action``. reward: Output of the environmet wrapper's ``get_reward``. next_state: The state immediately following ``state``. @@ -46,12 +46,12 @@ class AbsEnvWrapper(ABC): get_experience_func (Callable): Custom function to convert the replay buffer to training experiences. Defaults to None, in which case the replay buffer will be converted directly to SARS experiences for each agent. get_experience_func_kwargs (dict): Keyword arguments for the user-defined ``get_experience_func``. Defaults to - an empty dictionary. + an empty dictionary. step_callback (Callable): Custom function to gather information about a transition and the evolvement of the environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition is a ``Transition`` object. For example, this callback can be used to collect various statistics on the - simulation. Defaults to None. + simulation. Defaults to None. """ def __init__( self, @@ -106,7 +106,7 @@ def collect(self): def evaluate(self): self._replay = False - + def start(self): """Generate the initial environmental state at the beginning of a simulation episode.""" self._step_index = 0 @@ -169,7 +169,7 @@ def step(self, action_by_agent: dict): """ self._step_index += 1 env_action = self.to_env_action(action_by_agent) - + self._transition_cache.append(( self._state, action_by_agent, @@ -220,12 +220,12 @@ def get_experiences(self): buf["rewards"][:-1], buf["states"][1:], buf["info"][:-1], - ) for agent_id, buf in self._replay_buffer.items() + ) for agent_id, buf in self._replay_buffer.items() } else: exp_by_agent = self._get_experience_func(self._replay_buffer, **self._get_experience_func_kwargs) - # clear the replay buffer of transitions that have already been converted to experiences. + # clear the replay buffer of transitions that have already been converted to experiences. for buf in self._replay_buffer.values(): del buf["states"][:-1] del buf["actions"][:-1] diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 42945a4c1..959693aae 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -548,7 +548,7 @@ def evaluate(self, ep: int, policy_state_dict: dict): num_finishes += 1 if num_finishes == self._num_eval_workers: break - + if self._post_evaluate: self._post_evaluate(trackers) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 4d2c3ec6b..31097563f 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from typing import Callable from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler from maro.rl.exploration import AbsExploration From 56fd2d68155b0ea8e81ed42f7485c5c22ed4e715 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 14 Jul 2021 15:38:07 +0000 Subject: [PATCH 380/482] fixed lint issues --- maro/rl/experience/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index bf117a272..7c569e6f3 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -6,7 +6,7 @@ import numpy as np -from .experience_store import ExperienceStore, ExperienceSet +from .experience_store import ExperienceSet, ExperienceStore class AbsSampler(ABC): From d2d66cd7ad968a866890278f9e9c43e541101e04 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 15 Jul 2021 04:43:05 +0000 Subject: [PATCH 381/482] moved instantiation of policy manager inside simple learner --- examples/rl/cim/ac.py | 6 ++-- examples/rl/cim/callbacks.py | 29 +++++++++++++++++++ examples/rl/cim/dqn.py | 6 ++-- examples/rl/workflows/agent_wrapper.py | 21 +++++--------- examples/rl/workflows/simple_learner.py | 9 ++++-- .../workflows/synchronous/rollout_worker.py | 2 +- maro/rl/learning/agent_wrapper.py | 21 +++++++------- maro/rl/learning/simple_learner.py | 27 +++++++++++++++-- maro/rl/policy/policy.py | 2 +- 9 files changed, 87 insertions(+), 36 deletions(-) create mode 100644 examples/rl/cim/callbacks.py diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 949a6dffc..cee6b4127 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -92,14 +92,14 @@ def forward(self, states, actor: bool = True, critic: bool = True): ) if mode == "update": - exp_manager = ExperienceStore(**config["experience_store"]["update"]) + exp_store = ExperienceStore(**config["experience_store"]["update"]) experience_sampler_kwargs = config["sampler"]["update"] else: - exp_manager = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) + exp_store = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] return ActorCritic( - ac_net, ActorCriticConfig(**config["algorithm"]), exp_manager, + ac_net, ActorCriticConfig(**config["algorithm"]), exp_store, experience_sampler_cls=UniformSampler, experience_sampler_kwargs=experience_sampler_kwargs ) diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py new file mode 100644 index 000000000..7312e8a71 --- /dev/null +++ b/examples/rl/cim/callbacks.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time + +from os import makedirs +from os.path import dirname, join, realpath + +from maro.utils import Logger + +log_dir = join(dirname(realpath(__file__)), "log", str(time.time())) +makedirs(log_dir, exist_ok=True) + +simulation_logger = Logger("SIMUALTION", dump_folder=log_dir) + +def post_episode_callback(trackers): + # print the env metric from each rollout worker + for tracker in trackers: + simulation_logger.info(f"env metric: {tracker['env_metric']}") + + # print the average env metric + if len(trackers) > 1: + metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) + avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} + simulation_logger.info(f"average env metric: {avg_metric}") + + +post_collect = post_episode_callback +post_evaluate = post_episode_callback diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index c55eaf357..e86fb006a 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -84,7 +84,7 @@ def get_dqn_policy(mode="update"): optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None ) if mode == "update": - exp_manager = ExperienceStore(**config["experience_store"]["update"]) + exp_store = ExperienceStore(**config["experience_store"]["update"]) exploration = None experience_sampler_kwargs = config["sampler"]["update"] else: @@ -94,11 +94,11 @@ def get_dqn_policy(mode="update"): param_name="epsilon", **config["exploration"] ) - exp_manager = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) + exp_store = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] return DQN( - qnet, DQNConfig(**config["algorithm"]), exp_manager, + qnet, DQNConfig(**config["algorithm"]), exp_store, experience_sampler_cls=UniformSampler, experience_sampler_kwargs=experience_sampler_kwargs, exploration=exploration diff --git a/examples/rl/workflows/agent_wrapper.py b/examples/rl/workflows/agent_wrapper.py index d98981387..45cb77fa1 100644 --- a/examples/rl/workflows/agent_wrapper.py +++ b/examples/rl/workflows/agent_wrapper.py @@ -5,27 +5,20 @@ from os.path import dirname, realpath from maro.rl.learning import AgentWrapper -from maro.rl.policy import LocalPolicyManager workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import agent2policy, log_dir, non_rl_policy_func_index, rl_policy_func_index, update_trigger, warmup +from general import agent2policy, non_rl_policy_func_index, rl_policy_func_index -def get_agent_wrapper(mode: str = "inference-update"): - assert mode in {"inference", "inference-update"}, f"mode must be 'inference' or 'inference-update', got {mode}" - policy_dict = { - **{name: func() for name, func in non_rl_policy_func_index.items()}, - **{name: func(mode=mode) for name, func in rl_policy_func_index.items()} - } +def get_agent_wrapper(local_update: bool = False): + policy_mode = "inference-update" if local_update else "inference" return AgentWrapper( - LocalPolicyManager( - policy_dict, - update_trigger=update_trigger, - warmup=warmup, - log_dir=log_dir - ), + { + **{name: func() for name, func in non_rl_policy_func_index.items()}, + **{name: func(mode=policy_mode) for name, func in rl_policy_func_index.items()} + }, agent2policy ) diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index 5c48f4469..50148ac01 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -12,17 +12,22 @@ sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import config, get_env_wrapper, log_dir, post_collect, post_evaluate +from general import ( + config, get_env_wrapper, log_dir, post_collect, post_evaluate, post_update, update_trigger, warmup +) if __name__ == "__main__": SimpleLearner( get_env_wrapper(), - get_agent_wrapper(), + get_agent_wrapper(local_update=True), num_episodes=config["num_episodes"], num_steps=config["num_steps"], eval_schedule=config["eval_schedule"], + update_trigger=update_trigger, + warmup=warmup, post_collect=post_collect, post_evaluate=post_evaluate, + post_update=post_update, log_dir=log_dir ).run() diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index dfd4d5cbf..6ce7fc59f 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -22,7 +22,7 @@ config["sync"]["rollout_group"], worker_id, get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), - get_agent_wrapper(mode="inference"), + get_agent_wrapper(), proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index 752e87b0d..779ef3309 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -3,7 +3,7 @@ from typing import Dict -from maro.rl.policy import LocalPolicyManager +from maro.rl.policy import AbsPolicy from .env_wrapper import AbsEnvWrapper @@ -12,15 +12,16 @@ class AgentWrapper: """Multi-agent wrapper that interacts with an ``EnvWrapper`` with a unified inferface. Args: - policy_manager (LocalPolicyManager): ``LocalPolicyManager`` instance. + policy_dict (Dict[str, AbsPolicy]): Policies used by the agents to make decision when interacting with + the environment. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. """ - def __init__(self, policy_manager: LocalPolicyManager, agent2policy: Dict[str, str]): - self.policy_manager = policy_manager + def __init__(self, policy_dict: Dict[str, AbsPolicy], agent2policy: Dict[str, str]): + self.policy_dict = policy_dict self.agent2policy = agent2policy self.policy = { - agent_id: self.policy_manager.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() + agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() } def choose_action(self, state: dict) -> dict: @@ -40,24 +41,24 @@ def get_batch(self, env: AbsEnvWrapper): self.policy[agent_id].store(exp) names.add(self.agent2policy[agent_id]) - return {name: self.policy_manager.policy_dict[name].sampler.get() for name in names} + return {name: self.policy_dict[name].sampler.get() for name in names} def set_policy_states(self, policy_state_dict: dict): """Update policy states.""" for policy_id, policy_state in policy_state_dict.items(): - self.policy_manager.policy_dict[policy_id].set_state(policy_state) + self.policy_dict[policy_id].set_state(policy_state) def exploration_step(self): - for policy in self.policy_manager.policy_dict.values(): + for policy in self.policy_dict.values(): if hasattr(policy, "exploration_step"): policy.exploration_step() def exploit(self): - for policy in self.policy_manager.policy_dict.values(): + for policy in self.policy_dict.values(): if hasattr(policy, "exploit"): policy.exploit() def explore(self): - for policy in self.policy_manager.policy_dict.values(): + for policy in self.policy_dict.values(): if hasattr(policy, "explore"): policy.explore() diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py index 6c2e72262..58cf97308 100644 --- a/maro/rl/learning/simple_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -3,8 +3,9 @@ import time from os import getcwd -from typing import Callable, List, Union +from typing import Callable, Dict, List, Union +from maro.rl.policy import AbsCorePolicy, LocalPolicyManager from maro.utils import Logger from .agent_wrapper import AgentWrapper @@ -32,6 +33,12 @@ class SimpleLearner: as the evaluation environment. Defaults to None. early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the environment metrics are met following an evaluation episode. Default to None. + update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the + required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, + all triggers will be set to 1. + warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the + minimum number of experiences in the experience memory required to trigger a call to ``learn`` for + each policy. Defaults to None, in which case all warm-up sizes will be set to 1. post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults @@ -40,6 +47,10 @@ class SimpleLearner: environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, + ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to + None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -54,8 +65,11 @@ def __init__( eval_schedule: Union[int, List[int]] = None, eval_env: AbsEnvWrapper = None, early_stopper: AbsEarlyStopper = None, + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None, post_collect: Callable = None, post_evaluate: Callable = None, + post_update: Callable = None, log_dir: str = getcwd(), ): if num_steps == 0 or num_steps < -1: @@ -66,6 +80,14 @@ def __init__( self.eval_env = eval_env if eval_env else self.env self.agent = agent_wrapper + # Create a policy manager to manage all trainable policies from the agent wrapper + self.policy_manager = LocalPolicyManager( + {name: policy for name, policy in self.agent.policy_dict.items() if isinstance(policy, AbsCorePolicy)}, + update_trigger=update_trigger, + warmup=warmup, + post_update=post_update + ) + self.num_episodes = num_episodes self._num_steps = num_steps if num_steps > 0 else float("inf") @@ -89,6 +111,7 @@ def __init__( self.early_stopper = early_stopper self._post_collect = post_collect self._post_evaluate = post_evaluate + self._post_update = post_update def run(self): """Entry point for executing a learning workflow.""" @@ -116,7 +139,7 @@ def _collect_and_update(self, ep: int): segment += 1 self._collect(ep, segment) exp_by_policy = self.agent.get_batch(self.env) - self.agent.policy_manager.update(exp_by_policy) + self.policy_manager.update(exp_by_policy) num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) # update the exploration parameters if an episode is finished self.agent.exploration_step() diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 31097563f..d368ce4ed 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -92,7 +92,7 @@ def store(self, exp: ExperienceSet) -> bool: """ self.experience_store.put(exp) # print( - # f"exp mem size = {self.experience_manager.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" + # f"exp mem size = {self.experience_store.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" # ) def exploit(self): From 07fba7a6b1dcfb95c51592322a629d9869503431 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 15 Jul 2021 05:58:53 +0000 Subject: [PATCH 382/482] fixed env_wrapper get_reward signature --- examples/rl/supply_chain/env_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rl/supply_chain/env_wrapper.py b/examples/rl/supply_chain/env_wrapper.py index 38b52ae04..f288eef1d 100644 --- a/examples/rl/supply_chain/env_wrapper.py +++ b/examples/rl/supply_chain/env_wrapper.py @@ -309,7 +309,7 @@ def get_state(self, tick=None): return final_state - def get_reward(self, tick=None): + def get_reward(self, actions, tick=None): # get related product, seller, consumer, manufacture unit id # NOTE: this mapping does not contain facility id, so if id is not exist, then means it is a facility # product_unit_id, facility_id, seller_id, consumer_id, producer_id = self._unit2product_mapping[id] From a9e6b113a933195b99f33adca2f45e43fdc84269 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 15 Jul 2021 06:04:25 +0000 Subject: [PATCH 383/482] minor edits --- examples/rl/cim/dqn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index e86fb006a..952abc002 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -86,7 +86,7 @@ def get_dqn_policy(mode="update"): if mode == "update": exp_store = ExperienceStore(**config["experience_store"]["update"]) exploration = None - experience_sampler_kwargs = config["sampler"]["update"] + exp_sampler_kwargs = config["sampler"]["update"] else: exploration = EpsilonGreedyExploration() exploration.register_schedule( @@ -95,11 +95,11 @@ def get_dqn_policy(mode="update"): **config["exploration"] ) exp_store = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) - experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] + exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] return DQN( qnet, DQNConfig(**config["algorithm"]), exp_store, experience_sampler_cls=UniformSampler, - experience_sampler_kwargs=experience_sampler_kwargs, + experience_sampler_kwargs=exp_sampler_kwargs, exploration=exploration ) From 8a84b110280cd9c9e2b1e8fc352c883295e8cb74 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 15 Jul 2021 06:14:14 +0000 Subject: [PATCH 384/482] removed get_eperience kwargs from env_wrapper --- maro/rl/learning/env_wrapper.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index 4ae8e3dba..0a30ce891 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -45,8 +45,6 @@ class AbsEnvWrapper(ABC): to None. get_experience_func (Callable): Custom function to convert the replay buffer to training experiences. Defaults to None, in which case the replay buffer will be converted directly to SARS experiences for each agent. - get_experience_func_kwargs (dict): Keyword arguments for the user-defined ``get_experience_func``. Defaults to - an empty dictionary. step_callback (Callable): Custom function to gather information about a transition and the evolvement of the environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition @@ -59,14 +57,12 @@ def __init__( reward_eval_delay: int = 0, replay_agent_ids: list = None, get_experience_func: Callable = None, - get_experience_func_kwargs: dict = {}, step_callback: Callable = None ): self.env = env self.reward_eval_delay = reward_eval_delay self._get_experience_func = get_experience_func - self._get_experience_func_kwargs = get_experience_func_kwargs self._step_callback = step_callback replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids @@ -223,7 +219,7 @@ def get_experiences(self): ) for agent_id, buf in self._replay_buffer.items() } else: - exp_by_agent = self._get_experience_func(self._replay_buffer, **self._get_experience_func_kwargs) + exp_by_agent = self._get_experience_func(self._replay_buffer) # clear the replay buffer of transitions that have already been converted to experiences. for buf in self._replay_buffer.values(): From ec338fb840496fe281ce2fe7d127c728953413b0 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 15 Jul 2021 07:08:41 +0000 Subject: [PATCH 385/482] 1. renamed step_callback to post_step in env_wrapper; 2. added get_eval_env_func to RL workflows --- examples/rl/cim/callbacks.py | 7 +++---- examples/rl/cim/env_wrapper.py | 4 ++-- examples/rl/workflows/asynchronous/actor.py | 4 +++- examples/rl/workflows/general.py | 1 + examples/rl/workflows/simple_learner.py | 4 +++- examples/rl/workflows/synchronous/rollout_worker.py | 3 ++- maro/rl/learning/env_wrapper.py | 10 +++++----- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py index 7312e8a71..33859d31a 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/rl/cim/callbacks.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. import time - from os import makedirs from os.path import dirname, join, realpath @@ -13,7 +12,7 @@ simulation_logger = Logger("SIMUALTION", dump_folder=log_dir) -def post_episode_callback(trackers): +def post_episode(trackers): # print the env metric from each rollout worker for tracker in trackers: simulation_logger.info(f"env metric: {tracker['env_metric']}") @@ -25,5 +24,5 @@ def post_episode_callback(trackers): simulation_logger.info(f"average env metric: {avg_metric}") -post_collect = post_episode_callback -post_evaluate = post_episode_callback +post_collect = post_episode +post_evaluate = post_episode diff --git a/examples/rl/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py index f9ad93f6f..1245ac23d 100644 --- a/examples/rl/cim/env_wrapper.py +++ b/examples/rl/cim/env_wrapper.py @@ -8,7 +8,7 @@ from maro.simulator.scenarios.cim.common import Action, ActionType -def step_callback(env, tracker, transition): +def post_step(env, tracker, transition): tracker["env_metric"] = env.metrics @@ -22,7 +22,7 @@ def __init__( env, replay_agent_ids=replay_agent_ids, reward_eval_delay=reward_eval_delay, - step_callback=step_callback + post_step=post_step ) self.port_attributes = port_attributes self.vessel_attributes = vessel_attributes diff --git a/examples/rl/workflows/asynchronous/actor.py b/examples/rl/workflows/asynchronous/actor.py index 1237f72a9..ecbe28492 100644 --- a/examples/rl/workflows/asynchronous/actor.py +++ b/examples/rl/workflows/asynchronous/actor.py @@ -12,7 +12,7 @@ sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import config, get_env_wrapper, log_dir, replay_agents +from general import config, get_env_wrapper, get_eval_env_wrapper, log_dir, replay_agents if __name__ == "__main__": @@ -24,6 +24,8 @@ get_agent_wrapper(), config["num_episodes"], num_steps=config["num_steps"], + eval_env_wrapper=get_eval_env_wrapper(), + eval_schedule=config["eval_schedule"], proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir, ) diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 9b937fedb..74805d653 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -22,6 +22,7 @@ agent2policy = getattr(module, "agent2policy") get_env_wrapper = getattr(module, "get_env_wrapper") +get_eval_env_wrapper = getattr(module, "get_eval_env_wrapper", lambda: None) non_rl_policy_func_index = getattr(module, "non_rl_policy_func_index", {}) rl_policy_func_index = getattr(module, "rl_policy_func_index") update_trigger = getattr(module, "update_trigger") diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index 50148ac01..e965ef11d 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -13,7 +13,8 @@ from agent_wrapper import get_agent_wrapper from general import ( - config, get_env_wrapper, log_dir, post_collect, post_evaluate, post_update, update_trigger, warmup + config, get_env_wrapper, get_eval_env_wrapper, log_dir, post_collect, post_evaluate, post_update, update_trigger, + warmup ) @@ -23,6 +24,7 @@ get_agent_wrapper(local_update=True), num_episodes=config["num_episodes"], num_steps=config["num_steps"], + eval_env=get_eval_env_wrapper(), eval_schedule=config["eval_schedule"], update_trigger=update_trigger, warmup=warmup, diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 6ce7fc59f..2666fb140 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -13,7 +13,7 @@ sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import config, get_env_wrapper, log_dir, replay_agents +from general import config, get_env_wrapper, get_eval_env_wrapper, log_dir, replay_agents if __name__ == "__main__": @@ -23,6 +23,7 @@ worker_id, get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), get_agent_wrapper(), + eval_env_wrapper=get_eval_env_wrapper(), proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, log_dir=log_dir ) diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index 0a30ce891..8555d33c1 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -45,7 +45,7 @@ class AbsEnvWrapper(ABC): to None. get_experience_func (Callable): Custom function to convert the replay buffer to training experiences. Defaults to None, in which case the replay buffer will be converted directly to SARS experiences for each agent. - step_callback (Callable): Custom function to gather information about a transition and the evolvement of the + post_step (Callable): Custom function to gather information about a transition and the evolvement of the environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition is a ``Transition`` object. For example, this callback can be used to collect various statistics on the @@ -57,13 +57,13 @@ def __init__( reward_eval_delay: int = 0, replay_agent_ids: list = None, get_experience_func: Callable = None, - step_callback: Callable = None + post_step: Callable = None ): self.env = env self.reward_eval_delay = reward_eval_delay self._get_experience_func = get_experience_func - self._step_callback = step_callback + self._post_step = post_step replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids self._replay_buffer = {agent_id: defaultdict(list) for agent_id in replay_agent_ids} @@ -191,11 +191,11 @@ def step(self, action_by_agent: dict): ): state, action, env_action, info, tick = self._transition_cache.popleft() reward = self.get_reward(env_action, tick=tick) - if self._step_callback: + if self._post_step: next_state = self._transition_cache[0][0] if self._transition_cache else None transition = Transition(state, action, env_action, reward, next_state, info) # put things you want to track in the tracker attribute - self._step_callback(self.env, self.tracker, transition) + self._post_step(self.env, self.tracker, transition) if self._replay: for agent_id, agent_state in state.items(): From 1c94b62737db2c2496ee84dc848f34ce2069a223 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 15 Jul 2021 09:06:34 +0000 Subject: [PATCH 386/482] added rollout exp disribution option in RL examples --- examples/rl/cim/callbacks.py | 18 +++++++--- examples/rl/cim/policy_index.py | 3 +- examples/rl/workflows/config.yml | 8 +++-- examples/rl/workflows/general.py | 15 +++++--- examples/rl/workflows/synchronous/learner.py | 1 + maro/rl/learning/asynchronous/actor.py | 3 -- maro/rl/learning/simple_learner.py | 28 +++++++-------- maro/rl/learning/synchronous/learner.py | 2 -- .../learning/synchronous/rollout_manager.py | 36 +++++++++---------- 9 files changed, 63 insertions(+), 51 deletions(-) diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py index 33859d31a..5e91892d7 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/rl/cim/callbacks.py @@ -12,17 +12,25 @@ simulation_logger = Logger("SIMUALTION", dump_folder=log_dir) -def post_episode(trackers): +def post_collect(trackers, ep, segment): # print the env metric from each rollout worker for tracker in trackers: - simulation_logger.info(f"env metric: {tracker['env_metric']}") + simulation_logger.info(f"env summary (episode {ep}, segement {segment}): {tracker['env_metric']}") # print the average env metric if len(trackers) > 1: metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} - simulation_logger.info(f"average env metric: {avg_metric}") + simulation_logger.info(f"average env summary (episode {ep}, segement {segment}): {avg_metric}") -post_collect = post_episode -post_evaluate = post_episode +def post_evaluate(trackers, ep): + # print the env metric from each rollout worker + for tracker in trackers: + simulation_logger.info(f"env summary (evaluation episode {ep}): {tracker['env_metric']}") + + # print the average env metric + if len(trackers) > 1: + metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) + avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} + simulation_logger.info(f"average env summary (evaluation episode {ep}): {avg_metric}") diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index 2bc657351..a3ec1c1d0 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -8,7 +8,6 @@ cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) -from ac import get_ac_policy from dqn import get_dqn_policy from env_wrapper import AGENT_IDS @@ -16,5 +15,5 @@ warmup = {name: 1 for name in AGENT_IDS} # use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {name: get_ac_policy for name in AGENT_IDS} +rl_policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index a59531f54..f4a2d2d03 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -8,11 +8,15 @@ num_episodes: 5 eval_schedule: 5 num_steps: -1 max_lag: 0 +# If true, the roll-out experiences will be distributed amongst roll-out workers / actors +# in round-robin fashion based on agent index. Otherwise, every roll-out worker / actor will +# store the roll-out experiences for all agents whose experiences need to be stored. +rollout_experience_distribution: false sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 3 - min_finished_workers: 2 + num_rollout_workers: 4 + min_finished_workers: 4 # max_extra_recv_tries: 3 extra_recv_timeout: 100 async: diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 74805d653..310543732 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -20,18 +20,23 @@ module = importlib.import_module(f"{config['scenario']}") -agent2policy = getattr(module, "agent2policy") get_env_wrapper = getattr(module, "get_env_wrapper") get_eval_env_wrapper = getattr(module, "get_eval_env_wrapper", lambda: None) non_rl_policy_func_index = getattr(module, "non_rl_policy_func_index", {}) rl_policy_func_index = getattr(module, "rl_policy_func_index") +agent2policy = getattr(module, "agent2policy") +rl_agents = [agent_id for agent_id, policy_id in agent2policy.items() if policy_id in rl_policy_func_index] update_trigger = getattr(module, "update_trigger") warmup = getattr(module, "warmup") post_collect = getattr(module, "post_collect", None) -post_evaluate = getattr(module, "end_of_evaluate", None) +post_evaluate = getattr(module, "post_evaluate", None) post_update = getattr(module, "post_update", None) +# roll-out experience distribution amongst workers num_rollouts = config["sync"]["num_rollout_workers"] if config["mode"] == "sync" else config["async"]["num_actors"] -replay_agents = [[] for _ in range(num_rollouts)] -for i, agent in enumerate(list(agent2policy.keys())): - replay_agents[i % num_rollouts].append(agent) +if config["rollout_experience_distribution"]: + replay_agents = [[] for _ in range(num_rollouts)] + for i, agent in enumerate(rl_agents): + replay_agents[i % num_rollouts].append(agent) +else: + replay_agents = [rl_agents] * num_rollouts diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py index 14820e684..7cc14583d 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -48,6 +48,7 @@ def get_rollout_manager(): # max_extra_recv_tries=config["sync"]["max_extra_recv_tries"], extra_recv_timeout=config["sync"]["extra_recv_timeout"], post_collect=post_collect, + post_evaluate=post_evaluate, proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} ) diff --git a/maro/rl/learning/asynchronous/actor.py b/maro/rl/learning/asynchronous/actor.py index b3b4f2b07..d707ff7a3 100644 --- a/maro/rl/learning/asynchronous/actor.py +++ b/maro/rl/learning/asynchronous/actor.py @@ -142,9 +142,6 @@ def actor( action = agent_wrapper.choose_action(eval_env_wrapper.state) eval_env_wrapper.step(action) - # performance details - logger.info(f"Evaluation result: {eval_env_wrapper.summary}") - # tell the policy server I'm all done. proxy.isend(SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION)) proxy.close() diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py index 58cf97308..389e42851 100644 --- a/maro/rl/learning/simple_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -119,7 +119,7 @@ def run(self): self._collect_and_update(ep) if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 - self._evaluate() + self._evaluate(self._eval_point_index) # early stopping check if self.early_stopper: self.early_stopper.push(self.eval_env.summary) @@ -151,18 +151,6 @@ def _collect_and_update(self, ep: int): f"experiences collected: {num_experiences_collected}" ) - def _evaluate(self): - """Policy evaluation.""" - self._logger.info("Evaluating...") - self.agent.exploit() - self.eval_env.reset() - self.eval_env.start() # get initial state - while self.eval_env.state: - self.eval_env.step(self.agent.choose_action(self.eval_env.state)) - - if self._post_evaluate: - self._post_evaluate([self.env.tracker]) - def _collect(self, ep, segment): start_step_index = self.env.step_index + 1 steps_to_go = self._num_steps @@ -176,4 +164,16 @@ def _collect(self, ep, segment): ) if self._post_collect: - self._post_collect([self.env.tracker]) + self._post_collect([self.env.tracker], ep, segment) + + def _evaluate(self, ep: int): + """Policy evaluation.""" + self._logger.info("Evaluating...") + self.agent.exploit() + self.eval_env.reset() + self.eval_env.start() # get initial state + while self.eval_env.state: + self.eval_env.step(self.agent.choose_action(self.eval_env.state)) + + if self._post_evaluate: + self._post_evaluate([self.env.tracker], ep) diff --git a/maro/rl/learning/synchronous/learner.py b/maro/rl/learning/synchronous/learner.py index c1b4e6fba..87e496ac0 100644 --- a/maro/rl/learning/synchronous/learner.py +++ b/maro/rl/learning/synchronous/learner.py @@ -77,8 +77,6 @@ def run(self): if ep == self._eval_schedule[self._eval_point_index]: self._eval_point_index += 1 env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) - # performance details - self.logger.info(f"Evaluation result: {env_metric_dict}") # early stopping check if self.early_stopper: for env_metric in env_metric_dict.values(): diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 959693aae..15e813002 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -24,11 +24,11 @@ class AbsRolloutManager(ABC): Args: post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. + be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. + Defaults to None. post_evaluate (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current @@ -91,11 +91,11 @@ class LocalRolloutManager(AbsRolloutManager): be used as the evaluation environment. Defaults to None. post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. + be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. + Defaults to None. post_evaluate (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current @@ -169,7 +169,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): self.episode_complete = True if self._post_collect: - self._post_collect([self.env.tracker]) + self._post_collect([self.env.tracker], ep, segment) return self.env.get_experiences() @@ -194,7 +194,7 @@ def evaluate(self, ep: int, policy_state_dict: dict): self.eval_env.step(action) if self._post_evaluate: - self._post_evaluate([self.eval_env.tracker]) + self._post_evaluate([self.eval_env.tracker], ep) return self.eval_env.tracker @@ -219,11 +219,11 @@ class MultiProcessRolloutManager(AbsRolloutManager): num_eval_workers (int): Number of workers for evaluation. Defaults to 1. post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. + be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. + Defaults to None. post_evaluate (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current @@ -316,7 +316,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): self._exploration_step = True if self._post_collect: - self._post_collect(trackers) + self._post_collect(trackers, ep, segment) return combined_exp_by_policy @@ -340,7 +340,7 @@ def evaluate(self, ep: int, policy_state_dict: dict): trackers.append(result["tracker"]) if self._post_evaluate: - self._post_evaluate(trackers) + self._post_evaluate(trackers, ep) return trackers @@ -372,11 +372,11 @@ class MultiNodeRolloutManager(AbsRolloutManager): num_eval_workers (int): Number of workers for evaluation. Defaults to 1. post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. + be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. + Defaults to None. post_evaluate (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. @@ -488,7 +488,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): self._exploration_step = True if self._post_collect: - self._post_collect(trackers) + self._post_collect(trackers, ep, segment) return combined_exp_by_policy @@ -550,7 +550,7 @@ def evaluate(self, ep: int, policy_state_dict: dict): break if self._post_evaluate: - self._post_evaluate(trackers) + self._post_evaluate(trackers, ep) return trackers From 4252a0c1ad3afbc000cc7c887aee76725a765565 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 16 Jul 2021 08:32:05 +0000 Subject: [PATCH 387/482] removed unwanted files --- docs/source/key_components/rl_toolkit.rst | 14 +- examples/supply_chain/README.md | 8 - examples/supply_chain/__init__.py | 2 - examples/supply_chain/config.yml | 95 - examples/supply_chain/env_wrapper-2.py | 1187 - examples/supply_chain/env_wrapper.py | 1086 - .../supply_chain/evaluation_with_render.py | 81 - examples/supply_chain/exploration.py | 24 - examples/supply_chain/main.py | 153 - .../supply_chain/or_policy/base_policy.py | 51 - examples/supply_chain/or_policy/eoq_policy.py | 53 - .../supply_chain/or_policy/minmax_policy.py | 44 - examples/supply_chain/policies.py | 67 - examples/supply_chain/render_tools.py | 182 - examples/supply_chain/sc_state_in_maro.md | 312 - examples/supply_chain/scripts/build.sh | 8 - .../scripts/docker_compose_yml_generator.py | 49 - examples/supply_chain/scripts/kill.sh | 6 - examples/supply_chain/scripts/run.sh | 7 - .../supply_chain/topologies/random/config.yml | 29344 ---------------- .../topologies/sample1/config.yml | 335 - .../supply_chain/topologies/test/config.yml | 367 - maro/rl/env_wrapper.py | 152 - maro/rl/experience/experience.py | 43 - maro/rl/training/actor_manager.py | 144 - maro/rl/training/distributed_learner.py | 188 - maro/rl/training/policy_update_schedule.py | 58 - 27 files changed, 4 insertions(+), 34056 deletions(-) delete mode 100644 examples/supply_chain/README.md delete mode 100644 examples/supply_chain/__init__.py delete mode 100644 examples/supply_chain/config.yml delete mode 100644 examples/supply_chain/env_wrapper-2.py delete mode 100644 examples/supply_chain/env_wrapper.py delete mode 100644 examples/supply_chain/evaluation_with_render.py delete mode 100644 examples/supply_chain/exploration.py delete mode 100644 examples/supply_chain/main.py delete mode 100644 examples/supply_chain/or_policy/base_policy.py delete mode 100644 examples/supply_chain/or_policy/eoq_policy.py delete mode 100644 examples/supply_chain/or_policy/minmax_policy.py delete mode 100644 examples/supply_chain/policies.py delete mode 100644 examples/supply_chain/render_tools.py delete mode 100644 examples/supply_chain/sc_state_in_maro.md delete mode 100644 examples/supply_chain/scripts/build.sh delete mode 100644 examples/supply_chain/scripts/docker_compose_yml_generator.py delete mode 100644 examples/supply_chain/scripts/kill.sh delete mode 100644 examples/supply_chain/scripts/run.sh delete mode 100644 examples/supply_chain/topologies/random/config.yml delete mode 100644 examples/supply_chain/topologies/sample1/config.yml delete mode 100644 examples/supply_chain/topologies/test/config.yml delete mode 100644 maro/rl/env_wrapper.py delete mode 100644 maro/rl/experience/experience.py delete mode 100644 maro/rl/training/actor_manager.py delete mode 100644 maro/rl/training/distributed_learner.py delete mode 100644 maro/rl/training/policy_update_schedule.py diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 748734c25..ae677db81 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -142,24 +142,24 @@ The code snippet below shows how to create a model for the actor-critic algorith optim_option={ "representation": OptimizerOption(cls="adam", params={"lr": 0.0001}), "actor": OptimizerOption(cls="adam", params={"lr": 0.001}), - "critic": OptimizerOption(cls="rmsprop", params={"lr": 0.0001}) + "critic": OptimizerOption(cls="rmsprop", params={"lr": 0.0001}) } ) -To generate stochastic actions given a batch of states, call ``get_action`` on the model instance: +To generate stochastic actions given a batch of states, call ``get_action`` on the model instance: .. code-block:: python action, log_p = ac_model.get_action(state) -To performing a single gradient step on the model, call the ``step`` function: +To performing a single gradient step on the model, call the ``step`` function: .. code-block:: python ac_model.step(critic_loss + actor_loss) Here it is assumed that the losses have been computed using the same model instance and the gradients have -been generated for the internal components. +been generated for the internal components. Experience @@ -168,15 +168,9 @@ Experience An ``ExperienceSet`` is a synonym for training data for RL policies. The data originate from the simulator and get processed and organized into a set of transitions in the form of (state, action, reward, next_state, info), where ''info'' contains information about the transition that is not encoded in the state but may be necessary -<<<<<<< HEAD for sampling purposes. An ``ExperienceStore`` is a storage facility for experience sets and is maintained by a policy for storing and retrieving training data. Sampling from the experience memory can be customized by registering a user-defined sampler to it. -======= -for sampling purposes. An ``ExperienceManager`` is a storage facility for experience sets and is maintained by -a policy for storing and retrieving training data. Sampling from the experience memory can be customized by -registering a user-defined sampler to it. ->>>>>>> v0.2_sc Exploration diff --git a/examples/supply_chain/README.md b/examples/supply_chain/README.md deleted file mode 100644 index 6cc67f2d1..000000000 --- a/examples/supply_chain/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Supply Chain Scenario - -This README contains instructions for running the supply chain scenario using the scripts provided under ```examples/supply_chain/scripts```. For details on state, action and reward shaping based on MARO's supply chain business engine, refer to ```examples/supply_chain/sc_state_in_maro.md```. - -The instructions require that you have Docker and Docker Compose installed and set up on your machine. For installing Docker, refer to https://docs.docker.com/get-docker/. For installing Docker Compose, refer to https://docs.docker.com/compose/install/. To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: -1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. -2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the "distributed" section of ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). -3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file diff --git a/examples/supply_chain/__init__.py b/examples/supply_chain/__init__.py deleted file mode 100644 index b14b47650..000000000 --- a/examples/supply_chain/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. diff --git a/examples/supply_chain/config.yml b/examples/supply_chain/config.yml deleted file mode 100644 index bc9685049..000000000 --- a/examples/supply_chain/config.yml +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -experiment_name: test - -env: - scenario: supply_chain - # Currently available topologies are "sample1" or "random". New topologies must consist of a single folder - # that contains a single config.yml and should be placed under examples/supply_chain/envs/ - topology: test - durations: 64 # number of ticks per episode - -num_episodes: 1000 # number of episodes to simulate - -# Number of roll-out steps in each learning cycle. Each actor will perform at most this many roll-out steps -# before returning experiences to the learner. The learner uses these experiences to update the agents' policies -# and sync the updated policies to the actors for the next learning cycle. -experience_update_interval: 64 - -eval_schedule: 100 - -log_env_summary: true - -policy: - consumerstore: - algorithm: dqn - model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. - device: cpu - network: - hidden_dims: - - 256 - - 128 - - 32 - output_dim: 10 - activation: leaky_relu # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. - softmax: true - batch_norm: false - skip_connection: false - head: true - dropout_p: 0.0 - optimization: - optim_cls: adam # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. - optim_params: - lr: 0.0005 - algorithm_config: - reward_discount: .99 - train_epochs: 10 - gradient_iters: 1 - loss_cls: mse # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch loss classes. - target_update_freq: 4 # How many training iteration, to update DQN target model - soft_update_coefficient: 0.01 - double: true # whether to enable double DQN - experience_manager: - capacity: 128000 # experience memory size - # This determines how existing experiences are replaced when adding new experiences to a full experience - # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, - # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. - overwrite_type: rolling - batch_size: 2560 - replace: true - update_schedule: - type: step # "step" or "episode" - args: - start_ep: 3 # must be a positive number since episode is 1-based. - interval: 1 - end_ep_update: true - consumer: - model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. - network: - output_dim: 10 - producer: - model: # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. - network: - output_dim: 10 - -exploration: - last_ep: 800 - initial_value: 0.8 # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. - final_value: 0.0 - -distributed: - # this is used to group all actor / learner processes belonging to the same job for communication purposes. - # There is no need to change this if you use the scripts under examples/supply_chain/scripts to run the scenario. - group: sc-dqn - num_actors: 3 # number of parallel roll-out actors - # If you use the scripts under examples/supply_chain/scripts to run the scenario, you can set "redis_host" - # to any string supported by the pyyaml parser. If running in multi-process mode, change this to "localhost" and make - # sure that a local redis server is running and listening on the port specified by "redis_port". - redis_host: maro-redis - redis_port: 6379 - # The number of actor finishes required for the learner to enter the next learning cycle. This is used to prevent - # slow actors from dragging down the whole process. - required_actor_finishes: 3 - # If true, experiences from older segments (usually coming from slow actors) will not be used for learning. - discard_stale_experiences: True diff --git a/examples/supply_chain/env_wrapper-2.py b/examples/supply_chain/env_wrapper-2.py deleted file mode 100644 index f32277c3e..000000000 --- a/examples/supply_chain/env_wrapper-2.py +++ /dev/null @@ -1,1187 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict, namedtuple -from typing import List - -import scipy.stats as st -import numpy as np - -from maro.rl import AbsEnvWrapper -from maro.simulator import Env -from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction - -# from exploration import exploration_dict, agent2exploration -# from learner import SCLearner - -def stock_constraint(f_state): - return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean'] - - -def is_replenish_constraint(f_state): - return f_state['consumption_hist'][-1] > 0 - - -def low_profit(f_state): - return (f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000 - - -def low_stock_constraint(f_state): - return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean'] - - -def out_of_stock(f_state): - return 0 < f_state['inventory_in_stock'] - - -atoms = { - 'stock_constraint': stock_constraint, - 'is_replenish_constraint': is_replenish_constraint, - 'low_profit': low_profit, - 'low_stock_constraint': low_stock_constraint, - 'out_of_stock': out_of_stock -} - -# State extracted. -keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'consumption_hist']), - ('storage_capacity', ['storage_utilization']), - ('sale_mean', ['sale_std', - 'sale_hist', - 'pending_order', - 'inventory_in_stock', - 'inventory_in_transit', - 'inventory_estimated', - 'inventory_rop']), - ('max_price', ['sku_price', 'sku_cost'])] - -# Sku related agent types -sku_agent_types = {"consumer", "consumerstore", "producer", "product", "productstore"} - - -class UnitBaseInfo: - id: int = None - node_index: int = None - config: dict = None - summary: dict = None - - def __init__(self, unit_summary): - self.id = unit_summary["id"] - self.node_index = unit_summary["node_index"] - self.config = unit_summary.get("config", {}) - self.summary = unit_summary - - def __getitem__(self, key, default=None): - if key in self.summary: - return self.summary[key] - - return default - - -distribution_features = ("remaining_order_quantity", "remaining_order_number") -seller_features = ("total_demand", "sold", "demand") - - -class SCEnvWrapper(AbsEnvWrapper): - def __init__(self, env: Env, reward_eval_delay: int=0, save_replay: bool=True, replay_agent_ids: list=None): - super().__init__(env, reward_eval_delay, save_replay, replay_agent_ids) - self.balance_cal = BalanceSheetCalculator(env) - self.cur_balance_sheet_reward = None - self.storage_ss = env.snapshot_list["storage"] - self.distribution_ss = env.snapshot_list["distribution"] - self.consumer_ss = env.snapshot_list["consumer"] - self.seller_ss = env.snapshot_list["seller"] - - self._summary = env.summary['node_mapping'] - self._configs = env.configs - self._agent_types = self._summary["agent_types"] - self._units_mapping = self._summary["unit_mapping"] - self._agent_list = env.agent_idx_list - - self._sku_number = len(self._summary["skus"]) + 1 - self._max_price = self._summary["max_price"] - self._max_sources_per_facility = self._summary["max_sources_per_facility"] - - # state for each tick - self._cur_metrics = env.metrics - - # cache for ppf value. - self._service_index_ppf_cache = {} - - # facility -> { - # data_model_index:int, - # storage:UnitBaseInfo, - # distribution: UnitBaseInfo, - # sku_id: { - # skuproduct: UnitBaseInfo, - # consumer: UnitBaseInfo, - # seller: UnitBaseInfo, - # manufacture: UnitBaseInfo - # } - # } - self.facility_levels = {} - - # unit id -> (facility id) - self.unit_2_facility_dict = {} - - # our raw state - self._states = {} - - # facility id -> storage index - self._facility2storage_index_dict = {} - - # facility id -> product id -> number - self._storage_product_numbers = {} - - # facility id -> product_id -> index - self._storage_product_indices = {} - - # facility id -> storage product utilization - self._facility_product_utilization = {} - - # facility id -> in_transit_orders - self._facility_in_transit_orders = {} - - # current distribution states - self._cur_distribution_states = None - - # current consumer states - self._cur_consumer_states = None - - # current seller states - self._cur_seller_states = None - - # dim for state - self._dim = None - - # use this to quick find relationship between units (consumer, manufacture, seller or product) and product unit. - # unit id -> (product unit id, facility id, seller id, consumer id, manufacture id) - self._unit2product_mapping = {} - - # agent (unit id) -> AgentInfo - self._agent_id2info_mapping = {} - - # built internal helpers. - self._build_internal_helpers() - - self.stock_status = {} - self.demand_status = {} - # key: product unit id, value: number - self.orders_from_downstreams = {} - self.consumer_orders = {} - self.order_in_transit_status = {} - self.order_to_distribute_status = {} - - @property - def dim(self): - """Calculate dim per shape.""" - if self._dim is None: - self._dim = 0 - - first_state = next(iter(self._states.values())) - - for _, state_keys in keys_in_state: - for key in state_keys: - val = first_state[key] - - if type(val) == list: - self._dim += len(val) - else: - self._dim += 1 - - return self._dim - - def get_or_policy_state(self, state, agent_info): - state = {'is_facility': not (agent_info.agent_type in sku_agent_types)} - if agent_info.is_facility: - return state - - product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id - id, product_id, _, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ - self.balance_cal.products[self.balance_cal.product_id2index_dict[product_unit_id]] - - product_metrics = self._cur_metrics["products"][product_unit_id] - state['sale_mean'] = product_metrics["sale_mean"] - state['sale_std'] = product_metrics["sale_std"] - - facility = self.facility_levels[agent_info.facility_id] - state['unit_storage_cost'] = unit_storage_cost - state['order_cost'] = 1 - product_info = facility[agent_info.sku.id] - if "consumer" in product_info: - consumer_index = product_info["consumer"].node_index - state['order_cost'] = self.consumer_ss[self.env.tick:consumer_index:"order_cost"].flatten()[0] - state['storage_capacity'] = facility['storage'].config["capacity"] - state['storage_levels'] = self._storage_product_numbers[agent_info.facility_id] - state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 - state['vlt'] = agent_info.sku.vlt - state['service_level'] = agent_info.sku.service_level - return state - - def get_rl_policy_state(self, state, agent_info): - self._update_facility_features(state, agent_info) - self._update_storage_features(state, agent_info) - # bom do not need to update - # self._add_bom_features(state, agent_info) - self._update_distribution_features(state, agent_info) - self._update_sale_features(state, agent_info) - # vlt do not need to update - # self._update_vlt_features(state, agent_info) - self._update_consumer_features(state, agent_info) - # self._add_price_features(state, agent_info) - self._update_global_features(state) - - self.stock_status[agent_info.id] = state['inventory_in_stock'] - - self.demand_status[agent_info.id] = state['sale_hist'][-1] - - self.order_in_transit_status[agent_info.id] = state['inventory_in_transit'] - - self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] - - self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} - self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} - - np_state = self._serialize_state(state) - return np_state - - def get_state(self, event): - cur_tick = self.env.tick - settings: dict = self.env.configs.settings - consumption_hist_len = settings['consumption_hist_len'] - hist_len = settings['sale_hist_len'] - consumption_ticks = [cur_tick - i for i in range(consumption_hist_len-1, -1, -1)] - hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] - - self.cur_balance_sheet_reward = self.balance_cal.calc() - self._cur_metrics = self.env.metrics - - self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features] \ - .flatten() \ - .reshape(-1, len(distribution_features)) \ - .astype(np.int) - - self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"] \ - .flatten() \ - .reshape(-1, len(self.consumer_ss)) - - self._cur_seller_states = self.seller_ss[hist_ticks::seller_features] \ - .astype(np.int) - - - # facility level states - for facility_id in self._facility_product_utilization: - # reset for each step - self._facility_product_utilization[facility_id] = 0 - - in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] - - self._facility_in_transit_orders[facility_id] = [0] * self._sku_number - - for sku_id, number in in_transit_orders.items(): - self._facility_in_transit_orders[facility_id][sku_id] = number - - final_state = {} - - # calculate storage info first, then use it later to speed up. - for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"] \ - .flatten() \ - .astype(np.int) - - for pid, index in self._storage_product_indices[facility_id].items(): - product_number = product_numbers[index] - - self._storage_product_numbers[facility_id][pid] = product_number - self._facility_product_utilization[facility_id] += product_number - - for agent_info in self._agent_list: - state = self._states[agent_info.id] - - # storage_index = self._facility2storage_index_dict[agent_info.facility_id] - - np_state = self.get_rl_policy_state(state, agent_info) - if agent_info.agent_type in ["consumer", "producer"]: - np_state = self.get_or_policy_state(state, agent_info) - - # agent_info.agent_type -> policy - final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state - - return final_state - - def get_reward(self, tick=None, target_agents=None): - # get related product, seller, consumer, manufacture unit id - # NOTE: this mapping does not contain facility id, so if id is not exist, then means it is a facility - # product_unit_id, facility_id, seller_id, consumer_id, producer_id = self._unit2product_mapping[id] - # return { - # f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}": np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) - # for f_id, bwt in self.cur_balance_sheet_reward.items() - # } - self.cur_balance_sheet_reward = self.balance_cal.calc() - rewards = defaultdict(float) - for f_id, bwt in self.cur_balance_sheet_reward.items(): - agent = self._agent_id2info_mapping[f_id] - if agent.agent_type == 'consumerstore': - rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) - else: - rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = 0 - - return rewards - - def to_env_action(self, action_by_agent): - # cache the sources for each consumer if not yet cached - if not hasattr(self, "consumer2source"): - self.consumer2source, self.consumer2product = {}, {} - for facility in self.env.summary["node_mapping"]["facilities"].values(): - products = facility["units"]["products"] - for product_id, product in products.items(): - consumer = product["consumer"] - if consumer is not None: - consumer_id = consumer["id"] - self.consumer2source[consumer_id] = consumer["sources"] - self.consumer2product[consumer_id] = product_id - - env_action = {} - for agent_id, action in action_by_agent.items(): - unit_id = int(agent_id.split(".")[1]) - - is_facility = unit_id not in self._units_mapping - - # ignore facility to reduce action number - if is_facility: - continue - - # consumer action - if agent_id.startswith("consumer"): - product_id = self.consumer2product.get(unit_id, 0) - sources = self.consumer2source.get(unit_id, []) - if sources: - source_id = sources[0] - product_unit_id = self._unit2product_mapping[unit_id][0] - action_number = int(int(action) * self._cur_metrics["products"][product_unit_id]["sale_mean"]) - - # ignore 0 quantity to reduce action number - if action_number == 0: - continue - - sku = self._units_mapping[unit_id][3] - - reward_discount = 1 - - env_action[unit_id] = ConsumerAction( - unit_id, - product_id, - source_id, - action_number, - sku.vlt, - reward_discount - ) - - self.consumer_orders[product_unit_id] = action_number - self.orders_from_downstreams[self.facility_levels[source_id][product_id]["skuproduct"].id] = action_number - - # manufacturer action - elif agent_id.startswith("producer"): - sku = self._units_mapping[unit_id][3] - action = sku.production_rate - - # ignore invalid actions - if action is None or action == 0: - continue - - env_action[unit_id] = ManufactureAction(unit_id, action) - - return env_action - - def _update_facility_features(self, state, agent_info): - state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - - def _update_storage_features(self, state, agent_info): - facility_id = agent_info.facility_id - state['storage_utilization'] = 0 - - state['storage_levels'] = self._storage_product_numbers[facility_id] - state['storage_utilization'] = self._facility_product_utilization[facility_id] - - def _update_sale_features(self, state, agent_info): - if agent_info.agent_type not in sku_agent_types: - return - - # Get product unit id for current agent. - product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id - - product_metrics = self._cur_metrics["products"][product_unit_id] - - state['sale_mean'] = product_metrics["sale_mean"] - state['sale_std'] = product_metrics["sale_std"] - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - if "seller" not in product_info: - # TODO: why gamma sale as mean? - state['sale_gamma'] = state['sale_mean'] - - if "consumer" in product_info: - consumer_index = product_info["consumer"].node_index - - state['consumption_hist'] = list( - self._cur_consumer_states[:, consumer_index]) - state['pending_order'] = list( - product_metrics["pending_order_daily"]) - - if "seller" in product_info: - seller_index = product_info["seller"].node_index - - seller_states = self._cur_seller_states[:, seller_index, :] - - # For total demand, we need latest one. - state['total_backlog_demand'] = seller_states[:, 0][-1][0] - state['sale_hist'] = list(seller_states[:, 1].flatten()) - state['backlog_demand_hist'] = list(seller_states[:, 2]) - - def _update_distribution_features(self, state, agent_info): - facility = self.facility_levels[agent_info.facility_id] - distribution = facility.get("distribution", None) - - if distribution is not None: - dist_states = self._cur_distribution_states[distribution.node_index] - state['distributor_in_transit_orders'] = dist_states[1] - state['distributor_in_transit_orders_qty'] = dist_states[0] - - def _update_consumer_features(self, state, agent_info): - if agent_info.is_facility: - return - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - # if "consumer" not in product_info: - # return - - state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - - # FIX: we need plus 1 to this, as it is 0 based index, but we already aligned with 1 more - # slot to use sku id as index ( 1 based). - product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 - state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] - state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] - - pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] - - if pending_order is not None: - state['inventory_in_distribution'] = pending_order[agent_info.sku.id] - - state['inventory_estimated'] = (state['inventory_in_stock'] - + state['inventory_in_transit'] - - state['inventory_in_distribution']) - if state['inventory_estimated'] >= 0.5 * state['storage_capacity']: - state['is_over_stock'] = 1 - - if state['inventory_estimated'] <= 0: - state['is_out_of_stock'] = 1 - - service_index = state['service_level'] - - if service_index not in self._service_index_ppf_cache: - self._service_index_ppf_cache[service_index] = st.norm.ppf( - service_index) - - ppf = self._service_index_ppf_cache[service_index] - - state['inventory_rop'] = (state['max_vlt'] * state['sale_mean'] - + np.sqrt(state['max_vlt']) * state['sale_std'] * ppf) - - if state['inventory_estimated'] < state['inventory_rop']: - state['is_below_rop'] = 1 - - def _update_global_features(self, state): - state["global_time"] = self.env.tick - - def _serialize_state(self, state): - result = [] - - for norm, fields in keys_in_state: - for field in fields: - vals = state[field] - if not isinstance(vals, list): - vals = [vals] - if norm is not None: - vals = [max(0.0, min(20.0, x / (state[norm] + 0.01))) - for x in vals] - result.extend(vals) - - return np.asarray(result, dtype=np.float32) - - def _build_internal_helpers(self): - for agent_info in self.env.agent_idx_list: - self._agent_id2info_mapping[agent_info.id] = agent_info - - # facility levels - for facility_id, facility in self._summary["facilities"].items(): - self.facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility['configs'], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } - - units = facility["units"] - - storage = units["storage"] - if storage is not None: - self.facility_levels[facility_id]["storage"] = UnitBaseInfo( - storage) - - self.unit_2_facility_dict[storage["id"]] = facility_id - - self._facility2storage_index_dict[facility_id] = storage["node_index"] - - self._storage_product_numbers[facility_id] = [0] * self._sku_number - self._storage_product_indices[facility_id] = {} - self._facility_product_utilization[facility_id] = 0 - - for i, pid in enumerate(storage["product_list"]): - self._storage_product_indices[facility_id][pid] = i - self._storage_product_numbers[facility_id][pid] = 0 - - distribution = units["distribution"] - - if distribution is not None: - self.facility_levels[facility_id]["distribution"] = UnitBaseInfo( - distribution) - self.unit_2_facility_dict[distribution["id"]] = facility_id - - products = units["products"] - - if products: - for product_id, product in products.items(): - product_info = { - "skuproduct": UnitBaseInfo(product) - } - - self.unit_2_facility_dict[product["id"]] = facility_id - - seller = product['seller'] - - if seller is not None: - product_info["seller"] = UnitBaseInfo(seller) - self.unit_2_facility_dict[seller["id"]] = facility_id - - consumer = product["consumer"] - - if consumer is not None: - product_info["consumer"] = UnitBaseInfo(consumer) - self.unit_2_facility_dict[consumer["id"]] = facility_id - - manufacture = product["manufacture"] - - if manufacture is not None: - product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"] - ] = facility_id - - self.facility_levels[facility_id][product_id] = product_info - - for unit in (seller, consumer, manufacture, product): - if unit is not None: - self._unit2product_mapping[unit["id"]] = ( - product["id"], - facility_id, - seller["id"] if seller is not None else None, - consumer["id"] if consumer is not None else None, - manufacture["id"] if manufacture is not None else None - ) - - # create initial state structure - self._build_init_state() - - def _build_init_state(self): - # we will build the final state with default and const values, - # then update dynamic part per step - for agent_info in self._agent_list: - state = {} - - facility = self.facility_levels[agent_info.facility_id] - - # global features - state["global_time"] = 0 - - # facility features - state["facility"] = None - state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] - state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] - state['constraint_idx'] = [0] - state['facility_id'] = [0] * self._sku_number - state['sku_info'] = {} if agent_info.is_facility else agent_info.sku - state['echelon_level'] = 0 - - state['facility_info'] = facility['config'] - state["is_positive_balance"] = 0 - - if not agent_info.is_facility: - state['facility_id'][agent_info.sku.id] = 1 - - for atom_name in atoms.keys(): - state[atom_name] = list( - np.ones(self._configs.settings['constraint_state_hist_len'])) - - # storage features - state['storage_levels'] = [0] * self._sku_number - state['storage_capacity'] = facility['storage'].config["capacity"] - state['storage_utilization'] = 0 - - # bom features - state['bom_inputs'] = [0] * self._sku_number - state['bom_outputs'] = [0] * self._sku_number - - if not agent_info.is_facility: - state['bom_inputs'][agent_info.sku.id] = 1 - state['bom_outputs'][agent_info.sku.id] = 1 - - # vlt features - sku_list = self._summary["skus"] - current_source_list = [] - - if agent_info.sku is not None: - current_source_list = facility["upstreams"].get( - agent_info.sku.id, []) - - state['vlt'] = [0] * \ - (self._max_sources_per_facility * self._sku_number) - state['max_vlt'] = 0 - - if not agent_info.is_facility: - # only for sku product - product_info = facility[agent_info.sku.id] - - if "consumer" in product_info and len(current_source_list) > 0: - state['max_vlt'] = product_info["skuproduct"]["max_vlt"] - - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - # NOTE: different with original code, our config can make sure that source has product we need - - if sku.id == agent_info.sku.id: - state['vlt'][i * len(sku_list) + j + - 1] = facility["skus"][sku.id].vlt - - # sale features - settings = self.env.configs.settings - hist_len = settings['sale_hist_len'] - consumption_hist_len = settings['consumption_hist_len'] - - state['sale_mean'] = 1.0 - state['sale_std'] = 1.0 - state['sale_gamma'] = 1.0 - state['service_level'] = 0.95 - state['total_backlog_demand'] = 0 - - state['sale_hist'] = [0] * hist_len - state['backlog_demand_hist'] = [0] * hist_len - state['consumption_hist'] = [0] * consumption_hist_len - state['pending_order'] = [0] * settings['pending_order_len'] - - if not agent_info.is_facility: - state['service_level'] = agent_info.sku.service_level - - product_info = facility[agent_info.sku.id] - - if "seller" in product_info: - state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma - - # distribution features - state['distributor_in_transit_orders'] = 0 - state['distributor_in_transit_orders_qty'] = 0 - - # consumer features - state['consumer_source_export_mask'] = [0] * \ - (self._max_sources_per_facility * self._sku_number) - state['consumer_source_inventory'] = [0] * self._sku_number - state['consumer_in_transit_orders'] = [0] * self._sku_number - - state['inventory_in_stock'] = 0 - state['inventory_in_transit'] = 0 - state['inventory_in_distribution'] = 0 - state['inventory_estimated'] = 0 - state['inventory_rop'] = 0 - state['is_over_stock'] = 0 - state['is_out_of_stock'] = 0 - state['is_below_rop'] = 0 - - if len(current_source_list) > 0: - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - if sku.id == agent_info.sku.id: - state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = \ - self.facility_levels[source]["skus"][sku.id].vlt - - # price features - state['max_price'] = self._max_price - state['sku_price'] = 0 - state['sku_cost'] = 0 - - if not agent_info.is_facility: - state['sku_price'] = agent_info.sku.price - state['sku_cost'] = agent_info.sku.cost - - self._states[agent_info.id] = state - - -ProductInfo = namedtuple( - "ProductInfo", - ( - "unit_id", - "sku_id", - "node_index", - "storage_index", - "unit_storage_cost", - "distribution_index", - "downstream_product_units", - "consumer_id_index_tuple", - "seller_id_index_tuple", - "manufacture_id_index_tuple" - ) -) - -FacilityLevelInfo = namedtuple( - "FacilityLevelInfo", - ( - "unit_id", - "product_unit_id_list", - "storage_index", - "unit_storage_cost", - "distribution_index", - "vehicle_index_list" - ) -) - - -class BalanceSheetCalculator: - def __init__(self, env: Env): - self.env = env - self.products: List[ProductInfo] = [] - self.product_id2index_dict = {} - self.facility_levels: List[FacilityLevelInfo] = [] - self.consumer_id2product = {} - - self.facilities = env.summary["node_mapping"]["facilities"] - - for facility_id, facility in self.facilities.items(): - pid_list = [] - distribution = facility["units"]["distribution"] - - for product_id, product in facility["units"]["products"].items(): - pid_list.append(product["id"]) - consumer = product["consumer"] - if consumer is not None: - self.consumer_id2product[consumer["id"]] = product["id"] - seller = product["seller"] - manufacture = product["manufacture"] - - self.product_id2index_dict[product["id"]] = len(self.products) - - downstream_product_units = [] - downstreams = facility["downstreams"] - - if downstreams and len(downstreams) > 0 and product_id in downstreams: - for dfacility in downstreams[product_id]: - dproducts = self.facilities[dfacility]["units"]["products"] - - downstream_product_units.append(dproducts[product_id]["id"]) - - self.products.append( - ProductInfo( - unit_id=product["id"], - sku_id=product_id, - node_index=product["node_index"], - storage_index=facility["units"]["storage"]["node_index"], - unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution_index=distribution["node_index"] if distribution is not None else None, - downstream_product_units=downstream_product_units, - consumer_id_index_tuple=None if consumer is None else (consumer["id"], consumer["node_index"]), - seller_id_index_tuple=None if seller is None else (seller["id"], seller["node_index"]), - manufacture_id_index_tuple=None if manufacture is None else (manufacture["id"], manufacture["node_index"]) - ) - ) - - self.facility_levels.append( - FacilityLevelInfo( - unit_id=facility_id, - product_unit_id_list=pid_list, - storage_index=facility["units"]["storage"]["node_index"], - unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution_index=distribution["node_index"] if distribution is not None else None, - vehicle_index_list=[ - v["node_index"] for v in distribution["children"] - ] if distribution is not None else [] - ) - ) - - # TODO: order products make sure calculate reward from downstream to upstream - tmp_product_unit_dict = {} - - for product in self.products: - tmp_product_unit_dict[product.unit_id] = product - - self._ordered_products = [] - - tmp_stack = [] - - for product in self.products: - # skip if already being processed - if tmp_product_unit_dict[product.unit_id] is None: - continue - - for dproduct in product.downstream_product_units: - # push downstream id to stack - tmp_stack.append(dproduct) - - # insert current product to list head - self._ordered_products.insert(0, product) - # mark it as processed - tmp_product_unit_dict[product.unit_id] = None - - while len(tmp_stack) > 0: - # process downstream of product unit in stack - dproduct_unit_id = tmp_stack.pop() - - # if it was processed then ignore - if tmp_product_unit_dict[dproduct_unit_id] is None: - continue - - # or extract it downstreams - dproduct_unit = tmp_product_unit_dict[dproduct_unit_id] - - dproduct_downstreams = dproduct_unit.downstream_product_units - - for dproduct in dproduct_downstreams: - tmp_stack.append(dproduct) - - # current unit in final list - self._ordered_products.insert(0, dproduct_unit) - tmp_product_unit_dict[dproduct_unit_id] = None - - self.total_balance_sheet = defaultdict(int) - - # tick -> (product unit id, sku id, manufacture number, manufacture cost, checkin order, delay penaty) - self._supplier_reward_factors = {} - - def _check_attribute_keys(self, target_type: str, attribute: str): - valid_target_types = list(self.env.summary["node_detail"].keys()) - assert target_type in valid_target_types, f"Target_type {target_type} not in {valid_target_types}!" - - valid_attributes = list(self.env.summary["node_detail"][target_type]["attributes"].keys()) - assert attribute in valid_attributes, ( - f"Attribute {attribute} not valid for {target_type}. " - f"Valid attributes: {valid_attributes}" - ) - return - - def _get_attributes(self, target_type: str, attribute: str, tick: int=None) -> np.ndarray: - self._check_attribute_keys(target_type, attribute) - - if tick == None: - tick = self.env.tick - - return self.env.snapshot_list[target_type][tick::attribute].flatten() - - def _get_list_attributes(self, target_type: str, attribute: str, tick: int=None) -> List[np.ndarray]: - self._check_attribute_keys(target_type, attribute) - - if tick == None: - tick = self.env.tick - - indexes = list(range(len(self.env.snapshot_list[target_type]))) - return [self.env.snapshot_list[target_type][tick:index:attribute].flatten() for index in indexes] - - def _calc_consumer(self): - #### Consumer - consumer_ids = self._get_attributes("consumer", "id").astype(np.int) - - # quantity * price - order_profit = ( - self._get_attributes("consumer", "order_quantity") - * self._get_attributes("consumer", "price") - ) - - # order_cost + order_product_cost - consumer_step_balance_sheet_loss = -1 * ( - self._get_attributes("consumer", "order_cost") - + self._get_attributes("consumer", "order_product_cost") - ) - - # consumer step reward: balance sheet los + profile * discount - # consumer_step_reward = ( - # consumer_step_balance_sheet_loss - # + order_profit * self._get_attributes("consumer", "reward_discount") - # ) - consumer_step_reward = consumer_step_balance_sheet_loss - - consumer_step_balance_sheet = order_profit + consumer_step_balance_sheet_loss - - return consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet - - def _calc_seller(self): - #### Seller - # profit = sold * price - seller_balance_sheet_profit = ( - self._get_attributes("seller", "sold") - * self._get_attributes("seller", "price") - ) - - # loss = demand * price * backlog_ratio - seller_balance_sheet_loss = -1 * ( - self._get_attributes("seller", "demand") - * self._get_attributes("seller", "price") - * self._get_attributes("seller", "backlog_ratio") - ) - - # step reward = loss + profit - seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit - - return seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward - - def _calc_manufacture(self): - #### manufacture - manufacture_ids = self._get_attributes("manufacture", "id").astype(np.int) - - # loss = manufacture number * cost - manufacture_balance_sheet_loss = -1 * ( - self._get_attributes("manufacture", "manufacturing_number") - * self._get_attributes("manufacture", "product_unit_cost") - ) - - # step reward = loss - manufacture_step_reward = manufacture_balance_sheet_loss - manufacture_step_balance_sheet = manufacture_balance_sheet_loss - - return manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet - - def _calc_storage(self): - #### storage - # loss = (capacity-remaining space) * cost - storage_balance_sheet_loss = -1 * ( - self._get_attributes("storage", "capacity") - - self._get_attributes("storage", "remaining_space") - ) - - # create product number mapping for storages - product_list = self._get_list_attributes("storage", "product_list") - product_number = self._get_list_attributes("storage", "product_number") - storages_product_map = { - idx: { - id: num - for id, num in zip(id_list.astype(np.int), num_list.astype(np.int)) - } - for idx, (id_list, num_list) in enumerate(zip(product_list, product_number)) - } - - return storage_balance_sheet_loss, storages_product_map - - def _calc_vehicle(self): - ## vehicles - # loss = cost * payload - vehicle_balance_sheet_loss = -1 * ( - self._get_attributes("vehicle", "payload") - * self._get_attributes("vehicle", "unit_transport_cost") - ) - vehicle_step_reward = vehicle_balance_sheet_loss - return vehicle_balance_sheet_loss, vehicle_step_reward - - def _calc_product_distribution(self): - #### product - # product distribution profit = check order * price - product_distribution_balance_sheet_profit = ( - self._get_attributes("product", "distribution_check_order") - * self._get_attributes("product", "price") - ) - # product distribution loss = transportation cost + delay order penalty - product_distribution_balance_sheet_loss = -1 * ( - self._get_attributes("product", "distribution_transport_cost") - + self._get_attributes("product", "distribution_delay_order_penalty") - ) - return product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss - - def _calc_product( - self, - consumer_step_balance_sheet_loss, - consumer_step_reward, - seller_balance_sheet_profit, - seller_balance_sheet_loss, - seller_step_reward, - manufacture_balance_sheet_loss, - manufacture_step_reward, - storages_product_map, - product_distribution_balance_sheet_profit, - product_distribution_balance_sheet_loss, - ): - num_products = len(self.products) - product_step_reward = np.zeros(num_products) - product_balance_sheet_profit = np.zeros(num_products) - product_balance_sheet_loss = np.zeros(num_products) - - # product = consumer + seller + manufacture + storage + distribution + downstreams - for product in self._ordered_products: - i = product.node_index - - if product.consumer_id_index_tuple: - consumer_index = product.consumer_id_index_tuple[1] - product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer_index] - product_step_reward[i] += consumer_step_reward[consumer_index] - - if product.seller_id_index_tuple: - seller_index = product.seller_id_index_tuple[1] - product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller_index] - product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller_index] - product_step_reward[i] += seller_step_reward[seller_index] - - if product.manufacture_id_index_tuple: - manufacture_index = product.manufacture_id_index_tuple[1] - product_balance_sheet_loss[i] += manufacture_balance_sheet_loss[manufacture_index] - product_step_reward[i] += manufacture_step_reward[manufacture_index] - - storage_reward = -1 * storages_product_map[product.storage_index][product.sku_id] * product.unit_storage_cost - product_step_reward[i] += storage_reward - product_balance_sheet_loss[i] += storage_reward - - if product.distribution_index is not None: - product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[i] - product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[i] - product_step_reward[i] += product_distribution_balance_sheet_loss[i] + product_distribution_balance_sheet_profit[i] - - if len(product.downstream_product_units) > 0: - for did in product.downstream_product_units: - product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] - product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] - product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] - - product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss - - return product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet - - def _calc_facility( - self, - storage_balance_sheet_loss, - vehicle_balance_sheet_loss, - product_balance_sheet_profit, - product_balance_sheet_loss, - product_step_reward - ): - num_facilities = len(self.facility_levels) - facility_balance_sheet_loss = np.zeros(num_facilities) - facility_balance_sheet_profit = np.zeros(num_facilities) - facility_step_reward = np.zeros(num_facilities) - - # for facilities - for i, facility in enumerate(self.facility_levels): - # storage balance sheet - # profit=0 - facility_balance_sheet_loss[i] += storage_balance_sheet_loss[facility.storage_index] * facility.unit_storage_cost - - # distribution balance sheet - if facility.distribution_index is not None: - for vidx in facility.vehicle_index_list: - facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vidx] - # distribution unit do not provide reward - - # sku product unit balance sheet - for pid in facility.product_unit_id_list: - facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] - facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] - facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] - - facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit - - return facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet - - def calc(self): - #### Basic Units: Loss, Profit, Reward - consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet = self._calc_consumer() - seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward = self._calc_seller() - manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet = self._calc_manufacture() - storage_balance_sheet_loss, storages_product_map = self._calc_storage() - vehicle_balance_sheet_loss, vehicle_step_reward = self._calc_vehicle() - product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss = self._calc_product_distribution() - ######################################################################## - - #### Loss, profit, reward for each product - product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet = self._calc_product( - consumer_step_balance_sheet_loss, - consumer_step_reward, - seller_balance_sheet_profit, - seller_balance_sheet_loss, - seller_step_reward, - manufacture_balance_sheet_loss, - manufacture_step_reward, - storages_product_map, - product_distribution_balance_sheet_profit, - product_distribution_balance_sheet_loss - ) - ######################################################################## - - #### Loss, profit, reward for each facility - # facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet = self._calc_facility( - # storage_balance_sheet_loss, - # vehicle_balance_sheet_loss, - # product_balance_sheet_profit, - # product_balance_sheet_loss, - # product_step_reward - # ) - ######################################################################## - - # Final result for current tick, key is the facility/unit id, value is tuple of balance sheet and reward. - result = {} - - # For product units. - for id, bs, rw in zip([product.unit_id for product in self.products], product_balance_sheet, product_step_reward): - result[id] = (bs, rw) - self.total_balance_sheet[id] += bs - - # For consumers. - for id, bs, rw in zip(consumer_ids, consumer_step_balance_sheet, consumer_step_reward): - # result[id] = (bs, rw) - # let reward of a consumer equate its parent product - result[id] = result[self.consumer_id2product[id]] - self.total_balance_sheet[id] += result[id][0] - - # For producers. - for id, bs, rw in zip(manufacture_ids, manufacture_step_balance_sheet, manufacture_step_reward): - result[id] = (bs, rw) - self.total_balance_sheet[id] += bs - - # NOTE: add followings if you need. - # For storages. - # For distributions. - # For vehicles. - - return result - - -if __name__ == "__main__": - from time import time - import cProfile - - env = Env( - scenario="supply_chain", - topology="sample", - durations=100, - max_snapshots=10) - - ss = SCEnvWrapper(env) - - env.step(None) - - start_time = time() - - # cProfile.run("ss.get_state(None)", sort="cumtime") - states = ss.get_state(None) - print(env.agent_idx_list) - print(ss.cur_balance_sheet_reward) - print(states) - - # end_time = time() - # - # print("time cost:", end_time - start_time) - # - # print("dim:", ss.dim) diff --git a/examples/supply_chain/env_wrapper.py b/examples/supply_chain/env_wrapper.py deleted file mode 100644 index df742cc28..000000000 --- a/examples/supply_chain/env_wrapper.py +++ /dev/null @@ -1,1086 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict, namedtuple - -import scipy.stats as st -import numpy as np - -from maro.rl import AbsEnvWrapper -from maro.simulator import Env -from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction - -# from exploration import exploration_dict, agent2exploration -# from learner import SCLearner - -def stock_constraint(f_state): - return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean'] - - -def is_replenish_constraint(f_state): - return f_state['consumption_hist'][-1] > 0 - - -def low_profit(f_state): - return (f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000 - - -def low_stock_constraint(f_state): - return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean'] - - -def out_of_stock(f_state): - return 0 < f_state['inventory_in_stock'] - - -atoms = { - 'stock_constraint': stock_constraint, - 'is_replenish_constraint': is_replenish_constraint, - 'low_profit': low_profit, - 'low_stock_constraint': low_stock_constraint, - 'out_of_stock': out_of_stock -} - -# State extracted. -keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'consumption_hist']), - ('storage_capacity', ['storage_utilization']), - ('sale_mean', ['sale_std', - 'sale_hist', - 'pending_order', - 'inventory_in_stock', - 'inventory_in_transit', - 'inventory_estimated', - 'inventory_rop']), - ('max_price', ['sku_price', 'sku_cost'])] - -# Sku related agent types -sku_agent_types = {"consumer", "consumerstore", "producer", "product", "productstore"} - - -class UnitBaseInfo: - id: int = None - node_index: int = None - config: dict = None - summary: dict = None - - def __init__(self, unit_summary): - self.id = unit_summary["id"] - self.node_index = unit_summary["node_index"] - self.config = unit_summary.get("config", {}) - self.summary = unit_summary - - def __getitem__(self, key, default=None): - if key in self.summary: - return self.summary[key] - - return default - - -distribution_features = ("remaining_order_quantity", "remaining_order_number") -seller_features = ("total_demand", "sold", "demand") - - -class SCEnvWrapper(AbsEnvWrapper): - def __init__(self, env: Env, reward_eval_delay: int=0, save_replay: bool=True, replay_agent_ids: list=None): - super().__init__(env, reward_eval_delay, save_replay, replay_agent_ids) - self.balance_cal = BalanceSheetCalculator(env) - self.cur_balance_sheet_reward = None - self.storage_ss = env.snapshot_list["storage"] - self.distribution_ss = env.snapshot_list["distribution"] - self.consumer_ss = env.snapshot_list["consumer"] - self.seller_ss = env.snapshot_list["seller"] - - self._summary = env.summary['node_mapping'] - self._configs = env.configs - self._agent_types = self._summary["agent_types"] - self._units_mapping = self._summary["unit_mapping"] - self._agent_list = env.agent_idx_list - - self._sku_number = len(self._summary["skus"]) + 1 - self._max_price = self._summary["max_price"] - self._max_sources_per_facility = self._summary["max_sources_per_facility"] - - # state for each tick - self._cur_metrics = env.metrics - - # cache for ppf value. - self._service_index_ppf_cache = {} - - # facility -> { - # data_model_index:int, - # storage:UnitBaseInfo, - # distribution: UnitBaseInfo, - # sku_id: { - # skuproduct: UnitBaseInfo, - # consumer: UnitBaseInfo, - # seller: UnitBaseInfo, - # manufacture: UnitBaseInfo - # } - # } - self.facility_levels = {} - - # unit id -> (facility id) - self.unit_2_facility_dict = {} - - # our raw state - self._states = {} - - # facility id -> storage index - self._facility2storage_index_dict = {} - - # facility id -> product id -> number - self._storage_product_numbers = {} - - # facility id -> product_id -> index - self._storage_product_indices = {} - - # facility id -> storage product utilization - self._facility_product_utilization = {} - - # facility id -> in_transit_orders - self._facility_in_transit_orders = {} - - # current distribution states - self._cur_distribution_states = None - - # current consumer states - self._cur_consumer_states = None - - # current seller states - self._cur_seller_states = None - - # dim for state - self._dim = None - - # use this to quick find relationship between units (consumer, manufacture, seller or product) and product unit. - # unit id -> (product unit id, facility id, seller id, consumer id, manufacture id) - self._unit2product_mapping = {} - - # agent (unit id) -> AgentInfo - self._agent_id2info_mapping = {} - - # built internal helpers. - self._build_internal_helpers() - - self.stock_status = {} - self.demand_status = {} - # key: product unit id, value: number - self.orders_from_downstreams = {} - self.consumer_orders = {} - self.order_in_transit_status = {} - self.order_to_distribute_status = {} - - @property - def dim(self): - """Calculate dim per shape.""" - if self._dim is None: - self._dim = 0 - - first_state = next(iter(self._states.values())) - - for _, state_keys in keys_in_state: - for key in state_keys: - val = first_state[key] - - if type(val) == list: - self._dim += len(val) - else: - self._dim += 1 - - return self._dim - - def get_or_policy_state(self, state, agent_info): - state = {'is_facility': not (agent_info.agent_type in sku_agent_types)} - if agent_info.is_facility: - return state - - product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id - id, product_id, _, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ - self.balance_cal.products[self.balance_cal.product_id2index_dict[product_unit_id]] - - product_metrics = self._cur_metrics["products"][product_unit_id] - state['sale_mean'] = product_metrics["sale_mean"] - state['sale_std'] = product_metrics["sale_std"] - - facility = self.facility_levels[agent_info.facility_id] - state['unit_storage_cost'] = unit_storage_cost - state['order_cost'] = 1 - product_info = facility[agent_info.sku.id] - if "consumer" in product_info: - consumer_index = product_info["consumer"].node_index - state['order_cost'] = self.consumer_ss[self.env.tick:consumer_index:"order_cost"].flatten()[0] - state['storage_capacity'] = facility['storage'].config["capacity"] - state['storage_levels'] = self._storage_product_numbers[agent_info.facility_id] - state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 - state['vlt'] = agent_info.sku.vlt - state['service_level'] = agent_info.sku.service_level - return state - - def get_rl_policy_state(self, state, agent_info): - self._update_facility_features(state, agent_info) - self._update_storage_features(state, agent_info) - # bom do not need to update - # self._add_bom_features(state, agent_info) - self._update_distribution_features(state, agent_info) - self._update_sale_features(state, agent_info) - # vlt do not need to update - # self._update_vlt_features(state, agent_info) - self._update_consumer_features(state, agent_info) - # self._add_price_features(state, agent_info) - self._update_global_features(state) - - self.stock_status[agent_info.id] = state['inventory_in_stock'] - - self.demand_status[agent_info.id] = state['sale_hist'][-1] - - self.order_in_transit_status[agent_info.id] = state['inventory_in_transit'] - - self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] - - self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} - self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} - - np_state = self._serialize_state(state) - return np_state - - def get_state(self, event): - cur_tick = self.env.tick - settings: dict = self.env.configs.settings - consumption_hist_len = settings['consumption_hist_len'] - hist_len = settings['sale_hist_len'] - consumption_ticks = [cur_tick - i for i in range(consumption_hist_len-1, -1, -1)] - hist_ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] - - self.cur_balance_sheet_reward = self.balance_cal.calc() - self._cur_metrics = self.env.metrics - - self._cur_distribution_states = self.distribution_ss[cur_tick::distribution_features] \ - .flatten() \ - .reshape(-1, len(distribution_features)) \ - .astype(np.int) - - self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"] \ - .flatten() \ - .reshape(-1, len(self.consumer_ss)) - - self._cur_seller_states = self.seller_ss[hist_ticks::seller_features] \ - .astype(np.int) - - - # facility level states - for facility_id in self._facility_product_utilization: - # reset for each step - self._facility_product_utilization[facility_id] = 0 - - in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] - - self._facility_in_transit_orders[facility_id] = [0] * self._sku_number - - for sku_id, number in in_transit_orders.items(): - self._facility_in_transit_orders[facility_id][sku_id] = number - - final_state = {} - - # calculate storage info first, then use it later to speed up. - for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[cur_tick:storage_index:"product_number"] \ - .flatten() \ - .astype(np.int) - - for pid, index in self._storage_product_indices[facility_id].items(): - product_number = product_numbers[index] - - self._storage_product_numbers[facility_id][pid] = product_number - self._facility_product_utilization[facility_id] += product_number - - for agent_info in self._agent_list: - state = self._states[agent_info.id] - - # storage_index = self._facility2storage_index_dict[agent_info.facility_id] - - np_state = self.get_rl_policy_state(state, agent_info) - if agent_info.agent_type in ["consumer", "producer"]: - np_state = self.get_or_policy_state(state, agent_info) - - # agent_info.agent_type -> policy - final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state - - return final_state - - def get_reward(self, tick=None, target_agents=None): - # get related product, seller, consumer, manufacture unit id - # NOTE: this mapping does not contain facility id, so if id is not exist, then means it is a facility - # product_unit_id, facility_id, seller_id, consumer_id, producer_id = self._unit2product_mapping[id] - # return { - # f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}": np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) - # for f_id, bwt in self.cur_balance_sheet_reward.items() - # } - self.cur_balance_sheet_reward = self.balance_cal.calc() - rewards = defaultdict(float) - for f_id, bwt in self.cur_balance_sheet_reward.items(): - agent = self._agent_id2info_mapping[f_id] - if agent.agent_type == 'consumerstore': - rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) - else: - rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = 0 - - return rewards - - def to_env_action(self, action_by_agent): - # cache the sources for each consumer if not yet cached - if not hasattr(self, "consumer2source"): - self.consumer2source, self.consumer2product = {}, {} - for facility in self.env.summary["node_mapping"]["facilities"].values(): - products = facility["units"]["products"] - for product_id, product in products.items(): - consumer = product["consumer"] - if consumer is not None: - consumer_id = consumer["id"] - self.consumer2source[consumer_id] = consumer["sources"] - self.consumer2product[consumer_id] = product_id - - env_action = {} - for agent_id, action in action_by_agent.items(): - unit_id = int(agent_id.split(".")[1]) - - is_facility = unit_id not in self._units_mapping - - # ignore facility to reduce action number - if is_facility: - continue - - # consumer action - if agent_id.startswith("consumer"): - product_id = self.consumer2product.get(unit_id, 0) - sources = self.consumer2source.get(unit_id, []) - if sources: - source_id = sources[0] - product_unit_id = self._unit2product_mapping[unit_id][0] - action_number = int(int(action) * self._cur_metrics["products"][product_unit_id]["sale_mean"]) - - # ignore 0 quantity to reduce action number - if action_number == 0: - continue - - sku = self._units_mapping[unit_id][3] - - reward_discount = 1 - - env_action[unit_id] = ConsumerAction( - unit_id, - product_id, - source_id, - action_number, - sku.vlt, - reward_discount - ) - - self.consumer_orders[product_unit_id] = action_number - self.orders_from_downstreams[self.facility_levels[source_id][product_id]["skuproduct"].id] = action_number - - # manufacturer action - elif agent_id.startswith("producer"): - sku = self._units_mapping[unit_id][3] - action = sku.production_rate - - # ignore invalid actions - if action is None or action == 0: - continue - - env_action[unit_id] = ManufactureAction(unit_id, action) - - return env_action - - def _update_facility_features(self, state, agent_info): - state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - - def _update_storage_features(self, state, agent_info): - facility_id = agent_info.facility_id - state['storage_utilization'] = 0 - - state['storage_levels'] = self._storage_product_numbers[facility_id] - state['storage_utilization'] = self._facility_product_utilization[facility_id] - - def _update_sale_features(self, state, agent_info): - if agent_info.agent_type not in sku_agent_types: - return - - # Get product unit id for current agent. - product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id - - product_metrics = self._cur_metrics["products"][product_unit_id] - - state['sale_mean'] = product_metrics["sale_mean"] - state['sale_std'] = product_metrics["sale_std"] - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - if "seller" not in product_info: - # TODO: why gamma sale as mean? - state['sale_gamma'] = state['sale_mean'] - - if "consumer" in product_info: - consumer_index = product_info["consumer"].node_index - - state['consumption_hist'] = list( - self._cur_consumer_states[:, consumer_index]) - state['pending_order'] = list( - product_metrics["pending_order_daily"]) - - if "seller" in product_info: - seller_index = product_info["seller"].node_index - - seller_states = self._cur_seller_states[:, seller_index, :] - - # For total demand, we need latest one. - state['total_backlog_demand'] = seller_states[:, 0][-1][0] - state['sale_hist'] = list(seller_states[:, 1].flatten()) - state['backlog_demand_hist'] = list(seller_states[:, 2]) - - def _update_distribution_features(self, state, agent_info): - facility = self.facility_levels[agent_info.facility_id] - distribution = facility.get("distribution", None) - - if distribution is not None: - dist_states = self._cur_distribution_states[distribution.node_index] - state['distributor_in_transit_orders'] = dist_states[1] - state['distributor_in_transit_orders_qty'] = dist_states[0] - - def _update_consumer_features(self, state, agent_info): - if agent_info.is_facility: - return - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - # if "consumer" not in product_info: - # return - - state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - - # FIX: we need plus 1 to this, as it is 0 based index, but we already aligned with 1 more - # slot to use sku id as index ( 1 based). - product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 - state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] - state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] - - pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] - - if pending_order is not None: - state['inventory_in_distribution'] = pending_order[agent_info.sku.id] - - state['inventory_estimated'] = (state['inventory_in_stock'] - + state['inventory_in_transit'] - - state['inventory_in_distribution']) - if state['inventory_estimated'] >= 0.5 * state['storage_capacity']: - state['is_over_stock'] = 1 - - if state['inventory_estimated'] <= 0: - state['is_out_of_stock'] = 1 - - service_index = state['service_level'] - - if service_index not in self._service_index_ppf_cache: - self._service_index_ppf_cache[service_index] = st.norm.ppf( - service_index) - - ppf = self._service_index_ppf_cache[service_index] - - state['inventory_rop'] = (state['max_vlt'] * state['sale_mean'] - + np.sqrt(state['max_vlt']) * state['sale_std'] * ppf) - - if state['inventory_estimated'] < state['inventory_rop']: - state['is_below_rop'] = 1 - - def _update_global_features(self, state): - state["global_time"] = self.env.tick - - def _serialize_state(self, state): - result = [] - - for norm, fields in keys_in_state: - for field in fields: - vals = state[field] - if not isinstance(vals, list): - vals = [vals] - if norm is not None: - vals = [max(0.0, min(20.0, x / (state[norm] + 0.01))) - for x in vals] - result.extend(vals) - - return np.asarray(result, dtype=np.float32) - - def _build_internal_helpers(self): - for agent_info in self.env.agent_idx_list: - self._agent_id2info_mapping[agent_info.id] = agent_info - - # facility levels - for facility_id, facility in self._summary["facilities"].items(): - self.facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility['configs'], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } - - units = facility["units"] - - storage = units["storage"] - if storage is not None: - self.facility_levels[facility_id]["storage"] = UnitBaseInfo( - storage) - - self.unit_2_facility_dict[storage["id"]] = facility_id - - self._facility2storage_index_dict[facility_id] = storage["node_index"] - - self._storage_product_numbers[facility_id] = [0] * self._sku_number - self._storage_product_indices[facility_id] = {} - self._facility_product_utilization[facility_id] = 0 - - for i, pid in enumerate(storage["product_list"]): - self._storage_product_indices[facility_id][pid] = i - self._storage_product_numbers[facility_id][pid] = 0 - - distribution = units["distribution"] - - if distribution is not None: - self.facility_levels[facility_id]["distribution"] = UnitBaseInfo( - distribution) - self.unit_2_facility_dict[distribution["id"]] = facility_id - - products = units["products"] - - if products: - for product_id, product in products.items(): - product_info = { - "skuproduct": UnitBaseInfo(product) - } - - self.unit_2_facility_dict[product["id"]] = facility_id - - seller = product['seller'] - - if seller is not None: - product_info["seller"] = UnitBaseInfo(seller) - self.unit_2_facility_dict[seller["id"]] = facility_id - - consumer = product["consumer"] - - if consumer is not None: - product_info["consumer"] = UnitBaseInfo(consumer) - self.unit_2_facility_dict[consumer["id"]] = facility_id - - manufacture = product["manufacture"] - - if manufacture is not None: - product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"] - ] = facility_id - - self.facility_levels[facility_id][product_id] = product_info - - for unit in (seller, consumer, manufacture, product): - if unit is not None: - self._unit2product_mapping[unit["id"]] = ( - product["id"], - facility_id, - seller["id"] if seller is not None else None, - consumer["id"] if consumer is not None else None, - manufacture["id"] if manufacture is not None else None - ) - - # create initial state structure - self._build_init_state() - - def _build_init_state(self): - # we will build the final state with default and const values, - # then update dynamic part per step - for agent_info in self._agent_list: - state = {} - - facility = self.facility_levels[agent_info.facility_id] - - # global features - state["global_time"] = 0 - - # facility features - state["facility"] = None - state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] - state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] - state['constraint_idx'] = [0] - state['facility_id'] = [0] * self._sku_number - state['sku_info'] = {} if agent_info.is_facility else agent_info.sku - state['echelon_level'] = 0 - - state['facility_info'] = facility['config'] - state["is_positive_balance"] = 0 - - if not agent_info.is_facility: - state['facility_id'][agent_info.sku.id] = 1 - - for atom_name in atoms.keys(): - state[atom_name] = list( - np.ones(self._configs.settings['constraint_state_hist_len'])) - - # storage features - state['storage_levels'] = [0] * self._sku_number - state['storage_capacity'] = facility['storage'].config["capacity"] - state['storage_utilization'] = 0 - - # bom features - state['bom_inputs'] = [0] * self._sku_number - state['bom_outputs'] = [0] * self._sku_number - - if not agent_info.is_facility: - state['bom_inputs'][agent_info.sku.id] = 1 - state['bom_outputs'][agent_info.sku.id] = 1 - - # vlt features - sku_list = self._summary["skus"] - current_source_list = [] - - if agent_info.sku is not None: - current_source_list = facility["upstreams"].get( - agent_info.sku.id, []) - - state['vlt'] = [0] * \ - (self._max_sources_per_facility * self._sku_number) - state['max_vlt'] = 0 - - if not agent_info.is_facility: - # only for sku product - product_info = facility[agent_info.sku.id] - - if "consumer" in product_info and len(current_source_list) > 0: - state['max_vlt'] = product_info["skuproduct"]["max_vlt"] - - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - # NOTE: different with original code, our config can make sure that source has product we need - - if sku.id == agent_info.sku.id: - state['vlt'][i * len(sku_list) + j + - 1] = facility["skus"][sku.id].vlt - - # sale features - settings = self.env.configs.settings - hist_len = settings['sale_hist_len'] - consumption_hist_len = settings['consumption_hist_len'] - - state['sale_mean'] = 1.0 - state['sale_std'] = 1.0 - state['sale_gamma'] = 1.0 - state['service_level'] = 0.95 - state['total_backlog_demand'] = 0 - - state['sale_hist'] = [0] * hist_len - state['backlog_demand_hist'] = [0] * hist_len - state['consumption_hist'] = [0] * consumption_hist_len - state['pending_order'] = [0] * settings['pending_order_len'] - - if not agent_info.is_facility: - state['service_level'] = agent_info.sku.service_level - - product_info = facility[agent_info.sku.id] - - if "seller" in product_info: - state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma - - # distribution features - state['distributor_in_transit_orders'] = 0 - state['distributor_in_transit_orders_qty'] = 0 - - # consumer features - state['consumer_source_export_mask'] = [0] * \ - (self._max_sources_per_facility * self._sku_number) - state['consumer_source_inventory'] = [0] * self._sku_number - state['consumer_in_transit_orders'] = [0] * self._sku_number - - state['inventory_in_stock'] = 0 - state['inventory_in_transit'] = 0 - state['inventory_in_distribution'] = 0 - state['inventory_estimated'] = 0 - state['inventory_rop'] = 0 - state['is_over_stock'] = 0 - state['is_out_of_stock'] = 0 - state['is_below_rop'] = 0 - - if len(current_source_list) > 0: - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - if sku.id == agent_info.sku.id: - state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = \ - self.facility_levels[source]["skus"][sku.id].vlt - - # price features - state['max_price'] = self._max_price - state['sku_price'] = 0 - state['sku_cost'] = 0 - - if not agent_info.is_facility: - state['sku_price'] = agent_info.sku.price - state['sku_cost'] = agent_info.sku.cost - - self._states[agent_info.id] = state - - -class BalanceSheetCalculator: - consumer_features = ("id", "order_quantity", "price", - "order_cost", "order_product_cost", "reward_discount") - seller_features = ("id", "sold", "demand", "price", "backlog_ratio") - manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - product_features = ( - "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty") - storage_features = ("capacity", "remaining_space") - vehicle_features = ("id", "payload", "unit_transport_cost") - - def __init__(self, env: Env): - self.env = env - self.consumer_ss = env.snapshot_list["consumer"] - self.seller_ss = env.snapshot_list["seller"] - self.manufacture_ss = env.snapshot_list["manufacture"] - self.storage_ss = env.snapshot_list["storage"] - self.distribution_ss = env.snapshot_list["distribution"] - self.vehicle_ss = env.snapshot_list["vehicle"] - self.product_ss = env.snapshot_list["product"] - self.products = [] - self.product_id2index_dict = {} - self.facility_levels = [] - self.consumer_id2product = {} - - self.facilities = env.summary["node_mapping"]["facilities"] - - for facility_id, facility in self.facilities.items(): - pid_list = [] - distribution = facility["units"]["distribution"] - - for product_id, product in facility["units"]["products"].items(): - pid_list.append(product["id"]) - consumer = product["consumer"] - if consumer is not None: - self.consumer_id2product[consumer["id"]] = product["id"] - seller = product["seller"] - manufacture = product["manufacture"] - - self.product_id2index_dict[product["id"]] = len(self.products) - - downstream_product_units = [] - downstreams = facility["downstreams"] - - if downstreams and len(downstreams) > 0 and product_id in downstreams: - for dfacility in downstreams[product_id]: - dproducts = self.facilities[dfacility]["units"]["products"] - - downstream_product_units.append(dproducts[product_id]["id"]) - - self.products.append(( - product["id"], - product_id, - product["node_index"], - facility["units"]["storage"]["node_index"], - facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution["node_index"] if distribution is not None else None, - downstream_product_units, - None if consumer is None else (consumer["id"], consumer["node_index"]), - None if seller is None else (seller["id"], seller["node_index"]), - None if manufacture is None else (manufacture["id"], manufacture["node_index"]), - )) - - self.facility_levels.append(( - facility_id, - pid_list, - facility["units"]["storage"]["node_index"], - facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution["node_index"] if distribution is not None else None, - [v["node_index"] for v in distribution["children"]] if distribution is not None else [] - )) - - # TODO: order products make sure calculate reward from downstream to upstream - tmp_product_unit_dict = {} - - for product in self.products: - tmp_product_unit_dict[product[0]] = product - - self._ordered_products = [] - - tmp_stack = [] - - for product in self.products: - # skip if already being processed - if tmp_product_unit_dict[product[0]] is None: - continue - - for dproduct in product[6]: - # push downstream id to stack - tmp_stack.append(dproduct) - - # insert current product to list head - self._ordered_products.insert(0, product) - # mark it as processed - tmp_product_unit_dict[product[0]] = None - - while len(tmp_stack) > 0: - # process downstream of product unit in stack - dproduct_unit_id = tmp_stack.pop() - - # if it was processed then ignore - if tmp_product_unit_dict[dproduct_unit_id] is None: - continue - - # or extract it downstreams - dproduct_unit = tmp_product_unit_dict[dproduct_unit_id] - - dproduct_downstreams = dproduct_unit[6] - - for dproduct in dproduct_downstreams: - tmp_stack.append(dproduct) - - # current unit in final list - self._ordered_products.insert(0, dproduct_unit) - tmp_product_unit_dict[dproduct_unit_id] = None - - self.total_balance_sheet = defaultdict(int) - - # tick -> (product unit id, sku id, manufacture number, manufacture cost, checkin order, delay penaty) - self._supplier_reward_factors = {} - - def calc(self): - tick = self.env.tick - - ## consumer - # consumer_features = ( - # "id", "order_quantity", "price", "order_cost", "order_product_cost", "reward_discount" - # ) - consumer_bs_states = self.consumer_ss[tick::self.consumer_features]\ - .flatten()\ - .reshape(-1, len(self.consumer_features)) - - # quantity * price - order_profit = consumer_bs_states[:, 1] * consumer_bs_states[:, 2] - - # balance_sheet_profit = 0 - # order_cost + order_product_cost - consumer_step_balance_sheet_loss = -1 * (consumer_bs_states[:, 3] + consumer_bs_states[:, 4]) - - # consumer step reward: balance sheet los + profile * discount - # consumer_step_reward = consumer_step_balance_sheet_loss + \ - # order_profit * consumer_bs_states[:, 5] - consumer_step_reward = consumer_step_balance_sheet_loss - - ######################################################################## - - ## seller - # seller_features = ("id", "sold", "demand", "price", "backlog_ratio") - seller_bs_states = self.seller_ss[tick::self.seller_features] \ - .flatten()\ - .reshape(-1, len(self.seller_features)) - - # profit = sold * price - seller_balance_sheet_profit = seller_bs_states[:, 1] * seller_bs_states[:, 3] - - # loss = demand * price * backlog_ratio - seller_balance_sheet_loss = -1 * seller_bs_states[:, 2] * seller_bs_states[:, 3] * seller_bs_states[:, 4] - - # step reward = loss + profit - seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit - - ######################################################################## - - ## manufacture - # manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - man_bs_states = self.manufacture_ss[tick::self.manufacture_features] \ - .flatten()\ - .reshape(-1, len(self.manufacture_features)) - - # loss = manufacture number * cost - man_balance_sheet_profit_loss = -1 * man_bs_states[:, 1] * man_bs_states[:, 2] - - # step reward = loss - man_step_reward = man_balance_sheet_profit_loss - - ######################################################################## - - ## product - # product_features = ( - # "id", "price", "distribution_check_order", "distribution_transport_cost", "distribution_delay_order_penalty" - # ) - product_bs_states = self.product_ss[tick::self.product_features] \ - .flatten()\ - .reshape(-1, len(self.product_features)) - - # product distribution loss = transportation cost + delay order penalty - product_distribution_balance_sheet_loss = -1 * (product_bs_states[:, 3] + product_bs_states[:, 4]) - - # product distribution profit = check order * price - product_distribution_balance_sheet_profit = product_bs_states[:, 2] * product_bs_states[:, 1] - - # result we need - product_step_reward = np.zeros((len(self.products, ))) - product_balance_sheet_profit = np.zeros((len(self.products, ))) - product_balance_sheet_loss = np.zeros((len(self.products, ))) - - # create product number mapping for storages - storages_product_map = {} - for storage_index in range(len(self.storage_ss)): - product_list = self.storage_ss[tick:storage_index:"product_list"] \ - .flatten()\ - .astype(np.int) - - product_number = self.storage_ss[tick:storage_index:"product_number"] \ - .flatten()\ - .astype(np.int) - - storages_product_map[storage_index] = { - pid: pnum for pid, pnum in zip(product_list, product_number) - } - - # product balance sheet and reward - # loss = consumer loss + seller loss + manufacture loss + storage loss + distribution loss + downstreams loss - # profit = same as above - # reward = same as above - for product in self._ordered_products: - id, product_id, i, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = product - - if consumer: - product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer[1]] - product_step_reward[i] += consumer_step_reward[consumer[1]] - - if seller: - product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller[1]] - product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller[1]] - product_step_reward[i] += seller_step_reward[seller[1]] - - if manufacture: - product_balance_sheet_loss[i] += man_balance_sheet_profit_loss[manufacture[1]] - product_step_reward[i] += man_step_reward[manufacture[1]] - - storage_reward = -1 * storages_product_map[storage_index][product_id] * unit_storage_cost - - product_step_reward[i] += storage_reward - - product_balance_sheet_loss[i] += storage_reward - - if distribution_index is not None: - product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[i] - product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[i] - - product_step_reward[i] += product_distribution_balance_sheet_loss[i] + \ - product_distribution_balance_sheet_profit[i] - - if len(downstreams) > 0: - for did in downstreams: - product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] - product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] - product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] - - product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss - - # storage - storage_states = self.storage_ss[tick::self.storage_features] \ - .flatten()\ - .reshape(-1, len(self.storage_features)) - - # loss = (capacity-remaining space) * cost - storage_balance_sheet_loss = -1 * (storage_states[:, 0] - storage_states[:, 1]) - - # vehicles - vehicle_states = self.vehicle_ss[tick::self.vehicle_features] \ - .flatten()\ - .reshape(-1, len(self.vehicle_features)) - - # loss = cost * payload - vehicle_balance_sheet_loss = -1 * vehicle_states[:, 1] * vehicle_states[:, 2] - - vehicle_step_reward = vehicle_balance_sheet_loss - - facility_balance_sheet_loss = np.zeros((len(self.facility_levels),)) - facility_balance_sheet_profit = np.zeros((len(self.facility_levels),)) - facility_step_reward = np.zeros((len(self.facility_levels),)) - - # for facilities - for i, facility in enumerate(self.facility_levels): - id, pid_list, storage_index, unit_storage_cost, distribution_index, vehicle_indices = facility - - # storage balance sheet - # profit=0 - facility_balance_sheet_loss[i] += storage_balance_sheet_loss[storage_index] * unit_storage_cost - - # distribution balance sheet - if distribution_index is not None: - for vindex in vehicle_indices: - facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vindex] - # distribution unit do not provide reward - - # sku product unit balance sheet - for pid in pid_list: - facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] - facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] - facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] - - # Final result for current tick, key is the facility/unit id, value is tuple of balance sheet and reward. - result = {} - - # For product units. - for id, bs, rw in zip([item[0] for item in self.products], product_balance_sheet, product_step_reward): - result[id] = (bs, rw) - - self.total_balance_sheet[id] += bs - - facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit - - - # For consumers. - consumer_step_balance_sheet = order_profit + consumer_step_balance_sheet_loss - - for id, bs, rw in zip(consumer_bs_states[:, 0], consumer_step_balance_sheet, consumer_step_reward): - # result[int(id)] = (bs, rw) - # let reward of a consumer equate its parent product - result[int(id)] = result[self.consumer_id2product[int(id)]] - self.total_balance_sheet[id] += result[int(id)][0] - - # For producers. - man_step_balance_sheet = man_balance_sheet_profit_loss - - for id, bs, rw in zip(man_bs_states[:, 0], man_step_balance_sheet, man_step_reward): - result[int(id)] = (bs, rw) - - self.total_balance_sheet[id] += bs - - # NOTE: add followings if you need. - # For storages. - # For distributions. - # For vehicles. - - return result - - -if __name__ == "__main__": - from time import time - import cProfile - - env = Env( - scenario="supply_chain", - topology="sample", - durations=100, - max_snapshots=10) - - ss = SCEnvWrapper(env) - - env.step(None) - - start_time = time() - - # cProfile.run("ss.get_state(None)", sort="cumtime") - states = ss.get_state(None) - print(env.agent_idx_list) - print(ss.cur_balance_sheet_reward) - print(states) - - # end_time = time() - # - # print("time cost:", end_time - start_time) - # - # print("dim:", ss.dim) diff --git a/examples/supply_chain/evaluation_with_render.py b/examples/supply_chain/evaluation_with_render.py deleted file mode 100644 index 23469f557..000000000 --- a/examples/supply_chain/evaluation_with_render.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -from os import getcwd - -from maro.rl import Actor, LocalLearner - -from examples.supply_chain.render_tools import SimulationTracker -from maro.rl.training.message_enums import MsgKey - - -EPISODE_LEN = 60 -N_EPISODE = 1 -FACILITY_TYPES = ["productstore"] - - -class RenderActor(Actor): - def __init__( - self, env, policies, agent2policy, group, exploration_dict=None, - agent2exploration=None, eval_env=None, log_dir=getcwd(), **proxy_kwargs - ): - super().__init__( - env, policies, agent2policy, group, exploration_dict=exploration_dict, agent2exploration=agent2exploration, - eval_env=eval_env, log_dir=log_dir, **proxy_kwargs - ) - self._log_dir = log_dir - - def _evaluate(self, msg): - log_dir = os.path.join(self._log_dir, f"ep_{msg.body[MsgKey.EPISODE_INDEX]}", self._proxy._name) - tracker = SimulationTracker( - episode_len=EPISODE_LEN, - n_episodes=N_EPISODE, - env=self.eval_env, - policies=self.policy, - log_dir=log_dir, - logger_name=f"SimulationTracker.{self._proxy._name}" - ) - tracker.run_and_render(facility_types=FACILITY_TYPES) - - return super()._evaluate(msg) - - -class RenderLocalLearner(LocalLearner): - def __init__( - self, env, policies, agent2policy, num_episodes, num_steps=-1, exploration_dict=None, agent2exploration=None, - eval_schedule=None, eval_env=None, early_stopper=None, log_env_summary=True, log_dir=getcwd() - ): - super().__init__( - env, policies, agent2policy, num_episodes, num_steps=num_steps, exploration_dict=exploration_dict, - agent2exploration=agent2exploration, eval_schedule=eval_schedule, eval_env=eval_env, - early_stopper=early_stopper, log_env_summary=log_env_summary, log_dir=log_dir - ) - self._log_dir = log_dir - - def run(self): - """Entry point for executing a learning workflow.""" - for ep in range(1, self.num_episodes + 1): - self._train(ep) - if ep == self._eval_schedule[self._eval_point_index]: - self._eval_point_index += 1 - self._evaluate() - self._run_and_render(ep) - # early stopping check - if self.early_stopper: - self.early_stopper.push(self.eval_env.summary) - if self.early_stopper.stop(): - return - - def _run_and_render(self, ep: int): - log_dir = os.path.join(self._log_dir, f"ep_{ep}") - tracker = SimulationTracker( - episode_len=EPISODE_LEN, - n_episodes=N_EPISODE, - env=self.eval_env, - policies=self._policy, - log_dir=log_dir, - logger_name="SimulationTracker" - ) - tracker.run_and_render(facility_types=FACILITY_TYPES) - diff --git a/examples/supply_chain/exploration.py b/examples/supply_chain/exploration.py deleted file mode 100644 index 27edb4f51..000000000 --- a/examples/supply_chain/exploration.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.rl import EpsilonGreedyExploration, LinearExplorationScheduler - -def get_exploration_mapping(config) -> (dict, dict): - exploration = EpsilonGreedyExploration( - num_actions=config["policy"]["consumer"]["model"]["network"]["output_dim"] - ) - exploration.register_schedule( - scheduler_cls=LinearExplorationScheduler, - param_name="epsilon", - last_ep=config["exploration"]["last_ep"], - initial_value=config["exploration"]["initial_value"], - final_value=config["exploration"]["final_value"] - ) - - exploration_dict = {"consumerstore": exploration} - agent2exploration = { - agent_id: "consumerstore" - for agent_id in config["agent_id_list"] if agent_id.startswith("consumerstore") - } - - return exploration_dict, agent2exploration diff --git a/examples/supply_chain/main.py b/examples/supply_chain/main.py deleted file mode 100644 index 9e39209d8..000000000 --- a/examples/supply_chain/main.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import logging -import os -import sys -from argparse import ArgumentParser -from multiprocessing import Process - -import yaml - -from maro.rl import Learner, LocalPolicyManager, ParallelRolloutManager -from maro.simulator import Env -from maro.simulator.scenarios.supply_chain.world import AgentInfo -from maro.utils import set_seeds - -from examples.supply_chain.env_wrapper import SCEnvWrapper -from examples.supply_chain.evaluation_with_render import RenderActor, RenderLocalLearner -from examples.supply_chain.exploration import get_exploration_mapping -from examples.supply_chain.policies import get_policy_mapping, get_replay_agent_ids - - -# logging.basicConfig(level=logging.DEBUG) - -SC_CODE_DIR = os.path.dirname(os.path.realpath(__file__)) -config_path = os.getenv("CONFIG_PATH", default=os.path.join(SC_CODE_DIR, "config.yml")) -with open(config_path, "r") as config_file: - CONFIG = yaml.safe_load(config_file) -LOG_DIR = os.path.join(SC_CODE_DIR, "logs", CONFIG["experiment_name"]) -OUTPUT_DIR = os.path.join(LOG_DIR, "output") - -# AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id", "parent_id")) -def agent_info_to_agent_id(info: AgentInfo) -> str: - return f"{info.agent_type}.{info.id}" - -def single_thread_mode(config, env_wrapper): - exploration_dict, agent2exploration = get_exploration_mapping(config) - policies, agent2policy = get_policy_mapping(config) - - # create a learner to start training - learner = RenderLocalLearner( - env=env_wrapper, - policies=policies, - agent2policy=agent2policy, - num_episodes=config["num_episodes"], - num_steps=-1, - exploration_dict=exploration_dict, - agent2exploration=agent2exploration, - eval_schedule=config["eval_schedule"], - eval_env=None, - early_stopper=None, - log_env_summary=config["log_env_summary"], - log_dir=LOG_DIR - ) - learner.run() - -def sc_learner(config): - policies, _ = get_policy_mapping(config) - - policy_manager = LocalPolicyManager(policies=policies, log_dir=LOG_DIR) - - rollout_manager = ParallelRolloutManager( - num_actors=config["distributed"]["num_actors"], - group=config["distributed"]["group"], - num_steps=-1, - max_receive_attempts=None, - receive_timeout=None, - max_staleness=0, - num_eval_actors=1, - log_env_summary=True, - log_dir=LOG_DIR, - component_name="rollout_manager", - redis_address=(config["distributed"]["redis_host"], config["distributed"]["redis_port"]), - ) - - learner = Learner( - policy_manager=policy_manager, - rollout_manager=rollout_manager, - num_episodes=config["num_episodes"], - eval_schedule=config["eval_schedule"], - early_stopper=None, - log_dir=LOG_DIR - ) - learner.run() - -def sc_actor(component_name: str, config, env_wrapper): - policies, agent2policy = get_policy_mapping(config) - exploration_dict, agent2exploration = get_exploration_mapping(config) - actor = RenderActor( - env=env_wrapper, - policies=policies, - agent2policy=agent2policy, - group=config["distributed"]["group"], - exploration_dict=exploration_dict, - agent2exploration=agent2exploration, - eval_env=SCEnvWrapper(Env(**config["env"]), replay_agent_ids=config["replay_agent_ids"]), - log_dir=LOG_DIR, - component_name=component_name, - redis_address=(config["distributed"]["redis_host"], config["distributed"]["redis_port"]), - ) - actor.run() - -def multi_process_mode(config): - actor_processes = [ - Process( - target=sc_actor, - args=(f"actor_{i + 1}", config, SCEnvWrapper(Env(**config["env"]), replay_agent_ids=config["replay_agent_ids"]), ) - ) - for i in range(config["distributed"]["num_actors"]) - ] - learner_process = Process(target=sc_learner, args=(config, )) - - for i, actor_process in enumerate(actor_processes): - set_seeds(i) - actor_process.start() - learner_process.start() - - for actor_process in actor_processes: - actor_process.join() - learner_process.join() - - -if __name__ == "__main__": - CONFIG["env"]["topology"] = os.path.join(SC_CODE_DIR, "topologies", CONFIG["env"]["topology"]) - - env = Env(**CONFIG["env"]) - CONFIG["agent_id_list"] = [agent_info_to_agent_id(info) for info in env.agent_idx_list] - CONFIG["replay_agent_ids"] = get_replay_agent_ids(CONFIG["agent_id_list"]) - env_wrapper = SCEnvWrapper(env, replay_agent_ids=CONFIG["replay_agent_ids"]) - - CONFIG["policy"]["consumer"]["model"]["network"]["input_dim"] = env_wrapper.dim - CONFIG["policy"]["producer"]["model"]["network"]["input_dim"] = env_wrapper.dim - CONFIG["policy"]["consumerstore"]["model"]["network"]["input_dim"] = env_wrapper.dim - - parser = ArgumentParser() - parser.add_argument( - "-w", "--whoami", type=int, choices=[-1, 0, 1, 2], default=0, - help="Identify of this process: -1 - single thread mode, 0 - multi-process mode, 1 - learner, 2 - actor" - ) - args = parser.parse_args() - - if args.whoami == -1: - single_thread_mode(CONFIG, env_wrapper) - elif args.whoami == 0: - if CONFIG["distributed"]["redis_host"] != "localhost": - CONFIG["distributed"]["redis_host"] = "localhost" - print(f"******** [multi-process mode] automatically change the 'redis_host' field to 'localhost' ********") - multi_process_mode(CONFIG) - elif args.whoami == 1: - sc_learner(CONFIG) - elif args.whoami == 2: - component_name = os.getenv("COMPONENT") - sc_actor(component_name, CONFIG, env_wrapper) diff --git a/examples/supply_chain/or_policy/base_policy.py b/examples/supply_chain/or_policy/base_policy.py deleted file mode 100644 index bcad01a89..000000000 --- a/examples/supply_chain/or_policy/base_policy.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import random - -import numpy as np -import scipy.stats as st - -from maro.rl.policy import AbsPolicy - - -class ProducerBaselinePolicy(AbsPolicy): - - def __init__(self, name: str, config): - super(ProducerBaselinePolicy, self).__init__(name=name) - self.config = config - # self.num_actions = config["model"]["network"]["output_dim"] - - def choose_action(self, state): - return state.get('product_rate', 500) - - -class ConsumerBaselinePolicy(AbsPolicy): - - def __init__(self, name: str, config: dict): - super(ConsumerBaselinePolicy, self).__init__(name=name) - self.config = config - self.num_actions = config["model"]["network"]["output_dim"] - - def choose_action(self, state): - if state['is_facility']: - return 0 - # consumer_source_inventory - available_inventory = np.array(state['storage_levels']) - inflight_orders = np.array(state['consumer_in_transit_orders']) - booked_inventory = available_inventory + inflight_orders - - # stop placing orders when the facility runs out of capacity - if np.sum(booked_inventory) > state['storage_capacity']: - return 0 - - most_needed_product_id = state['product_idx'] - sale_mean, sale_std = state['sale_mean'], state['sale_std'] - service_level = state['service_level'] - vlt_buffer_days = 7 - vlt = vlt_buffer_days + state['vlt'] - if booked_inventory[most_needed_product_id] > vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level): - return 0 - consumer_action_space_size = self.num_actions - consumer_quantity = random.randint(0, consumer_action_space_size-1) - return consumer_quantity diff --git a/examples/supply_chain/or_policy/eoq_policy.py b/examples/supply_chain/or_policy/eoq_policy.py deleted file mode 100644 index 3bb092042..000000000 --- a/examples/supply_chain/or_policy/eoq_policy.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import random as rnd - -import numpy as np -import scipy.stats as st - -from examples.supply_chain.or_policy.base_policy import ConsumerBaselinePolicy - - -# Q = \sqrt{2DK/h} -# Q - optimal order quantity -# D - annual demand quantity -# K - fixed cost per order, setup cost (not per unit, typically cost of ordering and shipping and handling. This is not the cost of goods) -# h - annual holding cost per unit, -# also known as carrying cost or storage cost (capital cost, warehouse space, -# refrigeration, insurance, etc. usually not related to the unit production cost) -class ConsumerEOQPolicy(ConsumerBaselinePolicy): - - def __init__(self, name: str, config: dict): - super(ConsumerEOQPolicy, self).__init__(name=name, config=config) - - def _get_consumer_quantity(self, state): - order_cost = state['order_cost'] - holding_cost = state['unit_storage_cost'] - sale_gamma = state['sale_mean'] - consumer_quantity = int(np.sqrt(2*sale_gamma*order_cost / holding_cost) / sale_gamma) - return consumer_quantity - - def choose_action(self, state): - if state['is_facility']: - return 0 - # consumer_source_inventory - available_inventory = np.array(state['storage_levels']) - inflight_orders = np.array(state['consumer_in_transit_orders']) - booked_inventory = available_inventory + inflight_orders - - # stop placing orders when the facilty runs out of capacity - if np.sum(booked_inventory) > state['storage_capacity']: - return 0 - - most_needed_product_id = state['product_idx'] - vlt_buffer_days = 7 - vlt = vlt_buffer_days + state['vlt'] - sale_mean, sale_std = state['sale_mean'], state['sale_std'] - service_level = state['service_level'] - - # whether replenishment point is reached - if booked_inventory[most_needed_product_id] > vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level): - return 0 - consumer_quantity = self._get_consumer_quantity(state) - return consumer_quantity diff --git a/examples/supply_chain/or_policy/minmax_policy.py b/examples/supply_chain/or_policy/minmax_policy.py deleted file mode 100644 index c4dfd7fde..000000000 --- a/examples/supply_chain/or_policy/minmax_policy.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import random as rnd - -import numpy as np -import scipy.stats as st - -from examples.supply_chain.or_policy.base_policy import ConsumerBaselinePolicy - - -# parameters: (r, R), calculate according to VLT, demand variances, and service level -# replenish R - S units whenever the current stock is less than r -# S denotes the number of units in stock -class ConsumerMinMaxPolicy(ConsumerBaselinePolicy): - - def __init__(self, name:str, config: dict): - super(ConsumerMinMaxPolicy, self).__init__(name=name, config=config) - - def choose_action(self, state): - if state['is_facility']: - return 0 - # consumer_source_inventory - available_inventory = np.array(state['storage_levels']) - inflight_orders = np.array(state['consumer_in_transit_orders']) - booked_inventory = available_inventory + inflight_orders - - # stop placing orders when the facility runs out of capacity - if np.sum(booked_inventory) > state['storage_capacity']: - return 0 - - most_needed_product_id = state['product_idx'] - # stop placing orders if no risk of out of stock - vlt_buffer_days = 10 - vlt = state['vlt'] + vlt_buffer_days - sale_mean, sale_std = state['sale_mean'], state['sale_std'] - service_level = state['service_level'] - r = (vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level)) - print(booked_inventory, most_needed_product_id, r) - if booked_inventory[most_needed_product_id] > r: - return 0 - R = 3*r - consumer_quantity = int((R - r) / sale_mean) - return consumer_quantity diff --git a/examples/supply_chain/policies.py b/examples/supply_chain/policies.py deleted file mode 100644 index 0d134c313..000000000 --- a/examples/supply_chain/policies.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os.path import dirname, realpath -from typing import List - -import numpy as np -import torch - -from maro.rl import ( - DQN, DQNConfig, FullyConnectedBlock, NullPolicy, OptimOption, DiscreteQNet, ExperienceManager -) - -from examples.supply_chain.or_policy.minmax_policy import ConsumerMinMaxPolicy -from examples.supply_chain.or_policy.eoq_policy import ConsumerEOQPolicy -from examples.supply_chain.or_policy.base_policy import ProducerBaselinePolicy - - -class SimpleQNet(DiscreteQNet): - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component(states) - - -def get_dqn_policy(name: str, cfg): - q_net = SimpleQNet( - FullyConnectedBlock(**cfg["model"]["network"]), - optim_option=OptimOption(**cfg["model"]["optimization"]), - device=cfg["model"]["device"] - ) - dqn_policy = DQN( - name=name, - q_net=q_net, - experience_manager=ExperienceManager(**cfg["experience_manager"], sampler_cls=None), - config=DQNConfig(**cfg["algorithm_config"]) - ) - return dqn_policy - -def get_base_consumer_policy(name: str, config: dict): - return ConsumerMinMaxPolicy(name, config) - -def get_eoq_consumer_policy(name: str, config: dict): - return ConsumerEOQPolicy(name, config) - -def get_base_producer_policy(name: str, config: dict): - return ProducerBaselinePolicy(name, config) - -def get_policy_mapping(config) -> (list, dict): - # policy_ids = ["consumerstore", "consumer", "producer", "facility", "product", "productstore"] - policies = [ - get_base_consumer_policy("consumer", config["policy"]["consumer"]), - get_base_producer_policy("producer", config["policy"]["producer"]), - get_dqn_policy("consumerstore", config["policy"]["consumerstore"]), - NullPolicy(name="facility"), - NullPolicy(name="product"), - NullPolicy(name="productstore") - ] - - agent2policy = {agent_id: agent_id.split(".")[0] for agent_id in config["agent_id_list"]} - return policies, agent2policy - -def get_replay_agent_ids(agent_id_list) -> List[str]: - replay_agent_ids = [agent_id for agent_id in agent_id_list if agent_id.startswith("consumerstore")] - return replay_agent_ids diff --git a/examples/supply_chain/render_tools.py b/examples/supply_chain/render_tools.py deleted file mode 100644 index a03bdfb2d..000000000 --- a/examples/supply_chain/render_tools.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import matplotlib.pyplot as plt -import numpy as np -import os - -from maro.utils import Logger - -from examples.supply_chain.env_wrapper import sku_agent_types, SCEnvWrapper - - -class SimulationTracker: - def __init__(self, episode_len: int, n_episodes: int, env: SCEnvWrapper, policies: dict, log_dir="./", logger_name=None): - self.episode_len = episode_len - self.n_episodes = n_episodes - self.env = env - self.policies = policies - self.log_dir = log_dir - - self.logger = Logger(logger_name if logger_name else "SimulationTracker", dump_folder=self.log_dir) - - self.global_balances = np.zeros((n_episodes, episode_len)) - self.global_rewards = np.zeros((n_episodes, episode_len)) - - self.facility_names = [] - for agent in self.env._agent_list: - if agent.agent_type in sku_agent_types: - self.facility_names.append(agent) - - self.step_balances = np.zeros( - (n_episodes, self.episode_len, len(self.facility_names))) - self.step_rewards = np.zeros( - (n_episodes, self.episode_len, len(self.facility_names))) - - self.sku_to_track = None - self.stock_status = None - self.stock_in_transit_status = None - self.reward_status = None - self.demand_status = None - self.reward_discount_status = None - self.order_to_distribute = None - - def _add_sample(self, episode, t, global_balance, global_reward, step_balances, step_rewards): - self.global_balances[episode, t] = global_balance - self.global_rewards[episode, t] = global_reward - for i, f in enumerate(self.facility_names): - self.step_balances[episode, t, i] = step_balances[f.id] - self.step_rewards[episode, t, i] = step_rewards[f.id] - - def _add_sku_status(self, episode, t, stock, order_in_transit, demands, rewards, balances, order_to_distribute): - if self.sku_to_track is None: - self.sku_to_track = list(rewards.keys()) - self.stock_status = np.zeros( - (self.n_episodes, self.episode_len, len(self.sku_to_track))) - self.stock_in_transit_status = np.zeros( - (self.n_episodes, self.episode_len, len(self.sku_to_track))) - self.demand_status = np.zeros( - (self.n_episodes, self.episode_len, len(self.sku_to_track))) - self.reward_status = np.zeros( - (self.n_episodes, self.episode_len, len(self.sku_to_track))) - self.balance_status = np.zeros( - (self.n_episodes, self.episode_len, len(self.sku_to_track))) - self.order_to_distribute = np.zeros( - (self.n_episodes, self.episode_len, len(self.sku_to_track))) - for i, sku_name in enumerate(self.sku_to_track): - self.stock_status[episode, t, i] = stock[sku_name] - self.stock_in_transit_status[episode, t, i] = order_in_transit[sku_name] - self.demand_status[episode, t, i] = demands[sku_name] - self.reward_status[episode, t, i] = rewards[sku_name] - self.balance_status[episode, t, i] = balances[sku_name] - self.order_to_distribute[episode, t, i] = order_to_distribute[sku_name] - - def _render_sku(self, loc_path): - sku_name_dict = {} - facility_type_dict = {} - for agent in self.env._agent_list: - if agent.is_facility: - sku_name = f"{agent.facility_id}_{agent.agent_type}" - else: - sku_name = f"{agent.id}_{agent.sku.id}_{agent.agent_type}" - sku_name_dict[agent.id] = sku_name - facility_type_dict[agent.id] = self.env.env.summary["node_mapping"]["facilities"][agent.facility_id]['class'].__name__ - - for i, sku_name in enumerate(self.sku_to_track): - x = np.linspace(0, self.episode_len, self.episode_len) - stock = self.stock_status[0, :, i] - order_in_transit = self.stock_in_transit_status[0, :, i] - demand = self.demand_status[0, :, i] - reward = self.reward_status[0, :, i] - balance = self.balance_status[0, :, i] - order_to_distribute = self.order_to_distribute[0, :, i] - - fig, ax = plt.subplots(3, 1, figsize=(25, 10)) - ax[0].set_title('SKU Stock Status by Episode') - for y_label, y in [ - ('stock', stock), - ('order_in_transit', order_in_transit), - ('order_to_distribute', order_to_distribute) - ]: - ax[0].plot(x, y, label=y_label) - - ax[1].set_title('SKU Reward / Balance Status by Episode') - ax[1].plot(x, balance, label='Balance') - ax_r = ax[1].twinx() - ax_r.plot(x, reward, label='Reward', color='r') - - ax[2].set_title('SKU demand') - ax[2].plot(x, demand, label="Demand") - - fig.legend() - fig.savefig(os.path.join(loc_path, f"{facility_type_dict[sku_name]}_{sku_name_dict[sku_name]}.png")) - plt.close(fig=fig) - - def _render(self, file_name, metrics, facility_types): - fig, axs = plt.subplots(2, 1, figsize=(25, 10)) - x = np.linspace(0, self.episode_len, self.episode_len) - - _agent_list = [] - _step_idx = [] - for i, agent_info in enumerate(self.facility_names): - if agent_info.agent_type in ['productstore']: - _agent_list.append(agent_info.id) - _step_idx.append(i) - _step_metrics = [metrics[0, :, i] for i in _step_idx] - - # axs[0].set_title('Global balance') - # axs[0].plot(x, self.global_balances.T) - - axs[0].set_title('Cumulative Sum') - axs[0].plot(x, np.cumsum(np.sum(_step_metrics, axis=0))) - - axs[1].set_title('Breakdown by Agent (One Episode)') - axs[1].plot(x, np.cumsum(_step_metrics, axis=1).T) - axs[1].legend(_agent_list, loc='upper left') - - fig.savefig(file_name) - plt.close(fig=fig) - # plt.show() - - def _run_wth_render(self, facility_types): - self.env.reset() - self.env.start() - for epoch in range(self.episode_len): - action = {agent_id: self.policies[agent_id].choose_action(st) for agent_id, st in self.env.state.items()} - self.logger.info(f"epoch: {epoch}, action: {action}") - self.env.step(action) - self.logger.info(f"epoch: {epoch}, action: {self.env.to_env_action(action)}") - if hasattr(self.env, "consumer2product"): - self.logger.info(f"consumer2product: {self.env.consumer2product}") - self.env.get_reward() - step_balances = self.env.balance_status - step_rewards = self.env.reward_status - - self._add_sample(0, epoch, sum(step_balances.values()), sum( - step_rewards.values()), step_balances, step_rewards) - stock_status = self.env.stock_status - order_in_transit_status = self.env.order_in_transit_status - demand_status = self.env.demand_status - reward_status = self.env.reward_status - balance_status = self.env.balance_status - order_to_distribute_status = self.env.order_to_distribute_status - - self._add_sku_status(0, epoch, stock_status, - order_in_transit_status, demand_status, - reward_status, balance_status, - order_to_distribute_status) - - _step_idx = [] - for i, agent_info in enumerate(self.facility_names): - if agent_info.agent_type in facility_types: - _step_idx.append(i) - _step_metrics = [self.step_rewards[0, :, i] for i in _step_idx] - _step_metrics_list = np.cumsum(np.sum(_step_metrics, axis=0)) - return np.sum(_step_metrics), _step_metrics_list - - def run_and_render(self, facility_types): - metric, metric_list = self._run_wth_render(facility_types=facility_types) - self._render(os.path.join(self.log_dir, "plot_balance.png"), self.step_balances, facility_types) - self._render(os.path.join(self.log_dir, "plot_reward.png"), self.step_rewards, facility_types) - self._render_sku(self.log_dir) - return metric, metric_list diff --git a/examples/supply_chain/sc_state_in_maro.md b/examples/supply_chain/sc_state_in_maro.md deleted file mode 100644 index 0d512e25d..000000000 --- a/examples/supply_chain/sc_state_in_maro.md +++ /dev/null @@ -1,312 +0,0 @@ -# SC state in MARO - - -## Env.summary - -MARO通过summary属性对外提供节点相关信息和其他在环境初始化后不会改变的信息。 -在supply chain场景中, summary中包括了以下部分 - -### unit mapping: - -```python -env.summary["node_mapping"]["unit_mapping"] -``` - -unit id 及其对应的data model名字和索引. - -### facilities: - -```python -env.summary["node_mapping"]["facilities"] -``` - -每个facility的层次结构,如sku list, units - -### skus: - -```python -env.summary["node_mapping"]["skus"] -``` - -当前配置中的所有sku - -### max_price: - -```python -env.summary["node_mapping"]["max_price"] -``` - -当前配置中最大的price - -### max_sources_per_facility: - -```python -env.summary["node_mapping"]["max_sources_per_facility"] -``` - -## States - -MARO中有两种对外提供动态状态(state)的方法。 - - -### Metrics: - -起初是为了对外提供reward相关的信息,但是也可以用来作为对外提供状态的接口,用来对外提供不能存放在snapshot list中的数据,比如字典,或者复杂的数据结构。 - -当前实现包含了以下内容: - -#### products: - -product unit相关信息(sale_mean, sale_std, pending_order_daily)。 - -#### facilities: - -facility相关信息(in_transit_orders, pending_order) - - -### Snapshot list: - -snapshot list是MARO中主要的对外提供状态的接口,它提供了所有节点的历史状态(默认为保存所有历史记录,可配置保存最新的N个来节省内存).返回的结果是numpy array,适合做batch操作。 - -snapshot list中的属性都是按照节点组织起来的,每个节点包括多个属性,每个属性可以有多个slot(array like), 同一种节点类型可以有多个instance.节点及其属性的定义可以在maro/simulator/scenarios/supply_chain/datamodels查看。 - -snapshot list的查询是通过slice接口实现的,形式如下: - -```python -env.snapshot_list["node name"][tick(s):node index(s):attribute name(s)] -> np.array - -``` - -该接口返回的是一个4维(tick, node, attribute, slot)的numpy数组(float). - -其中: -1. node name是定义节点是通过node装饰器提供的名字,当前实现包括如下节点: - -consumer, distribution, facility, manufacture, product, seller, storage, vehicle - - -2. tick(s): 可以是一个int, list或者None, 其中None表示查询当前所有历史记录的状态。 - -3. node index(s): 同tick,可以为int, list或者None,None表示查询当前节点类型的所有实例(instance). 使用中需要注意的是,节点(data model)的index和unit的id并不相同,unit的id是在facility和unit之间连续且唯一的,但是节点的index是每种data model类型内部的索引方式。 -所以在实际使用过程中,通常需要得到每个unit和facility对应的index,这部分信息在env.summary中可以得到。 - -4. attribute name(s): 在对应节点上定义过的属性名,可以为一个str,或者List[str] - - -## 示例 - -### 示例1:通过env.summary构建facility&unit的层级信息 - -这个层级信息可以帮我们在之后的操作中快速索引。 -更详细的可以参考examples/supply_chain/env_wrapper.py, _build_internal_helpers方法。 - -```python -# unit info -class UnitBaseInfo: - id: int = None - node_index: int = None - config: dict = None - summary: dict = None - - def __init__(self, unit_summary): - self.id = unit_summary["id"] - self.node_index = unit_summary["node_index"] - self.config = unit_summary.get("config", {}) - self.summary = unit_summary - - def __getitem__(self, key, default=None): - if key in self.summary: - return self.summary[key] - - return default - -# facility id -> { -# data_model_index: int, -# storage: UnitBaseInfo -# distribution: UnitBaseInfo -# product_id: { -# consumer: UnitBaseInfo -# seller: UnitBaseInfo -# manufacture: UnitBaseInfo -# } -#} -facility_levels = {} - -# 默认env.summary包含node_mapping, node_detail 和 event_payload3个部分, -# 这里只需要node——mapping -summary = env.summary["node_mapping"] - -for facility_id, facility in summary["facilities"].items(): - facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility["configs"], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } - - # facility所属的unit都挂在units下面。 - units = facility["units"] - - facility_levels[facility_id]["storage"] = UnitBaseInfo(units["storage"]) - facility_levels[facility_id]["distribution"] = UnitBaseInfo(units["distribution"]) - - # 所有的product unit - product_units = units["products"] - - if product_units: - for product_id, product in products.items(): - # product unit 本身也包含state - product_info = { - "product": UnitBaseInfo(product) - } - - # 每个product unit可能包括下面3个unit - # 注意,为了简单我们没有检查对应的key时候存在! - product_info["seller"] = UnitBaseInfo(product["seller"]) - product_info["consumer"] = UnitBaseInfo(product["consumer"]) - product_info["manufacture"] = UnitBaseInfo(product["manufacture"]) - - # 这里我们用product_id作为product 的key,可按照需求更改 - facility_levels[product_id] = product_info -``` - -### 示例2:通过env.summary构建unit id到node index的索引表 - -实际上,在示例的遍历过程中,我们就已经可以得到unit及其对应的node index索引表了,如果你不在意层级关系的话,可以通过unit_mapping快速得到这个索引。 - -```python - -# unit_mapping是一个字典,key是unit id, value是(data model name, data model node index, facility id)类型的tuple。 - -summary = env.summary["node_mapping"] - -unitid2index_mapping = {} - -for unit_id, unit_detail in summary["unit_mapping"].items(): - unitid2index_mapping[unit_id] = unit_detail[1] - -``` - -### 示例3:在state shaping过程中查询seller的销售和需求的历史,时间长度为hist_len - -```python - -# 模拟器当前时间 -cur_tick = env.tick - -# 需要查询的历史长度 -hist_len = 4 - -# 历史长度对象当前时间的时间序列 -ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] - -# 查询seller节点的过去4(含当前)个tick的sold和demand值 -# NOTE:因为这两个是都是整数,所以做一次类型转换 -seller_states =env.snapshot_list["seller"][ticks::("sold", "demand")].astype(np.int) - -# 结果应为4为numpy array -# 假设我们有2个seller -""" -[ - [ - [ - [0.0], # sold (slot = 1) - [0.0] # demand (slot = 1) - ], # seller 0 - [...] # seller 1 - ], # tick 0 - [ - [...], - [...] - ], # tick 1 - [ - [...], - [...] - ], # tick 2 - [ - [...], - [...] - ] # tick 3 (latest) -] -""" - -# 这样得到的结果就是所有的seller unit对应的销售和需求历史。 - -# 假设我们当前需要的seller unit的data model index 为 1 的话。 -cur_seller_node_index = 1 - -# 那么当前seller的销售和需求历史分别为: -cur_seller_hist = seller_states[:, cur_seller_node_index, :] - -# 第二个参数为0,是因为sold是我们查询的第一个属性 -sale_hist = cur_seller_hist[:, 0].flatten() -demand_hist = cur_seller_hist[:, 1].flatten() - -``` - -### 示例4:计算unit或facility的balance sheet - -详细的可以参考examples/supply_chain/env_wrapper.py中的BalanceSheetCalculator类。 - -```python - -# 假设我们需要计算seller, consumer, manufacture的balance sheet. -# 实际情况需要用这3个计算出对应的product unit的balance sheet,这里只是作为示例 - -# 计算所需要属性 -consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") -seller_features = ("id", "sold", "demand", "price", "backlog_ratio") -manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - -# 对应的3种data model snapshot list -consumer_ss = env.snapshot_list["consumer"] -seller_ss = env.snapshot_list["seller"] -manufacture_ss = env.snapshot_list["manufacture"] - -# 当前时间 -tick = env.tick - -# 3种unit对应的所有实例的state -# 这里用len(features)做reshape的原因是,当前用到的属性都是slots=1 -# 又因为我们的tick数量为1,这样reshape之后, 每行对应一个unit的实例,每列对应一个属性 -consumer_states = consumer_ss[tick::consumer_features].flatten().reshape(-1, len(consumer_features)) - -seller_states = seller_ss[tick::seller_features].flatten().reshape(-1, len(seller_features)) - -man_states = manufacture_ss[tick::manufacture_features].flatten().reshape(-1, len(manufacture_features)) - -# balance sheet计算,通常balance sheet 包含profit和loss两部分,这里分开保存。 - -# consumer部分 -# profit = quantity * price -consumer_profit = consumer_states[:, 1] * consumer_states[:, 2] - -# loss = -1 * (order_cost + order_product_cost) -consumer_loss = -1 * (consumer_states[:, 3] + consumer_states[:, 4]) - -# discount在代码里似乎没有用到 -reward_discount = 0 - -# consumer step reward -consumer_reward = consumer_loss + consumer_profit * reward_discount - -# seller部分 -# profit = sold * price -seller_profit = seller_states[:, 1] * seller_states[:, 3] - -# loss = -1 * demand * price * backlog_ratio -seller_loss = -1 * seller_states[:, 2] * seller_states[:, 3] * seller_states[:, 4] - -seller_reward = seller_profit + seller_loss - -# manufacture部分 -# profit = 0 -# loss = manufacture_number * cost -man_loss = -1 * man_states[:, 1] * man_states[:, 2] - -man_reward = man_loss - -# 这样我们就用numpy的batch操作完成3种unit的balance sheet和reward计算, -# 后续需要我们按照product/facility对这些结果做聚合, 这需要类似示例1这样的层级结构,具体可参考现有代码。 - -``` \ No newline at end of file diff --git a/examples/supply_chain/scripts/build.sh b/examples/supply_chain/scripts/build.sh deleted file mode 100644 index 6f7a6522f..000000000 --- a/examples/supply_chain/scripts/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -BASEDIR=$(dirname "$0") -ROOTDIR=$BASEDIR/../../../ - -# script to build the docker image for running the supply chain scenario. -docker pull redis:6 -docker build -f $ROOTDIR/docker_files/dev.df -t maro-sc:latest $ROOTDIR diff --git a/examples/supply_chain/scripts/docker_compose_yml_generator.py b/examples/supply_chain/scripts/docker_compose_yml_generator.py deleted file mode 100644 index 9a1702def..000000000 --- a/examples/supply_chain/scripts/docker_compose_yml_generator.py +++ /dev/null @@ -1,49 +0,0 @@ -import yaml -from copy import deepcopy -from os import makedirs -from os.path import dirname, join, realpath - - -path = realpath(__file__) -script_dir = dirname(path) -sc_code_dir = dirname(script_dir) -root_dir = dirname(dirname(sc_code_dir)) -maro_rl_dir = join(root_dir, "maro", "rl") -maro_sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") -config_path = join(sc_code_dir, "config.yml") -dockerfile_path = join(root_dir, "docker_files", "dev.df") - -with open(config_path, "r") as fp: - config = yaml.safe_load(fp) - num_actors = config["distributed"]["num_actors"] - redis_host = config["distributed"]["redis_host"] - -docker_compose_manifest = { - "version": "3.9", - "services": { - "redis": {"image": "redis:6", "container_name": redis_host}, - "learner": { - "build": {"context": root_dir, "dockerfile": dockerfile_path}, - "image": "maro-sc", - "container_name": "learner", - "volumes": [ - f"{sc_code_dir}:/maro/examples/supply_chain", - f"{maro_rl_dir}:/maro/maro/rl", - f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" - ], - "command": ["python3", "/maro/examples/supply_chain/main.py", "-w", "1"] - } - } -} - -for i in range(num_actors): - actor_id = f"actor.{i}" - actor_manifest = deepcopy(docker_compose_manifest["services"]["learner"]) - del actor_manifest["build"] - actor_manifest["command"][-1] = "2" - actor_manifest["container_name"] = actor_id - actor_manifest["environment"] = [f"COMPONENT={actor_id}"] - docker_compose_manifest["services"][actor_id] = actor_manifest - -with open(join(sc_code_dir, "docker-compose.yml"), "w") as fp: - yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/supply_chain/scripts/kill.sh b/examples/supply_chain/scripts/kill.sh deleted file mode 100644 index a2716efbc..000000000 --- a/examples/supply_chain/scripts/kill.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -BASEDIR=$(dirname "$0") - -# script to kill a previously launcher supply chain training job. -docker-compose -f $BASEDIR/../docker-compose.yml down diff --git a/examples/supply_chain/scripts/run.sh b/examples/supply_chain/scripts/run.sh deleted file mode 100644 index 92ca64344..000000000 --- a/examples/supply_chain/scripts/run.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -BASEDIR=$(dirname "$0") - -# script to run the supply chain scenario in single-host multi-container mode. -python3 $BASEDIR/docker_compose_yml_generator.py -docker-compose -f $BASEDIR/../docker-compose.yml up diff --git a/examples/supply_chain/topologies/random/config.yml b/examples/supply_chain/topologies/random/config.yml deleted file mode 100644 index f4f2af16c..000000000 --- a/examples/supply_chain/topologies/random/config.yml +++ /dev/null @@ -1,29344 +0,0 @@ -facility_definitions: - RetailerFacility: - children: - products: - class: StoreProductUnit - config: - agent_type: product - consumer: - class: ConsumerUnit - config: consumer - seller: - class: SellerUnit - config: - sale_hist_len: 4 - is_template: true - storage: - class: StorageUnit - class: RetailerFacility - config: - agent_type: facility - SupplierFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: product - consumer: - class: ConsumerUnit - config: - agent_type: consumer - manufacture: - class: ManufactureUnit - config: - agent_type: producer - is_template: true - storage: - class: StorageUnit - class: SupplierFacility - config: - agent_type: facility - WarehouseFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: product - consumer: - class: ConsumerUnit - config: - agent_type: consumer - is_template: true - storage: - class: StorageUnit - class: WarehouseFacility - config: - agent_type: facility -normal_vehicle: &id001 - class: VehicleUnit - config: - patient: 100 - unit_transport_cost: 1 -settings: - constraint_state_hist_len: 4 - constraint_violate_reward: -1000000.0 - consumption_hist_len: 4 - downsampling_rate: 1 - episod_duration: 21 - gamma: 0.99 - global_reward_weight_consumer: 0.5 - global_reward_weight_producer: 0.5 - heading_timesteps: 7 - initial_balance: 100000 - pending_order_len: 4 - replenishment_discount: 0.9 - reward_normalization: 10000000.0 - sale_hist_len: 4 - tail_timesteps: 7 - total_echelons: 3 -world: - facilities: - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 200 - definition_ref: SupplierFacility - name: SUPPLIER0 - skus: - SKU0: - cost: 289 - init_stock: 3150 - price: 322 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU1: - cost: 255 - init_stock: 1100 - price: 284 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU10: - cost: 61 - init_stock: 1250 - price: 68 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU100: - cost: 14 - init_stock: 3250 - price: 16 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU101: - cost: 75 - init_stock: 3550 - price: 84 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU102: - cost: 295 - init_stock: 4650 - price: 328 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU103: - cost: 300 - init_stock: 4750 - price: 334 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU104: - cost: 44 - init_stock: 3250 - price: 49 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU105: - cost: 99 - init_stock: 2850 - price: 110 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU106: - cost: 225 - init_stock: 3650 - price: 251 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU107: - cost: 380 - init_stock: 4350 - price: 423 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU108: - cost: 412 - init_stock: 4600 - price: 458 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU109: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU11: - cost: 360 - init_stock: 2550 - price: 400 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU110: - cost: 59 - init_stock: 700 - price: 66 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU111: - cost: 234 - init_stock: 3050 - price: 260 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU112: - cost: 54 - init_stock: 4750 - price: 61 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU113: - cost: 313 - init_stock: 1550 - price: 348 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU114: - cost: 350 - init_stock: 1350 - price: 389 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU115: - cost: 257 - init_stock: 4300 - price: 286 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU116: - cost: 446 - init_stock: 3600 - price: 496 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU117: - cost: 288 - init_stock: 4600 - price: 320 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU118: - cost: 164 - init_stock: 1650 - price: 183 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU119: - cost: 188 - init_stock: 1600 - price: 209 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU12: - cost: 100 - init_stock: 4200 - price: 112 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU120: - cost: 108 - init_stock: 4900 - price: 121 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU121: - cost: 36 - init_stock: 4250 - price: 40 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU122: - cost: 393 - init_stock: 350 - price: 437 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU123: - cost: 209 - init_stock: 950 - price: 233 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU124: - cost: 163 - init_stock: 1800 - price: 182 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU125: - cost: 14 - init_stock: 4600 - price: 16 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU126: - cost: 32 - init_stock: 1950 - price: 36 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU127: - cost: 195 - init_stock: 1550 - price: 217 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU128: - cost: 148 - init_stock: 950 - price: 165 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU129: - cost: 128 - init_stock: 2500 - price: 143 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU13: - cost: 285 - init_stock: 2850 - price: 317 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU130: - cost: 313 - init_stock: 4900 - price: 348 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU131: - cost: 57 - init_stock: 2250 - price: 64 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU132: - cost: 384 - init_stock: 1050 - price: 427 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU133: - cost: 201 - init_stock: 1450 - price: 224 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU134: - cost: 302 - init_stock: 3850 - price: 336 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU135: - cost: 137 - init_stock: 5000 - price: 153 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU136: - cost: 179 - init_stock: 3550 - price: 199 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU137: - cost: 83 - init_stock: 3700 - price: 93 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU138: - cost: 205 - init_stock: 1800 - price: 228 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU139: - cost: 186 - init_stock: 1200 - price: 207 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU14: - cost: 241 - init_stock: 3100 - price: 268 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU140: - cost: 234 - init_stock: 1700 - price: 261 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU141: - cost: 171 - init_stock: 2050 - price: 190 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU142: - cost: 288 - init_stock: 1900 - price: 320 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU143: - cost: 286 - init_stock: 1300 - price: 318 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU144: - cost: 360 - init_stock: 600 - price: 400 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU145: - cost: 359 - init_stock: 4100 - price: 399 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU146: - cost: 159 - init_stock: 2400 - price: 177 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU147: - cost: 424 - init_stock: 2800 - price: 472 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU148: - cost: 281 - init_stock: 3850 - price: 313 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU149: - cost: 321 - init_stock: 3850 - price: 357 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU15: - cost: 46 - init_stock: 250 - price: 52 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU150: - cost: 95 - init_stock: 3500 - price: 106 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU151: - cost: 200 - init_stock: 3650 - price: 223 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU152: - cost: 9 - init_stock: 2550 - price: 10 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU153: - cost: 396 - init_stock: 3100 - price: 441 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU154: - cost: 69 - init_stock: 4250 - price: 77 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU155: - cost: 379 - init_stock: 2650 - price: 422 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU156: - cost: 9 - init_stock: 600 - price: 10 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU157: - cost: 369 - init_stock: 3750 - price: 410 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU158: - cost: 130 - init_stock: 4050 - price: 145 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU159: - cost: 173 - init_stock: 1250 - price: 193 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU16: - cost: 157 - init_stock: 2900 - price: 175 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU160: - cost: 413 - init_stock: 4250 - price: 459 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU161: - cost: 215 - init_stock: 2300 - price: 239 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU162: - cost: 142 - init_stock: 250 - price: 158 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU163: - cost: 437 - init_stock: 1950 - price: 486 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU164: - cost: 446 - init_stock: 5000 - price: 496 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU165: - cost: 246 - init_stock: 1650 - price: 274 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU166: - cost: 71 - init_stock: 4450 - price: 79 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU167: - cost: 398 - init_stock: 650 - price: 443 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU168: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU169: - cost: 332 - init_stock: 4900 - price: 369 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU17: - cost: 311 - init_stock: 450 - price: 346 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU170: - cost: 61 - init_stock: 2750 - price: 68 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU171: - cost: 358 - init_stock: 3800 - price: 398 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU172: - cost: 180 - init_stock: 3550 - price: 200 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU173: - cost: 157 - init_stock: 4800 - price: 175 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU174: - cost: 261 - init_stock: 3800 - price: 291 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU175: - cost: 75 - init_stock: 3750 - price: 84 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU176: - cost: 366 - init_stock: 3300 - price: 407 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU177: - cost: 231 - init_stock: 1550 - price: 257 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU178: - cost: 380 - init_stock: 250 - price: 423 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU179: - cost: 447 - init_stock: 4150 - price: 497 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU18: - cost: 232 - init_stock: 4050 - price: 258 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU180: - cost: 195 - init_stock: 2750 - price: 217 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU181: - cost: 128 - init_stock: 3000 - price: 143 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU182: - cost: 393 - init_stock: 4950 - price: 437 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU183: - cost: 130 - init_stock: 400 - price: 145 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU184: - cost: 65 - init_stock: 1200 - price: 73 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU185: - cost: 9 - init_stock: 4500 - price: 10 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU186: - cost: 323 - init_stock: 1100 - price: 359 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU187: - cost: 159 - init_stock: 1500 - price: 177 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU188: - cost: 351 - init_stock: 4350 - price: 391 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU189: - cost: 322 - init_stock: 1750 - price: 358 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU19: - cost: 429 - init_stock: 4550 - price: 477 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU190: - cost: 101 - init_stock: 850 - price: 113 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU191: - cost: 425 - init_stock: 2700 - price: 473 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU192: - cost: 373 - init_stock: 3050 - price: 415 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU193: - cost: 186 - init_stock: 1500 - price: 207 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU194: - cost: 388 - init_stock: 250 - price: 432 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU195: - cost: 196 - init_stock: 1550 - price: 218 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU196: - cost: 44 - init_stock: 3400 - price: 49 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU197: - cost: 272 - init_stock: 2850 - price: 303 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU198: - cost: 152 - init_stock: 2700 - price: 169 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU199: - cost: 404 - init_stock: 1150 - price: 449 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU2: - cost: 297 - init_stock: 3500 - price: 331 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU20: - cost: 301 - init_stock: 1250 - price: 335 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU200: - cost: 58 - init_stock: 1250 - price: 65 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU201: - cost: 93 - init_stock: 2950 - price: 104 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU202: - cost: 127 - init_stock: 3650 - price: 142 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU203: - cost: 396 - init_stock: 4100 - price: 440 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU204: - cost: 440 - init_stock: 2350 - price: 489 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU205: - cost: 117 - init_stock: 5000 - price: 130 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU206: - cost: 301 - init_stock: 550 - price: 335 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU207: - cost: 126 - init_stock: 4000 - price: 140 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU208: - cost: 441 - init_stock: 3850 - price: 491 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU209: - cost: 161 - init_stock: 1000 - price: 179 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU21: - cost: 110 - init_stock: 5000 - price: 123 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU210: - cost: 363 - init_stock: 3450 - price: 404 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU211: - cost: 156 - init_stock: 4550 - price: 174 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU212: - cost: 364 - init_stock: 3950 - price: 405 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU213: - cost: 108 - init_stock: 3200 - price: 121 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU214: - cost: 90 - init_stock: 500 - price: 101 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU215: - cost: 377 - init_stock: 2350 - price: 419 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU216: - cost: 297 - init_stock: 1150 - price: 330 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU217: - cost: 255 - init_stock: 3250 - price: 284 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU218: - cost: 184 - init_stock: 2950 - price: 205 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU219: - cost: 82 - init_stock: 2300 - price: 92 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU22: - cost: 443 - init_stock: 3300 - price: 493 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU220: - cost: 348 - init_stock: 4350 - price: 387 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU221: - cost: 35 - init_stock: 3900 - price: 39 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU222: - cost: 103 - init_stock: 1800 - price: 115 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU223: - cost: 176 - init_stock: 600 - price: 196 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU224: - cost: 348 - init_stock: 250 - price: 387 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU225: - cost: 147 - init_stock: 1450 - price: 164 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU226: - cost: 185 - init_stock: 650 - price: 206 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU227: - cost: 431 - init_stock: 1200 - price: 479 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU228: - cost: 12 - init_stock: 4500 - price: 14 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU229: - cost: 424 - init_stock: 2200 - price: 472 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU23: - cost: 348 - init_stock: 2100 - price: 387 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU230: - cost: 216 - init_stock: 1150 - price: 241 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU231: - cost: 43 - init_stock: 4050 - price: 48 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU232: - cost: 201 - init_stock: 4800 - price: 224 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU233: - cost: 324 - init_stock: 3750 - price: 360 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU234: - cost: 258 - init_stock: 250 - price: 287 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU235: - cost: 21 - init_stock: 2850 - price: 24 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU236: - cost: 139 - init_stock: 2750 - price: 155 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU237: - cost: 389 - init_stock: 2250 - price: 433 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU238: - cost: 57 - init_stock: 3300 - price: 64 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU239: - cost: 92 - init_stock: 4900 - price: 103 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU24: - cost: 87 - init_stock: 2350 - price: 97 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU240: - cost: 335 - init_stock: 2350 - price: 373 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU241: - cost: 395 - init_stock: 3550 - price: 439 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU242: - cost: 15 - init_stock: 2200 - price: 17 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU243: - cost: 316 - init_stock: 700 - price: 352 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU244: - cost: 156 - init_stock: 4100 - price: 174 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU245: - cost: 363 - init_stock: 3300 - price: 404 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU246: - cost: 270 - init_stock: 1500 - price: 300 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU247: - cost: 347 - init_stock: 1750 - price: 386 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU248: - cost: 319 - init_stock: 1450 - price: 355 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU249: - cost: 320 - init_stock: 1250 - price: 356 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU25: - cost: 144 - init_stock: 250 - price: 161 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU250: - cost: 114 - init_stock: 2700 - price: 127 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU251: - cost: 309 - init_stock: 3050 - price: 344 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU252: - cost: 162 - init_stock: 4150 - price: 181 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU253: - cost: 403 - init_stock: 800 - price: 448 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU254: - cost: 435 - init_stock: 2300 - price: 484 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU255: - cost: 261 - init_stock: 3350 - price: 290 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU256: - cost: 81 - init_stock: 3600 - price: 91 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU257: - cost: 313 - init_stock: 2850 - price: 348 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU258: - cost: 440 - init_stock: 2150 - price: 489 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU259: - cost: 299 - init_stock: 3450 - price: 333 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU26: - cost: 206 - init_stock: 3150 - price: 229 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU260: - cost: 438 - init_stock: 2600 - price: 487 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU261: - cost: 331 - init_stock: 1100 - price: 368 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU262: - cost: 298 - init_stock: 3900 - price: 332 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU263: - cost: 170 - init_stock: 3700 - price: 189 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU264: - cost: 324 - init_stock: 3950 - price: 361 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU265: - cost: 257 - init_stock: 2950 - price: 286 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU266: - cost: 115 - init_stock: 2350 - price: 128 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU267: - cost: 69 - init_stock: 4000 - price: 77 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU268: - cost: 198 - init_stock: 4450 - price: 221 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU269: - cost: 113 - init_stock: 2200 - price: 126 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU27: - cost: 333 - init_stock: 2800 - price: 370 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU270: - cost: 163 - init_stock: 3700 - price: 182 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU271: - cost: 207 - init_stock: 900 - price: 230 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU272: - cost: 329 - init_stock: 850 - price: 366 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU273: - cost: 378 - init_stock: 900 - price: 421 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU274: - cost: 26 - init_stock: 3850 - price: 29 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU275: - cost: 45 - init_stock: 2400 - price: 50 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU276: - cost: 146 - init_stock: 2700 - price: 163 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU277: - cost: 404 - init_stock: 2050 - price: 449 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU278: - cost: 94 - init_stock: 600 - price: 105 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU279: - cost: 45 - init_stock: 1950 - price: 51 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU28: - cost: 187 - init_stock: 2350 - price: 208 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU280: - cost: 265 - init_stock: 1650 - price: 295 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU281: - cost: 355 - init_stock: 3750 - price: 395 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU282: - cost: 56 - init_stock: 2300 - price: 63 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU283: - cost: 352 - init_stock: 4600 - price: 392 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU284: - cost: 309 - init_stock: 3350 - price: 344 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU285: - cost: 119 - init_stock: 4550 - price: 133 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU286: - cost: 303 - init_stock: 1950 - price: 337 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU287: - cost: 337 - init_stock: 2800 - price: 375 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU288: - cost: 162 - init_stock: 1900 - price: 181 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU289: - cost: 60 - init_stock: 1550 - price: 67 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU29: - cost: 220 - init_stock: 2900 - price: 245 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU290: - cost: 394 - init_stock: 3350 - price: 438 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU291: - cost: 84 - init_stock: 3050 - price: 94 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU292: - cost: 167 - init_stock: 250 - price: 186 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU293: - cost: 145 - init_stock: 2750 - price: 162 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU294: - cost: 368 - init_stock: 450 - price: 409 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU295: - cost: 391 - init_stock: 4650 - price: 435 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU296: - cost: 333 - init_stock: 4600 - price: 370 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU297: - cost: 268 - init_stock: 1900 - price: 298 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU298: - cost: 257 - init_stock: 1750 - price: 286 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU299: - cost: 330 - init_stock: 2550 - price: 367 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU3: - cost: 364 - init_stock: 600 - price: 405 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU30: - cost: 323 - init_stock: 3450 - price: 359 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU300: - cost: 338 - init_stock: 2900 - price: 376 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU301: - cost: 389 - init_stock: 4150 - price: 433 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU302: - cost: 165 - init_stock: 550 - price: 184 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU303: - cost: 221 - init_stock: 4700 - price: 246 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU304: - cost: 377 - init_stock: 1150 - price: 419 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU305: - cost: 445 - init_stock: 5000 - price: 495 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU306: - cost: 431 - init_stock: 2100 - price: 479 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU307: - cost: 189 - init_stock: 3900 - price: 210 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU308: - cost: 187 - init_stock: 250 - price: 208 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU309: - cost: 90 - init_stock: 4600 - price: 101 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU31: - cost: 62 - init_stock: 2800 - price: 69 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU310: - cost: 210 - init_stock: 2200 - price: 234 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU311: - cost: 275 - init_stock: 4000 - price: 306 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU312: - cost: 261 - init_stock: 1250 - price: 291 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU313: - cost: 291 - init_stock: 1900 - price: 324 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU314: - cost: 363 - init_stock: 1450 - price: 404 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU315: - cost: 423 - init_stock: 4200 - price: 471 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU316: - cost: 181 - init_stock: 900 - price: 202 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU317: - cost: 194 - init_stock: 1200 - price: 216 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU318: - cost: 250 - init_stock: 4250 - price: 278 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU319: - cost: 231 - init_stock: 2650 - price: 257 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU32: - cost: 302 - init_stock: 1100 - price: 336 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU320: - cost: 176 - init_stock: 1950 - price: 196 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU321: - cost: 60 - init_stock: 800 - price: 67 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU322: - cost: 216 - init_stock: 5000 - price: 240 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU323: - cost: 59 - init_stock: 1950 - price: 66 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU324: - cost: 397 - init_stock: 4650 - price: 442 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU325: - cost: 407 - init_stock: 3450 - price: 453 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU326: - cost: 310 - init_stock: 1200 - price: 345 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU327: - cost: 411 - init_stock: 700 - price: 457 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU328: - cost: 351 - init_stock: 2250 - price: 390 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU329: - cost: 60 - init_stock: 2100 - price: 67 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU33: - cost: 374 - init_stock: 4600 - price: 416 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU330: - cost: 275 - init_stock: 1950 - price: 306 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU331: - cost: 124 - init_stock: 2050 - price: 138 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU332: - cost: 351 - init_stock: 4800 - price: 390 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU333: - cost: 436 - init_stock: 2650 - price: 485 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU334: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU335: - cost: 72 - init_stock: 4050 - price: 80 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU336: - cost: 266 - init_stock: 1400 - price: 296 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU337: - cost: 220 - init_stock: 1450 - price: 245 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU338: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU339: - cost: 238 - init_stock: 3150 - price: 265 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU34: - cost: 85 - init_stock: 650 - price: 95 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU340: - cost: 432 - init_stock: 4350 - price: 480 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU341: - cost: 415 - init_stock: 3500 - price: 462 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU342: - cost: 129 - init_stock: 450 - price: 144 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU343: - cost: 279 - init_stock: 750 - price: 310 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU344: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU345: - cost: 397 - init_stock: 4450 - price: 442 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU346: - cost: 315 - init_stock: 2100 - price: 350 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU347: - cost: 447 - init_stock: 4100 - price: 497 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU348: - cost: 132 - init_stock: 1000 - price: 147 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU349: - cost: 60 - init_stock: 3350 - price: 67 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU35: - cost: 240 - init_stock: 4300 - price: 267 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU350: - cost: 251 - init_stock: 4600 - price: 279 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU351: - cost: 139 - init_stock: 3350 - price: 155 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU352: - cost: 63 - init_stock: 900 - price: 71 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU353: - cost: 227 - init_stock: 4650 - price: 253 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU354: - cost: 356 - init_stock: 3950 - price: 396 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU355: - cost: 56 - init_stock: 500 - price: 63 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU356: - cost: 313 - init_stock: 1450 - price: 348 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU357: - cost: 449 - init_stock: 4600 - price: 499 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU358: - cost: 242 - init_stock: 3450 - price: 269 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU359: - cost: 73 - init_stock: 3750 - price: 82 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU36: - cost: 94 - init_stock: 500 - price: 105 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU360: - cost: 435 - init_stock: 1250 - price: 484 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU361: - cost: 146 - init_stock: 4500 - price: 163 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU362: - cost: 417 - init_stock: 4350 - price: 464 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU363: - cost: 46 - init_stock: 3900 - price: 52 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU364: - cost: 348 - init_stock: 1650 - price: 387 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU365: - cost: 142 - init_stock: 4150 - price: 158 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU366: - cost: 399 - init_stock: 4300 - price: 444 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU367: - cost: 244 - init_stock: 2300 - price: 272 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU368: - cost: 424 - init_stock: 1650 - price: 472 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU369: - cost: 86 - init_stock: 3750 - price: 96 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU37: - cost: 59 - init_stock: 2950 - price: 66 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU370: - cost: 100 - init_stock: 750 - price: 112 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU371: - cost: 295 - init_stock: 250 - price: 328 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU372: - cost: 60 - init_stock: 1600 - price: 67 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU373: - cost: 52 - init_stock: 3350 - price: 58 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU374: - cost: 34 - init_stock: 2700 - price: 38 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU375: - cost: 254 - init_stock: 3600 - price: 283 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU376: - cost: 140 - init_stock: 4100 - price: 156 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU377: - cost: 70 - init_stock: 3350 - price: 78 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU378: - cost: 381 - init_stock: 1750 - price: 424 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU379: - cost: 9 - init_stock: 2450 - price: 11 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU38: - cost: 309 - init_stock: 2150 - price: 344 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU380: - cost: 353 - init_stock: 2650 - price: 393 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU381: - cost: 428 - init_stock: 300 - price: 476 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU382: - cost: 112 - init_stock: 3550 - price: 125 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU383: - cost: 273 - init_stock: 4600 - price: 304 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU384: - cost: 288 - init_stock: 450 - price: 320 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU385: - cost: 108 - init_stock: 650 - price: 120 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU386: - cost: 265 - init_stock: 1550 - price: 295 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU387: - cost: 100 - init_stock: 4850 - price: 112 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU388: - cost: 364 - init_stock: 2200 - price: 405 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU389: - cost: 338 - init_stock: 3500 - price: 376 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU39: - cost: 22 - init_stock: 2550 - price: 25 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU390: - cost: 129 - init_stock: 1950 - price: 144 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU391: - cost: 209 - init_stock: 3350 - price: 233 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU392: - cost: 146 - init_stock: 3700 - price: 163 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU393: - cost: 438 - init_stock: 3350 - price: 487 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU394: - cost: 138 - init_stock: 2650 - price: 154 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU395: - cost: 439 - init_stock: 1650 - price: 488 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU396: - cost: 299 - init_stock: 3050 - price: 333 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU397: - cost: 414 - init_stock: 2550 - price: 460 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU398: - cost: 210 - init_stock: 2900 - price: 234 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU399: - cost: 338 - init_stock: 1850 - price: 376 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU4: - cost: 395 - init_stock: 350 - price: 439 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU40: - cost: 441 - init_stock: 4950 - price: 490 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU400: - cost: 400 - init_stock: 4200 - price: 445 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU401: - cost: 369 - init_stock: 3900 - price: 410 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU402: - cost: 77 - init_stock: 350 - price: 86 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU403: - cost: 80 - init_stock: 4950 - price: 89 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU404: - cost: 258 - init_stock: 3050 - price: 287 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU405: - cost: 414 - init_stock: 950 - price: 460 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU406: - cost: 294 - init_stock: 5000 - price: 327 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU407: - cost: 23 - init_stock: 2300 - price: 26 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU408: - cost: 399 - init_stock: 400 - price: 444 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU409: - cost: 231 - init_stock: 4550 - price: 257 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU41: - cost: 126 - init_stock: 3950 - price: 141 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU410: - cost: 63 - init_stock: 800 - price: 70 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU411: - cost: 189 - init_stock: 4750 - price: 210 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU412: - cost: 257 - init_stock: 3100 - price: 286 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU413: - cost: 362 - init_stock: 4150 - price: 403 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU414: - cost: 148 - init_stock: 4350 - price: 165 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU415: - cost: 261 - init_stock: 1150 - price: 291 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU416: - cost: 205 - init_stock: 450 - price: 228 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU417: - cost: 398 - init_stock: 3600 - price: 443 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU418: - cost: 412 - init_stock: 650 - price: 458 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU419: - cost: 272 - init_stock: 4450 - price: 303 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU42: - cost: 415 - init_stock: 1300 - price: 462 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU420: - cost: 241 - init_stock: 2100 - price: 268 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU421: - cost: 192 - init_stock: 2300 - price: 214 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU422: - cost: 46 - init_stock: 2700 - price: 52 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU423: - cost: 169 - init_stock: 3300 - price: 188 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU424: - cost: 170 - init_stock: 550 - price: 189 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU425: - cost: 145 - init_stock: 600 - price: 162 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU426: - cost: 112 - init_stock: 4900 - price: 125 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU427: - cost: 371 - init_stock: 4700 - price: 413 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU428: - cost: 351 - init_stock: 3150 - price: 391 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU429: - cost: 35 - init_stock: 2050 - price: 39 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU43: - cost: 195 - init_stock: 3400 - price: 217 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU430: - cost: 194 - init_stock: 3650 - price: 216 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU431: - cost: 295 - init_stock: 1050 - price: 328 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU432: - cost: 22 - init_stock: 2300 - price: 25 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU433: - cost: 313 - init_stock: 2250 - price: 348 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU434: - cost: 33 - init_stock: 1500 - price: 37 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU435: - cost: 244 - init_stock: 1500 - price: 272 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU436: - cost: 64 - init_stock: 4050 - price: 72 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU437: - cost: 103 - init_stock: 700 - price: 115 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU438: - cost: 128 - init_stock: 500 - price: 143 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU439: - cost: 230 - init_stock: 1900 - price: 256 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU44: - cost: 324 - init_stock: 2400 - price: 360 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU440: - cost: 223 - init_stock: 4100 - price: 248 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU441: - cost: 227 - init_stock: 2800 - price: 253 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU442: - cost: 423 - init_stock: 4400 - price: 470 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU443: - cost: 196 - init_stock: 3650 - price: 218 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU444: - cost: 140 - init_stock: 300 - price: 156 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU445: - cost: 234 - init_stock: 4300 - price: 260 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU446: - cost: 447 - init_stock: 2450 - price: 497 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU447: - cost: 176 - init_stock: 250 - price: 196 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU448: - cost: 436 - init_stock: 3550 - price: 485 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU449: - cost: 253 - init_stock: 1900 - price: 282 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU45: - cost: 227 - init_stock: 500 - price: 253 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU450: - cost: 52 - init_stock: 4900 - price: 58 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU451: - cost: 421 - init_stock: 3450 - price: 468 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU452: - cost: 56 - init_stock: 3950 - price: 63 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU453: - cost: 33 - init_stock: 1750 - price: 37 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU454: - cost: 444 - init_stock: 4250 - price: 494 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU455: - cost: 122 - init_stock: 1300 - price: 136 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU456: - cost: 304 - init_stock: 1250 - price: 338 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU457: - cost: 152 - init_stock: 3200 - price: 169 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU458: - cost: 362 - init_stock: 350 - price: 403 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU459: - cost: 382 - init_stock: 1700 - price: 425 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU46: - cost: 269 - init_stock: 2500 - price: 299 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU460: - cost: 364 - init_stock: 3650 - price: 405 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU461: - cost: 225 - init_stock: 2400 - price: 251 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU462: - cost: 403 - init_stock: 450 - price: 448 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU463: - cost: 168 - init_stock: 4100 - price: 187 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU464: - cost: 251 - init_stock: 1700 - price: 279 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU465: - cost: 132 - init_stock: 5000 - price: 147 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU466: - cost: 87 - init_stock: 2100 - price: 97 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU467: - cost: 127 - init_stock: 1800 - price: 142 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU468: - cost: 49 - init_stock: 2500 - price: 55 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU469: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU47: - cost: 108 - init_stock: 1950 - price: 121 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU470: - cost: 335 - init_stock: 1600 - price: 373 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU471: - cost: 283 - init_stock: 3550 - price: 315 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU472: - cost: 378 - init_stock: 2950 - price: 421 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU473: - cost: 175 - init_stock: 1050 - price: 195 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU474: - cost: 403 - init_stock: 4300 - price: 448 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU475: - cost: 30 - init_stock: 4700 - price: 34 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU476: - cost: 225 - init_stock: 4500 - price: 251 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU477: - cost: 260 - init_stock: 2350 - price: 289 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU478: - cost: 431 - init_stock: 4400 - price: 479 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU479: - cost: 329 - init_stock: 1900 - price: 366 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU48: - cost: 306 - init_stock: 2900 - price: 340 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU480: - cost: 16 - init_stock: 1500 - price: 18 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU481: - cost: 297 - init_stock: 4400 - price: 330 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU482: - cost: 84 - init_stock: 4350 - price: 94 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU483: - cost: 268 - init_stock: 1700 - price: 298 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU484: - cost: 75 - init_stock: 3650 - price: 84 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU485: - cost: 286 - init_stock: 3350 - price: 318 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU486: - cost: 109 - init_stock: 2600 - price: 122 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU487: - cost: 249 - init_stock: 3300 - price: 277 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU488: - cost: 32 - init_stock: 1350 - price: 36 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU489: - cost: 53 - init_stock: 1550 - price: 59 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU49: - cost: 236 - init_stock: 4750 - price: 263 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU490: - cost: 144 - init_stock: 4050 - price: 161 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU491: - cost: 399 - init_stock: 3750 - price: 444 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU492: - cost: 47 - init_stock: 4000 - price: 53 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU493: - cost: 414 - init_stock: 1350 - price: 461 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU494: - cost: 130 - init_stock: 4700 - price: 145 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU495: - cost: 134 - init_stock: 3100 - price: 149 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU496: - cost: 307 - init_stock: 2950 - price: 342 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU497: - cost: 29 - init_stock: 450 - price: 33 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU498: - cost: 274 - init_stock: 3250 - price: 305 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU499: - cost: 276 - init_stock: 1450 - price: 307 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU5: - cost: 419 - init_stock: 3550 - price: 466 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU50: - cost: 253 - init_stock: 1850 - price: 282 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU500: - cost: 187 - init_stock: 2100 - price: 208 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU501: - cost: 174 - init_stock: 3800 - price: 194 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU502: - cost: 62 - init_stock: 3250 - price: 69 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU503: - cost: 187 - init_stock: 2850 - price: 208 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU504: - cost: 225 - init_stock: 1500 - price: 251 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU505: - cost: 383 - init_stock: 2950 - price: 426 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU506: - cost: 134 - init_stock: 4650 - price: 149 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU507: - cost: 168 - init_stock: 750 - price: 187 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU508: - cost: 244 - init_stock: 4800 - price: 272 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU509: - cost: 267 - init_stock: 4050 - price: 297 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU51: - cost: 265 - init_stock: 3350 - price: 295 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU510: - cost: 84 - init_stock: 450 - price: 94 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU511: - cost: 324 - init_stock: 3750 - price: 361 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU512: - cost: 420 - init_stock: 1100 - price: 467 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU513: - cost: 308 - init_stock: 350 - price: 343 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU514: - cost: 167 - init_stock: 3150 - price: 186 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU515: - cost: 130 - init_stock: 2700 - price: 145 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU516: - cost: 57 - init_stock: 1900 - price: 64 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU517: - cost: 105 - init_stock: 450 - price: 117 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU518: - cost: 23 - init_stock: 1050 - price: 26 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU519: - cost: 209 - init_stock: 2500 - price: 233 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU52: - cost: 19 - init_stock: 3350 - price: 22 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU520: - cost: 188 - init_stock: 1100 - price: 209 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU521: - cost: 198 - init_stock: 2500 - price: 220 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU522: - cost: 140 - init_stock: 4350 - price: 156 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU523: - cost: 58 - init_stock: 5000 - price: 65 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU524: - cost: 264 - init_stock: 4700 - price: 294 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU525: - cost: 388 - init_stock: 2100 - price: 432 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU526: - cost: 377 - init_stock: 1200 - price: 419 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU527: - cost: 117 - init_stock: 3500 - price: 131 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU528: - cost: 284 - init_stock: 1050 - price: 316 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU529: - cost: 268 - init_stock: 1550 - price: 298 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU53: - cost: 135 - init_stock: 3500 - price: 151 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU530: - cost: 117 - init_stock: 2250 - price: 131 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU531: - cost: 92 - init_stock: 3000 - price: 103 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU532: - cost: 315 - init_stock: 3850 - price: 351 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU533: - cost: 266 - init_stock: 2100 - price: 296 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU534: - cost: 382 - init_stock: 2050 - price: 425 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU535: - cost: 273 - init_stock: 4300 - price: 304 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU536: - cost: 322 - init_stock: 2600 - price: 358 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU537: - cost: 288 - init_stock: 450 - price: 321 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU538: - cost: 411 - init_stock: 2500 - price: 457 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU539: - cost: 148 - init_stock: 3900 - price: 165 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU54: - cost: 142 - init_stock: 2300 - price: 158 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU540: - cost: 349 - init_stock: 2100 - price: 388 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU541: - cost: 117 - init_stock: 1450 - price: 131 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU542: - cost: 34 - init_stock: 1300 - price: 38 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU543: - cost: 387 - init_stock: 4250 - price: 430 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU544: - cost: 311 - init_stock: 4850 - price: 346 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU545: - cost: 157 - init_stock: 750 - price: 175 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU546: - cost: 441 - init_stock: 4800 - price: 491 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU547: - cost: 144 - init_stock: 2200 - price: 161 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU548: - cost: 99 - init_stock: 300 - price: 111 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU549: - cost: 348 - init_stock: 3950 - price: 387 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU55: - cost: 433 - init_stock: 3250 - price: 482 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU550: - cost: 233 - init_stock: 4700 - price: 259 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU551: - cost: 41 - init_stock: 1550 - price: 46 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU552: - cost: 171 - init_stock: 4500 - price: 191 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU553: - cost: 187 - init_stock: 1500 - price: 208 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU554: - cost: 306 - init_stock: 2050 - price: 340 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU555: - cost: 440 - init_stock: 4200 - price: 489 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU556: - cost: 99 - init_stock: 1500 - price: 110 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU557: - cost: 300 - init_stock: 3650 - price: 334 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU558: - cost: 359 - init_stock: 850 - price: 399 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU559: - cost: 281 - init_stock: 2700 - price: 313 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU56: - cost: 339 - init_stock: 3700 - price: 377 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU560: - cost: 254 - init_stock: 2050 - price: 283 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU561: - cost: 181 - init_stock: 1950 - price: 202 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU562: - cost: 312 - init_stock: 4450 - price: 347 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU563: - cost: 360 - init_stock: 250 - price: 401 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU564: - cost: 98 - init_stock: 450 - price: 109 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU565: - cost: 51 - init_stock: 3750 - price: 57 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU566: - cost: 117 - init_stock: 550 - price: 131 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU567: - cost: 324 - init_stock: 450 - price: 361 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU568: - cost: 67 - init_stock: 3900 - price: 75 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU569: - cost: 425 - init_stock: 2400 - price: 473 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU57: - cost: 323 - init_stock: 2500 - price: 359 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU570: - cost: 349 - init_stock: 4150 - price: 388 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU571: - cost: 151 - init_stock: 1950 - price: 168 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU572: - cost: 23 - init_stock: 2250 - price: 26 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU573: - cost: 221 - init_stock: 2050 - price: 246 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU574: - cost: 84 - init_stock: 2500 - price: 94 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU575: - cost: 213 - init_stock: 3950 - price: 237 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU576: - cost: 238 - init_stock: 3900 - price: 265 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU577: - cost: 16 - init_stock: 4350 - price: 18 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU578: - cost: 90 - init_stock: 3550 - price: 100 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU579: - cost: 373 - init_stock: 450 - price: 415 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU58: - cost: 325 - init_stock: 2400 - price: 362 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU580: - cost: 182 - init_stock: 250 - price: 203 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU581: - cost: 136 - init_stock: 4300 - price: 152 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU582: - cost: 323 - init_stock: 4800 - price: 359 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU583: - cost: 77 - init_stock: 4350 - price: 86 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU584: - cost: 29 - init_stock: 2250 - price: 33 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU585: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU586: - cost: 54 - init_stock: 2200 - price: 61 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU587: - cost: 261 - init_stock: 3900 - price: 290 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU588: - cost: 428 - init_stock: 2200 - price: 476 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU589: - cost: 219 - init_stock: 1700 - price: 244 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU59: - cost: 130 - init_stock: 2550 - price: 145 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU590: - cost: 63 - init_stock: 1550 - price: 70 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU591: - cost: 185 - init_stock: 4200 - price: 206 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU592: - cost: 196 - init_stock: 2300 - price: 218 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU593: - cost: 111 - init_stock: 2200 - price: 124 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU594: - cost: 214 - init_stock: 2500 - price: 238 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU595: - cost: 65 - init_stock: 2150 - price: 73 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU596: - cost: 388 - init_stock: 350 - price: 432 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU597: - cost: 226 - init_stock: 4350 - price: 252 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU598: - cost: 313 - init_stock: 700 - price: 348 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU599: - cost: 48 - init_stock: 3400 - price: 54 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU6: - cost: 109 - init_stock: 4400 - price: 122 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU60: - cost: 180 - init_stock: 1950 - price: 200 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU600: - cost: 347 - init_stock: 3250 - price: 386 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU601: - cost: 124 - init_stock: 1950 - price: 138 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU602: - cost: 165 - init_stock: 4900 - price: 184 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU603: - cost: 250 - init_stock: 2100 - price: 278 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU604: - cost: 243 - init_stock: 2500 - price: 270 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU605: - cost: 259 - init_stock: 1200 - price: 288 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU606: - cost: 102 - init_stock: 550 - price: 114 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU607: - cost: 187 - init_stock: 3950 - price: 208 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU608: - cost: 35 - init_stock: 3650 - price: 39 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU609: - cost: 91 - init_stock: 950 - price: 102 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU61: - cost: 414 - init_stock: 3750 - price: 461 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU610: - cost: 404 - init_stock: 3400 - price: 449 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU611: - cost: 275 - init_stock: 3850 - price: 306 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU612: - cost: 351 - init_stock: 4400 - price: 391 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU613: - cost: 156 - init_stock: 350 - price: 174 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU614: - cost: 155 - init_stock: 750 - price: 173 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU615: - cost: 297 - init_stock: 4550 - price: 330 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU616: - cost: 208 - init_stock: 3650 - price: 232 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU617: - cost: 182 - init_stock: 3000 - price: 203 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU618: - cost: 69 - init_stock: 1150 - price: 77 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU619: - cost: 170 - init_stock: 4300 - price: 189 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU62: - cost: 111 - init_stock: 450 - price: 124 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU620: - cost: 207 - init_stock: 1950 - price: 231 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU621: - cost: 179 - init_stock: 3600 - price: 199 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU622: - cost: 227 - init_stock: 3250 - price: 253 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU623: - cost: 301 - init_stock: 2750 - price: 335 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU624: - cost: 433 - init_stock: 2700 - price: 482 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU625: - cost: 41 - init_stock: 4450 - price: 46 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU626: - cost: 405 - init_stock: 4450 - price: 451 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU627: - cost: 198 - init_stock: 4100 - price: 220 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU628: - cost: 111 - init_stock: 1300 - price: 124 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU629: - cost: 317 - init_stock: 4600 - price: 353 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU63: - cost: 61 - init_stock: 700 - price: 68 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU630: - cost: 109 - init_stock: 850 - price: 122 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU631: - cost: 266 - init_stock: 2450 - price: 296 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU632: - cost: 76 - init_stock: 4700 - price: 85 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU633: - cost: 165 - init_stock: 3350 - price: 184 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU634: - cost: 15 - init_stock: 3650 - price: 17 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU635: - cost: 306 - init_stock: 4650 - price: 341 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU636: - cost: 346 - init_stock: 1850 - price: 385 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU637: - cost: 74 - init_stock: 3050 - price: 83 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU638: - cost: 337 - init_stock: 400 - price: 375 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU639: - cost: 294 - init_stock: 2000 - price: 327 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU64: - cost: 32 - init_stock: 950 - price: 36 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU640: - cost: 247 - init_stock: 4550 - price: 275 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU641: - cost: 328 - init_stock: 4050 - price: 365 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU642: - cost: 372 - init_stock: 1250 - price: 414 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU643: - cost: 322 - init_stock: 2300 - price: 358 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU644: - cost: 41 - init_stock: 4100 - price: 46 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU645: - cost: 420 - init_stock: 4950 - price: 467 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU646: - cost: 170 - init_stock: 650 - price: 189 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU647: - cost: 272 - init_stock: 3850 - price: 303 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU648: - cost: 203 - init_stock: 2750 - price: 226 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU649: - cost: 178 - init_stock: 1250 - price: 198 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU65: - cost: 13 - init_stock: 4700 - price: 15 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU650: - cost: 315 - init_stock: 2800 - price: 351 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU651: - cost: 342 - init_stock: 4800 - price: 380 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU652: - cost: 110 - init_stock: 2000 - price: 123 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU653: - cost: 431 - init_stock: 4800 - price: 479 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU654: - cost: 59 - init_stock: 400 - price: 66 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU655: - cost: 299 - init_stock: 2100 - price: 333 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU656: - cost: 47 - init_stock: 3600 - price: 53 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU657: - cost: 65 - init_stock: 4050 - price: 73 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU658: - cost: 63 - init_stock: 1800 - price: 70 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU659: - cost: 18 - init_stock: 2800 - price: 21 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU66: - cost: 128 - init_stock: 4500 - price: 143 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU660: - cost: 438 - init_stock: 4150 - price: 487 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU661: - cost: 309 - init_stock: 3700 - price: 344 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU662: - cost: 334 - init_stock: 1900 - price: 372 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU663: - cost: 376 - init_stock: 2000 - price: 418 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU664: - cost: 315 - init_stock: 4200 - price: 351 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU665: - cost: 443 - init_stock: 2150 - price: 493 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU666: - cost: 306 - init_stock: 4400 - price: 341 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU667: - cost: 292 - init_stock: 650 - price: 325 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU668: - cost: 257 - init_stock: 3000 - price: 286 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU669: - cost: 66 - init_stock: 800 - price: 74 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU67: - cost: 222 - init_stock: 2650 - price: 247 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU670: - cost: 260 - init_stock: 4300 - price: 289 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU671: - cost: 240 - init_stock: 4150 - price: 267 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU672: - cost: 332 - init_stock: 650 - price: 369 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU673: - cost: 289 - init_stock: 2050 - price: 322 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU674: - cost: 200 - init_stock: 1100 - price: 223 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU675: - cost: 394 - init_stock: 1650 - price: 438 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU676: - cost: 219 - init_stock: 3050 - price: 244 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU677: - cost: 382 - init_stock: 2200 - price: 425 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU678: - cost: 151 - init_stock: 1800 - price: 168 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU679: - cost: 355 - init_stock: 2200 - price: 395 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU68: - cost: 152 - init_stock: 700 - price: 169 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU680: - cost: 234 - init_stock: 1500 - price: 260 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU681: - cost: 417 - init_stock: 4850 - price: 464 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU682: - cost: 333 - init_stock: 2650 - price: 370 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU683: - cost: 294 - init_stock: 3350 - price: 327 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU684: - cost: 319 - init_stock: 3950 - price: 355 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU685: - cost: 379 - init_stock: 2250 - price: 422 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU686: - cost: 56 - init_stock: 3150 - price: 63 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU687: - cost: 30 - init_stock: 3800 - price: 34 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU688: - cost: 435 - init_stock: 3850 - price: 484 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU689: - cost: 449 - init_stock: 750 - price: 499 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU69: - cost: 158 - init_stock: 3350 - price: 176 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU690: - cost: 393 - init_stock: 4150 - price: 437 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU691: - cost: 15 - init_stock: 3950 - price: 17 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU692: - cost: 202 - init_stock: 3250 - price: 225 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU693: - cost: 17 - init_stock: 3050 - price: 19 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU694: - cost: 187 - init_stock: 3750 - price: 208 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU695: - cost: 171 - init_stock: 350 - price: 190 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU696: - cost: 313 - init_stock: 2900 - price: 348 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU697: - cost: 409 - init_stock: 4000 - price: 455 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU698: - cost: 178 - init_stock: 3050 - price: 198 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU699: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU7: - cost: 90 - init_stock: 4800 - price: 101 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU70: - cost: 167 - init_stock: 1750 - price: 186 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU700: - cost: 66 - init_stock: 450 - price: 74 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU701: - cost: 359 - init_stock: 3850 - price: 399 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU702: - cost: 73 - init_stock: 1700 - price: 82 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU703: - cost: 326 - init_stock: 1900 - price: 363 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU704: - cost: 345 - init_stock: 4350 - price: 384 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU705: - cost: 115 - init_stock: 3150 - price: 128 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU706: - cost: 163 - init_stock: 4550 - price: 182 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU707: - cost: 16 - init_stock: 3450 - price: 18 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU708: - cost: 160 - init_stock: 1400 - price: 178 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU709: - cost: 91 - init_stock: 2650 - price: 102 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU71: - cost: 283 - init_stock: 2250 - price: 315 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU710: - cost: 226 - init_stock: 950 - price: 252 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU711: - cost: 252 - init_stock: 3450 - price: 281 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU712: - cost: 208 - init_stock: 550 - price: 232 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU713: - cost: 256 - init_stock: 2150 - price: 285 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU714: - cost: 71 - init_stock: 2050 - price: 79 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU715: - cost: 210 - init_stock: 850 - price: 234 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU716: - cost: 63 - init_stock: 3750 - price: 70 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU717: - cost: 310 - init_stock: 400 - price: 345 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU718: - cost: 321 - init_stock: 2900 - price: 357 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU719: - cost: 306 - init_stock: 3250 - price: 340 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU72: - cost: 412 - init_stock: 4250 - price: 458 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU720: - cost: 292 - init_stock: 2100 - price: 325 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU721: - cost: 65 - init_stock: 550 - price: 73 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU722: - cost: 352 - init_stock: 4850 - price: 392 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU723: - cost: 286 - init_stock: 4450 - price: 318 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU724: - cost: 360 - init_stock: 1650 - price: 400 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU725: - cost: 157 - init_stock: 1850 - price: 175 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU726: - cost: 412 - init_stock: 4450 - price: 458 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU727: - cost: 376 - init_stock: 2850 - price: 418 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU728: - cost: 427 - init_stock: 1850 - price: 475 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU729: - cost: 291 - init_stock: 1800 - price: 324 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU73: - cost: 190 - init_stock: 2700 - price: 212 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU730: - cost: 14 - init_stock: 650 - price: 16 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU731: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU732: - cost: 36 - init_stock: 3100 - price: 41 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU733: - cost: 283 - init_stock: 1300 - price: 315 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU734: - cost: 33 - init_stock: 2550 - price: 37 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU735: - cost: 239 - init_stock: 4950 - price: 266 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU736: - cost: 331 - init_stock: 1250 - price: 368 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU737: - cost: 427 - init_stock: 1550 - price: 475 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU738: - cost: 166 - init_stock: 2400 - price: 185 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU739: - cost: 427 - init_stock: 3700 - price: 475 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU74: - cost: 143 - init_stock: 2100 - price: 159 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU740: - cost: 351 - init_stock: 3200 - price: 390 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU741: - cost: 81 - init_stock: 2800 - price: 91 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU742: - cost: 169 - init_stock: 1100 - price: 188 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU743: - cost: 195 - init_stock: 2150 - price: 217 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU744: - cost: 341 - init_stock: 2900 - price: 379 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU745: - cost: 284 - init_stock: 4600 - price: 316 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU746: - cost: 393 - init_stock: 2000 - price: 437 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU747: - cost: 335 - init_stock: 3950 - price: 373 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU748: - cost: 247 - init_stock: 3300 - price: 275 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU749: - cost: 354 - init_stock: 2650 - price: 394 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU75: - cost: 117 - init_stock: 950 - price: 131 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU750: - cost: 230 - init_stock: 3100 - price: 256 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU751: - cost: 332 - init_stock: 3250 - price: 369 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU752: - cost: 233 - init_stock: 3900 - price: 259 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU753: - cost: 69 - init_stock: 3250 - price: 77 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU754: - cost: 348 - init_stock: 650 - price: 387 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU755: - cost: 318 - init_stock: 4000 - price: 354 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU756: - cost: 221 - init_stock: 4500 - price: 246 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU757: - cost: 36 - init_stock: 2850 - price: 40 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU758: - cost: 216 - init_stock: 2000 - price: 241 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU759: - cost: 391 - init_stock: 1900 - price: 435 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU76: - cost: 132 - init_stock: 3200 - price: 147 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU760: - cost: 158 - init_stock: 2550 - price: 176 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU761: - cost: 201 - init_stock: 2750 - price: 224 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU762: - cost: 237 - init_stock: 2550 - price: 264 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU763: - cost: 346 - init_stock: 3950 - price: 385 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU764: - cost: 314 - init_stock: 1700 - price: 349 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU765: - cost: 310 - init_stock: 650 - price: 345 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU766: - cost: 430 - init_stock: 1000 - price: 478 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU767: - cost: 85 - init_stock: 3800 - price: 95 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU768: - cost: 162 - init_stock: 4600 - price: 181 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU769: - cost: 21 - init_stock: 1450 - price: 24 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU77: - cost: 368 - init_stock: 3600 - price: 409 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU770: - cost: 135 - init_stock: 3100 - price: 150 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU771: - cost: 90 - init_stock: 350 - price: 101 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU772: - cost: 230 - init_stock: 300 - price: 256 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU773: - cost: 75 - init_stock: 4600 - price: 84 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU774: - cost: 402 - init_stock: 2950 - price: 447 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU775: - cost: 157 - init_stock: 4300 - price: 175 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU776: - cost: 92 - init_stock: 4250 - price: 103 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU777: - cost: 262 - init_stock: 2850 - price: 292 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU778: - cost: 182 - init_stock: 400 - price: 203 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU779: - cost: 105 - init_stock: 2150 - price: 117 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU78: - cost: 210 - init_stock: 650 - price: 234 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU780: - cost: 62 - init_stock: 4100 - price: 69 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU781: - cost: 334 - init_stock: 1800 - price: 372 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU782: - cost: 24 - init_stock: 500 - price: 27 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU783: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU784: - cost: 278 - init_stock: 2600 - price: 309 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU785: - cost: 171 - init_stock: 3500 - price: 191 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU786: - cost: 81 - init_stock: 1100 - price: 91 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU787: - cost: 324 - init_stock: 3050 - price: 360 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU788: - cost: 315 - init_stock: 1400 - price: 351 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU789: - cost: 137 - init_stock: 2900 - price: 153 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU79: - cost: 220 - init_stock: 550 - price: 245 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU790: - cost: 375 - init_stock: 3300 - price: 417 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU791: - cost: 120 - init_stock: 1400 - price: 134 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU792: - cost: 281 - init_stock: 1050 - price: 313 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU793: - cost: 175 - init_stock: 3800 - price: 195 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU794: - cost: 292 - init_stock: 4000 - price: 325 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU795: - cost: 248 - init_stock: 2400 - price: 276 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU796: - cost: 402 - init_stock: 2450 - price: 447 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU797: - cost: 90 - init_stock: 1950 - price: 100 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU798: - cost: 383 - init_stock: 1500 - price: 426 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU799: - cost: 56 - init_stock: 350 - price: 63 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU8: - cost: 112 - init_stock: 3150 - price: 125 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU80: - cost: 146 - init_stock: 3500 - price: 163 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU800: - cost: 268 - init_stock: 4700 - price: 298 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU801: - cost: 350 - init_stock: 1800 - price: 389 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU802: - cost: 155 - init_stock: 3100 - price: 173 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU803: - cost: 244 - init_stock: 950 - price: 272 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU804: - cost: 351 - init_stock: 2350 - price: 390 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU805: - cost: 54 - init_stock: 1650 - price: 61 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU806: - cost: 142 - init_stock: 2600 - price: 158 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU807: - cost: 407 - init_stock: 1300 - price: 453 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU808: - cost: 224 - init_stock: 4550 - price: 249 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU809: - cost: 49 - init_stock: 3250 - price: 55 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU81: - cost: 163 - init_stock: 3300 - price: 182 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU810: - cost: 248 - init_stock: 300 - price: 276 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU811: - cost: 228 - init_stock: 1850 - price: 254 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU812: - cost: 226 - init_stock: 4000 - price: 252 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU813: - cost: 227 - init_stock: 350 - price: 253 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU814: - cost: 436 - init_stock: 4850 - price: 485 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU815: - cost: 351 - init_stock: 1200 - price: 390 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU816: - cost: 136 - init_stock: 4550 - price: 152 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU817: - cost: 204 - init_stock: 250 - price: 227 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU818: - cost: 318 - init_stock: 2150 - price: 354 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU819: - cost: 271 - init_stock: 1350 - price: 302 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU82: - cost: 261 - init_stock: 1400 - price: 290 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU820: - cost: 237 - init_stock: 4100 - price: 264 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU821: - cost: 89 - init_stock: 3450 - price: 99 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU822: - cost: 122 - init_stock: 3500 - price: 136 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU823: - cost: 67 - init_stock: 1400 - price: 75 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU824: - cost: 153 - init_stock: 3500 - price: 170 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU825: - cost: 192 - init_stock: 750 - price: 214 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU826: - cost: 347 - init_stock: 2750 - price: 386 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU827: - cost: 133 - init_stock: 3200 - price: 148 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU828: - cost: 360 - init_stock: 3450 - price: 400 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU829: - cost: 54 - init_stock: 3700 - price: 61 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU83: - cost: 266 - init_stock: 850 - price: 296 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU830: - cost: 150 - init_stock: 1200 - price: 167 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU831: - cost: 235 - init_stock: 1450 - price: 262 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU832: - cost: 29 - init_stock: 4100 - price: 33 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU833: - cost: 360 - init_stock: 4900 - price: 400 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU834: - cost: 379 - init_stock: 250 - price: 422 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU835: - cost: 396 - init_stock: 2600 - price: 440 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU836: - cost: 290 - init_stock: 4800 - price: 323 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU837: - cost: 335 - init_stock: 1300 - price: 373 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU838: - cost: 410 - init_stock: 3850 - price: 456 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU839: - cost: 425 - init_stock: 3000 - price: 473 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU84: - cost: 286 - init_stock: 2100 - price: 318 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU840: - cost: 239 - init_stock: 2500 - price: 266 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU841: - cost: 256 - init_stock: 4250 - price: 285 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU842: - cost: 36 - init_stock: 4200 - price: 41 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU843: - cost: 324 - init_stock: 3600 - price: 360 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU844: - cost: 45 - init_stock: 3950 - price: 51 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU845: - cost: 259 - init_stock: 1100 - price: 288 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU846: - cost: 436 - init_stock: 1950 - price: 485 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU847: - cost: 349 - init_stock: 4550 - price: 388 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU848: - cost: 275 - init_stock: 2300 - price: 306 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU849: - cost: 288 - init_stock: 2000 - price: 320 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU85: - cost: 397 - init_stock: 5000 - price: 442 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU850: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU851: - cost: 47 - init_stock: 3200 - price: 53 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU852: - cost: 275 - init_stock: 2700 - price: 306 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU853: - cost: 347 - init_stock: 2800 - price: 386 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU854: - cost: 190 - init_stock: 3250 - price: 212 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU855: - cost: 68 - init_stock: 3600 - price: 76 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU856: - cost: 342 - init_stock: 1600 - price: 380 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU857: - cost: 415 - init_stock: 5000 - price: 462 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU858: - cost: 72 - init_stock: 1200 - price: 80 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU859: - cost: 193 - init_stock: 1400 - price: 215 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU86: - cost: 347 - init_stock: 4250 - price: 386 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU860: - cost: 281 - init_stock: 750 - price: 313 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU861: - cost: 429 - init_stock: 650 - price: 477 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU862: - cost: 216 - init_stock: 800 - price: 240 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU863: - cost: 423 - init_stock: 4650 - price: 470 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU864: - cost: 182 - init_stock: 950 - price: 203 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU865: - cost: 129 - init_stock: 950 - price: 144 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU866: - cost: 154 - init_stock: 4400 - price: 172 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU867: - cost: 449 - init_stock: 5000 - price: 499 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU868: - cost: 36 - init_stock: 2500 - price: 41 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU869: - cost: 87 - init_stock: 4850 - price: 97 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU87: - cost: 331 - init_stock: 3450 - price: 368 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU870: - cost: 252 - init_stock: 3350 - price: 281 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU871: - cost: 90 - init_stock: 4950 - price: 101 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU872: - cost: 119 - init_stock: 1600 - price: 133 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU873: - cost: 380 - init_stock: 1550 - price: 423 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU874: - cost: 422 - init_stock: 3000 - price: 469 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU875: - cost: 45 - init_stock: 1150 - price: 51 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU876: - cost: 272 - init_stock: 3050 - price: 303 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU877: - cost: 36 - init_stock: 1750 - price: 40 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU878: - cost: 24 - init_stock: 3400 - price: 27 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU879: - cost: 135 - init_stock: 5000 - price: 150 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU88: - cost: 423 - init_stock: 600 - price: 471 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU880: - cost: 389 - init_stock: 1550 - price: 433 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU881: - cost: 387 - init_stock: 3500 - price: 431 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU882: - cost: 174 - init_stock: 800 - price: 194 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU883: - cost: 255 - init_stock: 1900 - price: 284 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU884: - cost: 125 - init_stock: 1950 - price: 139 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU885: - cost: 44 - init_stock: 1350 - price: 49 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU886: - cost: 334 - init_stock: 3650 - price: 372 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU887: - cost: 239 - init_stock: 4350 - price: 266 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU888: - cost: 128 - init_stock: 450 - price: 143 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU889: - cost: 90 - init_stock: 1050 - price: 101 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU89: - cost: 124 - init_stock: 4650 - price: 138 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU890: - cost: 144 - init_stock: 5000 - price: 161 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU891: - cost: 320 - init_stock: 1100 - price: 356 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU892: - cost: 281 - init_stock: 4600 - price: 313 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU893: - cost: 206 - init_stock: 4950 - price: 229 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU894: - cost: 116 - init_stock: 3700 - price: 129 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU895: - cost: 207 - init_stock: 650 - price: 230 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU896: - cost: 260 - init_stock: 4000 - price: 289 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU897: - cost: 353 - init_stock: 3600 - price: 393 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU898: - cost: 429 - init_stock: 550 - price: 477 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU899: - cost: 209 - init_stock: 2400 - price: 233 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU9: - cost: 149 - init_stock: 4800 - price: 166 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU90: - cost: 408 - init_stock: 2550 - price: 454 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU900: - cost: 142 - init_stock: 2750 - price: 158 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU901: - cost: 193 - init_stock: 1450 - price: 215 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU902: - cost: 112 - init_stock: 4000 - price: 125 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU903: - cost: 321 - init_stock: 1900 - price: 357 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU904: - cost: 446 - init_stock: 2550 - price: 496 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU905: - cost: 224 - init_stock: 1550 - price: 249 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU906: - cost: 149 - init_stock: 2600 - price: 166 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU907: - cost: 19 - init_stock: 4150 - price: 22 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU908: - cost: 367 - init_stock: 3900 - price: 408 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU909: - cost: 433 - init_stock: 4800 - price: 482 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU91: - cost: 272 - init_stock: 800 - price: 303 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU910: - cost: 203 - init_stock: 1650 - price: 226 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU911: - cost: 414 - init_stock: 3500 - price: 461 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU912: - cost: 212 - init_stock: 1350 - price: 236 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU913: - cost: 289 - init_stock: 2300 - price: 322 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU914: - cost: 244 - init_stock: 3100 - price: 272 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU915: - cost: 303 - init_stock: 2800 - price: 337 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU916: - cost: 303 - init_stock: 4450 - price: 337 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU917: - cost: 232 - init_stock: 1500 - price: 258 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU918: - cost: 133 - init_stock: 2750 - price: 148 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU919: - cost: 353 - init_stock: 1250 - price: 393 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU92: - cost: 235 - init_stock: 3150 - price: 262 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU920: - cost: 321 - init_stock: 4300 - price: 357 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU921: - cost: 204 - init_stock: 3300 - price: 227 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU922: - cost: 100 - init_stock: 3350 - price: 112 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU923: - cost: 446 - init_stock: 2900 - price: 496 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU924: - cost: 284 - init_stock: 4250 - price: 316 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU925: - cost: 324 - init_stock: 750 - price: 360 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU926: - cost: 324 - init_stock: 3350 - price: 360 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU927: - cost: 234 - init_stock: 1050 - price: 260 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU928: - cost: 441 - init_stock: 4150 - price: 491 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU929: - cost: 323 - init_stock: 5000 - price: 359 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU93: - cost: 363 - init_stock: 3350 - price: 404 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU930: - cost: 178 - init_stock: 1400 - price: 198 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU931: - cost: 63 - init_stock: 700 - price: 71 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU932: - cost: 146 - init_stock: 3300 - price: 163 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU933: - cost: 101 - init_stock: 3900 - price: 113 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU934: - cost: 197 - init_stock: 3350 - price: 219 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU935: - cost: 327 - init_stock: 4700 - price: 364 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU936: - cost: 21 - init_stock: 250 - price: 24 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU937: - cost: 121 - init_stock: 850 - price: 135 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU938: - cost: 388 - init_stock: 1050 - price: 432 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU939: - cost: 155 - init_stock: 2950 - price: 173 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU94: - cost: 165 - init_stock: 2350 - price: 184 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU940: - cost: 12 - init_stock: 4650 - price: 14 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU941: - cost: 72 - init_stock: 2850 - price: 80 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU942: - cost: 181 - init_stock: 650 - price: 202 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU943: - cost: 124 - init_stock: 2450 - price: 138 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU944: - cost: 176 - init_stock: 2200 - price: 196 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU945: - cost: 126 - init_stock: 850 - price: 141 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU946: - cost: 292 - init_stock: 2450 - price: 325 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU947: - cost: 304 - init_stock: 4550 - price: 338 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU948: - cost: 382 - init_stock: 1400 - price: 425 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU949: - cost: 278 - init_stock: 250 - price: 309 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU95: - cost: 122 - init_stock: 1050 - price: 136 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU950: - cost: 91 - init_stock: 2700 - price: 102 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU951: - cost: 67 - init_stock: 900 - price: 75 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU952: - cost: 140 - init_stock: 550 - price: 156 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU953: - cost: 124 - init_stock: 1750 - price: 138 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU954: - cost: 266 - init_stock: 4300 - price: 296 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU955: - cost: 49 - init_stock: 3150 - price: 55 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU956: - cost: 253 - init_stock: 4250 - price: 282 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU957: - cost: 274 - init_stock: 2550 - price: 305 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU958: - cost: 332 - init_stock: 3300 - price: 369 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU959: - cost: 72 - init_stock: 4100 - price: 81 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU96: - cost: 158 - init_stock: 2200 - price: 176 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU960: - cost: 132 - init_stock: 300 - price: 147 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU961: - cost: 237 - init_stock: 2200 - price: 264 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU962: - cost: 318 - init_stock: 2200 - price: 354 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU963: - cost: 314 - init_stock: 1050 - price: 349 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU964: - cost: 219 - init_stock: 3650 - price: 244 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU965: - cost: 111 - init_stock: 2550 - price: 124 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU966: - cost: 271 - init_stock: 2200 - price: 302 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU967: - cost: 60 - init_stock: 2250 - price: 67 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU968: - cost: 252 - init_stock: 2450 - price: 281 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU969: - cost: 224 - init_stock: 300 - price: 249 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU97: - cost: 25 - init_stock: 1500 - price: 28 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU970: - cost: 219 - init_stock: 250 - price: 244 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU971: - cost: 331 - init_stock: 3650 - price: 368 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU972: - cost: 188 - init_stock: 3450 - price: 209 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU973: - cost: 243 - init_stock: 550 - price: 271 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU974: - cost: 153 - init_stock: 1600 - price: 170 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU975: - cost: 178 - init_stock: 1100 - price: 198 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU976: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU977: - cost: 210 - init_stock: 2700 - price: 234 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU978: - cost: 423 - init_stock: 3200 - price: 470 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU979: - cost: 398 - init_stock: 4800 - price: 443 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU98: - cost: 398 - init_stock: 1750 - price: 443 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU980: - cost: 35 - init_stock: 3400 - price: 39 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU981: - cost: 433 - init_stock: 3600 - price: 482 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU982: - cost: 191 - init_stock: 2600 - price: 213 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU983: - cost: 404 - init_stock: 4600 - price: 449 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU984: - cost: 208 - init_stock: 4550 - price: 232 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU985: - cost: 261 - init_stock: 2550 - price: 290 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU986: - cost: 247 - init_stock: 850 - price: 275 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU987: - cost: 390 - init_stock: 1700 - price: 434 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU988: - cost: 91 - init_stock: 1150 - price: 102 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU989: - cost: 435 - init_stock: 4650 - price: 484 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU99: - cost: 324 - init_stock: 2250 - price: 361 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU990: - cost: 97 - init_stock: 2850 - price: 108 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU991: - cost: 368 - init_stock: 950 - price: 409 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU992: - cost: 390 - init_stock: 4650 - price: 434 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU993: - cost: 130 - init_stock: 4300 - price: 145 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU994: - cost: 423 - init_stock: 2500 - price: 470 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU995: - cost: 216 - init_stock: 3800 - price: 241 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU996: - cost: 234 - init_stock: 3650 - price: 260 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU997: - cost: 360 - init_stock: 1700 - price: 400 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU998: - cost: 402 - init_stock: 2750 - price: 447 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU999: - cost: 71 - init_stock: 1150 - price: 79 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 500 - definition_ref: WarehouseFacility - name: WAREHOUSE0 - skus: - SKU0: - cost: 322 - init_stock: 1260 - price: 322 - service_level: 0.96 - vlt: 1 - SKU1: - cost: 284 - init_stock: 440 - price: 284 - service_level: 0.96 - vlt: 3 - SKU10: - cost: 68 - init_stock: 500 - price: 68 - service_level: 0.96 - vlt: 3 - SKU100: - cost: 16 - init_stock: 1300 - price: 16 - service_level: 0.96 - vlt: 3 - SKU101: - cost: 84 - init_stock: 1420 - price: 84 - service_level: 0.96 - vlt: 3 - SKU102: - cost: 328 - init_stock: 1860 - price: 328 - service_level: 0.96 - vlt: 2 - SKU103: - cost: 334 - init_stock: 1900 - price: 334 - service_level: 0.96 - vlt: 1 - SKU104: - cost: 49 - init_stock: 1300 - price: 49 - service_level: 0.96 - vlt: 3 - SKU105: - cost: 110 - init_stock: 1140 - price: 110 - service_level: 0.96 - vlt: 2 - SKU106: - cost: 251 - init_stock: 1460 - price: 251 - service_level: 0.96 - vlt: 3 - SKU107: - cost: 423 - init_stock: 1740 - price: 423 - service_level: 0.96 - vlt: 3 - SKU108: - cost: 458 - init_stock: 1840 - price: 458 - service_level: 0.96 - vlt: 3 - SKU109: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU11: - cost: 400 - init_stock: 1020 - price: 400 - service_level: 0.96 - vlt: 2 - SKU110: - cost: 66 - init_stock: 280 - price: 66 - service_level: 0.96 - vlt: 3 - SKU111: - cost: 260 - init_stock: 1220 - price: 260 - service_level: 0.96 - vlt: 1 - SKU112: - cost: 61 - init_stock: 1900 - price: 61 - service_level: 0.96 - vlt: 2 - SKU113: - cost: 348 - init_stock: 620 - price: 348 - service_level: 0.96 - vlt: 3 - SKU114: - cost: 389 - init_stock: 540 - price: 389 - service_level: 0.96 - vlt: 1 - SKU115: - cost: 286 - init_stock: 1720 - price: 286 - service_level: 0.96 - vlt: 2 - SKU116: - cost: 496 - init_stock: 1440 - price: 496 - service_level: 0.96 - vlt: 3 - SKU117: - cost: 320 - init_stock: 1840 - price: 320 - service_level: 0.96 - vlt: 3 - SKU118: - cost: 183 - init_stock: 660 - price: 183 - service_level: 0.96 - vlt: 1 - SKU119: - cost: 209 - init_stock: 640 - price: 209 - service_level: 0.96 - vlt: 3 - SKU12: - cost: 112 - init_stock: 1680 - price: 112 - service_level: 0.96 - vlt: 1 - SKU120: - cost: 121 - init_stock: 1960 - price: 121 - service_level: 0.96 - vlt: 1 - SKU121: - cost: 40 - init_stock: 1700 - price: 40 - service_level: 0.96 - vlt: 1 - SKU122: - cost: 437 - init_stock: 140 - price: 437 - service_level: 0.96 - vlt: 3 - SKU123: - cost: 233 - init_stock: 380 - price: 233 - service_level: 0.96 - vlt: 3 - SKU124: - cost: 182 - init_stock: 720 - price: 182 - service_level: 0.96 - vlt: 2 - SKU125: - cost: 16 - init_stock: 1840 - price: 16 - service_level: 0.96 - vlt: 2 - SKU126: - cost: 36 - init_stock: 780 - price: 36 - service_level: 0.96 - vlt: 3 - SKU127: - cost: 217 - init_stock: 620 - price: 217 - service_level: 0.96 - vlt: 2 - SKU128: - cost: 165 - init_stock: 380 - price: 165 - service_level: 0.96 - vlt: 1 - SKU129: - cost: 143 - init_stock: 1000 - price: 143 - service_level: 0.96 - vlt: 2 - SKU13: - cost: 317 - init_stock: 1140 - price: 317 - service_level: 0.96 - vlt: 3 - SKU130: - cost: 348 - init_stock: 1960 - price: 348 - service_level: 0.96 - vlt: 3 - SKU131: - cost: 64 - init_stock: 900 - price: 64 - service_level: 0.96 - vlt: 1 - SKU132: - cost: 427 - init_stock: 420 - price: 427 - service_level: 0.96 - vlt: 2 - SKU133: - cost: 224 - init_stock: 580 - price: 224 - service_level: 0.96 - vlt: 2 - SKU134: - cost: 336 - init_stock: 1540 - price: 336 - service_level: 0.96 - vlt: 3 - SKU135: - cost: 153 - init_stock: 2000 - price: 153 - service_level: 0.96 - vlt: 1 - SKU136: - cost: 199 - init_stock: 1420 - price: 199 - service_level: 0.96 - vlt: 3 - SKU137: - cost: 93 - init_stock: 1480 - price: 93 - service_level: 0.96 - vlt: 2 - SKU138: - cost: 228 - init_stock: 720 - price: 228 - service_level: 0.96 - vlt: 1 - SKU139: - cost: 207 - init_stock: 480 - price: 207 - service_level: 0.96 - vlt: 1 - SKU14: - cost: 268 - init_stock: 1240 - price: 268 - service_level: 0.96 - vlt: 1 - SKU140: - cost: 261 - init_stock: 680 - price: 261 - service_level: 0.96 - vlt: 3 - SKU141: - cost: 190 - init_stock: 820 - price: 190 - service_level: 0.96 - vlt: 1 - SKU142: - cost: 320 - init_stock: 760 - price: 320 - service_level: 0.96 - vlt: 3 - SKU143: - cost: 318 - init_stock: 520 - price: 318 - service_level: 0.96 - vlt: 3 - SKU144: - cost: 400 - init_stock: 240 - price: 400 - service_level: 0.96 - vlt: 1 - SKU145: - cost: 399 - init_stock: 1640 - price: 399 - service_level: 0.96 - vlt: 1 - SKU146: - cost: 177 - init_stock: 960 - price: 177 - service_level: 0.96 - vlt: 3 - SKU147: - cost: 472 - init_stock: 1120 - price: 472 - service_level: 0.96 - vlt: 3 - SKU148: - cost: 313 - init_stock: 1540 - price: 313 - service_level: 0.96 - vlt: 3 - SKU149: - cost: 357 - init_stock: 1540 - price: 357 - service_level: 0.96 - vlt: 3 - SKU15: - cost: 52 - init_stock: 100 - price: 52 - service_level: 0.96 - vlt: 2 - SKU150: - cost: 106 - init_stock: 1400 - price: 106 - service_level: 0.96 - vlt: 2 - SKU151: - cost: 223 - init_stock: 1460 - price: 223 - service_level: 0.96 - vlt: 1 - SKU152: - cost: 10 - init_stock: 1020 - price: 10 - service_level: 0.96 - vlt: 3 - SKU153: - cost: 441 - init_stock: 1240 - price: 441 - service_level: 0.96 - vlt: 1 - SKU154: - cost: 77 - init_stock: 1700 - price: 77 - service_level: 0.96 - vlt: 3 - SKU155: - cost: 422 - init_stock: 1060 - price: 422 - service_level: 0.96 - vlt: 1 - SKU156: - cost: 10 - init_stock: 240 - price: 10 - service_level: 0.96 - vlt: 1 - SKU157: - cost: 410 - init_stock: 1500 - price: 410 - service_level: 0.96 - vlt: 2 - SKU158: - cost: 145 - init_stock: 1620 - price: 145 - service_level: 0.96 - vlt: 3 - SKU159: - cost: 193 - init_stock: 500 - price: 193 - service_level: 0.96 - vlt: 2 - SKU16: - cost: 175 - init_stock: 1160 - price: 175 - service_level: 0.96 - vlt: 3 - SKU160: - cost: 459 - init_stock: 1700 - price: 459 - service_level: 0.96 - vlt: 1 - SKU161: - cost: 239 - init_stock: 920 - price: 239 - service_level: 0.96 - vlt: 2 - SKU162: - cost: 158 - init_stock: 100 - price: 158 - service_level: 0.96 - vlt: 2 - SKU163: - cost: 486 - init_stock: 780 - price: 486 - service_level: 0.96 - vlt: 2 - SKU164: - cost: 496 - init_stock: 2000 - price: 496 - service_level: 0.96 - vlt: 1 - SKU165: - cost: 274 - init_stock: 660 - price: 274 - service_level: 0.96 - vlt: 3 - SKU166: - cost: 79 - init_stock: 1780 - price: 79 - service_level: 0.96 - vlt: 1 - SKU167: - cost: 443 - init_stock: 260 - price: 443 - service_level: 0.96 - vlt: 1 - SKU168: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU169: - cost: 369 - init_stock: 1960 - price: 369 - service_level: 0.96 - vlt: 3 - SKU17: - cost: 346 - init_stock: 180 - price: 346 - service_level: 0.96 - vlt: 1 - SKU170: - cost: 68 - init_stock: 1100 - price: 68 - service_level: 0.96 - vlt: 2 - SKU171: - cost: 398 - init_stock: 1520 - price: 398 - service_level: 0.96 - vlt: 1 - SKU172: - cost: 200 - init_stock: 1420 - price: 200 - service_level: 0.96 - vlt: 2 - SKU173: - cost: 175 - init_stock: 1920 - price: 175 - service_level: 0.96 - vlt: 3 - SKU174: - cost: 291 - init_stock: 1520 - price: 291 - service_level: 0.96 - vlt: 2 - SKU175: - cost: 84 - init_stock: 1500 - price: 84 - service_level: 0.96 - vlt: 2 - SKU176: - cost: 407 - init_stock: 1320 - price: 407 - service_level: 0.96 - vlt: 1 - SKU177: - cost: 257 - init_stock: 620 - price: 257 - service_level: 0.96 - vlt: 1 - SKU178: - cost: 423 - init_stock: 100 - price: 423 - service_level: 0.96 - vlt: 2 - SKU179: - cost: 497 - init_stock: 1660 - price: 497 - service_level: 0.96 - vlt: 1 - SKU18: - cost: 258 - init_stock: 1620 - price: 258 - service_level: 0.96 - vlt: 1 - SKU180: - cost: 217 - init_stock: 1100 - price: 217 - service_level: 0.96 - vlt: 2 - SKU181: - cost: 143 - init_stock: 1200 - price: 143 - service_level: 0.96 - vlt: 1 - SKU182: - cost: 437 - init_stock: 1980 - price: 437 - service_level: 0.96 - vlt: 3 - SKU183: - cost: 145 - init_stock: 160 - price: 145 - service_level: 0.96 - vlt: 3 - SKU184: - cost: 73 - init_stock: 480 - price: 73 - service_level: 0.96 - vlt: 2 - SKU185: - cost: 10 - init_stock: 1800 - price: 10 - service_level: 0.96 - vlt: 2 - SKU186: - cost: 359 - init_stock: 440 - price: 359 - service_level: 0.96 - vlt: 1 - SKU187: - cost: 177 - init_stock: 600 - price: 177 - service_level: 0.96 - vlt: 3 - SKU188: - cost: 391 - init_stock: 1740 - price: 391 - service_level: 0.96 - vlt: 3 - SKU189: - cost: 358 - init_stock: 700 - price: 358 - service_level: 0.96 - vlt: 2 - SKU19: - cost: 477 - init_stock: 1820 - price: 477 - service_level: 0.96 - vlt: 3 - SKU190: - cost: 113 - init_stock: 340 - price: 113 - service_level: 0.96 - vlt: 3 - SKU191: - cost: 473 - init_stock: 1080 - price: 473 - service_level: 0.96 - vlt: 2 - SKU192: - cost: 415 - init_stock: 1220 - price: 415 - service_level: 0.96 - vlt: 2 - SKU193: - cost: 207 - init_stock: 600 - price: 207 - service_level: 0.96 - vlt: 2 - SKU194: - cost: 432 - init_stock: 100 - price: 432 - service_level: 0.96 - vlt: 2 - SKU195: - cost: 218 - init_stock: 620 - price: 218 - service_level: 0.96 - vlt: 2 - SKU196: - cost: 49 - init_stock: 1360 - price: 49 - service_level: 0.96 - vlt: 3 - SKU197: - cost: 303 - init_stock: 1140 - price: 303 - service_level: 0.96 - vlt: 1 - SKU198: - cost: 169 - init_stock: 1080 - price: 169 - service_level: 0.96 - vlt: 2 - SKU199: - cost: 449 - init_stock: 460 - price: 449 - service_level: 0.96 - vlt: 1 - SKU2: - cost: 331 - init_stock: 1400 - price: 331 - service_level: 0.96 - vlt: 3 - SKU20: - cost: 335 - init_stock: 500 - price: 335 - service_level: 0.96 - vlt: 3 - SKU200: - cost: 65 - init_stock: 500 - price: 65 - service_level: 0.96 - vlt: 1 - SKU201: - cost: 104 - init_stock: 1180 - price: 104 - service_level: 0.96 - vlt: 1 - SKU202: - cost: 142 - init_stock: 1460 - price: 142 - service_level: 0.96 - vlt: 1 - SKU203: - cost: 440 - init_stock: 1640 - price: 440 - service_level: 0.96 - vlt: 2 - SKU204: - cost: 489 - init_stock: 940 - price: 489 - service_level: 0.96 - vlt: 2 - SKU205: - cost: 130 - init_stock: 2000 - price: 130 - service_level: 0.96 - vlt: 3 - SKU206: - cost: 335 - init_stock: 220 - price: 335 - service_level: 0.96 - vlt: 2 - SKU207: - cost: 140 - init_stock: 1600 - price: 140 - service_level: 0.96 - vlt: 1 - SKU208: - cost: 491 - init_stock: 1540 - price: 491 - service_level: 0.96 - vlt: 1 - SKU209: - cost: 179 - init_stock: 400 - price: 179 - service_level: 0.96 - vlt: 3 - SKU21: - cost: 123 - init_stock: 2000 - price: 123 - service_level: 0.96 - vlt: 2 - SKU210: - cost: 404 - init_stock: 1380 - price: 404 - service_level: 0.96 - vlt: 3 - SKU211: - cost: 174 - init_stock: 1820 - price: 174 - service_level: 0.96 - vlt: 2 - SKU212: - cost: 405 - init_stock: 1580 - price: 405 - service_level: 0.96 - vlt: 3 - SKU213: - cost: 121 - init_stock: 1280 - price: 121 - service_level: 0.96 - vlt: 2 - SKU214: - cost: 101 - init_stock: 200 - price: 101 - service_level: 0.96 - vlt: 2 - SKU215: - cost: 419 - init_stock: 940 - price: 419 - service_level: 0.96 - vlt: 1 - SKU216: - cost: 330 - init_stock: 460 - price: 330 - service_level: 0.96 - vlt: 1 - SKU217: - cost: 284 - init_stock: 1300 - price: 284 - service_level: 0.96 - vlt: 2 - SKU218: - cost: 205 - init_stock: 1180 - price: 205 - service_level: 0.96 - vlt: 1 - SKU219: - cost: 92 - init_stock: 920 - price: 92 - service_level: 0.96 - vlt: 3 - SKU22: - cost: 493 - init_stock: 1320 - price: 493 - service_level: 0.96 - vlt: 1 - SKU220: - cost: 387 - init_stock: 1740 - price: 387 - service_level: 0.96 - vlt: 2 - SKU221: - cost: 39 - init_stock: 1560 - price: 39 - service_level: 0.96 - vlt: 2 - SKU222: - cost: 115 - init_stock: 720 - price: 115 - service_level: 0.96 - vlt: 2 - SKU223: - cost: 196 - init_stock: 240 - price: 196 - service_level: 0.96 - vlt: 2 - SKU224: - cost: 387 - init_stock: 100 - price: 387 - service_level: 0.96 - vlt: 2 - SKU225: - cost: 164 - init_stock: 580 - price: 164 - service_level: 0.96 - vlt: 1 - SKU226: - cost: 206 - init_stock: 260 - price: 206 - service_level: 0.96 - vlt: 2 - SKU227: - cost: 479 - init_stock: 480 - price: 479 - service_level: 0.96 - vlt: 3 - SKU228: - cost: 14 - init_stock: 1800 - price: 14 - service_level: 0.96 - vlt: 1 - SKU229: - cost: 472 - init_stock: 880 - price: 472 - service_level: 0.96 - vlt: 1 - SKU23: - cost: 387 - init_stock: 840 - price: 387 - service_level: 0.96 - vlt: 1 - SKU230: - cost: 241 - init_stock: 460 - price: 241 - service_level: 0.96 - vlt: 3 - SKU231: - cost: 48 - init_stock: 1620 - price: 48 - service_level: 0.96 - vlt: 3 - SKU232: - cost: 224 - init_stock: 1920 - price: 224 - service_level: 0.96 - vlt: 1 - SKU233: - cost: 360 - init_stock: 1500 - price: 360 - service_level: 0.96 - vlt: 2 - SKU234: - cost: 287 - init_stock: 100 - price: 287 - service_level: 0.96 - vlt: 1 - SKU235: - cost: 24 - init_stock: 1140 - price: 24 - service_level: 0.96 - vlt: 3 - SKU236: - cost: 155 - init_stock: 1100 - price: 155 - service_level: 0.96 - vlt: 2 - SKU237: - cost: 433 - init_stock: 900 - price: 433 - service_level: 0.96 - vlt: 3 - SKU238: - cost: 64 - init_stock: 1320 - price: 64 - service_level: 0.96 - vlt: 3 - SKU239: - cost: 103 - init_stock: 1960 - price: 103 - service_level: 0.96 - vlt: 1 - SKU24: - cost: 97 - init_stock: 940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU240: - cost: 373 - init_stock: 940 - price: 373 - service_level: 0.96 - vlt: 2 - SKU241: - cost: 439 - init_stock: 1420 - price: 439 - service_level: 0.96 - vlt: 2 - SKU242: - cost: 17 - init_stock: 880 - price: 17 - service_level: 0.96 - vlt: 1 - SKU243: - cost: 352 - init_stock: 280 - price: 352 - service_level: 0.96 - vlt: 1 - SKU244: - cost: 174 - init_stock: 1640 - price: 174 - service_level: 0.96 - vlt: 2 - SKU245: - cost: 404 - init_stock: 1320 - price: 404 - service_level: 0.96 - vlt: 2 - SKU246: - cost: 300 - init_stock: 600 - price: 300 - service_level: 0.96 - vlt: 2 - SKU247: - cost: 386 - init_stock: 700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU248: - cost: 355 - init_stock: 580 - price: 355 - service_level: 0.96 - vlt: 3 - SKU249: - cost: 356 - init_stock: 500 - price: 356 - service_level: 0.96 - vlt: 1 - SKU25: - cost: 161 - init_stock: 100 - price: 161 - service_level: 0.96 - vlt: 2 - SKU250: - cost: 127 - init_stock: 1080 - price: 127 - service_level: 0.96 - vlt: 3 - SKU251: - cost: 344 - init_stock: 1220 - price: 344 - service_level: 0.96 - vlt: 1 - SKU252: - cost: 181 - init_stock: 1660 - price: 181 - service_level: 0.96 - vlt: 1 - SKU253: - cost: 448 - init_stock: 320 - price: 448 - service_level: 0.96 - vlt: 1 - SKU254: - cost: 484 - init_stock: 920 - price: 484 - service_level: 0.96 - vlt: 3 - SKU255: - cost: 290 - init_stock: 1340 - price: 290 - service_level: 0.96 - vlt: 2 - SKU256: - cost: 91 - init_stock: 1440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU257: - cost: 348 - init_stock: 1140 - price: 348 - service_level: 0.96 - vlt: 1 - SKU258: - cost: 489 - init_stock: 860 - price: 489 - service_level: 0.96 - vlt: 1 - SKU259: - cost: 333 - init_stock: 1380 - price: 333 - service_level: 0.96 - vlt: 1 - SKU26: - cost: 229 - init_stock: 1260 - price: 229 - service_level: 0.96 - vlt: 1 - SKU260: - cost: 487 - init_stock: 1040 - price: 487 - service_level: 0.96 - vlt: 3 - SKU261: - cost: 368 - init_stock: 440 - price: 368 - service_level: 0.96 - vlt: 1 - SKU262: - cost: 332 - init_stock: 1560 - price: 332 - service_level: 0.96 - vlt: 3 - SKU263: - cost: 189 - init_stock: 1480 - price: 189 - service_level: 0.96 - vlt: 3 - SKU264: - cost: 361 - init_stock: 1580 - price: 361 - service_level: 0.96 - vlt: 1 - SKU265: - cost: 286 - init_stock: 1180 - price: 286 - service_level: 0.96 - vlt: 3 - SKU266: - cost: 128 - init_stock: 940 - price: 128 - service_level: 0.96 - vlt: 2 - SKU267: - cost: 77 - init_stock: 1600 - price: 77 - service_level: 0.96 - vlt: 1 - SKU268: - cost: 221 - init_stock: 1780 - price: 221 - service_level: 0.96 - vlt: 2 - SKU269: - cost: 126 - init_stock: 880 - price: 126 - service_level: 0.96 - vlt: 2 - SKU27: - cost: 370 - init_stock: 1120 - price: 370 - service_level: 0.96 - vlt: 3 - SKU270: - cost: 182 - init_stock: 1480 - price: 182 - service_level: 0.96 - vlt: 3 - SKU271: - cost: 230 - init_stock: 360 - price: 230 - service_level: 0.96 - vlt: 1 - SKU272: - cost: 366 - init_stock: 340 - price: 366 - service_level: 0.96 - vlt: 2 - SKU273: - cost: 421 - init_stock: 360 - price: 421 - service_level: 0.96 - vlt: 2 - SKU274: - cost: 29 - init_stock: 1540 - price: 29 - service_level: 0.96 - vlt: 2 - SKU275: - cost: 50 - init_stock: 960 - price: 50 - service_level: 0.96 - vlt: 1 - SKU276: - cost: 163 - init_stock: 1080 - price: 163 - service_level: 0.96 - vlt: 3 - SKU277: - cost: 449 - init_stock: 820 - price: 449 - service_level: 0.96 - vlt: 1 - SKU278: - cost: 105 - init_stock: 240 - price: 105 - service_level: 0.96 - vlt: 3 - SKU279: - cost: 51 - init_stock: 780 - price: 51 - service_level: 0.96 - vlt: 2 - SKU28: - cost: 208 - init_stock: 940 - price: 208 - service_level: 0.96 - vlt: 2 - SKU280: - cost: 295 - init_stock: 660 - price: 295 - service_level: 0.96 - vlt: 2 - SKU281: - cost: 395 - init_stock: 1500 - price: 395 - service_level: 0.96 - vlt: 1 - SKU282: - cost: 63 - init_stock: 920 - price: 63 - service_level: 0.96 - vlt: 2 - SKU283: - cost: 392 - init_stock: 1840 - price: 392 - service_level: 0.96 - vlt: 3 - SKU284: - cost: 344 - init_stock: 1340 - price: 344 - service_level: 0.96 - vlt: 3 - SKU285: - cost: 133 - init_stock: 1820 - price: 133 - service_level: 0.96 - vlt: 2 - SKU286: - cost: 337 - init_stock: 780 - price: 337 - service_level: 0.96 - vlt: 1 - SKU287: - cost: 375 - init_stock: 1120 - price: 375 - service_level: 0.96 - vlt: 3 - SKU288: - cost: 181 - init_stock: 760 - price: 181 - service_level: 0.96 - vlt: 1 - SKU289: - cost: 67 - init_stock: 620 - price: 67 - service_level: 0.96 - vlt: 1 - SKU29: - cost: 245 - init_stock: 1160 - price: 245 - service_level: 0.96 - vlt: 1 - SKU290: - cost: 438 - init_stock: 1340 - price: 438 - service_level: 0.96 - vlt: 1 - SKU291: - cost: 94 - init_stock: 1220 - price: 94 - service_level: 0.96 - vlt: 1 - SKU292: - cost: 186 - init_stock: 100 - price: 186 - service_level: 0.96 - vlt: 1 - SKU293: - cost: 162 - init_stock: 1100 - price: 162 - service_level: 0.96 - vlt: 2 - SKU294: - cost: 409 - init_stock: 180 - price: 409 - service_level: 0.96 - vlt: 2 - SKU295: - cost: 435 - init_stock: 1860 - price: 435 - service_level: 0.96 - vlt: 3 - SKU296: - cost: 370 - init_stock: 1840 - price: 370 - service_level: 0.96 - vlt: 3 - SKU297: - cost: 298 - init_stock: 760 - price: 298 - service_level: 0.96 - vlt: 1 - SKU298: - cost: 286 - init_stock: 700 - price: 286 - service_level: 0.96 - vlt: 2 - SKU299: - cost: 367 - init_stock: 1020 - price: 367 - service_level: 0.96 - vlt: 3 - SKU3: - cost: 405 - init_stock: 240 - price: 405 - service_level: 0.96 - vlt: 3 - SKU30: - cost: 359 - init_stock: 1380 - price: 359 - service_level: 0.96 - vlt: 2 - SKU300: - cost: 376 - init_stock: 1160 - price: 376 - service_level: 0.96 - vlt: 1 - SKU301: - cost: 433 - init_stock: 1660 - price: 433 - service_level: 0.96 - vlt: 2 - SKU302: - cost: 184 - init_stock: 220 - price: 184 - service_level: 0.96 - vlt: 2 - SKU303: - cost: 246 - init_stock: 1880 - price: 246 - service_level: 0.96 - vlt: 1 - SKU304: - cost: 419 - init_stock: 460 - price: 419 - service_level: 0.96 - vlt: 3 - SKU305: - cost: 495 - init_stock: 2000 - price: 495 - service_level: 0.96 - vlt: 1 - SKU306: - cost: 479 - init_stock: 840 - price: 479 - service_level: 0.96 - vlt: 2 - SKU307: - cost: 210 - init_stock: 1560 - price: 210 - service_level: 0.96 - vlt: 1 - SKU308: - cost: 208 - init_stock: 100 - price: 208 - service_level: 0.96 - vlt: 2 - SKU309: - cost: 101 - init_stock: 1840 - price: 101 - service_level: 0.96 - vlt: 2 - SKU31: - cost: 69 - init_stock: 1120 - price: 69 - service_level: 0.96 - vlt: 3 - SKU310: - cost: 234 - init_stock: 880 - price: 234 - service_level: 0.96 - vlt: 3 - SKU311: - cost: 306 - init_stock: 1600 - price: 306 - service_level: 0.96 - vlt: 3 - SKU312: - cost: 291 - init_stock: 500 - price: 291 - service_level: 0.96 - vlt: 1 - SKU313: - cost: 324 - init_stock: 760 - price: 324 - service_level: 0.96 - vlt: 1 - SKU314: - cost: 404 - init_stock: 580 - price: 404 - service_level: 0.96 - vlt: 1 - SKU315: - cost: 471 - init_stock: 1680 - price: 471 - service_level: 0.96 - vlt: 2 - SKU316: - cost: 202 - init_stock: 360 - price: 202 - service_level: 0.96 - vlt: 1 - SKU317: - cost: 216 - init_stock: 480 - price: 216 - service_level: 0.96 - vlt: 2 - SKU318: - cost: 278 - init_stock: 1700 - price: 278 - service_level: 0.96 - vlt: 1 - SKU319: - cost: 257 - init_stock: 1060 - price: 257 - service_level: 0.96 - vlt: 3 - SKU32: - cost: 336 - init_stock: 440 - price: 336 - service_level: 0.96 - vlt: 1 - SKU320: - cost: 196 - init_stock: 780 - price: 196 - service_level: 0.96 - vlt: 3 - SKU321: - cost: 67 - init_stock: 320 - price: 67 - service_level: 0.96 - vlt: 3 - SKU322: - cost: 240 - init_stock: 2000 - price: 240 - service_level: 0.96 - vlt: 1 - SKU323: - cost: 66 - init_stock: 780 - price: 66 - service_level: 0.96 - vlt: 2 - SKU324: - cost: 442 - init_stock: 1860 - price: 442 - service_level: 0.96 - vlt: 3 - SKU325: - cost: 453 - init_stock: 1380 - price: 453 - service_level: 0.96 - vlt: 2 - SKU326: - cost: 345 - init_stock: 480 - price: 345 - service_level: 0.96 - vlt: 2 - SKU327: - cost: 457 - init_stock: 280 - price: 457 - service_level: 0.96 - vlt: 2 - SKU328: - cost: 390 - init_stock: 900 - price: 390 - service_level: 0.96 - vlt: 1 - SKU329: - cost: 67 - init_stock: 840 - price: 67 - service_level: 0.96 - vlt: 1 - SKU33: - cost: 416 - init_stock: 1840 - price: 416 - service_level: 0.96 - vlt: 2 - SKU330: - cost: 306 - init_stock: 780 - price: 306 - service_level: 0.96 - vlt: 2 - SKU331: - cost: 138 - init_stock: 820 - price: 138 - service_level: 0.96 - vlt: 3 - SKU332: - cost: 390 - init_stock: 1920 - price: 390 - service_level: 0.96 - vlt: 2 - SKU333: - cost: 485 - init_stock: 1060 - price: 485 - service_level: 0.96 - vlt: 2 - SKU334: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 1 - SKU335: - cost: 80 - init_stock: 1620 - price: 80 - service_level: 0.96 - vlt: 1 - SKU336: - cost: 296 - init_stock: 560 - price: 296 - service_level: 0.96 - vlt: 1 - SKU337: - cost: 245 - init_stock: 580 - price: 245 - service_level: 0.96 - vlt: 2 - SKU338: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 3 - SKU339: - cost: 265 - init_stock: 1260 - price: 265 - service_level: 0.96 - vlt: 2 - SKU34: - cost: 95 - init_stock: 260 - price: 95 - service_level: 0.96 - vlt: 3 - SKU340: - cost: 480 - init_stock: 1740 - price: 480 - service_level: 0.96 - vlt: 2 - SKU341: - cost: 462 - init_stock: 1400 - price: 462 - service_level: 0.96 - vlt: 1 - SKU342: - cost: 144 - init_stock: 180 - price: 144 - service_level: 0.96 - vlt: 3 - SKU343: - cost: 310 - init_stock: 300 - price: 310 - service_level: 0.96 - vlt: 2 - SKU344: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU345: - cost: 442 - init_stock: 1780 - price: 442 - service_level: 0.96 - vlt: 2 - SKU346: - cost: 350 - init_stock: 840 - price: 350 - service_level: 0.96 - vlt: 3 - SKU347: - cost: 497 - init_stock: 1640 - price: 497 - service_level: 0.96 - vlt: 1 - SKU348: - cost: 147 - init_stock: 400 - price: 147 - service_level: 0.96 - vlt: 1 - SKU349: - cost: 67 - init_stock: 1340 - price: 67 - service_level: 0.96 - vlt: 3 - SKU35: - cost: 267 - init_stock: 1720 - price: 267 - service_level: 0.96 - vlt: 3 - SKU350: - cost: 279 - init_stock: 1840 - price: 279 - service_level: 0.96 - vlt: 3 - SKU351: - cost: 155 - init_stock: 1340 - price: 155 - service_level: 0.96 - vlt: 1 - SKU352: - cost: 71 - init_stock: 360 - price: 71 - service_level: 0.96 - vlt: 2 - SKU353: - cost: 253 - init_stock: 1860 - price: 253 - service_level: 0.96 - vlt: 2 - SKU354: - cost: 396 - init_stock: 1580 - price: 396 - service_level: 0.96 - vlt: 3 - SKU355: - cost: 63 - init_stock: 200 - price: 63 - service_level: 0.96 - vlt: 1 - SKU356: - cost: 348 - init_stock: 580 - price: 348 - service_level: 0.96 - vlt: 1 - SKU357: - cost: 499 - init_stock: 1840 - price: 499 - service_level: 0.96 - vlt: 3 - SKU358: - cost: 269 - init_stock: 1380 - price: 269 - service_level: 0.96 - vlt: 2 - SKU359: - cost: 82 - init_stock: 1500 - price: 82 - service_level: 0.96 - vlt: 3 - SKU36: - cost: 105 - init_stock: 200 - price: 105 - service_level: 0.96 - vlt: 2 - SKU360: - cost: 484 - init_stock: 500 - price: 484 - service_level: 0.96 - vlt: 1 - SKU361: - cost: 163 - init_stock: 1800 - price: 163 - service_level: 0.96 - vlt: 1 - SKU362: - cost: 464 - init_stock: 1740 - price: 464 - service_level: 0.96 - vlt: 2 - SKU363: - cost: 52 - init_stock: 1560 - price: 52 - service_level: 0.96 - vlt: 2 - SKU364: - cost: 387 - init_stock: 660 - price: 387 - service_level: 0.96 - vlt: 1 - SKU365: - cost: 158 - init_stock: 1660 - price: 158 - service_level: 0.96 - vlt: 3 - SKU366: - cost: 444 - init_stock: 1720 - price: 444 - service_level: 0.96 - vlt: 3 - SKU367: - cost: 272 - init_stock: 920 - price: 272 - service_level: 0.96 - vlt: 3 - SKU368: - cost: 472 - init_stock: 660 - price: 472 - service_level: 0.96 - vlt: 1 - SKU369: - cost: 96 - init_stock: 1500 - price: 96 - service_level: 0.96 - vlt: 2 - SKU37: - cost: 66 - init_stock: 1180 - price: 66 - service_level: 0.96 - vlt: 2 - SKU370: - cost: 112 - init_stock: 300 - price: 112 - service_level: 0.96 - vlt: 2 - SKU371: - cost: 328 - init_stock: 100 - price: 328 - service_level: 0.96 - vlt: 3 - SKU372: - cost: 67 - init_stock: 640 - price: 67 - service_level: 0.96 - vlt: 1 - SKU373: - cost: 58 - init_stock: 1340 - price: 58 - service_level: 0.96 - vlt: 3 - SKU374: - cost: 38 - init_stock: 1080 - price: 38 - service_level: 0.96 - vlt: 2 - SKU375: - cost: 283 - init_stock: 1440 - price: 283 - service_level: 0.96 - vlt: 1 - SKU376: - cost: 156 - init_stock: 1640 - price: 156 - service_level: 0.96 - vlt: 1 - SKU377: - cost: 78 - init_stock: 1340 - price: 78 - service_level: 0.96 - vlt: 3 - SKU378: - cost: 424 - init_stock: 700 - price: 424 - service_level: 0.96 - vlt: 1 - SKU379: - cost: 11 - init_stock: 980 - price: 11 - service_level: 0.96 - vlt: 1 - SKU38: - cost: 344 - init_stock: 860 - price: 344 - service_level: 0.96 - vlt: 2 - SKU380: - cost: 393 - init_stock: 1060 - price: 393 - service_level: 0.96 - vlt: 1 - SKU381: - cost: 476 - init_stock: 120 - price: 476 - service_level: 0.96 - vlt: 1 - SKU382: - cost: 125 - init_stock: 1420 - price: 125 - service_level: 0.96 - vlt: 2 - SKU383: - cost: 304 - init_stock: 1840 - price: 304 - service_level: 0.96 - vlt: 3 - SKU384: - cost: 320 - init_stock: 180 - price: 320 - service_level: 0.96 - vlt: 2 - SKU385: - cost: 120 - init_stock: 260 - price: 120 - service_level: 0.96 - vlt: 2 - SKU386: - cost: 295 - init_stock: 620 - price: 295 - service_level: 0.96 - vlt: 1 - SKU387: - cost: 112 - init_stock: 1940 - price: 112 - service_level: 0.96 - vlt: 3 - SKU388: - cost: 405 - init_stock: 880 - price: 405 - service_level: 0.96 - vlt: 2 - SKU389: - cost: 376 - init_stock: 1400 - price: 376 - service_level: 0.96 - vlt: 2 - SKU39: - cost: 25 - init_stock: 1020 - price: 25 - service_level: 0.96 - vlt: 3 - SKU390: - cost: 144 - init_stock: 780 - price: 144 - service_level: 0.96 - vlt: 2 - SKU391: - cost: 233 - init_stock: 1340 - price: 233 - service_level: 0.96 - vlt: 2 - SKU392: - cost: 163 - init_stock: 1480 - price: 163 - service_level: 0.96 - vlt: 2 - SKU393: - cost: 487 - init_stock: 1340 - price: 487 - service_level: 0.96 - vlt: 1 - SKU394: - cost: 154 - init_stock: 1060 - price: 154 - service_level: 0.96 - vlt: 2 - SKU395: - cost: 488 - init_stock: 660 - price: 488 - service_level: 0.96 - vlt: 3 - SKU396: - cost: 333 - init_stock: 1220 - price: 333 - service_level: 0.96 - vlt: 3 - SKU397: - cost: 460 - init_stock: 1020 - price: 460 - service_level: 0.96 - vlt: 1 - SKU398: - cost: 234 - init_stock: 1160 - price: 234 - service_level: 0.96 - vlt: 2 - SKU399: - cost: 376 - init_stock: 740 - price: 376 - service_level: 0.96 - vlt: 2 - SKU4: - cost: 439 - init_stock: 140 - price: 439 - service_level: 0.96 - vlt: 2 - SKU40: - cost: 490 - init_stock: 1980 - price: 490 - service_level: 0.96 - vlt: 3 - SKU400: - cost: 445 - init_stock: 1680 - price: 445 - service_level: 0.96 - vlt: 2 - SKU401: - cost: 410 - init_stock: 1560 - price: 410 - service_level: 0.96 - vlt: 1 - SKU402: - cost: 86 - init_stock: 140 - price: 86 - service_level: 0.96 - vlt: 3 - SKU403: - cost: 89 - init_stock: 1980 - price: 89 - service_level: 0.96 - vlt: 3 - SKU404: - cost: 287 - init_stock: 1220 - price: 287 - service_level: 0.96 - vlt: 1 - SKU405: - cost: 460 - init_stock: 380 - price: 460 - service_level: 0.96 - vlt: 1 - SKU406: - cost: 327 - init_stock: 2000 - price: 327 - service_level: 0.96 - vlt: 1 - SKU407: - cost: 26 - init_stock: 920 - price: 26 - service_level: 0.96 - vlt: 2 - SKU408: - cost: 444 - init_stock: 160 - price: 444 - service_level: 0.96 - vlt: 2 - SKU409: - cost: 257 - init_stock: 1820 - price: 257 - service_level: 0.96 - vlt: 2 - SKU41: - cost: 141 - init_stock: 1580 - price: 141 - service_level: 0.96 - vlt: 2 - SKU410: - cost: 70 - init_stock: 320 - price: 70 - service_level: 0.96 - vlt: 1 - SKU411: - cost: 210 - init_stock: 1900 - price: 210 - service_level: 0.96 - vlt: 3 - SKU412: - cost: 286 - init_stock: 1240 - price: 286 - service_level: 0.96 - vlt: 2 - SKU413: - cost: 403 - init_stock: 1660 - price: 403 - service_level: 0.96 - vlt: 3 - SKU414: - cost: 165 - init_stock: 1740 - price: 165 - service_level: 0.96 - vlt: 1 - SKU415: - cost: 291 - init_stock: 460 - price: 291 - service_level: 0.96 - vlt: 3 - SKU416: - cost: 228 - init_stock: 180 - price: 228 - service_level: 0.96 - vlt: 3 - SKU417: - cost: 443 - init_stock: 1440 - price: 443 - service_level: 0.96 - vlt: 1 - SKU418: - cost: 458 - init_stock: 260 - price: 458 - service_level: 0.96 - vlt: 3 - SKU419: - cost: 303 - init_stock: 1780 - price: 303 - service_level: 0.96 - vlt: 3 - SKU42: - cost: 462 - init_stock: 520 - price: 462 - service_level: 0.96 - vlt: 3 - SKU420: - cost: 268 - init_stock: 840 - price: 268 - service_level: 0.96 - vlt: 1 - SKU421: - cost: 214 - init_stock: 920 - price: 214 - service_level: 0.96 - vlt: 2 - SKU422: - cost: 52 - init_stock: 1080 - price: 52 - service_level: 0.96 - vlt: 3 - SKU423: - cost: 188 - init_stock: 1320 - price: 188 - service_level: 0.96 - vlt: 1 - SKU424: - cost: 189 - init_stock: 220 - price: 189 - service_level: 0.96 - vlt: 2 - SKU425: - cost: 162 - init_stock: 240 - price: 162 - service_level: 0.96 - vlt: 3 - SKU426: - cost: 125 - init_stock: 1960 - price: 125 - service_level: 0.96 - vlt: 3 - SKU427: - cost: 413 - init_stock: 1880 - price: 413 - service_level: 0.96 - vlt: 1 - SKU428: - cost: 391 - init_stock: 1260 - price: 391 - service_level: 0.96 - vlt: 3 - SKU429: - cost: 39 - init_stock: 820 - price: 39 - service_level: 0.96 - vlt: 1 - SKU43: - cost: 217 - init_stock: 1360 - price: 217 - service_level: 0.96 - vlt: 1 - SKU430: - cost: 216 - init_stock: 1460 - price: 216 - service_level: 0.96 - vlt: 2 - SKU431: - cost: 328 - init_stock: 420 - price: 328 - service_level: 0.96 - vlt: 1 - SKU432: - cost: 25 - init_stock: 920 - price: 25 - service_level: 0.96 - vlt: 3 - SKU433: - cost: 348 - init_stock: 900 - price: 348 - service_level: 0.96 - vlt: 2 - SKU434: - cost: 37 - init_stock: 600 - price: 37 - service_level: 0.96 - vlt: 1 - SKU435: - cost: 272 - init_stock: 600 - price: 272 - service_level: 0.96 - vlt: 3 - SKU436: - cost: 72 - init_stock: 1620 - price: 72 - service_level: 0.96 - vlt: 1 - SKU437: - cost: 115 - init_stock: 280 - price: 115 - service_level: 0.96 - vlt: 2 - SKU438: - cost: 143 - init_stock: 200 - price: 143 - service_level: 0.96 - vlt: 3 - SKU439: - cost: 256 - init_stock: 760 - price: 256 - service_level: 0.96 - vlt: 1 - SKU44: - cost: 360 - init_stock: 960 - price: 360 - service_level: 0.96 - vlt: 1 - SKU440: - cost: 248 - init_stock: 1640 - price: 248 - service_level: 0.96 - vlt: 2 - SKU441: - cost: 253 - init_stock: 1120 - price: 253 - service_level: 0.96 - vlt: 2 - SKU442: - cost: 470 - init_stock: 1760 - price: 470 - service_level: 0.96 - vlt: 2 - SKU443: - cost: 218 - init_stock: 1460 - price: 218 - service_level: 0.96 - vlt: 1 - SKU444: - cost: 156 - init_stock: 120 - price: 156 - service_level: 0.96 - vlt: 2 - SKU445: - cost: 260 - init_stock: 1720 - price: 260 - service_level: 0.96 - vlt: 2 - SKU446: - cost: 497 - init_stock: 980 - price: 497 - service_level: 0.96 - vlt: 1 - SKU447: - cost: 196 - init_stock: 100 - price: 196 - service_level: 0.96 - vlt: 1 - SKU448: - cost: 485 - init_stock: 1420 - price: 485 - service_level: 0.96 - vlt: 3 - SKU449: - cost: 282 - init_stock: 760 - price: 282 - service_level: 0.96 - vlt: 2 - SKU45: - cost: 253 - init_stock: 200 - price: 253 - service_level: 0.96 - vlt: 1 - SKU450: - cost: 58 - init_stock: 1960 - price: 58 - service_level: 0.96 - vlt: 2 - SKU451: - cost: 468 - init_stock: 1380 - price: 468 - service_level: 0.96 - vlt: 3 - SKU452: - cost: 63 - init_stock: 1580 - price: 63 - service_level: 0.96 - vlt: 1 - SKU453: - cost: 37 - init_stock: 700 - price: 37 - service_level: 0.96 - vlt: 3 - SKU454: - cost: 494 - init_stock: 1700 - price: 494 - service_level: 0.96 - vlt: 1 - SKU455: - cost: 136 - init_stock: 520 - price: 136 - service_level: 0.96 - vlt: 2 - SKU456: - cost: 338 - init_stock: 500 - price: 338 - service_level: 0.96 - vlt: 2 - SKU457: - cost: 169 - init_stock: 1280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU458: - cost: 403 - init_stock: 140 - price: 403 - service_level: 0.96 - vlt: 3 - SKU459: - cost: 425 - init_stock: 680 - price: 425 - service_level: 0.96 - vlt: 3 - SKU46: - cost: 299 - init_stock: 1000 - price: 299 - service_level: 0.96 - vlt: 3 - SKU460: - cost: 405 - init_stock: 1460 - price: 405 - service_level: 0.96 - vlt: 2 - SKU461: - cost: 251 - init_stock: 960 - price: 251 - service_level: 0.96 - vlt: 1 - SKU462: - cost: 448 - init_stock: 180 - price: 448 - service_level: 0.96 - vlt: 1 - SKU463: - cost: 187 - init_stock: 1640 - price: 187 - service_level: 0.96 - vlt: 3 - SKU464: - cost: 279 - init_stock: 680 - price: 279 - service_level: 0.96 - vlt: 3 - SKU465: - cost: 147 - init_stock: 2000 - price: 147 - service_level: 0.96 - vlt: 2 - SKU466: - cost: 97 - init_stock: 840 - price: 97 - service_level: 0.96 - vlt: 1 - SKU467: - cost: 142 - init_stock: 720 - price: 142 - service_level: 0.96 - vlt: 2 - SKU468: - cost: 55 - init_stock: 1000 - price: 55 - service_level: 0.96 - vlt: 2 - SKU469: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 3 - SKU47: - cost: 121 - init_stock: 780 - price: 121 - service_level: 0.96 - vlt: 1 - SKU470: - cost: 373 - init_stock: 640 - price: 373 - service_level: 0.96 - vlt: 2 - SKU471: - cost: 315 - init_stock: 1420 - price: 315 - service_level: 0.96 - vlt: 3 - SKU472: - cost: 421 - init_stock: 1180 - price: 421 - service_level: 0.96 - vlt: 2 - SKU473: - cost: 195 - init_stock: 420 - price: 195 - service_level: 0.96 - vlt: 1 - SKU474: - cost: 448 - init_stock: 1720 - price: 448 - service_level: 0.96 - vlt: 2 - SKU475: - cost: 34 - init_stock: 1880 - price: 34 - service_level: 0.96 - vlt: 1 - SKU476: - cost: 251 - init_stock: 1800 - price: 251 - service_level: 0.96 - vlt: 1 - SKU477: - cost: 289 - init_stock: 940 - price: 289 - service_level: 0.96 - vlt: 2 - SKU478: - cost: 479 - init_stock: 1760 - price: 479 - service_level: 0.96 - vlt: 2 - SKU479: - cost: 366 - init_stock: 760 - price: 366 - service_level: 0.96 - vlt: 1 - SKU48: - cost: 340 - init_stock: 1160 - price: 340 - service_level: 0.96 - vlt: 2 - SKU480: - cost: 18 - init_stock: 600 - price: 18 - service_level: 0.96 - vlt: 2 - SKU481: - cost: 330 - init_stock: 1760 - price: 330 - service_level: 0.96 - vlt: 1 - SKU482: - cost: 94 - init_stock: 1740 - price: 94 - service_level: 0.96 - vlt: 3 - SKU483: - cost: 298 - init_stock: 680 - price: 298 - service_level: 0.96 - vlt: 2 - SKU484: - cost: 84 - init_stock: 1460 - price: 84 - service_level: 0.96 - vlt: 3 - SKU485: - cost: 318 - init_stock: 1340 - price: 318 - service_level: 0.96 - vlt: 3 - SKU486: - cost: 122 - init_stock: 1040 - price: 122 - service_level: 0.96 - vlt: 1 - SKU487: - cost: 277 - init_stock: 1320 - price: 277 - service_level: 0.96 - vlt: 3 - SKU488: - cost: 36 - init_stock: 540 - price: 36 - service_level: 0.96 - vlt: 3 - SKU489: - cost: 59 - init_stock: 620 - price: 59 - service_level: 0.96 - vlt: 3 - SKU49: - cost: 263 - init_stock: 1900 - price: 263 - service_level: 0.96 - vlt: 3 - SKU490: - cost: 161 - init_stock: 1620 - price: 161 - service_level: 0.96 - vlt: 2 - SKU491: - cost: 444 - init_stock: 1500 - price: 444 - service_level: 0.96 - vlt: 2 - SKU492: - cost: 53 - init_stock: 1600 - price: 53 - service_level: 0.96 - vlt: 2 - SKU493: - cost: 461 - init_stock: 540 - price: 461 - service_level: 0.96 - vlt: 1 - SKU494: - cost: 145 - init_stock: 1880 - price: 145 - service_level: 0.96 - vlt: 1 - SKU495: - cost: 149 - init_stock: 1240 - price: 149 - service_level: 0.96 - vlt: 3 - SKU496: - cost: 342 - init_stock: 1180 - price: 342 - service_level: 0.96 - vlt: 1 - SKU497: - cost: 33 - init_stock: 180 - price: 33 - service_level: 0.96 - vlt: 3 - SKU498: - cost: 305 - init_stock: 1300 - price: 305 - service_level: 0.96 - vlt: 3 - SKU499: - cost: 307 - init_stock: 580 - price: 307 - service_level: 0.96 - vlt: 2 - SKU5: - cost: 466 - init_stock: 1420 - price: 466 - service_level: 0.96 - vlt: 2 - SKU50: - cost: 282 - init_stock: 740 - price: 282 - service_level: 0.96 - vlt: 3 - SKU500: - cost: 208 - init_stock: 840 - price: 208 - service_level: 0.96 - vlt: 3 - SKU501: - cost: 194 - init_stock: 1520 - price: 194 - service_level: 0.96 - vlt: 1 - SKU502: - cost: 69 - init_stock: 1300 - price: 69 - service_level: 0.96 - vlt: 2 - SKU503: - cost: 208 - init_stock: 1140 - price: 208 - service_level: 0.96 - vlt: 1 - SKU504: - cost: 251 - init_stock: 600 - price: 251 - service_level: 0.96 - vlt: 2 - SKU505: - cost: 426 - init_stock: 1180 - price: 426 - service_level: 0.96 - vlt: 1 - SKU506: - cost: 149 - init_stock: 1860 - price: 149 - service_level: 0.96 - vlt: 3 - SKU507: - cost: 187 - init_stock: 300 - price: 187 - service_level: 0.96 - vlt: 1 - SKU508: - cost: 272 - init_stock: 1920 - price: 272 - service_level: 0.96 - vlt: 1 - SKU509: - cost: 297 - init_stock: 1620 - price: 297 - service_level: 0.96 - vlt: 1 - SKU51: - cost: 295 - init_stock: 1340 - price: 295 - service_level: 0.96 - vlt: 3 - SKU510: - cost: 94 - init_stock: 180 - price: 94 - service_level: 0.96 - vlt: 1 - SKU511: - cost: 361 - init_stock: 1500 - price: 361 - service_level: 0.96 - vlt: 1 - SKU512: - cost: 467 - init_stock: 440 - price: 467 - service_level: 0.96 - vlt: 2 - SKU513: - cost: 343 - init_stock: 140 - price: 343 - service_level: 0.96 - vlt: 3 - SKU514: - cost: 186 - init_stock: 1260 - price: 186 - service_level: 0.96 - vlt: 3 - SKU515: - cost: 145 - init_stock: 1080 - price: 145 - service_level: 0.96 - vlt: 3 - SKU516: - cost: 64 - init_stock: 760 - price: 64 - service_level: 0.96 - vlt: 1 - SKU517: - cost: 117 - init_stock: 180 - price: 117 - service_level: 0.96 - vlt: 3 - SKU518: - cost: 26 - init_stock: 420 - price: 26 - service_level: 0.96 - vlt: 3 - SKU519: - cost: 233 - init_stock: 1000 - price: 233 - service_level: 0.96 - vlt: 2 - SKU52: - cost: 22 - init_stock: 1340 - price: 22 - service_level: 0.96 - vlt: 1 - SKU520: - cost: 209 - init_stock: 440 - price: 209 - service_level: 0.96 - vlt: 1 - SKU521: - cost: 220 - init_stock: 1000 - price: 220 - service_level: 0.96 - vlt: 3 - SKU522: - cost: 156 - init_stock: 1740 - price: 156 - service_level: 0.96 - vlt: 1 - SKU523: - cost: 65 - init_stock: 2000 - price: 65 - service_level: 0.96 - vlt: 3 - SKU524: - cost: 294 - init_stock: 1880 - price: 294 - service_level: 0.96 - vlt: 1 - SKU525: - cost: 432 - init_stock: 840 - price: 432 - service_level: 0.96 - vlt: 2 - SKU526: - cost: 419 - init_stock: 480 - price: 419 - service_level: 0.96 - vlt: 3 - SKU527: - cost: 131 - init_stock: 1400 - price: 131 - service_level: 0.96 - vlt: 1 - SKU528: - cost: 316 - init_stock: 420 - price: 316 - service_level: 0.96 - vlt: 3 - SKU529: - cost: 298 - init_stock: 620 - price: 298 - service_level: 0.96 - vlt: 3 - SKU53: - cost: 151 - init_stock: 1400 - price: 151 - service_level: 0.96 - vlt: 2 - SKU530: - cost: 131 - init_stock: 900 - price: 131 - service_level: 0.96 - vlt: 1 - SKU531: - cost: 103 - init_stock: 1200 - price: 103 - service_level: 0.96 - vlt: 3 - SKU532: - cost: 351 - init_stock: 1540 - price: 351 - service_level: 0.96 - vlt: 3 - SKU533: - cost: 296 - init_stock: 840 - price: 296 - service_level: 0.96 - vlt: 3 - SKU534: - cost: 425 - init_stock: 820 - price: 425 - service_level: 0.96 - vlt: 1 - SKU535: - cost: 304 - init_stock: 1720 - price: 304 - service_level: 0.96 - vlt: 2 - SKU536: - cost: 358 - init_stock: 1040 - price: 358 - service_level: 0.96 - vlt: 2 - SKU537: - cost: 321 - init_stock: 180 - price: 321 - service_level: 0.96 - vlt: 3 - SKU538: - cost: 457 - init_stock: 1000 - price: 457 - service_level: 0.96 - vlt: 1 - SKU539: - cost: 165 - init_stock: 1560 - price: 165 - service_level: 0.96 - vlt: 2 - SKU54: - cost: 158 - init_stock: 920 - price: 158 - service_level: 0.96 - vlt: 1 - SKU540: - cost: 388 - init_stock: 840 - price: 388 - service_level: 0.96 - vlt: 3 - SKU541: - cost: 131 - init_stock: 580 - price: 131 - service_level: 0.96 - vlt: 2 - SKU542: - cost: 38 - init_stock: 520 - price: 38 - service_level: 0.96 - vlt: 3 - SKU543: - cost: 430 - init_stock: 1700 - price: 430 - service_level: 0.96 - vlt: 2 - SKU544: - cost: 346 - init_stock: 1940 - price: 346 - service_level: 0.96 - vlt: 1 - SKU545: - cost: 175 - init_stock: 300 - price: 175 - service_level: 0.96 - vlt: 3 - SKU546: - cost: 491 - init_stock: 1920 - price: 491 - service_level: 0.96 - vlt: 3 - SKU547: - cost: 161 - init_stock: 880 - price: 161 - service_level: 0.96 - vlt: 2 - SKU548: - cost: 111 - init_stock: 120 - price: 111 - service_level: 0.96 - vlt: 1 - SKU549: - cost: 387 - init_stock: 1580 - price: 387 - service_level: 0.96 - vlt: 1 - SKU55: - cost: 482 - init_stock: 1300 - price: 482 - service_level: 0.96 - vlt: 3 - SKU550: - cost: 259 - init_stock: 1880 - price: 259 - service_level: 0.96 - vlt: 1 - SKU551: - cost: 46 - init_stock: 620 - price: 46 - service_level: 0.96 - vlt: 2 - SKU552: - cost: 191 - init_stock: 1800 - price: 191 - service_level: 0.96 - vlt: 3 - SKU553: - cost: 208 - init_stock: 600 - price: 208 - service_level: 0.96 - vlt: 1 - SKU554: - cost: 340 - init_stock: 820 - price: 340 - service_level: 0.96 - vlt: 1 - SKU555: - cost: 489 - init_stock: 1680 - price: 489 - service_level: 0.96 - vlt: 1 - SKU556: - cost: 110 - init_stock: 600 - price: 110 - service_level: 0.96 - vlt: 2 - SKU557: - cost: 334 - init_stock: 1460 - price: 334 - service_level: 0.96 - vlt: 2 - SKU558: - cost: 399 - init_stock: 340 - price: 399 - service_level: 0.96 - vlt: 1 - SKU559: - cost: 313 - init_stock: 1080 - price: 313 - service_level: 0.96 - vlt: 1 - SKU56: - cost: 377 - init_stock: 1480 - price: 377 - service_level: 0.96 - vlt: 1 - SKU560: - cost: 283 - init_stock: 820 - price: 283 - service_level: 0.96 - vlt: 1 - SKU561: - cost: 202 - init_stock: 780 - price: 202 - service_level: 0.96 - vlt: 3 - SKU562: - cost: 347 - init_stock: 1780 - price: 347 - service_level: 0.96 - vlt: 2 - SKU563: - cost: 401 - init_stock: 100 - price: 401 - service_level: 0.96 - vlt: 3 - SKU564: - cost: 109 - init_stock: 180 - price: 109 - service_level: 0.96 - vlt: 2 - SKU565: - cost: 57 - init_stock: 1500 - price: 57 - service_level: 0.96 - vlt: 3 - SKU566: - cost: 131 - init_stock: 220 - price: 131 - service_level: 0.96 - vlt: 2 - SKU567: - cost: 361 - init_stock: 180 - price: 361 - service_level: 0.96 - vlt: 1 - SKU568: - cost: 75 - init_stock: 1560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU569: - cost: 473 - init_stock: 960 - price: 473 - service_level: 0.96 - vlt: 2 - SKU57: - cost: 359 - init_stock: 1000 - price: 359 - service_level: 0.96 - vlt: 2 - SKU570: - cost: 388 - init_stock: 1660 - price: 388 - service_level: 0.96 - vlt: 2 - SKU571: - cost: 168 - init_stock: 780 - price: 168 - service_level: 0.96 - vlt: 1 - SKU572: - cost: 26 - init_stock: 900 - price: 26 - service_level: 0.96 - vlt: 3 - SKU573: - cost: 246 - init_stock: 820 - price: 246 - service_level: 0.96 - vlt: 2 - SKU574: - cost: 94 - init_stock: 1000 - price: 94 - service_level: 0.96 - vlt: 2 - SKU575: - cost: 237 - init_stock: 1580 - price: 237 - service_level: 0.96 - vlt: 1 - SKU576: - cost: 265 - init_stock: 1560 - price: 265 - service_level: 0.96 - vlt: 3 - SKU577: - cost: 18 - init_stock: 1740 - price: 18 - service_level: 0.96 - vlt: 3 - SKU578: - cost: 100 - init_stock: 1420 - price: 100 - service_level: 0.96 - vlt: 2 - SKU579: - cost: 415 - init_stock: 180 - price: 415 - service_level: 0.96 - vlt: 1 - SKU58: - cost: 362 - init_stock: 960 - price: 362 - service_level: 0.96 - vlt: 2 - SKU580: - cost: 203 - init_stock: 100 - price: 203 - service_level: 0.96 - vlt: 1 - SKU581: - cost: 152 - init_stock: 1720 - price: 152 - service_level: 0.96 - vlt: 2 - SKU582: - cost: 359 - init_stock: 1920 - price: 359 - service_level: 0.96 - vlt: 2 - SKU583: - cost: 86 - init_stock: 1740 - price: 86 - service_level: 0.96 - vlt: 1 - SKU584: - cost: 33 - init_stock: 900 - price: 33 - service_level: 0.96 - vlt: 2 - SKU585: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 2 - SKU586: - cost: 61 - init_stock: 880 - price: 61 - service_level: 0.96 - vlt: 2 - SKU587: - cost: 290 - init_stock: 1560 - price: 290 - service_level: 0.96 - vlt: 2 - SKU588: - cost: 476 - init_stock: 880 - price: 476 - service_level: 0.96 - vlt: 2 - SKU589: - cost: 244 - init_stock: 680 - price: 244 - service_level: 0.96 - vlt: 3 - SKU59: - cost: 145 - init_stock: 1020 - price: 145 - service_level: 0.96 - vlt: 1 - SKU590: - cost: 70 - init_stock: 620 - price: 70 - service_level: 0.96 - vlt: 2 - SKU591: - cost: 206 - init_stock: 1680 - price: 206 - service_level: 0.96 - vlt: 2 - SKU592: - cost: 218 - init_stock: 920 - price: 218 - service_level: 0.96 - vlt: 1 - SKU593: - cost: 124 - init_stock: 880 - price: 124 - service_level: 0.96 - vlt: 1 - SKU594: - cost: 238 - init_stock: 1000 - price: 238 - service_level: 0.96 - vlt: 1 - SKU595: - cost: 73 - init_stock: 860 - price: 73 - service_level: 0.96 - vlt: 3 - SKU596: - cost: 432 - init_stock: 140 - price: 432 - service_level: 0.96 - vlt: 2 - SKU597: - cost: 252 - init_stock: 1740 - price: 252 - service_level: 0.96 - vlt: 2 - SKU598: - cost: 348 - init_stock: 280 - price: 348 - service_level: 0.96 - vlt: 1 - SKU599: - cost: 54 - init_stock: 1360 - price: 54 - service_level: 0.96 - vlt: 2 - SKU6: - cost: 122 - init_stock: 1760 - price: 122 - service_level: 0.96 - vlt: 2 - SKU60: - cost: 200 - init_stock: 780 - price: 200 - service_level: 0.96 - vlt: 1 - SKU600: - cost: 386 - init_stock: 1300 - price: 386 - service_level: 0.96 - vlt: 1 - SKU601: - cost: 138 - init_stock: 780 - price: 138 - service_level: 0.96 - vlt: 3 - SKU602: - cost: 184 - init_stock: 1960 - price: 184 - service_level: 0.96 - vlt: 1 - SKU603: - cost: 278 - init_stock: 840 - price: 278 - service_level: 0.96 - vlt: 3 - SKU604: - cost: 270 - init_stock: 1000 - price: 270 - service_level: 0.96 - vlt: 3 - SKU605: - cost: 288 - init_stock: 480 - price: 288 - service_level: 0.96 - vlt: 2 - SKU606: - cost: 114 - init_stock: 220 - price: 114 - service_level: 0.96 - vlt: 3 - SKU607: - cost: 208 - init_stock: 1580 - price: 208 - service_level: 0.96 - vlt: 2 - SKU608: - cost: 39 - init_stock: 1460 - price: 39 - service_level: 0.96 - vlt: 1 - SKU609: - cost: 102 - init_stock: 380 - price: 102 - service_level: 0.96 - vlt: 2 - SKU61: - cost: 461 - init_stock: 1500 - price: 461 - service_level: 0.96 - vlt: 1 - SKU610: - cost: 449 - init_stock: 1360 - price: 449 - service_level: 0.96 - vlt: 1 - SKU611: - cost: 306 - init_stock: 1540 - price: 306 - service_level: 0.96 - vlt: 3 - SKU612: - cost: 391 - init_stock: 1760 - price: 391 - service_level: 0.96 - vlt: 2 - SKU613: - cost: 174 - init_stock: 140 - price: 174 - service_level: 0.96 - vlt: 3 - SKU614: - cost: 173 - init_stock: 300 - price: 173 - service_level: 0.96 - vlt: 1 - SKU615: - cost: 330 - init_stock: 1820 - price: 330 - service_level: 0.96 - vlt: 2 - SKU616: - cost: 232 - init_stock: 1460 - price: 232 - service_level: 0.96 - vlt: 1 - SKU617: - cost: 203 - init_stock: 1200 - price: 203 - service_level: 0.96 - vlt: 3 - SKU618: - cost: 77 - init_stock: 460 - price: 77 - service_level: 0.96 - vlt: 1 - SKU619: - cost: 189 - init_stock: 1720 - price: 189 - service_level: 0.96 - vlt: 2 - SKU62: - cost: 124 - init_stock: 180 - price: 124 - service_level: 0.96 - vlt: 2 - SKU620: - cost: 231 - init_stock: 780 - price: 231 - service_level: 0.96 - vlt: 3 - SKU621: - cost: 199 - init_stock: 1440 - price: 199 - service_level: 0.96 - vlt: 2 - SKU622: - cost: 253 - init_stock: 1300 - price: 253 - service_level: 0.96 - vlt: 3 - SKU623: - cost: 335 - init_stock: 1100 - price: 335 - service_level: 0.96 - vlt: 1 - SKU624: - cost: 482 - init_stock: 1080 - price: 482 - service_level: 0.96 - vlt: 2 - SKU625: - cost: 46 - init_stock: 1780 - price: 46 - service_level: 0.96 - vlt: 1 - SKU626: - cost: 451 - init_stock: 1780 - price: 451 - service_level: 0.96 - vlt: 3 - SKU627: - cost: 220 - init_stock: 1640 - price: 220 - service_level: 0.96 - vlt: 3 - SKU628: - cost: 124 - init_stock: 520 - price: 124 - service_level: 0.96 - vlt: 2 - SKU629: - cost: 353 - init_stock: 1840 - price: 353 - service_level: 0.96 - vlt: 1 - SKU63: - cost: 68 - init_stock: 280 - price: 68 - service_level: 0.96 - vlt: 3 - SKU630: - cost: 122 - init_stock: 340 - price: 122 - service_level: 0.96 - vlt: 1 - SKU631: - cost: 296 - init_stock: 980 - price: 296 - service_level: 0.96 - vlt: 2 - SKU632: - cost: 85 - init_stock: 1880 - price: 85 - service_level: 0.96 - vlt: 2 - SKU633: - cost: 184 - init_stock: 1340 - price: 184 - service_level: 0.96 - vlt: 2 - SKU634: - cost: 17 - init_stock: 1460 - price: 17 - service_level: 0.96 - vlt: 1 - SKU635: - cost: 341 - init_stock: 1860 - price: 341 - service_level: 0.96 - vlt: 1 - SKU636: - cost: 385 - init_stock: 740 - price: 385 - service_level: 0.96 - vlt: 1 - SKU637: - cost: 83 - init_stock: 1220 - price: 83 - service_level: 0.96 - vlt: 3 - SKU638: - cost: 375 - init_stock: 160 - price: 375 - service_level: 0.96 - vlt: 1 - SKU639: - cost: 327 - init_stock: 800 - price: 327 - service_level: 0.96 - vlt: 2 - SKU64: - cost: 36 - init_stock: 380 - price: 36 - service_level: 0.96 - vlt: 3 - SKU640: - cost: 275 - init_stock: 1820 - price: 275 - service_level: 0.96 - vlt: 1 - SKU641: - cost: 365 - init_stock: 1620 - price: 365 - service_level: 0.96 - vlt: 1 - SKU642: - cost: 414 - init_stock: 500 - price: 414 - service_level: 0.96 - vlt: 2 - SKU643: - cost: 358 - init_stock: 920 - price: 358 - service_level: 0.96 - vlt: 2 - SKU644: - cost: 46 - init_stock: 1640 - price: 46 - service_level: 0.96 - vlt: 2 - SKU645: - cost: 467 - init_stock: 1980 - price: 467 - service_level: 0.96 - vlt: 3 - SKU646: - cost: 189 - init_stock: 260 - price: 189 - service_level: 0.96 - vlt: 3 - SKU647: - cost: 303 - init_stock: 1540 - price: 303 - service_level: 0.96 - vlt: 3 - SKU648: - cost: 226 - init_stock: 1100 - price: 226 - service_level: 0.96 - vlt: 1 - SKU649: - cost: 198 - init_stock: 500 - price: 198 - service_level: 0.96 - vlt: 3 - SKU65: - cost: 15 - init_stock: 1880 - price: 15 - service_level: 0.96 - vlt: 2 - SKU650: - cost: 351 - init_stock: 1120 - price: 351 - service_level: 0.96 - vlt: 2 - SKU651: - cost: 380 - init_stock: 1920 - price: 380 - service_level: 0.96 - vlt: 3 - SKU652: - cost: 123 - init_stock: 800 - price: 123 - service_level: 0.96 - vlt: 2 - SKU653: - cost: 479 - init_stock: 1920 - price: 479 - service_level: 0.96 - vlt: 2 - SKU654: - cost: 66 - init_stock: 160 - price: 66 - service_level: 0.96 - vlt: 3 - SKU655: - cost: 333 - init_stock: 840 - price: 333 - service_level: 0.96 - vlt: 1 - SKU656: - cost: 53 - init_stock: 1440 - price: 53 - service_level: 0.96 - vlt: 2 - SKU657: - cost: 73 - init_stock: 1620 - price: 73 - service_level: 0.96 - vlt: 2 - SKU658: - cost: 70 - init_stock: 720 - price: 70 - service_level: 0.96 - vlt: 3 - SKU659: - cost: 21 - init_stock: 1120 - price: 21 - service_level: 0.96 - vlt: 1 - SKU66: - cost: 143 - init_stock: 1800 - price: 143 - service_level: 0.96 - vlt: 1 - SKU660: - cost: 487 - init_stock: 1660 - price: 487 - service_level: 0.96 - vlt: 1 - SKU661: - cost: 344 - init_stock: 1480 - price: 344 - service_level: 0.96 - vlt: 2 - SKU662: - cost: 372 - init_stock: 760 - price: 372 - service_level: 0.96 - vlt: 3 - SKU663: - cost: 418 - init_stock: 800 - price: 418 - service_level: 0.96 - vlt: 3 - SKU664: - cost: 351 - init_stock: 1680 - price: 351 - service_level: 0.96 - vlt: 1 - SKU665: - cost: 493 - init_stock: 860 - price: 493 - service_level: 0.96 - vlt: 3 - SKU666: - cost: 341 - init_stock: 1760 - price: 341 - service_level: 0.96 - vlt: 1 - SKU667: - cost: 325 - init_stock: 260 - price: 325 - service_level: 0.96 - vlt: 3 - SKU668: - cost: 286 - init_stock: 1200 - price: 286 - service_level: 0.96 - vlt: 3 - SKU669: - cost: 74 - init_stock: 320 - price: 74 - service_level: 0.96 - vlt: 1 - SKU67: - cost: 247 - init_stock: 1060 - price: 247 - service_level: 0.96 - vlt: 1 - SKU670: - cost: 289 - init_stock: 1720 - price: 289 - service_level: 0.96 - vlt: 1 - SKU671: - cost: 267 - init_stock: 1660 - price: 267 - service_level: 0.96 - vlt: 3 - SKU672: - cost: 369 - init_stock: 260 - price: 369 - service_level: 0.96 - vlt: 2 - SKU673: - cost: 322 - init_stock: 820 - price: 322 - service_level: 0.96 - vlt: 2 - SKU674: - cost: 223 - init_stock: 440 - price: 223 - service_level: 0.96 - vlt: 1 - SKU675: - cost: 438 - init_stock: 660 - price: 438 - service_level: 0.96 - vlt: 1 - SKU676: - cost: 244 - init_stock: 1220 - price: 244 - service_level: 0.96 - vlt: 1 - SKU677: - cost: 425 - init_stock: 880 - price: 425 - service_level: 0.96 - vlt: 2 - SKU678: - cost: 168 - init_stock: 720 - price: 168 - service_level: 0.96 - vlt: 1 - SKU679: - cost: 395 - init_stock: 880 - price: 395 - service_level: 0.96 - vlt: 2 - SKU68: - cost: 169 - init_stock: 280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU680: - cost: 260 - init_stock: 600 - price: 260 - service_level: 0.96 - vlt: 1 - SKU681: - cost: 464 - init_stock: 1940 - price: 464 - service_level: 0.96 - vlt: 3 - SKU682: - cost: 370 - init_stock: 1060 - price: 370 - service_level: 0.96 - vlt: 2 - SKU683: - cost: 327 - init_stock: 1340 - price: 327 - service_level: 0.96 - vlt: 3 - SKU684: - cost: 355 - init_stock: 1580 - price: 355 - service_level: 0.96 - vlt: 1 - SKU685: - cost: 422 - init_stock: 900 - price: 422 - service_level: 0.96 - vlt: 2 - SKU686: - cost: 63 - init_stock: 1260 - price: 63 - service_level: 0.96 - vlt: 1 - SKU687: - cost: 34 - init_stock: 1520 - price: 34 - service_level: 0.96 - vlt: 2 - SKU688: - cost: 484 - init_stock: 1540 - price: 484 - service_level: 0.96 - vlt: 3 - SKU689: - cost: 499 - init_stock: 300 - price: 499 - service_level: 0.96 - vlt: 1 - SKU69: - cost: 176 - init_stock: 1340 - price: 176 - service_level: 0.96 - vlt: 1 - SKU690: - cost: 437 - init_stock: 1660 - price: 437 - service_level: 0.96 - vlt: 2 - SKU691: - cost: 17 - init_stock: 1580 - price: 17 - service_level: 0.96 - vlt: 3 - SKU692: - cost: 225 - init_stock: 1300 - price: 225 - service_level: 0.96 - vlt: 1 - SKU693: - cost: 19 - init_stock: 1220 - price: 19 - service_level: 0.96 - vlt: 2 - SKU694: - cost: 208 - init_stock: 1500 - price: 208 - service_level: 0.96 - vlt: 3 - SKU695: - cost: 190 - init_stock: 140 - price: 190 - service_level: 0.96 - vlt: 2 - SKU696: - cost: 348 - init_stock: 1160 - price: 348 - service_level: 0.96 - vlt: 1 - SKU697: - cost: 455 - init_stock: 1600 - price: 455 - service_level: 0.96 - vlt: 1 - SKU698: - cost: 198 - init_stock: 1220 - price: 198 - service_level: 0.96 - vlt: 3 - SKU699: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 3 - SKU7: - cost: 101 - init_stock: 1920 - price: 101 - service_level: 0.96 - vlt: 2 - SKU70: - cost: 186 - init_stock: 700 - price: 186 - service_level: 0.96 - vlt: 1 - SKU700: - cost: 74 - init_stock: 180 - price: 74 - service_level: 0.96 - vlt: 2 - SKU701: - cost: 399 - init_stock: 1540 - price: 399 - service_level: 0.96 - vlt: 1 - SKU702: - cost: 82 - init_stock: 680 - price: 82 - service_level: 0.96 - vlt: 1 - SKU703: - cost: 363 - init_stock: 760 - price: 363 - service_level: 0.96 - vlt: 1 - SKU704: - cost: 384 - init_stock: 1740 - price: 384 - service_level: 0.96 - vlt: 2 - SKU705: - cost: 128 - init_stock: 1260 - price: 128 - service_level: 0.96 - vlt: 2 - SKU706: - cost: 182 - init_stock: 1820 - price: 182 - service_level: 0.96 - vlt: 2 - SKU707: - cost: 18 - init_stock: 1380 - price: 18 - service_level: 0.96 - vlt: 1 - SKU708: - cost: 178 - init_stock: 560 - price: 178 - service_level: 0.96 - vlt: 3 - SKU709: - cost: 102 - init_stock: 1060 - price: 102 - service_level: 0.96 - vlt: 3 - SKU71: - cost: 315 - init_stock: 900 - price: 315 - service_level: 0.96 - vlt: 2 - SKU710: - cost: 252 - init_stock: 380 - price: 252 - service_level: 0.96 - vlt: 2 - SKU711: - cost: 281 - init_stock: 1380 - price: 281 - service_level: 0.96 - vlt: 1 - SKU712: - cost: 232 - init_stock: 220 - price: 232 - service_level: 0.96 - vlt: 2 - SKU713: - cost: 285 - init_stock: 860 - price: 285 - service_level: 0.96 - vlt: 2 - SKU714: - cost: 79 - init_stock: 820 - price: 79 - service_level: 0.96 - vlt: 3 - SKU715: - cost: 234 - init_stock: 340 - price: 234 - service_level: 0.96 - vlt: 1 - SKU716: - cost: 70 - init_stock: 1500 - price: 70 - service_level: 0.96 - vlt: 2 - SKU717: - cost: 345 - init_stock: 160 - price: 345 - service_level: 0.96 - vlt: 2 - SKU718: - cost: 357 - init_stock: 1160 - price: 357 - service_level: 0.96 - vlt: 2 - SKU719: - cost: 340 - init_stock: 1300 - price: 340 - service_level: 0.96 - vlt: 3 - SKU72: - cost: 458 - init_stock: 1700 - price: 458 - service_level: 0.96 - vlt: 2 - SKU720: - cost: 325 - init_stock: 840 - price: 325 - service_level: 0.96 - vlt: 3 - SKU721: - cost: 73 - init_stock: 220 - price: 73 - service_level: 0.96 - vlt: 2 - SKU722: - cost: 392 - init_stock: 1940 - price: 392 - service_level: 0.96 - vlt: 1 - SKU723: - cost: 318 - init_stock: 1780 - price: 318 - service_level: 0.96 - vlt: 3 - SKU724: - cost: 400 - init_stock: 660 - price: 400 - service_level: 0.96 - vlt: 1 - SKU725: - cost: 175 - init_stock: 740 - price: 175 - service_level: 0.96 - vlt: 1 - SKU726: - cost: 458 - init_stock: 1780 - price: 458 - service_level: 0.96 - vlt: 3 - SKU727: - cost: 418 - init_stock: 1140 - price: 418 - service_level: 0.96 - vlt: 1 - SKU728: - cost: 475 - init_stock: 740 - price: 475 - service_level: 0.96 - vlt: 3 - SKU729: - cost: 324 - init_stock: 720 - price: 324 - service_level: 0.96 - vlt: 3 - SKU73: - cost: 212 - init_stock: 1080 - price: 212 - service_level: 0.96 - vlt: 2 - SKU730: - cost: 16 - init_stock: 260 - price: 16 - service_level: 0.96 - vlt: 2 - SKU731: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU732: - cost: 41 - init_stock: 1240 - price: 41 - service_level: 0.96 - vlt: 3 - SKU733: - cost: 315 - init_stock: 520 - price: 315 - service_level: 0.96 - vlt: 3 - SKU734: - cost: 37 - init_stock: 1020 - price: 37 - service_level: 0.96 - vlt: 3 - SKU735: - cost: 266 - init_stock: 1980 - price: 266 - service_level: 0.96 - vlt: 3 - SKU736: - cost: 368 - init_stock: 500 - price: 368 - service_level: 0.96 - vlt: 2 - SKU737: - cost: 475 - init_stock: 620 - price: 475 - service_level: 0.96 - vlt: 2 - SKU738: - cost: 185 - init_stock: 960 - price: 185 - service_level: 0.96 - vlt: 3 - SKU739: - cost: 475 - init_stock: 1480 - price: 475 - service_level: 0.96 - vlt: 2 - SKU74: - cost: 159 - init_stock: 840 - price: 159 - service_level: 0.96 - vlt: 1 - SKU740: - cost: 390 - init_stock: 1280 - price: 390 - service_level: 0.96 - vlt: 1 - SKU741: - cost: 91 - init_stock: 1120 - price: 91 - service_level: 0.96 - vlt: 1 - SKU742: - cost: 188 - init_stock: 440 - price: 188 - service_level: 0.96 - vlt: 1 - SKU743: - cost: 217 - init_stock: 860 - price: 217 - service_level: 0.96 - vlt: 3 - SKU744: - cost: 379 - init_stock: 1160 - price: 379 - service_level: 0.96 - vlt: 1 - SKU745: - cost: 316 - init_stock: 1840 - price: 316 - service_level: 0.96 - vlt: 2 - SKU746: - cost: 437 - init_stock: 800 - price: 437 - service_level: 0.96 - vlt: 2 - SKU747: - cost: 373 - init_stock: 1580 - price: 373 - service_level: 0.96 - vlt: 2 - SKU748: - cost: 275 - init_stock: 1320 - price: 275 - service_level: 0.96 - vlt: 2 - SKU749: - cost: 394 - init_stock: 1060 - price: 394 - service_level: 0.96 - vlt: 1 - SKU75: - cost: 131 - init_stock: 380 - price: 131 - service_level: 0.96 - vlt: 1 - SKU750: - cost: 256 - init_stock: 1240 - price: 256 - service_level: 0.96 - vlt: 3 - SKU751: - cost: 369 - init_stock: 1300 - price: 369 - service_level: 0.96 - vlt: 3 - SKU752: - cost: 259 - init_stock: 1560 - price: 259 - service_level: 0.96 - vlt: 3 - SKU753: - cost: 77 - init_stock: 1300 - price: 77 - service_level: 0.96 - vlt: 3 - SKU754: - cost: 387 - init_stock: 260 - price: 387 - service_level: 0.96 - vlt: 2 - SKU755: - cost: 354 - init_stock: 1600 - price: 354 - service_level: 0.96 - vlt: 3 - SKU756: - cost: 246 - init_stock: 1800 - price: 246 - service_level: 0.96 - vlt: 1 - SKU757: - cost: 40 - init_stock: 1140 - price: 40 - service_level: 0.96 - vlt: 3 - SKU758: - cost: 241 - init_stock: 800 - price: 241 - service_level: 0.96 - vlt: 2 - SKU759: - cost: 435 - init_stock: 760 - price: 435 - service_level: 0.96 - vlt: 1 - SKU76: - cost: 147 - init_stock: 1280 - price: 147 - service_level: 0.96 - vlt: 2 - SKU760: - cost: 176 - init_stock: 1020 - price: 176 - service_level: 0.96 - vlt: 3 - SKU761: - cost: 224 - init_stock: 1100 - price: 224 - service_level: 0.96 - vlt: 1 - SKU762: - cost: 264 - init_stock: 1020 - price: 264 - service_level: 0.96 - vlt: 1 - SKU763: - cost: 385 - init_stock: 1580 - price: 385 - service_level: 0.96 - vlt: 2 - SKU764: - cost: 349 - init_stock: 680 - price: 349 - service_level: 0.96 - vlt: 1 - SKU765: - cost: 345 - init_stock: 260 - price: 345 - service_level: 0.96 - vlt: 1 - SKU766: - cost: 478 - init_stock: 400 - price: 478 - service_level: 0.96 - vlt: 2 - SKU767: - cost: 95 - init_stock: 1520 - price: 95 - service_level: 0.96 - vlt: 1 - SKU768: - cost: 181 - init_stock: 1840 - price: 181 - service_level: 0.96 - vlt: 2 - SKU769: - cost: 24 - init_stock: 580 - price: 24 - service_level: 0.96 - vlt: 2 - SKU77: - cost: 409 - init_stock: 1440 - price: 409 - service_level: 0.96 - vlt: 1 - SKU770: - cost: 150 - init_stock: 1240 - price: 150 - service_level: 0.96 - vlt: 2 - SKU771: - cost: 101 - init_stock: 140 - price: 101 - service_level: 0.96 - vlt: 1 - SKU772: - cost: 256 - init_stock: 120 - price: 256 - service_level: 0.96 - vlt: 3 - SKU773: - cost: 84 - init_stock: 1840 - price: 84 - service_level: 0.96 - vlt: 1 - SKU774: - cost: 447 - init_stock: 1180 - price: 447 - service_level: 0.96 - vlt: 1 - SKU775: - cost: 175 - init_stock: 1720 - price: 175 - service_level: 0.96 - vlt: 3 - SKU776: - cost: 103 - init_stock: 1700 - price: 103 - service_level: 0.96 - vlt: 2 - SKU777: - cost: 292 - init_stock: 1140 - price: 292 - service_level: 0.96 - vlt: 2 - SKU778: - cost: 203 - init_stock: 160 - price: 203 - service_level: 0.96 - vlt: 3 - SKU779: - cost: 117 - init_stock: 860 - price: 117 - service_level: 0.96 - vlt: 1 - SKU78: - cost: 234 - init_stock: 260 - price: 234 - service_level: 0.96 - vlt: 3 - SKU780: - cost: 69 - init_stock: 1640 - price: 69 - service_level: 0.96 - vlt: 3 - SKU781: - cost: 372 - init_stock: 720 - price: 372 - service_level: 0.96 - vlt: 3 - SKU782: - cost: 27 - init_stock: 200 - price: 27 - service_level: 0.96 - vlt: 3 - SKU783: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 2 - SKU784: - cost: 309 - init_stock: 1040 - price: 309 - service_level: 0.96 - vlt: 3 - SKU785: - cost: 191 - init_stock: 1400 - price: 191 - service_level: 0.96 - vlt: 2 - SKU786: - cost: 91 - init_stock: 440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU787: - cost: 360 - init_stock: 1220 - price: 360 - service_level: 0.96 - vlt: 1 - SKU788: - cost: 351 - init_stock: 560 - price: 351 - service_level: 0.96 - vlt: 1 - SKU789: - cost: 153 - init_stock: 1160 - price: 153 - service_level: 0.96 - vlt: 2 - SKU79: - cost: 245 - init_stock: 220 - price: 245 - service_level: 0.96 - vlt: 1 - SKU790: - cost: 417 - init_stock: 1320 - price: 417 - service_level: 0.96 - vlt: 2 - SKU791: - cost: 134 - init_stock: 560 - price: 134 - service_level: 0.96 - vlt: 1 - SKU792: - cost: 313 - init_stock: 420 - price: 313 - service_level: 0.96 - vlt: 2 - SKU793: - cost: 195 - init_stock: 1520 - price: 195 - service_level: 0.96 - vlt: 2 - SKU794: - cost: 325 - init_stock: 1600 - price: 325 - service_level: 0.96 - vlt: 3 - SKU795: - cost: 276 - init_stock: 960 - price: 276 - service_level: 0.96 - vlt: 3 - SKU796: - cost: 447 - init_stock: 980 - price: 447 - service_level: 0.96 - vlt: 1 - SKU797: - cost: 100 - init_stock: 780 - price: 100 - service_level: 0.96 - vlt: 3 - SKU798: - cost: 426 - init_stock: 600 - price: 426 - service_level: 0.96 - vlt: 2 - SKU799: - cost: 63 - init_stock: 140 - price: 63 - service_level: 0.96 - vlt: 2 - SKU8: - cost: 125 - init_stock: 1260 - price: 125 - service_level: 0.96 - vlt: 3 - SKU80: - cost: 163 - init_stock: 1400 - price: 163 - service_level: 0.96 - vlt: 1 - SKU800: - cost: 298 - init_stock: 1880 - price: 298 - service_level: 0.96 - vlt: 2 - SKU801: - cost: 389 - init_stock: 720 - price: 389 - service_level: 0.96 - vlt: 2 - SKU802: - cost: 173 - init_stock: 1240 - price: 173 - service_level: 0.96 - vlt: 2 - SKU803: - cost: 272 - init_stock: 380 - price: 272 - service_level: 0.96 - vlt: 3 - SKU804: - cost: 390 - init_stock: 940 - price: 390 - service_level: 0.96 - vlt: 3 - SKU805: - cost: 61 - init_stock: 660 - price: 61 - service_level: 0.96 - vlt: 3 - SKU806: - cost: 158 - init_stock: 1040 - price: 158 - service_level: 0.96 - vlt: 1 - SKU807: - cost: 453 - init_stock: 520 - price: 453 - service_level: 0.96 - vlt: 3 - SKU808: - cost: 249 - init_stock: 1820 - price: 249 - service_level: 0.96 - vlt: 1 - SKU809: - cost: 55 - init_stock: 1300 - price: 55 - service_level: 0.96 - vlt: 1 - SKU81: - cost: 182 - init_stock: 1320 - price: 182 - service_level: 0.96 - vlt: 3 - SKU810: - cost: 276 - init_stock: 120 - price: 276 - service_level: 0.96 - vlt: 2 - SKU811: - cost: 254 - init_stock: 740 - price: 254 - service_level: 0.96 - vlt: 3 - SKU812: - cost: 252 - init_stock: 1600 - price: 252 - service_level: 0.96 - vlt: 1 - SKU813: - cost: 253 - init_stock: 140 - price: 253 - service_level: 0.96 - vlt: 3 - SKU814: - cost: 485 - init_stock: 1940 - price: 485 - service_level: 0.96 - vlt: 1 - SKU815: - cost: 390 - init_stock: 480 - price: 390 - service_level: 0.96 - vlt: 2 - SKU816: - cost: 152 - init_stock: 1820 - price: 152 - service_level: 0.96 - vlt: 3 - SKU817: - cost: 227 - init_stock: 100 - price: 227 - service_level: 0.96 - vlt: 2 - SKU818: - cost: 354 - init_stock: 860 - price: 354 - service_level: 0.96 - vlt: 2 - SKU819: - cost: 302 - init_stock: 540 - price: 302 - service_level: 0.96 - vlt: 1 - SKU82: - cost: 290 - init_stock: 560 - price: 290 - service_level: 0.96 - vlt: 1 - SKU820: - cost: 264 - init_stock: 1640 - price: 264 - service_level: 0.96 - vlt: 3 - SKU821: - cost: 99 - init_stock: 1380 - price: 99 - service_level: 0.96 - vlt: 1 - SKU822: - cost: 136 - init_stock: 1400 - price: 136 - service_level: 0.96 - vlt: 3 - SKU823: - cost: 75 - init_stock: 560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU824: - cost: 170 - init_stock: 1400 - price: 170 - service_level: 0.96 - vlt: 1 - SKU825: - cost: 214 - init_stock: 300 - price: 214 - service_level: 0.96 - vlt: 1 - SKU826: - cost: 386 - init_stock: 1100 - price: 386 - service_level: 0.96 - vlt: 2 - SKU827: - cost: 148 - init_stock: 1280 - price: 148 - service_level: 0.96 - vlt: 2 - SKU828: - cost: 400 - init_stock: 1380 - price: 400 - service_level: 0.96 - vlt: 1 - SKU829: - cost: 61 - init_stock: 1480 - price: 61 - service_level: 0.96 - vlt: 1 - SKU83: - cost: 296 - init_stock: 340 - price: 296 - service_level: 0.96 - vlt: 2 - SKU830: - cost: 167 - init_stock: 480 - price: 167 - service_level: 0.96 - vlt: 3 - SKU831: - cost: 262 - init_stock: 580 - price: 262 - service_level: 0.96 - vlt: 1 - SKU832: - cost: 33 - init_stock: 1640 - price: 33 - service_level: 0.96 - vlt: 1 - SKU833: - cost: 400 - init_stock: 1960 - price: 400 - service_level: 0.96 - vlt: 3 - SKU834: - cost: 422 - init_stock: 100 - price: 422 - service_level: 0.96 - vlt: 1 - SKU835: - cost: 440 - init_stock: 1040 - price: 440 - service_level: 0.96 - vlt: 2 - SKU836: - cost: 323 - init_stock: 1920 - price: 323 - service_level: 0.96 - vlt: 1 - SKU837: - cost: 373 - init_stock: 520 - price: 373 - service_level: 0.96 - vlt: 3 - SKU838: - cost: 456 - init_stock: 1540 - price: 456 - service_level: 0.96 - vlt: 1 - SKU839: - cost: 473 - init_stock: 1200 - price: 473 - service_level: 0.96 - vlt: 1 - SKU84: - cost: 318 - init_stock: 840 - price: 318 - service_level: 0.96 - vlt: 3 - SKU840: - cost: 266 - init_stock: 1000 - price: 266 - service_level: 0.96 - vlt: 1 - SKU841: - cost: 285 - init_stock: 1700 - price: 285 - service_level: 0.96 - vlt: 3 - SKU842: - cost: 41 - init_stock: 1680 - price: 41 - service_level: 0.96 - vlt: 1 - SKU843: - cost: 360 - init_stock: 1440 - price: 360 - service_level: 0.96 - vlt: 2 - SKU844: - cost: 51 - init_stock: 1580 - price: 51 - service_level: 0.96 - vlt: 3 - SKU845: - cost: 288 - init_stock: 440 - price: 288 - service_level: 0.96 - vlt: 3 - SKU846: - cost: 485 - init_stock: 780 - price: 485 - service_level: 0.96 - vlt: 1 - SKU847: - cost: 388 - init_stock: 1820 - price: 388 - service_level: 0.96 - vlt: 1 - SKU848: - cost: 306 - init_stock: 920 - price: 306 - service_level: 0.96 - vlt: 2 - SKU849: - cost: 320 - init_stock: 800 - price: 320 - service_level: 0.96 - vlt: 2 - SKU85: - cost: 442 - init_stock: 2000 - price: 442 - service_level: 0.96 - vlt: 3 - SKU850: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 2 - SKU851: - cost: 53 - init_stock: 1280 - price: 53 - service_level: 0.96 - vlt: 1 - SKU852: - cost: 306 - init_stock: 1080 - price: 306 - service_level: 0.96 - vlt: 2 - SKU853: - cost: 386 - init_stock: 1120 - price: 386 - service_level: 0.96 - vlt: 3 - SKU854: - cost: 212 - init_stock: 1300 - price: 212 - service_level: 0.96 - vlt: 3 - SKU855: - cost: 76 - init_stock: 1440 - price: 76 - service_level: 0.96 - vlt: 3 - SKU856: - cost: 380 - init_stock: 640 - price: 380 - service_level: 0.96 - vlt: 2 - SKU857: - cost: 462 - init_stock: 2000 - price: 462 - service_level: 0.96 - vlt: 1 - SKU858: - cost: 80 - init_stock: 480 - price: 80 - service_level: 0.96 - vlt: 3 - SKU859: - cost: 215 - init_stock: 560 - price: 215 - service_level: 0.96 - vlt: 3 - SKU86: - cost: 386 - init_stock: 1700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU860: - cost: 313 - init_stock: 300 - price: 313 - service_level: 0.96 - vlt: 2 - SKU861: - cost: 477 - init_stock: 260 - price: 477 - service_level: 0.96 - vlt: 2 - SKU862: - cost: 240 - init_stock: 320 - price: 240 - service_level: 0.96 - vlt: 2 - SKU863: - cost: 470 - init_stock: 1860 - price: 470 - service_level: 0.96 - vlt: 2 - SKU864: - cost: 203 - init_stock: 380 - price: 203 - service_level: 0.96 - vlt: 1 - SKU865: - cost: 144 - init_stock: 380 - price: 144 - service_level: 0.96 - vlt: 3 - SKU866: - cost: 172 - init_stock: 1760 - price: 172 - service_level: 0.96 - vlt: 1 - SKU867: - cost: 499 - init_stock: 2000 - price: 499 - service_level: 0.96 - vlt: 2 - SKU868: - cost: 41 - init_stock: 1000 - price: 41 - service_level: 0.96 - vlt: 2 - SKU869: - cost: 97 - init_stock: 1940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU87: - cost: 368 - init_stock: 1380 - price: 368 - service_level: 0.96 - vlt: 2 - SKU870: - cost: 281 - init_stock: 1340 - price: 281 - service_level: 0.96 - vlt: 1 - SKU871: - cost: 101 - init_stock: 1980 - price: 101 - service_level: 0.96 - vlt: 1 - SKU872: - cost: 133 - init_stock: 640 - price: 133 - service_level: 0.96 - vlt: 3 - SKU873: - cost: 423 - init_stock: 620 - price: 423 - service_level: 0.96 - vlt: 2 - SKU874: - cost: 469 - init_stock: 1200 - price: 469 - service_level: 0.96 - vlt: 1 - SKU875: - cost: 51 - init_stock: 460 - price: 51 - service_level: 0.96 - vlt: 2 - SKU876: - cost: 303 - init_stock: 1220 - price: 303 - service_level: 0.96 - vlt: 3 - SKU877: - cost: 40 - init_stock: 700 - price: 40 - service_level: 0.96 - vlt: 3 - SKU878: - cost: 27 - init_stock: 1360 - price: 27 - service_level: 0.96 - vlt: 3 - SKU879: - cost: 150 - init_stock: 2000 - price: 150 - service_level: 0.96 - vlt: 1 - SKU88: - cost: 471 - init_stock: 240 - price: 471 - service_level: 0.96 - vlt: 1 - SKU880: - cost: 433 - init_stock: 620 - price: 433 - service_level: 0.96 - vlt: 3 - SKU881: - cost: 431 - init_stock: 1400 - price: 431 - service_level: 0.96 - vlt: 3 - SKU882: - cost: 194 - init_stock: 320 - price: 194 - service_level: 0.96 - vlt: 1 - SKU883: - cost: 284 - init_stock: 760 - price: 284 - service_level: 0.96 - vlt: 1 - SKU884: - cost: 139 - init_stock: 780 - price: 139 - service_level: 0.96 - vlt: 2 - SKU885: - cost: 49 - init_stock: 540 - price: 49 - service_level: 0.96 - vlt: 3 - SKU886: - cost: 372 - init_stock: 1460 - price: 372 - service_level: 0.96 - vlt: 3 - SKU887: - cost: 266 - init_stock: 1740 - price: 266 - service_level: 0.96 - vlt: 1 - SKU888: - cost: 143 - init_stock: 180 - price: 143 - service_level: 0.96 - vlt: 2 - SKU889: - cost: 101 - init_stock: 420 - price: 101 - service_level: 0.96 - vlt: 2 - SKU89: - cost: 138 - init_stock: 1860 - price: 138 - service_level: 0.96 - vlt: 3 - SKU890: - cost: 161 - init_stock: 2000 - price: 161 - service_level: 0.96 - vlt: 3 - SKU891: - cost: 356 - init_stock: 440 - price: 356 - service_level: 0.96 - vlt: 3 - SKU892: - cost: 313 - init_stock: 1840 - price: 313 - service_level: 0.96 - vlt: 2 - SKU893: - cost: 229 - init_stock: 1980 - price: 229 - service_level: 0.96 - vlt: 1 - SKU894: - cost: 129 - init_stock: 1480 - price: 129 - service_level: 0.96 - vlt: 1 - SKU895: - cost: 230 - init_stock: 260 - price: 230 - service_level: 0.96 - vlt: 2 - SKU896: - cost: 289 - init_stock: 1600 - price: 289 - service_level: 0.96 - vlt: 3 - SKU897: - cost: 393 - init_stock: 1440 - price: 393 - service_level: 0.96 - vlt: 3 - SKU898: - cost: 477 - init_stock: 220 - price: 477 - service_level: 0.96 - vlt: 2 - SKU899: - cost: 233 - init_stock: 960 - price: 233 - service_level: 0.96 - vlt: 2 - SKU9: - cost: 166 - init_stock: 1920 - price: 166 - service_level: 0.96 - vlt: 2 - SKU90: - cost: 454 - init_stock: 1020 - price: 454 - service_level: 0.96 - vlt: 2 - SKU900: - cost: 158 - init_stock: 1100 - price: 158 - service_level: 0.96 - vlt: 1 - SKU901: - cost: 215 - init_stock: 580 - price: 215 - service_level: 0.96 - vlt: 3 - SKU902: - cost: 125 - init_stock: 1600 - price: 125 - service_level: 0.96 - vlt: 2 - SKU903: - cost: 357 - init_stock: 760 - price: 357 - service_level: 0.96 - vlt: 2 - SKU904: - cost: 496 - init_stock: 1020 - price: 496 - service_level: 0.96 - vlt: 1 - SKU905: - cost: 249 - init_stock: 620 - price: 249 - service_level: 0.96 - vlt: 3 - SKU906: - cost: 166 - init_stock: 1040 - price: 166 - service_level: 0.96 - vlt: 2 - SKU907: - cost: 22 - init_stock: 1660 - price: 22 - service_level: 0.96 - vlt: 3 - SKU908: - cost: 408 - init_stock: 1560 - price: 408 - service_level: 0.96 - vlt: 2 - SKU909: - cost: 482 - init_stock: 1920 - price: 482 - service_level: 0.96 - vlt: 1 - SKU91: - cost: 303 - init_stock: 320 - price: 303 - service_level: 0.96 - vlt: 2 - SKU910: - cost: 226 - init_stock: 660 - price: 226 - service_level: 0.96 - vlt: 2 - SKU911: - cost: 461 - init_stock: 1400 - price: 461 - service_level: 0.96 - vlt: 2 - SKU912: - cost: 236 - init_stock: 540 - price: 236 - service_level: 0.96 - vlt: 3 - SKU913: - cost: 322 - init_stock: 920 - price: 322 - service_level: 0.96 - vlt: 2 - SKU914: - cost: 272 - init_stock: 1240 - price: 272 - service_level: 0.96 - vlt: 3 - SKU915: - cost: 337 - init_stock: 1120 - price: 337 - service_level: 0.96 - vlt: 3 - SKU916: - cost: 337 - init_stock: 1780 - price: 337 - service_level: 0.96 - vlt: 3 - SKU917: - cost: 258 - init_stock: 600 - price: 258 - service_level: 0.96 - vlt: 3 - SKU918: - cost: 148 - init_stock: 1100 - price: 148 - service_level: 0.96 - vlt: 1 - SKU919: - cost: 393 - init_stock: 500 - price: 393 - service_level: 0.96 - vlt: 3 - SKU92: - cost: 262 - init_stock: 1260 - price: 262 - service_level: 0.96 - vlt: 2 - SKU920: - cost: 357 - init_stock: 1720 - price: 357 - service_level: 0.96 - vlt: 3 - SKU921: - cost: 227 - init_stock: 1320 - price: 227 - service_level: 0.96 - vlt: 2 - SKU922: - cost: 112 - init_stock: 1340 - price: 112 - service_level: 0.96 - vlt: 2 - SKU923: - cost: 496 - init_stock: 1160 - price: 496 - service_level: 0.96 - vlt: 2 - SKU924: - cost: 316 - init_stock: 1700 - price: 316 - service_level: 0.96 - vlt: 3 - SKU925: - cost: 360 - init_stock: 300 - price: 360 - service_level: 0.96 - vlt: 1 - SKU926: - cost: 360 - init_stock: 1340 - price: 360 - service_level: 0.96 - vlt: 2 - SKU927: - cost: 260 - init_stock: 420 - price: 260 - service_level: 0.96 - vlt: 3 - SKU928: - cost: 491 - init_stock: 1660 - price: 491 - service_level: 0.96 - vlt: 1 - SKU929: - cost: 359 - init_stock: 2000 - price: 359 - service_level: 0.96 - vlt: 3 - SKU93: - cost: 404 - init_stock: 1340 - price: 404 - service_level: 0.96 - vlt: 2 - SKU930: - cost: 198 - init_stock: 560 - price: 198 - service_level: 0.96 - vlt: 2 - SKU931: - cost: 71 - init_stock: 280 - price: 71 - service_level: 0.96 - vlt: 3 - SKU932: - cost: 163 - init_stock: 1320 - price: 163 - service_level: 0.96 - vlt: 2 - SKU933: - cost: 113 - init_stock: 1560 - price: 113 - service_level: 0.96 - vlt: 1 - SKU934: - cost: 219 - init_stock: 1340 - price: 219 - service_level: 0.96 - vlt: 3 - SKU935: - cost: 364 - init_stock: 1880 - price: 364 - service_level: 0.96 - vlt: 1 - SKU936: - cost: 24 - init_stock: 100 - price: 24 - service_level: 0.96 - vlt: 3 - SKU937: - cost: 135 - init_stock: 340 - price: 135 - service_level: 0.96 - vlt: 1 - SKU938: - cost: 432 - init_stock: 420 - price: 432 - service_level: 0.96 - vlt: 2 - SKU939: - cost: 173 - init_stock: 1180 - price: 173 - service_level: 0.96 - vlt: 3 - SKU94: - cost: 184 - init_stock: 940 - price: 184 - service_level: 0.96 - vlt: 3 - SKU940: - cost: 14 - init_stock: 1860 - price: 14 - service_level: 0.96 - vlt: 1 - SKU941: - cost: 80 - init_stock: 1140 - price: 80 - service_level: 0.96 - vlt: 3 - SKU942: - cost: 202 - init_stock: 260 - price: 202 - service_level: 0.96 - vlt: 3 - SKU943: - cost: 138 - init_stock: 980 - price: 138 - service_level: 0.96 - vlt: 1 - SKU944: - cost: 196 - init_stock: 880 - price: 196 - service_level: 0.96 - vlt: 1 - SKU945: - cost: 141 - init_stock: 340 - price: 141 - service_level: 0.96 - vlt: 2 - SKU946: - cost: 325 - init_stock: 980 - price: 325 - service_level: 0.96 - vlt: 1 - SKU947: - cost: 338 - init_stock: 1820 - price: 338 - service_level: 0.96 - vlt: 3 - SKU948: - cost: 425 - init_stock: 560 - price: 425 - service_level: 0.96 - vlt: 3 - SKU949: - cost: 309 - init_stock: 100 - price: 309 - service_level: 0.96 - vlt: 2 - SKU95: - cost: 136 - init_stock: 420 - price: 136 - service_level: 0.96 - vlt: 3 - SKU950: - cost: 102 - init_stock: 1080 - price: 102 - service_level: 0.96 - vlt: 2 - SKU951: - cost: 75 - init_stock: 360 - price: 75 - service_level: 0.96 - vlt: 2 - SKU952: - cost: 156 - init_stock: 220 - price: 156 - service_level: 0.96 - vlt: 3 - SKU953: - cost: 138 - init_stock: 700 - price: 138 - service_level: 0.96 - vlt: 3 - SKU954: - cost: 296 - init_stock: 1720 - price: 296 - service_level: 0.96 - vlt: 1 - SKU955: - cost: 55 - init_stock: 1260 - price: 55 - service_level: 0.96 - vlt: 1 - SKU956: - cost: 282 - init_stock: 1700 - price: 282 - service_level: 0.96 - vlt: 1 - SKU957: - cost: 305 - init_stock: 1020 - price: 305 - service_level: 0.96 - vlt: 2 - SKU958: - cost: 369 - init_stock: 1320 - price: 369 - service_level: 0.96 - vlt: 2 - SKU959: - cost: 81 - init_stock: 1640 - price: 81 - service_level: 0.96 - vlt: 1 - SKU96: - cost: 176 - init_stock: 880 - price: 176 - service_level: 0.96 - vlt: 3 - SKU960: - cost: 147 - init_stock: 120 - price: 147 - service_level: 0.96 - vlt: 3 - SKU961: - cost: 264 - init_stock: 880 - price: 264 - service_level: 0.96 - vlt: 1 - SKU962: - cost: 354 - init_stock: 880 - price: 354 - service_level: 0.96 - vlt: 1 - SKU963: - cost: 349 - init_stock: 420 - price: 349 - service_level: 0.96 - vlt: 1 - SKU964: - cost: 244 - init_stock: 1460 - price: 244 - service_level: 0.96 - vlt: 1 - SKU965: - cost: 124 - init_stock: 1020 - price: 124 - service_level: 0.96 - vlt: 1 - SKU966: - cost: 302 - init_stock: 880 - price: 302 - service_level: 0.96 - vlt: 3 - SKU967: - cost: 67 - init_stock: 900 - price: 67 - service_level: 0.96 - vlt: 3 - SKU968: - cost: 281 - init_stock: 980 - price: 281 - service_level: 0.96 - vlt: 2 - SKU969: - cost: 249 - init_stock: 120 - price: 249 - service_level: 0.96 - vlt: 1 - SKU97: - cost: 28 - init_stock: 600 - price: 28 - service_level: 0.96 - vlt: 3 - SKU970: - cost: 244 - init_stock: 100 - price: 244 - service_level: 0.96 - vlt: 2 - SKU971: - cost: 368 - init_stock: 1460 - price: 368 - service_level: 0.96 - vlt: 1 - SKU972: - cost: 209 - init_stock: 1380 - price: 209 - service_level: 0.96 - vlt: 1 - SKU973: - cost: 271 - init_stock: 220 - price: 271 - service_level: 0.96 - vlt: 2 - SKU974: - cost: 170 - init_stock: 640 - price: 170 - service_level: 0.96 - vlt: 1 - SKU975: - cost: 198 - init_stock: 440 - price: 198 - service_level: 0.96 - vlt: 3 - SKU976: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 2 - SKU977: - cost: 234 - init_stock: 1080 - price: 234 - service_level: 0.96 - vlt: 3 - SKU978: - cost: 470 - init_stock: 1280 - price: 470 - service_level: 0.96 - vlt: 3 - SKU979: - cost: 443 - init_stock: 1920 - price: 443 - service_level: 0.96 - vlt: 2 - SKU98: - cost: 443 - init_stock: 700 - price: 443 - service_level: 0.96 - vlt: 1 - SKU980: - cost: 39 - init_stock: 1360 - price: 39 - service_level: 0.96 - vlt: 3 - SKU981: - cost: 482 - init_stock: 1440 - price: 482 - service_level: 0.96 - vlt: 2 - SKU982: - cost: 213 - init_stock: 1040 - price: 213 - service_level: 0.96 - vlt: 3 - SKU983: - cost: 449 - init_stock: 1840 - price: 449 - service_level: 0.96 - vlt: 1 - SKU984: - cost: 232 - init_stock: 1820 - price: 232 - service_level: 0.96 - vlt: 3 - SKU985: - cost: 290 - init_stock: 1020 - price: 290 - service_level: 0.96 - vlt: 1 - SKU986: - cost: 275 - init_stock: 340 - price: 275 - service_level: 0.96 - vlt: 3 - SKU987: - cost: 434 - init_stock: 680 - price: 434 - service_level: 0.96 - vlt: 1 - SKU988: - cost: 102 - init_stock: 460 - price: 102 - service_level: 0.96 - vlt: 1 - SKU989: - cost: 484 - init_stock: 1860 - price: 484 - service_level: 0.96 - vlt: 1 - SKU99: - cost: 361 - init_stock: 900 - price: 361 - service_level: 0.96 - vlt: 3 - SKU990: - cost: 108 - init_stock: 1140 - price: 108 - service_level: 0.96 - vlt: 3 - SKU991: - cost: 409 - init_stock: 380 - price: 409 - service_level: 0.96 - vlt: 3 - SKU992: - cost: 434 - init_stock: 1860 - price: 434 - service_level: 0.96 - vlt: 3 - SKU993: - cost: 145 - init_stock: 1720 - price: 145 - service_level: 0.96 - vlt: 2 - SKU994: - cost: 470 - init_stock: 1000 - price: 470 - service_level: 0.96 - vlt: 3 - SKU995: - cost: 241 - init_stock: 1520 - price: 241 - service_level: 0.96 - vlt: 2 - SKU996: - cost: 260 - init_stock: 1460 - price: 260 - service_level: 0.96 - vlt: 3 - SKU997: - cost: 400 - init_stock: 680 - price: 400 - service_level: 0.96 - vlt: 1 - SKU998: - cost: 447 - init_stock: 1100 - price: 447 - service_level: 0.96 - vlt: 2 - SKU999: - cost: 79 - init_stock: 460 - price: 79 - service_level: 0.96 - vlt: 2 - - children: - storage: - config: - capacity: 1064900 - unit_storage_cost: 1 - config: - order_cost: 500 - definition_ref: RetailerFacility - name: STORE0 - skus: - SKU0: - constraint: null - cost: 322 - init_stock: 126 - max_stock: 1000 - price: 631 - sale_gamma: 63 - service_level: 0.95 - SKU1: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 284 - init_stock: 176 - max_stock: 1000 - price: 448 - sale_gamma: 22 - service_level: 0.95 - SKU10: - constraint: null - cost: 68 - init_stock: 150 - max_stock: 1000 - price: 116 - sale_gamma: 25 - service_level: 0.95 - SKU100: - constraint: G(low_profit -> low_stock_constraint) - cost: 16 - init_stock: 390 - max_stock: 1000 - price: 23 - sale_gamma: 65 - service_level: 0.95 - SKU101: - constraint: null - cost: 84 - init_stock: 497 - max_stock: 1000 - price: 149 - sale_gamma: 71 - service_level: 0.95 - SKU102: - constraint: null - cost: 328 - init_stock: 558 - max_stock: 1000 - price: 505 - sale_gamma: 93 - service_level: 0.95 - SKU103: - constraint: null - cost: 334 - init_stock: 285 - max_stock: 1000 - price: 601 - sale_gamma: 95 - service_level: 0.95 - SKU104: - constraint: null - cost: 49 - init_stock: 325 - max_stock: 1000 - price: 57 - sale_gamma: 65 - service_level: 0.95 - SKU105: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 110 - init_stock: 285 - max_stock: 1000 - price: 171 - sale_gamma: 57 - service_level: 0.95 - SKU106: - constraint: G(low_profit -> low_stock_constraint) - cost: 251 - init_stock: 584 - max_stock: 1000 - price: 454 - sale_gamma: 73 - service_level: 0.95 - SKU107: - constraint: G(stock_constraint) - cost: 423 - init_stock: 348 - max_stock: 1000 - price: 706 - sale_gamma: 87 - service_level: 0.95 - SKU108: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 458 - init_stock: 368 - max_stock: 1000 - price: 801 - sale_gamma: 92 - service_level: 0.95 - SKU109: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 88 - init_stock: 328 - max_stock: 1000 - price: 98 - sale_gamma: 82 - service_level: 0.95 - SKU11: - constraint: null - cost: 400 - init_stock: 306 - max_stock: 1000 - price: 680 - sale_gamma: 51 - service_level: 0.95 - SKU110: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 66 - init_stock: 112 - max_stock: 1000 - price: 100 - sale_gamma: 14 - service_level: 0.95 - SKU111: - constraint: G(stock_constraint) - cost: 260 - init_stock: 183 - max_stock: 1000 - price: 364 - sale_gamma: 61 - service_level: 0.95 - SKU112: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 61 - init_stock: 475 - max_stock: 1000 - price: 119 - sale_gamma: 95 - service_level: 0.95 - SKU113: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 155 - max_stock: 1000 - price: 678 - sale_gamma: 31 - service_level: 0.95 - SKU114: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 389 - init_stock: 162 - max_stock: 1000 - price: 564 - sale_gamma: 27 - service_level: 0.95 - SKU115: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 258 - max_stock: 1000 - price: 557 - sale_gamma: 86 - service_level: 0.95 - SKU116: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 288 - max_stock: 1000 - price: 768 - sale_gamma: 72 - service_level: 0.95 - SKU117: - constraint: null - cost: 320 - init_stock: 644 - max_stock: 1000 - price: 374 - sale_gamma: 92 - service_level: 0.95 - SKU118: - constraint: null - cost: 183 - init_stock: 66 - max_stock: 1000 - price: 208 - sale_gamma: 33 - service_level: 0.95 - SKU119: - constraint: null - cost: 209 - init_stock: 256 - max_stock: 1000 - price: 317 - sale_gamma: 32 - service_level: 0.95 - SKU12: - constraint: null - cost: 112 - init_stock: 336 - max_stock: 1000 - price: 210 - sale_gamma: 84 - service_level: 0.95 - SKU120: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 121 - init_stock: 392 - max_stock: 1000 - price: 151 - sale_gamma: 98 - service_level: 0.95 - SKU121: - constraint: null - cost: 40 - init_stock: 510 - max_stock: 1000 - price: 54 - sale_gamma: 85 - service_level: 0.95 - SKU122: - constraint: G(stock_constraint) - cost: 437 - init_stock: 35 - max_stock: 1000 - price: 589 - sale_gamma: 7 - service_level: 0.95 - SKU123: - constraint: G(stock_constraint) - cost: 233 - init_stock: 114 - max_stock: 1000 - price: 326 - sale_gamma: 19 - service_level: 0.95 - SKU124: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 108 - max_stock: 1000 - price: 298 - sale_gamma: 36 - service_level: 0.95 - SKU125: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 368 - max_stock: 1000 - price: 32 - sale_gamma: 92 - service_level: 0.95 - SKU126: - constraint: null - cost: 36 - init_stock: 156 - max_stock: 1000 - price: 40 - sale_gamma: 39 - service_level: 0.95 - SKU127: - constraint: G(stock_constraint) - cost: 217 - init_stock: 155 - max_stock: 1000 - price: 366 - sale_gamma: 31 - service_level: 0.95 - SKU128: - constraint: null - cost: 165 - init_stock: 95 - max_stock: 1000 - price: 283 - sale_gamma: 19 - service_level: 0.95 - SKU129: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 203 - sale_gamma: 50 - service_level: 0.95 - SKU13: - constraint: null - cost: 317 - init_stock: 456 - max_stock: 1000 - price: 573 - sale_gamma: 57 - service_level: 0.95 - SKU130: - constraint: null - cost: 348 - init_stock: 784 - max_stock: 1000 - price: 396 - sale_gamma: 98 - service_level: 0.95 - SKU131: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 64 - init_stock: 270 - max_stock: 1000 - price: 110 - sale_gamma: 45 - service_level: 0.95 - SKU132: - constraint: null - cost: 427 - init_stock: 105 - max_stock: 1000 - price: 678 - sale_gamma: 21 - service_level: 0.95 - SKU133: - constraint: null - cost: 224 - init_stock: 145 - max_stock: 1000 - price: 448 - sale_gamma: 29 - service_level: 0.95 - SKU134: - constraint: G(stock_constraint) - cost: 336 - init_stock: 539 - max_stock: 1000 - price: 470 - sale_gamma: 77 - service_level: 0.95 - SKU135: - constraint: null - cost: 153 - init_stock: 500 - max_stock: 1000 - price: 243 - sale_gamma: 100 - service_level: 0.95 - SKU136: - constraint: G(low_profit -> low_stock_constraint) - cost: 199 - init_stock: 497 - max_stock: 1000 - price: 370 - sale_gamma: 71 - service_level: 0.95 - SKU137: - constraint: G(stock_constraint) - cost: 93 - init_stock: 444 - max_stock: 1000 - price: 165 - sale_gamma: 74 - service_level: 0.95 - SKU138: - constraint: G(stock_constraint) - cost: 228 - init_stock: 180 - max_stock: 1000 - price: 253 - sale_gamma: 36 - service_level: 0.95 - SKU139: - constraint: G(stock_constraint) - cost: 207 - init_stock: 144 - max_stock: 1000 - price: 368 - sale_gamma: 24 - service_level: 0.95 - SKU14: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 268 - init_stock: 372 - max_stock: 1000 - price: 305 - sale_gamma: 62 - service_level: 0.95 - SKU140: - constraint: null - cost: 261 - init_stock: 238 - max_stock: 1000 - price: 357 - sale_gamma: 34 - service_level: 0.95 - SKU141: - constraint: null - cost: 190 - init_stock: 164 - max_stock: 1000 - price: 370 - sale_gamma: 41 - service_level: 0.95 - SKU142: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 320 - init_stock: 152 - max_stock: 1000 - price: 428 - sale_gamma: 38 - service_level: 0.95 - SKU143: - constraint: G(stock_constraint) - cost: 318 - init_stock: 182 - max_stock: 1000 - price: 566 - sale_gamma: 26 - service_level: 0.95 - SKU144: - constraint: null - cost: 400 - init_stock: 24 - max_stock: 1000 - price: 716 - sale_gamma: 12 - service_level: 0.95 - SKU145: - constraint: null - cost: 399 - init_stock: 328 - max_stock: 1000 - price: 774 - sale_gamma: 82 - service_level: 0.95 - SKU146: - constraint: null - cost: 177 - init_stock: 192 - max_stock: 1000 - price: 194 - sale_gamma: 48 - service_level: 0.95 - SKU147: - constraint: G(low_profit -> low_stock_constraint) - cost: 472 - init_stock: 392 - max_stock: 1000 - price: 840 - sale_gamma: 56 - service_level: 0.95 - SKU148: - constraint: G(stock_constraint) - cost: 313 - init_stock: 308 - max_stock: 1000 - price: 566 - sale_gamma: 77 - service_level: 0.95 - SKU149: - constraint: G(low_profit -> low_stock_constraint) - cost: 357 - init_stock: 539 - max_stock: 1000 - price: 549 - sale_gamma: 77 - service_level: 0.95 - SKU15: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 25 - max_stock: 1000 - price: 58 - sale_gamma: 5 - service_level: 0.95 - SKU150: - constraint: null - cost: 106 - init_stock: 210 - max_stock: 1000 - price: 159 - sale_gamma: 70 - service_level: 0.95 - SKU151: - constraint: G(low_profit -> low_stock_constraint) - cost: 223 - init_stock: 146 - max_stock: 1000 - price: 370 - sale_gamma: 73 - service_level: 0.95 - SKU152: - constraint: null - cost: 10 - init_stock: 408 - max_stock: 1000 - price: 14 - sale_gamma: 51 - service_level: 0.95 - SKU153: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 441 - init_stock: 372 - max_stock: 1000 - price: 538 - sale_gamma: 62 - service_level: 0.95 - SKU154: - constraint: null - cost: 77 - init_stock: 680 - max_stock: 1000 - price: 135 - sale_gamma: 85 - service_level: 0.95 - SKU155: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 159 - max_stock: 1000 - price: 641 - sale_gamma: 53 - service_level: 0.95 - SKU156: - constraint: null - cost: 10 - init_stock: 36 - max_stock: 1000 - price: 16 - sale_gamma: 12 - service_level: 0.95 - SKU157: - constraint: null - cost: 410 - init_stock: 525 - max_stock: 1000 - price: 594 - sale_gamma: 75 - service_level: 0.95 - SKU158: - constraint: null - cost: 145 - init_stock: 486 - max_stock: 1000 - price: 275 - sale_gamma: 81 - service_level: 0.95 - SKU159: - constraint: G(stock_constraint) - cost: 193 - init_stock: 100 - max_stock: 1000 - price: 368 - sale_gamma: 25 - service_level: 0.95 - SKU16: - constraint: null - cost: 175 - init_stock: 464 - max_stock: 1000 - price: 344 - sale_gamma: 58 - service_level: 0.95 - SKU160: - constraint: G(low_profit -> low_stock_constraint) - cost: 459 - init_stock: 510 - max_stock: 1000 - price: 876 - sale_gamma: 85 - service_level: 0.95 - SKU161: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 239 - init_stock: 230 - max_stock: 1000 - price: 478 - sale_gamma: 46 - service_level: 0.95 - SKU162: - constraint: null - cost: 158 - init_stock: 30 - max_stock: 1000 - price: 290 - sale_gamma: 5 - service_level: 0.95 - SKU163: - constraint: G(stock_constraint) - cost: 486 - init_stock: 273 - max_stock: 1000 - price: 704 - sale_gamma: 39 - service_level: 0.95 - SKU164: - constraint: null - cost: 496 - init_stock: 500 - max_stock: 1000 - price: 615 - sale_gamma: 100 - service_level: 0.95 - SKU165: - constraint: G(stock_constraint) - cost: 274 - init_stock: 231 - max_stock: 1000 - price: 548 - sale_gamma: 33 - service_level: 0.95 - SKU166: - constraint: null - cost: 79 - init_stock: 178 - max_stock: 1000 - price: 137 - sale_gamma: 89 - service_level: 0.95 - SKU167: - constraint: G(stock_constraint) - cost: 443 - init_stock: 52 - max_stock: 1000 - price: 854 - sale_gamma: 13 - service_level: 0.95 - SKU168: - constraint: null - cost: 357 - init_stock: 522 - max_stock: 1000 - price: 546 - sale_gamma: 87 - service_level: 0.95 - SKU169: - constraint: null - cost: 369 - init_stock: 686 - max_stock: 1000 - price: 505 - sale_gamma: 98 - service_level: 0.95 - SKU17: - constraint: null - cost: 346 - init_stock: 18 - max_stock: 1000 - price: 553 - sale_gamma: 9 - service_level: 0.95 - SKU170: - constraint: null - cost: 68 - init_stock: 275 - max_stock: 1000 - price: 113 - sale_gamma: 55 - service_level: 0.95 - SKU171: - constraint: G(low_profit -> low_stock_constraint) - cost: 398 - init_stock: 380 - max_stock: 1000 - price: 704 - sale_gamma: 76 - service_level: 0.95 - SKU172: - constraint: null - cost: 200 - init_stock: 426 - max_stock: 1000 - price: 222 - sale_gamma: 71 - service_level: 0.95 - SKU173: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 480 - max_stock: 1000 - price: 264 - sale_gamma: 96 - service_level: 0.95 - SKU174: - constraint: null - cost: 291 - init_stock: 380 - max_stock: 1000 - price: 582 - sale_gamma: 76 - service_level: 0.95 - SKU175: - constraint: null - cost: 84 - init_stock: 225 - max_stock: 1000 - price: 140 - sale_gamma: 75 - service_level: 0.95 - SKU176: - constraint: G(stock_constraint) - cost: 407 - init_stock: 330 - max_stock: 1000 - price: 610 - sale_gamma: 66 - service_level: 0.95 - SKU177: - constraint: null - cost: 257 - init_stock: 155 - max_stock: 1000 - price: 346 - sale_gamma: 31 - service_level: 0.95 - SKU178: - constraint: null - cost: 423 - init_stock: 20 - max_stock: 1000 - price: 499 - sale_gamma: 5 - service_level: 0.95 - SKU179: - constraint: null - cost: 497 - init_stock: 415 - max_stock: 1000 - price: 690 - sale_gamma: 83 - service_level: 0.95 - SKU18: - constraint: null - cost: 258 - init_stock: 405 - max_stock: 1000 - price: 387 - sale_gamma: 81 - service_level: 0.95 - SKU180: - constraint: null - cost: 217 - init_stock: 165 - max_stock: 1000 - price: 297 - sale_gamma: 55 - service_level: 0.95 - SKU181: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 214 - sale_gamma: 60 - service_level: 0.95 - SKU182: - constraint: null - cost: 437 - init_stock: 693 - max_stock: 1000 - price: 764 - sale_gamma: 99 - service_level: 0.95 - SKU183: - constraint: null - cost: 145 - init_stock: 56 - max_stock: 1000 - price: 178 - sale_gamma: 8 - service_level: 0.95 - SKU184: - constraint: null - cost: 73 - init_stock: 72 - max_stock: 1000 - price: 100 - sale_gamma: 24 - service_level: 0.95 - SKU185: - constraint: null - cost: 10 - init_stock: 360 - max_stock: 1000 - price: 14 - sale_gamma: 90 - service_level: 0.95 - SKU186: - constraint: null - cost: 359 - init_stock: 66 - max_stock: 1000 - price: 649 - sale_gamma: 22 - service_level: 0.95 - SKU187: - constraint: G(low_profit -> low_stock_constraint) - cost: 177 - init_stock: 120 - max_stock: 1000 - price: 249 - sale_gamma: 30 - service_level: 0.95 - SKU188: - constraint: null - cost: 391 - init_stock: 348 - max_stock: 1000 - price: 586 - sale_gamma: 87 - service_level: 0.95 - SKU189: - constraint: G(low_profit -> low_stock_constraint) - cost: 358 - init_stock: 210 - max_stock: 1000 - price: 400 - sale_gamma: 35 - service_level: 0.95 - SKU19: - constraint: G(stock_constraint) - cost: 477 - init_stock: 364 - max_stock: 1000 - price: 791 - sale_gamma: 91 - service_level: 0.95 - SKU190: - constraint: null - cost: 113 - init_stock: 102 - max_stock: 1000 - price: 153 - sale_gamma: 17 - service_level: 0.95 - SKU191: - constraint: G(stock_constraint) - cost: 473 - init_stock: 324 - max_stock: 1000 - price: 638 - sale_gamma: 54 - service_level: 0.95 - SKU192: - constraint: null - cost: 415 - init_stock: 244 - max_stock: 1000 - price: 539 - sale_gamma: 61 - service_level: 0.95 - SKU193: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 207 - init_stock: 120 - max_stock: 1000 - price: 353 - sale_gamma: 30 - service_level: 0.95 - SKU194: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 30 - max_stock: 1000 - price: 803 - sale_gamma: 5 - service_level: 0.95 - SKU195: - constraint: G(low_profit -> low_stock_constraint) - cost: 218 - init_stock: 93 - max_stock: 1000 - price: 248 - sale_gamma: 31 - service_level: 0.95 - SKU196: - constraint: null - cost: 49 - init_stock: 544 - max_stock: 1000 - price: 62 - sale_gamma: 68 - service_level: 0.95 - SKU197: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 285 - max_stock: 1000 - price: 509 - sale_gamma: 57 - service_level: 0.95 - SKU198: - constraint: G(low_profit -> low_stock_constraint) - cost: 169 - init_stock: 378 - max_stock: 1000 - price: 307 - sale_gamma: 54 - service_level: 0.95 - SKU199: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 138 - max_stock: 1000 - price: 794 - sale_gamma: 23 - service_level: 0.95 - SKU2: - constraint: null - cost: 331 - init_stock: 350 - max_stock: 1000 - price: 499 - sale_gamma: 70 - service_level: 0.95 - SKU20: - constraint: G(stock_constraint) - cost: 335 - init_stock: 200 - max_stock: 1000 - price: 649 - sale_gamma: 25 - service_level: 0.95 - SKU200: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 65 - init_stock: 150 - max_stock: 1000 - price: 85 - sale_gamma: 25 - service_level: 0.95 - SKU201: - constraint: G(stock_constraint) - cost: 104 - init_stock: 354 - max_stock: 1000 - price: 161 - sale_gamma: 59 - service_level: 0.95 - SKU202: - constraint: null - cost: 142 - init_stock: 365 - max_stock: 1000 - price: 222 - sale_gamma: 73 - service_level: 0.95 - SKU203: - constraint: null - cost: 440 - init_stock: 328 - max_stock: 1000 - price: 677 - sale_gamma: 82 - service_level: 0.95 - SKU204: - constraint: null - cost: 489 - init_stock: 188 - max_stock: 1000 - price: 831 - sale_gamma: 47 - service_level: 0.95 - SKU205: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 130 - init_stock: 500 - max_stock: 1000 - price: 175 - sale_gamma: 100 - service_level: 0.95 - SKU206: - constraint: null - cost: 335 - init_stock: 55 - max_stock: 1000 - price: 552 - sale_gamma: 11 - service_level: 0.95 - SKU207: - constraint: G(low_profit -> low_stock_constraint) - cost: 140 - init_stock: 240 - max_stock: 1000 - price: 170 - sale_gamma: 80 - service_level: 0.95 - SKU208: - constraint: null - cost: 491 - init_stock: 308 - max_stock: 1000 - price: 927 - sale_gamma: 77 - service_level: 0.95 - SKU209: - constraint: G(stock_constraint) - cost: 179 - init_stock: 120 - max_stock: 1000 - price: 322 - sale_gamma: 20 - service_level: 0.95 - SKU21: - constraint: null - cost: 123 - init_stock: 400 - max_stock: 1000 - price: 225 - sale_gamma: 100 - service_level: 0.95 - SKU210: - constraint: null - cost: 404 - init_stock: 345 - max_stock: 1000 - price: 468 - sale_gamma: 69 - service_level: 0.95 - SKU211: - constraint: null - cost: 174 - init_stock: 364 - max_stock: 1000 - price: 226 - sale_gamma: 91 - service_level: 0.95 - SKU212: - constraint: null - cost: 405 - init_stock: 632 - max_stock: 1000 - price: 534 - sale_gamma: 79 - service_level: 0.95 - SKU213: - constraint: null - cost: 121 - init_stock: 192 - max_stock: 1000 - price: 229 - sale_gamma: 64 - service_level: 0.95 - SKU214: - constraint: G(stock_constraint) - cost: 101 - init_stock: 60 - max_stock: 1000 - price: 144 - sale_gamma: 10 - service_level: 0.95 - SKU215: - constraint: null - cost: 419 - init_stock: 94 - max_stock: 1000 - price: 469 - sale_gamma: 47 - service_level: 0.95 - SKU216: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 330 - init_stock: 92 - max_stock: 1000 - price: 432 - sale_gamma: 23 - service_level: 0.95 - SKU217: - constraint: null - cost: 284 - init_stock: 455 - max_stock: 1000 - price: 312 - sale_gamma: 65 - service_level: 0.95 - SKU218: - constraint: G(low_profit -> low_stock_constraint) - cost: 205 - init_stock: 354 - max_stock: 1000 - price: 397 - sale_gamma: 59 - service_level: 0.95 - SKU219: - constraint: G(stock_constraint) - cost: 92 - init_stock: 368 - max_stock: 1000 - price: 135 - sale_gamma: 46 - service_level: 0.95 - SKU22: - constraint: null - cost: 493 - init_stock: 264 - max_stock: 1000 - price: 986 - sale_gamma: 66 - service_level: 0.95 - SKU220: - constraint: null - cost: 387 - init_stock: 435 - max_stock: 1000 - price: 572 - sale_gamma: 87 - service_level: 0.95 - SKU221: - constraint: null - cost: 39 - init_stock: 468 - max_stock: 1000 - price: 48 - sale_gamma: 78 - service_level: 0.95 - SKU222: - constraint: G(low_profit -> low_stock_constraint) - cost: 115 - init_stock: 180 - max_stock: 1000 - price: 210 - sale_gamma: 36 - service_level: 0.95 - SKU223: - constraint: G(low_profit -> low_stock_constraint) - cost: 196 - init_stock: 36 - max_stock: 1000 - price: 286 - sale_gamma: 12 - service_level: 0.95 - SKU224: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 387 - init_stock: 25 - max_stock: 1000 - price: 661 - sale_gamma: 5 - service_level: 0.95 - SKU225: - constraint: null - cost: 164 - init_stock: 116 - max_stock: 1000 - price: 216 - sale_gamma: 29 - service_level: 0.95 - SKU226: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 206 - init_stock: 65 - max_stock: 1000 - price: 350 - sale_gamma: 13 - service_level: 0.95 - SKU227: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 144 - max_stock: 1000 - price: 895 - sale_gamma: 24 - service_level: 0.95 - SKU228: - constraint: null - cost: 14 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 90 - service_level: 0.95 - SKU229: - constraint: null - cost: 472 - init_stock: 176 - max_stock: 1000 - price: 708 - sale_gamma: 44 - service_level: 0.95 - SKU23: - constraint: null - cost: 387 - init_stock: 210 - max_stock: 1000 - price: 700 - sale_gamma: 42 - service_level: 0.95 - SKU230: - constraint: null - cost: 241 - init_stock: 138 - max_stock: 1000 - price: 436 - sale_gamma: 23 - service_level: 0.95 - SKU231: - constraint: G(stock_constraint) - cost: 48 - init_stock: 486 - max_stock: 1000 - price: 96 - sale_gamma: 81 - service_level: 0.95 - SKU232: - constraint: G(low_profit -> low_stock_constraint) - cost: 224 - init_stock: 480 - max_stock: 1000 - price: 338 - sale_gamma: 96 - service_level: 0.95 - SKU233: - constraint: G(low_profit -> low_stock_constraint) - cost: 360 - init_stock: 300 - max_stock: 1000 - price: 428 - sale_gamma: 75 - service_level: 0.95 - SKU234: - constraint: null - cost: 287 - init_stock: 25 - max_stock: 1000 - price: 387 - sale_gamma: 5 - service_level: 0.95 - SKU235: - constraint: G(low_profit -> low_stock_constraint) - cost: 24 - init_stock: 228 - max_stock: 1000 - price: 32 - sale_gamma: 57 - service_level: 0.95 - SKU236: - constraint: G(low_profit -> low_stock_constraint) - cost: 155 - init_stock: 165 - max_stock: 1000 - price: 289 - sale_gamma: 55 - service_level: 0.95 - SKU237: - constraint: null - cost: 433 - init_stock: 270 - max_stock: 1000 - price: 779 - sale_gamma: 45 - service_level: 0.95 - SKU238: - constraint: G(stock_constraint) - cost: 64 - init_stock: 264 - max_stock: 1000 - price: 112 - sale_gamma: 66 - service_level: 0.95 - SKU239: - constraint: null - cost: 103 - init_stock: 490 - max_stock: 1000 - price: 139 - sale_gamma: 98 - service_level: 0.95 - SKU24: - constraint: null - cost: 97 - init_stock: 329 - max_stock: 1000 - price: 114 - sale_gamma: 47 - service_level: 0.95 - SKU240: - constraint: null - cost: 373 - init_stock: 188 - max_stock: 1000 - price: 738 - sale_gamma: 47 - service_level: 0.95 - SKU241: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 213 - max_stock: 1000 - price: 860 - sale_gamma: 71 - service_level: 0.95 - SKU242: - constraint: null - cost: 17 - init_stock: 88 - max_stock: 1000 - price: 29 - sale_gamma: 44 - service_level: 0.95 - SKU243: - constraint: null - cost: 352 - init_stock: 84 - max_stock: 1000 - price: 394 - sale_gamma: 14 - service_level: 0.95 - SKU244: - constraint: null - cost: 174 - init_stock: 410 - max_stock: 1000 - price: 226 - sale_gamma: 82 - service_level: 0.95 - SKU245: - constraint: G(low_profit -> low_stock_constraint) - cost: 404 - init_stock: 198 - max_stock: 1000 - price: 525 - sale_gamma: 66 - service_level: 0.95 - SKU246: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 300 - init_stock: 210 - max_stock: 1000 - price: 474 - sale_gamma: 30 - service_level: 0.95 - SKU247: - constraint: null - cost: 386 - init_stock: 210 - max_stock: 1000 - price: 497 - sale_gamma: 35 - service_level: 0.95 - SKU248: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 355 - init_stock: 174 - max_stock: 1000 - price: 539 - sale_gamma: 29 - service_level: 0.95 - SKU249: - constraint: null - cost: 356 - init_stock: 100 - max_stock: 1000 - price: 544 - sale_gamma: 25 - service_level: 0.95 - SKU25: - constraint: G(stock_constraint) - cost: 161 - init_stock: 35 - max_stock: 1000 - price: 249 - sale_gamma: 5 - service_level: 0.95 - SKU250: - constraint: null - cost: 127 - init_stock: 324 - max_stock: 1000 - price: 186 - sale_gamma: 54 - service_level: 0.95 - SKU251: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 344 - init_stock: 244 - max_stock: 1000 - price: 543 - sale_gamma: 61 - service_level: 0.95 - SKU252: - constraint: null - cost: 181 - init_stock: 415 - max_stock: 1000 - price: 334 - sale_gamma: 83 - service_level: 0.95 - SKU253: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 448 - init_stock: 32 - max_stock: 1000 - price: 864 - sale_gamma: 16 - service_level: 0.95 - SKU254: - constraint: null - cost: 484 - init_stock: 184 - max_stock: 1000 - price: 696 - sale_gamma: 46 - service_level: 0.95 - SKU255: - constraint: null - cost: 290 - init_stock: 335 - max_stock: 1000 - price: 568 - sale_gamma: 67 - service_level: 0.95 - SKU256: - constraint: null - cost: 91 - init_stock: 360 - max_stock: 1000 - price: 167 - sale_gamma: 72 - service_level: 0.95 - SKU257: - constraint: null - cost: 348 - init_stock: 171 - max_stock: 1000 - price: 497 - sale_gamma: 57 - service_level: 0.95 - SKU258: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 258 - max_stock: 1000 - price: 689 - sale_gamma: 43 - service_level: 0.95 - SKU259: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 207 - max_stock: 1000 - price: 559 - sale_gamma: 69 - service_level: 0.95 - SKU26: - constraint: null - cost: 229 - init_stock: 126 - max_stock: 1000 - price: 357 - sale_gamma: 63 - service_level: 0.95 - SKU260: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 364 - max_stock: 1000 - price: 866 - sale_gamma: 52 - service_level: 0.95 - SKU261: - constraint: null - cost: 368 - init_stock: 66 - max_stock: 1000 - price: 688 - sale_gamma: 22 - service_level: 0.95 - SKU262: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 332 - init_stock: 390 - max_stock: 1000 - price: 385 - sale_gamma: 78 - service_level: 0.95 - SKU263: - constraint: G(stock_constraint) - cost: 189 - init_stock: 444 - max_stock: 1000 - price: 372 - sale_gamma: 74 - service_level: 0.95 - SKU264: - constraint: null - cost: 361 - init_stock: 158 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU265: - constraint: null - cost: 286 - init_stock: 236 - max_stock: 1000 - price: 434 - sale_gamma: 59 - service_level: 0.95 - SKU266: - constraint: null - cost: 128 - init_stock: 282 - max_stock: 1000 - price: 172 - sale_gamma: 47 - service_level: 0.95 - SKU267: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 77 - init_stock: 320 - max_stock: 1000 - price: 130 - sale_gamma: 80 - service_level: 0.95 - SKU268: - constraint: null - cost: 221 - init_stock: 356 - max_stock: 1000 - price: 362 - sale_gamma: 89 - service_level: 0.95 - SKU269: - constraint: G(low_profit -> low_stock_constraint) - cost: 126 - init_stock: 132 - max_stock: 1000 - price: 175 - sale_gamma: 44 - service_level: 0.95 - SKU27: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 448 - max_stock: 1000 - price: 728 - sale_gamma: 56 - service_level: 0.95 - SKU270: - constraint: null - cost: 182 - init_stock: 370 - max_stock: 1000 - price: 263 - sale_gamma: 74 - service_level: 0.95 - SKU271: - constraint: G(stock_constraint) - cost: 230 - init_stock: 54 - max_stock: 1000 - price: 266 - sale_gamma: 18 - service_level: 0.95 - SKU272: - constraint: null - cost: 366 - init_stock: 51 - max_stock: 1000 - price: 625 - sale_gamma: 17 - service_level: 0.95 - SKU273: - constraint: null - cost: 421 - init_stock: 72 - max_stock: 1000 - price: 614 - sale_gamma: 18 - service_level: 0.95 - SKU274: - constraint: null - cost: 29 - init_stock: 308 - max_stock: 1000 - price: 42 - sale_gamma: 77 - service_level: 0.95 - SKU275: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 50 - init_stock: 144 - max_stock: 1000 - price: 76 - sale_gamma: 48 - service_level: 0.95 - SKU276: - constraint: G(stock_constraint) - cost: 163 - init_stock: 378 - max_stock: 1000 - price: 299 - sale_gamma: 54 - service_level: 0.95 - SKU277: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 82 - max_stock: 1000 - price: 507 - sale_gamma: 41 - service_level: 0.95 - SKU278: - constraint: null - cost: 105 - init_stock: 84 - max_stock: 1000 - price: 140 - sale_gamma: 12 - service_level: 0.95 - SKU279: - constraint: G(stock_constraint) - cost: 51 - init_stock: 273 - max_stock: 1000 - price: 61 - sale_gamma: 39 - service_level: 0.95 - SKU28: - constraint: G(stock_constraint) - cost: 208 - init_stock: 235 - max_stock: 1000 - price: 295 - sale_gamma: 47 - service_level: 0.95 - SKU280: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 198 - max_stock: 1000 - price: 436 - sale_gamma: 33 - service_level: 0.95 - SKU281: - constraint: null - cost: 395 - init_stock: 375 - max_stock: 1000 - price: 592 - sale_gamma: 75 - service_level: 0.95 - SKU282: - constraint: null - cost: 63 - init_stock: 184 - max_stock: 1000 - price: 112 - sale_gamma: 46 - service_level: 0.95 - SKU283: - constraint: null - cost: 392 - init_stock: 736 - max_stock: 1000 - price: 490 - sale_gamma: 92 - service_level: 0.95 - SKU284: - constraint: G(stock_constraint) - cost: 344 - init_stock: 268 - max_stock: 1000 - price: 643 - sale_gamma: 67 - service_level: 0.95 - SKU285: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 133 - init_stock: 546 - max_stock: 1000 - price: 148 - sale_gamma: 91 - service_level: 0.95 - SKU286: - constraint: null - cost: 337 - init_stock: 156 - max_stock: 1000 - price: 626 - sale_gamma: 39 - service_level: 0.95 - SKU287: - constraint: null - cost: 375 - init_stock: 224 - max_stock: 1000 - price: 660 - sale_gamma: 56 - service_level: 0.95 - SKU288: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 190 - max_stock: 1000 - price: 352 - sale_gamma: 38 - service_level: 0.95 - SKU289: - constraint: G(stock_constraint) - cost: 67 - init_stock: 62 - max_stock: 1000 - price: 91 - sale_gamma: 31 - service_level: 0.95 - SKU29: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 475 - sale_gamma: 58 - service_level: 0.95 - SKU290: - constraint: null - cost: 438 - init_stock: 268 - max_stock: 1000 - price: 779 - sale_gamma: 67 - service_level: 0.95 - SKU291: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 122 - max_stock: 1000 - price: 126 - sale_gamma: 61 - service_level: 0.95 - SKU292: - constraint: null - cost: 186 - init_stock: 15 - max_stock: 1000 - price: 344 - sale_gamma: 5 - service_level: 0.95 - SKU293: - constraint: G(stock_constraint) - cost: 162 - init_stock: 385 - max_stock: 1000 - price: 288 - sale_gamma: 55 - service_level: 0.95 - SKU294: - constraint: null - cost: 409 - init_stock: 45 - max_stock: 1000 - price: 605 - sale_gamma: 9 - service_level: 0.95 - SKU295: - constraint: null - cost: 435 - init_stock: 651 - max_stock: 1000 - price: 674 - sale_gamma: 93 - service_level: 0.95 - SKU296: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 552 - max_stock: 1000 - price: 580 - sale_gamma: 92 - service_level: 0.95 - SKU297: - constraint: null - cost: 298 - init_stock: 228 - max_stock: 1000 - price: 333 - sale_gamma: 38 - service_level: 0.95 - SKU298: - constraint: null - cost: 286 - init_stock: 140 - max_stock: 1000 - price: 500 - sale_gamma: 35 - service_level: 0.95 - SKU299: - constraint: G(stock_constraint) - cost: 367 - init_stock: 255 - max_stock: 1000 - price: 451 - sale_gamma: 51 - service_level: 0.95 - SKU3: - constraint: G(stock_constraint) - cost: 405 - init_stock: 48 - max_stock: 1000 - price: 733 - sale_gamma: 12 - service_level: 0.95 - SKU30: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 359 - init_stock: 276 - max_stock: 1000 - price: 581 - sale_gamma: 69 - service_level: 0.95 - SKU300: - constraint: G(low_profit -> low_stock_constraint) - cost: 376 - init_stock: 290 - max_stock: 1000 - price: 428 - sale_gamma: 58 - service_level: 0.95 - SKU301: - constraint: null - cost: 433 - init_stock: 581 - max_stock: 1000 - price: 857 - sale_gamma: 83 - service_level: 0.95 - SKU302: - constraint: null - cost: 184 - init_stock: 44 - max_stock: 1000 - price: 322 - sale_gamma: 11 - service_level: 0.95 - SKU303: - constraint: null - cost: 246 - init_stock: 188 - max_stock: 1000 - price: 487 - sale_gamma: 94 - service_level: 0.95 - SKU304: - constraint: G(low_profit -> low_stock_constraint) - cost: 419 - init_stock: 138 - max_stock: 1000 - price: 632 - sale_gamma: 23 - service_level: 0.95 - SKU305: - constraint: null - cost: 495 - init_stock: 500 - max_stock: 1000 - price: 772 - sale_gamma: 100 - service_level: 0.95 - SKU306: - constraint: null - cost: 479 - init_stock: 126 - max_stock: 1000 - price: 852 - sale_gamma: 42 - service_level: 0.95 - SKU307: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 210 - init_stock: 156 - max_stock: 1000 - price: 371 - sale_gamma: 78 - service_level: 0.95 - SKU308: - constraint: null - cost: 208 - init_stock: 25 - max_stock: 1000 - price: 262 - sale_gamma: 5 - service_level: 0.95 - SKU309: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 460 - max_stock: 1000 - price: 156 - sale_gamma: 92 - service_level: 0.95 - SKU31: - constraint: null - cost: 69 - init_stock: 280 - max_stock: 1000 - price: 120 - sale_gamma: 56 - service_level: 0.95 - SKU310: - constraint: null - cost: 234 - init_stock: 352 - max_stock: 1000 - price: 294 - sale_gamma: 44 - service_level: 0.95 - SKU311: - constraint: null - cost: 306 - init_stock: 400 - max_stock: 1000 - price: 437 - sale_gamma: 80 - service_level: 0.95 - SKU312: - constraint: G(stock_constraint) - cost: 291 - init_stock: 75 - max_stock: 1000 - price: 392 - sale_gamma: 25 - service_level: 0.95 - SKU313: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 324 - init_stock: 190 - max_stock: 1000 - price: 492 - sale_gamma: 38 - service_level: 0.95 - SKU314: - constraint: G(stock_constraint) - cost: 404 - init_stock: 116 - max_stock: 1000 - price: 456 - sale_gamma: 29 - service_level: 0.95 - SKU315: - constraint: G(stock_constraint) - cost: 471 - init_stock: 504 - max_stock: 1000 - price: 885 - sale_gamma: 84 - service_level: 0.95 - SKU316: - constraint: null - cost: 202 - init_stock: 90 - max_stock: 1000 - price: 282 - sale_gamma: 18 - service_level: 0.95 - SKU317: - constraint: null - cost: 216 - init_stock: 168 - max_stock: 1000 - price: 352 - sale_gamma: 24 - service_level: 0.95 - SKU318: - constraint: null - cost: 278 - init_stock: 255 - max_stock: 1000 - price: 350 - sale_gamma: 85 - service_level: 0.95 - SKU319: - constraint: null - cost: 257 - init_stock: 371 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU32: - constraint: null - cost: 336 - init_stock: 110 - max_stock: 1000 - price: 581 - sale_gamma: 22 - service_level: 0.95 - SKU320: - constraint: null - cost: 196 - init_stock: 156 - max_stock: 1000 - price: 258 - sale_gamma: 39 - service_level: 0.95 - SKU321: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 96 - max_stock: 1000 - price: 105 - sale_gamma: 16 - service_level: 0.95 - SKU322: - constraint: null - cost: 240 - init_stock: 600 - max_stock: 1000 - price: 453 - sale_gamma: 100 - service_level: 0.95 - SKU323: - constraint: G(stock_constraint) - cost: 66 - init_stock: 195 - max_stock: 1000 - price: 74 - sale_gamma: 39 - service_level: 0.95 - SKU324: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 442 - init_stock: 465 - max_stock: 1000 - price: 804 - sale_gamma: 93 - service_level: 0.95 - SKU325: - constraint: null - cost: 453 - init_stock: 345 - max_stock: 1000 - price: 579 - sale_gamma: 69 - service_level: 0.95 - SKU326: - constraint: null - cost: 345 - init_stock: 72 - max_stock: 1000 - price: 538 - sale_gamma: 24 - service_level: 0.95 - SKU327: - constraint: G(low_profit -> low_stock_constraint) - cost: 457 - init_stock: 84 - max_stock: 1000 - price: 749 - sale_gamma: 14 - service_level: 0.95 - SKU328: - constraint: null - cost: 390 - init_stock: 90 - max_stock: 1000 - price: 487 - sale_gamma: 45 - service_level: 0.95 - SKU329: - constraint: null - cost: 67 - init_stock: 84 - max_stock: 1000 - price: 126 - sale_gamma: 42 - service_level: 0.95 - SKU33: - constraint: G(low_profit -> low_stock_constraint) - cost: 416 - init_stock: 552 - max_stock: 1000 - price: 465 - sale_gamma: 92 - service_level: 0.95 - SKU330: - constraint: null - cost: 306 - init_stock: 273 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU331: - constraint: G(low_profit -> low_stock_constraint) - cost: 138 - init_stock: 164 - max_stock: 1000 - price: 180 - sale_gamma: 41 - service_level: 0.95 - SKU332: - constraint: null - cost: 390 - init_stock: 288 - max_stock: 1000 - price: 670 - sale_gamma: 96 - service_level: 0.95 - SKU333: - constraint: G(stock_constraint) - cost: 485 - init_stock: 318 - max_stock: 1000 - price: 800 - sale_gamma: 53 - service_level: 0.95 - SKU334: - constraint: null - cost: 183 - init_stock: 114 - max_stock: 1000 - price: 245 - sale_gamma: 57 - service_level: 0.95 - SKU335: - constraint: null - cost: 80 - init_stock: 243 - max_stock: 1000 - price: 141 - sale_gamma: 81 - service_level: 0.95 - SKU336: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 56 - max_stock: 1000 - price: 565 - sale_gamma: 28 - service_level: 0.95 - SKU337: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 394 - sale_gamma: 29 - service_level: 0.95 - SKU338: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 150 - sale_gamma: 81 - service_level: 0.95 - SKU339: - constraint: G(stock_constraint) - cost: 265 - init_stock: 441 - max_stock: 1000 - price: 368 - sale_gamma: 63 - service_level: 0.95 - SKU34: - constraint: null - cost: 95 - init_stock: 91 - max_stock: 1000 - price: 135 - sale_gamma: 13 - service_level: 0.95 - SKU340: - constraint: null - cost: 480 - init_stock: 435 - max_stock: 1000 - price: 633 - sale_gamma: 87 - service_level: 0.95 - SKU341: - constraint: G(low_profit -> low_stock_constraint) - cost: 462 - init_stock: 280 - max_stock: 1000 - price: 924 - sale_gamma: 70 - service_level: 0.95 - SKU342: - constraint: null - cost: 144 - init_stock: 72 - max_stock: 1000 - price: 216 - sale_gamma: 9 - service_level: 0.95 - SKU343: - constraint: null - cost: 310 - init_stock: 90 - max_stock: 1000 - price: 589 - sale_gamma: 15 - service_level: 0.95 - SKU344: - constraint: null - cost: 357 - init_stock: 348 - max_stock: 1000 - price: 442 - sale_gamma: 87 - service_level: 0.95 - SKU345: - constraint: null - cost: 442 - init_stock: 267 - max_stock: 1000 - price: 857 - sale_gamma: 89 - service_level: 0.95 - SKU346: - constraint: G(stock_constraint) - cost: 350 - init_stock: 336 - max_stock: 1000 - price: 549 - sale_gamma: 42 - service_level: 0.95 - SKU347: - constraint: G(low_profit -> low_stock_constraint) - cost: 497 - init_stock: 328 - max_stock: 1000 - price: 810 - sale_gamma: 82 - service_level: 0.95 - SKU348: - constraint: G(stock_constraint) - cost: 147 - init_stock: 100 - max_stock: 1000 - price: 277 - sale_gamma: 20 - service_level: 0.95 - SKU349: - constraint: null - cost: 67 - init_stock: 335 - max_stock: 1000 - price: 116 - sale_gamma: 67 - service_level: 0.95 - SKU35: - constraint: null - cost: 267 - init_stock: 430 - max_stock: 1000 - price: 373 - sale_gamma: 86 - service_level: 0.95 - SKU350: - constraint: G(stock_constraint) - cost: 279 - init_stock: 552 - max_stock: 1000 - price: 460 - sale_gamma: 92 - service_level: 0.95 - SKU351: - constraint: null - cost: 155 - init_stock: 335 - max_stock: 1000 - price: 294 - sale_gamma: 67 - service_level: 0.95 - SKU352: - constraint: null - cost: 71 - init_stock: 54 - max_stock: 1000 - price: 100 - sale_gamma: 18 - service_level: 0.95 - SKU353: - constraint: G(stock_constraint) - cost: 253 - init_stock: 465 - max_stock: 1000 - price: 437 - sale_gamma: 93 - service_level: 0.95 - SKU354: - constraint: G(low_profit -> low_stock_constraint) - cost: 396 - init_stock: 395 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU355: - constraint: G(stock_constraint) - cost: 63 - init_stock: 30 - max_stock: 1000 - price: 74 - sale_gamma: 10 - service_level: 0.95 - SKU356: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 87 - max_stock: 1000 - price: 441 - sale_gamma: 29 - service_level: 0.95 - SKU357: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 499 - init_stock: 460 - max_stock: 1000 - price: 868 - sale_gamma: 92 - service_level: 0.95 - SKU358: - constraint: null - cost: 269 - init_stock: 414 - max_stock: 1000 - price: 408 - sale_gamma: 69 - service_level: 0.95 - SKU359: - constraint: G(low_profit -> low_stock_constraint) - cost: 82 - init_stock: 600 - max_stock: 1000 - price: 110 - sale_gamma: 75 - service_level: 0.95 - SKU36: - constraint: null - cost: 105 - init_stock: 30 - max_stock: 1000 - price: 165 - sale_gamma: 10 - service_level: 0.95 - SKU360: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 75 - max_stock: 1000 - price: 682 - sale_gamma: 25 - service_level: 0.95 - SKU361: - constraint: null - cost: 163 - init_stock: 360 - max_stock: 1000 - price: 288 - sale_gamma: 90 - service_level: 0.95 - SKU362: - constraint: null - cost: 464 - init_stock: 435 - max_stock: 1000 - price: 867 - sale_gamma: 87 - service_level: 0.95 - SKU363: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 390 - max_stock: 1000 - price: 93 - sale_gamma: 78 - service_level: 0.95 - SKU364: - constraint: G(stock_constraint) - cost: 387 - init_stock: 66 - max_stock: 1000 - price: 472 - sale_gamma: 33 - service_level: 0.95 - SKU365: - constraint: null - cost: 158 - init_stock: 581 - max_stock: 1000 - price: 241 - sale_gamma: 83 - service_level: 0.95 - SKU366: - constraint: null - cost: 444 - init_stock: 344 - max_stock: 1000 - price: 639 - sale_gamma: 86 - service_level: 0.95 - SKU367: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 322 - max_stock: 1000 - price: 304 - sale_gamma: 46 - service_level: 0.95 - SKU368: - constraint: G(stock_constraint) - cost: 472 - init_stock: 198 - max_stock: 1000 - price: 613 - sale_gamma: 33 - service_level: 0.95 - SKU369: - constraint: null - cost: 96 - init_stock: 375 - max_stock: 1000 - price: 155 - sale_gamma: 75 - service_level: 0.95 - SKU37: - constraint: G(low_profit -> low_stock_constraint) - cost: 66 - init_stock: 177 - max_stock: 1000 - price: 110 - sale_gamma: 59 - service_level: 0.95 - SKU370: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 90 - max_stock: 1000 - price: 141 - sale_gamma: 15 - service_level: 0.95 - SKU371: - constraint: null - cost: 328 - init_stock: 25 - max_stock: 1000 - price: 511 - sale_gamma: 5 - service_level: 0.95 - SKU372: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 67 - init_stock: 192 - max_stock: 1000 - price: 134 - sale_gamma: 32 - service_level: 0.95 - SKU373: - constraint: null - cost: 58 - init_stock: 268 - max_stock: 1000 - price: 91 - sale_gamma: 67 - service_level: 0.95 - SKU374: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 216 - max_stock: 1000 - price: 73 - sale_gamma: 54 - service_level: 0.95 - SKU375: - constraint: G(low_profit -> low_stock_constraint) - cost: 283 - init_stock: 432 - max_stock: 1000 - price: 416 - sale_gamma: 72 - service_level: 0.95 - SKU376: - constraint: null - cost: 156 - init_stock: 164 - max_stock: 1000 - price: 291 - sale_gamma: 82 - service_level: 0.95 - SKU377: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 78 - init_stock: 536 - max_stock: 1000 - price: 134 - sale_gamma: 67 - service_level: 0.95 - SKU378: - constraint: G(low_profit -> low_stock_constraint) - cost: 424 - init_stock: 175 - max_stock: 1000 - price: 546 - sale_gamma: 35 - service_level: 0.95 - SKU379: - constraint: null - cost: 11 - init_stock: 196 - max_stock: 1000 - price: 20 - sale_gamma: 49 - service_level: 0.95 - SKU38: - constraint: null - cost: 344 - init_stock: 258 - max_stock: 1000 - price: 567 - sale_gamma: 43 - service_level: 0.95 - SKU380: - constraint: null - cost: 393 - init_stock: 212 - max_stock: 1000 - price: 605 - sale_gamma: 53 - service_level: 0.95 - SKU381: - constraint: null - cost: 476 - init_stock: 18 - max_stock: 1000 - price: 609 - sale_gamma: 6 - service_level: 0.95 - SKU382: - constraint: null - cost: 125 - init_stock: 426 - max_stock: 1000 - price: 177 - sale_gamma: 71 - service_level: 0.95 - SKU383: - constraint: null - cost: 304 - init_stock: 368 - max_stock: 1000 - price: 425 - sale_gamma: 92 - service_level: 0.95 - SKU384: - constraint: null - cost: 320 - init_stock: 54 - max_stock: 1000 - price: 460 - sale_gamma: 9 - service_level: 0.95 - SKU385: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 120 - init_stock: 91 - max_stock: 1000 - price: 184 - sale_gamma: 13 - service_level: 0.95 - SKU386: - constraint: G(stock_constraint) - cost: 295 - init_stock: 124 - max_stock: 1000 - price: 536 - sale_gamma: 31 - service_level: 0.95 - SKU387: - constraint: null - cost: 112 - init_stock: 582 - max_stock: 1000 - price: 154 - sale_gamma: 97 - service_level: 0.95 - SKU388: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 405 - init_stock: 264 - max_stock: 1000 - price: 635 - sale_gamma: 44 - service_level: 0.95 - SKU389: - constraint: G(stock_constraint) - cost: 376 - init_stock: 490 - max_stock: 1000 - price: 699 - sale_gamma: 70 - service_level: 0.95 - SKU39: - constraint: G(low_profit -> low_stock_constraint) - cost: 25 - init_stock: 204 - max_stock: 1000 - price: 45 - sale_gamma: 51 - service_level: 0.95 - SKU390: - constraint: null - cost: 144 - init_stock: 156 - max_stock: 1000 - price: 223 - sale_gamma: 39 - service_level: 0.95 - SKU391: - constraint: G(stock_constraint) - cost: 233 - init_stock: 402 - max_stock: 1000 - price: 403 - sale_gamma: 67 - service_level: 0.95 - SKU392: - constraint: null - cost: 163 - init_stock: 370 - max_stock: 1000 - price: 205 - sale_gamma: 74 - service_level: 0.95 - SKU393: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 402 - max_stock: 1000 - price: 701 - sale_gamma: 67 - service_level: 0.95 - SKU394: - constraint: null - cost: 154 - init_stock: 318 - max_stock: 1000 - price: 252 - sale_gamma: 53 - service_level: 0.95 - SKU395: - constraint: null - cost: 488 - init_stock: 198 - max_stock: 1000 - price: 854 - sale_gamma: 33 - service_level: 0.95 - SKU396: - constraint: null - cost: 333 - init_stock: 427 - max_stock: 1000 - price: 469 - sale_gamma: 61 - service_level: 0.95 - SKU397: - constraint: null - cost: 460 - init_stock: 153 - max_stock: 1000 - price: 791 - sale_gamma: 51 - service_level: 0.95 - SKU398: - constraint: G(stock_constraint) - cost: 234 - init_stock: 174 - max_stock: 1000 - price: 414 - sale_gamma: 58 - service_level: 0.95 - SKU399: - constraint: G(stock_constraint) - cost: 376 - init_stock: 148 - max_stock: 1000 - price: 612 - sale_gamma: 37 - service_level: 0.95 - SKU4: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 21 - max_stock: 1000 - price: 798 - sale_gamma: 7 - service_level: 0.95 - SKU40: - constraint: null - cost: 490 - init_stock: 594 - max_stock: 1000 - price: 563 - sale_gamma: 99 - service_level: 0.95 - SKU400: - constraint: G(low_profit -> low_stock_constraint) - cost: 445 - init_stock: 420 - max_stock: 1000 - price: 729 - sale_gamma: 84 - service_level: 0.95 - SKU401: - constraint: G(low_profit -> low_stock_constraint) - cost: 410 - init_stock: 312 - max_stock: 1000 - price: 774 - sale_gamma: 78 - service_level: 0.95 - SKU402: - constraint: G(low_profit -> low_stock_constraint) - cost: 86 - init_stock: 35 - max_stock: 1000 - price: 153 - sale_gamma: 7 - service_level: 0.95 - SKU403: - constraint: null - cost: 89 - init_stock: 594 - max_stock: 1000 - price: 133 - sale_gamma: 99 - service_level: 0.95 - SKU404: - constraint: null - cost: 287 - init_stock: 305 - max_stock: 1000 - price: 522 - sale_gamma: 61 - service_level: 0.95 - SKU405: - constraint: G(stock_constraint) - cost: 460 - init_stock: 114 - max_stock: 1000 - price: 584 - sale_gamma: 19 - service_level: 0.95 - SKU406: - constraint: null - cost: 327 - init_stock: 400 - max_stock: 1000 - price: 405 - sale_gamma: 100 - service_level: 0.95 - SKU407: - constraint: null - cost: 26 - init_stock: 184 - max_stock: 1000 - price: 41 - sale_gamma: 46 - service_level: 0.95 - SKU408: - constraint: null - cost: 444 - init_stock: 48 - max_stock: 1000 - price: 754 - sale_gamma: 8 - service_level: 0.95 - SKU409: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 257 - init_stock: 273 - max_stock: 1000 - price: 449 - sale_gamma: 91 - service_level: 0.95 - SKU41: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 141 - init_stock: 553 - max_stock: 1000 - price: 162 - sale_gamma: 79 - service_level: 0.95 - SKU410: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 70 - init_stock: 32 - max_stock: 1000 - price: 88 - sale_gamma: 16 - service_level: 0.95 - SKU411: - constraint: G(stock_constraint) - cost: 210 - init_stock: 380 - max_stock: 1000 - price: 405 - sale_gamma: 95 - service_level: 0.95 - SKU412: - constraint: null - cost: 286 - init_stock: 248 - max_stock: 1000 - price: 414 - sale_gamma: 62 - service_level: 0.95 - SKU413: - constraint: null - cost: 403 - init_stock: 581 - max_stock: 1000 - price: 801 - sale_gamma: 83 - service_level: 0.95 - SKU414: - constraint: G(stock_constraint) - cost: 165 - init_stock: 435 - max_stock: 1000 - price: 229 - sale_gamma: 87 - service_level: 0.95 - SKU415: - constraint: G(stock_constraint) - cost: 291 - init_stock: 184 - max_stock: 1000 - price: 372 - sale_gamma: 23 - service_level: 0.95 - SKU416: - constraint: null - cost: 228 - init_stock: 36 - max_stock: 1000 - price: 373 - sale_gamma: 9 - service_level: 0.95 - SKU417: - constraint: G(low_profit -> low_stock_constraint) - cost: 443 - init_stock: 288 - max_stock: 1000 - price: 872 - sale_gamma: 72 - service_level: 0.95 - SKU418: - constraint: null - cost: 458 - init_stock: 52 - max_stock: 1000 - price: 838 - sale_gamma: 13 - service_level: 0.95 - SKU419: - constraint: null - cost: 303 - init_stock: 712 - max_stock: 1000 - price: 448 - sale_gamma: 89 - service_level: 0.95 - SKU42: - constraint: null - cost: 462 - init_stock: 156 - max_stock: 1000 - price: 688 - sale_gamma: 26 - service_level: 0.95 - SKU420: - constraint: null - cost: 268 - init_stock: 84 - max_stock: 1000 - price: 506 - sale_gamma: 42 - service_level: 0.95 - SKU421: - constraint: G(stock_constraint) - cost: 214 - init_stock: 138 - max_stock: 1000 - price: 314 - sale_gamma: 46 - service_level: 0.95 - SKU422: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 432 - max_stock: 1000 - price: 97 - sale_gamma: 54 - service_level: 0.95 - SKU423: - constraint: G(low_profit -> low_stock_constraint) - cost: 188 - init_stock: 396 - max_stock: 1000 - price: 265 - sale_gamma: 66 - service_level: 0.95 - SKU424: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 66 - max_stock: 1000 - price: 317 - sale_gamma: 11 - service_level: 0.95 - SKU425: - constraint: G(stock_constraint) - cost: 162 - init_stock: 72 - max_stock: 1000 - price: 277 - sale_gamma: 12 - service_level: 0.95 - SKU426: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 125 - init_stock: 588 - max_stock: 1000 - price: 246 - sale_gamma: 98 - service_level: 0.95 - SKU427: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 413 - init_stock: 282 - max_stock: 1000 - price: 710 - sale_gamma: 94 - service_level: 0.95 - SKU428: - constraint: G(stock_constraint) - cost: 391 - init_stock: 378 - max_stock: 1000 - price: 629 - sale_gamma: 63 - service_level: 0.95 - SKU429: - constraint: G(stock_constraint) - cost: 39 - init_stock: 246 - max_stock: 1000 - price: 73 - sale_gamma: 41 - service_level: 0.95 - SKU43: - constraint: G(stock_constraint) - cost: 217 - init_stock: 272 - max_stock: 1000 - price: 353 - sale_gamma: 68 - service_level: 0.95 - SKU430: - constraint: null - cost: 216 - init_stock: 511 - max_stock: 1000 - price: 313 - sale_gamma: 73 - service_level: 0.95 - SKU431: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 328 - init_stock: 126 - max_stock: 1000 - price: 646 - sale_gamma: 21 - service_level: 0.95 - SKU432: - constraint: G(stock_constraint) - cost: 25 - init_stock: 368 - max_stock: 1000 - price: 35 - sale_gamma: 46 - service_level: 0.95 - SKU433: - constraint: null - cost: 348 - init_stock: 270 - max_stock: 1000 - price: 396 - sale_gamma: 45 - service_level: 0.95 - SKU434: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 90 - max_stock: 1000 - price: 40 - sale_gamma: 30 - service_level: 0.95 - SKU435: - constraint: G(stock_constraint) - cost: 272 - init_stock: 210 - max_stock: 1000 - price: 320 - sale_gamma: 30 - service_level: 0.95 - SKU436: - constraint: null - cost: 72 - init_stock: 405 - max_stock: 1000 - price: 105 - sale_gamma: 81 - service_level: 0.95 - SKU437: - constraint: G(stock_constraint) - cost: 115 - init_stock: 84 - max_stock: 1000 - price: 223 - sale_gamma: 14 - service_level: 0.95 - SKU438: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 40 - max_stock: 1000 - price: 190 - sale_gamma: 10 - service_level: 0.95 - SKU439: - constraint: G(stock_constraint) - cost: 256 - init_stock: 190 - max_stock: 1000 - price: 304 - sale_gamma: 38 - service_level: 0.95 - SKU44: - constraint: null - cost: 360 - init_stock: 240 - max_stock: 1000 - price: 669 - sale_gamma: 48 - service_level: 0.95 - SKU440: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 248 - init_stock: 246 - max_stock: 1000 - price: 357 - sale_gamma: 82 - service_level: 0.95 - SKU441: - constraint: null - cost: 253 - init_stock: 224 - max_stock: 1000 - price: 374 - sale_gamma: 56 - service_level: 0.95 - SKU442: - constraint: null - cost: 470 - init_stock: 352 - max_stock: 1000 - price: 629 - sale_gamma: 88 - service_level: 0.95 - SKU443: - constraint: null - cost: 218 - init_stock: 365 - max_stock: 1000 - price: 361 - sale_gamma: 73 - service_level: 0.95 - SKU444: - constraint: null - cost: 156 - init_stock: 18 - max_stock: 1000 - price: 182 - sale_gamma: 6 - service_level: 0.95 - SKU445: - constraint: null - cost: 260 - init_stock: 602 - max_stock: 1000 - price: 296 - sale_gamma: 86 - service_level: 0.95 - SKU446: - constraint: null - cost: 497 - init_stock: 147 - max_stock: 1000 - price: 690 - sale_gamma: 49 - service_level: 0.95 - SKU447: - constraint: null - cost: 196 - init_stock: 30 - max_stock: 1000 - price: 280 - sale_gamma: 5 - service_level: 0.95 - SKU448: - constraint: null - cost: 485 - init_stock: 497 - max_stock: 1000 - price: 742 - sale_gamma: 71 - service_level: 0.95 - SKU449: - constraint: null - cost: 282 - init_stock: 114 - max_stock: 1000 - price: 360 - sale_gamma: 38 - service_level: 0.95 - SKU45: - constraint: G(stock_constraint) - cost: 253 - init_stock: 30 - max_stock: 1000 - price: 351 - sale_gamma: 10 - service_level: 0.95 - SKU450: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 58 - init_stock: 294 - max_stock: 1000 - price: 77 - sale_gamma: 98 - service_level: 0.95 - SKU451: - constraint: null - cost: 468 - init_stock: 483 - max_stock: 1000 - price: 851 - sale_gamma: 69 - service_level: 0.95 - SKU452: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 63 - init_stock: 395 - max_stock: 1000 - price: 97 - sale_gamma: 79 - service_level: 0.95 - SKU453: - constraint: null - cost: 37 - init_stock: 140 - max_stock: 1000 - price: 65 - sale_gamma: 35 - service_level: 0.95 - SKU454: - constraint: G(low_profit -> low_stock_constraint) - cost: 494 - init_stock: 340 - max_stock: 1000 - price: 899 - sale_gamma: 85 - service_level: 0.95 - SKU455: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 136 - init_stock: 182 - max_stock: 1000 - price: 195 - sale_gamma: 26 - service_level: 0.95 - SKU456: - constraint: G(stock_constraint) - cost: 338 - init_stock: 75 - max_stock: 1000 - price: 659 - sale_gamma: 25 - service_level: 0.95 - SKU457: - constraint: null - cost: 169 - init_stock: 256 - max_stock: 1000 - price: 190 - sale_gamma: 64 - service_level: 0.95 - SKU458: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 403 - init_stock: 28 - max_stock: 1000 - price: 685 - sale_gamma: 7 - service_level: 0.95 - SKU459: - constraint: null - cost: 425 - init_stock: 272 - max_stock: 1000 - price: 731 - sale_gamma: 34 - service_level: 0.95 - SKU46: - constraint: G(low_profit -> low_stock_constraint) - cost: 299 - init_stock: 200 - max_stock: 1000 - price: 409 - sale_gamma: 50 - service_level: 0.95 - SKU460: - constraint: G(low_profit -> low_stock_constraint) - cost: 405 - init_stock: 292 - max_stock: 1000 - price: 558 - sale_gamma: 73 - service_level: 0.95 - SKU461: - constraint: G(stock_constraint) - cost: 251 - init_stock: 240 - max_stock: 1000 - price: 326 - sale_gamma: 48 - service_level: 0.95 - SKU462: - constraint: G(low_profit -> low_stock_constraint) - cost: 448 - init_stock: 36 - max_stock: 1000 - price: 757 - sale_gamma: 9 - service_level: 0.95 - SKU463: - constraint: null - cost: 187 - init_stock: 574 - max_stock: 1000 - price: 213 - sale_gamma: 82 - service_level: 0.95 - SKU464: - constraint: null - cost: 279 - init_stock: 272 - max_stock: 1000 - price: 438 - sale_gamma: 34 - service_level: 0.95 - SKU465: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 147 - init_stock: 400 - max_stock: 1000 - price: 288 - sale_gamma: 100 - service_level: 0.95 - SKU466: - constraint: G(stock_constraint) - cost: 97 - init_stock: 126 - max_stock: 1000 - price: 167 - sale_gamma: 42 - service_level: 0.95 - SKU467: - constraint: G(stock_constraint) - cost: 142 - init_stock: 252 - max_stock: 1000 - price: 230 - sale_gamma: 36 - service_level: 0.95 - SKU468: - constraint: G(stock_constraint) - cost: 55 - init_stock: 250 - max_stock: 1000 - price: 104 - sale_gamma: 50 - service_level: 0.95 - SKU469: - constraint: G(stock_constraint) - cost: 178 - init_stock: 105 - max_stock: 1000 - price: 331 - sale_gamma: 15 - service_level: 0.95 - SKU47: - constraint: G(stock_constraint) - cost: 121 - init_stock: 117 - max_stock: 1000 - price: 240 - sale_gamma: 39 - service_level: 0.95 - SKU470: - constraint: G(low_profit -> low_stock_constraint) - cost: 373 - init_stock: 128 - max_stock: 1000 - price: 548 - sale_gamma: 32 - service_level: 0.95 - SKU471: - constraint: G(low_profit -> low_stock_constraint) - cost: 315 - init_stock: 568 - max_stock: 1000 - price: 387 - sale_gamma: 71 - service_level: 0.95 - SKU472: - constraint: null - cost: 421 - init_stock: 177 - max_stock: 1000 - price: 627 - sale_gamma: 59 - service_level: 0.95 - SKU473: - constraint: G(low_profit -> low_stock_constraint) - cost: 195 - init_stock: 105 - max_stock: 1000 - price: 323 - sale_gamma: 21 - service_level: 0.95 - SKU474: - constraint: null - cost: 448 - init_stock: 602 - max_stock: 1000 - price: 775 - sale_gamma: 86 - service_level: 0.95 - SKU475: - constraint: null - cost: 34 - init_stock: 282 - max_stock: 1000 - price: 62 - sale_gamma: 94 - service_level: 0.95 - SKU476: - constraint: null - cost: 251 - init_stock: 180 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU477: - constraint: null - cost: 289 - init_stock: 329 - max_stock: 1000 - price: 338 - sale_gamma: 47 - service_level: 0.95 - SKU478: - constraint: null - cost: 479 - init_stock: 352 - max_stock: 1000 - price: 723 - sale_gamma: 88 - service_level: 0.95 - SKU479: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 366 - init_stock: 152 - max_stock: 1000 - price: 552 - sale_gamma: 38 - service_level: 0.95 - SKU48: - constraint: null - cost: 340 - init_stock: 232 - max_stock: 1000 - price: 387 - sale_gamma: 58 - service_level: 0.95 - SKU480: - constraint: G(stock_constraint) - cost: 18 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 30 - service_level: 0.95 - SKU481: - constraint: null - cost: 330 - init_stock: 352 - max_stock: 1000 - price: 422 - sale_gamma: 88 - service_level: 0.95 - SKU482: - constraint: G(stock_constraint) - cost: 94 - init_stock: 696 - max_stock: 1000 - price: 108 - sale_gamma: 87 - service_level: 0.95 - SKU483: - constraint: null - cost: 298 - init_stock: 170 - max_stock: 1000 - price: 533 - sale_gamma: 34 - service_level: 0.95 - SKU484: - constraint: null - cost: 84 - init_stock: 365 - max_stock: 1000 - price: 117 - sale_gamma: 73 - service_level: 0.95 - SKU485: - constraint: null - cost: 318 - init_stock: 536 - max_stock: 1000 - price: 543 - sale_gamma: 67 - service_level: 0.95 - SKU486: - constraint: G(low_profit -> low_stock_constraint) - cost: 122 - init_stock: 208 - max_stock: 1000 - price: 161 - sale_gamma: 52 - service_level: 0.95 - SKU487: - constraint: null - cost: 277 - init_stock: 264 - max_stock: 1000 - price: 362 - sale_gamma: 66 - service_level: 0.95 - SKU488: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 189 - max_stock: 1000 - price: 42 - sale_gamma: 27 - service_level: 0.95 - SKU489: - constraint: null - cost: 59 - init_stock: 124 - max_stock: 1000 - price: 97 - sale_gamma: 31 - service_level: 0.95 - SKU49: - constraint: null - cost: 263 - init_stock: 760 - max_stock: 1000 - price: 515 - sale_gamma: 95 - service_level: 0.95 - SKU490: - constraint: null - cost: 161 - init_stock: 486 - max_stock: 1000 - price: 264 - sale_gamma: 81 - service_level: 0.95 - SKU491: - constraint: null - cost: 444 - init_stock: 450 - max_stock: 1000 - price: 834 - sale_gamma: 75 - service_level: 0.95 - SKU492: - constraint: null - cost: 53 - init_stock: 240 - max_stock: 1000 - price: 63 - sale_gamma: 80 - service_level: 0.95 - SKU493: - constraint: null - cost: 461 - init_stock: 81 - max_stock: 1000 - price: 567 - sale_gamma: 27 - service_level: 0.95 - SKU494: - constraint: null - cost: 145 - init_stock: 282 - max_stock: 1000 - price: 242 - sale_gamma: 94 - service_level: 0.95 - SKU495: - constraint: G(stock_constraint) - cost: 149 - init_stock: 372 - max_stock: 1000 - price: 217 - sale_gamma: 62 - service_level: 0.95 - SKU496: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 342 - init_stock: 295 - max_stock: 1000 - price: 492 - sale_gamma: 59 - service_level: 0.95 - SKU497: - constraint: G(low_profit -> low_stock_constraint) - cost: 33 - init_stock: 45 - max_stock: 1000 - price: 42 - sale_gamma: 9 - service_level: 0.95 - SKU498: - constraint: null - cost: 305 - init_stock: 325 - max_stock: 1000 - price: 439 - sale_gamma: 65 - service_level: 0.95 - SKU499: - constraint: G(stock_constraint) - cost: 307 - init_stock: 174 - max_stock: 1000 - price: 472 - sale_gamma: 29 - service_level: 0.95 - SKU5: - constraint: G(stock_constraint) - cost: 466 - init_stock: 426 - max_stock: 1000 - price: 852 - sale_gamma: 71 - service_level: 0.95 - SKU50: - constraint: null - cost: 282 - init_stock: 185 - max_stock: 1000 - price: 516 - sale_gamma: 37 - service_level: 0.95 - SKU500: - constraint: null - cost: 208 - init_stock: 336 - max_stock: 1000 - price: 255 - sale_gamma: 42 - service_level: 0.95 - SKU501: - constraint: null - cost: 194 - init_stock: 152 - max_stock: 1000 - price: 265 - sale_gamma: 76 - service_level: 0.95 - SKU502: - constraint: null - cost: 69 - init_stock: 390 - max_stock: 1000 - price: 101 - sale_gamma: 65 - service_level: 0.95 - SKU503: - constraint: G(stock_constraint) - cost: 208 - init_stock: 228 - max_stock: 1000 - price: 280 - sale_gamma: 57 - service_level: 0.95 - SKU504: - constraint: null - cost: 251 - init_stock: 210 - max_stock: 1000 - price: 426 - sale_gamma: 30 - service_level: 0.95 - SKU505: - constraint: null - cost: 426 - init_stock: 295 - max_stock: 1000 - price: 515 - sale_gamma: 59 - service_level: 0.95 - SKU506: - constraint: null - cost: 149 - init_stock: 465 - max_stock: 1000 - price: 220 - sale_gamma: 93 - service_level: 0.95 - SKU507: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 187 - init_stock: 60 - max_stock: 1000 - price: 293 - sale_gamma: 15 - service_level: 0.95 - SKU508: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 576 - max_stock: 1000 - price: 432 - sale_gamma: 96 - service_level: 0.95 - SKU509: - constraint: G(low_profit -> low_stock_constraint) - cost: 297 - init_stock: 486 - max_stock: 1000 - price: 397 - sale_gamma: 81 - service_level: 0.95 - SKU51: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 469 - max_stock: 1000 - price: 477 - sale_gamma: 67 - service_level: 0.95 - SKU510: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 36 - max_stock: 1000 - price: 156 - sale_gamma: 9 - service_level: 0.95 - SKU511: - constraint: G(stock_constraint) - cost: 361 - init_stock: 450 - max_stock: 1000 - price: 480 - sale_gamma: 75 - service_level: 0.95 - SKU512: - constraint: null - cost: 467 - init_stock: 132 - max_stock: 1000 - price: 854 - sale_gamma: 22 - service_level: 0.95 - SKU513: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 343 - init_stock: 42 - max_stock: 1000 - price: 631 - sale_gamma: 7 - service_level: 0.95 - SKU514: - constraint: null - cost: 186 - init_stock: 504 - max_stock: 1000 - price: 332 - sale_gamma: 63 - service_level: 0.95 - SKU515: - constraint: null - cost: 145 - init_stock: 216 - max_stock: 1000 - price: 227 - sale_gamma: 54 - service_level: 0.95 - SKU516: - constraint: G(stock_constraint) - cost: 64 - init_stock: 114 - max_stock: 1000 - price: 96 - sale_gamma: 38 - service_level: 0.95 - SKU517: - constraint: null - cost: 117 - init_stock: 45 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU518: - constraint: G(stock_constraint) - cost: 26 - init_stock: 147 - max_stock: 1000 - price: 30 - sale_gamma: 21 - service_level: 0.95 - SKU519: - constraint: null - cost: 233 - init_stock: 250 - max_stock: 1000 - price: 410 - sale_gamma: 50 - service_level: 0.95 - SKU52: - constraint: null - cost: 22 - init_stock: 402 - max_stock: 1000 - price: 28 - sale_gamma: 67 - service_level: 0.95 - SKU520: - constraint: G(low_profit -> low_stock_constraint) - cost: 209 - init_stock: 44 - max_stock: 1000 - price: 248 - sale_gamma: 22 - service_level: 0.95 - SKU521: - constraint: G(low_profit -> low_stock_constraint) - cost: 220 - init_stock: 350 - max_stock: 1000 - price: 330 - sale_gamma: 50 - service_level: 0.95 - SKU522: - constraint: null - cost: 156 - init_stock: 522 - max_stock: 1000 - price: 277 - sale_gamma: 87 - service_level: 0.95 - SKU523: - constraint: null - cost: 65 - init_stock: 500 - max_stock: 1000 - price: 107 - sale_gamma: 100 - service_level: 0.95 - SKU524: - constraint: null - cost: 294 - init_stock: 188 - max_stock: 1000 - price: 464 - sale_gamma: 94 - service_level: 0.95 - SKU525: - constraint: G(stock_constraint) - cost: 432 - init_stock: 126 - max_stock: 1000 - price: 751 - sale_gamma: 42 - service_level: 0.95 - SKU526: - constraint: null - cost: 419 - init_stock: 168 - max_stock: 1000 - price: 716 - sale_gamma: 24 - service_level: 0.95 - SKU527: - constraint: null - cost: 131 - init_stock: 350 - max_stock: 1000 - price: 262 - sale_gamma: 70 - service_level: 0.95 - SKU528: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 84 - max_stock: 1000 - price: 581 - sale_gamma: 21 - service_level: 0.95 - SKU529: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 248 - max_stock: 1000 - price: 375 - sale_gamma: 31 - service_level: 0.95 - SKU53: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 151 - init_stock: 490 - max_stock: 1000 - price: 191 - sale_gamma: 70 - service_level: 0.95 - SKU530: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 135 - max_stock: 1000 - price: 205 - sale_gamma: 45 - service_level: 0.95 - SKU531: - constraint: null - cost: 103 - init_stock: 300 - max_stock: 1000 - price: 153 - sale_gamma: 60 - service_level: 0.95 - SKU532: - constraint: null - cost: 351 - init_stock: 539 - max_stock: 1000 - price: 431 - sale_gamma: 77 - service_level: 0.95 - SKU533: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 168 - max_stock: 1000 - price: 340 - sale_gamma: 42 - service_level: 0.95 - SKU534: - constraint: null - cost: 425 - init_stock: 82 - max_stock: 1000 - price: 548 - sale_gamma: 41 - service_level: 0.95 - SKU535: - constraint: null - cost: 304 - init_stock: 430 - max_stock: 1000 - price: 465 - sale_gamma: 86 - service_level: 0.95 - SKU536: - constraint: null - cost: 358 - init_stock: 208 - max_stock: 1000 - price: 415 - sale_gamma: 52 - service_level: 0.95 - SKU537: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 321 - init_stock: 45 - max_stock: 1000 - price: 455 - sale_gamma: 9 - service_level: 0.95 - SKU538: - constraint: null - cost: 457 - init_stock: 200 - max_stock: 1000 - price: 868 - sale_gamma: 50 - service_level: 0.95 - SKU539: - constraint: G(stock_constraint) - cost: 165 - init_stock: 234 - max_stock: 1000 - price: 229 - sale_gamma: 78 - service_level: 0.95 - SKU54: - constraint: G(low_profit -> low_stock_constraint) - cost: 158 - init_stock: 138 - max_stock: 1000 - price: 241 - sale_gamma: 46 - service_level: 0.95 - SKU540: - constraint: null - cost: 388 - init_stock: 294 - max_stock: 1000 - price: 496 - sale_gamma: 42 - service_level: 0.95 - SKU541: - constraint: null - cost: 131 - init_stock: 203 - max_stock: 1000 - price: 213 - sale_gamma: 29 - service_level: 0.95 - SKU542: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 130 - max_stock: 1000 - price: 42 - sale_gamma: 26 - service_level: 0.95 - SKU543: - constraint: G(stock_constraint) - cost: 430 - init_stock: 510 - max_stock: 1000 - price: 718 - sale_gamma: 85 - service_level: 0.95 - SKU544: - constraint: G(stock_constraint) - cost: 346 - init_stock: 194 - max_stock: 1000 - price: 474 - sale_gamma: 97 - service_level: 0.95 - SKU545: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU546: - constraint: null - cost: 491 - init_stock: 576 - max_stock: 1000 - price: 765 - sale_gamma: 96 - service_level: 0.95 - SKU547: - constraint: G(stock_constraint) - cost: 161 - init_stock: 264 - max_stock: 1000 - price: 251 - sale_gamma: 44 - service_level: 0.95 - SKU548: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 111 - init_stock: 36 - max_stock: 1000 - price: 217 - sale_gamma: 6 - service_level: 0.95 - SKU549: - constraint: G(stock_constraint) - cost: 387 - init_stock: 316 - max_stock: 1000 - price: 510 - sale_gamma: 79 - service_level: 0.95 - SKU55: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 455 - max_stock: 1000 - price: 896 - sale_gamma: 65 - service_level: 0.95 - SKU550: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 259 - init_stock: 282 - max_stock: 1000 - price: 502 - sale_gamma: 94 - service_level: 0.95 - SKU551: - constraint: null - cost: 46 - init_stock: 186 - max_stock: 1000 - price: 60 - sale_gamma: 31 - service_level: 0.95 - SKU552: - constraint: null - cost: 191 - init_stock: 450 - max_stock: 1000 - price: 315 - sale_gamma: 90 - service_level: 0.95 - SKU553: - constraint: G(low_profit -> low_stock_constraint) - cost: 208 - init_stock: 60 - max_stock: 1000 - price: 251 - sale_gamma: 30 - service_level: 0.95 - SKU554: - constraint: null - cost: 340 - init_stock: 82 - max_stock: 1000 - price: 387 - sale_gamma: 41 - service_level: 0.95 - SKU555: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 252 - max_stock: 1000 - price: 684 - sale_gamma: 84 - service_level: 0.95 - SKU556: - constraint: G(stock_constraint) - cost: 110 - init_stock: 90 - max_stock: 1000 - price: 213 - sale_gamma: 30 - service_level: 0.95 - SKU557: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 334 - init_stock: 365 - max_stock: 1000 - price: 661 - sale_gamma: 73 - service_level: 0.95 - SKU558: - constraint: null - cost: 399 - init_stock: 85 - max_stock: 1000 - price: 490 - sale_gamma: 17 - service_level: 0.95 - SKU559: - constraint: null - cost: 313 - init_stock: 270 - max_stock: 1000 - price: 591 - sale_gamma: 54 - service_level: 0.95 - SKU56: - constraint: null - cost: 377 - init_stock: 222 - max_stock: 1000 - price: 671 - sale_gamma: 74 - service_level: 0.95 - SKU560: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 283 - init_stock: 246 - max_stock: 1000 - price: 458 - sale_gamma: 41 - service_level: 0.95 - SKU561: - constraint: null - cost: 202 - init_stock: 312 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU562: - constraint: null - cost: 347 - init_stock: 356 - max_stock: 1000 - price: 666 - sale_gamma: 89 - service_level: 0.95 - SKU563: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 401 - init_stock: 30 - max_stock: 1000 - price: 509 - sale_gamma: 5 - service_level: 0.95 - SKU564: - constraint: null - cost: 109 - init_stock: 63 - max_stock: 1000 - price: 183 - sale_gamma: 9 - service_level: 0.95 - SKU565: - constraint: null - cost: 57 - init_stock: 525 - max_stock: 1000 - price: 90 - sale_gamma: 75 - service_level: 0.95 - SKU566: - constraint: null - cost: 131 - init_stock: 44 - max_stock: 1000 - price: 165 - sale_gamma: 11 - service_level: 0.95 - SKU567: - constraint: G(stock_constraint) - cost: 361 - init_stock: 27 - max_stock: 1000 - price: 465 - sale_gamma: 9 - service_level: 0.95 - SKU568: - constraint: null - cost: 75 - init_stock: 234 - max_stock: 1000 - price: 102 - sale_gamma: 78 - service_level: 0.95 - SKU569: - constraint: null - cost: 473 - init_stock: 192 - max_stock: 1000 - price: 591 - sale_gamma: 48 - service_level: 0.95 - SKU57: - constraint: G(stock_constraint) - cost: 359 - init_stock: 350 - max_stock: 1000 - price: 682 - sale_gamma: 50 - service_level: 0.95 - SKU570: - constraint: null - cost: 388 - init_stock: 581 - max_stock: 1000 - price: 686 - sale_gamma: 83 - service_level: 0.95 - SKU571: - constraint: G(stock_constraint) - cost: 168 - init_stock: 78 - max_stock: 1000 - price: 208 - sale_gamma: 39 - service_level: 0.95 - SKU572: - constraint: null - cost: 26 - init_stock: 225 - max_stock: 1000 - price: 41 - sale_gamma: 45 - service_level: 0.95 - SKU573: - constraint: null - cost: 246 - init_stock: 164 - max_stock: 1000 - price: 391 - sale_gamma: 41 - service_level: 0.95 - SKU574: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 150 - max_stock: 1000 - price: 166 - sale_gamma: 50 - service_level: 0.95 - SKU575: - constraint: null - cost: 237 - init_stock: 237 - max_stock: 1000 - price: 438 - sale_gamma: 79 - service_level: 0.95 - SKU576: - constraint: G(stock_constraint) - cost: 265 - init_stock: 468 - max_stock: 1000 - price: 416 - sale_gamma: 78 - service_level: 0.95 - SKU577: - constraint: G(low_profit -> low_stock_constraint) - cost: 18 - init_stock: 348 - max_stock: 1000 - price: 24 - sale_gamma: 87 - service_level: 0.95 - SKU578: - constraint: null - cost: 100 - init_stock: 284 - max_stock: 1000 - price: 148 - sale_gamma: 71 - service_level: 0.95 - SKU579: - constraint: null - cost: 415 - init_stock: 27 - max_stock: 1000 - price: 771 - sale_gamma: 9 - service_level: 0.95 - SKU58: - constraint: null - cost: 362 - init_stock: 240 - max_stock: 1000 - price: 521 - sale_gamma: 48 - service_level: 0.95 - SKU580: - constraint: null - cost: 203 - init_stock: 15 - max_stock: 1000 - price: 261 - sale_gamma: 5 - service_level: 0.95 - SKU581: - constraint: null - cost: 152 - init_stock: 516 - max_stock: 1000 - price: 185 - sale_gamma: 86 - service_level: 0.95 - SKU582: - constraint: G(stock_constraint) - cost: 359 - init_stock: 480 - max_stock: 1000 - price: 567 - sale_gamma: 96 - service_level: 0.95 - SKU583: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 86 - init_stock: 261 - max_stock: 1000 - price: 98 - sale_gamma: 87 - service_level: 0.95 - SKU584: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 33 - init_stock: 270 - max_stock: 1000 - price: 36 - sale_gamma: 45 - service_level: 0.95 - SKU585: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 53 - sale_gamma: 20 - service_level: 0.95 - SKU586: - constraint: G(stock_constraint) - cost: 61 - init_stock: 264 - max_stock: 1000 - price: 94 - sale_gamma: 44 - service_level: 0.95 - SKU587: - constraint: null - cost: 290 - init_stock: 468 - max_stock: 1000 - price: 423 - sale_gamma: 78 - service_level: 0.95 - SKU588: - constraint: G(stock_constraint) - cost: 476 - init_stock: 176 - max_stock: 1000 - price: 885 - sale_gamma: 44 - service_level: 0.95 - SKU589: - constraint: null - cost: 244 - init_stock: 170 - max_stock: 1000 - price: 380 - sale_gamma: 34 - service_level: 0.95 - SKU59: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 255 - max_stock: 1000 - price: 275 - sale_gamma: 51 - service_level: 0.95 - SKU590: - constraint: null - cost: 70 - init_stock: 93 - max_stock: 1000 - price: 103 - sale_gamma: 31 - service_level: 0.95 - SKU591: - constraint: G(stock_constraint) - cost: 206 - init_stock: 504 - max_stock: 1000 - price: 298 - sale_gamma: 84 - service_level: 0.95 - SKU592: - constraint: null - cost: 218 - init_stock: 276 - max_stock: 1000 - price: 418 - sale_gamma: 46 - service_level: 0.95 - SKU593: - constraint: null - cost: 124 - init_stock: 88 - max_stock: 1000 - price: 221 - sale_gamma: 44 - service_level: 0.95 - SKU594: - constraint: null - cost: 238 - init_stock: 150 - max_stock: 1000 - price: 459 - sale_gamma: 50 - service_level: 0.95 - SKU595: - constraint: null - cost: 73 - init_stock: 172 - max_stock: 1000 - price: 107 - sale_gamma: 43 - service_level: 0.95 - SKU596: - constraint: null - cost: 432 - init_stock: 35 - max_stock: 1000 - price: 540 - sale_gamma: 7 - service_level: 0.95 - SKU597: - constraint: G(stock_constraint) - cost: 252 - init_stock: 435 - max_stock: 1000 - price: 451 - sale_gamma: 87 - service_level: 0.95 - SKU598: - constraint: null - cost: 348 - init_stock: 28 - max_stock: 1000 - price: 612 - sale_gamma: 14 - service_level: 0.95 - SKU599: - constraint: null - cost: 54 - init_stock: 204 - max_stock: 1000 - price: 66 - sale_gamma: 68 - service_level: 0.95 - SKU6: - constraint: null - cost: 122 - init_stock: 616 - max_stock: 1000 - price: 195 - sale_gamma: 88 - service_level: 0.95 - SKU60: - constraint: G(stock_constraint) - cost: 200 - init_stock: 234 - max_stock: 1000 - price: 346 - sale_gamma: 39 - service_level: 0.95 - SKU600: - constraint: null - cost: 386 - init_stock: 325 - max_stock: 1000 - price: 505 - sale_gamma: 65 - service_level: 0.95 - SKU601: - constraint: null - cost: 138 - init_stock: 273 - max_stock: 1000 - price: 198 - sale_gamma: 39 - service_level: 0.95 - SKU602: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 184 - init_stock: 588 - max_stock: 1000 - price: 318 - sale_gamma: 98 - service_level: 0.95 - SKU603: - constraint: G(low_profit -> low_stock_constraint) - cost: 278 - init_stock: 252 - max_stock: 1000 - price: 550 - sale_gamma: 42 - service_level: 0.95 - SKU604: - constraint: G(stock_constraint) - cost: 270 - init_stock: 400 - max_stock: 1000 - price: 378 - sale_gamma: 50 - service_level: 0.95 - SKU605: - constraint: null - cost: 288 - init_stock: 120 - max_stock: 1000 - price: 443 - sale_gamma: 24 - service_level: 0.95 - SKU606: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 114 - init_stock: 66 - max_stock: 1000 - price: 161 - sale_gamma: 11 - service_level: 0.95 - SKU607: - constraint: null - cost: 208 - init_stock: 395 - max_stock: 1000 - price: 359 - sale_gamma: 79 - service_level: 0.95 - SKU608: - constraint: null - cost: 39 - init_stock: 365 - max_stock: 1000 - price: 49 - sale_gamma: 73 - service_level: 0.95 - SKU609: - constraint: null - cost: 102 - init_stock: 114 - max_stock: 1000 - price: 171 - sale_gamma: 19 - service_level: 0.95 - SKU61: - constraint: null - cost: 461 - init_stock: 150 - max_stock: 1000 - price: 645 - sale_gamma: 75 - service_level: 0.95 - SKU610: - constraint: null - cost: 449 - init_stock: 204 - max_stock: 1000 - price: 749 - sale_gamma: 68 - service_level: 0.95 - SKU611: - constraint: G(low_profit -> low_stock_constraint) - cost: 306 - init_stock: 539 - max_stock: 1000 - price: 489 - sale_gamma: 77 - service_level: 0.95 - SKU612: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 391 - init_stock: 616 - max_stock: 1000 - price: 613 - sale_gamma: 88 - service_level: 0.95 - SKU613: - constraint: G(stock_constraint) - cost: 174 - init_stock: 56 - max_stock: 1000 - price: 254 - sale_gamma: 7 - service_level: 0.95 - SKU614: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 45 - max_stock: 1000 - price: 318 - sale_gamma: 15 - service_level: 0.95 - SKU615: - constraint: G(stock_constraint) - cost: 330 - init_stock: 364 - max_stock: 1000 - price: 432 - sale_gamma: 91 - service_level: 0.95 - SKU616: - constraint: G(low_profit -> low_stock_constraint) - cost: 232 - init_stock: 219 - max_stock: 1000 - price: 382 - sale_gamma: 73 - service_level: 0.95 - SKU617: - constraint: null - cost: 203 - init_stock: 420 - max_stock: 1000 - price: 227 - sale_gamma: 60 - service_level: 0.95 - SKU618: - constraint: G(stock_constraint) - cost: 77 - init_stock: 138 - max_stock: 1000 - price: 97 - sale_gamma: 23 - service_level: 0.95 - SKU619: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 344 - max_stock: 1000 - price: 359 - sale_gamma: 86 - service_level: 0.95 - SKU62: - constraint: null - cost: 124 - init_stock: 36 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU620: - constraint: null - cost: 231 - init_stock: 156 - max_stock: 1000 - price: 267 - sale_gamma: 39 - service_level: 0.95 - SKU621: - constraint: null - cost: 199 - init_stock: 216 - max_stock: 1000 - price: 332 - sale_gamma: 72 - service_level: 0.95 - SKU622: - constraint: null - cost: 253 - init_stock: 455 - max_stock: 1000 - price: 468 - sale_gamma: 65 - service_level: 0.95 - SKU623: - constraint: null - cost: 335 - init_stock: 220 - max_stock: 1000 - price: 368 - sale_gamma: 55 - service_level: 0.95 - SKU624: - constraint: null - cost: 482 - init_stock: 270 - max_stock: 1000 - price: 824 - sale_gamma: 54 - service_level: 0.95 - SKU625: - constraint: null - cost: 46 - init_stock: 445 - max_stock: 1000 - price: 60 - sale_gamma: 89 - service_level: 0.95 - SKU626: - constraint: null - cost: 451 - init_stock: 534 - max_stock: 1000 - price: 694 - sale_gamma: 89 - service_level: 0.95 - SKU627: - constraint: null - cost: 220 - init_stock: 656 - max_stock: 1000 - price: 264 - sale_gamma: 82 - service_level: 0.95 - SKU628: - constraint: null - cost: 124 - init_stock: 156 - max_stock: 1000 - price: 229 - sale_gamma: 26 - service_level: 0.95 - SKU629: - constraint: G(stock_constraint) - cost: 353 - init_stock: 460 - max_stock: 1000 - price: 515 - sale_gamma: 92 - service_level: 0.95 - SKU63: - constraint: null - cost: 68 - init_stock: 70 - max_stock: 1000 - price: 86 - sale_gamma: 14 - service_level: 0.95 - SKU630: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 122 - init_stock: 34 - max_stock: 1000 - price: 137 - sale_gamma: 17 - service_level: 0.95 - SKU631: - constraint: null - cost: 296 - init_stock: 196 - max_stock: 1000 - price: 399 - sale_gamma: 49 - service_level: 0.95 - SKU632: - constraint: null - cost: 85 - init_stock: 470 - max_stock: 1000 - price: 141 - sale_gamma: 94 - service_level: 0.95 - SKU633: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 402 - max_stock: 1000 - price: 228 - sale_gamma: 67 - service_level: 0.95 - SKU634: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 17 - init_stock: 438 - max_stock: 1000 - price: 30 - sale_gamma: 73 - service_level: 0.95 - SKU635: - constraint: null - cost: 341 - init_stock: 372 - max_stock: 1000 - price: 654 - sale_gamma: 93 - service_level: 0.95 - SKU636: - constraint: G(stock_constraint) - cost: 385 - init_stock: 111 - max_stock: 1000 - price: 739 - sale_gamma: 37 - service_level: 0.95 - SKU637: - constraint: G(low_profit -> low_stock_constraint) - cost: 83 - init_stock: 305 - max_stock: 1000 - price: 118 - sale_gamma: 61 - service_level: 0.95 - SKU638: - constraint: G(stock_constraint) - cost: 375 - init_stock: 16 - max_stock: 1000 - price: 536 - sale_gamma: 8 - service_level: 0.95 - SKU639: - constraint: null - cost: 327 - init_stock: 160 - max_stock: 1000 - price: 487 - sale_gamma: 40 - service_level: 0.95 - SKU64: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 133 - max_stock: 1000 - price: 42 - sale_gamma: 19 - service_level: 0.95 - SKU640: - constraint: G(low_profit -> low_stock_constraint) - cost: 275 - init_stock: 546 - max_stock: 1000 - price: 382 - sale_gamma: 91 - service_level: 0.95 - SKU641: - constraint: null - cost: 365 - init_stock: 486 - max_stock: 1000 - price: 445 - sale_gamma: 81 - service_level: 0.95 - SKU642: - constraint: null - cost: 414 - init_stock: 150 - max_stock: 1000 - price: 554 - sale_gamma: 25 - service_level: 0.95 - SKU643: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 358 - init_stock: 322 - max_stock: 1000 - price: 461 - sale_gamma: 46 - service_level: 0.95 - SKU644: - constraint: G(low_profit -> low_stock_constraint) - cost: 46 - init_stock: 410 - max_stock: 1000 - price: 61 - sale_gamma: 82 - service_level: 0.95 - SKU645: - constraint: null - cost: 467 - init_stock: 594 - max_stock: 1000 - price: 742 - sale_gamma: 99 - service_level: 0.95 - SKU646: - constraint: G(low_profit -> low_stock_constraint) - cost: 189 - init_stock: 91 - max_stock: 1000 - price: 268 - sale_gamma: 13 - service_level: 0.95 - SKU647: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 462 - max_stock: 1000 - price: 369 - sale_gamma: 77 - service_level: 0.95 - SKU648: - constraint: G(stock_constraint) - cost: 226 - init_stock: 110 - max_stock: 1000 - price: 336 - sale_gamma: 55 - service_level: 0.95 - SKU649: - constraint: null - cost: 198 - init_stock: 175 - max_stock: 1000 - price: 229 - sale_gamma: 25 - service_level: 0.95 - SKU65: - constraint: null - cost: 15 - init_stock: 658 - max_stock: 1000 - price: 25 - sale_gamma: 94 - service_level: 0.95 - SKU650: - constraint: null - cost: 351 - init_stock: 224 - max_stock: 1000 - price: 498 - sale_gamma: 56 - service_level: 0.95 - SKU651: - constraint: null - cost: 380 - init_stock: 672 - max_stock: 1000 - price: 596 - sale_gamma: 96 - service_level: 0.95 - SKU652: - constraint: null - cost: 123 - init_stock: 280 - max_stock: 1000 - price: 168 - sale_gamma: 40 - service_level: 0.95 - SKU653: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 384 - max_stock: 1000 - price: 661 - sale_gamma: 96 - service_level: 0.95 - SKU654: - constraint: null - cost: 66 - init_stock: 32 - max_stock: 1000 - price: 110 - sale_gamma: 8 - service_level: 0.95 - SKU655: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 126 - max_stock: 1000 - price: 479 - sale_gamma: 42 - service_level: 0.95 - SKU656: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 53 - init_stock: 504 - max_stock: 1000 - price: 81 - sale_gamma: 72 - service_level: 0.95 - SKU657: - constraint: G(low_profit -> low_stock_constraint) - cost: 73 - init_stock: 567 - max_stock: 1000 - price: 110 - sale_gamma: 81 - service_level: 0.95 - SKU658: - constraint: null - cost: 70 - init_stock: 252 - max_stock: 1000 - price: 107 - sale_gamma: 36 - service_level: 0.95 - SKU659: - constraint: null - cost: 21 - init_stock: 336 - max_stock: 1000 - price: 27 - sale_gamma: 56 - service_level: 0.95 - SKU66: - constraint: null - cost: 143 - init_stock: 360 - max_stock: 1000 - price: 230 - sale_gamma: 90 - service_level: 0.95 - SKU660: - constraint: null - cost: 487 - init_stock: 415 - max_stock: 1000 - price: 560 - sale_gamma: 83 - service_level: 0.95 - SKU661: - constraint: null - cost: 344 - init_stock: 296 - max_stock: 1000 - price: 581 - sale_gamma: 74 - service_level: 0.95 - SKU662: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 266 - max_stock: 1000 - price: 628 - sale_gamma: 38 - service_level: 0.95 - SKU663: - constraint: G(stock_constraint) - cost: 418 - init_stock: 320 - max_stock: 1000 - price: 518 - sale_gamma: 40 - service_level: 0.95 - SKU664: - constraint: G(stock_constraint) - cost: 351 - init_stock: 420 - max_stock: 1000 - price: 494 - sale_gamma: 84 - service_level: 0.95 - SKU665: - constraint: null - cost: 493 - init_stock: 344 - max_stock: 1000 - price: 734 - sale_gamma: 43 - service_level: 0.95 - SKU666: - constraint: G(stock_constraint) - cost: 341 - init_stock: 176 - max_stock: 1000 - price: 572 - sale_gamma: 88 - service_level: 0.95 - SKU667: - constraint: null - cost: 325 - init_stock: 52 - max_stock: 1000 - price: 562 - sale_gamma: 13 - service_level: 0.95 - SKU668: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 300 - max_stock: 1000 - price: 463 - sale_gamma: 60 - service_level: 0.95 - SKU669: - constraint: null - cost: 74 - init_stock: 96 - max_stock: 1000 - price: 83 - sale_gamma: 16 - service_level: 0.95 - SKU67: - constraint: G(low_profit -> low_stock_constraint) - cost: 247 - init_stock: 159 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU670: - constraint: G(stock_constraint) - cost: 289 - init_stock: 258 - max_stock: 1000 - price: 430 - sale_gamma: 86 - service_level: 0.95 - SKU671: - constraint: G(stock_constraint) - cost: 267 - init_stock: 332 - max_stock: 1000 - price: 296 - sale_gamma: 83 - service_level: 0.95 - SKU672: - constraint: null - cost: 369 - init_stock: 78 - max_stock: 1000 - price: 420 - sale_gamma: 13 - service_level: 0.95 - SKU673: - constraint: G(low_profit -> low_stock_constraint) - cost: 322 - init_stock: 123 - max_stock: 1000 - price: 418 - sale_gamma: 41 - service_level: 0.95 - SKU674: - constraint: null - cost: 223 - init_stock: 66 - max_stock: 1000 - price: 263 - sale_gamma: 22 - service_level: 0.95 - SKU675: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 438 - init_stock: 99 - max_stock: 1000 - price: 529 - sale_gamma: 33 - service_level: 0.95 - SKU676: - constraint: null - cost: 244 - init_stock: 366 - max_stock: 1000 - price: 275 - sale_gamma: 61 - service_level: 0.95 - SKU677: - constraint: null - cost: 425 - init_stock: 176 - max_stock: 1000 - price: 658 - sale_gamma: 44 - service_level: 0.95 - SKU678: - constraint: null - cost: 168 - init_stock: 216 - max_stock: 1000 - price: 199 - sale_gamma: 36 - service_level: 0.95 - SKU679: - constraint: null - cost: 395 - init_stock: 132 - max_stock: 1000 - price: 734 - sale_gamma: 44 - service_level: 0.95 - SKU68: - constraint: G(stock_constraint) - cost: 169 - init_stock: 56 - max_stock: 1000 - price: 239 - sale_gamma: 14 - service_level: 0.95 - SKU680: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 260 - init_stock: 180 - max_stock: 1000 - price: 327 - sale_gamma: 30 - service_level: 0.95 - SKU681: - constraint: G(low_profit -> low_stock_constraint) - cost: 464 - init_stock: 776 - max_stock: 1000 - price: 510 - sale_gamma: 97 - service_level: 0.95 - SKU682: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 370 - init_stock: 318 - max_stock: 1000 - price: 407 - sale_gamma: 53 - service_level: 0.95 - SKU683: - constraint: G(low_profit -> low_stock_constraint) - cost: 327 - init_stock: 536 - max_stock: 1000 - price: 608 - sale_gamma: 67 - service_level: 0.95 - SKU684: - constraint: G(stock_constraint) - cost: 355 - init_stock: 158 - max_stock: 1000 - price: 401 - sale_gamma: 79 - service_level: 0.95 - SKU685: - constraint: null - cost: 422 - init_stock: 270 - max_stock: 1000 - price: 493 - sale_gamma: 45 - service_level: 0.95 - SKU686: - constraint: G(low_profit -> low_stock_constraint) - cost: 63 - init_stock: 378 - max_stock: 1000 - price: 122 - sale_gamma: 63 - service_level: 0.95 - SKU687: - constraint: G(low_profit -> low_stock_constraint) - cost: 34 - init_stock: 456 - max_stock: 1000 - price: 43 - sale_gamma: 76 - service_level: 0.95 - SKU688: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 462 - max_stock: 1000 - price: 759 - sale_gamma: 77 - service_level: 0.95 - SKU689: - constraint: G(stock_constraint) - cost: 499 - init_stock: 75 - max_stock: 1000 - price: 808 - sale_gamma: 15 - service_level: 0.95 - SKU69: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 176 - init_stock: 402 - max_stock: 1000 - price: 197 - sale_gamma: 67 - service_level: 0.95 - SKU690: - constraint: null - cost: 437 - init_stock: 415 - max_stock: 1000 - price: 498 - sale_gamma: 83 - service_level: 0.95 - SKU691: - constraint: null - cost: 17 - init_stock: 632 - max_stock: 1000 - price: 18 - sale_gamma: 79 - service_level: 0.95 - SKU692: - constraint: G(stock_constraint) - cost: 225 - init_stock: 195 - max_stock: 1000 - price: 258 - sale_gamma: 65 - service_level: 0.95 - SKU693: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 19 - init_stock: 244 - max_stock: 1000 - price: 21 - sale_gamma: 61 - service_level: 0.95 - SKU694: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 208 - init_stock: 300 - max_stock: 1000 - price: 386 - sale_gamma: 75 - service_level: 0.95 - SKU695: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 190 - init_stock: 49 - max_stock: 1000 - price: 361 - sale_gamma: 7 - service_level: 0.95 - SKU696: - constraint: G(low_profit -> low_stock_constraint) - cost: 348 - init_stock: 290 - max_stock: 1000 - price: 643 - sale_gamma: 58 - service_level: 0.95 - SKU697: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 455 - init_stock: 320 - max_stock: 1000 - price: 641 - sale_gamma: 80 - service_level: 0.95 - SKU698: - constraint: null - cost: 198 - init_stock: 488 - max_stock: 1000 - price: 316 - sale_gamma: 61 - service_level: 0.95 - SKU699: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 58 - sale_gamma: 20 - service_level: 0.95 - SKU7: - constraint: null - cost: 101 - init_stock: 480 - max_stock: 1000 - price: 131 - sale_gamma: 96 - service_level: 0.95 - SKU70: - constraint: G(stock_constraint) - cost: 186 - init_stock: 140 - max_stock: 1000 - price: 288 - sale_gamma: 35 - service_level: 0.95 - SKU700: - constraint: G(low_profit -> low_stock_constraint) - cost: 74 - init_stock: 45 - max_stock: 1000 - price: 130 - sale_gamma: 9 - service_level: 0.95 - SKU701: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 399 - init_stock: 462 - max_stock: 1000 - price: 770 - sale_gamma: 77 - service_level: 0.95 - SKU702: - constraint: G(stock_constraint) - cost: 82 - init_stock: 204 - max_stock: 1000 - price: 163 - sale_gamma: 34 - service_level: 0.95 - SKU703: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 363 - init_stock: 76 - max_stock: 1000 - price: 602 - sale_gamma: 38 - service_level: 0.95 - SKU704: - constraint: G(stock_constraint) - cost: 384 - init_stock: 261 - max_stock: 1000 - price: 518 - sale_gamma: 87 - service_level: 0.95 - SKU705: - constraint: null - cost: 128 - init_stock: 378 - max_stock: 1000 - price: 236 - sale_gamma: 63 - service_level: 0.95 - SKU706: - constraint: null - cost: 182 - init_stock: 455 - max_stock: 1000 - price: 242 - sale_gamma: 91 - service_level: 0.95 - SKU707: - constraint: null - cost: 18 - init_stock: 276 - max_stock: 1000 - price: 35 - sale_gamma: 69 - service_level: 0.95 - SKU708: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 178 - init_stock: 112 - max_stock: 1000 - price: 334 - sale_gamma: 28 - service_level: 0.95 - SKU709: - constraint: G(stock_constraint) - cost: 102 - init_stock: 212 - max_stock: 1000 - price: 173 - sale_gamma: 53 - service_level: 0.95 - SKU71: - constraint: null - cost: 315 - init_stock: 225 - max_stock: 1000 - price: 589 - sale_gamma: 45 - service_level: 0.95 - SKU710: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 252 - init_stock: 95 - max_stock: 1000 - price: 337 - sale_gamma: 19 - service_level: 0.95 - SKU711: - constraint: null - cost: 281 - init_stock: 414 - max_stock: 1000 - price: 320 - sale_gamma: 69 - service_level: 0.95 - SKU712: - constraint: null - cost: 232 - init_stock: 77 - max_stock: 1000 - price: 438 - sale_gamma: 11 - service_level: 0.95 - SKU713: - constraint: null - cost: 285 - init_stock: 129 - max_stock: 1000 - price: 413 - sale_gamma: 43 - service_level: 0.95 - SKU714: - constraint: null - cost: 79 - init_stock: 287 - max_stock: 1000 - price: 106 - sale_gamma: 41 - service_level: 0.95 - SKU715: - constraint: G(stock_constraint) - cost: 234 - init_stock: 34 - max_stock: 1000 - price: 271 - sale_gamma: 17 - service_level: 0.95 - SKU716: - constraint: null - cost: 70 - init_stock: 450 - max_stock: 1000 - price: 123 - sale_gamma: 75 - service_level: 0.95 - SKU717: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 345 - init_stock: 56 - max_stock: 1000 - price: 382 - sale_gamma: 8 - service_level: 0.95 - SKU718: - constraint: null - cost: 357 - init_stock: 174 - max_stock: 1000 - price: 692 - sale_gamma: 58 - service_level: 0.95 - SKU719: - constraint: null - cost: 340 - init_stock: 455 - max_stock: 1000 - price: 384 - sale_gamma: 65 - service_level: 0.95 - SKU72: - constraint: null - cost: 458 - init_stock: 595 - max_stock: 1000 - price: 870 - sale_gamma: 85 - service_level: 0.95 - SKU720: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 503 - sale_gamma: 42 - service_level: 0.95 - SKU721: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 73 - init_stock: 44 - max_stock: 1000 - price: 143 - sale_gamma: 11 - service_level: 0.95 - SKU722: - constraint: G(stock_constraint) - cost: 392 - init_stock: 194 - max_stock: 1000 - price: 572 - sale_gamma: 97 - service_level: 0.95 - SKU723: - constraint: null - cost: 318 - init_stock: 356 - max_stock: 1000 - price: 442 - sale_gamma: 89 - service_level: 0.95 - SKU724: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 198 - max_stock: 1000 - price: 520 - sale_gamma: 33 - service_level: 0.95 - SKU725: - constraint: null - cost: 175 - init_stock: 148 - max_stock: 1000 - price: 309 - sale_gamma: 37 - service_level: 0.95 - SKU726: - constraint: G(low_profit -> low_stock_constraint) - cost: 458 - init_stock: 356 - max_stock: 1000 - price: 567 - sale_gamma: 89 - service_level: 0.95 - SKU727: - constraint: null - cost: 418 - init_stock: 285 - max_stock: 1000 - price: 526 - sale_gamma: 57 - service_level: 0.95 - SKU728: - constraint: null - cost: 475 - init_stock: 185 - max_stock: 1000 - price: 864 - sale_gamma: 37 - service_level: 0.95 - SKU729: - constraint: null - cost: 324 - init_stock: 252 - max_stock: 1000 - price: 476 - sale_gamma: 36 - service_level: 0.95 - SKU73: - constraint: null - cost: 212 - init_stock: 216 - max_stock: 1000 - price: 248 - sale_gamma: 54 - service_level: 0.95 - SKU730: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 65 - max_stock: 1000 - price: 19 - sale_gamma: 13 - service_level: 0.95 - SKU731: - constraint: G(stock_constraint) - cost: 88 - init_stock: 410 - max_stock: 1000 - price: 155 - sale_gamma: 82 - service_level: 0.95 - SKU732: - constraint: null - cost: 41 - init_stock: 434 - max_stock: 1000 - price: 73 - sale_gamma: 62 - service_level: 0.95 - SKU733: - constraint: G(stock_constraint) - cost: 315 - init_stock: 104 - max_stock: 1000 - price: 541 - sale_gamma: 26 - service_level: 0.95 - SKU734: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 255 - max_stock: 1000 - price: 71 - sale_gamma: 51 - service_level: 0.95 - SKU735: - constraint: null - cost: 266 - init_stock: 594 - max_stock: 1000 - price: 409 - sale_gamma: 99 - service_level: 0.95 - SKU736: - constraint: G(stock_constraint) - cost: 368 - init_stock: 75 - max_stock: 1000 - price: 632 - sale_gamma: 25 - service_level: 0.95 - SKU737: - constraint: null - cost: 475 - init_stock: 93 - max_stock: 1000 - price: 655 - sale_gamma: 31 - service_level: 0.95 - SKU738: - constraint: null - cost: 185 - init_stock: 192 - max_stock: 1000 - price: 246 - sale_gamma: 48 - service_level: 0.95 - SKU739: - constraint: G(low_profit -> low_stock_constraint) - cost: 475 - init_stock: 296 - max_stock: 1000 - price: 612 - sale_gamma: 74 - service_level: 0.95 - SKU74: - constraint: null - cost: 159 - init_stock: 168 - max_stock: 1000 - price: 189 - sale_gamma: 42 - service_level: 0.95 - SKU740: - constraint: null - cost: 390 - init_stock: 256 - max_stock: 1000 - price: 549 - sale_gamma: 64 - service_level: 0.95 - SKU741: - constraint: G(stock_constraint) - cost: 91 - init_stock: 280 - max_stock: 1000 - price: 112 - sale_gamma: 56 - service_level: 0.95 - SKU742: - constraint: G(stock_constraint) - cost: 188 - init_stock: 110 - max_stock: 1000 - price: 374 - sale_gamma: 22 - service_level: 0.95 - SKU743: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 217 - init_stock: 301 - max_stock: 1000 - price: 431 - sale_gamma: 43 - service_level: 0.95 - SKU744: - constraint: G(low_profit -> low_stock_constraint) - cost: 379 - init_stock: 290 - max_stock: 1000 - price: 579 - sale_gamma: 58 - service_level: 0.95 - SKU745: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 368 - max_stock: 1000 - price: 376 - sale_gamma: 92 - service_level: 0.95 - SKU746: - constraint: null - cost: 437 - init_stock: 160 - max_stock: 1000 - price: 624 - sale_gamma: 40 - service_level: 0.95 - SKU747: - constraint: null - cost: 373 - init_stock: 474 - max_stock: 1000 - price: 589 - sale_gamma: 79 - service_level: 0.95 - SKU748: - constraint: null - cost: 275 - init_stock: 330 - max_stock: 1000 - price: 464 - sale_gamma: 66 - service_level: 0.95 - SKU749: - constraint: null - cost: 394 - init_stock: 106 - max_stock: 1000 - price: 705 - sale_gamma: 53 - service_level: 0.95 - SKU75: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 76 - max_stock: 1000 - price: 217 - sale_gamma: 19 - service_level: 0.95 - SKU750: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 310 - max_stock: 1000 - price: 463 - sale_gamma: 62 - service_level: 0.95 - SKU751: - constraint: null - cost: 369 - init_stock: 325 - max_stock: 1000 - price: 428 - sale_gamma: 65 - service_level: 0.95 - SKU752: - constraint: null - cost: 259 - init_stock: 546 - max_stock: 1000 - price: 435 - sale_gamma: 78 - service_level: 0.95 - SKU753: - constraint: G(stock_constraint) - cost: 77 - init_stock: 325 - max_stock: 1000 - price: 128 - sale_gamma: 65 - service_level: 0.95 - SKU754: - constraint: G(stock_constraint) - cost: 387 - init_stock: 91 - max_stock: 1000 - price: 545 - sale_gamma: 13 - service_level: 0.95 - SKU755: - constraint: null - cost: 354 - init_stock: 560 - max_stock: 1000 - price: 523 - sale_gamma: 80 - service_level: 0.95 - SKU756: - constraint: G(stock_constraint) - cost: 246 - init_stock: 360 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU757: - constraint: null - cost: 40 - init_stock: 399 - max_stock: 1000 - price: 76 - sale_gamma: 57 - service_level: 0.95 - SKU758: - constraint: null - cost: 241 - init_stock: 160 - max_stock: 1000 - price: 457 - sale_gamma: 40 - service_level: 0.95 - SKU759: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 435 - init_stock: 76 - max_stock: 1000 - price: 648 - sale_gamma: 38 - service_level: 0.95 - SKU76: - constraint: null - cost: 147 - init_stock: 256 - max_stock: 1000 - price: 191 - sale_gamma: 64 - service_level: 0.95 - SKU760: - constraint: null - cost: 176 - init_stock: 255 - max_stock: 1000 - price: 279 - sale_gamma: 51 - service_level: 0.95 - SKU761: - constraint: G(stock_constraint) - cost: 224 - init_stock: 275 - max_stock: 1000 - price: 250 - sale_gamma: 55 - service_level: 0.95 - SKU762: - constraint: G(stock_constraint) - cost: 264 - init_stock: 153 - max_stock: 1000 - price: 351 - sale_gamma: 51 - service_level: 0.95 - SKU763: - constraint: null - cost: 385 - init_stock: 316 - max_stock: 1000 - price: 677 - sale_gamma: 79 - service_level: 0.95 - SKU764: - constraint: null - cost: 349 - init_stock: 68 - max_stock: 1000 - price: 596 - sale_gamma: 34 - service_level: 0.95 - SKU765: - constraint: null - cost: 345 - init_stock: 52 - max_stock: 1000 - price: 472 - sale_gamma: 13 - service_level: 0.95 - SKU766: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 478 - init_stock: 140 - max_stock: 1000 - price: 855 - sale_gamma: 20 - service_level: 0.95 - SKU767: - constraint: null - cost: 95 - init_stock: 456 - max_stock: 1000 - price: 160 - sale_gamma: 76 - service_level: 0.95 - SKU768: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 644 - max_stock: 1000 - price: 244 - sale_gamma: 92 - service_level: 0.95 - SKU769: - constraint: null - cost: 24 - init_stock: 87 - max_stock: 1000 - price: 43 - sale_gamma: 29 - service_level: 0.95 - SKU77: - constraint: null - cost: 409 - init_stock: 144 - max_stock: 1000 - price: 687 - sale_gamma: 72 - service_level: 0.95 - SKU770: - constraint: null - cost: 150 - init_stock: 186 - max_stock: 1000 - price: 166 - sale_gamma: 62 - service_level: 0.95 - SKU771: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 35 - max_stock: 1000 - price: 182 - sale_gamma: 7 - service_level: 0.95 - SKU772: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 36 - max_stock: 1000 - price: 281 - sale_gamma: 6 - service_level: 0.95 - SKU773: - constraint: null - cost: 84 - init_stock: 368 - max_stock: 1000 - price: 117 - sale_gamma: 92 - service_level: 0.95 - SKU774: - constraint: null - cost: 447 - init_stock: 118 - max_stock: 1000 - price: 746 - sale_gamma: 59 - service_level: 0.95 - SKU775: - constraint: null - cost: 175 - init_stock: 688 - max_stock: 1000 - price: 192 - sale_gamma: 86 - service_level: 0.95 - SKU776: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 103 - init_stock: 425 - max_stock: 1000 - price: 172 - sale_gamma: 85 - service_level: 0.95 - SKU777: - constraint: null - cost: 292 - init_stock: 171 - max_stock: 1000 - price: 347 - sale_gamma: 57 - service_level: 0.95 - SKU778: - constraint: null - cost: 203 - init_stock: 40 - max_stock: 1000 - price: 288 - sale_gamma: 8 - service_level: 0.95 - SKU779: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 117 - init_stock: 215 - max_stock: 1000 - price: 186 - sale_gamma: 43 - service_level: 0.95 - SKU78: - constraint: null - cost: 234 - init_stock: 91 - max_stock: 1000 - price: 400 - sale_gamma: 13 - service_level: 0.95 - SKU780: - constraint: G(low_profit -> low_stock_constraint) - cost: 69 - init_stock: 410 - max_stock: 1000 - price: 102 - sale_gamma: 82 - service_level: 0.95 - SKU781: - constraint: null - cost: 372 - init_stock: 144 - max_stock: 1000 - price: 528 - sale_gamma: 36 - service_level: 0.95 - SKU782: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 70 - max_stock: 1000 - price: 35 - sale_gamma: 10 - service_level: 0.95 - SKU783: - constraint: null - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 139 - sale_gamma: 81 - service_level: 0.95 - SKU784: - constraint: null - cost: 309 - init_stock: 312 - max_stock: 1000 - price: 577 - sale_gamma: 52 - service_level: 0.95 - SKU785: - constraint: null - cost: 191 - init_stock: 210 - max_stock: 1000 - price: 255 - sale_gamma: 70 - service_level: 0.95 - SKU786: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 91 - init_stock: 132 - max_stock: 1000 - price: 102 - sale_gamma: 22 - service_level: 0.95 - SKU787: - constraint: null - cost: 360 - init_stock: 366 - max_stock: 1000 - price: 658 - sale_gamma: 61 - service_level: 0.95 - SKU788: - constraint: G(stock_constraint) - cost: 351 - init_stock: 112 - max_stock: 1000 - price: 417 - sale_gamma: 28 - service_level: 0.95 - SKU789: - constraint: G(stock_constraint) - cost: 153 - init_stock: 232 - max_stock: 1000 - price: 298 - sale_gamma: 58 - service_level: 0.95 - SKU79: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 245 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU790: - constraint: null - cost: 417 - init_stock: 330 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU791: - constraint: G(stock_constraint) - cost: 134 - init_stock: 56 - max_stock: 1000 - price: 155 - sale_gamma: 28 - service_level: 0.95 - SKU792: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 84 - max_stock: 1000 - price: 494 - sale_gamma: 21 - service_level: 0.95 - SKU793: - constraint: null - cost: 195 - init_stock: 532 - max_stock: 1000 - price: 282 - sale_gamma: 76 - service_level: 0.95 - SKU794: - constraint: null - cost: 325 - init_stock: 400 - max_stock: 1000 - price: 454 - sale_gamma: 80 - service_level: 0.95 - SKU795: - constraint: null - cost: 276 - init_stock: 288 - max_stock: 1000 - price: 549 - sale_gamma: 48 - service_level: 0.95 - SKU796: - constraint: null - cost: 447 - init_stock: 294 - max_stock: 1000 - price: 885 - sale_gamma: 49 - service_level: 0.95 - SKU797: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 100 - init_stock: 234 - max_stock: 1000 - price: 119 - sale_gamma: 39 - service_level: 0.95 - SKU798: - constraint: null - cost: 426 - init_stock: 180 - max_stock: 1000 - price: 630 - sale_gamma: 30 - service_level: 0.95 - SKU799: - constraint: null - cost: 63 - init_stock: 21 - max_stock: 1000 - price: 78 - sale_gamma: 7 - service_level: 0.95 - SKU8: - constraint: G(stock_constraint) - cost: 125 - init_stock: 252 - max_stock: 1000 - price: 196 - sale_gamma: 63 - service_level: 0.95 - SKU80: - constraint: G(stock_constraint) - cost: 163 - init_stock: 420 - max_stock: 1000 - price: 270 - sale_gamma: 70 - service_level: 0.95 - SKU800: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 470 - max_stock: 1000 - price: 455 - sale_gamma: 94 - service_level: 0.95 - SKU801: - constraint: G(low_profit -> low_stock_constraint) - cost: 389 - init_stock: 216 - max_stock: 1000 - price: 672 - sale_gamma: 36 - service_level: 0.95 - SKU802: - constraint: G(stock_constraint) - cost: 173 - init_stock: 310 - max_stock: 1000 - price: 281 - sale_gamma: 62 - service_level: 0.95 - SKU803: - constraint: null - cost: 272 - init_stock: 95 - max_stock: 1000 - price: 544 - sale_gamma: 19 - service_level: 0.95 - SKU804: - constraint: null - cost: 390 - init_stock: 188 - max_stock: 1000 - price: 491 - sale_gamma: 47 - service_level: 0.95 - SKU805: - constraint: null - cost: 61 - init_stock: 132 - max_stock: 1000 - price: 118 - sale_gamma: 33 - service_level: 0.95 - SKU806: - constraint: null - cost: 158 - init_stock: 156 - max_stock: 1000 - price: 186 - sale_gamma: 52 - service_level: 0.95 - SKU807: - constraint: null - cost: 453 - init_stock: 182 - max_stock: 1000 - price: 656 - sale_gamma: 26 - service_level: 0.95 - SKU808: - constraint: G(stock_constraint) - cost: 249 - init_stock: 182 - max_stock: 1000 - price: 435 - sale_gamma: 91 - service_level: 0.95 - SKU809: - constraint: null - cost: 55 - init_stock: 390 - max_stock: 1000 - price: 69 - sale_gamma: 65 - service_level: 0.95 - SKU81: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 528 - max_stock: 1000 - price: 223 - sale_gamma: 66 - service_level: 0.95 - SKU810: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 276 - init_stock: 42 - max_stock: 1000 - price: 322 - sale_gamma: 6 - service_level: 0.95 - SKU811: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 254 - init_stock: 148 - max_stock: 1000 - price: 322 - sale_gamma: 37 - service_level: 0.95 - SKU812: - constraint: null - cost: 252 - init_stock: 320 - max_stock: 1000 - price: 337 - sale_gamma: 80 - service_level: 0.95 - SKU813: - constraint: G(stock_constraint) - cost: 253 - init_stock: 49 - max_stock: 1000 - price: 333 - sale_gamma: 7 - service_level: 0.95 - SKU814: - constraint: null - cost: 485 - init_stock: 194 - max_stock: 1000 - price: 921 - sale_gamma: 97 - service_level: 0.95 - SKU815: - constraint: null - cost: 390 - init_stock: 168 - max_stock: 1000 - price: 429 - sale_gamma: 24 - service_level: 0.95 - SKU816: - constraint: null - cost: 152 - init_stock: 455 - max_stock: 1000 - price: 174 - sale_gamma: 91 - service_level: 0.95 - SKU817: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 227 - init_stock: 25 - max_stock: 1000 - price: 401 - sale_gamma: 5 - service_level: 0.95 - SKU818: - constraint: null - cost: 354 - init_stock: 215 - max_stock: 1000 - price: 534 - sale_gamma: 43 - service_level: 0.95 - SKU819: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 302 - init_stock: 135 - max_stock: 1000 - price: 480 - sale_gamma: 27 - service_level: 0.95 - SKU82: - constraint: null - cost: 290 - init_stock: 140 - max_stock: 1000 - price: 469 - sale_gamma: 28 - service_level: 0.95 - SKU820: - constraint: null - cost: 264 - init_stock: 410 - max_stock: 1000 - price: 464 - sale_gamma: 82 - service_level: 0.95 - SKU821: - constraint: G(stock_constraint) - cost: 99 - init_stock: 207 - max_stock: 1000 - price: 151 - sale_gamma: 69 - service_level: 0.95 - SKU822: - constraint: null - cost: 136 - init_stock: 490 - max_stock: 1000 - price: 266 - sale_gamma: 70 - service_level: 0.95 - SKU823: - constraint: null - cost: 75 - init_stock: 84 - max_stock: 1000 - price: 116 - sale_gamma: 28 - service_level: 0.95 - SKU824: - constraint: G(low_profit -> low_stock_constraint) - cost: 170 - init_stock: 420 - max_stock: 1000 - price: 200 - sale_gamma: 70 - service_level: 0.95 - SKU825: - constraint: null - cost: 214 - init_stock: 45 - max_stock: 1000 - price: 359 - sale_gamma: 15 - service_level: 0.95 - SKU826: - constraint: G(stock_constraint) - cost: 386 - init_stock: 330 - max_stock: 1000 - price: 428 - sale_gamma: 55 - service_level: 0.95 - SKU827: - constraint: G(stock_constraint) - cost: 148 - init_stock: 448 - max_stock: 1000 - price: 208 - sale_gamma: 64 - service_level: 0.95 - SKU828: - constraint: null - cost: 400 - init_stock: 276 - max_stock: 1000 - price: 627 - sale_gamma: 69 - service_level: 0.95 - SKU829: - constraint: null - cost: 61 - init_stock: 370 - max_stock: 1000 - price: 83 - sale_gamma: 74 - service_level: 0.95 - SKU83: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 68 - max_stock: 1000 - price: 535 - sale_gamma: 17 - service_level: 0.95 - SKU830: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 167 - init_stock: 144 - max_stock: 1000 - price: 252 - sale_gamma: 24 - service_level: 0.95 - SKU831: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 116 - max_stock: 1000 - price: 353 - sale_gamma: 29 - service_level: 0.95 - SKU832: - constraint: null - cost: 33 - init_stock: 164 - max_stock: 1000 - price: 48 - sale_gamma: 82 - service_level: 0.95 - SKU833: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 392 - max_stock: 1000 - price: 756 - sale_gamma: 98 - service_level: 0.95 - SKU834: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 10 - max_stock: 1000 - price: 662 - sale_gamma: 5 - service_level: 0.95 - SKU835: - constraint: null - cost: 440 - init_stock: 156 - max_stock: 1000 - price: 532 - sale_gamma: 52 - service_level: 0.95 - SKU836: - constraint: G(stock_constraint) - cost: 323 - init_stock: 192 - max_stock: 1000 - price: 552 - sale_gamma: 96 - service_level: 0.95 - SKU837: - constraint: null - cost: 373 - init_stock: 156 - max_stock: 1000 - price: 607 - sale_gamma: 26 - service_level: 0.95 - SKU838: - constraint: null - cost: 456 - init_stock: 308 - max_stock: 1000 - price: 711 - sale_gamma: 77 - service_level: 0.95 - SKU839: - constraint: null - cost: 473 - init_stock: 360 - max_stock: 1000 - price: 695 - sale_gamma: 60 - service_level: 0.95 - SKU84: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 318 - init_stock: 294 - max_stock: 1000 - price: 594 - sale_gamma: 42 - service_level: 0.95 - SKU840: - constraint: G(low_profit -> low_stock_constraint) - cost: 266 - init_stock: 150 - max_stock: 1000 - price: 436 - sale_gamma: 50 - service_level: 0.95 - SKU841: - constraint: G(stock_constraint) - cost: 285 - init_stock: 595 - max_stock: 1000 - price: 538 - sale_gamma: 85 - service_level: 0.95 - SKU842: - constraint: G(low_profit -> low_stock_constraint) - cost: 41 - init_stock: 252 - max_stock: 1000 - price: 70 - sale_gamma: 84 - service_level: 0.95 - SKU843: - constraint: null - cost: 360 - init_stock: 432 - max_stock: 1000 - price: 694 - sale_gamma: 72 - service_level: 0.95 - SKU844: - constraint: G(low_profit -> low_stock_constraint) - cost: 51 - init_stock: 474 - max_stock: 1000 - price: 63 - sale_gamma: 79 - service_level: 0.95 - SKU845: - constraint: G(low_profit -> low_stock_constraint) - cost: 288 - init_stock: 176 - max_stock: 1000 - price: 558 - sale_gamma: 22 - service_level: 0.95 - SKU846: - constraint: null - cost: 485 - init_stock: 234 - max_stock: 1000 - price: 940 - sale_gamma: 39 - service_level: 0.95 - SKU847: - constraint: G(low_profit -> low_stock_constraint) - cost: 388 - init_stock: 546 - max_stock: 1000 - price: 519 - sale_gamma: 91 - service_level: 0.95 - SKU848: - constraint: null - cost: 306 - init_stock: 184 - max_stock: 1000 - price: 358 - sale_gamma: 46 - service_level: 0.95 - SKU849: - constraint: G(low_profit -> low_stock_constraint) - cost: 320 - init_stock: 160 - max_stock: 1000 - price: 425 - sale_gamma: 40 - service_level: 0.95 - SKU85: - constraint: G(low_profit -> low_stock_constraint) - cost: 442 - init_stock: 400 - max_stock: 1000 - price: 583 - sale_gamma: 100 - service_level: 0.95 - SKU850: - constraint: G(stock_constraint) - cost: 183 - init_stock: 342 - max_stock: 1000 - price: 206 - sale_gamma: 57 - service_level: 0.95 - SKU851: - constraint: null - cost: 53 - init_stock: 384 - max_stock: 1000 - price: 96 - sale_gamma: 64 - service_level: 0.95 - SKU852: - constraint: null - cost: 306 - init_stock: 162 - max_stock: 1000 - price: 483 - sale_gamma: 54 - service_level: 0.95 - SKU853: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 386 - init_stock: 280 - max_stock: 1000 - price: 443 - sale_gamma: 56 - service_level: 0.95 - SKU854: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 212 - init_stock: 390 - max_stock: 1000 - price: 290 - sale_gamma: 65 - service_level: 0.95 - SKU855: - constraint: null - cost: 76 - init_stock: 432 - max_stock: 1000 - price: 114 - sale_gamma: 72 - service_level: 0.95 - SKU856: - constraint: G(low_profit -> low_stock_constraint) - cost: 380 - init_stock: 224 - max_stock: 1000 - price: 627 - sale_gamma: 32 - service_level: 0.95 - SKU857: - constraint: G(stock_constraint) - cost: 462 - init_stock: 500 - max_stock: 1000 - price: 646 - sale_gamma: 100 - service_level: 0.95 - SKU858: - constraint: null - cost: 80 - init_stock: 120 - max_stock: 1000 - price: 158 - sale_gamma: 24 - service_level: 0.95 - SKU859: - constraint: G(low_profit -> low_stock_constraint) - cost: 215 - init_stock: 224 - max_stock: 1000 - price: 298 - sale_gamma: 28 - service_level: 0.95 - SKU86: - constraint: null - cost: 386 - init_stock: 595 - max_stock: 1000 - price: 447 - sale_gamma: 85 - service_level: 0.95 - SKU860: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 105 - max_stock: 1000 - price: 435 - sale_gamma: 15 - service_level: 0.95 - SKU861: - constraint: null - cost: 477 - init_stock: 52 - max_stock: 1000 - price: 944 - sale_gamma: 13 - service_level: 0.95 - SKU862: - constraint: null - cost: 240 - init_stock: 64 - max_stock: 1000 - price: 412 - sale_gamma: 16 - service_level: 0.95 - SKU863: - constraint: null - cost: 470 - init_stock: 372 - max_stock: 1000 - price: 911 - sale_gamma: 93 - service_level: 0.95 - SKU864: - constraint: null - cost: 203 - init_stock: 114 - max_stock: 1000 - price: 341 - sale_gamma: 19 - service_level: 0.95 - SKU865: - constraint: G(low_profit -> low_stock_constraint) - cost: 144 - init_stock: 152 - max_stock: 1000 - price: 253 - sale_gamma: 19 - service_level: 0.95 - SKU866: - constraint: null - cost: 172 - init_stock: 264 - max_stock: 1000 - price: 201 - sale_gamma: 88 - service_level: 0.95 - SKU867: - constraint: G(low_profit -> low_stock_constraint) - cost: 499 - init_stock: 500 - max_stock: 1000 - price: 698 - sale_gamma: 100 - service_level: 0.95 - SKU868: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 41 - init_stock: 150 - max_stock: 1000 - price: 56 - sale_gamma: 50 - service_level: 0.95 - SKU869: - constraint: null - cost: 97 - init_stock: 388 - max_stock: 1000 - price: 111 - sale_gamma: 97 - service_level: 0.95 - SKU87: - constraint: null - cost: 368 - init_stock: 207 - max_stock: 1000 - price: 563 - sale_gamma: 69 - service_level: 0.95 - SKU870: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 281 - init_stock: 335 - max_stock: 1000 - price: 446 - sale_gamma: 67 - service_level: 0.95 - SKU871: - constraint: null - cost: 101 - init_stock: 396 - max_stock: 1000 - price: 112 - sale_gamma: 99 - service_level: 0.95 - SKU872: - constraint: null - cost: 133 - init_stock: 160 - max_stock: 1000 - price: 195 - sale_gamma: 32 - service_level: 0.95 - SKU873: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 423 - init_stock: 155 - max_stock: 1000 - price: 829 - sale_gamma: 31 - service_level: 0.95 - SKU874: - constraint: G(low_profit -> low_stock_constraint) - cost: 469 - init_stock: 360 - max_stock: 1000 - price: 689 - sale_gamma: 60 - service_level: 0.95 - SKU875: - constraint: G(stock_constraint) - cost: 51 - init_stock: 138 - max_stock: 1000 - price: 57 - sale_gamma: 23 - service_level: 0.95 - SKU876: - constraint: null - cost: 303 - init_stock: 427 - max_stock: 1000 - price: 427 - sale_gamma: 61 - service_level: 0.95 - SKU877: - constraint: null - cost: 40 - init_stock: 245 - max_stock: 1000 - price: 70 - sale_gamma: 35 - service_level: 0.95 - SKU878: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 476 - max_stock: 1000 - price: 32 - sale_gamma: 68 - service_level: 0.95 - SKU879: - constraint: null - cost: 150 - init_stock: 300 - max_stock: 1000 - price: 198 - sale_gamma: 100 - service_level: 0.95 - SKU88: - constraint: G(low_profit -> low_stock_constraint) - cost: 471 - init_stock: 36 - max_stock: 1000 - price: 824 - sale_gamma: 12 - service_level: 0.95 - SKU880: - constraint: null - cost: 433 - init_stock: 155 - max_stock: 1000 - price: 532 - sale_gamma: 31 - service_level: 0.95 - SKU881: - constraint: null - cost: 431 - init_stock: 420 - max_stock: 1000 - price: 560 - sale_gamma: 70 - service_level: 0.95 - SKU882: - constraint: G(stock_constraint) - cost: 194 - init_stock: 80 - max_stock: 1000 - price: 314 - sale_gamma: 16 - service_level: 0.95 - SKU883: - constraint: null - cost: 284 - init_stock: 152 - max_stock: 1000 - price: 479 - sale_gamma: 38 - service_level: 0.95 - SKU884: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 139 - init_stock: 195 - max_stock: 1000 - price: 222 - sale_gamma: 39 - service_level: 0.95 - SKU885: - constraint: G(low_profit -> low_stock_constraint) - cost: 49 - init_stock: 189 - max_stock: 1000 - price: 58 - sale_gamma: 27 - service_level: 0.95 - SKU886: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 584 - max_stock: 1000 - price: 602 - sale_gamma: 73 - service_level: 0.95 - SKU887: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 266 - init_stock: 174 - max_stock: 1000 - price: 494 - sale_gamma: 87 - service_level: 0.95 - SKU888: - constraint: G(low_profit -> low_stock_constraint) - cost: 143 - init_stock: 45 - max_stock: 1000 - price: 220 - sale_gamma: 9 - service_level: 0.95 - SKU889: - constraint: null - cost: 101 - init_stock: 147 - max_stock: 1000 - price: 199 - sale_gamma: 21 - service_level: 0.95 - SKU89: - constraint: null - cost: 138 - init_stock: 744 - max_stock: 1000 - price: 269 - sale_gamma: 93 - service_level: 0.95 - SKU890: - constraint: G(low_profit -> low_stock_constraint) - cost: 161 - init_stock: 600 - max_stock: 1000 - price: 247 - sale_gamma: 100 - service_level: 0.95 - SKU891: - constraint: null - cost: 356 - init_stock: 176 - max_stock: 1000 - price: 615 - sale_gamma: 22 - service_level: 0.95 - SKU892: - constraint: null - cost: 313 - init_stock: 552 - max_stock: 1000 - price: 516 - sale_gamma: 92 - service_level: 0.95 - SKU893: - constraint: null - cost: 229 - init_stock: 594 - max_stock: 1000 - price: 302 - sale_gamma: 99 - service_level: 0.95 - SKU894: - constraint: null - cost: 129 - init_stock: 222 - max_stock: 1000 - price: 176 - sale_gamma: 74 - service_level: 0.95 - SKU895: - constraint: null - cost: 230 - init_stock: 39 - max_stock: 1000 - price: 301 - sale_gamma: 13 - service_level: 0.95 - SKU896: - constraint: null - cost: 289 - init_stock: 400 - max_stock: 1000 - price: 465 - sale_gamma: 80 - service_level: 0.95 - SKU897: - constraint: null - cost: 393 - init_stock: 432 - max_stock: 1000 - price: 640 - sale_gamma: 72 - service_level: 0.95 - SKU898: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 477 - init_stock: 44 - max_stock: 1000 - price: 615 - sale_gamma: 11 - service_level: 0.95 - SKU899: - constraint: null - cost: 233 - init_stock: 240 - max_stock: 1000 - price: 309 - sale_gamma: 48 - service_level: 0.95 - SKU9: - constraint: null - cost: 166 - init_stock: 384 - max_stock: 1000 - price: 247 - sale_gamma: 96 - service_level: 0.95 - SKU90: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 454 - init_stock: 204 - max_stock: 1000 - price: 726 - sale_gamma: 51 - service_level: 0.95 - SKU900: - constraint: null - cost: 158 - init_stock: 165 - max_stock: 1000 - price: 263 - sale_gamma: 55 - service_level: 0.95 - SKU901: - constraint: G(stock_constraint) - cost: 215 - init_stock: 203 - max_stock: 1000 - price: 320 - sale_gamma: 29 - service_level: 0.95 - SKU902: - constraint: G(low_profit -> low_stock_constraint) - cost: 125 - init_stock: 560 - max_stock: 1000 - price: 205 - sale_gamma: 80 - service_level: 0.95 - SKU903: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 357 - init_stock: 266 - max_stock: 1000 - price: 517 - sale_gamma: 38 - service_level: 0.95 - SKU904: - constraint: G(low_profit -> low_stock_constraint) - cost: 496 - init_stock: 255 - max_stock: 1000 - price: 605 - sale_gamma: 51 - service_level: 0.95 - SKU905: - constraint: null - cost: 249 - init_stock: 217 - max_stock: 1000 - price: 383 - sale_gamma: 31 - service_level: 0.95 - SKU906: - constraint: G(low_profit -> low_stock_constraint) - cost: 166 - init_stock: 156 - max_stock: 1000 - price: 273 - sale_gamma: 52 - service_level: 0.95 - SKU907: - constraint: null - cost: 22 - init_stock: 498 - max_stock: 1000 - price: 33 - sale_gamma: 83 - service_level: 0.95 - SKU908: - constraint: null - cost: 408 - init_stock: 546 - max_stock: 1000 - price: 595 - sale_gamma: 78 - service_level: 0.95 - SKU909: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 480 - max_stock: 1000 - price: 703 - sale_gamma: 96 - service_level: 0.95 - SKU91: - constraint: G(low_profit -> low_stock_constraint) - cost: 303 - init_stock: 48 - max_stock: 1000 - price: 463 - sale_gamma: 16 - service_level: 0.95 - SKU910: - constraint: null - cost: 226 - init_stock: 132 - max_stock: 1000 - price: 350 - sale_gamma: 33 - service_level: 0.95 - SKU911: - constraint: null - cost: 461 - init_stock: 210 - max_stock: 1000 - price: 686 - sale_gamma: 70 - service_level: 0.95 - SKU912: - constraint: null - cost: 236 - init_stock: 135 - max_stock: 1000 - price: 328 - sale_gamma: 27 - service_level: 0.95 - SKU913: - constraint: null - cost: 322 - init_stock: 184 - max_stock: 1000 - price: 518 - sale_gamma: 46 - service_level: 0.95 - SKU914: - constraint: null - cost: 272 - init_stock: 434 - max_stock: 1000 - price: 446 - sale_gamma: 62 - service_level: 0.95 - SKU915: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 337 - init_stock: 280 - max_stock: 1000 - price: 545 - sale_gamma: 56 - service_level: 0.95 - SKU916: - constraint: null - cost: 337 - init_stock: 534 - max_stock: 1000 - price: 647 - sale_gamma: 89 - service_level: 0.95 - SKU917: - constraint: G(low_profit -> low_stock_constraint) - cost: 258 - init_stock: 120 - max_stock: 1000 - price: 366 - sale_gamma: 30 - service_level: 0.95 - SKU918: - constraint: G(stock_constraint) - cost: 148 - init_stock: 330 - max_stock: 1000 - price: 224 - sale_gamma: 55 - service_level: 0.95 - SKU919: - constraint: G(stock_constraint) - cost: 393 - init_stock: 125 - max_stock: 1000 - price: 711 - sale_gamma: 25 - service_level: 0.95 - SKU92: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 252 - max_stock: 1000 - price: 466 - sale_gamma: 63 - service_level: 0.95 - SKU920: - constraint: null - cost: 357 - init_stock: 344 - max_stock: 1000 - price: 696 - sale_gamma: 86 - service_level: 0.95 - SKU921: - constraint: G(low_profit -> low_stock_constraint) - cost: 227 - init_stock: 198 - max_stock: 1000 - price: 419 - sale_gamma: 66 - service_level: 0.95 - SKU922: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 201 - max_stock: 1000 - price: 123 - sale_gamma: 67 - service_level: 0.95 - SKU923: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 348 - max_stock: 1000 - price: 828 - sale_gamma: 58 - service_level: 0.95 - SKU924: - constraint: null - cost: 316 - init_stock: 425 - max_stock: 1000 - price: 423 - sale_gamma: 85 - service_level: 0.95 - SKU925: - constraint: null - cost: 360 - init_stock: 90 - max_stock: 1000 - price: 716 - sale_gamma: 15 - service_level: 0.95 - SKU926: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 360 - init_stock: 469 - max_stock: 1000 - price: 475 - sale_gamma: 67 - service_level: 0.95 - SKU927: - constraint: G(stock_constraint) - cost: 260 - init_stock: 147 - max_stock: 1000 - price: 439 - sale_gamma: 21 - service_level: 0.95 - SKU928: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 491 - init_stock: 415 - max_stock: 1000 - price: 869 - sale_gamma: 83 - service_level: 0.95 - SKU929: - constraint: null - cost: 359 - init_stock: 800 - max_stock: 1000 - price: 470 - sale_gamma: 100 - service_level: 0.95 - SKU93: - constraint: null - cost: 404 - init_stock: 268 - max_stock: 1000 - price: 557 - sale_gamma: 67 - service_level: 0.95 - SKU930: - constraint: G(low_profit -> low_stock_constraint) - cost: 198 - init_stock: 196 - max_stock: 1000 - price: 247 - sale_gamma: 28 - service_level: 0.95 - SKU931: - constraint: null - cost: 71 - init_stock: 56 - max_stock: 1000 - price: 101 - sale_gamma: 14 - service_level: 0.95 - SKU932: - constraint: null - cost: 163 - init_stock: 264 - max_stock: 1000 - price: 283 - sale_gamma: 66 - service_level: 0.95 - SKU933: - constraint: null - cost: 113 - init_stock: 156 - max_stock: 1000 - price: 179 - sale_gamma: 78 - service_level: 0.95 - SKU934: - constraint: G(stock_constraint) - cost: 219 - init_stock: 469 - max_stock: 1000 - price: 346 - sale_gamma: 67 - service_level: 0.95 - SKU935: - constraint: null - cost: 364 - init_stock: 376 - max_stock: 1000 - price: 527 - sale_gamma: 94 - service_level: 0.95 - SKU936: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 24 - init_stock: 20 - max_stock: 1000 - price: 41 - sale_gamma: 5 - service_level: 0.95 - SKU937: - constraint: G(stock_constraint) - cost: 135 - init_stock: 85 - max_stock: 1000 - price: 216 - sale_gamma: 17 - service_level: 0.95 - SKU938: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 63 - max_stock: 1000 - price: 704 - sale_gamma: 21 - service_level: 0.95 - SKU939: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 413 - max_stock: 1000 - price: 219 - sale_gamma: 59 - service_level: 0.95 - SKU94: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 235 - max_stock: 1000 - price: 261 - sale_gamma: 47 - service_level: 0.95 - SKU940: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 14 - init_stock: 279 - max_stock: 1000 - price: 24 - sale_gamma: 93 - service_level: 0.95 - SKU941: - constraint: G(stock_constraint) - cost: 80 - init_stock: 456 - max_stock: 1000 - price: 132 - sale_gamma: 57 - service_level: 0.95 - SKU942: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 202 - init_stock: 65 - max_stock: 1000 - price: 303 - sale_gamma: 13 - service_level: 0.95 - SKU943: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 182 - sale_gamma: 49 - service_level: 0.95 - SKU944: - constraint: null - cost: 196 - init_stock: 132 - max_stock: 1000 - price: 356 - sale_gamma: 44 - service_level: 0.95 - SKU945: - constraint: null - cost: 141 - init_stock: 85 - max_stock: 1000 - price: 176 - sale_gamma: 17 - service_level: 0.95 - SKU946: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 555 - sale_gamma: 49 - service_level: 0.95 - SKU947: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 338 - init_stock: 637 - max_stock: 1000 - price: 547 - sale_gamma: 91 - service_level: 0.95 - SKU948: - constraint: null - cost: 425 - init_stock: 112 - max_stock: 1000 - price: 476 - sale_gamma: 28 - service_level: 0.95 - SKU949: - constraint: G(stock_constraint) - cost: 309 - init_stock: 15 - max_stock: 1000 - price: 522 - sale_gamma: 5 - service_level: 0.95 - SKU95: - constraint: null - cost: 136 - init_stock: 84 - max_stock: 1000 - price: 214 - sale_gamma: 21 - service_level: 0.95 - SKU950: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 102 - init_stock: 324 - max_stock: 1000 - price: 128 - sale_gamma: 54 - service_level: 0.95 - SKU951: - constraint: G(low_profit -> low_stock_constraint) - cost: 75 - init_stock: 90 - max_stock: 1000 - price: 117 - sale_gamma: 18 - service_level: 0.95 - SKU952: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 156 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU953: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 230 - sale_gamma: 35 - service_level: 0.95 - SKU954: - constraint: null - cost: 296 - init_stock: 430 - max_stock: 1000 - price: 444 - sale_gamma: 86 - service_level: 0.95 - SKU955: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 55 - init_stock: 189 - max_stock: 1000 - price: 85 - sale_gamma: 63 - service_level: 0.95 - SKU956: - constraint: null - cost: 282 - init_stock: 425 - max_stock: 1000 - price: 397 - sale_gamma: 85 - service_level: 0.95 - SKU957: - constraint: null - cost: 305 - init_stock: 306 - max_stock: 1000 - price: 466 - sale_gamma: 51 - service_level: 0.95 - SKU958: - constraint: null - cost: 369 - init_stock: 462 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU959: - constraint: null - cost: 81 - init_stock: 164 - max_stock: 1000 - price: 127 - sale_gamma: 82 - service_level: 0.95 - SKU96: - constraint: G(low_profit -> low_stock_constraint) - cost: 176 - init_stock: 264 - max_stock: 1000 - price: 329 - sale_gamma: 44 - service_level: 0.95 - SKU960: - constraint: null - cost: 147 - init_stock: 42 - max_stock: 1000 - price: 235 - sale_gamma: 6 - service_level: 0.95 - SKU961: - constraint: null - cost: 264 - init_stock: 176 - max_stock: 1000 - price: 290 - sale_gamma: 44 - service_level: 0.95 - SKU962: - constraint: null - cost: 354 - init_stock: 176 - max_stock: 1000 - price: 392 - sale_gamma: 44 - service_level: 0.95 - SKU963: - constraint: null - cost: 349 - init_stock: 63 - max_stock: 1000 - price: 523 - sale_gamma: 21 - service_level: 0.95 - SKU964: - constraint: null - cost: 244 - init_stock: 219 - max_stock: 1000 - price: 480 - sale_gamma: 73 - service_level: 0.95 - SKU965: - constraint: G(stock_constraint) - cost: 124 - init_stock: 255 - max_stock: 1000 - price: 199 - sale_gamma: 51 - service_level: 0.95 - SKU966: - constraint: G(stock_constraint) - cost: 302 - init_stock: 308 - max_stock: 1000 - price: 474 - sale_gamma: 44 - service_level: 0.95 - SKU967: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 270 - max_stock: 1000 - price: 111 - sale_gamma: 45 - service_level: 0.95 - SKU968: - constraint: G(stock_constraint) - cost: 281 - init_stock: 294 - max_stock: 1000 - price: 528 - sale_gamma: 49 - service_level: 0.95 - SKU969: - constraint: G(low_profit -> low_stock_constraint) - cost: 249 - init_stock: 24 - max_stock: 1000 - price: 328 - sale_gamma: 6 - service_level: 0.95 - SKU97: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 28 - init_stock: 180 - max_stock: 1000 - price: 43 - sale_gamma: 30 - service_level: 0.95 - SKU970: - constraint: null - cost: 244 - init_stock: 30 - max_stock: 1000 - price: 373 - sale_gamma: 5 - service_level: 0.95 - SKU971: - constraint: null - cost: 368 - init_stock: 219 - max_stock: 1000 - price: 426 - sale_gamma: 73 - service_level: 0.95 - SKU972: - constraint: G(stock_constraint) - cost: 209 - init_stock: 345 - max_stock: 1000 - price: 307 - sale_gamma: 69 - service_level: 0.95 - SKU973: - constraint: null - cost: 271 - init_stock: 33 - max_stock: 1000 - price: 447 - sale_gamma: 11 - service_level: 0.95 - SKU974: - constraint: null - cost: 170 - init_stock: 192 - max_stock: 1000 - price: 285 - sale_gamma: 32 - service_level: 0.95 - SKU975: - constraint: G(stock_constraint) - cost: 198 - init_stock: 88 - max_stock: 1000 - price: 334 - sale_gamma: 22 - service_level: 0.95 - SKU976: - constraint: G(stock_constraint) - cost: 178 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU977: - constraint: G(stock_constraint) - cost: 234 - init_stock: 324 - max_stock: 1000 - price: 269 - sale_gamma: 54 - service_level: 0.95 - SKU978: - constraint: G(stock_constraint) - cost: 470 - init_stock: 320 - max_stock: 1000 - price: 921 - sale_gamma: 64 - service_level: 0.95 - SKU979: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 443 - init_stock: 480 - max_stock: 1000 - price: 753 - sale_gamma: 96 - service_level: 0.95 - SKU98: - constraint: G(stock_constraint) - cost: 443 - init_stock: 70 - max_stock: 1000 - price: 527 - sale_gamma: 35 - service_level: 0.95 - SKU980: - constraint: null - cost: 39 - init_stock: 340 - max_stock: 1000 - price: 60 - sale_gamma: 68 - service_level: 0.95 - SKU981: - constraint: null - cost: 482 - init_stock: 360 - max_stock: 1000 - price: 607 - sale_gamma: 72 - service_level: 0.95 - SKU982: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 213 - init_stock: 208 - max_stock: 1000 - price: 374 - sale_gamma: 52 - service_level: 0.95 - SKU983: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 276 - max_stock: 1000 - price: 776 - sale_gamma: 92 - service_level: 0.95 - SKU984: - constraint: G(stock_constraint) - cost: 232 - init_stock: 637 - max_stock: 1000 - price: 327 - sale_gamma: 91 - service_level: 0.95 - SKU985: - constraint: null - cost: 290 - init_stock: 153 - max_stock: 1000 - price: 420 - sale_gamma: 51 - service_level: 0.95 - SKU986: - constraint: null - cost: 275 - init_stock: 136 - max_stock: 1000 - price: 313 - sale_gamma: 17 - service_level: 0.95 - SKU987: - constraint: null - cost: 434 - init_stock: 204 - max_stock: 1000 - price: 516 - sale_gamma: 34 - service_level: 0.95 - SKU988: - constraint: null - cost: 102 - init_stock: 69 - max_stock: 1000 - price: 166 - sale_gamma: 23 - service_level: 0.95 - SKU989: - constraint: null - cost: 484 - init_stock: 558 - max_stock: 1000 - price: 653 - sale_gamma: 93 - service_level: 0.95 - SKU99: - constraint: null - cost: 361 - init_stock: 225 - max_stock: 1000 - price: 581 - sale_gamma: 45 - service_level: 0.95 - SKU990: - constraint: G(low_profit -> low_stock_constraint) - cost: 108 - init_stock: 228 - max_stock: 1000 - price: 174 - sale_gamma: 57 - service_level: 0.95 - SKU991: - constraint: null - cost: 409 - init_stock: 152 - max_stock: 1000 - price: 576 - sale_gamma: 19 - service_level: 0.95 - SKU992: - constraint: null - cost: 434 - init_stock: 651 - max_stock: 1000 - price: 685 - sale_gamma: 93 - service_level: 0.95 - SKU993: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 602 - max_stock: 1000 - price: 201 - sale_gamma: 86 - service_level: 0.95 - SKU994: - constraint: G(low_profit -> low_stock_constraint) - cost: 470 - init_stock: 300 - max_stock: 1000 - price: 916 - sale_gamma: 50 - service_level: 0.95 - SKU995: - constraint: null - cost: 241 - init_stock: 532 - max_stock: 1000 - price: 310 - sale_gamma: 76 - service_level: 0.95 - SKU996: - constraint: G(stock_constraint) - cost: 260 - init_stock: 438 - max_stock: 1000 - price: 416 - sale_gamma: 73 - service_level: 0.95 - SKU997: - constraint: G(stock_constraint) - cost: 400 - init_stock: 102 - max_stock: 1000 - price: 727 - sale_gamma: 34 - service_level: 0.95 - SKU998: - constraint: G(low_profit -> low_stock_constraint) - cost: 447 - init_stock: 165 - max_stock: 1000 - price: 688 - sale_gamma: 55 - service_level: 0.95 - SKU999: - constraint: null - cost: 79 - init_stock: 161 - max_stock: 1000 - price: 86 - sale_gamma: 23 - service_level: 0.95 - grid: - facilities: - STORE0: - - 9 - - 8 - SUPPLIER0: - - 14 - - 16 - WAREHOUSE0: - - 16 - - 18 - size: - - 20 - - 20 - skus: - - id: 0 - name: SKU0 - - id: 1 - name: SKU1 - - id: 2 - name: SKU2 - - id: 3 - name: SKU3 - - id: 4 - name: SKU4 - - id: 5 - name: SKU5 - - id: 6 - name: SKU6 - - id: 7 - name: SKU7 - - id: 8 - name: SKU8 - - id: 9 - name: SKU9 - - id: 10 - name: SKU10 - - id: 11 - name: SKU11 - - id: 12 - name: SKU12 - - id: 13 - name: SKU13 - - id: 14 - name: SKU14 - - id: 15 - name: SKU15 - - id: 16 - name: SKU16 - - id: 17 - name: SKU17 - - id: 18 - name: SKU18 - - id: 19 - name: SKU19 - - id: 20 - name: SKU20 - - id: 21 - name: SKU21 - - id: 22 - name: SKU22 - - id: 23 - name: SKU23 - - id: 24 - name: SKU24 - - id: 25 - name: SKU25 - - id: 26 - name: SKU26 - - id: 27 - name: SKU27 - - id: 28 - name: SKU28 - - id: 29 - name: SKU29 - - id: 30 - name: SKU30 - - id: 31 - name: SKU31 - - id: 32 - name: SKU32 - - id: 33 - name: SKU33 - - id: 34 - name: SKU34 - - id: 35 - name: SKU35 - - id: 36 - name: SKU36 - - id: 37 - name: SKU37 - - id: 38 - name: SKU38 - - id: 39 - name: SKU39 - - id: 40 - name: SKU40 - - id: 41 - name: SKU41 - - id: 42 - name: SKU42 - - id: 43 - name: SKU43 - - id: 44 - name: SKU44 - - id: 45 - name: SKU45 - - id: 46 - name: SKU46 - - id: 47 - name: SKU47 - - id: 48 - name: SKU48 - - id: 49 - name: SKU49 - - id: 50 - name: SKU50 - - id: 51 - name: SKU51 - - id: 52 - name: SKU52 - - id: 53 - name: SKU53 - - id: 54 - name: SKU54 - - id: 55 - name: SKU55 - - id: 56 - name: SKU56 - - id: 57 - name: SKU57 - - id: 58 - name: SKU58 - - id: 59 - name: SKU59 - - id: 60 - name: SKU60 - - id: 61 - name: SKU61 - - id: 62 - name: SKU62 - - id: 63 - name: SKU63 - - id: 64 - name: SKU64 - - id: 65 - name: SKU65 - - id: 66 - name: SKU66 - - id: 67 - name: SKU67 - - id: 68 - name: SKU68 - - id: 69 - name: SKU69 - - id: 70 - name: SKU70 - - id: 71 - name: SKU71 - - id: 72 - name: SKU72 - - id: 73 - name: SKU73 - - id: 74 - name: SKU74 - - id: 75 - name: SKU75 - - id: 76 - name: SKU76 - - id: 77 - name: SKU77 - - id: 78 - name: SKU78 - - id: 79 - name: SKU79 - - id: 80 - name: SKU80 - - id: 81 - name: SKU81 - - id: 82 - name: SKU82 - - id: 83 - name: SKU83 - - id: 84 - name: SKU84 - - id: 85 - name: SKU85 - - id: 86 - name: SKU86 - - id: 87 - name: SKU87 - - id: 88 - name: SKU88 - - id: 89 - name: SKU89 - - id: 90 - name: SKU90 - - id: 91 - name: SKU91 - - id: 92 - name: SKU92 - - id: 93 - name: SKU93 - - id: 94 - name: SKU94 - - id: 95 - name: SKU95 - - id: 96 - name: SKU96 - - id: 97 - name: SKU97 - - id: 98 - name: SKU98 - - id: 99 - name: SKU99 - - id: 100 - name: SKU100 - - id: 101 - name: SKU101 - - id: 102 - name: SKU102 - - id: 103 - name: SKU103 - - id: 104 - name: SKU104 - - id: 105 - name: SKU105 - - id: 106 - name: SKU106 - - id: 107 - name: SKU107 - - id: 108 - name: SKU108 - - id: 109 - name: SKU109 - - id: 110 - name: SKU110 - - id: 111 - name: SKU111 - - id: 112 - name: SKU112 - - id: 113 - name: SKU113 - - id: 114 - name: SKU114 - - id: 115 - name: SKU115 - - id: 116 - name: SKU116 - - id: 117 - name: SKU117 - - id: 118 - name: SKU118 - - id: 119 - name: SKU119 - - id: 120 - name: SKU120 - - id: 121 - name: SKU121 - - id: 122 - name: SKU122 - - id: 123 - name: SKU123 - - id: 124 - name: SKU124 - - id: 125 - name: SKU125 - - id: 126 - name: SKU126 - - id: 127 - name: SKU127 - - id: 128 - name: SKU128 - - id: 129 - name: SKU129 - - id: 130 - name: SKU130 - - id: 131 - name: SKU131 - - id: 132 - name: SKU132 - - id: 133 - name: SKU133 - - id: 134 - name: SKU134 - - id: 135 - name: SKU135 - - id: 136 - name: SKU136 - - id: 137 - name: SKU137 - - id: 138 - name: SKU138 - - id: 139 - name: SKU139 - - id: 140 - name: SKU140 - - id: 141 - name: SKU141 - - id: 142 - name: SKU142 - - id: 143 - name: SKU143 - - id: 144 - name: SKU144 - - id: 145 - name: SKU145 - - id: 146 - name: SKU146 - - id: 147 - name: SKU147 - - id: 148 - name: SKU148 - - id: 149 - name: SKU149 - - id: 150 - name: SKU150 - - id: 151 - name: SKU151 - - id: 152 - name: SKU152 - - id: 153 - name: SKU153 - - id: 154 - name: SKU154 - - id: 155 - name: SKU155 - - id: 156 - name: SKU156 - - id: 157 - name: SKU157 - - id: 158 - name: SKU158 - - id: 159 - name: SKU159 - - id: 160 - name: SKU160 - - id: 161 - name: SKU161 - - id: 162 - name: SKU162 - - id: 163 - name: SKU163 - - id: 164 - name: SKU164 - - id: 165 - name: SKU165 - - id: 166 - name: SKU166 - - id: 167 - name: SKU167 - - id: 168 - name: SKU168 - - id: 169 - name: SKU169 - - id: 170 - name: SKU170 - - id: 171 - name: SKU171 - - id: 172 - name: SKU172 - - id: 173 - name: SKU173 - - id: 174 - name: SKU174 - - id: 175 - name: SKU175 - - id: 176 - name: SKU176 - - id: 177 - name: SKU177 - - id: 178 - name: SKU178 - - id: 179 - name: SKU179 - - id: 180 - name: SKU180 - - id: 181 - name: SKU181 - - id: 182 - name: SKU182 - - id: 183 - name: SKU183 - - id: 184 - name: SKU184 - - id: 185 - name: SKU185 - - id: 186 - name: SKU186 - - id: 187 - name: SKU187 - - id: 188 - name: SKU188 - - id: 189 - name: SKU189 - - id: 190 - name: SKU190 - - id: 191 - name: SKU191 - - id: 192 - name: SKU192 - - id: 193 - name: SKU193 - - id: 194 - name: SKU194 - - id: 195 - name: SKU195 - - id: 196 - name: SKU196 - - id: 197 - name: SKU197 - - id: 198 - name: SKU198 - - id: 199 - name: SKU199 - - id: 200 - name: SKU200 - - id: 201 - name: SKU201 - - id: 202 - name: SKU202 - - id: 203 - name: SKU203 - - id: 204 - name: SKU204 - - id: 205 - name: SKU205 - - id: 206 - name: SKU206 - - id: 207 - name: SKU207 - - id: 208 - name: SKU208 - - id: 209 - name: SKU209 - - id: 210 - name: SKU210 - - id: 211 - name: SKU211 - - id: 212 - name: SKU212 - - id: 213 - name: SKU213 - - id: 214 - name: SKU214 - - id: 215 - name: SKU215 - - id: 216 - name: SKU216 - - id: 217 - name: SKU217 - - id: 218 - name: SKU218 - - id: 219 - name: SKU219 - - id: 220 - name: SKU220 - - id: 221 - name: SKU221 - - id: 222 - name: SKU222 - - id: 223 - name: SKU223 - - id: 224 - name: SKU224 - - id: 225 - name: SKU225 - - id: 226 - name: SKU226 - - id: 227 - name: SKU227 - - id: 228 - name: SKU228 - - id: 229 - name: SKU229 - - id: 230 - name: SKU230 - - id: 231 - name: SKU231 - - id: 232 - name: SKU232 - - id: 233 - name: SKU233 - - id: 234 - name: SKU234 - - id: 235 - name: SKU235 - - id: 236 - name: SKU236 - - id: 237 - name: SKU237 - - id: 238 - name: SKU238 - - id: 239 - name: SKU239 - - id: 240 - name: SKU240 - - id: 241 - name: SKU241 - - id: 242 - name: SKU242 - - id: 243 - name: SKU243 - - id: 244 - name: SKU244 - - id: 245 - name: SKU245 - - id: 246 - name: SKU246 - - id: 247 - name: SKU247 - - id: 248 - name: SKU248 - - id: 249 - name: SKU249 - - id: 250 - name: SKU250 - - id: 251 - name: SKU251 - - id: 252 - name: SKU252 - - id: 253 - name: SKU253 - - id: 254 - name: SKU254 - - id: 255 - name: SKU255 - - id: 256 - name: SKU256 - - id: 257 - name: SKU257 - - id: 258 - name: SKU258 - - id: 259 - name: SKU259 - - id: 260 - name: SKU260 - - id: 261 - name: SKU261 - - id: 262 - name: SKU262 - - id: 263 - name: SKU263 - - id: 264 - name: SKU264 - - id: 265 - name: SKU265 - - id: 266 - name: SKU266 - - id: 267 - name: SKU267 - - id: 268 - name: SKU268 - - id: 269 - name: SKU269 - - id: 270 - name: SKU270 - - id: 271 - name: SKU271 - - id: 272 - name: SKU272 - - id: 273 - name: SKU273 - - id: 274 - name: SKU274 - - id: 275 - name: SKU275 - - id: 276 - name: SKU276 - - id: 277 - name: SKU277 - - id: 278 - name: SKU278 - - id: 279 - name: SKU279 - - id: 280 - name: SKU280 - - id: 281 - name: SKU281 - - id: 282 - name: SKU282 - - id: 283 - name: SKU283 - - id: 284 - name: SKU284 - - id: 285 - name: SKU285 - - id: 286 - name: SKU286 - - id: 287 - name: SKU287 - - id: 288 - name: SKU288 - - id: 289 - name: SKU289 - - id: 290 - name: SKU290 - - id: 291 - name: SKU291 - - id: 292 - name: SKU292 - - id: 293 - name: SKU293 - - id: 294 - name: SKU294 - - id: 295 - name: SKU295 - - id: 296 - name: SKU296 - - id: 297 - name: SKU297 - - id: 298 - name: SKU298 - - id: 299 - name: SKU299 - - id: 300 - name: SKU300 - - id: 301 - name: SKU301 - - id: 302 - name: SKU302 - - id: 303 - name: SKU303 - - id: 304 - name: SKU304 - - id: 305 - name: SKU305 - - id: 306 - name: SKU306 - - id: 307 - name: SKU307 - - id: 308 - name: SKU308 - - id: 309 - name: SKU309 - - id: 310 - name: SKU310 - - id: 311 - name: SKU311 - - id: 312 - name: SKU312 - - id: 313 - name: SKU313 - - id: 314 - name: SKU314 - - id: 315 - name: SKU315 - - id: 316 - name: SKU316 - - id: 317 - name: SKU317 - - id: 318 - name: SKU318 - - id: 319 - name: SKU319 - - id: 320 - name: SKU320 - - id: 321 - name: SKU321 - - id: 322 - name: SKU322 - - id: 323 - name: SKU323 - - id: 324 - name: SKU324 - - id: 325 - name: SKU325 - - id: 326 - name: SKU326 - - id: 327 - name: SKU327 - - id: 328 - name: SKU328 - - id: 329 - name: SKU329 - - id: 330 - name: SKU330 - - id: 331 - name: SKU331 - - id: 332 - name: SKU332 - - id: 333 - name: SKU333 - - id: 334 - name: SKU334 - - id: 335 - name: SKU335 - - id: 336 - name: SKU336 - - id: 337 - name: SKU337 - - id: 338 - name: SKU338 - - id: 339 - name: SKU339 - - id: 340 - name: SKU340 - - id: 341 - name: SKU341 - - id: 342 - name: SKU342 - - id: 343 - name: SKU343 - - id: 344 - name: SKU344 - - id: 345 - name: SKU345 - - id: 346 - name: SKU346 - - id: 347 - name: SKU347 - - id: 348 - name: SKU348 - - id: 349 - name: SKU349 - - id: 350 - name: SKU350 - - id: 351 - name: SKU351 - - id: 352 - name: SKU352 - - id: 353 - name: SKU353 - - id: 354 - name: SKU354 - - id: 355 - name: SKU355 - - id: 356 - name: SKU356 - - id: 357 - name: SKU357 - - id: 358 - name: SKU358 - - id: 359 - name: SKU359 - - id: 360 - name: SKU360 - - id: 361 - name: SKU361 - - id: 362 - name: SKU362 - - id: 363 - name: SKU363 - - id: 364 - name: SKU364 - - id: 365 - name: SKU365 - - id: 366 - name: SKU366 - - id: 367 - name: SKU367 - - id: 368 - name: SKU368 - - id: 369 - name: SKU369 - - id: 370 - name: SKU370 - - id: 371 - name: SKU371 - - id: 372 - name: SKU372 - - id: 373 - name: SKU373 - - id: 374 - name: SKU374 - - id: 375 - name: SKU375 - - id: 376 - name: SKU376 - - id: 377 - name: SKU377 - - id: 378 - name: SKU378 - - id: 379 - name: SKU379 - - id: 380 - name: SKU380 - - id: 381 - name: SKU381 - - id: 382 - name: SKU382 - - id: 383 - name: SKU383 - - id: 384 - name: SKU384 - - id: 385 - name: SKU385 - - id: 386 - name: SKU386 - - id: 387 - name: SKU387 - - id: 388 - name: SKU388 - - id: 389 - name: SKU389 - - id: 390 - name: SKU390 - - id: 391 - name: SKU391 - - id: 392 - name: SKU392 - - id: 393 - name: SKU393 - - id: 394 - name: SKU394 - - id: 395 - name: SKU395 - - id: 396 - name: SKU396 - - id: 397 - name: SKU397 - - id: 398 - name: SKU398 - - id: 399 - name: SKU399 - - id: 400 - name: SKU400 - - id: 401 - name: SKU401 - - id: 402 - name: SKU402 - - id: 403 - name: SKU403 - - id: 404 - name: SKU404 - - id: 405 - name: SKU405 - - id: 406 - name: SKU406 - - id: 407 - name: SKU407 - - id: 408 - name: SKU408 - - id: 409 - name: SKU409 - - id: 410 - name: SKU410 - - id: 411 - name: SKU411 - - id: 412 - name: SKU412 - - id: 413 - name: SKU413 - - id: 414 - name: SKU414 - - id: 415 - name: SKU415 - - id: 416 - name: SKU416 - - id: 417 - name: SKU417 - - id: 418 - name: SKU418 - - id: 419 - name: SKU419 - - id: 420 - name: SKU420 - - id: 421 - name: SKU421 - - id: 422 - name: SKU422 - - id: 423 - name: SKU423 - - id: 424 - name: SKU424 - - id: 425 - name: SKU425 - - id: 426 - name: SKU426 - - id: 427 - name: SKU427 - - id: 428 - name: SKU428 - - id: 429 - name: SKU429 - - id: 430 - name: SKU430 - - id: 431 - name: SKU431 - - id: 432 - name: SKU432 - - id: 433 - name: SKU433 - - id: 434 - name: SKU434 - - id: 435 - name: SKU435 - - id: 436 - name: SKU436 - - id: 437 - name: SKU437 - - id: 438 - name: SKU438 - - id: 439 - name: SKU439 - - id: 440 - name: SKU440 - - id: 441 - name: SKU441 - - id: 442 - name: SKU442 - - id: 443 - name: SKU443 - - id: 444 - name: SKU444 - - id: 445 - name: SKU445 - - id: 446 - name: SKU446 - - id: 447 - name: SKU447 - - id: 448 - name: SKU448 - - id: 449 - name: SKU449 - - id: 450 - name: SKU450 - - id: 451 - name: SKU451 - - id: 452 - name: SKU452 - - id: 453 - name: SKU453 - - id: 454 - name: SKU454 - - id: 455 - name: SKU455 - - id: 456 - name: SKU456 - - id: 457 - name: SKU457 - - id: 458 - name: SKU458 - - id: 459 - name: SKU459 - - id: 460 - name: SKU460 - - id: 461 - name: SKU461 - - id: 462 - name: SKU462 - - id: 463 - name: SKU463 - - id: 464 - name: SKU464 - - id: 465 - name: SKU465 - - id: 466 - name: SKU466 - - id: 467 - name: SKU467 - - id: 468 - name: SKU468 - - id: 469 - name: SKU469 - - id: 470 - name: SKU470 - - id: 471 - name: SKU471 - - id: 472 - name: SKU472 - - id: 473 - name: SKU473 - - id: 474 - name: SKU474 - - id: 475 - name: SKU475 - - id: 476 - name: SKU476 - - id: 477 - name: SKU477 - - id: 478 - name: SKU478 - - id: 479 - name: SKU479 - - id: 480 - name: SKU480 - - id: 481 - name: SKU481 - - id: 482 - name: SKU482 - - id: 483 - name: SKU483 - - id: 484 - name: SKU484 - - id: 485 - name: SKU485 - - id: 486 - name: SKU486 - - id: 487 - name: SKU487 - - id: 488 - name: SKU488 - - id: 489 - name: SKU489 - - id: 490 - name: SKU490 - - id: 491 - name: SKU491 - - id: 492 - name: SKU492 - - id: 493 - name: SKU493 - - id: 494 - name: SKU494 - - id: 495 - name: SKU495 - - id: 496 - name: SKU496 - - id: 497 - name: SKU497 - - id: 498 - name: SKU498 - - id: 499 - name: SKU499 - - id: 500 - name: SKU500 - - id: 501 - name: SKU501 - - id: 502 - name: SKU502 - - id: 503 - name: SKU503 - - id: 504 - name: SKU504 - - id: 505 - name: SKU505 - - id: 506 - name: SKU506 - - id: 507 - name: SKU507 - - id: 508 - name: SKU508 - - id: 509 - name: SKU509 - - id: 510 - name: SKU510 - - id: 511 - name: SKU511 - - id: 512 - name: SKU512 - - id: 513 - name: SKU513 - - id: 514 - name: SKU514 - - id: 515 - name: SKU515 - - id: 516 - name: SKU516 - - id: 517 - name: SKU517 - - id: 518 - name: SKU518 - - id: 519 - name: SKU519 - - id: 520 - name: SKU520 - - id: 521 - name: SKU521 - - id: 522 - name: SKU522 - - id: 523 - name: SKU523 - - id: 524 - name: SKU524 - - id: 525 - name: SKU525 - - id: 526 - name: SKU526 - - id: 527 - name: SKU527 - - id: 528 - name: SKU528 - - id: 529 - name: SKU529 - - id: 530 - name: SKU530 - - id: 531 - name: SKU531 - - id: 532 - name: SKU532 - - id: 533 - name: SKU533 - - id: 534 - name: SKU534 - - id: 535 - name: SKU535 - - id: 536 - name: SKU536 - - id: 537 - name: SKU537 - - id: 538 - name: SKU538 - - id: 539 - name: SKU539 - - id: 540 - name: SKU540 - - id: 541 - name: SKU541 - - id: 542 - name: SKU542 - - id: 543 - name: SKU543 - - id: 544 - name: SKU544 - - id: 545 - name: SKU545 - - id: 546 - name: SKU546 - - id: 547 - name: SKU547 - - id: 548 - name: SKU548 - - id: 549 - name: SKU549 - - id: 550 - name: SKU550 - - id: 551 - name: SKU551 - - id: 552 - name: SKU552 - - id: 553 - name: SKU553 - - id: 554 - name: SKU554 - - id: 555 - name: SKU555 - - id: 556 - name: SKU556 - - id: 557 - name: SKU557 - - id: 558 - name: SKU558 - - id: 559 - name: SKU559 - - id: 560 - name: SKU560 - - id: 561 - name: SKU561 - - id: 562 - name: SKU562 - - id: 563 - name: SKU563 - - id: 564 - name: SKU564 - - id: 565 - name: SKU565 - - id: 566 - name: SKU566 - - id: 567 - name: SKU567 - - id: 568 - name: SKU568 - - id: 569 - name: SKU569 - - id: 570 - name: SKU570 - - id: 571 - name: SKU571 - - id: 572 - name: SKU572 - - id: 573 - name: SKU573 - - id: 574 - name: SKU574 - - id: 575 - name: SKU575 - - id: 576 - name: SKU576 - - id: 577 - name: SKU577 - - id: 578 - name: SKU578 - - id: 579 - name: SKU579 - - id: 580 - name: SKU580 - - id: 581 - name: SKU581 - - id: 582 - name: SKU582 - - id: 583 - name: SKU583 - - id: 584 - name: SKU584 - - id: 585 - name: SKU585 - - id: 586 - name: SKU586 - - id: 587 - name: SKU587 - - id: 588 - name: SKU588 - - id: 589 - name: SKU589 - - id: 590 - name: SKU590 - - id: 591 - name: SKU591 - - id: 592 - name: SKU592 - - id: 593 - name: SKU593 - - id: 594 - name: SKU594 - - id: 595 - name: SKU595 - - id: 596 - name: SKU596 - - id: 597 - name: SKU597 - - id: 598 - name: SKU598 - - id: 599 - name: SKU599 - - id: 600 - name: SKU600 - - id: 601 - name: SKU601 - - id: 602 - name: SKU602 - - id: 603 - name: SKU603 - - id: 604 - name: SKU604 - - id: 605 - name: SKU605 - - id: 606 - name: SKU606 - - id: 607 - name: SKU607 - - id: 608 - name: SKU608 - - id: 609 - name: SKU609 - - id: 610 - name: SKU610 - - id: 611 - name: SKU611 - - id: 612 - name: SKU612 - - id: 613 - name: SKU613 - - id: 614 - name: SKU614 - - id: 615 - name: SKU615 - - id: 616 - name: SKU616 - - id: 617 - name: SKU617 - - id: 618 - name: SKU618 - - id: 619 - name: SKU619 - - id: 620 - name: SKU620 - - id: 621 - name: SKU621 - - id: 622 - name: SKU622 - - id: 623 - name: SKU623 - - id: 624 - name: SKU624 - - id: 625 - name: SKU625 - - id: 626 - name: SKU626 - - id: 627 - name: SKU627 - - id: 628 - name: SKU628 - - id: 629 - name: SKU629 - - id: 630 - name: SKU630 - - id: 631 - name: SKU631 - - id: 632 - name: SKU632 - - id: 633 - name: SKU633 - - id: 634 - name: SKU634 - - id: 635 - name: SKU635 - - id: 636 - name: SKU636 - - id: 637 - name: SKU637 - - id: 638 - name: SKU638 - - id: 639 - name: SKU639 - - id: 640 - name: SKU640 - - id: 641 - name: SKU641 - - id: 642 - name: SKU642 - - id: 643 - name: SKU643 - - id: 644 - name: SKU644 - - id: 645 - name: SKU645 - - id: 646 - name: SKU646 - - id: 647 - name: SKU647 - - id: 648 - name: SKU648 - - id: 649 - name: SKU649 - - id: 650 - name: SKU650 - - id: 651 - name: SKU651 - - id: 652 - name: SKU652 - - id: 653 - name: SKU653 - - id: 654 - name: SKU654 - - id: 655 - name: SKU655 - - id: 656 - name: SKU656 - - id: 657 - name: SKU657 - - id: 658 - name: SKU658 - - id: 659 - name: SKU659 - - id: 660 - name: SKU660 - - id: 661 - name: SKU661 - - id: 662 - name: SKU662 - - id: 663 - name: SKU663 - - id: 664 - name: SKU664 - - id: 665 - name: SKU665 - - id: 666 - name: SKU666 - - id: 667 - name: SKU667 - - id: 668 - name: SKU668 - - id: 669 - name: SKU669 - - id: 670 - name: SKU670 - - id: 671 - name: SKU671 - - id: 672 - name: SKU672 - - id: 673 - name: SKU673 - - id: 674 - name: SKU674 - - id: 675 - name: SKU675 - - id: 676 - name: SKU676 - - id: 677 - name: SKU677 - - id: 678 - name: SKU678 - - id: 679 - name: SKU679 - - id: 680 - name: SKU680 - - id: 681 - name: SKU681 - - id: 682 - name: SKU682 - - id: 683 - name: SKU683 - - id: 684 - name: SKU684 - - id: 685 - name: SKU685 - - id: 686 - name: SKU686 - - id: 687 - name: SKU687 - - id: 688 - name: SKU688 - - id: 689 - name: SKU689 - - id: 690 - name: SKU690 - - id: 691 - name: SKU691 - - id: 692 - name: SKU692 - - id: 693 - name: SKU693 - - id: 694 - name: SKU694 - - id: 695 - name: SKU695 - - id: 696 - name: SKU696 - - id: 697 - name: SKU697 - - id: 698 - name: SKU698 - - id: 699 - name: SKU699 - - id: 700 - name: SKU700 - - id: 701 - name: SKU701 - - id: 702 - name: SKU702 - - id: 703 - name: SKU703 - - id: 704 - name: SKU704 - - id: 705 - name: SKU705 - - id: 706 - name: SKU706 - - id: 707 - name: SKU707 - - id: 708 - name: SKU708 - - id: 709 - name: SKU709 - - id: 710 - name: SKU710 - - id: 711 - name: SKU711 - - id: 712 - name: SKU712 - - id: 713 - name: SKU713 - - id: 714 - name: SKU714 - - id: 715 - name: SKU715 - - id: 716 - name: SKU716 - - id: 717 - name: SKU717 - - id: 718 - name: SKU718 - - id: 719 - name: SKU719 - - id: 720 - name: SKU720 - - id: 721 - name: SKU721 - - id: 722 - name: SKU722 - - id: 723 - name: SKU723 - - id: 724 - name: SKU724 - - id: 725 - name: SKU725 - - id: 726 - name: SKU726 - - id: 727 - name: SKU727 - - id: 728 - name: SKU728 - - id: 729 - name: SKU729 - - id: 730 - name: SKU730 - - id: 731 - name: SKU731 - - id: 732 - name: SKU732 - - id: 733 - name: SKU733 - - id: 734 - name: SKU734 - - id: 735 - name: SKU735 - - id: 736 - name: SKU736 - - id: 737 - name: SKU737 - - id: 738 - name: SKU738 - - id: 739 - name: SKU739 - - id: 740 - name: SKU740 - - id: 741 - name: SKU741 - - id: 742 - name: SKU742 - - id: 743 - name: SKU743 - - id: 744 - name: SKU744 - - id: 745 - name: SKU745 - - id: 746 - name: SKU746 - - id: 747 - name: SKU747 - - id: 748 - name: SKU748 - - id: 749 - name: SKU749 - - id: 750 - name: SKU750 - - id: 751 - name: SKU751 - - id: 752 - name: SKU752 - - id: 753 - name: SKU753 - - id: 754 - name: SKU754 - - id: 755 - name: SKU755 - - id: 756 - name: SKU756 - - id: 757 - name: SKU757 - - id: 758 - name: SKU758 - - id: 759 - name: SKU759 - - id: 760 - name: SKU760 - - id: 761 - name: SKU761 - - id: 762 - name: SKU762 - - id: 763 - name: SKU763 - - id: 764 - name: SKU764 - - id: 765 - name: SKU765 - - id: 766 - name: SKU766 - - id: 767 - name: SKU767 - - id: 768 - name: SKU768 - - id: 769 - name: SKU769 - - id: 770 - name: SKU770 - - id: 771 - name: SKU771 - - id: 772 - name: SKU772 - - id: 773 - name: SKU773 - - id: 774 - name: SKU774 - - id: 775 - name: SKU775 - - id: 776 - name: SKU776 - - id: 777 - name: SKU777 - - id: 778 - name: SKU778 - - id: 779 - name: SKU779 - - id: 780 - name: SKU780 - - id: 781 - name: SKU781 - - id: 782 - name: SKU782 - - id: 783 - name: SKU783 - - id: 784 - name: SKU784 - - id: 785 - name: SKU785 - - id: 786 - name: SKU786 - - id: 787 - name: SKU787 - - id: 788 - name: SKU788 - - id: 789 - name: SKU789 - - id: 790 - name: SKU790 - - id: 791 - name: SKU791 - - id: 792 - name: SKU792 - - id: 793 - name: SKU793 - - id: 794 - name: SKU794 - - id: 795 - name: SKU795 - - id: 796 - name: SKU796 - - id: 797 - name: SKU797 - - id: 798 - name: SKU798 - - id: 799 - name: SKU799 - - id: 800 - name: SKU800 - - id: 801 - name: SKU801 - - id: 802 - name: SKU802 - - id: 803 - name: SKU803 - - id: 804 - name: SKU804 - - id: 805 - name: SKU805 - - id: 806 - name: SKU806 - - id: 807 - name: SKU807 - - id: 808 - name: SKU808 - - id: 809 - name: SKU809 - - id: 810 - name: SKU810 - - id: 811 - name: SKU811 - - id: 812 - name: SKU812 - - id: 813 - name: SKU813 - - id: 814 - name: SKU814 - - id: 815 - name: SKU815 - - id: 816 - name: SKU816 - - id: 817 - name: SKU817 - - id: 818 - name: SKU818 - - id: 819 - name: SKU819 - - id: 820 - name: SKU820 - - id: 821 - name: SKU821 - - id: 822 - name: SKU822 - - id: 823 - name: SKU823 - - id: 824 - name: SKU824 - - id: 825 - name: SKU825 - - id: 826 - name: SKU826 - - id: 827 - name: SKU827 - - id: 828 - name: SKU828 - - id: 829 - name: SKU829 - - id: 830 - name: SKU830 - - id: 831 - name: SKU831 - - id: 832 - name: SKU832 - - id: 833 - name: SKU833 - - id: 834 - name: SKU834 - - id: 835 - name: SKU835 - - id: 836 - name: SKU836 - - id: 837 - name: SKU837 - - id: 838 - name: SKU838 - - id: 839 - name: SKU839 - - id: 840 - name: SKU840 - - id: 841 - name: SKU841 - - id: 842 - name: SKU842 - - id: 843 - name: SKU843 - - id: 844 - name: SKU844 - - id: 845 - name: SKU845 - - id: 846 - name: SKU846 - - id: 847 - name: SKU847 - - id: 848 - name: SKU848 - - id: 849 - name: SKU849 - - id: 850 - name: SKU850 - - id: 851 - name: SKU851 - - id: 852 - name: SKU852 - - id: 853 - name: SKU853 - - id: 854 - name: SKU854 - - id: 855 - name: SKU855 - - id: 856 - name: SKU856 - - id: 857 - name: SKU857 - - id: 858 - name: SKU858 - - id: 859 - name: SKU859 - - id: 860 - name: SKU860 - - id: 861 - name: SKU861 - - id: 862 - name: SKU862 - - id: 863 - name: SKU863 - - id: 864 - name: SKU864 - - id: 865 - name: SKU865 - - id: 866 - name: SKU866 - - id: 867 - name: SKU867 - - id: 868 - name: SKU868 - - id: 869 - name: SKU869 - - id: 870 - name: SKU870 - - id: 871 - name: SKU871 - - id: 872 - name: SKU872 - - id: 873 - name: SKU873 - - id: 874 - name: SKU874 - - id: 875 - name: SKU875 - - id: 876 - name: SKU876 - - id: 877 - name: SKU877 - - id: 878 - name: SKU878 - - id: 879 - name: SKU879 - - id: 880 - name: SKU880 - - id: 881 - name: SKU881 - - id: 882 - name: SKU882 - - id: 883 - name: SKU883 - - id: 884 - name: SKU884 - - id: 885 - name: SKU885 - - id: 886 - name: SKU886 - - id: 887 - name: SKU887 - - id: 888 - name: SKU888 - - id: 889 - name: SKU889 - - id: 890 - name: SKU890 - - id: 891 - name: SKU891 - - id: 892 - name: SKU892 - - id: 893 - name: SKU893 - - id: 894 - name: SKU894 - - id: 895 - name: SKU895 - - id: 896 - name: SKU896 - - id: 897 - name: SKU897 - - id: 898 - name: SKU898 - - id: 899 - name: SKU899 - - id: 900 - name: SKU900 - - id: 901 - name: SKU901 - - id: 902 - name: SKU902 - - id: 903 - name: SKU903 - - id: 904 - name: SKU904 - - id: 905 - name: SKU905 - - id: 906 - name: SKU906 - - id: 907 - name: SKU907 - - id: 908 - name: SKU908 - - id: 909 - name: SKU909 - - id: 910 - name: SKU910 - - id: 911 - name: SKU911 - - id: 912 - name: SKU912 - - id: 913 - name: SKU913 - - id: 914 - name: SKU914 - - id: 915 - name: SKU915 - - id: 916 - name: SKU916 - - id: 917 - name: SKU917 - - id: 918 - name: SKU918 - - id: 919 - name: SKU919 - - id: 920 - name: SKU920 - - id: 921 - name: SKU921 - - id: 922 - name: SKU922 - - id: 923 - name: SKU923 - - id: 924 - name: SKU924 - - id: 925 - name: SKU925 - - id: 926 - name: SKU926 - - id: 927 - name: SKU927 - - id: 928 - name: SKU928 - - id: 929 - name: SKU929 - - id: 930 - name: SKU930 - - id: 931 - name: SKU931 - - id: 932 - name: SKU932 - - id: 933 - name: SKU933 - - id: 934 - name: SKU934 - - id: 935 - name: SKU935 - - id: 936 - name: SKU936 - - id: 937 - name: SKU937 - - id: 938 - name: SKU938 - - id: 939 - name: SKU939 - - id: 940 - name: SKU940 - - id: 941 - name: SKU941 - - id: 942 - name: SKU942 - - id: 943 - name: SKU943 - - id: 944 - name: SKU944 - - id: 945 - name: SKU945 - - id: 946 - name: SKU946 - - id: 947 - name: SKU947 - - id: 948 - name: SKU948 - - id: 949 - name: SKU949 - - id: 950 - name: SKU950 - - id: 951 - name: SKU951 - - id: 952 - name: SKU952 - - id: 953 - name: SKU953 - - id: 954 - name: SKU954 - - id: 955 - name: SKU955 - - id: 956 - name: SKU956 - - id: 957 - name: SKU957 - - id: 958 - name: SKU958 - - id: 959 - name: SKU959 - - id: 960 - name: SKU960 - - id: 961 - name: SKU961 - - id: 962 - name: SKU962 - - id: 963 - name: SKU963 - - id: 964 - name: SKU964 - - id: 965 - name: SKU965 - - id: 966 - name: SKU966 - - id: 967 - name: SKU967 - - id: 968 - name: SKU968 - - id: 969 - name: SKU969 - - id: 970 - name: SKU970 - - id: 971 - name: SKU971 - - id: 972 - name: SKU972 - - id: 973 - name: SKU973 - - id: 974 - name: SKU974 - - id: 975 - name: SKU975 - - id: 976 - name: SKU976 - - id: 977 - name: SKU977 - - id: 978 - name: SKU978 - - id: 979 - name: SKU979 - - id: 980 - name: SKU980 - - id: 981 - name: SKU981 - - id: 982 - name: SKU982 - - id: 983 - name: SKU983 - - id: 984 - name: SKU984 - - id: 985 - name: SKU985 - - id: 986 - name: SKU986 - - id: 987 - name: SKU987 - - id: 988 - name: SKU988 - - id: 989 - name: SKU989 - - id: 990 - name: SKU990 - - id: 991 - name: SKU991 - - id: 992 - name: SKU992 - - id: 993 - name: SKU993 - - id: 994 - name: SKU994 - - id: 995 - name: SKU995 - - id: 996 - name: SKU996 - - id: 997 - name: SKU997 - - id: 998 - name: SKU998 - - id: 999 - name: SKU999 - topology: - STORE0: - SKU0: - - WAREHOUSE0 - SKU1: - - WAREHOUSE0 - SKU10: - - WAREHOUSE0 - SKU100: - - WAREHOUSE0 - SKU101: - - WAREHOUSE0 - SKU102: - - WAREHOUSE0 - SKU103: - - WAREHOUSE0 - SKU104: - - WAREHOUSE0 - SKU105: - - WAREHOUSE0 - SKU106: - - WAREHOUSE0 - SKU107: - - WAREHOUSE0 - SKU108: - - WAREHOUSE0 - SKU109: - - WAREHOUSE0 - SKU11: - - WAREHOUSE0 - SKU110: - - WAREHOUSE0 - SKU111: - - WAREHOUSE0 - SKU112: - - WAREHOUSE0 - SKU113: - - WAREHOUSE0 - SKU114: - - WAREHOUSE0 - SKU115: - - WAREHOUSE0 - SKU116: - - WAREHOUSE0 - SKU117: - - WAREHOUSE0 - SKU118: - - WAREHOUSE0 - SKU119: - - WAREHOUSE0 - SKU12: - - WAREHOUSE0 - SKU120: - - WAREHOUSE0 - SKU121: - - WAREHOUSE0 - SKU122: - - WAREHOUSE0 - SKU123: - - WAREHOUSE0 - SKU124: - - WAREHOUSE0 - SKU125: - - WAREHOUSE0 - SKU126: - - WAREHOUSE0 - SKU127: - - WAREHOUSE0 - SKU128: - - WAREHOUSE0 - SKU129: - - WAREHOUSE0 - SKU13: - - WAREHOUSE0 - SKU130: - - WAREHOUSE0 - SKU131: - - WAREHOUSE0 - SKU132: - - WAREHOUSE0 - SKU133: - - WAREHOUSE0 - SKU134: - - WAREHOUSE0 - SKU135: - - WAREHOUSE0 - SKU136: - - WAREHOUSE0 - SKU137: - - WAREHOUSE0 - SKU138: - - WAREHOUSE0 - SKU139: - - WAREHOUSE0 - SKU14: - - WAREHOUSE0 - SKU140: - - WAREHOUSE0 - SKU141: - - WAREHOUSE0 - SKU142: - - WAREHOUSE0 - SKU143: - - WAREHOUSE0 - SKU144: - - WAREHOUSE0 - SKU145: - - WAREHOUSE0 - SKU146: - - WAREHOUSE0 - SKU147: - - WAREHOUSE0 - SKU148: - - WAREHOUSE0 - SKU149: - - WAREHOUSE0 - SKU15: - - WAREHOUSE0 - SKU150: - - WAREHOUSE0 - SKU151: - - WAREHOUSE0 - SKU152: - - WAREHOUSE0 - SKU153: - - WAREHOUSE0 - SKU154: - - WAREHOUSE0 - SKU155: - - WAREHOUSE0 - SKU156: - - WAREHOUSE0 - SKU157: - - WAREHOUSE0 - SKU158: - - WAREHOUSE0 - SKU159: - - WAREHOUSE0 - SKU16: - - WAREHOUSE0 - SKU160: - - WAREHOUSE0 - SKU161: - - WAREHOUSE0 - SKU162: - - WAREHOUSE0 - SKU163: - - WAREHOUSE0 - SKU164: - - WAREHOUSE0 - SKU165: - - WAREHOUSE0 - SKU166: - - WAREHOUSE0 - SKU167: - - WAREHOUSE0 - SKU168: - - WAREHOUSE0 - SKU169: - - WAREHOUSE0 - SKU17: - - WAREHOUSE0 - SKU170: - - WAREHOUSE0 - SKU171: - - WAREHOUSE0 - SKU172: - - WAREHOUSE0 - SKU173: - - WAREHOUSE0 - SKU174: - - WAREHOUSE0 - SKU175: - - WAREHOUSE0 - SKU176: - - WAREHOUSE0 - SKU177: - - WAREHOUSE0 - SKU178: - - WAREHOUSE0 - SKU179: - - WAREHOUSE0 - SKU18: - - WAREHOUSE0 - SKU180: - - WAREHOUSE0 - SKU181: - - WAREHOUSE0 - SKU182: - - WAREHOUSE0 - SKU183: - - WAREHOUSE0 - SKU184: - - WAREHOUSE0 - SKU185: - - WAREHOUSE0 - SKU186: - - WAREHOUSE0 - SKU187: - - WAREHOUSE0 - SKU188: - - WAREHOUSE0 - SKU189: - - WAREHOUSE0 - SKU19: - - WAREHOUSE0 - SKU190: - - WAREHOUSE0 - SKU191: - - WAREHOUSE0 - SKU192: - - WAREHOUSE0 - SKU193: - - WAREHOUSE0 - SKU194: - - WAREHOUSE0 - SKU195: - - WAREHOUSE0 - SKU196: - - WAREHOUSE0 - SKU197: - - WAREHOUSE0 - SKU198: - - WAREHOUSE0 - SKU199: - - WAREHOUSE0 - SKU2: - - WAREHOUSE0 - SKU20: - - WAREHOUSE0 - SKU200: - - WAREHOUSE0 - SKU201: - - WAREHOUSE0 - SKU202: - - WAREHOUSE0 - SKU203: - - WAREHOUSE0 - SKU204: - - WAREHOUSE0 - SKU205: - - WAREHOUSE0 - SKU206: - - WAREHOUSE0 - SKU207: - - WAREHOUSE0 - SKU208: - - WAREHOUSE0 - SKU209: - - WAREHOUSE0 - SKU21: - - WAREHOUSE0 - SKU210: - - WAREHOUSE0 - SKU211: - - WAREHOUSE0 - SKU212: - - WAREHOUSE0 - SKU213: - - WAREHOUSE0 - SKU214: - - WAREHOUSE0 - SKU215: - - WAREHOUSE0 - SKU216: - - WAREHOUSE0 - SKU217: - - WAREHOUSE0 - SKU218: - - WAREHOUSE0 - SKU219: - - WAREHOUSE0 - SKU22: - - WAREHOUSE0 - SKU220: - - WAREHOUSE0 - SKU221: - - WAREHOUSE0 - SKU222: - - WAREHOUSE0 - SKU223: - - WAREHOUSE0 - SKU224: - - WAREHOUSE0 - SKU225: - - WAREHOUSE0 - SKU226: - - WAREHOUSE0 - SKU227: - - WAREHOUSE0 - SKU228: - - WAREHOUSE0 - SKU229: - - WAREHOUSE0 - SKU23: - - WAREHOUSE0 - SKU230: - - WAREHOUSE0 - SKU231: - - WAREHOUSE0 - SKU232: - - WAREHOUSE0 - SKU233: - - WAREHOUSE0 - SKU234: - - WAREHOUSE0 - SKU235: - - WAREHOUSE0 - SKU236: - - WAREHOUSE0 - SKU237: - - WAREHOUSE0 - SKU238: - - WAREHOUSE0 - SKU239: - - WAREHOUSE0 - SKU24: - - WAREHOUSE0 - SKU240: - - WAREHOUSE0 - SKU241: - - WAREHOUSE0 - SKU242: - - WAREHOUSE0 - SKU243: - - WAREHOUSE0 - SKU244: - - WAREHOUSE0 - SKU245: - - WAREHOUSE0 - SKU246: - - WAREHOUSE0 - SKU247: - - WAREHOUSE0 - SKU248: - - WAREHOUSE0 - SKU249: - - WAREHOUSE0 - SKU25: - - WAREHOUSE0 - SKU250: - - WAREHOUSE0 - SKU251: - - WAREHOUSE0 - SKU252: - - WAREHOUSE0 - SKU253: - - WAREHOUSE0 - SKU254: - - WAREHOUSE0 - SKU255: - - WAREHOUSE0 - SKU256: - - WAREHOUSE0 - SKU257: - - WAREHOUSE0 - SKU258: - - WAREHOUSE0 - SKU259: - - WAREHOUSE0 - SKU26: - - WAREHOUSE0 - SKU260: - - WAREHOUSE0 - SKU261: - - WAREHOUSE0 - SKU262: - - WAREHOUSE0 - SKU263: - - WAREHOUSE0 - SKU264: - - WAREHOUSE0 - SKU265: - - WAREHOUSE0 - SKU266: - - WAREHOUSE0 - SKU267: - - WAREHOUSE0 - SKU268: - - WAREHOUSE0 - SKU269: - - WAREHOUSE0 - SKU27: - - WAREHOUSE0 - SKU270: - - WAREHOUSE0 - SKU271: - - WAREHOUSE0 - SKU272: - - WAREHOUSE0 - SKU273: - - WAREHOUSE0 - SKU274: - - WAREHOUSE0 - SKU275: - - WAREHOUSE0 - SKU276: - - WAREHOUSE0 - SKU277: - - WAREHOUSE0 - SKU278: - - WAREHOUSE0 - SKU279: - - WAREHOUSE0 - SKU28: - - WAREHOUSE0 - SKU280: - - WAREHOUSE0 - SKU281: - - WAREHOUSE0 - SKU282: - - WAREHOUSE0 - SKU283: - - WAREHOUSE0 - SKU284: - - WAREHOUSE0 - SKU285: - - WAREHOUSE0 - SKU286: - - WAREHOUSE0 - SKU287: - - WAREHOUSE0 - SKU288: - - WAREHOUSE0 - SKU289: - - WAREHOUSE0 - SKU29: - - WAREHOUSE0 - SKU290: - - WAREHOUSE0 - SKU291: - - WAREHOUSE0 - SKU292: - - WAREHOUSE0 - SKU293: - - WAREHOUSE0 - SKU294: - - WAREHOUSE0 - SKU295: - - WAREHOUSE0 - SKU296: - - WAREHOUSE0 - SKU297: - - WAREHOUSE0 - SKU298: - - WAREHOUSE0 - SKU299: - - WAREHOUSE0 - SKU3: - - WAREHOUSE0 - SKU30: - - WAREHOUSE0 - SKU300: - - WAREHOUSE0 - SKU301: - - WAREHOUSE0 - SKU302: - - WAREHOUSE0 - SKU303: - - WAREHOUSE0 - SKU304: - - WAREHOUSE0 - SKU305: - - WAREHOUSE0 - SKU306: - - WAREHOUSE0 - SKU307: - - WAREHOUSE0 - SKU308: - - WAREHOUSE0 - SKU309: - - WAREHOUSE0 - SKU31: - - WAREHOUSE0 - SKU310: - - WAREHOUSE0 - SKU311: - - WAREHOUSE0 - SKU312: - - WAREHOUSE0 - SKU313: - - WAREHOUSE0 - SKU314: - - WAREHOUSE0 - SKU315: - - WAREHOUSE0 - SKU316: - - WAREHOUSE0 - SKU317: - - WAREHOUSE0 - SKU318: - - WAREHOUSE0 - SKU319: - - WAREHOUSE0 - SKU32: - - WAREHOUSE0 - SKU320: - - WAREHOUSE0 - SKU321: - - WAREHOUSE0 - SKU322: - - WAREHOUSE0 - SKU323: - - WAREHOUSE0 - SKU324: - - WAREHOUSE0 - SKU325: - - WAREHOUSE0 - SKU326: - - WAREHOUSE0 - SKU327: - - WAREHOUSE0 - SKU328: - - WAREHOUSE0 - SKU329: - - WAREHOUSE0 - SKU33: - - WAREHOUSE0 - SKU330: - - WAREHOUSE0 - SKU331: - - WAREHOUSE0 - SKU332: - - WAREHOUSE0 - SKU333: - - WAREHOUSE0 - SKU334: - - WAREHOUSE0 - SKU335: - - WAREHOUSE0 - SKU336: - - WAREHOUSE0 - SKU337: - - WAREHOUSE0 - SKU338: - - WAREHOUSE0 - SKU339: - - WAREHOUSE0 - SKU34: - - WAREHOUSE0 - SKU340: - - WAREHOUSE0 - SKU341: - - WAREHOUSE0 - SKU342: - - WAREHOUSE0 - SKU343: - - WAREHOUSE0 - SKU344: - - WAREHOUSE0 - SKU345: - - WAREHOUSE0 - SKU346: - - WAREHOUSE0 - SKU347: - - WAREHOUSE0 - SKU348: - - WAREHOUSE0 - SKU349: - - WAREHOUSE0 - SKU35: - - WAREHOUSE0 - SKU350: - - WAREHOUSE0 - SKU351: - - WAREHOUSE0 - SKU352: - - WAREHOUSE0 - SKU353: - - WAREHOUSE0 - SKU354: - - WAREHOUSE0 - SKU355: - - WAREHOUSE0 - SKU356: - - WAREHOUSE0 - SKU357: - - WAREHOUSE0 - SKU358: - - WAREHOUSE0 - SKU359: - - WAREHOUSE0 - SKU36: - - WAREHOUSE0 - SKU360: - - WAREHOUSE0 - SKU361: - - WAREHOUSE0 - SKU362: - - WAREHOUSE0 - SKU363: - - WAREHOUSE0 - SKU364: - - WAREHOUSE0 - SKU365: - - WAREHOUSE0 - SKU366: - - WAREHOUSE0 - SKU367: - - WAREHOUSE0 - SKU368: - - WAREHOUSE0 - SKU369: - - WAREHOUSE0 - SKU37: - - WAREHOUSE0 - SKU370: - - WAREHOUSE0 - SKU371: - - WAREHOUSE0 - SKU372: - - WAREHOUSE0 - SKU373: - - WAREHOUSE0 - SKU374: - - WAREHOUSE0 - SKU375: - - WAREHOUSE0 - SKU376: - - WAREHOUSE0 - SKU377: - - WAREHOUSE0 - SKU378: - - WAREHOUSE0 - SKU379: - - WAREHOUSE0 - SKU38: - - WAREHOUSE0 - SKU380: - - WAREHOUSE0 - SKU381: - - WAREHOUSE0 - SKU382: - - WAREHOUSE0 - SKU383: - - WAREHOUSE0 - SKU384: - - WAREHOUSE0 - SKU385: - - WAREHOUSE0 - SKU386: - - WAREHOUSE0 - SKU387: - - WAREHOUSE0 - SKU388: - - WAREHOUSE0 - SKU389: - - WAREHOUSE0 - SKU39: - - WAREHOUSE0 - SKU390: - - WAREHOUSE0 - SKU391: - - WAREHOUSE0 - SKU392: - - WAREHOUSE0 - SKU393: - - WAREHOUSE0 - SKU394: - - WAREHOUSE0 - SKU395: - - WAREHOUSE0 - SKU396: - - WAREHOUSE0 - SKU397: - - WAREHOUSE0 - SKU398: - - WAREHOUSE0 - SKU399: - - WAREHOUSE0 - SKU4: - - WAREHOUSE0 - SKU40: - - WAREHOUSE0 - SKU400: - - WAREHOUSE0 - SKU401: - - WAREHOUSE0 - SKU402: - - WAREHOUSE0 - SKU403: - - WAREHOUSE0 - SKU404: - - WAREHOUSE0 - SKU405: - - WAREHOUSE0 - SKU406: - - WAREHOUSE0 - SKU407: - - WAREHOUSE0 - SKU408: - - WAREHOUSE0 - SKU409: - - WAREHOUSE0 - SKU41: - - WAREHOUSE0 - SKU410: - - WAREHOUSE0 - SKU411: - - WAREHOUSE0 - SKU412: - - WAREHOUSE0 - SKU413: - - WAREHOUSE0 - SKU414: - - WAREHOUSE0 - SKU415: - - WAREHOUSE0 - SKU416: - - WAREHOUSE0 - SKU417: - - WAREHOUSE0 - SKU418: - - WAREHOUSE0 - SKU419: - - WAREHOUSE0 - SKU42: - - WAREHOUSE0 - SKU420: - - WAREHOUSE0 - SKU421: - - WAREHOUSE0 - SKU422: - - WAREHOUSE0 - SKU423: - - WAREHOUSE0 - SKU424: - - WAREHOUSE0 - SKU425: - - WAREHOUSE0 - SKU426: - - WAREHOUSE0 - SKU427: - - WAREHOUSE0 - SKU428: - - WAREHOUSE0 - SKU429: - - WAREHOUSE0 - SKU43: - - WAREHOUSE0 - SKU430: - - WAREHOUSE0 - SKU431: - - WAREHOUSE0 - SKU432: - - WAREHOUSE0 - SKU433: - - WAREHOUSE0 - SKU434: - - WAREHOUSE0 - SKU435: - - WAREHOUSE0 - SKU436: - - WAREHOUSE0 - SKU437: - - WAREHOUSE0 - SKU438: - - WAREHOUSE0 - SKU439: - - WAREHOUSE0 - SKU44: - - WAREHOUSE0 - SKU440: - - WAREHOUSE0 - SKU441: - - WAREHOUSE0 - SKU442: - - WAREHOUSE0 - SKU443: - - WAREHOUSE0 - SKU444: - - WAREHOUSE0 - SKU445: - - WAREHOUSE0 - SKU446: - - WAREHOUSE0 - SKU447: - - WAREHOUSE0 - SKU448: - - WAREHOUSE0 - SKU449: - - WAREHOUSE0 - SKU45: - - WAREHOUSE0 - SKU450: - - WAREHOUSE0 - SKU451: - - WAREHOUSE0 - SKU452: - - WAREHOUSE0 - SKU453: - - WAREHOUSE0 - SKU454: - - WAREHOUSE0 - SKU455: - - WAREHOUSE0 - SKU456: - - WAREHOUSE0 - SKU457: - - WAREHOUSE0 - SKU458: - - WAREHOUSE0 - SKU459: - - WAREHOUSE0 - SKU46: - - WAREHOUSE0 - SKU460: - - WAREHOUSE0 - SKU461: - - WAREHOUSE0 - SKU462: - - WAREHOUSE0 - SKU463: - - WAREHOUSE0 - SKU464: - - WAREHOUSE0 - SKU465: - - WAREHOUSE0 - SKU466: - - WAREHOUSE0 - SKU467: - - WAREHOUSE0 - SKU468: - - WAREHOUSE0 - SKU469: - - WAREHOUSE0 - SKU47: - - WAREHOUSE0 - SKU470: - - WAREHOUSE0 - SKU471: - - WAREHOUSE0 - SKU472: - - WAREHOUSE0 - SKU473: - - WAREHOUSE0 - SKU474: - - WAREHOUSE0 - SKU475: - - WAREHOUSE0 - SKU476: - - WAREHOUSE0 - SKU477: - - WAREHOUSE0 - SKU478: - - WAREHOUSE0 - SKU479: - - WAREHOUSE0 - SKU48: - - WAREHOUSE0 - SKU480: - - WAREHOUSE0 - SKU481: - - WAREHOUSE0 - SKU482: - - WAREHOUSE0 - SKU483: - - WAREHOUSE0 - SKU484: - - WAREHOUSE0 - SKU485: - - WAREHOUSE0 - SKU486: - - WAREHOUSE0 - SKU487: - - WAREHOUSE0 - SKU488: - - WAREHOUSE0 - SKU489: - - WAREHOUSE0 - SKU49: - - WAREHOUSE0 - SKU490: - - WAREHOUSE0 - SKU491: - - WAREHOUSE0 - SKU492: - - WAREHOUSE0 - SKU493: - - WAREHOUSE0 - SKU494: - - WAREHOUSE0 - SKU495: - - WAREHOUSE0 - SKU496: - - WAREHOUSE0 - SKU497: - - WAREHOUSE0 - SKU498: - - WAREHOUSE0 - SKU499: - - WAREHOUSE0 - SKU5: - - WAREHOUSE0 - SKU50: - - WAREHOUSE0 - SKU500: - - WAREHOUSE0 - SKU501: - - WAREHOUSE0 - SKU502: - - WAREHOUSE0 - SKU503: - - WAREHOUSE0 - SKU504: - - WAREHOUSE0 - SKU505: - - WAREHOUSE0 - SKU506: - - WAREHOUSE0 - SKU507: - - WAREHOUSE0 - SKU508: - - WAREHOUSE0 - SKU509: - - WAREHOUSE0 - SKU51: - - WAREHOUSE0 - SKU510: - - WAREHOUSE0 - SKU511: - - WAREHOUSE0 - SKU512: - - WAREHOUSE0 - SKU513: - - WAREHOUSE0 - SKU514: - - WAREHOUSE0 - SKU515: - - WAREHOUSE0 - SKU516: - - WAREHOUSE0 - SKU517: - - WAREHOUSE0 - SKU518: - - WAREHOUSE0 - SKU519: - - WAREHOUSE0 - SKU52: - - WAREHOUSE0 - SKU520: - - WAREHOUSE0 - SKU521: - - WAREHOUSE0 - SKU522: - - WAREHOUSE0 - SKU523: - - WAREHOUSE0 - SKU524: - - WAREHOUSE0 - SKU525: - - WAREHOUSE0 - SKU526: - - WAREHOUSE0 - SKU527: - - WAREHOUSE0 - SKU528: - - WAREHOUSE0 - SKU529: - - WAREHOUSE0 - SKU53: - - WAREHOUSE0 - SKU530: - - WAREHOUSE0 - SKU531: - - WAREHOUSE0 - SKU532: - - WAREHOUSE0 - SKU533: - - WAREHOUSE0 - SKU534: - - WAREHOUSE0 - SKU535: - - WAREHOUSE0 - SKU536: - - WAREHOUSE0 - SKU537: - - WAREHOUSE0 - SKU538: - - WAREHOUSE0 - SKU539: - - WAREHOUSE0 - SKU54: - - WAREHOUSE0 - SKU540: - - WAREHOUSE0 - SKU541: - - WAREHOUSE0 - SKU542: - - WAREHOUSE0 - SKU543: - - WAREHOUSE0 - SKU544: - - WAREHOUSE0 - SKU545: - - WAREHOUSE0 - SKU546: - - WAREHOUSE0 - SKU547: - - WAREHOUSE0 - SKU548: - - WAREHOUSE0 - SKU549: - - WAREHOUSE0 - SKU55: - - WAREHOUSE0 - SKU550: - - WAREHOUSE0 - SKU551: - - WAREHOUSE0 - SKU552: - - WAREHOUSE0 - SKU553: - - WAREHOUSE0 - SKU554: - - WAREHOUSE0 - SKU555: - - WAREHOUSE0 - SKU556: - - WAREHOUSE0 - SKU557: - - WAREHOUSE0 - SKU558: - - WAREHOUSE0 - SKU559: - - WAREHOUSE0 - SKU56: - - WAREHOUSE0 - SKU560: - - WAREHOUSE0 - SKU561: - - WAREHOUSE0 - SKU562: - - WAREHOUSE0 - SKU563: - - WAREHOUSE0 - SKU564: - - WAREHOUSE0 - SKU565: - - WAREHOUSE0 - SKU566: - - WAREHOUSE0 - SKU567: - - WAREHOUSE0 - SKU568: - - WAREHOUSE0 - SKU569: - - WAREHOUSE0 - SKU57: - - WAREHOUSE0 - SKU570: - - WAREHOUSE0 - SKU571: - - WAREHOUSE0 - SKU572: - - WAREHOUSE0 - SKU573: - - WAREHOUSE0 - SKU574: - - WAREHOUSE0 - SKU575: - - WAREHOUSE0 - SKU576: - - WAREHOUSE0 - SKU577: - - WAREHOUSE0 - SKU578: - - WAREHOUSE0 - SKU579: - - WAREHOUSE0 - SKU58: - - WAREHOUSE0 - SKU580: - - WAREHOUSE0 - SKU581: - - WAREHOUSE0 - SKU582: - - WAREHOUSE0 - SKU583: - - WAREHOUSE0 - SKU584: - - WAREHOUSE0 - SKU585: - - WAREHOUSE0 - SKU586: - - WAREHOUSE0 - SKU587: - - WAREHOUSE0 - SKU588: - - WAREHOUSE0 - SKU589: - - WAREHOUSE0 - SKU59: - - WAREHOUSE0 - SKU590: - - WAREHOUSE0 - SKU591: - - WAREHOUSE0 - SKU592: - - WAREHOUSE0 - SKU593: - - WAREHOUSE0 - SKU594: - - WAREHOUSE0 - SKU595: - - WAREHOUSE0 - SKU596: - - WAREHOUSE0 - SKU597: - - WAREHOUSE0 - SKU598: - - WAREHOUSE0 - SKU599: - - WAREHOUSE0 - SKU6: - - WAREHOUSE0 - SKU60: - - WAREHOUSE0 - SKU600: - - WAREHOUSE0 - SKU601: - - WAREHOUSE0 - SKU602: - - WAREHOUSE0 - SKU603: - - WAREHOUSE0 - SKU604: - - WAREHOUSE0 - SKU605: - - WAREHOUSE0 - SKU606: - - WAREHOUSE0 - SKU607: - - WAREHOUSE0 - SKU608: - - WAREHOUSE0 - SKU609: - - WAREHOUSE0 - SKU61: - - WAREHOUSE0 - SKU610: - - WAREHOUSE0 - SKU611: - - WAREHOUSE0 - SKU612: - - WAREHOUSE0 - SKU613: - - WAREHOUSE0 - SKU614: - - WAREHOUSE0 - SKU615: - - WAREHOUSE0 - SKU616: - - WAREHOUSE0 - SKU617: - - WAREHOUSE0 - SKU618: - - WAREHOUSE0 - SKU619: - - WAREHOUSE0 - SKU62: - - WAREHOUSE0 - SKU620: - - WAREHOUSE0 - SKU621: - - WAREHOUSE0 - SKU622: - - WAREHOUSE0 - SKU623: - - WAREHOUSE0 - SKU624: - - WAREHOUSE0 - SKU625: - - WAREHOUSE0 - SKU626: - - WAREHOUSE0 - SKU627: - - WAREHOUSE0 - SKU628: - - WAREHOUSE0 - SKU629: - - WAREHOUSE0 - SKU63: - - WAREHOUSE0 - SKU630: - - WAREHOUSE0 - SKU631: - - WAREHOUSE0 - SKU632: - - WAREHOUSE0 - SKU633: - - WAREHOUSE0 - SKU634: - - WAREHOUSE0 - SKU635: - - WAREHOUSE0 - SKU636: - - WAREHOUSE0 - SKU637: - - WAREHOUSE0 - SKU638: - - WAREHOUSE0 - SKU639: - - WAREHOUSE0 - SKU64: - - WAREHOUSE0 - SKU640: - - WAREHOUSE0 - SKU641: - - WAREHOUSE0 - SKU642: - - WAREHOUSE0 - SKU643: - - WAREHOUSE0 - SKU644: - - WAREHOUSE0 - SKU645: - - WAREHOUSE0 - SKU646: - - WAREHOUSE0 - SKU647: - - WAREHOUSE0 - SKU648: - - WAREHOUSE0 - SKU649: - - WAREHOUSE0 - SKU65: - - WAREHOUSE0 - SKU650: - - WAREHOUSE0 - SKU651: - - WAREHOUSE0 - SKU652: - - WAREHOUSE0 - SKU653: - - WAREHOUSE0 - SKU654: - - WAREHOUSE0 - SKU655: - - WAREHOUSE0 - SKU656: - - WAREHOUSE0 - SKU657: - - WAREHOUSE0 - SKU658: - - WAREHOUSE0 - SKU659: - - WAREHOUSE0 - SKU66: - - WAREHOUSE0 - SKU660: - - WAREHOUSE0 - SKU661: - - WAREHOUSE0 - SKU662: - - WAREHOUSE0 - SKU663: - - WAREHOUSE0 - SKU664: - - WAREHOUSE0 - SKU665: - - WAREHOUSE0 - SKU666: - - WAREHOUSE0 - SKU667: - - WAREHOUSE0 - SKU668: - - WAREHOUSE0 - SKU669: - - WAREHOUSE0 - SKU67: - - WAREHOUSE0 - SKU670: - - WAREHOUSE0 - SKU671: - - WAREHOUSE0 - SKU672: - - WAREHOUSE0 - SKU673: - - WAREHOUSE0 - SKU674: - - WAREHOUSE0 - SKU675: - - WAREHOUSE0 - SKU676: - - WAREHOUSE0 - SKU677: - - WAREHOUSE0 - SKU678: - - WAREHOUSE0 - SKU679: - - WAREHOUSE0 - SKU68: - - WAREHOUSE0 - SKU680: - - WAREHOUSE0 - SKU681: - - WAREHOUSE0 - SKU682: - - WAREHOUSE0 - SKU683: - - WAREHOUSE0 - SKU684: - - WAREHOUSE0 - SKU685: - - WAREHOUSE0 - SKU686: - - WAREHOUSE0 - SKU687: - - WAREHOUSE0 - SKU688: - - WAREHOUSE0 - SKU689: - - WAREHOUSE0 - SKU69: - - WAREHOUSE0 - SKU690: - - WAREHOUSE0 - SKU691: - - WAREHOUSE0 - SKU692: - - WAREHOUSE0 - SKU693: - - WAREHOUSE0 - SKU694: - - WAREHOUSE0 - SKU695: - - WAREHOUSE0 - SKU696: - - WAREHOUSE0 - SKU697: - - WAREHOUSE0 - SKU698: - - WAREHOUSE0 - SKU699: - - WAREHOUSE0 - SKU7: - - WAREHOUSE0 - SKU70: - - WAREHOUSE0 - SKU700: - - WAREHOUSE0 - SKU701: - - WAREHOUSE0 - SKU702: - - WAREHOUSE0 - SKU703: - - WAREHOUSE0 - SKU704: - - WAREHOUSE0 - SKU705: - - WAREHOUSE0 - SKU706: - - WAREHOUSE0 - SKU707: - - WAREHOUSE0 - SKU708: - - WAREHOUSE0 - SKU709: - - WAREHOUSE0 - SKU71: - - WAREHOUSE0 - SKU710: - - WAREHOUSE0 - SKU711: - - WAREHOUSE0 - SKU712: - - WAREHOUSE0 - SKU713: - - WAREHOUSE0 - SKU714: - - WAREHOUSE0 - SKU715: - - WAREHOUSE0 - SKU716: - - WAREHOUSE0 - SKU717: - - WAREHOUSE0 - SKU718: - - WAREHOUSE0 - SKU719: - - WAREHOUSE0 - SKU72: - - WAREHOUSE0 - SKU720: - - WAREHOUSE0 - SKU721: - - WAREHOUSE0 - SKU722: - - WAREHOUSE0 - SKU723: - - WAREHOUSE0 - SKU724: - - WAREHOUSE0 - SKU725: - - WAREHOUSE0 - SKU726: - - WAREHOUSE0 - SKU727: - - WAREHOUSE0 - SKU728: - - WAREHOUSE0 - SKU729: - - WAREHOUSE0 - SKU73: - - WAREHOUSE0 - SKU730: - - WAREHOUSE0 - SKU731: - - WAREHOUSE0 - SKU732: - - WAREHOUSE0 - SKU733: - - WAREHOUSE0 - SKU734: - - WAREHOUSE0 - SKU735: - - WAREHOUSE0 - SKU736: - - WAREHOUSE0 - SKU737: - - WAREHOUSE0 - SKU738: - - WAREHOUSE0 - SKU739: - - WAREHOUSE0 - SKU74: - - WAREHOUSE0 - SKU740: - - WAREHOUSE0 - SKU741: - - WAREHOUSE0 - SKU742: - - WAREHOUSE0 - SKU743: - - WAREHOUSE0 - SKU744: - - WAREHOUSE0 - SKU745: - - WAREHOUSE0 - SKU746: - - WAREHOUSE0 - SKU747: - - WAREHOUSE0 - SKU748: - - WAREHOUSE0 - SKU749: - - WAREHOUSE0 - SKU75: - - WAREHOUSE0 - SKU750: - - WAREHOUSE0 - SKU751: - - WAREHOUSE0 - SKU752: - - WAREHOUSE0 - SKU753: - - WAREHOUSE0 - SKU754: - - WAREHOUSE0 - SKU755: - - WAREHOUSE0 - SKU756: - - WAREHOUSE0 - SKU757: - - WAREHOUSE0 - SKU758: - - WAREHOUSE0 - SKU759: - - WAREHOUSE0 - SKU76: - - WAREHOUSE0 - SKU760: - - WAREHOUSE0 - SKU761: - - WAREHOUSE0 - SKU762: - - WAREHOUSE0 - SKU763: - - WAREHOUSE0 - SKU764: - - WAREHOUSE0 - SKU765: - - WAREHOUSE0 - SKU766: - - WAREHOUSE0 - SKU767: - - WAREHOUSE0 - SKU768: - - WAREHOUSE0 - SKU769: - - WAREHOUSE0 - SKU77: - - WAREHOUSE0 - SKU770: - - WAREHOUSE0 - SKU771: - - WAREHOUSE0 - SKU772: - - WAREHOUSE0 - SKU773: - - WAREHOUSE0 - SKU774: - - WAREHOUSE0 - SKU775: - - WAREHOUSE0 - SKU776: - - WAREHOUSE0 - SKU777: - - WAREHOUSE0 - SKU778: - - WAREHOUSE0 - SKU779: - - WAREHOUSE0 - SKU78: - - WAREHOUSE0 - SKU780: - - WAREHOUSE0 - SKU781: - - WAREHOUSE0 - SKU782: - - WAREHOUSE0 - SKU783: - - WAREHOUSE0 - SKU784: - - WAREHOUSE0 - SKU785: - - WAREHOUSE0 - SKU786: - - WAREHOUSE0 - SKU787: - - WAREHOUSE0 - SKU788: - - WAREHOUSE0 - SKU789: - - WAREHOUSE0 - SKU79: - - WAREHOUSE0 - SKU790: - - WAREHOUSE0 - SKU791: - - WAREHOUSE0 - SKU792: - - WAREHOUSE0 - SKU793: - - WAREHOUSE0 - SKU794: - - WAREHOUSE0 - SKU795: - - WAREHOUSE0 - SKU796: - - WAREHOUSE0 - SKU797: - - WAREHOUSE0 - SKU798: - - WAREHOUSE0 - SKU799: - - WAREHOUSE0 - SKU8: - - WAREHOUSE0 - SKU80: - - WAREHOUSE0 - SKU800: - - WAREHOUSE0 - SKU801: - - WAREHOUSE0 - SKU802: - - WAREHOUSE0 - SKU803: - - WAREHOUSE0 - SKU804: - - WAREHOUSE0 - SKU805: - - WAREHOUSE0 - SKU806: - - WAREHOUSE0 - SKU807: - - WAREHOUSE0 - SKU808: - - WAREHOUSE0 - SKU809: - - WAREHOUSE0 - SKU81: - - WAREHOUSE0 - SKU810: - - WAREHOUSE0 - SKU811: - - WAREHOUSE0 - SKU812: - - WAREHOUSE0 - SKU813: - - WAREHOUSE0 - SKU814: - - WAREHOUSE0 - SKU815: - - WAREHOUSE0 - SKU816: - - WAREHOUSE0 - SKU817: - - WAREHOUSE0 - SKU818: - - WAREHOUSE0 - SKU819: - - WAREHOUSE0 - SKU82: - - WAREHOUSE0 - SKU820: - - WAREHOUSE0 - SKU821: - - WAREHOUSE0 - SKU822: - - WAREHOUSE0 - SKU823: - - WAREHOUSE0 - SKU824: - - WAREHOUSE0 - SKU825: - - WAREHOUSE0 - SKU826: - - WAREHOUSE0 - SKU827: - - WAREHOUSE0 - SKU828: - - WAREHOUSE0 - SKU829: - - WAREHOUSE0 - SKU83: - - WAREHOUSE0 - SKU830: - - WAREHOUSE0 - SKU831: - - WAREHOUSE0 - SKU832: - - WAREHOUSE0 - SKU833: - - WAREHOUSE0 - SKU834: - - WAREHOUSE0 - SKU835: - - WAREHOUSE0 - SKU836: - - WAREHOUSE0 - SKU837: - - WAREHOUSE0 - SKU838: - - WAREHOUSE0 - SKU839: - - WAREHOUSE0 - SKU84: - - WAREHOUSE0 - SKU840: - - WAREHOUSE0 - SKU841: - - WAREHOUSE0 - SKU842: - - WAREHOUSE0 - SKU843: - - WAREHOUSE0 - SKU844: - - WAREHOUSE0 - SKU845: - - WAREHOUSE0 - SKU846: - - WAREHOUSE0 - SKU847: - - WAREHOUSE0 - SKU848: - - WAREHOUSE0 - SKU849: - - WAREHOUSE0 - SKU85: - - WAREHOUSE0 - SKU850: - - WAREHOUSE0 - SKU851: - - WAREHOUSE0 - SKU852: - - WAREHOUSE0 - SKU853: - - WAREHOUSE0 - SKU854: - - WAREHOUSE0 - SKU855: - - WAREHOUSE0 - SKU856: - - WAREHOUSE0 - SKU857: - - WAREHOUSE0 - SKU858: - - WAREHOUSE0 - SKU859: - - WAREHOUSE0 - SKU86: - - WAREHOUSE0 - SKU860: - - WAREHOUSE0 - SKU861: - - WAREHOUSE0 - SKU862: - - WAREHOUSE0 - SKU863: - - WAREHOUSE0 - SKU864: - - WAREHOUSE0 - SKU865: - - WAREHOUSE0 - SKU866: - - WAREHOUSE0 - SKU867: - - WAREHOUSE0 - SKU868: - - WAREHOUSE0 - SKU869: - - WAREHOUSE0 - SKU87: - - WAREHOUSE0 - SKU870: - - WAREHOUSE0 - SKU871: - - WAREHOUSE0 - SKU872: - - WAREHOUSE0 - SKU873: - - WAREHOUSE0 - SKU874: - - WAREHOUSE0 - SKU875: - - WAREHOUSE0 - SKU876: - - WAREHOUSE0 - SKU877: - - WAREHOUSE0 - SKU878: - - WAREHOUSE0 - SKU879: - - WAREHOUSE0 - SKU88: - - WAREHOUSE0 - SKU880: - - WAREHOUSE0 - SKU881: - - WAREHOUSE0 - SKU882: - - WAREHOUSE0 - SKU883: - - WAREHOUSE0 - SKU884: - - WAREHOUSE0 - SKU885: - - WAREHOUSE0 - SKU886: - - WAREHOUSE0 - SKU887: - - WAREHOUSE0 - SKU888: - - WAREHOUSE0 - SKU889: - - WAREHOUSE0 - SKU89: - - WAREHOUSE0 - SKU890: - - WAREHOUSE0 - SKU891: - - WAREHOUSE0 - SKU892: - - WAREHOUSE0 - SKU893: - - WAREHOUSE0 - SKU894: - - WAREHOUSE0 - SKU895: - - WAREHOUSE0 - SKU896: - - WAREHOUSE0 - SKU897: - - WAREHOUSE0 - SKU898: - - WAREHOUSE0 - SKU899: - - WAREHOUSE0 - SKU9: - - WAREHOUSE0 - SKU90: - - WAREHOUSE0 - SKU900: - - WAREHOUSE0 - SKU901: - - WAREHOUSE0 - SKU902: - - WAREHOUSE0 - SKU903: - - WAREHOUSE0 - SKU904: - - WAREHOUSE0 - SKU905: - - WAREHOUSE0 - SKU906: - - WAREHOUSE0 - SKU907: - - WAREHOUSE0 - SKU908: - - WAREHOUSE0 - SKU909: - - WAREHOUSE0 - SKU91: - - WAREHOUSE0 - SKU910: - - WAREHOUSE0 - SKU911: - - WAREHOUSE0 - SKU912: - - WAREHOUSE0 - SKU913: - - WAREHOUSE0 - SKU914: - - WAREHOUSE0 - SKU915: - - WAREHOUSE0 - SKU916: - - WAREHOUSE0 - SKU917: - - WAREHOUSE0 - SKU918: - - WAREHOUSE0 - SKU919: - - WAREHOUSE0 - SKU92: - - WAREHOUSE0 - SKU920: - - WAREHOUSE0 - SKU921: - - WAREHOUSE0 - SKU922: - - WAREHOUSE0 - SKU923: - - WAREHOUSE0 - SKU924: - - WAREHOUSE0 - SKU925: - - WAREHOUSE0 - SKU926: - - WAREHOUSE0 - SKU927: - - WAREHOUSE0 - SKU928: - - WAREHOUSE0 - SKU929: - - WAREHOUSE0 - SKU93: - - WAREHOUSE0 - SKU930: - - WAREHOUSE0 - SKU931: - - WAREHOUSE0 - SKU932: - - WAREHOUSE0 - SKU933: - - WAREHOUSE0 - SKU934: - - WAREHOUSE0 - SKU935: - - WAREHOUSE0 - SKU936: - - WAREHOUSE0 - SKU937: - - WAREHOUSE0 - SKU938: - - WAREHOUSE0 - SKU939: - - WAREHOUSE0 - SKU94: - - WAREHOUSE0 - SKU940: - - WAREHOUSE0 - SKU941: - - WAREHOUSE0 - SKU942: - - WAREHOUSE0 - SKU943: - - WAREHOUSE0 - SKU944: - - WAREHOUSE0 - SKU945: - - WAREHOUSE0 - SKU946: - - WAREHOUSE0 - SKU947: - - WAREHOUSE0 - SKU948: - - WAREHOUSE0 - SKU949: - - WAREHOUSE0 - SKU95: - - WAREHOUSE0 - SKU950: - - WAREHOUSE0 - SKU951: - - WAREHOUSE0 - SKU952: - - WAREHOUSE0 - SKU953: - - WAREHOUSE0 - SKU954: - - WAREHOUSE0 - SKU955: - - WAREHOUSE0 - SKU956: - - WAREHOUSE0 - SKU957: - - WAREHOUSE0 - SKU958: - - WAREHOUSE0 - SKU959: - - WAREHOUSE0 - SKU96: - - WAREHOUSE0 - SKU960: - - WAREHOUSE0 - SKU961: - - WAREHOUSE0 - SKU962: - - WAREHOUSE0 - SKU963: - - WAREHOUSE0 - SKU964: - - WAREHOUSE0 - SKU965: - - WAREHOUSE0 - SKU966: - - WAREHOUSE0 - SKU967: - - WAREHOUSE0 - SKU968: - - WAREHOUSE0 - SKU969: - - WAREHOUSE0 - SKU97: - - WAREHOUSE0 - SKU970: - - WAREHOUSE0 - SKU971: - - WAREHOUSE0 - SKU972: - - WAREHOUSE0 - SKU973: - - WAREHOUSE0 - SKU974: - - WAREHOUSE0 - SKU975: - - WAREHOUSE0 - SKU976: - - WAREHOUSE0 - SKU977: - - WAREHOUSE0 - SKU978: - - WAREHOUSE0 - SKU979: - - WAREHOUSE0 - SKU98: - - WAREHOUSE0 - SKU980: - - WAREHOUSE0 - SKU981: - - WAREHOUSE0 - SKU982: - - WAREHOUSE0 - SKU983: - - WAREHOUSE0 - SKU984: - - WAREHOUSE0 - SKU985: - - WAREHOUSE0 - SKU986: - - WAREHOUSE0 - SKU987: - - WAREHOUSE0 - SKU988: - - WAREHOUSE0 - SKU989: - - WAREHOUSE0 - SKU99: - - WAREHOUSE0 - SKU990: - - WAREHOUSE0 - SKU991: - - WAREHOUSE0 - SKU992: - - WAREHOUSE0 - SKU993: - - WAREHOUSE0 - SKU994: - - WAREHOUSE0 - SKU995: - - WAREHOUSE0 - SKU996: - - WAREHOUSE0 - SKU997: - - WAREHOUSE0 - SKU998: - - WAREHOUSE0 - SKU999: - - WAREHOUSE0 - WAREHOUSE0: - SKU0: - - SUPPLIER0 - SKU1: - - SUPPLIER0 - SKU10: - - SUPPLIER0 - SKU100: - - SUPPLIER0 - SKU101: - - SUPPLIER0 - SKU102: - - SUPPLIER0 - SKU103: - - SUPPLIER0 - SKU104: - - SUPPLIER0 - SKU105: - - SUPPLIER0 - SKU106: - - SUPPLIER0 - SKU107: - - SUPPLIER0 - SKU108: - - SUPPLIER0 - SKU109: - - SUPPLIER0 - SKU11: - - SUPPLIER0 - SKU110: - - SUPPLIER0 - SKU111: - - SUPPLIER0 - SKU112: - - SUPPLIER0 - SKU113: - - SUPPLIER0 - SKU114: - - SUPPLIER0 - SKU115: - - SUPPLIER0 - SKU116: - - SUPPLIER0 - SKU117: - - SUPPLIER0 - SKU118: - - SUPPLIER0 - SKU119: - - SUPPLIER0 - SKU12: - - SUPPLIER0 - SKU120: - - SUPPLIER0 - SKU121: - - SUPPLIER0 - SKU122: - - SUPPLIER0 - SKU123: - - SUPPLIER0 - SKU124: - - SUPPLIER0 - SKU125: - - SUPPLIER0 - SKU126: - - SUPPLIER0 - SKU127: - - SUPPLIER0 - SKU128: - - SUPPLIER0 - SKU129: - - SUPPLIER0 - SKU13: - - SUPPLIER0 - SKU130: - - SUPPLIER0 - SKU131: - - SUPPLIER0 - SKU132: - - SUPPLIER0 - SKU133: - - SUPPLIER0 - SKU134: - - SUPPLIER0 - SKU135: - - SUPPLIER0 - SKU136: - - SUPPLIER0 - SKU137: - - SUPPLIER0 - SKU138: - - SUPPLIER0 - SKU139: - - SUPPLIER0 - SKU14: - - SUPPLIER0 - SKU140: - - SUPPLIER0 - SKU141: - - SUPPLIER0 - SKU142: - - SUPPLIER0 - SKU143: - - SUPPLIER0 - SKU144: - - SUPPLIER0 - SKU145: - - SUPPLIER0 - SKU146: - - SUPPLIER0 - SKU147: - - SUPPLIER0 - SKU148: - - SUPPLIER0 - SKU149: - - SUPPLIER0 - SKU15: - - SUPPLIER0 - SKU150: - - SUPPLIER0 - SKU151: - - SUPPLIER0 - SKU152: - - SUPPLIER0 - SKU153: - - SUPPLIER0 - SKU154: - - SUPPLIER0 - SKU155: - - SUPPLIER0 - SKU156: - - SUPPLIER0 - SKU157: - - SUPPLIER0 - SKU158: - - SUPPLIER0 - SKU159: - - SUPPLIER0 - SKU16: - - SUPPLIER0 - SKU160: - - SUPPLIER0 - SKU161: - - SUPPLIER0 - SKU162: - - SUPPLIER0 - SKU163: - - SUPPLIER0 - SKU164: - - SUPPLIER0 - SKU165: - - SUPPLIER0 - SKU166: - - SUPPLIER0 - SKU167: - - SUPPLIER0 - SKU168: - - SUPPLIER0 - SKU169: - - SUPPLIER0 - SKU17: - - SUPPLIER0 - SKU170: - - SUPPLIER0 - SKU171: - - SUPPLIER0 - SKU172: - - SUPPLIER0 - SKU173: - - SUPPLIER0 - SKU174: - - SUPPLIER0 - SKU175: - - SUPPLIER0 - SKU176: - - SUPPLIER0 - SKU177: - - SUPPLIER0 - SKU178: - - SUPPLIER0 - SKU179: - - SUPPLIER0 - SKU18: - - SUPPLIER0 - SKU180: - - SUPPLIER0 - SKU181: - - SUPPLIER0 - SKU182: - - SUPPLIER0 - SKU183: - - SUPPLIER0 - SKU184: - - SUPPLIER0 - SKU185: - - SUPPLIER0 - SKU186: - - SUPPLIER0 - SKU187: - - SUPPLIER0 - SKU188: - - SUPPLIER0 - SKU189: - - SUPPLIER0 - SKU19: - - SUPPLIER0 - SKU190: - - SUPPLIER0 - SKU191: - - SUPPLIER0 - SKU192: - - SUPPLIER0 - SKU193: - - SUPPLIER0 - SKU194: - - SUPPLIER0 - SKU195: - - SUPPLIER0 - SKU196: - - SUPPLIER0 - SKU197: - - SUPPLIER0 - SKU198: - - SUPPLIER0 - SKU199: - - SUPPLIER0 - SKU2: - - SUPPLIER0 - SKU20: - - SUPPLIER0 - SKU200: - - SUPPLIER0 - SKU201: - - SUPPLIER0 - SKU202: - - SUPPLIER0 - SKU203: - - SUPPLIER0 - SKU204: - - SUPPLIER0 - SKU205: - - SUPPLIER0 - SKU206: - - SUPPLIER0 - SKU207: - - SUPPLIER0 - SKU208: - - SUPPLIER0 - SKU209: - - SUPPLIER0 - SKU21: - - SUPPLIER0 - SKU210: - - SUPPLIER0 - SKU211: - - SUPPLIER0 - SKU212: - - SUPPLIER0 - SKU213: - - SUPPLIER0 - SKU214: - - SUPPLIER0 - SKU215: - - SUPPLIER0 - SKU216: - - SUPPLIER0 - SKU217: - - SUPPLIER0 - SKU218: - - SUPPLIER0 - SKU219: - - SUPPLIER0 - SKU22: - - SUPPLIER0 - SKU220: - - SUPPLIER0 - SKU221: - - SUPPLIER0 - SKU222: - - SUPPLIER0 - SKU223: - - SUPPLIER0 - SKU224: - - SUPPLIER0 - SKU225: - - SUPPLIER0 - SKU226: - - SUPPLIER0 - SKU227: - - SUPPLIER0 - SKU228: - - SUPPLIER0 - SKU229: - - SUPPLIER0 - SKU23: - - SUPPLIER0 - SKU230: - - SUPPLIER0 - SKU231: - - SUPPLIER0 - SKU232: - - SUPPLIER0 - SKU233: - - SUPPLIER0 - SKU234: - - SUPPLIER0 - SKU235: - - SUPPLIER0 - SKU236: - - SUPPLIER0 - SKU237: - - SUPPLIER0 - SKU238: - - SUPPLIER0 - SKU239: - - SUPPLIER0 - SKU24: - - SUPPLIER0 - SKU240: - - SUPPLIER0 - SKU241: - - SUPPLIER0 - SKU242: - - SUPPLIER0 - SKU243: - - SUPPLIER0 - SKU244: - - SUPPLIER0 - SKU245: - - SUPPLIER0 - SKU246: - - SUPPLIER0 - SKU247: - - SUPPLIER0 - SKU248: - - SUPPLIER0 - SKU249: - - SUPPLIER0 - SKU25: - - SUPPLIER0 - SKU250: - - SUPPLIER0 - SKU251: - - SUPPLIER0 - SKU252: - - SUPPLIER0 - SKU253: - - SUPPLIER0 - SKU254: - - SUPPLIER0 - SKU255: - - SUPPLIER0 - SKU256: - - SUPPLIER0 - SKU257: - - SUPPLIER0 - SKU258: - - SUPPLIER0 - SKU259: - - SUPPLIER0 - SKU26: - - SUPPLIER0 - SKU260: - - SUPPLIER0 - SKU261: - - SUPPLIER0 - SKU262: - - SUPPLIER0 - SKU263: - - SUPPLIER0 - SKU264: - - SUPPLIER0 - SKU265: - - SUPPLIER0 - SKU266: - - SUPPLIER0 - SKU267: - - SUPPLIER0 - SKU268: - - SUPPLIER0 - SKU269: - - SUPPLIER0 - SKU27: - - SUPPLIER0 - SKU270: - - SUPPLIER0 - SKU271: - - SUPPLIER0 - SKU272: - - SUPPLIER0 - SKU273: - - SUPPLIER0 - SKU274: - - SUPPLIER0 - SKU275: - - SUPPLIER0 - SKU276: - - SUPPLIER0 - SKU277: - - SUPPLIER0 - SKU278: - - SUPPLIER0 - SKU279: - - SUPPLIER0 - SKU28: - - SUPPLIER0 - SKU280: - - SUPPLIER0 - SKU281: - - SUPPLIER0 - SKU282: - - SUPPLIER0 - SKU283: - - SUPPLIER0 - SKU284: - - SUPPLIER0 - SKU285: - - SUPPLIER0 - SKU286: - - SUPPLIER0 - SKU287: - - SUPPLIER0 - SKU288: - - SUPPLIER0 - SKU289: - - SUPPLIER0 - SKU29: - - SUPPLIER0 - SKU290: - - SUPPLIER0 - SKU291: - - SUPPLIER0 - SKU292: - - SUPPLIER0 - SKU293: - - SUPPLIER0 - SKU294: - - SUPPLIER0 - SKU295: - - SUPPLIER0 - SKU296: - - SUPPLIER0 - SKU297: - - SUPPLIER0 - SKU298: - - SUPPLIER0 - SKU299: - - SUPPLIER0 - SKU3: - - SUPPLIER0 - SKU30: - - SUPPLIER0 - SKU300: - - SUPPLIER0 - SKU301: - - SUPPLIER0 - SKU302: - - SUPPLIER0 - SKU303: - - SUPPLIER0 - SKU304: - - SUPPLIER0 - SKU305: - - SUPPLIER0 - SKU306: - - SUPPLIER0 - SKU307: - - SUPPLIER0 - SKU308: - - SUPPLIER0 - SKU309: - - SUPPLIER0 - SKU31: - - SUPPLIER0 - SKU310: - - SUPPLIER0 - SKU311: - - SUPPLIER0 - SKU312: - - SUPPLIER0 - SKU313: - - SUPPLIER0 - SKU314: - - SUPPLIER0 - SKU315: - - SUPPLIER0 - SKU316: - - SUPPLIER0 - SKU317: - - SUPPLIER0 - SKU318: - - SUPPLIER0 - SKU319: - - SUPPLIER0 - SKU32: - - SUPPLIER0 - SKU320: - - SUPPLIER0 - SKU321: - - SUPPLIER0 - SKU322: - - SUPPLIER0 - SKU323: - - SUPPLIER0 - SKU324: - - SUPPLIER0 - SKU325: - - SUPPLIER0 - SKU326: - - SUPPLIER0 - SKU327: - - SUPPLIER0 - SKU328: - - SUPPLIER0 - SKU329: - - SUPPLIER0 - SKU33: - - SUPPLIER0 - SKU330: - - SUPPLIER0 - SKU331: - - SUPPLIER0 - SKU332: - - SUPPLIER0 - SKU333: - - SUPPLIER0 - SKU334: - - SUPPLIER0 - SKU335: - - SUPPLIER0 - SKU336: - - SUPPLIER0 - SKU337: - - SUPPLIER0 - SKU338: - - SUPPLIER0 - SKU339: - - SUPPLIER0 - SKU34: - - SUPPLIER0 - SKU340: - - SUPPLIER0 - SKU341: - - SUPPLIER0 - SKU342: - - SUPPLIER0 - SKU343: - - SUPPLIER0 - SKU344: - - SUPPLIER0 - SKU345: - - SUPPLIER0 - SKU346: - - SUPPLIER0 - SKU347: - - SUPPLIER0 - SKU348: - - SUPPLIER0 - SKU349: - - SUPPLIER0 - SKU35: - - SUPPLIER0 - SKU350: - - SUPPLIER0 - SKU351: - - SUPPLIER0 - SKU352: - - SUPPLIER0 - SKU353: - - SUPPLIER0 - SKU354: - - SUPPLIER0 - SKU355: - - SUPPLIER0 - SKU356: - - SUPPLIER0 - SKU357: - - SUPPLIER0 - SKU358: - - SUPPLIER0 - SKU359: - - SUPPLIER0 - SKU36: - - SUPPLIER0 - SKU360: - - SUPPLIER0 - SKU361: - - SUPPLIER0 - SKU362: - - SUPPLIER0 - SKU363: - - SUPPLIER0 - SKU364: - - SUPPLIER0 - SKU365: - - SUPPLIER0 - SKU366: - - SUPPLIER0 - SKU367: - - SUPPLIER0 - SKU368: - - SUPPLIER0 - SKU369: - - SUPPLIER0 - SKU37: - - SUPPLIER0 - SKU370: - - SUPPLIER0 - SKU371: - - SUPPLIER0 - SKU372: - - SUPPLIER0 - SKU373: - - SUPPLIER0 - SKU374: - - SUPPLIER0 - SKU375: - - SUPPLIER0 - SKU376: - - SUPPLIER0 - SKU377: - - SUPPLIER0 - SKU378: - - SUPPLIER0 - SKU379: - - SUPPLIER0 - SKU38: - - SUPPLIER0 - SKU380: - - SUPPLIER0 - SKU381: - - SUPPLIER0 - SKU382: - - SUPPLIER0 - SKU383: - - SUPPLIER0 - SKU384: - - SUPPLIER0 - SKU385: - - SUPPLIER0 - SKU386: - - SUPPLIER0 - SKU387: - - SUPPLIER0 - SKU388: - - SUPPLIER0 - SKU389: - - SUPPLIER0 - SKU39: - - SUPPLIER0 - SKU390: - - SUPPLIER0 - SKU391: - - SUPPLIER0 - SKU392: - - SUPPLIER0 - SKU393: - - SUPPLIER0 - SKU394: - - SUPPLIER0 - SKU395: - - SUPPLIER0 - SKU396: - - SUPPLIER0 - SKU397: - - SUPPLIER0 - SKU398: - - SUPPLIER0 - SKU399: - - SUPPLIER0 - SKU4: - - SUPPLIER0 - SKU40: - - SUPPLIER0 - SKU400: - - SUPPLIER0 - SKU401: - - SUPPLIER0 - SKU402: - - SUPPLIER0 - SKU403: - - SUPPLIER0 - SKU404: - - SUPPLIER0 - SKU405: - - SUPPLIER0 - SKU406: - - SUPPLIER0 - SKU407: - - SUPPLIER0 - SKU408: - - SUPPLIER0 - SKU409: - - SUPPLIER0 - SKU41: - - SUPPLIER0 - SKU410: - - SUPPLIER0 - SKU411: - - SUPPLIER0 - SKU412: - - SUPPLIER0 - SKU413: - - SUPPLIER0 - SKU414: - - SUPPLIER0 - SKU415: - - SUPPLIER0 - SKU416: - - SUPPLIER0 - SKU417: - - SUPPLIER0 - SKU418: - - SUPPLIER0 - SKU419: - - SUPPLIER0 - SKU42: - - SUPPLIER0 - SKU420: - - SUPPLIER0 - SKU421: - - SUPPLIER0 - SKU422: - - SUPPLIER0 - SKU423: - - SUPPLIER0 - SKU424: - - SUPPLIER0 - SKU425: - - SUPPLIER0 - SKU426: - - SUPPLIER0 - SKU427: - - SUPPLIER0 - SKU428: - - SUPPLIER0 - SKU429: - - SUPPLIER0 - SKU43: - - SUPPLIER0 - SKU430: - - SUPPLIER0 - SKU431: - - SUPPLIER0 - SKU432: - - SUPPLIER0 - SKU433: - - SUPPLIER0 - SKU434: - - SUPPLIER0 - SKU435: - - SUPPLIER0 - SKU436: - - SUPPLIER0 - SKU437: - - SUPPLIER0 - SKU438: - - SUPPLIER0 - SKU439: - - SUPPLIER0 - SKU44: - - SUPPLIER0 - SKU440: - - SUPPLIER0 - SKU441: - - SUPPLIER0 - SKU442: - - SUPPLIER0 - SKU443: - - SUPPLIER0 - SKU444: - - SUPPLIER0 - SKU445: - - SUPPLIER0 - SKU446: - - SUPPLIER0 - SKU447: - - SUPPLIER0 - SKU448: - - SUPPLIER0 - SKU449: - - SUPPLIER0 - SKU45: - - SUPPLIER0 - SKU450: - - SUPPLIER0 - SKU451: - - SUPPLIER0 - SKU452: - - SUPPLIER0 - SKU453: - - SUPPLIER0 - SKU454: - - SUPPLIER0 - SKU455: - - SUPPLIER0 - SKU456: - - SUPPLIER0 - SKU457: - - SUPPLIER0 - SKU458: - - SUPPLIER0 - SKU459: - - SUPPLIER0 - SKU46: - - SUPPLIER0 - SKU460: - - SUPPLIER0 - SKU461: - - SUPPLIER0 - SKU462: - - SUPPLIER0 - SKU463: - - SUPPLIER0 - SKU464: - - SUPPLIER0 - SKU465: - - SUPPLIER0 - SKU466: - - SUPPLIER0 - SKU467: - - SUPPLIER0 - SKU468: - - SUPPLIER0 - SKU469: - - SUPPLIER0 - SKU47: - - SUPPLIER0 - SKU470: - - SUPPLIER0 - SKU471: - - SUPPLIER0 - SKU472: - - SUPPLIER0 - SKU473: - - SUPPLIER0 - SKU474: - - SUPPLIER0 - SKU475: - - SUPPLIER0 - SKU476: - - SUPPLIER0 - SKU477: - - SUPPLIER0 - SKU478: - - SUPPLIER0 - SKU479: - - SUPPLIER0 - SKU48: - - SUPPLIER0 - SKU480: - - SUPPLIER0 - SKU481: - - SUPPLIER0 - SKU482: - - SUPPLIER0 - SKU483: - - SUPPLIER0 - SKU484: - - SUPPLIER0 - SKU485: - - SUPPLIER0 - SKU486: - - SUPPLIER0 - SKU487: - - SUPPLIER0 - SKU488: - - SUPPLIER0 - SKU489: - - SUPPLIER0 - SKU49: - - SUPPLIER0 - SKU490: - - SUPPLIER0 - SKU491: - - SUPPLIER0 - SKU492: - - SUPPLIER0 - SKU493: - - SUPPLIER0 - SKU494: - - SUPPLIER0 - SKU495: - - SUPPLIER0 - SKU496: - - SUPPLIER0 - SKU497: - - SUPPLIER0 - SKU498: - - SUPPLIER0 - SKU499: - - SUPPLIER0 - SKU5: - - SUPPLIER0 - SKU50: - - SUPPLIER0 - SKU500: - - SUPPLIER0 - SKU501: - - SUPPLIER0 - SKU502: - - SUPPLIER0 - SKU503: - - SUPPLIER0 - SKU504: - - SUPPLIER0 - SKU505: - - SUPPLIER0 - SKU506: - - SUPPLIER0 - SKU507: - - SUPPLIER0 - SKU508: - - SUPPLIER0 - SKU509: - - SUPPLIER0 - SKU51: - - SUPPLIER0 - SKU510: - - SUPPLIER0 - SKU511: - - SUPPLIER0 - SKU512: - - SUPPLIER0 - SKU513: - - SUPPLIER0 - SKU514: - - SUPPLIER0 - SKU515: - - SUPPLIER0 - SKU516: - - SUPPLIER0 - SKU517: - - SUPPLIER0 - SKU518: - - SUPPLIER0 - SKU519: - - SUPPLIER0 - SKU52: - - SUPPLIER0 - SKU520: - - SUPPLIER0 - SKU521: - - SUPPLIER0 - SKU522: - - SUPPLIER0 - SKU523: - - SUPPLIER0 - SKU524: - - SUPPLIER0 - SKU525: - - SUPPLIER0 - SKU526: - - SUPPLIER0 - SKU527: - - SUPPLIER0 - SKU528: - - SUPPLIER0 - SKU529: - - SUPPLIER0 - SKU53: - - SUPPLIER0 - SKU530: - - SUPPLIER0 - SKU531: - - SUPPLIER0 - SKU532: - - SUPPLIER0 - SKU533: - - SUPPLIER0 - SKU534: - - SUPPLIER0 - SKU535: - - SUPPLIER0 - SKU536: - - SUPPLIER0 - SKU537: - - SUPPLIER0 - SKU538: - - SUPPLIER0 - SKU539: - - SUPPLIER0 - SKU54: - - SUPPLIER0 - SKU540: - - SUPPLIER0 - SKU541: - - SUPPLIER0 - SKU542: - - SUPPLIER0 - SKU543: - - SUPPLIER0 - SKU544: - - SUPPLIER0 - SKU545: - - SUPPLIER0 - SKU546: - - SUPPLIER0 - SKU547: - - SUPPLIER0 - SKU548: - - SUPPLIER0 - SKU549: - - SUPPLIER0 - SKU55: - - SUPPLIER0 - SKU550: - - SUPPLIER0 - SKU551: - - SUPPLIER0 - SKU552: - - SUPPLIER0 - SKU553: - - SUPPLIER0 - SKU554: - - SUPPLIER0 - SKU555: - - SUPPLIER0 - SKU556: - - SUPPLIER0 - SKU557: - - SUPPLIER0 - SKU558: - - SUPPLIER0 - SKU559: - - SUPPLIER0 - SKU56: - - SUPPLIER0 - SKU560: - - SUPPLIER0 - SKU561: - - SUPPLIER0 - SKU562: - - SUPPLIER0 - SKU563: - - SUPPLIER0 - SKU564: - - SUPPLIER0 - SKU565: - - SUPPLIER0 - SKU566: - - SUPPLIER0 - SKU567: - - SUPPLIER0 - SKU568: - - SUPPLIER0 - SKU569: - - SUPPLIER0 - SKU57: - - SUPPLIER0 - SKU570: - - SUPPLIER0 - SKU571: - - SUPPLIER0 - SKU572: - - SUPPLIER0 - SKU573: - - SUPPLIER0 - SKU574: - - SUPPLIER0 - SKU575: - - SUPPLIER0 - SKU576: - - SUPPLIER0 - SKU577: - - SUPPLIER0 - SKU578: - - SUPPLIER0 - SKU579: - - SUPPLIER0 - SKU58: - - SUPPLIER0 - SKU580: - - SUPPLIER0 - SKU581: - - SUPPLIER0 - SKU582: - - SUPPLIER0 - SKU583: - - SUPPLIER0 - SKU584: - - SUPPLIER0 - SKU585: - - SUPPLIER0 - SKU586: - - SUPPLIER0 - SKU587: - - SUPPLIER0 - SKU588: - - SUPPLIER0 - SKU589: - - SUPPLIER0 - SKU59: - - SUPPLIER0 - SKU590: - - SUPPLIER0 - SKU591: - - SUPPLIER0 - SKU592: - - SUPPLIER0 - SKU593: - - SUPPLIER0 - SKU594: - - SUPPLIER0 - SKU595: - - SUPPLIER0 - SKU596: - - SUPPLIER0 - SKU597: - - SUPPLIER0 - SKU598: - - SUPPLIER0 - SKU599: - - SUPPLIER0 - SKU6: - - SUPPLIER0 - SKU60: - - SUPPLIER0 - SKU600: - - SUPPLIER0 - SKU601: - - SUPPLIER0 - SKU602: - - SUPPLIER0 - SKU603: - - SUPPLIER0 - SKU604: - - SUPPLIER0 - SKU605: - - SUPPLIER0 - SKU606: - - SUPPLIER0 - SKU607: - - SUPPLIER0 - SKU608: - - SUPPLIER0 - SKU609: - - SUPPLIER0 - SKU61: - - SUPPLIER0 - SKU610: - - SUPPLIER0 - SKU611: - - SUPPLIER0 - SKU612: - - SUPPLIER0 - SKU613: - - SUPPLIER0 - SKU614: - - SUPPLIER0 - SKU615: - - SUPPLIER0 - SKU616: - - SUPPLIER0 - SKU617: - - SUPPLIER0 - SKU618: - - SUPPLIER0 - SKU619: - - SUPPLIER0 - SKU62: - - SUPPLIER0 - SKU620: - - SUPPLIER0 - SKU621: - - SUPPLIER0 - SKU622: - - SUPPLIER0 - SKU623: - - SUPPLIER0 - SKU624: - - SUPPLIER0 - SKU625: - - SUPPLIER0 - SKU626: - - SUPPLIER0 - SKU627: - - SUPPLIER0 - SKU628: - - SUPPLIER0 - SKU629: - - SUPPLIER0 - SKU63: - - SUPPLIER0 - SKU630: - - SUPPLIER0 - SKU631: - - SUPPLIER0 - SKU632: - - SUPPLIER0 - SKU633: - - SUPPLIER0 - SKU634: - - SUPPLIER0 - SKU635: - - SUPPLIER0 - SKU636: - - SUPPLIER0 - SKU637: - - SUPPLIER0 - SKU638: - - SUPPLIER0 - SKU639: - - SUPPLIER0 - SKU64: - - SUPPLIER0 - SKU640: - - SUPPLIER0 - SKU641: - - SUPPLIER0 - SKU642: - - SUPPLIER0 - SKU643: - - SUPPLIER0 - SKU644: - - SUPPLIER0 - SKU645: - - SUPPLIER0 - SKU646: - - SUPPLIER0 - SKU647: - - SUPPLIER0 - SKU648: - - SUPPLIER0 - SKU649: - - SUPPLIER0 - SKU65: - - SUPPLIER0 - SKU650: - - SUPPLIER0 - SKU651: - - SUPPLIER0 - SKU652: - - SUPPLIER0 - SKU653: - - SUPPLIER0 - SKU654: - - SUPPLIER0 - SKU655: - - SUPPLIER0 - SKU656: - - SUPPLIER0 - SKU657: - - SUPPLIER0 - SKU658: - - SUPPLIER0 - SKU659: - - SUPPLIER0 - SKU66: - - SUPPLIER0 - SKU660: - - SUPPLIER0 - SKU661: - - SUPPLIER0 - SKU662: - - SUPPLIER0 - SKU663: - - SUPPLIER0 - SKU664: - - SUPPLIER0 - SKU665: - - SUPPLIER0 - SKU666: - - SUPPLIER0 - SKU667: - - SUPPLIER0 - SKU668: - - SUPPLIER0 - SKU669: - - SUPPLIER0 - SKU67: - - SUPPLIER0 - SKU670: - - SUPPLIER0 - SKU671: - - SUPPLIER0 - SKU672: - - SUPPLIER0 - SKU673: - - SUPPLIER0 - SKU674: - - SUPPLIER0 - SKU675: - - SUPPLIER0 - SKU676: - - SUPPLIER0 - SKU677: - - SUPPLIER0 - SKU678: - - SUPPLIER0 - SKU679: - - SUPPLIER0 - SKU68: - - SUPPLIER0 - SKU680: - - SUPPLIER0 - SKU681: - - SUPPLIER0 - SKU682: - - SUPPLIER0 - SKU683: - - SUPPLIER0 - SKU684: - - SUPPLIER0 - SKU685: - - SUPPLIER0 - SKU686: - - SUPPLIER0 - SKU687: - - SUPPLIER0 - SKU688: - - SUPPLIER0 - SKU689: - - SUPPLIER0 - SKU69: - - SUPPLIER0 - SKU690: - - SUPPLIER0 - SKU691: - - SUPPLIER0 - SKU692: - - SUPPLIER0 - SKU693: - - SUPPLIER0 - SKU694: - - SUPPLIER0 - SKU695: - - SUPPLIER0 - SKU696: - - SUPPLIER0 - SKU697: - - SUPPLIER0 - SKU698: - - SUPPLIER0 - SKU699: - - SUPPLIER0 - SKU7: - - SUPPLIER0 - SKU70: - - SUPPLIER0 - SKU700: - - SUPPLIER0 - SKU701: - - SUPPLIER0 - SKU702: - - SUPPLIER0 - SKU703: - - SUPPLIER0 - SKU704: - - SUPPLIER0 - SKU705: - - SUPPLIER0 - SKU706: - - SUPPLIER0 - SKU707: - - SUPPLIER0 - SKU708: - - SUPPLIER0 - SKU709: - - SUPPLIER0 - SKU71: - - SUPPLIER0 - SKU710: - - SUPPLIER0 - SKU711: - - SUPPLIER0 - SKU712: - - SUPPLIER0 - SKU713: - - SUPPLIER0 - SKU714: - - SUPPLIER0 - SKU715: - - SUPPLIER0 - SKU716: - - SUPPLIER0 - SKU717: - - SUPPLIER0 - SKU718: - - SUPPLIER0 - SKU719: - - SUPPLIER0 - SKU72: - - SUPPLIER0 - SKU720: - - SUPPLIER0 - SKU721: - - SUPPLIER0 - SKU722: - - SUPPLIER0 - SKU723: - - SUPPLIER0 - SKU724: - - SUPPLIER0 - SKU725: - - SUPPLIER0 - SKU726: - - SUPPLIER0 - SKU727: - - SUPPLIER0 - SKU728: - - SUPPLIER0 - SKU729: - - SUPPLIER0 - SKU73: - - SUPPLIER0 - SKU730: - - SUPPLIER0 - SKU731: - - SUPPLIER0 - SKU732: - - SUPPLIER0 - SKU733: - - SUPPLIER0 - SKU734: - - SUPPLIER0 - SKU735: - - SUPPLIER0 - SKU736: - - SUPPLIER0 - SKU737: - - SUPPLIER0 - SKU738: - - SUPPLIER0 - SKU739: - - SUPPLIER0 - SKU74: - - SUPPLIER0 - SKU740: - - SUPPLIER0 - SKU741: - - SUPPLIER0 - SKU742: - - SUPPLIER0 - SKU743: - - SUPPLIER0 - SKU744: - - SUPPLIER0 - SKU745: - - SUPPLIER0 - SKU746: - - SUPPLIER0 - SKU747: - - SUPPLIER0 - SKU748: - - SUPPLIER0 - SKU749: - - SUPPLIER0 - SKU75: - - SUPPLIER0 - SKU750: - - SUPPLIER0 - SKU751: - - SUPPLIER0 - SKU752: - - SUPPLIER0 - SKU753: - - SUPPLIER0 - SKU754: - - SUPPLIER0 - SKU755: - - SUPPLIER0 - SKU756: - - SUPPLIER0 - SKU757: - - SUPPLIER0 - SKU758: - - SUPPLIER0 - SKU759: - - SUPPLIER0 - SKU76: - - SUPPLIER0 - SKU760: - - SUPPLIER0 - SKU761: - - SUPPLIER0 - SKU762: - - SUPPLIER0 - SKU763: - - SUPPLIER0 - SKU764: - - SUPPLIER0 - SKU765: - - SUPPLIER0 - SKU766: - - SUPPLIER0 - SKU767: - - SUPPLIER0 - SKU768: - - SUPPLIER0 - SKU769: - - SUPPLIER0 - SKU77: - - SUPPLIER0 - SKU770: - - SUPPLIER0 - SKU771: - - SUPPLIER0 - SKU772: - - SUPPLIER0 - SKU773: - - SUPPLIER0 - SKU774: - - SUPPLIER0 - SKU775: - - SUPPLIER0 - SKU776: - - SUPPLIER0 - SKU777: - - SUPPLIER0 - SKU778: - - SUPPLIER0 - SKU779: - - SUPPLIER0 - SKU78: - - SUPPLIER0 - SKU780: - - SUPPLIER0 - SKU781: - - SUPPLIER0 - SKU782: - - SUPPLIER0 - SKU783: - - SUPPLIER0 - SKU784: - - SUPPLIER0 - SKU785: - - SUPPLIER0 - SKU786: - - SUPPLIER0 - SKU787: - - SUPPLIER0 - SKU788: - - SUPPLIER0 - SKU789: - - SUPPLIER0 - SKU79: - - SUPPLIER0 - SKU790: - - SUPPLIER0 - SKU791: - - SUPPLIER0 - SKU792: - - SUPPLIER0 - SKU793: - - SUPPLIER0 - SKU794: - - SUPPLIER0 - SKU795: - - SUPPLIER0 - SKU796: - - SUPPLIER0 - SKU797: - - SUPPLIER0 - SKU798: - - SUPPLIER0 - SKU799: - - SUPPLIER0 - SKU8: - - SUPPLIER0 - SKU80: - - SUPPLIER0 - SKU800: - - SUPPLIER0 - SKU801: - - SUPPLIER0 - SKU802: - - SUPPLIER0 - SKU803: - - SUPPLIER0 - SKU804: - - SUPPLIER0 - SKU805: - - SUPPLIER0 - SKU806: - - SUPPLIER0 - SKU807: - - SUPPLIER0 - SKU808: - - SUPPLIER0 - SKU809: - - SUPPLIER0 - SKU81: - - SUPPLIER0 - SKU810: - - SUPPLIER0 - SKU811: - - SUPPLIER0 - SKU812: - - SUPPLIER0 - SKU813: - - SUPPLIER0 - SKU814: - - SUPPLIER0 - SKU815: - - SUPPLIER0 - SKU816: - - SUPPLIER0 - SKU817: - - SUPPLIER0 - SKU818: - - SUPPLIER0 - SKU819: - - SUPPLIER0 - SKU82: - - SUPPLIER0 - SKU820: - - SUPPLIER0 - SKU821: - - SUPPLIER0 - SKU822: - - SUPPLIER0 - SKU823: - - SUPPLIER0 - SKU824: - - SUPPLIER0 - SKU825: - - SUPPLIER0 - SKU826: - - SUPPLIER0 - SKU827: - - SUPPLIER0 - SKU828: - - SUPPLIER0 - SKU829: - - SUPPLIER0 - SKU83: - - SUPPLIER0 - SKU830: - - SUPPLIER0 - SKU831: - - SUPPLIER0 - SKU832: - - SUPPLIER0 - SKU833: - - SUPPLIER0 - SKU834: - - SUPPLIER0 - SKU835: - - SUPPLIER0 - SKU836: - - SUPPLIER0 - SKU837: - - SUPPLIER0 - SKU838: - - SUPPLIER0 - SKU839: - - SUPPLIER0 - SKU84: - - SUPPLIER0 - SKU840: - - SUPPLIER0 - SKU841: - - SUPPLIER0 - SKU842: - - SUPPLIER0 - SKU843: - - SUPPLIER0 - SKU844: - - SUPPLIER0 - SKU845: - - SUPPLIER0 - SKU846: - - SUPPLIER0 - SKU847: - - SUPPLIER0 - SKU848: - - SUPPLIER0 - SKU849: - - SUPPLIER0 - SKU85: - - SUPPLIER0 - SKU850: - - SUPPLIER0 - SKU851: - - SUPPLIER0 - SKU852: - - SUPPLIER0 - SKU853: - - SUPPLIER0 - SKU854: - - SUPPLIER0 - SKU855: - - SUPPLIER0 - SKU856: - - SUPPLIER0 - SKU857: - - SUPPLIER0 - SKU858: - - SUPPLIER0 - SKU859: - - SUPPLIER0 - SKU86: - - SUPPLIER0 - SKU860: - - SUPPLIER0 - SKU861: - - SUPPLIER0 - SKU862: - - SUPPLIER0 - SKU863: - - SUPPLIER0 - SKU864: - - SUPPLIER0 - SKU865: - - SUPPLIER0 - SKU866: - - SUPPLIER0 - SKU867: - - SUPPLIER0 - SKU868: - - SUPPLIER0 - SKU869: - - SUPPLIER0 - SKU87: - - SUPPLIER0 - SKU870: - - SUPPLIER0 - SKU871: - - SUPPLIER0 - SKU872: - - SUPPLIER0 - SKU873: - - SUPPLIER0 - SKU874: - - SUPPLIER0 - SKU875: - - SUPPLIER0 - SKU876: - - SUPPLIER0 - SKU877: - - SUPPLIER0 - SKU878: - - SUPPLIER0 - SKU879: - - SUPPLIER0 - SKU88: - - SUPPLIER0 - SKU880: - - SUPPLIER0 - SKU881: - - SUPPLIER0 - SKU882: - - SUPPLIER0 - SKU883: - - SUPPLIER0 - SKU884: - - SUPPLIER0 - SKU885: - - SUPPLIER0 - SKU886: - - SUPPLIER0 - SKU887: - - SUPPLIER0 - SKU888: - - SUPPLIER0 - SKU889: - - SUPPLIER0 - SKU89: - - SUPPLIER0 - SKU890: - - SUPPLIER0 - SKU891: - - SUPPLIER0 - SKU892: - - SUPPLIER0 - SKU893: - - SUPPLIER0 - SKU894: - - SUPPLIER0 - SKU895: - - SUPPLIER0 - SKU896: - - SUPPLIER0 - SKU897: - - SUPPLIER0 - SKU898: - - SUPPLIER0 - SKU899: - - SUPPLIER0 - SKU9: - - SUPPLIER0 - SKU90: - - SUPPLIER0 - SKU900: - - SUPPLIER0 - SKU901: - - SUPPLIER0 - SKU902: - - SUPPLIER0 - SKU903: - - SUPPLIER0 - SKU904: - - SUPPLIER0 - SKU905: - - SUPPLIER0 - SKU906: - - SUPPLIER0 - SKU907: - - SUPPLIER0 - SKU908: - - SUPPLIER0 - SKU909: - - SUPPLIER0 - SKU91: - - SUPPLIER0 - SKU910: - - SUPPLIER0 - SKU911: - - SUPPLIER0 - SKU912: - - SUPPLIER0 - SKU913: - - SUPPLIER0 - SKU914: - - SUPPLIER0 - SKU915: - - SUPPLIER0 - SKU916: - - SUPPLIER0 - SKU917: - - SUPPLIER0 - SKU918: - - SUPPLIER0 - SKU919: - - SUPPLIER0 - SKU92: - - SUPPLIER0 - SKU920: - - SUPPLIER0 - SKU921: - - SUPPLIER0 - SKU922: - - SUPPLIER0 - SKU923: - - SUPPLIER0 - SKU924: - - SUPPLIER0 - SKU925: - - SUPPLIER0 - SKU926: - - SUPPLIER0 - SKU927: - - SUPPLIER0 - SKU928: - - SUPPLIER0 - SKU929: - - SUPPLIER0 - SKU93: - - SUPPLIER0 - SKU930: - - SUPPLIER0 - SKU931: - - SUPPLIER0 - SKU932: - - SUPPLIER0 - SKU933: - - SUPPLIER0 - SKU934: - - SUPPLIER0 - SKU935: - - SUPPLIER0 - SKU936: - - SUPPLIER0 - SKU937: - - SUPPLIER0 - SKU938: - - SUPPLIER0 - SKU939: - - SUPPLIER0 - SKU94: - - SUPPLIER0 - SKU940: - - SUPPLIER0 - SKU941: - - SUPPLIER0 - SKU942: - - SUPPLIER0 - SKU943: - - SUPPLIER0 - SKU944: - - SUPPLIER0 - SKU945: - - SUPPLIER0 - SKU946: - - SUPPLIER0 - SKU947: - - SUPPLIER0 - SKU948: - - SUPPLIER0 - SKU949: - - SUPPLIER0 - SKU95: - - SUPPLIER0 - SKU950: - - SUPPLIER0 - SKU951: - - SUPPLIER0 - SKU952: - - SUPPLIER0 - SKU953: - - SUPPLIER0 - SKU954: - - SUPPLIER0 - SKU955: - - SUPPLIER0 - SKU956: - - SUPPLIER0 - SKU957: - - SUPPLIER0 - SKU958: - - SUPPLIER0 - SKU959: - - SUPPLIER0 - SKU96: - - SUPPLIER0 - SKU960: - - SUPPLIER0 - SKU961: - - SUPPLIER0 - SKU962: - - SUPPLIER0 - SKU963: - - SUPPLIER0 - SKU964: - - SUPPLIER0 - SKU965: - - SUPPLIER0 - SKU966: - - SUPPLIER0 - SKU967: - - SUPPLIER0 - SKU968: - - SUPPLIER0 - SKU969: - - SUPPLIER0 - SKU97: - - SUPPLIER0 - SKU970: - - SUPPLIER0 - SKU971: - - SUPPLIER0 - SKU972: - - SUPPLIER0 - SKU973: - - SUPPLIER0 - SKU974: - - SUPPLIER0 - SKU975: - - SUPPLIER0 - SKU976: - - SUPPLIER0 - SKU977: - - SUPPLIER0 - SKU978: - - SUPPLIER0 - SKU979: - - SUPPLIER0 - SKU98: - - SUPPLIER0 - SKU980: - - SUPPLIER0 - SKU981: - - SUPPLIER0 - SKU982: - - SUPPLIER0 - SKU983: - - SUPPLIER0 - SKU984: - - SUPPLIER0 - SKU985: - - SUPPLIER0 - SKU986: - - SUPPLIER0 - SKU987: - - SUPPLIER0 - SKU988: - - SUPPLIER0 - SKU989: - - SUPPLIER0 - SKU99: - - SUPPLIER0 - SKU990: - - SUPPLIER0 - SKU991: - - SUPPLIER0 - SKU992: - - SUPPLIER0 - SKU993: - - SUPPLIER0 - SKU994: - - SUPPLIER0 - SKU995: - - SUPPLIER0 - SKU996: - - SUPPLIER0 - SKU997: - - SUPPLIER0 - SKU998: - - SUPPLIER0 - SKU999: - - SUPPLIER0 diff --git a/examples/supply_chain/topologies/sample1/config.yml b/examples/supply_chain/topologies/sample1/config.yml deleted file mode 100644 index ffcd92d45..000000000 --- a/examples/supply_chain/topologies/sample1/config.yml +++ /dev/null @@ -1,335 +0,0 @@ - -# TODO: which config to inherit -# base: "" - -#core: -# datamodels: "xxx" -# units: "xxx" -# facilities: "xxx" - - -facility_definitions: - # facility definition - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - # if true then will call generate function of class type - is_template: true - # config will be passed to generator as parameters - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - config: - agent_type: "facility" - - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - manufacture: - class: "ManufactureUnit" - config: - agent_type: "producer" - config: - agent_type: "facility" - - RetailerFacility: &retailer_facility - class: "RetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - seller: - class: "SellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: "facility" - - - # definition for outer retailer that read demand from csv files. - # NOTE: make sure you provide a valid file path for each retailer instance. - OuterRetailerFacility: &outerretailer_facility - class: "OuterRetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "OuterSellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: 2 - seller_sampler_type: data # data, model. What kind of sampler to use for seller. - sku_column: "SKU" # SKU column name - price_column: "Price" # Price column name - sale_column: "Sales" # Sales column name - datetime_column: "DT" # Datetime column name - file_path: "/path/to/data.csv" # full path to data file, override by each store instance - - -# common entity/unit definition as reference to simplify the file. -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 100 - -# a normal distribution definition -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -small_storage: &small_storage - # config of data model of this unit - config: - # other config or storage unit - capacity: 10000 - unit_storage_cost: 1 - -midium_storage: &midium_storage - config: - capacity: 20000 - unit_storage_cost: 1 - -huge_storage: &huge_storage - config: - capacity: 30000 - unit_storage_cost: 1 - -# sku list in this world -# this list do not contains price, cost or other facility related attributes, -# but just base info, like name, id, bom -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 12 - # bill of material that used produce current sku, empty means do not need source material - bom: - # key is the source sku name, value is quantity needed to use per time to produce current sku - sku3: 10 - - - id: 2 - name: "sku2" - output_units_per_lot: 1 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - -# world definitions -world: - # here we use reference to make it each to edit. - skus: *sku_definitions - - # facilities in this world - facilities: - - name: "Supplier_001" # name of the facility - # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level - # use the facility definition as base, then we can override configs partially. - definition_ref: "SupplierFacility" - - # sku list of this facility - skus: - sku3: # sku name and attributes needed for this facility - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" # production means this is the output production of this facility - cost: 10 - price: 10 - vlt: 1 - - # configuration of child units. - children: - # config of storage unit - storage: *small_storage - distribution: *normal_distribution - - # products use default config in core.yml - - # config of this facility - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Supplier_002" - definition_ref: "SupplierFacility" - - skus: - sku1: - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" - cost: 10 - price: 100 - vlt: 1 - sku3: - init_stock: 100 - production_rate: 1 - type: "material" - cost: 10 - price: 100 - vlt: 1 - - children: - storage: *small_storage - distribution: *normal_distribution - - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - - skus: - sku1: - init_stock: 1000 - price: 100 - vlt: 1 - sku2: - init_stock: 1000 - price: 100 - vlt: 1 - sku3: - init_stock: 1000 - price: 100 - vlt: 1 - - children: - storage: *huge_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Retailer_001" - definition_ref: "RetailerFacility" - - skus: - sku1: - price: 300 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 # optional - vlt: 1 - sku3: - price: 200 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - sku2: - price: 100 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - - children: - storage: *midium_storage - - config: - order_cost: 0 - # topology used to specify the up/downstream for facilities - # we split it from facility, so that we can support configuration inherit to override it - # for a new topology - # TODO: change the name? - topology: - # key is current facility, value if upstream facilities that will provide a certain sku - Supplier_002: - # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", - # or any other facility in the list - sku3: - - "Supplier_001" - Warehouse_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - Retailer_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - - # map grid definitions - grid: - size: [20, 20] - - # facility position in grid - facilities: - Supplier_001: [0, 0] - Supplier_002: [3, 3] - Warehouse_001: [6, 6] - Retailer_001: [10, 18] - - # cells that un-traversable - blocks: - railroad: - - [10, 10] - - [10, 11] - - [10, 12] - - [11, 12] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 - start_date_time: "2010-12-01" # start time for data in files, for outer retailer diff --git a/examples/supply_chain/topologies/test/config.yml b/examples/supply_chain/topologies/test/config.yml deleted file mode 100644 index e3bd96bc7..000000000 --- a/examples/supply_chain/topologies/test/config.yml +++ /dev/null @@ -1,367 +0,0 @@ - -# TODO: which config to inherit -# base: "" - -#core: -# datamodels: "xxx" -# units: "xxx" -# facilities: "xxx" - - -facility_definitions: - # facility definition - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - # if true then will call generate function of class type - is_template: true - # config will be passed to generator as parameters - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - config: - agent_type: "facility" - - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: "product" - manufacture: - class: "SimpleManufactureUnit" - config: - agent_type: "producer" - config: - agent_type: "facility" - - RetailerFacility: &retailer_facility - class: "RetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: "productstore" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumerstore" - seller: - class: "SellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: "facility" - - - # definition for outer retailer that read demand from csv files. - # NOTE: make sure you provide a valid file path for each retailer instance. - OuterRetailerFacility: &outerretailer_facility - class: "OuterRetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: "consumer" - consumer: - class: "ConsumerUnit" - seller: - class: "OuterSellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: "facility" - seller_sampler_type: data # data, model. What kind of sampler to use for seller. - sku_column: "SKU" # SKU column name - price_column: "Price" # Price column name - sale_column: "Sales" # Sales column name - datetime_column: "DT" # Datetime column name - file_path: "/path/to/data.csv" # full path to data file, override by each store instance - - -# common entity/unit definition as reference to simplify the file. -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 2 - -# a normal distribution definition -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -huge_storage: &huge_storage - # config of data model of this unit - config: - # other config or storage unit - capacity: 10000 - unit_storage_cost: 1 - -medium_storage: &medium_storage - config: - capacity: 5000 - unit_storage_cost: 1 - -small_storage: &small_storage - config: - capacity: 3000 - unit_storage_cost: 1 - -# sku list in this world -# this list do not contains price, cost or other facility related attributes, -# but just base info, like name, id, bom -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 10 - # bill of material that used produce current sku, empty means do not need source material - bom: - # key is the source sku name, value is quantity needed to use per time to produce current sku - sku1: 1 - - - id: 2 - name: "sku2" - output_units_per_lot: 10 - bom: - sku2: 1 - - - id: 3 - name: "sku3" - output_units_per_lot: 10 - bom: - sku3: 1 - - -# world definitions -world: - # here we use reference to make it each to edit. - skus: *sku_definitions - - # facilities in this world - facilities: - - name: "Supplier_001" # name of the facility - # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level - # use the facility definition as base, then we can override configs partially. - definition_ref: "SupplierFacility" - - # sku list of this facility - skus: - sku1: # sku name and attributes needed for this facility - init_stock: 1000 - product_unit_cost: 90 - production_rate: 1000 - type: "production" # production means this is the output production of this facility - cost: 90 - price: 100 - vlt: 2 - sku2: - init_stock: 1000 - product_unit_cost: 150 - production_rate: 1000 - type: "production" - cost: 150 - price: 200 - vlt: 3 - sku3: - init_stock: 1000 - production_rate: 100 - product_unit_cost: 200 - type: "production" - cost: 200 - price: 300 - vlt: 1 - - # configuration of child units. - children: - # config of storage unit - storage: *huge_storage - distribution: *normal_distribution - - # products use default config in core.yml - - # config of this facility - config: - delay_order_penalty: 10 - order_cost: 0 - - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - - skus: - sku1: - init_stock: 1000 - price: 200 - vlt: 2 - sku2: - init_stock: 1000 - price: 200 - vlt: 3 - sku3: - init_stock: 1000 - price: 300 - vlt: 1 - - children: - storage: *medium_storage - distribution: *normal_distribution - config: - delay_order_penalty: 1 - order_cost: 10 - - - name: "Retailer_001" - definition_ref: "RetailerFacility" - - skus: - sku1: - price: 200 - cost: 100 - init_stock: 40 - sale_gamma: 20 - backlog_ratio: 0.1 # optional - vlt: 1 - sku2: - price: 350 - cost: 200 - init_stock: 160 - sale_gamma: 80 - backlog_ratio: 0.1 - vlt: 1 - sku3: - price: 650 - cost: 300 - init_stock: 100 - sale_gamma: 50 - backlog_ratio: 0.1 - vlt: 1 - - children: - storage: *small_storage - - config: - order_cost: 0 - - # topology used to specify the up/downstream for facilities - # we split it from facility, so that we can support configuration inherit to override it - # for a new topology - # TODO: change the name? - topology: - # key is current facility, value if upstream facilities that will provide a certain sku - Warehouse_001: - sku1: - - "Supplier_001" - sku2: - - "Supplier_001" - sku3: - - "Supplier_001" - Retailer_001: - sku1: - - "Warehouse_001" - sku2: - - "Warehouse_001" - sku3: - - "Warehouse_001" - - # map grid definitions - grid: - size: [20, 20] - - # facility position in grid - facilities: - Supplier_001: [0, 0] - Warehouse_001: [6, 6] - Retailer_001: [10, 18] - - # cells that un-traversable - blocks: - railroad: - - [10, 10] - - [10, 11] - - [10, 12] - - [11, 12] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e6 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 - start_date_time: "2010-12-01" # start time for data in files, for outer retailer diff --git a/maro/rl/env_wrapper.py b/maro/rl/env_wrapper.py deleted file mode 100644 index 9ce855844..000000000 --- a/maro/rl/env_wrapper.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from abc import ABC, abstractmethod -from collections import defaultdict, deque -from typing import List, Union - -from maro.rl.experience import Replay -from maro.simulator import Env - - -class AbsEnvWrapper(ABC): - """Environment wrapper that performs various shaping and other roll-out related logic. - - Args: - env (Env): Environment instance. - save_replay (bool): If True, the steps during roll-out will be recorded sequentially. This - includes states, actions and rewards. The decision events themselves will also be recorded - for delayed reward evaluation purposes. Defaults to True. - reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward - for the action taken for that event. Defaults to 0, which rewards are evaluated immediately - after executing an action. - """ - def __init__(self, env: Env, save_replay: bool = True, reward_eval_delay: int = 0): - self.env = env - self.replay = defaultdict(Replay) - self.state_info = None # context for converting model output to actions that can be executed by the env - self.save_replay = save_replay - self.reward_eval_delay = reward_eval_delay - self.pending_reward_ticks = deque() # list of ticks whose actions have not been given a reward - self.action_history = {} # store the tick-to-action mapping - self._step_index = None - self._total_reward = 0 - self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. - self._state = None # the latest extracted state is kept here - - @property - def step_index(self): - return self._step_index - - @property - def agent_idx_list(self): - return self.env.agent_idx_list - - def start(self): - self._step_index = 0 - _, self._event, _ = self.env.step(None) - self._state = self.get_state(self.env.tick) - - @property - def metrics(self): - return self.env.metrics - - @property - def state(self): - return self._state - - @property - def event(self): - return self._event - - @property - def total_reward(self): - return self._total_reward - - @abstractmethod - def get_state(self, tick: int = None) -> dict: - """Compute the state for a given tick. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - """ - pass - - @abstractmethod - def get_action(self, action) -> dict: - pass - - @abstractmethod - def get_reward(self, tick: int = None) -> dict: - """User-defined reward evaluation. - - Args: - tick (int): If given, the action that occured at this tick will be evaluated (useful for delayed - reward evaluation). Otherwise, the reward is evaluated for the latest action. Defaults to None. - """ - pass - - def step(self, action_by_agent: dict): - # t0 = time.time() - self._step_index += 1 - env_action = self.get_action(action_by_agent) - self.pending_reward_ticks.append(self.env.tick) - self.action_history[self.env.tick] = action_by_agent - if len(env_action) == 1: - env_action = list(env_action.values())[0] - # t1 = time.time() - _, self._event, done = self.env.step(env_action) - # t2 = time.time() - # self._tot_raw_step_time += t2 - t1 - - """ - If roll-out is complete, evaluate rewards for all remaining events except the last. - Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ - while ( - self.pending_reward_ticks and - (done or self.env.tick - self.pending_reward_ticks[0] >= self.reward_eval_delay) - ): - tick = self.pending_reward_ticks.popleft() - reward = self.get_reward(tick=tick) - # assign rewards to the agents that took action at that tick - for agent_id in self.action_history[tick]: - rw = reward.get(agent_id, 0) - if not done and self.save_replay: - self.replay[agent_id].rewards.append(rw) - self._total_reward += rw - - if not done: - prev_state = self._state - self._state = self.get_state(self.env.tick) - if self.save_replay: - for agent_id, state in prev_state.items(): - self.replay[agent_id].states.append(state) - self.replay[agent_id].actions.append(action_by_agent[agent_id]) - # t3 = time.time() - # self._tot_step_time += t3 - t0 - else: - self._state = None - self.end_of_episode() - - # print(f"total raw step time: {self._tot_raw_step_time}") - # print(f"total step time: {self._tot_step_time}") - # self._tot_raw_step_time = 0 - # self._tot_step_time = 0 - - def end_of_episode(self): - pass - - def get_experiences(self): - return {agent_id: replay.to_experience_set() for agent_id, replay in self.replay.items()} - - def reset(self): - self.env.reset() - self.state_info = None - self._total_reward = 0 - self._state = None - self.pending_reward_ticks.clear() - self.action_history.clear() - self.replay = defaultdict(Replay) diff --git a/maro/rl/experience/experience.py b/maro/rl/experience/experience.py deleted file mode 100644 index ee58c2721..000000000 --- a/maro/rl/experience/experience.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.utils.exception.rl_toolkit_exception import InvalidExperience - - -class ExperienceSet: - - __slots__ = ["states", "actions", "rewards", "next_states"] - - def __init__(self, states: list, actions: list, rewards: list, next_states: list): - if not len(states) == len(actions) == len(rewards) == len(next_states): - raise InvalidExperience("values of contents should consist of lists of the same length") - self.states = states - self.actions = actions - self.rewards = rewards - self.next_states = next_states - - def __len__(self): - return len(self.states) - - -class Replay(object): - def __init__(self): - self.states = [] - self.actions = [] - self.rewards = [] - - def to_experience_set(self): - # print(len(self.rewards), len(self.states)) - num_complete = min(len(self.rewards), len(self.states) - 1) - exp_set = ExperienceSet( - self.states[:num_complete], - self.actions[:num_complete], - self.rewards[:num_complete], - self.states[1:num_complete + 1] - ) - - del self.states[:num_complete] - del self.actions[:num_complete] - del self.rewards[:num_complete] - - return exp_set diff --git a/maro/rl/training/actor_manager.py b/maro/rl/training/actor_manager.py deleted file mode 100644 index 7e18a1ebc..000000000 --- a/maro/rl/training/actor_manager.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from os import getcwd -from random import choices - -from maro.communication import Proxy, SessionType -from maro.utils import Logger - -from .message_enums import MsgTag, MsgKey - - -class ActorManager(object): - """Learner class for distributed training. - - Args: - agent (Union[AbsPolicy, MultiAgentWrapper]): Learning agents. - scheduler (Scheduler): . - num_actors (int): Expected number of actors in the group identified by ``group_name``. - group_name (str): Identifier of the group to which the actor belongs. It must be the same group name - assigned to the actors (and roll-out clients, if any). - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to None. - update_trigger (str): Number or percentage of ``MsgTag.ROLLOUT_DONE`` messages required to trigger - learner updates, i.e., model training. - """ - def __init__( - self, - num_actors: int, - group_name: str, - required_finishes: int = None, - log_env_metrics: bool = False, - log_dir: str = getcwd(), - **proxy_kwargs - ): - super().__init__() - self._logger = Logger("ACTOR_MANAGER", dump_folder=log_dir) - self.num_actors = num_actors - peers = {"actor": num_actors} - self._proxy = Proxy(group_name, "actor_manager", peers, **proxy_kwargs) - self._actors = self._proxy.peers["actor"] # remote actor ID's - - if required_finishes and required_finishes > self.num_actors: - raise ValueError("required_finishes cannot exceed the number of available actors") - - if required_finishes is None: - required_finishes = self.num_actors - self._logger.info(f"Required number of actor finishes is set to {required_finishes}") - - self.required_finishes = required_finishes - self.total_experiences_collected = 0 - self.total_env_steps = 0 - self.total_reward = defaultdict(float) - self._log_env_metrics = log_env_metrics - - def collect( - self, - episode_index: int, - segment_index: int, - num_steps: int, - policy_dict: dict = None, - discard_stale_experiences: bool = True, - return_env_metrics: bool = False - ): - """Collect experiences from actors.""" - msg_body = { - MsgKey.EPISODE_INDEX: episode_index, - MsgKey.SEGMENT_INDEX: segment_index, - MsgKey.NUM_STEPS: num_steps, - MsgKey.POLICY: policy_dict, - MsgKey.RETURN_ENV_METRICS: return_env_metrics - } - - if self._log_env_metrics: - self._logger.info(f"EPISODE-{episode_index}, SEGMENT-{segment_index}: ") - - self._proxy.ibroadcast("actor", MsgTag.COLLECT, SessionType.TASK, body=msg_body) - self._logger.info(f"Sent collect requests for ep-{episode_index}, segment-{segment_index}") - - # Receive roll-out results from remote actors - num_finishes = 0 - for msg in self._proxy.receive(): - if msg.tag != MsgTag.COLLECT_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: - self._logger.info( - f"Ignore a message of type {msg.tag} with roll-out index {msg.body[MsgKey.EPISODE_INDEX]} " - f"(expected message type {MsgTag.COLLECT} and roll-out index {episode_index})" - ) - continue - - # log roll-out summary - if self._log_env_metrics: - env_metrics = msg.body[MsgKey.METRICS] - self._logger.info(f"env_metrics: {env_metrics}") - - if msg.body[MsgKey.SEGMENT_INDEX] == segment_index or not discard_stale_experiences: - experiences_by_agent = msg.body[MsgKey.EXPERIENCES] - self.total_experiences_collected += sum(len(exp) for exp in experiences_by_agent.values()) - self.total_env_steps += msg.body[MsgKey.NUM_STEPS] - is_env_end = msg.body[MsgKey.ENV_END] - if is_env_end: - self._logger.info(f"total rewards: {msg.body[MsgKey.TOTAL_REWARD]}") - yield experiences_by_agent, is_env_end - - if msg.body[MsgKey.SEGMENT_INDEX] == segment_index: - num_finishes += 1 - if num_finishes == self.required_finishes: - break - - def evaluate(self, episode_index: int, policy_dict: dict, num_actors: int): - """Collect experiences from actors.""" - msg_body = { - MsgKey.EPISODE_INDEX: episode_index, - MsgKey.POLICY: policy_dict, - MsgKey.RETURN_ENV_METRICS: True - } - - actors = choices(self._actors, k=num_actors) - self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(actor_id, msg_body) for actor_id in actors]) - self._logger.info(f"Sent evaluation requests to {actors}") - - # Receive roll-out results from remote actors - num_finishes = 0 - for msg in self._proxy.receive(): - if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE_INDEX] != episode_index: - self._logger.info( - f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE_INDEX]} " - f"(expected message type {MsgTag.EVAL} and episode index {episode_index})" - ) - continue - - # log roll-out summary - env_metrics = msg.body[MsgKey.METRICS] - self._logger.info(f"env metrics for evaluation episode {episode_index}: {env_metrics}") - - if msg.body[MsgKey.EPISODE_INDEX] == episode_index: - num_finishes += 1 - if num_finishes == num_actors: - break - - def exit(self): - """Tell the remote actors to exit.""" - self._proxy.ibroadcast("actor", MsgTag.EXIT, SessionType.NOTIFICATION) - self._logger.info("Exiting...") diff --git a/maro/rl/training/distributed_learner.py b/maro/rl/training/distributed_learner.py deleted file mode 100644 index 82600ec87..000000000 --- a/maro/rl/training/distributed_learner.py +++ /dev/null @@ -1,188 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from collections import defaultdict -from os import getcwd -from typing import Dict, List, Union - -from maro.rl.env_wrapper import AbsEnvWrapper -from maro.rl.policy import AbsPolicy, AbsCorePolicy -from maro.utils import Logger - -from .actor_manager import ActorManager -from .policy_update_schedule import EpisodeBasedSchedule, MultiPolicyUpdateSchedule, StepBasedSchedule - - -class DistributedLearner(object): - """Learner class for distributed training. - - Args: - policy (MultiAgentPolicy): Learning agents. - - """ - def __init__( - self, - policy_dict: Dict[str, AbsPolicy], - agent_to_policy: Dict[str, str], - num_episodes: int, - policy_update_schedule: Union[EpisodeBasedSchedule, StepBasedSchedule, dict], - actor_manager: ActorManager, - experience_update_interval: int = -1, - eval_env: AbsEnvWrapper = None, - eval_schedule: Union[int, List[int]] = None, - num_eval_actors: int = 1, - discard_stale_experiences: bool = True, - log_env_metrics: bool = True, - log_dir: str = getcwd(), - **end_of_episode_kwargs - ): - self._logger = Logger("LEARNER", dump_folder=log_dir) - self.policy_dict = policy_dict - self.agent_to_policy = agent_to_policy - self.policy = {agent_id: policy_dict[policy_id] for agent_id, policy_id in self.agent_to_policy.items()} - self.agent_groups_by_policy = defaultdict(list) - for agent_id, policy_id in agent_to_policy.items(): - self.agent_groups_by_policy[policy_id].append(agent_id) - - for policy_id, agent_ids in self.agent_groups_by_policy.items(): - self.agent_groups_by_policy[policy_id] = tuple(agent_ids) - - self.num_episodes = num_episodes - self.policy_update_schedule = MultiPolicyUpdateSchedule(policy_update_schedule) - if isinstance(policy_update_schedule, dict): - self._updatable_policy_ids = list(policy_update_schedule.keys()) - else: - self._updatable_policy_ids = [ - policy_id for policy_id, policy in self.policy_dict.items() if hasattr(policy, "update") - ] - self._updated_policy_ids = self._updatable_policy_ids - - self.experience_update_interval = experience_update_interval - - # evaluation - self.eval_env = eval_env - self.num_eval_actors = num_eval_actors - - if eval_schedule is None: - eval_schedule = [] - elif isinstance(eval_schedule, int): - num_eval_schedule = num_episodes // eval_schedule - eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - - self.eval_schedule = eval_schedule - self.eval_schedule.sort() - if not self.eval_schedule or num_episodes != self.eval_schedule[-1]: - self.eval_schedule.append(num_episodes) - - self._logger.info(f"Policy will be evaluated at the end of episodes {self.eval_schedule}") - self._eval_point_index = 0 - - self.actor_manager = actor_manager - self.discard_stale_experiences = discard_stale_experiences - - self.end_of_episode_kwargs = end_of_episode_kwargs - self._log_env_metrics = log_env_metrics - - self._action_dict = {} - - def run(self): - for ep in range(1, self.num_episodes + 1): - self._logger.info(f"Learner episode: {ep}") - self._train(ep) - - policy_ids = self.policy_update_schedule.pop_episode(ep) - if policy_ids == ["*"]: - policy_ids = self._updatable_policy_ids - - for policy_id in policy_ids: - self.policy_dict[policy_id].update() - - if policy_ids: - self._logger.info(f"Updated policies {policy_ids} at the end of episode {ep}") - - self._updated_policy_ids = policy_ids - self.end_of_episode(ep, **self.end_of_episode_kwargs) - - if ep == self.eval_schedule[self._eval_point_index]: - self._eval_point_index += 1 - self._evaluate(self._eval_point_index) - - if self.actor_manager: - self.actor_manager.exit() - - def _train(self, ep: int): - t0 = time.time() - learning_time = 0 - env_steps = 0 - num_experiences_collected = 0 - num_actor_finishes, segment_index = 0, 1 - - while num_actor_finishes < self.actor_manager.required_finishes: - # parallel experience collection - for exp_by_agent, done in self.actor_manager.collect( - ep, segment_index, self.experience_update_interval, - policy_dict={policy_id: self.policy_dict[policy_id].get_state() for policy_id in self._updated_policy_ids}, - discard_stale_experiences=self.discard_stale_experiences - ): - for agent_id, exp in exp_by_agent.items(): - if isinstance(self.policy[agent_id], AbsCorePolicy): - self.policy[agent_id].experience_manager.put(exp) - if agent_id not in self._action_dict.keys(): - self._action_dict[agent_id] = {} - for a in exp.actions: - self._action_dict[agent_id][a] = self._action_dict[agent_id].get(a, 0) + 1 - - env_steps += self.experience_update_interval - num_experiences_collected += sum(len(exp) for exp in exp_by_agent.values()) - num_actor_finishes += done - - # policy update - tl0 = time.time() - policy_ids = self.policy_update_schedule.pop_step(ep, segment_index) - if policy_ids == ["*"]: - policy_ids = self._updatable_policy_ids - for policy_id in policy_ids: - self.policy_dict[policy_id].update() - - if policy_ids: - self._logger.info(f"Updated policies {policy_ids} after segment {segment_index}") - - self._updated_policy_ids = policy_ids - learning_time += time.time() - tl0 - - segment_index += 1 - - # performance details - self._logger.debug( - f"ep {ep} summary - " - f"running time: {time.time() - t0} - " - f"env steps: {env_steps} - " - f"learning time: {learning_time} - " - f"experiences collected: {num_experiences_collected} - ", - f"action dist: {self._action_dict} - " - ) - - def _evaluate(self, ep: int): - self._logger.info("Evaluating...") - if self.eval_env: - self.eval_env.save_replay = False - self.eval_env.reset() - self.eval_env.start() # get initial state - while self.eval_env.state: - action = {id_: self.policy[id_].choose_action(st) for id_, st in self.eval_env.state.items()} - self.eval_env.step(action) - - if not self.eval_env.state: - self._logger.info(f"total reward: {self.eval_env.total_reward}") - - if self._log_env_metrics: - self._logger.info(f"eval ep {ep}: {self.eval_env.metrics}") - else: - policy_state = { - policy_id: self.policy_dict[policy_id].get_state() for policy_id in self._updatable_policy_ids - } - self.actor_manager.evaluate(ep, policy_state, self.num_eval_actors) - - def end_of_episode(self, ep: int, **kwargs): - pass diff --git a/maro/rl/training/policy_update_schedule.py b/maro/rl/training/policy_update_schedule.py deleted file mode 100644 index c32f02d28..000000000 --- a/maro/rl/training/policy_update_schedule.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict, namedtuple -from typing import List, Union - -EpisodeBasedSchedule = namedtuple("EpisodeBasedSchedule", ["start", "interval"]) -StepBasedSchedule = namedtuple("StepBasedSchedule", ["start_ep", "interval", "end_ep_update"]) - - -class MultiPolicyUpdateSchedule: - """ - """ - def __init__(self, schedule_option: Union[EpisodeBasedSchedule, StepBasedSchedule, dict] = -1): - self._pending_steps = defaultdict(set) - self._pending_episodes = defaultdict(set) - self._step_schedule_opt = {} - self._episode_schedule_opt = {} - - if isinstance(schedule_option, dict): - for policy_id, sch in schedule_option.items(): - if isinstance(sch, StepBasedSchedule): - self._step_schedule_opt[policy_id] = (sch.start_ep, sch.interval) - if sch.end_ep_update: - self._episode_schedule_opt[policy_id] = (sch.start_ep, 1) - self._pending_episodes[sch.start_ep].add(policy_id) - self._pending_steps[sch.interval].add(policy_id) - elif isinstance(sch, EpisodeBasedSchedule): - self._episode_schedule_opt[policy_id] = (sch.start, sch.interval) - self._pending_episodes[sch.start].add(policy_id) - else: - if isinstance(schedule_option, StepBasedSchedule): - self._step_schedule_opt["*"] = (schedule_option.start_ep, schedule_option.interval) - self._pending_steps[schedule_option.interval].add("*") - if schedule_option.end_ep_update: - self._episode_schedule_opt["*"] = (schedule_option.start_ep, 1) - self._pending_episodes[schedule_option.start_ep].add("*") - else: - self._episode_schedule_opt["*"] = (schedule_option.start, schedule_option.interval) - self._pending_episodes[schedule_option.start].add("*") - - self._episode = None - - def pop_step(self, ep: int, step: int) -> List[str]: - for policy_id in self._pending_steps[step]: - next_step = step + self._step_schedule_opt[policy_id][1] - self._pending_steps[next_step].add(policy_id) - - return [ - policy_id for policy_id in self._pending_steps[step] if self._step_schedule_opt[policy_id][0] <= ep - ] - - def pop_episode(self, ep: int) -> List[str]: - for policy_id in self._pending_episodes[ep]: - next_ep = ep + self._episode_schedule_opt[policy_id][1] - self._pending_episodes[next_ep].add(policy_id) - - return list(self._pending_episodes[ep]) From c2c1c62521ec70c45ea821b6279d44a7c7a85d08 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 16 Jul 2021 10:16:48 +0000 Subject: [PATCH 388/482] 1. made logger internal in learner; 2 removed logger creation in abs classes --- examples/rl/workflows/config.yml | 4 +- maro/rl/learning/synchronous/learner.py | 6 +-- .../learning/synchronous/rollout_manager.py | 33 ++++------------ maro/rl/policy/policy_manager.py | 38 ++++++------------- 4 files changed, 24 insertions(+), 57 deletions(-) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index f4a2d2d03..3dd71f6a2 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -15,9 +15,9 @@ rollout_experience_distribution: false sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 4 + num_rollout_workers: 6 min_finished_workers: 4 - # max_extra_recv_tries: 3 + max_extra_recv_tries: 2 extra_recv_timeout: 100 async: group: async diff --git a/maro/rl/learning/synchronous/learner.py b/maro/rl/learning/synchronous/learner.py index 87e496ac0..b7e56db44 100644 --- a/maro/rl/learning/synchronous/learner.py +++ b/maro/rl/learning/synchronous/learner.py @@ -45,7 +45,7 @@ def __init__( early_stopper: AbsEarlyStopper = None, log_dir: str = getcwd() ): - self.logger = Logger("LEARNER", dump_folder=log_dir) + self._logger = Logger("LEARNER", dump_folder=log_dir) self.policy_manager = policy_manager self.rollout_manager = rollout_manager @@ -65,7 +65,7 @@ def __init__( if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: self._eval_schedule.append(num_episodes) - self.logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") + self._logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") self._eval_point_index = 0 self.early_stopper = early_stopper @@ -108,7 +108,7 @@ def _collect_and_update(self, ep: int): num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) # performance details - self.logger.info( + self._logger.info( f"ep {ep} summary - " f"experiences collected: {num_experiences_collected} " f"experience collection time: {collect_time} " diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 15e813002..4f4774c43 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -30,20 +30,11 @@ class AbsRolloutManager(ABC): environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. """ - def __init__( - self, - post_collect: Callable = None, - post_evaluate: Callable = None, - log_dir: str = getcwd() - ): + def __init__(self, post_collect: Callable = None, post_evaluate: Callable = None): super().__init__() self._post_collect = post_collect self._post_evaluate = post_evaluate - self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) self.episode_complete = False @abstractmethod @@ -114,11 +105,7 @@ def __init__( if num_steps == 0 or num_steps < -1: raise ValueError("num_steps must be a positive integer or -1") - super().__init__( - post_collect=post_collect, - post_evaluate=post_evaluate, - log_dir=log_dir - ) + super().__init__(post_collect=post_collect, post_evaluate=post_evaluate) self.env = env_wrapper self.eval_env = eval_env_wrapper if eval_env_wrapper else self.env @@ -126,6 +113,8 @@ def __init__( self._num_steps = num_steps if num_steps > 0 else float("inf") + self._logger = Logger("LOCAL_ROLLOUT_MANAGER", dump_folder=log_dir) + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): """Collect simulation data, i.e., experiences for training. @@ -241,15 +230,12 @@ def __init__( post_evaluate: Callable = None, log_dir: str = getcwd() ): - super().__init__( - post_collect=post_collect, - post_evaluate=post_evaluate, - log_dir=log_dir - ) + super().__init__(post_collect=post_collect, post_evaluate=post_evaluate) self._num_workers = num_workers self._num_steps = num_steps self._num_eval_workers = num_eval_workers self._exploration_step = False + self._logger = Logger("MULTIPROCESS_ROLLOUT_MANAGER", dump_folder=log_dir) self._worker_processes = [] self._manager_ends = [] @@ -402,15 +388,12 @@ def __init__( if num_eval_workers > num_workers: raise ValueError("num_eval_workers cannot exceed the number of available workers") - super().__init__( - post_collect=post_collect, - post_evaluate=post_evaluate, - log_dir=log_dir - ) + super().__init__(post_collect=post_collect, post_evaluate=post_evaluate) self._num_workers = num_workers peers = {"rollout_worker": num_workers} self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's + self._logger = Logger("MULTINODE_ROLLOUT_MANAGER", dump_folder=log_dir) self._num_steps = num_steps if min_finished_workers is None: diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 68fdf7e2f..f4224a7a0 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -34,24 +34,19 @@ class AbsPolicyManager(ABC): trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. """ def __init__( self, policy_dict: Dict[str, AbsCorePolicy], update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, - post_update: Callable = None, - log_dir: str = getcwd() + post_update: Callable = None ): for policy in policy_dict.values(): if not isinstance(policy, AbsCorePolicy): raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") super().__init__() - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self.policy_dict = policy_dict if not update_trigger: self.update_trigger = {name: 1 for name in self.policy_dict} @@ -112,14 +107,9 @@ def __init__( post_update: Callable = None, log_dir: str = getcwd() ): - super().__init__( - policy_dict, - update_trigger=update_trigger, - warmup=warmup, - post_update=post_update, - log_dir=log_dir - ) + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) self._new_exp_counter = defaultdict(int) + self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) def update(self, exp_by_policy: Dict[str, ExperienceSet]): """Store experiences and update policies if possible. @@ -183,13 +173,7 @@ def __init__( post_update: Callable = None, log_dir: str = getcwd(), ): - super().__init__( - policy_dict, - update_trigger=update_trigger, - warmup=warmup, - post_update=post_update, - log_dir=log_dir - ) + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) self._policy2trainer = {} self._trainer2policies = defaultdict(list) self._exp_cache = defaultdict(ExperienceSet) @@ -200,6 +184,8 @@ def __init__( self._policy2trainer[name] = f"TRAINER.{trainer_id}" self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) + self._logger = Logger("MULTIPROCESS_POLICY_MANAGER", dump_folder=log_dir) + self._trainer_processes = [] self._manager_end = {} for trainer_id, policy_names in self._trainer2policies.items(): @@ -291,13 +277,7 @@ def __init__( log_dir: str = getcwd(), proxy_kwargs: dict = {} ): - super().__init__( - policy_dict, - update_trigger=update_trigger, - warmup=warmup, - post_update=post_update, - log_dir=log_dir - ) + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) peers = {"trainer": num_trainers} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) @@ -306,10 +286,14 @@ def __init__( self._exp_cache = defaultdict(ExperienceSet) self._num_experiences_by_policy = defaultdict(int) + self._logger = Logger("MULTINODE_POLICY_MANAGER", dump_folder=log_dir) + for i, name in enumerate(self.policy_dict): trainer_id = i % num_trainers self._policy2trainer[name] = f"TRAINER.{trainer_id}" self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) + + self._logger.info("Initializing policy states on trainers...") for trainer_name, policy_names in self._trainer2policies.items(): self._proxy.send( SessionMessage( From 0a051842a4c8142d030a981b6e73ace5dbd1e6e1 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 16 Jul 2021 11:13:53 +0000 Subject: [PATCH 389/482] checked out supply chain test files from v0.2_sc --- tests/supply_chain/simple_seller.py | 7 + tests/supply_chain/test_supply_chain.py | 1459 +++++++++++++++++++++++ 2 files changed, 1466 insertions(+) create mode 100644 tests/supply_chain/simple_seller.py create mode 100644 tests/supply_chain/test_supply_chain.py diff --git a/tests/supply_chain/simple_seller.py b/tests/supply_chain/simple_seller.py new file mode 100644 index 000000000..04e3c72e1 --- /dev/null +++ b/tests/supply_chain/simple_seller.py @@ -0,0 +1,7 @@ + +from maro.simulator.scenarios.supply_chain import SellerUnit + + +class SimpleSellerUnit(SellerUnit): + def market_demand(self, tick: int) -> int: + return tick diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py new file mode 100644 index 000000000..1260a0941 --- /dev/null +++ b/tests/supply_chain/test_supply_chain.py @@ -0,0 +1,1459 @@ +import os +import unittest + +import numpy as np + +from maro.simulator import Env +from maro.simulator.scenarios.supply_chain import ManufactureAction, ConsumerAction +from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase, VehicleUnit, \ + DistributionUnit, SellerUnit +from maro.simulator.scenarios.supply_chain.units.order import Order + + +def build_env(case_name: str, durations: int): + case_folder = os.path.join("tests", "data", "supply_chain", case_name) + + # config_path = os.path.join(case_folder, "config.yml") + + env = Env(scenario="supply_chain", topology=case_folder, durations=durations) + + return env + + +def get_product_dict_from_storage(env: Env, frame_index: int, node_index: int): + product_list = env.snapshot_list["storage"][frame_index:node_index:"product_list"].flatten().astype(np.int) + product_number = env.snapshot_list["storage"][frame_index:node_index:"product_number"].flatten().astype(np.int) + + return {pid: pnum for pid, pnum in zip(product_list, product_number)} + + +SKU1_ID = 1 +SKU2_ID = 2 +SKU3_ID = 3 +SKU4_ID = 4 + + +class MyTestCase(unittest.TestCase): + """ + manufacture unit testing: + + 1. with input sku + . meet the storage limitation + . not meet the storage limitation + . with enough source sku + . without enough source sku + . with product rate + . without product rate + 2. without input sku + . meet the storage limitation + . not meet the storage limitation + . with product rate + . without product rate + + """ + + def test_manufacture_meet_storage_limitation(self): + """Test sku3 manufacturing.""" + env = build_env("case_01", 100) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "facility_id", "capacity", "remaining_space") + + manufacture_nodes = env.snapshot_list["manufacture"] + manufacture_number = len(manufacture_nodes) + manufacture_features = ( + "id", "facility_id", "manufacturing_number", "product_id", + "product_unit_cost" + ) + + ############################### TICK: 0 ###################################### + + # tick 0 passed, no product manufacturing. + env.step(None) + + states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, + -1).astype(np.int) + + # try to find which one is sku3 manufacture unit. + for index, state in enumerate(states): + # Id of sku3 is 3. + if state[3] == SKU3_ID: + sku3_data_model_index = index + sku3_manufacture_id = state[0] + sku3_facility_id = state[1] + + # try to find sku3's storage from env.summary + sku3_storage_index = env.summary["node_mapping"]["facilities"][sku3_facility_id]["units"]["storage"]["node_index"] + + storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) + + # there should be 80 units been taken at the beginning according to the config file. + # so remaining space should be 20 + self.assertEqual(20, storage_states[3]) + # capacity is 100 by config + self.assertEqual(100, storage_states[2]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) + + # number should be same as configuration at beginning. + # 80 sku3 + self.assertEqual(80, product_dict[SKU3_ID]) + + # all the id is greater than 0 + self.assertGreater(sku3_manufacture_id, 0) + + ############################### TICK: 1 ###################################### + + # pass an action to start manufacturing for this tick. + action = ManufactureAction(sku3_manufacture_id, 1) + + env.step({action.id: action}) + + states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) + + # Sku3 produce rate is 1 per tick, so manufacturing_number should be 1. + self.assertEqual(1, states[2]) + + storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) + + # now remaining space should be 19 + self.assertEqual(19, storage_states[3]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) + + # sku3 number should be 80 + 1 + self.assertEqual(80 + 1, product_dict[SKU3_ID]) + + ############################### TICK: 2 ###################################### + + # leave the action as none will cause manufacture unit stop manufacturing. + env.step(None) + + states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) + + # so manufacturing_number should be 0 + self.assertEqual(0, states[2]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) + + # sku3 number should be same as last tick + self.assertEqual(80 + 1, product_dict[SKU3_ID]) + + # let is generate 20, but actually it can only procedure 19 because the storage will reach the limitation + env.step({sku3_manufacture_id: ManufactureAction(sku3_manufacture_id, 20)}) + + states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) + + # so manufacture_number should be 19 instead 20 + self.assertEqual(19, states[2]) + + storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) + + # now remaining space should be 0 + self.assertEqual(0, storage_states[3]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) + + # sku3 number should be 100 + self.assertEqual(80 + 1 + 19, product_dict[SKU3_ID]) + + def test_manufacture_meet_source_lack(self): + """Test sku4 manufacturing, this sku supplier does not have enough source material at the begging + , so it cannot produce anything without consumer purchase.""" + env = build_env("case_01", 100) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "facility_id", "capacity", "remaining_space") + + manufacture_nodes = env.snapshot_list["manufacture"] + manufacture_number = len(manufacture_nodes) + manufacture_features = ( + "id", "facility_id", "manufacturing_number", "product_id", + "product_unit_cost" + ) + + ############################### TICK: 0 ###################################### + + # tick 0 passed, no product manufacturing. + env.step(None) + + states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, + -1).astype(np.int) + + # try to find which one is sku3 manufacture unit. + for index, state in enumerate(states): + # Id of sku4 is 4. + if state[3] == SKU4_ID: + sku4_data_model_index = index + sku4_manufacture_id = state[0] + sku4_facility_id = state[1] + + # try to find sku4's storage from env.summary + sku4_storage_index = env.summary["node_mapping"]["facilities"][sku4_facility_id]["units"]["storage"]["node_index"] + + # the storage should be same as initialized (50 + 0). + storage_states = storage_nodes[env.frame_index:sku4_storage_index:storage_features].flatten().astype(np.int) + + # capacity is same as configured. + self.assertEqual(200, storage_states[2]) + + # remaining space should be capacity - (50+0) + self.assertEqual(200 - (50 + 0), storage_states[3]) + + # no manufacture number as we have not pass any action + manufature_states = manufacture_nodes[ + env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype(np.int) + + # manufacturing_number should be 0 + self.assertEqual(0, manufature_states[2]) + + # output product id should be same as configured. + self.assertEqual(4, manufature_states[3]) + + # product unit cost should be same as configured. + self.assertEqual(4, manufature_states[4]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) + + # 50 sku4 at beginning + self.assertEqual(50, product_dict[SKU4_ID]) + + # 0 sku2 + self.assertEqual(0, product_dict[SKU2_ID]) + + ############################### TICK: 1 - end ###################################### + + is_done = False + + while not is_done: + # push to the end, the storage should not changed, no matter what production rate we give it. + _, _, is_done = env.step({sku4_manufacture_id: ManufactureAction(sku4_manufacture_id, 10)}) + + manufature_states = manufacture_nodes[ + env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype( + np.int) + + # manufacturing_number should be 0 + self.assertEqual(0, manufature_states[2]) + + # output product id should be same as configured. + self.assertEqual(SKU4_ID, manufature_states[3]) + + # product unit cost should be same as configured. + self.assertEqual(4, manufature_states[4]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) + + # 50 sku4 at beginning + self.assertEqual(50, product_dict[SKU4_ID]) + + # 0 sku2 + self.assertEqual(0, product_dict[SKU2_ID]) + + def test_manufacture_meet_avg_storage_limitation(self): + """Test on sku1, it is configured with nearly full initial states.""" + + env = build_env("case_01", 100) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "facility_id", "capacity", "remaining_space") + + manufacture_nodes = env.snapshot_list["manufacture"] + manufacture_number = len(manufacture_nodes) + manufacture_features = ( + "id", "facility_id", "manufacturing_number", "product_id", + "product_unit_cost" + ) + + ############################### TICK: 0 ###################################### + + # tick 0 passed, no product manufacturing, verified in above case, pass checking it here. + env.step(None) + + states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, + -1).astype(np.int) + # try to find which one is sku3 manufacture unit. + for index, state in enumerate(states): + # Id of sku1 is 1. + if state[3] == SKU1_ID: + sku1_data_model_index = index + sku1_manufacture_id = state[0] + sku1_facility_id = state[1] + + sku1_storage_index = env.summary["node_mapping"]["facilities"][sku1_facility_id]["units"]["storage"]["node_index"] + + ############################### TICK: 1 ###################################### + + # ask sku1 manufacture start manufacturing, rate is 10. + env.step({sku1_manufacture_id: ManufactureAction(sku1_storage_index, 10)}) + + storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) + manufacture_states = manufacture_nodes[ + env.frame_index:sku1_data_model_index:manufacture_features].flatten().astype(np.int) + + # we can produce 4 sku1, as it will meet storage avg limitation per sku + self.assertEqual(4, manufacture_states[2]) + + # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) + self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku1_storage_index) + + # number of sku1 should 100, just reach the avg storage capacity limitation + self.assertEqual(100, product_dict[SKU1_ID]) + + # 4 sku1 cost 4*2 source material (sku3) + self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) + + ############################### TICK: 1 ###################################### + + # then fix the product rate to 20 every tick, but the manufacture will do nothing, as we have to enough space + + is_done = False + + while not is_done: + _, _, is_done = env.step({sku1_manufacture_id: ManufactureAction(sku1_storage_index, 20)}) + + storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) + manufacture_states = manufacture_nodes[ + env.frame_index:sku1_data_model_index:manufacture_features].flatten().astype(np.int) + + # but manufacture number is 0 + self.assertEqual(0, manufacture_states[2]) + + # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) + self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, sku1_storage_index) + + # number of sku1 should 100, just reach the avg storage capacity limitation + self.assertEqual(100, product_dict[SKU1_ID]) + + # 4 sku1 cost 4*2 source material (sku3) + self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) + + """ + Storage test: + + . take available + . enough + . not enough + . try add products + . meet whole storage capacity limitation + . fail if all + . not fail if all + . enough space + . try take products + . have enough + . not enough + . get product number + + """ + + def test_storage_take_available(self): + env = build_env("case_01", 100) + + env.step(None) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "capacity", "remaining_space") + + # find first storage unit id + storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] + + # get the unit reference from env internal + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + capacity = storage_states[1] + init_remaining_space = storage_states[2] + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + # call take_available for each product in storage. + products_taken = {} + for product_id, product_number in init_product_dict.items(): + num = np.random.randint(0, product_number) + actual_num = storage_unit.take_available(product_id, num) + + # we should get the number we want. + self.assertEqual(num, actual_num) + + products_taken[product_id] = num + + # check if internal state correct + for product_id, num in products_taken.items(): + remaining_num = storage_unit.product_level[product_id] + + self.assertEqual(init_product_dict[product_id] - num, remaining_num) + + # call env.step will cause states write into snapshot + env.step(None) + + product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + for product_id, num in products_taken.items(): + remaining_num = product_dict[product_id] + + self.assertEqual(init_product_dict[product_id] - num, remaining_num) + + # then take more than exist number for 1st product(sku) + lot_taken_product_id, lot_taken_product_number = product_dict.popitem() + + lot_taken_product_number += 100 + + actual_num = storage_unit.take_available(lot_taken_product_id, lot_taken_product_number) + + # we should get all available + self.assertEqual(actual_num, lot_taken_product_number - 100) + + # take snapshot + env.step(None) + + product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + # the product number should be 0, as we took all available + self.assertEqual(0, product_dict[lot_taken_product_id]) + + def test_storage_try_add_products(self): + """ + NOTE: + try_add_products method do not check avg storage capacity checking, so we will ignore it here. + + """ + env = build_env("case_01", 100) + + env.step(None) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "capacity", "remaining_space") + + # find first storage unit id + storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] + + # get the unit reference from env internal + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + capacity = storage_states[1] + init_remaining_space = storage_states[2] + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + first_product_id = [id for id in init_product_dict.keys()][0] + + # try put products out of capacity with all_or_nothing == True + products_to_put = {} + + avg_max_product_number = init_remaining_space // len(init_product_dict) + + for product_id in init_product_dict.keys(): + products_to_put[product_id] = avg_max_product_number + 1 + + result = storage_unit.try_add_products(products_to_put, all_or_nothing=True) + + # the method will return an empty dictionary if fail to add + self.assertEqual(0, len(result)) + + # so remaining space should not change + self.assertEqual(init_remaining_space, storage_unit.remaining_space) + + # each product number should be same as before + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, + storage_unit.product_level[product_id]) + + # if we set all_or_nothing=False, then part of the product will be added to storage, and cause remaining space being 0 + result = storage_unit.try_add_products(products_to_put, all_or_nothing=False) + + self.assertEqual(0, storage_unit.remaining_space) + + # take snapshot + env.step(None) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + # remaining space in snapshot should be 0 + self.assertEqual(0, storage_states[2]) + + product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + # total product number should be same as capacity + self.assertEqual(capacity, sum(product_dict.values())) + + #################################################### + #################################################### + # reset the env for next case + env.reset() + + # check the state after reset + self.assertEqual(capacity, storage_unit.capacity) + self.assertEqual(init_remaining_space, storage_unit.remaining_space) + + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, + storage_unit.product_level[product_id]) + + def test_storage_try_take_products(self): + env = build_env("case_01", 100) + + env.step(None) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "capacity", "remaining_space") + + # find first storage unit id + storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] + + # get the unit reference from env internal + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + capacity = storage_states[1] + init_remaining_space = storage_states[2] + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + product_to_take = {} + + for product_id, product_number in init_product_dict.items(): + product_to_take[product_id] = product_number + 1 + + # which this setting, it will return false, as no enough product for ous + self.assertFalse(storage_unit.try_take_products(product_to_take)) + + # so remaining space and product number should same as before + self.assertEqual(init_remaining_space, storage_unit.remaining_space) + + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, storage_unit.product_level[product_id]) + + # try to get all products + for product_id, product_number in product_to_take.items(): + product_to_take[product_id] = product_number - 1 + + self.assertTrue(storage_unit.try_take_products(product_to_take)) + + # now the remaining space should be same as capacity as we take all + self.assertEqual(capacity, storage_unit.remaining_space) + + # take snapshot + env.step(None) + + storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) + + # remaining space should be same as capacity in snapshot + self.assertEqual(storage_states[1], storage_states[2]) + + def test_storage_get_product_number(self): + env = build_env("case_01", 100) + + env.step(None) + + storage_nodes = env.snapshot_list["storage"] + storage_features = ("id", "capacity", "remaining_space") + + # find first storage unit id + storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] + + # get the unit reference from env internal + storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + # number in object should be same with states + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, + storage_unit.product_level[product_id]) + + # should not change even after reset + env.reset() + env.step(None) + + init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) + + # number in object should be same with states + for product_id, product_number in init_product_dict.items(): + self.assertEqual(product_number, + storage_unit.product_level[product_id]) + + """ + + Consumer test: + + . initial state + . state after reset + . set_action directly from code + . set_action by env.step + . call on_order_reception directly to simulation order arrived + . call update_open_orders directly + + """ + + def test_consumer_init_state(self): + """ + NOTE: we will use consumer on Supplier_SKU1, as it contains a source for sku3 (Supplier_SKU3) + """ + env = build_env("case_01", 100) + + # print(env.summary) + # we can get the consumer from env.summary + + # NOTE: though we are test with sku1, but the consumer is for sku3, as it is the source material from source + sku3_consumer_unit: ConsumerUnit + sku3_supplier_faiclity_id: int + sku3_consumer_data_model_index: int + sku3_product_unit_id: int + + for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): + if facility_defail["name"] == "Supplier_SKU1": + # try to find sku3 consumer + sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] + + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] + + if facility_defail["name"] == "Supplier_SKU3": + sku3_supplier_faiclity_id = facility_defail["id"] + + sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] + + # check initial state + self.assertEqual(0, sku3_consumer_unit.received) + self.assertEqual(0, sku3_consumer_unit.purchased) + self.assertEqual(0, sku3_consumer_unit.order_product_cost) + self.assertEqual(SKU3_ID, sku3_consumer_unit.product_id) + + # check data model state + # order cost from configuration + self.assertEqual(200, sku3_consumer_unit.data_model.order_cost) + self.assertEqual(0, sku3_consumer_unit.data_model.total_purchased) + self.assertEqual(0, sku3_consumer_unit.data_model.total_received) + + # NOTE: 0 is an invalid(initial) id + self.assertEqual(SKU3_ID, sku3_consumer_unit.data_model.product_id) + self.assertEqual(sku3_consumer_unit_id, sku3_consumer_unit.data_model.id) + self.assertEqual(sku3_product_unit_id, sku3_consumer_unit.data_model.product_unit_id) + self.assertEqual(0, sku3_consumer_unit.data_model.order_quantity) + self.assertEqual(0, sku3_consumer_unit.data_model.purchased) + self.assertEqual(0, sku3_consumer_unit.data_model.received) + self.assertEqual(0, sku3_consumer_unit.data_model.order_product_cost) + + # check sources + for source_facility_id in sku3_consumer_unit.sources: + source_facility: FacilityBase = env._business_engine.world.get_facility_by_id(source_facility_id) + + # check if source facility contains the sku3 config + self.assertTrue(SKU3_ID in source_facility.skus) + + env.step(None) + + # check state + features = ( + "id", + "facility_id", + "product_id", + "order_cost", + "total_purchased", + "total_received", + "order_quantity", + "purchased", + "received", + "order_product_cost" + ) + + consumer_nodes = env.snapshot_list["consumer"] + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # Nothing happened at tick 0, so most states will be 0 + self.assertTrue((states[4:] == 0).all()) + + self.assertEqual(sku3_consumer_unit_id, states[0]) + self.assertEqual(SKU3_ID, states[2]) + + env.reset() + env.step(None) + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # Nothing happened at tick 0, so most states will be 0 + self.assertTrue((states[4:] == 0).all()) + + self.assertEqual(sku3_consumer_unit_id, states[0]) + self.assertEqual(SKU3_ID, states[2]) + + def test_consumer_action(self): + env = build_env("case_01", 100) + + sku3_consumer_unit: ConsumerUnit + sku3_supplier_faiclity_id: int + sku3_consumer_data_model_index: int + sku3_product_unit_id: int + + for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): + if facility_defail["name"] == "Supplier_SKU1": + sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] + + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] + + if facility_defail["name"] == "Supplier_SKU3": + sku3_supplier_faiclity_id = facility_defail["id"] + + sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] + + # zero quantity will be ignore + action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 0, 1, 0) + + action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 10, 1, 0) + + sku3_consumer_unit.set_action(action_with_zero) + + env.step(None) + + features = ( + "id", + "facility_id", + "product_id", + "order_cost", + "total_purchased", + "total_received", + "product_id", + "order_quantity", + "purchased", + "received", + "order_product_cost" + ) + + consumer_nodes = env.snapshot_list["consumer"] + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # Nothing happened at tick 0, at the action will be recorded + self.assertEqual(action_with_zero.product_id, states[6]) + self.assertEqual(action_with_zero.quantity, states[8]) + + self.assertEqual(sku3_consumer_unit_id, states[0]) + self.assertEqual(SKU3_ID, states[2]) + + # NOTE: we cannot set_action directly here, as post_step will clear the action before starting next tick + env.step({action.id: action}) + + self.assertEqual(action.quantity, sku3_consumer_unit.purchased) + self.assertEqual(0, sku3_consumer_unit.received) + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # action field should be recorded + self.assertEqual(action.product_id, states[6]) + self.assertEqual(action.quantity, states[8]) + + # total purchased should be same as purchased at this tick. + self.assertEqual(action.quantity, states[4]) + + # no received now + self.assertEqual(0, states[5]) + + # purchased same as quantity + self.assertEqual(action.quantity, states[8]) + + # no receives + self.assertEqual(0, states[9]) + + # same action for next step, so total_XXX will be changed to double + env.step({action.id: action}) + + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) + + # action field should be recorded + self.assertEqual(action.product_id, states[6]) + self.assertEqual(action.quantity, states[8]) + + # total purchased should be same as purchased at this tick. + self.assertEqual(action.quantity * 2, states[4]) + + # no received now + self.assertEqual(0, states[5]) + + # purchased same as quantity + self.assertEqual(action.quantity, states[8]) + + # no receives + self.assertEqual(0, states[9]) + + def test_consumer_on_order_reception(self): + env = build_env("case_01", 100) + + sku3_consumer_unit: ConsumerUnit + sku3_supplier_facility_id: int + sku3_consumer_data_model_index: int + sku3_product_unit_id: int + + for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): + if facility_defail["name"] == "Supplier_SKU1": + sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] + + sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] + + if facility_defail["name"] == "Supplier_SKU3": + sku3_supplier_facility_id = facility_defail["id"] + + sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] + + action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1, 0) + + # 1st step must none action + env.step(None) + + env.step({action.id: action}) + + # simulate purchased product is arrived by vehicle unit + sku3_consumer_unit.on_order_reception(sku3_supplier_facility_id, SKU3_ID, 10, 10) + + # now all order is done + self.assertEqual(0, sku3_consumer_unit.open_orders[sku3_supplier_facility_id][SKU3_ID]) + self.assertEqual(10, sku3_consumer_unit.received) + + env.step(None) + + consumer_nodes = env.snapshot_list["consumer"] + states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"received"].flatten().astype(np.int) + + # NOTE: we cannot test the received state by calling on_order_reception directly, + # as it will be cleared by env.step, do it on vehicle unit test. + + """ + Vehicle unit test: + + . initial state + . if vehicle arrive at destination within special vlt + . schedule job + . try_load until patient <= 0 to cancel the schedule + . try_load until patient > 0 to load order + . try_unload + . target storage cannot take all + . target storage can take all + """ + + def test_vehicle_unit_state(self): + env = build_env("case_02", 100) + + # try to find first vehicle unit we meet + vehicle_unit: VehicleUnit + vehicle_unit_id: int + vehicle_unit_data_model_index: int + + for id, info in env.summary["node_mapping"]["unit_mapping"].items(): + if info[0] == "vehicle": + vehicle_unit_id = id + vehicle_unit = env._business_engine.world.get_entity(id) + vehicle_unit_data_model_index = vehicle_unit.data_model_index + + break + + # check initial state according to configuration file + self.assertEqual(10, vehicle_unit.max_patient) + + self.assertEqual(0, vehicle_unit.requested_quantity) + # not destination at first + self.assertIsNone(vehicle_unit.destination) + # no path + self.assertIsNone(vehicle_unit.path) + # no product + self.assertEqual(0, vehicle_unit.product_id) + # no steps + self.assertEqual(0, vehicle_unit.steps) + # + self.assertEqual(0, vehicle_unit.payload) + # + self.assertIsNone(vehicle_unit.product) + # + self.assertEqual(0, vehicle_unit.location) + # + self.assertEqual(0, vehicle_unit.velocity) + + # state in frame + self.assertEqual(0, vehicle_unit.data_model.payload) + self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) + + # reset to check again + env.step(None) + env.reset() + + # check initial state according to configuration file + self.assertEqual(10, vehicle_unit.max_patient) + + # not destination at first + self.assertIsNone(vehicle_unit.destination) + # no path + self.assertIsNone(vehicle_unit.path) + # no product + self.assertEqual(0, vehicle_unit.product_id) + # no steps + self.assertEqual(0, vehicle_unit.steps) + # + self.assertEqual(0, vehicle_unit.payload) + # + self.assertIsNone(vehicle_unit.product) + # + self.assertEqual(0, vehicle_unit.location) + # + self.assertEqual(0, vehicle_unit.velocity) + # + self.assertEqual(0, vehicle_unit.requested_quantity) + + # state in frame + self.assertEqual(0, vehicle_unit.data_model.payload) + self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) + + def test_vehicle_unit_schedule(self): + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + vehicle_unit: VehicleUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + for v in info["units"]["distribution"]["children"]: + vehicle_unit = env._business_engine.world.get_entity(v["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # make sure the upstream in the only one supplier in config + self.assertEqual(1, len(dest_facility.upstreams)) + self.assertEqual(1, len(dest_facility.upstreams[SKU3_ID])) + + # schedule job vehicle unit manually, from supplier to warehouse + vehicle_unit.schedule(dest_facility, SKU3_ID, 20, 2) + + # step to take snapshot + env.step(None) + + vehicle_nodes = env.snapshot_list["vehicle"] + + # check internal states + self.assertEqual(dest_facility, vehicle_unit.destination) + self.assertEqual(SKU3_ID, vehicle_unit.product_id) + self.assertEqual(20, vehicle_unit.requested_quantity) + self.assertEqual(2, vehicle_unit.velocity) + # 6/2 + self.assertEqual(3, vehicle_unit.steps) + + features = ( + "id", + "facility_id", + "payload", + "unit_transport_cost" + ) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # source id + self.assertEqual(vehicle_unit.facility.id, states[1]) + # payload should be 20, as we already env.step + self.assertEqual(20, states[2]) + + # push the vehicle on the way + env.step(None) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # payload + self.assertEqual(20, states[2]) + + env.step(None) + env.step(None) + + # next step vehicle will try to unload the products + env.step(None) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # the product is unloaded, vehicle states will be reset to initial + # not destination at first + self.assertIsNone(vehicle_unit.destination) + self.assertIsNone(vehicle_unit.path) + self.assertEqual(0, vehicle_unit.product_id) + self.assertEqual(0, vehicle_unit.steps) + self.assertEqual(0, vehicle_unit.payload) + self.assertIsNone(vehicle_unit.product) + self.assertEqual(0, vehicle_unit.location) + self.assertEqual(0, vehicle_unit.velocity) + self.assertEqual(0, vehicle_unit.requested_quantity) + + # check states + self.assertEqual(0, states[2]) + self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) + + def test_vehicle_unit_no_patient(self): + """ + NOTE: with patient is tried in above case after schedule the job + """ + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + vehicle_unit: VehicleUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + for v in info["units"]["distribution"]["children"]: + vehicle_unit = env._business_engine.world.get_entity(v["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # there is 80 sku3 in supplier, lets schedule a job for 100, to make sure it will fail to try load + vehicle_unit.schedule(dest_facility, SKU3_ID, 100, 3) + + # push env to next step + env.step(None) + + self.assertEqual(100, vehicle_unit.requested_quantity) + + # the patient will -1 as no enough product so load + self.assertEqual(10 - 1, vehicle_unit.patient) + + # no payload + self.assertEqual(0, vehicle_unit.payload) + self.assertEqual(0, vehicle_unit.data_model.payload) + + # step 9 ticks, patient will be 0 + for i in range(10 - 1): + env.step(None) + + self.assertEqual(10 - 1 - (i + 1), vehicle_unit.patient) + + vehicle_nodes = env.snapshot_list["vehicle"] + features = ( + "id", + "facility_id", + "payload", + "unit_transport_cost" + ) + + states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) + + # no payload from start to now + self.assertListEqual([0] * 10, list(states)) + + # push env to next step, vehicle will be reset to initial state + env.step(None) + + states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) + + # the product is unloaded, vehicle states will be reset to initial + # not destination at first + self.assertIsNone(vehicle_unit.destination) + self.assertIsNone(vehicle_unit.path) + self.assertEqual(0, vehicle_unit.product_id) + self.assertEqual(0, vehicle_unit.steps) + self.assertEqual(0, vehicle_unit.payload) + self.assertIsNone(vehicle_unit.product) + self.assertEqual(0, vehicle_unit.location) + self.assertEqual(0, vehicle_unit.velocity) + self.assertEqual(0, vehicle_unit.requested_quantity) + + # check states + + self.assertEqual(0, states[2]) + self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) + + def test_vehicle_unit_cannot_unload_at_destination(self): + """ + NOTE: If vehicle cannot unload at destination, it will keep waiting, until success to unload. + + """ + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + vehicle_unit: VehicleUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + for v in info["units"]["distribution"]["children"]: + vehicle_unit = env._business_engine.world.get_entity(v["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # move all 80 sku3 to destination, will cause vehicle keep waiting there + vehicle_unit.schedule(dest_facility, SKU3_ID, 80, 2) + + # step to the end. + is_done = False + + while not is_done: + _, _, is_done = env.step(None) + + vehicle_nodes = env.snapshot_list["vehicle"] + features = ( + "id", + "facility_id", + "source", + "destination", + "payload", + "product_id", + "requested_quantity", + "steps", + "unit_transport_cost" + ) + + # payload should be 80 for first 4 ticks, as it is on the way + # then it will unload 100 - 10 - 10 - 10 = 70 products, as this is the remaining space of destination storage + # so then it will keep waiting to unload remaining 10 + payload_states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) + self.assertListEqual([80] * 4 + [10] * 96, list(payload_states)) + + """ + Distribution unit test: + + . initial state + . place order + . dispatch orders without available vehicle + . dispatch order with vehicle + """ + + def test_distribution_unit_initial_state(self): + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + dist_unit: DistributionUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + self.assertEqual(0, len(dist_unit.order_queue)) + + # reset + env.reset() + + self.assertEqual(0, len(dist_unit.order_queue)) + + def test_distribution_unit_dispatch_order(self): + env = build_env("case_02", 100) + + # try to find first vehicle unit of Supplier + dist_unit: DistributionUnit + dest_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Supplier_SKU3": + dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) + + if info["name"] == "Warehouse_001": + dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + first_vehicle: VehicleUnit = dist_unit.vehicles[0] + + order = Order(dest_facility, SKU3_ID, 10, 2) + + dist_unit.place_order(order) + + # check if order is saved + self.assertEqual(1, len(dist_unit.order_queue)) + + # check get pending order correct + pending_order = dist_unit.get_pending_order() + + self.assertDictEqual({3: 10}, pending_order) + + # same as vehicle schedule case, distribution will try to schedule this order to vehicles from beginning to end + # so it will dispatch this order to first vehicle + env.step(None) + + self.assertEqual(dest_facility, first_vehicle.destination) + self.assertEqual(10, first_vehicle.requested_quantity) + self.assertEqual(2, first_vehicle.velocity) + self.assertEqual(SKU3_ID, first_vehicle.product_id) + + # since we already test vehicle unit, do not check the it again here + + # add another order to check pending order + dist_unit.place_order(order) + + pending_order = dist_unit.get_pending_order() + + self.assertDictEqual({3: 10}, pending_order) + + # another order, will cause the pending order increase + dist_unit.place_order(order) + + pending_order = dist_unit.get_pending_order() + + # 2 pending orders + self.assertDictEqual({3: 20}, pending_order) + + # now we have only one available vehicle, 2 pending order + # next step will cause delay_order_penalty + env.step(None) + + second_vehicle = dist_unit.vehicles[1] + + self.assertEqual(dest_facility, second_vehicle.destination) + self.assertEqual(10, second_vehicle.requested_quantity) + self.assertEqual(2, second_vehicle.velocity) + self.assertEqual(SKU3_ID, second_vehicle.product_id) + + """ + Seller unit test: + . initial state + . with a customized seller unit + . with built in one + """ + + def test_seller_unit_initial_states(self): + env = build_env("case_02", 100) + + # find seller for sku3 from retailer facility + sell_unit: SellerUnit + source_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Retailer_001": + for pid, pdetail in info["units"]["products"].items(): + if pdetail["sku_id"] == SKU3_ID: + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + + if info["name"] == "Warehouse_001": + source_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # from configuration + self.assertEqual(10, sell_unit.gamma) + + self.assertEqual(0, sell_unit.sold) + self.assertEqual(0, sell_unit.demand) + self.assertEqual(0, sell_unit.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + # + self.assertEqual(0, sell_unit.data_model.sold) + self.assertEqual(0, sell_unit.data_model.demand) + self.assertEqual(0, sell_unit.data_model.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + env.reset() + + # from configuration + self.assertEqual(10, sell_unit.gamma) + self.assertEqual(0, sell_unit.sold) + self.assertEqual(0, sell_unit.demand) + self.assertEqual(0, sell_unit.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + # + self.assertEqual(0, sell_unit.data_model.sold) + self.assertEqual(0, sell_unit.data_model.demand) + self.assertEqual(0, sell_unit.data_model.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + def test_seller_unit_demand_states(self): + env = build_env("case_02", 100) + + # find seller for sku3 from retailer facility + sell_unit: SellerUnit + source_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Retailer_001": + for pid, pdetail in info["units"]["products"].items(): + if pdetail["sku_id"] == SKU3_ID: + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + + if info["name"] == "Warehouse_001": + source_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + SKU3_INIT_NUMBER = sell_unit.facility.skus[SKU3_ID].init_stock + + env.step(None) + + # seller unit will try to count down the product number base on demand + # default seller use gamma distribution on each tick + demand = sell_unit.demand + + # demand should be same with original + self.assertEqual(demand, sell_unit.data_model.demand) + + actual_sold = min(demand, SKU3_INIT_NUMBER) + # sold may be not same as demand, depend on remaining number in storage + self.assertEqual(actual_sold, sell_unit.sold) + self.assertEqual(actual_sold, sell_unit.data_model.sold) + self.assertEqual(actual_sold, sell_unit.total_sold) + self.assertEqual(actual_sold, sell_unit.data_model.total_sold) + + states = env.snapshot_list["seller"][ + env.frame_index:sell_unit.data_model_index:("sold", "demand", "total_sold")].flatten().astype(np.int) + + self.assertEqual(actual_sold, states[0]) + self.assertEqual(demand, states[1]) + self.assertEqual(actual_sold, states[2]) + + # move to next step to check if state is correct + env.step(None) + + demand = sell_unit.demand + + # demand should be same with original + self.assertEqual(demand, sell_unit.data_model.demand) + + actual_sold_2 = min(demand, SKU3_INIT_NUMBER - actual_sold) + + # sold may be not same as demand, depend on remaining number in storage + self.assertEqual(actual_sold_2, sell_unit.sold) + self.assertEqual(actual_sold_2, sell_unit.data_model.sold) + self.assertEqual(actual_sold + actual_sold_2, sell_unit.total_sold) + self.assertEqual(actual_sold + actual_sold_2, sell_unit.data_model.total_sold) + + states = env.snapshot_list["seller"][ + env.frame_index:sell_unit.data_model_index:("sold", "demand", "total_sold")].flatten().astype(np.int) + + self.assertEqual(actual_sold_2, states[0]) + self.assertEqual(demand, states[1]) + self.assertEqual(actual_sold + actual_sold_2, states[2]) + + def test_seller_unit_customized(self): + env = build_env("case_03", 100) + + # find seller for sku3 from retailer facility + sell_unit: SellerUnit + source_facility: FacilityBase + + for id, info in env.summary["node_mapping"]["facilities"].items(): + if info["name"] == "Retailer_001": + for pid, pdetail in info["units"]["products"].items(): + if pdetail["sku_id"] == SKU3_ID: + sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + + if info["name"] == "Warehouse_001": + source_facility = env._business_engine.world.get_facility_by_id(info["id"]) + + # NOTE: + # this simple seller unit return demands that same as current tick + env.step(None) + + # so tick 0 will have demand == 0 + # from configuration + self.assertEqual(0, sell_unit.sold) + self.assertEqual(0, sell_unit.demand) + self.assertEqual(0, sell_unit.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + # + self.assertEqual(0, sell_unit.data_model.sold) + self.assertEqual(0, sell_unit.data_model.demand) + self.assertEqual(0, sell_unit.data_model.total_sold) + self.assertEqual(SKU3_ID, sell_unit.product_id) + + is_done = False + + while not is_done: + _, _, is_done = env.step(None) + + # check demand history, it should be same as tick + seller_nodes = env.snapshot_list["seller"] + + demand_states = seller_nodes[:sell_unit.data_model_index:"demand"].flatten().astype(np.int) + + self.assertListEqual([i for i in range(100)], list(demand_states)) + + # check sold states + # it should be 0 after tick 4 + sold_states = seller_nodes[:sell_unit.data_model_index:"sold"].flatten().astype(np.int) + self.assertListEqual([0, 1, 2, 3, 4] + [0] * 95, list(sold_states)) + + # total sold + total_sold_states = seller_nodes[:sell_unit.data_model_index:"total_sold"].flatten().astype(np.int) + # total sold will keep same after tick 4 + self.assertListEqual([0, 1, 3, 6, 10] + [10] * 95, list(total_sold_states)) + + def test_outer_seller(self): + env = build_env("case_04", 100) + + index2unitid_mapping = {} + skuid2index_mapping = {} + + # we only have one manufacture here + man_id = None + + # find all the sellers + for unit_id, unit_detail in env.summary["node_mapping"]["unit_mapping"].items(): + if unit_detail[0] == "seller": + index = unit_detail[1] + sku = unit_detail[3] + index2unitid_mapping[index] = unit_id + skuid2index_mapping[sku.id] = index + + if unit_detail[0] == "manufacture": + man_id = unit_id + + # tick 0 + env.step(None) + + seller_states = env.snapshot_list["seller"][0::"demand"].flatten().astype(np.int) + + # at tick 0 (2010-12-01) + # sku1 have 10 demand + self.assertEqual(10, seller_states[skuid2index_mapping[SKU1_ID]]) + + # sku2 have 20 demand + self.assertEqual(20, seller_states[skuid2index_mapping[SKU2_ID]]) + + # sku3 have 30 demand + self.assertEqual(30, seller_states[skuid2index_mapping[SKU3_ID]]) + + # tick 1 + env.step(None) + + seller_states = env.snapshot_list["seller"][1::"demand"].flatten().astype(np.int) + + # at tick 1 (2010-12-02) + # sku1 have 0 demand (no record in file) + self.assertEqual(0, seller_states[skuid2index_mapping[SKU1_ID]]) + + # sku2 have 20 demand + self.assertEqual(21, seller_states[skuid2index_mapping[SKU2_ID]]) + + # sku3 have 30 demand + self.assertEqual(31, seller_states[skuid2index_mapping[SKU3_ID]]) + + # tick 2 + env.step(None) + + seller_states = env.snapshot_list["seller"][2::"demand"].flatten().astype(np.int) + + # at tick 2 (2010-12-03) + # sku1 have 13 demand + self.assertEqual(13, seller_states[skuid2index_mapping[SKU1_ID]]) + + # sku2 have 20 demand + self.assertEqual(22, seller_states[skuid2index_mapping[SKU2_ID]]) + + # sku3 have 30 demand + self.assertEqual(0, seller_states[skuid2index_mapping[SKU3_ID]]) + + # test if simple manufacture work correctly. + + action = ManufactureAction(man_id, 10) + + env.step({man_id: action}) + + man_states = env.snapshot_list["manufacture"][env.tick::"manufacturing_number"].flatten().astype(np.int) + + self.assertEqual(action.production_rate, man_states[0]) + + +if __name__ == '__main__': + unittest.main() From c7bca773eaba210c80ac225a6c391d7741251d86 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 19 Jul 2021 05:23:39 +0000 Subject: [PATCH 390/482] 1. added missing model.eval() to choose_action; 2.added entropy features to AC --- maro/rl/policy/algorithms/ac.py | 27 ++++++++++++++++++++------- maro/rl/policy/algorithms/ddpg.py | 2 ++ maro/rl/policy/algorithms/dqn.py | 2 +- maro/rl/policy/algorithms/pg.py | 2 ++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/maro/rl/policy/algorithms/ac.py b/maro/rl/policy/algorithms/ac.py index b9c52236f..a97fd2a72 100644 --- a/maro/rl/policy/algorithms/ac.py +++ b/maro/rl/policy/algorithms/ac.py @@ -5,6 +5,7 @@ import numpy as np import torch +from torch.distributions import Categorical from maro.rl.experience import ExperienceStore, UniformSampler from maro.rl.model import DiscreteACNet @@ -20,16 +21,18 @@ class ActorCriticConfig: train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - actor_loss_coefficient (float): The coefficient for actor loss in the total loss function, e.g., - loss = critic_loss + ``actor_loss_coefficient`` * actor_loss. Defaults to 1.0. + min_logp (float): Lower bound for clamping logP values during learning. This is to prevent logP from becoming + very large in magnitude and cuasing stability issues. Defaults to None, which means no lower bound. + critic_loss_coeff (float): Coefficient for critic loss in total loss. Defaults to 1.0. + entropy_coeff (float): Coefficient for the entropy term in total loss. Defaults to 0.001. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. clear_experience_memory_every (int): Number of ``ActorCritic.learn`` calls between experience memory clearances. Defaults to 1. """ __slots__ = [ - "reward_discount", "train_epochs", "critic_loss_func", "actor_loss_coefficient", "clip_ratio", - "clear_experience_memory_every" + "reward_discount", "train_epochs", "critic_loss_func", "min_logp", "critic_loss_coeff", "entropy_coeff", + "clip_ratio", "clear_experience_memory_every" ] def __init__( @@ -37,14 +40,18 @@ def __init__( reward_discount: float, train_epochs: int = 1, critic_loss_cls="mse", - actor_loss_coefficient: float = 1.0, + min_logp: float = None, + critic_loss_coeff: float = 1.0, + entropy_coeff: float = 0.001, clip_ratio: float = None, clear_experience_memory_every: int = 1 ): self.reward_discount = reward_discount self.train_epochs = train_epochs self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() - self.actor_loss_coefficient = actor_loss_coefficient + self.min_logp = min_logp + self.critic_loss_coeff = critic_loss_coeff + self.entropy_coeff = entropy_coeff self.clip_ratio = clip_ratio self.clear_experience_memory_every = clear_experience_memory_every @@ -93,12 +100,14 @@ def __init__( def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: """Return actions and log probabilities for given states.""" + self.ac_net.eval() with torch.no_grad(): actions, log_p = self.ac_net.get_action(states) actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else (actions, log_p) def learn(self): + assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() for _ in range(self.config.train_epochs): experience_set = self.sampler.get() @@ -116,6 +125,7 @@ def learn(self): # actor loss log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) + log_p_new = torch.clamp(log_p_new, min=self.config.min_logp) if self.config.clip_ratio is not None: ratio = torch.exp(log_p_new - log_p) clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) @@ -125,8 +135,11 @@ def learn(self): # critic_loss critic_loss = self.config.critic_loss_func(state_values, return_est) - loss = critic_loss + self.config.actor_loss_coefficient * actor_loss + # entropy loss (with negative sign to encourange exploration) + entropy_loss = -Categorical(action_probs).entropy() + # total loss + loss = actor_loss + self.config.critic_loss_coeff * critic_loss + self.config.entropy_coeff * entropy_loss self.ac_net.step(loss) if self._post_step: diff --git a/maro/rl/policy/algorithms/ddpg.py b/maro/rl/policy/algorithms/ddpg.py index 3a8426be1..b84525564 100644 --- a/maro/rl/policy/algorithms/ddpg.py +++ b/maro/rl/policy/algorithms/ddpg.py @@ -99,12 +99,14 @@ def __init__( self._num_steps = 0 def choose_action(self, states) -> Union[float, np.ndarray]: + self.ac_net.eval() with torch.no_grad(): actions = self.ac_net.get_action(states).cpu().numpy() return actions[0] if len(actions) == 1 else actions def learn(self): + assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() for _ in range(self.config.train_epochs): experience_set = self.sampler.get() diff --git a/maro/rl/policy/algorithms/dqn.py b/maro/rl/policy/algorithms/dqn.py index 4f27080fa..eadd20f84 100644 --- a/maro/rl/policy/algorithms/dqn.py +++ b/maro/rl/policy/algorithms/dqn.py @@ -96,8 +96,8 @@ def __init__( self._loss_func = torch.nn.MSELoss() def choose_action(self, states) -> Union[int, np.ndarray]: + self.q_net.eval() with torch.no_grad(): - self.q_net.eval() q_for_all_actions = self.q_net(states) # (batch_size, num_actions) _, actions = q_for_all_actions.max(dim=1) diff --git a/maro/rl/policy/algorithms/pg.py b/maro/rl/policy/algorithms/pg.py index a3f5aef09..e53d9507f 100644 --- a/maro/rl/policy/algorithms/pg.py +++ b/maro/rl/policy/algorithms/pg.py @@ -68,6 +68,7 @@ def __init__( def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Return actions and log probabilities for given states.""" + self.policy_net.eval() with torch.no_grad(): actions, log_p = self.policy_net.get_action(states) actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() @@ -79,6 +80,7 @@ def learn(self): the experience store's ``get`` method should be a sequential set, i.e., in the order in which they are generated during the simulation. Otherwise, the return values may be meaningless. """ + assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." self.policy_net.train() experience_set = self.sampler.get() log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) From 81245dd923106f02ffc98f1dbd3e9bbc487c2c7e Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 19 Jul 2021 05:38:22 +0000 Subject: [PATCH 391/482] fixed a bug in ac entropy --- examples/rl/cim/ac.py | 2 +- examples/rl/workflows/config.yml | 4 ++-- maro/rl/policy/algorithms/ac.py | 13 +++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index cee6b4127..b909b2984 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -52,7 +52,7 @@ "reward_discount": .0, "critic_loss_cls": "smooth_l1", "train_epochs": 10, - "actor_loss_coefficient": 0.1, + "critic_loss_coeff": 0.1, # "clip_ratio": 0.8 # for PPO }, "experience_store": { diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 3dd71f6a2..f39994b74 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -15,8 +15,8 @@ rollout_experience_distribution: false sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 6 - min_finished_workers: 4 + num_rollout_workers: 3 + min_finished_workers: 2 max_extra_recv_tries: 2 extra_recv_timeout: 100 async: diff --git a/maro/rl/policy/algorithms/ac.py b/maro/rl/policy/algorithms/ac.py index a97fd2a72..7ee1f55f7 100644 --- a/maro/rl/policy/algorithms/ac.py +++ b/maro/rl/policy/algorithms/ac.py @@ -24,7 +24,8 @@ class ActorCriticConfig: min_logp (float): Lower bound for clamping logP values during learning. This is to prevent logP from becoming very large in magnitude and cuasing stability issues. Defaults to None, which means no lower bound. critic_loss_coeff (float): Coefficient for critic loss in total loss. Defaults to 1.0. - entropy_coeff (float): Coefficient for the entropy term in total loss. Defaults to 0.001. + entropy_coeff (float): Coefficient for the entropy term in total loss. Defaults to None, in which case the + total loss will not include an entropy term. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. clear_experience_memory_every (int): Number of ``ActorCritic.learn`` calls between experience memory clearances. @@ -42,7 +43,7 @@ def __init__( critic_loss_cls="mse", min_logp: float = None, critic_loss_coeff: float = 1.0, - entropy_coeff: float = 0.001, + entropy_coeff: float = None, clip_ratio: float = None, clear_experience_memory_every: int = 1 ): @@ -125,7 +126,7 @@ def learn(self): # actor loss log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - log_p_new = torch.clamp(log_p_new, min=self.config.min_logp) + log_p_new = torch.clamp(log_p_new, min=self.config.min_logp, max=.0) if self.config.clip_ratio is not None: ratio = torch.exp(log_p_new - log_p) clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) @@ -135,11 +136,11 @@ def learn(self): # critic_loss critic_loss = self.config.critic_loss_func(state_values, return_est) - # entropy loss (with negative sign to encourange exploration) - entropy_loss = -Categorical(action_probs).entropy() # total loss - loss = actor_loss + self.config.critic_loss_coeff * critic_loss + self.config.entropy_coeff * entropy_loss + loss = actor_loss + self.config.critic_loss_coeff * critic_loss + if self.config.entropy_coeff is not None: + loss -= self.config.entropy_coeff * Categorical(action_probs).entropy() self.ac_net.step(loss) if self._post_step: From e56e9b885dfde17fda7da2c485674aeb1a9b0022 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 19 Jul 2021 08:16:08 +0000 Subject: [PATCH 392/482] abbreviated coefficient to coeff --- examples/rl/cim/ac.py | 1 + examples/rl/cim/dqn.py | 2 +- examples/rl/cim/policy_index.py | 1 + maro/rl/policy/algorithms/ac.py | 2 +- maro/rl/policy/algorithms/ddpg.py | 25 ++++++++++++------------- maro/rl/policy/algorithms/dqn.py | 12 ++++++------ 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index b909b2984..0b306ae1a 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -53,6 +53,7 @@ "critic_loss_cls": "smooth_l1", "train_epochs": 10, "critic_loss_coeff": 0.1, + "entropy_coeff": 0.01, # "clip_ratio": 0.8 # for PPO }, "experience_store": { diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index 952abc002..3bfe3ad96 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -40,7 +40,7 @@ "reward_discount": .0, "update_target_every": 5, "train_epochs": 10, - "soft_update_coefficient": 0.1, + "soft_update_coeff": 0.1, "double": False }, "experience_store": { diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index a3ec1c1d0..3eb24c645 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -8,6 +8,7 @@ cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) +from ac import get_ac_policy from dqn import get_dqn_policy from env_wrapper import AGENT_IDS diff --git a/maro/rl/policy/algorithms/ac.py b/maro/rl/policy/algorithms/ac.py index 7ee1f55f7..8497f6f2d 100644 --- a/maro/rl/policy/algorithms/ac.py +++ b/maro/rl/policy/algorithms/ac.py @@ -140,7 +140,7 @@ def learn(self): # total loss loss = actor_loss + self.config.critic_loss_coeff * critic_loss if self.config.entropy_coeff is not None: - loss -= self.config.entropy_coeff * Categorical(action_probs).entropy() + loss -= self.config.entropy_coeff * Categorical(action_probs).entropy().mean() self.ac_net.step(loss) if self._post_step: diff --git a/maro/rl/policy/algorithms/ddpg.py b/maro/rl/policy/algorithms/ddpg.py index b84525564..974348962 100644 --- a/maro/rl/policy/algorithms/ddpg.py +++ b/maro/rl/policy/algorithms/ddpg.py @@ -22,15 +22,14 @@ class DDPGConfig: train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - policy_loss_coefficient (float): The coefficient for policy loss in the total loss function, e.g., - loss = q_value_loss + ``policy_loss_coefficient`` * policy_loss. Defaults to 1.0. - soft_update_coefficient (float): Soft update coefficient, e.g., - target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. - Defaults to 1.0. + q_value_loss_coeff (float): Coefficient for policy loss in the total loss function, e.g., + loss = policy_loss + ``q_value_loss_coeff`` * q_value_loss. Defaults to 1.0. + soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + + (1-soft_update_coeff) * target_model. Defaults to 1.0. """ __slots__ = [ - "reward_discount", "target_update_freq", "train_epochs", "q_value_loss_func", "policy_loss_coefficient", - "soft_update_coefficient" + "reward_discount", "target_update_freq", "train_epochs", "q_value_loss_func", "q_value_loss_coeff", + "soft_update_coeff" ] def __init__( @@ -39,15 +38,15 @@ def __init__( target_update_freq: int, train_epochs: int = 1, q_value_loss_cls="mse", - policy_loss_coefficient: float = 1.0, - soft_update_coefficient: float = 1.0, + policy_loss_coeff: float = 1.0, + soft_update_coeff: float = 1.0, ): self.reward_discount = reward_discount self.target_update_freq = target_update_freq self.train_epochs = train_epochs self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() - self.policy_loss_coefficient = policy_loss_coefficient - self.soft_update_coefficient = soft_update_coefficient + self.policy_loss_coeff = policy_loss_coeff + self.soft_update_coeff = soft_update_coeff class DDPG(AbsCorePolicy): @@ -123,14 +122,14 @@ def learn(self): q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) q_value_loss = self.config.q_value_loss_func(q_values, target_q_values) policy_loss = -self.ac_net.value(states).mean() - loss = q_value_loss + self.config.policy_loss_coefficient * policy_loss + loss = policy_loss + self.config.q_value_loss_coeff * q_value_loss self.ac_net.step(loss) if self._post_step: self._post_step(loss.detach().cpu().numpy(), self.tracker) self._num_steps += 1 if self._num_steps % self.config.target_update_freq == 0: - self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coefficient) + self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coeff) def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/algorithms/dqn.py b/maro/rl/policy/algorithms/dqn.py index eadd20f84..5569e065e 100644 --- a/maro/rl/policy/algorithms/dqn.py +++ b/maro/rl/policy/algorithms/dqn.py @@ -19,27 +19,27 @@ class DQNConfig: reward_discount (float): Reward decay as defined in standard RL terminology. update_target_every (int): Number of gradient steps between target model updates. train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. - soft_update_coefficient (float): Soft update coefficient, e.g., - target_model = (soft_update_coefficient) * eval_model + (1-soft_update_coefficient) * target_model. + soft_update_coeff (float): Soft update coefficient, e.g., + target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. Defaults to 1.0. double (bool): If True, the next Q values will be computed according to the double DQN algorithm, i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. """ - __slots__ = ["reward_discount", "update_target_every", "train_epochs", "soft_update_coefficient", "double"] + __slots__ = ["reward_discount", "update_target_every", "train_epochs", "soft_update_coeff", "double"] def __init__( self, reward_discount: float, update_target_every: int, train_epochs: int = 1, - soft_update_coefficient: float = 0.1, + soft_update_coeff: float = 0.1, double: bool = True ): self.reward_discount = reward_discount self.update_target_every = update_target_every self.train_epochs = train_epochs - self.soft_update_coefficient = soft_update_coefficient + self.soft_update_coeff = soft_update_coeff self.double = double @@ -147,7 +147,7 @@ def learn(self): # soft-update target network self._num_steps += 1 if self._num_steps % self.config.update_target_every == 0: - self.target_q_net.soft_update(self.q_net, self.config.soft_update_coefficient) + self.target_q_net.soft_update(self.q_net, self.config.soft_update_coeff) def set_state(self, policy_state): self.q_net.load_state_dict(policy_state) From 072d9de2e212e3dbf3272970cbe5437d531d51b6 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 22 Jul 2021 07:10:38 +0000 Subject: [PATCH 393/482] removed -dqn from job name in rl example config --- examples/rl/workflows/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index f39994b74..1f9fba9bb 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -job_name: cim-dqn +job_name: cim scenario: cim mode: sync num_episodes: 5 From 103eb407bd48ebb1fe31fcfbca2a320f61451de4 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 22 Jul 2021 07:48:21 +0000 Subject: [PATCH 394/482] added tmp patch to dev.df --- docker_files/dev.df | 1 + 1 file changed, 1 insertion(+) diff --git a/docker_files/dev.df b/docker_files/dev.df index 547473057..bcfd80325 100644 --- a/docker_files/dev.df +++ b/docker_files/dev.df @@ -15,6 +15,7 @@ RUN rm -rf /var/lib/apt/lists/* # Install Python packages RUN pip3 install --upgrade pip +RUN pip install --no-cache-dir dataclasses RUN pip install --no-cache-dir Cython==0.29.14 RUN pip install --no-cache-dir pyaml==20.4.0 RUN pip install --no-cache-dir pyzmq==19.0.2 From 9c5e1359256f3c14cdca5d76a2d92bc4a71608dc Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 22 Jul 2021 10:50:26 +0000 Subject: [PATCH 395/482] renamed image name for running rl examples --- docker_files/dev.df | 2 -- examples/rl/scripts/docker/build.sh | 2 +- examples/rl/scripts/docker/docker_compose_yml.py | 2 +- setup.py | 5 ++++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docker_files/dev.df b/docker_files/dev.df index bcfd80325..f2f126eb5 100644 --- a/docker_files/dev.df +++ b/docker_files/dev.df @@ -1,5 +1,4 @@ FROM ubuntu:18.04 - WORKDIR /maro # Install Apt packages @@ -15,7 +14,6 @@ RUN rm -rf /var/lib/apt/lists/* # Install Python packages RUN pip3 install --upgrade pip -RUN pip install --no-cache-dir dataclasses RUN pip install --no-cache-dir Cython==0.29.14 RUN pip install --no-cache-dir pyaml==20.4.0 RUN pip install --no-cache-dir pyzmq==19.0.2 diff --git a/examples/rl/scripts/docker/build.sh b/examples/rl/scripts/docker/build.sh index 005cbe30a..a0b35ee5d 100644 --- a/examples/rl/scripts/docker/build.sh +++ b/examples/rl/scripts/docker/build.sh @@ -5,4 +5,4 @@ ROOTDIR=$BASEDIR/../../../../ # script to build the docker image for running the CIM scenario. docker pull redis:6 -docker build -f $ROOTDIR/docker_files/dev.df -t maro:latest $ROOTDIR \ No newline at end of file +docker build -f $ROOTDIR/docker_files/dev.df -t maro-rl:latest $ROOTDIR \ No newline at end of file diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index fd0a50e56..db56f8fa8 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -22,7 +22,7 @@ docker_compose_manifest = {"version": "3.9", "services": {"redis": {"image": "redis:6", "container_name": redis_host}}} common_spec = { "build": {"context": root_dir, "dockerfile": dockerfile_path}, - "image": "maro", + "image": "maro-rl", "volumes": [f"{rl_example_dir}:/maro/rl_examples", f"{maro_rl_dir}:/maro/maro/rl"] } diff --git a/setup.py b/setup.py index c8e5c5c27..8bdab98f8 100644 --- a/setup.py +++ b/setup.py @@ -78,6 +78,9 @@ extra_compile_args=[compile_flag]) ) +specific_requires = [] +if sys.version.startswith("3.6"): + specific_requires.append("dataclasses>=0.5") readme = io.open("./maro/README.rst", encoding="utf-8").read() @@ -141,7 +144,7 @@ "kubernetes>=12.0.1", "prompt_toolkit<3.1.0", "stringcase>=1.2.0", - ], + ] + specific_requires, entry_points={ "console_scripts": [ "maro=maro.cli.maro:main", From d96aa4457ae9551886add3f5377eeab4d2a42bac Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 28 Jul 2021 16:23:37 +0000 Subject: [PATCH 396/482] added get_loss interface for core policies --- examples/rl/scripts/docker/build.sh | 2 +- .../rl/scripts/docker/docker_compose_yml.py | 62 +++++++++++++++-- examples/rl/workflows/asynchronous/actor.py | 29 +++++--- .../workflows/asynchronous/policy_server.py | 14 ++-- examples/rl/workflows/config.yml | 14 ++-- examples/rl/workflows/general.py | 16 ++--- .../policy_manager/policy_manager.py | 15 ++-- .../rl/workflows/policy_manager/trainer.py | 17 +++-- examples/rl/workflows/synchronous/learner.py | 59 +++++++++++----- .../workflows/synchronous/rollout_worker.py | 17 +++-- maro/rl/policy/algorithms/ac.py | 68 ++++++++++--------- maro/rl/policy/algorithms/ddpg.py | 35 +++++----- maro/rl/policy/algorithms/dqn.py | 58 ++++++++-------- maro/rl/policy/algorithms/pg.py | 17 +++-- maro/rl/policy/policy.py | 4 ++ 15 files changed, 275 insertions(+), 152 deletions(-) diff --git a/examples/rl/scripts/docker/build.sh b/examples/rl/scripts/docker/build.sh index a0b35ee5d..6928b023e 100644 --- a/examples/rl/scripts/docker/build.sh +++ b/examples/rl/scripts/docker/build.sh @@ -5,4 +5,4 @@ ROOTDIR=$BASEDIR/../../../../ # script to build the docker image for running the CIM scenario. docker pull redis:6 -docker build -f $ROOTDIR/docker_files/dev.df -t maro-rl:latest $ROOTDIR \ No newline at end of file +docker build -f $ROOTDIR/docker_files/dev.df -t marorl:latest $ROOTDIR \ No newline at end of file diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index db56f8fa8..0dc64f00f 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -11,6 +11,7 @@ root_dir = dirname(dirname(rl_example_dir)) workflow_dir = join(rl_example_dir, "workflows") maro_rl_dir = join(root_dir, "maro", "rl") +maro_sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") config_path = join(workflow_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") @@ -22,8 +23,12 @@ docker_compose_manifest = {"version": "3.9", "services": {"redis": {"image": "redis:6", "container_name": redis_host}}} common_spec = { "build": {"context": root_dir, "dockerfile": dockerfile_path}, - "image": "maro-rl", - "volumes": [f"{rl_example_dir}:/maro/rl_examples", f"{maro_rl_dir}:/maro/maro/rl"] + "image": "marorl", + "volumes": [ + f"{rl_example_dir}:/maro/rl_examples", + f"{maro_rl_dir}:/maro/maro/rl", + f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" + ] } # trainer spec @@ -34,7 +39,12 @@ del trainer_spec["build"] trainer_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/trainer.py" trainer_spec["container_name"] = str_id - trainer_spec["environment"] = [f"TRAINERID={trainer_id}"] + trainer_spec["environment"] = [ + f"TRAINERID={trainer_id}", + f"TRAINGROUP={config['policy_manager']['train_group']}", + f"REDISHOST={config['redis']['host']}", + f"REDISPORT={str(config['redis']['port'])}" + ] docker_compose_manifest["services"][str_id] = trainer_spec mode = config["mode"] @@ -44,7 +54,24 @@ **common_spec, **{ "container_name": "learner", - "command": "python3 /maro/rl_examples/workflows/synchronous/learner.py" + "command": "python3 /maro/rl_examples/workflows/synchronous/learner.py", + "environment": [ + f"ROLLOUTMODE={config['sync']['rollout_mode']}", + f"NUMSTEPS={config['num_steps']}", + f"NUMWORKERS={config['sync']['num_rollout_workers']}", + f"MAXLAG={config['max_lag']}", + f"MINFINISH={config['sync']['min_finished_workers']}", + f"MAXEXRECV={config['sync']['max_extra_recv_tries']}", + f"MAXRECVTIMEO={config['sync']['extra_recv_timeout']}", + f"ROLLOUTGROUP={config['sync']['rollout_group']}", + f"NUMEPISODES={config['num_episodes']}", + f"EVALSCH={config['eval_schedule']}", + f"TRAINMODE={config['policy_manager']['train_mode']}", + f"TRAINGROUP={config['policy_manager']['train_group']}", + f"NUMTRAINERS={config['policy_manager']['num_trainers']}", + f"REDISHOST={config['redis']['host']}", + f"REDISPORT={config['redis']['port']}" + ] } } # rollout worker spec @@ -55,7 +82,13 @@ del worker_spec["build"] worker_spec["command"] = "python3 /maro/rl_examples/workflows/synchronous/rollout_worker.py" worker_spec["container_name"] = str_id - worker_spec["environment"] = [f"WORKERID={worker_id}"] + worker_spec["environment"] = [ + f"WORKERID={worker_id}", + f"ROLLOUTGROUP={config['sync']['rollout_group']}", + f"REDISHOST={config['redis']['host']}", + f"REDISPORT={config['redis']['port']}", + f"EVALSCH={config['eval_schedule']}", + ] docker_compose_manifest["services"][str_id] = worker_spec elif mode == "async": # policy server spec @@ -63,7 +96,14 @@ **common_spec, **{ "container_name": "policy_server", - "command": "python3 /maro/rl_examples/workflows/asynchronous/policy_server.py" + "command": "python3 /maro/rl_examples/workflows/asynchronous/policy_server.py", + "environment": [ + f"GROUP={config['async']['group']}", + f"NUMACTORS={config['async']['num_actors']}", + f"MAXLAG={config['max_lag']}", + f"REDISHOST={config['redis']['host']}", + f"REDISPORT={config['redis']['port']}" + ] } } # actor spec @@ -73,7 +113,15 @@ del actor_spec["build"] actor_spec["command"] = "python3 /maro/rl_examples/workflows/asynchronous/actor.py" actor_spec["container_name"] = str_id - actor_spec["environment"] = [f"ACTORID={actor_id}"] + actor_spec["environment"] = [ + f"ACTORID={actor_id}", + f"GROUP={config['async']['group']}", + f"NUMEPISODES={config['num_episodes']}", + f"NUMSTEPS={config['num_steps']}", + f"EVALSCH={config['eval_schedule']}", + f"REDISHOST={config['redis']['host']}", + f"REDISPORT={config['redis']['port']}" + ] docker_compose_manifest["services"][str_id] = actor_spec else: raise ValueError(f"mode must be 'sync' or 'async', got {mode}") diff --git a/examples/rl/workflows/asynchronous/actor.py b/examples/rl/workflows/asynchronous/actor.py index ecbe28492..1c8a24298 100644 --- a/examples/rl/workflows/asynchronous/actor.py +++ b/examples/rl/workflows/asynchronous/actor.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. import sys -from os import environ +from os import getenv from os.path import dirname, realpath from maro.rl.learning.asynchronous import actor @@ -12,20 +12,33 @@ sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import config, get_env_wrapper, get_eval_env_wrapper, log_dir, replay_agents +from general import get_env_wrapper, get_eval_env_wrapper, log_dir, replay_agents if __name__ == "__main__": - actor_id = int(environ["ACTORID"]) + actor_id = getenv("ACTORID") + if actor_id is None: + raise ValueError("Missing environment variable: ACTORID") + actor_id = int(actor_id) + + num_episodes = getenv("NUMEPISODES") + if num_episodes is None: + raise ValueError("Missing envrionment variable: NUMEPISODES") + num_episodes = int(num_episodes) + num_steps = int(getenv("NUMSTEPS", default=-1)) + actor( - config["async"]["group"], + getenv("GROUP", default="ASYNC"), actor_id, get_env_wrapper(replay_agent_ids=replay_agents[actor_id]), get_agent_wrapper(), - config["num_episodes"], - num_steps=config["num_steps"], + num_episodes, + num_steps=num_steps, eval_env_wrapper=get_eval_env_wrapper(), - eval_schedule=config["eval_schedule"], - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + eval_schedule=int(getenv("EVALSCH")), + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, log_dir=log_dir, ) diff --git a/examples/rl/workflows/asynchronous/policy_server.py b/examples/rl/workflows/asynchronous/policy_server.py index 8cc6e43d8..53b91bbed 100644 --- a/examples/rl/workflows/asynchronous/policy_server.py +++ b/examples/rl/workflows/asynchronous/policy_server.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import sys +from os import getenv from os.path import dirname, realpath from maro.rl.learning.asynchronous import policy_server @@ -11,15 +12,18 @@ sys.path.insert(0, workflow_dir) from policy_manager.policy_manager import get_policy_manager -from general import config, log_dir +from general import log_dir if __name__ == "__main__": policy_server( - config["async"]["group"], + getenv("GROUP", default="ASYNC"), get_policy_manager(), - config["async"]["num_actors"], - max_lag=config["max_lag"], - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + int(getenv("NUMACTORS", default=5)), + max_lag=int(getenv("MAXLAG", default=0)), + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, log_dir=log_dir ) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 1f9fba9bb..5cc39e2d6 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -1,11 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -job_name: cim +job: cim scenario: cim mode: sync -num_episodes: 5 -eval_schedule: 5 +num_episodes: 100 +eval_schedule: 10 num_steps: -1 max_lag: 0 # If true, the roll-out experiences will be distributed amongst roll-out workers / actors @@ -15,10 +15,10 @@ rollout_experience_distribution: false sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 3 - min_finished_workers: 2 + num_rollout_workers: 5 + min_finished_workers: 4 max_extra_recv_tries: 2 - extra_recv_timeout: 100 + extra_recv_timeout: 200 async: group: async num_actors: 3 @@ -28,4 +28,4 @@ policy_manager: num_trainers: 2 redis: host: maro-redis - port: 6379 \ No newline at end of file + port: 6379 diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 310543732..2591fd1d2 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -3,7 +3,7 @@ import importlib import sys -import yaml +from os import getenv from os.path import dirname, join, realpath workflow_dir = dirname(realpath(__file__)) @@ -12,13 +12,9 @@ if rl_example_dir not in sys.path: sys.path.insert(0, rl_example_dir) -config_path = join(workflow_dir, "config.yml") -with open(config_path, "r") as config_file: - config = yaml.safe_load(config_file) +log_dir = join(rl_example_dir, "log", getenv("JOB")) -log_dir = join(rl_example_dir, "log", config["job_name"]) - -module = importlib.import_module(f"{config['scenario']}") +module = importlib.import_module(f"{getenv('SCENARIO')}") get_env_wrapper = getattr(module, "get_env_wrapper") get_eval_env_wrapper = getattr(module, "get_eval_env_wrapper", lambda: None) @@ -33,8 +29,10 @@ post_update = getattr(module, "post_update", None) # roll-out experience distribution amongst workers -num_rollouts = config["sync"]["num_rollout_workers"] if config["mode"] == "sync" else config["async"]["num_actors"] -if config["rollout_experience_distribution"]: +mode = getenv("MODE") +num_rollouts = int(getenv("NUMWORKERS")) if mode == "sync" else int(getenv("NUMACTORS")) +exp_dist = int(getenv("EXPDIST", default=0)) +if exp_dist: replay_agents = [[] for _ in range(num_rollouts)] for i, agent in enumerate(rl_agents): replay_agents[i % num_rollouts].append(agent) diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py index 2352045f4..2b18951a5 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import sys +from os import getenv from os.path import dirname, realpath from maro.rl.policy import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager @@ -10,11 +11,10 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import config, log_dir, rl_policy_func_index, update_trigger, warmup +from general import log_dir, rl_policy_func_index, update_trigger, warmup def get_policy_manager(): - train_mode = config["policy_manager"]["train_mode"] - num_trainers = config["policy_manager"]["num_trainers"] + train_mode = getenv("TRAINMODE", default="single-process") policy_dict = {name: func() for name, func in rl_policy_func_index.items()} if train_mode == "single-process": return LocalPolicyManager( @@ -23,6 +23,8 @@ def get_policy_manager(): warmup=warmup, log_dir=log_dir ) + + num_trainers = int(getenv("NUMTRAINERS", default=5)) if train_mode == "multi-process": return MultiProcessPolicyManager( policy_dict, @@ -35,11 +37,14 @@ def get_policy_manager(): if train_mode == "multi-node": return MultiNodePolicyManager( policy_dict, - config["policy_manager"]["train_group"], + getenv("TRAINGROUP", default="TRAIN"), num_trainers, update_trigger=update_trigger, warmup=warmup, - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, log_dir=log_dir ) diff --git a/examples/rl/workflows/policy_manager/trainer.py b/examples/rl/workflows/policy_manager/trainer.py index 628955b28..7ce5cf6ce 100644 --- a/examples/rl/workflows/policy_manager/trainer.py +++ b/examples/rl/workflows/policy_manager/trainer.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. import sys -from os import environ +from os import getenv from os.path import dirname, realpath from maro.rl.policy import trainer_node @@ -11,14 +11,21 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import config, log_dir, rl_policy_func_index +from general import log_dir, rl_policy_func_index if __name__ == "__main__": + trainer_id = getenv("TRAINERID") + if trainer_id is None: + raise ValueError("missing environment variable: TRAINERID") + trainer_node( - config["policy_manager"]["train_group"], - int(environ["TRAINERID"]), + getenv("TRAINGROUP", default="TRAIN"), + int(trainer_id), rl_policy_func_index, - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, log_dir=log_dir ) diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py index 7cc14583d..7d83bc740 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import sys +from os import getenv from os.path import dirname, realpath from maro.rl.learning.synchronous import ( @@ -14,42 +15,62 @@ from agent_wrapper import get_agent_wrapper from policy_manager.policy_manager import get_policy_manager -from general import config, post_collect, post_evaluate, get_env_wrapper, log_dir +from general import post_collect, post_evaluate, get_env_wrapper, log_dir def get_rollout_manager(): - rollout_mode = config["sync"]["rollout_mode"] + rollout_mode = getenv("ROLLOUTMODE", default="single-process") + num_steps = int(getenv("NUMSTEPS", default=-1)) if rollout_mode == "single-process": return LocalRolloutManager( get_env_wrapper(), get_agent_wrapper(), - num_steps=config["num_steps"], + num_steps=num_steps, post_collect=post_collect, post_evaluate=post_evaluate, log_dir=log_dir ) + + num_workers = int(getenv("NUMWORKERS", default=5)) if rollout_mode == "multi-process": return MultiProcessRolloutManager( - config["sync"]["num_rollout_workers"], + num_workers, get_env_wrapper, get_agent_wrapper, - num_steps=config["num_steps"], + num_steps=num_steps, post_collect=post_collect, post_evaluate=post_evaluate, log_dir=log_dir, ) + + max_lag = int(getenv("MAXLAG", default=0)) + min_finished_workers = getenv("MINFINISH") + if min_finished_workers is not None: + min_finished_workers = int(min_finished_workers) + + max_extra_recv_tries = getenv("MAXEXRECV") + if max_extra_recv_tries is not None: + max_extra_recv_tries = int(max_extra_recv_tries) + + extra_recv_timeout = getenv("MAXRECVTIMEO") + if extra_recv_timeout is not None: + extra_recv_timeout = int(extra_recv_timeout) + if rollout_mode == "multi-node": return MultiNodeRolloutManager( - config["sync"]["rollout_group"], - config["sync"]["num_rollout_workers"], - num_steps=config["num_steps"], - max_lag=config["max_lag"], - min_finished_workers=config["sync"]["min_finished_workers"], - # max_extra_recv_tries=config["sync"]["max_extra_recv_tries"], - extra_recv_timeout=config["sync"]["extra_recv_timeout"], + getenv("ROLLOUTGROUP", default="ROLLOUT"), + num_workers, + num_steps=num_steps, + max_lag=max_lag, + min_finished_workers=min_finished_workers, + max_extra_recv_tries=max_extra_recv_tries, + extra_recv_timeout=extra_recv_timeout, post_collect=post_collect, post_evaluate=post_evaluate, - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, ) raise ValueError( @@ -58,11 +79,15 @@ def get_rollout_manager(): if __name__ == "__main__": + num_episodes = getenv("NUMEPISODES") + if num_episodes is None: + raise ValueError("Missing envrionment variable: NUMEPISODES") + learner = Learner( - policy_manager=get_policy_manager(), - rollout_manager=get_rollout_manager(), - num_episodes=config["num_episodes"], - eval_schedule=config["eval_schedule"], + get_policy_manager(), + get_rollout_manager(), + int(num_episodes), + eval_schedule=int(getenv("EVALSCH")), log_dir=log_dir ) learner.run() diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 2666fb140..269beb3f8 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. import sys -from os import environ +from os import getenv from os.path import dirname, realpath from maro.rl.learning.synchronous import rollout_worker_node @@ -13,17 +13,24 @@ sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import config, get_env_wrapper, get_eval_env_wrapper, log_dir, replay_agents +from general import get_env_wrapper, get_eval_env_wrapper, log_dir, replay_agents if __name__ == "__main__": - worker_id = int(environ["WORKERID"]) + worker_id = getenv("WORKERID") + if worker_id is None: + raise ValueError("Missing environment variable: WORKERID") + worker_id = int(worker_id) + rollout_worker_node( - config["sync"]["rollout_group"], + getenv("ROLLOUTGROUP", default="ROLLOUT"), worker_id, get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), get_agent_wrapper(), eval_env_wrapper=get_eval_env_wrapper(), - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])}, + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, log_dir=log_dir ) diff --git a/maro/rl/policy/algorithms/ac.py b/maro/rl/policy/algorithms/ac.py index 8497f6f2d..524bfd195 100644 --- a/maro/rl/policy/algorithms/ac.py +++ b/maro/rl/policy/algorithms/ac.py @@ -7,7 +7,7 @@ import torch from torch.distributions import Categorical -from maro.rl.experience import ExperienceStore, UniformSampler +from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler from maro.rl.model import DiscreteACNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -107,42 +107,46 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else (actions, log_p) + def get_loss(self, batch: ExperienceSet): + self.ac_net.train() + states, next_states = batch.states, batch.next_states + actions = torch.from_numpy(np.asarray([act[0] for act in batch.actions])).to(self.device) + log_p = torch.from_numpy(np.asarray([act[1] for act in batch.actions])).to(self.device) + rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) + + action_probs, state_values = self.ac_net(states) + state_values = state_values.squeeze() + with torch.no_grad(): + next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() + return_est = rewards + self.config.reward_discount * next_state_values + advantages = return_est - state_values + + # actor loss + log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) + log_p_new = torch.clamp(log_p_new, min=self.config.min_logp, max=.0) + if self.config.clip_ratio is not None: + ratio = torch.exp(log_p_new - log_p) + clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() + else: + actor_loss = -(log_p_new * advantages).mean() + + # critic_loss + critic_loss = self.config.critic_loss_func(state_values, return_est) + + # total loss + loss = actor_loss + self.config.critic_loss_coeff * critic_loss + if self.config.entropy_coeff is not None: + loss -= self.config.entropy_coeff * Categorical(action_probs).entropy().mean() + + return loss + def learn(self): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() for _ in range(self.config.train_epochs): - experience_set = self.sampler.get() - states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray([act[0] for act in experience_set.actions])).to(self.device) - log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) - - action_probs, state_values = self.ac_net(states) - state_values = state_values.squeeze() - with torch.no_grad(): - next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() - return_est = rewards + self.config.reward_discount * next_state_values - advantages = return_est - state_values - - # actor loss - log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - log_p_new = torch.clamp(log_p_new, min=self.config.min_logp, max=.0) - if self.config.clip_ratio is not None: - ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() - else: - actor_loss = -(log_p_new * advantages).mean() - - # critic_loss - critic_loss = self.config.critic_loss_func(state_values, return_est) - - # total loss - loss = actor_loss + self.config.critic_loss_coeff * critic_loss - if self.config.entropy_coeff is not None: - loss -= self.config.entropy_coeff * Categorical(action_probs).entropy().mean() + loss = self.get_loss(self.sampler.get()) self.ac_net.step(loss) - if self._post_step: self._post_step(loss.detach().cpu().numpy(), self.tracker) diff --git a/maro/rl/policy/algorithms/ddpg.py b/maro/rl/policy/algorithms/ddpg.py index 974348962..7d95e8692 100644 --- a/maro/rl/policy/algorithms/ddpg.py +++ b/maro/rl/policy/algorithms/ddpg.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience import ExperienceStore, UniformSampler +from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler from maro.rl.exploration import GaussianNoiseExploration from maro.rl.model import ContinuousACNet from maro.rl.policy import AbsCorePolicy @@ -104,25 +104,28 @@ def choose_action(self, states) -> Union[float, np.ndarray]: return actions[0] if len(actions) == 1 else actions + def get_loss(self, batch: ExperienceSet): + self.ac_net.train() + states, next_states = batch.states, batch.next_states + actual_actions = torch.from_numpy(batch.actions).to(self.device) + rewards = torch.from_numpy(batch.rewards).to(self.device) + if len(actual_actions.shape) == 1: + actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) + + with torch.no_grad(): + next_q_values = self.target_ac_net.value(next_states) + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) + q_value_loss = self.config.q_value_loss_func(q_values, target_q_values) + policy_loss = -self.ac_net.value(states).mean() + return policy_loss + self.config.q_value_loss_coeff * q_value_loss + def learn(self): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() for _ in range(self.config.train_epochs): - experience_set = self.sampler.get() - states, next_states = experience_set.states, experience_set.next_states - actual_actions = torch.from_numpy(experience_set.actions).to(self.device) - rewards = torch.from_numpy(experience_set.rewards).to(self.device) - if len(actual_actions.shape) == 1: - actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - - with torch.no_grad(): - next_q_values = self.target_ac_net.value(next_states) - target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) - - q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) - q_value_loss = self.config.q_value_loss_func(q_values, target_q_values) - policy_loss = -self.ac_net.value(states).mean() - loss = policy_loss + self.config.q_value_loss_coeff * q_value_loss + loss = self.get_loss(self.sampler.get()) self.ac_net.step(loss) if self._post_step: self._post_step(loss.detach().cpu().numpy(), self.tracker) diff --git a/maro/rl/policy/algorithms/dqn.py b/maro/rl/policy/algorithms/dqn.py index 5569e065e..00d03df55 100644 --- a/maro/rl/policy/algorithms/dqn.py +++ b/maro/rl/policy/algorithms/dqn.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience import ExperienceStore, PrioritizedSampler, UniformSampler +from maro.rl.experience import ExperienceSet, ExperienceStore, PrioritizedSampler, UniformSampler from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.model import DiscreteQNet from maro.rl.policy import AbsCorePolicy @@ -108,37 +108,39 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions + def get_loss(self, batch: ExperienceSet): + self.q_net.train() + states, next_states = batch.states, batch.next_states + actions = torch.from_numpy(np.asarray(batch.actions)).to(self.device) + rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) + if self.prioritized_experience_replay: + indexes = [info["index"] for info in batch.info] + is_weights = torch.tensor([info["is_weight"] for info in batch.info]).to(self.device) + + # get target Q values + with torch.no_grad(): + if self.config.double: + actions_by_eval_q_net = self.q_net.get_action(next_states)[0] + next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) + else: + next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) + + target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + + # gradient step + q_values = self.q_net.q_values(states, actions) + if self.prioritized_experience_replay: + td_errors = target_q_values - q_values + self.sampler.update(indexes, td_errors.detach().cpu().numpy()) + return (td_errors * is_weights).mean() + else: + return self._loss_func(q_values, target_q_values) + def learn(self): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() for _ in range(self.config.train_epochs): - # sample from the replay memory - experience_set = self.sampler.get() - states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.device) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) - if self.prioritized_experience_replay: - indexes = [info["index"] for info in experience_set.info] - is_weights = torch.tensor([info["is_weight"] for info in experience_set.info]).to(self.device) - - # get target Q values - with torch.no_grad(): - if self.config.double: - actions_by_eval_q_net = self.q_net.get_action(next_states)[0] - next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) - else: - next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) - - target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) - - # gradient step - q_values = self.q_net.q_values(states, actions) - if self.prioritized_experience_replay: - td_errors = target_q_values - q_values - loss = (td_errors * is_weights).mean() - self.sampler.update(indexes, td_errors.detach().cpu().numpy()) - else: - loss = self._loss_func(q_values, target_q_values) + loss = self.get_loss(self.sampler.get()) self.q_net.step(loss) if self._post_step: diff --git a/maro/rl/policy/algorithms/pg.py b/maro/rl/policy/algorithms/pg.py index e53d9507f..342d151dd 100644 --- a/maro/rl/policy/algorithms/pg.py +++ b/maro/rl/policy/algorithms/pg.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.experience import ExperienceStore, UniformSampler +from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler from maro.rl.model import DiscretePolicyNet from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_truncated_cumulative_reward @@ -74,6 +74,14 @@ def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p + def get_loss(self, batch: ExperienceSet): + self.policy_net.train() + log_p = torch.from_numpy(np.asarray([act[1] for act in batch.actions])).to(self.device) + rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) + returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) + returns = torch.from_numpy(returns).to(self.device) + return -(log_p * returns).mean() + def learn(self): """ This should be called at the end of a simulation episode and the experiences obtained from @@ -82,12 +90,7 @@ def learn(self): """ assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." self.policy_net.train() - experience_set = self.sampler.get() - log_p = torch.from_numpy(np.asarray([act[1] for act in experience_set.actions])).to(self.device) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) - returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) - returns = torch.from_numpy(returns).to(self.device) - loss = -(log_p * returns).mean() + loss = self.get_loss(self.sampler.get()) self.policy_net.step(loss) if self._post_step: diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index d368ce4ed..9fb22cea3 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -66,6 +66,10 @@ def learn(self): """ raise NotImplementedError + def get_loss(self, batch: ExperienceSet): + """Compute the loss for a sample experience batch.""" + pass + @abstractmethod def get_state(self): """Return the current state of the policy. From 1f3736963d01bdc5c5c384cbeaeb6ec5388f36e5 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 30 Jul 2021 08:50:28 +0000 Subject: [PATCH 397/482] added policy manager in rl_toolkit.rst --- docs/source/key_components/rl_toolkit.rst | 37 ++++++++++++++++------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index ae677db81..abe67d186 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -14,9 +14,9 @@ In synchronous mode, a central controler executes learning cycles that consist o policy update. In a strictly synchronous learning process, the coordinator would wait for all data collectors, a.k.a. "roll-out workers", to return their results before moving on to the policy update phase. So what if a slow worker drags the whole process down? We provide users the flexibility to loosen the restriction by specifying the -minimum number of workers required to report back before proceeding to the next phase. If one is concerned about +minimum number of workers required to receive from before proceeding to the next phase. If one is concerned about losing precious data in the case of all workers reporting back at roughly the same time, we also provide the option -to continue to receive after receiving the minimum number of results, but with timeouts to keep the wait time +to continue to receive after receiving the minimum number of results, but with a timeout to keep the wait time upperbounded. Note that the transition from the policy update phase to the data collection phase is still strictly synchronous. This means that in the case of the policy instances distributed amongst a set of trainer nodes, the central controller waits until all trainers report back with the latest policy states before starting the next @@ -26,10 +26,9 @@ cycle. The components required for synchronous learning include: * Learner, which is the central coordinator for a learning process. The learner consists of a roll-out manager and -a training manager and executes learning cycles that alternate between data collection and policy update. + a training manager and executes learning cycles that alternate between data collection and policy update. * Rollout manager, which is responsible for collecting simulation data, in local or distributed fashion. -* Policy manager, which manages a set of policies and controls their updates. The policy instances may - reside with the manager or be distributed amongst a set of processes or remote nodes for parallelized training. +* Policy manager, which controls the policy update phase of a learning cycle. See "Policy Manager" below for details. .. image:: ../images/rl/learner.svg @@ -42,11 +41,6 @@ a training manager and executes learning cycles that alternate between data coll :alt: Overview -.. image:: ../images/rl/policy_manager.svg - :target: ../images/rl/policy_manager.svg - :alt: RL Overview - - Asynchronous Learning ===================== @@ -59,7 +53,7 @@ but always sends the latest policy states to every single actor. The components required for asynchronous learning include: * actor, which alternates between sending collected simulation data to the policy server and receiving updated -policy states from it. + policy states from it. * policy server, which receives data from the actors and update the policy states when necessary. @@ -110,6 +104,27 @@ based on which updates can be made. raise NotImplementedError +Policy Manager +-------------- + +A policy manager is an abstraction that controls policy update. It houses the latest policy states. +In synchrounous learning, the policy manager controls the policy update phase of a learning cycle. +In asynchrounous learning, the policy manager is the centerpiece of the policy server process. +Individual policy updates, however, may or may not occur within the policy manager itself depending +on the policy manager type used. The provided policy manager classes include: + +* ``LocalPolicyManager``, where the policies are updated within the manager itself; +* ``MultiProcessPolicyManager``, which distributes policies amongst a set of trainer processes to parallelize + policy update; +* ``MultiNodePolicyManager``, which distributes policies amongst a set of remote compute nodes to parallelize + policy update; + + +.. image:: ../images/rl/policy_manager.svg + :target: ../images/rl/policy_manager.svg + :alt: RL Overview + + Core Model ---------- From 12ac058c58314faf84cf5099919bc5b4ea84c6e8 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 30 Jul 2021 08:54:23 +0000 Subject: [PATCH 398/482] 1. env_wrapper bug fix; 2. policy manager update logic refinement --- maro/rl/learning/env_wrapper.py | 2 +- maro/rl/policy/policy_manager.py | 15 ++++++++++----- maro/rl/policy/trainer.py | 4 ++-- maro/rl/utils/message_enums.py | 1 + 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index 8555d33c1..dd752c83e 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -65,7 +65,7 @@ def __init__( self._get_experience_func = get_experience_func self._post_step = post_step - replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids + replay_agent_ids = self.env.agent_idx_list if replay_agent_ids is None else replay_agent_ids self._replay_buffer = {agent_id: defaultdict(list) for agent_id in replay_agent_ids} self._transition_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index f4224a7a0..5ba440e38 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -321,11 +321,16 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp - trackers = [] - for reply in self._proxy.scatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())): - trackers.append(reply.body[MsgKey.TRACKER]) - for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): - self.policy_dict[policy_name].set_state(policy_state) + trackers, dones = [], 0 + self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())) + for msg in self._proxy.receive(): + if msg.tag == MsgTag.TRAIN_DONE: + trackers.append(msg.body[MsgKey.TRACKER]) + for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): + self.policy_dict[policy_name].set_state(policy_state) + dones += 1 + if dones == len(msg_body_by_dest): + break if updated: self._update_history.append(updated) diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index 4b025dfaa..218c69d9a 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -96,5 +96,5 @@ def trainer_node( MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.EXPERIENCES]}, MsgKey.TRACKER: {name: policy_dict[name].tracker for name in msg.body[MsgKey.EXPERIENCES]} } - logger.debug(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, body=msg_body) + logger.info(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, tag=MsgTag.TRAIN_DONE, body=msg_body) diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 4a76fd279..df6813bc8 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -17,6 +17,7 @@ class MsgTag(Enum): ABORT_ROLLOUT = "abort_rollout" EVAL_DONE = "eval_done" COLLECT_DONE = "collect_done" + TRAIN_DONE = "train_done" DONE = "done" EXIT = "exit" From fc14e66b763831e9d62987e514b8d6716de6f7fd Mon Sep 17 00:00:00 2001 From: yaqiu Date: Tue, 3 Aug 2021 09:24:49 +0000 Subject: [PATCH 399/482] refactored policy and algorithms --- maro/rl/{policy => }/algorithms/__init__.py | 6 +- maro/rl/algorithms/abs_algorithm.py | 77 +++++++++++++++++++ maro/rl/{policy => }/algorithms/ac.py | 55 +++++-------- maro/rl/{policy => }/algorithms/ddpg.py | 37 ++++----- maro/rl/{policy => }/algorithms/dqn.py | 81 +++++++++----------- maro/rl/algorithms/index.py | 55 +++++++++++++ maro/rl/{policy => }/algorithms/pg.py | 42 ++++------ maro/rl/model/core_model.py | 30 +++++++- maro/rl/policy/algorithms/rl_policy_index.py | 55 ------------- maro/rl/policy/policy.py | 74 ++++++++++++------ maro/rl/policy/policy_manager.py | 67 ++++++++++------ maro/rl/policy/trainer.py | 30 +++++++- maro/rl/utils/message_enums.py | 5 ++ 13 files changed, 380 insertions(+), 234 deletions(-) rename maro/rl/{policy => }/algorithms/__init__.py (63%) create mode 100644 maro/rl/algorithms/abs_algorithm.py rename maro/rl/{policy => }/algorithms/ac.py (77%) rename maro/rl/{policy => }/algorithms/ddpg.py (82%) rename maro/rl/{policy => }/algorithms/dqn.py (65%) create mode 100644 maro/rl/algorithms/index.py rename maro/rl/{policy => }/algorithms/pg.py (75%) delete mode 100644 maro/rl/policy/algorithms/rl_policy_index.py diff --git a/maro/rl/policy/algorithms/__init__.py b/maro/rl/algorithms/__init__.py similarity index 63% rename from maro/rl/policy/algorithms/__init__.py rename to maro/rl/algorithms/__init__.py index 8449e3e59..1b4dfce5f 100644 --- a/maro/rl/policy/algorithms/__init__.py +++ b/maro/rl/algorithms/__init__.py @@ -1,16 +1,18 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .abs_algorithm import AbsAlgorithm from .ac import ActorCritic, ActorCriticConfig from .ddpg import DDPG, DDPGConfig from .dqn import DQN, DQNConfig from .pg import PolicyGradient, PolicyGradientConfig -from .rl_policy_index import get_rl_policy_cls, get_rl_policy_config_cls, get_rl_policy_model_cls +from .index import get_algorithm_cls, get_algorithm_config_cls, get_algorithm_model_cls __all__ = [ + "AbsAlgorithm", "ActorCritic", "ActorCriticConfig", "DDPG", "DDPGConfig", "DQN", "DQNConfig", "PolicyGradient", "PolicyGradientConfig", - "get_rl_policy_cls", "get_rl_policy_config_cls", "get_rl_policy_model_cls" + "get_algorithm_cls", "get_algorithm_config_cls", "get_algorithm_model_cls" ] diff --git a/maro/rl/algorithms/abs_algorithm.py b/maro/rl/algorithms/abs_algorithm.py new file mode 100644 index 000000000..8ca363670 --- /dev/null +++ b/maro/rl/algorithms/abs_algorithm.py @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from typing import Callable + +from maro.rl.experience import ExperienceSet +from maro.rl.exploration import AbsExploration + + +class AbsAlgorithm(ABC): + """Policy that can update itself using simulation experiences. + + Reinforcement learning (RL) policies should inherit from this. + + Args: + exploration (AbsExploration): Exploration strategy for generating exploratory actions. Defaults to None. + post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking + the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. + """ + def __init__(self, exploration: AbsExploration = None, post_learn: Callable = None): + super().__init__() + self.exploration = exploration + self.exploring = True + + @abstractmethod + def choose_action(self, state, explore: bool = False): + raise NotImplementedError + + def get_update_info(self, experience_batch: ExperienceSet): + pass + + def apply(self): + pass + + @abstractmethod + def learn(self, experience_batch: ExperienceSet): + """Update logic is implemented here.""" + raise NotImplementedError + + @abstractmethod + def get_state(self): + """Return the current state of the policy. + + The implementation must be in correspondence with that of ``set_state``. For example, if a torch model + is contained in the policy, ``get_state`` may include a call to ``state_dict()`` on the model, while + ``set_state`` should accordingly include ``load_state_dict()``. + """ + pass + + @abstractmethod + def set_state(self, policy_state): + """Set the policy state to ``policy_state``. + + The implementation must be in correspondence with that of ``get_state``. For example, if a torch model + is contained in the policy, ``set_state`` may include a call to ``load_state_dict()`` on the model, while + ``get_state`` should accordingly include ``state_dict()``. + """ + pass + + def explore(self): + self.exploring = False + + def exploit(self): + self.exploring = True + + def exploration_step(self): + if self.exploration: + self.exploration.step() + + def load(self, path: str): + """Load the policy state from disk.""" + pass + + def save(self, path: str): + """Save the policy state to disk.""" + pass diff --git a/maro/rl/policy/algorithms/ac.py b/maro/rl/algorithms/ac.py similarity index 77% rename from maro/rl/policy/algorithms/ac.py rename to maro/rl/algorithms/ac.py index 524bfd195..6ab6d2929 100644 --- a/maro/rl/policy/algorithms/ac.py +++ b/maro/rl/algorithms/ac.py @@ -7,9 +7,9 @@ import torch from torch.distributions import Categorical -from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler +from maro.rl.algorithms import AbsAlgorithm +from maro.rl.experience import ExperienceSet from maro.rl.model import DiscreteACNet -from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -18,7 +18,6 @@ class ActorCriticConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. - train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". min_logp (float): Lower bound for clamping logP values during learning. This is to prevent logP from becoming @@ -32,14 +31,13 @@ class ActorCriticConfig: Defaults to 1. """ __slots__ = [ - "reward_discount", "train_epochs", "critic_loss_func", "min_logp", "critic_loss_coeff", "entropy_coeff", - "clip_ratio", "clear_experience_memory_every" + "reward_discount", "critic_loss_func", "min_logp", "critic_loss_coeff", "entropy_coeff", "clip_ratio", + "clear_experience_memory_every" ] def __init__( self, reward_discount: float, - train_epochs: int = 1, critic_loss_cls="mse", min_logp: float = None, critic_loss_coeff: float = 1.0, @@ -48,7 +46,6 @@ def __init__( clear_experience_memory_every: int = 1 ): self.reward_discount = reward_discount - self.train_epochs = train_epochs self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.min_logp = min_logp self.critic_loss_coeff = critic_loss_coeff @@ -57,7 +54,7 @@ def __init__( self.clear_experience_memory_every = clear_experience_memory_every -class ActorCritic(AbsCorePolicy): +class ActorCritic(AbsAlgorithm): """Actor Critic algorithm with separate policy and value models. References: @@ -67,32 +64,15 @@ class ActorCritic(AbsCorePolicy): Args: ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. config: Configuration for the AC algorithm. - experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences - generated by the policy. - experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to - ``UnifromSampler``. - experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ - def __init__( - self, - ac_net: DiscreteACNet, - config: ActorCriticConfig, - experience_store: ExperienceStore, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs: dict = {}, - post_step: Callable = None - ): + def __init__(self, ac_net: DiscreteACNet, config: ActorCriticConfig, post_step: Callable = None): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") - super().__init__( - experience_store, - experience_sampler_cls=experience_sampler_cls, - experience_sampler_kwargs=experience_sampler_kwargs - ) + super().__init__() self.ac_net = ac_net self.config = config self._post_step = post_step @@ -107,7 +87,13 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else (actions, log_p) - def get_loss(self, batch: ExperienceSet): + def get_update_info(self, experience_batch: ExperienceSet): + return self.ac_net.get_gradients(self._get_loss(experience_batch)) + + def apply(self, grad_dict: dict): + self.ac_net.apply(grad_dict) + + def _get_loss(self, batch: ExperienceSet): self.ac_net.train() states, next_states = batch.states, batch.next_states actions = torch.from_numpy(np.asarray([act[0] for act in batch.actions])).to(self.device) @@ -141,14 +127,13 @@ def get_loss(self, batch: ExperienceSet): return loss - def learn(self): + def learn(self, experience_batch: ExperienceSet): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." - self.ac_net.train() - for _ in range(self.config.train_epochs): - loss = self.get_loss(self.sampler.get()) - self.ac_net.step(loss) - if self._post_step: - self._post_step(loss.detach().cpu().numpy(), self.tracker) + self.ac_net.train() + loss = self._get_loss(experience_batch) + self.ac_net.step(loss) + if self._post_step: + self._post_step(loss.detach().cpu().numpy(), self.tracker) # Empty the experience store due to the on-policy nature of the algorithm. self._num_learn_calls += 1 diff --git a/maro/rl/policy/algorithms/ddpg.py b/maro/rl/algorithms/ddpg.py similarity index 82% rename from maro/rl/policy/algorithms/ddpg.py rename to maro/rl/algorithms/ddpg.py index 7d95e8692..6faabe78e 100644 --- a/maro/rl/policy/algorithms/ddpg.py +++ b/maro/rl/algorithms/ddpg.py @@ -6,10 +6,10 @@ import numpy as np import torch -from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler +from maro.rl.algorithms import AbsAlgorithm +from maro.rl.experience import ExperienceSet from maro.rl.exploration import GaussianNoiseExploration from maro.rl.model import ContinuousACNet -from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_torch_loss_cls @@ -49,7 +49,7 @@ def __init__( self.soft_update_coeff = soft_update_coeff -class DDPG(AbsCorePolicy): +class DDPG(AbsAlgorithm): """The Deep Deterministic Policy Gradient (DDPG) algorithm. References: @@ -59,11 +59,6 @@ class DDPG(AbsCorePolicy): Args: ac_net (ContinuousACNet): DDPG policy and q-value models. config (DDPGConfig): Configuration for DDPG algorithm. - experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences - generated by the policy. - experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to - ``UnifromSampler``. - experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ @@ -71,21 +66,13 @@ def __init__( self, ac_net: ContinuousACNet, config: DDPGConfig, - experience_store: ExperienceStore, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs: dict = {}, exploration=GaussianNoiseExploration(), post_step=None ): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") - super().__init__( - experience_store, - experience_sampler_cls=experience_sampler_cls, - experience_sampler_kwargs=experience_sampler_kwargs, - exploration=exploration - ) + super().__init__(exploration=exploration) self.ac_net = ac_net if self.ac_net.trainable: self.target_ac_net = ac_net.copy() @@ -97,14 +84,22 @@ def __init__( self.device = self.ac_net.device self._num_steps = 0 - def choose_action(self, states) -> Union[float, np.ndarray]: + def choose_action(self, states, explore: bool = False) -> Union[float, np.ndarray]: self.ac_net.eval() with torch.no_grad(): actions = self.ac_net.get_action(states).cpu().numpy() + if explore: + actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def get_loss(self, batch: ExperienceSet): + def get_update_info(self, experience_batch: ExperienceSet): + return self.ac_net.get_gradients(self._get_loss(experience_batch)) + + def apply(self, grad_dict: dict): + self.ac_net.apply(grad_dict) + + def _get_loss(self, batch: ExperienceSet): self.ac_net.train() states, next_states = batch.states, batch.next_states actual_actions = torch.from_numpy(batch.actions).to(self.device) @@ -121,11 +116,11 @@ def get_loss(self, batch: ExperienceSet): policy_loss = -self.ac_net.value(states).mean() return policy_loss + self.config.q_value_loss_coeff * q_value_loss - def learn(self): + def learn(self, experience_batch: ExperienceSet): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() for _ in range(self.config.train_epochs): - loss = self.get_loss(self.sampler.get()) + loss = self._get_loss(experience_batch) self.ac_net.step(loss) if self._post_step: self._post_step(loss.detach().cpu().numpy(), self.tracker) diff --git a/maro/rl/policy/algorithms/dqn.py b/maro/rl/algorithms/dqn.py similarity index 65% rename from maro/rl/policy/algorithms/dqn.py rename to maro/rl/algorithms/dqn.py index 00d03df55..cada6efba 100644 --- a/maro/rl/policy/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -6,10 +6,10 @@ import numpy as np import torch -from maro.rl.experience import ExperienceSet, ExperienceStore, PrioritizedSampler, UniformSampler +from maro.rl.algorithms import AbsAlgorithm +from maro.rl.experience import ExperienceSet, PrioritizedSampler from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.model import DiscreteQNet -from maro.rl.policy import AbsCorePolicy class DQNConfig: @@ -18,7 +18,6 @@ class DQNConfig: Args: reward_discount (float): Reward decay as defined in standard RL terminology. update_target_every (int): Number of gradient steps between target model updates. - train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. Defaults to 1.0. @@ -26,24 +25,22 @@ class DQNConfig: i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. """ - __slots__ = ["reward_discount", "update_target_every", "train_epochs", "soft_update_coeff", "double"] + __slots__ = ["reward_discount", "update_target_every", "soft_update_coeff", "double"] def __init__( self, reward_discount: float, update_target_every: int, - train_epochs: int = 1, soft_update_coeff: float = 0.1, double: bool = True ): self.reward_discount = reward_discount self.update_target_every = update_target_every - self.train_epochs = train_epochs self.soft_update_coeff = soft_update_coeff self.double = double -class DQN(AbsCorePolicy): +class DQN(AbsAlgorithm): """The Deep-Q-Networks algorithm. See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. @@ -51,11 +48,6 @@ class DQN(AbsCorePolicy): Args: q_net (DiscreteQNet): Q-value model. config (DQNConfig): Configuration for DQN algorithm. - experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences - generated by the policy. - experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to - ``UnifromSampler``. - experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. exploration (DiscreteSpaceExploration): Exploration strategy for generating exploratory actions. Defaults to an ``EpsilonGreedyExploration`` instance. post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking @@ -65,21 +57,13 @@ def __init__( self, q_net: DiscreteQNet, config: DQNConfig, - experience_store: ExperienceStore, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs: dict = {}, exploration: DiscreteSpaceExploration = EpsilonGreedyExploration(), post_step: Callable = None ): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__( - experience_store, - experience_sampler_cls=experience_sampler_cls, - experience_sampler_kwargs=experience_sampler_kwargs, - exploration=exploration - ) + super().__init__(exploration=exploration) self.q_net = q_net if self.q_net.trainable: self.target_q_net = q_net.copy() @@ -89,13 +73,12 @@ def __init__( self.config = config self._post_step = post_step self.device = self.q_net.device - self._num_steps = 0 self.prioritized_experience_replay = isinstance(self.sampler, PrioritizedSampler) if not self.prioritized_experience_replay: self._loss_func = torch.nn.MSELoss() - def choose_action(self, states) -> Union[int, np.ndarray]: + def choose_action(self, states, explore: bool = True) -> Union[int, np.ndarray]: self.q_net.eval() with torch.no_grad(): q_for_all_actions = self.q_net(states) # (batch_size, num_actions) @@ -104,18 +87,23 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = actions.cpu().numpy() if self.exploration.action_space is None: self.exploration.set_action_space(np.arange(q_for_all_actions.shape[1])) - if self.exploring: + if explore: actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def get_loss(self, batch: ExperienceSet): - self.q_net.train() - states, next_states = batch.states, batch.next_states - actions = torch.from_numpy(np.asarray(batch.actions)).to(self.device) - rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) + def get_update_info(self, experience_batch: ExperienceSet): + return self.q_net.get_gradients(self._get_loss(experience_batch)) + + def apply(self, grad_dict: dict): + self.q_net.apply(grad_dict) + + def _get_loss(self, experience_batch: ExperienceSet): + states, next_states = experience_batch.states, experience_batch.next_states + actions = torch.from_numpy(np.asarray(experience_batch.actions)).to(self.device) + rewards = torch.from_numpy(np.asarray(experience_batch.rewards)).to(self.device) if self.prioritized_experience_replay: - indexes = [info["index"] for info in batch.info] - is_weights = torch.tensor([info["is_weight"] for info in batch.info]).to(self.device) + indexes = [info["index"] for info in experience_batch.info] + is_weights = torch.tensor([info["is_weight"] for info in experience_batch.info]).to(self.device) # get target Q values with torch.no_grad(): @@ -136,26 +124,27 @@ def get_loss(self, batch: ExperienceSet): else: return self._loss_func(q_values, target_q_values) - def learn(self): + def learn(self, experience_batch: ExperienceSet): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() - for _ in range(self.config.train_epochs): - loss = self.get_loss(self.sampler.get()) - self.q_net.step(loss) - if self._post_step: - self._post_step(loss.detach().cpu().numpy(), self.tracker) + loss = self._get_loss(experience_batch) + self.q_net.step(loss) + if self._post_step: + self._post_step(loss.detach().cpu().numpy(), self.tracker) - # soft-update target network - self._num_steps += 1 - if self._num_steps % self.config.update_target_every == 0: - self.target_q_net.soft_update(self.q_net, self.config.soft_update_coeff) + def update_target(self, num_steps: int): + # soft-update target network + if num_steps % self.config.update_target_every == 0: + self.target_q_net.soft_update(self.q_net, self.config.soft_update_coeff) def set_state(self, policy_state): - self.q_net.load_state_dict(policy_state) - self.target_q_net = self.q_net.copy() if self.q_net.trainable else None - if self.target_q_net: - self.target_q_net.eval() + self.q_net.load_state_dict(policy_state["eval"]) + if "target" in policy_state: + self.target_q_net.load_state_dict(policy_state["target"]) def get_state(self): - return self.q_net.state_dict() + policy_state = {"eval": self.q_net.state_dict()} + if self.target_q_net: + policy_state["target"] = self.target_q_net.state_dict() + return policy_state diff --git a/maro/rl/algorithms/index.py b/maro/rl/algorithms/index.py new file mode 100644 index 000000000..33ee92e4b --- /dev/null +++ b/maro/rl/algorithms/index.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac import ActorCritic, ActorCriticConfig, DiscreteACNet +from .ddpg import DDPG, ContinuousACNet, DDPGConfig +from .dqn import DQN, DiscreteQNet, DQNConfig +from .pg import DiscretePolicyNet, PolicyGradient, PolicyGradientConfig + +ALGORITHM_INDEX = { + "ac": ActorCritic, + "dqn": DQN, + "ddpg": DDPG, + "pg": PolicyGradient +} + +ALGORITHM_CONFIG_INDEX = { + "ac": ActorCriticConfig, + "dqn": DQNConfig, + "ddpg": DDPGConfig, + "pg": PolicyGradientConfig +} + +ALGORITHM_MODEL_INDEX = { + "ac": DiscreteACNet, + "dqn": DiscreteQNet, + "ddpg": ContinuousACNet, + "pg": DiscretePolicyNet +} + + +def get_algorithm_cls(algorithm_type): + if isinstance(algorithm_type, str): + if algorithm_type not in ALGORITHM_INDEX: + raise KeyError(f"A string algorithm_type must be one of {list(ALGORITHM_INDEX.keys())}.") + return ALGORITHM_INDEX[algorithm_type] + + return algorithm_type + + +def get_algorithm_config_cls(algorithm_config_type): + if isinstance(algorithm_config_type, str): + if algorithm_config_type not in ALGORITHM_CONFIG_INDEX: + raise KeyError(f"A string algorithm_config_type must be one of {list(ALGORITHM_CONFIG_INDEX.keys())}.") + return ALGORITHM_CONFIG_INDEX[algorithm_config_type] + + return algorithm_config_type + + +def get_algorithm_model_cls(algorithm_model_type): + if isinstance(algorithm_model_type, str): + if algorithm_model_type not in ALGORITHM_MODEL_INDEX: + raise KeyError(f"A string algorithm_model_type must be one of {list(ALGORITHM_MODEL_INDEX.keys())}.") + return ALGORITHM_MODEL_INDEX[algorithm_model_type] + + return algorithm_model_type diff --git a/maro/rl/policy/algorithms/pg.py b/maro/rl/algorithms/pg.py similarity index 75% rename from maro/rl/policy/algorithms/pg.py rename to maro/rl/algorithms/pg.py index 342d151dd..d08681bc9 100644 --- a/maro/rl/policy/algorithms/pg.py +++ b/maro/rl/algorithms/pg.py @@ -6,9 +6,9 @@ import numpy as np import torch -from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler +from maro.rl.algorithms import AbsAlgorithm +from maro.rl.experience import ExperienceSet from maro.rl.model import DiscretePolicyNet -from maro.rl.policy import AbsCorePolicy from maro.rl.utils import get_truncated_cumulative_reward @@ -27,7 +27,7 @@ def __init__(self, reward_discount: float, clear_experience_memory_every: int = self.clear_experience_memory_every = clear_experience_memory_every -class PolicyGradient(AbsCorePolicy): +class PolicyGradient(AbsAlgorithm): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. @@ -36,30 +36,13 @@ class PolicyGradient(AbsCorePolicy): policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. config (PolicyGradientConfig): Configuration for the PG algorithm. - experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences - generated by the policy. - experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to - ``UnifromSampler``. - experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ - def __init__( - self, - policy_net: DiscretePolicyNet, - config: PolicyGradientConfig, - experience_store: ExperienceStore, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs: dict = {}, - post_step: Callable = None - ): + def __init__(self, policy_net: DiscretePolicyNet, config: PolicyGradientConfig, post_step: Callable = None): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__( - experience_store, - experience_sampler_cls=experience_sampler_cls, - experience_sampler_kwargs=experience_sampler_kwargs - ) + super().__init__() self.policy_net = policy_net self.config = config self._post_step = post_step @@ -74,7 +57,13 @@ def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def get_loss(self, batch: ExperienceSet): + def get_update_info(self, experience_batch: ExperienceSet): + return self.policy_net.get_gradients(self._get_loss(experience_batch)) + + def apply(self, grad_dict: dict): + self.policy_net.apply(grad_dict) + + def _get_loss(self, batch: ExperienceSet): self.policy_net.train() log_p = torch.from_numpy(np.asarray([act[1] for act in batch.actions])).to(self.device) rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) @@ -82,7 +71,7 @@ def get_loss(self, batch: ExperienceSet): returns = torch.from_numpy(returns).to(self.device) return -(log_p * returns).mean() - def learn(self): + def learn(self, experience_batch: ExperienceSet): """ This should be called at the end of a simulation episode and the experiences obtained from the experience store's ``get`` method should be a sequential set, i.e., in the order in @@ -90,7 +79,7 @@ def learn(self): """ assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." self.policy_net.train() - loss = self.get_loss(self.sampler.get()) + loss = self._get_loss(experience_batch) self.policy_net.step(loss) if self._post_step: @@ -105,3 +94,6 @@ def set_state(self, policy_state): def get_state(self): return self.policy_net.state_dict() + + def post_step(self): + diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 82f1fde2d..c8984598b 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -91,9 +91,37 @@ def trainable(self) -> bool: def forward(self, *args, **kwargs): raise NotImplementedError - def step(self, loss): + def get_gradients(self, loss: torch.tensor): + """Compute gradients from a loss """ + if self.optimizer is None: + raise MissingOptimizer("No optimizer registered to the model") + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.zero_grad() + else: + self.optimizer.zero_grad() + + # Obtain gradients through back-propagation + loss.backward() + + return {name: param.grad for name, param in self.named_parameters()} + + def apply(self, grad_dict: dict): + for name, param in self.named_parameters(): + param.grad = grad_dict[name] + + # Apply gradients + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.step() + else: + self.optimizer.step() + + def step(self, loss: torch.tensor): """Use the loss to back-propagate gradients and apply them to the underlying parameters. + This is equivalent to a chained ``get_gradients`` and ``step``. + Args: loss: Result of a computation graph that involves the underlying parameters. """ diff --git a/maro/rl/policy/algorithms/rl_policy_index.py b/maro/rl/policy/algorithms/rl_policy_index.py deleted file mode 100644 index 45351beb6..000000000 --- a/maro/rl/policy/algorithms/rl_policy_index.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .ac import ActorCritic, ActorCriticConfig, DiscreteACNet -from .ddpg import DDPG, ContinuousACNet, DDPGConfig -from .dqn import DQN, DiscreteQNet, DQNConfig -from .pg import DiscretePolicyNet, PolicyGradient, PolicyGradientConfig - -RL_POLICY_INDEX = { - "ac": ActorCritic, - "dqn": DQN, - "ddpg": DDPG, - "pg": PolicyGradient -} - -RL_POLICY_CONFIG_INDEX = { - "ac": ActorCriticConfig, - "dqn": DQNConfig, - "ddpg": DDPGConfig, - "pg": PolicyGradientConfig -} - -RL_POLICY_MODEL_INDEX = { - "ac": DiscreteACNet, - "dqn": DiscreteQNet, - "ddpg": ContinuousACNet, - "pg": DiscretePolicyNet -} - - -def get_rl_policy_cls(policy_type): - if isinstance(policy_type, str): - if policy_type not in RL_POLICY_INDEX: - raise KeyError(f"A string policy_type must be one of {list(RL_POLICY_INDEX.keys())}.") - return RL_POLICY_INDEX[policy_type] - - return policy_type - - -def get_rl_policy_config_cls(policy_config_type): - if isinstance(policy_config_type, str): - if policy_config_type not in RL_POLICY_CONFIG_INDEX: - raise KeyError(f"A string policy_config_type must be one of {list(RL_POLICY_CONFIG_INDEX.keys())}.") - return RL_POLICY_CONFIG_INDEX[policy_config_type] - - return policy_config_type - - -def get_rl_policy_model_cls(policy_model_type): - if isinstance(policy_model_type, str): - if policy_model_type not in RL_POLICY_MODEL_INDEX: - raise KeyError(f"A string policy_model_type must be one of {list(RL_POLICY_MODEL_INDEX.keys())}.") - return RL_POLICY_MODEL_INDEX[policy_model_type] - - return policy_model_type diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 9fb22cea3..b8975bf80 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -3,8 +3,8 @@ from abc import ABC, abstractmethod +from maro.rl.algorithms import AbsAlgorithm from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler -from maro.rl.exploration import AbsExploration class AbsPolicy(ABC): @@ -32,45 +32,61 @@ class AbsCorePolicy(AbsPolicy): Reinforcement learning (RL) policies should inherit from this. Args: + algorithm (AbsAlgorithm): Algorithm instance. experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences generated by the policy. experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to ``UnifromSampler``. experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. - exploration (AbsExploration): Exploration strategy for generating exploratory actions. Defaults to None. + num_epochs (int): Number of times ``self.algorithm.learn()`` is called in each call to ``update``. Defaults + to 1. + update_trigger (int): The required number of new experiences to trigger a call to ``update``. Defaults to 1. + warmup (int): The minimum number of experiences in the experience memory required to trigger a call to ``update`` + Defaults to 1. + auto (bool): If true, the policy instance should be updated in the ``learn`` method. Otherwise, the method simply + returns the necessary update information. The latter is usually used in Defaults to True. """ def __init__( self, + algorithm: AbsAlgorithm, experience_store: ExperienceStore, experience_sampler_cls=UniformSampler, experience_sampler_kwargs: dict = {}, - exploration: AbsExploration = None + num_epochs: int = 1, + update_trigger: int = 1, + warmup: int = 1, + auto: bool = True ): super().__init__() + self.algorithm = algorithm self.experience_store = experience_store self.sampler = experience_sampler_cls(self.experience_store, **experience_sampler_kwargs) - self.exploration = exploration + self.num_epochs = num_epochs + self.update_trigger = update_trigger + self.warmup = warmup self.tracker = {} - self.exploring = True + self.auto = auto + + self._exp_cache = ExperienceSet() + self._new_exp_counter = 0 - @abstractmethod def choose_action(self, state): - raise NotImplementedError + return self.algorithm.choose_action(state) - @abstractmethod - def learn(self): + def update(self, experience_batch): """Policy update logic is implemented here. This usually includes retrieving experiences as training samples from the experience manager and updating the underlying models using these samples. """ - raise NotImplementedError - - def get_loss(self, batch: ExperienceSet): - """Compute the loss for a sample experience batch.""" - pass + if self.auto: + if self._new_exp_counter >= self.update_trigger and self.experience_store.size >= self.warmup: + experience_batch = self.sampler.get() + for _ in range(self.num_epochs): + self.algorithm.learn(experience_batch) + else: + return self.algorithm.get_update_info(experience_batch) - @abstractmethod def get_state(self): """Return the current state of the policy. @@ -78,18 +94,26 @@ def get_state(self): is contained in the policy, ``get_state`` may include a call to ``state_dict()`` on the model, while ``set_state`` should accordingly include ``load_state_dict()``. """ - pass + return self.algorithm.get_state() - @abstractmethod - def set_state(self, policy_state): + def set_state(self, state): """Set the policy state to ``policy_state``. The implementation must be in correspondence with that of ``get_state``. For example, if a torch model is contained in the policy, ``set_state`` may include a call to ``load_state_dict()`` on the model, while ``get_state`` should accordingly include ``state_dict()``. """ + self.algorithm.set_state(state) + + def get_update_info(self, experience_batch: ExperienceSet): pass + def apply(self, update_info): + if self.auto: + raise + + self.algorithm.apply(update_info) + def store(self, exp: ExperienceSet) -> bool: """ Store incoming experiences and update if necessary. @@ -99,20 +123,20 @@ def store(self, exp: ExperienceSet) -> bool: # f"exp mem size = {self.experience_store.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" # ) - def exploit(self): - self.exploring = False - def explore(self): + self.algorithm = False + + def exploit(self): self.exploring = True def exploration_step(self): - if self.exploration: - self.exploration.step() + if self.algorithm.exploration: + self.algorithm.exploration.step() def load(self, path: str): """Load the policy state from disk.""" - pass + self.algorithm.load(path) def save(self, path: str): """Save the policy state to disk.""" - pass + self.algorithm.save(path) diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 5ba440e38..7721f015e 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -24,6 +24,9 @@ class AbsPolicyManager(ABC): Args: policy_dict (Dict[str, AbsCorePolicy]): A list of policies managed by the manager. + num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of + times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the + number of learning epochs will be set to 1 for each policy. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, all triggers will be set to 1. @@ -35,28 +38,13 @@ class AbsPolicyManager(ABC): ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. """ - def __init__( - self, - policy_dict: Dict[str, AbsCorePolicy], - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - post_update: Callable = None - ): + def __init__(self, policy_dict: Dict[str, AbsCorePolicy], post_update: Callable = None): for policy in policy_dict.values(): if not isinstance(policy, AbsCorePolicy): raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") super().__init__() self.policy_dict = policy_dict - if not update_trigger: - self.update_trigger = {name: 1 for name in self.policy_dict} - else: - self.update_trigger = update_trigger - if not warmup: - self.warmup = {name: 1 for name in self.policy_dict} - else: - self.warmup = warmup - self._post_update = post_update self._update_history = [set(policy_dict.keys())] @@ -85,6 +73,9 @@ class LocalPolicyManager(AbsPolicyManager): Args: policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. + num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of + times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the + number of learning epochs will be set to 1 for each policy. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, all triggers will be set to 1. @@ -102,12 +93,19 @@ class LocalPolicyManager(AbsPolicyManager): def __init__( self, policy_dict: Dict[str, AbsCorePolicy], + num_epochs: Dict[str, int] = None, update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, post_update: Callable = None, log_dir: str = getcwd() ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) + super().__init__( + policy_dict, + num_epochs=num_epochs, + update_trigger=update_trigger, + warmup=warmup, + post_update=post_update + ) self._new_exp_counter = defaultdict(int) self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) @@ -127,7 +125,8 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._new_exp_counter[policy_name] >= self.update_trigger[policy_name] and policy.experience_store.size >= self.warmup[policy_name] ): - policy.learn() + for _ in range(self.num_epochs[policy_name]): + policy.learn() updated.add(policy_name) self._new_exp_counter[policy_name] = 0 @@ -150,6 +149,9 @@ class MultiProcessPolicyManager(AbsPolicyManager): create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. + num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of + times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the + number of learning epochs will be set to 1 for each policy. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, all triggers will be set to 1. @@ -168,12 +170,19 @@ def __init__( policy_dict: Dict[str, AbsCorePolicy], num_trainers: int, create_policy_func_dict: Dict[str, Callable], + num_epochs: Dict[str, int] = None, update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, post_update: Callable = None, log_dir: str = getcwd(), ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) + super().__init__( + policy_dict, + num_epochs=num_epochs, + update_trigger=update_trigger, + warmup=warmup, + post_update=post_update + ) self._policy2trainer = {} self._trainer2policies = defaultdict(list) self._exp_cache = defaultdict(ExperienceSet) @@ -197,7 +206,8 @@ def __init__( trainer_id, trainer_end, {name: create_policy_func_dict[name] for name in policy_names}, - {name: self.policy_dict[name].get_state() for name in self._trainer2policies[trainer_id]} + {name: self.policy_dict[name].get_state() for name in policy_names}, + {name: self.num_epochs[name] for name in policy_names} ), kwargs={"log_dir": log_dir} ) @@ -251,6 +261,9 @@ class MultiNodePolicyManager(AbsPolicyManager): manages them. num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where 0 <= i < num_trainers. + num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of + times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the + number of learning epochs will be set to 1 for each policy. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, all triggers will be set to 1. @@ -271,13 +284,20 @@ def __init__( policy_dict: Dict[str, AbsCorePolicy], group: str, num_trainers: int, + num_epochs: Dict[str, int] = None, update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, post_update: Callable = None, log_dir: str = getcwd(), proxy_kwargs: dict = {} ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) + super().__init__( + policy_dict, + num_epochs=num_epochs, + update_trigger=update_trigger, + warmup=warmup, + post_update=post_update + ) peers = {"trainer": num_trainers} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) @@ -298,7 +318,10 @@ def __init__( self._proxy.send( SessionMessage( MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, - body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} + body={ + MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}, + MsgKey.NUM_EPOCHS: {name: self.num_epochs[name] for name in policy_names} + } ) ) diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index 218c69d9a..c002356d4 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -16,6 +16,7 @@ def trainer_process( conn: Connection, create_policy_func_dict: Dict[str, Callable], initial_policy_states: dict, + num_epochs: Dict[str, int], log_dir: str = getcwd() ): """Policy trainer process which can be spawned by a ``MultiProcessPolicyManager``. @@ -26,6 +27,10 @@ def trainer_process( create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. + initial_policy_states (dict): States with which to initialize the policies. + num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of + times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the + number of learning epochs will be set to 1 for each policy. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} @@ -40,7 +45,8 @@ def trainer_process( t0 = time.time() for name, exp in msg["experiences"].items(): policy_dict[name].store(exp) - policy_dict[name].learn() + for _ in range(num_epochs[name]): + policy_dict[name].learn() logger.debug(f"total policy update time: {time.time() - t0}") conn.send({ "policy": {name: policy_dict[name].get_state() for name in msg["experiences"]}, @@ -54,6 +60,7 @@ def trainer_node( group: str, trainer_idx: int, create_policy_func_dict: Dict[str, Callable], + num_epochs: Dict[str, int], proxy_kwargs: dict = {}, log_dir: str = getcwd() ): @@ -66,6 +73,9 @@ def trainer_node( create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. + num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of + times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the + number of learning epochs will be set to 1 for each policy. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. @@ -90,7 +100,8 @@ def trainer_node( t0 = time.time() for name, exp in msg.body[MsgKey.EXPERIENCES].items(): policy_dict[name].store(exp) - policy_dict[name].learn() + for _ in range(num_epochs[name]): + policy_dict[name].learn() msg_body = { MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.EXPERIENCES]}, @@ -98,3 +109,18 @@ def trainer_node( } logger.info(f"total policy update time: {time.time() - t0}") proxy.reply(msg, tag=MsgTag.TRAIN_DONE, body=msg_body) + elif msg.tag == MsgTag.GET_UPDATE_INFO: + t0 = time.time() + msg_body = { + MsgKey.UPDATE_INFO: + {policy_dict[name].get_update_info(exp) for name, exp in msg.body[MsgKey.EXPERIENCES].items()}, + MsgKey.TRACKER: + {name: policy_dict[name].tracker for name in msg.body[MsgKey.EXPERIENCES]} + } + logger.info(f"total time to get update info: {time.time() - t0}") + proxy.reply(msg, tag=MsgTag.UPDATE_INFO, body=msg_body) + elif msg.tag == MsgTag.UPDATE_POLICY_STATE: + for name, state in msg.body[MsgKey.POLICY_STATE].items(): + policy_dict[name] = create_policy_func_dict[name]() + policy_dict[name].set_state(state) + logger.info(f"{proxy.name} updated policy {name}") diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index df6813bc8..2fdfff1d3 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -14,6 +14,9 @@ class MsgTag(Enum): CHOOSE_ACTION = "choose_action" ACTION = "action" LEARN = "LEARN" + GET_UPDATE_INFO = "get_update_info" + UPDATE_INFO = "update_info" + UPDATE_POLICY_STATE = "update_policy_state" ABORT_ROLLOUT = "abort_rollout" EVAL_DONE = "eval_done" COLLECT_DONE = "collect_done" @@ -32,6 +35,8 @@ class MsgKey(Enum): TRACKER = "tracker" STATE = "state" POLICY_STATE = "policy_state" + NUM_EPOCHS = "num_epochs" + UPDATE_INFO = "update_info" EXPLORATION_STEP = "exploration_step" VERSION = "version" NUM_STEPS = "num_steps" From 7702eba848ccb29b423d50b87e97103441bedfc1 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 5 Aug 2021 15:20:25 +0800 Subject: [PATCH 400/482] policy interface redesigned --- docs/source/apidoc/maro.rl.rst | 4 +- docs/source/key_components/rl_toolkit.rst | 2 +- examples/rl/cim/ac.py | 123 +++++++++++----------- examples/rl/cim/dqn.py | 107 ++++++++++--------- examples/rl/cim/policy_index.py | 9 +- maro/rl/algorithms/__init__.py | 17 ++- maro/rl/algorithms/abs_algorithm.py | 38 +++---- maro/rl/algorithms/ac.py | 92 ++++++---------- maro/rl/algorithms/ddpg.py | 86 ++++++--------- maro/rl/algorithms/dqn.py | 101 ++++++------------ maro/rl/algorithms/index.py | 23 +--- maro/rl/algorithms/pg.py | 56 +++------- maro/rl/experience/sampler.py | 26 ++--- maro/rl/policy/__init__.py | 4 +- maro/rl/policy/policy.py | 103 +++++------------- maro/rl/policy/policy_manager.py | 116 ++++++++++---------- maro/rl/policy/trainer.py | 32 ++++-- 17 files changed, 388 insertions(+), 551 deletions(-) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index 6aa78ce1f..b0687f81b 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -37,10 +37,10 @@ maro.rl.algorithms.pg Experience ================================================================================ -maro.rl.experience.experience_store +maro.rl.experience.experience_memory -------------------------------------------------------------------------------- -.. automodule:: maro.rl.experience.experience_store +.. automodule:: maro.rl.experience.experience_memory :members: :undoc-members: :show-inheritance: diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index abe67d186..f1ded31f2 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -97,7 +97,7 @@ based on which updates can be made. class AbsCorePolicy(AbsPolicy): def __init__(self, experience_store: ExperienceStore): super().__init__() - self.experience_store = experience_store + self.experience_memory = experience_store @abstractmethod def update(self): diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 0b306ae1a..128c2ec40 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -7,68 +7,77 @@ import numpy as np import torch -from maro.rl.experience import ExperienceStore, UniformSampler +from maro.rl.algorithms import ActorCritic +from maro.rl.experience import ExperienceStore from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption -from maro.rl.policy.algorithms import ActorCritic, ActorCriticConfig +from maro.rl.policy import CorePolicy + cim_path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, cim_path) from env_wrapper import STATE_DIM, env_config -config = { - "model": { - "network": { - "actor": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64], - "output_dim": env_config["wrapper"]["num_actions"], - "activation": "tanh", - "softmax": True, - "batch_norm": False, - "head": True - }, - "critic": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64], - "output_dim": 1, - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "head": True - } +ac_net_config = { + "network": { + "actor": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64], + "output_dim": env_config["wrapper"]["num_actions"], + "activation": "tanh", + "softmax": True, + "batch_norm": False, + "head": True }, - "optimization": { - "actor": { - "optim_cls": "adam", - "optim_params": {"lr": 0.001} - }, - "critic": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.001} - } + "critic": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64], + "output_dim": 1, + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "head": True } }, - "algorithm": { - "reward_discount": .0, - "critic_loss_cls": "smooth_l1", - "train_epochs": 10, - "critic_loss_coeff": 0.1, - "entropy_coeff": 0.01, - # "clip_ratio": 0.8 # for PPO - }, - "experience_store": { + "optimization": { + "actor": { + "optim_cls": "adam", + "optim_params": {"lr": 0.001} + }, + "critic": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.001} + } + } +} + +ac_config = { + "reward_discount": .0, + "critic_loss_cls": "smooth_l1", + "critic_loss_coeff": 0.1, + "entropy_coeff": 0.01, + # "clip_ratio": 0.8 # for PPO +} + + +experience_config = { + "memory": { "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, "update": {"capacity": 100000, "overwrite_type": "rolling"} }, "sampler": { - "rollout": {"batch_size": -1, "replace": False}, - "update": {"batch_size": 128, "replace": True} + "rollout": { + "batch_size": -1, + "replace": False + }, + "update": { + "batch_size": 128, + "replace": True + } } } -def get_ac_policy(mode="update"): - assert mode in {"inference", "update", "inference-update"} +def get_ac_policy(learning: bool = True): class MyACNET(DiscreteACNet): def forward(self, states, actor: bool = True, critic: bool = True): states = torch.from_numpy(np.asarray(states)) @@ -83,24 +92,16 @@ def forward(self, states, actor: bool = True, critic: bool = True): ac_net = MyACNET( component={ - "actor": FullyConnectedBlock(**config["model"]["network"]["actor"]), - "critic": FullyConnectedBlock(**config["model"]["network"]["critic"]) + "actor": FullyConnectedBlock(**ac_net_config["network"]["actor"]), + "critic": FullyConnectedBlock(**ac_net_config["network"]["critic"]) }, optim_option={ - "actor": OptimOption(**config["model"]["optimization"]["actor"]), - "critic": OptimOption(**config["model"]["optimization"]["critic"]) - } if mode != "inference" else None + "actor": OptimOption(**ac_net_config["optimization"]["actor"]), + "critic": OptimOption(**ac_net_config["model"]["optimization"]["critic"]) + } if learning else None ) - if mode == "update": - exp_store = ExperienceStore(**config["experience_store"]["update"]) - experience_sampler_kwargs = config["sampler"]["update"] - else: - exp_store = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) - experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - - return ActorCritic( - ac_net, ActorCriticConfig(**config["algorithm"]), exp_store, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs=experience_sampler_kwargs + return CorePolicy( + ActorCritic(ac_net, ac_config["algorithm"]), + ExperienceStore(**experience_config["memory"]) ) diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index 3bfe3ad96..9a00a673f 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -8,42 +8,55 @@ import torch import torch.nn as nn +from maro.rl.algorithms import DQN from maro.rl.experience import ExperienceStore, UniformSampler from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption -from maro.rl.policy.algorithms import DQN, DQNConfig +from maro.rl.policy import CorePolicy + cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) from env_wrapper import STATE_DIM, env_config -config = { - "model": { - "network": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64, 32], - "output_dim": env_config["wrapper"]["num_actions"], - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.05} - } +q_net_config = { + "network": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64, 32], + "output_dim": env_config["wrapper"]["num_actions"], + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 }, - "algorithm": { - "reward_discount": .0, - "update_target_every": 5, - "train_epochs": 10, - "soft_update_coeff": 0.1, - "double": False - }, - "experience_store": { + "optimization": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.05} + } +} + +dqn_config = { + "reward_discount": .0, + "update_target_every": 5, + "train_epochs": 10, + "soft_update_coeff": 0.1, + "double": False +} + + +exploration_config = { + "last_ep": 10, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(5, 0.32)] +} + + +experience_config = { + "memory": { "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, "update": {"capacity": 100000, "overwrite_type": "rolling"} }, @@ -56,12 +69,6 @@ "batch_size": 128, "replace": True } - }, - "exploration": { - "last_ep": 10, - "initial_value": 0.4, - "final_value": 0.0, - "splits": [(5, 0.32)] } } @@ -77,29 +84,27 @@ def forward(self, states): return self.component(states) -def get_dqn_policy(mode="update"): - assert mode in {"inference", "update", "inference-update"} +def get_dqn_policy(learning: bool = True): qnet = QNet( - FullyConnectedBlock(**config["model"]["network"]), - optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None + FullyConnectedBlock(**q_net_config["network"]), + optim_option=OptimOption(**q_net_config["optimization"]) if learning else None + ) + exploration = EpsilonGreedyExploration() + exploration.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **exploration_config + ) + return CorePolicy( + DQN(qnet, **dqn_config, exploration=exploration), + ExperienceStore(**experience_config) ) if mode == "update": - exp_store = ExperienceStore(**config["experience_store"]["update"]) - exploration = None + exp_memory = ExperienceStore(**config["experience_store"]["update"]) exp_sampler_kwargs = config["sampler"]["update"] else: - exploration = EpsilonGreedyExploration() - exploration.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - **config["exploration"] - ) - exp_store = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) + exp_memory = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - return DQN( - qnet, DQNConfig(**config["algorithm"]), exp_store, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs=exp_sampler_kwargs, - exploration=exploration - ) + algorithm = + return CorePolicy(algorithm, exp_memory, exp_sampler_cls=UniformSampler, exp_sampler_kwargs=exp_sampler_kwargs) diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index 3eb24c645..af237df5e 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -8,13 +8,16 @@ cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) -from ac import get_ac_policy -from dqn import get_dqn_policy +from algorithms.ac import get_ac +from algorithms.dqn import get_dqn from env_wrapper import AGENT_IDS update_trigger = {name: 128 for name in AGENT_IDS} warmup = {name: 1 for name in AGENT_IDS} + + + # use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} +rl_policy_func_index = {name: get_policy() for name in AGENT_IDS} agent2policy = {name: name for name in AGENT_IDS} diff --git a/maro/rl/algorithms/__init__.py b/maro/rl/algorithms/__init__.py index 1b4dfce5f..d37dcc0e3 100644 --- a/maro/rl/algorithms/__init__.py +++ b/maro/rl/algorithms/__init__.py @@ -2,17 +2,12 @@ # Licensed under the MIT license. from .abs_algorithm import AbsAlgorithm -from .ac import ActorCritic, ActorCriticConfig -from .ddpg import DDPG, DDPGConfig -from .dqn import DQN, DQNConfig -from .pg import PolicyGradient, PolicyGradientConfig -from .index import get_algorithm_cls, get_algorithm_config_cls, get_algorithm_model_cls +from .ac import ActorCritic +from .ddpg import DDPG +from .dqn import DQN +from .pg import PolicyGradient +from .index import get_algorithm_cls, get_algorithm_model_cls __all__ = [ - "AbsAlgorithm", - "ActorCritic", "ActorCriticConfig", - "DDPG", "DDPGConfig", - "DQN", "DQNConfig", - "PolicyGradient", "PolicyGradientConfig", - "get_algorithm_cls", "get_algorithm_config_cls", "get_algorithm_model_cls" + "AbsAlgorithm", "ActorCritic", "DDPG", "DQN", "PolicyGradient", "get_algorithm_cls", "get_algorithm_model_cls" ] diff --git a/maro/rl/algorithms/abs_algorithm.py b/maro/rl/algorithms/abs_algorithm.py index 8ca363670..5001a53ab 100644 --- a/maro/rl/algorithms/abs_algorithm.py +++ b/maro/rl/algorithms/abs_algorithm.py @@ -2,35 +2,34 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from typing import Callable +from typing import Union from maro.rl.experience import ExperienceSet from maro.rl.exploration import AbsExploration class AbsAlgorithm(ABC): - """Policy that can update itself using simulation experiences. + """Abstract algorithm. - Reinforcement learning (RL) policies should inherit from this. + Reinforcement learning (RL) algorithms should inherit from this. Args: exploration (AbsExploration): Exploration strategy for generating exploratory actions. Defaults to None. - post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking - the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ - def __init__(self, exploration: AbsExploration = None, post_learn: Callable = None): + def __init__(self, exploration: AbsExploration = None): super().__init__() self.exploration = exploration - self.exploring = True @abstractmethod def choose_action(self, state, explore: bool = False): raise NotImplementedError - def get_update_info(self, experience_batch: ExperienceSet): + @abstractmethod + def get_gradients(self, experience_batch: ExperienceSet): pass - def apply(self): + @abstractmethod + def apply(self, grad_dict: dict): pass @abstractmethod @@ -38,13 +37,20 @@ def learn(self, experience_batch: ExperienceSet): """Update logic is implemented here.""" raise NotImplementedError + def post_update(self, update_index: int): + pass + @abstractmethod - def get_state(self): + def get_state(self, inference: bool = True): """Return the current state of the policy. - The implementation must be in correspondence with that of ``set_state``. For example, if a torch model + The implementation must go hand in hand with that of ``set_state``. For example, if a torch model is contained in the policy, ``get_state`` may include a call to ``state_dict()`` on the model, while ``set_state`` should accordingly include ``load_state_dict()``. + + Args: + learning (bool): If True, the returned state is for inference purpose only. This parameter + may be ignored for some algorithms. """ pass @@ -58,16 +64,6 @@ def set_state(self, policy_state): """ pass - def explore(self): - self.exploring = False - - def exploit(self): - self.exploring = True - - def exploration_step(self): - if self.exploration: - self.exploration.step() - def load(self, path: str): """Load the policy state from disk.""" pass diff --git a/maro/rl/algorithms/ac.py b/maro/rl/algorithms/ac.py index 6ab6d2929..fc3cd71d5 100644 --- a/maro/rl/algorithms/ac.py +++ b/maro/rl/algorithms/ac.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, Tuple +from typing import Tuple, Union import numpy as np import torch @@ -13,10 +13,15 @@ from maro.rl.utils import get_torch_loss_cls -class ActorCriticConfig: - """Configuration for the Actor-Critic algorithm. +class ActorCritic(AbsAlgorithm): + """Actor Critic algorithm with separate policy and value models. + + References: + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f Args: + ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. reward_discount (float): Reward decay as defined in standard RL terminology. critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". @@ -27,56 +32,30 @@ class ActorCriticConfig: total loss will not include an entropy term. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. - clear_experience_memory_every (int): Number of ``ActorCritic.learn`` calls between experience memory clearances. - Defaults to 1. """ - __slots__ = [ - "reward_discount", "critic_loss_func", "min_logp", "critic_loss_coeff", "entropy_coeff", "clip_ratio", - "clear_experience_memory_every" - ] def __init__( self, + ac_net: DiscreteACNet, reward_discount: float, critic_loss_cls="mse", min_logp: float = None, critic_loss_coeff: float = 1.0, entropy_coeff: float = None, - clip_ratio: float = None, - clear_experience_memory_every: int = 1 + clip_ratio: float = None ): + if not isinstance(ac_net, DiscreteACNet): + raise TypeError("model must be an instance of 'DiscreteACNet'") + + super().__init__() + self.ac_net = ac_net self.reward_discount = reward_discount self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() self.min_logp = min_logp self.critic_loss_coeff = critic_loss_coeff self.entropy_coeff = entropy_coeff self.clip_ratio = clip_ratio - self.clear_experience_memory_every = clear_experience_memory_every - - -class ActorCritic(AbsAlgorithm): - """Actor Critic algorithm with separate policy and value models. - References: - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f - - Args: - ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. - config: Configuration for the AC algorithm. - post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking - the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. - """ - - def __init__(self, ac_net: DiscreteACNet, config: ActorCriticConfig, post_step: Callable = None): - if not isinstance(ac_net, DiscreteACNet): - raise TypeError("model must be an instance of 'DiscreteACNet'") - - super().__init__() - self.ac_net = ac_net - self.config = config - self._post_step = post_step - self._num_learn_calls = 0 self.device = self.ac_net.device def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: @@ -87,12 +66,9 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else (actions, log_p) - def get_update_info(self, experience_batch: ExperienceSet): + def get_update_info(self, experience_batch: ExperienceSet) -> dict: return self.ac_net.get_gradients(self._get_loss(experience_batch)) - def apply(self, grad_dict: dict): - self.ac_net.apply(grad_dict) - def _get_loss(self, batch: ExperienceSet): self.ac_net.train() states, next_states = batch.states, batch.next_states @@ -104,41 +80,39 @@ def _get_loss(self, batch: ExperienceSet): state_values = state_values.squeeze() with torch.no_grad(): next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() - return_est = rewards + self.config.reward_discount * next_state_values + return_est = rewards + self.reward_discount * next_state_values advantages = return_est - state_values # actor loss log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - log_p_new = torch.clamp(log_p_new, min=self.config.min_logp, max=.0) - if self.config.clip_ratio is not None: + log_p_new = torch.clamp(log_p_new, min=self.min_logp, max=.0) + if self.clip_ratio is not None: ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.config.clip_ratio, 1 + self.config.clip_ratio) + clip_ratio = torch.clamp(ratio, 1 - self.clip_ratio, 1 + self.clip_ratio) actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() else: actor_loss = -(log_p_new * advantages).mean() # critic_loss - critic_loss = self.config.critic_loss_func(state_values, return_est) + critic_loss = self.critic_loss_func(state_values, return_est) # total loss - loss = actor_loss + self.config.critic_loss_coeff * critic_loss - if self.config.entropy_coeff is not None: - loss -= self.config.entropy_coeff * Categorical(action_probs).entropy().mean() + loss = actor_loss + self.critic_loss_coeff * critic_loss + if self.entropy_coeff is not None: + loss -= self.entropy_coeff * Categorical(action_probs).entropy().mean() return loss - def learn(self, experience_batch: ExperienceSet): + def learn(self, data: Union[ExperienceSet, dict]): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." - self.ac_net.train() - loss = self._get_loss(experience_batch) - self.ac_net.step(loss) - if self._post_step: - self._post_step(loss.detach().cpu().numpy(), self.tracker) - - # Empty the experience store due to the on-policy nature of the algorithm. - self._num_learn_calls += 1 - if self._num_learn_calls % self.config.clear_experience_memory_every == 0: - self.experience_store.clear() + # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. + if isinstance(data, ExperienceSet): + self.ac_net.train() + loss = self._get_loss(data) + self.ac_net.step(loss) + # Otherwise treat the data as a dict of gradients that can be applied directly to the network. + else: + self.ac_net.apply(data) def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/algorithms/ddpg.py b/maro/rl/algorithms/ddpg.py index 6faabe78e..04b4c460a 100644 --- a/maro/rl/algorithms/ddpg.py +++ b/maro/rl/algorithms/ddpg.py @@ -13,13 +13,17 @@ from maro.rl.utils import get_torch_loss_cls -class DDPGConfig: - """Configuration for the DDPG algorithm. +class DDPG(AbsAlgorithm): + """The Deep Deterministic Policy Gradient (DDPG) algorithm. + + References: + https://arxiv.org/pdf/1509.02971.pdf + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg Args: + ac_net (ContinuousACNet): DDPG policy and q-value models. reward_discount (float): Reward decay as defined in standard RL terminology. - target_update_freq (int): Number of training rounds between policy target model updates. - train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. + update_target_every (int): Number of training rounds between policy target model updates. q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". q_value_loss_coeff (float): Coefficient for policy loss in the total loss function, e.g., @@ -27,47 +31,15 @@ class DDPGConfig: soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. Defaults to 1.0. """ - __slots__ = [ - "reward_discount", "target_update_freq", "train_epochs", "q_value_loss_func", "q_value_loss_coeff", - "soft_update_coeff" - ] - def __init__( self, + ac_net: ContinuousACNet, reward_discount: float, - target_update_freq: int, - train_epochs: int = 1, + update_target_every: int, q_value_loss_cls="mse", - policy_loss_coeff: float = 1.0, + q_value_loss_coeff: float = 1.0, soft_update_coeff: float = 1.0, - ): - self.reward_discount = reward_discount - self.target_update_freq = target_update_freq - self.train_epochs = train_epochs - self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() - self.policy_loss_coeff = policy_loss_coeff - self.soft_update_coeff = soft_update_coeff - - -class DDPG(AbsAlgorithm): - """The Deep Deterministic Policy Gradient (DDPG) algorithm. - - References: - https://arxiv.org/pdf/1509.02971.pdf - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg - - Args: - ac_net (ContinuousACNet): DDPG policy and q-value models. - config (DDPGConfig): Configuration for DDPG algorithm. - post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking - the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. - """ - def __init__( - self, - ac_net: ContinuousACNet, - config: DDPGConfig, exploration=GaussianNoiseExploration(), - post_step=None ): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") @@ -79,8 +51,11 @@ def __init__( self.target_ac_net.eval() else: self.target_ac_net = None - self.config = config - self._post_step = post_step + self.reward_discount = reward_discount + self.update_target_every = update_target_every + self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() + self.q_value_loss_coeff = q_value_loss_coeff + self.soft_update_coeff = soft_update_coeff self.device = self.ac_net.device self._num_steps = 0 @@ -93,7 +68,7 @@ def choose_action(self, states, explore: bool = False) -> Union[float, np.ndarra actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def get_update_info(self, experience_batch: ExperienceSet): + def get_update_info(self, experience_batch: ExperienceSet) -> dict: return self.ac_net.get_gradients(self._get_loss(experience_batch)) def apply(self, grad_dict: dict): @@ -109,25 +84,28 @@ def _get_loss(self, batch: ExperienceSet): with torch.no_grad(): next_q_values = self.target_ac_net.value(next_states) - target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) - q_value_loss = self.config.q_value_loss_func(q_values, target_q_values) + q_value_loss = self.q_value_loss_func(q_values, target_q_values) policy_loss = -self.ac_net.value(states).mean() - return policy_loss + self.config.q_value_loss_coeff * q_value_loss + return policy_loss + self.q_value_loss_coeff * q_value_loss - def learn(self, experience_batch: ExperienceSet): + def learn(self, data: Union[ExperienceSet, dict]): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." - self.ac_net.train() - for _ in range(self.config.train_epochs): - loss = self._get_loss(experience_batch) + # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. + if isinstance(data, ExperienceSet): + self.ac_net.train() + loss = self._get_loss(data) self.ac_net.step(loss) - if self._post_step: - self._post_step(loss.detach().cpu().numpy(), self.tracker) + # Otherwise treat the data as a dict of gradients that can be applied directly to the network. + else: + self.ac_net.apply(data) - self._num_steps += 1 - if self._num_steps % self.config.target_update_freq == 0: - self.target_ac_net.soft_update(self.ac_net, self.config.soft_update_coeff) + def post_update(self, update_index: int): + # soft-update target network + if update_index % self.update_target_every == 0: + self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index cada6efba..1af6846e0 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -1,21 +1,24 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, Union +from typing import Union import numpy as np import torch from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceSet, PrioritizedSampler +from maro.rl.experience import ExperienceSet from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.model import DiscreteQNet -class DQNConfig: - """Configuration for the DQN algorithm. +class DQN(AbsAlgorithm): + """The Deep-Q-Networks algorithm. + + See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: + q_net (DiscreteQNet): Q-value model. reward_discount (float): Reward decay as defined in standard RL terminology. update_target_every (int): Number of gradient steps between target model updates. soft_update_coeff (float): Soft update coefficient, e.g., @@ -24,41 +27,17 @@ class DQNConfig: double (bool): If True, the next Q values will be computed according to the double DQN algorithm, i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. - """ - __slots__ = ["reward_discount", "update_target_every", "soft_update_coeff", "double"] - - def __init__( - self, - reward_discount: float, - update_target_every: int, - soft_update_coeff: float = 0.1, - double: bool = True - ): - self.reward_discount = reward_discount - self.update_target_every = update_target_every - self.soft_update_coeff = soft_update_coeff - self.double = double - - -class DQN(AbsAlgorithm): - """The Deep-Q-Networks algorithm. - - See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. - - Args: - q_net (DiscreteQNet): Q-value model. - config (DQNConfig): Configuration for DQN algorithm. exploration (DiscreteSpaceExploration): Exploration strategy for generating exploratory actions. Defaults to an ``EpsilonGreedyExploration`` instance. - post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking - the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. """ def __init__( self, q_net: DiscreteQNet, - config: DQNConfig, - exploration: DiscreteSpaceExploration = EpsilonGreedyExploration(), - post_step: Callable = None + reward_discount: float, + update_target_every: int, + soft_update_coeff: float = 0.1, + double: bool = True, + exploration: DiscreteSpaceExploration = EpsilonGreedyExploration() ): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") @@ -70,13 +49,12 @@ def __init__( self.target_q_net.eval() else: self.target_q_net = None - self.config = config - self._post_step = post_step + self.reward_discount = reward_discount + self.update_target_every = update_target_every + self.soft_update_coeff = soft_update_coeff + self.double = double self.device = self.q_net.device - - self.prioritized_experience_replay = isinstance(self.sampler, PrioritizedSampler) - if not self.prioritized_experience_replay: - self._loss_func = torch.nn.MSELoss() + self._loss_func = torch.nn.MSELoss() def choose_action(self, states, explore: bool = True) -> Union[int, np.ndarray]: self.q_net.eval() @@ -91,60 +69,51 @@ def choose_action(self, states, explore: bool = True) -> Union[int, np.ndarray]: actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def get_update_info(self, experience_batch: ExperienceSet): + def get_update_info(self, experience_batch: ExperienceSet) -> dict: return self.q_net.get_gradients(self._get_loss(experience_batch)) - def apply(self, grad_dict: dict): - self.q_net.apply(grad_dict) - def _get_loss(self, experience_batch: ExperienceSet): states, next_states = experience_batch.states, experience_batch.next_states actions = torch.from_numpy(np.asarray(experience_batch.actions)).to(self.device) rewards = torch.from_numpy(np.asarray(experience_batch.rewards)).to(self.device) - if self.prioritized_experience_replay: - indexes = [info["index"] for info in experience_batch.info] - is_weights = torch.tensor([info["is_weight"] for info in experience_batch.info]).to(self.device) # get target Q values with torch.no_grad(): - if self.config.double: + if self.double: actions_by_eval_q_net = self.q_net.get_action(next_states)[0] next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) else: next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) - target_q_values = (rewards + self.config.reward_discount * next_q_values).detach() # (N,) + target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) # gradient step q_values = self.q_net.q_values(states, actions) - if self.prioritized_experience_replay: - td_errors = target_q_values - q_values - self.sampler.update(indexes, td_errors.detach().cpu().numpy()) - return (td_errors * is_weights).mean() - else: - return self._loss_func(q_values, target_q_values) + return self._loss_func(q_values, target_q_values) - def learn(self, experience_batch: ExperienceSet): + def learn(self, data: Union[ExperienceSet, dict]): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." - self.q_net.train() - - loss = self._get_loss(experience_batch) - self.q_net.step(loss) - if self._post_step: - self._post_step(loss.detach().cpu().numpy(), self.tracker) + # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. + if isinstance(data, ExperienceSet): + self.q_net.train() + loss = self._get_loss(data) + self.q_net.step(loss) + # Otherwise treat the data as a dict of gradients that can be applied directly to the network. + else: + self.q_net.apply(data) - def update_target(self, num_steps: int): + def post_update(self, update_index: int): # soft-update target network - if num_steps % self.config.update_target_every == 0: - self.target_q_net.soft_update(self.q_net, self.config.soft_update_coeff) + if update_index % self.update_target_every == 0: + self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) def set_state(self, policy_state): self.q_net.load_state_dict(policy_state["eval"]) if "target" in policy_state: self.target_q_net.load_state_dict(policy_state["target"]) - def get_state(self): + def get_state(self, inference: bool = True): policy_state = {"eval": self.q_net.state_dict()} - if self.target_q_net: + if not inference and self.target_q_net: policy_state["target"] = self.target_q_net.state_dict() return policy_state diff --git a/maro/rl/algorithms/index.py b/maro/rl/algorithms/index.py index 33ee92e4b..6ef27e68d 100644 --- a/maro/rl/algorithms/index.py +++ b/maro/rl/algorithms/index.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .ac import ActorCritic, ActorCriticConfig, DiscreteACNet -from .ddpg import DDPG, ContinuousACNet, DDPGConfig -from .dqn import DQN, DiscreteQNet, DQNConfig -from .pg import DiscretePolicyNet, PolicyGradient, PolicyGradientConfig +from .ac import ActorCritic, DiscreteACNet +from .ddpg import DDPG, ContinuousACNet +from .dqn import DQN, DiscreteQNet +from .pg import DiscretePolicyNet, PolicyGradient ALGORITHM_INDEX = { "ac": ActorCritic, @@ -13,12 +13,6 @@ "pg": PolicyGradient } -ALGORITHM_CONFIG_INDEX = { - "ac": ActorCriticConfig, - "dqn": DQNConfig, - "ddpg": DDPGConfig, - "pg": PolicyGradientConfig -} ALGORITHM_MODEL_INDEX = { "ac": DiscreteACNet, @@ -37,15 +31,6 @@ def get_algorithm_cls(algorithm_type): return algorithm_type -def get_algorithm_config_cls(algorithm_config_type): - if isinstance(algorithm_config_type, str): - if algorithm_config_type not in ALGORITHM_CONFIG_INDEX: - raise KeyError(f"A string algorithm_config_type must be one of {list(ALGORITHM_CONFIG_INDEX.keys())}.") - return ALGORITHM_CONFIG_INDEX[algorithm_config_type] - - return algorithm_config_type - - def get_algorithm_model_cls(algorithm_model_type): if isinstance(algorithm_model_type, str): if algorithm_model_type not in ALGORITHM_MODEL_INDEX: diff --git a/maro/rl/algorithms/pg.py b/maro/rl/algorithms/pg.py index d08681bc9..9b9be2dd5 100644 --- a/maro/rl/algorithms/pg.py +++ b/maro/rl/algorithms/pg.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, Tuple +from typing import Tuple, Union import numpy as np import torch @@ -12,21 +12,6 @@ from maro.rl.utils import get_truncated_cumulative_reward -class PolicyGradientConfig: - """Configuration for the Policy Gradient algorithm. - - Args: - reward_discount (float): Reward decay as defined in standard RL terminology. - clear_experience_memory_every (int): Number of ``ActorCritic.learn`` calls between experience memory clearances. - Defaults to 1. - """ - __slots__ = ["reward_discount", "clear_experience_memory_every"] - - def __init__(self, reward_discount: float, clear_experience_memory_every: int = 1): - self.reward_discount = reward_discount - self.clear_experience_memory_every = clear_experience_memory_every - - class PolicyGradient(AbsAlgorithm): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. @@ -35,17 +20,14 @@ class PolicyGradient(AbsAlgorithm): Args: policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. It may or may not have a shared bottom stack. - config (PolicyGradientConfig): Configuration for the PG algorithm. - post_step (Callable): Custom function to be called after each gradient step. This can be used for tracking - the learning progress. The function should have signature (loss, tracker) -> None. Defaults to None. + reward_discount (float): Reward decay as defined in standard RL terminology. """ - def __init__(self, policy_net: DiscretePolicyNet, config: PolicyGradientConfig, post_step: Callable = None): + def __init__(self, policy_net: DiscretePolicyNet, reward_discount: float): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") super().__init__() self.policy_net = policy_net - self.config = config - self._post_step = post_step + self.reward_discount = reward_discount self._num_learn_calls = 0 self.device = self.policy_net.device @@ -57,43 +39,35 @@ def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def get_update_info(self, experience_batch: ExperienceSet): + def get_update_info(self, experience_batch: ExperienceSet) -> dict: return self.policy_net.get_gradients(self._get_loss(experience_batch)) - def apply(self, grad_dict: dict): - self.policy_net.apply(grad_dict) - def _get_loss(self, batch: ExperienceSet): self.policy_net.train() log_p = torch.from_numpy(np.asarray([act[1] for act in batch.actions])).to(self.device) rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) - returns = get_truncated_cumulative_reward(rewards, self.config.reward_discount) + returns = get_truncated_cumulative_reward(rewards, self.reward_discount) returns = torch.from_numpy(returns).to(self.device) return -(log_p * returns).mean() - def learn(self, experience_batch: ExperienceSet): + def learn(self, data: Union[ExperienceSet, dict]): """ This should be called at the end of a simulation episode and the experiences obtained from the experience store's ``get`` method should be a sequential set, i.e., in the order in which they are generated during the simulation. Otherwise, the return values may be meaningless. """ assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." - self.policy_net.train() - loss = self._get_loss(experience_batch) - self.policy_net.step(loss) - - if self._post_step: - self._post_step(loss.detach().cpu().numpy(), self.tracker) - - self._num_learn_calls += 1 - if self._num_learn_calls % self.config.clear_experience_memory_every == 0: - self.experience_store.clear() + # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. + if isinstance(data, ExperienceSet): + self.policy_net.train() + loss = self._get_loss(data) + self.policy_net.step(loss) + # Otherwise treat the data as a dict of gradients that can be applied directly to the network. + else: + self.policy_net.apply(data) def set_state(self, policy_state): self.policy_net.load_state_dict(policy_state) def get_state(self): return self.policy_net.state_dict() - - def post_step(self): - diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index 7c569e6f3..9c6c30745 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -17,7 +17,7 @@ class AbsSampler(ABC): """ def __init__(self, experience_store: ExperienceStore): super().__init__() - self.experience_store = experience_store + self.experience_memory = experience_store @abstractmethod def get(self) -> ExperienceSet: @@ -44,15 +44,15 @@ def __init__(self, experience_store: ExperienceStore, batch_size: int = 32, repl if batch_size <= 0 and batch_size != -1: raise ValueError("batch_size must be -1 or a positive integer") super().__init__() - self.experience_store = experience_store + self.experience_memory = experience_store self.batch_size = batch_size self.replace = replace def get(self) -> ExperienceSet: - batch_size = self.experience_store.size if self.batch_size == -1 else self.batch_size - indexes = np.random.choice(self.experience_store.size, size=batch_size, replace=self.replace) + batch_size = self.experience_memory.size if self.batch_size == -1 else self.batch_size + indexes = np.random.choice(self.experience_memory.size, size=batch_size, replace=self.replace) return ExperienceSet( - *[[self.experience_store.data[key][idx] for idx in indexes] for key in self.experience_store.keys] + *[[self.experience_memory.data[key][idx] for idx in indexes] for key in self.experience_memory.keys] ) @@ -88,7 +88,7 @@ def __init__( if beta > 1.0: raise ValueError("beta should be between 0.0 and 1.0") super().__init__(experience_store) - self._sum_tree = np.zeros(2 * self.experience_store.capacity - 1) + self._sum_tree = np.zeros(2 * self.experience_memory.capacity - 1) self.batch_size = batch_size self.alpha = alpha self.beta = beta @@ -108,7 +108,7 @@ def update(self, indexes, td_errors): """Update priority values at given indexes.""" for idx, err in zip(indexes, td_errors): priority = self._get_priority(err) - tree_idx = idx + self.experience_store.capacity - 1 + tree_idx = idx + self.experience_memory.capacity - 1 delta = priority - self._sum_tree[tree_idx] self._sum_tree[tree_idx] = priority self._update(tree_idx, delta) @@ -121,20 +121,20 @@ def get(self): low, high = segment_len * i, segment_len * (i + 1) sampled_val = np.random.uniform(low=low, high=high) idx = self._get(0, sampled_val) - data_idx = idx - self.experience_store.capacity + 1 + data_idx = idx - self.experience_memory.capacity + 1 indexes.append(data_idx) priorities.append(self._sum_tree[idx]) self.beta = min(1., self.beta + self.beta_step) sampling_probabilities = priorities / self.total() - is_weights = np.power(self.experience_store.size * sampling_probabilities, -self.beta) + is_weights = np.power(self.experience_memory.size * sampling_probabilities, -self.beta) is_weights /= is_weights.max() return ExperienceSet( - states=[self.experience_store.data["states"][idx] for idx in indexes], - actions=[self.experience_store.data["actions"][idx] for idx in indexes], - rewards=[self.experience_store.data["rewards"][idx] for idx in indexes], - next_states=[self.experience_store.data["next_states"][idx] for idx in indexes], + states=[self.experience_memory.data["states"][idx] for idx in indexes], + actions=[self.experience_memory.data["actions"][idx] for idx in indexes], + rewards=[self.experience_memory.data["rewards"][idx] for idx in indexes], + next_states=[self.experience_memory.data["next_states"][idx] for idx in indexes], info=[{"index": idx, "is_weight": wt} for idx, wt in zip(indexes, is_weights)] ) diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 5bc1e5124..7a47fd158 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -1,11 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .policy import AbsCorePolicy, AbsPolicy, NullPolicy +from .policy import AbsPolicy, CorePolicy, NullPolicy from .policy_manager import AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager from .trainer import trainer_node, trainer_process __all__ = [ - "AbsCorePolicy", "AbsPolicy", "AbsPolicyManager", "LocalPolicyManager", "MultiNodePolicyManager", + "AbsPolicy", "AbsPolicyManager", "CorePolicy", "LocalPolicyManager", "MultiNodePolicyManager", "MultiProcessPolicyManager", "NullPolicy", "trainer_node", "trainer_process" ] diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index b8975bf80..3f45bde10 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -26,105 +26,62 @@ def choose_action(self, state): return None -class AbsCorePolicy(AbsPolicy): +class CorePolicy(AbsPolicy): """Policy that can update itself using simulation experiences. Reinforcement learning (RL) policies should inherit from this. Args: algorithm (AbsAlgorithm): Algorithm instance. - experience_store (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences + experience_memory (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences generated by the policy. experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to ``UnifromSampler``. experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. num_epochs (int): Number of times ``self.algorithm.learn()`` is called in each call to ``update``. Defaults to 1. - update_trigger (int): The required number of new experiences to trigger a call to ``update``. Defaults to 1. - warmup (int): The minimum number of experiences in the experience memory required to trigger a call to ``update`` - Defaults to 1. - auto (bool): If true, the policy instance should be updated in the ``learn`` method. Otherwise, the method simply - returns the necessary update information. The latter is usually used in Defaults to True. """ def __init__( self, algorithm: AbsAlgorithm, - experience_store: ExperienceStore, + experience_memory: ExperienceStore, experience_sampler_cls=UniformSampler, - experience_sampler_kwargs: dict = {}, - num_epochs: int = 1, - update_trigger: int = 1, - warmup: int = 1, - auto: bool = True + experience_sampler_kwargs: dict = {} ): super().__init__() self.algorithm = algorithm - self.experience_store = experience_store - self.sampler = experience_sampler_cls(self.experience_store, **experience_sampler_kwargs) - self.num_epochs = num_epochs - self.update_trigger = update_trigger - self.warmup = warmup - self.tracker = {} - self.auto = auto - - self._exp_cache = ExperienceSet() - self._new_exp_counter = 0 + self.experience_memory = experience_memory + self.sampler = experience_sampler_cls(self.experience_memory, **experience_sampler_kwargs) + self.exploring = False + self._update_index = 0 def choose_action(self, state): - return self.algorithm.choose_action(state) - - def update(self, experience_batch): - """Policy update logic is implemented here. - - This usually includes retrieving experiences as training samples from the experience manager and - updating the underlying models using these samples. - """ - if self.auto: - if self._new_exp_counter >= self.update_trigger and self.experience_store.size >= self.warmup: - experience_batch = self.sampler.get() - for _ in range(self.num_epochs): - self.algorithm.learn(experience_batch) - else: - return self.algorithm.get_update_info(experience_batch) - - def get_state(self): - """Return the current state of the policy. - - The implementation must be in correspondence with that of ``set_state``. For example, if a torch model - is contained in the policy, ``get_state`` may include a call to ``state_dict()`` on the model, while - ``set_state`` should accordingly include ``load_state_dict()``. - """ - return self.algorithm.get_state() - - def set_state(self, state): - """Set the policy state to ``policy_state``. - - The implementation must be in correspondence with that of ``get_state``. For example, if a torch model - is contained in the policy, ``set_state`` may include a call to ``load_state_dict()`` on the model, while - ``get_state`` should accordingly include ``state_dict()``. - """ - self.algorithm.set_state(state) - - def get_update_info(self, experience_batch: ExperienceSet): - pass - - def apply(self, update_info): - if self.auto: - raise + return self.algorithm.choose_action(state, explore=self.exploring) - self.algorithm.apply(update_info) - - def store(self, exp: ExperienceSet) -> bool: + def store_experiences(self, exp: ExperienceSet) -> bool: """ Store incoming experiences and update if necessary. """ - self.experience_store.put(exp) + self.experience_memory.put(exp) # print( - # f"exp mem size = {self.experience_store.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" + # f"exp mem size = {self.experience_memory.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" # ) + def reset_memory(self): + """Clear the experience store.""" + self.experience_memory.clear() + + def update(self, grad=None): + if grad: + self.algorithm.apply(grad) + else: + self.algorithm.learn(self.sampler.get()) + + self._update_index += 1 + self.algorithm.post_update(self._update_index) + def explore(self): - self.algorithm = False + self.exploring = False def exploit(self): self.exploring = True @@ -132,11 +89,3 @@ def exploit(self): def exploration_step(self): if self.algorithm.exploration: self.algorithm.exploration.step() - - def load(self, path: str): - """Load the policy state from disk.""" - self.algorithm.load(path) - - def save(self, path: str): - """Save the policy state to disk.""" - self.algorithm.save(path) diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 7721f015e..115795ad1 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -10,7 +10,7 @@ from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.experience import ExperienceSet -from maro.rl.policy import AbsCorePolicy +from maro.rl.policy import CorePolicy from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger @@ -23,32 +23,39 @@ class AbsPolicyManager(ABC): The actual policy instances may reside here or be distributed on a set of processes or remote nodes. Args: - policy_dict (Dict[str, AbsCorePolicy]): A list of policies managed by the manager. + policy_dict (Dict[str, CorePolicy]): A list of policies managed by the manager. num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of - times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the - number of learning epochs will be set to 1 for each policy. + times ``policy.learn()`` is called in each call to ``update``. Defaults to 1 for each policy. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the - required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, - all triggers will be set to 1. + required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to 1 for + each policy. warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for - each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, - ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to - None. + each policy. Defaults to 1 for each policy. + reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory + should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms + to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. """ - def __init__(self, policy_dict: Dict[str, AbsCorePolicy], post_update: Callable = None): + def __init__( + self, + policy_dict: Dict[str, CorePolicy], + num_epochs: Dict[str, int] = defaultdict(lambda: 1), + update_trigger: Dict[str, int] = defaultdict(lambda: 1), + warmup: Dict[str, int] = defaultdict(lambda: 1), + reset_memory: Dict[str, int] = defaultdict(lambda: False) + ): for policy in policy_dict.values(): - if not isinstance(policy, AbsCorePolicy): - raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") + if not isinstance(policy, CorePolicy): + raise ValueError("Only 'CorePolicy' instances can be managed by a policy manager.") super().__init__() self.policy_dict = policy_dict - self._post_update = post_update + self.num_epochs = num_epochs + self.update_trigger = update_trigger + self.warmup = warmup + self.reset_memory = reset_memory self._update_history = [set(policy_dict.keys())] - self.tracker = {} @property def version(self): @@ -59,20 +66,20 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): """Logic for handling incoming experiences is implemented here.""" raise NotImplementedError - def get_state(self, cur_version: int = None): + def get_state(self, cur_version: int = None, inference: bool = True): if cur_version is None: cur_version = self.version - 1 updated = set() for version in range(cur_version + 1, len(self._update_history)): updated |= self._update_history[version] - return {name: self.policy_dict[name].get_state() for name in updated} + return {name: self.policy_dict[name].algorithm.get_state(inference=inference) for name in updated} class LocalPolicyManager(AbsPolicyManager): """Policy manager that contains the actual policy instances. Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. + policy_dict (Dict[str, CorePolicy]): Policies managed by the manager. num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the number of learning epochs will be set to 1 for each policy. @@ -82,21 +89,20 @@ class LocalPolicyManager(AbsPolicyManager): warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, - ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to - None. + reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory + should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms + to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( self, - policy_dict: Dict[str, AbsCorePolicy], + policy_dict: Dict[str, CorePolicy], num_epochs: Dict[str, int] = None, update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, - post_update: Callable = None, + reset_memory: Dict[str, int] = defaultdict(lambda: False), log_dir: str = getcwd() ): super().__init__( @@ -104,7 +110,7 @@ def __init__( num_epochs=num_epochs, update_trigger=update_trigger, warmup=warmup, - post_update=post_update + reset_memory=reset_memory ) self._new_exp_counter = defaultdict(int) self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) @@ -119,14 +125,16 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): updated = set() for policy_name, exp in exp_by_policy.items(): policy = self.policy_dict[policy_name] - policy.experience_store.put(exp) + policy.experience_memory.put(exp) self._new_exp_counter[policy_name] += exp.size if ( self._new_exp_counter[policy_name] >= self.update_trigger[policy_name] and - policy.experience_store.size >= self.warmup[policy_name] + policy.experience_memory.size >= self.warmup[policy_name] ): for _ in range(self.num_epochs[policy_name]): - policy.learn() + policy.update() + if self.reset_memory[policy_name]: + policy.reset_memory() updated.add(policy_name) self._new_exp_counter[policy_name] = 0 @@ -134,9 +142,6 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") - if self._post_update: - self._post_update([policy.tracker for policy in self.policy_dict.values()]) - self._logger.debug(f"policy update time: {time.time() - t0}") @@ -144,7 +149,7 @@ class MultiProcessPolicyManager(AbsPolicyManager): """Policy manager that spawns a set of trainer processes for parallel training. Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. + policy_dict (Dict[str, CorePolicy]): Policies managed by the manager. num_trainers (int): Number of trainer processes to be forked. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` @@ -158,22 +163,22 @@ class MultiProcessPolicyManager(AbsPolicyManager): warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers,) - -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. + reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory + should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms + to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( self, - policy_dict: Dict[str, AbsCorePolicy], + policy_dict: Dict[str, CorePolicy], num_trainers: int, create_policy_func_dict: Dict[str, Callable], num_epochs: Dict[str, int] = None, update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, - post_update: Callable = None, + reset_memory: Dict[str, int] = defaultdict(lambda: False), log_dir: str = getcwd(), ): super().__init__( @@ -181,7 +186,7 @@ def __init__( num_epochs=num_epochs, update_trigger=update_trigger, warmup=warmup, - post_update=post_update + reset_memory=reset_memory ) self._policy2trainer = {} self._trainer2policies = defaultdict(list) @@ -206,7 +211,7 @@ def __init__( trainer_id, trainer_end, {name: create_policy_func_dict[name] for name in policy_names}, - {name: self.policy_dict[name].get_state() for name in policy_names}, + {name: self.policy_dict[name].algorithm.get_state() for name in policy_names}, {name: self.num_epochs[name] for name in policy_names} ), kwargs={"log_dir": log_dir} @@ -232,10 +237,8 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): "experiences": {name: exp_to_send[name] for name in self._trainer2policies[trainer_id]} }) - trackers = [] for conn in self._manager_end.values(): result = conn.recv() - trackers.append(result["tracker"]) for policy_name, policy_state in result["policy"].items(): self.policy_dict[policy_name].set_state(policy_state) @@ -243,9 +246,6 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") - if self._post_update: - self._post_update(trackers) - def exit(self): """Tell the trainer processes to exit.""" for conn in self._manager_end.values(): @@ -256,7 +256,7 @@ class MultiNodePolicyManager(AbsPolicyManager): """Policy manager that communicates with a set of remote nodes for parallel training. Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. + policy_dict (Dict[str, CorePolicy]): Policies managed by the manager. group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where @@ -270,9 +270,9 @@ class MultiNodePolicyManager(AbsPolicyManager): warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers,) - -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. + reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory + should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms + to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -281,13 +281,13 @@ class MultiNodePolicyManager(AbsPolicyManager): """ def __init__( self, - policy_dict: Dict[str, AbsCorePolicy], + policy_dict: Dict[str, CorePolicy], group: str, num_trainers: int, num_epochs: Dict[str, int] = None, update_trigger: Dict[str, int] = None, warmup: Dict[str, int] = None, - post_update: Callable = None, + reset_memory: Dict[str, int] = defaultdict(lambda: False), log_dir: str = getcwd(), proxy_kwargs: dict = {} ): @@ -296,7 +296,7 @@ def __init__( num_epochs=num_epochs, update_trigger=update_trigger, warmup=warmup, - post_update=post_update + reset_memory=reset_memory ) peers = {"trainer": num_trainers} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) @@ -319,8 +319,10 @@ def __init__( SessionMessage( MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, body={ - MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}, - MsgKey.NUM_EPOCHS: {name: self.num_epochs[name] for name in policy_names} + MsgKey.POLICY_STATE: + {name: self.policy_dict[name].algorithm.get_state() for name in policy_names}, + MsgKey.NUM_EPOCHS: + {name: self.num_epochs[name] for name in policy_names} } ) ) @@ -344,11 +346,10 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp - trackers, dones = [], 0 + dones = 0 self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())) for msg in self._proxy.receive(): if msg.tag == MsgTag.TRAIN_DONE: - trackers.append(msg.body[MsgKey.TRACKER]) for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): self.policy_dict[policy_name].set_state(policy_state) dones += 1 @@ -359,9 +360,6 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") - if self._post_update: - self._post_update(trackers) - def exit(self): """Tell the remote trainers to exit.""" self._proxy.ibroadcast("trainer", MsgTag.EXIT, SessionType.NOTIFICATION) diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index c002356d4..35dc33962 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import time +from collections import defaultdict from multiprocessing.connection import Connection from os import getcwd from typing import Callable, Dict @@ -17,6 +18,7 @@ def trainer_process( create_policy_func_dict: Dict[str, Callable], initial_policy_states: dict, num_epochs: Dict[str, int], + reset_memory: Dict[str, bool] = defaultdict(lambda: False), log_dir: str = getcwd() ): """Policy trainer process which can be spawned by a ``MultiProcessPolicyManager``. @@ -31,6 +33,9 @@ def trainer_process( num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the number of learning epochs will be set to 1 for each policy. + reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory + should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms + to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} @@ -46,11 +51,12 @@ def trainer_process( for name, exp in msg["experiences"].items(): policy_dict[name].store(exp) for _ in range(num_epochs[name]): - policy_dict[name].learn() + policy_dict[name].update() + if reset_memory[name]: + policy_dict[name].reset_memory() logger.debug(f"total policy update time: {time.time() - t0}") conn.send({ - "policy": {name: policy_dict[name].get_state() for name in msg["experiences"]}, - "tracker": {name: policy_dict[name].tracker for name in msg["experiences"]} + "policy": {name: policy_dict[name].algorithm.get_state() for name in msg["experiences"]} }) elif msg["type"] == "quit": break @@ -61,6 +67,7 @@ def trainer_node( trainer_idx: int, create_policy_func_dict: Dict[str, Callable], num_epochs: Dict[str, int], + reset_memory: Dict[str, int] = defaultdict(lambda: False), proxy_kwargs: dict = {}, log_dir: str = getcwd() ): @@ -73,9 +80,12 @@ def trainer_node( create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. - num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of - times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the - number of learning epochs will be set to 1 for each policy. + num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of times + ``policy.update()`` is called in each learning round. Defaults to None, in which case the number of + learning epochs will be set to 1 for each policy. + reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory + should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms + to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. @@ -101,11 +111,13 @@ def trainer_node( for name, exp in msg.body[MsgKey.EXPERIENCES].items(): policy_dict[name].store(exp) for _ in range(num_epochs[name]): - policy_dict[name].learn() + policy_dict[name].update() + if reset_memory[name]: + policy_dict[name].reset_memory() msg_body = { - MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.EXPERIENCES]}, - MsgKey.TRACKER: {name: policy_dict[name].tracker for name in msg.body[MsgKey.EXPERIENCES]} + MsgKey.POLICY_STATE: + {name: policy_dict[name].algorithm.get_state() for name in msg.body[MsgKey.EXPERIENCES]} } logger.info(f"total policy update time: {time.time() - t0}") proxy.reply(msg, tag=MsgTag.TRAIN_DONE, body=msg_body) @@ -114,8 +126,6 @@ def trainer_node( msg_body = { MsgKey.UPDATE_INFO: {policy_dict[name].get_update_info(exp) for name, exp in msg.body[MsgKey.EXPERIENCES].items()}, - MsgKey.TRACKER: - {name: policy_dict[name].tracker for name in msg.body[MsgKey.EXPERIENCES]} } logger.info(f"total time to get update info: {time.time() - t0}") proxy.reply(msg, tag=MsgTag.UPDATE_INFO, body=msg_body) From 704c17fcfbf05269a4cdb093cdfdb828ca4b9ebd Mon Sep 17 00:00:00 2001 From: ysqyang Date: Mon, 9 Aug 2021 00:26:09 +0800 Subject: [PATCH 401/482] refined policy interfaces --- examples/rl/cim/__init__.py | 2 +- examples/rl/cim/{ => algorithms}/ac.py | 29 +------ examples/rl/cim/{ => algorithms}/dqn.py | 38 +-------- examples/rl/cim/policies.py | 48 +++++++++++ examples/rl/cim/policy_index.py | 23 ----- examples/rl/workflows/agent_wrapper.py | 5 +- .../policy_manager/policy_manager.py | 2 +- examples/rl/workflows/simple_learner.py | 2 +- examples/rl/workflows/synchronous/learner.py | 2 +- .../workflows/synchronous/rollout_worker.py | 2 +- maro/rl/algorithms/abs_algorithm.py | 11 +-- maro/rl/algorithms/ac.py | 48 ++++++----- maro/rl/algorithms/ddpg.py | 52 +++++++----- maro/rl/algorithms/dqn.py | 56 +++++++----- maro/rl/algorithms/pg.py | 40 ++++----- maro/rl/experience/__init__.py | 4 +- maro/rl/experience/experience_store.py | 33 +++---- maro/rl/experience/sampler.py | 40 ++++++--- maro/rl/policy/__init__.py | 6 +- maro/rl/policy/policy.py | 44 +++++----- maro/rl/policy/policy_manager.py | 85 ++++++------------- maro/rl/policy/trainer.py | 14 ++- maro/rl/utils/message_enums.py | 5 +- 23 files changed, 282 insertions(+), 309 deletions(-) rename examples/rl/cim/{ => algorithms}/ac.py (77%) rename examples/rl/cim/{ => algorithms}/dqn.py (62%) create mode 100644 examples/rl/cim/policies.py delete mode 100644 examples/rl/cim/policy_index.py diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index 7af3aff3d..00fea2c06 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -3,7 +3,7 @@ from .callbacks import post_collect, post_evaluate from .env_wrapper import get_env_wrapper -from .policy_index import agent2policy, rl_policy_func_index, update_trigger, warmup +from .policies import agent2policy, rl_policy_func_index, update_trigger, warmup __all__ = [ "agent2policy", "post_collect", "post_evaluate", "get_env_wrapper", "rl_policy_func_index", diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/algorithms/ac.py similarity index 77% rename from examples/rl/cim/ac.py rename to examples/rl/cim/algorithms/ac.py index 128c2ec40..fb2102f1a 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/algorithms/ac.py @@ -8,9 +8,7 @@ import torch from maro.rl.algorithms import ActorCritic -from maro.rl.experience import ExperienceStore from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption -from maro.rl.policy import CorePolicy cim_path = os.path.dirname(os.path.dirname(__file__)) @@ -59,25 +57,7 @@ } -experience_config = { - "memory": { - "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, - "update": {"capacity": 100000, "overwrite_type": "rolling"} - }, - "sampler": { - "rollout": { - "batch_size": -1, - "replace": False - }, - "update": { - "batch_size": 128, - "replace": True - } - } -} - - -def get_ac_policy(learning: bool = True): +def get_algorithm(): class MyACNET(DiscreteACNet): def forward(self, states, actor: bool = True, critic: bool = True): states = torch.from_numpy(np.asarray(states)) @@ -98,10 +78,7 @@ def forward(self, states, actor: bool = True, critic: bool = True): optim_option={ "actor": OptimOption(**ac_net_config["optimization"]["actor"]), "critic": OptimOption(**ac_net_config["model"]["optimization"]["critic"]) - } if learning else None + } ) - return CorePolicy( - ActorCritic(ac_net, ac_config["algorithm"]), - ExperienceStore(**experience_config["memory"]) - ) + return ActorCritic(ac_net, ac_config["algorithm"]) diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/algorithms/dqn.py similarity index 62% rename from examples/rl/cim/dqn.py rename to examples/rl/cim/algorithms/dqn.py index 9a00a673f..426b01d83 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/algorithms/dqn.py @@ -9,10 +9,8 @@ import torch.nn as nn from maro.rl.algorithms import DQN -from maro.rl.experience import ExperienceStore, UniformSampler from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption -from maro.rl.policy import CorePolicy cim_path = os.path.dirname(os.path.realpath(__file__)) @@ -55,24 +53,6 @@ } -experience_config = { - "memory": { - "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, - "update": {"capacity": 100000, "overwrite_type": "rolling"} - }, - "sampler": { - "rollout": { - "batch_size": -1, - "replace": False - }, - "update": { - "batch_size": 128, - "replace": True - } - } -} - - class QNet(DiscreteQNet): def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): super().__init__(component, optim_option=optim_option, device=device) @@ -84,10 +64,10 @@ def forward(self, states): return self.component(states) -def get_dqn_policy(learning: bool = True): +def get_algorithm(): qnet = QNet( FullyConnectedBlock(**q_net_config["network"]), - optim_option=OptimOption(**q_net_config["optimization"]) if learning else None + optim_option=OptimOption(**q_net_config["optimization"]) ) exploration = EpsilonGreedyExploration() exploration.register_schedule( @@ -95,16 +75,4 @@ def get_dqn_policy(learning: bool = True): param_name="epsilon", **exploration_config ) - return CorePolicy( - DQN(qnet, **dqn_config, exploration=exploration), - ExperienceStore(**experience_config) - ) - if mode == "update": - exp_memory = ExperienceStore(**config["experience_store"]["update"]) - exp_sampler_kwargs = config["sampler"]["update"] - else: - exp_memory = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) - exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - - algorithm = - return CorePolicy(algorithm, exp_memory, exp_sampler_cls=UniformSampler, exp_sampler_kwargs=exp_sampler_kwargs) + return DQN(qnet, **dqn_config, exploration=exploration) diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py new file mode 100644 index 000000000..46915519d --- /dev/null +++ b/examples/rl/cim/policies.py @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import importlib +import os +import sys + +from maro.rl.policy import CorePolicy, PolicyUpdateOptions + +cim_path = os.path.dirname(os.path.realpath(__file__)) +algo_path = os.path.join(cim_path, "algorithms") + +if cim_path not in sys.path: + sys.path.insert(0, cim_path) +if algo_path not in sys.path: + sys.path.insert(0, algo_path) + +from env_wrapper import AGENT_IDS + +update_options = { + name: PolicyUpdateOptions(update_trigger=64, warmup=1024, num_epochs=10, reset_memory=False, data_parallel=False) + for name in AGENT_IDS +} + +experience_memory_config = { + "memory_capacity": 100000, + "random_overwrite": False +} + +sampling_config = { + "rollout": {"batch_size": 1280, "replace": True}, + "learning": {"batch_size": 128, "replace": True} +} + + +algorithm_type = "dqn" # "dqn" or "ac" +module = importlib.import_module(algorithm_type) + +def get_policy(rollout_only: bool = False): + return CorePolicy( + getattr(module, "get_algorithm")(), + **experience_memory_config, + sampler_kwargs=sampling_config["rollout" if rollout_only else "learning"] + ) + +# use agent IDs as policy names since each agent uses a separate policy +rl_policy_func_index = {name: get_policy for name in AGENT_IDS} +agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py deleted file mode 100644 index af237df5e..000000000 --- a/examples/rl/cim/policy_index.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - - -cim_path = os.path.dirname(os.path.realpath(__file__)) -if cim_path not in sys.path: - sys.path.insert(0, cim_path) -from algorithms.ac import get_ac -from algorithms.dqn import get_dqn -from env_wrapper import AGENT_IDS - -update_trigger = {name: 128 for name in AGENT_IDS} -warmup = {name: 1 for name in AGENT_IDS} - - - - -# use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {name: get_policy() for name in AGENT_IDS} -agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/workflows/agent_wrapper.py b/examples/rl/workflows/agent_wrapper.py index 45cb77fa1..a1b7bf597 100644 --- a/examples/rl/workflows/agent_wrapper.py +++ b/examples/rl/workflows/agent_wrapper.py @@ -13,12 +13,11 @@ from general import agent2policy, non_rl_policy_func_index, rl_policy_func_index -def get_agent_wrapper(local_update: bool = False): - policy_mode = "inference-update" if local_update else "inference" +def get_agent_wrapper(rollout_only: bool = False): return AgentWrapper( { **{name: func() for name, func in non_rl_policy_func_index.items()}, - **{name: func(mode=policy_mode) for name, func in rl_policy_func_index.items()} + **{name: func(rollout_only=rollout_only) for name, func in rl_policy_func_index.items()} }, agent2policy ) diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py index 2b18951a5..995f8ecdd 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -15,7 +15,7 @@ def get_policy_manager(): train_mode = getenv("TRAINMODE", default="single-process") - policy_dict = {name: func() for name, func in rl_policy_func_index.items()} + policy_dict = {name: func(rollout_only=False) for name, func in rl_policy_func_index.items()} if train_mode == "single-process": return LocalPolicyManager( policy_dict, diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index e965ef11d..fbee34204 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -21,7 +21,7 @@ if __name__ == "__main__": SimpleLearner( get_env_wrapper(), - get_agent_wrapper(local_update=True), + get_agent_wrapper(rollout_only=False), num_episodes=config["num_episodes"], num_steps=config["num_steps"], eval_env=get_eval_env_wrapper(), diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py index 7d83bc740..7207c08ef 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -24,7 +24,7 @@ def get_rollout_manager(): if rollout_mode == "single-process": return LocalRolloutManager( get_env_wrapper(), - get_agent_wrapper(), + get_agent_wrapper(rollout_only=True), num_steps=num_steps, post_collect=post_collect, post_evaluate=post_evaluate, diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 269beb3f8..103dcefd0 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -26,7 +26,7 @@ getenv("ROLLOUTGROUP", default="ROLLOUT"), worker_id, get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), - get_agent_wrapper(), + get_agent_wrapper(rollout_only=True), eval_env_wrapper=get_eval_env_wrapper(), proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), diff --git a/maro/rl/algorithms/abs_algorithm.py b/maro/rl/algorithms/abs_algorithm.py index 5001a53ab..4b08ebb49 100644 --- a/maro/rl/algorithms/abs_algorithm.py +++ b/maro/rl/algorithms/abs_algorithm.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod -from typing import Union from maro.rl.experience import ExperienceSet from maro.rl.exploration import AbsExploration @@ -24,22 +23,16 @@ def __init__(self, exploration: AbsExploration = None): def choose_action(self, state, explore: bool = False): raise NotImplementedError - @abstractmethod - def get_gradients(self, experience_batch: ExperienceSet): - pass - @abstractmethod def apply(self, grad_dict: dict): + """Update the underlying parameters (i.e., network weights) with gradients.""" pass @abstractmethod - def learn(self, experience_batch: ExperienceSet): + def learn(self, experience_batch: ExperienceSet, inplace: bool = True) -> tuple: """Update logic is implemented here.""" raise NotImplementedError - def post_update(self, update_index: int): - pass - @abstractmethod def get_state(self, inference: bool = True): """Return the current state of the policy. diff --git a/maro/rl/algorithms/ac.py b/maro/rl/algorithms/ac.py index fc3cd71d5..005399476 100644 --- a/maro/rl/algorithms/ac.py +++ b/maro/rl/algorithms/ac.py @@ -1,17 +1,21 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Tuple, Union +from collections import namedtuple +from operator import index +from typing import Tuple import numpy as np import torch from torch.distributions import Categorical from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceSet +from maro.rl.experience import ExperienceBatch from maro.rl.model import DiscreteACNet from maro.rl.utils import get_torch_loss_cls +ACLossInfo = namedtuple("ACLossInfo", ["actor_loss", "critic_loss", "entropy", "total_loss", "grad"]) + class ActorCritic(AbsAlgorithm): """Actor Critic algorithm with separate policy and value models. @@ -66,15 +70,17 @@ def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else (actions, log_p) - def get_update_info(self, experience_batch: ExperienceSet) -> dict: - return self.ac_net.get_gradients(self._get_loss(experience_batch)) + def apply(self, grad_dict: dict): + """Apply gradients to the underlying parameterized model.""" + self.ac_net.apply(grad_dict) - def _get_loss(self, batch: ExperienceSet): + def learn(self, batch: ExperienceBatch, inplace: bool = True): + assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() - states, next_states = batch.states, batch.next_states - actions = torch.from_numpy(np.asarray([act[0] for act in batch.actions])).to(self.device) - log_p = torch.from_numpy(np.asarray([act[1] for act in batch.actions])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) + states, next_states = batch.data.states, batch.data.next_states + actions = torch.from_numpy(np.asarray([act[0] for act in batch.data.actions])).to(self.device) + log_p = torch.from_numpy(np.asarray([act[1] for act in batch.data.actions])).to(self.device) + rewards = torch.from_numpy(np.asarray(batch.data.rewards)).to(self.device) action_probs, state_values = self.ac_net(states) state_values = state_values.squeeze() @@ -96,23 +102,21 @@ def _get_loss(self, batch: ExperienceSet): # critic_loss critic_loss = self.critic_loss_func(state_values, return_est) - # total loss - loss = actor_loss + self.critic_loss_coeff * critic_loss + # entropy if self.entropy_coeff is not None: - loss -= self.entropy_coeff * Categorical(action_probs).entropy().mean() + entropy = -Categorical(action_probs).entropy().mean() + else: + entropy = 0 - return loss + total_loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy - def learn(self, data: Union[ExperienceSet, dict]): - assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." - # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. - if isinstance(data, ExperienceSet): - self.ac_net.train() - loss = self._get_loss(data) - self.ac_net.step(loss) - # Otherwise treat the data as a dict of gradients that can be applied directly to the network. + if inplace: + self.ac_net.step(total_loss) + grad = None else: - self.ac_net.apply(data) + grad = self.ac_net.get_gradients(total_loss) + + return ACLossInfo(actor_loss, critic_loss, entropy, total_loss, grad), batch.indexes def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/algorithms/ddpg.py b/maro/rl/algorithms/ddpg.py index 04b4c460a..3bb9e8b6c 100644 --- a/maro/rl/algorithms/ddpg.py +++ b/maro/rl/algorithms/ddpg.py @@ -1,17 +1,20 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import namedtuple from typing import Union import numpy as np import torch from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceSet +from maro.rl.experience import ExperienceBatch from maro.rl.exploration import GaussianNoiseExploration from maro.rl.model import ContinuousACNet from maro.rl.utils import get_torch_loss_cls +DDPGLossInfo = namedtuple("DDPGLossInfo", ["policy_loss", "q_loss", "total_loss", "grad"]) + class DDPG(AbsAlgorithm): """The Deep Deterministic Policy Gradient (DDPG) algorithm. @@ -57,7 +60,9 @@ def __init__( self.q_value_loss_coeff = q_value_loss_coeff self.soft_update_coeff = soft_update_coeff self.device = self.ac_net.device - self._num_steps = 0 + + self._ac_net_version = 0 + self._target_ac_net_version = 0 def choose_action(self, states, explore: bool = False) -> Union[float, np.ndarray]: self.ac_net.eval() @@ -68,17 +73,17 @@ def choose_action(self, states, explore: bool = False) -> Union[float, np.ndarra actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def get_update_info(self, experience_batch: ExperienceSet) -> dict: - return self.ac_net.get_gradients(self._get_loss(experience_batch)) - def apply(self, grad_dict: dict): self.ac_net.apply(grad_dict) + if self._ac_net_version - self._target_ac_net_version == self.update_target_every: + self._update_target() - def _get_loss(self, batch: ExperienceSet): + def learn(self, batch: ExperienceBatch, inplace: bool = True): + assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() - states, next_states = batch.states, batch.next_states - actual_actions = torch.from_numpy(batch.actions).to(self.device) - rewards = torch.from_numpy(batch.rewards).to(self.device) + states, next_states = batch.data.states, batch.data.next_states + actual_actions = torch.from_numpy(batch.data.actions).to(self.device) + rewards = torch.from_numpy(batch.data.rewards).to(self.device) if len(actual_actions.shape) == 1: actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) @@ -87,25 +92,26 @@ def _get_loss(self, batch: ExperienceSet): target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) - q_value_loss = self.q_value_loss_func(q_values, target_q_values) + q_loss = self.q_value_loss_func(q_values, target_q_values) policy_loss = -self.ac_net.value(states).mean() - return policy_loss + self.q_value_loss_coeff * q_value_loss - def learn(self, data: Union[ExperienceSet, dict]): - assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." - # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. - if isinstance(data, ExperienceSet): - self.ac_net.train() - loss = self._get_loss(data) - self.ac_net.step(loss) - # Otherwise treat the data as a dict of gradients that can be applied directly to the network. + total_loss = policy_loss + self.q_value_loss_coeff * q_loss + + if inplace: + self.ac_net.step(total_loss) + grad = None + self._ac_net_version += 1 + if self._ac_net_version - self._target_ac_net_version == self.update_target_every: + self._update_target() else: - self.ac_net.apply(data) + grad = self.ac_net.get_gradients(total_loss) + + return DDPGLossInfo(policy_loss, q_loss, total_loss, grad), batch.indexes - def post_update(self, update_index: int): + def _update_target(self): # soft-update target network - if update_index % self.update_target_every == 0: - self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) + self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) + self._target_ac_net_version = self._ac_net_version def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index 1af6846e0..4e78c68d4 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -1,16 +1,19 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Union +from collections import namedtuple +from typing import List, Tuple, Union import numpy as np import torch from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceSet +from maro.rl.experience import ExperienceBatch, ExperienceStore from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.model import DiscreteQNet +DQNLossInfo = namedtuple("DQNLossInfo", ["td_error", "loss", "grad"]) + class DQN(AbsAlgorithm): """The Deep-Q-Networks algorithm. @@ -55,6 +58,10 @@ def __init__( self.double = double self.device = self.q_net.device self._loss_func = torch.nn.MSELoss() + self._experience_memory = None + + self._q_net_version = 0 + self._target_q_net_version = 0 def choose_action(self, states, explore: bool = True) -> Union[int, np.ndarray]: self.q_net.eval() @@ -69,13 +76,19 @@ def choose_action(self, states, explore: bool = True) -> Union[int, np.ndarray]: actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def get_update_info(self, experience_batch: ExperienceSet) -> dict: - return self.q_net.get_gradients(self._get_loss(experience_batch)) + def apply(self, grad_dict: dict): + self.q_net.apply(grad_dict) + self._q_net_version += 1 + if self._q_net_version - self._target_q_net_version == self.update_target_every: + self._update_target() - def _get_loss(self, experience_batch: ExperienceSet): - states, next_states = experience_batch.states, experience_batch.next_states - actions = torch.from_numpy(np.asarray(experience_batch.actions)).to(self.device) - rewards = torch.from_numpy(np.asarray(experience_batch.rewards)).to(self.device) + def learn(self, batch: ExperienceBatch, inplace: bool = True): + assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." + # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. + self.q_net.train() + states, next_states = batch.data.states, batch.data.next_states + actions = torch.from_numpy(np.asarray(batch.data.actions)).to(self.device) + rewards = torch.from_numpy(np.asarray(batch.data.rewards)).to(self.device) # get target Q values with torch.no_grad(): @@ -87,25 +100,26 @@ def _get_loss(self, experience_batch: ExperienceSet): target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) - # gradient step + # loss computation q_values = self.q_net.q_values(states, actions) - return self._loss_func(q_values, target_q_values) + td_errors = target_q_values - q_values + loss = self._loss_func(q_values, target_q_values) - def learn(self, data: Union[ExperienceSet, dict]): - assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." - # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. - if isinstance(data, ExperienceSet): - self.q_net.train() - loss = self._get_loss(data) + if inplace: self.q_net.step(loss) - # Otherwise treat the data as a dict of gradients that can be applied directly to the network. + grad = None + self._q_net_version += 1 + if self._q_net_version - self._target_q_net_version == self.update_target_every: + self._update_target() else: - self.q_net.apply(data) + grad = self.q_net.get_gradients(loss) + + return DQNLossInfo(td_errors, loss, grad), batch.indexes - def post_update(self, update_index: int): + def _update_target(self): # soft-update target network - if update_index % self.update_target_every == 0: - self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) + self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) + self._target_q_net_version = self._q_net_version def set_state(self, policy_state): self.q_net.load_state_dict(policy_state["eval"]) diff --git a/maro/rl/algorithms/pg.py b/maro/rl/algorithms/pg.py index 9b9be2dd5..9959ff31c 100644 --- a/maro/rl/algorithms/pg.py +++ b/maro/rl/algorithms/pg.py @@ -1,16 +1,19 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Tuple, Union +from collections import namedtuple +from typing import Tuple import numpy as np import torch from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceSet +from maro.rl.experience import ExperienceBatch from maro.rl.model import DiscretePolicyNet from maro.rl.utils import get_truncated_cumulative_reward +PGLossInfo = namedtuple("PGLossInfo", ["loss", "grad"]) + class PolicyGradient(AbsAlgorithm): """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. @@ -39,32 +42,31 @@ def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def get_update_info(self, experience_batch: ExperienceSet) -> dict: - return self.policy_net.get_gradients(self._get_loss(experience_batch)) - - def _get_loss(self, batch: ExperienceSet): - self.policy_net.train() - log_p = torch.from_numpy(np.asarray([act[1] for act in batch.actions])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) - returns = get_truncated_cumulative_reward(rewards, self.reward_discount) - returns = torch.from_numpy(returns).to(self.device) - return -(log_p * returns).mean() + def apply(self, grad_dict: dict): + """Apply gradients to the underlying parameterized model.""" + self.policy_net.apply(grad_dict) - def learn(self, data: Union[ExperienceSet, dict]): + def learn(self, batch: ExperienceBatch, inplace: bool = True): """ This should be called at the end of a simulation episode and the experiences obtained from the experience store's ``get`` method should be a sequential set, i.e., in the order in which they are generated during the simulation. Otherwise, the return values may be meaningless. """ assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." - # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. - if isinstance(data, ExperienceSet): - self.policy_net.train() - loss = self._get_loss(data) + self.policy_net.train() + log_p = torch.from_numpy(np.asarray([act[1] for act in batch.data.actions])).to(self.device) + rewards = torch.from_numpy(np.asarray(batch.data.rewards)).to(self.device) + returns = get_truncated_cumulative_reward(rewards, self.reward_discount) + returns = torch.from_numpy(returns).to(self.device) + loss = -(log_p * returns).mean() + + if inplace: self.policy_net.step(loss) - # Otherwise treat the data as a dict of gradients that can be applied directly to the network. + grad = None else: - self.policy_net.apply(data) + grad = self.policy_net.get_gradients(loss) + + return PGLossInfo(loss, grad), batch.indexes def set_state(self, policy_state): self.policy_net.load_state_dict(policy_state) diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 369546dd5..5984b0868 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -2,6 +2,6 @@ # Licensed under the MIT license. from .experience_store import ExperienceSet, ExperienceStore -from .sampler import AbsSampler, PrioritizedSampler, UniformSampler +from .sampler import AbsSampler, ExperienceBatch, PrioritizedSampler, UniformSampler -__all__ = ["AbsSampler", "ExperienceSet", "ExperienceStore", "PrioritizedSampler", "UniformSampler"] +__all__ = ["AbsSampler", "ExperienceBatch", "ExperienceSet", "ExperienceStore", "PrioritizedSampler", "UniformSampler"] diff --git a/maro/rl/experience/experience_store.py b/maro/rl/experience/experience_store.py index 9ec4cedc5..2f0da712e 100644 --- a/maro/rl/experience/experience_store.py +++ b/maro/rl/experience/experience_store.py @@ -53,18 +53,14 @@ class ExperienceStore: Args: capacity (int): Maximum number of experiences that can be stored. - overwrite_type (str): If storage capacity is bounded, this specifies how existing entries - are overwritten when the capacity is exceeded. Two types of overwrite behavior are supported: - - "rolling", where overwrite occurs sequentially with wrap-around. - - "random", where overwrite occurs randomly among filled positions. + random_overwrite (bool): This specifies overwrite behavior when the capacity is reached. If this is True, + overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with + wrap-around. Defaults to False. """ - def __init__(self, capacity: int, overwrite_type: str = "rolling"): - if overwrite_type not in {"rolling", "random"}: - raise ValueError(f"overwrite_type must be 'rolling' or 'random', got {overwrite_type}") - + def __init__(self, capacity: int, random_overwrite: bool = False): super().__init__() self._capacity = capacity - self._overwrite_type = overwrite_type + self._random_overwrite = random_overwrite self._keys = ExperienceSet.__slots__ self.data = {key: [None] * self._capacity for key in self._keys} self._size = 0 @@ -76,9 +72,9 @@ def capacity(self): return self._capacity @property - def overwrite_type(self): + def random_overwrite(self): """Overwrite method after the memory has reached capacity.""" - return self._overwrite_type + return self._random_overwrite @property def size(self): @@ -94,9 +90,7 @@ def put(self, experience_set: ExperienceSet): """Put a experience set in the store. Args: - experience_set (ExperienceSet): Experience set to be put in the store. If the store is full, - existing items will be overwritten according to the ``overwrite_type`` property. - + experience_set (ExperienceSet): Experience set to be put in the store. """ added_size = experience_set.size if added_size > self._capacity: @@ -107,19 +101,20 @@ def put(self, experience_set: ExperienceSet): if num_overwrites <= 0: indexes = list(range(self._size, num_experiences)) # follow the overwrite rule set at init - elif self._overwrite_type == "rolling": - # using the negative index convention for convenience - start_index = self._size - self._capacity - indexes = list(range(start_index, start_index + added_size)) - else: + elif self._random_overwrite: random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) indexes = list(range(self._size, self._capacity)) + list(random_indexes) + else: + # using the negative index convention for convenience + start_index = self._size - self._capacity + indexes = list(range(start_index, start_index + added_size)) for key in self.data: for idx, val in zip(indexes, getattr(experience_set, key)): self.data[key][idx] = val self._size = min(self._capacity, num_experiences) + return indexes def clear(self): """Empty the memory.""" diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index 9c6c30745..420783a06 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -2,12 +2,15 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod +from collections import namedtuple from typing import List import numpy as np from .experience_store import ExperienceSet, ExperienceStore +ExperienceBatch = namedtuple("ExperienceBatch", ["indexes", "data"]) + class AbsSampler(ABC): """Sampler class. @@ -20,7 +23,7 @@ def __init__(self, experience_store: ExperienceStore): self.experience_memory = experience_store @abstractmethod - def get(self) -> ExperienceSet: + def get(self) -> ExperienceBatch: """Sampling logic is defined here.""" raise NotImplementedError @@ -28,6 +31,9 @@ def on_put(self, experience_set: ExperienceSet, indexes: List[int]): """Callback to be executed after calling experience_store.put().""" pass + def update(self, indexes: List[int], info: list): + pass + class UniformSampler(ABC): """Uniform sampler class. @@ -48,11 +54,18 @@ def __init__(self, experience_store: ExperienceStore, batch_size: int = 32, repl self.batch_size = batch_size self.replace = replace - def get(self) -> ExperienceSet: + def get(self) -> ExperienceBatch: + if self.batch_size > self.experience_memory.size or self.batch_size == -1: + batch_size = self.experience_memory.size + else: + batch_size = self.batch_size batch_size = self.experience_memory.size if self.batch_size == -1 else self.batch_size indexes = np.random.choice(self.experience_memory.size, size=batch_size, replace=self.replace) - return ExperienceSet( - *[[self.experience_memory.data[key][idx] for idx in indexes] for key in self.experience_memory.keys] + return ExperienceBatch( + indexes=indexes, + data=ExperienceSet( + *[[self.experience_memory.data[key][idx] for idx in indexes] for key in self.experience_memory.keys] + ) ) @@ -104,7 +117,7 @@ def on_put(self, experience_set: ExperienceSet, indexes: List[int]): """Set the priorities of newly added experiences to the maximum value.""" self.update(indexes, [self._max_priority] * len(indexes)) - def update(self, indexes, td_errors): + def update(self, indexes: List[int], td_errors): """Update priority values at given indexes.""" for idx, err in zip(indexes, td_errors): priority = self._get_priority(err) @@ -113,7 +126,7 @@ def update(self, indexes, td_errors): self._sum_tree[tree_idx] = priority self._update(tree_idx, delta) - def get(self): + def get(self) -> ExperienceBatch: """Priority-based sampling.""" indexes, priorities = [], [] segment_len = self.total() / self.batch_size @@ -130,12 +143,15 @@ def get(self): is_weights = np.power(self.experience_memory.size * sampling_probabilities, -self.beta) is_weights /= is_weights.max() - return ExperienceSet( - states=[self.experience_memory.data["states"][idx] for idx in indexes], - actions=[self.experience_memory.data["actions"][idx] for idx in indexes], - rewards=[self.experience_memory.data["rewards"][idx] for idx in indexes], - next_states=[self.experience_memory.data["next_states"][idx] for idx in indexes], - info=[{"index": idx, "is_weight": wt} for idx, wt in zip(indexes, is_weights)] + return ExperienceBatch( + indexes=indexes, + data=ExperienceSet( + states=[self.experience_memory.data["states"][idx] for idx in indexes], + actions=[self.experience_memory.data["actions"][idx] for idx in indexes], + rewards=[self.experience_memory.data["rewards"][idx] for idx in indexes], + next_states=[self.experience_memory.data["next_states"][idx] for idx in indexes], + info=[{"is_weight": wt} for wt in is_weights] + ) ) def _get_priority(self, error): diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 7a47fd158..5ae80af34 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -2,10 +2,12 @@ # Licensed under the MIT license. from .policy import AbsPolicy, CorePolicy, NullPolicy -from .policy_manager import AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager +from .policy_manager import ( + AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, PolicyUpdateOptions +) from .trainer import trainer_node, trainer_process __all__ = [ "AbsPolicy", "AbsPolicyManager", "CorePolicy", "LocalPolicyManager", "MultiNodePolicyManager", - "MultiProcessPolicyManager", "NullPolicy", "trainer_node", "trainer_process" + "MultiProcessPolicyManager", "PolicyUpdateOptions", "NullPolicy", "trainer_node", "trainer_process" ] diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 3f45bde10..e41e7dbc8 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -33,27 +33,27 @@ class CorePolicy(AbsPolicy): Args: algorithm (AbsAlgorithm): Algorithm instance. - experience_memory (ExperienceStore): An ``ExperienceStore`` instance for storing and retrieving experiences - generated by the policy. - experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to - ``UnifromSampler``. - experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. - num_epochs (int): Number of times ``self.algorithm.learn()`` is called in each call to ``update``. Defaults - to 1. + memory_capacity (int): Capacity for the internal experience memory. + random_overwrite (bool): This specifies overwrite behavior when the capacity is reached. If this is True, + overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with + wrap-around. Defaults to False. + sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to ``UnifromSampler``. + sampler_kwargs (dict): Keyword arguments for ``sampler_cls``. """ def __init__( self, algorithm: AbsAlgorithm, - experience_memory: ExperienceStore, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs: dict = {} + memory_capacity: int, + random_overwrite: bool = False, + sampler_cls=UniformSampler, + sampler_kwargs: dict = {} ): super().__init__() self.algorithm = algorithm - self.experience_memory = experience_memory - self.sampler = experience_sampler_cls(self.experience_memory, **experience_sampler_kwargs) + self.experience_memory = ExperienceStore(memory_capacity, random_overwrite=random_overwrite) + self.sampler = sampler_cls(self.experience_memory, **sampler_kwargs) + self.exploring = False - self._update_index = 0 def choose_action(self, state): return self.algorithm.choose_action(state, explore=self.exploring) @@ -62,23 +62,23 @@ def store_experiences(self, exp: ExperienceSet) -> bool: """ Store incoming experiences and update if necessary. """ - self.experience_memory.put(exp) + indexes = self.experience_memory.put(exp) + self.sampler.on_put(exp, indexes) # print( # f"exp mem size = {self.experience_memory.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" # ) + def get_batch(self): + self.sampler.get() + def reset_memory(self): """Clear the experience store.""" self.experience_memory.clear() - def update(self, grad=None): - if grad: - self.algorithm.apply(grad) - else: - self.algorithm.learn(self.sampler.get()) - - self._update_index += 1 - self.algorithm.post_update(self._update_index) + def update(self): + batch = self.sampler.get() + loss_info, indexes = self.algorithm.learn(batch, inplace=True) + self.sampler.update(indexes, loss_info) def explore(self): self.exploring = False diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 115795ad1..1309765e2 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -3,7 +3,7 @@ import time from abc import ABC, abstractmethod -from collections import defaultdict +from collections import defaultdict, namedtuple from multiprocessing import Pipe, Process from os import getcwd from typing import Callable, Dict @@ -17,6 +17,11 @@ from .trainer import trainer_process +PolicyUpdateOptions = namedtuple( + "PolicyUpdateOptions", ["update_trigger", "warmup", "num_epochs", "reset_memory", "data_parallel"] +) + + class AbsPolicyManager(ABC): """Manage all policies. @@ -36,24 +41,14 @@ class AbsPolicyManager(ABC): should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. """ - def __init__( - self, - policy_dict: Dict[str, CorePolicy], - num_epochs: Dict[str, int] = defaultdict(lambda: 1), - update_trigger: Dict[str, int] = defaultdict(lambda: 1), - warmup: Dict[str, int] = defaultdict(lambda: 1), - reset_memory: Dict[str, int] = defaultdict(lambda: False) - ): + def __init__(self, policy_dict: Dict[str, CorePolicy], update_option: Dict[str, PolicyUpdateOptions]): for policy in policy_dict.values(): if not isinstance(policy, CorePolicy): raise ValueError("Only 'CorePolicy' instances can be managed by a policy manager.") super().__init__() self.policy_dict = policy_dict - self.num_epochs = num_epochs - self.update_trigger = update_trigger - self.warmup = warmup - self.reset_memory = reset_memory + self.update_option = update_option self._update_history = [set(policy_dict.keys())] @@ -99,19 +94,10 @@ class LocalPolicyManager(AbsPolicyManager): def __init__( self, policy_dict: Dict[str, CorePolicy], - num_epochs: Dict[str, int] = None, - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - reset_memory: Dict[str, int] = defaultdict(lambda: False), + update_option: Dict[str, PolicyUpdateOptions], log_dir: str = getcwd() ): - super().__init__( - policy_dict, - num_epochs=num_epochs, - update_trigger=update_trigger, - warmup=warmup, - reset_memory=reset_memory - ) + super().__init__(policy_dict, update_option) self._new_exp_counter = defaultdict(int) self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) @@ -128,12 +114,12 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): policy.experience_memory.put(exp) self._new_exp_counter[policy_name] += exp.size if ( - self._new_exp_counter[policy_name] >= self.update_trigger[policy_name] and - policy.experience_memory.size >= self.warmup[policy_name] + self._new_exp_counter[policy_name] >= self.update_option[policy_name].update_trigger and + policy.experience_memory.size >= self.update_option[policy_name].warmup ): - for _ in range(self.num_epochs[policy_name]): + for _ in range(self.update_option[policy_name].num_epochs): policy.update() - if self.reset_memory[policy_name]: + if self.update_option[policy_name].reset_memory: policy.reset_memory() updated.add(policy_name) self._new_exp_counter[policy_name] = 0 @@ -173,21 +159,12 @@ class MultiProcessPolicyManager(AbsPolicyManager): def __init__( self, policy_dict: Dict[str, CorePolicy], + update_option: Dict[str, PolicyUpdateOptions], num_trainers: int, create_policy_func_dict: Dict[str, Callable], - num_epochs: Dict[str, int] = None, - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - reset_memory: Dict[str, int] = defaultdict(lambda: False), log_dir: str = getcwd(), ): - super().__init__( - policy_dict, - num_epochs=num_epochs, - update_trigger=update_trigger, - warmup=warmup, - reset_memory=reset_memory - ) + super().__init__(policy_dict, update_option) self._policy2trainer = {} self._trainer2policies = defaultdict(list) self._exp_cache = defaultdict(ExperienceSet) @@ -212,7 +189,7 @@ def __init__( trainer_end, {name: create_policy_func_dict[name] for name in policy_names}, {name: self.policy_dict[name].algorithm.get_state() for name in policy_names}, - {name: self.num_epochs[name] for name in policy_names} + {name: self.update_option[name] for name in policy_names} ), kwargs={"log_dir": log_dir} ) @@ -225,8 +202,8 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._num_experiences_by_policy[policy_name] += exp.size self._exp_cache[policy_name].extend(exp) if ( - self._exp_cache[policy_name].size >= self.update_trigger[policy_name] and - self._num_experiences_by_policy[policy_name] >= self.warmup[policy_name] + self._exp_cache[policy_name].size >= self.update_option[policy_name].update_trigger and + self._num_experiences_by_policy[policy_name] >= self.update_option[policy_name].warmup ): exp_to_send[policy_name] = self._exp_cache.pop(policy_name) updated.add(policy_name) @@ -284,20 +261,11 @@ def __init__( policy_dict: Dict[str, CorePolicy], group: str, num_trainers: int, - num_epochs: Dict[str, int] = None, - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - reset_memory: Dict[str, int] = defaultdict(lambda: False), + update_option: Dict[str, PolicyUpdateOptions], log_dir: str = getcwd(), proxy_kwargs: dict = {} ): - super().__init__( - policy_dict, - num_epochs=num_epochs, - update_trigger=update_trigger, - warmup=warmup, - reset_memory=reset_memory - ) + super().__init__(policy_dict, update_option) peers = {"trainer": num_trainers} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) @@ -318,12 +286,7 @@ def __init__( self._proxy.send( SessionMessage( MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, - body={ - MsgKey.POLICY_STATE: - {name: self.policy_dict[name].algorithm.get_state() for name in policy_names}, - MsgKey.NUM_EPOCHS: - {name: self.num_epochs[name] for name in policy_names} - } + body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} ) ) @@ -333,8 +296,8 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._num_experiences_by_policy[policy_name] += exp.size self._exp_cache[policy_name].extend(exp) if ( - self._exp_cache[policy_name].size >= self.update_trigger[policy_name] and - self._num_experiences_by_policy[policy_name] >= self.warmup[policy_name] + self._exp_cache[policy_name].size >= self.update_option[policy_name].update_trigger and + self._num_experiences_by_policy[policy_name] >= self.update_option[policy_name].warmup ): exp_to_send[policy_name] = self._exp_cache.pop(policy_name) updated.add(policy_name) diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index 35dc33962..c3560c8aa 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -8,6 +8,7 @@ from typing import Callable, Dict from maro.communication import Proxy +from maro.rl.algorithms import AbsAlgorithm from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger @@ -125,12 +126,21 @@ def trainer_node( t0 = time.time() msg_body = { MsgKey.UPDATE_INFO: - {policy_dict[name].get_update_info(exp) for name, exp in msg.body[MsgKey.EXPERIENCES].items()}, + {name: policy_dict[name].get_update_info(exp) for name, exp in msg.body[MsgKey.EXPERIENCES].items()}, } logger.info(f"total time to get update info: {time.time() - t0}") proxy.reply(msg, tag=MsgTag.UPDATE_INFO, body=msg_body) elif msg.tag == MsgTag.UPDATE_POLICY_STATE: for name, state in msg.body[MsgKey.POLICY_STATE].items(): - policy_dict[name] = create_policy_func_dict[name]() policy_dict[name].set_state(state) logger.info(f"{proxy.name} updated policy {name}") + + +def gradient_worker_node( + group: str, + create_algorithm_func: Callable[[], AbsAlgorithm], + worker_idx: int, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + pass \ No newline at end of file diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 2fdfff1d3..c27d64d50 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -14,8 +14,7 @@ class MsgTag(Enum): CHOOSE_ACTION = "choose_action" ACTION = "action" LEARN = "LEARN" - GET_UPDATE_INFO = "get_update_info" - UPDATE_INFO = "update_info" + LOSS_INFO = "loss_info" UPDATE_POLICY_STATE = "update_policy_state" ABORT_ROLLOUT = "abort_rollout" EVAL_DONE = "eval_done" @@ -36,7 +35,7 @@ class MsgKey(Enum): STATE = "state" POLICY_STATE = "policy_state" NUM_EPOCHS = "num_epochs" - UPDATE_INFO = "update_info" + LOSS_INFO = "loss_info" EXPLORATION_STEP = "exploration_step" VERSION = "version" NUM_STEPS = "num_steps" From 56a54cb9d3a95b2adbd17bfe7ef01012499333a8 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 8 Aug 2021 16:35:33 +0000 Subject: [PATCH 402/482] fixed typo --- maro/rl/learning/simple_learner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py index 389e42851..6a95e9bbf 100644 --- a/maro/rl/learning/simple_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -5,7 +5,7 @@ from os import getcwd from typing import Callable, Dict, List, Union -from maro.rl.policy import AbsCorePolicy, LocalPolicyManager +from maro.rl.policy import CorePolicy, LocalPolicyManager from maro.utils import Logger from .agent_wrapper import AgentWrapper @@ -82,7 +82,7 @@ def __init__( # Create a policy manager to manage all trainable policies from the agent wrapper self.policy_manager = LocalPolicyManager( - {name: policy for name, policy in self.agent.policy_dict.items() if isinstance(policy, AbsCorePolicy)}, + {name: policy for name, policy in self.agent.policy_dict.items() if isinstance(policy, CorePolicy)}, update_trigger=update_trigger, warmup=warmup, post_update=post_update From 0b57d706d56f848a99a049e59d251dba50f9b2f5 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 9 Aug 2021 02:17:15 +0000 Subject: [PATCH 403/482] fixed bugs in refactored policy interface --- docs/source/key_components/rl_toolkit.rst | 6 +-- examples/rl/cim/__init__.py | 4 +- examples/rl/cim/algorithms/dqn.py | 1 - examples/rl/cim/policies.py | 2 +- .../rl/scripts/docker/docker_compose_yml.py | 46 ++++++++++--------- examples/rl/workflows/config.yml | 8 ++-- examples/rl/workflows/general.py | 3 +- .../policy_manager/policy_manager.py | 15 ++---- .../rl/workflows/policy_manager/trainer.py | 3 +- maro/rl/algorithms/dqn.py | 6 +-- maro/rl/experience/__init__.py | 4 +- .../{experience_store.py => memory.py} | 2 +- maro/rl/experience/sampler.py | 30 ++++++------ maro/rl/learning/agent_wrapper.py | 6 +-- .../learning/synchronous/rollout_manager.py | 2 +- maro/rl/policy/policy.py | 12 ++--- maro/rl/policy/policy_manager.py | 13 ++++-- maro/rl/policy/trainer.py | 10 ++-- .../rl_formulation.ipynb | 8 ++-- 19 files changed, 90 insertions(+), 91 deletions(-) rename maro/rl/experience/{experience_store.py => memory.py} (99%) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index f1ded31f2..bef7119b8 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -95,9 +95,9 @@ based on which updates can be made. class AbsCorePolicy(AbsPolicy): - def __init__(self, experience_store: ExperienceStore): + def __init__(self, experience_memory: ExperienceMemory): super().__init__() - self.experience_memory = experience_store + self.experience_memory = experience_memory @abstractmethod def update(self): @@ -183,7 +183,7 @@ Experience An ``ExperienceSet`` is a synonym for training data for RL policies. The data originate from the simulator and get processed and organized into a set of transitions in the form of (state, action, reward, next_state, info), where ''info'' contains information about the transition that is not encoded in the state but may be necessary -for sampling purposes. An ``ExperienceStore`` is a storage facility for experience sets and is maintained by +for sampling purposes. An ``ExperienceMemory`` is a storage facility for experience sets and is maintained by a policy for storing and retrieving training data. Sampling from the experience memory can be customized by registering a user-defined sampler to it. diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index 00fea2c06..47905f64a 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -3,9 +3,9 @@ from .callbacks import post_collect, post_evaluate from .env_wrapper import get_env_wrapper -from .policies import agent2policy, rl_policy_func_index, update_trigger, warmup +from .policies import agent2policy, rl_policy_func_index, update_option __all__ = [ "agent2policy", "post_collect", "post_evaluate", "get_env_wrapper", "rl_policy_func_index", - "update_trigger", "warmup" + "update_option" ] diff --git a/examples/rl/cim/algorithms/dqn.py b/examples/rl/cim/algorithms/dqn.py index 426b01d83..d1cd7f6e9 100644 --- a/examples/rl/cim/algorithms/dqn.py +++ b/examples/rl/cim/algorithms/dqn.py @@ -39,7 +39,6 @@ dqn_config = { "reward_discount": .0, "update_target_every": 5, - "train_epochs": 10, "soft_update_coeff": 0.1, "double": False } diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index 46915519d..94102bbba 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -17,7 +17,7 @@ from env_wrapper import AGENT_IDS -update_options = { +update_option = { name: PolicyUpdateOptions(update_trigger=64, warmup=1024, num_epochs=10, reset_memory=False, data_parallel=False) for name in AGENT_IDS } diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 0dc64f00f..462819962 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -31,6 +31,20 @@ ] } +common_env = [ + f"REDISHOST={config['redis']['host']}", + f"REDISPORT={config['redis']['port']}", + f"JOB={config['job']}", + f"SCENARIO={config['scenario']}", + f"MODE={config['mode']}", + f"EXPDIST={'1' if config['rollout_experience_distribution'] else '0'}" +] + +if config["mode"] == "sync": + common_env.append(f"NUMWORKERS={config['sync']['num_rollout_workers']}") +else: + common_env.append(f"NUMACTORS={config['async']['num_actors']}") + # trainer spec if config["policy_manager"]["train_mode"] == "multi-node": for trainer_id in range(num_trainers): @@ -41,10 +55,8 @@ trainer_spec["container_name"] = str_id trainer_spec["environment"] = [ f"TRAINERID={trainer_id}", - f"TRAINGROUP={config['policy_manager']['train_group']}", - f"REDISHOST={config['redis']['host']}", - f"REDISPORT={str(config['redis']['port'])}" - ] + f"TRAINGROUP={config['policy_manager']['train_group']}" + ] + common_env docker_compose_manifest["services"][str_id] = trainer_spec mode = config["mode"] @@ -58,7 +70,6 @@ "environment": [ f"ROLLOUTMODE={config['sync']['rollout_mode']}", f"NUMSTEPS={config['num_steps']}", - f"NUMWORKERS={config['sync']['num_rollout_workers']}", f"MAXLAG={config['max_lag']}", f"MINFINISH={config['sync']['min_finished_workers']}", f"MAXEXRECV={config['sync']['max_extra_recv_tries']}", @@ -68,10 +79,8 @@ f"EVALSCH={config['eval_schedule']}", f"TRAINMODE={config['policy_manager']['train_mode']}", f"TRAINGROUP={config['policy_manager']['train_group']}", - f"NUMTRAINERS={config['policy_manager']['num_trainers']}", - f"REDISHOST={config['redis']['host']}", - f"REDISPORT={config['redis']['port']}" - ] + f"NUMTRAINERS={config['policy_manager']['num_trainers']}" + ] + common_env } } # rollout worker spec @@ -85,10 +94,8 @@ worker_spec["environment"] = [ f"WORKERID={worker_id}", f"ROLLOUTGROUP={config['sync']['rollout_group']}", - f"REDISHOST={config['redis']['host']}", - f"REDISPORT={config['redis']['port']}", - f"EVALSCH={config['eval_schedule']}", - ] + f"EVALSCH={config['eval_schedule']}" + ] + common_env docker_compose_manifest["services"][str_id] = worker_spec elif mode == "async": # policy server spec @@ -99,11 +106,8 @@ "command": "python3 /maro/rl_examples/workflows/asynchronous/policy_server.py", "environment": [ f"GROUP={config['async']['group']}", - f"NUMACTORS={config['async']['num_actors']}", - f"MAXLAG={config['max_lag']}", - f"REDISHOST={config['redis']['host']}", - f"REDISPORT={config['redis']['port']}" - ] + f"MAXLAG={config['max_lag']}" + ] + common_env } } # actor spec @@ -118,10 +122,8 @@ f"GROUP={config['async']['group']}", f"NUMEPISODES={config['num_episodes']}", f"NUMSTEPS={config['num_steps']}", - f"EVALSCH={config['eval_schedule']}", - f"REDISHOST={config['redis']['host']}", - f"REDISPORT={config['redis']['port']}" - ] + f"EVALSCH={config['eval_schedule']}" + ] + common_env docker_compose_manifest["services"][str_id] = actor_spec else: raise ValueError(f"mode must be 'sync' or 'async', got {mode}") diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 5cc39e2d6..542e2cfdd 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -4,8 +4,8 @@ job: cim scenario: cim mode: sync -num_episodes: 100 -eval_schedule: 10 +num_episodes: 5 +eval_schedule: 5 num_steps: -1 max_lag: 0 # If true, the roll-out experiences will be distributed amongst roll-out workers / actors @@ -15,8 +15,8 @@ rollout_experience_distribution: false sync: rollout_group: rollout rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 5 - min_finished_workers: 4 + num_rollout_workers: 3 + min_finished_workers: 2 max_extra_recv_tries: 2 extra_recv_timeout: 200 async: diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 2591fd1d2..66df5533a 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -22,8 +22,7 @@ rl_policy_func_index = getattr(module, "rl_policy_func_index") agent2policy = getattr(module, "agent2policy") rl_agents = [agent_id for agent_id, policy_id in agent2policy.items() if policy_id in rl_policy_func_index] -update_trigger = getattr(module, "update_trigger") -warmup = getattr(module, "warmup") +update_option = getattr(module, "update_option") post_collect = getattr(module, "post_collect", None) post_evaluate = getattr(module, "post_evaluate", None) post_update = getattr(module, "post_update", None) diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py index 995f8ecdd..caa5f505c 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -11,36 +11,29 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import log_dir, rl_policy_func_index, update_trigger, warmup +from general import log_dir, rl_policy_func_index, update_option def get_policy_manager(): train_mode = getenv("TRAINMODE", default="single-process") policy_dict = {name: func(rollout_only=False) for name, func in rl_policy_func_index.items()} if train_mode == "single-process": - return LocalPolicyManager( - policy_dict, - update_trigger=update_trigger, - warmup=warmup, - log_dir=log_dir - ) + return LocalPolicyManager(policy_dict, update_option, log_dir=log_dir) num_trainers = int(getenv("NUMTRAINERS", default=5)) if train_mode == "multi-process": return MultiProcessPolicyManager( policy_dict, + update_option, num_trainers, rl_policy_func_index, - update_trigger=update_trigger, - warmup=warmup, log_dir=log_dir ) if train_mode == "multi-node": return MultiNodePolicyManager( policy_dict, + update_option, getenv("TRAINGROUP", default="TRAIN"), num_trainers, - update_trigger=update_trigger, - warmup=warmup, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 diff --git a/examples/rl/workflows/policy_manager/trainer.py b/examples/rl/workflows/policy_manager/trainer.py index 7ce5cf6ce..623f7ddd7 100644 --- a/examples/rl/workflows/policy_manager/trainer.py +++ b/examples/rl/workflows/policy_manager/trainer.py @@ -11,7 +11,7 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import log_dir, rl_policy_func_index +from general import log_dir, rl_policy_func_index, update_option if __name__ == "__main__": @@ -23,6 +23,7 @@ getenv("TRAINGROUP", default="TRAIN"), int(trainer_id), rl_policy_func_index, + num_epochs={policy_name: opt.num_epochs for policy_name, opt in update_option.items()}, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index 4e78c68d4..9a331a075 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -8,7 +8,7 @@ import torch from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceBatch, ExperienceStore +from maro.rl.experience import ExperienceBatch, ExperienceMemory from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.model import DiscreteQNet @@ -70,9 +70,9 @@ def choose_action(self, states, explore: bool = True) -> Union[int, np.ndarray]: _, actions = q_for_all_actions.max(dim=1) actions = actions.cpu().numpy() - if self.exploration.action_space is None: - self.exploration.set_action_space(np.arange(q_for_all_actions.shape[1])) if explore: + if self.exploration.action_space is None: + self.exploration.set_action_space(np.arange(q_for_all_actions.shape[1])) actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py index 5984b0868..8a14a135e 100644 --- a/maro/rl/experience/__init__.py +++ b/maro/rl/experience/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .experience_store import ExperienceSet, ExperienceStore +from .memory import ExperienceSet, ExperienceMemory from .sampler import AbsSampler, ExperienceBatch, PrioritizedSampler, UniformSampler -__all__ = ["AbsSampler", "ExperienceBatch", "ExperienceSet", "ExperienceStore", "PrioritizedSampler", "UniformSampler"] +__all__ = ["AbsSampler", "ExperienceBatch", "ExperienceSet", "ExperienceMemory", "PrioritizedSampler", "UniformSampler"] diff --git a/maro/rl/experience/experience_store.py b/maro/rl/experience/memory.py similarity index 99% rename from maro/rl/experience/experience_store.py rename to maro/rl/experience/memory.py index 2f0da712e..ef13ef691 100644 --- a/maro/rl/experience/experience_store.py +++ b/maro/rl/experience/memory.py @@ -45,7 +45,7 @@ def extend(self, other): self.info += other.info -class ExperienceStore: +class ExperienceMemory: """Storage facility for simulation experiences. This implementation uses a dictionary of lists as the internal data structure. The objects for each key diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index 420783a06..53f57c125 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -7,7 +7,7 @@ import numpy as np -from .experience_store import ExperienceSet, ExperienceStore +from .memory import ExperienceSet, ExperienceMemory ExperienceBatch = namedtuple("ExperienceBatch", ["indexes", "data"]) @@ -16,41 +16,41 @@ class AbsSampler(ABC): """Sampler class. Args: - experience_store (ExperienceStore): experience manager the sampler is associated with. + experience_memory (ExperienceMemory): experience manager the sampler is associated with. """ - def __init__(self, experience_store: ExperienceStore): + def __init__(self, experience_memory: ExperienceMemory): super().__init__() - self.experience_memory = experience_store + self.experience_memory = experience_memory @abstractmethod def get(self) -> ExperienceBatch: """Sampling logic is defined here.""" raise NotImplementedError - def on_put(self, experience_set: ExperienceSet, indexes: List[int]): - """Callback to be executed after calling experience_store.put().""" + def on_new(self, experience_set: ExperienceSet, indexes: List[int]): + """Callback to be executed after calling experience_memory.put().""" pass def update(self, indexes: List[int], info: list): pass -class UniformSampler(ABC): +class UniformSampler(AbsSampler): """Uniform sampler class. Args: - experience_store (ExperienceStore): experience manager the sampler is associated with. + experience_memory (ExperienceMemory): experience manager the sampler is associated with. batch_size (int): Batch size for the default uniform sampling. This can be set to -1 if the required batch size is the number of items stored. To get the whole data, set this to -1 and ``replace`` to False. Defaults to 32. replace (bool): A flag indicating whether the default uniform sampling is with replacement or without. Defaults to True. """ - def __init__(self, experience_store: ExperienceStore, batch_size: int = 32, replace: bool = True): + def __init__(self, experience_memory: ExperienceMemory, batch_size: int = 32, replace: bool = True): if batch_size <= 0 and batch_size != -1: raise ValueError("batch_size must be -1 or a positive integer") - super().__init__() - self.experience_memory = experience_store + super().__init__(experience_memory) + self.experience_memory = experience_memory self.batch_size = batch_size self.replace = replace @@ -80,7 +80,7 @@ class PrioritizedSampler(AbsSampler): The rank-based variant is not implemented here. Args: - experience_store (ExperienceStore): experience manager the sampler is associated with. + experience_memory (ExperienceMemory): experience manager the sampler is associated with. batch_size (int): mini-batch size. Defaults to 32. alpha (float): Prioritization strength. Sampling probabilities are calculated according to P = p_i^alpha / sum(p_k^alpha). Defaults to 0.6. @@ -92,7 +92,7 @@ class PrioritizedSampler(AbsSampler): """ def __init__( self, - experience_store: ExperienceStore, + experience_memory: ExperienceMemory, batch_size: int = 32, alpha: float = 0.6, beta: float = 0.4, @@ -100,7 +100,7 @@ def __init__( ): if beta > 1.0: raise ValueError("beta should be between 0.0 and 1.0") - super().__init__(experience_store) + super().__init__(experience_memory) self._sum_tree = np.zeros(2 * self.experience_memory.capacity - 1) self.batch_size = batch_size self.alpha = alpha @@ -113,7 +113,7 @@ def total(self): """Return the sum of priorities over all experiences.""" return self._sum_tree[0] - def on_put(self, experience_set: ExperienceSet, indexes: List[int]): + def on_new(self, experience_set: ExperienceSet, indexes: List[int]): """Set the priorities of newly added experiences to the maximum value.""" self.update(indexes, [self._max_priority] * len(indexes)) diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index 779ef3309..70a069d05 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -37,8 +37,8 @@ def get_batch(self, env: AbsEnvWrapper): names = set() exp_by_agent = env.get_experiences() for agent_id, exp in exp_by_agent.items(): - if hasattr(self.policy[agent_id], "store"): - self.policy[agent_id].store(exp) + if hasattr(self.policy[agent_id], "memorize"): + self.policy[agent_id].memorize(exp) names.add(self.agent2policy[agent_id]) return {name: self.policy_dict[name].sampler.get() for name in names} @@ -46,7 +46,7 @@ def get_batch(self, env: AbsEnvWrapper): def set_policy_states(self, policy_state_dict: dict): """Update policy states.""" for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].set_state(policy_state) + self.policy_dict[policy_id].algorithm.set_state(policy_state) def exploration_step(self): for policy in self.policy_dict.values(): diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 4f4774c43..4d9f18021 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -491,7 +491,7 @@ def _handle_worker_result(self, msg, ep, segment, version, combined_exp): return for policy_name, exp in msg.body[MsgKey.EXPERIENCES].items(): - combined_exp[policy_name].extend(exp) + combined_exp[policy_name].extend(exp.data) # The message is what we expect if msg.body[MsgKey.EPISODE] == ep and msg.body[MsgKey.SEGMENT] == segment: diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index e41e7dbc8..0ec2c3451 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceSet, ExperienceStore, UniformSampler +from maro.rl.experience import ExperienceSet, ExperienceMemory, UniformSampler class AbsPolicy(ABC): @@ -50,7 +50,7 @@ def __init__( ): super().__init__() self.algorithm = algorithm - self.experience_memory = ExperienceStore(memory_capacity, random_overwrite=random_overwrite) + self.experience_memory = ExperienceMemory(memory_capacity, random_overwrite=random_overwrite) self.sampler = sampler_cls(self.experience_memory, **sampler_kwargs) self.exploring = False @@ -58,12 +58,12 @@ def __init__( def choose_action(self, state): return self.algorithm.choose_action(state, explore=self.exploring) - def store_experiences(self, exp: ExperienceSet) -> bool: + def memorize(self, exp: ExperienceSet) -> bool: """ Store incoming experiences and update if necessary. """ indexes = self.experience_memory.put(exp) - self.sampler.on_put(exp, indexes) + self.sampler.on_new(exp, indexes) # print( # f"exp mem size = {self.experience_memory.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" # ) @@ -81,10 +81,10 @@ def update(self): self.sampler.update(indexes, loss_info) def explore(self): - self.exploring = False + self.exploring = True def exploit(self): - self.exploring = True + self.exploring = False def exploration_step(self): if self.algorithm.exploration: diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 1309765e2..9556e71c0 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -217,7 +217,7 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): for conn in self._manager_end.values(): result = conn.recv() for policy_name, policy_state in result["policy"].items(): - self.policy_dict[policy_name].set_state(policy_state) + self.policy_dict[policy_name].algorithm.set_state(policy_state) if updated: self._update_history.append(updated) @@ -259,9 +259,9 @@ class MultiNodePolicyManager(AbsPolicyManager): def __init__( self, policy_dict: Dict[str, CorePolicy], + update_option: Dict[str, PolicyUpdateOptions], group: str, num_trainers: int, - update_option: Dict[str, PolicyUpdateOptions], log_dir: str = getcwd(), proxy_kwargs: dict = {} ): @@ -286,7 +286,12 @@ def __init__( self._proxy.send( SessionMessage( MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, - body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} + body={ + MsgKey.POLICY_STATE: { + name: self.policy_dict[name].algorithm.get_state(inference=False) + for name in policy_names + } + } ) ) @@ -314,7 +319,7 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): for msg in self._proxy.receive(): if msg.tag == MsgTag.TRAIN_DONE: for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): - self.policy_dict[policy_name].set_state(policy_state) + self.policy_dict[policy_name].algorithm.set_state(policy_state) dones += 1 if dones == len(msg_body_by_dest): break diff --git a/maro/rl/policy/trainer.py b/maro/rl/policy/trainer.py index c3560c8aa..fe30f8020 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/policy/trainer.py @@ -42,7 +42,7 @@ def trainer_process( policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} logger = Logger("TRAINER", dump_folder=log_dir) for name, state in initial_policy_states.items(): - policy_dict[name].set_state(state) + policy_dict[name].algorithm.set_state(state) logger.info(f"{trainer_id} initialized policy {name}") while True: @@ -50,7 +50,7 @@ def trainer_process( if msg["type"] == "train": t0 = time.time() for name, exp in msg["experiences"].items(): - policy_dict[name].store(exp) + policy_dict[name].memorize(exp) for _ in range(num_epochs[name]): policy_dict[name].update() if reset_memory[name]: @@ -104,13 +104,13 @@ def trainer_node( if msg.tag == MsgTag.INIT_POLICY_STATE: for name, state in msg.body[MsgKey.POLICY_STATE].items(): policy_dict[name] = create_policy_func_dict[name]() - policy_dict[name].set_state(state) + policy_dict[name].algorithm.set_state(state) logger.info(f"{proxy.name} initialized policy {name}") proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) elif msg.tag == MsgTag.LEARN: t0 = time.time() for name, exp in msg.body[MsgKey.EXPERIENCES].items(): - policy_dict[name].store(exp) + policy_dict[name].memorize(exp) for _ in range(num_epochs[name]): policy_dict[name].update() if reset_memory[name]: @@ -132,7 +132,7 @@ def trainer_node( proxy.reply(msg, tag=MsgTag.UPDATE_INFO, body=msg_body) elif msg.tag == MsgTag.UPDATE_POLICY_STATE: for name, state in msg.body[MsgKey.POLICY_STATE].items(): - policy_dict[name].set_state(state) + policy_dict[name].algorithm.set_state(state) logger.info(f"{proxy.name} updated policy {name}") diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 63cae9f14..722e1f85d 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -153,7 +153,7 @@ "source": [ "import torch\n", "\n", - "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceStore, FullyConnectedBlock, OptimOption\n", + "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceMemory, FullyConnectedBlock, OptimOption\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", @@ -191,7 +191,7 @@ " \"critic\": OptimOption(optim_cls=\"rmsprop\", optim_params={\"lr\": 0.001})\n", " }\n", " },\n", - " \"experience_store\": {\n", + " \"experience_memory\": {\n", " \"capacity\": 10000\n", " },\n", " \"algorithm_config\": {\n", @@ -221,8 +221,8 @@ " actor = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"actor\"])\n", " critic = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"critic\"])\n", " ac_net = MyACNet({\"actor\": actor, \"critic\": critic}, optim_option=policy_config[\"model\"][\"optimization\"])\n", - " experience_store = ExperienceStore(policy_config[\"experience_store\"][\"capacity\"])\n", - " return ActorCritic(name, ac_net, experience_store, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" + " experience_memory = ExperienceMemory(policy_config[\"experience_memory\"][\"capacity\"])\n", + " return ActorCritic(name, ac_net, experience_memory, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" ] }, { From cad28721d18d8e39c6c3aad9b9857be0fec52fd9 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 9 Aug 2021 05:38:13 +0000 Subject: [PATCH 404/482] fixed some bugs --- examples/rl/cim/policies.py | 2 +- examples/rl/workflows/config.yml | 2 +- maro/rl/experience/sampler.py | 2 +- maro/rl/learning/agent_wrapper.py | 6 +- .../learning/synchronous/rollout_manager.py | 2 +- maro/rl/policy/policy.py | 3 - maro/rl/policy/policy_manager.py | 61 ++++++++++--------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index 94102bbba..69949997f 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -18,7 +18,7 @@ from env_wrapper import AGENT_IDS update_option = { - name: PolicyUpdateOptions(update_trigger=64, warmup=1024, num_epochs=10, reset_memory=False, data_parallel=False) + name: PolicyUpdateOptions(update_trigger=16, warmup=256, num_epochs=10, reset_memory=False, data_parallel=False) for name in AGENT_IDS } diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 542e2cfdd..fbd935236 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -17,7 +17,7 @@ sync: rollout_mode: multi-node # single-process, multi-process, multi-node num_rollout_workers: 3 min_finished_workers: 2 - max_extra_recv_tries: 2 + max_extra_recv_tries: 0 extra_recv_timeout: 200 async: group: async diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py index 53f57c125..052ae0df6 100644 --- a/maro/rl/experience/sampler.py +++ b/maro/rl/experience/sampler.py @@ -59,7 +59,7 @@ def get(self) -> ExperienceBatch: batch_size = self.experience_memory.size else: batch_size = self.batch_size - batch_size = self.experience_memory.size if self.batch_size == -1 else self.batch_size + indexes = np.random.choice(self.experience_memory.size, size=batch_size, replace=self.replace) return ExperienceBatch( indexes=indexes, diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index 70a069d05..c5be26fc8 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -28,7 +28,7 @@ def choose_action(self, state: dict) -> dict: """Generate an action based on the given state. Args: - state (dict): Dicitionary of agents' states based on which action decisions will be made. + state (dict): Dictionary of agents' states based on which action decisions will be made. """ return {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} @@ -41,7 +41,9 @@ def get_batch(self, env: AbsEnvWrapper): self.policy[agent_id].memorize(exp) names.add(self.agent2policy[agent_id]) - return {name: self.policy_dict[name].sampler.get() for name in names} + ret = {name: self.policy_dict[name].sampler.get() for name in names} + print({name: batch.data.size for name, batch in ret.items()}) + return ret def set_policy_states(self, policy_state_dict: dict): """Update policy states.""" diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 4d9f18021..15bb1d100 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -462,8 +462,8 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): else: tracker = self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) if tracker is not None: - num_finishes += 1 trackers.append(tracker) + num_finishes += 1 if num_finishes == self._num_workers: break diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 0ec2c3451..cca003a1a 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -64,9 +64,6 @@ def memorize(self, exp: ExperienceSet) -> bool: """ indexes = self.experience_memory.put(exp) self.sampler.on_new(exp, indexes) - # print( - # f"exp mem size = {self.experience_memory.size}, incoming: {exp.size}, new exp = {self._new_exp_counter}" - # ) def get_batch(self): self.sampler.get() diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/policy/policy_manager.py index 9556e71c0..a1e0490cc 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/policy/policy_manager.py @@ -208,20 +208,21 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): exp_to_send[policy_name] = self._exp_cache.pop(policy_name) updated.add(policy_name) - for trainer_id, conn in self._manager_end.items(): - conn.send({ - "type": "train", - "experiences": {name: exp_to_send[name] for name in self._trainer2policies[trainer_id]} - }) - - for conn in self._manager_end.values(): - result = conn.recv() - for policy_name, policy_state in result["policy"].items(): - self.policy_dict[policy_name].algorithm.set_state(policy_state) + if exp_to_send: + for trainer_id, conn in self._manager_end.items(): + conn.send({ + "type": "train", + "experiences": {name: exp_to_send[name] for name in self._trainer2policies[trainer_id]} + }) + + for conn in self._manager_end.values(): + result = conn.recv() + for policy_name, policy_state in result["policy"].items(): + self.policy_dict[policy_name].algorithm.set_state(policy_state) - if updated: - self._update_history.append(updated) - self._logger.info(f"Updated policies {updated}") + if updated: + self._update_history.append(updated) + self._logger.info(f"Updated policies {updated}") def exit(self): """Tell the trainer processes to exit.""" @@ -307,24 +308,24 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): exp_to_send[policy_name] = self._exp_cache.pop(policy_name) updated.add(policy_name) - msg_body_by_dest = defaultdict(dict) - for policy_name, exp in exp_to_send.items(): - trainer_id = self._policy2trainer[policy_name] - if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp - - dones = 0 - self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())) - for msg in self._proxy.receive(): - if msg.tag == MsgTag.TRAIN_DONE: - for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): - self.policy_dict[policy_name].algorithm.set_state(policy_state) - dones += 1 - if dones == len(msg_body_by_dest): - break + if exp_to_send: + msg_body_by_dest = defaultdict(dict) + for policy_name, exp in exp_to_send.items(): + trainer_id = self._policy2trainer[policy_name] + if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp + + dones = 0 + self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())) + for msg in self._proxy.receive(): + if msg.tag == MsgTag.TRAIN_DONE: + for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): + self.policy_dict[policy_name].algorithm.set_state(policy_state) + dones += 1 + if dones == len(msg_body_by_dest): + break - if updated: self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") From 3ba96d4bd293da3d1a120adc979a0faddaf30d58 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 11 Aug 2021 01:11:14 +0000 Subject: [PATCH 405/482] refactoring in progress --- examples/rl/cim/ac.py | 106 +++++++++ examples/rl/cim/dqn.py | 105 +++++++++ examples/rl/cim/policy_index.py | 20 ++ examples/rl/vm_scheduling/README.md | 10 + examples/rl/vm_scheduling/__init__.py | 11 + examples/rl/vm_scheduling/ac.py | 103 +++++++++ examples/rl/vm_scheduling/callbacks.py | 87 ++++++++ examples/rl/vm_scheduling/dqn.py | 124 +++++++++++ examples/rl/vm_scheduling/env_wrapper.py | 191 ++++++++++++++++ examples/rl/vm_scheduling/policy_index.py | 19 ++ maro/rl/algorithms/abs_algorithm.py | 64 ++++-- maro/rl/algorithms/dqn.py | 194 ++++++++++++---- maro/rl/experience/__init__.py | 7 - maro/rl/experience/sampler.py | 178 --------------- maro/rl/learning/agent_wrapper.py | 12 +- maro/rl/learning/env_wrapper.py | 2 +- .../rl/{policy => learning}/policy_manager.py | 207 +++++++++--------- maro/rl/learning/simple_learner.py | 4 +- .../learning/synchronous/rollout_manager.py | 4 +- maro/rl/{policy => learning}/trainer.py | 70 +----- maro/rl/model/core_model.py | 30 +-- maro/rl/policy/__init__.py | 13 -- maro/rl/policy/policy.py | 88 -------- maro/rl/replay/__init__.py | 6 + .../memory.py => replay/replay_memory.py} | 3 +- maro/rl/utils/message_enums.py | 5 - 26 files changed, 1114 insertions(+), 549 deletions(-) create mode 100644 examples/rl/cim/ac.py create mode 100644 examples/rl/cim/dqn.py create mode 100644 examples/rl/cim/policy_index.py create mode 100644 examples/rl/vm_scheduling/README.md create mode 100644 examples/rl/vm_scheduling/__init__.py create mode 100644 examples/rl/vm_scheduling/ac.py create mode 100644 examples/rl/vm_scheduling/callbacks.py create mode 100644 examples/rl/vm_scheduling/dqn.py create mode 100644 examples/rl/vm_scheduling/env_wrapper.py create mode 100644 examples/rl/vm_scheduling/policy_index.py delete mode 100644 maro/rl/experience/__init__.py delete mode 100644 maro/rl/experience/sampler.py rename maro/rl/{policy => learning}/policy_manager.py (61%) rename maro/rl/{policy => learning}/trainer.py (50%) delete mode 100644 maro/rl/policy/__init__.py delete mode 100644 maro/rl/policy/policy.py create mode 100644 maro/rl/replay/__init__.py rename maro/rl/{experience/memory.py => replay/replay_memory.py} (99%) diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py new file mode 100644 index 000000000..52f52cec9 --- /dev/null +++ b/examples/rl/cim/ac.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +import numpy as np +import torch + +from maro.rl.experience import ReplayMemory, UniformSampler +from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption +from maro.rl.policy.algorithms import ActorCritic, ActorCriticConfig + +cim_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, cim_path) +from env_wrapper import STATE_DIM, env_config + +config = { + "model": { + "network": { + "actor": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64], + "output_dim": env_config["wrapper"]["num_actions"], + "activation": "tanh", + "softmax": True, + "batch_norm": False, + "head": True + }, + "critic": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64], + "output_dim": 1, + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "head": True + } + }, + "optimization": { + "actor": { + "optim_cls": "adam", + "optim_params": {"lr": 0.001} + }, + "critic": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.001} + } + } + }, + "algorithm": { + "reward_discount": .0, + "critic_loss_cls": "smooth_l1", + "train_epochs": 10, + "critic_loss_coeff": 0.1, + "entropy_coeff": 0.01, + # "clip_ratio": 0.8 # for PPO + }, + "replay_memory": { + "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, + "update": {"capacity": 100000, "overwrite_type": "rolling"} + }, + "sampler": { + "rollout": {"batch_size": -1, "replace": False}, + "update": {"batch_size": 128, "replace": True} + } +} + + +def get_ac_policy(mode="update"): + assert mode in {"inference", "update", "inference-update"} + class MyACNET(DiscreteACNet): + def forward(self, states, actor: bool = True, critic: bool = True): + states = torch.from_numpy(np.asarray(states)) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + + states = states.to(self.device) + return ( + self.component["actor"](states) if actor else None, + self.component["critic"](states) if critic else None + ) + + ac_net = MyACNET( + component={ + "actor": FullyConnectedBlock(**config["model"]["network"]["actor"]), + "critic": FullyConnectedBlock(**config["model"]["network"]["critic"]) + }, + optim_option={ + "actor": OptimOption(**config["model"]["optimization"]["actor"]), + "critic": OptimOption(**config["model"]["optimization"]["critic"]) + } if mode != "inference" else None + ) + + if mode == "update": + exp_store = ReplayMemory(**config["replay_memory"]["update"]) + experience_sampler_kwargs = config["sampler"]["update"] + else: + exp_store = ReplayMemory(**config["replay_memory"]["rollout" if mode == "inference" else "update"]) + experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] + + return ActorCritic( + ac_net, ActorCriticConfig(**config["algorithm"]), exp_store, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs=experience_sampler_kwargs + ) diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py new file mode 100644 index 000000000..2047226e0 --- /dev/null +++ b/examples/rl/cim/dqn.py @@ -0,0 +1,105 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +import numpy as np +import torch +import torch.nn as nn + +from maro.rl.experience import ReplayMemory, UniformSampler +from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler +from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption +from maro.rl.policy.algorithms import DQN, DQNConfig + +cim_path = os.path.dirname(os.path.realpath(__file__)) +if cim_path not in sys.path: + sys.path.insert(0, cim_path) +from env_wrapper import STATE_DIM, env_config + +config = { + "model": { + "network": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64, 32], + "output_dim": env_config["wrapper"]["num_actions"], + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 + }, + "optimization": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.05} + } + }, + "algorithm": { + "reward_discount": .0, + "update_target_every": 5, + "train_epochs": 10, + "soft_update_coeff": 0.1, + "double": False + }, + "replay_memory": { + "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, + "update": {"capacity": 100000, "overwrite_type": "rolling"} + }, + "sampler": { + "rollout": { + "batch_size": -1, + "replace": False + }, + "update": { + "batch_size": 128, + "replace": True + } + }, + "exploration": { + "last_ep": 10, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(5, 0.32)] + } +} + + +class QNet(DiscreteQNet): + def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): + super().__init__(component, optim_option=optim_option, device=device) + + def forward(self, states): + states = torch.from_numpy(np.asarray(states)).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) + return self.component(states) + + +def get_dqn_policy(mode="update"): + assert mode in {"inference", "update", "inference-update"} + qnet = QNet( + FullyConnectedBlock(**config["model"]["network"]), + optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None + ) + if mode == "update": + exp_store = ReplayMemory(**config["replay_memory"]["update"]) + exploration = None + exp_sampler_kwargs = config["sampler"]["update"] + else: + exploration = EpsilonGreedyExploration() + exploration.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **config["exploration"] + ) + exp_store = ReplayMemory(**config["replay_memory"]["rollout" if mode == "inference" else "update"]) + exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] + + return DQN( + qnet, DQNConfig(**config["algorithm"]), exp_store, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs=exp_sampler_kwargs, + exploration=exploration + ) diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py new file mode 100644 index 000000000..3eb24c645 --- /dev/null +++ b/examples/rl/cim/policy_index.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + + +cim_path = os.path.dirname(os.path.realpath(__file__)) +if cim_path not in sys.path: + sys.path.insert(0, cim_path) +from ac import get_ac_policy +from dqn import get_dqn_policy +from env_wrapper import AGENT_IDS + +update_trigger = {name: 128 for name in AGENT_IDS} +warmup = {name: 1 for name in AGENT_IDS} + +# use agent IDs as policy names since each agent uses a separate policy +rl_policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} +agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/vm_scheduling/README.md b/examples/rl/vm_scheduling/README.md new file mode 100644 index 000000000..a428a5c33 --- /dev/null +++ b/examples/rl/vm_scheduling/README.md @@ -0,0 +1,10 @@ +# Virtual Machine Scheduling + +Virtual Machine (VM) scheduling is a scenario where reinforcement learning (RL) can help the virtual machine allocator allocate compute resources intelligently. In this folder you can find: +* ``env_wrapper.py``, which contains a function to generate an environment wrapper to interact +with our "agent" (see below); +* ``agent_wrapper.py``, which contains a function to generate an agent wrapper to interact +with the environment wrapper; +* ``policy_index``, which maps policy names to functions that create them; the functions to create DQN and Actor-Critic policies are defined in ``dqn.py`` and ``ac.py``, respectively. + +The code for the actual learning workflows (e.g., learner, roll-out worker and trainer) can be found under ``examples/rl/workflows``. The reason for putting it in a separate folder is that these workflows apply to any scenario, so long as the necessary component generators, such as the ones listed above, are provided. See ``README`` under ``examples/rl`` for details. We recommend that you follow this example to write your own scenarios. \ No newline at end of file diff --git a/examples/rl/vm_scheduling/__init__.py b/examples/rl/vm_scheduling/__init__.py new file mode 100644 index 000000000..3055ab873 --- /dev/null +++ b/examples/rl/vm_scheduling/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .callbacks import post_collect, post_evaluate +from .env_wrapper import get_env_wrapper, get_eval_env_wrapper +from .policy_index import agent2policy, rl_policy_func_index, update_trigger, warmup + +__all__ = [ + "agent2policy", "post_collect", "post_evaluate", "get_env_wrapper", "get_eval_env_wrapper", + "rl_policy_func_index", "update_trigger", "warmup" +] \ No newline at end of file diff --git a/examples/rl/vm_scheduling/ac.py b/examples/rl/vm_scheduling/ac.py new file mode 100644 index 000000000..b3f1821c6 --- /dev/null +++ b/examples/rl/vm_scheduling/ac.py @@ -0,0 +1,103 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +import numpy as np +import torch + +from maro.rl.experience import ReplayMemory, UniformSampler +from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption +from maro.rl.policy.algorithms import ActorCritic, ActorCriticConfig + +vm_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, vm_path) +from env_wrapper import NUM_PMS, STATE_DIM + +config = { + "model": { + "network": { + "actor": { + "input_dim": STATE_DIM, + "output_dim": NUM_PMS + 1, # action could be any PM or postponement, hence the plus 1 + "hidden_dims": [64, 32, 32], + "activation": "leaky_relu", + "softmax": True, + "batch_norm": False, + "head": True + }, + "critic": { + "input_dim": STATE_DIM, + "output_dim": 1, + "hidden_dims": [256, 128, 64], + "activation": "leaky_relu", + "softmax": False, + "batch_norm": False, + "head": True + } + }, + "optimization": { + "actor": { + "optim_cls": "adam", + "optim_params": {"lr": 0.0001} + }, + "critic": { + "optim_cls": "sgd", + "optim_params": {"lr": 0.001} + } + } + }, + "algorithm": { + "reward_discount": 0.9, + "train_epochs": 100, + "critic_loss_cls": "mse", + "critic_loss_coeff": 0.1 + }, + "replay_memory": { + "rollout": {"capacity": 10000, "overwrite_type": "rolling"}, + "update": {"capacity": 50000, "overwrite_type": "rolling"} + }, + "sampler": { + "rollout": {"batch_size": -1, "replace": False}, + "update": {"batch_size": 128, "replace": True} + } +} + + +def get_ac_policy(mode="update"): + class MyACNet(DiscreteACNet): + def forward(self, states, actor: bool = True, critic: bool = True): + if isinstance(states, dict): + states = [states] + inputs = torch.from_numpy(np.asarray([st["model"] for st in states])).to(self.device) + masks = torch.from_numpy(np.asarray([st["mask"] for st in states])).to(self.device) + if len(inputs.shape) == 1: + inputs = inputs.unsqueeze(dim=0) + return ( + self.component["actor"](inputs) * masks if actor else None, + self.component["critic"](inputs) if critic else None + ) + + ac_net = MyACNet( + component={ + "actor": FullyConnectedBlock(**config["model"]["network"]["actor"]), + "critic": FullyConnectedBlock(**config["model"]["network"]["critic"]) + }, + optim_option={ + "actor": OptimOption(**config["model"]["optimization"]["actor"]), + "critic": OptimOption(**config["model"]["optimization"]["critic"]) + } if mode != "inference" else None + ) + if mode == "update": + exp_store = ReplayMemory(**config["replay_memory"]["update"]) + exp_sampler_kwargs = config["sampler"]["update"] + else: + exp_store = ReplayMemory(**config["replay_memory"]["rollout" if mode == "inference" else "update"]) + exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] + + return ActorCritic( + ac_net, ActorCriticConfig(**config["algorithm"]), exp_store, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs=exp_sampler_kwargs + ) diff --git a/examples/rl/vm_scheduling/callbacks.py b/examples/rl/vm_scheduling/callbacks.py new file mode 100644 index 000000000..f82291374 --- /dev/null +++ b/examples/rl/vm_scheduling/callbacks.py @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from os import makedirs +from os.path import dirname, join, realpath + +import matplotlib.pyplot as plt + +from maro.utils import Logger + +timestamp = str(time.time()) + +log_dir = join(dirname(realpath(__file__)), "log", timestamp) +makedirs(log_dir, exist_ok=True) + +plt_path = join(dirname(realpath(__file__)), "plots", timestamp) +makedirs(plt_path, exist_ok=True) + + +simulation_logger = Logger("SIMUALTION", dump_folder=log_dir) + +def post_collect(trackers, ep, segment): + # print the env metric from each rollout worker + for tracker in trackers: + simulation_logger.info(f"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}") + + # print the average env metric + if len(trackers) > 1: + metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) + avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} + simulation_logger.info(f"average env metric (episode {ep}, segment {segment}): {avg_metric}") + +def post_evaluate(trackers, ep): + # print the env metric from each rollout worker + for tracker in trackers: + simulation_logger.info(f"env summary (evaluation episode {ep}): {tracker['env_metric']}") + + # print the average env metric + if len(trackers) > 1: + metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) + avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} + simulation_logger.info(f"average env metric (evaluation episode {ep}): {avg_metric}") + + for i, tracker in enumerate(trackers): + core_requirement = tracker["vm_core_requirement"] + action_sequence = tracker["action_sequence"] + # plot action sequence + fig = plt.figure(figsize=(40, 32)) + ax = fig.add_subplot(1, 1, 1) + ax.plot(action_sequence) + fig.savefig(f"{plt_path}/action_sequence_{ep}") + plt.cla() + plt.close("all") + + # plot with legal action mask + fig = plt.figure(figsize=(40, 32)) + for idx, key in enumerate(core_requirement.keys()): + ax = fig.add_subplot(len(core_requirement.keys()), 1, idx + 1) + for i in range(len(core_requirement[key])): + if i == 0: + ax.plot(core_requirement[key][i][0] * core_requirement[key][i][1], label=str(key)) + ax.legend() + else: + ax.plot(core_requirement[key][i][0] * core_requirement[key][i][1]) + + fig.savefig(f"{plt_path}/values_with_legal_action_{ep}") + + plt.cla() + plt.close("all") + + # plot without legal actin mask + fig = plt.figure(figsize=(40, 32)) + + for idx, key in enumerate(core_requirement.keys()): + ax = fig.add_subplot(len(core_requirement.keys()), 1, idx + 1) + for i in range(len(core_requirement[key])): + if i == 0: + ax.plot(core_requirement[key][i][0], label=str(key)) + ax.legend() + else: + ax.plot(core_requirement[key][i][0]) + + fig.savefig(f"{plt_path}/values_without_legal_action_{ep}") + + plt.cla() + plt.close("all") diff --git a/examples/rl/vm_scheduling/dqn.py b/examples/rl/vm_scheduling/dqn.py new file mode 100644 index 000000000..8b588601c --- /dev/null +++ b/examples/rl/vm_scheduling/dqn.py @@ -0,0 +1,124 @@ + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +import numpy as np +import torch + +from maro.rl.experience import ReplayMemory, UniformSampler +from maro.rl.exploration import DiscreteSpaceExploration, MultiPhaseLinearExplorationScheduler +from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption +from maro.rl.policy.algorithms import DQN, DQNConfig + +vm_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, vm_path) +from env_wrapper import NUM_PMS, STATE_DIM + +config = { + "model": { + "network": { + "input_dim": STATE_DIM, + "hidden_dims": [64, 128, 256], + "output_dim": NUM_PMS + 1, # action could be any PM or postponement, hence the plus 1 + "activation": "leaky_relu", + "softmax": False, + "batch_norm": False, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 + }, + "optimization": { + "optim_cls": "sgd", + "optim_params": {"lr": 0.0005}, + "scheduler_cls": "cosine_annealing_warm_restarts", + "scheduler_params": {"T_0": 500, "T_mult": 2} + } + }, + "algorithm": { + "reward_discount": 0.9, + "update_target_every": 5, + "train_epochs": 100, + "soft_update_coeff": 0.1, + "double": False + }, + "replay_memory": { + "rollout": {"capacity": 10000, "overwrite_type": "rolling"}, + "update": {"capacity": 50000, "overwrite_type": "rolling"} + }, + "sampler": { + "rollout": {"batch_size": -1, "replace": False}, + "update": {"batch_size": 256, "replace": True} + }, + "exploration": { + "last_ep": 400, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(100, 0.32)] + } +} + + +class MyQNet(DiscreteQNet): + def __init__(self, component, optim_option, device: str = None): + super().__init__(component, optim_option=optim_option, device=device) + for mdl in self.modules(): + if isinstance(mdl, torch.nn.Linear): + torch.nn.init.xavier_uniform_(mdl.weight, gain=torch.nn.init.calculate_gain('leaky_relu')) + + def forward(self, states): + if isinstance(states, dict): + states = [states] + inputs = torch.from_numpy(np.asarray([st["model"] for st in states])).to(self.device) + masks = torch.from_numpy(np.asarray([st["mask"] for st in states])).to(self.device) + if len(inputs.shape) == 1: + inputs = inputs.unsqueeze(dim=0) + q_for_all_actions = self.component(inputs) + return q_for_all_actions + (masks - 1) * 1e8 + + +class MaskedEpsilonGreedy(DiscreteSpaceExploration): + def __init__(self, epsilon: float = .0): + super().__init__() + self.epsilon = epsilon + + def __call__(self, action, state): + if isinstance(state, dict): + state = [state] + mask = [st["mask"] for st in state] + return np.array([ + act if np.random.random() > self.epsilon else np.random.choice(np.where(mk == 1)[0]) + for act, mk in zip(action, mask) + ]) + + +def get_dqn_policy(mode="update"): + assert mode in {"inference", "update", "inference-update"} + q_net = MyQNet( + FullyConnectedBlock(**config["model"]["network"]), + optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None + ) + + if mode == "update": + exp_store = ReplayMemory(**config["replay_memory"]["update"]) + exploration = None + exp_sampler_kwargs = config["sampler"]["update"] + else: + exp_store = ReplayMemory(**config["replay_memory"]["rollout"]) + exploration = MaskedEpsilonGreedy() + exploration.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **config["exploration"] + ) + exp_store = ReplayMemory(**config["replay_memory"]["rollout" if mode == "inference" else "update"]) + exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] + + return DQN( + q_net, DQNConfig(**config["algorithm"]), exp_store, + experience_sampler_cls=UniformSampler, + experience_sampler_kwargs=exp_sampler_kwargs, + exploration=exploration + ) diff --git a/examples/rl/vm_scheduling/env_wrapper.py b/examples/rl/vm_scheduling/env_wrapper.py new file mode 100644 index 000000000..0d3109f79 --- /dev/null +++ b/examples/rl/vm_scheduling/env_wrapper.py @@ -0,0 +1,191 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import numpy as np + +from maro.rl.learning import AbsEnvWrapper, Transition +from maro.simulator import Env +from maro.simulator.scenarios.vm_scheduling import AllocateAction, PostponeAction + +def post_step(env: Env, tracker: dict, transition: Transition): + tracker["env_metric"] = env.metrics + if "vm_cpu_cores_requirement" not in tracker: + tracker["vm_core_requirement"] = [] + if "action_sequence" not in tracker: + tracker["action_sequence"] = [] + + tracker["vm_core_requirement"].append([transition.action["AGENT"], transition.state["AGENT"]["mask"]]) + tracker["action_sequence"].append(transition.action["AGENT"]) + + +class VMEnvWrapper(AbsEnvWrapper): + def __init__( + self, + env: Env, + pm_attributes: list, + vm_attributes: list, + alpha: float, + beta: float, + pm_window_size: int = 1, + gamma: float = 0.0, + reward_eval_delay: int = 0 + ): + super().__init__(env, reward_eval_delay=reward_eval_delay, replay_agent_ids=["AGENT"], post_step=post_step) + self._pm_attributes = pm_attributes + self._vm_attributes = vm_attributes + self._st = 0 + self._pm_window_size = pm_window_size + # adjust the ratio of the success allocation and the total income when computing the reward + self._alpha = alpha + self._beta = beta + self._gamma = gamma # reward discount + self._num_pms = self.env._business_engine._pm_amount # the number of pms + self._durations = self.env._business_engine._max_tick + self._pm_state_history = np.zeros((pm_window_size - 1, self._num_pms, 2)) + self._legal_pm_mask = None + self._state_dim = 2 * self._num_pms * pm_window_size + 4 + + @property + def state_dim(self): + return self._state_dim + + @property + def num_pms(self): + return self._num_pms + + def get_state(self, tick=None): + pm_state, vm_state = self._get_pm_state(), self._get_vm_state() + # get the legal number of PM. + legal_pm_mask = np.zeros(self._num_pms + 1) + if len(self._event.valid_pms) <= 0: + # no pm available + legal_pm_mask[self._num_pms] = 1 + else: + legal_pm_mask[self._num_pms] = 1 + remain_cpu_dict = dict() + for pm in self._event.valid_pms: + # if two pm has same remaining cpu, only choose the one which has smaller id + if pm_state[-1, pm, 0] not in remain_cpu_dict: + remain_cpu_dict[pm_state[-1, pm, 0]] = 1 + legal_pm_mask[pm] = 1 + else: + legal_pm_mask[pm] = 0 + + self._legal_pm_mask = legal_pm_mask + return {"AGENT": {"model": np.concatenate((pm_state.flatten(), vm_state.flatten())), "mask": legal_pm_mask}} + + def to_env_action(self, action_info): + action_info = action_info["AGENT"] + model_action = action_info[0] if isinstance(action_info, tuple) else action_info + if model_action == self._num_pms: + return PostponeAction(vm_id=self._event.vm_id, postpone_step=1) + else: + return AllocateAction(vm_id=self._event.vm_id, pm_id=model_action) + + def get_reward(self, actions, tick=None): + if isinstance(actions, PostponeAction): # postponement + if np.sum(self._legal_pm_mask) != 1: + reward = -0.1 * self._alpha + 0.0 * self._beta + else: + reward = 0.0 * self._alpha + 0.0 * self._beta + elif self._event: + vm_unit_price = self.env._business_engine._get_unit_price( + self._event.vm_cpu_cores_requirement, self._event.vm_memory_requirement + ) + reward = ( + 1.0 * self._alpha + self._beta * vm_unit_price * + min(self._durations - self._event.frame_index, self._event.remaining_buffer_time) + ) + else: + reward = .0 + return {"AGENT": np.float32(reward)} + + def _get_pm_state(self): + total_pm_info = self.env.snapshot_list["pms"][self.env.frame_index::self._pm_attributes] + total_pm_info = total_pm_info.reshape(self._num_pms, len(self._pm_attributes)) + + # normalize the attributes of pms' cpu and memory + self._max_cpu_capacity = np.max(total_pm_info[:, 0]) + self._max_memory_capacity = np.max(total_pm_info[:, 1]) + total_pm_info[:, 2] /= self._max_cpu_capacity + total_pm_info[:, 3] /= self._max_memory_capacity + + # get the remaining cpu and memory of the pms + remain_cpu = (1 - total_pm_info[:, 2]).reshape(1, self._num_pms, 1) + remain_memory = (1 - total_pm_info[:, 3]).reshape(1, self._num_pms, 1) + + # get the pms' information + total_pm_info = np.concatenate((remain_cpu, remain_memory), axis=2) # (1, num_pms, 2) + + # get the sequence pms' information + self._pm_state_history = np.concatenate((self._pm_state_history, total_pm_info), axis=0) + return self._pm_state_history[-self._pm_window_size:, :, :].astype(np.float32) # (win_size, num_pms, 2) + + def _get_vm_state(self): + vm_info = np.array([ + self._event.vm_cpu_cores_requirement / self._max_cpu_capacity, + self._event.vm_memory_requirement / self._max_memory_capacity, + (self._durations - self.env.tick) * 1.0 / 200, # TODO: CHANGE 200 TO SOMETHING CONFIGURABLE + self.env._business_engine._get_unit_price( + self._event.vm_cpu_cores_requirement, self._event.vm_memory_requirement + ) + ], dtype=np.float32) + return vm_info + + +env_config = { + "basic": { + "scenario": "vm_scheduling", + "topology": "azure.2019.10k", + "start_tick": 0, + "durations": 300, # 8638 + "snapshot_resolution": 1 + }, + "wrapper": { + "pm_attributes": ["cpu_cores_capacity", "memory_capacity", "cpu_cores_allocated", "memory_allocated"], + "vm_attributes": ["cpu_cores_requirement", "memory_requirement", "lifetime", "remain_time", "total_income"], + "alpha": 0.0, + "beta": 1.0, + "pm_window_size": 1, + "gamma": 0.9 + }, + "seed": 666 +} + + +eval_env_config = { + "basic": { + "scenario": "vm_scheduling", + "topology": "azure.2019.10k.oversubscription", + "start_tick": 0, + "durations": 300, + "snapshot_resolution": 1 + }, + "wrapper": { + "pm_attributes": ["cpu_cores_capacity", "memory_capacity", "cpu_cores_allocated", "memory_allocated"], + "vm_attributes": ["cpu_cores_requirement", "memory_requirement", "lifetime", "remain_time", "total_income"], + "alpha": 0.0, + "beta": 1.0, + "pm_window_size": 1, + "gamma": 0.9 + }, + "seed": 1024 +} + + +def get_env_wrapper(replay_agent_ids=None): + env = Env(**env_config["basic"]) + env.set_seed(env_config["seed"]) + return VMEnvWrapper(env, **env_config["wrapper"]) + + +def get_eval_env_wrapper(): + eval_env = Env(**eval_env_config["basic"]) + eval_env.set_seed(eval_env_config["seed"]) + return VMEnvWrapper(eval_env, **eval_env_config["wrapper"]) + + +tmp_env_wrapper = get_env_wrapper() +STATE_DIM = tmp_env_wrapper.state_dim +NUM_PMS = tmp_env_wrapper.num_pms +del tmp_env_wrapper diff --git a/examples/rl/vm_scheduling/policy_index.py b/examples/rl/vm_scheduling/policy_index.py new file mode 100644 index 000000000..d9f3dad26 --- /dev/null +++ b/examples/rl/vm_scheduling/policy_index.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + + +cim_path = os.path.dirname(os.path.realpath(__file__)) +if cim_path not in sys.path: + sys.path.insert(0, cim_path) +from ac import get_ac_policy +from dqn import get_dqn_policy + +update_trigger = {"POLICY": 128} +warmup = {"POLICY": 1} + +# use agent IDs as policy names since each agent uses a separate policy +rl_policy_func_index = {"POLICY": get_ac_policy} +agent2policy = {"AGENT": "POLICY"} diff --git a/maro/rl/algorithms/abs_algorithm.py b/maro/rl/algorithms/abs_algorithm.py index 4b08ebb49..4f3b9a65d 100644 --- a/maro/rl/algorithms/abs_algorithm.py +++ b/maro/rl/algorithms/abs_algorithm.py @@ -3,47 +3,67 @@ from abc import ABC, abstractmethod -from maro.rl.experience import ExperienceSet +from maro.rl.replay import ExperienceSet, ReplayMemory, UniformSampler from maro.rl.exploration import AbsExploration -class AbsAlgorithm(ABC): - """Abstract algorithm. +class AbsPolicy(ABC): + """Abstract policy class.""" + def __init__(self): + super().__init__() + + @abstractmethod + def choose_action(self, state): + raise NotImplementedError + + +class NullPolicy(AbsPolicy): + """Dummy policy that does nothing. + + Note that the meaning of a "None" action may depend on the scenario. + """ + def choose_action(self, state): + return None + - Reinforcement learning (RL) algorithms should inherit from this. +class AbsCorePolicy(AbsPolicy): + """Policy that can update itself using simulation experiences. + + Reinforcement learning (RL) policies should inherit from this. Args: + replay_memory (ReplayMemory): An ``ReplayMemory`` instance for storing and retrieving experiences + generated by the policy. + experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to + ``UnifromSampler``. + experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. exploration (AbsExploration): Exploration strategy for generating exploratory actions. Defaults to None. """ def __init__(self, exploration: AbsExploration = None): super().__init__() self.exploration = exploration + self.exploring = True @abstractmethod - def choose_action(self, state, explore: bool = False): + def choose_action(self, state): raise NotImplementedError @abstractmethod - def apply(self, grad_dict: dict): - """Update the underlying parameters (i.e., network weights) with gradients.""" - pass + def learn(self): + """Policy update logic is implemented here. - @abstractmethod - def learn(self, experience_batch: ExperienceSet, inplace: bool = True) -> tuple: - """Update logic is implemented here.""" + This usually includes retrieving experiences as training samples from the experience manager and + updating the underlying models using these samples. + """ raise NotImplementedError @abstractmethod - def get_state(self, inference: bool = True): + def get_state(self): """Return the current state of the policy. - The implementation must go hand in hand with that of ``set_state``. For example, if a torch model + The implementation must be in correspondence with that of ``set_state``. For example, if a torch model is contained in the policy, ``get_state`` may include a call to ``state_dict()`` on the model, while ``set_state`` should accordingly include ``load_state_dict()``. - - Args: - learning (bool): If True, the returned state is for inference purpose only. This parameter - may be ignored for some algorithms. """ pass @@ -57,6 +77,16 @@ def set_state(self, policy_state): """ pass + def exploit(self): + self.exploring = False + + def explore(self): + self.exploring = True + + def exploration_step(self): + if self.exploration: + self.exploration.step() + def load(self, path: str): """Load the policy state from disk.""" pass diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/algorithms/dqn.py index 9a331a075..4e29a4cd1 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/algorithms/dqn.py @@ -1,21 +1,125 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import namedtuple from typing import List, Tuple, Union import numpy as np import torch -from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceBatch, ExperienceMemory +from maro.rl.algorithms.abs_algorithm import AbsPolicy +from maro.rl.replay import ExperienceSet, ReplayMemory from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.model import DiscreteQNet -DQNLossInfo = namedtuple("DQNLossInfo", ["td_error", "loss", "grad"]) +class PrioritizedSampler: + """Sampler for Prioritized Experience Replay (PER). -class DQN(AbsAlgorithm): + References: + https://arxiv.org/pdf/1511.05952.pdf + https://github.com/rlcode/per + + The implementation here is based on direct proportional prioritization (the first variant in the paper). + The rank-based variant is not implemented here. + + Args: + replay_memory (ReplayMemory): experience manager the sampler is associated with. + batch_size (int): mini-batch size. Defaults to 32. + alpha (float): Prioritization strength. Sampling probabilities are calculated according to + P = p_i^alpha / sum(p_k^alpha). Defaults to 0.6. + beta (float): Bias annealing strength using weighted importance sampling (IS) techniques. + IS weights are calculated according to (N * P)^(-beta), where P is the sampling probability. + This value of ``beta`` should not exceed 1.0, which corresponds to full annealing. Defaults to 0.4. + beta_step (float): The amount ``beta`` is incremented by after each get() call until it reaches 1.0. + Defaults to 0.001. + """ + def __init__( + self, + memory_capacity: int, + batch_size: int = 32, + alpha: float = 0.6, + beta: float = 0.4, + beta_step: float = 0.001 + ): + if beta > 1.0: + raise ValueError("beta should be between 0.0 and 1.0") + self.memory_capacity = memory_capacity + self._sum_tree = np.zeros(2 * self.memory_capacity - 1) + self.batch_size = batch_size + self.alpha = alpha + self.beta = beta + self.beta_step = beta_step + self.eps = 1e-7 + self._max_priority = 1e8 + + def total(self): + """Return the sum of priorities over all experiences.""" + return self._sum_tree[0] + + def on_new(self, experience_set: ExperienceSet, indexes: List[int]): + """Set the priorities of newly added experiences to the maximum value.""" + self.update(indexes, [self._max_priority] * len(indexes)) + + def update(self, indexes, td_errors): + """Update priority values at given indexes.""" + for idx, err in zip(indexes, td_errors): + priority = self._get_priority(err) + tree_idx = idx + self.memory_capacity - 1 + delta = priority - self._sum_tree[tree_idx] + self._sum_tree[tree_idx] = priority + self._update(tree_idx, delta) + + def get(self): + """Priority-based sampling.""" + indexes, priorities = [], [] + segment_len = self.total() / self.batch_size + for i in range(self.batch_size): + low, high = segment_len * i, segment_len * (i + 1) + sampled_val = np.random.uniform(low=low, high=high) + idx = self._get(0, sampled_val) + data_idx = idx - self.memory_capacity + 1 + indexes.append(data_idx) + priorities.append(self._sum_tree[idx]) + + self.beta = min(1., self.beta + self.beta_step) + sampling_probabilities = priorities / self.total() + is_weights = np.power(self.replay_memory.size * sampling_probabilities, -self.beta) + is_weights /= is_weights.max() + + return indexes, is_weights + return ExperienceSet( + states=[self.replay_memory.data["states"][idx] for idx in indexes], + actions=[self.replay_memory.data["actions"][idx] for idx in indexes], + rewards=[self.replay_memory.data["rewards"][idx] for idx in indexes], + next_states=[self.replay_memory.data["next_states"][idx] for idx in indexes], + info=[{"index": idx, "is_weight": wt} for idx, wt in zip(indexes, is_weights)] + ) + + def _get_priority(self, error): + return (np.abs(error) + self.eps) ** self.alpha + + def _update(self, idx, delta): + """Propagate priority change all the way to the root node.""" + parent = (idx - 1) // 2 + self._sum_tree[parent] += delta + if parent != 0: + self._update(parent, delta) + + def _get(self, idx, sampled_val): + """Get a leaf node according to a randomly sampled value.""" + left = 2 * idx + 1 + right = left + 1 + + if left >= len(self._sum_tree): + return idx + + if sampled_val <= self._sum_tree[left]: + return self._get(left, sampled_val) + else: + return self._get(right, sampled_val - self._sum_tree[left]) + + +class DQN(AbsPolicy): """The Deep-Q-Networks algorithm. See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. @@ -23,6 +127,7 @@ class DQN(AbsAlgorithm): Args: q_net (DiscreteQNet): Q-value model. reward_discount (float): Reward decay as defined in standard RL terminology. + train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. update_target_every (int): Number of gradient steps between target model updates. soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. @@ -36,10 +141,14 @@ class DQN(AbsAlgorithm): def __init__( self, q_net: DiscreteQNet, - reward_discount: float, - update_target_every: int, + reward_discount: float = 0.9, + train_epochs: int = 1, + update_target_every: int = 5, soft_update_coeff: float = 0.1, - double: bool = True, + double: bool = False, + prioritized_experience_sampler: PrioritizedSampler = None, + replay_memory_capacity: int = 10000, + random_overwrite: bool = False, exploration: DiscreteSpaceExploration = EpsilonGreedyExploration() ): if not isinstance(q_net, DiscreteQNet): @@ -53,13 +162,16 @@ def __init__( else: self.target_q_net = None self.reward_discount = reward_discount + self.train_epochs = train_epochs self.update_target_every = update_target_every self.soft_update_coeff = soft_update_coeff self.double = double - self.device = self.q_net.device - self._loss_func = torch.nn.MSELoss() - self._experience_memory = None + self.prioritized_experience_sampler = prioritized_experience_sampler + self.device = self.q_net.device + if not self.prioritized_experience_replay: + self._loss_func = torch.nn.MSELoss() + self._replay_memory = ReplayMemory(replay_memory_capacity, random_overwrite=random_overwrite) self._q_net_version = 0 self._target_q_net_version = 0 @@ -82,39 +194,43 @@ def apply(self, grad_dict: dict): if self._q_net_version - self._target_q_net_version == self.update_target_every: self._update_target() - def learn(self, batch: ExperienceBatch, inplace: bool = True): + def learn(self, exp: ExperienceSet): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." - # If data is an ExperienceSet, get DQN loss from the batch and backprop it throught the network. self.q_net.train() - states, next_states = batch.data.states, batch.data.next_states - actions = torch.from_numpy(np.asarray(batch.data.actions)).to(self.device) - rewards = torch.from_numpy(np.asarray(batch.data.rewards)).to(self.device) - - # get target Q values - with torch.no_grad(): - if self.double: - actions_by_eval_q_net = self.q_net.get_action(next_states)[0] - next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) + self._replay_memory.put(exp) + for _ in range(self.train_epochs): + # sample from the replay memory + experience_set = self.sampler.get() + states, next_states = experience_set.states, experience_set.next_states + actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.device) + rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) + if self.prioritized_experience_replay: + indexes = [info["index"] for info in experience_set.info] + is_weights = torch.tensor([info["is_weight"] for info in experience_set.info]).to(self.device) + + # get target Q values + with torch.no_grad(): + if self.double: + actions_by_eval_q_net = self.q_net.get_action(next_states)[0] + next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) + else: + next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) + + target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) + + # gradient step + q_values = self.q_net.q_values(states, actions) + if self.prioritized_experience_replay: + td_errors = target_q_values - q_values + loss = (td_errors * is_weights).mean() + self.sampler.update(indexes, td_errors.detach().cpu().numpy()) else: - next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) - - target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) - - # loss computation - q_values = self.q_net.q_values(states, actions) - td_errors = target_q_values - q_values - loss = self._loss_func(q_values, target_q_values) - - if inplace: + loss = self._loss_func(q_values, target_q_values) self.q_net.step(loss) - grad = None - self._q_net_version += 1 - if self._q_net_version - self._target_q_net_version == self.update_target_every: - self._update_target() - else: - grad = self.q_net.get_gradients(loss) - return DQNLossInfo(td_errors, loss, grad), batch.indexes + self._q_net_version += 1 + if self._q_net_version - self._target_q_net_version == self.update_target_every: + self._update_target() def _update_target(self): # soft-update target network diff --git a/maro/rl/experience/__init__.py b/maro/rl/experience/__init__.py deleted file mode 100644 index 8a14a135e..000000000 --- a/maro/rl/experience/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .memory import ExperienceSet, ExperienceMemory -from .sampler import AbsSampler, ExperienceBatch, PrioritizedSampler, UniformSampler - -__all__ = ["AbsSampler", "ExperienceBatch", "ExperienceSet", "ExperienceMemory", "PrioritizedSampler", "UniformSampler"] diff --git a/maro/rl/experience/sampler.py b/maro/rl/experience/sampler.py deleted file mode 100644 index 052ae0df6..000000000 --- a/maro/rl/experience/sampler.py +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from collections import namedtuple -from typing import List - -import numpy as np - -from .memory import ExperienceSet, ExperienceMemory - -ExperienceBatch = namedtuple("ExperienceBatch", ["indexes", "data"]) - - -class AbsSampler(ABC): - """Sampler class. - - Args: - experience_memory (ExperienceMemory): experience manager the sampler is associated with. - """ - def __init__(self, experience_memory: ExperienceMemory): - super().__init__() - self.experience_memory = experience_memory - - @abstractmethod - def get(self) -> ExperienceBatch: - """Sampling logic is defined here.""" - raise NotImplementedError - - def on_new(self, experience_set: ExperienceSet, indexes: List[int]): - """Callback to be executed after calling experience_memory.put().""" - pass - - def update(self, indexes: List[int], info: list): - pass - - -class UniformSampler(AbsSampler): - """Uniform sampler class. - - Args: - experience_memory (ExperienceMemory): experience manager the sampler is associated with. - batch_size (int): Batch size for the default uniform sampling. This can be set to -1 if the required - batch size is the number of items stored. To get the whole data, set this to -1 and ``replace`` to - False. Defaults to 32. - replace (bool): A flag indicating whether the default uniform sampling is with replacement or without. - Defaults to True. - """ - def __init__(self, experience_memory: ExperienceMemory, batch_size: int = 32, replace: bool = True): - if batch_size <= 0 and batch_size != -1: - raise ValueError("batch_size must be -1 or a positive integer") - super().__init__(experience_memory) - self.experience_memory = experience_memory - self.batch_size = batch_size - self.replace = replace - - def get(self) -> ExperienceBatch: - if self.batch_size > self.experience_memory.size or self.batch_size == -1: - batch_size = self.experience_memory.size - else: - batch_size = self.batch_size - - indexes = np.random.choice(self.experience_memory.size, size=batch_size, replace=self.replace) - return ExperienceBatch( - indexes=indexes, - data=ExperienceSet( - *[[self.experience_memory.data[key][idx] for idx in indexes] for key in self.experience_memory.keys] - ) - ) - - -class PrioritizedSampler(AbsSampler): - """Sampler for Prioritized Experience Replay (PER). - - References: - https://arxiv.org/pdf/1511.05952.pdf - https://github.com/rlcode/per - - The implementation here is based on direct proportional prioritization (the first variant in the paper). - The rank-based variant is not implemented here. - - Args: - experience_memory (ExperienceMemory): experience manager the sampler is associated with. - batch_size (int): mini-batch size. Defaults to 32. - alpha (float): Prioritization strength. Sampling probabilities are calculated according to - P = p_i^alpha / sum(p_k^alpha). Defaults to 0.6. - beta (float): Bias annealing strength using weighted importance sampling (IS) techniques. - IS weights are calculated according to (N * P)^(-beta), where P is the sampling probability. - This value of ``beta`` should not exceed 1.0, which corresponds to full annealing. Defaults to 0.4. - beta_step (float): The amount ``beta`` is incremented by after each get() call until it reaches 1.0. - Defaults to 0.001. - """ - def __init__( - self, - experience_memory: ExperienceMemory, - batch_size: int = 32, - alpha: float = 0.6, - beta: float = 0.4, - beta_step: float = 0.001 - ): - if beta > 1.0: - raise ValueError("beta should be between 0.0 and 1.0") - super().__init__(experience_memory) - self._sum_tree = np.zeros(2 * self.experience_memory.capacity - 1) - self.batch_size = batch_size - self.alpha = alpha - self.beta = beta - self.beta_step = beta_step - self.eps = 1e-7 - self._max_priority = 1e8 - - def total(self): - """Return the sum of priorities over all experiences.""" - return self._sum_tree[0] - - def on_new(self, experience_set: ExperienceSet, indexes: List[int]): - """Set the priorities of newly added experiences to the maximum value.""" - self.update(indexes, [self._max_priority] * len(indexes)) - - def update(self, indexes: List[int], td_errors): - """Update priority values at given indexes.""" - for idx, err in zip(indexes, td_errors): - priority = self._get_priority(err) - tree_idx = idx + self.experience_memory.capacity - 1 - delta = priority - self._sum_tree[tree_idx] - self._sum_tree[tree_idx] = priority - self._update(tree_idx, delta) - - def get(self) -> ExperienceBatch: - """Priority-based sampling.""" - indexes, priorities = [], [] - segment_len = self.total() / self.batch_size - for i in range(self.batch_size): - low, high = segment_len * i, segment_len * (i + 1) - sampled_val = np.random.uniform(low=low, high=high) - idx = self._get(0, sampled_val) - data_idx = idx - self.experience_memory.capacity + 1 - indexes.append(data_idx) - priorities.append(self._sum_tree[idx]) - - self.beta = min(1., self.beta + self.beta_step) - sampling_probabilities = priorities / self.total() - is_weights = np.power(self.experience_memory.size * sampling_probabilities, -self.beta) - is_weights /= is_weights.max() - - return ExperienceBatch( - indexes=indexes, - data=ExperienceSet( - states=[self.experience_memory.data["states"][idx] for idx in indexes], - actions=[self.experience_memory.data["actions"][idx] for idx in indexes], - rewards=[self.experience_memory.data["rewards"][idx] for idx in indexes], - next_states=[self.experience_memory.data["next_states"][idx] for idx in indexes], - info=[{"is_weight": wt} for wt in is_weights] - ) - ) - - def _get_priority(self, error): - return (np.abs(error) + self.eps) ** self.alpha - - def _update(self, idx, delta): - """Propagate priority change all the way to the root node.""" - parent = (idx - 1) // 2 - self._sum_tree[parent] += delta - if parent != 0: - self._update(parent, delta) - - def _get(self, idx, sampled_val): - """Get a leaf node according to a randomly sampled value.""" - left = 2 * idx + 1 - right = left + 1 - - if left >= len(self._sum_tree): - return idx - - if sampled_val <= self._sum_tree[left]: - return self._get(left, sampled_val) - else: - return self._get(right, sampled_val - self._sum_tree[left]) diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/learning/agent_wrapper.py index c5be26fc8..779ef3309 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/learning/agent_wrapper.py @@ -28,7 +28,7 @@ def choose_action(self, state: dict) -> dict: """Generate an action based on the given state. Args: - state (dict): Dictionary of agents' states based on which action decisions will be made. + state (dict): Dicitionary of agents' states based on which action decisions will be made. """ return {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} @@ -37,18 +37,16 @@ def get_batch(self, env: AbsEnvWrapper): names = set() exp_by_agent = env.get_experiences() for agent_id, exp in exp_by_agent.items(): - if hasattr(self.policy[agent_id], "memorize"): - self.policy[agent_id].memorize(exp) + if hasattr(self.policy[agent_id], "store"): + self.policy[agent_id].store(exp) names.add(self.agent2policy[agent_id]) - ret = {name: self.policy_dict[name].sampler.get() for name in names} - print({name: batch.data.size for name, batch in ret.items()}) - return ret + return {name: self.policy_dict[name].sampler.get() for name in names} def set_policy_states(self, policy_state_dict: dict): """Update policy states.""" for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].algorithm.set_state(policy_state) + self.policy_dict[policy_id].set_state(policy_state) def exploration_step(self): for policy in self.policy_dict.values(): diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py index dd752c83e..8555d33c1 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/learning/env_wrapper.py @@ -65,7 +65,7 @@ def __init__( self._get_experience_func = get_experience_func self._post_step = post_step - replay_agent_ids = self.env.agent_idx_list if replay_agent_ids is None else replay_agent_ids + replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids self._replay_buffer = {agent_id: defaultdict(list) for agent_id in replay_agent_ids} self._transition_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None diff --git a/maro/rl/policy/policy_manager.py b/maro/rl/learning/policy_manager.py similarity index 61% rename from maro/rl/policy/policy_manager.py rename to maro/rl/learning/policy_manager.py index a1e0490cc..34efc62aa 100644 --- a/maro/rl/policy/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -3,54 +3,64 @@ import time from abc import ABC, abstractmethod -from collections import defaultdict, namedtuple +from collections import defaultdict from multiprocessing import Pipe, Process from os import getcwd from typing import Callable, Dict from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.experience import ExperienceSet -from maro.rl.policy import CorePolicy +from maro.rl.policy import AbsCorePolicy from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger from .trainer import trainer_process -PolicyUpdateOptions = namedtuple( - "PolicyUpdateOptions", ["update_trigger", "warmup", "num_epochs", "reset_memory", "data_parallel"] -) - - class AbsPolicyManager(ABC): """Manage all policies. The actual policy instances may reside here or be distributed on a set of processes or remote nodes. Args: - policy_dict (Dict[str, CorePolicy]): A list of policies managed by the manager. - num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of - times ``policy.learn()`` is called in each call to ``update``. Defaults to 1 for each policy. + policy_dict (Dict[str, AbsCorePolicy]): A list of policies managed by the manager. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the - required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to 1 for - each policy. + required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, + all triggers will be set to 1. warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for - each policy. Defaults to 1 for each policy. - reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory - should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms - to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. + each policy. Defaults to None, in which case all warm-up sizes will be set to 1. + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, + ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to + None. """ - def __init__(self, policy_dict: Dict[str, CorePolicy], update_option: Dict[str, PolicyUpdateOptions]): + def __init__( + self, + policy_dict: Dict[str, AbsCorePolicy], + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None, + post_update: Callable = None + ): for policy in policy_dict.values(): - if not isinstance(policy, CorePolicy): - raise ValueError("Only 'CorePolicy' instances can be managed by a policy manager.") + if not isinstance(policy, AbsCorePolicy): + raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") super().__init__() self.policy_dict = policy_dict - self.update_option = update_option + if not update_trigger: + self.update_trigger = {name: 1 for name in self.policy_dict} + else: + self.update_trigger = update_trigger + if not warmup: + self.warmup = {name: 1 for name in self.policy_dict} + else: + self.warmup = warmup + + self._post_update = post_update self._update_history = [set(policy_dict.keys())] + self.tracker = {} @property def version(self): @@ -61,43 +71,43 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): """Logic for handling incoming experiences is implemented here.""" raise NotImplementedError - def get_state(self, cur_version: int = None, inference: bool = True): + def get_state(self, cur_version: int = None): if cur_version is None: cur_version = self.version - 1 updated = set() for version in range(cur_version + 1, len(self._update_history)): updated |= self._update_history[version] - return {name: self.policy_dict[name].algorithm.get_state(inference=inference) for name in updated} + return {name: self.policy_dict[name].get_state() for name in updated} class LocalPolicyManager(AbsPolicyManager): """Policy manager that contains the actual policy instances. Args: - policy_dict (Dict[str, CorePolicy]): Policies managed by the manager. - num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of - times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the - number of learning epochs will be set to 1 for each policy. + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, all triggers will be set to 1. warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory - should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms - to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, + ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to + None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( self, - policy_dict: Dict[str, CorePolicy], - update_option: Dict[str, PolicyUpdateOptions], + policy_dict: Dict[str, AbsCorePolicy], + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None, + post_update: Callable = None, log_dir: str = getcwd() ): - super().__init__(policy_dict, update_option) + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) self._new_exp_counter = defaultdict(int) self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) @@ -111,16 +121,13 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): updated = set() for policy_name, exp in exp_by_policy.items(): policy = self.policy_dict[policy_name] - policy.experience_memory.put(exp) + policy.replay_memory.put(exp) self._new_exp_counter[policy_name] += exp.size if ( - self._new_exp_counter[policy_name] >= self.update_option[policy_name].update_trigger and - policy.experience_memory.size >= self.update_option[policy_name].warmup + self._new_exp_counter[policy_name] >= self.update_trigger[policy_name] and + policy.replay_memory.size >= self.warmup[policy_name] ): - for _ in range(self.update_option[policy_name].num_epochs): - policy.update() - if self.update_option[policy_name].reset_memory: - policy.reset_memory() + policy.learn() updated.add(policy_name) self._new_exp_counter[policy_name] = 0 @@ -128,6 +135,9 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") + if self._post_update: + self._post_update([policy.tracker for policy in self.policy_dict.values()]) + self._logger.debug(f"policy update time: {time.time() - t0}") @@ -135,36 +145,35 @@ class MultiProcessPolicyManager(AbsPolicyManager): """Policy manager that spawns a set of trainer processes for parallel training. Args: - policy_dict (Dict[str, CorePolicy]): Policies managed by the manager. + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. num_trainers (int): Number of trainer processes to be forked. create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. - num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of - times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the - number of learning epochs will be set to 1 for each policy. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, all triggers will be set to 1. warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory - should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms - to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers,) + -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( self, - policy_dict: Dict[str, CorePolicy], - update_option: Dict[str, PolicyUpdateOptions], + policy_dict: Dict[str, AbsCorePolicy], num_trainers: int, create_policy_func_dict: Dict[str, Callable], + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None, + post_update: Callable = None, log_dir: str = getcwd(), ): - super().__init__(policy_dict, update_option) + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) self._policy2trainer = {} self._trainer2policies = defaultdict(list) self._exp_cache = defaultdict(ExperienceSet) @@ -188,8 +197,7 @@ def __init__( trainer_id, trainer_end, {name: create_policy_func_dict[name] for name in policy_names}, - {name: self.policy_dict[name].algorithm.get_state() for name in policy_names}, - {name: self.update_option[name] for name in policy_names} + {name: self.policy_dict[name].get_state() for name in self._trainer2policies[trainer_id]} ), kwargs={"log_dir": log_dir} ) @@ -202,27 +210,31 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._num_experiences_by_policy[policy_name] += exp.size self._exp_cache[policy_name].extend(exp) if ( - self._exp_cache[policy_name].size >= self.update_option[policy_name].update_trigger and - self._num_experiences_by_policy[policy_name] >= self.update_option[policy_name].warmup + self._exp_cache[policy_name].size >= self.update_trigger[policy_name] and + self._num_experiences_by_policy[policy_name] >= self.warmup[policy_name] ): exp_to_send[policy_name] = self._exp_cache.pop(policy_name) updated.add(policy_name) - if exp_to_send: - for trainer_id, conn in self._manager_end.items(): - conn.send({ - "type": "train", - "experiences": {name: exp_to_send[name] for name in self._trainer2policies[trainer_id]} - }) + for trainer_id, conn in self._manager_end.items(): + conn.send({ + "type": "train", + "experiences": {name: exp_to_send[name] for name in self._trainer2policies[trainer_id]} + }) - for conn in self._manager_end.values(): - result = conn.recv() - for policy_name, policy_state in result["policy"].items(): - self.policy_dict[policy_name].algorithm.set_state(policy_state) + trackers = [] + for conn in self._manager_end.values(): + result = conn.recv() + trackers.append(result["tracker"]) + for policy_name, policy_state in result["policy"].items(): + self.policy_dict[policy_name].set_state(policy_state) - if updated: - self._update_history.append(updated) - self._logger.info(f"Updated policies {updated}") + if updated: + self._update_history.append(updated) + self._logger.info(f"Updated policies {updated}") + + if self._post_update: + self._post_update(trackers) def exit(self): """Tell the trainer processes to exit.""" @@ -234,23 +246,20 @@ class MultiNodePolicyManager(AbsPolicyManager): """Policy manager that communicates with a set of remote nodes for parallel training. Args: - policy_dict (Dict[str, CorePolicy]): Policies managed by the manager. + policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. group (str): Group name for the training cluster, which includes all trainers and a training manager that manages them. num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where 0 <= i < num_trainers. - num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of - times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the - number of learning epochs will be set to 1 for each policy. update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, all triggers will be set to 1. warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory - should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms - to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. + post_update (Callable): Custom function to process whatever information is collected by each + trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers,) + -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -259,14 +268,16 @@ class MultiNodePolicyManager(AbsPolicyManager): """ def __init__( self, - policy_dict: Dict[str, CorePolicy], - update_option: Dict[str, PolicyUpdateOptions], + policy_dict: Dict[str, AbsCorePolicy], group: str, num_trainers: int, + update_trigger: Dict[str, int] = None, + warmup: Dict[str, int] = None, + post_update: Callable = None, log_dir: str = getcwd(), proxy_kwargs: dict = {} ): - super().__init__(policy_dict, update_option) + super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) peers = {"trainer": num_trainers} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) @@ -287,12 +298,7 @@ def __init__( self._proxy.send( SessionMessage( MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, - body={ - MsgKey.POLICY_STATE: { - name: self.policy_dict[name].algorithm.get_state(inference=False) - for name in policy_names - } - } + body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} ) ) @@ -302,33 +308,32 @@ def update(self, exp_by_policy: Dict[str, ExperienceSet]): self._num_experiences_by_policy[policy_name] += exp.size self._exp_cache[policy_name].extend(exp) if ( - self._exp_cache[policy_name].size >= self.update_option[policy_name].update_trigger and - self._num_experiences_by_policy[policy_name] >= self.update_option[policy_name].warmup + self._exp_cache[policy_name].size >= self.update_trigger[policy_name] and + self._num_experiences_by_policy[policy_name] >= self.warmup[policy_name] ): exp_to_send[policy_name] = self._exp_cache.pop(policy_name) updated.add(policy_name) - if exp_to_send: - msg_body_by_dest = defaultdict(dict) - for policy_name, exp in exp_to_send.items(): - trainer_id = self._policy2trainer[policy_name] - if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp - - dones = 0 - self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())) - for msg in self._proxy.receive(): - if msg.tag == MsgTag.TRAIN_DONE: - for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): - self.policy_dict[policy_name].algorithm.set_state(policy_state) - dones += 1 - if dones == len(msg_body_by_dest): - break + msg_body_by_dest = defaultdict(dict) + for policy_name, exp in exp_to_send.items(): + trainer_id = self._policy2trainer[policy_name] + if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} + msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp + + trackers = [] + for reply in self._proxy.scatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())): + trackers.append(reply.body[MsgKey.TRACKER]) + for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): + self.policy_dict[policy_name].set_state(policy_state) + if updated: self._update_history.append(updated) self._logger.info(f"Updated policies {updated}") + if self._post_update: + self._post_update(trackers) + def exit(self): """Tell the remote trainers to exit.""" self._proxy.ibroadcast("trainer", MsgTag.EXIT, SessionType.NOTIFICATION) diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py index 6a95e9bbf..389e42851 100644 --- a/maro/rl/learning/simple_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -5,7 +5,7 @@ from os import getcwd from typing import Callable, Dict, List, Union -from maro.rl.policy import CorePolicy, LocalPolicyManager +from maro.rl.policy import AbsCorePolicy, LocalPolicyManager from maro.utils import Logger from .agent_wrapper import AgentWrapper @@ -82,7 +82,7 @@ def __init__( # Create a policy manager to manage all trainable policies from the agent wrapper self.policy_manager = LocalPolicyManager( - {name: policy for name, policy in self.agent.policy_dict.items() if isinstance(policy, CorePolicy)}, + {name: policy for name, policy in self.agent.policy_dict.items() if isinstance(policy, AbsCorePolicy)}, update_trigger=update_trigger, warmup=warmup, post_update=post_update diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 15bb1d100..4f4774c43 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -462,8 +462,8 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): else: tracker = self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) if tracker is not None: + num_finishes += 1 trackers.append(tracker) - num_finishes += 1 if num_finishes == self._num_workers: break @@ -491,7 +491,7 @@ def _handle_worker_result(self, msg, ep, segment, version, combined_exp): return for policy_name, exp in msg.body[MsgKey.EXPERIENCES].items(): - combined_exp[policy_name].extend(exp.data) + combined_exp[policy_name].extend(exp) # The message is what we expect if msg.body[MsgKey.EPISODE] == ep and msg.body[MsgKey.SEGMENT] == segment: diff --git a/maro/rl/policy/trainer.py b/maro/rl/learning/trainer.py similarity index 50% rename from maro/rl/policy/trainer.py rename to maro/rl/learning/trainer.py index fe30f8020..4b025dfaa 100644 --- a/maro/rl/policy/trainer.py +++ b/maro/rl/learning/trainer.py @@ -2,13 +2,11 @@ # Licensed under the MIT license. import time -from collections import defaultdict from multiprocessing.connection import Connection from os import getcwd from typing import Callable, Dict from maro.communication import Proxy -from maro.rl.algorithms import AbsAlgorithm from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger @@ -18,8 +16,6 @@ def trainer_process( conn: Connection, create_policy_func_dict: Dict[str, Callable], initial_policy_states: dict, - num_epochs: Dict[str, int], - reset_memory: Dict[str, bool] = defaultdict(lambda: False), log_dir: str = getcwd() ): """Policy trainer process which can be spawned by a ``MultiProcessPolicyManager``. @@ -30,19 +26,12 @@ def trainer_process( create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. - initial_policy_states (dict): States with which to initialize the policies. - num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of - times ``policy.learn()`` is called in each call to ``update``. Defaults to None, in which case the - number of learning epochs will be set to 1 for each policy. - reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory - should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms - to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} logger = Logger("TRAINER", dump_folder=log_dir) for name, state in initial_policy_states.items(): - policy_dict[name].algorithm.set_state(state) + policy_dict[name].set_state(state) logger.info(f"{trainer_id} initialized policy {name}") while True: @@ -50,14 +39,12 @@ def trainer_process( if msg["type"] == "train": t0 = time.time() for name, exp in msg["experiences"].items(): - policy_dict[name].memorize(exp) - for _ in range(num_epochs[name]): - policy_dict[name].update() - if reset_memory[name]: - policy_dict[name].reset_memory() + policy_dict[name].store(exp) + policy_dict[name].learn() logger.debug(f"total policy update time: {time.time() - t0}") conn.send({ - "policy": {name: policy_dict[name].algorithm.get_state() for name in msg["experiences"]} + "policy": {name: policy_dict[name].get_state() for name in msg["experiences"]}, + "tracker": {name: policy_dict[name].tracker for name in msg["experiences"]} }) elif msg["type"] == "quit": break @@ -67,8 +54,6 @@ def trainer_node( group: str, trainer_idx: int, create_policy_func_dict: Dict[str, Callable], - num_epochs: Dict[str, int], - reset_memory: Dict[str, int] = defaultdict(lambda: False), proxy_kwargs: dict = {}, log_dir: str = getcwd() ): @@ -81,12 +66,6 @@ def trainer_node( create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` instance. - num_epochs (Dict[str, int]): Number of learning epochs for each policy. This determine the number of times - ``policy.update()`` is called in each learning round. Defaults to None, in which case the number of - learning epochs will be set to 1 for each policy. - reset_memory (Dict[str, bool]): A dictionary of flags indicating whether each policy's experience memory - should be reset after it is updated. It may be necessary to set this to True for on-policy algorithms - to ensure that the experiences to be learned from stay up to date. Defaults to False for each policy. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. @@ -104,43 +83,18 @@ def trainer_node( if msg.tag == MsgTag.INIT_POLICY_STATE: for name, state in msg.body[MsgKey.POLICY_STATE].items(): policy_dict[name] = create_policy_func_dict[name]() - policy_dict[name].algorithm.set_state(state) + policy_dict[name].set_state(state) logger.info(f"{proxy.name} initialized policy {name}") proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) elif msg.tag == MsgTag.LEARN: t0 = time.time() for name, exp in msg.body[MsgKey.EXPERIENCES].items(): - policy_dict[name].memorize(exp) - for _ in range(num_epochs[name]): - policy_dict[name].update() - if reset_memory[name]: - policy_dict[name].reset_memory() + policy_dict[name].store(exp) + policy_dict[name].learn() msg_body = { - MsgKey.POLICY_STATE: - {name: policy_dict[name].algorithm.get_state() for name in msg.body[MsgKey.EXPERIENCES]} + MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.EXPERIENCES]}, + MsgKey.TRACKER: {name: policy_dict[name].tracker for name in msg.body[MsgKey.EXPERIENCES]} } - logger.info(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, tag=MsgTag.TRAIN_DONE, body=msg_body) - elif msg.tag == MsgTag.GET_UPDATE_INFO: - t0 = time.time() - msg_body = { - MsgKey.UPDATE_INFO: - {name: policy_dict[name].get_update_info(exp) for name, exp in msg.body[MsgKey.EXPERIENCES].items()}, - } - logger.info(f"total time to get update info: {time.time() - t0}") - proxy.reply(msg, tag=MsgTag.UPDATE_INFO, body=msg_body) - elif msg.tag == MsgTag.UPDATE_POLICY_STATE: - for name, state in msg.body[MsgKey.POLICY_STATE].items(): - policy_dict[name].algorithm.set_state(state) - logger.info(f"{proxy.name} updated policy {name}") - - -def gradient_worker_node( - group: str, - create_algorithm_func: Callable[[], AbsAlgorithm], - worker_idx: int, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - pass \ No newline at end of file + logger.debug(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, body=msg_body) diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index c8984598b..82f1fde2d 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -91,37 +91,9 @@ def trainable(self) -> bool: def forward(self, *args, **kwargs): raise NotImplementedError - def get_gradients(self, loss: torch.tensor): - """Compute gradients from a loss """ - if self.optimizer is None: - raise MissingOptimizer("No optimizer registered to the model") - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.zero_grad() - else: - self.optimizer.zero_grad() - - # Obtain gradients through back-propagation - loss.backward() - - return {name: param.grad for name, param in self.named_parameters()} - - def apply(self, grad_dict: dict): - for name, param in self.named_parameters(): - param.grad = grad_dict[name] - - # Apply gradients - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.step() - else: - self.optimizer.step() - - def step(self, loss: torch.tensor): + def step(self, loss): """Use the loss to back-propagate gradients and apply them to the underlying parameters. - This is equivalent to a chained ``get_gradients`` and ``step``. - Args: loss: Result of a computation graph that involves the underlying parameters. """ diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py deleted file mode 100644 index 5ae80af34..000000000 --- a/maro/rl/policy/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .policy import AbsPolicy, CorePolicy, NullPolicy -from .policy_manager import ( - AbsPolicyManager, LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager, PolicyUpdateOptions -) -from .trainer import trainer_node, trainer_process - -__all__ = [ - "AbsPolicy", "AbsPolicyManager", "CorePolicy", "LocalPolicyManager", "MultiNodePolicyManager", - "MultiProcessPolicyManager", "PolicyUpdateOptions", "NullPolicy", "trainer_node", "trainer_process" -] diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py deleted file mode 100644 index cca003a1a..000000000 --- a/maro/rl/policy/policy.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - -from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceSet, ExperienceMemory, UniformSampler - - -class AbsPolicy(ABC): - """Abstract policy class.""" - def __init__(self): - super().__init__() - - @abstractmethod - def choose_action(self, state): - raise NotImplementedError - - -class NullPolicy(AbsPolicy): - """Dummy policy that does nothing. - - Note that the meaning of a "None" action may depend on the scenario. - """ - def choose_action(self, state): - return None - - -class CorePolicy(AbsPolicy): - """Policy that can update itself using simulation experiences. - - Reinforcement learning (RL) policies should inherit from this. - - Args: - algorithm (AbsAlgorithm): Algorithm instance. - memory_capacity (int): Capacity for the internal experience memory. - random_overwrite (bool): This specifies overwrite behavior when the capacity is reached. If this is True, - overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with - wrap-around. Defaults to False. - sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to ``UnifromSampler``. - sampler_kwargs (dict): Keyword arguments for ``sampler_cls``. - """ - def __init__( - self, - algorithm: AbsAlgorithm, - memory_capacity: int, - random_overwrite: bool = False, - sampler_cls=UniformSampler, - sampler_kwargs: dict = {} - ): - super().__init__() - self.algorithm = algorithm - self.experience_memory = ExperienceMemory(memory_capacity, random_overwrite=random_overwrite) - self.sampler = sampler_cls(self.experience_memory, **sampler_kwargs) - - self.exploring = False - - def choose_action(self, state): - return self.algorithm.choose_action(state, explore=self.exploring) - - def memorize(self, exp: ExperienceSet) -> bool: - """ - Store incoming experiences and update if necessary. - """ - indexes = self.experience_memory.put(exp) - self.sampler.on_new(exp, indexes) - - def get_batch(self): - self.sampler.get() - - def reset_memory(self): - """Clear the experience store.""" - self.experience_memory.clear() - - def update(self): - batch = self.sampler.get() - loss_info, indexes = self.algorithm.learn(batch, inplace=True) - self.sampler.update(indexes, loss_info) - - def explore(self): - self.exploring = True - - def exploit(self): - self.exploring = False - - def exploration_step(self): - if self.algorithm.exploration: - self.algorithm.exploration.step() diff --git a/maro/rl/replay/__init__.py b/maro/rl/replay/__init__.py new file mode 100644 index 000000000..d9a75090c --- /dev/null +++ b/maro/rl/replay/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .replay_memory import ExperienceSet, ReplayMemory + +__all__ = ["ExperienceSet", "ReplayMemory"] diff --git a/maro/rl/experience/memory.py b/maro/rl/replay/replay_memory.py similarity index 99% rename from maro/rl/experience/memory.py rename to maro/rl/replay/replay_memory.py index ef13ef691..106bf55bf 100644 --- a/maro/rl/experience/memory.py +++ b/maro/rl/replay/replay_memory.py @@ -45,7 +45,7 @@ def extend(self, other): self.info += other.info -class ExperienceMemory: +class ReplayMemory: """Storage facility for simulation experiences. This implementation uses a dictionary of lists as the internal data structure. The objects for each key @@ -88,7 +88,6 @@ def keys(self): def put(self, experience_set: ExperienceSet): """Put a experience set in the store. - Args: experience_set (ExperienceSet): Experience set to be put in the store. """ diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index c27d64d50..4a76fd279 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -14,12 +14,9 @@ class MsgTag(Enum): CHOOSE_ACTION = "choose_action" ACTION = "action" LEARN = "LEARN" - LOSS_INFO = "loss_info" - UPDATE_POLICY_STATE = "update_policy_state" ABORT_ROLLOUT = "abort_rollout" EVAL_DONE = "eval_done" COLLECT_DONE = "collect_done" - TRAIN_DONE = "train_done" DONE = "done" EXIT = "exit" @@ -34,8 +31,6 @@ class MsgKey(Enum): TRACKER = "tracker" STATE = "state" POLICY_STATE = "policy_state" - NUM_EPOCHS = "num_epochs" - LOSS_INFO = "loss_info" EXPLORATION_STEP = "exploration_step" VERSION = "version" NUM_STEPS = "num_steps" From 5f6c47c5df676da1c2adbc8a62dad18b084b5d9d Mon Sep 17 00:00:00 2001 From: yaqiu Date: Tue, 17 Aug 2021 16:41:13 +0000 Subject: [PATCH 406/482] policy interface and policy manager redesigned --- examples/rl/cim/__init__.py | 3 +- examples/rl/cim/ac.py | 115 ++--- examples/rl/cim/dqn.py | 118 +++-- examples/rl/cim/env_wrapper.py | 4 +- examples/rl/cim/policies.py | 48 -- .../rl/scripts/docker/docker_compose_yml.py | 32 +- examples/rl/scripts/docker/run.sh | 2 +- examples/rl/workflows/agent_wrapper.py | 9 +- examples/rl/workflows/config.yml | 11 +- examples/rl/workflows/general.py | 2 - .../{trainer.py => policy_host.py} | 17 +- .../policy_manager/policy_manager.py | 38 +- .../workflows/synchronous/rollout_worker.py | 2 +- maro/rl/algorithms/__init__.py | 13 - maro/rl/algorithms/ac.py | 125 ------ maro/rl/algorithms/ddpg.py | 121 ------ maro/rl/algorithms/index.py | 40 -- maro/rl/algorithms/pg.py | 75 ---- maro/rl/learning/__init__.py | 11 +- maro/rl/learning/asynchronous/actor.py | 6 +- .../rl/learning/asynchronous/policy_server.py | 5 +- maro/rl/learning/policy_host.py | 69 +++ maro/rl/learning/policy_manager.py | 409 +++++++----------- maro/rl/learning/simple_learner.py | 10 +- maro/rl/learning/synchronous/learner.py | 13 +- .../learning/synchronous/rollout_manager.py | 43 +- .../rl/learning/synchronous/rollout_worker.py | 6 +- maro/rl/learning/trainer.py | 100 ----- maro/rl/model/__init__.py | 4 +- maro/rl/model/core_model.py | 240 ++-------- maro/rl/policy/__init__.py | 18 + maro/rl/policy/ac.py | 204 +++++++++ maro/rl/policy/ddpg.py | 213 +++++++++ maro/rl/{algorithms => policy}/dqn.py | 238 +++++++--- maro/rl/policy/index.py | 40 ++ maro/rl/policy/pg.py | 140 ++++++ .../abs_algorithm.py => policy/policy.py} | 85 ++-- .../replay_memory.py => policy/replay.py} | 55 +-- maro/rl/replay/__init__.py | 6 - maro/rl/typing/__init__.py | 10 + maro/rl/typing/model_types.py | 232 ++++++++++ maro/rl/typing/rollout_object_types.py | 38 ++ maro/rl/utils/__init__.py | 6 +- maro/rl/utils/message_enums.py | 15 +- maro/rl/utils/remote_tools/__init__.py | 8 + maro/rl/utils/remote_tools/client.py | 6 + maro/rl/utils/remote_tools/task_manager.py | 10 + maro/rl/utils/remote_tools/worker.py | 52 +++ maro/rl/utils/trajectory_computation.py | 28 ++ maro/rl/utils/trajectory_utils.py | 103 ----- maro/rl/wrappers/__init__.py | 10 + .../{learning => wrappers}/agent_wrapper.py | 34 +- maro/rl/{learning => wrappers}/env_wrapper.py | 81 ++-- 53 files changed, 1754 insertions(+), 1569 deletions(-) delete mode 100644 examples/rl/cim/policies.py rename examples/rl/workflows/policy_manager/{trainer.py => policy_host.py} (57%) delete mode 100644 maro/rl/algorithms/__init__.py delete mode 100644 maro/rl/algorithms/ac.py delete mode 100644 maro/rl/algorithms/ddpg.py delete mode 100644 maro/rl/algorithms/index.py delete mode 100644 maro/rl/algorithms/pg.py create mode 100644 maro/rl/learning/policy_host.py delete mode 100644 maro/rl/learning/trainer.py create mode 100644 maro/rl/policy/__init__.py create mode 100644 maro/rl/policy/ac.py create mode 100644 maro/rl/policy/ddpg.py rename maro/rl/{algorithms => policy}/dqn.py (52%) create mode 100644 maro/rl/policy/index.py create mode 100644 maro/rl/policy/pg.py rename maro/rl/{algorithms/abs_algorithm.py => policy/policy.py} (61%) rename maro/rl/{replay/replay_memory.py => policy/replay.py} (61%) delete mode 100644 maro/rl/replay/__init__.py create mode 100644 maro/rl/typing/__init__.py create mode 100644 maro/rl/typing/model_types.py create mode 100644 maro/rl/typing/rollout_object_types.py create mode 100644 maro/rl/utils/remote_tools/__init__.py create mode 100644 maro/rl/utils/remote_tools/client.py create mode 100644 maro/rl/utils/remote_tools/task_manager.py create mode 100644 maro/rl/utils/remote_tools/worker.py create mode 100644 maro/rl/utils/trajectory_computation.py delete mode 100644 maro/rl/utils/trajectory_utils.py create mode 100644 maro/rl/wrappers/__init__.py rename maro/rl/{learning => wrappers}/agent_wrapper.py (59%) rename maro/rl/{learning => wrappers}/env_wrapper.py (76%) diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index 47905f64a..8560e95b0 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -3,9 +3,8 @@ from .callbacks import post_collect, post_evaluate from .env_wrapper import get_env_wrapper -from .policies import agent2policy, rl_policy_func_index, update_option +from .policy_index import agent2policy, rl_policy_func_index __all__ = [ "agent2policy", "post_collect", "post_evaluate", "get_env_wrapper", "rl_policy_func_index", - "update_option" ] diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 52f52cec9..3b2f56cd9 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -7,68 +7,60 @@ import numpy as np import torch -from maro.rl.experience import ReplayMemory, UniformSampler -from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption -from maro.rl.policy.algorithms import ActorCritic, ActorCriticConfig +from maro.rl.model import FullyConnectedBlock, OptimOption +from maro.rl.policy import ActorCritic, DiscreteACNet cim_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, cim_path) from env_wrapper import STATE_DIM, env_config -config = { - "model": { - "network": { - "actor": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64], - "output_dim": env_config["wrapper"]["num_actions"], - "activation": "tanh", - "softmax": True, - "batch_norm": False, - "head": True - }, - "critic": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64], - "output_dim": 1, - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "head": True - } +model_config = { + "network": { + "actor": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64], + "output_dim": env_config["wrapper"]["num_actions"], + "activation": "tanh", + "softmax": True, + "batch_norm": False, + "head": True }, - "optimization": { - "actor": { - "optim_cls": "adam", - "optim_params": {"lr": 0.001} - }, - "critic": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.001} - } + "critic": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64], + "output_dim": 1, + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "head": True } }, - "algorithm": { - "reward_discount": .0, - "critic_loss_cls": "smooth_l1", - "train_epochs": 10, - "critic_loss_coeff": 0.1, - "entropy_coeff": 0.01, - # "clip_ratio": 0.8 # for PPO - }, - "replay_memory": { - "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, - "update": {"capacity": 100000, "overwrite_type": "rolling"} - }, - "sampler": { - "rollout": {"batch_size": -1, "replace": False}, - "update": {"batch_size": 128, "replace": True} + "optimization": { + "actor": { + "optim_cls": "adam", + "optim_params": {"lr": 0.001} + }, + "critic": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.001} + } } } +ac_config = { + "reward_discount": .0, + "grad_iters": 10, + "critic_loss_cls": "smooth_l1", + "min_logp": None, + "critic_loss_coeff": 0.1, + "entropy_coeff": 0.01, + # "clip_ratio": 0.8 # for PPO + "lam": 0.9, + "get_loss_on_rollout_finish": False +} -def get_ac_policy(mode="update"): - assert mode in {"inference", "update", "inference-update"} + +def get_ac_policy(name: str, remote: bool = False): class MyACNET(DiscreteACNet): def forward(self, states, actor: bool = True, critic: bool = True): states = torch.from_numpy(np.asarray(states)) @@ -83,24 +75,13 @@ def forward(self, states, actor: bool = True, critic: bool = True): ac_net = MyACNET( component={ - "actor": FullyConnectedBlock(**config["model"]["network"]["actor"]), - "critic": FullyConnectedBlock(**config["model"]["network"]["critic"]) + "actor": FullyConnectedBlock(**model_config["network"]["actor"]), + "critic": FullyConnectedBlock(**model_config["network"]["critic"]) }, optim_option={ - "actor": OptimOption(**config["model"]["optimization"]["actor"]), - "critic": OptimOption(**config["model"]["optimization"]["critic"]) - } if mode != "inference" else None + "actor": OptimOption(**model_config["optimization"]["actor"]), + "critic": OptimOption(**model_config["optimization"]["critic"]) + } ) - if mode == "update": - exp_store = ReplayMemory(**config["replay_memory"]["update"]) - experience_sampler_kwargs = config["sampler"]["update"] - else: - exp_store = ReplayMemory(**config["replay_memory"]["rollout" if mode == "inference" else "update"]) - experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - - return ActorCritic( - ac_net, ActorCriticConfig(**config["algorithm"]), exp_store, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs=experience_sampler_kwargs - ) + return ActorCritic(name, ac_net, **ac_config, remote=remote) diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index 2047226e0..70706f889 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -8,63 +8,57 @@ import torch import torch.nn as nn -from maro.rl.experience import ReplayMemory, UniformSampler from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler -from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption -from maro.rl.policy.algorithms import DQN, DQNConfig +from maro.rl.model import FullyConnectedBlock, OptimOption +from maro.rl.policy import DQN, DiscreteQNet cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) from env_wrapper import STATE_DIM, env_config -config = { - "model": { - "network": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64, 32], - "output_dim": env_config["wrapper"]["num_actions"], - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.05} - } +model_config = { + "network": { + "input_dim": STATE_DIM, + "hidden_dims": [256, 128, 64, 32], + "output_dim": env_config["wrapper"]["num_actions"], + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 }, - "algorithm": { - "reward_discount": .0, - "update_target_every": 5, - "train_epochs": 10, - "soft_update_coeff": 0.1, - "double": False - }, - "replay_memory": { - "rollout": {"capacity": 1000, "overwrite_type": "rolling"}, - "update": {"capacity": 100000, "overwrite_type": "rolling"} - }, - "sampler": { - "rollout": { - "batch_size": -1, - "replace": False - }, - "update": { - "batch_size": 128, - "replace": True - } - }, - "exploration": { - "last_ep": 10, - "initial_value": 0.4, - "final_value": 0.0, - "splits": [(5, 0.32)] + "optimization": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.05} } } +dqn_config = { + "reward_discount": .0, + "update_target_every": 5, + "num_epochs": 10, + "soft_update_coeff": 0.1, + "double": False, + "replay_memory_capacity": 10000, + "random_overwrite": False, + "prioritized_replay_kwargs": { + "batch_size": 32, + "alpha": 0.6, + "beta": 0.4, + "beta_step": 0.001, + "max_priority": 1e8 + } +} + +exploration_config = { + "last_ep": 10, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(5, 0.32)] +} + class QNet(DiscreteQNet): def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): @@ -77,29 +71,15 @@ def forward(self, states): return self.component(states) -def get_dqn_policy(mode="update"): - assert mode in {"inference", "update", "inference-update"} +def get_dqn_policy(name: str, remote: bool = False): qnet = QNet( - FullyConnectedBlock(**config["model"]["network"]), - optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None + FullyConnectedBlock(**model_config["network"]), + optim_option=OptimOption(**model_config["optimization"]) ) - if mode == "update": - exp_store = ReplayMemory(**config["replay_memory"]["update"]) - exploration = None - exp_sampler_kwargs = config["sampler"]["update"] - else: - exploration = EpsilonGreedyExploration() - exploration.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - **config["exploration"] - ) - exp_store = ReplayMemory(**config["replay_memory"]["rollout" if mode == "inference" else "update"]) - exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - - return DQN( - qnet, DQNConfig(**config["algorithm"]), exp_store, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs=exp_sampler_kwargs, - exploration=exploration + exploration = EpsilonGreedyExploration() + exploration.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **exploration_config ) + return DQN(name, qnet, **dqn_config, remote=remote) diff --git a/examples/rl/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py index 1245ac23d..e206cb04c 100644 --- a/examples/rl/cim/env_wrapper.py +++ b/examples/rl/cim/env_wrapper.py @@ -3,7 +3,7 @@ import numpy as np -from maro.rl.learning import AbsEnvWrapper +from maro.rl.wrappers import AbsEnvWrapper from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType @@ -117,7 +117,7 @@ def get_reward(self, actions, tick=None): "basic": { "scenario": "cim", "topology": "toy.4p_ssdd_l0.0", - "durations": 560 + "durations": 280 }, "wrapper": { "port_attributes": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py deleted file mode 100644 index 69949997f..000000000 --- a/examples/rl/cim/policies.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import os -import sys - -from maro.rl.policy import CorePolicy, PolicyUpdateOptions - -cim_path = os.path.dirname(os.path.realpath(__file__)) -algo_path = os.path.join(cim_path, "algorithms") - -if cim_path not in sys.path: - sys.path.insert(0, cim_path) -if algo_path not in sys.path: - sys.path.insert(0, algo_path) - -from env_wrapper import AGENT_IDS - -update_option = { - name: PolicyUpdateOptions(update_trigger=16, warmup=256, num_epochs=10, reset_memory=False, data_parallel=False) - for name in AGENT_IDS -} - -experience_memory_config = { - "memory_capacity": 100000, - "random_overwrite": False -} - -sampling_config = { - "rollout": {"batch_size": 1280, "replace": True}, - "learning": {"batch_size": 128, "replace": True} -} - - -algorithm_type = "dqn" # "dqn" or "ac" -module = importlib.import_module(algorithm_type) - -def get_policy(rollout_only: bool = False): - return CorePolicy( - getattr(module, "get_algorithm")(), - **experience_memory_config, - sampler_kwargs=sampling_config["rollout" if rollout_only else "learning"] - ) - -# use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {name: get_policy for name in AGENT_IDS} -agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 462819962..94218ea4e 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -17,7 +17,6 @@ with open(config_path, "r") as fp: config = yaml.safe_load(fp) - num_trainers = config["policy_manager"]["num_trainers"] redis_host = config["redis"]["host"] docker_compose_manifest = {"version": "3.9", "services": {"redis": {"image": "redis:6", "container_name": redis_host}}} @@ -45,19 +44,19 @@ else: common_env.append(f"NUMACTORS={config['async']['num_actors']}") -# trainer spec -if config["policy_manager"]["train_mode"] == "multi-node": - for trainer_id in range(num_trainers): - str_id = f"trainer.{trainer_id}" - trainer_spec = deepcopy(common_spec) - del trainer_spec["build"] - trainer_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/trainer.py" - trainer_spec["container_name"] = str_id - trainer_spec["environment"] = [ - f"TRAINERID={trainer_id}", - f"TRAINGROUP={config['policy_manager']['train_group']}" +# host spec +if config["policy_manager"]["type"] == "distributed": + for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): + str_id = f"host.{host_id}" + host_spec = deepcopy(common_spec) + del host_spec["build"] + host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/host.py" + host_spec["container_name"] = str_id + host_spec["environment"] = [ + f"HOSTID={host_id}", + f"LEARNGROUP={config['policy_manager']['distributed']['learn_group']}" ] + common_env - docker_compose_manifest["services"][str_id] = trainer_spec + docker_compose_manifest["services"][str_id] = host_spec mode = config["mode"] if mode == "sync": @@ -77,9 +76,10 @@ f"ROLLOUTGROUP={config['sync']['rollout_group']}", f"NUMEPISODES={config['num_episodes']}", f"EVALSCH={config['eval_schedule']}", - f"TRAINMODE={config['policy_manager']['train_mode']}", - f"TRAINGROUP={config['policy_manager']['train_group']}", - f"NUMTRAINERS={config['policy_manager']['num_trainers']}" + f"POLICYMANAGERTYPE={config['policy_manager']['type']}", + f"PARALLEL={config['policy_manager']['simple']['parallel']}", + f"LEARNGROUP={config['policy_manager']['distributed']['learn_group']}", + f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}" ] + common_env } } diff --git a/examples/rl/scripts/docker/run.sh b/examples/rl/scripts/docker/run.sh index 8a51fa1eb..d8665c177 100644 --- a/examples/rl/scripts/docker/run.sh +++ b/examples/rl/scripts/docker/run.sh @@ -2,6 +2,6 @@ BASEDIR=$(dirname "$0") -# script to run the CIM scenario in single-host multi-container mode. +# script to run the multi-container mode. python3 $BASEDIR/docker_compose_yml.py docker-compose -f $BASEDIR/docker-compose.yml up \ No newline at end of file diff --git a/examples/rl/workflows/agent_wrapper.py b/examples/rl/workflows/agent_wrapper.py index a1b7bf597..730989903 100644 --- a/examples/rl/workflows/agent_wrapper.py +++ b/examples/rl/workflows/agent_wrapper.py @@ -4,7 +4,7 @@ import sys from os.path import dirname, realpath -from maro.rl.learning import AgentWrapper +from maro.rl.wrappers import AgentWrapper workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: @@ -13,11 +13,8 @@ from general import agent2policy, non_rl_policy_func_index, rl_policy_func_index -def get_agent_wrapper(rollout_only: bool = False): +def get_agent_wrapper(): return AgentWrapper( - { - **{name: func() for name, func in non_rl_policy_func_index.items()}, - **{name: func(rollout_only=rollout_only) for name, func in rl_policy_func_index.items()} - }, + {**non_rl_policy_func_index, **rl_policy_func_index}, agent2policy ) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index fbd935236..58d4293cb 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -17,15 +17,18 @@ sync: rollout_mode: multi-node # single-process, multi-process, multi-node num_rollout_workers: 3 min_finished_workers: 2 - max_extra_recv_tries: 0 + max_extra_recv_tries: 2 extra_recv_timeout: 200 async: group: async num_actors: 3 policy_manager: - train_group: policy-manager - train_mode: multi-node # single-process, multi-process, multi-node - num_trainers: 2 + type: simple # simple, distributed + simple: + parallel: false + distributed: + learn_group: policy-manager + num_hosts: 2 redis: host: maro-redis port: 6379 diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 66df5533a..097b6243e 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -22,10 +22,8 @@ rl_policy_func_index = getattr(module, "rl_policy_func_index") agent2policy = getattr(module, "agent2policy") rl_agents = [agent_id for agent_id, policy_id in agent2policy.items() if policy_id in rl_policy_func_index] -update_option = getattr(module, "update_option") post_collect = getattr(module, "post_collect", None) post_evaluate = getattr(module, "post_evaluate", None) -post_update = getattr(module, "post_update", None) # roll-out experience distribution amongst workers mode = getenv("MODE") diff --git a/examples/rl/workflows/policy_manager/trainer.py b/examples/rl/workflows/policy_manager/policy_host.py similarity index 57% rename from examples/rl/workflows/policy_manager/trainer.py rename to examples/rl/workflows/policy_manager/policy_host.py index 623f7ddd7..fcaa503b6 100644 --- a/examples/rl/workflows/policy_manager/trainer.py +++ b/examples/rl/workflows/policy_manager/policy_host.py @@ -5,25 +5,24 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.policy import trainer_node +from maro.rl.learning import policy_host workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import log_dir, rl_policy_func_index, update_option +from general import log_dir, rl_policy_func_index if __name__ == "__main__": - trainer_id = getenv("TRAINERID") - if trainer_id is None: - raise ValueError("missing environment variable: TRAINERID") + host_id = getenv("HOSTID") + if host_id is None: + raise ValueError("missing environment variable: HOSTID") - trainer_node( - getenv("TRAINGROUP", default="TRAIN"), - int(trainer_id), + policy_host( rl_policy_func_index, - num_epochs={policy_name: opt.num_epochs for policy_name, opt in update_option.items()}, + int(host_id), + getenv("LEARNGROUP", default="LEARN"), proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py index caa5f505c..1ce757565 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -5,35 +5,27 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.policy import LocalPolicyManager, MultiNodePolicyManager, MultiProcessPolicyManager +from maro.rl.learning import DistributedPolicyManager, SimplePolicyManager workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import log_dir, rl_policy_func_index, update_option +from general import log_dir, rl_policy_func_index def get_policy_manager(): - train_mode = getenv("TRAINMODE", default="single-process") - policy_dict = {name: func(rollout_only=False) for name, func in rl_policy_func_index.items()} - if train_mode == "single-process": - return LocalPolicyManager(policy_dict, update_option, log_dir=log_dir) + manager_type = getenv("POLICYMANAGERTYPE", default="simple") + parallel = getenv("PARALLEL", default=False) + print("parallel: ", parallel) + if manager_type == "simple": + return SimplePolicyManager(rl_policy_func_index, parallel=parallel, log_dir=log_dir) - num_trainers = int(getenv("NUMTRAINERS", default=5)) - if train_mode == "multi-process": - return MultiProcessPolicyManager( - policy_dict, - update_option, - num_trainers, - rl_policy_func_index, - log_dir=log_dir - ) - if train_mode == "multi-node": - return MultiNodePolicyManager( - policy_dict, - update_option, - getenv("TRAINGROUP", default="TRAIN"), - num_trainers, + num_hosts = int(getenv("NUMHOSTS", default=5)) + if manager_type == "distributed": + return DistributedPolicyManager( + list(rl_policy_func_index.keys()), + getenv("NODEGROUP", default="TRAIN"), + num_hosts, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 @@ -41,6 +33,4 @@ def get_policy_manager(): log_dir=log_dir ) - raise ValueError( - f"Unsupported policy training mode: {train_mode}. Supported modes: single-process, multi-process, multi-node" - ) + raise ValueError(f"Unsupported policy manager type: {manager_type}. Supported modes: simple, distributed") diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 103dcefd0..269beb3f8 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -26,7 +26,7 @@ getenv("ROLLOUTGROUP", default="ROLLOUT"), worker_id, get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), - get_agent_wrapper(rollout_only=True), + get_agent_wrapper(), eval_env_wrapper=get_eval_env_wrapper(), proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), diff --git a/maro/rl/algorithms/__init__.py b/maro/rl/algorithms/__init__.py deleted file mode 100644 index d37dcc0e3..000000000 --- a/maro/rl/algorithms/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .abs_algorithm import AbsAlgorithm -from .ac import ActorCritic -from .ddpg import DDPG -from .dqn import DQN -from .pg import PolicyGradient -from .index import get_algorithm_cls, get_algorithm_model_cls - -__all__ = [ - "AbsAlgorithm", "ActorCritic", "DDPG", "DQN", "PolicyGradient", "get_algorithm_cls", "get_algorithm_model_cls" -] diff --git a/maro/rl/algorithms/ac.py b/maro/rl/algorithms/ac.py deleted file mode 100644 index 005399476..000000000 --- a/maro/rl/algorithms/ac.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import namedtuple -from operator import index -from typing import Tuple - -import numpy as np -import torch -from torch.distributions import Categorical - -from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceBatch -from maro.rl.model import DiscreteACNet -from maro.rl.utils import get_torch_loss_cls - -ACLossInfo = namedtuple("ACLossInfo", ["actor_loss", "critic_loss", "entropy", "total_loss", "grad"]) - - -class ActorCritic(AbsAlgorithm): - """Actor Critic algorithm with separate policy and value models. - - References: - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f - - Args: - ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. - reward_discount (float): Reward decay as defined in standard RL terminology. - critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing - the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - min_logp (float): Lower bound for clamping logP values during learning. This is to prevent logP from becoming - very large in magnitude and cuasing stability issues. Defaults to None, which means no lower bound. - critic_loss_coeff (float): Coefficient for critic loss in total loss. Defaults to 1.0. - entropy_coeff (float): Coefficient for the entropy term in total loss. Defaults to None, in which case the - total loss will not include an entropy term. - clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, - in which case the actor loss is calculated using the usual policy gradient theorem. - """ - - def __init__( - self, - ac_net: DiscreteACNet, - reward_discount: float, - critic_loss_cls="mse", - min_logp: float = None, - critic_loss_coeff: float = 1.0, - entropy_coeff: float = None, - clip_ratio: float = None - ): - if not isinstance(ac_net, DiscreteACNet): - raise TypeError("model must be an instance of 'DiscreteACNet'") - - super().__init__() - self.ac_net = ac_net - self.reward_discount = reward_discount - self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() - self.min_logp = min_logp - self.critic_loss_coeff = critic_loss_coeff - self.entropy_coeff = entropy_coeff - self.clip_ratio = clip_ratio - - self.device = self.ac_net.device - - def choose_action(self, states) -> Tuple[np.ndarray, np.ndarray]: - """Return actions and log probabilities for given states.""" - self.ac_net.eval() - with torch.no_grad(): - actions, log_p = self.ac_net.get_action(states) - actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() - return (actions[0], log_p[0]) if len(actions) == 1 else (actions, log_p) - - def apply(self, grad_dict: dict): - """Apply gradients to the underlying parameterized model.""" - self.ac_net.apply(grad_dict) - - def learn(self, batch: ExperienceBatch, inplace: bool = True): - assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." - self.ac_net.train() - states, next_states = batch.data.states, batch.data.next_states - actions = torch.from_numpy(np.asarray([act[0] for act in batch.data.actions])).to(self.device) - log_p = torch.from_numpy(np.asarray([act[1] for act in batch.data.actions])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch.data.rewards)).to(self.device) - - action_probs, state_values = self.ac_net(states) - state_values = state_values.squeeze() - with torch.no_grad(): - next_state_values = self.ac_net(next_states, actor=False)[1].detach().squeeze() - return_est = rewards + self.reward_discount * next_state_values - advantages = return_est - state_values - - # actor loss - log_p_new = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - log_p_new = torch.clamp(log_p_new, min=self.min_logp, max=.0) - if self.clip_ratio is not None: - ratio = torch.exp(log_p_new - log_p) - clip_ratio = torch.clamp(ratio, 1 - self.clip_ratio, 1 + self.clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clip_ratio * advantages)).mean() - else: - actor_loss = -(log_p_new * advantages).mean() - - # critic_loss - critic_loss = self.critic_loss_func(state_values, return_est) - - # entropy - if self.entropy_coeff is not None: - entropy = -Categorical(action_probs).entropy().mean() - else: - entropy = 0 - - total_loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy - - if inplace: - self.ac_net.step(total_loss) - grad = None - else: - grad = self.ac_net.get_gradients(total_loss) - - return ACLossInfo(actor_loss, critic_loss, entropy, total_loss, grad), batch.indexes - - def set_state(self, policy_state): - self.ac_net.load_state_dict(policy_state) - - def get_state(self): - return self.ac_net.state_dict() diff --git a/maro/rl/algorithms/ddpg.py b/maro/rl/algorithms/ddpg.py deleted file mode 100644 index 3bb9e8b6c..000000000 --- a/maro/rl/algorithms/ddpg.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import namedtuple -from typing import Union - -import numpy as np -import torch - -from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceBatch -from maro.rl.exploration import GaussianNoiseExploration -from maro.rl.model import ContinuousACNet -from maro.rl.utils import get_torch_loss_cls - -DDPGLossInfo = namedtuple("DDPGLossInfo", ["policy_loss", "q_loss", "total_loss", "grad"]) - - -class DDPG(AbsAlgorithm): - """The Deep Deterministic Policy Gradient (DDPG) algorithm. - - References: - https://arxiv.org/pdf/1509.02971.pdf - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg - - Args: - ac_net (ContinuousACNet): DDPG policy and q-value models. - reward_discount (float): Reward decay as defined in standard RL terminology. - update_target_every (int): Number of training rounds between policy target model updates. - q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for - the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - q_value_loss_coeff (float): Coefficient for policy loss in the total loss function, e.g., - loss = policy_loss + ``q_value_loss_coeff`` * q_value_loss. Defaults to 1.0. - soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + - (1-soft_update_coeff) * target_model. Defaults to 1.0. - """ - def __init__( - self, - ac_net: ContinuousACNet, - reward_discount: float, - update_target_every: int, - q_value_loss_cls="mse", - q_value_loss_coeff: float = 1.0, - soft_update_coeff: float = 1.0, - exploration=GaussianNoiseExploration(), - ): - if not isinstance(ac_net, ContinuousACNet): - raise TypeError("model must be an instance of 'ContinuousACNet'") - - super().__init__(exploration=exploration) - self.ac_net = ac_net - if self.ac_net.trainable: - self.target_ac_net = ac_net.copy() - self.target_ac_net.eval() - else: - self.target_ac_net = None - self.reward_discount = reward_discount - self.update_target_every = update_target_every - self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() - self.q_value_loss_coeff = q_value_loss_coeff - self.soft_update_coeff = soft_update_coeff - self.device = self.ac_net.device - - self._ac_net_version = 0 - self._target_ac_net_version = 0 - - def choose_action(self, states, explore: bool = False) -> Union[float, np.ndarray]: - self.ac_net.eval() - with torch.no_grad(): - actions = self.ac_net.get_action(states).cpu().numpy() - - if explore: - actions = self.exploration(actions, state=states) - return actions[0] if len(actions) == 1 else actions - - def apply(self, grad_dict: dict): - self.ac_net.apply(grad_dict) - if self._ac_net_version - self._target_ac_net_version == self.update_target_every: - self._update_target() - - def learn(self, batch: ExperienceBatch, inplace: bool = True): - assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." - self.ac_net.train() - states, next_states = batch.data.states, batch.data.next_states - actual_actions = torch.from_numpy(batch.data.actions).to(self.device) - rewards = torch.from_numpy(batch.data.rewards).to(self.device) - if len(actual_actions.shape) == 1: - actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - - with torch.no_grad(): - next_q_values = self.target_ac_net.value(next_states) - target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) - - q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) - q_loss = self.q_value_loss_func(q_values, target_q_values) - policy_loss = -self.ac_net.value(states).mean() - - total_loss = policy_loss + self.q_value_loss_coeff * q_loss - - if inplace: - self.ac_net.step(total_loss) - grad = None - self._ac_net_version += 1 - if self._ac_net_version - self._target_ac_net_version == self.update_target_every: - self._update_target() - else: - grad = self.ac_net.get_gradients(total_loss) - - return DDPGLossInfo(policy_loss, q_loss, total_loss, grad), batch.indexes - - def _update_target(self): - # soft-update target network - self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) - self._target_ac_net_version = self._ac_net_version - - def set_state(self, policy_state): - self.ac_net.load_state_dict(policy_state) - self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None - - def get_state(self): - return self.ac_net.state_dict() diff --git a/maro/rl/algorithms/index.py b/maro/rl/algorithms/index.py deleted file mode 100644 index 6ef27e68d..000000000 --- a/maro/rl/algorithms/index.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .ac import ActorCritic, DiscreteACNet -from .ddpg import DDPG, ContinuousACNet -from .dqn import DQN, DiscreteQNet -from .pg import DiscretePolicyNet, PolicyGradient - -ALGORITHM_INDEX = { - "ac": ActorCritic, - "dqn": DQN, - "ddpg": DDPG, - "pg": PolicyGradient -} - - -ALGORITHM_MODEL_INDEX = { - "ac": DiscreteACNet, - "dqn": DiscreteQNet, - "ddpg": ContinuousACNet, - "pg": DiscretePolicyNet -} - - -def get_algorithm_cls(algorithm_type): - if isinstance(algorithm_type, str): - if algorithm_type not in ALGORITHM_INDEX: - raise KeyError(f"A string algorithm_type must be one of {list(ALGORITHM_INDEX.keys())}.") - return ALGORITHM_INDEX[algorithm_type] - - return algorithm_type - - -def get_algorithm_model_cls(algorithm_model_type): - if isinstance(algorithm_model_type, str): - if algorithm_model_type not in ALGORITHM_MODEL_INDEX: - raise KeyError(f"A string algorithm_model_type must be one of {list(ALGORITHM_MODEL_INDEX.keys())}.") - return ALGORITHM_MODEL_INDEX[algorithm_model_type] - - return algorithm_model_type diff --git a/maro/rl/algorithms/pg.py b/maro/rl/algorithms/pg.py deleted file mode 100644 index 9959ff31c..000000000 --- a/maro/rl/algorithms/pg.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import namedtuple -from typing import Tuple - -import numpy as np -import torch - -from maro.rl.algorithms import AbsAlgorithm -from maro.rl.experience import ExperienceBatch -from maro.rl.model import DiscretePolicyNet -from maro.rl.utils import get_truncated_cumulative_reward - -PGLossInfo = namedtuple("PGLossInfo", ["loss", "grad"]) - - -class PolicyGradient(AbsAlgorithm): - """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. - - Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - - Args: - policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. - It may or may not have a shared bottom stack. - reward_discount (float): Reward decay as defined in standard RL terminology. - """ - def __init__(self, policy_net: DiscretePolicyNet, reward_discount: float): - if not isinstance(policy_net, DiscretePolicyNet): - raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__() - self.policy_net = policy_net - self.reward_discount = reward_discount - self._num_learn_calls = 0 - self.device = self.policy_net.device - - def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """Return actions and log probabilities for given states.""" - self.policy_net.eval() - with torch.no_grad(): - actions, log_p = self.policy_net.get_action(states) - actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() - return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - - def apply(self, grad_dict: dict): - """Apply gradients to the underlying parameterized model.""" - self.policy_net.apply(grad_dict) - - def learn(self, batch: ExperienceBatch, inplace: bool = True): - """ - This should be called at the end of a simulation episode and the experiences obtained from - the experience store's ``get`` method should be a sequential set, i.e., in the order in - which they are generated during the simulation. Otherwise, the return values may be meaningless. - """ - assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." - self.policy_net.train() - log_p = torch.from_numpy(np.asarray([act[1] for act in batch.data.actions])).to(self.device) - rewards = torch.from_numpy(np.asarray(batch.data.rewards)).to(self.device) - returns = get_truncated_cumulative_reward(rewards, self.reward_discount) - returns = torch.from_numpy(returns).to(self.device) - loss = -(log_p * returns).mean() - - if inplace: - self.policy_net.step(loss) - grad = None - else: - grad = self.policy_net.get_gradients(loss) - - return PGLossInfo(loss, grad), batch.indexes - - def set_state(self, policy_state): - self.policy_net.load_state_dict(policy_state) - - def get_state(self): - return self.policy_net.state_dict() diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index 1fff5614b..438d3de2e 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -1,9 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .agent_wrapper import AgentWrapper from .early_stopper import AbsEarlyStopper -from .env_wrapper import AbsEnvWrapper, Transition +from .policy_host import policy_host +from .policy_manager import AbsPolicyManager, SimplePolicyManager, DistributedPolicyManager from .simple_learner import SimpleLearner -__all__ = ["AbsEarlyStopper", "AbsEnvWrapper", "AgentWrapper", "SimpleLearner", "Transition"] +__all__ = [ + "AbsEarlyStopper", + "policy_host", + "AbsPolicyManager", "SimplePolicyManager", "DistributedPolicyManager", + "SimpleLearner" +] diff --git a/maro/rl/learning/asynchronous/actor.py b/maro/rl/learning/asynchronous/actor.py index d707ff7a3..cc7bbc3f4 100644 --- a/maro/rl/learning/asynchronous/actor.py +++ b/maro/rl/learning/asynchronous/actor.py @@ -7,11 +7,9 @@ from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.utils import MsgKey, MsgTag +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger -from ..agent_wrapper import AgentWrapper -from ..env_wrapper import AbsEnvWrapper - def actor( group: str, @@ -112,7 +110,7 @@ def actor( reply = proxy.send( SessionMessage( MsgTag.COLLECT_DONE, proxy.name, policy_server_address, - body={MsgKey.EXPERIENCES: exp_by_policy, MsgKey.VERSION: policy_version} + body={MsgKey.ROLLOUT_INFO: exp_by_policy, MsgKey.VERSION: policy_version} ) )[0] policy_version = reply.body[MsgKey.VERSION] diff --git a/maro/rl/learning/asynchronous/policy_server.py b/maro/rl/learning/asynchronous/policy_server.py index 5a24debcc..491c7b1bb 100644 --- a/maro/rl/learning/asynchronous/policy_server.py +++ b/maro/rl/learning/asynchronous/policy_server.py @@ -4,10 +4,11 @@ from os import getcwd from maro.communication import Proxy -from maro.rl.policy import AbsPolicyManager from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger +from ..policy_manager import AbsPolicyManager + def policy_server( group: str, @@ -53,7 +54,7 @@ def policy_server( f"{policy_manager.version - max_lag}, got {msg.body[MsgKey.VERSION]}" ) else: - policy_manager.update(msg.body[MsgKey.EXPERIENCES]) + policy_manager.update(msg.body[MsgKey.ROLLOUT_INFO]) proxy.reply( msg, tag=MsgTag.POLICY_STATE, body={ diff --git a/maro/rl/learning/policy_host.py b/maro/rl/learning/policy_host.py new file mode 100644 index 000000000..a6d5cd238 --- /dev/null +++ b/maro/rl/learning/policy_host.py @@ -0,0 +1,69 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from os import getcwd +from typing import Callable, Dict + +from maro.communication import Proxy +from maro.rl.policy import LossInfo, RLPolicy +from maro.rl.typing import Trajectory +from maro.rl.utils import MsgKey, MsgTag +from maro.utils import Logger + + +def policy_host( + create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], + host_idx: int, + group: str, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + """Policy host process that can be launched on separate computation nodes. + + Args: + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. + host_idx (int): Integer host index. The host's ID in the cluster will be "POLICY_HOST.{host_idx}". + group (str): Group name for the training cluster, which includes all policy hosts and a policy manager that + manages them. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + policy_dict = {} + proxy = Proxy(group, "policy_host", {"policy_manager": 1}, component_name=f"POLICY_HOST.{host_idx}", **proxy_kwargs) + logger = Logger(proxy.name, dump_folder=log_dir) + + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.INIT_POLICIES: + for name in msg.body[MsgKey.POLICY_NAMES]: + policy_dict[name] = create_policy_func_dict[name](name) + + proxy.reply( + msg, + tag=MsgTag.INIT_POLICIES_DONE, + body={MsgKey.POLICY_STATE: {name: policy.get_state() for name, policy in policy_dict.items()}} + ) + elif msg.tag == MsgTag.LEARN: + t0 = time.time() + for name, info_list in msg.body[MsgKey.ROLLOUT_INFO].items(): + if isinstance(info_list[0], Trajectory): + policy_dict[name].learn_from_multi_trajectories(info_list) + elif isinstance(info_list[0], LossInfo): + policy_dict[name].apply(info_list) + else: + raise TypeError( + f"Roll-out information must be of type 'Trajectory' or 'LossInfo', " + f"got {type(info_list[0])}" + ) + msg_body = { + MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.TRAJECTORIES]} + } + logger.debug(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, tag=MsgTag.LEARN_DONE, body=msg_body) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 34efc62aa..7710930c2 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -6,16 +6,14 @@ from collections import defaultdict from multiprocessing import Pipe, Process from os import getcwd -from typing import Callable, Dict +from typing import Callable, Dict, List from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.experience import ExperienceSet -from maro.rl.policy import AbsCorePolicy +from maro.rl.policy import LossInfo, RLPolicy +from maro.rl.typing import Trajectory from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger -from .trainer import trainer_process - class AbsPolicyManager(ABC): """Manage all policies. @@ -23,243 +21,147 @@ class AbsPolicyManager(ABC): The actual policy instances may reside here or be distributed on a set of processes or remote nodes. Args: - policy_dict (Dict[str, AbsCorePolicy]): A list of policies managed by the manager. - update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the - required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, - all triggers will be set to 1. - warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the - minimum number of experiences in the experience memory required to trigger a call to ``learn`` for - each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, - ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to - None. + policies (List[RLPolicy]): A list of ``RLPolicy`` instances. """ - def __init__( - self, - policy_dict: Dict[str, AbsCorePolicy], - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - post_update: Callable = None - ): - for policy in policy_dict.values(): - if not isinstance(policy, AbsCorePolicy): - raise ValueError("Only 'AbsCorePolicy' instances can be managed by a policy manager.") - + def __init__(self): super().__init__() - self.policy_dict = policy_dict - if not update_trigger: - self.update_trigger = {name: 1 for name in self.policy_dict} - else: - self.update_trigger = update_trigger - if not warmup: - self.warmup = {name: 1 for name in self.policy_dict} - else: - self.warmup = warmup - - self._post_update = post_update - - self._update_history = [set(policy_dict.keys())] - self.tracker = {} + self.update_count = 0 @property def version(self): - return len(self._update_history) - 1 + return self.update_count @abstractmethod - def update(self, exp_by_policy: Dict[str, ExperienceSet]): + def update(self, rollout_info: Dict[str, list]): """Logic for handling incoming experiences is implemented here.""" raise NotImplementedError - def get_state(self, cur_version: int = None): - if cur_version is None: - cur_version = self.version - 1 - updated = set() - for version in range(cur_version + 1, len(self._update_history)): - updated |= self._update_history[version] - return {name: self.policy_dict[name].get_state() for name in updated} + @abstractmethod + def get_state(self): + raise NotImplementedError -class LocalPolicyManager(AbsPolicyManager): +class SimplePolicyManager(AbsPolicyManager): """Policy manager that contains the actual policy instances. Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. - update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the - required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, - all triggers will be set to 1. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, - ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to - None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( self, - policy_dict: Dict[str, AbsCorePolicy], - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - post_update: Callable = None, + create_policy_func_dict: Dict[str, Callable], + parallel: bool = False, log_dir: str = getcwd() ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) - self._new_exp_counter = defaultdict(int) - self._logger = Logger("LOCAL_POLICY_MANAGER", dump_folder=log_dir) + super().__init__() + self._policy_names = list(create_policy_func_dict.keys()) + self._parallel = parallel + self._logger = Logger("SIMPLE_POLICY_MANAGER", dump_folder=log_dir) + if self._parallel: + self._logger.info("Spawning policy host processes") + self._state_cache = {} + self._policy_hosts = [] + self._manager_end = {} + + def _policy_host(name, create_policy_func, conn): + policy = create_policy_func(name) + conn.send({"type": "init", "policy_state": policy.get_state()}) + while True: + msg = conn.recv() + if msg["type"] == "learn": + info_list = msg["rollout_info"] + if isinstance(info_list[0], Trajectory): + policy.learn_from_multi_trajectories(info_list) + elif isinstance(info_list[0], LossInfo): + policy.apply(info_list) + else: + raise TypeError( + f"Roll-out information must be of type 'Trajectory' or 'LossInfo', " + f"got {type(info_list[0])}" + ) + conn.send({"type": "learn_done", "policy_state": policy.get_state()}) + elif msg["type"] == "quit": + break + + for name, create_policy_func in create_policy_func_dict.items(): + manager_end, policy_end = Pipe() + self._manager_end[name] = manager_end + host = Process(target=_policy_host, args=(name, create_policy_func, policy_end)) + self._policy_hosts.append(host) + host.start() + + for policy_name, conn in self._manager_end.items(): + msg = conn.recv() + if msg["type"] == "init": + self._state_cache[policy_name] = msg["policy_state"] + else: + self._logger.info("local mode") + self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} - def update(self, exp_by_policy: Dict[str, ExperienceSet]): + def update(self, rollout_info: Dict[str, list]): """Store experiences and update policies if possible. The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding policy's experience manager. Policies whose update conditions have been met will then be updated. """ - t0 = time.time() - updated = set() - for policy_name, exp in exp_by_policy.items(): - policy = self.policy_dict[policy_name] - policy.replay_memory.put(exp) - self._new_exp_counter[policy_name] += exp.size - if ( - self._new_exp_counter[policy_name] >= self.update_trigger[policy_name] and - policy.replay_memory.size >= self.warmup[policy_name] - ): - policy.learn() - updated.add(policy_name) - self._new_exp_counter[policy_name] = 0 - - if updated: - self._update_history.append(updated) - self._logger.info(f"Updated policies {updated}") - - if self._post_update: - self._post_update([policy.tracker for policy in self.policy_dict.values()]) - + self._logger.info(f"parallel: {self._parallel}") + if self._parallel: + self._logger.info("I'm parallel") + self._logger.info(f"received rollout info from policies {list(rollout_info.keys())}") + for policy_name, info_list in rollout_info.items(): + self._manager_end[policy_name].send({"type": "train", "rollout_info": info_list}) + for policy_name, conn in self._manager_end.items(): + msg = conn.recv() + if msg["type"] == "learn_done": + self._state_cache[policy_name] = msg["policy_state"] + else: + self._logger.info("I'm NOT parallel") + self._logger.info(f"received rollout info from policies {list(rollout_info.keys())}") + t0 = time.time() + for policy_name, info_list in rollout_info.items(): + if isinstance(info_list[0], Trajectory): + self._logger.info("learning from multiple trajectories") + self._policy_dict[policy_name].learn_from_multi_trajectories(info_list) + elif isinstance(info_list[0], LossInfo): + self._logger.info("learning from loss info") + self._policy_dict[policy_name].apply(info_list) + + self._logger.info(f"Updated policies {list(rollout_info.keys())}") + + self.update_count += 1 self._logger.debug(f"policy update time: {time.time() - t0}") - -class MultiProcessPolicyManager(AbsPolicyManager): - """Policy manager that spawns a set of trainer processes for parallel training. - - Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. - num_trainers (int): Number of trainer processes to be forked. - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. - update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the - required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, - all triggers will be set to 1. - warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the - minimum number of experiences in the experience memory required to trigger a call to ``learn`` for - each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers,) - -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - policy_dict: Dict[str, AbsCorePolicy], - num_trainers: int, - create_policy_func_dict: Dict[str, Callable], - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - post_update: Callable = None, - log_dir: str = getcwd(), - ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) - self._policy2trainer = {} - self._trainer2policies = defaultdict(list) - self._exp_cache = defaultdict(ExperienceSet) - self._num_experiences_by_policy = defaultdict(int) - - for i, name in enumerate(self.policy_dict): - trainer_id = i % num_trainers - self._policy2trainer[name] = f"TRAINER.{trainer_id}" - self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) - - self._logger = Logger("MULTIPROCESS_POLICY_MANAGER", dump_folder=log_dir) - - self._trainer_processes = [] - self._manager_end = {} - for trainer_id, policy_names in self._trainer2policies.items(): - manager_end, trainer_end = Pipe() - self._manager_end[trainer_id] = manager_end - trainer = Process( - target=trainer_process, - args=( - trainer_id, - trainer_end, - {name: create_policy_func_dict[name] for name in policy_names}, - {name: self.policy_dict[name].get_state() for name in self._trainer2policies[trainer_id]} - ), - kwargs={"log_dir": log_dir} - ) - self._trainer_processes.append(trainer) - trainer.start() - - def update(self, exp_by_policy: Dict[str, ExperienceSet]): - exp_to_send, updated = {}, set() - for policy_name, exp in exp_by_policy.items(): - self._num_experiences_by_policy[policy_name] += exp.size - self._exp_cache[policy_name].extend(exp) - if ( - self._exp_cache[policy_name].size >= self.update_trigger[policy_name] and - self._num_experiences_by_policy[policy_name] >= self.warmup[policy_name] - ): - exp_to_send[policy_name] = self._exp_cache.pop(policy_name) - updated.add(policy_name) - - for trainer_id, conn in self._manager_end.items(): - conn.send({ - "type": "train", - "experiences": {name: exp_to_send[name] for name in self._trainer2policies[trainer_id]} - }) - - trackers = [] - for conn in self._manager_end.values(): - result = conn.recv() - trackers.append(result["tracker"]) - for policy_name, policy_state in result["policy"].items(): - self.policy_dict[policy_name].set_state(policy_state) - - if updated: - self._update_history.append(updated) - self._logger.info(f"Updated policies {updated}") - - if self._post_update: - self._post_update(trackers) + def get_state(self): + if self._parallel: + return self._state_cache + else: + return {name: policy.get_state() for name, policy in self._policy_dict.items()} def exit(self): - """Tell the trainer processes to exit.""" - for conn in self._manager_end.values(): - conn.send({"type": "quit"}) + """Tell the policy hosts to exit.""" + if self._parallel: + for conn in self._manager_end.values(): + conn.send({"type": "quit"}) -class MultiNodePolicyManager(AbsPolicyManager): +class DistributedPolicyManager(AbsPolicyManager): """Policy manager that communicates with a set of remote nodes for parallel training. Args: - policy_dict (Dict[str, AbsCorePolicy]): Policies managed by the manager. - group (str): Group name for the training cluster, which includes all trainers and a training manager that + policy_dict (Dict[str, RLPolicy]): Policies managed by the manager. + group (str): Group name for the training cluster, which includes all hosts and a policy manager that manages them. - num_trainers (int): Number of trainers. The trainers will be identified by "TRAINER.i", where - 0 <= i < num_trainers. - update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the - required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, - all triggers will be set to 1. + num_hosts (int): Number of hosts. The hosts will be identified by "POLICY_HOST.i", where 0 <= i < num_hosts. warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the minimum number of experiences in the experience memory required to trigger a call to ``learn`` for each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers,) - -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -268,74 +170,69 @@ class MultiNodePolicyManager(AbsPolicyManager): """ def __init__( self, - policy_dict: Dict[str, AbsCorePolicy], + policy_names: List[str], group: str, - num_trainers: int, - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - post_update: Callable = None, + num_hosts: int, log_dir: str = getcwd(), proxy_kwargs: dict = {} ): - super().__init__(policy_dict, update_trigger=update_trigger, warmup=warmup, post_update=post_update) - peers = {"trainer": num_trainers} + super().__init__() + self._policy_names = policy_names + peers = {"policy_host": num_hosts} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) + self._logger = Logger("MULTINODE_POLICY_MANAGER", dump_folder=log_dir) - self._policy2trainer = {} - self._trainer2policies = defaultdict(list) - self._exp_cache = defaultdict(ExperienceSet) - self._num_experiences_by_policy = defaultdict(int) - + self._policy2host = {} + self._host2policies = defaultdict(list) self._logger = Logger("MULTINODE_POLICY_MANAGER", dump_folder=log_dir) - for i, name in enumerate(self.policy_dict): - trainer_id = i % num_trainers - self._policy2trainer[name] = f"TRAINER.{trainer_id}" - self._trainer2policies[f"TRAINER.{trainer_id}"].append(name) - - self._logger.info("Initializing policy states on trainers...") - for trainer_name, policy_names in self._trainer2policies.items(): - self._proxy.send( - SessionMessage( - MsgTag.INIT_POLICY_STATE, self._proxy.name, trainer_name, - body={MsgKey.POLICY_STATE: {name: self.policy_dict[name].get_state() for name in policy_names}} - ) - ) - - def update(self, exp_by_policy: Dict[str, ExperienceSet]): - exp_to_send, updated = {}, set() - for policy_name, exp in exp_by_policy.items(): - self._num_experiences_by_policy[policy_name] += exp.size - self._exp_cache[policy_name].extend(exp) - if ( - self._exp_cache[policy_name].size >= self.update_trigger[policy_name] and - self._num_experiences_by_policy[policy_name] >= self.warmup[policy_name] - ): - exp_to_send[policy_name] = self._exp_cache.pop(policy_name) - updated.add(policy_name) - - msg_body_by_dest = defaultdict(dict) - for policy_name, exp in exp_to_send.items(): - trainer_id = self._policy2trainer[policy_name] - if MsgKey.EXPERIENCES not in msg_body_by_dest[trainer_id]: - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES] = {} - msg_body_by_dest[trainer_id][MsgKey.EXPERIENCES][policy_name] = exp - - trackers = [] - for reply in self._proxy.scatter(MsgTag.LEARN, SessionType.TASK, list(msg_body_by_dest.items())): - trackers.append(reply.body[MsgKey.TRACKER]) - for policy_name, policy_state in reply.body[MsgKey.POLICY_STATE].items(): - self.policy_dict[policy_name].set_state(policy_state) - - if updated: - self._update_history.append(updated) - self._logger.info(f"Updated policies {updated}") - - if self._post_update: - self._post_update(trackers) + # assign policies to hosts + for i, name in enumerate(self._policy_names): + host_id = i % num_hosts + self._policy2host[name] = f"POLICY_HOST.{host_id}" + self._host2policies[f"POLICY_HOST.{host_id}"].append(name) + + # ask the hosts to initialize the assigned policies + for host_name, policy_names in self._host2policies.items(): + self._proxy.send(SessionMessage( + MsgTag.INIT_POLICIES, self._proxy.name, host_name, body={MsgKey.POLICY_NAMES: policy_names} + )) + + # cache the initial policy states + self._state_cache, dones = {}, 0 + for msg in self._proxy.receive(): + if msg.tag == MsgTag.INIT_POLICIES_DONE: + for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): + self._state_cache[policy_name] = policy_state + self._logger.info(f"Policy {policy_name} initialized") + dones += 1 + if dones == num_hosts: + break + + def update(self, rollout_info: Dict[str, list]): + msg_dict = defaultdict(lambda: defaultdict(dict)) + for policy_name, info_list in rollout_info.items(): + host_id_str = self._policy2host[policy_name] + msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_name] = info_list + + dones = 0 + self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_dict.items())) + for msg in self._proxy.receive(): + if msg.tag == MsgTag.LEARN_DONE: + for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): + self._state_cache[policy_name] = policy_state + dones += 1 + if dones == len(msg_dict): + break + + self.update_count += 1 + self._logger.info(f"Updated policies {list(rollout_info.keys())}") + + def get_state(self): + return self._state_cache def exit(self): - """Tell the remote trainers to exit.""" - self._proxy.ibroadcast("trainer", MsgTag.EXIT, SessionType.NOTIFICATION) + """Tell the remote policy hosts to exit.""" + self._proxy.ibroadcast("policy_host", MsgTag.EXIT, SessionType.NOTIFICATION) self._proxy.close() self._logger.info("Exiting...") diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py index 389e42851..3d2e8cd17 100644 --- a/maro/rl/learning/simple_learner.py +++ b/maro/rl/learning/simple_learner.py @@ -5,12 +5,12 @@ from os import getcwd from typing import Callable, Dict, List, Union -from maro.rl.policy import AbsCorePolicy, LocalPolicyManager +from maro.rl.policy import RLPolicy +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger -from .agent_wrapper import AgentWrapper from .early_stopper import AbsEarlyStopper -from .env_wrapper import AbsEnvWrapper +from .policy_manager import SimplePolicyManager class SimpleLearner: @@ -81,8 +81,8 @@ def __init__( self.agent = agent_wrapper # Create a policy manager to manage all trainable policies from the agent wrapper - self.policy_manager = LocalPolicyManager( - {name: policy for name, policy in self.agent.policy_dict.items() if isinstance(policy, AbsCorePolicy)}, + self.policy_manager = SimplePolicyManager( + {name: policy for name, policy in self.agent.policy_dict.items() if isinstance(policy, RLPolicy)}, update_trigger=update_trigger, warmup=warmup, post_update=post_update diff --git a/maro/rl/learning/synchronous/learner.py b/maro/rl/learning/synchronous/learner.py index b7e56db44..8b32ce931 100644 --- a/maro/rl/learning/synchronous/learner.py +++ b/maro/rl/learning/synchronous/learner.py @@ -5,10 +5,10 @@ from os import getcwd from typing import List, Union -from maro.rl.policy import AbsPolicyManager from maro.utils import Logger from ..early_stopper import AbsEarlyStopper +from ..policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager @@ -91,7 +91,7 @@ def run(self): self.policy_manager.exit() def _collect_and_update(self, ep: int): - collect_time = policy_update_time = num_experiences_collected = 0 + collect_time = policy_update_time = 0 segment = 0 self.rollout_manager.reset() while not self.rollout_manager.episode_complete: @@ -100,17 +100,16 @@ def _collect_and_update(self, ep: int): policy_state_dict = self.policy_manager.get_state() policy_version = self.policy_manager.version tc0 = time.time() - exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) + rollout_info_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) collect_time += time.time() - tc0 + self._logger.info(f"collect time: {collect_time}") tu0 = time.time() - self.policy_manager.update(exp_by_policy) + self.policy_manager.update(rollout_info_by_policy) policy_update_time += time.time() - tu0 - num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) # performance details self._logger.info( f"ep {ep} summary - " - f"experiences collected: {num_experiences_collected} " - f"experience collection time: {collect_time} " + f"collect time: {collect_time} " f"policy update time: {policy_update_time}" ) diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 4f4774c43..6d1eb6256 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -9,12 +9,10 @@ from typing import Callable from maro.communication import Proxy, SessionType -from maro.rl.experience import ExperienceSet from maro.rl.utils import MsgKey, MsgTag +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger -from ..agent_wrapper import AgentWrapper -from ..env_wrapper import AbsEnvWrapper from .rollout_worker import rollout_worker_process @@ -287,14 +285,14 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if self._exploration_step: self._exploration_step = False - combined_exp_by_policy = defaultdict(ExperienceSet) + info_by_policy = defaultdict(list) trackers = [] for conn in self._manager_ends: result = conn.recv() - exp_by_policy = result["experiences"] + training_info = result["experiences"] trackers.append(result["tracker"]) - for policy_name, exp in exp_by_policy.items(): - combined_exp_by_policy[policy_name].extend(exp) + for policy_name, info in training_info.items(): + info_by_policy[policy_name].append(info) self.episode_complete = result["episode_end"] @@ -304,7 +302,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if self._post_collect: self._post_collect(trackers, ep, segment) - return combined_exp_by_policy + return info_by_policy def evaluate(self, ep: int, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``. @@ -441,15 +439,17 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): self._exploration_step = False # Receive roll-out results from remote workers - combined_exp_by_policy = defaultdict(ExperienceSet) + info_list_by_policy = defaultdict(list) trackers = [] num_finishes = 0 # Ensure the minimum number of worker results are received. for msg in self._proxy.receive(): - tracker = self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) - if tracker is not None: + info_by_policy, tracker = self._handle_worker_result(msg, ep, segment, version) + if info_by_policy: num_finishes += 1 + for policy_name, info in info_by_policy.items(): + info_list_by_policy[policy_name].append(info) trackers.append(tracker) if num_finishes == self._min_finished_workers: break @@ -460,9 +460,11 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if not msg: self._logger.info(f"Receive timeout, {self._max_extra_recv_tries - i - 1} attempts left") else: - tracker = self._handle_worker_result(msg, ep, segment, version, combined_exp_by_policy) - if tracker is not None: + info_by_policy, tracker = self._handle_worker_result(msg, ep, segment, version) + if info_by_policy: num_finishes += 1 + for policy_name, info in info_by_policy.items(): + info_list_by_policy[policy_name].append(info) trackers.append(tracker) if num_finishes == self._num_workers: break @@ -473,14 +475,14 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if self._post_collect: self._post_collect(trackers, ep, segment) - return combined_exp_by_policy + return info_list_by_policy - def _handle_worker_result(self, msg, ep, segment, version, combined_exp): + def _handle_worker_result(self, msg, ep, segment, version): if msg.tag != MsgTag.COLLECT_DONE: self._logger.info( f"Ignored a message of type {msg.tag} (expected message type {MsgTag.COLLECT_DONE})" ) - return + return None, None if version - msg.body[MsgKey.VERSION] > self._max_lag: self._logger.info( @@ -488,15 +490,14 @@ def _handle_worker_result(self, msg, ep, segment, version, combined_exp): f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " f"got {msg.body[MsgKey.VERSION]}" ) - return - - for policy_name, exp in msg.body[MsgKey.EXPERIENCES].items(): - combined_exp[policy_name].extend(exp) + return None, None # The message is what we expect if msg.body[MsgKey.EPISODE] == ep and msg.body[MsgKey.SEGMENT] == segment: self.episode_complete = msg.body[MsgKey.END_OF_EPISODE] - return msg.body[MsgKey.TRACKER] + return msg.body[MsgKey.ROLLOUT_INFO], msg.body[MsgKey.TRACKER] + + return None, None def evaluate(self, ep: int, policy_state_dict: dict): """Evaluate the performance of ``policy_state_dict``. diff --git a/maro/rl/learning/synchronous/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py index e834fedfa..af9cddb33 100644 --- a/maro/rl/learning/synchronous/rollout_worker.py +++ b/maro/rl/learning/synchronous/rollout_worker.py @@ -7,11 +7,9 @@ from maro.communication import Proxy from maro.rl.utils import MsgKey, MsgTag +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger, set_seeds -from ..agent_wrapper import AgentWrapper -from ..env_wrapper import AbsEnvWrapper - def rollout_worker_process( index: int, @@ -167,7 +165,7 @@ def collect(msg): MsgKey.EPISODE: ep, MsgKey.SEGMENT: segment, MsgKey.VERSION: msg.body[MsgKey.VERSION], - MsgKey.EXPERIENCES: agent_wrapper.get_batch(env_wrapper), + MsgKey.ROLLOUT_INFO: agent_wrapper.get_rollout_info(env_wrapper.get_trajectory()), MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1, MsgKey.TRACKER: env_wrapper.tracker, MsgKey.END_OF_EPISODE: not env_wrapper.state diff --git a/maro/rl/learning/trainer.py b/maro/rl/learning/trainer.py deleted file mode 100644 index 4b025dfaa..000000000 --- a/maro/rl/learning/trainer.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from multiprocessing.connection import Connection -from os import getcwd -from typing import Callable, Dict - -from maro.communication import Proxy -from maro.rl.utils import MsgKey, MsgTag -from maro.utils import Logger - - -def trainer_process( - trainer_id: int, - conn: Connection, - create_policy_func_dict: Dict[str, Callable], - initial_policy_states: dict, - log_dir: str = getcwd() -): - """Policy trainer process which can be spawned by a ``MultiProcessPolicyManager``. - - Args: - trainer_id (int): Integer trainer ID. - conn (Connection): Connection end for exchanging messages with the manager process. - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - policy_dict = {policy_name: func() for policy_name, func in create_policy_func_dict.items()} - logger = Logger("TRAINER", dump_folder=log_dir) - for name, state in initial_policy_states.items(): - policy_dict[name].set_state(state) - logger.info(f"{trainer_id} initialized policy {name}") - - while True: - msg = conn.recv() - if msg["type"] == "train": - t0 = time.time() - for name, exp in msg["experiences"].items(): - policy_dict[name].store(exp) - policy_dict[name].learn() - logger.debug(f"total policy update time: {time.time() - t0}") - conn.send({ - "policy": {name: policy_dict[name].get_state() for name in msg["experiences"]}, - "tracker": {name: policy_dict[name].tracker for name in msg["experiences"]} - }) - elif msg["type"] == "quit": - break - - -def trainer_node( - group: str, - trainer_idx: int, - create_policy_func_dict: Dict[str, Callable], - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Policy trainer process that can be launched on separate computation nodes. - - Args: - group (str): Group name for the training cluster, which includes all trainers and a training manager that - manages them. - trainer_idx (int): Integer trainer index. The trainer's ID in the cluster will be "TRAINER.{trainer_idx}". - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - policy_dict = {} - proxy = Proxy(group, "trainer", {"policy_manager": 1}, component_name=f"TRAINER.{trainer_idx}", **proxy_kwargs) - logger = Logger(proxy.name, dump_folder=log_dir) - - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.INIT_POLICY_STATE: - for name, state in msg.body[MsgKey.POLICY_STATE].items(): - policy_dict[name] = create_policy_func_dict[name]() - policy_dict[name].set_state(state) - logger.info(f"{proxy.name} initialized policy {name}") - proxy.reply(msg, tag=MsgTag.INIT_POLICY_STATE_DONE) - elif msg.tag == MsgTag.LEARN: - t0 = time.time() - for name, exp in msg.body[MsgKey.EXPERIENCES].items(): - policy_dict[name].store(exp) - policy_dict[name].learn() - - msg_body = { - MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.EXPERIENCES]}, - MsgKey.TRACKER: {name: policy_dict[name].tracker for name in msg.body[MsgKey.EXPERIENCES]} - } - logger.debug(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, body=msg_body) diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 4f714f1ea..a3926ddb3 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .core_model import AbsCoreModel, ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet, OptimOption +from .core_model import AbsCoreModel, OptimOption from .fc_block import FullyConnectedBlock __all__ = [ - "AbsCoreModel", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", "OptimOption", + "AbsCoreModel", "OptimOption", "FullyConnectedBlock", ] diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 82f1fde2d..69c5d4806 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -2,11 +2,11 @@ # Licensed under the MIT license. from abc import abstractmethod +from statistics import mean from typing import Dict, List, Union import torch import torch.nn as nn -from torch.distributions import Categorical from maro.rl.utils import get_torch_lr_scheduler_cls, get_torch_optim_cls from maro.utils import clone @@ -91,6 +91,35 @@ def trainable(self) -> bool: def forward(self, *args, **kwargs): raise NotImplementedError + def get_gradients(self, loss: torch.tensor): + """Compute gradients from a loss """ + if self.optimizer is None: + raise MissingOptimizer("No optimizer registered to the model") + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.zero_grad() + else: + self.optimizer.zero_grad() + + # Obtain gradients through back-propagation + loss.backward() + + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad_dict_list: List[Dict[str, float]]): + avg_grad_dict = { + param_name: mean(grad_dict[param_name] for grad_dict in grad_dict_list) for param_name in grad_dict_list[0] + } + for name, param in self.named_parameters(): + param.grad = avg_grad_dict[name] + + # Apply gradients + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.step() + else: + self.optimizer.step() + def step(self, loss): """Use the loss to back-propagate gradients and apply them to the underlying parameters. @@ -131,18 +160,6 @@ def update_learning_rate(self, component_name: Union[str, List[str]] = None): for sch in self.scheduler.values(): sch.step() - def soft_update(self, other_model: nn.Module, tau: float): - """Soft-update model parameters using another model. - - The update formulae is: param = (1 - tau) * param + tau * pther_param. - - Args: - other_model: The model to update the current model with. - tau (float): Soft-update coefficient. - """ - for params, other_params in zip(self.parameters(), other_model.parameters()): - params.data = (1 - tau) * params.data + tau * other_params.data - def copy(self, with_optimizer: bool = False, device: str = None): """Return a deep copy of the instance; @@ -161,200 +178,3 @@ def copy(self, with_optimizer: bool = False, device: str = None): model_copy.to(device) return model_copy - - -class DiscreteQNet(AbsCoreModel): - """Q-value model for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @abstractmethod - def forward(self, states) -> torch.tensor: - """Compute the Q-values for all actions as a tensor of shape (batch_size, action_space_size).""" - raise NotImplementedError - - def get_action(self, states): - """ - Given Q-values for a batch of states and all actions, return the action index and the corresponding - Q-values for each state. - """ - q_for_all_actions = self.forward(states) # (batch_size, num_actions) - greedy_q, actions = q_for_all_actions.max(dim=1) - return actions.detach(), greedy_q.detach(), q_for_all_actions.shape[1] - - def q_values(self, states, actions: torch.tensor): - """Return the Q-values for a batch of states and actions.""" - if len(actions.shape) == 1: - actions = actions.unsqueeze(dim=1) - q_for_all_actions = self.forward(states) # (batch_size, num_actions) - return q_for_all_actions.gather(1, actions).squeeze(dim=1) - - -class DiscretePolicyNet(AbsCoreModel): - """Parameterized policy for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @abstractmethod - def forward(self, states) -> torch.tensor: - """Compute action probabilities corresponding to each state in ``states``. - - The output must be a torch tensor with shape (batch_size, action_space_size). - - Args: - states: State batch to compute action probabilities for. - """ - raise NotImplementedError - - def get_action(self, states, max_prob: bool = False): - """ - Given a batch of states, return actions selected based on the probabilities computed by ``forward`` - and the corresponding log probabilities. - """ - action_prob = self.forward(states) # (batch_size, num_actions) - if max_prob: - prob, action = action_prob.max(dim=1) - return action, torch.log(prob) - else: - action_prob = Categorical(action_prob) # (batch_size, action_space_size) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - return action, log_p - - -class DiscreteACNet(AbsCoreModel): - """Model container for the actor-critic architecture for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @abstractmethod - def forward(self, states, actor: bool = True, critic: bool = True) -> tuple: - """Compute action probabilities and values for a state batch. - - The output is a tuple of (action_probs, values), where action probs is a tensor of shape - (batch_size, action_space_size) and values is a tensor of shape (batch_size,). If only one - of these two is needed, the return value for the other one can be set to None. - - Args: - states: State batch to compute action probabilities and values for. - actor (bool): If True, the first element of the output will be actin probabilities. Defaults to True. - critic (bool): If True, the second element of the output will be state values. Defaults to True. - """ - raise NotImplementedError - - def get_action(self, states, max_prob: bool = False): - """ - Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value - for each state. - """ - action_prob = self.forward(states, critic=False)[0] - if max_prob: - prob, action = action_prob.max(dim=1) - return action, torch.log(prob) - else: - action_prob = Categorical(action_prob) # (batch_size, action_space_size) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - return action, log_p - - -class ContinuousACNet(AbsCoreModel): - """Model container for the actor-critic architecture for continuous action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @abstractmethod - def forward(self, states, actions=None): - """Compute actions for a batch of states or Q-values for a batch of states and actions. - - Args: - states: State batch to compute the Q-values for. - actions: Action batch. If None, the output should be a batch of actions corresponding to - the state batch. Otherwise, the output should be the Q-values for the given states and - actions. Defaults to None. - """ - raise NotImplementedError - - def get_action(self, states): - """Compute actions given a batch of states.""" - return self.forward(states) - - def value(self, states): - """Compute the Q-values for a batch of states using the actions computed from them.""" - return self.forward(states, actions=self.get_action(states)) diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py new file mode 100644 index 000000000..586b8b472 --- /dev/null +++ b/maro/rl/policy/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac import ACActionInfo, ACBatch, ACLossInfo, ActorCritic, DiscreteACNet +from .ddpg import DDPG, DDPGBatch, DDPGLossInfo, ContinuousACNet +from .dqn import DQN, DQNBatch, DQNLossInfo, DiscreteQNet, PrioritizedSampler +from .pg import PGActionInfo, PGBatch, PGLossInfo, DiscretePolicyNet, PolicyGradient +from .policy import AbsPolicy, Batch, LossInfo, NullPolicy, RLPolicy +from .index import get_model_cls, get_policy_cls + +__all__ = [ + "ACActionInfo", "ACBatch", "ACLossInfo", "ActorCritic", "DiscreteACNet", + "DDPG", "DDPGBatch", "DDPGLossInfo", "ContinuousACNet", + "DQN", "DQNBatch", "DQNLossInfo", "DiscreteQNet", "PrioritizedSampler", + "PGActionInfo", "PGBatch", "PGLossInfo", "DiscretePolicyNet", "PolicyGradient", + "AbsPolicy", "Batch", "LossInfo", "NullPolicy", "RLPolicy", + "get_model_cls", "get_policy_cls" +] diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py new file mode 100644 index 000000000..dfc7f0cfe --- /dev/null +++ b/maro/rl/policy/ac.py @@ -0,0 +1,204 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List, Union + +import numpy as np +import torch +from torch.distributions import Categorical + +from maro.rl.typing import DiscreteACNet, Trajectory +from maro.rl.utils import get_torch_loss_cls, discount_cumsum +from maro.rl.utils.remote_tools import LearnTask + +from .policy import Batch, LossInfo, RLPolicy + + +class ACActionInfo: + + __slots__ = ["action", "logp", "value"] + + def __init__(self, action, logp: float, value: float): + self.action = action + self.logp = logp + self.value = value + + +class ACBatch(Batch): + + __slots__ = ["states", "actions", "rewards", "returns", "advantages", "logps"] + + def __init__(self, states, actions, rewards, returns, advantages, logps): + super().__init__() + self.states = states + self.actions = actions + self.rewards = rewards + self.returns = returns + self.advantages = advantages + self.logps = logps + + @property + def size(self): + return len(self.states) + + +class ACLossInfo(LossInfo): + + __slots__ = ["actor_loss", "critic_loss", "entropy"] + + def __init__(self, loss, actor_loss, critic_loss, entropy, grad=None): + super().__init__(loss, grad) + self.loss = loss + self.actor_loss = actor_loss + self.critic_loss = critic_loss + self.entropy = entropy + self.grad = grad + + +class ActorCritic(RLPolicy): + """Actor Critic algorithm with separate policy and value models. + + References: + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f + + Args: + name (str): Unique identifier for the policy. + ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. + reward_discount (float): Reward decay as defined in standard RL terminology. + grad_iters (int): Number of gradient steps for each batch or set of batches. Defaults to 1. + critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing + the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". + min_logp (float): Lower bound for clamping logP values during learning. This is to prevent logP from becoming + very large in magnitude and causing stability issues. Defaults to None, which means no lower bound. + critic_loss_coeff (float): Coefficient for critic loss in total loss. Defaults to 1.0. + entropy_coeff (float): Coefficient for the entropy term in total loss. Defaults to None, in which case the + total loss will not include an entropy term. + clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, + in which case the actor loss is calculated using the usual policy gradient theorem. + """ + + def __init__( + self, + name: str, + ac_net: DiscreteACNet, + reward_discount: float, + grad_iters: int = 1, + critic_loss_cls="mse", + min_logp: float = None, + critic_loss_coeff: float = 1.0, + entropy_coeff: float = None, + clip_ratio: float = None, + lam: float = 0.9, + get_loss_on_rollout_finish: bool = False, + remote: bool = False + ): + if not isinstance(name, ac_net, DiscreteACNet): + raise TypeError("model must be an instance of 'DiscreteACNet'") + + super().__init__(name, remote=remote) + self.ac_net = ac_net + self.device = self.ac_net.device + self.reward_discount = reward_discount + self.grad_iters = grad_iters + self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() + self.min_logp = min_logp + self.critic_loss_coeff = critic_loss_coeff + self.entropy_coeff = entropy_coeff + self.clip_ratio = clip_ratio + self.lam = lam + self._get_loss_on_rollout_finish = get_loss_on_rollout_finish + + def choose_action(self, states) -> Union[ACActionInfo, List[ACActionInfo]]: + """Return actions and log probabilities for given states.""" + self.ac_net.eval() + with torch.no_grad(): + actions, logps, values = self.ac_net.get_action(states) + actions, logps, values = actions.cpu().numpy(), logps.cpu().numpy(), values.cpu().numpy() + if len(actions) == 1: + return ACActionInfo(actions[0], logps[0], values[0]) + else: + return [ACActionInfo(action, logp, value) for action, logp, value in zip(actions, logps, values)] + + def get_rollout_info(self, trajectory: Trajectory): + if self._get_loss_on_rollout_finish: + return self.get_batch_loss(self._preprocess(trajectory)) + else: + return trajectory + + def _preprocess(self, trajectory: Trajectory): + if trajectory.actions[-1]: + rewards = np.append(trajectory.rewards, trajectory.actions[-1].value) + values = np.array([action_info.value for action_info in trajectory.actions]) + else: + rewards = np.append(trajectory.rewards, .0) + values = np.append([action_info.value for action_info in trajectory.actions[:-1]], .0) + + # Generalized advantage estimation using TD(Lambda) + deltas = rewards[:-1] + self.reward_discount * values[1:] - values[:-1] + advantages = discount_cumsum(deltas, self.reward_discount * self.lam) + + # Returns rewards-to-go, to be targets for the value function + returns = discount_cumsum(rewards, self.reward_discount)[:-1] + + return ACBatch( + states=trajectory.states, + actions=[action_info.action for action_info in trajectory.actions], + rewards=trajectory.rewards, + returns=returns, + advantages=advantages, + logps=[action_info.logp for action_info in trajectory.actions] + ) + + def get_batch_loss(self, batch: ACBatch, with_grad: bool = False) -> ACLossInfo: + assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." + self.ac_net.train() + actions = torch.from_numpy(np.asarray(batch.actions)).to(self.device) + logp_batch = torch.from_numpy(np.asarray(batch.logps)).to(self.device) + advantages = torch.from_numpy(np.asarray(batch.advantages)).to(self.device) + + action_probs, state_values = self.ac_net(batch.states) + state_values = state_values.squeeze() + + # actor loss + logp = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) + logp = torch.clamp(logp, min=self.min_logp, max=.0) + if self.clip_ratio is not None: + ratio = torch.exp(logp - logp_batch) + clipped_ratio = torch.clamp(ratio, 1 - self.clip_ratio, 1 + self.clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).mean() + else: + actor_loss = -(logp * advantages).mean() + + # critic_loss + critic_loss = self.critic_loss_func(state_values, batch.returns) + + # entropy + if self.entropy_coeff is not None: + entropy = -Categorical(action_probs).entropy().mean() + else: + entropy = 0 + + # total loss + loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy + grad=self.ac_net.get_gradients(loss) if with_grad else None + return ACLossInfo(actor_loss, critic_loss, entropy, loss, grad=grad) + + def apply(self, loss_info_list: List[ACLossInfo]): + """Apply gradients to the underlying parameterized model.""" + self.ac_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) + + def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): + if self.remote: + # TODO: distributed grad computation + pass + else: + batches = [self._preprocess(traj) for traj in trajectories] + for _ in range(self.grad_iters): + self.apply([self.get_batch_loss(batch, with_grad=True) for batch in batches]) + + def set_state(self, policy_state): + self.ac_net.load_state_dict(policy_state) + + def get_state(self): + return self.ac_net.state_dict() diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py new file mode 100644 index 000000000..5790296d9 --- /dev/null +++ b/maro/rl/policy/ddpg.py @@ -0,0 +1,213 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List, Union + +import numpy as np +import torch + +from maro.rl.exploration import GaussianNoiseExploration +from maro.rl.typing import ContinuousACNet, Trajectory +from maro.rl.utils import get_torch_loss_cls +from maro.rl.utils.remote_tools import LearnTask +from maro.utils.exception.rl_toolkit_exception import InvalidExperience + +from .policy import Batch, LossInfo, RLPolicy +from .replay import ReplayMemory + + +class DDPGBatch(Batch): + """Wrapper for a set of experiences. + + An experience consists of state, action, reward, next state and auxillary information. + """ + __slots__ = ["states", "actions", "rewards", "next_states"] + + def __init__(self, states: list, actions: list, rewards: list, next_states: list): + if not len(states) == len(actions) == len(rewards) == len(next_states): + raise InvalidExperience("values of contents should consist of lists of the same length") + super().__init__() + self.states = states + self.actions = actions + self.rewards = rewards + self.next_states = next_states + + @property + def size(self): + return len(self.states) + + +class DDPGLossInfo(LossInfo): + + __slots__ = ["policy_loss", "q_loss"] + + def __init__(self, loss, policy_loss, q_loss, grad=None): + super().__init__(loss, grad) + self.loss = loss + self.policy_loss = policy_loss + self.q_loss = q_loss + self.grad = grad + + +class DDPG(RLPolicy): + """The Deep Deterministic Policy Gradient (DDPG) algorithm. + + References: + https://arxiv.org/pdf/1509.02971.pdf + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg + + Args: + name (str): Unique identifier for the policy. + ac_net (ContinuousACNet): DDPG policy and q-value models. + reward_discount (float): Reward decay as defined in standard RL terminology. + update_target_every (int): Number of training rounds between policy target model updates. + q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for + the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". + q_value_loss_coeff (float): Coefficient for policy loss in the total loss function, e.g., + loss = policy_loss + ``q_value_loss_coeff`` * q_value_loss. Defaults to 1.0. + soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + + (1-soft_update_coeff) * target_model. Defaults to 1.0. + exploration: Exploration strategy for generating exploratory actions. Defaults to ``GaussianNoiseExploration``. + replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. + random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, + overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with + wrap-around. Defaults to False. + """ + def __init__( + self, + name: str, + ac_net: ContinuousACNet, + reward_discount: float, + num_epochs: int = 1, + update_target_every: int = 5, + q_value_loss_cls="mse", + q_value_loss_coeff: float = 1.0, + soft_update_coeff: float = 1.0, + exploration=GaussianNoiseExploration(), + replay_memory_capacity: int = 10000, + random_overwrite: bool = False, + remote: bool = False + ): + if not isinstance(ac_net, ContinuousACNet): + raise TypeError("model must be an instance of 'ContinuousACNet'") + + super().__init__(name, remote=remote) + self.ac_net = ac_net + self.device = self.ac_net.device + if self.ac_net.trainable: + self.target_ac_net = ac_net.copy() + self.target_ac_net.eval() + else: + self.target_ac_net = None + self.reward_discount = reward_discount + self.num_epochs = num_epochs + self.update_target_every = update_target_every + self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() + self.q_value_loss_coeff = q_value_loss_coeff + self.soft_update_coeff = soft_update_coeff + + self._ac_net_version = 0 + self._target_ac_net_version = 0 + + self._replay_memory = ReplayMemory(DDPGBatch, replay_memory_capacity, random_overwrite=random_overwrite) + + self.exploration = exploration + self.exploring = True # set initial exploration status to True + + def choose_action(self, states, explore: bool = False) -> Union[float, np.ndarray]: + self.ac_net.eval() + with torch.no_grad(): + actions = self.ac_net.get_action(states).cpu().numpy() + + if explore: + actions = self.exploration(actions, state=states) + return actions[0] if len(actions) == 1 else actions + + def _preprocess(self, trajectory: Trajectory): + if trajectory.states[-1]: + batch = DDPGBatch( + states=trajectory.states[:-1], + actions=trajectory.actions[:-1], + rewards=trajectory.rewards, + next_states=trajectory.states[1:] + ) + else: + batch = DDPGBatch( + states=trajectory.states[:-2], + actions=trajectory.actions[:-2], + rewards=trajectory.rewards[:-1], + next_states=trajectory.states[1:-1] + ) + self._replay_memory.put(batch) + + def _get_batch(self) -> DDPGBatch: + indexes = np.random.choice(self._replay_memory.size) + return DDPGBatch( + [self._replay_memory.data["states"][idx] for idx in indexes], + [self._replay_memory.data["actions"][idx] for idx in indexes], + [self._replay_memory.data["rewards"][idx] for idx in indexes], + [self._replay_memory.data["next_states"][idx] for idx in indexes] + ) + + def get_batch_loss(self, batch: DDPGBatch, with_grad: bool = False) -> DDPGLossInfo: + assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." + self.ac_net.train() + states, next_states = batch.states, batch.next_states + actual_actions = torch.from_numpy(batch.actions).to(self.device) + rewards = torch.from_numpy(batch.rewards).to(self.device) + if len(actual_actions.shape) == 1: + actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) + + with torch.no_grad(): + next_q_values = self.target_ac_net.value(next_states) + target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) + + q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) + q_loss = self.q_value_loss_func(q_values, target_q_values) + policy_loss = -self.ac_net.value(states).mean() + + # total loss + loss = policy_loss + self.q_value_loss_coeff * q_loss + grad = self.ac_net.get_gradients(loss) if with_grad else None + return DDPGLossInfo(policy_loss, q_loss, loss, grad=grad) + + def apply(self, loss_info_list: List[DDPGLossInfo]): + self.ac_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) + if self._ac_net_version - self._target_ac_net_version == self.update_target_every: + self._update_target() + + def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): + for traj in trajectories: + self._preprocess(traj) + + if self.remote: + # TODO: distributed grad computation + pass + else: + for _ in range(self.num_epochs): + loss_info = self.get_batch_loss(self._get_batch(), with_grad=False) + self.ac_net.step(loss_info.loss) + self._ac_net_version += 1 + if self._ac_net_version - self._target_ac_net_version == self.update_target_every: + self._update_target() + + def _update_target(self): + # soft-update target network + self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) + self._target_ac_net_version = self._ac_net_version + + def exploit(self): + self.exploring = False + + def explore(self): + self.exploring = True + + def exploration_step(self): + self.exploration.step() + + def set_state(self, policy_state): + self.ac_net.load_state_dict(policy_state) + self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None + + def get_state(self): + return self.ac_net.state_dict() diff --git a/maro/rl/algorithms/dqn.py b/maro/rl/policy/dqn.py similarity index 52% rename from maro/rl/algorithms/dqn.py rename to maro/rl/policy/dqn.py index 4e29a4cd1..80058b688 100644 --- a/maro/rl/algorithms/dqn.py +++ b/maro/rl/policy/dqn.py @@ -1,15 +1,61 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import List, Tuple, Union +from typing import List, Union import numpy as np import torch -from maro.rl.algorithms.abs_algorithm import AbsPolicy -from maro.rl.replay import ExperienceSet, ReplayMemory from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration -from maro.rl.model import DiscreteQNet +from maro.rl.typing import DiscreteQNet, Trajectory +from maro.rl.utils.remote_tools import LearnTask +from maro.utils.exception.rl_toolkit_exception import InvalidExperience + +from .policy import Batch, LossInfo, RLPolicy +from .replay import ReplayMemory + + +class DQNBatch(Batch): + """Wrapper for a set of experiences. + + An experience consists of state, action, reward, next state. + """ + __slots__ = ["states", "actions", "rewards", "next_states", "indexes", "is_weights"] + + def __init__( + self, + states: list, + actions: list, + rewards: list, + next_states: list, + indexes: list, + is_weights: list = None + ): + if not len(states) == len(actions) == len(rewards) == len(next_states) == len(is_weights) == len(indexes): + raise InvalidExperience("values of contents should consist of lists of the same length") + super().__init__() + self.states = states + self.actions = actions + self.rewards = rewards + self.next_states = next_states + self.is_weights = is_weights + self.indexes = indexes + + @property + def size(self): + return len(self.states) + + +class DQNLossInfo(LossInfo): + + __slots__ = ["td_errors", "indexes"] + + def __init__(self, loss, td_errors, indexes, grad=None): + super().__init__(loss, grad) + self.loss = loss + self.td_errors = td_errors + self.indexes = indexes + self.grad = grad class PrioritizedSampler: @@ -35,28 +81,30 @@ class PrioritizedSampler: """ def __init__( self, - memory_capacity: int, + replay_memory: ReplayMemory, + *, batch_size: int = 32, alpha: float = 0.6, beta: float = 0.4, - beta_step: float = 0.001 + beta_step: float = 0.001, + max_priority: float = 1e8 ): if beta > 1.0: raise ValueError("beta should be between 0.0 and 1.0") - self.memory_capacity = memory_capacity - self._sum_tree = np.zeros(2 * self.memory_capacity - 1) + self._replay_memory = replay_memory + self._sum_tree = np.zeros(2 * self._replay_memory.capacity - 1) self.batch_size = batch_size self.alpha = alpha self.beta = beta self.beta_step = beta_step self.eps = 1e-7 - self._max_priority = 1e8 + self._max_priority = max_priority def total(self): """Return the sum of priorities over all experiences.""" return self._sum_tree[0] - def on_new(self, experience_set: ExperienceSet, indexes: List[int]): + def set_max_priority(self, indexes): """Set the priorities of newly added experiences to the maximum value.""" self.update(indexes, [self._max_priority] * len(indexes)) @@ -64,7 +112,7 @@ def update(self, indexes, td_errors): """Update priority values at given indexes.""" for idx, err in zip(indexes, td_errors): priority = self._get_priority(err) - tree_idx = idx + self.memory_capacity - 1 + tree_idx = idx + self._replay_memory.capacity - 1 delta = priority - self._sum_tree[tree_idx] self._sum_tree[tree_idx] = priority self._update(tree_idx, delta) @@ -77,23 +125,16 @@ def get(self): low, high = segment_len * i, segment_len * (i + 1) sampled_val = np.random.uniform(low=low, high=high) idx = self._get(0, sampled_val) - data_idx = idx - self.memory_capacity + 1 + data_idx = idx - self._replay_memory.capacity + 1 indexes.append(data_idx) priorities.append(self._sum_tree[idx]) self.beta = min(1., self.beta + self.beta_step) sampling_probabilities = priorities / self.total() - is_weights = np.power(self.replay_memory.size * sampling_probabilities, -self.beta) + is_weights = np.power(self._replay_memory.size * sampling_probabilities, -self.beta) is_weights /= is_weights.max() return indexes, is_weights - return ExperienceSet( - states=[self.replay_memory.data["states"][idx] for idx in indexes], - actions=[self.replay_memory.data["actions"][idx] for idx in indexes], - rewards=[self.replay_memory.data["rewards"][idx] for idx in indexes], - next_states=[self.replay_memory.data["next_states"][idx] for idx in indexes], - info=[{"index": idx, "is_weight": wt} for idx, wt in zip(indexes, is_weights)] - ) def _get_priority(self, error): return (np.abs(error) + self.eps) ** self.alpha @@ -119,12 +160,13 @@ def _get(self, idx, sampled_val): return self._get(right, sampled_val - self._sum_tree[left]) -class DQN(AbsPolicy): +class DQN(RLPolicy): """The Deep-Q-Networks algorithm. See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. Args: + name (str): Unique identifier for the policy. q_net (DiscreteQNet): Q-value model. reward_discount (float): Reward decay as defined in standard RL terminology. train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. @@ -136,107 +178,163 @@ class DQN(AbsPolicy): i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. exploration (DiscreteSpaceExploration): Exploration strategy for generating exploratory actions. Defaults to - an ``EpsilonGreedyExploration`` instance. + ``EpsilonGreedyExploration``. + replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. + random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, + overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with + wrap-around. Defaults to False. """ def __init__( self, + name: str, q_net: DiscreteQNet, reward_discount: float = 0.9, - train_epochs: int = 1, + num_epochs: int = 1, update_target_every: int = 5, soft_update_coeff: float = 0.1, double: bool = False, - prioritized_experience_sampler: PrioritizedSampler = None, + exploration: DiscreteSpaceExploration = EpsilonGreedyExploration(), replay_memory_capacity: int = 10000, random_overwrite: bool = False, - exploration: DiscreteSpaceExploration = EpsilonGreedyExploration() + prioritized_replay_kwargs: dict = None, + remote: bool = False ): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(exploration=exploration) + super().__init__(name, remote=remote) self.q_net = q_net + self.device = self.q_net.device if self.q_net.trainable: self.target_q_net = q_net.copy() self.target_q_net.eval() else: self.target_q_net = None + self._q_net_version = 0 + self._target_q_net_version = 0 + self.reward_discount = reward_discount - self.train_epochs = train_epochs + self.num_epochs = num_epochs self.update_target_every = update_target_every self.soft_update_coeff = soft_update_coeff self.double = double - self.prioritized_experience_sampler = prioritized_experience_sampler - self.device = self.q_net.device - if not self.prioritized_experience_replay: + self._replay_memory = ReplayMemory(DQNBatch, replay_memory_capacity, random_overwrite=random_overwrite) + self.prioritized_replay = prioritized_replay_kwargs is not None + if self.prioritized_replay: + self._sampler = PrioritizedSampler(self._replay_memory, **prioritized_replay_kwargs) + else: self._loss_func = torch.nn.MSELoss() - self._replay_memory = ReplayMemory(replay_memory_capacity, random_overwrite=random_overwrite) - self._q_net_version = 0 - self._target_q_net_version = 0 - def choose_action(self, states, explore: bool = True) -> Union[int, np.ndarray]: + self.exploration = exploration + self.exploring = True # set initial exploration status to True + + def choose_action(self, states) -> Union[int, np.ndarray]: self.q_net.eval() with torch.no_grad(): q_for_all_actions = self.q_net(states) # (batch_size, num_actions) _, actions = q_for_all_actions.max(dim=1) actions = actions.cpu().numpy() - if explore: + if self.exploring: if self.exploration.action_space is None: self.exploration.set_action_space(np.arange(q_for_all_actions.shape[1])) actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def apply(self, grad_dict: dict): - self.q_net.apply(grad_dict) - self._q_net_version += 1 - if self._q_net_version - self._target_q_net_version == self.update_target_every: - self._update_target() + def _put_in_replay_memory(self, traj: Trajectory): + if traj.states[-1]: + batch = DQNBatch(traj.states[:-1], traj.actions[:-1], traj.rewards, traj.states[1:]) + else: + batch = DQNBatch(traj.states[:-2], traj.actions[:-2], traj.rewards[:-1], traj.states[1:-1]) + indexes = self._replay_memory.put(batch) + if self.prioritized_replay: + self._sampler.set_max_priority(indexes) + + def _sample(self) -> DQNBatch: + if self.prioritized_replay: + indexes, is_weights = self._sampler.get() + else: + indexes = np.random.choice(self._replay_memory.size) + is_weights = None + + return DQNBatch( + [self._replay_memory.data["states"][idx] for idx in indexes], + [self._replay_memory.data["actions"][idx] for idx in indexes], + [self._replay_memory.data["rewards"][idx] for idx in indexes], + [self._replay_memory.data["next_states"][idx] for idx in indexes], + indexes, + is_weights=is_weights + ) - def learn(self, exp: ExperienceSet): + def get_batch_loss(self, batch: DQNBatch, with_grad: bool = False): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() - self._replay_memory.put(exp) - for _ in range(self.train_epochs): - # sample from the replay memory - experience_set = self.sampler.get() - states, next_states = experience_set.states, experience_set.next_states - actions = torch.from_numpy(np.asarray(experience_set.actions)).to(self.device) - rewards = torch.from_numpy(np.asarray(experience_set.rewards)).to(self.device) - if self.prioritized_experience_replay: - indexes = [info["index"] for info in experience_set.info] - is_weights = torch.tensor([info["is_weight"] for info in experience_set.info]).to(self.device) - - # get target Q values - with torch.no_grad(): - if self.double: - actions_by_eval_q_net = self.q_net.get_action(next_states)[0] - next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) - else: - next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) - - target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) - - # gradient step - q_values = self.q_net.q_values(states, actions) - if self.prioritized_experience_replay: - td_errors = target_q_values - q_values - loss = (td_errors * is_weights).mean() - self.sampler.update(indexes, td_errors.detach().cpu().numpy()) + states, next_states = batch.states, batch.next_states + actions = torch.from_numpy(np.asarray(batch.actions)).to(self.device) + rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) + + # get target Q values + with torch.no_grad(): + if self.double: + actions_by_eval_q_net = self.q_net.get_action(next_states)[0] + next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) else: - loss = self._loss_func(q_values, target_q_values) - self.q_net.step(loss) + next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) + + target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) + # gradient step + q_values = self.q_net.q_values(states, actions) + td_errors = target_q_values - q_values + if self.prioritized_replay: + is_weights = torch.from_numpy(np.asarray(batch.is_weights)).to(self.device) + loss = (td_errors * is_weights).mean() + else: + loss = self._loss_func(q_values, target_q_values) + + grad = self.q_net.get_gradients(loss) if with_grad else None + return DQNLossInfo(loss, td_errors, batch.indexes, grad=grad) + + def apply(self, loss_info_list: List[DQNLossInfo]): + if self.prioritized_replay: + for loss_info in loss_info_list: + self._sampler.update(loss_info.indexes, loss_info.td_errors) + + self.q_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) self._q_net_version += 1 if self._q_net_version - self._target_q_net_version == self.update_target_every: self._update_target() + def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): + for traj in trajectories: + self._put_in_replay_memory(traj) + + if self.remote: + # TODO: distributed grad computation + pass + else: + for _ in range(self.num_epochs): + loss_info = self.get_batch_loss(self._sample()) + self.q_net.step(loss_info.loss) + self._q_net_version += 1 + if self._q_net_version - self._target_q_net_version == self.update_target_every: + self._update_target() + def _update_target(self): # soft-update target network self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) self._target_q_net_version = self._q_net_version + def exploit(self): + self.exploring = False + + def explore(self): + self.exploring = True + + def exploration_step(self): + self.exploration.step() + def set_state(self, policy_state): self.q_net.load_state_dict(policy_state["eval"]) if "target" in policy_state: diff --git a/maro/rl/policy/index.py b/maro/rl/policy/index.py new file mode 100644 index 000000000..01571c542 --- /dev/null +++ b/maro/rl/policy/index.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac import ActorCritic, DiscreteACNet +from .ddpg import DDPG, ContinuousACNet +from .dqn import DQN, DiscreteQNet +from .pg import DiscretePolicyNet, PolicyGradient + +POLICY_INDEX = { + "ac": ActorCritic, + "dqn": DQN, + "ddpg": DDPG, + "pg": PolicyGradient +} + + +MODEL_INDEX = { + "ac": DiscreteACNet, + "dqn": DiscreteQNet, + "ddpg": ContinuousACNet, + "pg": DiscretePolicyNet +} + + +def get_policy_cls(policy_type): + if isinstance(policy_type, str): + if policy_type not in POLICY_INDEX: + raise KeyError(f"A string policy_type must be one of {list(POLICY_INDEX.keys())}.") + return POLICY_INDEX[policy_type] + + return policy_type + + +def get_model_cls(model_type): + if isinstance(model_type, str): + if model_type not in MODEL_INDEX: + raise KeyError(f"A string model_type must be one of {list(MODEL_INDEX.keys())}.") + return MODEL_INDEX[model_type] + + return model_type diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py new file mode 100644 index 000000000..e3f6ff829 --- /dev/null +++ b/maro/rl/policy/pg.py @@ -0,0 +1,140 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List, Tuple + +import numpy as np +import torch + +from maro.rl.typing import DiscretePolicyNet, Trajectory +from maro.rl.utils import discount_cumsum +from maro.rl.utils.remote_tools import LearnTask + +from .policy import Batch, LossInfo, RLPolicy + + +class PGActionInfo: + + __slots__ = ["action", "logp", "value"] + + def __init__(self, action, logp: float, value: float): + self.action = action + self.logp = logp + self.value = value + + +class PGBatch(Batch): + + __slots__ = ["states", "actions", "rewards", "returns", "logps"] + + def __init__(self, states, actions, rewards, returns, logps): + super().__init__() + self.states = states + self.actions = actions + self.rewards = rewards + self.returns = returns + self.logps = logps + + @property + def size(self): + return len(self.states) + + +class PGLossInfo(LossInfo): + def __init__(self, loss, grad=None): + super().__init__(loss, grad) + + +class PolicyGradient(RLPolicy): + """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. + + Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + + Args: + name (str): Unique identifier for the policy. + policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. + It may or may not have a shared bottom stack. + reward_discount (float): Reward decay as defined in standard RL terminology. + grad_iters (int): Number of gradient steps for each batch or set of batches. Defaults to 1. + """ + def __init__( + self, + name: str, + policy_net: DiscretePolicyNet, + reward_discount: float, + grad_iters: int = 1, + get_loss_on_rollout_finish: bool = False, + remote: bool = False + ): + if not isinstance(policy_net, DiscretePolicyNet): + raise TypeError("model must be an instance of 'DiscretePolicyNet'") + super().__init__(name, remote=remote) + self.policy_net = policy_net + self.device = self.policy_net.device + self.reward_discount = reward_discount + self.grad_iters = grad_iters + self._get_loss_on_rollout_finish = get_loss_on_rollout_finish + + def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Return actions and log probabilities for given states.""" + self.policy_net.eval() + with torch.no_grad(): + actions, log_p = self.policy_net.get_action(states) + actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() + return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p + + def get_rollout_info(self, trajectory: Trajectory): + if self._get_loss_on_rollout_finish: + return self.get_batch_loss(self._preprocess(trajectory)) + else: + return trajectory + + def _preprocess(self, trajectory: Trajectory) -> PGBatch: + if trajectory.actions[-1]: + rewards = np.append(trajectory.rewards, trajectory.actions[-1].value) + else: + rewards = np.append(trajectory.rewards, .0) + + return PGBatch( + states=trajectory.states, + actions=[action_info.action for action_info in trajectory.actions], + rewards=trajectory.rewards, + returns=discount_cumsum(rewards, self.reward_discount)[:-1], + logps=[action_info.logp for action_info in trajectory.actions] + ) + + def get_batch_loss(self, batch: PGBatch, with_grad: bool = False): + """ + This should be called at the end of a simulation episode and the experiences obtained from + the experience store's ``get`` method should be a sequential set, i.e., in the order in + which they are generated during the simulation. Otherwise, the return values may be meaningless. + """ + assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." + self.policy_net.train() + + states = batch.states + returns = torch.from_numpy(np.asarray(batch.returns)).to(self.device) + + _, logp = self.policy_net(states) + loss = -(logp * returns).mean() + grad = self.policy_net.get_gradients(loss) if with_grad else None + return PGLossInfo(loss, grad=grad) + + def apply(self, loss_info_list: List[PGLossInfo]): + """Apply gradients to the underlying parameterized model.""" + self.policy_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) + + def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): + if self.remote: + # TODO: distributed grad computation + pass + else: + batches = [self._preprocess(traj) for traj in trajectories] + for _ in range(self.grad_iters): + self.apply([self.get_batch_loss(batch, with_grad=True) for batch in batches]) + + def set_state(self, policy_state): + self.policy_net.load_state_dict(policy_state) + + def get_state(self): + return self.policy_net.state_dict() diff --git a/maro/rl/algorithms/abs_algorithm.py b/maro/rl/policy/policy.py similarity index 61% rename from maro/rl/algorithms/abs_algorithm.py rename to maro/rl/policy/policy.py index 4f3b9a65d..b267d276a 100644 --- a/maro/rl/algorithms/abs_algorithm.py +++ b/maro/rl/policy/policy.py @@ -2,15 +2,25 @@ # Licensed under the MIT license. from abc import ABC, abstractmethod +from typing import List -from maro.rl.replay import ExperienceSet, ReplayMemory, UniformSampler -from maro.rl.exploration import AbsExploration +from maro.rl.typing import Trajectory class AbsPolicy(ABC): - """Abstract policy class.""" - def __init__(self): + """Abstract policy class. + + Args: + name (str): Unique identifier for the policy. + + """ + def __init__(self, name: str): super().__init__() + self._name = name + + @property + def name(self): + return self._name @abstractmethod def choose_action(self, state): @@ -26,37 +36,62 @@ def choose_action(self, state): return None -class AbsCorePolicy(AbsPolicy): +class Batch: + def __init__(self): + pass + + +class LossInfo: + + __slots__ = ["loss", "grad"] + + def __init__(self, loss, grad): + self.loss = loss + self.grad = grad + + +class RLPolicy(AbsPolicy): """Policy that can update itself using simulation experiences. Reinforcement learning (RL) policies should inherit from this. Args: - replay_memory (ReplayMemory): An ``ReplayMemory`` instance for storing and retrieving experiences - generated by the policy. - experience_sampler_cls: Type of experience sampler. Must be a subclass of ``AbsSampler``. Defaults to - ``UnifromSampler``. - experience_sampler_kwargs (dict): Keyword arguments for ``experience_sampler_cls``. - exploration (AbsExploration): Exploration strategy for generating exploratory actions. Defaults to None. + name (str): Name of the policy. + data_parallel (bool): If true, """ - def __init__(self, exploration: AbsExploration = None): - super().__init__() - self.exploration = exploration - self.exploring = True + def __init__(self, name: str, remote: bool = False): + super().__init__(name) + self.remote = remote @abstractmethod def choose_action(self, state): raise NotImplementedError + def get_rollout_info(self, trajectory: Trajectory): + return trajectory + @abstractmethod - def learn(self): - """Policy update logic is implemented here. + def get_batch_loss(self, batch: Batch, with_grad: bool = False): + raise NotImplementedError - This usually includes retrieving experiences as training samples from the experience manager and - updating the underlying models using these samples. - """ + @abstractmethod + def apply(self, loss_info_list: List[LossInfo]): + pass + + @abstractmethod + def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): + """Perform policy improvement based on a list of trajectories obtained from parallel rollouts.""" raise NotImplementedError + def exploit(self): + pass + + def explore(self): + pass + + def exploration_step(self): + pass + @abstractmethod def get_state(self): """Return the current state of the policy. @@ -77,16 +112,6 @@ def set_state(self, policy_state): """ pass - def exploit(self): - self.exploring = False - - def explore(self): - self.exploring = True - - def exploration_step(self): - if self.exploration: - self.exploration.step() - def load(self, path: str): """Load the policy state from disk.""" pass diff --git a/maro/rl/replay/replay_memory.py b/maro/rl/policy/replay.py similarity index 61% rename from maro/rl/replay/replay_memory.py rename to maro/rl/policy/replay.py index 106bf55bf..979d5d082 100644 --- a/maro/rl/replay/replay_memory.py +++ b/maro/rl/policy/replay.py @@ -3,47 +3,6 @@ import numpy as np -from maro.utils.exception.rl_toolkit_exception import InvalidExperience - - -class ExperienceSet: - """Wrapper for a set of experiences. - - An experience consists of state, action, reward, next state and auxillary information. - """ - __slots__ = ["states", "actions", "rewards", "next_states", "info"] - - def __init__( - self, - states: list = None, - actions: list = None, - rewards: list = None, - next_states: list = None, - info: list = None - ): - if states is None: - states, actions, rewards, next_states, info = [], [], [], [], [] - - if not len(states) == len(actions) == len(rewards) == len(next_states) == len(info): - raise InvalidExperience("values of contents should consist of lists of the same length") - self.states = states - self.actions = actions - self.rewards = rewards - self.next_states = next_states - self.info = info - - @property - def size(self): - return len(self.states) - - def extend(self, other): - """Concatenate the set with another experience set.""" - self.states += other.states - self.actions += other.actions - self.rewards += other.rewards - self.next_states += other.next_states - self.info += other.info - class ReplayMemory: """Storage facility for simulation experiences. @@ -57,11 +16,12 @@ class ReplayMemory: overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. """ - def __init__(self, capacity: int, random_overwrite: bool = False): + def __init__(self, batch_type, capacity: int, random_overwrite: bool = False): super().__init__() + self._batch_type = batch_type self._capacity = capacity self._random_overwrite = random_overwrite - self._keys = ExperienceSet.__slots__ + self._keys = batch_type.__slots__ self.data = {key: [None] * self._capacity for key in self._keys} self._size = 0 self._index = 0 @@ -83,15 +43,16 @@ def size(self): @property def keys(self): - """Keys as specified by ``ExperienceSet``.""" + """Keys as specified by ``batch_type``.""" return self._keys - def put(self, experience_set: ExperienceSet): + def put(self, batch): """Put a experience set in the store. Args: experience_set (ExperienceSet): Experience set to be put in the store. """ - added_size = experience_set.size + assert isinstance(batch, self._batch_type) + added_size = batch.size if added_size > self._capacity: raise ValueError("size of added items should not exceed the capacity.") @@ -109,7 +70,7 @@ def put(self, experience_set: ExperienceSet): indexes = list(range(start_index, start_index + added_size)) for key in self.data: - for idx, val in zip(indexes, getattr(experience_set, key)): + for idx, val in zip(indexes, getattr(batch, key)): self.data[key][idx] = val self._size = min(self._capacity, num_experiences) diff --git a/maro/rl/replay/__init__.py b/maro/rl/replay/__init__.py deleted file mode 100644 index d9a75090c..000000000 --- a/maro/rl/replay/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .replay_memory import ExperienceSet, ReplayMemory - -__all__ = ["ExperienceSet", "ReplayMemory"] diff --git a/maro/rl/typing/__init__.py b/maro/rl/typing/__init__.py new file mode 100644 index 000000000..b286cd8f0 --- /dev/null +++ b/maro/rl/typing/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .model_types import ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet +from .rollout_object_types import Trajectory, Transition + +__all__ = [ + "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", + "Trajectory", "Transition" +] diff --git a/maro/rl/typing/model_types.py b/maro/rl/typing/model_types.py new file mode 100644 index 000000000..cc4f37937 --- /dev/null +++ b/maro/rl/typing/model_types.py @@ -0,0 +1,232 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import abstractmethod +from typing import Dict, Union + +import torch +from torch import nn +from torch.distributions import Categorical + +from maro.rl.model import AbsCoreModel, OptimOption + + +class DiscreteACNet(AbsCoreModel): + """Model container for the actor-critic architecture for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + + @abstractmethod + def forward(self, states, actor: bool = True, critic: bool = True) -> tuple: + """Compute action probabilities and values for a state batch. + + The output is a tuple of (action_probs, values), where action probs is a tensor of shape + (batch_size, action_space_size) and values is a tensor of shape (batch_size,). If only one + of these two is needed, the return value for the other one can be set to None. + + Args: + states: State batch to compute action probabilities and values for. + actor (bool): If True, the first element of the output will be actin probabilities. Defaults to True. + critic (bool): If True, the second element of the output will be state values. Defaults to True. + """ + raise NotImplementedError + + def get_action(self, states, max_prob: bool = False): + """ + Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value + for each state. + """ + action_probs, values = self.forward(states)[0] + if max_prob: + probs, actions = action_probs.max(dim=1) + return actions, torch.log(probs), values + else: + action_probs = Categorical(action_probs) # (batch_size, action_space_size) + actions = action_probs.sample() + logps = action_probs.log_prob(actions) + return actions, logps, values + + +class DiscretePolicyNet(AbsCoreModel): + """Parameterized policy for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + + @abstractmethod + def forward(self, states) -> torch.tensor: + """Compute action probabilities corresponding to each state in ``states``. + + The output must be a torch tensor with shape (batch_size, action_space_size). + + Args: + states: State batch to compute action probabilities for. + """ + raise NotImplementedError + + def get_action(self, states, max_prob: bool = False): + """ + Given a batch of states, return actions selected based on the probabilities computed by ``forward`` + and the corresponding log probabilities. + """ + action_prob = self.forward(states) # (batch_size, num_actions) + if max_prob: + prob, action = action_prob.max(dim=1) + return action, torch.log(prob) + else: + action_prob = Categorical(action_prob) # (batch_size, action_space_size) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + return action, log_p + + +class DiscreteQNet(AbsCoreModel): + """Q-value model for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + + @abstractmethod + def forward(self, states) -> torch.tensor: + """Compute the Q-values for all actions as a tensor of shape (batch_size, action_space_size).""" + raise NotImplementedError + + def get_action(self, states): + """ + Given Q-values for a batch of states and all actions, return the action index and the corresponding + Q-values for each state. + """ + q_for_all_actions = self.forward(states) # (batch_size, num_actions) + greedy_q, actions = q_for_all_actions.max(dim=1) + return actions.detach(), greedy_q.detach(), q_for_all_actions.shape[1] + + def q_values(self, states, actions: torch.tensor): + """Return the Q-values for a batch of states and actions.""" + if len(actions.shape) == 1: + actions = actions.unsqueeze(dim=1) + q_for_all_actions = self.forward(states) # (batch_size, num_actions) + return q_for_all_actions.gather(1, actions).squeeze(dim=1) + + def soft_update(self, other_model: nn.Module, tau: float): + """Soft-update model parameters using another model. + + The update formulae is: param = (1 - tau) * param + tau * pther_param. + + Args: + other_model: The model to update the current model with. + tau (float): Soft-update coefficient. + """ + for params, other_params in zip(self.parameters(), other_model.parameters()): + params.data = (1 - tau) * params.data + tau * other_params.data + + +class ContinuousACNet(AbsCoreModel): + """Model container for the actor-critic architecture for continuous action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + + @abstractmethod + def forward(self, states, actions=None): + """Compute actions for a batch of states or Q-values for a batch of states and actions. + + Args: + states: State batch to compute the Q-values for. + actions: Action batch. If None, the output should be a batch of actions corresponding to + the state batch. Otherwise, the output should be the Q-values for the given states and + actions. Defaults to None. + """ + raise NotImplementedError + + def get_action(self, states): + """Compute actions given a batch of states.""" + return self.forward(states) + + def value(self, states): + """Compute the Q-values for a batch of states using the actions computed from them.""" + return self.forward(states, actions=self.get_action(states)) + + def soft_update(self, other_model: nn.Module, tau: float): + """Soft-update model parameters using another model. + + The update formulae is: param = (1 - tau) * param + tau * pther_param. + + Args: + other_model: The model to update the current model with. + tau (float): Soft-update coefficient. + """ + for params, other_params in zip(self.parameters(), other_model.parameters()): + params.data = (1 - tau) * params.data + tau * other_params.data diff --git a/maro/rl/typing/rollout_object_types.py b/maro/rl/typing/rollout_object_types.py new file mode 100644 index 000000000..ce9093029 --- /dev/null +++ b/maro/rl/typing/rollout_object_types.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +class Transition: + """Convenience class to be used in an environment wrapper's post-step processing function. + + Args: + state: Output of the environment wrapper's ``get_state``. + action: Output of the ``AgentWrapper`` that interacts with environment wrapper. + env_action: Output of the environment wrapper's ``to_env_action``. + reward: Output of the environmet wrapper's ``get_reward``. + next_state: The state immediately following ``state``. + info: Output of the environment wrapper's ``get_transition_info``. + """ + + __slots__ = ["state", "action", "env_action", "reward", "next_state", "info"] + + def __init__(self, state, action, env_action, reward, next_state, info): + self.state = state + self.action = action + self.env_action = env_action + self.reward = reward + self.next_state = next_state + self.info = info + + +class Trajectory: + + __slots__ = ["states", "actions", "rewards", "info", "length"] + + def __init__(self, states: list, actions: list, rewards: list, info: list): + assert len(states) == len(actions) == len(rewards) + 1 == len(info) + 1 + self.states = states + self.actions = actions + self.rewards = rewards + self.info = info + self.length = len(rewards) diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index b853032d4..70f9cef67 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -5,9 +5,9 @@ from .torch_cls_index import ( get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls ) -from .trajectory_utils import get_k_step_returns, get_lambda_returns, get_truncated_cumulative_reward +from .trajectory_computation import discount_cumsum __all__ = [ - "MsgKey", "MsgTag", "get_torch_activation_cls", "get_torch_loss_cls", "get_torch_lr_scheduler_cls", - "get_torch_optim_cls", "get_k_step_returns", "get_lambda_returns", "get_truncated_cumulative_reward" + "MsgKey", "MsgTag", "discount_cumsum", "get_torch_activation_cls", "get_torch_loss_cls", + "get_torch_lr_scheduler_cls", "get_torch_optim_cls" ] diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 4a76fd279..13e1e83fa 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -7,13 +7,15 @@ class MsgTag(Enum): COLLECT = "rollout" EVAL = "eval" - INIT_POLICY_STATE = "init_policy_state" - INIT_POLICY_STATE_DONE = "init_policy_state_done" - GET_INITIAL_POLICY_STATE = "get_initial_policy_state" + INIT_POLICIES = "init_policies" + INIT_POLICIES_DONE = "init_policies_done" POLICY_STATE = "policy_state" CHOOSE_ACTION = "choose_action" ACTION = "action" - LEARN = "LEARN" + LEARN = "learn" + LEARN_DONE = "learn_finished" + COMPUTE_GRAD = "compute_grad" + COMPUTE_GRAD_DONE = "compute_grad_done" ABORT_ROLLOUT = "abort_rollout" EVAL_DONE = "eval_done" COLLECT_DONE = "collect_done" @@ -27,8 +29,11 @@ class MsgKey(Enum): EPISODE = "episode" SEGMENT = "segment" STEP = "step" - EXPERIENCES = "experiences" + POLICY_NAMES = "policy_names" + ROLLOUT_INFO = "rollout_info" TRACKER = "tracker" + GRAD_TASK = "grad_task" + LOSS_INFO = "loss_info" STATE = "state" POLICY_STATE = "policy_state" EXPLORATION_STEP = "exploration_step" diff --git a/maro/rl/utils/remote_tools/__init__.py b/maro/rl/utils/remote_tools/__init__.py new file mode 100644 index 000000000..c403b480b --- /dev/null +++ b/maro/rl/utils/remote_tools/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .client import TaskClient +from .task_manager import LearnTask, TaskManager +from .worker import worker + +__all__ = ["LearnTask", "TaskClient", "TaskManager", "worker"] diff --git a/maro/rl/utils/remote_tools/client.py b/maro/rl/utils/remote_tools/client.py new file mode 100644 index 000000000..55c605998 --- /dev/null +++ b/maro/rl/utils/remote_tools/client.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +class TaskClient: + pass diff --git a/maro/rl/utils/remote_tools/task_manager.py b/maro/rl/utils/remote_tools/task_manager.py new file mode 100644 index 000000000..f560fedde --- /dev/null +++ b/maro/rl/utils/remote_tools/task_manager.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import namedtuple + +LearnTask = namedtuple("LearnTask", ["policy_name", "model_state", "batch"]) + + +class TaskManager: + pass diff --git a/maro/rl/utils/remote_tools/worker.py b/maro/rl/utils/remote_tools/worker.py new file mode 100644 index 000000000..978fb94af --- /dev/null +++ b/maro/rl/utils/remote_tools/worker.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from os import getcwd +from typing import Callable, Dict + +from maro.communication import Proxy +from maro.rl.utils import MsgKey, MsgTag +from maro.utils import Logger + + +def worker( + group: str, + worker_idx: int, + create_policy_func_dict: Dict[str, Callable], + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + """Policy trainer process that can be launched on separate computation nodes. + Args: + group (str): Group name for the training cluster, which includes all trainers and a training manager that + manages them. + trainer_idx (int): Integer trainer index. The trainer's ID in the cluster will be "TRAINER.{trainer_idx}". + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` + instance. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + policy_dict = {} + proxy = Proxy( + group, "GRADWORKER", {"grad_manager": 1}, component_name=f"GRADWORKER.{worker_idx}", **proxy_kwargs + ) + logger = Logger(proxy.name, dump_folder=log_dir) + + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.COMPUTE_GRAD: + t0 = time.time() + task = msg.body[MsgKey.GRAD_TASK] + policy_name = task.policy_name + if policy_name not in policy_dict: + policy_dict[policy_name] = create_policy_func_dict[policy_name]() + msg_body = {MsgKey.LOSS_INFO: policy_dict[policy_name].get_loss_info(task.batch, with_grad=True)} + logger.debug(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, tag=MsgTag.COMPUTE_GRAD_DONE, body=msg_body) diff --git a/maro/rl/utils/trajectory_computation.py b/maro/rl/utils/trajectory_computation.py new file mode 100644 index 000000000..03b765da3 --- /dev/null +++ b/maro/rl/utils/trajectory_computation.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import scipy + + +def discount_cumsum(x, discount): + """ + Magic from rllab for computing discounted cumulative sums of vectors. + + Original code from: + https://github.com/rll/rllab/blob/master/rllab/misc/special.py). + + For details about the scipy function, see: + https://docs.scipy.org/doc/scipy/reference/tutorial/signal.html#difference-equation-filtering + + input: + vector x, + [x0, + x1, + x2] + + output: + [x0 + discount * x1 + discount^2 * x2, + x1 + discount * x2, + x2] + """ + return scipy.signal.lfilter([1], [1, float(-discount)], x[::-1], axis=0)[::-1] diff --git a/maro/rl/utils/trajectory_utils.py b/maro/rl/utils/trajectory_utils.py deleted file mode 100644 index 3fe348adb..000000000 --- a/maro/rl/utils/trajectory_utils.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from functools import reduce -from typing import Union - -import numpy as np -import torch -import torch.nn.functional as F - - -def get_truncated_cumulative_reward( - rewards: Union[list, np.ndarray, torch.Tensor], - discount: float, - k: int = -1 -): - """Compute K-step cumulative rewards from a reward sequence. - Args: - rewards (Union[list, np.ndarray, torch.Tensor]): Reward sequence from a trajectory. - discount (float): Reward discount as in standard RL. - k (int): Number of steps in computing cumulative rewards. If it is -1, returns are computed using the - largest possible number of steps. Defaults to -1. - - Returns: - An ndarray or torch.Tensor instance containing the k-step cumulative rewards for each time step. - """ - if k < 0: - k = len(rewards) - 1 - pad = np.pad if isinstance(rewards, list) or isinstance(rewards, np.ndarray) else F.pad - return reduce( - lambda x, y: x * discount + y, - [pad(rewards[i:], (0, i)) for i in range(min(k, len(rewards)) - 1, -1, -1)] - ) - - -def get_k_step_returns( - rewards: Union[list, np.ndarray, torch.Tensor], - values: Union[list, np.ndarray, torch.Tensor], - discount: float, - k: int = -1 -): - """Compute K-step returns given reward and value sequences. - Args: - rewards (Union[list, np.ndarray, torch.Tensor]): Reward sequence from a trajectory. - values (Union[list, np.ndarray, torch.Tensor]): Sequence of values for the traversed states in a trajectory. - discount (float): Reward discount as in standard RL. - k (int): Number of steps in computing returns. If it is -1, returns are computed using the largest possible - number of steps. Defaults to -1. - - Returns: - An ndarray or torch.Tensor instance containing the k-step returns for each time step. - """ - assert len(rewards) == len(values), "rewards and values should have the same length" - assert len(values.shape) == 1, "values should be a one-dimensional array" - rewards[-1] = values[-1] - if k < 0: - k = len(rewards) - 1 - pad = np.pad if isinstance(rewards, list) or isinstance(rewards, np.ndarray) else F.pad - return reduce( - lambda x, y: x * discount + y, - [pad(rewards[i:], (0, i)) for i in range(min(k, len(rewards)) - 1, -1, -1)], - pad(values[k:], (0, k)) - ) - - -def get_lambda_returns( - rewards: Union[list, np.ndarray, torch.Tensor], - values: Union[list, np.ndarray, torch.Tensor], - discount: float, - lam: float, - k: int = -1 -): - """Compute lambda returns given reward and value sequences and a k. - Args: - rewards (Union[list, np.ndarray, torch.Tensor]): Reward sequence from a trajectory. - values (Union[list, np.ndarray, torch.Tensor]): Sequence of values for the traversed states in a trajectory. - discount (float): Reward discount as in standard RL. - lam (float): Lambda coefficient involved in computing lambda returns. - k (int): Number of steps where the lambda return series is truncated. If it is -1, no truncating is done and - the lambda return is carried out to the end of the sequence. Defaults to -1. - - Returns: - An ndarray or torch.Tensor instance containing the lambda returns for each time step. - """ - if k < 0: - k = len(rewards) - 1 - - # If lambda is zero, lambda return reduces to one-step return - if lam == .0: - return get_k_step_returns(rewards, values, discount, k=1) - - # If lambda is one, lambda return reduces to k-step return - if lam == 1.0: - return get_k_step_returns(rewards, values, discount, k=k) - - k = min(k, len(rewards) - 1) - pre_truncate = reduce( - lambda x, y: x * lam + y, - [get_k_step_returns(rewards, values, discount, k=k) for k in range(k - 1, 0, -1)] - ) - - post_truncate = get_k_step_returns(rewards, values, discount, k=k) * lam**(k - 1) - return (1 - lam) * pre_truncate + post_truncate diff --git a/maro/rl/wrappers/__init__.py b/maro/rl/wrappers/__init__.py new file mode 100644 index 000000000..5b388a0d6 --- /dev/null +++ b/maro/rl/wrappers/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .agent_wrapper import AgentWrapper +from .env_wrapper import AbsEnvWrapper, Trajectory, Transition + +__all__ = [ + "AgentWrapper", + "AbsEnvWrapper", "Trajectory", "Transition" +] diff --git a/maro/rl/learning/agent_wrapper.py b/maro/rl/wrappers/agent_wrapper.py similarity index 59% rename from maro/rl/learning/agent_wrapper.py rename to maro/rl/wrappers/agent_wrapper.py index 779ef3309..8b71bc177 100644 --- a/maro/rl/learning/agent_wrapper.py +++ b/maro/rl/wrappers/agent_wrapper.py @@ -1,24 +1,29 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import defaultdict from typing import Dict -from maro.rl.policy import AbsPolicy +from maro.rl.policy import AbsPolicy, RLPolicy -from .env_wrapper import AbsEnvWrapper +from .env_wrapper import Trajectory class AgentWrapper: """Multi-agent wrapper that interacts with an ``EnvWrapper`` with a unified inferface. Args: - policy_dict (Dict[str, AbsPolicy]): Policies used by the agents to make decision when interacting with - the environment. + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. """ - def __init__(self, policy_dict: Dict[str, AbsPolicy], agent2policy: Dict[str, str]): - self.policy_dict = policy_dict + def __init__( + self, + create_policy_func_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str] + ): + self.policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} self.agent2policy = agent2policy self.policy = { agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() @@ -28,20 +33,19 @@ def choose_action(self, state: dict) -> dict: """Generate an action based on the given state. Args: - state (dict): Dicitionary of agents' states based on which action decisions will be made. + state (dict): Dictionary of agents' states based on which action decisions will be made. """ return {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} - def get_batch(self, env: AbsEnvWrapper): + def get_rollout_info(self, trajectory: Dict[str, Trajectory]): """Get experiences by policy names.""" - names = set() - exp_by_agent = env.get_experiences() - for agent_id, exp in exp_by_agent.items(): - if hasattr(self.policy[agent_id], "store"): - self.policy[agent_id].store(exp) - names.add(self.agent2policy[agent_id]) + rollout_info = {} + for agent_id, traj in trajectory.items(): + if isinstance(self.policy[agent_id], RLPolicy): + policy_name = self.agent2policy[agent_id] + rollout_info[policy_name] = self.policy_dict[policy_name].get_rollout_info(traj) - return {name: self.policy_dict[name].sampler.get() for name in names} + return rollout_info def set_policy_states(self, policy_state_dict: dict): """Update policy states.""" diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/wrappers/env_wrapper.py similarity index 76% rename from maro/rl/learning/env_wrapper.py rename to maro/rl/wrappers/env_wrapper.py index 8555d33c1..6a5ca35e3 100644 --- a/maro/rl/learning/env_wrapper.py +++ b/maro/rl/wrappers/env_wrapper.py @@ -5,31 +5,8 @@ from collections import defaultdict, deque from typing import Callable -from maro.rl.experience import ExperienceSet from maro.simulator import Env - - -class Transition: - """Convenience class to be used in an environment wrapper's post-step processing function. - - Args: - state: Output of the environment wrapper's ``get_state``. - action: Output of the ``AgentWrapper`` that interacts with environment wrapper. - env_action: Output of the environment wrapper's ``to_env_action``. - reward: Output of the environmet wrapper's ``get_reward``. - next_state: The state immediately following ``state``. - info: Output of the environment wrapper's ``get_transition_info``. - """ - - __slots__ = ["state", "action", "env_action", "reward", "next_state", "info"] - - def __init__(self, state, action, env_action, reward, next_state, info): - self.state = state - self.action = action - self.env_action = env_action - self.reward = reward - self.next_state = next_state - self.info = info +from maro.rl.typing import Trajectory, Transition class AbsEnvWrapper(ABC): @@ -43,8 +20,6 @@ class AbsEnvWrapper(ABC): replay_agent_ids (list): List of agent IDs whose transitions will be stored in internal replay buffers. If it is None, it will be set to all agents in the environment (i.e., env.agent_idx_list). Defaults to None. - get_experience_func (Callable): Custom function to convert the replay buffer to training experiences. Defaults - to None, in which case the replay buffer will be converted directly to SARS experiences for each agent. post_step (Callable): Custom function to gather information about a transition and the evolvement of the environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition @@ -56,13 +31,11 @@ def __init__( env: Env, reward_eval_delay: int = 0, replay_agent_ids: list = None, - get_experience_func: Callable = None, post_step: Callable = None ): self.env = env self.reward_eval_delay = reward_eval_delay - self._get_experience_func = get_experience_func self._post_step = post_step replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids @@ -206,29 +179,35 @@ def step(self, action_by_agent: dict): buf["rewards"].append(reward[agent_id]) buf["info"].append(info[agent_id] if info else None) - def get_experiences(self): - """Get per-agent experiences from the replay buffer.""" - if not self._get_experience_func: - exp_by_agent = { - agent_id: ExperienceSet( - buf["states"][:-1], - buf["actions"][:-1], - buf["rewards"][:-1], - buf["states"][1:], - buf["info"][:-1], - ) for agent_id, buf in self._replay_buffer.items() - } - else: - exp_by_agent = self._get_experience_func(self._replay_buffer) - - # clear the replay buffer of transitions that have already been converted to experiences. - for buf in self._replay_buffer.values(): - del buf["states"][:-1] - del buf["actions"][:-1] - del buf["rewards"][:-1] - del buf["info"][:-1] - - return exp_by_agent + def get_trajectory(self, clear_buffer: bool = True): + """Get agent trajectories from the replay buffer and clear it in the process.""" + trajectory = {} + for agent_id, buf in self._replay_buffer.items(): + # end of an episode + if not self._state: + states = buf["states"] + [None] + actions = buf["actions"] + [None] + rewards = buf["rewards"][:] + info = buf["info"][:] + if clear_buffer: + del buf["states"] + del buf["actions"] + del buf["rewards"] + del buf["info"] + else: + states = buf["states"][:] + actions = buf["actions"][:] + rewards = buf["rewards"][:-1] + info = buf["info"][:-1] + if clear_buffer: + del buf["states"][:-1] + del buf["actions"][:-1] + del buf["rewards"][:-1] + del buf["info"][:-1] + + trajectory[agent_id] = Trajectory(states, actions, rewards, info) + + return trajectory def reset(self): self.env.reset() From cb8a355a009196e848cdb8856c5f5cd563c75256 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 18 Aug 2021 08:57:58 +0000 Subject: [PATCH 407/482] 1. fixed bugs in ac and pg; 2. fixed bugs rl workflow scripts --- examples/rl/cim/callbacks.py | 2 +- examples/rl/cim/dqn.py | 2 +- examples/rl/cim/env_wrapper.py | 3 +- examples/rl/cim/policy_index.py | 2 +- .../rl/scripts/docker/docker_compose_yml.py | 6 +-- examples/rl/workflows/config.yml | 4 +- .../workflows/policy_manager/policy_host.py | 2 +- .../policy_manager/policy_manager.py | 5 +-- examples/rl/workflows/synchronous/learner.py | 2 +- .../workflows/synchronous/rollout_worker.py | 2 +- maro/rl/learning/policy_host.py | 3 +- maro/rl/learning/policy_manager.py | 32 ++++++--------- maro/rl/learning/synchronous/learner.py | 1 + .../rl/learning/synchronous/rollout_worker.py | 1 + maro/rl/model/core_model.py | 4 +- maro/rl/policy/ac.py | 40 ++++++++----------- maro/rl/policy/ddpg.py | 9 ++++- maro/rl/policy/dqn.py | 14 ++++--- maro/rl/policy/pg.py | 23 +++-------- maro/rl/policy/policy.py | 6 ++- maro/rl/{typing => types}/__init__.py | 0 maro/rl/{typing => types}/model_types.py | 2 +- .../{typing => types}/rollout_object_types.py | 0 maro/rl/utils/trajectory_computation.py | 5 ++- maro/rl/wrappers/agent_wrapper.py | 13 +++--- maro/rl/wrappers/env_wrapper.py | 2 +- requirements.dev.txt | 2 +- 27 files changed, 90 insertions(+), 97 deletions(-) rename maro/rl/{typing => types}/__init__.py (100%) rename maro/rl/{typing => types}/model_types.py (99%) rename maro/rl/{typing => types}/rollout_object_types.py (100%) diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py index 5e91892d7..f19b30a67 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/rl/cim/callbacks.py @@ -21,7 +21,7 @@ def post_collect(trackers, ep, segment): if len(trackers) > 1: metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} - simulation_logger.info(f"average env summary (episode {ep}, segement {segment}): {avg_metric}") + simulation_logger.info(f"average env summary (episode {ep}, segment {segment}): {avg_metric}") def post_evaluate(trackers, ep): diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py index 70706f889..65303587f 100644 --- a/examples/rl/cim/dqn.py +++ b/examples/rl/cim/dqn.py @@ -82,4 +82,4 @@ def get_dqn_policy(name: str, remote: bool = False): param_name="epsilon", **exploration_config ) - return DQN(name, qnet, **dqn_config, remote=remote) + return DQN(name, qnet, exploration=exploration, **dqn_config, remote=remote) diff --git a/examples/rl/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py index e206cb04c..6b413f816 100644 --- a/examples/rl/cim/env_wrapper.py +++ b/examples/rl/cim/env_wrapper.py @@ -3,6 +3,7 @@ import numpy as np +from maro.rl.policy import ACActionInfo from maro.rl.wrappers import AbsEnvWrapper from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType @@ -73,7 +74,7 @@ def to_env_action(self, action_by_agent: dict): ) early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 - model_action = action_info[0] if isinstance(action_info, tuple) else action_info + model_action = action_info.action if isinstance(action_info, ACActionInfo) else action_info percent = abs(self.action_space[model_action]) zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. if model_action < zero_action_idx: diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index 3eb24c645..2bc657351 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -16,5 +16,5 @@ warmup = {name: 1 for name in AGENT_IDS} # use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} +rl_policy_func_index = {name: get_ac_policy for name in AGENT_IDS} agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 94218ea4e..988415916 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -47,10 +47,10 @@ # host spec if config["policy_manager"]["type"] == "distributed": for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): - str_id = f"host.{host_id}" + str_id = f"policy_host.{host_id}" host_spec = deepcopy(common_spec) del host_spec["build"] - host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/host.py" + host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/policy_host.py" host_spec["container_name"] = str_id host_spec["environment"] = [ f"HOSTID={host_id}", @@ -77,7 +77,7 @@ f"NUMEPISODES={config['num_episodes']}", f"EVALSCH={config['eval_schedule']}", f"POLICYMANAGERTYPE={config['policy_manager']['type']}", - f"PARALLEL={config['policy_manager']['simple']['parallel']}", + f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}", f"LEARNGROUP={config['policy_manager']['distributed']['learn_group']}", f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}" ] + common_env diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 58d4293cb..bc1856368 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -23,11 +23,11 @@ async: group: async num_actors: 3 policy_manager: - type: simple # simple, distributed + type: distributed # simple, distributed simple: parallel: false distributed: - learn_group: policy-manager + learn_group: learn num_hosts: 2 redis: host: maro-redis diff --git a/examples/rl/workflows/policy_manager/policy_host.py b/examples/rl/workflows/policy_manager/policy_host.py index fcaa503b6..3ccef71f2 100644 --- a/examples/rl/workflows/policy_manager/policy_host.py +++ b/examples/rl/workflows/policy_manager/policy_host.py @@ -22,7 +22,7 @@ policy_host( rl_policy_func_index, int(host_id), - getenv("LEARNGROUP", default="LEARN"), + getenv("LEARNGROUP", default="learn"), proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py index 1ce757565..bde564ea9 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -15,8 +15,7 @@ def get_policy_manager(): manager_type = getenv("POLICYMANAGERTYPE", default="simple") - parallel = getenv("PARALLEL", default=False) - print("parallel: ", parallel) + parallel = int(getenv("PARALLEL", default=0)) if manager_type == "simple": return SimplePolicyManager(rl_policy_func_index, parallel=parallel, log_dir=log_dir) @@ -24,7 +23,7 @@ def get_policy_manager(): if manager_type == "distributed": return DistributedPolicyManager( list(rl_policy_func_index.keys()), - getenv("NODEGROUP", default="TRAIN"), + getenv("LEARNGROUP", default="learn"), num_hosts, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py index 7207c08ef..fc71e1502 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -58,7 +58,7 @@ def get_rollout_manager(): if rollout_mode == "multi-node": return MultiNodeRolloutManager( - getenv("ROLLOUTGROUP", default="ROLLOUT"), + getenv("ROLLOUTGROUP", default="rollout"), num_workers, num_steps=num_steps, max_lag=max_lag, diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 269beb3f8..31ba7fefc 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -23,7 +23,7 @@ worker_id = int(worker_id) rollout_worker_node( - getenv("ROLLOUTGROUP", default="ROLLOUT"), + getenv("ROLLOUTGROUP", default="rollout"), worker_id, get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), get_agent_wrapper(), diff --git a/maro/rl/learning/policy_host.py b/maro/rl/learning/policy_host.py index a6d5cd238..d450410ea 100644 --- a/maro/rl/learning/policy_host.py +++ b/maro/rl/learning/policy_host.py @@ -7,7 +7,7 @@ from maro.communication import Proxy from maro.rl.policy import LossInfo, RLPolicy -from maro.rl.typing import Trajectory +from maro.rl.types import Trajectory from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger @@ -42,6 +42,7 @@ def policy_host( break if msg.tag == MsgTag.INIT_POLICIES: + logger.info(f"Received an INIT_POLICIES msg") for name in msg.body[MsgKey.POLICY_NAMES]: policy_dict[name] = create_policy_func_dict[name](name) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 7710930c2..d42fd555d 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -10,7 +10,7 @@ from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.policy import LossInfo, RLPolicy -from maro.rl.typing import Trajectory +from maro.rl.types import Trajectory from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger @@ -63,8 +63,8 @@ def __init__( super().__init__() self._policy_names = list(create_policy_func_dict.keys()) self._parallel = parallel - self._logger = Logger("SIMPLE_POLICY_MANAGER", dump_folder=log_dir) - if self._parallel: + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) + if parallel: self._logger.info("Spawning policy host processes") self._state_cache = {} self._policy_hosts = [] @@ -91,9 +91,9 @@ def _policy_host(name, create_policy_func, conn): break for name, create_policy_func in create_policy_func_dict.items(): - manager_end, policy_end = Pipe() + manager_end, host_end = Pipe() self._manager_end[name] = manager_end - host = Process(target=_policy_host, args=(name, create_policy_func, policy_end)) + host = Process(target=_policy_host, args=(name, create_policy_func, host_end)) self._policy_hosts.append(host) host.start() @@ -101,6 +101,7 @@ def _policy_host(name, create_policy_func, conn): msg = conn.recv() if msg["type"] == "init": self._state_cache[policy_name] = msg["policy_state"] + self._logger.info(f"Initial state for policy {policy_name} cached") else: self._logger.info("local mode") self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} @@ -111,32 +112,25 @@ def update(self, rollout_info: Dict[str, list]): The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding policy's experience manager. Policies whose update conditions have been met will then be updated. """ - self._logger.info(f"parallel: {self._parallel}") + t0 = time.time() if self._parallel: - self._logger.info("I'm parallel") - self._logger.info(f"received rollout info from policies {list(rollout_info.keys())}") for policy_name, info_list in rollout_info.items(): - self._manager_end[policy_name].send({"type": "train", "rollout_info": info_list}) + self._manager_end[policy_name].send({"type": "learn", "rollout_info": info_list}) for policy_name, conn in self._manager_end.items(): msg = conn.recv() if msg["type"] == "learn_done": self._state_cache[policy_name] = msg["policy_state"] + self._logger.info(f"Cached state for policy {policy_name}") else: - self._logger.info("I'm NOT parallel") - self._logger.info(f"received rollout info from policies {list(rollout_info.keys())}") - t0 = time.time() for policy_name, info_list in rollout_info.items(): if isinstance(info_list[0], Trajectory): - self._logger.info("learning from multiple trajectories") self._policy_dict[policy_name].learn_from_multi_trajectories(info_list) elif isinstance(info_list[0], LossInfo): - self._logger.info("learning from loss info") self._policy_dict[policy_name].apply(info_list) - self._logger.info(f"Updated policies {list(rollout_info.keys())}") - + self._logger.info(f"Updated policies {list(rollout_info.keys())}") self.update_count += 1 - self._logger.debug(f"policy update time: {time.time() - t0}") + self._logger.info(f"policy update time: {time.time() - t0}") def get_state(self): if self._parallel: @@ -180,11 +174,11 @@ def __init__( self._policy_names = policy_names peers = {"policy_host": num_hosts} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) - self._logger = Logger("MULTINODE_POLICY_MANAGER", dump_folder=log_dir) + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._policy2host = {} self._host2policies = defaultdict(list) - self._logger = Logger("MULTINODE_POLICY_MANAGER", dump_folder=log_dir) + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) # assign policies to hosts for i, name in enumerate(self._policy_names): diff --git a/maro/rl/learning/synchronous/learner.py b/maro/rl/learning/synchronous/learner.py index 8b32ce931..7c46ad825 100644 --- a/maro/rl/learning/synchronous/learner.py +++ b/maro/rl/learning/synchronous/learner.py @@ -106,6 +106,7 @@ def _collect_and_update(self, ep: int): tu0 = time.time() self.policy_manager.update(rollout_info_by_policy) policy_update_time += time.time() - tu0 + self._logger.info(f"update time: {policy_update_time}") # performance details self._logger.info( diff --git a/maro/rl/learning/synchronous/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py index af9cddb33..33c60d5c3 100644 --- a/maro/rl/learning/synchronous/rollout_worker.py +++ b/maro/rl/learning/synchronous/rollout_worker.py @@ -143,6 +143,7 @@ def collect(msg): if msg.body[MsgKey.EXPLORATION_STEP]: agent_wrapper.exploration_step() + logger.info(f"Exploring with parameters: {agent_wrapper.exploration_params}") if env_wrapper.state is None: logger.info(f"Rollout episode {msg.body[MsgKey.EPISODE]}") env_wrapper.reset() diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py index 69c5d4806..24e3ad92e 100644 --- a/maro/rl/model/core_model.py +++ b/maro/rl/model/core_model.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. from abc import abstractmethod -from statistics import mean from typing import Dict, List, Union import torch @@ -108,7 +107,8 @@ def get_gradients(self, loss: torch.tensor): def apply_gradients(self, grad_dict_list: List[Dict[str, float]]): avg_grad_dict = { - param_name: mean(grad_dict[param_name] for grad_dict in grad_dict_list) for param_name in grad_dict_list[0] + param_name: torch.mean(torch.stack([grad_dict[param_name] for grad_dict in grad_dict_list]), dim=0) + for param_name in grad_dict_list[0] } for name, param in self.named_parameters(): param.grad = avg_grad_dict[name] diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index dfc7f0cfe..f3b70b3ea 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -7,7 +7,7 @@ import torch from torch.distributions import Categorical -from maro.rl.typing import DiscreteACNet, Trajectory +from maro.rl.types import DiscreteACNet, Trajectory from maro.rl.utils import get_torch_loss_cls, discount_cumsum from maro.rl.utils.remote_tools import LearnTask @@ -26,13 +26,12 @@ def __init__(self, action, logp: float, value: float): class ACBatch(Batch): - __slots__ = ["states", "actions", "rewards", "returns", "advantages", "logps"] + __slots__ = ["states", "actions", "returns", "advantages", "logps"] - def __init__(self, states, actions, rewards, returns, advantages, logps): + def __init__(self, states, actions: np.ndarray, returns: np.ndarray, advantages: np.ndarray, logps: np.ndarray): super().__init__() self.states = states self.actions = actions - self.rewards = rewards self.returns = returns self.advantages = advantages self.logps = logps @@ -93,7 +92,7 @@ def __init__( get_loss_on_rollout_finish: bool = False, remote: bool = False ): - if not isinstance(name, ac_net, DiscreteACNet): + if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") super().__init__(name, remote=remote) @@ -128,34 +127,29 @@ def get_rollout_info(self, trajectory: Trajectory): def _preprocess(self, trajectory: Trajectory): if trajectory.actions[-1]: - rewards = np.append(trajectory.rewards, trajectory.actions[-1].value) values = np.array([action_info.value for action_info in trajectory.actions]) - else: - rewards = np.append(trajectory.rewards, .0) + rewards = np.append(trajectory.rewards, trajectory.actions[-1].value) + else: values = np.append([action_info.value for action_info in trajectory.actions[:-1]], .0) + rewards = np.append(trajectory.rewards, .0) + + actions = np.array([action_info.action for action_info in trajectory.actions[:-1]]) + logps = np.array([action_info.logp for action_info in trajectory.actions[:-1]], dtype=np.float32) # Generalized advantage estimation using TD(Lambda) deltas = rewards[:-1] + self.reward_discount * values[1:] - values[:-1] advantages = discount_cumsum(deltas, self.reward_discount * self.lam) - # Returns rewards-to-go, to be targets for the value function returns = discount_cumsum(rewards, self.reward_discount)[:-1] - - return ACBatch( - states=trajectory.states, - actions=[action_info.action for action_info in trajectory.actions], - rewards=trajectory.rewards, - returns=returns, - advantages=advantages, - logps=[action_info.logp for action_info in trajectory.actions] - ) + return ACBatch(trajectory.states[:-1], actions, returns, advantages, logps) def get_batch_loss(self, batch: ACBatch, with_grad: bool = False) -> ACLossInfo: assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() - actions = torch.from_numpy(np.asarray(batch.actions)).to(self.device) - logp_batch = torch.from_numpy(np.asarray(batch.logps)).to(self.device) - advantages = torch.from_numpy(np.asarray(batch.advantages)).to(self.device) + actions = torch.from_numpy(batch.actions).to(self.device) + logp_old = torch.from_numpy(batch.logps).to(self.device) + returns = torch.from_numpy(batch.returns).to(self.device) + advantages = torch.from_numpy(batch.advantages).to(self.device) action_probs, state_values = self.ac_net(batch.states) state_values = state_values.squeeze() @@ -164,14 +158,14 @@ def get_batch_loss(self, batch: ACBatch, with_grad: bool = False) -> ACLossInfo: logp = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) logp = torch.clamp(logp, min=self.min_logp, max=.0) if self.clip_ratio is not None: - ratio = torch.exp(logp - logp_batch) + ratio = torch.exp(logp - logp_old) clipped_ratio = torch.clamp(ratio, 1 - self.clip_ratio, 1 + self.clip_ratio) actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).mean() else: actor_loss = -(logp * advantages).mean() # critic_loss - critic_loss = self.critic_loss_func(state_values, batch.returns) + critic_loss = self.critic_loss_func(state_values, returns) # entropy if self.entropy_coeff is not None: diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 5790296d9..8cd857b30 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from maro.rl.exploration.abs_exploration import AbsExploration from typing import List, Union import numpy as np import torch from maro.rl.exploration import GaussianNoiseExploration -from maro.rl.typing import ContinuousACNet, Trajectory +from maro.rl.types import ContinuousACNet, Trajectory from maro.rl.utils import get_torch_loss_cls from maro.rl.utils.remote_tools import LearnTask from maro.utils.exception.rl_toolkit_exception import InvalidExperience @@ -83,7 +84,7 @@ def __init__( q_value_loss_cls="mse", q_value_loss_coeff: float = 1.0, soft_update_coeff: float = 1.0, - exploration=GaussianNoiseExploration(), + exploration: AbsExploration = GaussianNoiseExploration(), replay_memory_capacity: int = 10000, random_overwrite: bool = False, remote: bool = False @@ -196,6 +197,10 @@ def _update_target(self): self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) self._target_ac_net_version = self._ac_net_version + @property + def exploration_params(self): + return self.exploration.parameters + def exploit(self): self.exploring = False diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index 80058b688..a54649cf2 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -7,7 +7,7 @@ import torch from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration -from maro.rl.typing import DiscreteQNet, Trajectory +from maro.rl.types import DiscreteQNet, Trajectory from maro.rl.utils.remote_tools import LearnTask from maro.utils.exception.rl_toolkit_exception import InvalidExperience @@ -20,7 +20,7 @@ class DQNBatch(Batch): An experience consists of state, action, reward, next state. """ - __slots__ = ["states", "actions", "rewards", "next_states", "indexes", "is_weights"] + __slots__ = ["states", "actions", "rewards", "next_states"] def __init__( self, @@ -28,10 +28,10 @@ def __init__( actions: list, rewards: list, next_states: list, - indexes: list, + indexes: list = None, is_weights: list = None ): - if not len(states) == len(actions) == len(rewards) == len(next_states) == len(is_weights) == len(indexes): + if not len(states) == len(actions) == len(rewards) == len(next_states): raise InvalidExperience("values of contents should consist of lists of the same length") super().__init__() self.states = states @@ -263,7 +263,7 @@ def _sample(self) -> DQNBatch: [self._replay_memory.data["actions"][idx] for idx in indexes], [self._replay_memory.data["rewards"][idx] for idx in indexes], [self._replay_memory.data["next_states"][idx] for idx in indexes], - indexes, + indexes=indexes, is_weights=is_weights ) @@ -326,6 +326,10 @@ def _update_target(self): self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) self._target_q_net_version = self._q_net_version + @property + def exploration_params(self): + return self.exploration.parameters + def exploit(self): self.exploring = False diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index e3f6ff829..7535637e7 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -6,7 +6,7 @@ import numpy as np import torch -from maro.rl.typing import DiscretePolicyNet, Trajectory +from maro.rl.types import DiscretePolicyNet, Trajectory from maro.rl.utils import discount_cumsum from maro.rl.utils.remote_tools import LearnTask @@ -25,15 +25,12 @@ def __init__(self, action, logp: float, value: float): class PGBatch(Batch): - __slots__ = ["states", "actions", "rewards", "returns", "logps"] + __slots__ = ["states", "actions", "returns", "logps"] - def __init__(self, states, actions, rewards, returns, logps): + def __init__(self, states, returns: np.array): super().__init__() self.states = states - self.actions = actions - self.rewards = rewards self.returns = returns - self.logps = logps @property def size(self): @@ -90,18 +87,8 @@ def get_rollout_info(self, trajectory: Trajectory): return trajectory def _preprocess(self, trajectory: Trajectory) -> PGBatch: - if trajectory.actions[-1]: - rewards = np.append(trajectory.rewards, trajectory.actions[-1].value) - else: - rewards = np.append(trajectory.rewards, .0) - - return PGBatch( - states=trajectory.states, - actions=[action_info.action for action_info in trajectory.actions], - rewards=trajectory.rewards, - returns=discount_cumsum(rewards, self.reward_discount)[:-1], - logps=[action_info.logp for action_info in trajectory.actions] - ) + rewards = np.append(trajectory.rewards, trajectory.actions[-1].value if trajectory.actions[-1] else .0) + return PGBatch(trajectory.states[:-1], discount_cumsum(rewards, self.reward_discount)[:-1]) def get_batch_loss(self, batch: PGBatch, with_grad: bool = False): """ diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index b267d276a..988368035 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from typing import List -from maro.rl.typing import Trajectory +from maro.rl.types import Trajectory class AbsPolicy(ABC): @@ -92,6 +92,10 @@ def explore(self): def exploration_step(self): pass + @property + def exploration_params(self): + return None + @abstractmethod def get_state(self): """Return the current state of the policy. diff --git a/maro/rl/typing/__init__.py b/maro/rl/types/__init__.py similarity index 100% rename from maro/rl/typing/__init__.py rename to maro/rl/types/__init__.py diff --git a/maro/rl/typing/model_types.py b/maro/rl/types/model_types.py similarity index 99% rename from maro/rl/typing/model_types.py rename to maro/rl/types/model_types.py index cc4f37937..0845f1070 100644 --- a/maro/rl/typing/model_types.py +++ b/maro/rl/types/model_types.py @@ -55,7 +55,7 @@ def get_action(self, states, max_prob: bool = False): Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value for each state. """ - action_probs, values = self.forward(states)[0] + action_probs, values = self.forward(states) if max_prob: probs, actions = action_probs.max(dim=1) return actions, torch.log(probs), values diff --git a/maro/rl/typing/rollout_object_types.py b/maro/rl/types/rollout_object_types.py similarity index 100% rename from maro/rl/typing/rollout_object_types.py rename to maro/rl/types/rollout_object_types.py diff --git a/maro/rl/utils/trajectory_computation.py b/maro/rl/utils/trajectory_computation.py index 03b765da3..ea27c5767 100644 --- a/maro/rl/utils/trajectory_computation.py +++ b/maro/rl/utils/trajectory_computation.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import scipy +import numpy as np +import scipy.signal def discount_cumsum(x, discount): @@ -25,4 +26,4 @@ def discount_cumsum(x, discount): x1 + discount * x2, x2] """ - return scipy.signal.lfilter([1], [1, float(-discount)], x[::-1], axis=0)[::-1] + return np.array(scipy.signal.lfilter([1], [1, float(-discount)], x[::-1], axis=0)[::-1], dtype=np.float32) diff --git a/maro/rl/wrappers/agent_wrapper.py b/maro/rl/wrappers/agent_wrapper.py index 8b71bc177..5caa6d257 100644 --- a/maro/rl/wrappers/agent_wrapper.py +++ b/maro/rl/wrappers/agent_wrapper.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from collections import defaultdict from typing import Dict from maro.rl.policy import AbsPolicy, RLPolicy @@ -18,11 +17,7 @@ class AgentWrapper: agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's queries to the correct policy. """ - def __init__( - self, - create_policy_func_dict: Dict[str, AbsPolicy], - agent2policy: Dict[str, str] - ): + def __init__(self, create_policy_func_dict: Dict[str, AbsPolicy], agent2policy: Dict[str, str]): self.policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} self.agent2policy = agent2policy self.policy = { @@ -52,6 +47,12 @@ def set_policy_states(self, policy_state_dict: dict): for policy_id, policy_state in policy_state_dict.items(): self.policy_dict[policy_id].set_state(policy_state) + @property + def exploration_params(self): + return { + name: policy.exploration_params for name, policy in self.policy_dict.items() if isinstance(policy, RLPolicy) + } + def exploration_step(self): for policy in self.policy_dict.values(): if hasattr(policy, "exploration_step"): diff --git a/maro/rl/wrappers/env_wrapper.py b/maro/rl/wrappers/env_wrapper.py index 6a5ca35e3..80760a02d 100644 --- a/maro/rl/wrappers/env_wrapper.py +++ b/maro/rl/wrappers/env_wrapper.py @@ -6,7 +6,7 @@ from typing import Callable from maro.simulator import Env -from maro.rl.typing import Trajectory, Transition +from maro.rl.types import Trajectory, Transition class AbsEnvWrapper(ABC): diff --git a/requirements.dev.txt b/requirements.dev.txt index 156ea736f..a21ff0947 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -20,7 +20,7 @@ torchsummary==1.5.1 wrapt==1.11.2 zmq==0.0.0 numpy<1.20.0 -scipy +scipy==1.7.0 tabulate==0.8.5 networkx==2.4 palettable==3.3.0 From f0222a709a24def4fa9440a3309c611bbeb7fdda Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 18 Aug 2021 10:03:25 +0000 Subject: [PATCH 408/482] fixed bug in distributed policy manager --- examples/rl/cim/ac.py | 2 +- maro/rl/learning/policy_host.py | 7 ++++--- maro/rl/learning/policy_manager.py | 11 +++++++---- maro/rl/policy/ac.py | 10 +++++----- maro/rl/policy/ddpg.py | 8 ++++---- maro/rl/policy/dqn.py | 6 +++--- maro/rl/policy/pg.py | 8 ++++---- maro/rl/policy/policy.py | 4 ++-- maro/rl/utils/remote_tools/worker.py | 2 +- 9 files changed, 31 insertions(+), 27 deletions(-) diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 3b2f56cd9..605efcfb0 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -56,7 +56,7 @@ "entropy_coeff": 0.01, # "clip_ratio": 0.8 # for PPO "lam": 0.9, - "get_loss_on_rollout_finish": False + "get_loss_on_rollout_finish": True } diff --git a/maro/rl/learning/policy_host.py b/maro/rl/learning/policy_host.py index d450410ea..3a1042592 100644 --- a/maro/rl/learning/policy_host.py +++ b/maro/rl/learning/policy_host.py @@ -42,10 +42,10 @@ def policy_host( break if msg.tag == MsgTag.INIT_POLICIES: - logger.info(f"Received an INIT_POLICIES msg") for name in msg.body[MsgKey.POLICY_NAMES]: policy_dict[name] = create_policy_func_dict[name](name) + logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_NAMES]}") proxy.reply( msg, tag=MsgTag.INIT_POLICIES_DONE, @@ -57,14 +57,15 @@ def policy_host( if isinstance(info_list[0], Trajectory): policy_dict[name].learn_from_multi_trajectories(info_list) elif isinstance(info_list[0], LossInfo): - policy_dict[name].apply(info_list) + logger.info("apply loss info") + policy_dict[name].update_with_multi_loss_info(info_list) else: raise TypeError( f"Roll-out information must be of type 'Trajectory' or 'LossInfo', " f"got {type(info_list[0])}" ) msg_body = { - MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.TRAJECTORIES]} + MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.ROLLOUT_INFO]} } logger.debug(f"total policy update time: {time.time() - t0}") proxy.reply(msg, tag=MsgTag.LEARN_DONE, body=msg_body) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index d42fd555d..d4281240a 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -80,7 +80,7 @@ def _policy_host(name, create_policy_func, conn): if isinstance(info_list[0], Trajectory): policy.learn_from_multi_trajectories(info_list) elif isinstance(info_list[0], LossInfo): - policy.apply(info_list) + policy.update_with_multi_loss_info(info_list) else: raise TypeError( f"Roll-out information must be of type 'Trajectory' or 'LossInfo', " @@ -126,7 +126,7 @@ def update(self, rollout_info: Dict[str, list]): if isinstance(info_list[0], Trajectory): self._policy_dict[policy_name].learn_from_multi_trajectories(info_list) elif isinstance(info_list[0], LossInfo): - self._policy_dict[policy_name].apply(info_list) + self._policy_dict[policy_name].update_with_multi_loss_info(info_list) self._logger.info(f"Updated policies {list(rollout_info.keys())}") self.update_count += 1 @@ -186,19 +186,22 @@ def __init__( self._policy2host[name] = f"POLICY_HOST.{host_id}" self._host2policies[f"POLICY_HOST.{host_id}"].append(name) + self._logger.info(f"Policy assignment: {self._policy2host}") + # ask the hosts to initialize the assigned policies for host_name, policy_names in self._host2policies.items(): - self._proxy.send(SessionMessage( + self._proxy.isend(SessionMessage( MsgTag.INIT_POLICIES, self._proxy.name, host_name, body={MsgKey.POLICY_NAMES: policy_names} )) # cache the initial policy states self._state_cache, dones = {}, 0 for msg in self._proxy.receive(): + self._logger.info(f"received a msg of tag {msg.tag}") if msg.tag == MsgTag.INIT_POLICIES_DONE: for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): self._state_cache[policy_name] = policy_state - self._logger.info(f"Policy {policy_name} initialized") + self._logger.info(f"Cached state for policy {policy_name}") dones += 1 if dones == num_hosts: break diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index f3b70b3ea..44af06cf0 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -121,7 +121,7 @@ def choose_action(self, states) -> Union[ACActionInfo, List[ACActionInfo]]: def get_rollout_info(self, trajectory: Trajectory): if self._get_loss_on_rollout_finish: - return self.get_batch_loss(self._preprocess(trajectory)) + return self.get_batch_loss(self._preprocess(trajectory), explicit_grad=True) else: return trajectory @@ -143,7 +143,7 @@ def _preprocess(self, trajectory: Trajectory): returns = discount_cumsum(rewards, self.reward_discount)[:-1] return ACBatch(trajectory.states[:-1], actions, returns, advantages, logps) - def get_batch_loss(self, batch: ACBatch, with_grad: bool = False) -> ACLossInfo: + def get_batch_loss(self, batch: ACBatch, explicit_grad: bool = False) -> ACLossInfo: assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() actions = torch.from_numpy(batch.actions).to(self.device) @@ -175,10 +175,10 @@ def get_batch_loss(self, batch: ACBatch, with_grad: bool = False) -> ACLossInfo: # total loss loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy - grad=self.ac_net.get_gradients(loss) if with_grad else None + grad=self.ac_net.get_gradients(loss) if explicit_grad else None return ACLossInfo(actor_loss, critic_loss, entropy, loss, grad=grad) - def apply(self, loss_info_list: List[ACLossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[ACLossInfo]): """Apply gradients to the underlying parameterized model.""" self.ac_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) @@ -189,7 +189,7 @@ def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): else: batches = [self._preprocess(traj) for traj in trajectories] for _ in range(self.grad_iters): - self.apply([self.get_batch_loss(batch, with_grad=True) for batch in batches]) + self.update_with_multi_loss_info([self.get_batch_loss(batch, explicit_grad=True) for batch in batches]) def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 8cd857b30..85a3a970e 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -150,7 +150,7 @@ def _get_batch(self) -> DDPGBatch: [self._replay_memory.data["next_states"][idx] for idx in indexes] ) - def get_batch_loss(self, batch: DDPGBatch, with_grad: bool = False) -> DDPGLossInfo: + def get_batch_loss(self, batch: DDPGBatch, explicit_grad: bool = False) -> DDPGLossInfo: assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() states, next_states = batch.states, batch.next_states @@ -169,10 +169,10 @@ def get_batch_loss(self, batch: DDPGBatch, with_grad: bool = False) -> DDPGLossI # total loss loss = policy_loss + self.q_value_loss_coeff * q_loss - grad = self.ac_net.get_gradients(loss) if with_grad else None + grad = self.ac_net.get_gradients(loss) if explicit_grad else None return DDPGLossInfo(policy_loss, q_loss, loss, grad=grad) - def apply(self, loss_info_list: List[DDPGLossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[DDPGLossInfo]): self.ac_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) if self._ac_net_version - self._target_ac_net_version == self.update_target_every: self._update_target() @@ -186,7 +186,7 @@ def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): pass else: for _ in range(self.num_epochs): - loss_info = self.get_batch_loss(self._get_batch(), with_grad=False) + loss_info = self.get_batch_loss(self._get_batch(), explicit_grad=False) self.ac_net.step(loss_info.loss) self._ac_net_version += 1 if self._ac_net_version - self._target_ac_net_version == self.update_target_every: diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index a54649cf2..24424e4a4 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -267,7 +267,7 @@ def _sample(self) -> DQNBatch: is_weights=is_weights ) - def get_batch_loss(self, batch: DQNBatch, with_grad: bool = False): + def get_batch_loss(self, batch: DQNBatch, explicit_grad: bool = False): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() states, next_states = batch.states, batch.next_states @@ -293,10 +293,10 @@ def get_batch_loss(self, batch: DQNBatch, with_grad: bool = False): else: loss = self._loss_func(q_values, target_q_values) - grad = self.q_net.get_gradients(loss) if with_grad else None + grad = self.q_net.get_gradients(loss) if explicit_grad else None return DQNLossInfo(loss, td_errors, batch.indexes, grad=grad) - def apply(self, loss_info_list: List[DQNLossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[DQNLossInfo]): if self.prioritized_replay: for loss_info in loss_info_list: self._sampler.update(loss_info.indexes, loss_info.td_errors) diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index 7535637e7..b210d23df 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -90,7 +90,7 @@ def _preprocess(self, trajectory: Trajectory) -> PGBatch: rewards = np.append(trajectory.rewards, trajectory.actions[-1].value if trajectory.actions[-1] else .0) return PGBatch(trajectory.states[:-1], discount_cumsum(rewards, self.reward_discount)[:-1]) - def get_batch_loss(self, batch: PGBatch, with_grad: bool = False): + def get_batch_loss(self, batch: PGBatch, explicit_grad: bool = False): """ This should be called at the end of a simulation episode and the experiences obtained from the experience store's ``get`` method should be a sequential set, i.e., in the order in @@ -104,10 +104,10 @@ def get_batch_loss(self, batch: PGBatch, with_grad: bool = False): _, logp = self.policy_net(states) loss = -(logp * returns).mean() - grad = self.policy_net.get_gradients(loss) if with_grad else None + grad = self.policy_net.get_gradients(loss) if explicit_grad else None return PGLossInfo(loss, grad=grad) - def apply(self, loss_info_list: List[PGLossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[PGLossInfo]): """Apply gradients to the underlying parameterized model.""" self.policy_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) @@ -118,7 +118,7 @@ def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): else: batches = [self._preprocess(traj) for traj in trajectories] for _ in range(self.grad_iters): - self.apply([self.get_batch_loss(batch, with_grad=True) for batch in batches]) + self.update_with_multi_loss_info([self.get_batch_loss(batch, explicit_grad=True) for batch in batches]) def set_state(self, policy_state): self.policy_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 988368035..736bdaa28 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -71,11 +71,11 @@ def get_rollout_info(self, trajectory: Trajectory): return trajectory @abstractmethod - def get_batch_loss(self, batch: Batch, with_grad: bool = False): + def get_batch_loss(self, batch: Batch, explicit_grad: bool = False): raise NotImplementedError @abstractmethod - def apply(self, loss_info_list: List[LossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[LossInfo]): pass @abstractmethod diff --git a/maro/rl/utils/remote_tools/worker.py b/maro/rl/utils/remote_tools/worker.py index 978fb94af..27f6f3eb9 100644 --- a/maro/rl/utils/remote_tools/worker.py +++ b/maro/rl/utils/remote_tools/worker.py @@ -47,6 +47,6 @@ def worker( policy_name = task.policy_name if policy_name not in policy_dict: policy_dict[policy_name] = create_policy_func_dict[policy_name]() - msg_body = {MsgKey.LOSS_INFO: policy_dict[policy_name].get_loss_info(task.batch, with_grad=True)} + msg_body = {MsgKey.LOSS_INFO: policy_dict[policy_name].get_loss_info(task.batch, explicit_grad=True)} logger.debug(f"total policy update time: {time.time() - t0}") proxy.reply(msg, tag=MsgTag.COMPUTE_GRAD_DONE, body=msg_body) From c0a84801a269140be85a315f1b8e4aebb21b042f Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 18 Aug 2021 10:56:44 +0000 Subject: [PATCH 409/482] fixed lint issues --- maro/rl/learning/__init__.py | 4 ++-- maro/rl/learning/policy_manager.py | 2 +- maro/rl/policy/__init__.py | 8 ++++---- maro/rl/policy/ac.py | 7 +++---- maro/rl/policy/ddpg.py | 8 +++----- maro/rl/policy/dqn.py | 3 +-- maro/rl/policy/pg.py | 1 - maro/rl/policy/policy.py | 5 ++--- maro/rl/policy/replay.py | 2 +- maro/rl/types/model_types.py | 2 +- maro/rl/utils/trajectory_computation.py | 12 ++++-------- maro/rl/wrappers/env_wrapper.py | 6 +++--- 12 files changed, 25 insertions(+), 35 deletions(-) diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index 438d3de2e..6adbdaddc 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -3,12 +3,12 @@ from .early_stopper import AbsEarlyStopper from .policy_host import policy_host -from .policy_manager import AbsPolicyManager, SimplePolicyManager, DistributedPolicyManager +from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager from .simple_learner import SimpleLearner __all__ = [ "AbsEarlyStopper", "policy_host", - "AbsPolicyManager", "SimplePolicyManager", "DistributedPolicyManager", + "AbsPolicyManager", "DistributedPolicyManager", "SimplePolicyManager", "SimpleLearner" ] diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index d4281240a..f0801f6af 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -9,7 +9,7 @@ from typing import Callable, Dict, List from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.policy import LossInfo, RLPolicy +from maro.rl.policy import LossInfo from maro.rl.types import Trajectory from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 586b8b472..c92146f87 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -2,11 +2,11 @@ # Licensed under the MIT license. from .ac import ACActionInfo, ACBatch, ACLossInfo, ActorCritic, DiscreteACNet -from .ddpg import DDPG, DDPGBatch, DDPGLossInfo, ContinuousACNet -from .dqn import DQN, DQNBatch, DQNLossInfo, DiscreteQNet, PrioritizedSampler -from .pg import PGActionInfo, PGBatch, PGLossInfo, DiscretePolicyNet, PolicyGradient -from .policy import AbsPolicy, Batch, LossInfo, NullPolicy, RLPolicy +from .ddpg import DDPG, ContinuousACNet, DDPGBatch, DDPGLossInfo +from .dqn import DQN, DiscreteQNet, DQNBatch, DQNLossInfo, PrioritizedSampler from .index import get_model_cls, get_policy_cls +from .pg import DiscretePolicyNet, PGActionInfo, PGBatch, PGLossInfo, PolicyGradient +from .policy import AbsPolicy, Batch, LossInfo, NullPolicy, RLPolicy __all__ = [ "ACActionInfo", "ACBatch", "ACLossInfo", "ActorCritic", "DiscreteACNet", diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 44af06cf0..74c9beb2a 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -8,8 +8,7 @@ from torch.distributions import Categorical from maro.rl.types import DiscreteACNet, Trajectory -from maro.rl.utils import get_torch_loss_cls, discount_cumsum -from maro.rl.utils.remote_tools import LearnTask +from maro.rl.utils import discount_cumsum, get_torch_loss_cls from .policy import Batch, LossInfo, RLPolicy @@ -129,7 +128,7 @@ def _preprocess(self, trajectory: Trajectory): if trajectory.actions[-1]: values = np.array([action_info.value for action_info in trajectory.actions]) rewards = np.append(trajectory.rewards, trajectory.actions[-1].value) - else: + else: values = np.append([action_info.value for action_info in trajectory.actions[:-1]], .0) rewards = np.append(trajectory.rewards, .0) @@ -175,7 +174,7 @@ def get_batch_loss(self, batch: ACBatch, explicit_grad: bool = False) -> ACLossI # total loss loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy - grad=self.ac_net.get_gradients(loss) if explicit_grad else None + grad = self.ac_net.get_gradients(loss) if explicit_grad else None return ACLossInfo(actor_loss, critic_loss, entropy, loss, grad=grad) def update_with_multi_loss_info(self, loss_info_list: List[ACLossInfo]): diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 85a3a970e..27fe821ab 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -1,16 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.rl.exploration.abs_exploration import AbsExploration from typing import List, Union import numpy as np import torch -from maro.rl.exploration import GaussianNoiseExploration +from maro.rl.exploration import GaussianNoiseExploration, NoiseExploration from maro.rl.types import ContinuousACNet, Trajectory from maro.rl.utils import get_torch_loss_cls -from maro.rl.utils.remote_tools import LearnTask from maro.utils.exception.rl_toolkit_exception import InvalidExperience from .policy import Batch, LossInfo, RLPolicy @@ -39,7 +37,7 @@ def size(self): class DDPGLossInfo(LossInfo): - + __slots__ = ["policy_loss", "q_loss"] def __init__(self, loss, policy_loss, q_loss, grad=None): @@ -84,7 +82,7 @@ def __init__( q_value_loss_cls="mse", q_value_loss_coeff: float = 1.0, soft_update_coeff: float = 1.0, - exploration: AbsExploration = GaussianNoiseExploration(), + exploration: NoiseExploration = GaussianNoiseExploration(), replay_memory_capacity: int = 10000, random_overwrite: bool = False, remote: bool = False diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index 24424e4a4..b58431edf 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -8,7 +8,6 @@ from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.types import DiscreteQNet, Trajectory -from maro.rl.utils.remote_tools import LearnTask from maro.utils.exception.rl_toolkit_exception import InvalidExperience from .policy import Batch, LossInfo, RLPolicy @@ -47,7 +46,7 @@ def size(self): class DQNLossInfo(LossInfo): - + __slots__ = ["td_errors", "indexes"] def __init__(self, loss, td_errors, indexes, grad=None): diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index b210d23df..26825cb89 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -8,7 +8,6 @@ from maro.rl.types import DiscretePolicyNet, Trajectory from maro.rl.utils import discount_cumsum -from maro.rl.utils.remote_tools import LearnTask from .policy import Batch, LossInfo, RLPolicy diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 736bdaa28..7d44735bb 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -9,10 +9,10 @@ class AbsPolicy(ABC): """Abstract policy class. - + Args: name (str): Unique identifier for the policy. - + """ def __init__(self, name: str): super().__init__() @@ -57,7 +57,6 @@ class RLPolicy(AbsPolicy): Args: name (str): Name of the policy. - data_parallel (bool): If true, """ def __init__(self, name: str, remote: bool = False): super().__init__(name) diff --git a/maro/rl/policy/replay.py b/maro/rl/policy/replay.py index 979d5d082..4137a2d61 100644 --- a/maro/rl/policy/replay.py +++ b/maro/rl/policy/replay.py @@ -67,7 +67,7 @@ def put(self, batch): else: # using the negative index convention for convenience start_index = self._size - self._capacity - indexes = list(range(start_index, start_index + added_size)) + indexes = list(range(start_index, start_index + added_size)) for key in self.data: for idx, val in zip(indexes, getattr(batch, key)): diff --git a/maro/rl/types/model_types.py b/maro/rl/types/model_types.py index 0845f1070..bb468a9da 100644 --- a/maro/rl/types/model_types.py +++ b/maro/rl/types/model_types.py @@ -58,7 +58,7 @@ def get_action(self, states, max_prob: bool = False): action_probs, values = self.forward(states) if max_prob: probs, actions = action_probs.max(dim=1) - return actions, torch.log(probs), values + return actions, torch.log(probs), values else: action_probs = Categorical(action_probs) # (batch_size, action_space_size) actions = action_probs.sample() diff --git a/maro/rl/utils/trajectory_computation.py b/maro/rl/utils/trajectory_computation.py index ea27c5767..94afb51cb 100644 --- a/maro/rl/utils/trajectory_computation.py +++ b/maro/rl/utils/trajectory_computation.py @@ -15,15 +15,11 @@ def discount_cumsum(x, discount): For details about the scipy function, see: https://docs.scipy.org/doc/scipy/reference/tutorial/signal.html#difference-equation-filtering - input: - vector x, - [x0, - x1, - x2] + input: + vector x, + [x0, x1, x2] output: - [x0 + discount * x1 + discount^2 * x2, - x1 + discount * x2, - x2] + [x0 + discount * x1 + discount^2 * x2, x1 + discount * x2, x2] """ return np.array(scipy.signal.lfilter([1], [1, float(-discount)], x[::-1], axis=0)[::-1], dtype=np.float32) diff --git a/maro/rl/wrappers/env_wrapper.py b/maro/rl/wrappers/env_wrapper.py index 80760a02d..5efa8910f 100644 --- a/maro/rl/wrappers/env_wrapper.py +++ b/maro/rl/wrappers/env_wrapper.py @@ -5,8 +5,8 @@ from collections import defaultdict, deque from typing import Callable -from maro.simulator import Env from maro.rl.types import Trajectory, Transition +from maro.simulator import Env class AbsEnvWrapper(ABC): @@ -196,7 +196,7 @@ def get_trajectory(self, clear_buffer: bool = True): del buf["info"] else: states = buf["states"][:] - actions = buf["actions"][:] + actions = buf["actions"][:] rewards = buf["rewards"][:-1] info = buf["info"][:-1] if clear_buffer: @@ -205,7 +205,7 @@ def get_trajectory(self, clear_buffer: bool = True): del buf["rewards"][:-1] del buf["info"][:-1] - trajectory[agent_id] = Trajectory(states, actions, rewards, info) + trajectory[agent_id] = Trajectory(states, actions, rewards, info) return trajectory From 3a10544963e3f90b0c636a0cc9f587b6059ce5bc Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 18 Aug 2021 10:59:55 +0000 Subject: [PATCH 410/482] fixed lint issues --- maro/rl/policy/ac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 74c9beb2a..d5ae072d4 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -8,7 +8,7 @@ from torch.distributions import Categorical from maro.rl.types import DiscreteACNet, Trajectory -from maro.rl.utils import discount_cumsum, get_torch_loss_cls +from maro.rl.utils import discount_cumsum, get_torch_loss_cls from .policy import Batch, LossInfo, RLPolicy From 026bcd3b48c57f88e49d86927ab406536a3d44bd Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 18 Aug 2021 12:26:21 +0000 Subject: [PATCH 411/482] added scipy in setup --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8bdab98f8..71b7aedf0 100644 --- a/setup.py +++ b/setup.py @@ -127,6 +127,7 @@ install_requires=[ # TODO: use a helper function to collect these "numpy<1.20.0", + "scipy<=1.7.0", "torch<1.8.0", "holidays>=0.10.3", "pyaml>=20.4.0", From 00df5d8b597490c2b9bcd57e6ed93a4ccc1fb3f8 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 19 Aug 2021 15:58:06 +0000 Subject: [PATCH 412/482] 1. trimmed rollout manager code; 2. added option to docker scripts --- examples/rl/cim/ac.py | 2 +- examples/rl/cim/callbacks.py | 2 +- examples/rl/cim/policy_index.py | 2 +- .../rl/scripts/docker/docker_compose_yml.py | 243 +++++----- examples/rl/scripts/docker/kill.sh | 5 +- examples/rl/scripts/docker/run.sh | 5 +- .../workflows/asynchronous/policy_server.py | 2 +- examples/rl/workflows/config.yml | 23 +- examples/rl/workflows/general.py | 3 +- examples/rl/workflows/synchronous/learner.py | 40 +- .../workflows/synchronous/rollout_worker.py | 10 +- maro/rl/learning/policy_manager.py | 3 +- maro/rl/learning/synchronous/__init__.py | 9 +- .../learning/synchronous/rollout_manager.py | 415 +++++++++--------- .../rl/learning/synchronous/rollout_worker.py | 237 ++-------- maro/rl/utils/message_enums.py | 11 +- maro/rl/wrappers/env_wrapper.py | 10 +- 17 files changed, 444 insertions(+), 578 deletions(-) diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 605efcfb0..3b2f56cd9 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -56,7 +56,7 @@ "entropy_coeff": 0.01, # "clip_ratio": 0.8 # for PPO "lam": 0.9, - "get_loss_on_rollout_finish": True + "get_loss_on_rollout_finish": False } diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py index f19b30a67..b64b1604c 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/rl/cim/callbacks.py @@ -15,7 +15,7 @@ def post_collect(trackers, ep, segment): # print the env metric from each rollout worker for tracker in trackers: - simulation_logger.info(f"env summary (episode {ep}, segement {segment}): {tracker['env_metric']}") + simulation_logger.info(f"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}") # print the average env metric if len(trackers) > 1: diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index 2bc657351..3eb24c645 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -16,5 +16,5 @@ warmup = {name: 1 for name in AGENT_IDS} # use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {name: get_ac_policy for name in AGENT_IDS} +rl_policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} agent2policy = {name: name for name in AGENT_IDS} diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 988415916..d537a48cc 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -1,132 +1,149 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import argparse import yaml from copy import deepcopy from os.path import dirname, join, realpath -path = realpath(__file__) -docker_script_dir = dirname(path) -rl_example_dir = dirname(dirname(docker_script_dir)) -root_dir = dirname(dirname(rl_example_dir)) -workflow_dir = join(rl_example_dir, "workflows") -maro_rl_dir = join(root_dir, "maro", "rl") -maro_sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") -config_path = join(workflow_dir, "config.yml") -dockerfile_path = join(root_dir, "docker_files", "dev.df") -with open(config_path, "r") as fp: - config = yaml.safe_load(fp) +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--namespace', help="job namespace", default="maro") + args = parser.parse_args() + + namespace = args.namespace + path = realpath(__file__) + docker_script_dir = dirname(path) + rl_example_dir = dirname(dirname(docker_script_dir)) + root_dir = dirname(dirname(rl_example_dir)) + workflow_dir = join(rl_example_dir, "workflows") + maro_rl_dir = join(root_dir, "maro", "rl") + maro_sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") + config_path = join(workflow_dir, "config.yml") + dockerfile_path = join(root_dir, "docker_files", "dev.df") + + with open(config_path, "r") as fp: + config = yaml.safe_load(fp) + redis_host = config["redis"]["host"] + docker_compose_manifest = { + "version": "3.9", + "services": {"redis": {"image": "redis:6", "container_name": f"{namespace}.{redis_host}"}} + } + common_spec = { + "build": {"context": root_dir, "dockerfile": dockerfile_path}, + "image": "marorl", + "volumes": [ + f"{rl_example_dir}:/maro/rl_examples", + f"{maro_rl_dir}:/maro/maro/rl", + f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" + ] + } -docker_compose_manifest = {"version": "3.9", "services": {"redis": {"image": "redis:6", "container_name": redis_host}}} -common_spec = { - "build": {"context": root_dir, "dockerfile": dockerfile_path}, - "image": "marorl", - "volumes": [ - f"{rl_example_dir}:/maro/rl_examples", - f"{maro_rl_dir}:/maro/maro/rl", - f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" + common_env = [ + f"REDISHOST={namespace}.{redis_host}", + f"REDISPORT={config['redis']['port']}", + f"JOB={config['job']}", + f"SCENARIO={config['scenario']}", + f"MODE={config['mode']}", + f"EXPDIST={'1' if config['rollout_experience_distribution'] else '0'}" ] -} - -common_env = [ - f"REDISHOST={config['redis']['host']}", - f"REDISPORT={config['redis']['port']}", - f"JOB={config['job']}", - f"SCENARIO={config['scenario']}", - f"MODE={config['mode']}", - f"EXPDIST={'1' if config['rollout_experience_distribution'] else '0'}" -] -if config["mode"] == "sync": - common_env.append(f"NUMWORKERS={config['sync']['num_rollout_workers']}") -else: - common_env.append(f"NUMACTORS={config['async']['num_actors']}") + if config["mode"] == "async": + num_rollouts = config['async']['num_actors'] + elif config["sync"]["rollout_type"] == "simple": + num_rollouts = config['sync']['simple']['parallelism'] + else: + num_rollouts = config['sync']['distributed']['num_workers'] -# host spec -if config["policy_manager"]["type"] == "distributed": - for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): - str_id = f"policy_host.{host_id}" - host_spec = deepcopy(common_spec) - del host_spec["build"] - host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/policy_host.py" - host_spec["container_name"] = str_id - host_spec["environment"] = [ - f"HOSTID={host_id}", - f"LEARNGROUP={config['policy_manager']['distributed']['learn_group']}" - ] + common_env - docker_compose_manifest["services"][str_id] = host_spec + common_env.append(f"NUMROLLOUTS={num_rollouts}") -mode = config["mode"] -if mode == "sync": - # learner_spec - docker_compose_manifest["services"]["learner"] = { - **common_spec, - **{ - "container_name": "learner", - "command": "python3 /maro/rl_examples/workflows/synchronous/learner.py", - "environment": [ - f"ROLLOUTMODE={config['sync']['rollout_mode']}", - f"NUMSTEPS={config['num_steps']}", - f"MAXLAG={config['max_lag']}", - f"MINFINISH={config['sync']['min_finished_workers']}", - f"MAXEXRECV={config['sync']['max_extra_recv_tries']}", - f"MAXRECVTIMEO={config['sync']['extra_recv_timeout']}", - f"ROLLOUTGROUP={config['sync']['rollout_group']}", - f"NUMEPISODES={config['num_episodes']}", - f"EVALSCH={config['eval_schedule']}", - f"POLICYMANAGERTYPE={config['policy_manager']['type']}", - f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}", - f"LEARNGROUP={config['policy_manager']['distributed']['learn_group']}", - f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}" + # host spec + if config["policy_manager"]["type"] == "distributed": + for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): + str_id = f"policy_host.{host_id}" + host_spec = deepcopy(common_spec) + del host_spec["build"] + host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/policy_host.py" + host_spec["container_name"] = f"{namespace}.{str_id}" + host_spec["environment"] = [ + f"HOSTID={host_id}", + f"LEARNGROUP={config['policy_manager']['distributed']['group']}" ] + common_env + docker_compose_manifest["services"][str_id] = host_spec + + mode = config["mode"] + if mode == "sync": + # learner_spec + docker_compose_manifest["services"]["learner"] = { + **common_spec, + **{ + "container_name": f"{namespace}.learner", + "command": "python3 /maro/rl_examples/workflows/synchronous/learner.py", + "environment": [ + f"ROLLOUTTYPE={config['sync']['rollout_type']}", + f"EVALPARALLELISM={config['sync']['simple']['eval_parallelism']}", + f"ROLLOUTGROUP={config['sync']['distributed']['group']}", + f"NUMEPISODES={config['num_episodes']}", + f"EVALSCH={config['eval_schedule']}", + f"NUMEVALWORKERS={config['sync']['distributed']['num_eval_workers']}", + f"NUMSTEPS={config['num_steps']}", + f"MAXLAG={config['max_lag']}", + f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", + f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", + f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}", + f"POLICYMANAGERTYPE={config['policy_manager']['type']}", + f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}", + f"LEARNGROUP={config['policy_manager']['distributed']['group']}", + f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}" + ] + common_env + } } - } - # rollout worker spec - if config["sync"]["rollout_mode"] == "multi-node": - for worker_id in range(config["sync"]["num_rollout_workers"]): - str_id = f"rollout_worker.{worker_id}" - worker_spec = deepcopy(common_spec) - del worker_spec["build"] - worker_spec["command"] = "python3 /maro/rl_examples/workflows/synchronous/rollout_worker.py" - worker_spec["container_name"] = str_id - worker_spec["environment"] = [ - f"WORKERID={worker_id}", - f"ROLLOUTGROUP={config['sync']['rollout_group']}", - f"EVALSCH={config['eval_schedule']}" - ] + common_env - docker_compose_manifest["services"][str_id] = worker_spec -elif mode == "async": - # policy server spec - docker_compose_manifest["services"]["policy_server"] = { - **common_spec, - **{ - "container_name": "policy_server", - "command": "python3 /maro/rl_examples/workflows/asynchronous/policy_server.py", - "environment": [ + # rollout worker spec + if config["sync"]["rollout_type"] == "distributed": + for worker_id in range(config["sync"]["distributed"]["num_workers"]): + str_id = f"rollout_worker.{worker_id}" + worker_spec = deepcopy(common_spec) + del worker_spec["build"] + worker_spec["command"] = "python3 /maro/rl_examples/workflows/synchronous/rollout_worker.py" + worker_spec["container_name"] = f"{namespace}.{str_id}" + worker_spec["environment"] = [ + f"WORKERID={worker_id}", + f"ROLLOUTGROUP={config['sync']['distributed']['group']}", + f"EVALSCH={config['eval_schedule']}" + ] + common_env + docker_compose_manifest["services"][str_id] = worker_spec + elif mode == "async": + # policy server spec + docker_compose_manifest["services"]["policy_server"] = { + **common_spec, + **{ + "container_name": f"{namespace}.policy_server", + "command": "python3 /maro/rl_examples/workflows/asynchronous/policy_server.py", + "environment": [ + f"GROUP={config['async']['group']}", + f"MAXLAG={config['max_lag']}" + ] + common_env + } + } + # actor spec + for actor_id in range(config["async"]["num_actors"]): + str_id = f"actor.{actor_id}" + actor_spec = deepcopy(common_spec) + del actor_spec["build"] + actor_spec["command"] = "python3 /maro/rl_examples/workflows/asynchronous/actor.py" + actor_spec["container_name"] = f"{namespace}.{str_id}" + actor_spec["environment"] = [ + f"ACTORID={actor_id}", f"GROUP={config['async']['group']}", - f"MAXLAG={config['max_lag']}" + f"NUMEPISODES={config['num_episodes']}", + f"NUMSTEPS={config['num_steps']}", + f"EVALSCH={config['eval_schedule']}" ] + common_env - } - } - # actor spec - for actor_id in range(config["async"]["num_actors"]): - str_id = f"actor.{actor_id}" - actor_spec = deepcopy(common_spec) - del actor_spec["build"] - actor_spec["command"] = "python3 /maro/rl_examples/workflows/asynchronous/actor.py" - actor_spec["container_name"] = str_id - actor_spec["environment"] = [ - f"ACTORID={actor_id}", - f"GROUP={config['async']['group']}", - f"NUMEPISODES={config['num_episodes']}", - f"NUMSTEPS={config['num_steps']}", - f"EVALSCH={config['eval_schedule']}" - ] + common_env - docker_compose_manifest["services"][str_id] = actor_spec -else: - raise ValueError(f"mode must be 'sync' or 'async', got {mode}") + docker_compose_manifest["services"][str_id] = actor_spec + else: + raise ValueError(f"mode must be 'sync' or 'async', got {mode}") -with open(join(docker_script_dir, "docker-compose.yml"), "w") as fp: - yaml.safe_dump(docker_compose_manifest, fp) + with open(join(docker_script_dir, "yq.yml"), "w") as fp: + yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/rl/scripts/docker/kill.sh b/examples/rl/scripts/docker/kill.sh index b67e3f298..b15c69231 100644 --- a/examples/rl/scripts/docker/kill.sh +++ b/examples/rl/scripts/docker/kill.sh @@ -1,8 +1,9 @@ #!/bin/bash +NAMESPACE=${1:-maro} BASEDIR=$(dirname "$0") # script to kill a previously launched training job. -docker-compose -f $BASEDIR/docker-compose.yml down +docker-compose -f $BASEDIR/yq.yml --project-name $NAMESPACE down -rm $BASEDIR/docker-compose.yml \ No newline at end of file +rm $BASEDIR/yq.yml \ No newline at end of file diff --git a/examples/rl/scripts/docker/run.sh b/examples/rl/scripts/docker/run.sh index d8665c177..41fa10ac9 100644 --- a/examples/rl/scripts/docker/run.sh +++ b/examples/rl/scripts/docker/run.sh @@ -1,7 +1,8 @@ #!/bin/bash +NAMESPACE=${1:-maro} BASEDIR=$(dirname "$0") # script to run the multi-container mode. -python3 $BASEDIR/docker_compose_yml.py -docker-compose -f $BASEDIR/docker-compose.yml up \ No newline at end of file +python3 $BASEDIR/docker_compose_yml.py --namespace $NAMESPACE +docker-compose -f $BASEDIR/yq.yml --project-name $NAMESPACE up \ No newline at end of file diff --git a/examples/rl/workflows/asynchronous/policy_server.py b/examples/rl/workflows/asynchronous/policy_server.py index 53b91bbed..29ea4ce75 100644 --- a/examples/rl/workflows/asynchronous/policy_server.py +++ b/examples/rl/workflows/asynchronous/policy_server.py @@ -19,7 +19,7 @@ policy_server( getenv("GROUP", default="ASYNC"), get_policy_manager(), - int(getenv("NUMACTORS", default=5)), + int(getenv("NUMROLLOUTS", default=5)), max_lag=int(getenv("MAXLAG", default=0)), proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index bc1856368..f5c5881d2 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -13,22 +13,27 @@ max_lag: 0 # store the roll-out experiences for all agents whose experiences need to be stored. rollout_experience_distribution: false sync: - rollout_group: rollout - rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 3 - min_finished_workers: 2 - max_extra_recv_tries: 2 - extra_recv_timeout: 200 + rollout_type: distributed # simple, distributed + simple: + parallelism: 1 + eval_parallelism: 1 + distributed: + group: rollout + num_workers: 3 + num_eval_workers: 1 + min_finished_workers: 2 + max_extra_recv_tries: 2 + extra_recv_timeout: 200 async: group: async num_actors: 3 policy_manager: - type: distributed # simple, distributed + type: distributed # simple, distributed simple: parallel: false distributed: - learn_group: learn + group: learn num_hosts: 2 redis: - host: maro-redis + host: redis-server port: 6379 diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 097b6243e..37cdda3c0 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -26,8 +26,7 @@ post_evaluate = getattr(module, "post_evaluate", None) # roll-out experience distribution amongst workers -mode = getenv("MODE") -num_rollouts = int(getenv("NUMWORKERS")) if mode == "sync" else int(getenv("NUMACTORS")) +num_rollouts = int(getenv("NUMROLLOUTS")) exp_dist = int(getenv("EXPDIST", default=0)) if exp_dist: replay_agents = [[] for _ in range(num_rollouts)] diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py index fc71e1502..49b6d342e 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -5,9 +5,7 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning.synchronous import ( - Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager -) +from maro.rl.learning.synchronous import Learner, DistributedRolloutManager, SimpleRolloutManager workflow_dir = dirname(dirname((realpath(__file__)))) if workflow_dir not in sys.path: @@ -15,34 +13,27 @@ from agent_wrapper import get_agent_wrapper from policy_manager.policy_manager import get_policy_manager -from general import post_collect, post_evaluate, get_env_wrapper, log_dir +from general import post_collect, post_evaluate, get_env_wrapper, get_eval_env_wrapper, log_dir def get_rollout_manager(): - rollout_mode = getenv("ROLLOUTMODE", default="single-process") + rollout_type = getenv("ROLLOUTTYPE", default="simple") num_steps = int(getenv("NUMSTEPS", default=-1)) - if rollout_mode == "single-process": - return LocalRolloutManager( - get_env_wrapper(), - get_agent_wrapper(rollout_only=True), - num_steps=num_steps, - post_collect=post_collect, - post_evaluate=post_evaluate, - log_dir=log_dir - ) - - num_workers = int(getenv("NUMWORKERS", default=5)) - if rollout_mode == "multi-process": - return MultiProcessRolloutManager( - num_workers, + if rollout_type == "simple": + return SimpleRolloutManager( get_env_wrapper, get_agent_wrapper, + get_eval_env_wrapper=get_eval_env_wrapper, num_steps=num_steps, + parallelism=int(getenv("PARALLELISM", default="1")), + eval_parallelism=int(getenv("EVALPARALLELISM", default="1")), post_collect=post_collect, post_evaluate=post_evaluate, - log_dir=log_dir, + log_dir=log_dir ) + num_workers = int(getenv("NUMROLLOUTS", default=5)) + num_eval_workers = int(getenv("NUMEVALWORKERS", default=1)) max_lag = int(getenv("MAXLAG", default=0)) min_finished_workers = getenv("MINFINISH") if min_finished_workers is not None: @@ -56,10 +47,11 @@ def get_rollout_manager(): if extra_recv_timeout is not None: extra_recv_timeout = int(extra_recv_timeout) - if rollout_mode == "multi-node": - return MultiNodeRolloutManager( + if rollout_type == "distributed": + return DistributedRolloutManager( getenv("ROLLOUTGROUP", default="rollout"), num_workers, + num_eval_workers=num_eval_workers, num_steps=num_steps, max_lag=max_lag, min_finished_workers=min_finished_workers, @@ -73,9 +65,7 @@ def get_rollout_manager(): }, ) - raise ValueError( - f"Unsupported roll-out mode: {rollout_mode}. Supported modes: single-process, multi-process, multi-node" - ) + raise ValueError(f"Unsupported roll-out type: {rollout_type}. Supported: simple, distributed") if __name__ == "__main__": diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index 31ba7fefc..e341dfe74 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -5,7 +5,7 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning.synchronous import rollout_worker_node +from maro.rl.learning.synchronous import rollout_worker workflow_dir = dirname(dirname(realpath(__file__))) # template directory @@ -22,12 +22,12 @@ raise ValueError("Missing environment variable: WORKERID") worker_id = int(worker_id) - rollout_worker_node( + rollout_worker( getenv("ROLLOUTGROUP", default="rollout"), worker_id, - get_env_wrapper(replay_agent_ids=replay_agents[worker_id]), - get_agent_wrapper(), - eval_env_wrapper=get_eval_env_wrapper(), + get_env_wrapper, + get_agent_wrapper, + get_eval_env_wrapper=get_eval_env_wrapper, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index f0801f6af..c43d44660 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -178,7 +178,6 @@ def __init__( self._policy2host = {} self._host2policies = defaultdict(list) - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) # assign policies to hosts for i, name in enumerate(self._policy_names): @@ -197,7 +196,6 @@ def __init__( # cache the initial policy states self._state_cache, dones = {}, 0 for msg in self._proxy.receive(): - self._logger.info(f"received a msg of tag {msg.tag}") if msg.tag == MsgTag.INIT_POLICIES_DONE: for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): self._state_cache[policy_name] = policy_state @@ -218,6 +216,7 @@ def update(self, rollout_info: Dict[str, list]): if msg.tag == MsgTag.LEARN_DONE: for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): self._state_cache[policy_name] = policy_state + self._logger.info(f"Cached state for policy {policy_name}") dones += 1 if dones == len(msg_dict): break diff --git a/maro/rl/learning/synchronous/__init__.py b/maro/rl/learning/synchronous/__init__.py index e54f8cea7..c046dab4c 100644 --- a/maro/rl/learning/synchronous/__init__.py +++ b/maro/rl/learning/synchronous/__init__.py @@ -2,10 +2,11 @@ # Licensed under the MIT license. from .learner import Learner -from .rollout_manager import AbsRolloutManager, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager -from .rollout_worker import rollout_worker_node, rollout_worker_process +from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, SimpleRolloutManager, rollout_worker +from .rollout_worker import RolloutWorker __all__ = [ - "AbsRolloutManager", "Learner", "LocalRolloutManager", "MultiNodeRolloutManager", "MultiProcessRolloutManager", - "rollout_worker_node", "rollout_worker_process" + "Learner", + "AbsRolloutManager", "DistributedRolloutManager", "SimpleRolloutManager", "rollout_worker", + "RolloutWorker" ] diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/synchronous/rollout_manager.py index 6d1eb6256..3e48d1e67 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/synchronous/rollout_manager.py @@ -11,9 +11,19 @@ from maro.communication import Proxy, SessionType from maro.rl.utils import MsgKey, MsgTag from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper -from maro.utils import Logger +from maro.utils import Logger, set_seeds -from .rollout_worker import rollout_worker_process +from .rollout_worker import RolloutWorker + + +def get_rollout_finish_msg(ep, segment, step_range, exploration_params=None): + if exploration_params: + return ( + f"Roll-out finished (episode: {ep}, segment: {segment}, " + f"step range: {step_range}, exploration parameters: {exploration_params})" + ) + else: + return f"Roll-out finished (episode: {ep}, segment: {segment}, step range: {step_range})" class AbsRolloutManager(ABC): @@ -67,143 +77,22 @@ def reset(self): self.episode_complete = False -class LocalRolloutManager(AbsRolloutManager): +class SimpleRolloutManager(AbsRolloutManager): """Local roll-out controller. Args: - env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance to interact with a set of agents and collect - experiences for policy training / update. - agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - eval_env_wrapper (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will - be used as the evaluation environment. Defaults to None. - post_collect (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. - Defaults to None. - post_evaluate (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - env_wrapper: AbsEnvWrapper, - agent_wrapper: AgentWrapper, - num_steps: int = -1, - eval_env_wrapper: AbsEnvWrapper = None, - post_collect: Callable = None, - post_evaluate: Callable = None, - log_dir: str = getcwd() - ): - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - super().__init__(post_collect=post_collect, post_evaluate=post_evaluate) - - self.env = env_wrapper - self.eval_env = eval_env_wrapper if eval_env_wrapper else self.env - self.agent = agent_wrapper - - self._num_steps = num_steps if num_steps > 0 else float("inf") - - self._logger = Logger("LOCAL_ROLLOUT_MANAGER", dump_folder=log_dir) - - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): - """Collect simulation data, i.e., experiences for training. - - Args: - ep (int): Current episode index. - segment (int): Current segment index. - policy_state_dict (dict): Policy states to use for simulation. - version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. - - Returns: - Experiences for policy training. - """ - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") - - # start of new episode - if self.env.state is None: - self.env.reset() - self.env.collect() - self.env.start() # get initial state - self.agent.exploration_step() - - # set policy states - self.agent.set_policy_states(policy_state_dict) - # update exploration parameters - self.agent.explore() - - start_step_index = self.env.step_index + 1 - steps_to_go = self._num_steps - while self.env.state and steps_to_go > 0: - action = self.agent.choose_action(self.env.state) - self.env.step(action) - steps_to_go -= 1 - - self._logger.info( - f"Roll-out finished for ep {ep}, segment {segment}" - f"(steps {start_step_index} - {self.env.step_index})" - ) - - # update the exploration parameters if an episode is finished - if not self.env.state: - self.episode_complete = True - - if self._post_collect: - self._post_collect([self.env.tracker], ep, segment) - - return self.env.get_experiences() - - def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``. - - Args: - ep (int): Current training episode index. - policy_state_dict (dict): Policy states to use for simulation. - - Returns: - Environment summary. - """ - self._logger.info("Evaluating...") - self.agent.set_policy_states(policy_state_dict) - self.agent.exploit() - self.eval_env.reset() - self.eval_env.evaluate() - self.eval_env.start() # get initial state - while self.eval_env.state: - action = self.agent.choose_action(self.eval_env.state) - self.eval_env.step(action) - - if self._post_evaluate: - self._post_evaluate([self.eval_env.tracker], ep) - - return self.eval_env.tracker - - -class MultiProcessRolloutManager(AbsRolloutManager): - """Roll-out manager that spawns a set of roll-out worker processes for parallel data collection. - - Args: - num_workers (int): Number of remote roll-out workers. - create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an + get_env_wrapper (Callable): Function to be used by each spawned roll-out worker to create an environment wrapper for training data collection. The function should take no parameters and return an environment wrapper instance. - create_agent_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create a + get_agent_wrapper (Callable): Function to be used by each spawned roll-out worker to create a decision generator for interacting with the environment. The function should take no parameters and return a ``AgentWrapper`` instance. - create_env_wrapper_func (Callable): Function to be used by each spawned roll-out worker to create an + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + get_env_wrapper (Callable): Function to be used by each spawned roll-out worker to create an environment wrapper for evaluation. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the worker processes. Defaults to None. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - num_eval_workers (int): Number of workers for evaluation. Defaults to 1. post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. @@ -218,43 +107,75 @@ class MultiProcessRolloutManager(AbsRolloutManager): """ def __init__( self, - num_workers: int, - create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_agent_wrapper_func: Callable[[], AgentWrapper], - create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, + get_env_wrapper: Callable[[], AbsEnvWrapper], + get_agent_wrapper: Callable[[], AgentWrapper], num_steps: int = -1, - num_eval_workers: int = 1, + parallelism: int = 1, + get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None, + eval_parallelism: int = 1, post_collect: Callable = None, post_evaluate: Callable = None, log_dir: str = getcwd() ): + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1.") + + if parallelism < 1: + raise ValueError("'parallelism' must be equal to or greater than 1.") + + if eval_parallelism > parallelism: + raise ValueError("'num_eval_workers' can not be greater than 'parallelism'.") + super().__init__(post_collect=post_collect, post_evaluate=post_evaluate) - self._num_workers = num_workers - self._num_steps = num_steps - self._num_eval_workers = num_eval_workers + self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) + self._num_steps = num_steps if num_steps > 0 else float("inf") self._exploration_step = False - self._logger = Logger("MULTIPROCESS_ROLLOUT_MANAGER", dump_folder=log_dir) - - self._worker_processes = [] - self._manager_ends = [] - for index in range(self._num_workers): - manager_end, worker_end = Pipe() - self._manager_ends.append(manager_end) - worker = Process( - target=rollout_worker_process, - args=( - index, - worker_end, - create_env_wrapper_func, - create_agent_wrapper_func, - ), - kwargs={ - "create_eval_env_wrapper_func": create_eval_env_wrapper_func, - "log_dir": log_dir - } + self._parallelism = parallelism + self._eval_parallelism = eval_parallelism + if self._parallelism == 1: + self.worker = RolloutWorker( + get_env_wrapper, get_agent_wrapper, + get_eval_env_wrapper=get_eval_env_wrapper ) - self._worker_processes.append(worker) - worker.start() + else: + self._worker_processes = [] + self._manager_ends = [] + + def _rollout_worker(index, conn, get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=None): + set_seeds(index) + worker = RolloutWorker(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) + while True: + msg = conn.recv() + if msg["type"] == "sample": + ep, segment = msg["episode"], msg["segment"] + result = worker.sample( + policy_state_dict=msg["policy_state"], + num_steps=self._num_steps, + exploration_step=self._exploration_step + ) + logger.info(get_rollout_finish_msg( + ep, segment, result["step_range"], exploration_params=result["exploration_params"] + )) + result["worker_index"] = index + conn.send(result) + elif msg["type"] == "test": + tracker = worker.test(msg["policy_state"]) + logger.info("Evaluation...") + conn.send({"worker_id": index, "tracker": tracker}) + elif msg["type"] == "quit": + break + + for index in range(self._parallelism): + manager_end, worker_end = Pipe() + self._manager_ends.append(manager_end) + worker = Process( + target=_rollout_worker, + args=(index, worker_end, get_env_wrapper, get_agent_wrapper), + kwargs={"get_eval_env_wrapper": get_eval_env_wrapper} + ) + self._worker_processes.append(worker) + worker.start() def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): """Collect simulation data, i.e., experiences for training. @@ -268,33 +189,45 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): Returns: Experiences for policy training. """ - rollout_req = { - "type": "collect", - "episode": ep, - "segment": segment, - "num_steps": self._num_steps, - "policy": policy_state_dict, - "exploration_step": self._exploration_step - } - - for conn in self._manager_ends: - conn.send(rollout_req) - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") - if self._exploration_step: - self._exploration_step = False + info_by_policy, trackers = defaultdict(list), [] + if self._parallelism == 1: + result = self.worker.sample( + policy_state_dict=policy_state_dict, + num_steps=self._num_steps, + exploration_step=self._exploration_step + ) + self._logger.info(get_rollout_finish_msg( + ep, segment, result["step_range"], exploration_params=result["exploration_params"] + )) - info_by_policy = defaultdict(list) - trackers = [] - for conn in self._manager_ends: - result = conn.recv() - training_info = result["experiences"] - trackers.append(result["tracker"]) - for policy_name, info in training_info.items(): + for policy_name, info in result["rollout_info"].items(): info_by_policy[policy_name].append(info) - - self.episode_complete = result["episode_end"] + trackers.append(result["tracker"]) + self.episode_complete = result["end_of_episode"] + else: + rollout_req = { + "type": "sample", + "episode": ep, + "segment": segment, + "num_steps": self._num_steps, + "policy_state": policy_state_dict, + "exploration_step": self._exploration_step + } + + for conn in self._manager_ends: + conn.send(rollout_req) + + if self._exploration_step: + self._exploration_step = False + + for conn in self._manager_ends: + result = conn.recv() + for policy_name, info in result["rollout_info"].items(): + info_by_policy[policy_name].append(info) + trackers.append(result["tracker"]) + self.episode_complete = result["episode_end"] if self.episode_complete: self._exploration_step = True @@ -314,14 +247,19 @@ def evaluate(self, ep: int, policy_state_dict: dict): Returns: Environment summary. """ - eval_worker_conns = choices(self._manager_ends, k=self._num_eval_workers) - for conn in eval_worker_conns: - conn.send({"type": "evaluate", "episode": ep, "policy": policy_state_dict}) - trackers = [] - for conn in self._manager_ends: - result = conn.recv() - trackers.append(result["tracker"]) + if self._eval_parallelism == 1: + self._logger.info("Evaluating...") + tracker = self.worker.test(policy_state_dict) + trackers.append(tracker) + else: + eval_worker_conns = choices(self._manager_ends, k=self._eval_parallelism) + for conn in eval_worker_conns: + conn.send({"type": "test", "episode": ep, "policy_state": policy_state_dict}) + + for conn in self._manager_ends: + result = conn.recv() + trackers.append(result["tracker"]) if self._post_evaluate: self._post_evaluate(trackers, ep) @@ -330,11 +268,12 @@ def evaluate(self, ep: int, policy_state_dict: dict): def exit(self): """Tell the worker processes to exit.""" - for conn in self._manager_ends: - conn.send({"type": "quit"}) + if self._parallelism > 1: + for conn in self._manager_ends: + conn.send({"type": "quit"}) -class MultiNodeRolloutManager(AbsRolloutManager): +class DistributedRolloutManager(AbsRolloutManager): """Controller for a set of remote roll-out workers, possibly distributed on different computation nodes. Args: @@ -391,7 +330,7 @@ def __init__( peers = {"rollout_worker": num_workers} self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's - self._logger = Logger("MULTINODE_ROLLOUT_MANAGER", dump_folder=log_dir) + self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) self._num_steps = num_steps if min_finished_workers is None: @@ -432,17 +371,13 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): MsgKey.EXPLORATION_STEP: self._exploration_step } - self._proxy.iscatter(MsgTag.COLLECT, SessionType.TASK, [(worker_id, msg_body) for worker_id in self._workers]) + self._proxy.iscatter(MsgTag.SAMPLE, SessionType.TASK, [(worker_id, msg_body) for worker_id in self._workers]) self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") if self._exploration_step: self._exploration_step = False - # Receive roll-out results from remote workers - info_list_by_policy = defaultdict(list) - trackers = [] - num_finishes = 0 - + info_list_by_policy, trackers, num_finishes = defaultdict(list), [], 0 # Ensure the minimum number of worker results are received. for msg in self._proxy.receive(): info_by_policy, tracker = self._handle_worker_result(msg, ep, segment, version) @@ -478,9 +413,9 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): return info_list_by_policy def _handle_worker_result(self, msg, ep, segment, version): - if msg.tag != MsgTag.COLLECT_DONE: + if msg.tag != MsgTag.SAMPLE_DONE: self._logger.info( - f"Ignored a message of type {msg.tag} (expected message type {MsgTag.COLLECT_DONE})" + f"Ignored a message of type {msg.tag} (expected message type {MsgTag.SAMPLE_DONE})" ) return None, None @@ -512,22 +447,21 @@ def evaluate(self, ep: int, policy_state_dict: dict): msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY_STATE: policy_state_dict} workers = choices(self._workers, k=self._num_eval_workers) - self._proxy.iscatter(MsgTag.EVAL, SessionType.TASK, [(worker_id, msg_body) for worker_id in workers]) + self._proxy.iscatter(MsgTag.TEST, SessionType.TASK, [(worker_id, msg_body) for worker_id in workers]) self._logger.info(f"Sent evaluation requests to {workers}") # Receive roll-out results from remote workers num_finishes = 0 trackers = [] for msg in self._proxy.receive(): - if msg.tag != MsgTag.EVAL_DONE or msg.body[MsgKey.EPISODE] != ep: + if msg.tag != MsgTag.TEST_DONE or msg.body[MsgKey.EPISODE] != ep: self._logger.info( f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE]} " - f"(expected message type {MsgTag.EVAL_DONE} and episode index {ep})" + f"(expected message type {MsgTag.TEST_DONE} and episode index {ep})" ) continue trackers.append(msg.body[MsgKey.TRACKER]) - if msg.body[MsgKey.EPISODE] == ep: num_finishes += 1 if num_finishes == self._num_eval_workers: @@ -543,3 +477,76 @@ def exit(self): self._proxy.ibroadcast("rollout_worker", MsgTag.EXIT, SessionType.NOTIFICATION) self._proxy.close() self._logger.info("Exiting...") + + +def rollout_worker( + group: str, + worker_id: int, + get_env_wrapper: Callable[[], AbsEnvWrapper], + get_agent_wrapper: Callable[[], AgentWrapper], + get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + """Roll-out worker process that can be launched on separate computation nodes. + + Args: + group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager + that manages them. + worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". + This is used for bookkeeping by the parent manager. + env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. + agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. + eval_env_wrapper (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training + environment wrapper will be used for evaluation. Defaults to None. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + worker = RolloutWorker(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + proxy = Proxy( + group, "rollout_worker", {"rollout_manager": 1}, + component_name=f"ROLLOUT_WORKER.{int(worker_id)}", **proxy_kwargs + ) + logger = Logger(proxy.name, dump_folder=log_dir) + + """ + The event loop handles 3 types of messages from the roll-out manager: + 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps + and the collected experiences will be sent back to the roll-out manager; + 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire + duration of the evaluation environment. + 3) EXIT, upon which it will break out of the event loop and the process will terminate. + + """ + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.SAMPLE: + ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] + result = worker.sample( + policy_state_dict=msg.body[MsgKey.POLICY_STATE], + num_steps=msg.body[MsgKey.NUM_STEPS], + exploration_step=msg.body[MsgKey.EXPLORATION_STEP] + ) + logger.info(get_rollout_finish_msg( + ep, segment, result["step_range"], exploration_params=result["exploration_params"] + )) + return_info = { + MsgKey.EPISODE: ep, + MsgKey.SEGMENT: segment, + MsgKey.VERSION: msg.body[MsgKey.VERSION], + MsgKey.ROLLOUT_INFO: result["rollout_info"], + MsgKey.STEP_RANGE: result["step_range"], + MsgKey.TRACKER: result["tracker"], + MsgKey.END_OF_EPISODE: result["end_of_episode"] + } + proxy.reply(msg, tag=MsgTag.SAMPLE_DONE, body=return_info) + elif msg.tag == MsgTag.TEST: + tracker = worker.test(msg.body[MsgKey.POLICY_STATE]) + return_info = {MsgKey.TRACKER: tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} + logger.info("Testing complete") + proxy.reply(msg, tag=MsgTag.TEST_DONE, body=return_info) diff --git a/maro/rl/learning/synchronous/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py index 33c60d5c3..ad5841a0e 100644 --- a/maro/rl/learning/synchronous/rollout_worker.py +++ b/maro/rl/learning/synchronous/rollout_worker.py @@ -1,209 +1,60 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from multiprocessing.connection import Connection -from os import getcwd from typing import Callable -from maro.communication import Proxy -from maro.rl.utils import MsgKey, MsgTag from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper -from maro.utils import Logger, set_seeds -def rollout_worker_process( - index: int, - conn: Connection, - create_env_wrapper_func: Callable[[], AbsEnvWrapper], - create_agent_wrapper_func: Callable[[], AgentWrapper], - create_eval_env_wrapper_func: Callable[[], AbsEnvWrapper] = None, - log_dir: str = getcwd() -): - """Roll-out worker process that can be spawned by a ``MultiProcessRolloutManager``. +class RolloutWorker: + def __init__( + self, + get_env_wrapper: Callable[[], AbsEnvWrapper], + get_agent_wrapper: Callable[[], AgentWrapper], + get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None + ): + self.env = get_env_wrapper() + self.eval_env = get_env_wrapper() if get_eval_env_wrapper else self.env + self.agent = get_agent_wrapper() - Args: - index (int): Index for the worker process. This is used for bookkeeping by the parent manager process. - conn (Connection): Connection end for exchanging messages with the manager process. - create_env_wrapper_func (Callable): Function to create an environment wrapper for training data collection. - The function should take no parameters and return an environment wrapper instance. - create_agent_wrapper_func (Callable): Function to create a decision generator for interacting with - the environment. The function should take no parameters and return a ``AgentWrapper`` instance. - create_env_wrapper_func (Callable): Function to create an environment wrapper for evaluation. The function - should take no parameters and return an environment wrapper instance. If this is None, the training - environment wrapper will be used for evaluation. Defaults to None. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - set_seeds(index) - env_wrapper = create_env_wrapper_func() - eval_env_wrapper = env_wrapper if not create_eval_env_wrapper_func else create_eval_env_wrapper_func() - agent_wrapper = create_agent_wrapper_func() - logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) - - def collect(msg): - ep, segment = msg["episode"], msg["segment"] + def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): # set policy states - agent_wrapper.set_policy_states(msg["policy"]) - - # update exploration parameters - agent_wrapper.explore() - if msg["exploration_step"]: - agent_wrapper.exploration_step() - - if env_wrapper.state is None: - logger.info(f"Roll-out episode {ep}") - env_wrapper.reset() - env_wrapper.collect() - env_wrapper.start() # get initial state - - starting_step_index = env_wrapper.step_index + 1 - steps_to_go = float("inf") if msg["num_steps"] == -1 else msg["num_steps"] - while env_wrapper.state and steps_to_go > 0: - action = agent_wrapper.choose_action(env_wrapper.state) - env_wrapper.step(action) - steps_to_go -= 1 - - logger.info( - f"Roll-out finished (episode {ep}, segment {segment}, " - f"steps {starting_step_index} - {env_wrapper.step_index})" - ) - - return_info = { - "worker_index": index, - "episode_end": not env_wrapper.state, - "experiences": agent_wrapper.get_batch(env_wrapper), - "tracker": env_wrapper.tracker, - "num_steps": env_wrapper.step_index - starting_step_index + 1 - } - - conn.send(return_info) - - def evaluate(msg): - logger.info("Evaluating...") - agent_wrapper.set_policy_states(msg["policy"]) - agent_wrapper.exploit() - eval_env_wrapper.reset() - eval_env_wrapper.evaluate() - eval_env_wrapper.start() # get initial state - while eval_env_wrapper.state: - action = agent_wrapper.choose_action(eval_env_wrapper.state) - eval_env_wrapper.step(action) - - conn.send({"worker_id": index, "tracker": eval_env_wrapper.tracker}) - - while True: - msg = conn.recv() - if msg["type"] == "collect": - collect(msg) - elif msg["type"] == "evaluate": - evaluate(msg) - elif msg["type"] == "quit": - break - - -def rollout_worker_node( - group: str, - worker_id: int, - env_wrapper: AbsEnvWrapper, - agent_wrapper: AgentWrapper, - eval_env_wrapper: AbsEnvWrapper = None, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Roll-out worker process that can be launched on separate computation nodes. - - Args: - group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager - that manages them. - worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". - This is used for bookkeeping by the parent manager. - env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. - agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. - eval_env_wrapper (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training - environment wrapper will be used for evaluation. Defaults to None. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - eval_env_wrapper = env_wrapper if not eval_env_wrapper else eval_env_wrapper - - proxy = Proxy( - group, "rollout_worker", {"rollout_manager": 1}, - component_name=f"ROLLOUT_WORKER.{int(worker_id)}", **proxy_kwargs - ) - logger = Logger(proxy.name, dump_folder=log_dir) - - def collect(msg): - ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] - - # set policy states - agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) + if policy_state_dict: + self.agent.set_policy_states(policy_state_dict) # set exploration parameters - agent_wrapper.explore() - if msg.body[MsgKey.EXPLORATION_STEP]: - agent_wrapper.exploration_step() - - logger.info(f"Exploring with parameters: {agent_wrapper.exploration_params}") - if env_wrapper.state is None: - logger.info(f"Rollout episode {msg.body[MsgKey.EPISODE]}") - env_wrapper.reset() - env_wrapper.collect() - env_wrapper.start() # get initial state - - starting_step_index = env_wrapper.step_index + 1 - steps_to_go = float("inf") if msg.body[MsgKey.NUM_STEPS] == -1 else msg.body[MsgKey.NUM_STEPS] - while env_wrapper.state and steps_to_go > 0: - action = agent_wrapper.choose_action(env_wrapper.state) - env_wrapper.step(action) + self.agent.explore() + if exploration_step: + self.agent.exploration_step() + + if self.env.state is None: + self.env.reset() + self.env.replay = True + self.env.start() # get initial state + + starting_step_index = self.env.step_index + 1 + steps_to_go = float("inf") if num_steps == -1 else num_steps + while self.env.state and steps_to_go > 0: + action = self.agent.choose_action(self.env.state) + self.env.step(action) steps_to_go -= 1 - logger.info( - f"Roll-out finished (episode {ep}, segment {segment}, " - f"steps {starting_step_index} - {env_wrapper.step_index})" - ) - - return_info = { - MsgKey.EPISODE: ep, - MsgKey.SEGMENT: segment, - MsgKey.VERSION: msg.body[MsgKey.VERSION], - MsgKey.ROLLOUT_INFO: agent_wrapper.get_rollout_info(env_wrapper.get_trajectory()), - MsgKey.NUM_STEPS: env_wrapper.step_index - starting_step_index + 1, - MsgKey.TRACKER: env_wrapper.tracker, - MsgKey.END_OF_EPISODE: not env_wrapper.state + return { + "rollout_info": self.agent.get_rollout_info(self.env.get_trajectory()), + "step_range": (starting_step_index, self.env.step_index), + "tracker": self.env.tracker, + "end_of_episode": not self.env.state, + "exploration_params": self.agent.exploration_params } - proxy.reply(msg, tag=MsgTag.COLLECT_DONE, body=return_info) - - def evaluate(msg): - logger.info("Evaluating...") - agent_wrapper.set_policy_states(msg.body[MsgKey.POLICY_STATE]) - agent_wrapper.exploit() - eval_env_wrapper.reset() - eval_env_wrapper.evaluate() - eval_env_wrapper.start() # get initial state - while eval_env_wrapper.state: - action = agent_wrapper.choose_action(eval_env_wrapper.state) - eval_env_wrapper.step(action) - - return_info = {MsgKey.TRACKER: eval_env_wrapper.tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} - proxy.reply(msg, tag=MsgTag.EVAL_DONE, body=return_info) - - """ - The event loop handles 3 types of messages from the roll-out manager: - 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps - and the collected experiences will be sent back to the roll-out manager; - 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire - duration of the evaluation environment. - 3) EXIT, upon which it will break out of the event loop and the process will terminate. - - """ - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.COLLECT: - collect(msg) - elif msg.tag == MsgTag.EVAL: - evaluate(msg) + def test(self, policy_state_dict: dict): + self.agent.set_policy_states(policy_state_dict) + self.agent.exploit() + self.eval_env.reset() + self.eval_env.replay = False + self.eval_env.start() # get initial state + while self.eval_env.state: + action = self.agent.choose_action(self.eval_env.state) + self.eval_env.step(action) + + return self.eval_env.tracker diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 13e1e83fa..a3a9f9413 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -5,8 +5,10 @@ class MsgTag(Enum): - COLLECT = "rollout" - EVAL = "eval" + SAMPLE = "sample" + TEST = "test" + SAMPLE_DONE = "eval_done" + TEST_DONE = "collect_done" INIT_POLICIES = "init_policies" INIT_POLICIES_DONE = "init_policies_done" POLICY_STATE = "policy_state" @@ -17,8 +19,6 @@ class MsgTag(Enum): COMPUTE_GRAD = "compute_grad" COMPUTE_GRAD_DONE = "compute_grad_done" ABORT_ROLLOUT = "abort_rollout" - EVAL_DONE = "eval_done" - COLLECT_DONE = "collect_done" DONE = "done" EXIT = "exit" @@ -28,6 +28,7 @@ class MsgKey(Enum): AGENT_ID = "agent_id" EPISODE = "episode" SEGMENT = "segment" + NUM_STEPS = "num_steps" STEP = "step" POLICY_NAMES = "policy_names" ROLLOUT_INFO = "rollout_info" @@ -38,5 +39,5 @@ class MsgKey(Enum): POLICY_STATE = "policy_state" EXPLORATION_STEP = "exploration_step" VERSION = "version" - NUM_STEPS = "num_steps" + STEP_RANGE = "step_range" END_OF_EPISODE = "end_of_episode" diff --git a/maro/rl/wrappers/env_wrapper.py b/maro/rl/wrappers/env_wrapper.py index 5efa8910f..a86ceb9e2 100644 --- a/maro/rl/wrappers/env_wrapper.py +++ b/maro/rl/wrappers/env_wrapper.py @@ -46,7 +46,7 @@ def __init__( self._state = None # the latest extracted state is kept here self.tracker = {} # User-defined tracking information is placed here. - self._replay = True + self.replay = True @property def step_index(self): @@ -70,12 +70,6 @@ def state(self): def event(self): return self._event - def collect(self): - self._replay = True - - def evaluate(self): - self._replay = False - def start(self): """Generate the initial environmental state at the beginning of a simulation episode.""" self._step_index = 0 @@ -170,7 +164,7 @@ def step(self, action_by_agent: dict): # put things you want to track in the tracker attribute self._post_step(self.env, self.tracker, transition) - if self._replay: + if self.replay: for agent_id, agent_state in state.items(): if agent_id in self._replay_buffer: buf = self._replay_buffer[agent_id] From 8619408dfa63ceba517f0e2e348d825a41e52664 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 20 Aug 2021 10:01:27 +0000 Subject: [PATCH 413/482] updated api doc for policy manager --- examples/rl/cim/ac.py | 2 +- examples/rl/cim/algorithms/ac.py | 84 ------------------------------ examples/rl/cim/algorithms/dqn.py | 77 --------------------------- examples/rl/cim/env_wrapper.py | 11 ++-- examples/rl/cim/policy_index.py | 19 ++++--- maro/rl/learning/policy_host.py | 3 +- maro/rl/learning/policy_manager.py | 58 +++++++++++---------- 7 files changed, 49 insertions(+), 205 deletions(-) delete mode 100644 examples/rl/cim/algorithms/ac.py delete mode 100644 examples/rl/cim/algorithms/dqn.py diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py index 3b2f56cd9..605efcfb0 100644 --- a/examples/rl/cim/ac.py +++ b/examples/rl/cim/ac.py @@ -56,7 +56,7 @@ "entropy_coeff": 0.01, # "clip_ratio": 0.8 # for PPO "lam": 0.9, - "get_loss_on_rollout_finish": False + "get_loss_on_rollout_finish": True } diff --git a/examples/rl/cim/algorithms/ac.py b/examples/rl/cim/algorithms/ac.py deleted file mode 100644 index fb2102f1a..000000000 --- a/examples/rl/cim/algorithms/ac.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -import numpy as np -import torch - -from maro.rl.algorithms import ActorCritic -from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption - - -cim_path = os.path.dirname(os.path.dirname(__file__)) -sys.path.insert(0, cim_path) -from env_wrapper import STATE_DIM, env_config - -ac_net_config = { - "network": { - "actor": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64], - "output_dim": env_config["wrapper"]["num_actions"], - "activation": "tanh", - "softmax": True, - "batch_norm": False, - "head": True - }, - "critic": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64], - "output_dim": 1, - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "head": True - } - }, - "optimization": { - "actor": { - "optim_cls": "adam", - "optim_params": {"lr": 0.001} - }, - "critic": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.001} - } - } -} - -ac_config = { - "reward_discount": .0, - "critic_loss_cls": "smooth_l1", - "critic_loss_coeff": 0.1, - "entropy_coeff": 0.01, - # "clip_ratio": 0.8 # for PPO -} - - -def get_algorithm(): - class MyACNET(DiscreteACNet): - def forward(self, states, actor: bool = True, critic: bool = True): - states = torch.from_numpy(np.asarray(states)) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - - states = states.to(self.device) - return ( - self.component["actor"](states) if actor else None, - self.component["critic"](states) if critic else None - ) - - ac_net = MyACNET( - component={ - "actor": FullyConnectedBlock(**ac_net_config["network"]["actor"]), - "critic": FullyConnectedBlock(**ac_net_config["network"]["critic"]) - }, - optim_option={ - "actor": OptimOption(**ac_net_config["optimization"]["actor"]), - "critic": OptimOption(**ac_net_config["model"]["optimization"]["critic"]) - } - ) - - return ActorCritic(ac_net, ac_config["algorithm"]) diff --git a/examples/rl/cim/algorithms/dqn.py b/examples/rl/cim/algorithms/dqn.py deleted file mode 100644 index d1cd7f6e9..000000000 --- a/examples/rl/cim/algorithms/dqn.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -import numpy as np -import torch -import torch.nn as nn - -from maro.rl.algorithms import DQN -from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler -from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption - - -cim_path = os.path.dirname(os.path.realpath(__file__)) -if cim_path not in sys.path: - sys.path.insert(0, cim_path) -from env_wrapper import STATE_DIM, env_config - -q_net_config = { - "network": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64, 32], - "output_dim": env_config["wrapper"]["num_actions"], - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.05} - } -} - -dqn_config = { - "reward_discount": .0, - "update_target_every": 5, - "soft_update_coeff": 0.1, - "double": False -} - - -exploration_config = { - "last_ep": 10, - "initial_value": 0.4, - "final_value": 0.0, - "splits": [(5, 0.32)] -} - - -class QNet(DiscreteQNet): - def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): - super().__init__(component, optim_option=optim_option, device=device) - - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component(states) - - -def get_algorithm(): - qnet = QNet( - FullyConnectedBlock(**q_net_config["network"]), - optim_option=OptimOption(**q_net_config["optimization"]) - ) - exploration = EpsilonGreedyExploration() - exploration.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - **exploration_config - ) - return DQN(qnet, **dqn_config, exploration=exploration) diff --git a/examples/rl/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py index 6b413f816..37f7b2a40 100644 --- a/examples/rl/cim/env_wrapper.py +++ b/examples/rl/cim/env_wrapper.py @@ -118,7 +118,7 @@ def get_reward(self, actions, tick=None): "basic": { "scenario": "cim", "topology": "toy.4p_ssdd_l0.0", - "durations": 280 + "durations": 560 }, "wrapper": { "port_attributes": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], @@ -140,9 +140,6 @@ def get_reward(self, actions, tick=None): def get_env_wrapper(replay_agent_ids=None): return CIMEnvWrapper(Env(**env_config["basic"]), replay_agent_ids=replay_agent_ids, **env_config["wrapper"]) - - -tmp_env_wrapper = get_env_wrapper() -AGENT_IDS = tmp_env_wrapper.agent_idx_list -STATE_DIM = tmp_env_wrapper.state_dim -del tmp_env_wrapper + +# obtain state dimension from a temporary env_wrapper instance +STATE_DIM = get_env_wrapper().state_dim diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py index 3eb24c645..970529121 100644 --- a/examples/rl/cim/policy_index.py +++ b/examples/rl/cim/policy_index.py @@ -4,17 +4,22 @@ import os import sys - cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) from ac import get_ac_policy from dqn import get_dqn_policy -from env_wrapper import AGENT_IDS -update_trigger = {name: 128 for name in AGENT_IDS} -warmup = {name: 1 for name in AGENT_IDS} +rl_policy_func_index = { + "dqn": get_dqn_policy, + "ac.0": get_ac_policy, + "ac.1": get_ac_policy, + "ac.2": get_ac_policy +} -# use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {name: get_dqn_policy for name in AGENT_IDS} -agent2policy = {name: name for name in AGENT_IDS} +agent2policy = { + 0: "ac.0", + 1: "ac.1", + 2: "dqn", + 3: "ac.2" +} diff --git a/maro/rl/learning/policy_host.py b/maro/rl/learning/policy_host.py index 3a1042592..f36badfbe 100644 --- a/maro/rl/learning/policy_host.py +++ b/maro/rl/learning/policy_host.py @@ -55,9 +55,10 @@ def policy_host( t0 = time.time() for name, info_list in msg.body[MsgKey.ROLLOUT_INFO].items(): if isinstance(info_list[0], Trajectory): + logger.info("learning from multiple trajectories") policy_dict[name].learn_from_multi_trajectories(info_list) elif isinstance(info_list[0], LossInfo): - logger.info("apply loss info") + logger.info("updating with loss info") policy_dict[name].update_with_multi_loss_info(info_list) else: raise TypeError( diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index c43d44660..79947dbf5 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -9,20 +9,14 @@ from typing import Callable, Dict, List from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.policy import LossInfo +from maro.rl.policy import LossInfo, RLPolicy from maro.rl.types import Trajectory from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger class AbsPolicyManager(ABC): - """Manage all policies. - - The actual policy instances may reside here or be distributed on a set of processes or remote nodes. - - Args: - policies (List[RLPolicy]): A list of ``RLPolicy`` instances. - """ + """Facility that controls policy update and serves the latest policy states.""" def __init__(self): super().__init__() self.update_count = 0 @@ -33,30 +27,35 @@ def version(self): @abstractmethod def update(self, rollout_info: Dict[str, list]): - """Logic for handling incoming experiences is implemented here.""" + """Update policies using roll-out information. + + The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss + information computed directly by roll-out workers. + """ raise NotImplementedError @abstractmethod def get_state(self): + """Get the latest policy states.""" raise NotImplementedError class SimplePolicyManager(AbsPolicyManager): - """Policy manager that contains the actual policy instances. + """Policy manager that contains all policy instances. Args: - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. - warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the - minimum number of experiences in the experience memory required to trigger a call to ``learn`` for - each policy. Defaults to None, in which case all warm-up sizes will be set to 1. + create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a + function that takes policy name as the only parameter and return an ``RLPolicy`` instance. + parallel (bool): If True, the policies will be created in separate processes so that they can be updated in + parallel. Otherwise, they will be created by the manager itself, in which case they can only be updated + sequentially. Defaults to False. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. """ def __init__( self, - create_policy_func_dict: Dict[str, Callable], + create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], parallel: bool = False, log_dir: str = getcwd() ): @@ -107,10 +106,10 @@ def _policy_host(name, create_policy_func, conn): self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} def update(self, rollout_info: Dict[str, list]): - """Store experiences and update policies if possible. - - The incoming experiences are expected to be grouped by policy ID and will be stored in the corresponding - policy's experience manager. Policies whose update conditions have been met will then be updated. + """Update policies using roll-out information. + + The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss + information computed directly by roll-out workers. """ t0 = time.time() if self._parallel: @@ -133,29 +132,26 @@ def update(self, rollout_info: Dict[str, list]): self._logger.info(f"policy update time: {time.time() - t0}") def get_state(self): + """Get the latest policy states.""" if self._parallel: return self._state_cache else: return {name: policy.get_state() for name, policy in self._policy_dict.items()} def exit(self): - """Tell the policy hosts to exit.""" + """Tell the policy host processes to exit.""" if self._parallel: for conn in self._manager_end.values(): conn.send({"type": "quit"}) class DistributedPolicyManager(AbsPolicyManager): - """Policy manager that communicates with a set of remote nodes for parallel training. + """Policy manager that communicates with a set of remote nodes that house the policy instances. Args: - policy_dict (Dict[str, RLPolicy]): Policies managed by the manager. - group (str): Group name for the training cluster, which includes all hosts and a policy manager that - manages them. + policy_names (List[str]): Names of the registered policies. + group (str): Group name for the cluster consisting of the manager and all policy hosts. num_hosts (int): Number of hosts. The hosts will be identified by "POLICY_HOST.i", where 0 <= i < num_hosts. - warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the - minimum number of experiences in the experience memory required to trigger a call to ``learn`` for - each policy. Defaults to None, in which case all warm-up sizes will be set to 1. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -205,6 +201,11 @@ def __init__( break def update(self, rollout_info: Dict[str, list]): + """Update policies using roll-out information. + + The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss + information computed directly by roll-out workers. + """ msg_dict = defaultdict(lambda: defaultdict(dict)) for policy_name, info_list in rollout_info.items(): host_id_str = self._policy2host[policy_name] @@ -225,6 +226,7 @@ def update(self, rollout_info: Dict[str, list]): self._logger.info(f"Updated policies {list(rollout_info.keys())}") def get_state(self): + """Get the latest policy states.""" return self._state_cache def exit(self): From ca7b0d926b0f49b4445c88a7afee96ab5bf51460 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 23 Aug 2021 07:52:33 +0000 Subject: [PATCH 414/482] 1. simplified rl/learning code structure; 2. fixed bugs in rl example docker script --- .../rl/scripts/docker/docker_compose_yml.py | 17 +- examples/rl/workflows/asynchronous/actor.py | 21 +- .../workflows/asynchronous/policy_server.py | 6 +- examples/rl/workflows/config.yml | 2 +- .../workflows/policy_manager/policy_host.py | 3 +- .../policy_manager/policy_manager.py | 11 +- examples/rl/workflows/simple_learner.py | 23 +- examples/rl/workflows/synchronous/learner.py | 2 +- .../workflows/synchronous/rollout_worker.py | 21 +- maro/communication/proxy.py | 1 - maro/rl/learning/__init__.py | 15 +- maro/rl/learning/asynchronous/__init__.py | 7 - maro/rl/learning/asynchronous/actor.py | 145 ------------- .../rl/learning/asynchronous/policy_server.py | 69 ------ maro/rl/learning/common.py | 29 +++ maro/rl/learning/environment_sampler.py | 200 +++++++++++++++++ maro/rl/learning/learner.py | 203 ++++++++++++++++++ maro/rl/learning/policy_host.py | 72 ------- maro/rl/learning/policy_manager.py | 139 +++++++++++- .../{synchronous => }/rollout_manager.py | 134 +++--------- maro/rl/learning/simple_learner.py | 179 --------------- maro/rl/learning/synchronous/__init__.py | 12 -- maro/rl/learning/synchronous/learner.py | 116 ---------- .../rl/learning/synchronous/rollout_worker.py | 60 ------ maro/rl/policy/__init__.py | 4 +- maro/rl/policy/dqn.py | 14 +- maro/rl/utils/message_enums.py | 1 + 27 files changed, 653 insertions(+), 853 deletions(-) delete mode 100644 maro/rl/learning/asynchronous/__init__.py delete mode 100644 maro/rl/learning/asynchronous/actor.py delete mode 100644 maro/rl/learning/asynchronous/policy_server.py create mode 100644 maro/rl/learning/common.py create mode 100644 maro/rl/learning/environment_sampler.py create mode 100644 maro/rl/learning/learner.py delete mode 100644 maro/rl/learning/policy_host.py rename maro/rl/learning/{synchronous => }/rollout_manager.py (76%) delete mode 100644 maro/rl/learning/simple_learner.py delete mode 100644 maro/rl/learning/synchronous/__init__.py delete mode 100644 maro/rl/learning/synchronous/learner.py delete mode 100644 maro/rl/learning/synchronous/rollout_worker.py diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index d537a48cc..dd12cbbd6 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -19,6 +19,7 @@ root_dir = dirname(dirname(rl_example_dir)) workflow_dir = join(rl_example_dir, "workflows") maro_rl_dir = join(root_dir, "maro", "rl") + maro_comm_dir = join(root_dir, "maro", "communication") maro_sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") config_path = join(workflow_dir, "config.yml") dockerfile_path = join(root_dir, "docker_files", "dev.df") @@ -37,7 +38,8 @@ "volumes": [ f"{rl_example_dir}:/maro/rl_examples", f"{maro_rl_dir}:/maro/maro/rl", - f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" + f"{maro_comm_dir}:/maro/maro/communication", + f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" ] } @@ -47,6 +49,7 @@ f"JOB={config['job']}", f"SCENARIO={config['scenario']}", f"MODE={config['mode']}", + f"POLICYMANAGERTYPE={config['policy_manager']['type']}", f"EXPDIST={'1' if config['rollout_experience_distribution'] else '0'}" ] @@ -61,16 +64,15 @@ # host spec if config["policy_manager"]["type"] == "distributed": + common_env.append(f"LEARNGROUP={config['policy_manager']['distributed']['group']}") + common_env.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): str_id = f"policy_host.{host_id}" host_spec = deepcopy(common_spec) del host_spec["build"] host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/policy_host.py" host_spec["container_name"] = f"{namespace}.{str_id}" - host_spec["environment"] = [ - f"HOSTID={host_id}", - f"LEARNGROUP={config['policy_manager']['distributed']['group']}" - ] + common_env + host_spec["environment"] = [f"HOSTID={host_id}"] + common_env docker_compose_manifest["services"][str_id] = host_spec mode = config["mode"] @@ -93,10 +95,7 @@ f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}", - f"POLICYMANAGERTYPE={config['policy_manager']['type']}", - f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}", - f"LEARNGROUP={config['policy_manager']['distributed']['group']}", - f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}" + f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}" ] + common_env } } diff --git a/examples/rl/workflows/asynchronous/actor.py b/examples/rl/workflows/asynchronous/actor.py index 1c8a24298..5de8bf6d9 100644 --- a/examples/rl/workflows/asynchronous/actor.py +++ b/examples/rl/workflows/asynchronous/actor.py @@ -5,21 +5,21 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning.asynchronous import actor +from maro.rl.learning import EnvironmentSampler workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import get_env_wrapper, get_eval_env_wrapper, log_dir, replay_agents +from general import get_env_wrapper, log_dir if __name__ == "__main__": - actor_id = getenv("ACTORID") - if actor_id is None: + index = getenv("ACTORID") + if index is None: raise ValueError("Missing environment variable: ACTORID") - actor_id = int(actor_id) + index = int(index) num_episodes = getenv("NUMEPISODES") if num_episodes is None: @@ -27,15 +27,10 @@ num_episodes = int(num_episodes) num_steps = int(getenv("NUMSTEPS", default=-1)) - actor( - getenv("GROUP", default="ASYNC"), - actor_id, - get_env_wrapper(replay_agent_ids=replay_agents[actor_id]), - get_agent_wrapper(), - num_episodes, + env_sampler = EnvironmentSampler(get_env_wrapper, get_agent_wrapper) + env_sampler.actor( + getenv("GROUP", default="ASYNC"), index, num_episodes, num_steps=num_steps, - eval_env_wrapper=get_eval_env_wrapper(), - eval_schedule=int(getenv("EVALSCH")), proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 diff --git a/examples/rl/workflows/asynchronous/policy_server.py b/examples/rl/workflows/asynchronous/policy_server.py index 29ea4ce75..a33965024 100644 --- a/examples/rl/workflows/asynchronous/policy_server.py +++ b/examples/rl/workflows/asynchronous/policy_server.py @@ -5,8 +5,6 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning.asynchronous import policy_server - workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) @@ -16,9 +14,9 @@ if __name__ == "__main__": - policy_server( + policy_manager = get_policy_manager() + policy_manager.server( getenv("GROUP", default="ASYNC"), - get_policy_manager(), int(getenv("NUMROLLOUTS", default=5)), max_lag=int(getenv("MAXLAG", default=0)), proxy_kwargs={ diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index f5c5881d2..615dbe8ca 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -3,7 +3,7 @@ job: cim scenario: cim -mode: sync +mode: async num_episodes: 5 eval_schedule: 5 num_steps: -1 diff --git a/examples/rl/workflows/policy_manager/policy_host.py b/examples/rl/workflows/policy_manager/policy_host.py index 3ccef71f2..03a1f4c39 100644 --- a/examples/rl/workflows/policy_manager/policy_host.py +++ b/examples/rl/workflows/policy_manager/policy_host.py @@ -19,10 +19,11 @@ if host_id is None: raise ValueError("missing environment variable: HOSTID") + group = getenv("LEARNGROUP", default="learn") policy_host( rl_policy_func_index, int(host_id), - getenv("LEARNGROUP", default="learn"), + group, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager/policy_manager.py index bde564ea9..fbd9e7ae0 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager/policy_manager.py @@ -6,6 +6,7 @@ from os.path import dirname, realpath from maro.rl.learning import DistributedPolicyManager, SimplePolicyManager +from maro.utils import Logger workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: @@ -14,22 +15,24 @@ from general import log_dir, rl_policy_func_index def get_policy_manager(): + logger = Logger("policy manager creator") manager_type = getenv("POLICYMANAGERTYPE", default="simple") parallel = int(getenv("PARALLEL", default=0)) if manager_type == "simple": return SimplePolicyManager(rl_policy_func_index, parallel=parallel, log_dir=log_dir) + group = getenv("LEARNGROUP", default="learn") num_hosts = int(getenv("NUMHOSTS", default=5)) if manager_type == "distributed": - return DistributedPolicyManager( - list(rl_policy_func_index.keys()), - getenv("LEARNGROUP", default="learn"), - num_hosts, + policy_manager = DistributedPolicyManager( + list(rl_policy_func_index.keys()), group, num_hosts, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 }, log_dir=log_dir ) + logger.info("Distributed policy manager created") + return policy_manager raise ValueError(f"Unsupported policy manager type: {manager_type}. Supported modes: simple, distributed") diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index fbee34204..7a0c082a4 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -2,34 +2,31 @@ # Licensed under the MIT license. import sys -from os.path import dirname, realpath - -from maro.rl.learning import SimpleLearner +import yaml +from os.path import dirname, join, realpath +from maro.rl.learning import simple_learner workflow_dir = dirname((realpath(__file__))) if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) +with open(join(workflow_dir, "config.yml"), "r") as fp: + config = yaml.safe_load(fp) + from agent_wrapper import get_agent_wrapper -from general import ( - config, get_env_wrapper, get_eval_env_wrapper, log_dir, post_collect, post_evaluate, post_update, update_trigger, - warmup -) +from general import get_env_wrapper, get_eval_env_wrapper, log_dir, post_collect, post_evaluate if __name__ == "__main__": - SimpleLearner( + simple_learner( get_env_wrapper(), get_agent_wrapper(rollout_only=False), num_episodes=config["num_episodes"], num_steps=config["num_steps"], - eval_env=get_eval_env_wrapper(), + get_eval_env_wrapper=get_eval_env_wrapper, eval_schedule=config["eval_schedule"], - update_trigger=update_trigger, - warmup=warmup, post_collect=post_collect, post_evaluate=post_evaluate, - post_update=post_update, log_dir=log_dir - ).run() + ) diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py index 49b6d342e..5f0a0ca79 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/synchronous/learner.py @@ -5,7 +5,7 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning.synchronous import Learner, DistributedRolloutManager, SimpleRolloutManager +from maro.rl.learning import Learner, DistributedRolloutManager, SimpleRolloutManager workflow_dir = dirname(dirname((realpath(__file__)))) if workflow_dir not in sys.path: diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py index e341dfe74..8fc3d46f9 100644 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ b/examples/rl/workflows/synchronous/rollout_worker.py @@ -5,7 +5,7 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning.synchronous import rollout_worker +from maro.rl.learning import EnvironmentSampler workflow_dir = dirname(dirname(realpath(__file__))) # template directory @@ -13,21 +13,18 @@ sys.path.insert(0, workflow_dir) from agent_wrapper import get_agent_wrapper -from general import get_env_wrapper, get_eval_env_wrapper, log_dir, replay_agents +from general import get_env_wrapper, get_eval_env_wrapper, log_dir if __name__ == "__main__": - worker_id = getenv("WORKERID") - if worker_id is None: + index = getenv("WORKERID") + if index is None: raise ValueError("Missing environment variable: WORKERID") - worker_id = int(worker_id) - - rollout_worker( - getenv("ROLLOUTGROUP", default="rollout"), - worker_id, - get_env_wrapper, - get_agent_wrapper, - get_eval_env_wrapper=get_eval_env_wrapper, + index = int(index) + + env_sampler = EnvironmentSampler(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + env_sampler.worker( + getenv("ROLLOUTGROUP", default="rollout"), index, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index 4dcc499d5..f79524b09 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -236,7 +236,6 @@ def _register_redis(self): the value of table is the peer's socket address. """ self._redis_connection.hset(self._redis_hash_name, self._name, json.dumps(self._driver.address)) - # Handle interrupt signal for clearing Redis record. try: signal.signal(signal.SIGINT, self._signal_handler) diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index 6adbdaddc..c54a723ba 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -2,13 +2,16 @@ # Licensed under the MIT license. from .early_stopper import AbsEarlyStopper -from .policy_host import policy_host -from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager -from .simple_learner import SimpleLearner +from .environment_sampler import EnvironmentSampler +from .learner import Learner, simple_learner +from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager, policy_host +from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, SimpleRolloutManager + __all__ = [ "AbsEarlyStopper", - "policy_host", - "AbsPolicyManager", "DistributedPolicyManager", "SimplePolicyManager", - "SimpleLearner" + "EnvironmentSampler", + "Learner", "simple_learner", + "AbsPolicyManager", "DistributedPolicyManager", "SimplePolicyManager", "policy_host", + "AbsRolloutManager", "DistributedRolloutManager", "SimpleRolloutManager" ] diff --git a/maro/rl/learning/asynchronous/__init__.py b/maro/rl/learning/asynchronous/__init__.py deleted file mode 100644 index 6265ea7eb..000000000 --- a/maro/rl/learning/asynchronous/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .actor import actor -from .policy_server import policy_server - -__all__ = ["actor", "policy_server"] diff --git a/maro/rl/learning/asynchronous/actor.py b/maro/rl/learning/asynchronous/actor.py deleted file mode 100644 index cc7bbc3f4..000000000 --- a/maro/rl/learning/asynchronous/actor.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import List, Union - -from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.utils import MsgKey, MsgTag -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper -from maro.utils import Logger - - -def actor( - group: str, - actor_idx: int, - env_wrapper: AbsEnvWrapper, - agent_wrapper: AgentWrapper, - num_episodes: int, - num_steps: int = -1, - eval_env_wrapper: AbsEnvWrapper = None, - eval_schedule: Union[int, List[int]] = None, - log_env_summary: bool = True, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Controller for single-threaded learning workflows. - - Args: - group (str): Group name for the cluster that includes the server and all actors. - actor_idx (int): Integer actor index. The actor's ID in the cluster will be "ACTOR.{actor_idx}". - env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. - agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on how the implementation of the roll-out manager. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in - which case the roll-out will be executed until the end of the environment. - eval_env_wrapper_func (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training - environment wrapper will be used for evaluation. Defaults to None. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the training episodes given in the list. In any case, the policies will be evaluated - at the end of the last training episode. Defaults to None, in which case the policies will only be - evaluated after the last training episode. - log_env_summary (bool): If True, the ``summary`` property of the environment wrapper will be logged at the end - of each episode. Defaults to True. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - eval_env_wrapper = env_wrapper if not eval_env_wrapper else eval_env_wrapper - peers = {"policy_server": 1} - proxy = Proxy(group, "actor", peers, component_name=f"ACTOR.{actor_idx}", **proxy_kwargs) - policy_server_address = proxy.peers["policy_server"][0] - logger = Logger(proxy.name, dump_folder=log_dir) - policy_version = None - - # evaluation schedule - if eval_schedule is None: - eval_schedule = [] - elif isinstance(eval_schedule, int): - num_eval_schedule = num_episodes // eval_schedule - eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - else: - eval_schedule.sort() - - # always evaluate after the last episode - if not eval_schedule or num_episodes != eval_schedule[-1]: - eval_schedule.append(num_episodes) - - eval_point_index = 0 - - # get initial policy states from the policy manager - msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, policy_server_address) - reply = proxy.send(msg)[0] - policy_version = reply.body[MsgKey.VERSION] - agent_wrapper.set_policy_states(reply.body[MsgKey.POLICY_STATE]) - - # main loop - for ep in range(1, num_episodes + 1): - t0 = time.time() - num_training_experiences = 0 - agent_wrapper.explore() - env_wrapper.reset() - env_wrapper.start() # get initial state - segment = 0 - while env_wrapper.state: - segment += 1 - logger.info( - f"Collecting simulation data (episode {ep}, segment {segment}, policy version {policy_version})" - ) - start_step_index = env_wrapper.step_index + 1 - steps_to_go = num_steps - while env_wrapper.state and steps_to_go: - env_wrapper.step(agent_wrapper.choose_action(env_wrapper.state)) - steps_to_go -= 1 - - logger.info( - f"Roll-out finished (episode {ep}, segment {segment}, " - f"steps {start_step_index} - {env_wrapper.step_index})" - ) - - exp_by_policy = agent_wrapper.get_batch(env_wrapper) - num_training_experiences += sum(exp.size for exp in exp_by_policy.values()) - reply = proxy.send( - SessionMessage( - MsgTag.COLLECT_DONE, proxy.name, policy_server_address, - body={MsgKey.ROLLOUT_INFO: exp_by_policy, MsgKey.VERSION: policy_version} - ) - )[0] - policy_version = reply.body[MsgKey.VERSION] - agent_wrapper.set_policy_states(reply.body[MsgKey.POLICY_STATE]) - - # update the exploration parameters - agent_wrapper.exploration_step() - - # performance details - if log_env_summary: - logger.info(f"ep {ep}: {env_wrapper.summary}") - - logger.info( - f"ep {ep} summary - " - f"running time: {time.time() - t0} " - f"env steps: {env_wrapper.step_index} " - f"experiences collected: {num_training_experiences}" - ) - if ep == eval_schedule[eval_point_index]: - # evaluation - eval_point_index += 1 - logger.info("Evaluating...") - agent_wrapper.exploit() - eval_env_wrapper.reset() - eval_env_wrapper.start() # get initial state - while eval_env_wrapper.state: - action = agent_wrapper.choose_action(eval_env_wrapper.state) - eval_env_wrapper.step(action) - - # tell the policy server I'm all done. - proxy.isend(SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION)) - proxy.close() diff --git a/maro/rl/learning/asynchronous/policy_server.py b/maro/rl/learning/asynchronous/policy_server.py deleted file mode 100644 index 491c7b1bb..000000000 --- a/maro/rl/learning/asynchronous/policy_server.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from os import getcwd - -from maro.communication import Proxy -from maro.rl.utils import MsgKey, MsgTag -from maro.utils import Logger - -from ..policy_manager import AbsPolicyManager - - -def policy_server( - group: str, - policy_manager: AbsPolicyManager, - num_actors: int, - max_lag: int = 0, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Policy server process. - - The process serves the latest policy states to a set of remote actors and receives simulated experiences from them. - - Args: - group (str): Group name for the cluster that includes the server and all actors. - policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that hosts all policies and updates - them using experiences collected by the actors. - num_actors (int): Number of remote actors to collect simulation experiences. - max_lag (int): Maximum policy version lag allowed for experiences collected from remote actors. Experiences - collected using policy versions older than (current_version - max_lag) will be discarded. - Defaults to 0, in which case only experiences collected using the latest policy version will be returned. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - peers = {"actor": num_actors} - name = "POLICY_SERVER" - proxy = Proxy(group, "policy_server", peers, component_name=name, **proxy_kwargs) - logger = Logger(name, dump_folder=log_dir) - - num_active_actors = num_actors - for msg in proxy.receive(): - if msg.tag == MsgTag.GET_INITIAL_POLICY_STATE: - proxy.reply( - msg, tag=MsgTag.POLICY_STATE, - body={MsgKey.POLICY_STATE: policy_manager.get_state(), MsgKey.VERSION: policy_manager.version} - ) - elif msg.tag == MsgTag.COLLECT_DONE: - if policy_manager.version - msg.body[MsgKey.VERSION] > max_lag: - logger.info( - f"Ignored a message because it contains experiences generated using a stale policy version. " - f"Expected experiences generated using policy versions no earlier than " - f"{policy_manager.version - max_lag}, got {msg.body[MsgKey.VERSION]}" - ) - else: - policy_manager.update(msg.body[MsgKey.ROLLOUT_INFO]) - proxy.reply( - msg, tag=MsgTag.POLICY_STATE, - body={ - MsgKey.POLICY_STATE: policy_manager.get_state(cur_version=msg.body[MsgKey.VERSION]), - MsgKey.VERSION: policy_manager.version - } - ) - elif msg.tag == MsgTag.DONE: - num_active_actors -= 1 - if num_active_actors == 0: - proxy.close() - return diff --git a/maro/rl/learning/common.py b/maro/rl/learning/common.py new file mode 100644 index 000000000..c3cb1eae3 --- /dev/null +++ b/maro/rl/learning/common.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List, Union + +def get_rollout_finish_msg(ep, step_range, exploration_params=None): + if exploration_params: + return ( + f"Roll-out finished (episode {ep}, " + f"step range: {step_range}, exploration parameters: {exploration_params})" + ) + else: + return f"Roll-out finished (episode: {ep}, step range: {step_range})" + + +def get_eval_schedule(sch: Union[int, List[int]], num_episodes: int, final: bool = True): + if sch is None: + schedule = [] + elif isinstance(sch, int): + num_eval_schedule = num_episodes // sch + schedule = [sch * i for i in range(1, num_eval_schedule + 1)] + else: + schedule = sorted(sch) + + # always evaluate after the last episode + if final and (not schedule or num_episodes != schedule[-1]): + schedule.append(num_episodes) + + return schedule diff --git a/maro/rl/learning/environment_sampler.py b/maro/rl/learning/environment_sampler.py new file mode 100644 index 000000000..de9ce7b4a --- /dev/null +++ b/maro/rl/learning/environment_sampler.py @@ -0,0 +1,200 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from os import getcwd +from typing import Callable + +from maro.communication import Proxy, SessionMessage, SessionType +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper +from maro.rl.utils import MsgKey, MsgTag +from maro.utils import Logger + +from .common import get_rollout_finish_msg + + +class EnvironmentSampler: + """Simulation data collector and policy evaluator. + + Args: + get_env_wrapper (Callable): Function to create an environment wrapper for collecting training data. The function + should take no parameters and return an environment wrapper instance. + get_agent_wrapper (Callable): Function to create an agent wrapper that interacts with the environment wrapper. + The function should take no parameters and return a ``AgentWrapper`` instance. + get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should + take no parameters and return an environment wrapper instance. If this is None, the training environment + wrapper will be used for evaluation in the worker processes. Defaults to None. + """ + def __init__( + self, + get_env_wrapper: Callable[[], AbsEnvWrapper], + get_agent_wrapper: Callable[[], AgentWrapper], + get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None + ): + self.env = get_env_wrapper() + self.eval_env = get_env_wrapper() if get_eval_env_wrapper else self.env + self.agent = get_agent_wrapper() + + def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): + # set policy states + if policy_state_dict: + self.agent.set_policy_states(policy_state_dict) + + # set exploration parameters + self.agent.explore() + if exploration_step: + self.agent.exploration_step() + + if self.env.state is None: + self.env.reset() + self.env.replay = True + self.env.start() # get initial state + + starting_step_index = self.env.step_index + 1 + steps_to_go = float("inf") if num_steps == -1 else num_steps + while self.env.state and steps_to_go > 0: + action = self.agent.choose_action(self.env.state) + self.env.step(action) + steps_to_go -= 1 + + return { + "rollout_info": self.agent.get_rollout_info(self.env.get_trajectory()), + "step_range": (starting_step_index, self.env.step_index), + "tracker": self.env.tracker, + "end_of_episode": not self.env.state, + "exploration_params": self.agent.exploration_params + } + + def test(self, policy_state_dict: dict = None): + if policy_state_dict: + self.agent.set_policy_states(policy_state_dict) + self.agent.exploit() + self.eval_env.reset() + self.eval_env.replay = False + self.eval_env.start() # get initial state + while self.eval_env.state: + action = self.agent.choose_action(self.eval_env.state) + self.eval_env.step(action) + + return self.eval_env.tracker + + def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = getcwd()): + """Roll-out worker process that can be launched on separate computation nodes. + + Args: + group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager + that manages them. + worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". + This is used for bookkeeping by the parent manager. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + proxy = Proxy( + group, "rollout_worker", {"rollout_manager": 1}, component_name=f"ROLLOUT_WORKER.{index}", **proxy_kwargs + ) + logger = Logger(proxy.name, dump_folder=log_dir) + + """ + The event loop handles 3 types of messages from the roll-out manager: + 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps + and the collected experiences will be sent back to the roll-out manager; + 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire + duration of the evaluation environment. + 3) EXIT, upon which it will break out of the event loop and the process will terminate. + + """ + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.SAMPLE: + ep = msg.body[MsgKey.EPISODE] + result = self.sample( + policy_state_dict=msg.body[MsgKey.POLICY_STATE], + num_steps=msg.body[MsgKey.NUM_STEPS], + exploration_step=msg.body[MsgKey.EXPLORATION_STEP] + ) + logger.info( + get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) + ) + return_info = { + MsgKey.EPISODE: ep, + MsgKey.SEGMENT: msg.body[MsgKey.SEGMENT], + MsgKey.VERSION: msg.body[MsgKey.VERSION], + MsgKey.ROLLOUT_INFO: result["rollout_info"], + MsgKey.STEP_RANGE: result["step_range"], + MsgKey.TRACKER: result["tracker"], + MsgKey.END_OF_EPISODE: result["end_of_episode"] + } + proxy.reply(msg, tag=MsgTag.SAMPLE_DONE, body=return_info) + elif msg.tag == MsgTag.TEST: + tracker = self.test(msg.body[MsgKey.POLICY_STATE]) + return_info = {MsgKey.TRACKER: tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} + logger.info("Testing complete") + proxy.reply(msg, tag=MsgTag.TEST_DONE, body=return_info) + + def actor( + self, + group: str, + index: int, + num_episodes: int, + num_steps: int = -1, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() + ): + """Controller for single-threaded learning workflows. + + Args: + group (str): Group name for the cluster that includes the server and all actors. + index (int): Integer actor index. The actor's ID in the cluster will be "ACTOR.{actor_idx}". + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on how the implementation of the roll-out manager. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in + which case the roll-out will be executed until the end of the environment. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + peers = {"policy_server": 1} + proxy = Proxy(group, "actor", peers, component_name=f"ACTOR.{index}", **proxy_kwargs) + policy_server_address = proxy.peers["policy_server"][0] + logger = Logger(proxy.name, dump_folder=log_dir) + + # get initial policy states from the policy manager + msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, policy_server_address) + reply = proxy.send(msg)[0] + policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] + + # main loop + for ep in range(1, num_episodes + 1): + exploration_step = True + while True: + result = self.sample( + policy_state_dict=policy_state_dict, num_steps=num_steps, exploration_step=exploration_step + ) + logger.info( + get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) + ) + # Send roll-out info to policy server and + reply = proxy.send( + SessionMessage( + MsgTag.SAMPLE_DONE, proxy.name, policy_server_address, + body={MsgKey.ROLLOUT_INFO: result["rollout_info"], MsgKey.VERSION: policy_version} + ) + )[0] + policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] + if result["end_of_episode"]: + break + + exploration_step = False + + # tell the policy server I'm all done. + proxy.isend(SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION)) + proxy.close() diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py new file mode 100644 index 000000000..d0f9138cd --- /dev/null +++ b/maro/rl/learning/learner.py @@ -0,0 +1,203 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from os import getcwd +from typing import Callable, List, Union + +from maro.rl.policy import LossInfo +from maro.rl.types import Trajectory +from maro.rl.wrappers import AgentWrapper, AbsEnvWrapper +from maro.utils import Logger + +from .common import get_eval_schedule, get_rollout_finish_msg +from .early_stopper import AbsEarlyStopper +from .environment_sampler import EnvironmentSampler +from .policy_manager import AbsPolicyManager +from .rollout_manager import AbsRolloutManager + + +def simple_learner( + get_env_wrapper: Callable[[], AbsEnvWrapper], + get_agent_wrapper: Callable[[], AgentWrapper], + num_episodes: int, + num_steps: int = -1, + get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None, + eval_schedule: Union[int, List[int]] = None, + eval_after_last_episode: bool = True, + early_stopper: AbsEarlyStopper = None, + post_collect: Callable = None, + post_evaluate: Callable = None, + log_dir: str = getcwd() +): + """Single-threaded learning workflow. + + Args: + get_env_wrapper (Callable): Function to create an environment wrapper for collecting training data. The function + should take no parameters and return an environment wrapper instance. + get_agent_wrapper (Callable): Function to create an agent wrapper that interacts with the environment wrapper. + The function should take no parameters and return a ``AgentWrapper`` instance. + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on how the implementation of the roll-out manager. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should + take no parameters and return an environment wrapper instance. If this is None, the training environment + wrapper will be used for evaluation in the worker processes. Defaults to None. + eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will + will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated + at the end of the episodes given in the list. Defaults to None, in which case no evaluation is performed + unless ``eval_after_last_episode`` is set to True. + eval_after_last_episode (bool): If True, the policy will be evaluated after the last episode of learning is + finished. Defaults to True. + early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the + environment metrics are met following an evaluation episode. Default to None. + post_collect (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + post_evaluate (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should + be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) + env_sampler = EnvironmentSampler(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + + # evaluation schedule + eval_schedule = get_eval_schedule(eval_schedule, num_episodes, final=eval_after_last_episode) + logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") + eval_point_index = 0 + + def collect_and_update(ep): + """Collect simulation data for training.""" + segment, exploration_step = 1, True + while True: + result = env_sampler.sample(num_steps=num_steps, exploration_step=exploration_step) + logger.info( + get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) + ) + for policy_name, info_list in result["rollout_info"].items(): + if isinstance(info_list[0], Trajectory): + env_sampler.agent.policy_dict[policy_name].learn_from_multi_trajectories(info_list) + elif isinstance(info_list[0], LossInfo): + env_sampler.agent.policy_dict[policy_name].update_with_multi_loss_info(info_list) + + if post_collect: + post_collect([result["tracker"]], ep, segment) + + if result["end_of_episode"]: + break + + segment += 1 + exploration_step = False + + for ep in range(1, num_episodes + 1): + collect_and_update(ep) + if ep == eval_schedule[eval_point_index]: + eval_point_index += 1 + tracker = env_sampler.test() + if post_evaluate: + post_evaluate([tracker]) + # early stopping check + if early_stopper: + early_stopper.push(env_sampler.eval_env.summary) + if early_stopper.stop(): + return + + +class Learner: + """Main controller for learning. + + This should be used in multi-process or distributed settings where either the policy manager or the roll-out + manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such + as duplicate experience storage. Use ``SimpleLearner`` instead. + + Args: + policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. + rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data + collection. + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on the implementation of the roll-out manager. + eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will + will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated + at the end of the episodes given in the list. Defaults to None, in which case no evaluation is performed + unless ``eval_after_last_episode`` is set to True. + eval_after_last_episode (bool): If True, the policy will be evaluated after the last episode of learning is + finished. Defaults to False. + early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the + environment metric are met following an evaluation episode. Default to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time + and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + def __init__( + self, + policy_manager: AbsPolicyManager, + rollout_manager: AbsRolloutManager, + num_episodes: int, + eval_schedule: Union[int, List[int]] = None, + eval_after_last_episode: bool = False, + early_stopper: AbsEarlyStopper = None, + log_dir: str = getcwd() + ): + self._logger = Logger("LEARNER", dump_folder=log_dir) + self.policy_manager = policy_manager + self.rollout_manager = rollout_manager + + self.num_episodes = num_episodes + + # evaluation schedule + self._eval_schedule = get_eval_schedule(eval_schedule, num_episodes, final=eval_after_last_episode) + self._logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") + self._eval_point_index = 0 + + self.early_stopper = early_stopper + + def run(self): + """Entry point for executing a learning workflow.""" + for ep in range(1, self.num_episodes + 1): + self._collect_and_update(ep) + if ep == self._eval_schedule[self._eval_point_index]: + self._eval_point_index += 1 + env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) + # early stopping check + if self.early_stopper: + for env_metric in env_metric_dict.values(): + self.early_stopper.push(env_metric) + if self.early_stopper.stop(): + return + + if hasattr(self.rollout_manager, "exit"): + self.rollout_manager.exit() + + if hasattr(self.policy_manager, "exit"): + self.policy_manager.exit() + + def _collect_and_update(self, ep: int): + collect_time = policy_update_time = 0 + self.rollout_manager.reset() + segment = 1 + while not self.rollout_manager.episode_complete: + # experience collection + policy_state_dict, policy_version = self.policy_manager.get_state(), self.policy_manager.get_version() + tc0 = time.time() + rollout_info_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) + collect_time += time.time() - tc0 + tu0 = time.time() + self.policy_manager.update(rollout_info_by_policy) + policy_update_time += time.time() - tu0 + segment += 1 + + # performance details + self._logger.info( + f"ep {ep} summary - " + f"collect time: {collect_time} " + f"policy update time: {policy_update_time}" + ) diff --git a/maro/rl/learning/policy_host.py b/maro/rl/learning/policy_host.py deleted file mode 100644 index f36badfbe..000000000 --- a/maro/rl/learning/policy_host.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import Callable, Dict - -from maro.communication import Proxy -from maro.rl.policy import LossInfo, RLPolicy -from maro.rl.types import Trajectory -from maro.rl.utils import MsgKey, MsgTag -from maro.utils import Logger - - -def policy_host( - create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - host_idx: int, - group: str, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Policy host process that can be launched on separate computation nodes. - - Args: - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. - host_idx (int): Integer host index. The host's ID in the cluster will be "POLICY_HOST.{host_idx}". - group (str): Group name for the training cluster, which includes all policy hosts and a policy manager that - manages them. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - policy_dict = {} - proxy = Proxy(group, "policy_host", {"policy_manager": 1}, component_name=f"POLICY_HOST.{host_idx}", **proxy_kwargs) - logger = Logger(proxy.name, dump_folder=log_dir) - - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.INIT_POLICIES: - for name in msg.body[MsgKey.POLICY_NAMES]: - policy_dict[name] = create_policy_func_dict[name](name) - - logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_NAMES]}") - proxy.reply( - msg, - tag=MsgTag.INIT_POLICIES_DONE, - body={MsgKey.POLICY_STATE: {name: policy.get_state() for name, policy in policy_dict.items()}} - ) - elif msg.tag == MsgTag.LEARN: - t0 = time.time() - for name, info_list in msg.body[MsgKey.ROLLOUT_INFO].items(): - if isinstance(info_list[0], Trajectory): - logger.info("learning from multiple trajectories") - policy_dict[name].learn_from_multi_trajectories(info_list) - elif isinstance(info_list[0], LossInfo): - logger.info("updating with loss info") - policy_dict[name].update_with_multi_loss_info(info_list) - else: - raise TypeError( - f"Roll-out information must be of type 'Trajectory' or 'LossInfo', " - f"got {type(info_list[0])}" - ) - msg_body = { - MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.ROLLOUT_INFO]} - } - logger.debug(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, tag=MsgTag.LEARN_DONE, body=msg_body) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 79947dbf5..bda746b75 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -19,11 +19,6 @@ class AbsPolicyManager(ABC): """Facility that controls policy update and serves the latest policy states.""" def __init__(self): super().__init__() - self.update_count = 0 - - @property - def version(self): - return self.update_count @abstractmethod def update(self, rollout_info: Dict[str, list]): @@ -39,6 +34,57 @@ def get_state(self): """Get the latest policy states.""" raise NotImplementedError + @abstractmethod + def get_version(self): + """Get the collective policy version.""" + raise NotImplementedError + + def server(self, group: str, num_actors: int, max_lag: int = 0, proxy_kwargs: dict = {}, log_dir: str = getcwd()): + """Run a server process. + + The process serves the latest policy states to a set of remote actors and receives simulated experiences from them. + + Args: + group (str): Group name for the cluster that includes the server and all actors. + num_actors (int): Number of remote actors to collect simulation experiences. + max_lag (int): Maximum policy version lag allowed for experiences collected from remote actors. Experiences + collected using policy versions older than (current_version - max_lag) will be discarded. + Defaults to 0, in which case only experiences collected using the latest policy version will be returned. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + peers = {"actor": num_actors} + name = "POLICY_SERVER" + proxy = Proxy(group, "policy_server", peers, component_name=name, **proxy_kwargs) + logger = Logger(name, dump_folder=log_dir) + + num_active_actors = num_actors + for msg in proxy.receive(): + if msg.tag == MsgTag.GET_INITIAL_POLICY_STATE: + proxy.reply( + msg, tag=MsgTag.POLICY_STATE, + body={MsgKey.POLICY_STATE: self.get_state(), MsgKey.VERSION: self.get_version()} + ) + elif msg.tag == MsgTag.SAMPLE_DONE: + if self.get_version() - msg.body[MsgKey.VERSION] > max_lag: + logger.info( + f"Ignored a message because it contains experiences generated using a stale policy version. " + f"Expected experiences generated using policy versions no earlier than " + f"{self.get_version() - max_lag}, got {msg.body[MsgKey.VERSION]}" + ) + else: + self.update(msg.body[MsgKey.ROLLOUT_INFO]) + proxy.reply( + msg, tag=MsgTag.POLICY_STATE, + body={MsgKey.POLICY_STATE: self.get_state(), MsgKey.VERSION: self.get_version()} + ) + elif msg.tag == MsgTag.DONE: + num_active_actors -= 1 + if num_active_actors == 0: + proxy.close() + return + class SimplePolicyManager(AbsPolicyManager): """Policy manager that contains all policy instances. @@ -102,9 +148,11 @@ def _policy_host(name, create_policy_func, conn): self._state_cache[policy_name] = msg["policy_state"] self._logger.info(f"Initial state for policy {policy_name} cached") else: - self._logger.info("local mode") + self._logger.info("Creating policy instances locally") self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} + self._version = 0 + def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. @@ -122,13 +170,15 @@ def update(self, rollout_info: Dict[str, list]): self._logger.info(f"Cached state for policy {policy_name}") else: for policy_name, info_list in rollout_info.items(): + if not isinstance(info_list, list): + info_list = [info_list] if isinstance(info_list[0], Trajectory): self._policy_dict[policy_name].learn_from_multi_trajectories(info_list) elif isinstance(info_list[0], LossInfo): self._policy_dict[policy_name].update_with_multi_loss_info(info_list) self._logger.info(f"Updated policies {list(rollout_info.keys())}") - self.update_count += 1 + self._version += 1 self._logger.info(f"policy update time: {time.time() - t0}") def get_state(self): @@ -138,6 +188,10 @@ def get_state(self): else: return {name: policy.get_state() for name, policy in self._policy_dict.items()} + def get_version(self): + """Get the collective policy version.""" + return self._version + def exit(self): """Tell the policy host processes to exit.""" if self._parallel: @@ -200,14 +254,18 @@ def __init__( if dones == num_hosts: break + self._version = 0 + def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. - + The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss information computed directly by roll-out workers. """ msg_dict = defaultdict(lambda: defaultdict(dict)) for policy_name, info_list in rollout_info.items(): + if not isinstance(info_list, list): + info_list = [info_list] host_id_str = self._policy2host[policy_name] msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_name] = info_list @@ -222,15 +280,78 @@ def update(self, rollout_info: Dict[str, list]): if dones == len(msg_dict): break - self.update_count += 1 + self._version += 1 self._logger.info(f"Updated policies {list(rollout_info.keys())}") def get_state(self): """Get the latest policy states.""" return self._state_cache + def get_version(self): + """Get the collective policy version.""" + return self._version + def exit(self): """Tell the remote policy hosts to exit.""" self._proxy.ibroadcast("policy_host", MsgTag.EXIT, SessionType.NOTIFICATION) self._proxy.close() self._logger.info("Exiting...") + + +def policy_host( + create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], + host_idx: int, + group: str, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() +): + """Policy host process that can be launched on separate computation nodes. + + Args: + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. + host_idx (int): Integer host index. The host's ID in the cluster will be "POLICY_HOST.{host_idx}". + group (str): Group name for the training cluster, which includes all policy hosts and a policy manager that + manages them. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + policy_dict = {} + proxy = Proxy(group, "policy_host", {"policy_manager": 1}, component_name=f"POLICY_HOST.{host_idx}", **proxy_kwargs) + logger = Logger(proxy.name, dump_folder=log_dir) + + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.INIT_POLICIES: + for name in msg.body[MsgKey.POLICY_NAMES]: + policy_dict[name] = create_policy_func_dict[name](name) + + logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_NAMES]}") + proxy.reply( + msg, + tag=MsgTag.INIT_POLICIES_DONE, + body={MsgKey.POLICY_STATE: {name: policy.get_state() for name, policy in policy_dict.items()}} + ) + elif msg.tag == MsgTag.LEARN: + t0 = time.time() + for name, info_list in msg.body[MsgKey.ROLLOUT_INFO].items(): + if isinstance(info_list[0], Trajectory): + logger.info("learning from multiple trajectories") + policy_dict[name].learn_from_multi_trajectories(info_list) + elif isinstance(info_list[0], LossInfo): + logger.info("updating with loss info") + policy_dict[name].update_with_multi_loss_info(info_list) + else: + raise TypeError( + f"Roll-out information must be of type 'Trajectory' or 'LossInfo', got {type(info_list[0])}" + ) + msg_body = { + MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.ROLLOUT_INFO]} + } + logger.debug(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, tag=MsgTag.LEARN_DONE, body=msg_body) diff --git a/maro/rl/learning/synchronous/rollout_manager.py b/maro/rl/learning/rollout_manager.py similarity index 76% rename from maro/rl/learning/synchronous/rollout_manager.py rename to maro/rl/learning/rollout_manager.py index 3e48d1e67..3923db671 100644 --- a/maro/rl/learning/synchronous/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -13,17 +13,8 @@ from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger, set_seeds -from .rollout_worker import RolloutWorker - - -def get_rollout_finish_msg(ep, segment, step_range, exploration_params=None): - if exploration_params: - return ( - f"Roll-out finished (episode: {ep}, segment: {segment}, " - f"step range: {step_range}, exploration parameters: {exploration_params})" - ) - else: - return f"Roll-out finished (episode: {ep}, segment: {segment}, step range: {step_range})" +from .common import get_rollout_finish_msg +from .environment_sampler import EnvironmentSampler class AbsRolloutManager(ABC): @@ -81,18 +72,15 @@ class SimpleRolloutManager(AbsRolloutManager): """Local roll-out controller. Args: - get_env_wrapper (Callable): Function to be used by each spawned roll-out worker to create an - environment wrapper for training data collection. The function should take no parameters and return an - environment wrapper instance. - get_agent_wrapper (Callable): Function to be used by each spawned roll-out worker to create a - decision generator for interacting with the environment. The function should take no parameters and return - a ``AgentWrapper`` instance. + get_env_wrapper (Callable): Function to create an environment wrapper for collecting training data. The function + should take no parameters and return an environment wrapper instance. + get_agent_wrapper (Callable): Function to create an agent wrapper that interacts with the environment wrapper. + The function should take no parameters and return a ``AgentWrapper`` instance. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - get_env_wrapper (Callable): Function to be used by each spawned roll-out worker to create an - environment wrapper for evaluation. The function should take no parameters and return an environment - wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the - worker processes. Defaults to None. + get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should + take no parameters and return an environment wrapper instance. If this is None, the training environment + wrapper will be used for evaluation in the worker processes. Defaults to None. post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. @@ -133,9 +121,8 @@ def __init__( self._parallelism = parallelism self._eval_parallelism = eval_parallelism if self._parallelism == 1: - self.worker = RolloutWorker( - get_env_wrapper, get_agent_wrapper, - get_eval_env_wrapper=get_eval_env_wrapper + self.env_sampler = EnvironmentSampler( + get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper ) else: self._worker_processes = [] @@ -143,24 +130,25 @@ def __init__( def _rollout_worker(index, conn, get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=None): set_seeds(index) - worker = RolloutWorker(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + env_sampler = EnvironmentSampler( + get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper + ) logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) while True: msg = conn.recv() if msg["type"] == "sample": - ep, segment = msg["episode"], msg["segment"] - result = worker.sample( + result = env_sampler.sample( policy_state_dict=msg["policy_state"], num_steps=self._num_steps, exploration_step=self._exploration_step ) logger.info(get_rollout_finish_msg( - ep, segment, result["step_range"], exploration_params=result["exploration_params"] + msg["episode"], result["step_range"], exploration_params=result["exploration_params"] )) result["worker_index"] = index conn.send(result) elif msg["type"] == "test": - tracker = worker.test(msg["policy_state"]) + tracker = env_sampler.test(msg["policy_state"]) logger.info("Evaluation...") conn.send({"worker_id": index, "tracker": tracker}) elif msg["type"] == "quit": @@ -189,18 +177,18 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): Returns: Experiences for policy training. """ - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") + self._logger.info(f"Collecting simulation data (episode {ep}, policy version {version})") info_by_policy, trackers = defaultdict(list), [] if self._parallelism == 1: - result = self.worker.sample( + result = self.env_sampler.sample( policy_state_dict=policy_state_dict, num_steps=self._num_steps, exploration_step=self._exploration_step ) - self._logger.info(get_rollout_finish_msg( - ep, segment, result["step_range"], exploration_params=result["exploration_params"] - )) + self._logger.info( + get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) + ) for policy_name, info in result["rollout_info"].items(): info_by_policy[policy_name].append(info) @@ -210,7 +198,6 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): rollout_req = { "type": "sample", "episode": ep, - "segment": segment, "num_steps": self._num_steps, "policy_state": policy_state_dict, "exploration_step": self._exploration_step @@ -250,7 +237,7 @@ def evaluate(self, ep: int, policy_state_dict: dict): trackers = [] if self._eval_parallelism == 1: self._logger.info("Evaluating...") - tracker = self.worker.test(policy_state_dict) + tracker = self.env_sampler.test(policy_state_dict) trackers.append(tracker) else: eval_worker_conns = choices(self._manager_ends, k=self._eval_parallelism) @@ -295,7 +282,7 @@ class DistributedRolloutManager(AbsRolloutManager): num_eval_workers (int): Number of workers for evaluation. Defaults to 1. post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. + be (trackers, ep, step_range) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to None. post_evaluate (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should @@ -477,76 +464,3 @@ def exit(self): self._proxy.ibroadcast("rollout_worker", MsgTag.EXIT, SessionType.NOTIFICATION) self._proxy.close() self._logger.info("Exiting...") - - -def rollout_worker( - group: str, - worker_id: int, - get_env_wrapper: Callable[[], AbsEnvWrapper], - get_agent_wrapper: Callable[[], AgentWrapper], - get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Roll-out worker process that can be launched on separate computation nodes. - - Args: - group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager - that manages them. - worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". - This is used for bookkeeping by the parent manager. - env_wrapper (AbsEnvWrapper): Environment wrapper for training data collection. - agent_wrapper (AgentWrapper): Agent wrapper to interact with the environment wrapper. - eval_env_wrapper (AbsEnvWrapper): Environment wrapper for evaluation. If this is None, the training - environment wrapper will be used for evaluation. Defaults to None. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - worker = RolloutWorker(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) - proxy = Proxy( - group, "rollout_worker", {"rollout_manager": 1}, - component_name=f"ROLLOUT_WORKER.{int(worker_id)}", **proxy_kwargs - ) - logger = Logger(proxy.name, dump_folder=log_dir) - - """ - The event loop handles 3 types of messages from the roll-out manager: - 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps - and the collected experiences will be sent back to the roll-out manager; - 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire - duration of the evaluation environment. - 3) EXIT, upon which it will break out of the event loop and the process will terminate. - - """ - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.SAMPLE: - ep, segment = msg.body[MsgKey.EPISODE], msg.body[MsgKey.SEGMENT] - result = worker.sample( - policy_state_dict=msg.body[MsgKey.POLICY_STATE], - num_steps=msg.body[MsgKey.NUM_STEPS], - exploration_step=msg.body[MsgKey.EXPLORATION_STEP] - ) - logger.info(get_rollout_finish_msg( - ep, segment, result["step_range"], exploration_params=result["exploration_params"] - )) - return_info = { - MsgKey.EPISODE: ep, - MsgKey.SEGMENT: segment, - MsgKey.VERSION: msg.body[MsgKey.VERSION], - MsgKey.ROLLOUT_INFO: result["rollout_info"], - MsgKey.STEP_RANGE: result["step_range"], - MsgKey.TRACKER: result["tracker"], - MsgKey.END_OF_EPISODE: result["end_of_episode"] - } - proxy.reply(msg, tag=MsgTag.SAMPLE_DONE, body=return_info) - elif msg.tag == MsgTag.TEST: - tracker = worker.test(msg.body[MsgKey.POLICY_STATE]) - return_info = {MsgKey.TRACKER: tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} - logger.info("Testing complete") - proxy.reply(msg, tag=MsgTag.TEST_DONE, body=return_info) diff --git a/maro/rl/learning/simple_learner.py b/maro/rl/learning/simple_learner.py deleted file mode 100644 index 3d2e8cd17..000000000 --- a/maro/rl/learning/simple_learner.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import Callable, Dict, List, Union - -from maro.rl.policy import RLPolicy -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper -from maro.utils import Logger - -from .early_stopper import AbsEarlyStopper -from .policy_manager import SimplePolicyManager - - -class SimpleLearner: - """Controller for single-threaded learning workflows. - - Args: - env_wrapper (AbsEnvWrapper): Environment wrapper instance to interact with a set of agents and collect - experiences for learning. - agent_wrapper (AgentWrapper): Multi-policy wrapper that interacts with the ``env_wrapper`` directly. - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on how the implementation of the roll-out manager. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the training episodes given in the list. In any case, the policies will be evaluated - at the end of the last training episode. Defaults to None, in which case the policies will only be - evaluated after the last training episode. - eval_env (AbsEnvWrapper): An ``AbsEnvWrapper`` instance for policy evaluation. If None, ``env`` will be used - as the evaluation environment. Defaults to None. - early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metrics are met following an evaluation episode. Default to None. - update_trigger (Dict[str, int]): A dictionary of (policy_name, trigger), where "trigger" indicates the - required number of new experiences to trigger a call to ``learn`` for each policy. Defaults to None, - all triggers will be set to 1. - warmup (Dict[str, int]): A dictionary of (policy_name, warmup_size), where "warmup_size" indicates the - minimum number of experiences in the experience memory required to trigger a call to ``learn`` for - each policy. Defaults to None, in which case all warm-up sizes will be set to 1. - post_collect (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. - post_evaluate (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. - post_update (Callable): Custom function to process whatever information is collected by each - trainer (local or remote) at the end of ``update`` calls. The function signature should be (trackers, - ) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults to - None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - - def __init__( - self, - env_wrapper: AbsEnvWrapper, - agent_wrapper: AgentWrapper, - num_episodes: int, - num_steps: int = -1, - eval_schedule: Union[int, List[int]] = None, - eval_env: AbsEnvWrapper = None, - early_stopper: AbsEarlyStopper = None, - update_trigger: Dict[str, int] = None, - warmup: Dict[str, int] = None, - post_collect: Callable = None, - post_evaluate: Callable = None, - post_update: Callable = None, - log_dir: str = getcwd(), - ): - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - self._logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) - self.env = env_wrapper - self.eval_env = eval_env if eval_env else self.env - self.agent = agent_wrapper - - # Create a policy manager to manage all trainable policies from the agent wrapper - self.policy_manager = SimplePolicyManager( - {name: policy for name, policy in self.agent.policy_dict.items() if isinstance(policy, RLPolicy)}, - update_trigger=update_trigger, - warmup=warmup, - post_update=post_update - ) - - self.num_episodes = num_episodes - self._num_steps = num_steps if num_steps > 0 else float("inf") - - # evaluation schedule - if eval_schedule is None: - self._eval_schedule = [] - elif isinstance(eval_schedule, int): - num_eval_schedule = num_episodes // eval_schedule - self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - else: - self._eval_schedule = eval_schedule - self._eval_schedule.sort() - - # always evaluate after the last episode - if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - self._eval_schedule.append(num_episodes) - - self._logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") - self._eval_point_index = 0 - - self.early_stopper = early_stopper - self._post_collect = post_collect - self._post_evaluate = post_evaluate - self._post_update = post_update - - def run(self): - """Entry point for executing a learning workflow.""" - for ep in range(1, self.num_episodes + 1): - self._collect_and_update(ep) - if ep == self._eval_schedule[self._eval_point_index]: - self._eval_point_index += 1 - self._evaluate(self._eval_point_index) - # early stopping check - if self.early_stopper: - self.early_stopper.push(self.eval_env.summary) - if self.early_stopper.stop(): - return - - def _collect_and_update(self, ep: int): - """Collect simulation data for training.""" - t0 = time.time() - num_experiences_collected = 0 - - self.agent.explore() - self.env.reset() - self.env.start() # get initial state - segment = 0 - while self.env.state: - segment += 1 - self._collect(ep, segment) - exp_by_policy = self.agent.get_batch(self.env) - self.policy_manager.update(exp_by_policy) - num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) - # update the exploration parameters if an episode is finished - self.agent.exploration_step() - - self._logger.info( - f"ep {ep} summary - " - f"running time: {time.time() - t0} " - f"env steps: {self.env.step_index} " - f"experiences collected: {num_experiences_collected}" - ) - - def _collect(self, ep, segment): - start_step_index = self.env.step_index + 1 - steps_to_go = self._num_steps - while self.env.state and steps_to_go: - self.env.step(self.agent.choose_action(self.env.state)) - steps_to_go -= 1 - - self._logger.info( - f"Roll-out finished for ep {ep}, segment {segment}" - f"(steps {start_step_index} - {self.env.step_index})" - ) - - if self._post_collect: - self._post_collect([self.env.tracker], ep, segment) - - def _evaluate(self, ep: int): - """Policy evaluation.""" - self._logger.info("Evaluating...") - self.agent.exploit() - self.eval_env.reset() - self.eval_env.start() # get initial state - while self.eval_env.state: - self.eval_env.step(self.agent.choose_action(self.eval_env.state)) - - if self._post_evaluate: - self._post_evaluate([self.env.tracker], ep) diff --git a/maro/rl/learning/synchronous/__init__.py b/maro/rl/learning/synchronous/__init__.py deleted file mode 100644 index c046dab4c..000000000 --- a/maro/rl/learning/synchronous/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .learner import Learner -from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, SimpleRolloutManager, rollout_worker -from .rollout_worker import RolloutWorker - -__all__ = [ - "Learner", - "AbsRolloutManager", "DistributedRolloutManager", "SimpleRolloutManager", "rollout_worker", - "RolloutWorker" -] diff --git a/maro/rl/learning/synchronous/learner.py b/maro/rl/learning/synchronous/learner.py deleted file mode 100644 index 7c46ad825..000000000 --- a/maro/rl/learning/synchronous/learner.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import List, Union - -from maro.utils import Logger - -from ..early_stopper import AbsEarlyStopper -from ..policy_manager import AbsPolicyManager -from .rollout_manager import AbsRolloutManager - - -class Learner: - """Main controller for learning. - - This should be used in multi-process or distributed settings where either the policy manager or the roll-out - manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such - as duplicate experience storage. Use ``SimpleLearner`` instead. - - Args: - policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. - rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data - collection. - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on the implementation of the roll-out manager. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the training episodes given in the list. In any case, the policies will be evaluated - at the end of the last training episode. Defaults to None, in which case the policies will only be - evaluated after the last training episode. - early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metric are met following an evaluation episode. Default to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time - and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - def __init__( - self, - policy_manager: AbsPolicyManager, - rollout_manager: AbsRolloutManager, - num_episodes: int, - eval_schedule: Union[int, List[int]] = None, - early_stopper: AbsEarlyStopper = None, - log_dir: str = getcwd() - ): - self._logger = Logger("LEARNER", dump_folder=log_dir) - self.policy_manager = policy_manager - self.rollout_manager = rollout_manager - - self.num_episodes = num_episodes - - # evaluation schedule - if eval_schedule is None: - self._eval_schedule = [] - elif isinstance(eval_schedule, int): - num_eval_schedule = num_episodes // eval_schedule - self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - else: - self._eval_schedule = eval_schedule - self._eval_schedule.sort() - - # always evaluate after the last episode - if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - self._eval_schedule.append(num_episodes) - - self._logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") - self._eval_point_index = 0 - - self.early_stopper = early_stopper - - def run(self): - """Entry point for executing a learning workflow.""" - for ep in range(1, self.num_episodes + 1): - self._collect_and_update(ep) - if ep == self._eval_schedule[self._eval_point_index]: - self._eval_point_index += 1 - env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) - # early stopping check - if self.early_stopper: - for env_metric in env_metric_dict.values(): - self.early_stopper.push(env_metric) - if self.early_stopper.stop(): - return - - if hasattr(self.rollout_manager, "exit"): - self.rollout_manager.exit() - - if hasattr(self.policy_manager, "exit"): - self.policy_manager.exit() - - def _collect_and_update(self, ep: int): - collect_time = policy_update_time = 0 - segment = 0 - self.rollout_manager.reset() - while not self.rollout_manager.episode_complete: - segment += 1 - # experience collection - policy_state_dict = self.policy_manager.get_state() - policy_version = self.policy_manager.version - tc0 = time.time() - rollout_info_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) - collect_time += time.time() - tc0 - self._logger.info(f"collect time: {collect_time}") - tu0 = time.time() - self.policy_manager.update(rollout_info_by_policy) - policy_update_time += time.time() - tu0 - self._logger.info(f"update time: {policy_update_time}") - - # performance details - self._logger.info( - f"ep {ep} summary - " - f"collect time: {collect_time} " - f"policy update time: {policy_update_time}" - ) diff --git a/maro/rl/learning/synchronous/rollout_worker.py b/maro/rl/learning/synchronous/rollout_worker.py deleted file mode 100644 index ad5841a0e..000000000 --- a/maro/rl/learning/synchronous/rollout_worker.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Callable - -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper - - -class RolloutWorker: - def __init__( - self, - get_env_wrapper: Callable[[], AbsEnvWrapper], - get_agent_wrapper: Callable[[], AgentWrapper], - get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None - ): - self.env = get_env_wrapper() - self.eval_env = get_env_wrapper() if get_eval_env_wrapper else self.env - self.agent = get_agent_wrapper() - - def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): - # set policy states - if policy_state_dict: - self.agent.set_policy_states(policy_state_dict) - - # set exploration parameters - self.agent.explore() - if exploration_step: - self.agent.exploration_step() - - if self.env.state is None: - self.env.reset() - self.env.replay = True - self.env.start() # get initial state - - starting_step_index = self.env.step_index + 1 - steps_to_go = float("inf") if num_steps == -1 else num_steps - while self.env.state and steps_to_go > 0: - action = self.agent.choose_action(self.env.state) - self.env.step(action) - steps_to_go -= 1 - - return { - "rollout_info": self.agent.get_rollout_info(self.env.get_trajectory()), - "step_range": (starting_step_index, self.env.step_index), - "tracker": self.env.tracker, - "end_of_episode": not self.env.state, - "exploration_params": self.agent.exploration_params - } - - def test(self, policy_state_dict: dict): - self.agent.set_policy_states(policy_state_dict) - self.agent.exploit() - self.eval_env.reset() - self.eval_env.replay = False - self.eval_env.start() # get initial state - while self.eval_env.state: - action = self.agent.choose_action(self.eval_env.state) - self.eval_env.step(action) - - return self.eval_env.tracker diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index c92146f87..17a1e0a98 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -3,7 +3,7 @@ from .ac import ACActionInfo, ACBatch, ACLossInfo, ActorCritic, DiscreteACNet from .ddpg import DDPG, ContinuousACNet, DDPGBatch, DDPGLossInfo -from .dqn import DQN, DiscreteQNet, DQNBatch, DQNLossInfo, PrioritizedSampler +from .dqn import DQN, DiscreteQNet, DQNBatch, DQNLossInfo, PrioritizedExperienceReplay from .index import get_model_cls, get_policy_cls from .pg import DiscretePolicyNet, PGActionInfo, PGBatch, PGLossInfo, PolicyGradient from .policy import AbsPolicy, Batch, LossInfo, NullPolicy, RLPolicy @@ -11,7 +11,7 @@ __all__ = [ "ACActionInfo", "ACBatch", "ACLossInfo", "ActorCritic", "DiscreteACNet", "DDPG", "DDPGBatch", "DDPGLossInfo", "ContinuousACNet", - "DQN", "DQNBatch", "DQNLossInfo", "DiscreteQNet", "PrioritizedSampler", + "DQN", "DQNBatch", "DQNLossInfo", "DiscreteQNet", "PrioritizedExperienceReplay", "PGActionInfo", "PGBatch", "PGLossInfo", "DiscretePolicyNet", "PolicyGradient", "AbsPolicy", "Batch", "LossInfo", "NullPolicy", "RLPolicy", "get_model_cls", "get_policy_cls" diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index b58431edf..836c86bab 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -57,8 +57,8 @@ def __init__(self, loss, td_errors, indexes, grad=None): self.grad = grad -class PrioritizedSampler: - """Sampler for Prioritized Experience Replay (PER). +class PrioritizedExperienceReplay: + """Prioritized Experience Replay (PER). References: https://arxiv.org/pdf/1511.05952.pdf @@ -116,7 +116,7 @@ def update(self, indexes, td_errors): self._sum_tree[tree_idx] = priority self._update(tree_idx, delta) - def get(self): + def sample(self): """Priority-based sampling.""" indexes, priorities = [], [] segment_len = self.total() / self.batch_size @@ -221,7 +221,7 @@ def __init__( self._replay_memory = ReplayMemory(DQNBatch, replay_memory_capacity, random_overwrite=random_overwrite) self.prioritized_replay = prioritized_replay_kwargs is not None if self.prioritized_replay: - self._sampler = PrioritizedSampler(self._replay_memory, **prioritized_replay_kwargs) + self._per = PrioritizedExperienceReplay(self._replay_memory, **prioritized_replay_kwargs) else: self._loss_func = torch.nn.MSELoss() @@ -248,11 +248,11 @@ def _put_in_replay_memory(self, traj: Trajectory): batch = DQNBatch(traj.states[:-2], traj.actions[:-2], traj.rewards[:-1], traj.states[1:-1]) indexes = self._replay_memory.put(batch) if self.prioritized_replay: - self._sampler.set_max_priority(indexes) + self._per.set_max_priority(indexes) def _sample(self) -> DQNBatch: if self.prioritized_replay: - indexes, is_weights = self._sampler.get() + indexes, is_weights = self._per.sample() else: indexes = np.random.choice(self._replay_memory.size) is_weights = None @@ -298,7 +298,7 @@ def get_batch_loss(self, batch: DQNBatch, explicit_grad: bool = False): def update_with_multi_loss_info(self, loss_info_list: List[DQNLossInfo]): if self.prioritized_replay: for loss_info in loss_info_list: - self._sampler.update(loss_info.indexes, loss_info.td_errors) + self._per.update(loss_info.indexes, loss_info.td_errors) self.q_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) self._q_net_version += 1 diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index a3a9f9413..7dc73ce5d 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -14,6 +14,7 @@ class MsgTag(Enum): POLICY_STATE = "policy_state" CHOOSE_ACTION = "choose_action" ACTION = "action" + GET_INITIAL_POLICY_STATE = "get_initial_policy_state" LEARN = "learn" LEARN_DONE = "learn_finished" COMPUTE_GRAD = "compute_grad" From aefd3b51c3bd3c49d4cf8507ab4393c8d9821274 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 23 Aug 2021 11:15:20 +0000 Subject: [PATCH 415/482] 1. simplified rl example structure; 2. fixed lint issues --- .../rl/scripts/docker/docker_compose_yml.py | 10 ++-- examples/rl/workflows/agent_wrapper.py | 20 ------- examples/rl/workflows/asynchronous/actor.py | 39 ------------- .../workflows/asynchronous/policy_server.py | 27 --------- examples/rl/workflows/config.yml | 2 +- .../rl/workflows/{synchronous => }/learner.py | 4 +- .../{policy_manager => }/policy_host.py | 0 .../{policy_manager => }/policy_manager.py | 14 +++++ examples/rl/workflows/rollout.py | 57 +++++++++++++++++++ examples/rl/workflows/simple_learner.py | 2 +- .../workflows/synchronous/rollout_worker.py | 33 ----------- maro/rl/learning/__init__.py | 1 - maro/rl/learning/common.py | 1 + maro/rl/learning/environment_sampler.py | 16 +++--- maro/rl/learning/learner.py | 4 +- maro/rl/learning/policy_manager.py | 13 +++-- 16 files changed, 99 insertions(+), 144 deletions(-) delete mode 100644 examples/rl/workflows/agent_wrapper.py delete mode 100644 examples/rl/workflows/asynchronous/actor.py delete mode 100644 examples/rl/workflows/asynchronous/policy_server.py rename examples/rl/workflows/{synchronous => }/learner.py (96%) rename examples/rl/workflows/{policy_manager => }/policy_host.py (100%) rename examples/rl/workflows/{policy_manager => }/policy_manager.py (75%) create mode 100644 examples/rl/workflows/rollout.py delete mode 100644 examples/rl/workflows/synchronous/rollout_worker.py diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index dd12cbbd6..5b277c146 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -70,7 +70,7 @@ str_id = f"policy_host.{host_id}" host_spec = deepcopy(common_spec) del host_spec["build"] - host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_manager/policy_host.py" + host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_host.py" host_spec["container_name"] = f"{namespace}.{str_id}" host_spec["environment"] = [f"HOSTID={host_id}"] + common_env docker_compose_manifest["services"][str_id] = host_spec @@ -82,7 +82,7 @@ **common_spec, **{ "container_name": f"{namespace}.learner", - "command": "python3 /maro/rl_examples/workflows/synchronous/learner.py", + "command": "python3 /maro/rl_examples/workflows/learner.py", "environment": [ f"ROLLOUTTYPE={config['sync']['rollout_type']}", f"EVALPARALLELISM={config['sync']['simple']['eval_parallelism']}", @@ -105,7 +105,7 @@ str_id = f"rollout_worker.{worker_id}" worker_spec = deepcopy(common_spec) del worker_spec["build"] - worker_spec["command"] = "python3 /maro/rl_examples/workflows/synchronous/rollout_worker.py" + worker_spec["command"] = "python3 /maro/rl_examples/workflows/rollout.py" worker_spec["container_name"] = f"{namespace}.{str_id}" worker_spec["environment"] = [ f"WORKERID={worker_id}", @@ -119,7 +119,7 @@ **common_spec, **{ "container_name": f"{namespace}.policy_server", - "command": "python3 /maro/rl_examples/workflows/asynchronous/policy_server.py", + "command": "python3 /maro/rl_examples/workflows/policy_manager.py", "environment": [ f"GROUP={config['async']['group']}", f"MAXLAG={config['max_lag']}" @@ -131,7 +131,7 @@ str_id = f"actor.{actor_id}" actor_spec = deepcopy(common_spec) del actor_spec["build"] - actor_spec["command"] = "python3 /maro/rl_examples/workflows/asynchronous/actor.py" + actor_spec["command"] = "python3 /maro/rl_examples/workflows/rollout.py" actor_spec["container_name"] = f"{namespace}.{str_id}" actor_spec["environment"] = [ f"ACTORID={actor_id}", diff --git a/examples/rl/workflows/agent_wrapper.py b/examples/rl/workflows/agent_wrapper.py deleted file mode 100644 index 730989903..000000000 --- a/examples/rl/workflows/agent_wrapper.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os.path import dirname, realpath - -from maro.rl.wrappers import AgentWrapper - -workflow_dir = dirname(dirname(realpath(__file__))) # template directory -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from general import agent2policy, non_rl_policy_func_index, rl_policy_func_index - - -def get_agent_wrapper(): - return AgentWrapper( - {**non_rl_policy_func_index, **rl_policy_func_index}, - agent2policy - ) diff --git a/examples/rl/workflows/asynchronous/actor.py b/examples/rl/workflows/asynchronous/actor.py deleted file mode 100644 index 5de8bf6d9..000000000 --- a/examples/rl/workflows/asynchronous/actor.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os import getenv -from os.path import dirname, realpath - -from maro.rl.learning import EnvironmentSampler - -workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from agent_wrapper import get_agent_wrapper -from general import get_env_wrapper, log_dir - - -if __name__ == "__main__": - index = getenv("ACTORID") - if index is None: - raise ValueError("Missing environment variable: ACTORID") - index = int(index) - - num_episodes = getenv("NUMEPISODES") - if num_episodes is None: - raise ValueError("Missing envrionment variable: NUMEPISODES") - num_episodes = int(num_episodes) - num_steps = int(getenv("NUMSTEPS", default=-1)) - - env_sampler = EnvironmentSampler(get_env_wrapper, get_agent_wrapper) - env_sampler.actor( - getenv("GROUP", default="ASYNC"), index, num_episodes, - num_steps=num_steps, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir, - ) diff --git a/examples/rl/workflows/asynchronous/policy_server.py b/examples/rl/workflows/asynchronous/policy_server.py deleted file mode 100644 index a33965024..000000000 --- a/examples/rl/workflows/asynchronous/policy_server.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os import getenv -from os.path import dirname, realpath - -workflow_dir = dirname(dirname(realpath(__file__))) # DQN directory -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from policy_manager.policy_manager import get_policy_manager -from general import log_dir - - -if __name__ == "__main__": - policy_manager = get_policy_manager() - policy_manager.server( - getenv("GROUP", default="ASYNC"), - int(getenv("NUMROLLOUTS", default=5)), - max_lag=int(getenv("MAXLAG", default=0)), - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir - ) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 615dbe8ca..f5c5881d2 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -3,7 +3,7 @@ job: cim scenario: cim -mode: async +mode: sync num_episodes: 5 eval_schedule: 5 num_steps: -1 diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/learner.py similarity index 96% rename from examples/rl/workflows/synchronous/learner.py rename to examples/rl/workflows/learner.py index 5f0a0ca79..a5c6a21ac 100644 --- a/examples/rl/workflows/synchronous/learner.py +++ b/examples/rl/workflows/learner.py @@ -11,8 +11,8 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from agent_wrapper import get_agent_wrapper -from policy_manager.policy_manager import get_policy_manager +from policy_manager import get_policy_manager +from rollout import get_agent_wrapper from general import post_collect, post_evaluate, get_env_wrapper, get_eval_env_wrapper, log_dir diff --git a/examples/rl/workflows/policy_manager/policy_host.py b/examples/rl/workflows/policy_host.py similarity index 100% rename from examples/rl/workflows/policy_manager/policy_host.py rename to examples/rl/workflows/policy_host.py diff --git a/examples/rl/workflows/policy_manager/policy_manager.py b/examples/rl/workflows/policy_manager.py similarity index 75% rename from examples/rl/workflows/policy_manager/policy_manager.py rename to examples/rl/workflows/policy_manager.py index fbd9e7ae0..b3666393e 100644 --- a/examples/rl/workflows/policy_manager/policy_manager.py +++ b/examples/rl/workflows/policy_manager.py @@ -36,3 +36,17 @@ def get_policy_manager(): return policy_manager raise ValueError(f"Unsupported policy manager type: {manager_type}. Supported modes: simple, distributed") + + +if __name__ == "__main__": + policy_manager = get_policy_manager() + policy_manager.server( + getenv("GROUP", default="ASYNC"), + int(getenv("NUMROLLOUTS", default=5)), + max_lag=int(getenv("MAXLAG", default=0)), + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir + ) diff --git a/examples/rl/workflows/rollout.py b/examples/rl/workflows/rollout.py new file mode 100644 index 000000000..921c07a07 --- /dev/null +++ b/examples/rl/workflows/rollout.py @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os import getenv +from os.path import dirname, realpath + +from maro.rl.learning import EnvironmentSampler +from maro.rl.wrappers import AgentWrapper + +workflow_dir = dirname(dirname(realpath(__file__))) # template directory +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) + +from general import ( + agent2policy, get_env_wrapper, get_eval_env_wrapper, log_dir, non_rl_policy_func_index, rl_policy_func_index +) + + +def get_agent_wrapper(): + return AgentWrapper( + {**non_rl_policy_func_index, **rl_policy_func_index}, + agent2policy + ) + + +if __name__ == "__main__": + index = getenv("WORKERID") + if index is None: + raise ValueError("Missing environment variable: WORKERID") + index = int(index) + + env_sampler = EnvironmentSampler(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + if getenv("MODE") == "sync": + env_sampler.worker( + getenv("ROLLOUTGROUP", default="rollout"), index, + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir + ) + else: + num_episodes = getenv("NUMEPISODES") + if num_episodes is None: + raise ValueError("Missing envrionment variable: NUMEPISODES") + num_episodes = int(num_episodes) + num_steps = int(getenv("NUMSTEPS", default=-1)) + env_sampler.actor( + getenv("GROUP", default="ASYNC"), index, num_episodes, + num_steps=num_steps, + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir, + ) diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index 7a0c082a4..92f2fd59a 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -14,8 +14,8 @@ with open(join(workflow_dir, "config.yml"), "r") as fp: config = yaml.safe_load(fp) -from agent_wrapper import get_agent_wrapper from general import get_env_wrapper, get_eval_env_wrapper, log_dir, post_collect, post_evaluate +from rollout import get_agent_wrapper if __name__ == "__main__": diff --git a/examples/rl/workflows/synchronous/rollout_worker.py b/examples/rl/workflows/synchronous/rollout_worker.py deleted file mode 100644 index 8fc3d46f9..000000000 --- a/examples/rl/workflows/synchronous/rollout_worker.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os import getenv -from os.path import dirname, realpath - -from maro.rl.learning import EnvironmentSampler - - -workflow_dir = dirname(dirname(realpath(__file__))) # template directory -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from agent_wrapper import get_agent_wrapper -from general import get_env_wrapper, get_eval_env_wrapper, log_dir - - -if __name__ == "__main__": - index = getenv("WORKERID") - if index is None: - raise ValueError("Missing environment variable: WORKERID") - index = int(index) - - env_sampler = EnvironmentSampler(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) - env_sampler.worker( - getenv("ROLLOUTGROUP", default="rollout"), index, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir - ) diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index c54a723ba..4edaf13e6 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -7,7 +7,6 @@ from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager, policy_host from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, SimpleRolloutManager - __all__ = [ "AbsEarlyStopper", "EnvironmentSampler", diff --git a/maro/rl/learning/common.py b/maro/rl/learning/common.py index c3cb1eae3..a4990991f 100644 --- a/maro/rl/learning/common.py +++ b/maro/rl/learning/common.py @@ -3,6 +3,7 @@ from typing import List, Union + def get_rollout_finish_msg(ep, step_range, exploration_params=None): if exploration_params: return ( diff --git a/maro/rl/learning/environment_sampler.py b/maro/rl/learning/environment_sampler.py index de9ce7b4a..8dbad9747 100644 --- a/maro/rl/learning/environment_sampler.py +++ b/maro/rl/learning/environment_sampler.py @@ -5,9 +5,9 @@ from typing import Callable from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.rl.utils import MsgKey, MsgTag -from maro.utils import Logger +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper +from maro.utils import Logger from .common import get_rollout_finish_msg @@ -155,9 +155,9 @@ def actor( which case the roll-out will be executed until the end of the environment. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current working - directory. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. """ if num_steps == 0 or num_steps < -1: raise ValueError("num_steps must be a positive integer or -1") @@ -182,7 +182,7 @@ def actor( logger.info( get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) ) - # Send roll-out info to policy server and + # Send roll-out info to policy server for learning reply = proxy.send( SessionMessage( MsgTag.SAMPLE_DONE, proxy.name, policy_server_address, @@ -196,5 +196,7 @@ def actor( exploration_step = False # tell the policy server I'm all done. - proxy.isend(SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION)) + proxy.isend( + SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION) + ) proxy.close() diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py index d0f9138cd..e4a19ff68 100644 --- a/maro/rl/learning/learner.py +++ b/maro/rl/learning/learner.py @@ -7,7 +7,7 @@ from maro.rl.policy import LossInfo from maro.rl.types import Trajectory -from maro.rl.wrappers import AgentWrapper, AbsEnvWrapper +from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger from .common import get_eval_schedule, get_rollout_finish_msg @@ -90,7 +90,7 @@ def collect_and_update(ep): env_sampler.agent.policy_dict[policy_name].update_with_multi_loss_info(info_list) if post_collect: - post_collect([result["tracker"]], ep, segment) + post_collect([result["tracker"]], ep, segment) if result["end_of_episode"]: break diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index bda746b75..d50c904f6 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -23,7 +23,7 @@ def __init__(self): @abstractmethod def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. - + The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss information computed directly by roll-out workers. """ @@ -42,14 +42,15 @@ def get_version(self): def server(self, group: str, num_actors: int, max_lag: int = 0, proxy_kwargs: dict = {}, log_dir: str = getcwd()): """Run a server process. - The process serves the latest policy states to a set of remote actors and receives simulated experiences from them. + The process serves the latest policy states to a set of remote actors and receives simulated experiences from + them. Args: group (str): Group name for the cluster that includes the server and all actors. num_actors (int): Number of remote actors to collect simulation experiences. max_lag (int): Maximum policy version lag allowed for experiences collected from remote actors. Experiences - collected using policy versions older than (current_version - max_lag) will be discarded. - Defaults to 0, in which case only experiences collected using the latest policy version will be returned. + collected using policy versions older than (current_version - max_lag) will be discarded. Defaults to 0, + in which case only experiences collected using the latest policy version will be returned. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. @@ -87,7 +88,7 @@ def server(self, group: str, num_actors: int, max_lag: int = 0, proxy_kwargs: di class SimplePolicyManager(AbsPolicyManager): - """Policy manager that contains all policy instances. + """Policy manager that contains all policy instances. Args: create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a @@ -155,7 +156,7 @@ def _policy_host(name, create_policy_func, conn): def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. - + The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss information computed directly by roll-out workers. """ From db99ce2e7f8ed6f89ffa39136598b4976e1dc8a7 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 25 Aug 2021 12:50:50 +0000 Subject: [PATCH 416/482] further rl toolkit code simplifications --- maro/rl/learning/env_sampler.py | 456 ++++++++++++++++++++++++++++++ maro/rl/modeling/__init__.py | 12 + maro/rl/modeling/core_model.py | 180 ++++++++++++ maro/rl/modeling/fc_block.py | 115 ++++++++ maro/rl/modeling/special_types.py | 265 +++++++++++++++++ maro/rl/policy/ac.py | 5 + maro/rl/types.py | 81 ++++++ 7 files changed, 1114 insertions(+) create mode 100644 maro/rl/learning/env_sampler.py create mode 100644 maro/rl/modeling/__init__.py create mode 100644 maro/rl/modeling/core_model.py create mode 100644 maro/rl/modeling/fc_block.py create mode 100644 maro/rl/modeling/special_types.py create mode 100644 maro/rl/types.py diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py new file mode 100644 index 000000000..5e917d0ba --- /dev/null +++ b/maro/rl/learning/env_sampler.py @@ -0,0 +1,456 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABC, abstractmethod +from collections import defaultdict, deque +from os import getcwd +from typing import Callable, Dict + +import numpy as np + +from maro.communication import Proxy, SessionMessage, SessionType +from maro.rl.policy import AbsPolicy, RLPolicy +from maro.rl.types import Trajectory, Transition +from maro.rl.utils import MsgKey, MsgTag +from maro.simulator import Env +from maro.utils import Logger + +from .common import get_rollout_finish_msg + + +class AbsEnvWrapper(ABC): + """Environment wrapper that performs scenario-specific processing, transition caching and experience generation. + + Args: + env (Env): Environment instance. + reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward + for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately + after executing an action. + replay_agent_ids (list): List of agent IDs whose transitions will be stored in internal replay buffers. + If it is None, it will be set to all agents in the environment (i.e., env.agent_idx_list). Defaults + to None. + post_step (Callable): Custom function to gather information about a transition and the evolvement of the + environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` + instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition + is a ``Transition`` object. For example, this callback can be used to collect various statistics on the + simulation. Defaults to None. + """ + def __init__( + self, + env: Env, + reward_eval_delay: int = 0, + replay_agent_ids: list = None, + max_trajectory_len: Dict[str, int] = None, + post_step: Callable = None + ): + self.env = env + self.reward_eval_delay = reward_eval_delay + + self._post_step = post_step + + self.replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids + self.max_trajectory_len = max_trajectory_len if max_trajectory_len else defaultdict(lambda: self.env._durations) + self._trajectory_ptr = defaultdict(int) + self._last_trajectory_ptr = defaultdict(int) + self._replay_buffer = { + agent_id: { + "states": np.zeros((self.max_trajectory_len[agent_id], self.state_dim), dtype=np.float32), + "actions": np.zeros((self.max_trajectory_len[agent_id], self.action_dim), dtype=np.float32), + "rewards": np.zeros(self.max_trajectory_len[agent_id], dtype=np.float32), + "terminal": np.zeros(self.max_trajectory_len[agent_id], dtype=np.bool), + "info": [None] * self.max_trajectory_len[agent_id] + } + for agent_id in replay_agent_ids + } + self._transition_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated + self._step_index = None + self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. + self._state = None # the latest extracted state is kept here + + self.tracker = {} # User-defined tracking information is placed here. + self.replay = True + + @property + @abstractmethod + def state_dim(self) -> int: + raise NotImplementedError + + @property + @abstractmethod + def action_dim(self) -> int: + raise NotImplementedError + + @property + def step_index(self): + """Number of environmental steps taken so far.""" + return self._step_index + + @property + def agent_idx_list(self): + return self.env.agent_idx_list + + @property + def summary(self): + return self.env.metrics + + @property + def state(self): + """The current environmental state.""" + return self._state + + @property + def event(self): + return self._event + + def start(self): + """Generate the initial environmental state at the beginning of a simulation episode.""" + self._step_index = 0 + _, self._event, _ = self.env.step(None) + self._state = self.get_state(self.env.tick) + + @abstractmethod + def get_state(self, tick: int = None) -> dict: + """Compute the state for a given tick. + + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + + Returns: + A dictionary with (agent ID, state) as key-value pairs. + """ + raise NotImplementedError + + @abstractmethod + def to_env_action(self, action) -> dict: + """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" + raise NotImplementedError + + @abstractmethod + def get_reward(self, actions: list, tick: int = None): + """Evaluate the reward for an action. + + Args: + tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in + ``actions`` must be an Action object defined for the environment in question. The tick may + be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). + Defaults to None. + + Returns: + A dictionary with (agent ID, reward) as key-value pairs. + """ + raise NotImplementedError + + def get_transition_info(self, tick: int = None): + """Get additional info for a transition. + + The returned transition info will be stored in the experience manager alongside states, actions, rewards. + + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + + Returns: + A dictionary with (agent ID, transition_info) as key-value pairs. + + """ + pass + + def step(self, action_by_agent: dict): + """Wrapper for env.step(). + + The new transition is stored in the replay buffer or cached in a separate data structure if the + reward cannot be determined yet due to a non-zero ``reward_eval_delay``. + """ + self._step_index += 1 + env_action = self.to_env_action(action_by_agent) + + self._transition_cache.append(( + self._state, + action_by_agent, + env_action, + self.get_transition_info(), + self.env.tick + )) + + _, self._event, done = self.env.step(env_action) + + if not done: + self._state = self.get_state(self.env.tick) # current env state + else: + self._state = None + + """ + If this is the final step, evaluate rewards for all remaining events except the last. + Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. + """ + while ( + self._transition_cache and + (done or self.env.tick - self._transition_cache[0][-1] >= self.reward_eval_delay) + ): + state, action, env_action, info, tick = self._transition_cache.popleft() + reward = self.get_reward(env_action, tick=tick) + if self._post_step: + next_state = self._transition_cache[0][0] if self._transition_cache else None + transition = Transition(state, action, env_action, reward, next_state, info) + # put things you want to track in the tracker attribute + self._post_step(self.env, self.tracker, transition) + + if self.replay: + for agent_id, agent_state in state.items(): + if agent_id in self._replay_buffer: + buf, ptr = self._replay_buffer[agent_id], self._trajectory_ptr[agent_id] + buf["states"][ptr] = agent_state + buf["actions"][ptr] = action[agent_id] + buf["rewards"][ptr] = reward[agent_id] + buf["terminal"][ptr] = self._transition_cache.empty() and not self._state + if info: + buf["info"][ptr] = info[agent_id] + # increment pointer + self._trajectory_ptr[agent_id] += 1 + if self._trajectory_ptr[agent_id] == self.max_trajectory_len[agent_id]: + self._trajectory_ptr[agent_id] = 0 + + def get_trajectory(self): + """Get agent trajectories from the replay buffer and clear it in the process.""" + trajectory = {} + for agent_id, buf in self._replay_buffer.items(): + traj_slice = slice(self._last_trajectory_ptr[agent_id], self._trajectory_ptr[agent_id]) + states = buf["states"][traj_slice] + actions = buf["actions"][traj_slice] + rewards = buf["rewards"][traj_slice] + terminal_flags = buf["terminal"][traj_slice] + info = buf["info"][traj_slice] + # else: + # traj_slice_minus_1 = slice(self._last_trajectory_ptr[agent_id], self._trajectory_ptr[agent_id] - 1) + # rewards = buf["rewards"][traj_slice_minus_1] + # info = buf["info"][traj_slice_minus_1] + + self._last_trajectory_ptr[agent_id] = self._trajectory_ptr[agent_id] + trajectory[agent_id] = Trajectory(states, , actions, rewards, info, terminal=not self._state) + + return trajectory + + def reset(self): + self.env.reset() + self._state = None + self._transition_cache.clear() + self.tracker.clear() + self._trajectory_ptr = defaultdict(int) + self._last_trajectory_ptr = defaultdict(int) + + +class EnvSampler: + """Simulation data collector and policy evaluator. + + Args: + get_env_wrapper (Callable): Function to create an environment wrapper for collecting training data. The function + should take no parameters and return an environment wrapper instance. + get_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. + agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's + queries to the correct policy. + get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should + take no parameters and return an environment wrapper instance. If this is None, the training environment + wrapper will be used for evaluation in the worker processes. Defaults to None. + """ + def __init__( + self, + get_env_wrapper: Callable[[], AbsEnvWrapper], + get_policy_func_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str], + get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None + ): + self.env = get_env_wrapper() + self.eval_env = get_env_wrapper() if get_eval_env_wrapper else self.env + self.policy_dict = {name: func(name) for name, func in get_policy_func_dict.items()} + self.agent2policy = agent2policy + self.policy = { + agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() + } + + def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): + # set policy states + if policy_state_dict: + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].set_state(policy_state) + + # update exploration states if necessary + for policy in self.policy_dict.values(): + if hasattr(policy, "explore"): + policy.explore() + + if exploration_step: + for policy in self.policy_dict.values(): + if hasattr(policy, "exploration_step"): + policy.exploration_step() + + if self.env.state is None: + self.env.reset() + self.env.replay = True + self.env.start() # get initial state + + starting_step_index = self.env.step_index + 1 + steps_to_go = float("inf") if num_steps == -1 else num_steps + while self.env.state and steps_to_go > 0: + self.env.step({agent: self.policy[agent].choose_action(st) for agent, st in self.env.state.items()}) + steps_to_go -= 1 + + rollout_info = {} + for agent_id, traj in self.env.get_trajectory().items(): + if isinstance(self.policy[agent_id], RLPolicy): + policy_name = self.agent2policy[agent_id] + rollout_info[policy_name] = self.policy_dict[policy_name].get_rollout_info(traj) + + return { + "rollout_info": rollout_info, + "step_range": (starting_step_index, self.env.step_index), + "tracker": self.env.tracker, + "end_of_episode": not self.env.state, + "exploration_params": { + name: policy.exploration_params for name, policy in self.policy_dict.items() + if isinstance(policy, RLPolicy) + } + } + + def test(self, policy_state_dict: dict = None): + if policy_state_dict: + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].set_state(policy_state) + + # Set policies to exploitation mode + for policy in self.policy_dict.values(): + if hasattr(policy, "exploit"): + policy.exploit() + + self.eval_env.reset() + self.eval_env.replay = False + self.eval_env.start() # get initial state + while self.eval_env.state: + self.eval_env.step({agent: self.policy[agent].choose_action(st) for agent, st in self.env.state.items()}) + + return self.eval_env.tracker + + def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = getcwd()): + """Roll-out worker process that can be launched on separate computation nodes. + + Args: + group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager + that manages them. + worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". + This is used for bookkeeping by the parent manager. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + proxy = Proxy( + group, "rollout_worker", {"rollout_manager": 1}, component_name=f"ROLLOUT_WORKER.{index}", **proxy_kwargs + ) + logger = Logger(proxy.name, dump_folder=log_dir) + + """ + The event loop handles 3 types of messages from the roll-out manager: + 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps + and the collected experiences will be sent back to the roll-out manager; + 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire + duration of the evaluation environment. + 3) EXIT, upon which it will break out of the event loop and the process will terminate. + + """ + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + + if msg.tag == MsgTag.SAMPLE: + ep = msg.body[MsgKey.EPISODE] + result = self.sample( + policy_state_dict=msg.body[MsgKey.POLICY_STATE], + num_steps=msg.body[MsgKey.NUM_STEPS], + exploration_step=msg.body[MsgKey.EXPLORATION_STEP] + ) + logger.info( + get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) + ) + return_info = { + MsgKey.EPISODE: ep, + MsgKey.SEGMENT: msg.body[MsgKey.SEGMENT], + MsgKey.VERSION: msg.body[MsgKey.VERSION], + MsgKey.ROLLOUT_INFO: result["rollout_info"], + MsgKey.STEP_RANGE: result["step_range"], + MsgKey.TRACKER: result["tracker"], + MsgKey.END_OF_EPISODE: result["end_of_episode"] + } + proxy.reply(msg, tag=MsgTag.SAMPLE_DONE, body=return_info) + elif msg.tag == MsgTag.TEST: + tracker = self.test(msg.body[MsgKey.POLICY_STATE]) + return_info = {MsgKey.TRACKER: tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} + logger.info("Testing complete") + proxy.reply(msg, tag=MsgTag.TEST_DONE, body=return_info) + + def actor( + self, + group: str, + index: int, + num_episodes: int, + num_steps: int = -1, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() + ): + """Controller for single-threaded learning workflows. + + Args: + group (str): Group name for the cluster that includes the server and all actors. + index (int): Integer actor index. The actor's ID in the cluster will be "ACTOR.{actor_idx}". + num_episodes (int): Number of training episodes. Each training episode may contain one or more + collect-update cycles, depending on how the implementation of the roll-out manager. + num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in + which case the roll-out will be executed until the end of the environment. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at + init time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + peers = {"policy_server": 1} + proxy = Proxy(group, "actor", peers, component_name=f"ACTOR.{index}", **proxy_kwargs) + policy_server_address = proxy.peers["policy_server"][0] + logger = Logger(proxy.name, dump_folder=log_dir) + + # get initial policy states from the policy manager + msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, policy_server_address) + reply = proxy.send(msg)[0] + policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] + + # main loop + for ep in range(1, num_episodes + 1): + exploration_step = True + while True: + result = self.sample( + policy_state_dict=policy_state_dict, num_steps=num_steps, exploration_step=exploration_step + ) + logger.info( + get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) + ) + # Send roll-out info to policy server for learning + reply = proxy.send( + SessionMessage( + MsgTag.SAMPLE_DONE, proxy.name, policy_server_address, + body={MsgKey.ROLLOUT_INFO: result["rollout_info"], MsgKey.VERSION: policy_version} + ) + )[0] + policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] + if result["end_of_episode"]: + break + + exploration_step = False + + # tell the policy server I'm all done. + proxy.isend( + SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION) + ) + proxy.close() diff --git a/maro/rl/modeling/__init__.py b/maro/rl/modeling/__init__.py new file mode 100644 index 000000000..dbb571bde --- /dev/null +++ b/maro/rl/modeling/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .core_model import AbsCoreModel, OptimOption +from .fc_block import FullyConnectedBlock +from .special_types import ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet + +__all__ = [ + "AbsCoreModel", "OptimOption", + "FullyConnectedBlock", + "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet" +] diff --git a/maro/rl/modeling/core_model.py b/maro/rl/modeling/core_model.py new file mode 100644 index 000000000..24e3ad92e --- /dev/null +++ b/maro/rl/modeling/core_model.py @@ -0,0 +1,180 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import abstractmethod +from typing import Dict, List, Union + +import torch +import torch.nn as nn + +from maro.rl.utils import get_torch_lr_scheduler_cls, get_torch_optim_cls +from maro.utils import clone +from maro.utils.exception.rl_toolkit_exception import MissingOptimizer + + +class OptimOption: + """Model optimization options. + + Args: + optim_cls: A string indicating an optimizer class provided by torch.optim or custom subclass of + torch.optim.Optimizer. If a string is provided, it must be present in the ``TORCH_OPTIM`` index. + optim_params (dict): Parameters for the optimizer class. + scheduler_cls: A string indicating an lr-scheduler class provided by torch.optim.lr_scheduler or custom + subclass of torch.optim.lr_scheduler. If a string is provided, it must be present in the + ``TORCH_LR_SCHEDULER`` index. Defaults to None. + scheduler_params (dict): Parameters for the scheduler class. Defaults to None. + """ + __slots__ = ["optim_cls", "optim_params", "scheduler_cls", "scheduler_params"] + + def __init__(self, optim_cls, optim_params: dict, scheduler_cls=None, scheduler_params: dict = None): + self.optim_cls = get_torch_optim_cls(optim_cls) + self.optim_params = optim_params + self.scheduler_cls = get_torch_lr_scheduler_cls(scheduler_cls) + self.scheduler_params = scheduler_params + + +class AbsCoreModel(nn.Module): + """Trainable model that consists of multiple network components. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__() + self.component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) + if optim_option is None: + self.optimizer = None + self.scheduler = None + self.eval() + for param in self.parameters(): + param.requires_grad = False + else: + if isinstance(optim_option, dict): + self.optimizer, self.scheduler = {}, {} + for name, opt in optim_option.items(): + self.optimizer[name] = opt.optim_cls(self.component[name].parameters(), **opt.optim_params) + if opt.scheduler_cls: + self.scheduler[name] = opt.scheduler_cls(self.optimizer[name], **opt.scheduler_params) + else: + self.optimizer = optim_option.optim_cls(self.parameters(), **optim_option.optim_params) + if optim_option.scheduler_cls: + self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) + + if device is None: + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + else: + self.device = torch.device(device) + self.to(self.device) + + @property + def trainable(self) -> bool: + """Return True if at least one optimizer is registered.""" + return self.optimizer is not None + + @abstractmethod + def forward(self, *args, **kwargs): + raise NotImplementedError + + def get_gradients(self, loss: torch.tensor): + """Compute gradients from a loss """ + if self.optimizer is None: + raise MissingOptimizer("No optimizer registered to the model") + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.zero_grad() + else: + self.optimizer.zero_grad() + + # Obtain gradients through back-propagation + loss.backward() + + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad_dict_list: List[Dict[str, float]]): + avg_grad_dict = { + param_name: torch.mean(torch.stack([grad_dict[param_name] for grad_dict in grad_dict_list]), dim=0) + for param_name in grad_dict_list[0] + } + for name, param in self.named_parameters(): + param.grad = avg_grad_dict[name] + + # Apply gradients + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.step() + else: + self.optimizer.step() + + def step(self, loss): + """Use the loss to back-propagate gradients and apply them to the underlying parameters. + + Args: + loss: Result of a computation graph that involves the underlying parameters. + """ + if self.optimizer is None: + raise MissingOptimizer("No optimizer registered to the model") + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.zero_grad() + else: + self.optimizer.zero_grad() + + # Obtain gradients through back-propagation + loss.backward() + + # Apply gradients + if isinstance(self.optimizer, dict): + for optimizer in self.optimizer.values(): + optimizer.step() + else: + self.optimizer.step() + + def update_learning_rate(self, component_name: Union[str, List[str]] = None): + if not isinstance(self.scheduler, dict): + self.scheduler.step() + elif isinstance(component_name, str): + if component_name not in self.scheduler: + raise KeyError(f"Component {component_name} does not have a learning rate scheduler") + self.scheduler[component_name].step() + elif isinstance(component_name, list): + for key in component_name: + if key not in self.scheduler: + raise KeyError(f"Component {key} does not have a learning rate scheduler") + self.scheduler[key].step() + else: + for sch in self.scheduler.values(): + sch.step() + + def copy(self, with_optimizer: bool = False, device: str = None): + """Return a deep copy of the instance; + + Args: + with_opimizer (bool): If True, the registered optimizers will also be deep copied. + Defaults to False. + device (str): The device the copied instance should be placed on. Defaults to None, + in which case the copied instance will be placed on the same device as the instance itself. + """ + model_copy = clone(self) + if not with_optimizer: + model_copy.optimizer = None + model_copy.scheduler = None + + device = self.device if device is None else torch.device(device) + model_copy.to(device) + + return model_copy diff --git a/maro/rl/modeling/fc_block.py b/maro/rl/modeling/fc_block.py new file mode 100644 index 000000000..b2f03af5d --- /dev/null +++ b/maro/rl/modeling/fc_block.py @@ -0,0 +1,115 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import OrderedDict +from typing import List + +import torch +import torch.nn as nn + +from maro.rl.utils import get_torch_activation_cls + + +class FullyConnectedBlock(nn.Module): + """Fully connected network with optional batch normalization, activation and dropout components. + + Args: + name (str): Network name. + input_dim (int): Network input dimension. + output_dim (int): Network output dimension. + hidden_dims ([int]): Dimensions of hidden layers. Its length is the number of hidden layers. + activation: A string indicatinfg an activation class provided by ``torch.nn`` or a custom activation class. + If it is a string, it must be a key in ``TORCH_ACTIVATION``. If None, there will be no activation. + Defaults to "relu". + head (bool): If true, this block will be the top block of the full model and the top layer of this block + will be the final output layer. Defaults to False. + softmax (bool): If true, the output of the net will be a softmax transformation of the top layer's + output. Defaults to False. + batch_norm (bool): If true, batch normalization will be performed at each layer. + skip_connection (bool): If true, a skip connection will be built between the bottom (input) layer and + top (output) layer. Defaults to False. + dropout_p (float): Dropout probability. Defaults to None, in which case there is no drop-out. + gradient_threshold (float): Gradient clipping threshold. Defaults to None, in which case not gradient clipping + is performed. + """ + def __init__( + self, + input_dim: int, + output_dim: int, + hidden_dims: List[int], + activation="relu", + head: bool = False, + softmax: bool = False, + batch_norm: bool = False, + skip_connection: bool = False, + dropout_p: float = None, + gradient_threshold: float = None, + name: str = None + ): + super().__init__() + self._input_dim = input_dim + self._hidden_dims = hidden_dims if hidden_dims is not None else [] + self._output_dim = output_dim + + # network features + self._activation = get_torch_activation_cls(activation)() if activation else None + self._head = head + self._softmax = nn.Softmax(dim=1) if softmax else None + self._batch_norm = batch_norm + self._dropout_p = dropout_p + + if skip_connection and input_dim != output_dim: + raise ValueError( + f"input and output dimensions must match if skip connection is enabled, " + f"got {input_dim} and {output_dim}" + ) + + self._skip_connection = skip_connection + + # build the net + dims = [self._input_dim] + self._hidden_dims + layers = [self._build_layer(in_dim, out_dim) for in_dim, out_dim in zip(dims, dims[1:])] + # top layer + layers.append(self._build_layer(dims[-1], self._output_dim, head=self._head)) + + self._net = nn.Sequential(*layers) + + self._gradient_threshold = gradient_threshold + if gradient_threshold is not None: + for param in self._net.parameters(): + param.register_hook(lambda grad: torch.clamp(grad, -gradient_threshold, gradient_threshold)) + + self._name = name + + def forward(self, x): + out = self._net(x) + if self._skip_connection: + out += x + return self._softmax(out) if self._softmax else out + + @property + def name(self): + return self._name + + @property + def input_dim(self): + return self._input_dim + + @property + def output_dim(self): + return self._output_dim + + def _build_layer(self, input_dim, output_dim, head: bool = False): + """Build basic layer. + + BN -> Linear -> Activation -> Dropout + """ + components = [] + if self._batch_norm: + components.append(("batch_norm", nn.BatchNorm1d(input_dim))) + components.append(("linear", nn.Linear(input_dim, output_dim))) + if not head and self._activation is not None: + components.append(("activation", self._activation)) + if not head and self._dropout_p: + components.append(("dropout", nn.Dropout(p=self._dropout_p))) + return nn.Sequential(OrderedDict(components)) diff --git a/maro/rl/modeling/special_types.py b/maro/rl/modeling/special_types.py new file mode 100644 index 000000000..088e0c807 --- /dev/null +++ b/maro/rl/modeling/special_types.py @@ -0,0 +1,265 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import abstractmethod +from typing import Dict, List, Union + +import numpy as np +import torch +from torch import nn +from torch.distributions import Categorical + +from maro.rl.types import State + +from .core_model import AbsCoreModel, OptimOption + + +class DiscreteACNet(AbsCoreModel): + """Model container for the actor-critic architecture for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + + @abstractmethod + def forward(self, states: List[State], actor: bool = True, critic: bool = True) -> tuple: + """Compute action probabilities and values for a state batch. + + The output is a tuple of (action_probs, values), where action probs is a tensor of shape + (batch_size, action_space_size) and values is a tensor of shape (batch_size,). If only one + of these two is needed, the return value for the other one can be set to None. + + Args: + states (List[State]): State batch to compute action probabilities and values for. + actor (bool): If True, the first element of the output will be actin probabilities. Defaults to True. + critic (bool): If True, the second element of the output will be state values. Defaults to True. + """ + raise NotImplementedError + + def get_action(self, states, max_prob: bool = False): + """ + Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value + for each state. + """ + action_probs, values = self.forward(states) + if max_prob: + probs, actions = action_probs.max(dim=1) + return actions, torch.log(probs), values + else: + action_probs = Categorical(action_probs) # (batch_size, action_space_size) + actions = action_probs.sample() + logps = action_probs.log_prob(actions) + return actions, logps, values + + +class DiscretePolicyNet(AbsCoreModel): + """Parameterized policy for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + + @abstractmethod + def forward(self, states) -> torch.tensor: + """Compute action probabilities corresponding to each state in ``states``. + + The output must be a torch tensor with shape (batch_size, action_space_size). + + Args: + states (List[State]): State batch to compute action probabilities for. + """ + raise NotImplementedError + + def get_action(self, states: List[State], max_prob: bool = False): + """ + Given a batch of states, return actions selected based on the probabilities computed by ``forward`` + and the corresponding log probabilities. + """ + action_prob = self.forward(states) # (batch_size, num_actions) + if max_prob: + prob, action = action_prob.max(dim=1) + return action, torch.log(prob) + else: + action_prob = Categorical(action_prob) # (batch_size, action_space_size) + action = action_prob.sample() + log_p = action_prob.log_prob(action) + return action, log_p + + +class DiscreteQNet(AbsCoreModel): + """Q-value model for finite and discrete action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + + @property + @abstractmethod + def num_actions(self): + raise NotImplementedError + + @abstractmethod + def forward(self, states: List[State]) -> torch.tensor: + """Compute the Q-values for all actions as a tensor of shape (batch_size, action_space_size).""" + raise NotImplementedError + + def get_action(self, states: List[State]): + """ + Given Q-values for a batch of states and all actions, return the action index and the corresponding + Q-values for each state. + """ + q_for_all_actions = self.forward(states) # (batch_size, num_actions) + greedy_q, actions = q_for_all_actions.max(dim=1) + return actions.detach(), greedy_q.detach(), q_for_all_actions.shape[1] + + def q_values(self, states, actions: torch.tensor): + """Return the Q-values for a batch of states and actions.""" + if len(actions.shape) == 1: + actions = actions.unsqueeze(dim=1) + q_for_all_actions = self.forward(states) # (batch_size, num_actions) + return q_for_all_actions.gather(1, actions).squeeze(dim=1) + + def soft_update(self, other_model: nn.Module, tau: float): + """Soft-update model parameters using another model. + + Update formulae: param = (1 - tau) * param + tau * other_param. + + Args: + other_model: The model to update the current model with. + tau (float): Soft-update coefficient. + """ + for params, other_params in zip(self.parameters(), other_model.parameters()): + params.data = (1 - tau) * params.data + tau * other_params.data + + +class ContinuousACNet(AbsCoreModel): + """Model container for the actor-critic architecture for continuous action spaces. + + Args: + component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. + optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. + If none, no optimizer will be created for the model which means the model is not trainable. + If it is a OptimOption instance, a single optimizer will be created to jointly optimize all + parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against + the component names and optimizers created for them. Note that it is possible to freeze certain + components while optimizing others by providing a subset of the keys in ``component``. + Defaults toNone. + device (str): Identifier for the torch device. The model instance will be moved to the specified + device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + Defaults to None. + """ + def __init__( + self, + component: Union[nn.Module, Dict[str, nn.Module]], + optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, + device: str = None + ): + super().__init__(component, optim_option=optim_option, device=device) + + def set_action_space( + self, + min_action: Union[float, np.ndarray] = None, + max_action: Union[float, np.ndarray] = None + ): + """Set action clamping bounds. + + Args: + min_action (Union[float, np.ndarray]): Lower bound for action. Actions generated from the model will be + clipped according to this bound. Defaults to None, which means no lower bound. + max_action (Union[float, np.ndarray]): Upper bound for action. Actions generated from the model will be + clipped according to this bound. Defaults to None, which means no upper bound. + """ + if min_action: + assert isinstance(min_action, (float, np.ndarray)), "min_action must be a float or a numpy array" + if max_action: + assert isinstance(max_action, (float, np.ndarray)), "max_action must be a float or a numpy array" + + if isinstance(min_action, np.ndarray) and isinstance(max_action, np.ndarray): + assert len(min_action) == len(max_action), "min_action and max_action should have the same dimension." + + # For torch clamping + self._min_action = torch.from_numpy(min_action) if isinstance(min_action, np.ndarray) else min_action + self._max_action = torch.from_numpy(max_action) if isinstance(max_action, np.ndarray) else max_action + + @abstractmethod + def forward(self, states: List[State], actions=None) -> torch.tensor: + """Compute actions for a batch of states or Q-values for a batch of states and actions. + + Args: + states (List[State]): State batch to compute the Q-values for. + actions: Action batch. If None, the output should be a batch of actions corresponding to + the state batch. Otherwise, the output should be the Q-values for the given states and + actions. Defaults to None. + """ + raise NotImplementedError + + def get_action(self, states: List[State]) -> torch.tensor: + """Compute actions given a batch of states.""" + return torch.clamp(self.forward(states), min=self._min_action, max=self._max_action) + + def value(self, states: List[State]): + """Compute the Q-values for a batch of states using the actions computed from them.""" + return self.forward(states, actions=self.get_action(states)) + + def soft_update(self, other_model: nn.Module, tau: float): + """Soft-update model parameters using another model. + + Update formulae: param = (1 - tau) * param + tau * other_param. + + Args: + other_model: The model to update the current model with. + tau (float): Soft-update coefficient. + """ + for params, other_params in zip(self.parameters(), other_model.parameters()): + params.data = (1 - tau) * params.data + tau * other_params.data diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index d5ae072d4..c9b2d1e18 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -23,6 +23,11 @@ def __init__(self, action, logp: float, value: float): self.value = value +class ACTrajectory(Trajectory): + def __init__(self): + + + class ACBatch(Batch): __slots__ = ["states", "actions", "returns", "advantages", "logps"] diff --git a/maro/rl/types.py b/maro/rl/types.py new file mode 100644 index 000000000..917d2c8e5 --- /dev/null +++ b/maro/rl/types.py @@ -0,0 +1,81 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List + +import numpy as np + + +class State: + """Convenience class to be used in an environment wrapper's post-step processing function. + + Args: + state: Output of the environment wrapper's ``get_state``. + action: Output of the ``AgentWrapper`` that interacts with environment wrapper. + env_action: Output of the environment wrapper's ``to_env_action``. + reward: Output of the environmet wrapper's ``get_reward``. + next_state: The state immediately following ``state``. + info: Output of the environment wrapper's ``get_transition_info``. + """ + + __slots__ = ["input", "meta"] + + def __init__(self, input: np.ndarray, meta=None): + self.input = input + self.meta = meta + + def + + +class Transition: + """Convenience class to be used in an environment wrapper's post-step processing function. + + Args: + state: ``State`` object generated by the environment wrapper's ``get_state``. + action: Output of the ``AgentWrapper`` that interacts with environment wrapper. + env_action: Output of the environment wrapper's ``to_env_action``. + reward: Output of the environmet wrapper's ``get_reward``. + next_state: The state immediately following ``state``. + info: Output of the environment wrapper's ``get_transition_info``. + """ + + __slots__ = ["state", "action", "env_action", "reward", "next_state", "info"] + + def __init__(self, state: State, action, env_action, reward, next_state: State, info): + self.state = state + self.action = action + self.env_action = env_action + self.reward = reward + self.next_state = next_state + self.info = info + + +class Trajectory: + """Sequence of transitions for an agent. + + Args: + states: Sequence of ``State`` objects traversed during simulation. + actions: Sequence of actions taken in response to the states. + rewards: Sequence of rewards received as a result of the actions. + info: Sequence of each transition's auxillary information. + """ + + __slots__ = ["states", "state_meta", "actions", "rewards", "info", "max_len", "terminal"] + + def __init__( + self, + states: np.ndarray, + state_meta, + actions: np.ndarray, + rewards: np.ndarray, + info: list, + max_len: int = 10000, + terminal: bool = True + ): + self.states = states + self.state_meta = state_meta + self.actions = actions + self.rewards = rewards + self.info = info + self.max_len = max_len + self.terminal = terminal From b3a244d2b6e111b935289cfee1ec595b36d06df7 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 26 Aug 2021 09:42:06 +0000 Subject: [PATCH 417/482] more numpy-based optimization in RL toolkit --- maro/rl/learning/env_sampler.py | 325 ++++++++---------------- maro/rl/learning/environment_sampler.py | 202 --------------- maro/rl/policy/ac.py | 168 +++++++----- maro/rl/policy/dqn.py | 116 ++++----- maro/rl/policy/policy.py | 74 ++++-- maro/rl/policy/replay.py | 70 +++-- maro/rl/types.py | 29 --- maro/rl/wrappers/__init__.py | 10 - maro/rl/wrappers/agent_wrapper.py | 69 ----- maro/rl/wrappers/env_wrapper.py | 212 ---------------- 10 files changed, 363 insertions(+), 912 deletions(-) delete mode 100644 maro/rl/learning/environment_sampler.py delete mode 100644 maro/rl/wrappers/__init__.py delete mode 100644 maro/rl/wrappers/agent_wrapper.py delete mode 100644 maro/rl/wrappers/env_wrapper.py diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 5e917d0ba..a758a0142 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -6,11 +6,9 @@ from os import getcwd from typing import Callable, Dict -import numpy as np - from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.policy import AbsPolicy, RLPolicy -from maro.rl.types import Trajectory, Transition +from maro.rl.types import Transition from maro.rl.utils import MsgKey, MsgTag from maro.simulator import Env from maro.utils import Logger @@ -18,11 +16,12 @@ from .common import get_rollout_finish_msg -class AbsEnvWrapper(ABC): - """Environment wrapper that performs scenario-specific processing, transition caching and experience generation. +class AbsEnvSampler(ABC): + """Simulation data collector and policy evaluator. Args: - env (Env): Environment instance. + get_env (Callable[[], Env]): Function to create an ``Env`` instance for collecting training data. The function + should take no parameters and return an environment wrapper instance. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. @@ -34,38 +33,44 @@ class AbsEnvWrapper(ABC): instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition is a ``Transition`` object. For example, this callback can be used to collect various statistics on the simulation. Defaults to None. + + get_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. + agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's + queries to the correct policy. + get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should + take no parameters and return an environment wrapper instance. If this is None, the training environment + wrapper will be used for evaluation in the worker processes. Defaults to None. """ def __init__( self, - env: Env, + get_env: Callable[[], Env], + get_policy_func_dict: Dict[str, AbsPolicy], + agent2policy: Dict[str, str], + get_eval_env: Callable[[], Env] = None, reward_eval_delay: int = 0, - replay_agent_ids: list = None, max_trajectory_len: Dict[str, int] = None, - post_step: Callable = None + post_step: Callable = None, ): - self.env = env - self.reward_eval_delay = reward_eval_delay + self.env = get_env() + self.eval_env = get_eval_env() if get_eval_env else self.env + self.policy_dict = {name: func(name) for name, func in get_policy_func_dict.items()} + self.agent2policy = agent2policy + self.policy = { + agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() + } + self.reward_eval_delay = reward_eval_delay self._post_step = post_step - self.replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids self.max_trajectory_len = max_trajectory_len if max_trajectory_len else defaultdict(lambda: self.env._durations) - self._trajectory_ptr = defaultdict(int) - self._last_trajectory_ptr = defaultdict(int) self._replay_buffer = { - agent_id: { - "states": np.zeros((self.max_trajectory_len[agent_id], self.state_dim), dtype=np.float32), - "actions": np.zeros((self.max_trajectory_len[agent_id], self.action_dim), dtype=np.float32), - "rewards": np.zeros(self.max_trajectory_len[agent_id], dtype=np.float32), - "terminal": np.zeros(self.max_trajectory_len[agent_id], dtype=np.bool), - "info": [None] * self.max_trajectory_len[agent_id] - } - for agent_id in replay_agent_ids + agent_id: policy.Buffer(self.state_dim[agent_id], ) + for agent_id, policy in self.policy.items() if isinstance(policy, RLPolicy) } self._transition_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated self._step_index = None - self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. - self._state = None # the latest extracted state is kept here + self._terminal = False self.tracker = {} # User-defined tracking information is placed here. self.replay = True @@ -93,182 +98,6 @@ def agent_idx_list(self): def summary(self): return self.env.metrics - @property - def state(self): - """The current environmental state.""" - return self._state - - @property - def event(self): - return self._event - - def start(self): - """Generate the initial environmental state at the beginning of a simulation episode.""" - self._step_index = 0 - _, self._event, _ = self.env.step(None) - self._state = self.get_state(self.env.tick) - - @abstractmethod - def get_state(self, tick: int = None) -> dict: - """Compute the state for a given tick. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - - Returns: - A dictionary with (agent ID, state) as key-value pairs. - """ - raise NotImplementedError - - @abstractmethod - def to_env_action(self, action) -> dict: - """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" - raise NotImplementedError - - @abstractmethod - def get_reward(self, actions: list, tick: int = None): - """Evaluate the reward for an action. - - Args: - tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in - ``actions`` must be an Action object defined for the environment in question. The tick may - be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). - Defaults to None. - - Returns: - A dictionary with (agent ID, reward) as key-value pairs. - """ - raise NotImplementedError - - def get_transition_info(self, tick: int = None): - """Get additional info for a transition. - - The returned transition info will be stored in the experience manager alongside states, actions, rewards. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - - Returns: - A dictionary with (agent ID, transition_info) as key-value pairs. - - """ - pass - - def step(self, action_by_agent: dict): - """Wrapper for env.step(). - - The new transition is stored in the replay buffer or cached in a separate data structure if the - reward cannot be determined yet due to a non-zero ``reward_eval_delay``. - """ - self._step_index += 1 - env_action = self.to_env_action(action_by_agent) - - self._transition_cache.append(( - self._state, - action_by_agent, - env_action, - self.get_transition_info(), - self.env.tick - )) - - _, self._event, done = self.env.step(env_action) - - if not done: - self._state = self.get_state(self.env.tick) # current env state - else: - self._state = None - - """ - If this is the final step, evaluate rewards for all remaining events except the last. - Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ - while ( - self._transition_cache and - (done or self.env.tick - self._transition_cache[0][-1] >= self.reward_eval_delay) - ): - state, action, env_action, info, tick = self._transition_cache.popleft() - reward = self.get_reward(env_action, tick=tick) - if self._post_step: - next_state = self._transition_cache[0][0] if self._transition_cache else None - transition = Transition(state, action, env_action, reward, next_state, info) - # put things you want to track in the tracker attribute - self._post_step(self.env, self.tracker, transition) - - if self.replay: - for agent_id, agent_state in state.items(): - if agent_id in self._replay_buffer: - buf, ptr = self._replay_buffer[agent_id], self._trajectory_ptr[agent_id] - buf["states"][ptr] = agent_state - buf["actions"][ptr] = action[agent_id] - buf["rewards"][ptr] = reward[agent_id] - buf["terminal"][ptr] = self._transition_cache.empty() and not self._state - if info: - buf["info"][ptr] = info[agent_id] - # increment pointer - self._trajectory_ptr[agent_id] += 1 - if self._trajectory_ptr[agent_id] == self.max_trajectory_len[agent_id]: - self._trajectory_ptr[agent_id] = 0 - - def get_trajectory(self): - """Get agent trajectories from the replay buffer and clear it in the process.""" - trajectory = {} - for agent_id, buf in self._replay_buffer.items(): - traj_slice = slice(self._last_trajectory_ptr[agent_id], self._trajectory_ptr[agent_id]) - states = buf["states"][traj_slice] - actions = buf["actions"][traj_slice] - rewards = buf["rewards"][traj_slice] - terminal_flags = buf["terminal"][traj_slice] - info = buf["info"][traj_slice] - # else: - # traj_slice_minus_1 = slice(self._last_trajectory_ptr[agent_id], self._trajectory_ptr[agent_id] - 1) - # rewards = buf["rewards"][traj_slice_minus_1] - # info = buf["info"][traj_slice_minus_1] - - self._last_trajectory_ptr[agent_id] = self._trajectory_ptr[agent_id] - trajectory[agent_id] = Trajectory(states, , actions, rewards, info, terminal=not self._state) - - return trajectory - - def reset(self): - self.env.reset() - self._state = None - self._transition_cache.clear() - self.tracker.clear() - self._trajectory_ptr = defaultdict(int) - self._last_trajectory_ptr = defaultdict(int) - - -class EnvSampler: - """Simulation data collector and policy evaluator. - - Args: - get_env_wrapper (Callable): Function to create an environment wrapper for collecting training data. The function - should take no parameters and return an environment wrapper instance. - get_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. - agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's - queries to the correct policy. - get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should - take no parameters and return an environment wrapper instance. If this is None, the training environment - wrapper will be used for evaluation in the worker processes. Defaults to None. - """ - def __init__( - self, - get_env_wrapper: Callable[[], AbsEnvWrapper], - get_policy_func_dict: Dict[str, AbsPolicy], - agent2policy: Dict[str, str], - get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None - ): - self.env = get_env_wrapper() - self.eval_env = get_env_wrapper() if get_eval_env_wrapper else self.env - self.policy_dict = {name: func(name) for name, func in get_policy_func_dict.items()} - self.agent2policy = agent2policy - self.policy = { - agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() - } - def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): # set policy states if policy_state_dict: @@ -285,28 +114,61 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio if hasattr(policy, "exploration_step"): policy.exploration_step() - if self.env.state is None: - self.env.reset() - self.env.replay = True - self.env.start() # get initial state + if self._terminal: + # get initial state + self._step_index = 0 + _, self._event, _ = self.env.step(None) + self._state = self.get_state(self.env.tick) - starting_step_index = self.env.step_index + 1 + starting_step_index = self._step_index + 1 steps_to_go = float("inf") if num_steps == -1 else num_steps - while self.env.state and steps_to_go > 0: - self.env.step({agent: self.policy[agent].choose_action(st) for agent, st in self.env.state.items()}) + while not self._terminal and steps_to_go > 0: + state_by_agent = self.get_state() + action_by_agent = {agent: self.policy[agent].choose_action(st) for agent, st in state_by_agent.items()} + env_action = self.to_env_action(action_by_agent) + self._transition_cache.append(( + state_by_agent, + action_by_agent, + env_action, + self.env.tick + )) + _, self._event, self._terminal = self.env.step(env_action) + self._step_index += 1 steps_to_go -= 1 + """ + If this is the final step, evaluate rewards for all remaining events except the last. + Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. + """ + while ( + self._transition_cache and + (self._terminal or self.env.tick - self._transition_cache[0][-1] >= self.reward_eval_delay) + ): + state_by_agent, action_by_agent, env_action, tick = self._transition_cache.popleft() + reward_by_agent = self.get_reward(env_action, tick=tick) + if self._post_step: + next_state = self._transition_cache[0][0] if self._transition_cache else None + transition = Transition(state_by_agent, action_by_agent, env_action, reward_by_agent, next_state) + # put things you want to track in the tracker attribute + self._post_step(self.env, self.tracker, transition) + + for agent_id, state in state_by_agent.items(): + if agent_id in self._replay_buffer: + self._replay_buffer[agent_id].store( + state, action_by_agent[agent_id], reward_by_agent[agent_id], + terminal=not self._transition_cache and self._terminal, + ) rollout_info = {} - for agent_id, traj in self.env.get_trajectory().items(): + for agent_id, buf in self._replay_buffer.items(): if isinstance(self.policy[agent_id], RLPolicy): policy_name = self.agent2policy[agent_id] - rollout_info[policy_name] = self.policy_dict[policy_name].get_rollout_info(traj) + rollout_info[policy_name] = self.policy_dict[policy_name].get_rollout_info(buf.get()) return { "rollout_info": rollout_info, - "step_range": (starting_step_index, self.env.step_index), - "tracker": self.env.tracker, - "end_of_episode": not self.env.state, + "step_range": (starting_step_index, self._step_index), + "tracker": self.tracker, + "end_of_episode": not self._terminal, "exploration_params": { name: policy.exploration_params for name, policy in self.policy_dict.items() if isinstance(policy, RLPolicy) @@ -331,6 +193,45 @@ def test(self, policy_state_dict: dict = None): return self.eval_env.tracker + @abstractmethod + def get_state(self, tick: int = None) -> dict: + """Compute the state for a given tick. + + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + + Returns: + A dictionary with (agent ID, state) as key-value pairs. + """ + raise NotImplementedError + + @abstractmethod + def to_env_action(self, action) -> dict: + """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" + raise NotImplementedError + + @abstractmethod + def get_reward(self, actions: list, tick: int = None): + """Evaluate the reward for an action. + + Args: + tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in + ``actions`` must be an Action object defined for the environment in question. The tick may + be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). + Defaults to None. + + Returns: + A dictionary with (agent ID, reward) as key-value pairs. + """ + raise NotImplementedError + + def reset(self): + self.env.reset() + self._state = None + self._transition_cache.clear() + self.tracker.clear() + def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = getcwd()): """Roll-out worker process that can be launched on separate computation nodes. diff --git a/maro/rl/learning/environment_sampler.py b/maro/rl/learning/environment_sampler.py deleted file mode 100644 index 8dbad9747..000000000 --- a/maro/rl/learning/environment_sampler.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from os import getcwd -from typing import Callable - -from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.utils import MsgKey, MsgTag -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper -from maro.utils import Logger - -from .common import get_rollout_finish_msg - - -class EnvironmentSampler: - """Simulation data collector and policy evaluator. - - Args: - get_env_wrapper (Callable): Function to create an environment wrapper for collecting training data. The function - should take no parameters and return an environment wrapper instance. - get_agent_wrapper (Callable): Function to create an agent wrapper that interacts with the environment wrapper. - The function should take no parameters and return a ``AgentWrapper`` instance. - get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should - take no parameters and return an environment wrapper instance. If this is None, the training environment - wrapper will be used for evaluation in the worker processes. Defaults to None. - """ - def __init__( - self, - get_env_wrapper: Callable[[], AbsEnvWrapper], - get_agent_wrapper: Callable[[], AgentWrapper], - get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None - ): - self.env = get_env_wrapper() - self.eval_env = get_env_wrapper() if get_eval_env_wrapper else self.env - self.agent = get_agent_wrapper() - - def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): - # set policy states - if policy_state_dict: - self.agent.set_policy_states(policy_state_dict) - - # set exploration parameters - self.agent.explore() - if exploration_step: - self.agent.exploration_step() - - if self.env.state is None: - self.env.reset() - self.env.replay = True - self.env.start() # get initial state - - starting_step_index = self.env.step_index + 1 - steps_to_go = float("inf") if num_steps == -1 else num_steps - while self.env.state and steps_to_go > 0: - action = self.agent.choose_action(self.env.state) - self.env.step(action) - steps_to_go -= 1 - - return { - "rollout_info": self.agent.get_rollout_info(self.env.get_trajectory()), - "step_range": (starting_step_index, self.env.step_index), - "tracker": self.env.tracker, - "end_of_episode": not self.env.state, - "exploration_params": self.agent.exploration_params - } - - def test(self, policy_state_dict: dict = None): - if policy_state_dict: - self.agent.set_policy_states(policy_state_dict) - self.agent.exploit() - self.eval_env.reset() - self.eval_env.replay = False - self.eval_env.start() # get initial state - while self.eval_env.state: - action = self.agent.choose_action(self.eval_env.state) - self.eval_env.step(action) - - return self.eval_env.tracker - - def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = getcwd()): - """Roll-out worker process that can be launched on separate computation nodes. - - Args: - group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager - that manages them. - worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". - This is used for bookkeeping by the parent manager. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - proxy = Proxy( - group, "rollout_worker", {"rollout_manager": 1}, component_name=f"ROLLOUT_WORKER.{index}", **proxy_kwargs - ) - logger = Logger(proxy.name, dump_folder=log_dir) - - """ - The event loop handles 3 types of messages from the roll-out manager: - 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps - and the collected experiences will be sent back to the roll-out manager; - 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire - duration of the evaluation environment. - 3) EXIT, upon which it will break out of the event loop and the process will terminate. - - """ - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.SAMPLE: - ep = msg.body[MsgKey.EPISODE] - result = self.sample( - policy_state_dict=msg.body[MsgKey.POLICY_STATE], - num_steps=msg.body[MsgKey.NUM_STEPS], - exploration_step=msg.body[MsgKey.EXPLORATION_STEP] - ) - logger.info( - get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) - ) - return_info = { - MsgKey.EPISODE: ep, - MsgKey.SEGMENT: msg.body[MsgKey.SEGMENT], - MsgKey.VERSION: msg.body[MsgKey.VERSION], - MsgKey.ROLLOUT_INFO: result["rollout_info"], - MsgKey.STEP_RANGE: result["step_range"], - MsgKey.TRACKER: result["tracker"], - MsgKey.END_OF_EPISODE: result["end_of_episode"] - } - proxy.reply(msg, tag=MsgTag.SAMPLE_DONE, body=return_info) - elif msg.tag == MsgTag.TEST: - tracker = self.test(msg.body[MsgKey.POLICY_STATE]) - return_info = {MsgKey.TRACKER: tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} - logger.info("Testing complete") - proxy.reply(msg, tag=MsgTag.TEST_DONE, body=return_info) - - def actor( - self, - group: str, - index: int, - num_episodes: int, - num_steps: int = -1, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - """Controller for single-threaded learning workflows. - - Args: - group (str): Group name for the cluster that includes the server and all actors. - index (int): Integer actor index. The actor's ID in the cluster will be "ACTOR.{actor_idx}". - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on how the implementation of the roll-out manager. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in - which case the roll-out will be executed until the end of the environment. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - peers = {"policy_server": 1} - proxy = Proxy(group, "actor", peers, component_name=f"ACTOR.{index}", **proxy_kwargs) - policy_server_address = proxy.peers["policy_server"][0] - logger = Logger(proxy.name, dump_folder=log_dir) - - # get initial policy states from the policy manager - msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, policy_server_address) - reply = proxy.send(msg)[0] - policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] - - # main loop - for ep in range(1, num_episodes + 1): - exploration_step = True - while True: - result = self.sample( - policy_state_dict=policy_state_dict, num_steps=num_steps, exploration_step=exploration_step - ) - logger.info( - get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) - ) - # Send roll-out info to policy server for learning - reply = proxy.send( - SessionMessage( - MsgTag.SAMPLE_DONE, proxy.name, policy_server_address, - body={MsgKey.ROLLOUT_INFO: result["rollout_info"], MsgKey.VERSION: policy_version} - ) - )[0] - policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] - if result["end_of_episode"]: - break - - exploration_step = False - - # tell the policy server I'm all done. - proxy.isend( - SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION) - ) - proxy.close() diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index c9b2d1e18..a96f2a957 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -1,61 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import List, Union +from typing import List import numpy as np import torch from torch.distributions import Categorical -from maro.rl.types import DiscreteACNet, Trajectory +from maro.rl.types import DiscreteACNet from maro.rl.utils import discount_cumsum, get_torch_loss_cls -from .policy import Batch, LossInfo, RLPolicy - - -class ACActionInfo: - - __slots__ = ["action", "logp", "value"] - - def __init__(self, action, logp: float, value: float): - self.action = action - self.logp = logp - self.value = value - - -class ACTrajectory(Trajectory): - def __init__(self): - - - -class ACBatch(Batch): - - __slots__ = ["states", "actions", "returns", "advantages", "logps"] - - def __init__(self, states, actions: np.ndarray, returns: np.ndarray, advantages: np.ndarray, logps: np.ndarray): - super().__init__() - self.states = states - self.actions = actions - self.returns = returns - self.advantages = advantages - self.logps = logps - - @property - def size(self): - return len(self.states) - - -class ACLossInfo(LossInfo): - - __slots__ = ["actor_loss", "critic_loss", "entropy"] - - def __init__(self, loss, actor_loss, critic_loss, entropy, grad=None): - super().__init__(loss, grad) - self.loss = loss - self.actor_loss = actor_loss - self.critic_loss = critic_loss - self.entropy = entropy - self.grad = grad +from .policy import RLPolicy class ActorCritic(RLPolicy): @@ -81,6 +36,82 @@ class ActorCritic(RLPolicy): in which case the actor loss is calculated using the usual policy gradient theorem. """ + class Buffer(RLPolicy.Buffer): + """Sequence of transitions for an agent. + + Args: + states: Sequence of ``State`` objects traversed during simulation. + actions: Sequence of actions taken in response to the states. + rewards: Sequence of rewards received as a result of the actions. + info: Sequence of each transition's auxillary information. + """ + def __init__(self, state_dim, action_dim: int = 1, max_len: int = 10000): + super.__init__(state_dim, action_dim=1, max_len=max_len) + self.logps = np.zeros(max_len, dtype=np.float32) + self.values = np.zeros(max_len, dtype=np.float32) + + def store(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): + self.states[self._ptr] = state + self.actions[self._ptr] = action["action"] + self.logps[self._ptr] = action["logp"] + self.values[self._ptr] = action["value"] + self.rewards[self._ptr] = reward + self.terminal[self._ptr] = terminal + # increment pointer + self._ptr += 1 + if self._ptr == self.max_len: + self._ptr = 0 + + def get(self): + traj_slice = slice(self._last_ptr, self._ptr) + self._last_ptr = self._ptr + return { + "states": self.states[traj_slice], + "actions": self.actions[traj_slice], + "logps": self.logps[traj_slice], + "values": self.values[traj_slice], + "rewards": self.rewards[traj_slice], + "terminal": self.terminal[self._ptr - 1] + } + + + class Batch(RLPolicy.Batch): + + __slots__ = ["states", "actions", "returns", "advantages", "logps"] + + def __init__( + self, + states: np.ndarray, + actions: np.ndarray, + returns: np.ndarray, + advantages: np.ndarray, + logps: np.ndarray + ): + super().__init__() + self.states = states + self.actions = actions + self.returns = returns + self.advantages = advantages + self.logps = logps + + @property + def size(self): + return len(self.states) + + + class LossInfo(RLPolicy.LossInfo): + + __slots__ = ["actor_loss", "critic_loss", "entropy"] + + def __init__(self, loss, actor_loss, critic_loss, entropy, grad=None): + super().__init__(loss, grad) + self.loss = loss + self.actor_loss = actor_loss + self.critic_loss = critic_loss + self.entropy = entropy + self.grad = grad + + def __init__( self, name: str, @@ -112,42 +143,47 @@ def __init__( self.lam = lam self._get_loss_on_rollout_finish = get_loss_on_rollout_finish - def choose_action(self, states) -> Union[ACActionInfo, List[ACActionInfo]]: + def choose_action(self, states): """Return actions and log probabilities for given states.""" self.ac_net.eval() with torch.no_grad(): actions, logps, values = self.ac_net.get_action(states) actions, logps, values = actions.cpu().numpy(), logps.cpu().numpy(), values.cpu().numpy() if len(actions) == 1: - return ACActionInfo(actions[0], logps[0], values[0]) + return {"action": actions[0], "logp": logps[0], "value": values[0]} else: - return [ACActionInfo(action, logp, value) for action, logp, value in zip(actions, logps, values)] + return [ + {"action": action, "logp": logp, "value": value} for action, logp, value in zip(actions, logps, values) + ] - def get_rollout_info(self, trajectory: Trajectory): + def get_rollout_info(self, trajectory: dict): if self._get_loss_on_rollout_finish: return self.get_batch_loss(self._preprocess(trajectory), explicit_grad=True) else: return trajectory - def _preprocess(self, trajectory: Trajectory): - if trajectory.actions[-1]: - values = np.array([action_info.value for action_info in trajectory.actions]) - rewards = np.append(trajectory.rewards, trajectory.actions[-1].value) + def _preprocess(self, trajectory: dict): + if trajectory["terminal"]: + states = trajectory["states"] + actions = trajectory["actions"] + logps = trajectory["logps"] + values = np.append(trajectory["values"], .0) + rewards = np.append(trajectory["rewards"], .0) else: - values = np.append([action_info.value for action_info in trajectory.actions[:-1]], .0) - rewards = np.append(trajectory.rewards, .0) - - actions = np.array([action_info.action for action_info in trajectory.actions[:-1]]) - logps = np.array([action_info.logp for action_info in trajectory.actions[:-1]], dtype=np.float32) + states = trajectory["states"][:-1] + actions = trajectory["actions"][:-1] + logps = trajectory["logps"][:-1] + values = trajectory["values"] + rewards = np.append(trajectory["rewards"][:-1], trajectory["values"][-1]) # Generalized advantage estimation using TD(Lambda) deltas = rewards[:-1] + self.reward_discount * values[1:] - values[:-1] advantages = discount_cumsum(deltas, self.reward_discount * self.lam) # Returns rewards-to-go, to be targets for the value function returns = discount_cumsum(rewards, self.reward_discount)[:-1] - return ACBatch(trajectory.states[:-1], actions, returns, advantages, logps) + return self.Batch(states, actions, returns, advantages, logps) - def get_batch_loss(self, batch: ACBatch, explicit_grad: bool = False) -> ACLossInfo: + def get_batch_loss(self, batch: Batch, explicit_grad: bool = False): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() actions = torch.from_numpy(batch.actions).to(self.device) @@ -180,13 +216,13 @@ def get_batch_loss(self, batch: ACBatch, explicit_grad: bool = False) -> ACLossI # total loss loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy grad = self.ac_net.get_gradients(loss) if explicit_grad else None - return ACLossInfo(actor_loss, critic_loss, entropy, loss, grad=grad) + return self.LossInfo(actor_loss, critic_loss, entropy, loss, grad=grad) - def update_with_multi_loss_info(self, loss_info_list: List[ACLossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[LossInfo]): """Apply gradients to the underlying parameterized model.""" self.ac_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) - def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): + def learn_from_multi_trajectories(self, trajectories: List[dict]): if self.remote: # TODO: distributed grad computation pass diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index 836c86bab..0f07b04ea 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -7,56 +7,13 @@ import torch from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration -from maro.rl.types import DiscreteQNet, Trajectory +from maro.rl.modeling import DiscreteQNet from maro.utils.exception.rl_toolkit_exception import InvalidExperience -from .policy import Batch, LossInfo, RLPolicy +from .policy import RLPolicy from .replay import ReplayMemory -class DQNBatch(Batch): - """Wrapper for a set of experiences. - - An experience consists of state, action, reward, next state. - """ - __slots__ = ["states", "actions", "rewards", "next_states"] - - def __init__( - self, - states: list, - actions: list, - rewards: list, - next_states: list, - indexes: list = None, - is_weights: list = None - ): - if not len(states) == len(actions) == len(rewards) == len(next_states): - raise InvalidExperience("values of contents should consist of lists of the same length") - super().__init__() - self.states = states - self.actions = actions - self.rewards = rewards - self.next_states = next_states - self.is_weights = is_weights - self.indexes = indexes - - @property - def size(self): - return len(self.states) - - -class DQNLossInfo(LossInfo): - - __slots__ = ["td_errors", "indexes"] - - def __init__(self, loss, td_errors, indexes, grad=None): - super().__init__(loss, grad) - self.loss = loss - self.td_errors = td_errors - self.indexes = indexes - self.grad = grad - - class PrioritizedExperienceReplay: """Prioritized Experience Replay (PER). @@ -183,6 +140,52 @@ class DQN(RLPolicy): overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. """ + + class Batch(RLPolicy.Batch): + """Wrapper for a set of experiences. + + An experience consists of state, action, reward, next state. + """ + __slots__ = ["states", "actions", "rewards", "next_states", "terminal"] + + def __init__( + self, + states: np.ndarray, + actions: np.ndarray, + rewards: np.ndarray, + next_states: np.ndarray, + terminal: np.ndarray, + indexes: list = None, + is_weights: list = None + ): + if not len(states) == len(actions) == len(rewards) == len(next_states) == len(terminal): + raise InvalidExperience("values of contents should consist of lists of the same length") + super().__init__() + self.states = states + self.actions = actions + self.rewards = rewards + self.next_states = next_states + self.terminal = terminal + self.is_weights = is_weights + self.indexes = indexes + + @property + def size(self): + return len(self.states) + + + class LossInfo(RLPolicy.LossInfo): + + __slots__ = ["td_errors", "indexes"] + + def __init__(self, loss, td_errors, indexes, grad=None): + super().__init__(loss, grad) + self.loss = loss + self.td_errors = td_errors + self.indexes = indexes + self.grad = grad + + def __init__( self, name: str, @@ -218,7 +221,7 @@ def __init__( self.soft_update_coeff = soft_update_coeff self.double = double - self._replay_memory = ReplayMemory(DQNBatch, replay_memory_capacity, random_overwrite=random_overwrite) + self._replay_memory = ReplayMemory(self.Batch, replay_memory_capacity, random_overwrite=random_overwrite) self.prioritized_replay = prioritized_replay_kwargs is not None if self.prioritized_replay: self._per = PrioritizedExperienceReplay(self._replay_memory, **prioritized_replay_kwargs) @@ -241,23 +244,20 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def _put_in_replay_memory(self, traj: Trajectory): - if traj.states[-1]: - batch = DQNBatch(traj.states[:-1], traj.actions[:-1], traj.rewards, traj.states[1:]) - else: - batch = DQNBatch(traj.states[:-2], traj.actions[:-2], traj.rewards[:-1], traj.states[1:-1]) + def _put_in_replay_memory(self, traj: dict): + batch = self.Batch(traj["states"][:-1], traj["actions"][:-1], traj["rewards"], traj["states"][1:]) indexes = self._replay_memory.put(batch) if self.prioritized_replay: self._per.set_max_priority(indexes) - def _sample(self) -> DQNBatch: + def _sample(self): if self.prioritized_replay: indexes, is_weights = self._per.sample() else: indexes = np.random.choice(self._replay_memory.size) is_weights = None - return DQNBatch( + return self.Batch( [self._replay_memory.data["states"][idx] for idx in indexes], [self._replay_memory.data["actions"][idx] for idx in indexes], [self._replay_memory.data["rewards"][idx] for idx in indexes], @@ -266,7 +266,7 @@ def _sample(self) -> DQNBatch: is_weights=is_weights ) - def get_batch_loss(self, batch: DQNBatch, explicit_grad: bool = False): + def get_batch_loss(self, batch: Batch, explicit_grad: bool = False): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() states, next_states = batch.states, batch.next_states @@ -281,7 +281,7 @@ def get_batch_loss(self, batch: DQNBatch, explicit_grad: bool = False): else: next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) - target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) + target_q_values = (rewards + self.reward_discount * (1 - ) * next_q_values).detach() # (N,) # gradient step q_values = self.q_net.q_values(states, actions) @@ -293,9 +293,9 @@ def get_batch_loss(self, batch: DQNBatch, explicit_grad: bool = False): loss = self._loss_func(q_values, target_q_values) grad = self.q_net.get_gradients(loss) if explicit_grad else None - return DQNLossInfo(loss, td_errors, batch.indexes, grad=grad) + return self.LossInfo(loss, td_errors, batch.indexes, grad=grad) - def update_with_multi_loss_info(self, loss_info_list: List[DQNLossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[LossInfo]): if self.prioritized_replay: for loss_info in loss_info_list: self._per.update(loss_info.indexes, loss_info.td_errors) @@ -305,7 +305,7 @@ def update_with_multi_loss_info(self, loss_info_list: List[DQNLossInfo]): if self._q_net_version - self._target_q_net_version == self.update_target_every: self._update_target() - def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): + def learn_from_multi_trajectories(self, trajectories: List[dict]): for traj in trajectories: self._put_in_replay_memory(traj) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 7d44735bb..888625939 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from typing import List -from maro.rl.types import Trajectory +import numpy as np class AbsPolicy(ABC): @@ -36,20 +36,6 @@ def choose_action(self, state): return None -class Batch: - def __init__(self): - pass - - -class LossInfo: - - __slots__ = ["loss", "grad"] - - def __init__(self, loss, grad): - self.loss = loss - self.grad = grad - - class RLPolicy(AbsPolicy): """Policy that can update itself using simulation experiences. @@ -58,6 +44,64 @@ class RLPolicy(AbsPolicy): Args: name (str): Name of the policy. """ + + class Buffer: + """Sequence of transitions for an agent. + + Args: + states: Sequence of ``State`` objects traversed during simulation. + actions: Sequence of actions taken in response to the states. + rewards: Sequence of rewards received as a result of the actions. + info: Sequence of each transition's auxillary information. + """ + def __init__(self, state_dim: int, action_dim: int = 1, max_len: int = 10000): + self.states = np.zeros((max_len, state_dim), dtype=np.float32) + if action_dim == 1: + self.actions = np.zeros(max_len, dtype=np.float32) + else: + self.actions = np.zeros((max_len, action_dim), dtype=np.float32) + self.rewards = np.zeros(max_len, dtype=np.float32) + self.terminal = np.zeros(max_len, dtype=np.bool) + self.max_len = max_len + + self._ptr = 0 + self._last_ptr = 0 + + def store(self, state: np.ndarray, action, reward: float, terminal: bool = False): + self.states[self._ptr] = state + self.actions[self._ptr] = action + self.rewards[self._ptr] = reward + self.terminal[self._ptr] = terminal + # increment pointer + self._ptr += 1 + if self._ptr == self.max_len: + self._ptr = 0 + + def get(self): + traj_slice = slice(self._last_ptr, self._ptr) + self._last_ptr = self._ptr + return { + "states": self.states[traj_slice], + "actions": self.actions[traj_slice], + "rewards": self.rewards[traj_slice], + "terminal": self.terminal[traj_slice], + } + + + class Batch: + def __init__(self): + pass + + + class LossInfo: + + __slots__ = ["loss", "grad"] + + def __init__(self, loss, grad): + self.loss = loss + self.grad = grad + + def __init__(self, name: str, remote: bool = False): super().__init__(name) self.remote = remote diff --git a/maro/rl/policy/replay.py b/maro/rl/policy/replay.py index 4137a2d61..0ef9d9535 100644 --- a/maro/rl/policy/replay.py +++ b/maro/rl/policy/replay.py @@ -16,15 +16,21 @@ class ReplayMemory: overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. """ - def __init__(self, batch_type, capacity: int, random_overwrite: bool = False): + def __init__(self, capacity: int, state_dim: int, action_dim: int = 1, random_overwrite: bool = False): super().__init__() - self._batch_type = batch_type + self._state_dim = state_dim + self._action_dim = action_dim self._capacity = capacity self._random_overwrite = random_overwrite - self._keys = batch_type.__slots__ - self.data = {key: [None] * self._capacity for key in self._keys} - self._size = 0 - self._index = 0 + self.states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) + if action_dim > 1: + self.actions = np.zeros((self._capacity, self._action_dim), dtype=np.float32) + else: + self.actions = np.zeros(self._capacity, dtype=np.float32) + self.rewards = np.zeros(self._capacity, dtype=np.float32) + self.next_states = np.zeros(self._capacity, dtype=np.float32) + self.terminal = np.zeros(self._capacity, dtype=np.bool) + self._ptr = 0 @property def capacity(self): @@ -39,44 +45,30 @@ def random_overwrite(self): @property def size(self): """Current number of experiences stored.""" - return self._size - - @property - def keys(self): - """Keys as specified by ``batch_type``.""" - return self._keys + return self._ptr - def put(self, batch): - """Put a experience set in the store. - Args: - experience_set (ExperienceSet): Experience set to be put in the store. - """ - assert isinstance(batch, self._batch_type) - added_size = batch.size - if added_size > self._capacity: + def put(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): + """Put SARS and terminal flags in the memory.""" + assert len(states) == len(actions) == len(rewards) == len(next_states) + added = len(states) + if added > self._capacity: raise ValueError("size of added items should not exceed the capacity.") - num_experiences = self._size + added_size - num_overwrites = num_experiences - self._capacity - if num_overwrites <= 0: - indexes = list(range(self._size, num_experiences)) + if self._ptr + added <= self._capacity: + indexes = np.arange(self._ptr, self._ptr + added) # follow the overwrite rule set at init - elif self._random_overwrite: - random_indexes = np.random.choice(self._size, size=num_overwrites, replace=False) - indexes = list(range(self._size, self._capacity)) + list(random_indexes) else: - # using the negative index convention for convenience - start_index = self._size - self._capacity - indexes = list(range(start_index, start_index + added_size)) + overwrites = self._ptr + added - self._capacity + indexes = np.concatenate([ + np.arange(self._ptr, self._capacity), + np.random.choice(self._ptr, size=overwrites, replace=False) if self._random_overwrite + else np.arange(overwrites) + ]) - for key in self.data: - for idx, val in zip(indexes, getattr(batch, key)): - self.data[key][idx] = val + self.states[indexes] = states + self.actions[indexes] = actions + self.rewards[indexes] = rewards + self.next_states[indexes] = next_states - self._size = min(self._capacity, num_experiences) + self._ptr = min(self._ptr + added, self._capacity) return indexes - - def clear(self): - """Empty the memory.""" - self.data = {key: [None] * self._capacity for key in self._keys} - self._size = 0 diff --git a/maro/rl/types.py b/maro/rl/types.py index 917d2c8e5..ab6b433b4 100644 --- a/maro/rl/types.py +++ b/maro/rl/types.py @@ -50,32 +50,3 @@ def __init__(self, state: State, action, env_action, reward, next_state: State, self.info = info -class Trajectory: - """Sequence of transitions for an agent. - - Args: - states: Sequence of ``State`` objects traversed during simulation. - actions: Sequence of actions taken in response to the states. - rewards: Sequence of rewards received as a result of the actions. - info: Sequence of each transition's auxillary information. - """ - - __slots__ = ["states", "state_meta", "actions", "rewards", "info", "max_len", "terminal"] - - def __init__( - self, - states: np.ndarray, - state_meta, - actions: np.ndarray, - rewards: np.ndarray, - info: list, - max_len: int = 10000, - terminal: bool = True - ): - self.states = states - self.state_meta = state_meta - self.actions = actions - self.rewards = rewards - self.info = info - self.max_len = max_len - self.terminal = terminal diff --git a/maro/rl/wrappers/__init__.py b/maro/rl/wrappers/__init__.py deleted file mode 100644 index 5b388a0d6..000000000 --- a/maro/rl/wrappers/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .agent_wrapper import AgentWrapper -from .env_wrapper import AbsEnvWrapper, Trajectory, Transition - -__all__ = [ - "AgentWrapper", - "AbsEnvWrapper", "Trajectory", "Transition" -] diff --git a/maro/rl/wrappers/agent_wrapper.py b/maro/rl/wrappers/agent_wrapper.py deleted file mode 100644 index 5caa6d257..000000000 --- a/maro/rl/wrappers/agent_wrapper.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Dict - -from maro.rl.policy import AbsPolicy, RLPolicy - -from .env_wrapper import Trajectory - - -class AgentWrapper: - """Multi-agent wrapper that interacts with an ``EnvWrapper`` with a unified inferface. - - Args: - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. - agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's - queries to the correct policy. - """ - def __init__(self, create_policy_func_dict: Dict[str, AbsPolicy], agent2policy: Dict[str, str]): - self.policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} - self.agent2policy = agent2policy - self.policy = { - agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() - } - - def choose_action(self, state: dict) -> dict: - """Generate an action based on the given state. - - Args: - state (dict): Dictionary of agents' states based on which action decisions will be made. - """ - return {agent_id: self.policy[agent_id].choose_action(st) for agent_id, st in state.items()} - - def get_rollout_info(self, trajectory: Dict[str, Trajectory]): - """Get experiences by policy names.""" - rollout_info = {} - for agent_id, traj in trajectory.items(): - if isinstance(self.policy[agent_id], RLPolicy): - policy_name = self.agent2policy[agent_id] - rollout_info[policy_name] = self.policy_dict[policy_name].get_rollout_info(traj) - - return rollout_info - - def set_policy_states(self, policy_state_dict: dict): - """Update policy states.""" - for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].set_state(policy_state) - - @property - def exploration_params(self): - return { - name: policy.exploration_params for name, policy in self.policy_dict.items() if isinstance(policy, RLPolicy) - } - - def exploration_step(self): - for policy in self.policy_dict.values(): - if hasattr(policy, "exploration_step"): - policy.exploration_step() - - def exploit(self): - for policy in self.policy_dict.values(): - if hasattr(policy, "exploit"): - policy.exploit() - - def explore(self): - for policy in self.policy_dict.values(): - if hasattr(policy, "explore"): - policy.explore() diff --git a/maro/rl/wrappers/env_wrapper.py b/maro/rl/wrappers/env_wrapper.py deleted file mode 100644 index a86ceb9e2..000000000 --- a/maro/rl/wrappers/env_wrapper.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from collections import defaultdict, deque -from typing import Callable - -from maro.rl.types import Trajectory, Transition -from maro.simulator import Env - - -class AbsEnvWrapper(ABC): - """Environment wrapper that performs scenario-specific processing, transition caching and experience generation. - - Args: - env (Env): Environment instance. - reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward - for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately - after executing an action. - replay_agent_ids (list): List of agent IDs whose transitions will be stored in internal replay buffers. - If it is None, it will be set to all agents in the environment (i.e., env.agent_idx_list). Defaults - to None. - post_step (Callable): Custom function to gather information about a transition and the evolvement of the - environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` - instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition - is a ``Transition`` object. For example, this callback can be used to collect various statistics on the - simulation. Defaults to None. - """ - def __init__( - self, - env: Env, - reward_eval_delay: int = 0, - replay_agent_ids: list = None, - post_step: Callable = None - ): - self.env = env - self.reward_eval_delay = reward_eval_delay - - self._post_step = post_step - - replay_agent_ids = self.env.agent_idx_list if not replay_agent_ids else replay_agent_ids - self._replay_buffer = {agent_id: defaultdict(list) for agent_id in replay_agent_ids} - self._transition_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated - self._step_index = None - self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. - self._state = None # the latest extracted state is kept here - - self.tracker = {} # User-defined tracking information is placed here. - self.replay = True - - @property - def step_index(self): - """Number of environmental steps taken so far.""" - return self._step_index - - @property - def agent_idx_list(self): - return self.env.agent_idx_list - - @property - def summary(self): - return self.env.metrics - - @property - def state(self): - """The current environmental state.""" - return self._state - - @property - def event(self): - return self._event - - def start(self): - """Generate the initial environmental state at the beginning of a simulation episode.""" - self._step_index = 0 - _, self._event, _ = self.env.step(None) - self._state = self.get_state(self.env.tick) - - @abstractmethod - def get_state(self, tick: int = None) -> dict: - """Compute the state for a given tick. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - - Returns: - A dictionary with (agent ID, state) as key-value pairs. - """ - raise NotImplementedError - - @abstractmethod - def to_env_action(self, action) -> dict: - """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" - raise NotImplementedError - - @abstractmethod - def get_reward(self, actions: list, tick: int = None): - """Evaluate the reward for an action. - - Args: - tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in - ``actions`` must be an Action object defined for the environment in question. The tick may - be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). - Defaults to None. - - Returns: - A dictionary with (agent ID, reward) as key-value pairs. - """ - raise NotImplementedError - - def get_transition_info(self, tick: int = None): - """Get additional info for a transition. - - The returned transition info will be stored in the experience manager alongside states, actions, rewards. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - - Returns: - A dictionary with (agent ID, transition_info) as key-value pairs. - - """ - pass - - def step(self, action_by_agent: dict): - """Wrapper for env.step(). - - The new transition is stored in the replay buffer or cached in a separate data structure if the - reward cannot be determined yet due to a non-zero ``reward_eval_delay``. - """ - self._step_index += 1 - env_action = self.to_env_action(action_by_agent) - - self._transition_cache.append(( - self._state, - action_by_agent, - env_action, - self.get_transition_info(), - self.env.tick - )) - - _, self._event, done = self.env.step(env_action) - - if not done: - self._state = self.get_state(self.env.tick) # current env state - else: - self._state = None - - """ - If this is the final step, evaluate rewards for all remaining events except the last. - Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ - while ( - self._transition_cache and - (done or self.env.tick - self._transition_cache[0][-1] >= self.reward_eval_delay) - ): - state, action, env_action, info, tick = self._transition_cache.popleft() - reward = self.get_reward(env_action, tick=tick) - if self._post_step: - next_state = self._transition_cache[0][0] if self._transition_cache else None - transition = Transition(state, action, env_action, reward, next_state, info) - # put things you want to track in the tracker attribute - self._post_step(self.env, self.tracker, transition) - - if self.replay: - for agent_id, agent_state in state.items(): - if agent_id in self._replay_buffer: - buf = self._replay_buffer[agent_id] - buf["states"].append(agent_state) - buf["actions"].append(action[agent_id]) - buf["rewards"].append(reward[agent_id]) - buf["info"].append(info[agent_id] if info else None) - - def get_trajectory(self, clear_buffer: bool = True): - """Get agent trajectories from the replay buffer and clear it in the process.""" - trajectory = {} - for agent_id, buf in self._replay_buffer.items(): - # end of an episode - if not self._state: - states = buf["states"] + [None] - actions = buf["actions"] + [None] - rewards = buf["rewards"][:] - info = buf["info"][:] - if clear_buffer: - del buf["states"] - del buf["actions"] - del buf["rewards"] - del buf["info"] - else: - states = buf["states"][:] - actions = buf["actions"][:] - rewards = buf["rewards"][:-1] - info = buf["info"][:-1] - if clear_buffer: - del buf["states"][:-1] - del buf["actions"][:-1] - del buf["rewards"][:-1] - del buf["info"][:-1] - - trajectory[agent_id] = Trajectory(states, actions, rewards, info) - - return trajectory - - def reset(self): - self.env.reset() - self._state = None - self._transition_cache.clear() - self.tracker.clear() - for replay in self._replay_buffer.values(): - replay.clear() From 505cf4e3d3d3311e11b88dcbbbb336134a9921a4 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 27 Aug 2021 11:04:24 +0000 Subject: [PATCH 418/482] moved replay buffer inside policy --- maro/rl/learning/env_sampler.py | 127 ++++------ maro/rl/model/__init__.py | 10 - maro/rl/model/core_model.py | 180 -------------- maro/rl/model/fc_block.py | 115 --------- maro/rl/modeling/__init__.py | 2 +- .../{special_types.py => specials.py} | 52 ++-- maro/rl/policy/ac.py | 204 ++++++++------- maro/rl/policy/dqn.py | 131 ++++------ maro/rl/policy/policy.py | 66 ++--- maro/rl/policy/replay.py | 22 +- maro/rl/types.py | 52 ---- maro/rl/types/__init__.py | 10 - maro/rl/types/model_types.py | 232 ------------------ maro/rl/types/rollout_object_types.py | 38 --- 14 files changed, 274 insertions(+), 967 deletions(-) delete mode 100644 maro/rl/model/__init__.py delete mode 100644 maro/rl/model/core_model.py delete mode 100644 maro/rl/model/fc_block.py rename maro/rl/modeling/{special_types.py => specials.py} (89%) delete mode 100644 maro/rl/types.py delete mode 100644 maro/rl/types/__init__.py delete mode 100644 maro/rl/types/model_types.py delete mode 100644 maro/rl/types/rollout_object_types.py diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index a758a0142..2c46f2428 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -1,14 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from abc import ABC, abstractmethod +from abc import abstractmethod from collections import defaultdict, deque from os import getcwd from typing import Callable, Dict from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.policy import AbsPolicy, RLPolicy -from maro.rl.types import Transition from maro.rl.utils import MsgKey, MsgTag from maro.simulator import Env from maro.utils import Logger @@ -16,7 +15,7 @@ from .common import get_rollout_finish_msg -class AbsEnvSampler(ABC): +class EnvSampler: """Simulation data collector and policy evaluator. Args: @@ -33,11 +32,8 @@ class AbsEnvSampler(ABC): instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition is a ``Transition`` object. For example, this callback can be used to collect various statistics on the simulation. Defaults to None. - get_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. - agent2policy (Dict[str, str]): Mapping from agent ID's to policy ID's. This is used to direct an agent's - queries to the correct policy. get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the worker processes. Defaults to None. @@ -46,7 +42,7 @@ def __init__( self, get_env: Callable[[], Env], get_policy_func_dict: Dict[str, AbsPolicy], - agent2policy: Dict[str, str], + get_state_func_dict: Dict[str, Callable], get_eval_env: Callable[[], Env] = None, reward_eval_delay: int = 0, max_trajectory_len: Dict[str, int] = None, @@ -55,10 +51,12 @@ def __init__( self.env = get_env() self.eval_env = get_eval_env() if get_eval_env else self.env self.policy_dict = {name: func(name) for name, func in get_policy_func_dict.items()} - self.agent2policy = agent2policy - self.policy = { - agent_id: self.policy_dict[policy_id] for agent_id, policy_id in self.agent2policy.items() - } + self.policy = {} + for policy in self.policy_dict.values(): + for agent in policy.agents: + self.policy[agent] = policy + + self.get_state = get_state_func_dict self.reward_eval_delay = reward_eval_delay self._post_step = post_step @@ -68,23 +66,13 @@ def __init__( agent_id: policy.Buffer(self.state_dim[agent_id], ) for agent_id, policy in self.policy.items() if isinstance(policy, RLPolicy) } - self._transition_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated + self._transition_cache = defaultdict(deque()) # for caching transitions whose rewards have yet to be evaluated self._step_index = None self._terminal = False self.tracker = {} # User-defined tracking information is placed here. self.replay = True - @property - @abstractmethod - def state_dim(self) -> int: - raise NotImplementedError - - @property - @abstractmethod - def action_dim(self) -> int: - raise NotImplementedError - @property def step_index(self): """Number of environmental steps taken so far.""" @@ -98,6 +86,9 @@ def agent_idx_list(self): def summary(self): return self.env.metrics + def get_agents_from_event(self, event): + return self.agent_idx_list + def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): # set policy states if policy_state_dict: @@ -118,54 +109,41 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio # get initial state self._step_index = 0 _, self._event, _ = self.env.step(None) - self._state = self.get_state(self.env.tick) + self._state = {agent: self.get_state[agent](self.env) for agent in self.get_agents_from_event(self._event)} starting_step_index = self._step_index + 1 steps_to_go = float("inf") if num_steps == -1 else num_steps while not self._terminal and steps_to_go > 0: - state_by_agent = self.get_state() - action_by_agent = {agent: self.policy[agent].choose_action(st) for agent, st in state_by_agent.items()} - env_action = self.to_env_action(action_by_agent) - self._transition_cache.append(( - state_by_agent, - action_by_agent, - env_action, - self.env.tick - )) + action = {agent: self.policy[agent].choose_action(st) for agent, st in self._state.items()} + env_action = self.to_env_action(action) _, self._event, self._terminal = self.env.step(env_action) + prev_state = self._state + self._state = None if self._terminal else { + agent: self.get_state[agent](self.env) for agent in self.get_agents_from_event(self._event) + } + self._transition_cache.append((prev_state, action, env_action, self._state, self.env.tick)) self._step_index += 1 steps_to_go -= 1 """ If this is the final step, evaluate rewards for all remaining events except the last. Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ - while ( - self._transition_cache and - (self._terminal or self.env.tick - self._transition_cache[0][-1] >= self.reward_eval_delay) - ): - state_by_agent, action_by_agent, env_action, tick = self._transition_cache.popleft() - reward_by_agent = self.get_reward(env_action, tick=tick) - if self._post_step: - next_state = self._transition_cache[0][0] if self._transition_cache else None - transition = Transition(state_by_agent, action_by_agent, env_action, reward_by_agent, next_state) - # put things you want to track in the tracker attribute - self._post_step(self.env, self.tracker, transition) - - for agent_id, state in state_by_agent.items(): - if agent_id in self._replay_buffer: - self._replay_buffer[agent_id].store( - state, action_by_agent[agent_id], reward_by_agent[agent_id], - terminal=not self._transition_cache and self._terminal, - ) - - rollout_info = {} - for agent_id, buf in self._replay_buffer.items(): - if isinstance(self.policy[agent_id], RLPolicy): - policy_name = self.agent2policy[agent_id] - rollout_info[policy_name] = self.policy_dict[policy_name].get_rollout_info(buf.get()) + for agent, cache in self._transition_cache.items(): + while cache and (self._terminal or self.env.tick - cache[0][-1] >= self.reward_eval_delay): + state, action, env_action, state_, tick = cache.popleft() + reward = self.get_reward(env_action, tick=tick) + if self._post_step: + # put things you want to track in the tracker attribute + self._post_step(self.env, self.tracker, state, action, env_action, reward, state_, tick) + + if isinstance(self.policy[agent], RLPolicy): + self.policy[agent].record(agent, state, action, reward, state_, not cache and self._terminal) return { - "rollout_info": rollout_info, + "rollout_info": { + id_: policy.get_rollout_info() for id_, policy in self.policy_dict.items() + if isinstance(policy, RLPolicy) + }, "step_range": (starting_step_index, self._step_index), "tracker": self.tracker, "end_of_episode": not self._terminal, @@ -176,9 +154,10 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio } def test(self, policy_state_dict: dict = None): + # set policy states if policy_state_dict: - for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].set_state(policy_state) + for id_, policy_state in policy_state_dict.items(): + self.policy_dict[id_].set_state(policy_state) # Set policies to exploitation mode for policy in self.policy_dict.values(): @@ -186,25 +165,17 @@ def test(self, policy_state_dict: dict = None): policy.exploit() self.eval_env.reset() - self.eval_env.replay = False - self.eval_env.start() # get initial state - while self.eval_env.state: - self.eval_env.step({agent: self.policy[agent].choose_action(st) for agent, st in self.env.state.items()}) - - return self.eval_env.tracker - - @abstractmethod - def get_state(self, tick: int = None) -> dict: - """Compute the state for a given tick. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - - Returns: - A dictionary with (agent ID, state) as key-value pairs. - """ - raise NotImplementedError + terminal = False + # get initial state + _, event, _ = self.eval_env.step(None) + state = {agent: self.get_state[agent](self.eval_env) for agent in self.get_agents_from_event(event)} + while not terminal: + action = {agent: self.policy[agent].choose_action(st) for agent, st in state.items()} + env_action = self.to_env_action(action) + _, event, terminal = self.eval_env.step(env_action) + state = {agent: self.get_state[agent](self.eval_env) for agent in self.get_agents_from_event(event)} + + return self.tracker @abstractmethod def to_env_action(self, action) -> dict: diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py deleted file mode 100644 index a3926ddb3..000000000 --- a/maro/rl/model/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .core_model import AbsCoreModel, OptimOption -from .fc_block import FullyConnectedBlock - -__all__ = [ - "AbsCoreModel", "OptimOption", - "FullyConnectedBlock", -] diff --git a/maro/rl/model/core_model.py b/maro/rl/model/core_model.py deleted file mode 100644 index 24e3ad92e..000000000 --- a/maro/rl/model/core_model.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import abstractmethod -from typing import Dict, List, Union - -import torch -import torch.nn as nn - -from maro.rl.utils import get_torch_lr_scheduler_cls, get_torch_optim_cls -from maro.utils import clone -from maro.utils.exception.rl_toolkit_exception import MissingOptimizer - - -class OptimOption: - """Model optimization options. - - Args: - optim_cls: A string indicating an optimizer class provided by torch.optim or custom subclass of - torch.optim.Optimizer. If a string is provided, it must be present in the ``TORCH_OPTIM`` index. - optim_params (dict): Parameters for the optimizer class. - scheduler_cls: A string indicating an lr-scheduler class provided by torch.optim.lr_scheduler or custom - subclass of torch.optim.lr_scheduler. If a string is provided, it must be present in the - ``TORCH_LR_SCHEDULER`` index. Defaults to None. - scheduler_params (dict): Parameters for the scheduler class. Defaults to None. - """ - __slots__ = ["optim_cls", "optim_params", "scheduler_cls", "scheduler_params"] - - def __init__(self, optim_cls, optim_params: dict, scheduler_cls=None, scheduler_params: dict = None): - self.optim_cls = get_torch_optim_cls(optim_cls) - self.optim_params = optim_params - self.scheduler_cls = get_torch_lr_scheduler_cls(scheduler_cls) - self.scheduler_params = scheduler_params - - -class AbsCoreModel(nn.Module): - """Trainable model that consists of multiple network components. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__() - self.component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) - if optim_option is None: - self.optimizer = None - self.scheduler = None - self.eval() - for param in self.parameters(): - param.requires_grad = False - else: - if isinstance(optim_option, dict): - self.optimizer, self.scheduler = {}, {} - for name, opt in optim_option.items(): - self.optimizer[name] = opt.optim_cls(self.component[name].parameters(), **opt.optim_params) - if opt.scheduler_cls: - self.scheduler[name] = opt.scheduler_cls(self.optimizer[name], **opt.scheduler_params) - else: - self.optimizer = optim_option.optim_cls(self.parameters(), **optim_option.optim_params) - if optim_option.scheduler_cls: - self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) - - if device is None: - self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - else: - self.device = torch.device(device) - self.to(self.device) - - @property - def trainable(self) -> bool: - """Return True if at least one optimizer is registered.""" - return self.optimizer is not None - - @abstractmethod - def forward(self, *args, **kwargs): - raise NotImplementedError - - def get_gradients(self, loss: torch.tensor): - """Compute gradients from a loss """ - if self.optimizer is None: - raise MissingOptimizer("No optimizer registered to the model") - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.zero_grad() - else: - self.optimizer.zero_grad() - - # Obtain gradients through back-propagation - loss.backward() - - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad_dict_list: List[Dict[str, float]]): - avg_grad_dict = { - param_name: torch.mean(torch.stack([grad_dict[param_name] for grad_dict in grad_dict_list]), dim=0) - for param_name in grad_dict_list[0] - } - for name, param in self.named_parameters(): - param.grad = avg_grad_dict[name] - - # Apply gradients - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.step() - else: - self.optimizer.step() - - def step(self, loss): - """Use the loss to back-propagate gradients and apply them to the underlying parameters. - - Args: - loss: Result of a computation graph that involves the underlying parameters. - """ - if self.optimizer is None: - raise MissingOptimizer("No optimizer registered to the model") - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.zero_grad() - else: - self.optimizer.zero_grad() - - # Obtain gradients through back-propagation - loss.backward() - - # Apply gradients - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.step() - else: - self.optimizer.step() - - def update_learning_rate(self, component_name: Union[str, List[str]] = None): - if not isinstance(self.scheduler, dict): - self.scheduler.step() - elif isinstance(component_name, str): - if component_name not in self.scheduler: - raise KeyError(f"Component {component_name} does not have a learning rate scheduler") - self.scheduler[component_name].step() - elif isinstance(component_name, list): - for key in component_name: - if key not in self.scheduler: - raise KeyError(f"Component {key} does not have a learning rate scheduler") - self.scheduler[key].step() - else: - for sch in self.scheduler.values(): - sch.step() - - def copy(self, with_optimizer: bool = False, device: str = None): - """Return a deep copy of the instance; - - Args: - with_opimizer (bool): If True, the registered optimizers will also be deep copied. - Defaults to False. - device (str): The device the copied instance should be placed on. Defaults to None, - in which case the copied instance will be placed on the same device as the instance itself. - """ - model_copy = clone(self) - if not with_optimizer: - model_copy.optimizer = None - model_copy.scheduler = None - - device = self.device if device is None else torch.device(device) - model_copy.to(device) - - return model_copy diff --git a/maro/rl/model/fc_block.py b/maro/rl/model/fc_block.py deleted file mode 100644 index b2f03af5d..000000000 --- a/maro/rl/model/fc_block.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import OrderedDict -from typing import List - -import torch -import torch.nn as nn - -from maro.rl.utils import get_torch_activation_cls - - -class FullyConnectedBlock(nn.Module): - """Fully connected network with optional batch normalization, activation and dropout components. - - Args: - name (str): Network name. - input_dim (int): Network input dimension. - output_dim (int): Network output dimension. - hidden_dims ([int]): Dimensions of hidden layers. Its length is the number of hidden layers. - activation: A string indicatinfg an activation class provided by ``torch.nn`` or a custom activation class. - If it is a string, it must be a key in ``TORCH_ACTIVATION``. If None, there will be no activation. - Defaults to "relu". - head (bool): If true, this block will be the top block of the full model and the top layer of this block - will be the final output layer. Defaults to False. - softmax (bool): If true, the output of the net will be a softmax transformation of the top layer's - output. Defaults to False. - batch_norm (bool): If true, batch normalization will be performed at each layer. - skip_connection (bool): If true, a skip connection will be built between the bottom (input) layer and - top (output) layer. Defaults to False. - dropout_p (float): Dropout probability. Defaults to None, in which case there is no drop-out. - gradient_threshold (float): Gradient clipping threshold. Defaults to None, in which case not gradient clipping - is performed. - """ - def __init__( - self, - input_dim: int, - output_dim: int, - hidden_dims: List[int], - activation="relu", - head: bool = False, - softmax: bool = False, - batch_norm: bool = False, - skip_connection: bool = False, - dropout_p: float = None, - gradient_threshold: float = None, - name: str = None - ): - super().__init__() - self._input_dim = input_dim - self._hidden_dims = hidden_dims if hidden_dims is not None else [] - self._output_dim = output_dim - - # network features - self._activation = get_torch_activation_cls(activation)() if activation else None - self._head = head - self._softmax = nn.Softmax(dim=1) if softmax else None - self._batch_norm = batch_norm - self._dropout_p = dropout_p - - if skip_connection and input_dim != output_dim: - raise ValueError( - f"input and output dimensions must match if skip connection is enabled, " - f"got {input_dim} and {output_dim}" - ) - - self._skip_connection = skip_connection - - # build the net - dims = [self._input_dim] + self._hidden_dims - layers = [self._build_layer(in_dim, out_dim) for in_dim, out_dim in zip(dims, dims[1:])] - # top layer - layers.append(self._build_layer(dims[-1], self._output_dim, head=self._head)) - - self._net = nn.Sequential(*layers) - - self._gradient_threshold = gradient_threshold - if gradient_threshold is not None: - for param in self._net.parameters(): - param.register_hook(lambda grad: torch.clamp(grad, -gradient_threshold, gradient_threshold)) - - self._name = name - - def forward(self, x): - out = self._net(x) - if self._skip_connection: - out += x - return self._softmax(out) if self._softmax else out - - @property - def name(self): - return self._name - - @property - def input_dim(self): - return self._input_dim - - @property - def output_dim(self): - return self._output_dim - - def _build_layer(self, input_dim, output_dim, head: bool = False): - """Build basic layer. - - BN -> Linear -> Activation -> Dropout - """ - components = [] - if self._batch_norm: - components.append(("batch_norm", nn.BatchNorm1d(input_dim))) - components.append(("linear", nn.Linear(input_dim, output_dim))) - if not head and self._activation is not None: - components.append(("activation", self._activation)) - if not head and self._dropout_p: - components.append(("dropout", nn.Dropout(p=self._dropout_p))) - return nn.Sequential(OrderedDict(components)) diff --git a/maro/rl/modeling/__init__.py b/maro/rl/modeling/__init__.py index dbb571bde..8179d2a6c 100644 --- a/maro/rl/modeling/__init__.py +++ b/maro/rl/modeling/__init__.py @@ -3,7 +3,7 @@ from .core_model import AbsCoreModel, OptimOption from .fc_block import FullyConnectedBlock -from .special_types import ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet +from .specials import ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet __all__ = [ "AbsCoreModel", "OptimOption", diff --git a/maro/rl/modeling/special_types.py b/maro/rl/modeling/specials.py similarity index 89% rename from maro/rl/modeling/special_types.py rename to maro/rl/modeling/specials.py index 088e0c807..add2d311f 100644 --- a/maro/rl/modeling/special_types.py +++ b/maro/rl/modeling/specials.py @@ -2,15 +2,13 @@ # Licensed under the MIT license. from abc import abstractmethod -from typing import Dict, List, Union +from typing import Dict, Union import numpy as np import torch from torch import nn from torch.distributions import Categorical -from maro.rl.types import State - from .core_model import AbsCoreModel, OptimOption @@ -38,8 +36,13 @@ def __init__( ): super().__init__(component, optim_option=optim_option, device=device) + @property + @abstractmethod + def input_dim(self): + raise NotImplementedError + @abstractmethod - def forward(self, states: List[State], actor: bool = True, critic: bool = True) -> tuple: + def forward(self, states: torch.tensor, actor: bool = True, critic: bool = True) -> tuple: """Compute action probabilities and values for a state batch. The output is a tuple of (action_probs, values), where action probs is a tensor of shape @@ -47,13 +50,13 @@ def forward(self, states: List[State], actor: bool = True, critic: bool = True) of these two is needed, the return value for the other one can be set to None. Args: - states (List[State]): State batch to compute action probabilities and values for. + states (torch.tensor): State batch to compute action probabilities and values for. actor (bool): If True, the first element of the output will be actin probabilities. Defaults to True. critic (bool): If True, the second element of the output will be state values. Defaults to True. """ raise NotImplementedError - def get_action(self, states, max_prob: bool = False): + def get_action(self, states: torch.tensor, max_prob: bool = False): """ Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value for each state. @@ -80,7 +83,7 @@ class DiscretePolicyNet(AbsCoreModel): parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against the component names and optimizers created for them. Note that it is possible to freeze certain components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. + Defaults to None. device (str): Identifier for the torch device. The model instance will be moved to the specified device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. @@ -93,18 +96,23 @@ def __init__( ): super().__init__(component, optim_option=optim_option, device=device) + @property @abstractmethod - def forward(self, states) -> torch.tensor: + def input_dim(self): + raise NotImplementedError + + @abstractmethod + def forward(self, states: torch.tensor) -> torch.tensor: """Compute action probabilities corresponding to each state in ``states``. The output must be a torch tensor with shape (batch_size, action_space_size). Args: - states (List[State]): State batch to compute action probabilities for. + states (torch.tensor): State batch to compute action probabilities for. """ raise NotImplementedError - def get_action(self, states: List[State], max_prob: bool = False): + def get_action(self, states: torch.tensor, max_prob: bool = False): """ Given a batch of states, return actions selected based on the probabilities computed by ``forward`` and the corresponding log probabilities. @@ -144,17 +152,22 @@ def __init__( ): super().__init__(component, optim_option=optim_option, device=device) + @property + @abstractmethod + def input_dim(self): + raise NotImplementedError + @property @abstractmethod def num_actions(self): raise NotImplementedError @abstractmethod - def forward(self, states: List[State]) -> torch.tensor: + def forward(self, states: torch.tensor) -> torch.tensor: """Compute the Q-values for all actions as a tensor of shape (batch_size, action_space_size).""" raise NotImplementedError - def get_action(self, states: List[State]): + def get_action(self, states: torch.tensor): """ Given Q-values for a batch of states and all actions, return the action index and the corresponding Q-values for each state. @@ -163,7 +176,7 @@ def get_action(self, states: List[State]): greedy_q, actions = q_for_all_actions.max(dim=1) return actions.detach(), greedy_q.detach(), q_for_all_actions.shape[1] - def q_values(self, states, actions: torch.tensor): + def q_values(self, states: torch.tensor, actions: torch.tensor): """Return the Q-values for a batch of states and actions.""" if len(actions.shape) == 1: actions = actions.unsqueeze(dim=1) @@ -207,6 +220,11 @@ def __init__( ): super().__init__(component, optim_option=optim_option, device=device) + @property + @abstractmethod + def input_dim(self): + raise NotImplementedError + def set_action_space( self, min_action: Union[float, np.ndarray] = None, @@ -233,22 +251,22 @@ def set_action_space( self._max_action = torch.from_numpy(max_action) if isinstance(max_action, np.ndarray) else max_action @abstractmethod - def forward(self, states: List[State], actions=None) -> torch.tensor: + def forward(self, states: torch.tensor, actions=None) -> torch.tensor: """Compute actions for a batch of states or Q-values for a batch of states and actions. Args: - states (List[State]): State batch to compute the Q-values for. + states (torch.tensor): State batch to compute the Q-values for. actions: Action batch. If None, the output should be a batch of actions corresponding to the state batch. Otherwise, the output should be the Q-values for the given states and actions. Defaults to None. """ raise NotImplementedError - def get_action(self, states: List[State]) -> torch.tensor: + def get_action(self, states: torch.tensor) -> torch.tensor: """Compute actions given a batch of states.""" return torch.clamp(self.forward(states), min=self._min_action, max=self._max_action) - def value(self, states: List[State]): + def value(self, states: torch.tensor): """Compute the Q-values for a batch of states using the actions computed from them.""" return self.forward(states, actions=self.get_action(states)) diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index a96f2a957..adc75f803 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -1,42 +1,21 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import defaultdict from typing import List import numpy as np import torch from torch.distributions import Categorical -from maro.rl.types import DiscreteACNet +from maro.rl.modeling import DiscreteACNet from maro.rl.utils import discount_cumsum, get_torch_loss_cls from .policy import RLPolicy class ActorCritic(RLPolicy): - """Actor Critic algorithm with separate policy and value models. - - References: - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f - - Args: - name (str): Unique identifier for the policy. - ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. - reward_discount (float): Reward decay as defined in standard RL terminology. - grad_iters (int): Number of gradient steps for each batch or set of batches. Defaults to 1. - critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing - the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - min_logp (float): Lower bound for clamping logP values during learning. This is to prevent logP from becoming - very large in magnitude and causing stability issues. Defaults to None, which means no lower bound. - critic_loss_coeff (float): Coefficient for critic loss in total loss. Defaults to 1.0. - entropy_coeff (float): Coefficient for the entropy term in total loss. Defaults to None, in which case the - total loss will not include an entropy term. - clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, - in which case the actor loss is calculated using the usual policy gradient theorem. - """ - - class Buffer(RLPolicy.Buffer): + class Buffer: """Sequence of transitions for an agent. Args: @@ -45,12 +24,16 @@ class Buffer(RLPolicy.Buffer): rewards: Sequence of rewards received as a result of the actions. info: Sequence of each transition's auxillary information. """ - def __init__(self, state_dim, action_dim: int = 1, max_len: int = 10000): - super.__init__(state_dim, action_dim=1, max_len=max_len) - self.logps = np.zeros(max_len, dtype=np.float32) - self.values = np.zeros(max_len, dtype=np.float32) - - def store(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): + def __init__(self, state_dim, size: int = 10000): + self.states = np.zeros((size, state_dim), dtype=np.float32) + self.actions = np.zeros(size, dtype=np.int) + self.logps = np.zeros(size, dtype=np.float32) + self.values = np.zeros(size, dtype=np.float32) + self.rewards = np.zeros(size, dtype=np.float32) + self.terminal = np.zeros(size, dtype=np.bool) + self.size = size + + def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): self.states[self._ptr] = state self.actions[self._ptr] = action["action"] self.logps[self._ptr] = action["logp"] @@ -59,58 +42,43 @@ def store(self, state: np.ndarray, action: dict, reward: float, terminal: bool = self.terminal[self._ptr] = terminal # increment pointer self._ptr += 1 - if self._ptr == self.max_len: + if self._ptr == self.size: self._ptr = 0 def get(self): - traj_slice = slice(self._last_ptr, self._ptr) - self._last_ptr = self._ptr + terminal = self.terminal[self._ptr - 1] + traj_slice = slice(self._last_ptr, self._ptr - (not terminal)) + self._last_ptr = self._ptr - (not terminal) return { "states": self.states[traj_slice], "actions": self.actions[traj_slice], "logps": self.logps[traj_slice], "values": self.values[traj_slice], "rewards": self.rewards[traj_slice], - "terminal": self.terminal[self._ptr - 1] + "last_value": self.values[-1] } + """Actor Critic algorithm with separate policy and value models. - class Batch(RLPolicy.Batch): - - __slots__ = ["states", "actions", "returns", "advantages", "logps"] - - def __init__( - self, - states: np.ndarray, - actions: np.ndarray, - returns: np.ndarray, - advantages: np.ndarray, - logps: np.ndarray - ): - super().__init__() - self.states = states - self.actions = actions - self.returns = returns - self.advantages = advantages - self.logps = logps - - @property - def size(self): - return len(self.states) - - - class LossInfo(RLPolicy.LossInfo): - - __slots__ = ["actor_loss", "critic_loss", "entropy"] - - def __init__(self, loss, actor_loss, critic_loss, entropy, grad=None): - super().__init__(loss, grad) - self.loss = loss - self.actor_loss = actor_loss - self.critic_loss = critic_loss - self.entropy = entropy - self.grad = grad + References: + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f + Args: + name (str): Unique identifier for the policy. + ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. + reward_discount (float): Reward decay as defined in standard RL terminology. + grad_iters (int): Number of gradient steps for each batch or set of batches. Defaults to 1. + critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing + the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". + min_logp (float): Lower bound for clamping logP values during learning. This is to prevent logP from becoming + very large in magnitude and causing stability issues. Defaults to None, which means no lower bound. + critic_loss_coeff (float): Coefficient for critic loss in total loss. Defaults to 1.0. + entropy_coeff (float): Coefficient for the entropy term in total loss. Defaults to None, in which case the + total loss will not include an entropy term. + clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, + in which case the actor loss is calculated using the usual policy gradient theorem. + """ def __init__( self, @@ -124,6 +92,7 @@ def __init__( entropy_coeff: float = None, clip_ratio: float = None, lam: float = 0.9, + buffer_size: int = 10000, get_loss_on_rollout_finish: bool = False, remote: bool = False ): @@ -141,8 +110,15 @@ def __init__( self.entropy_coeff = entropy_coeff self.clip_ratio = clip_ratio self.lam = lam + self.buffer_size = buffer_size self._get_loss_on_rollout_finish = get_loss_on_rollout_finish + self._buffer = {} + + def add_agent(self, agent: str): + super().add_agent(agent) + self._buffer[agent] = self.Buffer(self.ac_net.input_dim, size=self.buffer_size) + def choose_action(self, states): """Return actions and log probabilities for given states.""" self.ac_net.eval() @@ -156,42 +132,54 @@ def choose_action(self, states): {"action": action, "logp": logp, "value": value} for action, logp, value in zip(actions, logps, values) ] - def get_rollout_info(self, trajectory: dict): + def record( + self, + agent: str, + state: np.ndarray, + action: dict, + reward: float, + next_state: np.ndarray, + terminal: bool + ): + if agent not in self._buffer: + raise KeyError( + f"Agent {agent} has not been added to this policy. " + f"Make sure to add it using 'add_agent' before using it for inference." + ) + self._buffer[agent].put(state, action, reward, terminal) + + def get_rollout_info(self): if self._get_loss_on_rollout_finish: - return self.get_batch_loss(self._preprocess(trajectory), explicit_grad=True) + return self.get_batch_loss(self._get_batch(), explicit_grad=True) else: - return trajectory - - def _preprocess(self, trajectory: dict): - if trajectory["terminal"]: - states = trajectory["states"] - actions = trajectory["actions"] - logps = trajectory["logps"] - values = np.append(trajectory["values"], .0) - rewards = np.append(trajectory["rewards"], .0) - else: - states = trajectory["states"][:-1] - actions = trajectory["actions"][:-1] - logps = trajectory["logps"][:-1] - values = trajectory["values"] - rewards = np.append(trajectory["rewards"][:-1], trajectory["values"][-1]) - - # Generalized advantage estimation using TD(Lambda) - deltas = rewards[:-1] + self.reward_discount * values[1:] - values[:-1] - advantages = discount_cumsum(deltas, self.reward_discount * self.lam) - # Returns rewards-to-go, to be targets for the value function - returns = discount_cumsum(rewards, self.reward_discount)[:-1] - return self.Batch(states, actions, returns, advantages, logps) - - def get_batch_loss(self, batch: Batch, explicit_grad: bool = False): + return self._get_batch() + + def _get_batch(self): + batch = defaultdict(list) + for buf in self._buffer: + trajectory = buf.get() + values = np.append(trajectory["values"], trajectory["last_val"]) + rewards = np.append(trajectory["rewards"], trajectory["last_val"]) + deltas = rewards[:-1] + self.reward_discount * values[1:] - values[:-1] + batch["states"].append(trajectory["states"]) + batch["actions"].append(trajectory["actions"]) + # Returns rewards-to-go, to be targets for the value function + batch["returns"].append(discount_cumsum(rewards, self.reward_discount)[:-1]) + # Generalized advantage estimation using TD(Lambda) + batch["advantages"].append(discount_cumsum(deltas, self.reward_discount * self.lam)) + batch["logps"].append(trajectory["logps"]) + + return {key: np.concatenate(vals) for key, vals in batch.items} + + def get_batch_loss(self, batch: dict, explicit_grad: bool = False): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() - actions = torch.from_numpy(batch.actions).to(self.device) - logp_old = torch.from_numpy(batch.logps).to(self.device) - returns = torch.from_numpy(batch.returns).to(self.device) - advantages = torch.from_numpy(batch.advantages).to(self.device) + actions = torch.from_numpy(batch["actions"]).to(self.device) + logp_old = torch.from_numpy(batch["logps"]).to(self.device) + returns = torch.from_numpy(batch["returns"]).to(self.device) + advantages = torch.from_numpy(batch["advantages"]).to(self.device) - action_probs, state_values = self.ac_net(batch.states) + action_probs, state_values = self.ac_net(batch["states"]) state_values = state_values.squeeze() # actor loss @@ -206,19 +194,19 @@ def get_batch_loss(self, batch: Batch, explicit_grad: bool = False): # critic_loss critic_loss = self.critic_loss_func(state_values, returns) - # entropy - if self.entropy_coeff is not None: - entropy = -Categorical(action_probs).entropy().mean() - else: - entropy = 0 + entropy = -Categorical(action_probs).entropy().mean() if self.entropy_coeff is not None else 0 # total loss loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy - grad = self.ac_net.get_gradients(loss) if explicit_grad else None - return self.LossInfo(actor_loss, critic_loss, entropy, loss, grad=grad) - def update_with_multi_loss_info(self, loss_info_list: List[LossInfo]): + loss_info = {"actor_loss": actor_loss, "critic_loss": critic_loss, "entropy": entropy, "loss": loss} + if explicit_grad: + loss_info["grad"] = self.ac_net.get_gradients(loss) + + return loss_info + + def update_with_multi_loss_info(self, loss_info_list: List[dict]): """Apply gradients to the underlying parameterized model.""" self.ac_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index 0f07b04ea..6edbf7657 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -8,7 +8,6 @@ from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration from maro.rl.modeling import DiscreteQNet -from maro.utils.exception.rl_toolkit_exception import InvalidExperience from .policy import RLPolicy from .replay import ReplayMemory @@ -26,7 +25,6 @@ class PrioritizedExperienceReplay: Args: replay_memory (ReplayMemory): experience manager the sampler is associated with. - batch_size (int): mini-batch size. Defaults to 32. alpha (float): Prioritization strength. Sampling probabilities are calculated according to P = p_i^alpha / sum(p_k^alpha). Defaults to 0.6. beta (float): Bias annealing strength using weighted importance sampling (IS) techniques. @@ -39,7 +37,6 @@ def __init__( self, replay_memory: ReplayMemory, *, - batch_size: int = 32, alpha: float = 0.6, beta: float = 0.4, beta_step: float = 0.001, @@ -49,7 +46,6 @@ def __init__( raise ValueError("beta should be between 0.0 and 1.0") self._replay_memory = replay_memory self._sum_tree = np.zeros(2 * self._replay_memory.capacity - 1) - self.batch_size = batch_size self.alpha = alpha self.beta = beta self.beta_step = beta_step @@ -73,11 +69,11 @@ def update(self, indexes, td_errors): self._sum_tree[tree_idx] = priority self._update(tree_idx, delta) - def sample(self): + def sample(self, size: int): """Priority-based sampling.""" indexes, priorities = [], [] - segment_len = self.total() / self.batch_size - for i in range(self.batch_size): + segment_len = self.total() / size + for i in range(size): low, high = segment_len * i, segment_len * (i + 1) sampled_val = np.random.uniform(low=low, high=high) idx = self._get(0, sampled_val) @@ -139,53 +135,9 @@ class DQN(RLPolicy): random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. + batch_size (int): Training sample. Defaults to 32. """ - class Batch(RLPolicy.Batch): - """Wrapper for a set of experiences. - - An experience consists of state, action, reward, next state. - """ - __slots__ = ["states", "actions", "rewards", "next_states", "terminal"] - - def __init__( - self, - states: np.ndarray, - actions: np.ndarray, - rewards: np.ndarray, - next_states: np.ndarray, - terminal: np.ndarray, - indexes: list = None, - is_weights: list = None - ): - if not len(states) == len(actions) == len(rewards) == len(next_states) == len(terminal): - raise InvalidExperience("values of contents should consist of lists of the same length") - super().__init__() - self.states = states - self.actions = actions - self.rewards = rewards - self.next_states = next_states - self.terminal = terminal - self.is_weights = is_weights - self.indexes = indexes - - @property - def size(self): - return len(self.states) - - - class LossInfo(RLPolicy.LossInfo): - - __slots__ = ["td_errors", "indexes"] - - def __init__(self, loss, td_errors, indexes, grad=None): - super().__init__(loss, grad) - self.loss = loss - self.td_errors = td_errors - self.indexes = indexes - self.grad = grad - - def __init__( self, name: str, @@ -198,6 +150,7 @@ def __init__( exploration: DiscreteSpaceExploration = EpsilonGreedyExploration(), replay_memory_capacity: int = 10000, random_overwrite: bool = False, + batch_size: int = 32, prioritized_replay_kwargs: dict = None, remote: bool = False ): @@ -221,7 +174,10 @@ def __init__( self.soft_update_coeff = soft_update_coeff self.double = double - self._replay_memory = ReplayMemory(self.Batch, replay_memory_capacity, random_overwrite=random_overwrite) + self._replay_memory = ReplayMemory( + replay_memory_capacity, self.q_net.input_dim, action_dim=1, random_overwrite=random_overwrite + ) + self.batch_size = batch_size self.prioritized_replay = prioritized_replay_kwargs is not None if self.prioritized_replay: self._per = PrioritizedExperienceReplay(self._replay_memory, **prioritized_replay_kwargs) @@ -231,7 +187,7 @@ def __init__( self.exploration = exploration self.exploring = True # set initial exploration status to True - def choose_action(self, states) -> Union[int, np.ndarray]: + def choose_action(self, states: torch.tensor) -> Union[int, np.ndarray]: self.q_net.eval() with torch.no_grad(): q_for_all_actions = self.q_net(states) # (batch_size, num_actions) @@ -244,34 +200,43 @@ def choose_action(self, states) -> Union[int, np.ndarray]: actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def _put_in_replay_memory(self, traj: dict): - batch = self.Batch(traj["states"][:-1], traj["actions"][:-1], traj["rewards"], traj["states"][1:]) - indexes = self._replay_memory.put(batch) + def record( + self, + agent: str, + state: np.ndarray, + action: Union[int, float, np.ndarray], + reward: float, + next_state: np.ndarray, + terminal: bool + ): + indexes = self._replay_memory.put(state, action, reward, next_state, terminal) if self.prioritized_replay: self._per.set_max_priority(indexes) + def get_rollout_info(self): + return self.get_batch_loss(self._get_batch(), explicit_grad=True) + def _sample(self): if self.prioritized_replay: - indexes, is_weights = self._per.sample() + indexes, is_weights = self._per.sample(self.batch_size) + return { + "states": self._replay_memory.states[indexes], + "actions": self._replay_memory.actions[indexes], + "rewards": self._replay_memory.rewards[indexes], + "next_states": self._replay_memory.next_states[indexes], + "indexes": indexes, + "is_weights": is_weights + } else: - indexes = np.random.choice(self._replay_memory.size) - is_weights = None - - return self.Batch( - [self._replay_memory.data["states"][idx] for idx in indexes], - [self._replay_memory.data["actions"][idx] for idx in indexes], - [self._replay_memory.data["rewards"][idx] for idx in indexes], - [self._replay_memory.data["next_states"][idx] for idx in indexes], - indexes=indexes, - is_weights=is_weights - ) + return self._replay_memory.sample(self.batch_size) - def get_batch_loss(self, batch: Batch, explicit_grad: bool = False): + def get_batch_loss(self, batch: dict, explicit_grad: bool = False): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() - states, next_states = batch.states, batch.next_states - actions = torch.from_numpy(np.asarray(batch.actions)).to(self.device) - rewards = torch.from_numpy(np.asarray(batch.rewards)).to(self.device) + states, next_states = batch["states"], batch["next_states"] + actions = torch.from_numpy(batch["actions"]).to(self.device) + rewards = torch.from_numpy(batch["rewards"]).to(self.device) + terminals = torch.from_numpy(batch["terminals"]).to(self.device) # get target Q values with torch.no_grad(): @@ -281,26 +246,30 @@ def get_batch_loss(self, batch: Batch, explicit_grad: bool = False): else: next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) - target_q_values = (rewards + self.reward_discount * (1 - ) * next_q_values).detach() # (N,) + target_q_values = (rewards + self.reward_discount * (1 - terminals) * next_q_values).detach() # (N,) # gradient step + loss_info = {} q_values = self.q_net.q_values(states, actions) td_errors = target_q_values - q_values if self.prioritized_replay: - is_weights = torch.from_numpy(np.asarray(batch.is_weights)).to(self.device) + is_weights = torch.from_numpy(batch["is_weights"]).to(self.device) loss = (td_errors * is_weights).mean() + loss_info["td_errors"], loss_info["indexes"] = td_errors, batch["indexes"] else: loss = self._loss_func(q_values, target_q_values) - grad = self.q_net.get_gradients(loss) if explicit_grad else None - return self.LossInfo(loss, td_errors, batch.indexes, grad=grad) + loss_info["loss"] = loss + if explicit_grad: + loss_info["grad"] = self.q_net.get_gradients(loss) + return loss_info - def update_with_multi_loss_info(self, loss_info_list: List[LossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[dict]): if self.prioritized_replay: for loss_info in loss_info_list: - self._per.update(loss_info.indexes, loss_info.td_errors) + self._per.update(loss_info["indexes"], loss_info["td_errors"]) - self.q_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) + self.q_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) self._q_net_version += 1 if self._q_net_version - self._target_q_net_version == self.update_target_every: self._update_target() @@ -315,6 +284,8 @@ def learn_from_multi_trajectories(self, trajectories: List[dict]): else: for _ in range(self.num_epochs): loss_info = self.get_batch_loss(self._sample()) + if self.prioritized_replay: + self._per.update(loss_info["indexes"], loss_info["td_errors"]) self.q_net.step(loss_info.loss) self._q_net_version += 1 if self._q_net_version - self._target_q_net_version == self.update_target_every: diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 888625939..1f82ba315 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -17,11 +17,15 @@ class AbsPolicy(ABC): def __init__(self, name: str): super().__init__() self._name = name + self.agents = set() @property def name(self): return self._name + def add_agent(self, agent: str): + self.agents.add(agent) + @abstractmethod def choose_action(self, state): raise NotImplementedError @@ -37,14 +41,6 @@ def choose_action(self, state): class RLPolicy(AbsPolicy): - """Policy that can update itself using simulation experiences. - - Reinforcement learning (RL) policies should inherit from this. - - Args: - name (str): Name of the policy. - """ - class Buffer: """Sequence of transitions for an agent. @@ -67,41 +63,21 @@ def __init__(self, state_dim: int, action_dim: int = 1, max_len: int = 10000): self._ptr = 0 self._last_ptr = 0 - def store(self, state: np.ndarray, action, reward: float, terminal: bool = False): - self.states[self._ptr] = state - self.actions[self._ptr] = action - self.rewards[self._ptr] = reward - self.terminal[self._ptr] = terminal - # increment pointer - self._ptr += 1 - if self._ptr == self.max_len: - self._ptr = 0 + @abstractmethod + def put(self, transition): + raise NotImplementedError + @abstractmethod def get(self): - traj_slice = slice(self._last_ptr, self._ptr) - self._last_ptr = self._ptr - return { - "states": self.states[traj_slice], - "actions": self.actions[traj_slice], - "rewards": self.rewards[traj_slice], - "terminal": self.terminal[traj_slice], - } - - - class Batch: - def __init__(self): - pass + raise NotImplementedError + """Policy that learns from simulation experiences. - class LossInfo: - - __slots__ = ["loss", "grad"] - - def __init__(self, loss, grad): - self.loss = loss - self.grad = grad - + Reinforcement learning (RL) policies should inherit from this. + Args: + name (str): Name of the policy. + """ def __init__(self, name: str, remote: bool = False): super().__init__(name) self.remote = remote @@ -110,19 +86,23 @@ def __init__(self, name: str, remote: bool = False): def choose_action(self, state): raise NotImplementedError - def get_rollout_info(self, trajectory: Trajectory): - return trajectory + def record(self, key: str, state, action, reward, next_state, terminal: bool): + pass + + @abstractmethod + def get_rollout_info(self): + raise NotImplementedError @abstractmethod - def get_batch_loss(self, batch: Batch, explicit_grad: bool = False): + def get_batch_loss(self, batch: dict, explicit_grad: bool = False): raise NotImplementedError @abstractmethod - def update_with_multi_loss_info(self, loss_info_list: List[LossInfo]): + def update_with_multi_loss_info(self, loss_info_list: List[dict]): pass @abstractmethod - def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): + def learn_from_multi_trajectories(self, trajectories: List[dict]): """Perform policy improvement based on a list of trajectories obtained from parallel rollouts.""" raise NotImplementedError diff --git a/maro/rl/policy/replay.py b/maro/rl/policy/replay.py index 0ef9d9535..17a1fd9ae 100644 --- a/maro/rl/policy/replay.py +++ b/maro/rl/policy/replay.py @@ -29,7 +29,7 @@ def __init__(self, capacity: int, state_dim: int, action_dim: int = 1, random_ov self.actions = np.zeros(self._capacity, dtype=np.float32) self.rewards = np.zeros(self._capacity, dtype=np.float32) self.next_states = np.zeros(self._capacity, dtype=np.float32) - self.terminal = np.zeros(self._capacity, dtype=np.bool) + self.terminals = np.zeros(self._capacity, dtype=np.bool) self._ptr = 0 @property @@ -47,9 +47,16 @@ def size(self): """Current number of experiences stored.""" return self._ptr - def put(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next_states: np.ndarray): + def put( + self, + states: np.ndarray, + actions: np.ndarray, + rewards: np.ndarray, + next_states: np.ndarray, + terminals: np.ndarray + ): """Put SARS and terminal flags in the memory.""" - assert len(states) == len(actions) == len(rewards) == len(next_states) + assert len(states) == len(actions) == len(rewards) == len(next_states) == len(terminals) added = len(states) if added > self._capacity: raise ValueError("size of added items should not exceed the capacity.") @@ -72,3 +79,12 @@ def put(self, states: np.ndarray, actions: np.ndarray, rewards: np.ndarray, next self._ptr = min(self._ptr + added, self._capacity) return indexes + + def sample(self, size: int): + indexes = np.random.choice(self._ptr, size=size) + return { + "states": self.states[indexes], + "actions": self.actions[indexes], + "rewards": self.rewards[indexes], + "next_states": self.next_states[indexes] + } diff --git a/maro/rl/types.py b/maro/rl/types.py deleted file mode 100644 index ab6b433b4..000000000 --- a/maro/rl/types.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import List - -import numpy as np - - -class State: - """Convenience class to be used in an environment wrapper's post-step processing function. - - Args: - state: Output of the environment wrapper's ``get_state``. - action: Output of the ``AgentWrapper`` that interacts with environment wrapper. - env_action: Output of the environment wrapper's ``to_env_action``. - reward: Output of the environmet wrapper's ``get_reward``. - next_state: The state immediately following ``state``. - info: Output of the environment wrapper's ``get_transition_info``. - """ - - __slots__ = ["input", "meta"] - - def __init__(self, input: np.ndarray, meta=None): - self.input = input - self.meta = meta - - def - - -class Transition: - """Convenience class to be used in an environment wrapper's post-step processing function. - - Args: - state: ``State`` object generated by the environment wrapper's ``get_state``. - action: Output of the ``AgentWrapper`` that interacts with environment wrapper. - env_action: Output of the environment wrapper's ``to_env_action``. - reward: Output of the environmet wrapper's ``get_reward``. - next_state: The state immediately following ``state``. - info: Output of the environment wrapper's ``get_transition_info``. - """ - - __slots__ = ["state", "action", "env_action", "reward", "next_state", "info"] - - def __init__(self, state: State, action, env_action, reward, next_state: State, info): - self.state = state - self.action = action - self.env_action = env_action - self.reward = reward - self.next_state = next_state - self.info = info - - diff --git a/maro/rl/types/__init__.py b/maro/rl/types/__init__.py deleted file mode 100644 index b286cd8f0..000000000 --- a/maro/rl/types/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .model_types import ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet -from .rollout_object_types import Trajectory, Transition - -__all__ = [ - "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet", - "Trajectory", "Transition" -] diff --git a/maro/rl/types/model_types.py b/maro/rl/types/model_types.py deleted file mode 100644 index bb468a9da..000000000 --- a/maro/rl/types/model_types.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import abstractmethod -from typing import Dict, Union - -import torch -from torch import nn -from torch.distributions import Categorical - -from maro.rl.model import AbsCoreModel, OptimOption - - -class DiscreteACNet(AbsCoreModel): - """Model container for the actor-critic architecture for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @abstractmethod - def forward(self, states, actor: bool = True, critic: bool = True) -> tuple: - """Compute action probabilities and values for a state batch. - - The output is a tuple of (action_probs, values), where action probs is a tensor of shape - (batch_size, action_space_size) and values is a tensor of shape (batch_size,). If only one - of these two is needed, the return value for the other one can be set to None. - - Args: - states: State batch to compute action probabilities and values for. - actor (bool): If True, the first element of the output will be actin probabilities. Defaults to True. - critic (bool): If True, the second element of the output will be state values. Defaults to True. - """ - raise NotImplementedError - - def get_action(self, states, max_prob: bool = False): - """ - Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value - for each state. - """ - action_probs, values = self.forward(states) - if max_prob: - probs, actions = action_probs.max(dim=1) - return actions, torch.log(probs), values - else: - action_probs = Categorical(action_probs) # (batch_size, action_space_size) - actions = action_probs.sample() - logps = action_probs.log_prob(actions) - return actions, logps, values - - -class DiscretePolicyNet(AbsCoreModel): - """Parameterized policy for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @abstractmethod - def forward(self, states) -> torch.tensor: - """Compute action probabilities corresponding to each state in ``states``. - - The output must be a torch tensor with shape (batch_size, action_space_size). - - Args: - states: State batch to compute action probabilities for. - """ - raise NotImplementedError - - def get_action(self, states, max_prob: bool = False): - """ - Given a batch of states, return actions selected based on the probabilities computed by ``forward`` - and the corresponding log probabilities. - """ - action_prob = self.forward(states) # (batch_size, num_actions) - if max_prob: - prob, action = action_prob.max(dim=1) - return action, torch.log(prob) - else: - action_prob = Categorical(action_prob) # (batch_size, action_space_size) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - return action, log_p - - -class DiscreteQNet(AbsCoreModel): - """Q-value model for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @abstractmethod - def forward(self, states) -> torch.tensor: - """Compute the Q-values for all actions as a tensor of shape (batch_size, action_space_size).""" - raise NotImplementedError - - def get_action(self, states): - """ - Given Q-values for a batch of states and all actions, return the action index and the corresponding - Q-values for each state. - """ - q_for_all_actions = self.forward(states) # (batch_size, num_actions) - greedy_q, actions = q_for_all_actions.max(dim=1) - return actions.detach(), greedy_q.detach(), q_for_all_actions.shape[1] - - def q_values(self, states, actions: torch.tensor): - """Return the Q-values for a batch of states and actions.""" - if len(actions.shape) == 1: - actions = actions.unsqueeze(dim=1) - q_for_all_actions = self.forward(states) # (batch_size, num_actions) - return q_for_all_actions.gather(1, actions).squeeze(dim=1) - - def soft_update(self, other_model: nn.Module, tau: float): - """Soft-update model parameters using another model. - - The update formulae is: param = (1 - tau) * param + tau * pther_param. - - Args: - other_model: The model to update the current model with. - tau (float): Soft-update coefficient. - """ - for params, other_params in zip(self.parameters(), other_model.parameters()): - params.data = (1 - tau) * params.data + tau * other_params.data - - -class ContinuousACNet(AbsCoreModel): - """Model container for the actor-critic architecture for continuous action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @abstractmethod - def forward(self, states, actions=None): - """Compute actions for a batch of states or Q-values for a batch of states and actions. - - Args: - states: State batch to compute the Q-values for. - actions: Action batch. If None, the output should be a batch of actions corresponding to - the state batch. Otherwise, the output should be the Q-values for the given states and - actions. Defaults to None. - """ - raise NotImplementedError - - def get_action(self, states): - """Compute actions given a batch of states.""" - return self.forward(states) - - def value(self, states): - """Compute the Q-values for a batch of states using the actions computed from them.""" - return self.forward(states, actions=self.get_action(states)) - - def soft_update(self, other_model: nn.Module, tau: float): - """Soft-update model parameters using another model. - - The update formulae is: param = (1 - tau) * param + tau * pther_param. - - Args: - other_model: The model to update the current model with. - tau (float): Soft-update coefficient. - """ - for params, other_params in zip(self.parameters(), other_model.parameters()): - params.data = (1 - tau) * params.data + tau * other_params.data diff --git a/maro/rl/types/rollout_object_types.py b/maro/rl/types/rollout_object_types.py deleted file mode 100644 index ce9093029..000000000 --- a/maro/rl/types/rollout_object_types.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -class Transition: - """Convenience class to be used in an environment wrapper's post-step processing function. - - Args: - state: Output of the environment wrapper's ``get_state``. - action: Output of the ``AgentWrapper`` that interacts with environment wrapper. - env_action: Output of the environment wrapper's ``to_env_action``. - reward: Output of the environmet wrapper's ``get_reward``. - next_state: The state immediately following ``state``. - info: Output of the environment wrapper's ``get_transition_info``. - """ - - __slots__ = ["state", "action", "env_action", "reward", "next_state", "info"] - - def __init__(self, state, action, env_action, reward, next_state, info): - self.state = state - self.action = action - self.env_action = env_action - self.reward = reward - self.next_state = next_state - self.info = info - - -class Trajectory: - - __slots__ = ["states", "actions", "rewards", "info", "length"] - - def __init__(self, states: list, actions: list, rewards: list, info: list): - assert len(states) == len(actions) == len(rewards) + 1 == len(info) + 1 - self.states = states - self.actions = actions - self.rewards = rewards - self.info = info - self.length = len(rewards) From af1eed6ab6af540ec3a25a8af336e65ba34ea8b1 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 27 Aug 2021 13:22:00 +0000 Subject: [PATCH 419/482] bug fixes --- maro/rl/learning/env_sampler.py | 54 ++++++++++++++++----------------- maro/rl/learning/learner.py | 7 ++--- maro/rl/policy/dqn.py | 8 +++-- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 2c46f2428..1c0154776 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -4,7 +4,7 @@ from abc import abstractmethod from collections import defaultdict, deque from os import getcwd -from typing import Callable, Dict +from typing import Callable, Dict, List from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.policy import AbsPolicy, RLPolicy @@ -40,54 +40,50 @@ class EnvSampler: """ def __init__( self, - get_env: Callable[[], Env], - get_policy_func_dict: Dict[str, AbsPolicy], + env: Env, + policies: List[AbsPolicy], get_state_func_dict: Dict[str, Callable], - get_eval_env: Callable[[], Env] = None, + eval_env: Env = None, reward_eval_delay: int = 0, - max_trajectory_len: Dict[str, int] = None, post_step: Callable = None, ): - self.env = get_env() - self.eval_env = get_eval_env() if get_eval_env else self.env - self.policy_dict = {name: func(name) for name, func in get_policy_func_dict.items()} - self.policy = {} - for policy in self.policy_dict.values(): + unbound_agents = set(self.env.agent_idx_list) + self.policy_dict = {} + self.policy_by_agent = {} + for policy in policies: + self.policy_dict[policy.name] = policy for agent in policy.agents: - self.policy[agent] = policy + if agent in self.policy_by_agent: + raise Exception(f"Agent {agent} is already bound to a policy") + self.policy_by_agent[agent] = policy + unbound_agents.remove(agent) - self.get_state = get_state_func_dict + if unbound_agents: + raise Exception(f"Agents {unbound_agents} are not bound to any policy") + self.env = env + self.eval_env = eval_env if eval_env else self.env + self.get_state = get_state_func_dict self.reward_eval_delay = reward_eval_delay self._post_step = post_step - self.max_trajectory_len = max_trajectory_len if max_trajectory_len else defaultdict(lambda: self.env._durations) - self._replay_buffer = { - agent_id: policy.Buffer(self.state_dim[agent_id], ) - for agent_id, policy in self.policy.items() if isinstance(policy, RLPolicy) - } - self._transition_cache = defaultdict(deque()) # for caching transitions whose rewards have yet to be evaluated self._step_index = None self._terminal = False + self._transition_cache = defaultdict(deque()) # for caching transitions whose rewards have yet to be evaluated self.tracker = {} # User-defined tracking information is placed here. - self.replay = True @property def step_index(self): """Number of environmental steps taken so far.""" return self._step_index - @property - def agent_idx_list(self): - return self.env.agent_idx_list - @property def summary(self): return self.env.metrics def get_agents_from_event(self, event): - return self.agent_idx_list + return self.env.agent_idx_list def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): # set policy states @@ -114,7 +110,7 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio starting_step_index = self._step_index + 1 steps_to_go = float("inf") if num_steps == -1 else num_steps while not self._terminal and steps_to_go > 0: - action = {agent: self.policy[agent].choose_action(st) for agent, st in self._state.items()} + action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in self._state.items()} env_action = self.to_env_action(action) _, self._event, self._terminal = self.env.step(env_action) prev_state = self._state @@ -136,8 +132,10 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio # put things you want to track in the tracker attribute self._post_step(self.env, self.tracker, state, action, env_action, reward, state_, tick) - if isinstance(self.policy[agent], RLPolicy): - self.policy[agent].record(agent, state, action, reward, state_, not cache and self._terminal) + if isinstance(self.policy_by_agent[agent], RLPolicy): + self.policy_by_agent[agent].record( + agent, state, action, reward, state_, not cache and self._terminal + ) return { "rollout_info": { @@ -170,7 +168,7 @@ def test(self, policy_state_dict: dict = None): _, event, _ = self.eval_env.step(None) state = {agent: self.get_state[agent](self.eval_env) for agent in self.get_agents_from_event(event)} while not terminal: - action = {agent: self.policy[agent].choose_action(st) for agent, st in state.items()} + action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in state.items()} env_action = self.to_env_action(action) _, event, terminal = self.eval_env.step(env_action) state = {agent: self.get_state[agent](self.eval_env) for agent in self.get_agents_from_event(event)} diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py index e4a19ff68..f037ad5f7 100644 --- a/maro/rl/learning/learner.py +++ b/maro/rl/learning/learner.py @@ -5,14 +5,11 @@ from os import getcwd from typing import Callable, List, Union -from maro.rl.policy import LossInfo -from maro.rl.types import Trajectory -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger from .common import get_eval_schedule, get_rollout_finish_msg from .early_stopper import AbsEarlyStopper -from .environment_sampler import EnvironmentSampler +from .env_sampler import EnvSampler from .policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager @@ -68,7 +65,7 @@ def simple_learner( raise ValueError("num_steps must be a positive integer or -1") logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) - env_sampler = EnvironmentSampler(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + env_sampler = EnvSampler(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) # evaluation schedule eval_schedule = get_eval_schedule(eval_schedule, num_episodes, final=eval_after_last_episode) diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index 6edbf7657..b08105ca0 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -151,6 +151,7 @@ def __init__( replay_memory_capacity: int = 10000, random_overwrite: bool = False, batch_size: int = 32, + rollout_info_size: int = 1000, prioritized_replay_kwargs: dict = None, remote: bool = False ): @@ -178,6 +179,7 @@ def __init__( replay_memory_capacity, self.q_net.input_dim, action_dim=1, random_overwrite=random_overwrite ) self.batch_size = batch_size + self.rollout_info_size = rollout_info_size self.prioritized_replay = prioritized_replay_kwargs is not None if self.prioritized_replay: self._per = PrioritizedExperienceReplay(self._replay_memory, **prioritized_replay_kwargs) @@ -214,9 +216,9 @@ def record( self._per.set_max_priority(indexes) def get_rollout_info(self): - return self.get_batch_loss(self._get_batch(), explicit_grad=True) + return self._replay_memory.sample(self.rollout_info_size) - def _sample(self): + def _get_batch(self): if self.prioritized_replay: indexes, is_weights = self._per.sample(self.batch_size) return { @@ -283,7 +285,7 @@ def learn_from_multi_trajectories(self, trajectories: List[dict]): pass else: for _ in range(self.num_epochs): - loss_info = self.get_batch_loss(self._sample()) + loss_info = self.get_batch_loss(self._get_batch()) if self.prioritized_replay: self._per.update(loss_info["indexes"], loss_info["td_errors"]) self.q_net.step(loss_info.loss) From e9244955e0d5e56e56f234b7ce92d98c1159b82c Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 29 Aug 2021 16:47:39 +0000 Subject: [PATCH 420/482] numpy optimization and associated refactoring --- docs/source/examples/multi_agent_dqn_cim.rst | 14 +- docs/source/key_components/rl_toolkit.rst | 6 +- examples/rl/README.md | 2 +- examples/rl/cim/__init__.py | 8 +- examples/rl/cim/ac.py | 87 --------- examples/rl/cim/callbacks.py | 5 + examples/rl/cim/config.py | 123 ++++++++++++ examples/rl/cim/dqn.py | 85 --------- examples/rl/cim/env_sampler.py | 114 +++++++++++ examples/rl/cim/env_wrapper.py | 145 -------------- examples/rl/cim/policies.py | 75 ++++++++ examples/rl/cim/policy_index.py | 25 --- examples/rl/vm_scheduling/__init__.py | 4 +- examples/rl/vm_scheduling/ac.py | 6 +- examples/rl/vm_scheduling/dqn.py | 4 +- examples/rl/vm_scheduling/env_wrapper.py | 4 +- examples/rl/workflows/config.yml | 4 +- examples/rl/workflows/general.py | 23 +-- examples/rl/workflows/learner.py | 7 +- examples/rl/workflows/policy_host.py | 4 +- examples/rl/workflows/policy_manager.py | 9 +- examples/rl/workflows/rollout.py | 16 +- examples/rl/workflows/simple_learner.py | 4 +- maro/rl/learning/__init__.py | 4 +- maro/rl/learning/env_sampler.py | 178 +++++++++--------- maro/rl/learning/learner.py | 20 +- maro/rl/learning/policy_manager.py | 62 +++--- maro/rl/learning/rollout_manager.py | 76 ++++---- maro/rl/modeling/__init__.py | 4 +- maro/rl/modeling/fc_block.py | 2 +- maro/rl/policy/__init__.py | 22 +-- maro/rl/policy/ac.py | 66 +++---- maro/rl/policy/ddpg.py | 143 ++++++-------- maro/rl/policy/dqn.py | 72 ++++--- maro/rl/policy/pg.py | 137 ++++++++------ maro/rl/policy/policy.py | 38 +--- maro/rl/policy/replay.py | 7 +- .../rl_formulation.ipynb | 26 +-- 38 files changed, 775 insertions(+), 856 deletions(-) delete mode 100644 examples/rl/cim/ac.py create mode 100644 examples/rl/cim/config.py delete mode 100644 examples/rl/cim/dqn.py create mode 100644 examples/rl/cim/env_sampler.py delete mode 100644 examples/rl/cim/env_wrapper.py create mode 100644 examples/rl/cim/policies.py delete mode 100644 examples/rl/cim/policy_index.py diff --git a/docs/source/examples/multi_agent_dqn_cim.rst b/docs/source/examples/multi_agent_dqn_cim.rst index 8df2015a4..ef6302117 100644 --- a/docs/source/examples/multi_agent_dqn_cim.rst +++ b/docs/source/examples/multi_agent_dqn_cim.rst @@ -25,13 +25,13 @@ in the roll-out loop. In this example, .. code-block:: python class CIMTrajectoryForDQN(Trajectory): def __init__( - self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream, + self, env, *, port_features, vessel_features, action_space, look_back, max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, finite_vessel_space=True, has_early_discharge=True ): super().__init__(env) - self.port_attributes = port_attributes - self.vessel_attributes = vessel_attributes + self.port_features = port_features + self.vessel_features = vessel_features self.action_space = action_space self.look_back = look_back self.max_ports_downstream = max_ports_downstream @@ -47,8 +47,8 @@ in the roll-out loop. In this example, tick, port_idx, vessel_idx = event.tick, event.port_idx, event.vessel_idx ticks = [max(0, tick - rt) for rt in range(self.look_back - 1)] future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') - port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] - vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] + port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_features] + vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_features] return {port_idx: np.concatenate((port_features, vessel_features))} def get_action(self, action_by_agent, event): @@ -57,7 +57,7 @@ in the roll-out loop. In this example, model_action = action_info[0] if isinstance(action_info, tuple) else action_info scope, tick, port, vessel = event.action_scope, event.tick, event.port_idx, event.vessel_idx zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. - vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") + vessel_space = vessel_snapshots[tick:vessel:self.vessel_features][2] if self.finite_vessel_space else float("inf") early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 percent = abs(self.action_space[model_action]) @@ -122,7 +122,7 @@ The out-of-the-box DQN is used as our agent. def get_dqn_agent(): q_model = SimpleMultiHeadModel( - FullyConnectedBlock(**agent_config["model"]), optim_option=agent_config["optimization"] + FullyConnected(**agent_config["model"]), optim_option=agent_config["optimization"] ) return DQN(q_model, DQNConfig(**agent_config["hyper_params"])) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index bef7119b8..175cf032d 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -149,9 +149,9 @@ The code snippet below shows how to create a model for the actor-critic algorith ) - representation_stack = FullyConnectedBlock(...) - actor_head = FullyConnectedBlock(...) - critic_head = FullyConnectedBlock(...) + representation_stack = FullyConnected(...) + actor_head = FullyConnected(...) + critic_head = FullyConnected(...) ac_model = SimpleMultiHeadModel( {"representation": representation_stack, "actor": actor_head, "critic": critic_head}, optim_option={ diff --git a/examples/rl/README.md b/examples/rl/README.md index e84fa1cdb..f29649d50 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -11,7 +11,7 @@ To run the distributed workflow, start by choosing "sync" or "async" for the ``m ## Write Your Own Scenarios To use the workflow provided under ``workflows``, the following ingredients are required: -* ``get_env_wrapper``, a function that takes no parameters and returns an environment wrapper instance. +* ``get_env_sampler``, a function that takes no parameters and returns an environment wrapper instance. * ``get_agent_wrapper``, a function that takes no parameters and returns an agent wrapper instance. * ``policy_func_index``, a dictionary mapping policy names to functions that create them. The policy-creating functions should take as its sole parameter a flag indicating whether the created policy is for roll-out or training. diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index 8560e95b0..280488bcc 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -2,9 +2,7 @@ # Licensed under the MIT license. from .callbacks import post_collect, post_evaluate -from .env_wrapper import get_env_wrapper -from .policy_index import agent2policy, rl_policy_func_index +from .env_sampler import get_env_sampler +from .policies import policy_func_dict -__all__ = [ - "agent2policy", "post_collect", "post_evaluate", "get_env_wrapper", "rl_policy_func_index", -] +__all__ = ["post_collect", "post_evaluate", "get_env_sampler", "policy_func_dict"] diff --git a/examples/rl/cim/ac.py b/examples/rl/cim/ac.py deleted file mode 100644 index 605efcfb0..000000000 --- a/examples/rl/cim/ac.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -import numpy as np -import torch - -from maro.rl.model import FullyConnectedBlock, OptimOption -from maro.rl.policy import ActorCritic, DiscreteACNet - -cim_path = os.path.dirname(os.path.realpath(__file__)) -sys.path.insert(0, cim_path) -from env_wrapper import STATE_DIM, env_config - -model_config = { - "network": { - "actor": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64], - "output_dim": env_config["wrapper"]["num_actions"], - "activation": "tanh", - "softmax": True, - "batch_norm": False, - "head": True - }, - "critic": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64], - "output_dim": 1, - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "head": True - } - }, - "optimization": { - "actor": { - "optim_cls": "adam", - "optim_params": {"lr": 0.001} - }, - "critic": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.001} - } - } -} - -ac_config = { - "reward_discount": .0, - "grad_iters": 10, - "critic_loss_cls": "smooth_l1", - "min_logp": None, - "critic_loss_coeff": 0.1, - "entropy_coeff": 0.01, - # "clip_ratio": 0.8 # for PPO - "lam": 0.9, - "get_loss_on_rollout_finish": True -} - - -def get_ac_policy(name: str, remote: bool = False): - class MyACNET(DiscreteACNet): - def forward(self, states, actor: bool = True, critic: bool = True): - states = torch.from_numpy(np.asarray(states)) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - - states = states.to(self.device) - return ( - self.component["actor"](states) if actor else None, - self.component["critic"](states) if critic else None - ) - - ac_net = MyACNET( - component={ - "actor": FullyConnectedBlock(**model_config["network"]["actor"]), - "critic": FullyConnectedBlock(**model_config["network"]["critic"]) - }, - optim_option={ - "actor": OptimOption(**model_config["optimization"]["actor"]), - "critic": OptimOption(**model_config["optimization"]["critic"]) - } - ) - - return ActorCritic(name, ac_net, **ac_config, remote=remote) diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py index b64b1604c..0d56e71c4 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/rl/cim/callbacks.py @@ -12,6 +12,11 @@ simulation_logger = Logger("SIMUALTION", dump_folder=log_dir) + +def post_step(env, tracker, state, action, env_action, reward, tick): + tracker["env_metric"] = env.metrics + + def post_collect(trackers, ep, segment): # print the env metric from each rollout worker for tracker in trackers: diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py new file mode 100644 index 000000000..aef59901b --- /dev/null +++ b/examples/rl/cim/config.py @@ -0,0 +1,123 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +env_conf = { + "scenario": "cim", + "topology": "toy.4p_ssdd_l0.0", + "durations": 560 +} + +env_sampler_conf = { + "port_features": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], + "vessel_features": ["empty", "full", "remaining_space"], + "num_actions": 21, + # Parameters for computing states + "look_back": 7, + "max_ports_downstream": 2, + # Parameters for computing actions + "finite_vessel_space": True, + "has_early_discharge": True, + # Parameters for computing rewards + "reward_eval_delay": 99, + "fulfillment_factor": 1.0, + "shortage_factor": 1.0, + "time_decay": 0.97 +} + +# obtain state dimension from a temporary env_wrapper instance +state_dim = ( + (env_sampler_conf["look_back"] + 1) * (env_sampler_conf["max_ports_downstream"] + 1) * + len(env_sampler_conf["port_features"]) + len(env_sampler_conf["vessel_features"]) +) + +# DQN settings +q_net_conf = { + "network": { + "input_dim": state_dim, + "hidden_dims": [256, 128, 64, 32], + "output_dim": env_sampler_conf["num_actions"], + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 + }, + "optimization": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.05} + } +} + +dqn_conf = { + "reward_discount": .0, + "update_target_every": 5, + "num_epochs": 10, + "soft_update_coeff": 0.1, + "double": False, + "replay_memory_capacity": 10000, + "random_overwrite": False, + "rollout_batch_size": 128, + "train_batch_size": 32, + # "prioritized_replay_kwargs": { + # "alpha": 0.6, + # "beta": 0.4, + # "beta_step": 0.001, + # "max_priority": 1e8 + # } +} + +exploration_conf = { + "last_ep": 10, + "initial_value": 0.4, + "final_value": 0.0, + "splits": [(5, 0.32)] +} + + +# AC settings +ac_net_conf = { + "network": { + "actor": { + "input_dim": state_dim, + "hidden_dims": [256, 128, 64], + "output_dim": env_sampler_conf["num_actions"], + "activation": "tanh", + "softmax": True, + "batch_norm": False, + "head": True + }, + "critic": { + "input_dim": state_dim, + "hidden_dims": [256, 128, 64], + "output_dim": 1, + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "head": True + } + }, + "optimization": { + "actor": { + "optim_cls": "adam", + "optim_params": {"lr": 0.001} + }, + "critic": { + "optim_cls": "rmsprop", + "optim_params": {"lr": 0.001} + } + } +} + +ac_conf = { + "reward_discount": .0, + "grad_iters": 10, + "critic_loss_cls": "smooth_l1", + "min_logp": None, + "critic_loss_coeff": 0.1, + "entropy_coeff": 0.01, + # "clip_ratio": 0.8 # for PPO + "lam": 0.9, + "get_loss_on_rollout": True +} diff --git a/examples/rl/cim/dqn.py b/examples/rl/cim/dqn.py deleted file mode 100644 index 65303587f..000000000 --- a/examples/rl/cim/dqn.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -import numpy as np -import torch -import torch.nn as nn - -from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler -from maro.rl.model import FullyConnectedBlock, OptimOption -from maro.rl.policy import DQN, DiscreteQNet - -cim_path = os.path.dirname(os.path.realpath(__file__)) -if cim_path not in sys.path: - sys.path.insert(0, cim_path) -from env_wrapper import STATE_DIM, env_config - -model_config = { - "network": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 64, 32], - "output_dim": env_config["wrapper"]["num_actions"], - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.05} - } -} - -dqn_config = { - "reward_discount": .0, - "update_target_every": 5, - "num_epochs": 10, - "soft_update_coeff": 0.1, - "double": False, - "replay_memory_capacity": 10000, - "random_overwrite": False, - "prioritized_replay_kwargs": { - "batch_size": 32, - "alpha": 0.6, - "beta": 0.4, - "beta_step": 0.001, - "max_priority": 1e8 - } -} - -exploration_config = { - "last_ep": 10, - "initial_value": 0.4, - "final_value": 0.0, - "splits": [(5, 0.32)] -} - - -class QNet(DiscreteQNet): - def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): - super().__init__(component, optim_option=optim_option, device=device) - - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component(states) - - -def get_dqn_policy(name: str, remote: bool = False): - qnet = QNet( - FullyConnectedBlock(**model_config["network"]), - optim_option=OptimOption(**model_config["optimization"]) - ) - exploration = EpsilonGreedyExploration() - exploration.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, - param_name="epsilon", - **exploration_config - ) - return DQN(name, qnet, exploration=exploration, **dqn_config, remote=remote) diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py new file mode 100644 index 000000000..7300aa7f6 --- /dev/null +++ b/examples/rl/cim/env_sampler.py @@ -0,0 +1,114 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +import numpy as np + +from maro.rl.learning import AbsEnvSampler +from maro.simulator import Env +from maro.simulator.scenarios.cim.common import Action, ActionType + +cim_path = os.path.dirname(os.path.realpath(__file__)) +if cim_path not in sys.path: + sys.path.insert(0, cim_path) + +from callbacks import post_step +from config import env_conf, env_sampler_conf +from policies import policy_func_dict + + +class CIMEnvSampler(AbsEnvSampler): + def __init__( + self, get_env, get_policy_func_dict, agent2policy, get_eval_env=None, reward_eval_delay=99, post_step=None + ): + super().__init__( + get_env, get_policy_func_dict, agent2policy, + get_eval_env=get_eval_env, reward_eval_delay=reward_eval_delay, post_step=post_step + ) + self.action_space = list(np.linspace(-1.0, 1.0, env_sampler_conf["num_actions"])) + self._last_action_tick = None + self._state_info = None + + def get_state(self, event, tick=None): + if tick is None: + tick = self.env.tick + vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] + port_idx, vessel_idx = event.port_idx, event.vessel_idx + ticks = [max(0, tick - rt) for rt in range(env_sampler_conf["look_back"] - 1)] + future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') + port_features = port_snapshots[ticks: [port_idx] + list(future_port_list): env_sampler_conf["port_features"]] + vessel_features = vessel_snapshots[tick: vessel_idx: env_sampler_conf["vessel_features"]] + self._state_info = {"tick": tick, "action_scope": event.action_scope, "vessel_idx": vessel_idx} + state = np.concatenate((port_features, vessel_features)) + self._last_action_tick = tick + return {port_idx: state} + + def to_env_action(self, action_by_agent: dict): + port_idx, action = list(action_by_agent.items()).pop() + tick = self._state_info["tick"] + vessel = self._state_info["vessel_idx"] + action_scope = self._state_info["action_scope"] + vessel_snapshots = self.env.snapshot_list["vessels"] + vessel_space = ( + vessel_snapshots[tick:vessel:env_sampler_conf["vessel_features"]][2] + if env_sampler_conf["finite_vessel_space"] else float("inf") + ) + + model_action = action["action"] if isinstance(action, dict) else action + percent = abs(self.action_space[model_action]) + zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. + if model_action < zero_action_idx: + action_type = ActionType.LOAD + actual_action = min(round(percent * action_scope.load), vessel_space) + elif model_action > zero_action_idx: + action_type = ActionType.DISCHARGE + if env_sampler_conf["has_early_discharge"]: + early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] + else: + early_discharge = 0 + plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge + actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) + else: + actual_action, action_type = 0, None + + return [Action(port_idx=port_idx, vessel_idx=vessel, quantity=actual_action, action_type=action_type)] + + def get_reward(self, actions, tick=None): + """Delayed reward evaluation.""" + if tick is None: + tick = self._last_action_tick + start_tick = tick + 1 + ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) + + # Get the ports that took actions at the given tick + ports = [action.port_idx for action in actions] + port_snapshots = self.env.snapshot_list["ports"] + future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) + future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) + + decay_list = [env_sampler_conf["time_decay"] ** i for i in range(self.reward_eval_delay)] + rewards = np.float32( + env_sampler_conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) + - env_sampler_conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) + ) + return {agent_id: reward for agent_id, reward in zip(ports, rewards)} + + +agent2policy = { + 0: "ac.0", + 1: "ac.1", + 2: "dqn", + 3: "ac.2" +} + + +def get_env_sampler(): + return CIMEnvSampler( + lambda: Env(**env_conf), + policy_func_dict, + agent2policy, + reward_eval_delay=env_sampler_conf["reward_eval_delay"], + post_step=post_step + ) diff --git a/examples/rl/cim/env_wrapper.py b/examples/rl/cim/env_wrapper.py deleted file mode 100644 index 37f7b2a40..000000000 --- a/examples/rl/cim/env_wrapper.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import numpy as np - -from maro.rl.policy import ACActionInfo -from maro.rl.wrappers import AbsEnvWrapper -from maro.simulator import Env -from maro.simulator.scenarios.cim.common import Action, ActionType - - -def post_step(env, tracker, transition): - tracker["env_metric"] = env.metrics - - -class CIMEnvWrapper(AbsEnvWrapper): - def __init__( - self, env, replay_agent_ids=None, *, port_attributes, vessel_attributes, num_actions, look_back, - max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, - finite_vessel_space=True, has_early_discharge=True - ): - super().__init__( - env, - replay_agent_ids=replay_agent_ids, - reward_eval_delay=reward_eval_delay, - post_step=post_step - ) - self.port_attributes = port_attributes - self.vessel_attributes = vessel_attributes - self.action_space = list(np.linspace(-1.0, 1.0, num_actions)) - self.look_back = look_back - self.max_ports_downstream = max_ports_downstream - self.fulfillment_factor = fulfillment_factor - self.shortage_factor = shortage_factor - self.time_decay = time_decay - self.finite_vessel_space = finite_vessel_space - self.has_early_discharge = has_early_discharge - self._last_action_tick = None - self._state_dim = ( - (self.look_back + 1) * (self.max_ports_downstream + 1) * len(self.port_attributes) - + len(self.vessel_attributes) - ) - self._state_info = None - - @property - def state_dim(self): - return self._state_dim - - def get_state(self, tick=None): - if tick is None: - tick = self.env.tick - vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] - port_idx, vessel_idx = self.event.port_idx, self.event.vessel_idx - ticks = [max(0, tick - rt) for rt in range(self.look_back - 1)] - future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') - port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] - vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] - self._state_info = { - port_idx: {"tick": tick, "action_scope": self.event.action_scope, "vessel_idx": vessel_idx} - } - state = np.concatenate((port_features, vessel_features)) - self._last_action_tick = tick - return {port_idx: state} - - def to_env_action(self, action_by_agent: dict): - env_action = [] - for agent_id, action_info in action_by_agent.items(): - tick = self._state_info[agent_id]["tick"] - vessel = self._state_info[agent_id]["vessel_idx"] - action_scope = self._state_info[agent_id]["action_scope"] - vessel_snapshots = self.env.snapshot_list["vessels"] - vessel_space = ( - vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") - ) - early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 - - model_action = action_info.action if isinstance(action_info, ACActionInfo) else action_info - percent = abs(self.action_space[model_action]) - zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. - if model_action < zero_action_idx: - action_type = ActionType.LOAD - actual_action = min(round(percent * action_scope.load), vessel_space) - elif model_action > zero_action_idx: - action_type = ActionType.DISCHARGE - plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge - actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) - else: - actual_action, action_type = 0, None - - env_action.append( - Action(port_idx=agent_id, vessel_idx=vessel, quantity=actual_action, action_type=action_type) - ) - - return env_action - - def get_reward(self, actions, tick=None): - """Delayed reward evaluation.""" - if tick is None: - tick = self._last_action_tick - start_tick = tick + 1 - ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) - - # Get the ports that took actions at the given tick - ports = [action.port_idx for action in actions] - port_snapshots = self.env.snapshot_list["ports"] - future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) - future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) - - decay_list = [self.time_decay ** i for i in range(self.reward_eval_delay)] - rewards = np.float32( - self.fulfillment_factor * np.dot(future_fulfillment.T, decay_list) - - self.shortage_factor * np.dot(future_shortage.T, decay_list) - ) - return {agent_id: reward for agent_id, reward in zip(ports, rewards)} - - -env_config = { - "basic": { - "scenario": "cim", - "topology": "toy.4p_ssdd_l0.0", - "durations": 560 - }, - "wrapper": { - "port_attributes": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], - "vessel_attributes": ["empty", "full", "remaining_space"], - "num_actions": 21, - # Parameters for computing states - "look_back": 7, - "max_ports_downstream": 2, - # Parameters for computing actions - "finite_vessel_space": True, - "has_early_discharge": True, - # Parameters for computing rewards - "reward_eval_delay": 99, - "fulfillment_factor": 1.0, - "shortage_factor": 1.0, - "time_decay": 0.97 - } -} - -def get_env_wrapper(replay_agent_ids=None): - return CIMEnvWrapper(Env(**env_config["basic"]), replay_agent_ids=replay_agent_ids, **env_config["wrapper"]) - -# obtain state dimension from a temporary env_wrapper instance -STATE_DIM = get_env_wrapper().state_dim diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py new file mode 100644 index 000000000..77dab99d8 --- /dev/null +++ b/examples/rl/cim/policies.py @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import sys + +import numpy as np +import torch +import torch.nn as nn + +from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler +from maro.rl.modeling import DiscreteACNet, DiscreteQNet, FullyConnected, OptimOption +from maro.rl.policy import DQN, ActorCritic + +cim_path = os.path.dirname(os.path.realpath(__file__)) +if cim_path not in sys.path: + sys.path.insert(0, cim_path) +from config import ac_conf, ac_net_conf, dqn_conf, q_net_conf, exploration_conf, state_dim + + +class QNet(DiscreteQNet): + def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): + super().__init__(component, optim_option=optim_option, device=device) + + @property + def input_dim(self): + return state_dim + + def forward(self, states): + return self.component(states) + + +def get_dqn(name: str): + qnet = QNet(FullyConnected(**q_net_conf["network"]), optim_option=OptimOption(**q_net_conf["optimization"])) + exploration = EpsilonGreedyExploration() + exploration.register_schedule( + scheduler_cls=MultiPhaseLinearExplorationScheduler, + param_name="epsilon", + **exploration_conf + ) + return DQN(name, qnet, exploration=exploration, **dqn_conf) + + +def get_ac(name: str): + class MyACNET(DiscreteACNet): + @property + def input_dim(self): + return state_dim + + def forward(self, states, actor: bool = True, critic: bool = True): + return ( + self.component["actor"](states) if actor else None, + self.component["critic"](states) if critic else None + ) + + ac_net = MyACNET( + component={ + "actor": FullyConnected(**ac_net_conf["network"]["actor"]), + "critic": FullyConnected(**ac_net_conf["network"]["critic"]) + }, + optim_option={ + "actor": OptimOption(**ac_net_conf["optimization"]["actor"]), + "critic": OptimOption(**ac_net_conf["optimization"]["critic"]) + } + ) + + return ActorCritic(name, ac_net, **ac_conf) + + +policy_func_dict = { + "dqn": get_dqn, + "ac.0": get_ac, + "ac.1": get_ac, + "ac.2": get_ac +} diff --git a/examples/rl/cim/policy_index.py b/examples/rl/cim/policy_index.py deleted file mode 100644 index 970529121..000000000 --- a/examples/rl/cim/policy_index.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -cim_path = os.path.dirname(os.path.realpath(__file__)) -if cim_path not in sys.path: - sys.path.insert(0, cim_path) -from ac import get_ac_policy -from dqn import get_dqn_policy - -rl_policy_func_index = { - "dqn": get_dqn_policy, - "ac.0": get_ac_policy, - "ac.1": get_ac_policy, - "ac.2": get_ac_policy -} - -agent2policy = { - 0: "ac.0", - 1: "ac.1", - 2: "dqn", - 3: "ac.2" -} diff --git a/examples/rl/vm_scheduling/__init__.py b/examples/rl/vm_scheduling/__init__.py index 3055ab873..4c417a160 100644 --- a/examples/rl/vm_scheduling/__init__.py +++ b/examples/rl/vm_scheduling/__init__.py @@ -2,10 +2,10 @@ # Licensed under the MIT license. from .callbacks import post_collect, post_evaluate -from .env_wrapper import get_env_wrapper, get_eval_env_wrapper +from .env_wrapper import get_env_sampler, get_eval_env_wrapper from .policy_index import agent2policy, rl_policy_func_index, update_trigger, warmup __all__ = [ - "agent2policy", "post_collect", "post_evaluate", "get_env_wrapper", "get_eval_env_wrapper", + "agent2policy", "post_collect", "post_evaluate", "get_env_sampler", "get_eval_env_wrapper", "rl_policy_func_index", "update_trigger", "warmup" ] \ No newline at end of file diff --git a/examples/rl/vm_scheduling/ac.py b/examples/rl/vm_scheduling/ac.py index b3f1821c6..34eacf69d 100644 --- a/examples/rl/vm_scheduling/ac.py +++ b/examples/rl/vm_scheduling/ac.py @@ -8,7 +8,7 @@ import torch from maro.rl.experience import ReplayMemory, UniformSampler -from maro.rl.model import DiscreteACNet, FullyConnectedBlock, OptimOption +from maro.rl.model import DiscreteACNet, FullyConnected, OptimOption from maro.rl.policy.algorithms import ActorCritic, ActorCriticConfig vm_path = os.path.dirname(os.path.realpath(__file__)) @@ -81,8 +81,8 @@ def forward(self, states, actor: bool = True, critic: bool = True): ac_net = MyACNet( component={ - "actor": FullyConnectedBlock(**config["model"]["network"]["actor"]), - "critic": FullyConnectedBlock(**config["model"]["network"]["critic"]) + "actor": FullyConnected(**config["model"]["network"]["actor"]), + "critic": FullyConnected(**config["model"]["network"]["critic"]) }, optim_option={ "actor": OptimOption(**config["model"]["optimization"]["actor"]), diff --git a/examples/rl/vm_scheduling/dqn.py b/examples/rl/vm_scheduling/dqn.py index 8b588601c..2a9a0d055 100644 --- a/examples/rl/vm_scheduling/dqn.py +++ b/examples/rl/vm_scheduling/dqn.py @@ -10,7 +10,7 @@ from maro.rl.experience import ReplayMemory, UniformSampler from maro.rl.exploration import DiscreteSpaceExploration, MultiPhaseLinearExplorationScheduler -from maro.rl.model import DiscreteQNet, FullyConnectedBlock, OptimOption +from maro.rl.model import DiscreteQNet, FullyConnected, OptimOption from maro.rl.policy.algorithms import DQN, DQNConfig vm_path = os.path.dirname(os.path.realpath(__file__)) @@ -97,7 +97,7 @@ def __call__(self, action, state): def get_dqn_policy(mode="update"): assert mode in {"inference", "update", "inference-update"} q_net = MyQNet( - FullyConnectedBlock(**config["model"]["network"]), + FullyConnected(**config["model"]["network"]), optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None ) diff --git a/examples/rl/vm_scheduling/env_wrapper.py b/examples/rl/vm_scheduling/env_wrapper.py index 0d3109f79..86b45bd88 100644 --- a/examples/rl/vm_scheduling/env_wrapper.py +++ b/examples/rl/vm_scheduling/env_wrapper.py @@ -173,7 +173,7 @@ def _get_vm_state(self): } -def get_env_wrapper(replay_agent_ids=None): +def get_env_sampler(replay_agent_ids=None): env = Env(**env_config["basic"]) env.set_seed(env_config["seed"]) return VMEnvWrapper(env, **env_config["wrapper"]) @@ -185,7 +185,7 @@ def get_eval_env_wrapper(): return VMEnvWrapper(eval_env, **eval_env_config["wrapper"]) -tmp_env_wrapper = get_env_wrapper() +tmp_env_wrapper = get_env_sampler() STATE_DIM = tmp_env_wrapper.state_dim NUM_PMS = tmp_env_wrapper.num_pms del tmp_env_wrapper diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index f5c5881d2..cd52cffbf 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -19,9 +19,9 @@ sync: eval_parallelism: 1 distributed: group: rollout - num_workers: 3 + num_workers: 4 num_eval_workers: 1 - min_finished_workers: 2 + min_finished_workers: 3 max_extra_recv_tries: 2 extra_recv_timeout: 200 async: diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 37cdda3c0..9a470fbb4 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -16,21 +16,18 @@ module = importlib.import_module(f"{getenv('SCENARIO')}") -get_env_wrapper = getattr(module, "get_env_wrapper") -get_eval_env_wrapper = getattr(module, "get_eval_env_wrapper", lambda: None) -non_rl_policy_func_index = getattr(module, "non_rl_policy_func_index", {}) -rl_policy_func_index = getattr(module, "rl_policy_func_index") -agent2policy = getattr(module, "agent2policy") -rl_agents = [agent_id for agent_id, policy_id in agent2policy.items() if policy_id in rl_policy_func_index] +get_env_sampler = getattr(module, "get_env_sampler") +policy_func_dict = getattr(module, "policy_func_dict") +# rl_agents = [agent_id for agent_id, policy_id in agent2policy.items() if policy_id in rl_policy_func_index] post_collect = getattr(module, "post_collect", None) post_evaluate = getattr(module, "post_evaluate", None) # roll-out experience distribution amongst workers num_rollouts = int(getenv("NUMROLLOUTS")) -exp_dist = int(getenv("EXPDIST", default=0)) -if exp_dist: - replay_agents = [[] for _ in range(num_rollouts)] - for i, agent in enumerate(rl_agents): - replay_agents[i % num_rollouts].append(agent) -else: - replay_agents = [rl_agents] * num_rollouts +# exp_dist = int(getenv("EXPDIST", default=0)) +# if exp_dist: +# replay_agents = [[] for _ in range(num_rollouts)] +# for i, agent in enumerate(rl_agents): +# replay_agents[i % num_rollouts].append(agent) +# else: +# replay_agents = [rl_agents] * num_rollouts diff --git a/examples/rl/workflows/learner.py b/examples/rl/workflows/learner.py index a5c6a21ac..3be94bf4b 100644 --- a/examples/rl/workflows/learner.py +++ b/examples/rl/workflows/learner.py @@ -12,8 +12,7 @@ sys.path.insert(0, workflow_dir) from policy_manager import get_policy_manager -from rollout import get_agent_wrapper -from general import post_collect, post_evaluate, get_env_wrapper, get_eval_env_wrapper, log_dir +from general import post_collect, post_evaluate, get_env_sampler, log_dir def get_rollout_manager(): @@ -21,9 +20,7 @@ def get_rollout_manager(): num_steps = int(getenv("NUMSTEPS", default=-1)) if rollout_type == "simple": return SimpleRolloutManager( - get_env_wrapper, - get_agent_wrapper, - get_eval_env_wrapper=get_eval_env_wrapper, + get_env_sampler, num_steps=num_steps, parallelism=int(getenv("PARALLELISM", default="1")), eval_parallelism=int(getenv("EVALPARALLELISM", default="1")), diff --git a/examples/rl/workflows/policy_host.py b/examples/rl/workflows/policy_host.py index 03a1f4c39..745201096 100644 --- a/examples/rl/workflows/policy_host.py +++ b/examples/rl/workflows/policy_host.py @@ -11,7 +11,7 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import log_dir, rl_policy_func_index +from general import log_dir, policy_func_dict if __name__ == "__main__": @@ -21,7 +21,7 @@ group = getenv("LEARNGROUP", default="learn") policy_host( - rl_policy_func_index, + policy_func_dict, int(host_id), group, proxy_kwargs={ diff --git a/examples/rl/workflows/policy_manager.py b/examples/rl/workflows/policy_manager.py index b3666393e..b692d39de 100644 --- a/examples/rl/workflows/policy_manager.py +++ b/examples/rl/workflows/policy_manager.py @@ -6,33 +6,30 @@ from os.path import dirname, realpath from maro.rl.learning import DistributedPolicyManager, SimplePolicyManager -from maro.utils import Logger workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import log_dir, rl_policy_func_index +from general import log_dir, policy_func_dict def get_policy_manager(): - logger = Logger("policy manager creator") manager_type = getenv("POLICYMANAGERTYPE", default="simple") parallel = int(getenv("PARALLEL", default=0)) if manager_type == "simple": - return SimplePolicyManager(rl_policy_func_index, parallel=parallel, log_dir=log_dir) + return SimplePolicyManager(policy_func_dict, parallel=parallel, log_dir=log_dir) group = getenv("LEARNGROUP", default="learn") num_hosts = int(getenv("NUMHOSTS", default=5)) if manager_type == "distributed": policy_manager = DistributedPolicyManager( - list(rl_policy_func_index.keys()), group, num_hosts, + list(policy_func_dict.keys()), group, num_hosts, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 }, log_dir=log_dir ) - logger.info("Distributed policy manager created") return policy_manager raise ValueError(f"Unsupported policy manager type: {manager_type}. Supported modes: simple, distributed") diff --git a/examples/rl/workflows/rollout.py b/examples/rl/workflows/rollout.py index 921c07a07..4d8504efa 100644 --- a/examples/rl/workflows/rollout.py +++ b/examples/rl/workflows/rollout.py @@ -5,23 +5,11 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning import EnvironmentSampler -from maro.rl.wrappers import AgentWrapper - workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import ( - agent2policy, get_env_wrapper, get_eval_env_wrapper, log_dir, non_rl_policy_func_index, rl_policy_func_index -) - - -def get_agent_wrapper(): - return AgentWrapper( - {**non_rl_policy_func_index, **rl_policy_func_index}, - agent2policy - ) +from general import get_env_sampler, log_dir if __name__ == "__main__": @@ -30,7 +18,7 @@ def get_agent_wrapper(): raise ValueError("Missing environment variable: WORKERID") index = int(index) - env_sampler = EnvironmentSampler(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + env_sampler = get_env_sampler() if getenv("MODE") == "sync": env_sampler.worker( getenv("ROLLOUTGROUP", default="rollout"), index, diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index 92f2fd59a..0738eff7a 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -14,13 +14,13 @@ with open(join(workflow_dir, "config.yml"), "r") as fp: config = yaml.safe_load(fp) -from general import get_env_wrapper, get_eval_env_wrapper, log_dir, post_collect, post_evaluate +from general import get_env_sampler, get_eval_env_wrapper, log_dir, post_collect, post_evaluate from rollout import get_agent_wrapper if __name__ == "__main__": simple_learner( - get_env_wrapper(), + get_env_sampler(), get_agent_wrapper(rollout_only=False), num_episodes=config["num_episodes"], num_steps=config["num_steps"], diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index 4edaf13e6..8bf013138 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -2,14 +2,14 @@ # Licensed under the MIT license. from .early_stopper import AbsEarlyStopper -from .environment_sampler import EnvironmentSampler +from .env_sampler import AbsEnvSampler from .learner import Learner, simple_learner from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager, policy_host from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, SimpleRolloutManager __all__ = [ "AbsEarlyStopper", - "EnvironmentSampler", + "AbsEnvSampler", "Learner", "simple_learner", "AbsPolicyManager", "DistributedPolicyManager", "SimplePolicyManager", "policy_host", "AbsRolloutManager", "DistributedRolloutManager", "SimpleRolloutManager" diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 1c0154776..5412df9a3 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from abc import abstractmethod +from abc import ABC, abstractmethod from collections import defaultdict, deque from os import getcwd -from typing import Callable, Dict, List +from typing import Callable, Dict from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.policy import AbsPolicy, RLPolicy @@ -15,12 +15,19 @@ from .common import get_rollout_finish_msg -class EnvSampler: +class AbsEnvSampler(ABC): """Simulation data collector and policy evaluator. Args: get_env (Callable[[], Env]): Function to create an ``Env`` instance for collecting training data. The function should take no parameters and return an environment wrapper instance. + get_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. + agent2policy (Dict[str, str]): A dictionary that maps agent IDs to policy IDs, i.e., specifies the policy used + by each agent. + get_eval_env (Callable): Function to create an ``Env`` instance for evaluation. The function should + take no parameters and return an environment wrapper instance. If this is None, the training environment + wrapper will be used for evaluation in the worker processes. Defaults to None. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. @@ -32,55 +39,63 @@ class EnvSampler: instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition is a ``Transition`` object. For example, this callback can be used to collect various statistics on the simulation. Defaults to None. - get_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. - get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should - take no parameters and return an environment wrapper instance. If this is None, the training environment - wrapper will be used for evaluation in the worker processes. Defaults to None. """ def __init__( self, - env: Env, - policies: List[AbsPolicy], - get_state_func_dict: Dict[str, Callable], - eval_env: Env = None, + get_env: Callable[[], Env], + get_policy_func_dict: Dict[str, Callable], + agent2policy: Dict[str, str], + get_eval_env: Callable[[], Env] = None, reward_eval_delay: int = 0, - post_step: Callable = None, + post_step: Callable = None ): - unbound_agents = set(self.env.agent_idx_list) - self.policy_dict = {} - self.policy_by_agent = {} - for policy in policies: - self.policy_dict[policy.name] = policy - for agent in policy.agents: - if agent in self.policy_by_agent: - raise Exception(f"Agent {agent} is already bound to a policy") - self.policy_by_agent[agent] = policy - unbound_agents.remove(agent) - - if unbound_agents: - raise Exception(f"Agents {unbound_agents} are not bound to any policy") - - self.env = env - self.eval_env = eval_env if eval_env else self.env - self.get_state = get_state_func_dict + self.env = get_env() + self.policy_dict = {id_: func(id_) for id_, func in get_policy_func_dict.items()} + self.policy_by_agent = {agent: self.policy_dict[policy_id] for agent, policy_id in agent2policy.items()} + self.eval_env = get_eval_env() if get_eval_env else self.env self.reward_eval_delay = reward_eval_delay self._post_step = post_step - self._step_index = None - self._terminal = False + self._step_index = 0 + self._terminal = True + + self._transition_cache = defaultdict(deque) # for caching transitions whose rewards have yet to be evaluated + self._prev_state = defaultdict(lambda: None) - self._transition_cache = defaultdict(deque()) # for caching transitions whose rewards have yet to be evaluated self.tracker = {} # User-defined tracking information is placed here. - @property - def step_index(self): - """Number of environmental steps taken so far.""" - return self._step_index + @abstractmethod + def get_state(self, event, eval: bool = False, tick: int = None) -> dict: + """Compute the state for a given tick. + + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + + Returns: + A dictionary with (agent ID, state) as key-value pairs. + """ + raise NotImplementedError + + @abstractmethod + def to_env_action(self, action, eval: bool = False) -> dict: + """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" + raise NotImplementedError - @property - def summary(self): - return self.env.metrics + @abstractmethod + def get_reward(self, actions: list, tick: int = None, eval: bool = False): + """Evaluate the reward for an action. + + Args: + tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in + ``actions`` must be an Action object defined for the environment in question. The tick may + be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). + Defaults to None. + + Returns: + A dictionary with (agent ID, reward) as key-value pairs. + """ + raise NotImplementedError def get_agents_from_event(self, event): return self.env.agent_idx_list @@ -103,39 +118,39 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio if self._terminal: # get initial state - self._step_index = 0 - _, self._event, _ = self.env.step(None) - self._state = {agent: self.get_state[agent](self.env) for agent in self.get_agents_from_event(self._event)} + self.reset() + _, event, _ = self.env.step(None) + self._state = self.get_state(event) starting_step_index = self._step_index + 1 steps_to_go = float("inf") if num_steps == -1 else num_steps while not self._terminal and steps_to_go > 0: action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in self._state.items()} env_action = self.to_env_action(action) - _, self._event, self._terminal = self.env.step(env_action) - prev_state = self._state - self._state = None if self._terminal else { - agent: self.get_state[agent](self.env) for agent in self.get_agents_from_event(self._event) - } - self._transition_cache.append((prev_state, action, env_action, self._state, self.env.tick)) + for agent in self._state: + self._transition_cache[agent].append((self._state[agent], action[agent], env_action, self.env.tick)) + _, event, self._terminal = self.env.step(env_action) + self._state = None if self._terminal else self.get_state(event) self._step_index += 1 steps_to_go -= 1 - """ - If this is the final step, evaluate rewards for all remaining events except the last. - Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ - for agent, cache in self._transition_cache.items(): - while cache and (self._terminal or self.env.tick - cache[0][-1] >= self.reward_eval_delay): - state, action, env_action, state_, tick = cache.popleft() - reward = self.get_reward(env_action, tick=tick) - if self._post_step: - # put things you want to track in the tracker attribute - self._post_step(self.env, self.tracker, state, action, env_action, reward, state_, tick) - - if isinstance(self.policy_by_agent[agent], RLPolicy): - self.policy_by_agent[agent].record( - agent, state, action, reward, state_, not cache and self._terminal - ) + + """ + If this is the final step, evaluate rewards for all remaining events except the last. + Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. + """ + for agent, cache in self._transition_cache.items(): + while cache and (self._terminal or self.env.tick - cache[0][-1] >= self.reward_eval_delay): + state, action, env_action, tick = cache.popleft() + reward = self.get_reward(env_action, tick=tick) + if self._post_step: + # put things you want to track in the tracker attribute + self._post_step(self.env, self.tracker, state, action, env_action, reward, tick) + + if isinstance(self.policy_by_agent[agent], RLPolicy) and self._prev_state[agent] is not None: + self.policy_by_agent[agent].record( + agent, self._prev_state[agent], action, reward[agent], state, not cache and self._terminal + ) + self._prev_state[agent] = state return { "rollout_info": { @@ -144,7 +159,7 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio }, "step_range": (starting_step_index, self._step_index), "tracker": self.tracker, - "end_of_episode": not self._terminal, + "end_of_episode": self._terminal, "exploration_params": { name: policy.exploration_params for name, policy in self.policy_dict.items() if isinstance(policy, RLPolicy) @@ -166,40 +181,23 @@ def test(self, policy_state_dict: dict = None): terminal = False # get initial state _, event, _ = self.eval_env.step(None) - state = {agent: self.get_state[agent](self.eval_env) for agent in self.get_agents_from_event(event)} + state = self.get_state(event, eval=True) while not terminal: action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in state.items()} - env_action = self.to_env_action(action) - _, event, terminal = self.eval_env.step(env_action) - state = {agent: self.get_state[agent](self.eval_env) for agent in self.get_agents_from_event(event)} + env_action = self.to_env_action(action, eval=True) + _, _, terminal = self.eval_env.step(env_action) + if not terminal: + state = self.get_state(event, eval=True) return self.tracker - @abstractmethod - def to_env_action(self, action) -> dict: - """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" - raise NotImplementedError - - @abstractmethod - def get_reward(self, actions: list, tick: int = None): - """Evaluate the reward for an action. - - Args: - tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in - ``actions`` must be an Action object defined for the environment in question. The tick may - be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). - Defaults to None. - - Returns: - A dictionary with (agent ID, reward) as key-value pairs. - """ - raise NotImplementedError - def reset(self): self.env.reset() + self._step_index = 0 self._state = None self._transition_cache.clear() self.tracker.clear() + self._terminal = False def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = getcwd()): """Roll-out worker process that can be launched on separate computation nodes. diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py index f037ad5f7..bb639e6a0 100644 --- a/maro/rl/learning/learner.py +++ b/maro/rl/learning/learner.py @@ -9,17 +9,15 @@ from .common import get_eval_schedule, get_rollout_finish_msg from .early_stopper import AbsEarlyStopper -from .env_sampler import EnvSampler +from .env_sampler import AbsEnvSampler from .policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager def simple_learner( - get_env_wrapper: Callable[[], AbsEnvWrapper], - get_agent_wrapper: Callable[[], AgentWrapper], + get_env_sampler: Callable[[], AbsEnvSampler], num_episodes: int, num_steps: int = -1, - get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None, eval_schedule: Union[int, List[int]] = None, eval_after_last_episode: bool = True, early_stopper: AbsEarlyStopper = None, @@ -30,7 +28,7 @@ def simple_learner( """Single-threaded learning workflow. Args: - get_env_wrapper (Callable): Function to create an environment wrapper for collecting training data. The function + get_env_sampler (Callable): Function to create an environment wrapper for collecting training data. The function should take no parameters and return an environment wrapper instance. get_agent_wrapper (Callable): Function to create an agent wrapper that interacts with the environment wrapper. The function should take no parameters and return a ``AgentWrapper`` instance. @@ -65,7 +63,7 @@ def simple_learner( raise ValueError("num_steps must be a positive integer or -1") logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) - env_sampler = EnvSampler(get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper) + env_sampler = get_env_sampler() # evaluation schedule eval_schedule = get_eval_schedule(eval_schedule, num_episodes, final=eval_after_last_episode) @@ -80,11 +78,11 @@ def collect_and_update(ep): logger.info( get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) ) - for policy_name, info_list in result["rollout_info"].items(): - if isinstance(info_list[0], Trajectory): - env_sampler.agent.policy_dict[policy_name].learn_from_multi_trajectories(info_list) - elif isinstance(info_list[0], LossInfo): - env_sampler.agent.policy_dict[policy_name].update_with_multi_loss_info(info_list) + for policy_name, info in result["rollout_info"].items(): + if "grad" in info: + env_sampler.policy_dict[policy_name].update([info]) + else: + env_sampler.policy_dict[policy_name].learn([info]) if post_collect: post_collect([result["tracker"]], ep, segment) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index d50c904f6..38f3edd71 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -9,8 +9,7 @@ from typing import Callable, Dict, List from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.policy import LossInfo, RLPolicy -from maro.rl.types import Trajectory +from maro.rl.policy import RLPolicy from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger @@ -24,8 +23,8 @@ def __init__(self): def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. - The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss - information computed directly by roll-out workers. + The roll-out information is grouped by policy name and may be either a training batch or a list of loss + information dictionaries computed directly by roll-out workers. """ raise NotImplementedError @@ -123,15 +122,13 @@ def _policy_host(name, create_policy_func, conn): msg = conn.recv() if msg["type"] == "learn": info_list = msg["rollout_info"] - if isinstance(info_list[0], Trajectory): - policy.learn_from_multi_trajectories(info_list) - elif isinstance(info_list[0], LossInfo): - policy.update_with_multi_loss_info(info_list) + if not isinstance(info_list, list): + info_list = [info_list] + if "loss" in info_list[0]: + policy.update(info_list) else: - raise TypeError( - f"Roll-out information must be of type 'Trajectory' or 'LossInfo', " - f"got {type(info_list[0])}" - ) + policy.learn(info_list) + conn.send({"type": "learn_done", "policy_state": policy.get_state()}) elif msg["type"] == "quit": break @@ -157,8 +154,8 @@ def _policy_host(name, create_policy_func, conn): def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. - The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss - information computed directly by roll-out workers. + The roll-out information is grouped by policy name and may be either a training batch or a list of loss + information dictionaries computed directly by roll-out workers. """ t0 = time.time() if self._parallel: @@ -170,13 +167,11 @@ def update(self, rollout_info: Dict[str, list]): self._state_cache[policy_name] = msg["policy_state"] self._logger.info(f"Cached state for policy {policy_name}") else: - for policy_name, info_list in rollout_info.items(): - if not isinstance(info_list, list): - info_list = [info_list] - if isinstance(info_list[0], Trajectory): - self._policy_dict[policy_name].learn_from_multi_trajectories(info_list) - elif isinstance(info_list[0], LossInfo): - self._policy_dict[policy_name].update_with_multi_loss_info(info_list) + for policy_name, info in rollout_info.items(): + if isinstance(info, list): + self._policy_dict[policy_name].update(info) + else: + self._policy_dict[policy_name].learn(info) self._logger.info(f"Updated policies {list(rollout_info.keys())}") self._version += 1 @@ -260,15 +255,13 @@ def __init__( def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. - The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss - information computed directly by roll-out workers. + The roll-out information is grouped by policy name and may be either a training batch or a list if loss + information dictionaries computed directly by roll-out workers. """ msg_dict = defaultdict(lambda: defaultdict(dict)) - for policy_name, info_list in rollout_info.items(): - if not isinstance(info_list, list): - info_list = [info_list] + for policy_name, info in rollout_info.items(): host_id_str = self._policy2host[policy_name] - msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_name] = info_list + msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_name] = info dones = 0 self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_dict.items())) @@ -340,17 +333,14 @@ def policy_host( ) elif msg.tag == MsgTag.LEARN: t0 = time.time() - for name, info_list in msg.body[MsgKey.ROLLOUT_INFO].items(): - if isinstance(info_list[0], Trajectory): - logger.info("learning from multiple trajectories") - policy_dict[name].learn_from_multi_trajectories(info_list) - elif isinstance(info_list[0], LossInfo): + for name, info in msg.body[MsgKey.ROLLOUT_INFO].items(): + if isinstance(info, list): logger.info("updating with loss info") - policy_dict[name].update_with_multi_loss_info(info_list) + policy_dict[name].update(info) else: - raise TypeError( - f"Roll-out information must be of type 'Trajectory' or 'LossInfo', got {type(info_list[0])}" - ) + logger.info("learning from batch") + policy_dict[name].learn(info) + msg_body = { MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.ROLLOUT_INFO]} } diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index 3923db671..f17be58a2 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -6,15 +6,20 @@ from multiprocessing import Pipe, Process from os import getcwd from random import choices -from typing import Callable +from typing import Callable, List + +import numpy as np from maro.communication import Proxy, SessionType from maro.rl.utils import MsgKey, MsgTag -from maro.rl.wrappers import AbsEnvWrapper, AgentWrapper from maro.utils import Logger, set_seeds from .common import get_rollout_finish_msg -from .environment_sampler import EnvironmentSampler +from .env_sampler import AbsEnvSampler + + +def concat_batches(batch_list: List[dict]): + return {key: np.concatenate([batch[key] for batch in batch_list]) for key in batch_list[0]} class AbsRolloutManager(ABC): @@ -72,15 +77,10 @@ class SimpleRolloutManager(AbsRolloutManager): """Local roll-out controller. Args: - get_env_wrapper (Callable): Function to create an environment wrapper for collecting training data. The function - should take no parameters and return an environment wrapper instance. - get_agent_wrapper (Callable): Function to create an agent wrapper that interacts with the environment wrapper. - The function should take no parameters and return a ``AgentWrapper`` instance. + get_env_sampler (Callable): Function to create an environment sampler for collecting training data. The function + should take no parameters and return an ``AbsEnvSampler`` instance. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should - take no parameters and return an environment wrapper instance. If this is None, the training environment - wrapper will be used for evaluation in the worker processes. Defaults to None. post_collect (Callable): Custom function to process whatever information is collected by each environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. @@ -95,11 +95,9 @@ class SimpleRolloutManager(AbsRolloutManager): """ def __init__( self, - get_env_wrapper: Callable[[], AbsEnvWrapper], - get_agent_wrapper: Callable[[], AgentWrapper], + get_env_sampler: Callable[[], AbsEnvSampler], num_steps: int = -1, parallelism: int = 1, - get_eval_env_wrapper: Callable[[], AbsEnvWrapper] = None, eval_parallelism: int = 1, post_collect: Callable = None, post_evaluate: Callable = None, @@ -121,18 +119,14 @@ def __init__( self._parallelism = parallelism self._eval_parallelism = eval_parallelism if self._parallelism == 1: - self.env_sampler = EnvironmentSampler( - get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper - ) + self.env_sampler = get_env_sampler() else: self._worker_processes = [] self._manager_ends = [] - def _rollout_worker(index, conn, get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=None): + def _rollout_worker(index, conn, get_env_sampler): set_seeds(index) - env_sampler = EnvironmentSampler( - get_env_wrapper, get_agent_wrapper, get_eval_env_wrapper=get_eval_env_wrapper - ) + env_sampler = get_env_sampler() logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) while True: msg = conn.recv() @@ -157,11 +151,7 @@ def _rollout_worker(index, conn, get_env_wrapper, get_agent_wrapper, get_eval_en for index in range(self._parallelism): manager_end, worker_end = Pipe() self._manager_ends.append(manager_end) - worker = Process( - target=_rollout_worker, - args=(index, worker_end, get_env_wrapper, get_agent_wrapper), - kwargs={"get_eval_env_wrapper": get_eval_env_wrapper} - ) + worker = Process(target=_rollout_worker, args=(index, worker_end, get_env_sampler)) self._worker_processes.append(worker) worker.start() @@ -190,8 +180,8 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) ) - for policy_name, info in result["rollout_info"].items(): - info_by_policy[policy_name].append(info) + for policy_id, info in result["rollout_info"].items(): + info_by_policy[policy_id].append(info) trackers.append(result["tracker"]) self.episode_complete = result["end_of_episode"] else: @@ -211,8 +201,8 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): for conn in self._manager_ends: result = conn.recv() - for policy_name, info in result["rollout_info"].items(): - info_by_policy[policy_name].append(info) + for policy_id, info in result["rollout_info"].items(): + info_by_policy[policy_id].append(info) trackers.append(result["tracker"]) self.episode_complete = result["episode_end"] @@ -222,6 +212,11 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if self._post_collect: self._post_collect(trackers, ep, segment) + # concat batches from different roll-out workers + for policy_id, info_list in info_by_policy.items(): + if not "loss" in info_list[0]: + info_by_policy[policy_id] = concat_batches(info_list) + return info_by_policy def evaluate(self, ep: int, policy_state_dict: dict): @@ -364,14 +359,14 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if self._exploration_step: self._exploration_step = False - info_list_by_policy, trackers, num_finishes = defaultdict(list), [], 0 + info_by_policy, trackers, num_finishes = defaultdict(list), [], 0 # Ensure the minimum number of worker results are received. for msg in self._proxy.receive(): - info_by_policy, tracker = self._handle_worker_result(msg, ep, segment, version) - if info_by_policy: + rollout_info, tracker = self._handle_worker_result(msg, ep, segment, version) + if rollout_info: num_finishes += 1 - for policy_name, info in info_by_policy.items(): - info_list_by_policy[policy_name].append(info) + for policy_id, info in rollout_info.items(): + info_by_policy[policy_id].append(info) trackers.append(tracker) if num_finishes == self._min_finished_workers: break @@ -382,11 +377,11 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if not msg: self._logger.info(f"Receive timeout, {self._max_extra_recv_tries - i - 1} attempts left") else: - info_by_policy, tracker = self._handle_worker_result(msg, ep, segment, version) + rollout_info, tracker = self._handle_worker_result(msg, ep, segment, version) if info_by_policy: num_finishes += 1 - for policy_name, info in info_by_policy.items(): - info_list_by_policy[policy_name].append(info) + for policy_id, info in rollout_info.items(): + info_by_policy[policy_id].append(info) trackers.append(tracker) if num_finishes == self._num_workers: break @@ -397,7 +392,12 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if self._post_collect: self._post_collect(trackers, ep, segment) - return info_list_by_policy + # concat batches from different roll-out workers + for policy_id, info_list in info_by_policy.items(): + if not "loss" in info_list[0]: + info_by_policy[policy_id] = concat_batches(info_list) + + return info_by_policy def _handle_worker_result(self, msg, ep, segment, version): if msg.tag != MsgTag.SAMPLE_DONE: diff --git a/maro/rl/modeling/__init__.py b/maro/rl/modeling/__init__.py index 8179d2a6c..832593839 100644 --- a/maro/rl/modeling/__init__.py +++ b/maro/rl/modeling/__init__.py @@ -2,11 +2,11 @@ # Licensed under the MIT license. from .core_model import AbsCoreModel, OptimOption -from .fc_block import FullyConnectedBlock +from .fc_block import FullyConnected from .specials import ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet __all__ = [ "AbsCoreModel", "OptimOption", - "FullyConnectedBlock", + "FullyConnected", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet" ] diff --git a/maro/rl/modeling/fc_block.py b/maro/rl/modeling/fc_block.py index b2f03af5d..c736a643d 100644 --- a/maro/rl/modeling/fc_block.py +++ b/maro/rl/modeling/fc_block.py @@ -10,7 +10,7 @@ from maro.rl.utils import get_torch_activation_cls -class FullyConnectedBlock(nn.Module): +class FullyConnected(nn.Module): """Fully connected network with optional batch normalization, activation and dropout components. Args: diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 17a1e0a98..19094d4ff 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -1,18 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .ac import ACActionInfo, ACBatch, ACLossInfo, ActorCritic, DiscreteACNet -from .ddpg import DDPG, ContinuousACNet, DDPGBatch, DDPGLossInfo -from .dqn import DQN, DiscreteQNet, DQNBatch, DQNLossInfo, PrioritizedExperienceReplay -from .index import get_model_cls, get_policy_cls -from .pg import DiscretePolicyNet, PGActionInfo, PGBatch, PGLossInfo, PolicyGradient -from .policy import AbsPolicy, Batch, LossInfo, NullPolicy, RLPolicy +from .ac import ActorCritic +from .ddpg import DDPG +from .dqn import DQN, PrioritizedExperienceReplay +from .pg import PolicyGradient +from .policy import AbsPolicy, NullPolicy, RLPolicy __all__ = [ - "ACActionInfo", "ACBatch", "ACLossInfo", "ActorCritic", "DiscreteACNet", - "DDPG", "DDPGBatch", "DDPGLossInfo", "ContinuousACNet", - "DQN", "DQNBatch", "DQNLossInfo", "DiscreteQNet", "PrioritizedExperienceReplay", - "PGActionInfo", "PGBatch", "PGLossInfo", "DiscretePolicyNet", "PolicyGradient", - "AbsPolicy", "Batch", "LossInfo", "NullPolicy", "RLPolicy", - "get_model_cls", "get_policy_cls" + "ActorCritic", + "DDPG", + "DQN", "PrioritizedExperienceReplay", + "PolicyGradient", + "AbsPolicy", "NullPolicy", "RLPolicy" ] diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index adc75f803..fe4ae4f2c 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -30,8 +30,10 @@ def __init__(self, state_dim, size: int = 10000): self.logps = np.zeros(size, dtype=np.float32) self.values = np.zeros(size, dtype=np.float32) self.rewards = np.zeros(size, dtype=np.float32) - self.terminal = np.zeros(size, dtype=np.bool) + self.terminals = np.zeros(size, dtype=np.bool) self.size = size + self._ptr = 0 + self._last_ptr = 0 def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): self.states[self._ptr] = state @@ -39,14 +41,14 @@ def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = F self.logps[self._ptr] = action["logp"] self.values[self._ptr] = action["value"] self.rewards[self._ptr] = reward - self.terminal[self._ptr] = terminal + self.terminals[self._ptr] = terminal # increment pointer self._ptr += 1 if self._ptr == self.size: self._ptr = 0 def get(self): - terminal = self.terminal[self._ptr - 1] + terminal = self.terminals[self._ptr - 1] traj_slice = slice(self._last_ptr, self._ptr - (not terminal)) self._last_ptr = self._ptr - (not terminal) return { @@ -93,13 +95,12 @@ def __init__( clip_ratio: float = None, lam: float = 0.9, buffer_size: int = 10000, - get_loss_on_rollout_finish: bool = False, - remote: bool = False + get_loss_on_rollout: bool = False ): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") - super().__init__(name, remote=remote) + super().__init__(name) self.ac_net = ac_net self.device = self.ac_net.device self.reward_discount = reward_discount @@ -111,17 +112,16 @@ def __init__( self.clip_ratio = clip_ratio self.lam = lam self.buffer_size = buffer_size - self._get_loss_on_rollout_finish = get_loss_on_rollout_finish + self.get_loss_on_rollout = get_loss_on_rollout - self._buffer = {} + self._buffer = defaultdict(lambda: self.Buffer(self.ac_net.input_dim, size=self.buffer_size)) - def add_agent(self, agent: str): - super().add_agent(agent) - self._buffer[agent] = self.Buffer(self.ac_net.input_dim, size=self.buffer_size) - - def choose_action(self, states): + def choose_action(self, states: np.ndarray): """Return actions and log probabilities for given states.""" self.ac_net.eval() + states = torch.from_numpy(states).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) with torch.no_grad(): actions, logps, values = self.ac_net.get_action(states) actions, logps, values = actions.cpu().numpy(), logps.cpu().numpy(), values.cpu().numpy() @@ -134,32 +134,27 @@ def choose_action(self, states): def record( self, - agent: str, + key: str, state: np.ndarray, action: dict, reward: float, next_state: np.ndarray, terminal: bool ): - if agent not in self._buffer: - raise KeyError( - f"Agent {agent} has not been added to this policy. " - f"Make sure to add it using 'add_agent' before using it for inference." - ) - self._buffer[agent].put(state, action, reward, terminal) + self._buffer[key].put(state, action, reward, terminal) def get_rollout_info(self): - if self._get_loss_on_rollout_finish: + if self.get_loss_on_rollout: return self.get_batch_loss(self._get_batch(), explicit_grad=True) else: return self._get_batch() def _get_batch(self): batch = defaultdict(list) - for buf in self._buffer: + for buf in self._buffer.values(): trajectory = buf.get() - values = np.append(trajectory["values"], trajectory["last_val"]) - rewards = np.append(trajectory["rewards"], trajectory["last_val"]) + values = np.append(trajectory["values"], trajectory["last_value"]) + rewards = np.append(trajectory["rewards"], trajectory["last_value"]) deltas = rewards[:-1] + self.reward_discount * values[1:] - values[:-1] batch["states"].append(trajectory["states"]) batch["actions"].append(trajectory["actions"]) @@ -169,17 +164,18 @@ def _get_batch(self): batch["advantages"].append(discount_cumsum(deltas, self.reward_discount * self.lam)) batch["logps"].append(trajectory["logps"]) - return {key: np.concatenate(vals) for key, vals in batch.items} + return {key: np.concatenate(vals) for key, vals in batch.items()} def get_batch_loss(self, batch: dict, explicit_grad: bool = False): assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() + states = torch.from_numpy(batch["states"]).to(self.device) actions = torch.from_numpy(batch["actions"]).to(self.device) logp_old = torch.from_numpy(batch["logps"]).to(self.device) returns = torch.from_numpy(batch["returns"]).to(self.device) advantages = torch.from_numpy(batch["advantages"]).to(self.device) - action_probs, state_values = self.ac_net(batch["states"]) + action_probs, state_values = self.ac_net(states) state_values = state_values.squeeze() # actor loss @@ -200,24 +196,28 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): # total loss loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy - loss_info = {"actor_loss": actor_loss, "critic_loss": critic_loss, "entropy": entropy, "loss": loss} + loss_info = { + "actor_loss": actor_loss.detach().cpu().numpy(), + "critic_loss": critic_loss.detach().cpu().numpy(), + "entropy": entropy.detach().cpu().numpy(), + "loss": loss + } if explicit_grad: loss_info["grad"] = self.ac_net.get_gradients(loss) return loss_info - def update_with_multi_loss_info(self, loss_info_list: List[dict]): + def update(self, loss_info_list: List[dict]): """Apply gradients to the underlying parameterized model.""" - self.ac_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) + self.ac_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) - def learn_from_multi_trajectories(self, trajectories: List[dict]): - if self.remote: + def learn(self, batch: dict): + if self.grad_parallel: # TODO: distributed grad computation pass else: - batches = [self._preprocess(traj) for traj in trajectories] for _ in range(self.grad_iters): - self.update_with_multi_loss_info([self.get_batch_loss(batch, explicit_grad=True) for batch in batches]) + self.ac_net.step(self.get_batch_loss(batch)["loss"]) def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 27fe821ab..95b2f5eab 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -7,47 +7,13 @@ import torch from maro.rl.exploration import GaussianNoiseExploration, NoiseExploration -from maro.rl.types import ContinuousACNet, Trajectory +from maro.rl.modeling import ContinuousACNet from maro.rl.utils import get_torch_loss_cls -from maro.utils.exception.rl_toolkit_exception import InvalidExperience -from .policy import Batch, LossInfo, RLPolicy +from .policy import RLPolicy from .replay import ReplayMemory -class DDPGBatch(Batch): - """Wrapper for a set of experiences. - - An experience consists of state, action, reward, next state and auxillary information. - """ - __slots__ = ["states", "actions", "rewards", "next_states"] - - def __init__(self, states: list, actions: list, rewards: list, next_states: list): - if not len(states) == len(actions) == len(rewards) == len(next_states): - raise InvalidExperience("values of contents should consist of lists of the same length") - super().__init__() - self.states = states - self.actions = actions - self.rewards = rewards - self.next_states = next_states - - @property - def size(self): - return len(self.states) - - -class DDPGLossInfo(LossInfo): - - __slots__ = ["policy_loss", "q_loss"] - - def __init__(self, loss, policy_loss, q_loss, grad=None): - super().__init__(loss, grad) - self.loss = loss - self.policy_loss = policy_loss - self.q_loss = q_loss - self.grad = grad - - class DDPG(RLPolicy): """The Deep Deterministic Policy Gradient (DDPG) algorithm. @@ -71,6 +37,7 @@ class DDPG(RLPolicy): random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. + batch_size (int): Training sample. Defaults to 32. """ def __init__( self, @@ -85,12 +52,12 @@ def __init__( exploration: NoiseExploration = GaussianNoiseExploration(), replay_memory_capacity: int = 10000, random_overwrite: bool = False, - remote: bool = False + batch_size: int = 32 ): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") - super().__init__(name, remote=remote) + super().__init__(name) self.ac_net = ac_net self.device = self.ac_net.device if self.ac_net.trainable: @@ -108,84 +75,88 @@ def __init__( self._ac_net_version = 0 self._target_ac_net_version = 0 - self._replay_memory = ReplayMemory(DDPGBatch, replay_memory_capacity, random_overwrite=random_overwrite) + self._replay_memory = ReplayMemory( + replay_memory_capacity, self.ac_net.input_dim, action_dim=1, random_overwrite=random_overwrite + ) + self.batch_size = batch_size self.exploration = exploration - self.exploring = True # set initial exploration status to True + self.greedy = True - def choose_action(self, states, explore: bool = False) -> Union[float, np.ndarray]: + def choose_action(self, states) -> Union[float, np.ndarray]: self.ac_net.eval() with torch.no_grad(): actions = self.ac_net.get_action(states).cpu().numpy() - if explore: + if not self.greedy: actions = self.exploration(actions, state=states) return actions[0] if len(actions) == 1 else actions - def _preprocess(self, trajectory: Trajectory): - if trajectory.states[-1]: - batch = DDPGBatch( - states=trajectory.states[:-1], - actions=trajectory.actions[:-1], - rewards=trajectory.rewards, - next_states=trajectory.states[1:] - ) - else: - batch = DDPGBatch( - states=trajectory.states[:-2], - actions=trajectory.actions[:-2], - rewards=trajectory.rewards[:-1], - next_states=trajectory.states[1:-1] - ) - self._replay_memory.put(batch) - - def _get_batch(self) -> DDPGBatch: - indexes = np.random.choice(self._replay_memory.size) - return DDPGBatch( - [self._replay_memory.data["states"][idx] for idx in indexes], - [self._replay_memory.data["actions"][idx] for idx in indexes], - [self._replay_memory.data["rewards"][idx] for idx in indexes], - [self._replay_memory.data["next_states"][idx] for idx in indexes] + def record( + self, + key: str, + state: np.ndarray, + action: Union[int, float, np.ndarray], + reward: float, + next_state: np.ndarray, + terminal: bool + ): + self._replay_memory.put( + np.expand_dims(state, axis=0), + np.expand_dims(action, axis=0), + np.expand_dims(reward, axis=0), + np.expand_dims(next_state, axis=0), + np.expand_dims(terminal, axis=0) ) - def get_batch_loss(self, batch: DDPGBatch, explicit_grad: bool = False) -> DDPGLossInfo: + def get_batch_loss(self, batch: dict, explicit_grad: bool = False) -> dict: assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() - states, next_states = batch.states, batch.next_states - actual_actions = torch.from_numpy(batch.actions).to(self.device) - rewards = torch.from_numpy(batch.rewards).to(self.device) + states = torch.from_numpy(batch["states"]).to(self.device) + next_states = torch.from_numpy(["next_states"]).to(self.device) + actual_actions = torch.from_numpy(batch["actions"]).to(self.device) + rewards = torch.from_numpy(batch["rewards"]).to(self.device) + terminals = torch.from_numpy(batch["terminals"]).float().to(self.device) if len(actual_actions.shape) == 1: actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) with torch.no_grad(): next_q_values = self.target_ac_net.value(next_states) - target_q_values = (rewards + self.reward_discount * next_q_values).detach() # (N,) + target_q_values = (rewards + self.reward_discount * (1 - terminals) * next_q_values).detach() # (N,) + # loss info + loss_info = {} q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) q_loss = self.q_value_loss_func(q_values, target_q_values) policy_loss = -self.ac_net.value(states).mean() - - # total loss loss = policy_loss + self.q_value_loss_coeff * q_loss - grad = self.ac_net.get_gradients(loss) if explicit_grad else None - return DDPGLossInfo(policy_loss, q_loss, loss, grad=grad) - - def update_with_multi_loss_info(self, loss_info_list: List[DDPGLossInfo]): - self.ac_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) + loss_info = { + "policy_loss": policy_loss.detach().cpu().numpy(), + "q_loss": q_loss.detach().cpu().numpy(), + "loss": loss.detach().cpu().numpy() + } + if explicit_grad: + loss_info["grad"] = self.ac_net.get_gradients(loss) + + return loss_info + + def update(self, loss_info_list: List[dict]): + self.ac_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) if self._ac_net_version - self._target_ac_net_version == self.update_target_every: self._update_target() - def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): - for traj in trajectories: - self._preprocess(traj) + def learn(self, batch: dict): + self._replay_memory.put( + batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] + ) - if self.remote: + if self.grad_parallel: # TODO: distributed grad computation pass else: for _ in range(self.num_epochs): - loss_info = self.get_batch_loss(self._get_batch(), explicit_grad=False) - self.ac_net.step(loss_info.loss) + train_batch = self._replay_memory.sample(self.batch_size) + self.ac_net.step(self.get_batch_loss(train_batch)["loss"]) self._ac_net_version += 1 if self._ac_net_version - self._target_ac_net_version == self.update_target_every: self._update_target() @@ -200,10 +171,10 @@ def exploration_params(self): return self.exploration.parameters def exploit(self): - self.exploring = False + self.greedy = True def explore(self): - self.exploring = True + self.greedy = False def exploration_step(self): self.exploration.step() diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index b08105ca0..daa4cb08b 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -82,9 +82,9 @@ def sample(self, size: int): priorities.append(self._sum_tree[idx]) self.beta = min(1., self.beta + self.beta_step) - sampling_probabilities = priorities / self.total() + sampling_probabilities = priorities / (self.total() + 1e-8) is_weights = np.power(self._replay_memory.size * sampling_probabilities, -self.beta) - is_weights /= is_weights.max() + is_weights /= (is_weights.max() + 1e-8) return indexes, is_weights @@ -150,15 +150,14 @@ def __init__( exploration: DiscreteSpaceExploration = EpsilonGreedyExploration(), replay_memory_capacity: int = 10000, random_overwrite: bool = False, - batch_size: int = 32, - rollout_info_size: int = 1000, - prioritized_replay_kwargs: dict = None, - remote: bool = False + train_batch_size: int = 32, + rollout_batch_size: int = 1000, + prioritized_replay_kwargs: dict = None ): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") - super().__init__(name, remote=remote) + super().__init__(name) self.q_net = q_net self.device = self.q_net.device if self.q_net.trainable: @@ -178,8 +177,8 @@ def __init__( self._replay_memory = ReplayMemory( replay_memory_capacity, self.q_net.input_dim, action_dim=1, random_overwrite=random_overwrite ) - self.batch_size = batch_size - self.rollout_info_size = rollout_info_size + self.rollout_batch_size = rollout_batch_size + self.train_batch_size = train_batch_size self.prioritized_replay = prioritized_replay_kwargs is not None if self.prioritized_replay: self._per = PrioritizedExperienceReplay(self._replay_memory, **prioritized_replay_kwargs) @@ -187,16 +186,19 @@ def __init__( self._loss_func = torch.nn.MSELoss() self.exploration = exploration - self.exploring = True # set initial exploration status to True + self.greedy = True # set initial exploration status to True - def choose_action(self, states: torch.tensor) -> Union[int, np.ndarray]: + def choose_action(self, states: np.ndarray) -> Union[int, np.ndarray]: self.q_net.eval() + states = torch.from_numpy(states).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) with torch.no_grad(): q_for_all_actions = self.q_net(states) # (batch_size, num_actions) _, actions = q_for_all_actions.max(dim=1) actions = actions.cpu().numpy() - if self.exploring: + if not self.greedy: if self.exploration.action_space is None: self.exploration.set_action_space(np.arange(q_for_all_actions.shape[1])) actions = self.exploration(actions, state=states) @@ -204,41 +206,49 @@ def choose_action(self, states: torch.tensor) -> Union[int, np.ndarray]: def record( self, - agent: str, + key: str, state: np.ndarray, action: Union[int, float, np.ndarray], reward: float, next_state: np.ndarray, terminal: bool ): - indexes = self._replay_memory.put(state, action, reward, next_state, terminal) + indexes = self._replay_memory.put( + np.expand_dims(state, axis=0), + np.expand_dims(action, axis=0), + np.expand_dims(reward, axis=0), + np.expand_dims(next_state, axis=0), + np.expand_dims(terminal, axis=0) + ) if self.prioritized_replay: self._per.set_max_priority(indexes) def get_rollout_info(self): - return self._replay_memory.sample(self.rollout_info_size) + return self._replay_memory.sample(self.rollout_batch_size) def _get_batch(self): if self.prioritized_replay: - indexes, is_weights = self._per.sample(self.batch_size) + indexes, is_weights = self._per.sample(self.train_batch_size) return { "states": self._replay_memory.states[indexes], "actions": self._replay_memory.actions[indexes], "rewards": self._replay_memory.rewards[indexes], "next_states": self._replay_memory.next_states[indexes], + "terminals": self._replay_memory.terminals[indexes], "indexes": indexes, "is_weights": is_weights } else: - return self._replay_memory.sample(self.batch_size) + return self._replay_memory.sample(self.train_batch_size) def get_batch_loss(self, batch: dict, explicit_grad: bool = False): assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() - states, next_states = batch["states"], batch["next_states"] + states = torch.from_numpy(batch["states"]).to(self.device) + next_states = torch.from_numpy(batch["next_states"]).to(self.device) actions = torch.from_numpy(batch["actions"]).to(self.device) rewards = torch.from_numpy(batch["rewards"]).to(self.device) - terminals = torch.from_numpy(batch["terminals"]).to(self.device) + terminals = torch.from_numpy(batch["terminals"]).float().to(self.device) # get target Q values with torch.no_grad(): @@ -250,14 +260,15 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): target_q_values = (rewards + self.reward_discount * (1 - terminals) * next_q_values).detach() # (N,) - # gradient step + # loss info loss_info = {} q_values = self.q_net.q_values(states, actions) + # print(f"target: {target_q_values}, eval: {q_values}") td_errors = target_q_values - q_values if self.prioritized_replay: is_weights = torch.from_numpy(batch["is_weights"]).to(self.device) loss = (td_errors * is_weights).mean() - loss_info["td_errors"], loss_info["indexes"] = td_errors, batch["indexes"] + loss_info["td_errors"], loss_info["indexes"] = td_errors.detach().cpu().numpy(), batch["indexes"] else: loss = self._loss_func(q_values, target_q_values) @@ -266,7 +277,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): loss_info["grad"] = self.q_net.get_gradients(loss) return loss_info - def update_with_multi_loss_info(self, loss_info_list: List[dict]): + def update(self, loss_info_list: List[dict]): if self.prioritized_replay: for loss_info in loss_info_list: self._per.update(loss_info["indexes"], loss_info["td_errors"]) @@ -276,19 +287,20 @@ def update_with_multi_loss_info(self, loss_info_list: List[dict]): if self._q_net_version - self._target_q_net_version == self.update_target_every: self._update_target() - def learn_from_multi_trajectories(self, trajectories: List[dict]): - for traj in trajectories: - self._put_in_replay_memory(traj) + def learn(self, batch: dict): + self._replay_memory.put( + batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] + ) - if self.remote: + if self.grad_parallel: # TODO: distributed grad computation pass - else: + else: for _ in range(self.num_epochs): loss_info = self.get_batch_loss(self._get_batch()) if self.prioritized_replay: self._per.update(loss_info["indexes"], loss_info["td_errors"]) - self.q_net.step(loss_info.loss) + self.q_net.step(loss_info["loss"]) self._q_net_version += 1 if self._q_net_version - self._target_q_net_version == self.update_target_every: self._update_target() @@ -303,10 +315,10 @@ def exploration_params(self): return self.exploration.parameters def exploit(self): - self.exploring = False + self.greedy = True def explore(self): - self.exploring = True + self.greedy = False def exploration_step(self): self.exploration.step() diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index 26825cb89..ee97ac8c2 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -1,47 +1,55 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import defaultdict from typing import List, Tuple import numpy as np import torch -from maro.rl.types import DiscretePolicyNet, Trajectory +from maro.rl.modeling import DiscretePolicyNet from maro.rl.utils import discount_cumsum -from .policy import Batch, LossInfo, RLPolicy - - -class PGActionInfo: - - __slots__ = ["action", "logp", "value"] - - def __init__(self, action, logp: float, value: float): - self.action = action - self.logp = logp - self.value = value - - -class PGBatch(Batch): - - __slots__ = ["states", "actions", "returns", "logps"] - - def __init__(self, states, returns: np.array): - super().__init__() - self.states = states - self.returns = returns - - @property - def size(self): - return len(self.states) - - -class PGLossInfo(LossInfo): - def __init__(self, loss, grad=None): - super().__init__(loss, grad) +from .policy import RLPolicy class PolicyGradient(RLPolicy): + class Buffer: + """Sequence of transitions for an agent. + + Args: + states: Sequence of ``State`` objects traversed during simulation. + actions: Sequence of actions taken in response to the states. + rewards: Sequence of rewards received as a result of the actions. + info: Sequence of each transition's auxillary information. + """ + def __init__(self, state_dim, size: int = 10000): + self.states = np.zeros((size, state_dim), dtype=np.float32) + self.values = np.zeros(size, dtype=np.float32) + self.rewards = np.zeros(size, dtype=np.float32) + self.terminals = np.zeros(size, dtype=np.bool) + self.size = size + + def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): + self.states[self._ptr] = state + self.values[self._ptr] = action["value"] + self.rewards[self._ptr] = reward + self.terminals[self._ptr] = terminal + # increment pointer + self._ptr += 1 + if self._ptr == self.size: + self._ptr = 0 + + def get(self): + terminal = self.terminals[self._ptr - 1] + traj_slice = slice(self._last_ptr, self._ptr - (not terminal)) + self._last_ptr = self._ptr - (not terminal) + return { + "states": self.states[traj_slice], + "rewards": self.rewards[traj_slice], + "last_value": self.values[-1] + } + """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. @@ -59,17 +67,20 @@ def __init__( policy_net: DiscretePolicyNet, reward_discount: float, grad_iters: int = 1, - get_loss_on_rollout_finish: bool = False, - remote: bool = False + buffer_size: int = 10000, + get_loss_on_rollout: bool = False ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__(name, remote=remote) + super().__init__(name) self.policy_net = policy_net self.device = self.policy_net.device self.reward_discount = reward_discount self.grad_iters = grad_iters - self._get_loss_on_rollout_finish = get_loss_on_rollout_finish + self.buffer_size = buffer_size + self.get_loss_on_rollout = get_loss_on_rollout + + self._buffer = defaultdict(lambda: self.Buffer(self.policy_net.input_dim, size=self.buffer_size)) def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Return actions and log probabilities for given states.""" @@ -79,17 +90,35 @@ def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p - def get_rollout_info(self, trajectory: Trajectory): + def record( + self, + key: str, + state: np.ndarray, + action: dict, + reward: float, + next_state: np.ndarray, + terminal: bool + ): + self._buffer[key].put(state, action, reward, terminal) + + def get_rollout_info(self): if self._get_loss_on_rollout_finish: - return self.get_batch_loss(self._preprocess(trajectory)) + return self.get_batch_loss(self._get_batch(), explicit_grad=True) else: - return trajectory - - def _preprocess(self, trajectory: Trajectory) -> PGBatch: - rewards = np.append(trajectory.rewards, trajectory.actions[-1].value if trajectory.actions[-1] else .0) - return PGBatch(trajectory.states[:-1], discount_cumsum(rewards, self.reward_discount)[:-1]) - - def get_batch_loss(self, batch: PGBatch, explicit_grad: bool = False): + return self._get_batch() + + def _get_batch(self): + batch = defaultdict(list) + for buf in self._buffer: + trajectory = buf.get() + rewards = np.append(trajectory["rewards"], trajectory["last_val"]) + batch["states"].append(trajectory["states"]) + # Returns rewards-to-go, to be targets for the value function + batch["returns"].append(discount_cumsum(rewards, self.reward_discount)[:-1]) + + return {key: np.concatenate(vals) for key, vals in batch.items} + + def get_batch_loss(self, batch: dict, explicit_grad: bool = False): """ This should be called at the end of a simulation episode and the experiences obtained from the experience store's ``get`` method should be a sequential set, i.e., in the order in @@ -98,26 +127,26 @@ def get_batch_loss(self, batch: PGBatch, explicit_grad: bool = False): assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." self.policy_net.train() - states = batch.states returns = torch.from_numpy(np.asarray(batch.returns)).to(self.device) - _, logp = self.policy_net(states) + _, logp = self.policy_net(batch["states"]) loss = -(logp * returns).mean() - grad = self.policy_net.get_gradients(loss) if explicit_grad else None - return PGLossInfo(loss, grad=grad) + loss_info = {"loss": loss} + if explicit_grad: + loss_info["grad"] = self.policy_net.get_gradients(loss) + return loss_info - def update_with_multi_loss_info(self, loss_info_list: List[PGLossInfo]): + def update(self, loss_info_list: List[dict]): """Apply gradients to the underlying parameterized model.""" - self.policy_net.apply_gradients([loss_info.grad for loss_info in loss_info_list]) + self.policy_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) - def learn_from_multi_trajectories(self, trajectories: List[Trajectory]): - if self.remote: + def learn(self, batch: dict): + if self.grad_parallel: # TODO: distributed grad computation pass else: - batches = [self._preprocess(traj) for traj in trajectories] for _ in range(self.grad_iters): - self.update_with_multi_loss_info([self.get_batch_loss(batch, explicit_grad=True) for batch in batches]) + self.policy_net.step(self.get_batch_loss(batch)["grad"]) def set_state(self, policy_state): self.policy_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 1f82ba315..5bd70e971 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -41,36 +41,6 @@ def choose_action(self, state): class RLPolicy(AbsPolicy): - class Buffer: - """Sequence of transitions for an agent. - - Args: - states: Sequence of ``State`` objects traversed during simulation. - actions: Sequence of actions taken in response to the states. - rewards: Sequence of rewards received as a result of the actions. - info: Sequence of each transition's auxillary information. - """ - def __init__(self, state_dim: int, action_dim: int = 1, max_len: int = 10000): - self.states = np.zeros((max_len, state_dim), dtype=np.float32) - if action_dim == 1: - self.actions = np.zeros(max_len, dtype=np.float32) - else: - self.actions = np.zeros((max_len, action_dim), dtype=np.float32) - self.rewards = np.zeros(max_len, dtype=np.float32) - self.terminal = np.zeros(max_len, dtype=np.bool) - self.max_len = max_len - - self._ptr = 0 - self._last_ptr = 0 - - @abstractmethod - def put(self, transition): - raise NotImplementedError - - @abstractmethod - def get(self): - raise NotImplementedError - """Policy that learns from simulation experiences. Reinforcement learning (RL) policies should inherit from this. @@ -78,9 +48,9 @@ def get(self): Args: name (str): Name of the policy. """ - def __init__(self, name: str, remote: bool = False): + def __init__(self, name: str): super().__init__(name) - self.remote = remote + self.grad_parallel = False @abstractmethod def choose_action(self, state): @@ -98,11 +68,11 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): raise NotImplementedError @abstractmethod - def update_with_multi_loss_info(self, loss_info_list: List[dict]): + def update(self, loss_info_list: List[dict]): pass @abstractmethod - def learn_from_multi_trajectories(self, trajectories: List[dict]): + def learn(self, trajectories: List[dict]): """Perform policy improvement based on a list of trajectories obtained from parallel rollouts.""" raise NotImplementedError diff --git a/maro/rl/policy/replay.py b/maro/rl/policy/replay.py index 17a1fd9ae..3296644af 100644 --- a/maro/rl/policy/replay.py +++ b/maro/rl/policy/replay.py @@ -26,9 +26,9 @@ def __init__(self, capacity: int, state_dim: int, action_dim: int = 1, random_ov if action_dim > 1: self.actions = np.zeros((self._capacity, self._action_dim), dtype=np.float32) else: - self.actions = np.zeros(self._capacity, dtype=np.float32) + self.actions = np.zeros(self._capacity, dtype=np.int64) self.rewards = np.zeros(self._capacity, dtype=np.float32) - self.next_states = np.zeros(self._capacity, dtype=np.float32) + self.next_states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) self.terminals = np.zeros(self._capacity, dtype=np.bool) self._ptr = 0 @@ -86,5 +86,6 @@ def sample(self, size: int): "states": self.states[indexes], "actions": self.actions[indexes], "rewards": self.rewards[indexes], - "next_states": self.next_states[indexes] + "next_states": self.next_states[indexes], + "terminals": self.terminals[indexes] } diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 722e1f85d..6516163e1 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -19,8 +19,8 @@ "\n", "# Common info\n", "common_config = {\n", - " \"port_attributes\": [\"empty\", \"full\", \"on_shipper\", \"on_consignee\", \"booking\", \"shortage\", \"fulfillment\"],\n", - " \"vessel_attributes\": [\"empty\", \"full\", \"remaining_space\"],\n", + " \"port_features\": [\"empty\", \"full\", \"on_shipper\", \"on_consignee\", \"booking\", \"shortage\", \"fulfillment\"],\n", + " \"vessel_features\": [\"empty\", \"full\", \"remaining_space\"],\n", " # Parameters for computing states\n", " \"look_back\": 7,\n", " \"max_ports_downstream\": 2,\n", @@ -56,13 +56,13 @@ "\n", "class CIMEnvWrapper(AbsEnvWrapper):\n", " def __init__(\n", - " self, env, save_replay=True, replay_agent_ids=None, *, port_attributes, vessel_attributes, num_actions,\n", + " self, env, save_replay=True, replay_agent_ids=None, *, port_features, vessel_features, num_actions,\n", " look_back,max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay,\n", " finite_vessel_space=True, has_early_discharge=True \n", " ):\n", " super().__init__(env, save_replay=save_replay, replay_agent_ids=replay_agent_ids, reward_eval_delay=reward_eval_delay)\n", - " self.port_attributes = port_attributes\n", - " self.vessel_attributes = vessel_attributes\n", + " self.port_features = port_features\n", + " self.vessel_features = vessel_features\n", " self.action_space = list(np.linspace(-1.0, 1.0, num_actions))\n", " self.look_back = look_back\n", " self.max_ports_downstream = max_ports_downstream\n", @@ -80,8 +80,8 @@ " port_idx, vessel_idx = self.event.port_idx, self.event.vessel_idx\n", " ticks = [max(0, tick - rt) for rt in range(self.look_back - 1)]\n", " future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int')\n", - " port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes]\n", - " vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes]\n", + " port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_features]\n", + " vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_features]\n", " self.state_info = {\n", " \"tick\": tick, \"action_scope\": self.event.action_scope, \"port_idx\": port_idx, \"vessel_idx\": vessel_idx\n", " }\n", @@ -95,7 +95,7 @@ " model_action = action_info[0] if isinstance(action_info, tuple) else action_info\n", " tick, port, vessel = self.state_info[\"tick\"], self.state_info[\"port_idx\"], self.state_info[\"vessel_idx\"]\n", " zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero.\n", - " vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float(\"inf\")\n", + " vessel_space = vessel_snapshots[tick:vessel:self.vessel_features][2] if self.finite_vessel_space else float(\"inf\")\n", " early_discharge = vessel_snapshots[tick:vessel:\"early_discharge\"][0] if self.has_early_discharge else 0\n", " percent = abs(self.action_space[model_action])\n", "\n", @@ -153,15 +153,15 @@ "source": [ "import torch\n", "\n", - "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceMemory, FullyConnectedBlock, OptimOption\n", + "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceMemory, FullyConnected, OptimOption\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", "input_dim = (\n", " (common_config[\"look_back\"] + 1) *\n", " (common_config[\"max_ports_downstream\"] + 1) *\n", - " len(common_config[\"port_attributes\"]) +\n", - " len(common_config[\"vessel_attributes\"])\n", + " len(common_config[\"port_features\"]) +\n", + " len(common_config[\"vessel_features\"])\n", ")\n", "\n", "policy_config = {\n", @@ -218,8 +218,8 @@ "\n", "\n", "def get_ac_policy(name):\n", - " actor = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"actor\"])\n", - " critic = FullyConnectedBlock(**policy_config[\"model\"][\"network\"][\"critic\"])\n", + " actor = FullyConnected(**policy_config[\"model\"][\"network\"][\"actor\"])\n", + " critic = FullyConnected(**policy_config[\"model\"][\"network\"][\"critic\"])\n", " ac_net = MyACNet({\"actor\": actor, \"critic\": critic}, optim_option=policy_config[\"model\"][\"optimization\"])\n", " experience_memory = ExperienceMemory(policy_config[\"experience_memory\"][\"capacity\"])\n", " return ActorCritic(name, ac_net, experience_memory, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" From 7c407a43ca4072cb1a6f8ed5d8cd148c14382a9b Mon Sep 17 00:00:00 2001 From: ysqyang Date: Tue, 31 Aug 2021 17:15:46 +0800 Subject: [PATCH 421/482] extracted shaping logic out of env_sampler --- examples/rl/cim/config.py | 32 ++--- examples/rl/cim/env_sampler.py | 146 +++++++++++------------ examples/rl/vm_scheduling/__init__.py | 4 +- examples/rl/vm_scheduling/env_wrapper.py | 10 +- examples/rl/workflows/simple_learner.py | 4 +- maro/rl/learning/__init__.py | 4 +- maro/rl/learning/env_sampler.py | 145 ++++++++++------------ maro/rl/learning/learner.py | 8 +- maro/rl/learning/rollout_manager.py | 10 +- maro/rl/policy/ac.py | 10 +- maro/rl/policy/ddpg.py | 16 +-- maro/rl/policy/dqn.py | 20 ++-- maro/rl/policy/pg.py | 12 +- maro/rl/policy/policy.py | 9 +- 14 files changed, 192 insertions(+), 238 deletions(-) diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py index aef59901b..55885d9ef 100644 --- a/examples/rl/cim/config.py +++ b/examples/rl/cim/config.py @@ -8,18 +8,22 @@ "durations": 560 } -env_sampler_conf = { - "port_features": ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], - "vessel_features": ["empty", "full", "remaining_space"], - "num_actions": 21, - # Parameters for computing states +port_features = ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], +vessel_features = ["empty", "full", "remaining_space"] + +state_shaping_conf = { "look_back": 7, - "max_ports_downstream": 2, - # Parameters for computing actions + "max_ports_downstream": 2 +} + +action_shaping_conf = { + "action_space": [(i - 10) / 10 for i in range(21)], "finite_vessel_space": True, - "has_early_discharge": True, - # Parameters for computing rewards - "reward_eval_delay": 99, + "has_early_discharge": True +} + +reward_shaping_conf = { + "time_window": 99, "fulfillment_factor": 1.0, "shortage_factor": 1.0, "time_decay": 0.97 @@ -27,8 +31,8 @@ # obtain state dimension from a temporary env_wrapper instance state_dim = ( - (env_sampler_conf["look_back"] + 1) * (env_sampler_conf["max_ports_downstream"] + 1) * - len(env_sampler_conf["port_features"]) + len(env_sampler_conf["vessel_features"]) + (state_shaping_conf["look_back"] + 1) * (state_shaping_conf["max_ports_downstream"] + 1) * + len(state_shaping_conf["port_features"]) + len(state_shaping_conf["vessel_features"]) ) # DQN settings @@ -36,7 +40,7 @@ "network": { "input_dim": state_dim, "hidden_dims": [256, 128, 64, 32], - "output_dim": env_sampler_conf["num_actions"], + "output_dim": len(action_shaping_conf["num_actions"]), "activation": "leaky_relu", "softmax": False, "batch_norm": True, @@ -82,7 +86,7 @@ "actor": { "input_dim": state_dim, "hidden_dims": [256, 128, 64], - "output_dim": env_sampler_conf["num_actions"], + "output_dim": len(action_shaping_conf["action_space"]), "activation": "tanh", "softmax": True, "batch_norm": False, diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 7300aa7f6..5b67217cd 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -6,7 +6,7 @@ import numpy as np -from maro.rl.learning import AbsEnvSampler +from maro.rl.learning import EnvSampler from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType @@ -15,85 +15,67 @@ sys.path.insert(0, cim_path) from callbacks import post_step -from config import env_conf, env_sampler_conf +from config import action_shaping_conf, env_conf, reward_shaping_conf, state_shaping_conf, vessel_features from policies import policy_func_dict -class CIMEnvSampler(AbsEnvSampler): - def __init__( - self, get_env, get_policy_func_dict, agent2policy, get_eval_env=None, reward_eval_delay=99, post_step=None - ): - super().__init__( - get_env, get_policy_func_dict, agent2policy, - get_eval_env=get_eval_env, reward_eval_delay=reward_eval_delay, post_step=post_step - ) - self.action_space = list(np.linspace(-1.0, 1.0, env_sampler_conf["num_actions"])) - self._last_action_tick = None - self._state_info = None - - def get_state(self, event, tick=None): - if tick is None: - tick = self.env.tick - vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] - port_idx, vessel_idx = event.port_idx, event.vessel_idx - ticks = [max(0, tick - rt) for rt in range(env_sampler_conf["look_back"] - 1)] - future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') - port_features = port_snapshots[ticks: [port_idx] + list(future_port_list): env_sampler_conf["port_features"]] - vessel_features = vessel_snapshots[tick: vessel_idx: env_sampler_conf["vessel_features"]] - self._state_info = {"tick": tick, "action_scope": event.action_scope, "vessel_idx": vessel_idx} - state = np.concatenate((port_features, vessel_features)) - self._last_action_tick = tick - return {port_idx: state} - - def to_env_action(self, action_by_agent: dict): - port_idx, action = list(action_by_agent.items()).pop() - tick = self._state_info["tick"] - vessel = self._state_info["vessel_idx"] - action_scope = self._state_info["action_scope"] - vessel_snapshots = self.env.snapshot_list["vessels"] - vessel_space = ( - vessel_snapshots[tick:vessel:env_sampler_conf["vessel_features"]][2] - if env_sampler_conf["finite_vessel_space"] else float("inf") - ) - - model_action = action["action"] if isinstance(action, dict) else action - percent = abs(self.action_space[model_action]) - zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. - if model_action < zero_action_idx: - action_type = ActionType.LOAD - actual_action = min(round(percent * action_scope.load), vessel_space) - elif model_action > zero_action_idx: - action_type = ActionType.DISCHARGE - if env_sampler_conf["has_early_discharge"]: - early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] - else: - early_discharge = 0 - plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge - actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) - else: - actual_action, action_type = 0, None - - return [Action(port_idx=port_idx, vessel_idx=vessel, quantity=actual_action, action_type=action_type)] - - def get_reward(self, actions, tick=None): - """Delayed reward evaluation.""" - if tick is None: - tick = self._last_action_tick - start_tick = tick + 1 - ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) - - # Get the ports that took actions at the given tick - ports = [action.port_idx for action in actions] - port_snapshots = self.env.snapshot_list["ports"] - future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) - future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) +def get_state(env, event, state_shaping_conf): + vessel_snapshots, port_snapshots = env.snapshot_list["vessels"], env.snapshot_list["ports"] + port_idx, vessel_idx = event.port_idx, event.vessel_idx + ticks = [max(0, env.tick - rt) for rt in range(state_shaping_conf["look_back"] - 1)] + future_port_list = vessel_snapshots[env.tick: vessel_idx: 'future_stop_list'].astype('int') + port_features = port_snapshots[ticks: [port_idx] + list(future_port_list): state_shaping_conf["port_features"]] + vessel_features = vessel_snapshots[env.tick: vessel_idx: state_shaping_conf["vessel_features"]] + state = np.concatenate((port_features, vessel_features)) + return {port_idx: state} + + +def get_env_actions(action_by_agent, env, event, action_shaping_conf): + port_idx, action = list(action_by_agent.items()).pop() + vessel_idx, action_scope = event.vessel_idx, event.action_scope + vessel_snapshots = env.snapshot_list["vessels"] + vessel_space = ( + vessel_snapshots[env.tick:vessel_idx:vessel_features][2] + if action_shaping_conf["finite_vessel_space"] else float("inf") + ) - decay_list = [env_sampler_conf["time_decay"] ** i for i in range(self.reward_eval_delay)] - rewards = np.float32( - env_sampler_conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) - - env_sampler_conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) - ) - return {agent_id: reward for agent_id, reward in zip(ports, rewards)} + model_action = action["action"] if isinstance(action, dict) else action + percent = abs(action_shaping_conf["action_space"][model_action]) + zero_action_idx = len(action_shaping_conf["action_space"]) / 2 # index corresponding to value zero. + if model_action < zero_action_idx: + action_type = ActionType.LOAD + actual_action = min(round(percent * action_scope.load), vessel_space) + elif model_action > zero_action_idx: + action_type = ActionType.DISCHARGE + if action_shaping_conf["has_early_discharge"]: + early_discharge = vessel_snapshots[env.tick:vessel_idx:"early_discharge"][0] + else: + early_discharge = 0 + plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge + actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) + else: + actual_action, action_type = 0, None + + return [Action(port_idx=port_idx, vessel_idx=vessel_idx, quantity=actual_action, action_type=action_type)] + + +def get_reward(env, actions, tick, reward_shaping_conf): + """Delayed reward evaluation.""" + start_tick = tick + 1 + ticks = list(range(start_tick, start_tick + reward_shaping_conf["time_window"])) + + # Get the ports that took actions at the given tick + ports = [action.port_idx for action in actions] + port_snapshots = env.snapshot_list["ports"] + future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) + future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) + + decay_list = [reward_shaping_conf["time_decay"] ** i for i in range(reward_shaping_conf["time_window"])] + rewards = np.float32( + reward_shaping_conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) + - reward_shaping_conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) + ) + return {agent_id: reward for agent_id, reward in zip(ports, rewards)} agent2policy = { @@ -105,10 +87,16 @@ def get_reward(self, actions, tick=None): def get_env_sampler(): - return CIMEnvSampler( + return EnvSampler( lambda: Env(**env_conf), policy_func_dict, agent2policy, - reward_eval_delay=env_sampler_conf["reward_eval_delay"], - post_step=post_step + get_state, + get_env_actions, + get_reward, + reward_eval_delay=reward_shaping_conf["reward_eval_delay"], + post_step=post_step, + state_shaping_conf=state_shaping_conf, + action_shaping_conf=action_shaping_conf, + reward_shaping_conf=reward_shaping_conf ) diff --git a/examples/rl/vm_scheduling/__init__.py b/examples/rl/vm_scheduling/__init__.py index 4c417a160..44af25424 100644 --- a/examples/rl/vm_scheduling/__init__.py +++ b/examples/rl/vm_scheduling/__init__.py @@ -2,10 +2,10 @@ # Licensed under the MIT license. from .callbacks import post_collect, post_evaluate -from .env_wrapper import get_env_sampler, get_eval_env_wrapper +from .env_wrapper import get_env_sampler, get_test_env_wrapper from .policy_index import agent2policy, rl_policy_func_index, update_trigger, warmup __all__ = [ - "agent2policy", "post_collect", "post_evaluate", "get_env_sampler", "get_eval_env_wrapper", + "agent2policy", "post_collect", "post_evaluate", "get_env_sampler", "get_test_env_wrapper", "rl_policy_func_index", "update_trigger", "warmup" ] \ No newline at end of file diff --git a/examples/rl/vm_scheduling/env_wrapper.py b/examples/rl/vm_scheduling/env_wrapper.py index 86b45bd88..0e2d714e7 100644 --- a/examples/rl/vm_scheduling/env_wrapper.py +++ b/examples/rl/vm_scheduling/env_wrapper.py @@ -153,7 +153,7 @@ def _get_vm_state(self): } -eval_env_config = { +test_env_config = { "basic": { "scenario": "vm_scheduling", "topology": "azure.2019.10k.oversubscription", @@ -179,10 +179,10 @@ def get_env_sampler(replay_agent_ids=None): return VMEnvWrapper(env, **env_config["wrapper"]) -def get_eval_env_wrapper(): - eval_env = Env(**eval_env_config["basic"]) - eval_env.set_seed(eval_env_config["seed"]) - return VMEnvWrapper(eval_env, **eval_env_config["wrapper"]) +def get_test_env_wrapper(): + test_env = Env(**test_env_config["basic"]) + test_env.set_seed(test_env_config["seed"]) + return VMEnvWrapper(test_env, **test_env_config["wrapper"]) tmp_env_wrapper = get_env_sampler() diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index 0738eff7a..1df418fd0 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -14,7 +14,7 @@ with open(join(workflow_dir, "config.yml"), "r") as fp: config = yaml.safe_load(fp) -from general import get_env_sampler, get_eval_env_wrapper, log_dir, post_collect, post_evaluate +from general import get_env_sampler, get_test_env_wrapper, log_dir, post_collect, post_evaluate from rollout import get_agent_wrapper @@ -24,7 +24,7 @@ get_agent_wrapper(rollout_only=False), num_episodes=config["num_episodes"], num_steps=config["num_steps"], - get_eval_env_wrapper=get_eval_env_wrapper, + get_test_env_wrapper=get_test_env_wrapper, eval_schedule=config["eval_schedule"], post_collect=post_collect, post_evaluate=post_evaluate, diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index 8bf013138..8cac0dff3 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -2,14 +2,14 @@ # Licensed under the MIT license. from .early_stopper import AbsEarlyStopper -from .env_sampler import AbsEnvSampler +from .env_sampler import EnvSampler from .learner import Learner, simple_learner from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager, policy_host from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, SimpleRolloutManager __all__ = [ "AbsEarlyStopper", - "AbsEnvSampler", + "EnvSampler", "Learner", "simple_learner", "AbsPolicyManager", "DistributedPolicyManager", "SimplePolicyManager", "policy_host", "AbsRolloutManager", "DistributedRolloutManager", "SimpleRolloutManager" diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 5412df9a3..b9fb59415 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -1,13 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from abc import ABC, abstractmethod from collections import defaultdict, deque from os import getcwd from typing import Callable, Dict from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.policy import AbsPolicy, RLPolicy +from maro.rl.policy import RLPolicy from maro.rl.utils import MsgKey, MsgTag from maro.simulator import Env from maro.utils import Logger @@ -15,7 +14,7 @@ from .common import get_rollout_finish_msg -class AbsEnvSampler(ABC): +class EnvSampler: """Simulation data collector and policy evaluator. Args: @@ -24,10 +23,19 @@ class AbsEnvSampler(ABC): get_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. agent2policy (Dict[str, str]): A dictionary that maps agent IDs to policy IDs, i.e., specifies the policy used - by each agent. - get_eval_env (Callable): Function to create an ``Env`` instance for evaluation. The function should - take no parameters and return an environment wrapper instance. If this is None, the training environment - wrapper will be used for evaluation in the worker processes. Defaults to None. + by each agent. + get_state (Callable): Function to compute the state. The function takes as input an ``Env``, an event and a + dictionary of keyword parameters and returns a state vector encoded as a one-dimensional (flattened) Numpy + arrays for each agent involved as a dictionary. + get_env_actions (Callable): Function to convert policy outputs to action objects that can be passed directly to + the environment's ``step`` method. The function takes as input an ``Env``, a dictionary of a set of agents' + policy outputs, an event and a dictionary of keyword parameters and returns a list of action objects. + get_reward (Callable): Function to compute rewards for a list of actions that occurred at a given tick. The + function takes as input an ``Env``, a list of actions (output by ``get_env_actions``), a tick and a + dictionary of keyword parameters and returns a scalar reward for each agent as a dictionary. + get_test_env (Callable): Function to create an ``Env`` instance for testing policy performance. The function + should take no parameters and return an environment wrapper instance. If this is None, the training + environment wrapper will be used for evaluation in the worker processes. Defaults to None. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. @@ -45,62 +53,43 @@ def __init__( get_env: Callable[[], Env], get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str], - get_eval_env: Callable[[], Env] = None, + get_state: Dict[str, Callable], + get_env_actions: Callable, + get_reward: Callable, + get_test_env: Callable[[], Env] = None, reward_eval_delay: int = 0, - post_step: Callable = None + post_step: Callable = None, + state_shaping_kwargs: dict = {}, + action_shaping_kwargs: dict = {}, + reward_shaping_kwargs: dict = {} ): - self.env = get_env() + self._learn_env = get_env() + self._test_env = get_test_env() if get_test_env else self._learn_env + self.env = None + self.policy_dict = {id_: func(id_) for id_, func in get_policy_func_dict.items()} self.policy_by_agent = {agent: self.policy_dict[policy_id] for agent, policy_id in agent2policy.items()} - self.eval_env = get_eval_env() if get_eval_env else self.env self.reward_eval_delay = reward_eval_delay self._post_step = post_step + # shaping + self._get_state = get_state + self._state_shaping_kwargs = state_shaping_kwargs + self._get_env_actions = get_env_actions + self._action_shaping_kwargs = action_shaping_kwargs + self._get_reward = get_reward + self._reward_shaping_kwargs = reward_shaping_kwargs + self._step_index = 0 self._terminal = True self._transition_cache = defaultdict(deque) # for caching transitions whose rewards have yet to be evaluated - self._prev_state = defaultdict(lambda: None) + self._prev_state = defaultdict(lambda: None) self.tracker = {} # User-defined tracking information is placed here. - @abstractmethod - def get_state(self, event, eval: bool = False, tick: int = None) -> dict: - """Compute the state for a given tick. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - - Returns: - A dictionary with (agent ID, state) as key-value pairs. - """ - raise NotImplementedError - - @abstractmethod - def to_env_action(self, action, eval: bool = False) -> dict: - """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" - raise NotImplementedError - - @abstractmethod - def get_reward(self, actions: list, tick: int = None, eval: bool = False): - """Evaluate the reward for an action. - - Args: - tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in - ``actions`` must be an Action object defined for the environment in question. The tick may - be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). - Defaults to None. - - Returns: - A dictionary with (agent ID, reward) as key-value pairs. - """ - raise NotImplementedError - - def get_agents_from_event(self, event): - return self.env.agent_idx_list - def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): + self.env = self._learn_env # set policy states if policy_state_dict: for policy_id, policy_state in policy_state_dict.items(): @@ -117,20 +106,25 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio policy.exploration_step() if self._terminal: - # get initial state - self.reset() + # reset and get initial state + self.env.reset() + self._step_index = 0 + self._transition_cache.clear() + self._prev_state = defaultdict(lambda: None) + self.tracker.clear() + self._terminal = False _, event, _ = self.env.step(None) - self._state = self.get_state(event) + state = self._get_state(self.env, event, self._state_shaping_kwargs) starting_step_index = self._step_index + 1 steps_to_go = float("inf") if num_steps == -1 else num_steps while not self._terminal and steps_to_go > 0: - action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in self._state.items()} - env_action = self.to_env_action(action) - for agent in self._state: - self._transition_cache[agent].append((self._state[agent], action[agent], env_action, self.env.tick)) - _, event, self._terminal = self.env.step(env_action) - self._state = None if self._terminal else self.get_state(event) + action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in state.items()} + env_actions = self._get_env_actions(self.env, action, event, self._action_shaping_kwargs) + for agent in state: + self._transition_cache[agent].append((state[agent], action[agent], env_actions, self.env.tick)) + _, event, self._terminal = self.env.step(env_actions) + state = None if self._terminal else self._get_state(self.env, event, self._state_shaping_kwargs) self._step_index += 1 steps_to_go -= 1 @@ -140,11 +134,11 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio """ for agent, cache in self._transition_cache.items(): while cache and (self._terminal or self.env.tick - cache[0][-1] >= self.reward_eval_delay): - state, action, env_action, tick = cache.popleft() - reward = self.get_reward(env_action, tick=tick) + state, action, env_actions, tick = cache.popleft() + reward = self._get_reward(self.env, env_actions, tick, self._reward_shaping_kwargs) if self._post_step: # put things you want to track in the tracker attribute - self._post_step(self.env, self.tracker, state, action, env_action, reward, tick) + self._post_step(self.env, self.tracker, state, action, env_actions, reward, tick) if isinstance(self.policy_by_agent[agent], RLPolicy) and self._prev_state[agent] is not None: self.policy_by_agent[agent].record( @@ -167,6 +161,7 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio } def test(self, policy_state_dict: dict = None): + self.env = self._test_env # set policy states if policy_state_dict: for id_, policy_state in policy_state_dict.items(): @@ -177,28 +172,20 @@ def test(self, policy_state_dict: dict = None): if hasattr(policy, "exploit"): policy.exploit() - self.eval_env.reset() + self.env.reset() terminal = False # get initial state - _, event, _ = self.eval_env.step(None) - state = self.get_state(event, eval=True) + _, event, _ = self.env.step(None) + state = self._get_state(self.env, event, self._state_shaping_kwargs) while not terminal: - action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in state.items()} - env_action = self.to_env_action(action, eval=True) - _, _, terminal = self.eval_env.step(env_action) + action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in state.items()} + env_actions = self._get_env_actions(self.env, action, event, self._action_shaping_kwargs) + _, event, terminal = self.env.step(env_actions) if not terminal: - state = self.get_state(event, eval=True) + state = self._get_state(self.env, event, self._state_shaping_kwargs) return self.tracker - def reset(self): - self.env.reset() - self._step_index = 0 - self._state = None - self._transition_cache.clear() - self.tracker.clear() - self._terminal = False - def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = getcwd()): """Roll-out worker process that can be launched on separate computation nodes. @@ -286,7 +273,7 @@ def actor( peers = {"policy_server": 1} proxy = Proxy(group, "actor", peers, component_name=f"ACTOR.{index}", **proxy_kwargs) - policy_server_address = proxy.peers["policy_server"][0] + server_address = proxy.peers["policy_server"][0] logger = Logger(proxy.name, dump_folder=log_dir) # get initial policy states from the policy manager @@ -307,7 +294,7 @@ def actor( # Send roll-out info to policy server for learning reply = proxy.send( SessionMessage( - MsgTag.SAMPLE_DONE, proxy.name, policy_server_address, + MsgTag.SAMPLE_DONE, proxy.name, server_address, body={MsgKey.ROLLOUT_INFO: result["rollout_info"], MsgKey.VERSION: policy_version} ) )[0] @@ -318,7 +305,5 @@ def actor( exploration_step = False # tell the policy server I'm all done. - proxy.isend( - SessionMessage(MsgTag.DONE, proxy.name, policy_server_address, session_type=SessionType.NOTIFICATION) - ) + proxy.isend(SessionMessage(MsgTag.DONE, proxy.name, server_address, session_type=SessionType.NOTIFICATION)) proxy.close() diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py index bb639e6a0..fa94d9538 100644 --- a/maro/rl/learning/learner.py +++ b/maro/rl/learning/learner.py @@ -9,13 +9,13 @@ from .common import get_eval_schedule, get_rollout_finish_msg from .early_stopper import AbsEarlyStopper -from .env_sampler import AbsEnvSampler +from .env_sampler import EnvSampler from .policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager def simple_learner( - get_env_sampler: Callable[[], AbsEnvSampler], + get_env_sampler: Callable[[], EnvSampler], num_episodes: int, num_steps: int = -1, eval_schedule: Union[int, List[int]] = None, @@ -36,7 +36,7 @@ def simple_learner( collect-update cycles, depending on how the implementation of the roll-out manager. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - get_eval_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should + get_test_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the worker processes. Defaults to None. eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will @@ -102,7 +102,7 @@ def collect_and_update(ep): post_evaluate([tracker]) # early stopping check if early_stopper: - early_stopper.push(env_sampler.eval_env.summary) + early_stopper.push(env_sampler.test_env.summary) if early_stopper.stop(): return diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index f17be58a2..d99e96747 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -15,7 +15,7 @@ from maro.utils import Logger, set_seeds from .common import get_rollout_finish_msg -from .env_sampler import AbsEnvSampler +from .env_sampler import EnvSampler def concat_batches(batch_list: List[dict]): @@ -78,7 +78,7 @@ class SimpleRolloutManager(AbsRolloutManager): Args: get_env_sampler (Callable): Function to create an environment sampler for collecting training data. The function - should take no parameters and return an ``AbsEnvSampler`` instance. + should take no parameters and return an ``EnvSampler`` instance. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. post_collect (Callable): Custom function to process whatever information is collected by each @@ -95,7 +95,7 @@ class SimpleRolloutManager(AbsRolloutManager): """ def __init__( self, - get_env_sampler: Callable[[], AbsEnvSampler], + get_env_sampler: Callable[[], EnvSampler], num_steps: int = -1, parallelism: int = 1, eval_parallelism: int = 1, @@ -214,7 +214,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): # concat batches from different roll-out workers for policy_id, info_list in info_by_policy.items(): - if not "loss" in info_list[0]: + if "loss" not in info_list[0]: info_by_policy[policy_id] = concat_batches(info_list) return info_by_policy @@ -394,7 +394,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): # concat batches from different roll-out workers for policy_id, info_list in info_by_policy.items(): - if not "loss" in info_list[0]: + if "loss" not in info_list[0]: info_by_policy[policy_id] = concat_batches(info_list) return info_by_policy diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index fe4ae4f2c..493c887bf 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -141,7 +141,7 @@ def record( next_state: np.ndarray, terminal: bool ): - self._buffer[key].put(state, action, reward, terminal) + self._buffer[key].put(state, action, reward, terminal) def get_rollout_info(self): if self.get_loss_on_rollout: @@ -212,12 +212,8 @@ def update(self, loss_info_list: List[dict]): self.ac_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) def learn(self, batch: dict): - if self.grad_parallel: - # TODO: distributed grad computation - pass - else: - for _ in range(self.grad_iters): - self.ac_net.step(self.get_batch_loss(batch)["loss"]) + for _ in range(self.grad_iters): + self.ac_net.step(self.get_batch_loss(batch)["loss"]) def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 95b2f5eab..bf10d1dc2 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -150,16 +150,12 @@ def learn(self, batch: dict): batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] ) - if self.grad_parallel: - # TODO: distributed grad computation - pass - else: - for _ in range(self.num_epochs): - train_batch = self._replay_memory.sample(self.batch_size) - self.ac_net.step(self.get_batch_loss(train_batch)["loss"]) - self._ac_net_version += 1 - if self._ac_net_version - self._target_ac_net_version == self.update_target_every: - self._update_target() + for _ in range(self.num_epochs): + train_batch = self._replay_memory.sample(self.batch_size) + self.ac_net.step(self.get_batch_loss(train_batch)["loss"]) + self._ac_net_version += 1 + if self._ac_net_version - self._target_ac_net_version == self.update_target_every: + self._update_target() def _update_target(self): # soft-update target network diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index daa4cb08b..1a4577c88 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -292,18 +292,14 @@ def learn(self, batch: dict): batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] ) - if self.grad_parallel: - # TODO: distributed grad computation - pass - else: - for _ in range(self.num_epochs): - loss_info = self.get_batch_loss(self._get_batch()) - if self.prioritized_replay: - self._per.update(loss_info["indexes"], loss_info["td_errors"]) - self.q_net.step(loss_info["loss"]) - self._q_net_version += 1 - if self._q_net_version - self._target_q_net_version == self.update_target_every: - self._update_target() + for _ in range(self.num_epochs): + loss_info = self.get_batch_loss(self._get_batch()) + if self.prioritized_replay: + self._per.update(loss_info["indexes"], loss_info["td_errors"]) + self.q_net.step(loss_info["loss"]) + self._q_net_version += 1 + if self._q_net_version - self._target_q_net_version == self.update_target_every: + self._update_target() def _update_target(self): # soft-update target network diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index ee97ac8c2..d9daafb95 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -99,14 +99,14 @@ def record( next_state: np.ndarray, terminal: bool ): - self._buffer[key].put(state, action, reward, terminal) + self._buffer[key].put(state, action, reward, terminal) def get_rollout_info(self): if self._get_loss_on_rollout_finish: return self.get_batch_loss(self._get_batch(), explicit_grad=True) else: return self._get_batch() - + def _get_batch(self): batch = defaultdict(list) for buf in self._buffer: @@ -141,12 +141,8 @@ def update(self, loss_info_list: List[dict]): self.policy_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) def learn(self, batch: dict): - if self.grad_parallel: - # TODO: distributed grad computation - pass - else: - for _ in range(self.grad_iters): - self.policy_net.step(self.get_batch_loss(batch)["grad"]) + for _ in range(self.grad_iters): + self.policy_net.step(self.get_batch_loss(batch)["grad"]) def set_state(self, policy_state): self.policy_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 5bd70e971..918729617 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -4,8 +4,6 @@ from abc import ABC, abstractmethod from typing import List -import numpy as np - class AbsPolicy(ABC): """Abstract policy class. @@ -17,15 +15,11 @@ class AbsPolicy(ABC): def __init__(self, name: str): super().__init__() self._name = name - self.agents = set() @property def name(self): return self._name - def add_agent(self, agent: str): - self.agents.add(agent) - @abstractmethod def choose_action(self, state): raise NotImplementedError @@ -50,7 +44,6 @@ class RLPolicy(AbsPolicy): """ def __init__(self, name: str): super().__init__(name) - self.grad_parallel = False @abstractmethod def choose_action(self, state): @@ -58,7 +51,7 @@ def choose_action(self, state): def record(self, key: str, state, action, reward, next_state, terminal: bool): pass - + @abstractmethod def get_rollout_info(self): raise NotImplementedError From 07a051b73fa3b6c01a445403724c50f28013c5f5 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Tue, 31 Aug 2021 09:57:33 +0000 Subject: [PATCH 422/482] fixed bug in CIM shaping and lint issues --- examples/rl/cim/config.py | 10 +++--- examples/rl/cim/env_sampler.py | 59 ++++++++++++++++----------------- maro/rl/learning/env_sampler.py | 6 ++-- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py index 55885d9ef..53b4fd9ea 100644 --- a/examples/rl/cim/config.py +++ b/examples/rl/cim/config.py @@ -8,8 +8,8 @@ "durations": 560 } -port_features = ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"], -vessel_features = ["empty", "full", "remaining_space"] +port_attributes = ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"] +vessel_attributes = ["empty", "full", "remaining_space"] state_shaping_conf = { "look_back": 7, @@ -31,8 +31,8 @@ # obtain state dimension from a temporary env_wrapper instance state_dim = ( - (state_shaping_conf["look_back"] + 1) * (state_shaping_conf["max_ports_downstream"] + 1) * - len(state_shaping_conf["port_features"]) + len(state_shaping_conf["vessel_features"]) + (state_shaping_conf["look_back"] + 1) * (state_shaping_conf["max_ports_downstream"] + 1) * len(port_attributes) + + len(vessel_attributes) ) # DQN settings @@ -40,7 +40,7 @@ "network": { "input_dim": state_dim, "hidden_dims": [256, 128, 64, 32], - "output_dim": len(action_shaping_conf["num_actions"]), + "output_dim": len(action_shaping_conf["action_space"]), "activation": "leaky_relu", "softmax": False, "batch_norm": True, diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 5b67217cd..51d8f6003 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -15,54 +15,51 @@ sys.path.insert(0, cim_path) from callbacks import post_step -from config import action_shaping_conf, env_conf, reward_shaping_conf, state_shaping_conf, vessel_features +from config import ( + action_shaping_conf, env_conf, port_attributes, reward_shaping_conf, state_shaping_conf, vessel_attributes +) from policies import policy_func_dict -def get_state(env, event, state_shaping_conf): +def get_state(env, event, conf): vessel_snapshots, port_snapshots = env.snapshot_list["vessels"], env.snapshot_list["ports"] port_idx, vessel_idx = event.port_idx, event.vessel_idx - ticks = [max(0, env.tick - rt) for rt in range(state_shaping_conf["look_back"] - 1)] - future_port_list = vessel_snapshots[env.tick: vessel_idx: 'future_stop_list'].astype('int') - port_features = port_snapshots[ticks: [port_idx] + list(future_port_list): state_shaping_conf["port_features"]] - vessel_features = vessel_snapshots[env.tick: vessel_idx: state_shaping_conf["vessel_features"]] - state = np.concatenate((port_features, vessel_features)) + ticks = [max(0, env.tick - rt) for rt in range(conf["look_back"] - 1)] + future_port_list = vessel_snapshots[env.tick: vessel_idx: 'future_stop_list'].astype('int') + state = np.concatenate([ + port_snapshots[ticks : [port_idx] + list(future_port_list) : port_attributes], + vessel_snapshots[env.tick : vessel_idx : vessel_attributes] + ]) return {port_idx: state} -def get_env_actions(action_by_agent, env, event, action_shaping_conf): +def get_env_actions(env, action_by_agent, event, conf): port_idx, action = list(action_by_agent.items()).pop() - vessel_idx, action_scope = event.vessel_idx, event.action_scope - vessel_snapshots = env.snapshot_list["vessels"] - vessel_space = ( - vessel_snapshots[env.tick:vessel_idx:vessel_features][2] - if action_shaping_conf["finite_vessel_space"] else float("inf") - ) + vsl_idx, action_scope = event.vessel_idx, event.action_scope + vsl_snapshots = env.snapshot_list["vessels"] + vsl_space = vsl_snapshots[env.tick:vsl_idx:vessel_attributes][2] if conf["finite_vessel_space"] else float("inf") model_action = action["action"] if isinstance(action, dict) else action - percent = abs(action_shaping_conf["action_space"][model_action]) - zero_action_idx = len(action_shaping_conf["action_space"]) / 2 # index corresponding to value zero. + percent = abs(conf["action_space"][model_action]) + zero_action_idx = len(conf["action_space"]) / 2 # index corresponding to value zero. if model_action < zero_action_idx: action_type = ActionType.LOAD - actual_action = min(round(percent * action_scope.load), vessel_space) + actual_action = min(round(percent * action_scope.load), vsl_space) elif model_action > zero_action_idx: action_type = ActionType.DISCHARGE - if action_shaping_conf["has_early_discharge"]: - early_discharge = vessel_snapshots[env.tick:vessel_idx:"early_discharge"][0] - else: - early_discharge = 0 + early_discharge = vsl_snapshots[env.tick:vsl_idx:"early_discharge"][0] if conf["has_early_discharge"] else 0 plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) else: actual_action, action_type = 0, None - return [Action(port_idx=port_idx, vessel_idx=vessel_idx, quantity=actual_action, action_type=action_type)] + return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)] -def get_reward(env, actions, tick, reward_shaping_conf): +def get_reward(env, actions, tick, conf): """Delayed reward evaluation.""" start_tick = tick + 1 - ticks = list(range(start_tick, start_tick + reward_shaping_conf["time_window"])) + ticks = list(range(start_tick, start_tick + conf["time_window"])) # Get the ports that took actions at the given tick ports = [action.port_idx for action in actions] @@ -70,10 +67,10 @@ def get_reward(env, actions, tick, reward_shaping_conf): future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) - decay_list = [reward_shaping_conf["time_decay"] ** i for i in range(reward_shaping_conf["time_window"])] + decay_list = [conf["time_decay"] ** i for i in range(conf["time_window"])] rewards = np.float32( - reward_shaping_conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) - - reward_shaping_conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) + conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) + - conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) ) return {agent_id: reward for agent_id, reward in zip(ports, rewards)} @@ -94,9 +91,9 @@ def get_env_sampler(): get_state, get_env_actions, get_reward, - reward_eval_delay=reward_shaping_conf["reward_eval_delay"], + reward_eval_delay=reward_shaping_conf["time_window"], post_step=post_step, - state_shaping_conf=state_shaping_conf, - action_shaping_conf=action_shaping_conf, - reward_shaping_conf=reward_shaping_conf + state_shaping_kwargs=state_shaping_conf, + action_shaping_kwargs=action_shaping_conf, + reward_shaping_kwargs=reward_shaping_conf ) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index b9fb59415..e9a2a0228 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -32,7 +32,7 @@ class EnvSampler: policy outputs, an event and a dictionary of keyword parameters and returns a list of action objects. get_reward (Callable): Function to compute rewards for a list of actions that occurred at a given tick. The function takes as input an ``Env``, a list of actions (output by ``get_env_actions``), a tick and a - dictionary of keyword parameters and returns a scalar reward for each agent as a dictionary. + dictionary of keyword parameters and returns a scalar reward for each agent as a dictionary. get_test_env (Callable): Function to create an ``Env`` instance for testing policy performance. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the worker processes. Defaults to None. @@ -55,7 +55,7 @@ def __init__( agent2policy: Dict[str, str], get_state: Dict[str, Callable], get_env_actions: Callable, - get_reward: Callable, + get_reward: Callable, get_test_env: Callable[[], Env] = None, reward_eval_delay: int = 0, post_step: Callable = None, @@ -277,7 +277,7 @@ def actor( logger = Logger(proxy.name, dump_folder=log_dir) # get initial policy states from the policy manager - msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, policy_server_address) + msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, server_address) reply = proxy.send(msg)[0] policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] From 6a027faf8361810da99f2429671b07fc8cada66b Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 1 Sep 2021 06:41:45 +0000 Subject: [PATCH 423/482] preliminary implemetation of parallel batch inference --- examples/rl/cim/config.py | 2 +- examples/rl/cim/env_sampler.py | 40 ++-- examples/rl/cim/policies.py | 4 +- examples/rl/workflows/general.py | 2 +- examples/rl/workflows/simple_learner.py | 18 +- maro/rl/learning/env_sampler.py | 246 ++++++++++++++++----- maro/rl/learning/{common.py => helpers.py} | 0 maro/rl/learning/learner.py | 2 +- maro/rl/learning/rollout_manager.py | 2 +- maro/rl/policy/ac.py | 2 +- maro/rl/policy/dqn.py | 3 + maro/rl/policy/pg.py | 2 +- 12 files changed, 229 insertions(+), 94 deletions(-) rename maro/rl/learning/{common.py => helpers.py} (100%) diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py index 53b4fd9ea..76316b3ba 100644 --- a/examples/rl/cim/config.py +++ b/examples/rl/cim/config.py @@ -5,7 +5,7 @@ env_conf = { "scenario": "cim", "topology": "toy.4p_ssdd_l0.0", - "durations": 560 + "durations": 280 } port_attributes = ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"] diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 51d8f6003..451970828 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -21,10 +21,10 @@ from policies import policy_func_dict -def get_state(env, event, conf): +def get_state(env, event): vessel_snapshots, port_snapshots = env.snapshot_list["vessels"], env.snapshot_list["ports"] port_idx, vessel_idx = event.port_idx, event.vessel_idx - ticks = [max(0, env.tick - rt) for rt in range(conf["look_back"] - 1)] + ticks = [max(0, env.tick - rt) for rt in range(state_shaping_conf["look_back"] - 1)] future_port_list = vessel_snapshots[env.tick: vessel_idx: 'future_stop_list'].astype('int') state = np.concatenate([ port_snapshots[ticks : [port_idx] + list(future_port_list) : port_attributes], @@ -33,21 +33,25 @@ def get_state(env, event, conf): return {port_idx: state} -def get_env_actions(env, action_by_agent, event, conf): +def get_env_actions(env, action_by_agent, event): + action_space = action_shaping_conf["action_space"] + finite_vsl_space = action_shaping_conf["finite_vessel_space"] + has_early_discharge = action_shaping_conf["has_early_discharge"] + port_idx, action = list(action_by_agent.items()).pop() vsl_idx, action_scope = event.vessel_idx, event.action_scope vsl_snapshots = env.snapshot_list["vessels"] - vsl_space = vsl_snapshots[env.tick:vsl_idx:vessel_attributes][2] if conf["finite_vessel_space"] else float("inf") + vsl_space = vsl_snapshots[env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float("inf") - model_action = action["action"] if isinstance(action, dict) else action - percent = abs(conf["action_space"][model_action]) - zero_action_idx = len(conf["action_space"]) / 2 # index corresponding to value zero. + model_action = action["action"] if isinstance(action, dict) else action + percent = abs(action_space[model_action]) + zero_action_idx = len(action_space) / 2 # index corresponding to value zero. if model_action < zero_action_idx: action_type = ActionType.LOAD actual_action = min(round(percent * action_scope.load), vsl_space) elif model_action > zero_action_idx: action_type = ActionType.DISCHARGE - early_discharge = vsl_snapshots[env.tick:vsl_idx:"early_discharge"][0] if conf["has_early_discharge"] else 0 + early_discharge = vsl_snapshots[env.tick:vsl_idx:"early_discharge"][0] if has_early_discharge else 0 plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) else: @@ -56,10 +60,10 @@ def get_env_actions(env, action_by_agent, event, conf): return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)] -def get_reward(env, actions, tick, conf): +def get_reward(env, actions, tick): """Delayed reward evaluation.""" start_tick = tick + 1 - ticks = list(range(start_tick, start_tick + conf["time_window"])) + ticks = list(range(start_tick, start_tick + reward_shaping_conf["time_window"])) # Get the ports that took actions at the given tick ports = [action.port_idx for action in actions] @@ -67,19 +71,19 @@ def get_reward(env, actions, tick, conf): future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) - decay_list = [conf["time_decay"] ** i for i in range(conf["time_window"])] + decay_list = [reward_shaping_conf["time_decay"] ** i for i in range(reward_shaping_conf["time_window"])] rewards = np.float32( - conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) - - conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) + reward_shaping_conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) + - reward_shaping_conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) ) return {agent_id: reward for agent_id, reward in zip(ports, rewards)} agent2policy = { - 0: "ac.0", - 1: "ac.1", + 0: "ac", + 1: "ac", 2: "dqn", - 3: "ac.2" + 3: "ac" } @@ -93,7 +97,5 @@ def get_env_sampler(): get_reward, reward_eval_delay=reward_shaping_conf["time_window"], post_step=post_step, - state_shaping_kwargs=state_shaping_conf, - action_shaping_kwargs=action_shaping_conf, - reward_shaping_kwargs=reward_shaping_conf + policies_to_parallelize=["ac"] ) diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index 77dab99d8..986059f25 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -69,7 +69,5 @@ def forward(self, states, actor: bool = True, critic: bool = True): policy_func_dict = { "dqn": get_dqn, - "ac.0": get_ac, - "ac.1": get_ac, - "ac.2": get_ac + "ac": get_ac } diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 9a470fbb4..460d84ee3 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -12,7 +12,7 @@ if rl_example_dir not in sys.path: sys.path.insert(0, rl_example_dir) -log_dir = join(rl_example_dir, "log", getenv("JOB")) +log_dir = join(rl_example_dir, "log", getenv("JOB", "")) module = importlib.import_module(f"{getenv('SCENARIO')}") diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py index 1df418fd0..64821676e 100644 --- a/examples/rl/workflows/simple_learner.py +++ b/examples/rl/workflows/simple_learner.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import importlib import sys import yaml from os.path import dirname, join, realpath @@ -14,17 +15,24 @@ with open(join(workflow_dir, "config.yml"), "r") as fp: config = yaml.safe_load(fp) -from general import get_env_sampler, get_test_env_wrapper, log_dir, post_collect, post_evaluate -from rollout import get_agent_wrapper +rl_example_dir = dirname(workflow_dir) +if rl_example_dir not in sys.path: + sys.path.insert(0, rl_example_dir) + +log_dir = join(rl_example_dir, "log", config["job"]) + +module = importlib.import_module(config["scenario"]) +get_env_sampler = getattr(module, "get_env_sampler") +policy_func_dict = getattr(module, "policy_func_dict") +post_collect = getattr(module, "post_collect", None) +post_evaluate = getattr(module, "post_evaluate", None) if __name__ == "__main__": simple_learner( - get_env_sampler(), - get_agent_wrapper(rollout_only=False), + get_env_sampler, num_episodes=config["num_episodes"], num_steps=config["num_steps"], - get_test_env_wrapper=get_test_env_wrapper, eval_schedule=config["eval_schedule"], post_collect=post_collect, post_evaluate=post_evaluate, diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index e9a2a0228..95269788c 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -2,8 +2,11 @@ # Licensed under the MIT license. from collections import defaultdict, deque +from multiprocessing import Pipe, Process from os import getcwd -from typing import Callable, Dict +from typing import Callable, Dict, List + +import numpy as np from maro.communication import Proxy, SessionMessage, SessionType from maro.rl.policy import RLPolicy @@ -11,7 +14,150 @@ from maro.simulator import Env from maro.utils import Logger -from .common import get_rollout_finish_msg +from .helpers import get_rollout_finish_msg + + +class AgentWrapper: + def __init__( + self, + get_policy_func_dict: Dict[str, Callable], + agent2policy: Dict[str, str], + policies_to_parallelize: List[str] = [] + ): + self._policies_to_parallelize = set(policies_to_parallelize) + self.policy_dict = { + id_: func(id_) for id_, func in get_policy_func_dict.items() if id_ not in self._policies_to_parallelize + } + self.agent2policy = agent2policy + self.policy_by_agent = { + agent: self.policy_dict[policy_id] for agent, policy_id in agent2policy.items() + if policy_id in self.policy_dict + } + + self._policy_hosts = [] + self._conn = {} + + def _policy_host(name, get_policy, conn): + policy = get_policy(name) + while True: + msg = conn.recv() + if msg["type"] == "choose_action": + actions = policy.choose_action(msg["states"]) + conn.send(actions) + elif msg["type"] == "set_state": + policy.set_state(msg["policy_state"]) + elif msg["type"] == "explore": + policy.explore() + elif msg["type"] == "exploit": + policy.exploit() + elif msg["type"] == "exploration_step": + policy.exploration_step() + elif msg["type"] == "rollout_info": + conn.send(policy.get_rollout_info()) + elif msg["type"] == "exploration_params": + conn.send(policy.exploration_params) + elif msg["type"] == "record": + policy.record( + msg["agent"], msg["state"], msg["action"], msg["reward"], msg["next_state"], msg["terminal"] + ) + elif msg["type"] == "update": + policy.update(msg["loss_info"]) + elif msg["type"] == "learn": + policy.learn(msg["batch"]) + + for id_ in policies_to_parallelize: + conn1, conn2 = Pipe() + self._conn[id_] = conn1 + host = Process(target=_policy_host, args=(id_, get_policy_func_dict[id_], conn2)) + self._policy_hosts.append(host) + host.start() + + def choose_action(self, state_by_agent: Dict[str, np.ndarray]): + states_by_policy, agents_by_policy, action = defaultdict(list), defaultdict(list), {} + for agent, state in state_by_agent.items(): + if self.agent2policy[agent] in self._conn: + states_by_policy[self.agent2policy[agent]].append(state) + agents_by_policy[self.agent2policy[agent]].append(agent) + + for policy_id, states in states_by_policy.items(): + self._conn[policy_id].send({"type": "choose_action", "states": np.concatenate(states)}) + + for policy_id, states in states_by_policy.items(): + msg = self._conn[policy_id].recv() + if len(states) == 1: + msg = [msg] + for agent, act in zip(agents_by_policy[policy_id], msg): + action[agent] = act + + return { + **action, + **{ + agent: self.policy_by_agent[agent].choose_action(state) for agent, state in state_by_agent.items() + if agent in self.policy_by_agent + } + } + + def set_policy_states(self, policy_state_dict: dict): + for policy_id, conn in self._conn.items(): + conn.send({"type": "set_state", "policy_state": policy_state_dict[policy_id]}) + for policy_id, policy_state in policy_state_dict.items(): + if policy_id not in self._conn: + self.policy_dict[policy_id].set_state(policy_state) + + def explore(self): + for conn in self._conn.values(): + conn.send({"type": "explore"}) + for policy in self.policy_dict.values(): + if hasattr(policy, "explore"): + policy.explore() + + def exploit(self): + for conn in self._conn.values(): + conn.send({"type": "exploit"}) + for policy in self.policy_dict.values(): + if hasattr(policy, "exploit"): + policy.exploit() + + def exploration_step(self): + for conn in self._conn.values(): + conn.send({"type": "exploration_step"}) + for policy in self.policy_dict.values(): + if hasattr(policy, "exploration_step"): + policy.exploration_step() + + def get_rollout_info(self): + for conn in self._conn.values(): + conn.send({"type": "rollout_info"}) + + return { + **{ + id_: policy.get_rollout_info() for id_, policy in self.policy_dict.items() + if isinstance(policy, RLPolicy) + }, + **{id_: conn.recv() for id_, conn in self._conn.items()} + } + + def get_exploration_params(self): + for conn in self._conn.values(): + conn.send({"type": "exploration_params"}) + + return { + **{ + id_: policy.exploration_params for id_, policy in self.policy_dict.items() + if isinstance(policy, RLPolicy) + }, + **{id_: conn.recv() for id_, conn in self._conn.items()} + } + + def record_transition(self, agent: str, state, action, reward, next_state, terminal: bool): + if agent in self.policy_by_agent: + if isinstance(self.policy_by_agent[agent], RLPolicy): + self.policy_by_agent[agent].record(agent, state, action, reward, next_state, terminal) + else: + self._conn[self.agent2policy[agent]].send({ + "type": "record", "agent": agent, "state": state, "action": action, "reward": reward, + "next_state": next_state, "terminal": terminal + }) class EnvSampler: @@ -24,24 +170,21 @@ class EnvSampler: creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. agent2policy (Dict[str, str]): A dictionary that maps agent IDs to policy IDs, i.e., specifies the policy used by each agent. - get_state (Callable): Function to compute the state. The function takes as input an ``Env``, an event and a - dictionary of keyword parameters and returns a state vector encoded as a one-dimensional (flattened) Numpy - arrays for each agent involved as a dictionary. + get_state (Callable): Function to compute the state. The function takes as input an ``Env`` and an event and + returns a state vector encoded as a one-dimensional (flattened) Numpy arrays for each agent involved as a + dictionary. get_env_actions (Callable): Function to convert policy outputs to action objects that can be passed directly to the environment's ``step`` method. The function takes as input an ``Env``, a dictionary of a set of agents' - policy outputs, an event and a dictionary of keyword parameters and returns a list of action objects. + policy output and an event and returns a list of action objects. get_reward (Callable): Function to compute rewards for a list of actions that occurred at a given tick. The - function takes as input an ``Env``, a list of actions (output by ``get_env_actions``), a tick and a - dictionary of keyword parameters and returns a scalar reward for each agent as a dictionary. + function takes as input an ``Env``, a list of actions (output by ``get_env_actions``) and a tick and returns + a scalar reward for each agent as a dictionary. get_test_env (Callable): Function to create an ``Env`` instance for testing policy performance. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the worker processes. Defaults to None. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. - replay_agent_ids (list): List of agent IDs whose transitions will be stored in internal replay buffers. - If it is None, it will be set to all agents in the environment (i.e., env.agent_idx_list). Defaults - to None. post_step (Callable): Custom function to gather information about a transition and the evolvement of the environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition @@ -59,72 +202,63 @@ def __init__( get_test_env: Callable[[], Env] = None, reward_eval_delay: int = 0, post_step: Callable = None, - state_shaping_kwargs: dict = {}, - action_shaping_kwargs: dict = {}, - reward_shaping_kwargs: dict = {} + policies_to_parallelize: List[str] = [] ): self._learn_env = get_env() self._test_env = get_test_env() if get_test_env else self._learn_env self.env = None - self.policy_dict = {id_: func(id_) for id_, func in get_policy_func_dict.items()} - self.policy_by_agent = {agent: self.policy_dict[policy_id] for agent, policy_id in agent2policy.items()} + self.agent_wrapper = AgentWrapper( + get_policy_func_dict, agent2policy, policies_to_parallelize=policies_to_parallelize + ) + self.reward_eval_delay = reward_eval_delay self._post_step = post_step # shaping self._get_state = get_state - self._state_shaping_kwargs = state_shaping_kwargs self._get_env_actions = get_env_actions - self._action_shaping_kwargs = action_shaping_kwargs self._get_reward = get_reward - self._reward_shaping_kwargs = reward_shaping_kwargs + self._state = None + self._event = None self._step_index = 0 self._terminal = True self._transition_cache = defaultdict(deque) # for caching transitions whose rewards have yet to be evaluated - self._prev_state = defaultdict(lambda: None) - self.tracker = {} # User-defined tracking information is placed here. def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): self.env = self._learn_env # set policy states if policy_state_dict: - for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].set_state(policy_state) + self.agent_wrapper.set_policy_states(policy_state_dict) # update exploration states if necessary - for policy in self.policy_dict.values(): - if hasattr(policy, "explore"): - policy.explore() + self.agent_wrapper.explore() if exploration_step: - for policy in self.policy_dict.values(): - if hasattr(policy, "exploration_step"): - policy.exploration_step() + self.agent_wrapper.exploration_step() if self._terminal: # reset and get initial state self.env.reset() self._step_index = 0 self._transition_cache.clear() - self._prev_state = defaultdict(lambda: None) self.tracker.clear() self._terminal = False - _, event, _ = self.env.step(None) - state = self._get_state(self.env, event, self._state_shaping_kwargs) + _, self._event, _ = self.env.step(None) + self._state = self._get_state(self.env, self._event) starting_step_index = self._step_index + 1 steps_to_go = float("inf") if num_steps == -1 else num_steps while not self._terminal and steps_to_go > 0: - action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in state.items()} - env_actions = self._get_env_actions(self.env, action, event, self._action_shaping_kwargs) - for agent in state: - self._transition_cache[agent].append((state[agent], action[agent], env_actions, self.env.tick)) - _, event, self._terminal = self.env.step(env_actions) - state = None if self._terminal else self._get_state(self.env, event, self._state_shaping_kwargs) + action = self.agent_wrapper.choose_action(self._state) + env_actions = self._get_env_actions(self.env, action, self._event) + for agent, state in self._state.items(): + self._transition_cache[agent].append((state, action[agent], env_actions, self.env.tick)) + _, self._event, self._terminal = self.env.step(env_actions) + self._state = None if self._terminal else self._get_state(self.env, self._event) self._step_index += 1 steps_to_go -= 1 @@ -135,54 +269,44 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio for agent, cache in self._transition_cache.items(): while cache and (self._terminal or self.env.tick - cache[0][-1] >= self.reward_eval_delay): state, action, env_actions, tick = cache.popleft() - reward = self._get_reward(self.env, env_actions, tick, self._reward_shaping_kwargs) + reward = self._get_reward(self.env, env_actions, tick) if self._post_step: # put things you want to track in the tracker attribute self._post_step(self.env, self.tracker, state, action, env_actions, reward, tick) - if isinstance(self.policy_by_agent[agent], RLPolicy) and self._prev_state[agent] is not None: - self.policy_by_agent[agent].record( - agent, self._prev_state[agent], action, reward[agent], state, not cache and self._terminal - ) - self._prev_state[agent] = state + self.agent_wrapper.record_transition( + agent, state, action, reward[agent], cache[0][0] if cache else self._state, + not cache and self._terminal + ) return { - "rollout_info": { - id_: policy.get_rollout_info() for id_, policy in self.policy_dict.items() - if isinstance(policy, RLPolicy) - }, + "rollout_info": self.agent_wrapper.get_rollout_info(), "step_range": (starting_step_index, self._step_index), "tracker": self.tracker, "end_of_episode": self._terminal, - "exploration_params": { - name: policy.exploration_params for name, policy in self.policy_dict.items() - if isinstance(policy, RLPolicy) - } + "exploration_params": self.agent_wrapper.get_exploration_params() } def test(self, policy_state_dict: dict = None): self.env = self._test_env # set policy states if policy_state_dict: - for id_, policy_state in policy_state_dict.items(): - self.policy_dict[id_].set_state(policy_state) + self.agent_wrapper.set_policy_states(policy_state_dict) # Set policies to exploitation mode - for policy in self.policy_dict.values(): - if hasattr(policy, "exploit"): - policy.exploit() + self.agent_wrapper.exploit() self.env.reset() terminal = False # get initial state _, event, _ = self.env.step(None) - state = self._get_state(self.env, event, self._state_shaping_kwargs) + state = self._get_state(self.env, event) while not terminal: - action = {agent: self.policy_by_agent[agent].choose_action(st) for agent, st in state.items()} - env_actions = self._get_env_actions(self.env, action, event, self._action_shaping_kwargs) + action = self.agent_wrapper.choose_action(state) + env_actions = self._get_env_actions(self.env, action, event) _, event, terminal = self.env.step(env_actions) if not terminal: - state = self._get_state(self.env, event, self._state_shaping_kwargs) + state = self._get_state(self.env, event) return self.tracker diff --git a/maro/rl/learning/common.py b/maro/rl/learning/helpers.py similarity index 100% rename from maro/rl/learning/common.py rename to maro/rl/learning/helpers.py diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py index fa94d9538..47784e014 100644 --- a/maro/rl/learning/learner.py +++ b/maro/rl/learning/learner.py @@ -7,9 +7,9 @@ from maro.utils import Logger -from .common import get_eval_schedule, get_rollout_finish_msg from .early_stopper import AbsEarlyStopper from .env_sampler import EnvSampler +from .helpers import get_eval_schedule, get_rollout_finish_msg from .policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index d99e96747..cc3723991 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -14,8 +14,8 @@ from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger, set_seeds -from .common import get_rollout_finish_msg from .env_sampler import EnvSampler +from .helpers import get_rollout_finish_msg def concat_batches(batch_list: List[dict]): diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 493c887bf..920ea51d6 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -200,7 +200,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): "actor_loss": actor_loss.detach().cpu().numpy(), "critic_loss": critic_loss.detach().cpu().numpy(), "entropy": entropy.detach().cpu().numpy(), - "loss": loss + "loss": loss.detach().cpu().numpy() } if explicit_grad: loss_info["grad"] = self.ac_net.get_gradients(loss) diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index 1a4577c88..742d884fb 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -213,6 +213,9 @@ def record( next_state: np.ndarray, terminal: bool ): + if next_state is None: + next_state = np.zeros(state.shape, dtype=np.float32) + indexes = self._replay_memory.put( np.expand_dims(state, axis=0), np.expand_dims(action, axis=0), diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index d9daafb95..e5e204b18 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -131,7 +131,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): _, logp = self.policy_net(batch["states"]) loss = -(logp * returns).mean() - loss_info = {"loss": loss} + loss_info = {"loss": loss.detach().cpu().numpy()} if explicit_grad: loss_info["grad"] = self.policy_net.get_gradients(loss) return loss_info From fde789555d89ad385f9228c2e0a97e71db2201b4 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Thu, 2 Sep 2021 15:37:43 +0800 Subject: [PATCH 424/482] fixed bug in ddpg transition recording --- maro/rl/policy/ddpg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index bf10d1dc2..b5afcc749 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -101,6 +101,9 @@ def record( next_state: np.ndarray, terminal: bool ): + if next_state is None: + next_state = np.zeros(state.shape, dtype=np.float32) + self._replay_memory.put( np.expand_dims(state, axis=0), np.expand_dims(action, axis=0), From b9010ef7a2cb1c710a468321f269621560e291d5 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Thu, 2 Sep 2021 08:37:42 +0000 Subject: [PATCH 425/482] put get_state, get_env_actions, get_reward back in EnvSampler --- examples/rl/cim/env_sampler.py | 122 ++++++++++++++-------------- maro/rl/learning/__init__.py | 4 +- maro/rl/learning/env_sampler.py | 71 ++++++++++------ maro/rl/learning/learner.py | 4 +- maro/rl/learning/rollout_manager.py | 6 +- 5 files changed, 111 insertions(+), 96 deletions(-) diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 451970828..ae37a3146 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -6,7 +6,7 @@ import numpy as np -from maro.rl.learning import EnvSampler +from maro.rl.learning import AbsEnvSampler from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType @@ -21,62 +21,63 @@ from policies import policy_func_dict -def get_state(env, event): - vessel_snapshots, port_snapshots = env.snapshot_list["vessels"], env.snapshot_list["ports"] - port_idx, vessel_idx = event.port_idx, event.vessel_idx - ticks = [max(0, env.tick - rt) for rt in range(state_shaping_conf["look_back"] - 1)] - future_port_list = vessel_snapshots[env.tick: vessel_idx: 'future_stop_list'].astype('int') - state = np.concatenate([ - port_snapshots[ticks : [port_idx] + list(future_port_list) : port_attributes], - vessel_snapshots[env.tick : vessel_idx : vessel_attributes] - ]) - return {port_idx: state} - - -def get_env_actions(env, action_by_agent, event): - action_space = action_shaping_conf["action_space"] - finite_vsl_space = action_shaping_conf["finite_vessel_space"] - has_early_discharge = action_shaping_conf["has_early_discharge"] - - port_idx, action = list(action_by_agent.items()).pop() - vsl_idx, action_scope = event.vessel_idx, event.action_scope - vsl_snapshots = env.snapshot_list["vessels"] - vsl_space = vsl_snapshots[env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float("inf") - - model_action = action["action"] if isinstance(action, dict) else action - percent = abs(action_space[model_action]) - zero_action_idx = len(action_space) / 2 # index corresponding to value zero. - if model_action < zero_action_idx: - action_type = ActionType.LOAD - actual_action = min(round(percent * action_scope.load), vsl_space) - elif model_action > zero_action_idx: - action_type = ActionType.DISCHARGE - early_discharge = vsl_snapshots[env.tick:vsl_idx:"early_discharge"][0] if has_early_discharge else 0 - plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge - actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) - else: - actual_action, action_type = 0, None - - return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)] - - -def get_reward(env, actions, tick): - """Delayed reward evaluation.""" - start_tick = tick + 1 - ticks = list(range(start_tick, start_tick + reward_shaping_conf["time_window"])) - - # Get the ports that took actions at the given tick - ports = [action.port_idx for action in actions] - port_snapshots = env.snapshot_list["ports"] - future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) - future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) - - decay_list = [reward_shaping_conf["time_decay"] ** i for i in range(reward_shaping_conf["time_window"])] - rewards = np.float32( - reward_shaping_conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) - - reward_shaping_conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) - ) - return {agent_id: reward for agent_id, reward in zip(ports, rewards)} +class CIMEnvSampler(AbsEnvSampler): + def get_state(self, tick=None): + if tick is None: + tick = self.env.tick + vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] + port_idx, vessel_idx = self.event.port_idx, self.event.vessel_idx + ticks = [max(0, tick - rt) for rt in range(state_shaping_conf["look_back"] - 1)] + future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') + state = np.concatenate([ + port_snapshots[ticks : [port_idx] + list(future_port_list) : port_attributes], + vessel_snapshots[tick : vessel_idx : vessel_attributes] + ]) + return {port_idx: state} + + def get_env_actions(self, action_by_agent): + action_space = action_shaping_conf["action_space"] + finite_vsl_space = action_shaping_conf["finite_vessel_space"] + has_early_discharge = action_shaping_conf["has_early_discharge"] + + port_idx, action = list(action_by_agent.items()).pop() + vsl_idx, action_scope = self.event.vessel_idx, self.event.action_scope + vsl_snapshots = self.env.snapshot_list["vessels"] + vsl_space = vsl_snapshots[self.env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float("inf") + + model_action = action["action"] if isinstance(action, dict) else action + percent = abs(action_space[model_action]) + zero_action_idx = len(action_space) / 2 # index corresponding to value zero. + if model_action < zero_action_idx: + action_type = ActionType.LOAD + actual_action = min(round(percent * action_scope.load), vsl_space) + elif model_action > zero_action_idx: + action_type = ActionType.DISCHARGE + early_discharge = vsl_snapshots[self.env.tick:vsl_idx:"early_discharge"][0] if has_early_discharge else 0 + plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge + actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) + else: + actual_action, action_type = 0, None + + return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)] + + def get_reward(self, actions, tick): + """Delayed reward evaluation.""" + start_tick = tick + 1 + ticks = list(range(start_tick, start_tick + reward_shaping_conf["time_window"])) + + # Get the ports that took actions at the given tick + ports = [action.port_idx for action in actions] + port_snapshots = self.env.snapshot_list["ports"] + future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) + future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) + + decay_list = [reward_shaping_conf["time_decay"] ** i for i in range(reward_shaping_conf["time_window"])] + rewards = np.float32( + reward_shaping_conf["fulfillment_factor"] * np.dot(future_fulfillment.T, decay_list) + - reward_shaping_conf["shortage_factor"] * np.dot(future_shortage.T, decay_list) + ) + return {agent_id: reward for agent_id, reward in zip(ports, rewards)} agent2policy = { @@ -88,14 +89,11 @@ def get_reward(env, actions, tick): def get_env_sampler(): - return EnvSampler( + return CIMEnvSampler( lambda: Env(**env_conf), policy_func_dict, agent2policy, - get_state, - get_env_actions, - get_reward, reward_eval_delay=reward_shaping_conf["time_window"], post_step=post_step, - policies_to_parallelize=["ac"] + policies_to_parallelize=["ac", "dqn"] ) diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index 8cac0dff3..8bf013138 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -2,14 +2,14 @@ # Licensed under the MIT license. from .early_stopper import AbsEarlyStopper -from .env_sampler import EnvSampler +from .env_sampler import AbsEnvSampler from .learner import Learner, simple_learner from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager, policy_host from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, SimpleRolloutManager __all__ = [ "AbsEarlyStopper", - "EnvSampler", + "AbsEnvSampler", "Learner", "simple_learner", "AbsPolicyManager", "DistributedPolicyManager", "SimplePolicyManager", "policy_host", "AbsRolloutManager", "DistributedRolloutManager", "SimpleRolloutManager" diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 95269788c..10c719390 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from abc import ABC, abstractmethod from collections import defaultdict, deque from multiprocessing import Pipe, Process from os import getcwd @@ -160,7 +161,7 @@ def record_transition(self, agent: str, state, action, reward, next_state, termi }) -class EnvSampler: +class AbsEnvSampler(ABC): """Simulation data collector and policy evaluator. Args: @@ -170,15 +171,6 @@ class EnvSampler: creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. agent2policy (Dict[str, str]): A dictionary that maps agent IDs to policy IDs, i.e., specifies the policy used by each agent. - get_state (Callable): Function to compute the state. The function takes as input an ``Env`` and an event and - returns a state vector encoded as a one-dimensional (flattened) Numpy arrays for each agent involved as a - dictionary. - get_env_actions (Callable): Function to convert policy outputs to action objects that can be passed directly to - the environment's ``step`` method. The function takes as input an ``Env``, a dictionary of a set of agents' - policy output and an event and returns a list of action objects. - get_reward (Callable): Function to compute rewards for a list of actions that occurred at a given tick. The - function takes as input an ``Env``, a list of actions (output by ``get_env_actions``) and a tick and returns - a scalar reward for each agent as a dictionary. get_test_env (Callable): Function to create an ``Env`` instance for testing policy performance. The function should take no parameters and return an environment wrapper instance. If this is None, the training environment wrapper will be used for evaluation in the worker processes. Defaults to None. @@ -196,9 +188,6 @@ def __init__( get_env: Callable[[], Env], get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str], - get_state: Dict[str, Callable], - get_env_actions: Callable, - get_reward: Callable, get_test_env: Callable[[], Env] = None, reward_eval_delay: int = 0, post_step: Callable = None, @@ -215,11 +204,6 @@ def __init__( self.reward_eval_delay = reward_eval_delay self._post_step = post_step - # shaping - self._get_state = get_state - self._get_env_actions = get_env_actions - self._get_reward = get_reward - self._state = None self._event = None self._step_index = 0 @@ -228,6 +212,39 @@ def __init__( self._transition_cache = defaultdict(deque) # for caching transitions whose rewards have yet to be evaluated self.tracker = {} # User-defined tracking information is placed here. + @property + def event(self): + return self._event + + @abstractmethod + def get_state(self, tick: int = None) -> dict: + """Compute the state for a given tick. + + Args: + tick (int): The tick for which to compute the environmental state. If computing the current state, + use tick=self.env.tick. + Returns: + A dictionary with (agent ID, state) as key-value pairs. + """ + raise NotImplementedError + + @abstractmethod + def get_env_actions(self, action) -> dict: + """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" + raise NotImplementedError + + @abstractmethod + def get_reward(self, actions: list, tick: int): + """Evaluate the reward for an action. + Args: + tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in + ``actions`` must be an Action object defined for the environment in question. + + Returns: + A dictionary with (agent ID, reward) as key-value pairs. + """ + raise NotImplementedError + def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): self.env = self._learn_env # set policy states @@ -248,17 +265,17 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio self.tracker.clear() self._terminal = False _, self._event, _ = self.env.step(None) - self._state = self._get_state(self.env, self._event) + self._state = self.get_state() starting_step_index = self._step_index + 1 steps_to_go = float("inf") if num_steps == -1 else num_steps while not self._terminal and steps_to_go > 0: action = self.agent_wrapper.choose_action(self._state) - env_actions = self._get_env_actions(self.env, action, self._event) + env_actions = self.get_env_actions(action) for agent, state in self._state.items(): self._transition_cache[agent].append((state, action[agent], env_actions, self.env.tick)) _, self._event, self._terminal = self.env.step(env_actions) - self._state = None if self._terminal else self._get_state(self.env, self._event) + self._state = None if self._terminal else self.get_state() self._step_index += 1 steps_to_go -= 1 @@ -269,7 +286,7 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio for agent, cache in self._transition_cache.items(): while cache and (self._terminal or self.env.tick - cache[0][-1] >= self.reward_eval_delay): state, action, env_actions, tick = cache.popleft() - reward = self._get_reward(self.env, env_actions, tick) + reward = self.get_reward(env_actions, tick) if self._post_step: # put things you want to track in the tracker attribute self._post_step(self.env, self.tracker, state, action, env_actions, reward, tick) @@ -299,14 +316,14 @@ def test(self, policy_state_dict: dict = None): self.env.reset() terminal = False # get initial state - _, event, _ = self.env.step(None) - state = self._get_state(self.env, event) + _, self._event, _ = self.env.step(None) + state = self.get_state() while not terminal: action = self.agent_wrapper.choose_action(state) - env_actions = self._get_env_actions(self.env, action, event) - _, event, terminal = self.env.step(env_actions) + env_actions = self.get_env_actions(action) + _, self._event, terminal = self.env.step(env_actions) if not terminal: - state = self._get_state(self.env, event) + state = self.get_state() return self.tracker diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py index 47784e014..3065803ab 100644 --- a/maro/rl/learning/learner.py +++ b/maro/rl/learning/learner.py @@ -8,14 +8,14 @@ from maro.utils import Logger from .early_stopper import AbsEarlyStopper -from .env_sampler import EnvSampler +from .env_sampler import AbsEnvSampler from .helpers import get_eval_schedule, get_rollout_finish_msg from .policy_manager import AbsPolicyManager from .rollout_manager import AbsRolloutManager def simple_learner( - get_env_sampler: Callable[[], EnvSampler], + get_env_sampler: Callable[[], AbsEnvSampler], num_episodes: int, num_steps: int = -1, eval_schedule: Union[int, List[int]] = None, diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index cc3723991..26f740655 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -14,7 +14,7 @@ from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger, set_seeds -from .env_sampler import EnvSampler +from .env_sampler import AbsEnvSampler from .helpers import get_rollout_finish_msg @@ -78,7 +78,7 @@ class SimpleRolloutManager(AbsRolloutManager): Args: get_env_sampler (Callable): Function to create an environment sampler for collecting training data. The function - should take no parameters and return an ``EnvSampler`` instance. + should take no parameters and return an ``AbsEnvSampler`` instance. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. post_collect (Callable): Custom function to process whatever information is collected by each @@ -95,7 +95,7 @@ class SimpleRolloutManager(AbsRolloutManager): """ def __init__( self, - get_env_sampler: Callable[[], EnvSampler], + get_env_sampler: Callable[[], AbsEnvSampler], num_steps: int = -1, parallelism: int = 1, eval_parallelism: int = 1, From aa694096a4407e1acae8131446af432034b05a1e Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 5 Sep 2021 16:33:30 +0000 Subject: [PATCH 426/482] simplified exploration and core model interfaces --- examples/rl/cim/config.py | 81 ++++----- examples/rl/cim/policies.py | 64 +++---- .../rl/scripts/docker/docker_compose_yml.py | 17 +- examples/rl/vm_scheduling/dqn.py | 4 +- examples/rl/workflows/config.yml | 4 - examples/rl/workflows/general.py | 11 -- maro/rl/exploration/__init__.py | 14 +- maro/rl/exploration/abs_exploration.py | 40 ----- .../exploration/discrete_space_exploration.py | 57 ------- maro/rl/exploration/noise_exploration.py | 85 ---------- ...{exploration_scheduler.py => scheduler.py} | 73 +++++--- maro/rl/exploration/strategies.py | 55 ++++++ maro/rl/learning/env_sampler.py | 87 ++++++---- maro/rl/modeling/__init__.py | 4 +- maro/rl/modeling/core_model.py | 158 +---------------- maro/rl/modeling/specials.py | 159 +++++------------- maro/rl/policy/ac.py | 22 +-- maro/rl/policy/ddpg.py | 69 ++++---- maro/rl/policy/dqn.py | 68 ++++---- maro/rl/policy/pg.py | 18 +- maro/rl/policy/policy.py | 13 +- maro/rl/utils/gradient_averaging.py | 12 ++ 22 files changed, 399 insertions(+), 716 deletions(-) delete mode 100644 maro/rl/exploration/abs_exploration.py delete mode 100644 maro/rl/exploration/discrete_space_exploration.py delete mode 100644 maro/rl/exploration/noise_exploration.py rename maro/rl/exploration/{exploration_scheduler.py => scheduler.py} (65%) create mode 100644 maro/rl/exploration/strategies.py create mode 100644 maro/rl/utils/gradient_averaging.py diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py index 76316b3ba..db47df593 100644 --- a/examples/rl/cim/config.py +++ b/examples/rl/cim/config.py @@ -1,11 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from torch.optim import Adam, RMSprop env_conf = { "scenario": "cim", "topology": "toy.4p_ssdd_l0.0", - "durations": 280 + "durations": 560 } port_attributes = ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"] @@ -37,23 +38,19 @@ # DQN settings q_net_conf = { - "network": { - "input_dim": state_dim, - "hidden_dims": [256, 128, 64, 32], - "output_dim": len(action_shaping_conf["action_space"]), - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.05} - } + "input_dim": state_dim, + "hidden_dims": [256, 128, 64, 32], + "output_dim": len(action_shaping_conf["action_space"]), + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 } +q_net_optim_conf = (RMSprop, {"lr": 0.05}) + dqn_conf = { "reward_discount": .0, "update_target_every": 5, @@ -81,39 +78,29 @@ # AC settings -ac_net_conf = { - "network": { - "actor": { - "input_dim": state_dim, - "hidden_dims": [256, 128, 64], - "output_dim": len(action_shaping_conf["action_space"]), - "activation": "tanh", - "softmax": True, - "batch_norm": False, - "head": True - }, - "critic": { - "input_dim": state_dim, - "hidden_dims": [256, 128, 64], - "output_dim": 1, - "activation": "leaky_relu", - "softmax": False, - "batch_norm": True, - "head": True - } - }, - "optimization": { - "actor": { - "optim_cls": "adam", - "optim_params": {"lr": 0.001} - }, - "critic": { - "optim_cls": "rmsprop", - "optim_params": {"lr": 0.001} - } - } +actor_net_conf = { + "input_dim": state_dim, + "hidden_dims": [256, 128, 64], + "output_dim": len(action_shaping_conf["action_space"]), + "activation": "tanh", + "softmax": True, + "batch_norm": False, + "head": True +} + +critic_net_conf = { + "input_dim": state_dim, + "hidden_dims": [256, 128, 64], + "output_dim": 1, + "activation": "leaky_relu", + "softmax": False, + "batch_norm": True, + "head": True } +actor_optim_conf = (Adam, {"lr": 0.001}) +critic_optim_conf = (RMSprop, {"lr": 0.001}) + ac_conf = { "reward_discount": .0, "grad_iters": 10, diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index 986059f25..c160f3aaa 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -4,67 +4,71 @@ import os import sys -import numpy as np -import torch -import torch.nn as nn - -from maro.rl.exploration import EpsilonGreedyExploration, MultiPhaseLinearExplorationScheduler -from maro.rl.modeling import DiscreteACNet, DiscreteQNet, FullyConnected, OptimOption +from maro.rl.exploration import MultiLinearExplorationScheduler +from maro.rl.modeling import DiscreteACNet, DiscreteQNet, FullyConnected from maro.rl.policy import DQN, ActorCritic cim_path = os.path.dirname(os.path.realpath(__file__)) if cim_path not in sys.path: sys.path.insert(0, cim_path) -from config import ac_conf, ac_net_conf, dqn_conf, q_net_conf, exploration_conf, state_dim +from config import ( + ac_conf, actor_net_conf, actor_optim_conf, critic_net_conf, critic_optim_conf, dqn_conf, q_net_conf, + q_net_optim_conf, exploration_conf, state_dim +) class QNet(DiscreteQNet): - def __init__(self, component: nn.Module, optim_option: OptimOption=None, device=None): - super().__init__(component, optim_option=optim_option, device=device) + def __init__(self, device=None): + super().__init__(device=device) + self.fc = FullyConnected(**q_net_conf) + self.optim = q_net_optim_conf[0](self.fc.parameters(), **q_net_optim_conf[1]) @property def input_dim(self): return state_dim - + def forward(self, states): return self.component(states) + def step(self, loss): + self.optim.zero_grad() + loss.backward() + self.optim.step() + def get_dqn(name: str): - qnet = QNet(FullyConnected(**q_net_conf["network"]), optim_option=OptimOption(**q_net_conf["optimization"])) exploration = EpsilonGreedyExploration() exploration.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, + scheduler_cls=MultiLinearExplorationScheduler, param_name="epsilon", **exploration_conf ) - return DQN(name, qnet, exploration=exploration, **dqn_conf) + return DQN(name, QNet(), exploration=exploration, **dqn_conf) def get_ac(name: str): - class MyACNET(DiscreteACNet): + class MyACNet(DiscreteACNet): + def __init__(self): + self.actor = FullyConnected(**actor_net_conf) + self.critic = FullyConnected(**critic_net_conf) + self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1]) + self.critic_optim = critic_optim_conf[0](self.critic.parameters, **critic_optim_conf[1]) + @property def input_dim(self): return state_dim def forward(self, states, actor: bool = True, critic: bool = True): - return ( - self.component["actor"](states) if actor else None, - self.component["critic"](states) if critic else None - ) - - ac_net = MyACNET( - component={ - "actor": FullyConnected(**ac_net_conf["network"]["actor"]), - "critic": FullyConnected(**ac_net_conf["network"]["critic"]) - }, - optim_option={ - "actor": OptimOption(**ac_net_conf["optimization"]["actor"]), - "critic": OptimOption(**ac_net_conf["optimization"]["critic"]) - } - ) + return (self.actor(states) if actor else None), (self.critic(states) if critic else None) + + def step(self, loss): + self.actor_optim.zero_grad() + self.critic_optim.zero_grad() + loss.backward() + self.actor_optim.step() + self.critic_optim.step() - return ActorCritic(name, ac_net, **ac_conf) + return ActorCritic(name, MyACNet(), **ac_conf) policy_func_dict = { diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 5b277c146..d2a50dc06 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -49,8 +49,7 @@ f"JOB={config['job']}", f"SCENARIO={config['scenario']}", f"MODE={config['mode']}", - f"POLICYMANAGERTYPE={config['policy_manager']['type']}", - f"EXPDIST={'1' if config['rollout_experience_distribution'] else '0'}" + f"POLICYMANAGERTYPE={config['policy_manager']['type']}" ] if config["mode"] == "async": @@ -85,17 +84,17 @@ "command": "python3 /maro/rl_examples/workflows/learner.py", "environment": [ f"ROLLOUTTYPE={config['sync']['rollout_type']}", - f"EVALPARALLELISM={config['sync']['simple']['eval_parallelism']}", - f"ROLLOUTGROUP={config['sync']['distributed']['group']}", f"NUMEPISODES={config['num_episodes']}", + f"NUMSTEPS={config['num_steps']}", f"EVALSCH={config['eval_schedule']}", + f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}", + f"EVALPARALLELISM={config['sync']['simple']['eval_parallelism']}", + f"ROLLOUTGROUP={config['sync']['distributed']['group']}", f"NUMEVALWORKERS={config['sync']['distributed']['num_eval_workers']}", - f"NUMSTEPS={config['num_steps']}", f"MAXLAG={config['max_lag']}", f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}", - f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}" ] + common_env } } @@ -109,8 +108,7 @@ worker_spec["container_name"] = f"{namespace}.{str_id}" worker_spec["environment"] = [ f"WORKERID={worker_id}", - f"ROLLOUTGROUP={config['sync']['distributed']['group']}", - f"EVALSCH={config['eval_schedule']}" + f"ROLLOUTGROUP={config['sync']['distributed']['group']}" ] + common_env docker_compose_manifest["services"][str_id] = worker_spec elif mode == "async": @@ -137,8 +135,7 @@ f"ACTORID={actor_id}", f"GROUP={config['async']['group']}", f"NUMEPISODES={config['num_episodes']}", - f"NUMSTEPS={config['num_steps']}", - f"EVALSCH={config['eval_schedule']}" + f"NUMSTEPS={config['num_steps']}" ] + common_env docker_compose_manifest["services"][str_id] = actor_spec else: diff --git a/examples/rl/vm_scheduling/dqn.py b/examples/rl/vm_scheduling/dqn.py index 2a9a0d055..4f7499779 100644 --- a/examples/rl/vm_scheduling/dqn.py +++ b/examples/rl/vm_scheduling/dqn.py @@ -9,7 +9,7 @@ import torch from maro.rl.experience import ReplayMemory, UniformSampler -from maro.rl.exploration import DiscreteSpaceExploration, MultiPhaseLinearExplorationScheduler +from maro.rl.exploration import DiscreteSpaceExploration, MultiLinearExplorationScheduler from maro.rl.model import DiscreteQNet, FullyConnected, OptimOption from maro.rl.policy.algorithms import DQN, DQNConfig @@ -109,7 +109,7 @@ def get_dqn_policy(mode="update"): exp_store = ReplayMemory(**config["replay_memory"]["rollout"]) exploration = MaskedEpsilonGreedy() exploration.register_schedule( - scheduler_cls=MultiPhaseLinearExplorationScheduler, + scheduler_cls=MultiLinearExplorationScheduler, param_name="epsilon", **config["exploration"] ) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index cd52cffbf..7db3cfa5b 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -8,10 +8,6 @@ num_episodes: 5 eval_schedule: 5 num_steps: -1 max_lag: 0 -# If true, the roll-out experiences will be distributed amongst roll-out workers / actors -# in round-robin fashion based on agent index. Otherwise, every roll-out worker / actor will -# store the roll-out experiences for all agents whose experiences need to be stored. -rollout_experience_distribution: false sync: rollout_type: distributed # simple, distributed simple: diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 460d84ee3..5783c4403 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -18,16 +18,5 @@ get_env_sampler = getattr(module, "get_env_sampler") policy_func_dict = getattr(module, "policy_func_dict") -# rl_agents = [agent_id for agent_id, policy_id in agent2policy.items() if policy_id in rl_policy_func_index] post_collect = getattr(module, "post_collect", None) post_evaluate = getattr(module, "post_evaluate", None) - -# roll-out experience distribution amongst workers -num_rollouts = int(getenv("NUMROLLOUTS")) -# exp_dist = int(getenv("EXPDIST", default=0)) -# if exp_dist: -# replay_agents = [[] for _ in range(num_rollouts)] -# for i, agent in enumerate(rl_agents): -# replay_agents[i % num_rollouts].append(agent) -# else: -# replay_agents = [rl_agents] * num_rollouts diff --git a/maro/rl/exploration/__init__.py b/maro/rl/exploration/__init__.py index 0edfd93bc..1d4991bea 100644 --- a/maro/rl/exploration/__init__.py +++ b/maro/rl/exploration/__init__.py @@ -1,16 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .abs_exploration import AbsExploration, NullExploration -from .discrete_space_exploration import DiscreteSpaceExploration, EpsilonGreedyExploration -from .exploration_scheduler import ( - AbsExplorationScheduler, LinearExplorationScheduler, MultiPhaseLinearExplorationScheduler +from .scheduler import ( + AbsExplorationScheduler, LinearExplorationScheduler, MultiLinearExplorationScheduler ) -from .noise_exploration import GaussianNoiseExploration, NoiseExploration, UniformNoiseExploration +from .strategies import eps_greedy, gaussian_noise, uniform_noise __all__ = [ - "AbsExploration", "NullExploration", - "DiscreteSpaceExploration", "EpsilonGreedyExploration", - "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiPhaseLinearExplorationScheduler", - "GaussianNoiseExploration", "NoiseExploration", "UniformNoiseExploration" + "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiLinearExplorationScheduler", + "eps_greedy", "gaussian_noise", "uniform_noise" ] diff --git a/maro/rl/exploration/abs_exploration.py b/maro/rl/exploration/abs_exploration.py deleted file mode 100644 index b66a596cd..000000000 --- a/maro/rl/exploration/abs_exploration.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - - -class AbsExploration(ABC): - """Abstract exploration class for generating exploration rates.""" - def __init__(self): - self.scheduler = {} - - def register_schedule( - self, - scheduler_cls, - param_name: str, - last_ep: int, - initial_value=None, - **kwargs - ): - self.scheduler[param_name] = scheduler_cls(self, param_name, last_ep, initial_value=initial_value, **kwargs) - - @abstractmethod - def __call__(self, action): - return NotImplementedError - - @property - def parameters(self): - return {param_name: getattr(self, param_name) for param_name in self.scheduler} - - def step(self): - for scheduler in self.scheduler.values(): - scheduler.step() - - -class NullExploration(AbsExploration): - def __init__(self): - pass - - def __call__(self, action): - return action diff --git a/maro/rl/exploration/discrete_space_exploration.py b/maro/rl/exploration/discrete_space_exploration.py deleted file mode 100644 index eb52e2b63..000000000 --- a/maro/rl/exploration/discrete_space_exploration.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import abstractmethod -from typing import Union - -import numpy as np - -from .abs_exploration import AbsExploration - - -class DiscreteSpaceExploration(AbsExploration): - """Exploration for discrete action spaces.""" - def __init__(self): - super().__init__() - self.action_space = None - - def set_action_space(self, action_space): - self.action_space = action_space - - @abstractmethod - def __call__(self, action, state=None): - """Generate an exploratory action. - - Args: - action: Optimal action according to the policy to which this exploration instance belongs. - state: State information which might be needed as context to generate an exploratory action. - Defaults to None. - """ - raise NotImplementedError - - -class EpsilonGreedyExploration(DiscreteSpaceExploration): - """Epsilon greedy exploration for discrete action spaces. - - Args: - num_actions (int): Number of all possible actions. - """ - def __init__(self, epsilon: float = .0): - super().__init__() - self.epsilon = epsilon - - def __call__(self, action: Union[int, np.ndarray], state=None): - """Generate an exploratory action. - - Args: - action: Optimal action according to the policy to which this exploration instance belongs. - state: State information which might be needed as context to generate an exploratory action. - In this simple epsilon-greedy scheme, it is not used. Defaults to None. - """ - if isinstance(action, np.ndarray): - return np.array([self._get_exploration_action(act) for act in action]) - else: - return self._get_exploration_action(action) - - def _get_exploration_action(self, action): - return action if np.random.random() > self.epsilon else np.random.choice(self.action_space) diff --git a/maro/rl/exploration/noise_exploration.py b/maro/rl/exploration/noise_exploration.py deleted file mode 100644 index 45f14ce6a..000000000 --- a/maro/rl/exploration/noise_exploration.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import abstractmethod -from typing import Union - -import numpy as np - -from .abs_exploration import AbsExploration - - -class NoiseExploration(AbsExploration): - """Exploration that adds a random noise to a model-generated action.""" - def __init__( - self, - min_action: Union[float, list, np.ndarray] = None, - max_action: Union[float, list, np.ndarray] = None - ): - if isinstance(min_action, (list, np.ndarray)) and isinstance(max_action, (list, np.ndarray)): - assert len(min_action) == len(max_action), "min_action and max_action should have the same dimension." - super().__init__() - self.min_action = min_action - self.max_action = max_action - - @abstractmethod - def __call__(self, action) -> np.ndarray: - raise NotImplementedError - - -class UniformNoiseExploration(NoiseExploration): - """Exploration that adds a random noise to a model-generated action sampled from a uniform distribution.""" - def __init__( - self, - min_action: Union[float, list, np.ndarray] = None, - max_action: Union[float, list, np.ndarray] = None, - noise_lower_bound: Union[float, list, np.ndarray] = .0, - noise_upper_bound: Union[float, list, np.ndarray] = .0 - ): - if isinstance(noise_upper_bound, (list, np.ndarray)) and isinstance(noise_upper_bound, (list, np.ndarray)): - assert len(noise_lower_bound) == len(noise_upper_bound), \ - "noise_lower_bound and noise_upper_bound should have the same dimension." - super().__init__(min_action, max_action) - self.noise_lower_bound = noise_lower_bound - self.noise_upper_bound = noise_upper_bound - - def __call__(self, action: np.ndarray) -> np.ndarray: - return np.array([self._get_exploration_action(act) for act in action]) - - def _get_exploration_action(self, action): - action += np.random.uniform(self.noise_lower_bound, self.noise_upper_bound) - if self.min_action is not None or self.max_action is not None: - return np.clip(action, self.min_action, self.max_action) - else: - return action - - -class GaussianNoiseExploration(NoiseExploration): - """Exploration that adds a random noise to a model-generated action sampled from a Gaussian distribution.""" - def __init__( - self, - min_action: Union[float, list, np.ndarray] = None, - max_action: Union[float, list, np.ndarray] = None, - noise_mean: Union[float, list, np.ndarray] = .0, - noise_stddev: Union[float, list, np.ndarray] = .0, - is_relative: bool = False - ): - if isinstance(noise_mean, (list, np.ndarray)) and isinstance(noise_stddev, (list, np.ndarray)): - assert len(noise_mean) == len(noise_stddev), "noise_mean and noise_stddev should have the same dimension." - if is_relative and noise_mean != .0: - raise ValueError("Standard deviation cannot be relative if noise mean is non-zero.") - super().__init__(min_action, max_action) - self.noise_mean = noise_mean - self.noise_stddev = noise_stddev - self.is_relative = is_relative - - def __call__(self, action: np.ndarray) -> np.ndarray: - return np.array([self._get_exploration_action(act) for act in action]) - - def _get_exploration_action(self, action): - noise = np.random.normal(loc=self.noise_mean, scale=self.noise_stddev) - action += (noise * action) if self.is_relative else noise - if self.min_action is not None or self.max_action is not None: - return np.clip(action, self.min_action, self.max_action) - else: - return action diff --git a/maro/rl/exploration/exploration_scheduler.py b/maro/rl/exploration/scheduler.py similarity index 65% rename from maro/rl/exploration/exploration_scheduler.py rename to maro/rl/exploration/scheduler.py index 1d0c285d9..42361535d 100644 --- a/maro/rl/exploration/exploration_scheduler.py +++ b/maro/rl/exploration/scheduler.py @@ -4,16 +4,16 @@ from abc import ABC, abstractmethod from typing import List, Tuple -from maro.rl.exploration.abs_exploration import AbsExploration +from maro.rl.policy.policy import RLPolicy class AbsExplorationScheduler(ABC): """Abstract exploration scheduler. - Each exploration scheduler is registered to a single parameter of an exploration instance. + Each exploration scheduler is registered to a single parameter of an ``RLPolicy`` instance. Args: - exploration (AbsExploration): An exploration instance to which the scheduler is applied. + policy (RLPolicy): An ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. initial_value: Initial value for the exploration parameter. If None, the value the exploration @@ -22,20 +22,20 @@ class AbsExplorationScheduler(ABC): def __init__( self, - exploration: AbsExploration, + policy: RLPolicy, param_name: str, last_ep: int, initial_value=None ): super().__init__() - self.exploration = exploration + self._policy = policy self.param_name = param_name self.last_ep = last_ep if initial_value is not None: - setattr(self.exploration, self.param_name, initial_value) + self._policy.exploration_params[self.param_name] = initial_value def get_value(self): - return getattr(self.exploration, self.param_name) + return self._policy.exploration_params[self.param_name] @abstractmethod def step(self): @@ -46,7 +46,7 @@ class LinearExplorationScheduler(AbsExplorationScheduler): """Linear exploration parameter schedule. Args: - exploration (AbsExploration): An exploration instance to which the scheduler is applied. + policy (RLPolicy): An ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. @@ -56,31 +56,31 @@ class LinearExplorationScheduler(AbsExplorationScheduler): def __init__( self, - exploration: AbsExploration, + policy: RLPolicy, param_name: str, last_ep: int, final_value: float, initial_value: float = None, ): - super().__init__(exploration, param_name, last_ep, initial_value=initial_value) + super().__init__(policy, param_name, last_ep, initial_value=initial_value) self.final_value = final_value if self.last_ep > 1: - self.delta = (self.final_value - getattr(self.exploration, self.param_name)) / (self.last_ep - 1) + self.delta = (self.final_value - getattr(self._policy, self.param_name)) / (self.last_ep - 1) else: self.delta = 0 def step(self): - if self.get_value() == self.final_value: + if self._policy.exploration_params[self.param_name] == self.final_value: return - setattr(self.exploration, self.param_name, self.get_value() + self.delta) + self._policy.exploration_params[self.param_name] += self.delta -class MultiPhaseLinearExplorationScheduler(AbsExplorationScheduler): +class MultiLinearExplorationScheduler(AbsExplorationScheduler): """Exploration parameter schedule that consists of multiple linear phases. Args: - exploration (AbsExploration): An exploration instance to which the scheduler is applied. + policy (RLPolicy): An ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. splits (List[Tuple[int, float]]): List of points that separate adjacent linear phases. Each @@ -96,7 +96,7 @@ class MultiPhaseLinearExplorationScheduler(AbsExplorationScheduler): """ def __init__( self, - exploration: AbsExploration, + policy: RLPolicy, param_name: str, last_ep: int, splits: List[Tuple[int, float]], @@ -110,18 +110,18 @@ def __init__( if ep == ep2: raise ValueError("The zeroth element of split points must be unique") - super().__init__(exploration, param_name, last_ep, initial_value=initial_value) + super().__init__(policy, param_name, last_ep, initial_value=initial_value) self.final_value = final_value self._splits = splits self._ep = 1 self._split_index = 1 - self._delta = (self._splits[1][1] - self.get_value()) / (self._splits[1][0] - 1) + self._delta = (self._splits[1][1] - self._policy.exploration_params[self.param_name]) / (self._splits[1][0] - 1) def step(self): if self._split_index == len(self._splits): return - setattr(self.exploration, self.param_name, self.get_value() + self._delta) + self._policy.exploration_params[self.param_name] += self._delta self._ep += 1 if self._ep == self._splits[self._split_index][0]: self._split_index += 1 @@ -133,11 +133,34 @@ def step(self): if __name__ == "__main__": - from maro.rl.exploration.discrete_space_exploration import EpsilonGreedyExploration - exploration = EpsilonGreedyExploration(5, epsilon=0.6) - scheduler = MultiPhaseLinearExplorationScheduler( - exploration, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0 - ) + class TestPolicy(RLPolicy): + def __init__(self, name, epsilon: float): + super().__init__(name) + self.exploration_params["epsilon"] = epsilon + + def choose_action(self, state): + pass + + def get_rollout_info(self): + pass + + def get_batch_loss(self, batch: dict, explicit_grad: bool = False): + pass + + def update(self, loss_info_list: List[dict]): + pass + + def learn(self, trajectories: List[dict]): + pass + + def get_state(self): + pass + + def set_state(self, policy_state): + pass + + policy = TestPolicy("test", epsilon=0.6) + scheduler = MultiLinearExplorationScheduler(policy, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0) for ep in range(1, scheduler.last_ep + 1): - print(f"ep = {ep}, value = {exploration.epsilon}") + print(f"ep = {ep}, value = {policy.exploration_params['epsilon']}") scheduler.step() diff --git a/maro/rl/exploration/strategies.py b/maro/rl/exploration/strategies.py new file mode 100644 index 000000000..4e4ea4687 --- /dev/null +++ b/maro/rl/exploration/strategies.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Union + +import numpy as np + + +def eps_greedy(action: Union[int, np.ndarray], num_actions, *, epsilon: float, state=None): + """epsilon-greedy exploration. + + Args: + action (Union[int, np.ndarray]): Action(s) chosen greedily by the policy. + num_actions (int): Number of possible actions. + epsilon (float): The probability that a random action will be used + state: State information which might be needed as context to generate an exploratory action. + In this simple epsilon-greedy scheme, it is not used. Defaults to None. + """ + def get_exploration_action(action): + return action if np.random.random() > epsilon else np.random.randint(num_actions) + + if isinstance(action, np.ndarray): + return np.array([get_exploration_action(act) for act in action]) + else: + return get_exploration_action(action) + + +def uniform_noise( + action: Union[float, np.ndarray], + *, + low: Union[float, list, np.ndarray], + high: Union[float, list, np.ndarray], + min_action: Union[float, list, np.ndarray] = None, + max_action: Union[float, list, np.ndarray] = None, +) -> Union[float, np.ndarray]: + if min_action is None and max_action is None: + return action + np.random.uniform(low, high) + else: + return np.clip(action + np.random.uniform(low, high), min_action, max_action) + + +def gaussian_noise( + action: Union[float, np.ndarray], + *, + mean: Union[float, list, np.ndarray] = .0, + stddev: Union[float, list, np.ndarray] = 1.0, + relative: bool = False, + min_action: Union[float, list, np.ndarray] = None, + max_action: Union[float, list, np.ndarray] = None, +) -> Union[float, np.ndarray]: + noise = np.random.normal(loc=mean, scale=stddev) + if min_action is None and max_action is None: + return action + ((noise * action) if relative else noise) + else: + return np.clip(action + ((noise * action) if relative else noise), min_action, max_action) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 10c719390..efa959ed7 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -23,11 +23,13 @@ def __init__( self, get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str], + exploration_scheduler_option: Dict[str, tuple], policies_to_parallelize: List[str] = [] ): self._policies_to_parallelize = set(policies_to_parallelize) self.policy_dict = { - id_: func(id_) for id_, func in get_policy_func_dict.items() if id_ not in self._policies_to_parallelize + policy_id: func(policy_id) for policy_id, func in get_policy_func_dict.items() + if policy_id not in self._policies_to_parallelize } self.agent2policy = agent2policy self.policy_by_agent = { @@ -35,11 +37,20 @@ def __init__( if policy_id in self.policy_dict } - self._policy_hosts = [] + self.exploration_scheduler = { + policy_id: sch_cls(self.policy_dict[policy_id], **params) + for policy_id, (sch_cls, params) in exploration_scheduler_option.items() if policy_id in self.policy_dict + } + + self._inference_services = [] self._conn = {} - def _policy_host(name, get_policy, conn): + def _inference_service(name, get_policy, conn, exploration_scheduler_option): policy = get_policy(name) + if exploration_scheduler_option: + sch_cls, params = exploration_scheduler_option + exploration_scheduler = sch_cls(policy, **params) + while True: msg = conn.recv() if msg["type"] == "choose_action": @@ -52,7 +63,7 @@ def _policy_host(name, get_policy, conn): elif msg["type"] == "exploit": policy.exploit() elif msg["type"] == "exploration_step": - policy.exploration_step() + exploration_scheduler.step() elif msg["type"] == "rollout_info": conn.send(policy.get_rollout_info()) elif msg["type"] == "exploration_params": @@ -66,37 +77,42 @@ def _policy_host(name, get_policy, conn): elif msg["type"] == "learn": policy.learn(msg["batch"]) - for id_ in policies_to_parallelize: + for policy_id in policies_to_parallelize: conn1, conn2 = Pipe() - self._conn[id_] = conn1 - host = Process(target=_policy_host, args=(id_, get_policy_func_dict[id_], conn2)) - self._policy_hosts.append(host) + self._conn[policy_id] = conn1 + host = Process( + target=_inference_service, + args=( + policy_id, get_policy_func_dict[policy_id], conn2, exploration_scheduler_option.get(policy_id, None) + ) + ) + self._inference_services.append(host) host.start() def choose_action(self, state_by_agent: Dict[str, np.ndarray]): - states_by_policy, agents_by_policy, action = defaultdict(list), defaultdict(list), {} + states_by_policy, agents_by_policy = defaultdict(list), defaultdict(list) for agent, state in state_by_agent.items(): - if self.agent2policy[agent] in self._conn: - states_by_policy[self.agent2policy[agent]].append(state) - agents_by_policy[self.agent2policy[agent]].append(agent) + states_by_policy[self.agent2policy[agent]].append(state) + agents_by_policy[self.agent2policy[agent]].append(agent) - for policy_id, states in states_by_policy.items(): - self._conn[policy_id].send({"type": "choose_action", "states": np.concatenate(states)}) + # send state batch to inference processes for parallelized inference + for policy_id, conn in self._conn.items(): + if states_by_policy[policy_id]: + conn.send({"type": "choose_action", "states": np.vstack(states_by_policy[policy_id])}) + + action_by_agent = {} + # compute the actions for local policies first while the inferences processes do their work + for policy_id, policy in self.policy_dict.items(): + if states_by_policy[policy_id]: + action_by_agent.update( + zip(agents_by_policy[policy_id], policy.choose_action(np.vstack(states_by_policy[policy_id]))) + ) - for policy_id, states in states_by_policy.items(): - msg = self._conn[policy_id].recv() - if len(states) == 1: - msg = [msg] - for agent, act in zip(agents_by_policy[policy_id], msg): - action[agent] = act + for policy_id, conn in self._conn.items(): + if states_by_policy[policy_id]: + action_by_agent.update(zip(agents_by_policy[policy_id], conn.recv())) - return { - **action, - **{ - agent: self.policy_by_agent[agent].choose_action(state) for agent, state in state_by_agent.items() - if agent in self.policy_by_agent - } - } + return action_by_agent def set_policy_states(self, policy_state_dict: dict): for policy_id, conn in self._conn.items(): @@ -122,9 +138,8 @@ def exploit(self): def exploration_step(self): for conn in self._conn.values(): conn.send({"type": "exploration_step"}) - for policy in self.policy_dict.values(): - if hasattr(policy, "exploration_step"): - policy.exploration_step() + for sch in self.exploration_scheduler.values(): + sch.step() def get_rollout_info(self): for conn in self._conn.values(): @@ -132,10 +147,10 @@ def get_rollout_info(self): return { **{ - id_: policy.get_rollout_info() for id_, policy in self.policy_dict.items() + policy_id: policy.get_rollout_info() for policy_id, policy in self.policy_dict.items() if isinstance(policy, RLPolicy) }, - **{id_: conn.recv() for id_, conn in self._conn.items()} + **{policy_id: conn.recv() for policy_id, conn in self._conn.items()} } def get_exploration_params(self): @@ -144,10 +159,10 @@ def get_exploration_params(self): return { **{ - id_: policy.exploration_params for id_, policy in self.policy_dict.items() + policy_id: policy.exploration_params for policy_id, policy in self.policy_dict.items() if isinstance(policy, RLPolicy) }, - **{id_: conn.recv() for id_, conn in self._conn.items()} + **{policy_id: conn.recv() for policy_id, conn in self._conn.items()} } def record_transition(self, agent: str, state, action, reward, next_state, terminal: bool): @@ -187,6 +202,7 @@ def __init__( self, get_env: Callable[[], Env], get_policy_func_dict: Dict[str, Callable], + exploration_scheduler_option: Dict[str, tuple], agent2policy: Dict[str, str], get_test_env: Callable[[], Env] = None, reward_eval_delay: int = 0, @@ -198,7 +214,8 @@ def __init__( self.env = None self.agent_wrapper = AgentWrapper( - get_policy_func_dict, agent2policy, policies_to_parallelize=policies_to_parallelize + get_policy_func_dict, agent2policy, exploration_scheduler_option, + policies_to_parallelize=policies_to_parallelize ) self.reward_eval_delay = reward_eval_delay diff --git a/maro/rl/modeling/__init__.py b/maro/rl/modeling/__init__.py index 832593839..b2d7d4bec 100644 --- a/maro/rl/modeling/__init__.py +++ b/maro/rl/modeling/__init__.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .core_model import AbsCoreModel, OptimOption +from .core_model import AbsCoreModel from .fc_block import FullyConnected from .specials import ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet __all__ = [ - "AbsCoreModel", "OptimOption", + "AbsCoreModel", "FullyConnected", "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet" ] diff --git a/maro/rl/modeling/core_model.py b/maro/rl/modeling/core_model.py index 24e3ad92e..c155fba5c 100644 --- a/maro/rl/modeling/core_model.py +++ b/maro/rl/modeling/core_model.py @@ -2,179 +2,37 @@ # Licensed under the MIT license. from abc import abstractmethod -from typing import Dict, List, Union import torch import torch.nn as nn -from maro.rl.utils import get_torch_lr_scheduler_cls, get_torch_optim_cls -from maro.utils import clone -from maro.utils.exception.rl_toolkit_exception import MissingOptimizer - - -class OptimOption: - """Model optimization options. - - Args: - optim_cls: A string indicating an optimizer class provided by torch.optim or custom subclass of - torch.optim.Optimizer. If a string is provided, it must be present in the ``TORCH_OPTIM`` index. - optim_params (dict): Parameters for the optimizer class. - scheduler_cls: A string indicating an lr-scheduler class provided by torch.optim.lr_scheduler or custom - subclass of torch.optim.lr_scheduler. If a string is provided, it must be present in the - ``TORCH_LR_SCHEDULER`` index. Defaults to None. - scheduler_params (dict): Parameters for the scheduler class. Defaults to None. - """ - __slots__ = ["optim_cls", "optim_params", "scheduler_cls", "scheduler_params"] - - def __init__(self, optim_cls, optim_params: dict, scheduler_cls=None, scheduler_params: dict = None): - self.optim_cls = get_torch_optim_cls(optim_cls) - self.optim_params = optim_params - self.scheduler_cls = get_torch_lr_scheduler_cls(scheduler_cls) - self.scheduler_params = scheduler_params - class AbsCoreModel(nn.Module): """Trainable model that consists of multiple network components. Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. device (str): Identifier for the torch device. The model instance will be moved to the specified device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): + def __init__(self): super().__init__() - self.component = component if isinstance(component, nn.Module) else nn.ModuleDict(component) - if optim_option is None: - self.optimizer = None - self.scheduler = None - self.eval() - for param in self.parameters(): - param.requires_grad = False - else: - if isinstance(optim_option, dict): - self.optimizer, self.scheduler = {}, {} - for name, opt in optim_option.items(): - self.optimizer[name] = opt.optim_cls(self.component[name].parameters(), **opt.optim_params) - if opt.scheduler_cls: - self.scheduler[name] = opt.scheduler_cls(self.optimizer[name], **opt.scheduler_params) - else: - self.optimizer = optim_option.optim_cls(self.parameters(), **optim_option.optim_params) - if optim_option.scheduler_cls: - self.scheduler = optim_option.scheduler_cls(self.optimizer, **optim_option.scheduler_params) - - if device is None: - self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - else: - self.device = torch.device(device) - self.to(self.device) - - @property - def trainable(self) -> bool: - """Return True if at least one optimizer is registered.""" - return self.optimizer is not None @abstractmethod def forward(self, *args, **kwargs): raise NotImplementedError - def get_gradients(self, loss: torch.tensor): - """Compute gradients from a loss """ - if self.optimizer is None: - raise MissingOptimizer("No optimizer registered to the model") - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.zero_grad() - else: - self.optimizer.zero_grad() - - # Obtain gradients through back-propagation - loss.backward() - - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad_dict_list: List[Dict[str, float]]): - avg_grad_dict = { - param_name: torch.mean(torch.stack([grad_dict[param_name] for grad_dict in grad_dict_list]), dim=0) - for param_name in grad_dict_list[0] - } - for name, param in self.named_parameters(): - param.grad = avg_grad_dict[name] - - # Apply gradients - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.step() - else: - self.optimizer.step() - - def step(self, loss): + @abstractmethod + def step(self, loss: torch.tensor): """Use the loss to back-propagate gradients and apply them to the underlying parameters. Args: loss: Result of a computation graph that involves the underlying parameters. """ - if self.optimizer is None: - raise MissingOptimizer("No optimizer registered to the model") - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.zero_grad() - else: - self.optimizer.zero_grad() - - # Obtain gradients through back-propagation - loss.backward() - - # Apply gradients - if isinstance(self.optimizer, dict): - for optimizer in self.optimizer.values(): - optimizer.step() - else: - self.optimizer.step() - - def update_learning_rate(self, component_name: Union[str, List[str]] = None): - if not isinstance(self.scheduler, dict): - self.scheduler.step() - elif isinstance(component_name, str): - if component_name not in self.scheduler: - raise KeyError(f"Component {component_name} does not have a learning rate scheduler") - self.scheduler[component_name].step() - elif isinstance(component_name, list): - for key in component_name: - if key not in self.scheduler: - raise KeyError(f"Component {key} does not have a learning rate scheduler") - self.scheduler[key].step() - else: - for sch in self.scheduler.values(): - sch.step() - - def copy(self, with_optimizer: bool = False, device: str = None): - """Return a deep copy of the instance; - - Args: - with_opimizer (bool): If True, the registered optimizers will also be deep copied. - Defaults to False. - device (str): The device the copied instance should be placed on. Defaults to None, - in which case the copied instance will be placed on the same device as the instance itself. - """ - model_copy = clone(self) - if not with_optimizer: - model_copy.optimizer = None - model_copy.scheduler = None + raise NotImplementedError - device = self.device if device is None else torch.device(device) - model_copy.to(device) + def get_gradients(self, loss: torch.tensor): + pass - return model_copy + def apply_gradients(self, grad: dict): + pass diff --git a/maro/rl/modeling/specials.py b/maro/rl/modeling/specials.py index add2d311f..d5d76654f 100644 --- a/maro/rl/modeling/specials.py +++ b/maro/rl/modeling/specials.py @@ -2,39 +2,18 @@ # Licensed under the MIT license. from abc import abstractmethod -from typing import Dict, Union +from typing import Union import numpy as np import torch from torch import nn from torch.distributions import Categorical -from .core_model import AbsCoreModel, OptimOption +from .core_model import AbsCoreModel class DiscreteACNet(AbsCoreModel): - """Model container for the actor-critic architecture for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) + """Model framework for the actor-critic architecture for finite and discrete action spaces.""" @property @abstractmethod @@ -56,13 +35,17 @@ def forward(self, states: torch.tensor, actor: bool = True, critic: bool = True) """ raise NotImplementedError - def get_action(self, states: torch.tensor, max_prob: bool = False): + def get_action(self, states: torch.tensor, greedy: bool = False): """ - Given Q-values for a batch of states, return the action index and the corresponding maximum Q-value - for each state. + Given Q-values for a batch of states, return the actions, the corresponding log-P values and the state values. + + Args: + states (torch.tensor): State batch to compute actions for. + greedy (bool): If True, the action with the greatest probability will be returned for each state in the + batch. Defaults to False. """ action_probs, values = self.forward(states) - if max_prob: + if greedy: probs, actions = action_probs.max(dim=1) return actions, torch.log(probs), values else: @@ -73,28 +56,7 @@ def get_action(self, states: torch.tensor, max_prob: bool = False): class DiscretePolicyNet(AbsCoreModel): - """Parameterized policy for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults to None. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) + """Parameterized policy for finite and discrete action spaces.""" @property @abstractmethod @@ -112,13 +74,17 @@ def forward(self, states: torch.tensor) -> torch.tensor: """ raise NotImplementedError - def get_action(self, states: torch.tensor, max_prob: bool = False): + def get_action(self, states: torch.tensor, greedy: bool = False): """ - Given a batch of states, return actions selected based on the probabilities computed by ``forward`` - and the corresponding log probabilities. + Given Q-values for a batch of states, return the actions and the corresponding log-P values. + + Args: + states (torch.tensor): State batch to compute actions for. + greedy (bool): If True, the action with the greatest probability will be returned for each state in the + batch. Defaults to False. """ action_prob = self.forward(states) # (batch_size, num_actions) - if max_prob: + if greedy: prob, action = action_prob.max(dim=1) return action, torch.log(prob) else: @@ -129,28 +95,7 @@ def get_action(self, states: torch.tensor, max_prob: bool = False): class DiscreteQNet(AbsCoreModel): - """Q-value model for finite and discrete action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) + """Q-value model for finite and discrete action spaces.""" @property @abstractmethod @@ -197,47 +142,9 @@ def soft_update(self, other_model: nn.Module, tau: float): class ContinuousACNet(AbsCoreModel): - """Model container for the actor-critic architecture for continuous action spaces. - - Args: - component (Union[nn.Module, Dict[str, nn.Module]]): Network component(s) comprising the model. - optim_option (Union[OptimOption, Dict[str, OptimOption]]): Optimizer options for the components. - If none, no optimizer will be created for the model which means the model is not trainable. - If it is a OptimOption instance, a single optimizer will be created to jointly optimize all - parameters of the model. If it is a dictionary of OptimOptions, the keys will be matched against - the component names and optimizers created for them. Note that it is possible to freeze certain - components while optimizing others by providing a subset of the keys in ``component``. - Defaults toNone. - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ - def __init__( - self, - component: Union[nn.Module, Dict[str, nn.Module]], - optim_option: Union[OptimOption, Dict[str, OptimOption]] = None, - device: str = None - ): - super().__init__(component, optim_option=optim_option, device=device) - - @property - @abstractmethod - def input_dim(self): - raise NotImplementedError - - def set_action_space( - self, - min_action: Union[float, np.ndarray] = None, - max_action: Union[float, np.ndarray] = None - ): - """Set action clamping bounds. - - Args: - min_action (Union[float, np.ndarray]): Lower bound for action. Actions generated from the model will be - clipped according to this bound. Defaults to None, which means no lower bound. - max_action (Union[float, np.ndarray]): Upper bound for action. Actions generated from the model will be - clipped according to this bound. Defaults to None, which means no upper bound. - """ + """Model container for the actor-critic architecture for continuous action spaces.""" + def __init__(self, min_action: Union[float, np.ndarray] = None, max_action: Union[float, np.ndarray] = None): + super().__init__() if min_action: assert isinstance(min_action, (float, np.ndarray)), "min_action must be a float or a numpy array" if max_action: @@ -250,6 +157,24 @@ def set_action_space( self._min_action = torch.from_numpy(min_action) if isinstance(min_action, np.ndarray) else min_action self._max_action = torch.from_numpy(max_action) if isinstance(max_action, np.ndarray) else max_action + @property + @abstractmethod + def input_dim(self): + raise NotImplementedError + + @property + @abstractmethod + def action_dim(self): + raise NotImplementedError + + @property + def min_action(self): + return self._min_action + + @property + def max_action(self): + return self._max_action + @abstractmethod def forward(self, states: torch.tensor, actions=None) -> torch.tensor: """Compute actions for a batch of states or Q-values for a batch of states and actions. diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 920ea51d6..b1d9614e9 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -95,14 +95,18 @@ def __init__( clip_ratio: float = None, lam: float = 0.9, buffer_size: int = 10000, - get_loss_on_rollout: bool = False + get_loss_on_rollout: bool = False, + device: str = None ): if not isinstance(ac_net, DiscreteACNet): raise TypeError("model must be an instance of 'DiscreteACNet'") super().__init__(name) - self.ac_net = ac_net - self.device = self.ac_net.device + if device is None: + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + else: + self.device = torch.device(device) + self.ac_net = ac_net.to(self.device) self.reward_discount = reward_discount self.grad_iters = grad_iters self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() @@ -123,14 +127,11 @@ def choose_action(self, states: np.ndarray): if len(states.shape) == 1: states = states.unsqueeze(dim=0) with torch.no_grad(): - actions, logps, values = self.ac_net.get_action(states) + actions, logps, values = self.ac_net.get_action(states, greedy=self.greedy) actions, logps, values = actions.cpu().numpy(), logps.cpu().numpy(), values.cpu().numpy() - if len(actions) == 1: - return {"action": actions[0], "logp": logps[0], "value": values[0]} - else: - return [ - {"action": action, "logp": logp, "value": value} for action, logp, value in zip(actions, logps, values) - ] + return [ + {"action": action, "logp": logp, "value": value} for action, logp, value in zip(actions, logps, values) + ] def record( self, @@ -167,7 +168,6 @@ def _get_batch(self): return {key: np.concatenate(vals) for key, vals in batch.items()} def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() states = torch.from_numpy(batch["states"]).to(self.device) actions = torch.from_numpy(batch["actions"]).to(self.device) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index b5afcc749..9c386d03c 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -1,14 +1,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import List, Union +from typing import Callable, List, Union import numpy as np import torch -from maro.rl.exploration import GaussianNoiseExploration, NoiseExploration +from maro.rl.exploration import gaussian_noise from maro.rl.modeling import ContinuousACNet from maro.rl.utils import get_torch_loss_cls +from maro.utils import clone from .policy import RLPolicy from .replay import ReplayMemory @@ -32,12 +33,17 @@ class DDPG(RLPolicy): loss = policy_loss + ``q_value_loss_coeff`` * q_value_loss. Defaults to 1.0. soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. Defaults to 1.0. - exploration: Exploration strategy for generating exploratory actions. Defaults to ``GaussianNoiseExploration``. + exploration_func: Exploration strategy for generating exploratory actions. Defaults to ``gaussian_noise``. + exploration_params (dict): Keyword parameters for the exploration strategy. replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. - batch_size (int): Training sample. Defaults to 32. + warmup (int): When the total number of experiences in the replay memory is below this threshold, + ``choose_action`` will return uniformly random actions for warm-up purposes. Defaults to 1000. + rollout_batch_size (int): Size of the experience batch to use as roll-out information by calling + ``get_rollout_info``. Defaults to 1000. + train_batch_size (int): Batch size for training the Q-net. Defaults to 32. """ def __init__( self, @@ -49,22 +55,26 @@ def __init__( q_value_loss_cls="mse", q_value_loss_coeff: float = 1.0, soft_update_coeff: float = 1.0, - exploration: NoiseExploration = GaussianNoiseExploration(), + exploration_func: Callable = gaussian_noise, + exploration_params: dict = {"mean": .0, "stddev": 1.0, "relative": False}, replay_memory_capacity: int = 10000, random_overwrite: bool = False, - batch_size: int = 32 + warmup: int = 1000, + rollout_batch_size: int = 1000, + train_batch_size: int = 32, + device: str = None ): if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") super().__init__(name) - self.ac_net = ac_net - self.device = self.ac_net.device - if self.ac_net.trainable: - self.target_ac_net = ac_net.copy() - self.target_ac_net.eval() + if device is None: + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') else: - self.target_ac_net = None + self.device = torch.device(device) + self.ac_net = ac_net.to(self.device) + self.target_ac_net = clone(self.ac_net) + self.target_ac_net.eval() self.reward_discount = reward_discount self.num_epochs = num_epochs self.update_target_every = update_target_every @@ -78,19 +88,27 @@ def __init__( self._replay_memory = ReplayMemory( replay_memory_capacity, self.ac_net.input_dim, action_dim=1, random_overwrite=random_overwrite ) - self.batch_size = batch_size + self.warmup = warmup + self.rollout_batch_size = rollout_batch_size + self.train_batch_size = train_batch_size - self.exploration = exploration + self.exploration_func = exploration_func + self.exploration_params = exploration_params self.greedy = True def choose_action(self, states) -> Union[float, np.ndarray]: + if self._replay_memory.size < self.warmup: + return np.random.uniform( + low=self.ac_net.min_action, high=self.ac_net.max_action, size=self.ac_net.action_dim + ) + self.ac_net.eval() with torch.no_grad(): actions = self.ac_net.get_action(states).cpu().numpy() if not self.greedy: - actions = self.exploration(actions, state=states) - return actions[0] if len(actions) == 1 else actions + actions = self.exploration_func(actions, state=states) + return actions def record( self, @@ -112,8 +130,10 @@ def record( np.expand_dims(terminal, axis=0) ) + def get_rollout_info(self): + return self._replay_memory.sample(self.rollout_batch_size) + def get_batch_loss(self, batch: dict, explicit_grad: bool = False) -> dict: - assert self.ac_net.trainable, "ac_net needs to have at least one optimizer registered." self.ac_net.train() states = torch.from_numpy(batch["states"]).to(self.device) next_states = torch.from_numpy(["next_states"]).to(self.device) @@ -154,7 +174,7 @@ def learn(self, batch: dict): ) for _ in range(self.num_epochs): - train_batch = self._replay_memory.sample(self.batch_size) + train_batch = self._replay_memory.sample(self.train_batch_size) self.ac_net.step(self.get_batch_loss(train_batch)["loss"]) self._ac_net_version += 1 if self._ac_net_version - self._target_ac_net_version == self.update_target_every: @@ -165,19 +185,6 @@ def _update_target(self): self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) self._target_ac_net_version = self._ac_net_version - @property - def exploration_params(self): - return self.exploration.parameters - - def exploit(self): - self.greedy = True - - def explore(self): - self.greedy = False - - def exploration_step(self): - self.exploration.step() - def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index 742d884fb..fdbe90cec 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import List, Union +from typing import Callable, List, Union import numpy as np import torch -from maro.rl.exploration import DiscreteSpaceExploration, EpsilonGreedyExploration +from maro.rl.exploration import eps_greedy from maro.rl.modeling import DiscreteQNet +from maro.utils import clone from .policy import RLPolicy from .replay import ReplayMemory @@ -32,6 +33,7 @@ class PrioritizedExperienceReplay: This value of ``beta`` should not exceed 1.0, which corresponds to full annealing. Defaults to 0.4. beta_step (float): The amount ``beta`` is incremented by after each get() call until it reaches 1.0. Defaults to 0.001. + max_priority (float): Maximum priority value to use for new experiences. Defaults to 1e8. """ def __init__( self, @@ -135,7 +137,14 @@ class DQN(RLPolicy): random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. - batch_size (int): Training sample. Defaults to 32. + warmup (int): When the total number of experiences in the replay memory is below this threshold, + ``choose_action`` will return uniformly random actions for warm-up purposes. Defaults to 1000. + rollout_batch_size (int): Size of the experience batch to use as roll-out information by calling + ``get_rollout_info``. Defaults to 1000. + train_batch_size (int): Batch size for training the Q-net. Defaults to 32. + prioritized_replay_kwargs (dict): Keyword arguments for prioritized experience replay. See + ``PrioritizedExperienceReplay`` for details. Defaults to None, in which case experiences will be sampled + from the replay memory uniformly randomly. """ def __init__( @@ -147,24 +156,28 @@ def __init__( update_target_every: int = 5, soft_update_coeff: float = 0.1, double: bool = False, - exploration: DiscreteSpaceExploration = EpsilonGreedyExploration(), + exploration_func: Callable = eps_greedy, + exploration_params: dict = {"epsilon": 0.1}, replay_memory_capacity: int = 10000, random_overwrite: bool = False, - train_batch_size: int = 32, + warmup: int = 1000, rollout_batch_size: int = 1000, - prioritized_replay_kwargs: dict = None + train_batch_size: int = 32, + prioritized_replay_kwargs: dict = None, + device: str = None ): if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") super().__init__(name) - self.q_net = q_net - self.device = self.q_net.device - if self.q_net.trainable: - self.target_q_net = q_net.copy() - self.target_q_net.eval() + self._num_actions = self.q_net.num_actions + if device is None: + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') else: - self.target_q_net = None + self.device = torch.device(device) + self.q_net = q_net.to(self.device) + self.target_q_net = clone(q_net) + self.target_q_net.eval() self._q_net_version = 0 self._target_q_net_version = 0 @@ -177,6 +190,7 @@ def __init__( self._replay_memory = ReplayMemory( replay_memory_capacity, self.q_net.input_dim, action_dim=1, random_overwrite=random_overwrite ) + self.warmup = warmup self.rollout_batch_size = rollout_batch_size self.train_batch_size = train_batch_size self.prioritized_replay = prioritized_replay_kwargs is not None @@ -185,10 +199,14 @@ def __init__( else: self._loss_func = torch.nn.MSELoss() - self.exploration = exploration + self.exploration_func = exploration_func + self.exploration_params = exploration_params self.greedy = True # set initial exploration status to True def choose_action(self, states: np.ndarray) -> Union[int, np.ndarray]: + if self._replay_memory.size < self.warmup: + return np.random.randint(self._num_actions, size=(states.shape[0] if len(states.shape) > 1 else 1,)) + self.q_net.eval() states = torch.from_numpy(states).to(self.device) if len(states.shape) == 1: @@ -197,12 +215,10 @@ def choose_action(self, states: np.ndarray) -> Union[int, np.ndarray]: q_for_all_actions = self.q_net(states) # (batch_size, num_actions) _, actions = q_for_all_actions.max(dim=1) - actions = actions.cpu().numpy() - if not self.greedy: - if self.exploration.action_space is None: - self.exploration.set_action_space(np.arange(q_for_all_actions.shape[1])) - actions = self.exploration(actions, state=states) - return actions[0] if len(actions) == 1 else actions + if self.greedy: + return actions.cpu().numpy() + else: + return self.exploration_func(actions.cpu().numpy(), self._num_actions, **self.exploration_params) def record( self, @@ -245,7 +261,6 @@ def _get_batch(self): return self._replay_memory.sample(self.train_batch_size) def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - assert self.q_net.trainable, "q_net needs to have at least one optimizer registered." self.q_net.train() states = torch.from_numpy(batch["states"]).to(self.device) next_states = torch.from_numpy(batch["next_states"]).to(self.device) @@ -309,19 +324,6 @@ def _update_target(self): self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) self._target_q_net_version = self._q_net_version - @property - def exploration_params(self): - return self.exploration.parameters - - def exploit(self): - self.greedy = True - - def explore(self): - self.greedy = False - - def exploration_step(self): - self.exploration.step() - def set_state(self, policy_state): self.q_net.load_state_dict(policy_state["eval"]) if "target" in policy_state: diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index e5e204b18..c38ee19ae 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -68,13 +68,17 @@ def __init__( reward_discount: float, grad_iters: int = 1, buffer_size: int = 10000, - get_loss_on_rollout: bool = False + get_loss_on_rollout: bool = False, + device: str = None ): if not isinstance(policy_net, DiscretePolicyNet): raise TypeError("model must be an instance of 'DiscretePolicyNet'") super().__init__(name) - self.policy_net = policy_net - self.device = self.policy_net.device + if device is None: + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + else: + self.device = torch.device(device) + self.policy_net = policy_net.to(self.device) self.reward_discount = reward_discount self.grad_iters = grad_iters self.buffer_size = buffer_size @@ -86,9 +90,9 @@ def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Return actions and log probabilities for given states.""" self.policy_net.eval() with torch.no_grad(): - actions, log_p = self.policy_net.get_action(states) - actions, log_p = actions.cpu().numpy(), log_p.cpu().numpy() - return (actions[0], log_p[0]) if len(actions) == 1 else actions, log_p + actions, logps = self.policy_net.get_action(states, greedy=self.greedy) + actions, logps = actions.cpu().numpy(), logps.cpu().numpy() + return [{"action": action, "logp": logp} for action, logp in zip(actions, logps)] def record( self, @@ -124,9 +128,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): the experience store's ``get`` method should be a sequential set, i.e., in the order in which they are generated during the simulation. Otherwise, the return values may be meaningless. """ - assert self.policy_net.trainable, "policy_net needs to have at least one optimizer registered." self.policy_net.train() - returns = torch.from_numpy(np.asarray(batch.returns)).to(self.device) _, logp = self.policy_net(batch["states"]) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 918729617..59ddb34b0 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -44,6 +44,8 @@ class RLPolicy(AbsPolicy): """ def __init__(self, name: str): super().__init__(name) + self.exploration_params = {} + self.greedy = True @abstractmethod def choose_action(self, state): @@ -70,17 +72,10 @@ def learn(self, trajectories: List[dict]): raise NotImplementedError def exploit(self): - pass + self.greedy = True def explore(self): - pass - - def exploration_step(self): - pass - - @property - def exploration_params(self): - return None + self.greedy = False @abstractmethod def get_state(self): diff --git a/maro/rl/utils/gradient_averaging.py b/maro/rl/utils/gradient_averaging.py new file mode 100644 index 000000000..5c81e2d5f --- /dev/null +++ b/maro/rl/utils/gradient_averaging.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List + +import torch + +def average_grad(grad_list: List[str, dict]): + return { + param_name: torch.mean(torch.stack([grad[param_name] for grad in grad_list]), dim=0) + for param_name in grad_list[0] + } From 2dbf3c3dd6af703b24058604d2e6bed9e0bae8be Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 6 Sep 2021 06:35:33 +0000 Subject: [PATCH 427/482] bug fixes and doc update --- examples/rl/cim/env_sampler.py | 11 ++-- examples/rl/cim/policies.py | 65 +++++++++------------ examples/rl/workflows/config.yml | 4 +- maro/rl/exploration/scheduler.py | 89 ++++++++++------------------- maro/rl/learning/env_sampler.py | 19 ++++-- maro/rl/learning/rollout_manager.py | 2 +- maro/rl/modeling/core_model.py | 2 +- maro/rl/policy/ac.py | 34 +++++++---- maro/rl/policy/ddpg.py | 10 +++- maro/rl/policy/dqn.py | 12 ++-- maro/rl/policy/pg.py | 22 ++++--- 11 files changed, 135 insertions(+), 135 deletions(-) diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index ae37a3146..f3305c401 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -6,6 +6,7 @@ import numpy as np +from maro.rl.exploration import MultiLinearExplorationScheduler from maro.rl.learning import AbsEnvSampler from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType @@ -16,7 +17,8 @@ from callbacks import post_step from config import ( - action_shaping_conf, env_conf, port_attributes, reward_shaping_conf, state_shaping_conf, vessel_attributes + action_shaping_conf, env_conf, exploration_conf, port_attributes, reward_shaping_conf, state_shaping_conf, + vessel_attributes ) from policies import policy_func_dict @@ -90,9 +92,10 @@ def get_reward(self, actions, tick): def get_env_sampler(): return CIMEnvSampler( - lambda: Env(**env_conf), - policy_func_dict, - agent2policy, + get_env=lambda: Env(**env_conf), + get_policy_func_dict=policy_func_dict, + exploration_scheduler_option={"dqn": {"epsilon": (MultiLinearExplorationScheduler, exploration_conf)}}, + agent2policy=agent2policy, reward_eval_delay=reward_shaping_conf["time_window"], post_step=post_step, policies_to_parallelize=["ac", "dqn"] diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index c160f3aaa..15b1bb151 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -4,7 +4,6 @@ import os import sys -from maro.rl.exploration import MultiLinearExplorationScheduler from maro.rl.modeling import DiscreteACNet, DiscreteQNet, FullyConnected from maro.rl.policy import DQN, ActorCritic @@ -13,13 +12,13 @@ sys.path.insert(0, cim_path) from config import ( ac_conf, actor_net_conf, actor_optim_conf, critic_net_conf, critic_optim_conf, dqn_conf, q_net_conf, - q_net_optim_conf, exploration_conf, state_dim + q_net_optim_conf, state_dim ) class QNet(DiscreteQNet): - def __init__(self, device=None): - super().__init__(device=device) + def __init__(self): + super().__init__() self.fc = FullyConnected(**q_net_conf) self.optim = q_net_optim_conf[0](self.fc.parameters(), **q_net_optim_conf[1]) @@ -27,8 +26,12 @@ def __init__(self, device=None): def input_dim(self): return state_dim + @property + def num_actions(self): + return q_net_conf["output_dim"] + def forward(self, states): - return self.component(states) + return self.fc(states) def step(self, loss): self.optim.zero_grad() @@ -36,42 +39,30 @@ def step(self, loss): self.optim.step() -def get_dqn(name: str): - exploration = EpsilonGreedyExploration() - exploration.register_schedule( - scheduler_cls=MultiLinearExplorationScheduler, - param_name="epsilon", - **exploration_conf - ) - return DQN(name, QNet(), exploration=exploration, **dqn_conf) - - -def get_ac(name: str): - class MyACNet(DiscreteACNet): - def __init__(self): - self.actor = FullyConnected(**actor_net_conf) - self.critic = FullyConnected(**critic_net_conf) - self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1]) - self.critic_optim = critic_optim_conf[0](self.critic.parameters, **critic_optim_conf[1]) - - @property - def input_dim(self): - return state_dim +class MyACNet(DiscreteACNet): + def __init__(self): + super().__init__() + self.actor = FullyConnected(**actor_net_conf) + self.critic = FullyConnected(**critic_net_conf) + self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1]) + self.critic_optim = critic_optim_conf[0](self.critic.parameters(), **critic_optim_conf[1]) - def forward(self, states, actor: bool = True, critic: bool = True): - return (self.actor(states) if actor else None), (self.critic(states) if critic else None) + @property + def input_dim(self): + return state_dim - def step(self, loss): - self.actor_optim.zero_grad() - self.critic_optim.zero_grad() - loss.backward() - self.actor_optim.step() - self.critic_optim.step() + def forward(self, states, actor: bool = True, critic: bool = True): + return (self.actor(states) if actor else None), (self.critic(states) if critic else None) - return ActorCritic(name, MyACNet(), **ac_conf) + def step(self, loss): + self.actor_optim.zero_grad() + self.critic_optim.zero_grad() + loss.backward() + self.actor_optim.step() + self.critic_optim.step() policy_func_dict = { - "dqn": get_dqn, - "ac": get_ac + "dqn": lambda name: DQN(name, QNet(), **dqn_conf), + "ac": lambda name: ActorCritic(name, MyACNet(), **ac_conf) } diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 7db3cfa5b..61caf604a 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -4,8 +4,8 @@ job: cim scenario: cim mode: sync -num_episodes: 5 -eval_schedule: 5 +num_episodes: 10 +eval_schedule: 10 num_steps: -1 max_lag: 0 sync: diff --git a/maro/rl/exploration/scheduler.py b/maro/rl/exploration/scheduler.py index 42361535d..f58608125 100644 --- a/maro/rl/exploration/scheduler.py +++ b/maro/rl/exploration/scheduler.py @@ -4,8 +4,6 @@ from abc import ABC, abstractmethod from typing import List, Tuple -from maro.rl.policy.policy import RLPolicy - class AbsExplorationScheduler(ABC): """Abstract exploration scheduler. @@ -13,29 +11,24 @@ class AbsExplorationScheduler(ABC): Each exploration scheduler is registered to a single parameter of an ``RLPolicy`` instance. Args: - policy (RLPolicy): An ``RLPolicy`` instance to which the scheduler is applied. + exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the + scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. - initial_value: Initial value for the exploration parameter. If None, the value the exploration - instance is instantiated with will be used as the initial value. Defaults to None. + initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is + instantiated with will be used as the initial value. Defaults to None. """ - def __init__( - self, - policy: RLPolicy, - param_name: str, - last_ep: int, - initial_value=None - ): + def __init__(self, exploration_params: dict, param_name: str, last_ep: int, initial_value=None): super().__init__() - self._policy = policy + self._exploration_params = exploration_params self.param_name = param_name self.last_ep = last_ep if initial_value is not None: - self._policy.exploration_params[self.param_name] = initial_value + self._exploration_params[self.param_name] = initial_value def get_value(self): - return self._policy.exploration_params[self.param_name] + return self._exploration_params[self.param_name] @abstractmethod def step(self): @@ -46,41 +39,43 @@ class LinearExplorationScheduler(AbsExplorationScheduler): """Linear exploration parameter schedule. Args: - policy (RLPolicy): An ``RLPolicy`` instance to which the scheduler is applied. + exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the + scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. - initial_value: Initial value for the exploration parameter. If None, the value the exploration - instance is instantiated with will be used as the initial value. Defaults to None. + initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is + instantiated with will be used as the initial value. Defaults to None. """ def __init__( self, - policy: RLPolicy, + exploration_params: dict, param_name: str, last_ep: int, final_value: float, initial_value: float = None, ): - super().__init__(policy, param_name, last_ep, initial_value=initial_value) + super().__init__(exploration_params, param_name, last_ep, initial_value=initial_value) self.final_value = final_value if self.last_ep > 1: - self.delta = (self.final_value - getattr(self._policy, self.param_name)) / (self.last_ep - 1) + self.delta = (self.final_value - self._exploration_params[self.param_name]) / (self.last_ep - 1) else: self.delta = 0 def step(self): - if self._policy.exploration_params[self.param_name] == self.final_value: + if self._exploration_params[self.param_name] == self.final_value: return - self._policy.exploration_params[self.param_name] += self.delta + self._exploration_params[self.param_name] += self.delta class MultiLinearExplorationScheduler(AbsExplorationScheduler): """Exploration parameter schedule that consists of multiple linear phases. Args: - policy (RLPolicy): An ``RLPolicy`` instance to which the scheduler is applied. + exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the + scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. splits (List[Tuple[int, float]]): List of points that separate adjacent linear phases. Each @@ -88,15 +83,15 @@ class MultiLinearExplorationScheduler(AbsExplorationScheduler): the start of another. These points do not have to be given in any particular order. There cannot be two points with the same first element (episode), or a ``ValueError`` will be raised. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. - initial_value: Initial value for the exploration parameter. If None, the value the exploration - instance is instantiated with will be used as the initial value. Defaults to None. + initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is + instantiated with will be used as the initial value. Defaults to None. Returns: An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. """ def __init__( self, - policy: RLPolicy, + exploration_params: dict, param_name: str, last_ep: int, splits: List[Tuple[int, float]], @@ -110,18 +105,18 @@ def __init__( if ep == ep2: raise ValueError("The zeroth element of split points must be unique") - super().__init__(policy, param_name, last_ep, initial_value=initial_value) + super().__init__(exploration_params, param_name, last_ep, initial_value=initial_value) self.final_value = final_value self._splits = splits self._ep = 1 self._split_index = 1 - self._delta = (self._splits[1][1] - self._policy.exploration_params[self.param_name]) / (self._splits[1][0] - 1) + self._delta = (self._splits[1][1] - self._exploration_params[self.param_name]) / (self._splits[1][0] - 1) def step(self): if self._split_index == len(self._splits): return - self._policy.exploration_params[self.param_name] += self._delta + self._exploration_params[self.param_name] += self._delta self._ep += 1 if self._ep == self._splits[self._split_index][0]: self._split_index += 1 @@ -133,34 +128,10 @@ def step(self): if __name__ == "__main__": - class TestPolicy(RLPolicy): - def __init__(self, name, epsilon: float): - super().__init__(name) - self.exploration_params["epsilon"] = epsilon - - def choose_action(self, state): - pass - - def get_rollout_info(self): - pass - - def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - pass - - def update(self, loss_info_list: List[dict]): - pass - - def learn(self, trajectories: List[dict]): - pass - - def get_state(self): - pass - - def set_state(self, policy_state): - pass - - policy = TestPolicy("test", epsilon=0.6) - scheduler = MultiLinearExplorationScheduler(policy, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0) + exploration_params = {"epsilon": 0.6} + scheduler = MultiLinearExplorationScheduler( + exploration_params, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0 + ) for ep in range(1, scheduler.last_ep + 1): - print(f"ep = {ep}, value = {policy.exploration_params['epsilon']}") + print(f"ep = {ep}, value = {exploration_params['epsilon']}") scheduler.step() diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index efa959ed7..90287dcd1 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -19,11 +19,12 @@ class AgentWrapper: + """Wrapper for multiple agents using multiple policies that exposes single-agent interfaces.""" def __init__( self, get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str], - exploration_scheduler_option: Dict[str, tuple], + exploration_scheduler_option: Dict[str, Dict[str, tuple]], policies_to_parallelize: List[str] = [] ): self._policies_to_parallelize = set(policies_to_parallelize) @@ -38,8 +39,10 @@ def __init__( } self.exploration_scheduler = { - policy_id: sch_cls(self.policy_dict[policy_id], **params) - for policy_id, (sch_cls, params) in exploration_scheduler_option.items() if policy_id in self.policy_dict + policy_id: sch_cls(self.policy_dict[policy_id].exploration_params, param_name, **sch_params) + for policy_id, opt_dict in exploration_scheduler_option.items() + for param_name, (sch_cls, sch_params) in opt_dict.items() + if policy_id in self.policy_dict } self._inference_services = [] @@ -48,8 +51,10 @@ def __init__( def _inference_service(name, get_policy, conn, exploration_scheduler_option): policy = get_policy(name) if exploration_scheduler_option: - sch_cls, params = exploration_scheduler_option - exploration_scheduler = sch_cls(policy, **params) + exploration_scheduler = { + param_name: sch_cls(policy.exploration_params, param_name, **sch_params) + for param_name, (sch_cls, sch_params) in exploration_scheduler_option.items() + } while True: msg = conn.recv() @@ -63,7 +68,9 @@ def _inference_service(name, get_policy, conn, exploration_scheduler_option): elif msg["type"] == "exploit": policy.exploit() elif msg["type"] == "exploration_step": - exploration_scheduler.step() + if exploration_scheduler_option: + for sch in exploration_scheduler.values(): + sch.step() elif msg["type"] == "rollout_info": conn.send(policy.get_rollout_info()) elif msg["type"] == "exploration_params": diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index 26f740655..de64e72de 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -378,7 +378,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): self._logger.info(f"Receive timeout, {self._max_extra_recv_tries - i - 1} attempts left") else: rollout_info, tracker = self._handle_worker_result(msg, ep, segment, version) - if info_by_policy: + if rollout_info: num_finishes += 1 for policy_id, info in rollout_info.items(): info_by_policy[policy_id].append(info) diff --git a/maro/rl/modeling/core_model.py b/maro/rl/modeling/core_model.py index c155fba5c..69673b592 100644 --- a/maro/rl/modeling/core_model.py +++ b/maro/rl/modeling/core_model.py @@ -8,7 +8,7 @@ class AbsCoreModel(nn.Module): - """Trainable model that consists of multiple network components. + """General model abstraction for Args: device (str): Identifier for the torch device. The model instance will be moved to the specified diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index b1d9614e9..0c80087ba 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -16,13 +16,11 @@ class ActorCritic(RLPolicy): class Buffer: - """Sequence of transitions for an agent. + """Store a sequence of transitions, i.e., a trajectory. Args: - states: Sequence of ``State`` objects traversed during simulation. - actions: Sequence of actions taken in response to the states. - rewards: Sequence of rewards received as a result of the actions. - info: Sequence of each transition's auxillary information. + state_dim (int): State vector dimension. + size (int): Buffer capacity, i.e., the maximum number of stored transitions. """ def __init__(self, state_dim, size: int = 10000): self.states = np.zeros((size, state_dim), dtype=np.float32) @@ -33,7 +31,7 @@ def __init__(self, state_dim, size: int = 10000): self.terminals = np.zeros(size, dtype=np.bool) self.size = size self._ptr = 0 - self._last_ptr = 0 + self._prev_ptr = 0 def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): self.states[self._ptr] = state @@ -48,9 +46,14 @@ def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = F self._ptr = 0 def get(self): + """Retrieve the latest trajectory segment.""" terminal = self.terminals[self._ptr - 1] - traj_slice = slice(self._last_ptr, self._ptr - (not terminal)) - self._last_ptr = self._ptr - (not terminal) + last = self._ptr - (not terminal) + if last > self._prev_ptr: + traj_slice = np.arange(self._prev_ptr, last) + else: # wrap-around + traj_slice = np.concatenate([np.arange(self._prev_ptr, self.size), np.arange(last)]) + self._prev_ptr = last return { "states": self.states[traj_slice], "actions": self.actions[traj_slice], @@ -80,6 +83,15 @@ def get(self): total loss will not include an entropy term. clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, in which case the actor loss is calculated using the usual policy gradient theorem. + lambda (float): Lambda value for generalized advantage estimation (TD-Lambda). Defaults to 0.9. + max_trajectory_len (int): Maximum trajectory length that can be held by the buffer (for each agent that uses + this policy). Defaults to 10000. + get_loss_on_rollout (bool): If True, ``get_rollout_info`` will return the loss information (including gradients) + for the trajectories stored in the buffers. The loss information, along with that from other roll-out + instances, can be passed directly to ``update``. Otherwise, it will simply process the trajectories into a + single data batch that can be passed directly to ``learn``. Defaults to False. + device (str): Identifier for the torch device. The ``ac_net`` will be moved to the specified device. If it is + None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. """ def __init__( @@ -94,7 +106,7 @@ def __init__( entropy_coeff: float = None, clip_ratio: float = None, lam: float = 0.9, - buffer_size: int = 10000, + max_trajectory_len: int = 10000, get_loss_on_rollout: bool = False, device: str = None ): @@ -115,10 +127,10 @@ def __init__( self.entropy_coeff = entropy_coeff self.clip_ratio = clip_ratio self.lam = lam - self.buffer_size = buffer_size + self.max_trajectory_len = max_trajectory_len self.get_loss_on_rollout = get_loss_on_rollout - self._buffer = defaultdict(lambda: self.Buffer(self.ac_net.input_dim, size=self.buffer_size)) + self._buffer = defaultdict(lambda: self.Buffer(self.ac_net.input_dim, size=self.max_trajectory_len)) def choose_action(self, states: np.ndarray): """Return actions and log probabilities for given states.""" diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 9c386d03c..abf084059 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -26,6 +26,7 @@ class DDPG(RLPolicy): name (str): Unique identifier for the policy. ac_net (ContinuousACNet): DDPG policy and q-value models. reward_discount (float): Reward decay as defined in standard RL terminology. + num_epochs (int): Number of training epochs per call to ``learn``. Defaults to 1. update_target_every (int): Number of training rounds between policy target model updates. q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". @@ -33,8 +34,11 @@ class DDPG(RLPolicy): loss = policy_loss + ``q_value_loss_coeff`` * q_value_loss. Defaults to 1.0. soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. Defaults to 1.0. - exploration_func: Exploration strategy for generating exploratory actions. Defaults to ``gaussian_noise``. - exploration_params (dict): Keyword parameters for the exploration strategy. + exploration_func (Callable): Function to generate exploratory actions. The function takes an action + (single or batch) and a set of keyword arguments, and returns an exploratory action (single or batch + depending on the input). Defaults to ``gaussian_noise``. + exploration_params (dict): Keyword arguments for ``exploration_func``. Defaults to {"mean": .0, "stddev": 1.0, + "relative": False}. replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with @@ -44,6 +48,8 @@ class DDPG(RLPolicy): rollout_batch_size (int): Size of the experience batch to use as roll-out information by calling ``get_rollout_info``. Defaults to 1000. train_batch_size (int): Batch size for training the Q-net. Defaults to 32. + device (str): Identifier for the torch device. The ``ac_net`` will be moved to the specified device. If it is + None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. """ def __init__( self, diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index fdbe90cec..d1c6cda9d 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -123,7 +123,7 @@ class DQN(RLPolicy): name (str): Unique identifier for the policy. q_net (DiscreteQNet): Q-value model. reward_discount (float): Reward decay as defined in standard RL terminology. - train_epochs (int): Number of training epochs per call to ``update()``. Defaults to 1. + num_epochs (int): Number of training epochs per call to ``learn``. Defaults to 1. update_target_every (int): Number of gradient steps between target model updates. soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. @@ -131,8 +131,10 @@ class DQN(RLPolicy): double (bool): If True, the next Q values will be computed according to the double DQN algorithm, i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. - exploration (DiscreteSpaceExploration): Exploration strategy for generating exploratory actions. Defaults to - ``EpsilonGreedyExploration``. + exploration_func (Callable): Function to generate exploratory actions. The function takes an action + (single or batch), the total number of possible actions and a set of keyword arguments, and returns an + exploratory action (single or batch depending on the input). Defaults to ``epsilon_greedy``. + exploration_params (dict): Keyword arguments for ``exploration_func``. Defaults to {"epsilon": 0.1}. replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with @@ -145,6 +147,8 @@ class DQN(RLPolicy): prioritized_replay_kwargs (dict): Keyword arguments for prioritized experience replay. See ``PrioritizedExperienceReplay`` for details. Defaults to None, in which case experiences will be sampled from the replay memory uniformly randomly. + device (str): Identifier for the torch device. The ``q_net`` will be moved to the specified device. If it is + None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. """ def __init__( @@ -170,7 +174,6 @@ def __init__( raise TypeError("model must be an instance of 'DiscreteQNet'") super().__init__(name) - self._num_actions = self.q_net.num_actions if device is None: self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') else: @@ -181,6 +184,7 @@ def __init__( self._q_net_version = 0 self._target_q_net_version = 0 + self._num_actions = self.q_net.num_actions self.reward_discount = reward_discount self.num_epochs = num_epochs self.update_target_every = update_target_every diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index c38ee19ae..b3fa53238 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -15,13 +15,11 @@ class PolicyGradient(RLPolicy): class Buffer: - """Sequence of transitions for an agent. + """Store a sequence of transitions, i.e., a trajectory. Args: - states: Sequence of ``State`` objects traversed during simulation. - actions: Sequence of actions taken in response to the states. - rewards: Sequence of rewards received as a result of the actions. - info: Sequence of each transition's auxillary information. + state_dim (int): State vector dimension. + size (int): Buffer capacity, i.e., the maximum number of stored transitions. """ def __init__(self, state_dim, size: int = 10000): self.states = np.zeros((size, state_dim), dtype=np.float32) @@ -60,6 +58,14 @@ def get(self): It may or may not have a shared bottom stack. reward_discount (float): Reward decay as defined in standard RL terminology. grad_iters (int): Number of gradient steps for each batch or set of batches. Defaults to 1. + max_trajectory_len (int): Maximum trajectory length that can be held by the buffer (for each agent that uses + this policy). Defaults to 10000. + get_loss_on_rollout (bool): If True, ``get_rollout_info`` will return the loss information (including gradients) + for the trajectories stored in the buffers. The loss information, along with that from other roll-out + instances, can be passed directly to ``update``. Otherwise, it will simply process the trajectories into a + single data batch that can be passed directly to ``learn``. Defaults to False. + device (str): Identifier for the torch device. The ``policy net`` will be moved to the specified device. If it + is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. """ def __init__( self, @@ -67,7 +73,7 @@ def __init__( policy_net: DiscretePolicyNet, reward_discount: float, grad_iters: int = 1, - buffer_size: int = 10000, + max_trajectory_len: int = 10000, get_loss_on_rollout: bool = False, device: str = None ): @@ -81,10 +87,10 @@ def __init__( self.policy_net = policy_net.to(self.device) self.reward_discount = reward_discount self.grad_iters = grad_iters - self.buffer_size = buffer_size + self.max_trajectory_len = max_trajectory_len self.get_loss_on_rollout = get_loss_on_rollout - self._buffer = defaultdict(lambda: self.Buffer(self.policy_net.input_dim, size=self.buffer_size)) + self._buffer = defaultdict(lambda: self.Buffer(self.policy_net.input_dim, size=self.max_trajectory_len)) def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Return actions and log probabilities for given states.""" From f136e3cc6d3b65f021035f49365bd88d502cca71 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 6 Sep 2021 16:41:05 +0000 Subject: [PATCH 428/482] added improve() interface for RLPolicy for single-thread support --- examples/rl/cim/config.py | 10 +++-- examples/rl/cim/policies.py | 24 ++++++++++++ maro/rl/exploration/__init__.py | 4 +- maro/rl/exploration/strategies.py | 8 ++-- maro/rl/learning/env_sampler.py | 31 ++++++++++++--- maro/rl/learning/learner.py | 21 +++++------ maro/rl/learning/policy_manager.py | 58 ++++++++++++++--------------- maro/rl/modeling/core_model.py | 10 +---- maro/rl/modeling/fc_block.py | 6 +-- maro/rl/policy/ac.py | 15 +++++--- maro/rl/policy/ddpg.py | 10 +++-- maro/rl/policy/dqn.py | 7 +++- maro/rl/policy/pg.py | 11 ++++-- maro/rl/policy/policy.py | 17 ++++----- maro/rl/utils/__init__.py | 9 +---- maro/rl/utils/gradient_averaging.py | 3 +- maro/rl/utils/message_enums.py | 2 +- 17 files changed, 142 insertions(+), 104 deletions(-) diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py index db47df593..eaa84941f 100644 --- a/examples/rl/cim/config.py +++ b/examples/rl/cim/config.py @@ -1,8 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import torch from torch.optim import Adam, RMSprop + env_conf = { "scenario": "cim", "topology": "toy.4p_ssdd_l0.0", @@ -41,7 +43,7 @@ "input_dim": state_dim, "hidden_dims": [256, 128, 64, 32], "output_dim": len(action_shaping_conf["action_space"]), - "activation": "leaky_relu", + "activation": torch.nn.LeakyReLU, "softmax": False, "batch_norm": True, "skip_connection": False, @@ -82,7 +84,7 @@ "input_dim": state_dim, "hidden_dims": [256, 128, 64], "output_dim": len(action_shaping_conf["action_space"]), - "activation": "tanh", + "activation": torch.nn.Tanh, "softmax": True, "batch_norm": False, "head": True @@ -92,7 +94,7 @@ "input_dim": state_dim, "hidden_dims": [256, 128, 64], "output_dim": 1, - "activation": "leaky_relu", + "activation": torch.nn.LeakyReLU, "softmax": False, "batch_norm": True, "head": True @@ -104,7 +106,7 @@ ac_conf = { "reward_discount": .0, "grad_iters": 10, - "critic_loss_cls": "smooth_l1", + "critic_loss_cls": torch.nn.SmoothL1Loss, "min_logp": None, "critic_loss_coeff": 0.1, "entropy_coeff": 0.01, diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index 15b1bb151..1f233fbdb 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -38,6 +38,17 @@ def step(self, loss): loss.backward() self.optim.step() + def get_gradients(self, loss): + self.optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad): + for name, param in self.named_parameters(): + param.grad = grad[name] + + self.optim.step() + class MyACNet(DiscreteACNet): def __init__(self): @@ -61,6 +72,19 @@ def step(self, loss): self.actor_optim.step() self.critic_optim.step() + def get_gradients(self, loss): + self.actor_optim.zero_grad() + self.critic_optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad): + for name, param in self.named_parameters(): + param.grad = grad[name] + + self.actor_optim.step() + self.critic_optim.step() + policy_func_dict = { "dqn": lambda name: DQN(name, QNet(), **dqn_conf), diff --git a/maro/rl/exploration/__init__.py b/maro/rl/exploration/__init__.py index 1d4991bea..053a7eae7 100644 --- a/maro/rl/exploration/__init__.py +++ b/maro/rl/exploration/__init__.py @@ -1,9 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .scheduler import ( - AbsExplorationScheduler, LinearExplorationScheduler, MultiLinearExplorationScheduler -) +from .scheduler import AbsExplorationScheduler, LinearExplorationScheduler, MultiLinearExplorationScheduler from .strategies import eps_greedy, gaussian_noise, uniform_noise __all__ = [ diff --git a/maro/rl/exploration/strategies.py b/maro/rl/exploration/strategies.py index 4e4ea4687..4c3fff7ab 100644 --- a/maro/rl/exploration/strategies.py +++ b/maro/rl/exploration/strategies.py @@ -6,15 +6,13 @@ import numpy as np -def eps_greedy(action: Union[int, np.ndarray], num_actions, *, epsilon: float, state=None): +def eps_greedy(action: Union[int, np.ndarray], num_actions, *, epsilon: float): """epsilon-greedy exploration. Args: action (Union[int, np.ndarray]): Action(s) chosen greedily by the policy. num_actions (int): Number of possible actions. - epsilon (float): The probability that a random action will be used - state: State information which might be needed as context to generate an exploratory action. - In this simple epsilon-greedy scheme, it is not used. Defaults to None. + epsilon (float): The probability that a random action will be selected. """ def get_exploration_action(action): return action if np.random.random() > epsilon else np.random.randint(num_actions) @@ -33,7 +31,7 @@ def uniform_noise( min_action: Union[float, list, np.ndarray] = None, max_action: Union[float, list, np.ndarray] = None, ) -> Union[float, np.ndarray]: - if min_action is None and max_action is None: + if min_action is None and max_action is None: return action + np.random.uniform(low, high) else: return np.clip(action + np.random.uniform(low, high), min_action, max_action) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 90287dcd1..e3f13996a 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -41,7 +41,7 @@ def __init__( self.exploration_scheduler = { policy_id: sch_cls(self.policy_dict[policy_id].exploration_params, param_name, **sch_params) for policy_id, opt_dict in exploration_scheduler_option.items() - for param_name, (sch_cls, sch_params) in opt_dict.items() + for param_name, (sch_cls, sch_params) in opt_dict.items() if policy_id in self.policy_dict } @@ -83,6 +83,8 @@ def _inference_service(name, get_policy, conn, exploration_scheduler_option): policy.update(msg["loss_info"]) elif msg["type"] == "learn": policy.learn(msg["batch"]) + elif msg["type"] == "improve": + policy.improve() for policy_id in policies_to_parallelize: conn1, conn2 = Pipe() @@ -102,13 +104,13 @@ def choose_action(self, state_by_agent: Dict[str, np.ndarray]): states_by_policy[self.agent2policy[agent]].append(state) agents_by_policy[self.agent2policy[agent]].append(agent) - # send state batch to inference processes for parallelized inference + # send state batch to inference processes for parallelized inference. for policy_id, conn in self._conn.items(): if states_by_policy[policy_id]: conn.send({"type": "choose_action", "states": np.vstack(states_by_policy[policy_id])}) action_by_agent = {} - # compute the actions for local policies first while the inferences processes do their work + # compute the actions for local policies first while the inferences processes do their work. for policy_id, policy in self.policy_dict.items(): if states_by_policy[policy_id]: action_by_agent.update( @@ -182,6 +184,13 @@ def record_transition(self, agent: str, state, action, reward, next_state, termi "next_state": next_state, "terminal": terminal }) + def improve(self): + for conn in self._conn.values(): + conn.send({"type": "improve"}) + + for policy in self.policy_dict.values(): + policy.improve() + class AbsEnvSampler(ABC): """Simulation data collector and policy evaluator. @@ -269,7 +278,13 @@ def get_reward(self, actions: list, tick: int): """ raise NotImplementedError - def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploration_step: bool = False): + def sample( + self, + policy_state_dict: dict = None, + num_steps: int = -1, + exploration_step: bool = False, + return_rollout_info: bool = True + ): self.env = self._learn_env # set policy states if policy_state_dict: @@ -320,14 +335,18 @@ def sample(self, policy_state_dict: dict = None, num_steps: int = -1, exploratio not cache and self._terminal ) - return { - "rollout_info": self.agent_wrapper.get_rollout_info(), + result = { "step_range": (starting_step_index, self._step_index), "tracker": self.tracker, "end_of_episode": self._terminal, "exploration_params": self.agent_wrapper.get_exploration_params() } + if return_rollout_info: + result["rollout_info"] = self.agent_wrapper.get_rollout_info() + + return result + def test(self, policy_state_dict: dict = None): self.env = self._test_env # set policy states diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py index 3065803ab..81d7df89f 100644 --- a/maro/rl/learning/learner.py +++ b/maro/rl/learning/learner.py @@ -70,19 +70,17 @@ def simple_learner( logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") eval_point_index = 0 - def collect_and_update(ep): + def collect_and_update(ep, exploration_step): """Collect simulation data for training.""" - segment, exploration_step = 1, True + segment = 1 while True: - result = env_sampler.sample(num_steps=num_steps, exploration_step=exploration_step) + result = env_sampler.sample( + num_steps=num_steps, exploration_step=exploration_step, return_rollout_info=False + ) logger.info( get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) ) - for policy_name, info in result["rollout_info"].items(): - if "grad" in info: - env_sampler.policy_dict[policy_name].update([info]) - else: - env_sampler.policy_dict[policy_name].learn([info]) + env_sampler.agent_wrapper.improve() if post_collect: post_collect([result["tracker"]], ep, segment) @@ -91,15 +89,16 @@ def collect_and_update(ep): break segment += 1 - exploration_step = False + exploration_step = False for ep in range(1, num_episodes + 1): - collect_and_update(ep) + collect_and_update(ep, exploration_step) + exploration_step = True if ep == eval_schedule[eval_point_index]: eval_point_index += 1 tracker = env_sampler.test() if post_evaluate: - post_evaluate([tracker]) + post_evaluate([tracker], eval_point_index) # early stopping check if early_stopper: early_stopper.push(env_sampler.test_env.summary) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 38f3edd71..77135a510 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -106,7 +106,7 @@ def __init__( log_dir: str = getcwd() ): super().__init__() - self._policy_names = list(create_policy_func_dict.keys()) + self._policy_ids = list(create_policy_func_dict.keys()) self._parallel = parallel self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) if parallel: @@ -140,11 +140,11 @@ def _policy_host(name, create_policy_func, conn): self._policy_hosts.append(host) host.start() - for policy_name, conn in self._manager_end.items(): + for policy_id, conn in self._manager_end.items(): msg = conn.recv() if msg["type"] == "init": - self._state_cache[policy_name] = msg["policy_state"] - self._logger.info(f"Initial state for policy {policy_name} cached") + self._state_cache[policy_id] = msg["policy_state"] + self._logger.info(f"Initial state for policy {policy_id} cached") else: self._logger.info("Creating policy instances locally") self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} @@ -159,19 +159,19 @@ def update(self, rollout_info: Dict[str, list]): """ t0 = time.time() if self._parallel: - for policy_name, info_list in rollout_info.items(): - self._manager_end[policy_name].send({"type": "learn", "rollout_info": info_list}) - for policy_name, conn in self._manager_end.items(): + for policy_id, info_list in rollout_info.items(): + self._manager_end[policy_id].send({"type": "learn", "rollout_info": info_list}) + for policy_id, conn in self._manager_end.items(): msg = conn.recv() if msg["type"] == "learn_done": - self._state_cache[policy_name] = msg["policy_state"] - self._logger.info(f"Cached state for policy {policy_name}") + self._state_cache[policy_id] = msg["policy_state"] + self._logger.info(f"Cached state for policy {policy_id}") else: - for policy_name, info in rollout_info.items(): + for policy_id, info in rollout_info.items(): if isinstance(info, list): - self._policy_dict[policy_name].update(info) + self._policy_dict[policy_id].update(info) else: - self._policy_dict[policy_name].learn(info) + self._policy_dict[policy_id].learn(info) self._logger.info(f"Updated policies {list(rollout_info.keys())}") self._version += 1 @@ -199,7 +199,7 @@ class DistributedPolicyManager(AbsPolicyManager): """Policy manager that communicates with a set of remote nodes that house the policy instances. Args: - policy_names (List[str]): Names of the registered policies. + policy_ids (List[str]): Names of the registered policies. group (str): Group name for the cluster consisting of the manager and all policy hosts. num_hosts (int): Number of hosts. The hosts will be identified by "POLICY_HOST.i", where 0 <= i < num_hosts. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init @@ -210,14 +210,14 @@ class DistributedPolicyManager(AbsPolicyManager): """ def __init__( self, - policy_names: List[str], + policy_ids: List[str], group: str, num_hosts: int, log_dir: str = getcwd(), proxy_kwargs: dict = {} ): super().__init__() - self._policy_names = policy_names + self._policy_ids = policy_ids peers = {"policy_host": num_hosts} self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) @@ -226,7 +226,7 @@ def __init__( self._host2policies = defaultdict(list) # assign policies to hosts - for i, name in enumerate(self._policy_names): + for i, name in enumerate(self._policy_ids): host_id = i % num_hosts self._policy2host[name] = f"POLICY_HOST.{host_id}" self._host2policies[f"POLICY_HOST.{host_id}"].append(name) @@ -234,18 +234,18 @@ def __init__( self._logger.info(f"Policy assignment: {self._policy2host}") # ask the hosts to initialize the assigned policies - for host_name, policy_names in self._host2policies.items(): + for host_name, policy_ids in self._host2policies.items(): self._proxy.isend(SessionMessage( - MsgTag.INIT_POLICIES, self._proxy.name, host_name, body={MsgKey.POLICY_NAMES: policy_names} + MsgTag.INIT_POLICIES, self._proxy.name, host_name, body={MsgKey.POLICY_IDS: policy_ids} )) # cache the initial policy states self._state_cache, dones = {}, 0 for msg in self._proxy.receive(): if msg.tag == MsgTag.INIT_POLICIES_DONE: - for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): - self._state_cache[policy_name] = policy_state - self._logger.info(f"Cached state for policy {policy_name}") + for policy_id, policy_state in msg.body[MsgKey.POLICY_STATE].items(): + self._state_cache[policy_id] = policy_state + self._logger.info(f"Cached state for policy {policy_id}") dones += 1 if dones == num_hosts: break @@ -259,17 +259,17 @@ def update(self, rollout_info: Dict[str, list]): information dictionaries computed directly by roll-out workers. """ msg_dict = defaultdict(lambda: defaultdict(dict)) - for policy_name, info in rollout_info.items(): - host_id_str = self._policy2host[policy_name] - msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_name] = info + for policy_id, info in rollout_info.items(): + host_id_str = self._policy2host[policy_id] + msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_id] = info dones = 0 self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_dict.items())) for msg in self._proxy.receive(): if msg.tag == MsgTag.LEARN_DONE: - for policy_name, policy_state in msg.body[MsgKey.POLICY_STATE].items(): - self._state_cache[policy_name] = policy_state - self._logger.info(f"Cached state for policy {policy_name}") + for policy_id, policy_state in msg.body[MsgKey.POLICY_STATE].items(): + self._state_cache[policy_id] = policy_state + self._logger.info(f"Cached state for policy {policy_id}") dones += 1 if dones == len(msg_dict): break @@ -322,10 +322,10 @@ def policy_host( break if msg.tag == MsgTag.INIT_POLICIES: - for name in msg.body[MsgKey.POLICY_NAMES]: + for name in msg.body[MsgKey.POLICY_IDS]: policy_dict[name] = create_policy_func_dict[name](name) - logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_NAMES]}") + logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_IDS]}") proxy.reply( msg, tag=MsgTag.INIT_POLICIES_DONE, diff --git a/maro/rl/modeling/core_model.py b/maro/rl/modeling/core_model.py index 69673b592..3513906fe 100644 --- a/maro/rl/modeling/core_model.py +++ b/maro/rl/modeling/core_model.py @@ -8,13 +8,7 @@ class AbsCoreModel(nn.Module): - """General model abstraction for - - Args: - device (str): Identifier for the torch device. The model instance will be moved to the specified - device. If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - Defaults to None. - """ + """General model abstraction for use in RL algorithms.""" def __init__(self): super().__init__() @@ -32,7 +26,7 @@ def step(self, loss: torch.tensor): raise NotImplementedError def get_gradients(self, loss: torch.tensor): - pass + pass def apply_gradients(self, grad: dict): pass diff --git a/maro/rl/modeling/fc_block.py b/maro/rl/modeling/fc_block.py index c736a643d..2788b4885 100644 --- a/maro/rl/modeling/fc_block.py +++ b/maro/rl/modeling/fc_block.py @@ -7,8 +7,6 @@ import torch import torch.nn as nn -from maro.rl.utils import get_torch_activation_cls - class FullyConnected(nn.Module): """Fully connected network with optional batch normalization, activation and dropout components. @@ -37,7 +35,7 @@ def __init__( input_dim: int, output_dim: int, hidden_dims: List[int], - activation="relu", + activation=nn.ReLU, head: bool = False, softmax: bool = False, batch_norm: bool = False, @@ -52,7 +50,7 @@ def __init__( self._output_dim = output_dim # network features - self._activation = get_torch_activation_cls(activation)() if activation else None + self._activation = activation() if activation else None self._head = head self._softmax = nn.Softmax(dim=1) if softmax else None self._batch_norm = batch_norm diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 0c80087ba..e479702e5 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -9,7 +9,7 @@ from torch.distributions import Categorical from maro.rl.modeling import DiscreteACNet -from maro.rl.utils import discount_cumsum, get_torch_loss_cls +from maro.rl.utils import average_grads, discount_cumsum from .policy import RLPolicy @@ -52,7 +52,7 @@ def get(self): if last > self._prev_ptr: traj_slice = np.arange(self._prev_ptr, last) else: # wrap-around - traj_slice = np.concatenate([np.arange(self._prev_ptr, self.size), np.arange(last)]) + traj_slice = np.concatenate([np.arange(self._prev_ptr, self.size), np.arange(last)]) self._prev_ptr = last return { "states": self.states[traj_slice], @@ -121,13 +121,13 @@ def __init__( self.ac_net = ac_net.to(self.device) self.reward_discount = reward_discount self.grad_iters = grad_iters - self.critic_loss_func = get_torch_loss_cls(critic_loss_cls)() + self.critic_loss_func = critic_loss_cls() self.min_logp = min_logp self.critic_loss_coeff = critic_loss_coeff self.entropy_coeff = entropy_coeff self.clip_ratio = clip_ratio self.lam = lam - self.max_trajectory_len = max_trajectory_len + self.max_trajectory_len = max_trajectory_len self.get_loss_on_rollout = get_loss_on_rollout self._buffer = defaultdict(lambda: self.Buffer(self.ac_net.input_dim, size=self.max_trajectory_len)) @@ -212,7 +212,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): "actor_loss": actor_loss.detach().cpu().numpy(), "critic_loss": critic_loss.detach().cpu().numpy(), "entropy": entropy.detach().cpu().numpy(), - "loss": loss.detach().cpu().numpy() + "loss": loss.detach().cpu().numpy() if explicit_grad else loss } if explicit_grad: loss_info["grad"] = self.ac_net.get_gradients(loss) @@ -221,12 +221,15 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): def update(self, loss_info_list: List[dict]): """Apply gradients to the underlying parameterized model.""" - self.ac_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) + self.ac_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) def learn(self, batch: dict): for _ in range(self.grad_iters): self.ac_net.step(self.get_batch_loss(batch)["loss"]) + def improve(self): + self.learn(self._get_batch()) + def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index abf084059..daeb886f9 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -8,7 +8,7 @@ from maro.rl.exploration import gaussian_noise from maro.rl.modeling import ContinuousACNet -from maro.rl.utils import get_torch_loss_cls +from maro.rl.utils import average_grads from maro.utils import clone from .policy import RLPolicy @@ -84,7 +84,7 @@ def __init__( self.reward_discount = reward_discount self.num_epochs = num_epochs self.update_target_every = update_target_every - self.q_value_loss_func = get_torch_loss_cls(q_value_loss_cls)() + self.q_value_loss_func = q_value_loss_cls() self.q_value_loss_coeff = q_value_loss_coeff self.soft_update_coeff = soft_update_coeff @@ -162,7 +162,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False) -> dict: loss_info = { "policy_loss": policy_loss.detach().cpu().numpy(), "q_loss": q_loss.detach().cpu().numpy(), - "loss": loss.detach().cpu().numpy() + "loss": loss.detach().cpu().numpy() if explicit_grad else loss } if explicit_grad: loss_info["grad"] = self.ac_net.get_gradients(loss) @@ -170,7 +170,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False) -> dict: return loss_info def update(self, loss_info_list: List[dict]): - self.ac_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) + self.ac_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) if self._ac_net_version - self._target_ac_net_version == self.update_target_every: self._update_target() @@ -178,7 +178,9 @@ def learn(self, batch: dict): self._replay_memory.put( batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] ) + self.improve() + def improve(self): for _ in range(self.num_epochs): train_batch = self._replay_memory.sample(self.train_batch_size) self.ac_net.step(self.get_batch_loss(train_batch)["loss"]) diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index d1c6cda9d..dfb453375 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -8,6 +8,7 @@ from maro.rl.exploration import eps_greedy from maro.rl.modeling import DiscreteQNet +from maro.rl.utils import average_grads from maro.utils import clone from .policy import RLPolicy @@ -294,7 +295,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): else: loss = self._loss_func(q_values, target_q_values) - loss_info["loss"] = loss + loss_info["loss"] = loss.detach().cpu().numpy() if explicit_grad else loss if explicit_grad: loss_info["grad"] = self.q_net.get_gradients(loss) return loss_info @@ -304,7 +305,7 @@ def update(self, loss_info_list: List[dict]): for loss_info in loss_info_list: self._per.update(loss_info["indexes"], loss_info["td_errors"]) - self.q_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) + self.q_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) self._q_net_version += 1 if self._q_net_version - self._target_q_net_version == self.update_target_every: self._update_target() @@ -313,7 +314,9 @@ def learn(self, batch: dict): self._replay_memory.put( batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] ) + self.improve() + def improve(self): for _ in range(self.num_epochs): loss_info = self.get_batch_loss(self._get_batch()) if self.prioritized_replay: diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index b3fa53238..063dea68c 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -8,7 +8,7 @@ import torch from maro.rl.modeling import DiscretePolicyNet -from maro.rl.utils import discount_cumsum +from maro.rl.utils import average_grads, discount_cumsum from .policy import RLPolicy @@ -112,7 +112,7 @@ def record( self._buffer[key].put(state, action, reward, terminal) def get_rollout_info(self): - if self._get_loss_on_rollout_finish: + if self.get_loss_on_rollout: return self.get_batch_loss(self._get_batch(), explicit_grad=True) else: return self._get_batch() @@ -139,19 +139,22 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): _, logp = self.policy_net(batch["states"]) loss = -(logp * returns).mean() - loss_info = {"loss": loss.detach().cpu().numpy()} + loss_info = {"loss": loss.detach().cpu().numpy() if explicit_grad else loss} if explicit_grad: loss_info["grad"] = self.policy_net.get_gradients(loss) return loss_info def update(self, loss_info_list: List[dict]): """Apply gradients to the underlying parameterized model.""" - self.policy_net.apply_gradients([loss_info["grad"] for loss_info in loss_info_list]) + self.policy_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) def learn(self, batch: dict): for _ in range(self.grad_iters): self.policy_net.step(self.get_batch_loss(batch)["grad"]) + def improve(self): + self.learn(self._get_batch()) + def set_state(self, policy_state): self.policy_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 59ddb34b0..a20484f1d 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -54,22 +54,21 @@ def choose_action(self, state): def record(self, key: str, state, action, reward, next_state, terminal: bool): pass - @abstractmethod def get_rollout_info(self): - raise NotImplementedError + pass - @abstractmethod def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - raise NotImplementedError + pass - @abstractmethod def update(self, loss_info_list: List[dict]): pass - @abstractmethod - def learn(self, trajectories: List[dict]): - """Perform policy improvement based on a list of trajectories obtained from parallel rollouts.""" - raise NotImplementedError + def learn(self, batch: dict): + """Perform policy improvement based on a single data batch collected from one or more roll-out instances.""" + pass + + def improve(self): + pass def exploit(self): self.greedy = True diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 70f9cef67..8f21f4c52 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,13 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from .gradient_averaging import average_grads from .message_enums import MsgKey, MsgTag -from .torch_cls_index import ( - get_torch_activation_cls, get_torch_loss_cls, get_torch_lr_scheduler_cls, get_torch_optim_cls -) from .trajectory_computation import discount_cumsum -__all__ = [ - "MsgKey", "MsgTag", "discount_cumsum", "get_torch_activation_cls", "get_torch_loss_cls", - "get_torch_lr_scheduler_cls", "get_torch_optim_cls" -] +__all__ = ["MsgKey", "MsgTag", "average_grads", "discount_cumsum"] diff --git a/maro/rl/utils/gradient_averaging.py b/maro/rl/utils/gradient_averaging.py index 5c81e2d5f..5b404cae9 100644 --- a/maro/rl/utils/gradient_averaging.py +++ b/maro/rl/utils/gradient_averaging.py @@ -5,7 +5,8 @@ import torch -def average_grad(grad_list: List[str, dict]): + +def average_grads(grad_list: List[dict]): return { param_name: torch.mean(torch.stack([grad[param_name] for grad in grad_list]), dim=0) for param_name in grad_list[0] diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 7dc73ce5d..49302e681 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -31,7 +31,7 @@ class MsgKey(Enum): SEGMENT = "segment" NUM_STEPS = "num_steps" STEP = "step" - POLICY_NAMES = "policy_names" + POLICY_IDS = "policy_ids" ROLLOUT_INFO = "rollout_info" TRACKER = "tracker" GRAD_TASK = "grad_task" From 92561f6092d477fc0afe45945dab3a07c09071ba Mon Sep 17 00:00:00 2001 From: yaqiu Date: Tue, 7 Sep 2021 08:29:26 +0000 Subject: [PATCH 429/482] fixed simple policy manager bug --- maro/rl/learning/policy_manager.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 77135a510..8f37b9d29 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -121,14 +121,11 @@ def _policy_host(name, create_policy_func, conn): while True: msg = conn.recv() if msg["type"] == "learn": - info_list = msg["rollout_info"] - if not isinstance(info_list, list): - info_list = [info_list] - if "loss" in info_list[0]: - policy.update(info_list) + info = msg["rollout_info"] + if isinstance(info, list): + policy.update(info) else: - policy.learn(info_list) - + policy.learn(info) conn.send({"type": "learn_done", "policy_state": policy.get_state()}) elif msg["type"] == "quit": break @@ -159,8 +156,8 @@ def update(self, rollout_info: Dict[str, list]): """ t0 = time.time() if self._parallel: - for policy_id, info_list in rollout_info.items(): - self._manager_end[policy_id].send({"type": "learn", "rollout_info": info_list}) + for policy_id, info in rollout_info.items(): + self._manager_end[policy_id].send({"type": "learn", "rollout_info": info}) for policy_id, conn in self._manager_end.items(): msg = conn.recv() if msg["type"] == "learn_done": From 013d0fb4d084adb390c6e9618df0a891cf46afb3 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sat, 11 Sep 2021 07:57:42 +0000 Subject: [PATCH 430/482] updated doc, rst, notebook --- docs/source/key_components/rl_toolkit.rst | 192 +++++------- examples/rl/cim/config.py | 23 +- examples/rl/cim/env_sampler.py | 17 +- examples/rl/cim/policies.py | 7 +- examples/rl/workflows/config.yml | 10 +- maro/rl/exploration/__init__.py | 6 +- .../{scheduler.py => scheduling.py} | 32 +- maro/rl/exploration/strategies.py | 60 +++- maro/rl/learning/env_sampler.py | 100 +++--- maro/rl/learning/rollout_manager.py | 32 +- maro/rl/modeling/specials.py | 42 +-- maro/rl/policy/ac.py | 2 +- maro/rl/policy/ddpg.py | 54 +++- maro/rl/policy/dqn.py | 61 ++-- maro/rl/policy/pg.py | 5 +- maro/rl/policy/policy.py | 79 ++++- .../rl_formulation.ipynb | 293 +++++++++--------- 17 files changed, 537 insertions(+), 478 deletions(-) rename maro/rl/exploration/{scheduler.py => scheduling.py} (89%) diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 175cf032d..89d5bd8d1 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -10,17 +10,16 @@ distributed learning workflows. The distributed workflow can be synchronous or a Synchronous Learning ==================== -In synchronous mode, a central controler executes learning cycles that consist of simulation data collection and -policy update. In a strictly synchronous learning process, the coordinator would wait for all data collectors, +In synchronous mode, a central controller executes learning cycles that consist of simulation data collection and +policy update. In a strictly synchronous learning process, a roll-out manager waits for all data collectors, a.k.a. "roll-out workers", to return their results before moving on to the policy update phase. So what if a slow -worker drags the whole process down? We provide users the flexibility to loosen the restriction by specifying the -minimum number of workers required to receive from before proceeding to the next phase. If one is concerned about -losing precious data in the case of all workers reporting back at roughly the same time, we also provide the option -to continue to receive after receiving the minimum number of results, but with a timeout to keep the wait time -upperbounded. Note that the transition from the policy update phase to the data collection phase is still strictly -synchronous. This means that in the case of the policy instances distributed amongst a set of trainer nodes, the -central controller waits until all trainers report back with the latest policy states before starting the next -cycle. +worker drags the whole process down? We provide the flexibility to loosen the restriction by specifying the +number of workers required to report back before proceeding to the next phase. After the required number of workers +report back, the roll-out manager can optionally try to receive from the remaining workers a certain number of times +but with a timeout to keep the wait time upperbounded. Note that the transition from the policy update phase to the +data collection phase is always synchronous. This means that in the case of the policy instances distributed amongst +a set of trainer nodes, the central controller waits until all trainers report back with the latest policy states before +starting the next cycle. The components required for synchronous learning include: @@ -57,67 +56,68 @@ The components required for asynchronous learning include: * policy server, which receives data from the actors and update the policy states when necessary. -Environment Wrapper +Environment Sampler ------------------- -To use the training components described above, it is necessary to implement an environment wrapper for the environment of -your choice. An environment wrapper serves as a bridge between a simulator and the policies that interact with it by providing -unified interfaces to the interaction workflow. It is also responsible for caching transitions and preparing experiences for -training. Key methods that need to be implemented for an environment wrapper include: - -* ``get_state``, which encodes agents' observations into policy input. The encoded state for each agent must correspond - to the policy used by the agent. -* ``to_env_action``, which provides model output with context so that it can be executed by the environment simulator. -* ``get_reward``, for evaluating rewards. - -.. image:: ../images/rl/env_wrapper.svg - :target: ../images/rl/env_wrapper.svg - :alt: Environment Wrapper +It is necessary to implement an environment sampler (a subclass of ``AbsEnvSampler``) with user-defined state, action +and reward shaping to collect roll-out information for learning and testing purposes. An environment sampler can be +easily turned into a roll-out worker or an actor for synchronous and asynchronous learning, respectively. Policy ------ -A policy is a an agent's mechanism to choose actions based on its observations of the environment. -Accordingly, the abstract ``AbsPolicy`` class exposes a ``choose_action`` interface. This abstraction encompasses -both static policies, such as rule-based policies, and updatable policies, such as RL policies. The latter is -abstracted through the ``AbsCorePolicy`` sub-class which also exposes a ``update`` interface. By default, updatable -policies require an experience manager to store and retrieve simulation data (in the form of "experiences sets") -based on which updates can be made. +The ``AbsPolicy`` abstraction is the core component of MARO's RL toolkit. A policy is a an agent's mechanism to select +actions in its interaction with an environment. MARO allows arbitrary agent-to-policy mapping to make policy sharing +easily configurable. It is also possible to use a mixture of rule-based policies and ``RLPolicy`` instances. The latter +provides various policy improvement interfaces to support single-threaded and distributed learning. .. code-block:: python class AbsPolicy(ABC): @abstractmethod - def choose_action(self, state): + def __call__(self, state): + """Select an action based on a state.""" raise NotImplementedError - class AbsCorePolicy(AbsPolicy): - def __init__(self, experience_memory: ExperienceMemory): - super().__init__() - self.experience_memory = experience_memory + class RLPolicy(AbsPolicy): + ... - @abstractmethod - def update(self): - raise NotImplementedError + def record(self, key: str, state, action, reward, next_state, terminal: bool): + pass + + def get_rollout_info(self): + pass + + def get_batch_loss(self, batch: dict, explicit_grad: bool = False): + pass + + def update(self, loss_info_list: List[dict]): + pass + + def learn(self, batch: dict): + pass + + def improve(self): + pass Policy Manager -------------- -A policy manager is an abstraction that controls policy update. It houses the latest policy states. -In synchrounous learning, the policy manager controls the policy update phase of a learning cycle. -In asynchrounous learning, the policy manager is the centerpiece of the policy server process. -Individual policy updates, however, may or may not occur within the policy manager itself depending -on the policy manager type used. The provided policy manager classes include: +A policy manager controls policy update and provides the latest policy states for roll-outs. In synchrounous learning, +the policy manager controls the policy update phase of a learning cycle. In asynchrounous learning, the policy manager +is present as a server process. The types of policy manager include: -* ``LocalPolicyManager``, where the policies are updated within the manager itself; -* ``MultiProcessPolicyManager``, which distributes policies amongst a set of trainer processes to parallelize - policy update; -* ``MultiNodePolicyManager``, which distributes policies amongst a set of remote compute nodes to parallelize - policy update; +* ``SimplePolicyManager``, where all policies instances reside within the manager and are updated sequentially; +* ``MultiProcessPolicyManager``, where each policy is placed in a dedicated process that runs event loops to receive + roll-out information for update; this approach takes advantage of the parallelism from Python's multi-processing, so + the policies can be updated in parallel, but comes with inter-process communication overhead; +* ``DistributedPolicyManager``, where policies are distributed among a set of remote compute nodes that run event loops + to reeeive roll-out information for update. This approach allows the policies to be updated in parallel and may be + necessary when the combined size of the policies is too big to fit in a single node. .. image:: ../images/rl/policy_manager.svg @@ -128,78 +128,56 @@ on the policy manager type used. The provided policy manager classes include: Core Model ---------- -In the deep reinforcement learning (DRL) world, a core policy usually includes one or more neural-network-based models, -which may be used to compute action preferences or estimate state / action values. The core model abstraction is designed -to decouple the the inner workings of these models from the algorithmic aspects of the policy that uses them. For example, -the actor-critic algorithm does not need to concern itself with the structures and optimizing schemes of the actor and -critic models. The ``AbsCoreModel`` abstraction represents a collection of network components with embedded optimizers. -Subclasses of ``AbsCoreModel`` provided for use with specific RL algorithms include ``DiscreteQNet`` for DQN, ``DiscretePolicyNet`` +In the deep reinforcement learning (DRL) world, a policy usually includes one or more neural-network com-based models, +which may be used to compute action preferences or estimate state / action values. The ``AbsCoreModel`` represents a +collection of network components with embedded optimizers and exposes unified interfaces to decouple model inference +and optimization from the algorithmic aspects of the policy that uses them. For example, the actor-critic algorithm +does not need to concern itself with how the action probabilities and state values are computed. Subclasses of +``AbsCoreModel`` provided for use with specific RL algorithms include ``DiscreteQNet`` for DQN, ``DiscretePolicyNet`` for Policy Gradient, ``DiscreteACNet`` for Actor-Critic and ``ContinuousACNet`` for DDPG. The code snippet below shows how to create a model for the actor-critic algorithm with a shared bottom stack: .. code-block:: python - class MyACModel(DiscreteACNet): - def forward(self, states, actor=True, critic=True): - features = self.component["representation"](states) - return ( - self.component["actor"](features) if actor else None, - self.component["critic"](features) if critic else None - ) - - - representation_stack = FullyConnected(...) - actor_head = FullyConnected(...) - critic_head = FullyConnected(...) - ac_model = SimpleMultiHeadModel( - {"representation": representation_stack, "actor": actor_head, "critic": critic_head}, - optim_option={ - "representation": OptimizerOption(cls="adam", params={"lr": 0.0001}), - "actor": OptimizerOption(cls="adam", params={"lr": 0.001}), - "critic": OptimizerOption(cls="rmsprop", params={"lr": 0.0001}) - } - ) + shared_net_conf = {...} + actor_net_conf = {...} + critic_net_conf = {...} + shared_optim_conf = {torch.optim.SGD, {"lr": 0.0001}} + actor_optim_conf = (torch.optim.Adam, {"lr": 0.001}) + critic_optim_conf = (torch.optim.RMSprop, {"lr": 0.001}) + + class MyACNet(DiscreteACNet): + def __init__(self): + super().__init__() + self.shared = FullyConnected(**shared_net_conf) + self.actor = FullyConnected(**actor_net_conf) + self.critic = FullyConnected(**critic_net_conf) + self.shared_optim = shared_optim_conf[0](self.shared.parameters(), **shared_optim_conf[1]) + self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1]) + self.critic_optim = critic_optim_conf[0](self.critic.parameters(), **critic_optim_conf[1]) + + def forward(self, states, actor: bool = True, critic: bool = True): + representation = self.shared(states) + return (self.actor(representation) if actor else None), (self.critic(representation) if critic else None) + + def step(self, loss): + self.shared_optim.zero_grad() + self.actor_optim.zero_grad() + self.critic_optim.zero_grad() + loss.backward() + self.hsared_optim.step() + self.actor_optim.step() + self.critic_optim.step() To generate stochastic actions given a batch of states, call ``get_action`` on the model instance: .. code-block:: python - action, log_p = ac_model.get_action(state) + action, log_p, values = ac_model.get_action(state) -To performing a single gradient step on the model, call the ``step`` function: +To performing a single gradient step on the model, pass the loss to the ``step`` function: .. code-block:: python ac_model.step(critic_loss + actor_loss) - -Here it is assumed that the losses have been computed using the same model instance and the gradients have -been generated for the internal components. - - -Experience ----------- - -An ``ExperienceSet`` is a synonym for training data for RL policies. The data originate from the simulator and -get processed and organized into a set of transitions in the form of (state, action, reward, next_state, info), -where ''info'' contains information about the transition that is not encoded in the state but may be necessary -for sampling purposes. An ``ExperienceMemory`` is a storage facility for experience sets and is maintained by -a policy for storing and retrieving training data. Sampling from the experience memory can be customized by -registering a user-defined sampler to it. - - -Exploration ------------ - -Some RL algorithms such as DQN and DDPG require explicit exploration governed by a set of parameters. The -``AbsExploration`` class is designed to cater to these needs. Simple exploration schemes, such as ``EpsilonGreedyExploration`` for discrete action space -and ``UniformNoiseExploration`` and ``GaussianNoiseExploration`` for continuous action space, are provided in -the toolkit. - -As an example, the exploration for DQN may be carried out with the aid of an ``EpsilonGreedyExploration``: - -.. code-block:: python - - exploration = EpsilonGreedyExploration(num_actions=10) - greedy_action = q_net.get_action(state) - exploration_action = exploration(greedy_action) diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py index eaa84941f..f0a726a88 100644 --- a/examples/rl/cim/config.py +++ b/examples/rl/cim/config.py @@ -4,6 +4,8 @@ import torch from torch.optim import Adam, RMSprop +from maro.rl.exploration import MultiLinearExplorationScheduler, epsilon_greedy + env_conf = { "scenario": "cim", @@ -59,8 +61,18 @@ "num_epochs": 10, "soft_update_coeff": 0.1, "double": False, + "exploration_strategy": (epsilon_greedy, {"epsilon": 0.4}), + "exploration_scheduling_options": [( + "epsilon", MultiLinearExplorationScheduler, { + "splits": [(2, 0.32)], + "initial_value": 0.4, + "last_ep": 5, + "final_value": 0.0, + } + )], "replay_memory_capacity": 10000, "random_overwrite": False, + "warmup": 100, "rollout_batch_size": 128, "train_batch_size": 32, # "prioritized_replay_kwargs": { @@ -71,13 +83,6 @@ # } } -exploration_conf = { - "last_ep": 10, - "initial_value": 0.4, - "final_value": 0.0, - "splits": [(5, 0.32)] -} - # AC settings actor_net_conf = { @@ -111,6 +116,6 @@ "critic_loss_coeff": 0.1, "entropy_coeff": 0.01, # "clip_ratio": 0.8 # for PPO - "lam": 0.9, - "get_loss_on_rollout": True + "lam": .0, + "get_loss_on_rollout": False } diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index f3305c401..2890c5c5f 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -6,7 +6,6 @@ import numpy as np -from maro.rl.exploration import MultiLinearExplorationScheduler from maro.rl.learning import AbsEnvSampler from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType @@ -17,8 +16,7 @@ from callbacks import post_step from config import ( - action_shaping_conf, env_conf, exploration_conf, port_attributes, reward_shaping_conf, state_shaping_conf, - vessel_attributes + action_shaping_conf, env_conf, port_attributes, reward_shaping_conf, state_shaping_conf, vessel_attributes ) from policies import policy_func_dict @@ -82,21 +80,12 @@ def get_reward(self, actions, tick): return {agent_id: reward for agent_id, reward in zip(ports, rewards)} -agent2policy = { - 0: "ac", - 1: "ac", - 2: "dqn", - 3: "ac" -} - - def get_env_sampler(): return CIMEnvSampler( get_env=lambda: Env(**env_conf), get_policy_func_dict=policy_func_dict, - exploration_scheduler_option={"dqn": {"epsilon": (MultiLinearExplorationScheduler, exploration_conf)}}, - agent2policy=agent2policy, + agent2policy={agent: f"ac.{agent}" for agent in Env(**env_conf).agent_idx_list}, reward_eval_delay=reward_shaping_conf["time_window"], post_step=post_step, - policies_to_parallelize=["ac", "dqn"] + policies_to_parallelize=[] ) diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index 1f233fbdb..93ccc7459 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -62,6 +62,10 @@ def __init__(self): def input_dim(self): return state_dim + @property + def num_actions(self): + return q_net_conf["output_dim"] + def forward(self, states, actor: bool = True, critic: bool = True): return (self.actor(states) if actor else None), (self.critic(states) if critic else None) @@ -87,6 +91,5 @@ def apply_gradients(self, grad): policy_func_dict = { - "dqn": lambda name: DQN(name, QNet(), **dqn_conf), - "ac": lambda name: ActorCritic(name, MyACNet(), **ac_conf) + f"ac.{i}": lambda name: ActorCritic(name, MyACNet(), **ac_conf) for i in range(4) } diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 61caf604a..f88293441 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -4,8 +4,8 @@ job: cim scenario: cim mode: sync -num_episodes: 10 -eval_schedule: 10 +num_episodes: 5 +eval_schedule: 5 num_steps: -1 max_lag: 0 sync: @@ -15,10 +15,10 @@ sync: eval_parallelism: 1 distributed: group: rollout - num_workers: 4 + num_workers: 3 num_eval_workers: 1 - min_finished_workers: 3 - max_extra_recv_tries: 2 + min_finished_workers: 2 + max_extra_recv_tries: 1 extra_recv_timeout: 200 async: group: async diff --git a/maro/rl/exploration/__init__.py b/maro/rl/exploration/__init__.py index 053a7eae7..8ee78737c 100644 --- a/maro/rl/exploration/__init__.py +++ b/maro/rl/exploration/__init__.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .scheduler import AbsExplorationScheduler, LinearExplorationScheduler, MultiLinearExplorationScheduler -from .strategies import eps_greedy, gaussian_noise, uniform_noise +from .scheduling import AbsExplorationScheduler, LinearExplorationScheduler, MultiLinearExplorationScheduler +from .strategies import epsilon_greedy, gaussian_noise, uniform_noise __all__ = [ "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiLinearExplorationScheduler", - "eps_greedy", "gaussian_noise", "uniform_noise" + "epsilon_greedy", "gaussian_noise", "uniform_noise" ] diff --git a/maro/rl/exploration/scheduler.py b/maro/rl/exploration/scheduling.py similarity index 89% rename from maro/rl/exploration/scheduler.py rename to maro/rl/exploration/scheduling.py index f58608125..08438bc22 100644 --- a/maro/rl/exploration/scheduler.py +++ b/maro/rl/exploration/scheduling.py @@ -8,22 +8,18 @@ class AbsExplorationScheduler(ABC): """Abstract exploration scheduler. - Each exploration scheduler is registered to a single parameter of an ``RLPolicy`` instance. - Args: exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. - last_ep (int): Last episode. initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is instantiated with will be used as the initial value. Defaults to None. """ - def __init__(self, exploration_params: dict, param_name: str, last_ep: int, initial_value=None): + def __init__(self, exploration_params: dict, param_name: str, initial_value=None): super().__init__() self._exploration_params = exploration_params self.param_name = param_name - self.last_ep = last_ep if initial_value is not None: self._exploration_params[self.param_name] = initial_value @@ -37,13 +33,13 @@ def step(self): class LinearExplorationScheduler(AbsExplorationScheduler): """Linear exploration parameter schedule. - Args: exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. + start_ep (int): starting episode. Defaults to 1. initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is instantiated with will be used as the initial value. Defaults to None. """ @@ -52,14 +48,16 @@ def __init__( self, exploration_params: dict, param_name: str, + *, last_ep: int, final_value: float, + start_ep: int = 1, initial_value: float = None, ): - super().__init__(exploration_params, param_name, last_ep, initial_value=initial_value) + super().__init__(exploration_params, param_name, initial_value=initial_value) self.final_value = final_value - if self.last_ep > 1: - self.delta = (self.final_value - self._exploration_params[self.param_name]) / (self.last_ep - 1) + if last_ep > 1: + self.delta = (self.final_value - self._exploration_params[self.param_name]) / (last_ep - start_ep) else: self.delta = 0 @@ -72,17 +70,17 @@ def step(self): class MultiLinearExplorationScheduler(AbsExplorationScheduler): """Exploration parameter schedule that consists of multiple linear phases. - Args: exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. - last_ep (int): Last episode. splits (List[Tuple[int, float]]): List of points that separate adjacent linear phases. Each point is a (episode, parameter_value) tuple that indicates the end of one linear phase and the start of another. These points do not have to be given in any particular order. There cannot be two points with the same first element (episode), or a ``ValueError`` will be raised. + last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. + start_ep (int): starting episode. Defaults to 1. initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is instantiated with will be used as the initial value. Defaults to None. @@ -93,24 +91,26 @@ def __init__( self, exploration_params: dict, param_name: str, - last_ep: int, + *, splits: List[Tuple[int, float]], + last_ep: int, final_value: float, + start_ep: int = 1, initial_value: float = None ): # validate splits - splits = [(1, initial_value)] + splits + [(last_ep, final_value)] + splits = [(start_ep, initial_value)] + splits + [(last_ep, final_value)] splits.sort() for (ep, _), (ep2, _) in zip(splits, splits[1:]): if ep == ep2: raise ValueError("The zeroth element of split points must be unique") - super().__init__(exploration_params, param_name, last_ep, initial_value=initial_value) + super().__init__(exploration_params, param_name, initial_value=initial_value) self.final_value = final_value self._splits = splits - self._ep = 1 + self._ep = start_ep self._split_index = 1 - self._delta = (self._splits[1][1] - self._exploration_params[self.param_name]) / (self._splits[1][0] - 1) + self._delta = (self._splits[1][1] - self._exploration_params[self.param_name]) / (self._splits[1][0] - start_ep) def step(self): if self._split_index == len(self._splits): diff --git a/maro/rl/exploration/strategies.py b/maro/rl/exploration/strategies.py index 4c3fff7ab..ba1620c9a 100644 --- a/maro/rl/exploration/strategies.py +++ b/maro/rl/exploration/strategies.py @@ -6,31 +6,41 @@ import numpy as np -def eps_greedy(action: Union[int, np.ndarray], num_actions, *, epsilon: float): +def epsilon_greedy(state: np.ndarray, action: np.ndarray, num_actions, *, epsilon: float): """epsilon-greedy exploration. Args: - action (Union[int, np.ndarray]): Action(s) chosen greedily by the policy. + state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the vanilla + eps-greedy exploration and is put here to conform to the function signature required for the exploration + strategy parameter for ``DQN``. See ``maro.rl.policy.DQN`` for more details. + action (np.ndarray): Action(s) chosen greedily by the policy. num_actions (int): Number of possible actions. epsilon (float): The probability that a random action will be selected. """ - def get_exploration_action(action): - return action if np.random.random() > epsilon else np.random.randint(num_actions) - - if isinstance(action, np.ndarray): - return np.array([get_exploration_action(act) for act in action]) - else: - return get_exploration_action(action) + return np.array([act if np.random.random() > epsilon else np.random.randint(num_actions) for act in action]) def uniform_noise( - action: Union[float, np.ndarray], - *, - low: Union[float, list, np.ndarray], - high: Union[float, list, np.ndarray], + state: np.ndarray, + action: np.ndarray, min_action: Union[float, list, np.ndarray] = None, max_action: Union[float, list, np.ndarray] = None, + *, + low: Union[float, list, np.ndarray], + high: Union[float, list, np.ndarray] ) -> Union[float, np.ndarray]: + """Apply a uniform noise to a continuous multi-dimensional action. + + Args: + state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the gaussian noise + exploration scheme and is put here to conform to the function signature for the exploration in continuous + action spaces. + action (np.ndarray): Action(s) chosen greedily by the policy. + min_action (Union[float, list, np.ndarray]): Lower bound for the multi-dimensional action space. + max_action (Union[float, list, np.ndarray]): Upper bound for the multi-dimensional action space. + low (Union[float, list, np.ndarray]): Lower bound for the noise range. + high (Union[float, list, np.ndarray]): Upper bound for the noise range. + """ if min_action is None and max_action is None: return action + np.random.uniform(low, high) else: @@ -38,14 +48,30 @@ def uniform_noise( def gaussian_noise( - action: Union[float, np.ndarray], + state: np.ndarray, + action: np.ndarray, + min_action: Union[float, list, np.ndarray] = None, + max_action: Union[float, list, np.ndarray] = None, *, mean: Union[float, list, np.ndarray] = .0, stddev: Union[float, list, np.ndarray] = 1.0, - relative: bool = False, - min_action: Union[float, list, np.ndarray] = None, - max_action: Union[float, list, np.ndarray] = None, + relative: bool = False ) -> Union[float, np.ndarray]: + """Apply a gaussian noise to a continuous multi-dimensional action. + + Args: + state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the gaussian noise + exploration scheme and is put here to conform to the function signature for the exploration in continuous + action spaces. + action (np.ndarray): Action(s) chosen greedily by the policy. + min_action (Union[float, list, np.ndarray]): Lower bound for the multi-dimensional action space. + max_action (Union[float, list, np.ndarray]): Upper bound for the multi-dimensional action space. + mean (Union[float, list, np.ndarray]): Gaussian noise mean. Defaults to .0. + stddev (Union[float, list, np.ndarray]): Standard deviation for the Gaussian noise. Defaults to 1.0. + relative (bool): If True, the generated noise will be multiplied by the action itself before being added to + the action. Defaults to False. + + """ noise = np.random.normal(loc=mean, scale=stddev) if min_action is None and max_action is None: return action + ((noise * action) if relative else noise) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index e3f13996a..d80f30a78 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -13,7 +13,7 @@ from maro.rl.policy import RLPolicy from maro.rl.utils import MsgKey, MsgTag from maro.simulator import Env -from maro.utils import Logger +from maro.utils import Logger, clone from .helpers import get_rollout_finish_msg @@ -24,7 +24,6 @@ def __init__( self, get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str], - exploration_scheduler_option: Dict[str, Dict[str, tuple]], policies_to_parallelize: List[str] = [] ): self._policies_to_parallelize = set(policies_to_parallelize) @@ -38,39 +37,28 @@ def __init__( if policy_id in self.policy_dict } - self.exploration_scheduler = { - policy_id: sch_cls(self.policy_dict[policy_id].exploration_params, param_name, **sch_params) - for policy_id, opt_dict in exploration_scheduler_option.items() - for param_name, (sch_cls, sch_params) in opt_dict.items() - if policy_id in self.policy_dict - } - self._inference_services = [] self._conn = {} - def _inference_service(name, get_policy, conn, exploration_scheduler_option): + def _inference_service(name, get_policy, conn): policy = get_policy(name) - if exploration_scheduler_option: - exploration_scheduler = { - param_name: sch_cls(policy.exploration_params, param_name, **sch_params) - for param_name, (sch_cls, sch_params) in exploration_scheduler_option.items() - } + if not isinstance(policy, RLPolicy): + raise TypeError("Only an 'RLPolicy' can be parallelized") while True: msg = conn.recv() if msg["type"] == "choose_action": - actions = policy.choose_action(msg["states"]) + actions = policy(msg["states"]) conn.send(actions) elif msg["type"] == "set_state": policy.set_state(msg["policy_state"]) elif msg["type"] == "explore": - policy.explore() + policy.greedy = False elif msg["type"] == "exploit": - policy.exploit() + policy.greedy = True elif msg["type"] == "exploration_step": - if exploration_scheduler_option: - for sch in exploration_scheduler.values(): - sch.step() + if hasattr(policy, "exploration_step"): + policy.exploration_step() elif msg["type"] == "rollout_info": conn.send(policy.get_rollout_info()) elif msg["type"] == "exploration_params": @@ -91,9 +79,7 @@ def _inference_service(name, get_policy, conn, exploration_scheduler_option): self._conn[policy_id] = conn1 host = Process( target=_inference_service, - args=( - policy_id, get_policy_func_dict[policy_id], conn2, exploration_scheduler_option.get(policy_id, None) - ) + args=(policy_id, get_policy_func_dict[policy_id], conn2) ) self._inference_services.append(host) host.start() @@ -114,7 +100,7 @@ def choose_action(self, state_by_agent: Dict[str, np.ndarray]): for policy_id, policy in self.policy_dict.items(): if states_by_policy[policy_id]: action_by_agent.update( - zip(agents_by_policy[policy_id], policy.choose_action(np.vstack(states_by_policy[policy_id]))) + zip(agents_by_policy[policy_id], policy(np.vstack(states_by_policy[policy_id]))) ) for policy_id, conn in self._conn.items(): @@ -134,21 +120,22 @@ def explore(self): for conn in self._conn.values(): conn.send({"type": "explore"}) for policy in self.policy_dict.values(): - if hasattr(policy, "explore"): - policy.explore() + if hasattr(policy, "greedy"): + policy.greedy = False def exploit(self): for conn in self._conn.values(): conn.send({"type": "exploit"}) for policy in self.policy_dict.values(): - if hasattr(policy, "exploit"): - policy.exploit() + if hasattr(policy, "greedy"): + policy.greedy = True def exploration_step(self): for conn in self._conn.values(): conn.send({"type": "exploration_step"}) - for sch in self.exploration_scheduler.values(): - sch.step() + for policy in self.policy_dict.values(): + if hasattr(policy, "exploration_step"): + policy.exploration_step() def get_rollout_info(self): for conn in self._conn.values(): @@ -168,7 +155,7 @@ def get_exploration_params(self): return { **{ - policy_id: policy.exploration_params for policy_id, policy in self.policy_dict.items() + policy_id: clone(policy.exploration_params) for policy_id, policy in self.policy_dict.items() if isinstance(policy, RLPolicy) }, **{policy_id: conn.recv() for policy_id, conn in self._conn.items()} @@ -218,7 +205,6 @@ def __init__( self, get_env: Callable[[], Env], get_policy_func_dict: Dict[str, Callable], - exploration_scheduler_option: Dict[str, tuple], agent2policy: Dict[str, str], get_test_env: Callable[[], Env] = None, reward_eval_delay: int = 0, @@ -230,8 +216,7 @@ def __init__( self.env = None self.agent_wrapper = AgentWrapper( - get_policy_func_dict, agent2policy, exploration_scheduler_option, - policies_to_parallelize=policies_to_parallelize + get_policy_func_dict, agent2policy, policies_to_parallelize=policies_to_parallelize ) self.reward_eval_delay = reward_eval_delay @@ -240,7 +225,6 @@ def __init__( self._state = None self._event = None self._step_index = 0 - self._terminal = True self._transition_cache = defaultdict(deque) # for caching transitions whose rewards have yet to be evaluated self.tracker = {} # User-defined tracking information is placed here. @@ -282,39 +266,32 @@ def sample( self, policy_state_dict: dict = None, num_steps: int = -1, - exploration_step: bool = False, return_rollout_info: bool = True ): self.env = self._learn_env - # set policy states - if policy_state_dict: - self.agent_wrapper.set_policy_states(policy_state_dict) - - # update exploration states if necessary - self.agent_wrapper.explore() - - if exploration_step: - self.agent_wrapper.exploration_step() - - if self._terminal: + if not self._state: # reset and get initial state self.env.reset() self._step_index = 0 self._transition_cache.clear() self.tracker.clear() - self._terminal = False _, self._event, _ = self.env.step(None) self._state = self.get_state() + # set policy states + if policy_state_dict: + self.agent_wrapper.set_policy_states(policy_state_dict) + self.agent_wrapper.explore() + starting_step_index = self._step_index + 1 steps_to_go = float("inf") if num_steps == -1 else num_steps - while not self._terminal and steps_to_go > 0: + while self._state and steps_to_go > 0: action = self.agent_wrapper.choose_action(self._state) env_actions = self.get_env_actions(action) for agent, state in self._state.items(): self._transition_cache[agent].append((state, action[agent], env_actions, self.env.tick)) - _, self._event, self._terminal = self.env.step(env_actions) - self._state = None if self._terminal else self.get_state() + _, self._event, done = self.env.step(env_actions) + self._state = None if done else self.get_state() self._step_index += 1 steps_to_go -= 1 @@ -323,7 +300,7 @@ def sample( Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. """ for agent, cache in self._transition_cache.items(): - while cache and (self._terminal or self.env.tick - cache[0][-1] >= self.reward_eval_delay): + while cache and (not self._state or self.env.tick - cache[0][-1] >= self.reward_eval_delay): state, action, env_actions, tick = cache.popleft() reward = self.get_reward(env_actions, tick) if self._post_step: @@ -332,19 +309,20 @@ def sample( self.agent_wrapper.record_transition( agent, state, action, reward[agent], cache[0][0] if cache else self._state, - not cache and self._terminal + not cache and not self._state ) result = { "step_range": (starting_step_index, self._step_index), "tracker": self.tracker, - "end_of_episode": self._terminal, + "end_of_episode": not self._state, "exploration_params": self.agent_wrapper.get_exploration_params() } - if return_rollout_info: result["rollout_info"] = self.agent_wrapper.get_rollout_info() + if not self._state: + self.agent_wrapper.exploration_step() return result def test(self, policy_state_dict: dict = None): @@ -406,8 +384,7 @@ def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = ep = msg.body[MsgKey.EPISODE] result = self.sample( policy_state_dict=msg.body[MsgKey.POLICY_STATE], - num_steps=msg.body[MsgKey.NUM_STEPS], - exploration_step=msg.body[MsgKey.EXPLORATION_STEP] + num_steps=msg.body[MsgKey.NUM_STEPS] ) logger.info( get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) @@ -467,11 +444,8 @@ def actor( # main loop for ep in range(1, num_episodes + 1): - exploration_step = True while True: - result = self.sample( - policy_state_dict=policy_state_dict, num_steps=num_steps, exploration_step=exploration_step - ) + result = self.sample(policy_state_dict=policy_state_dict, num_steps=num_steps) logger.info( get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) ) @@ -486,8 +460,6 @@ def actor( if result["end_of_episode"]: break - exploration_step = False - # tell the policy server I'm all done. proxy.isend(SessionMessage(MsgTag.DONE, proxy.name, server_address, session_type=SessionType.NOTIFICATION)) proxy.close() diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index de64e72de..42523cc22 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -115,7 +115,6 @@ def __init__( super().__init__(post_collect=post_collect, post_evaluate=post_evaluate) self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) self._num_steps = num_steps if num_steps > 0 else float("inf") - self._exploration_step = False self._parallelism = parallelism self._eval_parallelism = eval_parallelism if self._parallelism == 1: @@ -131,11 +130,7 @@ def _rollout_worker(index, conn, get_env_sampler): while True: msg = conn.recv() if msg["type"] == "sample": - result = env_sampler.sample( - policy_state_dict=msg["policy_state"], - num_steps=self._num_steps, - exploration_step=self._exploration_step - ) + result = env_sampler.sample(policy_state_dict=msg["policy_state"], num_steps=self._num_steps) logger.info(get_rollout_finish_msg( msg["episode"], result["step_range"], exploration_params=result["exploration_params"] )) @@ -171,11 +166,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): info_by_policy, trackers = defaultdict(list), [] if self._parallelism == 1: - result = self.env_sampler.sample( - policy_state_dict=policy_state_dict, - num_steps=self._num_steps, - exploration_step=self._exploration_step - ) + result = self.env_sampler.sample(policy_state_dict=policy_state_dict, num_steps=self._num_steps) self._logger.info( get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) ) @@ -189,16 +180,12 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): "type": "sample", "episode": ep, "num_steps": self._num_steps, - "policy_state": policy_state_dict, - "exploration_step": self._exploration_step + "policy_state": policy_state_dict } for conn in self._manager_ends: conn.send(rollout_req) - if self._exploration_step: - self._exploration_step = False - for conn in self._manager_ends: result = conn.recv() for policy_id, info in result["rollout_info"].items(): @@ -206,9 +193,6 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): trackers.append(result["tracker"]) self.episode_complete = result["episode_end"] - if self.episode_complete: - self._exploration_step = True - if self._post_collect: self._post_collect(trackers, ep, segment) @@ -330,7 +314,6 @@ def __init__( self._max_lag = max_lag self._num_eval_workers = num_eval_workers - self._exploration_step = False def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): """Collect simulation data, i.e., experiences for training. @@ -349,16 +332,12 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): MsgKey.SEGMENT: segment, MsgKey.NUM_STEPS: self._num_steps, MsgKey.POLICY_STATE: policy_state_dict, - MsgKey.VERSION: version, - MsgKey.EXPLORATION_STEP: self._exploration_step + MsgKey.VERSION: version } self._proxy.iscatter(MsgTag.SAMPLE, SessionType.TASK, [(worker_id, msg_body) for worker_id in self._workers]) self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") - if self._exploration_step: - self._exploration_step = False - info_by_policy, trackers, num_finishes = defaultdict(list), [], 0 # Ensure the minimum number of worker results are received. for msg in self._proxy.receive(): @@ -386,9 +365,6 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if num_finishes == self._num_workers: break - if self.episode_complete: - self._exploration_step = True - if self._post_collect: self._post_collect(trackers, ep, segment) diff --git a/maro/rl/modeling/specials.py b/maro/rl/modeling/specials.py index d5d76654f..73bc0bb6e 100644 --- a/maro/rl/modeling/specials.py +++ b/maro/rl/modeling/specials.py @@ -20,6 +20,11 @@ class DiscreteACNet(AbsCoreModel): def input_dim(self): raise NotImplementedError + @property + @abstractmethod + def num_actions(self): + raise NotImplementedError + @abstractmethod def forward(self, states: torch.tensor, actor: bool = True, critic: bool = True) -> tuple: """Compute action probabilities and values for a state batch. @@ -63,6 +68,11 @@ class DiscretePolicyNet(AbsCoreModel): def input_dim(self): raise NotImplementedError + @property + @abstractmethod + def num_actions(self): + raise NotImplementedError + @abstractmethod def forward(self, states: torch.tensor) -> torch.tensor: """Compute action probabilities corresponding to each state in ``states``. @@ -143,19 +153,21 @@ def soft_update(self, other_model: nn.Module, tau: float): class ContinuousACNet(AbsCoreModel): """Model container for the actor-critic architecture for continuous action spaces.""" - def __init__(self, min_action: Union[float, np.ndarray] = None, max_action: Union[float, np.ndarray] = None): + def __init__(self, out_min: Union[float, np.ndarray] = None, out_max: Union[float, np.ndarray] = None): super().__init__() - if min_action: - assert isinstance(min_action, (float, np.ndarray)), "min_action must be a float or a numpy array" - if max_action: - assert isinstance(max_action, (float, np.ndarray)), "max_action must be a float or a numpy array" + if out_min: + assert isinstance(out_min, (float, np.ndarray)), "out_min must be a float or a numpy array" + if out_max: + assert isinstance(out_max, (float, np.ndarray)), "out_max must be a float or a numpy array" - if isinstance(min_action, np.ndarray) and isinstance(max_action, np.ndarray): - assert len(min_action) == len(max_action), "min_action and max_action should have the same dimension." + if isinstance(out_min, np.ndarray) and isinstance(out_max, np.ndarray): + assert len(out_min) == len(out_max), "out_min and out_max should have the same dimension." + self.out_min = out_min + self.out_max = out_max # For torch clamping - self._min_action = torch.from_numpy(min_action) if isinstance(min_action, np.ndarray) else min_action - self._max_action = torch.from_numpy(max_action) if isinstance(max_action, np.ndarray) else max_action + self._min = torch.from_numpy(out_min) if isinstance(out_min, np.ndarray) else out_min + self._max = torch.from_numpy(out_max) if isinstance(out_max, np.ndarray) else out_max @property @abstractmethod @@ -167,14 +179,6 @@ def input_dim(self): def action_dim(self): raise NotImplementedError - @property - def min_action(self): - return self._min_action - - @property - def max_action(self): - return self._max_action - @abstractmethod def forward(self, states: torch.tensor, actions=None) -> torch.tensor: """Compute actions for a batch of states or Q-values for a batch of states and actions. @@ -189,7 +193,9 @@ def forward(self, states: torch.tensor, actions=None) -> torch.tensor: def get_action(self, states: torch.tensor) -> torch.tensor: """Compute actions given a batch of states.""" - return torch.clamp(self.forward(states), min=self._min_action, max=self._max_action) + if self._min is None and self._max is None: + return self.forward(states) + return torch.clamp(self.forward(states), min=self._min, max=self._max) def value(self, states: torch.tensor): """Compute the Q-values for a batch of states using the actions computed from them.""" diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index e479702e5..62d041959 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -132,7 +132,7 @@ def __init__( self._buffer = defaultdict(lambda: self.Buffer(self.ac_net.input_dim, size=self.max_trajectory_len)) - def choose_action(self, states: np.ndarray): + def __call__(self, states: np.ndarray): """Return actions and log probabilities for given states.""" self.ac_net.eval() states = torch.from_numpy(states).to(self.device) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index daeb886f9..515fdf15f 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, List, Union +from typing import Callable, List, Tuple, Union import numpy as np import torch @@ -34,17 +34,23 @@ class DDPG(RLPolicy): loss = policy_loss + ``q_value_loss_coeff`` * q_value_loss. Defaults to 1.0. soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. Defaults to 1.0. - exploration_func (Callable): Function to generate exploratory actions. The function takes an action - (single or batch) and a set of keyword arguments, and returns an exploratory action (single or batch - depending on the input). Defaults to ``gaussian_noise``. + exploration_strategy (Tuple[Callable, dict]): A 2-tuple that consists of a) a function that takes a state + (single or batch), an action (single or batch), the total number of possible actions and a set of keyword + arguments, and returns an exploratory action (single or batch depending on the input), and b) a dictionary + of keyword arguments for the function in a) (this will be assigned to the ``exploration_params`` member + variable). Defaults to (``gaussian_noise``, {"mean": .0, "stddev": 1.0, "relative": False}). + exploration_scheduling_option (List[tuple]): A list of 3-tuples specifying the exploration schedulers to be + registered to the exploration parameters. Each tuple consists of an exploration parameter name, an + exploration scheduler class (subclass of ``AbsExplorationScheduler``) and keyword arguments for that class. + The exploration parameter name must be a key in the keyword arguments (second element) of + ``exploration_strategy``. Defaults to an empty list. + replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. exploration_params (dict): Keyword arguments for ``exploration_func``. Defaults to {"mean": .0, "stddev": 1.0, "relative": False}. replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. - warmup (int): When the total number of experiences in the replay memory is below this threshold, - ``choose_action`` will return uniformly random actions for warm-up purposes. Defaults to 1000. rollout_batch_size (int): Size of the experience batch to use as roll-out information by calling ``get_rollout_info``. Defaults to 1000. train_batch_size (int): Batch size for training the Q-net. Defaults to 32. @@ -61,11 +67,11 @@ def __init__( q_value_loss_cls="mse", q_value_loss_coeff: float = 1.0, soft_update_coeff: float = 1.0, - exploration_func: Callable = gaussian_noise, - exploration_params: dict = {"mean": .0, "stddev": 1.0, "relative": False}, - replay_memory_capacity: int = 10000, + exploration_strategy: Tuple[Callable, dict] = (gaussian_noise, {"mean": .0, "stddev": 1.0, "relative": False}), + exploration_scheduling_options: List[tuple] = [], + replay_memory_capacity: int = 1000000, random_overwrite: bool = False, - warmup: int = 1000, + warmup: int = 50000, rollout_batch_size: int = 1000, train_batch_size: int = 32, device: str = None @@ -73,6 +79,12 @@ def __init__( if not isinstance(ac_net, ContinuousACNet): raise TypeError("model must be an instance of 'ContinuousACNet'") + if any(opt[0] not in exploration_strategy[1] for opt in exploration_scheduling_options): + raise ValueError( + f"The first element of an exploration scheduling option must be one of " + f"{list(exploration_strategy[1].keys())}" + ) + super().__init__(name) if device is None: self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') @@ -98,22 +110,28 @@ def __init__( self.rollout_batch_size = rollout_batch_size self.train_batch_size = train_batch_size - self.exploration_func = exploration_func - self.exploration_params = exploration_params - self.greedy = True + self.exploration_func = exploration_strategy[0] + self._exploration_params = clone(exploration_strategy[1]) + self.exploration_schedulers = [ + opt[1](self._exploration_params, opt[0], **opt[2]) for opt in exploration_scheduling_options + ] - def choose_action(self, states) -> Union[float, np.ndarray]: + def __call__(self, states: np.ndarray): if self._replay_memory.size < self.warmup: return np.random.uniform( - low=self.ac_net.min_action, high=self.ac_net.max_action, size=self.ac_net.action_dim + low=self.ac_net.out_min, high=self.ac_net.out_max, + size=(states.shape[0] if len(states.shape) > 1 else 1, self.ac_net.action_dim) ) self.ac_net.eval() + states = torch.from_numpy(states).to(self.device) + if len(states.shape) == 1: + states = states.unsqueeze(dim=0) with torch.no_grad(): actions = self.ac_net.get_action(states).cpu().numpy() if not self.greedy: - actions = self.exploration_func(actions, state=states) + actions = self.exploration_func(states, actions, **self._exploration_params) return actions def record( @@ -193,6 +211,10 @@ def _update_target(self): self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) self._target_ac_net_version = self._ac_net_version + def exploration_step(self): + for sch in self.exploration_schedulers: + sch.step() + def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index dfb453375..de40f1452 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, List, Union +from typing import Callable, List, Tuple, Union import numpy as np import torch -from maro.rl.exploration import eps_greedy +from maro.rl.exploration import epsilon_greedy from maro.rl.modeling import DiscreteQNet from maro.rl.utils import average_grads from maro.utils import clone @@ -132,16 +132,22 @@ class DQN(RLPolicy): double (bool): If True, the next Q values will be computed according to the double DQN algorithm, i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. - exploration_func (Callable): Function to generate exploratory actions. The function takes an action - (single or batch), the total number of possible actions and a set of keyword arguments, and returns an - exploratory action (single or batch depending on the input). Defaults to ``epsilon_greedy``. - exploration_params (dict): Keyword arguments for ``exploration_func``. Defaults to {"epsilon": 0.1}. - replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. + exploration_strategy (Tuple[Callable, dict]): A 2-tuple that consists of a) a function that takes a state + (single or batch), an action (single or batch), the total number of possible actions and a set of keyword + arguments, and returns an exploratory action (single or batch depending on the input), and b) a dictionary + of keyword arguments for the function in a) (this will be assigned to the ``_exploration_params`` member + variable). Defaults to (``epsilon_greedy``, {"epsilon": 0.1}). + exploration_scheduling_options (List[tuple]): A list of 3-tuples specifying the exploration schedulers to be + registered to the exploration parameters. Each tuple consists of an exploration parameter name, an + exploration scheduler class (subclass of ``AbsExplorationScheduler``) and keyword arguments for that class. + The exploration parameter name must be a key in the keyword arguments (second element) of + ``exploration_strategy``. Defaults to an empty list. + replay_memory_capacity (int): Capacity of the replay memory. Defaults to 1000000. random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. warmup (int): When the total number of experiences in the replay memory is below this threshold, - ``choose_action`` will return uniformly random actions for warm-up purposes. Defaults to 1000. + ``choose_action`` will return uniformly random actions for warm-up purposes. Defaults to 50000. rollout_batch_size (int): Size of the experience batch to use as roll-out information by calling ``get_rollout_info``. Defaults to 1000. train_batch_size (int): Batch size for training the Q-net. Defaults to 32. @@ -161,11 +167,11 @@ def __init__( update_target_every: int = 5, soft_update_coeff: float = 0.1, double: bool = False, - exploration_func: Callable = eps_greedy, - exploration_params: dict = {"epsilon": 0.1}, - replay_memory_capacity: int = 10000, + exploration_strategy: Tuple[Callable, dict] = (epsilon_greedy, {"epsilon": 0.1}), + exploration_scheduling_options: List[tuple] = [], + replay_memory_capacity: int = 1000000, random_overwrite: bool = False, - warmup: int = 1000, + warmup: int = 50000, rollout_batch_size: int = 1000, train_batch_size: int = 32, prioritized_replay_kwargs: dict = None, @@ -174,6 +180,12 @@ def __init__( if not isinstance(q_net, DiscreteQNet): raise TypeError("model must be an instance of 'DiscreteQNet'") + if any(opt[0] not in exploration_strategy[1] for opt in exploration_scheduling_options): + raise ValueError( + f"The first element of an exploration scheduling option must be one of " + f"{list(exploration_strategy[1].keys())}" + ) + super().__init__(name) if device is None: self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') @@ -204,11 +216,13 @@ def __init__( else: self._loss_func = torch.nn.MSELoss() - self.exploration_func = exploration_func - self.exploration_params = exploration_params - self.greedy = True # set initial exploration status to True + self.exploration_func = exploration_strategy[0] + self._exploration_params = clone(exploration_strategy[1]) # deep copy is needed to avoid unwanted sharing + self.exploration_schedulers = [ + opt[1](self._exploration_params, opt[0], **opt[2]) for opt in exploration_scheduling_options + ] - def choose_action(self, states: np.ndarray) -> Union[int, np.ndarray]: + def __call__(self, states: np.ndarray): if self._replay_memory.size < self.warmup: return np.random.randint(self._num_actions, size=(states.shape[0] if len(states.shape) > 1 else 1,)) @@ -223,7 +237,7 @@ def choose_action(self, states: np.ndarray) -> Union[int, np.ndarray]: if self.greedy: return actions.cpu().numpy() else: - return self.exploration_func(actions.cpu().numpy(), self._num_actions, **self.exploration_params) + return self.exploration_func(states, actions.cpu().numpy(), self._num_actions, **self._exploration_params) def record( self, @@ -286,7 +300,6 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): # loss info loss_info = {} q_values = self.q_net.q_values(states, actions) - # print(f"target: {target_q_values}, eval: {q_values}") td_errors = target_q_values - q_values if self.prioritized_replay: is_weights = torch.from_numpy(batch["is_weights"]).to(self.device) @@ -331,13 +344,17 @@ def _update_target(self): self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) self._target_q_net_version = self._q_net_version - def set_state(self, policy_state): - self.q_net.load_state_dict(policy_state["eval"]) - if "target" in policy_state: - self.target_q_net.load_state_dict(policy_state["target"]) + def exploration_step(self): + for sch in self.exploration_schedulers: + sch.step() def get_state(self, inference: bool = True): policy_state = {"eval": self.q_net.state_dict()} if not inference and self.target_q_net: policy_state["target"] = self.target_q_net.state_dict() return policy_state + + def set_state(self, policy_state): + self.q_net.load_state_dict(policy_state["eval"]) + if "target" in policy_state: + self.target_q_net.load_state_dict(policy_state["target"]) diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index 063dea68c..40471347e 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -92,7 +92,10 @@ def __init__( self._buffer = defaultdict(lambda: self.Buffer(self.policy_net.input_dim, size=self.max_trajectory_len)) - def choose_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + def get_random_action(self, states: np.ndarray): + return np.random.randint(self.policy_net.num_actions, size=(states.shape[0] if len(states.shape) > 1 else 1,)) + + def get_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Return actions and log probabilities for given states.""" self.policy_net.eval() with torch.no_grad(): diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index a20484f1d..c03f22552 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -4,6 +4,8 @@ from abc import ABC, abstractmethod from typing import List +import numpy as np + class AbsPolicy(ABC): """Abstract policy class. @@ -21,7 +23,7 @@ def name(self): return self._name @abstractmethod - def choose_action(self, state): + def __call__(self, state): raise NotImplementedError @@ -30,7 +32,7 @@ class NullPolicy(AbsPolicy): Note that the meaning of a "None" action may depend on the scenario. """ - def choose_action(self, state): + def __call__(self, state): return None @@ -44,37 +46,88 @@ class RLPolicy(AbsPolicy): """ def __init__(self, name: str): super().__init__(name) - self.exploration_params = {} + self._exploration_params = {} self.greedy = True + @property + def exploration_params(self): + return self._exploration_params + @abstractmethod - def choose_action(self, state): - raise NotImplementedError + def __call__(self, states: np.ndarray): + raise NotImplementedError + + def record(self, agent_id: str, state, action, reward, next_state, terminal: bool): + """Record a transition in an internal buffer or memory. - def record(self, key: str, state, action, reward, next_state, terminal: bool): + Since we may have multiple agents sharing this policy, the internal buffer / memory should use the agents' + names to separate storage for these agents. The ``agent_id`` parameter serves this purpose. + """ pass def get_rollout_info(self): + """Extract information from the recorded transitions. + + Implement this method if you are doing distributed learning. What this function returns will be used to update + policy parameters on the learning side (abstracted through ``AbsPolicyManager``) with or without invoking the + policy improvement algorithm, depending on the type of information. If you want the policy improvement algorithm + to be invoked on roll-out instances (i.e., in distributed fashion), this should return loss information (which + can be obtained by calling ``get_batch_loss`` function with ``explicit_grad`` set to True) to be used by + ``update`` on the learning side. If you want the policy improvement algorithm to be invoked on the learning + side, this should return a data batch to be used by ``learn`` on the learning side. See the implementation of + this function in ``ActorCritic`` for reference. + """ pass def get_batch_loss(self, batch: dict, explicit_grad: bool = False): + """Compute policy improvement information, i.e., loss, from a data batch. + + This can be used as a sub-routine in ``learn`` and ``improve``, as these methods usually require computing + loss from a batch. + + Args: + batch (dict): Data batch to compute the policy improvement information for. + explicit_grad (bool): If True, the gradients should be explicitly returned. Defaults to False. + """ pass def update(self, loss_info_list: List[dict]): + """Update using loss information collected from multiple loss computing instances. + + There are two possible scenarios where you need to implement this interface: 1) if you are doing distributed + learning and want each roll-out instance to collect information that can be used to update policy parameters + on the learning side (abstracted through ``AbsPolicyManager``) without invoking the policy improvement + algorithm. Such information usually includes gradients with respect to the policy parameters. An example where + this can be useful is the Asynchronous Advantage Actor Acritic (A3C) (https://arxiv.org/abs/1602.01783); + 2) if you are computing loss in data-parallel fashion, i.e., by splitting a data batch to several smaller + batches and sending them to a set of remote workers for parallelized loss computation. + + Args: + loss_info_list (List[dict]): A list of dictionaries containing loss information (e.g., gradients) computed + by distributed roll-out instances. + """ pass def learn(self, batch: dict): - """Perform policy improvement based on a single data batch collected from one or more roll-out instances.""" - pass + """Learn from a batch of roll-out data. - def improve(self): + Implement this interface if you are doing distributed learning and want the roll-out instances to collect + information that can be used to update policy parameters on the learning side (abstracted through + ``AbsPolicyManager``) using the policy improvement algorithm. + + Args: + batch (dict): Training data to train the policy with. + """ pass - def exploit(self): - self.greedy = True + def improve(self): + """Learn using data collected locally. - def explore(self): - self.greedy = False + Implement this interface if you are doing single-threaded learning where a single policy instance is used for + roll-out and training. The policy should have some kind of internal buffer / memory to store roll-out data and + use as the source of training data. + """ + pass @abstractmethod def get_state(self): diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 6516163e1..d3b15a070 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -15,21 +15,29 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", + "# env and shaping config\n", + "env_conf = {\n", + " \"scenario\": \"cim\",\n", + " \"topology\": \"toy.4p_ssdd_l0.0\",\n", + " \"durations\": 560\n", + "}\n", "\n", - "# Common info\n", - "common_config = {\n", - " \"port_features\": [\"empty\", \"full\", \"on_shipper\", \"on_consignee\", \"booking\", \"shortage\", \"fulfillment\"],\n", - " \"vessel_features\": [\"empty\", \"full\", \"remaining_space\"],\n", - " # Parameters for computing states\n", + "port_attributes = [\"empty\", \"full\", \"on_shipper\", \"on_consignee\", \"booking\", \"shortage\", \"fulfillment\"]\n", + "vessel_attributes = [\"empty\", \"full\", \"remaining_space\"]\n", + "\n", + "state_shaping_conf = {\n", " \"look_back\": 7,\n", - " \"max_ports_downstream\": 2,\n", - " # Parameters for computing actions\n", - " \"num_actions\": 21,\n", + " \"max_ports_downstream\": 2\n", + "}\n", + "\n", + "action_shaping_conf = {\n", + " \"action_space\": [(i - 10) / 10 for i in range(21)],\n", " \"finite_vessel_space\": True,\n", - " \"has_early_discharge\": True,\n", - " # Parameters for computing rewards\n", - " \"reward_eval_delay\": 99,\n", + " \"has_early_discharge\": True\n", + "}\n", + "\n", + "reward_shaping_conf = {\n", + " \"time_window\": 99,\n", " \"fulfillment_factor\": 1.0,\n", " \"shortage_factor\": 1.0,\n", " \"time_decay\": 0.97\n", @@ -40,7 +48,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Shaping" + "## Environment Sampler" ] }, { @@ -50,97 +58,74 @@ "outputs": [], "source": [ "import numpy as np\n", - "from maro.rl import AbsEnvWrapper\n", + "from maro.rl.learning import AbsEnvSampler\n", "from maro.simulator.scenarios.cim.common import Action, ActionType\n", "\n", "\n", - "class CIMEnvWrapper(AbsEnvWrapper):\n", - " def __init__(\n", - " self, env, save_replay=True, replay_agent_ids=None, *, port_features, vessel_features, num_actions,\n", - " look_back,max_ports_downstream, reward_eval_delay, fulfillment_factor, shortage_factor, time_decay,\n", - " finite_vessel_space=True, has_early_discharge=True \n", - " ):\n", - " super().__init__(env, save_replay=save_replay, replay_agent_ids=replay_agent_ids, reward_eval_delay=reward_eval_delay)\n", - " self.port_features = port_features\n", - " self.vessel_features = vessel_features\n", - " self.action_space = list(np.linspace(-1.0, 1.0, num_actions))\n", - " self.look_back = look_back\n", - " self.max_ports_downstream = max_ports_downstream\n", - " self.fulfillment_factor = fulfillment_factor\n", - " self.shortage_factor = shortage_factor\n", - " self.time_decay = time_decay\n", - " self.finite_vessel_space = finite_vessel_space\n", - " self.has_early_discharge = has_early_discharge\n", - " self._last_action_tick = None\n", - "\n", + "class CIMEnvSampler(AbsEnvSampler):\n", " def get_state(self, tick=None):\n", " if tick is None:\n", " tick = self.env.tick\n", " vessel_snapshots, port_snapshots = self.env.snapshot_list[\"vessels\"], self.env.snapshot_list[\"ports\"]\n", " port_idx, vessel_idx = self.event.port_idx, self.event.vessel_idx\n", - " ticks = [max(0, tick - rt) for rt in range(self.look_back - 1)]\n", - " future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int')\n", - " port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_features]\n", - " vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_features]\n", - " self.state_info = {\n", - " \"tick\": tick, \"action_scope\": self.event.action_scope, \"port_idx\": port_idx, \"vessel_idx\": vessel_idx\n", - " }\n", - " state = np.concatenate((port_features, vessel_features))\n", - " self._last_action_tick = tick\n", + " ticks = [max(0, tick - rt) for rt in range(state_shaping_conf[\"look_back\"] - 1)]\n", + " future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') \n", + " state = np.concatenate([\n", + " port_snapshots[ticks : [port_idx] + list(future_port_list) : port_attributes],\n", + " vessel_snapshots[tick : vessel_idx : vessel_attributes]\n", + " ])\n", " return {port_idx: state}\n", "\n", - " def to_env_action(self, action_by_agent):\n", - " vessel_snapshots = self.env.snapshot_list[\"vessels\"]\n", - " action_info = list(action_by_agent.values())[0]\n", - " model_action = action_info[0] if isinstance(action_info, tuple) else action_info\n", - " tick, port, vessel = self.state_info[\"tick\"], self.state_info[\"port_idx\"], self.state_info[\"vessel_idx\"]\n", - " zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero.\n", - " vessel_space = vessel_snapshots[tick:vessel:self.vessel_features][2] if self.finite_vessel_space else float(\"inf\")\n", - " early_discharge = vessel_snapshots[tick:vessel:\"early_discharge\"][0] if self.has_early_discharge else 0\n", - " percent = abs(self.action_space[model_action])\n", - "\n", - " action_scope = self.state_info[\"action_scope\"]\n", + " def get_env_actions(self, action_by_agent):\n", + " action_space = action_shaping_conf[\"action_space\"]\n", + " finite_vsl_space = action_shaping_conf[\"finite_vessel_space\"]\n", + " has_early_discharge = action_shaping_conf[\"has_early_discharge\"]\n", + "\n", + " port_idx, action = list(action_by_agent.items()).pop()\n", + " vsl_idx, action_scope = self.event.vessel_idx, self.event.action_scope\n", + " vsl_snapshots = self.env.snapshot_list[\"vessels\"]\n", + " vsl_space = vsl_snapshots[self.env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float(\"inf\")\n", + "\n", + " model_action = action[\"action\"] if isinstance(action, dict) else action \n", + " percent = abs(action_space[model_action])\n", + " zero_action_idx = len(action_space) / 2 # index corresponding to value zero.\n", " if model_action < zero_action_idx:\n", " action_type = ActionType.LOAD\n", - " actual_action = min(round(percent * action_scope.load), vessel_space)\n", + " actual_action = min(round(percent * action_scope.load), vsl_space)\n", " elif model_action > zero_action_idx:\n", " action_type = ActionType.DISCHARGE\n", + " early_discharge = vsl_snapshots[self.env.tick:vsl_idx:\"early_discharge\"][0] if has_early_discharge else 0\n", " plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge\n", " actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge)\n", " else:\n", " actual_action, action_type = 0, None\n", "\n", - " return Action(vessel, port, actual_action, action_type)\n", + " return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)]\n", "\n", - " def get_reward(self, tick=None):\n", + " def get_reward(self, actions, tick):\n", " \"\"\"Delayed reward evaluation.\"\"\"\n", - " if tick is None:\n", - " tick = self._last_action_tick\n", - " port_snapshots = self.env.snapshot_list[\"ports\"]\n", " start_tick = tick + 1\n", - " ticks = list(range(start_tick, start_tick + self.reward_eval_delay))\n", - "\n", - " future_fulfillment = port_snapshots[ticks::\"fulfillment\"]\n", - " future_shortage = port_snapshots[ticks::\"shortage\"]\n", - " decay_list = [\n", - " self.time_decay ** i for i in range(self.reward_eval_delay)\n", - " for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay)\n", - " ]\n", - "\n", - " return {\n", - " agent_id: np.float32(\n", - " self.fulfillment_factor * np.dot(future_fulfillment, decay_list) - \n", - " self.shortage_factor * np.dot(future_shortage, decay_list)\n", - " )\n", - " for agent_id in self.action_history[tick]\n", - " }" + " ticks = list(range(start_tick, start_tick + reward_shaping_conf[\"time_window\"]))\n", + "\n", + " # Get the ports that took actions at the given tick\n", + " ports = [action.port_idx for action in actions]\n", + " port_snapshots = self.env.snapshot_list[\"ports\"]\n", + " future_fulfillment = port_snapshots[ticks:ports:\"fulfillment\"].reshape(len(ticks), -1)\n", + " future_shortage = port_snapshots[ticks:ports:\"shortage\"].reshape(len(ticks), -1)\n", + "\n", + " decay_list = [reward_shaping_conf[\"time_decay\"] ** i for i in range(reward_shaping_conf[\"time_window\"])]\n", + " rewards = np.float32(\n", + " reward_shaping_conf[\"fulfillment_factor\"] * np.dot(future_fulfillment.T, decay_list)\n", + " - reward_shaping_conf[\"shortage_factor\"] * np.dot(future_shortage.T, decay_list)\n", + " )\n", + " return {agent_id: reward for agent_id, reward in zip(ports, rewards)}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## [Policy](https://maro.readthedocs.io/en/latest/key_components/rl_toolkit.html#agent)\n", + "## [Policies](https://maro.readthedocs.io/en/latest/key_components/rl_toolkit.html#agent)\n", "\n", "The out-of-the-box ActorCritic is used as our agent." ] @@ -152,77 +137,97 @@ "outputs": [], "source": [ "import torch\n", + "from torch.optim import Adam, RMSprop\n", "\n", - "from maro.rl import ActorCritic, ActorCriticConfig, DiscreteACNet, ExperienceMemory, FullyConnected, OptimOption\n", + "from maro.rl.exploration import MultiLinearExplorationScheduler, epsilon_greedy\n", + "from maro.rl.policy import ActorCritic, DiscreteACNet, FullyConnected\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", - "input_dim = (\n", - " (common_config[\"look_back\"] + 1) *\n", - " (common_config[\"max_ports_downstream\"] + 1) *\n", - " len(common_config[\"port_features\"]) +\n", - " len(common_config[\"vessel_features\"])\n", + "# obtain state dimension from a temporary env_wrapper instance\n", + "state_dim = (\n", + " (state_shaping_conf[\"look_back\"] + 1) * (state_shaping_conf[\"max_ports_downstream\"] + 1) * len(port_attributes)\n", + " + len(vessel_attributes)\n", ")\n", "\n", - "policy_config = {\n", - " \"model\": { \n", - " \"network\": {\n", - " \"actor\": {\n", - " \"input_dim\": input_dim,\n", - " \"output_dim\": common_config[\"num_actions\"],\n", - " \"hidden_dims\": [256, 128, 64],\n", - " \"activation\": \"tanh\",\n", - " \"softmax\": True,\n", - " \"batch_norm\": False,\n", - " \"head\": True\n", - " },\n", - " \"critic\": {\n", - " \"input_dim\": input_dim,\n", - " \"output_dim\": 1,\n", - " \"hidden_dims\": [256, 128, 64],\n", - " \"activation\": \"leaky_relu\",\n", - " \"softmax\": False,\n", - " \"batch_norm\": True,\n", - " \"head\": True\n", - " }\n", - " },\n", - " \"optimization\": {\n", - " \"actor\": OptimOption(optim_cls=\"adam\", optim_params={\"lr\": 0.001}),\n", - " \"critic\": OptimOption(optim_cls=\"rmsprop\", optim_params={\"lr\": 0.001})\n", - " }\n", - " },\n", - " \"experience_memory\": {\n", - " \"capacity\": 10000\n", - " },\n", - " \"algorithm_config\": {\n", - " \"reward_discount\": .0,\n", - " \"train_epochs\": 10,\n", - " \"gradient_iters\": 1,\n", - " \"actor_loss_coefficient\": 0.1, # loss = actor_loss_coefficient * actor_loss + critic_loss\n", - " \"critic_loss_cls\": \"smooth_l1\",\n", - " }\n", + "# AC settings\n", + "actor_net_conf = {\n", + " \"input_dim\": state_dim,\n", + " \"hidden_dims\": [256, 128, 64],\n", + " \"output_dim\": len(action_shaping_conf[\"action_space\"]),\n", + " \"activation\": torch.nn.Tanh,\n", + " \"softmax\": True,\n", + " \"batch_norm\": False,\n", + " \"head\": True\n", + "}\n", + "\n", + "critic_net_conf = {\n", + " \"input_dim\": state_dim,\n", + " \"hidden_dims\": [256, 128, 64],\n", + " \"output_dim\": 1,\n", + " \"activation\": torch.nn.LeakyReLU,\n", + " \"softmax\": False,\n", + " \"batch_norm\": True,\n", + " \"head\": True\n", + "}\n", + "\n", + "actor_optim_conf = (Adam, {\"lr\": 0.001})\n", + "critic_optim_conf = (RMSprop, {\"lr\": 0.001})\n", + "\n", + "ac_conf = {\n", + " \"reward_discount\": .0,\n", + " \"grad_iters\": 10,\n", + " \"critic_loss_cls\": torch.nn.SmoothL1Loss,\n", + " \"min_logp\": None,\n", + " \"critic_loss_coeff\": 0.1,\n", + " \"entropy_coeff\": 0.01,\n", + " # \"clip_ratio\": 0.8 # for PPO\n", + " \"lam\": .0,\n", + " \"get_loss_on_rollout\": False\n", "}\n", "\n", "\n", "class MyACNet(DiscreteACNet):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.actor = FullyConnected(**actor_net_conf)\n", + " self.critic = FullyConnected(**critic_net_conf)\n", + " self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1])\n", + " self.critic_optim = critic_optim_conf[0](self.critic.parameters(), **critic_optim_conf[1])\n", + "\n", + " @property\n", + " def input_dim(self):\n", + " return state_dim\n", + "\n", + " @property\n", + " def num_actions(self):\n", + " return q_net_conf[\"output_dim\"]\n", + "\n", " def forward(self, states, actor: bool = True, critic: bool = True):\n", - " states = torch.from_numpy(np.asarray(states))\n", - " if len(states.shape) == 1:\n", - " states = states.unsqueeze(dim=0)\n", - "\n", - " states = states.to(self.device)\n", - " return (\n", - " self.component[\"actor\"](states) if actor else None,\n", - " self.component[\"critic\"](states) if critic else None\n", - " )\n", + " return (self.actor(states) if actor else None), (self.critic(states) if critic else None)\n", + "\n", + " def step(self, loss):\n", + " self.actor_optim.zero_grad()\n", + " self.critic_optim.zero_grad()\n", + " loss.backward()\n", + " self.actor_optim.step()\n", + " self.critic_optim.step()\n", + "\n", + " def get_gradients(self, loss):\n", + " self.actor_optim.zero_grad()\n", + " self.critic_optim.zero_grad()\n", + " loss.backward()\n", + " return {name: param.grad for name, param in self.named_parameters()}\n", + "\n", + " def apply_gradients(self, grad):\n", + " for name, param in self.named_parameters():\n", + " param.grad = grad[name]\n", "\n", + " self.actor_optim.step()\n", + " self.critic_optim.step()\n", "\n", - "def get_ac_policy(name):\n", - " actor = FullyConnected(**policy_config[\"model\"][\"network\"][\"actor\"])\n", - " critic = FullyConnected(**policy_config[\"model\"][\"network\"][\"critic\"])\n", - " ac_net = MyACNet({\"actor\": actor, \"critic\": critic}, optim_option=policy_config[\"model\"][\"optimization\"])\n", - " experience_memory = ExperienceMemory(policy_config[\"experience_memory\"][\"capacity\"])\n", - " return ActorCritic(name, ac_net, experience_memory, ActorCriticConfig(**policy_config[\"algorithm_config\"]))" + "\n", + "policy_func_dict = {f\"ac.{i}\": lambda name: ActorCritic(name, MyACNet(), **ac_conf) for i in range(4)}" ] }, { @@ -244,21 +249,25 @@ "from maro.rl import SimpleLearner\n", "from maro.utils import set_seeds\n", "\n", + "\n", + "def get_env_sampler():\n", + " return CIMEnvSampler(\n", + " get_env=lambda: Env(**env_conf),\n", + " get_policy_func_dict=policy_func_dict,\n", + " agent2policy={agent: f\"ac.{agent}\" for agent in Env(**env_conf).agent_idx_list},\n", + " reward_eval_delay=reward_shaping_conf[\"time_window\"],\n", + " )\n", + "\n", "set_seeds(1024) # for reproducibility\n", - "env = Env(\"cim\", \"toy.4p_ssdd_l0.0\", durations=1120)\n", - "env_wrapper = CIMEnvWrapper(env, **common_config)\n", - "policies = [get_ac_policy(id_) for id_ in env.agent_idx_list]\n", - "agent2policy = {agent_id: agent_id for agent_id in env.agent_idx_list}\n", - "learner = SimpleLearner(env_wrapper, policies, agent2policy, 40) # 40 episodes\n", - "learner.run()" + "simple_learner(get_env_sampler, num_episodes=50)" ] } ], "metadata": { "kernelspec": { - "display_name": "maro", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "maro" + "name": "python3" }, "language_info": { "codemirror_mode": { From 8f652b4ce4743c599a2d76c59847f537aa4191c2 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sat, 11 Sep 2021 15:24:13 +0000 Subject: [PATCH 431/482] updated notebook --- maro/rl/learning/learner.py | 10 ++-- .../rl_formulation.ipynb | 47 +++++++++++++++++-- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py index 81d7df89f..a5c52cb64 100644 --- a/maro/rl/learning/learner.py +++ b/maro/rl/learning/learner.py @@ -70,13 +70,11 @@ def simple_learner( logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") eval_point_index = 0 - def collect_and_update(ep, exploration_step): + def collect_and_update(ep): """Collect simulation data for training.""" segment = 1 while True: - result = env_sampler.sample( - num_steps=num_steps, exploration_step=exploration_step, return_rollout_info=False - ) + result = env_sampler.sample(num_steps=num_steps, return_rollout_info=False) logger.info( get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) ) @@ -90,10 +88,8 @@ def collect_and_update(ep, exploration_step): segment += 1 - exploration_step = False for ep in range(1, num_episodes + 1): - collect_and_update(ep, exploration_step) - exploration_step = True + collect_and_update(ep) if ep == eval_schedule[eval_point_index]: eval_point_index += 1 tracker = env_sampler.test() diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index d3b15a070..8355859cc 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -140,7 +140,8 @@ "from torch.optim import Adam, RMSprop\n", "\n", "from maro.rl.exploration import MultiLinearExplorationScheduler, epsilon_greedy\n", - "from maro.rl.policy import ActorCritic, DiscreteACNet, FullyConnected\n", + "from maro.rl.modeling import DiscreteACNet, FullyConnected\n", + "from maro.rl.policy import ActorCritic\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", @@ -246,9 +247,12 @@ "outputs": [], "source": [ "from maro.simulator import Env\n", - "from maro.rl import SimpleLearner\n", + "from maro.rl.learning import simple_learner\n", "from maro.utils import set_seeds\n", "\n", + "# post-env-step callback\n", + "def post_step(env, tracker, state, action, env_action, reward, tick):\n", + " tracker[\"env_metric\"] = env.metrics\n", "\n", "def get_env_sampler():\n", " return CIMEnvSampler(\n", @@ -256,18 +260,51 @@ " get_policy_func_dict=policy_func_dict,\n", " agent2policy={agent: f\"ac.{agent}\" for agent in Env(**env_conf).agent_idx_list},\n", " reward_eval_delay=reward_shaping_conf[\"time_window\"],\n", + " post_step=post_step\n", " )\n", "\n", + "# post-episode callback\n", + "def post_collect(trackers, ep, segment):\n", + " # print the env metric from each rollout worker\n", + " for tracker in trackers:\n", + " print(f\"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}\")\n", + "\n", + " # print the average env metric\n", + " if len(trackers) > 1:\n", + " metric_keys, num_trackers = trackers[0][\"env_metric\"].keys(), len(trackers)\n", + " avg_metric = {key: sum(tr[\"env_metric\"][key] for tr in trackers) / num_trackers for key in metric_keys}\n", + " print(f\"average env summary (episode {ep}, segment {segment}): {avg_metric}\")\n", + "\n", + "# post-evaluation callback\n", + "def post_evaluate(trackers, ep):\n", + " # print the env metric from each rollout worker\n", + " for tracker in trackers:\n", + " print(f\"env summary (evaluation episode {ep}): {tracker['env_metric']}\")\n", + "\n", + " # print the average env metric\n", + " if len(trackers) > 1:\n", + " metric_keys, num_trackers = trackers[0][\"env_metric\"].keys(), len(trackers)\n", + " avg_metric = {key: sum(tr[\"env_metric\"][key] for tr in trackers) / num_trackers for key in metric_keys}\n", + " simulation_logger.info(f\"average env summary (evaluation episode {ep}): {avg_metric}\")\n", + "\n", + "\n", "set_seeds(1024) # for reproducibility\n", - "simple_learner(get_env_sampler, num_episodes=50)" + "simple_learner(get_env_sampler, num_episodes=50, post_collect=post_collect, post_evaluate=post_evaluate)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "maro", "language": "python", - "name": "python3" + "name": "maro" }, "language_info": { "codemirror_mode": { From 8dd708f32e2634bda43da1b68376b919dc9addef Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sat, 11 Sep 2021 15:27:00 +0000 Subject: [PATCH 432/482] fixed lint issues --- maro/rl/exploration/strategies.py | 12 ++++++------ maro/rl/policy/policy.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/maro/rl/exploration/strategies.py b/maro/rl/exploration/strategies.py index ba1620c9a..ecda1ee3d 100644 --- a/maro/rl/exploration/strategies.py +++ b/maro/rl/exploration/strategies.py @@ -12,7 +12,7 @@ def epsilon_greedy(state: np.ndarray, action: np.ndarray, num_actions, *, epsilo Args: state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the vanilla eps-greedy exploration and is put here to conform to the function signature required for the exploration - strategy parameter for ``DQN``. See ``maro.rl.policy.DQN`` for more details. + strategy parameter for ``DQN``. See ``maro.rl.policy.DQN`` for more details. action (np.ndarray): Action(s) chosen greedily by the policy. num_actions (int): Number of possible actions. epsilon (float): The probability that a random action will be selected. @@ -30,11 +30,11 @@ def uniform_noise( high: Union[float, list, np.ndarray] ) -> Union[float, np.ndarray]: """Apply a uniform noise to a continuous multi-dimensional action. - + Args: state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the gaussian noise exploration scheme and is put here to conform to the function signature for the exploration in continuous - action spaces. + action spaces. action (np.ndarray): Action(s) chosen greedily by the policy. min_action (Union[float, list, np.ndarray]): Lower bound for the multi-dimensional action space. max_action (Union[float, list, np.ndarray]): Upper bound for the multi-dimensional action space. @@ -58,11 +58,11 @@ def gaussian_noise( relative: bool = False ) -> Union[float, np.ndarray]: """Apply a gaussian noise to a continuous multi-dimensional action. - + Args: state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the gaussian noise exploration scheme and is put here to conform to the function signature for the exploration in continuous - action spaces. + action spaces. action (np.ndarray): Action(s) chosen greedily by the policy. min_action (Union[float, list, np.ndarray]): Lower bound for the multi-dimensional action space. max_action (Union[float, list, np.ndarray]): Upper bound for the multi-dimensional action space. @@ -70,7 +70,7 @@ def gaussian_noise( stddev (Union[float, list, np.ndarray]): Standard deviation for the Gaussian noise. Defaults to 1.0. relative (bool): If True, the generated noise will be multiplied by the action itself before being added to the action. Defaults to False. - + """ noise = np.random.normal(loc=mean, scale=stddev) if min_action is None and max_action is None: diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index c03f22552..d3faf1728 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -55,13 +55,13 @@ def exploration_params(self): @abstractmethod def __call__(self, states: np.ndarray): - raise NotImplementedError + raise NotImplementedError def record(self, agent_id: str, state, action, reward, next_state, terminal: bool): """Record a transition in an internal buffer or memory. Since we may have multiple agents sharing this policy, the internal buffer / memory should use the agents' - names to separate storage for these agents. The ``agent_id`` parameter serves this purpose. + names to separate storage for these agents. The ``agent_id`` parameter serves this purpose. """ pass @@ -74,7 +74,7 @@ def get_rollout_info(self): to be invoked on roll-out instances (i.e., in distributed fashion), this should return loss information (which can be obtained by calling ``get_batch_loss`` function with ``explicit_grad`` set to True) to be used by ``update`` on the learning side. If you want the policy improvement algorithm to be invoked on the learning - side, this should return a data batch to be used by ``learn`` on the learning side. See the implementation of + side, this should return a data batch to be used by ``learn`` on the learning side. See the implementation of this function in ``ActorCritic`` for reference. """ pass @@ -83,7 +83,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): """Compute policy improvement information, i.e., loss, from a data batch. This can be used as a sub-routine in ``learn`` and ``improve``, as these methods usually require computing - loss from a batch. + loss from a batch. Args: batch (dict): Data batch to compute the policy improvement information for. @@ -100,7 +100,7 @@ def update(self, loss_info_list: List[dict]): algorithm. Such information usually includes gradients with respect to the policy parameters. An example where this can be useful is the Asynchronous Advantage Actor Acritic (A3C) (https://arxiv.org/abs/1602.01783); 2) if you are computing loss in data-parallel fashion, i.e., by splitting a data batch to several smaller - batches and sending them to a set of remote workers for parallelized loss computation. + batches and sending them to a set of remote workers for parallelized loss computation. Args: loss_info_list (List[dict]): A list of dictionaries containing loss information (e.g., gradients) computed @@ -125,7 +125,7 @@ def improve(self): Implement this interface if you are doing single-threaded learning where a single policy instance is used for roll-out and training. The policy should have some kind of internal buffer / memory to store roll-out data and - use as the source of training data. + use as the source of training data. """ pass From 971fd042eb8cb3163dc41efae7863dfafc8302ea Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 12 Sep 2021 09:09:00 +0000 Subject: [PATCH 433/482] fixed entropy bugs in ac.py --- maro/rl/policy/ac.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 62d041959..d328b318e 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -103,7 +103,7 @@ def __init__( critic_loss_cls="mse", min_logp: float = None, critic_loss_coeff: float = 1.0, - entropy_coeff: float = None, + entropy_coeff: float = .0, clip_ratio: float = None, lam: float = 0.9, max_trajectory_len: int = 10000, @@ -203,7 +203,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): # critic_loss critic_loss = self.critic_loss_func(state_values, returns) # entropy - entropy = -Categorical(action_probs).entropy().mean() if self.entropy_coeff is not None else 0 + entropy = -Categorical(action_probs).entropy().mean() if self.entropy_coeff else 0 # total loss loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy @@ -211,7 +211,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): loss_info = { "actor_loss": actor_loss.detach().cpu().numpy(), "critic_loss": critic_loss.detach().cpu().numpy(), - "entropy": entropy.detach().cpu().numpy(), + "entropy": entropy.detach().cpu().numpy() if self.entropy_coeff else .0, "loss": loss.detach().cpu().numpy() if explicit_grad else loss } if explicit_grad: From bf3cadbac4f6f1d214506575a101f061136e385a Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 12 Sep 2021 09:17:12 +0000 Subject: [PATCH 434/482] reverted to simple policy manager as default --- examples/rl/workflows/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index f88293441..6d5f14ac2 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -24,7 +24,7 @@ async: group: async num_actors: 3 policy_manager: - type: distributed # simple, distributed + type: simple # simple, distributed simple: parallel: false distributed: From e89b6db63525f6ee7ead04eb749bc952df9f67b2 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Tue, 14 Sep 2021 12:24:20 +0000 Subject: [PATCH 435/482] 1. unified single-thread and distributed mode in learning_loop.py; 2. updated api doc for algorithms and rst for rl toolkit --- docs/source/key_components/rl_toolkit.rst | 54 ++-- .../rl/scripts/docker/docker_compose_yml.py | 218 +++++++------- examples/rl/scripts/run.sh | 4 - examples/rl/workflows/config.yml | 17 +- .../{learner.py => learning_loop.py} | 27 +- examples/rl/workflows/simple_learner.py | 40 --- maro/rl/exploration/scheduling.py | 12 +- maro/rl/exploration/strategies.py | 20 +- maro/rl/learning/__init__.py | 10 +- maro/rl/learning/early_stopper.py | 17 -- maro/rl/learning/env_sampler.py | 44 ++- maro/rl/learning/helpers.py | 16 + maro/rl/learning/learner.py | 193 ------------ maro/rl/learning/learning_loop.py | 127 ++++++++ maro/rl/learning/rollout_manager.py | 285 ++++++++---------- maro/rl/modeling/core_model.py | 16 +- maro/rl/modeling/fc_block.py | 4 +- maro/rl/policy/__init__.py | 4 +- maro/rl/policy/ac.py | 32 +- maro/rl/policy/ddpg.py | 24 ++ maro/rl/policy/dqn.py | 27 +- maro/rl/policy/pg.py | 39 ++- maro/rl/policy/policy.py | 6 +- 23 files changed, 597 insertions(+), 639 deletions(-) delete mode 100644 examples/rl/scripts/run.sh rename examples/rl/workflows/{learner.py => learning_loop.py} (75%) delete mode 100644 examples/rl/workflows/simple_learner.py delete mode 100644 maro/rl/learning/early_stopper.py delete mode 100644 maro/rl/learning/learner.py create mode 100644 maro/rl/learning/learning_loop.py diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 89d5bd8d1..6be782c4e 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -10,27 +10,15 @@ distributed learning workflows. The distributed workflow can be synchronous or a Synchronous Learning ==================== -In synchronous mode, a central controller executes learning cycles that consist of simulation data collection and -policy update. In a strictly synchronous learning process, a roll-out manager waits for all data collectors, -a.k.a. "roll-out workers", to return their results before moving on to the policy update phase. So what if a slow -worker drags the whole process down? We provide the flexibility to loosen the restriction by specifying the -number of workers required to report back before proceeding to the next phase. After the required number of workers -report back, the roll-out manager can optionally try to receive from the remaining workers a certain number of times -but with a timeout to keep the wait time upperbounded. Note that the transition from the policy update phase to the -data collection phase is always synchronous. This means that in the case of the policy instances distributed amongst -a set of trainer nodes, the central controller waits until all trainers report back with the latest policy states before -starting the next cycle. +In synchronous mode, a main process executes 2-phase learning cycles consisting of simulation data collection and +policy update. The data collection phase is controlled by a roll-out manager and the policy update phase is controlled +by a :ref:`policy-manager`. The transition from one phase to the other is synchrounous. To handle slow roll-out workers, the +roll-out manager can be configured to pass the results from a subset of roll-out workers (i.e., the faster ones) to the +policy manager. On the other hand, the policy manager always waits until all policies are updated before passing the +policy states to the roll-out manager. -The components required for synchronous learning include: - -* Learner, which is the central coordinator for a learning process. The learner consists of a roll-out manager and - a training manager and executes learning cycles that alternate between data collection and policy update. -* Rollout manager, which is responsible for collecting simulation data, in local or distributed fashion. -* Policy manager, which controls the policy update phase of a learning cycle. See "Policy Manager" below for details. - - -.. image:: ../images/rl/learner.svg +.. image:: ../images/rl/learning_cycle.svg :target: ../images/rl/learner.svg :alt: Overview @@ -43,17 +31,12 @@ The components required for synchronous learning include: Asynchronous Learning ===================== -The asynchronous mode consists of a policy server and multiple actors. No central controller is present. Each data collector, -a.k.a., actor, operates independently. As soon as the server receives data from an actor, it feeds the data to the policy -manager for perform updates. The server then returns the updated policy states to the actor for the next round of data collection. -As the actors may differ significantly in speed, the policy server only uses data generated by policies sufficiently up-to-date. -but always sends the latest policy states to every single actor. - -The components required for asynchronous learning include: - -* actor, which alternates between sending collected simulation data to the policy server and receiving updated - policy states from it. -* policy server, which receives data from the actors and update the policy states when necessary. +The asynchronous mode consists of a policy server and multiple actors in a client-server architecture. Each actor runs +its own roll-out loop and periodically sends the collected data to the server. The server uses the data to update the +policies and responds with the latest policy states. As the actors may differ significantly in speed, the policy server +only uses data generated by policies sufficiently up-to-date, but always sends the latest policy states to every single +actor. The asynchronous learning feature is still under development and not ready for production use, as it only +supports a single policy server and hence does not provide fault tolerence. Environment Sampler @@ -64,6 +47,11 @@ and reward shaping to collect roll-out information for learning and testing purp easily turned into a roll-out worker or an actor for synchronous and asynchronous learning, respectively. +.. image:: ../images/rl/env_sampler.svg + :target: ../images/rl/env_sampler.svg + :alt: Overview + + Policy ------ @@ -103,6 +91,7 @@ provides various policy improvement interfaces to support single-threaded and di def improve(self): pass +.. _policy-manager: Policy Manager -------------- @@ -120,11 +109,6 @@ is present as a server process. The types of policy manager include: necessary when the combined size of the policies is too big to fit in a single node. -.. image:: ../images/rl/policy_manager.svg - :target: ../images/rl/policy_manager.svg - :alt: RL Overview - - Core Model ---------- diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index d2a50dc06..0c87d27c7 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -13,133 +13,137 @@ args = parser.parse_args() namespace = args.namespace - path = realpath(__file__) - docker_script_dir = dirname(path) + docker_script_dir = dirname(realpath(__file__)) rl_example_dir = dirname(dirname(docker_script_dir)) root_dir = dirname(dirname(rl_example_dir)) workflow_dir = join(rl_example_dir, "workflows") maro_rl_dir = join(root_dir, "maro", "rl") - maro_comm_dir = join(root_dir, "maro", "communication") - maro_sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") - config_path = join(workflow_dir, "config.yml") - dockerfile_path = join(root_dir, "docker_files", "dev.df") - with open(config_path, "r") as fp: + with open(join(workflow_dir, "config.yml"), "r") as fp: config = yaml.safe_load(fp) - - redis_host = config["redis"]["host"] - docker_compose_manifest = { - "version": "3.9", - "services": {"redis": {"image": "redis:6", "container_name": f"{namespace}.{redis_host}"}} - } + common_spec = { - "build": {"context": root_dir, "dockerfile": dockerfile_path}, + "build": {"context": root_dir, "dockerfile": join(root_dir, "docker_files", "dev.df")}, "image": "marorl", "volumes": [ f"{rl_example_dir}:/maro/rl_examples", - f"{maro_rl_dir}:/maro/maro/rl", - f"{maro_comm_dir}:/maro/maro/communication", - f"{maro_sc_dir}:/maro/maro/simulator/scenarios/supply_chain" + f"{maro_rl_dir}:/maro/maro/rl" ] } - common_env = [ - f"REDISHOST={namespace}.{redis_host}", - f"REDISPORT={config['redis']['port']}", - f"JOB={config['job']}", - f"SCENARIO={config['scenario']}", - f"MODE={config['mode']}", - f"POLICYMANAGERTYPE={config['policy_manager']['type']}" - ] - - if config["mode"] == "async": - num_rollouts = config['async']['num_actors'] - elif config["sync"]["rollout_type"] == "simple": - num_rollouts = config['sync']['simple']['parallelism'] + if config["mode"] == "single": + docker_compose_manifest = {"services": { + "main": { + **common_spec, + **{ + "container_name": f"{namespace}.main", + "command": "python3 /maro/rl_examples/workflows/learning_loop.py", + "environment": [ + f"JOB={config['job']}", + f"SCENARIO={config['scenario']}", + f"MODE=single", + f"NUMEPISODES={config['num_episodes']}", + f"NUMSTEPS={config['num_steps']}", + f"EVALSCH={config['eval_schedule']}", + ] + } + } + }} else: - num_rollouts = config['sync']['distributed']['num_workers'] + redis_host = config["redis"]["host"] + docker_compose_manifest = { + "version": "3.9", + "services": {"redis": {"image": "redis:6", "container_name": f"{namespace}.{redis_host}"}} + } - common_env.append(f"NUMROLLOUTS={num_rollouts}") + common_env = [ + f"REDISHOST={namespace}.{redis_host}", + f"REDISPORT={config['redis']['port']}", + f"JOB={config['job']}", + f"SCENARIO={config['scenario']}", + f"MODE={config['mode']}", + f"POLICYMANAGERTYPE={config['policy_manager']['type']}" + ] - # host spec - if config["policy_manager"]["type"] == "distributed": - common_env.append(f"LEARNGROUP={config['policy_manager']['distributed']['group']}") - common_env.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") - for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): - str_id = f"policy_host.{host_id}" - host_spec = deepcopy(common_spec) - del host_spec["build"] - host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_host.py" - host_spec["container_name"] = f"{namespace}.{str_id}" - host_spec["environment"] = [f"HOSTID={host_id}"] + common_env - docker_compose_manifest["services"][str_id] = host_spec + common_env.append(f"NUMROLLOUTS={config[config['mode']]['num_rollouts']}") + # host spec + if config["policy_manager"]["type"] == "distributed": + common_env.append(f"LEARNGROUP={config['policy_manager']['distributed']['group']}") + common_env.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") + for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): + str_id = f"policy_host.{host_id}" + host_spec = deepcopy(common_spec) + del host_spec["build"] + host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_host.py" + host_spec["container_name"] = f"{namespace}.{str_id}" + host_spec["environment"] = [f"HOSTID={host_id}"] + common_env + docker_compose_manifest["services"][str_id] = host_spec - mode = config["mode"] - if mode == "sync": - # learner_spec - docker_compose_manifest["services"]["learner"] = { - **common_spec, - **{ - "container_name": f"{namespace}.learner", - "command": "python3 /maro/rl_examples/workflows/learner.py", - "environment": [ - f"ROLLOUTTYPE={config['sync']['rollout_type']}", - f"NUMEPISODES={config['num_episodes']}", - f"NUMSTEPS={config['num_steps']}", - f"EVALSCH={config['eval_schedule']}", - f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}", - f"EVALPARALLELISM={config['sync']['simple']['eval_parallelism']}", - f"ROLLOUTGROUP={config['sync']['distributed']['group']}", - f"NUMEVALWORKERS={config['sync']['distributed']['num_eval_workers']}", - f"MAXLAG={config['max_lag']}", - f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", - f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", - f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}", - ] + common_env + mode = config["mode"] + if mode == "sync": + # main process spec + docker_compose_manifest["services"]["main"] = { + **common_spec, + **{ + "container_name": f"{namespace}.main", + "command": "python3 /maro/rl_examples/workflows/learning_loop.py", + "environment": [ + f"ROLLOUTTYPE={config['sync']['rollout_type']}", + f"NUMEPISODES={config['num_episodes']}", + f"NUMSTEPS={config['num_steps']}", + f"EVALSCH={config['eval_schedule']}", + f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}", + f"NUMEVALROLLOUTS={config[config['mode']]['num_eval_rollouts']}", + f"ROLLOUTGROUP={config['sync']['distributed']['group']}", + f"MAXLAG={config['max_lag']}", + f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", + f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", + f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}", + ] + common_env + } } - } - # rollout worker spec - if config["sync"]["rollout_type"] == "distributed": - for worker_id in range(config["sync"]["distributed"]["num_workers"]): - str_id = f"rollout_worker.{worker_id}" - worker_spec = deepcopy(common_spec) - del worker_spec["build"] - worker_spec["command"] = "python3 /maro/rl_examples/workflows/rollout.py" - worker_spec["container_name"] = f"{namespace}.{str_id}" - worker_spec["environment"] = [ - f"WORKERID={worker_id}", - f"ROLLOUTGROUP={config['sync']['distributed']['group']}" - ] + common_env - docker_compose_manifest["services"][str_id] = worker_spec - elif mode == "async": - # policy server spec - docker_compose_manifest["services"]["policy_server"] = { - **common_spec, - **{ - "container_name": f"{namespace}.policy_server", - "command": "python3 /maro/rl_examples/workflows/policy_manager.py", - "environment": [ + # rollout worker spec + if config["sync"]["rollout_type"] == "distributed": + for worker_id in range(config["sync"]["num_rollouts"]): + str_id = f"rollout_worker.{worker_id}" + worker_spec = deepcopy(common_spec) + del worker_spec["build"] + worker_spec["command"] = "python3 /maro/rl_examples/workflows/rollout.py" + worker_spec["container_name"] = f"{namespace}.{str_id}" + worker_spec["environment"] = [ + f"WORKERID={worker_id}", + f"ROLLOUTGROUP={config['sync']['distributed']['group']}" + ] + common_env + docker_compose_manifest["services"][str_id] = worker_spec + elif mode == "async": + # policy server spec + docker_compose_manifest["services"]["policy_server"] = { + **common_spec, + **{ + "container_name": f"{namespace}.policy_server", + "command": "python3 /maro/rl_examples/workflows/policy_manager.py", + "environment": [ + f"GROUP={config['async']['group']}", + f"MAXLAG={config['max_lag']}" + ] + common_env + } + } + # actor spec + for actor_id in range(config["async"]["num_actors"]): + str_id = f"actor.{actor_id}" + actor_spec = deepcopy(common_spec) + del actor_spec["build"] + actor_spec["command"] = "python3 /maro/rl_examples/workflows/rollout.py" + actor_spec["container_name"] = f"{namespace}.{str_id}" + actor_spec["environment"] = [ + f"ACTORID={actor_id}", f"GROUP={config['async']['group']}", - f"MAXLAG={config['max_lag']}" + f"NUMEPISODES={config['num_episodes']}", + f"NUMSTEPS={config['num_steps']}" ] + common_env - } - } - # actor spec - for actor_id in range(config["async"]["num_actors"]): - str_id = f"actor.{actor_id}" - actor_spec = deepcopy(common_spec) - del actor_spec["build"] - actor_spec["command"] = "python3 /maro/rl_examples/workflows/rollout.py" - actor_spec["container_name"] = f"{namespace}.{str_id}" - actor_spec["environment"] = [ - f"ACTORID={actor_id}", - f"GROUP={config['async']['group']}", - f"NUMEPISODES={config['num_episodes']}", - f"NUMSTEPS={config['num_steps']}" - ] + common_env - docker_compose_manifest["services"][str_id] = actor_spec - else: - raise ValueError(f"mode must be 'sync' or 'async', got {mode}") + docker_compose_manifest["services"][str_id] = actor_spec + else: + raise ValueError(f"mode must be 'sync' or 'async', got {mode}") with open(join(docker_script_dir, "yq.yml"), "w") as fp: yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/rl/scripts/run.sh b/examples/rl/scripts/run.sh deleted file mode 100644 index b241ba0e7..000000000 --- a/examples/rl/scripts/run.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -BASEDIR=$(dirname "$0") -python3 $BASEDIR/../workflows/simple_learner.py \ No newline at end of file diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 6d5f14ac2..d39eb2de5 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -3,30 +3,27 @@ job: cim scenario: cim -mode: sync +mode: sync # single, sync, async num_episodes: 5 eval_schedule: 5 num_steps: -1 max_lag: 0 sync: - rollout_type: distributed # simple, distributed - simple: - parallelism: 1 - eval_parallelism: 1 + num_rollouts: 3 + num_eval_rollouts: 1 + rollout_type: distributed # multi-process, distributed distributed: group: rollout - num_workers: 3 - num_eval_workers: 1 min_finished_workers: 2 max_extra_recv_tries: 1 extra_recv_timeout: 200 async: group: async - num_actors: 3 + num_rollouts: 3 policy_manager: - type: simple # simple, distributed + type: distributed # simple, distributed simple: - parallel: false + parallel: true distributed: group: learn num_hosts: 2 diff --git a/examples/rl/workflows/learner.py b/examples/rl/workflows/learning_loop.py similarity index 75% rename from examples/rl/workflows/learner.py rename to examples/rl/workflows/learning_loop.py index 3be94bf4b..d3f19f98b 100644 --- a/examples/rl/workflows/learner.py +++ b/examples/rl/workflows/learning_loop.py @@ -5,7 +5,7 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning import Learner, DistributedRolloutManager, SimpleRolloutManager +from maro.rl.learning import DistributedRolloutManager, MultiProcessRolloutManager, learn workflow_dir = dirname(dirname((realpath(__file__)))) if workflow_dir not in sys.path: @@ -18,19 +18,17 @@ def get_rollout_manager(): rollout_type = getenv("ROLLOUTTYPE", default="simple") num_steps = int(getenv("NUMSTEPS", default=-1)) - if rollout_type == "simple": - return SimpleRolloutManager( + if rollout_type == "multi-process": + return MultiProcessRolloutManager( get_env_sampler, num_steps=num_steps, - parallelism=int(getenv("PARALLELISM", default="1")), - eval_parallelism=int(getenv("EVALPARALLELISM", default="1")), - post_collect=post_collect, - post_evaluate=post_evaluate, + num_rollouts=int(getenv("NUMROLLOUTS", default="1")), + num_eval_rollouts=int(getenv("NUMEVALROLLOUTS", default="1")), log_dir=log_dir ) num_workers = int(getenv("NUMROLLOUTS", default=5)) - num_eval_workers = int(getenv("NUMEVALWORKERS", default=1)) + num_eval_workers = int(getenv("NUMEVALROLLOUTS", default=1)) max_lag = int(getenv("MAXLAG", default=0)) min_finished_workers = getenv("MINFINISH") if min_finished_workers is not None: @@ -54,8 +52,6 @@ def get_rollout_manager(): min_finished_workers=min_finished_workers, max_extra_recv_tries=max_extra_recv_tries, extra_recv_timeout=extra_recv_timeout, - post_collect=post_collect, - post_evaluate=post_evaluate, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 @@ -68,13 +64,14 @@ def get_rollout_manager(): if __name__ == "__main__": num_episodes = getenv("NUMEPISODES") if num_episodes is None: - raise ValueError("Missing envrionment variable: NUMEPISODES") + raise ValueError("Missing environment variable: NUMEPISODES") - learner = Learner( - get_policy_manager(), - get_rollout_manager(), + learn( + get_rollout_manager if getenv("MODE") != "single" else get_env_sampler, int(num_episodes), + get_policy_manager=get_policy_manager if getenv("MODE") != "single" else None, eval_schedule=int(getenv("EVALSCH")), + post_collect=post_collect, + post_evaluate=post_evaluate, log_dir=log_dir ) - learner.run() diff --git a/examples/rl/workflows/simple_learner.py b/examples/rl/workflows/simple_learner.py deleted file mode 100644 index 64821676e..000000000 --- a/examples/rl/workflows/simple_learner.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import sys -import yaml -from os.path import dirname, join, realpath - -from maro.rl.learning import simple_learner - -workflow_dir = dirname((realpath(__file__))) -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -with open(join(workflow_dir, "config.yml"), "r") as fp: - config = yaml.safe_load(fp) - -rl_example_dir = dirname(workflow_dir) -if rl_example_dir not in sys.path: - sys.path.insert(0, rl_example_dir) - -log_dir = join(rl_example_dir, "log", config["job"]) - -module = importlib.import_module(config["scenario"]) -get_env_sampler = getattr(module, "get_env_sampler") -policy_func_dict = getattr(module, "policy_func_dict") -post_collect = getattr(module, "post_collect", None) -post_evaluate = getattr(module, "post_evaluate", None) - - -if __name__ == "__main__": - simple_learner( - get_env_sampler, - num_episodes=config["num_episodes"], - num_steps=config["num_steps"], - eval_schedule=config["eval_schedule"], - post_collect=post_collect, - post_evaluate=post_evaluate, - log_dir=log_dir - ) diff --git a/maro/rl/exploration/scheduling.py b/maro/rl/exploration/scheduling.py index 08438bc22..5b32528c7 100644 --- a/maro/rl/exploration/scheduling.py +++ b/maro/rl/exploration/scheduling.py @@ -12,8 +12,8 @@ class AbsExplorationScheduler(ABC): exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. - initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is - instantiated with will be used as the initial value. Defaults to None. + initial_value: Initial value for the exploration parameter. If None, the value from the original dictionary + the policy is instantiated with will be used as the initial value. Defaults to None. """ def __init__(self, exploration_params: dict, param_name: str, initial_value=None): @@ -40,8 +40,8 @@ class LinearExplorationScheduler(AbsExplorationScheduler): last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. start_ep (int): starting episode. Defaults to 1. - initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is - instantiated with will be used as the initial value. Defaults to None. + initial_value: Initial value for the exploration parameter. If None, the value from the original dictionary + the policy is instantiated with will be used as the initial value. Defaults to None. """ def __init__( @@ -81,8 +81,8 @@ class MultiLinearExplorationScheduler(AbsExplorationScheduler): last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. start_ep (int): starting episode. Defaults to 1. - initial_value: Initial value for the exploration parameter. If None, the value the exploration instance is - instantiated with will be used as the initial value. Defaults to None. + initial_value: Initial value for the exploration parameter. If None, the value from the original dictionary + the policy is instantiated with will be used as the initial value. Defaults to None. Returns: An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. diff --git a/maro/rl/exploration/strategies.py b/maro/rl/exploration/strategies.py index ecda1ee3d..aa822c86f 100644 --- a/maro/rl/exploration/strategies.py +++ b/maro/rl/exploration/strategies.py @@ -6,7 +6,7 @@ import numpy as np -def epsilon_greedy(state: np.ndarray, action: np.ndarray, num_actions, *, epsilon: float): +def epsilon_greedy(state: np.ndarray, action: np.ndarray, num_actions, *, epsilon: float) -> np.ndarray: """epsilon-greedy exploration. Args: @@ -16,6 +16,9 @@ def epsilon_greedy(state: np.ndarray, action: np.ndarray, num_actions, *, epsilo action (np.ndarray): Action(s) chosen greedily by the policy. num_actions (int): Number of possible actions. epsilon (float): The probability that a random action will be selected. + + Returns: + Exploratory actions. """ return np.array([act if np.random.random() > epsilon else np.random.randint(num_actions) for act in action]) @@ -40,11 +43,14 @@ def uniform_noise( max_action (Union[float, list, np.ndarray]): Upper bound for the multi-dimensional action space. low (Union[float, list, np.ndarray]): Lower bound for the noise range. high (Union[float, list, np.ndarray]): Upper bound for the noise range. + + Returns: + Exploration actions with added noise. """ if min_action is None and max_action is None: - return action + np.random.uniform(low, high) + return action + np.random.uniform(low, high, size=action.shape) else: - return np.clip(action + np.random.uniform(low, high), min_action, max_action) + return np.clip(action + np.random.uniform(low, high, size=action.shape), min_action, max_action) def gaussian_noise( @@ -68,11 +74,13 @@ def gaussian_noise( max_action (Union[float, list, np.ndarray]): Upper bound for the multi-dimensional action space. mean (Union[float, list, np.ndarray]): Gaussian noise mean. Defaults to .0. stddev (Union[float, list, np.ndarray]): Standard deviation for the Gaussian noise. Defaults to 1.0. - relative (bool): If True, the generated noise will be multiplied by the action itself before being added to - the action. Defaults to False. + relative (bool): If True, the generated noise is treated as a relative measure and will be multiplied by the + action itself before being added to the action. Defaults to False. + Returns: + Exploration actions with added noise. """ - noise = np.random.normal(loc=mean, scale=stddev) + noise = np.random.normal(loc=mean, scale=stddev, size=action.shape) if min_action is None and max_action is None: return action + ((noise * action) if relative else noise) else: diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index 8bf013138..c0410e398 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -1,16 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .early_stopper import AbsEarlyStopper from .env_sampler import AbsEnvSampler -from .learner import Learner, simple_learner +from .learning_loop import learn from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager, policy_host -from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, SimpleRolloutManager +from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, MultiProcessRolloutManager __all__ = [ - "AbsEarlyStopper", "AbsEnvSampler", - "Learner", "simple_learner", + "learn", "AbsPolicyManager", "DistributedPolicyManager", "SimplePolicyManager", "policy_host", - "AbsRolloutManager", "DistributedRolloutManager", "SimpleRolloutManager" + "AbsRolloutManager", "DistributedRolloutManager", "MultiProcessRolloutManager" ] diff --git a/maro/rl/learning/early_stopper.py b/maro/rl/learning/early_stopper.py deleted file mode 100644 index 20cfb1561..000000000 --- a/maro/rl/learning/early_stopper.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod - - -class AbsEarlyStopper(ABC): - def __init__(self): - super().__init__() - self.metric_history = [] - - def push(self, metric): - self.metric_history.append(metric) - - @abstractmethod - def stop(self) -> bool: - raise NotImplementedError diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index d80f30a78..88258d916 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -19,7 +19,7 @@ class AgentWrapper: - """Wrapper for multiple agents using multiple policies that exposes single-agent interfaces.""" + """Wrapper for multiple agents using multiple policies to expose simple single-agent interfaces.""" def __init__( self, get_policy_func_dict: Dict[str, Callable], @@ -200,6 +200,9 @@ class AbsEnvSampler(ABC): instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition is a ``Transition`` object. For example, this callback can be used to collect various statistics on the simulation. Defaults to None. + policies_to_parallelize (List[str]): Policies to be placed in separate processes so that inference can be + performed in parallel to speed up simulation. This is useful if some policies are big and takes long times + to compute actions. Defaults to an empty list. """ def __init__( self, @@ -348,14 +351,26 @@ def test(self, policy_state_dict: dict = None): return self.tracker - def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = getcwd()): + def worker( + self, + group: str, + index: int, + num_extra_recv_attempts: int = 0, + recv_timeout: int = 100, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() + ): """Roll-out worker process that can be launched on separate computation nodes. Args: group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager that manages them. - worker_idx (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". - This is used for bookkeeping by the parent manager. + index (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". + This is used for bookkeeping by the roll-out manager. + num_extra_recv_attempts (int): Number of extra receive attempts after each received ``SAMPLE`` message. This + is used to catch the worker up to the latest episode in case it trails the main learning loop by at + least one full episode. Defaults to 0. + recv_timeout (int): Timeout for the extra receive attempts. Defaults to 100 (miliseconds). proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. @@ -374,31 +389,38 @@ def worker(self, group: str, index: int, proxy_kwargs: dict = {}, log_dir: str = 3) EXIT, upon which it will break out of the event loop and the process will terminate. """ - for msg in proxy.receive(): + while True: + msg = proxy.receive_once() if msg.tag == MsgTag.EXIT: logger.info("Exiting...") proxy.close() break if msg.tag == MsgTag.SAMPLE: - ep = msg.body[MsgKey.EPISODE] + latest = msg + for _ in range(num_extra_recv_attempts): + msg = proxy.receive_once(timeout=recv_timeout) + if msg.body[MsgKey.EPISODE] > latest.body[MsgKey.EPISODE]: + logger.info(f"Skipped roll-out message for ep {latest.body[MsgKey.EPISODE]}") + latest = msg + + ep = latest.body[MsgKey.EPISODE] result = self.sample( - policy_state_dict=msg.body[MsgKey.POLICY_STATE], - num_steps=msg.body[MsgKey.NUM_STEPS] + policy_state_dict=latest.body[MsgKey.POLICY_STATE], num_steps=latest.body[MsgKey.NUM_STEPS] ) logger.info( get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) ) return_info = { MsgKey.EPISODE: ep, - MsgKey.SEGMENT: msg.body[MsgKey.SEGMENT], - MsgKey.VERSION: msg.body[MsgKey.VERSION], + MsgKey.SEGMENT: latest.body[MsgKey.SEGMENT], + MsgKey.VERSION: latest.body[MsgKey.VERSION], MsgKey.ROLLOUT_INFO: result["rollout_info"], MsgKey.STEP_RANGE: result["step_range"], MsgKey.TRACKER: result["tracker"], MsgKey.END_OF_EPISODE: result["end_of_episode"] } - proxy.reply(msg, tag=MsgTag.SAMPLE_DONE, body=return_info) + proxy.reply(latest, tag=MsgTag.SAMPLE_DONE, body=return_info) elif msg.tag == MsgTag.TEST: tracker = self.test(msg.body[MsgKey.POLICY_STATE]) return_info = {MsgKey.TRACKER: tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} diff --git a/maro/rl/learning/helpers.py b/maro/rl/learning/helpers.py index a4990991f..55fa39b12 100644 --- a/maro/rl/learning/helpers.py +++ b/maro/rl/learning/helpers.py @@ -5,6 +5,7 @@ def get_rollout_finish_msg(ep, step_range, exploration_params=None): + """Generate a brief summary message for a finished roll-out""" if exploration_params: return ( f"Roll-out finished (episode {ep}, " @@ -15,6 +16,21 @@ def get_rollout_finish_msg(ep, step_range, exploration_params=None): def get_eval_schedule(sch: Union[int, List[int]], num_episodes: int, final: bool = True): + """Helper function to the policy evaluation schedule. + + Args: + sch (Union[int, List[int]]): Evaluation schedule. If it is an int, it is treated as the number of episodes + between two adjacent evaluations. For example, if the total number of episodes is 20 and ``sch`` is 6, + this will return [6, 12, 18] if ``final`` is False or [6, 12, 18, 20] otherwise. If it is a list, it will + return a sorted version of the list (with the last episode appended if ``final`` is True). + num_episodes (int): Total number of learning episodes. + final (bool): If True, the last episode number will be appended to the returned list to indicate that an + evaluation is required after the last episode is complete. Defaults to True. + + Returns: + A list of episodes indicating when to perform policy evaluation. + + """ if sch is None: schedule = [] elif isinstance(sch, int): diff --git a/maro/rl/learning/learner.py b/maro/rl/learning/learner.py deleted file mode 100644 index a5c52cb64..000000000 --- a/maro/rl/learning/learner.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import Callable, List, Union - -from maro.utils import Logger - -from .early_stopper import AbsEarlyStopper -from .env_sampler import AbsEnvSampler -from .helpers import get_eval_schedule, get_rollout_finish_msg -from .policy_manager import AbsPolicyManager -from .rollout_manager import AbsRolloutManager - - -def simple_learner( - get_env_sampler: Callable[[], AbsEnvSampler], - num_episodes: int, - num_steps: int = -1, - eval_schedule: Union[int, List[int]] = None, - eval_after_last_episode: bool = True, - early_stopper: AbsEarlyStopper = None, - post_collect: Callable = None, - post_evaluate: Callable = None, - log_dir: str = getcwd() -): - """Single-threaded learning workflow. - - Args: - get_env_sampler (Callable): Function to create an environment wrapper for collecting training data. The function - should take no parameters and return an environment wrapper instance. - get_agent_wrapper (Callable): Function to create an agent wrapper that interacts with the environment wrapper. - The function should take no parameters and return a ``AgentWrapper`` instance. - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on how the implementation of the roll-out manager. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - get_test_env_wrapper (Callable): Function to create an environment wrapper for evaluation. The function should - take no parameters and return an environment wrapper instance. If this is None, the training environment - wrapper will be used for evaluation in the worker processes. Defaults to None. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the episodes given in the list. Defaults to None, in which case no evaluation is performed - unless ``eval_after_last_episode`` is set to True. - eval_after_last_episode (bool): If True, the policy will be evaluated after the last episode of learning is - finished. Defaults to True. - early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metrics are met following an evaluation episode. Default to None. - post_collect (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. - post_evaluate (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers,) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - logger = Logger("LOCAL_LEARNER", dump_folder=log_dir) - env_sampler = get_env_sampler() - - # evaluation schedule - eval_schedule = get_eval_schedule(eval_schedule, num_episodes, final=eval_after_last_episode) - logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") - eval_point_index = 0 - - def collect_and_update(ep): - """Collect simulation data for training.""" - segment = 1 - while True: - result = env_sampler.sample(num_steps=num_steps, return_rollout_info=False) - logger.info( - get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) - ) - env_sampler.agent_wrapper.improve() - - if post_collect: - post_collect([result["tracker"]], ep, segment) - - if result["end_of_episode"]: - break - - segment += 1 - - for ep in range(1, num_episodes + 1): - collect_and_update(ep) - if ep == eval_schedule[eval_point_index]: - eval_point_index += 1 - tracker = env_sampler.test() - if post_evaluate: - post_evaluate([tracker], eval_point_index) - # early stopping check - if early_stopper: - early_stopper.push(env_sampler.test_env.summary) - if early_stopper.stop(): - return - - -class Learner: - """Main controller for learning. - - This should be used in multi-process or distributed settings where either the policy manager or the roll-out - manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such - as duplicate experience storage. Use ``SimpleLearner`` instead. - - Args: - policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. - rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data - collection. - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on the implementation of the roll-out manager. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the episodes given in the list. Defaults to None, in which case no evaluation is performed - unless ``eval_after_last_episode`` is set to True. - eval_after_last_episode (bool): If True, the policy will be evaluated after the last episode of learning is - finished. Defaults to False. - early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metric are met following an evaluation episode. Default to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time - and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - def __init__( - self, - policy_manager: AbsPolicyManager, - rollout_manager: AbsRolloutManager, - num_episodes: int, - eval_schedule: Union[int, List[int]] = None, - eval_after_last_episode: bool = False, - early_stopper: AbsEarlyStopper = None, - log_dir: str = getcwd() - ): - self._logger = Logger("LEARNER", dump_folder=log_dir) - self.policy_manager = policy_manager - self.rollout_manager = rollout_manager - - self.num_episodes = num_episodes - - # evaluation schedule - self._eval_schedule = get_eval_schedule(eval_schedule, num_episodes, final=eval_after_last_episode) - self._logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") - self._eval_point_index = 0 - - self.early_stopper = early_stopper - - def run(self): - """Entry point for executing a learning workflow.""" - for ep in range(1, self.num_episodes + 1): - self._collect_and_update(ep) - if ep == self._eval_schedule[self._eval_point_index]: - self._eval_point_index += 1 - env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) - # early stopping check - if self.early_stopper: - for env_metric in env_metric_dict.values(): - self.early_stopper.push(env_metric) - if self.early_stopper.stop(): - return - - if hasattr(self.rollout_manager, "exit"): - self.rollout_manager.exit() - - if hasattr(self.policy_manager, "exit"): - self.policy_manager.exit() - - def _collect_and_update(self, ep: int): - collect_time = policy_update_time = 0 - self.rollout_manager.reset() - segment = 1 - while not self.rollout_manager.episode_complete: - # experience collection - policy_state_dict, policy_version = self.policy_manager.get_state(), self.policy_manager.get_version() - tc0 = time.time() - rollout_info_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) - collect_time += time.time() - tc0 - tu0 = time.time() - self.policy_manager.update(rollout_info_by_policy) - policy_update_time += time.time() - tu0 - segment += 1 - - # performance details - self._logger.info( - f"ep {ep} summary - " - f"collect time: {collect_time} " - f"policy update time: {policy_update_time}" - ) diff --git a/maro/rl/learning/learning_loop.py b/maro/rl/learning/learning_loop.py new file mode 100644 index 000000000..a075ce139 --- /dev/null +++ b/maro/rl/learning/learning_loop.py @@ -0,0 +1,127 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from os import getcwd +from typing import Callable, List, Union + +from maro.utils import Logger + +from .env_sampler import AbsEnvSampler +from .helpers import get_eval_schedule, get_rollout_finish_msg +from .policy_manager import AbsPolicyManager +from .rollout_manager import AbsRolloutManager + + +def learn( + get_rollout_manager: Callable[[], Union[AbsEnvSampler, AbsRolloutManager]], + num_episodes: int, + num_steps: int = -1, + get_policy_manager: Callable[[], AbsPolicyManager] = None, + eval_schedule: Union[int, List[int]] = None, + eval_after_last_episode: bool = False, + post_collect: Callable = None, + post_evaluate: Callable = None, + log_dir: str = getcwd() +): + """Run the main learning loop in single-threaded or distributed mode. + + In distributed mode, this is the main process that executes 2-phase learning cycles: simulation data collection + and policy update. The transition from one phase to another is synchronous. + + Args: + get_rollout_manager (AbsRolloutManager): Function to create an ``AbsEnvSampler`` or ``AbsRolloutManager`` + instance to control the data collecting phase of the learning cycle. The function takes no parameters + and returns an ``AbsRolloutManager``. Use this for multi-process or distributed policy training. + num_episodes (int): Number of learning episodes. The environment always runs to completion in each episode. + num_steps (int): Number of environment steps to roll out each time. Defaults to -1, in which + case the roll-out will be executed until the end of the environment. + get_policy_manager (AbsPolicyManager): Function to create an ``AbsPolicyManager`` instance to control policy + update phase of the learning cycle. The function takes no parameters and returns an ``AbsPolicyManager``. + Use this for multi-process or distributed data collection. Defaults to None. + eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will + will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated + at the end of the episodes given in the list. Defaults to None, in which case no evaluation is performed + unless ``eval_after_last_episode`` is set to True. + eval_after_last_episode (bool): If True, the policy will be evaluated after the last episode of learning is + finished. Defaults to False. + early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the + environment metric are met following an evaluation episode. Default to None. + post_collect (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should + be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. + Defaults to None. + post_evaluate (Callable): Custom function to process whatever information is collected by each + environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should + be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults + to None. + log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time + and this directory will be used to save the log files generated by it. Defaults to the current working + directory. + """ + if num_steps == 0 or num_steps < -1: + raise ValueError("num_steps must be a positive integer or -1") + + rollout_manager = get_rollout_manager() + if not get_policy_manager: + assert isinstance(rollout_manager, AbsEnvSampler), \ + "'get_rollout_manager' must return an 'AbsEnvSampler' if 'get_policy_manager' is None." + + policy_manager = get_policy_manager() if get_policy_manager else None + logger = Logger("LEARNER", dump_folder=log_dir) + # evaluation schedule + eval_schedule = get_eval_schedule(eval_schedule, num_episodes, final=eval_after_last_episode) + logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") + eval_point_index = 0 + + def collect_and_update(): + collect_time = policy_update_time = 0 + if isinstance(rollout_manager, AbsRolloutManager): + rollout_manager.reset() + segment, end_of_episode = 1, False + while not end_of_episode: + # experience collection + tc0 = time.time() + if policy_manager: + policy_state_dict, version = policy_manager.get_state(), policy_manager.get_version() + rollout_info_by_policy, trackers = rollout_manager.collect(ep, segment, policy_state_dict, version) + end_of_episode = rollout_manager.end_of_episode + else: + result = rollout_manager.sample(num_steps=num_steps, return_rollout_info=False) + trackers = [result["tracker"]] + logger.info( + get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) + ) + end_of_episode = result["end_of_episode"] + + if post_collect: + post_collect(trackers, ep, segment) + + collect_time += time.time() - tc0 + tu0 = time.time() + if policy_manager: + policy_manager.update(rollout_info_by_policy) + else: + rollout_manager.agent_wrapper.improve() + policy_update_time += time.time() - tu0 + segment += 1 + + # performance details + logger.info(f"ep {ep} summary - collect time: {collect_time}, policy update time: {policy_update_time}") + + for ep in range(1, num_episodes + 1): + collect_and_update() + if ep == eval_schedule[eval_point_index]: + eval_point_index += 1 + if isinstance(rollout_manager, AbsEnvSampler): + trackers = [rollout_manager.test()] + else: + trackers = rollout_manager.evaluate(ep, policy_manager.get_state()) + if post_evaluate: + post_evaluate(trackers, ep) + + if isinstance(rollout_manager, AbsRolloutManager): + rollout_manager.exit() + + if hasattr(policy_manager, "exit"): + policy_manager.exit() diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index 42523cc22..f75c81752 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -6,7 +6,7 @@ from multiprocessing import Pipe, Process from os import getcwd from random import choices -from typing import Callable, List +from typing import Callable, Dict, List, Tuple import numpy as np @@ -23,72 +23,60 @@ def concat_batches(batch_list: List[dict]): class AbsRolloutManager(ABC): - """Controller for simulation data collection. - - Args: - post_collect (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. - Defaults to None. - post_evaluate (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. - """ - def __init__(self, post_collect: Callable = None, post_evaluate: Callable = None): + """Controller for simulation data collection.""" + def __init__(self): super().__init__() - self._post_collect = post_collect - self._post_evaluate = post_evaluate - self.episode_complete = False + self.end_of_episode = False @abstractmethod - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) -> Tuple[Dict, List[Dict]]: """Collect simulation data, i.e., experiences for training. Args: - ep (int): Current episode index. - segment (int): Current segment index. - policy_state_dict (dict): Policy states to use for simulation. - version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. + ep (int): Current episode. + segment (int): Current segment. + policy_state_dict (dict): Policy states to use for collecting training info. + version (int): Version for the policy states. Returns: - Experiences for policy training. + A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries + containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. + An RL policy's roll-out information must be either loss information or a data batch that can be passed to + the policy's ``update`` or ``learn``, respectively. """ raise NotImplementedError @abstractmethod def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``. + """Evaluate policy performance. Args: - ep (int): Current training episode index. - policy_state_dict (dict): Policy states to use for simulation. + ep (int): Current training episode. + policy_state_dict (dict): Policy states to use for evaluation. Returns: - Environment summary. + A list of dictionaries containing step-level information collected by the user-defined ``post_step`` + callback in ``AbsEnvSampler`` for evaluation purposes. """ raise NotImplementedError def reset(self): - self.episode_complete = False + self.end_of_episode = False + + def exit(self): + pass -class SimpleRolloutManager(AbsRolloutManager): +class MultiProcessRolloutManager(AbsRolloutManager): """Local roll-out controller. Args: get_env_sampler (Callable): Function to create an environment sampler for collecting training data. The function should take no parameters and return an ``AbsEnvSampler`` instance. + num_rollouts (int): Number of processes to spawn for parallel roll-out. num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which case the roll-out will be executed until the end of the environment. - post_collect (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. - Defaults to None. - post_evaluate (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. + num_eval_rollout (int): Number of roll-out processes to use for evaluation. Defaults to 1. log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -96,147 +84,122 @@ class SimpleRolloutManager(AbsRolloutManager): def __init__( self, get_env_sampler: Callable[[], AbsEnvSampler], + num_rollouts: int, num_steps: int = -1, - parallelism: int = 1, - eval_parallelism: int = 1, - post_collect: Callable = None, - post_evaluate: Callable = None, + num_eval_rollouts: int = 1, log_dir: str = getcwd() ): if num_steps == 0 or num_steps < -1: raise ValueError("num_steps must be a positive integer or -1.") - if parallelism < 1: - raise ValueError("'parallelism' must be equal to or greater than 1.") + if num_rollouts <= 1: + raise ValueError("'num_rollouts' must be greater than 1.") - if eval_parallelism > parallelism: - raise ValueError("'num_eval_workers' can not be greater than 'parallelism'.") + if num_eval_rollouts > num_rollouts: + raise ValueError("'num_eval_rollouts' can not be greater than 'num_rollouts'.") - super().__init__(post_collect=post_collect, post_evaluate=post_evaluate) + super().__init__() self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) self._num_steps = num_steps if num_steps > 0 else float("inf") - self._parallelism = parallelism - self._eval_parallelism = eval_parallelism - if self._parallelism == 1: - self.env_sampler = get_env_sampler() - else: - self._worker_processes = [] - self._manager_ends = [] - - def _rollout_worker(index, conn, get_env_sampler): - set_seeds(index) - env_sampler = get_env_sampler() - logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) - while True: - msg = conn.recv() - if msg["type"] == "sample": - result = env_sampler.sample(policy_state_dict=msg["policy_state"], num_steps=self._num_steps) - logger.info(get_rollout_finish_msg( - msg["episode"], result["step_range"], exploration_params=result["exploration_params"] - )) - result["worker_index"] = index - conn.send(result) - elif msg["type"] == "test": - tracker = env_sampler.test(msg["policy_state"]) - logger.info("Evaluation...") - conn.send({"worker_id": index, "tracker": tracker}) - elif msg["type"] == "quit": - break - - for index in range(self._parallelism): - manager_end, worker_end = Pipe() - self._manager_ends.append(manager_end) - worker = Process(target=_rollout_worker, args=(index, worker_end, get_env_sampler)) - self._worker_processes.append(worker) - worker.start() - - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): + self._num_rollouts = num_rollouts + self._num_eval_rollouts = num_eval_rollouts + self._worker_processes = [] + self._manager_ends = [] + + def _rollout_worker(index, conn, get_env_sampler): + set_seeds(index) + env_sampler = get_env_sampler() + logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) + while True: + msg = conn.recv() + if msg["type"] == "sample": + result = env_sampler.sample(policy_state_dict=msg["policy_state"], num_steps=self._num_steps) + logger.info(get_rollout_finish_msg( + msg["episode"], result["step_range"], exploration_params=result["exploration_params"] + )) + result["worker_index"] = index + conn.send(result) + elif msg["type"] == "test": + logger.info("Evaluating...") + tracker = env_sampler.test(msg["policy_state"]) + conn.send({"worker_id": index, "tracker": tracker}) + elif msg["type"] == "quit": + break + + for index in range(self._num_rollouts): + manager_end, worker_end = Pipe() + self._manager_ends.append(manager_end) + worker = Process(target=_rollout_worker, args=(index, worker_end, get_env_sampler)) + self._worker_processes.append(worker) + worker.start() + + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) -> Tuple[Dict, List[Dict]]: """Collect simulation data, i.e., experiences for training. Args: - ep (int): Current episode index. - segment (int): Current segment index. - policy_state_dict (dict): Policy states to use for simulation. - version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. + ep (int): Current episode. + segment (int): Current segment. + policy_state_dict (dict): Policy states to use for collecting training info. + version (int): Version for the policy states. Returns: - Experiences for policy training. + A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries + containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. + An RL policy's roll-out information must be either loss information or a data batch that can be passed to + the policy's ``update`` or ``learn``, respectively. """ self._logger.info(f"Collecting simulation data (episode {ep}, policy version {version})") info_by_policy, trackers = defaultdict(list), [] - if self._parallelism == 1: - result = self.env_sampler.sample(policy_state_dict=policy_state_dict, num_steps=self._num_steps) - self._logger.info( - get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) - ) + rollout_req = { + "type": "sample", + "episode": ep, + "num_steps": self._num_steps, + "policy_state": policy_state_dict + } + + for conn in self._manager_ends: + conn.send(rollout_req) + for conn in self._manager_ends: + result = conn.recv() for policy_id, info in result["rollout_info"].items(): info_by_policy[policy_id].append(info) trackers.append(result["tracker"]) - self.episode_complete = result["end_of_episode"] - else: - rollout_req = { - "type": "sample", - "episode": ep, - "num_steps": self._num_steps, - "policy_state": policy_state_dict - } - - for conn in self._manager_ends: - conn.send(rollout_req) - - for conn in self._manager_ends: - result = conn.recv() - for policy_id, info in result["rollout_info"].items(): - info_by_policy[policy_id].append(info) - trackers.append(result["tracker"]) - self.episode_complete = result["episode_end"] - - if self._post_collect: - self._post_collect(trackers, ep, segment) + self.end_of_episode = result["end_of_episode"] # concat batches from different roll-out workers for policy_id, info_list in info_by_policy.items(): if "loss" not in info_list[0]: info_by_policy[policy_id] = concat_batches(info_list) - return info_by_policy + return info_by_policy, trackers def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``. + """Evaluate policy performance. Args: - ep (int): Current training episode index. - policy_state_dict (dict): Policy states to use for simulation. + ep (int): Current training episode. + policy_state_dict (dict): Policy states to use for evaluation. Returns: - Environment summary. + A list of dictionaries containing step-level information collected by the user-defined ``post_step`` + callback in ``AbsEnvSampler`` for evaluation purposes. """ trackers = [] - if self._eval_parallelism == 1: - self._logger.info("Evaluating...") - tracker = self.env_sampler.test(policy_state_dict) - trackers.append(tracker) - else: - eval_worker_conns = choices(self._manager_ends, k=self._eval_parallelism) - for conn in eval_worker_conns: - conn.send({"type": "test", "episode": ep, "policy_state": policy_state_dict}) - - for conn in self._manager_ends: - result = conn.recv() - trackers.append(result["tracker"]) - - if self._post_evaluate: - self._post_evaluate(trackers, ep) + eval_worker_conns = choices(self._manager_ends, k=self._num_eval_rollouts) + for conn in eval_worker_conns: + conn.send({"type": "test", "policy_state": policy_state_dict}) + for conn in eval_worker_conns: + result = conn.recv() + trackers.append(result["tracker"]) return trackers def exit(self): """Tell the worker processes to exit.""" - if self._parallelism > 1: - for conn in self._manager_ends: - conn.send({"type": "quit"}) + for conn in self._manager_ends: + conn.send({"type": "quit"}) class DistributedRolloutManager(AbsRolloutManager): @@ -259,14 +222,6 @@ class DistributedRolloutManager(AbsRolloutManager): Experiences collected using policy versions older than (current_version - max_lag) will be discarded. Defaults to 0, in which case only experiences collected using the latest policy version will be returned. num_eval_workers (int): Number of workers for evaluation. Defaults to 1. - post_collect (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers, ep, step_range) -> None, where tracker is a list of environment wrappers' ``tracker`` members. - Defaults to None. - post_evaluate (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init @@ -283,15 +238,13 @@ def __init__( extra_recv_timeout: int = None, max_lag: int = 0, num_eval_workers: int = 1, - post_collect: Callable = None, - post_evaluate: Callable = None, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): if num_eval_workers > num_workers: raise ValueError("num_eval_workers cannot exceed the number of available workers") - super().__init__(post_collect=post_collect, post_evaluate=post_evaluate) + super().__init__() self._num_workers = num_workers peers = {"rollout_worker": num_workers} self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) @@ -315,17 +268,20 @@ def __init__( self._max_lag = max_lag self._num_eval_workers = num_eval_workers - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): + def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) -> Tuple[Dict, List[Dict]]: """Collect simulation data, i.e., experiences for training. Args: - ep (int): Current episode index. - segment (int): Current segment index. - policy_state_dict (dict): Policy states to use for simulation. - version (int): Version index from the policy manager from which the ``policy_state_dict`` is obtained. + ep (int): Current episode. + segment (int): Current segment. + policy_state_dict (dict): Policy states to use for collecting training info. + version (int): Version for the policy states. Returns: - Experiences for policy training. + A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries + containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. + An RL policy's roll-out information must be either loss information or a data batch that can be passed to + the policy's ``update`` or ``learn``, respectively. """ msg_body = { MsgKey.EPISODE: ep, @@ -365,15 +321,12 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int): if num_finishes == self._num_workers: break - if self._post_collect: - self._post_collect(trackers, ep, segment) - # concat batches from different roll-out workers for policy_id, info_list in info_by_policy.items(): if "loss" not in info_list[0]: info_by_policy[policy_id] = concat_batches(info_list) - return info_by_policy + return info_by_policy, trackers def _handle_worker_result(self, msg, ep, segment, version): if msg.tag != MsgTag.SAMPLE_DONE: @@ -392,20 +345,21 @@ def _handle_worker_result(self, msg, ep, segment, version): # The message is what we expect if msg.body[MsgKey.EPISODE] == ep and msg.body[MsgKey.SEGMENT] == segment: - self.episode_complete = msg.body[MsgKey.END_OF_EPISODE] + self.end_of_episode = msg.body[MsgKey.END_OF_EPISODE] return msg.body[MsgKey.ROLLOUT_INFO], msg.body[MsgKey.TRACKER] return None, None def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate the performance of ``policy_state_dict``. + """Evaluate policy performance. Args: - ep (int): Current training episode index. - policy_state_dict (dict): Policy states to use for simulation. + ep (int): Current training episode. + policy_state_dict (dict): Policy states to use for evaluation. Returns: - Environment summary. + A list of dictionaries containing step-level information collected by the user-defined ``post_step`` + callback in ``AbsEnvSampler`` for evaluation purposes. """ msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY_STATE: policy_state_dict} @@ -419,8 +373,8 @@ def evaluate(self, ep: int, policy_state_dict: dict): for msg in self._proxy.receive(): if msg.tag != MsgTag.TEST_DONE or msg.body[MsgKey.EPISODE] != ep: self._logger.info( - f"Ignore a message of type {msg.tag} with episode index {msg.body[MsgKey.EPISODE]} " - f"(expected message type {MsgTag.TEST_DONE} and episode index {ep})" + f"Ignore a message of type {msg.tag} with episode {msg.body[MsgKey.EPISODE]} " + f"(expected message type {MsgTag.TEST_DONE} and episode {ep})" ) continue @@ -430,9 +384,6 @@ def evaluate(self, ep: int, policy_state_dict: dict): if num_finishes == self._num_eval_workers: break - if self._post_evaluate: - self._post_evaluate(trackers, ep) - return trackers def exit(self): diff --git a/maro/rl/modeling/core_model.py b/maro/rl/modeling/core_model.py index 3513906fe..e7644e26f 100644 --- a/maro/rl/modeling/core_model.py +++ b/maro/rl/modeling/core_model.py @@ -8,7 +8,7 @@ class AbsCoreModel(nn.Module): - """General model abstraction for use in RL algorithms.""" + """General model abstraction for use in deep RL algorithms.""" def __init__(self): super().__init__() @@ -18,7 +18,7 @@ def forward(self, *args, **kwargs): @abstractmethod def step(self, loss: torch.tensor): - """Use the loss to back-propagate gradients and apply them to the underlying parameters. + """Use a computed loss to back-propagate gradients and apply them to the underlying parameters. Args: loss: Result of a computation graph that involves the underlying parameters. @@ -26,7 +26,19 @@ def step(self, loss: torch.tensor): raise NotImplementedError def get_gradients(self, loss: torch.tensor): + """Get gradients from a computed loss. + + There are two possible scenarios where you need to implement this interface: 1) if you are doing distributed + learning and want each roll-out instance to collect gradients that can be directly applied to policy parameters + on the learning side (abstracted through ``AbsPolicyManager``); 2) if you are computing loss in data-parallel fashion, + i.e., by splitting a data batch to several smaller batches and sending them to a set of remote workers for + parallelized gradient computation. In this case, this method will be used by the remote workers. + """ pass def apply_gradients(self, grad: dict): + """Apply gradients to the model parameters. + + This needs to be implemented together with ``get_gradients``. + """ pass diff --git a/maro/rl/modeling/fc_block.py b/maro/rl/modeling/fc_block.py index 2788b4885..4c3c95fd3 100644 --- a/maro/rl/modeling/fc_block.py +++ b/maro/rl/modeling/fc_block.py @@ -12,7 +12,6 @@ class FullyConnected(nn.Module): """Fully connected network with optional batch normalization, activation and dropout components. Args: - name (str): Network name. input_dim (int): Network input dimension. output_dim (int): Network output dimension. hidden_dims ([int]): Dimensions of hidden layers. Its length is the number of hidden layers. @@ -29,6 +28,7 @@ class FullyConnected(nn.Module): dropout_p (float): Dropout probability. Defaults to None, in which case there is no drop-out. gradient_threshold (float): Gradient clipping threshold. Defaults to None, in which case not gradient clipping is performed. + name (str): Network name. Defaults to None. """ def __init__( self, @@ -98,7 +98,7 @@ def output_dim(self): return self._output_dim def _build_layer(self, input_dim, output_dim, head: bool = False): - """Build basic layer. + """Build a basic layer. BN -> Linear -> Activation -> Dropout """ diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 19094d4ff..41166428c 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -5,12 +5,12 @@ from .ddpg import DDPG from .dqn import DQN, PrioritizedExperienceReplay from .pg import PolicyGradient -from .policy import AbsPolicy, NullPolicy, RLPolicy +from .policy import AbsPolicy, DummyPolicy, RLPolicy __all__ = [ "ActorCritic", "DDPG", "DQN", "PrioritizedExperienceReplay", "PolicyGradient", - "AbsPolicy", "NullPolicy", "RLPolicy" + "AbsPolicy", "DummyPolicy", "RLPolicy" ] diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index d328b318e..84307b003 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -133,7 +133,11 @@ def __init__( self._buffer = defaultdict(lambda: self.Buffer(self.ac_net.input_dim, size=self.max_trajectory_len)) def __call__(self, states: np.ndarray): - """Return actions and log probabilities for given states.""" + """Return a list of action information dict given a batch of states. + + An action information dict contains the action itself, the corresponding log-P value and the corresponding + state value. + """ self.ac_net.eval() states = torch.from_numpy(states).to(self.device) if len(states.shape) == 1: @@ -157,6 +161,12 @@ def record( self._buffer[key].put(state, action, reward, terminal) def get_rollout_info(self): + """Extract information from the recorded transitions. + + Returns: + Loss (including gradients) for the latest trajectory segment in the replay buffer if ``get_loss_on_rollout`` + is True or the latest trajectory segment with pre-computed return and advantage values. + """ if self.get_loss_on_rollout: return self.get_batch_loss(self._get_batch(), explicit_grad=True) else: @@ -180,6 +190,13 @@ def _get_batch(self): return {key: np.concatenate(vals) for key, vals in batch.items()} def get_batch_loss(self, batch: dict, explicit_grad: bool = False): + """Compute AC loss for a data batch. + + Args: + batch (dict): A batch containing "states", "actions", "logps", "returns" and "advantages" as keys. + explicit_grad (bool): If True, the gradients should be returned as part of the loss information. Defaults + to False. + """ self.ac_net.train() states = torch.from_numpy(batch["states"]).to(self.device) actions = torch.from_numpy(batch["actions"]).to(self.device) @@ -220,14 +237,25 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): return loss_info def update(self, loss_info_list: List[dict]): - """Apply gradients to the underlying parameterized model.""" + """Update the model parameters with gradients computed by multiple roll-out instances or gradient workers. + + Args: + loss_info_list (List[dict]): A list of dictionaries containing loss information (including gradients) + computed by multiple roll-out instances or gradient workers. + """ self.ac_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) def learn(self, batch: dict): + """Learn from a batch containing data required for policy improvement. + + Args: + batch (dict): A batch containing "states", "actions", "logps", "returns" and "advantages" as keys. + """ for _ in range(self.grad_iters): self.ac_net.step(self.get_batch_loss(batch)["loss"]) def improve(self): + """Learn using data from the buffer.""" self.learn(self._get_batch()) def set_state(self, policy_state): diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 515fdf15f..878ddab8a 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -155,9 +155,21 @@ def record( ) def get_rollout_info(self): + """Randomly sample a batch of transitions from the replay memory. + + This is used in a distributed learning setting and the returned data will be sent to its parent instance + on the learning side (serving as the source of the latest model parameters) for training. + """ return self._replay_memory.sample(self.rollout_batch_size) def get_batch_loss(self, batch: dict, explicit_grad: bool = False) -> dict: + """Compute loss for a data batch. + + Args: + batch (dict): A batch containing "states", "actions", "rewards", "next_states" and "terminals" as keys. + explicit_grad (bool): If True, the gradients should be returned as part of the loss information. Defaults + to False. + """ self.ac_net.train() states = torch.from_numpy(batch["states"]).to(self.device) next_states = torch.from_numpy(["next_states"]).to(self.device) @@ -188,17 +200,29 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False) -> dict: return loss_info def update(self, loss_info_list: List[dict]): + """Update the model parameters with gradients computed by multiple gradient workers. + + Args: + loss_info_list (List[dict]): A list of dictionaries containing loss information (including gradients) + computed by multiple gradient workers. + """ self.ac_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) if self._ac_net_version - self._target_ac_net_version == self.update_target_every: self._update_target() def learn(self, batch: dict): + """Learn from a batch containing data required for policy improvement. + + Args: + batch (dict): A batch containing "states", "actions", "rewards", "next_states" and "terminals" as keys. + """ self._replay_memory.put( batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] ) self.improve() def improve(self): + """Learn using data from the replay memory.""" for _ in range(self.num_epochs): train_batch = self._replay_memory.sample(self.train_batch_size) self.ac_net.step(self.get_batch_loss(train_batch)["loss"]) diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index de40f1452..25b3af1bd 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -262,6 +262,11 @@ def record( self._per.set_max_priority(indexes) def get_rollout_info(self): + """Randomly sample a batch of transitions from the replay memory. + + This is used in a distributed learning setting and the returned data will be sent to its parent instance + on the learning side (serving as the source of the latest model parameters) for training. + """ return self._replay_memory.sample(self.rollout_batch_size) def _get_batch(self): @@ -280,6 +285,13 @@ def _get_batch(self): return self._replay_memory.sample(self.train_batch_size) def get_batch_loss(self, batch: dict, explicit_grad: bool = False): + """Compute loss for a data batch. + + Args: + batch (dict): A batch containing "states", "actions", "rewards", "next_states" and "terminals" as keys. + explicit_grad (bool): If True, the gradients should be returned as part of the loss information. Defaults + to False. + """ self.q_net.train() states = torch.from_numpy(batch["states"]).to(self.device) next_states = torch.from_numpy(batch["next_states"]).to(self.device) @@ -314,22 +326,35 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): return loss_info def update(self, loss_info_list: List[dict]): + """Update the Q-net parameters with gradients computed by multiple gradient workers. + + Args: + loss_info_list (List[dict]): A list of dictionaries containing loss information (including gradients) + computed by multiple gradient workers. + """ if self.prioritized_replay: for loss_info in loss_info_list: self._per.update(loss_info["indexes"], loss_info["td_errors"]) self.q_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) self._q_net_version += 1 + # soft-update target network if self._q_net_version - self._target_q_net_version == self.update_target_every: self._update_target() def learn(self, batch: dict): + """Learn from a batch containing data required for policy improvement. + + Args: + batch (dict): A batch containing "states", "actions", "rewards", "next_states" and "terminals" as keys. + """ self._replay_memory.put( batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] ) self.improve() def improve(self): + """Learn using data from the replay memory.""" for _ in range(self.num_epochs): loss_info = self.get_batch_loss(self._get_batch()) if self.prioritized_replay: @@ -340,11 +365,11 @@ def improve(self): self._update_target() def _update_target(self): - # soft-update target network self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) self._target_q_net_version = self._q_net_version def exploration_step(self): + """Update the exploration parameters according to the exploration scheduler.""" for sch in self.exploration_schedulers: sch.step() diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index 40471347e..03a29bfac 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -92,11 +92,11 @@ def __init__( self._buffer = defaultdict(lambda: self.Buffer(self.policy_net.input_dim, size=self.max_trajectory_len)) - def get_random_action(self, states: np.ndarray): - return np.random.randint(self.policy_net.num_actions, size=(states.shape[0] if len(states.shape) > 1 else 1,)) + def __call__(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Return a list of action information dict given a batch of states. - def get_action(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """Return actions and log probabilities for given states.""" + An action information dict contains the action itself and the corresponding log-P value. + """ self.policy_net.eval() with torch.no_grad(): actions, logps = self.policy_net.get_action(states, greedy=self.greedy) @@ -115,6 +115,12 @@ def record( self._buffer[key].put(state, action, reward, terminal) def get_rollout_info(self): + """Extract information from the recorded transitions. + + Returns: + Loss (including gradients) for the latest trajectory segment in the replay buffer if ``get_loss_on_rollout`` + is True or the latest trajectory segment with pre-computed return values. + """ if self.get_loss_on_rollout: return self.get_batch_loss(self._get_batch(), explicit_grad=True) else: @@ -132,13 +138,15 @@ def _get_batch(self): return {key: np.concatenate(vals) for key, vals in batch.items} def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - """ - This should be called at the end of a simulation episode and the experiences obtained from - the experience store's ``get`` method should be a sequential set, i.e., in the order in - which they are generated during the simulation. Otherwise, the return values may be meaningless. + """Compute AC loss for a data batch. + + Args: + batch (dict): A batch containing "states" and "returns" as keys. + explicit_grad (bool): If True, the gradients should be returned as part of the loss information. Defaults + to False. """ self.policy_net.train() - returns = torch.from_numpy(np.asarray(batch.returns)).to(self.device) + returns = torch.from_numpy(np.asarray(batch["returns"])).to(self.device) _, logp = self.policy_net(batch["states"]) loss = -(logp * returns).mean() @@ -148,14 +156,25 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): return loss_info def update(self, loss_info_list: List[dict]): - """Apply gradients to the underlying parameterized model.""" + """Update the model parameters with gradients computed by multiple roll-out instances or gradient workers. + + Args: + loss_info_list (List[dict]): A list of dictionaries containing loss information (including gradients) + computed by multiple roll-out instances or gradient workers. + """ self.policy_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) def learn(self, batch: dict): + """Learn from a batch containing data required for policy improvement. + + Args: + batch (dict): A batch containing "states" and "returns" as keys. + """ for _ in range(self.grad_iters): self.policy_net.step(self.get_batch_loss(batch)["grad"]) def improve(self): + """Learn using data from the buffer.""" self.learn(self._get_batch()) def set_state(self, policy_state): diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index d3faf1728..01e2f08dc 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -27,7 +27,7 @@ def __call__(self, state): raise NotImplementedError -class NullPolicy(AbsPolicy): +class DummyPolicy(AbsPolicy): """Dummy policy that does nothing. Note that the meaning of a "None" action may depend on the scenario. @@ -92,7 +92,7 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): pass def update(self, loss_info_list: List[dict]): - """Update using loss information collected from multiple loss computing instances. + """Update with loss information computed by multiple sources. There are two possible scenarios where you need to implement this interface: 1) if you are doing distributed learning and want each roll-out instance to collect information that can be used to update policy parameters @@ -104,7 +104,7 @@ def update(self, loss_info_list: List[dict]): Args: loss_info_list (List[dict]): A list of dictionaries containing loss information (e.g., gradients) computed - by distributed roll-out instances. + by multiple sources. """ pass From 3738bd1f6def05afcb156f57dfbef1d3413218e9 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Tue, 14 Sep 2021 15:11:03 +0000 Subject: [PATCH 436/482] fixed lint issues and updated rl toolkit images --- docs/source/images/rl/core_model.svg | 3 - docs/source/images/rl/env_sampler.svg | 3 + docs/source/images/rl/env_wrapper.svg | 3 - docs/source/images/rl/learning_cycle.svg | 3 + docs/source/images/rl/rollout_manager.svg | 2 +- docs/source/images/rl/training_manager.svg | 3 - maro/rl/learning/env_sampler.py | 4 +- maro/rl/learning/helpers.py | 2 +- maro/rl/learning/rollout_manager.py | 6 +- maro/rl/modeling/core_model.py | 6 +- maro/rl/policy/ac.py | 2 +- maro/rl/policy/replay.py | 3 + maro/rl/utils/gradient_averaging.py | 1 + maro/rl/utils/remote_tools/__init__.py | 8 -- maro/rl/utils/remote_tools/client.py | 6 -- maro/rl/utils/remote_tools/task_manager.py | 10 -- maro/rl/utils/remote_tools/worker.py | 52 --------- maro/rl/utils/torch_cls_index.py | 118 --------------------- 18 files changed, 21 insertions(+), 214 deletions(-) delete mode 100644 docs/source/images/rl/core_model.svg create mode 100644 docs/source/images/rl/env_sampler.svg delete mode 100644 docs/source/images/rl/env_wrapper.svg create mode 100644 docs/source/images/rl/learning_cycle.svg delete mode 100644 docs/source/images/rl/training_manager.svg delete mode 100644 maro/rl/utils/remote_tools/__init__.py delete mode 100644 maro/rl/utils/remote_tools/client.py delete mode 100644 maro/rl/utils/remote_tools/task_manager.py delete mode 100644 maro/rl/utils/remote_tools/worker.py delete mode 100644 maro/rl/utils/torch_cls_index.py diff --git a/docs/source/images/rl/core_model.svg b/docs/source/images/rl/core_model.svg deleted file mode 100644 index 0fd1af68c..000000000 --- a/docs/source/images/rl/core_model.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Multi-head Model
Multi-head Model
%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Task%20Stack%201%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D15%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22260.63%22%20y%3D%22500%22%20width%3D%2298.75%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
%3CmxGraphModel%...
......
.......
......
.......
Head 1
Head 1
Optimizer
Optimizer
Optimizer
Optimizer
Head N
Head N
Shared Stack
Shared Stack
Optimizer
Optimizer
Multi-head NN model - a Core Model implementation 
Multi-head NN model - a Core Model implementation 
Core Model
Core Model
%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Task%20Stack%201%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D15%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22260.63%22%20y%3D%22500%22%20width%3D%2298.75%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
%3CmxGraphModel%...
......
.......
step
step
Component 1
Component 1
Optimizer
Optimizer
Optimizer
Optimizer
Component N
Component N
Core Model
Core Model
forward
forw...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/env_sampler.svg b/docs/source/images/rl/env_sampler.svg new file mode 100644 index 000000000..a56026596 --- /dev/null +++ b/docs/source/images/rl/env_sampler.svg @@ -0,0 +1,3 @@ + + +
get_state()
get_state()
Environment Simulator
Environment S...
get_env_actions()
get_env_actions()
Environment Sampler
Environment Sampler
Agent
Agent
Agent
Agent
Policy
Policy
state by agent
state by agent
action by agent
action by age...
get_reward()
get_reward()
agent-policy mapping
agent-policy...
state batch by policy
state batch by po...
agent-policy mapping
agent-policy...
action batch by policy
action batch by pol...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/env_wrapper.svg b/docs/source/images/rl/env_wrapper.svg deleted file mode 100644 index c0aef1f00..000000000 --- a/docs/source/images/rl/env_wrapper.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
get_state()
get_state()
Environment Simulator
Environment Simul...
to_env_action()
to_env_action()
Environment Wrapper
Environment Wrapper
Agent
Agent
Agent
Agent
Policy
Policy
state
state
action
action
execute
execute
Replay Buffer
Replay Buffer
get_reward()
get_reward()
experiences
experiences
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/learning_cycle.svg b/docs/source/images/rl/learning_cycle.svg new file mode 100644 index 000000000..74f77815e --- /dev/null +++ b/docs/source/images/rl/learning_cycle.svg @@ -0,0 +1,3 @@ + + +
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
roll-out info
roll-out i...
Policy
Manager
Policy...
policy states
policy stat...
Synchronous Learning Cycle
Synchronous Learning Cycle
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/rollout_manager.svg b/docs/source/images/rl/rollout_manager.svg index 29a8325fd..aa70df3c0 100644 --- a/docs/source/images/rl/rollout_manager.svg +++ b/docs/source/images/rl/rollout_manager.svg @@ -1,3 +1,3 @@ -
Actor
Actor
Local Roll-out Manager
Local Roll-out Manager
training
environment wrapper
training...
policies
policies
collect
colle...
evaluation environment wrapper
evaluation envi...
evaluate
evalu...
Parallel Roll-out Manager
Parallel Roll-out Manager
collect
colle...
evaluate
evalu...
training env wrapper
training env wra...
eval env wrapper
eval env wrapper
policies
policies
actor proxy
actor proxy
roll-out message
roll-out messa...
results
results
remote roll-out config
remote roll-out...
Local Roll-out Manager
Local Roll-out Manager
Parallel Roll-out Manager
Parallel Roll-out Manager
Viewer does not support full SVG 1.1
\ No newline at end of file +
Roll-out Worker
Roll-out Worker
Rollout Manager
Rollout Manager
collect
colle...
evaluate
evalu...
env sampler
env sampler
roll-out message
roll-out messa...
results
results
Roll-out Manager
Roll-out Manager
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/training_manager.svg b/docs/source/images/rl/training_manager.svg deleted file mode 100644 index 0491ebce4..000000000 --- a/docs/source/images/rl/training_manager.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Local Training Manager
Local Training Manager
policies
policies
on_experiences
on_experiences
Parallel Training Manager
Parallel Training Manager
on_experiences
on_experiences
policy proxy
policy proxy
remote training config
remote training...
Policy Server 1
Policy Server 1
get_state
get_state
get_state
get_state
subset of policies
subset of pol...
training message
traini...
policy state
policy state
Local Training Manager
Local Training Manager
Parallel Training Manager
Parallel Training Manager
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 88258d916..a4228b76e 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -202,7 +202,7 @@ class AbsEnvSampler(ABC): simulation. Defaults to None. policies_to_parallelize (List[str]): Policies to be placed in separate processes so that inference can be performed in parallel to speed up simulation. This is useful if some policies are big and takes long times - to compute actions. Defaults to an empty list. + to compute actions. Defaults to an empty list. """ def __init__( self, @@ -370,7 +370,7 @@ def worker( num_extra_recv_attempts (int): Number of extra receive attempts after each received ``SAMPLE`` message. This is used to catch the worker up to the latest episode in case it trails the main learning loop by at least one full episode. Defaults to 0. - recv_timeout (int): Timeout for the extra receive attempts. Defaults to 100 (miliseconds). + recv_timeout (int): Timeout for the extra receive attempts. Defaults to 100 (miliseconds). proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. diff --git a/maro/rl/learning/helpers.py b/maro/rl/learning/helpers.py index 55fa39b12..b2729e9a8 100644 --- a/maro/rl/learning/helpers.py +++ b/maro/rl/learning/helpers.py @@ -17,7 +17,7 @@ def get_rollout_finish_msg(ep, step_range, exploration_params=None): def get_eval_schedule(sch: Union[int, List[int]], num_episodes: int, final: bool = True): """Helper function to the policy evaluation schedule. - + Args: sch (Union[int, List[int]]): Evaluation schedule. If it is an int, it is treated as the number of episodes between two adjacent evaluations. For example, if the total number of episodes is 20 and ``sch`` is 6, diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index f75c81752..2917d199c 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -40,7 +40,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) Returns: A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries - containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. + containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. An RL policy's roll-out information must be either loss information or a data batch that can be passed to the policy's ``update`` or ``learn``, respectively. """ @@ -144,7 +144,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) Returns: A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries - containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. + containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. An RL policy's roll-out information must be either loss information or a data batch that can be passed to the policy's ``update`` or ``learn``, respectively. """ @@ -279,7 +279,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) Returns: A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries - containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. + containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. An RL policy's roll-out information must be either loss information or a data batch that can be passed to the policy's ``update`` or ``learn``, respectively. """ diff --git a/maro/rl/modeling/core_model.py b/maro/rl/modeling/core_model.py index e7644e26f..af118cf29 100644 --- a/maro/rl/modeling/core_model.py +++ b/maro/rl/modeling/core_model.py @@ -30,9 +30,9 @@ def get_gradients(self, loss: torch.tensor): There are two possible scenarios where you need to implement this interface: 1) if you are doing distributed learning and want each roll-out instance to collect gradients that can be directly applied to policy parameters - on the learning side (abstracted through ``AbsPolicyManager``); 2) if you are computing loss in data-parallel fashion, - i.e., by splitting a data batch to several smaller batches and sending them to a set of remote workers for - parallelized gradient computation. In this case, this method will be used by the remote workers. + on the learning side (abstracted through ``AbsPolicyManager``); 2) if you are computing loss in data-parallel + fashion, i.e., by splitting a data batch to several smaller batches and sending them to a set of remote workers + for parallelized gradient computation. In this case, this method will be used by the remote workers. """ pass diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 84307b003..52977204c 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -134,7 +134,7 @@ def __init__( def __call__(self, states: np.ndarray): """Return a list of action information dict given a batch of states. - + An action information dict contains the action itself, the corresponding log-P value and the corresponding state value. """ diff --git a/maro/rl/policy/replay.py b/maro/rl/policy/replay.py index 3296644af..567820b2f 100644 --- a/maro/rl/policy/replay.py +++ b/maro/rl/policy/replay.py @@ -12,6 +12,8 @@ class ReplayMemory: Args: capacity (int): Maximum number of experiences that can be stored. + state_dim (int): Dimension of flattened state. + action_dim (int): Action dimension. Defaults to 1. random_overwrite (bool): This specifies overwrite behavior when the capacity is reached. If this is True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. Defaults to False. @@ -81,6 +83,7 @@ def put( return indexes def sample(self, size: int): + """Obtain a random sample.""" indexes = np.random.choice(self._ptr, size=size) return { "states": self.states[indexes], diff --git a/maro/rl/utils/gradient_averaging.py b/maro/rl/utils/gradient_averaging.py index 5b404cae9..3b8c83f34 100644 --- a/maro/rl/utils/gradient_averaging.py +++ b/maro/rl/utils/gradient_averaging.py @@ -7,6 +7,7 @@ def average_grads(grad_list: List[dict]): + """Obtain the average of a list of gradients.""" return { param_name: torch.mean(torch.stack([grad[param_name] for grad in grad_list]), dim=0) for param_name in grad_list[0] diff --git a/maro/rl/utils/remote_tools/__init__.py b/maro/rl/utils/remote_tools/__init__.py deleted file mode 100644 index c403b480b..000000000 --- a/maro/rl/utils/remote_tools/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .client import TaskClient -from .task_manager import LearnTask, TaskManager -from .worker import worker - -__all__ = ["LearnTask", "TaskClient", "TaskManager", "worker"] diff --git a/maro/rl/utils/remote_tools/client.py b/maro/rl/utils/remote_tools/client.py deleted file mode 100644 index 55c605998..000000000 --- a/maro/rl/utils/remote_tools/client.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -class TaskClient: - pass diff --git a/maro/rl/utils/remote_tools/task_manager.py b/maro/rl/utils/remote_tools/task_manager.py deleted file mode 100644 index f560fedde..000000000 --- a/maro/rl/utils/remote_tools/task_manager.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import namedtuple - -LearnTask = namedtuple("LearnTask", ["policy_name", "model_state", "batch"]) - - -class TaskManager: - pass diff --git a/maro/rl/utils/remote_tools/worker.py b/maro/rl/utils/remote_tools/worker.py deleted file mode 100644 index 27f6f3eb9..000000000 --- a/maro/rl/utils/remote_tools/worker.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import Callable, Dict - -from maro.communication import Proxy -from maro.rl.utils import MsgKey, MsgTag -from maro.utils import Logger - - -def worker( - group: str, - worker_idx: int, - create_policy_func_dict: Dict[str, Callable], - proxy_kwargs: dict = {}, - log_dir: str = getcwd() -): - """Policy trainer process that can be launched on separate computation nodes. - Args: - group (str): Group name for the training cluster, which includes all trainers and a training manager that - manages them. - trainer_idx (int): Integer trainer index. The trainer's ID in the cluster will be "TRAINER.{trainer_idx}". - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have exactly one parameter which is the policy name and return an ``AbsPolicy`` - instance. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - policy_dict = {} - proxy = Proxy( - group, "GRADWORKER", {"grad_manager": 1}, component_name=f"GRADWORKER.{worker_idx}", **proxy_kwargs - ) - logger = Logger(proxy.name, dump_folder=log_dir) - - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.COMPUTE_GRAD: - t0 = time.time() - task = msg.body[MsgKey.GRAD_TASK] - policy_name = task.policy_name - if policy_name not in policy_dict: - policy_dict[policy_name] = create_policy_func_dict[policy_name]() - msg_body = {MsgKey.LOSS_INFO: policy_dict[policy_name].get_loss_info(task.batch, explicit_grad=True)} - logger.debug(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, tag=MsgTag.COMPUTE_GRAD_DONE, body=msg_body) diff --git a/maro/rl/utils/torch_cls_index.py b/maro/rl/utils/torch_cls_index.py deleted file mode 100644 index 5d1ed29b8..000000000 --- a/maro/rl/utils/torch_cls_index.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from torch import nn, optim -from torch.optim import lr_scheduler - -# For details, see https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity -TORCH_ACTIVATION = { - "elu": nn.ELU, - "hard_shrink": nn.Hardshrink, - "hard_sigmoid": nn.Hardsigmoid, - "hard_tanh": nn.Hardtanh, - "hardswish": nn.Hardswish, - "leaky_relu": nn.LeakyReLU, - "log_sigmoid": nn.LogSigmoid, - "multihead_attention": nn.MultiheadAttention, - "prelu": nn.PReLU, - "relu": nn.ReLU, - "relu6": nn.ReLU6, - "rrelu": nn.RReLU, - "selu": nn.SELU, - "celu": nn.CELU, - "gelu": nn.GELU, - "sigmoid": nn.Sigmoid, - "soft_plus": nn.Softplus, - "soft_shrink": nn.Softshrink, - "soft_sign": nn.Softsign, - "tanh": nn.Tanh, - "tanh_shrink": nn.Tanhshrink, - "threshold": nn.Threshold -} - -# For details, see https://pytorch.org/docs/stable/nn.html#loss-functions -TORCH_LOSS = { - "l1": nn.L1Loss, - "mse": nn.MSELoss, - "cross_entropy": nn.CrossEntropyLoss, - "ctc": nn.CTCLoss, - "nll": nn.NLLLoss, - "poisson_nll": nn.PoissonNLLLoss, - "kl": nn.KLDivLoss, - "bce": nn.BCELoss, - "bce_logits": nn.BCEWithLogitsLoss, - "margin_ranking": nn.MarginRankingLoss, - "hinge_embedding": nn.HingeEmbeddingLoss, - "multi_label_margin": nn.MultiLabelMarginLoss, - "multi_label_soft_margin": nn.MultiLabelSoftMarginLoss, - "smooth_l1": nn.SmoothL1Loss, - "soft_margin": nn.SoftMarginLoss, - "cosine_embedding": nn.CosineEmbeddingLoss, - "multi_margin": nn.MultiMarginLoss, - "triplet_margin": nn.TripletMarginLoss, -} - -# For details, see https://pytorch.org/docs/stable/optim.html -TORCH_OPTIM = { - "sgd": optim.SGD, - "asgd": optim.ASGD, - "adadelta": optim.Adadelta, - "adagrad": optim.Adagrad, - "adam": optim.Adam, - "adamax": optim.Adamax, - "adamw": optim.AdamW, - "sparse_adam": optim.SparseAdam, - "lbfgs": optim.LBFGS, - "rmsprop": optim.RMSprop, - "rprop": optim.Rprop -} - -# For details, see https://pytorch.org/docs/stable/optim.html -TORCH_LR_SCHEDULER = { - "lambda": lr_scheduler.LambdaLR, - "multiplicative": lr_scheduler.MultiplicativeLR, - "step": lr_scheduler.StepLR, - "multi_step": lr_scheduler.MultiStepLR, - "exponential": lr_scheduler.ExponentialLR, - "cosine_annealing": lr_scheduler.CosineAnnealingLR, - "reduce_on_plateau": lr_scheduler.ReduceLROnPlateau, - "cyclic": lr_scheduler.CyclicLR, - "one_cycle": lr_scheduler.OneCycleLR, - "cosine_annealing_warm_restarts": lr_scheduler.CosineAnnealingWarmRestarts -} - - -def get_torch_activation_cls(activation_type): - if isinstance(activation_type, str): - if activation_type not in TORCH_ACTIVATION: - raise KeyError(f"A string activation_type must be one of {list(TORCH_ACTIVATION.keys())}.") - return TORCH_ACTIVATION[activation_type] - - return activation_type - - -def get_torch_loss_cls(loss_type): - if isinstance(loss_type, str): - if loss_type not in TORCH_LOSS: - raise KeyError(f"A string loss_type must be one of {list(TORCH_LOSS.keys())}.") - return TORCH_LOSS[loss_type] - - return loss_type - - -def get_torch_optim_cls(optim_type): - if isinstance(optim_type, str): - if optim_type not in TORCH_OPTIM: - raise KeyError(f"A string optim_type must be one of {list(TORCH_OPTIM.keys())}.") - return TORCH_OPTIM[optim_type] - - return optim_type - - -def get_torch_lr_scheduler_cls(lr_scheduler_type): - if isinstance(lr_scheduler_type, str): - if lr_scheduler_type not in TORCH_LR_SCHEDULER: - raise KeyError(f"A string lr_scheduler_type must be one of {list(TORCH_LR_SCHEDULER.keys())}.") - return TORCH_LR_SCHEDULER[lr_scheduler_type] - - return lr_scheduler_type From 69c5a56d0b928f61572bc12df300d193ca2a349b Mon Sep 17 00:00:00 2001 From: ysqyang Date: Wed, 15 Sep 2021 16:52:26 +0800 Subject: [PATCH 437/482] removed obsolete images --- docs/source/images/rl/core_model.svg | 3 --- docs/source/images/rl/env_wrapper.svg | 3 --- docs/source/images/rl/rollout_manager.svg | 2 +- docs/source/images/rl/training_manager.svg | 3 --- 4 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 docs/source/images/rl/core_model.svg delete mode 100644 docs/source/images/rl/env_wrapper.svg delete mode 100644 docs/source/images/rl/training_manager.svg diff --git a/docs/source/images/rl/core_model.svg b/docs/source/images/rl/core_model.svg deleted file mode 100644 index 0fd1af68c..000000000 --- a/docs/source/images/rl/core_model.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Multi-head Model
Multi-head Model
%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Task%20Stack%201%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D15%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22260.63%22%20y%3D%22500%22%20width%3D%2298.75%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
%3CmxGraphModel%...
......
.......
......
.......
Head 1
Head 1
Optimizer
Optimizer
Optimizer
Optimizer
Head N
Head N
Shared Stack
Shared Stack
Optimizer
Optimizer
Multi-head NN model - a Core Model implementation 
Multi-head NN model - a Core Model implementation 
Core Model
Core Model
%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Task%20Stack%201%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D15%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22260.63%22%20y%3D%22500%22%20width%3D%2298.75%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
%3CmxGraphModel%...
......
.......
step
step
Component 1
Component 1
Optimizer
Optimizer
Optimizer
Optimizer
Component N
Component N
Core Model
Core Model
forward
forw...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/env_wrapper.svg b/docs/source/images/rl/env_wrapper.svg deleted file mode 100644 index c0aef1f00..000000000 --- a/docs/source/images/rl/env_wrapper.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
get_state()
get_state()
Environment Simulator
Environment Simul...
to_env_action()
to_env_action()
Environment Wrapper
Environment Wrapper
Agent
Agent
Agent
Agent
Policy
Policy
state
state
action
action
execute
execute
Replay Buffer
Replay Buffer
get_reward()
get_reward()
experiences
experiences
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/rollout_manager.svg b/docs/source/images/rl/rollout_manager.svg index 29a8325fd..aa70df3c0 100644 --- a/docs/source/images/rl/rollout_manager.svg +++ b/docs/source/images/rl/rollout_manager.svg @@ -1,3 +1,3 @@ -
Actor
Actor
Local Roll-out Manager
Local Roll-out Manager
training
environment wrapper
training...
policies
policies
collect
colle...
evaluation environment wrapper
evaluation envi...
evaluate
evalu...
Parallel Roll-out Manager
Parallel Roll-out Manager
collect
colle...
evaluate
evalu...
training env wrapper
training env wra...
eval env wrapper
eval env wrapper
policies
policies
actor proxy
actor proxy
roll-out message
roll-out messa...
results
results
remote roll-out config
remote roll-out...
Local Roll-out Manager
Local Roll-out Manager
Parallel Roll-out Manager
Parallel Roll-out Manager
Viewer does not support full SVG 1.1
\ No newline at end of file +
Roll-out Worker
Roll-out Worker
Rollout Manager
Rollout Manager
collect
colle...
evaluate
evalu...
env sampler
env sampler
roll-out message
roll-out messa...
results
results
Roll-out Manager
Roll-out Manager
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/training_manager.svg b/docs/source/images/rl/training_manager.svg deleted file mode 100644 index 0491ebce4..000000000 --- a/docs/source/images/rl/training_manager.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Local Training Manager
Local Training Manager
policies
policies
on_experiences
on_experiences
Parallel Training Manager
Parallel Training Manager
on_experiences
on_experiences
policy proxy
policy proxy
remote training config
remote training...
Policy Server 1
Policy Server 1
get_state
get_state
get_state
get_state
subset of policies
subset of pol...
training message
traini...
policy state
policy state
Local Training Manager
Local Training Manager
Parallel Training Manager
Parallel Training Manager
Viewer does not support full SVG 1.1
\ No newline at end of file From 9030200a1a4e08577377aba53e163f910b3464df Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 15 Sep 2021 09:32:01 +0000 Subject: [PATCH 438/482] added back agent2policy for general workflow use --- examples/rl/cim/__init__.py | 4 ++-- examples/rl/cim/env_sampler.py | 4 +++- examples/rl/workflows/general.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index 280488bcc..a103060e1 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. from .callbacks import post_collect, post_evaluate -from .env_sampler import get_env_sampler +from .env_sampler import agent2policy, get_env_sampler from .policies import policy_func_dict -__all__ = ["post_collect", "post_evaluate", "get_env_sampler", "policy_func_dict"] +__all__ = ["agent2policy", "post_collect", "post_evaluate", "get_env_sampler", "policy_func_dict"] diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 2890c5c5f..a1148311c 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -80,11 +80,13 @@ def get_reward(self, actions, tick): return {agent_id: reward for agent_id, reward in zip(ports, rewards)} +agent2policy = {agent: f"ac.{agent}" for agent in Env(**env_conf).agent_idx_list} + def get_env_sampler(): return CIMEnvSampler( get_env=lambda: Env(**env_conf), get_policy_func_dict=policy_func_dict, - agent2policy={agent: f"ac.{agent}" for agent in Env(**env_conf).agent_idx_list}, + agent2policy=agent2policy, reward_eval_delay=reward_shaping_conf["time_window"], post_step=post_step, policies_to_parallelize=[] diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index 5783c4403..dc88c83a3 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -16,7 +16,8 @@ module = importlib.import_module(f"{getenv('SCENARIO')}") -get_env_sampler = getattr(module, "get_env_sampler") policy_func_dict = getattr(module, "policy_func_dict") +agent2policy = getattr(module, "agent2policy") +get_env_sampler = getattr(module, "get_env_sampler") post_collect = getattr(module, "post_collect", None) post_evaluate = getattr(module, "post_evaluate", None) From f2dd5c07e728d67c203edab2167ba4d249536241 Mon Sep 17 00:00:00 2001 From: "GQ.Chen" <675865907@qq.com> Date: Thu, 16 Sep 2021 22:53:21 +0800 Subject: [PATCH 439/482] V0.2 rl refinement dist (#377) * Support `slice` operation in ExperienceSet * Support naive distributed policy training by proxy * Dynamically allocate trainers according to number of experience * code check * code check * code check * Fix a bug in distributed trianing with no gradient * Code check * Move Back-Propagation from trainer to policy_manager and extract trainer-allocation strategy * 1.call allocate_trainer() at first of update(); 2.refine according to code review * Code check * Refine code with new interface * Update docs of PolicyManger and ExperienceSet * Add images for rl_toolkit docs * Update diagram of PolicyManager * Refine with new interface * Extract allocation strategy into `allocation_strategy.py` * add `distributed_learn()` in policies for data-parallel training * Update doc of RL_toolkit * Add gradient workers for data-parallel * Refine code and update docs * Lint check * Refine by comments * Rename `trainer` to `worker` * Rename `distributed_learn` to `learn_with_data_parallel` * Refine allocator and remove redundant code in policy_manager * remove arugments in allocate_by_policy and so on --- docs/source/images/rl/policy_manager.svg | 3 + docs/source/key_components/rl_toolkit.rst | 25 ++ .../rl/scripts/docker/docker_compose_yml.py | 20 +- examples/rl/workflows/config.yml | 4 + examples/rl/workflows/general.py | 1 + examples/rl/workflows/grad_worker.py | 41 ++ examples/rl/workflows/policy_host.py | 7 + examples/rl/workflows/policy_manager.py | 40 +- maro/rl/learning/__init__.py | 7 +- maro/rl/learning/policy_manager.py | 376 ++++++++++++++---- maro/rl/policy/__init__.py | 2 + maro/rl/policy/ac.py | 33 +- maro/rl/policy/ddpg.py | 38 +- maro/rl/policy/dqn.py | 45 ++- maro/rl/policy/pg.py | 33 +- maro/rl/policy/policy.py | 18 + maro/rl/policy/worker_allocator.py | 123 ++++++ maro/rl/utils/message_enums.py | 1 + 18 files changed, 728 insertions(+), 89 deletions(-) create mode 100644 docs/source/images/rl/policy_manager.svg create mode 100644 examples/rl/workflows/grad_worker.py create mode 100644 maro/rl/policy/worker_allocator.py diff --git a/docs/source/images/rl/policy_manager.svg b/docs/source/images/rl/policy_manager.svg new file mode 100644 index 000000000..946b1b28b --- /dev/null +++ b/docs/source/images/rl/policy_manager.svg @@ -0,0 +1,3 @@ + + +
Simple Policy Manager
Simple Policy Manager
policies
policies
update
update
get_state
get_state
Simple Policy Manager 
Simple Policy Manager 
Distributed Policy Manager
Distributed Policy Manager
Policy 
Policy 
update
update
get_state
get_state
Trainer (Grad Worker)
Trainer (Grad Worker)
Auto-balanced task dispatching message
Auto-balanced tas...
gradient
gradient
Policy State 1
Policy State 1
Experience Batch 1
Experience Batch 1
Task
Task
Policy State 2
Policy State 2
Experience Batch 2
Experience Batch 2
Task
Task
...
...
Distributed Policy Manager
Distributed Policy Manager
policies
policies
Policy
Policy
Trainer (Grad Worker)
Trainer (Grad Worker)
Auto-balanced task dispatching message
Auto-balanced tas...
gradient
gradient
Policy State 1
Policy State 1
Experience Batch 1
Experience Batch 1
Task
Task
Policy State 2
Policy State 2
Experience Batch 2
Experience Batch 2
Task
Task
...
...
Policy Host
Policy Host
Experience Memory
Experience Memory
Experience Memory
Experience Me...
Experience Memory
Experience Me...
Experience Memory
Experience Me...
Viewer does not support full SVG 1.1
diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 6be782c4e..8e5bedb36 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -64,6 +64,10 @@ provides various policy improvement interfaces to support single-threaded and di .. code-block:: python class AbsPolicy(ABC): + def __init__(self, name) + super().__init__() + self._name = name + @abstractmethod def __call__(self, state): """Select an action based on a state.""" @@ -108,6 +112,27 @@ is present as a server process. The types of policy manager include: to reeeive roll-out information for update. This approach allows the policies to be updated in parallel and may be necessary when the combined size of the policies is too big to fit in a single node. +Moreover, in ``data-parallel`` mode, each policy manager has an additional worker(``grad_worker``) +allocator, which provides a policy-to-worker mapping. The worker allocator performs auto-balance +during training, by dynamically adjusting worker number for policies according to the +experience/agent/policy number. + +.. image:: ../images/rl/policy_manager.svg + :target: ../images/rl/policy_manager.svg + :alt: PolicyManager + +The ``DistributedPolicyManager`` runs a set of ``policy_host`` and a ``TrainerAllocator``. +``policy_host`` is a process/VM/node that hosts the update of a policy. The ``TrainerAllocator`` +dynamically adjusts worker node numbers for policies according to the experience/agent/policy +number. Each ``policy_host`` independently updates its own policies for policy-level parallelism. + +During training, the ``PolicyManager`` receives training data collected by the ``RolloutManager``, +then send them to corresponding ``policy_host``. Each ``policy_host`` will send gradient tasks consist +of policy state and experience batch, to several stateless ``grad_worker`` for gradient computation. +The ``grad_worker`` is stateless, and computes gradients using the policy state and data +batch provided in a task. +Then ``policy_host`` aggregates the gradients from ``grad_worker`` s, and performs gradient descent +on its parameters. Core Model ---------- diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 0c87d27c7..9e0c6a39b 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -66,10 +66,28 @@ ] common_env.append(f"NUMROLLOUTS={config[config['mode']]['num_rollouts']}") - # host spec + common_env.append(f"DATAPARALLEL={config['data_parallel']['enable']}") + common_env.append(f"DISTRIBUTED={config['policy_manager']['type'] == 'distributed'}") + if config["data_parallel"]["enable"]: + common_env.append(f"NUMGRADWORKERS={config['data_parallel']['num_workers']}") + common_env.append(f"ALLOCATIONMODE={config['data_parallel']['allocation_mode']}") if config["policy_manager"]["type"] == "distributed": common_env.append(f"LEARNGROUP={config['policy_manager']['distributed']['group']}") common_env.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") + + # grad worker config + if config["data_parallel"]["enable"]: + for worker_id in range(config['data_parallel']['num_workers']): + str_id = f"grad_worker.{worker_id}" + grad_worker_spec = deepcopy(common_spec) + del grad_worker_spec["build"] + grad_worker_spec["command"] = "python3 /maro/rl_examples/workflows/grad_worker.py" + grad_worker_spec["container_name"] = f"{namespace}.{str_id}" + grad_worker_spec["environment"] = [f"WORKERID={worker_id}"] + common_env + docker_compose_manifest["services"][str_id] = grad_worker_spec + + # host spec + if config["policy_manager"]["type"] == "distributed": for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): str_id = f"policy_host.{host_id}" host_spec = deepcopy(common_spec) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index d39eb2de5..2704e7036 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -27,6 +27,10 @@ policy_manager: distributed: group: learn num_hosts: 2 +data_parallel: + enable: false + num_workers: 2 + allocation_mode: by-policy # by-policy, by-agent, by-experience redis: host: redis-server port: 6379 diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py index dc88c83a3..c14e7aca7 100644 --- a/examples/rl/workflows/general.py +++ b/examples/rl/workflows/general.py @@ -21,3 +21,4 @@ get_env_sampler = getattr(module, "get_env_sampler") post_collect = getattr(module, "post_collect", None) post_evaluate = getattr(module, "post_evaluate", None) +agent2policy = getattr(module, "agent2policy", None) diff --git a/examples/rl/workflows/grad_worker.py b/examples/rl/workflows/grad_worker.py new file mode 100644 index 000000000..5841a46b3 --- /dev/null +++ b/examples/rl/workflows/grad_worker.py @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from os import getenv +from os.path import dirname, realpath + +from maro.rl.learning import grad_worker + +workflow_dir = dirname(dirname(realpath(__file__))) # template directory +if workflow_dir not in sys.path: + sys.path.insert(0, workflow_dir) + +from general import log_dir, policy_func_dict + + +if __name__ == "__main__": + # TODO: WORKERID in docker compose script. + worker_id = getenv("WORKERID") + num_hosts = getenv("NUMHOSTS") + distributed = getenv("DISTRIBUTED") == "True" + if worker_id is None: + raise ValueError("missing environment variable: WORKERID") + if num_hosts is None: + if distributed: + raise ValueError("missing environment variable: NUMHOSTS") + else: + num_hosts = 0 + + group = getenv("LEARNGROUP", default="learn") + grad_worker( + policy_func_dict, + int(worker_id), + int(num_hosts), + group, + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir + ) diff --git a/examples/rl/workflows/policy_host.py b/examples/rl/workflows/policy_host.py index 745201096..198ba8738 100644 --- a/examples/rl/workflows/policy_host.py +++ b/examples/rl/workflows/policy_host.py @@ -16,8 +16,13 @@ if __name__ == "__main__": host_id = getenv("HOSTID") + data_parallel = getenv("DATAPARALLEL") == "True" + num_grad_workers = getenv("NUMGRADWORKERS") + if host_id is None: raise ValueError("missing environment variable: HOSTID") + if num_grad_workers is None: + num_grad_workers = 0 group = getenv("LEARNGROUP", default="learn") policy_host( @@ -28,5 +33,7 @@ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 }, + data_parallel=data_parallel, + num_grad_workers=int(num_grad_workers), log_dir=log_dir ) diff --git a/examples/rl/workflows/policy_manager.py b/examples/rl/workflows/policy_manager.py index b692d39de..a44108c1e 100644 --- a/examples/rl/workflows/policy_manager.py +++ b/examples/rl/workflows/policy_manager.py @@ -5,28 +5,56 @@ from os import getenv from os.path import dirname, realpath -from maro.rl.learning import DistributedPolicyManager, SimplePolicyManager +from maro.rl.learning import DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager +from maro.rl.policy import WorkerAllocator workflow_dir = dirname(dirname(realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from general import log_dir, policy_func_dict +from general import agent2policy, log_dir, policy_func_dict def get_policy_manager(): manager_type = getenv("POLICYMANAGERTYPE", default="simple") parallel = int(getenv("PARALLEL", default=0)) - if manager_type == "simple": - return SimplePolicyManager(policy_func_dict, parallel=parallel, log_dir=log_dir) - + data_parallel = getenv("DATAPARALLEL") == "True" + num_grad_workers = int(getenv("NUMGRADWORKERS", default=1)) group = getenv("LEARNGROUP", default="learn") + allocation_mode = getenv("ALLOCATIONMODE", default="by-policy") + allocator = WorkerAllocator(allocation_mode, num_grad_workers, list(policy_func_dict.keys()), agent2policy) + if manager_type == "simple": + if parallel == 0: + return SimplePolicyManager( + policy_func_dict, group, + data_parallel=data_parallel, + num_grad_workers=num_grad_workers, + worker_allocator=allocator, + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir) + else: + return MultiProcessPolicyManager( + policy_func_dict, group, + data_parallel=data_parallel, + num_grad_workers=num_grad_workers, + worker_allocator=allocator, + proxy_kwargs={ + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir) num_hosts = int(getenv("NUMHOSTS", default=5)) if manager_type == "distributed": policy_manager = DistributedPolicyManager( list(policy_func_dict.keys()), group, num_hosts, + data_parallel=data_parallel, + num_grad_workers=num_grad_workers, + worker_allocator=allocator, proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 + "max_peer_discovery_retries": 50 }, log_dir=log_dir ) diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index c0410e398..f80dc21e5 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -3,12 +3,15 @@ from .env_sampler import AbsEnvSampler from .learning_loop import learn -from .policy_manager import AbsPolicyManager, DistributedPolicyManager, SimplePolicyManager, policy_host +from .policy_manager import ( + AbsPolicyManager, DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager, grad_worker, policy_host +) from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, MultiProcessRolloutManager __all__ = [ "AbsEnvSampler", "learn", - "AbsPolicyManager", "DistributedPolicyManager", "SimplePolicyManager", "policy_host", + "AbsPolicyManager", "DistributedPolicyManager", "MultiProcessPolicyManager", "SimplePolicyManager", + "grad_worker", "policy_host", "AbsRolloutManager", "DistributedRolloutManager", "MultiProcessRolloutManager" ] diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 8f37b9d29..75f799d68 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -9,7 +9,7 @@ from typing import Callable, Dict, List from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.policy import RLPolicy +from maro.rl.policy import RLPolicy, WorkerAllocator from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger @@ -92,9 +92,15 @@ class SimplePolicyManager(AbsPolicyManager): Args: create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a function that takes policy name as the only parameter and return an ``RLPolicy`` instance. - parallel (bool): If True, the policies will be created in separate processes so that they can be updated in - parallel. Otherwise, they will be created by the manager itself, in which case they can only be updated - sequentially. Defaults to False. + group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless + if ``data_parallel`` is False. Defaults to "learn". + data_parallel (bool): If True, the policies training tasks will be sent to remote gradient workers for + data-parallel, otherwise learnt locally. Defaults to False. + num_grad_workers (int): Number of gradient workers, which is meaningless when ``data_parallel`` is False. + Defaults to 1. + worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies + for parallelization. + proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -102,49 +108,33 @@ class SimplePolicyManager(AbsPolicyManager): def __init__( self, create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - parallel: bool = False, + group: str = "learn", + data_parallel: bool = False, + num_grad_workers: int = 1, + worker_allocator: WorkerAllocator = None, + proxy_kwargs: dict = {}, log_dir: str = getcwd() ): super().__init__() self._policy_ids = list(create_policy_func_dict.keys()) - self._parallel = parallel + self._data_parallel = data_parallel + self._num_grad_workers = num_grad_workers + self._worker_allocator = worker_allocator self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - if parallel: - self._logger.info("Spawning policy host processes") - self._state_cache = {} - self._policy_hosts = [] - self._manager_end = {} - - def _policy_host(name, create_policy_func, conn): - policy = create_policy_func(name) - conn.send({"type": "init", "policy_state": policy.get_state()}) - while True: - msg = conn.recv() - if msg["type"] == "learn": - info = msg["rollout_info"] - if isinstance(info, list): - policy.update(info) - else: - policy.learn(info) - conn.send({"type": "learn_done", "policy_state": policy.get_state()}) - elif msg["type"] == "quit": - break - - for name, create_policy_func in create_policy_func_dict.items(): - manager_end, host_end = Pipe() - self._manager_end[name] = manager_end - host = Process(target=_policy_host, args=(name, create_policy_func, host_end)) - self._policy_hosts.append(host) - host.start() - - for policy_id, conn in self._manager_end.items(): - msg = conn.recv() - if msg["type"] == "init": - self._state_cache[policy_id] = msg["policy_state"] - self._logger.info(f"Initial state for policy {policy_id} cached") - else: - self._logger.info("Creating policy instances locally") - self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} + + self._worker_allocator.set_logger(self._logger) + self._logger.info("Creating policy instances locally") + self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} + + if self._data_parallel: + self._proxy = Proxy( + group, "policy_manager", {"grad_worker": self._num_grad_workers}, + component_name="POLICY_MANAGER", **proxy_kwargs) + + for name in create_policy_func_dict: + self._policy_dict[name].data_parallel( + group, "policy_host", {"grad_worker": self._num_grad_workers}, + component_name=f"POLICY_HOST.{name}", **proxy_kwargs) self._version = 0 @@ -155,20 +145,19 @@ def update(self, rollout_info: Dict[str, list]): information dictionaries computed directly by roll-out workers. """ t0 = time.time() - if self._parallel: - for policy_id, info in rollout_info.items(): - self._manager_end[policy_id].send({"type": "learn", "rollout_info": info}) - for policy_id, conn in self._manager_end.items(): - msg = conn.recv() - if msg["type"] == "learn_done": - self._state_cache[policy_id] = msg["policy_state"] - self._logger.info(f"Cached state for policy {policy_id}") - else: - for policy_id, info in rollout_info.items(): - if isinstance(info, list): - self._policy_dict[policy_id].update(info) + if self._data_parallel: + # re-allocate grad workers before update. + self._policy2workers, self._worker2policies = self._worker_allocator.allocate() + + for policy_id, info_list in rollout_info.items(): + # in some cases e.g. Actor-Critic that get loss from rollout workers + if isinstance(info_list, list): + self._policy_dict[policy_id].update(info_list) + else: + if self._data_parallel: + self._policy_dict[policy_id].learn_with_data_parallel(info_list, self._policy2workers[policy_id]) else: - self._policy_dict[policy_id].learn(info) + self._policy_dict[policy_id].learn(info_list) self._logger.info(f"Updated policies {list(rollout_info.keys())}") self._version += 1 @@ -176,10 +165,141 @@ def update(self, rollout_info: Dict[str, list]): def get_state(self): """Get the latest policy states.""" - if self._parallel: - return self._state_cache - else: - return {name: policy.get_state() for name, policy in self._policy_dict.items()} + return {name: policy.get_state() for name, policy in self._policy_dict.items()} + + def get_version(self): + """Get the collective policy version.""" + return self._version + + def exit(self): + """Tell the policy host processes to exit.""" + if self._data_parallel: + self._proxy.close() + for name in self._policy_ids: + self._policy_dict[name].exit_data_parallel() + + +class MultiProcessPolicyManager(AbsPolicyManager): + """Policy manager with multi-processing parallism that contains all policy instances. + + Args: + create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a + function that takes policy name as the only parameter and return an ``RLPolicy`` instance. + group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless + if ``data_parallel`` is False. Defaults to "learn". + data_parallel (bool): If True, the policies training tasks will be sent to remote gradient workers for + data-parallel, otherwise learnt locally. Defaults to False. + num_grad_workers (int): Number of gradient workers, which is meaningless when ``data_parallel`` is False. + Defaults to 1. + worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies + for parallelization. + proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init + time and this directory will be used to save the log files generated by it. Defaults to the current + working directory. + """ + def __init__( + self, + create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], + group: str = "learn", + data_parallel: bool = False, + num_grad_workers: int = 1, + worker_allocator: WorkerAllocator = None, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() + ): + super().__init__() + self._policy_ids = list(create_policy_func_dict.keys()) + self._data_parallel = data_parallel + self._num_grad_workers = num_grad_workers + self._worker_allocator = worker_allocator + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) + + self._worker_allocator.set_logger(self._logger) + + if self._data_parallel: + self._proxy = Proxy( + group, "policy_manager", {"grad_worker": self._num_grad_workers}, + component_name="POLICY_MANAGER", **proxy_kwargs) + + self._logger.info("Spawning policy host processes") + self._state_cache = {} + self._policy_hosts = [] + self._manager_end = {} + + def _policy_host(name: str, create_policy_func: Callable[[str], RLPolicy], conn: Pipe): + policy = create_policy_func(name) + if self._data_parallel: + self._logger.info("========== data parallel mode ==========") + policy.data_parallel( + group, "policy_host", {"grad_worker": self._num_grad_workers}, component_name=f"POLICY_HOST.{name}", + **proxy_kwargs) + + conn.send({"type": "init", "policy_state": policy.get_state()}) + while True: + msg = conn.recv() + if msg["type"] == "learn": + info_list = msg["rollout_info"] + policy2workers = msg["policy2workers"] + if isinstance(info_list, list): + # in some cases e.g. Actor-Critic that get loss from rollout workers + policy.update(info_list) + else: + if self._data_parallel: + policy.learn_with_data_parallel(info_list, policy2workers[name]) + else: + policy.learn(info_list) + conn.send({"type": "learn_done", "policy_state": policy.get_state()}) + elif msg["type"] == "quit": + if self._data_parallel: + policy.exit_data_parallel() + break + + # start host process + for name, create_policy_func in create_policy_func_dict.items(): + manager_end, host_end = Pipe() + self._manager_end[name] = manager_end + host = Process(target=_policy_host, args=(name, create_policy_func, host_end)) + self._policy_hosts.append(host) + host.start() + + for policy_id, conn in self._manager_end.items(): + msg = conn.recv() + if msg["type"] == "init": + self._state_cache[policy_id] = msg["policy_state"] + self._logger.info(f"Initial state for policy {policy_id} cached") + + self._version = 0 + + def update(self, rollout_info: Dict[str, list]): + """Update policies using roll-out information. + + The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss + information computed directly by roll-out workers. + """ + t0 = time.time() + if self._data_parallel: + # re-allocate grad workers before update. + self._policy2workers, self._worker2policies = self._worker_allocator.allocate() + + for policy_id, info_list in rollout_info.items(): + self._manager_end[policy_id].send( + {"type": "learn", "rollout_info": info_list, "policy2workers": self._policy2workers}) + for policy_id, conn in self._manager_end.items(): + msg = conn.recv() + if msg["type"] == "learn_done": + self._state_cache[policy_id] = msg["policy_state"] + self._logger.info(f"Cached state for policy {policy_id}") + else: + self._logger.info(f"Warning: Wrong message type: {msg['type']}") + + self._logger.info(f"Updated policies {list(rollout_info.keys())}") + self._version += 1 + self._logger.info(f"policy update time: {time.time() - t0}") + + def get_state(self): + """Get the latest policy states.""" + return self._state_cache def get_version(self): """Get the collective policy version.""" @@ -187,9 +307,10 @@ def get_version(self): def exit(self): """Tell the policy host processes to exit.""" - if self._parallel: - for conn in self._manager_end.values(): - conn.send({"type": "quit"}) + for conn in self._manager_end.values(): + conn.send({"type": "quit"}) + if self._data_parallel: + self._proxy.close() class DistributedPolicyManager(AbsPolicyManager): @@ -199,28 +320,44 @@ class DistributedPolicyManager(AbsPolicyManager): policy_ids (List[str]): Names of the registered policies. group (str): Group name for the cluster consisting of the manager and all policy hosts. num_hosts (int): Number of hosts. The hosts will be identified by "POLICY_HOST.i", where 0 <= i < num_hosts. + data_parallel (bool): Whether to train policy on remote gradient workers or locally on policy hosts. + num_grad_workers (int): Number of gradient workers, which is meaningless when ``data_parallel`` is False. + Defaults to 1. + worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies + for parallelization. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. """ def __init__( self, policy_ids: List[str], group: str, num_hosts: int, - log_dir: str = getcwd(), - proxy_kwargs: dict = {} + data_parallel: bool = False, + num_grad_workers: int = 1, + worker_allocator: WorkerAllocator = None, + proxy_kwargs: dict = {}, + log_dir: str = getcwd() ): super().__init__() self._policy_ids = policy_ids peers = {"policy_host": num_hosts} + if data_parallel: + peers["grad_worker"] = num_grad_workers self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) + self._worker_allocator = worker_allocator + self._data_parallel = data_parallel + + self._worker_allocator.set_logger(self._logger) self._policy2host = {} + self._policy2workers = {} self._host2policies = defaultdict(list) + self._worker2policies = defaultdict(list) # assign policies to hosts for i, name in enumerate(self._policy_ids): @@ -255,10 +392,14 @@ def update(self, rollout_info: Dict[str, list]): The roll-out information is grouped by policy name and may be either a training batch or a list if loss information dictionaries computed directly by roll-out workers. """ + if self._data_parallel: + self._policy2workers, self._worker2policies = self._worker_allocator.allocate() + msg_dict = defaultdict(lambda: defaultdict(dict)) - for policy_id, info in rollout_info.items(): + for policy_id, info_list in rollout_info.items(): host_id_str = self._policy2host[policy_id] - msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_id] = info + msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_id] = info_list + msg_dict[host_id_str][MsgKey.WORKER_INFO][policy_id] = self._policy2workers dones = 0 self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_dict.items())) @@ -294,6 +435,8 @@ def policy_host( host_idx: int, group: str, proxy_kwargs: dict = {}, + data_parallel: bool = False, + num_grad_workers: int = 1, log_dir: str = getcwd() ): """Policy host process that can be launched on separate computation nodes. @@ -306,10 +449,19 @@ def policy_host( manages them. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. + data_parallel (bool): Whether to train policy on remote gradient workers to perform data-parallel. + Defaluts to False. + num_grad_workers (int): The number of gradient worker nodes in data-parallel mode. Defaluts to 1. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ policy_dict = {} - proxy = Proxy(group, "policy_host", {"policy_manager": 1}, component_name=f"POLICY_HOST.{host_idx}", **proxy_kwargs) + peers = {"policy_manager": 1} + if data_parallel: + peers["grad_worker"] = num_grad_workers + + proxy = Proxy( + group, "policy_host", peers, + component_name=f"POLICY_HOST.{host_idx}", **proxy_kwargs) logger = Logger(proxy.name, dump_folder=log_dir) for msg in proxy.receive(): @@ -317,10 +469,11 @@ def policy_host( logger.info("Exiting...") proxy.close() break - - if msg.tag == MsgTag.INIT_POLICIES: + elif msg.tag == MsgTag.INIT_POLICIES: for name in msg.body[MsgKey.POLICY_IDS]: policy_dict[name] = create_policy_func_dict[name](name) + if data_parallel: + policy_dict[name].data_parallel_with_existing_proxy(proxy) logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_IDS]}") proxy.reply( @@ -331,15 +484,90 @@ def policy_host( elif msg.tag == MsgTag.LEARN: t0 = time.time() for name, info in msg.body[MsgKey.ROLLOUT_INFO].items(): + # in some cases e.g. Actor-Critic that get loss from rollout workers if isinstance(info, list): logger.info("updating with loss info") policy_dict[name].update(info) else: - logger.info("learning from batch") - policy_dict[name].learn(info) + if data_parallel: + logger.info("learning on remote grad workers") + policy2workers = msg.body[MsgKey.WORKER_INFO][name] + policy_dict[name].learn_with_data_parallel(info, policy2workers[name]) + else: + logger.info("learning from batch") + policy_dict[name].learn(info) msg_body = { MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.ROLLOUT_INFO]} } - logger.debug(f"total policy update time: {time.time() - t0}") + logger.info(f"total policy update time: {time.time() - t0}") proxy.reply(msg, tag=MsgTag.LEARN_DONE, body=msg_body) + else: + logger.info(f"Wrong message tag: {msg.tag}") + raise TypeError + + +def grad_worker( + create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], + worker_idx: int, + num_hosts: int, + group: str, + proxy_kwargs: dict = {}, + max_policy_number: int = 10, + log_dir: str = getcwd() +): + """Stateless gradient workers that excute gradient computation tasks. + + Args: + create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy + creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. + worker_idx (int): Integer worker index. The worker's ID in the cluster will be "GRAD_WORKER.{worker_idx}". + num_hosts (int): Number of policy hosts, which is required to find peers in proxy initialization. + num_hosts=0 means policy hosts are hosted by policy manager while no remote nodes for them. + group (str): Group name for the training cluster, which includes all policy hosts and a policy manager that + manages them. + proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + for details. Defaults to the empty dictionary. + max_policy_number (int): Maximum policy number in a single worker node. Defaults to 10. + log_dir (str): Directory to store logs in. Defaults to the current working directory. + """ + policy_dict = {} + active_policies = [] + if num_hosts == 0: + # no remote nodes for policy hosts + num_hosts = len(create_policy_func_dict) + peers = {"policy_manager": 1, "policy_host": num_hosts} + proxy = Proxy(group, "grad_worker", peers, component_name=f"GRAD_WORKER.{worker_idx}", **proxy_kwargs) + logger = Logger(proxy.name, dump_folder=log_dir) + + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + elif msg.tag == MsgTag.COMPUTE_GRAD: + t0 = time.time() + msg_body = {MsgKey.LOSS_INFO: dict(), MsgKey.POLICY_IDS: list()} + for name, batch in msg.body[MsgKey.GRAD_TASK].items(): + if name not in policy_dict: + if len(policy_dict) > max_policy_number: + # remove the oldest one when size exceeds. + policy_to_remove = active_policies.pop() + policy_dict.pop(policy_to_remove) + policy_dict[name] = create_policy_func_dict[name](name) + active_policies.insert(0, name) + logger.info(f"Initialized policies {name}") + + policy_dict[name].set_state(msg.body[MsgKey.POLICY_STATE][name]) + loss_info = policy_dict[name].get_batch_loss(batch, explicit_grad=True) + msg_body[MsgKey.LOSS_INFO][name] = loss_info + msg_body[MsgKey.POLICY_IDS].append(name) + # put the latest one to queue head + active_policies.remove(name) + active_policies.insert(0, name) + + logger.debug(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, tag=MsgTag.COMPUTE_GRAD_DONE, body=msg_body) + else: + logger.info(f"Wrong message tag: {msg.tag}") + raise TypeError diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index 41166428c..fa76533cb 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -6,6 +6,7 @@ from .dqn import DQN, PrioritizedExperienceReplay from .pg import PolicyGradient from .policy import AbsPolicy, DummyPolicy, RLPolicy +from .worker_allocator import WorkerAllocator __all__ = [ "ActorCritic", @@ -13,4 +14,5 @@ "DQN", "PrioritizedExperienceReplay", "PolicyGradient", "AbsPolicy", "DummyPolicy", "RLPolicy" + "WorkerAllocator" ] diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 52977204c..d7c052883 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -8,8 +8,9 @@ import torch from torch.distributions import Categorical +from maro.communication import SessionMessage from maro.rl.modeling import DiscreteACNet -from maro.rl.utils import average_grads, discount_cumsum +from maro.rl.utils import MsgKey, MsgTag, average_grads, discount_cumsum from .policy import RLPolicy @@ -258,6 +259,36 @@ def improve(self): """Learn using data from the buffer.""" self.learn(self._get_batch()) + def learn_with_data_parallel(self, batch: dict, worker_id_list: list): + assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." + + for _ in range(self.grad_iters): + msg_dict = defaultdict(lambda: defaultdict(dict)) + for i, worker_id in enumerate(worker_id_list): + sub_batch = {key: batch[key][i::len(worker_id_list)] for key in batch} + msg_dict[worker_id][MsgKey.GRAD_TASK][self._name] = sub_batch + msg_dict[worker_id][MsgKey.POLICY_STATE][self._name] = self.get_state() + # data-parallel + self._proxy.isend(SessionMessage( + MsgTag.COMPUTE_GRAD, self._proxy.name, worker_id, body=msg_dict[worker_id])) + dones = 0 + loss_info_by_policy = {self._name: []} + for msg in self._proxy.receive(): + if msg.tag == MsgTag.COMPUTE_GRAD_DONE: + for policy_name, loss_info in msg.body[MsgKey.LOSS_INFO].items(): + if isinstance(loss_info, list): + loss_info_by_policy[policy_name] += loss_info + elif isinstance(loss_info, dict): + loss_info_by_policy[policy_name].append(loss_info) + else: + raise TypeError(f"Wrong type of loss_info: {type(loss_info)}") + dones += 1 + if dones == len(msg_dict): + break + # build dummy computation graph by `get_batch_loss` before apply gradients. + _ = self.get_batch_loss(sub_batch, explicit_grad=True) + self.update(loss_info_by_policy[self._name]) + def set_state(self, policy_state): self.ac_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 878ddab8a..673baddbe 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -1,14 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import defaultdict from typing import Callable, List, Tuple, Union import numpy as np import torch +from maro.communication import SessionMessage from maro.rl.exploration import gaussian_noise from maro.rl.modeling import ContinuousACNet -from maro.rl.utils import average_grads +from maro.rl.utils import MsgKey, MsgTag, average_grads from maro.utils import clone from .policy import RLPolicy @@ -230,6 +232,40 @@ def improve(self): if self._ac_net_version - self._target_ac_net_version == self.update_target_every: self._update_target() + def learn_with_data_parallel(self, batch: dict, worker_id_list: list): + assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." + + self._replay_memory.put( + batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] + ) + for _ in range(self.num_epochs): + msg_dict = defaultdict(lambda: defaultdict(dict)) + for worker_id in worker_id_list: + msg_dict[worker_id][MsgKey.GRAD_TASK][self._name] = self._replay_memory.sample( + self.train_batch_size // len(worker_id_list)) + msg_dict[worker_id][MsgKey.POLICY_STATE][self._name] = self.get_state() + # data-parallel by multiple hosts/workers + self._proxy.isend(SessionMessage( + MsgTag.COMPUTE_GRAD, self._proxy.name, worker_id, body=msg_dict[worker_id])) + dones = 0 + loss_info_by_policy = {self._name: []} + for msg in self._proxy.receive(): + if msg.tag == MsgTag.COMPUTE_GRAD_DONE: + for policy_name, loss_info in msg.body[MsgKey.LOSS_INFO].items(): + if isinstance(loss_info, list): + loss_info_by_policy[policy_name] += loss_info + elif isinstance(loss_info, dict): + loss_info_by_policy[policy_name].append(loss_info["grad"]) + else: + raise TypeError(f"Wrong type of loss_info: {type(loss_info)}") + dones += 1 + if dones == len(msg_dict): + break + # build dummy computation graph by `get_batch_loss` before apply gradients. + # batch_size=2 because torch.nn.functional.batch_norm doesn't support batch_size=1. + _ = self.get_batch_loss(self._replay_memory.sample(2), explicit_grad=True) + self.update(loss_info_by_policy[self._name]) + def _update_target(self): # soft-update target network self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index 25b3af1bd..edb5ff352 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -1,14 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from collections import defaultdict from typing import Callable, List, Tuple, Union import numpy as np import torch +from maro.communication import SessionMessage from maro.rl.exploration import epsilon_greedy from maro.rl.modeling import DiscreteQNet -from maro.rl.utils import average_grads +from maro.rl.utils import MsgKey, MsgTag, average_grads from maro.utils import clone from .policy import RLPolicy @@ -92,6 +94,8 @@ def sample(self, size: int): return indexes, is_weights def _get_priority(self, error): + if isinstance(error, torch.Tensor): + error = error.detach().numpy() return (np.abs(error) + self.eps) ** self.alpha def _update(self, idx, delta): @@ -269,9 +273,11 @@ def get_rollout_info(self): """ return self._replay_memory.sample(self.rollout_batch_size) - def _get_batch(self): + def _get_batch(self, batch_size: int = None): + if batch_size is None: + batch_size = self.train_batch_size if self.prioritized_replay: - indexes, is_weights = self._per.sample(self.train_batch_size) + indexes, is_weights = self._per.sample(batch_size) return { "states": self._replay_memory.states[indexes], "actions": self._replay_memory.actions[indexes], @@ -368,6 +374,39 @@ def _update_target(self): self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) self._target_q_net_version = self._q_net_version + def learn_with_data_parallel(self, batch: dict, worker_id_list: list): + assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." + + self._replay_memory.put( + batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] + ) + for _ in range(self.num_epochs): + msg_dict = defaultdict(lambda: defaultdict(dict)) + for worker_id in worker_id_list: + msg_dict[worker_id][MsgKey.GRAD_TASK][self._name] = self._get_batch( + self.train_batch_size // len(worker_id_list)) + msg_dict[worker_id][MsgKey.POLICY_STATE][self._name] = self.get_state() + # data-parallel by multiple remote gradient workers + self._proxy.isend(SessionMessage( + MsgTag.COMPUTE_GRAD, self._proxy.name, worker_id, body=msg_dict[worker_id])) + dones = 0 + loss_info_by_policy = {self._name: []} + for msg in self._proxy.receive(): + if msg.tag == MsgTag.COMPUTE_GRAD_DONE: + for policy_name, loss_info in msg.body[MsgKey.LOSS_INFO].items(): + if isinstance(loss_info, list): + loss_info_by_policy[policy_name] += loss_info + elif isinstance(loss_info, dict): + loss_info_by_policy[policy_name].append(loss_info) + else: + raise TypeError(f"Wrong type of loss_info: {type(loss_info)}") + dones += 1 + if dones == len(msg_dict): + break + # build dummy computation graph before apply gradients. + _ = self.get_batch_loss(self._get_batch(), explicit_grad=True) + self.update(loss_info_by_policy[self._name]) + def exploration_step(self): """Update the exploration parameters according to the exploration scheduler.""" for sch in self.exploration_schedulers: diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index 03a29bfac..b4b3c86b6 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -7,8 +7,9 @@ import numpy as np import torch +from maro.communication import SessionMessage from maro.rl.modeling import DiscretePolicyNet -from maro.rl.utils import average_grads, discount_cumsum +from maro.rl.utils import MsgKey, MsgTag, average_grads, discount_cumsum from .policy import RLPolicy @@ -177,6 +178,36 @@ def improve(self): """Learn using data from the buffer.""" self.learn(self._get_batch()) + def learn_with_data_parallel(self, batch: dict, worker_id_list: list): + assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." + + for _ in range(self.grad_iters): + msg_dict = defaultdict(lambda: defaultdict(dict)) + for i, worker_id in enumerate(worker_id_list): + sub_batch = {key: batch[key][i::len(worker_id_list)] for key in batch} + msg_dict[worker_id][MsgKey.GRAD_TASK][self._name] = sub_batch + msg_dict[worker_id][MsgKey.POLICY_STATE][self._name] = self.get_state() + # data-parallel + self._proxy.isend(SessionMessage( + MsgTag.COMPUTE_GRAD, self._proxy.name, worker_id, body=msg_dict[worker_id])) + dones = 0 + loss_info_by_policy = {self._name: []} + for msg in self._proxy.receive(): + if msg.tag == MsgTag.COMPUTE_GRAD_DONE: + for policy_name, loss_info in msg.body[MsgKey.LOSS_INFO].items(): + if isinstance(loss_info, list): + loss_info_by_policy[policy_name] += loss_info + elif isinstance(loss_info, dict): + loss_info_by_policy[policy_name].append(loss_info["grad"]) + else: + raise TypeError(f"Wrong type of loss_info: {type(loss_info)}") + dones += 1 + if dones == len(msg_dict): + break + # build dummy computation graph before apply gradients. + _ = self.get_batch_loss(sub_batch, explicit_grad=True) + self.policy_net.step(loss_info_by_policy[self._name]) + def set_state(self, policy_state): self.policy_net.load_state_dict(policy_state) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py index 01e2f08dc..277862cf8 100644 --- a/maro/rl/policy/policy.py +++ b/maro/rl/policy/policy.py @@ -6,6 +6,8 @@ import numpy as np +from maro.communication import Proxy + class AbsPolicy(ABC): """Abstract policy class. @@ -91,6 +93,22 @@ def get_batch_loss(self, batch: dict, explicit_grad: bool = False): """ pass + def data_parallel(self, *args, **kwargs): + """"Initialize a proxy in the policy, for data-parallel training. + Using the same arguments as `Proxy`.""" + self._proxy = Proxy(*args, **kwargs) + + def data_parallel_with_existing_proxy(self, proxy): + """"Initialize a proxy in the policy with an existing one, for data-parallel training.""" + self._proxy = proxy + + def exit_data_parallel(self): + if hasattr(self, '_proxy'): + self._proxy.close() + + def learn_with_data_parallel(self): + pass + def update(self, loss_info_list: List[dict]): """Update with loss information computed by multiple sources. diff --git a/maro/rl/policy/worker_allocator.py b/maro/rl/policy/worker_allocator.py new file mode 100644 index 000000000..1e4894b06 --- /dev/null +++ b/maro/rl/policy/worker_allocator.py @@ -0,0 +1,123 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict +from enum import Enum +from typing import Dict + + +class AllocationMode(Enum): + BY_POLICY = "by-policy" + BY_AGENT = "by-agent" + BY_EXPERIENCE = "by-experience" + + +class WorkerAllocator(object): + """Allocate workers following some strategy.""" + def __init__(self, mode: str, num_workers: int, policy_names: list, agent2policy: dict): + assert num_workers > 0, f"Invalid arguments: num_workers should be greater than 0 instead of {num_workers}." + assert len(policy_names) > 0, "Invalid arguments: policy_names should not be empty." + assert len(agent2policy) > 0, "Invalid arguments: agent2policy should not be empty." + self.mode = mode + self.num_workers = num_workers + self.policy_names = policy_names + self.agent2policy = agent2policy + self.worker_prefix = "GRAD_WORKER" + self.logger = None + + self._cached_mappings = dict() + + def set_logger(self, logger): + self.logger = logger + + def allocate(self, **kwargs): + if self.mode not in self._cached_mappings: + if self.mode == AllocationMode.BY_POLICY.value: + self._cached_mappings[self.mode] = self.allocate_by_policy() + elif self.mode == AllocationMode.BY_AGENT.value: + self._cached_mappings[self.mode] = self.allocate_by_agent() + elif self.mode == AllocationMode.BY_EXPERIENCE.value: + assert 'num_experiences_by_policy' in kwargs + num_experiences_by_policy = kwargs.pop('num_experiences_by_policy') + self._cached_mappings[self.mode] = self.allocate_by_experience(num_experiences_by_policy) + else: + raise NotImplementedError(f"{self.mode} is not implemented.") + return self._cached_mappings[self.mode] + + def allocate_by_policy(self): + """Evenly allocate grad workers to each policy.""" + policy_names = self.policy_names + num_workers = self.num_workers + policy2workers = defaultdict(list) + worker2policies = defaultdict(list) + + if len(policy_names) >= num_workers: + for i, name in enumerate(policy_names): + worker_id = i % num_workers + policy2workers[name].append(f"{self.worker_prefix}.{worker_id}") + worker2policies[f"{self.worker_prefix}.{worker_id}"].append(name) + else: + worker_id_list = list(range(num_workers)) + for i, name in enumerate(policy_names): + for worker_id in worker_id_list[i::len(policy_names)]: + policy2workers[name].append(f"{self.worker_prefix}.{worker_id}") + worker2policies[f"{self.worker_prefix}.{worker_id}"].append(name) + return policy2workers, worker2policies + + def allocate_by_agent(self): + agent2policy = self.agent2policy + num_agents_by_policy = {} + for agent_id, policy_name in agent2policy.items(): + num_agents_by_policy[policy_name] = num_agents_by_policy.get(policy_name, 0) + 1 + return self._allocate_by_payload(num_agents_by_policy) + + def allocate_by_experience(self, num_experiences_by_policy: dict): + return self._allocate_by_payload(num_experiences_by_policy) + + def _allocate_by_payload(self, num_payload: Dict[str, int]): + """Allocate grad workers by payload of each policy. + + Args: + num_payload (Dict[str, int]): Payload of each policy, could be experience numbers + or agent nums. + + Returns: + policy2workers (Dict[str, list]): The mapping from policy name to assigned worker ids. + worker2policies (Dict[str, list]): The mapping from worker id to according policies. + """ + num_workers = self.num_workers + policy2workers = defaultdict(list) + worker2policies = defaultdict(list) + + # no payload yet + if len(num_payload) == 0: + return self.allocate_by_policy() + # allocate workers according to historical payload. + else: + total_num_payload = sum(num_payload.values()) + average_payload = total_num_payload / num_workers + + offset = 0 + policy_quota = dict() + for name, payload in num_payload.items(): + quota = payload / average_payload + quota = max(1, int(round(quota))) + policy_quota[name] = quota + + # adjust quota if any redundancy occurs. + redundancy = num_workers - sum(policy_quota.values()) + if redundancy > 0: + busiest_policy = max(policy_quota, key=lambda name: policy_quota[name]) + policy_quota[busiest_policy] += redundancy + + for name, quota in policy_quota.items(): + if self.logger is not None: + self.logger.info( + f"policy {name} payload: {num_payload[name]}, quota: {quota} node(s)") + for i in range(quota): + worker_id = (i + offset) % num_workers + policy2workers[name].append(f"{self.worker_prefix}.{worker_id}") + worker2policies[f"{self.worker_prefix}.{worker_id}"].append(name) + offset = (offset + quota) % num_workers + + return policy2workers, worker2policies diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 49302e681..1cb1d9f62 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -35,6 +35,7 @@ class MsgKey(Enum): ROLLOUT_INFO = "rollout_info" TRACKER = "tracker" GRAD_TASK = "grad_task" + WORKER_INFO = "worker_info" LOSS_INFO = "loss_info" STATE = "state" POLICY_STATE = "policy_state" From 5f70b65f124f9086cbf3c8eac3ad98decca1d707 Mon Sep 17 00:00:00 2001 From: ysqyang Date: Fri, 17 Sep 2021 16:13:58 +0800 Subject: [PATCH 440/482] added checkpointing for simple and multi-process policy managers --- docs/source/apidoc/maro.rl.rst | 197 ++-------------------- docs/source/images/rl/env_sampler.svg | 2 +- docs/source/images/rl/learning_cycle.svg | 2 +- docs/source/images/rl/rollout_manager.svg | 2 +- docs/source/key_components/rl_toolkit.rst | 22 ++- maro/rl/learning/policy_manager.py | 156 ++++++++++------- maro/rl/modeling/core_model.py | 26 ++- maro/rl/policy/ac.py | 16 +- maro/rl/policy/ddpg.py | 29 +++- maro/rl/policy/dqn.py | 39 +++-- maro/rl/policy/pg.py | 16 +- 11 files changed, 227 insertions(+), 280 deletions(-) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index b0687f81b..c0d9f19e2 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -1,222 +1,47 @@ -Algorithms -================================================================================ - -maro.rl.algorithms.ac --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.algorithms.ac - :members: - :undoc-members: - :show-inheritance: - -maro.rl.algorithms.dqn --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.algorithms.dqn - :members: - :undoc-members: - :show-inheritance: - -maro.rl.algorithms.ddpg --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.algorithms.ddpg - :members: - :undoc-members: - :show-inheritance: - -maro.rl.algorithms.pg --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.agent.pg - :members: - :undoc-members: - :show-inheritance: - - -Experience -================================================================================ - -maro.rl.experience.experience_memory --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.experience.experience_memory - :members: - :undoc-members: - :show-inheritance: - -maro.rl.experience.sampler --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.experience.sampler - :members: - :undoc-members: - :show-inheritance: - - Exploration ================================================================================ -maro.rl.exploration.abs_exploration --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.exploration.abs_exploration - :members: - :undoc-members: - :show-inheritance: - -maro.rl.exploration.epsilon_greedy_exploration --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.exploration.epsilon_greedy_exploration - :members: - :undoc-members: - :show-inheritance: - -maro.rl.exploration.noise_exploration --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.exploration.noise_exploration - :members: - :undoc-members: - :show-inheritance: - -maro.rl.exploration.exploration_scheduler --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.exploration.exploration_scheduler - :members: - :undoc-members: - :show-inheritance: - - -Model -================================================================================ - -maro.rl.model.abs_block +maro.rl.exploration.strategies -------------------------------------------------------------------------------- -.. automodule:: maro.rl.model.abs_block +.. automodule:: maro.rl.exploration.strategies :members: :undoc-members: :show-inheritance: -maro.rl.model.core_model --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.model.core_model - :members: - :undoc-members: - :show-inheritance: -maro.rl.model.fc_block +maro.rl.exploration.scheduling -------------------------------------------------------------------------------- -.. automodule:: maro.rl.model.fc_block +.. automodule:: maro.rl.exploration.scheduling :members: :undoc-members: :show-inheritance: -Policy +Modeling ================================================================================ -maro.rl.policy.policy +maro.rl.modeling.core_model -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.policy +.. automodule:: maro.rl.modeling.core_model :members: :undoc-members: :show-inheritance: -maro.rl.policy.policy_manager +maro.rl.modeling.fc_block -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.policy_manager +.. automodule:: maro.rl.modeling.fc_block :members: :undoc-members: :show-inheritance: -maro.rl.policy.trainer +maro.rl.modeling.specials -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.trainer +.. automodule:: maro.rl.modeling.specials :members: :undoc-members: :show-inheritance: - - -Learning -================================================================================ - -maro.rl.learning.env_wrapper --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.env_wrapper - :members: - :undoc-members: - :show-inheritance: - -maro.rl.learning.agent_wrapper --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.agent_wrapper - :members: - :undoc-members: - :show-inheritance: - -maro.rl.learning.early_stopper --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.early_stopper - :members: - :undoc-members: - :show-inheritance: - -maro.rl.learning.simple_learner --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.simple_learner - :members: - :undoc-members: - :show-inheritance: - -maro.rl.learning.synchronous.learner --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.synchronous.learner - :members: - :undoc-members: - :show-inheritance: - -maro.rl.learning.synchronous.rollout_manager --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.synchronous.rollout_manager - :members: - :undoc-members: - :show-inheritance: - -maro.rl.learning.synchronous.rollout_worker --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.synchronous.rollout_worker - :members: - :undoc-members: - :show-inheritance: - -maro.rl.learning.asynchronous.actor --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.asynchronous.actor - :members: - :undoc-members: - :show-inheritance: - -maro.rl.learning.asynchronous.policy_server --------------------------------------------------------------------------------- - -.. automodule:: maro.rl.learning.asynchronous.policy_server - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/images/rl/env_sampler.svg b/docs/source/images/rl/env_sampler.svg index a56026596..7ccffae1d 100644 --- a/docs/source/images/rl/env_sampler.svg +++ b/docs/source/images/rl/env_sampler.svg @@ -1,3 +1,3 @@ -
get_state()
get_state()
Environment Simulator
Environment S...
get_env_actions()
get_env_actions()
Environment Sampler
Environment Sampler
Agent
Agent
Agent
Agent
Policy
Policy
state by agent
state by agent
action by agent
action by age...
get_reward()
get_reward()
agent-policy mapping
agent-policy...
state batch by policy
state batch by po...
agent-policy mapping
agent-policy...
action batch by policy
action batch by pol...
Viewer does not support full SVG 1.1
\ No newline at end of file +
get_state()
get_state()
Environment Simulator
Environment S...
get_env_actions()
get_env_actions()
Environment Sampler
Environment Sampler
Agent
Agent
Agent
Agent
Policy
Policy
state by agent
state by agent
action by agent
action by age...
get_reward()
get_reward()
agent-policy mapping
agent-policy...
state batch by policy
state batch by po...
agent-policy mapping
agent-policy...
action batch by policy
action batch by pol...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/learning_cycle.svg b/docs/source/images/rl/learning_cycle.svg index 74f77815e..9705e800c 100644 --- a/docs/source/images/rl/learning_cycle.svg +++ b/docs/source/images/rl/learning_cycle.svg @@ -1,3 +1,3 @@ -
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
roll-out info
roll-out i...
Policy
Manager
Policy...
policy states
policy stat...
Synchronous Learning Cycle
Synchronous Learning Cycle
Viewer does not support full SVG 1.1
\ No newline at end of file +
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
roll-out info
roll-out i...
Policy
Manager
Policy...
policy states
policy stat...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/rollout_manager.svg b/docs/source/images/rl/rollout_manager.svg index aa70df3c0..6f296e4a8 100644 --- a/docs/source/images/rl/rollout_manager.svg +++ b/docs/source/images/rl/rollout_manager.svg @@ -1,3 +1,3 @@ -
Roll-out Worker
Roll-out Worker
Rollout Manager
Rollout Manager
collect
colle...
evaluate
evalu...
env sampler
env sampler
roll-out message
roll-out messa...
results
results
Roll-out Manager
Roll-out Manager
Viewer does not support full SVG 1.1
\ No newline at end of file +
Roll-out Worker
Roll-out Worker
Rollout Manager
Rollout Manager
collect
colle...
evaluate
evalu...
env sampler
env sampler
roll-out message
roll-out messa...
results
results
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 8e5bedb36..b28da9bb1 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -8,7 +8,7 @@ distributed learning workflows. The distributed workflow can be synchronous or a Synchronous Learning -==================== +-------------------- In synchronous mode, a main process executes 2-phase learning cycles consisting of simulation data collection and policy update. The data collection phase is controlled by a roll-out manager and the policy update phase is controlled @@ -18,18 +18,20 @@ policy manager. On the other hand, the policy manager always waits until all pol policy states to the roll-out manager. -.. image:: ../images/rl/learning_cycle.svg - :target: ../images/rl/learner.svg +.. figure:: ../images/rl/learning_cycle.svg :alt: Overview + + Synchronous Learning Cycle -.. image:: ../images/rl/rollout_manager.svg - :target: ../images/rl/rollout_manager.svg +.. figure:: ../images/rl/rollout_manager.svg :alt: Overview + Roll-out Manager + Asynchronous Learning -===================== +--------------------- The asynchronous mode consists of a policy server and multiple actors in a client-server architecture. Each actor runs its own roll-out loop and periodically sends the collected data to the server. The server uses the data to update the @@ -47,10 +49,11 @@ and reward shaping to collect roll-out information for learning and testing purp easily turned into a roll-out worker or an actor for synchronous and asynchronous learning, respectively. -.. image:: ../images/rl/env_sampler.svg - :target: ../images/rl/env_sampler.svg +.. figure:: ../images/rl/env_sampler.svg :alt: Overview + Environment Sampler + Policy ------ @@ -95,6 +98,7 @@ provides various policy improvement interfaces to support single-threaded and di def improve(self): pass + .. _policy-manager: Policy Manager @@ -175,7 +179,7 @@ The code snippet below shows how to create a model for the actor-critic algorith self.actor_optim.zero_grad() self.critic_optim.zero_grad() loss.backward() - self.hsared_optim.step() + self.shared_optim.step() self.actor_optim.step() self.critic_optim.step() diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 75f799d68..7bc192f53 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import os +import threading import time from abc import ABC, abstractmethod from collections import defaultdict @@ -92,6 +94,12 @@ class SimplePolicyManager(AbsPolicyManager): Args: create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a function that takes policy name as the only parameter and return an ``RLPolicy`` instance. + load_path_dict (Dict[str, str]): If provided, policies whose IDs are in the dictionary keys will load the states + from the corresponding path. Defaults to None. + checkpoint_every (int): The policies will be checkpointed (i.e., persisted to disk) every this number of seconds + only if there are updates since the last checkpoint. This must be a positive integer or -1, with -1 meaning + no checkpointing. Defaults to -1. + save_dir (str): The directory under which to checkpoint the policy states. group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless if ``data_parallel`` is False. Defaults to "learn". data_parallel (bool): If True, the policies training tasks will be sent to remote gradient workers for @@ -108,6 +116,9 @@ class SimplePolicyManager(AbsPolicyManager): def __init__( self, create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], + load_path_dict: Dict[str, str] = None, + checkpoint_every: int = -1, + save_dir: str = None, group: str = "learn", data_parallel: bool = False, num_grad_workers: int = 1, @@ -115,18 +126,30 @@ def __init__( proxy_kwargs: dict = {}, log_dir: str = getcwd() ): + assert checkpoint_every == -1 or checkpoint_every > 0, "'checkpoint_every' can only be -1 or a positive integer" super().__init__() self._policy_ids = list(create_policy_func_dict.keys()) - self._data_parallel = data_parallel - self._num_grad_workers = num_grad_workers - self._worker_allocator = worker_allocator - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) + self.checkpoint_every = checkpoint_every + self.save_path = {id_: os.path.join(save_dir, id_) for id_ in self._policy_ids} + if self.checkpoint_every > 0: + checkpoint_thread = threading.Thread(target=self._checkpoint, daemon=True) + checkpoint_thread.start() - self._worker_allocator.set_logger(self._logger) + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._logger.info("Creating policy instances locally") self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} + for id_, path in load_path_dict.items(): + self._policy_dict[id_].load(path) + self._version = defaultdict(int) + + # data parallelism + self._data_parallel = data_parallel if self._data_parallel: + self._num_grad_workers = num_grad_workers + self._worker_allocator = worker_allocator + self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) + self._worker_allocator.set_logger(self._logger) self._proxy = Proxy( group, "policy_manager", {"grad_worker": self._num_grad_workers}, component_name="POLICY_MANAGER", **proxy_kwargs) @@ -136,8 +159,6 @@ def __init__( group, "policy_host", {"grad_worker": self._num_grad_workers}, component_name=f"POLICY_HOST.{name}", **proxy_kwargs) - self._version = 0 - def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. @@ -145,22 +166,14 @@ def update(self, rollout_info: Dict[str, list]): information dictionaries computed directly by roll-out workers. """ t0 = time.time() - if self._data_parallel: - # re-allocate grad workers before update. - self._policy2workers, self._worker2policies = self._worker_allocator.allocate() - - for policy_id, info_list in rollout_info.items(): - # in some cases e.g. Actor-Critic that get loss from rollout workers - if isinstance(info_list, list): - self._policy_dict[policy_id].update(info_list) + for policy_id, info in rollout_info.items(): + if isinstance(info, list): + self._policy_dict[policy_id].update(info) else: - if self._data_parallel: - self._policy_dict[policy_id].learn_with_data_parallel(info_list, self._policy2workers[policy_id]) - else: - self._policy_dict[policy_id].learn(info_list) + self._policy_dict[policy_id].learn(info) + self._version[policy_id] += 1 self._logger.info(f"Updated policies {list(rollout_info.keys())}") - self._version += 1 self._logger.info(f"policy update time: {time.time() - t0}") def get_state(self): @@ -171,20 +184,36 @@ def get_version(self): """Get the collective policy version.""" return self._version + def save(self): + for id_, policy in self._policy_dict.items(): + policy.save(self.save_path[id_]) + self._logger.info(f"Saved policy {id_} to {self.save_path[id_]}") + + def _checkpoint(self): + last_checkpointed_version = defaultdict(lambda: -1) + while True: + for id_, policy in self._policy_dict.items(): + if self._version[id_] > last_checkpointed_version[id_]: + policy.save(self.save_path[id_]) + self._logger.info(f"Saved policy {id_} to {self.save_path[id_]}") + last_checkpointed_version[id_] = self._version[id_] + time.sleep(self.checkpoint_every) + def exit(self): - """Tell the policy host processes to exit.""" - if self._data_parallel: - self._proxy.close() - for name in self._policy_ids: - self._policy_dict[name].exit_data_parallel() + pass class MultiProcessPolicyManager(AbsPolicyManager): - """Policy manager with multi-processing parallism that contains all policy instances. + """Policy manager that places each policy instance in a separate process. Args: create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a function that takes policy name as the only parameter and return an ``RLPolicy`` instance. + load_path_dict (Dict[str, str]): If provided, policies whose IDs are in the dictionary keys will load the states + from the corresponding path. Defaults to None. + auto_checkpoint (bool): If True, the policies will be checkpointed (i.e., persisted to disk) every time they are + updated. Defaults to -1. + save_dir (str): The directory under which to checkpoint the policy states. group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless if ``data_parallel`` is False. Defaults to "learn". data_parallel (bool): If True, the policies training tasks will be sent to remote gradient workers for @@ -193,7 +222,7 @@ class MultiProcessPolicyManager(AbsPolicyManager): Defaults to 1. worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies for parallelization. - proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -201,6 +230,9 @@ class MultiProcessPolicyManager(AbsPolicyManager): def __init__( self, create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], + load_path_dict: Dict[str, str] = None, + auto_checkpoint: bool = False, + save_dir: str = None, group: str = "learn", data_parallel: bool = False, num_grad_workers: int = 1, @@ -210,56 +242,65 @@ def __init__( ): super().__init__() self._policy_ids = list(create_policy_func_dict.keys()) - self._data_parallel = data_parallel - self._num_grad_workers = num_grad_workers - self._worker_allocator = worker_allocator + self.auto_checkpoint = auto_checkpoint self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - self._worker_allocator.set_logger(self._logger) - + # data parallelism + self._data_parallel = data_parallel if self._data_parallel: + self._num_grad_workers = num_grad_workers + self._worker_allocator = worker_allocator + + self._worker_allocator.set_logger(self._logger) self._proxy = Proxy( group, "policy_manager", {"grad_worker": self._num_grad_workers}, - component_name="POLICY_MANAGER", **proxy_kwargs) + component_name="POLICY_MANAGER", **proxy_kwargs + ) - self._logger.info("Spawning policy host processes") self._state_cache = {} self._policy_hosts = [] self._manager_end = {} + self._logger.info("Spawning policy host processes") - def _policy_host(name: str, create_policy_func: Callable[[str], RLPolicy], conn: Pipe): - policy = create_policy_func(name) + def policy_host(id_, create_policy_func, conn, initial_state_path): + policy = create_policy_func(id_) + save_path = os.path.join(save_dir, id_) if self._data_parallel: self._logger.info("========== data parallel mode ==========") policy.data_parallel( - group, "policy_host", {"grad_worker": self._num_grad_workers}, component_name=f"POLICY_HOST.{name}", + group, "policy_host", {"grad_worker": self._num_grad_workers}, component_name=f"POLICY_HOST.{id_}", **proxy_kwargs) + if initial_state_path: + policy.load(initial_state_path) conn.send({"type": "init", "policy_state": policy.get_state()}) while True: msg = conn.recv() if msg["type"] == "learn": - info_list = msg["rollout_info"] - policy2workers = msg["policy2workers"] - if isinstance(info_list, list): - # in some cases e.g. Actor-Critic that get loss from rollout workers - policy.update(info_list) + info = msg["rollout_info"] + if isinstance(info, list): + policy.update(info) + elif self._data_parallel: + policy.learn_with_data_parallel(info, msg["workers"]) else: - if self._data_parallel: - policy.learn_with_data_parallel(info_list, policy2workers[name]) - else: - policy.learn(info_list) + policy.learn(info) conn.send({"type": "learn_done", "policy_state": policy.get_state()}) + if self.auto_checkpoint: + policy.save(save_path) + self._logger.info(f"Saved policy {id_} to {save_path}") elif msg["type"] == "quit": if self._data_parallel: policy.exit_data_parallel() + policy.save(save_path) + self._logger.info(f"Saved policy {id_} to {save_path}") break - # start host process - for name, create_policy_func in create_policy_func_dict.items(): + for id_, create_policy_func in create_policy_func_dict.items(): manager_end, host_end = Pipe() - self._manager_end[name] = manager_end - host = Process(target=_policy_host, args=(name, create_policy_func, host_end)) + self._manager_end[id_] = manager_end + host = Process( + target=policy_host, args=(id_, create_policy_func, host_end, load_path_dict.get(id_, None)) + ) self._policy_hosts.append(host) host.start() @@ -269,33 +310,32 @@ def _policy_host(name: str, create_policy_func: Callable[[str], RLPolicy], conn: self._state_cache[policy_id] = msg["policy_state"] self._logger.info(f"Initial state for policy {policy_id} cached") - self._version = 0 + self._version = defaultdict(int) def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. - The roll-out information is grouped by policy name and may be either raw simulation trajectories or loss - information computed directly by roll-out workers. + The roll-out information is grouped by policy name and may be either a training batch or a list of loss + information dictionaries computed directly by roll-out workers. """ - t0 = time.time() if self._data_parallel: # re-allocate grad workers before update. self._policy2workers, self._worker2policies = self._worker_allocator.allocate() - for policy_id, info_list in rollout_info.items(): + for policy_id, info in rollout_info.items(): self._manager_end[policy_id].send( - {"type": "learn", "rollout_info": info_list, "policy2workers": self._policy2workers}) + {"type": "learn", "rollout_info": info, "workers": self._policy2workers[policy_id]} + ) for policy_id, conn in self._manager_end.items(): msg = conn.recv() if msg["type"] == "learn_done": self._state_cache[policy_id] = msg["policy_state"] self._logger.info(f"Cached state for policy {policy_id}") + self._version[policy_id] += 1 else: self._logger.info(f"Warning: Wrong message type: {msg['type']}") self._logger.info(f"Updated policies {list(rollout_info.keys())}") - self._version += 1 - self._logger.info(f"policy update time: {time.time() - t0}") def get_state(self): """Get the latest policy states.""" diff --git a/maro/rl/modeling/core_model.py b/maro/rl/modeling/core_model.py index af118cf29..269372cb1 100644 --- a/maro/rl/modeling/core_model.py +++ b/maro/rl/modeling/core_model.py @@ -8,7 +8,12 @@ class AbsCoreModel(nn.Module): - """General model abstraction for use in deep RL algorithms.""" + """Model abstraction for use in deep RL algorithms. + + This can be viewed as a container of one or more network components with embedded optimizers. This abstraction + exposes simple and unified interfaces to decouple model inference and optimization from the algorithmic aspects + of the policy that uses it. + """ def __init__(self): super().__init__() @@ -42,3 +47,22 @@ def apply_gradients(self, grad: dict): This needs to be implemented together with ``get_gradients``. """ pass + + @abstractmethod + def get_state(self): + """Return the current model state. + + Ths model state usually involves the "state_dict" of the module as well as those of the embedded optimizers. + """ + pass + + @abstractmethod + def set_state(self, state): + """Set model state. + + Args: + state: Model state to be applied to the instance. Ths model state is either the result of a previous call + to ``get_state`` or something loaded from disk and involves the "state_dict" of the module as well as those + of the embedded optimizers. + """ + pass diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index d7c052883..9fd7fdadc 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -289,8 +289,16 @@ def learn_with_data_parallel(self, batch: dict, worker_id_list: list): _ = self.get_batch_loss(sub_batch, explicit_grad=True) self.update(loss_info_by_policy[self._name]) - def set_state(self, policy_state): - self.ac_net.load_state_dict(policy_state) - def get_state(self): - return self.ac_net.state_dict() + return self.ac_net.get_state() + + def set_state(self, state): + self.ac_net.set_state(state) + + def load(self, path: str): + """Load the policy state from disk.""" + self.ac_net.set_state(torch.load(path)) + + def save(self, path: str): + """Save the policy state to disk.""" + torch.save(self.ac_net.get_state(), path) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py index 673baddbe..91f24aa3c 100644 --- a/maro/rl/policy/ddpg.py +++ b/maro/rl/policy/ddpg.py @@ -275,9 +275,28 @@ def exploration_step(self): for sch in self.exploration_schedulers: sch.step() - def set_state(self, policy_state): - self.ac_net.load_state_dict(policy_state) - self.target_ac_net = self.ac_net.copy() if self.ac_net.trainable else None - def get_state(self): - return self.ac_net.state_dict() + return self.ac_net.get_state() + + def set_state(self, state): + self.ac_net.set_state(state) + + def load(self, path: str): + """Load the policy state from disk.""" + checkpoint = torch.load(path) + self.ac_net.set_state(checkpoint["ac_net"]) + self._ac_net_version = checkpoint["ac_net_version"] + self.target_ac_net.set_state(checkpoint["target_ac_net"]) + self._target_ac_net_version = checkpoint["target_ac_net_version"] + self._replay_memory = checkpoint["replay_memory"] + + def save(self, path: str): + """Save the policy state to disk.""" + policy_state = { + "ac_net": self.ac_net.get_state(), + "ac_net_version": self._ac_net_version, + "target_ac_net": self.target_ac_net.get_state(), + "target_ac_net_version": self._target_ac_net_version, + "replay_memory": self._replay_memory + } + torch.save(policy_state, path) diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py index edb5ff352..4fd6a7eb4 100644 --- a/maro/rl/policy/dqn.py +++ b/maro/rl/policy/dqn.py @@ -412,13 +412,32 @@ def exploration_step(self): for sch in self.exploration_schedulers: sch.step() - def get_state(self, inference: bool = True): - policy_state = {"eval": self.q_net.state_dict()} - if not inference and self.target_q_net: - policy_state["target"] = self.target_q_net.state_dict() - return policy_state - - def set_state(self, policy_state): - self.q_net.load_state_dict(policy_state["eval"]) - if "target" in policy_state: - self.target_q_net.load_state_dict(policy_state["target"]) + def get_state(self): + return self.q_net.get_state() + + def set_state(self, state): + self.q_net.set_state(state) + + def load(self, path: str): + """Load the policy state from disk.""" + checkpoint = torch.load(path) + self.q_net.set_state(checkpoint["q_net"]) + self._q_net_version = checkpoint["q_net_version"] + self.target_q_net.set_state(checkpoint["target_q_net"]) + self._target_q_net_version = checkpoint["target_q_net_version"] + self._replay_memory = checkpoint["replay_memory"] + if self.prioritized_replay: + self._per = checkpoint["prioritized_replay"] + + def save(self, path: str): + """Save the policy state to disk.""" + policy_state = { + "q_net": self.q_net.get_state(), + "q_net_version": self._q_net_version, + "target_q_net": self.target_q_net.get_state(), + "target_q_net_version": self._target_q_net_version, + "replay_memory": self._replay_memory + } + if self.prioritized_replay: + policy_state["prioritized_replay"] = self._per + torch.save(policy_state, path) diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index b4b3c86b6..5d3e94299 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -208,8 +208,16 @@ def learn_with_data_parallel(self, batch: dict, worker_id_list: list): _ = self.get_batch_loss(sub_batch, explicit_grad=True) self.policy_net.step(loss_info_by_policy[self._name]) - def set_state(self, policy_state): - self.policy_net.load_state_dict(policy_state) - def get_state(self): - return self.policy_net.state_dict() + return self.policy_net.get_state() + + def set_state(self, state): + self.policy_net.set_state(state) + + def load(self, path: str): + """Load the policy state from disk.""" + self.policy_net.set_state(torch.load(path)) + + def save(self, path: str): + """Save the policy state to disk.""" + torch.save(self.policy_net.get_state(), path) From 7b76dce323e307ca9fe3a8122f2365e0a9c8d655 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 17 Sep 2021 12:47:10 +0000 Subject: [PATCH 441/482] 1. bug fixes in checkpointing; 2. removed version and max_lag in rollout manager --- .../rl/scripts/docker/docker_compose_yml.py | 4 +- examples/rl/workflows/config.yml | 5 +- examples/rl/workflows/policy_manager.py | 75 ++++++++++--------- maro/rl/learning/env_sampler.py | 1 - maro/rl/learning/learning_loop.py | 4 +- maro/rl/learning/policy_manager.py | 40 +++++----- maro/rl/learning/rollout_manager.py | 35 +++------ maro/rl/policy/ac.py | 1 - 8 files changed, 78 insertions(+), 87 deletions(-) diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 9e0c6a39b..1abe1c3ac 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -62,7 +62,8 @@ f"JOB={config['job']}", f"SCENARIO={config['scenario']}", f"MODE={config['mode']}", - f"POLICYMANAGERTYPE={config['policy_manager']['type']}" + f"POLICYMANAGERTYPE={config['policy_manager']['type']}", + f"CHECKPOINTDIR={config['checkpoint_dir']}" ] common_env.append(f"NUMROLLOUTS={config[config['mode']]['num_rollouts']}") @@ -110,7 +111,6 @@ f"NUMEPISODES={config['num_episodes']}", f"NUMSTEPS={config['num_steps']}", f"EVALSCH={config['eval_schedule']}", - f"PARALLEL={'1' if config['policy_manager']['simple']['parallel'] else '0'}", f"NUMEVALROLLOUTS={config[config['mode']]['num_eval_rollouts']}", f"ROLLOUTGROUP={config['sync']['distributed']['group']}", f"MAXLAG={config['max_lag']}", diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 2704e7036..7273ecf01 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -3,6 +3,7 @@ job: cim scenario: cim +checkpoint_dir: "/maro/rl_examples/checkpoints" mode: sync # single, sync, async num_episodes: 5 eval_schedule: 5 @@ -21,9 +22,7 @@ async: group: async num_rollouts: 3 policy_manager: - type: distributed # simple, distributed - simple: - parallel: true + type: multi-process # simple, multi-process, distributed distributed: group: learn num_hosts: 2 diff --git a/examples/rl/workflows/policy_manager.py b/examples/rl/workflows/policy_manager.py index a44108c1e..4d873e9fc 100644 --- a/examples/rl/workflows/policy_manager.py +++ b/examples/rl/workflows/policy_manager.py @@ -2,63 +2,68 @@ # Licensed under the MIT license. import sys -from os import getenv -from os.path import dirname, realpath +from os import getenv, makedirs +from os.path import dirname, join, realpath from maro.rl.learning import DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager from maro.rl.policy import WorkerAllocator -workflow_dir = dirname(dirname(realpath(__file__))) # template directory +workflow_dir = dirname((realpath(__file__))) # template directory if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) from general import agent2policy, log_dir, policy_func_dict +checkpoint_dir = join(getenv("CHECKPOINTDIR"), getenv("JOB")) +makedirs(checkpoint_dir, exist_ok=True) + def get_policy_manager(): manager_type = getenv("POLICYMANAGERTYPE", default="simple") - parallel = int(getenv("PARALLEL", default=0)) data_parallel = getenv("DATAPARALLEL") == "True" num_grad_workers = int(getenv("NUMGRADWORKERS", default=1)) group = getenv("LEARNGROUP", default="learn") allocation_mode = getenv("ALLOCATIONMODE", default="by-policy") allocator = WorkerAllocator(allocation_mode, num_grad_workers, list(policy_func_dict.keys()), agent2policy) + proxy_kwargs = { + "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), + "max_peer_discovery_retries": 50 + } if manager_type == "simple": - if parallel == 0: - return SimplePolicyManager( - policy_func_dict, group, - data_parallel=data_parallel, - num_grad_workers=num_grad_workers, - worker_allocator=allocator, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir) - else: - return MultiProcessPolicyManager( - policy_func_dict, group, - data_parallel=data_parallel, - num_grad_workers=num_grad_workers, - worker_allocator=allocator, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir) - num_hosts = int(getenv("NUMHOSTS", default=5)) - if manager_type == "distributed": - policy_manager = DistributedPolicyManager( + return SimplePolicyManager( + policy_func_dict, + load_path_dict={id_: join(checkpoint_dir, id_) for id_ in policy_func_dict}, + checkpoint_every=7, + save_dir=checkpoint_dir, + group=group, + data_parallel=data_parallel, + num_grad_workers=num_grad_workers, + worker_allocator=allocator, + proxy_kwargs=proxy_kwargs, + log_dir=log_dir + ) + elif manager_type == "multi-process": + return MultiProcessPolicyManager( + policy_func_dict, + load_path_dict={id_: join(checkpoint_dir, id_) for id_ in policy_func_dict}, + auto_checkpoint=True, + save_dir=checkpoint_dir, + group=group, + data_parallel=data_parallel, + num_grad_workers=num_grad_workers, + worker_allocator=allocator, + proxy_kwargs=proxy_kwargs, + log_dir=log_dir + ) + elif manager_type == "distributed": + num_hosts = int(getenv("NUMHOSTS", default=5)) + return DistributedPolicyManager( list(policy_func_dict.keys()), group, num_hosts, data_parallel=data_parallel, num_grad_workers=num_grad_workers, worker_allocator=allocator, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, + proxy_kwargs=proxy_kwargs, log_dir=log_dir ) - return policy_manager raise ValueError(f"Unsupported policy manager type: {manager_type}. Supported modes: simple, distributed") @@ -71,7 +76,7 @@ def get_policy_manager(): max_lag=int(getenv("MAXLAG", default=0)), proxy_kwargs={ "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 + "max_peer_discovery_retries": 50 }, log_dir=log_dir ) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index a4228b76e..d6b7e18c8 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -414,7 +414,6 @@ def worker( return_info = { MsgKey.EPISODE: ep, MsgKey.SEGMENT: latest.body[MsgKey.SEGMENT], - MsgKey.VERSION: latest.body[MsgKey.VERSION], MsgKey.ROLLOUT_INFO: result["rollout_info"], MsgKey.STEP_RANGE: result["step_range"], MsgKey.TRACKER: result["tracker"], diff --git a/maro/rl/learning/learning_loop.py b/maro/rl/learning/learning_loop.py index a075ce139..88c1f814e 100644 --- a/maro/rl/learning/learning_loop.py +++ b/maro/rl/learning/learning_loop.py @@ -83,8 +83,8 @@ def collect_and_update(): # experience collection tc0 = time.time() if policy_manager: - policy_state_dict, version = policy_manager.get_state(), policy_manager.get_version() - rollout_info_by_policy, trackers = rollout_manager.collect(ep, segment, policy_state_dict, version) + policy_state_dict = policy_manager.get_state() + rollout_info_by_policy, trackers = rollout_manager.collect(ep, segment, policy_state_dict) end_of_episode = rollout_manager.end_of_episode else: result = rollout_manager.sample(num_steps=num_steps, return_rollout_info=False) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 7bc192f53..922fce464 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -128,21 +128,22 @@ def __init__( ): assert checkpoint_every == -1 or checkpoint_every > 0, "'checkpoint_every' can only be -1 or a positive integer" super().__init__() - self._policy_ids = list(create_policy_func_dict.keys()) - self.checkpoint_every = checkpoint_every - self.save_path = {id_: os.path.join(save_dir, id_) for id_ in self._policy_ids} - if self.checkpoint_every > 0: - checkpoint_thread = threading.Thread(target=self._checkpoint, daemon=True) - checkpoint_thread.start() - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - self._logger.info("Creating policy instances locally") self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} for id_, path in load_path_dict.items(): - self._policy_dict[id_].load(path) + self._policy_dict[id_].load(path) + self._logger.info(f"Loaded policy {id_} from {path}") self._version = defaultdict(int) + # auto-checkpointing + self.checkpoint_every = checkpoint_every + self.save_path = {id_: os.path.join(save_dir, id_) for id_ in create_policy_func_dict} + if self.checkpoint_every > 0: + checkpoint_thread = threading.Thread(target=self._checkpoint, daemon=True) + checkpoint_thread.start() + self._logger.info(f"Auto-checkpoint policy state every {self.checkpoint_every} seconds") + # data parallelism self._data_parallel = data_parallel if self._data_parallel: @@ -182,7 +183,7 @@ def get_state(self): def get_version(self): """Get the collective policy version.""" - return self._version + return dict(self._version) def save(self): for id_, policy in self._policy_dict.items(): @@ -197,6 +198,8 @@ def _checkpoint(self): policy.save(self.save_path[id_]) self._logger.info(f"Saved policy {id_} to {self.save_path[id_]}") last_checkpointed_version[id_] = self._version[id_] + else: + self._logger.info(f"Latest version of policy {id_} already checkpointed") time.sleep(self.checkpoint_every) def exit(self): @@ -241,7 +244,6 @@ def __init__( log_dir: str = getcwd() ): super().__init__() - self._policy_ids = list(create_policy_func_dict.keys()) self.auto_checkpoint = auto_checkpoint self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) @@ -273,6 +275,7 @@ def policy_host(id_, create_policy_func, conn, initial_state_path): if initial_state_path: policy.load(initial_state_path) + self._logger.info(f"Loaded policy {id_} from {initial_state_path}") conn.send({"type": "init", "policy_state": policy.get_state()}) while True: msg = conn.recv() @@ -285,6 +288,7 @@ def policy_host(id_, create_policy_func, conn, initial_state_path): else: policy.learn(info) conn.send({"type": "learn_done", "policy_state": policy.get_state()}) + self._logger.info("learning finished") if self.auto_checkpoint: policy.save(save_path) self._logger.info(f"Saved policy {id_} to {save_path}") @@ -323,9 +327,10 @@ def update(self, rollout_info: Dict[str, list]): self._policy2workers, self._worker2policies = self._worker_allocator.allocate() for policy_id, info in rollout_info.items(): - self._manager_end[policy_id].send( - {"type": "learn", "rollout_info": info, "workers": self._policy2workers[policy_id]} - ) + msg = {"type": "learn", "rollout_info": info} + if self._data_parallel: + msg["workers"] = self._policy2workers[policy_id] + self._manager_end[policy_id].send(msg) for policy_id, conn in self._manager_end.items(): msg = conn.recv() if msg["type"] == "learn_done": @@ -343,7 +348,7 @@ def get_state(self): def get_version(self): """Get the collective policy version.""" - return self._version + return dict(self._version) def exit(self): """Tell the policy host processes to exit.""" @@ -383,7 +388,6 @@ def __init__( log_dir: str = getcwd() ): super().__init__() - self._policy_ids = policy_ids peers = {"policy_host": num_hosts} if data_parallel: peers["grad_worker"] = num_grad_workers @@ -400,7 +404,7 @@ def __init__( self._worker2policies = defaultdict(list) # assign policies to hosts - for i, name in enumerate(self._policy_ids): + for i, name in enumerate(policy_ids): host_id = i % num_hosts self._policy2host[name] = f"POLICY_HOST.{host_id}" self._host2policies[f"POLICY_HOST.{host_id}"].append(name) @@ -461,7 +465,7 @@ def get_state(self): def get_version(self): """Get the collective policy version.""" - return self._version + return dict(self._version) def exit(self): """Tell the remote policy hosts to exit.""" diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index 2917d199c..611059854 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -29,14 +29,13 @@ def __init__(self): self.end_of_episode = False @abstractmethod - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) -> Tuple[Dict, List[Dict]]: + def collect(self, ep: int, segment: int, policy_state_dict: dict) -> Tuple[Dict, List[Dict]]: """Collect simulation data, i.e., experiences for training. Args: ep (int): Current episode. segment (int): Current segment. policy_state_dict (dict): Policy states to use for collecting training info. - version (int): Version for the policy states. Returns: A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries @@ -133,14 +132,13 @@ def _rollout_worker(index, conn, get_env_sampler): self._worker_processes.append(worker) worker.start() - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) -> Tuple[Dict, List[Dict]]: + def collect(self, ep: int, segment: int, policy_state_dict: dict) -> Tuple[Dict, List[Dict]]: """Collect simulation data, i.e., experiences for training. Args: ep (int): Current episode. segment (int): Current segment. policy_state_dict (dict): Policy states to use for collecting training info. - version (int): Version for the policy states. Returns: A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries @@ -148,7 +146,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) An RL policy's roll-out information must be either loss information or a data batch that can be passed to the policy's ``update`` or ``learn``, respectively. """ - self._logger.info(f"Collecting simulation data (episode {ep}, policy version {version})") + self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment})") info_by_policy, trackers = defaultdict(list), [] rollout_req = { @@ -218,9 +216,6 @@ class DistributedRolloutManager(AbsRolloutManager): ``min_finished_workers``. extra_recv_timeout (int): Timeout (in milliseconds) for each attempt to receive from a worker after ``min_finished_workers`` have been received in ``collect``. Defaults to 100 (milliseconds). - max_lag (int): Maximum policy version lag allowed for experiences collected from remote roll-out workers. - Experiences collected using policy versions older than (current_version - max_lag) will be discarded. - Defaults to 0, in which case only experiences collected using the latest policy version will be returned. num_eval_workers (int): Number of workers for evaluation. Defaults to 1. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. @@ -236,7 +231,7 @@ def __init__( min_finished_workers: int = None, max_extra_recv_tries: int = None, extra_recv_timeout: int = None, - max_lag: int = 0, + max_lag: Dict[str, int] = defaultdict(int), num_eval_workers: int = 1, proxy_kwargs: dict = {}, log_dir: str = getcwd() @@ -268,14 +263,13 @@ def __init__( self._max_lag = max_lag self._num_eval_workers = num_eval_workers - def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) -> Tuple[Dict, List[Dict]]: + def collect(self, ep: int, segment: int, policy_state_dict: dict) -> Tuple[Dict, List[Dict]]: """Collect simulation data, i.e., experiences for training. Args: ep (int): Current episode. segment (int): Current segment. policy_state_dict (dict): Policy states to use for collecting training info. - version (int): Version for the policy states. Returns: A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries @@ -287,17 +281,16 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) MsgKey.EPISODE: ep, MsgKey.SEGMENT: segment, MsgKey.NUM_STEPS: self._num_steps, - MsgKey.POLICY_STATE: policy_state_dict, - MsgKey.VERSION: version + MsgKey.POLICY_STATE: policy_state_dict } self._proxy.iscatter(MsgTag.SAMPLE, SessionType.TASK, [(worker_id, msg_body) for worker_id in self._workers]) - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment}, policy version {version})") + self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment})") info_by_policy, trackers, num_finishes = defaultdict(list), [], 0 # Ensure the minimum number of worker results are received. for msg in self._proxy.receive(): - rollout_info, tracker = self._handle_worker_result(msg, ep, segment, version) + rollout_info, tracker = self._handle_worker_result(msg, ep, segment) if rollout_info: num_finishes += 1 for policy_id, info in rollout_info.items(): @@ -312,7 +305,7 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) if not msg: self._logger.info(f"Receive timeout, {self._max_extra_recv_tries - i - 1} attempts left") else: - rollout_info, tracker = self._handle_worker_result(msg, ep, segment, version) + rollout_info, tracker = self._handle_worker_result(msg, ep, segment) if rollout_info: num_finishes += 1 for policy_id, info in rollout_info.items(): @@ -328,21 +321,13 @@ def collect(self, ep: int, segment: int, policy_state_dict: dict, version: int) return info_by_policy, trackers - def _handle_worker_result(self, msg, ep, segment, version): + def _handle_worker_result(self, msg, ep, segment): if msg.tag != MsgTag.SAMPLE_DONE: self._logger.info( f"Ignored a message of type {msg.tag} (expected message type {MsgTag.SAMPLE_DONE})" ) return None, None - if version - msg.body[MsgKey.VERSION] > self._max_lag: - self._logger.info( - f"Ignored a message because it contains experiences generated using a stale policy version. " - f"Expected experiences generated using policy versions no earlier than {version - self._max_lag} " - f"got {msg.body[MsgKey.VERSION]}" - ) - return None, None - # The message is what we expect if msg.body[MsgKey.EPISODE] == ep and msg.body[MsgKey.SEGMENT] == segment: self.end_of_episode = msg.body[MsgKey.END_OF_EPISODE] diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py index 9fd7fdadc..546fae1ee 100644 --- a/maro/rl/policy/ac.py +++ b/maro/rl/policy/ac.py @@ -261,7 +261,6 @@ def improve(self): def learn_with_data_parallel(self, batch: dict, worker_id_list: list): assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." - for _ in range(self.grad_iters): msg_dict = defaultdict(lambda: defaultdict(dict)) for i, worker_id in enumerate(worker_id_list): From c1d987162c4bd0b07903fd0d645c699eb3e1057e Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 17 Sep 2021 13:19:34 +0000 Subject: [PATCH 442/482] added missing set_state and get_state for CIM policies --- examples/rl/cim/policies.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index 93ccc7459..faa82c74e 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -49,6 +49,13 @@ def apply_gradients(self, grad): self.optim.step() + def get_state(self): + return {"network": self.state_dict(), "optim": self.optim.state_dict()} + + def set_state(self, state): + self.load_state_dict(state["network"]) + self.optim.load_state_dict(state["optim"]) + class MyACNet(DiscreteACNet): def __init__(self): @@ -89,6 +96,18 @@ def apply_gradients(self, grad): self.actor_optim.step() self.critic_optim.step() + def get_state(self): + return { + "network": self.state_dict(), + "actor_optim": self.actor_optim.state_dict(), + "critic_optim": self.critic_optim.state_dict() + } + + def set_state(self, state): + self.load_state_dict(state["network"]) + self.actor_optim.load_state_dict(state["actor_optim"]) + self.critic_optim.load_state_dict(state["critic_optim"]) + policy_func_dict = { f"ac.{i}": lambda name: ActorCritic(name, MyACNet(), **ac_conf) for i in range(4) From fc593793b809341d3570c885923d2f1427ac2840 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 17 Sep 2021 13:24:33 +0000 Subject: [PATCH 443/482] removed blank line --- maro/rl/learning/policy_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 922fce464..38667beb5 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -252,7 +252,6 @@ def __init__( if self._data_parallel: self._num_grad_workers = num_grad_workers self._worker_allocator = worker_allocator - self._worker_allocator.set_logger(self._logger) self._proxy = Proxy( group, "policy_manager", {"grad_worker": self._num_grad_workers}, From f23fdc66b6f38060a653f048d41e6983fc896097 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 22 Sep 2021 06:32:28 +0000 Subject: [PATCH 444/482] updated RL workflow README --- examples/rl/README.md | 6 ++---- maro/rl/learning/policy_manager.py | 25 +++++++++++-------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/examples/rl/README.md b/examples/rl/README.md index f29649d50..ebd112431 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -1,12 +1,10 @@ # Reinforcement Learning (RL) Examples -This folder contains scenarios that employ reinforcement learning. It is possible to apply a common workflow to different scenarios, thanks to the abstractions from MARO's RL toolkit. The workflow can be found under ``workflows``, which contains Python scripts for running the necessary components in single-threaded and distributed modes. The scenario can be chosen by setting the ``scenario`` field in ``config.yml``. +This folder contains scenarios that employ reinforcement learning. MARO's RL toolkit makes it possible to use a common workflow on different scenarios, so long as the necessary scenario-related components are provided. The workflow consists of Python scripts for running the necessary components in single-threaded and distributed modes under ``workflows``. General scenario-independent settings can be found in ``config.yml``. The scenario can be chosen by setting the ``scenario`` field in this file. ## How to Run -To run the single-threaded workflow, go to ``workflows`` and run ``python3 simple_learner.py``. Alternatively, go to ``scripts`` and run ``bash run.sh``. - -To run the distributed workflow, start by choosing "sync" or "async" for the ``mode`` field in ``config.yml``. The ``scrpts/docker`` folder provides bash scripts for simulating the distributed workflow using multiple docker containers on a single host. Go to this folder and execute ``bash run.sh`` to launch the program and Docker Compose will take care of starting the necessary containers. Note that the script will build the docker image first if it has not already been built by running ``bash build.sh``. When the program is finished, be sure to run ``bash kill.sh`` to clean up the containers and remove the network. +Scripts do run scenarios using the common workflow, start by choosing "sync" or "async" for the ``mode`` field in ``config.yml``. The ``scripts/docker`` folder provides bash scripts for simulating the distributed workflow using multiple docker containers on a single host. Go to this folder and execute ``bash run.sh`` to launch the program and Docker Compose will take care of starting the necessary containers. Note that the script will build the docker image first if it has not already been built by running ``bash build.sh``. When the program is finished, be sure to run ``bash kill.sh`` to clean up the containers and remove the network. ## Write Your Own Scenarios diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 38667beb5..608fb2a11 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -102,13 +102,11 @@ class SimplePolicyManager(AbsPolicyManager): save_dir (str): The directory under which to checkpoint the policy states. group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless if ``data_parallel`` is False. Defaults to "learn". - data_parallel (bool): If True, the policies training tasks will be sent to remote gradient workers for - data-parallel, otherwise learnt locally. Defaults to False. - num_grad_workers (int): Number of gradient workers, which is meaningless when ``data_parallel`` is False. - Defaults to 1. - worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies - for parallelization. - proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class + data_parallel (bool): If True, the policies will send gradient tasks to remote gradient workers for parallel + gradient computation. Defaults to False. + num_grad_workers (int): Number of gradient workers. Ignored if ``data_parallel`` is False. Defaults to 1. + worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. + proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -219,10 +217,9 @@ class MultiProcessPolicyManager(AbsPolicyManager): save_dir (str): The directory under which to checkpoint the policy states. group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless if ``data_parallel`` is False. Defaults to "learn". - data_parallel (bool): If True, the policies training tasks will be sent to remote gradient workers for - data-parallel, otherwise learnt locally. Defaults to False. - num_grad_workers (int): Number of gradient workers, which is meaningless when ``data_parallel`` is False. - Defaults to 1. + data_parallel (bool): If True, the policies will send gradient tasks to remote gradient workers for parallel + gradient computation. Defaults to False. + num_grad_workers (int): Number of gradient workers. Ignored if ``data_parallel`` is False. Defaults to 1. worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies for parallelization. proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. @@ -364,9 +361,9 @@ class DistributedPolicyManager(AbsPolicyManager): policy_ids (List[str]): Names of the registered policies. group (str): Group name for the cluster consisting of the manager and all policy hosts. num_hosts (int): Number of hosts. The hosts will be identified by "POLICY_HOST.i", where 0 <= i < num_hosts. - data_parallel (bool): Whether to train policy on remote gradient workers or locally on policy hosts. - num_grad_workers (int): Number of gradient workers, which is meaningless when ``data_parallel`` is False. - Defaults to 1. + data_parallel (bool): If True, the policies will send gradient tasks to remote gradient workers for parallel + gradient computation. Defaults to False. + num_grad_workers (int): Number of gradient workers. Ignored if ``data_parallel`` is False. Defaults to 1. worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies for parallelization. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class From 78a2cb8ffd15b584a181744c8dc630d2d6898533 Mon Sep 17 00:00:00 2001 From: "GQ.Chen" <675865907@qq.com> Date: Wed, 22 Sep 2021 16:07:27 +0800 Subject: [PATCH 445/482] Integrate `data_parallel` arguments into `worker_allocator` (#402) --- examples/rl/workflows/policy_manager.py | 11 +++---- maro/rl/learning/policy_manager.py | 44 +++++++++---------------- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/examples/rl/workflows/policy_manager.py b/examples/rl/workflows/policy_manager.py index 4d873e9fc..68ee41776 100644 --- a/examples/rl/workflows/policy_manager.py +++ b/examples/rl/workflows/policy_manager.py @@ -23,7 +23,10 @@ def get_policy_manager(): num_grad_workers = int(getenv("NUMGRADWORKERS", default=1)) group = getenv("LEARNGROUP", default="learn") allocation_mode = getenv("ALLOCATIONMODE", default="by-policy") - allocator = WorkerAllocator(allocation_mode, num_grad_workers, list(policy_func_dict.keys()), agent2policy) + if data_parallel: + allocator = WorkerAllocator(allocation_mode, num_grad_workers, list(policy_func_dict.keys()), agent2policy) + else: + allocator = None proxy_kwargs = { "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), "max_peer_discovery_retries": 50 @@ -35,8 +38,6 @@ def get_policy_manager(): checkpoint_every=7, save_dir=checkpoint_dir, group=group, - data_parallel=data_parallel, - num_grad_workers=num_grad_workers, worker_allocator=allocator, proxy_kwargs=proxy_kwargs, log_dir=log_dir @@ -48,8 +49,6 @@ def get_policy_manager(): auto_checkpoint=True, save_dir=checkpoint_dir, group=group, - data_parallel=data_parallel, - num_grad_workers=num_grad_workers, worker_allocator=allocator, proxy_kwargs=proxy_kwargs, log_dir=log_dir @@ -58,8 +57,6 @@ def get_policy_manager(): num_hosts = int(getenv("NUMHOSTS", default=5)) return DistributedPolicyManager( list(policy_func_dict.keys()), group, num_hosts, - data_parallel=data_parallel, - num_grad_workers=num_grad_workers, worker_allocator=allocator, proxy_kwargs=proxy_kwargs, log_dir=log_dir diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 608fb2a11..72dbb12ce 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -101,11 +101,9 @@ class SimplePolicyManager(AbsPolicyManager): no checkpointing. Defaults to -1. save_dir (str): The directory under which to checkpoint the policy states. group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless - if ``data_parallel`` is False. Defaults to "learn". - data_parallel (bool): If True, the policies will send gradient tasks to remote gradient workers for parallel - gradient computation. Defaults to False. - num_grad_workers (int): Number of gradient workers. Ignored if ``data_parallel`` is False. Defaults to 1. + if not in ``data_parallel`` mode. Defaults to "learn". worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. + If it is not None, the policy manager will run in ``data_parallel`` mode. Defaults to None. proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current @@ -118,8 +116,6 @@ def __init__( checkpoint_every: int = -1, save_dir: str = None, group: str = "learn", - data_parallel: bool = False, - num_grad_workers: int = 1, worker_allocator: WorkerAllocator = None, proxy_kwargs: dict = {}, log_dir: str = getcwd() @@ -143,9 +139,9 @@ def __init__( self._logger.info(f"Auto-checkpoint policy state every {self.checkpoint_every} seconds") # data parallelism - self._data_parallel = data_parallel + self._data_parallel = worker_allocator is not None if self._data_parallel: - self._num_grad_workers = num_grad_workers + self._num_grad_workers = worker_allocator.num_workers self._worker_allocator = worker_allocator self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._worker_allocator.set_logger(self._logger) @@ -216,12 +212,10 @@ class MultiProcessPolicyManager(AbsPolicyManager): updated. Defaults to -1. save_dir (str): The directory under which to checkpoint the policy states. group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless - if ``data_parallel`` is False. Defaults to "learn". - data_parallel (bool): If True, the policies will send gradient tasks to remote gradient workers for parallel - gradient computation. Defaults to False. - num_grad_workers (int): Number of gradient workers. Ignored if ``data_parallel`` is False. Defaults to 1. + if not in ``data_parallel`` mode. Defaults to "learn". worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies - for parallelization. + for parallelization. If it is not None, the policy manager will run in ``data_parallel`` mode. + Defaults to None. proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current @@ -234,8 +228,6 @@ def __init__( auto_checkpoint: bool = False, save_dir: str = None, group: str = "learn", - data_parallel: bool = False, - num_grad_workers: int = 1, worker_allocator: WorkerAllocator = None, proxy_kwargs: dict = {}, log_dir: str = getcwd() @@ -245,9 +237,9 @@ def __init__( self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) # data parallelism - self._data_parallel = data_parallel + self._data_parallel = worker_allocator is not None if self._data_parallel: - self._num_grad_workers = num_grad_workers + self._num_grad_workers = worker_allocator.num_workers self._worker_allocator = worker_allocator self._worker_allocator.set_logger(self._logger) self._proxy = Proxy( @@ -361,11 +353,9 @@ class DistributedPolicyManager(AbsPolicyManager): policy_ids (List[str]): Names of the registered policies. group (str): Group name for the cluster consisting of the manager and all policy hosts. num_hosts (int): Number of hosts. The hosts will be identified by "POLICY_HOST.i", where 0 <= i < num_hosts. - data_parallel (bool): If True, the policies will send gradient tasks to remote gradient workers for parallel - gradient computation. Defaults to False. - num_grad_workers (int): Number of gradient workers. Ignored if ``data_parallel`` is False. Defaults to 1. worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies - for parallelization. + for parallelization. If it is not None, the policy manager will run in ``data_parallel`` mode. + Defaults to None. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init @@ -377,20 +367,18 @@ def __init__( policy_ids: List[str], group: str, num_hosts: int, - data_parallel: bool = False, - num_grad_workers: int = 1, worker_allocator: WorkerAllocator = None, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): super().__init__() + self._data_parallel = worker_allocator is not None peers = {"policy_host": num_hosts} - if data_parallel: - peers["grad_worker"] = num_grad_workers + if self._data_parallel: + peers["grad_worker"] = worker_allocator.num_workers self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._worker_allocator = worker_allocator - self._data_parallel = data_parallel self._worker_allocator.set_logger(self._logger) @@ -490,8 +478,8 @@ def policy_host( proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to the empty dictionary. data_parallel (bool): Whether to train policy on remote gradient workers to perform data-parallel. - Defaluts to False. - num_grad_workers (int): The number of gradient worker nodes in data-parallel mode. Defaluts to 1. + Defaults to False. + num_grad_workers (int): The number of gradient worker nodes in data-parallel mode. Defaults to 1. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ policy_dict = {} From f8f2e6a4ed5061597076b06f78da2764402b6dca Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 22 Sep 2021 15:32:56 +0000 Subject: [PATCH 446/482] 1. simplified workflow config; 2. added comments to CIM shaping --- docs/source/examples/multi_agent_dqn_cim.rst | 168 ------------------ examples/rl/README.md | 16 +- examples/rl/cim/README.md | 13 +- examples/rl/cim/callbacks.py | 12 +- examples/rl/cim/env_sampler.py | 18 +- .../rl/scripts/docker/docker_compose_yml.py | 16 +- examples/rl/workflows/config.yml | 5 +- examples/rl/workflows/grad_worker.py | 2 +- examples/rl/workflows/learning_loop.py | 3 +- examples/rl/workflows/policy_host.py | 2 +- examples/rl/workflows/policy_manager.py | 12 +- maro/rl/learning/helpers.py | 2 + maro/rl/learning/learning_loop.py | 2 +- maro/rl/learning/policy_manager.py | 86 ++++----- .../rl_formulation.ipynb | 81 ++++++--- 15 files changed, 154 insertions(+), 284 deletions(-) delete mode 100644 docs/source/examples/multi_agent_dqn_cim.rst diff --git a/docs/source/examples/multi_agent_dqn_cim.rst b/docs/source/examples/multi_agent_dqn_cim.rst deleted file mode 100644 index ef6302117..000000000 --- a/docs/source/examples/multi_agent_dqn_cim.rst +++ /dev/null @@ -1,168 +0,0 @@ -Multi Agent DQN for CIM -================================================ - -This example demonstrates how to use MARO's reinforcement learning (RL) toolkit to solve the container -inventory management (CIM) problem. It is formalized as a multi-agent reinforcement learning problem, -where each port acts as a decision agent. When a vessel arrives at a port, these agents must take actions -by transfering a certain amount of containers to / from the vessel. The objective is for the agents to -learn policies that minimize the overall container shortage. - -Trajectory ----------- - -The ``CIMTrajectoryForDQN`` inherits from ``Trajectory`` function and implements methods to be used as callbacks -in the roll-out loop. In this example, - * ``get_state`` converts environment observations to state vectors that encode temporal and spatial information. - The temporal information includes relevant port and vessel information, such as shortage and remaining space, - over the past k days (here k = 7). The spatial information includes features of the downstream ports. - * ``get_action`` converts agents' output (an integer that maps to a percentage of containers to be loaded - to or unloaded from the vessel) to action objects that can be executed by the environment. - * ``get_offline_reward`` computes the reward of a given action as a linear combination of fulfillment and - shortage within a future time frame. - * ``on_finish`` processes a complete trajectory into data that can be used directly by the learning agents. - - -.. code-block:: python - class CIMTrajectoryForDQN(Trajectory): - def __init__( - self, env, *, port_features, vessel_features, action_space, look_back, max_ports_downstream, - reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, - finite_vessel_space=True, has_early_discharge=True - ): - super().__init__(env) - self.port_features = port_features - self.vessel_features = vessel_features - self.action_space = action_space - self.look_back = look_back - self.max_ports_downstream = max_ports_downstream - self.reward_eval_delay = reward_eval_delay - self.fulfillment_factor = fulfillment_factor - self.shortage_factor = shortage_factor - self.time_decay = time_decay - self.finite_vessel_space = finite_vessel_space - self.has_early_discharge = has_early_discharge - - def get_state(self, event): - vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] - tick, port_idx, vessel_idx = event.tick, event.port_idx, event.vessel_idx - ticks = [max(0, tick - rt) for rt in range(self.look_back - 1)] - future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') - port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_features] - vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_features] - return {port_idx: np.concatenate((port_features, vessel_features))} - - def get_action(self, action_by_agent, event): - vessel_snapshots = self.env.snapshot_list["vessels"] - action_info = list(action_by_agent.values())[0] - model_action = action_info[0] if isinstance(action_info, tuple) else action_info - scope, tick, port, vessel = event.action_scope, event.tick, event.port_idx, event.vessel_idx - zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. - vessel_space = vessel_snapshots[tick:vessel:self.vessel_features][2] if self.finite_vessel_space else float("inf") - early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 - percent = abs(self.action_space[model_action]) - - if model_action < zero_action_idx: - action_type = ActionType.LOAD - actual_action = min(round(percent * scope.load), vessel_space) - elif model_action > zero_action_idx: - action_type = ActionType.DISCHARGE - plan_action = percent * (scope.discharge + early_discharge) - early_discharge - actual_action = round(plan_action) if plan_action > 0 else round(percent * scope.discharge) - else: - actual_action, action_type = 0, None - - return {port: Action(vessel, port, actual_action, action_type)} - - def get_offline_reward(self, event): - port_snapshots = self.env.snapshot_list["ports"] - start_tick = event.tick + 1 - ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) - - future_fulfillment = port_snapshots[ticks::"fulfillment"] - future_shortage = port_snapshots[ticks::"shortage"] - decay_list = [ - self.time_decay ** i for i in range(self.reward_eval_delay) - for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay) - ] - - tot_fulfillment = np.dot(future_fulfillment, decay_list) - tot_shortage = np.dot(future_shortage, decay_list) - - return np.float32(self.fulfillment_factor * tot_fulfillment - self.shortage_factor * tot_shortage) - - def on_env_feedback(self, event, state_by_agent, action_by_agent, reward): - self.trajectory["event"].append(event) - self.trajectory["state"].append(state_by_agent) - self.trajectory["action"].append(action_by_agent) - - def on_finish(self): - exp_by_agent = defaultdict(lambda: defaultdict(list)) - for i in range(len(self.trajectory["state"]) - 1): - agent_id = list(self.trajectory["state"][i].keys())[0] - exp = exp_by_agent[agent_id] - exp["S"].append(self.trajectory["state"][i][agent_id]) - exp["A"].append(self.trajectory["action"][i][agent_id]) - exp["R"].append(self.get_offline_reward(self.trajectory["event"][i])) - exp["S_"].append(list(self.trajectory["state"][i + 1].values())[0]) - - return dict(exp_by_agent) - - -Agent ------ - -The out-of-the-box DQN is used as our agent. - -.. code-block:: python - agent_config = { - "model": ..., - "optimization": ..., - "hyper_params": ... - } - - def get_dqn_agent(): - q_model = SimpleMultiHeadModel( - FullyConnected(**agent_config["model"]), optim_option=agent_config["optimization"] - ) - return DQN(q_model, DQNConfig(**agent_config["hyper_params"])) - - -Training --------- - -The distributed training consists of one learner process and multiple actor processes. The learner optimizes -the policy by collecting roll-out data from the actors to train the underlying agents. - -The actor process must create a roll-out executor for performing the requested roll-outs, which means that the -the environment simulator and shapers should be created here. In this example, inference is performed on the -actor's side, so a set of DQN agents must be created in order to load the models (and exploration parameters) -from the learner. - -.. code-block:: python - def cim_dqn_actor(): - env = Env(**training_config["env"]) - agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) - actor = Actor(env, agent, CIMTrajectoryForDQN, trajectory_kwargs=common_config) - actor.worker(training_config["group"]) - -The learner's side requires a concrete learner class that inherits from ``AbsLearner`` and implements the ``run`` -method which contains the main training loop. Here the implementation is similar to the single-threaded version -except that the ``collect`` method is used to obtain roll-out data from the actors (since the roll-out executors -are located on the actors' side). The agents created here are where training occurs and hence always contains the -latest policies. - -.. code-block:: python - def cim_dqn_learner(): - env = Env(**training_config["env"]) - agent = MultiAgentWrapper({name: get_dqn_agent() for name in env.agent_idx_list}) - scheduler = TwoPhaseLinearParameterScheduler(training_config["max_episode"], **training_config["exploration"]) - actor = ActorProxy( - training_config["group"], training_config["num_actors"], - update_trigger=training_config["learner_update_trigger"] - ) - learner = OffPolicyLearner(actor, scheduler, agent, **training_config["training"]) - learner.run() - -.. note:: - - All related code snippets are supported in `maro playground `_. diff --git a/examples/rl/README.md b/examples/rl/README.md index ebd112431..3bdbca4aa 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -4,13 +4,13 @@ This folder contains scenarios that employ reinforcement learning. MARO's RL too ## How to Run -Scripts do run scenarios using the common workflow, start by choosing "sync" or "async" for the ``mode`` field in ``config.yml``. The ``scripts/docker`` folder provides bash scripts for simulating the distributed workflow using multiple docker containers on a single host. Go to this folder and execute ``bash run.sh`` to launch the program and Docker Compose will take care of starting the necessary containers. Note that the script will build the docker image first if it has not already been built by running ``bash build.sh``. When the program is finished, be sure to run ``bash kill.sh`` to clean up the containers and remove the network. +Scripts to run the common workflow in docker containers are in ``scripts/docker``. Start by choosing "single", "sync" or "async" for the ``mode`` field in ``config.yml`` to run a scenario in single-threaded, synchronous and asynchronous modes, respectively. Go to this folder and execute ``bash run.sh`` to launch the program and Docker Compose will take care of starting the necessary containers. Note that the script will build the docker image first if it has not already been built by running ``bash build.sh``. When the program is finished, be sure to run ``bash kill.sh`` to clean up the containers and remove the network. -## Write Your Own Scenarios +## Create Your Own Scenarios -To use the workflow provided under ``workflows``, the following ingredients are required: -* ``get_env_sampler``, a function that takes no parameters and returns an environment wrapper instance. -* ``get_agent_wrapper``, a function that takes no parameters and returns an agent wrapper instance. -* ``policy_func_index``, a dictionary mapping policy names to functions that create them. -The policy-creating functions should take as its sole parameter a flag indicating whether the created policy is for roll-out or training. -We recommend that you follow the ``cim`` example to write your own scenario. +The workflow scripts make it easy to create your own scenarios by only supplying the necessary ingredients without worrying about putting them together. The ingredients include: +* Definitions of state, action and reward shaping logic pertinent to your simulator and policies. +These definitions should be encapsulated in ``get_env_sampler``, which is a function that takes no parameters and returns an environment sampler; +* Definitions of policies and agent-to-policy mappings. These definitions should be provided as a dictionary named ``policy_func_index`` that maps policy names to functions that create them. A policy creating function takes a name and return a policy instance with that name; + +Optionally, if you want to diff --git a/examples/rl/cim/README.md b/examples/rl/cim/README.md index 2d280f476..2e17ac435 100644 --- a/examples/rl/cim/README.md +++ b/examples/rl/cim/README.md @@ -1,10 +1,9 @@ # Container Inventory Management -Container inventory management (CIM) is a scenario where reinforcement learning (RL) can potentially prove useful. In this folder you can find: -* ``env_wrapper.py``, which contains a function to generate an environment wrapper to interact -with our "agent" (see below); -* ``agent_wrapper.py``, which contains a function to generate an agent wrapper to interact -with the environment wrapper; -* ``policy_index``, which maps policy names to functions that create them; the functions to create DQN and Actor-Critic policies are defined in ``dqn.py`` and ``ac.py``, respectively. +This example demonstrates the use of MARO's RL toolkit to optimize container inventory management. The scenario consists of a set of ports, each acting as a learning agent, and vessels that transfer empty containers among them. Each port must decide 1) whether to load or discharge containers when a vessel arrives and 2) how many containers to be loaded or discharged. The objective is to minimize the overall container shortage over a certain period of time. In this folder you can find: +* ``config.py``, which contains environment and policy configurations for the scenario. +* ``env_sampler.py``, which contains definitions of state, action and reward shaping. +* ``policies.py``, which contains definitions of DQN and Actor-Critic. +* ``callbacks.py``, which contains processing logic to be executed at the step and episode levels. -The code for the actual learning workflows (e.g., learner, roll-out worker and trainer) can be found under ``examples/rl/workflows``. The reason for putting it in a separate folder is that these workflows apply to any scenario, so long as the necessary component generators, such as the ones listed above, are provided. See ``README`` under ``examples/rl`` for details. We recommend that you follow this example to write your own scenarios. \ No newline at end of file +The scripts for running the learning workflows can be found under ``examples/rl/workflows``. See ``README`` under ``examples/rl`` for details about the general applicability of these scripts. We recommend that you follow this example to write your own scenarios. \ No newline at end of file diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py index 0d56e71c4..9f9839ccb 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/rl/cim/callbacks.py @@ -5,13 +5,9 @@ from os import makedirs from os.path import dirname, join, realpath -from maro.utils import Logger - log_dir = join(dirname(realpath(__file__)), "log", str(time.time())) makedirs(log_dir, exist_ok=True) -simulation_logger = Logger("SIMUALTION", dump_folder=log_dir) - def post_step(env, tracker, state, action, env_action, reward, tick): tracker["env_metric"] = env.metrics @@ -20,22 +16,22 @@ def post_step(env, tracker, state, action, env_action, reward, tick): def post_collect(trackers, ep, segment): # print the env metric from each rollout worker for tracker in trackers: - simulation_logger.info(f"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}") + print(f"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}") # print the average env metric if len(trackers) > 1: metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} - simulation_logger.info(f"average env summary (episode {ep}, segment {segment}): {avg_metric}") + print(f"average env summary (episode {ep}, segment {segment}): {avg_metric}") def post_evaluate(trackers, ep): # print the env metric from each rollout worker for tracker in trackers: - simulation_logger.info(f"env summary (evaluation episode {ep}): {tracker['env_metric']}") + print(f"env summary (episode {ep}): {tracker['env_metric']}") # print the average env metric if len(trackers) > 1: metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} - simulation_logger.info(f"average env summary (evaluation episode {ep}): {avg_metric}") + print(f"average env summary (episode {ep}): {avg_metric}") diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index a1148311c..1258286af 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -23,6 +23,10 @@ class CIMEnvSampler(AbsEnvSampler): def get_state(self, tick=None): + """ + The state vector includes shortage and remaining vessel space over the past k days (where k is the "look_back" + value in ``state_shaping_conf``), as well as all downstream port features. + """ if tick is None: tick = self.env.tick vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] @@ -36,6 +40,12 @@ def get_state(self, tick=None): return {port_idx: state} def get_env_actions(self, action_by_agent): + """ + The policy output is an integer from [0, 20] which is to be interpreted as the index of ``action_space`` in + ``action_shaping_conf``. For example, action 5 corresponds to -0.5, which means loading 50% of the containers + available at the current port to the vessel, while action 18 corresponds to 0.8, which means loading 80% of the + containers on the vessel to the port. Note that action 10 corresponds 0.0, which means doing nothing. + """ action_space = action_shaping_conf["action_space"] finite_vsl_space = action_shaping_conf["finite_vessel_space"] has_early_discharge = action_shaping_conf["has_early_discharge"] @@ -62,7 +72,13 @@ def get_env_actions(self, action_by_agent): return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)] def get_reward(self, actions, tick): - """Delayed reward evaluation.""" + """ + The reward is defined as a linear combination of fulfillment and shortage measures. The fulfillment and + shortage measures are the sums of fulfillment and shortage values over the next k days, respectively, each + adjusted with exponential decay factors (using the "time_decay" value in ``reward_shaping_conf``) to put more + emphasis on the near future. Here k is the "time_window" value in ``reward_shaping_conf``. The linear + combination coefficients are given by "fulfillment_factor" and "shortage_factor" in ``reward_shaping_conf``. + """ start_tick = tick + 1 ticks = list(range(start_tick, start_tick + reward_shaping_conf["time_window"])) diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index 1abe1c3ac..af958fc42 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -44,7 +44,7 @@ f"MODE=single", f"NUMEPISODES={config['num_episodes']}", f"NUMSTEPS={config['num_steps']}", - f"EVALSCH={config['eval_schedule']}", + f"EVALSCH={config['eval_schedule']}" ] } } @@ -56,6 +56,7 @@ "services": {"redis": {"image": "redis:6", "container_name": f"{namespace}.{redis_host}"}} } + policy_group = "-".join([config['job'], 'policies']) common_env = [ f"REDISHOST={namespace}.{redis_host}", f"REDISPORT={config['redis']['port']}", @@ -63,7 +64,8 @@ f"SCENARIO={config['scenario']}", f"MODE={config['mode']}", f"POLICYMANAGERTYPE={config['policy_manager']['type']}", - f"CHECKPOINTDIR={config['checkpoint_dir']}" + f"CHECKPOINTDIR={config['checkpoint_dir']}", + f"POLICYGROUP={policy_group}" ] common_env.append(f"NUMROLLOUTS={config[config['mode']]['num_rollouts']}") @@ -73,7 +75,6 @@ common_env.append(f"NUMGRADWORKERS={config['data_parallel']['num_workers']}") common_env.append(f"ALLOCATIONMODE={config['data_parallel']['allocation_mode']}") if config["policy_manager"]["type"] == "distributed": - common_env.append(f"LEARNGROUP={config['policy_manager']['distributed']['group']}") common_env.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") # grad worker config @@ -101,6 +102,7 @@ mode = config["mode"] if mode == "sync": # main process spec + rollout_group = "-".join([config['job'], 'rollout']) docker_compose_manifest["services"]["main"] = { **common_spec, **{ @@ -112,7 +114,7 @@ f"NUMSTEPS={config['num_steps']}", f"EVALSCH={config['eval_schedule']}", f"NUMEVALROLLOUTS={config[config['mode']]['num_eval_rollouts']}", - f"ROLLOUTGROUP={config['sync']['distributed']['group']}", + f"ROLLOUTGROUP={rollout_group}", f"MAXLAG={config['max_lag']}", f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", @@ -130,7 +132,7 @@ worker_spec["container_name"] = f"{namespace}.{str_id}" worker_spec["environment"] = [ f"WORKERID={worker_id}", - f"ROLLOUTGROUP={config['sync']['distributed']['group']}" + f"ROLLOUTGROUP={rollout_group}" ] + common_env docker_compose_manifest["services"][str_id] = worker_spec elif mode == "async": @@ -141,7 +143,7 @@ "container_name": f"{namespace}.policy_server", "command": "python3 /maro/rl_examples/workflows/policy_manager.py", "environment": [ - f"GROUP={config['async']['group']}", + f"GROUP={config['job']}", f"MAXLAG={config['max_lag']}" ] + common_env } @@ -155,7 +157,7 @@ actor_spec["container_name"] = f"{namespace}.{str_id}" actor_spec["environment"] = [ f"ACTORID={actor_id}", - f"GROUP={config['async']['group']}", + f"GROUP={config['job']}", f"NUMEPISODES={config['num_episodes']}", f"NUMSTEPS={config['num_steps']}" ] + common_env diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 7273ecf01..972e2b687 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -14,20 +14,17 @@ sync: num_eval_rollouts: 1 rollout_type: distributed # multi-process, distributed distributed: - group: rollout min_finished_workers: 2 max_extra_recv_tries: 1 extra_recv_timeout: 200 async: - group: async num_rollouts: 3 policy_manager: type: multi-process # simple, multi-process, distributed distributed: - group: learn num_hosts: 2 data_parallel: - enable: false + enable: true num_workers: 2 allocation_mode: by-policy # by-policy, by-agent, by-experience redis: diff --git a/examples/rl/workflows/grad_worker.py b/examples/rl/workflows/grad_worker.py index 5841a46b3..8d38b7fa6 100644 --- a/examples/rl/workflows/grad_worker.py +++ b/examples/rl/workflows/grad_worker.py @@ -27,7 +27,7 @@ else: num_hosts = 0 - group = getenv("LEARNGROUP", default="learn") + group = getenv("POLICYGROUP", default="learn") grad_worker( policy_func_dict, int(worker_id), diff --git a/examples/rl/workflows/learning_loop.py b/examples/rl/workflows/learning_loop.py index d3f19f98b..6e522ee5a 100644 --- a/examples/rl/workflows/learning_loop.py +++ b/examples/rl/workflows/learning_loop.py @@ -11,7 +11,6 @@ if workflow_dir not in sys.path: sys.path.insert(0, workflow_dir) -from policy_manager import get_policy_manager from general import post_collect, post_evaluate, get_env_sampler, log_dir @@ -66,6 +65,8 @@ def get_rollout_manager(): if num_episodes is None: raise ValueError("Missing environment variable: NUMEPISODES") + if getenv("MODE") != "single": + from policy_manager import get_policy_manager learn( get_rollout_manager if getenv("MODE") != "single" else get_env_sampler, int(num_episodes), diff --git a/examples/rl/workflows/policy_host.py b/examples/rl/workflows/policy_host.py index 198ba8738..83a44fa3d 100644 --- a/examples/rl/workflows/policy_host.py +++ b/examples/rl/workflows/policy_host.py @@ -24,7 +24,7 @@ if num_grad_workers is None: num_grad_workers = 0 - group = getenv("LEARNGROUP", default="learn") + group = getenv("POLICYGROUP", default="learn") policy_host( policy_func_dict, int(host_id), diff --git a/examples/rl/workflows/policy_manager.py b/examples/rl/workflows/policy_manager.py index 68ee41776..19a7c058d 100644 --- a/examples/rl/workflows/policy_manager.py +++ b/examples/rl/workflows/policy_manager.py @@ -21,7 +21,6 @@ def get_policy_manager(): manager_type = getenv("POLICYMANAGERTYPE", default="simple") data_parallel = getenv("DATAPARALLEL") == "True" num_grad_workers = int(getenv("NUMGRADWORKERS", default=1)) - group = getenv("LEARNGROUP", default="learn") allocation_mode = getenv("ALLOCATIONMODE", default="by-policy") if data_parallel: allocator = WorkerAllocator(allocation_mode, num_grad_workers, list(policy_func_dict.keys()), agent2policy) @@ -37,8 +36,8 @@ def get_policy_manager(): load_path_dict={id_: join(checkpoint_dir, id_) for id_ in policy_func_dict}, checkpoint_every=7, save_dir=checkpoint_dir, - group=group, worker_allocator=allocator, + group=getenv("POLICYGROUP"), proxy_kwargs=proxy_kwargs, log_dir=log_dir ) @@ -48,21 +47,24 @@ def get_policy_manager(): load_path_dict={id_: join(checkpoint_dir, id_) for id_ in policy_func_dict}, auto_checkpoint=True, save_dir=checkpoint_dir, - group=group, worker_allocator=allocator, + group=getenv("POLICYGROUP"), proxy_kwargs=proxy_kwargs, log_dir=log_dir ) elif manager_type == "distributed": num_hosts = int(getenv("NUMHOSTS", default=5)) return DistributedPolicyManager( - list(policy_func_dict.keys()), group, num_hosts, + list(policy_func_dict.keys()), num_hosts, + group=getenv("POLICYGROUP"), worker_allocator=allocator, proxy_kwargs=proxy_kwargs, log_dir=log_dir ) - raise ValueError(f"Unsupported policy manager type: {manager_type}. Supported modes: simple, distributed") + raise ValueError( + f"Unsupported policy manager type: {manager_type}. Supported modes: simple, multi-process, distributed" + ) if __name__ == "__main__": diff --git a/maro/rl/learning/helpers.py b/maro/rl/learning/helpers.py index b2729e9a8..240eaff83 100644 --- a/maro/rl/learning/helpers.py +++ b/maro/rl/learning/helpers.py @@ -6,6 +6,8 @@ def get_rollout_finish_msg(ep, step_range, exploration_params=None): """Generate a brief summary message for a finished roll-out""" + if exploration_params: + exploration_params = {policy_id: params for policy_id, params in exploration_params.items() if params} if exploration_params: return ( f"Roll-out finished (episode {ep}, " diff --git a/maro/rl/learning/learning_loop.py b/maro/rl/learning/learning_loop.py index 88c1f814e..2e25ff72d 100644 --- a/maro/rl/learning/learning_loop.py +++ b/maro/rl/learning/learning_loop.py @@ -111,7 +111,7 @@ def collect_and_update(): for ep in range(1, num_episodes + 1): collect_and_update() - if ep == eval_schedule[eval_point_index]: + if eval_schedule and ep == eval_schedule[eval_point_index]: eval_point_index += 1 if isinstance(rollout_manager, AbsEnvSampler): trackers = [rollout_manager.test()] diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 72dbb12ce..bc5ad4c47 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -15,6 +15,10 @@ from maro.rl.utils import MsgKey, MsgTag from maro.utils import Logger +# default group name for the cluster consisting of a policy manager and all policy hosts. +# If data parallelism is enabled, the gradient workers will also belong in this group. +DEFAULT_POLICY_GROUP = "policy_group_default" + class AbsPolicyManager(ABC): """Facility that controls policy update and serves the latest policy states.""" @@ -53,7 +57,7 @@ def server(self, group: str, num_actors: int, max_lag: int = 0, proxy_kwargs: di collected using policy versions older than (current_version - max_lag) will be discarded. Defaults to 0, in which case only experiences collected using the latest policy version will be returned. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. + for details. Defaults to an empty dictionary. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ peers = {"actor": num_actors} @@ -95,15 +99,15 @@ class SimplePolicyManager(AbsPolicyManager): create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a function that takes policy name as the only parameter and return an ``RLPolicy`` instance. load_path_dict (Dict[str, str]): If provided, policies whose IDs are in the dictionary keys will load the states - from the corresponding path. Defaults to None. + from the corresponding path. Defaults to an empty dictionary. checkpoint_every (int): The policies will be checkpointed (i.e., persisted to disk) every this number of seconds only if there are updates since the last checkpoint. This must be a positive integer or -1, with -1 meaning no checkpointing. Defaults to -1. save_dir (str): The directory under which to checkpoint the policy states. - group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless - if not in ``data_parallel`` mode. Defaults to "learn". worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. - If it is not None, the policy manager will run in ``data_parallel`` mode. Defaults to None. + If not None, the policies will be trained in data-parallel mode. Defaults to None. + group (str): Group name for the cluster consisting of the manager and all policy hosts. Ignored if + ``worker_allocator`` is None. Defaults to DEFAULT_POLICY_GROUP. proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current @@ -112,11 +116,11 @@ class SimplePolicyManager(AbsPolicyManager): def __init__( self, create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - load_path_dict: Dict[str, str] = None, + load_path_dict: Dict[str, str] = {}, checkpoint_every: int = -1, save_dir: str = None, - group: str = "learn", worker_allocator: WorkerAllocator = None, + group: str = DEFAULT_POLICY_GROUP, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): @@ -139,11 +143,9 @@ def __init__( self._logger.info(f"Auto-checkpoint policy state every {self.checkpoint_every} seconds") # data parallelism - self._data_parallel = worker_allocator is not None - if self._data_parallel: + self._worker_allocator = worker_allocator + if self._worker_allocator: self._num_grad_workers = worker_allocator.num_workers - self._worker_allocator = worker_allocator - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._worker_allocator.set_logger(self._logger) self._proxy = Proxy( group, "policy_manager", {"grad_worker": self._num_grad_workers}, @@ -192,8 +194,6 @@ def _checkpoint(self): policy.save(self.save_path[id_]) self._logger.info(f"Saved policy {id_} to {self.save_path[id_]}") last_checkpointed_version[id_] = self._version[id_] - else: - self._logger.info(f"Latest version of policy {id_} already checkpointed") time.sleep(self.checkpoint_every) def exit(self): @@ -207,15 +207,14 @@ class MultiProcessPolicyManager(AbsPolicyManager): create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a function that takes policy name as the only parameter and return an ``RLPolicy`` instance. load_path_dict (Dict[str, str]): If provided, policies whose IDs are in the dictionary keys will load the states - from the corresponding path. Defaults to None. + from the corresponding path. Defaults to an empty dictionary. auto_checkpoint (bool): If True, the policies will be checkpointed (i.e., persisted to disk) every time they are updated. Defaults to -1. save_dir (str): The directory under which to checkpoint the policy states. - group (str): Group name for the cluster consisting of the manager and all policy hosts. It will be meaningless - if not in ``data_parallel`` mode. Defaults to "learn". - worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies - for parallelization. If it is not None, the policy manager will run in ``data_parallel`` mode. - Defaults to None. + worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. + If not None, the policies will be trained in data-parallel mode. Defaults to None. + group (str): Group name for the cluster consisting of the manager and all policy hosts. Ignored if + ``worker_allocator`` is None. Defaults to DEFAULT_POLICY_GROUP. proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current @@ -224,11 +223,11 @@ class MultiProcessPolicyManager(AbsPolicyManager): def __init__( self, create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - load_path_dict: Dict[str, str] = None, + load_path_dict: Dict[str, str] = {}, auto_checkpoint: bool = False, save_dir: str = None, - group: str = "learn", worker_allocator: WorkerAllocator = None, + group: str = DEFAULT_POLICY_GROUP, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): @@ -237,8 +236,8 @@ def __init__( self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) # data parallelism - self._data_parallel = worker_allocator is not None - if self._data_parallel: + self._worker_allocator = worker_allocator is not None + if self._worker_allocator: self._num_grad_workers = worker_allocator.num_workers self._worker_allocator = worker_allocator self._worker_allocator.set_logger(self._logger) @@ -255,7 +254,7 @@ def __init__( def policy_host(id_, create_policy_func, conn, initial_state_path): policy = create_policy_func(id_) save_path = os.path.join(save_dir, id_) - if self._data_parallel: + if self._worker_allocator: self._logger.info("========== data parallel mode ==========") policy.data_parallel( group, "policy_host", {"grad_worker": self._num_grad_workers}, component_name=f"POLICY_HOST.{id_}", @@ -271,7 +270,7 @@ def policy_host(id_, create_policy_func, conn, initial_state_path): info = msg["rollout_info"] if isinstance(info, list): policy.update(info) - elif self._data_parallel: + elif self._worker_allocator: policy.learn_with_data_parallel(info, msg["workers"]) else: policy.learn(info) @@ -281,7 +280,7 @@ def policy_host(id_, create_policy_func, conn, initial_state_path): policy.save(save_path) self._logger.info(f"Saved policy {id_} to {save_path}") elif msg["type"] == "quit": - if self._data_parallel: + if self._worker_allocator: policy.exit_data_parallel() policy.save(save_path) self._logger.info(f"Saved policy {id_} to {save_path}") @@ -310,13 +309,13 @@ def update(self, rollout_info: Dict[str, list]): The roll-out information is grouped by policy name and may be either a training batch or a list of loss information dictionaries computed directly by roll-out workers. """ - if self._data_parallel: + if self._worker_allocator: # re-allocate grad workers before update. self._policy2workers, self._worker2policies = self._worker_allocator.allocate() for policy_id, info in rollout_info.items(): msg = {"type": "learn", "rollout_info": info} - if self._data_parallel: + if self._worker_allocator: msg["workers"] = self._policy2workers[policy_id] self._manager_end[policy_id].send(msg) for policy_id, conn in self._manager_end.items(): @@ -342,7 +341,7 @@ def exit(self): """Tell the policy host processes to exit.""" for conn in self._manager_end.values(): conn.send({"type": "quit"}) - if self._data_parallel: + if self._worker_allocator: self._proxy.close() @@ -351,13 +350,14 @@ class DistributedPolicyManager(AbsPolicyManager): Args: policy_ids (List[str]): Names of the registered policies. - group (str): Group name for the cluster consisting of the manager and all policy hosts. num_hosts (int): Number of hosts. The hosts will be identified by "POLICY_HOST.i", where 0 <= i < num_hosts. - worker_allocator (WorkerAllocator): The allocation strategy of allocating workers to policies - for parallelization. If it is not None, the policy manager will run in ``data_parallel`` mode. - Defaults to None. + group (str): Group name for the cluster consisting of the manager and all policy hosts. If ``worker_allocator`` + is provided, the gradient workers will also belong to the same cluster. Defaults to + DEFAULT_POLICY_GROUP. + worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. + If not None, the policies will be trained in data-parallel mode. Defaults to None. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. + for details. Defaults to an empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init time and this directory will be used to save the log files generated by it. Defaults to the current working directory. @@ -365,16 +365,16 @@ class DistributedPolicyManager(AbsPolicyManager): def __init__( self, policy_ids: List[str], - group: str, num_hosts: int, + group: str = DEFAULT_POLICY_GROUP, worker_allocator: WorkerAllocator = None, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): super().__init__() - self._data_parallel = worker_allocator is not None + self._worker_allocator = worker_allocator peers = {"policy_host": num_hosts} - if self._data_parallel: + if self._worker_allocator: peers["grad_worker"] = worker_allocator.num_workers self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) @@ -420,7 +420,7 @@ def update(self, rollout_info: Dict[str, list]): The roll-out information is grouped by policy name and may be either a training batch or a list if loss information dictionaries computed directly by roll-out workers. """ - if self._data_parallel: + if self._worker_allocator: self._policy2workers, self._worker2policies = self._worker_allocator.allocate() msg_dict = defaultdict(lambda: defaultdict(dict)) @@ -461,7 +461,7 @@ def exit(self): def policy_host( create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], host_idx: int, - group: str, + group: str = DEFAULT_POLICY_GROUP, proxy_kwargs: dict = {}, data_parallel: bool = False, num_grad_workers: int = 1, @@ -474,9 +474,9 @@ def policy_host( creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. host_idx (int): Integer host index. The host's ID in the cluster will be "POLICY_HOST.{host_idx}". group (str): Group name for the training cluster, which includes all policy hosts and a policy manager that - manages them. + manages them. Defaults to DEFAULT_POLICY_GROUP. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. + for details. Defaults to an empty dictionary. data_parallel (bool): Whether to train policy on remote gradient workers to perform data-parallel. Defaults to False. num_grad_workers (int): The number of gradient worker nodes in data-parallel mode. Defaults to 1. @@ -539,7 +539,7 @@ def grad_worker( create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], worker_idx: int, num_hosts: int, - group: str, + group: str = DEFAULT_POLICY_GROUP, proxy_kwargs: dict = {}, max_policy_number: int = 10, log_dir: str = getcwd() @@ -555,7 +555,7 @@ def grad_worker( group (str): Group name for the training cluster, which includes all policy hosts and a policy manager that manages them. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. + for details. Defaults to an empty dictionary. max_policy_number (int): Maximum policy number in a single worker node. Defaults to 10. log_dir (str): Directory to store logs in. Defaults to the current working directory. """ diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 8355859cc..05fb08980 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -6,7 +6,7 @@ "source": [ "# Quick Start\n", "\n", - "This notebook demonstrates how to use MARO's reinforcement learning (RL) toolkit to solve the container inventory management ([CIM](https://maro.readthedocs.io/en/latest/scenarios/container_inventory_management.html)) problem. It is formalized as a multi-agent reinforcement learning problem, where each port acts as a decision agent. When a vessel arrives at a port, these agents must take actions by transfering a certain amount of containers to / from the vessel. The objective is for the agents to learn policies that minimize the cumulative container shortage. " + "This notebook demonstrates the use of MARO's RL toolkit to optimize container inventory management. The scenario consists of a set of ports, each acting as a learning agent, and vessels that transfer empty containers among them. Each port must decide 1) whether to load or discharge containers when a vessel arrives and 2) how many containers to be loaded or discharged. The objective is to minimize the overall container shortage over a certain period of time." ] }, { @@ -48,7 +48,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Environment Sampler" + "## Environment Sampler\n", + "\n", + "An environment sampler defines state, action and reward shaping logic so that policies can interact with the environment." ] }, { @@ -64,6 +66,10 @@ "\n", "class CIMEnvSampler(AbsEnvSampler):\n", " def get_state(self, tick=None):\n", + " \"\"\"\n", + " The state vector includes shortage and remaining vessel space over the past k days (where k is the \"look_back\"\n", + " value in \"state_shaping_conf\" from the cell above), as well as all downstream port features.\n", + " \"\"\"\n", " if tick is None:\n", " tick = self.env.tick\n", " vessel_snapshots, port_snapshots = self.env.snapshot_list[\"vessels\"], self.env.snapshot_list[\"ports\"]\n", @@ -77,6 +83,13 @@ " return {port_idx: state}\n", "\n", " def get_env_actions(self, action_by_agent):\n", + " \"\"\"\n", + " The policy output is an integer from [0, 20] which is to be interpreted as the index of \"action_space\" in\n", + " \"action_shaping_conf\" from the cell above. For example, action 5 corresponds to -0.5, which means loading\n", + " 50% of the containers available at the current port to the vessel, while action 18 corresponds to 0.8, which\n", + " means loading 80% of the containers on the vessel to the port. Note that action 10 corresponds 0.0, which\n", + " means doing nothing. \n", + " \"\"\"\n", " action_space = action_shaping_conf[\"action_space\"]\n", " finite_vsl_space = action_shaping_conf[\"finite_vessel_space\"]\n", " has_early_discharge = action_shaping_conf[\"has_early_discharge\"]\n", @@ -103,7 +116,13 @@ " return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)]\n", "\n", " def get_reward(self, actions, tick):\n", - " \"\"\"Delayed reward evaluation.\"\"\"\n", + " \"\"\"\n", + " The reward is defined as a linear combination of fulfillment and shortage measures. The fulfillment and\n", + " shortage measure are the sums of fulfillment and shortage values over the next k days, respectively, each\n", + " adjusted with exponential decay factors (using the \"time_decay\" value in \"reward_shaping_conf\" from the\n", + " cell above) to put more emphasis on the near future. Here k is the \"time_window\" value in \"reward_shaping_conf\".\n", + " The linear combination coefficients are given by \"fulfillment_factor\" and \"shortage_factor\" in \"reward_shaping_conf\".\n", + " \"\"\"\n", " start_tick = tick + 1\n", " ticks = list(range(start_tick, start_tick + reward_shaping_conf[\"time_window\"]))\n", "\n", @@ -125,7 +144,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## [Policies](https://maro.readthedocs.io/en/latest/key_components/rl_toolkit.html#agent)\n", + "## [Policies](https://maro.readthedocs.io/en/latest/key_components/rl_toolkit.html#policy)\n", "\n", "The out-of-the-box ActorCritic is used as our agent." ] @@ -235,7 +254,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Training\n", + "## Learning Loop\n", "\n", "This code cell demonstrates a typical single-threaded training workflow." ] @@ -247,11 +266,18 @@ "outputs": [], "source": [ "from maro.simulator import Env\n", - "from maro.rl.learning import simple_learner\n", + "from maro.rl.learning import learn\n", "from maro.utils import set_seeds\n", "\n", - "# post-env-step callback\n", + "\"\"\"\n", + "The environment sampler contains a \"tracker\" dict inherited from the \"AbsEnvSampler\" base class, which can be\n", + "used to record any information one wishes to keep track of during a roll-out episode throught the \"post_step\"\n", + "callback function.\n", + "\"\"\"\n", "def post_step(env, tracker, state, action, env_action, reward, tick):\n", + " \"\"\"\n", + " Here we simply record the latest env metric without keeping the history for logging purposes.\n", + " \"\"\"\n", " tracker[\"env_metric\"] = env.metrics\n", "\n", "def get_env_sampler():\n", @@ -263,33 +289,30 @@ " post_step=post_step\n", " )\n", "\n", - "# post-episode callback\n", + "# post-episode callback, executed at the end of an episode or episode segment.\n", "def post_collect(trackers, ep, segment):\n", - " # print the env metric from each rollout worker\n", - " for tracker in trackers:\n", - " print(f\"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}\")\n", - "\n", - " # print the average env metric\n", - " if len(trackers) > 1:\n", - " metric_keys, num_trackers = trackers[0][\"env_metric\"].keys(), len(trackers)\n", - " avg_metric = {key: sum(tr[\"env_metric\"][key] for tr in trackers) / num_trackers for key in metric_keys}\n", - " print(f\"average env summary (episode {ep}, segment {segment}): {avg_metric}\")\n", - "\n", - "# post-evaluation callback\n", + " \"\"\"\n", + " Print the metric recorded in the env tracker at the end of an episode. The parameter \"trackers\" is actually\n", + " a list because in a distributed setting, the main thread usually receives trackers from multiple roll-out\n", + " instances.\n", + " \"\"\"\n", + " print(f\"env summary (episode {ep}, segment {segment}): {trackers[0]['env_metric']}\")\n", + "\n", + "# post-evaluation callback, executed at the end of an evaluation episode.\n", "def post_evaluate(trackers, ep):\n", - " # print the env metric from each rollout worker\n", - " for tracker in trackers:\n", - " print(f\"env summary (evaluation episode {ep}): {tracker['env_metric']}\")\n", - "\n", - " # print the average env metric\n", - " if len(trackers) > 1:\n", - " metric_keys, num_trackers = trackers[0][\"env_metric\"].keys(), len(trackers)\n", - " avg_metric = {key: sum(tr[\"env_metric\"][key] for tr in trackers) / num_trackers for key in metric_keys}\n", - " simulation_logger.info(f\"average env summary (evaluation episode {ep}): {avg_metric}\")\n", + " \"\"\"\n", + " Print the metric recorded in the env tracker at the end of an evaluation episode. The parameter \"trackers\"\n", + " is actually a list because in a distributed setting, the main thread usually receives trackers from multiple\n", + " roll-out instances.\n", + " \"\"\"\n", + " print(f\"env summary (evaluation episode {ep}): {trackers[0]['env_metric']}\")\n", "\n", "\n", "set_seeds(1024) # for reproducibility\n", - "simple_learner(get_env_sampler, num_episodes=50, post_collect=post_collect, post_evaluate=post_evaluate)" + "learn(\n", + " get_env_sampler, num_episodes=50, eval_after_last_episode=True,\n", + " post_collect=post_collect, post_evaluate=post_evaluate\n", + ")" ] }, { From 0b5fcd18672804d4b892c66660cc60094745ebeb Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 22 Sep 2021 15:36:38 +0000 Subject: [PATCH 447/482] lint issue fix --- maro/rl/learning/policy_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index bc5ad4c47..a8f53d02e 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -16,7 +16,7 @@ from maro.utils import Logger # default group name for the cluster consisting of a policy manager and all policy hosts. -# If data parallelism is enabled, the gradient workers will also belong in this group. +# If data parallelism is enabled, the gradient workers will also belong in this group. DEFAULT_POLICY_GROUP = "policy_group_default" @@ -355,7 +355,7 @@ class DistributedPolicyManager(AbsPolicyManager): is provided, the gradient workers will also belong to the same cluster. Defaults to DEFAULT_POLICY_GROUP. worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. - If not None, the policies will be trained in data-parallel mode. Defaults to None. + If not None, the policies will be trained in data-parallel mode. Defaults to None. proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. Defaults to an empty dictionary. log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init From 190802fa40ff02e6030a2ed0e0d843524b31f8cd Mon Sep 17 00:00:00 2001 From: yaqiu Date: Wed, 22 Sep 2021 16:21:11 +0000 Subject: [PATCH 448/482] 1. added algorithm type setting in CIM config; 2. added try-except clause for initial policy state loading --- examples/rl/cim/config.py | 4 ++++ examples/rl/cim/env_sampler.py | 5 +++-- examples/rl/cim/policies.py | 15 ++++++++++----- maro/rl/learning/policy_manager.py | 20 ++++++++++++++++---- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py index f0a726a88..f02b6991a 100644 --- a/examples/rl/cim/config.py +++ b/examples/rl/cim/config.py @@ -40,6 +40,10 @@ + len(vessel_attributes) ) +############################################## POLICIES ############################################### + +algorithm = "ac" + # DQN settings q_net_conf = { "input_dim": state_dim, diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 1258286af..9eff7c852 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -16,7 +16,8 @@ from callbacks import post_step from config import ( - action_shaping_conf, env_conf, port_attributes, reward_shaping_conf, state_shaping_conf, vessel_attributes + action_shaping_conf, algorithm, env_conf, port_attributes, reward_shaping_conf, state_shaping_conf, + vessel_attributes ) from policies import policy_func_dict @@ -96,7 +97,7 @@ def get_reward(self, actions, tick): return {agent_id: reward for agent_id, reward in zip(ports, rewards)} -agent2policy = {agent: f"ac.{agent}" for agent in Env(**env_conf).agent_idx_list} +agent2policy = {agent: f"{algorithm}.{agent}" for agent in Env(**env_conf).agent_idx_list} def get_env_sampler(): return CIMEnvSampler( diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py index faa82c74e..3bcacf88f 100644 --- a/examples/rl/cim/policies.py +++ b/examples/rl/cim/policies.py @@ -11,12 +11,12 @@ if cim_path not in sys.path: sys.path.insert(0, cim_path) from config import ( - ac_conf, actor_net_conf, actor_optim_conf, critic_net_conf, critic_optim_conf, dqn_conf, q_net_conf, + ac_conf, actor_net_conf, actor_optim_conf, algorithm, critic_net_conf, critic_optim_conf, dqn_conf, q_net_conf, q_net_optim_conf, state_dim ) -class QNet(DiscreteQNet): +class MyQNet(DiscreteQNet): def __init__(self): super().__init__() self.fc = FullyConnected(**q_net_conf) @@ -109,6 +109,11 @@ def set_state(self, state): self.critic_optim.load_state_dict(state["critic_optim"]) -policy_func_dict = { - f"ac.{i}": lambda name: ActorCritic(name, MyACNet(), **ac_conf) for i in range(4) -} +if algorithm == "dqn": + policy_func_dict = { + f"dqn.{i}": lambda name: DQN(name, MyQNet(), **dqn_conf) for i in range(4) + } +else: + policy_func_dict = { + f"ac.{i}": lambda name: ActorCritic(name, MyACNet(), **ac_conf) for i in range(4) + } diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index a8f53d02e..00a458488 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -129,8 +129,14 @@ def __init__( self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} for id_, path in load_path_dict.items(): - self._policy_dict[id_].load(path) - self._logger.info(f"Loaded policy {id_} from {path}") + try: + self._policy_dict[id_].load(path) + self._logger.info(f"Loaded policy {id_} from {path}") + except FileNotFoundError: + self._logger.warn( + f"Failed to load state for policy {id_} from path {path}..." + f"using the current policy state as the initial state" + ) self._version = defaultdict(int) @@ -261,8 +267,14 @@ def policy_host(id_, create_policy_func, conn, initial_state_path): **proxy_kwargs) if initial_state_path: - policy.load(initial_state_path) - self._logger.info(f"Loaded policy {id_} from {initial_state_path}") + try: + policy.load(initial_state_path) + self._logger.info(f"Loaded policy {id_} from {initial_state_path}") + except FileNotFoundError: + self._logger.warn( + f"Failed to load state for policy {id_} from path {initial_state_path}..." + f"using the current policy state as the initial state" + ) conn.send({"type": "init", "policy_state": policy.get_state()}) while True: msg = conn.recv() From 6e941a450c303bb9d2abef54e9b67a0df08221af Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 24 Sep 2021 11:35:34 +0000 Subject: [PATCH 449/482] 1. moved post_step callback inside env sampler; 2. updated README for rl workflows --- docs/source/apidoc/maro.rl.rst | 88 +++++++++++++++++++ examples/rl/README.md | 6 +- examples/rl/cim/README.md | 8 +- examples/rl/cim/callbacks.py | 4 - examples/rl/cim/env_sampler.py | 10 ++- examples/rl/workflows/config.yml | 2 +- maro/rl/learning/env_sampler.py | 26 ++---- .../rl_formulation.ipynb | 24 +++-- 8 files changed, 122 insertions(+), 46 deletions(-) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index c0d9f19e2..0d7e0b831 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -45,3 +45,91 @@ maro.rl.modeling.specials :members: :undoc-members: :show-inheritance: + + +Learning +================================================================================ + +maro.rl.learning.env_sampler +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.learning.env_sampler + :members: + :undoc-members: + :show-inheritance: + +maro.rl.learning.learning_loop +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.learning.learning_loop + :members: + :undoc-members: + :show-inheritance: + +maro.rl.learning.policy_manager +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.learning.policy_manager + :members: + :undoc-members: + :show-inheritance: + +maro.rl.learning.rollout_manager +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.learning.rollout_manager + :members: + :undoc-members: + :show-inheritance: + + +Policy +================================================================================ + +maro.rl.policy.ac +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.ac + :members: + :undoc-members: + :show-inheritance: + +maro.rl.policy.ddpg +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.ddpg + :members: + :undoc-members: + :show-inheritance: + +maro.rl.policy.dqn +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.dqn + :members: + :undoc-members: + :show-inheritance: + +maro.rl.policy.pg +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.pg + :members: + :undoc-members: + :show-inheritance: + +maro.rl.policy.policy +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.policy + :members: + :undoc-members: + :show-inheritance: + +maro.rl.policy.replay +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.replay + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/rl/README.md b/examples/rl/README.md index 3bdbca4aa..c90605908 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -8,9 +8,9 @@ Scripts to run the common workflow in docker containers are in ``scripts/docker` ## Create Your Own Scenarios -The workflow scripts make it easy to create your own scenarios by only supplying the necessary ingredients without worrying about putting them together. The ingredients include: +The workflow scripts make it easy to create your own scenarios by only supplying the necessary ingredients without worrying about putting them together. It is necessary to create an ``__init__.py`` under your scenario folder (so that it can be treated as a package) and expose all ingredients in it. The ingredients include: * Definitions of state, action and reward shaping logic pertinent to your simulator and policies. These definitions should be encapsulated in ``get_env_sampler``, which is a function that takes no parameters and returns an environment sampler; -* Definitions of policies and agent-to-policy mappings. These definitions should be provided as a dictionary named ``policy_func_index`` that maps policy names to functions that create them. A policy creating function takes a name and return a policy instance with that name; +* Definitions of policies and agent-to-policy mappings. These definitions should be provided as a dictionary named ``policy_func_index`` that maps the name of each policy to a function that creates a policy instance with that name (the policy name should be the function's only parameter). The agent-to-policy mapping should be provided as a dictionary named ``agent2policy``. -Optionally, if you want to +It is possible to have customized routines invoked at the end of a roll-out episode or episode segment. These routines usually involve processing or rendering information collected during roll-out. To do this, first implement the ``post_step`` method in your environment sampler class and populate the ``tracker`` member with whatever information you wish to track during roll-out. Then create two functions, ``post_collect`` and ``post_evaluate``, to process the information contained in each ``tracker`` and expose them in the scenario folder's ``__init__.py``. These functions are used as callbacks in the main learning loop and executed at the end of each training or evaluation episode. See ``cim/callbacks.py`` for a simple example of how to create these functions. diff --git a/examples/rl/cim/README.md b/examples/rl/cim/README.md index 2e17ac435..eedb78b6e 100644 --- a/examples/rl/cim/README.md +++ b/examples/rl/cim/README.md @@ -1,9 +1,9 @@ # Container Inventory Management This example demonstrates the use of MARO's RL toolkit to optimize container inventory management. The scenario consists of a set of ports, each acting as a learning agent, and vessels that transfer empty containers among them. Each port must decide 1) whether to load or discharge containers when a vessel arrives and 2) how many containers to be loaded or discharged. The objective is to minimize the overall container shortage over a certain period of time. In this folder you can find: -* ``config.py``, which contains environment and policy configurations for the scenario. -* ``env_sampler.py``, which contains definitions of state, action and reward shaping. -* ``policies.py``, which contains definitions of DQN and Actor-Critic. -* ``callbacks.py``, which contains processing logic to be executed at the step and episode levels. +* ``config.py``, which contains environment and policy configurations for the scenario; +* ``env_sampler.py``, which defines state, action and reward shaping in the ``CIMEnvSampler`` class; +* ``policies.py``, which defines the Q-net for DQN and the actor and critic network components for Actor-Critic; +* ``callbacks.py``, which contains processing logic to be executed at step and episode levels. The scripts for running the learning workflows can be found under ``examples/rl/workflows``. See ``README`` under ``examples/rl`` for details about the general applicability of these scripts. We recommend that you follow this example to write your own scenarios. \ No newline at end of file diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py index 9f9839ccb..a5d6d1edb 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/rl/cim/callbacks.py @@ -9,10 +9,6 @@ makedirs(log_dir, exist_ok=True) -def post_step(env, tracker, state, action, env_action, reward, tick): - tracker["env_metric"] = env.metrics - - def post_collect(trackers, ep, segment): # print the env metric from each rollout worker for tracker in trackers: diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 9eff7c852..80d951543 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -14,7 +14,6 @@ if cim_path not in sys.path: sys.path.insert(0, cim_path) -from callbacks import post_step from config import ( action_shaping_conf, algorithm, env_conf, port_attributes, reward_shaping_conf, state_shaping_conf, vessel_attributes @@ -96,6 +95,14 @@ def get_reward(self, actions, tick): ) return {agent_id: reward for agent_id, reward in zip(ports, rewards)} + def post_step(self, state, action, env_action, reward, tick): + """ + The environment sampler contains a "tracker" dict inherited from the "AbsEnvSampler" base class, which can + be used to record any information one wishes to keep track of during a roll-out episode. Here we simply record + the latest env metric without keeping the history for logging purposes. + """ + self.tracker["env_metric"] = self.env.metrics + agent2policy = {agent: f"{algorithm}.{agent}" for agent in Env(**env_conf).agent_idx_list} @@ -105,6 +112,5 @@ def get_env_sampler(): get_policy_func_dict=policy_func_dict, agent2policy=agent2policy, reward_eval_delay=reward_shaping_conf["time_window"], - post_step=post_step, policies_to_parallelize=[] ) diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 972e2b687..29839088b 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -20,7 +20,7 @@ sync: async: num_rollouts: 3 policy_manager: - type: multi-process # simple, multi-process, distributed + type: distributed # simple, multi-process, distributed distributed: num_hosts: 2 data_parallel: diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index d6b7e18c8..b6d532d1f 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -195,11 +195,6 @@ class AbsEnvSampler(ABC): reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. - post_step (Callable): Custom function to gather information about a transition and the evolvement of the - environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` - instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition - is a ``Transition`` object. For example, this callback can be used to collect various statistics on the - simulation. Defaults to None. policies_to_parallelize (List[str]): Policies to be placed in separate processes so that inference can be performed in parallel to speed up simulation. This is useful if some policies are big and takes long times to compute actions. Defaults to an empty list. @@ -211,7 +206,6 @@ def __init__( agent2policy: Dict[str, str], get_test_env: Callable[[], Env] = None, reward_eval_delay: int = 0, - post_step: Callable = None, policies_to_parallelize: List[str] = [] ): self._learn_env = get_env() @@ -223,8 +217,6 @@ def __init__( ) self.reward_eval_delay = reward_eval_delay - self._post_step = post_step - self._state = None self._event = None self._step_index = 0 @@ -265,12 +257,7 @@ def get_reward(self, actions: list, tick: int): """ raise NotImplementedError - def sample( - self, - policy_state_dict: dict = None, - num_steps: int = -1, - return_rollout_info: bool = True - ): + def sample(self, policy_state_dict: dict = None, num_steps: int = -1, return_rollout_info: bool = True): self.env = self._learn_env if not self._state: # reset and get initial state @@ -306,10 +293,7 @@ def sample( while cache and (not self._state or self.env.tick - cache[0][-1] >= self.reward_eval_delay): state, action, env_actions, tick = cache.popleft() reward = self.get_reward(env_actions, tick) - if self._post_step: - # put things you want to track in the tracker attribute - self._post_step(self.env, self.tracker, state, action, env_actions, reward, tick) - + self.post_step(state, action, env_actions, reward, tick) self.agent_wrapper.record_transition( agent, state, action, reward[agent], cache[0][0] if cache else self._state, not cache and not self._state @@ -351,6 +335,12 @@ def test(self, policy_state_dict: dict = None): return self.tracker + def post_step(self, state, action, env_actions, reward, tick): + """ + Gather any information you wish to track during a roll-out episode and store it in the ``tracker`` attribute. + """ + pass + def worker( self, group: str, diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 05fb08980..41398fde4 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -137,7 +137,15 @@ " reward_shaping_conf[\"fulfillment_factor\"] * np.dot(future_fulfillment.T, decay_list)\n", " - reward_shaping_conf[\"shortage_factor\"] * np.dot(future_shortage.T, decay_list)\n", " )\n", - " return {agent_id: reward for agent_id, reward in zip(ports, rewards)}" + " return {agent_id: reward for agent_id, reward in zip(ports, rewards)}\n", + "\n", + " def post_step(self, state, action, env_action, reward, tick):\n", + " \"\"\"\n", + " The environment sampler contains a \"tracker\" dict inherited from the \"AbsEnvSampler\" base class, which can\n", + " be used to record any information one wishes to keep track of during a roll-out episode. Here we simply\n", + " record the latest env metric without keeping the history for logging purposes.\n", + " \"\"\"\n", + " self.tracker[\"env_metric\"] = self.env.metrics" ] }, { @@ -269,24 +277,12 @@ "from maro.rl.learning import learn\n", "from maro.utils import set_seeds\n", "\n", - "\"\"\"\n", - "The environment sampler contains a \"tracker\" dict inherited from the \"AbsEnvSampler\" base class, which can be\n", - "used to record any information one wishes to keep track of during a roll-out episode throught the \"post_step\"\n", - "callback function.\n", - "\"\"\"\n", - "def post_step(env, tracker, state, action, env_action, reward, tick):\n", - " \"\"\"\n", - " Here we simply record the latest env metric without keeping the history for logging purposes.\n", - " \"\"\"\n", - " tracker[\"env_metric\"] = env.metrics\n", - "\n", "def get_env_sampler():\n", " return CIMEnvSampler(\n", " get_env=lambda: Env(**env_conf),\n", " get_policy_func_dict=policy_func_dict,\n", " agent2policy={agent: f\"ac.{agent}\" for agent in Env(**env_conf).agent_idx_list},\n", - " reward_eval_delay=reward_shaping_conf[\"time_window\"],\n", - " post_step=post_step\n", + " reward_eval_delay=reward_shaping_conf[\"time_window\"]\n", " )\n", "\n", "# post-episode callback, executed at the end of an episode or episode segment.\n", From 1edd4c49b9e03fe18a44deb47c67f1287044b4f5 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Fri, 24 Sep 2021 12:56:09 +0000 Subject: [PATCH 450/482] refined READEME for CIM --- examples/rl/cim/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/rl/cim/README.md b/examples/rl/cim/README.md index eedb78b6e..5113acd18 100644 --- a/examples/rl/cim/README.md +++ b/examples/rl/cim/README.md @@ -3,7 +3,7 @@ This example demonstrates the use of MARO's RL toolkit to optimize container inventory management. The scenario consists of a set of ports, each acting as a learning agent, and vessels that transfer empty containers among them. Each port must decide 1) whether to load or discharge containers when a vessel arrives and 2) how many containers to be loaded or discharged. The objective is to minimize the overall container shortage over a certain period of time. In this folder you can find: * ``config.py``, which contains environment and policy configurations for the scenario; * ``env_sampler.py``, which defines state, action and reward shaping in the ``CIMEnvSampler`` class; -* ``policies.py``, which defines the Q-net for DQN and the actor and critic network components for Actor-Critic; -* ``callbacks.py``, which contains processing logic to be executed at step and episode levels. +* ``policies.py``, which defines the Q-net for DQN and the network components for Actor-Critic; +* ``callbacks.py``, which defines routines to be invoked at the end of training or evaluation episodes. The scripts for running the learning workflows can be found under ``examples/rl/workflows``. See ``README`` under ``examples/rl`` for details about the general applicability of these scripts. We recommend that you follow this example to write your own scenarios. \ No newline at end of file From 2b4d4eb39cde99e6c94bbb2d8f2d68039b335f6d Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 26 Sep 2021 14:57:49 +0800 Subject: [PATCH 451/482] VM scheduling with RL (#375) * added part of vm scheduling RL code * refined vm env_wrapper code style * added DQN * added get_experiences func for ac in vm scheduling * added post_step callback to env wrapper * moved Aiming's tracking and plotting logic into callbacks * added eval env wrapper * renamed AC config variable name for VM * vm scheduling RL code finished * updated README * fixed various bugs and hard coding for vm_scheduling * uncommented callbacks for VM scheduling * Minor revision for better code style * added part of vm scheduling RL code * refined vm env_wrapper code style * vm scheduling RL code finished * added config.py for vm scheduing * vm example refactoring * fixed bugs in vm_scheduling * removed unwanted files from cim dir * reverted to simple policy manager as default * added part of vm scheduling RL code * refined vm env_wrapper code style * vm scheduling RL code finished * added config.py for vm scheduing * resolved rebase conflicts * fixed bugs in vm_scheduling * added get_state and set_state to vm_scheduling policy models * updated README for vm_scheduling with RL Co-authored-by: yaqiu Co-authored-by: Huoran Li --- docker_files/dev.df | 1 + examples/rl/vm_scheduling/README.md | 30 +++- examples/rl/vm_scheduling/__init__.py | 19 +-- examples/rl/vm_scheduling/ac.py | 103 ------------ examples/rl/vm_scheduling/callbacks.py | 21 +-- examples/rl/vm_scheduling/config.py | 126 ++++++++++++++ examples/rl/vm_scheduling/dqn.py | 124 -------------- examples/rl/vm_scheduling/env_sampler.py | 137 ++++++++++++++++ examples/rl/vm_scheduling/env_wrapper.py | 191 ---------------------- examples/rl/vm_scheduling/policies.py | 118 +++++++++++++ examples/rl/vm_scheduling/policy_index.py | 19 --- examples/rl/workflows/config.yml | 2 +- 12 files changed, 421 insertions(+), 470 deletions(-) delete mode 100644 examples/rl/vm_scheduling/ac.py create mode 100644 examples/rl/vm_scheduling/config.py delete mode 100644 examples/rl/vm_scheduling/dqn.py create mode 100644 examples/rl/vm_scheduling/env_sampler.py delete mode 100644 examples/rl/vm_scheduling/env_wrapper.py create mode 100644 examples/rl/vm_scheduling/policies.py delete mode 100644 examples/rl/vm_scheduling/policy_index.py diff --git a/docker_files/dev.df b/docker_files/dev.df index f2f126eb5..4b08d2671 100644 --- a/docker_files/dev.df +++ b/docker_files/dev.df @@ -20,6 +20,7 @@ RUN pip install --no-cache-dir pyzmq==19.0.2 RUN pip install --no-cache-dir numpy==1.19.1 RUN pip install --no-cache-dir torch==1.6.0 RUN pip install --no-cache-dir scipy +RUN pip install --no-cache-dir matplotlib RUN pip install --no-cache-dir redis COPY maro /maro/maro diff --git a/examples/rl/vm_scheduling/README.md b/examples/rl/vm_scheduling/README.md index a428a5c33..8d6350873 100644 --- a/examples/rl/vm_scheduling/README.md +++ b/examples/rl/vm_scheduling/README.md @@ -1,10 +1,24 @@ # Virtual Machine Scheduling -Virtual Machine (VM) scheduling is a scenario where reinforcement learning (RL) can help the virtual machine allocator allocate compute resources intelligently. In this folder you can find: -* ``env_wrapper.py``, which contains a function to generate an environment wrapper to interact -with our "agent" (see below); -* ``agent_wrapper.py``, which contains a function to generate an agent wrapper to interact -with the environment wrapper; -* ``policy_index``, which maps policy names to functions that create them; the functions to create DQN and Actor-Critic policies are defined in ``dqn.py`` and ``ac.py``, respectively. - -The code for the actual learning workflows (e.g., learner, roll-out worker and trainer) can be found under ``examples/rl/workflows``. The reason for putting it in a separate folder is that these workflows apply to any scenario, so long as the necessary component generators, such as the ones listed above, are provided. See ``README`` under ``examples/rl`` for details. We recommend that you follow this example to write your own scenarios. \ No newline at end of file +A virtual machine (VM) scheduler is a cloud computing service component responsible for providing compute resources to satisfy user demands. A good resource allocation policy should aim to optimize several metrics at the same time, such as user wait time, profit, energy consumption and physical machine (PM) overload. Many commercial cloud providers use rule-based policies. Alternatively, the policy can also be optimized using reinforcement learning (RL) techniques, which involves simulating with historical data. This example demonstrates how DQN and Actor-Critic algorithms can be applied to this scenario. In this folder, you can find: + +* ``config.py``, which contains environment and policy configurations. +* ``env_sampler.py``, which defines state, action and reward shaping in the ``VMEnvSampler`` class; +* ``policies.py``, which defines the Q-net for DQN and the network components for Actor-Critic. +* ``callbacks.py``, which contains routines to be invoked at the end of a training or evaluation episode. + +The scripts to run the learning workflows can be found under ``examples/rl/workflows``. See ``README`` under ``examples/rl`` for details about the general applicability of these scripts. We recommend that you follow this example to write your own scenarios. + + +# Some Comments About the Results + +This example is meant to serve as a demonstration of using MARO's RL toolkit in a real-life scenario. In fact, we have yet to find a configuration that makes the policy learned by either DQN or Actor-Critic perform reasonably well in our experimental settings. + +For reference, the best results have been achieved by the ``Best Fit`` algorithm (see ``examples/vm_scheduling/rule_based_algorithm/best_fit.py`` for details). The over-subscription rate is 115% in the over-subscription settings. + +|Topology | PM Setting | Time Spent(s) | Total VM Requests |Successful Allocation| Energy Consumption| Total Oversubscriptions | Total Overload PMs +|:----:|-----|:--------:|:---:|:-------:|:----:|:---:|:---:| +|10k| 100 PMs, 32 Cores, 128 GB | 104.98|10,000| 10,000| 2,399,610 | 0 | 0| +|10k.oversubscription| 100 PMs, 32 Cores, 128 GB| 101.00 |10,000 |10,000| 2,386,371| 279,331 | 0| +|336k| 880 PMs, 16 Cores, 112 GB | 7,896.37 |335,985| 109,249 |26,425,878 | 0 | 0 | +|336k.oversubscription| 880 PMs, 16 Cores, 112 GB | 7,903.33| 335,985| 115,008 | 27,440,946 | 3,868,475 | 0 diff --git a/examples/rl/vm_scheduling/__init__.py b/examples/rl/vm_scheduling/__init__.py index 44af25424..c3e93fabd 100644 --- a/examples/rl/vm_scheduling/__init__.py +++ b/examples/rl/vm_scheduling/__init__.py @@ -1,11 +1,8 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .callbacks import post_collect, post_evaluate -from .env_wrapper import get_env_sampler, get_test_env_wrapper -from .policy_index import agent2policy, rl_policy_func_index, update_trigger, warmup - -__all__ = [ - "agent2policy", "post_collect", "post_evaluate", "get_env_sampler", "get_test_env_wrapper", - "rl_policy_func_index", "update_trigger", "warmup" -] \ No newline at end of file +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .callbacks import post_collect, post_evaluate +from .env_sampler import agent2policy, get_env_sampler +from .policies import policy_func_dict + +__all__ = ["agent2policy", "post_collect", "post_evaluate", "get_env_sampler", "policy_func_dict"] diff --git a/examples/rl/vm_scheduling/ac.py b/examples/rl/vm_scheduling/ac.py deleted file mode 100644 index 34eacf69d..000000000 --- a/examples/rl/vm_scheduling/ac.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -import numpy as np -import torch - -from maro.rl.experience import ReplayMemory, UniformSampler -from maro.rl.model import DiscreteACNet, FullyConnected, OptimOption -from maro.rl.policy.algorithms import ActorCritic, ActorCriticConfig - -vm_path = os.path.dirname(os.path.realpath(__file__)) -sys.path.insert(0, vm_path) -from env_wrapper import NUM_PMS, STATE_DIM - -config = { - "model": { - "network": { - "actor": { - "input_dim": STATE_DIM, - "output_dim": NUM_PMS + 1, # action could be any PM or postponement, hence the plus 1 - "hidden_dims": [64, 32, 32], - "activation": "leaky_relu", - "softmax": True, - "batch_norm": False, - "head": True - }, - "critic": { - "input_dim": STATE_DIM, - "output_dim": 1, - "hidden_dims": [256, 128, 64], - "activation": "leaky_relu", - "softmax": False, - "batch_norm": False, - "head": True - } - }, - "optimization": { - "actor": { - "optim_cls": "adam", - "optim_params": {"lr": 0.0001} - }, - "critic": { - "optim_cls": "sgd", - "optim_params": {"lr": 0.001} - } - } - }, - "algorithm": { - "reward_discount": 0.9, - "train_epochs": 100, - "critic_loss_cls": "mse", - "critic_loss_coeff": 0.1 - }, - "replay_memory": { - "rollout": {"capacity": 10000, "overwrite_type": "rolling"}, - "update": {"capacity": 50000, "overwrite_type": "rolling"} - }, - "sampler": { - "rollout": {"batch_size": -1, "replace": False}, - "update": {"batch_size": 128, "replace": True} - } -} - - -def get_ac_policy(mode="update"): - class MyACNet(DiscreteACNet): - def forward(self, states, actor: bool = True, critic: bool = True): - if isinstance(states, dict): - states = [states] - inputs = torch.from_numpy(np.asarray([st["model"] for st in states])).to(self.device) - masks = torch.from_numpy(np.asarray([st["mask"] for st in states])).to(self.device) - if len(inputs.shape) == 1: - inputs = inputs.unsqueeze(dim=0) - return ( - self.component["actor"](inputs) * masks if actor else None, - self.component["critic"](inputs) if critic else None - ) - - ac_net = MyACNet( - component={ - "actor": FullyConnected(**config["model"]["network"]["actor"]), - "critic": FullyConnected(**config["model"]["network"]["critic"]) - }, - optim_option={ - "actor": OptimOption(**config["model"]["optimization"]["actor"]), - "critic": OptimOption(**config["model"]["optimization"]["critic"]) - } if mode != "inference" else None - ) - if mode == "update": - exp_store = ReplayMemory(**config["replay_memory"]["update"]) - exp_sampler_kwargs = config["sampler"]["update"] - else: - exp_store = ReplayMemory(**config["replay_memory"]["rollout" if mode == "inference" else "update"]) - exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - - return ActorCritic( - ac_net, ActorCriticConfig(**config["algorithm"]), exp_store, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs=exp_sampler_kwargs - ) diff --git a/examples/rl/vm_scheduling/callbacks.py b/examples/rl/vm_scheduling/callbacks.py index f82291374..240c735b0 100644 --- a/examples/rl/vm_scheduling/callbacks.py +++ b/examples/rl/vm_scheduling/callbacks.py @@ -5,45 +5,40 @@ from os import makedirs from os.path import dirname, join, realpath -import matplotlib.pyplot as plt - -from maro.utils import Logger +from matplotlib import pyplot as plt timestamp = str(time.time()) - log_dir = join(dirname(realpath(__file__)), "log", timestamp) makedirs(log_dir, exist_ok=True) - plt_path = join(dirname(realpath(__file__)), "plots", timestamp) makedirs(plt_path, exist_ok=True) -simulation_logger = Logger("SIMUALTION", dump_folder=log_dir) - def post_collect(trackers, ep, segment): # print the env metric from each rollout worker for tracker in trackers: - simulation_logger.info(f"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}") + print(f"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}") # print the average env metric if len(trackers) > 1: metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} - simulation_logger.info(f"average env metric (episode {ep}, segment {segment}): {avg_metric}") + print(f"average env metric (episode {ep}, segment {segment}): {avg_metric}") + def post_evaluate(trackers, ep): # print the env metric from each rollout worker for tracker in trackers: - simulation_logger.info(f"env summary (evaluation episode {ep}): {tracker['env_metric']}") + print(f"env summary (evaluation episode {ep}): {tracker['env_metric']}") # print the average env metric if len(trackers) > 1: metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} - simulation_logger.info(f"average env metric (evaluation episode {ep}): {avg_metric}") + print(f"average env metric (evaluation episode {ep}): {avg_metric}") - for i, tracker in enumerate(trackers): - core_requirement = tracker["vm_core_requirement"] + for tracker in trackers: + core_requirement = tracker["actions_by_core_requirement"] action_sequence = tracker["action_sequence"] # plot action sequence fig = plt.figure(figsize=(40, 32)) diff --git a/examples/rl/vm_scheduling/config.py b/examples/rl/vm_scheduling/config.py new file mode 100644 index 000000000..90be7b7c6 --- /dev/null +++ b/examples/rl/vm_scheduling/config.py @@ -0,0 +1,126 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import numpy as np +import torch +from torch.optim import Adam, SGD, lr_scheduler + +from maro.rl.exploration import MultiLinearExplorationScheduler +from maro.simulator import Env + + +env_conf = { + "scenario": "vm_scheduling", + "topology": "azure.2019.10k", + "start_tick": 0, + "durations": 300, # 8638 + "snapshot_resolution": 1 +} + +num_pms = Env(**env_conf).business_engine._pm_amount +pm_window_size = 1 +num_features = 2 * num_pms * pm_window_size + 4 + +pm_attributes = ["cpu_cores_capacity", "memory_capacity", "cpu_cores_allocated", "memory_allocated"] +# vm_attributes = ["cpu_cores_requirement", "memory_requirement", "lifetime", "remain_time", "total_income"] + + +reward_shaping_conf = { + "alpha": 0.0, + "beta": 1.0 +} +seed = 666 + +test_env_conf = { + "scenario": "vm_scheduling", + "topology": "azure.2019.10k.oversubscription", + "start_tick": 0, + "durations": 300, + "snapshot_resolution": 1 +} +test_reward_shaping_conf = { + "alpha": 0.0, + "beta": 1.0 +} + +test_seed = 1024 + +algorithm = "ac" # "dqn" or "ac" + +######################################### A2C settings ######################################## +actor_net_conf = { + "input_dim": num_features, + "output_dim": num_pms + 1, # action could be any PM or postponement, hence the plus 1 + "hidden_dims": [64, 32, 32], + "activation": torch.nn.LeakyReLU, + "softmax": True, + "batch_norm": False, + "head": True +} + +critic_net_conf = { + "input_dim": num_features, + "output_dim": 1, + "hidden_dims": [256, 128, 64], + "activation": torch.nn.LeakyReLU, + "softmax": False, + "batch_norm": False, + "head": True +} + +actor_optim_conf = (Adam, {"lr": 0.0001}) +critic_optim_conf = (SGD, {"lr": 0.001}) + +ac_conf = { + "reward_discount": 0.9, + "grad_iters": 100, + "critic_loss_cls": torch.nn.MSELoss, + "critic_loss_coeff": 0.1, + "min_logp": -20, + "max_trajectory_len": 10000, + "get_loss_on_rollout": False +} + +######################################### DQN settings ######################################## +q_net_conf = { + "input_dim": num_features, + "hidden_dims": [64, 128, 256], + "output_dim": num_pms + 1, # action could be any PM or postponement, hence the plus 1 + "activation": torch.nn.LeakyReLU, + "softmax": False, + "batch_norm": False, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 +} + +q_net_optim_conf = (SGD, {"lr": 0.0005}) +q_net_lr_scheduler_conf = (lr_scheduler.CosineAnnealingWarmRestarts, {"T_0": 500, "T_mult": 2}) + + +def masked_eps_greedy(states, actions, num_actions, *, epsilon): + masks = states[:, num_features:] + return np.array([ + action if np.random.random() > epsilon else np.random.choice(np.where(mask == 1)[0]) + for action, mask in zip(actions, masks) + ]) + +dqn_conf = { + "reward_discount": 0.9, + "update_target_every": 5, + "num_epochs": 100, + "soft_update_coeff": 0.1, + "double": False, + "exploration_strategy": (masked_eps_greedy, {"epsilon": 0.4}), + "exploration_scheduling_options": [( + "epsilon", MultiLinearExplorationScheduler, { + "splits": [(100, 0.32)], + "initial_value": 0.4, + "last_ep": 400, + "final_value": 0.0, + } + )], + "replay_memory_capacity": 10000, + "rollout_batch_size": 2560, + "train_batch_size": 256, +} diff --git a/examples/rl/vm_scheduling/dqn.py b/examples/rl/vm_scheduling/dqn.py deleted file mode 100644 index 4f7499779..000000000 --- a/examples/rl/vm_scheduling/dqn.py +++ /dev/null @@ -1,124 +0,0 @@ - -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -import numpy as np -import torch - -from maro.rl.experience import ReplayMemory, UniformSampler -from maro.rl.exploration import DiscreteSpaceExploration, MultiLinearExplorationScheduler -from maro.rl.model import DiscreteQNet, FullyConnected, OptimOption -from maro.rl.policy.algorithms import DQN, DQNConfig - -vm_path = os.path.dirname(os.path.realpath(__file__)) -sys.path.insert(0, vm_path) -from env_wrapper import NUM_PMS, STATE_DIM - -config = { - "model": { - "network": { - "input_dim": STATE_DIM, - "hidden_dims": [64, 128, 256], - "output_dim": NUM_PMS + 1, # action could be any PM or postponement, hence the plus 1 - "activation": "leaky_relu", - "softmax": False, - "batch_norm": False, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": { - "optim_cls": "sgd", - "optim_params": {"lr": 0.0005}, - "scheduler_cls": "cosine_annealing_warm_restarts", - "scheduler_params": {"T_0": 500, "T_mult": 2} - } - }, - "algorithm": { - "reward_discount": 0.9, - "update_target_every": 5, - "train_epochs": 100, - "soft_update_coeff": 0.1, - "double": False - }, - "replay_memory": { - "rollout": {"capacity": 10000, "overwrite_type": "rolling"}, - "update": {"capacity": 50000, "overwrite_type": "rolling"} - }, - "sampler": { - "rollout": {"batch_size": -1, "replace": False}, - "update": {"batch_size": 256, "replace": True} - }, - "exploration": { - "last_ep": 400, - "initial_value": 0.4, - "final_value": 0.0, - "splits": [(100, 0.32)] - } -} - - -class MyQNet(DiscreteQNet): - def __init__(self, component, optim_option, device: str = None): - super().__init__(component, optim_option=optim_option, device=device) - for mdl in self.modules(): - if isinstance(mdl, torch.nn.Linear): - torch.nn.init.xavier_uniform_(mdl.weight, gain=torch.nn.init.calculate_gain('leaky_relu')) - - def forward(self, states): - if isinstance(states, dict): - states = [states] - inputs = torch.from_numpy(np.asarray([st["model"] for st in states])).to(self.device) - masks = torch.from_numpy(np.asarray([st["mask"] for st in states])).to(self.device) - if len(inputs.shape) == 1: - inputs = inputs.unsqueeze(dim=0) - q_for_all_actions = self.component(inputs) - return q_for_all_actions + (masks - 1) * 1e8 - - -class MaskedEpsilonGreedy(DiscreteSpaceExploration): - def __init__(self, epsilon: float = .0): - super().__init__() - self.epsilon = epsilon - - def __call__(self, action, state): - if isinstance(state, dict): - state = [state] - mask = [st["mask"] for st in state] - return np.array([ - act if np.random.random() > self.epsilon else np.random.choice(np.where(mk == 1)[0]) - for act, mk in zip(action, mask) - ]) - - -def get_dqn_policy(mode="update"): - assert mode in {"inference", "update", "inference-update"} - q_net = MyQNet( - FullyConnected(**config["model"]["network"]), - optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None - ) - - if mode == "update": - exp_store = ReplayMemory(**config["replay_memory"]["update"]) - exploration = None - exp_sampler_kwargs = config["sampler"]["update"] - else: - exp_store = ReplayMemory(**config["replay_memory"]["rollout"]) - exploration = MaskedEpsilonGreedy() - exploration.register_schedule( - scheduler_cls=MultiLinearExplorationScheduler, - param_name="epsilon", - **config["exploration"] - ) - exp_store = ReplayMemory(**config["replay_memory"]["rollout" if mode == "inference" else "update"]) - exp_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - - return DQN( - q_net, DQNConfig(**config["algorithm"]), exp_store, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs=exp_sampler_kwargs, - exploration=exploration - ) diff --git a/examples/rl/vm_scheduling/env_sampler.py b/examples/rl/vm_scheduling/env_sampler.py new file mode 100644 index 000000000..d9e0ff55f --- /dev/null +++ b/examples/rl/vm_scheduling/env_sampler.py @@ -0,0 +1,137 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import sys +from collections import defaultdict +from os.path import dirname, realpath + +import numpy as np + +from maro.rl.learning import AbsEnvSampler +from maro.simulator import Env +from maro.simulator.scenarios.vm_scheduling import AllocateAction, PostponeAction + +vm_path = dirname(realpath(__file__)) +sys.path.insert(0, vm_path) +from config import ( + algorithm, env_conf, pm_attributes, pm_window_size, reward_shaping_conf, num_features, seed, test_env_conf, + test_reward_shaping_conf, test_seed +) +from policies import policy_func_dict + + +class VMEnvSampler(AbsEnvSampler): + def __init__(self, get_env, get_policy_func_dict, agent2policy, get_test_env=None): + super().__init__(get_env, get_policy_func_dict, agent2policy, get_test_env=get_test_env) + self._learn_env.set_seed(seed) + self._test_env.set_seed(test_seed) + + # adjust the ratio of the success allocation and the total income when computing the reward + self.num_pms = self._learn_env.business_engine._pm_amount # the number of pms + self._durations = self._learn_env.business_engine._max_tick + self._pm_state_history = np.zeros((pm_window_size - 1, self.num_pms, 2)) + self._legal_pm_mask = None + + def get_state(self, tick=None): + pm_state, vm_state = self._get_pm_state(), self._get_vm_state() + # get the legal number of PM. + legal_pm_mask = np.zeros(self.num_pms + 1) + if len(self.event.valid_pms) <= 0: + # no pm available + legal_pm_mask[self.num_pms] = 1 + else: + legal_pm_mask[self.num_pms] = 1 + remain_cpu_dict = dict() + for pm in self.event.valid_pms: + # If two pms have the same remaining cpu, choose the one with the smaller id + if pm_state[-1, pm, 0] not in remain_cpu_dict: + remain_cpu_dict[pm_state[-1, pm, 0]] = 1 + legal_pm_mask[pm] = 1 + else: + legal_pm_mask[pm] = 0 + + self._legal_pm_mask = legal_pm_mask + return {"AGENT": np.concatenate((pm_state.flatten(), vm_state.flatten(), legal_pm_mask)).astype(np.float32)} + + def get_env_actions(self, action_info): + action_info = action_info["AGENT"] + model_action = action_info["action"] if isinstance(action_info, dict) else action_info + if model_action == self.num_pms: + return PostponeAction(vm_id=self.event.vm_id, postpone_step=1) + else: + return AllocateAction(vm_id=self.event.vm_id, pm_id=model_action) + + def get_reward(self, actions, tick): + conf = reward_shaping_conf if self.env == self._learn_env else test_reward_shaping_conf + if isinstance(actions, PostponeAction): # postponement + if np.sum(self._legal_pm_mask) != 1: + reward = -0.1 * conf["alpha"] + 0.0 * conf["beta"] + else: + reward = 0.0 * conf["alpha"] + 0.0 * conf["beta"] + elif self.event: + vm_unit_price = self.env.business_engine._get_unit_price( + self.event.vm_cpu_cores_requirement, self.event.vm_memory_requirement + ) + reward = ( + 1.0 * conf["alpha"] + conf["beta"] * vm_unit_price * + min(self._durations - self.event.frame_index, self.event.remaining_buffer_time) + ) + else: + reward = .0 + return {"AGENT": np.float32(reward)} + + def _get_pm_state(self): + total_pm_info = self.env.snapshot_list["pms"][self.env.frame_index::pm_attributes] + total_pm_info = total_pm_info.reshape(self.num_pms, len(pm_attributes)) + + # normalize the attributes of pms' cpu and memory + self._max_cpu_capacity = np.max(total_pm_info[:, 0]) + self._max_memory_capacity = np.max(total_pm_info[:, 1]) + total_pm_info[:, 2] /= self._max_cpu_capacity + total_pm_info[:, 3] /= self._max_memory_capacity + + # get the remaining cpu and memory of the pms + remain_cpu = (1 - total_pm_info[:, 2]).reshape(1, self.num_pms, 1) + remain_memory = (1 - total_pm_info[:, 3]).reshape(1, self.num_pms, 1) + + # get the pms' information + total_pm_info = np.concatenate((remain_cpu, remain_memory), axis=2) # (1, num_pms, 2) + + # get the sequence pms' information + self._pm_state_history = np.concatenate((self._pm_state_history, total_pm_info), axis=0) + return self._pm_state_history[-pm_window_size:, :, :] # (win_size, num_pms, 2) + + def _get_vm_state(self): + return np.array([ + self.event.vm_cpu_cores_requirement / self._max_cpu_capacity, + self.event.vm_memory_requirement / self._max_memory_capacity, + (self._durations - self.env.tick) * 1.0 / 200, # TODO: CHANGE 200 TO SOMETHING CONFIGURABLE + self.env.business_engine._get_unit_price( + self.event.vm_cpu_cores_requirement, self.event.vm_memory_requirement + ) + ]) + + def post_step(self, state, action, env_actions, reward, tick): + self.tracker["env_metric"] = {key: metric for key, metric in self.env.metrics.items() if key != "total_latency"} + self.tracker["env_metric"]["latency_due_to_agent"] = self.env.metrics["total_latency"].due_to_agent + self.tracker["env_metric"]["latency_due_to_resource"] = self.env.metrics["total_latency"].due_to_resource + if "actions_by_core_requirement" not in self.tracker: + self.tracker["actions_by_core_requirement"] = defaultdict(list) + if "action_sequence" not in self.tracker: + self.tracker["action_sequence"] = [] + + if self.event: + mask = state[num_features:] + self.tracker["actions_by_core_requirement"][self.event.vm_cpu_cores_requirement].append([action, mask]) + self.tracker["action_sequence"].append(action["action"] if isinstance(action, dict) else action) + + +agent2policy = {"AGENT": algorithm} + +def get_env_sampler(): + return VMEnvSampler( + get_env=lambda: Env(**env_conf), + get_policy_func_dict=policy_func_dict, + agent2policy=agent2policy, + get_test_env=lambda: Env(**test_env_conf) + ) diff --git a/examples/rl/vm_scheduling/env_wrapper.py b/examples/rl/vm_scheduling/env_wrapper.py deleted file mode 100644 index 0e2d714e7..000000000 --- a/examples/rl/vm_scheduling/env_wrapper.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import numpy as np - -from maro.rl.learning import AbsEnvWrapper, Transition -from maro.simulator import Env -from maro.simulator.scenarios.vm_scheduling import AllocateAction, PostponeAction - -def post_step(env: Env, tracker: dict, transition: Transition): - tracker["env_metric"] = env.metrics - if "vm_cpu_cores_requirement" not in tracker: - tracker["vm_core_requirement"] = [] - if "action_sequence" not in tracker: - tracker["action_sequence"] = [] - - tracker["vm_core_requirement"].append([transition.action["AGENT"], transition.state["AGENT"]["mask"]]) - tracker["action_sequence"].append(transition.action["AGENT"]) - - -class VMEnvWrapper(AbsEnvWrapper): - def __init__( - self, - env: Env, - pm_attributes: list, - vm_attributes: list, - alpha: float, - beta: float, - pm_window_size: int = 1, - gamma: float = 0.0, - reward_eval_delay: int = 0 - ): - super().__init__(env, reward_eval_delay=reward_eval_delay, replay_agent_ids=["AGENT"], post_step=post_step) - self._pm_attributes = pm_attributes - self._vm_attributes = vm_attributes - self._st = 0 - self._pm_window_size = pm_window_size - # adjust the ratio of the success allocation and the total income when computing the reward - self._alpha = alpha - self._beta = beta - self._gamma = gamma # reward discount - self._num_pms = self.env._business_engine._pm_amount # the number of pms - self._durations = self.env._business_engine._max_tick - self._pm_state_history = np.zeros((pm_window_size - 1, self._num_pms, 2)) - self._legal_pm_mask = None - self._state_dim = 2 * self._num_pms * pm_window_size + 4 - - @property - def state_dim(self): - return self._state_dim - - @property - def num_pms(self): - return self._num_pms - - def get_state(self, tick=None): - pm_state, vm_state = self._get_pm_state(), self._get_vm_state() - # get the legal number of PM. - legal_pm_mask = np.zeros(self._num_pms + 1) - if len(self._event.valid_pms) <= 0: - # no pm available - legal_pm_mask[self._num_pms] = 1 - else: - legal_pm_mask[self._num_pms] = 1 - remain_cpu_dict = dict() - for pm in self._event.valid_pms: - # if two pm has same remaining cpu, only choose the one which has smaller id - if pm_state[-1, pm, 0] not in remain_cpu_dict: - remain_cpu_dict[pm_state[-1, pm, 0]] = 1 - legal_pm_mask[pm] = 1 - else: - legal_pm_mask[pm] = 0 - - self._legal_pm_mask = legal_pm_mask - return {"AGENT": {"model": np.concatenate((pm_state.flatten(), vm_state.flatten())), "mask": legal_pm_mask}} - - def to_env_action(self, action_info): - action_info = action_info["AGENT"] - model_action = action_info[0] if isinstance(action_info, tuple) else action_info - if model_action == self._num_pms: - return PostponeAction(vm_id=self._event.vm_id, postpone_step=1) - else: - return AllocateAction(vm_id=self._event.vm_id, pm_id=model_action) - - def get_reward(self, actions, tick=None): - if isinstance(actions, PostponeAction): # postponement - if np.sum(self._legal_pm_mask) != 1: - reward = -0.1 * self._alpha + 0.0 * self._beta - else: - reward = 0.0 * self._alpha + 0.0 * self._beta - elif self._event: - vm_unit_price = self.env._business_engine._get_unit_price( - self._event.vm_cpu_cores_requirement, self._event.vm_memory_requirement - ) - reward = ( - 1.0 * self._alpha + self._beta * vm_unit_price * - min(self._durations - self._event.frame_index, self._event.remaining_buffer_time) - ) - else: - reward = .0 - return {"AGENT": np.float32(reward)} - - def _get_pm_state(self): - total_pm_info = self.env.snapshot_list["pms"][self.env.frame_index::self._pm_attributes] - total_pm_info = total_pm_info.reshape(self._num_pms, len(self._pm_attributes)) - - # normalize the attributes of pms' cpu and memory - self._max_cpu_capacity = np.max(total_pm_info[:, 0]) - self._max_memory_capacity = np.max(total_pm_info[:, 1]) - total_pm_info[:, 2] /= self._max_cpu_capacity - total_pm_info[:, 3] /= self._max_memory_capacity - - # get the remaining cpu and memory of the pms - remain_cpu = (1 - total_pm_info[:, 2]).reshape(1, self._num_pms, 1) - remain_memory = (1 - total_pm_info[:, 3]).reshape(1, self._num_pms, 1) - - # get the pms' information - total_pm_info = np.concatenate((remain_cpu, remain_memory), axis=2) # (1, num_pms, 2) - - # get the sequence pms' information - self._pm_state_history = np.concatenate((self._pm_state_history, total_pm_info), axis=0) - return self._pm_state_history[-self._pm_window_size:, :, :].astype(np.float32) # (win_size, num_pms, 2) - - def _get_vm_state(self): - vm_info = np.array([ - self._event.vm_cpu_cores_requirement / self._max_cpu_capacity, - self._event.vm_memory_requirement / self._max_memory_capacity, - (self._durations - self.env.tick) * 1.0 / 200, # TODO: CHANGE 200 TO SOMETHING CONFIGURABLE - self.env._business_engine._get_unit_price( - self._event.vm_cpu_cores_requirement, self._event.vm_memory_requirement - ) - ], dtype=np.float32) - return vm_info - - -env_config = { - "basic": { - "scenario": "vm_scheduling", - "topology": "azure.2019.10k", - "start_tick": 0, - "durations": 300, # 8638 - "snapshot_resolution": 1 - }, - "wrapper": { - "pm_attributes": ["cpu_cores_capacity", "memory_capacity", "cpu_cores_allocated", "memory_allocated"], - "vm_attributes": ["cpu_cores_requirement", "memory_requirement", "lifetime", "remain_time", "total_income"], - "alpha": 0.0, - "beta": 1.0, - "pm_window_size": 1, - "gamma": 0.9 - }, - "seed": 666 -} - - -test_env_config = { - "basic": { - "scenario": "vm_scheduling", - "topology": "azure.2019.10k.oversubscription", - "start_tick": 0, - "durations": 300, - "snapshot_resolution": 1 - }, - "wrapper": { - "pm_attributes": ["cpu_cores_capacity", "memory_capacity", "cpu_cores_allocated", "memory_allocated"], - "vm_attributes": ["cpu_cores_requirement", "memory_requirement", "lifetime", "remain_time", "total_income"], - "alpha": 0.0, - "beta": 1.0, - "pm_window_size": 1, - "gamma": 0.9 - }, - "seed": 1024 -} - - -def get_env_sampler(replay_agent_ids=None): - env = Env(**env_config["basic"]) - env.set_seed(env_config["seed"]) - return VMEnvWrapper(env, **env_config["wrapper"]) - - -def get_test_env_wrapper(): - test_env = Env(**test_env_config["basic"]) - test_env.set_seed(test_env_config["seed"]) - return VMEnvWrapper(test_env, **test_env_config["wrapper"]) - - -tmp_env_wrapper = get_env_sampler() -STATE_DIM = tmp_env_wrapper.state_dim -NUM_PMS = tmp_env_wrapper.num_pms -del tmp_env_wrapper diff --git a/examples/rl/vm_scheduling/policies.py b/examples/rl/vm_scheduling/policies.py new file mode 100644 index 000000000..7e0d6d267 --- /dev/null +++ b/examples/rl/vm_scheduling/policies.py @@ -0,0 +1,118 @@ + +import sys +from os.path import dirname, realpath + +import torch + +from maro.rl.modeling import DiscreteACNet, DiscreteQNet, FullyConnected +from maro.rl.policy import DQN, ActorCritic + +vm_path = dirname(realpath(__file__)) +sys.path.insert(0, vm_path) +from config import ( + ac_conf, actor_net_conf, actor_optim_conf, algorithm, critic_net_conf, critic_optim_conf, dqn_conf, q_net_conf, + num_features, num_pms, q_net_optim_conf +) + + +class MyQNet(DiscreteQNet): + def __init__(self): + super().__init__() + for mdl in self.modules(): + if isinstance(mdl, torch.nn.Linear): + torch.nn.init.xavier_uniform_(mdl.weight, gain=torch.nn.init.calculate_gain('leaky_relu')) + + self.fc = FullyConnected(**q_net_conf) + self.optim = q_net_optim_conf[0](self.fc.parameters(), **q_net_optim_conf[1]) + + @property + def input_dim(self): + return num_features + num_pms + 1 + + @property + def num_actions(self): + return q_net_conf["output_dim"] + + def forward(self, states): + masks = states[:, num_features:] + q_for_all_actions = self.fc(states[:, :num_features]) + return q_for_all_actions + (masks - 1) * 1e8 + + def step(self, loss): + self.optim.zero_grad() + loss.backward() + self.optim.step() + + def get_gradients(self, loss): + self.optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad): + for name, param in self.named_parameters(): + param.grad = grad[name] + + self.optim.step() + + def get_state(self): + return {"network": self.state_dict(), "optim": self.optim.state_dict()} + + def set_state(self, state): + self.load_state_dict(state["network"]) + self.optim.load_state_dict(state["optim"]) + + +class MyACNet(DiscreteACNet): + def __init__(self): + super().__init__() + self.actor = FullyConnected(**actor_net_conf) + self.critic = FullyConnected(**critic_net_conf) + self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1]) + self.critic_optim = critic_optim_conf[0](self.critic.parameters(), **critic_optim_conf[1]) + + @property + def input_dim(self): + return num_features + num_pms + 1 + + def forward(self, states, actor: bool = True, critic: bool = True): + features, masks = states[:, :num_features], states[:, num_features:] + masks += 1e-8 # this is to prevent zero probability and infinite logP. + return (self.actor(features) * masks if actor else None), (self.critic(features) if critic else None) + + def step(self, loss): + self.actor_optim.zero_grad() + self.critic_optim.zero_grad() + loss.backward() + self.actor_optim.step() + self.critic_optim.step() + + def get_gradients(self, loss): + self.actor_optim.zero_grad() + self.critic_optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad): + for name, param in self.named_parameters(): + param.grad = grad[name] + + self.actor_optim.step() + self.critic_optim.step() + + def get_state(self): + return { + "network": self.state_dict(), + "actor_optim": self.actor_optim.state_dict(), + "critic_optim": self.critic_optim.state_dict() + } + + def set_state(self, state): + self.load_state_dict(state["network"]) + self.actor_optim.load_state_dict(state["actor_optim"]) + self.critic_optim.load_state_dict(state["critic_optim"]) + + +if algorithm == "dqn": + policy_func_dict = {"dqn": lambda name: DQN(name, MyQNet(), **dqn_conf)} +else: + policy_func_dict = {"ac": lambda name: ActorCritic(name, MyACNet(), **ac_conf)} diff --git a/examples/rl/vm_scheduling/policy_index.py b/examples/rl/vm_scheduling/policy_index.py deleted file mode 100644 index d9f3dad26..000000000 --- a/examples/rl/vm_scheduling/policy_index.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - - -cim_path = os.path.dirname(os.path.realpath(__file__)) -if cim_path not in sys.path: - sys.path.insert(0, cim_path) -from ac import get_ac_policy -from dqn import get_dqn_policy - -update_trigger = {"POLICY": 128} -warmup = {"POLICY": 1} - -# use agent IDs as policy names since each agent uses a separate policy -rl_policy_func_index = {"POLICY": get_ac_policy} -agent2policy = {"AGENT": "POLICY"} diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml index 29839088b..cf831283e 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/workflows/config.yml @@ -20,7 +20,7 @@ sync: async: num_rollouts: 3 policy_manager: - type: distributed # simple, multi-process, distributed + type: simple # simple, multi-process, distributed distributed: num_hosts: 2 data_parallel: From 3a928b9edfa498a937ea9383888eca59534c95bf Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Sun, 26 Sep 2021 15:04:42 +0800 Subject: [PATCH 452/482] SC refinement (#397) * Refine test scripts & pending_order_daily logic * Refactor code for better code style: complete type hint, correct typos, remove unused items. Refactor code for better code style: complete type hint, correct typos, remove unused items. * Polish test_supply_chain.py * update import format * Modify vehicle steps logic & remove outdated test case * Optimize imports * Optimize imports * Lint error * Lint error * Lint error * Add SupplyChainAction * Lint error Co-authored-by: Jinyu Wang --- .../scenarios/supply_chain/__init__.py | 4 +- .../scenarios/supply_chain/actions.py | 22 +- .../supply_chain/facilities/facility.py | 40 +- .../scenarios/supply_chain/parser.py | 1 + .../supply_chain/topologies/random/config.yml | 2 +- .../scenarios/supply_chain/units/__init__.py | 2 +- .../scenarios/supply_chain/units/consumer.py | 12 +- .../supply_chain/units/manufacture.py | 2 + .../scenarios/supply_chain/units/order.py | 13 +- .../supply_chain/units/outerseller.py | 5 +- .../scenarios/supply_chain/units/product.py | 4 +- .../scenarios/supply_chain/units/seller.py | 2 + .../scenarios/supply_chain/units/storage.py | 3 +- .../scenarios/supply_chain/units/unitbase.py | 31 +- .../scenarios/supply_chain/units/vehicle.py | 27 +- .../simulator/scenarios/supply_chain/world.py | 18 +- tests/supply_chain/test_supply_chain.py | 402 ++++++++---------- 17 files changed, 299 insertions(+), 291 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index b6e6162ff..beefa64d7 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -7,4 +7,6 @@ ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel ) from .facilities import FacilityBase, RetailerFacility, SupplierFacility, WarehouseFacility -from .units import ConsumerUnit, DistributionUnit, ProductUnit, SellerUnit, ExtendUnitBase, StorageUnit, UnitBase, VehicleUnit +from .units import ( + ConsumerUnit, DistributionUnit, ExtendUnitBase, ProductUnit, SellerUnit, StorageUnit, UnitBase, VehicleUnit +) diff --git a/maro/simulator/scenarios/supply_chain/actions.py b/maro/simulator/scenarios/supply_chain/actions.py index b6791a851..fbb40d511 100644 --- a/maro/simulator/scenarios/supply_chain/actions.py +++ b/maro/simulator/scenarios/supply_chain/actions.py @@ -2,8 +2,24 @@ # Licensed under the MIT license. -from collections import namedtuple +class SupplyChainAction: + def __init__(self, id: int) -> None: + self.id = id -ConsumerAction = namedtuple("ConsumerAction", ("id", "product_id", "source_id", "quantity", "vlt", "reward_discount")) -ManufactureAction = namedtuple("ManufactureAction", ("id", "production_rate")) +class ConsumerAction(SupplyChainAction): + def __init__( + self, id: int, product_id: int, source_id: int, quantity: int, vlt: int, reward_discount: float + ) -> None: + super(ConsumerAction, self).__init__(id=id) + self.product_id = product_id + self.source_id = source_id + self.quantity = quantity + self.vlt = vlt + self.reward_discount = reward_discount + + +class ManufactureAction(SupplyChainAction): + def __init__(self, id: int, production_rate: float) -> None: + super(ManufactureAction, self).__init__(id=id) + self.production_rate = production_rate diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py index c5a3e6781..73228d6ac 100644 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ b/maro/simulator/scenarios/supply_chain/facilities/facility.py @@ -1,59 +1,69 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from __future__ import annotations +import typing from abc import ABC from collections import defaultdict -from typing import Dict, List +from typing import Dict, List, Optional from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit +if typing.TYPE_CHECKING: + from maro.simulator.scenarios.supply_chain.datamodels.base import DataModelBase + from maro.simulator.scenarios.supply_chain.world import World + class FacilityBase(ABC): """Base of all facilities.""" # Id of this facility. - id: int = None + id: Optional[int] = None # Name of this facility. - name: str = None + name: Optional[str] = None # World of this facility belongs to. - world = None + world: Optional[World] = None # Skus in this facility. - skus: Dict[int, SkuInfo] = None + skus: Optional[Dict[int, SkuInfo]] = None # Product units for each sku in this facility. # Key is sku(product) id, value is the instance of product unit. - products: Dict[int, ProductUnit] = None + products: Optional[Dict[int, ProductUnit]] = None # Storage unit in this facility. - storage: StorageUnit = None + storage: Optional[StorageUnit] = None # Distribution unit in this facility. - distribution: DistributionUnit = None + distribution: Optional[DistributionUnit] = None # Upstream facilities. - # Key is sku id, value is the list of product unit from upstream. - upstreams: Dict[int, List[ProductUnit]] = None + # Key is sku id, value is the list of facilities from upstream. + upstreams: Optional[Dict[int, List[FacilityBase]]] = None # Down stream facilities, value same as upstreams. - downstreams: Dict[int, List[ProductUnit]] = None + downstreams: Optional[Dict[int, List[FacilityBase]]] = None # Configuration of this facility. - configs: dict = None + configs: Optional[dict] = None # Name of data model, from configuration. - data_model_name: str = None + data_model_name: Optional[str] = None # Index of the data model node. data_model_index: int = 0 - data_model: object = None + data_model: Optional[DataModelBase] = None # Children of this facility (valid units). - children: list = None + children: Optional[list] = None + + # Facility's coordinates + x: Optional[int] = None + y: Optional[int] = None def __init__(self): self.upstreams = {} diff --git a/maro/simulator/scenarios/supply_chain/parser.py b/maro/simulator/scenarios/supply_chain/parser.py index 16046e07a..e80124a51 100644 --- a/maro/simulator/scenarios/supply_chain/parser.py +++ b/maro/simulator/scenarios/supply_chain/parser.py @@ -109,6 +109,7 @@ def add_facility_definition(self, alias: str, class_name: str, module_path: str, alias (str): Alias of this facility. class_name (str): Name of this class. module_path (str): Full path of the module. + data_model_alias (str): Data model alias. """ assert alias not in self.facilities diff --git a/maro/simulator/scenarios/supply_chain/topologies/random/config.yml b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml index e103236fe..9043327b6 100644 --- a/maro/simulator/scenarios/supply_chain/topologies/random/config.yml +++ b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml @@ -7,7 +7,7 @@ facility_definitions: agent_type: productstore consumer: class: ConsumerUnit - config: + config: agent_type: consumerstore seller: class: SellerUnit diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py index 6fef50d02..11f4abb3a 100644 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ b/maro/simulator/scenarios/supply_chain/units/__init__.py @@ -4,12 +4,12 @@ from .consumer import ConsumerUnit from .distribution import DistributionUnit +from .extendunitbase import ExtendUnitBase from .manufacture import ManufactureUnit from .outerseller import DataFileDemandSampler, OuterSellerUnit, SellerDemandSampler from .product import ProductUnit from .seller import SellerUnit from .simplemanufacture import SimpleManufactureUnit -from .extendunitbase import ExtendUnitBase from .storage import StorageUnit from .storeproduct import StoreProductUnit from .unitbase import UnitBase diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 097d60c19..dfa01af75 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -1,13 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - - from collections import Counter, defaultdict from scipy.ndimage.interpolation import shift -from .order import Order +from .. import ConsumerDataModel from .extendunitbase import ExtendUnitBase +from .order import Order class ConsumerUnit(ExtendUnitBase): @@ -57,7 +56,9 @@ def initialize(self): sku = self.facility.skus[self.product_id] order_cost = self.facility.get_config("order_cost") + assert isinstance(order_cost, int) + assert isinstance(self.data_model, ConsumerDataModel) self.data_model.initialize(sku.price, order_cost) if self.facility.upstreams is not None: @@ -66,7 +67,7 @@ def initialize(self): if sources is not None: # Is we are a supplier facility? - is_supplier = self.parent.manufacture is not None + is_supplier = getattr(self.parent, "manufacture", None) is not None # Current sku information. sku = self.world.get_sku_by_id(self.product_id) @@ -104,9 +105,6 @@ def step(self, tick: int): self.purchased = self.action.quantity - if order.vlt < len(self.pending_order_daily): - self.pending_order_daily[order.vlt - 1] += order.quantity - def flush_states(self): if self.received > 0: self.data_model.received = self.received diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index e54da60fa..b705f91e0 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. +from .. import ManufactureDataModel from .extendunitbase import ExtendUnitBase @@ -29,6 +30,7 @@ def initialize(self): facility_sku_info = self.facility.skus[self.product_id] product_unit_cost = facility_sku_info.product_unit_cost + assert isinstance(self.data_model, ManufactureDataModel) self.data_model.initialize(product_unit_cost) global_sku_info = self.world.get_sku_by_id(self.product_id) diff --git a/maro/simulator/scenarios/supply_chain/units/order.py b/maro/simulator/scenarios/supply_chain/units/order.py index 47a8335d0..3624402bc 100644 --- a/maro/simulator/scenarios/supply_chain/units/order.py +++ b/maro/simulator/scenarios/supply_chain/units/order.py @@ -1,7 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from __future__ import annotations +import typing +from typing import NamedTuple -from collections import namedtuple +if typing.TYPE_CHECKING: + from maro.simulator.scenarios.supply_chain import FacilityBase -Order = namedtuple("Order", ("destination", "product_id", "quantity", "vlt")) + +class Order(NamedTuple): + destination: FacilityBase + product_id: int + quantity: int + vlt: int diff --git a/maro/simulator/scenarios/supply_chain/units/outerseller.py b/maro/simulator/scenarios/supply_chain/units/outerseller.py index c7eb0b3a1..8ebce4174 100644 --- a/maro/simulator/scenarios/supply_chain/units/outerseller.py +++ b/maro/simulator/scenarios/supply_chain/units/outerseller.py @@ -1,10 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - import warnings from abc import ABC, abstractmethod from collections import namedtuple from csv import DictReader +from datetime import datetime +from typing import Optional, Union from dateutil import parser @@ -59,7 +60,7 @@ def __init__(self, configs: dict, world): self._file_path = configs["file_path"] # If start date time is None, then will use first row as start date time (tick 0). - self._start_date_time = self._world.configs.settings["start_date_time"] + self._start_date_time: Optional[Union[str, datetime]] = self._world.configs.settings["start_date_time"] if self._start_date_time is not None: self._start_date_time = parser.parse(self._start_date_time, ignoretz=True) diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py index 917e99275..9ed0c342c 100644 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ b/maro/simulator/scenarios/supply_chain/units/product.py @@ -3,11 +3,12 @@ import numpy as np +from ..datamodels import ProductDataModel from .consumer import ConsumerUnit from .distribution import DistributionUnit +from .extendunitbase import ExtendUnitBase from .manufacture import ManufactureUnit from .seller import SellerUnit -from .extendunitbase import ExtendUnitBase from .storage import StorageUnit @@ -39,6 +40,7 @@ def initialize(self): facility_sku = self.facility.skus[self.product_id] + assert isinstance(self.data_model, ProductDataModel) self.data_model.initialize(facility_sku.price) def step(self, tick: int): diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py index 3db33b376..e0803cec9 100644 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ b/maro/simulator/scenarios/supply_chain/units/seller.py @@ -4,6 +4,7 @@ import numpy as np +from .. import SellerDataModel from .extendunitbase import ExtendUnitBase @@ -44,6 +45,7 @@ def initialize(self): self.gamma = sku.sale_gamma + assert isinstance(self.data_model, SellerDataModel) self.data_model.initialize(sku.price, sku.backlog_ratio) self.sale_hist = [self.gamma] * self.config["sale_hist_len"] diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py index b0065a9a7..e78cb0ff3 100644 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ b/maro/simulator/scenarios/supply_chain/units/storage.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import random from typing import Dict from .unitbase import UnitBase @@ -117,7 +116,7 @@ def initialize(self): self.data_model.initialize( capacity=self.capacity, remaining_space=self.remaining_space, - product_list=[id for id in self.product_level.keys()], + product_list=[sku_id for sku_id in self.product_level.keys()], product_number=[n for n in self.product_level.values()] ) diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py index 6d239e52d..8e027ee37 100644 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -1,5 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from __future__ import annotations + +import typing +from typing import Optional, Union + +from maro.simulator.scenarios.supply_chain.actions import SupplyChainAction + +if typing.TYPE_CHECKING: + from maro.simulator.scenarios.supply_chain import FacilityBase + from maro.simulator.scenarios.supply_chain.datamodels.base import DataModelBase + from maro.simulator.scenarios.supply_chain.world import World class UnitBase: @@ -24,31 +35,31 @@ class UnitBase: id: int = 0 # Which this unit belongs to. - facility = None + facility: Optional[FacilityBase] = None # Which world this unit belongs to. - world = None + world: Optional[World] = None # Parent of this unit, it can be a facility or another unit. - parent: object = None + parent: Optional[Union[FacilityBase, UnitBase]] = None # Child units, extended unit can add their own child as property, this is used as a collection. - children: list = None + children: Optional[list] = None # Data model name in the frame, used to query binding data model instance. - data_model_name: str = None + data_model_name: Optional[str] = None # Data model instance index in the frame, used to query binding data model instance. - data_model_index: int = None + data_model_index: Optional[int] = None # Real data model binding with this unit. - data_model = None + data_model: Optional[DataModelBase] = None # Current action. - action: object = None + action: Optional[SupplyChainAction] = None # Current unit configurations. - config: dict = None + config: Optional[dict] = None def __init__(self): pass @@ -97,7 +108,7 @@ def initialize(self): if self.data_model is not None: self.data_model.set_id(self.id, self.facility.id) - def set_action(self, action: object): + def set_action(self, action: SupplyChainAction): """Set action for this agent. Args: diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py index c0c126fb2..e74b59d44 100644 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ b/maro/simulator/scenarios/supply_chain/units/vehicle.py @@ -1,9 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from __future__ import annotations +import math +import typing +from typing import Optional from .unitbase import UnitBase +if typing.TYPE_CHECKING: + from .. import FacilityBase + class VehicleUnit(UnitBase): """Unit used to move production from source to destination by order.""" @@ -11,13 +18,13 @@ class VehicleUnit(UnitBase): def __init__(self): super().__init__() # Max patient of current vehicle. - self.max_patient: int = None + self.max_patient: Optional[int] = None # Current products' destination. - self.destination = None + self.destination: Optional[FacilityBase] = None # Path to destination. - self.path: list = None + self.path: Optional[list] = None # Product to load self.product_id = 0 @@ -41,7 +48,7 @@ def __init__(self): self.cost = 0 self.unit_transport_cost = 0 - def schedule(self, destination: object, product_id: int, quantity: int, vlt: int): + def schedule(self, destination: FacilityBase, product_id: int, quantity: int, vlt: int): """Schedule a job for this vehicle. Args: @@ -66,8 +73,10 @@ def schedule(self, destination: object, product_id: int, quantity: int, vlt: int raise Exception(f"Destination {destination} is unreachable") # Steps to destination. - # self.steps = len(self.path) // vlt - self.steps = vlt + self.steps = int(math.ceil(float(len(self.path) - 1) / float(vlt))) + dest_consumer = destination.products[product_id].consumer + if self.steps < len(dest_consumer.pending_order_daily): + dest_consumer.pending_order_daily[self.steps] += quantity # We are waiting for product loading. self.location = 0 @@ -167,9 +176,9 @@ def step(self, tick: int): self.try_unload() # Back to source if we unload all. - # if self.payload == 0: - self._reset_internal_states() - self._reset_data_model() + if self.payload == 0: + self._reset_internal_states() + self._reset_data_model() self.cost = self.payload * self.unit_transport_cost diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py index 8bfe0043b..15cc9586d 100644 --- a/maro/simulator/scenarios/supply_chain/world.py +++ b/maro/simulator/scenarios/supply_chain/world.py @@ -2,8 +2,7 @@ # Licensed under the MIT license. -from collections import namedtuple -from typing import List, Tuple, Union +from typing import List, NamedTuple, Optional, Tuple, Union import networkx as nx @@ -15,7 +14,14 @@ from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef from .units import ExtendUnitBase, UnitBase -AgentInfo = namedtuple("AgentInfo", ("id", "agent_type", "is_facility", "sku", "facility_id", "parent_id")) + +class AgentInfo(NamedTuple): + id: int + agent_type: object + is_facility: bool + sku: Optional[SkuInfo] + facility_id: int + parent_id: Optional[int] class World: @@ -23,10 +29,10 @@ class World: def __init__(self): # Frame for current world configuration. - self.frame: FrameBase = None + self.frame: Optional[FrameBase] = None # Current configuration. - self.configs: SupplyChainConfiguration = None + self.configs: Optional[SupplyChainConfiguration] = None # Durations of current simulation. self.durations = 0 @@ -41,7 +47,7 @@ def __init__(self): self._id_counter = 1 # Grid of the world - self._graph: nx.Graph = None + self._graph: Optional[nx.Graph] = None # Sku id to name mapping, used for querying. self._sku_id2name_mapping = {} diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py index 1260a0941..98e6a1415 100644 --- a/tests/supply_chain/test_supply_chain.py +++ b/tests/supply_chain/test_supply_chain.py @@ -1,12 +1,14 @@ import os import unittest +from typing import Optional import numpy as np from maro.simulator import Env -from maro.simulator.scenarios.supply_chain import ManufactureAction, ConsumerAction -from maro.simulator.scenarios.supply_chain import StorageUnit, ConsumerUnit, FacilityBase, VehicleUnit, \ - DistributionUnit, SellerUnit +from maro.simulator.scenarios.supply_chain import ( + ConsumerAction, ConsumerUnit, DistributionUnit, FacilityBase, ManufactureAction, SellerUnit, StorageUnit, VehicleUnit +) +from maro.simulator.scenarios.supply_chain.business_engine import SupplyChainBusinessEngine from maro.simulator.scenarios.supply_chain.units.order import Order @@ -55,6 +57,7 @@ class MyTestCase(unittest.TestCase): def test_manufacture_meet_storage_limitation(self): """Test sku3 manufacturing.""" env = build_env("case_01", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) storage_nodes = env.snapshot_list["storage"] storage_features = ("id", "facility_id", "capacity", "remaining_space") @@ -66,7 +69,7 @@ def test_manufacture_meet_storage_limitation(self): "product_unit_cost" ) - ############################### TICK: 0 ###################################### + # ############################### TICK: 0 ###################################### # tick 0 passed, no product manufacturing. env.step(None) @@ -75,15 +78,24 @@ def test_manufacture_meet_storage_limitation(self): -1).astype(np.int) # try to find which one is sku3 manufacture unit. + sku3_data_model_index: Optional[int] = None + sku3_manufacture_id: Optional[int] = None + sku3_facility_id: Optional[int] = None for index, state in enumerate(states): # Id of sku3 is 3. if state[3] == SKU3_ID: sku3_data_model_index = index sku3_manufacture_id = state[0] sku3_facility_id = state[1] + self.assertTrue(all([ + sku3_data_model_index is not None, + sku3_manufacture_id is not None, + sku3_facility_id is not None + ])) # try to find sku3's storage from env.summary - sku3_storage_index = env.summary["node_mapping"]["facilities"][sku3_facility_id]["units"]["storage"]["node_index"] + sku3_facility_info = env.summary["node_mapping"]["facilities"][sku3_facility_id] + sku3_storage_index = sku3_facility_info["units"]["storage"]["node_index"] storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) @@ -102,12 +114,12 @@ def test_manufacture_meet_storage_limitation(self): # all the id is greater than 0 self.assertGreater(sku3_manufacture_id, 0) - ############################### TICK: 1 ###################################### + # ############################### TICK: 1 ###################################### # pass an action to start manufacturing for this tick. action = ManufactureAction(sku3_manufacture_id, 1) - env.step({action.id: action}) + env.step([action]) states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) @@ -124,7 +136,7 @@ def test_manufacture_meet_storage_limitation(self): # sku3 number should be 80 + 1 self.assertEqual(80 + 1, product_dict[SKU3_ID]) - ############################### TICK: 2 ###################################### + # ############################### TICK: 2 ###################################### # leave the action as none will cause manufacture unit stop manufacturing. env.step(None) @@ -140,7 +152,7 @@ def test_manufacture_meet_storage_limitation(self): self.assertEqual(80 + 1, product_dict[SKU3_ID]) # let is generate 20, but actually it can only procedure 19 because the storage will reach the limitation - env.step({sku3_manufacture_id: ManufactureAction(sku3_manufacture_id, 20)}) + env.step([ManufactureAction(sku3_manufacture_id, 20)]) states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) @@ -161,6 +173,7 @@ def test_manufacture_meet_source_lack(self): """Test sku4 manufacturing, this sku supplier does not have enough source material at the begging , so it cannot produce anything without consumer purchase.""" env = build_env("case_01", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) storage_nodes = env.snapshot_list["storage"] storage_features = ("id", "facility_id", "capacity", "remaining_space") @@ -172,7 +185,7 @@ def test_manufacture_meet_source_lack(self): "product_unit_cost" ) - ############################### TICK: 0 ###################################### + # ############################### TICK: 0 ###################################### # tick 0 passed, no product manufacturing. env.step(None) @@ -181,15 +194,24 @@ def test_manufacture_meet_source_lack(self): -1).astype(np.int) # try to find which one is sku3 manufacture unit. + sku4_data_model_index: Optional[int] = None + sku4_manufacture_id: Optional[int] = None + sku4_facility_id: Optional[int] = None for index, state in enumerate(states): # Id of sku4 is 4. if state[3] == SKU4_ID: sku4_data_model_index = index sku4_manufacture_id = state[0] sku4_facility_id = state[1] + self.assertTrue(all([ + sku4_data_model_index is not None, + sku4_manufacture_id is not None, + sku4_facility_id is not None + ])) # try to find sku4's storage from env.summary - sku4_storage_index = env.summary["node_mapping"]["facilities"][sku4_facility_id]["units"]["storage"]["node_index"] + sku4_facility_info = env.summary["node_mapping"]["facilities"][sku4_facility_id] + sku4_storage_index = sku4_facility_info["units"]["storage"]["node_index"] # the storage should be same as initialized (50 + 0). storage_states = storage_nodes[env.frame_index:sku4_storage_index:storage_features].flatten().astype(np.int) @@ -221,13 +243,13 @@ def test_manufacture_meet_source_lack(self): # 0 sku2 self.assertEqual(0, product_dict[SKU2_ID]) - ############################### TICK: 1 - end ###################################### + # ############################### TICK: 1 - end ###################################### is_done = False while not is_done: # push to the end, the storage should not changed, no matter what production rate we give it. - _, _, is_done = env.step({sku4_manufacture_id: ManufactureAction(sku4_manufacture_id, 10)}) + _, _, is_done = env.step([ManufactureAction(sku4_manufacture_id, 10)]) manufature_states = manufacture_nodes[ env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype( @@ -254,6 +276,7 @@ def test_manufacture_meet_avg_storage_limitation(self): """Test on sku1, it is configured with nearly full initial states.""" env = build_env("case_01", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) storage_nodes = env.snapshot_list["storage"] storage_features = ("id", "facility_id", "capacity", "remaining_space") @@ -265,7 +288,7 @@ def test_manufacture_meet_avg_storage_limitation(self): "product_unit_cost" ) - ############################### TICK: 0 ###################################### + # ############################### TICK: 0 ###################################### # tick 0 passed, no product manufacturing, verified in above case, pass checking it here. env.step(None) @@ -273,19 +296,28 @@ def test_manufacture_meet_avg_storage_limitation(self): states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, -1).astype(np.int) # try to find which one is sku3 manufacture unit. + sku1_data_model_index: Optional[int] = None + sku1_manufacture_id: Optional[int] = None + sku1_facility_id: Optional[int] = None for index, state in enumerate(states): # Id of sku1 is 1. if state[3] == SKU1_ID: sku1_data_model_index = index sku1_manufacture_id = state[0] sku1_facility_id = state[1] + self.assertTrue(all([ + sku1_data_model_index is not None, + sku1_manufacture_id is not None, + sku1_facility_id is not None + ])) - sku1_storage_index = env.summary["node_mapping"]["facilities"][sku1_facility_id]["units"]["storage"]["node_index"] + sku1_facility_info = env.summary["node_mapping"]["facilities"][sku1_facility_id] + sku1_storage_index = sku1_facility_info["units"]["storage"]["node_index"] - ############################### TICK: 1 ###################################### + # ############################### TICK: 1 ###################################### # ask sku1 manufacture start manufacturing, rate is 10. - env.step({sku1_manufacture_id: ManufactureAction(sku1_storage_index, 10)}) + env.step([ManufactureAction(sku1_manufacture_id, 10)]) storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) manufacture_states = manufacture_nodes[ @@ -305,14 +337,14 @@ def test_manufacture_meet_avg_storage_limitation(self): # 4 sku1 cost 4*2 source material (sku3) self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) - ############################### TICK: 1 ###################################### + # ############################### TICK: 1 ###################################### # then fix the product rate to 20 every tick, but the manufacture will do nothing, as we have to enough space is_done = False while not is_done: - _, _, is_done = env.step({sku1_manufacture_id: ManufactureAction(sku1_storage_index, 20)}) + _, _, is_done = env.step([ManufactureAction(sku1_storage_index, 20)]) storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) manufacture_states = manufacture_nodes[ @@ -352,22 +384,17 @@ def test_manufacture_meet_avg_storage_limitation(self): def test_storage_take_available(self): env = build_env("case_01", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) env.step(None) storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "capacity", "remaining_space") # find first storage unit id storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - capacity = storage_states[1] - init_remaining_space = storage_states[2] + storage_unit: StorageUnit = env.business_engine.world.get_entity(storage_unit_id) init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) @@ -423,6 +450,7 @@ def test_storage_try_add_products(self): """ env = build_env("case_01", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) env.step(None) @@ -433,7 +461,7 @@ def test_storage_try_add_products(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + storage_unit: StorageUnit = env.business_engine.world.get_entity(storage_unit_id) storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) @@ -442,8 +470,6 @@ def test_storage_try_add_products(self): init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - first_product_id = [id for id in init_product_dict.keys()][0] - # try put products out of capacity with all_or_nothing == True products_to_put = {} @@ -465,8 +491,9 @@ def test_storage_try_add_products(self): self.assertEqual(product_number, storage_unit.product_level[product_id]) - # if we set all_or_nothing=False, then part of the product will be added to storage, and cause remaining space being 0 - result = storage_unit.try_add_products(products_to_put, all_or_nothing=False) + # if we set all_or_nothing=False, then part of the product will be added to storage, and cause remaining + # space being 0 + storage_unit.try_add_products(products_to_put, all_or_nothing=False) self.assertEqual(0, storage_unit.remaining_space) @@ -483,8 +510,8 @@ def test_storage_try_add_products(self): # total product number should be same as capacity self.assertEqual(capacity, sum(product_dict.values())) - #################################################### - #################################################### + # ################################################### + # ################################################### # reset the env for next case env.reset() @@ -498,6 +525,7 @@ def test_storage_try_add_products(self): def test_storage_try_take_products(self): env = build_env("case_01", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) env.step(None) @@ -508,7 +536,7 @@ def test_storage_try_take_products(self): storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + storage_unit: StorageUnit = env.business_engine.world.get_entity(storage_unit_id) storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) @@ -550,17 +578,17 @@ def test_storage_try_take_products(self): def test_storage_get_product_number(self): env = build_env("case_01", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) env.step(None) storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "capacity", "remaining_space") # find first storage unit id storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] # get the unit reference from env internal - storage_unit: StorageUnit = env._business_engine.world.get_entity(storage_unit_id) + storage_unit: StorageUnit = env.business_engine.world.get_entity(storage_unit_id) init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) @@ -598,26 +626,23 @@ def test_consumer_init_state(self): NOTE: we will use consumer on Supplier_SKU1, as it contains a source for sku3 (Supplier_SKU3) """ env = build_env("case_01", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # print(env.summary) # we can get the consumer from env.summary # NOTE: though we are test with sku1, but the consumer is for sku3, as it is the source material from source - sku3_consumer_unit: ConsumerUnit - sku3_supplier_faiclity_id: int - sku3_consumer_data_model_index: int - sku3_product_unit_id: int + sku3_consumer_unit: Optional[ConsumerUnit] = None + sku3_product_unit_id: Optional[int] = None + sku3_consumer_unit_id: Optional[int] = None - for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): - if facility_defail["name"] == "Supplier_SKU1": + for facility_id, facility_detail in env.summary["node_mapping"]["facilities"].items(): + if facility_detail["name"] == "Supplier_SKU1": # try to find sku3 consumer - sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] + sku3_consumer_unit_id = facility_detail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) - sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] - - if facility_defail["name"] == "Supplier_SKU3": - sku3_supplier_faiclity_id = facility_defail["id"] + sku3_consumer_unit = env.business_engine.world.get_entity(sku3_consumer_unit_id) + sku3_product_unit_id = facility_detail["units"]["products"][SKU3_ID]["id"] sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] @@ -644,7 +669,7 @@ def test_consumer_init_state(self): # check sources for source_facility_id in sku3_consumer_unit.sources: - source_facility: FacilityBase = env._business_engine.world.get_facility_by_id(source_facility_id) + source_facility: FacilityBase = env.business_engine.world.get_facility_by_id(source_facility_id) # check if source facility contains the sku3 config self.assertTrue(SKU3_ID in source_facility.skus) @@ -688,28 +713,30 @@ def test_consumer_init_state(self): def test_consumer_action(self): env = build_env("case_01", 100) - - sku3_consumer_unit: ConsumerUnit - sku3_supplier_faiclity_id: int - sku3_consumer_data_model_index: int - sku3_product_unit_id: int - - for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): - if facility_defail["name"] == "Supplier_SKU1": - sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) - sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] - - if facility_defail["name"] == "Supplier_SKU3": - sku3_supplier_faiclity_id = facility_defail["id"] + assert isinstance(env.business_engine, SupplyChainBusinessEngine) + + sku3_consumer_unit_id: Optional[int] = None + sku3_consumer_unit: Optional[ConsumerUnit] = None + sku3_supplier_facility_id: Optional[int] = None + + for facility_id, facility_detail in env.summary["node_mapping"]["facilities"].items(): + if facility_detail["name"] == "Supplier_SKU1": + sku3_consumer_unit_id = facility_detail["units"]["products"][SKU3_ID]["consumer"]["id"] + sku3_consumer_unit = env.business_engine.world.get_entity(sku3_consumer_unit_id) + if facility_detail["name"] == "Supplier_SKU3": + sku3_supplier_facility_id = facility_detail["id"] + self.assertTrue(all([ + sku3_consumer_unit_id is not None, + sku3_consumer_unit is not None, + sku3_supplier_facility_id is not None + ])) sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] # zero quantity will be ignore - action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 0, 1, 0) + action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 0, 1, 0) - action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_faiclity_id, 10, 1, 0) + action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1, 0) sku3_consumer_unit.set_action(action_with_zero) @@ -741,7 +768,7 @@ def test_consumer_action(self): self.assertEqual(SKU3_ID, states[2]) # NOTE: we cannot set_action directly here, as post_step will clear the action before starting next tick - env.step({action.id: action}) + env.step([action]) self.assertEqual(action.quantity, sku3_consumer_unit.purchased) self.assertEqual(0, sku3_consumer_unit.received) @@ -765,7 +792,7 @@ def test_consumer_action(self): self.assertEqual(0, states[9]) # same action for next step, so total_XXX will be changed to double - env.step({action.id: action}) + env.step([action]) states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) @@ -787,30 +814,30 @@ def test_consumer_action(self): def test_consumer_on_order_reception(self): env = build_env("case_01", 100) - - sku3_consumer_unit: ConsumerUnit - sku3_supplier_facility_id: int - sku3_consumer_data_model_index: int - sku3_product_unit_id: int - - for facility_id, facility_defail in env.summary["node_mapping"]["facilities"].items(): - if facility_defail["name"] == "Supplier_SKU1": - sku3_consumer_unit_id = facility_defail["units"]["products"][SKU3_ID]["consumer"]["id"] - - sku3_consumer_unit = env._business_engine.world.get_entity(sku3_consumer_unit_id) - sku3_product_unit_id = facility_defail["units"]["products"][SKU3_ID]["id"] - - if facility_defail["name"] == "Supplier_SKU3": - sku3_supplier_facility_id = facility_defail["id"] - - sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] + assert isinstance(env.business_engine, SupplyChainBusinessEngine) + + sku3_consumer_unit_id: Optional[int] = None + sku3_consumer_unit: Optional[ConsumerUnit] = None + sku3_supplier_facility_id: Optional[int] = None + + for facility_id, facility_detail in env.summary["node_mapping"]["facilities"].items(): + if facility_detail["name"] == "Supplier_SKU1": + sku3_consumer_unit_id = facility_detail["units"]["products"][SKU3_ID]["consumer"]["id"] + sku3_consumer_unit = env.business_engine.world.get_entity(sku3_consumer_unit_id) + if facility_detail["name"] == "Supplier_SKU3": + sku3_supplier_facility_id = facility_detail["id"] + self.assertTrue(all([ + sku3_consumer_unit_id is not None, + sku3_consumer_unit is not None, + sku3_supplier_facility_id is not None + ])) action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1, 0) # 1st step must none action env.step(None) - env.step({action.id: action}) + env.step([action]) # simulate purchased product is arrived by vehicle unit sku3_consumer_unit.on_order_reception(sku3_supplier_facility_id, SKU3_ID, 10, 10) @@ -821,9 +848,6 @@ def test_consumer_on_order_reception(self): env.step(None) - consumer_nodes = env.snapshot_list["consumer"] - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:"received"].flatten().astype(np.int) - # NOTE: we cannot test the received state by calling on_order_reception directly, # as it will be cleared by env.step, do it on vehicle unit test. @@ -842,19 +866,17 @@ def test_consumer_on_order_reception(self): def test_vehicle_unit_state(self): env = build_env("case_02", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # try to find first vehicle unit we meet - vehicle_unit: VehicleUnit - vehicle_unit_id: int - vehicle_unit_data_model_index: int + vehicle_unit: Optional[VehicleUnit] = None - for id, info in env.summary["node_mapping"]["unit_mapping"].items(): + for unit_id, info in env.summary["node_mapping"]["unit_mapping"].items(): if info[0] == "vehicle": - vehicle_unit_id = id - vehicle_unit = env._business_engine.world.get_entity(id) - vehicle_unit_data_model_index = vehicle_unit.data_model_index + vehicle_unit = env.business_engine.world.get_entity(unit_id) break + self.assertTrue(vehicle_unit is not None) # check initial state according to configuration file self.assertEqual(10, vehicle_unit.max_patient) @@ -913,18 +935,20 @@ def test_vehicle_unit_state(self): def test_vehicle_unit_schedule(self): env = build_env("case_02", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # try to find first vehicle unit of Supplier - vehicle_unit: VehicleUnit - dest_facility: FacilityBase + vehicle_unit: Optional[VehicleUnit] = None + dest_facility: Optional[FacilityBase] = None - for id, info in env.summary["node_mapping"]["facilities"].items(): + for _, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) + vehicle_unit = env.business_engine.world.get_entity(v["id"]) if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) + self.assertTrue(all([vehicle_unit is not None, dest_facility is not None])) # make sure the upstream in the only one supplier in config self.assertEqual(1, len(dest_facility.upstreams)) @@ -997,18 +1021,20 @@ def test_vehicle_unit_no_patient(self): NOTE: with patient is tried in above case after schedule the job """ env = build_env("case_02", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # try to find first vehicle unit of Supplier - vehicle_unit: VehicleUnit - dest_facility: FacilityBase + vehicle_unit: Optional[VehicleUnit] = None + dest_facility: Optional[FacilityBase] = None - for id, info in env.summary["node_mapping"]["facilities"].items(): + for _, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) + vehicle_unit = env.business_engine.world.get_entity(v["id"]) if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) + self.assertTrue(all([vehicle_unit is not None, dest_facility is not None])) # there is 80 sku3 in supplier, lets schedule a job for 100, to make sure it will fail to try load vehicle_unit.schedule(dest_facility, SKU3_ID, 100, 3) @@ -1072,18 +1098,20 @@ def test_vehicle_unit_cannot_unload_at_destination(self): """ env = build_env("case_02", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # try to find first vehicle unit of Supplier - vehicle_unit: VehicleUnit - dest_facility: FacilityBase + vehicle_unit: Optional[VehicleUnit] = None + dest_facility: Optional[FacilityBase] = None - for id, info in env.summary["node_mapping"]["facilities"].items(): + for _, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": for v in info["units"]["distribution"]["children"]: - vehicle_unit = env._business_engine.world.get_entity(v["id"]) + vehicle_unit = env.business_engine.world.get_entity(v["id"]) if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) + self.assertTrue(all([vehicle_unit is not None, dest_facility is not None])) # move all 80 sku3 to destination, will cause vehicle keep waiting there vehicle_unit.schedule(dest_facility, SKU3_ID, 80, 2) @@ -1095,17 +1123,6 @@ def test_vehicle_unit_cannot_unload_at_destination(self): _, _, is_done = env.step(None) vehicle_nodes = env.snapshot_list["vehicle"] - features = ( - "id", - "facility_id", - "source", - "destination", - "payload", - "product_id", - "requested_quantity", - "steps", - "unit_transport_cost" - ) # payload should be 80 for first 4 ticks, as it is on the way # then it will unload 100 - 10 - 10 - 10 = 70 products, as this is the remaining space of destination storage @@ -1124,17 +1141,19 @@ def test_vehicle_unit_cannot_unload_at_destination(self): def test_distribution_unit_initial_state(self): env = build_env("case_02", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # try to find first vehicle unit of Supplier - dist_unit: DistributionUnit - dest_facility: FacilityBase + dist_unit: Optional[DistributionUnit] = None + dest_facility: Optional[FacilityBase] = None - for id, info in env.summary["node_mapping"]["facilities"].items(): + for _, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": - dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) + dist_unit = env.business_engine.world.get_entity(info["units"]["distribution"]["id"]) if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) + self.assertTrue(all([dist_unit is not None, dest_facility is not None])) self.assertEqual(0, len(dist_unit.order_queue)) @@ -1145,17 +1164,19 @@ def test_distribution_unit_initial_state(self): def test_distribution_unit_dispatch_order(self): env = build_env("case_02", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # try to find first vehicle unit of Supplier - dist_unit: DistributionUnit - dest_facility: FacilityBase + dist_unit: Optional[DistributionUnit] = None + dest_facility: Optional[FacilityBase] = None - for id, info in env.summary["node_mapping"]["facilities"].items(): + for _, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Supplier_SKU3": - dist_unit = env._business_engine.world.get_entity(info["units"]["distribution"]["id"]) + dist_unit = env.business_engine.world.get_entity(info["units"]["distribution"]["id"]) if info["name"] == "Warehouse_001": - dest_facility = env._business_engine.world.get_facility_by_id(info["id"]) + dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) + self.assertTrue(all([dist_unit is not None, dest_facility is not None])) first_vehicle: VehicleUnit = dist_unit.vehicles[0] @@ -1217,19 +1238,17 @@ def test_distribution_unit_dispatch_order(self): def test_seller_unit_initial_states(self): env = build_env("case_02", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # find seller for sku3 from retailer facility - sell_unit: SellerUnit - source_facility: FacilityBase + sell_unit: Optional[SellerUnit] = None - for id, info in env.summary["node_mapping"]["facilities"].items(): + for _, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) - - if info["name"] == "Warehouse_001": - source_facility = env._business_engine.world.get_facility_by_id(info["id"]) + sell_unit = env.business_engine.world.get_entity(pdetail["seller"]["id"]) + self.assertTrue(sell_unit is not None) # from configuration self.assertEqual(10, sell_unit.gamma) @@ -1262,21 +1281,19 @@ def test_seller_unit_initial_states(self): def test_seller_unit_demand_states(self): env = build_env("case_02", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # find seller for sku3 from retailer facility - sell_unit: SellerUnit - source_facility: FacilityBase + sell_unit: Optional[SellerUnit] = None - for id, info in env.summary["node_mapping"]["facilities"].items(): + for _, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) + sell_unit = env.business_engine.world.get_entity(pdetail["seller"]["id"]) + self.assertTrue(sell_unit is not None) - if info["name"] == "Warehouse_001": - source_facility = env._business_engine.world.get_facility_by_id(info["id"]) - - SKU3_INIT_NUMBER = sell_unit.facility.skus[SKU3_ID].init_stock + sku3_init_number = sell_unit.facility.skus[SKU3_ID].init_stock env.step(None) @@ -1287,7 +1304,7 @@ def test_seller_unit_demand_states(self): # demand should be same with original self.assertEqual(demand, sell_unit.data_model.demand) - actual_sold = min(demand, SKU3_INIT_NUMBER) + actual_sold = min(demand, sku3_init_number) # sold may be not same as demand, depend on remaining number in storage self.assertEqual(actual_sold, sell_unit.sold) self.assertEqual(actual_sold, sell_unit.data_model.sold) @@ -1309,7 +1326,7 @@ def test_seller_unit_demand_states(self): # demand should be same with original self.assertEqual(demand, sell_unit.data_model.demand) - actual_sold_2 = min(demand, SKU3_INIT_NUMBER - actual_sold) + actual_sold_2 = min(demand, sku3_init_number - actual_sold) # sold may be not same as demand, depend on remaining number in storage self.assertEqual(actual_sold_2, sell_unit.sold) @@ -1326,19 +1343,17 @@ def test_seller_unit_demand_states(self): def test_seller_unit_customized(self): env = build_env("case_03", 100) + assert isinstance(env.business_engine, SupplyChainBusinessEngine) # find seller for sku3 from retailer facility - sell_unit: SellerUnit - source_facility: FacilityBase + sell_unit: Optional[SellerUnit] = None - for id, info in env.summary["node_mapping"]["facilities"].items(): + for _, info in env.summary["node_mapping"]["facilities"].items(): if info["name"] == "Retailer_001": for pid, pdetail in info["units"]["products"].items(): if pdetail["sku_id"] == SKU3_ID: - sell_unit = env._business_engine.world.get_entity(pdetail["seller"]["id"]) - - if info["name"] == "Warehouse_001": - source_facility = env._business_engine.world.get_facility_by_id(info["id"]) + sell_unit = env.business_engine.world.get_entity(pdetail["seller"]["id"]) + self.assertTrue(sell_unit is not None) # NOTE: # this simple seller unit return demands that same as current tick @@ -1379,81 +1394,6 @@ def test_seller_unit_customized(self): # total sold will keep same after tick 4 self.assertListEqual([0, 1, 3, 6, 10] + [10] * 95, list(total_sold_states)) - def test_outer_seller(self): - env = build_env("case_04", 100) - - index2unitid_mapping = {} - skuid2index_mapping = {} - - # we only have one manufacture here - man_id = None - - # find all the sellers - for unit_id, unit_detail in env.summary["node_mapping"]["unit_mapping"].items(): - if unit_detail[0] == "seller": - index = unit_detail[1] - sku = unit_detail[3] - index2unitid_mapping[index] = unit_id - skuid2index_mapping[sku.id] = index - - if unit_detail[0] == "manufacture": - man_id = unit_id - - # tick 0 - env.step(None) - - seller_states = env.snapshot_list["seller"][0::"demand"].flatten().astype(np.int) - - # at tick 0 (2010-12-01) - # sku1 have 10 demand - self.assertEqual(10, seller_states[skuid2index_mapping[SKU1_ID]]) - - # sku2 have 20 demand - self.assertEqual(20, seller_states[skuid2index_mapping[SKU2_ID]]) - - # sku3 have 30 demand - self.assertEqual(30, seller_states[skuid2index_mapping[SKU3_ID]]) - - # tick 1 - env.step(None) - - seller_states = env.snapshot_list["seller"][1::"demand"].flatten().astype(np.int) - - # at tick 1 (2010-12-02) - # sku1 have 0 demand (no record in file) - self.assertEqual(0, seller_states[skuid2index_mapping[SKU1_ID]]) - - # sku2 have 20 demand - self.assertEqual(21, seller_states[skuid2index_mapping[SKU2_ID]]) - - # sku3 have 30 demand - self.assertEqual(31, seller_states[skuid2index_mapping[SKU3_ID]]) - - # tick 2 - env.step(None) - - seller_states = env.snapshot_list["seller"][2::"demand"].flatten().astype(np.int) - - # at tick 2 (2010-12-03) - # sku1 have 13 demand - self.assertEqual(13, seller_states[skuid2index_mapping[SKU1_ID]]) - - # sku2 have 20 demand - self.assertEqual(22, seller_states[skuid2index_mapping[SKU2_ID]]) - - # sku3 have 30 demand - self.assertEqual(0, seller_states[skuid2index_mapping[SKU3_ID]]) - - # test if simple manufacture work correctly. - - action = ManufactureAction(man_id, 10) - - env.step({man_id: action}) - - man_states = env.snapshot_list["manufacture"][env.tick::"manufacturing_number"].flatten().astype(np.int) - - self.assertEqual(action.production_rate, man_states[0]) - if __name__ == '__main__': unittest.main() From ea7fdde034b676b58824e8591e45c27f2e4adf10 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sat, 9 Oct 2021 07:20:12 +0000 Subject: [PATCH 453/482] refined workflow scripts --- examples/rl/cim/env_sampler.py | 2 +- examples/rl/{workflows => }/config.yml | 23 +- .../rl/scripts/docker/docker_compose_yml.py | 169 +++++++---- examples/rl/workflows/general.py | 24 -- examples/rl/workflows/grad_worker.py | 41 --- examples/rl/workflows/learning_loop.py | 78 ----- examples/rl/workflows/policy_host.py | 39 --- examples/rl/workflows/policy_manager.py | 81 ------ examples/rl/workflows/rollout.py | 45 --- maro/rl/learning/__init__.py | 7 +- maro/rl/learning/env_sampler.py | 223 ++++++++------ maro/rl/learning/helpers.py | 34 --- maro/rl/learning/learning_loop.py | 127 -------- maro/rl/learning/policy_manager.py | 274 ++++-------------- maro/rl/learning/rollout_manager.py | 10 +- maro/rl/policy/pg.py | 1 + maro/rl/workflows/__init__.py | 2 + maro/rl/workflows/grad_worker.py | 72 +++++ maro/rl/workflows/helpers.py | 47 +++ maro/rl/workflows/main.py | 122 ++++++++ maro/rl/workflows/policy_host.py | 98 +++++++ maro/rl/workflows/policy_manager.py | 84 ++++++ maro/rl/workflows/rollout.py | 43 +++ maro/rl/workflows/rollout_manager.py | 54 ++++ 24 files changed, 837 insertions(+), 863 deletions(-) rename examples/rl/{workflows => }/config.yml (53%) delete mode 100644 examples/rl/workflows/general.py delete mode 100644 examples/rl/workflows/grad_worker.py delete mode 100644 examples/rl/workflows/learning_loop.py delete mode 100644 examples/rl/workflows/policy_host.py delete mode 100644 examples/rl/workflows/policy_manager.py delete mode 100644 examples/rl/workflows/rollout.py delete mode 100644 maro/rl/learning/learning_loop.py create mode 100644 maro/rl/workflows/__init__.py create mode 100644 maro/rl/workflows/grad_worker.py create mode 100644 maro/rl/workflows/helpers.py create mode 100644 maro/rl/workflows/main.py create mode 100644 maro/rl/workflows/policy_host.py create mode 100644 maro/rl/workflows/policy_manager.py create mode 100644 maro/rl/workflows/rollout.py create mode 100644 maro/rl/workflows/rollout_manager.py diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 80d951543..82a7cc7cd 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -112,5 +112,5 @@ def get_env_sampler(): get_policy_func_dict=policy_func_dict, agent2policy=agent2policy, reward_eval_delay=reward_shaping_conf["time_window"], - policies_to_parallelize=[] + parallel_inference=True ) diff --git a/examples/rl/workflows/config.yml b/examples/rl/config.yml similarity index 53% rename from examples/rl/workflows/config.yml rename to examples/rl/config.yml index cf831283e..753ba15f4 100644 --- a/examples/rl/workflows/config.yml +++ b/examples/rl/config.yml @@ -1,14 +1,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -job: cim +job: cim4 +scenario_dir: "/maro/examples" scenario: cim -checkpoint_dir: "/maro/rl_examples/checkpoints" -mode: sync # single, sync, async -num_episodes: 5 +load_policy_dir: "/maro/examples/checkpoints/cim4" +checkpoint_dir: "/maro/examples/checkpoints/cim4" +log_dir: "/maro/examples/logs/cim4" +mode: single # single, sync, async +num_episodes: 10 eval_schedule: 5 -num_steps: -1 -max_lag: 0 sync: num_rollouts: 3 num_eval_rollouts: 1 @@ -20,13 +21,9 @@ sync: async: num_rollouts: 3 policy_manager: - type: simple # simple, multi-process, distributed + type: distributed # simple, multi-process, distributed distributed: num_hosts: 2 -data_parallel: - enable: true - num_workers: 2 - allocation_mode: by-policy # by-policy, by-agent, by-experience redis: - host: redis-server - port: 6379 + host: maro + port: 6379 \ No newline at end of file diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index af958fc42..eb5205c40 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -14,38 +14,79 @@ namespace = args.namespace docker_script_dir = dirname(realpath(__file__)) - rl_example_dir = dirname(dirname(docker_script_dir)) - root_dir = dirname(dirname(rl_example_dir)) - workflow_dir = join(rl_example_dir, "workflows") + example_dir = dirname(dirname(docker_script_dir)) + root_dir = dirname(dirname(example_dir)) maro_rl_dir = join(root_dir, "maro", "rl") - with open(join(workflow_dir, "config.yml"), "r") as fp: + with open(join(example_dir, "config.yml"), "r") as fp: config = yaml.safe_load(fp) common_spec = { "build": {"context": root_dir, "dockerfile": join(root_dir, "docker_files", "dev.df")}, "image": "marorl", "volumes": [ - f"{rl_example_dir}:/maro/rl_examples", + f"{example_dir}:/maro/examples", f"{maro_rl_dir}:/maro/maro/rl" ] } + workflow_dir_in_container = "/maro/maro/rl/workflows" + # get script paths + if "scripts" in config and "main" in config["scripts"]: + main_path = config["scripts"]["main"] + else: + main_path = join(workflow_dir_in_container, "main.py") + + if "scripts" in config and "rollout_worker" in config["scripts"]: + rollout_worker_path = config["scripts"]["rollout_worker"] + else: + rollout_worker_path = join(workflow_dir_in_container, "rollout.py") + + if "scripts" in config and "policy_host" in config["scripts"]: + policy_host_path = config["scripts"]["policy_host"] + else: + policy_host_path = join(workflow_dir_in_container, "policy_host.py") + + if "scripts" in config and "policy_server" in config["scripts"]: + policy_server_path = config["scripts"]["policy_server"] + else: + policy_server_path = join(workflow_dir_in_container, "policy_manager.py") + + if "scripts" in config and "actor" in config["scripts"]: + actor_path = config["scripts"]["actor"] + else: + actor_path = join(workflow_dir_in_container, "rollout.py") + + if "scripts" in config and "grad_worker" in config["scripts"]: + grad_worker_path = config["scripts"]["grad_worker"] + else: + grad_worker_path = join(workflow_dir_in_container, "grad_worker.py") + if config["mode"] == "single": + envs = [ + f"JOB={config['job']}", + f"SCENARIODIR={config['scenario_dir']}", + f"SCENARIO={config['scenario']}", + f"MODE=single", + f"NUMEPISODES={config['num_episodes']}", + f"EVALSCH={config['eval_schedule']}" + ] + if "num_steps" in config: + envs.append(f"NUMSTEPS={config['num_steps']}") + if "load_policy_dir" in config: + envs.append(f"LOADDIR={config['load_policy_dir']}") + if "checkpoint_dir" in config: + envs.append(f"CHECKPOINTDIR={config['checkpoint_dir']}") + if "log_dir" in config: + envs.append(f"LOGDIR={config['log_dir']}") + docker_compose_manifest = {"services": { "main": { **common_spec, **{ "container_name": f"{namespace}.main", - "command": "python3 /maro/rl_examples/workflows/learning_loop.py", - "environment": [ - f"JOB={config['job']}", - f"SCENARIO={config['scenario']}", - f"MODE=single", - f"NUMEPISODES={config['num_episodes']}", - f"NUMSTEPS={config['num_steps']}", - f"EVALSCH={config['eval_schedule']}" - ] + "command": f"python3 {main_path}", + "environment": envs } } }} @@ -57,35 +98,39 @@ } policy_group = "-".join([config['job'], 'policies']) - common_env = [ + common_envs = [ f"REDISHOST={namespace}.{redis_host}", f"REDISPORT={config['redis']['port']}", f"JOB={config['job']}", + f"SCENARIODIR={config['scenario_dir']}", f"SCENARIO={config['scenario']}", f"MODE={config['mode']}", - f"POLICYMANAGERTYPE={config['policy_manager']['type']}", - f"CHECKPOINTDIR={config['checkpoint_dir']}", - f"POLICYGROUP={policy_group}" + f"POLICYMANAGERTYPE={config['policy_manager']['type']}" ] - common_env.append(f"NUMROLLOUTS={config[config['mode']]['num_rollouts']}") - common_env.append(f"DATAPARALLEL={config['data_parallel']['enable']}") - common_env.append(f"DISTRIBUTED={config['policy_manager']['type'] == 'distributed'}") - if config["data_parallel"]["enable"]: - common_env.append(f"NUMGRADWORKERS={config['data_parallel']['num_workers']}") - common_env.append(f"ALLOCATIONMODE={config['data_parallel']['allocation_mode']}") + if "load_policy_dir" in config: + common_envs.append(f"LOADDIR={config['load_policy_dir']}") + if "checkpoint_dir" in config: + common_envs.append(f"CHECKPOINTDIR={config['checkpoint_dir']}") + if "log_dir" in config: + common_envs.append(f"LOGDIR={config['log_dir']}") + if "data_parallel" in config: + common_envs.append(f"DATAPARALLEL=1") + common_envs.append(f"NUMGRADWORKERS={config['data_parallel']['num_workers']}") + common_envs.append(f"ALLOCATIONMODE={config['data_parallel']['allocation_mode']}") if config["policy_manager"]["type"] == "distributed": - common_env.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") + common_envs.append(f"POLICYGROUP={policy_group}") + common_envs.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") # grad worker config - if config["data_parallel"]["enable"]: + if "data_parallel" in config: for worker_id in range(config['data_parallel']['num_workers']): str_id = f"grad_worker.{worker_id}" grad_worker_spec = deepcopy(common_spec) del grad_worker_spec["build"] - grad_worker_spec["command"] = "python3 /maro/rl_examples/workflows/grad_worker.py" + grad_worker_spec["command"] = f"python3 {grad_worker_path}" grad_worker_spec["container_name"] = f"{namespace}.{str_id}" - grad_worker_spec["environment"] = [f"WORKERID={worker_id}"] + common_env + grad_worker_spec["environment"] = [f"WORKERID={worker_id}"] + common_envs docker_compose_manifest["services"][str_id] = grad_worker_spec # host spec @@ -94,32 +139,35 @@ str_id = f"policy_host.{host_id}" host_spec = deepcopy(common_spec) del host_spec["build"] - host_spec["command"] = "python3 /maro/rl_examples/workflows/policy_host.py" + host_spec["command"] = f"python3 {policy_host_path}" host_spec["container_name"] = f"{namespace}.{str_id}" - host_spec["environment"] = [f"HOSTID={host_id}"] + common_env + host_spec["environment"] = [f"HOSTID={host_id}"] + common_envs docker_compose_manifest["services"][str_id] = host_spec mode = config["mode"] if mode == "sync": # main process spec rollout_group = "-".join([config['job'], 'rollout']) + envs = [ + f"ROLLOUTTYPE={config['sync']['rollout_type']}", + f"NUMEPISODES={config['num_episodes']}", + f"EVALSCH={config['eval_schedule']}", + f"NUMROLLOUTS={config['sync']['num_rollouts']}", + f"NUMEVALROLLOUTS={config['sync']['num_eval_rollouts']}", + f"ROLLOUTGROUP={rollout_group}", + f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", + f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", + f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}", + ] + if "num_steps" in config: + envs.append(f"NUMSTEPS={config['num_steps']}") + docker_compose_manifest["services"]["main"] = { **common_spec, **{ "container_name": f"{namespace}.main", - "command": "python3 /maro/rl_examples/workflows/learning_loop.py", - "environment": [ - f"ROLLOUTTYPE={config['sync']['rollout_type']}", - f"NUMEPISODES={config['num_episodes']}", - f"NUMSTEPS={config['num_steps']}", - f"EVALSCH={config['eval_schedule']}", - f"NUMEVALROLLOUTS={config[config['mode']]['num_eval_rollouts']}", - f"ROLLOUTGROUP={rollout_group}", - f"MAXLAG={config['max_lag']}", - f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", - f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", - f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}", - ] + common_env + "command": f"python3 {main_path}", + "environment": envs + common_envs } } # rollout worker spec @@ -128,41 +176,42 @@ str_id = f"rollout_worker.{worker_id}" worker_spec = deepcopy(common_spec) del worker_spec["build"] - worker_spec["command"] = "python3 /maro/rl_examples/workflows/rollout.py" + worker_spec["command"] = f"python3 {rollout_worker_path}" worker_spec["container_name"] = f"{namespace}.{str_id}" - worker_spec["environment"] = [ - f"WORKERID={worker_id}", - f"ROLLOUTGROUP={rollout_group}" - ] + common_env + worker_spec["environment"] = [f"WORKERID={worker_id}", f"ROLLOUTGROUP={rollout_group}"] + common_envs docker_compose_manifest["services"][str_id] = worker_spec elif mode == "async": # policy server spec + envs = [ + f"GROUP={config['job']}", + f"NUMROLLOUTS={config['async']['num_rollouts']}" + ] + if "max_lag" in config["async"]: + envs.append(f"MAXLAG={config['async']['max_lag']}") docker_compose_manifest["services"]["policy_server"] = { - **common_spec, + **common_spec, **{ "container_name": f"{namespace}.policy_server", - "command": "python3 /maro/rl_examples/workflows/policy_manager.py", - "environment": [ - f"GROUP={config['job']}", - f"MAXLAG={config['max_lag']}" - ] + common_env + "command": f"python3 {policy_server_path}", + "environment": envs + common_envs } } # actor spec - for actor_id in range(config["async"]["num_actors"]): + for actor_id in range(config["async"]["num_rollouts"]): str_id = f"actor.{actor_id}" actor_spec = deepcopy(common_spec) del actor_spec["build"] - actor_spec["command"] = "python3 /maro/rl_examples/workflows/rollout.py" + actor_spec["command"] = f"python3 {actor_path}" actor_spec["container_name"] = f"{namespace}.{str_id}" actor_spec["environment"] = [ f"ACTORID={actor_id}", f"GROUP={config['job']}", - f"NUMEPISODES={config['num_episodes']}", - f"NUMSTEPS={config['num_steps']}" - ] + common_env + f"NUMEPISODES={config['num_episodes']}" + ] + common_envs + if "num_steps" in config: + actor_spec["environment"].append(f"NUMSTEPS={config['num_steps']}") docker_compose_manifest["services"][str_id] = actor_spec - else: + else: raise ValueError(f"mode must be 'sync' or 'async', got {mode}") with open(join(docker_script_dir, "yq.yml"), "w") as fp: diff --git a/examples/rl/workflows/general.py b/examples/rl/workflows/general.py deleted file mode 100644 index c14e7aca7..000000000 --- a/examples/rl/workflows/general.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import sys -from os import getenv -from os.path import dirname, join, realpath - -workflow_dir = dirname(realpath(__file__)) -rl_example_dir = dirname(workflow_dir) - -if rl_example_dir not in sys.path: - sys.path.insert(0, rl_example_dir) - -log_dir = join(rl_example_dir, "log", getenv("JOB", "")) - -module = importlib.import_module(f"{getenv('SCENARIO')}") - -policy_func_dict = getattr(module, "policy_func_dict") -agent2policy = getattr(module, "agent2policy") -get_env_sampler = getattr(module, "get_env_sampler") -post_collect = getattr(module, "post_collect", None) -post_evaluate = getattr(module, "post_evaluate", None) -agent2policy = getattr(module, "agent2policy", None) diff --git a/examples/rl/workflows/grad_worker.py b/examples/rl/workflows/grad_worker.py deleted file mode 100644 index 8d38b7fa6..000000000 --- a/examples/rl/workflows/grad_worker.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os import getenv -from os.path import dirname, realpath - -from maro.rl.learning import grad_worker - -workflow_dir = dirname(dirname(realpath(__file__))) # template directory -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from general import log_dir, policy_func_dict - - -if __name__ == "__main__": - # TODO: WORKERID in docker compose script. - worker_id = getenv("WORKERID") - num_hosts = getenv("NUMHOSTS") - distributed = getenv("DISTRIBUTED") == "True" - if worker_id is None: - raise ValueError("missing environment variable: WORKERID") - if num_hosts is None: - if distributed: - raise ValueError("missing environment variable: NUMHOSTS") - else: - num_hosts = 0 - - group = getenv("POLICYGROUP", default="learn") - grad_worker( - policy_func_dict, - int(worker_id), - int(num_hosts), - group, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir - ) diff --git a/examples/rl/workflows/learning_loop.py b/examples/rl/workflows/learning_loop.py deleted file mode 100644 index 6e522ee5a..000000000 --- a/examples/rl/workflows/learning_loop.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os import getenv -from os.path import dirname, realpath - -from maro.rl.learning import DistributedRolloutManager, MultiProcessRolloutManager, learn - -workflow_dir = dirname(dirname((realpath(__file__)))) -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from general import post_collect, post_evaluate, get_env_sampler, log_dir - - -def get_rollout_manager(): - rollout_type = getenv("ROLLOUTTYPE", default="simple") - num_steps = int(getenv("NUMSTEPS", default=-1)) - if rollout_type == "multi-process": - return MultiProcessRolloutManager( - get_env_sampler, - num_steps=num_steps, - num_rollouts=int(getenv("NUMROLLOUTS", default="1")), - num_eval_rollouts=int(getenv("NUMEVALROLLOUTS", default="1")), - log_dir=log_dir - ) - - num_workers = int(getenv("NUMROLLOUTS", default=5)) - num_eval_workers = int(getenv("NUMEVALROLLOUTS", default=1)) - max_lag = int(getenv("MAXLAG", default=0)) - min_finished_workers = getenv("MINFINISH") - if min_finished_workers is not None: - min_finished_workers = int(min_finished_workers) - - max_extra_recv_tries = getenv("MAXEXRECV") - if max_extra_recv_tries is not None: - max_extra_recv_tries = int(max_extra_recv_tries) - - extra_recv_timeout = getenv("MAXRECVTIMEO") - if extra_recv_timeout is not None: - extra_recv_timeout = int(extra_recv_timeout) - - if rollout_type == "distributed": - return DistributedRolloutManager( - getenv("ROLLOUTGROUP", default="rollout"), - num_workers, - num_eval_workers=num_eval_workers, - num_steps=num_steps, - max_lag=max_lag, - min_finished_workers=min_finished_workers, - max_extra_recv_tries=max_extra_recv_tries, - extra_recv_timeout=extra_recv_timeout, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - ) - - raise ValueError(f"Unsupported roll-out type: {rollout_type}. Supported: simple, distributed") - - -if __name__ == "__main__": - num_episodes = getenv("NUMEPISODES") - if num_episodes is None: - raise ValueError("Missing environment variable: NUMEPISODES") - - if getenv("MODE") != "single": - from policy_manager import get_policy_manager - learn( - get_rollout_manager if getenv("MODE") != "single" else get_env_sampler, - int(num_episodes), - get_policy_manager=get_policy_manager if getenv("MODE") != "single" else None, - eval_schedule=int(getenv("EVALSCH")), - post_collect=post_collect, - post_evaluate=post_evaluate, - log_dir=log_dir - ) diff --git a/examples/rl/workflows/policy_host.py b/examples/rl/workflows/policy_host.py deleted file mode 100644 index 83a44fa3d..000000000 --- a/examples/rl/workflows/policy_host.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os import getenv -from os.path import dirname, realpath - -from maro.rl.learning import policy_host - -workflow_dir = dirname(dirname(realpath(__file__))) # template directory -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from general import log_dir, policy_func_dict - - -if __name__ == "__main__": - host_id = getenv("HOSTID") - data_parallel = getenv("DATAPARALLEL") == "True" - num_grad_workers = getenv("NUMGRADWORKERS") - - if host_id is None: - raise ValueError("missing environment variable: HOSTID") - if num_grad_workers is None: - num_grad_workers = 0 - - group = getenv("POLICYGROUP", default="learn") - policy_host( - policy_func_dict, - int(host_id), - group, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - data_parallel=data_parallel, - num_grad_workers=int(num_grad_workers), - log_dir=log_dir - ) diff --git a/examples/rl/workflows/policy_manager.py b/examples/rl/workflows/policy_manager.py deleted file mode 100644 index 19a7c058d..000000000 --- a/examples/rl/workflows/policy_manager.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os import getenv, makedirs -from os.path import dirname, join, realpath - -from maro.rl.learning import DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager -from maro.rl.policy import WorkerAllocator - -workflow_dir = dirname((realpath(__file__))) # template directory -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from general import agent2policy, log_dir, policy_func_dict - -checkpoint_dir = join(getenv("CHECKPOINTDIR"), getenv("JOB")) -makedirs(checkpoint_dir, exist_ok=True) - -def get_policy_manager(): - manager_type = getenv("POLICYMANAGERTYPE", default="simple") - data_parallel = getenv("DATAPARALLEL") == "True" - num_grad_workers = int(getenv("NUMGRADWORKERS", default=1)) - allocation_mode = getenv("ALLOCATIONMODE", default="by-policy") - if data_parallel: - allocator = WorkerAllocator(allocation_mode, num_grad_workers, list(policy_func_dict.keys()), agent2policy) - else: - allocator = None - proxy_kwargs = { - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - } - if manager_type == "simple": - return SimplePolicyManager( - policy_func_dict, - load_path_dict={id_: join(checkpoint_dir, id_) for id_ in policy_func_dict}, - checkpoint_every=7, - save_dir=checkpoint_dir, - worker_allocator=allocator, - group=getenv("POLICYGROUP"), - proxy_kwargs=proxy_kwargs, - log_dir=log_dir - ) - elif manager_type == "multi-process": - return MultiProcessPolicyManager( - policy_func_dict, - load_path_dict={id_: join(checkpoint_dir, id_) for id_ in policy_func_dict}, - auto_checkpoint=True, - save_dir=checkpoint_dir, - worker_allocator=allocator, - group=getenv("POLICYGROUP"), - proxy_kwargs=proxy_kwargs, - log_dir=log_dir - ) - elif manager_type == "distributed": - num_hosts = int(getenv("NUMHOSTS", default=5)) - return DistributedPolicyManager( - list(policy_func_dict.keys()), num_hosts, - group=getenv("POLICYGROUP"), - worker_allocator=allocator, - proxy_kwargs=proxy_kwargs, - log_dir=log_dir - ) - - raise ValueError( - f"Unsupported policy manager type: {manager_type}. Supported modes: simple, multi-process, distributed" - ) - - -if __name__ == "__main__": - policy_manager = get_policy_manager() - policy_manager.server( - getenv("GROUP", default="ASYNC"), - int(getenv("NUMROLLOUTS", default=5)), - max_lag=int(getenv("MAXLAG", default=0)), - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir - ) diff --git a/examples/rl/workflows/rollout.py b/examples/rl/workflows/rollout.py deleted file mode 100644 index 4d8504efa..000000000 --- a/examples/rl/workflows/rollout.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os import getenv -from os.path import dirname, realpath - -workflow_dir = dirname(dirname(realpath(__file__))) # template directory -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from general import get_env_sampler, log_dir - - -if __name__ == "__main__": - index = getenv("WORKERID") - if index is None: - raise ValueError("Missing environment variable: WORKERID") - index = int(index) - - env_sampler = get_env_sampler() - if getenv("MODE") == "sync": - env_sampler.worker( - getenv("ROLLOUTGROUP", default="rollout"), index, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir - ) - else: - num_episodes = getenv("NUMEPISODES") - if num_episodes is None: - raise ValueError("Missing envrionment variable: NUMEPISODES") - num_episodes = int(num_episodes) - num_steps = int(getenv("NUMSTEPS", default=-1)) - env_sampler.actor( - getenv("GROUP", default="ASYNC"), index, num_episodes, - num_steps=num_steps, - proxy_kwargs={ - "redis_address": (getenv("REDISHOST", default="maro-redis"), int(getenv("REDISPORT", default=6379))), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir, - ) diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py index f80dc21e5..388b016eb 100644 --- a/maro/rl/learning/__init__.py +++ b/maro/rl/learning/__init__.py @@ -2,16 +2,11 @@ # Licensed under the MIT license. from .env_sampler import AbsEnvSampler -from .learning_loop import learn -from .policy_manager import ( - AbsPolicyManager, DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager, grad_worker, policy_host -) +from .policy_manager import AbsPolicyManager, DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, MultiProcessRolloutManager __all__ = [ "AbsEnvSampler", - "learn", "AbsPolicyManager", "DistributedPolicyManager", "MultiProcessPolicyManager", "SimplePolicyManager", - "grad_worker", "policy_host", "AbsRolloutManager", "DistributedRolloutManager", "MultiProcessRolloutManager" ] diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index b6d532d1f..74cbc19a9 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -4,8 +4,8 @@ from abc import ABC, abstractmethod from collections import defaultdict, deque from multiprocessing import Pipe, Process -from os import getcwd -from typing import Callable, Dict, List +from os import getcwd, path +from typing import Callable, Dict import numpy as np @@ -18,63 +18,131 @@ from .helpers import get_rollout_finish_msg -class AgentWrapper: +class SimpleAgentWrapper: """Wrapper for multiple agents using multiple policies to expose simple single-agent interfaces.""" - def __init__( - self, - get_policy_func_dict: Dict[str, Callable], - agent2policy: Dict[str, str], - policies_to_parallelize: List[str] = [] - ): - self._policies_to_parallelize = set(policies_to_parallelize) - self.policy_dict = { - policy_id: func(policy_id) for policy_id, func in get_policy_func_dict.items() - if policy_id not in self._policies_to_parallelize - } + def __init__(self, get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str]): + self.policy_dict = {policy_id: func(policy_id) for policy_id, func in get_policy_func_dict.items()} self.agent2policy = agent2policy - self.policy_by_agent = { - agent: self.policy_dict[policy_id] for agent, policy_id in agent2policy.items() - if policy_id in self.policy_dict + self.policy_by_agent = {agent: self.policy_dict[policy_id] for agent, policy_id in agent2policy.items()} + + def load(self, dir: str): + for id_, policy in self.policy_dict.items(): + pth = path.join(dir, id_) + if path.exists(pth): + policy.load(pth) + + def choose_action(self, state_by_agent: Dict[str, np.ndarray]): + states_by_policy, agents_by_policy = defaultdict(list), defaultdict(list) + for agent, state in state_by_agent.items(): + states_by_policy[self.agent2policy[agent]].append(state) + agents_by_policy[self.agent2policy[agent]].append(agent) + + action_by_agent = {} + # compute the actions for local policies first while the inferences processes do their work. + for policy_id, policy in self.policy_dict.items(): + if states_by_policy[policy_id]: + action_by_agent.update( + zip(agents_by_policy[policy_id], policy(np.vstack(states_by_policy[policy_id]))) + ) + + return action_by_agent + + def set_policy_states(self, policy_state_dict: dict): + for policy_id, policy_state in policy_state_dict.items(): + self.policy_dict[policy_id].set_state(policy_state) + + def explore(self): + for policy in self.policy_dict.values(): + if hasattr(policy, "greedy"): + policy.greedy = False + + def exploit(self): + for policy in self.policy_dict.values(): + if hasattr(policy, "greedy"): + policy.greedy = True + + def exploration_step(self): + for policy in self.policy_dict.values(): + if hasattr(policy, "exploration_step"): + policy.exploration_step() + + def get_rollout_info(self): + return { + policy_id: policy.get_rollout_info() for policy_id, policy in self.policy_dict.items() + if isinstance(policy, RLPolicy) } + def get_exploration_params(self): + return { + policy_id: clone(policy.exploration_params) for policy_id, policy in self.policy_dict.items() + if isinstance(policy, RLPolicy) + } + + def record_transition(self, agent: str, state, action, reward, next_state, terminal: bool): + if isinstance(self.policy_by_agent[agent], RLPolicy): + self.policy_by_agent[agent].record(agent, state, action, reward, next_state, terminal) + + def improve(self, checkpoint_dir: str = None): + for id_, policy in self.policy_dict.items(): + policy.improve() + if checkpoint_dir: + policy.save(path.join(checkpoint_dir, id_)) + + +class ParallelAgentWrapper: + """Wrapper for multiple agents using multiple policies to expose simple single-agent interfaces. + + The policy instances are distributed across multiple processes to achieve parallel inference. + """ + def __init__(self, get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str]): + self.agent2policy = agent2policy self._inference_services = [] self._conn = {} - def _inference_service(name, get_policy, conn): - policy = get_policy(name) - if not isinstance(policy, RLPolicy): - raise TypeError("Only an 'RLPolicy' can be parallelized") - + def _inference_service(id_, get_policy, conn): + policy = get_policy(id_) while True: msg = conn.recv() - if msg["type"] == "choose_action": + if msg["type"] == "load": + if hasattr(policy, "load"): + policy.load(path.join(msg["dir"], id_)) + elif msg["type"] == "choose_action": actions = policy(msg["states"]) conn.send(actions) elif msg["type"] == "set_state": - policy.set_state(msg["policy_state"]) + if hasattr(policy, "set_state"): + policy.set_state(msg["policy_state"]) elif msg["type"] == "explore": - policy.greedy = False + if hasattr(policy, "greedy"): + policy.greedy = False elif msg["type"] == "exploit": - policy.greedy = True + if hasattr(policy, "greedy"): + policy.greedy = True elif msg["type"] == "exploration_step": if hasattr(policy, "exploration_step"): policy.exploration_step() elif msg["type"] == "rollout_info": - conn.send(policy.get_rollout_info()) + conn.send(policy.get_rollout_info() if hasattr(policy, "get_rollout_info") else None) elif msg["type"] == "exploration_params": - conn.send(policy.exploration_params) + conn.send(policy.exploration_params if hasattr(policy, "exploration_params") else None) elif msg["type"] == "record": - policy.record( - msg["agent"], msg["state"], msg["action"], msg["reward"], msg["next_state"], msg["terminal"] - ) + if hasattr(policy, "record"): + policy.record( + msg["agent"], msg["state"], msg["action"], msg["reward"], msg["next_state"], msg["terminal"] + ) elif msg["type"] == "update": - policy.update(msg["loss_info"]) + if hasattr(policy, "update"): + policy.update(msg["loss_info"]) elif msg["type"] == "learn": - policy.learn(msg["batch"]) + if hasattr(policy, "learn"): + policy.learn(msg["batch"]) elif msg["type"] == "improve": - policy.improve() + if hasattr(policy, "improve"): + policy.improve() + if msg["checkpoint_dir"]: + policy.save(path.join(msg["checkpoint_dir"], id_)) - for policy_id in policies_to_parallelize: + for policy_id in get_policy_func_dict: conn1, conn2 = Pipe() self._conn[policy_id] = conn1 host = Process( @@ -84,6 +152,10 @@ def _inference_service(name, get_policy, conn): self._inference_services.append(host) host.start() + def load(self, dir: str): + for conn in self._conn.items(): + conn.send({"type": "load", "dir": dir}) + def choose_action(self, state_by_agent: Dict[str, np.ndarray]): states_by_policy, agents_by_policy = defaultdict(list), defaultdict(list) for agent, state in state_by_agent.items(): @@ -96,13 +168,6 @@ def choose_action(self, state_by_agent: Dict[str, np.ndarray]): conn.send({"type": "choose_action", "states": np.vstack(states_by_policy[policy_id])}) action_by_agent = {} - # compute the actions for local policies first while the inferences processes do their work. - for policy_id, policy in self.policy_dict.items(): - if states_by_policy[policy_id]: - action_by_agent.update( - zip(agents_by_policy[policy_id], policy(np.vstack(states_by_policy[policy_id]))) - ) - for policy_id, conn in self._conn.items(): if states_by_policy[policy_id]: action_by_agent.update(zip(agents_by_policy[policy_id], conn.recv())) @@ -112,71 +177,52 @@ def choose_action(self, state_by_agent: Dict[str, np.ndarray]): def set_policy_states(self, policy_state_dict: dict): for policy_id, conn in self._conn.items(): conn.send({"type": "set_state", "policy_state": policy_state_dict[policy_id]}) - for policy_id, policy_state in policy_state_dict.items(): - if policy_id not in self._conn: - self.policy_dict[policy_id].set_state(policy_state) def explore(self): for conn in self._conn.values(): conn.send({"type": "explore"}) - for policy in self.policy_dict.values(): - if hasattr(policy, "greedy"): - policy.greedy = False def exploit(self): for conn in self._conn.values(): conn.send({"type": "exploit"}) - for policy in self.policy_dict.values(): - if hasattr(policy, "greedy"): - policy.greedy = True def exploration_step(self): for conn in self._conn.values(): conn.send({"type": "exploration_step"}) - for policy in self.policy_dict.values(): - if hasattr(policy, "exploration_step"): - policy.exploration_step() def get_rollout_info(self): + rollout_info = {} for conn in self._conn.values(): conn.send({"type": "rollout_info"}) - return { - **{ - policy_id: policy.get_rollout_info() for policy_id, policy in self.policy_dict.items() - if isinstance(policy, RLPolicy) - }, - **{policy_id: conn.recv() for policy_id, conn in self._conn.items()} - } + for policy_id, conn in self._conn.items(): + info = conn.recv() + if info: + rollout_info[policy_id] = info + + return rollout_info def get_exploration_params(self): + exploration_params = {} for conn in self._conn.values(): conn.send({"type": "exploration_params"}) - return { - **{ - policy_id: clone(policy.exploration_params) for policy_id, policy in self.policy_dict.items() - if isinstance(policy, RLPolicy) - }, - **{policy_id: conn.recv() for policy_id, conn in self._conn.items()} - } + for policy_id, conn in self._conn.items(): + params = conn.recv() + if params: + exploration_params[policy_id] = params + + return exploration_params def record_transition(self, agent: str, state, action, reward, next_state, terminal: bool): - if agent in self.policy_by_agent: - if isinstance(self.policy_by_agent[agent], RLPolicy): - self.policy_by_agent[agent].record(agent, state, action, reward, next_state, terminal) - else: - self._conn[self.agent2policy[agent]].send({ - "type": "record", "agent": agent, "state": state, "action": action, "reward": reward, - "next_state": next_state, "terminal": terminal - }) - - def improve(self): - for conn in self._conn.values(): - conn.send({"type": "improve"}) + self._conn[self.agent2policy[agent]].send({ + "type": "record", "agent": agent, "state": state, "action": action, "reward": reward, + "next_state": next_state, "terminal": terminal + }) - for policy in self.policy_dict.values(): - policy.improve() + def improve(self, checkpoint_dir: str = None): + for conn in self._conn.values(): + conn.send({"type": "improve", "checkpoint_dir": checkpoint_dir}) class AbsEnvSampler(ABC): @@ -195,9 +241,9 @@ class AbsEnvSampler(ABC): reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately after executing an action. - policies_to_parallelize (List[str]): Policies to be placed in separate processes so that inference can be - performed in parallel to speed up simulation. This is useful if some policies are big and takes long times - to compute actions. Defaults to an empty list. + parallel_inference (bool): If True, the policies will be placed in separate processes so that inference can be + performed in parallel to speed up simulation. This is useful if some policies are big and take a long time + to generate actions. Defaults to False. """ def __init__( self, @@ -206,15 +252,14 @@ def __init__( agent2policy: Dict[str, str], get_test_env: Callable[[], Env] = None, reward_eval_delay: int = 0, - policies_to_parallelize: List[str] = [] + parallel_inference: bool = False ): self._learn_env = get_env() self._test_env = get_test_env() if get_test_env else self._learn_env self.env = None - self.agent_wrapper = AgentWrapper( - get_policy_func_dict, agent2policy, policies_to_parallelize=policies_to_parallelize - ) + agent_wrapper_cls = ParallelAgentWrapper if parallel_inference else SimpleAgentWrapper + self.agent_wrapper = agent_wrapper_cls(get_policy_func_dict, agent2policy) self.reward_eval_delay = reward_eval_delay self._state = None diff --git a/maro/rl/learning/helpers.py b/maro/rl/learning/helpers.py index 240eaff83..00b5e2196 100644 --- a/maro/rl/learning/helpers.py +++ b/maro/rl/learning/helpers.py @@ -1,9 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import List, Union - - def get_rollout_finish_msg(ep, step_range, exploration_params=None): """Generate a brief summary message for a finished roll-out""" if exploration_params: @@ -15,34 +12,3 @@ def get_rollout_finish_msg(ep, step_range, exploration_params=None): ) else: return f"Roll-out finished (episode: {ep}, step range: {step_range})" - - -def get_eval_schedule(sch: Union[int, List[int]], num_episodes: int, final: bool = True): - """Helper function to the policy evaluation schedule. - - Args: - sch (Union[int, List[int]]): Evaluation schedule. If it is an int, it is treated as the number of episodes - between two adjacent evaluations. For example, if the total number of episodes is 20 and ``sch`` is 6, - this will return [6, 12, 18] if ``final`` is False or [6, 12, 18, 20] otherwise. If it is a list, it will - return a sorted version of the list (with the last episode appended if ``final`` is True). - num_episodes (int): Total number of learning episodes. - final (bool): If True, the last episode number will be appended to the returned list to indicate that an - evaluation is required after the last episode is complete. Defaults to True. - - Returns: - A list of episodes indicating when to perform policy evaluation. - - """ - if sch is None: - schedule = [] - elif isinstance(sch, int): - num_eval_schedule = num_episodes // sch - schedule = [sch * i for i in range(1, num_eval_schedule + 1)] - else: - schedule = sorted(sch) - - # always evaluate after the last episode - if final and (not schedule or num_episodes != schedule[-1]): - schedule.append(num_episodes) - - return schedule diff --git a/maro/rl/learning/learning_loop.py b/maro/rl/learning/learning_loop.py deleted file mode 100644 index 2e25ff72d..000000000 --- a/maro/rl/learning/learning_loop.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import Callable, List, Union - -from maro.utils import Logger - -from .env_sampler import AbsEnvSampler -from .helpers import get_eval_schedule, get_rollout_finish_msg -from .policy_manager import AbsPolicyManager -from .rollout_manager import AbsRolloutManager - - -def learn( - get_rollout_manager: Callable[[], Union[AbsEnvSampler, AbsRolloutManager]], - num_episodes: int, - num_steps: int = -1, - get_policy_manager: Callable[[], AbsPolicyManager] = None, - eval_schedule: Union[int, List[int]] = None, - eval_after_last_episode: bool = False, - post_collect: Callable = None, - post_evaluate: Callable = None, - log_dir: str = getcwd() -): - """Run the main learning loop in single-threaded or distributed mode. - - In distributed mode, this is the main process that executes 2-phase learning cycles: simulation data collection - and policy update. The transition from one phase to another is synchronous. - - Args: - get_rollout_manager (AbsRolloutManager): Function to create an ``AbsEnvSampler`` or ``AbsRolloutManager`` - instance to control the data collecting phase of the learning cycle. The function takes no parameters - and returns an ``AbsRolloutManager``. Use this for multi-process or distributed policy training. - num_episodes (int): Number of learning episodes. The environment always runs to completion in each episode. - num_steps (int): Number of environment steps to roll out each time. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - get_policy_manager (AbsPolicyManager): Function to create an ``AbsPolicyManager`` instance to control policy - update phase of the learning cycle. The function takes no parameters and returns an ``AbsPolicyManager``. - Use this for multi-process or distributed data collection. Defaults to None. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the episodes given in the list. Defaults to None, in which case no evaluation is performed - unless ``eval_after_last_episode`` is set to True. - eval_after_last_episode (bool): If True, the policy will be evaluated after the last episode of learning is - finished. Defaults to False. - early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metric are met following an evaluation episode. Default to None. - post_collect (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``collect`` calls. The function signature should - be (trackers, ep, segment) -> None, where tracker is a list of environment wrappers' ``tracker`` members. - Defaults to None. - post_evaluate (Callable): Custom function to process whatever information is collected by each - environment wrapper (local or remote) at the end of ``evaluate`` calls. The function signature should - be (trackers, ep) -> None, where tracker is a list of environment wrappers' ``tracker`` members. Defaults - to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time - and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - rollout_manager = get_rollout_manager() - if not get_policy_manager: - assert isinstance(rollout_manager, AbsEnvSampler), \ - "'get_rollout_manager' must return an 'AbsEnvSampler' if 'get_policy_manager' is None." - - policy_manager = get_policy_manager() if get_policy_manager else None - logger = Logger("LEARNER", dump_folder=log_dir) - # evaluation schedule - eval_schedule = get_eval_schedule(eval_schedule, num_episodes, final=eval_after_last_episode) - logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") - eval_point_index = 0 - - def collect_and_update(): - collect_time = policy_update_time = 0 - if isinstance(rollout_manager, AbsRolloutManager): - rollout_manager.reset() - segment, end_of_episode = 1, False - while not end_of_episode: - # experience collection - tc0 = time.time() - if policy_manager: - policy_state_dict = policy_manager.get_state() - rollout_info_by_policy, trackers = rollout_manager.collect(ep, segment, policy_state_dict) - end_of_episode = rollout_manager.end_of_episode - else: - result = rollout_manager.sample(num_steps=num_steps, return_rollout_info=False) - trackers = [result["tracker"]] - logger.info( - get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) - ) - end_of_episode = result["end_of_episode"] - - if post_collect: - post_collect(trackers, ep, segment) - - collect_time += time.time() - tc0 - tu0 = time.time() - if policy_manager: - policy_manager.update(rollout_info_by_policy) - else: - rollout_manager.agent_wrapper.improve() - policy_update_time += time.time() - tu0 - segment += 1 - - # performance details - logger.info(f"ep {ep} summary - collect time: {collect_time}, policy update time: {policy_update_time}") - - for ep in range(1, num_episodes + 1): - collect_and_update() - if eval_schedule and ep == eval_schedule[eval_point_index]: - eval_point_index += 1 - if isinstance(rollout_manager, AbsEnvSampler): - trackers = [rollout_manager.test()] - else: - trackers = rollout_manager.evaluate(ep, policy_manager.get_state()) - if post_evaluate: - post_evaluate(trackers, ep) - - if isinstance(rollout_manager, AbsRolloutManager): - rollout_manager.exit() - - if hasattr(policy_manager, "exit"): - policy_manager.exit() diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 00a458488..36d5f3d46 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -2,12 +2,11 @@ # Licensed under the MIT license. import os -import threading import time from abc import ABC, abstractmethod from collections import defaultdict from multiprocessing import Pipe, Process -from os import getcwd +from os import getcwd, getpid from typing import Callable, Dict, List from maro.communication import Proxy, SessionMessage, SessionType @@ -98,12 +97,12 @@ class SimplePolicyManager(AbsPolicyManager): Args: create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a function that takes policy name as the only parameter and return an ``RLPolicy`` instance. - load_path_dict (Dict[str, str]): If provided, policies whose IDs are in the dictionary keys will load the states - from the corresponding path. Defaults to an empty dictionary. + load_dir (str): If provided, policies whose IDs are in the dictionary keys will load the states + from the corresponding path. Defaults to None. checkpoint_every (int): The policies will be checkpointed (i.e., persisted to disk) every this number of seconds only if there are updates since the last checkpoint. This must be a positive integer or -1, with -1 meaning no checkpointing. Defaults to -1. - save_dir (str): The directory under which to checkpoint the policy states. + checkpoint_dir (str): The directory under which to checkpoint the policy states. worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. If not None, the policies will be trained in data-parallel mode. Defaults to None. group (str): Group name for the cluster consisting of the manager and all policy hosts. Ignored if @@ -116,37 +115,30 @@ class SimplePolicyManager(AbsPolicyManager): def __init__( self, create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - load_path_dict: Dict[str, str] = {}, - checkpoint_every: int = -1, - save_dir: str = None, + load_dir: str = None, + checkpoint_dir: str = None, worker_allocator: WorkerAllocator = None, group: str = DEFAULT_POLICY_GROUP, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): - assert checkpoint_every == -1 or checkpoint_every > 0, "'checkpoint_every' can only be -1 or a positive integer" super().__init__() self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} - for id_, path in load_path_dict.items(): - try: - self._policy_dict[id_].load(path) - self._logger.info(f"Loaded policy {id_} from {path}") - except FileNotFoundError: - self._logger.warn( - f"Failed to load state for policy {id_} from path {path}..." - f"using the current policy state as the initial state" - ) + if load_dir: + for id_, policy in self._policy_dict.items(): + path = os.path.join(load_dir, id_) + if os.path.exists(path): + policy.load(path) + self._logger.info(f"Loaded policy {id_} from {path}") - self._version = defaultdict(int) + self._version = 0 # auto-checkpointing - self.checkpoint_every = checkpoint_every - self.save_path = {id_: os.path.join(save_dir, id_) for id_ in create_policy_func_dict} - if self.checkpoint_every > 0: - checkpoint_thread = threading.Thread(target=self._checkpoint, daemon=True) - checkpoint_thread.start() - self._logger.info(f"Auto-checkpoint policy state every {self.checkpoint_every} seconds") + if checkpoint_dir: + self.checkpoint_path = {id_: os.path.join(checkpoint_dir, id_) for id_ in self._policy_dict} + else: + self.checkpoint_path = None # data parallelism self._worker_allocator = worker_allocator @@ -155,7 +147,8 @@ def __init__( self._worker_allocator.set_logger(self._logger) self._proxy = Proxy( group, "policy_manager", {"grad_worker": self._num_grad_workers}, - component_name="POLICY_MANAGER", **proxy_kwargs) + component_name="POLICY_MANAGER", **proxy_kwargs + ) for name in create_policy_func_dict: self._policy_dict[name].data_parallel( @@ -174,7 +167,12 @@ def update(self, rollout_info: Dict[str, list]): self._policy_dict[policy_id].update(info) else: self._policy_dict[policy_id].learn(info) - self._version[policy_id] += 1 + + if self.checkpoint_path: + self._policy_dict[policy_id].save(self.checkpoint_path[policy_id]) + self._logger.info(f"Saved policy {policy_id} to {self.checkpoint_path[policy_id]}") + + self._version += 1 self._logger.info(f"Updated policies {list(rollout_info.keys())}") self._logger.info(f"policy update time: {time.time() - t0}") @@ -185,22 +183,7 @@ def get_state(self): def get_version(self): """Get the collective policy version.""" - return dict(self._version) - - def save(self): - for id_, policy in self._policy_dict.items(): - policy.save(self.save_path[id_]) - self._logger.info(f"Saved policy {id_} to {self.save_path[id_]}") - - def _checkpoint(self): - last_checkpointed_version = defaultdict(lambda: -1) - while True: - for id_, policy in self._policy_dict.items(): - if self._version[id_] > last_checkpointed_version[id_]: - policy.save(self.save_path[id_]) - self._logger.info(f"Saved policy {id_} to {self.save_path[id_]}") - last_checkpointed_version[id_] = self._version[id_] - time.sleep(self.checkpoint_every) + return self._version def exit(self): pass @@ -212,11 +195,10 @@ class MultiProcessPolicyManager(AbsPolicyManager): Args: create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a function that takes policy name as the only parameter and return an ``RLPolicy`` instance. - load_path_dict (Dict[str, str]): If provided, policies whose IDs are in the dictionary keys will load the states - from the corresponding path. Defaults to an empty dictionary. - auto_checkpoint (bool): If True, the policies will be checkpointed (i.e., persisted to disk) every time they are - updated. Defaults to -1. - save_dir (str): The directory under which to checkpoint the policy states. + load_dir (str): If provided, policies whose IDs are in the dictionary keys will load the states + from the corresponding path. Defaults to None. + checkpoint_dir (str): The directory under which to checkpoint the policy states. Defaults to None, in which case + no checkpointing will be performed. worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. If not None, the policies will be trained in data-parallel mode. Defaults to None. group (str): Group name for the cluster consisting of the manager and all policy hosts. Ignored if @@ -229,16 +211,14 @@ class MultiProcessPolicyManager(AbsPolicyManager): def __init__( self, create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - load_path_dict: Dict[str, str] = {}, - auto_checkpoint: bool = False, - save_dir: str = None, + load_dir: Dict[str, str] = None, + checkpoint_dir: str = None, worker_allocator: WorkerAllocator = None, group: str = DEFAULT_POLICY_GROUP, proxy_kwargs: dict = {}, log_dir: str = getcwd() ): super().__init__() - self.auto_checkpoint = auto_checkpoint self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) # data parallelism @@ -257,24 +237,22 @@ def __init__( self._manager_end = {} self._logger.info("Spawning policy host processes") - def policy_host(id_, create_policy_func, conn, initial_state_path): + def policy_host(id_, create_policy_func, conn): + self._logger.info(f"Host for policy {id_} started with PID {getpid()}") policy = create_policy_func(id_) - save_path = os.path.join(save_dir, id_) + checkpoint_path = os.path.join(checkpoint_dir, id_) if checkpoint_dir else None if self._worker_allocator: self._logger.info("========== data parallel mode ==========") policy.data_parallel( group, "policy_host", {"grad_worker": self._num_grad_workers}, component_name=f"POLICY_HOST.{id_}", **proxy_kwargs) - if initial_state_path: - try: - policy.load(initial_state_path) - self._logger.info(f"Loaded policy {id_} from {initial_state_path}") - except FileNotFoundError: - self._logger.warn( - f"Failed to load state for policy {id_} from path {initial_state_path}..." - f"using the current policy state as the initial state" - ) + if load_dir: + load_path = os.path.join(load_dir, id_) if load_dir else None + if os.path.exists(load_path): + policy.load(load_path) + self._logger.info(f"Loaded policy {id_} from {load_path}") + conn.send({"type": "init", "policy_state": policy.get_state()}) while True: msg = conn.recv() @@ -288,22 +266,20 @@ def policy_host(id_, create_policy_func, conn, initial_state_path): policy.learn(info) conn.send({"type": "learn_done", "policy_state": policy.get_state()}) self._logger.info("learning finished") - if self.auto_checkpoint: - policy.save(save_path) - self._logger.info(f"Saved policy {id_} to {save_path}") + if checkpoint_path: + policy.save(checkpoint_path) + self._logger.info(f"Saved policy {id_} to {checkpoint_path}") elif msg["type"] == "quit": if self._worker_allocator: policy.exit_data_parallel() - policy.save(save_path) - self._logger.info(f"Saved policy {id_} to {save_path}") + policy.save(checkpoint_path) + self._logger.info(f"Saved policy {id_} to {checkpoint_path}") break for id_, create_policy_func in create_policy_func_dict.items(): manager_end, host_end = Pipe() self._manager_end[id_] = manager_end - host = Process( - target=policy_host, args=(id_, create_policy_func, host_end, load_path_dict.get(id_, None)) - ) + host = Process(target=policy_host, args=(id_, create_policy_func, host_end)) self._policy_hosts.append(host) host.start() @@ -313,7 +289,7 @@ def policy_host(id_, create_policy_func, conn, initial_state_path): self._state_cache[policy_id] = msg["policy_state"] self._logger.info(f"Initial state for policy {policy_id} cached") - self._version = defaultdict(int) + self._version = 0 def update(self, rollout_info: Dict[str, list]): """Update policies using roll-out information. @@ -335,7 +311,7 @@ def update(self, rollout_info: Dict[str, list]): if msg["type"] == "learn_done": self._state_cache[policy_id] = msg["policy_state"] self._logger.info(f"Cached state for policy {policy_id}") - self._version[policy_id] += 1 + self._version += 1 else: self._logger.info(f"Warning: Wrong message type: {msg['type']}") @@ -347,7 +323,7 @@ def get_state(self): def get_version(self): """Get the collective policy version.""" - return dict(self._version) + return self._version def exit(self): """Tell the policy host processes to exit.""" @@ -392,7 +368,8 @@ def __init__( self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) self._worker_allocator = worker_allocator - self._worker_allocator.set_logger(self._logger) + if self._worker_allocator: + self._worker_allocator.set_logger(self._logger) self._policy2host = {} self._policy2workers = {} @@ -461,153 +438,10 @@ def get_state(self): def get_version(self): """Get the collective policy version.""" - return dict(self._version) + return self._version def exit(self): """Tell the remote policy hosts to exit.""" self._proxy.ibroadcast("policy_host", MsgTag.EXIT, SessionType.NOTIFICATION) self._proxy.close() - self._logger.info("Exiting...") - - -def policy_host( - create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - host_idx: int, - group: str = DEFAULT_POLICY_GROUP, - proxy_kwargs: dict = {}, - data_parallel: bool = False, - num_grad_workers: int = 1, - log_dir: str = getcwd() -): - """Policy host process that can be launched on separate computation nodes. - - Args: - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. - host_idx (int): Integer host index. The host's ID in the cluster will be "POLICY_HOST.{host_idx}". - group (str): Group name for the training cluster, which includes all policy hosts and a policy manager that - manages them. Defaults to DEFAULT_POLICY_GROUP. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to an empty dictionary. - data_parallel (bool): Whether to train policy on remote gradient workers to perform data-parallel. - Defaults to False. - num_grad_workers (int): The number of gradient worker nodes in data-parallel mode. Defaults to 1. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - policy_dict = {} - peers = {"policy_manager": 1} - if data_parallel: - peers["grad_worker"] = num_grad_workers - - proxy = Proxy( - group, "policy_host", peers, - component_name=f"POLICY_HOST.{host_idx}", **proxy_kwargs) - logger = Logger(proxy.name, dump_folder=log_dir) - - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - elif msg.tag == MsgTag.INIT_POLICIES: - for name in msg.body[MsgKey.POLICY_IDS]: - policy_dict[name] = create_policy_func_dict[name](name) - if data_parallel: - policy_dict[name].data_parallel_with_existing_proxy(proxy) - - logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_IDS]}") - proxy.reply( - msg, - tag=MsgTag.INIT_POLICIES_DONE, - body={MsgKey.POLICY_STATE: {name: policy.get_state() for name, policy in policy_dict.items()}} - ) - elif msg.tag == MsgTag.LEARN: - t0 = time.time() - for name, info in msg.body[MsgKey.ROLLOUT_INFO].items(): - # in some cases e.g. Actor-Critic that get loss from rollout workers - if isinstance(info, list): - logger.info("updating with loss info") - policy_dict[name].update(info) - else: - if data_parallel: - logger.info("learning on remote grad workers") - policy2workers = msg.body[MsgKey.WORKER_INFO][name] - policy_dict[name].learn_with_data_parallel(info, policy2workers[name]) - else: - logger.info("learning from batch") - policy_dict[name].learn(info) - - msg_body = { - MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.ROLLOUT_INFO]} - } - logger.info(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, tag=MsgTag.LEARN_DONE, body=msg_body) - else: - logger.info(f"Wrong message tag: {msg.tag}") - raise TypeError - - -def grad_worker( - create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - worker_idx: int, - num_hosts: int, - group: str = DEFAULT_POLICY_GROUP, - proxy_kwargs: dict = {}, - max_policy_number: int = 10, - log_dir: str = getcwd() -): - """Stateless gradient workers that excute gradient computation tasks. - - Args: - create_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have policy name as the only parameter and return an ``RLPolicy`` instance. - worker_idx (int): Integer worker index. The worker's ID in the cluster will be "GRAD_WORKER.{worker_idx}". - num_hosts (int): Number of policy hosts, which is required to find peers in proxy initialization. - num_hosts=0 means policy hosts are hosted by policy manager while no remote nodes for them. - group (str): Group name for the training cluster, which includes all policy hosts and a policy manager that - manages them. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to an empty dictionary. - max_policy_number (int): Maximum policy number in a single worker node. Defaults to 10. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - policy_dict = {} - active_policies = [] - if num_hosts == 0: - # no remote nodes for policy hosts - num_hosts = len(create_policy_func_dict) - peers = {"policy_manager": 1, "policy_host": num_hosts} - proxy = Proxy(group, "grad_worker", peers, component_name=f"GRAD_WORKER.{worker_idx}", **proxy_kwargs) - logger = Logger(proxy.name, dump_folder=log_dir) - - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - elif msg.tag == MsgTag.COMPUTE_GRAD: - t0 = time.time() - msg_body = {MsgKey.LOSS_INFO: dict(), MsgKey.POLICY_IDS: list()} - for name, batch in msg.body[MsgKey.GRAD_TASK].items(): - if name not in policy_dict: - if len(policy_dict) > max_policy_number: - # remove the oldest one when size exceeds. - policy_to_remove = active_policies.pop() - policy_dict.pop(policy_to_remove) - policy_dict[name] = create_policy_func_dict[name](name) - active_policies.insert(0, name) - logger.info(f"Initialized policies {name}") - - policy_dict[name].set_state(msg.body[MsgKey.POLICY_STATE][name]) - loss_info = policy_dict[name].get_batch_loss(batch, explicit_grad=True) - msg_body[MsgKey.LOSS_INFO][name] = loss_info - msg_body[MsgKey.POLICY_IDS].append(name) - # put the latest one to queue head - active_policies.remove(name) - active_policies.insert(0, name) - - logger.debug(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, tag=MsgTag.COMPUTE_GRAD_DONE, body=msg_body) - else: - logger.info(f"Wrong message tag: {msg.tag}") - raise TypeError + self._logger.info("Exiting...") diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py index 611059854..e7243eb98 100644 --- a/maro/rl/learning/rollout_manager.py +++ b/maro/rl/learning/rollout_manager.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from multiprocessing import Pipe, Process -from os import getcwd +from os import getcwd, getpid from random import choices from typing import Callable, Dict, List, Tuple @@ -109,6 +109,7 @@ def _rollout_worker(index, conn, get_env_sampler): set_seeds(index) env_sampler = get_env_sampler() logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) + logger.info(f"Roll-out worker {index} started with PID {getpid()}") while True: msg = conn.recv() if msg["type"] == "sample": @@ -212,8 +213,7 @@ class DistributedRolloutManager(AbsRolloutManager): min_finished_workers (int): Minimum number of finished workers required for a ``collect`` call. Defaults to None, in which case it will be set to ``num_workers``. max_extra_recv_tries (int): Maximum number of attempts to receive worker results after ``min_finished_workers`` - have been received in ``collect``. Defaults to None, in which case it is set to ``num_workers`` - - ``min_finished_workers``. + have been received in ``collect``. Defaults to 0. extra_recv_timeout (int): Timeout (in milliseconds) for each attempt to receive from a worker after ``min_finished_workers`` have been received in ``collect``. Defaults to 100 (milliseconds). num_eval_workers (int): Number of workers for evaluation. Defaults to 1. @@ -229,8 +229,8 @@ def __init__( num_workers: int, num_steps: int = -1, min_finished_workers: int = None, - max_extra_recv_tries: int = None, - extra_recv_timeout: int = None, + max_extra_recv_tries: int = 0, + extra_recv_timeout: int = 100, max_lag: Dict[str, int] = defaultdict(int), num_eval_workers: int = 1, proxy_kwargs: dict = {}, diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py index 5d3e94299..4c51d1a0e 100644 --- a/maro/rl/policy/pg.py +++ b/maro/rl/policy/pg.py @@ -28,6 +28,7 @@ def __init__(self, state_dim, size: int = 10000): self.rewards = np.zeros(size, dtype=np.float32) self.terminals = np.zeros(size, dtype=np.bool) self.size = size + self._ptr = 0 def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): self.states[self._ptr] = state diff --git a/maro/rl/workflows/__init__.py b/maro/rl/workflows/__init__.py new file mode 100644 index 000000000..9a0454564 --- /dev/null +++ b/maro/rl/workflows/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. diff --git a/maro/rl/workflows/grad_worker.py b/maro/rl/workflows/grad_worker.py new file mode 100644 index 000000000..30d72b09a --- /dev/null +++ b/maro/rl/workflows/grad_worker.py @@ -0,0 +1,72 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import importlib +import os +import sys +import time + +from maro.communication import Proxy +from maro.rl.utils import MsgKey, MsgTag +from maro.rl.workflows.helpers import from_env, get_default_log_dir +from maro.utils import Logger + +sys.path.insert(0, from_env("SCENARIODIR")) +module = importlib.import_module(from_env("SCENARIO")) +policy_func_dict = getattr(module, "policy_func_dict") +log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) +os.makedirs(log_dir, exist_ok=True) + + +if __name__ == "__main__": + # TODO: WORKERID in docker compose script. + worker_id = from_env("WORKERID") + num_hosts = from_env("NUMHOSTS") if from_env("POLICYMANAGERTYPE") == "distributed" else 0 + max_cached_policies = from_env("MAXCACHED", required=False, default=10) + + group = from_env("POLICYGROUP") + policy_dict = {} + active_policies = [] + if num_hosts == 0: + # no remote nodes for policy hosts + num_hosts = len(policy_func_dict) + + peers = {"policy_manager": 1, "policy_host": num_hosts} + proxy = Proxy( + group, "grad_worker", peers, component_name=f"GRAD_WORKER.{worker_id}", + redis_address=(from_env("REDISHOST"), from_env("REDISPORT")), + max_peer_discovery_retries=50 + ) + logger = Logger(proxy.name, dump_folder=log_dir) + + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + elif msg.tag == MsgTag.COMPUTE_GRAD: + t0 = time.time() + msg_body = {MsgKey.LOSS_INFO: dict(), MsgKey.POLICY_IDS: list()} + for name, batch in msg.body[MsgKey.GRAD_TASK].items(): + if name not in policy_dict: + if len(policy_dict) > max_cached_policies: + # remove the oldest one when size exceeds. + policy_to_remove = active_policies.pop() + policy_dict.pop(policy_to_remove) + policy_dict[name] = policy_func_dict[name](name) + active_policies.insert(0, name) + logger.info(f"Initialized policies {name}") + + policy_dict[name].set_state(msg.body[MsgKey.POLICY_STATE][name]) + loss_info = policy_dict[name].get_batch_loss(batch, explicit_grad=True) + msg_body[MsgKey.LOSS_INFO][name] = loss_info + msg_body[MsgKey.POLICY_IDS].append(name) + # put the latest one to queue head + active_policies.remove(name) + active_policies.insert(0, name) + + logger.debug(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, tag=MsgTag.COMPUTE_GRAD_DONE, body=msg_body) + else: + logger.info(f"Wrong message tag: {msg.tag}") + raise TypeError diff --git a/maro/rl/workflows/helpers.py b/maro/rl/workflows/helpers.py new file mode 100644 index 000000000..7bb47e27d --- /dev/null +++ b/maro/rl/workflows/helpers.py @@ -0,0 +1,47 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +from typing import List, Union + + +def from_env(var_name, required=True, default=None): + if var_name not in os.environ: + if required: + raise KeyError(f"Missing environment variable: {var_name}") + else: + return default + + var = os.getenv(var_name) + return int(var) if var.isnumeric() or var[0] == "-" and var[1:].isnumeric() else var + + +def get_default_log_dir(job): + return os.path.join(os.getcwd(), "logs", job) + + +def get_eval_schedule(sch: Union[int, List[int]], num_episodes: int): + """Helper function to the policy evaluation schedule. + + Args: + sch (Union[int, List[int]]): Evaluation schedule. If it is an int, it is treated as the number of episodes + between two adjacent evaluations. For example, if the total number of episodes is 20 and ``sch`` is 6, + this will return [6, 12, 18] if ``final`` is False or [6, 12, 18, 20] otherwise. If it is a list, it will + return a sorted version of the list (with the last episode appended if ``final`` is True). + num_episodes (int): Total number of learning episodes. + + Returns: + A list of episodes indicating when to perform policy evaluation. + + """ + if sch is None: + schedule = [] + elif isinstance(sch, int): + num_eval_schedule = num_episodes // sch + schedule = [sch * i for i in range(1, num_eval_schedule + 1)] + else: + schedule = sorted(sch) + + return schedule + + diff --git a/maro/rl/workflows/main.py b/maro/rl/workflows/main.py new file mode 100644 index 000000000..fa390f984 --- /dev/null +++ b/maro/rl/workflows/main.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import importlib +import os +import sys +import time + +from maro.rl.learning.helpers import get_rollout_finish_msg +from maro.rl.workflows.helpers import from_env, get_default_log_dir, get_eval_schedule +from maro.utils import Logger + +sys.path.insert(0, from_env("SCENARIODIR")) +module = importlib.import_module(from_env("SCENARIO")) +get_env_sampler = getattr(module, "get_env_sampler") +post_collect = getattr(module, "post_collect", None) +post_evaluate = getattr(module, "post_evaluate", None) + +checkpoint_dir = from_env("CHECKPOINTDIR", required=False, default=None) +if checkpoint_dir: + os.makedirs(checkpoint_dir, exist_ok=True) +load_policy_dir = from_env("LOADDIR", required=False, default=None) +log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) +os.makedirs(log_dir, exist_ok=True) + + +if __name__ == "__main__": + mode = from_env("MODE") + num_episodes = from_env("NUMEPISODES") + num_steps = from_env("NUMSTEPS", required=False, default=-1) + + if mode == "single": + logger = Logger("LEARNER", dump_folder=log_dir) + env_sampler = get_env_sampler() + if load_policy_dir: + env_sampler.agent_wrapper.load(load_policy_dir) + logger.info(f"Loaded policy states from {load_policy_dir}") + + # evaluation schedule + eval_schedule = get_eval_schedule(from_env("EVALSCH", required=False, default=None), num_episodes) + logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") + eval_point_index = 0 + + def collect_and_update(): + collect_time = policy_update_time = 0 + segment, end_of_episode = 1, False + while not end_of_episode: + # experience collection + tc0 = time.time() + result = env_sampler.sample(num_steps=num_steps, return_rollout_info=False) + trackers = [result["tracker"]] + logger.info( + get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) + ) + end_of_episode = result["end_of_episode"] + + if post_collect: + post_collect(trackers, ep, segment) + + collect_time += time.time() - tc0 + tu0 = time.time() + env_sampler.agent_wrapper.improve(checkpoint_dir=checkpoint_dir) + if checkpoint_dir: + logger.info(f"Saved policy states to {checkpoint_dir}") + policy_update_time += time.time() - tu0 + segment += 1 + + # performance details + logger.info(f"ep {ep} summary - collect time: {collect_time}, policy update time: {policy_update_time}") + + for ep in range(1, num_episodes + 1): + collect_and_update() + if eval_schedule and ep == eval_schedule[eval_point_index]: + eval_point_index += 1 + trackers = [env_sampler.test()] + if post_evaluate: + post_evaluate(trackers, ep) + else: + from rollout_manager import get_rollout_manager + from policy_manager import get_policy_manager + rollout_manager = get_rollout_manager() + policy_manager = get_policy_manager() + logger = Logger("LEARNER", dump_folder=log_dir) + # evaluation schedule + eval_schedule = get_eval_schedule(from_env("EVALSCH", required=False, default=None), num_episodes) + logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") + eval_point_index = 0 + + def collect_and_update(): + collect_time = policy_update_time = 0 + rollout_manager.reset() + segment, end_of_episode = 1, False + while not end_of_episode: + # experience collection + tc0 = time.time() + policy_state_dict = policy_manager.get_state() + rollout_info_by_policy, trackers = rollout_manager.collect(ep, segment, policy_state_dict) + end_of_episode = rollout_manager.end_of_episode + + if post_collect: + post_collect(trackers, ep, segment) + + collect_time += time.time() - tc0 + tu0 = time.time() + policy_manager.update(rollout_info_by_policy) + policy_update_time += time.time() - tu0 + segment += 1 + + # performance details + logger.info(f"ep {ep} summary - collect time: {collect_time}, policy update time: {policy_update_time}") + + for ep in range(1, num_episodes + 1): + collect_and_update() + if eval_schedule and ep == eval_schedule[eval_point_index]: + eval_point_index += 1 + trackers = rollout_manager.evaluate(ep, policy_manager.get_state()) + if post_evaluate: + post_evaluate(trackers, ep) + + rollout_manager.exit() + if hasattr(policy_manager, "exit"): + policy_manager.exit() diff --git a/maro/rl/workflows/policy_host.py b/maro/rl/workflows/policy_host.py new file mode 100644 index 000000000..c9a100a03 --- /dev/null +++ b/maro/rl/workflows/policy_host.py @@ -0,0 +1,98 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import importlib +import os +import sys +import time + +from maro.communication import Proxy +from maro.rl.utils import MsgKey, MsgTag +from maro.rl.workflows.helpers import from_env, get_default_log_dir +from maro.utils import Logger + +sys.path.insert(0, from_env("SCENARIODIR")) +module = importlib.import_module(from_env("SCENARIO")) +policy_func_dict = getattr(module, "policy_func_dict") + +checkpoint_dir = from_env("CHECKPOINTDIR", required=False, default=None) +if checkpoint_dir: + os.makedirs(checkpoint_dir, exist_ok=True) +load_policy_dir = from_env("LOADDIR", required=False, default=None) +log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) +os.makedirs(log_dir, exist_ok=True) + + +if __name__ == "__main__": + host_id = from_env("HOSTID") + peers = {"policy_manager": 1} + data_parallel = from_env("DATAPARALLEL", required=False, default=False) + if data_parallel: + num_grad_workers = from_env("NUMGRADWORKERS") + peers["grad_worker"] = num_grad_workers + + if host_id is None: + raise ValueError("missing environment variable: HOSTID") + + group = from_env("POLICYGROUP") + policy_dict, checkpoint_path = {}, {} + + proxy = Proxy( + group, "policy_host", peers, + component_name=f"POLICY_HOST.{host_id}", + redis_address=(from_env("REDISHOST"), from_env("REDISPORT")), + max_peer_discovery_retries=50 + ) + logger = Logger(proxy.name, dump_folder=log_dir) + + for msg in proxy.receive(): + if msg.tag == MsgTag.EXIT: + logger.info("Exiting...") + proxy.close() + break + elif msg.tag == MsgTag.INIT_POLICIES: + for id_ in msg.body[MsgKey.POLICY_IDS]: + policy_dict[id_] = policy_func_dict[id_](id_) + checkpoint_path[id_] = os.path.join(checkpoint_dir, id_) if checkpoint_dir else None + if load_policy_dir: + path = os.path.join(load_policy_dir, id_) + if os.path.exists(path): + policy_dict[id_].load(path) + logger.info(f"Loaded policy {id_} from {path}") + if data_parallel: + policy_dict[id_].data_parallel_with_existing_proxy(proxy) + + logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_IDS]}") + proxy.reply( + msg, + tag=MsgTag.INIT_POLICIES_DONE, + body={MsgKey.POLICY_STATE: {id_: policy.get_state() for id_, policy in policy_dict.items()}} + ) + elif msg.tag == MsgTag.LEARN: + t0 = time.time() + for id_, info in msg.body[MsgKey.ROLLOUT_INFO].items(): + # in some cases e.g. Actor-Critic that get loss from rollout workers + if isinstance(info, list): + logger.info("updating with loss info") + policy_dict[id_].update(info) + else: + if data_parallel: + logger.info("learning on remote grad workers") + policy2workers = msg.body[MsgKey.WORKER_INFO][id_] + policy_dict[id_].learn_with_data_parallel(info, policy2workers[id_]) + else: + logger.info("learning from batch") + policy_dict[id_].learn(info) + + if checkpoint_path[id_]: + policy_dict[id_].save(checkpoint_path[id_]) + logger.info(f"Saved policy {id_} to {checkpoint_path[id_]}") + + msg_body = { + MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.ROLLOUT_INFO]} + } + logger.info(f"total policy update time: {time.time() - t0}") + proxy.reply(msg, tag=MsgTag.LEARN_DONE, body=msg_body) + else: + logger.info(f"Wrong message tag: {msg.tag}") + raise TypeError diff --git a/maro/rl/workflows/policy_manager.py b/maro/rl/workflows/policy_manager.py new file mode 100644 index 000000000..ddf09d4bb --- /dev/null +++ b/maro/rl/workflows/policy_manager.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import importlib +import os +import sys + +from maro.rl.learning import DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager +from maro.rl.workflows.helpers import from_env, get_default_log_dir +from maro.rl.policy import WorkerAllocator + +sys.path.insert(0, from_env("SCENARIODIR")) +module = importlib.import_module(from_env("SCENARIO")) +policy_func_dict = getattr(module, "policy_func_dict") +agent2policy = getattr(module, "agent2policy") + +checkpoint_dir = from_env("CHECKPOINTDIR", required=False, default=None) +if checkpoint_dir: + os.makedirs(checkpoint_dir, exist_ok=True) +load_policy_dir = from_env("LOADDIR", required=False, default=None) +log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) +os.makedirs(log_dir, exist_ok=True) + + +def get_policy_manager(): + manager_type = from_env("POLICYMANAGERTYPE") + data_parallel = from_env("DATAPARALLEL", required=False, default=False) + if data_parallel: + allocator = WorkerAllocator( + from_env("ALLOCATIONMODE"), from_env("NUMGRADWORKERS"), list(policy_func_dict.keys()), agent2policy + ) + else: + allocator = None + proxy_kwargs = { + "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), + "max_peer_discovery_retries": 50 + } + if manager_type == "simple": + return SimplePolicyManager( + policy_func_dict, + load_dir=load_policy_dir, + checkpoint_dir=checkpoint_dir, + worker_allocator=allocator, + group=from_env("POLICYGROUP"), + proxy_kwargs=proxy_kwargs, + log_dir=log_dir + ) + elif manager_type == "multi-process": + return MultiProcessPolicyManager( + policy_func_dict, + load_dir=load_policy_dir, + checkpoint_dir=checkpoint_dir, + worker_allocator=allocator, + group=from_env("POLICYGROUP"), + proxy_kwargs=proxy_kwargs, + log_dir=log_dir + ) + elif manager_type == "distributed": + num_hosts = from_env("NUMHOSTS") + return DistributedPolicyManager( + list(policy_func_dict.keys()), num_hosts, + group=from_env("POLICYGROUP"), + worker_allocator=allocator, + proxy_kwargs=proxy_kwargs, + log_dir=log_dir + ) + + raise ValueError( + f"Unsupported policy manager type: {manager_type}. Supported modes: simple, multi-process, distributed" + ) + + +if __name__ == "__main__": + policy_manager = get_policy_manager() + policy_manager.server( + from_env("GROUP", default="ASYNC"), + from_env("NUMROLLOUTS"), + max_lag=from_env("MAXLAG", required=False, default=0), + proxy_kwargs={ + "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir + ) diff --git a/maro/rl/workflows/rollout.py b/maro/rl/workflows/rollout.py new file mode 100644 index 000000000..fe185c993 --- /dev/null +++ b/maro/rl/workflows/rollout.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import importlib +import os +import sys + +from maro.rl.workflows.helpers import from_env, get_default_log_dir + +sys.path.insert(0, from_env("SCENARIODIR")) +module = importlib.import_module(from_env("SCENARIO")) +get_env_sampler = getattr(module, "get_env_sampler") + +log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) +os.makedirs(log_dir, exist_ok=True) + + +if __name__ == "__main__": + mode, index = from_env("MODE"), from_env("WORKERID") + env_sampler = get_env_sampler() + if mode == "sync": + env_sampler.worker( + from_env("ROLLOUTGROUP"), index, + proxy_kwargs={ + "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir + ) + elif mode == "async": + num_episodes = from_env("NUMEPISODES") + num_steps = from_env("NUMSTEPS", required=False, default=-1) + env_sampler.actor( + from_env("GROUP"), index, num_episodes, + num_steps=num_steps, + proxy_kwargs={ + "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), + "max_peer_discovery_retries": 50 + }, + log_dir=log_dir, + ) + else: + raise ValueError(f"MODE environment variable must be 'sync' or 'async', got {mode}") diff --git a/maro/rl/workflows/rollout_manager.py b/maro/rl/workflows/rollout_manager.py new file mode 100644 index 000000000..87f027546 --- /dev/null +++ b/maro/rl/workflows/rollout_manager.py @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import importlib +import os +import sys + +from maro.rl.learning import DistributedRolloutManager, MultiProcessRolloutManager +from maro.rl.workflows.helpers import from_env, get_default_log_dir + +sys.path.insert(0, from_env("SCENARIODIR")) +module = importlib.import_module(from_env("SCENARIO")) +get_env_sampler = getattr(module, "get_env_sampler") +post_collect = getattr(module, "post_collect", None) +post_evaluate = getattr(module, "post_evaluate", None) + +log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) +os.makedirs(log_dir, exist_ok=True) + + +def get_rollout_manager(): + rollout_type = from_env("ROLLOUTTYPE") + num_steps = from_env("NUMSTEPS", required=False, default=-1) + if rollout_type == "multi-process": + return MultiProcessRolloutManager( + get_env_sampler, + num_steps=num_steps, + num_rollouts=from_env("NUMROLLOUTS"), + num_eval_rollouts=from_env("NUMEVALROLLOUTS", required=False, default=1), + log_dir=log_dir + ) + + if rollout_type == "distributed": + num_workers = from_env("NUMROLLOUTS") + num_eval_workers = from_env("NUMEVALROLLOUTS", required=False, default=1) + min_finished_workers = from_env("MINFINISH", required=False, default=None) + max_extra_recv_tries = from_env("MAXEXRECV", required=False, default=0) + extra_recv_timeout = from_env("MAXRECVTIMEO", required=False, default=100) + + return DistributedRolloutManager( + from_env("ROLLOUTGROUP"), + num_workers, + num_eval_workers=num_eval_workers, + num_steps=num_steps, + min_finished_workers=min_finished_workers, + max_extra_recv_tries=max_extra_recv_tries, + extra_recv_timeout=extra_recv_timeout, + proxy_kwargs={ + "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), + "max_peer_discovery_retries": 50 + }, + ) + + raise ValueError(f"Unsupported roll-out type: {rollout_type}. Supported: multi-process, distributed") From c1f8faf5683db35bf5e92d387b321227cd6a85c5 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sat, 9 Oct 2021 08:14:30 +0000 Subject: [PATCH 454/482] fixed bug in ParallelAgentWrapper --- maro/rl/learning/env_sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 74cbc19a9..a895a4e3e 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -153,7 +153,7 @@ def _inference_service(id_, get_policy, conn): host.start() def load(self, dir: str): - for conn in self._conn.items(): + for conn in self._conn.values(): conn.send({"type": "load", "dir": dir}) def choose_action(self, state_by_agent: Dict[str, np.ndarray]): From cf1430a9d07f43755bd81385b7719e1e2226a90a Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 10 Oct 2021 05:42:11 +0000 Subject: [PATCH 455/482] 1. fixed lint issues; 2. refined main script in workflows --- maro/rl/learning/env_sampler.py | 2 +- maro/rl/learning/policy_manager.py | 2 +- maro/rl/workflows/grad_worker.py | 2 +- maro/rl/workflows/helpers.py | 2 +- maro/rl/workflows/main.py | 32 +++++++++-------------------- maro/rl/workflows/policy_manager.py | 7 ++++--- maro/rl/workflows/rollout.py | 2 +- 7 files changed, 19 insertions(+), 30 deletions(-) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index a895a4e3e..682cb27a7 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -91,7 +91,7 @@ def improve(self, checkpoint_dir: str = None): class ParallelAgentWrapper: """Wrapper for multiple agents using multiple policies to expose simple single-agent interfaces. - + The policy instances are distributed across multiple processes to achieve parallel inference. """ def __init__(self, get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str]): diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py index 36d5f3d46..f64d727b1 100644 --- a/maro/rl/learning/policy_manager.py +++ b/maro/rl/learning/policy_manager.py @@ -444,4 +444,4 @@ def exit(self): """Tell the remote policy hosts to exit.""" self._proxy.ibroadcast("policy_host", MsgTag.EXIT, SessionType.NOTIFICATION) self._proxy.close() - self._logger.info("Exiting...") + self._logger.info("Exiting...") diff --git a/maro/rl/workflows/grad_worker.py b/maro/rl/workflows/grad_worker.py index 30d72b09a..dc17ecb9b 100644 --- a/maro/rl/workflows/grad_worker.py +++ b/maro/rl/workflows/grad_worker.py @@ -19,7 +19,7 @@ if __name__ == "__main__": - # TODO: WORKERID in docker compose script. + # TODO: WORKERID in docker compose script. worker_id = from_env("WORKERID") num_hosts = from_env("NUMHOSTS") if from_env("POLICYMANAGERTYPE") == "distributed" else 0 max_cached_policies = from_env("MAXCACHED", required=False, default=10) diff --git a/maro/rl/workflows/helpers.py b/maro/rl/workflows/helpers.py index 7bb47e27d..76c0c8c4f 100644 --- a/maro/rl/workflows/helpers.py +++ b/maro/rl/workflows/helpers.py @@ -8,7 +8,7 @@ def from_env(var_name, required=True, default=None): if var_name not in os.environ: if required: - raise KeyError(f"Missing environment variable: {var_name}") + raise KeyError(f"Missing environment variable: {var_name}") else: return default diff --git a/maro/rl/workflows/main.py b/maro/rl/workflows/main.py index fa390f984..bf527cf82 100644 --- a/maro/rl/workflows/main.py +++ b/maro/rl/workflows/main.py @@ -24,24 +24,23 @@ os.makedirs(log_dir, exist_ok=True) -if __name__ == "__main__": +if __name__ == "__main__": mode = from_env("MODE") num_episodes = from_env("NUMEPISODES") num_steps = from_env("NUMSTEPS", required=False, default=-1) + logger = Logger("MAIN", dump_folder=log_dir) + # evaluation schedule + eval_schedule = get_eval_schedule(from_env("EVALSCH", required=False, default=None), num_episodes) + logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") + eval_point_index = 0 if mode == "single": - logger = Logger("LEARNER", dump_folder=log_dir) env_sampler = get_env_sampler() if load_policy_dir: env_sampler.agent_wrapper.load(load_policy_dir) logger.info(f"Loaded policy states from {load_policy_dir}") - # evaluation schedule - eval_schedule = get_eval_schedule(from_env("EVALSCH", required=False, default=None), num_episodes) - logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") - eval_point_index = 0 - - def collect_and_update(): + for ep in range(1, num_episodes + 1): collect_time = policy_update_time = 0 segment, end_of_episode = 1, False while not end_of_episode: @@ -67,26 +66,18 @@ def collect_and_update(): # performance details logger.info(f"ep {ep} summary - collect time: {collect_time}, policy update time: {policy_update_time}") - - for ep in range(1, num_episodes + 1): - collect_and_update() if eval_schedule and ep == eval_schedule[eval_point_index]: eval_point_index += 1 trackers = [env_sampler.test()] if post_evaluate: post_evaluate(trackers, ep) else: - from rollout_manager import get_rollout_manager from policy_manager import get_policy_manager + from rollout_manager import get_rollout_manager + rollout_manager = get_rollout_manager() policy_manager = get_policy_manager() - logger = Logger("LEARNER", dump_folder=log_dir) - # evaluation schedule - eval_schedule = get_eval_schedule(from_env("EVALSCH", required=False, default=None), num_episodes) - logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") - eval_point_index = 0 - - def collect_and_update(): + for ep in range(1, num_episodes + 1): collect_time = policy_update_time = 0 rollout_manager.reset() segment, end_of_episode = 1, False @@ -108,9 +99,6 @@ def collect_and_update(): # performance details logger.info(f"ep {ep} summary - collect time: {collect_time}, policy update time: {policy_update_time}") - - for ep in range(1, num_episodes + 1): - collect_and_update() if eval_schedule and ep == eval_schedule[eval_point_index]: eval_point_index += 1 trackers = rollout_manager.evaluate(ep, policy_manager.get_state()) diff --git a/maro/rl/workflows/policy_manager.py b/maro/rl/workflows/policy_manager.py index ddf09d4bb..486ce5556 100644 --- a/maro/rl/workflows/policy_manager.py +++ b/maro/rl/workflows/policy_manager.py @@ -6,8 +6,9 @@ import sys from maro.rl.learning import DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager -from maro.rl.workflows.helpers import from_env, get_default_log_dir from maro.rl.policy import WorkerAllocator +from maro.rl.workflows.helpers import from_env, get_default_log_dir + sys.path.insert(0, from_env("SCENARIODIR")) module = importlib.import_module(from_env("SCENARIO")) @@ -37,7 +38,7 @@ def get_policy_manager(): } if manager_type == "simple": return SimplePolicyManager( - policy_func_dict, + policy_func_dict, load_dir=load_policy_dir, checkpoint_dir=checkpoint_dir, worker_allocator=allocator, @@ -47,7 +48,7 @@ def get_policy_manager(): ) elif manager_type == "multi-process": return MultiProcessPolicyManager( - policy_func_dict, + policy_func_dict, load_dir=load_policy_dir, checkpoint_dir=checkpoint_dir, worker_allocator=allocator, diff --git a/maro/rl/workflows/rollout.py b/maro/rl/workflows/rollout.py index fe185c993..405ea202f 100644 --- a/maro/rl/workflows/rollout.py +++ b/maro/rl/workflows/rollout.py @@ -23,7 +23,7 @@ from_env("ROLLOUTGROUP"), index, proxy_kwargs={ "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), - "max_peer_discovery_retries": 50 + "max_peer_discovery_retries": 50 }, log_dir=log_dir ) From 485ffd71156561a77b02b4cd8615ff6bb060b8a7 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 10 Oct 2021 05:50:05 +0000 Subject: [PATCH 456/482] lint issue fix --- maro/rl/workflows/helpers.py | 2 -- maro/rl/workflows/policy_manager.py | 1 - 2 files changed, 3 deletions(-) diff --git a/maro/rl/workflows/helpers.py b/maro/rl/workflows/helpers.py index 76c0c8c4f..ce10c9ac5 100644 --- a/maro/rl/workflows/helpers.py +++ b/maro/rl/workflows/helpers.py @@ -43,5 +43,3 @@ def get_eval_schedule(sch: Union[int, List[int]], num_episodes: int): schedule = sorted(sch) return schedule - - diff --git a/maro/rl/workflows/policy_manager.py b/maro/rl/workflows/policy_manager.py index 486ce5556..b7e983e9b 100644 --- a/maro/rl/workflows/policy_manager.py +++ b/maro/rl/workflows/policy_manager.py @@ -9,7 +9,6 @@ from maro.rl.policy import WorkerAllocator from maro.rl.workflows.helpers import from_env, get_default_log_dir - sys.path.insert(0, from_env("SCENARIODIR")) module = importlib.import_module(from_env("SCENARIO")) policy_func_dict = getattr(module, "policy_func_dict") From 4e1d37c9f1d39037397f6c7e254a56d761ba1477 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Sun, 10 Oct 2021 07:02:34 +0000 Subject: [PATCH 457/482] restored default config for rl example --- examples/rl/config.yml | 11 ++++++----- maro/rl/learning/env_sampler.py | 5 +++-- maro/rl/workflows/policy_manager.py | 7 +++---- maro/rl/workflows/rollout.py | 9 ++++++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/examples/rl/config.yml b/examples/rl/config.yml index 753ba15f4..9419cc34f 100644 --- a/examples/rl/config.yml +++ b/examples/rl/config.yml @@ -1,13 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -job: cim4 +job: cim scenario_dir: "/maro/examples" scenario: cim -load_policy_dir: "/maro/examples/checkpoints/cim4" -checkpoint_dir: "/maro/examples/checkpoints/cim4" -log_dir: "/maro/examples/logs/cim4" -mode: single # single, sync, async +load_policy_dir: "/maro/examples/checkpoints/cim" +checkpoint_dir: "/maro/examples/checkpoints/cim" +log_dir: "/maro/examples/logs/cim" +mode: sync # single, sync, async num_episodes: 10 eval_schedule: 5 sync: @@ -19,6 +19,7 @@ sync: max_extra_recv_tries: 1 extra_recv_timeout: 200 async: + max_lag: 3 num_rollouts: 3 policy_manager: type: distributed # simple, multi-process, distributed diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index 682cb27a7..edb9d4e0d 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -488,10 +488,11 @@ def actor( if num_steps == 0 or num_steps < -1: raise ValueError("num_steps must be a positive integer or -1") + name = f"ACTOR.{index}" + logger = Logger(name, dump_folder=log_dir) peers = {"policy_server": 1} - proxy = Proxy(group, "actor", peers, component_name=f"ACTOR.{index}", **proxy_kwargs) + proxy = Proxy(group, "actor", peers, component_name=name, **proxy_kwargs) server_address = proxy.peers["policy_server"][0] - logger = Logger(proxy.name, dump_folder=log_dir) # get initial policy states from the policy manager msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, server_address) diff --git a/maro/rl/workflows/policy_manager.py b/maro/rl/workflows/policy_manager.py index b7e983e9b..8a6aa201e 100644 --- a/maro/rl/workflows/policy_manager.py +++ b/maro/rl/workflows/policy_manager.py @@ -41,7 +41,7 @@ def get_policy_manager(): load_dir=load_policy_dir, checkpoint_dir=checkpoint_dir, worker_allocator=allocator, - group=from_env("POLICYGROUP"), + group=from_env("POLICYGROUP", required=False, default=None), proxy_kwargs=proxy_kwargs, log_dir=log_dir ) @@ -56,9 +56,8 @@ def get_policy_manager(): log_dir=log_dir ) elif manager_type == "distributed": - num_hosts = from_env("NUMHOSTS") return DistributedPolicyManager( - list(policy_func_dict.keys()), num_hosts, + list(policy_func_dict.keys()), from_env("NUMHOSTS"), group=from_env("POLICYGROUP"), worker_allocator=allocator, proxy_kwargs=proxy_kwargs, @@ -73,7 +72,7 @@ def get_policy_manager(): if __name__ == "__main__": policy_manager = get_policy_manager() policy_manager.server( - from_env("GROUP", default="ASYNC"), + from_env("GROUP"), from_env("NUMROLLOUTS"), max_lag=from_env("MAXLAG", required=False, default=0), proxy_kwargs={ diff --git a/maro/rl/workflows/rollout.py b/maro/rl/workflows/rollout.py index 405ea202f..99cfc8eea 100644 --- a/maro/rl/workflows/rollout.py +++ b/maro/rl/workflows/rollout.py @@ -6,6 +6,7 @@ import sys from maro.rl.workflows.helpers import from_env, get_default_log_dir +from maro.utils import Logger sys.path.insert(0, from_env("SCENARIODIR")) module = importlib.import_module(from_env("SCENARIO")) @@ -16,11 +17,12 @@ if __name__ == "__main__": - mode, index = from_env("MODE"), from_env("WORKERID") + mode = from_env("MODE") env_sampler = get_env_sampler() if mode == "sync": + worker_id = from_env("WORKERID") env_sampler.worker( - from_env("ROLLOUTGROUP"), index, + from_env("ROLLOUTGROUP"), worker_id, proxy_kwargs={ "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), "max_peer_discovery_retries": 50 @@ -28,10 +30,11 @@ log_dir=log_dir ) elif mode == "async": + actor_id = from_env("ACTORID") num_episodes = from_env("NUMEPISODES") num_steps = from_env("NUMSTEPS", required=False, default=-1) env_sampler.actor( - from_env("GROUP"), index, num_episodes, + from_env("GROUP"), actor_id, num_episodes, num_steps=num_steps, proxy_kwargs={ "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), From 5b21e67de8fe37dd0a1d0362ed7e39db031e710c Mon Sep 17 00:00:00 2001 From: ysqyang Date: Sun, 10 Oct 2021 15:51:26 +0800 Subject: [PATCH 458/482] Update rollout.py --- maro/rl/workflows/rollout.py | 1 - 1 file changed, 1 deletion(-) diff --git a/maro/rl/workflows/rollout.py b/maro/rl/workflows/rollout.py index 99cfc8eea..103367b18 100644 --- a/maro/rl/workflows/rollout.py +++ b/maro/rl/workflows/rollout.py @@ -6,7 +6,6 @@ import sys from maro.rl.workflows.helpers import from_env, get_default_log_dir -from maro.utils import Logger sys.path.insert(0, from_env("SCENARIODIR")) module = importlib.import_module(from_env("SCENARIO")) From 868bd534542f0f0929f065073f8a323fa335cc83 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Mon, 11 Oct 2021 06:20:21 +0000 Subject: [PATCH 459/482] refined env var processing in policy manager workflow --- maro/rl/workflows/policy_manager.py | 7 ++++--- maro/rl/workflows/rollout.py | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/maro/rl/workflows/policy_manager.py b/maro/rl/workflows/policy_manager.py index 8a6aa201e..646eb9063 100644 --- a/maro/rl/workflows/policy_manager.py +++ b/maro/rl/workflows/policy_manager.py @@ -29,8 +29,9 @@ def get_policy_manager(): allocator = WorkerAllocator( from_env("ALLOCATIONMODE"), from_env("NUMGRADWORKERS"), list(policy_func_dict.keys()), agent2policy ) + group = from_env("POLICYGROUP") else: - allocator = None + allocator, group = None, None proxy_kwargs = { "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), "max_peer_discovery_retries": 50 @@ -41,7 +42,7 @@ def get_policy_manager(): load_dir=load_policy_dir, checkpoint_dir=checkpoint_dir, worker_allocator=allocator, - group=from_env("POLICYGROUP", required=False, default=None), + group=group, proxy_kwargs=proxy_kwargs, log_dir=log_dir ) @@ -51,7 +52,7 @@ def get_policy_manager(): load_dir=load_policy_dir, checkpoint_dir=checkpoint_dir, worker_allocator=allocator, - group=from_env("POLICYGROUP"), + group=group, proxy_kwargs=proxy_kwargs, log_dir=log_dir ) diff --git a/maro/rl/workflows/rollout.py b/maro/rl/workflows/rollout.py index 103367b18..8305ff5f7 100644 --- a/maro/rl/workflows/rollout.py +++ b/maro/rl/workflows/rollout.py @@ -19,9 +19,8 @@ mode = from_env("MODE") env_sampler = get_env_sampler() if mode == "sync": - worker_id = from_env("WORKERID") env_sampler.worker( - from_env("ROLLOUTGROUP"), worker_id, + from_env("ROLLOUTGROUP"), from_env("WORKERID"), proxy_kwargs={ "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), "max_peer_discovery_retries": 50 @@ -29,11 +28,10 @@ log_dir=log_dir ) elif mode == "async": - actor_id = from_env("ACTORID") num_episodes = from_env("NUMEPISODES") num_steps = from_env("NUMSTEPS", required=False, default=-1) env_sampler.actor( - from_env("GROUP"), actor_id, num_episodes, + from_env("GROUP"), from_env("ACTORID"), num_episodes, num_steps=num_steps, proxy_kwargs={ "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), From 12ffd983144e99e10d12ef2030a02f06961b71b1 Mon Sep 17 00:00:00 2001 From: yaqiu Date: Tue, 12 Oct 2021 01:33:00 +0000 Subject: [PATCH 460/482] added hasattr check in agent wrapper --- maro/rl/learning/env_sampler.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py index edb9d4e0d..4ad074b9b 100644 --- a/maro/rl/learning/env_sampler.py +++ b/maro/rl/learning/env_sampler.py @@ -84,9 +84,10 @@ def record_transition(self, agent: str, state, action, reward, next_state, termi def improve(self, checkpoint_dir: str = None): for id_, policy in self.policy_dict.items(): - policy.improve() - if checkpoint_dir: - policy.save(path.join(checkpoint_dir, id_)) + if hasattr(policy, "improve"): + policy.improve() + if checkpoint_dir: + policy.save(path.join(checkpoint_dir, id_)) class ParallelAgentWrapper: From c0bae0b2e617fc69d3005338089e329f58e088fb Mon Sep 17 00:00:00 2001 From: yaqiu Date: Tue, 12 Oct 2021 12:36:59 +0000 Subject: [PATCH 461/482] updated docker_compose_yml.py --- .../rl/scripts/docker/docker_compose_yml.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index eb5205c40..f14ac757a 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -17,6 +17,7 @@ example_dir = dirname(dirname(docker_script_dir)) root_dir = dirname(dirname(example_dir)) maro_rl_dir = join(root_dir, "maro", "rl") + sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") with open(join(example_dir, "config.yml"), "r") as fp: config = yaml.safe_load(fp) @@ -26,7 +27,8 @@ "image": "marorl", "volumes": [ f"{example_dir}:/maro/examples", - f"{maro_rl_dir}:/maro/maro/rl" + f"{maro_rl_dir}:/maro/maro/rl", + f"{sc_dir}:/maro/maro/simulator/scenarios/supply_chain" ] } @@ -151,16 +153,23 @@ envs = [ f"ROLLOUTTYPE={config['sync']['rollout_type']}", f"NUMEPISODES={config['num_episodes']}", - f"EVALSCH={config['eval_schedule']}", - f"NUMROLLOUTS={config['sync']['num_rollouts']}", - f"NUMEVALROLLOUTS={config['sync']['num_eval_rollouts']}", - f"ROLLOUTGROUP={rollout_group}", - f"MINFINISH={config['sync']['distributed']['min_finished_workers']}", - f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}", - f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}", + f"NUMROLLOUTS={config['sync']['num_rollouts']}" ] if "num_steps" in config: envs.append(f"NUMSTEPS={config['num_steps']}") + if "eval_schedule" in config: + envs.append(f"EVALSCH={config['eval_schedule']}") + if config["sync"]["rollout_type"] == "distributed": + envs.append(f"ROLLOUTGROUP={rollout_group}") + if "min_finished_workers" in config["sync"]["distributed"]: + envs.append(f"MINFINISH={config['sync']['distributed']['min_finished_workers']}") + if "max_extra_recv_tries" in config["sync"]["distributed"]: + envs.append(f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}") + if "extra_recv_timeout" in config["sync"]["distributed"]: + envs.append(f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}") + + if "num_eval_rollouts" in config["sync"]: + envs.append(f"NUMEVALROLLOUTS={config['sync']['num_eval_rollouts']}") docker_compose_manifest["services"]["main"] = { **common_spec, From a5ddfd55aeba89475f2a2b094a348e445c2edb4c Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 13 Oct 2021 16:17:04 +0800 Subject: [PATCH 462/482] Minor refinement --- maro/simulator/scenarios/supply_chain/__init__.py | 2 +- maro/simulator/scenarios/supply_chain/units/consumer.py | 8 +++++++- .../simulator/scenarios/supply_chain/units/manufacture.py | 4 +++- .../scenarios/supply_chain/units/simplemanufacture.py | 3 +++ maro/simulator/scenarios/supply_chain/units/unitbase.py | 4 +--- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py index beefa64d7..175e0392b 100644 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ b/maro/simulator/scenarios/supply_chain/__init__.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. -from .actions import ConsumerAction, ManufactureAction +from .actions import ConsumerAction, ManufactureAction, SupplyChainAction from .datamodels import ( ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel ) diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index dfa01af75..052f365fc 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -4,9 +4,9 @@ from scipy.ndimage.interpolation import shift -from .. import ConsumerDataModel from .extendunitbase import ExtendUnitBase from .order import Order +from .. import ConsumerAction, ConsumerDataModel class ConsumerUnit(ExtendUnitBase): @@ -89,6 +89,8 @@ def initialize(self): def step(self, tick: int): self._update_pending_order() + assert isinstance(self.action, ConsumerAction) + # NOTE: id == 0 means invalid,as our id is 1 based. if not self.action or self.action.quantity <= 0 or self.action.product_id <= 0 or self.action.source_id == 0: return @@ -106,6 +108,8 @@ def step(self, tick: int): self.purchased = self.action.quantity def flush_states(self): + assert isinstance(self.action, ConsumerAction) + if self.received > 0: self.data_model.received = self.received self.data_model.total_received += self.received @@ -123,6 +127,8 @@ def flush_states(self): self.data_model.reward_discount = self.action.reward_discount def post_step(self, tick: int): + assert isinstance(self.action, ConsumerAction) + # Clear the action states per step. if self.action is not None: self.data_model.latest_consumptions = 0 diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py index b705f91e0..fae2d414d 100644 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/manufacture.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. -from .. import ManufactureDataModel +from .. import ManufactureAction, ManufactureDataModel from .extendunitbase import ExtendUnitBase @@ -42,6 +42,8 @@ def initialize(self): self.input_units_per_lot = sum(self.bom.values()) def step(self, tick: int): + assert isinstance(self.action, ManufactureAction) + # Try to produce production if we have positive rate. if self.action is not None and self.action.production_rate > 0: sku_num = len(self.facility.skus) diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py index c4a2c81af..49a37dce1 100644 --- a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py @@ -3,6 +3,7 @@ from .manufacture import ManufactureUnit +from .. import ManufactureAction class SimpleManufactureUnit(ManufactureUnit): @@ -12,6 +13,8 @@ def step(self, tick: int): # Try to produce production if we have positive rate. self.manufacture_number = 0 + assert isinstance(self.action, ManufactureAction) + if self.action is not None and self.action.production_rate > 0: production_rate = self.action.production_rate diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py index 8e027ee37..7d9413c60 100644 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ b/maro/simulator/scenarios/supply_chain/units/unitbase.py @@ -5,10 +5,8 @@ import typing from typing import Optional, Union -from maro.simulator.scenarios.supply_chain.actions import SupplyChainAction - if typing.TYPE_CHECKING: - from maro.simulator.scenarios.supply_chain import FacilityBase + from maro.simulator.scenarios.supply_chain import FacilityBase, SupplyChainAction from maro.simulator.scenarios.supply_chain.datamodels.base import DataModelBase from maro.simulator.scenarios.supply_chain.world import World From 6a1179c8c02326be6b040cb5d5312376db43ac9a Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Mon, 6 Dec 2021 11:40:27 +0800 Subject: [PATCH 463/482] Minor PR. Prepare to merge latest master branch into v0.3 branch. (#412) * Prepare to merge master_mirror * Lint error * Minor --- .gitignore | 5 +- docs/source/examples/multi_agent_dqn_cim.rst | 168 ------------------ .../scenarios/supply_chain/units/consumer.py | 2 +- .../supply_chain/units/simplemanufacture.py | 2 +- 4 files changed, 5 insertions(+), 172 deletions(-) delete mode 100644 docs/source/examples/multi_agent_dqn_cim.rst diff --git a/.gitignore b/.gitignore index 4a729c484..13e3fa8f8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ data/ maro_venv/ pyvenv.cfg htmlcov/ -.coverage *supply_chain_*/ -examples/supply_chain/docker-compose.yml \ No newline at end of file +examples/supply_chain/docker-compose.yml +.coverage +.coveragerc diff --git a/docs/source/examples/multi_agent_dqn_cim.rst b/docs/source/examples/multi_agent_dqn_cim.rst deleted file mode 100644 index 465171299..000000000 --- a/docs/source/examples/multi_agent_dqn_cim.rst +++ /dev/null @@ -1,168 +0,0 @@ -Multi Agent DQN for CIM -================================================ - -This example demonstrates how to use MARO's reinforcement learning (RL) toolkit to solve the container -inventory management (CIM) problem. It is formalized as a multi-agent reinforcement learning problem, -where each port acts as a decision agent. When a vessel arrives at a port, these agents must take actions -by transfering a certain amount of containers to / from the vessel. The objective is for the agents to -learn policies that minimize the overall container shortage. - -Trajectory ----------- - -The ``CIMTrajectoryForDQN`` inherits from ``Trajectory`` function and implements methods to be used as callbacks -in the roll-out loop. In this example, - * ``get_state`` converts environment observations to state vectors that encode temporal and spatial information. - The temporal information includes relevant port and vessel information, such as shortage and remaining space, - over the past k days (here k = 7). The spatial information includes features of the downstream ports. - * ``get_action`` converts agents' output (an integer that maps to a percentage of containers to be loaded - to or unloaded from the vessel) to action objects that can be executed by the environment. - * ``get_offline_reward`` computes the reward of a given action as a linear combination of fulfillment and - shortage within a future time frame. - * ``on_finish`` processes a complete trajectory into data that can be used directly by the learning agents. - - -.. code-block:: python - class CIMTrajectoryForDQN(Trajectory): - def __init__( - self, env, *, port_attributes, vessel_attributes, action_space, look_back, max_ports_downstream, - reward_eval_delay, fulfillment_factor, shortage_factor, time_decay, - finite_vessel_space=True, has_early_discharge=True - ): - super().__init__(env) - self.port_attributes = port_attributes - self.vessel_attributes = vessel_attributes - self.action_space = action_space - self.look_back = look_back - self.max_ports_downstream = max_ports_downstream - self.reward_eval_delay = reward_eval_delay - self.fulfillment_factor = fulfillment_factor - self.shortage_factor = shortage_factor - self.time_decay = time_decay - self.finite_vessel_space = finite_vessel_space - self.has_early_discharge = has_early_discharge - - def get_state(self, event): - vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] - tick, port_idx, vessel_idx = event.tick, event.port_idx, event.vessel_idx - ticks = [max(0, tick - rt) for rt in range(self.look_back - 1)] - future_port_idx_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') - port_features = port_snapshots[ticks: [port_idx] + list(future_port_idx_list): self.port_attributes] - vessel_features = vessel_snapshots[tick: vessel_idx: self.vessel_attributes] - return {port_idx: np.concatenate((port_features, vessel_features))} - - def get_action(self, action_by_agent, event): - vessel_snapshots = self.env.snapshot_list["vessels"] - action_info = list(action_by_agent.values())[0] - model_action = action_info[0] if isinstance(action_info, tuple) else action_info - scope, tick, port, vessel = event.action_scope, event.tick, event.port_idx, event.vessel_idx - zero_action_idx = len(self.action_space) / 2 # index corresponding to value zero. - vessel_space = vessel_snapshots[tick:vessel:self.vessel_attributes][2] if self.finite_vessel_space else float("inf") - early_discharge = vessel_snapshots[tick:vessel:"early_discharge"][0] if self.has_early_discharge else 0 - percent = abs(self.action_space[model_action]) - - if model_action < zero_action_idx: - action_type = ActionType.LOAD - actual_action = min(round(percent * scope.load), vessel_space) - elif model_action > zero_action_idx: - action_type = ActionType.DISCHARGE - plan_action = percent * (scope.discharge + early_discharge) - early_discharge - actual_action = round(plan_action) if plan_action > 0 else round(percent * scope.discharge) - else: - actual_action, action_type = 0, None - - return {port: Action(vessel, port, actual_action, action_type)} - - def get_offline_reward(self, event): - port_snapshots = self.env.snapshot_list["ports"] - start_tick = event.tick + 1 - ticks = list(range(start_tick, start_tick + self.reward_eval_delay)) - - future_fulfillment = port_snapshots[ticks::"fulfillment"] - future_shortage = port_snapshots[ticks::"shortage"] - decay_list = [ - self.time_decay ** i for i in range(self.reward_eval_delay) - for _ in range(future_fulfillment.shape[0] // self.reward_eval_delay) - ] - - tot_fulfillment = np.dot(future_fulfillment, decay_list) - tot_shortage = np.dot(future_shortage, decay_list) - - return np.float32(self.fulfillment_factor * tot_fulfillment - self.shortage_factor * tot_shortage) - - def on_env_feedback(self, event, state_by_agent, action_by_agent, reward): - self.trajectory["event"].append(event) - self.trajectory["state"].append(state_by_agent) - self.trajectory["action"].append(action_by_agent) - - def on_finish(self): - exp_by_agent = defaultdict(lambda: defaultdict(list)) - for i in range(len(self.trajectory["state"]) - 1): - agent_id = list(self.trajectory["state"][i].keys())[0] - exp = exp_by_agent[agent_id] - exp["S"].append(self.trajectory["state"][i][agent_id]) - exp["A"].append(self.trajectory["action"][i][agent_id]) - exp["R"].append(self.get_offline_reward(self.trajectory["event"][i])) - exp["S_"].append(list(self.trajectory["state"][i + 1].values())[0]) - - return dict(exp_by_agent) - - -Agent ------ - -The out-of-the-box DQN is used as our agent. - -.. code-block:: python - agent_config = { - "model": ..., - "optimization": ..., - "hyper_params": ... - } - - def get_dqn_agent(): - q_model = SimpleMultiHeadModel( - FullyConnectedBlock(**agent_config["model"]), optim_option=agent_config["optimization"] - ) - return DQN(q_model, DQNConfig(**agent_config["hyper_params"])) - - -Training --------- - -The distributed training consists of one learner process and multiple actor processes. The learner optimizes -the policy by collecting roll-out data from the actors to train the underlying agents. - -The actor process must create a roll-out executor for performing the requested roll-outs, which means that the -the environment simulator and shapers should be created here. In this example, inference is performed on the -actor's side, so a set of DQN agents must be created in order to load the models (and exploration parameters) -from the learner. - -.. code-block:: python - def cim_dqn_actor(): - env = Env(**training_config["env"]) - agent = AgentManager({name: get_dqn_agent() for name in env.agent_idx_list}) - actor = Actor(env, agent, CIMTrajectoryForDQN, trajectory_kwargs=common_config) - actor.worker(training_config["group"]) - -The learner's side requires a concrete learner class that inherits from ``AbsLearner`` and implements the ``run`` -method which contains the main training loop. Here the implementation is similar to the single-threaded version -except that the ``collect`` method is used to obtain roll-out data from the actors (since the roll-out executors -are located on the actors' side). The agents created here are where training occurs and hence always contains the -latest policies. - -.. code-block:: python - def cim_dqn_learner(): - env = Env(**training_config["env"]) - agent = AgentManager({name: get_dqn_agent() for name in env.agent_idx_list}) - scheduler = TwoPhaseLinearParameterScheduler(training_config["max_episode"], **training_config["exploration"]) - actor = ActorProxy( - training_config["group"], training_config["num_actors"], - update_trigger=training_config["learner_update_trigger"] - ) - learner = OffPolicyLearner(actor, scheduler, agent, **training_config["training"]) - learner.run() - -.. note:: - - All related code snippets are supported in `maro playground `_. diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py index 052f365fc..a9d408afb 100644 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ b/maro/simulator/scenarios/supply_chain/units/consumer.py @@ -4,9 +4,9 @@ from scipy.ndimage.interpolation import shift +from .. import ConsumerAction, ConsumerDataModel from .extendunitbase import ExtendUnitBase from .order import Order -from .. import ConsumerAction, ConsumerDataModel class ConsumerUnit(ExtendUnitBase): diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py index 49a37dce1..adfa6d574 100644 --- a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py +++ b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py @@ -2,8 +2,8 @@ # Licensed under the MIT license. -from .manufacture import ManufactureUnit from .. import ManufactureAction +from .manufacture import ManufactureUnit class SimpleManufactureUnit(ManufactureUnit): From ff0f7061843ec9ab0e18ab8d463eab20dc99d728 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 8 Dec 2021 11:08:17 +0800 Subject: [PATCH 464/482] Merge latest master into v0.3 (#426) * update docker hub init (#367) * update docker hub init * replace personal account with maro-team * update hello files for CIM * update docker repository name * update docker file name * fix bugs in notebook, rectify docs * fix doc build issue * remove docs from playground; fix citibike lp example Event issue * update the exampel for vector env * update vector env example * update README due to PR comments * add link to playground above MARO installation in README * fix some typos Co-authored-by: Jinyu Wang * update package version * update README for package description * update image links for pypi package description * update image links for pypi package description * change the input topology schema for CIM real data mode (#372) * change the input topology schema for CIM real data mode * remove unused importing * update test config file correspondingly * add Exception for env test * add cost factors to cim data dump * update CimDataCollection field name * update field name of data collection related code * update package version * adjust interface to reflect actual signature (#374) Co-authored-by: Jeremy Reynolds * update dataclasses requirement to setup * fix: fixing spelling grammarr * fix: fix typo spelling code commented and data_model.rst * Fix Geo vis IP address & SQL logic bugs. (#383) Fix Geo vis IP address & SQL logic bugs (issue [352](https://github.com/microsoft/maro/issues/352) and [314](https://github.com/microsoft/maro/issues/314)). * Fix the "Wrong future stop tick predictions" bug (#386) * Propose my new solution Refine to the pre-process version . * Optimize import * Fix reset random seed bug (#387) * update the reset interface of Env and BE * Try to fix reset routes generation seed issue * Refine random related logics. * Minor refinement * Test check * Minor * Remove unused functions so far * Minor Co-authored-by: Jinyu Wang * update package version * Add _init_vessel_plans in business_engine.reset (#388) * update package version * change the default solver used in Citibike OnlineLP example, from GLPK to CBC (#391) Co-authored-by: Jinyu Wang * Refine `event_buffer/` module (#389) * Core & Business Engine code refinement (#392) * First version * Optimize imports * Add typehint * Lint check * Lint check * add higher python version (#398) * add higher python version * update pytorch version * update torchvision version Co-authored-by: Jinyu Wang * CIM scenario refinement (#400) * Cim scenario refinement (#394) * CIM refinement * Fix lint error * Fix lint error * Cim test coverage (#395) * Enrich tests * Refactor CimDataGenerator * Refactor CIM parsers * Minor refinement * Fix lint error * Fix lint error * Fix lint error * Minor refactor * Type * Add two test file folders. Make a slight change to CIM BE. * Lint error * Lint error * Remove unnecessary public interfaces of CIM BE * Cim disable auto action type detection (#399) * Haven't been tested * Modify document * Add ActionType checking * Minor * Lint error * Action quantity should be a position number * Modify related docs & notebooks * Minor * Change test file name. Prepare to merge into master. * . * Minor test patch * Add `clear()` function to class `SimRandom` (#401) * Add SimRandom.clear() * Minor * Remove commented codes * Lint error * update package version * Minor * Remove docs/source/examples/multi_agent_dqn_cim.rst * Update .gitignore * Update .gitignore Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> Co-authored-by: Jinyu Wang Co-authored-by: Jinyu Wang Co-authored-by: Jeremy Reynolds Co-authored-by: Jeremy Reynolds Co-authored-by: slowy07 --- .github/ISSUE_TEMPLATE/---bug-report.md | 2 +- .github/workflows/deploy_docker_image.yml | 2 +- .github/workflows/test.yml | 4 +- README.md | 30 +- .../{cpu.play.df => cpu.playground.df} | 62 +- docs/source/apidoc/maro.utils.rst | 4 +- .../examples/greedy_policy_citi_bike.rst | 2 +- docs/source/index.rst | 62 +- docs/source/installation/playground.rst | 26 +- docs/source/key_components/data_model.rst | 2 +- docs/source/scenarios/citi_bike.rst | 2 +- .../container_inventory_management.rst | 91 +- examples/citi_bike/online_lp/README.md | 18 + examples/citi_bike/online_lp/citi_bike_ilp.py | 4 +- examples/citi_bike/online_lp/launcher.py | 6 +- examples/hello_world/cim/hello.py | 35 +- examples/hello_world/cim/hello_geo_vis.py | 42 +- examples/hello_world/cim/hello_streamlit.py | 32 +- examples/hello_world/test_vm.py | 0 examples/requirements.ex.txt | 1 + examples/vector_env/hello.py | 60 +- .../rule_based_algorithm/README.md | 6 +- maro/README.rst | 291 ++- maro/__misc__.py | 2 +- maro/cli/envs/list_available.py | 2 +- .../grass/lib/services/master_agent/agent.py | 2 +- .../back_end/vis_app/app.py | 2 +- .../data_process/request/request_decision.py | 7 +- .../data_process/request/request_exp_info.py | 2 +- .../data_process/request/request_order.py | 7 +- .../data_process/request/request_port.py | 7 +- .../data_process/request/request_vessel.py | 7 +- .../vis_app/data_process/request/utils.py | 10 +- .../maro_real_time_vis/start_maro_geo_vis.py | 16 +- maro/cli/utils/node_admin.py | 2 +- maro/cli/utils/web_terminal/terminal-srv.py | 2 +- maro/data_lib/cim/README.md | 27 +- maro/data_lib/cim/cim_data_container.py | 75 +- .../cim/cim_data_container_helpers.py | 51 +- maro/data_lib/cim/cim_data_dump.py | 63 +- maro/data_lib/cim/cim_data_generator.py | 329 ++-- maro/data_lib/cim/cim_data_loader.py | 685 ++++--- maro/data_lib/cim/entities.py | 29 +- maro/data_lib/cim/global_order_proportion.py | 68 - maro/data_lib/cim/parsers.py | 210 +++ maro/data_lib/cim/port_buffer_tick_wrapper.py | 12 +- maro/data_lib/cim/port_parser.py | 85 - maro/data_lib/cim/route_parser.py | 36 - maro/data_lib/cim/utils.py | 33 +- .../cim/vessel_future_stops_prediction.py | 57 +- maro/data_lib/cim/vessel_parser.py | 50 - .../data_lib/cim/vessel_past_stops_wrapper.py | 12 +- .../cim/vessel_reachable_stops_wrapper.py | 15 +- .../cim/vessel_sailing_plan_wrapper.py | 24 +- maro/data_lib/cim/vessel_stop_wrapper.py | 6 +- maro/data_lib/item_meta.py | 2 +- maro/event_buffer/__init__.py | 7 +- maro/event_buffer/atom_event.py | 39 - maro/event_buffer/cascade_event.py | 65 - maro/event_buffer/event.py | 138 ++ maro/event_buffer/event_buffer.py | 67 +- maro/event_buffer/event_linked_list.py | 171 +- maro/event_buffer/event_pool.py | 83 +- maro/event_buffer/typings.py | 11 - maro/simulator/abs_core.py | 42 +- maro/simulator/core.py | 66 +- .../scenarios/abs_business_engine.py | 43 +- .../scenarios/cim/business_engine.py | 92 +- maro/simulator/scenarios/cim/common.py | 20 +- maro/simulator/scenarios/cim/frame_builder.py | 1 + maro/simulator/scenarios/cim/matrix.py | 16 +- .../global_trade.22p_l0.0/config.yml | 5 +- .../global_trade.22p_l0.1/config.yml | 5 +- .../global_trade.22p_l0.2/config.yml | 5 +- .../global_trade.22p_l0.3/config.yml | 5 +- .../global_trade.22p_l0.4/config.yml | 5 +- .../global_trade.22p_l0.5/config.yml | 5 +- .../global_trade.22p_l0.6/config.yml | 5 +- .../global_trade.22p_l0.7/config.yml | 5 +- .../global_trade.22p_l0.8/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.0/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.1/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.2/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.3/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.4/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.5/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.6/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.7/config.yml | 5 +- .../topologies/toy.4p_ssdd_l0.8/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.0/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.1/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.2/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.3/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.4/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.5/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.6/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.7/config.yml | 5 +- .../topologies/toy.5p_ssddd_l0.8/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.0/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.1/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.2/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.3/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.4/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.5/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.6/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.7/config.yml | 5 +- .../topologies/toy.6p_sssbdd_l0.8/config.yml | 5 +- .../scenarios/citi_bike/business_engine.py | 22 +- maro/simulator/scenarios/helpers.py | 6 +- .../vm_scheduling/business_engine.py | 36 +- maro/simulator/utils/sim_random.py | 45 +- ..._lib_exeption.py => data_lib_exception.py} | 0 maro/utils/utils.py | 2 +- maro/vector_env/vector_env.py | 2 +- .../interact_with_environment.ipynb | 95 +- .../interact_with_environment.ipynb | 90 +- .../rl_formulation.ipynb | 2 +- playground.md | 17 +- scripts/build_playground.bat | 16 +- scripts/build_playground.sh | 2 +- scripts/import_cim_dumps.py | 2 +- scripts/run_playground.sh | 5 +- setup.py | 8 +- .../test_data_collection_dump.py | 10 +- .../test_data_collection_load.py | 38 +- .../cim/data_generator/test_data_generator.py | 32 +- .../test_order_global_proportion.py | 37 +- tests/cim/data_generator/test_ports_parser.py | 10 +- tests/cim/data_generator/test_route_parser.py | 10 +- .../cim/data_generator/test_vessel_parser.py | 11 +- tests/cim/mock_data_container.py | 276 --- tests/cim/test_cim_scenario.py | 831 ++++---- .../cim/case_data/config_folder/config.yml | 1671 +++++++++++++++++ .../dump_folder/global_order_proportion.txt | 200 ++ tests/data/cim/case_data/dump_folder/misc.yml | 10 + .../dump_folder/order_proportion.csv | 158 ++ .../data/cim/case_data/dump_folder/ports.csv | 23 + .../data/cim/case_data/dump_folder/routes.csv | 69 + .../data/cim/case_data/dump_folder/stops.csv | 616 ++++++ .../cim/case_data/dump_folder/vessels.csv | 47 + .../cim/case_data/real_folder_bin/misc.yml | 7 + .../cim/case_data/real_folder_bin/orders.bin | Bin 0 -> 14050 bytes .../cim/case_data/real_folder_bin/ports.csv | 5 + .../cim/case_data/real_folder_bin/routes.csv | 6 + .../cim/case_data/real_folder_bin/stops.bin | Bin 0 -> 3588 bytes .../cim/case_data/real_folder_bin/vessels.csv | 6 + .../cim/case_data/real_folder_csv/misc.yml | 7 + .../cim/case_data/real_folder_csv/orders.csv | 673 +++++++ .../cim/case_data/real_folder_csv/ports.csv | 5 + .../cim/case_data/real_folder_csv/routes.csv | 6 + .../cim/case_data/real_folder_csv/stops.csv | 150 ++ .../cim/case_data/real_folder_csv/vessels.csv | 6 + tests/data/cim/customized_config/config.yml | 5 +- .../data/cim/data_generator/dumps/config.yml | 5 +- tests/dummy/dummy_business_engine.py | 2 +- tests/test_event_buffer.py | 174 +- tests/test_snapshot.py | 2 +- tests/utils.py | 28 + 158 files changed, 6409 insertions(+), 2839 deletions(-) rename docker_files/{cpu.play.df => cpu.playground.df} (69%) delete mode 100644 examples/hello_world/test_vm.py create mode 100644 examples/requirements.ex.txt delete mode 100644 maro/data_lib/cim/global_order_proportion.py create mode 100644 maro/data_lib/cim/parsers.py delete mode 100644 maro/data_lib/cim/port_parser.py delete mode 100644 maro/data_lib/cim/route_parser.py delete mode 100644 maro/data_lib/cim/vessel_parser.py delete mode 100644 maro/event_buffer/atom_event.py delete mode 100644 maro/event_buffer/cascade_event.py create mode 100644 maro/event_buffer/event.py delete mode 100644 maro/event_buffer/typings.py rename maro/utils/exception/{data_lib_exeption.py => data_lib_exception.py} (100%) delete mode 100644 tests/cim/mock_data_container.py create mode 100644 tests/data/cim/case_data/config_folder/config.yml create mode 100644 tests/data/cim/case_data/dump_folder/global_order_proportion.txt create mode 100644 tests/data/cim/case_data/dump_folder/misc.yml create mode 100644 tests/data/cim/case_data/dump_folder/order_proportion.csv create mode 100644 tests/data/cim/case_data/dump_folder/ports.csv create mode 100644 tests/data/cim/case_data/dump_folder/routes.csv create mode 100644 tests/data/cim/case_data/dump_folder/stops.csv create mode 100644 tests/data/cim/case_data/dump_folder/vessels.csv create mode 100644 tests/data/cim/case_data/real_folder_bin/misc.yml create mode 100644 tests/data/cim/case_data/real_folder_bin/orders.bin create mode 100644 tests/data/cim/case_data/real_folder_bin/ports.csv create mode 100644 tests/data/cim/case_data/real_folder_bin/routes.csv create mode 100644 tests/data/cim/case_data/real_folder_bin/stops.bin create mode 100644 tests/data/cim/case_data/real_folder_bin/vessels.csv create mode 100644 tests/data/cim/case_data/real_folder_csv/misc.yml create mode 100644 tests/data/cim/case_data/real_folder_csv/orders.csv create mode 100644 tests/data/cim/case_data/real_folder_csv/ports.csv create mode 100644 tests/data/cim/case_data/real_folder_csv/routes.csv create mode 100644 tests/data/cim/case_data/real_folder_csv/stops.csv create mode 100644 tests/data/cim/case_data/real_folder_csv/vessels.csv diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md index d3712f58b..4d25a843c 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.md +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -38,7 +38,7 @@ Steps to reproduce the behavior: - How you installed MARO (`pip`, `source`): - OS (`Linux`, `Windows`, `macOS`): - Python version (`3.6`, `3.7`): -- Docker image (e.g., arthursjiang/maro:cpu[5f36ed]): +- Docker image (e.g., maro2020/maro:latest): - CPU/GPU: - Any other relevant information: diff --git a/.github/workflows/deploy_docker_image.yml b/.github/workflows/deploy_docker_image.yml index 2d186975b..a876abf93 100644 --- a/.github/workflows/deploy_docker_image.yml +++ b/.github/workflows/deploy_docker_image.yml @@ -30,7 +30,7 @@ jobs: run: | pip install -r ./maro/requirements.build.txt cython ./maro/backends/backend.pyx ./maro/backends/np_backend.pyx ./maro/backends/raw_backend.pyx ./maro/backends/frame.pyx --cplus -3 -E NODES_MEMORY_LAYOUT=ONE_BLOCK -X embedsignature=True - cat ./maro/__misc__.py | grep __version__ | egrep -o [0-9].[0-9].[0-9,a-z]+ | { read version; docker build -f ./docker_files/cpu.play.df . -t ${{ secrets.DOCKER_HUB_USERNAME }}/maro:cpu -t ${{ secrets.DOCKER_HUB_USERNAME }}/maro:latest -t ${{ secrets.DOCKER_HUB_USERNAME }}/maro:cpu-$version; } + cat ./maro/__misc__.py | grep __version__ | egrep -o [0-9].[0-9].[0-9,a-z]+ | { read version; docker build -f ./docker_files/cpu.playground.df . -t ${{ secrets.DOCKER_HUB_USERNAME }}/maro:cpu -t ${{ secrets.DOCKER_HUB_USERNAME }}/maro:latest -t ${{ secrets.DOCKER_HUB_USERNAME }}/maro:cpu-$version; } - name: Login docker hub run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64421da8e..d90771db0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: os: [ubuntu-16.04, ubuntu-18.04, windows-latest, macos-latest] - python-version: [3.6, 3.7] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -39,7 +39,7 @@ jobs: - name: Install torch on Windows if: runner.os == 'Windows' run: | - pip install torch===1.6.0 torchvision===0.7.0 -f https://download.pytorch.org/whl/torch_stable.html + pip install torch===1.7.1 torchvision===0.8.2 -f https://download.pytorch.org/whl/torch_stable.html - name: Install test dependencies run: | diff --git a/README.md b/README.md index e09ed5277..bfb4f050d 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ [![Platform](https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/platform.svg)](https://pypi.org/project/pymaro/) [![Python Versions](https://img.shields.io/pypi/pyversions/pymaro.svg?logo=python&logoColor=white)](https://pypi.org/project/pymaro/#files) [![Code Size](https://img.shields.io/github/languages/code-size/microsoft/maro)](https://github.com/microsoft/maro) -[![Docker Size](https://img.shields.io/docker/image-size/arthursjiang/maro)](https://hub.docker.com/repository/docker/arthursjiang/maro/tags?page=1) +[![Docker Size](https://img.shields.io/docker/image-size/maro2020/maro)](https://hub.docker.com/repository/docker/maro2020/maro/tags?page=1) [![Issues](https://img.shields.io/github/issues/microsoft/maro)](https://github.com/microsoft/maro/issues) [![Pull Requests](https://img.shields.io/github/issues-pr/microsoft/maro)](https://github.com/microsoft/maro/pulls) [![Dependencies](https://img.shields.io/librariesio/github/microsoft/maro)](https://libraries.io/pypi/pymaro) [![test](https://github.com/microsoft/maro/workflows/test/badge.svg)](https://github.com/microsoft/maro/actions?query=workflow%3Atest) [![build](https://github.com/microsoft/maro/workflows/build/badge.svg)](https://github.com/microsoft/maro/actions?query=workflow%3Abuild) -[![docker](https://github.com/microsoft/maro/workflows/docker/badge.svg)](https://hub.docker.com/repository/docker/arthursjiang/maro) +[![docker](https://github.com/microsoft/maro/workflows/docker/badge.svg)](https://hub.docker.com/repository/docker/maro2020/maro) [![docs](https://readthedocs.org/projects/maro/badge/?version=latest)](https://maro.readthedocs.io/) [![PypI Versions](https://img.shields.io/pypi/v/pymaro)](https://pypi.org/project/pymaro/#files) [![Wheel](https://img.shields.io/pypi/wheel/pymaro)](https://pypi.org/project/pymaro/#files) @@ -23,8 +23,8 @@ [![Lint](https://github.com/microsoft/maro/workflows/lint/badge.svg)](https://github.com/microsoft/maro/actions?query=workflow%3Alint) [![Coverage](https://img.shields.io/codecov/c/github/microsoft/maro)](https://codecov.io/gh/microsoft/maro) [![Downloads](https://img.shields.io/pypi/dm/pymaro)](https://pypi.org/project/pymaro/#files) -[![Docker Pulls](https://img.shields.io/docker/pulls/arthursjiang/maro)](https://hub.docker.com/repository/docker/arthursjiang/maro) -[![Play with MARO](https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/play_with_maro.svg)](https://hub.docker.com/r/arthursjiang/maro) +[![Docker Pulls](https://img.shields.io/docker/pulls/maro2020/maro)](https://hub.docker.com/repository/docker/maro2020/maro) +[![Play with MARO](https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/play_with_maro.svg)](https://hub.docker.com/r/maro2020/maro) # [![MARO LOGO](./docs/source/images/logo.svg)](https://maro.readthedocs.io/en/latest/) @@ -58,6 +58,8 @@ of user-defined functions for message auto-handling, cluster provision, and job | `examples` | Showcase of MARO. | | `notebooks` | MARO quick-start notebooks. | +*Try [MARO playground](#run-playground) to have a quick experience.* + ## Install MARO from [PyPI](https://pypi.org/project/pymaro/#files) *Notes: The CLI commands (including the visualization tool) are not included in pymaro package. To enable these support, you need to install from source.* @@ -185,14 +187,16 @@ maro inspector dashboard --source_path ./dump_data/YOUR_SNAPSHOT_DUMP_FOLDER ## Run Playground -- Pull from [Docker Hub](https://hub.docker.com/repository/registry-1.docker.io/arthursjiang/maro/tags?page=1) +- Pull from [Docker Hub](https://hub.docker.com/r/maro2020/playground) ```sh + # Pull the docker image from docker hub + docker pull maro2020/playground + # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 arthursjiang/maro:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground ``` - Build from source @@ -204,9 +208,8 @@ maro inspector dashboard --source_path ./dump_data/YOUR_SNAPSHOT_DUMP_FOLDER # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 maro/playground:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground ``` - Windows @@ -217,9 +220,8 @@ maro inspector dashboard --source_path ./dump_data/YOUR_SNAPSHOT_DUMP_FOLDER # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 maro/playground:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground ``` ## Contributing diff --git a/docker_files/cpu.play.df b/docker_files/cpu.playground.df similarity index 69% rename from docker_files/cpu.play.df rename to docker_files/cpu.playground.df index 1c999f1ec..43def8908 100644 --- a/docker_files/cpu.play.df +++ b/docker_files/cpu.playground.df @@ -1,20 +1,8 @@ -FROM python:3.6 +FROM python:3.7 WORKDIR /maro_playground -# Setup notebook -ADD ./notebooks ./notebooks -RUN /usr/local/bin/python -m pip install --upgrade pip -RUN pip install -r ./notebooks/requirements.nb.txt -RUN jupyter contrib nbextension install --system -RUN jt -t onedork -fs 95 -altp -tfs 11 -nfs 115 -cellw 88% -T - - -# Install redis -RUN wget http://download.redis.io/releases/redis-6.0.6.tar.gz; tar xzf redis-6.0.6.tar.gz; cd redis-6.0.6; make -RUN rm redis-6.0.6.tar.gz - -# Install others +# Install zsh and other packages for the terminal usage RUN apt-get -o Acquire::Check-Valid-Until=false -o Acquire::Check-Date=false update RUN apt-get install -y zsh RUN apt-get install -y htop @@ -23,42 +11,36 @@ RUN wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh - RUN chsh -s `which zsh` && wget https://raw.githubusercontent.com/ArthurJiang/config/master/.zshrc -O ~/.zshrc RUN apt-get install -y npm RUN rm -rf /var/lib/apt/lists/* + +# Install redis +RUN wget http://download.redis.io/releases/redis-6.0.6.tar.gz; tar xzf redis-6.0.6.tar.gz; cd redis-6.0.6; make +RUN rm redis-6.0.6.tar.gz RUN npm install -g redis-commander +# Setup notebook +ADD ./notebooks ./notebooks +RUN /usr/local/bin/python -m pip install --upgrade pip +RUN pip install -r ./notebooks/requirements.nb.txt +RUN jupyter contrib nbextension install --system +RUN jt -t onedork -fs 95 -altp -tfs 11 -nfs 115 -cellw 88% -T +RUN rm ./notebooks/*.txt +RUN rm ./notebooks/*.sh + # Add examples ADD ./examples ./examples +ADD ./examples/requirements.ex.txt ./examples/requirements.ex.txt +RUN pip install -r ./examples/requirements.ex.txt +RUN rm ./examples/requirements.ex.txt -# Add local docs -ADD ./docs ./docs -ADD ./maro ./maro -ADD setup.py setup.py -ADD ./scripts ./scripts -RUN bash scripts/install_maro.sh -RUN pip install -U -r ./docs/requirements.docs.txt -RUN cd docs; make html -RUN rm -rf ./maro -RUN rm setup.py -RUN rm -rf ./scripts +# Install MARO +RUN pip install pymaro +ENV PYTHONPATH ./ # Add run cmd ADD ./scripts/run_playground.sh ./run.sh -# Add readme +# Add README ADD ./playground.md ./README.md -# Clean -RUN rm ./notebooks/*.txt -RUN rm ./notebooks/*.sh -RUN rm -r ./docs/source -RUN rm ./docs/make.bat -RUN rm ./docs/Makefile -RUN rm ./docs/README.md -RUN rm ./docs/requirements.docs.txt -RUN rm -rf ./build -RUN rm -rf ./pymaro.egg-info - -# Install maro -RUN pip install pymaro - # Start service CMD ["/bin/bash", "./run.sh"] diff --git a/docs/source/apidoc/maro.utils.rst b/docs/source/apidoc/maro.utils.rst index 6e413a39a..edefd6fa4 100644 --- a/docs/source/apidoc/maro.utils.rst +++ b/docs/source/apidoc/maro.utils.rst @@ -45,10 +45,10 @@ maro.utils.exception.communication\_exception :undoc-members: :show-inheritance: -maro.utils.exception.data\_lib\_exeption +maro.utils.exception.data\_lib\_exception -------------------------------------------------------------------------------- -.. automodule:: maro.utils.exception.data_lib_exeption +.. automodule:: maro.utils.exception.data_lib_exception :members: :undoc-members: :show-inheritance: diff --git a/docs/source/examples/greedy_policy_citi_bike.rst b/docs/source/examples/greedy_policy_citi_bike.rst index 20fa2062a..601c016a7 100644 --- a/docs/source/examples/greedy_policy_citi_bike.rst +++ b/docs/source/examples/greedy_policy_citi_bike.rst @@ -76,4 +76,4 @@ This environment is driven by `real trip history data `_. + All related code snippets are supported in `maro playground `_. diff --git a/docs/source/index.rst b/docs/source/index.rst index 8ef7efafa..2b3fe29b4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -32,45 +32,35 @@ Quick Start .. code-block:: python from maro.simulator import Env - from maro.simulator.scenarios.cim.common import Action + from maro.simulator.scenarios.cim.common import Action, ActionType, DecisionEvent - # Initialize an environment with a specific scenario, related topology. - # In Container Inventory Management, 1 tick means 1 day, here durations=100 means a length of 100 days + from random import randint + + # Initialize an Env for cim scenario env = Env(scenario="cim", topology="toy.5p_ssddd_l0.0", start_tick=0, durations=100) - # Query environment summary, which includes business instances, intra-instance attributes, etc. - print(env.summary) - - for ep in range(2): - # Gym-like step function. - metrics, decision_event, is_done = env.step(None) - - while not is_done: - past_week_ticks = [ - x for x in range(decision_event.tick - 7, decision_event.tick) - ] - decision_port_idx = decision_event.port_idx - intr_port_infos = ["booking", "empty", "shortage"] - - # Query the snapshot list of the environment to get the information of - # the booking, empty container inventory, shortage of the decision port in the past week. - past_week_info = env.snapshot_list["ports"][ - past_week_ticks : decision_port_idx : intr_port_infos - ] - - dummy_action = Action( - vessel_idx=decision_event.vessel_idx, - port_idx=decision_event.port_idx, - quantity=0 - ) - - # Drive environment with dummy action (no repositioning). - metrics, decision_event, is_done = env.step(dummy_action) - - # Query environment business metrics at the end of an episode, - # it is your optimized object (usually includes multi-target). - print(f"ep: {ep}, environment metrics: {env.metrics}") - env.reset() + metrics: object = None + decision_event: DecisionEvent = None + is_done: bool = False + action: Action = None + + # Start the env with a None Action + metrics, decision_event, is_done = env.step(None) + + while not is_done: + # Generate a random Action according to the action_scope in DecisionEvent + action_scope = decision_event.action_scope + to_discharge = action_scope.discharge > 0 and randint(0, 1) > 0 + + action = Action( + decision_event.vessel_idx, + decision_event.port_idx, + randint(0, action_scope.discharge if to_discharge else action_scope.load), + ActionType.DISCHARGE if to_discharge else ActionType.LOAD + ) + + # Respond the environment with the generated Action + metrics, decision_event, is_done = env.step(action) Contents ---------- diff --git a/docs/source/installation/playground.rst b/docs/source/installation/playground.rst index 34ffc33f6..e7ba52b6b 100644 --- a/docs/source/installation/playground.rst +++ b/docs/source/installation/playground.rst @@ -2,16 +2,15 @@ Playground Docker Image ======================= -Pull from `Docker Hub `_ +Pull from `Docker Hub `_ ------------------------------------------------------------------------------------------------------------------ .. code-block:: sh # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 arthursjiang/maro:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground Run from Source --------------- @@ -25,9 +24,8 @@ Run from Source # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 maro/playground:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground * Windows @@ -38,9 +36,8 @@ Run from Source # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 maro/playground:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground Major Services in Playground ---------------------------- @@ -54,15 +51,12 @@ Major Services in Playground * - ``Redis Commander`` - Redis web GUI. - http://127.0.0.1:40009 - * - ``Read the Docs`` - - Local host docs. - - http://127.0.0.1:40010 * - ``Jupyter Lab`` - Jupyter lab with MARO environment, examples, notebooks. - - http://127.0.0.1:40011 + - http://127.0.0.1:40010 -*(If you use other port mapping, remember to change the port number.)* +*(Remember to change ports if you use different ports mapping.)* Major Materials in Root Folder ------------------------------ @@ -78,4 +72,4 @@ Major Materials in Root Folder - Quick-start tutorial. -*(Those not mentioned in the table can be ignored.)* +*(The ones not mentioned in this table can be ignored.)* diff --git a/docs/source/key_components/data_model.rst b/docs/source/key_components/data_model.rst index eebd5fae9..03c2be0cf 100644 --- a/docs/source/key_components/data_model.rst +++ b/docs/source/key_components/data_model.rst @@ -8,7 +8,7 @@ the backend language for improving the execution reference. What's more, the backend store is a pluggable design, user can choose different backend implementation based on their real performance requirement and device limitation. -Currenty there are two data model backend implementation: static and dynamic. +Currently there are two data model backend implementation: static and dynamic. Static implementation used Numpy as its data store, do not support dynamic attribute length, the advance of this version is that its memory size is same as its declaration. diff --git a/docs/source/scenarios/citi_bike.rst b/docs/source/scenarios/citi_bike.rst index bec912069..a78c8f5ec 100644 --- a/docs/source/scenarios/citi_bike.rst +++ b/docs/source/scenarios/citi_bike.rst @@ -22,7 +22,7 @@ too few empty docks. In such a situation, the bike repositioning is essential to balance the bike's supply and demand. A good bike repositioning can not only meet the needs in the stations with heavy ride demand but also free the stations that do not have enough empty docks. Also, in the long run, a good bike repositioning -can improve the bike useability, empower citizens' daily life, and reduce the +can improve the bike usability, empower citizens' daily life, and reduce the carbon emission. Resource Flow diff --git a/docs/source/scenarios/container_inventory_management.rst b/docs/source/scenarios/container_inventory_management.rst index 3e5e8ab98..f044901b0 100644 --- a/docs/source/scenarios/container_inventory_management.rst +++ b/docs/source/scenarios/container_inventory_management.rst @@ -571,10 +571,8 @@ Once we get a ``DecisionEvent`` from the environment, we should respond with an * **vessel_idx** (int): The id of the vessel/operation object of the port/agent. * **port_idx** (int): The id of the port/agent that take this action. - * **quantity** (int): The sign of this value denotes different meanings: - - * Positive quantity means discharging empty containers from vessel to port. - * Negative quantity means loading empty containers from port to vessel. + * **action_type** (ActionType): Whether to load or discharge empty containers in this action. + * **quantity** (int): The (non-negative) quantity of empty containers to be loaded/discharged. Example ^^^^^^^ @@ -583,61 +581,36 @@ Here we will show you a simple example of interaction with the environment in random mode, we hope this could help you learn how to use the environment interfaces: .. code-block:: python + from maro.simulator import Env + from maro.simulator.scenarios.cim.common import Action, ActionType, DecisionEvent + + from random import randint + + # Initialize an Env for cim scenario + env = Env(scenario="cim", topology="toy.5p_ssddd_l0.0", start_tick=0, durations=100) + + metrics: object = None + decision_event: DecisionEvent = None + is_done: bool = False + action: Action = None + + # Start the env with a None Action + metrics, decision_event, is_done = env.step(None) + + while not is_done: + # Generate a random Action according to the action_scope in DecisionEvent + action_scope = decision_event.action_scope + to_discharge = action_scope.discharge > 0 and randint(0, 1) > 0 + + action = Action( + decision_event.vessel_idx, + decision_event.port_idx, + randint(0, action_scope.discharge if to_discharge else action_scope.load), + ActionType.DISCHARGE if to_discharge else ActionType.LOAD + ) - from maro.simulator import Env - from maro.simulator.scenarios.cim.common import Action, DecisionEvent - - import random - - # Initialize an environment of CIM scenario, with a specific topology. - # In Container Inventory Management, 1 tick means 1 day, durations=100 here indicates a length of 100 days. - env = Env(scenario="cim", topology="toy.5p_ssddd_l0.0", start_tick=0, durations=100) - - # Query for the environment summary, the business instances and intra-instance attributes - # will be listed in the output for your reference. - print(env.summary) - - metrics: object = None - decision_event: DecisionEvent = None - is_done: bool = False - action: Action = None - - num_episode = 2 - for ep in range(num_episode): - # Gym-like step function. - metrics, decision_event, is_done = env.step(None) - - while not is_done: - past_week_ticks = [ - x for x in range(decision_event.tick - 7, decision_event.tick) - ] - decision_port_idx = decision_event.port_idx - intr_port_infos = ["booking", "empty", "shortage"] - - # Query the snapshot list of this environment to get the information of - # the booking, empty, shortage of the decision port in the past week. - past_week_info = env.snapshot_list["ports"][ - past_week_ticks : decision_port_idx : intr_port_infos - ] - - # Generate a random Action according to the action_scope in DecisionEvent. - random_quantity = random.randint( - -decision_event.action_scope.load, - decision_event.action_scope.discharge - ) - action = Action( - vessel_idx=decision_event.vessel_idx, - port_idx=decision_event.port_idx, - quantity=random_quantity - ) - - # Drive the environment with the random action. - metrics, decision_event, is_done = env.step(action) - - # Query for the environment business metrics at the end of each episode, - # it is usually users' optimized object in CIM scenario (usually includes multi-target). - print(f"ep: {ep}, environment metrics: {env.metrics}") - env.reset() + # Respond the environment with the generated Action + metrics, decision_event, is_done = env.step(action) Jump to `this notebook `_ for a quick experience. @@ -646,7 +619,7 @@ Visualization ------------- The resource holders in this scenario is the port and vessel. -In order to facilitate users to select specific data and +In order to facilitate users to select specific data and observe the overall or partial data trend, the visualization tool provides data selection options in two dimensions: Inter-epoch view & Intra-epoch view. diff --git a/examples/citi_bike/online_lp/README.md b/examples/citi_bike/online_lp/README.md index ae5b7ab02..3cedc729d 100644 --- a/examples/citi_bike/online_lp/README.md +++ b/examples/citi_bike/online_lp/README.md @@ -1,5 +1,23 @@ # Performance +## Solver Used + +To get stable problem solution, the experiments are made with **GLPK solver**. +To use GLPK solver, you need to first install the GLPK package in your platform +accordingly. Then replace the *PULP_CBC_CMD* solver with GLPK in the code. + +```python +from pulp import GLPK + +class CitiBikeILP(): + + def _formulate_and_solve(...): + ... + problem.solve(GLPK(msg=0)) +``` + +## Configuration + Below are the final environment metrics of the onlineLP in different topologies. For each experiment, we setup the environment and test for a duration of 1 week with environment random seed 0, 128, 1024. Besides the parameter listed in the diff --git a/examples/citi_bike/online_lp/citi_bike_ilp.py b/examples/citi_bike/online_lp/citi_bike_ilp.py index c2ce86a2d..d8954d15b 100644 --- a/examples/citi_bike/online_lp/citi_bike_ilp.py +++ b/examples/citi_bike/online_lp/citi_bike_ilp.py @@ -2,7 +2,7 @@ from typing import List, Tuple import numpy as np -from pulp import GLPK, LpInteger, LpMaximize, LpProblem, LpVariable, lpSum +from pulp import PULP_CBC_CMD, LpInteger, LpMaximize, LpProblem, LpVariable, lpSum from maro.utils import DottableDict @@ -176,7 +176,7 @@ def _formulate_and_solve( self._init_variables(init_inventory=init_inventory) self._add_constraints(problem=problem, demand=demand, supply=supply) self._set_objective(problem=problem) - problem.solve(GLPK(msg=0)) + problem.solve(PULP_CBC_CMD(msg=0)) # ============================= private end ============================= diff --git a/examples/citi_bike/online_lp/launcher.py b/examples/citi_bike/online_lp/launcher.py index 477cfeb6f..dfc776617 100644 --- a/examples/citi_bike/online_lp/launcher.py +++ b/examples/citi_bike/online_lp/launcher.py @@ -8,7 +8,7 @@ from citi_bike_ilp import CitiBikeILP from maro.data_lib import BinaryReader, ItemTickPicker -from maro.event_buffer import Event +from maro.event_buffer import AbsEvent from maro.forecasting import OneStepFixWindowMA as Forecaster from maro.simulator import Env from maro.simulator.scenarios.citi_bike.adj_loader import load_adj_from_csv @@ -50,7 +50,7 @@ def __init__( # ============================= private start ============================= - def _record_history(self, env_tick: int, finished_events: List[Event]): + def _record_history(self, env_tick: int, finished_events: List[AbsEvent]): """ Args: env_tick (int): The current Env tick. @@ -136,7 +136,7 @@ def __peep_at_the_future(self, env_tick: int): # ============================= private end ============================= - def get_action_list(self, env_tick: int, init_inventory: np.ndarray, finished_events: List[Event]): + def get_action_list(self, env_tick: int, init_inventory: np.ndarray, finished_events: List[AbsEvent]): if PEEP_AND_USE_REAL_DATA: demand, supply = self.__peep_at_the_future(env_tick=env_tick) else: diff --git a/examples/hello_world/cim/hello.py b/examples/hello_world/cim/hello.py index 879e497a3..3e564775a 100644 --- a/examples/hello_world/cim/hello.py +++ b/examples/hello_world/cim/hello.py @@ -1,25 +1,19 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from random import seed, randint + from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType if __name__ == "__main__": - start_tick = 0 - durations = 100 # 100 days - - opts = dict() - """ - enable-dump-snapshot parameter means business_engine needs dump snapshot data before reset. - If you leave value to empty string, it will dump to current folder. - For getting dump data, please uncomment below line and specify dump destination folder. - """ # Initialize an environment with a specific scenario, related topology. - env = Env(scenario="cim", topology="global_trade.22p_l0.1", - start_tick=start_tick, durations=durations) + env = Env(scenario="cim", topology="global_trade.22p_l0.1", start_tick=0, durations=100) + # To reset environmental data before starting a new experiment. env.reset() + # Query environment summary, which includes business instances, intra-instance attributes, etc. print(env.summary) @@ -28,25 +22,18 @@ metrics, decision_event, is_done = env.step(None) while not is_done: - past_week_ticks = [x for x in range( - max(decision_event.tick - 7, 0), decision_event.tick)] - decision_port_idx = decision_event.port_idx - intr_port_infos = ["booking", "empty", "shortage"] - - # Query the decision port booking, empty container inventory, shortage information in the past week - past_week_info = env.snapshot_list["ports"][past_week_ticks: - decision_port_idx: - intr_port_infos] + action_scope = decision_event.action_scope + to_discharge = action_scope.discharge > 0 and randint(0, 1) > 0 - dummy_action = Action( + random_action = Action( decision_event.vessel_idx, decision_event.port_idx, - 0, - ActionType.LOAD + randint(0, action_scope.discharge if to_discharge else action_scope.load), + ActionType.DISCHARGE if to_discharge else ActionType.LOAD ) # Drive environment with dummy action (no repositioning) - metrics, decision_event, is_done = env.step(dummy_action) + metrics, decision_event, is_done = env.step(random_action) # Query environment business metrics at the end of an episode, # it is your optimized object (usually includes multi-target). diff --git a/examples/hello_world/cim/hello_geo_vis.py b/examples/hello_world/cim/hello_geo_vis.py index 54f159940..d358a90e7 100644 --- a/examples/hello_world/cim/hello_geo_vis.py +++ b/examples/hello_world/cim/hello_geo_vis.py @@ -9,32 +9,25 @@ os.environ["MARO_STREAMIT_EXPERIMENT_NAME"] = "experiment_example" +from random import seed, randint + from maro.simulator import Env -from maro.simulator.scenarios.cim.common import Action, ActionType +from maro.simulator.scenarios.cim.common import Action, ActionScope, ActionType from maro.streamit import streamit if __name__ == "__main__": - start_tick = 0 - durations = 100 # 100 days + seed(0) + NUM_EPISODE = 2 - opts = dict() with streamit: - """ - enable-dump-snapshot parameter means business_engine needs dump snapshot data before reset. - If you leave value to empty string, it will dump to current folder. - For getting dump data, please uncomment below line and specify dump destination folder. - """ - # Initialize an environment with a specific scenario, related topology. - env = Env(scenario="cim", topology="global_trade.22p_l0.1", - start_tick=start_tick, durations=durations, options=opts) + env = Env(scenario="cim", topology="global_trade.22p_l0.1", start_tick=0, durations=100) + # To reset environmental data before starting a new experiment. env.reset() - # Query environment summary, which includes business instances, intra-instance attributes, etc. - print(env.summary) - for ep in range(2): + for ep in range(NUM_EPISODE): # Tell streamit we are in a new episode. streamit.episode(ep) @@ -42,25 +35,18 @@ metrics, decision_event, is_done = env.step(None) while not is_done: - past_week_ticks = [x for x in range( - max(decision_event.tick - 7, 0), decision_event.tick)] - decision_port_idx = decision_event.port_idx - intr_port_infos = ["booking", "empty", "shortage"] - - # Query the decision port booking, empty container inventory, shortage information in the past week - past_week_info = env.snapshot_list["ports"][past_week_ticks: - decision_port_idx: - intr_port_infos] + action_scope = decision_event.action_scope + to_discharge = action_scope.discharge > 0 and randint(0, 1) > 0 - dummy_action = Action( + random_action = Action( decision_event.vessel_idx, decision_event.port_idx, - 0, - ActionType.LOAD + randint(0, action_scope.discharge if to_discharge else action_scope.load), + ActionType.DISCHARGE if to_discharge else ActionType.LOAD ) # Drive environment with dummy action (no repositioning) - metrics, decision_event, is_done = env.step(dummy_action) + metrics, decision_event, is_done = env.step(random_action) # Query environment business metrics at the end of an episode, # it is your optimized object (usually includes multi-target). diff --git a/examples/hello_world/cim/hello_streamlit.py b/examples/hello_world/cim/hello_streamlit.py index 1875ff5f1..31855cfba 100644 --- a/examples/hello_world/cim/hello_streamlit.py +++ b/examples/hello_world/cim/hello_streamlit.py @@ -1,13 +1,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from random import seed, randint + from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType if __name__ == "__main__": - start_tick = 0 - durations = 100 # 100 days + seed(0) + NUM_EPISODE = 2 opts = dict() """ @@ -20,37 +22,29 @@ # Initialize an environment with a specific scenario, related topology. env = Env( scenario="cim", topology="global_trade.22p_l0.1", - start_tick=start_tick, durations=durations, options=opts + start_tick=0, durations=100, options=opts ) + # To reset environmental data before starting a new experiment. env.reset() - # Query environment summary, which includes business instances, intra-instance attributes, etc. - print(env.summary) - for ep in range(2): + for ep in range(NUM_EPISODE): # Gym-like step function. metrics, decision_event, is_done = env.step(None) while not is_done: - past_week_ticks = [x for x in range( - max(decision_event.tick - 7, 0), decision_event.tick)] - decision_port_idx = decision_event.port_idx - intr_port_infos = ["booking", "empty", "shortage"] - - # Query the decision port booking, empty container inventory, shortage information in the past week - past_week_info = env.snapshot_list["ports"][past_week_ticks: - decision_port_idx: - intr_port_infos] + action_scope = decision_event.action_scope + to_discharge = action_scope.discharge > 0 and randint(0, 1) > 0 - dummy_action = Action( + random_action = Action( decision_event.vessel_idx, decision_event.port_idx, - 0, - ActionType.LOAD + randint(0, action_scope.discharge if to_discharge else action_scope.load), + ActionType.DISCHARGE if to_discharge else ActionType.LOAD ) # Drive environment with dummy action (no repositioning) - metrics, decision_event, is_done = env.step(dummy_action) + metrics, decision_event, is_done = env.step(random_action) # Query environment business metrics at the end of an episode, # it is your optimized object (usually includes multi-target). diff --git a/examples/hello_world/test_vm.py b/examples/hello_world/test_vm.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/requirements.ex.txt b/examples/requirements.ex.txt new file mode 100644 index 000000000..dbdc8c561 --- /dev/null +++ b/examples/requirements.ex.txt @@ -0,0 +1 @@ +PuLP==2.1 diff --git a/examples/vector_env/hello.py b/examples/vector_env/hello.py index 3a1eec24e..1d630373b 100644 --- a/examples/vector_env/hello.py +++ b/examples/vector_env/hello.py @@ -1,46 +1,66 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from maro.simulator.scenarios.cim.common import Action, DecisionEvent +from enum import Enum +from maro.simulator.scenarios.cim.common import Action, ActionType, DecisionEvent from maro.vector_env import VectorEnv +class VectorEnvUsage(Enum): + PUSH_ONE_FORWARD = "push the first environment forward and left others behind" + PUSH_ALL_FORWARD = "push all environments forward together" + +USAGE = VectorEnvUsage.PUSH_ONE_FORWARD if __name__ == "__main__": with VectorEnv(batch_num=4, scenario="cim", topology="toy.5p_ssddd_l0.0", durations=100) as env: - for ep in range(2): - print("current episode:", ep) + for USAGE in [VectorEnvUsage.PUSH_ONE_FORWARD, VectorEnvUsage.PUSH_ALL_FORWARD]: + print(f"******************************************************") + print(f"Mode: {USAGE} ({USAGE.value})") + intermediate_status_reported = False metrics, decision_event, is_done = (None, None, False) while not is_done: action = None - # Usage: - # 1. Only push speicified (1st for this example) environment, leave others behind - # if decision_event: - # env0_dec: DecisionEvent = decision_event[0] - - # # 1.1 After 1st environment is done, then others will push forward. - # if env0_dec: - # ss0 = env.snapshot_list["vessels"][env0_dec.tick:env0_dec.vessel_idx:"remaining_space"] - # action = {0: Action(env0_dec.vessel_idx, env0_dec.port_idx, -env0_dec.action_scope.load)} - - # 2. Only pass action to 1st environment (give None to other environments), - # but keep pushing all the environment, until the end if decision_event: env0_dec: DecisionEvent = decision_event[0] + # Showcase: how to access information from snapshot list in vector env. if env0_dec: ss0 = env.snapshot_list["vessels"][env0_dec.tick:env0_dec.vessel_idx:"remaining_space"] - action = [None] * env.batch_number + # 1. Only push specified (1st for this example) environment, leave others behind. + if USAGE == VectorEnvUsage.PUSH_ONE_FORWARD and env0_dec: + # Only action for the 1st Env. After 1st environment is done, then others will push forward. + action = { + 0: Action( + vessel_idx=env0_dec.vessel_idx, + port_idx=env0_dec.port_idx, + quantity=env0_dec.action_scope.load, + action_type=ActionType.LOAD + ) + } - # with a list of action, will push all environment to next step - action[0] = Action(env0_dec.vessel_idx, env0_dec.port_idx, -env0_dec.action_scope.load) + # 2. Only pass action to 1st environment (give None to other environments), + # but keep pushing all the environment, until the end + elif USAGE == VectorEnvUsage.PUSH_ALL_FORWARD and env0_dec: + # With a list of action, will push all environment to next step. + action = [None] * env.batch_number + action[0] = Action( + vessel_idx=env0_dec.vessel_idx, + port_idx=env0_dec.port_idx, + quantity=env0_dec.action_scope.load, + action_type=ActionType.LOAD + ) metrics, decision_event, is_done = env.step(action) - print("Final tick for each environment:", env.tick) - print("Final frame index for each environment:", env.frame_index) + if not intermediate_status_reported and env.tick[0] >= 99: + print(f"When env 0 reach tick {env.tick[0]}, ticks for each environment: {env.tick}") + intermediate_status_reported = True + + print(f"Final tick for each environment: {env.tick}") + print(f"Final frame index for each environment: {env.frame_index}") env.reset() diff --git a/examples/vm_scheduling/rule_based_algorithm/README.md b/examples/vm_scheduling/rule_based_algorithm/README.md index 94b1c11e7..82166dce8 100644 --- a/examples/vm_scheduling/rule_based_algorithm/README.md +++ b/examples/vm_scheduling/rule_based_algorithm/README.md @@ -6,7 +6,7 @@ The VM scheduling problem is one of the quintessential use cases of the data cen ## Algorithms -We offer some rule-based algorithms for VM scheduling problem, inlcuding: +We offer some rule-based algorithms for VM scheduling problem, includings: - **Random Pick**: When a VM request is coming, randomly choose an available PM with enough resources to place the VM. - **First Fit**: When a VM request is coming, choose the available PM with enough resources whose index is smallest to place the VM. @@ -16,11 +16,11 @@ We offer some rule-based algorithms for VM scheduling problem, inlcuding: - Remaining Memory: Choose the available PM with minimal memory. - Remaining CPU Cores and Energy Consumption: Choose the available PM with minimal CPU cores and maximum energy consumption. - **Bin Packing**: Divide the PMs into several bins based on their remaining CPU cores. When a VM request is coming, allocate it to the bin, which can make the variance of the PM number in each bin as small as possible. If there are more than one PM in the chosen bin, randomly choose an available PM with enough resources. -- **Round Robin**: If the algorithm pick the PM $i$ at time $t−1$, then the algorithm will pick PM $i+1$ at current time $t$ for the comming VM request if PM $i+1$ is available, otherwise the algorithm will pick PM $i+2$ ... until the PM $i+k$ is available. +- **Round Robin**: If the algorithm pick the PM $i$ at time $t−1$, then the algorithm will pick PM $i+1$ at current time $t$ for the coming VM request if PM $i+1$ is available, otherwise the algorithm will pick PM $i+2$ ... until the PM $i+k$ is available. ## Metrics -We offer some metrics for evaluating the performance of each rule based algorithms, inlcuding: +We offer some metrics for evaluating the performance of each rule based algorithms, including: - **Failed Allocation**: The number of the failed allocation VMs. - **Energy Consumption**: The total energy consumption of PMs. diff --git a/maro/README.rst b/maro/README.rst index 9f40c3628..a63d78a50 100644 --- a/maro/README.rst +++ b/maro/README.rst @@ -1,5 +1,45 @@ +.. image:: https://img.shields.io/pypi/l/pymaro + :target: https://github.com/microsoft/maro/blob/master/LICENSE + :alt: License + + +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/platform.svg + :target: https://pypi.org/project/pymaro/ + :alt: Platform + + +.. image:: https://img.shields.io/pypi/pyversions/pymaro.svg?logo=python&logoColor=white + :target: https://pypi.org/project/pymaro/#files + :alt: Python Versions + + +.. image:: https://img.shields.io/github/languages/code-size/microsoft/maro + :target: https://github.com/microsoft/maro + :alt: Code Size + + +.. image:: https://img.shields.io/docker/image-size/maro2020/maro + :target: https://hub.docker.com/repository/docker/maro2020/maro/tags?page=1 + :alt: Docker Size + + +.. image:: https://img.shields.io/github/issues/microsoft/maro + :target: https://github.com/microsoft/maro/issues + :alt: Issues + + +.. image:: https://img.shields.io/github/issues-pr/microsoft/maro + :target: https://github.com/microsoft/maro/pulls + :alt: Pull Requests + + +.. image:: https://img.shields.io/librariesio/github/microsoft/maro + :target: https://libraries.io/pypi/pymaro + :alt: Dependencies + + .. image:: https://github.com/microsoft/maro/workflows/test/badge.svg :target: https://github.com/microsoft/maro/actions?query=workflow%3Atest :alt: test @@ -11,7 +51,7 @@ .. image:: https://github.com/microsoft/maro/workflows/docker/badge.svg - :target: https://hub.docker.com/repository/docker/arthursjiang/maro + :target: https://hub.docker.com/repository/docker/maro2020/maro :alt: docker @@ -22,26 +62,92 @@ .. image:: https://img.shields.io/pypi/v/pymaro :target: https://pypi.org/project/pymaro/#files - :alt: Python Versions + :alt: PypI Versions -.. image:: https://img.shields.io/pypi/pyversions/pymaro.svg?logo=python&logoColor=white +.. image:: https://img.shields.io/pypi/wheel/pymaro :target: https://pypi.org/project/pymaro/#files - :alt: Python Versions + :alt: Wheel +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/citi_bike.svg + :target: https://maro.readthedocs.io/en/latest/scenarios/citi_bike.html + :alt: Citi Bike -.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/logo.svg - :target: https://github.com/microsoft/maro + +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/cim.svg + :target: https://maro.readthedocs.io/en/latest/scenarios/container_inventory_management.html + :alt: CIM + + +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/vm_scheduling.svg + :target: https://maro.readthedocs.io/en/latest/scenarios/vm_scheduling.html + :alt: VM Scheduling + + +.. image:: https://img.shields.io/gitter/room/microsoft/maro + :target: https://gitter.im/Microsoft/MARO# + :alt: Gitter + + +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/stack_overflow.svg + :target: https://stackoverflow.com/questions/ask?tags=maro + :alt: Stack Overflow + + +.. image:: https://img.shields.io/github/release-date-pre/microsoft/maro + :target: https://github.com/microsoft/maro/releases + :alt: Releases + + +.. image:: https://img.shields.io/github/commits-since/microsoft/maro/latest/master + :target: https://github.com/microsoft/maro/commits/master + :alt: Commits + + +.. image:: https://github.com/microsoft/maro/workflows/vulnerability%20scan/badge.svg + :target: https://github.com/microsoft/maro/actions?query=workflow%3A%22vulnerability+scan%22 + :alt: Vulnerability Scan + + +.. image:: https://github.com/microsoft/maro/workflows/lint/badge.svg + :target: https://github.com/microsoft/maro/actions?query=workflow%3Alint + :alt: Lint + + +.. image:: https://img.shields.io/codecov/c/github/microsoft/maro + :target: https://codecov.io/gh/microsoft/maro + :alt: Coverage + + +.. image:: https://img.shields.io/pypi/dm/pymaro + :target: https://pypi.org/project/pymaro/#files + :alt: Downloads + + +.. image:: https://img.shields.io/docker/pulls/maro2020/maro + :target: https://hub.docker.com/repository/docker/maro2020/maro + :alt: Docker Pulls + + +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/play_with_maro.svg + :target: https://hub.docker.com/r/maro2020/maro + :alt: Play with MARO + + + +.. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/logo.svg + :target: https://maro.readthedocs.io/en/latest/ :alt: MARO LOGO -======================================================================================================= +================================================================================================================ Multi-Agent Resource Optimization (MARO) platform is an instance of Reinforcement learning as a Service (RaaS) for real-world resource optimization. It can be -applied to many important industrial domains, such as container inventory -management in logistics, bike repositioning in transportation, virtual machine -provisioning in data centers, and asset management in finance. Besides +applied to many important industrial domains, such as `container inventory +management `_ +in logistics, `bike repositioning `_ +in transportation, `virtual machine `_ provisioning in data centers, and asset management in finance. Besides `Reinforcement Learning `_ (RL), it also supports other planning/decision mechanisms, such as `Operations Research `_. @@ -57,8 +163,8 @@ Key Components of MARO: of user-defined functions for message auto-handling, cluster provision, and job orchestration. -.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/maro_overview.svg - :target: https://maro.readthedocs.io/en/latest/ +.. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/maro_overview.svg + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/maro_overview.svg :alt: MARO Key Components @@ -80,18 +186,22 @@ Contents - MARO quick-start notebooks. +*Try `MARO playground <#run-playground>`_ to have a quick experience.* + Install MARO from `PyPI `_ ---------------------------------------------------------------------- +*Notes: The CLI commands (including the visualization tool) are not included in pymaro package. To enable these support, you need to install from source.* -* - Max OS / Linux + +* + Mac OS / Linux .. code-block:: sh pip install pymaro -* +* Windows .. code-block:: powershell @@ -101,24 +211,26 @@ Install MARO from `PyPI `_ pip install pymaro -Install MARO from Source (\ `Editable Mode `_\ ) ------------------------------------------------------------------------------------------------------------------------- +Install MARO from Source +------------------------ + +*Notes: Install from source if you want to use the CLI commands (including the visualization tool).* -* +* Prerequisites * C++ Compiler * Linux or Mac OS X: ``gcc`` - * Windows: `Build Tools for Visual Studio 2017 `_ + * Windows: `Build Tools for Visual Studio 2017 `_ -* +* Enable Virtual Environment - * + * Mac OS / Linux .. code-block:: sh @@ -127,23 +239,30 @@ Install MARO from Source (\ `Editable Mode `_ +------------------------------------------------------------------------- + +.. code-block:: sh + + # Enable environment dump feature, when initializing the environment instance + env = Env(scenario="cim", + topology="toy.5p_ssddd_l0.0", + start_tick=0, + durations=100, + options={"enable-dump-snapshot": "./dump_data"}) + + # Inspect environment with the dump data + maro inspector dashboard --source_path ./dump_data/YOUR_SNAPSHOT_DUMP_FOLDER + +Show Cases +^^^^^^^^^^ + + +* + Case I - Container Inventory Management + + .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_inter_epoch.gif + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_inter_epoch.gif + :alt: CIM Inter Epoch + + + .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_intra_epoch_by_ports.gif + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_intra_epoch_by_ports.gif + :alt: CIM Intra Epoch + + +* + Case II - Citi Bike + + .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_inter_epoch.gif + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_inter_epoch.gif + :alt: Citi Bike Inter Epoch + + + .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_intra_epoch_by_station.gif + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_intra_epoch_by_station.gif + :alt: Citi Bike Intra Epoch + + Run Playground -------------- -* - Pull from `Docker Hub `_ +* + Pull from `Docker Hub `_ .. code-block:: sh + # Pull the docker image from docker hub + docker pull maro2020/playground + # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 arthursjiang/maro:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground -* +* Build from source - * + * Mac OS / Linux .. code-block:: sh @@ -204,11 +387,10 @@ Run Playground # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 maro/playground:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground - * + * Windows .. code-block:: powershell @@ -218,9 +400,8 @@ Run Playground # Run playground container. # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Local host docs -> http://127.0.0.1:40010 - # Jupyter lab with maro -> http://127.0.0.1:40011 - docker run -p 40009:40009 -p 40010:40010 -p 40011:40011 maro/playground:cpu + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground Contributing ------------ @@ -242,9 +423,37 @@ For more information see the or contact `opencode@microsoft.com `_ with any additional questions or comments. +Related Papers +-------------- + +`Container Inventory Management `_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +.. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/scenario/cim_vis.gif + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/scenario/cim_vis.gif + :alt: CIM Vis + + +Wenlei Shi, Xinran Wei, Jia Zhang, Xiaoyuan Ni, Arthur Jiang, Jiang Bian, Tie-Yan Liu. "\ `Cooperative Policy Learning with Pre-trained Heterogeneous Observation Representations `_\ ". AAMAS 2021 + +Xihan Li, Jia Zhang, Jiang Bian, Yunhai Tong, Tie-Yan Liu. "\ `A Cooperative Multi-Agent Reinforcement Learning Framework for Resource Balancing in Complex Logistics Network `_\ ". AAMAS 2019 + +Related News +------------ + +`MSRA Top-10 Hack-Techs in 2021 `_ + +`Open Source Platform MARO: Anywhere Door for Resource Optimization `_ + +`AI from "Point" to "Surface" `_ + +`Cite Us `_ +--------------------------------------------------------------------- + License ------- Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the `MIT <./LICENSE>`_ License. +Licensed under the `MIT `_ License. diff --git a/maro/__misc__.py b/maro/__misc__.py index ee55d26de..d647fe08c 100644 --- a/maro/__misc__.py +++ b/maro/__misc__.py @@ -2,6 +2,6 @@ # Licensed under the MIT license. -__version__ = "0.2.2a3" +__version__ = "0.2.4a1" __data_version__ = "0.2" diff --git a/maro/cli/envs/list_available.py b/maro/cli/envs/list_available.py index b17d9434e..279844add 100644 --- a/maro/cli/envs/list_available.py +++ b/maro/cli/envs/list_available.py @@ -25,7 +25,7 @@ def list_available(): # maro env list def list_scenarios(**kwargs): """ - Show all avaiable scenarios + Show all available scenarios """ for scenario in get_scenarios(): diff --git a/maro/cli/grass/lib/services/master_agent/agent.py b/maro/cli/grass/lib/services/master_agent/agent.py index 58d4bd26b..af8f281f8 100644 --- a/maro/cli/grass/lib/services/master_agent/agent.py +++ b/maro/cli/grass/lib/services/master_agent/agent.py @@ -209,7 +209,7 @@ def update_name_to_container_details(self) -> None: class ContainerRuntimeAgent(multiprocessing.Process): """Container runtime agent. - Auto-restart the container if it matchs the constraint of fault recovery. + Auto-restart the container if it matches the constraint of fault recovery. """ def __init__(self, local_cluster_details: dict, local_master_details: dict, check_interval: int = 5): diff --git a/maro/cli/maro_real_time_vis/back_end/vis_app/app.py b/maro/cli/maro_real_time_vis/back_end/vis_app/app.py index 73279fe03..c54d68535 100644 --- a/maro/cli/maro_real_time_vis/back_end/vis_app/app.py +++ b/maro/cli/maro_real_time_vis/back_end/vis_app/app.py @@ -244,5 +244,5 @@ def get_acc_attrs(): # Use Only For Local Debug # ************************************ if __name__ == '__main__': - app_backend.run(debug=True, port=5000) + app_backend.run(debug=False, port=5000, host="0.0.0.0") # ************************************ diff --git a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_decision.py b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_decision.py index 84b6f41e9..66269b528 100644 --- a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_decision.py +++ b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_decision.py @@ -50,9 +50,12 @@ def get_acc_decision_data(experiment_name: str, episode: str, start_tick: str, e """ input_range = get_input_range(start_tick, end_tick) + query = f"select {request_column.decision_header.value} from {experiment_name}.full_on_vessels"\ + f" where episode='{episode}'" + if input_range != "()": + query += f" and tick in {input_range}" params = { - "query": f"select {request_column.decision_header.value} from {experiment_name}.full_on_vessels" - f" where episode='{episode}' and tick in {input_range}", + "query": query, "count": "true" } original_decision_data = requests.get( diff --git a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_exp_info.py b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_exp_info.py index a95bc6874..ecc1eb8d8 100644 --- a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_exp_info.py +++ b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_exp_info.py @@ -66,7 +66,7 @@ def get_experiment_info(): data_in_format["start_episode"] = start_episode_num data_in_format['start_snapshot'] = start_snapshot_num total_params = { - "query": f"select count(episode), count(tick) from {experiment_name}.port_details", + "query": f"select count_distinct(episode), count_distinct(tick) from {experiment_name}.port_details", "count": "true" } total_episode = requests.get( diff --git a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_order.py b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_order.py index af0ddc7d1..ae563534b 100644 --- a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_order.py +++ b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_order.py @@ -50,9 +50,12 @@ def get_acc_order_data(experiment_name: str, episode: str, start_tick: str, end_ """ input_range = get_input_range(start_tick, end_tick) + query = f"select {request_column.order_header.value} from {experiment_name}.full_on_ports"\ + f" where episode='{episode}'" + if input_range != "()": + query += f" and tick in {input_range}" params = { - "query": f"select {request_column.order_header.value} from {experiment_name}.full_on_ports" - f" where episode='{episode}' and tick in {input_range}", + "query": query, "count": "true" } original_order_data = requests.get( diff --git a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_port.py b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_port.py index 1e38b9f8e..205eb6e23 100644 --- a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_port.py +++ b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_port.py @@ -71,9 +71,12 @@ def get_acc_port_data(experiment_name: str, episode: str, start_tick: str, end_t """ input_range = get_input_range(start_tick, end_tick) + query = f"select {request_column.port_header.value} from {experiment_name}.port_details"\ + f" where episode='{episode}'" + if input_range != "()": + query += f" and tick in {input_range}" params = { - "query": f"select {request_column.port_header.value} from {experiment_name}.port_details" - f" where episode='{episode}' and tick in {input_range}", + "query": query, "count": "true" } db_port_data = requests.get( diff --git a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_vessel.py b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_vessel.py index 84951b4e3..f0523d22c 100644 --- a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_vessel.py +++ b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/request_vessel.py @@ -51,9 +51,12 @@ def get_acc_vessel_data(experiment_name: str, episode: str, start_tick: str, end """ input_range = get_input_range(start_tick, end_tick) + query = f"select {request_column.vessel_header.value} from {experiment_name}.vessel_details"\ + f" where episode='{episode}'" + if input_range != "()": + query += f" and tick in {input_range}" params = { - "query": f"select {request_column.vessel_header.value} from {experiment_name}.vessel_details" - f" where episode='{episode}' and tick in {input_range}", + "query": query, "count": "true" } db_vessel_data = requests.get( diff --git a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/utils.py b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/utils.py index 1ed9806d0..cf049e513 100644 --- a/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/utils.py +++ b/maro/cli/maro_real_time_vis/back_end/vis_app/data_process/request/utils.py @@ -34,12 +34,4 @@ def get_input_range(start_tick: str, end_tick: str) -> str: str: Range of tick in string format. """ - i = int(start_tick) - input_range = "(" - while i < int(end_tick): - if i == int(end_tick) - 1: - input_range = input_range + "'" + str(i) + "'" + ")" - else: - input_range = input_range + "'" + str(i) + "'" + "," - i = i + 1 - return input_range + return "(" + ", ".join([f"'{i}'" for i in range(int(start_tick), int(end_tick))]) + ")" diff --git a/maro/cli/maro_real_time_vis/start_maro_geo_vis.py b/maro/cli/maro_real_time_vis/start_maro_geo_vis.py index 6da7d62a4..16186e5b9 100644 --- a/maro/cli/maro_real_time_vis/start_maro_geo_vis.py +++ b/maro/cli/maro_real_time_vis/start_maro_geo_vis.py @@ -12,12 +12,15 @@ from .back_end.vis_app.data_process.request.request_params import request_settings +GEO_FRONT_SERVICE_DOCKER_IMAGE = "maro2020/geo_front_service" +DEFAULT_FRONT_END_PORT = 8080 + def start_geo_vis(start: str, experiment_name: str, front_end_port: int, **kwargs: dict): grader_path = os.path.dirname(os.path.dirname(os.path.abspath(os.path.dirname(__file__)))) if start == 'database': - # Start the databse container. + # Start the database container. database_start_path = f"{grader_path}/streamit/server" subprocess.check_call( 'sh run_docker.sh', @@ -77,15 +80,14 @@ def start_geo_vis(start: str, experiment_name: str, front_end_port: int, **kwarg # Start front-end docker container. exec_path = os.path.dirname(inspect.getfile(inspect.currentframe())) - os.system("docker pull meroychen/geo_front_service") + os.system(f"docker pull {GEO_FRONT_SERVICE_DOCKER_IMAGE}") os.system("docker stop geo-vis") os.system("docker rm geo-vis") - if front_end_port is not None: - os.system(f"docker run -d -p {front_end_port}:8080 --name geo-vis meroychen/geo_front_service") - else: - os.system("docker run -d -p 8080:8080 --name geo-vis meroychen/geo_front_service") + os.system( + f"docker run -d -p {front_end_port if front_end_port is not None else DEFAULT_FRONT_END_PORT}:8080 " + f"--name geo-vis {GEO_FRONT_SERVICE_DOCKER_IMAGE}" + ) back_end_path = f"{exec_path}/back_end/vis_app/app.py" os.system(f"python {back_end_path}") - else: raise CliError("Please input 'database' or 'service'.") diff --git a/maro/cli/utils/node_admin.py b/maro/cli/utils/node_admin.py index bffdb859a..c77f9df32 100644 --- a/maro/cli/utils/node_admin.py +++ b/maro/cli/utils/node_admin.py @@ -7,7 +7,7 @@ def start_admin(*args, **kwargs): - print("""If got python moudle or file not found error, please run + print("""If got python module or file not found error, please run maro admin stop maro meta deploy maro admin req diff --git a/maro/cli/utils/web_terminal/terminal-srv.py b/maro/cli/utils/web_terminal/terminal-srv.py index b5fa92b58..ef9864a79 100644 --- a/maro/cli/utils/web_terminal/terminal-srv.py +++ b/maro/cli/utils/web_terminal/terminal-srv.py @@ -104,7 +104,7 @@ def connect(): cmd = " ".join(shlex.quote(c) for c in app.config["cmd"]) print("child pid is", child_pid) print( - f"starting background task with command `{cmd}` to continously read " + f"starting background task with command `{cmd}` to continuously read " "and forward pty output to client" ) socketio.start_background_task(target=read_and_forward_pty_output) diff --git a/maro/data_lib/cim/README.md b/maro/data_lib/cim/README.md index b0b2c4087..460808631 100644 --- a/maro/data_lib/cim/README.md +++ b/maro/data_lib/cim/README.md @@ -2,34 +2,19 @@ Current the real data input are supported by the **CimRealDataLoader** and **CimRealDataContainer** implemented in *maro/data_lib/cim/cim_data_loader.py* and *maro/data_lib/cim/cim_data_container.py* respectively. -## Configurations in the Topology File *config.yml* +To build up a topology with real data as input, different to the synthetic mode, no need for any *config.yml* file, +just put the input data files under the folder of the specific topology. -To build up a topology with real data as input, 2 parts of configurations are required for the config.yml: - -- input_setting: describes what type of the input data files and where are the data files. -- transfer_cost_factors: used by the business engine to calculate the transfer cost automatically. - -Here is an example for the *config.yml* file: - -```text -input_setting: - from_files: true - input_type: real # real, dumped - data_folder: "~\.maro\data\cim\test.new_schema" - -transfer_cost_factors: - load: 0.05 - dsch: 0.05 -``` - -## Input CSV Files +## Input Data Files The required input data files includes: *misc.yml, orders.csv, ports.csv, routes.csv, stops.csv, vessels.csv*. -The *misc.yml* file includes 5 settings, they are (for example): +The *misc.yml* file includes 7 settings, they are (for example): ```text max_tick: 224 container_volume: 1 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 past_stop_number: 4 future_stop_number: 3 seed: 4096 diff --git a/maro/data_lib/cim/cim_data_container.py b/maro/data_lib/cim/cim_data_container.py index 64db6c3b0..40971e009 100644 --- a/maro/data_lib/cim/cim_data_container.py +++ b/maro/data_lib/cim/cim_data_container.py @@ -6,14 +6,14 @@ from math import ceil from typing import Dict, List +from maro.simulator.utils import random + from .entities import ( CimBaseDataCollection, CimRealDataCollection, CimSyntheticDataCollection, NoisedItem, Order, OrderGenerateMode, - PortSetting, VesselSetting + PortSetting, SyntheticPortSetting, VesselSetting ) from .port_buffer_tick_wrapper import PortBufferTickWrapper -from .utils import ( - apply_noise, buffer_tick_rand, get_buffer_tick_seed, get_order_num_seed, list_sum_normalize, order_num_rand -) +from .utils import BUFFER_TICK_RAND_KEY, ORDER_NUM_RAND_KEY, apply_noise, list_sum_normalize from .vessel_future_stops_prediction import VesselFutureStopsPrediction from .vessel_past_stops_wrapper import VesselPastStopsWrapper from .vessel_reachable_stops_wrapper import VesselReachableStopsWrapper @@ -42,7 +42,7 @@ class CimBaseDataContainer(ABC): Args: data_collection (CimBaseDataCollection): Corresponding data collection. """ - def __init__(self, data_collection: CimBaseDataCollection): + def __init__(self, data_collection: CimBaseDataCollection) -> None: self._data_collection = data_collection # wrapper for interfaces, to make it easy to use @@ -60,9 +60,6 @@ def __init__(self, data_collection: CimBaseDataCollection): self._vessel_plan_wrapper = VesselSailingPlanWrapper(self._data_collection) self._reachable_stops_wrapper = VesselReachableStopsWrapper(self._data_collection) - # keep the seed so we can reproduce the sequence after reset - self._buffer_tick_seed: int = get_buffer_tick_seed() - # flag to tell if we need to reset seed, we need this flag as outside may set the seed after env.reset self._is_need_reset_seed = False @@ -76,25 +73,35 @@ def future_stop_number(self) -> int: """int: Number of future stops to predict to store in snapshot.""" return self._data_collection.future_stop_number + @property + def load_cost_factor(self) -> float: + """float: Factor of the cost for each empty load.""" + return self._data_collection.load_cost_factor + + @property + def dsch_cost_factor(self) -> float: + """float: Factor of the cost for each empty discharge.""" + return self._data_collection.dsch_cost_factor + @property def ports(self) -> List[PortSetting]: """List[PortSetting]: List of port initial settings.""" - return self._data_collection.ports_settings + return self._data_collection.port_settings @property def port_number(self) -> int: """int: Number of ports.""" - return len(self._data_collection.ports_settings) + return len(self._data_collection.port_settings) @property def vessels(self) -> List[VesselSetting]: """List[VesselSetting]: List of vessel initial settings.""" - return self._data_collection.vessels_settings + return self._data_collection.vessel_settings @property def vessel_number(self) -> int: """int: Number of vessels.""" - return len(self._data_collection.vessels_settings) + return len(self._data_collection.vessel_settings) @property def container_volume(self) -> int: @@ -144,7 +151,7 @@ def full_return_buffers(self) -> PortBufferTickWrapper: .. code-block:: python # Get full return buffer tick of port 0. - buffer_tick = data_cnr.full_return_buffers[0] + buffer_tick = data_cntr.full_return_buffers[0] """ return self._full_return_buffer_wrapper @@ -202,14 +209,14 @@ def reachable_stops(self) -> VesselReachableStopsWrapper: return self._reachable_stops_wrapper @property - def vessel_period(self) -> int: - """Wrapper to get vessel's planed sailing period (without noise to complete a whole route). + def vessel_period(self) -> List[int]: + """Wrapper to get vessel's planned sailing period (without noise to complete a whole route). Examples: .. code-block:: python - # Get planed sailing for vessel 0. + # Get planned sailing for vessel 0. period = data_cntr.vessel_period[0] """ return self._data_collection.vessel_period_without_noise @@ -234,8 +241,8 @@ def reset(self): self._is_need_reset_seed = True def _reset_seed(self): - """Reset internal seed for generate reproduceable data""" - buffer_tick_rand.seed(self._buffer_tick_seed) + """Reset internal seed for generate reproduce-able data""" + random.reset_seed(BUFFER_TICK_RAND_KEY) @abstractmethod def get_orders(self, tick: int, total_empty_container: int) -> List[Order]: @@ -262,9 +269,6 @@ class CimSyntheticDataContainer(CimBaseDataContainer): def __init__(self, data_collection: CimSyntheticDataCollection): super().__init__(data_collection) - # keep the seed so we can reproduce the sequence after reset - self._order_num_seed: int = get_order_num_seed() - # TODO: get_events which composed with arrive, departure and order def get_orders(self, tick: int, total_empty_container: int) -> List[Order]: @@ -284,16 +288,16 @@ def get_orders(self, tick: int, total_empty_container: int) -> List[Order]: self._is_need_reset_seed = False - if tick >= self._data_collection.max_tick: + if tick >= self._data_collection.max_tick: # pragma: no cover warnings.warn(f"{tick} out of max tick {self._data_collection.max_tick}") return [] return self._gen_orders(tick, total_empty_container) def _reset_seed(self): - """Reset internal seed for generate reproduceable data""" + """Reset internal seed for generate reproduce-able data""" super()._reset_seed() - order_num_rand.seed(self._order_num_seed) + random.reset_seed(ORDER_NUM_RAND_KEY) def _gen_orders(self, tick: int, total_empty_container: int) -> List[Order]: """Generate order for specified tick. @@ -304,6 +308,7 @@ def _gen_orders(self, tick: int, total_empty_container: int) -> List[Order]: """ # result order_list: List[Order] = [] + assert isinstance(self._data_collection, CimSyntheticDataCollection) order_proportion = self._data_collection.order_proportion order_mode = self._data_collection.order_mode total_containers = self._data_collection.total_containers @@ -312,7 +317,7 @@ def _gen_orders(self, tick: int, total_empty_container: int) -> List[Order]: orders_to_gen = int(order_proportion[tick]) # if under unfixed mode, we will consider current empty container as factor - if order_mode == OrderGenerateMode.UNFIXED: + if order_mode == OrderGenerateMode.UNFIXED: # pragma: no cover. TODO: remove this mark later delta = total_containers - total_empty_container if orders_to_gen <= delta: @@ -327,9 +332,11 @@ def _gen_orders(self, tick: int, total_empty_container: int) -> List[Order]: # calculate orders distribution for each port as source for port_idx in range(self.port_number): - source_dist: NoisedItem = self.ports[port_idx].source_proportion + port = self.ports[port_idx] + assert isinstance(port, SyntheticPortSetting) + source_dist: NoisedItem = port.source_proportion - noised_source_order_number = apply_noise(source_dist.base, source_dist.noise, order_num_rand) + noised_source_order_number = apply_noise(source_dist.base, source_dist.noise, random[ORDER_NUM_RAND_KEY]) noised_source_order_dist.append(noised_source_order_number) @@ -342,11 +349,13 @@ def _gen_orders(self, tick: int, total_empty_container: int) -> List[Order]: if remaining_orders == 0: break - targets_dist: List[NoisedItem] = self.ports[port_idx].target_proportions + port = self.ports[port_idx] + assert isinstance(port, SyntheticPortSetting) + targets_dist: List[NoisedItem] = port.target_proportions # apply noise and normalize noised_targets_dist = list_sum_normalize( - [apply_noise(target.base, target.noise, order_num_rand) for target in targets_dist]) + [apply_noise(target.base, target.noise, random[ORDER_NUM_RAND_KEY]) for target in targets_dist]) # order for current ports cur_port_order_num = ceil(orders_to_gen * noised_source_order_dist[port_idx]) @@ -399,6 +408,7 @@ def __init__(self, data_collection: CimRealDataCollection): super().__init__(data_collection) # orders + assert isinstance(self._data_collection, CimRealDataCollection) self._orders: Dict[int, List[Order]] = self._data_collection.orders def get_orders(self, tick: int, total_empty_container: int) -> List[Order]: @@ -418,11 +428,8 @@ def get_orders(self, tick: int, total_empty_container: int) -> List[Order]: self._is_need_reset_seed = False - if tick >= self._data_collection.max_tick: + if tick >= self._data_collection.max_tick: # pragma: no cover warnings.warn(f"{tick} out of max tick {self._data_collection.max_tick}") return [] - if tick not in self._orders: - return [] - - return self._orders[tick] + return self._orders[tick] if tick in self._orders else [] diff --git a/maro/data_lib/cim/cim_data_container_helpers.py b/maro/data_lib/cim/cim_data_container_helpers.py index 972f26104..e2b2dcfec 100644 --- a/maro/data_lib/cim/cim_data_container_helpers.py +++ b/maro/data_lib/cim/cim_data_container_helpers.py @@ -3,21 +3,21 @@ import os import urllib.parse - -from yaml import safe_load +from typing import Optional from maro.cli.data_pipeline.utils import StaticParameter -from maro.simulator.utils import seed +from maro.simulator.utils import random, seed from .cim_data_container import CimBaseDataContainer, CimRealDataContainer, CimSyntheticDataContainer -from .cim_data_generator import CimDataGenerator +from .cim_data_generator import gen_cim_data from .cim_data_loader import load_from_folder, load_real_data_from_folder +from .utils import DATA_CONTAINER_INIT_SEED_LIMIT, ROUTE_INIT_RAND_KEY class CimDataContainerWrapper: def __init__(self, config_path: str, max_tick: int, topology: str): - self._data_cntr: CimBaseDataContainer = None + self._data_cntr: Optional[CimBaseDataContainer] = None self._max_tick = max_tick self._config_path = config_path self._start_tick = 0 @@ -30,23 +30,28 @@ def __init__(self, config_path: str, max_tick: int, topology: str): self._init_data_container() - def _init_data_container(self): - # read config - with open(self._config_path, "r") as fp: - conf: dict = safe_load(fp) - if "input_setting" in conf and conf["input_setting"]["from_files"]: - if conf["input_setting"]["input_type"] == "real": - self._data_cntr = data_from_files(data_folder=conf["input_setting"]["data_folder"]) - else: - self._data_cntr = data_from_dumps(dumps_folder=conf["input_setting"]["data_folder"]) - else: + def _init_data_container(self, topology_seed: int = None): + if not os.path.exists(self._config_path): + raise FileNotFoundError + # Synthetic Data Mode: config.yml must exist. + config_path = os.path.join(self._config_path, "config.yml") + if os.path.exists(config_path): self._data_cntr = data_from_generator( - config_path=self._config_path, max_tick=self._max_tick, start_tick=self._start_tick + config_path=config_path, max_tick=self._max_tick, start_tick=self._start_tick, + topology_seed=topology_seed ) + elif os.path.exists(os.path.join(self._config_path, "order_proportion.csv")): + self._data_cntr = data_from_dumps(dumps_folder=self._config_path) + else: + # Real Data Mode: read data from input data files, no need for any config.yml. + self._data_cntr = data_from_files(data_folder=self._config_path) - def reset(self): + def reset(self, keep_seed: bool): """Reset data container internal state""" - self._data_cntr.reset() + if not keep_seed: + self._init_data_container(random[ROUTE_INIT_RAND_KEY].randint(0, DATA_CONTAINER_INIT_SEED_LIMIT - 1)) + else: + self._data_cntr.reset() def __getattr__(self, name): return getattr(self._data_cntr, name) @@ -71,20 +76,22 @@ def data_from_dumps(dumps_folder: str) -> CimSyntheticDataContainer: return CimSyntheticDataContainer(data_collection) -def data_from_generator(config_path: str, max_tick: int, start_tick: int = 0) -> CimSyntheticDataContainer: +def data_from_generator(config_path: str, max_tick: int, start_tick: int = 0, + topology_seed: int = None) -> CimSyntheticDataContainer: """Collect data from data generator with configurations. Args: config_path(str): Path of configuration file (yaml). max_tick (int): Max tick to generate data. start_tick(int): Start tick to generate data. + topology_seed(int): Random seed of the business engine. \ + 'None' means using the seed in the configuration file. Returns: CimSyntheticDataContainer: Data container used to provide cim data related interfaces. """ - edg = CimDataGenerator() - - data_collection = edg.gen_data(config_path, start_tick=start_tick, max_tick=max_tick) + data_collection = gen_cim_data( + config_path, start_tick=start_tick, max_tick=max_tick, topology_seed=topology_seed) return CimSyntheticDataContainer(data_collection) diff --git a/maro/data_lib/cim/cim_data_dump.py b/maro/data_lib/cim/cim_data_dump.py index 9c634ccf6..a53770555 100644 --- a/maro/data_lib/cim/cim_data_dump.py +++ b/maro/data_lib/cim/cim_data_dump.py @@ -8,8 +8,25 @@ import numpy as np from yaml import safe_dump -from .cim_data_generator import CimDataGenerator -from .entities import CimSyntheticDataCollection +from .cim_data_generator import gen_cim_data +from .entities import CimSyntheticDataCollection, SyntheticPortSetting + + +def _dump_csv_file(file_path: str, headers: List[str], line_generator: callable): + """helper method to dump csv file + + Args: + file_path(str): path of output csv file + headers(List[str]): list of header + line_generator(callable): generator function to generate line to write + """ + with open(file_path, "wt+", newline="") as fp: + writer = csv.writer(fp) + + writer.writerow(headers) + + for line in line_generator(): + writer.writerow(line) class CimDataDumpUtil: @@ -59,7 +76,7 @@ def _dump_stops(self, output_folder: str, vessel_idx2name_dict: dict, port_idx2n headers = ["vessel_name", "vessel_index", "port_name", "port_index", "arrival_tick", "departure_tick"] def stop_generator(): - for vessel_stops in self._data_collection.vessels_stops: + for vessel_stops in self._data_collection.vessel_stops: for stop in vessel_stops: yield [ vessel_idx2name_dict[stop.vessel_idx], @@ -70,7 +87,7 @@ def stop_generator(): stop.leave_tick ] - self._dump_csv_file(stops_file_path, headers, stop_generator) + _dump_csv_file(stops_file_path, headers, stop_generator) def _dump_ports(self, output_folder: str): """ @@ -85,7 +102,8 @@ def _dump_ports(self, output_folder: str): ] def port_generator(): - for port in self._data_collection.ports_settings: + for port in self._data_collection.port_settings: + assert isinstance(port, SyntheticPortSetting) yield [ port.index, port.name, @@ -99,7 +117,7 @@ def port_generator(): port.full_return_buffer.noise ] - self._dump_csv_file(ports_file_path, headers, port_generator) + _dump_csv_file(ports_file_path, headers, port_generator) def _dump_vessels(self, output_folder: str): """ @@ -116,7 +134,7 @@ def _dump_vessels(self, output_folder: str): route_mapping = self._data_collection.route_mapping port_mapping = self._data_collection.port_mapping - vessels = self._data_collection.vessels_settings + vessels = self._data_collection.vessel_settings vessel_period = self._data_collection.vessel_period_without_noise def vessel_generator(): @@ -137,7 +155,7 @@ def vessel_generator(): vessel.empty ] - self._dump_csv_file(vessels_file_path, headers, vessel_generator) + _dump_csv_file(vessels_file_path, headers, vessel_generator) def _dump_routes(self, output_folder: str, route_idx2name_dict: dict): """ @@ -161,7 +179,7 @@ def route_generator(): point.distance_to_next_port ] - self._dump_csv_file(routes_file_path, headers, route_generator) + _dump_csv_file(routes_file_path, headers, route_generator) def _dump_order_proportions(self, output_folder: str, port_idx2name_dict: dict): """ @@ -175,10 +193,11 @@ def _dump_order_proportions(self, output_folder: str, port_idx2name_dict: dict): "dest_port_index", "proportion", "proportion_noise" ] - ports = self._data_collection.ports_settings + ports = self._data_collection.port_settings def order_prop_generator(): for port in ports: + assert isinstance(port, SyntheticPortSetting) for prop in port.target_proportions: yield [ port.name, @@ -189,7 +208,7 @@ def order_prop_generator(): prop.noise ] - self._dump_csv_file(proportion_file_path, headers, order_prop_generator) + _dump_csv_file(proportion_file_path, headers, order_prop_generator) def _dump_misc(self, output_folder: str): """ @@ -201,6 +220,8 @@ def _dump_misc(self, output_folder: str): "past_stop_number": self._data_collection.past_stop_number, "future_stop_number": self._data_collection.future_stop_number, "container_volume": self._data_collection.container_volume, + "load_cost_factor": self._data_collection.load_cost_factor, + "dsch_cost_factor": self._data_collection.dsch_cost_factor, "max_tick": self._data_collection.max_tick, "seed": self._data_collection.seed, "version": self._data_collection.version @@ -211,22 +232,6 @@ def _dump_misc(self, output_folder: str): with open(misc_file_path, "wt+") as fp: safe_dump(misc_items, fp) - def _dump_csv_file(self, file_path: str, headers: List[str], line_generator: callable): - """helper method to dump csv file - - Args: - file_path(str): path of output csv file - headers(List[str]): list of header - line_generator(callable): generator function to generate line to write - """ - with open(file_path, "wt+", newline="") as fp: - writer = csv.writer(fp) - - writer.writerow(headers) - - for line in line_generator(): - writer.writerow(line) - def dump_from_config(config_file: str, output_folder: str, max_tick: int): """Dump cim data from config, this will call data generator to generate data , and dump it. @@ -243,9 +248,7 @@ def dump_from_config(config_file: str, output_folder: str, max_tick: int): assert output_folder is not None and os.path.exists(output_folder), f"Got output folder path: {output_folder}" assert max_tick is not None and max_tick > 0, f"Got max tick: {max_tick}" - generator = CimDataGenerator() - - data_collection = generator.gen_data(config_file, max_tick=max_tick, start_tick=0) + data_collection = gen_cim_data(config_file, max_tick=max_tick, start_tick=0, topology_seed=None) dump_util = CimDataDumpUtil(data_collection) diff --git a/maro/data_lib/cim/cim_data_generator.py b/maro/data_lib/cim/cim_data_generator.py index 9d6895357..1a1ce43d1 100644 --- a/maro/data_lib/cim/cim_data_generator.py +++ b/maro/data_lib/cim/cim_data_generator.py @@ -6,183 +6,180 @@ from yaml import safe_load -from maro.simulator.utils import seed -from maro.utils.exception.data_lib_exeption import CimGeneratorInvalidParkingDuration +from maro.simulator.utils import random, seed from .entities import CimSyntheticDataCollection, OrderGenerateMode, Stop -from .global_order_proportion import GlobalOrderProportion -from .port_parser import PortsParser -from .route_parser import RoutesParser -from .utils import apply_noise, route_init_rand -from .vessel_parser import VesselsParser +from .parsers import parse_global_order_proportion, parse_ports, parse_routes, parse_vessels +from .utils import ROUTE_INIT_RAND_KEY, apply_noise CIM_GENERATOR_VERSION = 0x000001 -class CimDataGenerator: - """Utility to generate cim data from configuration file.""" +def _extend_route( + future_stop_number: int, max_tick: int, + vessels_setting, port_mapping, routes, route_mapping +) -> (List[List[Stop]], List[int]): + """Extend route with specified tick range.""" - def __init__(self): - # parsers - self._ports_parser = PortsParser() - self._vessels_parser = VesselsParser() - self._routes_parser = RoutesParser() - self._global_order_proportion = GlobalOrderProportion() + vessel_stops: List[List[Stop]] = [] + vessel_period_without_noise: List[int] = [] - def gen_data(self, config_file: str, max_tick: int, start_tick: int = 0) -> CimSyntheticDataCollection: - """Generate data with specified configurations. + # fill the result stops with empty list + # NOTE: not using [[]] * N + for _ in range(len(vessels_setting)): + vessel_stops.append([]) - Args: - config_file(str): File of configuration (yaml). - max_tick(int): Max tick to generate. - start_tick(int): Start tick to generate. + # extend for each vessel + for vessel_setting in vessels_setting: + route_name = vessel_setting.route_name - Returns: - CimSyntheticDataCollection: Data collection contains all cim data. - """ + # route definition points from configuration + route_points = routes[route_mapping[route_name]] + route_length = len(route_points) - # read config - with open(config_file, "r") as fp: - conf: dict = safe_load(fp) + loc_idx_in_route = 0 + # find the start point + while route_points[loc_idx_in_route].port_name != vessel_setting.start_port_name: + loc_idx_in_route += 1 + + # update initial value fields + speed = vessel_setting.sailing_speed + speed_noise = vessel_setting.sailing_noise + duration = vessel_setting.parking_duration + duration_noise = vessel_setting.parking_noise + + tick = 0 + period_no_noise = 0 + extra_stop_counter = 0 + stop_index = 0 + + # unpack the route by max tick and future stop number + while extra_stop_counter <= future_stop_number: + cur_route_point = route_points[loc_idx_in_route] + port_idx = port_mapping[cur_route_point.port_name] + + # apply noise to parking duration + parking_duration = ceil(apply_noise(duration, duration_noise, random[ROUTE_INIT_RAND_KEY])) + assert parking_duration > 0 + + # a new stop + stop = Stop( + stop_index, + tick, + tick + parking_duration, + port_idx, + vessel_setting.index + ) + + # append to current vessels stops list + vessel_stops[vessel_setting.index].append(stop) + + # use distance_to_next_port and speed (all with noise) to calculate tick of arrival and departure + distance_to_next_port = cur_route_point.distance_to_next_port + + # apply noise to speed + noised_speed = apply_noise(speed, speed_noise, random[ROUTE_INIT_RAND_KEY]) + sailing_duration = ceil(distance_to_next_port / noised_speed) + + # next tick + tick += parking_duration + sailing_duration + + # sailing durations without noise + whole_duration_no_noise = duration + ceil(distance_to_next_port / speed) + + # only add period at 1st route circle + period_no_noise += (whole_duration_no_noise if len( + vessel_stops[vessel_setting.index]) <= route_length else 0) + + # next location index + loc_idx_in_route = (loc_idx_in_route + 1) % route_length + + # counter to append extra stops which after max tick for future predict + extra_stop_counter += (1 if tick > max_tick else 0) + + stop_index += 1 + + vessel_period_without_noise.append(period_no_noise) + + return vessel_stops, vessel_period_without_noise + + +def gen_cim_data( + config_file: str, max_tick: int, + start_tick: int = 0, + topology_seed: int = None +) -> CimSyntheticDataCollection: + """Generate data with specified configurations. + + Args: + config_file(str): File of configuration (yaml). + max_tick(int): Max tick to generate. + start_tick(int): Start tick to generate. + topology_seed(int): Random seed of the business engine. \ + 'None' means using the seed in the configuration file. + + Returns: + CimSyntheticDataCollection: Data collection contains all cim data. + """ + + # read config + with open(config_file, "r") as fp: + conf: dict = safe_load(fp) + + if topology_seed is None: topology_seed = conf["seed"] - # set seed to generate data - seed(topology_seed) - - # misc configurations - total_containers = conf["total_containers"] - past_stop_number, future_stop_number = conf["stop_number"] - container_volumes = conf["container_volumes"] - order_mode = OrderGenerateMode(conf["order_generate_mode"]) - - # parse configurations - vessel_mapping, vessels_setting = self._vessels_parser.parse(conf["vessels"]) - port_mapping, ports_setting = self._ports_parser.parse(conf["ports"], total_containers) - route_mapping, routes = self._routes_parser.parse(conf["routes"]) - global_order_proportion = self._global_order_proportion.parse( - conf["container_usage_proportion"], - total_containers, start_tick=start_tick, max_tick=max_tick) - - # extend routes with specified tick range - vessels_stops, vessel_period_without_noise = self._extend_route( - future_stop_number, max_tick, vessels_setting, ports_setting, port_mapping, routes, route_mapping) - - return CimSyntheticDataCollection( - # Port - ports_setting, - port_mapping, - # Vessel - vessels_setting, - vessel_mapping, - # Stop - vessels_stops, - # Route - routes, - route_mapping, - # Vessel Period - vessel_period_without_noise, - # Volume/Container - container_volumes[0], - # Visible Voyage Window - past_stop_number, - future_stop_number, - # Time Length of the Data Collection - max_tick, - # Random Seed for Data Generation - topology_seed, - # For Order Generation - total_containers, - order_mode, - global_order_proportion, - # Data Generator Version - CIM_GENERATOR_VERSION) - - def _extend_route( - self, future_stop_number: int, max_tick: int, - vessels_setting, ports_setting, port_mapping, routes, route_mapping - ) -> (List[List[Stop]], List[int]): - """Extend route with specified tick range.""" - - vessels_stops: List[List[Stop]] = [] - vessel_period_without_noise: List[int] = [] - - # fill the result stops with empty list - # NOTE: not using [[]] * N - for _ in range(len(vessels_setting)): - vessels_stops.append([]) - - # extend for each vessel - for vessel_setting in vessels_setting: - route_name = vessel_setting.route_name - - # route definition points from configuration - route_points = routes[route_mapping[route_name]] - route_length = len(route_points) - - loc_idx_in_route = 0 - - # find the start point - while route_points[loc_idx_in_route].port_name != vessel_setting.start_port_name: - loc_idx_in_route += 1 - - # update initial value fields - speed = vessel_setting.sailing_speed - speed_noise = vessel_setting.sailing_noise - duration = vessel_setting.parking_duration - duration_noise = vessel_setting.parking_noise - - tick = 0 - period_no_noise = 0 - extra_stop_counter = 0 - stop_index = 0 - - # unpack the route by max tick and future stop number - while extra_stop_counter <= future_stop_number: - cur_route_point = route_points[loc_idx_in_route] - port_idx = port_mapping[cur_route_point.port_name] - - # apply noise to parking duration - parking_duration = ceil(apply_noise(duration, duration_noise, route_init_rand)) - - if parking_duration <= 0: - raise CimGeneratorInvalidParkingDuration() - - # a new stop - stop = Stop(stop_index, - tick, - tick + parking_duration, - port_idx, - vessel_setting.index) - - # append to current vessels stops list - vessels_stops[vessel_setting.index].append(stop) - - # use distance_to_next_port and speed (all with noise) to calculate tick of arrival and departure - distance_to_next_port = cur_route_point.distance_to_next_port - - # apply noise to speed - noised_speed = apply_noise(speed, speed_noise, route_init_rand) - sailing_duration = ceil(distance_to_next_port / noised_speed) - - # next tick - tick += parking_duration + sailing_duration - - # sailing durations without noise - whole_duration_no_noise = duration + ceil(distance_to_next_port / speed) - - # only add period at 1st route circle - period_no_noise += (whole_duration_no_noise if len( - vessels_stops[vessel_setting.index]) <= route_length else 0) - - # next location index - loc_idx_in_route = (loc_idx_in_route + 1) % route_length - - # counter to append extra stops which after max tick for future predict - extra_stop_counter += (1 if tick > max_tick else 0) - - stop_index += 1 - - vessel_period_without_noise.append(period_no_noise) - - return vessels_stops, vessel_period_without_noise + # set seed to generate data + seed(topology_seed) + + # misc configurations + total_containers = conf["total_containers"] + past_stop_number, future_stop_number = conf["stop_number"] + container_volumes = conf["container_volumes"] + + # parse configurations + vessel_mapping, vessels_setting = parse_vessels(conf["vessels"]) + port_mapping, ports_setting = parse_ports(conf["ports"], total_containers) + route_mapping, routes = parse_routes(conf["routes"]) + global_order_proportion = parse_global_order_proportion( + conf["container_usage_proportion"], + total_containers, start_tick=start_tick, max_tick=max_tick) + + # extend routes with specified tick range + vessel_stops, vessel_period_without_noise = _extend_route( + future_stop_number, max_tick, vessels_setting, port_mapping, routes, route_mapping) + + return CimSyntheticDataCollection( + # Port + port_settings=ports_setting, + port_mapping=port_mapping, + # Vessel + vessel_settings=vessels_setting, + vessel_mapping=vessel_mapping, + # Stop + vessel_stops=vessel_stops, + # Route + routes=routes, + route_mapping=route_mapping, + # Vessel Period + vessel_period_without_noise=vessel_period_without_noise, + # Volume/Container + container_volume=container_volumes[0], + # Cost Factors + load_cost_factor=conf["load_cost_factor"], + dsch_cost_factor=conf["dsch_cost_factor"], + # Visible Voyage Window + past_stop_number=past_stop_number, + future_stop_number=future_stop_number, + # Time Length of the Data Collection + max_tick=max_tick, + # Random Seed for Data Generation + seed=topology_seed, + # For Order Generation + total_containers=total_containers, + order_mode=OrderGenerateMode(conf["order_generate_mode"]), + order_proportion=global_order_proportion, + # Data Generator Version + version=str(CIM_GENERATOR_VERSION) + ) diff --git a/maro/data_lib/cim/cim_data_loader.py b/maro/data_lib/cim/cim_data_loader.py index 795da1efa..45ed1a722 100644 --- a/maro/data_lib/cim/cim_data_loader.py +++ b/maro/data_lib/cim/cim_data_loader.py @@ -5,9 +5,8 @@ import math import os import time -from abc import ABC, abstractmethod from collections import defaultdict -from typing import Dict, List +from typing import Dict, List, Tuple import numpy as np from yaml import safe_load @@ -15,418 +14,328 @@ from maro.data_lib import BinaryReader from .entities import ( - CimBaseDataCollection, CimRealDataCollection, CimSyntheticDataCollection, NoisedItem, Order, OrderGenerateMode, - PortSetting, RoutePoint, Stop, SyntheticPortSetting, VesselSetting + CimRealDataCollection, CimSyntheticDataCollection, NoisedItem, Order, OrderGenerateMode, PortSetting, RoutePoint, + Stop, SyntheticPortSetting, VesselSetting ) -class CimBaseDataLoader(ABC): - """Utility to load data from dump folder and real input data folder.""" - - @abstractmethod - def load(self, data_folder: str) -> CimBaseDataCollection: - pass - - def _load_misc(self, data_folder: str) -> dict: - """Load misc items from yaml""" - misc_file_path = os.path.join(data_folder, "misc.yml") - for _ in range(3): - if not os.path.exists(misc_file_path): - time.sleep(10) - with open(misc_file_path, "rt") as fp: - return safe_load(fp) - - def _read_csv_lines(self, file_path: str): - """Helper to read and yield line from csv file""" - for _ in range(3): - if not os.path.exists(file_path): - time.sleep(10) - with open(file_path, "rt") as fp: - reader = csv.DictReader(fp) - - for line in reader: - yield line - - def _load_vessels(self, data_folder: str) -> (Dict[str, int], List[VesselSetting]): - vessel_mapping: Dict[str, int] = {} - vessels: List[VesselSetting] = [] - - vessels_file_path = os.path.join(data_folder, "vessels.csv") - - for line in self._read_csv_lines(vessels_file_path): - vessel_name = line["name"] - vessel_index = int(line["index"]) - - vessel_mapping[vessel_name] = vessel_index - - vessel = VesselSetting( - vessel_index, - vessel_name, - int(line["capacity"]), - line["route_name"], - line["start_port_name"], - float(line["sailing_speed"]), - float(line["sailing_speed_noise"]), - int(line["parking_duration"]), - float(line["parking_noise"]), - int(line["empty"]) - ) +def _load_misc(data_folder: str) -> dict: + """Load misc items from yaml""" + misc_file_path = os.path.join(data_folder, "misc.yml") + for _ in range(3): # pragma: no cover + if not os.path.exists(misc_file_path): + time.sleep(10) + with open(misc_file_path, "rt") as fp: + return safe_load(fp) - vessels.append(vessel) - return vessel_mapping, vessels +def _read_csv_lines(file_path: str): + """Helper to read and yield line from csv file""" + for _ in range(3): # pragma: no cover + if not os.path.exists(file_path): + time.sleep(10) + with open(file_path, "rt") as fp: + reader = csv.DictReader(fp) - def _load_vessel_period(self, data_folder: str) -> List[int]: - vessels_file_path = os.path.join(data_folder, "vessels.csv") + for line in reader: + yield line - periods_without_noise: List[int] = [] - for line in self._read_csv_lines(vessels_file_path): - periods_without_noise.append(int(line["period"])) - return periods_without_noise +def _load_vessels(data_folder: str) -> (Dict[str, int], List[VesselSetting]): + vessel_mapping: Dict[str, int] = {} + vessels: List[VesselSetting] = [] - def _calculate_vessel_period( - self, vessels: List[VesselSetting], routes: List[List[RoutePoint]], route_mapping: Dict[str, int] - ) -> List[int]: - expected_periods: List[int] = [] - for vessel in vessels: - route_points = routes[route_mapping[vessel.route_name]] + vessels_file_path = os.path.join(data_folder, "vessels.csv") - expected_period = 0 - for route_point in route_points: - expected_period += ( - vessel.parking_duration - + math.ceil(route_point.distance_to_next_port / vessel.sailing_speed) - ) - expected_periods.append(expected_period) + for line in _read_csv_lines(vessels_file_path): + vessel_name = line["name"] + vessel_index = int(line["index"]) - return expected_periods + vessel_mapping[vessel_name] = vessel_index - def _load_routes(self, data_folder: str) -> (Dict[str, int], List[List[RoutePoint]]): - route_mapping: Dict[str, int] = {} - routes: List[List[RoutePoint]] = [] + vessel = VesselSetting( + vessel_index, + vessel_name, + int(line["capacity"]), + line["route_name"], + line["start_port_name"], + float(line["sailing_speed"]), + float(line["sailing_speed_noise"]), + int(line["parking_duration"]), + float(line["parking_noise"]), + int(line["empty"]) + ) - route_file_path = os.path.join(data_folder, "routes.csv") + vessels.append(vessel) - for line in self._read_csv_lines(route_file_path): - route_index = int(line["index"]) - route_name = line["name"] + return vessel_mapping, vessels - route_mapping[route_name] = route_index - if route_index >= len(routes): - routes.append([]) +def _load_vessel_period(data_folder: str) -> List[int]: + vessels_file_path = os.path.join(data_folder, "vessels.csv") - route_point = RoutePoint( - route_index, line["port_name"], float(line["distance_to_next_port"])) + periods_without_noise: List[int] = [] + for line in _read_csv_lines(vessels_file_path): + periods_without_noise.append(int(line["period"])) - routes[route_index].append(route_point) + return periods_without_noise - return route_mapping, routes - def _load_stops(self, data_folder: str, vessel_number: int) -> List[List[Stop]]: - bin_path = os.path.join(data_folder, "stops.bin") - if os.path.exists(bin_path): - return self._load_stops_from_bin(bin_path, vessel_number) - else: - print(f"No stops binary file in {data_folder}, read from csv file instead...") - csv_path = os.path.join(data_folder, "stops.csv") - return self._load_stops_from_csv(csv_path, vessel_number) +def _calculate_vessel_period( + vessels: List[VesselSetting], routes: List[List[RoutePoint]], route_mapping: Dict[str, int] +) -> List[int]: + expected_periods: List[int] = [] + for vessel in vessels: + route_points = routes[route_mapping[vessel.route_name]] - def _load_stops_from_csv(self, stops_file_path: str, vessel_number: int) -> List[List[Stop]]: - stops: List[List[Stop]] = [] + expected_period = 0 + for route_point in route_points: + expected_period += ( + vessel.parking_duration + + math.ceil(route_point.distance_to_next_port / vessel.sailing_speed) + ) + expected_periods.append(expected_period) - for _ in range(vessel_number): - stops.append([]) + return expected_periods - for line in self._read_csv_lines(stops_file_path): - vessel_stops: List[Stop] = stops[int(line["vessel_index"])] - stop = Stop( - len(vessel_stops), - int(line["arrival_tick"]), - int(line["departure_tick"]), - int(line["port_index"]), - int(line["vessel_index"]) - ) +def _load_routes(data_folder: str) -> (Dict[str, int], List[List[RoutePoint]]): + route_mapping: Dict[str, int] = {} + routes: List[List[RoutePoint]] = [] + + route_file_path = os.path.join(data_folder, "routes.csv") + + for line in _read_csv_lines(route_file_path): + route_index = int(line["index"]) + route_name = line["name"] + + route_mapping[route_name] = route_index + + if route_index >= len(routes): + routes.append([]) + + route_point = RoutePoint( + route_index, line["port_name"], int(line["distance_to_next_port"])) + + routes[route_index].append(route_point) + + return route_mapping, routes + + +def _load_stops_from_csv(stops_file_path: str, vessel_number: int) -> List[List[Stop]]: + stops: List[List[Stop]] = [] + + for _ in range(vessel_number): + stops.append([]) - vessel_stops.append(stop) - - return stops - - def _load_stops_from_bin(self, stops_file_path: str, vessel_number: int) -> List[List[Stop]]: - stops: List[List[Stop]] = [] - - for _ in range(vessel_number): - stops.append([]) - - reader = BinaryReader(stops_file_path) - - for stop_item in reader.items(): - vessel_stops: List[Stop] = stops[stop_item.vessel_index] - - stop = Stop(len(vessel_stops), - stop_item.timestamp, - stop_item.leave_tick, - stop_item.port_index, - stop_item.vessel_index) - - vessel_stops.append(stop) - - return stops - - -class CimDumpDataLoader(CimBaseDataLoader): - """Utility to load data from dump folder""" - - def load(self, data_folder: str) -> CimSyntheticDataCollection: - """Load data from dump folder - - NOTE: - dumps folder should contains following files. - ports.csv, vessels.csv, routes.csv, order_proportion.csv, - global_order_proportion.txt, misc.yml, stops.bin - - Args: - data_folders(str): folder that contains dumped files - - Returns: - CimSyntheticDataCollection: data collection for data container - """ - # load from files - misc_items = self._load_misc(data_folder) - order_target_proportion = self._load_order_proportions(data_folder) - port_mapping, ports = self._load_ports(data_folder, order_target_proportion) - route_mapping, routes = self._load_routes(data_folder) - vessel_mapping, vessels = self._load_vessels(data_folder) - periods_without_noise = self._load_vessel_period(data_folder) - stops = self._load_stops(data_folder, len(vessels)) - global_order_proportions = self._load_global_order_proportions(data_folder) - - # construct data collection - # NOTE: this is a namedtuple, so out-side cannot change it - data_collection = CimSyntheticDataCollection( - ports, - port_mapping, - vessels, - vessel_mapping, - stops, - routes, - route_mapping, - periods_without_noise, - misc_items["container_volume"], - misc_items["past_stop_number"], - misc_items["future_stop_number"], - misc_items["max_tick"], - misc_items["seed"], - misc_items["total_container"], - OrderGenerateMode(misc_items["order_mode"]), - global_order_proportions, - misc_items["version"] + for line in _read_csv_lines(stops_file_path): + vessel_stops: List[Stop] = stops[int(line["vessel_index"])] + + stop = Stop( + len(vessel_stops), + int(line["arrival_tick"]), + int(line["departure_tick"]), + int(line["port_index"]), + int(line["vessel_index"]) ) - return data_collection + vessel_stops.append(stop) - def _load_global_order_proportions(self, data_folder: str) -> np.ndarray: - """load global order proportions from txt file""" - global_order_prop_file = os.path.join( - data_folder, "global_order_proportion.txt") + return stops - return np.loadtxt(global_order_prop_file) - def _load_order_proportions(self, data_folder: str) -> Dict[int, List[NoisedItem]]: - """Load target order proportions from file""" - target_proportions: Dict[int, List[NoisedItem]] = defaultdict(list) +def _load_stops_from_bin(stops_file_path: str, vessel_number: int) -> List[List[Stop]]: + stops: List[List[Stop]] = [] - proportion_file_path = os.path.join(data_folder, "order_proportion.csv") + for _ in range(vessel_number): + stops.append([]) - for line in self._read_csv_lines(proportion_file_path): - source_port_index = int(line["source_port_index"]) + reader = BinaryReader(stops_file_path) - target_prop = NoisedItem( - int(line["dest_port_index"]), - float(line["proportion"]), - float(line["proportion_noise"]) - ) + for stop_item in reader.items(): + vessel_stops: List[Stop] = stops[stop_item.vessel_index] - target_proportions[source_port_index].append(target_prop) + stop = Stop(len(vessel_stops), + stop_item.timestamp, + stop_item.leave_tick, + stop_item.port_index, + stop_item.vessel_index) - return target_proportions + vessel_stops.append(stop) - def _load_ports(self, data_folder: str, order_target_proportion: dict) -> dict: - ports_file_path = os.path.join(data_folder, "ports.csv") + return stops - port_mapping: Dict[str, int] = {} - ports: List[SyntheticPortSetting] = [] - for line in self._read_csv_lines(ports_file_path): - port_name = line["name"] - port_index = int(line["index"]) +def _load_stops(data_folder: str, vessel_number: int) -> List[List[Stop]]: + bin_path = os.path.join(data_folder, "stops.bin") + if os.path.exists(bin_path): + return _load_stops_from_bin(bin_path, vessel_number) + else: + print(f"No stops binary file in {data_folder}, read from csv file instead...") + csv_path = os.path.join(data_folder, "stops.csv") + return _load_stops_from_csv(csv_path, vessel_number) - port_mapping[port_name] = port_index - full_rtn_buffer = NoisedItem( - port_index, - int(line["full_return_buffer"]), - int(line["full_return_buffer_noise"])) +def _load_global_order_proportions(data_folder: str) -> np.ndarray: + """load global order proportions from txt file""" + global_order_prop_file = os.path.join( + data_folder, "global_order_proportion.txt") - empty_rtn_buffer = NoisedItem( - port_index, - int(line["empty_return_buffer"]), - int(line["empty_return_buffer_noise"])) + return np.loadtxt(global_order_prop_file) - source_order_proportion = NoisedItem( - port_index, - float(line["order_proportion"]), - float(line["order_proportion_noise"]) - ) - port = SyntheticPortSetting( - port_index, - port_name, - int(line["capacity"]), - int(line["empty"]), - empty_rtn_buffer, - full_rtn_buffer, - source_order_proportion, - order_target_proportion[port_index] - ) +def _load_order_proportions(data_folder: str) -> Dict[int, List[NoisedItem]]: + """Load target order proportions from file""" + target_proportions: Dict[int, List[NoisedItem]] = defaultdict(list) + + proportion_file_path = os.path.join(data_folder, "order_proportion.csv") + + for line in _read_csv_lines(proportion_file_path): + source_port_index = int(line["source_port_index"]) - ports.append(port) - - return port_mapping, ports - - -class CimRealDataLoader(CimBaseDataLoader): - """Utility to load data from data folder""" - - def load(self, data_folder: str) -> CimRealDataCollection: - """Load data from data folder - - NOTE: - data folder should contains following files. - ports.csv, vessels.csv, routes.csv, order.csv, - misc.yml, stops.csv - - Args: - data_folders(str): folder that contains data files. - - Returns: - CimDataCollection: data collection for data container - """ - # load from files - misc_items = self._load_misc(data_folder) - port_mapping, ports = self._load_ports(data_folder) - route_mapping, routes = self._load_routes(data_folder) - vessel_mapping, vessels = self._load_vessels(data_folder) - periods_without_noise = self._calculate_vessel_period(vessels, routes, route_mapping) - stops = self._load_stops(data_folder, len(vessels)) - orders = self._load_orders(data_folder) - - # construct data collection - # NOTE: this is a namedtuple, so out-side cannot change it - data_collection = CimRealDataCollection( - ports, - port_mapping, - vessels, - vessel_mapping, - stops, - routes, - route_mapping, - periods_without_noise, - misc_items["container_volume"], - misc_items["past_stop_number"], - misc_items["future_stop_number"], - misc_items["max_tick"], - misc_items["seed"], - orders + target_prop = NoisedItem( + int(line["dest_port_index"]), + float(line["proportion"]), + float(line["proportion_noise"]) ) - return data_collection + target_proportions[source_port_index].append(target_prop) - def _load_ports(self, data_folder: str) -> dict: - ports_file_path = os.path.join(data_folder, "ports.csv") + return target_proportions - port_mapping: Dict[str, int] = {} - ports: List[PortSetting] = [] - for line in self._read_csv_lines(ports_file_path): - port_index = int(line["index"]) - port_name = line["name"] +def _load_ports_dump( + data_folder: str, order_target_proportion: Dict[int, List[NoisedItem]] +) -> Tuple[dict, List[SyntheticPortSetting]]: + ports_file_path = os.path.join(data_folder, "ports.csv") - port_mapping[port_name] = port_index + port_mapping: Dict[str, int] = {} + ports: List[SyntheticPortSetting] = [] - full_rtn_buffer = NoisedItem( - port_index, - int(line["full_return_buffer"]), - int(line["full_return_buffer_noise"]) - ) + for line in _read_csv_lines(ports_file_path): + port_name = line["name"] + port_index = int(line["index"]) - empty_rtn_buffer = NoisedItem( - port_index, - int(line["empty_return_buffer"]), - int(line["empty_return_buffer_noise"]) - ) + port_mapping[port_name] = port_index - port = PortSetting( - port_index, - port_name, - int(line["capacity"]), - int(line["empty"]), - empty_rtn_buffer, - full_rtn_buffer - ) + full_rtn_buffer = NoisedItem( + port_index, + int(line["full_return_buffer"]), + int(line["full_return_buffer_noise"])) + + empty_rtn_buffer = NoisedItem( + port_index, + int(line["empty_return_buffer"]), + int(line["empty_return_buffer_noise"])) + + source_order_proportion = NoisedItem( + port_index, + float(line["order_proportion"]), + float(line["order_proportion_noise"]) + ) - ports.append(port) - - return port_mapping, ports - - def _load_orders(self, data_folder: str) -> Dict[int, List[Order]]: - bin_path = os.path.join(data_folder, "orders.bin") - if os.path.exists(bin_path): - return self._load_orders_from_bin(bin_path) - else: - print(f"No orders binary file in {data_folder}, read from csv file instead...") - csv_path = os.path.join(data_folder, "orders.csv") - return self._load_orders_from_csv(csv_path) - - def _load_orders_from_csv(self, order_file_path: str) -> Dict[int, List[Order]]: - orders: Dict[int, List[Order]] = {} - - for line in self._read_csv_lines(order_file_path): - tick = int(line["tick"]) - if tick not in orders: - orders[tick] = [] - orders[tick].append( - Order( - int(line["tick"]), - int(line["source_port_index"]), - int(line["dest_port_index"]), - int(line["quantity"]) - ) + port = SyntheticPortSetting( + port_index, + port_name, + int(line["capacity"]), + int(line["empty"]), + empty_rtn_buffer, + full_rtn_buffer, + source_order_proportion, + order_target_proportion[port_index] + ) + + ports.append(port) + + return port_mapping, ports + + +def _load_ports_real(data_folder: str) -> Tuple[dict, List[PortSetting]]: + ports_file_path = os.path.join(data_folder, "ports.csv") + + port_mapping: Dict[str, int] = {} + ports: List[PortSetting] = [] + + for line in _read_csv_lines(ports_file_path): + port_index = int(line["index"]) + port_name = line["name"] + + port_mapping[port_name] = port_index + + full_rtn_buffer = NoisedItem( + port_index, + int(line["full_return_buffer"]), + int(line["full_return_buffer_noise"]) + ) + + empty_rtn_buffer = NoisedItem( + port_index, + int(line["empty_return_buffer"]), + int(line["empty_return_buffer_noise"]) + ) + + port = PortSetting( + port_index, + port_name, + int(line["capacity"]), + int(line["empty"]), + empty_rtn_buffer, + full_rtn_buffer + ) + + ports.append(port) + + return port_mapping, ports + + +def _load_orders_from_csv(order_file_path: str) -> Dict[int, List[Order]]: + orders: Dict[int, List[Order]] = {} + + for line in _read_csv_lines(order_file_path): + tick = int(line["tick"]) + if tick not in orders: + orders[tick] = [] + orders[tick].append( + Order( + int(line["tick"]), + int(line["source_port_index"]), + int(line["dest_port_index"]), + int(line["quantity"]) ) + ) - return orders + return orders - def _load_orders_from_bin(self, order_file_path: str) -> Dict[int, List[Order]]: - orders: Dict[int, List[Order]] = {} - reader = BinaryReader(order_file_path) +def _load_orders_from_bin(order_file_path: str) -> Dict[int, List[Order]]: + orders: Dict[int, List[Order]] = {} - for order in reader.items(): - tick = order.timestamp - if tick not in orders: - orders[tick] = [] - orders[tick].append( - Order( - order.timestamp, - order.src_port_index, - order.dest_port_index, - order.quantity - ) + reader = BinaryReader(order_file_path) + + for order in reader.items(): + tick = order.timestamp + if tick not in orders: + orders[tick] = [] + orders[tick].append( + Order( + order.timestamp, + order.src_port_index, + order.dest_port_index, + order.quantity ) + ) + + return orders - return orders + +def _load_orders(data_folder: str) -> Dict[int, List[Order]]: + bin_path = os.path.join(data_folder, "orders.bin") + if os.path.exists(bin_path): + return _load_orders_from_bin(bin_path) + else: + print(f"No orders binary file in {data_folder}, read from csv file instead...") + csv_path = os.path.join(data_folder, "orders.csv") + return _load_orders_from_csv(csv_path) def load_from_folder(source_folder: str) -> CimSyntheticDataCollection: @@ -443,9 +352,39 @@ def load_from_folder(source_folder: str) -> CimSyntheticDataCollection: Returns: CimSyntheticDataCollection: Data collection for cim data container. """ - loader = CimDumpDataLoader() - - return loader.load(source_folder) + # load from files + misc_items = _load_misc(source_folder) + order_target_proportion = _load_order_proportions(source_folder) + port_mapping, ports = _load_ports_dump(source_folder, order_target_proportion) + route_mapping, routes = _load_routes(source_folder) + vessel_mapping, vessels = _load_vessels(source_folder) + periods_without_noise = _load_vessel_period(source_folder) + stops = _load_stops(source_folder, len(vessels)) + global_order_proportions = _load_global_order_proportions(source_folder) + + # construct data collection + # NOTE: this is a namedtuple, so out-side cannot change it + return CimSyntheticDataCollection( + port_settings=ports, + port_mapping=port_mapping, + vessel_settings=vessels, + vessel_mapping=vessel_mapping, + vessel_stops=stops, + routes=routes, + route_mapping=route_mapping, + vessel_period_without_noise=periods_without_noise, + container_volume=misc_items["container_volume"], + load_cost_factor=misc_items["load_cost_factor"], + dsch_cost_factor=misc_items["dsch_cost_factor"], + past_stop_number=misc_items["past_stop_number"], + future_stop_number=misc_items["future_stop_number"], + max_tick=misc_items["max_tick"], + seed=misc_items["seed"], + total_containers=misc_items["total_container"], + order_mode=OrderGenerateMode(misc_items["order_mode"]), + order_proportion=global_order_proportions, + version=misc_items["version"] + ) def load_real_data_from_folder(source_folder: str) -> CimRealDataCollection: @@ -456,11 +395,37 @@ def load_real_data_from_folder(source_folder: str) -> CimRealDataCollection: ports.csv, vessels.csv, routes.csv, misc.yml, stops.csv, orders.csv. Args: - source_folder(str): Source folder contains data files. + source_folder (str): Source folder contains data files. Returns: CimRealDataCollection: Data collection for cim data container. """ - loader = CimRealDataLoader() - - return loader.load(source_folder) + # load from files + misc_items = _load_misc(source_folder) + port_mapping, ports = _load_ports_real(source_folder) + route_mapping, routes = _load_routes(source_folder) + vessel_mapping, vessels = _load_vessels(source_folder) + periods_without_noise = _calculate_vessel_period(vessels, routes, route_mapping) + stops = _load_stops(source_folder, len(vessels)) + orders = _load_orders(source_folder) + + # construct data collection + # NOTE: this is a namedtuple, so out-side cannot change it + return CimRealDataCollection( + port_settings=ports, + port_mapping=port_mapping, + vessel_settings=vessels, + vessel_mapping=vessel_mapping, + vessel_stops=stops, + routes=routes, + route_mapping=route_mapping, + vessel_period_without_noise=periods_without_noise, + container_volume=misc_items["container_volume"], + load_cost_factor=misc_items["load_cost_factor"], + dsch_cost_factor=misc_items["dsch_cost_factor"], + past_stop_number=misc_items["past_stop_number"], + future_stop_number=misc_items["future_stop_number"], + max_tick=misc_items["max_tick"], + seed=misc_items["seed"], + orders=orders + ) diff --git a/maro/data_lib/cim/entities.py b/maro/data_lib/cim/entities.py index 7cd8b6469..ffd929b01 100644 --- a/maro/data_lib/cim/entities.py +++ b/maro/data_lib/cim/entities.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from enum import Enum -from typing import Dict, List +from typing import Dict, List, Optional import numpy as np @@ -33,14 +33,14 @@ class PortSetting: name: str capacity: int empty: int - empty_return_buffer: int - full_return_buffer: int + empty_return_buffer: Optional[NoisedItem] + full_return_buffer: Optional[NoisedItem] @dataclass(frozen=True) class SyntheticPortSetting(PortSetting): - source_proportion: float - target_proportions: float + source_proportion: Optional[NoisedItem] + target_proportions: Optional[List[NoisedItem]] # settings for vessel @@ -50,11 +50,11 @@ class VesselSetting: name: str capacity: int route_name: str - start_port_name: str - sailing_speed: float - sailing_noise: float - parking_duration: int - parking_noise: float + start_port_name: Optional[str] + sailing_speed: Optional[float] + sailing_noise: Optional[float] + parking_duration: Optional[int] + parking_noise: Optional[float] empty: int @@ -108,13 +108,13 @@ def __repr__(self): @dataclass(frozen=True) class CimBaseDataCollection: # Port - ports_settings: List[PortSetting] + port_settings: List[PortSetting] port_mapping: Dict[str, int] # Vessel - vessels_settings: List[VesselSetting] + vessel_settings: List[VesselSetting] vessel_mapping: Dict[str, int] # Stop - vessels_stops: List[List[Stop]] + vessel_stops: List[List[Optional[Stop]]] # Route routes: List[List[RoutePoint]] route_mapping: Dict[str, int] @@ -122,6 +122,9 @@ class CimBaseDataCollection: vessel_period_without_noise: List[int] # Volume/Container container_volume: int + # Cost Factors + load_cost_factor: float + dsch_cost_factor: float # Visible Voyage Window past_stop_number: int future_stop_number: int diff --git a/maro/data_lib/cim/global_order_proportion.py b/maro/data_lib/cim/global_order_proportion.py deleted file mode 100644 index cb4351061..000000000 --- a/maro/data_lib/cim/global_order_proportion.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from math import floor -from typing import Union - -import numpy as np - -from .utils import apply_noise, clip, order_init_rand - - -class GlobalOrderProportion: - """Helper used to generate order proportion at specified tick range. - """ - - def parse(self, conf: dict, total_container: int, max_tick: int, start_tick: int = 0) -> np.ndarray: - """Parse specified configuration, and generate order proportion. - - Args: - conf (dict): Configuration to parse. - total_container (int): Total containers in this environment. - max_tick (int): Max tick to generate. - start_tick (int): Start tick to generate. - - Returns: - np.ndarray: 1-dim numpy array for specified range. - """ - durations: int = max_tick - start_tick - - order_proportion = np.zeros(durations, dtype="i") - - # read configurations - period: int = conf["period"] - noise: Union[float, int] = conf["sample_noise"] - sample_nodes: list = [(x, y) for x, y in conf["sample_nodes"]] - - # step 1: interpolate with configured sample nodes to generate proportion in period - - # check if there is 0 and max_tick - 1 node exist ,insert if not exist - if sample_nodes[0][0] != 0: - sample_nodes.insert(0, (0, 0)) - - if sample_nodes[-1][0] != period - 1: - sample_nodes.append((period - 1, 0)) - - # our xp is period - xp = [node[0] for node in sample_nodes] - yp = [node[1] for node in sample_nodes] - - # distribution per period - order_period_distribution = np.interp( - [t for t in range(period)], xp, yp) - - # step 2: extend to specified range - - for t in range(start_tick, max_tick): - orders = order_period_distribution[t % period] # ratio - - # apply noise if the distribution not zero - if orders != 0: - if noise != 0: - orders = apply_noise(orders, noise, order_init_rand) - - # clip and gen order - orders = floor(clip(0, 1, orders) * total_container) - order_proportion[t - start_tick] = orders - - return order_proportion diff --git a/maro/data_lib/cim/parsers.py b/maro/data_lib/cim/parsers.py new file mode 100644 index 000000000..393a6dceb --- /dev/null +++ b/maro/data_lib/cim/parsers.py @@ -0,0 +1,210 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from math import floor +from typing import Dict, List, Union + +import numpy as np + +from maro.data_lib.cim.entities import NoisedItem, RoutePoint, SyntheticPortSetting, VesselSetting +from maro.data_lib.cim.utils import ORDER_INIT_RAND_KEY, apply_noise, clip +from maro.simulator.utils import random + + +def parse_vessels(conf: dict) -> (Dict[str, int], List[VesselSetting]): + """Parse specified vessel configurations. + + Args: + conf(dict): Configurations to parse. + + Returns: + (Dict[str, int], List[VesselSetting]): Vessel mappings (name to index), and settings list for all vessels. + + """ + mapping: Dict[str, int] = {} + vessels: List[VesselSetting] = [] + + index = 0 + + for vessel_name, vessel_node in conf.items(): + mapping[vessel_name] = index + + sailing = vessel_node["sailing"] + parking = vessel_node["parking"] + route = vessel_node["route"] + + vessels.append( + VesselSetting( + index, + vessel_name, + vessel_node["capacity"], + route["route_name"], + route["initial_port_name"], + sailing["speed"], + sailing["noise"], + parking["duration"], + parking["noise"], + # default no empty + vessel_node.get("empty", 0) + ) + ) + + index += 1 + + return mapping, vessels + + +def parse_global_order_proportion(conf: dict, total_container: int, max_tick: int, start_tick: int = 0) -> np.ndarray: + """Parse specified configuration, and generate order proportion. + + Args: + conf (dict): Configuration to parse. + total_container (int): Total containers in this environment. + max_tick (int): Max tick to generate. + start_tick (int): Start tick to generate. + + Returns: + np.ndarray: 1-dim numpy array for specified range. + """ + durations: int = max_tick - start_tick + + order_proportion = np.zeros(durations, dtype="i") + + # read configurations + period: int = conf["period"] + noise: Union[float, int] = conf["sample_noise"] + sample_nodes: list = [(x, y) for x, y in conf["sample_nodes"]] + + # step 1: interpolate with configured sample nodes to generate proportion in period + + # check if there is 0 and max_tick - 1 node exist ,insert if not exist + if sample_nodes[0][0] != 0: + sample_nodes.insert(0, (0, 0)) + if sample_nodes[-1][0] != period - 1: + sample_nodes.append((period - 1, 0)) + + # our xp is period + xp = [node[0] for node in sample_nodes] + yp = [node[1] for node in sample_nodes] + + # distribution per period + order_period_distribution = np.interp(list(range(period)), xp, yp) + + # step 2: extend to specified range + for t in range(start_tick, max_tick): + orders = order_period_distribution[t % period] # ratio + + # apply noise if the distribution not zero + if orders != 0: + if noise != 0: + orders = apply_noise(orders, noise, random[ORDER_INIT_RAND_KEY]) + + # clip and gen order + orders = floor(clip(0, 1, orders) * total_container) + order_proportion[t - start_tick] = orders + + return order_proportion + + +def parse_routes(conf: dict) -> (Dict[str, int], List[List[RoutePoint]]): + """Parse specified route configuration. + + Args: + conf (dict): Configuration to parse. + + Returns: + (Dict[str, int], List[List[RoutePoint]]): Route mapping (name to index), + and list of route point list (index is route index). + """ + routes: List[List[RoutePoint]] = [] + route_mapping: Dict[str, int] = {} # name->idx + + idx = 0 + + for name, node in conf.items(): + route_mapping[name] = idx + + routes.append([RoutePoint(idx, n["port_name"], n["distance_to_next_port"]) for n in node]) + + idx += 1 + + return route_mapping, routes + + +def parse_ports(conf: dict, total_container: int) -> (Dict[str, int], List[SyntheticPortSetting]): + """Parse specified port configurations. + + Args: + conf (dict): Configuration to parse. + total_container (int): Total container in current environment, used to calculate initial empty. + + Returns: + (Dict[str, int], List[SyntheticPortSetting]): Port mapping (name to index), list of port settings. + """ + # sum of ratio cannot large than 1 + total_ratio = sum([p["initial_container_proportion"] for p in conf.values()]) + + assert round(total_ratio, 7) == 1 + + ports_mapping: Dict[str, int] = {} + + index = 0 + + # step 1: create mapping + for port_name, port_info in conf.items(): + ports_mapping[port_name] = index + index += 1 + + port_settings: List[SyntheticPortSetting] = [] + + for port_idx, port in enumerate(conf.items()): + port_name, port_info = port + + # step 2: update initial values + empty_ratio = port_info["initial_container_proportion"] + + # full return buffer configurations + full_return_conf = port_info["full_return"] + empty_return_conf = port_info["empty_return"] + + dist_conf = port_info["order_distribution"] + source_dist_conf = dist_conf["source"] + + targets_dist = [] + + # orders distribution to destination + if "targets" in dist_conf: + for target_port_name, target_conf in dist_conf["targets"].items(): + dist = NoisedItem( + ports_mapping[target_port_name], + target_conf["proportion"], + target_conf["noise"]) + + targets_dist.append(dist) + + port_setting = SyntheticPortSetting( + port_idx, + port_name, + port_info["capacity"], + int(empty_ratio * total_container), + NoisedItem( + port_idx, + empty_return_conf["buffer_ticks"], + empty_return_conf["noise"] + ), + NoisedItem( + port_idx, + full_return_conf["buffer_ticks"], + full_return_conf["noise"] + ), + NoisedItem( + port_idx, + source_dist_conf["proportion"], + source_dist_conf["noise"] + ), + targets_dist + ) + + port_settings.append(port_setting) + + return ports_mapping, port_settings diff --git a/maro/data_lib/cim/port_buffer_tick_wrapper.py b/maro/data_lib/cim/port_buffer_tick_wrapper.py index 74c844e9b..d1336a720 100644 --- a/maro/data_lib/cim/port_buffer_tick_wrapper.py +++ b/maro/data_lib/cim/port_buffer_tick_wrapper.py @@ -1,10 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - from math import ceil +from typing import Callable + +from maro.simulator.utils import random from .entities import CimBaseDataCollection, NoisedItem, PortSetting -from .utils import apply_noise, buffer_tick_rand +from .utils import BUFFER_TICK_RAND_KEY, apply_noise class PortBufferTickWrapper: @@ -19,8 +21,8 @@ class PortBufferTickWrapper: attribute_func (callable): Function to get attribute, used to switch between empty and full. """ - def __init__(self, data: CimBaseDataCollection, attribute_func: callable): - self._ports = data.ports_settings + def __init__(self, data: CimBaseDataCollection, attribute_func: Callable[[PortSetting], NoisedItem]) -> None: + self._ports = data.port_settings self._attribute_func = attribute_func def __getitem__(self, key): @@ -29,4 +31,4 @@ def __getitem__(self, key): buffer_setting: NoisedItem = self._attribute_func(port) - return ceil(apply_noise(buffer_setting.base, buffer_setting.noise, buffer_tick_rand)) + return ceil(apply_noise(buffer_setting.base, buffer_setting.noise, random[BUFFER_TICK_RAND_KEY])) diff --git a/maro/data_lib/cim/port_parser.py b/maro/data_lib/cim/port_parser.py deleted file mode 100644 index 832870ec5..000000000 --- a/maro/data_lib/cim/port_parser.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Dict, List - -from .entities import NoisedItem, SyntheticPortSetting - - -class PortsParser: - """Parser used to parse port information from configurations - """ - - def parse(self, conf: dict, total_container: int) -> (Dict[str, int], List[SyntheticPortSetting]): - """Parse specified port configurations. - - Args: - conf (dict): Configuration to parse. - total_container (int): Total container in current environment, used to calculate initial empty. - - Returns: - (Dict[str, int], List[SyntheticPortSetting]): Port mapping (name to index), list of port settings. - """ - # sum of ratio cannot large than 1 - total_ratio = sum([p["initial_container_proportion"] for p in conf.values()]) - - assert round(total_ratio, 7) == 1 - - ports_mapping: Dict[str, int] = {} - - index = 0 - - # step 1: create mapping - for port_name, port_info in conf.items(): - ports_mapping[port_name] = index - index += 1 - - ports_settings: List[SyntheticPortSetting] = [] - - for port_idx, port in enumerate(conf.items()): - port_name, port_info = port - - # step 2: update initial values - empty_ratio = port_info["initial_container_proportion"] - - # full return buffer configurations - full_return_conf = port_info["full_return"] - empty_return_conf = port_info["empty_return"] - - dist_conf = port_info["order_distribution"] - source_dist_conf = dist_conf["source"] - - targets_dist = [] - - # orders distribution to destination - if "targets" in dist_conf: - for target_port_name, target_conf in dist_conf["targets"].items(): - dist = NoisedItem( - ports_mapping[target_port_name], - target_conf["proportion"], - target_conf["noise"]) - - targets_dist.append(dist) - - port_setting = SyntheticPortSetting( - port_idx, - port_name, - port_info["capacity"], - int(empty_ratio * total_container), - NoisedItem( - port_idx, - empty_return_conf["buffer_ticks"], - empty_return_conf["noise"]), - NoisedItem( - port_idx, - full_return_conf["buffer_ticks"], - full_return_conf["noise"]), - NoisedItem( - port_idx, - source_dist_conf["proportion"], - source_dist_conf["noise"]), - targets_dist) - - ports_settings.append(port_setting) - - return ports_mapping, ports_settings diff --git a/maro/data_lib/cim/route_parser.py b/maro/data_lib/cim/route_parser.py deleted file mode 100644 index c942cac17..000000000 --- a/maro/data_lib/cim/route_parser.py +++ /dev/null @@ -1,36 +0,0 @@ - -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Dict, List - -from .entities import RoutePoint - - -class RoutesParser: - """Parser used to parse route information from configurations. - """ - - def parse(self, conf: dict) -> (Dict[str, int], List[List[RoutePoint]]): - """Parse specified route configuration. - - Args: - conf (dict): Configuration to parse. - - Returns: - (Dict[str, int], List[List[RoutePoint]]): Route mapping (name to index), - and list of route point list (index is route index). - """ - routes: List[List[RoutePoint]] = [] - route_mapping: Dict[str, int] = {} # name->idx - - idx = 0 - - for name, node in conf.items(): - route_mapping[name] = idx - - routes.append([RoutePoint(idx, n["port_name"], n["distance_to_next_port"]) for n in node]) - - idx += 1 - - return route_mapping, routes diff --git a/maro/data_lib/cim/utils.py b/maro/data_lib/cim/utils.py index eb56e1420..5dc773dcf 100644 --- a/maro/data_lib/cim/utils.py +++ b/maro/data_lib/cim/utils.py @@ -1,23 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from random import Random from typing import List, Union -from maro.simulator.utils.sim_random import SimRandom, random +# we keep 4 random generator to make the result is reproduce-able with same seed(s), no matter if agent passed actions +ORDER_INIT_RAND_KEY = "order_init" +ROUTE_INIT_RAND_KEY = "route_init" +ORDER_NUM_RAND_KEY = "order_number" +BUFFER_TICK_RAND_KEY = "buffer_time" -# we keep 4 random generator to make the result is reproduceable with same seed(s), no matter if agent passed actions -route_init_rand = random["route_init"] -order_init_rand = random["order_init"] -buffer_tick_rand = random["buffer_time"] -order_num_rand = random["order_number"] - - -def get_buffer_tick_seed(): - return random.get_seed("buffer_time") - - -def get_order_num_seed(): - return random.get_seed("order_number") +DATA_CONTAINER_INIT_SEED_LIMIT = 4096 def clip(min_val: Union[int, float], max_val: Union[int, float], value: Union[int, float]) -> Union[int, float]: @@ -34,7 +27,7 @@ def clip(min_val: Union[int, float], max_val: Union[int, float], value: Union[in return max(min_val, min(max_val, value)) -def apply_noise(value: Union[int, float], noise: Union[int, float], rand: SimRandom) -> float: +def apply_noise(value: Union[int, float], noise: Union[int, float], rand: Random) -> float: """Apply noise with specified random generator Args: @@ -60,7 +53,11 @@ def list_sum_normalize(num_list: List[Union[int, float]]) -> List[float]: t = sum(num_list) # avoid dive zero exception - if t == 0: - return 0 + return num_list if t == 0 else [d / t for d in num_list] + + +def extract_key_of_three_ints(key) -> (int, int, int): + assert type(key) == tuple or type(key) == list + assert len(key) == 3 - return [d / t for d in num_list] + return int(key[0]), int(key[1]), int(key[2]) diff --git a/maro/data_lib/cim/vessel_future_stops_prediction.py b/maro/data_lib/cim/vessel_future_stops_prediction.py index 522722b77..3b2be4ca5 100644 --- a/maro/data_lib/cim/vessel_future_stops_prediction.py +++ b/maro/data_lib/cim/vessel_future_stops_prediction.py @@ -2,8 +2,10 @@ # Licensed under the MIT license. from math import ceil +from typing import List from .entities import CimBaseDataCollection, Stop +from .utils import extract_key_of_three_ints class VesselFutureStopsPrediction: @@ -17,61 +19,56 @@ class VesselFutureStopsPrediction: stops = data_cntr.vessel_future_stops[0] """ - def __init__(self, data: CimBaseDataCollection): - self._vessels = data.vessels_settings - self._stops = data.vessels_stops + def __init__(self, data: CimBaseDataCollection) -> None: + self._vessels = data.vessel_settings + self._stops = data.vessel_stops self._routes = data.routes self._route_mapping = data.route_mapping self._port_mapping = data.port_mapping self._stop_number = data.future_stop_number + self._vessel_start_port_offsets = self._make_vessel_start_port_offsets() def __getitem__(self, key): """Used to support querying future stops by vessel index, last location index, next location index.""" - assert type(key) == tuple or type(key) == list - assert len(key) == 3 - - vessel_idx = key[0] - last_loc_idx = key[1] - loc_idx = key[2] + vessel_idx, last_loc_idx, loc_idx = extract_key_of_three_ints(key) # ignore current port if parking last_stop_idx = loc_idx + (0 if last_loc_idx == loc_idx else -1) - last_stop = self._stops[vessel_idx][last_stop_idx] - last_port_idx = last_stop.port_idx - last_port_arrival_tick = last_stop.arrival_tick + return self._predict_future_stops(vessel_idx, last_stop_idx, self._stop_number) - return self._predict_future_stops(vessel_idx, last_port_idx, last_port_arrival_tick, self._stop_number) + def _make_vessel_start_port_offsets(self) -> List[int]: + vessel_start_port_offsets = [] + for vessel in self._vessels: + route_points = self._routes[self._route_mapping[vessel.route_name]] + route_point_names = [rp.port_name for rp in route_points] + vessel_start_port_offset = route_point_names.index(vessel.start_port_name) + vessel_start_port_offsets.append(vessel_start_port_offset) + return vessel_start_port_offsets - def _predict_future_stops(self, vessel_idx: int, last_port_idx: int, last_port_arrival_tick: int, stop_number: int): + def _predict_future_stops(self, vessel_idx: int, last_stop_idx: int, stop_number: int) -> List[Stop]: """Do predict future stops. """ vessel = self._vessels[vessel_idx] - speed = vessel.sailing_speed - duration = vessel.parking_duration - route_name = vessel.route_name - route_points = self._routes[self._route_mapping[route_name]] + speed, duration = vessel.sailing_speed, vessel.parking_duration + route_points = self._routes[self._route_mapping[vessel.route_name]] route_length = len(route_points) - last_loc_idx = -1 - - # try to find the last location index from route - for loc_idx, route_point in enumerate(route_points): - if self._port_mapping[route_point.port_name] == last_port_idx: - last_loc_idx = loc_idx - break + last_stop = self._stops[vessel_idx][last_stop_idx] + last_port_arrival_tick = last_stop.arrival_tick - # return if not in current route - if last_loc_idx < 0: - return [] + vessel_start_port_offset = self._vessel_start_port_offsets[vessel_idx] + last_loc_idx = (vessel_start_port_offset + last_stop_idx) % len(route_points) predicted_future_stops = [] arrival_tick = last_port_arrival_tick # predict from configured sailing plan, not from stops for loc_idx in range(last_loc_idx + 1, last_loc_idx + stop_number + 1): - route_info = route_points[loc_idx % route_length] - port_idx, distance_to_next_port = self._port_mapping[route_info.port_name], route_info.distance_to_next_port + next_route_info = route_points[loc_idx % route_length] + last_route_info = route_points[(loc_idx - 1) % route_length] + port_idx = self._port_mapping[next_route_info.port_name] + distance_to_next_port = last_route_info.distance_to_next_port # NO noise for speed arrival_tick += duration + ceil(distance_to_next_port / speed) diff --git a/maro/data_lib/cim/vessel_parser.py b/maro/data_lib/cim/vessel_parser.py deleted file mode 100644 index 304af2d2e..000000000 --- a/maro/data_lib/cim/vessel_parser.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Dict, List - -from .entities import VesselSetting - - -class VesselsParser: - """Parser used to parse vessel configurations. - """ - - def parse(self, conf: dict) -> (Dict[str, int], List[VesselSetting]): - """Parse specified vessel configurations. - - Args: - conf(dict): Configurations to parse. - - Returns: - (Dict[str, int], List[VesselSetting]): Vessel mappings (name to index), and settings list for all vessels. - - """ - mapping: Dict[str, int] = {} - vessels: List[VesselSetting] = [] - - index = 0 - - for vessel_name, vessel_node in conf.items(): - mapping[vessel_name] = index - - sailing = vessel_node["sailing"] - parking = vessel_node["parking"] - route = vessel_node["route"] - - vessels.append(VesselSetting( - index, - vessel_name, - vessel_node["capacity"], - route["route_name"], - route["initial_port_name"], - sailing["speed"], - sailing["noise"], - parking["duration"], - parking["noise"], - # default no empty - vessel_node.get("empty", 0))) - - index += 1 - - return mapping, vessels diff --git a/maro/data_lib/cim/vessel_past_stops_wrapper.py b/maro/data_lib/cim/vessel_past_stops_wrapper.py index 9211bd3fc..02bd1c7aa 100644 --- a/maro/data_lib/cim/vessel_past_stops_wrapper.py +++ b/maro/data_lib/cim/vessel_past_stops_wrapper.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from .entities import CimBaseDataCollection +from .utils import extract_key_of_three_ints class VesselPastStopsWrapper: @@ -15,17 +16,12 @@ class VesselPastStopsWrapper: stops = data_cntr.vessel_past_stops[0] """ - def __init__(self, data: CimBaseDataCollection): + def __init__(self, data: CimBaseDataCollection) -> None: self._stop_number = data.past_stop_number - self._stops = data.vessels_stops + self._stops = data.vessel_stops def __getitem__(self, key): - assert type(key) == tuple or type(key) == list - assert len(key) == 3 - - vessel_idx = key[0] - last_loc_idx = key[1] - loc_idx = key[2] + vessel_idx, last_loc_idx, loc_idx = extract_key_of_three_ints(key) # ignore current port if parking last_stop_idx = loc_idx + (0 if last_loc_idx == loc_idx else -1) diff --git a/maro/data_lib/cim/vessel_reachable_stops_wrapper.py b/maro/data_lib/cim/vessel_reachable_stops_wrapper.py index 318f4cac8..e094e39fa 100644 --- a/maro/data_lib/cim/vessel_reachable_stops_wrapper.py +++ b/maro/data_lib/cim/vessel_reachable_stops_wrapper.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from .entities import CimBaseDataCollection +from .utils import extract_key_of_three_ints class VesselReachableStopsWrapper: @@ -15,20 +16,14 @@ class VesselReachableStopsWrapper: stop_list = data_cntr.reachable_stops[0] """ - def __init__(self, data: CimBaseDataCollection): + def __init__(self, data: CimBaseDataCollection) -> None: self._routes = data.routes - self._stops = data.vessels_stops + self._stops = data.vessel_stops def __getitem__(self, key): - assert type(key) == tuple or type(key) == list - assert len(key) == 3 - - vessel_idx = key[0] - route_idx = key[1] - next_loc_idx = key[2] + vessel_idx, route_idx, next_loc_idx = extract_key_of_three_ints(key) route_length = len(self._routes[route_idx]) - stops = self._stops[vessel_idx][ - next_loc_idx + 1: next_loc_idx + 1 + route_length] + stops = self._stops[vessel_idx][next_loc_idx + 1: next_loc_idx + 1 + route_length] return [(stop.port_idx, stop.arrival_tick) for stop in stops] diff --git a/maro/data_lib/cim/vessel_sailing_plan_wrapper.py b/maro/data_lib/cim/vessel_sailing_plan_wrapper.py index 2b21920f1..ffbd50a3e 100644 --- a/maro/data_lib/cim/vessel_sailing_plan_wrapper.py +++ b/maro/data_lib/cim/vessel_sailing_plan_wrapper.py @@ -1,13 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .entities import CimBaseDataCollection, Stop +from .entities import CimBaseDataCollection +from .utils import extract_key_of_three_ints from .vessel_future_stops_prediction import VesselFutureStopsPrediction class VesselSailingPlanWrapper(VesselFutureStopsPrediction): """Wrapper to get vessel sailing plan, this method will return a stop - list that within configured time peroid (means no same port in list). + list that within configured time period (means no same port in list). Examples: @@ -17,24 +18,11 @@ class VesselSailingPlanWrapper(VesselFutureStopsPrediction): stops = data_cntr.vessel_planned_stops[0] """ - def __init__(self, data: CimBaseDataCollection): + def __init__(self, data: CimBaseDataCollection) -> None: super().__init__(data) def __getitem__(self, key): - assert type(key) == tuple or type(key) == list - assert len(key) == 3 - - vessel_idx = key[0] - route_idx = key[1] - next_loc_idx = key[2] - + vessel_idx, route_idx, next_loc_idx = extract_key_of_three_ints(key) route_length = len(self._routes[route_idx]) - - last_stop: Stop = self._stops[vessel_idx][next_loc_idx] - last_port_idx = last_stop.port_idx - last_port_arrival_tick = last_stop.arrival_tick - - stops = self._predict_future_stops( - vessel_idx, last_port_idx, last_port_arrival_tick, route_length) - + stops = self._predict_future_stops(vessel_idx, next_loc_idx, route_length) return [(stop.port_idx, stop.arrival_tick) for stop in stops] diff --git a/maro/data_lib/cim/vessel_stop_wrapper.py b/maro/data_lib/cim/vessel_stop_wrapper.py index 185cd7fc6..14bbfcb9f 100644 --- a/maro/data_lib/cim/vessel_stop_wrapper.py +++ b/maro/data_lib/cim/vessel_stop_wrapper.py @@ -21,8 +21,8 @@ class VesselStopsWrapper: stops = data_cntr.vessel_stops[:] """ - def __init__(self, data: CimBaseDataCollection): - self._stops = data.vessels_stops + def __init__(self, data: CimBaseDataCollection) -> None: + self._stops = data.vessel_stops def __getitem__(self, key): key_type = type(key) @@ -36,7 +36,7 @@ def __getitem__(self, key): loc_idx = key[1] return self._stops[vessel_idx][loc_idx] - elif key_type == slice and key.start is None and key.step is None and key.stop is None: + elif key_type == slice and all([key.start is None, key.step is None, key.stop is None]): return self._stops return None diff --git a/maro/data_lib/item_meta.py b/maro/data_lib/item_meta.py index 25120b2c6..02a6a8f82 100644 --- a/maro/data_lib/item_meta.py +++ b/maro/data_lib/item_meta.py @@ -12,7 +12,7 @@ from yaml import SafeDumper, SafeLoader, YAMLObject, safe_dump, safe_load from maro.data_lib.common import dtype_pack_map -from maro.utils.exception.data_lib_exeption import MetaTimestampNotExist +from maro.utils.exception.data_lib_exception import MetaTimestampNotExist class EntityAttr(YAMLObject): diff --git a/maro/event_buffer/__init__.py b/maro/event_buffer/__init__.py index 68caf7a14..2d16dc1cf 100644 --- a/maro/event_buffer/__init__.py +++ b/maro/event_buffer/__init__.py @@ -1,10 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .atom_event import AtomEvent -from .cascade_event import CascadeEvent +from .event import AbsEvent, ActualEvent, AtomEvent, CascadeEvent, DummyEvent from .event_buffer import EventBuffer from .event_state import EventState from .maro_events import MaroEvents -__all__ = ["AtomEvent", "CascadeEvent", "EventBuffer", "EventState", "MaroEvents"] +__all__ = [ + "AbsEvent", "ActualEvent", "AtomEvent", "CascadeEvent", "DummyEvent", "EventBuffer", "EventState", "MaroEvents" +] diff --git a/maro/event_buffer/atom_event.py b/maro/event_buffer/atom_event.py deleted file mode 100644 index a32c40992..000000000 --- a/maro/event_buffer/atom_event.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .event_state import EventState - - -class AtomEvent: - """Basic event object that used to hold information that for callback. - - Note: - The payload of event can be any object that related with specified logic. - - Args: - id (int): Id of this event. - tick (int): Tick that this event will be processed. - event_type (int): Type of this event, this is a customize field, - there is one predefined event type is 0 (PREDEFINE_EVENT_ACTION). - payload (object): Payload of this event. - - Attributes: - id (int): Id of this event, usually this is used for "joint decision" node - that need "sequential action". - tick (int): Process tick of this event. - payload (object): Payload of this event, can be any object. - event_type (object): Type of this event, can be any type, - EventBuffer will use this to match handlers. - state (EventState): Internal life-circle state of event. - """ - - def __init__(self, id: int, tick: int, event_type: object, payload: object): - self.id = id - self.tick = tick - self.payload = payload - self.event_type = event_type - self.state = EventState.PENDING - - # Used to link to next event in linked list - self._next_event_ = None diff --git a/maro/event_buffer/cascade_event.py b/maro/event_buffer/cascade_event.py deleted file mode 100644 index 7abe2e7da..000000000 --- a/maro/event_buffer/cascade_event.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .atom_event import AtomEvent - - -class CascadeEvent(AtomEvent): - """Special event type that support add immediate events (or sub events), these - events will be execute right after its parent. - - Some times there may be some events that depend on another one, - then you can append these events with add_immediate_event method, then - these events will be processed after the parent event. - """ - - def __init__(self, id: int, tick: int, event_type: object, payload: object): - super().__init__(id, tick, event_type, payload) - - # Header of immediate event list. - self._immediate_event_head = AtomEvent(None, None, None, None) - - # Pointer to last immediate event, used for speed up immediate event extract. - self._last_immediate_event = None - self._immediate_event_count = 0 - - def add_immediate_event(self, event, is_head: bool = False) -> bool: - """Add an immediate event, that will be processed right after the current event. - - Immediate events are only supported to be inserted into the head or tail of the immediate event list. - By default, the events will be appended to the end. - - NOTE: - The tick of the event to insert must be the same as the current event, or will fail to insert. - - Args: - event (Event): Event object to insert. - is_head (bool): Whether to insert at the head or append to the end. - - Returns: - bool: True if success, or False. - """ - # Make sure the immediate event's tick is the same as the current one. - if event.tick != self.tick: - return False - - if is_head: - event._next_event_ = self._immediate_event_head._next_event_ - - self._immediate_event_head._next_event_ = event - - # Point to new event if not last one, or keep last one, as we are insert to head. - if self._last_immediate_event is None: - self._last_immediate_event = event - else: - if self._last_immediate_event is None: - self._last_immediate_event = event - self._immediate_event_head._next_event_ = event - else: - self._last_immediate_event._next_event_ = event - self._last_immediate_event = event - - self._immediate_event_count += 1 - - return True diff --git a/maro/event_buffer/event.py b/maro/event_buffer/event.py new file mode 100644 index 000000000..ec84b221a --- /dev/null +++ b/maro/event_buffer/event.py @@ -0,0 +1,138 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +import abc +from typing import Optional + +from .event_state import EventState + + +class AbsEvent(metaclass=abc.ABCMeta): + """Abstract interface for events. Hold information that for callback. + + Note: + The payload of event can be any object that related with specified logic. + + Args: + id (int): Id of this event. + tick (int): Tick that this event will be processed. + event_type (int): Type of this event, this is a customized field, + there is one predefined event type 0 (PREDEFINE_EVENT_ACTION). + payload (object): Payload of this event. + + Attributes: + id (int): Id of this event, usually this is used for "joint decision" node + that need "sequential action". + tick (int): Process tick of this event. + payload (object): Payload of this event, can be any object. + event_type (object): Type of this event, can be any type, + EventBuffer will use this to match handlers. + state (EventState): Internal life-circle state of event. + """ + + def __init__(self, id: Optional[int], tick: Optional[int], event_type: object, payload: object) -> None: + self.id: Optional[int] = id + self.tick: Optional[int] = tick + self.payload: object = payload + self.event_type: object = event_type + self.state: EventState = EventState.PENDING + + # Used to link to next event in linked list + self.next_event: Optional[ActualEvent] = None + + def reset_value( + self, id: Optional[int], tick: Optional[int], event_type: object, payload: object, state: EventState + ) -> None: + self.id: Optional[int] = id + self.tick: Optional[int] = tick + self.event_type: object = event_type + self.payload: object = payload + self.state: EventState = state + + +class DummyEvent(AbsEvent): + def __init__(self) -> None: + # Add parameters could be set to None since the event is dummy. + super().__init__(None, None, None, None) + + +class ActualEvent(AbsEvent, metaclass=abc.ABCMeta): + def __init__(self, id: Optional[int], tick: Optional[int], event_type: object, payload: object) -> None: + super().__init__(id, tick, event_type, payload) + + +class AtomEvent(ActualEvent): + """Basic atom event without any additional functions or attributes. + """ + def __init__(self, id: Optional[int], tick: Optional[int], event_type: object, payload: object) -> None: + super().__init__(id, tick, event_type, payload) + + +class CascadeEvent(ActualEvent): + """Event that support add immediate events (or sub events), these + events will be execute right after its parent. + + Some times there may be some events that depend on another one, + then you can append these events with add_immediate_event method, then + these events will be processed after the parent event. + """ + + def __init__(self, id: Optional[int], tick: Optional[int], event_type: object, payload: object) -> None: + super().__init__(id, tick, event_type, payload) + + # Head & tail of immediate event list. + self._immediate_event_head: DummyEvent = DummyEvent() + self._immediate_event_tail: Optional[ActualEvent] = None + + self._immediate_event_count = 0 + + @property + def immediate_event_count(self) -> int: + return self._immediate_event_count + + @property + def immediate_event_head(self) -> DummyEvent: + return self._immediate_event_head + + @property + def immediate_event_tail(self) -> Optional[ActualEvent]: + return self._immediate_event_tail + + def clear(self) -> None: + self._immediate_event_head.next_event = self._immediate_event_tail = None + self._immediate_event_count = 0 + + def add_immediate_event(self, event: ActualEvent, is_head: bool = False) -> bool: + """Add an immediate event, that will be processed right after the current event. + + Immediate events are only supported to be inserted into the head or tail of the immediate event list. + By default, the events will be appended to the end. + + NOTE: + The tick of the event to insert must be the same as the current event, or will fail to insert. + + Args: + event (ActualEvent): Event object to insert. It has to be an actual event. A dummy event is unacceptable. + is_head (bool): Whether to insert at the head or append to the end. + + Returns: + bool: True if success, or False. + """ + # Make sure the immediate event's tick is the same as the current one. + if event.tick != self.tick: + return False + + if self._immediate_event_count == 0: + # 'self._immediate_event_count == 0' means the linked list is empty. + # In this case, inserting at the head is identical with appending to the end. + self._immediate_event_head.next_event = self._immediate_event_tail = event + elif is_head: + assert event.next_event is None, 'Follow-up events are unacceptable when inserting the event into the head' + event.next_event = self._immediate_event_head.next_event + self._immediate_event_head.next_event = event + else: + self._immediate_event_tail.next_event = event + self._immediate_event_tail = event + + self._immediate_event_count += 1 + + return True diff --git a/maro/event_buffer/event_buffer.py b/maro/event_buffer/event_buffer.py index 47dcda0bd..ae09e4f27 100644 --- a/maro/event_buffer/event_buffer.py +++ b/maro/event_buffer/event_buffer.py @@ -4,27 +4,25 @@ import csv from collections import defaultdict -from typing import Callable +from typing import Callable, List, Optional -from .atom_event import AtomEvent -from .cascade_event import CascadeEvent +from .event import ActualEvent, AtomEvent, CascadeEvent from .event_linked_list import EventLinkedList from .event_pool import EventPool from .event_state import EventState from .maro_events import MaroEvents -from .typings import Event, EventList class EventRecorder: """Recorder used to record events to csv file.""" - def __init__(self, path: str): + def __init__(self, path: str) -> None: self._fp = open(path, "wt+", newline='') self._writer = csv.writer(self._fp) self._writer.writerow(['episode', 'tick', 'event_type', 'payload']) - def record(self, o: dict): + def record(self, o: dict) -> None: # yaml.dump(o, self._fp, sort_keys=False) - self._writer.writerow([o['episode'], o["tick"], o["type"], o["payload"]]) + self._writer.writerow([o["episode"], o["tick"], o["type"], o["payload"]]) def __del__(self): if self._fp is not None and not self._fp.closed: @@ -59,24 +57,23 @@ def __init__(self, disable_finished_events: bool = False, record_events: bool = self._handlers = defaultdict(list) # used to hold all the events that been processed - self._finished_events = [] + self._finished_events: List[ActualEvent] = [] - self._event_pool = EventPool() + self._event_pool: EventPool = EventPool() - self._disable_finished_events = disable_finished_events + self._disable_finished_events: bool = disable_finished_events - self._record_events = record_events + self._record_events: bool = record_events - self._recorder = None - self._recorder_ep = None + self._recorder: Optional[EventRecorder] = None + self._recorder_ep: int = 0 if self._record_events: if record_path is None: raise ValueError("Invalid path to save finished events.") - self._recorder = EventRecorder(record_path) - def get_finished_events(self) -> EventList: + def get_finished_events(self) -> List[ActualEvent]: """Get all the processed events, call this function before reset method. Returns: @@ -84,7 +81,7 @@ def get_finished_events(self) -> EventList: """ return self._finished_events - def get_pending_events(self, tick: int) -> EventList: + def get_pending_events(self, tick: int) -> List[ActualEvent]: """Get pending event at specified tick. Args: @@ -95,27 +92,22 @@ def get_pending_events(self, tick: int) -> EventList: """ return [evt for evt in self._pending_events[tick] if evt is not None] - def reset(self): + def reset(self) -> None: """Reset internal states, this method will clear all events. NOTE: After reset the get_finished_event method will return empty list. """ - # Collect the events from pendding and finished pool to reuse them. + # Collect the events from pending and finished pool to reuse them. self._event_pool.recycle(self._finished_events) - self._finished_events.clear() for pending_pool in self._pending_events.values(): self._event_pool.recycle(pending_pool) - pending_pool.clear() if self._record_events: - if self._recorder_ep is not None: - self._recorder_ep += 1 - else: - self._recorder_ep = 0 + self._recorder_ep += 1 def gen_atom_event(self, tick: int, event_type: object, payload: object = None) -> AtomEvent: """Generate an atom event, an atom event is for normal usages, @@ -129,7 +121,9 @@ def gen_atom_event(self, tick: int, event_type: object, payload: object = None) Returns: AtomEvent: Atom event object """ - return self._event_pool.gen(tick, event_type, payload, False) + event = self._event_pool.gen(tick, event_type, payload, False) + assert isinstance(event, AtomEvent) + return event def gen_cascade_event(self, tick: int, event_type: object, payload: object) -> CascadeEvent: """Generate an cascade event that used to hold immediate events that @@ -143,7 +137,9 @@ def gen_cascade_event(self, tick: int, event_type: object, payload: object) -> C Returns: CascadeEvent: Cascade event object. """ - return self._event_pool.gen(tick, event_type, payload, True) + event = self._event_pool.gen(tick, event_type, payload, True) + assert isinstance(event, CascadeEvent) + return event def gen_decision_event(self, tick: int, payload: object) -> CascadeEvent: """Generate a decision event that will stop current simulation, and ask agent for action. @@ -154,7 +150,7 @@ def gen_decision_event(self, tick: int, payload: object) -> CascadeEvent: Returns: CascadeEvent: Event object """ - return self._event_pool.gen(tick, MaroEvents.PENDING_DECISION, payload, True) + return self.gen_cascade_event(tick, MaroEvents.PENDING_DECISION, payload) def gen_action_event(self, tick: int, payload: object) -> CascadeEvent: """Generate an event that used to dispatch action to business engine. @@ -165,9 +161,9 @@ def gen_action_event(self, tick: int, payload: object) -> CascadeEvent: Returns: CascadeEvent: Event object """ - return self._event_pool.gen(tick, MaroEvents.TAKE_ACTION, payload, True) + return self.gen_cascade_event(tick, MaroEvents.TAKE_ACTION, payload) - def register_event_handler(self, event_type: object, handler: Callable): + def register_event_handler(self, event_type: object, handler: Callable) -> None: """Register an event with handler, when there is an event need to be processed, EventBuffer will invoke the handler if there are any event's type match specified at each tick. @@ -180,7 +176,7 @@ def register_event_handler(self, event_type: object, handler: Callable): """ self._handlers[event_type].append(handler) - def insert_event(self, event: Event): + def insert_event(self, event: ActualEvent) -> None: """Insert an event to the pending queue. Args: @@ -190,7 +186,7 @@ def insert_event(self, event: Event): self._pending_events[event.tick].append(event) - def execute(self, tick: int) -> EventList: + def execute(self, tick: int) -> List[ActualEvent]: """Process and dispatch event by tick. NOTE: @@ -205,11 +201,11 @@ def execute(self, tick: int) -> EventList: EventList: A list of events that are pending decisions at the current tick. """ if tick in self._pending_events: - cur_events_list: EventLinkedList = self._pending_events[tick] + cur_events_list = self._pending_events[tick] # 1. check if current events match tick. while len(cur_events_list): - next_events = cur_events_list.pop() + next_events = cur_events_list.clear_finished_and_get_front() if next_events is None: # End of current tick. @@ -217,7 +213,7 @@ def execute(self, tick: int) -> EventList: # Only decision event is a list (even only one item). # NOTE: decision event do not have handlers, and simulator will set its state - # to finished after recieved an action. + # to finished after received an action. if type(next_events) == list: return next_events else: @@ -238,9 +234,6 @@ def execute(self, tick: int) -> EventList: self._finished_events.append(next_events) if self._record_events: - if self._recorder_ep is None: - self._recorder_ep = 0 - self._recorder.record({ "episode": self._recorder_ep, "tick": next_events.tick, diff --git a/maro/event_buffer/event_linked_list.py b/maro/event_buffer/event_linked_list.py index 432168d63..fd05201dd 100644 --- a/maro/event_buffer/event_linked_list.py +++ b/maro/event_buffer/event_linked_list.py @@ -1,11 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from typing import List, Optional, Union -from .atom_event import AtomEvent -from .cascade_event import CascadeEvent +from .event import AbsEvent, ActualEvent, CascadeEvent, DummyEvent from .event_state import EventState from .maro_events import MaroEvents -from .typings import Event, EventList, Union class EventLinkedList: @@ -17,7 +16,7 @@ class EventLinkedList: 2. insert: Insert event to the head. Pop method used to pop event(s) from list head, according to current event type, - it may return a list of deicision event, or just an AtomEvent object. + it may return a list of decision event, or just an AtomEvent object. .. code-block:: python @@ -32,140 +31,122 @@ class EventLinkedList: # Pop first event event = event_list.pop() """ - def __init__(self): - # Head of events. - self._head = AtomEvent(None, None, None, None) - # Tail of events. - self._tail = self._head - - # Current events count. - self._count = 0 + def __init__(self) -> None: + # Head & tail of events. + self._head: DummyEvent = DummyEvent() + self._tail: AbsEvent = self._head + self._count: int = 0 # Used to support for loop. - self._iter_cur_event: Event = None + self._iter_cur_event: Optional[AbsEvent] = None - def clear(self): + def clear(self) -> None: """Clear current events.""" # We just drop the next events reference, GC or EventPool will collect them. - self._head._next_event_ = None + self._head.next_event = None self._tail = self._head self._count = 0 - def append(self, event: Event): + def append_tail(self, event: ActualEvent) -> None: """Append an event to the end. Args: event (Event): New event to append. """ # Link to the tail, update the tail. - self._tail._next_event_ = event + self._tail.next_event = event self._tail = event - - # Counting. self._count += 1 - def insert(self, event: Event): + def append(self, event: ActualEvent) -> None: + """Alias for append_tail. + + Args: + event (Event): New event to append. + """ + self.append_tail(event) + + def append_head(self, event: ActualEvent) -> None: """Insert an event to the head, will be the first one to pop. Args: event (Event): Event to insert. """ # Link to head, update head. - event._next_event_ = self._head._next_event_ - self._head._next_event_ = event - - # Counting. - self._count += 1 - - def pop(self) -> Union[Event, EventList]: - """Pop first event that its state is not Finished. - - Returns: - Union[Event, EventList]: A list of decision events if current event is decision event, or an AtomEvent. + if self._count == 0: + self.append_tail(event) + else: + event.next_event = self._head.next_event + self._head.next_event = event + self._count += 1 + + def _extract_sub_events(self, event: CascadeEvent) -> None: + """Extract sub events (immediate events) of CascadeEvent to the head. """ - event: Event = self._head._next_event_ - - while event is not None: - # We will remove event from list until its state is FINISHED. - if event.state == EventState.FINISHED: - # Remove it (head). - self._head._next_event_ = event._next_event_ - - event._next_event_ = None + # Make immediate event list as the head of current list. + event.immediate_event_tail.next_event = self._head.next_event + self._head.next_event = event.immediate_event_head.next_event + self._count += event.immediate_event_count + event.clear() + + def _clear_finished_events(self) -> None: + """Remove all finished events from the head of the list. + """ + def _is_finish(event: ActualEvent) -> bool: + return event.state in (EventState.FINISHED, EventState.RECYCLING) - # Counting. - self._count -= 1 + while self._head.next_event is not None and _is_finish(self._head.next_event): + event = self._head.next_event + self._head.next_event = event.next_event + self._count -= 1 - # Extract sub events, this will change the head. + if isinstance(event, CascadeEvent) and event.immediate_event_count != 0: self._extract_sub_events(event) - event = self._head._next_event_ + def _collect_pending_decision_events(self) -> List[CascadeEvent]: + event = self._head.next_event + decision_events = [] + while event is not None and event.event_type == MaroEvents.PENDING_DECISION: + assert isinstance(event, CascadeEvent) + decision_events.append(event) + event = event.next_event + return decision_events - continue + def clear_finished_and_get_front(self) -> Union[None, ActualEvent, List[ActualEvent]]: + """Clear all finished events in the head of the list + and then get the first event that its state is not Finished. - if event.state == EventState.EXECUTING: - return event - - if event.event_type == MaroEvents.PENDING_DECISION: - decision_events = [event] - - # Find following decision events. - next_event: Event = event._next_event_ - - while next_event is not None and next_event.event_type == MaroEvents.PENDING_DECISION: - decision_events.append(next_event) - - next_event = next_event._next_event_ + Returns: + Union[Event, EventList]: A list of decision events if current event is a decision event, or an AtomEvent. + """ - return decision_events - else: - return event + self._clear_finished_events() - return None + if self._head.next_event is None: + return None + elif any([ + self._head.next_event.state == EventState.EXECUTING, + self._head.next_event.event_type != MaroEvents.PENDING_DECISION + ]): + return self._head.next_event + else: + return self._collect_pending_decision_events() def __len__(self): """Length of current list.""" return self._count def __iter__(self): - """Beginning of for loopping.""" + """Beginning of 'for' loop.""" self._iter_cur_event = self._head - return self def __next__(self): - """Get next item for 'for' loopping.""" - event: Event = None - - if self._iter_cur_event is None: + """Get next item for 'for' loop.""" + if self._iter_cur_event.next_event is None: raise StopIteration() - if self._iter_cur_event is not None: - event = self._iter_cur_event._next_event_ - - if event is None: - raise StopIteration() - else: - self._iter_cur_event = event._next_event_ - - return event - - def _extract_sub_events(self, event: Event): - """Extract sub events (immediate events) of CascadeEvent to the head. - - Args: - event (Event): Event to extract. - """ - if type(event) == CascadeEvent: - # Make immediate event list as the head of current list. - if event._last_immediate_event is not None: - event._last_immediate_event._next_event_ = self._head._next_event_ - self._head._next_event_ = event._immediate_event_head._next_event_ - - self._count += event._immediate_event_count - - # Clear the reference for finished event. - event._immediate_event_head._next_event_ = None - event._last_immediate_event = None + self._iter_cur_event = self._iter_cur_event.next_event + return self._iter_cur_event diff --git a/maro/event_buffer/event_pool.py b/maro/event_buffer/event_pool.py index 3921c52c1..0a67f071e 100644 --- a/maro/event_buffer/event_pool.py +++ b/maro/event_buffer/event_pool.py @@ -1,11 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from itertools import count +from typing import Iterable, Iterator, List, Union -from .atom_event import AtomEvent -from .cascade_event import CascadeEvent +from .event import ActualEvent, AtomEvent, CascadeEvent from .event_linked_list import EventLinkedList from .event_state import EventState -from .typings import Event, EventList, List, Union + + +def _pop(cntr: List[ActualEvent], event_cls_type: type) -> ActualEvent: + """Pop an event from related pool, generate buffer events if not enough.""" + return event_cls_type(None, None, None, None) if len(cntr) == 0 else cntr.pop() class EventPool: @@ -16,15 +21,24 @@ class EventPool: When enable pooling, it will recycle events. """ - def __init__(self, capacity: int = 1000): - self._capacity = capacity - self._atom_pool: List[AtomEvent] = [] - self._cascade_pool: List[CascadeEvent] = [] - self._recycle_buffer: EventList = [] + def __init__(self): + self._atom_events: List[AtomEvent] = [] + self._cascade_events: List[CascadeEvent] = [] + + self._event_count: Iterator[int] = count() + + @property + def atom_event_count(self) -> int: + return len(self._atom_events) - self._event_id: int = 0 + @property + def cascade_event_count(self) -> int: + return len(self._cascade_events) - def gen(self, tick: int, event_type: object, payload: object, is_cascade: bool = False) -> Event: + def gen( + self, tick: int, event_type: object, payload: object, + is_cascade: bool = False + ) -> ActualEvent: """Generate an event. Args: @@ -36,50 +50,35 @@ def gen(self, tick: int, event_type: object, payload: object, is_cascade: bool = Returns: Event: AtomEvent or CascadeEvent instance. """ - if is_cascade: - event = self._pop(self._cascade_pool, CascadeEvent) - else: - event = self._pop(self._atom_pool, AtomEvent) - - event.tick = tick - event.event_type = event_type - event.payload = payload - event.id = self._event_id - event.state = EventState.PENDING - - self._event_id += 1 - + event = _pop(self._cascade_events, CascadeEvent) if is_cascade else _pop(self._atom_events, AtomEvent) + event.reset_value( + id=next(self._event_count), tick=tick, event_type=event_type, + payload=payload, state=EventState.PENDING + ) return event - def recycle(self, events: Union[Event, EventList]): + def recycle(self, events: Union[ActualEvent, List[ActualEvent], EventLinkedList]) -> None: """Recycle specified event for further using. Args: events (Union[Event, EventList]): Event object(s) to recycle. """ - if type(events) != list and type(events) != EventLinkedList: - events = [events] + self._append(events) if isinstance(events, ActualEvent) else self._extend(events) + def _extend(self, events: Iterable[ActualEvent]) -> None: for event in events: - if event is not None: - self._append(event) + self._append(event) - def _append(self, event: Event): + def _append(self, event: ActualEvent) -> None: """Append event to related pool""" - if event: - # Deattach the payload before recycle. + if isinstance(event, ActualEvent): + # Detach the payload before recycle. event.payload = None - event._next_event_ = None - event.state = EventState.FINISHED + event.next_event = None + event.state = EventState.RECYCLING if isinstance(event, CascadeEvent): - self._cascade_pool.append(event) + self._cascade_events.append(event) else: - self._atom_pool.append(event) - - def _pop(self, cntr: EventList, event_cls_type: type): - """Pop an event from related pool, generate buffer events if not enough.""" - if len(cntr) == 0: - return event_cls_type(None, None, None, None) - else: - return cntr.pop() + assert isinstance(event, AtomEvent) + self._atom_events.append(event) diff --git a/maro/event_buffer/typings.py b/maro/event_buffer/typings.py deleted file mode 100644 index 706fb7351..000000000 --- a/maro/event_buffer/typings.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import List, Union - -from .atom_event import AtomEvent -from .cascade_event import CascadeEvent - -Event = Union[AtomEvent, CascadeEvent] - -EventList = List[Event] diff --git a/maro/simulator/abs_core.py b/maro/simulator/abs_core.py index 6a3b97bbb..cdbe0362f 100644 --- a/maro/simulator/abs_core.py +++ b/maro/simulator/abs_core.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from enum import IntEnum -from typing import List +from typing import List, Optional, Tuple from maro.backends.frame import SnapshotList from maro.event_buffer import EventBuffer @@ -46,26 +46,27 @@ def __init__( disable_finished_events: bool, options: dict ): - self._tick = start_tick - self._scenario = scenario - self._topology = topology - self._start_tick = start_tick - self._durations = durations - self._snapshot_resolution = snapshot_resolution - self._max_snapshots = max_snapshots - self._decision_mode = decision_mode - self._business_engine_cls = business_engine_cls - self._additional_options = options - - self._business_engine: AbsBusinessEngine = None - self._event_buffer: EventBuffer = None + self._tick: int = start_tick + self._scenario: str = scenario + self._topology: str = topology + self._start_tick: int = start_tick + self._durations: int = durations + self._snapshot_resolution: int = snapshot_resolution + self._max_snapshots: int = max_snapshots + self._decision_mode: DecisionMode = decision_mode + self._business_engine_cls: type = business_engine_cls + self._disable_finished_events: bool = disable_finished_events + self._additional_options: dict = options + + self._business_engine: Optional[AbsBusinessEngine] = None + self._event_buffer: Optional[EventBuffer] = None @property - def business_engine(self): + def business_engine(self) -> AbsBusinessEngine: return self._business_engine @abstractmethod - def step(self, action): + def step(self, action) -> Tuple[Optional[dict], Optional[List[object]], Optional[bool]]: """Push the environment to next step with action. Args: @@ -77,12 +78,12 @@ def step(self, action): pass @abstractmethod - def dump(self): + def dump(self) -> None: """Dump environment for restore.""" pass @abstractmethod - def reset(self): + def reset(self) -> None: """Reset environment.""" pass @@ -111,6 +112,7 @@ def tick(self) -> int: pass @property + @abstractmethod def frame_index(self) -> int: """int: Frame index in snapshot list for current tick, USE this for snapshot querying.""" pass @@ -127,7 +129,7 @@ def snapshot_list(self) -> SnapshotList: """SnapshotList: Current snapshot list, a snapshot list contains all the snapshots of frame at each tick.""" pass - def set_seed(self, seed: int): + def set_seed(self, seed: int) -> None: """Set random seed used by simulator. NOTE: @@ -147,10 +149,12 @@ def metrics(self) -> dict: """ return {} + @abstractmethod def get_finished_events(self) -> list: """list: All events finished so far.""" pass + @abstractmethod def get_pending_events(self, tick: int) -> list: """list: Pending events at certain tick. diff --git a/maro/simulator/core.py b/maro/simulator/core.py index 176c6b34f..a8910c1c2 100644 --- a/maro/simulator/core.py +++ b/maro/simulator/core.py @@ -4,17 +4,17 @@ from collections.abc import Iterable from importlib import import_module from inspect import getmembers, isclass -from typing import List +from typing import Generator, List, Optional, Tuple from maro.backends.frame import FrameBase, SnapshotList from maro.data_lib.dump_csv_converter import DumpConverter -from maro.event_buffer import EventBuffer, EventState +from maro.event_buffer import ActualEvent, CascadeEvent, EventBuffer, EventState from maro.streamit import streamit from maro.utils.exception.simulator_exception import BusinessEngineNotFoundError from .abs_core import AbsEnv, DecisionMode from .scenarios.abs_business_engine import AbsBusinessEngine -from .utils import seed as sim_seed +from .utils import random from .utils.common import tick_to_frame_index @@ -47,17 +47,16 @@ def __init__( business_engine_cls: type = None, disable_finished_events: bool = False, record_finished_events: bool = False, record_file_path: str = None, - options: dict = {} - ): + options: Optional[dict] = None + ) -> None: super().__init__( scenario, topology, start_tick, durations, snapshot_resolution, max_snapshots, decision_mode, business_engine_cls, - disable_finished_events, options + disable_finished_events, options if options is not None else {} ) self._name = f'{self._scenario}:{self._topology}' if business_engine_cls is None \ else business_engine_cls.__name__ - self._business_engine: AbsBusinessEngine = None self._event_buffer = EventBuffer(disable_finished_events, record_finished_events, record_file_path) @@ -72,12 +71,12 @@ def __init__( if "enable-dump-snapshot" in self._additional_options: parent_path = self._additional_options["enable-dump-snapshot"] - self._converter = DumpConverter(parent_path, self._business_engine._scenario_name) + self._converter = DumpConverter(parent_path, self._business_engine.scenario_name) self._converter.reset_folder_path() self._streamit_episode = 0 - def step(self, action): + def step(self, action) -> Tuple[Optional[dict], Optional[List[object]], Optional[bool]]: """Push the environment to next step with action. Args: @@ -93,7 +92,7 @@ def step(self, action): return metrics, decision_event, _is_done - def dump(self): + def dump(self) -> None: """Dump environment for restore. NOTE: @@ -101,8 +100,12 @@ def dump(self): """ return - def reset(self): - """Reset environment.""" + def reset(self, keep_seed: bool = False) -> None: + """Reset environment. + + Args: + keep_seed (bool): Reset the random seed to the generate the same data sequence or not. Defaults to False. + """ self._tick = self._start_tick self._simulate_generator.close() @@ -110,17 +113,17 @@ def reset(self): self._event_buffer.reset() - if ("enable-dump-snapshot" in self._additional_options) and (self._business_engine._frame is not None): + if "enable-dump-snapshot" in self._additional_options and self._business_engine.frame is not None: dump_folder = self._converter.get_new_snapshot_folder() - self._business_engine._frame.dump(dump_folder) + self._business_engine.frame.dump(dump_folder) self._converter.start_processing(self.configs) self._converter.dump_descsion_events(self._decision_events, self._start_tick, self._snapshot_resolution) self._business_engine.dump(dump_folder) self._decision_events.clear() - self._business_engine.reset() + self._business_engine.reset(keep_seed) @property def configs(self) -> dict: @@ -133,7 +136,7 @@ def summary(self) -> dict: return { "node_mapping": self._business_engine.get_node_mapping(), "node_detail": self.current_frame.get_node_info(), - "event_payload": self._business_engine.get_event_payload_detail(), + "event_payload": self._business_engine.get_event_payload_detail() } @property @@ -169,7 +172,7 @@ def agent_idx_list(self) -> List[int]: """List[int]: Agent index list that related to this environment.""" return self._business_engine.get_agent_idx_list() - def set_seed(self, seed: int): + def set_seed(self, seed: int) -> None: """Set random seed used by simulator. NOTE: @@ -180,7 +183,7 @@ def set_seed(self, seed: int): """ if seed is not None: - sim_seed(seed) + random.seed(seed) @property def metrics(self) -> dict: @@ -192,11 +195,11 @@ def metrics(self) -> dict: return self._business_engine.get_metrics() - def get_finished_events(self): + def get_finished_events(self) -> List[ActualEvent]: """List[Event]: All events finished so far.""" return self._event_buffer.get_finished_events() - def get_pending_events(self, tick): + def get_pending_events(self, tick) -> List[ActualEvent]: """Pending events at certain tick. Args: @@ -204,7 +207,7 @@ def get_pending_events(self, tick): """ return self._event_buffer.get_pending_events(tick) - def _init_business_engine(self): + def _init_business_engine(self) -> None: """Initialize business engine object. NOTE: @@ -234,7 +237,7 @@ def _init_business_engine(self): if business_class is None: raise BusinessEngineNotFoundError() - self._business_engine = business_class( + self._business_engine: AbsBusinessEngine = business_class( event_buffer=self._event_buffer, topology=self._topology, start_tick=self._start_tick, @@ -244,10 +247,8 @@ def _init_business_engine(self): additional_options=self._additional_options ) - def _simulate(self): + def _simulate(self) -> Generator[Tuple[dict, List[object], bool], object, None]: """This is the generator to wrap each episode process.""" - is_end_tick = False - self._streamit_episode += 1 streamit.episode(self._streamit_episode) @@ -263,10 +264,7 @@ def _simulate(self): # Keep processing events, until no more events in this tick. pending_events = self._event_buffer.execute(self._tick) - # Processing pending events. - pending_event_length: int = len(pending_events) - - if pending_event_length == 0: + if len(pending_events) == 0: # We have processed all the event of current tick, lets go for next tick. break @@ -287,8 +285,7 @@ def _simulate(self): if actions is None: # Make business engine easy to work. actions = [] - - if actions is not None and not isinstance(actions, Iterable): + elif not isinstance(actions, Iterable): actions = [actions] if self._decision_mode == DecisionMode.Sequential: @@ -297,8 +294,10 @@ def _simulate(self): # NOTE: decision event always be a CascadeEvent # We just append the action into sub event of first pending cascade event. - pending_events[0].state = EventState.EXECUTING - pending_events[0].add_immediate_event(action_event, is_head=True) + event = pending_events[0] + assert isinstance(event, CascadeEvent) + event.state = EventState.EXECUTING + event.add_immediate_event(action_event, is_head=True) else: # For joint mode, we will assign actions from beginning to end. # Then mark others pending events to finished if not sequential action mode. @@ -314,6 +313,7 @@ def _simulate(self): pending_event.state = EventState.EXECUTING action_event = self._event_buffer.gen_action_event(self._tick, action) + assert isinstance(pending_event, CascadeEvent) pending_event.add_immediate_event(action_event, is_head=True) # Check the end tick of the simulation to decide if we should end the simulation. diff --git a/maro/simulator/scenarios/abs_business_engine.py b/maro/simulator/scenarios/abs_business_engine.py index 5aeb77aa8..382e8b506 100644 --- a/maro/simulator/scenarios/abs_business_engine.py +++ b/maro/simulator/scenarios/abs_business_engine.py @@ -4,6 +4,7 @@ import os from abc import ABC, abstractmethod from pathlib import Path +from typing import List, Optional from maro.backends.frame import FrameBase, SnapshotList from maro.event_buffer import EventBuffer @@ -31,23 +32,23 @@ class AbsBusinessEngine(ABC): max_tick (int): Max tick of this business engine. snapshot_resolution (int): Frequency to take a snapshot. max_snapshots(int): Max number of in-memory snapshots, default is None that means max number of snapshots. - addition_options (dict): Additional options for this business engine from outside. + additional_options (dict): Additional options for this business engine from outside. """ def __init__( - self, scenario_name: str, event_buffer: EventBuffer, topology: str, - start_tick: int, max_tick: int, snapshot_resolution: int, max_snapshots: int, + self, scenario_name: str, event_buffer: EventBuffer, topology: Optional[str], + start_tick: int, max_tick: int, snapshot_resolution: int, max_snapshots: Optional[int], additional_options: dict = None ): - self._scenario_name = scenario_name - self._topology = topology - self._event_buffer = event_buffer - self._start_tick = start_tick - self._max_tick = max_tick - self._snapshot_resolution = snapshot_resolution - self._max_snapshots = max_snapshots - self._additional_options = additional_options - self._config_path = None + self._scenario_name: str = scenario_name + self._topology: str = topology + self._event_buffer: EventBuffer = event_buffer + self._start_tick: int = start_tick + self._max_tick: int = max_tick + self._snapshot_resolution: int = snapshot_resolution + self._max_snapshots: int = max_snapshots + self._additional_options: dict = additional_options + self._config_path: Optional[str] = None assert start_tick >= 0 assert max_tick > start_tick @@ -65,6 +66,15 @@ def snapshots(self) -> SnapshotList: """SnapshotList: Snapshot list of current frame, this is used to expose querying interface for outside.""" pass + @property + def scenario_name(self) -> str: + return self._scenario_name + + @abstractmethod + def get_agent_idx_list(self) -> List[int]: + """Get a list of agent index.""" + pass + def frame_index(self, tick: int) -> int: """Helper method for child class, used to get index of frame in snapshot list for specified tick. @@ -89,7 +99,7 @@ def calc_max_snapshots(self) -> int: return self._max_snapshots if self._max_snapshots is not None \ else total_frames(self._start_tick, self._max_tick, self._snapshot_resolution) - def update_config_root_path(self, business_engine_file_path: str): + def update_config_root_path(self, business_engine_file_path: str) -> None: """Helper method used to update the config path with business engine path if you follow the way to load configuration file as built-in scenarios. @@ -125,7 +135,7 @@ def __init__(self, *args, **kwargs): self._config_path = os.path.join(be_file_path, "topologies", self._topology) @abstractmethod - def step(self, tick: int): + def step(self, tick: int) -> None: """Method that is called at each tick, usually used to trigger business logic at current tick. Args: @@ -134,12 +144,13 @@ def step(self, tick: int): pass @property + @abstractmethod def configs(self) -> dict: """dict: Configurations of this business engine.""" pass @abstractmethod - def reset(self): + def reset(self, keep_seed: bool = False) -> None: """Reset states business engine.""" pass @@ -183,7 +194,7 @@ def get_metrics(self) -> dict: """ return {} - def dump(self, folder: str): + def dump(self, folder: str) -> None: """Dump something from business engine. Args: diff --git a/maro/simulator/scenarios/cim/business_engine.py b/maro/simulator/scenarios/cim/business_engine.py index 1370a860a..ccee78268 100644 --- a/maro/simulator/scenarios/cim/business_engine.py +++ b/maro/simulator/scenarios/cim/business_engine.py @@ -4,6 +4,7 @@ import os from math import ceil, floor +from typing import Optional import numpy as np from yaml import safe_load @@ -28,8 +29,8 @@ order_requirements (int): Accumulative orders until now. container_shortage (int): Accumulative shortage until now. -operation_number (int): Total empty transfer (both load and discharge) cost, - the cost factors can be configured in configuration file at section "transfer_cost_factors". +operation_number (int): Total empty operation (both load and discharge) cost, + the cost factors can be configured through 'load_cost_factor' and 'dsch_cost_factor' in configuration file. """ @@ -37,8 +38,8 @@ class CimBusinessEngine(AbsBusinessEngine): """Cim business engine, used simulate CIM related problem.""" def __init__( - self, event_buffer: EventBuffer, topology: str, start_tick: int, max_tick: int, - snapshot_resolution: int, max_snapshots: int, additional_options: dict = None + self, event_buffer: EventBuffer, topology: Optional[str], start_tick: int, max_tick: int, + snapshot_resolution: int, max_snapshots: Optional[int], additional_options: dict = None ): super().__init__( "cim", event_buffer, topology, start_tick, max_tick, @@ -48,29 +49,28 @@ def __init__( # Update self._config_path with current file path. self.update_config_root_path(__file__) - config_path = os.path.join(self._config_path, "config.yml") - # Load data from wrapper. self._data_cntr: CimDataContainerWrapper = CimDataContainerWrapper( - config_path, max_tick, self._topology) + self._config_path, max_tick, self._topology + ) # Create a copy of config object to expose to others, and not affect generator. - with open(config_path) as fp: - self._config = safe_load(fp) + self._config = {} + config_path = os.path.join(self._config_path, "config.yml") + if os.path.exists(config_path): + with open(config_path) as fp: + self._config = safe_load(fp) self._vessels = [] self._ports = [] - self._frame = None - self._full_on_ports: MatrixAttributeAccessor = None - self._full_on_vessels: MatrixAttributeAccessor = None - self._vessel_plans: MatrixAttributeAccessor = None + self._frame: Optional[FrameBase] = None + self._full_on_ports: Optional[MatrixAttributeAccessor] = None + self._full_on_vessels: Optional[MatrixAttributeAccessor] = None + self._vessel_plans: Optional[MatrixAttributeAccessor] = None self._port_orders_exporter = PortOrderExporter("enable-dump-snapshot" in additional_options) - # Read transfer cost factors. - transfer_cost_factors = self._config["transfer_cost_factors"] - - self._load_cost_factor: float = transfer_cost_factors["load"] - self._dsch_cost_factor: float = transfer_cost_factors["dsch"] + self._load_cost_factor: float = self._data_cntr.load_cost_factor + self._dsch_cost_factor: float = self._data_cntr.dsch_cost_factor # Used to collect total cost to avoid to much snapshot querying. self._total_operate_num: float = 0 @@ -197,7 +197,7 @@ def post_step(self, tick: int): return tick + 1 == self._max_tick - def reset(self): + def reset(self, keep_seed: bool = False): """Reset the business engine, it will reset frame value.""" self._snapshots.reset() @@ -206,11 +206,13 @@ def reset(self): self._reset_nodes() - self._data_cntr.reset() + self._data_cntr.reset(keep_seed) # Insert departure event again. self._load_departure_events() + self._init_vessel_plans() + self._total_operate_num = 0 def action_scope(self, port_idx: int, vessel_idx: int) -> ActionScope: @@ -238,19 +240,17 @@ def early_discharge(self, vessel_idx: int) -> int: def get_metrics(self) -> DocableDict: """Get metrics information for cim scenario. - - Args: - dict: A dict that contains "perf", "total_shortage" and "total_cost", - and can use help method to show help docs. """ total_shortage = sum([p.acc_shortage for p in self._ports]) total_booking = sum([p.acc_booking for p in self._ports]) return DocableDict( metrics_desc, - order_requirements=total_booking, - container_shortage=total_shortage, - operation_number=self._total_operate_num + { + 'order_requirements': total_booking, + 'container_shortage': total_shortage, + 'operation_number': self._total_operate_num + } ) def get_node_mapping(self) -> dict: @@ -416,6 +416,7 @@ def _on_order_generated(self, event: CascadeEvent): Args: event (CascadeEvent): Order event object. """ + assert isinstance(event.payload, Order) order: Order = event.payload src_port = self._ports[order.src_port_idx] @@ -434,7 +435,7 @@ def _on_order_generated(self, event: CascadeEvent): # Update port state. src_port.empty -= execute_qty - # Full contianers that pending to return. + # Full containers that pending to return. src_port.on_shipper += execute_qty buffer_ticks = self._data_cntr.full_return_buffers[src_port.idx] @@ -448,10 +449,8 @@ def _on_order_generated(self, event: CascadeEvent): ) # If buffer_tick is 0, we should execute it as this tick. - if buffer_ticks == 0: - event.add_immediate_event(laden_return_evt) - else: - self._event_buffer.insert_event(laden_return_evt) + event.add_immediate_event(laden_return_evt) if buffer_ticks == 0 \ + else self._event_buffer.insert_event(laden_return_evt) def _on_full_return(self, event: AtomEvent): """Handler for processing the event that full containers are returned from shipper. @@ -460,6 +459,7 @@ def _on_full_return(self, event: AtomEvent): 1. First move the container from on_shipper to full (update state: on_shipper -> full). 2. Then append the container to the port pending list. """ + assert isinstance(event.payload, LadenReturnPayload) payload: LadenReturnPayload = event.payload src_port = self._ports[payload.src_port_idx] @@ -486,7 +486,7 @@ def _on_full_load(self, event: AtomEvent): Args: event (AtomEvent): Arrival event object. """ - + assert isinstance(event.payload, VesselStatePayload) arrival_obj: VesselStatePayload = event.payload vessel_idx: int = arrival_obj.vessel_idx port_idx: int = arrival_obj.port_idx @@ -555,7 +555,7 @@ def _on_arrival(self, event: AtomEvent): Args: event (AtomEvent): Arrival event object. """ - + assert isinstance(event.payload, VesselStatePayload) arrival_payload: VesselStatePayload = event.payload vessel_idx = arrival_payload.vessel_idx vessel = self._vessels[vessel_idx] @@ -586,7 +586,7 @@ def _on_departure(self, event: AtomEvent): Args: event (AtomEvent): Departure event object. """ - + assert isinstance(event.payload, VesselStatePayload) departure_payload: VesselStatePayload = event.payload vessel_idx = departure_payload.vessel_idx vessel = self._vessels[vessel_idx] @@ -612,6 +612,7 @@ def _on_discharge(self, event: CascadeEvent): Args: event (AtomEvent): Discharge event object. """ + assert isinstance(event.payload, VesselDischargePayload) discharge_payload: VesselDischargePayload = event.payload vessel_idx = discharge_payload.vessel_idx port_idx = discharge_payload.port_idx @@ -630,10 +631,8 @@ def _on_discharge(self, event: CascadeEvent): tick=event.tick + buffer_ticks, event_type=Events.RETURN_EMPTY, payload=payload ) - if buffer_ticks == 0: - event.add_immediate_event(mt_return_evt) - else: - self._event_buffer.insert_event(mt_return_evt) + event.add_immediate_event(mt_return_evt) if buffer_ticks == 0 \ + else self._event_buffer.insert_event(mt_return_evt) def _on_empty_return(self, event: AtomEvent): """Handler for processing event when there are some empty container return to port. @@ -641,6 +640,7 @@ def _on_empty_return(self, event: AtomEvent): Args: event (AtomEvent): Empty-return event object. """ + assert isinstance(event.payload, EmptyReturnPayload) payload: EmptyReturnPayload = event.payload port = self._ports[payload.port_idx] @@ -654,11 +654,9 @@ def _on_action_received(self, event: CascadeEvent): event (CascadeEvent): Action event object with expected payload: {vessel_id: empty_number_to_move}}. """ actions = event.payload + assert isinstance(actions, list) if actions: - if type(actions) is not list: - actions = [actions] - for action in actions: vessel_idx = action.vessel_idx port_idx = action.port_idx @@ -668,14 +666,8 @@ def _on_action_received(self, event: CascadeEvent): port_empty = port.empty vessel_empty = vessel.empty - action_type: ActionType = getattr(action, "action_type", None) - - # Make it compatiable with previous action. - if action_type is None: - action_type = ActionType.DISCHARGE if move_num > 0 else ActionType.LOAD - - # Make sure the move number is positive, as we have the action type. - move_num = abs(move_num) + assert isinstance(action, Action) + action_type = action.action_type if action_type == ActionType.DISCHARGE: assert(move_num <= vessel_empty) diff --git a/maro/simulator/scenarios/cim/common.py b/maro/simulator/scenarios/cim/common.py index 67c092eb6..9add975bc 100644 --- a/maro/simulator/scenarios/cim/common.py +++ b/maro/simulator/scenarios/cim/common.py @@ -25,16 +25,20 @@ class Action: Args: vessel_idx (int): Which vessel will take action. port_idx (int): Which port will take action. - quantity (int): How many containers can be moved from vessel to port (negative in reverse). + action_type (ActionType): Whether the action is a Load or a Discharge. + quantity (int): How many containers are loaded/discharged in this Action. """ - summary_key = ["port_idx", "vessel_idx", "quantity"] + summary_key = ["port_idx", "vessel_idx", "action_type", "quantity"] def __init__(self, vessel_idx: int, port_idx: int, quantity: int, action_type: ActionType): - self.vessel_idx = vessel_idx - self.port_idx = port_idx - self.quantity = quantity - self.action_type = action_type + assert action_type is not None + assert quantity >= 0 + + self.vessel_idx: int = vessel_idx + self.port_idx: int = port_idx + self.quantity: int = quantity + self.action_type: ActionType = action_type def __repr__(self): return "%s {action_type: %r, port_idx: %r, vessel_idx: %r, quantity: %r}" % \ @@ -65,7 +69,7 @@ class DecisionEvent: tick (int): On which tick we need an action. port_idx (int): Which port will take action. vessel_idx (int): Which vessel will take action. - snapshot_list (int): Snapshots of the environment to input into the decision model. + snapshot_list (SnapshotList): Snapshots of the environment to input into the decision model. action_scope_func (Function): Function to calculate action scope, we use function here to make it getting the value as late as possible. early_discharge_func (Function): Function to fetch early discharge number of specified vessel, we @@ -107,7 +111,7 @@ def early_discharge(self) -> int: return int(self._early_discharge) def __getstate__(self): - """Return pickleable dictionary. + """Return pickle-able dictionary. NOTE: this class do not support unpickle""" return { diff --git a/maro/simulator/scenarios/cim/frame_builder.py b/maro/simulator/scenarios/cim/frame_builder.py index 3a7a05903..77d10a931 100644 --- a/maro/simulator/scenarios/cim/frame_builder.py +++ b/maro/simulator/scenarios/cim/frame_builder.py @@ -15,6 +15,7 @@ def gen_cim_frame(port_num: int, vessel_num: int, stop_nums: tuple, snapshots_nu port_num (int): Number of ports. vessel_num (int): Number of vessels. stop_nums (tuple): Past stops number and future stop number. + snapshots_num (int): Number of snapshots. """ vessel_cls = gen_vessel_definition(stop_nums) matrix_cls = gen_matrix(port_num, vessel_num) diff --git a/maro/simulator/scenarios/cim/matrix.py b/maro/simulator/scenarios/cim/matrix.py index c3620fe08..1c90e89d9 100644 --- a/maro/simulator/scenarios/cim/matrix.py +++ b/maro/simulator/scenarios/cim/matrix.py @@ -15,6 +15,7 @@ def gen_matrix(port_num: int, vessel_num: int): Return: type: Matrix class definition. """ + @node("matrices") class GeneralInfoMatrix(NodeBase): """Used to save matrix, and provide matrix accessor.""" @@ -23,20 +24,17 @@ class GeneralInfoMatrix(NodeBase): full_on_ports = NodeAttribute("i", slot_num=port_num * port_num) # distribution of full from vessel to port full_on_vessels = NodeAttribute("i", slot_num=vessel_num * port_num) - # planed route info for vessels + # planned route info for vessels vessel_plans = NodeAttribute("i", slot_num=vessel_num * port_num) def __init__(self): # we cannot create matrix accessor here, since the attributes will be bind after frame setup, - self._acc_dict = {} - self._acc_dict["full_on_ports"] = MatrixAttributeAccessor(self, "full_on_ports", port_num, port_num) - self._acc_dict["full_on_vessels"] = MatrixAttributeAccessor(self, "full_on_vessels", vessel_num, port_num) - self._acc_dict["vessel_plans"] = MatrixAttributeAccessor(self, "vessel_plans", vessel_num, port_num) + self._acc_dict = { + "full_on_ports": MatrixAttributeAccessor(self, "full_on_ports", port_num, port_num), + "full_on_vessels": MatrixAttributeAccessor(self, "full_on_vessels", vessel_num, port_num), + "vessel_plans": MatrixAttributeAccessor(self, "vessel_plans", vessel_num, port_num)} def __getitem__(self, key): - if key in self._acc_dict: - return self._acc_dict[key] - - return None + return self._acc_dict.get(key, None) return GeneralInfoMatrix diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.0/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.0/config.yml index 70a7e1b87..ffda23a80 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.0/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.0/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.1/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.1/config.yml index 6f958cc54..1dd848626 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.1/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.1/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.2/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.2/config.yml index 17ec993ae..03f13a339 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.2/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.2/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.3/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.3/config.yml index 34c8cf339..8d67342cd 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.3/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.3/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.4/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.4/config.yml index 4d4b4d7a8..7e1fe59f7 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.4/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.4/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.5/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.5/config.yml index 247f390ed..785ad3eb3 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.5/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.5/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.6/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.6/config.yml index 409f18a99..3d9dc207f 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.6/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.6/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.7/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.7/config.yml index 37177b357..bb9f7bb06 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.7/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.7/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.8/config.yml b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.8/config.yml index 4015e2653..e878b40e9 100644 --- a/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.8/config.yml +++ b/maro/simulator/scenarios/cim/topologies/global_trade.22p_l0.8/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.0/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.0/config.yml index 05f5b2a09..b48e42940 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.0/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.0/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.1/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.1/config.yml index 1f6a39441..44b690373 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.1/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.1/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.2/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.2/config.yml index 61a74fae9..4a17fa8bc 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.2/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.2/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.3/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.3/config.yml index d1178a4be..7241f73f2 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.3/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.3/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.4/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.4/config.yml index ee13998f8..7684f215f 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.4/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.4/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.5/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.5/config.yml index a119ac238..6ba921a4b 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.5/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.5/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.6/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.6/config.yml index f3d5fac23..ad59c217f 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.6/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.6/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.7/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.7/config.yml index 563aa8825..5358c02e9 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.7/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.7/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.8/config.yml b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.8/config.yml index ed4365ecd..0c97217bf 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.8/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.4p_ssdd_l0.8/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.0/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.0/config.yml index 760d79ef3..061319b39 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.0/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.0/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.1/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.1/config.yml index df4a285f1..ac3fe7a76 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.1/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.1/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.2/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.2/config.yml index 2c7d44363..715cebb3c 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.2/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.2/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.3/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.3/config.yml index d286e9be9..7006dc74c 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.3/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.3/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.4/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.4/config.yml index 01631756b..a78ff1c4e 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.4/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.4/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.5/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.5/config.yml index 4e223baab..bebd8f4d2 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.5/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.5/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.6/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.6/config.yml index d08640fe1..c6605725e 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.6/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.6/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.7/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.7/config.yml index ce577f531..03d2b215a 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.7/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.7/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.8/config.yml b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.8/config.yml index efcca672b..91ca5067a 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.8/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.5p_ssddd_l0.8/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.0/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.0/config.yml index e441a579e..7838bc01c 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.0/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.0/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.1/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.1/config.yml index 98a08d4e0..c22606280 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.1/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.1/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.2/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.2/config.yml index da33dd7f1..dd9107709 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.2/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.2/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.3/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.3/config.yml index 7f2d344a5..5e5673071 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.3/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.3/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.4/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.4/config.yml index d73462929..99e932182 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.4/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.4/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.5/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.5/config.yml index 9a6c46b64..ca34e9b2d 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.5/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.5/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.6/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.6/config.yml index b9ef67105..a98cf594c 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.6/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.6/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.7/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.7/config.yml index 04e148445..dfb6dc863 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.7/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.7/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.8/config.yml b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.8/config.yml index 835be1493..a630f6983 100644 --- a/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.8/config.yml +++ b/maro/simulator/scenarios/cim/topologies/toy.6p_sssbdd_l0.8/config.yml @@ -1,7 +1,6 @@ seed: 4096 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/maro/simulator/scenarios/citi_bike/business_engine.py b/maro/simulator/scenarios/citi_bike/business_engine.py index cc3da8051..cff8170e1 100644 --- a/maro/simulator/scenarios/citi_bike/business_engine.py +++ b/maro/simulator/scenarios/citi_bike/business_engine.py @@ -3,7 +3,7 @@ import datetime import os -from typing import List +from typing import List, Optional import holidays import numpy as np @@ -46,8 +46,8 @@ class CitibikeBusinessEngine(AbsBusinessEngine): def __init__( - self, event_buffer: EventBuffer, topology: str, start_tick: int, - max_tick: int, snapshot_resolution: int, max_snapshots: int, additional_options: dict = {} + self, event_buffer: EventBuffer, topology: Optional[str], start_tick: int, + max_tick: int, snapshot_resolution: int, max_snapshots: Optional[int], additional_options: dict = {} ): super().__init__( "citi_bike", event_buffer, topology, start_tick, max_tick, @@ -103,10 +103,10 @@ def step(self, tick: int): if self._decision_strategy.is_decision_tick(tick): # Generate an event, so that we can do the checking after all the trip requirement processed. - decition_checking_evt = self._event_buffer.gen_atom_event( + decision_checking_evt = self._event_buffer.gen_atom_event( tick, CitiBikeEvents.RebalanceBike) - self._event_buffer.insert_event(decition_checking_evt) + self._event_buffer.insert_event(decision_checking_evt) # Update our additional features that not trip related. self._update_station_extra_features(tick) @@ -146,7 +146,7 @@ def get_event_payload_detail(self) -> dict: CitiBikeEvents.DeliverBike.name: BikeTransferPayload.summary_key } - def reset(self): + def reset(self, keep_seed: bool = False): """Reset internal states for episode.""" self._total_trips = 0 self._total_operate_num = 0 @@ -178,7 +178,7 @@ def get_agent_idx_list(self) -> List[int]: """ return [station.index for station in self._stations] - def get_metrics(self) -> dict: + def get_metrics(self) -> DocableDict: """Get current metrics information. Note: @@ -189,9 +189,11 @@ def get_metrics(self) -> dict: """ return DocableDict( metrics_desc, - trip_requirements=self._total_trips, - bike_shortage=self._total_shortages, - operation_number=self._total_operate_num + { + 'trip_requirements': self._total_trips, + 'bike_shortage': self._total_shortages, + 'operation_number': self._total_operate_num + } ) def __del__(self): diff --git a/maro/simulator/scenarios/helpers.py b/maro/simulator/scenarios/helpers.py index b1b15c12d..2adad47f3 100644 --- a/maro/simulator/scenarios/helpers.py +++ b/maro/simulator/scenarios/helpers.py @@ -30,11 +30,11 @@ class DocableDict: Args: doc (str): Customized doc of the dict. - kwargs (dict): Dictionary items to store. + origin_dict (dict): Dictionary items to store. """ - def __init__(self, doc: str, **kwargs): - self._original_dict = kwargs + def __init__(self, doc: str, origin_dict: dict): + self._original_dict = origin_dict DocableDict.__doc__ = doc def __getattr__(self, name): diff --git a/maro/simulator/scenarios/vm_scheduling/business_engine.py b/maro/simulator/scenarios/vm_scheduling/business_engine.py index 0b64be1cd..d13a49f6a 100644 --- a/maro/simulator/scenarios/vm_scheduling/business_engine.py +++ b/maro/simulator/scenarios/vm_scheduling/business_engine.py @@ -4,7 +4,7 @@ import os import shutil import tarfile -from typing import Dict, List +from typing import Dict, List, Optional from yaml import safe_load @@ -51,11 +51,11 @@ class VmSchedulingBusinessEngine(AbsBusinessEngine): def __init__( self, event_buffer: EventBuffer, - topology: str, + topology: Optional[str], start_tick: int, max_tick: int, snapshot_resolution: int, - max_snapshots: int, + max_snapshots: Optional[int], additional_options: dict = {} ): super().__init__( @@ -405,7 +405,7 @@ def _init_pms(self, pm_dict: dict): return start_pm_id - def reset(self): + def reset(self, keep_seed: bool = False): """Reset internal states for episode.""" self._init_metrics() @@ -565,19 +565,21 @@ def get_metrics(self) -> DocableDict: return DocableDict( metrics_desc, - total_vm_requests=self._total_vm_requests, - total_incomes=self._total_incomes, - energy_consumption_cost=self._energy_consumption_cost, - total_profit=self._total_profit, - total_energy_consumption=self._total_energy_consumption, - successful_allocation=self._successful_allocation, - successful_completion=self._successful_completion, - failed_allocation=self._failed_allocation, - failed_completion=self._failed_completion, - total_latency=self._total_latency, - total_oversubscriptions=self._total_oversubscriptions, - total_overload_pms=self._total_overload_pms, - total_overload_vms=self._total_overload_vms + { + 'total_vm_requests': self._total_vm_requests, + 'total_incomes': self._total_incomes, + 'energy_consumption_cost': self._energy_consumption_cost, + 'total_profit': self._total_profit, + 'total_energy_consumption': self._total_energy_consumption, + 'successful_allocation': self._successful_allocation, + 'successful_completion': self._successful_completion, + 'failed_allocation': self._failed_allocation, + 'failed_completion': self._failed_completion, + 'total_latency': self._total_latency, + 'total_oversubscriptions': self._total_oversubscriptions, + 'total_overload_pms': self._total_overload_pms, + 'total_overload_vms': self._total_overload_vms + } ) def _register_events(self): diff --git a/maro/simulator/utils/sim_random.py b/maro/simulator/utils/sim_random.py index 3916169d5..960b75a11 100644 --- a/maro/simulator/utils/sim_random.py +++ b/maro/simulator/utils/sim_random.py @@ -31,7 +31,6 @@ def __init__(self): self._rand_instances: Dict[str, Random] = OrderedDict() self._seed_dict: Dict[str, int] = {} self._seed = int(time.time()) - self._index = 0 def seed(self, seed_num: int): """Set seed for simulator random objects. @@ -46,47 +45,53 @@ def seed(self, seed_num: int): self._seed = seed_num - self._index = 0 - for key, rand in self._rand_instances.items(): + for index, (key, rand) in enumerate(self._rand_instances.items()): # we set seed for each random instance with 1 offset - seed = seed_num + self._index + seed = seed_num + index rand.seed(seed) self._seed_dict[key] = seed - self._index += 1 - - def __getitem__(self, key): + def create_instance(self, key: str) -> None: assert type(key) is str if key not in self._rand_instances: + self._seed_dict[key] = self._seed + len(self._rand_instances) r = Random() - r.seed(self._seed + self._index) + r.seed(self._seed_dict[key]) + self._rand_instances[key] = r - self._index += 1 + def __getitem__(self, key): + assert type(key) is str - self._rand_instances[key] = r + if key not in self._rand_instances: + self.create_instance(key) return self._rand_instances[key] - def get_seed(self, key: str = None) -> int: - """Get seed of current random generator. + def reset_seed(self, key: str) -> None: + """Reset seed of current random generator. NOTE: - This will only return the seed of first random object that specified by user (or default). + This will reset the seed to the value that specified by user (or default). Args: key(str): Key of item to get. - - Returns: - int: If key is None return seed for 1st instance (same as what passed to seed function), - else return seed for specified generator. """ - if key is not None: - return self._seed_dict.get(key, None) + assert type(key) is str - return self._seed + if key not in self._seed_dict: + self.create_instance(key) + rand = self._rand_instances[key] + rand.seed(self._seed_dict[key]) + + def clear(self) -> None: + """Clear all existing random keys. + """ + self._rand_instances = OrderedDict() + self._seed_dict = {} + self._seed = int(time.time()) random = SimRandom() diff --git a/maro/utils/exception/data_lib_exeption.py b/maro/utils/exception/data_lib_exception.py similarity index 100% rename from maro/utils/exception/data_lib_exeption.py rename to maro/utils/exception/data_lib_exception.py diff --git a/maro/utils/utils.py b/maro/utils/utils.py index 7e59239da..dd83f5f7b 100644 --- a/maro/utils/utils.py +++ b/maro/utils/utils.py @@ -146,7 +146,7 @@ def deploy(hide_info: bool = True): # Deployment failed. error_list.append( - "An issue occured while deploying meta files for MARO." + "An issue occurred while deploying meta files for MARO." f" {e} Please run 'maro meta deploy' to deploy the data files." ) version_info["MARO_DATA"]["deploy_status"] = "failed" diff --git a/maro/vector_env/vector_env.py b/maro/vector_env/vector_env.py index facc28759..9c8f4c144 100644 --- a/maro/vector_env/vector_env.py +++ b/maro/vector_env/vector_env.py @@ -27,7 +27,7 @@ class SnapshotListNodeWrapper: Args: env (VectorEnv): VectorEnv instance to send query command. - node_name (str): Name of node bind to this wrapper, used for furthur querying. + node_name (str): Name of node bind to this wrapper, used for further querying. """ def __init__(self, env, node_name: str): self.node_name = node_name diff --git a/notebooks/bike_repositioning/interact_with_environment.ipynb b/notebooks/bike_repositioning/interact_with_environment.ipynb index b8cabb777..2b9bdf34a 100644 --- a/notebooks/bike_repositioning/interact_with_environment.ipynb +++ b/notebooks/bike_repositioning/interact_with_environment.ipynb @@ -18,11 +18,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "12:03:56 | WARNING | \u001b[33mBinary data files for scenario: citi_bike topology: toy.3s_4t not found.\u001b[0m\n", - "12:03:56 | WARNING | \u001b[33mGenerating temp binary data file for scenario: citi_bike topology: toy.3s_4t pid: 129013. If you want to keep the data, please use MARO CLI command 'maro env data generate -s citi_bike -t toy.3s_4t' to generate the binary data files first.\u001b[0m\n", - "12:03:56 | INFO | \u001b[32mGenerating trip data for topology toy.3s_4t .\u001b[0m\n", - "12:03:57 | INFO | \u001b[32mBuilding binary data from ~/.maro/data/citi_bike/.source/.clean/toy.3s_4t/b37f36f2de334eb3/trips.csv to ~/.maro/data/citi_bike/.build/toy.3s_4t/b37f36f2de334eb3/trips.bin\u001b[0m\n", - "{'trip_requirements': 2156, 'bike_shortage': 1163, 'operation_number': 0}\n" + "{'trip_requirements': 2169, 'bike_shortage': 1199, 'operation_number': 0}\n" ] } ], @@ -73,7 +69,7 @@ "output_type": "stream", "text": [ "'The available scenarios in MARO:'\n", - "['cim', 'citi_bike']\n", + "['cim', 'citi_bike', 'vm_scheduling']\n", "\n", "'The predefined topologies in Citi Bike:'\n", "['ny.201801',\n", @@ -144,17 +140,28 @@ "name": "stdout", "output_type": "stream", "text": [ - "12:04:06 | WARNING | \u001b[33mBinary data files for scenario: citi_bike topology: toy.3s_4t not found.\u001b[0m\n", - "12:04:06 | WARNING | \u001b[33mGenerating temp binary data file for scenario: citi_bike topology: toy.3s_4t pid: 129013. If you want to keep the data, please use MARO CLI command 'maro env data generate -s citi_bike -t toy.3s_4t' to generate the binary data files first.\u001b[0m\n", - "12:04:06 | INFO | \u001b[32mGenerating trip data for topology toy.3s_4t .\u001b[0m\n", - "12:04:07 | INFO | \u001b[32mBuilding binary data from ~/.maro/data/citi_bike/.source/.clean/toy.3s_4t/0ad85988e84a43bd/trips.csv to ~/.maro/data/citi_bike/.build/toy.3s_4t/0ad85988e84a43bd/trips.bin\u001b[0m\n", "The current tick: 0.\n", "The current frame index: 0.\n", "There are 3 agents in this Env.\n", "There will be 48 snapshots in total.\n", "\n", "Env Summary:\n", - "{'node_detail': {'matrices': {'attributes': {'trips_adj': {'slots': 9,\n", + "{'event_payload': {'DeliverBike': ['from_station_idx',\n", + " 'to_station_idx',\n", + " 'number'],\n", + " 'RebalanceBike': ['station_idx',\n", + " 'tick',\n", + " 'frame_index',\n", + " 'type',\n", + " 'action_scope'],\n", + " 'RequireBike': ['timestamp',\n", + " 'durations',\n", + " 'src_station',\n", + " 'dest_station'],\n", + " 'ReturnBike': ['from_station_idx',\n", + " 'to_station_idx',\n", + " 'number']},\n", + " 'node_detail': {'matrices': {'attributes': {'trips_adj': {'slots': 9,\n", " 'type': 'i'}},\n", " 'number': 1},\n", " 'stations': {'attributes': {'bikes': {'slots': 1, 'type': 'i'},\n", @@ -275,19 +282,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "12:04:16 | WARNING | \u001b[33mBinary data files for scenario: citi_bike topology: toy.3s_4t not found.\u001b[0m\n", - "12:04:16 | WARNING | \u001b[33mGenerating temp binary data file for scenario: citi_bike topology: toy.3s_4t pid: 129013. If you want to keep the data, please use MARO CLI command 'maro env data generate -s citi_bike -t toy.3s_4t' to generate the binary data files first.\u001b[0m\n", - "12:04:16 | INFO | \u001b[32mGenerating trip data for topology toy.3s_4t .\u001b[0m\n", - "12:04:17 | INFO | \u001b[32mBuilding binary data from ~/.maro/data/citi_bike/.source/.clean/toy.3s_4t/56e2ca55ba6d4881/trips.csv to ~/.maro/data/citi_bike/.build/toy.3s_4t/56e2ca55ba6d4881/trips.bin\u001b[0m\n", "*************\n", - "DecisionEvent(tick=79, station_idx=1, type=DecisionType.Demand, action_scope={0: 0, 2: 0, 1: 30})\n", - "Action(from_station_idx=2, to_station_idx=1, number=0)\n", + "DecisionEvent {station_idx: 2, type: 'DecisionType.Demand', action_scope:{1: 0, 0: 0, 2: 30}}\n", + "Action {from_station_idx: 0, to_station_idx: '2', number:0}\n", "*************\n", - "DecisionEvent(tick=799, station_idx=2, type=DecisionType.Demand, action_scope={0: 0, 1: 0, 2: 30})\n", - "Action(from_station_idx=1, to_station_idx=2, number=0)\n", + "DecisionEvent {station_idx: 0, type: 'DecisionType.Demand', action_scope:{2: 1, 1: 1, 0: 30}}\n", + "Action {from_station_idx: 1, to_station_idx: '0', number:0}\n", "*************\n", - "DecisionEvent(tick=959, station_idx=2, type=DecisionType.Demand, action_scope={0: 1, 1: 1, 2: 30})\n", - "Action(from_station_idx=0, to_station_idx=2, number=0)\n" + "DecisionEvent {station_idx: 1, type: 'DecisionType.Demand', action_scope:{2: 0, 0: 0, 1: 29}}\n", + "Action {from_station_idx: 0, to_station_idx: '1', number:0}\n", + "*************\n", + "DecisionEvent {station_idx: 1, type: 'DecisionType.Demand', action_scope:{2: 0, 0: 0, 1: 29}}\n", + "Action {from_station_idx: 2, to_station_idx: '1', number:0}\n", + "*************\n", + "DecisionEvent {station_idx: 0, type: 'DecisionType.Demand', action_scope:{2: 1, 1: 0, 0: 30}}\n", + "Action {from_station_idx: 2, to_station_idx: '0', number:0}\n" ] } ], @@ -316,11 +325,16 @@ " target_idx_dock_tuple_list = [\n", " (k, v) for k, v in decision_event.action_scope.items() if k != decision_event.station_idx\n", " ]\n", - " # Randomly choose a target station weighted by the quantity of empty docks\n", - " target_idx, target_dock = random.choices(\n", - " target_idx_dock_tuple_list,\n", - " weights=[item[1] for item in target_idx_dock_tuple_list]\n", - " )[0]\n", + " weights=[item[1] for item in target_idx_dock_tuple_list]\n", + " if sum(weights) == 0:\n", + " target_idx = random.choices(target_idx_dock_tuple_list)[0][0]\n", + " target_dock = 0\n", + " else:\n", + " # Randomly choose a target station weighted by the quantity of empty docks\n", + " target_idx, target_dock = random.choices(\n", + " target_idx_dock_tuple_list,\n", + " weights=weights\n", + " )[0]\n", " # Generate the corresponding random Action\n", " action = Action(\n", " from_station_idx=decision_event.station_idx,\n", @@ -335,11 +349,16 @@ " target_idx_inventory_tuple_list = [\n", " (k, v) for k, v in decision_event.action_scope.items() if k != decision_event.station_idx\n", " ]\n", - " # Randomly choose a target station weighted by the bike inventory\n", - " target_idx, target_inventory = random.choices(\n", - " target_idx_inventory_tuple_list,\n", - " weights=[item[1] for item in target_idx_inventory_tuple_list]\n", - " )[0]\n", + " weights = [item[1] for item in target_idx_inventory_tuple_list]\n", + " if sum(weights) == 0:\n", + " target_idx = random.choices(target_idx_inventory_tuple_list)[0][0]\n", + " target_inventory = 0\n", + " else:\n", + " # Randomly choose a target station weighted by the bike inventory\n", + " target_idx, target_inventory = random.choices(\n", + " target_idx_inventory_tuple_list,\n", + " weights=weights\n", + " )[0]\n", " # Generate the corresponding random Action\n", " action = Action(\n", " from_station_idx=target_idx,\n", @@ -385,10 +404,6 @@ "name": "stdout", "output_type": "stream", "text": [ - "12:04:26 | WARNING | \u001b[33mBinary data files for scenario: citi_bike topology: toy.3s_4t not found.\u001b[0m\n", - "12:04:26 | WARNING | \u001b[33mGenerating temp binary data file for scenario: citi_bike topology: toy.3s_4t pid: 129013. If you want to keep the data, please use MARO CLI command 'maro env data generate -s citi_bike -t toy.3s_4t' to generate the binary data files first.\u001b[0m\n", - "12:04:26 | INFO | \u001b[32mGenerating trip data for topology toy.3s_4t .\u001b[0m\n", - "12:04:27 | INFO | \u001b[32mBuilding binary data from ~/.maro/data/citi_bike/.source/.clean/toy.3s_4t/278c7458875b4474/trips.csv to ~/.maro/data/citi_bike/.build/toy.3s_4t/278c7458875b4474/trips.bin\u001b[0m\n", "{'matrices': {'attributes': {...}, 'number': 1},\n", " 'stations': {'attributes': {...}, 'number': 3}}\n", "\n", @@ -427,18 +442,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "12:04:35 | WARNING | \u001b[33mBinary data files for scenario: citi_bike topology: toy.3s_4t not found.\u001b[0m\n", - "12:04:35 | WARNING | \u001b[33mGenerating temp binary data file for scenario: citi_bike topology: toy.3s_4t pid: 129013. If you want to keep the data, please use MARO CLI command 'maro env data generate -s citi_bike -t toy.3s_4t' to generate the binary data files first.\u001b[0m\n", - "12:04:35 | INFO | \u001b[32mGenerating trip data for topology toy.3s_4t .\u001b[0m\n", - "12:04:36 | INFO | \u001b[32mBuilding binary data from ~/.maro/data/citi_bike/.source/.clean/toy.3s_4t/80aa59d9c58d4b5e/trips.csv to ~/.maro/data/citi_bike/.build/toy.3s_4t/80aa59d9c58d4b5e/trips.bin\u001b[0m\n", - "array([11., 0., 11., 15., 0., 15., 16., 0., 16., 16., 0., 16.],\n", + "array([15., 0., 15., 16., 0., 16., 16., 0., 16., 18., 0., 18.],\n", " dtype=float32)\n" ] } @@ -496,7 +507,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.7.0" } }, "nbformat": 4, diff --git a/notebooks/container_inventory_management/interact_with_environment.ipynb b/notebooks/container_inventory_management/interact_with_environment.ipynb index 198f16db6..c9c354aa7 100644 --- a/notebooks/container_inventory_management/interact_with_environment.ipynb +++ b/notebooks/container_inventory_management/interact_with_environment.ipynb @@ -67,7 +67,7 @@ "output_type": "stream", "text": [ "'The available scenarios in MARO:'\n", - "['cim', 'citi_bike']\n", + "['cim', 'citi_bike', 'vm_scheduling']\n", "\n", "'The predefined topologies in CIM:'\n", "['global_trade.22p_l0.0',\n", @@ -147,7 +147,34 @@ "There will be 100 snapshots in total.\n", "\n", "Env Summary:\n", - "{'node_detail': {'matrices': {'attributes': {'full_on_ports': {'slots': 25,\n", + "{'event_payload': {'DISCHARGE_EMPTY': ['port_idx',\n", + " 'vessel_idx',\n", + " 'action_type',\n", + " 'quantity'],\n", + " 'DISCHARGE_FULL': ['vessel_idx',\n", + " 'port_idx',\n", + " 'from_port_idx',\n", + " 'quantity'],\n", + " 'LOAD_EMPTY': ['port_idx',\n", + " 'vessel_idx',\n", + " 'action_type',\n", + " 'quantity'],\n", + " 'LOAD_FULL': ['port_idx', 'vessel_idx'],\n", + " 'ORDER': ['tick',\n", + " 'src_port_idx',\n", + " 'dest_port_idx',\n", + " 'quantity'],\n", + " 'PENDING_DECISION': ['tick',\n", + " 'port_idx',\n", + " 'vessel_idx',\n", + " 'snapshot_list',\n", + " 'action_scope',\n", + " 'early_discharge'],\n", + " 'RETURN_EMPTY': ['port_idx', 'quantity'],\n", + " 'RETURN_FULL': ['src_port_idx', 'dest_port_idx', 'quantity'],\n", + " 'VESSEL_ARRIVAL': ['port_idx', 'vessel_idx'],\n", + " 'VESSEL_DEPARTURE': ['port_idx', 'vessel_idx']},\n", + " 'node_detail': {'matrices': {'attributes': {'full_on_ports': {'slots': 25,\n", " 'type': 'i'},\n", " 'full_on_vessels': {'slots': 30,\n", " 'type': 'i'},\n", @@ -184,8 +211,12 @@ " 'type': 'i'},\n", " 'future_stop_tick_list': {'slots': 3,\n", " 'type': 'i'},\n", + " 'is_parking': {'slots': 1,\n", + " 'type': 'i2'},\n", " 'last_loc_idx': {'slots': 1,\n", " 'type': 'i'},\n", + " 'loc_port_idx': {'slots': 1,\n", + " 'type': 'i'},\n", " 'next_loc_idx': {'slots': 1,\n", " 'type': 'i'},\n", " 'past_stop_list': {'slots': 4,\n", @@ -279,9 +310,8 @@ "- A valid `Action` instance, including:\n", " - **vessel_idx**: (int) The id of the vessel/operation object of the port/agent;\n", " - **port_idx**: (int) The id of the port/agent that take this action;\n", - " - **quantity**: (int) The sign of this value denotes different meanings:\n", - " - Positive quantity means unloading empty containers from vessel to port;\n", - " - Negative quantity means loading empty containers from port to vessel.\n", + " - **action_type**: (ActionType) Whether to load or discharge empty containers in this action;\n", + " - **quantity**: (int) The (non-negative) quantity of empty containers to be loaded/discharged.\n", "\n", "## Generate random actions based on the DecisionEvent\n", "\n", @@ -298,19 +328,32 @@ "output_type": "stream", "text": [ "*************\n", - "DecisionEvent(tick=14, port_idx=0, vessel_idx=5, action_scope=ActionScope(load=8856, discharge=1981))\n", - "Action(port_idx=0, vessel_idx=5, quantity=-2667)\n", + "DecisionEvent {port_idx: 4, vessel_idx: 5, action_scope: ActionScope {load: 6599, discharge: 0}, early_discharge: 0}\n", + "Action {action_type: 'ActionType.LOAD', port_idx: 4, vessel_idx: 5, quantity: 1960}\n", + "*************\n", + "DecisionEvent {port_idx: 1, vessel_idx: 3, action_scope: ActionScope {load: 0, discharge: 6027}, early_discharge: 0}\n", + "Action {action_type: 'ActionType.DISCHARGE', port_idx: 1, vessel_idx: 3, quantity: 3396}\n", + "*************\n", + "DecisionEvent {port_idx: 3, vessel_idx: 2, action_scope: ActionScope {load: 4730, discharge: 14383}, early_discharge: 0}\n", + "Action {action_type: 'ActionType.DISCHARGE', port_idx: 3, vessel_idx: 2, quantity: 10070}\n", "*************\n", - "DecisionEvent(tick=21, port_idx=2, vessel_idx=1, action_scope=ActionScope(load=15997, discharge=3061))\n", - "Action(port_idx=2, vessel_idx=1, quantity=-11608)\n" + "DecisionEvent {port_idx: 0, vessel_idx: 3, action_scope: ActionScope {load: 0, discharge: 2205}, early_discharge: 0}\n", + "Action {action_type: 'ActionType.LOAD', port_idx: 0, vessel_idx: 3, quantity: 0}\n", + "*************\n", + "DecisionEvent {port_idx: 4, vessel_idx: 1, action_scope: ActionScope {load: 0, discharge: 14495}, early_discharge: 0}\n", + "Action {action_type: 'ActionType.DISCHARGE', port_idx: 4, vessel_idx: 1, quantity: 8300}\n", + "*************\n", + "DecisionEvent {port_idx: 4, vessel_idx: 3, action_scope: ActionScope {load: 13824, discharge: 2205}, early_discharge: 0}\n", + "Action {action_type: 'ActionType.DISCHARGE', port_idx: 4, vessel_idx: 3, quantity: 841}\n" ] } ], "source": [ "from maro.simulator import Env\n", - "from maro.simulator.scenarios.cim.common import Action, DecisionEvent\n", + "from maro.simulator.scenarios.cim.common import Action, ActionType, DecisionEvent\n", "\n", "import random\n", + "from random import randint\n", "\n", "# Initialize an Env for cim scenario\n", "env = Env(scenario=\"cim\", topology=\"toy.5p_ssddd_l0.0\", start_tick=0, durations=100)\n", @@ -325,20 +368,20 @@ "\n", "while not is_done:\n", " # Generate a random Action according to the action_scope in DecisionEvent\n", - " random_quantity = random.randint(\n", - " -decision_event.action_scope.load,\n", - " decision_event.action_scope.discharge\n", - " )\n", + " action_scope = decision_event.action_scope\n", + " to_discharge = action_scope.discharge > 0 and randint(0, 1) > 0\n", + "\n", " action = Action(\n", - " vessel_idx=decision_event.vessel_idx,\n", - " port_idx=decision_event.port_idx,\n", - " quantity=random_quantity\n", + " decision_event.vessel_idx,\n", + " decision_event.port_idx,\n", + " randint(0, action_scope.discharge if to_discharge else action_scope.load),\n", + " ActionType.DISCHARGE if to_discharge else ActionType.LOAD\n", " )\n", - " \n", + "\n", " # Randomly sample some records to show in the output\n", " if random.random() > 0.95:\n", " print(f\"*************\\n{decision_event}\\n{action}\")\n", - " \n", + "\n", " # Respond the environment with the generated Action\n", " metrics, decision_event, is_done = env.step(action)" ] @@ -458,10 +501,13 @@ } ], "metadata": { + "interpreter": { + "hash": "767d51c1340bd893661ea55ea3124f6de3c7a262a8b4abca0554b478b1e2ff90" + }, "kernelspec": { - "display_name": "Python 3.7.7 64-bit", + "display_name": "Python 3", "language": "python", - "name": "python37764bita867a8fada044a15b631200655c7181c" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -473,7 +519,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.0" } }, "nbformat": 4, diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 41398fde4..8743f50a8 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -111,7 +111,7 @@ " plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge\n", " actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge)\n", " else:\n", - " actual_action, action_type = 0, None\n", + " actual_action, action_type = 0, ActionType.LOAD\n", "\n", " return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)]\n", "\n", diff --git a/playground.md b/playground.md index 6cf1cba2a..3cb93b860 100644 --- a/playground.md +++ b/playground.md @@ -1,18 +1,21 @@ # Playground -Playground provides a standalone MARO running environment, it also includes examples of some predefined scenarios and quick-start tutorial. -## Main service in playground +The playground provides a standalone MARO environment along with several predefined scenario examples and quick-start tutorials. + +## Main service in the playground + | Service | Description | Host | |-------------------|------------------------------------------------------------|----------------------------| | `Redis Commander` | Redis Web GUI. | http://127.0.0.1:40009 | -| `Read the Docs` | Local host docs. | http://127.0.0.1:40010 | -| `Jupyter Lab` | Jupyter lab with MARO environment, examples, notebooks. | http://127.0.0.1:40011 | -*(Remember change ports, if you used different ports mapping.)* +| `Jupyter Lab` | Jupyter lab with MARO environment, examples, notebooks. | http://127.0.0.1:40010 | -## Major materials in root folder +*(Remember to change ports if you use different ports mapping.)* + +## Major materials in the root folder | Folder | Description | |-------------------|--------------------------------------------| | `examples` | Showcases of predefined scenarios. | | `notebooks` | Quick-start tutorial. | -*(Others can be ignored, which aren't mentioned in the table.)* \ No newline at end of file + +*(The ones not mentioned in this table can be ignored.)* diff --git a/scripts/build_playground.bat b/scripts/build_playground.bat index 6d2605c8e..7468fce4c 100644 --- a/scripts/build_playground.bat +++ b/scripts/build_playground.bat @@ -1,8 +1,8 @@ - -rem script to build docker for playground image on Windows, this require the source code of maro - -chdir "%~dp0.." - -call .\scripts\compile_cython.bat - -docker build -f ./docker_files/cpu.play.df . -t maro/playground:cpu \ No newline at end of file + +rem script to build docker for playground image on Windows, this require the source code of maro + +chdir "%~dp0.." + +call .\scripts\compile_cython.bat + +docker build -f ./docker_files/cpu.playground.df . -t maro2020/playground diff --git a/scripts/build_playground.sh b/scripts/build_playground.sh index a8be86989..446c39cc4 100644 --- a/scripts/build_playground.sh +++ b/scripts/build_playground.sh @@ -10,4 +10,4 @@ fi bash ./scripts/compile_cython.sh -docker build -f ./docker_files/cpu.play.df . -t maro/playground:cpu \ No newline at end of file +docker build -f ./docker_files/cpu.playground.df . -t maro2020/playground diff --git a/scripts/import_cim_dumps.py b/scripts/import_cim_dumps.py index 0c4319862..15302d257 100644 --- a/scripts/import_cim_dumps.py +++ b/scripts/import_cim_dumps.py @@ -208,7 +208,7 @@ def import_attention(streamit, atts_path: str): parser = ArgumentParser() parser.add_argument("--name", required=True, - help="Experiment name show in databas") + help="Experiment name show in database") parser.add_argument("--scenario", required=True, help="Scenario name of import experiment") parser.add_argument("--topology", required=True, diff --git a/scripts/run_playground.sh b/scripts/run_playground.sh index 217545892..812435ce2 100644 --- a/scripts/run_playground.sh +++ b/scripts/run_playground.sh @@ -5,8 +5,5 @@ ./redis-6.0.6/src/redis-server --port 6379 & redis-commander --port 40009 & -# Python 3.6 -cd ./docs/_build/html; python -m http.server 40010 -b 0.0.0.0 & - # It's only for your play locally or in an internal network environment, so disable the token for convenience -cd ../../..; jupyter lab --port 40011 --allow-root --ip 0.0.0.0 --NotebookApp.token='' \ No newline at end of file +jupyter lab --port 40010 --allow-root --ip 0.0.0.0 --NotebookApp.token='' diff --git a/setup.py b/setup.py index 71b7aedf0..2797ae1dc 100644 --- a/setup.py +++ b/setup.py @@ -90,8 +90,8 @@ description="MARO Python Package", long_description=readme, long_description_content_type="text/x-rst", - author="Arthur Jiang", - author_email="shujia.jiang@microsoft.com", + author="MARO Team", + author_email="maro-team@microsoft.com", url="https://github.com/microsoft/maro", project_urls={ "Code": "https://github.com/microsoft/maro", @@ -119,8 +119,10 @@ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering :: Artificial Intelligence"], - python_requires=">=3.6,<3.8", + python_requires=">=3.6", setup_requires=[ "numpy<1.20.0", ], diff --git a/tests/cim/data_generator/test_data_collection_dump.py b/tests/cim/data_generator/test_data_collection_dump.py index 6e88ce552..b5d1f0dd8 100644 --- a/tests/cim/data_generator/test_data_collection_dump.py +++ b/tests/cim/data_generator/test_data_collection_dump.py @@ -5,12 +5,11 @@ import tempfile import unittest -import numpy as np - from maro.data_lib.cim.cim_data_dump import dump_from_config MAX_TICK = 20 + class TestDataCollectionDump(unittest.TestCase): def test_dump(self): output_folder = tempfile.mkdtemp() @@ -19,7 +18,9 @@ def test_dump(self): dump_from_config(config_path, output_folder, 20) # check output folder - for fname in ["ports.csv", "vessels.csv", "routes.csv", "global_order_proportion.txt", "order_proportion.csv", "misc.yml"]: + for fname in [ + "ports.csv", "vessels.csv", "routes.csv", "global_order_proportion.txt", "order_proportion.csv", "misc.yml" + ]: self.assertTrue(os.path.exists(os.path.join(output_folder, fname)), fname) # TODO: check content? @@ -37,5 +38,6 @@ def test_dump_with_invalid_parameters(self): with self.assertRaises(AssertionError): dump_from_config(config_path, None, 20) -if __name__=="__main__": + +if __name__ == "__main__": unittest.main() diff --git a/tests/cim/data_generator/test_data_collection_load.py b/tests/cim/data_generator/test_data_collection_load.py index c74a67fc6..b9d4366b7 100644 --- a/tests/cim/data_generator/test_data_collection_load.py +++ b/tests/cim/data_generator/test_data_collection_load.py @@ -4,25 +4,28 @@ import os import tempfile import unittest -from typing import Dict, List +from typing import List from maro.data_lib import BinaryConverter -from maro.data_lib.cim import dump_from_config, load_from_folder +from maro.data_lib.cim import load_from_folder from maro.data_lib.cim.cim_data_dump import CimDataDumpUtil -from maro.data_lib.cim.cim_data_generator import CimDataGenerator -from maro.data_lib.cim.entities import CimSyntheticDataCollection, NoisedItem, SyntheticPortSetting, RoutePoint, Stop, VesselSetting +from maro.data_lib.cim.cim_data_generator import gen_cim_data +from maro.data_lib.cim.entities import ( + CimSyntheticDataCollection, NoisedItem, RoutePoint, Stop, SyntheticPortSetting, VesselSetting +) MAX_TICK = 20 + class TestDumpsLoad(unittest.TestCase): def test_load_correct(self): config_path = os.path.join("tests", "data", "cim", "data_generator", "dumps", "config.yml") - stops_meta_path = os.path.join("tests", "data", "cim", "data_generator" ,"dumps", "cim.stops.meta.yml") + stops_meta_path = os.path.join("tests", "data", "cim", "data_generator", "dumps", "cim.stops.meta.yml") output_folder = tempfile.mkdtemp() # here we need to use CimDataDumpUtil manually to compare the result - dc: CimSyntheticDataCollection = CimDataGenerator().gen_data(config_path, 20) + dc: CimSyntheticDataCollection = gen_cim_data(config_path, 20) dumper = CimDataDumpUtil(dc) @@ -74,14 +77,16 @@ def _compare_routes(self, dc1: CimSyntheticDataCollection, dc2: CimSyntheticData self.assertTrue(p1.distance_to_next_port, p2.distance_to_next_port) def _compare_ports(self, dc1: CimSyntheticDataCollection, dc2: CimSyntheticDataCollection): - ports_1 = dc1.ports_settings - ports_2 = dc2.ports_settings + ports_1 = dc1.port_settings + ports_2 = dc2.port_settings self.assertTrue(len(ports_1) == len(ports_2)) for port_index in range(len(ports_1)): - port1: SyntheticPortSetting = ports_1[port_index] - port2: SyntheticPortSetting = ports_2[port_index] + port1 = ports_1[port_index] + port2 = ports_2[port_index] + assert isinstance(port1, SyntheticPortSetting) + assert isinstance(port2, SyntheticPortSetting) self.assertTrue(port1.index == port2.index, f"{port1.index}, {port2.index}") self.assertTrue(port1.name == port2.name, f"{port1.name}, {port2.name}") @@ -102,10 +107,9 @@ def _compare_ports(self, dc1: CimSyntheticDataCollection, dc2: CimSyntheticDataC self.assertTrue(tprop1.base == tprop2.base) self.assertTrue(tprop1.noise == tprop2.noise) - def _compare_vessels(self, dc1: CimSyntheticDataCollection, dc2: CimSyntheticDataCollection): - vessels_1: List[VesselSetting] = dc1.vessels_settings - vessels_2: List[VesselSetting] = dc2.vessels_settings + vessels_1: List[VesselSetting] = dc1.vessel_settings + vessels_2: List[VesselSetting] = dc2.vessel_settings self.assertTrue(len(vessels_1) == len(vessels_2)) @@ -123,10 +127,9 @@ def _compare_vessels(self, dc1: CimSyntheticDataCollection, dc2: CimSyntheticDat self.assertTrue(vessel1.parking_duration == vessel2.parking_duration) self.assertTrue(vessel1.parking_noise == vessel2.parking_noise) - def _compare_stops(self, dc1: CimSyntheticDataCollection, dc2: CimSyntheticDataCollection): - stops_1: List[List[Stop]] = dc1.vessels_stops - stops_2: List[List[Stop]] = dc2.vessels_stops + stops_1: List[List[Stop]] = dc1.vessel_stops + stops_2: List[List[Stop]] = dc2.vessel_stops self.assertTrue(len(stops_1) == len(stops_2)) @@ -147,5 +150,6 @@ def _compare_stops(self, dc1: CimSyntheticDataCollection, dc2: CimSyntheticDataC self.assertTrue(stop1.port_idx == stop2.port_idx) self.assertTrue(stop1.vessel_idx == stop2.vessel_idx) -if __name__=="__main__": + +if __name__ == "__main__": unittest.main() diff --git a/tests/cim/data_generator/test_data_generator.py b/tests/cim/data_generator/test_data_generator.py index 6fae46ec1..bf58ad890 100644 --- a/tests/cim/data_generator/test_data_generator.py +++ b/tests/cim/data_generator/test_data_generator.py @@ -4,20 +4,17 @@ import os import unittest -import yaml - -from maro.data_lib.cim.cim_data_generator import CimDataGenerator +from maro.data_lib.cim.cim_data_generator import gen_cim_data from maro.data_lib.cim.entities import CimSyntheticDataCollection MAX_TICK = 20 + class TestDataGenerator(unittest.TestCase): def test_data_generator_without_noise(self): config_path = os.path.join("tests", "data", "cim", "data_generator", "dumps", "config.yml") - ge = CimDataGenerator() - - dc: CimSyntheticDataCollection = ge.gen_data(config_path, max_tick=MAX_TICK) + dc: CimSyntheticDataCollection = gen_cim_data(config_path, max_tick=MAX_TICK) self.assertEqual(MAX_TICK, len(dc.order_proportion)) self.assertEqual(100000, dc.total_containers) @@ -26,26 +23,27 @@ def test_data_generator_without_noise(self): self.assertListEqual([0.02 * dc.total_containers] * MAX_TICK, list(dc.order_proportion)) # there should be 4 ports - self.assertEqual(4, len(dc.ports_settings)) + self.assertEqual(4, len(dc.port_settings)) # and 5 vessels - self.assertEqual(5, len(dc.vessels_settings)) + self.assertEqual(5, len(dc.vessel_settings)) # check vessel capacity - self.assertListEqual([92400, 92400, 187600, 187600, 187600], [v.capacity for v in dc.vessels_settings]) - self.assertListEqual([10] * 5, [v.sailing_speed for v in dc.vessels_settings]) - self.assertListEqual([0] * 5, [v.sailing_noise for v in dc.vessels_settings]) - self.assertListEqual([1] * 5, [v.parking_duration for v in dc.vessels_settings]) - self.assertListEqual([0] * 5, [v.parking_noise for v in dc.vessels_settings]) + self.assertListEqual([92400, 92400, 187600, 187600, 187600], [v.capacity for v in dc.vessel_settings]) + self.assertListEqual([10] * 5, [v.sailing_speed for v in dc.vessel_settings]) + self.assertListEqual([0] * 5, [v.sailing_noise for v in dc.vessel_settings]) + self.assertListEqual([1] * 5, [v.parking_duration for v in dc.vessel_settings]) + self.assertListEqual([0] * 5, [v.parking_noise for v in dc.vessel_settings]) # empty of vessel should be 0 by default from configuration - self.assertListEqual([0] * 5, [v.empty for v in dc.vessels_settings]) + self.assertListEqual([0] * 5, [v.empty for v in dc.vessel_settings]) # port - self.assertListEqual([100000,100000, 1000000, 100000], [p.capacity for p in dc.ports_settings]) - self.assertListEqual([0.25 * dc.total_containers] * 4, [p.empty for p in dc.ports_settings]) + self.assertListEqual([100000, 100000, 1000000, 100000], [p.capacity for p in dc.port_settings]) + self.assertListEqual([0.25 * dc.total_containers] * 4, [p.empty for p in dc.port_settings]) # TODO: more -if __name__=="__main__": + +if __name__ == "__main__": unittest.main() diff --git a/tests/cim/data_generator/test_order_global_proportion.py b/tests/cim/data_generator/test_order_global_proportion.py index 798e00fa6..e0837a327 100644 --- a/tests/cim/data_generator/test_order_global_proportion.py +++ b/tests/cim/data_generator/test_order_global_proportion.py @@ -4,78 +4,73 @@ import unittest from math import floor -from maro.data_lib.cim.global_order_proportion import GlobalOrderProportion +from maro.data_lib.cim.parsers import parse_global_order_proportion class TestOrderGlobalProportion(unittest.TestCase): def test_orders_without_noise(self): - total_cntrs = 100 + total_cnt = 100 max_tick = 50 period = 10 ratio = 0.02 - conf ={ + conf = { "period": period, "sample_nodes": [ (0, ratio), - (period-1, ratio) + (period - 1, ratio) ], "sample_noise": 0 } - op = GlobalOrderProportion() - - prop = op.parse(conf, total_container=total_cntrs, max_tick=max_tick) + prop = parse_global_order_proportion(conf, total_container=total_cnt, max_tick=max_tick) # check the order number - self.assertEqual(floor(total_cntrs * ratio) * max_tick, prop.sum()) + self.assertEqual(floor(total_cnt * ratio) * max_tick, prop.sum()) # check proportion, it should be a line - self.assertListEqual([floor(total_cntrs * ratio)] * max_tick, list(prop)) + self.assertListEqual([floor(total_cnt * ratio)] * max_tick, list(prop)) def test_orders_with_noise(self): - total_cntrs = 100 + total_cnt = 100 max_tick = 50 period = 10 ratio = 0.02 - conf ={ + conf = { "period": period, "sample_nodes": [ (0, ratio), - (period-1, ratio) + (period - 1, ratio) ], "sample_noise": 0.1 } - op = GlobalOrderProportion() - - prop = op.parse(conf, total_container=total_cntrs, max_tick=max_tick) + prop = parse_global_order_proportion(conf, total_container=total_cnt, max_tick=max_tick) self.assertTrue(prop.sum() > 0) def test_orders_with_start_tick(self): - total_cntrs = 100 + total_cnt = 100 start_tick = 10 max_tick = 50 period = 10 ratio = 0.02 - conf ={ + conf = { "period": period, "sample_nodes": [ (0, ratio), - (period-1, ratio) + (period - 1, ratio) ], "sample_noise": 0 } - op = GlobalOrderProportion() + prop = parse_global_order_proportion(conf, total_container=total_cnt, start_tick=start_tick, max_tick=max_tick) - prop = op.parse(conf, total_container=total_cntrs, start_tick=start_tick, max_tick=max_tick) + self.assertEqual(floor(total_cnt * ratio) * (max_tick - start_tick), prop.sum()) - self.assertEqual(floor(total_cntrs * ratio) * (max_tick - start_tick), prop.sum()) if __name__ == "__main__": unittest.main() diff --git a/tests/cim/data_generator/test_ports_parser.py b/tests/cim/data_generator/test_ports_parser.py index 04c75e33d..7d9c7d20e 100644 --- a/tests/cim/data_generator/test_ports_parser.py +++ b/tests/cim/data_generator/test_ports_parser.py @@ -5,7 +5,7 @@ from yaml import safe_load -from maro.data_lib.cim.port_parser import PortsParser +from maro.data_lib.cim.parsers import parse_ports default_conf = """ ports: @@ -41,6 +41,7 @@ proportion: 0.67 """ + class TestPortParser(unittest.TestCase): def test_port_parser(self): @@ -48,14 +49,12 @@ def test_port_parser(self): conf = safe_load(default_conf) - ppr = PortsParser() - - port_mapping, ports = ppr.parse(conf["ports"], total_cntr) + port_mapping, ports = parse_ports(conf["ports"], total_cntr) # port number should be same with config self.assertEqual(2, len(ports)) - # all empty sould be used + # all empty should be used self.assertEqual(total_cntr, sum([p.empty for p in ports])) # capacity should same with config @@ -71,5 +70,6 @@ def test_port_parser(self): # self.assertEqual(len(port_mapping), len(ports)) + if __name__ == "__main__": unittest.main() diff --git a/tests/cim/data_generator/test_route_parser.py b/tests/cim/data_generator/test_route_parser.py index 21c09e923..259282cd1 100644 --- a/tests/cim/data_generator/test_route_parser.py +++ b/tests/cim/data_generator/test_route_parser.py @@ -5,8 +5,7 @@ import yaml -from maro.data_lib.cim.entities import RoutePoint -from maro.data_lib.cim.route_parser import RoutesParser +from maro.data_lib.cim.parsers import parse_routes conf_str = """ routes: @@ -24,13 +23,12 @@ port_name: demand_port_002 """ + class TestRoutePoint(unittest.TestCase): def test_route_parser(self): conf = yaml.safe_load(conf_str) - parser = RoutesParser() - - route_mapping, routes = parser.parse(conf["routes"]) + route_mapping, routes = parse_routes(conf["routes"]) # there should be 2 routes self.assertEqual(2, len(route_mapping)) @@ -44,5 +42,5 @@ def test_route_parser(self): self.assertListEqual([60, 70, 80], [r.distance_to_next_port for r in routes[1]]) -if __name__=="__main__": +if __name__ == "__main__": unittest.main() diff --git a/tests/cim/data_generator/test_vessel_parser.py b/tests/cim/data_generator/test_vessel_parser.py index 0dc43f8b2..2e8d2eb29 100644 --- a/tests/cim/data_generator/test_vessel_parser.py +++ b/tests/cim/data_generator/test_vessel_parser.py @@ -5,8 +5,7 @@ import yaml -from maro.data_lib.cim.entities import VesselSetting -from maro.data_lib.cim.vessel_parser import VesselsParser +from maro.data_lib.cim.parsers import parse_vessels conf_str = """ vessels: @@ -34,13 +33,12 @@ speed: 10 """ + class TestVesselParser(unittest.TestCase): def test_vessel_parse(self): conf = yaml.safe_load(conf_str) - parser = VesselsParser() - - vessel_mapping, vessels = parser.parse(conf["vessels"]) + vessel_mapping, vessels = parse_vessels(conf["vessels"]) self.assertEqual(2, len(vessel_mapping)) self.assertEqual(2, len(vessels)) @@ -56,5 +54,6 @@ def test_vessel_parse(self): self.assertListEqual([10, 10], [v.sailing_speed for v in vessels]) self.assertListEqual([0, 0], [v.sailing_noise for v in vessels]) -if __name__=="__main__": + +if __name__ == "__main__": unittest.main() diff --git a/tests/cim/mock_data_container.py b/tests/cim/mock_data_container.py deleted file mode 100644 index 144a5aa51..000000000 --- a/tests/cim/mock_data_container.py +++ /dev/null @@ -1,276 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import csv -import os -from collections import defaultdict - -from maro.data_lib.cim.entities import Order, SyntheticPortSetting, Stop, VesselSetting - -PORT_NUM = 3 -VESSEL_NUM = 2 -PAST_STOP_NUM = 2 -FUTURE_STOP_NUM = 3 -TOTAL_CONTAINER_NUM = 100 -CONTAINER_VOLUME = 1 - - -class MockVesselStopsWrapper: - def __init__(self, stops): - self._stops = stops - - def __getitem__(self, key): - key_type = type(key) - - if key_type == int: - # get stops for vessel - vessel_idx = key - return self._stops[vessel_idx] - elif key_type == tuple: - vessel_idx = key[0] - loc_idx = key[1] - - return self._stops[vessel_idx][loc_idx] - - else: - ret = [] - - for i in range(VESSEL_NUM): - ret.append(self._stops[i]) - - return ret - -class MockEmptyReturnBufferWrapper: - def __getitem__(self, key): - return 1 - - -class MockFullReturnBufferWrapper: - def __getitem__(self, key): - return 0 - - -class MockVesselSailingPlanWrapper: - def __getitem__(self, key): - return [ - (0, 11), - (2, 13) - ] - - -class MockVeselPastStopWapper: - def __getitem__(self, key): - return [None, Stop(-1, 0, 2, 0, 0)] - - -class MockVesselFutureStopWrapper: - def __getitem__(self, key): - return [Stop(-1, 4, 6, 2, 0), Stop(-1, 10, 12, 3, 0), Stop(-1, 20, 22, 4, 0)] - - -class MockReachableStopsWrapper: - def __init__(self, stops, route_length): - self._stops = stops - self._route_length = route_length - - def __getitem__(self, key): - vessel_idx = key[0] - route_idx = key[1] - next_loc_idx = key[2] - - return [(stop.port_idx, stop.arrival_tick) for stop in - self._stops[vessel_idx][next_loc_idx + 1:next_loc_idx + 1 + self._route_length[route_idx]]] - -class MockDataContainer: - def __init__(self, case_folder: str): - self.route_dict = defaultdict(list) - self.order_dict = {} - self.ports_dict = {} - self.vessels_dict = defaultdict(float) - self._ports = [] - self._vessels = [] - - route_csv = os.path.join(case_folder, "route.csv") - - self._read_route(route_csv) - - order_csv = os.path.join(case_folder, "order.csv") - - self._read_order(order_csv) - - ports_csv = os.path.join(case_folder, "ports.csv") - - self._read_ports(ports_csv) - - vessel_csv = os.path.join(case_folder, "vessels.csv") - - self._read_vessels(vessel_csv) - - self.route_length = defaultdict(lambda: 3) - - self.port_name_to_id = {i: i for i in range(PORT_NUM)} - self.vessel_name_to_id = {i: i for i in range(VESSEL_NUM)} - - - - self._vessel_stops_wrapper = MockVesselStopsWrapper(self.route_dict) - self._reachable_stops_wrapper = MockReachableStopsWrapper(self.route_dict, self.route_length) - - @property - def past_stop_number(self) -> int: - return PAST_STOP_NUM - - @property - def future_stop_number(self) -> int: - return FUTURE_STOP_NUM - - @property - def ports(self): - return self._ports - - @property - def port_number(self) -> int: - return PORT_NUM - - @property - def vessels(self): - return self._vessels - - @property - def vessel_number(self) -> int: - return VESSEL_NUM - - @property - def container_volume(self): - return CONTAINER_VOLUME - - @property - def vessel_stops(self) : - return self._vessel_stops_wrapper - - @property - def empty_return_buffers(self): - return MockEmptyReturnBufferWrapper() - - @property - def full_return_buffers(self): - return MockFullReturnBufferWrapper() - - @property - def vessel_past_stops(self): - return MockVeselPastStopWapper() - - @property - def vessel_future_stops(self): - return MockVesselFutureStopWrapper() - - @property - def vessel_planned_stops(self): - return MockVesselSailingPlanWrapper() - - @property - def reachable_stops(self): - return self._reachable_stops_wrapper - - @property - def vessel_peroid(self): - pass - - @property - def route_mapping(self): - return {"r1": 0} - - @property - def vessel_mapping(self): - return {v.name: v.index for v in self._vessels} - - @property - def port_mapping(self): - return {p.name: p.index for p in self._ports} - - def get_orders(self, tick: int, total_empty_container: int): - result = [] - - if tick in self.order_dict: - - for src, _ in self.order_dict[tick].items(): - for dest, qty in self.order_dict[tick][src].items(): - result.append(Order(tick, src, dest, qty)) - - return result - - def _read_ports(self, path: str): - if os.path.exists(path): - with open(path, "r") as fp: - reader = csv.reader(fp) - - next(reader) - - for l in reader: - port_id = int(l[0]) - cap = float(l[1]) - cntr = int(l[2]) - - if port_id not in self.ports_dict: - self.ports_dict[port_id] = {} - - port = SyntheticPortSetting(port_id, f"p{port_id}", cap, cntr, None, None, None, None) - - self._ports.append(port) - - def _read_vessels(self, path: str): - if os.path.exists(path): - with open(path, "r") as fp: - reader = csv.reader(fp) - next(reader) - - for l in reader: - vessel_id = int(l[0]) - cap = float(l[1]) - cntr = int(l[2]) - - if vessel_id not in self.vessels_dict: - self.vessels_dict[vessel_id] = {} - - self.vessels_dict[vessel_id]['cap'] = cap - self.vessels_dict[vessel_id]['cntr'] = cntr - - vessel = VesselSetting(vessel_id, f"v{vessel_id}", cap, "r1", None, None, None, None, None, cntr) - - self._vessels.append(vessel) - - def _read_route(self, path: str): - with open(path, "r") as fp: - reader = csv.reader(fp) - - next(reader) # skip header - - for l in reader: - self.route_dict[int(l[0])].append( - Stop(-1, int(l[1]), int(l[2]), int(l[3]), int(l[0]))) - - def _read_order(self, path: str): - with open(path, "r") as fp: - reader = csv.reader(fp) - - next(reader) # skip header - - for l in reader: - if l == "": - continue - - tick = int(l[0]) - src = int(l[1]) - dest = int(l[2]) - qty = int(l[3]) - - if tick not in self.order_dict: - self.order_dict[tick] = {} - - if src not in self.order_dict[tick]: - self.order_dict[tick][src] = {} - - if dest not in self.order_dict[tick][src]: - self.order_dict[tick][src][dest] = {} - - self.order_dict[tick][src][dest] = qty diff --git a/tests/cim/test_cim_scenario.py b/tests/cim/test_cim_scenario.py index f696355b2..7c10da636 100644 --- a/tests/cim/test_cim_scenario.py +++ b/tests/cim/test_cim_scenario.py @@ -1,500 +1,387 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - import csv -import unittest import os +import pickle import tempfile - +import unittest from collections import namedtuple -from maro.event_buffer import EventBuffer, EventState -from maro.simulator.scenarios.cim.business_engine import CimBusinessEngine, Events -from maro.simulator.scenarios.cim.ports_order_export import PortOrderExporter -from tests.utils import next_step -from .mock_data_container import MockDataContainer - -from tests.utils import next_step, backends_to_test - -MAX_TICK = 20 - +from typing import List, Optional -def setup_case(case_name: str): - eb = EventBuffer() - case_folder = os.path.join("tests", "data", "cim", case_name) +import yaml - CimBusinessEngine.__init__ = mock_cim_init_func +from maro.simulator.utils import random - be = CimBusinessEngine(eb, case_folder, MAX_TICK) +os.environ["MARO_STREAMIT_ENABLED"] = "true" +os.environ["MARO_STREAMIT_EXPERIMENT_NAME"] = "cim_testing" - return eb, be - - -def mock_cim_init_func(self, event_buffer, topology_path, max_tick): - - self._start_tick = 0 - self._max_tick = max_tick - self._topology_path = topology_path - self._event_buffer = event_buffer - self._max_snapshots = None - self._snapshot_resolution = 1 - - self._data_cntr = MockDataContainer(topology_path) - - self._vessels = [] - self._ports = [] - self._frame = None - self._port_orders_exporter = PortOrderExporter(False) - self._init_frame() - - self._snapshots = self._frame.snapshots - - self._register_events() - - self._load_departure_events() +from maro.data_lib.cim import dump_from_config +from maro.data_lib.cim.entities import PortSetting, Stop, SyntheticPortSetting, VesselSetting +from maro.data_lib.cim.vessel_stop_wrapper import VesselStopsWrapper +from maro.simulator import Env +from maro.simulator.scenarios.cim.business_engine import CimBusinessEngine +from maro.simulator.scenarios.cim.common import Action, ActionType, DecisionEvent +from maro.simulator.scenarios.cim.ports_order_export import PortOrderExporter +from tests.utils import backends_to_test, compare_dictionary - self._init_vessel_plans() +TOPOLOGY_PATH_CONFIG = "tests/data/cim/case_data/config_folder" +TOPOLOGY_PATH_DUMP = "tests/data/cim/case_data/dump_folder" +TOPOLOGY_PATH_REAL_BIN = "tests/data/cim/case_data/real_folder_bin" +TOPOLOGY_PATH_REAL_CSV = "tests/data/cim/case_data/real_folder_csv" class TestCimScenarios(unittest.TestCase): - def setUp(self): - pass - - def test_init_state(self): + def __init__(self, *args, **kwargs): + super(TestCimScenarios, self).__init__(*args, **kwargs) + + with open(os.path.join(TOPOLOGY_PATH_CONFIG, "config.yml"), "r") as input_stream: + self._raw_topology = yaml.safe_load(input_stream) + + self._env: Optional[Env] = None + self._reload_topology: str = TOPOLOGY_PATH_CONFIG + self._business_engine: Optional[CimBusinessEngine] = None + + random.clear() + + def _init_env(self, backend_name: str) -> None: + os.environ["DEFAULT_BACKEND_NAME"] = backend_name + self._env = Env( + scenario="cim", + topology=self._reload_topology, + start_tick=0, + durations=200, + options={"enable-dump-snapshot": tempfile.gettempdir()} + ) + self._business_engine = self._env.business_engine + + def test_load_from_config(self) -> None: for backend_name in backends_to_test: - os.environ["DEFAULT_BACKEND_NAME"] = backend_name - - eb: EventBuffer = None - be: CimBusinessEngine = None - eb, be = setup_case("case_01") - - # check frame - self.assertEqual(3, len(be.frame.ports), "static node number should be same with port number after " - "initialization") - self.assertEqual(2, len(be.frame.vessels), "dynamic node number should be same with vessel number " - "after initialization") - - # check snapshot - self.assertEqual(0, len(be.snapshots), f"snapshots should be 0 after initialization") - - def test_vessel_moving_correct(self): + self._init_env(backend_name) + + ######################################################### + if len(self._business_engine.configs) > 0: # Env will not have `configs` if loaded from dump/real. + self.assertTrue(compare_dictionary(self._business_engine.configs, self._raw_topology)) + + self.assertEqual(len(getattr(self._business_engine.frame, "ports")), 22) + self.assertEqual(self._business_engine._data_cntr.port_number, 22) + self.assertEqual(len(getattr(self._business_engine.frame, "vessels")), 46) + self.assertEqual(self._business_engine._data_cntr.vessel_number, 46) + self.assertEqual(len(self._business_engine.snapshots), 0) + + ######################################################### + # Vessel + vessels: List[VesselSetting] = self._business_engine._data_cntr.vessels + for i, vessel in enumerate(vessels): + vessel_config = self._raw_topology["vessels"][vessel.name] + self.assertEqual(vessel.index, i) + self.assertEqual(vessel.capacity, vessel_config["capacity"]) + self.assertEqual(vessel.parking_duration, vessel_config["parking"]["duration"]) + self.assertEqual(vessel.parking_noise, vessel_config["parking"]["noise"]) + self.assertEqual(vessel.start_port_name, vessel_config["route"]["initial_port_name"]) + self.assertEqual(vessel.route_name, vessel_config["route"]["route_name"]) + self.assertEqual(vessel.sailing_noise, vessel_config["sailing"]["noise"]) + self.assertEqual(vessel.sailing_speed, vessel_config["sailing"]["speed"]) + + for name, idx in self._business_engine.get_node_mapping()["vessels"].items(): + self.assertEqual(vessels[idx].name, name) + + ######################################################### + # Port + ports: List[PortSetting] = self._business_engine._data_cntr.ports + port_names = [port.name for port in ports] + for i, port in enumerate(ports): + assert isinstance(port, SyntheticPortSetting) + port_config = self._raw_topology["ports"][port.name] + self.assertEqual(port.index, i) + self.assertEqual(port.capacity, port_config["capacity"]) + self.assertEqual(port.empty_return_buffer.noise, port_config["empty_return"]["noise"]) + self.assertEqual(port.full_return_buffer.noise, port_config["full_return"]["noise"]) + self.assertEqual(port.source_proportion.noise, port_config["order_distribution"]["source"]["noise"]) + for target in port.target_proportions: + self.assertEqual( + target.noise, + port_config["order_distribution"]["targets"][port_names[target.index]]["noise"] + ) + + for name, idx in self._business_engine.get_node_mapping()["ports"].items(): + self.assertEqual(ports[idx].name, name) + + def test_load_from_real(self) -> None: + for topology in [TOPOLOGY_PATH_REAL_BIN, TOPOLOGY_PATH_REAL_CSV]: + self._reload_topology = topology + for backend_name in backends_to_test: + self._init_env(backend_name) + + for i, port in enumerate(self._business_engine._ports): + self.assertEqual(port.booking, 0) + self.assertEqual(port.shortage, 0) + + hard_coded_truth = [556, 0, 20751], [1042, 0, 17320], [0, 0, 25000], [0, 0, 25000] + + self._env.step(action=None) + for i, port in enumerate(self._business_engine._ports): + self.assertEqual(port.booking, hard_coded_truth[i][0]) + self.assertEqual(port.shortage, hard_coded_truth[i][1]) + self.assertEqual(port.empty, hard_coded_truth[i][2]) + + self._env.reset(keep_seed=True) + self._env.step(action=None) + for i, port in enumerate(self._business_engine._ports): + self.assertEqual(port.booking, hard_coded_truth[i][0]) + self.assertEqual(port.shortage, hard_coded_truth[i][1]) + self.assertEqual(port.empty, hard_coded_truth[i][2]) + + self._reload_topology = TOPOLOGY_PATH_CONFIG + + def test_dump_and_load(self) -> None: + dump_from_config(os.path.join(TOPOLOGY_PATH_CONFIG, "config.yml"), TOPOLOGY_PATH_DUMP, 200) + + self._reload_topology = TOPOLOGY_PATH_DUMP + + # The reloaded Env should have same behaviors + self.test_load_from_config() + self.test_vessel_movement() + self.test_order_state() + self.test_order_export() + self.test_early_discharge() + + self._reload_topology = TOPOLOGY_PATH_CONFIG + + def test_vessel_movement(self) -> None: for backend_name in backends_to_test: - os.environ["DEFAULT_BACKEND_NAME"] = backend_name - eb, be = setup_case("case_01") - tick = 0 - - ##################################### - # STEP : beginning - v = be._vessels[0] - - self.assertEqual( - 0, v.next_loc_idx, "next_loc_idx of vessel 0 should be 0 at beginning") - self.assertEqual( - 0, v.last_loc_idx, "last_loc_idx of vessel 0 should be 0 at beginning") - - stop = be._data_cntr.vessel_stops[0, v.next_loc_idx] - - self.assertEqual(0, stop.port_idx, - "vessel 0 should parking at port 0 at beginning") - - v = be._vessels[1] - - self.assertEqual( - 0, v.next_loc_idx, "next_loc_idx of vessel 1 should be 0 at beginning") - self.assertEqual( - 0, v.last_loc_idx, "last_loc_idx of vessel 1 should be 0 at beginning") - - stop = be._data_cntr.vessel_stops[1, v.next_loc_idx] - - self.assertEqual(1, stop.port_idx, - "vessel 1 should parking at port 1 at beginning") - - ##################################### - # STEP : tick = 2 - for i in range(3): - next_step(eb, be, tick) - - tick += 1 - - v = be._vessels[0] - - # if these 2 idx not equal, then means at sailing state - self.assertEqual(1, v.next_loc_idx, - "next_loc_idx of vessel 0 should be 1 at tick 2") - self.assertEqual(0, v.last_loc_idx, - "last_loc_idx of vessel 0 should be 0 at tick 2") - - v = be._vessels[1] - - self.assertEqual(1, v.next_loc_idx, - "next_loc_idx of vessel 1 should be 1 at tick 2") - self.assertEqual(0, v.last_loc_idx, - "last_loc_idx of vessel 1 should be 0 at tick 2") - - v = be.snapshots["matrices"][2::"vessel_plans"].flatten() - - # since we already fixed the vessel plans, we just check the value - for i in range(2): - self.assertEqual(11, v[i*3+0]) - self.assertEqual(-1, v[i*3+1]) - self.assertEqual(13, v[i*3+2]) - - ##################################### - # STEP : tick = 8 - for i in range(6): - next_step(eb, be, tick) - - tick += 1 - - v = be._vessels[0] - - # vessel 0 parking - self.assertEqual(1, v.next_loc_idx, - "next_loc_idx of vessel 0 should be 1 at tick 8") - self.assertEqual(1, v.last_loc_idx, - "last_loc_idx of vessel 0 should be 1 at tick 8") - - stop = be._data_cntr.vessel_stops[0, v.next_loc_idx] - - self.assertEqual(1, stop.port_idx, - "vessel 0 should parking at port 1 at tick 8") - - v = be._vessels[1] - - # vessel 1 sailing - self.assertEqual(1, v.next_loc_idx, - "next_loc_idx of vessel 1 should be 1 at tick 8") - self.assertEqual(0, v.last_loc_idx, - "last_loc_idx of vessel 1 should be 0 at tick 8") - - ##################################### - # STEP : tick = 10 - for i in range(2): - next_step(eb, be, tick) - - tick += 1 - - v = be._vessels[0] - - # vessel 0 parking - self.assertEqual(1, v.next_loc_idx, - "next_loc_idx of vessel 0 should be 1 at tick 10") - self.assertEqual(1, v.last_loc_idx, - "last_loc_idx of vessel 0 should be 1 at tick 10") - - v = be._vessels[1] - - # vessel 1 parking - self.assertEqual(1, v.next_loc_idx, - "next_loc_idx of vessel 1 should be 1 at tick 10") - self.assertEqual(1, v.last_loc_idx, - "last_loc_idx of vessel 1 should be 1 at tick 10") - - ##################################### - # STEP : tick = 11 - for i in range(1): - next_step(eb, be, tick) - - tick += 1 - - v = be._vessels[0] - - # vessel 0 parking - self.assertEqual(2, v.next_loc_idx, - "next_loc_idx of vessel 0 should be 2 at tick 11") - self.assertEqual(1, v.last_loc_idx, - "last_loc_idx of vessel 0 should be 1 at tick 11") - - v = be._vessels[1] - - # vessel 1 parking - self.assertEqual(1, v.next_loc_idx, - "next_loc_idx of vessel 1 should be 1 at tick 11") - self.assertEqual(1, v.last_loc_idx, - "last_loc_idx of vessel 1 should be 1 at tick 11") - - # move the env to next step, so it will take snapshot for current tick 11 - next_step(eb, be, tick) - - # we have hard coded the future stops, here we just check if the value correct at each tick - for i in range(tick - 1): - # check if the future stop at tick 8 (vessel 0 arrive at port 1) - stop_list = be.snapshots["vessels"][i:0:[ - "past_stop_list", "past_stop_tick_list"]].flatten() - - self.assertEqual(-1, stop_list[0]) - self.assertEqual(-1, stop_list[2]) - - stop_list = be.snapshots["vessels"][i:0:[ - "future_stop_list", "future_stop_tick_list"]].flatten() - - self.assertEqual(2, stop_list[0]) - self.assertEqual(3, stop_list[1]) - self.assertEqual(4, stop_list[2]) - self.assertEqual(4, stop_list[3]) - self.assertEqual(10, stop_list[4]) - self.assertEqual(20, stop_list[5]) - - # check if statistics data correct - order_states = be.snapshots["ports"][i:0:[ - "shortage", "acc_shortage", "booking", "acc_booking"]].flatten() - - # all the value should be 0 for this case - self.assertEqual( - 0, order_states[0], f"shortage of port 0 should be 0 at tick {i}") - self.assertEqual( - 0, order_states[1], f"acc_shortage of port 0 should be 0 until tick {i}") - self.assertEqual( - 0, order_states[2], f"booking of port 0 should be 0 at tick {i}") - self.assertEqual( - 0, order_states[3], f"acc_booking of port 0 should be 0 until tick {i}") - - # check fulfillment - fulfill_states = be.snapshots["ports"][i:0:[ - "fulfillment", "acc_fulfillment"]].flatten() - - self.assertEqual( - 0, fulfill_states[0], f"fulfillment of port 0 should be 0 at tick {i}") - self.assertEqual( - 0, fulfill_states[1], f"acc_fulfillment of port 0 should be 0 until tick {i}") - - v = be.snapshots["matrices"][2:: "vessel_plans"].flatten() - - # since we already fixed the vessel plans, we just check the value - for i in range(2): - self.assertEqual(11, v[i*3+0]) - self.assertEqual(-1, v[i*3+1]) - self.assertEqual(13, v[i*3+2]) - - def test_order_state(self): + self._init_env(backend_name) + + hard_coded_period = [ + 67, 75, 84, 67, 53, 58, 51, 58, 61, 49, 164, 182, 146, 164, 182, 146, 90, 98, 79, 95, 104, 84, 87, 97, + 78, 154, 169, 136, 154, 169, 94, 105, 117, 94, 189, 210, 167, 189, 210, 167, 141, 158, 125, 141, 158, + 125 + ] + self.assertListEqual(self._business_engine._data_cntr.vessel_period, hard_coded_period) + + ports: List[PortSetting] = self._business_engine._data_cntr.ports + port_names: List[str] = [port.name for port in ports] + vessel_stops: VesselStopsWrapper = self._business_engine._data_cntr.vessel_stops + vessels: List[VesselSetting] = self._business_engine._data_cntr.vessels + + # Test invalid argument + self.assertIsNone(vessel_stops[None]) + + ######################################################### + for i, vessel in enumerate(vessels): + start_port_index = port_names.index(vessel.start_port_name) + self.assertEqual(vessel_stops[i, 0].port_idx, start_port_index) + + ######################################################### + for i, vessel in enumerate(vessels): + stop_port_indices = [stop.port_idx for stop in vessel_stops[i]] + + raw_route = self._raw_topology["routes"][vessel.route_name] + route_stop_names = [stop["port_name"] for stop in raw_route] + route_stop_indices = [port_names.index(name) for name in route_stop_names] + start_offset = route_stop_indices.index(port_names.index(vessel.start_port_name)) + + for j, stop_port_index in enumerate(stop_port_indices): + self.assertEqual(stop_port_index, route_stop_indices[(j + start_offset) % len(route_stop_indices)]) + + ######################################################### + # STEP: beginning + for i, vessel in enumerate(self._business_engine._vessels): + self.assertEqual(vessel.idx, i) + self.assertEqual(vessel.next_loc_idx, 0) + self.assertEqual(vessel.last_loc_idx, 0) + + ######################################################### + self._env.step(action=None) + self.assertEqual(self._env.tick, 5) # Vessel 35 will trigger the first arrival event at tick 5 + for i, vessel in enumerate(self._business_engine._vessels): + if i == 35: + self.assertEqual(vessel.next_loc_idx, 1) + self.assertEqual(vessel.last_loc_idx, 1) + else: + self.assertEqual(vessel.next_loc_idx, 1) + self.assertEqual(vessel.last_loc_idx, 0) + + ######################################################### + self._env.step(action=None) + self.assertEqual(self._env.tick, 6) # Vessel 27 will trigger the second arrival event at tick 6 + for i, vessel in enumerate(self._business_engine._vessels): + if i == 27: # Vessel 27 just arrives + self.assertEqual(vessel.next_loc_idx, 1) + self.assertEqual(vessel.last_loc_idx, 1) + elif i == 35: # Vessel 35 has already departed + self.assertEqual(vessel.next_loc_idx, 2) + self.assertEqual(vessel.last_loc_idx, 1) + else: + self.assertEqual(vessel.next_loc_idx, 1) + self.assertEqual(vessel.last_loc_idx, 0) + + ######################################################### + while self._env.tick < 100: + self._env.step(action=None) + self.assertEqual(self._env.tick, 100) + for i, vessel in enumerate(self._business_engine._vessels): + expected_next_loc_idx = expected_last_loc_idx = -1 + for j, stop in enumerate(vessel_stops[i]): + if stop.arrival_tick == self._env.tick: + expected_next_loc_idx = expected_last_loc_idx = j + break + if stop.arrival_tick > self._env.tick: + expected_next_loc_idx = j + expected_last_loc_idx = j - 1 + break + + self.assertEqual(vessel.next_loc_idx, expected_next_loc_idx) + self.assertEqual(vessel.last_loc_idx, expected_last_loc_idx) + + def test_order_state(self) -> None: for backend_name in backends_to_test: - os.environ["DEFAULT_BACKEND_NAME"] = backend_name - - eb, be = setup_case("case_02") - tick = 0 - - p = be._ports[0] - - self.assertEqual( - 0, p.booking, "port 0 have no booking at beginning") - self.assertEqual( - 0, p.shortage, "port 0 have no shortage at beginning") - self.assertEqual( - 100, p.empty, "port 0 have 100 empty containers at beginning") - - ##################################### - # STEP : tick = 0 - for i in range(1): - next_step(eb, be, tick) - tick += 1 - - # there should be 10 order generated at tick 0 - self.assertEqual( - 10, p.booking, "port 0 should have 10 bookings at tick 0") - self.assertEqual( - 0, p.shortage, "port 0 have no shortage at tick 0") - self.assertEqual( - 90, p.empty, "port 0 have 90 empty containers at tick 0") - - ##################################### - # STEP : tick = 1 - for i in range(1): - next_step(eb, be, tick) - tick += 1 - - # we have 0 booking, so no shortage - self.assertEqual( - 0, p.booking, "port 0 should have 0 bookings at tick 1") - self.assertEqual( - 0, p.shortage, "port 0 have no shortage at tick 1") - self.assertEqual( - 90, p.empty, "port 0 have 90 empty containers at tick 1") - - ##################################### - # STEP : tick = 3 - for i in range(2): - next_step(eb, be, tick) - tick += 1 - - # there is an order that take 40 containers - self.assertEqual( - 40, p.booking, "port 0 should have 40 booking at tick 3") - self.assertEqual( - 0, p.shortage, "port 0 have no shortage at tick 3") - self.assertEqual( - 50, p.empty, "port 0 have 90 empty containers at tick 3") - - ##################################### - # STEP : tick = 7 - for i in range(4): - next_step(eb, be, tick) - tick += 1 - - # there is an order that take 51 containers - self.assertEqual( - 51, p.booking, "port 0 should have 51 booking at tick 7") - self.assertEqual(1, p.shortage, "port 0 have 1 shortage at tick 7") - self.assertEqual( - 0, p.empty, "port 0 have 0 empty containers at tick 7") - - # push the simulator to next tick to update snapshot - next_step(eb, be, tick) - - # check if there is any container missing - total_cntr_number = sum([port.empty for port in be._ports]) + \ - sum([vessel.empty for vessel in be._vessels]) + \ - sum([port.full for port in be._ports]) + \ - sum([vessel.full for vessel in be._vessels]) - - # NOTE: we flatten here, as raw backend query result has 4dim shape - # check if statistics data correct - order_states = be.snapshots["ports"][7:0:[ - "shortage", "acc_shortage", "booking", "acc_booking"]].flatten() - - # all the value should be 0 for this case - self.assertEqual( - 1, order_states[0], f"shortage of port 0 should be 0 at tick {i}") - self.assertEqual( - 1, order_states[1], f"acc_shortage of port 0 should be 0 until tick {i}") - self.assertEqual( - 51, order_states[2], f"booking of port 0 should be 0 at tick {i}") - self.assertEqual( - 101, order_states[3], f"acc_booking of port 0 should be 0 until tick {i}") - - # check fulfillment - fulfill_states = be.snapshots["ports"][7:0:[ - "fulfillment", "acc_fulfillment"]].flatten() - - self.assertEqual( - 50, fulfill_states[0], f"fulfillment of port 0 should be 50 at tick {i}") - self.assertEqual( - 100, fulfill_states[1], f"acc_fulfillment of port 0 should be 100 until tick {i}") - - def test_order_load_discharge_state(self): + self._init_env(backend_name) + + for i, port in enumerate(self._business_engine._ports): + total_containers = self._raw_topology['total_containers'] + initial_container_proportion = self._raw_topology['ports'][port.name]['initial_container_proportion'] + + self.assertEqual(port.booking, 0) + self.assertEqual(port.shortage, 0) + self.assertEqual(port.empty, int(total_containers * initial_container_proportion)) + + ######################################################### + self._env.step(action=None) + self.assertEqual(self._env.tick, 5) + + hard_coded_truth = [ # Should get same results under default random seed + [223, 0, 14726], [16, 0, 916], [18, 0, 917], [89, 0, 5516], [84, 0, 4613], [72, 0, 4603], + [26, 0, 1374], [24, 0, 1378], [48, 0, 2756], [54, 0, 2760], [26, 0, 1379], [99, 0, 5534], + [137, 0, 7340], [19, 0, 912], [13, 0, 925], [107, 0, 6429], [136, 0, 9164], [64, 0, 3680], + [24, 0, 1377], [31, 0, 1840], [109, 0, 6454], [131, 0, 7351] + ] + for i, port in enumerate(self._business_engine._ports): + self.assertEqual(port.booking, hard_coded_truth[i][0]) + self.assertEqual(port.shortage, hard_coded_truth[i][1]) + self.assertEqual(port.empty, hard_coded_truth[i][2]) + + def test_keep_seed(self) -> None: for backend_name in backends_to_test: - os.environ["DEFAULT_BACKEND_NAME"] = backend_name - - eb, be = setup_case("case_03") - tick = 0 - - ##################################### - # STEP : tick = 5 - for i in range(6): - next_step(eb, be, tick) - tick += 1 - - # check if we have load all 50 full container - p = be._ports[0] - v = be._vessels[0] - - self.assertEqual(0, p.full, "port 0 should have no full at tick 5") - self.assertEqual( - 50, v.full, "all 50 full container should be loaded on vessel 0") - self.assertEqual( - 50, p.empty, "remaining empty should be 50 after order generated at tick 5") - self.assertEqual(0, p.shortage, "no shortage at tick 5 for port 0") - self.assertEqual(0, p.booking, "no booking at tick 5 for pot 0") - - ##################################### - # STEP : tick = 10 - for i in range(5): - next_step(eb, be, tick) - tick += 1 - - # at tick 10 vessel 0 arrive at port 1, it should discharge all the full containers - p1 = be._ports[1] - - self.assertEqual( - 0, v.full, "all 0 full container on vessel 0 after arrive at port 1 at tick 10") - self.assertEqual(50, p1.on_consignee, - "there should be 50 full containers pending to be empty at tick 10 after discharge") - self.assertEqual(0, p1.empty, "no empty for port 1 at tick 10") - self.assertEqual(0, p1.full, "no full for port 1 at tick 10") - - ##################################### - # STEP : tick = 12 - for i in range(2): - next_step(eb, be, tick) - tick += 1 - - # we hard coded the buffer time to 2, so - self.assertEqual(0, p1.on_consignee, - "all the full become empty at tick 12 for port 1") - self.assertEqual( - 50, p1.empty, "there will be 50 empty at tick 12 for port 1") - - def test_early_discharge(self): - for backend_name in backends_to_test: - os.environ["DEFAULT_BACKEND_NAME"] = backend_name - - eb, be = setup_case("case_04") - tick = 0 - - p0 = be._ports[0] - p1 = be._ports[1] - p2 = be._ports[2] - v = be._vessels[0] - - ##################################### - # STEP : tick = 10 - for i in range(11): - next_step(eb, be, tick) - tick += 1 - - # at tick 10, vessel 0 arrive port 2, it already loaded 50 full, it need to load 50 at port 2, so it will early dicharge 10 empty - self.assertEqual( - 0, v.empty, "vessel 0 should early discharge all the empty at tick 10") - self.assertEqual( - 100, v.full, "vessel 0 should have 100 full on-board at tick 10") - self.assertEqual( - 10, p2.empty, "port 2 have 10 more empty due to early discharge at tick 10") - self.assertEqual(0, p2.full, "no full at port 2 at tick 10") - - ##################################### - # STEP : tick = 18 - for i in range(8): - next_step(eb, be, tick) - tick += 1 - - # at tick 18, vessel 0 arrive at port 1, it will discharge all the full - self.assertEqual( - 0, v.empty, "vessel 0 should have no empty at tick 18") - self.assertEqual( - 0, v.full, "vessel 0 should discharge all full on-board at tick 18") - self.assertEqual( - 100, p1.on_consignee, "100 full pending to become empty at port 1 at tick 18") - self.assertEqual(0, p1.empty, "no empty for port 1 at tick 18") - - ##################################### - # STEP : tick = 20 - for i in range(2): - next_step(eb, be, tick) - tick += 1 - - self.assertEqual( - 100, p1.empty, "there should be 100 empty at tick 20 at port 1") - - def test_order_export(self): + self._init_env(backend_name) + + vessel_stops_1: List[List[Stop]] = self._business_engine._data_cntr.vessel_stops + self._env.step(action=None) + port_info_1 = [(port.booking, port.shortage, port.empty) for port in self._business_engine._ports] + + self._env.reset(keep_seed=True) + vessel_stops_2: List[List[Stop]] = self._business_engine._data_cntr.vessel_stops + self._env.step(action=None) + port_info_2 = [(port.booking, port.shortage, port.empty) for port in self._business_engine._ports] + + self._env.reset(keep_seed=False) + vessel_stops_3: List[List[Stop]] = self._business_engine._data_cntr.vessel_stops + self._env.step(action=None) + port_info_3 = [(port.booking, port.shortage, port.empty) for port in self._business_engine._ports] + + # Vessel + for i in range(self._business_engine._data_cntr.vessel_number): + # 1 and 2 should be totally equal + self.assertListEqual(vessel_stops_1[i], vessel_stops_2[i]) + + # 1 and 3 should have difference + flag = True + for stop1, stop3 in zip(vessel_stops_1[i], vessel_stops_3[i]): + self.assertListEqual( + [stop1.index, stop1.port_idx, stop1.vessel_idx], + [stop3.index, stop3.port_idx, stop3.vessel_idx] + ) + if (stop1.arrival_tick, stop1.leave_tick) != (stop3.arrival_tick, stop3.leave_tick): + flag = False + self.assertFalse(flag) + + # Port + self.assertListEqual(port_info_1, port_info_2) + self.assertFalse(all(port1 == port3 for port1, port3 in zip(port_info_1, port_info_3))) + + def test_order_export(self) -> None: """order.tick, order.src_port_idx, order.dest_port_idx, order.quantity""" Order = namedtuple("Order", ["tick", "src_port_idx", "dest_port_idx", "quantity"]) - exportor = PortOrderExporter(True) + # + for enabled in [False, True]: + exporter = PortOrderExporter(enabled) - for i in range(5): - exportor.add(Order(0, 0, 1, i + 1)) + for i in range(5): + exporter.add(Order(0, 0, 1, i + 1)) - out_folder = tempfile.gettempdir() + out_folder = tempfile.gettempdir() + if os.path.exists(f"{out_folder}/orders.csv"): + os.remove(f"{out_folder}/orders.csv") - exportor.dump(out_folder) + exporter.dump(out_folder) - with open(f"{out_folder}/orders.csv") as fp: - reader = csv.DictReader(fp) + if enabled: + with open(f"{out_folder}/orders.csv") as fp: + reader = csv.DictReader(fp) + row = 0 + for line in reader: + self.assertEqual(row + 1, int(line["quantity"])) + row += 1 + else: # Should done nothing + self.assertFalse(os.path.exists(f"{out_folder}/orders.csv")) - row = 0 - for line in reader: - self.assertEqual(row+1, int(line["quantity"])) + def test_early_discharge(self) -> None: + for backend_name in backends_to_test: + self._init_env(backend_name) + + metric, decision_event, is_done = self._env.step(None) + assert isinstance(decision_event, DecisionEvent) + + self.assertEqual(decision_event.action_scope.load, 1240) + self.assertEqual(decision_event.action_scope.discharge, 0) + self.assertEqual(decision_event.early_discharge, 0) + + decision_event = pickle.loads(pickle.dumps(decision_event)) # Test serialization + + load_action = Action( + vessel_idx=decision_event.vessel_idx, + port_idx=decision_event.port_idx, + quantity=1201, + action_type=ActionType.LOAD + ) + discharge_action = Action( + vessel_idx=decision_event.vessel_idx, + port_idx=decision_event.port_idx, + quantity=1, + action_type=ActionType.DISCHARGE + ) + metric, decision_event, is_done = self._env.step([load_action, discharge_action]) + + history = [] + while not is_done: + metric, decision_event, is_done = self._env.step(None) + assert decision_event is None or isinstance(decision_event, DecisionEvent) + if decision_event is not None and decision_event.vessel_idx == 35: + v = self._business_engine._vessels[35] + history.append((v.full, v.empty, v.early_discharge)) + + hard_coded_benchmark = [ + (465, 838, 362), (756, 547, 291), (1261, 42, 505), (1303, 0, 42), (1303, 0, 0), (1303, 0, 0), + (803, 0, 0) + ] + self.assertListEqual(history, hard_coded_benchmark) + + # + payload_detail_benchmark = { + 'ORDER': ['tick', 'src_port_idx', 'dest_port_idx', 'quantity'], + 'RETURN_FULL': ['src_port_idx', 'dest_port_idx', 'quantity'], + 'VESSEL_ARRIVAL': ['port_idx', 'vessel_idx'], + 'LOAD_FULL': ['port_idx', 'vessel_idx'], + 'DISCHARGE_FULL': ['vessel_idx', 'port_idx', 'from_port_idx', 'quantity'], + 'PENDING_DECISION': [ + 'tick', 'port_idx', 'vessel_idx', 'snapshot_list', 'action_scope', 'early_discharge'], + 'LOAD_EMPTY': ['port_idx', 'vessel_idx', 'action_type', 'quantity'], + 'DISCHARGE_EMPTY': ['port_idx', 'vessel_idx', 'action_type', 'quantity'], + 'VESSEL_DEPARTURE': ['port_idx', 'vessel_idx'], 'RETURN_EMPTY': ['port_idx', 'quantity'] + } + self.assertTrue( + compare_dictionary(self._business_engine.get_event_payload_detail(), payload_detail_benchmark)) + port_number = self._business_engine._data_cntr.port_number + self.assertListEqual(self._business_engine.get_agent_idx_list(), list(range(port_number))) - row += 1 if __name__ == "__main__": unittest.main() diff --git a/tests/data/cim/case_data/config_folder/config.yml b/tests/data/cim/case_data/config_folder/config.yml new file mode 100644 index 000000000..eb300f6f7 --- /dev/null +++ b/tests/data/cim/case_data/config_folder/config.yml @@ -0,0 +1,1671 @@ +seed: 4096 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 +container_usage_proportion: + period: 112 + sample_nodes: + - - 1 + - 0.015012045981107782 + - - 2 + - 0.015044493615969741 + - - 3 + - 0.015086758870970732 + - - 4 + - 0.01512277494153067 + - - 5 + - 0.01513318262505517 + - - 6 + - 0.01509807371913584 + - - 7 + - 0.015 + - - 8 + - 0.015173074266317171 + - - 9 + - 0.015425203006617148 + - - 10 + - 0.015750572812854108 + - - 11 + - 0.016132546658976815 + - - 12 + - 0.01654380252377108 + - - 13 + - 0.016947634709925108 + - - 14 + - 0.017300377961276522 + - - 15 + - 0.017554806245962416 + - - 16 + - 0.01766425880641032 + - - 17 + - 0.01758716473640939 + - - 18 + - 0.017291581573131123 + - - 19 + - 0.016759338850452894 + - - 20 + - 0.015989387422188653 + - - 21 + - 0.015000000000000001 + - - 22 + - 0.016170454760379226 + - - 23 + - 0.017464338416109185 + - - 24 + - 0.01880721970850913 + - - 25 + - 0.020111919797577836 + - - 26 + - 0.021283910705811104 + - - 27 + - 0.02222773080624969 + - - 28 + - 0.022853981633974483 + - - 29 + - 0.023086401027200568 + - - 30 + - 0.022868475176765664 + - - 31 + - 0.022169060410756062 + - - 32 + - 0.02098653519702863 + - - 33 + - 0.01935109142050977 + - - 34 + - 0.017324895896556256 + - - 35 + - 0.015 + - - 36 + - 0.017505963234746832 + - - 37 + - 0.02005609098616606 + - - 38 + - 0.022502173332406644 + - - 39 + - 0.024693815471924507 + - - 40 + - 0.02648812707616644 + - - 41 + - 0.027759325587487844 + - - 42 + - 0.028407585306672437 + - - 43 + - 0.028366497123525156 + - - 44 + - 0.027608583358805686 + - - 45 + - 0.026148433549357086 + - - 46 + - 0.024043182092683656 + - - 47 + - 0.02139022683000181 + - - 48 + - 0.018322276390618314 + - - 49 + - 0.015000000000000003 + - - 50 + - 0.01839727693779964 + - - 51 + - 0.021682247211563782 + - - 52 + - 0.024670979964007092 + - - 53 + - 0.027194221337363163 + - - 54 + - 0.02910789226660702 + - - 55 + - 0.030302085852342477 + - - 56 + - 0.030707963267948966 + - - 57 + - 0.03030208585234248 + - - 58 + - 0.029107892266607024 + - - 59 + - 0.027194221337363163 + - - 60 + - 0.0246709799640071 + - - 61 + - 0.021682247211563786 + - - 62 + - 0.018397276937799648 + - - 63 + - 0.015000000000000001 + - - 64 + - 0.01832227639061831 + - - 65 + - 0.021390226830001805 + - - 66 + - 0.024043182092683656 + - - 67 + - 0.026148433549357086 + - - 68 + - 0.02760858335880569 + - - 69 + - 0.02836649712352516 + - - 70 + - 0.028407585306672443 + - - 71 + - 0.027759325587487844 + - - 72 + - 0.026488127076166445 + - - 73 + - 0.024693815471924514 + - - 74 + - 0.022502173332406648 + - - 75 + - 0.020056090986166064 + - - 76 + - 0.017505963234746836 + - - 77 + - 0.015000000000000001 + - - 78 + - 0.017324895896556256 + - - 79 + - 0.019351091420509767 + - - 80 + - 0.020986535197028634 + - - 81 + - 0.022169060410756065 + - - 82 + - 0.02286847517676566 + - - 83 + - 0.02308640102720056 + - - 84 + - 0.022853981633974483 + - - 85 + - 0.022227730806249697 + - - 86 + - 0.021283910705811104 + - - 87 + - 0.020111919797577836 + - - 88 + - 0.018807219708509137 + - - 89 + - 0.017464338416109188 + - - 90 + - 0.01617045476037923 + - - 91 + - 0.015 + - - 92 + - 0.01598938742218865 + - - 93 + - 0.01675933885045289 + - - 94 + - 0.017291581573131126 + - - 95 + - 0.017587164736409394 + - - 96 + - 0.01766425880641032 + - - 97 + - 0.017554806245962416 + - - 98 + - 0.01730037796127653 + - - 99 + - 0.016947634709925108 + - - 100 + - 0.01654380252377108 + - - 101 + - 0.01613254665897681 + - - 102 + - 0.01575057281285411 + - - 103 + - 0.015425203006617148 + - - 104 + - 0.015173074266317171 + - - 105 + - 0.015 + - - 106 + - 0.01509807371913584 + - - 107 + - 0.01513318262505517 + - - 108 + - 0.01512277494153067 + - - 109 + - 0.015086758870970732 + - - 110 + - 0.015044493615969741 + sample_noise: 0.002 +container_volumes: +- 1 +order_generate_mode: fixed +ports: + bremerhaven_ger: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.16 + order_distribution: + source: + noise: 0.025134751494652507 + proportion: 0.16 + targets: + leHavre_fra: + noise: 0.025432733187591285 + proportion: 0.18755329471737509 + montreal_can: + noise: 0.014981815264884504 + proportion: 0.0756906048807678 + newYork_usa: + noise: 0.07860790872534353 + proportion: 0.47933778254893905 + pusan_kor: + noise: 0.002902145091237068 + proportion: 0.07667664751459422 + qingdao_chn: + noise: 0.0016703057083900894 + proportion: 0.0512157380304634 + shanghai_chn: + noise: 0.004329384963154748 + proportion: 0.04763588234031061 + singapore_sgp: + noise: 0.00014700838968303593 + proportion: 0.005882857005910926 + yantian_chn: + noise: 0.014277274600949947 + proportion: 0.07600719296163895 + durban_sau: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.01 + order_distribution: + source: + noise: 0.0003905851406890507 + proportion: 0.01 + targets: + qingdao_chn: + noise: 0.03386602700899302 + proportion: 0.2502879616953551 + shanghai_chn: + noise: 0.04099351916502508 + proportion: 0.2501170152675811 + singapore_sgp: + noise: 0.02983331512414685 + proportion: 0.24812321657873454 + yantian_chn: + noise: 0.0398838857119423 + proportion: 0.25147180645832923 + itagual_bra: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.01 + order_distribution: + source: + noise: 0.0017049012580563228 + proportion: 0.01 + targets: + qingdao_chn: + noise: 0.006704071585248814 + proportion: 0.19992517767067194 + santos_bra: + noise: 0.01065215665926791 + proportion: 0.20145578663243896 + shanghai_chn: + noise: 0.01889939427623029 + proportion: 0.19975353803822685 + singapore_sgp: + noise: 0.030425501405892584 + proportion: 0.19775168293424744 + yantian_chn: + noise: 0.0038049637706619867 + proportion: 0.20111381472441478 + leHavre_fra: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.06 + order_distribution: + source: + noise: 0.010023093144315015 + proportion: 0.06 + targets: + bremerhaven_ger: + noise: 0.046713884416130814 + proportion: 0.2613085269688243 + montreal_can: + noise: 0.007011738109808054 + proportion: 0.09474419644790176 + newYork_usa: + noise: 0.0075575968616494 + proportion: 0.20255309034522379 + pusan_kor: + noise: 0.0022204132834372364 + proportion: 0.09500755942526971 + qingdao_chn: + noise: 0.0025163323117031356 + proportion: 0.08820727646821312 + shanghai_chn: + noise: 0.012253843912920523 + proportion: 0.08725114792024982 + singapore_sgp: + noise: 0.014556815408633276 + proportion: 0.07609945087001035 + yantian_chn: + noise: 0.007509358525794673 + proportion: 0.09482875155430699 + losAngeles_usa: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.05 + order_distribution: + source: + noise: 0.007441531984981497 + proportion: 0.05 + targets: + oakland_usa: + noise: 0.04265188459544233 + proportion: 0.27616189497862537 + princeRupert_can: + noise: 0.00273897680516611 + proportion: 0.15754923155692402 + qingdao_chn: + noise: 0.01517255937598155 + proportion: 0.1623492157793647 + seattle_usa: + noise: 0.014299850500812094 + proportion: 0.2423747616801366 + shanghai_chn: + noise: 0.0051011672478115 + proportion: 0.16156489600494922 + manzanillo_mex: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.05 + order_distribution: + source: + noise: 0.005425915310841583 + proportion: 0.05 + targets: + manzanillo_mex: + noise: 0.0073296927183103605 + proportion: 0.1642819028757581 + pusan_kor: + noise: 0.018169915140256784 + proportion: 0.1269237319062987 + qingdao_chn: + noise: 0.02383787277319661 + proportion: 0.12072756120502863 + sanAntonio_par: + noise: 0.0025563544317216205 + proportion: 0.10518021746764428 + shanghai_chn: + noise: 0.02058384394808081 + proportion: 0.11985637174894495 + yantian_chn: + noise: 0.00989286132515691 + proportion: 0.12676080545008 + yokohama_jpn: + noise: 0.0007488162438248653 + proportion: 0.23626940934624543 + melbourne_aus: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.015 + order_distribution: + source: + noise: 0.0029979553756433923 + proportion: 0.015 + targets: + singapore_sgp: + noise: 0.0566906410775888 + proportion: 0.3293364398135728 + sydney_aus: + noise: 0.01865388192132147 + proportion: 0.3364188275147848 + yantian_chn: + noise: 0.014238633997660635 + proportion: 0.33424473267164256 + montreal_can: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.015 + order_distribution: + source: + noise: 0.0005621554647653919 + proportion: 0.015 + targets: + bremerhaven_ger: + noise: 0.08779643369212999 + proportion: 0.5172441914124826 + leHavre_fra: + noise: 0.07842477295179612 + proportion: 0.4827558085875176 + newYork_usa: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.03 + order_distribution: + source: + noise: 0.00016057717079772171 + proportion: 0.03 + targets: + bremerhaven_ger: + noise: 0.05981761122488196 + proportion: 0.5332513888590196 + leHavre_fra: + noise: 0.03990533991720246 + proportion: 0.46674861114098054 + oakland_usa: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.03 + order_distribution: + source: + noise: 0.004910915544061917 + proportion: 0.03 + targets: + losAngeles_usa: + noise: 0.048963812038470556 + proportion: 0.30175478642890274 + princeRupert_can: + noise: 0.0254961817963097 + proportion: 0.23086917432389525 + qingdao_chn: + noise: 0.03349874892050399 + proportion: 0.23393880851694335 + shanghai_chn: + noise: 0.012993114869682515 + proportion: 0.23343723073025868 + princeRupert_can: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.015 + order_distribution: + source: + noise: 0.0027667472862969076 + proportion: 0.015 + targets: + losAngeles_usa: + noise: 0.05091486367601389 + proportion: 0.2657898185062753 + oakland_usa: + noise: 0.028397329439457514 + proportion: 0.26818216862044597 + qingdao_chn: + noise: 0.0016646658939369785 + proportion: 0.23313476502804797 + shanghai_chn: + noise: 0.013742472852754001 + proportion: 0.23289324784523086 + pusan_kor: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.06 + order_distribution: + source: + noise: 0.009320427377742116 + proportion: 0.06 + targets: + bremerhaven_ger: + noise: 0.031991420231325084 + proportion: 0.21204226502906795 + leHavre_fra: + noise: 0.005105457599858164 + proportion: 0.08535909653471886 + manzanillo_mex: + noise: 0.005809147275802185 + proportion: 0.0959125358074426 + qingdao_chn: + noise: 0.0033945457967072133 + proportion: 0.051610240792657115 + sanAntonio_par: + noise: 0.001854192137165468 + proportion: 0.035795894860646466 + seattle_usa: + noise: 0.018330317944630015 + proportion: 0.14202580193983683 + shanghai_chn: + noise: 0.009459052846765289 + proportion: 0.05072408949335503 + singapore_sgp: + noise: 0.0016197423680226325 + proportion: 0.04038858042581788 + vancouver_can: + noise: 0.00469787131269142 + proportion: 0.059258114284493034 + yantian_chn: + noise: 0.003856257701007031 + proportion: 0.05774708854229363 + yokohama_jpn: + noise: 0.014803870326138143 + proportion: 0.1691362922896707 + qingdao_chn: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.08 + order_distribution: + source: + noise: 0.007181454765417904 + proportion: 0.08 + targets: + bremerhaven_ger: + noise: 0.02952708760272511 + proportion: 0.19166868266386958 + durban_sau: + noise: 0.0026860007722877676 + proportion: 0.025475188174797537 + itagual_bra: + noise: 0.0023058721270403397 + proportion: 0.020374390901285715 + leHavre_fra: + noise: 0.008262798336030506 + proportion: 0.04940051725539669 + losAngeles_usa: + noise: 0.014599621251693524 + proportion: 0.14605127409748705 + manzanillo_mex: + noise: 0.004578888771735588 + proportion: 0.061252276599797005 + oakland_usa: + noise: 0.0018968128853613635 + proportion: 0.15590868899027802 + princeRupert_can: + noise: 7.825075850514062e-05 + proportion: 0.005409393840032325 + pusan_kor: + noise: 0.0007151691535729856 + proportion: 0.018577689927305203 + santos_bra: + noise: 0.0036806154087330586 + proportion: 0.02037439104592543 + seattle_usa: + noise: 0.0016116464454415448 + proportion: 0.113038562331156 + shanghai_chn: + noise: 0.0011503058311188963 + proportion: 0.010504586083902057 + vancouver_can: + noise: 0.0031091344585237185 + proportion: 0.02008850220677704 + yantian_chn: + noise: 0.0017512950798533396 + proportion: 0.018391591436727996 + yokohama_jpn: + noise: 0.023837737047619264 + proportion: 0.14348426444526224 + sanAntonio_par: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.01 + order_distribution: + source: + noise: 0.0015229565945744795 + proportion: 0.01 + targets: + manzanillo_mex: + noise: 0.029864110340333205 + proportion: 0.1695702560614863 + pusan_kor: + noise: 0.030434021872288986 + proportion: 0.16240810942168046 + qingdao_chn: + noise: 0.031654173870326326 + proportion: 0.1612201993861513 + shanghai_chn: + noise: 0.022840905205567065 + proportion: 0.16105317774270345 + yantian_chn: + noise: 0.022058626605590343 + proportion: 0.16237686231768783 + yokohama_jpn: + noise: 0.020882193597463152 + proportion: 0.18337139507029068 + santos_bra: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.01 + order_distribution: + source: + noise: 0.0017591918390130468 + proportion: 0.01 + targets: + itagual_bra: + noise: 0.00015677478442405662 + proportion: 0.20145578737905992 + qingdao_chn: + noise: 0.007951708444894913 + proportion: 0.19992517738434895 + shanghai_chn: + noise: 0.008394529742993468 + proportion: 0.19975353831991421 + singapore_sgp: + noise: 0.01406059678653582 + proportion: 0.19775168256638195 + yantian_chn: + noise: 0.011310434190498734 + proportion: 0.20111381435029493 + seattle_usa: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.07 + order_distribution: + source: + noise: 0.009178972285261822 + proportion: 0.07 + targets: + losAngeles_usa: + noise: 0.023523817623575388 + proportion: 0.2912736754142012 + pusan_kor: + noise: 0.01876947062956955 + proportion: 0.12538821688519616 + qingdao_chn: + noise: 0.02029673306956859 + proportion: 0.11617748395503369 + shanghai_chn: + noise: 0.011329185762784782 + proportion: 0.11488243812354347 + singapore_sgp: + noise: 0.00024014816912197655 + proportion: 0.09977785851511455 + vancouver_can: + noise: 0.02057342705041732 + proportion: 0.1273542861551621 + yantian_chn: + noise: 0.020083358186626803 + proportion: 0.12514604095174878 + shanghai_chn: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.1 + order_distribution: + source: + noise: 0.017109626305467616 + proportion: 0.1 + targets: + bremerhaven_ger: + noise: 0.021959106592952006 + proportion: 0.21398371679064213 + durban_sau: + noise: 0.00010435314516980227 + proportion: 0.017462167174606275 + itagual_bra: + noise: 0.0020594969751376174 + proportion: 0.011430536063236027 + leHavre_fra: + noise: 0.0009873199353313093 + proportion: 0.04575353580273499 + losAngeles_usa: + noise: 0.014743978442771179 + proportion: 0.16004173887997492 + manzanillo_mex: + noise: 0.0012374290703743276 + proportion: 0.05976808154382374 + oakland_usa: + noise: 0.022426459611386496 + proportion: 0.1716979971992254 + pusan_kor: + noise: 0.0005070099495173756 + proportion: 0.00930596400725694 + qingdao_chn: + noise: 1.8783239577936313e-05 + proportion: 0.0009364036349727401 + santos_bra: + noise: 0.0012407696401487867 + proportion: 0.011430536346128936 + seattle_usa: + noise: 0.011299147787220907 + proportion: 0.12100465463531186 + vancouver_can: + noise: 0.002147321460772641 + proportion: 0.011092475826618629 + yantian_chn: + noise: 0.000883163479902347 + proportion: 0.009085896257062817 + yokohama_jpn: + noise: 0.012495339718107597 + proportion: 0.1570062958384047 + singapore_sgp: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.04 + order_distribution: + source: + noise: 0.0014198451145378962 + proportion: 0.04 + targets: + bremerhaven_ger: + noise: 0.02403294164090424 + proportion: 0.16163465526933787 + durban_sau: + noise: 0.007339857249890276 + proportion: 0.052947229587582224 + itagual_bra: + noise: 0.004826948332878951 + proportion: 0.049611409618781833 + leHavre_fra: + noise: 0.000697323478201817 + proportion: 0.06859394881607536 + melbourne_aus: + noise: 0.009748446374379698 + proportion: 0.054598902525719965 + pusan_kor: + noise: 0.0052864459821632135 + proportion: 0.04843640111672956 + qingdao_chn: + noise: 0.00020264478777542088 + proportion: 0.04380756610509279 + santos_bra: + noise: 0.0060662606703405855 + proportion: 0.0496114180206766 + seattle_usa: + noise: 0.002184489196293904 + proportion: 0.11021203091597927 + shanghai_chn: + noise: 0.00446890645579302 + proportion: 0.043156750350596414 + singapore_sgp: + noise: 0.0005079804300151535 + proportion: 0.03556597867096123 + sydney_aus: + noise: 0.0032814259751614335 + proportion: 0.053961624659387086 + vancouver_can: + noise: 0.0013725415556900408 + proportion: 0.04942443509903704 + yantian_chn: + noise: 0.007866049277209214 + proportion: 0.04831469461601993 + yokohama_jpn: + noise: 0.013995694253814077 + proportion: 0.13012295462802295 + sydney_aus: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.015 + order_distribution: + source: + noise: 5.318259543305715e-05 + proportion: 0.015 + targets: + melbourne_aus: + noise: 0.010959524006592607 + proportion: 0.33665441119009204 + singapore_sgp: + noise: 0.013378806626384264 + proportion: 0.3291642505650358 + yantian_chn: + noise: 0.043398566324619615 + proportion: 0.3341813382448723 + vancouver_can: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.02 + order_distribution: + source: + noise: 0.002260078974868654 + proportion: 0.02 + targets: + pusan_kor: + noise: 0.01022517907397126 + proportion: 0.16323476832180567 + qingdao_chn: + noise: 0.025111111836521687 + proportion: 0.16078294599848347 + seattle_usa: + noise: 0.025666860266381805 + proportion: 0.19595625809774195 + shanghai_chn: + noise: 0.00391575019491024 + proportion: 0.1604382196746799 + singapore_sgp: + noise: 0.009869415376296499 + proportion: 0.15641751563334233 + yantian_chn: + noise: 0.01646828787230074 + proportion: 0.1631702922739467 + yantian_chn: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.07 + order_distribution: + source: + noise: 0.005460106707641842 + proportion: 0.07 + targets: + bremerhaven_ger: + noise: 0.03183934815230198 + proportion: 0.23063506921338092 + durban_sau: + noise: 0.006955761417407662 + proportion: 0.036977112629701755 + itagual_bra: + noise: 0.003879986047538319 + proportion: 0.03103337062146673 + leHavre_fra: + noise: 0.0033746270986396783 + proportion: 0.06485623632186012 + manzanillo_mex: + noise: 0.014297914647911386 + proportion: 0.07866656356344162 + melbourne_aus: + noise: 0.0055998037357593185 + proportion: 0.03992004007199072 + pusan_kor: + noise: 0.004337884540903886 + proportion: 0.028939753926030695 + qingdao_chn: + noise: 0.0034845335834618927 + proportion: 0.020692152164438926 + santos_bra: + noise: 0.0023084677027446535 + proportion: 0.03103337095193521 + seattle_usa: + noise: 0.0043141199296154705 + proportion: 0.13901084519757625 + shanghai_chn: + noise: 0.0001996474876791818 + proportion: 0.01953252624072474 + singapore_sgp: + noise: 0.0011502086812016938 + proportion: 0.006007386215135132 + sydney_aus: + noise: 0.0019319056238978993 + proportion: 0.03878455068512264 + vancouver_can: + noise: 0.0007858696914021227 + proportion: 0.03070023678474206 + yantian_chn: + noise: 0.0016168667042640794 + proportion: 0.02872289950079146 + yokohama_jpn: + noise: 0.023246811699828965 + proportion: 0.17448788591166092 + yokohama_jpn: + capacity: 10000 + empty_return: + buffer_ticks: 1 + noise: 1 + full_return: + buffer_ticks: 1 + noise: 1 + initial_container_proportion: 0.08 + order_distribution: + source: + noise: 0.014254332625813097 + proportion: 0.08 + targets: + manzanillo_mex: + noise: 0.03244047063421112 + proportion: 0.224773870587767 + pusan_kor: + noise: 0.018126081484701975 + proportion: 0.1473122137604972 + qingdao_chn: + noise: 0.017152304680050526 + proportion: 0.1344645529933837 + sanAntonio_par: + noise: 0.0003086488347039955 + proportion: 0.10222734144856105 + shanghai_chn: + noise: 0.0056287128415065625 + proportion: 0.13265815413379464 + singapore_sgp: + noise: 0.019438990767313084 + proportion: 0.1115894561579222 + yantian_chn: + noise: 0.014838419143613479 + proportion: 0.14697441091807414 +routes: + a3s: + - distance_to_next_port: 320 + port_name: yantian_chn + - distance_to_next_port: 240 + port_name: sydney_aus + - distance_to_next_port: 80 + port_name: melbourne_aus + asa: + - distance_to_next_port: 320 + port_name: singapore_sgp + - distance_to_next_port: 260 + port_name: sydney_aus + - distance_to_next_port: 60 + port_name: melbourne_aus + ate1: + - distance_to_next_port: 160 + port_name: newYork_usa + - distance_to_next_port: 240 + port_name: bremerhaven_ger + - distance_to_next_port: 40 + port_name: leHavre_fra + gex1: + - distance_to_next_port: 220 + port_name: montreal_can + - distance_to_next_port: 220 + port_name: bremerhaven_ger + - distance_to_next_port: 40 + port_name: leHavre_fra + ktx5: + - distance_to_next_port: 240 + port_name: singapore_sgp + - distance_to_next_port: 80 + port_name: yantian_chn + - distance_to_next_port: 140 + port_name: yokohama_jpn + ll4: + - distance_to_next_port: 80 + port_name: shanghai_chn + - distance_to_next_port: 60 + port_name: yantian_chn + - distance_to_next_port: 80 + port_name: singapore_sgp + - distance_to_next_port: 380 + port_name: leHavre_fra + - distance_to_next_port: 80 + port_name: bremerhaven_ger + - distance_to_next_port: 420 + port_name: singapore_sgp + - distance_to_next_port: 200 + port_name: qingdao_chn + - distance_to_next_port: 80 + port_name: pusan_kor + pcc1: + - distance_to_next_port: 240 + port_name: qingdao_chn + - distance_to_next_port: 40 + port_name: shanghai_chn + - distance_to_next_port: 240 + port_name: princeRupert_can + - distance_to_next_port: 120 + port_name: losAngeles_usa + - distance_to_next_port: 100 + port_name: oakland_usa + pcc2: + - distance_to_next_port: 80 + port_name: shanghai_chn + - distance_to_next_port: 280 + port_name: losAngeles_usa + - distance_to_next_port: 160 + port_name: seattle_usa + - distance_to_next_port: 280 + port_name: qingdao_chn + pnw1: + - distance_to_next_port: 80 + port_name: yantian_chn + - distance_to_next_port: 260 + port_name: vancouver_can + - distance_to_next_port: 80 + port_name: seattle_usa + - distance_to_next_port: 320 + port_name: pusan_kor + pnw2: + - distance_to_next_port: 200 + port_name: singapore_sgp + - distance_to_next_port: 120 + port_name: yantian_chn + - distance_to_next_port: 80 + port_name: shanghai_chn + - distance_to_next_port: 60 + port_name: pusan_kor + - distance_to_next_port: 200 + port_name: seattle_usa + - distance_to_next_port: 40 + port_name: vancouver_can + - distance_to_next_port: 520 + port_name: qingdao_chn + - distance_to_next_port: 60 + port_name: shanghai_chn + saf3: + - distance_to_next_port: 80 + port_name: qingdao_chn + - distance_to_next_port: 40 + port_name: shanghai_chn + - distance_to_next_port: 60 + port_name: yantian_chn + - distance_to_next_port: 80 + port_name: singapore_sgp + - distance_to_next_port: 260 + port_name: durban_sau + - distance_to_next_port: 360 + port_name: yantian_chn + tla2: + - distance_to_next_port: 60 + port_name: qingdao_chn + - distance_to_next_port: 60 + port_name: shanghai_chn + - distance_to_next_port: 80 + port_name: yantian_chn + - distance_to_next_port: 100 + port_name: singapore_sgp + - distance_to_next_port: 460 + port_name: itagual_bra + - distance_to_next_port: 20 + port_name: santos_bra + - distance_to_next_port: 580 + port_name: singapore_sgp + - distance_to_next_port: 140 + port_name: yantian_chn + - distance_to_next_port: 80 + port_name: shanghai_chn + tlp1: + - distance_to_next_port: 100 + port_name: yantian_chn + - distance_to_next_port: 60 + port_name: shanghai_chn + - distance_to_next_port: 60 + port_name: qingdao_chn + - distance_to_next_port: 40 + port_name: pusan_kor + - distance_to_next_port: 260 + port_name: manzanillo_mex + - distance_to_next_port: 140 + port_name: sanAntonio_par + - distance_to_next_port: 180 + port_name: manzanillo_mex + - distance_to_next_port: 260 + port_name: yokohama_jpn + - distance_to_next_port: 60 + port_name: shanghai_chn +stop_number: +- 4 +- 3 +total_containers: 100000 +vessels: + a3s_vessel_000: + capacity: 871 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yantian_chn + route_name: a3s + sailing: + noise: 2 + speed: 10 + a3s_vessel_001: + capacity: 967 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: sydney_aus + route_name: a3s + sailing: + noise: 2 + speed: 9 + asa_vessel_000: + capacity: 601 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: singapore_sgp + route_name: asa + sailing: + noise: 1 + speed: 8 + asa_vessel_001: + capacity: 493 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: melbourne_aus + route_name: asa + sailing: + noise: 2 + speed: 10 + ate1_vessel_000: + capacity: 5758 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: newYork_usa + route_name: ate1 + sailing: + noise: 1 + speed: 9 + ate1_vessel_001: + capacity: 6333 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: bremerhaven_ger + route_name: ate1 + sailing: + noise: 1 + speed: 8 + gex1_vessel_000: + capacity: 1033 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: bremerhaven_ger + route_name: gex1 + sailing: + noise: 2 + speed: 10 + gex1_vessel_001: + capacity: 1147 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: montreal_can + route_name: gex1 + sailing: + noise: 1 + speed: 9 + ktx5_vessel_000: + capacity: 1557 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: singapore_sgp + route_name: ktx5 + sailing: + noise: 1 + speed: 8 + ktx5_vessel_001: + capacity: 1275 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yokohama_jpn + route_name: ktx5 + sailing: + noise: 1 + speed: 10 + ll4_vessel_000: + capacity: 6603 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: bremerhaven_ger + route_name: ll4 + sailing: + noise: 2 + speed: 9 + ll4_vessel_001: + capacity: 7263 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yantian_chn + route_name: ll4 + sailing: + noise: 2 + speed: 8 + ll4_vessel_002: + capacity: 5943 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: singapore_sgp + route_name: ll4 + sailing: + noise: 1 + speed: 10 + ll4_vessel_003: + capacity: 6603 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: leHavre_fra + route_name: ll4 + sailing: + noise: 1 + speed: 9 + ll4_vessel_004: + capacity: 7263 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: pusan_kor + route_name: ll4 + sailing: + noise: 1 + speed: 8 + ll4_vessel_005: + capacity: 5943 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: qingdao_chn + route_name: ll4 + sailing: + noise: 1 + speed: 10 + pcc1_vessel_000: + capacity: 4922 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: oakland_usa + route_name: pcc1 + sailing: + noise: 1 + speed: 9 + pcc1_vessel_001: + capacity: 5414 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: qingdao_chn + route_name: pcc1 + sailing: + noise: 2 + speed: 8 + pcc1_vessel_002: + capacity: 4430 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: princeRupert_can + route_name: pcc1 + sailing: + noise: 1 + speed: 10 + pcc2_vessel_000: + capacity: 2443 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: seattle_usa + route_name: pcc2 + sailing: + noise: 2 + speed: 9 + pcc2_vessel_001: + capacity: 2687 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: losAngeles_usa + route_name: pcc2 + sailing: + noise: 2 + speed: 8 + pcc2_vessel_002: + capacity: 2199 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: shanghai_chn + route_name: pcc2 + sailing: + noise: 2 + speed: 10 + pnw1_vessel_000: + capacity: 2129 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: seattle_usa + route_name: pnw1 + sailing: + noise: 1 + speed: 9 + pnw1_vessel_001: + capacity: 2341 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: vancouver_can + route_name: pnw1 + sailing: + noise: 1 + speed: 8 + pnw1_vessel_002: + capacity: 1917 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yantian_chn + route_name: pnw1 + sailing: + noise: 1 + speed: 10 + pnw2_vessel_000: + capacity: 897 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: pusan_kor + route_name: pnw2 + sailing: + noise: 2 + speed: 9 + pnw2_vessel_001: + capacity: 986 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yantian_chn + route_name: pnw2 + sailing: + noise: 2 + speed: 8 + pnw2_vessel_002: + capacity: 808 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: vancouver_can + route_name: pnw2 + sailing: + noise: 1 + speed: 10 + pnw2_vessel_003: + capacity: 897 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: pusan_kor + route_name: pnw2 + sailing: + noise: 1 + speed: 9 + pnw2_vessel_004: + capacity: 986 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: qingdao_chn + route_name: pnw2 + sailing: + noise: 1 + speed: 8 + saf3_vessel_000: + capacity: 583 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yantian_chn + route_name: saf3 + sailing: + noise: 1 + speed: 10 + saf3_vessel_001: + capacity: 647 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: qingdao_chn + route_name: saf3 + sailing: + noise: 1 + speed: 9 + saf3_vessel_002: + capacity: 711 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: singapore_sgp + route_name: saf3 + sailing: + noise: 2 + speed: 8 + saf3_vessel_003: + capacity: 583 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: durban_sau + route_name: saf3 + sailing: + noise: 2 + speed: 10 + tla2_vessel_000: + capacity: 1185 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: itagual_bra + route_name: tla2 + sailing: + noise: 1 + speed: 9 + tla2_vessel_001: + capacity: 1303 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: santos_bra + route_name: tla2 + sailing: + noise: 1 + speed: 8 + tla2_vessel_002: + capacity: 1067 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: singapore_sgp + route_name: tla2 + sailing: + noise: 2 + speed: 10 + tla2_vessel_003: + capacity: 1185 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: shanghai_chn + route_name: tla2 + sailing: + noise: 1 + speed: 9 + tla2_vessel_004: + capacity: 1303 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: qingdao_chn + route_name: tla2 + sailing: + noise: 2 + speed: 8 + tla2_vessel_005: + capacity: 1067 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yantian_chn + route_name: tla2 + sailing: + noise: 1 + speed: 10 + tlp1_vessel_000: + capacity: 6332 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yantian_chn + route_name: tlp1 + sailing: + noise: 2 + speed: 9 + tlp1_vessel_001: + capacity: 6965 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: manzanillo_mex + route_name: tlp1 + sailing: + noise: 1 + speed: 8 + tlp1_vessel_002: + capacity: 5699 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: pusan_kor + route_name: tlp1 + sailing: + noise: 1 + speed: 10 + tlp1_vessel_003: + capacity: 6332 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: manzanillo_mex + route_name: tlp1 + sailing: + noise: 2 + speed: 9 + tlp1_vessel_004: + capacity: 6965 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: sanAntonio_par + route_name: tlp1 + sailing: + noise: 1 + speed: 8 + tlp1_vessel_005: + capacity: 5699 + parking: + duration: 1 + noise: 1 + route: + initial_port_name: yokohama_jpn + route_name: tlp1 + sailing: + noise: 2 + speed: 10 diff --git a/tests/data/cim/case_data/dump_folder/global_order_proportion.txt b/tests/data/cim/case_data/dump_folder/global_order_proportion.txt new file mode 100644 index 000000000..a97fde48d --- /dev/null +++ b/tests/data/cim/case_data/dump_folder/global_order_proportion.txt @@ -0,0 +1,200 @@ +0.000000000000000000e+00 +1.567000000000000000e+03 +1.629000000000000000e+03 +1.698000000000000000e+03 +1.612000000000000000e+03 +1.550000000000000000e+03 +1.477000000000000000e+03 +1.641000000000000000e+03 +1.353000000000000000e+03 +1.479000000000000000e+03 +1.743000000000000000e+03 +1.754000000000000000e+03 +1.473000000000000000e+03 +1.609000000000000000e+03 +1.819000000000000000e+03 +1.815000000000000000e+03 +1.760000000000000000e+03 +1.925000000000000000e+03 +1.533000000000000000e+03 +1.665000000000000000e+03 +1.434000000000000000e+03 +1.658000000000000000e+03 +1.423000000000000000e+03 +1.707000000000000000e+03 +1.943000000000000000e+03 +2.151000000000000000e+03 +2.262000000000000000e+03 +2.282000000000000000e+03 +2.356000000000000000e+03 +2.256000000000000000e+03 +2.366000000000000000e+03 +2.168000000000000000e+03 +2.207000000000000000e+03 +1.891000000000000000e+03 +1.799000000000000000e+03 +1.682000000000000000e+03 +1.847000000000000000e+03 +2.130000000000000000e+03 +2.296000000000000000e+03 +2.309000000000000000e+03 +2.569000000000000000e+03 +2.930000000000000000e+03 +2.640000000000000000e+03 +2.708000000000000000e+03 +2.631000000000000000e+03 +2.532000000000000000e+03 +2.380000000000000000e+03 +2.231000000000000000e+03 +1.979000000000000000e+03 +1.381000000000000000e+03 +1.829000000000000000e+03 +1.987000000000000000e+03 +2.382000000000000000e+03 +2.837000000000000000e+03 +3.090000000000000000e+03 +2.943000000000000000e+03 +2.974000000000000000e+03 +3.070000000000000000e+03 +2.839000000000000000e+03 +2.647000000000000000e+03 +2.580000000000000000e+03 +1.984000000000000000e+03 +1.829000000000000000e+03 +1.476000000000000000e+03 +1.955000000000000000e+03 +2.228000000000000000e+03 +2.319000000000000000e+03 +2.740000000000000000e+03 +2.715000000000000000e+03 +2.736000000000000000e+03 +3.014000000000000000e+03 +2.945000000000000000e+03 +2.804000000000000000e+03 +2.400000000000000000e+03 +2.446000000000000000e+03 +1.845000000000000000e+03 +1.943000000000000000e+03 +1.302000000000000000e+03 +1.695000000000000000e+03 +1.833000000000000000e+03 +1.966000000000000000e+03 +2.118000000000000000e+03 +2.110000000000000000e+03 +2.437000000000000000e+03 +2.143000000000000000e+03 +2.087000000000000000e+03 +2.120000000000000000e+03 +1.967000000000000000e+03 +1.802000000000000000e+03 +1.599000000000000000e+03 +1.512000000000000000e+03 +1.456000000000000000e+03 +1.748000000000000000e+03 +1.702000000000000000e+03 +1.619000000000000000e+03 +1.882000000000000000e+03 +1.730000000000000000e+03 +1.629000000000000000e+03 +1.814000000000000000e+03 +1.655000000000000000e+03 +1.683000000000000000e+03 +1.560000000000000000e+03 +1.499000000000000000e+03 +1.728000000000000000e+03 +1.557000000000000000e+03 +1.582000000000000000e+03 +1.708000000000000000e+03 +1.432000000000000000e+03 +1.421000000000000000e+03 +1.493000000000000000e+03 +1.674000000000000000e+03 +0.000000000000000000e+00 +0.000000000000000000e+00 +1.403000000000000000e+03 +1.513000000000000000e+03 +1.366000000000000000e+03 +1.593000000000000000e+03 +1.479000000000000000e+03 +1.453000000000000000e+03 +1.378000000000000000e+03 +1.492000000000000000e+03 +1.564000000000000000e+03 +1.600000000000000000e+03 +1.706000000000000000e+03 +1.465000000000000000e+03 +1.877000000000000000e+03 +1.685000000000000000e+03 +1.924000000000000000e+03 +1.963000000000000000e+03 +1.705000000000000000e+03 +1.667000000000000000e+03 +1.571000000000000000e+03 +1.702000000000000000e+03 +1.589000000000000000e+03 +1.459000000000000000e+03 +1.654000000000000000e+03 +1.968000000000000000e+03 +1.837000000000000000e+03 +2.095000000000000000e+03 +2.270000000000000000e+03 +2.252000000000000000e+03 +2.265000000000000000e+03 +2.257000000000000000e+03 +2.364000000000000000e+03 +2.171000000000000000e+03 +1.808000000000000000e+03 +1.795000000000000000e+03 +1.568000000000000000e+03 +1.709000000000000000e+03 +2.176000000000000000e+03 +2.246000000000000000e+03 +2.299000000000000000e+03 +2.682000000000000000e+03 +2.959000000000000000e+03 +2.806000000000000000e+03 +2.871000000000000000e+03 +2.565000000000000000e+03 +2.707000000000000000e+03 +2.222000000000000000e+03 +1.953000000000000000e+03 +1.871000000000000000e+03 +1.435000000000000000e+03 +1.708000000000000000e+03 +2.056000000000000000e+03 +2.343000000000000000e+03 +2.784000000000000000e+03 +2.824000000000000000e+03 +2.940000000000000000e+03 +2.929000000000000000e+03 +3.218000000000000000e+03 +3.008000000000000000e+03 +2.870000000000000000e+03 +2.615000000000000000e+03 +2.247000000000000000e+03 +1.831000000000000000e+03 +1.516000000000000000e+03 +1.827000000000000000e+03 +2.125000000000000000e+03 +2.252000000000000000e+03 +2.612000000000000000e+03 +2.627000000000000000e+03 +2.636000000000000000e+03 +2.985000000000000000e+03 +2.895000000000000000e+03 +2.800000000000000000e+03 +2.429000000000000000e+03 +2.438000000000000000e+03 +2.037000000000000000e+03 +1.878000000000000000e+03 +1.595000000000000000e+03 +1.823000000000000000e+03 +2.094000000000000000e+03 +2.093000000000000000e+03 +2.178000000000000000e+03 +2.206000000000000000e+03 +2.313000000000000000e+03 +2.329000000000000000e+03 +2.056000000000000000e+03 +2.143000000000000000e+03 +1.842000000000000000e+03 diff --git a/tests/data/cim/case_data/dump_folder/misc.yml b/tests/data/cim/case_data/dump_folder/misc.yml new file mode 100644 index 000000000..0656014a5 --- /dev/null +++ b/tests/data/cim/case_data/dump_folder/misc.yml @@ -0,0 +1,10 @@ +container_volume: 1 +dsch_cost_factor: 0.05 +future_stop_number: 3 +load_cost_factor: 0.05 +max_tick: 200 +order_mode: fixed +past_stop_number: 4 +seed: 4096 +total_container: 100000 +version: '1' diff --git a/tests/data/cim/case_data/dump_folder/order_proportion.csv b/tests/data/cim/case_data/dump_folder/order_proportion.csv new file mode 100644 index 000000000..06b5b4b09 --- /dev/null +++ b/tests/data/cim/case_data/dump_folder/order_proportion.csv @@ -0,0 +1,158 @@ +source_port_name,source_port_index,dest_port_name,dest_port_index,proportion,proportion_noise +bremerhaven_ger,0,leHavre_fra,3,0.18755329471737509,0.025432733187591285 +bremerhaven_ger,0,montreal_can,7,0.0756906048807678,0.014981815264884504 +bremerhaven_ger,0,newYork_usa,8,0.47933778254893905,0.07860790872534353 +bremerhaven_ger,0,pusan_kor,11,0.07667664751459422,0.002902145091237068 +bremerhaven_ger,0,qingdao_chn,12,0.0512157380304634,0.0016703057083900894 +bremerhaven_ger,0,shanghai_chn,16,0.04763588234031061,0.004329384963154748 +bremerhaven_ger,0,singapore_sgp,17,0.005882857005910926,0.00014700838968303593 +bremerhaven_ger,0,yantian_chn,20,0.07600719296163895,0.014277274600949947 +durban_sau,1,qingdao_chn,12,0.2502879616953551,0.03386602700899302 +durban_sau,1,shanghai_chn,16,0.2501170152675811,0.04099351916502508 +durban_sau,1,singapore_sgp,17,0.24812321657873454,0.02983331512414685 +durban_sau,1,yantian_chn,20,0.25147180645832923,0.0398838857119423 +itagual_bra,2,qingdao_chn,12,0.19992517767067194,0.006704071585248814 +itagual_bra,2,santos_bra,14,0.20145578663243896,0.01065215665926791 +itagual_bra,2,shanghai_chn,16,0.19975353803822685,0.01889939427623029 +itagual_bra,2,singapore_sgp,17,0.19775168293424744,0.030425501405892584 +itagual_bra,2,yantian_chn,20,0.20111381472441478,0.0038049637706619867 +leHavre_fra,3,bremerhaven_ger,0,0.2613085269688243,0.046713884416130814 +leHavre_fra,3,montreal_can,7,0.09474419644790176,0.007011738109808054 +leHavre_fra,3,newYork_usa,8,0.20255309034522379,0.0075575968616494 +leHavre_fra,3,pusan_kor,11,0.09500755942526971,0.0022204132834372364 +leHavre_fra,3,qingdao_chn,12,0.08820727646821312,0.0025163323117031356 +leHavre_fra,3,shanghai_chn,16,0.08725114792024982,0.012253843912920523 +leHavre_fra,3,singapore_sgp,17,0.07609945087001035,0.014556815408633276 +leHavre_fra,3,yantian_chn,20,0.09482875155430699,0.007509358525794673 +losAngeles_usa,4,oakland_usa,9,0.27616189497862537,0.04265188459544233 +losAngeles_usa,4,princeRupert_can,10,0.15754923155692402,0.00273897680516611 +losAngeles_usa,4,qingdao_chn,12,0.1623492157793647,0.01517255937598155 +losAngeles_usa,4,seattle_usa,15,0.2423747616801366,0.014299850500812094 +losAngeles_usa,4,shanghai_chn,16,0.16156489600494922,0.0051011672478115 +manzanillo_mex,5,manzanillo_mex,5,0.1642819028757581,0.0073296927183103605 +manzanillo_mex,5,pusan_kor,11,0.1269237319062987,0.018169915140256784 +manzanillo_mex,5,qingdao_chn,12,0.12072756120502863,0.02383787277319661 +manzanillo_mex,5,sanAntonio_par,13,0.10518021746764428,0.0025563544317216205 +manzanillo_mex,5,shanghai_chn,16,0.11985637174894495,0.02058384394808081 +manzanillo_mex,5,yantian_chn,20,0.12676080545008,0.00989286132515691 +manzanillo_mex,5,yokohama_jpn,21,0.23626940934624543,0.0007488162438248653 +melbourne_aus,6,singapore_sgp,17,0.3293364398135728,0.0566906410775888 +melbourne_aus,6,sydney_aus,18,0.3364188275147848,0.01865388192132147 +melbourne_aus,6,yantian_chn,20,0.33424473267164256,0.014238633997660635 +montreal_can,7,bremerhaven_ger,0,0.5172441914124826,0.08779643369212999 +montreal_can,7,leHavre_fra,3,0.4827558085875176,0.07842477295179612 +newYork_usa,8,bremerhaven_ger,0,0.5332513888590196,0.05981761122488196 +newYork_usa,8,leHavre_fra,3,0.46674861114098054,0.03990533991720246 +oakland_usa,9,losAngeles_usa,4,0.30175478642890274,0.048963812038470556 +oakland_usa,9,princeRupert_can,10,0.23086917432389525,0.0254961817963097 +oakland_usa,9,qingdao_chn,12,0.23393880851694335,0.03349874892050399 +oakland_usa,9,shanghai_chn,16,0.23343723073025868,0.012993114869682515 +princeRupert_can,10,losAngeles_usa,4,0.2657898185062753,0.05091486367601389 +princeRupert_can,10,oakland_usa,9,0.26818216862044597,0.028397329439457514 +princeRupert_can,10,qingdao_chn,12,0.23313476502804797,0.0016646658939369785 +princeRupert_can,10,shanghai_chn,16,0.23289324784523086,0.013742472852754001 +pusan_kor,11,bremerhaven_ger,0,0.21204226502906795,0.031991420231325084 +pusan_kor,11,leHavre_fra,3,0.08535909653471886,0.005105457599858164 +pusan_kor,11,manzanillo_mex,5,0.0959125358074426,0.005809147275802185 +pusan_kor,11,qingdao_chn,12,0.051610240792657115,0.0033945457967072133 +pusan_kor,11,sanAntonio_par,13,0.035795894860646466,0.001854192137165468 +pusan_kor,11,seattle_usa,15,0.14202580193983683,0.018330317944630015 +pusan_kor,11,shanghai_chn,16,0.05072408949335503,0.009459052846765289 +pusan_kor,11,singapore_sgp,17,0.04038858042581788,0.0016197423680226325 +pusan_kor,11,vancouver_can,19,0.059258114284493034,0.00469787131269142 +pusan_kor,11,yantian_chn,20,0.05774708854229363,0.003856257701007031 +pusan_kor,11,yokohama_jpn,21,0.1691362922896707,0.014803870326138143 +qingdao_chn,12,bremerhaven_ger,0,0.19166868266386958,0.02952708760272511 +qingdao_chn,12,durban_sau,1,0.025475188174797537,0.0026860007722877676 +qingdao_chn,12,itagual_bra,2,0.020374390901285715,0.0023058721270403397 +qingdao_chn,12,leHavre_fra,3,0.04940051725539669,0.008262798336030506 +qingdao_chn,12,losAngeles_usa,4,0.14605127409748705,0.014599621251693524 +qingdao_chn,12,manzanillo_mex,5,0.061252276599797005,0.004578888771735588 +qingdao_chn,12,oakland_usa,9,0.15590868899027802,0.0018968128853613635 +qingdao_chn,12,princeRupert_can,10,0.005409393840032325,7.825075850514062e-05 +qingdao_chn,12,pusan_kor,11,0.018577689927305203,0.0007151691535729856 +qingdao_chn,12,santos_bra,14,0.02037439104592543,0.0036806154087330586 +qingdao_chn,12,seattle_usa,15,0.113038562331156,0.0016116464454415448 +qingdao_chn,12,shanghai_chn,16,0.010504586083902057,0.0011503058311188963 +qingdao_chn,12,vancouver_can,19,0.02008850220677704,0.0031091344585237185 +qingdao_chn,12,yantian_chn,20,0.018391591436727996,0.0017512950798533396 +qingdao_chn,12,yokohama_jpn,21,0.14348426444526224,0.023837737047619264 +sanAntonio_par,13,manzanillo_mex,5,0.1695702560614863,0.029864110340333205 +sanAntonio_par,13,pusan_kor,11,0.16240810942168046,0.030434021872288986 +sanAntonio_par,13,qingdao_chn,12,0.1612201993861513,0.031654173870326326 +sanAntonio_par,13,shanghai_chn,16,0.16105317774270345,0.022840905205567065 +sanAntonio_par,13,yantian_chn,20,0.16237686231768783,0.022058626605590343 +sanAntonio_par,13,yokohama_jpn,21,0.18337139507029068,0.020882193597463152 +santos_bra,14,itagual_bra,2,0.20145578737905992,0.00015677478442405662 +santos_bra,14,qingdao_chn,12,0.19992517738434895,0.007951708444894913 +santos_bra,14,shanghai_chn,16,0.19975353831991421,0.008394529742993468 +santos_bra,14,singapore_sgp,17,0.19775168256638195,0.01406059678653582 +santos_bra,14,yantian_chn,20,0.20111381435029493,0.011310434190498734 +seattle_usa,15,losAngeles_usa,4,0.2912736754142012,0.023523817623575388 +seattle_usa,15,pusan_kor,11,0.12538821688519616,0.01876947062956955 +seattle_usa,15,qingdao_chn,12,0.11617748395503369,0.02029673306956859 +seattle_usa,15,shanghai_chn,16,0.11488243812354347,0.011329185762784782 +seattle_usa,15,singapore_sgp,17,0.09977785851511455,0.00024014816912197655 +seattle_usa,15,vancouver_can,19,0.1273542861551621,0.02057342705041732 +seattle_usa,15,yantian_chn,20,0.12514604095174878,0.020083358186626803 +shanghai_chn,16,bremerhaven_ger,0,0.21398371679064213,0.021959106592952006 +shanghai_chn,16,durban_sau,1,0.017462167174606275,0.00010435314516980227 +shanghai_chn,16,itagual_bra,2,0.011430536063236027,0.0020594969751376174 +shanghai_chn,16,leHavre_fra,3,0.04575353580273499,0.0009873199353313093 +shanghai_chn,16,losAngeles_usa,4,0.16004173887997492,0.014743978442771179 +shanghai_chn,16,manzanillo_mex,5,0.05976808154382374,0.0012374290703743276 +shanghai_chn,16,oakland_usa,9,0.1716979971992254,0.022426459611386496 +shanghai_chn,16,pusan_kor,11,0.00930596400725694,0.0005070099495173756 +shanghai_chn,16,qingdao_chn,12,0.0009364036349727401,1.8783239577936313e-05 +shanghai_chn,16,santos_bra,14,0.011430536346128936,0.0012407696401487867 +shanghai_chn,16,seattle_usa,15,0.12100465463531186,0.011299147787220907 +shanghai_chn,16,vancouver_can,19,0.011092475826618629,0.002147321460772641 +shanghai_chn,16,yantian_chn,20,0.009085896257062817,0.000883163479902347 +shanghai_chn,16,yokohama_jpn,21,0.1570062958384047,0.012495339718107597 +singapore_sgp,17,bremerhaven_ger,0,0.16163465526933787,0.02403294164090424 +singapore_sgp,17,durban_sau,1,0.052947229587582224,0.007339857249890276 +singapore_sgp,17,itagual_bra,2,0.049611409618781833,0.004826948332878951 +singapore_sgp,17,leHavre_fra,3,0.06859394881607536,0.000697323478201817 +singapore_sgp,17,melbourne_aus,6,0.054598902525719965,0.009748446374379698 +singapore_sgp,17,pusan_kor,11,0.04843640111672956,0.0052864459821632135 +singapore_sgp,17,qingdao_chn,12,0.04380756610509279,0.00020264478777542088 +singapore_sgp,17,santos_bra,14,0.0496114180206766,0.0060662606703405855 +singapore_sgp,17,seattle_usa,15,0.11021203091597927,0.002184489196293904 +singapore_sgp,17,shanghai_chn,16,0.043156750350596414,0.00446890645579302 +singapore_sgp,17,singapore_sgp,17,0.03556597867096123,0.0005079804300151535 +singapore_sgp,17,sydney_aus,18,0.053961624659387086,0.0032814259751614335 +singapore_sgp,17,vancouver_can,19,0.04942443509903704,0.0013725415556900408 +singapore_sgp,17,yantian_chn,20,0.04831469461601993,0.007866049277209214 +singapore_sgp,17,yokohama_jpn,21,0.13012295462802295,0.013995694253814077 +sydney_aus,18,melbourne_aus,6,0.33665441119009204,0.010959524006592607 +sydney_aus,18,singapore_sgp,17,0.3291642505650358,0.013378806626384264 +sydney_aus,18,yantian_chn,20,0.3341813382448723,0.043398566324619615 +vancouver_can,19,pusan_kor,11,0.16323476832180567,0.01022517907397126 +vancouver_can,19,qingdao_chn,12,0.16078294599848347,0.025111111836521687 +vancouver_can,19,seattle_usa,15,0.19595625809774195,0.025666860266381805 +vancouver_can,19,shanghai_chn,16,0.1604382196746799,0.00391575019491024 +vancouver_can,19,singapore_sgp,17,0.15641751563334233,0.009869415376296499 +vancouver_can,19,yantian_chn,20,0.1631702922739467,0.01646828787230074 +yantian_chn,20,bremerhaven_ger,0,0.23063506921338092,0.03183934815230198 +yantian_chn,20,durban_sau,1,0.036977112629701755,0.006955761417407662 +yantian_chn,20,itagual_bra,2,0.03103337062146673,0.003879986047538319 +yantian_chn,20,leHavre_fra,3,0.06485623632186012,0.0033746270986396783 +yantian_chn,20,manzanillo_mex,5,0.07866656356344162,0.014297914647911386 +yantian_chn,20,melbourne_aus,6,0.03992004007199072,0.0055998037357593185 +yantian_chn,20,pusan_kor,11,0.028939753926030695,0.004337884540903886 +yantian_chn,20,qingdao_chn,12,0.020692152164438926,0.0034845335834618927 +yantian_chn,20,santos_bra,14,0.03103337095193521,0.0023084677027446535 +yantian_chn,20,seattle_usa,15,0.13901084519757625,0.0043141199296154705 +yantian_chn,20,shanghai_chn,16,0.01953252624072474,0.0001996474876791818 +yantian_chn,20,singapore_sgp,17,0.006007386215135132,0.0011502086812016938 +yantian_chn,20,sydney_aus,18,0.03878455068512264,0.0019319056238978993 +yantian_chn,20,vancouver_can,19,0.03070023678474206,0.0007858696914021227 +yantian_chn,20,yantian_chn,20,0.02872289950079146,0.0016168667042640794 +yantian_chn,20,yokohama_jpn,21,0.17448788591166092,0.023246811699828965 +yokohama_jpn,21,manzanillo_mex,5,0.224773870587767,0.03244047063421112 +yokohama_jpn,21,pusan_kor,11,0.1473122137604972,0.018126081484701975 +yokohama_jpn,21,qingdao_chn,12,0.1344645529933837,0.017152304680050526 +yokohama_jpn,21,sanAntonio_par,13,0.10222734144856105,0.0003086488347039955 +yokohama_jpn,21,shanghai_chn,16,0.13265815413379464,0.0056287128415065625 +yokohama_jpn,21,singapore_sgp,17,0.1115894561579222,0.019438990767313084 +yokohama_jpn,21,yantian_chn,20,0.14697441091807414,0.014838419143613479 diff --git a/tests/data/cim/case_data/dump_folder/ports.csv b/tests/data/cim/case_data/dump_folder/ports.csv new file mode 100644 index 000000000..5a103ca3c --- /dev/null +++ b/tests/data/cim/case_data/dump_folder/ports.csv @@ -0,0 +1,23 @@ +index,name,capacity,empty,order_proportion,order_proportion_noise,empty_return_buffer,empty_return_buffer_noise,full_return_buffer,full_return_buffer_noise +0,bremerhaven_ger,10000,16000,0.16,0.025134751494652507,1,1,1,1 +1,durban_sau,10000,1000,0.01,0.0003905851406890507,1,1,1,1 +2,itagual_bra,10000,1000,0.01,0.0017049012580563228,1,1,1,1 +3,leHavre_fra,10000,6000,0.06,0.010023093144315015,1,1,1,1 +4,losAngeles_usa,10000,5000,0.05,0.007441531984981497,1,1,1,1 +5,manzanillo_mex,10000,5000,0.05,0.005425915310841583,1,1,1,1 +6,melbourne_aus,10000,1500,0.015,0.0029979553756433923,1,1,1,1 +7,montreal_can,10000,1500,0.015,0.0005621554647653919,1,1,1,1 +8,newYork_usa,10000,3000,0.03,0.00016057717079772171,1,1,1,1 +9,oakland_usa,10000,3000,0.03,0.004910915544061917,1,1,1,1 +10,princeRupert_can,10000,1500,0.015,0.0027667472862969076,1,1,1,1 +11,pusan_kor,10000,6000,0.06,0.009320427377742116,1,1,1,1 +12,qingdao_chn,10000,8000,0.08,0.007181454765417904,1,1,1,1 +13,sanAntonio_par,10000,1000,0.01,0.0015229565945744795,1,1,1,1 +14,santos_bra,10000,1000,0.01,0.0017591918390130468,1,1,1,1 +15,seattle_usa,10000,7000,0.07,0.009178972285261822,1,1,1,1 +16,shanghai_chn,10000,10000,0.1,0.017109626305467616,1,1,1,1 +17,singapore_sgp,10000,4000,0.04,0.0014198451145378962,1,1,1,1 +18,sydney_aus,10000,1500,0.015,5.318259543305715e-05,1,1,1,1 +19,vancouver_can,10000,2000,0.02,0.002260078974868654,1,1,1,1 +20,yantian_chn,10000,7000,0.07,0.005460106707641842,1,1,1,1 +21,yokohama_jpn,10000,8000,0.08,0.014254332625813097,1,1,1,1 diff --git a/tests/data/cim/case_data/dump_folder/routes.csv b/tests/data/cim/case_data/dump_folder/routes.csv new file mode 100644 index 000000000..7ff5b8c29 --- /dev/null +++ b/tests/data/cim/case_data/dump_folder/routes.csv @@ -0,0 +1,69 @@ +index,name,port_name,port_index,distance_to_next_port +0,a3s,yantian_chn,20,320 +0,a3s,sydney_aus,18,240 +0,a3s,melbourne_aus,6,80 +1,asa,singapore_sgp,17,320 +1,asa,sydney_aus,18,260 +1,asa,melbourne_aus,6,60 +2,ate1,newYork_usa,8,160 +2,ate1,bremerhaven_ger,0,240 +2,ate1,leHavre_fra,3,40 +3,gex1,montreal_can,7,220 +3,gex1,bremerhaven_ger,0,220 +3,gex1,leHavre_fra,3,40 +4,ktx5,singapore_sgp,17,240 +4,ktx5,yantian_chn,20,80 +4,ktx5,yokohama_jpn,21,140 +5,ll4,shanghai_chn,16,80 +5,ll4,yantian_chn,20,60 +5,ll4,singapore_sgp,17,80 +5,ll4,leHavre_fra,3,380 +5,ll4,bremerhaven_ger,0,80 +5,ll4,singapore_sgp,17,420 +5,ll4,qingdao_chn,12,200 +5,ll4,pusan_kor,11,80 +6,pcc1,qingdao_chn,12,240 +6,pcc1,shanghai_chn,16,40 +6,pcc1,princeRupert_can,10,240 +6,pcc1,losAngeles_usa,4,120 +6,pcc1,oakland_usa,9,100 +7,pcc2,shanghai_chn,16,80 +7,pcc2,losAngeles_usa,4,280 +7,pcc2,seattle_usa,15,160 +7,pcc2,qingdao_chn,12,280 +8,pnw1,yantian_chn,20,80 +8,pnw1,vancouver_can,19,260 +8,pnw1,seattle_usa,15,80 +8,pnw1,pusan_kor,11,320 +9,pnw2,singapore_sgp,17,200 +9,pnw2,yantian_chn,20,120 +9,pnw2,shanghai_chn,16,80 +9,pnw2,pusan_kor,11,60 +9,pnw2,seattle_usa,15,200 +9,pnw2,vancouver_can,19,40 +9,pnw2,qingdao_chn,12,520 +9,pnw2,shanghai_chn,16,60 +10,saf3,qingdao_chn,12,80 +10,saf3,shanghai_chn,16,40 +10,saf3,yantian_chn,20,60 +10,saf3,singapore_sgp,17,80 +10,saf3,durban_sau,1,260 +10,saf3,yantian_chn,20,360 +11,tla2,qingdao_chn,12,60 +11,tla2,shanghai_chn,16,60 +11,tla2,yantian_chn,20,80 +11,tla2,singapore_sgp,17,100 +11,tla2,itagual_bra,2,460 +11,tla2,santos_bra,14,20 +11,tla2,singapore_sgp,17,580 +11,tla2,yantian_chn,20,140 +11,tla2,shanghai_chn,16,80 +12,tlp1,yantian_chn,20,100 +12,tlp1,shanghai_chn,16,60 +12,tlp1,qingdao_chn,12,60 +12,tlp1,pusan_kor,11,40 +12,tlp1,manzanillo_mex,5,260 +12,tlp1,sanAntonio_par,13,140 +12,tlp1,manzanillo_mex,5,180 +12,tlp1,yokohama_jpn,21,260 +12,tlp1,shanghai_chn,16,60 diff --git a/tests/data/cim/case_data/dump_folder/stops.csv b/tests/data/cim/case_data/dump_folder/stops.csv new file mode 100644 index 000000000..16d64d4df --- /dev/null +++ b/tests/data/cim/case_data/dump_folder/stops.csv @@ -0,0 +1,616 @@ +vessel_name,vessel_index,port_name,port_index,arrival_tick,departure_tick +a3s_vessel_000,0,yantian_chn,20,0,1 +a3s_vessel_000,0,sydney_aus,18,38,40 +a3s_vessel_000,0,melbourne_aus,6,65,66 +a3s_vessel_000,0,yantian_chn,20,73,74 +a3s_vessel_000,0,sydney_aus,18,104,105 +a3s_vessel_000,0,melbourne_aus,6,127,129 +a3s_vessel_000,0,yantian_chn,20,139,141 +a3s_vessel_000,0,sydney_aus,18,181,183 +a3s_vessel_000,0,melbourne_aus,6,209,211 +a3s_vessel_000,0,yantian_chn,20,220,222 +a3s_vessel_000,0,sydney_aus,18,255,256 +a3s_vessel_001,1,sydney_aus,18,0,2 +a3s_vessel_001,1,melbourne_aus,6,30,31 +a3s_vessel_001,1,yantian_chn,20,39,40 +a3s_vessel_001,1,sydney_aus,18,77,79 +a3s_vessel_001,1,melbourne_aus,6,108,109 +a3s_vessel_001,1,yantian_chn,20,117,118 +a3s_vessel_001,1,sydney_aus,18,150,152 +a3s_vessel_001,1,melbourne_aus,6,177,179 +a3s_vessel_001,1,yantian_chn,20,191,193 +a3s_vessel_001,1,sydney_aus,18,237,239 +a3s_vessel_001,1,melbourne_aus,6,262,264 +a3s_vessel_001,1,yantian_chn,20,274,276 +asa_vessel_000,2,singapore_sgp,17,0,2 +asa_vessel_000,2,sydney_aus,18,45,47 +asa_vessel_000,2,melbourne_aus,6,77,79 +asa_vessel_000,2,singapore_sgp,17,87,88 +asa_vessel_000,2,sydney_aus,18,129,131 +asa_vessel_000,2,melbourne_aus,6,161,162 +asa_vessel_000,2,singapore_sgp,17,170,171 +asa_vessel_000,2,sydney_aus,18,207,208 +asa_vessel_000,2,melbourne_aus,6,245,246 +asa_vessel_000,2,singapore_sgp,17,254,255 +asa_vessel_001,3,melbourne_aus,6,0,2 +asa_vessel_001,3,singapore_sgp,17,8,9 +asa_vessel_001,3,sydney_aus,18,47,48 +asa_vessel_001,3,melbourne_aus,6,74,75 +asa_vessel_001,3,singapore_sgp,17,81,82 +asa_vessel_001,3,sydney_aus,18,111,113 +asa_vessel_001,3,melbourne_aus,6,138,139 +asa_vessel_001,3,singapore_sgp,17,146,148 +asa_vessel_001,3,sydney_aus,18,185,186 +asa_vessel_001,3,melbourne_aus,6,214,216 +asa_vessel_001,3,singapore_sgp,17,223,224 +asa_vessel_001,3,sydney_aus,18,253,254 +ate1_vessel_000,4,newYork_usa,8,0,1 +ate1_vessel_000,4,bremerhaven_ger,0,19,20 +ate1_vessel_000,4,leHavre_fra,3,46,47 +ate1_vessel_000,4,newYork_usa,8,52,54 +ate1_vessel_000,4,bremerhaven_ger,0,74,75 +ate1_vessel_000,4,leHavre_fra,3,100,101 +ate1_vessel_000,4,newYork_usa,8,106,108 +ate1_vessel_000,4,bremerhaven_ger,0,127,128 +ate1_vessel_000,4,leHavre_fra,3,157,158 +ate1_vessel_000,4,newYork_usa,8,163,164 +ate1_vessel_000,4,bremerhaven_ger,0,183,185 +ate1_vessel_000,4,leHavre_fra,3,210,212 +ate1_vessel_000,4,newYork_usa,8,217,219 +ate1_vessel_000,4,bremerhaven_ger,0,238,240 +ate1_vessel_001,5,bremerhaven_ger,0,0,2 +ate1_vessel_001,5,leHavre_fra,3,34,36 +ate1_vessel_001,5,newYork_usa,8,42,44 +ate1_vessel_001,5,bremerhaven_ger,0,63,64 +ate1_vessel_001,5,leHavre_fra,3,92,94 +ate1_vessel_001,5,newYork_usa,8,100,102 +ate1_vessel_001,5,bremerhaven_ger,0,120,121 +ate1_vessel_001,5,leHavre_fra,3,151,152 +ate1_vessel_001,5,newYork_usa,8,158,160 +ate1_vessel_001,5,bremerhaven_ger,0,183,185 +ate1_vessel_001,5,leHavre_fra,3,212,214 +ate1_vessel_001,5,newYork_usa,8,220,222 +ate1_vessel_001,5,bremerhaven_ger,0,242,244 +gex1_vessel_000,6,bremerhaven_ger,0,0,1 +gex1_vessel_000,6,leHavre_fra,3,20,22 +gex1_vessel_000,6,montreal_can,7,26,27 +gex1_vessel_000,6,bremerhaven_ger,0,47,48 +gex1_vessel_000,6,leHavre_fra,3,73,75 +gex1_vessel_000,6,montreal_can,7,80,82 +gex1_vessel_000,6,bremerhaven_ger,0,102,103 +gex1_vessel_000,6,leHavre_fra,3,131,133 +gex1_vessel_000,6,montreal_can,7,138,139 +gex1_vessel_000,6,bremerhaven_ger,0,165,166 +gex1_vessel_000,6,leHavre_fra,3,190,192 +gex1_vessel_000,6,montreal_can,7,196,197 +gex1_vessel_000,6,bremerhaven_ger,0,216,218 +gex1_vessel_000,6,leHavre_fra,3,238,240 +gex1_vessel_000,6,montreal_can,7,245,247 +gex1_vessel_001,7,montreal_can,7,0,2 +gex1_vessel_001,7,bremerhaven_ger,0,30,31 +gex1_vessel_001,7,leHavre_fra,3,57,58 +gex1_vessel_001,7,montreal_can,7,63,64 +gex1_vessel_001,7,bremerhaven_ger,0,88,90 +gex1_vessel_001,7,leHavre_fra,3,115,117 +gex1_vessel_001,7,montreal_can,7,122,123 +gex1_vessel_001,7,bremerhaven_ger,0,149,151 +gex1_vessel_001,7,leHavre_fra,3,175,177 +gex1_vessel_001,7,montreal_can,7,182,183 +gex1_vessel_001,7,bremerhaven_ger,0,210,212 +gex1_vessel_001,7,leHavre_fra,3,237,238 +gex1_vessel_001,7,montreal_can,7,243,245 +ktx5_vessel_000,8,singapore_sgp,17,0,2 +ktx5_vessel_000,8,yantian_chn,20,35,37 +ktx5_vessel_000,8,yokohama_jpn,21,47,49 +ktx5_vessel_000,8,singapore_sgp,17,69,70 +ktx5_vessel_000,8,yantian_chn,20,103,104 +ktx5_vessel_000,8,yokohama_jpn,21,114,116 +ktx5_vessel_000,8,singapore_sgp,17,135,136 +ktx5_vessel_000,8,yantian_chn,20,164,166 +ktx5_vessel_000,8,yokohama_jpn,21,176,178 +ktx5_vessel_000,8,singapore_sgp,17,196,197 +ktx5_vessel_000,8,yantian_chn,20,225,226 +ktx5_vessel_000,8,yokohama_jpn,21,236,238 +ktx5_vessel_000,8,singapore_sgp,17,257,259 +ktx5_vessel_001,9,yokohama_jpn,21,0,1 +ktx5_vessel_001,9,singapore_sgp,17,14,15 +ktx5_vessel_001,9,yantian_chn,20,38,39 +ktx5_vessel_001,9,yokohama_jpn,21,48,49 +ktx5_vessel_001,9,singapore_sgp,17,63,64 +ktx5_vessel_001,9,yantian_chn,20,86,88 +ktx5_vessel_001,9,yokohama_jpn,21,96,97 +ktx5_vessel_001,9,singapore_sgp,17,112,114 +ktx5_vessel_001,9,yantian_chn,20,140,141 +ktx5_vessel_001,9,yokohama_jpn,21,149,150 +ktx5_vessel_001,9,singapore_sgp,17,166,168 +ktx5_vessel_001,9,yantian_chn,20,193,194 +ktx5_vessel_001,9,yokohama_jpn,21,203,204 +ktx5_vessel_001,9,singapore_sgp,17,219,220 +ktx5_vessel_001,9,yantian_chn,20,242,243 +ll4_vessel_000,10,bremerhaven_ger,0,0,2 +ll4_vessel_000,10,singapore_sgp,17,13,15 +ll4_vessel_000,10,qingdao_chn,12,69,71 +ll4_vessel_000,10,pusan_kor,11,97,98 +ll4_vessel_000,10,shanghai_chn,16,108,110 +ll4_vessel_000,10,yantian_chn,20,119,120 +ll4_vessel_000,10,singapore_sgp,17,129,131 +ll4_vessel_000,10,leHavre_fra,3,140,141 +ll4_vessel_000,10,bremerhaven_ger,0,193,194 +ll4_vessel_000,10,singapore_sgp,17,205,207 +ll4_vessel_000,10,qingdao_chn,12,247,249 +ll4_vessel_000,10,pusan_kor,11,272,273 +ll4_vessel_001,11,yantian_chn,20,0,2 +ll4_vessel_001,11,singapore_sgp,17,12,14 +ll4_vessel_001,11,leHavre_fra,3,25,27 +ll4_vessel_001,11,bremerhaven_ger,0,78,80 +ll4_vessel_001,11,singapore_sgp,17,94,96 +ll4_vessel_001,11,qingdao_chn,12,163,164 +ll4_vessel_001,11,pusan_kor,11,191,192 +ll4_vessel_001,11,shanghai_chn,16,202,203 +ll4_vessel_001,11,yantian_chn,20,215,216 +ll4_vessel_001,11,singapore_sgp,17,224,226 +ll4_vessel_002,12,singapore_sgp,17,0,1 +ll4_vessel_002,12,leHavre_fra,3,10,12 +ll4_vessel_002,12,bremerhaven_ger,0,51,53 +ll4_vessel_002,12,singapore_sgp,17,61,62 +ll4_vessel_002,12,qingdao_chn,12,106,108 +ll4_vessel_002,12,pusan_kor,11,131,133 +ll4_vessel_002,12,shanghai_chn,16,142,144 +ll4_vessel_002,12,yantian_chn,20,152,153 +ll4_vessel_002,12,singapore_sgp,17,159,160 +ll4_vessel_002,12,leHavre_fra,3,169,171 +ll4_vessel_002,12,bremerhaven_ger,0,207,209 +ll4_vessel_002,12,singapore_sgp,17,218,220 +ll4_vessel_002,12,qingdao_chn,12,260,261 +ll4_vessel_003,13,leHavre_fra,3,0,2 +ll4_vessel_003,13,bremerhaven_ger,0,46,47 +ll4_vessel_003,13,singapore_sgp,17,56,58 +ll4_vessel_003,13,qingdao_chn,12,102,103 +ll4_vessel_003,13,pusan_kor,11,124,125 +ll4_vessel_003,13,shanghai_chn,16,134,136 +ll4_vessel_003,13,yantian_chn,20,146,147 +ll4_vessel_003,13,singapore_sgp,17,154,155 +ll4_vessel_003,13,leHavre_fra,3,165,166 +ll4_vessel_003,13,bremerhaven_ger,0,209,211 +ll4_vessel_003,13,singapore_sgp,17,220,221 +ll4_vessel_003,13,qingdao_chn,12,267,268 +ll4_vessel_004,14,pusan_kor,11,0,2 +ll4_vessel_004,14,shanghai_chn,16,12,14 +ll4_vessel_004,14,yantian_chn,20,23,25 +ll4_vessel_004,14,singapore_sgp,17,34,36 +ll4_vessel_004,14,leHavre_fra,3,47,48 +ll4_vessel_004,14,bremerhaven_ger,0,93,95 +ll4_vessel_004,14,singapore_sgp,17,107,109 +ll4_vessel_004,14,qingdao_chn,12,168,170 +ll4_vessel_004,14,pusan_kor,11,194,196 +ll4_vessel_004,14,shanghai_chn,16,207,208 +ll4_vessel_004,14,yantian_chn,20,218,219 +ll4_vessel_004,14,singapore_sgp,17,228,230 +ll4_vessel_005,15,qingdao_chn,12,0,2 +ll4_vessel_005,15,pusan_kor,11,22,24 +ll4_vessel_005,15,shanghai_chn,16,32,33 +ll4_vessel_005,15,yantian_chn,20,41,42 +ll4_vessel_005,15,singapore_sgp,17,49,51 +ll4_vessel_005,15,leHavre_fra,3,60,62 +ll4_vessel_005,15,bremerhaven_ger,0,98,99 +ll4_vessel_005,15,singapore_sgp,17,107,109 +ll4_vessel_005,15,qingdao_chn,12,154,155 +ll4_vessel_005,15,pusan_kor,11,177,178 +ll4_vessel_005,15,shanghai_chn,16,187,188 +ll4_vessel_005,15,yantian_chn,20,197,198 +ll4_vessel_005,15,singapore_sgp,17,204,206 +ll4_vessel_005,15,leHavre_fra,3,215,216 +ll4_vessel_005,15,bremerhaven_ger,0,253,254 +pcc1_vessel_000,16,oakland_usa,9,0,2 +pcc1_vessel_000,16,qingdao_chn,12,14,16 +pcc1_vessel_000,16,shanghai_chn,16,45,46 +pcc1_vessel_000,16,princeRupert_can,10,51,53 +pcc1_vessel_000,16,losAngeles_usa,4,79,81 +pcc1_vessel_000,16,oakland_usa,9,95,96 +pcc1_vessel_000,16,qingdao_chn,12,109,110 +pcc1_vessel_000,16,shanghai_chn,16,136,137 +pcc1_vessel_000,16,princeRupert_can,10,142,144 +pcc1_vessel_000,16,losAngeles_usa,4,170,172 +pcc1_vessel_000,16,oakland_usa,9,185,186 +pcc1_vessel_000,16,qingdao_chn,12,197,198 +pcc1_vessel_000,16,shanghai_chn,16,223,225 +pcc1_vessel_000,16,princeRupert_can,10,230,232 +pcc1_vessel_000,16,losAngeles_usa,4,259,261 +pcc1_vessel_001,17,qingdao_chn,12,0,2 +pcc1_vessel_001,17,shanghai_chn,16,34,36 +pcc1_vessel_001,17,princeRupert_can,10,43,45 +pcc1_vessel_001,17,losAngeles_usa,4,70,71 +pcc1_vessel_001,17,oakland_usa,9,88,90 +pcc1_vessel_001,17,qingdao_chn,12,106,108 +pcc1_vessel_001,17,shanghai_chn,16,142,143 +pcc1_vessel_001,17,princeRupert_can,10,148,149 +pcc1_vessel_001,17,losAngeles_usa,4,181,182 +pcc1_vessel_001,17,oakland_usa,9,200,202 +pcc1_vessel_001,17,qingdao_chn,12,213,215 +pcc1_vessel_001,17,shanghai_chn,16,244,246 +pcc1_vessel_001,17,princeRupert_can,10,251,252 +pcc1_vessel_002,18,princeRupert_can,10,0,2 +pcc1_vessel_002,18,losAngeles_usa,4,27,28 +pcc1_vessel_002,18,oakland_usa,9,41,42 +pcc1_vessel_002,18,qingdao_chn,12,53,54 +pcc1_vessel_002,18,shanghai_chn,16,78,79 +pcc1_vessel_002,18,princeRupert_can,10,83,84 +pcc1_vessel_002,18,losAngeles_usa,4,109,111 +pcc1_vessel_002,18,oakland_usa,9,123,125 +pcc1_vessel_002,18,qingdao_chn,12,136,137 +pcc1_vessel_002,18,shanghai_chn,16,163,164 +pcc1_vessel_002,18,princeRupert_can,10,168,170 +pcc1_vessel_002,18,losAngeles_usa,4,193,195 +pcc1_vessel_002,18,oakland_usa,9,207,209 +pcc1_vessel_002,18,qingdao_chn,12,220,222 +pcc1_vessel_002,18,shanghai_chn,16,249,250 +pcc2_vessel_000,19,seattle_usa,15,0,2 +pcc2_vessel_000,19,qingdao_chn,12,21,22 +pcc2_vessel_000,19,shanghai_chn,16,62,63 +pcc2_vessel_000,19,losAngeles_usa,4,72,74 +pcc2_vessel_000,19,seattle_usa,15,100,101 +pcc2_vessel_000,19,qingdao_chn,12,117,118 +pcc2_vessel_000,19,shanghai_chn,16,154,155 +pcc2_vessel_000,19,losAngeles_usa,4,166,167 +pcc2_vessel_000,19,seattle_usa,15,196,198 +pcc2_vessel_000,19,qingdao_chn,12,213,214 +pcc2_vessel_000,19,shanghai_chn,16,242,244 +pcc2_vessel_000,19,losAngeles_usa,4,253,255 +pcc2_vessel_001,20,losAngeles_usa,4,0,1 +pcc2_vessel_001,20,seattle_usa,15,41,43 +pcc2_vessel_001,20,qingdao_chn,12,60,62 +pcc2_vessel_001,20,shanghai_chn,16,96,97 +pcc2_vessel_001,20,losAngeles_usa,4,109,110 +pcc2_vessel_001,20,seattle_usa,15,145,147 +pcc2_vessel_001,20,qingdao_chn,12,165,167 +pcc2_vessel_001,20,shanghai_chn,16,197,199 +pcc2_vessel_001,20,losAngeles_usa,4,212,213 +pcc2_vessel_001,20,seattle_usa,15,247,248 +pcc2_vessel_001,20,qingdao_chn,12,274,275 +pcc2_vessel_002,21,shanghai_chn,16,0,2 +pcc2_vessel_002,21,losAngeles_usa,4,10,11 +pcc2_vessel_002,21,seattle_usa,15,40,41 +pcc2_vessel_002,21,qingdao_chn,12,61,62 +pcc2_vessel_002,21,shanghai_chn,16,92,93 +pcc2_vessel_002,21,losAngeles_usa,4,101,102 +pcc2_vessel_002,21,seattle_usa,15,128,130 +pcc2_vessel_002,21,qingdao_chn,12,149,150 +pcc2_vessel_002,21,shanghai_chn,16,183,184 +pcc2_vessel_002,21,losAngeles_usa,4,191,192 +pcc2_vessel_002,21,seattle_usa,15,218,220 +pcc2_vessel_002,21,qingdao_chn,12,240,242 +pcc2_vessel_002,21,shanghai_chn,16,270,272 +pnw1_vessel_000,22,seattle_usa,15,0,2 +pnw1_vessel_000,22,pusan_kor,11,11,13 +pnw1_vessel_000,22,yantian_chn,20,48,50 +pnw1_vessel_000,22,vancouver_can,19,59,61 +pnw1_vessel_000,22,seattle_usa,15,92,94 +pnw1_vessel_000,22,pusan_kor,11,104,105 +pnw1_vessel_000,22,yantian_chn,20,139,141 +pnw1_vessel_000,22,vancouver_can,19,151,153 +pnw1_vessel_000,22,seattle_usa,15,182,184 +pnw1_vessel_000,22,pusan_kor,11,194,195 +pnw1_vessel_000,22,yantian_chn,20,230,232 +pnw1_vessel_000,22,vancouver_can,19,241,242 +pnw1_vessel_000,22,seattle_usa,15,272,274 +pnw1_vessel_001,23,vancouver_can,19,0,2 +pnw1_vessel_001,23,seattle_usa,15,38,39 +pnw1_vessel_001,23,pusan_kor,11,50,52 +pnw1_vessel_001,23,yantian_chn,20,90,92 +pnw1_vessel_001,23,vancouver_can,19,104,106 +pnw1_vessel_001,23,seattle_usa,15,140,142 +pnw1_vessel_001,23,pusan_kor,11,152,154 +pnw1_vessel_001,23,yantian_chn,20,196,198 +pnw1_vessel_001,23,vancouver_can,19,208,210 +pnw1_vessel_001,23,seattle_usa,15,241,242 +pnw1_vessel_001,23,pusan_kor,11,252,254 +pnw1_vessel_002,24,yantian_chn,20,0,1 +pnw1_vessel_002,24,vancouver_can,19,9,10 +pnw1_vessel_002,24,seattle_usa,15,35,36 +pnw1_vessel_002,24,pusan_kor,11,45,46 +pnw1_vessel_002,24,yantian_chn,20,76,78 +pnw1_vessel_002,24,vancouver_can,19,86,87 +pnw1_vessel_002,24,seattle_usa,15,114,115 +pnw1_vessel_002,24,pusan_kor,11,123,124 +pnw1_vessel_002,24,yantian_chn,20,156,157 +pnw1_vessel_002,24,vancouver_can,19,165,167 +pnw1_vessel_002,24,seattle_usa,15,194,195 +pnw1_vessel_002,24,pusan_kor,11,204,205 +pnw1_vessel_002,24,yantian_chn,20,237,239 +pnw1_vessel_002,24,vancouver_can,19,248,249 +pnw2_vessel_000,25,pusan_kor,11,0,1 +pnw2_vessel_000,25,seattle_usa,15,9,10 +pnw2_vessel_000,25,vancouver_can,19,36,38 +pnw2_vessel_000,25,qingdao_chn,12,44,46 +pnw2_vessel_000,25,shanghai_chn,16,97,99 +pnw2_vessel_000,25,singapore_sgp,17,107,109 +pnw2_vessel_000,25,yantian_chn,20,133,135 +pnw2_vessel_000,25,shanghai_chn,16,150,151 +pnw2_vessel_000,25,pusan_kor,11,161,163 +pnw2_vessel_000,25,seattle_usa,15,170,172 +pnw2_vessel_000,25,vancouver_can,19,194,196 +pnw2_vessel_000,25,qingdao_chn,12,201,202 +pnw2_vessel_000,25,shanghai_chn,16,254,255 +pnw2_vessel_000,25,singapore_sgp,17,263,264 +pnw2_vessel_001,26,yantian_chn,20,0,2 +pnw2_vessel_001,26,shanghai_chn,16,16,18 +pnw2_vessel_001,26,pusan_kor,11,27,28 +pnw2_vessel_001,26,seattle_usa,15,37,38 +pnw2_vessel_001,26,vancouver_can,19,71,72 +pnw2_vessel_001,26,qingdao_chn,12,78,80 +pnw2_vessel_001,26,shanghai_chn,16,134,136 +pnw2_vessel_001,26,singapore_sgp,17,146,148 +pnw2_vessel_001,26,yantian_chn,20,174,176 +pnw2_vessel_001,26,shanghai_chn,16,189,191 +pnw2_vessel_001,26,pusan_kor,11,200,202 +pnw2_vessel_001,26,seattle_usa,15,210,212 +pnw2_vessel_001,26,vancouver_can,19,242,244 +pnw2_vessel_001,26,qingdao_chn,12,251,252 +pnw2_vessel_002,27,vancouver_can,19,0,2 +pnw2_vessel_002,27,qingdao_chn,12,6,7 +pnw2_vessel_002,27,shanghai_chn,16,57,59 +pnw2_vessel_002,27,singapore_sgp,17,66,68 +pnw2_vessel_002,27,yantian_chn,20,89,91 +pnw2_vessel_002,27,shanghai_chn,16,103,105 +pnw2_vessel_002,27,pusan_kor,11,113,115 +pnw2_vessel_002,27,seattle_usa,15,122,124 +pnw2_vessel_002,27,vancouver_can,19,144,146 +pnw2_vessel_002,27,qingdao_chn,12,150,152 +pnw2_vessel_002,27,shanghai_chn,16,209,211 +pnw2_vessel_002,27,singapore_sgp,17,218,219 +pnw2_vessel_002,27,yantian_chn,20,238,240 +pnw2_vessel_003,28,pusan_kor,11,0,2 +pnw2_vessel_003,28,seattle_usa,15,9,10 +pnw2_vessel_003,28,vancouver_can,19,34,36 +pnw2_vessel_003,28,qingdao_chn,12,41,42 +pnw2_vessel_003,28,shanghai_chn,16,103,104 +pnw2_vessel_003,28,singapore_sgp,17,112,113 +pnw2_vessel_003,28,yantian_chn,20,135,136 +pnw2_vessel_003,28,shanghai_chn,16,149,150 +pnw2_vessel_003,28,pusan_kor,11,159,160 +pnw2_vessel_003,28,seattle_usa,15,167,168 +pnw2_vessel_003,28,vancouver_can,19,192,194 +pnw2_vessel_003,28,qingdao_chn,12,199,201 +pnw2_vessel_003,28,shanghai_chn,16,262,264 +pnw2_vessel_003,28,singapore_sgp,17,271,272 +pnw2_vessel_003,28,yantian_chn,20,294,295 +pnw2_vessel_004,29,qingdao_chn,12,0,2 +pnw2_vessel_004,29,shanghai_chn,16,76,77 +pnw2_vessel_004,29,singapore_sgp,17,84,86 +pnw2_vessel_004,29,yantian_chn,20,110,112 +pnw2_vessel_004,29,shanghai_chn,16,130,131 +pnw2_vessel_004,29,pusan_kor,11,143,145 +pnw2_vessel_004,29,seattle_usa,15,153,155 +pnw2_vessel_004,29,vancouver_can,19,179,180 +pnw2_vessel_004,29,qingdao_chn,12,185,187 +pnw2_vessel_004,29,shanghai_chn,16,252,253 +pnw2_vessel_004,29,singapore_sgp,17,260,262 +pnw2_vessel_004,29,yantian_chn,20,285,287 +saf3_vessel_000,30,yantian_chn,20,0,1 +saf3_vessel_000,30,singapore_sgp,17,8,10 +saf3_vessel_000,30,durban_sau,1,18,19 +saf3_vessel_000,30,yantian_chn,20,48,50 +saf3_vessel_000,30,qingdao_chn,12,86,88 +saf3_vessel_000,30,shanghai_chn,16,96,97 +saf3_vessel_000,30,yantian_chn,20,101,102 +saf3_vessel_000,30,singapore_sgp,17,109,111 +saf3_vessel_000,30,durban_sau,1,119,120 +saf3_vessel_000,30,yantian_chn,20,146,148 +saf3_vessel_000,30,qingdao_chn,12,184,185 +saf3_vessel_000,30,shanghai_chn,16,193,194 +saf3_vessel_000,30,yantian_chn,20,198,199 +saf3_vessel_000,30,singapore_sgp,17,205,206 +saf3_vessel_000,30,durban_sau,1,214,216 +saf3_vessel_000,30,yantian_chn,20,242,244 +saf3_vessel_001,31,qingdao_chn,12,0,2 +saf3_vessel_001,31,shanghai_chn,16,12,14 +saf3_vessel_001,31,yantian_chn,20,19,20 +saf3_vessel_001,31,singapore_sgp,17,28,30 +saf3_vessel_001,31,durban_sau,1,40,42 +saf3_vessel_001,31,yantian_chn,20,71,73 +saf3_vessel_001,31,qingdao_chn,12,116,118 +saf3_vessel_001,31,shanghai_chn,16,128,130 +saf3_vessel_001,31,yantian_chn,20,135,137 +saf3_vessel_001,31,singapore_sgp,17,144,145 +saf3_vessel_001,31,durban_sau,1,155,156 +saf3_vessel_001,31,yantian_chn,20,188,189 +saf3_vessel_001,31,qingdao_chn,12,230,232 +saf3_vessel_001,31,shanghai_chn,16,241,243 +saf3_vessel_001,31,yantian_chn,20,248,250 +saf3_vessel_002,32,singapore_sgp,17,0,2 +saf3_vessel_002,32,durban_sau,1,14,16 +saf3_vessel_002,32,yantian_chn,20,53,55 +saf3_vessel_002,32,qingdao_chn,12,96,97 +saf3_vessel_002,32,shanghai_chn,16,110,112 +saf3_vessel_002,32,yantian_chn,20,117,118 +saf3_vessel_002,32,singapore_sgp,17,128,130 +saf3_vessel_002,32,durban_sau,1,142,143 +saf3_vessel_002,32,yantian_chn,20,175,177 +saf3_vessel_002,32,qingdao_chn,12,222,224 +saf3_vessel_002,32,shanghai_chn,16,233,235 +saf3_vessel_002,32,yantian_chn,20,240,241 +saf3_vessel_003,33,durban_sau,1,0,2 +saf3_vessel_003,33,yantian_chn,20,29,30 +saf3_vessel_003,33,qingdao_chn,12,63,65 +saf3_vessel_003,33,shanghai_chn,16,74,76 +saf3_vessel_003,33,yantian_chn,20,80,82 +saf3_vessel_003,33,singapore_sgp,17,89,91 +saf3_vessel_003,33,durban_sau,1,98,100 +saf3_vessel_003,33,yantian_chn,20,122,123 +saf3_vessel_003,33,qingdao_chn,12,163,165 +saf3_vessel_003,33,shanghai_chn,16,173,174 +saf3_vessel_003,33,yantian_chn,20,178,180 +saf3_vessel_003,33,singapore_sgp,17,187,189 +saf3_vessel_003,33,durban_sau,1,196,197 +saf3_vessel_003,33,yantian_chn,20,225,227 +saf3_vessel_003,33,qingdao_chn,12,261,263 +saf3_vessel_003,33,shanghai_chn,16,271,272 +tla2_vessel_000,34,itagual_bra,2,0,1 +tla2_vessel_000,34,santos_bra,14,53,55 +tla2_vessel_000,34,singapore_sgp,17,58,60 +tla2_vessel_000,34,yantian_chn,20,131,133 +tla2_vessel_000,34,shanghai_chn,16,151,153 +tla2_vessel_000,34,qingdao_chn,12,163,165 +tla2_vessel_000,34,shanghai_chn,16,173,175 +tla2_vessel_000,34,yantian_chn,20,182,184 +tla2_vessel_000,34,singapore_sgp,17,193,194 +tla2_vessel_000,34,itagual_bra,2,205,206 +tla2_vessel_000,34,santos_bra,14,258,259 +tla2_vessel_000,34,singapore_sgp,17,262,264 +tla2_vessel_001,35,santos_bra,14,0,2 +tla2_vessel_001,35,singapore_sgp,17,5,6 +tla2_vessel_001,35,yantian_chn,20,76,78 +tla2_vessel_001,35,shanghai_chn,16,97,99 +tla2_vessel_001,35,qingdao_chn,12,109,110 +tla2_vessel_001,35,shanghai_chn,16,118,120 +tla2_vessel_001,35,yantian_chn,20,127,128 +tla2_vessel_001,35,singapore_sgp,17,138,139 +tla2_vessel_001,35,itagual_bra,2,152,154 +tla2_vessel_001,35,santos_bra,14,209,210 +tla2_vessel_001,35,singapore_sgp,17,213,215 +tla2_vessel_001,35,yantian_chn,20,291,292 +tla2_vessel_002,36,singapore_sgp,17,0,1 +tla2_vessel_002,36,itagual_bra,2,12,14 +tla2_vessel_002,36,santos_bra,14,64,66 +tla2_vessel_002,36,singapore_sgp,17,69,71 +tla2_vessel_002,36,yantian_chn,20,124,125 +tla2_vessel_002,36,shanghai_chn,16,138,139 +tla2_vessel_002,36,qingdao_chn,12,146,147 +tla2_vessel_002,36,shanghai_chn,16,153,155 +tla2_vessel_002,36,yantian_chn,20,162,164 +tla2_vessel_002,36,singapore_sgp,17,174,176 +tla2_vessel_002,36,itagual_bra,2,185,186 +tla2_vessel_002,36,santos_bra,14,243,245 +tla2_vessel_002,36,singapore_sgp,17,248,250 +tla2_vessel_002,36,yantian_chn,20,301,303 +tla2_vessel_003,37,shanghai_chn,16,0,2 +tla2_vessel_003,37,yantian_chn,20,9,10 +tla2_vessel_003,37,singapore_sgp,17,19,21 +tla2_vessel_003,37,itagual_bra,2,34,35 +tla2_vessel_003,37,santos_bra,14,90,92 +tla2_vessel_003,37,singapore_sgp,17,95,97 +tla2_vessel_003,37,yantian_chn,20,169,170 +tla2_vessel_003,37,shanghai_chn,16,185,187 +tla2_vessel_003,37,qingdao_chn,12,196,198 +tla2_vessel_003,37,shanghai_chn,16,205,206 +tla2_vessel_003,37,yantian_chn,20,213,215 +tla2_vessel_003,37,singapore_sgp,17,224,226 +tla2_vessel_004,38,qingdao_chn,12,0,2 +tla2_vessel_004,38,shanghai_chn,16,9,11 +tla2_vessel_004,38,yantian_chn,20,21,22 +tla2_vessel_004,38,singapore_sgp,17,32,33 +tla2_vessel_004,38,itagual_bra,2,46,48 +tla2_vessel_004,38,santos_bra,14,124,126 +tla2_vessel_004,38,singapore_sgp,17,129,131 +tla2_vessel_004,38,yantian_chn,20,210,212 +tla2_vessel_004,38,shanghai_chn,16,232,234 +tla2_vessel_004,38,qingdao_chn,12,245,247 +tla2_vessel_005,39,yantian_chn,20,0,1 +tla2_vessel_005,39,singapore_sgp,17,10,11 +tla2_vessel_005,39,itagual_bra,2,23,25 +tla2_vessel_005,39,santos_bra,14,76,77 +tla2_vessel_005,39,singapore_sgp,17,80,81 +tla2_vessel_005,39,yantian_chn,20,135,137 +tla2_vessel_005,39,shanghai_chn,16,151,153 +tla2_vessel_005,39,qingdao_chn,12,162,163 +tla2_vessel_005,39,shanghai_chn,16,170,172 +tla2_vessel_005,39,yantian_chn,20,178,179 +tla2_vessel_005,39,singapore_sgp,17,187,189 +tla2_vessel_005,39,itagual_bra,2,200,201 +tla2_vessel_005,39,santos_bra,14,249,250 +tla2_vessel_005,39,singapore_sgp,17,252,253 +tla2_vessel_005,39,yantian_chn,20,307,308 +tlp1_vessel_000,40,yantian_chn,20,0,2 +tlp1_vessel_000,40,shanghai_chn,16,12,14 +tlp1_vessel_000,40,qingdao_chn,12,21,22 +tlp1_vessel_000,40,pusan_kor,11,28,30 +tlp1_vessel_000,40,manzanillo_mex,5,34,36 +tlp1_vessel_000,40,sanAntonio_par,13,64,65 +tlp1_vessel_000,40,manzanillo_mex,5,82,83 +tlp1_vessel_000,40,yokohama_jpn,21,107,109 +tlp1_vessel_000,40,shanghai_chn,16,135,137 +tlp1_vessel_000,40,yantian_chn,20,144,145 +tlp1_vessel_000,40,shanghai_chn,16,160,161 +tlp1_vessel_000,40,qingdao_chn,12,169,170 +tlp1_vessel_000,40,pusan_kor,11,179,181 +tlp1_vessel_000,40,manzanillo_mex,5,187,188 +tlp1_vessel_000,40,sanAntonio_par,13,214,216 +tlp1_vessel_000,40,manzanillo_mex,5,233,235 +tlp1_vessel_000,40,yokohama_jpn,21,252,253 +tlp1_vessel_001,41,manzanillo_mex,5,0,1 +tlp1_vessel_001,41,sanAntonio_par,13,35,37 +tlp1_vessel_001,41,manzanillo_mex,5,54,55 +tlp1_vessel_001,41,yokohama_jpn,21,81,83 +tlp1_vessel_001,41,shanghai_chn,16,114,116 +tlp1_vessel_001,41,yantian_chn,20,125,126 +tlp1_vessel_001,41,shanghai_chn,16,138,140 +tlp1_vessel_001,41,qingdao_chn,12,149,151 +tlp1_vessel_001,41,pusan_kor,11,160,161 +tlp1_vessel_001,41,manzanillo_mex,5,167,168 +tlp1_vessel_001,41,sanAntonio_par,13,198,200 +tlp1_vessel_001,41,manzanillo_mex,5,220,221 +tlp1_vessel_001,41,yokohama_jpn,21,245,247 +tlp1_vessel_001,41,shanghai_chn,16,284,285 +tlp1_vessel_002,42,pusan_kor,11,0,2 +tlp1_vessel_002,42,manzanillo_mex,5,7,9 +tlp1_vessel_002,42,sanAntonio_par,13,36,38 +tlp1_vessel_002,42,manzanillo_mex,5,54,56 +tlp1_vessel_002,42,yokohama_jpn,21,76,77 +tlp1_vessel_002,42,shanghai_chn,16,103,104 +tlp1_vessel_002,42,yantian_chn,20,111,113 +tlp1_vessel_002,42,shanghai_chn,16,123,124 +tlp1_vessel_002,42,qingdao_chn,12,131,132 +tlp1_vessel_002,42,pusan_kor,11,139,141 +tlp1_vessel_002,42,manzanillo_mex,5,145,146 +tlp1_vessel_002,42,sanAntonio_par,13,171,172 +tlp1_vessel_002,42,manzanillo_mex,5,185,187 +tlp1_vessel_002,42,yokohama_jpn,21,204,206 +tlp1_vessel_002,42,shanghai_chn,16,234,235 +tlp1_vessel_002,42,yantian_chn,20,242,244 +tlp1_vessel_003,43,manzanillo_mex,5,0,2 +tlp1_vessel_003,43,sanAntonio_par,13,39,40 +tlp1_vessel_003,43,manzanillo_mex,5,59,61 +tlp1_vessel_003,43,yokohama_jpn,21,81,83 +tlp1_vessel_003,43,shanghai_chn,16,119,120 +tlp1_vessel_003,43,yantian_chn,20,126,128 +tlp1_vessel_003,43,shanghai_chn,16,138,140 +tlp1_vessel_003,43,qingdao_chn,12,146,148 +tlp1_vessel_003,43,pusan_kor,11,155,157 +tlp1_vessel_003,43,manzanillo_mex,5,163,165 +tlp1_vessel_003,43,sanAntonio_par,13,200,202 +tlp1_vessel_003,43,manzanillo_mex,5,217,218 +tlp1_vessel_003,43,yokohama_jpn,21,241,243 +tlp1_vessel_003,43,shanghai_chn,16,269,270 +tlp1_vessel_004,44,sanAntonio_par,13,0,2 +tlp1_vessel_004,44,manzanillo_mex,5,21,22 +tlp1_vessel_004,44,yokohama_jpn,21,43,45 +tlp1_vessel_004,44,shanghai_chn,16,76,77 +tlp1_vessel_004,44,yantian_chn,20,85,86 +tlp1_vessel_004,44,shanghai_chn,16,98,99 +tlp1_vessel_004,44,qingdao_chn,12,108,110 +tlp1_vessel_004,44,pusan_kor,11,119,120 +tlp1_vessel_004,44,manzanillo_mex,5,125,127 +tlp1_vessel_004,44,sanAntonio_par,13,161,162 +tlp1_vessel_004,44,manzanillo_mex,5,179,181 +tlp1_vessel_004,44,yokohama_jpn,21,203,204 +tlp1_vessel_004,44,shanghai_chn,16,235,237 +tlp1_vessel_004,44,yantian_chn,20,244,245 +tlp1_vessel_005,45,yokohama_jpn,21,0,1 +tlp1_vessel_005,45,shanghai_chn,16,23,24 +tlp1_vessel_005,45,yantian_chn,20,30,31 +tlp1_vessel_005,45,shanghai_chn,16,42,44 +tlp1_vessel_005,45,qingdao_chn,12,51,52 +tlp1_vessel_005,45,pusan_kor,11,59,61 +tlp1_vessel_005,45,manzanillo_mex,5,66,68 +tlp1_vessel_005,45,sanAntonio_par,13,94,96 +tlp1_vessel_005,45,manzanillo_mex,5,111,113 +tlp1_vessel_005,45,yokohama_jpn,21,129,131 +tlp1_vessel_005,45,shanghai_chn,16,155,157 +tlp1_vessel_005,45,yantian_chn,20,163,164 +tlp1_vessel_005,45,shanghai_chn,16,173,174 +tlp1_vessel_005,45,qingdao_chn,12,182,184 +tlp1_vessel_005,45,pusan_kor,11,191,193 +tlp1_vessel_005,45,manzanillo_mex,5,198,199 +tlp1_vessel_005,45,sanAntonio_par,13,223,224 +tlp1_vessel_005,45,manzanillo_mex,5,240,241 +tlp1_vessel_005,45,yokohama_jpn,21,257,259 diff --git a/tests/data/cim/case_data/dump_folder/vessels.csv b/tests/data/cim/case_data/dump_folder/vessels.csv new file mode 100644 index 000000000..bfa5ecc9c --- /dev/null +++ b/tests/data/cim/case_data/dump_folder/vessels.csv @@ -0,0 +1,47 @@ +index,name,capacity,route_name,route_index,start_port_name,start_port_index,sailing_speed,sailing_speed_noise,parking_duration,parking_noise,period,empty +0,a3s_vessel_000,871,a3s,0,yantian_chn,20,10,2,1,1,67,0 +1,a3s_vessel_001,967,a3s,0,sydney_aus,18,9,2,1,1,75,0 +2,asa_vessel_000,601,asa,1,singapore_sgp,17,8,1,1,1,84,0 +3,asa_vessel_001,493,asa,1,melbourne_aus,6,10,2,1,1,67,0 +4,ate1_vessel_000,5758,ate1,2,newYork_usa,8,9,1,1,1,53,0 +5,ate1_vessel_001,6333,ate1,2,bremerhaven_ger,0,8,1,1,1,58,0 +6,gex1_vessel_000,1033,gex1,3,bremerhaven_ger,0,10,2,1,1,51,0 +7,gex1_vessel_001,1147,gex1,3,montreal_can,7,9,1,1,1,58,0 +8,ktx5_vessel_000,1557,ktx5,4,singapore_sgp,17,8,1,1,1,61,0 +9,ktx5_vessel_001,1275,ktx5,4,yokohama_jpn,21,10,1,1,1,49,0 +10,ll4_vessel_000,6603,ll4,5,bremerhaven_ger,0,9,2,1,1,164,0 +11,ll4_vessel_001,7263,ll4,5,yantian_chn,20,8,2,1,1,182,0 +12,ll4_vessel_002,5943,ll4,5,singapore_sgp,17,10,1,1,1,146,0 +13,ll4_vessel_003,6603,ll4,5,leHavre_fra,3,9,1,1,1,164,0 +14,ll4_vessel_004,7263,ll4,5,pusan_kor,11,8,1,1,1,182,0 +15,ll4_vessel_005,5943,ll4,5,qingdao_chn,12,10,1,1,1,146,0 +16,pcc1_vessel_000,4922,pcc1,6,oakland_usa,9,9,1,1,1,90,0 +17,pcc1_vessel_001,5414,pcc1,6,qingdao_chn,12,8,2,1,1,98,0 +18,pcc1_vessel_002,4430,pcc1,6,princeRupert_can,10,10,1,1,1,79,0 +19,pcc2_vessel_000,2443,pcc2,7,seattle_usa,15,9,2,1,1,95,0 +20,pcc2_vessel_001,2687,pcc2,7,losAngeles_usa,4,8,2,1,1,104,0 +21,pcc2_vessel_002,2199,pcc2,7,shanghai_chn,16,10,2,1,1,84,0 +22,pnw1_vessel_000,2129,pnw1,8,seattle_usa,15,9,1,1,1,87,0 +23,pnw1_vessel_001,2341,pnw1,8,vancouver_can,19,8,1,1,1,97,0 +24,pnw1_vessel_002,1917,pnw1,8,yantian_chn,20,10,1,1,1,78,0 +25,pnw2_vessel_000,897,pnw2,9,pusan_kor,11,9,2,1,1,154,0 +26,pnw2_vessel_001,986,pnw2,9,yantian_chn,20,8,2,1,1,169,0 +27,pnw2_vessel_002,808,pnw2,9,vancouver_can,19,10,1,1,1,136,0 +28,pnw2_vessel_003,897,pnw2,9,pusan_kor,11,9,1,1,1,154,0 +29,pnw2_vessel_004,986,pnw2,9,qingdao_chn,12,8,1,1,1,169,0 +30,saf3_vessel_000,583,saf3,10,yantian_chn,20,10,1,1,1,94,0 +31,saf3_vessel_001,647,saf3,10,qingdao_chn,12,9,1,1,1,105,0 +32,saf3_vessel_002,711,saf3,10,singapore_sgp,17,8,2,1,1,117,0 +33,saf3_vessel_003,583,saf3,10,durban_sau,1,10,2,1,1,94,0 +34,tla2_vessel_000,1185,tla2,11,itagual_bra,2,9,1,1,1,189,0 +35,tla2_vessel_001,1303,tla2,11,santos_bra,14,8,1,1,1,210,0 +36,tla2_vessel_002,1067,tla2,11,singapore_sgp,17,10,2,1,1,167,0 +37,tla2_vessel_003,1185,tla2,11,shanghai_chn,16,9,1,1,1,189,0 +38,tla2_vessel_004,1303,tla2,11,qingdao_chn,12,8,2,1,1,210,0 +39,tla2_vessel_005,1067,tla2,11,yantian_chn,20,10,1,1,1,167,0 +40,tlp1_vessel_000,6332,tlp1,12,yantian_chn,20,9,2,1,1,141,0 +41,tlp1_vessel_001,6965,tlp1,12,manzanillo_mex,5,8,1,1,1,158,0 +42,tlp1_vessel_002,5699,tlp1,12,pusan_kor,11,10,1,1,1,125,0 +43,tlp1_vessel_003,6332,tlp1,12,manzanillo_mex,5,9,2,1,1,141,0 +44,tlp1_vessel_004,6965,tlp1,12,sanAntonio_par,13,8,1,1,1,158,0 +45,tlp1_vessel_005,5699,tlp1,12,yokohama_jpn,21,10,2,1,1,125,0 diff --git a/tests/data/cim/case_data/real_folder_bin/misc.yml b/tests/data/cim/case_data/real_folder_bin/misc.yml new file mode 100644 index 000000000..7b9a713ac --- /dev/null +++ b/tests/data/cim/case_data/real_folder_bin/misc.yml @@ -0,0 +1,7 @@ +max_tick: 224 +container_volume: 1 +past_stop_number: 4 +future_stop_number: 3 +seed: 4096 +dsch_cost_factor: 0.05 +load_cost_factor: 0.05 diff --git a/tests/data/cim/case_data/real_folder_bin/orders.bin b/tests/data/cim/case_data/real_folder_bin/orders.bin new file mode 100644 index 0000000000000000000000000000000000000000..d087fd8a56ce5407028b6b8ee45cd883deb54b22 GIT binary patch literal 14050 zcmb{2d6?Z*eaG<&O;`ddVnkL+WT}#{ssw@r0Z{}ai--{jVMr!5A(@2943G#lkra2f zDilkRN{b=|mqMUe7AKT|tP%u7mZDJ>g+f805zzN%df(;C(SQ0p{+@?(4)5Q2^v^nWaG-a= z#8KOi89TYBf8O4vQDesR%$Pm6U|?E*&%msC6UX!o&Y44;F|crc@5C{)c4OVwGneJS zthv1l272bsr}X!nIL$i!IKqNC^9Cl4*_qYAqIrGn9{y+kzZd(zYc1%XK5hQI{()(; z`eyW=^qLDUm^auzz4x_e|IAmP!RPtf3%vUI{_(j#^Tm%J?CBerHL&nCpLB_T`f@XR zXZ8%v8JO04LT}$d_of-X3H~?a9`HwX!Nf5~P90V6GPxgJyT+>}%e2d0O`WvA;J=qR zQML~6+qg9CCG2&dHh1RyyB=BZTbgzm-F@1VxOlGbk@Gt+*t9p&Ys>l>_KgrS$36zI zX)mDH7W+#!k7V|Uom`ssCc695V&^lg#q1IL94Bqs74+I-&tvo2Lgv`RP5U%^YRh$R;2P^T_RXbfH)G+xwAh0f>K!W|^II8g+85}x#ZF*YkJ%&k zWq?ilIK8&m=h?izkU92CrD<2w-Io@73d8G|Jz`gI(x%OAR9oza7+x=Aj$NO@rX8Z! z7W-RO%pS308Eo1$SkxAKKEsAW z=Gem-Y}yCtwZ#rGyouQ(HsAX;?fvxHVrMht2cSpndl_unE9texuFLRdA#?0A0Gsw| zdTp^Uvw0(CkJvnJ*tFNvYm2>^;VnYu*xbH0?eFNd#U9JBF|$YPmJBv+z6WcIeVkzv zA#?0X2AlR67PZAr;J&eGV-LpKw7Gq2i#-#&S!1{0giX6Oi`rt3#BSc$@tm+}-^HS~ z*wH-bZqe9fe9Sg&AB);z&*!>tZR~HbHtkQ@Q(Np&*ex5oDJN{&?OD_oy8=6=u`{`@ zO*@fAZLynR$2Rs^tPL`Jn~!2qTkOjWZ)@y6SerIKQ)-J{$*@&p-+;Agf5)EMV4ZOc zTQ_!HtWEoF7PZALXV|8(qp&vZbL^=tc1yli+cx%NoUmypv#2fh+t|Ns?A=_~rp?D( zTkK2Nw>LIF-)!2uS=1K0g!}V58halnY}(^l)D}B|ClT8<_G(Vpv@==M7P|;LuCbru zgiU)ii`rrz;A0-&*!*0zY4>MQTkIUJyM1Fn!3mr82o|-)PUgBhG}cE%s#Y8#_04SFBBY3wvsdJ(XdX#^!mVP5T&o zYK#2^!#f*$3)ZF`$D+2_`FzavcT89(kCQg-Bo?*BUV(jgbKMVe!lwNki`rs`xQ%vg z?3(<0Htl^ZYKz^I`_+WTj>X!vH?pWL_C)M%ja|eEoAx{wwZ(oOyL)5vyx68ajzw*; zKg3RK?8Th0X%ApgTkJD@t@dc_r#WHMp2VWI*eP81J&nzC2AejIIkm-(=em0~_I|8Q z`!aiKi~Tjjq{i-#wP}CGp4wsu8TM-I(*T?HHulsOJD1_TjqT;SHf??m))u=9!`{pu zu+9dhY1e1rzO>kpe698oGRNk*o=v+Si`ruM#qQhK(VVbpA7W7(TRLZ9-`ChYKHIcs zu&6C|26n&3uI7YIyCaL*VtcUfZ|ozSuxZD$s4ezX><1eAASZ0vvsu&@`$g>jjr|TM zY}${qs4eyx9wR>3*gZL6(_Y1*w%F-h_khNJlN;Hlox!5E*lAq%z{ckGwP`nCQCsXJ z?1vhA4cE14^K+rL*ozn@H}*iRO?wG@YKzVDx(_#YG}fkF#-7?@_hC4wvAM2IoA0CA zV%O&LJ-D&AVr|-8S=1JLH1?3jeg$jO=4V-Lu~V^!Hg+l2rrnlBZLv$SA8G7ZPS~{h zKB_Ht3HGqYzQ}cL+T5ROi_Q1%M;m)O)~4N#MQyRyaoxikdllBE-Izsfu~V=gYwYb< zoAwTF!3D((=!KU4bMQyS9xo|{K_lW%^`)t~^ zniiYi!HyI%$G*ZooAy$AZLvEt9L4Mr`zXMs&G%7lu>%ZKh0L*o3^wg*dTp`&3`aA2 z#I6L`w5#Z~#U8~lO~@R355T7FG%fZ64975g#OC$7W4b}R28-HYo$oUAboXV7&Cfg= z%i(qdb8WE~FdWP55&IDaoAy`q+G6)&m@Z_F?PsuQpQYCZ8-AWMgV`fCuesT@*U)Q= z{U7%A3YlYn0kCNwqSqFi-`{33d&KU9xh~!El_AId&0)P5WDV zZLu3O%whJ3JqKXZK1#1GHqVvk3YlXs2iUY%&})m$bJ0F#kJvpJY}#+pYm1%4Fi*%F zo7aMD+FR(g#h%SDpV=e!bqqG`bM)F`XEGcwWRA@nnl|mv>9xg9XXt14hXy@P!N%pS4%I@`35(`$>}pJ7nQ9J>jFP5TXcZLym$oWSf6 zo99LSgv_yPm!`d$?!L6x_b@DE_K3}MWt;YH zdTp^&85RkdV;3{nv=7s3i#>zklgu8md5pDbpQ6_mdl$pWLgv`quWZ^o>9xh?esv17 zM{K^2Y}&kzR9ozI41XMfY)fW3A!>5C~N9>{{5f8ao?n z)4sx<+G20Vp5539Sey1y_S6=;9`)<_ z@8j6Cd0(Tp*iHGE&uQ#qSetfB7PZCZJHJ+|zsCuiHqR4l zi|xgp*Vw#1Y18JuQCsX?*snHrDJN{&t69_*yWv`$&fhmS&l7Fhg)C}|9mJmB*!*0z zX{WQOE%vK?%>U5XyytJz=6#LYV)w&d(AeuaVbkuwqPEyAuwQFzeumn#Z)8zh?1#B; zT-ezB9feK1l0CJ>zQFL0%pS1L^#Gf88NIgHN7(#zA#?23rD?Zf;l8xk9)^pUJz_t} zVAH;dMQyO*$E$x5GRNM_VAGz+qPEyRzRv&L*zNfG*|f7*)E0XM_Tt7a;e<__*Pv>P zeLeTdJSX?a$DF^9uxXdFs4X_{<9t)d9NWR#v=_6eE%pTLCE4FSVpnm(rahfSZLuq{ z`CZQ=_FJ5=X)k3_TkKibONGp_>u|!ReJ6|BVsGQ?{4b4N$O)VFC>FKF&c zHtp}(Q(No^zNVKq_DrlzJBCGVv3Z{J?Z$qAeKze??5QpGNv`po#vWFh_5t>|FD>>k zhVM4^9;{89$LHE&^Zw=c8k^^sHtkpzwZ(o7`>&17*TAOzEQ{J=H{v$BqOo~ww`ub{ zr?%Kt*ee@*8YgVpc`Ry+J&pTh9>+ZRG7Z0vZPOmiqPEz*u(|C#V)MJWP5XHkwZ*Q7 zy;{f|djKbF+L<=3I9M-0Nf<3jxz7IRp*!+#5O`Gqd+G3x= zE^llehiuxsUshY}lh_*@`+KZSyB>?$VlT)3M`Lrl*|fQD)D}C0z3ER_n>N?2E%qGj zipJ(PvT1+Kp4wu^V{dM3em}Bl^D);JyAAf1#^!IfZQ57ZQ(J5v+ke>DZMm*Zo1Y7{ z#pdVqt&M#iVAI~kp4wtBVz{lbJzUqO&G%nzv3U>j_Qu{1uxazT))spp!yS#y-)!5o zd7P^)b|%A}joqA&*{1z5duoea%#g2x2duLiVAJNYuD00Y7=9#Vj?Lek*tGZ2Ym3eA zbayj*#2&(6)BcoRTkL3Tp3i&4=Ji;c_Fj5zv3c%&kB~Vwe>-T?uAEKB_Ht1w%d_kJ!BDWz+tOUR&(m4EY**#6AeHX3shvMop}D6wL+D_W6VXd8*z+U~6jMM#9r9-Cv1&CU!nXI59b7*d82 zks;B=AXy8-EGoOu{YKJ-?sp;4Br21l>pw>CbLRXyuUim-A3U7zJo~+KX2#Fo^!Hvo zFs|Iy)wNPk2Z*!NuB&VEZk=W@yN>Ri{UN*T$gWY6G%Iu4N!%)SA6uBxA2sW}L3I}j zQF%eTm6V!MQmGdUwRW{CSx!b8aj{V8QD2LO)FzdoxRpdh4Vh-Nu+*_pvsoFAOG)L* ztL0nOdQvRRP?s#O*Az-$?f*XW??(>DtyWwuRchsU(N0et^M_sh*hamX{L4dk(!2PP z)i}z(#r)eX$Bn3&w449&1()M_QM;Oy;^DZKWdAVfH{TKb`~SnXiiOMPbo;`vMKIo_ zdWI&LV`e`w$sFxuahN&g4j0qR(HVx~FT(?p*+=FSnb%+U^r+2)wLOho2r z=Zg8}m|Gwk=4hK@kvZl@#P#NAmx!BavSMzTxHX9EdYi7{zb}fpJH;w)tnO34#_pMW zP&{Ie_EGVaIp&@gFPfu$S-fG6xeel7bF}Y?Pt7s+x%kQ)?I!UJO;*f(D}D?jyZ)r> z@8+2M!|umrJ=Z<0XKpVs(H!mmg59FXFn6$+DyZ;!zH4D_y8b>RoyzA<(0z|NUhhZ0$Sx3^PgKYz>aR&np1oH0H<}~6OmG%a zA;lCcydyplRLFj&eiK54 z<2`Q{oG}y*z860WihJHOd$B`MII@3olNB?7A8(F(&YXSCk)J3IG)MLzai}@61#zS~ zvPX;K%#me(PBBNeN1SesY_B-i9NF{4EOTTBMA#u_$g)G>9Kr!R6$^@cvR8>AL4|Bh zJ$pojY+HTUtB_@{mNKLYS@!If)Z|%y1Me`$b61Lc%<-PtwbkZ$%~27~rtsXga*x>@ z*(XFeo0x;=#jAn}S@!SE)a37(vnrfTA^WjhIGaLtquiD;*{!<&!5p*O#V_XgyuW4l zT=oz+2>XZQbFhCC1cl@E*gy7-3fW2Or=+Ii$W9g0%#ocg_>H4N_C)oZPgHpBO!a4_ zrsK%=i3`o~e)~n(wUA}k_>H4*WCsPiMd8TK7Y#v$Ea%rqYVz!2-QQr2>{7AZ9G{mx zTVampvS(qh!gE*2joKVp&aj8gkzFU&o8!4piWkh0eMxLEN0yy>-yGQw#24nsvQwMQ Tk=-J`Ge>rt2s;$MD|YBFMVCy2 literal 0 HcmV?d00001 diff --git a/tests/data/cim/case_data/real_folder_bin/vessels.csv b/tests/data/cim/case_data/real_folder_bin/vessels.csv new file mode 100644 index 000000000..0507805ba --- /dev/null +++ b/tests/data/cim/case_data/real_folder_bin/vessels.csv @@ -0,0 +1,6 @@ +index,name,capacity,route_name,route_index,start_port_name,start_port_index,sailing_speed,sailing_speed_noise,parking_duration,parking_noise,period,empty +0,rt1_vessel_001,10395,route_001,0,supply_port_001,2,10,2,1,1,14,0 +1,rt1_vessel_002,11550,route_001,0,demand_port_001,0,9,2,1,1,16,0 +2,rt2_vessel_001,25795,route_002,1,supply_port_001,2,8,1,1,1,27,0 +3,rt2_vessel_002,21105,route_002,1,supply_port_002,3,10,2,1,1,21,0 +4,rt2_vessel_003,23450,route_002,1,demand_port_002,1,9,1,1,1,24,0 diff --git a/tests/data/cim/case_data/real_folder_csv/misc.yml b/tests/data/cim/case_data/real_folder_csv/misc.yml new file mode 100644 index 000000000..7b9a713ac --- /dev/null +++ b/tests/data/cim/case_data/real_folder_csv/misc.yml @@ -0,0 +1,7 @@ +max_tick: 224 +container_volume: 1 +past_stop_number: 4 +future_stop_number: 3 +seed: 4096 +dsch_cost_factor: 0.05 +load_cost_factor: 0.05 diff --git a/tests/data/cim/case_data/real_folder_csv/orders.csv b/tests/data/cim/case_data/real_folder_csv/orders.csv new file mode 100644 index 000000000..02a559874 --- /dev/null +++ b/tests/data/cim/case_data/real_folder_csv/orders.csv @@ -0,0 +1,673 @@ +tick,source_port_name,source_port_index,dest_port_name,dest_port_index,quantity +0,demand_port_001,0,supply_port_001,2,445 +0,demand_port_002,1,supply_port_001,2,173 +0,demand_port_002,1,supply_port_002,3,764 +1,demand_port_001,0,supply_port_001,2,536 +1,demand_port_002,1,supply_port_001,2,169 +1,demand_port_002,1,supply_port_002,3,672 +2,demand_port_001,0,supply_port_001,2,579 +2,demand_port_002,1,supply_port_001,2,182 +2,demand_port_002,1,supply_port_002,3,888 +3,demand_port_001,0,supply_port_001,2,558 +3,demand_port_002,1,supply_port_001,2,196 +3,demand_port_002,1,supply_port_002,3,752 +4,demand_port_001,0,supply_port_001,2,487 +4,demand_port_002,1,supply_port_001,2,158 +4,demand_port_002,1,supply_port_002,3,742 +5,demand_port_001,0,supply_port_001,2,589 +5,demand_port_002,1,supply_port_001,2,198 +5,demand_port_002,1,supply_port_002,3,880 +6,demand_port_001,0,supply_port_001,2,499 +6,demand_port_002,1,supply_port_001,2,199 +6,demand_port_002,1,supply_port_002,3,665 +7,demand_port_001,0,supply_port_001,2,556 +7,demand_port_002,1,supply_port_001,2,220 +7,demand_port_002,1,supply_port_002,3,822 +8,demand_port_001,0,supply_port_001,2,429 +8,demand_port_002,1,supply_port_001,2,238 +8,demand_port_002,1,supply_port_002,3,704 +9,demand_port_001,0,supply_port_001,2,533 +9,demand_port_002,1,supply_port_001,2,283 +9,demand_port_002,1,supply_port_002,3,847 +10,demand_port_001,0,supply_port_001,2,542 +10,demand_port_002,1,supply_port_001,2,247 +10,demand_port_002,1,supply_port_002,3,823 +11,demand_port_001,0,supply_port_001,2,509 +11,demand_port_002,1,supply_port_001,2,232 +11,demand_port_002,1,supply_port_002,3,756 +12,demand_port_001,0,supply_port_001,2,653 +12,demand_port_002,1,supply_port_001,2,250 +12,demand_port_002,1,supply_port_002,3,893 +13,demand_port_001,0,supply_port_001,2,455 +13,demand_port_002,1,supply_port_001,2,199 +13,demand_port_002,1,supply_port_002,3,844 +14,demand_port_001,0,supply_port_001,2,524 +14,demand_port_002,1,supply_port_001,2,194 +14,demand_port_002,1,supply_port_002,3,1014 +15,demand_port_001,0,supply_port_001,2,607 +15,demand_port_002,1,supply_port_001,2,232 +15,demand_port_002,1,supply_port_002,3,858 +16,demand_port_001,0,supply_port_001,2,542 +16,demand_port_002,1,supply_port_001,2,233 +16,demand_port_002,1,supply_port_002,3,1087 +17,demand_port_001,0,supply_port_001,2,545 +17,demand_port_002,1,supply_port_001,2,257 +17,demand_port_002,1,supply_port_002,3,924 +18,demand_port_001,0,supply_port_001,2,598 +18,demand_port_002,1,supply_port_001,2,218 +18,demand_port_002,1,supply_port_002,3,962 +19,demand_port_001,0,supply_port_001,2,579 +19,demand_port_002,1,supply_port_001,2,216 +19,demand_port_002,1,supply_port_002,3,874 +20,demand_port_001,0,supply_port_001,2,576 +20,demand_port_002,1,supply_port_001,2,181 +20,demand_port_002,1,supply_port_002,3,777 +21,demand_port_001,0,supply_port_001,2,496 +21,demand_port_002,1,supply_port_001,2,184 +21,demand_port_002,1,supply_port_002,3,765 +22,demand_port_001,0,supply_port_001,2,487 +22,demand_port_002,1,supply_port_001,2,191 +22,demand_port_002,1,supply_port_002,3,968 +23,demand_port_001,0,supply_port_001,2,607 +23,demand_port_002,1,supply_port_001,2,251 +23,demand_port_002,1,supply_port_002,3,866 +24,demand_port_001,0,supply_port_001,2,543 +24,demand_port_002,1,supply_port_001,2,283 +24,demand_port_002,1,supply_port_002,3,1000 +25,demand_port_001,0,supply_port_001,2,723 +25,demand_port_002,1,supply_port_001,2,352 +25,demand_port_002,1,supply_port_002,3,1079 +26,demand_port_001,0,supply_port_001,2,592 +26,demand_port_002,1,supply_port_001,2,218 +26,demand_port_002,1,supply_port_002,3,1168 +27,demand_port_001,0,supply_port_001,2,797 +27,demand_port_002,1,supply_port_001,2,292 +27,demand_port_002,1,supply_port_002,3,1115 +28,demand_port_001,0,supply_port_001,2,812 +28,demand_port_002,1,supply_port_001,2,308 +28,demand_port_002,1,supply_port_002,3,1300 +29,demand_port_001,0,supply_port_001,2,694 +29,demand_port_002,1,supply_port_001,2,366 +29,demand_port_002,1,supply_port_002,3,1180 +30,demand_port_001,0,supply_port_001,2,757 +30,demand_port_002,1,supply_port_001,2,219 +30,demand_port_002,1,supply_port_002,3,1116 +31,demand_port_001,0,supply_port_001,2,794 +31,demand_port_002,1,supply_port_001,2,301 +31,demand_port_002,1,supply_port_002,3,1223 +32,demand_port_001,0,supply_port_001,2,614 +32,demand_port_002,1,supply_port_001,2,314 +32,demand_port_002,1,supply_port_002,3,1049 +33,demand_port_001,0,supply_port_001,2,753 +33,demand_port_002,1,supply_port_001,2,266 +33,demand_port_002,1,supply_port_002,3,1021 +34,demand_port_001,0,supply_port_001,2,578 +34,demand_port_002,1,supply_port_001,2,220 +34,demand_port_002,1,supply_port_002,3,979 +35,demand_port_001,0,supply_port_001,2,527 +35,demand_port_002,1,supply_port_001,2,251 +35,demand_port_002,1,supply_port_002,3,811 +36,demand_port_001,0,supply_port_001,2,521 +36,demand_port_002,1,supply_port_001,2,309 +36,demand_port_002,1,supply_port_002,3,963 +37,demand_port_001,0,supply_port_001,2,522 +37,demand_port_002,1,supply_port_001,2,243 +37,demand_port_002,1,supply_port_002,3,1055 +38,demand_port_001,0,supply_port_001,2,855 +38,demand_port_002,1,supply_port_001,2,333 +38,demand_port_002,1,supply_port_002,3,1199 +39,demand_port_001,0,supply_port_001,2,722 +39,demand_port_002,1,supply_port_001,2,309 +39,demand_port_002,1,supply_port_002,3,1276 +40,demand_port_001,0,supply_port_001,2,823 +40,demand_port_002,1,supply_port_001,2,466 +40,demand_port_002,1,supply_port_002,3,1409 +41,demand_port_001,0,supply_port_001,2,983 +41,demand_port_002,1,supply_port_001,2,369 +41,demand_port_002,1,supply_port_002,3,1591 +42,demand_port_001,0,supply_port_001,2,952 +42,demand_port_002,1,supply_port_001,2,358 +42,demand_port_002,1,supply_port_002,3,1658 +43,demand_port_001,0,supply_port_001,2,895 +43,demand_port_002,1,supply_port_001,2,350 +43,demand_port_002,1,supply_port_002,3,1498 +44,demand_port_001,0,supply_port_001,2,893 +44,demand_port_002,1,supply_port_001,2,328 +44,demand_port_002,1,supply_port_002,3,1644 +45,demand_port_001,0,supply_port_001,2,872 +45,demand_port_002,1,supply_port_001,2,346 +45,demand_port_002,1,supply_port_002,3,1357 +46,demand_port_001,0,supply_port_001,2,762 +46,demand_port_002,1,supply_port_001,2,352 +46,demand_port_002,1,supply_port_002,3,1389 +47,demand_port_001,0,supply_port_001,2,750 +47,demand_port_002,1,supply_port_001,2,284 +47,demand_port_002,1,supply_port_002,3,1012 +48,demand_port_001,0,supply_port_001,2,625 +48,demand_port_002,1,supply_port_001,2,258 +48,demand_port_002,1,supply_port_002,3,1148 +49,demand_port_001,0,supply_port_001,2,566 +49,demand_port_002,1,supply_port_001,2,201 +49,demand_port_002,1,supply_port_002,3,899 +50,demand_port_001,0,supply_port_001,2,665 +50,demand_port_002,1,supply_port_001,2,231 +50,demand_port_002,1,supply_port_002,3,997 +51,demand_port_001,0,supply_port_001,2,713 +51,demand_port_002,1,supply_port_001,2,299 +51,demand_port_002,1,supply_port_002,3,1136 +52,demand_port_001,0,supply_port_001,2,757 +52,demand_port_002,1,supply_port_001,2,318 +52,demand_port_002,1,supply_port_002,3,1204 +53,demand_port_001,0,supply_port_001,2,841 +53,demand_port_002,1,supply_port_001,2,393 +53,demand_port_002,1,supply_port_002,3,1474 +54,demand_port_001,0,supply_port_001,2,1026 +54,demand_port_002,1,supply_port_001,2,471 +54,demand_port_002,1,supply_port_002,3,1597 +55,demand_port_001,0,supply_port_001,2,1057 +55,demand_port_002,1,supply_port_001,2,452 +55,demand_port_002,1,supply_port_002,3,1655 +56,demand_port_001,0,supply_port_001,2,890 +56,demand_port_002,1,supply_port_001,2,409 +56,demand_port_002,1,supply_port_002,3,1674 +57,demand_port_001,0,supply_port_001,2,847 +57,demand_port_002,1,supply_port_001,2,363 +57,demand_port_002,1,supply_port_002,3,1738 +58,demand_port_001,0,supply_port_001,2,934 +58,demand_port_002,1,supply_port_001,2,330 +58,demand_port_002,1,supply_port_002,3,1520 +59,demand_port_001,0,supply_port_001,2,898 +59,demand_port_002,1,supply_port_001,2,376 +59,demand_port_002,1,supply_port_002,3,1624 +60,demand_port_001,0,supply_port_001,2,723 +60,demand_port_002,1,supply_port_001,2,369 +60,demand_port_002,1,supply_port_002,3,1324 +61,demand_port_001,0,supply_port_001,2,728 +61,demand_port_002,1,supply_port_001,2,253 +61,demand_port_002,1,supply_port_002,3,1004 +62,demand_port_001,0,supply_port_001,2,584 +62,demand_port_002,1,supply_port_001,2,226 +62,demand_port_002,1,supply_port_002,3,885 +63,demand_port_001,0,supply_port_001,2,494 +63,demand_port_002,1,supply_port_001,2,204 +63,demand_port_002,1,supply_port_002,3,877 +64,demand_port_001,0,supply_port_001,2,613 +64,demand_port_002,1,supply_port_001,2,220 +64,demand_port_002,1,supply_port_002,3,818 +65,demand_port_001,0,supply_port_001,2,784 +65,demand_port_002,1,supply_port_001,2,268 +65,demand_port_002,1,supply_port_002,3,1285 +66,demand_port_001,0,supply_port_001,2,866 +66,demand_port_002,1,supply_port_001,2,325 +66,demand_port_002,1,supply_port_002,3,1347 +67,demand_port_001,0,supply_port_001,2,788 +67,demand_port_002,1,supply_port_001,2,477 +67,demand_port_002,1,supply_port_002,3,1425 +68,demand_port_001,0,supply_port_001,2,893 +68,demand_port_002,1,supply_port_001,2,399 +68,demand_port_002,1,supply_port_002,3,1380 +69,demand_port_001,0,supply_port_001,2,1018 +69,demand_port_002,1,supply_port_001,2,303 +69,demand_port_002,1,supply_port_002,3,1377 +70,demand_port_001,0,supply_port_001,2,991 +70,demand_port_002,1,supply_port_001,2,300 +70,demand_port_002,1,supply_port_002,3,1411 +71,demand_port_001,0,supply_port_001,2,988 +71,demand_port_002,1,supply_port_001,2,402 +71,demand_port_002,1,supply_port_002,3,1420 +72,demand_port_001,0,supply_port_001,2,944 +72,demand_port_002,1,supply_port_001,2,341 +72,demand_port_002,1,supply_port_002,3,1264 +73,demand_port_001,0,supply_port_001,2,829 +73,demand_port_002,1,supply_port_001,2,439 +73,demand_port_002,1,supply_port_002,3,1379 +74,demand_port_001,0,supply_port_001,2,680 +74,demand_port_002,1,supply_port_001,2,356 +74,demand_port_002,1,supply_port_002,3,1119 +75,demand_port_001,0,supply_port_001,2,788 +75,demand_port_002,1,supply_port_001,2,272 +75,demand_port_002,1,supply_port_002,3,1086 +76,demand_port_001,0,supply_port_001,2,698 +76,demand_port_002,1,supply_port_001,2,231 +76,demand_port_002,1,supply_port_002,3,931 +77,demand_port_001,0,supply_port_001,2,587 +77,demand_port_002,1,supply_port_001,2,169 +77,demand_port_002,1,supply_port_002,3,811 +78,demand_port_001,0,supply_port_001,2,532 +78,demand_port_002,1,supply_port_001,2,182 +78,demand_port_002,1,supply_port_002,3,834 +79,demand_port_001,0,supply_port_001,2,587 +79,demand_port_002,1,supply_port_001,2,268 +79,demand_port_002,1,supply_port_002,3,1031 +80,demand_port_001,0,supply_port_001,2,715 +80,demand_port_002,1,supply_port_001,2,310 +80,demand_port_002,1,supply_port_002,3,1118 +81,demand_port_001,0,supply_port_001,2,661 +81,demand_port_002,1,supply_port_001,2,318 +81,demand_port_002,1,supply_port_002,3,1117 +82,demand_port_001,0,supply_port_001,2,644 +82,demand_port_002,1,supply_port_001,2,294 +82,demand_port_002,1,supply_port_002,3,1156 +83,demand_port_001,0,supply_port_001,2,801 +83,demand_port_002,1,supply_port_001,2,303 +83,demand_port_002,1,supply_port_002,3,1153 +84,demand_port_001,0,supply_port_001,2,760 +84,demand_port_002,1,supply_port_001,2,341 +84,demand_port_002,1,supply_port_002,3,1380 +85,demand_port_001,0,supply_port_001,2,654 +85,demand_port_002,1,supply_port_001,2,298 +85,demand_port_002,1,supply_port_002,3,1214 +86,demand_port_001,0,supply_port_001,2,695 +86,demand_port_002,1,supply_port_001,2,280 +86,demand_port_002,1,supply_port_002,3,1112 +87,demand_port_001,0,supply_port_001,2,717 +87,demand_port_002,1,supply_port_001,2,297 +87,demand_port_002,1,supply_port_002,3,1106 +88,demand_port_001,0,supply_port_001,2,552 +88,demand_port_002,1,supply_port_001,2,206 +88,demand_port_002,1,supply_port_002,3,979 +89,demand_port_001,0,supply_port_001,2,530 +89,demand_port_002,1,supply_port_001,2,304 +89,demand_port_002,1,supply_port_002,3,877 +90,demand_port_001,0,supply_port_001,2,486 +90,demand_port_002,1,supply_port_001,2,262 +90,demand_port_002,1,supply_port_002,3,753 +91,demand_port_001,0,supply_port_001,2,510 +91,demand_port_002,1,supply_port_001,2,171 +91,demand_port_002,1,supply_port_002,3,815 +92,demand_port_001,0,supply_port_001,2,481 +92,demand_port_002,1,supply_port_001,2,182 +92,demand_port_002,1,supply_port_002,3,884 +93,demand_port_001,0,supply_port_001,2,629 +93,demand_port_002,1,supply_port_001,2,250 +93,demand_port_002,1,supply_port_002,3,882 +94,demand_port_001,0,supply_port_001,2,467 +94,demand_port_002,1,supply_port_001,2,228 +94,demand_port_002,1,supply_port_002,3,860 +95,demand_port_001,0,supply_port_001,2,468 +95,demand_port_002,1,supply_port_001,2,256 +95,demand_port_002,1,supply_port_002,3,839 +96,demand_port_001,0,supply_port_001,2,653 +96,demand_port_002,1,supply_port_001,2,257 +96,demand_port_002,1,supply_port_002,3,945 +97,demand_port_001,0,supply_port_001,2,477 +97,demand_port_002,1,supply_port_001,2,192 +97,demand_port_002,1,supply_port_002,3,926 +98,demand_port_001,0,supply_port_001,2,595 +98,demand_port_002,1,supply_port_001,2,235 +98,demand_port_002,1,supply_port_002,3,831 +99,demand_port_001,0,supply_port_001,2,626 +99,demand_port_002,1,supply_port_001,2,241 +99,demand_port_002,1,supply_port_002,3,1004 +100,demand_port_001,0,supply_port_001,2,545 +100,demand_port_002,1,supply_port_001,2,186 +100,demand_port_002,1,supply_port_002,3,761 +101,demand_port_001,0,supply_port_001,2,485 +101,demand_port_002,1,supply_port_001,2,221 +101,demand_port_002,1,supply_port_002,3,895 +102,demand_port_001,0,supply_port_001,2,560 +102,demand_port_002,1,supply_port_001,2,200 +102,demand_port_002,1,supply_port_002,3,961 +103,demand_port_001,0,supply_port_001,2,484 +103,demand_port_002,1,supply_port_001,2,187 +103,demand_port_002,1,supply_port_002,3,804 +104,demand_port_001,0,supply_port_001,2,568 +104,demand_port_002,1,supply_port_001,2,148 +104,demand_port_002,1,supply_port_002,3,757 +105,demand_port_001,0,supply_port_001,2,504 +105,demand_port_002,1,supply_port_001,2,174 +105,demand_port_002,1,supply_port_002,3,710 +106,demand_port_001,0,supply_port_001,2,455 +106,demand_port_002,1,supply_port_001,2,213 +106,demand_port_002,1,supply_port_002,3,827 +107,demand_port_001,0,supply_port_001,2,634 +107,demand_port_002,1,supply_port_001,2,246 +107,demand_port_002,1,supply_port_002,3,792 +108,demand_port_001,0,supply_port_001,2,406 +108,demand_port_002,1,supply_port_001,2,225 +108,demand_port_002,1,supply_port_002,3,728 +109,demand_port_001,0,supply_port_001,2,430 +109,demand_port_002,1,supply_port_001,2,180 +109,demand_port_002,1,supply_port_002,3,818 +110,demand_port_001,0,supply_port_001,2,571 +110,demand_port_002,1,supply_port_001,2,170 +110,demand_port_002,1,supply_port_002,3,830 +111,demand_port_001,0,supply_port_001,2,578 +111,demand_port_002,1,supply_port_001,2,201 +111,demand_port_002,1,supply_port_002,3,914 +112,demand_port_001,0,supply_port_001,2,525 +112,demand_port_002,1,supply_port_001,2,243 +112,demand_port_002,1,supply_port_002,3,870 +113,demand_port_001,0,supply_port_001,2,541 +113,demand_port_002,1,supply_port_001,2,227 +113,demand_port_002,1,supply_port_002,3,867 +114,demand_port_001,0,supply_port_001,2,497 +114,demand_port_002,1,supply_port_001,2,245 +114,demand_port_002,1,supply_port_002,3,825 +115,demand_port_001,0,supply_port_001,2,554 +115,demand_port_002,1,supply_port_001,2,181 +115,demand_port_002,1,supply_port_002,3,718 +116,demand_port_001,0,supply_port_001,2,571 +116,demand_port_002,1,supply_port_001,2,232 +116,demand_port_002,1,supply_port_002,3,840 +117,demand_port_001,0,supply_port_001,2,537 +117,demand_port_002,1,supply_port_001,2,167 +117,demand_port_002,1,supply_port_002,3,793 +118,demand_port_001,0,supply_port_001,2,532 +118,demand_port_002,1,supply_port_001,2,192 +118,demand_port_002,1,supply_port_002,3,892 +119,demand_port_001,0,supply_port_001,2,494 +119,demand_port_002,1,supply_port_001,2,195 +119,demand_port_002,1,supply_port_002,3,713 +120,demand_port_001,0,supply_port_001,2,519 +120,demand_port_002,1,supply_port_001,2,200 +120,demand_port_002,1,supply_port_002,3,828 +121,demand_port_001,0,supply_port_001,2,439 +121,demand_port_002,1,supply_port_001,2,210 +121,demand_port_002,1,supply_port_002,3,861 +122,demand_port_001,0,supply_port_001,2,640 +122,demand_port_002,1,supply_port_001,2,222 +122,demand_port_002,1,supply_port_002,3,911 +123,demand_port_001,0,supply_port_001,2,545 +123,demand_port_002,1,supply_port_001,2,237 +123,demand_port_002,1,supply_port_002,3,976 +124,demand_port_001,0,supply_port_001,2,497 +124,demand_port_002,1,supply_port_001,2,207 +124,demand_port_002,1,supply_port_002,3,825 +125,demand_port_001,0,supply_port_001,2,578 +125,demand_port_002,1,supply_port_001,2,284 +125,demand_port_002,1,supply_port_002,3,958 +126,demand_port_001,0,supply_port_001,2,663 +126,demand_port_002,1,supply_port_001,2,221 +126,demand_port_002,1,supply_port_002,3,976 +127,demand_port_001,0,supply_port_001,2,490 +127,demand_port_002,1,supply_port_001,2,233 +127,demand_port_002,1,supply_port_002,3,857 +128,demand_port_001,0,supply_port_001,2,706 +128,demand_port_002,1,supply_port_001,2,194 +128,demand_port_002,1,supply_port_002,3,941 +129,demand_port_001,0,supply_port_001,2,592 +129,demand_port_002,1,supply_port_001,2,328 +129,demand_port_002,1,supply_port_002,3,1016 +130,demand_port_001,0,supply_port_001,2,605 +130,demand_port_002,1,supply_port_001,2,214 +130,demand_port_002,1,supply_port_002,3,766 +131,demand_port_001,0,supply_port_001,2,555 +131,demand_port_002,1,supply_port_001,2,290 +131,demand_port_002,1,supply_port_002,3,869 +132,demand_port_001,0,supply_port_001,2,474 +132,demand_port_002,1,supply_port_001,2,252 +132,demand_port_002,1,supply_port_002,3,864 +133,demand_port_001,0,supply_port_001,2,416 +133,demand_port_002,1,supply_port_001,2,205 +133,demand_port_002,1,supply_port_002,3,772 +134,demand_port_001,0,supply_port_001,2,637 +134,demand_port_002,1,supply_port_001,2,252 +134,demand_port_002,1,supply_port_002,3,890 +135,demand_port_001,0,supply_port_001,2,566 +135,demand_port_002,1,supply_port_001,2,248 +135,demand_port_002,1,supply_port_002,3,768 +136,demand_port_001,0,supply_port_001,2,674 +136,demand_port_002,1,supply_port_001,2,250 +136,demand_port_002,1,supply_port_002,3,990 +137,demand_port_001,0,supply_port_001,2,752 +137,demand_port_002,1,supply_port_001,2,298 +137,demand_port_002,1,supply_port_002,3,1158 +138,demand_port_001,0,supply_port_001,2,751 +138,demand_port_002,1,supply_port_001,2,344 +138,demand_port_002,1,supply_port_002,3,1223 +139,demand_port_001,0,supply_port_001,2,750 +139,demand_port_002,1,supply_port_001,2,242 +139,demand_port_002,1,supply_port_002,3,1149 +140,demand_port_001,0,supply_port_001,2,729 +140,demand_port_002,1,supply_port_001,2,337 +140,demand_port_002,1,supply_port_002,3,1231 +141,demand_port_001,0,supply_port_001,2,856 +141,demand_port_002,1,supply_port_001,2,261 +141,demand_port_002,1,supply_port_002,3,1206 +142,demand_port_001,0,supply_port_001,2,850 +142,demand_port_002,1,supply_port_001,2,313 +142,demand_port_002,1,supply_port_002,3,1191 +143,demand_port_001,0,supply_port_001,2,856 +143,demand_port_002,1,supply_port_001,2,264 +143,demand_port_002,1,supply_port_002,3,1175 +144,demand_port_001,0,supply_port_001,2,621 +144,demand_port_002,1,supply_port_001,2,309 +144,demand_port_002,1,supply_port_002,3,1030 +145,demand_port_001,0,supply_port_001,2,618 +145,demand_port_002,1,supply_port_001,2,254 +145,demand_port_002,1,supply_port_002,3,1229 +146,demand_port_001,0,supply_port_001,2,567 +146,demand_port_002,1,supply_port_001,2,225 +146,demand_port_002,1,supply_port_002,3,1035 +147,demand_port_001,0,supply_port_001,2,516 +147,demand_port_002,1,supply_port_001,2,237 +147,demand_port_002,1,supply_port_002,3,752 +148,demand_port_001,0,supply_port_001,2,567 +148,demand_port_002,1,supply_port_001,2,247 +148,demand_port_002,1,supply_port_002,3,864 +149,demand_port_001,0,supply_port_001,2,722 +149,demand_port_002,1,supply_port_001,2,350 +149,demand_port_002,1,supply_port_002,3,1050 +150,demand_port_001,0,supply_port_001,2,743 +150,demand_port_002,1,supply_port_001,2,287 +150,demand_port_002,1,supply_port_002,3,1115 +151,demand_port_001,0,supply_port_001,2,707 +151,demand_port_002,1,supply_port_001,2,314 +151,demand_port_002,1,supply_port_002,3,1328 +152,demand_port_001,0,supply_port_001,2,946 +152,demand_port_002,1,supply_port_001,2,430 +152,demand_port_002,1,supply_port_002,3,1381 +153,demand_port_001,0,supply_port_001,2,784 +153,demand_port_002,1,supply_port_001,2,309 +153,demand_port_002,1,supply_port_002,3,1488 +154,demand_port_001,0,supply_port_001,2,900 +154,demand_port_002,1,supply_port_001,2,440 +154,demand_port_002,1,supply_port_002,3,1554 +155,demand_port_001,0,supply_port_001,2,914 +155,demand_port_002,1,supply_port_001,2,377 +155,demand_port_002,1,supply_port_002,3,1653 +156,demand_port_001,0,supply_port_001,2,825 +156,demand_port_002,1,supply_port_001,2,355 +156,demand_port_002,1,supply_port_002,3,1434 +157,demand_port_001,0,supply_port_001,2,777 +157,demand_port_002,1,supply_port_001,2,299 +157,demand_port_002,1,supply_port_002,3,1349 +158,demand_port_001,0,supply_port_001,2,959 +158,demand_port_002,1,supply_port_001,2,302 +158,demand_port_002,1,supply_port_002,3,1309 +159,demand_port_001,0,supply_port_001,2,706 +159,demand_port_002,1,supply_port_001,2,273 +159,demand_port_002,1,supply_port_002,3,1102 +160,demand_port_001,0,supply_port_001,2,598 +160,demand_port_002,1,supply_port_001,2,211 +160,demand_port_002,1,supply_port_002,3,1015 +161,demand_port_001,0,supply_port_001,2,447 +161,demand_port_002,1,supply_port_001,2,182 +161,demand_port_002,1,supply_port_002,3,735 +162,demand_port_001,0,supply_port_001,2,548 +162,demand_port_002,1,supply_port_001,2,291 +162,demand_port_002,1,supply_port_002,3,865 +163,demand_port_001,0,supply_port_001,2,635 +163,demand_port_002,1,supply_port_001,2,275 +163,demand_port_002,1,supply_port_002,3,1175 +164,demand_port_001,0,supply_port_001,2,726 +164,demand_port_002,1,supply_port_001,2,375 +164,demand_port_002,1,supply_port_002,3,1390 +165,demand_port_001,0,supply_port_001,2,1066 +165,demand_port_002,1,supply_port_001,2,361 +165,demand_port_002,1,supply_port_002,3,1460 +166,demand_port_001,0,supply_port_001,2,897 +166,demand_port_002,1,supply_port_001,2,297 +166,demand_port_002,1,supply_port_002,3,1550 +167,demand_port_001,0,supply_port_001,2,1079 +167,demand_port_002,1,supply_port_001,2,388 +167,demand_port_002,1,supply_port_002,3,1738 +168,demand_port_001,0,supply_port_001,2,1024 +168,demand_port_002,1,supply_port_001,2,422 +168,demand_port_002,1,supply_port_002,3,1654 +169,demand_port_001,0,supply_port_001,2,996 +169,demand_port_002,1,supply_port_001,2,398 +169,demand_port_002,1,supply_port_002,3,1747 +170,demand_port_001,0,supply_port_001,2,938 +170,demand_port_002,1,supply_port_001,2,427 +170,demand_port_002,1,supply_port_002,3,1681 +171,demand_port_001,0,supply_port_001,2,776 +171,demand_port_002,1,supply_port_001,2,307 +171,demand_port_002,1,supply_port_002,3,1484 +172,demand_port_001,0,supply_port_001,2,889 +172,demand_port_002,1,supply_port_001,2,348 +172,demand_port_002,1,supply_port_002,3,1386 +173,demand_port_001,0,supply_port_001,2,736 +173,demand_port_002,1,supply_port_001,2,255 +173,demand_port_002,1,supply_port_002,3,1028 +174,demand_port_001,0,supply_port_001,2,656 +174,demand_port_002,1,supply_port_001,2,288 +174,demand_port_002,1,supply_port_002,3,990 +175,demand_port_001,0,supply_port_001,2,395 +175,demand_port_002,1,supply_port_001,2,183 +175,demand_port_002,1,supply_port_002,3,746 +176,demand_port_001,0,supply_port_001,2,596 +176,demand_port_002,1,supply_port_001,2,218 +176,demand_port_002,1,supply_port_002,3,852 +177,demand_port_001,0,supply_port_001,2,724 +177,demand_port_002,1,supply_port_001,2,309 +177,demand_port_002,1,supply_port_002,3,1057 +178,demand_port_001,0,supply_port_001,2,772 +178,demand_port_002,1,supply_port_001,2,289 +178,demand_port_002,1,supply_port_002,3,1183 +179,demand_port_001,0,supply_port_001,2,990 +179,demand_port_002,1,supply_port_001,2,392 +179,demand_port_002,1,supply_port_002,3,1302 +180,demand_port_001,0,supply_port_001,2,914 +180,demand_port_002,1,supply_port_001,2,313 +180,demand_port_002,1,supply_port_002,3,1508 +181,demand_port_001,0,supply_port_001,2,902 +181,demand_port_002,1,supply_port_001,2,367 +181,demand_port_002,1,supply_port_002,3,1670 +182,demand_port_001,0,supply_port_001,2,997 +182,demand_port_002,1,supply_port_001,2,336 +182,demand_port_002,1,supply_port_002,3,1599 +183,demand_port_001,0,supply_port_001,2,840 +183,demand_port_002,1,supply_port_001,2,394 +183,demand_port_002,1,supply_port_002,3,1547 +184,demand_port_001,0,supply_port_001,2,842 +184,demand_port_002,1,supply_port_001,2,358 +184,demand_port_002,1,supply_port_002,3,1617 +185,demand_port_001,0,supply_port_001,2,721 +185,demand_port_002,1,supply_port_001,2,276 +185,demand_port_002,1,supply_port_002,3,1368 +186,demand_port_001,0,supply_port_001,2,761 +186,demand_port_002,1,supply_port_001,2,361 +186,demand_port_002,1,supply_port_002,3,1110 +187,demand_port_001,0,supply_port_001,2,574 +187,demand_port_002,1,supply_port_001,2,290 +187,demand_port_002,1,supply_port_002,3,1067 +188,demand_port_001,0,supply_port_001,2,524 +188,demand_port_002,1,supply_port_001,2,278 +188,demand_port_002,1,supply_port_002,3,949 +189,demand_port_001,0,supply_port_001,2,552 +189,demand_port_002,1,supply_port_001,2,194 +189,demand_port_002,1,supply_port_002,3,803 +190,demand_port_001,0,supply_port_001,2,562 +190,demand_port_002,1,supply_port_001,2,298 +190,demand_port_002,1,supply_port_002,3,1029 +191,demand_port_001,0,supply_port_001,2,549 +191,demand_port_002,1,supply_port_001,2,279 +191,demand_port_002,1,supply_port_002,3,1033 +192,demand_port_001,0,supply_port_001,2,641 +192,demand_port_002,1,supply_port_001,2,305 +192,demand_port_002,1,supply_port_002,3,1073 +193,demand_port_001,0,supply_port_001,2,755 +193,demand_port_002,1,supply_port_001,2,233 +193,demand_port_002,1,supply_port_002,3,1092 +194,demand_port_001,0,supply_port_001,2,765 +194,demand_port_002,1,supply_port_001,2,313 +194,demand_port_002,1,supply_port_002,3,1261 +195,demand_port_001,0,supply_port_001,2,746 +195,demand_port_002,1,supply_port_001,2,268 +195,demand_port_002,1,supply_port_002,3,1258 +196,demand_port_001,0,supply_port_001,2,767 +196,demand_port_002,1,supply_port_001,2,267 +196,demand_port_002,1,supply_port_002,3,1198 +197,demand_port_001,0,supply_port_001,2,803 +197,demand_port_002,1,supply_port_001,2,278 +197,demand_port_002,1,supply_port_002,3,1218 +198,demand_port_001,0,supply_port_001,2,803 +198,demand_port_002,1,supply_port_001,2,267 +198,demand_port_002,1,supply_port_002,3,1174 +199,demand_port_001,0,supply_port_001,2,641 +199,demand_port_002,1,supply_port_001,2,236 +199,demand_port_002,1,supply_port_002,3,1068 +200,demand_port_001,0,supply_port_001,2,621 +200,demand_port_002,1,supply_port_001,2,278 +200,demand_port_002,1,supply_port_002,3,1061 +201,demand_port_001,0,supply_port_001,2,499 +201,demand_port_002,1,supply_port_001,2,254 +201,demand_port_002,1,supply_port_002,3,852 +202,demand_port_001,0,supply_port_001,2,550 +202,demand_port_002,1,supply_port_001,2,218 +202,demand_port_002,1,supply_port_002,3,938 +203,demand_port_001,0,supply_port_001,2,500 +203,demand_port_002,1,supply_port_001,2,208 +203,demand_port_002,1,supply_port_002,3,931 +204,demand_port_001,0,supply_port_001,2,609 +204,demand_port_002,1,supply_port_001,2,189 +204,demand_port_002,1,supply_port_002,3,801 +205,demand_port_001,0,supply_port_001,2,472 +205,demand_port_002,1,supply_port_001,2,201 +205,demand_port_002,1,supply_port_002,3,843 +206,demand_port_001,0,supply_port_001,2,509 +206,demand_port_002,1,supply_port_001,2,185 +206,demand_port_002,1,supply_port_002,3,870 +207,demand_port_001,0,supply_port_001,2,540 +207,demand_port_002,1,supply_port_001,2,213 +207,demand_port_002,1,supply_port_002,3,896 +208,demand_port_001,0,supply_port_001,2,506 +208,demand_port_002,1,supply_port_001,2,290 +208,demand_port_002,1,supply_port_002,3,875 +209,demand_port_001,0,supply_port_001,2,584 +209,demand_port_002,1,supply_port_001,2,215 +209,demand_port_002,1,supply_port_002,3,1115 +210,demand_port_001,0,supply_port_001,2,593 +210,demand_port_002,1,supply_port_001,2,219 +210,demand_port_002,1,supply_port_002,3,1044 +211,demand_port_001,0,supply_port_001,2,494 +211,demand_port_002,1,supply_port_001,2,214 +211,demand_port_002,1,supply_port_002,3,865 +212,demand_port_001,0,supply_port_001,2,466 +212,demand_port_002,1,supply_port_001,2,228 +212,demand_port_002,1,supply_port_002,3,814 +213,demand_port_001,0,supply_port_001,2,539 +213,demand_port_002,1,supply_port_001,2,236 +213,demand_port_002,1,supply_port_002,3,967 +214,demand_port_001,0,supply_port_001,2,626 +214,demand_port_002,1,supply_port_001,2,235 +214,demand_port_002,1,supply_port_002,3,833 +215,demand_port_001,0,supply_port_001,2,476 +215,demand_port_002,1,supply_port_001,2,247 +215,demand_port_002,1,supply_port_002,3,847 +216,demand_port_001,0,supply_port_001,2,466 +216,demand_port_002,1,supply_port_001,2,206 +216,demand_port_002,1,supply_port_002,3,886 +217,demand_port_001,0,supply_port_001,2,459 +217,demand_port_002,1,supply_port_001,2,198 +217,demand_port_002,1,supply_port_002,3,879 +218,demand_port_001,0,supply_port_001,2,521 +218,demand_port_002,1,supply_port_001,2,234 +218,demand_port_002,1,supply_port_002,3,739 +219,demand_port_001,0,supply_port_001,2,519 +219,demand_port_002,1,supply_port_001,2,256 +219,demand_port_002,1,supply_port_002,3,867 +220,demand_port_001,0,supply_port_001,2,482 +220,demand_port_002,1,supply_port_001,2,180 +220,demand_port_002,1,supply_port_002,3,715 +221,demand_port_001,0,supply_port_001,2,538 +221,demand_port_002,1,supply_port_001,2,213 +221,demand_port_002,1,supply_port_002,3,795 +222,demand_port_001,0,supply_port_001,2,451 +222,demand_port_002,1,supply_port_001,2,197 +222,demand_port_002,1,supply_port_002,3,863 +223,demand_port_001,0,supply_port_001,2,445 +223,demand_port_002,1,supply_port_001,2,194 +223,demand_port_002,1,supply_port_002,3,762 diff --git a/tests/data/cim/case_data/real_folder_csv/ports.csv b/tests/data/cim/case_data/real_folder_csv/ports.csv new file mode 100644 index 000000000..cfb96fcab --- /dev/null +++ b/tests/data/cim/case_data/real_folder_csv/ports.csv @@ -0,0 +1,5 @@ +index,name,capacity,empty,empty_return_buffer,empty_return_buffer_noise,full_return_buffer,full_return_buffer_noise +0,demand_port_001,100000,25000,1,1,1,1 +1,demand_port_002,100000,25000,1,1,1,1 +2,supply_port_001,1000000,25000,1,1,1,1 +3,supply_port_002,100000,25000,1,1,1,1 diff --git a/tests/data/cim/case_data/real_folder_csv/routes.csv b/tests/data/cim/case_data/real_folder_csv/routes.csv new file mode 100644 index 000000000..5c1ca39e4 --- /dev/null +++ b/tests/data/cim/case_data/real_folder_csv/routes.csv @@ -0,0 +1,6 @@ +index,name,port_name,port_index,distance_to_next_port +0,route_001,supply_port_001,2,60 +0,route_001,demand_port_001,0,60 +1,route_002,supply_port_001,2,60 +1,route_002,supply_port_002,3,60 +1,route_002,demand_port_002,1,60 diff --git a/tests/data/cim/case_data/real_folder_csv/stops.csv b/tests/data/cim/case_data/real_folder_csv/stops.csv new file mode 100644 index 000000000..ce7e02610 --- /dev/null +++ b/tests/data/cim/case_data/real_folder_csv/stops.csv @@ -0,0 +1,150 @@ +vessel_name,vessel_index,port_name,port_index,arrival_tick,departure_tick +rt1_vessel_001,0,supply_port_001,2,0,2 +rt1_vessel_001,0,demand_port_001,0,8,10 +rt1_vessel_001,0,supply_port_001,2,16,18 +rt1_vessel_001,0,demand_port_001,0,25,27 +rt1_vessel_001,0,supply_port_001,2,35,36 +rt1_vessel_001,0,demand_port_001,0,42,44 +rt1_vessel_001,0,supply_port_001,2,52,53 +rt1_vessel_001,0,demand_port_001,0,59,61 +rt1_vessel_001,0,supply_port_001,2,68,70 +rt1_vessel_001,0,demand_port_001,0,78,79 +rt1_vessel_001,0,supply_port_001,2,87,89 +rt1_vessel_001,0,demand_port_001,0,97,98 +rt1_vessel_001,0,supply_port_001,2,104,106 +rt1_vessel_001,0,demand_port_001,0,112,114 +rt1_vessel_001,0,supply_port_001,2,120,121 +rt1_vessel_001,0,demand_port_001,0,127,128 +rt1_vessel_001,0,supply_port_001,2,134,135 +rt1_vessel_001,0,demand_port_001,0,141,143 +rt1_vessel_001,0,supply_port_001,2,149,151 +rt1_vessel_001,0,demand_port_001,0,157,158 +rt1_vessel_001,0,supply_port_001,2,165,167 +rt1_vessel_001,0,demand_port_001,0,175,176 +rt1_vessel_001,0,supply_port_001,2,183,184 +rt1_vessel_001,0,demand_port_001,0,191,193 +rt1_vessel_001,0,supply_port_001,2,199,200 +rt1_vessel_001,0,demand_port_001,0,207,208 +rt1_vessel_001,0,supply_port_001,2,215,217 +rt1_vessel_001,0,demand_port_001,0,223,224 +rt1_vessel_001,0,supply_port_001,2,231,233 +rt1_vessel_001,0,demand_port_001,0,240,241 +rt1_vessel_001,0,supply_port_001,2,247,248 +rt1_vessel_002,1,demand_port_001,0,0,1 +rt1_vessel_002,1,supply_port_001,2,7,9 +rt1_vessel_002,1,demand_port_001,0,17,19 +rt1_vessel_002,1,supply_port_001,2,27,28 +rt1_vessel_002,1,demand_port_001,0,34,36 +rt1_vessel_002,1,supply_port_001,2,42,43 +rt1_vessel_002,1,demand_port_001,0,49,50 +rt1_vessel_002,1,supply_port_001,2,56,57 +rt1_vessel_002,1,demand_port_001,0,64,65 +rt1_vessel_002,1,supply_port_001,2,73,74 +rt1_vessel_002,1,demand_port_001,0,83,85 +rt1_vessel_002,1,supply_port_001,2,93,94 +rt1_vessel_002,1,demand_port_001,0,101,102 +rt1_vessel_002,1,supply_port_001,2,110,111 +rt1_vessel_002,1,demand_port_001,0,119,120 +rt1_vessel_002,1,supply_port_001,2,126,128 +rt1_vessel_002,1,demand_port_001,0,136,138 +rt1_vessel_002,1,supply_port_001,2,145,146 +rt1_vessel_002,1,demand_port_001,0,153,154 +rt1_vessel_002,1,supply_port_001,2,161,162 +rt1_vessel_002,1,demand_port_001,0,170,172 +rt1_vessel_002,1,supply_port_001,2,179,181 +rt1_vessel_002,1,demand_port_001,0,187,188 +rt1_vessel_002,1,supply_port_001,2,196,197 +rt1_vessel_002,1,demand_port_001,0,203,204 +rt1_vessel_002,1,supply_port_001,2,211,212 +rt1_vessel_002,1,demand_port_001,0,219,220 +rt1_vessel_002,1,supply_port_001,2,228,229 +rt1_vessel_002,1,demand_port_001,0,236,238 +rt1_vessel_002,1,supply_port_001,2,245,247 +rt2_vessel_001,2,supply_port_001,2,0,2 +rt2_vessel_001,2,supply_port_002,3,10,12 +rt2_vessel_001,2,demand_port_002,1,19,20 +rt2_vessel_001,2,supply_port_001,2,28,29 +rt2_vessel_001,2,supply_port_002,3,37,39 +rt2_vessel_001,2,demand_port_002,1,48,49 +rt2_vessel_001,2,supply_port_001,2,57,58 +rt2_vessel_001,2,supply_port_002,3,66,68 +rt2_vessel_001,2,demand_port_002,1,76,77 +rt2_vessel_001,2,supply_port_001,2,85,87 +rt2_vessel_001,2,supply_port_002,3,95,96 +rt2_vessel_001,2,demand_port_002,1,104,106 +rt2_vessel_001,2,supply_port_001,2,114,116 +rt2_vessel_001,2,supply_port_002,3,124,125 +rt2_vessel_001,2,demand_port_002,1,133,135 +rt2_vessel_001,2,supply_port_001,2,143,145 +rt2_vessel_001,2,supply_port_002,3,154,156 +rt2_vessel_001,2,demand_port_002,1,165,166 +rt2_vessel_001,2,supply_port_001,2,174,175 +rt2_vessel_001,2,supply_port_002,3,184,185 +rt2_vessel_001,2,demand_port_002,1,194,196 +rt2_vessel_001,2,supply_port_001,2,204,205 +rt2_vessel_001,2,supply_port_002,3,214,216 +rt2_vessel_001,2,demand_port_002,1,224,226 +rt2_vessel_001,2,supply_port_001,2,233,235 +rt2_vessel_001,2,supply_port_002,3,243,245 +rt2_vessel_001,2,demand_port_002,1,253,254 +rt2_vessel_002,3,supply_port_002,3,0,1 +rt2_vessel_002,3,demand_port_002,1,8,9 +rt2_vessel_002,3,supply_port_001,2,15,17 +rt2_vessel_002,3,supply_port_002,3,23,24 +rt2_vessel_002,3,demand_port_002,1,30,32 +rt2_vessel_002,3,supply_port_001,2,38,40 +rt2_vessel_002,3,supply_port_002,3,46,48 +rt2_vessel_002,3,demand_port_002,1,55,56 +rt2_vessel_002,3,supply_port_001,2,63,65 +rt2_vessel_002,3,supply_port_002,3,71,72 +rt2_vessel_002,3,demand_port_002,1,78,79 +rt2_vessel_002,3,supply_port_001,2,85,87 +rt2_vessel_002,3,supply_port_002,3,93,95 +rt2_vessel_002,3,demand_port_002,1,101,103 +rt2_vessel_002,3,supply_port_001,2,109,110 +rt2_vessel_002,3,supply_port_002,3,116,117 +rt2_vessel_002,3,demand_port_002,1,124,125 +rt2_vessel_002,3,supply_port_001,2,131,133 +rt2_vessel_002,3,supply_port_002,3,139,141 +rt2_vessel_002,3,demand_port_002,1,147,149 +rt2_vessel_002,3,supply_port_001,2,155,156 +rt2_vessel_002,3,supply_port_002,3,162,164 +rt2_vessel_002,3,demand_port_002,1,170,172 +rt2_vessel_002,3,supply_port_001,2,178,180 +rt2_vessel_002,3,supply_port_002,3,187,189 +rt2_vessel_002,3,demand_port_002,1,195,196 +rt2_vessel_002,3,supply_port_001,2,202,203 +rt2_vessel_002,3,supply_port_002,3,211,213 +rt2_vessel_002,3,demand_port_002,1,219,220 +rt2_vessel_002,3,supply_port_001,2,227,229 +rt2_vessel_002,3,supply_port_002,3,237,238 +rt2_vessel_002,3,demand_port_002,1,244,246 +rt2_vessel_003,4,demand_port_002,1,0,1 +rt2_vessel_003,4,supply_port_001,2,8,9 +rt2_vessel_003,4,supply_port_002,3,16,17 +rt2_vessel_003,4,demand_port_002,1,24,25 +rt2_vessel_003,4,supply_port_001,2,33,34 +rt2_vessel_003,4,supply_port_002,3,42,43 +rt2_vessel_003,4,demand_port_002,1,50,51 +rt2_vessel_003,4,supply_port_001,2,58,59 +rt2_vessel_003,4,supply_port_002,3,67,68 +rt2_vessel_003,4,demand_port_002,1,75,77 +rt2_vessel_003,4,supply_port_001,2,85,87 +rt2_vessel_003,4,supply_port_002,3,94,96 +rt2_vessel_003,4,demand_port_002,1,103,104 +rt2_vessel_003,4,supply_port_001,2,112,114 +rt2_vessel_003,4,supply_port_002,3,121,122 +rt2_vessel_003,4,demand_port_002,1,129,131 +rt2_vessel_003,4,supply_port_001,2,138,139 +rt2_vessel_003,4,supply_port_002,3,146,147 +rt2_vessel_003,4,demand_port_002,1,155,157 +rt2_vessel_003,4,supply_port_001,2,164,166 +rt2_vessel_003,4,supply_port_002,3,173,174 +rt2_vessel_003,4,demand_port_002,1,181,182 +rt2_vessel_003,4,supply_port_001,2,190,192 +rt2_vessel_003,4,supply_port_002,3,200,202 +rt2_vessel_003,4,demand_port_002,1,209,210 +rt2_vessel_003,4,supply_port_001,2,218,219 +rt2_vessel_003,4,supply_port_002,3,226,227 +rt2_vessel_003,4,demand_port_002,1,234,236 +rt2_vessel_003,4,supply_port_001,2,244,246 diff --git a/tests/data/cim/case_data/real_folder_csv/vessels.csv b/tests/data/cim/case_data/real_folder_csv/vessels.csv new file mode 100644 index 000000000..0507805ba --- /dev/null +++ b/tests/data/cim/case_data/real_folder_csv/vessels.csv @@ -0,0 +1,6 @@ +index,name,capacity,route_name,route_index,start_port_name,start_port_index,sailing_speed,sailing_speed_noise,parking_duration,parking_noise,period,empty +0,rt1_vessel_001,10395,route_001,0,supply_port_001,2,10,2,1,1,14,0 +1,rt1_vessel_002,11550,route_001,0,demand_port_001,0,9,2,1,1,16,0 +2,rt2_vessel_001,25795,route_002,1,supply_port_001,2,8,1,1,1,27,0 +3,rt2_vessel_002,21105,route_002,1,supply_port_002,3,10,2,1,1,21,0 +4,rt2_vessel_003,23450,route_002,1,demand_port_002,1,9,1,1,1,24,0 diff --git a/tests/data/cim/customized_config/config.yml b/tests/data/cim/customized_config/config.yml index fcd2a83df..c5e756889 100644 --- a/tests/data/cim/customized_config/config.yml +++ b/tests/data/cim/customized_config/config.yml @@ -1,8 +1,7 @@ seed: 123 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/tests/data/cim/data_generator/dumps/config.yml b/tests/data/cim/data_generator/dumps/config.yml index 18a9a7c18..93cdd1601 100644 --- a/tests/data/cim/data_generator/dumps/config.yml +++ b/tests/data/cim/data_generator/dumps/config.yml @@ -1,7 +1,6 @@ seed: 123 -transfer_cost_factors: - load: 0.05 - dsch: 0.05 +load_cost_factor: 0.05 +dsch_cost_factor: 0.05 container_usage_proportion: period: 112 sample_nodes: diff --git a/tests/dummy/dummy_business_engine.py b/tests/dummy/dummy_business_engine.py index fca3e9c5e..fd635863b 100644 --- a/tests/dummy/dummy_business_engine.py +++ b/tests/dummy/dummy_business_engine.py @@ -45,7 +45,7 @@ def post_step(self, tick:int): return tick+1 == self._max_tick - def reset(self): + def reset(self, keep_seed: bool = False): self._frame.reset() self._frame.snapshots.reset() diff --git a/tests/test_event_buffer.py b/tests/test_event_buffer.py index 11e123605..9d549321d 100644 --- a/tests/test_event_buffer.py +++ b/tests/test_event_buffer.py @@ -1,15 +1,122 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - - +import os +import tempfile +import time import unittest -from maro.event_buffer import EventBuffer, AtomEvent, CascadeEvent, EventState +from typing import Optional + +from maro.event_buffer import ActualEvent, AtomEvent, CascadeEvent, DummyEvent, EventBuffer, EventState, MaroEvents +from maro.event_buffer.event_linked_list import EventLinkedList +from maro.event_buffer.event_pool import EventPool class TestEventBuffer(unittest.TestCase): def setUp(self): self.eb = EventBuffer() + def test_cascade_event(self): + evt = CascadeEvent(0, 1, None, None) + self.assertEqual(type(evt.immediate_event_head), DummyEvent) + self.assertIsNone(evt.immediate_event_head.next_event) + self.assertIsNone(evt.immediate_event_tail) + self.assertEqual(evt.immediate_event_head.next_event, evt.immediate_event_tail) + self.assertEqual(evt.immediate_event_count, 0) + + evt.add_immediate_event(AtomEvent(1, 1, None, None), is_head=False) + evt.add_immediate_event(AtomEvent(2, 1, None, None), is_head=False) + evt.add_immediate_event(AtomEvent(3, 1, None, None), is_head=True) + evt.add_immediate_event(AtomEvent(4, 1, None, None), is_head=True) + evt.add_immediate_event(AtomEvent(5, 1, None, None), is_head=False) + evt.add_immediate_event(AtomEvent(6, 1, None, None), is_head=False) + evt.add_immediate_event(AtomEvent(7, 1, None, None), is_head=True) + evt.add_immediate_event(AtomEvent(8, 1, None, None), is_head=True) + self.assertEqual(evt.immediate_event_count, 8) + + # Should be declined because the tick of the events are not equal + self.assertTrue(not evt.add_immediate_event(AtomEvent(9, 2, None, None))) + + iter_evt: Optional[ActualEvent] = evt.immediate_event_head.next_event + event_ids = [] + while iter_evt is not None: + event_ids.append(iter_evt.id) + iter_evt = iter_evt.next_event + self.assertListEqual(event_ids, [8, 7, 4, 3, 1, 2, 5, 6]) + + evt.clear() + self.assertIsNone(evt.immediate_event_head.next_event) + self.assertIsNone(evt.immediate_event_tail) + self.assertEqual(evt.immediate_event_head.next_event, evt.immediate_event_tail) + self.assertEqual(evt.immediate_event_count, 0) + + def test_event_linked_list(self): + event_linked_list = EventLinkedList() + self.assertEqual(len(event_linked_list), 0) + self.assertListEqual([evt for evt in event_linked_list], []) + + evt_list = [CascadeEvent(i, None, None, None) for i in range(7)] + evt_list[0].add_immediate_event(evt_list[3]) + evt_list[0].add_immediate_event(evt_list[4]) + evt_list[0].add_immediate_event(evt_list[5]) + evt_list[0].add_immediate_event(evt_list[6]) + + event_linked_list.append_head(evt_list[1]) + event_linked_list.append_tail(evt_list[2]) + event_linked_list.append_head(evt_list[0]) + self.assertEqual(len(event_linked_list), 3) + + event_ids = [event.id for event in event_linked_list] + self.assertListEqual(event_ids, [0, 1, 2]) + + evt = event_linked_list.clear_finished_and_get_front() + self.assertEqual(evt.id, 0) + + # Test `_clear_finished_events()` + evt_list[0].state = EventState.FINISHED + evt = event_linked_list.clear_finished_and_get_front() + self.assertIsInstance(evt, ActualEvent) + self.assertEqual(evt.id, 3) + self.assertEqual(len(event_linked_list), 6) + + self.assertListEqual([evt.id for evt in event_linked_list], [3, 4, 5, 6, 1, 2]) + + evt_list[3].event_type = MaroEvents.PENDING_DECISION + evt_list[4].event_type = MaroEvents.PENDING_DECISION + evt_list[5].event_type = MaroEvents.PENDING_DECISION + evts = event_linked_list.clear_finished_and_get_front() + self.assertTrue(all(isinstance(evt, ActualEvent) for evt in evts)) + self.assertEqual(len(evts), 3) + self.assertListEqual([evt.id for evt in evts], [3, 4, 5]) + self.assertListEqual([evt.id for evt in event_linked_list], [3, 4, 5, 6, 1, 2]) + + event_linked_list.clear() + self.assertEqual(len(event_linked_list), 0) + self.assertListEqual([evt for evt in event_linked_list], []) + + def test_event_pool(self): + ep = EventPool() + cascade_events = [CascadeEvent(i, None, None, None) for i in range(5)] + atom_events = [AtomEvent(i, None, None, None) for i in range(5, 10)] + + for evt in cascade_events: + ep.recycle(evt) + ep.recycle(atom_events) + + self.assertEqual(ep.atom_event_count, 5) + self.assertEqual(ep.cascade_event_count, 5) + + for i in range(5): + is_cascade = i % 2 == 0 + evt = ep.gen(tick=i, event_type=-1, payload=-1, is_cascade=is_cascade) + self.assertEqual(evt.id, i) + self.assertEqual(evt.tick, i) + self.assertEqual(evt.event_type, -1) + self.assertEqual(evt.payload, -1) + self.assertIsInstance(evt, CascadeEvent if is_cascade else AtomEvent) + + self.assertEqual(ep.atom_event_count, 3) + self.assertEqual(ep.cascade_event_count, 2) + def test_gen_event(self): """Test event generating correct""" evt = self.eb.gen_atom_event(1, 1, (0, 0)) @@ -45,11 +152,11 @@ def test_event_dispatch(self): def cb(evt): # test event tick self.assertEqual( - 1, evt.tick, msg="recieved event tick should be 1") + 1, evt.tick, msg="received event tick should be 1") # test event payload self.assertTupleEqual( - (1, 3), evt.payload, msg="recieved event's payload should be (1, 3)") + (1, 3), evt.payload, msg="received event's payload should be (1, 3)") evt = self.eb.gen_atom_event(1, 1, (1, 3)) @@ -62,7 +169,7 @@ def cb(evt): def test_get_finish_events(self): """Test if we can get correct finished events""" - # no finised at first + # no finished at first self.assertListEqual([], self.eb.get_finished_events(), msg="finished pool should be empty") @@ -74,7 +181,7 @@ def test_get_finish_events(self): # after dispatching, finish pool should contains 1 object self.assertEqual(1, len(self.eb.get_finished_events()), - msg="after dispathing, there should 1 object") + msg="after dispatching, there should 1 object") def test_get_pending_events(self): """Test if we can get correct pending events""" @@ -142,7 +249,7 @@ def test_sub_events_with_decision(self): self.assertEqual(1, len(decision_events)) self.assertEqual(evt1, decision_events[0]) - # mark decision event as executing to make it process folloing events + # mark decision event as executing to make it process following events decision_events[0].state = EventState.FINISHED # then there will be 2 additional decision event from sub events @@ -152,6 +259,57 @@ def test_sub_events_with_decision(self): self.assertEqual(sub1, decision_events[0]) self.assertEqual(sub2, decision_events[1]) + def test_disable_finished_events(self): + eb = EventBuffer(disable_finished_events=True) + self.assertListEqual([], eb.get_finished_events(), msg="finished pool should be empty") + + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.execute(1) + + # after dispatching, finish pool should still contains no object + self.assertListEqual([], eb.get_finished_events(), msg="finished pool should be empty") + + def test_record_events(self): + timestamp = str(time.time()).replace(".", "_") + temp_file_path = f'{tempfile.gettempdir()}/{timestamp}.txt' + + try: + EventBuffer(record_events=True, record_path=None) + self.assertTrue(False) + except ValueError: + pass + + eb = EventBuffer(record_events=True, record_path=temp_file_path) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.execute(1) + eb.reset() + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.insert_event(eb.gen_atom_event(1, 1, (1, 3))) + eb.execute(1) + del eb + + with open(temp_file_path, "r") as input_stream: + texts = input_stream.readlines() + self.assertListEqual(texts, [ + 'episode,tick,event_type,payload\n', + '0,1,1,"(1, 3)"\n', + '0,1,1,"(1, 3)"\n', + '0,1,1,"(1, 3)"\n', + '0,1,1,"(1, 3)"\n', + '1,1,1,"(1, 3)"\n', + '1,1,1,"(1, 3)"\n', + '1,1,1,"(1, 3)"\n', + '1,1,1,"(1, 3)"\n' + ]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 33fde23f5..587525cc1 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -147,7 +147,7 @@ def test_slice_quering(self): self.assertListEqual(list(states[0].astype("i")), [100 * i + 1 for i in range( len(frame.static_nodes))], msg="a2 at tick 1 for all nodes should be correct") - # 2nd row should be lastest one + # 2nd row should be latest one self.assertEqual( 1000, states[1][0], msg="a2 for 1st static node for 2nd row should be 1000") diff --git a/tests/utils.py b/tests/utils.py index 63d0142e5..5e76a594e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,6 +7,7 @@ backends_to_test = ["static", "dynamic"] + def next_step(eb: EventBuffer, be: AbsBusinessEngine, tick: int): if tick > 0: # lets post process last tick first before start a new tick @@ -38,3 +39,30 @@ def be_run_to_end(eb, be): while not is_done: is_done = next_step(eb, be, tick) tick += 1 + + +def compare_list(list1: list, list2: list) -> bool: + return len(list1) == len(list2) and all(val1 == val2 for val1, val2 in zip(list1, list2)) + + +def compare_dictionary(dict1: dict, dict2: dict) -> bool: + keys1 = sorted(list(dict1.keys())) + keys2 = sorted(list(dict2.keys())) + if not compare_list(keys1, keys2): + return False + + for key in keys1: + value1 = dict1[key] + value2 = dict2[key] + if type(value1) != type(value2): + return False + if type(value1) == dict: + if not compare_dictionary(value1, value2): + return False + elif type(value1) == list: + if not compare_list(value1, value2): + return False + else: + if value1 != value2: + return False + return True From 8a25f9eaf8b595a8a52679753412728943bd1314 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Mon, 24 Jan 2022 10:58:52 +0800 Subject: [PATCH 465/482] Change `Env.set_seed()` logic (#456) * Change Env.set_seed() logic * Redesign CIM reset logic; fix lint issues; * Lint * Seed type assertion --- maro/README.rst | 264 +++++++++--------- .../cim/cim_data_container_helpers.py | 21 +- maro/simulator/core.py | 6 +- .../scenarios/abs_business_engine.py | 4 + .../scenarios/cim/business_engine.py | 3 + .../scenarios/citi_bike/business_engine.py | 3 + .../scenarios/supply_chain/business_engine.py | 3 + .../vm_scheduling/business_engine.py | 3 + 8 files changed, 167 insertions(+), 140 deletions(-) diff --git a/maro/README.rst b/maro/README.rst index a63d78a50..dfbe33c30 100644 --- a/maro/README.rst +++ b/maro/README.rst @@ -1,144 +1,144 @@ .. image:: https://img.shields.io/pypi/l/pymaro - :target: https://github.com/microsoft/maro/blob/master/LICENSE - :alt: License + :target: https://github.com/microsoft/maro/blob/master/LICENSE + :alt: License .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/platform.svg - :target: https://pypi.org/project/pymaro/ - :alt: Platform + :target: https://pypi.org/project/pymaro/ + :alt: Platform .. image:: https://img.shields.io/pypi/pyversions/pymaro.svg?logo=python&logoColor=white - :target: https://pypi.org/project/pymaro/#files - :alt: Python Versions + :target: https://pypi.org/project/pymaro/#files + :alt: Python Versions .. image:: https://img.shields.io/github/languages/code-size/microsoft/maro - :target: https://github.com/microsoft/maro - :alt: Code Size + :target: https://github.com/microsoft/maro + :alt: Code Size .. image:: https://img.shields.io/docker/image-size/maro2020/maro - :target: https://hub.docker.com/repository/docker/maro2020/maro/tags?page=1 - :alt: Docker Size + :target: https://hub.docker.com/repository/docker/maro2020/maro/tags?page=1 + :alt: Docker Size .. image:: https://img.shields.io/github/issues/microsoft/maro - :target: https://github.com/microsoft/maro/issues - :alt: Issues + :target: https://github.com/microsoft/maro/issues + :alt: Issues .. image:: https://img.shields.io/github/issues-pr/microsoft/maro - :target: https://github.com/microsoft/maro/pulls - :alt: Pull Requests + :target: https://github.com/microsoft/maro/pulls + :alt: Pull Requests .. image:: https://img.shields.io/librariesio/github/microsoft/maro - :target: https://libraries.io/pypi/pymaro - :alt: Dependencies + :target: https://libraries.io/pypi/pymaro + :alt: Dependencies .. image:: https://github.com/microsoft/maro/workflows/test/badge.svg - :target: https://github.com/microsoft/maro/actions?query=workflow%3Atest - :alt: test + :target: https://github.com/microsoft/maro/actions?query=workflow%3Atest + :alt: test .. image:: https://github.com/microsoft/maro/workflows/build/badge.svg - :target: https://github.com/microsoft/maro/actions?query=workflow%3Abuild - :alt: build + :target: https://github.com/microsoft/maro/actions?query=workflow%3Abuild + :alt: build .. image:: https://github.com/microsoft/maro/workflows/docker/badge.svg - :target: https://hub.docker.com/repository/docker/maro2020/maro - :alt: docker + :target: https://hub.docker.com/repository/docker/maro2020/maro + :alt: docker .. image:: https://readthedocs.org/projects/maro/badge/?version=latest - :target: https://maro.readthedocs.io/ - :alt: docs + :target: https://maro.readthedocs.io/ + :alt: docs .. image:: https://img.shields.io/pypi/v/pymaro - :target: https://pypi.org/project/pymaro/#files - :alt: PypI Versions + :target: https://pypi.org/project/pymaro/#files + :alt: PypI Versions .. image:: https://img.shields.io/pypi/wheel/pymaro - :target: https://pypi.org/project/pymaro/#files - :alt: Wheel + :target: https://pypi.org/project/pymaro/#files + :alt: Wheel .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/citi_bike.svg - :target: https://maro.readthedocs.io/en/latest/scenarios/citi_bike.html - :alt: Citi Bike + :target: https://maro.readthedocs.io/en/latest/scenarios/citi_bike.html + :alt: Citi Bike .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/cim.svg - :target: https://maro.readthedocs.io/en/latest/scenarios/container_inventory_management.html - :alt: CIM + :target: https://maro.readthedocs.io/en/latest/scenarios/container_inventory_management.html + :alt: CIM .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/vm_scheduling.svg - :target: https://maro.readthedocs.io/en/latest/scenarios/vm_scheduling.html - :alt: VM Scheduling + :target: https://maro.readthedocs.io/en/latest/scenarios/vm_scheduling.html + :alt: VM Scheduling .. image:: https://img.shields.io/gitter/room/microsoft/maro - :target: https://gitter.im/Microsoft/MARO# - :alt: Gitter + :target: https://gitter.im/Microsoft/MARO# + :alt: Gitter .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/stack_overflow.svg - :target: https://stackoverflow.com/questions/ask?tags=maro - :alt: Stack Overflow + :target: https://stackoverflow.com/questions/ask?tags=maro + :alt: Stack Overflow .. image:: https://img.shields.io/github/release-date-pre/microsoft/maro - :target: https://github.com/microsoft/maro/releases - :alt: Releases + :target: https://github.com/microsoft/maro/releases + :alt: Releases .. image:: https://img.shields.io/github/commits-since/microsoft/maro/latest/master - :target: https://github.com/microsoft/maro/commits/master - :alt: Commits + :target: https://github.com/microsoft/maro/commits/master + :alt: Commits .. image:: https://github.com/microsoft/maro/workflows/vulnerability%20scan/badge.svg - :target: https://github.com/microsoft/maro/actions?query=workflow%3A%22vulnerability+scan%22 - :alt: Vulnerability Scan + :target: https://github.com/microsoft/maro/actions?query=workflow%3A%22vulnerability+scan%22 + :alt: Vulnerability Scan .. image:: https://github.com/microsoft/maro/workflows/lint/badge.svg - :target: https://github.com/microsoft/maro/actions?query=workflow%3Alint - :alt: Lint + :target: https://github.com/microsoft/maro/actions?query=workflow%3Alint + :alt: Lint .. image:: https://img.shields.io/codecov/c/github/microsoft/maro - :target: https://codecov.io/gh/microsoft/maro - :alt: Coverage + :target: https://codecov.io/gh/microsoft/maro + :alt: Coverage .. image:: https://img.shields.io/pypi/dm/pymaro - :target: https://pypi.org/project/pymaro/#files - :alt: Downloads + :target: https://pypi.org/project/pymaro/#files + :alt: Downloads .. image:: https://img.shields.io/docker/pulls/maro2020/maro - :target: https://hub.docker.com/repository/docker/maro2020/maro - :alt: Docker Pulls + :target: https://hub.docker.com/repository/docker/maro2020/maro + :alt: Docker Pulls .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/play_with_maro.svg - :target: https://hub.docker.com/r/maro2020/maro - :alt: Play with MARO + :target: https://hub.docker.com/r/maro2020/maro + :alt: Play with MARO .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/logo.svg - :target: https://maro.readthedocs.io/en/latest/ - :alt: MARO LOGO + :target: https://maro.readthedocs.io/en/latest/ + :alt: MARO LOGO ================================================================================================================ @@ -164,26 +164,26 @@ Key Components of MARO: .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/maro_overview.svg - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/maro_overview.svg - :alt: MARO Key Components + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/maro_overview.svg + :alt: MARO Key Components Contents -------- .. list-table:: - :header-rows: 1 + :header-rows: 1 - * - File/folder - - Description - * - ``maro`` - - MARO source code. - * - ``docs`` - - MARO docs, it is host on `readthedocs `_. - * - ``examples`` - - Showcase of MARO. - * - ``notebooks`` - - MARO quick-start notebooks. + * - File/folder + - Description + * - ``maro`` + - MARO source code. + * - ``docs`` + - MARO docs, it is host on `readthedocs `_. + * - ``examples`` + - Showcase of MARO. + * - ``notebooks`` + - MARO quick-start notebooks. *Try `MARO playground <#run-playground>`_ to have a quick experience.* @@ -199,17 +199,17 @@ Install MARO from `PyPI `_ .. code-block:: sh - pip install pymaro + pip install pymaro * Windows .. code-block:: powershell - # Install torch first, if you don't have one. - pip install torch===1.6.0 torchvision===0.7.0 -f https://download.pytorch.org/whl/torch_stable.html + # Install torch first, if you don't have one. + pip install torch===1.6.0 torchvision===0.7.0 -f https://download.pytorch.org/whl/torch_stable.html - pip install pymaro + pip install pymaro Install MARO from Source ------------------------ @@ -235,31 +235,31 @@ Install MARO from Source .. code-block:: sh - # If your environment is not clean, create a virtual environment firstly. - python -m venv maro_venv - source ./maro_venv/bin/activate + # If your environment is not clean, create a virtual environment firstly. + python -m venv maro_venv + source ./maro_venv/bin/activate * Windows .. code-block:: powershell - # If your environment is not clean, create a virtual environment firstly. - python -m venv maro_venv + # If your environment is not clean, create a virtual environment firstly. + python -m venv maro_venv - # You may need this for SecurityError in PowerShell. - Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted + # You may need this for SecurityError in PowerShell. + Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted - # Activate the virtual environment. - .\maro_venv\Scripts\activate + # Activate the virtual environment. + .\maro_venv\Scripts\activate * Install MARO .. code-block:: sh - # Git Clone the whole source code. - git clone https://github.com/microsoft/maro.git + # Git Clone the whole source code. + git clone https://github.com/microsoft/maro.git * @@ -267,16 +267,16 @@ Install MARO from Source .. code-block:: sh - # Install MARO from source. - bash scripts/install_maro.sh + # Install MARO from source. + bash scripts/install_maro.sh * Windows .. code-block:: powershell - # Install MARO from source. - .\scripts\install_maro.bat + # Install MARO from source. + .\scripts\install_maro.bat * *Notes: If your package is not found, remember to set your PYTHONPATH* @@ -286,45 +286,45 @@ Install MARO from Source .. code-block:: sh - export PYTHONPATH=PATH-TO-MARO + export PYTHONPATH=PATH-TO-MARO * Windows .. code-block:: powershell - $Env:PYTHONPATH=PATH-TO-MARO + $Env:PYTHONPATH=PATH-TO-MARO Quick Example ------------- .. code-block:: python - from maro.simulator import Env + from maro.simulator import Env - env = Env(scenario="cim", topology="toy.5p_ssddd_l0.0", start_tick=0, durations=100) + env = Env(scenario="cim", topology="toy.5p_ssddd_l0.0", start_tick=0, durations=100) - metrics, decision_event, is_done = env.step(None) + metrics, decision_event, is_done = env.step(None) - while not is_done: - metrics, decision_event, is_done = env.step(None) + while not is_done: + metrics, decision_event, is_done = env.step(None) - print(f"environment metrics: {env.metrics}") + print(f"environment metrics: {env.metrics}") `Environment Visualization `_ ------------------------------------------------------------------------- .. code-block:: sh - # Enable environment dump feature, when initializing the environment instance - env = Env(scenario="cim", - topology="toy.5p_ssddd_l0.0", - start_tick=0, - durations=100, - options={"enable-dump-snapshot": "./dump_data"}) + # Enable environment dump feature, when initializing the environment instance + env = Env(scenario="cim", + topology="toy.5p_ssddd_l0.0", + start_tick=0, + durations=100, + options={"enable-dump-snapshot": "./dump_data"}) - # Inspect environment with the dump data - maro inspector dashboard --source_path ./dump_data/YOUR_SNAPSHOT_DUMP_FOLDER + # Inspect environment with the dump data + maro inspector dashboard --source_path ./dump_data/YOUR_SNAPSHOT_DUMP_FOLDER Show Cases ^^^^^^^^^^ @@ -334,26 +334,26 @@ Show Cases Case I - Container Inventory Management .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_inter_epoch.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_inter_epoch.gif - :alt: CIM Inter Epoch + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_inter_epoch.gif + :alt: CIM Inter Epoch .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_intra_epoch_by_ports.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_intra_epoch_by_ports.gif - :alt: CIM Intra Epoch + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_intra_epoch_by_ports.gif + :alt: CIM Intra Epoch * Case II - Citi Bike .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_inter_epoch.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_inter_epoch.gif - :alt: Citi Bike Inter Epoch + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_inter_epoch.gif + :alt: Citi Bike Inter Epoch .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_intra_epoch_by_station.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_intra_epoch_by_station.gif - :alt: Citi Bike Intra Epoch + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_intra_epoch_by_station.gif + :alt: Citi Bike Intra Epoch Run Playground @@ -365,13 +365,13 @@ Run Playground .. code-block:: sh - # Pull the docker image from docker hub - docker pull maro2020/playground + # Pull the docker image from docker hub + docker pull maro2020/playground - # Run playground container. - # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Jupyter lab with maro -> http://127.0.0.1:40010 - docker run -p 40009:40009 -p 40010:40010 maro2020/playground + # Run playground container. + # Redis commander (GUI for redis) -> http://127.0.0.1:40009 + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground * Build from source @@ -382,26 +382,26 @@ Run Playground .. code-block:: sh - # Build playground image. - bash ./scripts/build_playground.sh + # Build playground image. + bash ./scripts/build_playground.sh - # Run playground container. - # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Jupyter lab with maro -> http://127.0.0.1:40010 - docker run -p 40009:40009 -p 40010:40010 maro2020/playground + # Run playground container. + # Redis commander (GUI for redis) -> http://127.0.0.1:40009 + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground * Windows .. code-block:: powershell - # Build playground image. - .\scripts\build_playground.bat + # Build playground image. + .\scripts\build_playground.bat - # Run playground container. - # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Jupyter lab with maro -> http://127.0.0.1:40010 - docker run -p 40009:40009 -p 40010:40010 maro2020/playground + # Run playground container. + # Redis commander (GUI for redis) -> http://127.0.0.1:40009 + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground Contributing ------------ @@ -431,8 +431,8 @@ Related Papers .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/scenario/cim_vis.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/scenario/cim_vis.gif - :alt: CIM Vis + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/scenario/cim_vis.gif + :alt: CIM Vis Wenlei Shi, Xinran Wei, Jia Zhang, Xiaoyuan Ni, Arthur Jiang, Jiang Bian, Tie-Yan Liu. "\ `Cooperative Policy Learning with Pre-trained Heterogeneous Observation Representations `_\ ". AAMAS 2021 diff --git a/maro/data_lib/cim/cim_data_container_helpers.py b/maro/data_lib/cim/cim_data_container_helpers.py index e2b2dcfec..f423bb7ea 100644 --- a/maro/data_lib/cim/cim_data_container_helpers.py +++ b/maro/data_lib/cim/cim_data_container_helpers.py @@ -30,6 +30,9 @@ def __init__(self, config_path: str, max_tick: int, topology: str): self._init_data_container() + self._random_seed: Optional[int] = None + self._re_init_data_cntr_flag: bool = False + def _init_data_container(self, topology_seed: int = None): if not os.path.exists(self._config_path): raise FileNotFoundError @@ -46,12 +49,22 @@ def _init_data_container(self, topology_seed: int = None): # Real Data Mode: read data from input data files, no need for any config.yml. self._data_cntr = data_from_files(data_folder=self._config_path) - def reset(self, keep_seed: bool): - """Reset data container internal state""" + def reset(self, keep_seed: bool) -> None: + """Reset data container internal state + """ if not keep_seed: - self._init_data_container(random[ROUTE_INIT_RAND_KEY].randint(0, DATA_CONTAINER_INIT_SEED_LIMIT - 1)) + self._random_seed = random[ROUTE_INIT_RAND_KEY].randint(0, DATA_CONTAINER_INIT_SEED_LIMIT - 1) + self._re_init_data_cntr_flag = True + + if self._re_init_data_cntr_flag: + self._init_data_container(self._random_seed) + self._re_init_data_cntr_flag = False else: - self._data_cntr.reset() + self._data_cntr.reset() # Reset the data container with reproduce-ability + + def set_seed(self, random_seed: int) -> None: + self._random_seed = random_seed + self._re_init_data_cntr_flag = True def __getattr__(self, name): return getattr(self._data_cntr, name) diff --git a/maro/simulator/core.py b/maro/simulator/core.py index a8910c1c2..35c01c6bc 100644 --- a/maro/simulator/core.py +++ b/maro/simulator/core.py @@ -14,7 +14,6 @@ from .abs_core import AbsEnv, DecisionMode from .scenarios.abs_business_engine import AbsBusinessEngine -from .utils import random from .utils.common import tick_to_frame_index @@ -181,9 +180,8 @@ def set_seed(self, seed: int) -> None: Args: seed (int): Seed to set. """ - - if seed is not None: - random.seed(seed) + assert seed is not None and isinstance(seed, int) + self._business_engine.set_seed(seed) @property def metrics(self) -> dict: diff --git a/maro/simulator/scenarios/abs_business_engine.py b/maro/simulator/scenarios/abs_business_engine.py index 382e8b506..ed0a1224e 100644 --- a/maro/simulator/scenarios/abs_business_engine.py +++ b/maro/simulator/scenarios/abs_business_engine.py @@ -154,6 +154,10 @@ def reset(self, keep_seed: bool = False) -> None: """Reset states business engine.""" pass + @abstractmethod + def set_seed(self, seed: int) -> None: + raise NotImplementedError + def post_step(self, tick: int) -> bool: """This method will be called at the end of each tick, used to post-process for each tick, for complex business logic with many events, it maybe not easy to determine diff --git a/maro/simulator/scenarios/cim/business_engine.py b/maro/simulator/scenarios/cim/business_engine.py index ccee78268..ec7a1c9cf 100644 --- a/maro/simulator/scenarios/cim/business_engine.py +++ b/maro/simulator/scenarios/cim/business_engine.py @@ -215,6 +215,9 @@ def reset(self, keep_seed: bool = False): self._total_operate_num = 0 + def set_seed(self, seed: int) -> None: + self._data_cntr.set_seed(seed) + def action_scope(self, port_idx: int, vessel_idx: int) -> ActionScope: """Get the action scope of specified agent. diff --git a/maro/simulator/scenarios/citi_bike/business_engine.py b/maro/simulator/scenarios/citi_bike/business_engine.py index cff8170e1..6465ff89b 100644 --- a/maro/simulator/scenarios/citi_bike/business_engine.py +++ b/maro/simulator/scenarios/citi_bike/business_engine.py @@ -170,6 +170,9 @@ def reset(self, keep_seed: bool = False): self._last_date = None + def set_seed(self, seed: int) -> None: + pass + def get_agent_idx_list(self) -> List[int]: """Get a list of agent index. diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py index c17c713b3..7b7a5ac8b 100644 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ b/maro/simulator/scenarios/supply_chain/business_engine.py @@ -77,6 +77,9 @@ def reset(self): self._action_cache = None + def set_seed(self, seed: int) -> None: + pass + def get_node_mapping(self) -> dict: return self._node_mapping diff --git a/maro/simulator/scenarios/vm_scheduling/business_engine.py b/maro/simulator/scenarios/vm_scheduling/business_engine.py index d13a49f6a..86a535bb4 100644 --- a/maro/simulator/scenarios/vm_scheduling/business_engine.py +++ b/maro/simulator/scenarios/vm_scheduling/business_engine.py @@ -438,6 +438,9 @@ def reset(self, keep_seed: bool = False): self._cpu_reader.reset() + def set_seed(self, seed: int) -> None: + pass + def _init_frame(self): self._frame = build_frame( snapshots_num=self.calc_max_snapshots(), From 526627c98710c8ee1ab61db76ffcceab09c66e96 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Fri, 4 Mar 2022 14:23:59 +0800 Subject: [PATCH 466/482] Remove all SC related files (#473) --- examples/rl/supply_chain/README.md | 8 - examples/rl/supply_chain/__init__.py | 9 - examples/rl/supply_chain/dqn.py | 101 - examples/rl/supply_chain/env_wrapper.py | 1207 - examples/rl/supply_chain/or_policies.py | 132 - examples/rl/supply_chain/policy_index.py | 38 - examples/rl/supply_chain/sc_state_in_maro.md | 312 - .../scenarios/supply_chain/__init__.py | 12 - .../scenarios/supply_chain/actions.py | 25 - .../scenarios/supply_chain/business_engine.py | 177 - .../supply_chain/datamodels/__init__.py | 12 - .../scenarios/supply_chain/datamodels/base.py | 45 - .../supply_chain/datamodels/consumer.py | 46 - .../supply_chain/datamodels/distribution.py | 22 - .../supply_chain/datamodels/extend.py | 33 - .../supply_chain/datamodels/facility.py | 17 - .../supply_chain/datamodels/manufacture.py | 33 - .../supply_chain/datamodels/product.py | 31 - .../supply_chain/datamodels/seller.py | 36 - .../supply_chain/datamodels/storage.py | 59 - .../supply_chain/datamodels/vehicle.py | 31 - .../scenarios/supply_chain/easy_config.py | 33 - .../supply_chain/facilities/__init__.py | 9 - .../supply_chain/facilities/facility.py | 187 - .../supply_chain/facilities/outerretailer.py | 37 - .../supply_chain/facilities/retailer.py | 10 - .../supply_chain/facilities/supplier.py | 10 - .../supply_chain/facilities/warehouse.py | 10 - .../scenarios/supply_chain/frame_builder.py | 18 - .../scenarios/supply_chain/parser.py | 232 - .../supply_chain/topologies/core.yml | 81 - .../supply_chain/topologies/random/config.yml | 29345 ---------------- .../supply_chain/topologies/sample/config.yml | 306 - .../scenarios/supply_chain/units/__init__.py | 16 - .../scenarios/supply_chain/units/consumer.py | 178 - .../supply_chain/units/distribution.py | 128 - .../supply_chain/units/extendunitbase.py | 25 - .../supply_chain/units/manufacture.py | 99 - .../scenarios/supply_chain/units/order.py | 16 - .../supply_chain/units/outerseller.py | 119 - .../scenarios/supply_chain/units/product.py | 215 - .../scenarios/supply_chain/units/seller.py | 94 - .../supply_chain/units/simplemanufacture.py | 27 - .../scenarios/supply_chain/units/storage.py | 157 - .../supply_chain/units/storeproduct.py | 13 - .../scenarios/supply_chain/units/unitbase.py | 125 - .../scenarios/supply_chain/units/vehicle.py | 204 - .../simulator/scenarios/supply_chain/world.py | 487 - notebooks/supply_chain/sc_demo.ipynb | 2216 -- notebooks/supply_chain/single_launcher.ipynb | 835 - tests/data/supply_chain/case_01/config.yml | 198 - tests/data/supply_chain/case_01/readme.md | 5 - tests/data/supply_chain/case_02/config.yml | 199 - tests/data/supply_chain/case_02/readme.md | 4 - tests/data/supply_chain/case_03/config.yml | 211 - tests/data/supply_chain/case_03/readme.md | 1 - tests/supply_chain/simple_seller.py | 7 - tests/supply_chain/test_supply_chain.py | 1399 - 58 files changed, 39642 deletions(-) delete mode 100644 examples/rl/supply_chain/README.md delete mode 100644 examples/rl/supply_chain/__init__.py delete mode 100644 examples/rl/supply_chain/dqn.py delete mode 100644 examples/rl/supply_chain/env_wrapper.py delete mode 100644 examples/rl/supply_chain/or_policies.py delete mode 100644 examples/rl/supply_chain/policy_index.py delete mode 100644 examples/rl/supply_chain/sc_state_in_maro.md delete mode 100644 maro/simulator/scenarios/supply_chain/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/actions.py delete mode 100644 maro/simulator/scenarios/supply_chain/business_engine.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/base.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/consumer.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/distribution.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/extend.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/facility.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/manufacture.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/product.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/seller.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/storage.py delete mode 100644 maro/simulator/scenarios/supply_chain/datamodels/vehicle.py delete mode 100644 maro/simulator/scenarios/supply_chain/easy_config.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/facility.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/outerretailer.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/retailer.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/supplier.py delete mode 100644 maro/simulator/scenarios/supply_chain/facilities/warehouse.py delete mode 100644 maro/simulator/scenarios/supply_chain/frame_builder.py delete mode 100644 maro/simulator/scenarios/supply_chain/parser.py delete mode 100644 maro/simulator/scenarios/supply_chain/topologies/core.yml delete mode 100644 maro/simulator/scenarios/supply_chain/topologies/random/config.yml delete mode 100644 maro/simulator/scenarios/supply_chain/topologies/sample/config.yml delete mode 100644 maro/simulator/scenarios/supply_chain/units/__init__.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/consumer.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/distribution.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/extendunitbase.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/manufacture.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/order.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/outerseller.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/product.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/seller.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/simplemanufacture.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/storage.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/storeproduct.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/unitbase.py delete mode 100644 maro/simulator/scenarios/supply_chain/units/vehicle.py delete mode 100644 maro/simulator/scenarios/supply_chain/world.py delete mode 100644 notebooks/supply_chain/sc_demo.ipynb delete mode 100644 notebooks/supply_chain/single_launcher.ipynb delete mode 100644 tests/data/supply_chain/case_01/config.yml delete mode 100644 tests/data/supply_chain/case_01/readme.md delete mode 100644 tests/data/supply_chain/case_02/config.yml delete mode 100644 tests/data/supply_chain/case_02/readme.md delete mode 100644 tests/data/supply_chain/case_03/config.yml delete mode 100644 tests/data/supply_chain/case_03/readme.md delete mode 100644 tests/supply_chain/simple_seller.py delete mode 100644 tests/supply_chain/test_supply_chain.py diff --git a/examples/rl/supply_chain/README.md b/examples/rl/supply_chain/README.md deleted file mode 100644 index 6cc67f2d1..000000000 --- a/examples/rl/supply_chain/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Supply Chain Scenario - -This README contains instructions for running the supply chain scenario using the scripts provided under ```examples/supply_chain/scripts```. For details on state, action and reward shaping based on MARO's supply chain business engine, refer to ```examples/supply_chain/sc_state_in_maro.md```. - -The instructions require that you have Docker and Docker Compose installed and set up on your machine. For installing Docker, refer to https://docs.docker.com/get-docker/. For installing Docker Compose, refer to https://docs.docker.com/compose/install/. To run the supply chain scenario, go to ```examples/supply_chain/scripts``` and follow the steps below: -1. Run ```bash build.sh``` to build the docker images required for running the supply chain scenario. This only needs to be done once, unless changes are made to any of the source code in maro/maro except that in maro/maro/rl, which is mounted to the containers. -2. Execute ```bash run.sh``` to run the scenario in multiple containers. A docker-compose manifest yaml will be generated based on the value of ```num_actors``` in the "distributed" section of ```examples/supply_chain/config.yml```. The number of containers launched will be equal to this value plus 2 (one for the learner and one for the Redis server). -3. After the program terminates, execute ```bash kill.sh``` to clean up the containers created in Step 2. \ No newline at end of file diff --git a/examples/rl/supply_chain/__init__.py b/examples/rl/supply_chain/__init__.py deleted file mode 100644 index 71c206463..000000000 --- a/examples/rl/supply_chain/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .env_wrapper import get_env_wrapper -from .policy_index import agent2policy, non_rl_policy_func_index, rl_policy_func_index, update_trigger, warmup - -__all__ = [ - "agent2policy", "get_env_wrapper", "non_rl_policy_func_index", "rl_policy_func_index", "update_trigger", "warmup" -] diff --git a/examples/rl/supply_chain/dqn.py b/examples/rl/supply_chain/dqn.py deleted file mode 100644 index 076f08c1c..000000000 --- a/examples/rl/supply_chain/dqn.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -import numpy as np -import torch - -from maro.rl.experience import ExperienceStore, UniformSampler -from maro.rl.exploration import EpsilonGreedyExploration, LinearExplorationScheduler -from maro.rl.model import FullyConnectedBlock, OptimOption, DiscreteQNet -from maro.rl.policy.algorithms import DQN, DQNConfig - -sc_path = os.path.dirname(os.path.realpath(__file__)) -if sc_path not in sys.path: - sys.path.insert(0, sc_path) -from env_wrapper import NUM_ACTIONS, STATE_DIM - -config = { - "model": { # Edit the get_dqn_agent() code in examples\supply_chain\agent.py if you need to customize the model. - "device": "cpu", - "network": { - "input_dim": STATE_DIM, - "hidden_dims": [256, 128, 32], - "output_dim": NUM_ACTIONS, - "activation": "leaky_relu", # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch activation classes. - "softmax": True, - "batch_norm": False, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 - }, - "optimization": { - "optim_cls": "adam", # refer to maro/maro/rl/utils/torch_cls_index.py for the mapping of strings to torch optimizer classes. - "optim_params": {"lr": 0.0005} - } - }, - "algorithm": { - "reward_discount": .99, - "train_epochs": 10, - "update_target_every": 4, # How many training iteration, to update DQN target model - "soft_update_coefficient": 0.01, - "double": True # whether to enable double DQN - }, - "experience_store": { - "rollout": { - "capacity": 10000, - # This determines how existing experiences are replaced when adding new experiences to a full experience - # memory. Must be one of "rolling" or "random". If "rolling", experiences will be replaced sequentially, - # with the oldest one being the first to be replaced. If "random", experiences will be replaced randomly. - "overwrite_type": "rolling" - }, - "update": {"capacity": 100000, "overwrite_type": "rolling"} - }, - "sampler": { - "rollout": {"batch_size": 2560, "replace": True}, - "update": {"batch_size": 256, "replace": True} - }, - "exploration": { - "last_ep": 10, - "initial_value": 0.8, # Here (start: 0.4, end: 0.0) means: the exploration rate will start at 0.4 and decrease linearly to 0.0 in the last episode. - "final_value": 0.0 - } -} - - -class QNet(DiscreteQNet): - def forward(self, states): - states = torch.from_numpy(np.asarray(states)).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - return self.component(states) - - -def get_dqn_policy(mode="update"): - assert mode in {"inference", "update", "inference-update"} - qnet = QNet( - FullyConnectedBlock(**config["model"]["network"]), - optim_option=OptimOption(**config["model"]["optimization"]) if mode != "inference" else None - ) - if mode == "update": - exp_store = ExperienceStore(**config["experience_store"]["update"]) - exploration = None - experience_sampler_kwargs = config["sampler"]["update"] - else: - exploration = EpsilonGreedyExploration() - exploration.register_schedule( - scheduler_cls=LinearExplorationScheduler, - param_name="epsilon", - **config["exploration"] - ) - exp_store = ExperienceStore(**config["experience_store"]["rollout" if mode == "inference" else "update"]) - experience_sampler_kwargs = config["sampler"]["rollout" if mode == "inference" else "update"] - - return DQN( - qnet, DQNConfig(**config["algorithm"]), exp_store, - experience_sampler_cls=UniformSampler, - experience_sampler_kwargs=experience_sampler_kwargs, - exploration=exploration - ) diff --git a/examples/rl/supply_chain/env_wrapper.py b/examples/rl/supply_chain/env_wrapper.py deleted file mode 100644 index f288eef1d..000000000 --- a/examples/rl/supply_chain/env_wrapper.py +++ /dev/null @@ -1,1207 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict, namedtuple -from os.path import dirname, join, realpath -from typing import List - -import scipy.stats as st -import numpy as np - -from maro.rl.learning import AbsEnvWrapper -from maro.simulator import Env -from maro.simulator.scenarios.supply_chain.actions import ConsumerAction, ManufactureAction - - -def stock_constraint(f_state): - return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 7) * f_state['sale_mean'] - - -def is_replenish_constraint(f_state): - return f_state['consumption_hist'][-1] > 0 - - -def low_profit(f_state): - return (f_state['sku_price'] - f_state['sku_cost']) * f_state['sale_mean'] <= 1000 - - -def low_stock_constraint(f_state): - return 0 < f_state['inventory_in_stock'] <= (f_state['max_vlt'] + 3) * f_state['sale_mean'] - - -def out_of_stock(f_state): - return 0 < f_state['inventory_in_stock'] - - -atoms = { - 'stock_constraint': stock_constraint, - 'is_replenish_constraint': is_replenish_constraint, - 'low_profit': low_profit, - 'low_stock_constraint': low_stock_constraint, - 'out_of_stock': out_of_stock -} - -# State extracted. -keys_in_state = [(None, ['is_over_stock', 'is_out_of_stock', 'is_below_rop', 'consumption_hist']), - ('storage_capacity', ['storage_utilization']), - ('sale_mean', ['sale_std', - 'sale_hist', - 'pending_order', - 'inventory_in_stock', - 'inventory_in_transit', - 'inventory_estimated', - 'inventory_rop']), - ('max_price', ['sku_price', 'sku_cost'])] - -# Sku related agent types -sku_agent_types = {"consumer", "consumerstore", "producer", "product", "productstore"} - - -class UnitBaseInfo: - id: int = None - node_index: int = None - config: dict = None - summary: dict = None - - def __init__(self, unit_summary): - self.id = unit_summary["id"] - self.node_index = unit_summary["node_index"] - self.config = unit_summary.get("config", {}) - self.summary = unit_summary - - def __getitem__(self, key, default=None): - if key in self.summary: - return self.summary[key] - - return default - - -distribution_features = ("remaining_order_quantity", "remaining_order_number") -seller_features = ("total_demand", "sold", "demand") - - -class SCEnvWrapper(AbsEnvWrapper): - def __init__(self, env: Env, reward_eval_delay: int=0, replay_agent_ids: list=None): - super().__init__(env, reward_eval_delay, replay_agent_ids=replay_agent_ids) - self.balance_cal = BalanceSheetCalculator(env) - self.cur_balance_sheet_reward = None - self.storage_ss = env.snapshot_list["storage"] - self.distribution_ss = env.snapshot_list["distribution"] - self.consumer_ss = env.snapshot_list["consumer"] - self.seller_ss = env.snapshot_list["seller"] - - self._summary = env.summary['node_mapping'] - self._configs = env.configs - self._agent_types = self._summary["agent_types"] - self._units_mapping = self._summary["unit_mapping"] - self._agent_list = env.agent_idx_list - - self._sku_number = len(self._summary["skus"]) + 1 - self._max_price = self._summary["max_price"] - self._max_sources_per_facility = self._summary["max_sources_per_facility"] - - # state for each tick - self._cur_metrics = env.metrics - - # cache for ppf value. - self._service_index_ppf_cache = {} - - # facility -> { - # data_model_index:int, - # storage:UnitBaseInfo, - # distribution: UnitBaseInfo, - # sku_id: { - # skuproduct: UnitBaseInfo, - # consumer: UnitBaseInfo, - # seller: UnitBaseInfo, - # manufacture: UnitBaseInfo - # } - # } - self.facility_levels = {} - - # unit id -> (facility id) - self.unit_2_facility_dict = {} - - # our raw state - self._states = {} - - # facility id -> storage index - self._facility2storage_index_dict = {} - - # facility id -> product id -> number - self._storage_product_numbers = {} - - # facility id -> product_id -> index - self._storage_product_indices = {} - - # facility id -> storage product utilization - self._facility_product_utilization = {} - - # facility id -> in_transit_orders - self._facility_in_transit_orders = {} - - # current distribution states - self._cur_distribution_states = None - - # current consumer states - self._cur_consumer_states = None - - # current seller states - self._cur_seller_states = None - - # dim for state - self._dim = None - - # use this to quick find relationship between units (consumer, manufacture, seller or product) and product unit. - # unit id -> (product unit id, facility id, seller id, consumer id, manufacture id) - self._unit2product_mapping = {} - - # agent (unit id) -> AgentInfo - self._agent_id2info_mapping = {} - - # built internal helpers. - self._build_internal_helpers() - - self.stock_status = {} - self.demand_status = {} - # key: product unit id, value: number - self.orders_from_downstreams = {} - self.consumer_orders = {} - self.order_in_transit_status = {} - self.order_to_distribute_status = {} - - @property - def dim(self): - """Calculate dim per shape.""" - if self._dim is None: - self._dim = 0 - - first_state = next(iter(self._states.values())) - - for _, state_keys in keys_in_state: - for key in state_keys: - val = first_state[key] - - if type(val) == list: - self._dim += len(val) - else: - self._dim += 1 - - return self._dim - - def get_or_policy_state(self, state, agent_info): - state = {'is_facility': not (agent_info.agent_type in sku_agent_types)} - if agent_info.is_facility: - return state - - product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id - id, product_id, _, storage_index, unit_storage_cost, distribution_index, downstreams, consumer, seller, manufacture = \ - self.balance_cal.products[self.balance_cal.product_id2index_dict[product_unit_id]] - - product_metrics = self._cur_metrics["products"][product_unit_id] - state['sale_mean'] = product_metrics["sale_mean"] - state['sale_std'] = product_metrics["sale_std"] - - facility = self.facility_levels[agent_info.facility_id] - state['unit_storage_cost'] = unit_storage_cost - state['order_cost'] = 1 - product_info = facility[agent_info.sku.id] - if "consumer" in product_info: - consumer_index = product_info["consumer"].node_index - state['order_cost'] = self.consumer_ss[self.env.tick:consumer_index:"order_cost"].flatten()[0] - state['storage_capacity'] = facility['storage'].config["capacity"] - state['storage_levels'] = self._storage_product_numbers[agent_info.facility_id] - state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - state['product_idx'] = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 - state['vlt'] = agent_info.sku.vlt - state['service_level'] = agent_info.sku.service_level - return state - - def get_rl_policy_state(self, state, agent_info): - self._update_facility_features(state, agent_info) - self._update_storage_features(state, agent_info) - # bom do not need to update - # self._add_bom_features(state, agent_info) - self._update_distribution_features(state, agent_info) - self._update_sale_features(state, agent_info) - # vlt do not need to update - # self._update_vlt_features(state, agent_info) - self._update_consumer_features(state, agent_info) - # self._add_price_features(state, agent_info) - self._update_global_features(state) - - self.stock_status[agent_info.id] = state['inventory_in_stock'] - - self.demand_status[agent_info.id] = state['sale_hist'][-1] - - self.order_in_transit_status[agent_info.id] = state['inventory_in_transit'] - - self.order_to_distribute_status[agent_info.id] = state['distributor_in_transit_orders_qty'] - - np_state = self._serialize_state(state) - return np_state - - def get_state(self, tick=None): - if tick is None: - tick = self.env.tick - settings: dict = self.env.configs.settings - consumption_hist_len = settings['consumption_hist_len'] - hist_len = settings['sale_hist_len'] - consumption_ticks = [tick - i for i in range(consumption_hist_len-1, -1, -1)] - hist_ticks = [tick - i for i in range(hist_len-1, -1, -1)] - - self.cur_balance_sheet_reward = self.balance_cal.calc() - self._cur_metrics = self.env.metrics - - self._cur_distribution_states = self.distribution_ss[tick::distribution_features] \ - .flatten() \ - .reshape(-1, len(distribution_features)) \ - .astype(np.int) - - self._cur_consumer_states = self.consumer_ss[consumption_ticks::"latest_consumptions"] \ - .flatten() \ - .reshape(-1, len(self.consumer_ss)) - - self._cur_seller_states = self.seller_ss[hist_ticks::seller_features] \ - .astype(np.int) - - - # facility level states - for facility_id in self._facility_product_utilization: - # reset for each step - self._facility_product_utilization[facility_id] = 0 - - in_transit_orders = self._cur_metrics['facilities'][facility_id]["in_transit_orders"] - - self._facility_in_transit_orders[facility_id] = [0] * self._sku_number - - for sku_id, number in in_transit_orders.items(): - self._facility_in_transit_orders[facility_id][sku_id] = number - - final_state = {} - - # calculate storage info first, then use it later to speed up. - for facility_id, storage_index in self._facility2storage_index_dict.items(): - product_numbers = self.storage_ss[tick:storage_index:"product_number"] \ - .flatten() \ - .astype(np.int) - - for pid, index in self._storage_product_indices[facility_id].items(): - product_number = product_numbers[index] - - self._storage_product_numbers[facility_id][pid] = product_number - self._facility_product_utilization[facility_id] += product_number - - for agent_info in self._agent_list: - state = self._states[agent_info.id] - - # storage_index = self._facility2storage_index_dict[agent_info.facility_id] - - np_state = self.get_rl_policy_state(state, agent_info) - if agent_info.agent_type in ["consumer", "producer"]: - np_state = self.get_or_policy_state(state, agent_info) - - # agent_info.agent_type -> policy - final_state[f"{agent_info.agent_type}.{agent_info.id}"] = np_state - - #self.reward_status = {f_id: np.float32(reward[1]) for f_id, reward in self.cur_balance_sheet_reward.items()} - #self.balance_status = {f_id: np.float32(reward[0]) for f_id, reward in self.cur_balance_sheet_reward.items()} - - return final_state - - def get_reward(self, actions, tick=None): - # get related product, seller, consumer, manufacture unit id - # NOTE: this mapping does not contain facility id, so if id is not exist, then means it is a facility - # product_unit_id, facility_id, seller_id, consumer_id, producer_id = self._unit2product_mapping[id] - # return { - # f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}": np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) - # for f_id, bwt in self.cur_balance_sheet_reward.items() - # } - self.cur_balance_sheet_reward = self.balance_cal.calc() - rewards = defaultdict(float) - for f_id, bwt in self.cur_balance_sheet_reward.items(): - agent = self._agent_id2info_mapping[f_id] - if agent.agent_type == 'consumerstore': - rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = np.float32(bwt[1]) / np.float32(self._configs.settings["reward_normalization"]) - else: - rewards[f"{self._agent_id2info_mapping[f_id].agent_type}.{f_id}"] = 0 - - return rewards - - def to_env_action(self, action_by_agent): - # cache the sources for each consumer if not yet cached - if not hasattr(self, "consumer2source"): - self.consumer2source, self.consumer2product = {}, {} - for facility in self.env.summary["node_mapping"]["facilities"].values(): - products = facility["units"]["products"] - for product_id, product in products.items(): - consumer = product["consumer"] - if consumer is not None: - consumer_id = consumer["id"] - self.consumer2source[consumer_id] = consumer["sources"] - self.consumer2product[consumer_id] = product_id - - env_action = [] - for agent_id, action in action_by_agent.items(): - unit_id = int(agent_id.split(".")[1]) - - is_facility = unit_id not in self._units_mapping - - # ignore facility to reduce action number - if is_facility: - continue - - # consumer action - if agent_id.startswith("consumer"): - product_id = self.consumer2product.get(unit_id, 0) - sources = self.consumer2source.get(unit_id, []) - if sources: - source_id = sources[0] - product_unit_id = self._unit2product_mapping[unit_id][0] - action_number = int(int(action) * self._cur_metrics["products"][product_unit_id]["sale_mean"]) - - # ignore 0 quantity to reduce action number - if action_number == 0: - continue - - sku = self._units_mapping[unit_id][3] - - reward_discount = 1 - - env_action.append(ConsumerAction( - unit_id, - product_id, - source_id, - action_number, - sku.vlt, - reward_discount - )) - - self.consumer_orders[product_unit_id] = action_number - self.orders_from_downstreams[self.facility_levels[source_id][product_id]["skuproduct"].id] = action_number - - # manufacturer action - elif agent_id.startswith("producer"): - sku = self._units_mapping[unit_id][3] - action = sku.production_rate - - # ignore invalid actions - if action is None or action == 0: - continue - - env_action.append(ManufactureAction(unit_id, action)) - - return env_action - - def _update_facility_features(self, state, agent_info): - state['is_positive_balance'] = 1 if self.balance_cal.total_balance_sheet[agent_info.id] > 0 else 0 - - def _update_storage_features(self, state, agent_info): - facility_id = agent_info.facility_id - state['storage_utilization'] = 0 - - state['storage_levels'] = self._storage_product_numbers[facility_id] - state['storage_utilization'] = self._facility_product_utilization[facility_id] - - def _update_sale_features(self, state, agent_info): - if agent_info.agent_type not in sku_agent_types: - return - - # Get product unit id for current agent. - product_unit_id = agent_info.id if agent_info.agent_type in ["product", "productstore"] else agent_info.parent_id - - product_metrics = self._cur_metrics["products"][product_unit_id] - - state['sale_mean'] = product_metrics["sale_mean"] - state['sale_std'] = product_metrics["sale_std"] - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - if "seller" not in product_info: - # TODO: why gamma sale as mean? - state['sale_gamma'] = state['sale_mean'] - - if "consumer" in product_info: - consumer_index = product_info["consumer"].node_index - - state['consumption_hist'] = list( - self._cur_consumer_states[:, consumer_index]) - state['pending_order'] = list( - product_metrics["pending_order_daily"]) - - if "seller" in product_info: - seller_index = product_info["seller"].node_index - - seller_states = self._cur_seller_states[:, seller_index, :] - - # For total demand, we need latest one. - state['total_backlog_demand'] = seller_states[:, 0][-1][0] - state['sale_hist'] = list(seller_states[:, 1].flatten()) - state['backlog_demand_hist'] = list(seller_states[:, 2]) - - def _update_distribution_features(self, state, agent_info): - facility = self.facility_levels[agent_info.facility_id] - distribution = facility.get("distribution", None) - - if distribution is not None: - dist_states = self._cur_distribution_states[distribution.node_index] - state['distributor_in_transit_orders'] = dist_states[1] - state['distributor_in_transit_orders_qty'] = dist_states[0] - - def _update_consumer_features(self, state, agent_info): - if agent_info.is_facility: - return - - facility = self.facility_levels[agent_info.facility_id] - product_info = facility[agent_info.sku.id] - - # if "consumer" not in product_info: - # return - - state['consumer_in_transit_orders'] = self._facility_in_transit_orders[agent_info.facility_id] - - # FIX: we need plus 1 to this, as it is 0 based index, but we already aligned with 1 more - # slot to use sku id as index ( 1 based). - product_index = self._storage_product_indices[agent_info.facility_id][agent_info.sku.id] + 1 - state['inventory_in_stock'] = self._storage_product_numbers[agent_info.facility_id][product_index] - state['inventory_in_transit'] = state['consumer_in_transit_orders'][agent_info.sku.id] - - pending_order = self._cur_metrics["facilities"][agent_info.facility_id]["pending_order"] - - if pending_order is not None: - state['inventory_in_distribution'] = pending_order[agent_info.sku.id] - - state['inventory_estimated'] = (state['inventory_in_stock'] - + state['inventory_in_transit'] - - state['inventory_in_distribution']) - if state['inventory_estimated'] >= 0.5 * state['storage_capacity']: - state['is_over_stock'] = 1 - - if state['inventory_estimated'] <= 0: - state['is_out_of_stock'] = 1 - - service_index = state['service_level'] - - if service_index not in self._service_index_ppf_cache: - self._service_index_ppf_cache[service_index] = st.norm.ppf( - service_index) - - ppf = self._service_index_ppf_cache[service_index] - - state['inventory_rop'] = (state['max_vlt'] * state['sale_mean'] - + np.sqrt(state['max_vlt']) * state['sale_std'] * ppf) - - if state['inventory_estimated'] < state['inventory_rop']: - state['is_below_rop'] = 1 - - def _update_global_features(self, state): - state["global_time"] = self.env.tick - - def _serialize_state(self, state): - result = [] - - for norm, fields in keys_in_state: - for field in fields: - vals = state[field] - if not isinstance(vals, list): - vals = [vals] - if norm is not None: - vals = [max(0.0, min(20.0, x / (state[norm] + 0.01))) - for x in vals] - result.extend(vals) - - return np.asarray(result, dtype=np.float32) - - def _build_internal_helpers(self): - for agent_info in self.env.agent_idx_list: - self._agent_id2info_mapping[agent_info.id] = agent_info - - # facility levels - for facility_id, facility in self._summary["facilities"].items(): - self.facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility['configs'], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } - - units = facility["units"] - - storage = units["storage"] - if storage is not None: - self.facility_levels[facility_id]["storage"] = UnitBaseInfo( - storage) - - self.unit_2_facility_dict[storage["id"]] = facility_id - - self._facility2storage_index_dict[facility_id] = storage["node_index"] - - self._storage_product_numbers[facility_id] = [0] * self._sku_number - self._storage_product_indices[facility_id] = {} - self._facility_product_utilization[facility_id] = 0 - - for i, pid in enumerate(storage["product_list"]): - self._storage_product_indices[facility_id][pid] = i - self._storage_product_numbers[facility_id][pid] = 0 - - distribution = units["distribution"] - - if distribution is not None: - self.facility_levels[facility_id]["distribution"] = UnitBaseInfo( - distribution) - self.unit_2_facility_dict[distribution["id"]] = facility_id - - products = units["products"] - - if products: - for product_id, product in products.items(): - product_info = { - "skuproduct": UnitBaseInfo(product) - } - - self.unit_2_facility_dict[product["id"]] = facility_id - - seller = product['seller'] - - if seller is not None: - product_info["seller"] = UnitBaseInfo(seller) - self.unit_2_facility_dict[seller["id"]] = facility_id - - consumer = product["consumer"] - - if consumer is not None: - product_info["consumer"] = UnitBaseInfo(consumer) - self.unit_2_facility_dict[consumer["id"]] = facility_id - - manufacture = product["manufacture"] - - if manufacture is not None: - product_info["manufacture"] = UnitBaseInfo(manufacture) - self.unit_2_facility_dict[manufacture["id"] - ] = facility_id - - self.facility_levels[facility_id][product_id] = product_info - - for unit in (seller, consumer, manufacture, product): - if unit is not None: - self._unit2product_mapping[unit["id"]] = ( - product["id"], - facility_id, - seller["id"] if seller is not None else None, - consumer["id"] if consumer is not None else None, - manufacture["id"] if manufacture is not None else None - ) - - # create initial state structure - self._build_init_state() - - def _build_init_state(self): - # we will build the final state with default and const values, - # then update dynamic part per step - for agent_info in self._agent_list: - state = {} - - facility = self.facility_levels[agent_info.facility_id] - - # global features - state["global_time"] = 0 - - # facility features - state["facility"] = None - state["facility_type"] = [1 if i == agent_info.agent_type else 0 for i in range(len(self._agent_types))] - state["is_accepted"] = [0] * self._configs.settings["constraint_state_hist_len"] - state['constraint_idx'] = [0] - state['facility_id'] = [0] * self._sku_number - state['sku_info'] = {} if agent_info.is_facility else agent_info.sku - state['echelon_level'] = 0 - - state['facility_info'] = facility['config'] - state["is_positive_balance"] = 0 - - if not agent_info.is_facility: - state['facility_id'][agent_info.sku.id] = 1 - - for atom_name in atoms.keys(): - state[atom_name] = list( - np.ones(self._configs.settings['constraint_state_hist_len'])) - - # storage features - state['storage_levels'] = [0] * self._sku_number - state['storage_capacity'] = facility['storage'].config["capacity"] - state['storage_utilization'] = 0 - - # bom features - state['bom_inputs'] = [0] * self._sku_number - state['bom_outputs'] = [0] * self._sku_number - - if not agent_info.is_facility: - state['bom_inputs'][agent_info.sku.id] = 1 - state['bom_outputs'][agent_info.sku.id] = 1 - - # vlt features - sku_list = self._summary["skus"] - current_source_list = [] - - if agent_info.sku is not None: - current_source_list = facility["upstreams"].get( - agent_info.sku.id, []) - - state['vlt'] = [0] * \ - (self._max_sources_per_facility * self._sku_number) - state['max_vlt'] = 0 - - if not agent_info.is_facility: - # only for sku product - product_info = facility[agent_info.sku.id] - - if "consumer" in product_info and len(current_source_list) > 0: - state['max_vlt'] = product_info["skuproduct"]["max_vlt"] - - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - # NOTE: different with original code, our config can make sure that source has product we need - - if sku.id == agent_info.sku.id: - state['vlt'][i * len(sku_list) + j + - 1] = facility["skus"][sku.id].vlt - - # sale features - settings = self.env.configs.settings - hist_len = settings['sale_hist_len'] - consumption_hist_len = settings['consumption_hist_len'] - - state['sale_mean'] = 1.0 - state['sale_std'] = 1.0 - state['sale_gamma'] = 1.0 - state['service_level'] = 0.95 - state['total_backlog_demand'] = 0 - - state['sale_hist'] = [0] * hist_len - state['backlog_demand_hist'] = [0] * hist_len - state['consumption_hist'] = [0] * consumption_hist_len - state['pending_order'] = [0] * settings['pending_order_len'] - - if not agent_info.is_facility: - state['service_level'] = agent_info.sku.service_level - - product_info = facility[agent_info.sku.id] - - if "seller" in product_info: - state['sale_gamma'] = facility["skus"][agent_info.sku.id].sale_gamma - - # distribution features - state['distributor_in_transit_orders'] = 0 - state['distributor_in_transit_orders_qty'] = 0 - - # consumer features - state['consumer_source_export_mask'] = [0] * \ - (self._max_sources_per_facility * self._sku_number) - state['consumer_source_inventory'] = [0] * self._sku_number - state['consumer_in_transit_orders'] = [0] * self._sku_number - - state['inventory_in_stock'] = 0 - state['inventory_in_transit'] = 0 - state['inventory_in_distribution'] = 0 - state['inventory_estimated'] = 0 - state['inventory_rop'] = 0 - state['is_over_stock'] = 0 - state['is_out_of_stock'] = 0 - state['is_below_rop'] = 0 - - if len(current_source_list) > 0: - for i, source in enumerate(current_source_list): - for j, sku in enumerate(sku_list.values()): - if sku.id == agent_info.sku.id: - state['consumer_source_export_mask'][i * len(sku_list) + j + 1] = \ - self.facility_levels[source]["skus"][sku.id].vlt - - # price features - state['max_price'] = self._max_price - state['sku_price'] = 0 - state['sku_cost'] = 0 - - if not agent_info.is_facility: - state['sku_price'] = agent_info.sku.price - state['sku_cost'] = agent_info.sku.cost - - self._states[agent_info.id] = state - - -ProductInfo = namedtuple( - "ProductInfo", - ( - "unit_id", - "sku_id", - "node_index", - "storage_index", - "unit_storage_cost", - "distribution_index", - "downstream_product_units", - "consumer_id_index_tuple", - "seller_id_index_tuple", - "manufacture_id_index_tuple" - ) -) - -FacilityLevelInfo = namedtuple( - "FacilityLevelInfo", - ( - "unit_id", - "product_unit_id_list", - "storage_index", - "unit_storage_cost", - "distribution_index", - "vehicle_index_list" - ) -) - - -class BalanceSheetCalculator: - def __init__(self, env: Env): - self.env = env - self.products: List[ProductInfo] = [] - self.product_id2index_dict = {} - self.facility_levels: List[FacilityLevelInfo] = [] - self.consumer_id2product = {} - - self.facilities = env.summary["node_mapping"]["facilities"] - - for facility_id, facility in self.facilities.items(): - pid_list = [] - distribution = facility["units"]["distribution"] - - for product_id, product in facility["units"]["products"].items(): - pid_list.append(product["id"]) - consumer = product["consumer"] - if consumer is not None: - self.consumer_id2product[consumer["id"]] = product["id"] - seller = product["seller"] - manufacture = product["manufacture"] - - self.product_id2index_dict[product["id"]] = len(self.products) - - downstream_product_units = [] - downstreams = facility["downstreams"] - - if downstreams and len(downstreams) > 0 and product_id in downstreams: - for dfacility in downstreams[product_id]: - dproducts = self.facilities[dfacility]["units"]["products"] - - downstream_product_units.append(dproducts[product_id]["id"]) - - self.products.append( - ProductInfo( - unit_id=product["id"], - sku_id=product_id, - node_index=product["node_index"], - storage_index=facility["units"]["storage"]["node_index"], - unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution_index=distribution["node_index"] if distribution is not None else None, - downstream_product_units=downstream_product_units, - consumer_id_index_tuple=None if consumer is None else (consumer["id"], consumer["node_index"]), - seller_id_index_tuple=None if seller is None else (seller["id"], seller["node_index"]), - manufacture_id_index_tuple=None if manufacture is None else (manufacture["id"], manufacture["node_index"]) - ) - ) - - self.facility_levels.append( - FacilityLevelInfo( - unit_id=facility_id, - product_unit_id_list=pid_list, - storage_index=facility["units"]["storage"]["node_index"], - unit_storage_cost=facility["units"]["storage"]["config"]["unit_storage_cost"], - distribution_index=distribution["node_index"] if distribution is not None else None, - vehicle_index_list=[ - v["node_index"] for v in distribution["children"] - ] if distribution is not None else [] - ) - ) - - # TODO: order products make sure calculate reward from downstream to upstream - tmp_product_unit_dict = {} - - for product in self.products: - tmp_product_unit_dict[product.unit_id] = product - - self._ordered_products = [] - - tmp_stack = [] - - for product in self.products: - # skip if already being processed - if tmp_product_unit_dict[product.unit_id] is None: - continue - - for dproduct in product.downstream_product_units: - # push downstream id to stack - tmp_stack.append(dproduct) - - # insert current product to list head - self._ordered_products.insert(0, product) - # mark it as processed - tmp_product_unit_dict[product.unit_id] = None - - while len(tmp_stack) > 0: - # process downstream of product unit in stack - dproduct_unit_id = tmp_stack.pop() - - # if it was processed then ignore - if tmp_product_unit_dict[dproduct_unit_id] is None: - continue - - # or extract it downstreams - dproduct_unit = tmp_product_unit_dict[dproduct_unit_id] - - dproduct_downstreams = dproduct_unit.downstream_product_units - - for dproduct in dproduct_downstreams: - tmp_stack.append(dproduct) - - # current unit in final list - self._ordered_products.insert(0, dproduct_unit) - tmp_product_unit_dict[dproduct_unit_id] = None - - self.total_balance_sheet = defaultdict(int) - - # tick -> (product unit id, sku id, manufacture number, manufacture cost, checkin order, delay penaty) - self._supplier_reward_factors = {} - - def _check_attribute_keys(self, target_type: str, attribute: str): - valid_target_types = list(self.env.summary["node_detail"].keys()) - assert target_type in valid_target_types, f"Target_type {target_type} not in {valid_target_types}!" - - valid_attributes = list(self.env.summary["node_detail"][target_type]["attributes"].keys()) - assert attribute in valid_attributes, ( - f"Attribute {attribute} not valid for {target_type}. " - f"Valid attributes: {valid_attributes}" - ) - return - - def _get_attributes(self, target_type: str, attribute: str, tick: int=None) -> np.ndarray: - self._check_attribute_keys(target_type, attribute) - - if tick == None: - tick = self.env.tick - - return self.env.snapshot_list[target_type][tick::attribute].flatten() - - def _get_list_attributes(self, target_type: str, attribute: str, tick: int=None) -> List[np.ndarray]: - self._check_attribute_keys(target_type, attribute) - - if tick == None: - tick = self.env.tick - - indexes = list(range(len(self.env.snapshot_list[target_type]))) - return [self.env.snapshot_list[target_type][tick:index:attribute].flatten() for index in indexes] - - def _calc_consumer(self): - #### Consumer - consumer_ids = self._get_attributes("consumer", "id").astype(np.int) - - # quantity * price - order_profit = ( - self._get_attributes("consumer", "order_quantity") - * self._get_attributes("consumer", "price") - ) - - # order_cost + order_product_cost - consumer_step_balance_sheet_loss = -1 * ( - self._get_attributes("consumer", "order_cost") - + self._get_attributes("consumer", "order_product_cost") - ) - - # consumer step reward: balance sheet los + profile * discount - # consumer_step_reward = ( - # consumer_step_balance_sheet_loss - # + order_profit * self._get_attributes("consumer", "reward_discount") - # ) - consumer_step_reward = consumer_step_balance_sheet_loss - - consumer_step_balance_sheet = order_profit + consumer_step_balance_sheet_loss - - return consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet - - def _calc_seller(self): - #### Seller - # profit = sold * price - seller_balance_sheet_profit = ( - self._get_attributes("seller", "sold") - * self._get_attributes("seller", "price") - ) - - # loss = demand * price * backlog_ratio - seller_balance_sheet_loss = -1 * ( - self._get_attributes("seller", "demand") - * self._get_attributes("seller", "price") - * self._get_attributes("seller", "backlog_ratio") - ) - - # step reward = loss + profit - seller_step_reward = seller_balance_sheet_loss + seller_balance_sheet_profit - - return seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward - - def _calc_manufacture(self): - #### manufacture - manufacture_ids = self._get_attributes("manufacture", "id").astype(np.int) - - # loss = manufacture number * cost - manufacture_balance_sheet_loss = -1 * ( - self._get_attributes("manufacture", "manufacturing_number") - * self._get_attributes("manufacture", "product_unit_cost") - ) - - # step reward = loss - manufacture_step_reward = manufacture_balance_sheet_loss - manufacture_step_balance_sheet = manufacture_balance_sheet_loss - - return manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet - - def _calc_storage(self): - #### storage - # loss = (capacity-remaining space) * cost - storage_balance_sheet_loss = -1 * ( - self._get_attributes("storage", "capacity") - - self._get_attributes("storage", "remaining_space") - ) - - # create product number mapping for storages - product_list = self._get_list_attributes("storage", "product_list") - product_number = self._get_list_attributes("storage", "product_number") - storages_product_map = { - idx: { - id: num - for id, num in zip(id_list.astype(np.int), num_list.astype(np.int)) - } - for idx, (id_list, num_list) in enumerate(zip(product_list, product_number)) - } - - return storage_balance_sheet_loss, storages_product_map - - def _calc_vehicle(self): - ## vehicles - # loss = cost * payload - vehicle_balance_sheet_loss = -1 * ( - self._get_attributes("vehicle", "payload") - * self._get_attributes("vehicle", "unit_transport_cost") - ) - vehicle_step_reward = vehicle_balance_sheet_loss - return vehicle_balance_sheet_loss, vehicle_step_reward - - def _calc_product_distribution(self): - #### product - # product distribution profit = check order * price - product_distribution_balance_sheet_profit = ( - self._get_attributes("product", "distribution_check_order") - * self._get_attributes("product", "price") - ) - # product distribution loss = transportation cost + delay order penalty - product_distribution_balance_sheet_loss = -1 * ( - self._get_attributes("product", "distribution_transport_cost") - + self._get_attributes("product", "distribution_delay_order_penalty") - ) - return product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss - - def _calc_product( - self, - consumer_step_balance_sheet_loss, - consumer_step_reward, - seller_balance_sheet_profit, - seller_balance_sheet_loss, - seller_step_reward, - manufacture_balance_sheet_loss, - manufacture_step_reward, - storages_product_map, - product_distribution_balance_sheet_profit, - product_distribution_balance_sheet_loss, - ): - num_products = len(self.products) - product_step_reward = np.zeros(num_products) - product_balance_sheet_profit = np.zeros(num_products) - product_balance_sheet_loss = np.zeros(num_products) - - # product = consumer + seller + manufacture + storage + distribution + downstreams - for product in self._ordered_products: - i = product.node_index - - if product.consumer_id_index_tuple: - consumer_index = product.consumer_id_index_tuple[1] - product_balance_sheet_loss[i] += consumer_step_balance_sheet_loss[consumer_index] - product_step_reward[i] += consumer_step_reward[consumer_index] - - if product.seller_id_index_tuple: - seller_index = product.seller_id_index_tuple[1] - product_balance_sheet_profit[i] += seller_balance_sheet_profit[seller_index] - product_balance_sheet_loss[i] += seller_balance_sheet_loss[seller_index] - product_step_reward[i] += seller_step_reward[seller_index] - - if product.manufacture_id_index_tuple: - manufacture_index = product.manufacture_id_index_tuple[1] - product_balance_sheet_loss[i] += manufacture_balance_sheet_loss[manufacture_index] - product_step_reward[i] += manufacture_step_reward[manufacture_index] - - storage_reward = -1 * storages_product_map[product.storage_index][product.sku_id] * product.unit_storage_cost - product_step_reward[i] += storage_reward - product_balance_sheet_loss[i] += storage_reward - - if product.distribution_index is not None: - product_balance_sheet_profit[i] += product_distribution_balance_sheet_profit[i] - product_balance_sheet_loss[i] += product_distribution_balance_sheet_loss[i] - product_step_reward[i] += product_distribution_balance_sheet_loss[i] + product_distribution_balance_sheet_profit[i] - - if len(product.downstream_product_units) > 0: - for did in product.downstream_product_units: - product_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[did]] - product_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[did]] - product_step_reward[i] += product_step_reward[self.product_id2index_dict[did]] - - product_balance_sheet = product_balance_sheet_profit + product_balance_sheet_loss - - return product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet - - def _calc_facility( - self, - storage_balance_sheet_loss, - vehicle_balance_sheet_loss, - product_balance_sheet_profit, - product_balance_sheet_loss, - product_step_reward - ): - num_facilities = len(self.facility_levels) - facility_balance_sheet_loss = np.zeros(num_facilities) - facility_balance_sheet_profit = np.zeros(num_facilities) - facility_step_reward = np.zeros(num_facilities) - - # for facilities - for i, facility in enumerate(self.facility_levels): - # storage balance sheet - # profit=0 - facility_balance_sheet_loss[i] += storage_balance_sheet_loss[facility.storage_index] * facility.unit_storage_cost - - # distribution balance sheet - if facility.distribution_index is not None: - for vidx in facility.vehicle_index_list: - facility_balance_sheet_loss[i] += vehicle_balance_sheet_loss[vidx] - # distribution unit do not provide reward - - # sku product unit balance sheet - for pid in facility.product_unit_id_list: - facility_balance_sheet_profit[i] += product_balance_sheet_profit[self.product_id2index_dict[pid]] - facility_balance_sheet_loss[i] += product_balance_sheet_loss[self.product_id2index_dict[pid]] - facility_step_reward[i] += product_step_reward[self.product_id2index_dict[pid]] - - facility_balance_sheet = facility_balance_sheet_loss + facility_balance_sheet_profit - - return facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet - - def calc(self): - #### Basic Units: Loss, Profit, Reward - consumer_ids, consumer_step_balance_sheet_loss, consumer_step_reward, consumer_step_balance_sheet = self._calc_consumer() - seller_balance_sheet_profit, seller_balance_sheet_loss, seller_step_reward = self._calc_seller() - manufacture_ids, manufacture_balance_sheet_loss, manufacture_step_reward, manufacture_step_balance_sheet = self._calc_manufacture() - storage_balance_sheet_loss, storages_product_map = self._calc_storage() - vehicle_balance_sheet_loss, vehicle_step_reward = self._calc_vehicle() - product_distribution_balance_sheet_profit, product_distribution_balance_sheet_loss = self._calc_product_distribution() - ######################################################################## - - #### Loss, profit, reward for each product - product_balance_sheet_profit, product_balance_sheet_loss, product_step_reward, product_balance_sheet = self._calc_product( - consumer_step_balance_sheet_loss, - consumer_step_reward, - seller_balance_sheet_profit, - seller_balance_sheet_loss, - seller_step_reward, - manufacture_balance_sheet_loss, - manufacture_step_reward, - storages_product_map, - product_distribution_balance_sheet_profit, - product_distribution_balance_sheet_loss - ) - ######################################################################## - - #### Loss, profit, reward for each facility - # facility_balance_sheet_profit, facility_balance_sheet_loss, facility_step_reward, facility_balance_sheet = self._calc_facility( - # storage_balance_sheet_loss, - # vehicle_balance_sheet_loss, - # product_balance_sheet_profit, - # product_balance_sheet_loss, - # product_step_reward - # ) - ######################################################################## - - # Final result for current tick, key is the facility/unit id, value is tuple of balance sheet and reward. - result = {} - - # For product units. - for id, bs, rw in zip([product.unit_id for product in self.products], product_balance_sheet, product_step_reward): - result[id] = (bs, rw) - self.total_balance_sheet[id] += bs - - # For consumers. - for id, bs, rw in zip(consumer_ids, consumer_step_balance_sheet, consumer_step_reward): - # result[id] = (bs, rw) - # let reward of a consumer equate its parent product - result[id] = result[self.consumer_id2product[id]] - self.total_balance_sheet[id] += result[id][0] - - # For producers. - for id, bs, rw in zip(manufacture_ids, manufacture_step_balance_sheet, manufacture_step_reward): - result[id] = (bs, rw) - self.total_balance_sheet[id] += bs - - # NOTE: add followings if you need. - # For storages. - # For distributions. - # For vehicles. - - return result - - -env_config = { - "scenario": "supply_chain", - # Currently available topologies are "sample" or "random". New topologies must consist of a single folder - # that contains a single config.yml and should be placed under /maro/simulator/scenarios/supply_chain/topologies - "topology": "random", - "durations": 100 # number of ticks per episode -} - -def get_env_wrapper(replay_agent_ids=None): - return SCEnvWrapper(env=Env(**env_config), replay_agent_ids=replay_agent_ids) - - -tmp_env_wrapper = get_env_wrapper(replay_agent_ids=[]) -AGENT_IDS = [f"{info.agent_type}.{info.id}" for info in tmp_env_wrapper.agent_idx_list] -STATE_DIM = tmp_env_wrapper.dim -NUM_ACTIONS = 10 - -del tmp_env_wrapper - - -if __name__ == "__main__": - from time import time - import cProfile - - env = Env( - scenario="supply_chain", - topology="sample", - durations=100, - max_snapshots=10) - - ss = SCEnvWrapper(env) - - env.step(None) - - start_time = time() - - # cProfile.run("ss.get_state(None)", sort="cumtime") - states = ss.get_state(None) - print(env.agent_idx_list) - print(ss.cur_balance_sheet_reward) - print(states) - - # end_time = time() - # - # print("time cost:", end_time - start_time) - # - # print("dim:", ss.dim) diff --git a/examples/rl/supply_chain/or_policies.py b/examples/rl/supply_chain/or_policies.py deleted file mode 100644 index 12cc42b71..000000000 --- a/examples/rl/supply_chain/or_policies.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import random - -import numpy as np -import scipy.stats as st - -from maro.rl.policy import AbsPolicy - - -class ProducerBaselinePolicy(AbsPolicy): - def __init__(self): - super().__init__() - - def choose_action(self, state): - return state.get('product_rate', 500) - - -class ConsumerBaselinePolicy(AbsPolicy): - def __init__(self, num_actions: int): - super().__init__() - self.num_actions = num_actions - - def choose_action(self, state): - if state['is_facility']: - return 0 - # consumer_source_inventory - available_inventory = np.array(state['storage_levels']) - inflight_orders = np.array(state['consumer_in_transit_orders']) - booked_inventory = available_inventory + inflight_orders - - # stop placing orders when the facility runs out of capacity - if np.sum(booked_inventory) > state['storage_capacity']: - return 0 - - most_needed_product_id = state['product_idx'] - sale_mean, sale_std = state['sale_mean'], state['sale_std'] - service_level = state['service_level'] - vlt_buffer_days = 7 - vlt = vlt_buffer_days + state['vlt'] - if booked_inventory[most_needed_product_id] > vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level): - return 0 - consumer_action_space_size = self.num_actions - consumer_quantity = random.randint(0, consumer_action_space_size-1) - return consumer_quantity - - -# Q = \sqrt{2DK/h} -# Q - optimal order quantity -# D - annual demand quantity -# K - fixed cost per order, setup cost (not per unit, typically cost of ordering and shipping and handling. This is not the cost of goods) -# h - annual holding cost per unit, -# also known as carrying cost or storage cost (capital cost, warehouse space, -# refrigeration, insurance, etc. usually not related to the unit production cost) -class ConsumerEOQPolicy(AbsPolicy): - def _get_consumer_quantity(self, state): - order_cost = state['order_cost'] - holding_cost = state['unit_storage_cost'] - sale_gamma = state['sale_mean'] - consumer_quantity = int(np.sqrt(2*sale_gamma*order_cost / holding_cost) / sale_gamma) - return consumer_quantity - - def choose_action(self, state): - if state['is_facility']: - return 0 - # consumer_source_inventory - available_inventory = np.array(state['storage_levels']) - inflight_orders = np.array(state['consumer_in_transit_orders']) - booked_inventory = available_inventory + inflight_orders - - # stop placing orders when the facilty runs out of capacity - if np.sum(booked_inventory) > state['storage_capacity']: - return 0 - - most_needed_product_id = state['product_idx'] - vlt_buffer_days = 7 - vlt = vlt_buffer_days + state['vlt'] - sale_mean, sale_std = state['sale_mean'], state['sale_std'] - service_level = state['service_level'] - - # whether replenishment point is reached - if booked_inventory[most_needed_product_id] > vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level): - return 0 - consumer_quantity = self._get_consumer_quantity(state) - return consumer_quantity - - -# parameters: (r, R), calculate according to VLT, demand variances, and service level -# replenish R - S units whenever the current stock is less than r -# S denotes the number of units in stock -class ConsumerMinMaxPolicy(AbsPolicy): - def choose_action(self, state): - if state['is_facility']: - return 0 - # consumer_source_inventory - available_inventory = np.array(state['storage_levels']) - inflight_orders = np.array(state['consumer_in_transit_orders']) - booked_inventory = available_inventory + inflight_orders - - # stop placing orders when the facility runs out of capacity - if np.sum(booked_inventory) > state['storage_capacity']: - return 0 - - most_needed_product_id = state['product_idx'] - # stop placing orders if no risk of out of stock - vlt_buffer_days = 10 - vlt = state['vlt'] + vlt_buffer_days - sale_mean, sale_std = state['sale_mean'], state['sale_std'] - service_level = state['service_level'] - r = (vlt*sale_mean + np.sqrt(vlt)*sale_std*st.norm.ppf(service_level)) - # print(booked_inventory, most_needed_product_id, r) - if booked_inventory[most_needed_product_id] > r: - return 0 - R = 3*r - consumer_quantity = int((R - r) / sale_mean) - return consumer_quantity - - -CONSUMER_NUM_ACTIONS = 10 - -def get_producer_baseline_policy(): - return ProducerBaselinePolicy() - -def get_consumer_baseline_policy(): - return ConsumerBaselinePolicy(CONSUMER_NUM_ACTIONS) - -def get_consumer_minmax_policy(): - return ConsumerMinMaxPolicy() - -def get_consumer_eoq_policy(): - return ConsumerEOQPolicy() diff --git a/examples/rl/supply_chain/policy_index.py b/examples/rl/supply_chain/policy_index.py deleted file mode 100644 index 430282eab..000000000 --- a/examples/rl/supply_chain/policy_index.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl.policy import NullPolicy - -sc_path = os.path.dirname(__file__) -sys.path.insert(0, sc_path) -from dqn import get_dqn_policy -from env_wrapper import AGENT_IDS -from or_policies import ( - get_consumer_baseline_policy, get_consumer_eoq_policy, get_consumer_minmax_policy, get_producer_baseline_policy -) - -NUM_RL_POLICIES = 100 - -non_rl_policy_func_index = { - "consumer": get_consumer_minmax_policy, - "producer": get_producer_baseline_policy, - "facility": lambda: NullPolicy(), - "product": lambda: NullPolicy(), - "productstore": lambda: NullPolicy() -} - -rl_policy_func_index = {f"consumerstore-{i}": get_dqn_policy for i in range(NUM_RL_POLICIES)} -consumerstores = [agent_id for agent_id in AGENT_IDS if agent_id.startswith("consumerstore")] - -agent2policy = { - agent_id: agent_id.split(".")[0] for agent_id in AGENT_IDS if not agent_id.startswith("consumerstore") -} - -for i, agent_id in enumerate(consumerstores): - agent2policy[agent_id] = f"consumerstore-{i % NUM_RL_POLICIES}" - -update_trigger = {name: 128 for name in rl_policy_func_index} -warmup = {name: 1 for name in rl_policy_func_index} diff --git a/examples/rl/supply_chain/sc_state_in_maro.md b/examples/rl/supply_chain/sc_state_in_maro.md deleted file mode 100644 index 0d512e25d..000000000 --- a/examples/rl/supply_chain/sc_state_in_maro.md +++ /dev/null @@ -1,312 +0,0 @@ -# SC state in MARO - - -## Env.summary - -MARO通过summary属性对外提供节点相关信息和其他在环境初始化后不会改变的信息。 -在supply chain场景中, summary中包括了以下部分 - -### unit mapping: - -```python -env.summary["node_mapping"]["unit_mapping"] -``` - -unit id 及其对应的data model名字和索引. - -### facilities: - -```python -env.summary["node_mapping"]["facilities"] -``` - -每个facility的层次结构,如sku list, units - -### skus: - -```python -env.summary["node_mapping"]["skus"] -``` - -当前配置中的所有sku - -### max_price: - -```python -env.summary["node_mapping"]["max_price"] -``` - -当前配置中最大的price - -### max_sources_per_facility: - -```python -env.summary["node_mapping"]["max_sources_per_facility"] -``` - -## States - -MARO中有两种对外提供动态状态(state)的方法。 - - -### Metrics: - -起初是为了对外提供reward相关的信息,但是也可以用来作为对外提供状态的接口,用来对外提供不能存放在snapshot list中的数据,比如字典,或者复杂的数据结构。 - -当前实现包含了以下内容: - -#### products: - -product unit相关信息(sale_mean, sale_std, pending_order_daily)。 - -#### facilities: - -facility相关信息(in_transit_orders, pending_order) - - -### Snapshot list: - -snapshot list是MARO中主要的对外提供状态的接口,它提供了所有节点的历史状态(默认为保存所有历史记录,可配置保存最新的N个来节省内存).返回的结果是numpy array,适合做batch操作。 - -snapshot list中的属性都是按照节点组织起来的,每个节点包括多个属性,每个属性可以有多个slot(array like), 同一种节点类型可以有多个instance.节点及其属性的定义可以在maro/simulator/scenarios/supply_chain/datamodels查看。 - -snapshot list的查询是通过slice接口实现的,形式如下: - -```python -env.snapshot_list["node name"][tick(s):node index(s):attribute name(s)] -> np.array - -``` - -该接口返回的是一个4维(tick, node, attribute, slot)的numpy数组(float). - -其中: -1. node name是定义节点是通过node装饰器提供的名字,当前实现包括如下节点: - -consumer, distribution, facility, manufacture, product, seller, storage, vehicle - - -2. tick(s): 可以是一个int, list或者None, 其中None表示查询当前所有历史记录的状态。 - -3. node index(s): 同tick,可以为int, list或者None,None表示查询当前节点类型的所有实例(instance). 使用中需要注意的是,节点(data model)的index和unit的id并不相同,unit的id是在facility和unit之间连续且唯一的,但是节点的index是每种data model类型内部的索引方式。 -所以在实际使用过程中,通常需要得到每个unit和facility对应的index,这部分信息在env.summary中可以得到。 - -4. attribute name(s): 在对应节点上定义过的属性名,可以为一个str,或者List[str] - - -## 示例 - -### 示例1:通过env.summary构建facility&unit的层级信息 - -这个层级信息可以帮我们在之后的操作中快速索引。 -更详细的可以参考examples/supply_chain/env_wrapper.py, _build_internal_helpers方法。 - -```python -# unit info -class UnitBaseInfo: - id: int = None - node_index: int = None - config: dict = None - summary: dict = None - - def __init__(self, unit_summary): - self.id = unit_summary["id"] - self.node_index = unit_summary["node_index"] - self.config = unit_summary.get("config", {}) - self.summary = unit_summary - - def __getitem__(self, key, default=None): - if key in self.summary: - return self.summary[key] - - return default - -# facility id -> { -# data_model_index: int, -# storage: UnitBaseInfo -# distribution: UnitBaseInfo -# product_id: { -# consumer: UnitBaseInfo -# seller: UnitBaseInfo -# manufacture: UnitBaseInfo -# } -#} -facility_levels = {} - -# 默认env.summary包含node_mapping, node_detail 和 event_payload3个部分, -# 这里只需要node——mapping -summary = env.summary["node_mapping"] - -for facility_id, facility in summary["facilities"].items(): - facility_levels[facility_id] = { - "node_index": facility["node_index"], - "config": facility["configs"], - "upstreams": facility["upstreams"], - "skus": facility["skus"] - } - - # facility所属的unit都挂在units下面。 - units = facility["units"] - - facility_levels[facility_id]["storage"] = UnitBaseInfo(units["storage"]) - facility_levels[facility_id]["distribution"] = UnitBaseInfo(units["distribution"]) - - # 所有的product unit - product_units = units["products"] - - if product_units: - for product_id, product in products.items(): - # product unit 本身也包含state - product_info = { - "product": UnitBaseInfo(product) - } - - # 每个product unit可能包括下面3个unit - # 注意,为了简单我们没有检查对应的key时候存在! - product_info["seller"] = UnitBaseInfo(product["seller"]) - product_info["consumer"] = UnitBaseInfo(product["consumer"]) - product_info["manufacture"] = UnitBaseInfo(product["manufacture"]) - - # 这里我们用product_id作为product 的key,可按照需求更改 - facility_levels[product_id] = product_info -``` - -### 示例2:通过env.summary构建unit id到node index的索引表 - -实际上,在示例的遍历过程中,我们就已经可以得到unit及其对应的node index索引表了,如果你不在意层级关系的话,可以通过unit_mapping快速得到这个索引。 - -```python - -# unit_mapping是一个字典,key是unit id, value是(data model name, data model node index, facility id)类型的tuple。 - -summary = env.summary["node_mapping"] - -unitid2index_mapping = {} - -for unit_id, unit_detail in summary["unit_mapping"].items(): - unitid2index_mapping[unit_id] = unit_detail[1] - -``` - -### 示例3:在state shaping过程中查询seller的销售和需求的历史,时间长度为hist_len - -```python - -# 模拟器当前时间 -cur_tick = env.tick - -# 需要查询的历史长度 -hist_len = 4 - -# 历史长度对象当前时间的时间序列 -ticks = [cur_tick - i for i in range(hist_len-1, -1, -1)] - -# 查询seller节点的过去4(含当前)个tick的sold和demand值 -# NOTE:因为这两个是都是整数,所以做一次类型转换 -seller_states =env.snapshot_list["seller"][ticks::("sold", "demand")].astype(np.int) - -# 结果应为4为numpy array -# 假设我们有2个seller -""" -[ - [ - [ - [0.0], # sold (slot = 1) - [0.0] # demand (slot = 1) - ], # seller 0 - [...] # seller 1 - ], # tick 0 - [ - [...], - [...] - ], # tick 1 - [ - [...], - [...] - ], # tick 2 - [ - [...], - [...] - ] # tick 3 (latest) -] -""" - -# 这样得到的结果就是所有的seller unit对应的销售和需求历史。 - -# 假设我们当前需要的seller unit的data model index 为 1 的话。 -cur_seller_node_index = 1 - -# 那么当前seller的销售和需求历史分别为: -cur_seller_hist = seller_states[:, cur_seller_node_index, :] - -# 第二个参数为0,是因为sold是我们查询的第一个属性 -sale_hist = cur_seller_hist[:, 0].flatten() -demand_hist = cur_seller_hist[:, 1].flatten() - -``` - -### 示例4:计算unit或facility的balance sheet - -详细的可以参考examples/supply_chain/env_wrapper.py中的BalanceSheetCalculator类。 - -```python - -# 假设我们需要计算seller, consumer, manufacture的balance sheet. -# 实际情况需要用这3个计算出对应的product unit的balance sheet,这里只是作为示例 - -# 计算所需要属性 -consumer_features = ("id", "order_quantity", "price", "order_cost", "order_product_cost") -seller_features = ("id", "sold", "demand", "price", "backlog_ratio") -manufacture_features = ("id", "manufacturing_number", "product_unit_cost") - -# 对应的3种data model snapshot list -consumer_ss = env.snapshot_list["consumer"] -seller_ss = env.snapshot_list["seller"] -manufacture_ss = env.snapshot_list["manufacture"] - -# 当前时间 -tick = env.tick - -# 3种unit对应的所有实例的state -# 这里用len(features)做reshape的原因是,当前用到的属性都是slots=1 -# 又因为我们的tick数量为1,这样reshape之后, 每行对应一个unit的实例,每列对应一个属性 -consumer_states = consumer_ss[tick::consumer_features].flatten().reshape(-1, len(consumer_features)) - -seller_states = seller_ss[tick::seller_features].flatten().reshape(-1, len(seller_features)) - -man_states = manufacture_ss[tick::manufacture_features].flatten().reshape(-1, len(manufacture_features)) - -# balance sheet计算,通常balance sheet 包含profit和loss两部分,这里分开保存。 - -# consumer部分 -# profit = quantity * price -consumer_profit = consumer_states[:, 1] * consumer_states[:, 2] - -# loss = -1 * (order_cost + order_product_cost) -consumer_loss = -1 * (consumer_states[:, 3] + consumer_states[:, 4]) - -# discount在代码里似乎没有用到 -reward_discount = 0 - -# consumer step reward -consumer_reward = consumer_loss + consumer_profit * reward_discount - -# seller部分 -# profit = sold * price -seller_profit = seller_states[:, 1] * seller_states[:, 3] - -# loss = -1 * demand * price * backlog_ratio -seller_loss = -1 * seller_states[:, 2] * seller_states[:, 3] * seller_states[:, 4] - -seller_reward = seller_profit + seller_loss - -# manufacture部分 -# profit = 0 -# loss = manufacture_number * cost -man_loss = -1 * man_states[:, 1] * man_states[:, 2] - -man_reward = man_loss - -# 这样我们就用numpy的batch操作完成3种unit的balance sheet和reward计算, -# 后续需要我们按照product/facility对这些结果做聚合, 这需要类似示例1这样的层级结构,具体可参考现有代码。 - -``` \ No newline at end of file diff --git a/maro/simulator/scenarios/supply_chain/__init__.py b/maro/simulator/scenarios/supply_chain/__init__.py deleted file mode 100644 index 175e0392b..000000000 --- a/maro/simulator/scenarios/supply_chain/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .actions import ConsumerAction, ManufactureAction, SupplyChainAction -from .datamodels import ( - ConsumerDataModel, DistributionDataModel, ManufactureDataModel, SellerDataModel, StorageDataModel, VehicleDataModel -) -from .facilities import FacilityBase, RetailerFacility, SupplierFacility, WarehouseFacility -from .units import ( - ConsumerUnit, DistributionUnit, ExtendUnitBase, ProductUnit, SellerUnit, StorageUnit, UnitBase, VehicleUnit -) diff --git a/maro/simulator/scenarios/supply_chain/actions.py b/maro/simulator/scenarios/supply_chain/actions.py deleted file mode 100644 index fbb40d511..000000000 --- a/maro/simulator/scenarios/supply_chain/actions.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -class SupplyChainAction: - def __init__(self, id: int) -> None: - self.id = id - - -class ConsumerAction(SupplyChainAction): - def __init__( - self, id: int, product_id: int, source_id: int, quantity: int, vlt: int, reward_discount: float - ) -> None: - super(ConsumerAction, self).__init__(id=id) - self.product_id = product_id - self.source_id = source_id - self.quantity = quantity - self.vlt = vlt - self.reward_discount = reward_discount - - -class ManufactureAction(SupplyChainAction): - def __init__(self, id: int, production_rate: float) -> None: - super(ManufactureAction, self).__init__(id=id) - self.production_rate = production_rate diff --git a/maro/simulator/scenarios/supply_chain/business_engine.py b/maro/simulator/scenarios/supply_chain/business_engine.py deleted file mode 100644 index 7b7a5ac8b..000000000 --- a/maro/simulator/scenarios/supply_chain/business_engine.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -import os -from typing import List, Tuple - -from maro.event_buffer import MaroEvents -from maro.simulator.scenarios import AbsBusinessEngine - -from .parser import ConfigParser, SupplyChainConfiguration -from .units import ProductUnit, UnitBase -from .world import World - - -class SupplyChainBusinessEngine(AbsBusinessEngine): - def __init__(self, **kwargs): - super().__init__(scenario_name="supply_chain", **kwargs) - - self._register_events() - - self._build_world() - - self._product_units = [] - - # Prepare product unit for later using. - for unit in self.world.units.values(): - if issubclass(type(unit), ProductUnit): - self._product_units.append(unit) - - self._frame = self.world.frame - - self._node_mapping = self.world.get_node_mapping() - - # Used to cache the action from outside, then dispatch to units at the beginning of step. - self._action_cache = None - - self._metrics_cache = None - - @property - def frame(self): - return self._frame - - @property - def snapshots(self): - return self._frame.snapshots - - @property - def configs(self) -> SupplyChainConfiguration: - return self.world.configs - - def step(self, tick: int): - # Clear the metrics cache. - self._metrics_cache = None - - # NOTE: we have to dispatch the action here. - self._dispatch_action() - self._step_by_facility(tick) - - # We do not have payload here. - decision_event = self._event_buffer.gen_decision_event(tick, None) - - self._event_buffer.insert_event(decision_event) - - def post_step(self, tick: int): - self._post_step_by_facility(tick) - - return tick + 1 == self._max_tick - - def reset(self): - self._frame.reset() - - if self._frame.snapshots: - self._frame.snapshots.reset() - - self._reset_by_facility() - - self._action_cache = None - - def set_seed(self, seed: int) -> None: - pass - - def get_node_mapping(self) -> dict: - return self._node_mapping - - def get_agent_idx_list(self) -> List[Tuple[str, int]]: - """Get a list of agent index. - - Returns: - list: List of agent index. - """ - return self.world.agent_list - - def _step_by_facility(self, tick: int): - """Call step functions by facility. - - Args: - tick (int): Current tick. - """ - # Step first. - for facility in self.world.facilities.values(): - facility.step(tick) - - # Then flush states to frame before generate decision event. - for facility in self.world.facilities.values(): - facility.flush_states() - - def _post_step_by_facility(self, tick: int): - """Call post_step functions by facility.""" - for facility in self.world.facilities.values(): - facility.post_step(tick) - - def _reset_by_facility(self): - """Call reset functions by facility.""" - for facility in self.world.facilities.values(): - facility.reset() - - def _register_events(self): - self._event_buffer.register_event_handler( - MaroEvents.TAKE_ACTION, self._on_action_received) - - def _build_world(self): - self.update_config_root_path(__file__) - - # Core configuration always in topologies folder. - be_root = os.path.split(os.path.realpath(__file__))[0] - core_config = os.path.join(be_root, "topologies", "core.yml") - - config_path = os.path.join(self._config_path, "config.yml") - - parser = ConfigParser(core_config, config_path) - - conf = parser.parse() - - self.world = World() - - self.world.build(conf, self.calc_max_snapshots(), self._max_tick) - - def _on_action_received(self, event): - action = event.payload - - if action is not None and type(action) == list and len(action) > 0: - self._action_cache = action - - def _dispatch_action(self): - if self._action_cache is not None: - # NOTE: we assume that the action_cache is a list of action, and each action has an id field. - for action in self._action_cache: - entity = self.world.get_entity(action.id) - - if entity is not None and issubclass(type(entity), UnitBase): - entity.set_action(action) - - self._action_cache = None - - def get_metrics(self): - if self._metrics_cache is None: - self._metrics_cache = { - "products": { - product.id: { - "sale_mean": product.get_sale_mean(), - "sale_std": product.get_sale_std(), - "selling_price": product.get_selling_price(), - "pending_order_daily": - None if product.consumer is None else product.consumer.pending_order_daily - } for product in self._product_units - }, - "facilities": { - facility.id: { - "in_transit_orders": facility.get_in_transit_orders(), - "pending_order": - None if facility.distribution is None else facility.distribution.get_pending_order() - } for facility in self.world.facilities.values() - } - } - - return self._metrics_cache diff --git a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py b/maro/simulator/scenarios/supply_chain/datamodels/__init__.py deleted file mode 100644 index c1a8c9b9a..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .consumer import ConsumerDataModel -from .distribution import DistributionDataModel -from .facility import FacilityDataModel -from .manufacture import ManufactureDataModel -from .product import ProductDataModel -from .seller import SellerDataModel -from .storage import StorageDataModel -from .vehicle import VehicleDataModel diff --git a/maro/simulator/scenarios/supply_chain/datamodels/base.py b/maro/simulator/scenarios/supply_chain/datamodels/base.py deleted file mode 100644 index 834864072..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/base.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, NodeBase - - -class DataModelBase(NodeBase): - """Base of all data model of this scenario.""" - # Id of related unit or facility, 0 is invalid by default. - id = NodeAttribute(AttributeType.Int) - - # Id of facility this unit belongs to. - facility_id = NodeAttribute(AttributeType.Int) - - def __init__(self): - self._unit_id = 0 - self._facility_id = 0 - - def initialize(self, **kwargs): - """Initialize the fields with configs, the config should be a dict. - - Args: - kwargs (dict): Configuration of related data model, used to hold value to - reset after frame reset. - """ - # Called from unit after frame is ready. - pass - - def reset(self): - """Reset after each episode.""" - # Called per episode. - self.id = self._unit_id - self.facility_id = self._facility_id - - def set_id(self, unit_id: int, facility_id: int): - """Used to assign id(s), so that it will be assigned after frame rest. - - Args: - unit_id (int): Id of related unit. - facility_id (int): Id of this unit belongs to. - """ - self._unit_id = unit_id - self._facility_id = facility_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py b/maro/simulator/scenarios/supply_chain/datamodels/consumer.py deleted file mode 100644 index 5cd9855e5..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/consumer.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .extend import ExtendDataModel - - -@node("consumer") -class ConsumerDataModel(ExtendDataModel): - """Data model for consumer unit.""" - total_purchased = NodeAttribute(AttributeType.UInt) - total_received = NodeAttribute(AttributeType.UInt) - - purchased = NodeAttribute(AttributeType.UInt) - received = NodeAttribute(AttributeType.UInt) - order_product_cost = NodeAttribute(AttributeType.UInt) - - latest_consumptions = NodeAttribute(AttributeType.Float) - - order_quantity = NodeAttribute(AttributeType.UInt) - - price = NodeAttribute(AttributeType.Float) - order_cost = NodeAttribute(AttributeType.Float) - - reward_discount = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(ConsumerDataModel, self).__init__() - - self._price = 0 - self._order_cost = 0 - - def initialize(self, price: int, order_cost: int): - self._price = price - self._order_cost = order_cost - - self.reset() - - def reset(self): - super(ConsumerDataModel, self).reset() - - self.price = self._price - self.order_cost = self._order_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py b/maro/simulator/scenarios/supply_chain/datamodels/distribution.py deleted file mode 100644 index ee28e4c7f..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/distribution.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .base import DataModelBase - - -@node("distribution") -class DistributionDataModel(DataModelBase): - """Distribution data model for distribution unit.""" - - remaining_order_quantity = NodeAttribute(AttributeType.UInt) - remaining_order_number = NodeAttribute(AttributeType.UInt) - - def __init__(self): - super(DistributionDataModel, self).__init__() - - def reset(self): - super(DistributionDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/extend.py b/maro/simulator/scenarios/supply_chain/datamodels/extend.py deleted file mode 100644 index e16c09c05..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/extend.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute - -from .base import DataModelBase - - -class ExtendDataModel(DataModelBase): - """Data model for sku related unit.""" - # Product id of this consumer belongs to. - product_id = NodeAttribute(AttributeType.UInt) - - # Parent unit id. - product_unit_id = NodeAttribute(AttributeType.UInt) - - def __init__(self): - super(ExtendDataModel, self).__init__() - - self._product_id = 0 - self._product_unit_id = 0 - - def reset(self): - super(ExtendDataModel, self).reset() - - self.product_id = self._product_id - self.product_unit_id = self._product_unit_id - - def set_product_id(self, product_id: int, product_unit_id: int): - self._product_id = product_id - self._product_unit_id = product_unit_id diff --git a/maro/simulator/scenarios/supply_chain/datamodels/facility.py b/maro/simulator/scenarios/supply_chain/datamodels/facility.py deleted file mode 100644 index 8aae3d146..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/facility.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.frame import node - -from .base import DataModelBase - - -@node("facility") -class FacilityDataModel(DataModelBase): - - def __init__(self): - super(FacilityDataModel, self).__init__() - - def reset(self): - super(FacilityDataModel, self).reset() diff --git a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py b/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py deleted file mode 100644 index f2039c498..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/manufacture.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .extend import ExtendDataModel - - -@node("manufacture") -class ManufactureDataModel(ExtendDataModel): - """Data model for manufacture unit.""" - # Number per tick, different with original manufacturing cost, we just provide number, and cost - # user can determine how to calculate the cost. - manufacturing_number = NodeAttribute(AttributeType.UInt) - - product_unit_cost = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(ManufactureDataModel, self).__init__() - - self._product_unit_cost = 0 - - def initialize(self, product_unit_cost): - self._product_unit_cost = product_unit_cost - - self.reset() - - def reset(self): - super(ManufactureDataModel, self).reset() - - self.product_unit_cost = self._product_unit_cost diff --git a/maro/simulator/scenarios/supply_chain/datamodels/product.py b/maro/simulator/scenarios/supply_chain/datamodels/product.py deleted file mode 100644 index 3c3d4fa56..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/product.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .extend import ExtendDataModel - - -@node("product") -class ProductDataModel(ExtendDataModel): - price = NodeAttribute(AttributeType.Float) - distribution_check_order = NodeAttribute(AttributeType.UInt) - distribution_transport_cost = NodeAttribute(AttributeType.Float) - distribution_delay_order_penalty = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(ProductDataModel, self).__init__() - - self._price = 0 - - def initialize(self, price: float): - self._price = price - - self.reset() - - def reset(self): - super(ProductDataModel, self).reset() - - self.price = self._price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/seller.py b/maro/simulator/scenarios/supply_chain/datamodels/seller.py deleted file mode 100644 index ce4b4b9b9..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/seller.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .extend import ExtendDataModel - - -@node("seller") -class SellerDataModel(ExtendDataModel): - """Data model for seller unit.""" - demand = NodeAttribute(AttributeType.UInt) - sold = NodeAttribute(AttributeType.UInt) - total_demand = NodeAttribute(AttributeType.UInt) - total_sold = NodeAttribute(AttributeType.UInt) - price = NodeAttribute(AttributeType.Float) - backlog_ratio = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(SellerDataModel, self).__init__() - self._price = 0 - self._backlog_ratio = 0 - - def initialize(self, price: int, backlog_ratio: float): - self._price = price - self._backlog_ratio = backlog_ratio - - self.reset() - - def reset(self): - super(SellerDataModel, self).reset() - - self.backlog_ratio = self._backlog_ratio - self.price = self._price diff --git a/maro/simulator/scenarios/supply_chain/datamodels/storage.py b/maro/simulator/scenarios/supply_chain/datamodels/storage.py deleted file mode 100644 index 2e1280961..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/storage.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .base import DataModelBase - - -@node("storage") -class StorageDataModel(DataModelBase): - """Data model for storage unit.""" - remaining_space = NodeAttribute(AttributeType.UInt) - capacity = NodeAttribute(AttributeType.UInt) - - # original is , used to save product and its number - product_list = NodeAttribute(AttributeType.UInt, 1, is_list=True) - product_number = NodeAttribute(AttributeType.UInt, 1, is_list=True) - - def __init__(self): - super(StorageDataModel, self).__init__() - - self._capacity = 0 - self._remaining_space = None - self._product_list = None - self._product_number = None - - def initialize( - self, - capacity: int = 0, - remaining_space: int = None, - product_list: list = None, - product_number: list = None - ): - self._capacity = capacity - self._remaining_space = remaining_space - self._product_list = product_list - self._product_number = product_number - - self.reset() - - def reset(self): - super(StorageDataModel, self).reset() - - self.capacity = self._capacity - - if self._remaining_space is not None: - self.remaining_space = self._remaining_space - else: - self.remaining_space = self._capacity - - if self._product_list is not None: - for id in self._product_list: - self.product_list.append(id) - - if self._product_number is not None: - for n in self._product_number: - self.product_number.append(n) diff --git a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py b/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py deleted file mode 100644 index 34cb1df64..000000000 --- a/maro/simulator/scenarios/supply_chain/datamodels/vehicle.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.backends.backend import AttributeType -from maro.backends.frame import NodeAttribute, node - -from .base import DataModelBase - - -@node("vehicle") -class VehicleDataModel(DataModelBase): - # Number of product. - payload = NodeAttribute(AttributeType.UInt) - - unit_transport_cost = NodeAttribute(AttributeType.Float) - - def __init__(self): - super(VehicleDataModel, self).__init__() - - self._unit_transport_cost = 1 - - def initialize(self, unit_transport_cost: int = 1): - self._unit_transport_cost = unit_transport_cost - - self.reset() - - def reset(self): - super(VehicleDataModel, self).reset() - - self.unit_transport_cost = self._unit_transport_cost diff --git a/maro/simulator/scenarios/supply_chain/easy_config.py b/maro/simulator/scenarios/supply_chain/easy_config.py deleted file mode 100644 index 83e5f7551..000000000 --- a/maro/simulator/scenarios/supply_chain/easy_config.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -class EasyConfig(dict): - """A wrapper base on dictionary, give it ability to access key as property.""" - - # Default value for not exist keys. - default_values: dict = {} - - def __getattr__(self, key): - if key in self: - return self[key] - - if key in self.default_values: - return self.default_values[key] - - return None - - def __setattr__(self, key, value): - self[key] = value - - -class SkuInfo(EasyConfig): - """Sku information wrapper, with default property value.""" - default_values = { - "price": 0, - "vlt": 1, - "product_unit_cost": 0, - "backlog_ratio": 0, - "service_level": 0.90, - "cost": 0 - } diff --git a/maro/simulator/scenarios/supply_chain/facilities/__init__.py b/maro/simulator/scenarios/supply_chain/facilities/__init__.py deleted file mode 100644 index 7ce1f382d..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .facility import FacilityBase -from .outerretailer import OuterRetailerFacility -from .retailer import RetailerFacility -from .supplier import SupplierFacility -from .warehouse import WarehouseFacility diff --git a/maro/simulator/scenarios/supply_chain/facilities/facility.py b/maro/simulator/scenarios/supply_chain/facilities/facility.py deleted file mode 100644 index 73228d6ac..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/facility.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -from __future__ import annotations - -import typing -from abc import ABC -from collections import defaultdict -from typing import Dict, List, Optional - -from maro.simulator.scenarios.supply_chain.easy_config import SkuInfo -from maro.simulator.scenarios.supply_chain.units import DistributionUnit, ProductUnit, StorageUnit - -if typing.TYPE_CHECKING: - from maro.simulator.scenarios.supply_chain.datamodels.base import DataModelBase - from maro.simulator.scenarios.supply_chain.world import World - - -class FacilityBase(ABC): - """Base of all facilities.""" - - # Id of this facility. - id: Optional[int] = None - - # Name of this facility. - name: Optional[str] = None - - # World of this facility belongs to. - world: Optional[World] = None - - # Skus in this facility. - skus: Optional[Dict[int, SkuInfo]] = None - - # Product units for each sku in this facility. - # Key is sku(product) id, value is the instance of product unit. - products: Optional[Dict[int, ProductUnit]] = None - - # Storage unit in this facility. - storage: Optional[StorageUnit] = None - - # Distribution unit in this facility. - distribution: Optional[DistributionUnit] = None - - # Upstream facilities. - # Key is sku id, value is the list of facilities from upstream. - upstreams: Optional[Dict[int, List[FacilityBase]]] = None - - # Down stream facilities, value same as upstreams. - downstreams: Optional[Dict[int, List[FacilityBase]]] = None - - # Configuration of this facility. - configs: Optional[dict] = None - - # Name of data model, from configuration. - data_model_name: Optional[str] = None - - # Index of the data model node. - data_model_index: int = 0 - - data_model: Optional[DataModelBase] = None - - # Children of this facility (valid units). - children: Optional[list] = None - - # Facility's coordinates - x: Optional[int] = None - y: Optional[int] = None - - def __init__(self): - self.upstreams = {} - self.downstreams = {} - self.children = [] - self.skus = {} - - def parse_skus(self, configs: dict): - """Parse sku information from config. - - Args: - configs (dict): Configuration of skus belongs to this facility. - """ - for sku_name, sku_config in configs.items(): - global_sku = self.world.get_sku_by_name(sku_name) - facility_sku = SkuInfo(sku_config) - facility_sku.id = global_sku.id - - self.skus[global_sku.id] = facility_sku - - def parse_configs(self, configs: dict): - """Parse configuration of this facility. - - Args: - configs (dict): Configuration of this facility. - """ - self.configs = configs - - def get_config(self, key: str, default: object = None) -> object: - """Get specified configuration of facility. - - Args: - key (str): Key of the configuration. - default (object): Default value if key not exist, default is None. - - Returns: - object: value in configuration. - """ - return default if self.configs is None else self.configs.get(key, default) - - def initialize(self): - """Initialize this facility after frame is ready.""" - self.data_model.initialize() - - # Put valid units into the children, used to simplify following usage. - if self.storage is not None: - self.children.append(self.storage) - - if self.distribution is not None: - self.children.append(self.distribution) - - if self.products is not None: - for product in self.products.values(): - self.children.append(product) - - def step(self, tick: int): - """Push facility to next step. - - Args: - tick (int): Current simulator tick. - """ - for unit in self.children: - unit.step(tick) - - def flush_states(self): - """Flush states into frame.""" - for unit in self.children: - unit.flush_states() - - def post_step(self, tick: int): - """Post processing at the end of step.""" - for unit in self.children: - unit.post_step(tick) - - def reset(self): - """Reset facility for new episode.""" - for unit in self.children: - unit.reset() - - if self.data_model is not None: - self.data_model.reset() - - def get_in_transit_orders(self): - in_transit_orders = defaultdict(int) - - for product_id, product in self.products.items(): - if product.consumer is not None: - in_transit_orders[product_id] = product.consumer.get_in_transit_quantity() - - return in_transit_orders - - def set_action(self, action: object): - pass - - def get_node_info(self) -> dict: - products_info = {} - - for product_id, product in self.products.items(): - products_info[product_id] = product.get_unit_info() - - return { - "id": self.id, - "name": self.name, - "class": type(self), - "node_index": self.data_model_index, - "units": { - "storage": self.storage.get_unit_info() if self.storage is not None else None, - "distribution": self.distribution.get_unit_info() if self.distribution is not None else None, - "products": products_info - }, - "configs": self.configs, - "skus": self.skus, - "upstreams": { - product_id: [f.id for f in source_list] - for product_id, source_list in self.upstreams.items() - }, - "downstreams": { - product_id: [f.id for f in source_list] - for product_id, source_list in self.downstreams.items() - } - } diff --git a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py b/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py deleted file mode 100644 index 1d8e85efe..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/outerretailer.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.simulator.scenarios.supply_chain.units import DataFileDemandSampler, OuterSellerUnit - -from .retailer import RetailerFacility - -# Mapping for supported sampler. -sampler_mapping = { - "data": DataFileDemandSampler -} - - -class OuterRetailerFacility(RetailerFacility): - """Retailer (store) facility that use outer data as seller demand. - - NOTE: - This require that all product seller is subclass of OuterSellerUnit. - """ - - def initialize(self): - super(OuterRetailerFacility, self).initialize() - - # What kind of sampler we need? - sampler_cls = sampler_mapping[self.configs.get("seller_sampler_type", "data")] - - sampler = sampler_cls(self.configs, self.world) - - # Go though product to find sellers. - for product in self.products.values(): - seller = product.seller - - if seller is not None: - assert issubclass(type(seller), OuterSellerUnit) - - seller.sampler = sampler diff --git a/maro/simulator/scenarios/supply_chain/facilities/retailer.py b/maro/simulator/scenarios/supply_chain/facilities/retailer.py deleted file mode 100644 index 6a08fc64d..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/retailer.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .facility import FacilityBase - - -class RetailerFacility(FacilityBase): - """Retail facility used to generate order from upstream, and sell products by demand.""" - pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/supplier.py b/maro/simulator/scenarios/supply_chain/facilities/supplier.py deleted file mode 100644 index 7ddface02..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/supplier.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .facility import FacilityBase - - -class SupplierFacility(FacilityBase): - """Supplier facilities used to produce products with material products.""" - pass diff --git a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py b/maro/simulator/scenarios/supply_chain/facilities/warehouse.py deleted file mode 100644 index 97725d171..000000000 --- a/maro/simulator/scenarios/supply_chain/facilities/warehouse.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .facility import FacilityBase - - -class WarehouseFacility(FacilityBase): - """Warehouse facility that used to storage products, composed with storage, distribution and product units.""" - pass diff --git a/maro/simulator/scenarios/supply_chain/frame_builder.py b/maro/simulator/scenarios/supply_chain/frame_builder.py deleted file mode 100644 index 9cde1d97a..000000000 --- a/maro/simulator/scenarios/supply_chain/frame_builder.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import List, Tuple - -from maro.backends.frame import FrameBase, FrameNode, NodeBase - - -def build_frame(enable_snapshot: bool, total_snapshots: int, nodes: List[Tuple[NodeBase, str, int]]): - class Frame(FrameBase): - def __init__(self): - # Inject the node definition to frame to support add node dynamically. - for node_cls, name, number in nodes: - setattr(Frame, name, FrameNode(node_cls, number)) - - super().__init__(enable_snapshot=enable_snapshot, total_snapshot=total_snapshots, backend_name="dynamic") - - return Frame() diff --git a/maro/simulator/scenarios/supply_chain/parser.py b/maro/simulator/scenarios/supply_chain/parser.py deleted file mode 100644 index e80124a51..000000000 --- a/maro/simulator/scenarios/supply_chain/parser.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from collections import namedtuple -from importlib import import_module - -from yaml import safe_load - -DataModelDef = namedtuple("DataModelDef", ("alias", "module_path", "class_name", "class_type", "name_in_frame")) -UnitDef = namedtuple("UnitDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) -FacilityDef = namedtuple("FacilityDef", ("alias", "module_path", "class_name", "class_type", "data_model_alias")) - - -def find_class_type(module_path: str, class_name: str) -> type: - """Find class type by module path and class name. - - Args: - module_path (str): Full path of the module. - class_name (str): Class name to find. - - Returns: - type: Type of specified class. - """ - target_module = import_module(module_path) - - return getattr(target_module, class_name) - - -def copy_dict(target: dict, source: dict): - """Copy values from source to target dict. - - Args: - target (dict): Target dictionary to copy to. - source (dict): Source dictionary to copy from. - """ - for k, v in source.items(): - if type(v) != dict: - target[k] = v - else: - if k not in target: - target[k] = {} - - copy_dict(target[k], v) - - -class SupplyChainConfiguration: - """Configuration of supply chain scenario.""" - - def __init__(self): - # Data model definitions. - self.data_models = {} - - # Unit definitions. - self.units = {} - - # Facility definitions. - self.facilities = {} - - # World configurations. - self.world = {} - - # Other settings. - self.settings = {} - - def add_data_definition(self, alias: str, class_name: str, module_path: str, name_in_frame: str): - """Add a data model definition. - - Args: - alias (str): Alias of this data model. - class_name (str): Name of class. - module_path (str): Full path of module. - name_in_frame (str): Data model name in frame. - """ - # Check conflicting. - assert alias not in self.data_models - - self.data_models[alias] = DataModelDef( - alias, - module_path, - class_name, - find_class_type(module_path, class_name), - name_in_frame - ) - - def add_unit_definition(self, alias: str, class_name: str, module_path: str, data_model: str): - """Add unit definition. - - Args: - alias (str): Alias of this data model. - class_name (str): Name of class. - module_path (str): Full path of module. - data_model (str): Data model used for this unit. - """ - assert alias not in self.units - - self.units[alias] = UnitDef( - alias, - module_path, - class_name, - find_class_type(module_path, class_name), - data_model - ) - - def add_facility_definition(self, alias: str, class_name: str, module_path: str, data_model_alias: str): - """Add a facility definition. - - Args: - alias (str): Alias of this facility. - class_name (str): Name of this class. - module_path (str): Full path of the module. - data_model_alias (str): Data model alias. - """ - assert alias not in self.facilities - - self.facilities[alias] = FacilityDef( - alias, - module_path, - class_name, - find_class_type(module_path, class_name), - data_model_alias - ) - - -class ConfigParser: - """Supply chain configuration parser.""" - - def __init__(self, core_path: str, config_path: str): - self._result = SupplyChainConfiguration() - - self._core_path = core_path - self._config_path = config_path - - def parse(self): - """Parse configuration of current scenario. - - Returns: - SupplyChainConfiguration: Configuration result of this scenario. - """ - self._parse_core() - self._parse_config() - - return self._result - - def _parse_core(self): - """Parse configuration from core.yml.""" - with open(self._core_path, "rt") as fp: - conf = safe_load(fp) - - self._parse_core_conf(conf) - - def _parse_core_conf(self, conf: dict): - # Data models. - if "datamodels" in conf: - for module_conf in conf["datamodels"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_data_definition( - class_alias, - class_def["class"], - module_path, - class_def["name_in_frame"] - ) - - # TODO: dup code - # Units. - if "units" in conf: - for module_conf in conf["units"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - # children not in unit definition - self._result.add_unit_definition( - class_alias, - class_def["class"], - module_path, - class_def.get("datamodel", None) - ) - - # Facilities. - if "facilities" in conf: - for module_conf in conf["facilities"]["modules"]: - module_path = module_conf["path"] - - for class_alias, class_def in module_conf["definitions"].items(): - self._result.add_facility_definition( - class_alias, - class_def["class"], - module_path, - class_def.get("datamodel", None) - ) - - def _parse_config(self): - """Parse configurations.""" - with open(self._config_path, "rt") as fp: - conf = safe_load(fp) - - # Read customized core part. - customized_core_conf = conf.get("core", None) - - if customized_core_conf is not None: - self._parse_core_conf(customized_core_conf) - - # Facility definitions is not required, but it would be much simple to config with it - facility_definitions = conf.get("facility_definitions", {}) - world_def = conf["world"] - - # Go through world configurations to generate a full one. - # . Copy other configurations first - for sub_conf_name in ("skus", "topology", "grid"): - self._result.world[sub_conf_name] = world_def[sub_conf_name] - - # . Copy facilities content different if without definition reference. - # or copy from definition first, then override with current. - self._result.world["facilities"] = [] - - for facility_conf in world_def["facilities"]: - facility_ref = facility_conf.get("definition_ref", None) - - facility = {} - - if facility_ref is not None: - # Copy definition from base. - copy_dict(facility, facility_definitions[facility_ref]) - - # Override with current. - copy_dict(facility, facility_conf) - - self._result.world["facilities"].append(facility) - - self._result.settings = conf.get("settings", {}) diff --git a/maro/simulator/scenarios/supply_chain/topologies/core.yml b/maro/simulator/scenarios/supply_chain/topologies/core.yml deleted file mode 100644 index f650719a2..000000000 --- a/maro/simulator/scenarios/supply_chain/topologies/core.yml +++ /dev/null @@ -1,81 +0,0 @@ - -datamodels: - modules: - - path: "maro.simulator.scenarios.supply_chain.datamodels" - definitions: - StorageDataModel: - class: "StorageDataModel" - name_in_frame: "storage" - VehicleDataModel: - class: "VehicleDataModel" - name_in_frame: "vehicle" - DistributionDataModel: - class: "DistributionDataModel" - name_in_frame: "distribution" - ConsumerDataModel: - class: "ConsumerDataModel" - name_in_frame: "consumer" - SellerDataModel: - class: "SellerDataModel" - name_in_frame: "seller" - ManufactureDataModel: - class: "ManufactureDataModel" - name_in_frame: "manufacture" - ProductDataModel: - class: "ProductDataModel" - name_in_frame: "product" - FacilityDataModel: - class: "FacilityDataModel" - name_in_frame: "facility" - -units: - modules: - - path: "maro.simulator.scenarios.supply_chain.units" - definitions: - StorageUnit: - class: "StorageUnit" - datamodel: "StorageDataModel" - VehicleUnit: - class: "VehicleUnit" - datamodel: "VehicleDataModel" - DistributionUnit: - class: "DistributionUnit" - datamodel: "DistributionDataModel" - ConsumerUnit: - class: "ConsumerUnit" - datamodel: "ConsumerDataModel" - SellerUnit: - class: "SellerUnit" - datamodel: "SellerDataModel" - OuterSellerUnit: - class: "OuterSellerUnit" - datamodel: "SellerDataModel" - ManufactureUnit: - class: "ManufactureUnit" - datamodel: "ManufactureDataModel" - SimpleManufactureUnit: - class: "SimpleManufactureUnit" - datamodel: "ManufactureDataModel" - ProductUnit: - class: "ProductUnit" - datamodel: "ProductDataModel" - StoreProductUnit: - class: "StoreProductUnit" - datamodel: "ProductDataModel" - -facilities: - modules: - - path: "maro.simulator.scenarios.supply_chain.facilities" - definitions: - WarehouseFacility: - class: "WarehouseFacility" - datamodel: "FacilityDataModel" - SupplierFacility: - class: "SupplierFacility" - datamodel: "FacilityDataModel" - RetailerFacility: - class: "RetailerFacility" - datamodel: "FacilityDataModel" - OuterRetailerFacility: - class: "OuterRetailerFacility" - datamodel: "FacilityDataModel" diff --git a/maro/simulator/scenarios/supply_chain/topologies/random/config.yml b/maro/simulator/scenarios/supply_chain/topologies/random/config.yml deleted file mode 100644 index 9043327b6..000000000 --- a/maro/simulator/scenarios/supply_chain/topologies/random/config.yml +++ /dev/null @@ -1,29345 +0,0 @@ -facility_definitions: - RetailerFacility: - children: - products: - class: StoreProductUnit - config: - agent_type: productstore - consumer: - class: ConsumerUnit - config: - agent_type: consumerstore - seller: - class: SellerUnit - config: - sale_hist_len: 4 - is_template: true - storage: - class: StorageUnit - class: RetailerFacility - config: - agent_type: facility - SupplierFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: product - consumer: - class: ConsumerUnit - config: - agent_type: consumer - manufacture: - class: SimpleManufactureUnit - config: - agent_type: producer - is_template: true - storage: - class: StorageUnit - class: SupplierFacility - config: - agent_type: facility - WarehouseFacility: - children: - distribution: - class: DistributionUnit - products: - class: ProductUnit - config: - agent_type: product - consumer: - class: ConsumerUnit - config: - agent_type: consumer - is_template: true - storage: - class: StorageUnit - class: WarehouseFacility - config: - agent_type: facility -normal_vehicle: &id001 - class: VehicleUnit - config: - patient: 100 - unit_transport_cost: 1 -settings: - constraint_state_hist_len: 4 - constraint_violate_reward: -1000000.0 - consumption_hist_len: 4 - downsampling_rate: 1 - episod_duration: 21 - gamma: 0.99 - global_reward_weight_consumer: 0.5 - global_reward_weight_producer: 0.5 - heading_timesteps: 7 - initial_balance: 100000 - pending_order_len: 4 - replenishment_discount: 0.9 - reward_normalization: 10000000.0 - sale_hist_len: 4 - tail_timesteps: 7 - total_echelons: 3 -world: - facilities: - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 200 - definition_ref: SupplierFacility - name: SUPPLIER0 - skus: - SKU0: - cost: 289 - init_stock: 3150 - price: 322 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU1: - cost: 255 - init_stock: 1100 - price: 284 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU10: - cost: 61 - init_stock: 1250 - price: 68 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU100: - cost: 14 - init_stock: 3250 - price: 16 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU101: - cost: 75 - init_stock: 3550 - price: 84 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU102: - cost: 295 - init_stock: 4650 - price: 328 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU103: - cost: 300 - init_stock: 4750 - price: 334 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU104: - cost: 44 - init_stock: 3250 - price: 49 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU105: - cost: 99 - init_stock: 2850 - price: 110 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU106: - cost: 225 - init_stock: 3650 - price: 251 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU107: - cost: 380 - init_stock: 4350 - price: 423 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU108: - cost: 412 - init_stock: 4600 - price: 458 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU109: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU11: - cost: 360 - init_stock: 2550 - price: 400 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU110: - cost: 59 - init_stock: 700 - price: 66 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU111: - cost: 234 - init_stock: 3050 - price: 260 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU112: - cost: 54 - init_stock: 4750 - price: 61 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU113: - cost: 313 - init_stock: 1550 - price: 348 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU114: - cost: 350 - init_stock: 1350 - price: 389 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU115: - cost: 257 - init_stock: 4300 - price: 286 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU116: - cost: 446 - init_stock: 3600 - price: 496 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU117: - cost: 288 - init_stock: 4600 - price: 320 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU118: - cost: 164 - init_stock: 1650 - price: 183 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU119: - cost: 188 - init_stock: 1600 - price: 209 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU12: - cost: 100 - init_stock: 4200 - price: 112 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU120: - cost: 108 - init_stock: 4900 - price: 121 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU121: - cost: 36 - init_stock: 4250 - price: 40 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU122: - cost: 393 - init_stock: 350 - price: 437 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU123: - cost: 209 - init_stock: 950 - price: 233 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU124: - cost: 163 - init_stock: 1800 - price: 182 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU125: - cost: 14 - init_stock: 4600 - price: 16 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU126: - cost: 32 - init_stock: 1950 - price: 36 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU127: - cost: 195 - init_stock: 1550 - price: 217 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU128: - cost: 148 - init_stock: 950 - price: 165 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU129: - cost: 128 - init_stock: 2500 - price: 143 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU13: - cost: 285 - init_stock: 2850 - price: 317 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU130: - cost: 313 - init_stock: 4900 - price: 348 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU131: - cost: 57 - init_stock: 2250 - price: 64 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU132: - cost: 384 - init_stock: 1050 - price: 427 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU133: - cost: 201 - init_stock: 1450 - price: 224 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU134: - cost: 302 - init_stock: 3850 - price: 336 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU135: - cost: 137 - init_stock: 5000 - price: 153 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU136: - cost: 179 - init_stock: 3550 - price: 199 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU137: - cost: 83 - init_stock: 3700 - price: 93 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU138: - cost: 205 - init_stock: 1800 - price: 228 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU139: - cost: 186 - init_stock: 1200 - price: 207 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU14: - cost: 241 - init_stock: 3100 - price: 268 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU140: - cost: 234 - init_stock: 1700 - price: 261 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU141: - cost: 171 - init_stock: 2050 - price: 190 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU142: - cost: 288 - init_stock: 1900 - price: 320 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU143: - cost: 286 - init_stock: 1300 - price: 318 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU144: - cost: 360 - init_stock: 600 - price: 400 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU145: - cost: 359 - init_stock: 4100 - price: 399 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU146: - cost: 159 - init_stock: 2400 - price: 177 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU147: - cost: 424 - init_stock: 2800 - price: 472 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU148: - cost: 281 - init_stock: 3850 - price: 313 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU149: - cost: 321 - init_stock: 3850 - price: 357 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU15: - cost: 46 - init_stock: 250 - price: 52 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU150: - cost: 95 - init_stock: 3500 - price: 106 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU151: - cost: 200 - init_stock: 3650 - price: 223 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU152: - cost: 9 - init_stock: 2550 - price: 10 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU153: - cost: 396 - init_stock: 3100 - price: 441 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU154: - cost: 69 - init_stock: 4250 - price: 77 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU155: - cost: 379 - init_stock: 2650 - price: 422 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU156: - cost: 9 - init_stock: 600 - price: 10 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU157: - cost: 369 - init_stock: 3750 - price: 410 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU158: - cost: 130 - init_stock: 4050 - price: 145 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU159: - cost: 173 - init_stock: 1250 - price: 193 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU16: - cost: 157 - init_stock: 2900 - price: 175 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU160: - cost: 413 - init_stock: 4250 - price: 459 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU161: - cost: 215 - init_stock: 2300 - price: 239 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU162: - cost: 142 - init_stock: 250 - price: 158 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU163: - cost: 437 - init_stock: 1950 - price: 486 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU164: - cost: 446 - init_stock: 5000 - price: 496 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU165: - cost: 246 - init_stock: 1650 - price: 274 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU166: - cost: 71 - init_stock: 4450 - price: 79 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU167: - cost: 398 - init_stock: 650 - price: 443 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU168: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU169: - cost: 332 - init_stock: 4900 - price: 369 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU17: - cost: 311 - init_stock: 450 - price: 346 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU170: - cost: 61 - init_stock: 2750 - price: 68 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU171: - cost: 358 - init_stock: 3800 - price: 398 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU172: - cost: 180 - init_stock: 3550 - price: 200 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU173: - cost: 157 - init_stock: 4800 - price: 175 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU174: - cost: 261 - init_stock: 3800 - price: 291 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU175: - cost: 75 - init_stock: 3750 - price: 84 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU176: - cost: 366 - init_stock: 3300 - price: 407 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU177: - cost: 231 - init_stock: 1550 - price: 257 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU178: - cost: 380 - init_stock: 250 - price: 423 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU179: - cost: 447 - init_stock: 4150 - price: 497 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU18: - cost: 232 - init_stock: 4050 - price: 258 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU180: - cost: 195 - init_stock: 2750 - price: 217 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU181: - cost: 128 - init_stock: 3000 - price: 143 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU182: - cost: 393 - init_stock: 4950 - price: 437 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU183: - cost: 130 - init_stock: 400 - price: 145 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU184: - cost: 65 - init_stock: 1200 - price: 73 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU185: - cost: 9 - init_stock: 4500 - price: 10 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU186: - cost: 323 - init_stock: 1100 - price: 359 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU187: - cost: 159 - init_stock: 1500 - price: 177 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU188: - cost: 351 - init_stock: 4350 - price: 391 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU189: - cost: 322 - init_stock: 1750 - price: 358 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU19: - cost: 429 - init_stock: 4550 - price: 477 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU190: - cost: 101 - init_stock: 850 - price: 113 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU191: - cost: 425 - init_stock: 2700 - price: 473 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU192: - cost: 373 - init_stock: 3050 - price: 415 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU193: - cost: 186 - init_stock: 1500 - price: 207 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU194: - cost: 388 - init_stock: 250 - price: 432 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU195: - cost: 196 - init_stock: 1550 - price: 218 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU196: - cost: 44 - init_stock: 3400 - price: 49 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU197: - cost: 272 - init_stock: 2850 - price: 303 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU198: - cost: 152 - init_stock: 2700 - price: 169 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU199: - cost: 404 - init_stock: 1150 - price: 449 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU2: - cost: 297 - init_stock: 3500 - price: 331 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU20: - cost: 301 - init_stock: 1250 - price: 335 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU200: - cost: 58 - init_stock: 1250 - price: 65 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU201: - cost: 93 - init_stock: 2950 - price: 104 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU202: - cost: 127 - init_stock: 3650 - price: 142 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU203: - cost: 396 - init_stock: 4100 - price: 440 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU204: - cost: 440 - init_stock: 2350 - price: 489 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU205: - cost: 117 - init_stock: 5000 - price: 130 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU206: - cost: 301 - init_stock: 550 - price: 335 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU207: - cost: 126 - init_stock: 4000 - price: 140 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU208: - cost: 441 - init_stock: 3850 - price: 491 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU209: - cost: 161 - init_stock: 1000 - price: 179 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU21: - cost: 110 - init_stock: 5000 - price: 123 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU210: - cost: 363 - init_stock: 3450 - price: 404 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU211: - cost: 156 - init_stock: 4550 - price: 174 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU212: - cost: 364 - init_stock: 3950 - price: 405 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU213: - cost: 108 - init_stock: 3200 - price: 121 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU214: - cost: 90 - init_stock: 500 - price: 101 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU215: - cost: 377 - init_stock: 2350 - price: 419 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU216: - cost: 297 - init_stock: 1150 - price: 330 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU217: - cost: 255 - init_stock: 3250 - price: 284 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU218: - cost: 184 - init_stock: 2950 - price: 205 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU219: - cost: 82 - init_stock: 2300 - price: 92 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU22: - cost: 443 - init_stock: 3300 - price: 493 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU220: - cost: 348 - init_stock: 4350 - price: 387 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU221: - cost: 35 - init_stock: 3900 - price: 39 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU222: - cost: 103 - init_stock: 1800 - price: 115 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU223: - cost: 176 - init_stock: 600 - price: 196 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU224: - cost: 348 - init_stock: 250 - price: 387 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU225: - cost: 147 - init_stock: 1450 - price: 164 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU226: - cost: 185 - init_stock: 650 - price: 206 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU227: - cost: 431 - init_stock: 1200 - price: 479 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU228: - cost: 12 - init_stock: 4500 - price: 14 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU229: - cost: 424 - init_stock: 2200 - price: 472 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU23: - cost: 348 - init_stock: 2100 - price: 387 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU230: - cost: 216 - init_stock: 1150 - price: 241 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU231: - cost: 43 - init_stock: 4050 - price: 48 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU232: - cost: 201 - init_stock: 4800 - price: 224 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU233: - cost: 324 - init_stock: 3750 - price: 360 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU234: - cost: 258 - init_stock: 250 - price: 287 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU235: - cost: 21 - init_stock: 2850 - price: 24 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU236: - cost: 139 - init_stock: 2750 - price: 155 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU237: - cost: 389 - init_stock: 2250 - price: 433 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU238: - cost: 57 - init_stock: 3300 - price: 64 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU239: - cost: 92 - init_stock: 4900 - price: 103 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU24: - cost: 87 - init_stock: 2350 - price: 97 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU240: - cost: 335 - init_stock: 2350 - price: 373 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU241: - cost: 395 - init_stock: 3550 - price: 439 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU242: - cost: 15 - init_stock: 2200 - price: 17 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU243: - cost: 316 - init_stock: 700 - price: 352 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU244: - cost: 156 - init_stock: 4100 - price: 174 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU245: - cost: 363 - init_stock: 3300 - price: 404 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU246: - cost: 270 - init_stock: 1500 - price: 300 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU247: - cost: 347 - init_stock: 1750 - price: 386 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU248: - cost: 319 - init_stock: 1450 - price: 355 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU249: - cost: 320 - init_stock: 1250 - price: 356 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU25: - cost: 144 - init_stock: 250 - price: 161 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU250: - cost: 114 - init_stock: 2700 - price: 127 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU251: - cost: 309 - init_stock: 3050 - price: 344 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU252: - cost: 162 - init_stock: 4150 - price: 181 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU253: - cost: 403 - init_stock: 800 - price: 448 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU254: - cost: 435 - init_stock: 2300 - price: 484 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU255: - cost: 261 - init_stock: 3350 - price: 290 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU256: - cost: 81 - init_stock: 3600 - price: 91 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU257: - cost: 313 - init_stock: 2850 - price: 348 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU258: - cost: 440 - init_stock: 2150 - price: 489 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU259: - cost: 299 - init_stock: 3450 - price: 333 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU26: - cost: 206 - init_stock: 3150 - price: 229 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU260: - cost: 438 - init_stock: 2600 - price: 487 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU261: - cost: 331 - init_stock: 1100 - price: 368 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU262: - cost: 298 - init_stock: 3900 - price: 332 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU263: - cost: 170 - init_stock: 3700 - price: 189 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU264: - cost: 324 - init_stock: 3950 - price: 361 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU265: - cost: 257 - init_stock: 2950 - price: 286 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU266: - cost: 115 - init_stock: 2350 - price: 128 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU267: - cost: 69 - init_stock: 4000 - price: 77 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU268: - cost: 198 - init_stock: 4450 - price: 221 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU269: - cost: 113 - init_stock: 2200 - price: 126 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU27: - cost: 333 - init_stock: 2800 - price: 370 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU270: - cost: 163 - init_stock: 3700 - price: 182 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU271: - cost: 207 - init_stock: 900 - price: 230 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU272: - cost: 329 - init_stock: 850 - price: 366 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU273: - cost: 378 - init_stock: 900 - price: 421 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU274: - cost: 26 - init_stock: 3850 - price: 29 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU275: - cost: 45 - init_stock: 2400 - price: 50 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU276: - cost: 146 - init_stock: 2700 - price: 163 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU277: - cost: 404 - init_stock: 2050 - price: 449 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU278: - cost: 94 - init_stock: 600 - price: 105 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU279: - cost: 45 - init_stock: 1950 - price: 51 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU28: - cost: 187 - init_stock: 2350 - price: 208 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU280: - cost: 265 - init_stock: 1650 - price: 295 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU281: - cost: 355 - init_stock: 3750 - price: 395 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU282: - cost: 56 - init_stock: 2300 - price: 63 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU283: - cost: 352 - init_stock: 4600 - price: 392 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU284: - cost: 309 - init_stock: 3350 - price: 344 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU285: - cost: 119 - init_stock: 4550 - price: 133 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU286: - cost: 303 - init_stock: 1950 - price: 337 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU287: - cost: 337 - init_stock: 2800 - price: 375 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU288: - cost: 162 - init_stock: 1900 - price: 181 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU289: - cost: 60 - init_stock: 1550 - price: 67 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU29: - cost: 220 - init_stock: 2900 - price: 245 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU290: - cost: 394 - init_stock: 3350 - price: 438 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU291: - cost: 84 - init_stock: 3050 - price: 94 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU292: - cost: 167 - init_stock: 250 - price: 186 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU293: - cost: 145 - init_stock: 2750 - price: 162 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU294: - cost: 368 - init_stock: 450 - price: 409 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU295: - cost: 391 - init_stock: 4650 - price: 435 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU296: - cost: 333 - init_stock: 4600 - price: 370 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU297: - cost: 268 - init_stock: 1900 - price: 298 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU298: - cost: 257 - init_stock: 1750 - price: 286 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU299: - cost: 330 - init_stock: 2550 - price: 367 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU3: - cost: 364 - init_stock: 600 - price: 405 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU30: - cost: 323 - init_stock: 3450 - price: 359 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU300: - cost: 338 - init_stock: 2900 - price: 376 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU301: - cost: 389 - init_stock: 4150 - price: 433 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU302: - cost: 165 - init_stock: 550 - price: 184 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU303: - cost: 221 - init_stock: 4700 - price: 246 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU304: - cost: 377 - init_stock: 1150 - price: 419 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU305: - cost: 445 - init_stock: 5000 - price: 495 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU306: - cost: 431 - init_stock: 2100 - price: 479 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU307: - cost: 189 - init_stock: 3900 - price: 210 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU308: - cost: 187 - init_stock: 250 - price: 208 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU309: - cost: 90 - init_stock: 4600 - price: 101 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU31: - cost: 62 - init_stock: 2800 - price: 69 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU310: - cost: 210 - init_stock: 2200 - price: 234 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU311: - cost: 275 - init_stock: 4000 - price: 306 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU312: - cost: 261 - init_stock: 1250 - price: 291 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU313: - cost: 291 - init_stock: 1900 - price: 324 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU314: - cost: 363 - init_stock: 1450 - price: 404 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU315: - cost: 423 - init_stock: 4200 - price: 471 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU316: - cost: 181 - init_stock: 900 - price: 202 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU317: - cost: 194 - init_stock: 1200 - price: 216 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU318: - cost: 250 - init_stock: 4250 - price: 278 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU319: - cost: 231 - init_stock: 2650 - price: 257 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU32: - cost: 302 - init_stock: 1100 - price: 336 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU320: - cost: 176 - init_stock: 1950 - price: 196 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU321: - cost: 60 - init_stock: 800 - price: 67 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU322: - cost: 216 - init_stock: 5000 - price: 240 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU323: - cost: 59 - init_stock: 1950 - price: 66 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU324: - cost: 397 - init_stock: 4650 - price: 442 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU325: - cost: 407 - init_stock: 3450 - price: 453 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU326: - cost: 310 - init_stock: 1200 - price: 345 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU327: - cost: 411 - init_stock: 700 - price: 457 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU328: - cost: 351 - init_stock: 2250 - price: 390 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU329: - cost: 60 - init_stock: 2100 - price: 67 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU33: - cost: 374 - init_stock: 4600 - price: 416 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU330: - cost: 275 - init_stock: 1950 - price: 306 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU331: - cost: 124 - init_stock: 2050 - price: 138 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU332: - cost: 351 - init_stock: 4800 - price: 390 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU333: - cost: 436 - init_stock: 2650 - price: 485 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU334: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU335: - cost: 72 - init_stock: 4050 - price: 80 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU336: - cost: 266 - init_stock: 1400 - price: 296 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU337: - cost: 220 - init_stock: 1450 - price: 245 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU338: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU339: - cost: 238 - init_stock: 3150 - price: 265 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU34: - cost: 85 - init_stock: 650 - price: 95 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU340: - cost: 432 - init_stock: 4350 - price: 480 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU341: - cost: 415 - init_stock: 3500 - price: 462 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU342: - cost: 129 - init_stock: 450 - price: 144 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU343: - cost: 279 - init_stock: 750 - price: 310 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU344: - cost: 321 - init_stock: 4350 - price: 357 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU345: - cost: 397 - init_stock: 4450 - price: 442 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU346: - cost: 315 - init_stock: 2100 - price: 350 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU347: - cost: 447 - init_stock: 4100 - price: 497 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU348: - cost: 132 - init_stock: 1000 - price: 147 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU349: - cost: 60 - init_stock: 3350 - price: 67 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU35: - cost: 240 - init_stock: 4300 - price: 267 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU350: - cost: 251 - init_stock: 4600 - price: 279 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU351: - cost: 139 - init_stock: 3350 - price: 155 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU352: - cost: 63 - init_stock: 900 - price: 71 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU353: - cost: 227 - init_stock: 4650 - price: 253 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU354: - cost: 356 - init_stock: 3950 - price: 396 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU355: - cost: 56 - init_stock: 500 - price: 63 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU356: - cost: 313 - init_stock: 1450 - price: 348 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU357: - cost: 449 - init_stock: 4600 - price: 499 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU358: - cost: 242 - init_stock: 3450 - price: 269 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU359: - cost: 73 - init_stock: 3750 - price: 82 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU36: - cost: 94 - init_stock: 500 - price: 105 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU360: - cost: 435 - init_stock: 1250 - price: 484 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU361: - cost: 146 - init_stock: 4500 - price: 163 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU362: - cost: 417 - init_stock: 4350 - price: 464 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU363: - cost: 46 - init_stock: 3900 - price: 52 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU364: - cost: 348 - init_stock: 1650 - price: 387 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU365: - cost: 142 - init_stock: 4150 - price: 158 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU366: - cost: 399 - init_stock: 4300 - price: 444 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU367: - cost: 244 - init_stock: 2300 - price: 272 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU368: - cost: 424 - init_stock: 1650 - price: 472 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU369: - cost: 86 - init_stock: 3750 - price: 96 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU37: - cost: 59 - init_stock: 2950 - price: 66 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU370: - cost: 100 - init_stock: 750 - price: 112 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU371: - cost: 295 - init_stock: 250 - price: 328 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU372: - cost: 60 - init_stock: 1600 - price: 67 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU373: - cost: 52 - init_stock: 3350 - price: 58 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU374: - cost: 34 - init_stock: 2700 - price: 38 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU375: - cost: 254 - init_stock: 3600 - price: 283 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU376: - cost: 140 - init_stock: 4100 - price: 156 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU377: - cost: 70 - init_stock: 3350 - price: 78 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU378: - cost: 381 - init_stock: 1750 - price: 424 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU379: - cost: 9 - init_stock: 2450 - price: 11 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU38: - cost: 309 - init_stock: 2150 - price: 344 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU380: - cost: 353 - init_stock: 2650 - price: 393 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU381: - cost: 428 - init_stock: 300 - price: 476 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU382: - cost: 112 - init_stock: 3550 - price: 125 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU383: - cost: 273 - init_stock: 4600 - price: 304 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU384: - cost: 288 - init_stock: 450 - price: 320 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU385: - cost: 108 - init_stock: 650 - price: 120 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU386: - cost: 265 - init_stock: 1550 - price: 295 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU387: - cost: 100 - init_stock: 4850 - price: 112 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU388: - cost: 364 - init_stock: 2200 - price: 405 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU389: - cost: 338 - init_stock: 3500 - price: 376 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU39: - cost: 22 - init_stock: 2550 - price: 25 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU390: - cost: 129 - init_stock: 1950 - price: 144 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU391: - cost: 209 - init_stock: 3350 - price: 233 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU392: - cost: 146 - init_stock: 3700 - price: 163 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU393: - cost: 438 - init_stock: 3350 - price: 487 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU394: - cost: 138 - init_stock: 2650 - price: 154 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU395: - cost: 439 - init_stock: 1650 - price: 488 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU396: - cost: 299 - init_stock: 3050 - price: 333 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU397: - cost: 414 - init_stock: 2550 - price: 460 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU398: - cost: 210 - init_stock: 2900 - price: 234 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU399: - cost: 338 - init_stock: 1850 - price: 376 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU4: - cost: 395 - init_stock: 350 - price: 439 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU40: - cost: 441 - init_stock: 4950 - price: 490 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU400: - cost: 400 - init_stock: 4200 - price: 445 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU401: - cost: 369 - init_stock: 3900 - price: 410 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU402: - cost: 77 - init_stock: 350 - price: 86 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU403: - cost: 80 - init_stock: 4950 - price: 89 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU404: - cost: 258 - init_stock: 3050 - price: 287 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU405: - cost: 414 - init_stock: 950 - price: 460 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU406: - cost: 294 - init_stock: 5000 - price: 327 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU407: - cost: 23 - init_stock: 2300 - price: 26 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU408: - cost: 399 - init_stock: 400 - price: 444 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU409: - cost: 231 - init_stock: 4550 - price: 257 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU41: - cost: 126 - init_stock: 3950 - price: 141 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU410: - cost: 63 - init_stock: 800 - price: 70 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU411: - cost: 189 - init_stock: 4750 - price: 210 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU412: - cost: 257 - init_stock: 3100 - price: 286 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU413: - cost: 362 - init_stock: 4150 - price: 403 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU414: - cost: 148 - init_stock: 4350 - price: 165 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU415: - cost: 261 - init_stock: 1150 - price: 291 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU416: - cost: 205 - init_stock: 450 - price: 228 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU417: - cost: 398 - init_stock: 3600 - price: 443 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU418: - cost: 412 - init_stock: 650 - price: 458 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU419: - cost: 272 - init_stock: 4450 - price: 303 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU42: - cost: 415 - init_stock: 1300 - price: 462 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU420: - cost: 241 - init_stock: 2100 - price: 268 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU421: - cost: 192 - init_stock: 2300 - price: 214 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU422: - cost: 46 - init_stock: 2700 - price: 52 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU423: - cost: 169 - init_stock: 3300 - price: 188 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU424: - cost: 170 - init_stock: 550 - price: 189 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU425: - cost: 145 - init_stock: 600 - price: 162 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU426: - cost: 112 - init_stock: 4900 - price: 125 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU427: - cost: 371 - init_stock: 4700 - price: 413 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU428: - cost: 351 - init_stock: 3150 - price: 391 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU429: - cost: 35 - init_stock: 2050 - price: 39 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU43: - cost: 195 - init_stock: 3400 - price: 217 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU430: - cost: 194 - init_stock: 3650 - price: 216 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU431: - cost: 295 - init_stock: 1050 - price: 328 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU432: - cost: 22 - init_stock: 2300 - price: 25 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU433: - cost: 313 - init_stock: 2250 - price: 348 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU434: - cost: 33 - init_stock: 1500 - price: 37 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU435: - cost: 244 - init_stock: 1500 - price: 272 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU436: - cost: 64 - init_stock: 4050 - price: 72 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU437: - cost: 103 - init_stock: 700 - price: 115 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU438: - cost: 128 - init_stock: 500 - price: 143 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU439: - cost: 230 - init_stock: 1900 - price: 256 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU44: - cost: 324 - init_stock: 2400 - price: 360 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU440: - cost: 223 - init_stock: 4100 - price: 248 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU441: - cost: 227 - init_stock: 2800 - price: 253 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU442: - cost: 423 - init_stock: 4400 - price: 470 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU443: - cost: 196 - init_stock: 3650 - price: 218 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU444: - cost: 140 - init_stock: 300 - price: 156 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU445: - cost: 234 - init_stock: 4300 - price: 260 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU446: - cost: 447 - init_stock: 2450 - price: 497 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU447: - cost: 176 - init_stock: 250 - price: 196 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU448: - cost: 436 - init_stock: 3550 - price: 485 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU449: - cost: 253 - init_stock: 1900 - price: 282 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU45: - cost: 227 - init_stock: 500 - price: 253 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU450: - cost: 52 - init_stock: 4900 - price: 58 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU451: - cost: 421 - init_stock: 3450 - price: 468 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU452: - cost: 56 - init_stock: 3950 - price: 63 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU453: - cost: 33 - init_stock: 1750 - price: 37 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU454: - cost: 444 - init_stock: 4250 - price: 494 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU455: - cost: 122 - init_stock: 1300 - price: 136 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU456: - cost: 304 - init_stock: 1250 - price: 338 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU457: - cost: 152 - init_stock: 3200 - price: 169 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU458: - cost: 362 - init_stock: 350 - price: 403 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU459: - cost: 382 - init_stock: 1700 - price: 425 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU46: - cost: 269 - init_stock: 2500 - price: 299 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU460: - cost: 364 - init_stock: 3650 - price: 405 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU461: - cost: 225 - init_stock: 2400 - price: 251 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU462: - cost: 403 - init_stock: 450 - price: 448 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU463: - cost: 168 - init_stock: 4100 - price: 187 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU464: - cost: 251 - init_stock: 1700 - price: 279 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU465: - cost: 132 - init_stock: 5000 - price: 147 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU466: - cost: 87 - init_stock: 2100 - price: 97 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU467: - cost: 127 - init_stock: 1800 - price: 142 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU468: - cost: 49 - init_stock: 2500 - price: 55 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU469: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU47: - cost: 108 - init_stock: 1950 - price: 121 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU470: - cost: 335 - init_stock: 1600 - price: 373 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU471: - cost: 283 - init_stock: 3550 - price: 315 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU472: - cost: 378 - init_stock: 2950 - price: 421 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU473: - cost: 175 - init_stock: 1050 - price: 195 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU474: - cost: 403 - init_stock: 4300 - price: 448 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU475: - cost: 30 - init_stock: 4700 - price: 34 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU476: - cost: 225 - init_stock: 4500 - price: 251 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU477: - cost: 260 - init_stock: 2350 - price: 289 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU478: - cost: 431 - init_stock: 4400 - price: 479 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU479: - cost: 329 - init_stock: 1900 - price: 366 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU48: - cost: 306 - init_stock: 2900 - price: 340 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU480: - cost: 16 - init_stock: 1500 - price: 18 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU481: - cost: 297 - init_stock: 4400 - price: 330 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU482: - cost: 84 - init_stock: 4350 - price: 94 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU483: - cost: 268 - init_stock: 1700 - price: 298 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU484: - cost: 75 - init_stock: 3650 - price: 84 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU485: - cost: 286 - init_stock: 3350 - price: 318 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU486: - cost: 109 - init_stock: 2600 - price: 122 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU487: - cost: 249 - init_stock: 3300 - price: 277 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU488: - cost: 32 - init_stock: 1350 - price: 36 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU489: - cost: 53 - init_stock: 1550 - price: 59 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU49: - cost: 236 - init_stock: 4750 - price: 263 - product_unit_cost: 1 - production_rate: 4750 - service_level: 0.95 - type: production - vlt: 3 - SKU490: - cost: 144 - init_stock: 4050 - price: 161 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU491: - cost: 399 - init_stock: 3750 - price: 444 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU492: - cost: 47 - init_stock: 4000 - price: 53 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU493: - cost: 414 - init_stock: 1350 - price: 461 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU494: - cost: 130 - init_stock: 4700 - price: 145 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU495: - cost: 134 - init_stock: 3100 - price: 149 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU496: - cost: 307 - init_stock: 2950 - price: 342 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU497: - cost: 29 - init_stock: 450 - price: 33 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU498: - cost: 274 - init_stock: 3250 - price: 305 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU499: - cost: 276 - init_stock: 1450 - price: 307 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU5: - cost: 419 - init_stock: 3550 - price: 466 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU50: - cost: 253 - init_stock: 1850 - price: 282 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU500: - cost: 187 - init_stock: 2100 - price: 208 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU501: - cost: 174 - init_stock: 3800 - price: 194 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU502: - cost: 62 - init_stock: 3250 - price: 69 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU503: - cost: 187 - init_stock: 2850 - price: 208 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU504: - cost: 225 - init_stock: 1500 - price: 251 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU505: - cost: 383 - init_stock: 2950 - price: 426 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU506: - cost: 134 - init_stock: 4650 - price: 149 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU507: - cost: 168 - init_stock: 750 - price: 187 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU508: - cost: 244 - init_stock: 4800 - price: 272 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU509: - cost: 267 - init_stock: 4050 - price: 297 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU51: - cost: 265 - init_stock: 3350 - price: 295 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU510: - cost: 84 - init_stock: 450 - price: 94 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU511: - cost: 324 - init_stock: 3750 - price: 361 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU512: - cost: 420 - init_stock: 1100 - price: 467 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU513: - cost: 308 - init_stock: 350 - price: 343 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU514: - cost: 167 - init_stock: 3150 - price: 186 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU515: - cost: 130 - init_stock: 2700 - price: 145 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU516: - cost: 57 - init_stock: 1900 - price: 64 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU517: - cost: 105 - init_stock: 450 - price: 117 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU518: - cost: 23 - init_stock: 1050 - price: 26 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU519: - cost: 209 - init_stock: 2500 - price: 233 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU52: - cost: 19 - init_stock: 3350 - price: 22 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU520: - cost: 188 - init_stock: 1100 - price: 209 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU521: - cost: 198 - init_stock: 2500 - price: 220 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU522: - cost: 140 - init_stock: 4350 - price: 156 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU523: - cost: 58 - init_stock: 5000 - price: 65 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU524: - cost: 264 - init_stock: 4700 - price: 294 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU525: - cost: 388 - init_stock: 2100 - price: 432 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU526: - cost: 377 - init_stock: 1200 - price: 419 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU527: - cost: 117 - init_stock: 3500 - price: 131 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU528: - cost: 284 - init_stock: 1050 - price: 316 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU529: - cost: 268 - init_stock: 1550 - price: 298 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU53: - cost: 135 - init_stock: 3500 - price: 151 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU530: - cost: 117 - init_stock: 2250 - price: 131 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU531: - cost: 92 - init_stock: 3000 - price: 103 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU532: - cost: 315 - init_stock: 3850 - price: 351 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU533: - cost: 266 - init_stock: 2100 - price: 296 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU534: - cost: 382 - init_stock: 2050 - price: 425 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU535: - cost: 273 - init_stock: 4300 - price: 304 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU536: - cost: 322 - init_stock: 2600 - price: 358 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU537: - cost: 288 - init_stock: 450 - price: 321 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU538: - cost: 411 - init_stock: 2500 - price: 457 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU539: - cost: 148 - init_stock: 3900 - price: 165 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU54: - cost: 142 - init_stock: 2300 - price: 158 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU540: - cost: 349 - init_stock: 2100 - price: 388 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU541: - cost: 117 - init_stock: 1450 - price: 131 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU542: - cost: 34 - init_stock: 1300 - price: 38 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU543: - cost: 387 - init_stock: 4250 - price: 430 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU544: - cost: 311 - init_stock: 4850 - price: 346 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU545: - cost: 157 - init_stock: 750 - price: 175 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU546: - cost: 441 - init_stock: 4800 - price: 491 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU547: - cost: 144 - init_stock: 2200 - price: 161 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU548: - cost: 99 - init_stock: 300 - price: 111 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU549: - cost: 348 - init_stock: 3950 - price: 387 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU55: - cost: 433 - init_stock: 3250 - price: 482 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU550: - cost: 233 - init_stock: 4700 - price: 259 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU551: - cost: 41 - init_stock: 1550 - price: 46 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU552: - cost: 171 - init_stock: 4500 - price: 191 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU553: - cost: 187 - init_stock: 1500 - price: 208 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU554: - cost: 306 - init_stock: 2050 - price: 340 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU555: - cost: 440 - init_stock: 4200 - price: 489 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU556: - cost: 99 - init_stock: 1500 - price: 110 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU557: - cost: 300 - init_stock: 3650 - price: 334 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU558: - cost: 359 - init_stock: 850 - price: 399 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU559: - cost: 281 - init_stock: 2700 - price: 313 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU56: - cost: 339 - init_stock: 3700 - price: 377 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU560: - cost: 254 - init_stock: 2050 - price: 283 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU561: - cost: 181 - init_stock: 1950 - price: 202 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU562: - cost: 312 - init_stock: 4450 - price: 347 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU563: - cost: 360 - init_stock: 250 - price: 401 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU564: - cost: 98 - init_stock: 450 - price: 109 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU565: - cost: 51 - init_stock: 3750 - price: 57 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU566: - cost: 117 - init_stock: 550 - price: 131 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU567: - cost: 324 - init_stock: 450 - price: 361 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU568: - cost: 67 - init_stock: 3900 - price: 75 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU569: - cost: 425 - init_stock: 2400 - price: 473 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU57: - cost: 323 - init_stock: 2500 - price: 359 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU570: - cost: 349 - init_stock: 4150 - price: 388 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU571: - cost: 151 - init_stock: 1950 - price: 168 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU572: - cost: 23 - init_stock: 2250 - price: 26 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU573: - cost: 221 - init_stock: 2050 - price: 246 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU574: - cost: 84 - init_stock: 2500 - price: 94 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU575: - cost: 213 - init_stock: 3950 - price: 237 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU576: - cost: 238 - init_stock: 3900 - price: 265 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU577: - cost: 16 - init_stock: 4350 - price: 18 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU578: - cost: 90 - init_stock: 3550 - price: 100 - product_unit_cost: 1 - production_rate: 3550 - service_level: 0.95 - type: production - vlt: 3 - SKU579: - cost: 373 - init_stock: 450 - price: 415 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU58: - cost: 325 - init_stock: 2400 - price: 362 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU580: - cost: 182 - init_stock: 250 - price: 203 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU581: - cost: 136 - init_stock: 4300 - price: 152 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU582: - cost: 323 - init_stock: 4800 - price: 359 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU583: - cost: 77 - init_stock: 4350 - price: 86 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU584: - cost: 29 - init_stock: 2250 - price: 33 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU585: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU586: - cost: 54 - init_stock: 2200 - price: 61 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU587: - cost: 261 - init_stock: 3900 - price: 290 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU588: - cost: 428 - init_stock: 2200 - price: 476 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU589: - cost: 219 - init_stock: 1700 - price: 244 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU59: - cost: 130 - init_stock: 2550 - price: 145 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU590: - cost: 63 - init_stock: 1550 - price: 70 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU591: - cost: 185 - init_stock: 4200 - price: 206 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU592: - cost: 196 - init_stock: 2300 - price: 218 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU593: - cost: 111 - init_stock: 2200 - price: 124 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU594: - cost: 214 - init_stock: 2500 - price: 238 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU595: - cost: 65 - init_stock: 2150 - price: 73 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU596: - cost: 388 - init_stock: 350 - price: 432 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU597: - cost: 226 - init_stock: 4350 - price: 252 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU598: - cost: 313 - init_stock: 700 - price: 348 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU599: - cost: 48 - init_stock: 3400 - price: 54 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU6: - cost: 109 - init_stock: 4400 - price: 122 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU60: - cost: 180 - init_stock: 1950 - price: 200 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU600: - cost: 347 - init_stock: 3250 - price: 386 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU601: - cost: 124 - init_stock: 1950 - price: 138 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU602: - cost: 165 - init_stock: 4900 - price: 184 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU603: - cost: 250 - init_stock: 2100 - price: 278 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU604: - cost: 243 - init_stock: 2500 - price: 270 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU605: - cost: 259 - init_stock: 1200 - price: 288 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU606: - cost: 102 - init_stock: 550 - price: 114 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU607: - cost: 187 - init_stock: 3950 - price: 208 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU608: - cost: 35 - init_stock: 3650 - price: 39 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU609: - cost: 91 - init_stock: 950 - price: 102 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU61: - cost: 414 - init_stock: 3750 - price: 461 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU610: - cost: 404 - init_stock: 3400 - price: 449 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU611: - cost: 275 - init_stock: 3850 - price: 306 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU612: - cost: 351 - init_stock: 4400 - price: 391 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU613: - cost: 156 - init_stock: 350 - price: 174 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU614: - cost: 155 - init_stock: 750 - price: 173 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU615: - cost: 297 - init_stock: 4550 - price: 330 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU616: - cost: 208 - init_stock: 3650 - price: 232 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU617: - cost: 182 - init_stock: 3000 - price: 203 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU618: - cost: 69 - init_stock: 1150 - price: 77 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU619: - cost: 170 - init_stock: 4300 - price: 189 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU62: - cost: 111 - init_stock: 450 - price: 124 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU620: - cost: 207 - init_stock: 1950 - price: 231 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU621: - cost: 179 - init_stock: 3600 - price: 199 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU622: - cost: 227 - init_stock: 3250 - price: 253 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU623: - cost: 301 - init_stock: 2750 - price: 335 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU624: - cost: 433 - init_stock: 2700 - price: 482 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU625: - cost: 41 - init_stock: 4450 - price: 46 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU626: - cost: 405 - init_stock: 4450 - price: 451 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU627: - cost: 198 - init_stock: 4100 - price: 220 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU628: - cost: 111 - init_stock: 1300 - price: 124 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU629: - cost: 317 - init_stock: 4600 - price: 353 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU63: - cost: 61 - init_stock: 700 - price: 68 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU630: - cost: 109 - init_stock: 850 - price: 122 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU631: - cost: 266 - init_stock: 2450 - price: 296 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU632: - cost: 76 - init_stock: 4700 - price: 85 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU633: - cost: 165 - init_stock: 3350 - price: 184 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU634: - cost: 15 - init_stock: 3650 - price: 17 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU635: - cost: 306 - init_stock: 4650 - price: 341 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU636: - cost: 346 - init_stock: 1850 - price: 385 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU637: - cost: 74 - init_stock: 3050 - price: 83 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU638: - cost: 337 - init_stock: 400 - price: 375 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU639: - cost: 294 - init_stock: 2000 - price: 327 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU64: - cost: 32 - init_stock: 950 - price: 36 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU640: - cost: 247 - init_stock: 4550 - price: 275 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU641: - cost: 328 - init_stock: 4050 - price: 365 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU642: - cost: 372 - init_stock: 1250 - price: 414 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU643: - cost: 322 - init_stock: 2300 - price: 358 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU644: - cost: 41 - init_stock: 4100 - price: 46 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU645: - cost: 420 - init_stock: 4950 - price: 467 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU646: - cost: 170 - init_stock: 650 - price: 189 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU647: - cost: 272 - init_stock: 3850 - price: 303 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU648: - cost: 203 - init_stock: 2750 - price: 226 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU649: - cost: 178 - init_stock: 1250 - price: 198 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU65: - cost: 13 - init_stock: 4700 - price: 15 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU650: - cost: 315 - init_stock: 2800 - price: 351 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU651: - cost: 342 - init_stock: 4800 - price: 380 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU652: - cost: 110 - init_stock: 2000 - price: 123 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU653: - cost: 431 - init_stock: 4800 - price: 479 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU654: - cost: 59 - init_stock: 400 - price: 66 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU655: - cost: 299 - init_stock: 2100 - price: 333 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU656: - cost: 47 - init_stock: 3600 - price: 53 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU657: - cost: 65 - init_stock: 4050 - price: 73 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU658: - cost: 63 - init_stock: 1800 - price: 70 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU659: - cost: 18 - init_stock: 2800 - price: 21 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU66: - cost: 128 - init_stock: 4500 - price: 143 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU660: - cost: 438 - init_stock: 4150 - price: 487 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU661: - cost: 309 - init_stock: 3700 - price: 344 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU662: - cost: 334 - init_stock: 1900 - price: 372 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU663: - cost: 376 - init_stock: 2000 - price: 418 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU664: - cost: 315 - init_stock: 4200 - price: 351 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU665: - cost: 443 - init_stock: 2150 - price: 493 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU666: - cost: 306 - init_stock: 4400 - price: 341 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU667: - cost: 292 - init_stock: 650 - price: 325 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU668: - cost: 257 - init_stock: 3000 - price: 286 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU669: - cost: 66 - init_stock: 800 - price: 74 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU67: - cost: 222 - init_stock: 2650 - price: 247 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU670: - cost: 260 - init_stock: 4300 - price: 289 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU671: - cost: 240 - init_stock: 4150 - price: 267 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU672: - cost: 332 - init_stock: 650 - price: 369 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU673: - cost: 289 - init_stock: 2050 - price: 322 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU674: - cost: 200 - init_stock: 1100 - price: 223 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU675: - cost: 394 - init_stock: 1650 - price: 438 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU676: - cost: 219 - init_stock: 3050 - price: 244 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU677: - cost: 382 - init_stock: 2200 - price: 425 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU678: - cost: 151 - init_stock: 1800 - price: 168 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU679: - cost: 355 - init_stock: 2200 - price: 395 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU68: - cost: 152 - init_stock: 700 - price: 169 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU680: - cost: 234 - init_stock: 1500 - price: 260 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU681: - cost: 417 - init_stock: 4850 - price: 464 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU682: - cost: 333 - init_stock: 2650 - price: 370 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU683: - cost: 294 - init_stock: 3350 - price: 327 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU684: - cost: 319 - init_stock: 3950 - price: 355 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU685: - cost: 379 - init_stock: 2250 - price: 422 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU686: - cost: 56 - init_stock: 3150 - price: 63 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU687: - cost: 30 - init_stock: 3800 - price: 34 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU688: - cost: 435 - init_stock: 3850 - price: 484 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU689: - cost: 449 - init_stock: 750 - price: 499 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU69: - cost: 158 - init_stock: 3350 - price: 176 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU690: - cost: 393 - init_stock: 4150 - price: 437 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU691: - cost: 15 - init_stock: 3950 - price: 17 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU692: - cost: 202 - init_stock: 3250 - price: 225 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU693: - cost: 17 - init_stock: 3050 - price: 19 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU694: - cost: 187 - init_stock: 3750 - price: 208 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU695: - cost: 171 - init_stock: 350 - price: 190 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU696: - cost: 313 - init_stock: 2900 - price: 348 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU697: - cost: 409 - init_stock: 4000 - price: 455 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU698: - cost: 178 - init_stock: 3050 - price: 198 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU699: - cost: 27 - init_stock: 1000 - price: 31 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU7: - cost: 90 - init_stock: 4800 - price: 101 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU70: - cost: 167 - init_stock: 1750 - price: 186 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU700: - cost: 66 - init_stock: 450 - price: 74 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU701: - cost: 359 - init_stock: 3850 - price: 399 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU702: - cost: 73 - init_stock: 1700 - price: 82 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU703: - cost: 326 - init_stock: 1900 - price: 363 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU704: - cost: 345 - init_stock: 4350 - price: 384 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU705: - cost: 115 - init_stock: 3150 - price: 128 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU706: - cost: 163 - init_stock: 4550 - price: 182 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU707: - cost: 16 - init_stock: 3450 - price: 18 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU708: - cost: 160 - init_stock: 1400 - price: 178 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU709: - cost: 91 - init_stock: 2650 - price: 102 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU71: - cost: 283 - init_stock: 2250 - price: 315 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU710: - cost: 226 - init_stock: 950 - price: 252 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU711: - cost: 252 - init_stock: 3450 - price: 281 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU712: - cost: 208 - init_stock: 550 - price: 232 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU713: - cost: 256 - init_stock: 2150 - price: 285 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU714: - cost: 71 - init_stock: 2050 - price: 79 - product_unit_cost: 1 - production_rate: 2050 - service_level: 0.95 - type: production - vlt: 3 - SKU715: - cost: 210 - init_stock: 850 - price: 234 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU716: - cost: 63 - init_stock: 3750 - price: 70 - product_unit_cost: 1 - production_rate: 3750 - service_level: 0.95 - type: production - vlt: 3 - SKU717: - cost: 310 - init_stock: 400 - price: 345 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU718: - cost: 321 - init_stock: 2900 - price: 357 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU719: - cost: 306 - init_stock: 3250 - price: 340 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU72: - cost: 412 - init_stock: 4250 - price: 458 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU720: - cost: 292 - init_stock: 2100 - price: 325 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU721: - cost: 65 - init_stock: 550 - price: 73 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU722: - cost: 352 - init_stock: 4850 - price: 392 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU723: - cost: 286 - init_stock: 4450 - price: 318 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU724: - cost: 360 - init_stock: 1650 - price: 400 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU725: - cost: 157 - init_stock: 1850 - price: 175 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU726: - cost: 412 - init_stock: 4450 - price: 458 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU727: - cost: 376 - init_stock: 2850 - price: 418 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU728: - cost: 427 - init_stock: 1850 - price: 475 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU729: - cost: 291 - init_stock: 1800 - price: 324 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU73: - cost: 190 - init_stock: 2700 - price: 212 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU730: - cost: 14 - init_stock: 650 - price: 16 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU731: - cost: 79 - init_stock: 4100 - price: 88 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU732: - cost: 36 - init_stock: 3100 - price: 41 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU733: - cost: 283 - init_stock: 1300 - price: 315 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU734: - cost: 33 - init_stock: 2550 - price: 37 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU735: - cost: 239 - init_stock: 4950 - price: 266 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU736: - cost: 331 - init_stock: 1250 - price: 368 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU737: - cost: 427 - init_stock: 1550 - price: 475 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU738: - cost: 166 - init_stock: 2400 - price: 185 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU739: - cost: 427 - init_stock: 3700 - price: 475 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU74: - cost: 143 - init_stock: 2100 - price: 159 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU740: - cost: 351 - init_stock: 3200 - price: 390 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU741: - cost: 81 - init_stock: 2800 - price: 91 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU742: - cost: 169 - init_stock: 1100 - price: 188 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU743: - cost: 195 - init_stock: 2150 - price: 217 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU744: - cost: 341 - init_stock: 2900 - price: 379 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU745: - cost: 284 - init_stock: 4600 - price: 316 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU746: - cost: 393 - init_stock: 2000 - price: 437 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU747: - cost: 335 - init_stock: 3950 - price: 373 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU748: - cost: 247 - init_stock: 3300 - price: 275 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU749: - cost: 354 - init_stock: 2650 - price: 394 - product_unit_cost: 1 - production_rate: 2650 - service_level: 0.95 - type: production - vlt: 3 - SKU75: - cost: 117 - init_stock: 950 - price: 131 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU750: - cost: 230 - init_stock: 3100 - price: 256 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU751: - cost: 332 - init_stock: 3250 - price: 369 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU752: - cost: 233 - init_stock: 3900 - price: 259 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU753: - cost: 69 - init_stock: 3250 - price: 77 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU754: - cost: 348 - init_stock: 650 - price: 387 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU755: - cost: 318 - init_stock: 4000 - price: 354 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU756: - cost: 221 - init_stock: 4500 - price: 246 - product_unit_cost: 1 - production_rate: 4500 - service_level: 0.95 - type: production - vlt: 3 - SKU757: - cost: 36 - init_stock: 2850 - price: 40 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU758: - cost: 216 - init_stock: 2000 - price: 241 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU759: - cost: 391 - init_stock: 1900 - price: 435 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU76: - cost: 132 - init_stock: 3200 - price: 147 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU760: - cost: 158 - init_stock: 2550 - price: 176 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU761: - cost: 201 - init_stock: 2750 - price: 224 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU762: - cost: 237 - init_stock: 2550 - price: 264 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU763: - cost: 346 - init_stock: 3950 - price: 385 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU764: - cost: 314 - init_stock: 1700 - price: 349 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU765: - cost: 310 - init_stock: 650 - price: 345 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU766: - cost: 430 - init_stock: 1000 - price: 478 - product_unit_cost: 1 - production_rate: 1000 - service_level: 0.95 - type: production - vlt: 3 - SKU767: - cost: 85 - init_stock: 3800 - price: 95 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU768: - cost: 162 - init_stock: 4600 - price: 181 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU769: - cost: 21 - init_stock: 1450 - price: 24 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU77: - cost: 368 - init_stock: 3600 - price: 409 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU770: - cost: 135 - init_stock: 3100 - price: 150 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU771: - cost: 90 - init_stock: 350 - price: 101 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU772: - cost: 230 - init_stock: 300 - price: 256 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU773: - cost: 75 - init_stock: 4600 - price: 84 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU774: - cost: 402 - init_stock: 2950 - price: 447 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU775: - cost: 157 - init_stock: 4300 - price: 175 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU776: - cost: 92 - init_stock: 4250 - price: 103 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU777: - cost: 262 - init_stock: 2850 - price: 292 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU778: - cost: 182 - init_stock: 400 - price: 203 - product_unit_cost: 1 - production_rate: 400 - service_level: 0.95 - type: production - vlt: 3 - SKU779: - cost: 105 - init_stock: 2150 - price: 117 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU78: - cost: 210 - init_stock: 650 - price: 234 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU780: - cost: 62 - init_stock: 4100 - price: 69 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU781: - cost: 334 - init_stock: 1800 - price: 372 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU782: - cost: 24 - init_stock: 500 - price: 27 - product_unit_cost: 1 - production_rate: 500 - service_level: 0.95 - type: production - vlt: 3 - SKU783: - cost: 78 - init_stock: 4050 - price: 87 - product_unit_cost: 1 - production_rate: 4050 - service_level: 0.95 - type: production - vlt: 3 - SKU784: - cost: 278 - init_stock: 2600 - price: 309 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU785: - cost: 171 - init_stock: 3500 - price: 191 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU786: - cost: 81 - init_stock: 1100 - price: 91 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU787: - cost: 324 - init_stock: 3050 - price: 360 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU788: - cost: 315 - init_stock: 1400 - price: 351 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU789: - cost: 137 - init_stock: 2900 - price: 153 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU79: - cost: 220 - init_stock: 550 - price: 245 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU790: - cost: 375 - init_stock: 3300 - price: 417 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU791: - cost: 120 - init_stock: 1400 - price: 134 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU792: - cost: 281 - init_stock: 1050 - price: 313 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU793: - cost: 175 - init_stock: 3800 - price: 195 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU794: - cost: 292 - init_stock: 4000 - price: 325 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU795: - cost: 248 - init_stock: 2400 - price: 276 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU796: - cost: 402 - init_stock: 2450 - price: 447 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU797: - cost: 90 - init_stock: 1950 - price: 100 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU798: - cost: 383 - init_stock: 1500 - price: 426 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU799: - cost: 56 - init_stock: 350 - price: 63 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU8: - cost: 112 - init_stock: 3150 - price: 125 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU80: - cost: 146 - init_stock: 3500 - price: 163 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU800: - cost: 268 - init_stock: 4700 - price: 298 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU801: - cost: 350 - init_stock: 1800 - price: 389 - product_unit_cost: 1 - production_rate: 1800 - service_level: 0.95 - type: production - vlt: 3 - SKU802: - cost: 155 - init_stock: 3100 - price: 173 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU803: - cost: 244 - init_stock: 950 - price: 272 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU804: - cost: 351 - init_stock: 2350 - price: 390 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU805: - cost: 54 - init_stock: 1650 - price: 61 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU806: - cost: 142 - init_stock: 2600 - price: 158 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU807: - cost: 407 - init_stock: 1300 - price: 453 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU808: - cost: 224 - init_stock: 4550 - price: 249 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU809: - cost: 49 - init_stock: 3250 - price: 55 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU81: - cost: 163 - init_stock: 3300 - price: 182 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU810: - cost: 248 - init_stock: 300 - price: 276 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU811: - cost: 228 - init_stock: 1850 - price: 254 - product_unit_cost: 1 - production_rate: 1850 - service_level: 0.95 - type: production - vlt: 3 - SKU812: - cost: 226 - init_stock: 4000 - price: 252 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU813: - cost: 227 - init_stock: 350 - price: 253 - product_unit_cost: 1 - production_rate: 350 - service_level: 0.95 - type: production - vlt: 3 - SKU814: - cost: 436 - init_stock: 4850 - price: 485 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU815: - cost: 351 - init_stock: 1200 - price: 390 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU816: - cost: 136 - init_stock: 4550 - price: 152 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU817: - cost: 204 - init_stock: 250 - price: 227 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU818: - cost: 318 - init_stock: 2150 - price: 354 - product_unit_cost: 1 - production_rate: 2150 - service_level: 0.95 - type: production - vlt: 3 - SKU819: - cost: 271 - init_stock: 1350 - price: 302 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU82: - cost: 261 - init_stock: 1400 - price: 290 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU820: - cost: 237 - init_stock: 4100 - price: 264 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU821: - cost: 89 - init_stock: 3450 - price: 99 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU822: - cost: 122 - init_stock: 3500 - price: 136 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU823: - cost: 67 - init_stock: 1400 - price: 75 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU824: - cost: 153 - init_stock: 3500 - price: 170 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU825: - cost: 192 - init_stock: 750 - price: 214 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU826: - cost: 347 - init_stock: 2750 - price: 386 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU827: - cost: 133 - init_stock: 3200 - price: 148 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU828: - cost: 360 - init_stock: 3450 - price: 400 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU829: - cost: 54 - init_stock: 3700 - price: 61 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU83: - cost: 266 - init_stock: 850 - price: 296 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU830: - cost: 150 - init_stock: 1200 - price: 167 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU831: - cost: 235 - init_stock: 1450 - price: 262 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU832: - cost: 29 - init_stock: 4100 - price: 33 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU833: - cost: 360 - init_stock: 4900 - price: 400 - product_unit_cost: 1 - production_rate: 4900 - service_level: 0.95 - type: production - vlt: 3 - SKU834: - cost: 379 - init_stock: 250 - price: 422 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU835: - cost: 396 - init_stock: 2600 - price: 440 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU836: - cost: 290 - init_stock: 4800 - price: 323 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU837: - cost: 335 - init_stock: 1300 - price: 373 - product_unit_cost: 1 - production_rate: 1300 - service_level: 0.95 - type: production - vlt: 3 - SKU838: - cost: 410 - init_stock: 3850 - price: 456 - product_unit_cost: 1 - production_rate: 3850 - service_level: 0.95 - type: production - vlt: 3 - SKU839: - cost: 425 - init_stock: 3000 - price: 473 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU84: - cost: 286 - init_stock: 2100 - price: 318 - product_unit_cost: 1 - production_rate: 2100 - service_level: 0.95 - type: production - vlt: 3 - SKU840: - cost: 239 - init_stock: 2500 - price: 266 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU841: - cost: 256 - init_stock: 4250 - price: 285 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU842: - cost: 36 - init_stock: 4200 - price: 41 - product_unit_cost: 1 - production_rate: 4200 - service_level: 0.95 - type: production - vlt: 3 - SKU843: - cost: 324 - init_stock: 3600 - price: 360 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU844: - cost: 45 - init_stock: 3950 - price: 51 - product_unit_cost: 1 - production_rate: 3950 - service_level: 0.95 - type: production - vlt: 3 - SKU845: - cost: 259 - init_stock: 1100 - price: 288 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU846: - cost: 436 - init_stock: 1950 - price: 485 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU847: - cost: 349 - init_stock: 4550 - price: 388 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU848: - cost: 275 - init_stock: 2300 - price: 306 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU849: - cost: 288 - init_stock: 2000 - price: 320 - product_unit_cost: 1 - production_rate: 2000 - service_level: 0.95 - type: production - vlt: 3 - SKU85: - cost: 397 - init_stock: 5000 - price: 442 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU850: - cost: 164 - init_stock: 2850 - price: 183 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU851: - cost: 47 - init_stock: 3200 - price: 53 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU852: - cost: 275 - init_stock: 2700 - price: 306 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU853: - cost: 347 - init_stock: 2800 - price: 386 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU854: - cost: 190 - init_stock: 3250 - price: 212 - product_unit_cost: 1 - production_rate: 3250 - service_level: 0.95 - type: production - vlt: 3 - SKU855: - cost: 68 - init_stock: 3600 - price: 76 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU856: - cost: 342 - init_stock: 1600 - price: 380 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU857: - cost: 415 - init_stock: 5000 - price: 462 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU858: - cost: 72 - init_stock: 1200 - price: 80 - product_unit_cost: 1 - production_rate: 1200 - service_level: 0.95 - type: production - vlt: 3 - SKU859: - cost: 193 - init_stock: 1400 - price: 215 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU86: - cost: 347 - init_stock: 4250 - price: 386 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU860: - cost: 281 - init_stock: 750 - price: 313 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU861: - cost: 429 - init_stock: 650 - price: 477 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU862: - cost: 216 - init_stock: 800 - price: 240 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU863: - cost: 423 - init_stock: 4650 - price: 470 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU864: - cost: 182 - init_stock: 950 - price: 203 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU865: - cost: 129 - init_stock: 950 - price: 144 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU866: - cost: 154 - init_stock: 4400 - price: 172 - product_unit_cost: 1 - production_rate: 4400 - service_level: 0.95 - type: production - vlt: 3 - SKU867: - cost: 449 - init_stock: 5000 - price: 499 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU868: - cost: 36 - init_stock: 2500 - price: 41 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU869: - cost: 87 - init_stock: 4850 - price: 97 - product_unit_cost: 1 - production_rate: 4850 - service_level: 0.95 - type: production - vlt: 3 - SKU87: - cost: 331 - init_stock: 3450 - price: 368 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU870: - cost: 252 - init_stock: 3350 - price: 281 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU871: - cost: 90 - init_stock: 4950 - price: 101 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU872: - cost: 119 - init_stock: 1600 - price: 133 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU873: - cost: 380 - init_stock: 1550 - price: 423 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU874: - cost: 422 - init_stock: 3000 - price: 469 - product_unit_cost: 1 - production_rate: 3000 - service_level: 0.95 - type: production - vlt: 3 - SKU875: - cost: 45 - init_stock: 1150 - price: 51 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU876: - cost: 272 - init_stock: 3050 - price: 303 - product_unit_cost: 1 - production_rate: 3050 - service_level: 0.95 - type: production - vlt: 3 - SKU877: - cost: 36 - init_stock: 1750 - price: 40 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU878: - cost: 24 - init_stock: 3400 - price: 27 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU879: - cost: 135 - init_stock: 5000 - price: 150 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU88: - cost: 423 - init_stock: 600 - price: 471 - product_unit_cost: 1 - production_rate: 600 - service_level: 0.95 - type: production - vlt: 3 - SKU880: - cost: 389 - init_stock: 1550 - price: 433 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU881: - cost: 387 - init_stock: 3500 - price: 431 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU882: - cost: 174 - init_stock: 800 - price: 194 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU883: - cost: 255 - init_stock: 1900 - price: 284 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU884: - cost: 125 - init_stock: 1950 - price: 139 - product_unit_cost: 1 - production_rate: 1950 - service_level: 0.95 - type: production - vlt: 3 - SKU885: - cost: 44 - init_stock: 1350 - price: 49 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU886: - cost: 334 - init_stock: 3650 - price: 372 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU887: - cost: 239 - init_stock: 4350 - price: 266 - product_unit_cost: 1 - production_rate: 4350 - service_level: 0.95 - type: production - vlt: 3 - SKU888: - cost: 128 - init_stock: 450 - price: 143 - product_unit_cost: 1 - production_rate: 450 - service_level: 0.95 - type: production - vlt: 3 - SKU889: - cost: 90 - init_stock: 1050 - price: 101 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU89: - cost: 124 - init_stock: 4650 - price: 138 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU890: - cost: 144 - init_stock: 5000 - price: 161 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU891: - cost: 320 - init_stock: 1100 - price: 356 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU892: - cost: 281 - init_stock: 4600 - price: 313 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU893: - cost: 206 - init_stock: 4950 - price: 229 - product_unit_cost: 1 - production_rate: 4950 - service_level: 0.95 - type: production - vlt: 3 - SKU894: - cost: 116 - init_stock: 3700 - price: 129 - product_unit_cost: 1 - production_rate: 3700 - service_level: 0.95 - type: production - vlt: 3 - SKU895: - cost: 207 - init_stock: 650 - price: 230 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU896: - cost: 260 - init_stock: 4000 - price: 289 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU897: - cost: 353 - init_stock: 3600 - price: 393 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU898: - cost: 429 - init_stock: 550 - price: 477 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU899: - cost: 209 - init_stock: 2400 - price: 233 - product_unit_cost: 1 - production_rate: 2400 - service_level: 0.95 - type: production - vlt: 3 - SKU9: - cost: 149 - init_stock: 4800 - price: 166 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU90: - cost: 408 - init_stock: 2550 - price: 454 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU900: - cost: 142 - init_stock: 2750 - price: 158 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU901: - cost: 193 - init_stock: 1450 - price: 215 - product_unit_cost: 1 - production_rate: 1450 - service_level: 0.95 - type: production - vlt: 3 - SKU902: - cost: 112 - init_stock: 4000 - price: 125 - product_unit_cost: 1 - production_rate: 4000 - service_level: 0.95 - type: production - vlt: 3 - SKU903: - cost: 321 - init_stock: 1900 - price: 357 - product_unit_cost: 1 - production_rate: 1900 - service_level: 0.95 - type: production - vlt: 3 - SKU904: - cost: 446 - init_stock: 2550 - price: 496 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU905: - cost: 224 - init_stock: 1550 - price: 249 - product_unit_cost: 1 - production_rate: 1550 - service_level: 0.95 - type: production - vlt: 3 - SKU906: - cost: 149 - init_stock: 2600 - price: 166 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU907: - cost: 19 - init_stock: 4150 - price: 22 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU908: - cost: 367 - init_stock: 3900 - price: 408 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU909: - cost: 433 - init_stock: 4800 - price: 482 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU91: - cost: 272 - init_stock: 800 - price: 303 - product_unit_cost: 1 - production_rate: 800 - service_level: 0.95 - type: production - vlt: 3 - SKU910: - cost: 203 - init_stock: 1650 - price: 226 - product_unit_cost: 1 - production_rate: 1650 - service_level: 0.95 - type: production - vlt: 3 - SKU911: - cost: 414 - init_stock: 3500 - price: 461 - product_unit_cost: 1 - production_rate: 3500 - service_level: 0.95 - type: production - vlt: 3 - SKU912: - cost: 212 - init_stock: 1350 - price: 236 - product_unit_cost: 1 - production_rate: 1350 - service_level: 0.95 - type: production - vlt: 3 - SKU913: - cost: 289 - init_stock: 2300 - price: 322 - product_unit_cost: 1 - production_rate: 2300 - service_level: 0.95 - type: production - vlt: 3 - SKU914: - cost: 244 - init_stock: 3100 - price: 272 - product_unit_cost: 1 - production_rate: 3100 - service_level: 0.95 - type: production - vlt: 3 - SKU915: - cost: 303 - init_stock: 2800 - price: 337 - product_unit_cost: 1 - production_rate: 2800 - service_level: 0.95 - type: production - vlt: 3 - SKU916: - cost: 303 - init_stock: 4450 - price: 337 - product_unit_cost: 1 - production_rate: 4450 - service_level: 0.95 - type: production - vlt: 3 - SKU917: - cost: 232 - init_stock: 1500 - price: 258 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU918: - cost: 133 - init_stock: 2750 - price: 148 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU919: - cost: 353 - init_stock: 1250 - price: 393 - product_unit_cost: 1 - production_rate: 1250 - service_level: 0.95 - type: production - vlt: 3 - SKU92: - cost: 235 - init_stock: 3150 - price: 262 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU920: - cost: 321 - init_stock: 4300 - price: 357 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU921: - cost: 204 - init_stock: 3300 - price: 227 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU922: - cost: 100 - init_stock: 3350 - price: 112 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU923: - cost: 446 - init_stock: 2900 - price: 496 - product_unit_cost: 1 - production_rate: 2900 - service_level: 0.95 - type: production - vlt: 3 - SKU924: - cost: 284 - init_stock: 4250 - price: 316 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU925: - cost: 324 - init_stock: 750 - price: 360 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU926: - cost: 324 - init_stock: 3350 - price: 360 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU927: - cost: 234 - init_stock: 1050 - price: 260 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU928: - cost: 441 - init_stock: 4150 - price: 491 - product_unit_cost: 1 - production_rate: 4150 - service_level: 0.95 - type: production - vlt: 3 - SKU929: - cost: 323 - init_stock: 5000 - price: 359 - product_unit_cost: 1 - production_rate: 5000 - service_level: 0.95 - type: production - vlt: 3 - SKU93: - cost: 363 - init_stock: 3350 - price: 404 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU930: - cost: 178 - init_stock: 1400 - price: 198 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU931: - cost: 63 - init_stock: 700 - price: 71 - product_unit_cost: 1 - production_rate: 700 - service_level: 0.95 - type: production - vlt: 3 - SKU932: - cost: 146 - init_stock: 3300 - price: 163 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU933: - cost: 101 - init_stock: 3900 - price: 113 - product_unit_cost: 1 - production_rate: 3900 - service_level: 0.95 - type: production - vlt: 3 - SKU934: - cost: 197 - init_stock: 3350 - price: 219 - product_unit_cost: 1 - production_rate: 3350 - service_level: 0.95 - type: production - vlt: 3 - SKU935: - cost: 327 - init_stock: 4700 - price: 364 - product_unit_cost: 1 - production_rate: 4700 - service_level: 0.95 - type: production - vlt: 3 - SKU936: - cost: 21 - init_stock: 250 - price: 24 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU937: - cost: 121 - init_stock: 850 - price: 135 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU938: - cost: 388 - init_stock: 1050 - price: 432 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU939: - cost: 155 - init_stock: 2950 - price: 173 - product_unit_cost: 1 - production_rate: 2950 - service_level: 0.95 - type: production - vlt: 3 - SKU94: - cost: 165 - init_stock: 2350 - price: 184 - product_unit_cost: 1 - production_rate: 2350 - service_level: 0.95 - type: production - vlt: 3 - SKU940: - cost: 12 - init_stock: 4650 - price: 14 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU941: - cost: 72 - init_stock: 2850 - price: 80 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU942: - cost: 181 - init_stock: 650 - price: 202 - product_unit_cost: 1 - production_rate: 650 - service_level: 0.95 - type: production - vlt: 3 - SKU943: - cost: 124 - init_stock: 2450 - price: 138 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU944: - cost: 176 - init_stock: 2200 - price: 196 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU945: - cost: 126 - init_stock: 850 - price: 141 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU946: - cost: 292 - init_stock: 2450 - price: 325 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU947: - cost: 304 - init_stock: 4550 - price: 338 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU948: - cost: 382 - init_stock: 1400 - price: 425 - product_unit_cost: 1 - production_rate: 1400 - service_level: 0.95 - type: production - vlt: 3 - SKU949: - cost: 278 - init_stock: 250 - price: 309 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU95: - cost: 122 - init_stock: 1050 - price: 136 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU950: - cost: 91 - init_stock: 2700 - price: 102 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU951: - cost: 67 - init_stock: 900 - price: 75 - product_unit_cost: 1 - production_rate: 900 - service_level: 0.95 - type: production - vlt: 3 - SKU952: - cost: 140 - init_stock: 550 - price: 156 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU953: - cost: 124 - init_stock: 1750 - price: 138 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU954: - cost: 266 - init_stock: 4300 - price: 296 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU955: - cost: 49 - init_stock: 3150 - price: 55 - product_unit_cost: 1 - production_rate: 3150 - service_level: 0.95 - type: production - vlt: 3 - SKU956: - cost: 253 - init_stock: 4250 - price: 282 - product_unit_cost: 1 - production_rate: 4250 - service_level: 0.95 - type: production - vlt: 3 - SKU957: - cost: 274 - init_stock: 2550 - price: 305 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU958: - cost: 332 - init_stock: 3300 - price: 369 - product_unit_cost: 1 - production_rate: 3300 - service_level: 0.95 - type: production - vlt: 3 - SKU959: - cost: 72 - init_stock: 4100 - price: 81 - product_unit_cost: 1 - production_rate: 4100 - service_level: 0.95 - type: production - vlt: 3 - SKU96: - cost: 158 - init_stock: 2200 - price: 176 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU960: - cost: 132 - init_stock: 300 - price: 147 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU961: - cost: 237 - init_stock: 2200 - price: 264 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU962: - cost: 318 - init_stock: 2200 - price: 354 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU963: - cost: 314 - init_stock: 1050 - price: 349 - product_unit_cost: 1 - production_rate: 1050 - service_level: 0.95 - type: production - vlt: 3 - SKU964: - cost: 219 - init_stock: 3650 - price: 244 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU965: - cost: 111 - init_stock: 2550 - price: 124 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU966: - cost: 271 - init_stock: 2200 - price: 302 - product_unit_cost: 1 - production_rate: 2200 - service_level: 0.95 - type: production - vlt: 3 - SKU967: - cost: 60 - init_stock: 2250 - price: 67 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU968: - cost: 252 - init_stock: 2450 - price: 281 - product_unit_cost: 1 - production_rate: 2450 - service_level: 0.95 - type: production - vlt: 3 - SKU969: - cost: 224 - init_stock: 300 - price: 249 - product_unit_cost: 1 - production_rate: 300 - service_level: 0.95 - type: production - vlt: 3 - SKU97: - cost: 25 - init_stock: 1500 - price: 28 - product_unit_cost: 1 - production_rate: 1500 - service_level: 0.95 - type: production - vlt: 3 - SKU970: - cost: 219 - init_stock: 250 - price: 244 - product_unit_cost: 1 - production_rate: 250 - service_level: 0.95 - type: production - vlt: 3 - SKU971: - cost: 331 - init_stock: 3650 - price: 368 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU972: - cost: 188 - init_stock: 3450 - price: 209 - product_unit_cost: 1 - production_rate: 3450 - service_level: 0.95 - type: production - vlt: 3 - SKU973: - cost: 243 - init_stock: 550 - price: 271 - product_unit_cost: 1 - production_rate: 550 - service_level: 0.95 - type: production - vlt: 3 - SKU974: - cost: 153 - init_stock: 1600 - price: 170 - product_unit_cost: 1 - production_rate: 1600 - service_level: 0.95 - type: production - vlt: 3 - SKU975: - cost: 178 - init_stock: 1100 - price: 198 - product_unit_cost: 1 - production_rate: 1100 - service_level: 0.95 - type: production - vlt: 3 - SKU976: - cost: 160 - init_stock: 750 - price: 178 - product_unit_cost: 1 - production_rate: 750 - service_level: 0.95 - type: production - vlt: 3 - SKU977: - cost: 210 - init_stock: 2700 - price: 234 - product_unit_cost: 1 - production_rate: 2700 - service_level: 0.95 - type: production - vlt: 3 - SKU978: - cost: 423 - init_stock: 3200 - price: 470 - product_unit_cost: 1 - production_rate: 3200 - service_level: 0.95 - type: production - vlt: 3 - SKU979: - cost: 398 - init_stock: 4800 - price: 443 - product_unit_cost: 1 - production_rate: 4800 - service_level: 0.95 - type: production - vlt: 3 - SKU98: - cost: 398 - init_stock: 1750 - price: 443 - product_unit_cost: 1 - production_rate: 1750 - service_level: 0.95 - type: production - vlt: 3 - SKU980: - cost: 35 - init_stock: 3400 - price: 39 - product_unit_cost: 1 - production_rate: 3400 - service_level: 0.95 - type: production - vlt: 3 - SKU981: - cost: 433 - init_stock: 3600 - price: 482 - product_unit_cost: 1 - production_rate: 3600 - service_level: 0.95 - type: production - vlt: 3 - SKU982: - cost: 191 - init_stock: 2600 - price: 213 - product_unit_cost: 1 - production_rate: 2600 - service_level: 0.95 - type: production - vlt: 3 - SKU983: - cost: 404 - init_stock: 4600 - price: 449 - product_unit_cost: 1 - production_rate: 4600 - service_level: 0.95 - type: production - vlt: 3 - SKU984: - cost: 208 - init_stock: 4550 - price: 232 - product_unit_cost: 1 - production_rate: 4550 - service_level: 0.95 - type: production - vlt: 3 - SKU985: - cost: 261 - init_stock: 2550 - price: 290 - product_unit_cost: 1 - production_rate: 2550 - service_level: 0.95 - type: production - vlt: 3 - SKU986: - cost: 247 - init_stock: 850 - price: 275 - product_unit_cost: 1 - production_rate: 850 - service_level: 0.95 - type: production - vlt: 3 - SKU987: - cost: 390 - init_stock: 1700 - price: 434 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU988: - cost: 91 - init_stock: 1150 - price: 102 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - SKU989: - cost: 435 - init_stock: 4650 - price: 484 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU99: - cost: 324 - init_stock: 2250 - price: 361 - product_unit_cost: 1 - production_rate: 2250 - service_level: 0.95 - type: production - vlt: 3 - SKU990: - cost: 97 - init_stock: 2850 - price: 108 - product_unit_cost: 1 - production_rate: 2850 - service_level: 0.95 - type: production - vlt: 3 - SKU991: - cost: 368 - init_stock: 950 - price: 409 - product_unit_cost: 1 - production_rate: 950 - service_level: 0.95 - type: production - vlt: 3 - SKU992: - cost: 390 - init_stock: 4650 - price: 434 - product_unit_cost: 1 - production_rate: 4650 - service_level: 0.95 - type: production - vlt: 3 - SKU993: - cost: 130 - init_stock: 4300 - price: 145 - product_unit_cost: 1 - production_rate: 4300 - service_level: 0.95 - type: production - vlt: 3 - SKU994: - cost: 423 - init_stock: 2500 - price: 470 - product_unit_cost: 1 - production_rate: 2500 - service_level: 0.95 - type: production - vlt: 3 - SKU995: - cost: 216 - init_stock: 3800 - price: 241 - product_unit_cost: 1 - production_rate: 3800 - service_level: 0.95 - type: production - vlt: 3 - SKU996: - cost: 234 - init_stock: 3650 - price: 260 - product_unit_cost: 1 - production_rate: 3650 - service_level: 0.95 - type: production - vlt: 3 - SKU997: - cost: 360 - init_stock: 1700 - price: 400 - product_unit_cost: 1 - production_rate: 1700 - service_level: 0.95 - type: production - vlt: 3 - SKU998: - cost: 402 - init_stock: 2750 - price: 447 - product_unit_cost: 1 - production_rate: 2750 - service_level: 0.95 - type: production - vlt: 3 - SKU999: - cost: 71 - init_stock: 1150 - price: 79 - product_unit_cost: 1 - production_rate: 1150 - service_level: 0.95 - type: production - vlt: 3 - - children: - distribution: - children: - vehicles: - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - - *id001 - config: - unit_price: 1 - storage: - config: - capacity: 5324500 - unit_storage_cost: 1 - config: - delay_order_penalty: 1000 - order_cost: 500 - definition_ref: WarehouseFacility - name: WAREHOUSE0 - skus: - SKU0: - cost: 322 - init_stock: 1260 - price: 322 - service_level: 0.96 - vlt: 1 - SKU1: - cost: 284 - init_stock: 440 - price: 284 - service_level: 0.96 - vlt: 3 - SKU10: - cost: 68 - init_stock: 500 - price: 68 - service_level: 0.96 - vlt: 3 - SKU100: - cost: 16 - init_stock: 1300 - price: 16 - service_level: 0.96 - vlt: 3 - SKU101: - cost: 84 - init_stock: 1420 - price: 84 - service_level: 0.96 - vlt: 3 - SKU102: - cost: 328 - init_stock: 1860 - price: 328 - service_level: 0.96 - vlt: 2 - SKU103: - cost: 334 - init_stock: 1900 - price: 334 - service_level: 0.96 - vlt: 1 - SKU104: - cost: 49 - init_stock: 1300 - price: 49 - service_level: 0.96 - vlt: 3 - SKU105: - cost: 110 - init_stock: 1140 - price: 110 - service_level: 0.96 - vlt: 2 - SKU106: - cost: 251 - init_stock: 1460 - price: 251 - service_level: 0.96 - vlt: 3 - SKU107: - cost: 423 - init_stock: 1740 - price: 423 - service_level: 0.96 - vlt: 3 - SKU108: - cost: 458 - init_stock: 1840 - price: 458 - service_level: 0.96 - vlt: 3 - SKU109: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU11: - cost: 400 - init_stock: 1020 - price: 400 - service_level: 0.96 - vlt: 2 - SKU110: - cost: 66 - init_stock: 280 - price: 66 - service_level: 0.96 - vlt: 3 - SKU111: - cost: 260 - init_stock: 1220 - price: 260 - service_level: 0.96 - vlt: 1 - SKU112: - cost: 61 - init_stock: 1900 - price: 61 - service_level: 0.96 - vlt: 2 - SKU113: - cost: 348 - init_stock: 620 - price: 348 - service_level: 0.96 - vlt: 3 - SKU114: - cost: 389 - init_stock: 540 - price: 389 - service_level: 0.96 - vlt: 1 - SKU115: - cost: 286 - init_stock: 1720 - price: 286 - service_level: 0.96 - vlt: 2 - SKU116: - cost: 496 - init_stock: 1440 - price: 496 - service_level: 0.96 - vlt: 3 - SKU117: - cost: 320 - init_stock: 1840 - price: 320 - service_level: 0.96 - vlt: 3 - SKU118: - cost: 183 - init_stock: 660 - price: 183 - service_level: 0.96 - vlt: 1 - SKU119: - cost: 209 - init_stock: 640 - price: 209 - service_level: 0.96 - vlt: 3 - SKU12: - cost: 112 - init_stock: 1680 - price: 112 - service_level: 0.96 - vlt: 1 - SKU120: - cost: 121 - init_stock: 1960 - price: 121 - service_level: 0.96 - vlt: 1 - SKU121: - cost: 40 - init_stock: 1700 - price: 40 - service_level: 0.96 - vlt: 1 - SKU122: - cost: 437 - init_stock: 140 - price: 437 - service_level: 0.96 - vlt: 3 - SKU123: - cost: 233 - init_stock: 380 - price: 233 - service_level: 0.96 - vlt: 3 - SKU124: - cost: 182 - init_stock: 720 - price: 182 - service_level: 0.96 - vlt: 2 - SKU125: - cost: 16 - init_stock: 1840 - price: 16 - service_level: 0.96 - vlt: 2 - SKU126: - cost: 36 - init_stock: 780 - price: 36 - service_level: 0.96 - vlt: 3 - SKU127: - cost: 217 - init_stock: 620 - price: 217 - service_level: 0.96 - vlt: 2 - SKU128: - cost: 165 - init_stock: 380 - price: 165 - service_level: 0.96 - vlt: 1 - SKU129: - cost: 143 - init_stock: 1000 - price: 143 - service_level: 0.96 - vlt: 2 - SKU13: - cost: 317 - init_stock: 1140 - price: 317 - service_level: 0.96 - vlt: 3 - SKU130: - cost: 348 - init_stock: 1960 - price: 348 - service_level: 0.96 - vlt: 3 - SKU131: - cost: 64 - init_stock: 900 - price: 64 - service_level: 0.96 - vlt: 1 - SKU132: - cost: 427 - init_stock: 420 - price: 427 - service_level: 0.96 - vlt: 2 - SKU133: - cost: 224 - init_stock: 580 - price: 224 - service_level: 0.96 - vlt: 2 - SKU134: - cost: 336 - init_stock: 1540 - price: 336 - service_level: 0.96 - vlt: 3 - SKU135: - cost: 153 - init_stock: 2000 - price: 153 - service_level: 0.96 - vlt: 1 - SKU136: - cost: 199 - init_stock: 1420 - price: 199 - service_level: 0.96 - vlt: 3 - SKU137: - cost: 93 - init_stock: 1480 - price: 93 - service_level: 0.96 - vlt: 2 - SKU138: - cost: 228 - init_stock: 720 - price: 228 - service_level: 0.96 - vlt: 1 - SKU139: - cost: 207 - init_stock: 480 - price: 207 - service_level: 0.96 - vlt: 1 - SKU14: - cost: 268 - init_stock: 1240 - price: 268 - service_level: 0.96 - vlt: 1 - SKU140: - cost: 261 - init_stock: 680 - price: 261 - service_level: 0.96 - vlt: 3 - SKU141: - cost: 190 - init_stock: 820 - price: 190 - service_level: 0.96 - vlt: 1 - SKU142: - cost: 320 - init_stock: 760 - price: 320 - service_level: 0.96 - vlt: 3 - SKU143: - cost: 318 - init_stock: 520 - price: 318 - service_level: 0.96 - vlt: 3 - SKU144: - cost: 400 - init_stock: 240 - price: 400 - service_level: 0.96 - vlt: 1 - SKU145: - cost: 399 - init_stock: 1640 - price: 399 - service_level: 0.96 - vlt: 1 - SKU146: - cost: 177 - init_stock: 960 - price: 177 - service_level: 0.96 - vlt: 3 - SKU147: - cost: 472 - init_stock: 1120 - price: 472 - service_level: 0.96 - vlt: 3 - SKU148: - cost: 313 - init_stock: 1540 - price: 313 - service_level: 0.96 - vlt: 3 - SKU149: - cost: 357 - init_stock: 1540 - price: 357 - service_level: 0.96 - vlt: 3 - SKU15: - cost: 52 - init_stock: 100 - price: 52 - service_level: 0.96 - vlt: 2 - SKU150: - cost: 106 - init_stock: 1400 - price: 106 - service_level: 0.96 - vlt: 2 - SKU151: - cost: 223 - init_stock: 1460 - price: 223 - service_level: 0.96 - vlt: 1 - SKU152: - cost: 10 - init_stock: 1020 - price: 10 - service_level: 0.96 - vlt: 3 - SKU153: - cost: 441 - init_stock: 1240 - price: 441 - service_level: 0.96 - vlt: 1 - SKU154: - cost: 77 - init_stock: 1700 - price: 77 - service_level: 0.96 - vlt: 3 - SKU155: - cost: 422 - init_stock: 1060 - price: 422 - service_level: 0.96 - vlt: 1 - SKU156: - cost: 10 - init_stock: 240 - price: 10 - service_level: 0.96 - vlt: 1 - SKU157: - cost: 410 - init_stock: 1500 - price: 410 - service_level: 0.96 - vlt: 2 - SKU158: - cost: 145 - init_stock: 1620 - price: 145 - service_level: 0.96 - vlt: 3 - SKU159: - cost: 193 - init_stock: 500 - price: 193 - service_level: 0.96 - vlt: 2 - SKU16: - cost: 175 - init_stock: 1160 - price: 175 - service_level: 0.96 - vlt: 3 - SKU160: - cost: 459 - init_stock: 1700 - price: 459 - service_level: 0.96 - vlt: 1 - SKU161: - cost: 239 - init_stock: 920 - price: 239 - service_level: 0.96 - vlt: 2 - SKU162: - cost: 158 - init_stock: 100 - price: 158 - service_level: 0.96 - vlt: 2 - SKU163: - cost: 486 - init_stock: 780 - price: 486 - service_level: 0.96 - vlt: 2 - SKU164: - cost: 496 - init_stock: 2000 - price: 496 - service_level: 0.96 - vlt: 1 - SKU165: - cost: 274 - init_stock: 660 - price: 274 - service_level: 0.96 - vlt: 3 - SKU166: - cost: 79 - init_stock: 1780 - price: 79 - service_level: 0.96 - vlt: 1 - SKU167: - cost: 443 - init_stock: 260 - price: 443 - service_level: 0.96 - vlt: 1 - SKU168: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU169: - cost: 369 - init_stock: 1960 - price: 369 - service_level: 0.96 - vlt: 3 - SKU17: - cost: 346 - init_stock: 180 - price: 346 - service_level: 0.96 - vlt: 1 - SKU170: - cost: 68 - init_stock: 1100 - price: 68 - service_level: 0.96 - vlt: 2 - SKU171: - cost: 398 - init_stock: 1520 - price: 398 - service_level: 0.96 - vlt: 1 - SKU172: - cost: 200 - init_stock: 1420 - price: 200 - service_level: 0.96 - vlt: 2 - SKU173: - cost: 175 - init_stock: 1920 - price: 175 - service_level: 0.96 - vlt: 3 - SKU174: - cost: 291 - init_stock: 1520 - price: 291 - service_level: 0.96 - vlt: 2 - SKU175: - cost: 84 - init_stock: 1500 - price: 84 - service_level: 0.96 - vlt: 2 - SKU176: - cost: 407 - init_stock: 1320 - price: 407 - service_level: 0.96 - vlt: 1 - SKU177: - cost: 257 - init_stock: 620 - price: 257 - service_level: 0.96 - vlt: 1 - SKU178: - cost: 423 - init_stock: 100 - price: 423 - service_level: 0.96 - vlt: 2 - SKU179: - cost: 497 - init_stock: 1660 - price: 497 - service_level: 0.96 - vlt: 1 - SKU18: - cost: 258 - init_stock: 1620 - price: 258 - service_level: 0.96 - vlt: 1 - SKU180: - cost: 217 - init_stock: 1100 - price: 217 - service_level: 0.96 - vlt: 2 - SKU181: - cost: 143 - init_stock: 1200 - price: 143 - service_level: 0.96 - vlt: 1 - SKU182: - cost: 437 - init_stock: 1980 - price: 437 - service_level: 0.96 - vlt: 3 - SKU183: - cost: 145 - init_stock: 160 - price: 145 - service_level: 0.96 - vlt: 3 - SKU184: - cost: 73 - init_stock: 480 - price: 73 - service_level: 0.96 - vlt: 2 - SKU185: - cost: 10 - init_stock: 1800 - price: 10 - service_level: 0.96 - vlt: 2 - SKU186: - cost: 359 - init_stock: 440 - price: 359 - service_level: 0.96 - vlt: 1 - SKU187: - cost: 177 - init_stock: 600 - price: 177 - service_level: 0.96 - vlt: 3 - SKU188: - cost: 391 - init_stock: 1740 - price: 391 - service_level: 0.96 - vlt: 3 - SKU189: - cost: 358 - init_stock: 700 - price: 358 - service_level: 0.96 - vlt: 2 - SKU19: - cost: 477 - init_stock: 1820 - price: 477 - service_level: 0.96 - vlt: 3 - SKU190: - cost: 113 - init_stock: 340 - price: 113 - service_level: 0.96 - vlt: 3 - SKU191: - cost: 473 - init_stock: 1080 - price: 473 - service_level: 0.96 - vlt: 2 - SKU192: - cost: 415 - init_stock: 1220 - price: 415 - service_level: 0.96 - vlt: 2 - SKU193: - cost: 207 - init_stock: 600 - price: 207 - service_level: 0.96 - vlt: 2 - SKU194: - cost: 432 - init_stock: 100 - price: 432 - service_level: 0.96 - vlt: 2 - SKU195: - cost: 218 - init_stock: 620 - price: 218 - service_level: 0.96 - vlt: 2 - SKU196: - cost: 49 - init_stock: 1360 - price: 49 - service_level: 0.96 - vlt: 3 - SKU197: - cost: 303 - init_stock: 1140 - price: 303 - service_level: 0.96 - vlt: 1 - SKU198: - cost: 169 - init_stock: 1080 - price: 169 - service_level: 0.96 - vlt: 2 - SKU199: - cost: 449 - init_stock: 460 - price: 449 - service_level: 0.96 - vlt: 1 - SKU2: - cost: 331 - init_stock: 1400 - price: 331 - service_level: 0.96 - vlt: 3 - SKU20: - cost: 335 - init_stock: 500 - price: 335 - service_level: 0.96 - vlt: 3 - SKU200: - cost: 65 - init_stock: 500 - price: 65 - service_level: 0.96 - vlt: 1 - SKU201: - cost: 104 - init_stock: 1180 - price: 104 - service_level: 0.96 - vlt: 1 - SKU202: - cost: 142 - init_stock: 1460 - price: 142 - service_level: 0.96 - vlt: 1 - SKU203: - cost: 440 - init_stock: 1640 - price: 440 - service_level: 0.96 - vlt: 2 - SKU204: - cost: 489 - init_stock: 940 - price: 489 - service_level: 0.96 - vlt: 2 - SKU205: - cost: 130 - init_stock: 2000 - price: 130 - service_level: 0.96 - vlt: 3 - SKU206: - cost: 335 - init_stock: 220 - price: 335 - service_level: 0.96 - vlt: 2 - SKU207: - cost: 140 - init_stock: 1600 - price: 140 - service_level: 0.96 - vlt: 1 - SKU208: - cost: 491 - init_stock: 1540 - price: 491 - service_level: 0.96 - vlt: 1 - SKU209: - cost: 179 - init_stock: 400 - price: 179 - service_level: 0.96 - vlt: 3 - SKU21: - cost: 123 - init_stock: 2000 - price: 123 - service_level: 0.96 - vlt: 2 - SKU210: - cost: 404 - init_stock: 1380 - price: 404 - service_level: 0.96 - vlt: 3 - SKU211: - cost: 174 - init_stock: 1820 - price: 174 - service_level: 0.96 - vlt: 2 - SKU212: - cost: 405 - init_stock: 1580 - price: 405 - service_level: 0.96 - vlt: 3 - SKU213: - cost: 121 - init_stock: 1280 - price: 121 - service_level: 0.96 - vlt: 2 - SKU214: - cost: 101 - init_stock: 200 - price: 101 - service_level: 0.96 - vlt: 2 - SKU215: - cost: 419 - init_stock: 940 - price: 419 - service_level: 0.96 - vlt: 1 - SKU216: - cost: 330 - init_stock: 460 - price: 330 - service_level: 0.96 - vlt: 1 - SKU217: - cost: 284 - init_stock: 1300 - price: 284 - service_level: 0.96 - vlt: 2 - SKU218: - cost: 205 - init_stock: 1180 - price: 205 - service_level: 0.96 - vlt: 1 - SKU219: - cost: 92 - init_stock: 920 - price: 92 - service_level: 0.96 - vlt: 3 - SKU22: - cost: 493 - init_stock: 1320 - price: 493 - service_level: 0.96 - vlt: 1 - SKU220: - cost: 387 - init_stock: 1740 - price: 387 - service_level: 0.96 - vlt: 2 - SKU221: - cost: 39 - init_stock: 1560 - price: 39 - service_level: 0.96 - vlt: 2 - SKU222: - cost: 115 - init_stock: 720 - price: 115 - service_level: 0.96 - vlt: 2 - SKU223: - cost: 196 - init_stock: 240 - price: 196 - service_level: 0.96 - vlt: 2 - SKU224: - cost: 387 - init_stock: 100 - price: 387 - service_level: 0.96 - vlt: 2 - SKU225: - cost: 164 - init_stock: 580 - price: 164 - service_level: 0.96 - vlt: 1 - SKU226: - cost: 206 - init_stock: 260 - price: 206 - service_level: 0.96 - vlt: 2 - SKU227: - cost: 479 - init_stock: 480 - price: 479 - service_level: 0.96 - vlt: 3 - SKU228: - cost: 14 - init_stock: 1800 - price: 14 - service_level: 0.96 - vlt: 1 - SKU229: - cost: 472 - init_stock: 880 - price: 472 - service_level: 0.96 - vlt: 1 - SKU23: - cost: 387 - init_stock: 840 - price: 387 - service_level: 0.96 - vlt: 1 - SKU230: - cost: 241 - init_stock: 460 - price: 241 - service_level: 0.96 - vlt: 3 - SKU231: - cost: 48 - init_stock: 1620 - price: 48 - service_level: 0.96 - vlt: 3 - SKU232: - cost: 224 - init_stock: 1920 - price: 224 - service_level: 0.96 - vlt: 1 - SKU233: - cost: 360 - init_stock: 1500 - price: 360 - service_level: 0.96 - vlt: 2 - SKU234: - cost: 287 - init_stock: 100 - price: 287 - service_level: 0.96 - vlt: 1 - SKU235: - cost: 24 - init_stock: 1140 - price: 24 - service_level: 0.96 - vlt: 3 - SKU236: - cost: 155 - init_stock: 1100 - price: 155 - service_level: 0.96 - vlt: 2 - SKU237: - cost: 433 - init_stock: 900 - price: 433 - service_level: 0.96 - vlt: 3 - SKU238: - cost: 64 - init_stock: 1320 - price: 64 - service_level: 0.96 - vlt: 3 - SKU239: - cost: 103 - init_stock: 1960 - price: 103 - service_level: 0.96 - vlt: 1 - SKU24: - cost: 97 - init_stock: 940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU240: - cost: 373 - init_stock: 940 - price: 373 - service_level: 0.96 - vlt: 2 - SKU241: - cost: 439 - init_stock: 1420 - price: 439 - service_level: 0.96 - vlt: 2 - SKU242: - cost: 17 - init_stock: 880 - price: 17 - service_level: 0.96 - vlt: 1 - SKU243: - cost: 352 - init_stock: 280 - price: 352 - service_level: 0.96 - vlt: 1 - SKU244: - cost: 174 - init_stock: 1640 - price: 174 - service_level: 0.96 - vlt: 2 - SKU245: - cost: 404 - init_stock: 1320 - price: 404 - service_level: 0.96 - vlt: 2 - SKU246: - cost: 300 - init_stock: 600 - price: 300 - service_level: 0.96 - vlt: 2 - SKU247: - cost: 386 - init_stock: 700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU248: - cost: 355 - init_stock: 580 - price: 355 - service_level: 0.96 - vlt: 3 - SKU249: - cost: 356 - init_stock: 500 - price: 356 - service_level: 0.96 - vlt: 1 - SKU25: - cost: 161 - init_stock: 100 - price: 161 - service_level: 0.96 - vlt: 2 - SKU250: - cost: 127 - init_stock: 1080 - price: 127 - service_level: 0.96 - vlt: 3 - SKU251: - cost: 344 - init_stock: 1220 - price: 344 - service_level: 0.96 - vlt: 1 - SKU252: - cost: 181 - init_stock: 1660 - price: 181 - service_level: 0.96 - vlt: 1 - SKU253: - cost: 448 - init_stock: 320 - price: 448 - service_level: 0.96 - vlt: 1 - SKU254: - cost: 484 - init_stock: 920 - price: 484 - service_level: 0.96 - vlt: 3 - SKU255: - cost: 290 - init_stock: 1340 - price: 290 - service_level: 0.96 - vlt: 2 - SKU256: - cost: 91 - init_stock: 1440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU257: - cost: 348 - init_stock: 1140 - price: 348 - service_level: 0.96 - vlt: 1 - SKU258: - cost: 489 - init_stock: 860 - price: 489 - service_level: 0.96 - vlt: 1 - SKU259: - cost: 333 - init_stock: 1380 - price: 333 - service_level: 0.96 - vlt: 1 - SKU26: - cost: 229 - init_stock: 1260 - price: 229 - service_level: 0.96 - vlt: 1 - SKU260: - cost: 487 - init_stock: 1040 - price: 487 - service_level: 0.96 - vlt: 3 - SKU261: - cost: 368 - init_stock: 440 - price: 368 - service_level: 0.96 - vlt: 1 - SKU262: - cost: 332 - init_stock: 1560 - price: 332 - service_level: 0.96 - vlt: 3 - SKU263: - cost: 189 - init_stock: 1480 - price: 189 - service_level: 0.96 - vlt: 3 - SKU264: - cost: 361 - init_stock: 1580 - price: 361 - service_level: 0.96 - vlt: 1 - SKU265: - cost: 286 - init_stock: 1180 - price: 286 - service_level: 0.96 - vlt: 3 - SKU266: - cost: 128 - init_stock: 940 - price: 128 - service_level: 0.96 - vlt: 2 - SKU267: - cost: 77 - init_stock: 1600 - price: 77 - service_level: 0.96 - vlt: 1 - SKU268: - cost: 221 - init_stock: 1780 - price: 221 - service_level: 0.96 - vlt: 2 - SKU269: - cost: 126 - init_stock: 880 - price: 126 - service_level: 0.96 - vlt: 2 - SKU27: - cost: 370 - init_stock: 1120 - price: 370 - service_level: 0.96 - vlt: 3 - SKU270: - cost: 182 - init_stock: 1480 - price: 182 - service_level: 0.96 - vlt: 3 - SKU271: - cost: 230 - init_stock: 360 - price: 230 - service_level: 0.96 - vlt: 1 - SKU272: - cost: 366 - init_stock: 340 - price: 366 - service_level: 0.96 - vlt: 2 - SKU273: - cost: 421 - init_stock: 360 - price: 421 - service_level: 0.96 - vlt: 2 - SKU274: - cost: 29 - init_stock: 1540 - price: 29 - service_level: 0.96 - vlt: 2 - SKU275: - cost: 50 - init_stock: 960 - price: 50 - service_level: 0.96 - vlt: 1 - SKU276: - cost: 163 - init_stock: 1080 - price: 163 - service_level: 0.96 - vlt: 3 - SKU277: - cost: 449 - init_stock: 820 - price: 449 - service_level: 0.96 - vlt: 1 - SKU278: - cost: 105 - init_stock: 240 - price: 105 - service_level: 0.96 - vlt: 3 - SKU279: - cost: 51 - init_stock: 780 - price: 51 - service_level: 0.96 - vlt: 2 - SKU28: - cost: 208 - init_stock: 940 - price: 208 - service_level: 0.96 - vlt: 2 - SKU280: - cost: 295 - init_stock: 660 - price: 295 - service_level: 0.96 - vlt: 2 - SKU281: - cost: 395 - init_stock: 1500 - price: 395 - service_level: 0.96 - vlt: 1 - SKU282: - cost: 63 - init_stock: 920 - price: 63 - service_level: 0.96 - vlt: 2 - SKU283: - cost: 392 - init_stock: 1840 - price: 392 - service_level: 0.96 - vlt: 3 - SKU284: - cost: 344 - init_stock: 1340 - price: 344 - service_level: 0.96 - vlt: 3 - SKU285: - cost: 133 - init_stock: 1820 - price: 133 - service_level: 0.96 - vlt: 2 - SKU286: - cost: 337 - init_stock: 780 - price: 337 - service_level: 0.96 - vlt: 1 - SKU287: - cost: 375 - init_stock: 1120 - price: 375 - service_level: 0.96 - vlt: 3 - SKU288: - cost: 181 - init_stock: 760 - price: 181 - service_level: 0.96 - vlt: 1 - SKU289: - cost: 67 - init_stock: 620 - price: 67 - service_level: 0.96 - vlt: 1 - SKU29: - cost: 245 - init_stock: 1160 - price: 245 - service_level: 0.96 - vlt: 1 - SKU290: - cost: 438 - init_stock: 1340 - price: 438 - service_level: 0.96 - vlt: 1 - SKU291: - cost: 94 - init_stock: 1220 - price: 94 - service_level: 0.96 - vlt: 1 - SKU292: - cost: 186 - init_stock: 100 - price: 186 - service_level: 0.96 - vlt: 1 - SKU293: - cost: 162 - init_stock: 1100 - price: 162 - service_level: 0.96 - vlt: 2 - SKU294: - cost: 409 - init_stock: 180 - price: 409 - service_level: 0.96 - vlt: 2 - SKU295: - cost: 435 - init_stock: 1860 - price: 435 - service_level: 0.96 - vlt: 3 - SKU296: - cost: 370 - init_stock: 1840 - price: 370 - service_level: 0.96 - vlt: 3 - SKU297: - cost: 298 - init_stock: 760 - price: 298 - service_level: 0.96 - vlt: 1 - SKU298: - cost: 286 - init_stock: 700 - price: 286 - service_level: 0.96 - vlt: 2 - SKU299: - cost: 367 - init_stock: 1020 - price: 367 - service_level: 0.96 - vlt: 3 - SKU3: - cost: 405 - init_stock: 240 - price: 405 - service_level: 0.96 - vlt: 3 - SKU30: - cost: 359 - init_stock: 1380 - price: 359 - service_level: 0.96 - vlt: 2 - SKU300: - cost: 376 - init_stock: 1160 - price: 376 - service_level: 0.96 - vlt: 1 - SKU301: - cost: 433 - init_stock: 1660 - price: 433 - service_level: 0.96 - vlt: 2 - SKU302: - cost: 184 - init_stock: 220 - price: 184 - service_level: 0.96 - vlt: 2 - SKU303: - cost: 246 - init_stock: 1880 - price: 246 - service_level: 0.96 - vlt: 1 - SKU304: - cost: 419 - init_stock: 460 - price: 419 - service_level: 0.96 - vlt: 3 - SKU305: - cost: 495 - init_stock: 2000 - price: 495 - service_level: 0.96 - vlt: 1 - SKU306: - cost: 479 - init_stock: 840 - price: 479 - service_level: 0.96 - vlt: 2 - SKU307: - cost: 210 - init_stock: 1560 - price: 210 - service_level: 0.96 - vlt: 1 - SKU308: - cost: 208 - init_stock: 100 - price: 208 - service_level: 0.96 - vlt: 2 - SKU309: - cost: 101 - init_stock: 1840 - price: 101 - service_level: 0.96 - vlt: 2 - SKU31: - cost: 69 - init_stock: 1120 - price: 69 - service_level: 0.96 - vlt: 3 - SKU310: - cost: 234 - init_stock: 880 - price: 234 - service_level: 0.96 - vlt: 3 - SKU311: - cost: 306 - init_stock: 1600 - price: 306 - service_level: 0.96 - vlt: 3 - SKU312: - cost: 291 - init_stock: 500 - price: 291 - service_level: 0.96 - vlt: 1 - SKU313: - cost: 324 - init_stock: 760 - price: 324 - service_level: 0.96 - vlt: 1 - SKU314: - cost: 404 - init_stock: 580 - price: 404 - service_level: 0.96 - vlt: 1 - SKU315: - cost: 471 - init_stock: 1680 - price: 471 - service_level: 0.96 - vlt: 2 - SKU316: - cost: 202 - init_stock: 360 - price: 202 - service_level: 0.96 - vlt: 1 - SKU317: - cost: 216 - init_stock: 480 - price: 216 - service_level: 0.96 - vlt: 2 - SKU318: - cost: 278 - init_stock: 1700 - price: 278 - service_level: 0.96 - vlt: 1 - SKU319: - cost: 257 - init_stock: 1060 - price: 257 - service_level: 0.96 - vlt: 3 - SKU32: - cost: 336 - init_stock: 440 - price: 336 - service_level: 0.96 - vlt: 1 - SKU320: - cost: 196 - init_stock: 780 - price: 196 - service_level: 0.96 - vlt: 3 - SKU321: - cost: 67 - init_stock: 320 - price: 67 - service_level: 0.96 - vlt: 3 - SKU322: - cost: 240 - init_stock: 2000 - price: 240 - service_level: 0.96 - vlt: 1 - SKU323: - cost: 66 - init_stock: 780 - price: 66 - service_level: 0.96 - vlt: 2 - SKU324: - cost: 442 - init_stock: 1860 - price: 442 - service_level: 0.96 - vlt: 3 - SKU325: - cost: 453 - init_stock: 1380 - price: 453 - service_level: 0.96 - vlt: 2 - SKU326: - cost: 345 - init_stock: 480 - price: 345 - service_level: 0.96 - vlt: 2 - SKU327: - cost: 457 - init_stock: 280 - price: 457 - service_level: 0.96 - vlt: 2 - SKU328: - cost: 390 - init_stock: 900 - price: 390 - service_level: 0.96 - vlt: 1 - SKU329: - cost: 67 - init_stock: 840 - price: 67 - service_level: 0.96 - vlt: 1 - SKU33: - cost: 416 - init_stock: 1840 - price: 416 - service_level: 0.96 - vlt: 2 - SKU330: - cost: 306 - init_stock: 780 - price: 306 - service_level: 0.96 - vlt: 2 - SKU331: - cost: 138 - init_stock: 820 - price: 138 - service_level: 0.96 - vlt: 3 - SKU332: - cost: 390 - init_stock: 1920 - price: 390 - service_level: 0.96 - vlt: 2 - SKU333: - cost: 485 - init_stock: 1060 - price: 485 - service_level: 0.96 - vlt: 2 - SKU334: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 1 - SKU335: - cost: 80 - init_stock: 1620 - price: 80 - service_level: 0.96 - vlt: 1 - SKU336: - cost: 296 - init_stock: 560 - price: 296 - service_level: 0.96 - vlt: 1 - SKU337: - cost: 245 - init_stock: 580 - price: 245 - service_level: 0.96 - vlt: 2 - SKU338: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 3 - SKU339: - cost: 265 - init_stock: 1260 - price: 265 - service_level: 0.96 - vlt: 2 - SKU34: - cost: 95 - init_stock: 260 - price: 95 - service_level: 0.96 - vlt: 3 - SKU340: - cost: 480 - init_stock: 1740 - price: 480 - service_level: 0.96 - vlt: 2 - SKU341: - cost: 462 - init_stock: 1400 - price: 462 - service_level: 0.96 - vlt: 1 - SKU342: - cost: 144 - init_stock: 180 - price: 144 - service_level: 0.96 - vlt: 3 - SKU343: - cost: 310 - init_stock: 300 - price: 310 - service_level: 0.96 - vlt: 2 - SKU344: - cost: 357 - init_stock: 1740 - price: 357 - service_level: 0.96 - vlt: 2 - SKU345: - cost: 442 - init_stock: 1780 - price: 442 - service_level: 0.96 - vlt: 2 - SKU346: - cost: 350 - init_stock: 840 - price: 350 - service_level: 0.96 - vlt: 3 - SKU347: - cost: 497 - init_stock: 1640 - price: 497 - service_level: 0.96 - vlt: 1 - SKU348: - cost: 147 - init_stock: 400 - price: 147 - service_level: 0.96 - vlt: 1 - SKU349: - cost: 67 - init_stock: 1340 - price: 67 - service_level: 0.96 - vlt: 3 - SKU35: - cost: 267 - init_stock: 1720 - price: 267 - service_level: 0.96 - vlt: 3 - SKU350: - cost: 279 - init_stock: 1840 - price: 279 - service_level: 0.96 - vlt: 3 - SKU351: - cost: 155 - init_stock: 1340 - price: 155 - service_level: 0.96 - vlt: 1 - SKU352: - cost: 71 - init_stock: 360 - price: 71 - service_level: 0.96 - vlt: 2 - SKU353: - cost: 253 - init_stock: 1860 - price: 253 - service_level: 0.96 - vlt: 2 - SKU354: - cost: 396 - init_stock: 1580 - price: 396 - service_level: 0.96 - vlt: 3 - SKU355: - cost: 63 - init_stock: 200 - price: 63 - service_level: 0.96 - vlt: 1 - SKU356: - cost: 348 - init_stock: 580 - price: 348 - service_level: 0.96 - vlt: 1 - SKU357: - cost: 499 - init_stock: 1840 - price: 499 - service_level: 0.96 - vlt: 3 - SKU358: - cost: 269 - init_stock: 1380 - price: 269 - service_level: 0.96 - vlt: 2 - SKU359: - cost: 82 - init_stock: 1500 - price: 82 - service_level: 0.96 - vlt: 3 - SKU36: - cost: 105 - init_stock: 200 - price: 105 - service_level: 0.96 - vlt: 2 - SKU360: - cost: 484 - init_stock: 500 - price: 484 - service_level: 0.96 - vlt: 1 - SKU361: - cost: 163 - init_stock: 1800 - price: 163 - service_level: 0.96 - vlt: 1 - SKU362: - cost: 464 - init_stock: 1740 - price: 464 - service_level: 0.96 - vlt: 2 - SKU363: - cost: 52 - init_stock: 1560 - price: 52 - service_level: 0.96 - vlt: 2 - SKU364: - cost: 387 - init_stock: 660 - price: 387 - service_level: 0.96 - vlt: 1 - SKU365: - cost: 158 - init_stock: 1660 - price: 158 - service_level: 0.96 - vlt: 3 - SKU366: - cost: 444 - init_stock: 1720 - price: 444 - service_level: 0.96 - vlt: 3 - SKU367: - cost: 272 - init_stock: 920 - price: 272 - service_level: 0.96 - vlt: 3 - SKU368: - cost: 472 - init_stock: 660 - price: 472 - service_level: 0.96 - vlt: 1 - SKU369: - cost: 96 - init_stock: 1500 - price: 96 - service_level: 0.96 - vlt: 2 - SKU37: - cost: 66 - init_stock: 1180 - price: 66 - service_level: 0.96 - vlt: 2 - SKU370: - cost: 112 - init_stock: 300 - price: 112 - service_level: 0.96 - vlt: 2 - SKU371: - cost: 328 - init_stock: 100 - price: 328 - service_level: 0.96 - vlt: 3 - SKU372: - cost: 67 - init_stock: 640 - price: 67 - service_level: 0.96 - vlt: 1 - SKU373: - cost: 58 - init_stock: 1340 - price: 58 - service_level: 0.96 - vlt: 3 - SKU374: - cost: 38 - init_stock: 1080 - price: 38 - service_level: 0.96 - vlt: 2 - SKU375: - cost: 283 - init_stock: 1440 - price: 283 - service_level: 0.96 - vlt: 1 - SKU376: - cost: 156 - init_stock: 1640 - price: 156 - service_level: 0.96 - vlt: 1 - SKU377: - cost: 78 - init_stock: 1340 - price: 78 - service_level: 0.96 - vlt: 3 - SKU378: - cost: 424 - init_stock: 700 - price: 424 - service_level: 0.96 - vlt: 1 - SKU379: - cost: 11 - init_stock: 980 - price: 11 - service_level: 0.96 - vlt: 1 - SKU38: - cost: 344 - init_stock: 860 - price: 344 - service_level: 0.96 - vlt: 2 - SKU380: - cost: 393 - init_stock: 1060 - price: 393 - service_level: 0.96 - vlt: 1 - SKU381: - cost: 476 - init_stock: 120 - price: 476 - service_level: 0.96 - vlt: 1 - SKU382: - cost: 125 - init_stock: 1420 - price: 125 - service_level: 0.96 - vlt: 2 - SKU383: - cost: 304 - init_stock: 1840 - price: 304 - service_level: 0.96 - vlt: 3 - SKU384: - cost: 320 - init_stock: 180 - price: 320 - service_level: 0.96 - vlt: 2 - SKU385: - cost: 120 - init_stock: 260 - price: 120 - service_level: 0.96 - vlt: 2 - SKU386: - cost: 295 - init_stock: 620 - price: 295 - service_level: 0.96 - vlt: 1 - SKU387: - cost: 112 - init_stock: 1940 - price: 112 - service_level: 0.96 - vlt: 3 - SKU388: - cost: 405 - init_stock: 880 - price: 405 - service_level: 0.96 - vlt: 2 - SKU389: - cost: 376 - init_stock: 1400 - price: 376 - service_level: 0.96 - vlt: 2 - SKU39: - cost: 25 - init_stock: 1020 - price: 25 - service_level: 0.96 - vlt: 3 - SKU390: - cost: 144 - init_stock: 780 - price: 144 - service_level: 0.96 - vlt: 2 - SKU391: - cost: 233 - init_stock: 1340 - price: 233 - service_level: 0.96 - vlt: 2 - SKU392: - cost: 163 - init_stock: 1480 - price: 163 - service_level: 0.96 - vlt: 2 - SKU393: - cost: 487 - init_stock: 1340 - price: 487 - service_level: 0.96 - vlt: 1 - SKU394: - cost: 154 - init_stock: 1060 - price: 154 - service_level: 0.96 - vlt: 2 - SKU395: - cost: 488 - init_stock: 660 - price: 488 - service_level: 0.96 - vlt: 3 - SKU396: - cost: 333 - init_stock: 1220 - price: 333 - service_level: 0.96 - vlt: 3 - SKU397: - cost: 460 - init_stock: 1020 - price: 460 - service_level: 0.96 - vlt: 1 - SKU398: - cost: 234 - init_stock: 1160 - price: 234 - service_level: 0.96 - vlt: 2 - SKU399: - cost: 376 - init_stock: 740 - price: 376 - service_level: 0.96 - vlt: 2 - SKU4: - cost: 439 - init_stock: 140 - price: 439 - service_level: 0.96 - vlt: 2 - SKU40: - cost: 490 - init_stock: 1980 - price: 490 - service_level: 0.96 - vlt: 3 - SKU400: - cost: 445 - init_stock: 1680 - price: 445 - service_level: 0.96 - vlt: 2 - SKU401: - cost: 410 - init_stock: 1560 - price: 410 - service_level: 0.96 - vlt: 1 - SKU402: - cost: 86 - init_stock: 140 - price: 86 - service_level: 0.96 - vlt: 3 - SKU403: - cost: 89 - init_stock: 1980 - price: 89 - service_level: 0.96 - vlt: 3 - SKU404: - cost: 287 - init_stock: 1220 - price: 287 - service_level: 0.96 - vlt: 1 - SKU405: - cost: 460 - init_stock: 380 - price: 460 - service_level: 0.96 - vlt: 1 - SKU406: - cost: 327 - init_stock: 2000 - price: 327 - service_level: 0.96 - vlt: 1 - SKU407: - cost: 26 - init_stock: 920 - price: 26 - service_level: 0.96 - vlt: 2 - SKU408: - cost: 444 - init_stock: 160 - price: 444 - service_level: 0.96 - vlt: 2 - SKU409: - cost: 257 - init_stock: 1820 - price: 257 - service_level: 0.96 - vlt: 2 - SKU41: - cost: 141 - init_stock: 1580 - price: 141 - service_level: 0.96 - vlt: 2 - SKU410: - cost: 70 - init_stock: 320 - price: 70 - service_level: 0.96 - vlt: 1 - SKU411: - cost: 210 - init_stock: 1900 - price: 210 - service_level: 0.96 - vlt: 3 - SKU412: - cost: 286 - init_stock: 1240 - price: 286 - service_level: 0.96 - vlt: 2 - SKU413: - cost: 403 - init_stock: 1660 - price: 403 - service_level: 0.96 - vlt: 3 - SKU414: - cost: 165 - init_stock: 1740 - price: 165 - service_level: 0.96 - vlt: 1 - SKU415: - cost: 291 - init_stock: 460 - price: 291 - service_level: 0.96 - vlt: 3 - SKU416: - cost: 228 - init_stock: 180 - price: 228 - service_level: 0.96 - vlt: 3 - SKU417: - cost: 443 - init_stock: 1440 - price: 443 - service_level: 0.96 - vlt: 1 - SKU418: - cost: 458 - init_stock: 260 - price: 458 - service_level: 0.96 - vlt: 3 - SKU419: - cost: 303 - init_stock: 1780 - price: 303 - service_level: 0.96 - vlt: 3 - SKU42: - cost: 462 - init_stock: 520 - price: 462 - service_level: 0.96 - vlt: 3 - SKU420: - cost: 268 - init_stock: 840 - price: 268 - service_level: 0.96 - vlt: 1 - SKU421: - cost: 214 - init_stock: 920 - price: 214 - service_level: 0.96 - vlt: 2 - SKU422: - cost: 52 - init_stock: 1080 - price: 52 - service_level: 0.96 - vlt: 3 - SKU423: - cost: 188 - init_stock: 1320 - price: 188 - service_level: 0.96 - vlt: 1 - SKU424: - cost: 189 - init_stock: 220 - price: 189 - service_level: 0.96 - vlt: 2 - SKU425: - cost: 162 - init_stock: 240 - price: 162 - service_level: 0.96 - vlt: 3 - SKU426: - cost: 125 - init_stock: 1960 - price: 125 - service_level: 0.96 - vlt: 3 - SKU427: - cost: 413 - init_stock: 1880 - price: 413 - service_level: 0.96 - vlt: 1 - SKU428: - cost: 391 - init_stock: 1260 - price: 391 - service_level: 0.96 - vlt: 3 - SKU429: - cost: 39 - init_stock: 820 - price: 39 - service_level: 0.96 - vlt: 1 - SKU43: - cost: 217 - init_stock: 1360 - price: 217 - service_level: 0.96 - vlt: 1 - SKU430: - cost: 216 - init_stock: 1460 - price: 216 - service_level: 0.96 - vlt: 2 - SKU431: - cost: 328 - init_stock: 420 - price: 328 - service_level: 0.96 - vlt: 1 - SKU432: - cost: 25 - init_stock: 920 - price: 25 - service_level: 0.96 - vlt: 3 - SKU433: - cost: 348 - init_stock: 900 - price: 348 - service_level: 0.96 - vlt: 2 - SKU434: - cost: 37 - init_stock: 600 - price: 37 - service_level: 0.96 - vlt: 1 - SKU435: - cost: 272 - init_stock: 600 - price: 272 - service_level: 0.96 - vlt: 3 - SKU436: - cost: 72 - init_stock: 1620 - price: 72 - service_level: 0.96 - vlt: 1 - SKU437: - cost: 115 - init_stock: 280 - price: 115 - service_level: 0.96 - vlt: 2 - SKU438: - cost: 143 - init_stock: 200 - price: 143 - service_level: 0.96 - vlt: 3 - SKU439: - cost: 256 - init_stock: 760 - price: 256 - service_level: 0.96 - vlt: 1 - SKU44: - cost: 360 - init_stock: 960 - price: 360 - service_level: 0.96 - vlt: 1 - SKU440: - cost: 248 - init_stock: 1640 - price: 248 - service_level: 0.96 - vlt: 2 - SKU441: - cost: 253 - init_stock: 1120 - price: 253 - service_level: 0.96 - vlt: 2 - SKU442: - cost: 470 - init_stock: 1760 - price: 470 - service_level: 0.96 - vlt: 2 - SKU443: - cost: 218 - init_stock: 1460 - price: 218 - service_level: 0.96 - vlt: 1 - SKU444: - cost: 156 - init_stock: 120 - price: 156 - service_level: 0.96 - vlt: 2 - SKU445: - cost: 260 - init_stock: 1720 - price: 260 - service_level: 0.96 - vlt: 2 - SKU446: - cost: 497 - init_stock: 980 - price: 497 - service_level: 0.96 - vlt: 1 - SKU447: - cost: 196 - init_stock: 100 - price: 196 - service_level: 0.96 - vlt: 1 - SKU448: - cost: 485 - init_stock: 1420 - price: 485 - service_level: 0.96 - vlt: 3 - SKU449: - cost: 282 - init_stock: 760 - price: 282 - service_level: 0.96 - vlt: 2 - SKU45: - cost: 253 - init_stock: 200 - price: 253 - service_level: 0.96 - vlt: 1 - SKU450: - cost: 58 - init_stock: 1960 - price: 58 - service_level: 0.96 - vlt: 2 - SKU451: - cost: 468 - init_stock: 1380 - price: 468 - service_level: 0.96 - vlt: 3 - SKU452: - cost: 63 - init_stock: 1580 - price: 63 - service_level: 0.96 - vlt: 1 - SKU453: - cost: 37 - init_stock: 700 - price: 37 - service_level: 0.96 - vlt: 3 - SKU454: - cost: 494 - init_stock: 1700 - price: 494 - service_level: 0.96 - vlt: 1 - SKU455: - cost: 136 - init_stock: 520 - price: 136 - service_level: 0.96 - vlt: 2 - SKU456: - cost: 338 - init_stock: 500 - price: 338 - service_level: 0.96 - vlt: 2 - SKU457: - cost: 169 - init_stock: 1280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU458: - cost: 403 - init_stock: 140 - price: 403 - service_level: 0.96 - vlt: 3 - SKU459: - cost: 425 - init_stock: 680 - price: 425 - service_level: 0.96 - vlt: 3 - SKU46: - cost: 299 - init_stock: 1000 - price: 299 - service_level: 0.96 - vlt: 3 - SKU460: - cost: 405 - init_stock: 1460 - price: 405 - service_level: 0.96 - vlt: 2 - SKU461: - cost: 251 - init_stock: 960 - price: 251 - service_level: 0.96 - vlt: 1 - SKU462: - cost: 448 - init_stock: 180 - price: 448 - service_level: 0.96 - vlt: 1 - SKU463: - cost: 187 - init_stock: 1640 - price: 187 - service_level: 0.96 - vlt: 3 - SKU464: - cost: 279 - init_stock: 680 - price: 279 - service_level: 0.96 - vlt: 3 - SKU465: - cost: 147 - init_stock: 2000 - price: 147 - service_level: 0.96 - vlt: 2 - SKU466: - cost: 97 - init_stock: 840 - price: 97 - service_level: 0.96 - vlt: 1 - SKU467: - cost: 142 - init_stock: 720 - price: 142 - service_level: 0.96 - vlt: 2 - SKU468: - cost: 55 - init_stock: 1000 - price: 55 - service_level: 0.96 - vlt: 2 - SKU469: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 3 - SKU47: - cost: 121 - init_stock: 780 - price: 121 - service_level: 0.96 - vlt: 1 - SKU470: - cost: 373 - init_stock: 640 - price: 373 - service_level: 0.96 - vlt: 2 - SKU471: - cost: 315 - init_stock: 1420 - price: 315 - service_level: 0.96 - vlt: 3 - SKU472: - cost: 421 - init_stock: 1180 - price: 421 - service_level: 0.96 - vlt: 2 - SKU473: - cost: 195 - init_stock: 420 - price: 195 - service_level: 0.96 - vlt: 1 - SKU474: - cost: 448 - init_stock: 1720 - price: 448 - service_level: 0.96 - vlt: 2 - SKU475: - cost: 34 - init_stock: 1880 - price: 34 - service_level: 0.96 - vlt: 1 - SKU476: - cost: 251 - init_stock: 1800 - price: 251 - service_level: 0.96 - vlt: 1 - SKU477: - cost: 289 - init_stock: 940 - price: 289 - service_level: 0.96 - vlt: 2 - SKU478: - cost: 479 - init_stock: 1760 - price: 479 - service_level: 0.96 - vlt: 2 - SKU479: - cost: 366 - init_stock: 760 - price: 366 - service_level: 0.96 - vlt: 1 - SKU48: - cost: 340 - init_stock: 1160 - price: 340 - service_level: 0.96 - vlt: 2 - SKU480: - cost: 18 - init_stock: 600 - price: 18 - service_level: 0.96 - vlt: 2 - SKU481: - cost: 330 - init_stock: 1760 - price: 330 - service_level: 0.96 - vlt: 1 - SKU482: - cost: 94 - init_stock: 1740 - price: 94 - service_level: 0.96 - vlt: 3 - SKU483: - cost: 298 - init_stock: 680 - price: 298 - service_level: 0.96 - vlt: 2 - SKU484: - cost: 84 - init_stock: 1460 - price: 84 - service_level: 0.96 - vlt: 3 - SKU485: - cost: 318 - init_stock: 1340 - price: 318 - service_level: 0.96 - vlt: 3 - SKU486: - cost: 122 - init_stock: 1040 - price: 122 - service_level: 0.96 - vlt: 1 - SKU487: - cost: 277 - init_stock: 1320 - price: 277 - service_level: 0.96 - vlt: 3 - SKU488: - cost: 36 - init_stock: 540 - price: 36 - service_level: 0.96 - vlt: 3 - SKU489: - cost: 59 - init_stock: 620 - price: 59 - service_level: 0.96 - vlt: 3 - SKU49: - cost: 263 - init_stock: 1900 - price: 263 - service_level: 0.96 - vlt: 3 - SKU490: - cost: 161 - init_stock: 1620 - price: 161 - service_level: 0.96 - vlt: 2 - SKU491: - cost: 444 - init_stock: 1500 - price: 444 - service_level: 0.96 - vlt: 2 - SKU492: - cost: 53 - init_stock: 1600 - price: 53 - service_level: 0.96 - vlt: 2 - SKU493: - cost: 461 - init_stock: 540 - price: 461 - service_level: 0.96 - vlt: 1 - SKU494: - cost: 145 - init_stock: 1880 - price: 145 - service_level: 0.96 - vlt: 1 - SKU495: - cost: 149 - init_stock: 1240 - price: 149 - service_level: 0.96 - vlt: 3 - SKU496: - cost: 342 - init_stock: 1180 - price: 342 - service_level: 0.96 - vlt: 1 - SKU497: - cost: 33 - init_stock: 180 - price: 33 - service_level: 0.96 - vlt: 3 - SKU498: - cost: 305 - init_stock: 1300 - price: 305 - service_level: 0.96 - vlt: 3 - SKU499: - cost: 307 - init_stock: 580 - price: 307 - service_level: 0.96 - vlt: 2 - SKU5: - cost: 466 - init_stock: 1420 - price: 466 - service_level: 0.96 - vlt: 2 - SKU50: - cost: 282 - init_stock: 740 - price: 282 - service_level: 0.96 - vlt: 3 - SKU500: - cost: 208 - init_stock: 840 - price: 208 - service_level: 0.96 - vlt: 3 - SKU501: - cost: 194 - init_stock: 1520 - price: 194 - service_level: 0.96 - vlt: 1 - SKU502: - cost: 69 - init_stock: 1300 - price: 69 - service_level: 0.96 - vlt: 2 - SKU503: - cost: 208 - init_stock: 1140 - price: 208 - service_level: 0.96 - vlt: 1 - SKU504: - cost: 251 - init_stock: 600 - price: 251 - service_level: 0.96 - vlt: 2 - SKU505: - cost: 426 - init_stock: 1180 - price: 426 - service_level: 0.96 - vlt: 1 - SKU506: - cost: 149 - init_stock: 1860 - price: 149 - service_level: 0.96 - vlt: 3 - SKU507: - cost: 187 - init_stock: 300 - price: 187 - service_level: 0.96 - vlt: 1 - SKU508: - cost: 272 - init_stock: 1920 - price: 272 - service_level: 0.96 - vlt: 1 - SKU509: - cost: 297 - init_stock: 1620 - price: 297 - service_level: 0.96 - vlt: 1 - SKU51: - cost: 295 - init_stock: 1340 - price: 295 - service_level: 0.96 - vlt: 3 - SKU510: - cost: 94 - init_stock: 180 - price: 94 - service_level: 0.96 - vlt: 1 - SKU511: - cost: 361 - init_stock: 1500 - price: 361 - service_level: 0.96 - vlt: 1 - SKU512: - cost: 467 - init_stock: 440 - price: 467 - service_level: 0.96 - vlt: 2 - SKU513: - cost: 343 - init_stock: 140 - price: 343 - service_level: 0.96 - vlt: 3 - SKU514: - cost: 186 - init_stock: 1260 - price: 186 - service_level: 0.96 - vlt: 3 - SKU515: - cost: 145 - init_stock: 1080 - price: 145 - service_level: 0.96 - vlt: 3 - SKU516: - cost: 64 - init_stock: 760 - price: 64 - service_level: 0.96 - vlt: 1 - SKU517: - cost: 117 - init_stock: 180 - price: 117 - service_level: 0.96 - vlt: 3 - SKU518: - cost: 26 - init_stock: 420 - price: 26 - service_level: 0.96 - vlt: 3 - SKU519: - cost: 233 - init_stock: 1000 - price: 233 - service_level: 0.96 - vlt: 2 - SKU52: - cost: 22 - init_stock: 1340 - price: 22 - service_level: 0.96 - vlt: 1 - SKU520: - cost: 209 - init_stock: 440 - price: 209 - service_level: 0.96 - vlt: 1 - SKU521: - cost: 220 - init_stock: 1000 - price: 220 - service_level: 0.96 - vlt: 3 - SKU522: - cost: 156 - init_stock: 1740 - price: 156 - service_level: 0.96 - vlt: 1 - SKU523: - cost: 65 - init_stock: 2000 - price: 65 - service_level: 0.96 - vlt: 3 - SKU524: - cost: 294 - init_stock: 1880 - price: 294 - service_level: 0.96 - vlt: 1 - SKU525: - cost: 432 - init_stock: 840 - price: 432 - service_level: 0.96 - vlt: 2 - SKU526: - cost: 419 - init_stock: 480 - price: 419 - service_level: 0.96 - vlt: 3 - SKU527: - cost: 131 - init_stock: 1400 - price: 131 - service_level: 0.96 - vlt: 1 - SKU528: - cost: 316 - init_stock: 420 - price: 316 - service_level: 0.96 - vlt: 3 - SKU529: - cost: 298 - init_stock: 620 - price: 298 - service_level: 0.96 - vlt: 3 - SKU53: - cost: 151 - init_stock: 1400 - price: 151 - service_level: 0.96 - vlt: 2 - SKU530: - cost: 131 - init_stock: 900 - price: 131 - service_level: 0.96 - vlt: 1 - SKU531: - cost: 103 - init_stock: 1200 - price: 103 - service_level: 0.96 - vlt: 3 - SKU532: - cost: 351 - init_stock: 1540 - price: 351 - service_level: 0.96 - vlt: 3 - SKU533: - cost: 296 - init_stock: 840 - price: 296 - service_level: 0.96 - vlt: 3 - SKU534: - cost: 425 - init_stock: 820 - price: 425 - service_level: 0.96 - vlt: 1 - SKU535: - cost: 304 - init_stock: 1720 - price: 304 - service_level: 0.96 - vlt: 2 - SKU536: - cost: 358 - init_stock: 1040 - price: 358 - service_level: 0.96 - vlt: 2 - SKU537: - cost: 321 - init_stock: 180 - price: 321 - service_level: 0.96 - vlt: 3 - SKU538: - cost: 457 - init_stock: 1000 - price: 457 - service_level: 0.96 - vlt: 1 - SKU539: - cost: 165 - init_stock: 1560 - price: 165 - service_level: 0.96 - vlt: 2 - SKU54: - cost: 158 - init_stock: 920 - price: 158 - service_level: 0.96 - vlt: 1 - SKU540: - cost: 388 - init_stock: 840 - price: 388 - service_level: 0.96 - vlt: 3 - SKU541: - cost: 131 - init_stock: 580 - price: 131 - service_level: 0.96 - vlt: 2 - SKU542: - cost: 38 - init_stock: 520 - price: 38 - service_level: 0.96 - vlt: 3 - SKU543: - cost: 430 - init_stock: 1700 - price: 430 - service_level: 0.96 - vlt: 2 - SKU544: - cost: 346 - init_stock: 1940 - price: 346 - service_level: 0.96 - vlt: 1 - SKU545: - cost: 175 - init_stock: 300 - price: 175 - service_level: 0.96 - vlt: 3 - SKU546: - cost: 491 - init_stock: 1920 - price: 491 - service_level: 0.96 - vlt: 3 - SKU547: - cost: 161 - init_stock: 880 - price: 161 - service_level: 0.96 - vlt: 2 - SKU548: - cost: 111 - init_stock: 120 - price: 111 - service_level: 0.96 - vlt: 1 - SKU549: - cost: 387 - init_stock: 1580 - price: 387 - service_level: 0.96 - vlt: 1 - SKU55: - cost: 482 - init_stock: 1300 - price: 482 - service_level: 0.96 - vlt: 3 - SKU550: - cost: 259 - init_stock: 1880 - price: 259 - service_level: 0.96 - vlt: 1 - SKU551: - cost: 46 - init_stock: 620 - price: 46 - service_level: 0.96 - vlt: 2 - SKU552: - cost: 191 - init_stock: 1800 - price: 191 - service_level: 0.96 - vlt: 3 - SKU553: - cost: 208 - init_stock: 600 - price: 208 - service_level: 0.96 - vlt: 1 - SKU554: - cost: 340 - init_stock: 820 - price: 340 - service_level: 0.96 - vlt: 1 - SKU555: - cost: 489 - init_stock: 1680 - price: 489 - service_level: 0.96 - vlt: 1 - SKU556: - cost: 110 - init_stock: 600 - price: 110 - service_level: 0.96 - vlt: 2 - SKU557: - cost: 334 - init_stock: 1460 - price: 334 - service_level: 0.96 - vlt: 2 - SKU558: - cost: 399 - init_stock: 340 - price: 399 - service_level: 0.96 - vlt: 1 - SKU559: - cost: 313 - init_stock: 1080 - price: 313 - service_level: 0.96 - vlt: 1 - SKU56: - cost: 377 - init_stock: 1480 - price: 377 - service_level: 0.96 - vlt: 1 - SKU560: - cost: 283 - init_stock: 820 - price: 283 - service_level: 0.96 - vlt: 1 - SKU561: - cost: 202 - init_stock: 780 - price: 202 - service_level: 0.96 - vlt: 3 - SKU562: - cost: 347 - init_stock: 1780 - price: 347 - service_level: 0.96 - vlt: 2 - SKU563: - cost: 401 - init_stock: 100 - price: 401 - service_level: 0.96 - vlt: 3 - SKU564: - cost: 109 - init_stock: 180 - price: 109 - service_level: 0.96 - vlt: 2 - SKU565: - cost: 57 - init_stock: 1500 - price: 57 - service_level: 0.96 - vlt: 3 - SKU566: - cost: 131 - init_stock: 220 - price: 131 - service_level: 0.96 - vlt: 2 - SKU567: - cost: 361 - init_stock: 180 - price: 361 - service_level: 0.96 - vlt: 1 - SKU568: - cost: 75 - init_stock: 1560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU569: - cost: 473 - init_stock: 960 - price: 473 - service_level: 0.96 - vlt: 2 - SKU57: - cost: 359 - init_stock: 1000 - price: 359 - service_level: 0.96 - vlt: 2 - SKU570: - cost: 388 - init_stock: 1660 - price: 388 - service_level: 0.96 - vlt: 2 - SKU571: - cost: 168 - init_stock: 780 - price: 168 - service_level: 0.96 - vlt: 1 - SKU572: - cost: 26 - init_stock: 900 - price: 26 - service_level: 0.96 - vlt: 3 - SKU573: - cost: 246 - init_stock: 820 - price: 246 - service_level: 0.96 - vlt: 2 - SKU574: - cost: 94 - init_stock: 1000 - price: 94 - service_level: 0.96 - vlt: 2 - SKU575: - cost: 237 - init_stock: 1580 - price: 237 - service_level: 0.96 - vlt: 1 - SKU576: - cost: 265 - init_stock: 1560 - price: 265 - service_level: 0.96 - vlt: 3 - SKU577: - cost: 18 - init_stock: 1740 - price: 18 - service_level: 0.96 - vlt: 3 - SKU578: - cost: 100 - init_stock: 1420 - price: 100 - service_level: 0.96 - vlt: 2 - SKU579: - cost: 415 - init_stock: 180 - price: 415 - service_level: 0.96 - vlt: 1 - SKU58: - cost: 362 - init_stock: 960 - price: 362 - service_level: 0.96 - vlt: 2 - SKU580: - cost: 203 - init_stock: 100 - price: 203 - service_level: 0.96 - vlt: 1 - SKU581: - cost: 152 - init_stock: 1720 - price: 152 - service_level: 0.96 - vlt: 2 - SKU582: - cost: 359 - init_stock: 1920 - price: 359 - service_level: 0.96 - vlt: 2 - SKU583: - cost: 86 - init_stock: 1740 - price: 86 - service_level: 0.96 - vlt: 1 - SKU584: - cost: 33 - init_stock: 900 - price: 33 - service_level: 0.96 - vlt: 2 - SKU585: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 2 - SKU586: - cost: 61 - init_stock: 880 - price: 61 - service_level: 0.96 - vlt: 2 - SKU587: - cost: 290 - init_stock: 1560 - price: 290 - service_level: 0.96 - vlt: 2 - SKU588: - cost: 476 - init_stock: 880 - price: 476 - service_level: 0.96 - vlt: 2 - SKU589: - cost: 244 - init_stock: 680 - price: 244 - service_level: 0.96 - vlt: 3 - SKU59: - cost: 145 - init_stock: 1020 - price: 145 - service_level: 0.96 - vlt: 1 - SKU590: - cost: 70 - init_stock: 620 - price: 70 - service_level: 0.96 - vlt: 2 - SKU591: - cost: 206 - init_stock: 1680 - price: 206 - service_level: 0.96 - vlt: 2 - SKU592: - cost: 218 - init_stock: 920 - price: 218 - service_level: 0.96 - vlt: 1 - SKU593: - cost: 124 - init_stock: 880 - price: 124 - service_level: 0.96 - vlt: 1 - SKU594: - cost: 238 - init_stock: 1000 - price: 238 - service_level: 0.96 - vlt: 1 - SKU595: - cost: 73 - init_stock: 860 - price: 73 - service_level: 0.96 - vlt: 3 - SKU596: - cost: 432 - init_stock: 140 - price: 432 - service_level: 0.96 - vlt: 2 - SKU597: - cost: 252 - init_stock: 1740 - price: 252 - service_level: 0.96 - vlt: 2 - SKU598: - cost: 348 - init_stock: 280 - price: 348 - service_level: 0.96 - vlt: 1 - SKU599: - cost: 54 - init_stock: 1360 - price: 54 - service_level: 0.96 - vlt: 2 - SKU6: - cost: 122 - init_stock: 1760 - price: 122 - service_level: 0.96 - vlt: 2 - SKU60: - cost: 200 - init_stock: 780 - price: 200 - service_level: 0.96 - vlt: 1 - SKU600: - cost: 386 - init_stock: 1300 - price: 386 - service_level: 0.96 - vlt: 1 - SKU601: - cost: 138 - init_stock: 780 - price: 138 - service_level: 0.96 - vlt: 3 - SKU602: - cost: 184 - init_stock: 1960 - price: 184 - service_level: 0.96 - vlt: 1 - SKU603: - cost: 278 - init_stock: 840 - price: 278 - service_level: 0.96 - vlt: 3 - SKU604: - cost: 270 - init_stock: 1000 - price: 270 - service_level: 0.96 - vlt: 3 - SKU605: - cost: 288 - init_stock: 480 - price: 288 - service_level: 0.96 - vlt: 2 - SKU606: - cost: 114 - init_stock: 220 - price: 114 - service_level: 0.96 - vlt: 3 - SKU607: - cost: 208 - init_stock: 1580 - price: 208 - service_level: 0.96 - vlt: 2 - SKU608: - cost: 39 - init_stock: 1460 - price: 39 - service_level: 0.96 - vlt: 1 - SKU609: - cost: 102 - init_stock: 380 - price: 102 - service_level: 0.96 - vlt: 2 - SKU61: - cost: 461 - init_stock: 1500 - price: 461 - service_level: 0.96 - vlt: 1 - SKU610: - cost: 449 - init_stock: 1360 - price: 449 - service_level: 0.96 - vlt: 1 - SKU611: - cost: 306 - init_stock: 1540 - price: 306 - service_level: 0.96 - vlt: 3 - SKU612: - cost: 391 - init_stock: 1760 - price: 391 - service_level: 0.96 - vlt: 2 - SKU613: - cost: 174 - init_stock: 140 - price: 174 - service_level: 0.96 - vlt: 3 - SKU614: - cost: 173 - init_stock: 300 - price: 173 - service_level: 0.96 - vlt: 1 - SKU615: - cost: 330 - init_stock: 1820 - price: 330 - service_level: 0.96 - vlt: 2 - SKU616: - cost: 232 - init_stock: 1460 - price: 232 - service_level: 0.96 - vlt: 1 - SKU617: - cost: 203 - init_stock: 1200 - price: 203 - service_level: 0.96 - vlt: 3 - SKU618: - cost: 77 - init_stock: 460 - price: 77 - service_level: 0.96 - vlt: 1 - SKU619: - cost: 189 - init_stock: 1720 - price: 189 - service_level: 0.96 - vlt: 2 - SKU62: - cost: 124 - init_stock: 180 - price: 124 - service_level: 0.96 - vlt: 2 - SKU620: - cost: 231 - init_stock: 780 - price: 231 - service_level: 0.96 - vlt: 3 - SKU621: - cost: 199 - init_stock: 1440 - price: 199 - service_level: 0.96 - vlt: 2 - SKU622: - cost: 253 - init_stock: 1300 - price: 253 - service_level: 0.96 - vlt: 3 - SKU623: - cost: 335 - init_stock: 1100 - price: 335 - service_level: 0.96 - vlt: 1 - SKU624: - cost: 482 - init_stock: 1080 - price: 482 - service_level: 0.96 - vlt: 2 - SKU625: - cost: 46 - init_stock: 1780 - price: 46 - service_level: 0.96 - vlt: 1 - SKU626: - cost: 451 - init_stock: 1780 - price: 451 - service_level: 0.96 - vlt: 3 - SKU627: - cost: 220 - init_stock: 1640 - price: 220 - service_level: 0.96 - vlt: 3 - SKU628: - cost: 124 - init_stock: 520 - price: 124 - service_level: 0.96 - vlt: 2 - SKU629: - cost: 353 - init_stock: 1840 - price: 353 - service_level: 0.96 - vlt: 1 - SKU63: - cost: 68 - init_stock: 280 - price: 68 - service_level: 0.96 - vlt: 3 - SKU630: - cost: 122 - init_stock: 340 - price: 122 - service_level: 0.96 - vlt: 1 - SKU631: - cost: 296 - init_stock: 980 - price: 296 - service_level: 0.96 - vlt: 2 - SKU632: - cost: 85 - init_stock: 1880 - price: 85 - service_level: 0.96 - vlt: 2 - SKU633: - cost: 184 - init_stock: 1340 - price: 184 - service_level: 0.96 - vlt: 2 - SKU634: - cost: 17 - init_stock: 1460 - price: 17 - service_level: 0.96 - vlt: 1 - SKU635: - cost: 341 - init_stock: 1860 - price: 341 - service_level: 0.96 - vlt: 1 - SKU636: - cost: 385 - init_stock: 740 - price: 385 - service_level: 0.96 - vlt: 1 - SKU637: - cost: 83 - init_stock: 1220 - price: 83 - service_level: 0.96 - vlt: 3 - SKU638: - cost: 375 - init_stock: 160 - price: 375 - service_level: 0.96 - vlt: 1 - SKU639: - cost: 327 - init_stock: 800 - price: 327 - service_level: 0.96 - vlt: 2 - SKU64: - cost: 36 - init_stock: 380 - price: 36 - service_level: 0.96 - vlt: 3 - SKU640: - cost: 275 - init_stock: 1820 - price: 275 - service_level: 0.96 - vlt: 1 - SKU641: - cost: 365 - init_stock: 1620 - price: 365 - service_level: 0.96 - vlt: 1 - SKU642: - cost: 414 - init_stock: 500 - price: 414 - service_level: 0.96 - vlt: 2 - SKU643: - cost: 358 - init_stock: 920 - price: 358 - service_level: 0.96 - vlt: 2 - SKU644: - cost: 46 - init_stock: 1640 - price: 46 - service_level: 0.96 - vlt: 2 - SKU645: - cost: 467 - init_stock: 1980 - price: 467 - service_level: 0.96 - vlt: 3 - SKU646: - cost: 189 - init_stock: 260 - price: 189 - service_level: 0.96 - vlt: 3 - SKU647: - cost: 303 - init_stock: 1540 - price: 303 - service_level: 0.96 - vlt: 3 - SKU648: - cost: 226 - init_stock: 1100 - price: 226 - service_level: 0.96 - vlt: 1 - SKU649: - cost: 198 - init_stock: 500 - price: 198 - service_level: 0.96 - vlt: 3 - SKU65: - cost: 15 - init_stock: 1880 - price: 15 - service_level: 0.96 - vlt: 2 - SKU650: - cost: 351 - init_stock: 1120 - price: 351 - service_level: 0.96 - vlt: 2 - SKU651: - cost: 380 - init_stock: 1920 - price: 380 - service_level: 0.96 - vlt: 3 - SKU652: - cost: 123 - init_stock: 800 - price: 123 - service_level: 0.96 - vlt: 2 - SKU653: - cost: 479 - init_stock: 1920 - price: 479 - service_level: 0.96 - vlt: 2 - SKU654: - cost: 66 - init_stock: 160 - price: 66 - service_level: 0.96 - vlt: 3 - SKU655: - cost: 333 - init_stock: 840 - price: 333 - service_level: 0.96 - vlt: 1 - SKU656: - cost: 53 - init_stock: 1440 - price: 53 - service_level: 0.96 - vlt: 2 - SKU657: - cost: 73 - init_stock: 1620 - price: 73 - service_level: 0.96 - vlt: 2 - SKU658: - cost: 70 - init_stock: 720 - price: 70 - service_level: 0.96 - vlt: 3 - SKU659: - cost: 21 - init_stock: 1120 - price: 21 - service_level: 0.96 - vlt: 1 - SKU66: - cost: 143 - init_stock: 1800 - price: 143 - service_level: 0.96 - vlt: 1 - SKU660: - cost: 487 - init_stock: 1660 - price: 487 - service_level: 0.96 - vlt: 1 - SKU661: - cost: 344 - init_stock: 1480 - price: 344 - service_level: 0.96 - vlt: 2 - SKU662: - cost: 372 - init_stock: 760 - price: 372 - service_level: 0.96 - vlt: 3 - SKU663: - cost: 418 - init_stock: 800 - price: 418 - service_level: 0.96 - vlt: 3 - SKU664: - cost: 351 - init_stock: 1680 - price: 351 - service_level: 0.96 - vlt: 1 - SKU665: - cost: 493 - init_stock: 860 - price: 493 - service_level: 0.96 - vlt: 3 - SKU666: - cost: 341 - init_stock: 1760 - price: 341 - service_level: 0.96 - vlt: 1 - SKU667: - cost: 325 - init_stock: 260 - price: 325 - service_level: 0.96 - vlt: 3 - SKU668: - cost: 286 - init_stock: 1200 - price: 286 - service_level: 0.96 - vlt: 3 - SKU669: - cost: 74 - init_stock: 320 - price: 74 - service_level: 0.96 - vlt: 1 - SKU67: - cost: 247 - init_stock: 1060 - price: 247 - service_level: 0.96 - vlt: 1 - SKU670: - cost: 289 - init_stock: 1720 - price: 289 - service_level: 0.96 - vlt: 1 - SKU671: - cost: 267 - init_stock: 1660 - price: 267 - service_level: 0.96 - vlt: 3 - SKU672: - cost: 369 - init_stock: 260 - price: 369 - service_level: 0.96 - vlt: 2 - SKU673: - cost: 322 - init_stock: 820 - price: 322 - service_level: 0.96 - vlt: 2 - SKU674: - cost: 223 - init_stock: 440 - price: 223 - service_level: 0.96 - vlt: 1 - SKU675: - cost: 438 - init_stock: 660 - price: 438 - service_level: 0.96 - vlt: 1 - SKU676: - cost: 244 - init_stock: 1220 - price: 244 - service_level: 0.96 - vlt: 1 - SKU677: - cost: 425 - init_stock: 880 - price: 425 - service_level: 0.96 - vlt: 2 - SKU678: - cost: 168 - init_stock: 720 - price: 168 - service_level: 0.96 - vlt: 1 - SKU679: - cost: 395 - init_stock: 880 - price: 395 - service_level: 0.96 - vlt: 2 - SKU68: - cost: 169 - init_stock: 280 - price: 169 - service_level: 0.96 - vlt: 2 - SKU680: - cost: 260 - init_stock: 600 - price: 260 - service_level: 0.96 - vlt: 1 - SKU681: - cost: 464 - init_stock: 1940 - price: 464 - service_level: 0.96 - vlt: 3 - SKU682: - cost: 370 - init_stock: 1060 - price: 370 - service_level: 0.96 - vlt: 2 - SKU683: - cost: 327 - init_stock: 1340 - price: 327 - service_level: 0.96 - vlt: 3 - SKU684: - cost: 355 - init_stock: 1580 - price: 355 - service_level: 0.96 - vlt: 1 - SKU685: - cost: 422 - init_stock: 900 - price: 422 - service_level: 0.96 - vlt: 2 - SKU686: - cost: 63 - init_stock: 1260 - price: 63 - service_level: 0.96 - vlt: 1 - SKU687: - cost: 34 - init_stock: 1520 - price: 34 - service_level: 0.96 - vlt: 2 - SKU688: - cost: 484 - init_stock: 1540 - price: 484 - service_level: 0.96 - vlt: 3 - SKU689: - cost: 499 - init_stock: 300 - price: 499 - service_level: 0.96 - vlt: 1 - SKU69: - cost: 176 - init_stock: 1340 - price: 176 - service_level: 0.96 - vlt: 1 - SKU690: - cost: 437 - init_stock: 1660 - price: 437 - service_level: 0.96 - vlt: 2 - SKU691: - cost: 17 - init_stock: 1580 - price: 17 - service_level: 0.96 - vlt: 3 - SKU692: - cost: 225 - init_stock: 1300 - price: 225 - service_level: 0.96 - vlt: 1 - SKU693: - cost: 19 - init_stock: 1220 - price: 19 - service_level: 0.96 - vlt: 2 - SKU694: - cost: 208 - init_stock: 1500 - price: 208 - service_level: 0.96 - vlt: 3 - SKU695: - cost: 190 - init_stock: 140 - price: 190 - service_level: 0.96 - vlt: 2 - SKU696: - cost: 348 - init_stock: 1160 - price: 348 - service_level: 0.96 - vlt: 1 - SKU697: - cost: 455 - init_stock: 1600 - price: 455 - service_level: 0.96 - vlt: 1 - SKU698: - cost: 198 - init_stock: 1220 - price: 198 - service_level: 0.96 - vlt: 3 - SKU699: - cost: 31 - init_stock: 400 - price: 31 - service_level: 0.96 - vlt: 3 - SKU7: - cost: 101 - init_stock: 1920 - price: 101 - service_level: 0.96 - vlt: 2 - SKU70: - cost: 186 - init_stock: 700 - price: 186 - service_level: 0.96 - vlt: 1 - SKU700: - cost: 74 - init_stock: 180 - price: 74 - service_level: 0.96 - vlt: 2 - SKU701: - cost: 399 - init_stock: 1540 - price: 399 - service_level: 0.96 - vlt: 1 - SKU702: - cost: 82 - init_stock: 680 - price: 82 - service_level: 0.96 - vlt: 1 - SKU703: - cost: 363 - init_stock: 760 - price: 363 - service_level: 0.96 - vlt: 1 - SKU704: - cost: 384 - init_stock: 1740 - price: 384 - service_level: 0.96 - vlt: 2 - SKU705: - cost: 128 - init_stock: 1260 - price: 128 - service_level: 0.96 - vlt: 2 - SKU706: - cost: 182 - init_stock: 1820 - price: 182 - service_level: 0.96 - vlt: 2 - SKU707: - cost: 18 - init_stock: 1380 - price: 18 - service_level: 0.96 - vlt: 1 - SKU708: - cost: 178 - init_stock: 560 - price: 178 - service_level: 0.96 - vlt: 3 - SKU709: - cost: 102 - init_stock: 1060 - price: 102 - service_level: 0.96 - vlt: 3 - SKU71: - cost: 315 - init_stock: 900 - price: 315 - service_level: 0.96 - vlt: 2 - SKU710: - cost: 252 - init_stock: 380 - price: 252 - service_level: 0.96 - vlt: 2 - SKU711: - cost: 281 - init_stock: 1380 - price: 281 - service_level: 0.96 - vlt: 1 - SKU712: - cost: 232 - init_stock: 220 - price: 232 - service_level: 0.96 - vlt: 2 - SKU713: - cost: 285 - init_stock: 860 - price: 285 - service_level: 0.96 - vlt: 2 - SKU714: - cost: 79 - init_stock: 820 - price: 79 - service_level: 0.96 - vlt: 3 - SKU715: - cost: 234 - init_stock: 340 - price: 234 - service_level: 0.96 - vlt: 1 - SKU716: - cost: 70 - init_stock: 1500 - price: 70 - service_level: 0.96 - vlt: 2 - SKU717: - cost: 345 - init_stock: 160 - price: 345 - service_level: 0.96 - vlt: 2 - SKU718: - cost: 357 - init_stock: 1160 - price: 357 - service_level: 0.96 - vlt: 2 - SKU719: - cost: 340 - init_stock: 1300 - price: 340 - service_level: 0.96 - vlt: 3 - SKU72: - cost: 458 - init_stock: 1700 - price: 458 - service_level: 0.96 - vlt: 2 - SKU720: - cost: 325 - init_stock: 840 - price: 325 - service_level: 0.96 - vlt: 3 - SKU721: - cost: 73 - init_stock: 220 - price: 73 - service_level: 0.96 - vlt: 2 - SKU722: - cost: 392 - init_stock: 1940 - price: 392 - service_level: 0.96 - vlt: 1 - SKU723: - cost: 318 - init_stock: 1780 - price: 318 - service_level: 0.96 - vlt: 3 - SKU724: - cost: 400 - init_stock: 660 - price: 400 - service_level: 0.96 - vlt: 1 - SKU725: - cost: 175 - init_stock: 740 - price: 175 - service_level: 0.96 - vlt: 1 - SKU726: - cost: 458 - init_stock: 1780 - price: 458 - service_level: 0.96 - vlt: 3 - SKU727: - cost: 418 - init_stock: 1140 - price: 418 - service_level: 0.96 - vlt: 1 - SKU728: - cost: 475 - init_stock: 740 - price: 475 - service_level: 0.96 - vlt: 3 - SKU729: - cost: 324 - init_stock: 720 - price: 324 - service_level: 0.96 - vlt: 3 - SKU73: - cost: 212 - init_stock: 1080 - price: 212 - service_level: 0.96 - vlt: 2 - SKU730: - cost: 16 - init_stock: 260 - price: 16 - service_level: 0.96 - vlt: 2 - SKU731: - cost: 88 - init_stock: 1640 - price: 88 - service_level: 0.96 - vlt: 2 - SKU732: - cost: 41 - init_stock: 1240 - price: 41 - service_level: 0.96 - vlt: 3 - SKU733: - cost: 315 - init_stock: 520 - price: 315 - service_level: 0.96 - vlt: 3 - SKU734: - cost: 37 - init_stock: 1020 - price: 37 - service_level: 0.96 - vlt: 3 - SKU735: - cost: 266 - init_stock: 1980 - price: 266 - service_level: 0.96 - vlt: 3 - SKU736: - cost: 368 - init_stock: 500 - price: 368 - service_level: 0.96 - vlt: 2 - SKU737: - cost: 475 - init_stock: 620 - price: 475 - service_level: 0.96 - vlt: 2 - SKU738: - cost: 185 - init_stock: 960 - price: 185 - service_level: 0.96 - vlt: 3 - SKU739: - cost: 475 - init_stock: 1480 - price: 475 - service_level: 0.96 - vlt: 2 - SKU74: - cost: 159 - init_stock: 840 - price: 159 - service_level: 0.96 - vlt: 1 - SKU740: - cost: 390 - init_stock: 1280 - price: 390 - service_level: 0.96 - vlt: 1 - SKU741: - cost: 91 - init_stock: 1120 - price: 91 - service_level: 0.96 - vlt: 1 - SKU742: - cost: 188 - init_stock: 440 - price: 188 - service_level: 0.96 - vlt: 1 - SKU743: - cost: 217 - init_stock: 860 - price: 217 - service_level: 0.96 - vlt: 3 - SKU744: - cost: 379 - init_stock: 1160 - price: 379 - service_level: 0.96 - vlt: 1 - SKU745: - cost: 316 - init_stock: 1840 - price: 316 - service_level: 0.96 - vlt: 2 - SKU746: - cost: 437 - init_stock: 800 - price: 437 - service_level: 0.96 - vlt: 2 - SKU747: - cost: 373 - init_stock: 1580 - price: 373 - service_level: 0.96 - vlt: 2 - SKU748: - cost: 275 - init_stock: 1320 - price: 275 - service_level: 0.96 - vlt: 2 - SKU749: - cost: 394 - init_stock: 1060 - price: 394 - service_level: 0.96 - vlt: 1 - SKU75: - cost: 131 - init_stock: 380 - price: 131 - service_level: 0.96 - vlt: 1 - SKU750: - cost: 256 - init_stock: 1240 - price: 256 - service_level: 0.96 - vlt: 3 - SKU751: - cost: 369 - init_stock: 1300 - price: 369 - service_level: 0.96 - vlt: 3 - SKU752: - cost: 259 - init_stock: 1560 - price: 259 - service_level: 0.96 - vlt: 3 - SKU753: - cost: 77 - init_stock: 1300 - price: 77 - service_level: 0.96 - vlt: 3 - SKU754: - cost: 387 - init_stock: 260 - price: 387 - service_level: 0.96 - vlt: 2 - SKU755: - cost: 354 - init_stock: 1600 - price: 354 - service_level: 0.96 - vlt: 3 - SKU756: - cost: 246 - init_stock: 1800 - price: 246 - service_level: 0.96 - vlt: 1 - SKU757: - cost: 40 - init_stock: 1140 - price: 40 - service_level: 0.96 - vlt: 3 - SKU758: - cost: 241 - init_stock: 800 - price: 241 - service_level: 0.96 - vlt: 2 - SKU759: - cost: 435 - init_stock: 760 - price: 435 - service_level: 0.96 - vlt: 1 - SKU76: - cost: 147 - init_stock: 1280 - price: 147 - service_level: 0.96 - vlt: 2 - SKU760: - cost: 176 - init_stock: 1020 - price: 176 - service_level: 0.96 - vlt: 3 - SKU761: - cost: 224 - init_stock: 1100 - price: 224 - service_level: 0.96 - vlt: 1 - SKU762: - cost: 264 - init_stock: 1020 - price: 264 - service_level: 0.96 - vlt: 1 - SKU763: - cost: 385 - init_stock: 1580 - price: 385 - service_level: 0.96 - vlt: 2 - SKU764: - cost: 349 - init_stock: 680 - price: 349 - service_level: 0.96 - vlt: 1 - SKU765: - cost: 345 - init_stock: 260 - price: 345 - service_level: 0.96 - vlt: 1 - SKU766: - cost: 478 - init_stock: 400 - price: 478 - service_level: 0.96 - vlt: 2 - SKU767: - cost: 95 - init_stock: 1520 - price: 95 - service_level: 0.96 - vlt: 1 - SKU768: - cost: 181 - init_stock: 1840 - price: 181 - service_level: 0.96 - vlt: 2 - SKU769: - cost: 24 - init_stock: 580 - price: 24 - service_level: 0.96 - vlt: 2 - SKU77: - cost: 409 - init_stock: 1440 - price: 409 - service_level: 0.96 - vlt: 1 - SKU770: - cost: 150 - init_stock: 1240 - price: 150 - service_level: 0.96 - vlt: 2 - SKU771: - cost: 101 - init_stock: 140 - price: 101 - service_level: 0.96 - vlt: 1 - SKU772: - cost: 256 - init_stock: 120 - price: 256 - service_level: 0.96 - vlt: 3 - SKU773: - cost: 84 - init_stock: 1840 - price: 84 - service_level: 0.96 - vlt: 1 - SKU774: - cost: 447 - init_stock: 1180 - price: 447 - service_level: 0.96 - vlt: 1 - SKU775: - cost: 175 - init_stock: 1720 - price: 175 - service_level: 0.96 - vlt: 3 - SKU776: - cost: 103 - init_stock: 1700 - price: 103 - service_level: 0.96 - vlt: 2 - SKU777: - cost: 292 - init_stock: 1140 - price: 292 - service_level: 0.96 - vlt: 2 - SKU778: - cost: 203 - init_stock: 160 - price: 203 - service_level: 0.96 - vlt: 3 - SKU779: - cost: 117 - init_stock: 860 - price: 117 - service_level: 0.96 - vlt: 1 - SKU78: - cost: 234 - init_stock: 260 - price: 234 - service_level: 0.96 - vlt: 3 - SKU780: - cost: 69 - init_stock: 1640 - price: 69 - service_level: 0.96 - vlt: 3 - SKU781: - cost: 372 - init_stock: 720 - price: 372 - service_level: 0.96 - vlt: 3 - SKU782: - cost: 27 - init_stock: 200 - price: 27 - service_level: 0.96 - vlt: 3 - SKU783: - cost: 87 - init_stock: 1620 - price: 87 - service_level: 0.96 - vlt: 2 - SKU784: - cost: 309 - init_stock: 1040 - price: 309 - service_level: 0.96 - vlt: 3 - SKU785: - cost: 191 - init_stock: 1400 - price: 191 - service_level: 0.96 - vlt: 2 - SKU786: - cost: 91 - init_stock: 440 - price: 91 - service_level: 0.96 - vlt: 3 - SKU787: - cost: 360 - init_stock: 1220 - price: 360 - service_level: 0.96 - vlt: 1 - SKU788: - cost: 351 - init_stock: 560 - price: 351 - service_level: 0.96 - vlt: 1 - SKU789: - cost: 153 - init_stock: 1160 - price: 153 - service_level: 0.96 - vlt: 2 - SKU79: - cost: 245 - init_stock: 220 - price: 245 - service_level: 0.96 - vlt: 1 - SKU790: - cost: 417 - init_stock: 1320 - price: 417 - service_level: 0.96 - vlt: 2 - SKU791: - cost: 134 - init_stock: 560 - price: 134 - service_level: 0.96 - vlt: 1 - SKU792: - cost: 313 - init_stock: 420 - price: 313 - service_level: 0.96 - vlt: 2 - SKU793: - cost: 195 - init_stock: 1520 - price: 195 - service_level: 0.96 - vlt: 2 - SKU794: - cost: 325 - init_stock: 1600 - price: 325 - service_level: 0.96 - vlt: 3 - SKU795: - cost: 276 - init_stock: 960 - price: 276 - service_level: 0.96 - vlt: 3 - SKU796: - cost: 447 - init_stock: 980 - price: 447 - service_level: 0.96 - vlt: 1 - SKU797: - cost: 100 - init_stock: 780 - price: 100 - service_level: 0.96 - vlt: 3 - SKU798: - cost: 426 - init_stock: 600 - price: 426 - service_level: 0.96 - vlt: 2 - SKU799: - cost: 63 - init_stock: 140 - price: 63 - service_level: 0.96 - vlt: 2 - SKU8: - cost: 125 - init_stock: 1260 - price: 125 - service_level: 0.96 - vlt: 3 - SKU80: - cost: 163 - init_stock: 1400 - price: 163 - service_level: 0.96 - vlt: 1 - SKU800: - cost: 298 - init_stock: 1880 - price: 298 - service_level: 0.96 - vlt: 2 - SKU801: - cost: 389 - init_stock: 720 - price: 389 - service_level: 0.96 - vlt: 2 - SKU802: - cost: 173 - init_stock: 1240 - price: 173 - service_level: 0.96 - vlt: 2 - SKU803: - cost: 272 - init_stock: 380 - price: 272 - service_level: 0.96 - vlt: 3 - SKU804: - cost: 390 - init_stock: 940 - price: 390 - service_level: 0.96 - vlt: 3 - SKU805: - cost: 61 - init_stock: 660 - price: 61 - service_level: 0.96 - vlt: 3 - SKU806: - cost: 158 - init_stock: 1040 - price: 158 - service_level: 0.96 - vlt: 1 - SKU807: - cost: 453 - init_stock: 520 - price: 453 - service_level: 0.96 - vlt: 3 - SKU808: - cost: 249 - init_stock: 1820 - price: 249 - service_level: 0.96 - vlt: 1 - SKU809: - cost: 55 - init_stock: 1300 - price: 55 - service_level: 0.96 - vlt: 1 - SKU81: - cost: 182 - init_stock: 1320 - price: 182 - service_level: 0.96 - vlt: 3 - SKU810: - cost: 276 - init_stock: 120 - price: 276 - service_level: 0.96 - vlt: 2 - SKU811: - cost: 254 - init_stock: 740 - price: 254 - service_level: 0.96 - vlt: 3 - SKU812: - cost: 252 - init_stock: 1600 - price: 252 - service_level: 0.96 - vlt: 1 - SKU813: - cost: 253 - init_stock: 140 - price: 253 - service_level: 0.96 - vlt: 3 - SKU814: - cost: 485 - init_stock: 1940 - price: 485 - service_level: 0.96 - vlt: 1 - SKU815: - cost: 390 - init_stock: 480 - price: 390 - service_level: 0.96 - vlt: 2 - SKU816: - cost: 152 - init_stock: 1820 - price: 152 - service_level: 0.96 - vlt: 3 - SKU817: - cost: 227 - init_stock: 100 - price: 227 - service_level: 0.96 - vlt: 2 - SKU818: - cost: 354 - init_stock: 860 - price: 354 - service_level: 0.96 - vlt: 2 - SKU819: - cost: 302 - init_stock: 540 - price: 302 - service_level: 0.96 - vlt: 1 - SKU82: - cost: 290 - init_stock: 560 - price: 290 - service_level: 0.96 - vlt: 1 - SKU820: - cost: 264 - init_stock: 1640 - price: 264 - service_level: 0.96 - vlt: 3 - SKU821: - cost: 99 - init_stock: 1380 - price: 99 - service_level: 0.96 - vlt: 1 - SKU822: - cost: 136 - init_stock: 1400 - price: 136 - service_level: 0.96 - vlt: 3 - SKU823: - cost: 75 - init_stock: 560 - price: 75 - service_level: 0.96 - vlt: 2 - SKU824: - cost: 170 - init_stock: 1400 - price: 170 - service_level: 0.96 - vlt: 1 - SKU825: - cost: 214 - init_stock: 300 - price: 214 - service_level: 0.96 - vlt: 1 - SKU826: - cost: 386 - init_stock: 1100 - price: 386 - service_level: 0.96 - vlt: 2 - SKU827: - cost: 148 - init_stock: 1280 - price: 148 - service_level: 0.96 - vlt: 2 - SKU828: - cost: 400 - init_stock: 1380 - price: 400 - service_level: 0.96 - vlt: 1 - SKU829: - cost: 61 - init_stock: 1480 - price: 61 - service_level: 0.96 - vlt: 1 - SKU83: - cost: 296 - init_stock: 340 - price: 296 - service_level: 0.96 - vlt: 2 - SKU830: - cost: 167 - init_stock: 480 - price: 167 - service_level: 0.96 - vlt: 3 - SKU831: - cost: 262 - init_stock: 580 - price: 262 - service_level: 0.96 - vlt: 1 - SKU832: - cost: 33 - init_stock: 1640 - price: 33 - service_level: 0.96 - vlt: 1 - SKU833: - cost: 400 - init_stock: 1960 - price: 400 - service_level: 0.96 - vlt: 3 - SKU834: - cost: 422 - init_stock: 100 - price: 422 - service_level: 0.96 - vlt: 1 - SKU835: - cost: 440 - init_stock: 1040 - price: 440 - service_level: 0.96 - vlt: 2 - SKU836: - cost: 323 - init_stock: 1920 - price: 323 - service_level: 0.96 - vlt: 1 - SKU837: - cost: 373 - init_stock: 520 - price: 373 - service_level: 0.96 - vlt: 3 - SKU838: - cost: 456 - init_stock: 1540 - price: 456 - service_level: 0.96 - vlt: 1 - SKU839: - cost: 473 - init_stock: 1200 - price: 473 - service_level: 0.96 - vlt: 1 - SKU84: - cost: 318 - init_stock: 840 - price: 318 - service_level: 0.96 - vlt: 3 - SKU840: - cost: 266 - init_stock: 1000 - price: 266 - service_level: 0.96 - vlt: 1 - SKU841: - cost: 285 - init_stock: 1700 - price: 285 - service_level: 0.96 - vlt: 3 - SKU842: - cost: 41 - init_stock: 1680 - price: 41 - service_level: 0.96 - vlt: 1 - SKU843: - cost: 360 - init_stock: 1440 - price: 360 - service_level: 0.96 - vlt: 2 - SKU844: - cost: 51 - init_stock: 1580 - price: 51 - service_level: 0.96 - vlt: 3 - SKU845: - cost: 288 - init_stock: 440 - price: 288 - service_level: 0.96 - vlt: 3 - SKU846: - cost: 485 - init_stock: 780 - price: 485 - service_level: 0.96 - vlt: 1 - SKU847: - cost: 388 - init_stock: 1820 - price: 388 - service_level: 0.96 - vlt: 1 - SKU848: - cost: 306 - init_stock: 920 - price: 306 - service_level: 0.96 - vlt: 2 - SKU849: - cost: 320 - init_stock: 800 - price: 320 - service_level: 0.96 - vlt: 2 - SKU85: - cost: 442 - init_stock: 2000 - price: 442 - service_level: 0.96 - vlt: 3 - SKU850: - cost: 183 - init_stock: 1140 - price: 183 - service_level: 0.96 - vlt: 2 - SKU851: - cost: 53 - init_stock: 1280 - price: 53 - service_level: 0.96 - vlt: 1 - SKU852: - cost: 306 - init_stock: 1080 - price: 306 - service_level: 0.96 - vlt: 2 - SKU853: - cost: 386 - init_stock: 1120 - price: 386 - service_level: 0.96 - vlt: 3 - SKU854: - cost: 212 - init_stock: 1300 - price: 212 - service_level: 0.96 - vlt: 3 - SKU855: - cost: 76 - init_stock: 1440 - price: 76 - service_level: 0.96 - vlt: 3 - SKU856: - cost: 380 - init_stock: 640 - price: 380 - service_level: 0.96 - vlt: 2 - SKU857: - cost: 462 - init_stock: 2000 - price: 462 - service_level: 0.96 - vlt: 1 - SKU858: - cost: 80 - init_stock: 480 - price: 80 - service_level: 0.96 - vlt: 3 - SKU859: - cost: 215 - init_stock: 560 - price: 215 - service_level: 0.96 - vlt: 3 - SKU86: - cost: 386 - init_stock: 1700 - price: 386 - service_level: 0.96 - vlt: 2 - SKU860: - cost: 313 - init_stock: 300 - price: 313 - service_level: 0.96 - vlt: 2 - SKU861: - cost: 477 - init_stock: 260 - price: 477 - service_level: 0.96 - vlt: 2 - SKU862: - cost: 240 - init_stock: 320 - price: 240 - service_level: 0.96 - vlt: 2 - SKU863: - cost: 470 - init_stock: 1860 - price: 470 - service_level: 0.96 - vlt: 2 - SKU864: - cost: 203 - init_stock: 380 - price: 203 - service_level: 0.96 - vlt: 1 - SKU865: - cost: 144 - init_stock: 380 - price: 144 - service_level: 0.96 - vlt: 3 - SKU866: - cost: 172 - init_stock: 1760 - price: 172 - service_level: 0.96 - vlt: 1 - SKU867: - cost: 499 - init_stock: 2000 - price: 499 - service_level: 0.96 - vlt: 2 - SKU868: - cost: 41 - init_stock: 1000 - price: 41 - service_level: 0.96 - vlt: 2 - SKU869: - cost: 97 - init_stock: 1940 - price: 97 - service_level: 0.96 - vlt: 2 - SKU87: - cost: 368 - init_stock: 1380 - price: 368 - service_level: 0.96 - vlt: 2 - SKU870: - cost: 281 - init_stock: 1340 - price: 281 - service_level: 0.96 - vlt: 1 - SKU871: - cost: 101 - init_stock: 1980 - price: 101 - service_level: 0.96 - vlt: 1 - SKU872: - cost: 133 - init_stock: 640 - price: 133 - service_level: 0.96 - vlt: 3 - SKU873: - cost: 423 - init_stock: 620 - price: 423 - service_level: 0.96 - vlt: 2 - SKU874: - cost: 469 - init_stock: 1200 - price: 469 - service_level: 0.96 - vlt: 1 - SKU875: - cost: 51 - init_stock: 460 - price: 51 - service_level: 0.96 - vlt: 2 - SKU876: - cost: 303 - init_stock: 1220 - price: 303 - service_level: 0.96 - vlt: 3 - SKU877: - cost: 40 - init_stock: 700 - price: 40 - service_level: 0.96 - vlt: 3 - SKU878: - cost: 27 - init_stock: 1360 - price: 27 - service_level: 0.96 - vlt: 3 - SKU879: - cost: 150 - init_stock: 2000 - price: 150 - service_level: 0.96 - vlt: 1 - SKU88: - cost: 471 - init_stock: 240 - price: 471 - service_level: 0.96 - vlt: 1 - SKU880: - cost: 433 - init_stock: 620 - price: 433 - service_level: 0.96 - vlt: 3 - SKU881: - cost: 431 - init_stock: 1400 - price: 431 - service_level: 0.96 - vlt: 3 - SKU882: - cost: 194 - init_stock: 320 - price: 194 - service_level: 0.96 - vlt: 1 - SKU883: - cost: 284 - init_stock: 760 - price: 284 - service_level: 0.96 - vlt: 1 - SKU884: - cost: 139 - init_stock: 780 - price: 139 - service_level: 0.96 - vlt: 2 - SKU885: - cost: 49 - init_stock: 540 - price: 49 - service_level: 0.96 - vlt: 3 - SKU886: - cost: 372 - init_stock: 1460 - price: 372 - service_level: 0.96 - vlt: 3 - SKU887: - cost: 266 - init_stock: 1740 - price: 266 - service_level: 0.96 - vlt: 1 - SKU888: - cost: 143 - init_stock: 180 - price: 143 - service_level: 0.96 - vlt: 2 - SKU889: - cost: 101 - init_stock: 420 - price: 101 - service_level: 0.96 - vlt: 2 - SKU89: - cost: 138 - init_stock: 1860 - price: 138 - service_level: 0.96 - vlt: 3 - SKU890: - cost: 161 - init_stock: 2000 - price: 161 - service_level: 0.96 - vlt: 3 - SKU891: - cost: 356 - init_stock: 440 - price: 356 - service_level: 0.96 - vlt: 3 - SKU892: - cost: 313 - init_stock: 1840 - price: 313 - service_level: 0.96 - vlt: 2 - SKU893: - cost: 229 - init_stock: 1980 - price: 229 - service_level: 0.96 - vlt: 1 - SKU894: - cost: 129 - init_stock: 1480 - price: 129 - service_level: 0.96 - vlt: 1 - SKU895: - cost: 230 - init_stock: 260 - price: 230 - service_level: 0.96 - vlt: 2 - SKU896: - cost: 289 - init_stock: 1600 - price: 289 - service_level: 0.96 - vlt: 3 - SKU897: - cost: 393 - init_stock: 1440 - price: 393 - service_level: 0.96 - vlt: 3 - SKU898: - cost: 477 - init_stock: 220 - price: 477 - service_level: 0.96 - vlt: 2 - SKU899: - cost: 233 - init_stock: 960 - price: 233 - service_level: 0.96 - vlt: 2 - SKU9: - cost: 166 - init_stock: 1920 - price: 166 - service_level: 0.96 - vlt: 2 - SKU90: - cost: 454 - init_stock: 1020 - price: 454 - service_level: 0.96 - vlt: 2 - SKU900: - cost: 158 - init_stock: 1100 - price: 158 - service_level: 0.96 - vlt: 1 - SKU901: - cost: 215 - init_stock: 580 - price: 215 - service_level: 0.96 - vlt: 3 - SKU902: - cost: 125 - init_stock: 1600 - price: 125 - service_level: 0.96 - vlt: 2 - SKU903: - cost: 357 - init_stock: 760 - price: 357 - service_level: 0.96 - vlt: 2 - SKU904: - cost: 496 - init_stock: 1020 - price: 496 - service_level: 0.96 - vlt: 1 - SKU905: - cost: 249 - init_stock: 620 - price: 249 - service_level: 0.96 - vlt: 3 - SKU906: - cost: 166 - init_stock: 1040 - price: 166 - service_level: 0.96 - vlt: 2 - SKU907: - cost: 22 - init_stock: 1660 - price: 22 - service_level: 0.96 - vlt: 3 - SKU908: - cost: 408 - init_stock: 1560 - price: 408 - service_level: 0.96 - vlt: 2 - SKU909: - cost: 482 - init_stock: 1920 - price: 482 - service_level: 0.96 - vlt: 1 - SKU91: - cost: 303 - init_stock: 320 - price: 303 - service_level: 0.96 - vlt: 2 - SKU910: - cost: 226 - init_stock: 660 - price: 226 - service_level: 0.96 - vlt: 2 - SKU911: - cost: 461 - init_stock: 1400 - price: 461 - service_level: 0.96 - vlt: 2 - SKU912: - cost: 236 - init_stock: 540 - price: 236 - service_level: 0.96 - vlt: 3 - SKU913: - cost: 322 - init_stock: 920 - price: 322 - service_level: 0.96 - vlt: 2 - SKU914: - cost: 272 - init_stock: 1240 - price: 272 - service_level: 0.96 - vlt: 3 - SKU915: - cost: 337 - init_stock: 1120 - price: 337 - service_level: 0.96 - vlt: 3 - SKU916: - cost: 337 - init_stock: 1780 - price: 337 - service_level: 0.96 - vlt: 3 - SKU917: - cost: 258 - init_stock: 600 - price: 258 - service_level: 0.96 - vlt: 3 - SKU918: - cost: 148 - init_stock: 1100 - price: 148 - service_level: 0.96 - vlt: 1 - SKU919: - cost: 393 - init_stock: 500 - price: 393 - service_level: 0.96 - vlt: 3 - SKU92: - cost: 262 - init_stock: 1260 - price: 262 - service_level: 0.96 - vlt: 2 - SKU920: - cost: 357 - init_stock: 1720 - price: 357 - service_level: 0.96 - vlt: 3 - SKU921: - cost: 227 - init_stock: 1320 - price: 227 - service_level: 0.96 - vlt: 2 - SKU922: - cost: 112 - init_stock: 1340 - price: 112 - service_level: 0.96 - vlt: 2 - SKU923: - cost: 496 - init_stock: 1160 - price: 496 - service_level: 0.96 - vlt: 2 - SKU924: - cost: 316 - init_stock: 1700 - price: 316 - service_level: 0.96 - vlt: 3 - SKU925: - cost: 360 - init_stock: 300 - price: 360 - service_level: 0.96 - vlt: 1 - SKU926: - cost: 360 - init_stock: 1340 - price: 360 - service_level: 0.96 - vlt: 2 - SKU927: - cost: 260 - init_stock: 420 - price: 260 - service_level: 0.96 - vlt: 3 - SKU928: - cost: 491 - init_stock: 1660 - price: 491 - service_level: 0.96 - vlt: 1 - SKU929: - cost: 359 - init_stock: 2000 - price: 359 - service_level: 0.96 - vlt: 3 - SKU93: - cost: 404 - init_stock: 1340 - price: 404 - service_level: 0.96 - vlt: 2 - SKU930: - cost: 198 - init_stock: 560 - price: 198 - service_level: 0.96 - vlt: 2 - SKU931: - cost: 71 - init_stock: 280 - price: 71 - service_level: 0.96 - vlt: 3 - SKU932: - cost: 163 - init_stock: 1320 - price: 163 - service_level: 0.96 - vlt: 2 - SKU933: - cost: 113 - init_stock: 1560 - price: 113 - service_level: 0.96 - vlt: 1 - SKU934: - cost: 219 - init_stock: 1340 - price: 219 - service_level: 0.96 - vlt: 3 - SKU935: - cost: 364 - init_stock: 1880 - price: 364 - service_level: 0.96 - vlt: 1 - SKU936: - cost: 24 - init_stock: 100 - price: 24 - service_level: 0.96 - vlt: 3 - SKU937: - cost: 135 - init_stock: 340 - price: 135 - service_level: 0.96 - vlt: 1 - SKU938: - cost: 432 - init_stock: 420 - price: 432 - service_level: 0.96 - vlt: 2 - SKU939: - cost: 173 - init_stock: 1180 - price: 173 - service_level: 0.96 - vlt: 3 - SKU94: - cost: 184 - init_stock: 940 - price: 184 - service_level: 0.96 - vlt: 3 - SKU940: - cost: 14 - init_stock: 1860 - price: 14 - service_level: 0.96 - vlt: 1 - SKU941: - cost: 80 - init_stock: 1140 - price: 80 - service_level: 0.96 - vlt: 3 - SKU942: - cost: 202 - init_stock: 260 - price: 202 - service_level: 0.96 - vlt: 3 - SKU943: - cost: 138 - init_stock: 980 - price: 138 - service_level: 0.96 - vlt: 1 - SKU944: - cost: 196 - init_stock: 880 - price: 196 - service_level: 0.96 - vlt: 1 - SKU945: - cost: 141 - init_stock: 340 - price: 141 - service_level: 0.96 - vlt: 2 - SKU946: - cost: 325 - init_stock: 980 - price: 325 - service_level: 0.96 - vlt: 1 - SKU947: - cost: 338 - init_stock: 1820 - price: 338 - service_level: 0.96 - vlt: 3 - SKU948: - cost: 425 - init_stock: 560 - price: 425 - service_level: 0.96 - vlt: 3 - SKU949: - cost: 309 - init_stock: 100 - price: 309 - service_level: 0.96 - vlt: 2 - SKU95: - cost: 136 - init_stock: 420 - price: 136 - service_level: 0.96 - vlt: 3 - SKU950: - cost: 102 - init_stock: 1080 - price: 102 - service_level: 0.96 - vlt: 2 - SKU951: - cost: 75 - init_stock: 360 - price: 75 - service_level: 0.96 - vlt: 2 - SKU952: - cost: 156 - init_stock: 220 - price: 156 - service_level: 0.96 - vlt: 3 - SKU953: - cost: 138 - init_stock: 700 - price: 138 - service_level: 0.96 - vlt: 3 - SKU954: - cost: 296 - init_stock: 1720 - price: 296 - service_level: 0.96 - vlt: 1 - SKU955: - cost: 55 - init_stock: 1260 - price: 55 - service_level: 0.96 - vlt: 1 - SKU956: - cost: 282 - init_stock: 1700 - price: 282 - service_level: 0.96 - vlt: 1 - SKU957: - cost: 305 - init_stock: 1020 - price: 305 - service_level: 0.96 - vlt: 2 - SKU958: - cost: 369 - init_stock: 1320 - price: 369 - service_level: 0.96 - vlt: 2 - SKU959: - cost: 81 - init_stock: 1640 - price: 81 - service_level: 0.96 - vlt: 1 - SKU96: - cost: 176 - init_stock: 880 - price: 176 - service_level: 0.96 - vlt: 3 - SKU960: - cost: 147 - init_stock: 120 - price: 147 - service_level: 0.96 - vlt: 3 - SKU961: - cost: 264 - init_stock: 880 - price: 264 - service_level: 0.96 - vlt: 1 - SKU962: - cost: 354 - init_stock: 880 - price: 354 - service_level: 0.96 - vlt: 1 - SKU963: - cost: 349 - init_stock: 420 - price: 349 - service_level: 0.96 - vlt: 1 - SKU964: - cost: 244 - init_stock: 1460 - price: 244 - service_level: 0.96 - vlt: 1 - SKU965: - cost: 124 - init_stock: 1020 - price: 124 - service_level: 0.96 - vlt: 1 - SKU966: - cost: 302 - init_stock: 880 - price: 302 - service_level: 0.96 - vlt: 3 - SKU967: - cost: 67 - init_stock: 900 - price: 67 - service_level: 0.96 - vlt: 3 - SKU968: - cost: 281 - init_stock: 980 - price: 281 - service_level: 0.96 - vlt: 2 - SKU969: - cost: 249 - init_stock: 120 - price: 249 - service_level: 0.96 - vlt: 1 - SKU97: - cost: 28 - init_stock: 600 - price: 28 - service_level: 0.96 - vlt: 3 - SKU970: - cost: 244 - init_stock: 100 - price: 244 - service_level: 0.96 - vlt: 2 - SKU971: - cost: 368 - init_stock: 1460 - price: 368 - service_level: 0.96 - vlt: 1 - SKU972: - cost: 209 - init_stock: 1380 - price: 209 - service_level: 0.96 - vlt: 1 - SKU973: - cost: 271 - init_stock: 220 - price: 271 - service_level: 0.96 - vlt: 2 - SKU974: - cost: 170 - init_stock: 640 - price: 170 - service_level: 0.96 - vlt: 1 - SKU975: - cost: 198 - init_stock: 440 - price: 198 - service_level: 0.96 - vlt: 3 - SKU976: - cost: 178 - init_stock: 300 - price: 178 - service_level: 0.96 - vlt: 2 - SKU977: - cost: 234 - init_stock: 1080 - price: 234 - service_level: 0.96 - vlt: 3 - SKU978: - cost: 470 - init_stock: 1280 - price: 470 - service_level: 0.96 - vlt: 3 - SKU979: - cost: 443 - init_stock: 1920 - price: 443 - service_level: 0.96 - vlt: 2 - SKU98: - cost: 443 - init_stock: 700 - price: 443 - service_level: 0.96 - vlt: 1 - SKU980: - cost: 39 - init_stock: 1360 - price: 39 - service_level: 0.96 - vlt: 3 - SKU981: - cost: 482 - init_stock: 1440 - price: 482 - service_level: 0.96 - vlt: 2 - SKU982: - cost: 213 - init_stock: 1040 - price: 213 - service_level: 0.96 - vlt: 3 - SKU983: - cost: 449 - init_stock: 1840 - price: 449 - service_level: 0.96 - vlt: 1 - SKU984: - cost: 232 - init_stock: 1820 - price: 232 - service_level: 0.96 - vlt: 3 - SKU985: - cost: 290 - init_stock: 1020 - price: 290 - service_level: 0.96 - vlt: 1 - SKU986: - cost: 275 - init_stock: 340 - price: 275 - service_level: 0.96 - vlt: 3 - SKU987: - cost: 434 - init_stock: 680 - price: 434 - service_level: 0.96 - vlt: 1 - SKU988: - cost: 102 - init_stock: 460 - price: 102 - service_level: 0.96 - vlt: 1 - SKU989: - cost: 484 - init_stock: 1860 - price: 484 - service_level: 0.96 - vlt: 1 - SKU99: - cost: 361 - init_stock: 900 - price: 361 - service_level: 0.96 - vlt: 3 - SKU990: - cost: 108 - init_stock: 1140 - price: 108 - service_level: 0.96 - vlt: 3 - SKU991: - cost: 409 - init_stock: 380 - price: 409 - service_level: 0.96 - vlt: 3 - SKU992: - cost: 434 - init_stock: 1860 - price: 434 - service_level: 0.96 - vlt: 3 - SKU993: - cost: 145 - init_stock: 1720 - price: 145 - service_level: 0.96 - vlt: 2 - SKU994: - cost: 470 - init_stock: 1000 - price: 470 - service_level: 0.96 - vlt: 3 - SKU995: - cost: 241 - init_stock: 1520 - price: 241 - service_level: 0.96 - vlt: 2 - SKU996: - cost: 260 - init_stock: 1460 - price: 260 - service_level: 0.96 - vlt: 3 - SKU997: - cost: 400 - init_stock: 680 - price: 400 - service_level: 0.96 - vlt: 1 - SKU998: - cost: 447 - init_stock: 1100 - price: 447 - service_level: 0.96 - vlt: 2 - SKU999: - cost: 79 - init_stock: 460 - price: 79 - service_level: 0.96 - vlt: 2 - - children: - storage: - config: - capacity: 1064900 - unit_storage_cost: 1 - config: - order_cost: 500 - definition_ref: RetailerFacility - name: STORE0 - skus: - SKU0: - constraint: null - cost: 322 - init_stock: 126 - max_stock: 1000 - price: 631 - sale_gamma: 63 - service_level: 0.95 - SKU1: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 284 - init_stock: 176 - max_stock: 1000 - price: 448 - sale_gamma: 22 - service_level: 0.95 - SKU10: - constraint: null - cost: 68 - init_stock: 150 - max_stock: 1000 - price: 116 - sale_gamma: 25 - service_level: 0.95 - SKU100: - constraint: G(low_profit -> low_stock_constraint) - cost: 16 - init_stock: 390 - max_stock: 1000 - price: 23 - sale_gamma: 65 - service_level: 0.95 - SKU101: - constraint: null - cost: 84 - init_stock: 497 - max_stock: 1000 - price: 149 - sale_gamma: 71 - service_level: 0.95 - SKU102: - constraint: null - cost: 328 - init_stock: 558 - max_stock: 1000 - price: 505 - sale_gamma: 93 - service_level: 0.95 - SKU103: - constraint: null - cost: 334 - init_stock: 285 - max_stock: 1000 - price: 601 - sale_gamma: 95 - service_level: 0.95 - SKU104: - constraint: null - cost: 49 - init_stock: 325 - max_stock: 1000 - price: 57 - sale_gamma: 65 - service_level: 0.95 - SKU105: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 110 - init_stock: 285 - max_stock: 1000 - price: 171 - sale_gamma: 57 - service_level: 0.95 - SKU106: - constraint: G(low_profit -> low_stock_constraint) - cost: 251 - init_stock: 584 - max_stock: 1000 - price: 454 - sale_gamma: 73 - service_level: 0.95 - SKU107: - constraint: G(stock_constraint) - cost: 423 - init_stock: 348 - max_stock: 1000 - price: 706 - sale_gamma: 87 - service_level: 0.95 - SKU108: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 458 - init_stock: 368 - max_stock: 1000 - price: 801 - sale_gamma: 92 - service_level: 0.95 - SKU109: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 88 - init_stock: 328 - max_stock: 1000 - price: 98 - sale_gamma: 82 - service_level: 0.95 - SKU11: - constraint: null - cost: 400 - init_stock: 306 - max_stock: 1000 - price: 680 - sale_gamma: 51 - service_level: 0.95 - SKU110: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 66 - init_stock: 112 - max_stock: 1000 - price: 100 - sale_gamma: 14 - service_level: 0.95 - SKU111: - constraint: G(stock_constraint) - cost: 260 - init_stock: 183 - max_stock: 1000 - price: 364 - sale_gamma: 61 - service_level: 0.95 - SKU112: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 61 - init_stock: 475 - max_stock: 1000 - price: 119 - sale_gamma: 95 - service_level: 0.95 - SKU113: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 155 - max_stock: 1000 - price: 678 - sale_gamma: 31 - service_level: 0.95 - SKU114: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 389 - init_stock: 162 - max_stock: 1000 - price: 564 - sale_gamma: 27 - service_level: 0.95 - SKU115: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 258 - max_stock: 1000 - price: 557 - sale_gamma: 86 - service_level: 0.95 - SKU116: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 288 - max_stock: 1000 - price: 768 - sale_gamma: 72 - service_level: 0.95 - SKU117: - constraint: null - cost: 320 - init_stock: 644 - max_stock: 1000 - price: 374 - sale_gamma: 92 - service_level: 0.95 - SKU118: - constraint: null - cost: 183 - init_stock: 66 - max_stock: 1000 - price: 208 - sale_gamma: 33 - service_level: 0.95 - SKU119: - constraint: null - cost: 209 - init_stock: 256 - max_stock: 1000 - price: 317 - sale_gamma: 32 - service_level: 0.95 - SKU12: - constraint: null - cost: 112 - init_stock: 336 - max_stock: 1000 - price: 210 - sale_gamma: 84 - service_level: 0.95 - SKU120: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 121 - init_stock: 392 - max_stock: 1000 - price: 151 - sale_gamma: 98 - service_level: 0.95 - SKU121: - constraint: null - cost: 40 - init_stock: 510 - max_stock: 1000 - price: 54 - sale_gamma: 85 - service_level: 0.95 - SKU122: - constraint: G(stock_constraint) - cost: 437 - init_stock: 35 - max_stock: 1000 - price: 589 - sale_gamma: 7 - service_level: 0.95 - SKU123: - constraint: G(stock_constraint) - cost: 233 - init_stock: 114 - max_stock: 1000 - price: 326 - sale_gamma: 19 - service_level: 0.95 - SKU124: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 108 - max_stock: 1000 - price: 298 - sale_gamma: 36 - service_level: 0.95 - SKU125: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 368 - max_stock: 1000 - price: 32 - sale_gamma: 92 - service_level: 0.95 - SKU126: - constraint: null - cost: 36 - init_stock: 156 - max_stock: 1000 - price: 40 - sale_gamma: 39 - service_level: 0.95 - SKU127: - constraint: G(stock_constraint) - cost: 217 - init_stock: 155 - max_stock: 1000 - price: 366 - sale_gamma: 31 - service_level: 0.95 - SKU128: - constraint: null - cost: 165 - init_stock: 95 - max_stock: 1000 - price: 283 - sale_gamma: 19 - service_level: 0.95 - SKU129: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 203 - sale_gamma: 50 - service_level: 0.95 - SKU13: - constraint: null - cost: 317 - init_stock: 456 - max_stock: 1000 - price: 573 - sale_gamma: 57 - service_level: 0.95 - SKU130: - constraint: null - cost: 348 - init_stock: 784 - max_stock: 1000 - price: 396 - sale_gamma: 98 - service_level: 0.95 - SKU131: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 64 - init_stock: 270 - max_stock: 1000 - price: 110 - sale_gamma: 45 - service_level: 0.95 - SKU132: - constraint: null - cost: 427 - init_stock: 105 - max_stock: 1000 - price: 678 - sale_gamma: 21 - service_level: 0.95 - SKU133: - constraint: null - cost: 224 - init_stock: 145 - max_stock: 1000 - price: 448 - sale_gamma: 29 - service_level: 0.95 - SKU134: - constraint: G(stock_constraint) - cost: 336 - init_stock: 539 - max_stock: 1000 - price: 470 - sale_gamma: 77 - service_level: 0.95 - SKU135: - constraint: null - cost: 153 - init_stock: 500 - max_stock: 1000 - price: 243 - sale_gamma: 100 - service_level: 0.95 - SKU136: - constraint: G(low_profit -> low_stock_constraint) - cost: 199 - init_stock: 497 - max_stock: 1000 - price: 370 - sale_gamma: 71 - service_level: 0.95 - SKU137: - constraint: G(stock_constraint) - cost: 93 - init_stock: 444 - max_stock: 1000 - price: 165 - sale_gamma: 74 - service_level: 0.95 - SKU138: - constraint: G(stock_constraint) - cost: 228 - init_stock: 180 - max_stock: 1000 - price: 253 - sale_gamma: 36 - service_level: 0.95 - SKU139: - constraint: G(stock_constraint) - cost: 207 - init_stock: 144 - max_stock: 1000 - price: 368 - sale_gamma: 24 - service_level: 0.95 - SKU14: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 268 - init_stock: 372 - max_stock: 1000 - price: 305 - sale_gamma: 62 - service_level: 0.95 - SKU140: - constraint: null - cost: 261 - init_stock: 238 - max_stock: 1000 - price: 357 - sale_gamma: 34 - service_level: 0.95 - SKU141: - constraint: null - cost: 190 - init_stock: 164 - max_stock: 1000 - price: 370 - sale_gamma: 41 - service_level: 0.95 - SKU142: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 320 - init_stock: 152 - max_stock: 1000 - price: 428 - sale_gamma: 38 - service_level: 0.95 - SKU143: - constraint: G(stock_constraint) - cost: 318 - init_stock: 182 - max_stock: 1000 - price: 566 - sale_gamma: 26 - service_level: 0.95 - SKU144: - constraint: null - cost: 400 - init_stock: 24 - max_stock: 1000 - price: 716 - sale_gamma: 12 - service_level: 0.95 - SKU145: - constraint: null - cost: 399 - init_stock: 328 - max_stock: 1000 - price: 774 - sale_gamma: 82 - service_level: 0.95 - SKU146: - constraint: null - cost: 177 - init_stock: 192 - max_stock: 1000 - price: 194 - sale_gamma: 48 - service_level: 0.95 - SKU147: - constraint: G(low_profit -> low_stock_constraint) - cost: 472 - init_stock: 392 - max_stock: 1000 - price: 840 - sale_gamma: 56 - service_level: 0.95 - SKU148: - constraint: G(stock_constraint) - cost: 313 - init_stock: 308 - max_stock: 1000 - price: 566 - sale_gamma: 77 - service_level: 0.95 - SKU149: - constraint: G(low_profit -> low_stock_constraint) - cost: 357 - init_stock: 539 - max_stock: 1000 - price: 549 - sale_gamma: 77 - service_level: 0.95 - SKU15: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 25 - max_stock: 1000 - price: 58 - sale_gamma: 5 - service_level: 0.95 - SKU150: - constraint: null - cost: 106 - init_stock: 210 - max_stock: 1000 - price: 159 - sale_gamma: 70 - service_level: 0.95 - SKU151: - constraint: G(low_profit -> low_stock_constraint) - cost: 223 - init_stock: 146 - max_stock: 1000 - price: 370 - sale_gamma: 73 - service_level: 0.95 - SKU152: - constraint: null - cost: 10 - init_stock: 408 - max_stock: 1000 - price: 14 - sale_gamma: 51 - service_level: 0.95 - SKU153: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 441 - init_stock: 372 - max_stock: 1000 - price: 538 - sale_gamma: 62 - service_level: 0.95 - SKU154: - constraint: null - cost: 77 - init_stock: 680 - max_stock: 1000 - price: 135 - sale_gamma: 85 - service_level: 0.95 - SKU155: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 159 - max_stock: 1000 - price: 641 - sale_gamma: 53 - service_level: 0.95 - SKU156: - constraint: null - cost: 10 - init_stock: 36 - max_stock: 1000 - price: 16 - sale_gamma: 12 - service_level: 0.95 - SKU157: - constraint: null - cost: 410 - init_stock: 525 - max_stock: 1000 - price: 594 - sale_gamma: 75 - service_level: 0.95 - SKU158: - constraint: null - cost: 145 - init_stock: 486 - max_stock: 1000 - price: 275 - sale_gamma: 81 - service_level: 0.95 - SKU159: - constraint: G(stock_constraint) - cost: 193 - init_stock: 100 - max_stock: 1000 - price: 368 - sale_gamma: 25 - service_level: 0.95 - SKU16: - constraint: null - cost: 175 - init_stock: 464 - max_stock: 1000 - price: 344 - sale_gamma: 58 - service_level: 0.95 - SKU160: - constraint: G(low_profit -> low_stock_constraint) - cost: 459 - init_stock: 510 - max_stock: 1000 - price: 876 - sale_gamma: 85 - service_level: 0.95 - SKU161: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 239 - init_stock: 230 - max_stock: 1000 - price: 478 - sale_gamma: 46 - service_level: 0.95 - SKU162: - constraint: null - cost: 158 - init_stock: 30 - max_stock: 1000 - price: 290 - sale_gamma: 5 - service_level: 0.95 - SKU163: - constraint: G(stock_constraint) - cost: 486 - init_stock: 273 - max_stock: 1000 - price: 704 - sale_gamma: 39 - service_level: 0.95 - SKU164: - constraint: null - cost: 496 - init_stock: 500 - max_stock: 1000 - price: 615 - sale_gamma: 100 - service_level: 0.95 - SKU165: - constraint: G(stock_constraint) - cost: 274 - init_stock: 231 - max_stock: 1000 - price: 548 - sale_gamma: 33 - service_level: 0.95 - SKU166: - constraint: null - cost: 79 - init_stock: 178 - max_stock: 1000 - price: 137 - sale_gamma: 89 - service_level: 0.95 - SKU167: - constraint: G(stock_constraint) - cost: 443 - init_stock: 52 - max_stock: 1000 - price: 854 - sale_gamma: 13 - service_level: 0.95 - SKU168: - constraint: null - cost: 357 - init_stock: 522 - max_stock: 1000 - price: 546 - sale_gamma: 87 - service_level: 0.95 - SKU169: - constraint: null - cost: 369 - init_stock: 686 - max_stock: 1000 - price: 505 - sale_gamma: 98 - service_level: 0.95 - SKU17: - constraint: null - cost: 346 - init_stock: 18 - max_stock: 1000 - price: 553 - sale_gamma: 9 - service_level: 0.95 - SKU170: - constraint: null - cost: 68 - init_stock: 275 - max_stock: 1000 - price: 113 - sale_gamma: 55 - service_level: 0.95 - SKU171: - constraint: G(low_profit -> low_stock_constraint) - cost: 398 - init_stock: 380 - max_stock: 1000 - price: 704 - sale_gamma: 76 - service_level: 0.95 - SKU172: - constraint: null - cost: 200 - init_stock: 426 - max_stock: 1000 - price: 222 - sale_gamma: 71 - service_level: 0.95 - SKU173: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 480 - max_stock: 1000 - price: 264 - sale_gamma: 96 - service_level: 0.95 - SKU174: - constraint: null - cost: 291 - init_stock: 380 - max_stock: 1000 - price: 582 - sale_gamma: 76 - service_level: 0.95 - SKU175: - constraint: null - cost: 84 - init_stock: 225 - max_stock: 1000 - price: 140 - sale_gamma: 75 - service_level: 0.95 - SKU176: - constraint: G(stock_constraint) - cost: 407 - init_stock: 330 - max_stock: 1000 - price: 610 - sale_gamma: 66 - service_level: 0.95 - SKU177: - constraint: null - cost: 257 - init_stock: 155 - max_stock: 1000 - price: 346 - sale_gamma: 31 - service_level: 0.95 - SKU178: - constraint: null - cost: 423 - init_stock: 20 - max_stock: 1000 - price: 499 - sale_gamma: 5 - service_level: 0.95 - SKU179: - constraint: null - cost: 497 - init_stock: 415 - max_stock: 1000 - price: 690 - sale_gamma: 83 - service_level: 0.95 - SKU18: - constraint: null - cost: 258 - init_stock: 405 - max_stock: 1000 - price: 387 - sale_gamma: 81 - service_level: 0.95 - SKU180: - constraint: null - cost: 217 - init_stock: 165 - max_stock: 1000 - price: 297 - sale_gamma: 55 - service_level: 0.95 - SKU181: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 300 - max_stock: 1000 - price: 214 - sale_gamma: 60 - service_level: 0.95 - SKU182: - constraint: null - cost: 437 - init_stock: 693 - max_stock: 1000 - price: 764 - sale_gamma: 99 - service_level: 0.95 - SKU183: - constraint: null - cost: 145 - init_stock: 56 - max_stock: 1000 - price: 178 - sale_gamma: 8 - service_level: 0.95 - SKU184: - constraint: null - cost: 73 - init_stock: 72 - max_stock: 1000 - price: 100 - sale_gamma: 24 - service_level: 0.95 - SKU185: - constraint: null - cost: 10 - init_stock: 360 - max_stock: 1000 - price: 14 - sale_gamma: 90 - service_level: 0.95 - SKU186: - constraint: null - cost: 359 - init_stock: 66 - max_stock: 1000 - price: 649 - sale_gamma: 22 - service_level: 0.95 - SKU187: - constraint: G(low_profit -> low_stock_constraint) - cost: 177 - init_stock: 120 - max_stock: 1000 - price: 249 - sale_gamma: 30 - service_level: 0.95 - SKU188: - constraint: null - cost: 391 - init_stock: 348 - max_stock: 1000 - price: 586 - sale_gamma: 87 - service_level: 0.95 - SKU189: - constraint: G(low_profit -> low_stock_constraint) - cost: 358 - init_stock: 210 - max_stock: 1000 - price: 400 - sale_gamma: 35 - service_level: 0.95 - SKU19: - constraint: G(stock_constraint) - cost: 477 - init_stock: 364 - max_stock: 1000 - price: 791 - sale_gamma: 91 - service_level: 0.95 - SKU190: - constraint: null - cost: 113 - init_stock: 102 - max_stock: 1000 - price: 153 - sale_gamma: 17 - service_level: 0.95 - SKU191: - constraint: G(stock_constraint) - cost: 473 - init_stock: 324 - max_stock: 1000 - price: 638 - sale_gamma: 54 - service_level: 0.95 - SKU192: - constraint: null - cost: 415 - init_stock: 244 - max_stock: 1000 - price: 539 - sale_gamma: 61 - service_level: 0.95 - SKU193: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 207 - init_stock: 120 - max_stock: 1000 - price: 353 - sale_gamma: 30 - service_level: 0.95 - SKU194: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 30 - max_stock: 1000 - price: 803 - sale_gamma: 5 - service_level: 0.95 - SKU195: - constraint: G(low_profit -> low_stock_constraint) - cost: 218 - init_stock: 93 - max_stock: 1000 - price: 248 - sale_gamma: 31 - service_level: 0.95 - SKU196: - constraint: null - cost: 49 - init_stock: 544 - max_stock: 1000 - price: 62 - sale_gamma: 68 - service_level: 0.95 - SKU197: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 285 - max_stock: 1000 - price: 509 - sale_gamma: 57 - service_level: 0.95 - SKU198: - constraint: G(low_profit -> low_stock_constraint) - cost: 169 - init_stock: 378 - max_stock: 1000 - price: 307 - sale_gamma: 54 - service_level: 0.95 - SKU199: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 138 - max_stock: 1000 - price: 794 - sale_gamma: 23 - service_level: 0.95 - SKU2: - constraint: null - cost: 331 - init_stock: 350 - max_stock: 1000 - price: 499 - sale_gamma: 70 - service_level: 0.95 - SKU20: - constraint: G(stock_constraint) - cost: 335 - init_stock: 200 - max_stock: 1000 - price: 649 - sale_gamma: 25 - service_level: 0.95 - SKU200: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 65 - init_stock: 150 - max_stock: 1000 - price: 85 - sale_gamma: 25 - service_level: 0.95 - SKU201: - constraint: G(stock_constraint) - cost: 104 - init_stock: 354 - max_stock: 1000 - price: 161 - sale_gamma: 59 - service_level: 0.95 - SKU202: - constraint: null - cost: 142 - init_stock: 365 - max_stock: 1000 - price: 222 - sale_gamma: 73 - service_level: 0.95 - SKU203: - constraint: null - cost: 440 - init_stock: 328 - max_stock: 1000 - price: 677 - sale_gamma: 82 - service_level: 0.95 - SKU204: - constraint: null - cost: 489 - init_stock: 188 - max_stock: 1000 - price: 831 - sale_gamma: 47 - service_level: 0.95 - SKU205: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 130 - init_stock: 500 - max_stock: 1000 - price: 175 - sale_gamma: 100 - service_level: 0.95 - SKU206: - constraint: null - cost: 335 - init_stock: 55 - max_stock: 1000 - price: 552 - sale_gamma: 11 - service_level: 0.95 - SKU207: - constraint: G(low_profit -> low_stock_constraint) - cost: 140 - init_stock: 240 - max_stock: 1000 - price: 170 - sale_gamma: 80 - service_level: 0.95 - SKU208: - constraint: null - cost: 491 - init_stock: 308 - max_stock: 1000 - price: 927 - sale_gamma: 77 - service_level: 0.95 - SKU209: - constraint: G(stock_constraint) - cost: 179 - init_stock: 120 - max_stock: 1000 - price: 322 - sale_gamma: 20 - service_level: 0.95 - SKU21: - constraint: null - cost: 123 - init_stock: 400 - max_stock: 1000 - price: 225 - sale_gamma: 100 - service_level: 0.95 - SKU210: - constraint: null - cost: 404 - init_stock: 345 - max_stock: 1000 - price: 468 - sale_gamma: 69 - service_level: 0.95 - SKU211: - constraint: null - cost: 174 - init_stock: 364 - max_stock: 1000 - price: 226 - sale_gamma: 91 - service_level: 0.95 - SKU212: - constraint: null - cost: 405 - init_stock: 632 - max_stock: 1000 - price: 534 - sale_gamma: 79 - service_level: 0.95 - SKU213: - constraint: null - cost: 121 - init_stock: 192 - max_stock: 1000 - price: 229 - sale_gamma: 64 - service_level: 0.95 - SKU214: - constraint: G(stock_constraint) - cost: 101 - init_stock: 60 - max_stock: 1000 - price: 144 - sale_gamma: 10 - service_level: 0.95 - SKU215: - constraint: null - cost: 419 - init_stock: 94 - max_stock: 1000 - price: 469 - sale_gamma: 47 - service_level: 0.95 - SKU216: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 330 - init_stock: 92 - max_stock: 1000 - price: 432 - sale_gamma: 23 - service_level: 0.95 - SKU217: - constraint: null - cost: 284 - init_stock: 455 - max_stock: 1000 - price: 312 - sale_gamma: 65 - service_level: 0.95 - SKU218: - constraint: G(low_profit -> low_stock_constraint) - cost: 205 - init_stock: 354 - max_stock: 1000 - price: 397 - sale_gamma: 59 - service_level: 0.95 - SKU219: - constraint: G(stock_constraint) - cost: 92 - init_stock: 368 - max_stock: 1000 - price: 135 - sale_gamma: 46 - service_level: 0.95 - SKU22: - constraint: null - cost: 493 - init_stock: 264 - max_stock: 1000 - price: 986 - sale_gamma: 66 - service_level: 0.95 - SKU220: - constraint: null - cost: 387 - init_stock: 435 - max_stock: 1000 - price: 572 - sale_gamma: 87 - service_level: 0.95 - SKU221: - constraint: null - cost: 39 - init_stock: 468 - max_stock: 1000 - price: 48 - sale_gamma: 78 - service_level: 0.95 - SKU222: - constraint: G(low_profit -> low_stock_constraint) - cost: 115 - init_stock: 180 - max_stock: 1000 - price: 210 - sale_gamma: 36 - service_level: 0.95 - SKU223: - constraint: G(low_profit -> low_stock_constraint) - cost: 196 - init_stock: 36 - max_stock: 1000 - price: 286 - sale_gamma: 12 - service_level: 0.95 - SKU224: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 387 - init_stock: 25 - max_stock: 1000 - price: 661 - sale_gamma: 5 - service_level: 0.95 - SKU225: - constraint: null - cost: 164 - init_stock: 116 - max_stock: 1000 - price: 216 - sale_gamma: 29 - service_level: 0.95 - SKU226: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 206 - init_stock: 65 - max_stock: 1000 - price: 350 - sale_gamma: 13 - service_level: 0.95 - SKU227: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 144 - max_stock: 1000 - price: 895 - sale_gamma: 24 - service_level: 0.95 - SKU228: - constraint: null - cost: 14 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 90 - service_level: 0.95 - SKU229: - constraint: null - cost: 472 - init_stock: 176 - max_stock: 1000 - price: 708 - sale_gamma: 44 - service_level: 0.95 - SKU23: - constraint: null - cost: 387 - init_stock: 210 - max_stock: 1000 - price: 700 - sale_gamma: 42 - service_level: 0.95 - SKU230: - constraint: null - cost: 241 - init_stock: 138 - max_stock: 1000 - price: 436 - sale_gamma: 23 - service_level: 0.95 - SKU231: - constraint: G(stock_constraint) - cost: 48 - init_stock: 486 - max_stock: 1000 - price: 96 - sale_gamma: 81 - service_level: 0.95 - SKU232: - constraint: G(low_profit -> low_stock_constraint) - cost: 224 - init_stock: 480 - max_stock: 1000 - price: 338 - sale_gamma: 96 - service_level: 0.95 - SKU233: - constraint: G(low_profit -> low_stock_constraint) - cost: 360 - init_stock: 300 - max_stock: 1000 - price: 428 - sale_gamma: 75 - service_level: 0.95 - SKU234: - constraint: null - cost: 287 - init_stock: 25 - max_stock: 1000 - price: 387 - sale_gamma: 5 - service_level: 0.95 - SKU235: - constraint: G(low_profit -> low_stock_constraint) - cost: 24 - init_stock: 228 - max_stock: 1000 - price: 32 - sale_gamma: 57 - service_level: 0.95 - SKU236: - constraint: G(low_profit -> low_stock_constraint) - cost: 155 - init_stock: 165 - max_stock: 1000 - price: 289 - sale_gamma: 55 - service_level: 0.95 - SKU237: - constraint: null - cost: 433 - init_stock: 270 - max_stock: 1000 - price: 779 - sale_gamma: 45 - service_level: 0.95 - SKU238: - constraint: G(stock_constraint) - cost: 64 - init_stock: 264 - max_stock: 1000 - price: 112 - sale_gamma: 66 - service_level: 0.95 - SKU239: - constraint: null - cost: 103 - init_stock: 490 - max_stock: 1000 - price: 139 - sale_gamma: 98 - service_level: 0.95 - SKU24: - constraint: null - cost: 97 - init_stock: 329 - max_stock: 1000 - price: 114 - sale_gamma: 47 - service_level: 0.95 - SKU240: - constraint: null - cost: 373 - init_stock: 188 - max_stock: 1000 - price: 738 - sale_gamma: 47 - service_level: 0.95 - SKU241: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 213 - max_stock: 1000 - price: 860 - sale_gamma: 71 - service_level: 0.95 - SKU242: - constraint: null - cost: 17 - init_stock: 88 - max_stock: 1000 - price: 29 - sale_gamma: 44 - service_level: 0.95 - SKU243: - constraint: null - cost: 352 - init_stock: 84 - max_stock: 1000 - price: 394 - sale_gamma: 14 - service_level: 0.95 - SKU244: - constraint: null - cost: 174 - init_stock: 410 - max_stock: 1000 - price: 226 - sale_gamma: 82 - service_level: 0.95 - SKU245: - constraint: G(low_profit -> low_stock_constraint) - cost: 404 - init_stock: 198 - max_stock: 1000 - price: 525 - sale_gamma: 66 - service_level: 0.95 - SKU246: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 300 - init_stock: 210 - max_stock: 1000 - price: 474 - sale_gamma: 30 - service_level: 0.95 - SKU247: - constraint: null - cost: 386 - init_stock: 210 - max_stock: 1000 - price: 497 - sale_gamma: 35 - service_level: 0.95 - SKU248: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 355 - init_stock: 174 - max_stock: 1000 - price: 539 - sale_gamma: 29 - service_level: 0.95 - SKU249: - constraint: null - cost: 356 - init_stock: 100 - max_stock: 1000 - price: 544 - sale_gamma: 25 - service_level: 0.95 - SKU25: - constraint: G(stock_constraint) - cost: 161 - init_stock: 35 - max_stock: 1000 - price: 249 - sale_gamma: 5 - service_level: 0.95 - SKU250: - constraint: null - cost: 127 - init_stock: 324 - max_stock: 1000 - price: 186 - sale_gamma: 54 - service_level: 0.95 - SKU251: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 344 - init_stock: 244 - max_stock: 1000 - price: 543 - sale_gamma: 61 - service_level: 0.95 - SKU252: - constraint: null - cost: 181 - init_stock: 415 - max_stock: 1000 - price: 334 - sale_gamma: 83 - service_level: 0.95 - SKU253: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 448 - init_stock: 32 - max_stock: 1000 - price: 864 - sale_gamma: 16 - service_level: 0.95 - SKU254: - constraint: null - cost: 484 - init_stock: 184 - max_stock: 1000 - price: 696 - sale_gamma: 46 - service_level: 0.95 - SKU255: - constraint: null - cost: 290 - init_stock: 335 - max_stock: 1000 - price: 568 - sale_gamma: 67 - service_level: 0.95 - SKU256: - constraint: null - cost: 91 - init_stock: 360 - max_stock: 1000 - price: 167 - sale_gamma: 72 - service_level: 0.95 - SKU257: - constraint: null - cost: 348 - init_stock: 171 - max_stock: 1000 - price: 497 - sale_gamma: 57 - service_level: 0.95 - SKU258: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 258 - max_stock: 1000 - price: 689 - sale_gamma: 43 - service_level: 0.95 - SKU259: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 207 - max_stock: 1000 - price: 559 - sale_gamma: 69 - service_level: 0.95 - SKU26: - constraint: null - cost: 229 - init_stock: 126 - max_stock: 1000 - price: 357 - sale_gamma: 63 - service_level: 0.95 - SKU260: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 364 - max_stock: 1000 - price: 866 - sale_gamma: 52 - service_level: 0.95 - SKU261: - constraint: null - cost: 368 - init_stock: 66 - max_stock: 1000 - price: 688 - sale_gamma: 22 - service_level: 0.95 - SKU262: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 332 - init_stock: 390 - max_stock: 1000 - price: 385 - sale_gamma: 78 - service_level: 0.95 - SKU263: - constraint: G(stock_constraint) - cost: 189 - init_stock: 444 - max_stock: 1000 - price: 372 - sale_gamma: 74 - service_level: 0.95 - SKU264: - constraint: null - cost: 361 - init_stock: 158 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU265: - constraint: null - cost: 286 - init_stock: 236 - max_stock: 1000 - price: 434 - sale_gamma: 59 - service_level: 0.95 - SKU266: - constraint: null - cost: 128 - init_stock: 282 - max_stock: 1000 - price: 172 - sale_gamma: 47 - service_level: 0.95 - SKU267: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 77 - init_stock: 320 - max_stock: 1000 - price: 130 - sale_gamma: 80 - service_level: 0.95 - SKU268: - constraint: null - cost: 221 - init_stock: 356 - max_stock: 1000 - price: 362 - sale_gamma: 89 - service_level: 0.95 - SKU269: - constraint: G(low_profit -> low_stock_constraint) - cost: 126 - init_stock: 132 - max_stock: 1000 - price: 175 - sale_gamma: 44 - service_level: 0.95 - SKU27: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 448 - max_stock: 1000 - price: 728 - sale_gamma: 56 - service_level: 0.95 - SKU270: - constraint: null - cost: 182 - init_stock: 370 - max_stock: 1000 - price: 263 - sale_gamma: 74 - service_level: 0.95 - SKU271: - constraint: G(stock_constraint) - cost: 230 - init_stock: 54 - max_stock: 1000 - price: 266 - sale_gamma: 18 - service_level: 0.95 - SKU272: - constraint: null - cost: 366 - init_stock: 51 - max_stock: 1000 - price: 625 - sale_gamma: 17 - service_level: 0.95 - SKU273: - constraint: null - cost: 421 - init_stock: 72 - max_stock: 1000 - price: 614 - sale_gamma: 18 - service_level: 0.95 - SKU274: - constraint: null - cost: 29 - init_stock: 308 - max_stock: 1000 - price: 42 - sale_gamma: 77 - service_level: 0.95 - SKU275: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 50 - init_stock: 144 - max_stock: 1000 - price: 76 - sale_gamma: 48 - service_level: 0.95 - SKU276: - constraint: G(stock_constraint) - cost: 163 - init_stock: 378 - max_stock: 1000 - price: 299 - sale_gamma: 54 - service_level: 0.95 - SKU277: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 82 - max_stock: 1000 - price: 507 - sale_gamma: 41 - service_level: 0.95 - SKU278: - constraint: null - cost: 105 - init_stock: 84 - max_stock: 1000 - price: 140 - sale_gamma: 12 - service_level: 0.95 - SKU279: - constraint: G(stock_constraint) - cost: 51 - init_stock: 273 - max_stock: 1000 - price: 61 - sale_gamma: 39 - service_level: 0.95 - SKU28: - constraint: G(stock_constraint) - cost: 208 - init_stock: 235 - max_stock: 1000 - price: 295 - sale_gamma: 47 - service_level: 0.95 - SKU280: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 198 - max_stock: 1000 - price: 436 - sale_gamma: 33 - service_level: 0.95 - SKU281: - constraint: null - cost: 395 - init_stock: 375 - max_stock: 1000 - price: 592 - sale_gamma: 75 - service_level: 0.95 - SKU282: - constraint: null - cost: 63 - init_stock: 184 - max_stock: 1000 - price: 112 - sale_gamma: 46 - service_level: 0.95 - SKU283: - constraint: null - cost: 392 - init_stock: 736 - max_stock: 1000 - price: 490 - sale_gamma: 92 - service_level: 0.95 - SKU284: - constraint: G(stock_constraint) - cost: 344 - init_stock: 268 - max_stock: 1000 - price: 643 - sale_gamma: 67 - service_level: 0.95 - SKU285: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 133 - init_stock: 546 - max_stock: 1000 - price: 148 - sale_gamma: 91 - service_level: 0.95 - SKU286: - constraint: null - cost: 337 - init_stock: 156 - max_stock: 1000 - price: 626 - sale_gamma: 39 - service_level: 0.95 - SKU287: - constraint: null - cost: 375 - init_stock: 224 - max_stock: 1000 - price: 660 - sale_gamma: 56 - service_level: 0.95 - SKU288: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 190 - max_stock: 1000 - price: 352 - sale_gamma: 38 - service_level: 0.95 - SKU289: - constraint: G(stock_constraint) - cost: 67 - init_stock: 62 - max_stock: 1000 - price: 91 - sale_gamma: 31 - service_level: 0.95 - SKU29: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 475 - sale_gamma: 58 - service_level: 0.95 - SKU290: - constraint: null - cost: 438 - init_stock: 268 - max_stock: 1000 - price: 779 - sale_gamma: 67 - service_level: 0.95 - SKU291: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 122 - max_stock: 1000 - price: 126 - sale_gamma: 61 - service_level: 0.95 - SKU292: - constraint: null - cost: 186 - init_stock: 15 - max_stock: 1000 - price: 344 - sale_gamma: 5 - service_level: 0.95 - SKU293: - constraint: G(stock_constraint) - cost: 162 - init_stock: 385 - max_stock: 1000 - price: 288 - sale_gamma: 55 - service_level: 0.95 - SKU294: - constraint: null - cost: 409 - init_stock: 45 - max_stock: 1000 - price: 605 - sale_gamma: 9 - service_level: 0.95 - SKU295: - constraint: null - cost: 435 - init_stock: 651 - max_stock: 1000 - price: 674 - sale_gamma: 93 - service_level: 0.95 - SKU296: - constraint: G(low_profit -> low_stock_constraint) - cost: 370 - init_stock: 552 - max_stock: 1000 - price: 580 - sale_gamma: 92 - service_level: 0.95 - SKU297: - constraint: null - cost: 298 - init_stock: 228 - max_stock: 1000 - price: 333 - sale_gamma: 38 - service_level: 0.95 - SKU298: - constraint: null - cost: 286 - init_stock: 140 - max_stock: 1000 - price: 500 - sale_gamma: 35 - service_level: 0.95 - SKU299: - constraint: G(stock_constraint) - cost: 367 - init_stock: 255 - max_stock: 1000 - price: 451 - sale_gamma: 51 - service_level: 0.95 - SKU3: - constraint: G(stock_constraint) - cost: 405 - init_stock: 48 - max_stock: 1000 - price: 733 - sale_gamma: 12 - service_level: 0.95 - SKU30: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 359 - init_stock: 276 - max_stock: 1000 - price: 581 - sale_gamma: 69 - service_level: 0.95 - SKU300: - constraint: G(low_profit -> low_stock_constraint) - cost: 376 - init_stock: 290 - max_stock: 1000 - price: 428 - sale_gamma: 58 - service_level: 0.95 - SKU301: - constraint: null - cost: 433 - init_stock: 581 - max_stock: 1000 - price: 857 - sale_gamma: 83 - service_level: 0.95 - SKU302: - constraint: null - cost: 184 - init_stock: 44 - max_stock: 1000 - price: 322 - sale_gamma: 11 - service_level: 0.95 - SKU303: - constraint: null - cost: 246 - init_stock: 188 - max_stock: 1000 - price: 487 - sale_gamma: 94 - service_level: 0.95 - SKU304: - constraint: G(low_profit -> low_stock_constraint) - cost: 419 - init_stock: 138 - max_stock: 1000 - price: 632 - sale_gamma: 23 - service_level: 0.95 - SKU305: - constraint: null - cost: 495 - init_stock: 500 - max_stock: 1000 - price: 772 - sale_gamma: 100 - service_level: 0.95 - SKU306: - constraint: null - cost: 479 - init_stock: 126 - max_stock: 1000 - price: 852 - sale_gamma: 42 - service_level: 0.95 - SKU307: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 210 - init_stock: 156 - max_stock: 1000 - price: 371 - sale_gamma: 78 - service_level: 0.95 - SKU308: - constraint: null - cost: 208 - init_stock: 25 - max_stock: 1000 - price: 262 - sale_gamma: 5 - service_level: 0.95 - SKU309: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 460 - max_stock: 1000 - price: 156 - sale_gamma: 92 - service_level: 0.95 - SKU31: - constraint: null - cost: 69 - init_stock: 280 - max_stock: 1000 - price: 120 - sale_gamma: 56 - service_level: 0.95 - SKU310: - constraint: null - cost: 234 - init_stock: 352 - max_stock: 1000 - price: 294 - sale_gamma: 44 - service_level: 0.95 - SKU311: - constraint: null - cost: 306 - init_stock: 400 - max_stock: 1000 - price: 437 - sale_gamma: 80 - service_level: 0.95 - SKU312: - constraint: G(stock_constraint) - cost: 291 - init_stock: 75 - max_stock: 1000 - price: 392 - sale_gamma: 25 - service_level: 0.95 - SKU313: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 324 - init_stock: 190 - max_stock: 1000 - price: 492 - sale_gamma: 38 - service_level: 0.95 - SKU314: - constraint: G(stock_constraint) - cost: 404 - init_stock: 116 - max_stock: 1000 - price: 456 - sale_gamma: 29 - service_level: 0.95 - SKU315: - constraint: G(stock_constraint) - cost: 471 - init_stock: 504 - max_stock: 1000 - price: 885 - sale_gamma: 84 - service_level: 0.95 - SKU316: - constraint: null - cost: 202 - init_stock: 90 - max_stock: 1000 - price: 282 - sale_gamma: 18 - service_level: 0.95 - SKU317: - constraint: null - cost: 216 - init_stock: 168 - max_stock: 1000 - price: 352 - sale_gamma: 24 - service_level: 0.95 - SKU318: - constraint: null - cost: 278 - init_stock: 255 - max_stock: 1000 - price: 350 - sale_gamma: 85 - service_level: 0.95 - SKU319: - constraint: null - cost: 257 - init_stock: 371 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU32: - constraint: null - cost: 336 - init_stock: 110 - max_stock: 1000 - price: 581 - sale_gamma: 22 - service_level: 0.95 - SKU320: - constraint: null - cost: 196 - init_stock: 156 - max_stock: 1000 - price: 258 - sale_gamma: 39 - service_level: 0.95 - SKU321: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 96 - max_stock: 1000 - price: 105 - sale_gamma: 16 - service_level: 0.95 - SKU322: - constraint: null - cost: 240 - init_stock: 600 - max_stock: 1000 - price: 453 - sale_gamma: 100 - service_level: 0.95 - SKU323: - constraint: G(stock_constraint) - cost: 66 - init_stock: 195 - max_stock: 1000 - price: 74 - sale_gamma: 39 - service_level: 0.95 - SKU324: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 442 - init_stock: 465 - max_stock: 1000 - price: 804 - sale_gamma: 93 - service_level: 0.95 - SKU325: - constraint: null - cost: 453 - init_stock: 345 - max_stock: 1000 - price: 579 - sale_gamma: 69 - service_level: 0.95 - SKU326: - constraint: null - cost: 345 - init_stock: 72 - max_stock: 1000 - price: 538 - sale_gamma: 24 - service_level: 0.95 - SKU327: - constraint: G(low_profit -> low_stock_constraint) - cost: 457 - init_stock: 84 - max_stock: 1000 - price: 749 - sale_gamma: 14 - service_level: 0.95 - SKU328: - constraint: null - cost: 390 - init_stock: 90 - max_stock: 1000 - price: 487 - sale_gamma: 45 - service_level: 0.95 - SKU329: - constraint: null - cost: 67 - init_stock: 84 - max_stock: 1000 - price: 126 - sale_gamma: 42 - service_level: 0.95 - SKU33: - constraint: G(low_profit -> low_stock_constraint) - cost: 416 - init_stock: 552 - max_stock: 1000 - price: 465 - sale_gamma: 92 - service_level: 0.95 - SKU330: - constraint: null - cost: 306 - init_stock: 273 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU331: - constraint: G(low_profit -> low_stock_constraint) - cost: 138 - init_stock: 164 - max_stock: 1000 - price: 180 - sale_gamma: 41 - service_level: 0.95 - SKU332: - constraint: null - cost: 390 - init_stock: 288 - max_stock: 1000 - price: 670 - sale_gamma: 96 - service_level: 0.95 - SKU333: - constraint: G(stock_constraint) - cost: 485 - init_stock: 318 - max_stock: 1000 - price: 800 - sale_gamma: 53 - service_level: 0.95 - SKU334: - constraint: null - cost: 183 - init_stock: 114 - max_stock: 1000 - price: 245 - sale_gamma: 57 - service_level: 0.95 - SKU335: - constraint: null - cost: 80 - init_stock: 243 - max_stock: 1000 - price: 141 - sale_gamma: 81 - service_level: 0.95 - SKU336: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 56 - max_stock: 1000 - price: 565 - sale_gamma: 28 - service_level: 0.95 - SKU337: - constraint: null - cost: 245 - init_stock: 174 - max_stock: 1000 - price: 394 - sale_gamma: 29 - service_level: 0.95 - SKU338: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 150 - sale_gamma: 81 - service_level: 0.95 - SKU339: - constraint: G(stock_constraint) - cost: 265 - init_stock: 441 - max_stock: 1000 - price: 368 - sale_gamma: 63 - service_level: 0.95 - SKU34: - constraint: null - cost: 95 - init_stock: 91 - max_stock: 1000 - price: 135 - sale_gamma: 13 - service_level: 0.95 - SKU340: - constraint: null - cost: 480 - init_stock: 435 - max_stock: 1000 - price: 633 - sale_gamma: 87 - service_level: 0.95 - SKU341: - constraint: G(low_profit -> low_stock_constraint) - cost: 462 - init_stock: 280 - max_stock: 1000 - price: 924 - sale_gamma: 70 - service_level: 0.95 - SKU342: - constraint: null - cost: 144 - init_stock: 72 - max_stock: 1000 - price: 216 - sale_gamma: 9 - service_level: 0.95 - SKU343: - constraint: null - cost: 310 - init_stock: 90 - max_stock: 1000 - price: 589 - sale_gamma: 15 - service_level: 0.95 - SKU344: - constraint: null - cost: 357 - init_stock: 348 - max_stock: 1000 - price: 442 - sale_gamma: 87 - service_level: 0.95 - SKU345: - constraint: null - cost: 442 - init_stock: 267 - max_stock: 1000 - price: 857 - sale_gamma: 89 - service_level: 0.95 - SKU346: - constraint: G(stock_constraint) - cost: 350 - init_stock: 336 - max_stock: 1000 - price: 549 - sale_gamma: 42 - service_level: 0.95 - SKU347: - constraint: G(low_profit -> low_stock_constraint) - cost: 497 - init_stock: 328 - max_stock: 1000 - price: 810 - sale_gamma: 82 - service_level: 0.95 - SKU348: - constraint: G(stock_constraint) - cost: 147 - init_stock: 100 - max_stock: 1000 - price: 277 - sale_gamma: 20 - service_level: 0.95 - SKU349: - constraint: null - cost: 67 - init_stock: 335 - max_stock: 1000 - price: 116 - sale_gamma: 67 - service_level: 0.95 - SKU35: - constraint: null - cost: 267 - init_stock: 430 - max_stock: 1000 - price: 373 - sale_gamma: 86 - service_level: 0.95 - SKU350: - constraint: G(stock_constraint) - cost: 279 - init_stock: 552 - max_stock: 1000 - price: 460 - sale_gamma: 92 - service_level: 0.95 - SKU351: - constraint: null - cost: 155 - init_stock: 335 - max_stock: 1000 - price: 294 - sale_gamma: 67 - service_level: 0.95 - SKU352: - constraint: null - cost: 71 - init_stock: 54 - max_stock: 1000 - price: 100 - sale_gamma: 18 - service_level: 0.95 - SKU353: - constraint: G(stock_constraint) - cost: 253 - init_stock: 465 - max_stock: 1000 - price: 437 - sale_gamma: 93 - service_level: 0.95 - SKU354: - constraint: G(low_profit -> low_stock_constraint) - cost: 396 - init_stock: 395 - max_stock: 1000 - price: 566 - sale_gamma: 79 - service_level: 0.95 - SKU355: - constraint: G(stock_constraint) - cost: 63 - init_stock: 30 - max_stock: 1000 - price: 74 - sale_gamma: 10 - service_level: 0.95 - SKU356: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 348 - init_stock: 87 - max_stock: 1000 - price: 441 - sale_gamma: 29 - service_level: 0.95 - SKU357: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 499 - init_stock: 460 - max_stock: 1000 - price: 868 - sale_gamma: 92 - service_level: 0.95 - SKU358: - constraint: null - cost: 269 - init_stock: 414 - max_stock: 1000 - price: 408 - sale_gamma: 69 - service_level: 0.95 - SKU359: - constraint: G(low_profit -> low_stock_constraint) - cost: 82 - init_stock: 600 - max_stock: 1000 - price: 110 - sale_gamma: 75 - service_level: 0.95 - SKU36: - constraint: null - cost: 105 - init_stock: 30 - max_stock: 1000 - price: 165 - sale_gamma: 10 - service_level: 0.95 - SKU360: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 75 - max_stock: 1000 - price: 682 - sale_gamma: 25 - service_level: 0.95 - SKU361: - constraint: null - cost: 163 - init_stock: 360 - max_stock: 1000 - price: 288 - sale_gamma: 90 - service_level: 0.95 - SKU362: - constraint: null - cost: 464 - init_stock: 435 - max_stock: 1000 - price: 867 - sale_gamma: 87 - service_level: 0.95 - SKU363: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 390 - max_stock: 1000 - price: 93 - sale_gamma: 78 - service_level: 0.95 - SKU364: - constraint: G(stock_constraint) - cost: 387 - init_stock: 66 - max_stock: 1000 - price: 472 - sale_gamma: 33 - service_level: 0.95 - SKU365: - constraint: null - cost: 158 - init_stock: 581 - max_stock: 1000 - price: 241 - sale_gamma: 83 - service_level: 0.95 - SKU366: - constraint: null - cost: 444 - init_stock: 344 - max_stock: 1000 - price: 639 - sale_gamma: 86 - service_level: 0.95 - SKU367: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 322 - max_stock: 1000 - price: 304 - sale_gamma: 46 - service_level: 0.95 - SKU368: - constraint: G(stock_constraint) - cost: 472 - init_stock: 198 - max_stock: 1000 - price: 613 - sale_gamma: 33 - service_level: 0.95 - SKU369: - constraint: null - cost: 96 - init_stock: 375 - max_stock: 1000 - price: 155 - sale_gamma: 75 - service_level: 0.95 - SKU37: - constraint: G(low_profit -> low_stock_constraint) - cost: 66 - init_stock: 177 - max_stock: 1000 - price: 110 - sale_gamma: 59 - service_level: 0.95 - SKU370: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 90 - max_stock: 1000 - price: 141 - sale_gamma: 15 - service_level: 0.95 - SKU371: - constraint: null - cost: 328 - init_stock: 25 - max_stock: 1000 - price: 511 - sale_gamma: 5 - service_level: 0.95 - SKU372: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 67 - init_stock: 192 - max_stock: 1000 - price: 134 - sale_gamma: 32 - service_level: 0.95 - SKU373: - constraint: null - cost: 58 - init_stock: 268 - max_stock: 1000 - price: 91 - sale_gamma: 67 - service_level: 0.95 - SKU374: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 216 - max_stock: 1000 - price: 73 - sale_gamma: 54 - service_level: 0.95 - SKU375: - constraint: G(low_profit -> low_stock_constraint) - cost: 283 - init_stock: 432 - max_stock: 1000 - price: 416 - sale_gamma: 72 - service_level: 0.95 - SKU376: - constraint: null - cost: 156 - init_stock: 164 - max_stock: 1000 - price: 291 - sale_gamma: 82 - service_level: 0.95 - SKU377: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 78 - init_stock: 536 - max_stock: 1000 - price: 134 - sale_gamma: 67 - service_level: 0.95 - SKU378: - constraint: G(low_profit -> low_stock_constraint) - cost: 424 - init_stock: 175 - max_stock: 1000 - price: 546 - sale_gamma: 35 - service_level: 0.95 - SKU379: - constraint: null - cost: 11 - init_stock: 196 - max_stock: 1000 - price: 20 - sale_gamma: 49 - service_level: 0.95 - SKU38: - constraint: null - cost: 344 - init_stock: 258 - max_stock: 1000 - price: 567 - sale_gamma: 43 - service_level: 0.95 - SKU380: - constraint: null - cost: 393 - init_stock: 212 - max_stock: 1000 - price: 605 - sale_gamma: 53 - service_level: 0.95 - SKU381: - constraint: null - cost: 476 - init_stock: 18 - max_stock: 1000 - price: 609 - sale_gamma: 6 - service_level: 0.95 - SKU382: - constraint: null - cost: 125 - init_stock: 426 - max_stock: 1000 - price: 177 - sale_gamma: 71 - service_level: 0.95 - SKU383: - constraint: null - cost: 304 - init_stock: 368 - max_stock: 1000 - price: 425 - sale_gamma: 92 - service_level: 0.95 - SKU384: - constraint: null - cost: 320 - init_stock: 54 - max_stock: 1000 - price: 460 - sale_gamma: 9 - service_level: 0.95 - SKU385: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 120 - init_stock: 91 - max_stock: 1000 - price: 184 - sale_gamma: 13 - service_level: 0.95 - SKU386: - constraint: G(stock_constraint) - cost: 295 - init_stock: 124 - max_stock: 1000 - price: 536 - sale_gamma: 31 - service_level: 0.95 - SKU387: - constraint: null - cost: 112 - init_stock: 582 - max_stock: 1000 - price: 154 - sale_gamma: 97 - service_level: 0.95 - SKU388: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 405 - init_stock: 264 - max_stock: 1000 - price: 635 - sale_gamma: 44 - service_level: 0.95 - SKU389: - constraint: G(stock_constraint) - cost: 376 - init_stock: 490 - max_stock: 1000 - price: 699 - sale_gamma: 70 - service_level: 0.95 - SKU39: - constraint: G(low_profit -> low_stock_constraint) - cost: 25 - init_stock: 204 - max_stock: 1000 - price: 45 - sale_gamma: 51 - service_level: 0.95 - SKU390: - constraint: null - cost: 144 - init_stock: 156 - max_stock: 1000 - price: 223 - sale_gamma: 39 - service_level: 0.95 - SKU391: - constraint: G(stock_constraint) - cost: 233 - init_stock: 402 - max_stock: 1000 - price: 403 - sale_gamma: 67 - service_level: 0.95 - SKU392: - constraint: null - cost: 163 - init_stock: 370 - max_stock: 1000 - price: 205 - sale_gamma: 74 - service_level: 0.95 - SKU393: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 487 - init_stock: 402 - max_stock: 1000 - price: 701 - sale_gamma: 67 - service_level: 0.95 - SKU394: - constraint: null - cost: 154 - init_stock: 318 - max_stock: 1000 - price: 252 - sale_gamma: 53 - service_level: 0.95 - SKU395: - constraint: null - cost: 488 - init_stock: 198 - max_stock: 1000 - price: 854 - sale_gamma: 33 - service_level: 0.95 - SKU396: - constraint: null - cost: 333 - init_stock: 427 - max_stock: 1000 - price: 469 - sale_gamma: 61 - service_level: 0.95 - SKU397: - constraint: null - cost: 460 - init_stock: 153 - max_stock: 1000 - price: 791 - sale_gamma: 51 - service_level: 0.95 - SKU398: - constraint: G(stock_constraint) - cost: 234 - init_stock: 174 - max_stock: 1000 - price: 414 - sale_gamma: 58 - service_level: 0.95 - SKU399: - constraint: G(stock_constraint) - cost: 376 - init_stock: 148 - max_stock: 1000 - price: 612 - sale_gamma: 37 - service_level: 0.95 - SKU4: - constraint: G(low_profit -> low_stock_constraint) - cost: 439 - init_stock: 21 - max_stock: 1000 - price: 798 - sale_gamma: 7 - service_level: 0.95 - SKU40: - constraint: null - cost: 490 - init_stock: 594 - max_stock: 1000 - price: 563 - sale_gamma: 99 - service_level: 0.95 - SKU400: - constraint: G(low_profit -> low_stock_constraint) - cost: 445 - init_stock: 420 - max_stock: 1000 - price: 729 - sale_gamma: 84 - service_level: 0.95 - SKU401: - constraint: G(low_profit -> low_stock_constraint) - cost: 410 - init_stock: 312 - max_stock: 1000 - price: 774 - sale_gamma: 78 - service_level: 0.95 - SKU402: - constraint: G(low_profit -> low_stock_constraint) - cost: 86 - init_stock: 35 - max_stock: 1000 - price: 153 - sale_gamma: 7 - service_level: 0.95 - SKU403: - constraint: null - cost: 89 - init_stock: 594 - max_stock: 1000 - price: 133 - sale_gamma: 99 - service_level: 0.95 - SKU404: - constraint: null - cost: 287 - init_stock: 305 - max_stock: 1000 - price: 522 - sale_gamma: 61 - service_level: 0.95 - SKU405: - constraint: G(stock_constraint) - cost: 460 - init_stock: 114 - max_stock: 1000 - price: 584 - sale_gamma: 19 - service_level: 0.95 - SKU406: - constraint: null - cost: 327 - init_stock: 400 - max_stock: 1000 - price: 405 - sale_gamma: 100 - service_level: 0.95 - SKU407: - constraint: null - cost: 26 - init_stock: 184 - max_stock: 1000 - price: 41 - sale_gamma: 46 - service_level: 0.95 - SKU408: - constraint: null - cost: 444 - init_stock: 48 - max_stock: 1000 - price: 754 - sale_gamma: 8 - service_level: 0.95 - SKU409: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 257 - init_stock: 273 - max_stock: 1000 - price: 449 - sale_gamma: 91 - service_level: 0.95 - SKU41: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 141 - init_stock: 553 - max_stock: 1000 - price: 162 - sale_gamma: 79 - service_level: 0.95 - SKU410: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 70 - init_stock: 32 - max_stock: 1000 - price: 88 - sale_gamma: 16 - service_level: 0.95 - SKU411: - constraint: G(stock_constraint) - cost: 210 - init_stock: 380 - max_stock: 1000 - price: 405 - sale_gamma: 95 - service_level: 0.95 - SKU412: - constraint: null - cost: 286 - init_stock: 248 - max_stock: 1000 - price: 414 - sale_gamma: 62 - service_level: 0.95 - SKU413: - constraint: null - cost: 403 - init_stock: 581 - max_stock: 1000 - price: 801 - sale_gamma: 83 - service_level: 0.95 - SKU414: - constraint: G(stock_constraint) - cost: 165 - init_stock: 435 - max_stock: 1000 - price: 229 - sale_gamma: 87 - service_level: 0.95 - SKU415: - constraint: G(stock_constraint) - cost: 291 - init_stock: 184 - max_stock: 1000 - price: 372 - sale_gamma: 23 - service_level: 0.95 - SKU416: - constraint: null - cost: 228 - init_stock: 36 - max_stock: 1000 - price: 373 - sale_gamma: 9 - service_level: 0.95 - SKU417: - constraint: G(low_profit -> low_stock_constraint) - cost: 443 - init_stock: 288 - max_stock: 1000 - price: 872 - sale_gamma: 72 - service_level: 0.95 - SKU418: - constraint: null - cost: 458 - init_stock: 52 - max_stock: 1000 - price: 838 - sale_gamma: 13 - service_level: 0.95 - SKU419: - constraint: null - cost: 303 - init_stock: 712 - max_stock: 1000 - price: 448 - sale_gamma: 89 - service_level: 0.95 - SKU42: - constraint: null - cost: 462 - init_stock: 156 - max_stock: 1000 - price: 688 - sale_gamma: 26 - service_level: 0.95 - SKU420: - constraint: null - cost: 268 - init_stock: 84 - max_stock: 1000 - price: 506 - sale_gamma: 42 - service_level: 0.95 - SKU421: - constraint: G(stock_constraint) - cost: 214 - init_stock: 138 - max_stock: 1000 - price: 314 - sale_gamma: 46 - service_level: 0.95 - SKU422: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 52 - init_stock: 432 - max_stock: 1000 - price: 97 - sale_gamma: 54 - service_level: 0.95 - SKU423: - constraint: G(low_profit -> low_stock_constraint) - cost: 188 - init_stock: 396 - max_stock: 1000 - price: 265 - sale_gamma: 66 - service_level: 0.95 - SKU424: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 66 - max_stock: 1000 - price: 317 - sale_gamma: 11 - service_level: 0.95 - SKU425: - constraint: G(stock_constraint) - cost: 162 - init_stock: 72 - max_stock: 1000 - price: 277 - sale_gamma: 12 - service_level: 0.95 - SKU426: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 125 - init_stock: 588 - max_stock: 1000 - price: 246 - sale_gamma: 98 - service_level: 0.95 - SKU427: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 413 - init_stock: 282 - max_stock: 1000 - price: 710 - sale_gamma: 94 - service_level: 0.95 - SKU428: - constraint: G(stock_constraint) - cost: 391 - init_stock: 378 - max_stock: 1000 - price: 629 - sale_gamma: 63 - service_level: 0.95 - SKU429: - constraint: G(stock_constraint) - cost: 39 - init_stock: 246 - max_stock: 1000 - price: 73 - sale_gamma: 41 - service_level: 0.95 - SKU43: - constraint: G(stock_constraint) - cost: 217 - init_stock: 272 - max_stock: 1000 - price: 353 - sale_gamma: 68 - service_level: 0.95 - SKU430: - constraint: null - cost: 216 - init_stock: 511 - max_stock: 1000 - price: 313 - sale_gamma: 73 - service_level: 0.95 - SKU431: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 328 - init_stock: 126 - max_stock: 1000 - price: 646 - sale_gamma: 21 - service_level: 0.95 - SKU432: - constraint: G(stock_constraint) - cost: 25 - init_stock: 368 - max_stock: 1000 - price: 35 - sale_gamma: 46 - service_level: 0.95 - SKU433: - constraint: null - cost: 348 - init_stock: 270 - max_stock: 1000 - price: 396 - sale_gamma: 45 - service_level: 0.95 - SKU434: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 90 - max_stock: 1000 - price: 40 - sale_gamma: 30 - service_level: 0.95 - SKU435: - constraint: G(stock_constraint) - cost: 272 - init_stock: 210 - max_stock: 1000 - price: 320 - sale_gamma: 30 - service_level: 0.95 - SKU436: - constraint: null - cost: 72 - init_stock: 405 - max_stock: 1000 - price: 105 - sale_gamma: 81 - service_level: 0.95 - SKU437: - constraint: G(stock_constraint) - cost: 115 - init_stock: 84 - max_stock: 1000 - price: 223 - sale_gamma: 14 - service_level: 0.95 - SKU438: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 143 - init_stock: 40 - max_stock: 1000 - price: 190 - sale_gamma: 10 - service_level: 0.95 - SKU439: - constraint: G(stock_constraint) - cost: 256 - init_stock: 190 - max_stock: 1000 - price: 304 - sale_gamma: 38 - service_level: 0.95 - SKU44: - constraint: null - cost: 360 - init_stock: 240 - max_stock: 1000 - price: 669 - sale_gamma: 48 - service_level: 0.95 - SKU440: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 248 - init_stock: 246 - max_stock: 1000 - price: 357 - sale_gamma: 82 - service_level: 0.95 - SKU441: - constraint: null - cost: 253 - init_stock: 224 - max_stock: 1000 - price: 374 - sale_gamma: 56 - service_level: 0.95 - SKU442: - constraint: null - cost: 470 - init_stock: 352 - max_stock: 1000 - price: 629 - sale_gamma: 88 - service_level: 0.95 - SKU443: - constraint: null - cost: 218 - init_stock: 365 - max_stock: 1000 - price: 361 - sale_gamma: 73 - service_level: 0.95 - SKU444: - constraint: null - cost: 156 - init_stock: 18 - max_stock: 1000 - price: 182 - sale_gamma: 6 - service_level: 0.95 - SKU445: - constraint: null - cost: 260 - init_stock: 602 - max_stock: 1000 - price: 296 - sale_gamma: 86 - service_level: 0.95 - SKU446: - constraint: null - cost: 497 - init_stock: 147 - max_stock: 1000 - price: 690 - sale_gamma: 49 - service_level: 0.95 - SKU447: - constraint: null - cost: 196 - init_stock: 30 - max_stock: 1000 - price: 280 - sale_gamma: 5 - service_level: 0.95 - SKU448: - constraint: null - cost: 485 - init_stock: 497 - max_stock: 1000 - price: 742 - sale_gamma: 71 - service_level: 0.95 - SKU449: - constraint: null - cost: 282 - init_stock: 114 - max_stock: 1000 - price: 360 - sale_gamma: 38 - service_level: 0.95 - SKU45: - constraint: G(stock_constraint) - cost: 253 - init_stock: 30 - max_stock: 1000 - price: 351 - sale_gamma: 10 - service_level: 0.95 - SKU450: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 58 - init_stock: 294 - max_stock: 1000 - price: 77 - sale_gamma: 98 - service_level: 0.95 - SKU451: - constraint: null - cost: 468 - init_stock: 483 - max_stock: 1000 - price: 851 - sale_gamma: 69 - service_level: 0.95 - SKU452: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 63 - init_stock: 395 - max_stock: 1000 - price: 97 - sale_gamma: 79 - service_level: 0.95 - SKU453: - constraint: null - cost: 37 - init_stock: 140 - max_stock: 1000 - price: 65 - sale_gamma: 35 - service_level: 0.95 - SKU454: - constraint: G(low_profit -> low_stock_constraint) - cost: 494 - init_stock: 340 - max_stock: 1000 - price: 899 - sale_gamma: 85 - service_level: 0.95 - SKU455: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 136 - init_stock: 182 - max_stock: 1000 - price: 195 - sale_gamma: 26 - service_level: 0.95 - SKU456: - constraint: G(stock_constraint) - cost: 338 - init_stock: 75 - max_stock: 1000 - price: 659 - sale_gamma: 25 - service_level: 0.95 - SKU457: - constraint: null - cost: 169 - init_stock: 256 - max_stock: 1000 - price: 190 - sale_gamma: 64 - service_level: 0.95 - SKU458: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 403 - init_stock: 28 - max_stock: 1000 - price: 685 - sale_gamma: 7 - service_level: 0.95 - SKU459: - constraint: null - cost: 425 - init_stock: 272 - max_stock: 1000 - price: 731 - sale_gamma: 34 - service_level: 0.95 - SKU46: - constraint: G(low_profit -> low_stock_constraint) - cost: 299 - init_stock: 200 - max_stock: 1000 - price: 409 - sale_gamma: 50 - service_level: 0.95 - SKU460: - constraint: G(low_profit -> low_stock_constraint) - cost: 405 - init_stock: 292 - max_stock: 1000 - price: 558 - sale_gamma: 73 - service_level: 0.95 - SKU461: - constraint: G(stock_constraint) - cost: 251 - init_stock: 240 - max_stock: 1000 - price: 326 - sale_gamma: 48 - service_level: 0.95 - SKU462: - constraint: G(low_profit -> low_stock_constraint) - cost: 448 - init_stock: 36 - max_stock: 1000 - price: 757 - sale_gamma: 9 - service_level: 0.95 - SKU463: - constraint: null - cost: 187 - init_stock: 574 - max_stock: 1000 - price: 213 - sale_gamma: 82 - service_level: 0.95 - SKU464: - constraint: null - cost: 279 - init_stock: 272 - max_stock: 1000 - price: 438 - sale_gamma: 34 - service_level: 0.95 - SKU465: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 147 - init_stock: 400 - max_stock: 1000 - price: 288 - sale_gamma: 100 - service_level: 0.95 - SKU466: - constraint: G(stock_constraint) - cost: 97 - init_stock: 126 - max_stock: 1000 - price: 167 - sale_gamma: 42 - service_level: 0.95 - SKU467: - constraint: G(stock_constraint) - cost: 142 - init_stock: 252 - max_stock: 1000 - price: 230 - sale_gamma: 36 - service_level: 0.95 - SKU468: - constraint: G(stock_constraint) - cost: 55 - init_stock: 250 - max_stock: 1000 - price: 104 - sale_gamma: 50 - service_level: 0.95 - SKU469: - constraint: G(stock_constraint) - cost: 178 - init_stock: 105 - max_stock: 1000 - price: 331 - sale_gamma: 15 - service_level: 0.95 - SKU47: - constraint: G(stock_constraint) - cost: 121 - init_stock: 117 - max_stock: 1000 - price: 240 - sale_gamma: 39 - service_level: 0.95 - SKU470: - constraint: G(low_profit -> low_stock_constraint) - cost: 373 - init_stock: 128 - max_stock: 1000 - price: 548 - sale_gamma: 32 - service_level: 0.95 - SKU471: - constraint: G(low_profit -> low_stock_constraint) - cost: 315 - init_stock: 568 - max_stock: 1000 - price: 387 - sale_gamma: 71 - service_level: 0.95 - SKU472: - constraint: null - cost: 421 - init_stock: 177 - max_stock: 1000 - price: 627 - sale_gamma: 59 - service_level: 0.95 - SKU473: - constraint: G(low_profit -> low_stock_constraint) - cost: 195 - init_stock: 105 - max_stock: 1000 - price: 323 - sale_gamma: 21 - service_level: 0.95 - SKU474: - constraint: null - cost: 448 - init_stock: 602 - max_stock: 1000 - price: 775 - sale_gamma: 86 - service_level: 0.95 - SKU475: - constraint: null - cost: 34 - init_stock: 282 - max_stock: 1000 - price: 62 - sale_gamma: 94 - service_level: 0.95 - SKU476: - constraint: null - cost: 251 - init_stock: 180 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU477: - constraint: null - cost: 289 - init_stock: 329 - max_stock: 1000 - price: 338 - sale_gamma: 47 - service_level: 0.95 - SKU478: - constraint: null - cost: 479 - init_stock: 352 - max_stock: 1000 - price: 723 - sale_gamma: 88 - service_level: 0.95 - SKU479: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 366 - init_stock: 152 - max_stock: 1000 - price: 552 - sale_gamma: 38 - service_level: 0.95 - SKU48: - constraint: null - cost: 340 - init_stock: 232 - max_stock: 1000 - price: 387 - sale_gamma: 58 - service_level: 0.95 - SKU480: - constraint: G(stock_constraint) - cost: 18 - init_stock: 180 - max_stock: 1000 - price: 21 - sale_gamma: 30 - service_level: 0.95 - SKU481: - constraint: null - cost: 330 - init_stock: 352 - max_stock: 1000 - price: 422 - sale_gamma: 88 - service_level: 0.95 - SKU482: - constraint: G(stock_constraint) - cost: 94 - init_stock: 696 - max_stock: 1000 - price: 108 - sale_gamma: 87 - service_level: 0.95 - SKU483: - constraint: null - cost: 298 - init_stock: 170 - max_stock: 1000 - price: 533 - sale_gamma: 34 - service_level: 0.95 - SKU484: - constraint: null - cost: 84 - init_stock: 365 - max_stock: 1000 - price: 117 - sale_gamma: 73 - service_level: 0.95 - SKU485: - constraint: null - cost: 318 - init_stock: 536 - max_stock: 1000 - price: 543 - sale_gamma: 67 - service_level: 0.95 - SKU486: - constraint: G(low_profit -> low_stock_constraint) - cost: 122 - init_stock: 208 - max_stock: 1000 - price: 161 - sale_gamma: 52 - service_level: 0.95 - SKU487: - constraint: null - cost: 277 - init_stock: 264 - max_stock: 1000 - price: 362 - sale_gamma: 66 - service_level: 0.95 - SKU488: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 189 - max_stock: 1000 - price: 42 - sale_gamma: 27 - service_level: 0.95 - SKU489: - constraint: null - cost: 59 - init_stock: 124 - max_stock: 1000 - price: 97 - sale_gamma: 31 - service_level: 0.95 - SKU49: - constraint: null - cost: 263 - init_stock: 760 - max_stock: 1000 - price: 515 - sale_gamma: 95 - service_level: 0.95 - SKU490: - constraint: null - cost: 161 - init_stock: 486 - max_stock: 1000 - price: 264 - sale_gamma: 81 - service_level: 0.95 - SKU491: - constraint: null - cost: 444 - init_stock: 450 - max_stock: 1000 - price: 834 - sale_gamma: 75 - service_level: 0.95 - SKU492: - constraint: null - cost: 53 - init_stock: 240 - max_stock: 1000 - price: 63 - sale_gamma: 80 - service_level: 0.95 - SKU493: - constraint: null - cost: 461 - init_stock: 81 - max_stock: 1000 - price: 567 - sale_gamma: 27 - service_level: 0.95 - SKU494: - constraint: null - cost: 145 - init_stock: 282 - max_stock: 1000 - price: 242 - sale_gamma: 94 - service_level: 0.95 - SKU495: - constraint: G(stock_constraint) - cost: 149 - init_stock: 372 - max_stock: 1000 - price: 217 - sale_gamma: 62 - service_level: 0.95 - SKU496: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 342 - init_stock: 295 - max_stock: 1000 - price: 492 - sale_gamma: 59 - service_level: 0.95 - SKU497: - constraint: G(low_profit -> low_stock_constraint) - cost: 33 - init_stock: 45 - max_stock: 1000 - price: 42 - sale_gamma: 9 - service_level: 0.95 - SKU498: - constraint: null - cost: 305 - init_stock: 325 - max_stock: 1000 - price: 439 - sale_gamma: 65 - service_level: 0.95 - SKU499: - constraint: G(stock_constraint) - cost: 307 - init_stock: 174 - max_stock: 1000 - price: 472 - sale_gamma: 29 - service_level: 0.95 - SKU5: - constraint: G(stock_constraint) - cost: 466 - init_stock: 426 - max_stock: 1000 - price: 852 - sale_gamma: 71 - service_level: 0.95 - SKU50: - constraint: null - cost: 282 - init_stock: 185 - max_stock: 1000 - price: 516 - sale_gamma: 37 - service_level: 0.95 - SKU500: - constraint: null - cost: 208 - init_stock: 336 - max_stock: 1000 - price: 255 - sale_gamma: 42 - service_level: 0.95 - SKU501: - constraint: null - cost: 194 - init_stock: 152 - max_stock: 1000 - price: 265 - sale_gamma: 76 - service_level: 0.95 - SKU502: - constraint: null - cost: 69 - init_stock: 390 - max_stock: 1000 - price: 101 - sale_gamma: 65 - service_level: 0.95 - SKU503: - constraint: G(stock_constraint) - cost: 208 - init_stock: 228 - max_stock: 1000 - price: 280 - sale_gamma: 57 - service_level: 0.95 - SKU504: - constraint: null - cost: 251 - init_stock: 210 - max_stock: 1000 - price: 426 - sale_gamma: 30 - service_level: 0.95 - SKU505: - constraint: null - cost: 426 - init_stock: 295 - max_stock: 1000 - price: 515 - sale_gamma: 59 - service_level: 0.95 - SKU506: - constraint: null - cost: 149 - init_stock: 465 - max_stock: 1000 - price: 220 - sale_gamma: 93 - service_level: 0.95 - SKU507: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 187 - init_stock: 60 - max_stock: 1000 - price: 293 - sale_gamma: 15 - service_level: 0.95 - SKU508: - constraint: G(low_profit -> low_stock_constraint) - cost: 272 - init_stock: 576 - max_stock: 1000 - price: 432 - sale_gamma: 96 - service_level: 0.95 - SKU509: - constraint: G(low_profit -> low_stock_constraint) - cost: 297 - init_stock: 486 - max_stock: 1000 - price: 397 - sale_gamma: 81 - service_level: 0.95 - SKU51: - constraint: G(low_profit -> low_stock_constraint) - cost: 295 - init_stock: 469 - max_stock: 1000 - price: 477 - sale_gamma: 67 - service_level: 0.95 - SKU510: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 36 - max_stock: 1000 - price: 156 - sale_gamma: 9 - service_level: 0.95 - SKU511: - constraint: G(stock_constraint) - cost: 361 - init_stock: 450 - max_stock: 1000 - price: 480 - sale_gamma: 75 - service_level: 0.95 - SKU512: - constraint: null - cost: 467 - init_stock: 132 - max_stock: 1000 - price: 854 - sale_gamma: 22 - service_level: 0.95 - SKU513: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 343 - init_stock: 42 - max_stock: 1000 - price: 631 - sale_gamma: 7 - service_level: 0.95 - SKU514: - constraint: null - cost: 186 - init_stock: 504 - max_stock: 1000 - price: 332 - sale_gamma: 63 - service_level: 0.95 - SKU515: - constraint: null - cost: 145 - init_stock: 216 - max_stock: 1000 - price: 227 - sale_gamma: 54 - service_level: 0.95 - SKU516: - constraint: G(stock_constraint) - cost: 64 - init_stock: 114 - max_stock: 1000 - price: 96 - sale_gamma: 38 - service_level: 0.95 - SKU517: - constraint: null - cost: 117 - init_stock: 45 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU518: - constraint: G(stock_constraint) - cost: 26 - init_stock: 147 - max_stock: 1000 - price: 30 - sale_gamma: 21 - service_level: 0.95 - SKU519: - constraint: null - cost: 233 - init_stock: 250 - max_stock: 1000 - price: 410 - sale_gamma: 50 - service_level: 0.95 - SKU52: - constraint: null - cost: 22 - init_stock: 402 - max_stock: 1000 - price: 28 - sale_gamma: 67 - service_level: 0.95 - SKU520: - constraint: G(low_profit -> low_stock_constraint) - cost: 209 - init_stock: 44 - max_stock: 1000 - price: 248 - sale_gamma: 22 - service_level: 0.95 - SKU521: - constraint: G(low_profit -> low_stock_constraint) - cost: 220 - init_stock: 350 - max_stock: 1000 - price: 330 - sale_gamma: 50 - service_level: 0.95 - SKU522: - constraint: null - cost: 156 - init_stock: 522 - max_stock: 1000 - price: 277 - sale_gamma: 87 - service_level: 0.95 - SKU523: - constraint: null - cost: 65 - init_stock: 500 - max_stock: 1000 - price: 107 - sale_gamma: 100 - service_level: 0.95 - SKU524: - constraint: null - cost: 294 - init_stock: 188 - max_stock: 1000 - price: 464 - sale_gamma: 94 - service_level: 0.95 - SKU525: - constraint: G(stock_constraint) - cost: 432 - init_stock: 126 - max_stock: 1000 - price: 751 - sale_gamma: 42 - service_level: 0.95 - SKU526: - constraint: null - cost: 419 - init_stock: 168 - max_stock: 1000 - price: 716 - sale_gamma: 24 - service_level: 0.95 - SKU527: - constraint: null - cost: 131 - init_stock: 350 - max_stock: 1000 - price: 262 - sale_gamma: 70 - service_level: 0.95 - SKU528: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 84 - max_stock: 1000 - price: 581 - sale_gamma: 21 - service_level: 0.95 - SKU529: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 248 - max_stock: 1000 - price: 375 - sale_gamma: 31 - service_level: 0.95 - SKU53: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 151 - init_stock: 490 - max_stock: 1000 - price: 191 - sale_gamma: 70 - service_level: 0.95 - SKU530: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 135 - max_stock: 1000 - price: 205 - sale_gamma: 45 - service_level: 0.95 - SKU531: - constraint: null - cost: 103 - init_stock: 300 - max_stock: 1000 - price: 153 - sale_gamma: 60 - service_level: 0.95 - SKU532: - constraint: null - cost: 351 - init_stock: 539 - max_stock: 1000 - price: 431 - sale_gamma: 77 - service_level: 0.95 - SKU533: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 168 - max_stock: 1000 - price: 340 - sale_gamma: 42 - service_level: 0.95 - SKU534: - constraint: null - cost: 425 - init_stock: 82 - max_stock: 1000 - price: 548 - sale_gamma: 41 - service_level: 0.95 - SKU535: - constraint: null - cost: 304 - init_stock: 430 - max_stock: 1000 - price: 465 - sale_gamma: 86 - service_level: 0.95 - SKU536: - constraint: null - cost: 358 - init_stock: 208 - max_stock: 1000 - price: 415 - sale_gamma: 52 - service_level: 0.95 - SKU537: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 321 - init_stock: 45 - max_stock: 1000 - price: 455 - sale_gamma: 9 - service_level: 0.95 - SKU538: - constraint: null - cost: 457 - init_stock: 200 - max_stock: 1000 - price: 868 - sale_gamma: 50 - service_level: 0.95 - SKU539: - constraint: G(stock_constraint) - cost: 165 - init_stock: 234 - max_stock: 1000 - price: 229 - sale_gamma: 78 - service_level: 0.95 - SKU54: - constraint: G(low_profit -> low_stock_constraint) - cost: 158 - init_stock: 138 - max_stock: 1000 - price: 241 - sale_gamma: 46 - service_level: 0.95 - SKU540: - constraint: null - cost: 388 - init_stock: 294 - max_stock: 1000 - price: 496 - sale_gamma: 42 - service_level: 0.95 - SKU541: - constraint: null - cost: 131 - init_stock: 203 - max_stock: 1000 - price: 213 - sale_gamma: 29 - service_level: 0.95 - SKU542: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 38 - init_stock: 130 - max_stock: 1000 - price: 42 - sale_gamma: 26 - service_level: 0.95 - SKU543: - constraint: G(stock_constraint) - cost: 430 - init_stock: 510 - max_stock: 1000 - price: 718 - sale_gamma: 85 - service_level: 0.95 - SKU544: - constraint: G(stock_constraint) - cost: 346 - init_stock: 194 - max_stock: 1000 - price: 474 - sale_gamma: 97 - service_level: 0.95 - SKU545: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 175 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU546: - constraint: null - cost: 491 - init_stock: 576 - max_stock: 1000 - price: 765 - sale_gamma: 96 - service_level: 0.95 - SKU547: - constraint: G(stock_constraint) - cost: 161 - init_stock: 264 - max_stock: 1000 - price: 251 - sale_gamma: 44 - service_level: 0.95 - SKU548: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 111 - init_stock: 36 - max_stock: 1000 - price: 217 - sale_gamma: 6 - service_level: 0.95 - SKU549: - constraint: G(stock_constraint) - cost: 387 - init_stock: 316 - max_stock: 1000 - price: 510 - sale_gamma: 79 - service_level: 0.95 - SKU55: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 455 - max_stock: 1000 - price: 896 - sale_gamma: 65 - service_level: 0.95 - SKU550: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 259 - init_stock: 282 - max_stock: 1000 - price: 502 - sale_gamma: 94 - service_level: 0.95 - SKU551: - constraint: null - cost: 46 - init_stock: 186 - max_stock: 1000 - price: 60 - sale_gamma: 31 - service_level: 0.95 - SKU552: - constraint: null - cost: 191 - init_stock: 450 - max_stock: 1000 - price: 315 - sale_gamma: 90 - service_level: 0.95 - SKU553: - constraint: G(low_profit -> low_stock_constraint) - cost: 208 - init_stock: 60 - max_stock: 1000 - price: 251 - sale_gamma: 30 - service_level: 0.95 - SKU554: - constraint: null - cost: 340 - init_stock: 82 - max_stock: 1000 - price: 387 - sale_gamma: 41 - service_level: 0.95 - SKU555: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 489 - init_stock: 252 - max_stock: 1000 - price: 684 - sale_gamma: 84 - service_level: 0.95 - SKU556: - constraint: G(stock_constraint) - cost: 110 - init_stock: 90 - max_stock: 1000 - price: 213 - sale_gamma: 30 - service_level: 0.95 - SKU557: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 334 - init_stock: 365 - max_stock: 1000 - price: 661 - sale_gamma: 73 - service_level: 0.95 - SKU558: - constraint: null - cost: 399 - init_stock: 85 - max_stock: 1000 - price: 490 - sale_gamma: 17 - service_level: 0.95 - SKU559: - constraint: null - cost: 313 - init_stock: 270 - max_stock: 1000 - price: 591 - sale_gamma: 54 - service_level: 0.95 - SKU56: - constraint: null - cost: 377 - init_stock: 222 - max_stock: 1000 - price: 671 - sale_gamma: 74 - service_level: 0.95 - SKU560: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 283 - init_stock: 246 - max_stock: 1000 - price: 458 - sale_gamma: 41 - service_level: 0.95 - SKU561: - constraint: null - cost: 202 - init_stock: 312 - max_stock: 1000 - price: 385 - sale_gamma: 39 - service_level: 0.95 - SKU562: - constraint: null - cost: 347 - init_stock: 356 - max_stock: 1000 - price: 666 - sale_gamma: 89 - service_level: 0.95 - SKU563: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 401 - init_stock: 30 - max_stock: 1000 - price: 509 - sale_gamma: 5 - service_level: 0.95 - SKU564: - constraint: null - cost: 109 - init_stock: 63 - max_stock: 1000 - price: 183 - sale_gamma: 9 - service_level: 0.95 - SKU565: - constraint: null - cost: 57 - init_stock: 525 - max_stock: 1000 - price: 90 - sale_gamma: 75 - service_level: 0.95 - SKU566: - constraint: null - cost: 131 - init_stock: 44 - max_stock: 1000 - price: 165 - sale_gamma: 11 - service_level: 0.95 - SKU567: - constraint: G(stock_constraint) - cost: 361 - init_stock: 27 - max_stock: 1000 - price: 465 - sale_gamma: 9 - service_level: 0.95 - SKU568: - constraint: null - cost: 75 - init_stock: 234 - max_stock: 1000 - price: 102 - sale_gamma: 78 - service_level: 0.95 - SKU569: - constraint: null - cost: 473 - init_stock: 192 - max_stock: 1000 - price: 591 - sale_gamma: 48 - service_level: 0.95 - SKU57: - constraint: G(stock_constraint) - cost: 359 - init_stock: 350 - max_stock: 1000 - price: 682 - sale_gamma: 50 - service_level: 0.95 - SKU570: - constraint: null - cost: 388 - init_stock: 581 - max_stock: 1000 - price: 686 - sale_gamma: 83 - service_level: 0.95 - SKU571: - constraint: G(stock_constraint) - cost: 168 - init_stock: 78 - max_stock: 1000 - price: 208 - sale_gamma: 39 - service_level: 0.95 - SKU572: - constraint: null - cost: 26 - init_stock: 225 - max_stock: 1000 - price: 41 - sale_gamma: 45 - service_level: 0.95 - SKU573: - constraint: null - cost: 246 - init_stock: 164 - max_stock: 1000 - price: 391 - sale_gamma: 41 - service_level: 0.95 - SKU574: - constraint: G(low_profit -> low_stock_constraint) - cost: 94 - init_stock: 150 - max_stock: 1000 - price: 166 - sale_gamma: 50 - service_level: 0.95 - SKU575: - constraint: null - cost: 237 - init_stock: 237 - max_stock: 1000 - price: 438 - sale_gamma: 79 - service_level: 0.95 - SKU576: - constraint: G(stock_constraint) - cost: 265 - init_stock: 468 - max_stock: 1000 - price: 416 - sale_gamma: 78 - service_level: 0.95 - SKU577: - constraint: G(low_profit -> low_stock_constraint) - cost: 18 - init_stock: 348 - max_stock: 1000 - price: 24 - sale_gamma: 87 - service_level: 0.95 - SKU578: - constraint: null - cost: 100 - init_stock: 284 - max_stock: 1000 - price: 148 - sale_gamma: 71 - service_level: 0.95 - SKU579: - constraint: null - cost: 415 - init_stock: 27 - max_stock: 1000 - price: 771 - sale_gamma: 9 - service_level: 0.95 - SKU58: - constraint: null - cost: 362 - init_stock: 240 - max_stock: 1000 - price: 521 - sale_gamma: 48 - service_level: 0.95 - SKU580: - constraint: null - cost: 203 - init_stock: 15 - max_stock: 1000 - price: 261 - sale_gamma: 5 - service_level: 0.95 - SKU581: - constraint: null - cost: 152 - init_stock: 516 - max_stock: 1000 - price: 185 - sale_gamma: 86 - service_level: 0.95 - SKU582: - constraint: G(stock_constraint) - cost: 359 - init_stock: 480 - max_stock: 1000 - price: 567 - sale_gamma: 96 - service_level: 0.95 - SKU583: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 86 - init_stock: 261 - max_stock: 1000 - price: 98 - sale_gamma: 87 - service_level: 0.95 - SKU584: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 33 - init_stock: 270 - max_stock: 1000 - price: 36 - sale_gamma: 45 - service_level: 0.95 - SKU585: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 53 - sale_gamma: 20 - service_level: 0.95 - SKU586: - constraint: G(stock_constraint) - cost: 61 - init_stock: 264 - max_stock: 1000 - price: 94 - sale_gamma: 44 - service_level: 0.95 - SKU587: - constraint: null - cost: 290 - init_stock: 468 - max_stock: 1000 - price: 423 - sale_gamma: 78 - service_level: 0.95 - SKU588: - constraint: G(stock_constraint) - cost: 476 - init_stock: 176 - max_stock: 1000 - price: 885 - sale_gamma: 44 - service_level: 0.95 - SKU589: - constraint: null - cost: 244 - init_stock: 170 - max_stock: 1000 - price: 380 - sale_gamma: 34 - service_level: 0.95 - SKU59: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 255 - max_stock: 1000 - price: 275 - sale_gamma: 51 - service_level: 0.95 - SKU590: - constraint: null - cost: 70 - init_stock: 93 - max_stock: 1000 - price: 103 - sale_gamma: 31 - service_level: 0.95 - SKU591: - constraint: G(stock_constraint) - cost: 206 - init_stock: 504 - max_stock: 1000 - price: 298 - sale_gamma: 84 - service_level: 0.95 - SKU592: - constraint: null - cost: 218 - init_stock: 276 - max_stock: 1000 - price: 418 - sale_gamma: 46 - service_level: 0.95 - SKU593: - constraint: null - cost: 124 - init_stock: 88 - max_stock: 1000 - price: 221 - sale_gamma: 44 - service_level: 0.95 - SKU594: - constraint: null - cost: 238 - init_stock: 150 - max_stock: 1000 - price: 459 - sale_gamma: 50 - service_level: 0.95 - SKU595: - constraint: null - cost: 73 - init_stock: 172 - max_stock: 1000 - price: 107 - sale_gamma: 43 - service_level: 0.95 - SKU596: - constraint: null - cost: 432 - init_stock: 35 - max_stock: 1000 - price: 540 - sale_gamma: 7 - service_level: 0.95 - SKU597: - constraint: G(stock_constraint) - cost: 252 - init_stock: 435 - max_stock: 1000 - price: 451 - sale_gamma: 87 - service_level: 0.95 - SKU598: - constraint: null - cost: 348 - init_stock: 28 - max_stock: 1000 - price: 612 - sale_gamma: 14 - service_level: 0.95 - SKU599: - constraint: null - cost: 54 - init_stock: 204 - max_stock: 1000 - price: 66 - sale_gamma: 68 - service_level: 0.95 - SKU6: - constraint: null - cost: 122 - init_stock: 616 - max_stock: 1000 - price: 195 - sale_gamma: 88 - service_level: 0.95 - SKU60: - constraint: G(stock_constraint) - cost: 200 - init_stock: 234 - max_stock: 1000 - price: 346 - sale_gamma: 39 - service_level: 0.95 - SKU600: - constraint: null - cost: 386 - init_stock: 325 - max_stock: 1000 - price: 505 - sale_gamma: 65 - service_level: 0.95 - SKU601: - constraint: null - cost: 138 - init_stock: 273 - max_stock: 1000 - price: 198 - sale_gamma: 39 - service_level: 0.95 - SKU602: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 184 - init_stock: 588 - max_stock: 1000 - price: 318 - sale_gamma: 98 - service_level: 0.95 - SKU603: - constraint: G(low_profit -> low_stock_constraint) - cost: 278 - init_stock: 252 - max_stock: 1000 - price: 550 - sale_gamma: 42 - service_level: 0.95 - SKU604: - constraint: G(stock_constraint) - cost: 270 - init_stock: 400 - max_stock: 1000 - price: 378 - sale_gamma: 50 - service_level: 0.95 - SKU605: - constraint: null - cost: 288 - init_stock: 120 - max_stock: 1000 - price: 443 - sale_gamma: 24 - service_level: 0.95 - SKU606: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 114 - init_stock: 66 - max_stock: 1000 - price: 161 - sale_gamma: 11 - service_level: 0.95 - SKU607: - constraint: null - cost: 208 - init_stock: 395 - max_stock: 1000 - price: 359 - sale_gamma: 79 - service_level: 0.95 - SKU608: - constraint: null - cost: 39 - init_stock: 365 - max_stock: 1000 - price: 49 - sale_gamma: 73 - service_level: 0.95 - SKU609: - constraint: null - cost: 102 - init_stock: 114 - max_stock: 1000 - price: 171 - sale_gamma: 19 - service_level: 0.95 - SKU61: - constraint: null - cost: 461 - init_stock: 150 - max_stock: 1000 - price: 645 - sale_gamma: 75 - service_level: 0.95 - SKU610: - constraint: null - cost: 449 - init_stock: 204 - max_stock: 1000 - price: 749 - sale_gamma: 68 - service_level: 0.95 - SKU611: - constraint: G(low_profit -> low_stock_constraint) - cost: 306 - init_stock: 539 - max_stock: 1000 - price: 489 - sale_gamma: 77 - service_level: 0.95 - SKU612: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 391 - init_stock: 616 - max_stock: 1000 - price: 613 - sale_gamma: 88 - service_level: 0.95 - SKU613: - constraint: G(stock_constraint) - cost: 174 - init_stock: 56 - max_stock: 1000 - price: 254 - sale_gamma: 7 - service_level: 0.95 - SKU614: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 45 - max_stock: 1000 - price: 318 - sale_gamma: 15 - service_level: 0.95 - SKU615: - constraint: G(stock_constraint) - cost: 330 - init_stock: 364 - max_stock: 1000 - price: 432 - sale_gamma: 91 - service_level: 0.95 - SKU616: - constraint: G(low_profit -> low_stock_constraint) - cost: 232 - init_stock: 219 - max_stock: 1000 - price: 382 - sale_gamma: 73 - service_level: 0.95 - SKU617: - constraint: null - cost: 203 - init_stock: 420 - max_stock: 1000 - price: 227 - sale_gamma: 60 - service_level: 0.95 - SKU618: - constraint: G(stock_constraint) - cost: 77 - init_stock: 138 - max_stock: 1000 - price: 97 - sale_gamma: 23 - service_level: 0.95 - SKU619: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 189 - init_stock: 344 - max_stock: 1000 - price: 359 - sale_gamma: 86 - service_level: 0.95 - SKU62: - constraint: null - cost: 124 - init_stock: 36 - max_stock: 1000 - price: 168 - sale_gamma: 9 - service_level: 0.95 - SKU620: - constraint: null - cost: 231 - init_stock: 156 - max_stock: 1000 - price: 267 - sale_gamma: 39 - service_level: 0.95 - SKU621: - constraint: null - cost: 199 - init_stock: 216 - max_stock: 1000 - price: 332 - sale_gamma: 72 - service_level: 0.95 - SKU622: - constraint: null - cost: 253 - init_stock: 455 - max_stock: 1000 - price: 468 - sale_gamma: 65 - service_level: 0.95 - SKU623: - constraint: null - cost: 335 - init_stock: 220 - max_stock: 1000 - price: 368 - sale_gamma: 55 - service_level: 0.95 - SKU624: - constraint: null - cost: 482 - init_stock: 270 - max_stock: 1000 - price: 824 - sale_gamma: 54 - service_level: 0.95 - SKU625: - constraint: null - cost: 46 - init_stock: 445 - max_stock: 1000 - price: 60 - sale_gamma: 89 - service_level: 0.95 - SKU626: - constraint: null - cost: 451 - init_stock: 534 - max_stock: 1000 - price: 694 - sale_gamma: 89 - service_level: 0.95 - SKU627: - constraint: null - cost: 220 - init_stock: 656 - max_stock: 1000 - price: 264 - sale_gamma: 82 - service_level: 0.95 - SKU628: - constraint: null - cost: 124 - init_stock: 156 - max_stock: 1000 - price: 229 - sale_gamma: 26 - service_level: 0.95 - SKU629: - constraint: G(stock_constraint) - cost: 353 - init_stock: 460 - max_stock: 1000 - price: 515 - sale_gamma: 92 - service_level: 0.95 - SKU63: - constraint: null - cost: 68 - init_stock: 70 - max_stock: 1000 - price: 86 - sale_gamma: 14 - service_level: 0.95 - SKU630: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 122 - init_stock: 34 - max_stock: 1000 - price: 137 - sale_gamma: 17 - service_level: 0.95 - SKU631: - constraint: null - cost: 296 - init_stock: 196 - max_stock: 1000 - price: 399 - sale_gamma: 49 - service_level: 0.95 - SKU632: - constraint: null - cost: 85 - init_stock: 470 - max_stock: 1000 - price: 141 - sale_gamma: 94 - service_level: 0.95 - SKU633: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 402 - max_stock: 1000 - price: 228 - sale_gamma: 67 - service_level: 0.95 - SKU634: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 17 - init_stock: 438 - max_stock: 1000 - price: 30 - sale_gamma: 73 - service_level: 0.95 - SKU635: - constraint: null - cost: 341 - init_stock: 372 - max_stock: 1000 - price: 654 - sale_gamma: 93 - service_level: 0.95 - SKU636: - constraint: G(stock_constraint) - cost: 385 - init_stock: 111 - max_stock: 1000 - price: 739 - sale_gamma: 37 - service_level: 0.95 - SKU637: - constraint: G(low_profit -> low_stock_constraint) - cost: 83 - init_stock: 305 - max_stock: 1000 - price: 118 - sale_gamma: 61 - service_level: 0.95 - SKU638: - constraint: G(stock_constraint) - cost: 375 - init_stock: 16 - max_stock: 1000 - price: 536 - sale_gamma: 8 - service_level: 0.95 - SKU639: - constraint: null - cost: 327 - init_stock: 160 - max_stock: 1000 - price: 487 - sale_gamma: 40 - service_level: 0.95 - SKU64: - constraint: G(low_profit -> low_stock_constraint) - cost: 36 - init_stock: 133 - max_stock: 1000 - price: 42 - sale_gamma: 19 - service_level: 0.95 - SKU640: - constraint: G(low_profit -> low_stock_constraint) - cost: 275 - init_stock: 546 - max_stock: 1000 - price: 382 - sale_gamma: 91 - service_level: 0.95 - SKU641: - constraint: null - cost: 365 - init_stock: 486 - max_stock: 1000 - price: 445 - sale_gamma: 81 - service_level: 0.95 - SKU642: - constraint: null - cost: 414 - init_stock: 150 - max_stock: 1000 - price: 554 - sale_gamma: 25 - service_level: 0.95 - SKU643: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 358 - init_stock: 322 - max_stock: 1000 - price: 461 - sale_gamma: 46 - service_level: 0.95 - SKU644: - constraint: G(low_profit -> low_stock_constraint) - cost: 46 - init_stock: 410 - max_stock: 1000 - price: 61 - sale_gamma: 82 - service_level: 0.95 - SKU645: - constraint: null - cost: 467 - init_stock: 594 - max_stock: 1000 - price: 742 - sale_gamma: 99 - service_level: 0.95 - SKU646: - constraint: G(low_profit -> low_stock_constraint) - cost: 189 - init_stock: 91 - max_stock: 1000 - price: 268 - sale_gamma: 13 - service_level: 0.95 - SKU647: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 303 - init_stock: 462 - max_stock: 1000 - price: 369 - sale_gamma: 77 - service_level: 0.95 - SKU648: - constraint: G(stock_constraint) - cost: 226 - init_stock: 110 - max_stock: 1000 - price: 336 - sale_gamma: 55 - service_level: 0.95 - SKU649: - constraint: null - cost: 198 - init_stock: 175 - max_stock: 1000 - price: 229 - sale_gamma: 25 - service_level: 0.95 - SKU65: - constraint: null - cost: 15 - init_stock: 658 - max_stock: 1000 - price: 25 - sale_gamma: 94 - service_level: 0.95 - SKU650: - constraint: null - cost: 351 - init_stock: 224 - max_stock: 1000 - price: 498 - sale_gamma: 56 - service_level: 0.95 - SKU651: - constraint: null - cost: 380 - init_stock: 672 - max_stock: 1000 - price: 596 - sale_gamma: 96 - service_level: 0.95 - SKU652: - constraint: null - cost: 123 - init_stock: 280 - max_stock: 1000 - price: 168 - sale_gamma: 40 - service_level: 0.95 - SKU653: - constraint: G(low_profit -> low_stock_constraint) - cost: 479 - init_stock: 384 - max_stock: 1000 - price: 661 - sale_gamma: 96 - service_level: 0.95 - SKU654: - constraint: null - cost: 66 - init_stock: 32 - max_stock: 1000 - price: 110 - sale_gamma: 8 - service_level: 0.95 - SKU655: - constraint: G(low_profit -> low_stock_constraint) - cost: 333 - init_stock: 126 - max_stock: 1000 - price: 479 - sale_gamma: 42 - service_level: 0.95 - SKU656: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 53 - init_stock: 504 - max_stock: 1000 - price: 81 - sale_gamma: 72 - service_level: 0.95 - SKU657: - constraint: G(low_profit -> low_stock_constraint) - cost: 73 - init_stock: 567 - max_stock: 1000 - price: 110 - sale_gamma: 81 - service_level: 0.95 - SKU658: - constraint: null - cost: 70 - init_stock: 252 - max_stock: 1000 - price: 107 - sale_gamma: 36 - service_level: 0.95 - SKU659: - constraint: null - cost: 21 - init_stock: 336 - max_stock: 1000 - price: 27 - sale_gamma: 56 - service_level: 0.95 - SKU66: - constraint: null - cost: 143 - init_stock: 360 - max_stock: 1000 - price: 230 - sale_gamma: 90 - service_level: 0.95 - SKU660: - constraint: null - cost: 487 - init_stock: 415 - max_stock: 1000 - price: 560 - sale_gamma: 83 - service_level: 0.95 - SKU661: - constraint: null - cost: 344 - init_stock: 296 - max_stock: 1000 - price: 581 - sale_gamma: 74 - service_level: 0.95 - SKU662: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 266 - max_stock: 1000 - price: 628 - sale_gamma: 38 - service_level: 0.95 - SKU663: - constraint: G(stock_constraint) - cost: 418 - init_stock: 320 - max_stock: 1000 - price: 518 - sale_gamma: 40 - service_level: 0.95 - SKU664: - constraint: G(stock_constraint) - cost: 351 - init_stock: 420 - max_stock: 1000 - price: 494 - sale_gamma: 84 - service_level: 0.95 - SKU665: - constraint: null - cost: 493 - init_stock: 344 - max_stock: 1000 - price: 734 - sale_gamma: 43 - service_level: 0.95 - SKU666: - constraint: G(stock_constraint) - cost: 341 - init_stock: 176 - max_stock: 1000 - price: 572 - sale_gamma: 88 - service_level: 0.95 - SKU667: - constraint: null - cost: 325 - init_stock: 52 - max_stock: 1000 - price: 562 - sale_gamma: 13 - service_level: 0.95 - SKU668: - constraint: G(low_profit -> low_stock_constraint) - cost: 286 - init_stock: 300 - max_stock: 1000 - price: 463 - sale_gamma: 60 - service_level: 0.95 - SKU669: - constraint: null - cost: 74 - init_stock: 96 - max_stock: 1000 - price: 83 - sale_gamma: 16 - service_level: 0.95 - SKU67: - constraint: G(low_profit -> low_stock_constraint) - cost: 247 - init_stock: 159 - max_stock: 1000 - price: 454 - sale_gamma: 53 - service_level: 0.95 - SKU670: - constraint: G(stock_constraint) - cost: 289 - init_stock: 258 - max_stock: 1000 - price: 430 - sale_gamma: 86 - service_level: 0.95 - SKU671: - constraint: G(stock_constraint) - cost: 267 - init_stock: 332 - max_stock: 1000 - price: 296 - sale_gamma: 83 - service_level: 0.95 - SKU672: - constraint: null - cost: 369 - init_stock: 78 - max_stock: 1000 - price: 420 - sale_gamma: 13 - service_level: 0.95 - SKU673: - constraint: G(low_profit -> low_stock_constraint) - cost: 322 - init_stock: 123 - max_stock: 1000 - price: 418 - sale_gamma: 41 - service_level: 0.95 - SKU674: - constraint: null - cost: 223 - init_stock: 66 - max_stock: 1000 - price: 263 - sale_gamma: 22 - service_level: 0.95 - SKU675: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 438 - init_stock: 99 - max_stock: 1000 - price: 529 - sale_gamma: 33 - service_level: 0.95 - SKU676: - constraint: null - cost: 244 - init_stock: 366 - max_stock: 1000 - price: 275 - sale_gamma: 61 - service_level: 0.95 - SKU677: - constraint: null - cost: 425 - init_stock: 176 - max_stock: 1000 - price: 658 - sale_gamma: 44 - service_level: 0.95 - SKU678: - constraint: null - cost: 168 - init_stock: 216 - max_stock: 1000 - price: 199 - sale_gamma: 36 - service_level: 0.95 - SKU679: - constraint: null - cost: 395 - init_stock: 132 - max_stock: 1000 - price: 734 - sale_gamma: 44 - service_level: 0.95 - SKU68: - constraint: G(stock_constraint) - cost: 169 - init_stock: 56 - max_stock: 1000 - price: 239 - sale_gamma: 14 - service_level: 0.95 - SKU680: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 260 - init_stock: 180 - max_stock: 1000 - price: 327 - sale_gamma: 30 - service_level: 0.95 - SKU681: - constraint: G(low_profit -> low_stock_constraint) - cost: 464 - init_stock: 776 - max_stock: 1000 - price: 510 - sale_gamma: 97 - service_level: 0.95 - SKU682: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 370 - init_stock: 318 - max_stock: 1000 - price: 407 - sale_gamma: 53 - service_level: 0.95 - SKU683: - constraint: G(low_profit -> low_stock_constraint) - cost: 327 - init_stock: 536 - max_stock: 1000 - price: 608 - sale_gamma: 67 - service_level: 0.95 - SKU684: - constraint: G(stock_constraint) - cost: 355 - init_stock: 158 - max_stock: 1000 - price: 401 - sale_gamma: 79 - service_level: 0.95 - SKU685: - constraint: null - cost: 422 - init_stock: 270 - max_stock: 1000 - price: 493 - sale_gamma: 45 - service_level: 0.95 - SKU686: - constraint: G(low_profit -> low_stock_constraint) - cost: 63 - init_stock: 378 - max_stock: 1000 - price: 122 - sale_gamma: 63 - service_level: 0.95 - SKU687: - constraint: G(low_profit -> low_stock_constraint) - cost: 34 - init_stock: 456 - max_stock: 1000 - price: 43 - sale_gamma: 76 - service_level: 0.95 - SKU688: - constraint: G(low_profit -> low_stock_constraint) - cost: 484 - init_stock: 462 - max_stock: 1000 - price: 759 - sale_gamma: 77 - service_level: 0.95 - SKU689: - constraint: G(stock_constraint) - cost: 499 - init_stock: 75 - max_stock: 1000 - price: 808 - sale_gamma: 15 - service_level: 0.95 - SKU69: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 176 - init_stock: 402 - max_stock: 1000 - price: 197 - sale_gamma: 67 - service_level: 0.95 - SKU690: - constraint: null - cost: 437 - init_stock: 415 - max_stock: 1000 - price: 498 - sale_gamma: 83 - service_level: 0.95 - SKU691: - constraint: null - cost: 17 - init_stock: 632 - max_stock: 1000 - price: 18 - sale_gamma: 79 - service_level: 0.95 - SKU692: - constraint: G(stock_constraint) - cost: 225 - init_stock: 195 - max_stock: 1000 - price: 258 - sale_gamma: 65 - service_level: 0.95 - SKU693: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 19 - init_stock: 244 - max_stock: 1000 - price: 21 - sale_gamma: 61 - service_level: 0.95 - SKU694: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 208 - init_stock: 300 - max_stock: 1000 - price: 386 - sale_gamma: 75 - service_level: 0.95 - SKU695: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 190 - init_stock: 49 - max_stock: 1000 - price: 361 - sale_gamma: 7 - service_level: 0.95 - SKU696: - constraint: G(low_profit -> low_stock_constraint) - cost: 348 - init_stock: 290 - max_stock: 1000 - price: 643 - sale_gamma: 58 - service_level: 0.95 - SKU697: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 455 - init_stock: 320 - max_stock: 1000 - price: 641 - sale_gamma: 80 - service_level: 0.95 - SKU698: - constraint: null - cost: 198 - init_stock: 488 - max_stock: 1000 - price: 316 - sale_gamma: 61 - service_level: 0.95 - SKU699: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 31 - init_stock: 100 - max_stock: 1000 - price: 58 - sale_gamma: 20 - service_level: 0.95 - SKU7: - constraint: null - cost: 101 - init_stock: 480 - max_stock: 1000 - price: 131 - sale_gamma: 96 - service_level: 0.95 - SKU70: - constraint: G(stock_constraint) - cost: 186 - init_stock: 140 - max_stock: 1000 - price: 288 - sale_gamma: 35 - service_level: 0.95 - SKU700: - constraint: G(low_profit -> low_stock_constraint) - cost: 74 - init_stock: 45 - max_stock: 1000 - price: 130 - sale_gamma: 9 - service_level: 0.95 - SKU701: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 399 - init_stock: 462 - max_stock: 1000 - price: 770 - sale_gamma: 77 - service_level: 0.95 - SKU702: - constraint: G(stock_constraint) - cost: 82 - init_stock: 204 - max_stock: 1000 - price: 163 - sale_gamma: 34 - service_level: 0.95 - SKU703: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 363 - init_stock: 76 - max_stock: 1000 - price: 602 - sale_gamma: 38 - service_level: 0.95 - SKU704: - constraint: G(stock_constraint) - cost: 384 - init_stock: 261 - max_stock: 1000 - price: 518 - sale_gamma: 87 - service_level: 0.95 - SKU705: - constraint: null - cost: 128 - init_stock: 378 - max_stock: 1000 - price: 236 - sale_gamma: 63 - service_level: 0.95 - SKU706: - constraint: null - cost: 182 - init_stock: 455 - max_stock: 1000 - price: 242 - sale_gamma: 91 - service_level: 0.95 - SKU707: - constraint: null - cost: 18 - init_stock: 276 - max_stock: 1000 - price: 35 - sale_gamma: 69 - service_level: 0.95 - SKU708: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 178 - init_stock: 112 - max_stock: 1000 - price: 334 - sale_gamma: 28 - service_level: 0.95 - SKU709: - constraint: G(stock_constraint) - cost: 102 - init_stock: 212 - max_stock: 1000 - price: 173 - sale_gamma: 53 - service_level: 0.95 - SKU71: - constraint: null - cost: 315 - init_stock: 225 - max_stock: 1000 - price: 589 - sale_gamma: 45 - service_level: 0.95 - SKU710: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 252 - init_stock: 95 - max_stock: 1000 - price: 337 - sale_gamma: 19 - service_level: 0.95 - SKU711: - constraint: null - cost: 281 - init_stock: 414 - max_stock: 1000 - price: 320 - sale_gamma: 69 - service_level: 0.95 - SKU712: - constraint: null - cost: 232 - init_stock: 77 - max_stock: 1000 - price: 438 - sale_gamma: 11 - service_level: 0.95 - SKU713: - constraint: null - cost: 285 - init_stock: 129 - max_stock: 1000 - price: 413 - sale_gamma: 43 - service_level: 0.95 - SKU714: - constraint: null - cost: 79 - init_stock: 287 - max_stock: 1000 - price: 106 - sale_gamma: 41 - service_level: 0.95 - SKU715: - constraint: G(stock_constraint) - cost: 234 - init_stock: 34 - max_stock: 1000 - price: 271 - sale_gamma: 17 - service_level: 0.95 - SKU716: - constraint: null - cost: 70 - init_stock: 450 - max_stock: 1000 - price: 123 - sale_gamma: 75 - service_level: 0.95 - SKU717: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 345 - init_stock: 56 - max_stock: 1000 - price: 382 - sale_gamma: 8 - service_level: 0.95 - SKU718: - constraint: null - cost: 357 - init_stock: 174 - max_stock: 1000 - price: 692 - sale_gamma: 58 - service_level: 0.95 - SKU719: - constraint: null - cost: 340 - init_stock: 455 - max_stock: 1000 - price: 384 - sale_gamma: 65 - service_level: 0.95 - SKU72: - constraint: null - cost: 458 - init_stock: 595 - max_stock: 1000 - price: 870 - sale_gamma: 85 - service_level: 0.95 - SKU720: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 503 - sale_gamma: 42 - service_level: 0.95 - SKU721: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 73 - init_stock: 44 - max_stock: 1000 - price: 143 - sale_gamma: 11 - service_level: 0.95 - SKU722: - constraint: G(stock_constraint) - cost: 392 - init_stock: 194 - max_stock: 1000 - price: 572 - sale_gamma: 97 - service_level: 0.95 - SKU723: - constraint: null - cost: 318 - init_stock: 356 - max_stock: 1000 - price: 442 - sale_gamma: 89 - service_level: 0.95 - SKU724: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 198 - max_stock: 1000 - price: 520 - sale_gamma: 33 - service_level: 0.95 - SKU725: - constraint: null - cost: 175 - init_stock: 148 - max_stock: 1000 - price: 309 - sale_gamma: 37 - service_level: 0.95 - SKU726: - constraint: G(low_profit -> low_stock_constraint) - cost: 458 - init_stock: 356 - max_stock: 1000 - price: 567 - sale_gamma: 89 - service_level: 0.95 - SKU727: - constraint: null - cost: 418 - init_stock: 285 - max_stock: 1000 - price: 526 - sale_gamma: 57 - service_level: 0.95 - SKU728: - constraint: null - cost: 475 - init_stock: 185 - max_stock: 1000 - price: 864 - sale_gamma: 37 - service_level: 0.95 - SKU729: - constraint: null - cost: 324 - init_stock: 252 - max_stock: 1000 - price: 476 - sale_gamma: 36 - service_level: 0.95 - SKU73: - constraint: null - cost: 212 - init_stock: 216 - max_stock: 1000 - price: 248 - sale_gamma: 54 - service_level: 0.95 - SKU730: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 16 - init_stock: 65 - max_stock: 1000 - price: 19 - sale_gamma: 13 - service_level: 0.95 - SKU731: - constraint: G(stock_constraint) - cost: 88 - init_stock: 410 - max_stock: 1000 - price: 155 - sale_gamma: 82 - service_level: 0.95 - SKU732: - constraint: null - cost: 41 - init_stock: 434 - max_stock: 1000 - price: 73 - sale_gamma: 62 - service_level: 0.95 - SKU733: - constraint: G(stock_constraint) - cost: 315 - init_stock: 104 - max_stock: 1000 - price: 541 - sale_gamma: 26 - service_level: 0.95 - SKU734: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 37 - init_stock: 255 - max_stock: 1000 - price: 71 - sale_gamma: 51 - service_level: 0.95 - SKU735: - constraint: null - cost: 266 - init_stock: 594 - max_stock: 1000 - price: 409 - sale_gamma: 99 - service_level: 0.95 - SKU736: - constraint: G(stock_constraint) - cost: 368 - init_stock: 75 - max_stock: 1000 - price: 632 - sale_gamma: 25 - service_level: 0.95 - SKU737: - constraint: null - cost: 475 - init_stock: 93 - max_stock: 1000 - price: 655 - sale_gamma: 31 - service_level: 0.95 - SKU738: - constraint: null - cost: 185 - init_stock: 192 - max_stock: 1000 - price: 246 - sale_gamma: 48 - service_level: 0.95 - SKU739: - constraint: G(low_profit -> low_stock_constraint) - cost: 475 - init_stock: 296 - max_stock: 1000 - price: 612 - sale_gamma: 74 - service_level: 0.95 - SKU74: - constraint: null - cost: 159 - init_stock: 168 - max_stock: 1000 - price: 189 - sale_gamma: 42 - service_level: 0.95 - SKU740: - constraint: null - cost: 390 - init_stock: 256 - max_stock: 1000 - price: 549 - sale_gamma: 64 - service_level: 0.95 - SKU741: - constraint: G(stock_constraint) - cost: 91 - init_stock: 280 - max_stock: 1000 - price: 112 - sale_gamma: 56 - service_level: 0.95 - SKU742: - constraint: G(stock_constraint) - cost: 188 - init_stock: 110 - max_stock: 1000 - price: 374 - sale_gamma: 22 - service_level: 0.95 - SKU743: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 217 - init_stock: 301 - max_stock: 1000 - price: 431 - sale_gamma: 43 - service_level: 0.95 - SKU744: - constraint: G(low_profit -> low_stock_constraint) - cost: 379 - init_stock: 290 - max_stock: 1000 - price: 579 - sale_gamma: 58 - service_level: 0.95 - SKU745: - constraint: G(low_profit -> low_stock_constraint) - cost: 316 - init_stock: 368 - max_stock: 1000 - price: 376 - sale_gamma: 92 - service_level: 0.95 - SKU746: - constraint: null - cost: 437 - init_stock: 160 - max_stock: 1000 - price: 624 - sale_gamma: 40 - service_level: 0.95 - SKU747: - constraint: null - cost: 373 - init_stock: 474 - max_stock: 1000 - price: 589 - sale_gamma: 79 - service_level: 0.95 - SKU748: - constraint: null - cost: 275 - init_stock: 330 - max_stock: 1000 - price: 464 - sale_gamma: 66 - service_level: 0.95 - SKU749: - constraint: null - cost: 394 - init_stock: 106 - max_stock: 1000 - price: 705 - sale_gamma: 53 - service_level: 0.95 - SKU75: - constraint: G(low_profit -> low_stock_constraint) - cost: 131 - init_stock: 76 - max_stock: 1000 - price: 217 - sale_gamma: 19 - service_level: 0.95 - SKU750: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 310 - max_stock: 1000 - price: 463 - sale_gamma: 62 - service_level: 0.95 - SKU751: - constraint: null - cost: 369 - init_stock: 325 - max_stock: 1000 - price: 428 - sale_gamma: 65 - service_level: 0.95 - SKU752: - constraint: null - cost: 259 - init_stock: 546 - max_stock: 1000 - price: 435 - sale_gamma: 78 - service_level: 0.95 - SKU753: - constraint: G(stock_constraint) - cost: 77 - init_stock: 325 - max_stock: 1000 - price: 128 - sale_gamma: 65 - service_level: 0.95 - SKU754: - constraint: G(stock_constraint) - cost: 387 - init_stock: 91 - max_stock: 1000 - price: 545 - sale_gamma: 13 - service_level: 0.95 - SKU755: - constraint: null - cost: 354 - init_stock: 560 - max_stock: 1000 - price: 523 - sale_gamma: 80 - service_level: 0.95 - SKU756: - constraint: G(stock_constraint) - cost: 246 - init_stock: 360 - max_stock: 1000 - price: 361 - sale_gamma: 90 - service_level: 0.95 - SKU757: - constraint: null - cost: 40 - init_stock: 399 - max_stock: 1000 - price: 76 - sale_gamma: 57 - service_level: 0.95 - SKU758: - constraint: null - cost: 241 - init_stock: 160 - max_stock: 1000 - price: 457 - sale_gamma: 40 - service_level: 0.95 - SKU759: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 435 - init_stock: 76 - max_stock: 1000 - price: 648 - sale_gamma: 38 - service_level: 0.95 - SKU76: - constraint: null - cost: 147 - init_stock: 256 - max_stock: 1000 - price: 191 - sale_gamma: 64 - service_level: 0.95 - SKU760: - constraint: null - cost: 176 - init_stock: 255 - max_stock: 1000 - price: 279 - sale_gamma: 51 - service_level: 0.95 - SKU761: - constraint: G(stock_constraint) - cost: 224 - init_stock: 275 - max_stock: 1000 - price: 250 - sale_gamma: 55 - service_level: 0.95 - SKU762: - constraint: G(stock_constraint) - cost: 264 - init_stock: 153 - max_stock: 1000 - price: 351 - sale_gamma: 51 - service_level: 0.95 - SKU763: - constraint: null - cost: 385 - init_stock: 316 - max_stock: 1000 - price: 677 - sale_gamma: 79 - service_level: 0.95 - SKU764: - constraint: null - cost: 349 - init_stock: 68 - max_stock: 1000 - price: 596 - sale_gamma: 34 - service_level: 0.95 - SKU765: - constraint: null - cost: 345 - init_stock: 52 - max_stock: 1000 - price: 472 - sale_gamma: 13 - service_level: 0.95 - SKU766: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 478 - init_stock: 140 - max_stock: 1000 - price: 855 - sale_gamma: 20 - service_level: 0.95 - SKU767: - constraint: null - cost: 95 - init_stock: 456 - max_stock: 1000 - price: 160 - sale_gamma: 76 - service_level: 0.95 - SKU768: - constraint: G(low_profit -> low_stock_constraint) - cost: 181 - init_stock: 644 - max_stock: 1000 - price: 244 - sale_gamma: 92 - service_level: 0.95 - SKU769: - constraint: null - cost: 24 - init_stock: 87 - max_stock: 1000 - price: 43 - sale_gamma: 29 - service_level: 0.95 - SKU77: - constraint: null - cost: 409 - init_stock: 144 - max_stock: 1000 - price: 687 - sale_gamma: 72 - service_level: 0.95 - SKU770: - constraint: null - cost: 150 - init_stock: 186 - max_stock: 1000 - price: 166 - sale_gamma: 62 - service_level: 0.95 - SKU771: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 101 - init_stock: 35 - max_stock: 1000 - price: 182 - sale_gamma: 7 - service_level: 0.95 - SKU772: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 256 - init_stock: 36 - max_stock: 1000 - price: 281 - sale_gamma: 6 - service_level: 0.95 - SKU773: - constraint: null - cost: 84 - init_stock: 368 - max_stock: 1000 - price: 117 - sale_gamma: 92 - service_level: 0.95 - SKU774: - constraint: null - cost: 447 - init_stock: 118 - max_stock: 1000 - price: 746 - sale_gamma: 59 - service_level: 0.95 - SKU775: - constraint: null - cost: 175 - init_stock: 688 - max_stock: 1000 - price: 192 - sale_gamma: 86 - service_level: 0.95 - SKU776: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 103 - init_stock: 425 - max_stock: 1000 - price: 172 - sale_gamma: 85 - service_level: 0.95 - SKU777: - constraint: null - cost: 292 - init_stock: 171 - max_stock: 1000 - price: 347 - sale_gamma: 57 - service_level: 0.95 - SKU778: - constraint: null - cost: 203 - init_stock: 40 - max_stock: 1000 - price: 288 - sale_gamma: 8 - service_level: 0.95 - SKU779: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 117 - init_stock: 215 - max_stock: 1000 - price: 186 - sale_gamma: 43 - service_level: 0.95 - SKU78: - constraint: null - cost: 234 - init_stock: 91 - max_stock: 1000 - price: 400 - sale_gamma: 13 - service_level: 0.95 - SKU780: - constraint: G(low_profit -> low_stock_constraint) - cost: 69 - init_stock: 410 - max_stock: 1000 - price: 102 - sale_gamma: 82 - service_level: 0.95 - SKU781: - constraint: null - cost: 372 - init_stock: 144 - max_stock: 1000 - price: 528 - sale_gamma: 36 - service_level: 0.95 - SKU782: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 70 - max_stock: 1000 - price: 35 - sale_gamma: 10 - service_level: 0.95 - SKU783: - constraint: null - cost: 87 - init_stock: 324 - max_stock: 1000 - price: 139 - sale_gamma: 81 - service_level: 0.95 - SKU784: - constraint: null - cost: 309 - init_stock: 312 - max_stock: 1000 - price: 577 - sale_gamma: 52 - service_level: 0.95 - SKU785: - constraint: null - cost: 191 - init_stock: 210 - max_stock: 1000 - price: 255 - sale_gamma: 70 - service_level: 0.95 - SKU786: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 91 - init_stock: 132 - max_stock: 1000 - price: 102 - sale_gamma: 22 - service_level: 0.95 - SKU787: - constraint: null - cost: 360 - init_stock: 366 - max_stock: 1000 - price: 658 - sale_gamma: 61 - service_level: 0.95 - SKU788: - constraint: G(stock_constraint) - cost: 351 - init_stock: 112 - max_stock: 1000 - price: 417 - sale_gamma: 28 - service_level: 0.95 - SKU789: - constraint: G(stock_constraint) - cost: 153 - init_stock: 232 - max_stock: 1000 - price: 298 - sale_gamma: 58 - service_level: 0.95 - SKU79: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 245 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU790: - constraint: null - cost: 417 - init_stock: 330 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU791: - constraint: G(stock_constraint) - cost: 134 - init_stock: 56 - max_stock: 1000 - price: 155 - sale_gamma: 28 - service_level: 0.95 - SKU792: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 84 - max_stock: 1000 - price: 494 - sale_gamma: 21 - service_level: 0.95 - SKU793: - constraint: null - cost: 195 - init_stock: 532 - max_stock: 1000 - price: 282 - sale_gamma: 76 - service_level: 0.95 - SKU794: - constraint: null - cost: 325 - init_stock: 400 - max_stock: 1000 - price: 454 - sale_gamma: 80 - service_level: 0.95 - SKU795: - constraint: null - cost: 276 - init_stock: 288 - max_stock: 1000 - price: 549 - sale_gamma: 48 - service_level: 0.95 - SKU796: - constraint: null - cost: 447 - init_stock: 294 - max_stock: 1000 - price: 885 - sale_gamma: 49 - service_level: 0.95 - SKU797: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 100 - init_stock: 234 - max_stock: 1000 - price: 119 - sale_gamma: 39 - service_level: 0.95 - SKU798: - constraint: null - cost: 426 - init_stock: 180 - max_stock: 1000 - price: 630 - sale_gamma: 30 - service_level: 0.95 - SKU799: - constraint: null - cost: 63 - init_stock: 21 - max_stock: 1000 - price: 78 - sale_gamma: 7 - service_level: 0.95 - SKU8: - constraint: G(stock_constraint) - cost: 125 - init_stock: 252 - max_stock: 1000 - price: 196 - sale_gamma: 63 - service_level: 0.95 - SKU80: - constraint: G(stock_constraint) - cost: 163 - init_stock: 420 - max_stock: 1000 - price: 270 - sale_gamma: 70 - service_level: 0.95 - SKU800: - constraint: G(low_profit -> low_stock_constraint) - cost: 298 - init_stock: 470 - max_stock: 1000 - price: 455 - sale_gamma: 94 - service_level: 0.95 - SKU801: - constraint: G(low_profit -> low_stock_constraint) - cost: 389 - init_stock: 216 - max_stock: 1000 - price: 672 - sale_gamma: 36 - service_level: 0.95 - SKU802: - constraint: G(stock_constraint) - cost: 173 - init_stock: 310 - max_stock: 1000 - price: 281 - sale_gamma: 62 - service_level: 0.95 - SKU803: - constraint: null - cost: 272 - init_stock: 95 - max_stock: 1000 - price: 544 - sale_gamma: 19 - service_level: 0.95 - SKU804: - constraint: null - cost: 390 - init_stock: 188 - max_stock: 1000 - price: 491 - sale_gamma: 47 - service_level: 0.95 - SKU805: - constraint: null - cost: 61 - init_stock: 132 - max_stock: 1000 - price: 118 - sale_gamma: 33 - service_level: 0.95 - SKU806: - constraint: null - cost: 158 - init_stock: 156 - max_stock: 1000 - price: 186 - sale_gamma: 52 - service_level: 0.95 - SKU807: - constraint: null - cost: 453 - init_stock: 182 - max_stock: 1000 - price: 656 - sale_gamma: 26 - service_level: 0.95 - SKU808: - constraint: G(stock_constraint) - cost: 249 - init_stock: 182 - max_stock: 1000 - price: 435 - sale_gamma: 91 - service_level: 0.95 - SKU809: - constraint: null - cost: 55 - init_stock: 390 - max_stock: 1000 - price: 69 - sale_gamma: 65 - service_level: 0.95 - SKU81: - constraint: G(low_profit -> low_stock_constraint) - cost: 182 - init_stock: 528 - max_stock: 1000 - price: 223 - sale_gamma: 66 - service_level: 0.95 - SKU810: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 276 - init_stock: 42 - max_stock: 1000 - price: 322 - sale_gamma: 6 - service_level: 0.95 - SKU811: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 254 - init_stock: 148 - max_stock: 1000 - price: 322 - sale_gamma: 37 - service_level: 0.95 - SKU812: - constraint: null - cost: 252 - init_stock: 320 - max_stock: 1000 - price: 337 - sale_gamma: 80 - service_level: 0.95 - SKU813: - constraint: G(stock_constraint) - cost: 253 - init_stock: 49 - max_stock: 1000 - price: 333 - sale_gamma: 7 - service_level: 0.95 - SKU814: - constraint: null - cost: 485 - init_stock: 194 - max_stock: 1000 - price: 921 - sale_gamma: 97 - service_level: 0.95 - SKU815: - constraint: null - cost: 390 - init_stock: 168 - max_stock: 1000 - price: 429 - sale_gamma: 24 - service_level: 0.95 - SKU816: - constraint: null - cost: 152 - init_stock: 455 - max_stock: 1000 - price: 174 - sale_gamma: 91 - service_level: 0.95 - SKU817: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 227 - init_stock: 25 - max_stock: 1000 - price: 401 - sale_gamma: 5 - service_level: 0.95 - SKU818: - constraint: null - cost: 354 - init_stock: 215 - max_stock: 1000 - price: 534 - sale_gamma: 43 - service_level: 0.95 - SKU819: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 302 - init_stock: 135 - max_stock: 1000 - price: 480 - sale_gamma: 27 - service_level: 0.95 - SKU82: - constraint: null - cost: 290 - init_stock: 140 - max_stock: 1000 - price: 469 - sale_gamma: 28 - service_level: 0.95 - SKU820: - constraint: null - cost: 264 - init_stock: 410 - max_stock: 1000 - price: 464 - sale_gamma: 82 - service_level: 0.95 - SKU821: - constraint: G(stock_constraint) - cost: 99 - init_stock: 207 - max_stock: 1000 - price: 151 - sale_gamma: 69 - service_level: 0.95 - SKU822: - constraint: null - cost: 136 - init_stock: 490 - max_stock: 1000 - price: 266 - sale_gamma: 70 - service_level: 0.95 - SKU823: - constraint: null - cost: 75 - init_stock: 84 - max_stock: 1000 - price: 116 - sale_gamma: 28 - service_level: 0.95 - SKU824: - constraint: G(low_profit -> low_stock_constraint) - cost: 170 - init_stock: 420 - max_stock: 1000 - price: 200 - sale_gamma: 70 - service_level: 0.95 - SKU825: - constraint: null - cost: 214 - init_stock: 45 - max_stock: 1000 - price: 359 - sale_gamma: 15 - service_level: 0.95 - SKU826: - constraint: G(stock_constraint) - cost: 386 - init_stock: 330 - max_stock: 1000 - price: 428 - sale_gamma: 55 - service_level: 0.95 - SKU827: - constraint: G(stock_constraint) - cost: 148 - init_stock: 448 - max_stock: 1000 - price: 208 - sale_gamma: 64 - service_level: 0.95 - SKU828: - constraint: null - cost: 400 - init_stock: 276 - max_stock: 1000 - price: 627 - sale_gamma: 69 - service_level: 0.95 - SKU829: - constraint: null - cost: 61 - init_stock: 370 - max_stock: 1000 - price: 83 - sale_gamma: 74 - service_level: 0.95 - SKU83: - constraint: G(low_profit -> low_stock_constraint) - cost: 296 - init_stock: 68 - max_stock: 1000 - price: 535 - sale_gamma: 17 - service_level: 0.95 - SKU830: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 167 - init_stock: 144 - max_stock: 1000 - price: 252 - sale_gamma: 24 - service_level: 0.95 - SKU831: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 116 - max_stock: 1000 - price: 353 - sale_gamma: 29 - service_level: 0.95 - SKU832: - constraint: null - cost: 33 - init_stock: 164 - max_stock: 1000 - price: 48 - sale_gamma: 82 - service_level: 0.95 - SKU833: - constraint: G(low_profit -> low_stock_constraint) - cost: 400 - init_stock: 392 - max_stock: 1000 - price: 756 - sale_gamma: 98 - service_level: 0.95 - SKU834: - constraint: G(low_profit -> low_stock_constraint) - cost: 422 - init_stock: 10 - max_stock: 1000 - price: 662 - sale_gamma: 5 - service_level: 0.95 - SKU835: - constraint: null - cost: 440 - init_stock: 156 - max_stock: 1000 - price: 532 - sale_gamma: 52 - service_level: 0.95 - SKU836: - constraint: G(stock_constraint) - cost: 323 - init_stock: 192 - max_stock: 1000 - price: 552 - sale_gamma: 96 - service_level: 0.95 - SKU837: - constraint: null - cost: 373 - init_stock: 156 - max_stock: 1000 - price: 607 - sale_gamma: 26 - service_level: 0.95 - SKU838: - constraint: null - cost: 456 - init_stock: 308 - max_stock: 1000 - price: 711 - sale_gamma: 77 - service_level: 0.95 - SKU839: - constraint: null - cost: 473 - init_stock: 360 - max_stock: 1000 - price: 695 - sale_gamma: 60 - service_level: 0.95 - SKU84: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 318 - init_stock: 294 - max_stock: 1000 - price: 594 - sale_gamma: 42 - service_level: 0.95 - SKU840: - constraint: G(low_profit -> low_stock_constraint) - cost: 266 - init_stock: 150 - max_stock: 1000 - price: 436 - sale_gamma: 50 - service_level: 0.95 - SKU841: - constraint: G(stock_constraint) - cost: 285 - init_stock: 595 - max_stock: 1000 - price: 538 - sale_gamma: 85 - service_level: 0.95 - SKU842: - constraint: G(low_profit -> low_stock_constraint) - cost: 41 - init_stock: 252 - max_stock: 1000 - price: 70 - sale_gamma: 84 - service_level: 0.95 - SKU843: - constraint: null - cost: 360 - init_stock: 432 - max_stock: 1000 - price: 694 - sale_gamma: 72 - service_level: 0.95 - SKU844: - constraint: G(low_profit -> low_stock_constraint) - cost: 51 - init_stock: 474 - max_stock: 1000 - price: 63 - sale_gamma: 79 - service_level: 0.95 - SKU845: - constraint: G(low_profit -> low_stock_constraint) - cost: 288 - init_stock: 176 - max_stock: 1000 - price: 558 - sale_gamma: 22 - service_level: 0.95 - SKU846: - constraint: null - cost: 485 - init_stock: 234 - max_stock: 1000 - price: 940 - sale_gamma: 39 - service_level: 0.95 - SKU847: - constraint: G(low_profit -> low_stock_constraint) - cost: 388 - init_stock: 546 - max_stock: 1000 - price: 519 - sale_gamma: 91 - service_level: 0.95 - SKU848: - constraint: null - cost: 306 - init_stock: 184 - max_stock: 1000 - price: 358 - sale_gamma: 46 - service_level: 0.95 - SKU849: - constraint: G(low_profit -> low_stock_constraint) - cost: 320 - init_stock: 160 - max_stock: 1000 - price: 425 - sale_gamma: 40 - service_level: 0.95 - SKU85: - constraint: G(low_profit -> low_stock_constraint) - cost: 442 - init_stock: 400 - max_stock: 1000 - price: 583 - sale_gamma: 100 - service_level: 0.95 - SKU850: - constraint: G(stock_constraint) - cost: 183 - init_stock: 342 - max_stock: 1000 - price: 206 - sale_gamma: 57 - service_level: 0.95 - SKU851: - constraint: null - cost: 53 - init_stock: 384 - max_stock: 1000 - price: 96 - sale_gamma: 64 - service_level: 0.95 - SKU852: - constraint: null - cost: 306 - init_stock: 162 - max_stock: 1000 - price: 483 - sale_gamma: 54 - service_level: 0.95 - SKU853: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 386 - init_stock: 280 - max_stock: 1000 - price: 443 - sale_gamma: 56 - service_level: 0.95 - SKU854: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 212 - init_stock: 390 - max_stock: 1000 - price: 290 - sale_gamma: 65 - service_level: 0.95 - SKU855: - constraint: null - cost: 76 - init_stock: 432 - max_stock: 1000 - price: 114 - sale_gamma: 72 - service_level: 0.95 - SKU856: - constraint: G(low_profit -> low_stock_constraint) - cost: 380 - init_stock: 224 - max_stock: 1000 - price: 627 - sale_gamma: 32 - service_level: 0.95 - SKU857: - constraint: G(stock_constraint) - cost: 462 - init_stock: 500 - max_stock: 1000 - price: 646 - sale_gamma: 100 - service_level: 0.95 - SKU858: - constraint: null - cost: 80 - init_stock: 120 - max_stock: 1000 - price: 158 - sale_gamma: 24 - service_level: 0.95 - SKU859: - constraint: G(low_profit -> low_stock_constraint) - cost: 215 - init_stock: 224 - max_stock: 1000 - price: 298 - sale_gamma: 28 - service_level: 0.95 - SKU86: - constraint: null - cost: 386 - init_stock: 595 - max_stock: 1000 - price: 447 - sale_gamma: 85 - service_level: 0.95 - SKU860: - constraint: G(low_profit -> low_stock_constraint) - cost: 313 - init_stock: 105 - max_stock: 1000 - price: 435 - sale_gamma: 15 - service_level: 0.95 - SKU861: - constraint: null - cost: 477 - init_stock: 52 - max_stock: 1000 - price: 944 - sale_gamma: 13 - service_level: 0.95 - SKU862: - constraint: null - cost: 240 - init_stock: 64 - max_stock: 1000 - price: 412 - sale_gamma: 16 - service_level: 0.95 - SKU863: - constraint: null - cost: 470 - init_stock: 372 - max_stock: 1000 - price: 911 - sale_gamma: 93 - service_level: 0.95 - SKU864: - constraint: null - cost: 203 - init_stock: 114 - max_stock: 1000 - price: 341 - sale_gamma: 19 - service_level: 0.95 - SKU865: - constraint: G(low_profit -> low_stock_constraint) - cost: 144 - init_stock: 152 - max_stock: 1000 - price: 253 - sale_gamma: 19 - service_level: 0.95 - SKU866: - constraint: null - cost: 172 - init_stock: 264 - max_stock: 1000 - price: 201 - sale_gamma: 88 - service_level: 0.95 - SKU867: - constraint: G(low_profit -> low_stock_constraint) - cost: 499 - init_stock: 500 - max_stock: 1000 - price: 698 - sale_gamma: 100 - service_level: 0.95 - SKU868: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 41 - init_stock: 150 - max_stock: 1000 - price: 56 - sale_gamma: 50 - service_level: 0.95 - SKU869: - constraint: null - cost: 97 - init_stock: 388 - max_stock: 1000 - price: 111 - sale_gamma: 97 - service_level: 0.95 - SKU87: - constraint: null - cost: 368 - init_stock: 207 - max_stock: 1000 - price: 563 - sale_gamma: 69 - service_level: 0.95 - SKU870: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 281 - init_stock: 335 - max_stock: 1000 - price: 446 - sale_gamma: 67 - service_level: 0.95 - SKU871: - constraint: null - cost: 101 - init_stock: 396 - max_stock: 1000 - price: 112 - sale_gamma: 99 - service_level: 0.95 - SKU872: - constraint: null - cost: 133 - init_stock: 160 - max_stock: 1000 - price: 195 - sale_gamma: 32 - service_level: 0.95 - SKU873: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 423 - init_stock: 155 - max_stock: 1000 - price: 829 - sale_gamma: 31 - service_level: 0.95 - SKU874: - constraint: G(low_profit -> low_stock_constraint) - cost: 469 - init_stock: 360 - max_stock: 1000 - price: 689 - sale_gamma: 60 - service_level: 0.95 - SKU875: - constraint: G(stock_constraint) - cost: 51 - init_stock: 138 - max_stock: 1000 - price: 57 - sale_gamma: 23 - service_level: 0.95 - SKU876: - constraint: null - cost: 303 - init_stock: 427 - max_stock: 1000 - price: 427 - sale_gamma: 61 - service_level: 0.95 - SKU877: - constraint: null - cost: 40 - init_stock: 245 - max_stock: 1000 - price: 70 - sale_gamma: 35 - service_level: 0.95 - SKU878: - constraint: G(low_profit -> low_stock_constraint) - cost: 27 - init_stock: 476 - max_stock: 1000 - price: 32 - sale_gamma: 68 - service_level: 0.95 - SKU879: - constraint: null - cost: 150 - init_stock: 300 - max_stock: 1000 - price: 198 - sale_gamma: 100 - service_level: 0.95 - SKU88: - constraint: G(low_profit -> low_stock_constraint) - cost: 471 - init_stock: 36 - max_stock: 1000 - price: 824 - sale_gamma: 12 - service_level: 0.95 - SKU880: - constraint: null - cost: 433 - init_stock: 155 - max_stock: 1000 - price: 532 - sale_gamma: 31 - service_level: 0.95 - SKU881: - constraint: null - cost: 431 - init_stock: 420 - max_stock: 1000 - price: 560 - sale_gamma: 70 - service_level: 0.95 - SKU882: - constraint: G(stock_constraint) - cost: 194 - init_stock: 80 - max_stock: 1000 - price: 314 - sale_gamma: 16 - service_level: 0.95 - SKU883: - constraint: null - cost: 284 - init_stock: 152 - max_stock: 1000 - price: 479 - sale_gamma: 38 - service_level: 0.95 - SKU884: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 139 - init_stock: 195 - max_stock: 1000 - price: 222 - sale_gamma: 39 - service_level: 0.95 - SKU885: - constraint: G(low_profit -> low_stock_constraint) - cost: 49 - init_stock: 189 - max_stock: 1000 - price: 58 - sale_gamma: 27 - service_level: 0.95 - SKU886: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 372 - init_stock: 584 - max_stock: 1000 - price: 602 - sale_gamma: 73 - service_level: 0.95 - SKU887: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 266 - init_stock: 174 - max_stock: 1000 - price: 494 - sale_gamma: 87 - service_level: 0.95 - SKU888: - constraint: G(low_profit -> low_stock_constraint) - cost: 143 - init_stock: 45 - max_stock: 1000 - price: 220 - sale_gamma: 9 - service_level: 0.95 - SKU889: - constraint: null - cost: 101 - init_stock: 147 - max_stock: 1000 - price: 199 - sale_gamma: 21 - service_level: 0.95 - SKU89: - constraint: null - cost: 138 - init_stock: 744 - max_stock: 1000 - price: 269 - sale_gamma: 93 - service_level: 0.95 - SKU890: - constraint: G(low_profit -> low_stock_constraint) - cost: 161 - init_stock: 600 - max_stock: 1000 - price: 247 - sale_gamma: 100 - service_level: 0.95 - SKU891: - constraint: null - cost: 356 - init_stock: 176 - max_stock: 1000 - price: 615 - sale_gamma: 22 - service_level: 0.95 - SKU892: - constraint: null - cost: 313 - init_stock: 552 - max_stock: 1000 - price: 516 - sale_gamma: 92 - service_level: 0.95 - SKU893: - constraint: null - cost: 229 - init_stock: 594 - max_stock: 1000 - price: 302 - sale_gamma: 99 - service_level: 0.95 - SKU894: - constraint: null - cost: 129 - init_stock: 222 - max_stock: 1000 - price: 176 - sale_gamma: 74 - service_level: 0.95 - SKU895: - constraint: null - cost: 230 - init_stock: 39 - max_stock: 1000 - price: 301 - sale_gamma: 13 - service_level: 0.95 - SKU896: - constraint: null - cost: 289 - init_stock: 400 - max_stock: 1000 - price: 465 - sale_gamma: 80 - service_level: 0.95 - SKU897: - constraint: null - cost: 393 - init_stock: 432 - max_stock: 1000 - price: 640 - sale_gamma: 72 - service_level: 0.95 - SKU898: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 477 - init_stock: 44 - max_stock: 1000 - price: 615 - sale_gamma: 11 - service_level: 0.95 - SKU899: - constraint: null - cost: 233 - init_stock: 240 - max_stock: 1000 - price: 309 - sale_gamma: 48 - service_level: 0.95 - SKU9: - constraint: null - cost: 166 - init_stock: 384 - max_stock: 1000 - price: 247 - sale_gamma: 96 - service_level: 0.95 - SKU90: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 454 - init_stock: 204 - max_stock: 1000 - price: 726 - sale_gamma: 51 - service_level: 0.95 - SKU900: - constraint: null - cost: 158 - init_stock: 165 - max_stock: 1000 - price: 263 - sale_gamma: 55 - service_level: 0.95 - SKU901: - constraint: G(stock_constraint) - cost: 215 - init_stock: 203 - max_stock: 1000 - price: 320 - sale_gamma: 29 - service_level: 0.95 - SKU902: - constraint: G(low_profit -> low_stock_constraint) - cost: 125 - init_stock: 560 - max_stock: 1000 - price: 205 - sale_gamma: 80 - service_level: 0.95 - SKU903: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 357 - init_stock: 266 - max_stock: 1000 - price: 517 - sale_gamma: 38 - service_level: 0.95 - SKU904: - constraint: G(low_profit -> low_stock_constraint) - cost: 496 - init_stock: 255 - max_stock: 1000 - price: 605 - sale_gamma: 51 - service_level: 0.95 - SKU905: - constraint: null - cost: 249 - init_stock: 217 - max_stock: 1000 - price: 383 - sale_gamma: 31 - service_level: 0.95 - SKU906: - constraint: G(low_profit -> low_stock_constraint) - cost: 166 - init_stock: 156 - max_stock: 1000 - price: 273 - sale_gamma: 52 - service_level: 0.95 - SKU907: - constraint: null - cost: 22 - init_stock: 498 - max_stock: 1000 - price: 33 - sale_gamma: 83 - service_level: 0.95 - SKU908: - constraint: null - cost: 408 - init_stock: 546 - max_stock: 1000 - price: 595 - sale_gamma: 78 - service_level: 0.95 - SKU909: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 482 - init_stock: 480 - max_stock: 1000 - price: 703 - sale_gamma: 96 - service_level: 0.95 - SKU91: - constraint: G(low_profit -> low_stock_constraint) - cost: 303 - init_stock: 48 - max_stock: 1000 - price: 463 - sale_gamma: 16 - service_level: 0.95 - SKU910: - constraint: null - cost: 226 - init_stock: 132 - max_stock: 1000 - price: 350 - sale_gamma: 33 - service_level: 0.95 - SKU911: - constraint: null - cost: 461 - init_stock: 210 - max_stock: 1000 - price: 686 - sale_gamma: 70 - service_level: 0.95 - SKU912: - constraint: null - cost: 236 - init_stock: 135 - max_stock: 1000 - price: 328 - sale_gamma: 27 - service_level: 0.95 - SKU913: - constraint: null - cost: 322 - init_stock: 184 - max_stock: 1000 - price: 518 - sale_gamma: 46 - service_level: 0.95 - SKU914: - constraint: null - cost: 272 - init_stock: 434 - max_stock: 1000 - price: 446 - sale_gamma: 62 - service_level: 0.95 - SKU915: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 337 - init_stock: 280 - max_stock: 1000 - price: 545 - sale_gamma: 56 - service_level: 0.95 - SKU916: - constraint: null - cost: 337 - init_stock: 534 - max_stock: 1000 - price: 647 - sale_gamma: 89 - service_level: 0.95 - SKU917: - constraint: G(low_profit -> low_stock_constraint) - cost: 258 - init_stock: 120 - max_stock: 1000 - price: 366 - sale_gamma: 30 - service_level: 0.95 - SKU918: - constraint: G(stock_constraint) - cost: 148 - init_stock: 330 - max_stock: 1000 - price: 224 - sale_gamma: 55 - service_level: 0.95 - SKU919: - constraint: G(stock_constraint) - cost: 393 - init_stock: 125 - max_stock: 1000 - price: 711 - sale_gamma: 25 - service_level: 0.95 - SKU92: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 262 - init_stock: 252 - max_stock: 1000 - price: 466 - sale_gamma: 63 - service_level: 0.95 - SKU920: - constraint: null - cost: 357 - init_stock: 344 - max_stock: 1000 - price: 696 - sale_gamma: 86 - service_level: 0.95 - SKU921: - constraint: G(low_profit -> low_stock_constraint) - cost: 227 - init_stock: 198 - max_stock: 1000 - price: 419 - sale_gamma: 66 - service_level: 0.95 - SKU922: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 112 - init_stock: 201 - max_stock: 1000 - price: 123 - sale_gamma: 67 - service_level: 0.95 - SKU923: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 496 - init_stock: 348 - max_stock: 1000 - price: 828 - sale_gamma: 58 - service_level: 0.95 - SKU924: - constraint: null - cost: 316 - init_stock: 425 - max_stock: 1000 - price: 423 - sale_gamma: 85 - service_level: 0.95 - SKU925: - constraint: null - cost: 360 - init_stock: 90 - max_stock: 1000 - price: 716 - sale_gamma: 15 - service_level: 0.95 - SKU926: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 360 - init_stock: 469 - max_stock: 1000 - price: 475 - sale_gamma: 67 - service_level: 0.95 - SKU927: - constraint: G(stock_constraint) - cost: 260 - init_stock: 147 - max_stock: 1000 - price: 439 - sale_gamma: 21 - service_level: 0.95 - SKU928: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 491 - init_stock: 415 - max_stock: 1000 - price: 869 - sale_gamma: 83 - service_level: 0.95 - SKU929: - constraint: null - cost: 359 - init_stock: 800 - max_stock: 1000 - price: 470 - sale_gamma: 100 - service_level: 0.95 - SKU93: - constraint: null - cost: 404 - init_stock: 268 - max_stock: 1000 - price: 557 - sale_gamma: 67 - service_level: 0.95 - SKU930: - constraint: G(low_profit -> low_stock_constraint) - cost: 198 - init_stock: 196 - max_stock: 1000 - price: 247 - sale_gamma: 28 - service_level: 0.95 - SKU931: - constraint: null - cost: 71 - init_stock: 56 - max_stock: 1000 - price: 101 - sale_gamma: 14 - service_level: 0.95 - SKU932: - constraint: null - cost: 163 - init_stock: 264 - max_stock: 1000 - price: 283 - sale_gamma: 66 - service_level: 0.95 - SKU933: - constraint: null - cost: 113 - init_stock: 156 - max_stock: 1000 - price: 179 - sale_gamma: 78 - service_level: 0.95 - SKU934: - constraint: G(stock_constraint) - cost: 219 - init_stock: 469 - max_stock: 1000 - price: 346 - sale_gamma: 67 - service_level: 0.95 - SKU935: - constraint: null - cost: 364 - init_stock: 376 - max_stock: 1000 - price: 527 - sale_gamma: 94 - service_level: 0.95 - SKU936: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 24 - init_stock: 20 - max_stock: 1000 - price: 41 - sale_gamma: 5 - service_level: 0.95 - SKU937: - constraint: G(stock_constraint) - cost: 135 - init_stock: 85 - max_stock: 1000 - price: 216 - sale_gamma: 17 - service_level: 0.95 - SKU938: - constraint: G(low_profit -> low_stock_constraint) - cost: 432 - init_stock: 63 - max_stock: 1000 - price: 704 - sale_gamma: 21 - service_level: 0.95 - SKU939: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 173 - init_stock: 413 - max_stock: 1000 - price: 219 - sale_gamma: 59 - service_level: 0.95 - SKU94: - constraint: G(low_profit -> low_stock_constraint) - cost: 184 - init_stock: 235 - max_stock: 1000 - price: 261 - sale_gamma: 47 - service_level: 0.95 - SKU940: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 14 - init_stock: 279 - max_stock: 1000 - price: 24 - sale_gamma: 93 - service_level: 0.95 - SKU941: - constraint: G(stock_constraint) - cost: 80 - init_stock: 456 - max_stock: 1000 - price: 132 - sale_gamma: 57 - service_level: 0.95 - SKU942: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 202 - init_stock: 65 - max_stock: 1000 - price: 303 - sale_gamma: 13 - service_level: 0.95 - SKU943: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 182 - sale_gamma: 49 - service_level: 0.95 - SKU944: - constraint: null - cost: 196 - init_stock: 132 - max_stock: 1000 - price: 356 - sale_gamma: 44 - service_level: 0.95 - SKU945: - constraint: null - cost: 141 - init_stock: 85 - max_stock: 1000 - price: 176 - sale_gamma: 17 - service_level: 0.95 - SKU946: - constraint: null - cost: 325 - init_stock: 294 - max_stock: 1000 - price: 555 - sale_gamma: 49 - service_level: 0.95 - SKU947: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 338 - init_stock: 637 - max_stock: 1000 - price: 547 - sale_gamma: 91 - service_level: 0.95 - SKU948: - constraint: null - cost: 425 - init_stock: 112 - max_stock: 1000 - price: 476 - sale_gamma: 28 - service_level: 0.95 - SKU949: - constraint: G(stock_constraint) - cost: 309 - init_stock: 15 - max_stock: 1000 - price: 522 - sale_gamma: 5 - service_level: 0.95 - SKU95: - constraint: null - cost: 136 - init_stock: 84 - max_stock: 1000 - price: 214 - sale_gamma: 21 - service_level: 0.95 - SKU950: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 102 - init_stock: 324 - max_stock: 1000 - price: 128 - sale_gamma: 54 - service_level: 0.95 - SKU951: - constraint: G(low_profit -> low_stock_constraint) - cost: 75 - init_stock: 90 - max_stock: 1000 - price: 117 - sale_gamma: 18 - service_level: 0.95 - SKU952: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 156 - init_stock: 66 - max_stock: 1000 - price: 276 - sale_gamma: 11 - service_level: 0.95 - SKU953: - constraint: null - cost: 138 - init_stock: 245 - max_stock: 1000 - price: 230 - sale_gamma: 35 - service_level: 0.95 - SKU954: - constraint: null - cost: 296 - init_stock: 430 - max_stock: 1000 - price: 444 - sale_gamma: 86 - service_level: 0.95 - SKU955: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 55 - init_stock: 189 - max_stock: 1000 - price: 85 - sale_gamma: 63 - service_level: 0.95 - SKU956: - constraint: null - cost: 282 - init_stock: 425 - max_stock: 1000 - price: 397 - sale_gamma: 85 - service_level: 0.95 - SKU957: - constraint: null - cost: 305 - init_stock: 306 - max_stock: 1000 - price: 466 - sale_gamma: 51 - service_level: 0.95 - SKU958: - constraint: null - cost: 369 - init_stock: 462 - max_stock: 1000 - price: 704 - sale_gamma: 66 - service_level: 0.95 - SKU959: - constraint: null - cost: 81 - init_stock: 164 - max_stock: 1000 - price: 127 - sale_gamma: 82 - service_level: 0.95 - SKU96: - constraint: G(low_profit -> low_stock_constraint) - cost: 176 - init_stock: 264 - max_stock: 1000 - price: 329 - sale_gamma: 44 - service_level: 0.95 - SKU960: - constraint: null - cost: 147 - init_stock: 42 - max_stock: 1000 - price: 235 - sale_gamma: 6 - service_level: 0.95 - SKU961: - constraint: null - cost: 264 - init_stock: 176 - max_stock: 1000 - price: 290 - sale_gamma: 44 - service_level: 0.95 - SKU962: - constraint: null - cost: 354 - init_stock: 176 - max_stock: 1000 - price: 392 - sale_gamma: 44 - service_level: 0.95 - SKU963: - constraint: null - cost: 349 - init_stock: 63 - max_stock: 1000 - price: 523 - sale_gamma: 21 - service_level: 0.95 - SKU964: - constraint: null - cost: 244 - init_stock: 219 - max_stock: 1000 - price: 480 - sale_gamma: 73 - service_level: 0.95 - SKU965: - constraint: G(stock_constraint) - cost: 124 - init_stock: 255 - max_stock: 1000 - price: 199 - sale_gamma: 51 - service_level: 0.95 - SKU966: - constraint: G(stock_constraint) - cost: 302 - init_stock: 308 - max_stock: 1000 - price: 474 - sale_gamma: 44 - service_level: 0.95 - SKU967: - constraint: G(low_profit -> low_stock_constraint) - cost: 67 - init_stock: 270 - max_stock: 1000 - price: 111 - sale_gamma: 45 - service_level: 0.95 - SKU968: - constraint: G(stock_constraint) - cost: 281 - init_stock: 294 - max_stock: 1000 - price: 528 - sale_gamma: 49 - service_level: 0.95 - SKU969: - constraint: G(low_profit -> low_stock_constraint) - cost: 249 - init_stock: 24 - max_stock: 1000 - price: 328 - sale_gamma: 6 - service_level: 0.95 - SKU97: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 28 - init_stock: 180 - max_stock: 1000 - price: 43 - sale_gamma: 30 - service_level: 0.95 - SKU970: - constraint: null - cost: 244 - init_stock: 30 - max_stock: 1000 - price: 373 - sale_gamma: 5 - service_level: 0.95 - SKU971: - constraint: null - cost: 368 - init_stock: 219 - max_stock: 1000 - price: 426 - sale_gamma: 73 - service_level: 0.95 - SKU972: - constraint: G(stock_constraint) - cost: 209 - init_stock: 345 - max_stock: 1000 - price: 307 - sale_gamma: 69 - service_level: 0.95 - SKU973: - constraint: null - cost: 271 - init_stock: 33 - max_stock: 1000 - price: 447 - sale_gamma: 11 - service_level: 0.95 - SKU974: - constraint: null - cost: 170 - init_stock: 192 - max_stock: 1000 - price: 285 - sale_gamma: 32 - service_level: 0.95 - SKU975: - constraint: G(stock_constraint) - cost: 198 - init_stock: 88 - max_stock: 1000 - price: 334 - sale_gamma: 22 - service_level: 0.95 - SKU976: - constraint: G(stock_constraint) - cost: 178 - init_stock: 75 - max_stock: 1000 - price: 323 - sale_gamma: 15 - service_level: 0.95 - SKU977: - constraint: G(stock_constraint) - cost: 234 - init_stock: 324 - max_stock: 1000 - price: 269 - sale_gamma: 54 - service_level: 0.95 - SKU978: - constraint: G(stock_constraint) - cost: 470 - init_stock: 320 - max_stock: 1000 - price: 921 - sale_gamma: 64 - service_level: 0.95 - SKU979: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 443 - init_stock: 480 - max_stock: 1000 - price: 753 - sale_gamma: 96 - service_level: 0.95 - SKU98: - constraint: G(stock_constraint) - cost: 443 - init_stock: 70 - max_stock: 1000 - price: 527 - sale_gamma: 35 - service_level: 0.95 - SKU980: - constraint: null - cost: 39 - init_stock: 340 - max_stock: 1000 - price: 60 - sale_gamma: 68 - service_level: 0.95 - SKU981: - constraint: null - cost: 482 - init_stock: 360 - max_stock: 1000 - price: 607 - sale_gamma: 72 - service_level: 0.95 - SKU982: - constraint: G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint))) - cost: 213 - init_stock: 208 - max_stock: 1000 - price: 374 - sale_gamma: 52 - service_level: 0.95 - SKU983: - constraint: G(low_profit -> low_stock_constraint) - cost: 449 - init_stock: 276 - max_stock: 1000 - price: 776 - sale_gamma: 92 - service_level: 0.95 - SKU984: - constraint: G(stock_constraint) - cost: 232 - init_stock: 637 - max_stock: 1000 - price: 327 - sale_gamma: 91 - service_level: 0.95 - SKU985: - constraint: null - cost: 290 - init_stock: 153 - max_stock: 1000 - price: 420 - sale_gamma: 51 - service_level: 0.95 - SKU986: - constraint: null - cost: 275 - init_stock: 136 - max_stock: 1000 - price: 313 - sale_gamma: 17 - service_level: 0.95 - SKU987: - constraint: null - cost: 434 - init_stock: 204 - max_stock: 1000 - price: 516 - sale_gamma: 34 - service_level: 0.95 - SKU988: - constraint: null - cost: 102 - init_stock: 69 - max_stock: 1000 - price: 166 - sale_gamma: 23 - service_level: 0.95 - SKU989: - constraint: null - cost: 484 - init_stock: 558 - max_stock: 1000 - price: 653 - sale_gamma: 93 - service_level: 0.95 - SKU99: - constraint: null - cost: 361 - init_stock: 225 - max_stock: 1000 - price: 581 - sale_gamma: 45 - service_level: 0.95 - SKU990: - constraint: G(low_profit -> low_stock_constraint) - cost: 108 - init_stock: 228 - max_stock: 1000 - price: 174 - sale_gamma: 57 - service_level: 0.95 - SKU991: - constraint: null - cost: 409 - init_stock: 152 - max_stock: 1000 - price: 576 - sale_gamma: 19 - service_level: 0.95 - SKU992: - constraint: null - cost: 434 - init_stock: 651 - max_stock: 1000 - price: 685 - sale_gamma: 93 - service_level: 0.95 - SKU993: - constraint: G(low_profit -> low_stock_constraint) - cost: 145 - init_stock: 602 - max_stock: 1000 - price: 201 - sale_gamma: 86 - service_level: 0.95 - SKU994: - constraint: G(low_profit -> low_stock_constraint) - cost: 470 - init_stock: 300 - max_stock: 1000 - price: 916 - sale_gamma: 50 - service_level: 0.95 - SKU995: - constraint: null - cost: 241 - init_stock: 532 - max_stock: 1000 - price: 310 - sale_gamma: 76 - service_level: 0.95 - SKU996: - constraint: G(stock_constraint) - cost: 260 - init_stock: 438 - max_stock: 1000 - price: 416 - sale_gamma: 73 - service_level: 0.95 - SKU997: - constraint: G(stock_constraint) - cost: 400 - init_stock: 102 - max_stock: 1000 - price: 727 - sale_gamma: 34 - service_level: 0.95 - SKU998: - constraint: G(low_profit -> low_stock_constraint) - cost: 447 - init_stock: 165 - max_stock: 1000 - price: 688 - sale_gamma: 55 - service_level: 0.95 - SKU999: - constraint: null - cost: 79 - init_stock: 161 - max_stock: 1000 - price: 86 - sale_gamma: 23 - service_level: 0.95 - grid: - facilities: - STORE0: - - 9 - - 8 - SUPPLIER0: - - 14 - - 16 - WAREHOUSE0: - - 16 - - 18 - size: - - 20 - - 20 - skus: - - id: 0 - name: SKU0 - - id: 1 - name: SKU1 - - id: 2 - name: SKU2 - - id: 3 - name: SKU3 - - id: 4 - name: SKU4 - - id: 5 - name: SKU5 - - id: 6 - name: SKU6 - - id: 7 - name: SKU7 - - id: 8 - name: SKU8 - - id: 9 - name: SKU9 - - id: 10 - name: SKU10 - - id: 11 - name: SKU11 - - id: 12 - name: SKU12 - - id: 13 - name: SKU13 - - id: 14 - name: SKU14 - - id: 15 - name: SKU15 - - id: 16 - name: SKU16 - - id: 17 - name: SKU17 - - id: 18 - name: SKU18 - - id: 19 - name: SKU19 - - id: 20 - name: SKU20 - - id: 21 - name: SKU21 - - id: 22 - name: SKU22 - - id: 23 - name: SKU23 - - id: 24 - name: SKU24 - - id: 25 - name: SKU25 - - id: 26 - name: SKU26 - - id: 27 - name: SKU27 - - id: 28 - name: SKU28 - - id: 29 - name: SKU29 - - id: 30 - name: SKU30 - - id: 31 - name: SKU31 - - id: 32 - name: SKU32 - - id: 33 - name: SKU33 - - id: 34 - name: SKU34 - - id: 35 - name: SKU35 - - id: 36 - name: SKU36 - - id: 37 - name: SKU37 - - id: 38 - name: SKU38 - - id: 39 - name: SKU39 - - id: 40 - name: SKU40 - - id: 41 - name: SKU41 - - id: 42 - name: SKU42 - - id: 43 - name: SKU43 - - id: 44 - name: SKU44 - - id: 45 - name: SKU45 - - id: 46 - name: SKU46 - - id: 47 - name: SKU47 - - id: 48 - name: SKU48 - - id: 49 - name: SKU49 - - id: 50 - name: SKU50 - - id: 51 - name: SKU51 - - id: 52 - name: SKU52 - - id: 53 - name: SKU53 - - id: 54 - name: SKU54 - - id: 55 - name: SKU55 - - id: 56 - name: SKU56 - - id: 57 - name: SKU57 - - id: 58 - name: SKU58 - - id: 59 - name: SKU59 - - id: 60 - name: SKU60 - - id: 61 - name: SKU61 - - id: 62 - name: SKU62 - - id: 63 - name: SKU63 - - id: 64 - name: SKU64 - - id: 65 - name: SKU65 - - id: 66 - name: SKU66 - - id: 67 - name: SKU67 - - id: 68 - name: SKU68 - - id: 69 - name: SKU69 - - id: 70 - name: SKU70 - - id: 71 - name: SKU71 - - id: 72 - name: SKU72 - - id: 73 - name: SKU73 - - id: 74 - name: SKU74 - - id: 75 - name: SKU75 - - id: 76 - name: SKU76 - - id: 77 - name: SKU77 - - id: 78 - name: SKU78 - - id: 79 - name: SKU79 - - id: 80 - name: SKU80 - - id: 81 - name: SKU81 - - id: 82 - name: SKU82 - - id: 83 - name: SKU83 - - id: 84 - name: SKU84 - - id: 85 - name: SKU85 - - id: 86 - name: SKU86 - - id: 87 - name: SKU87 - - id: 88 - name: SKU88 - - id: 89 - name: SKU89 - - id: 90 - name: SKU90 - - id: 91 - name: SKU91 - - id: 92 - name: SKU92 - - id: 93 - name: SKU93 - - id: 94 - name: SKU94 - - id: 95 - name: SKU95 - - id: 96 - name: SKU96 - - id: 97 - name: SKU97 - - id: 98 - name: SKU98 - - id: 99 - name: SKU99 - - id: 100 - name: SKU100 - - id: 101 - name: SKU101 - - id: 102 - name: SKU102 - - id: 103 - name: SKU103 - - id: 104 - name: SKU104 - - id: 105 - name: SKU105 - - id: 106 - name: SKU106 - - id: 107 - name: SKU107 - - id: 108 - name: SKU108 - - id: 109 - name: SKU109 - - id: 110 - name: SKU110 - - id: 111 - name: SKU111 - - id: 112 - name: SKU112 - - id: 113 - name: SKU113 - - id: 114 - name: SKU114 - - id: 115 - name: SKU115 - - id: 116 - name: SKU116 - - id: 117 - name: SKU117 - - id: 118 - name: SKU118 - - id: 119 - name: SKU119 - - id: 120 - name: SKU120 - - id: 121 - name: SKU121 - - id: 122 - name: SKU122 - - id: 123 - name: SKU123 - - id: 124 - name: SKU124 - - id: 125 - name: SKU125 - - id: 126 - name: SKU126 - - id: 127 - name: SKU127 - - id: 128 - name: SKU128 - - id: 129 - name: SKU129 - - id: 130 - name: SKU130 - - id: 131 - name: SKU131 - - id: 132 - name: SKU132 - - id: 133 - name: SKU133 - - id: 134 - name: SKU134 - - id: 135 - name: SKU135 - - id: 136 - name: SKU136 - - id: 137 - name: SKU137 - - id: 138 - name: SKU138 - - id: 139 - name: SKU139 - - id: 140 - name: SKU140 - - id: 141 - name: SKU141 - - id: 142 - name: SKU142 - - id: 143 - name: SKU143 - - id: 144 - name: SKU144 - - id: 145 - name: SKU145 - - id: 146 - name: SKU146 - - id: 147 - name: SKU147 - - id: 148 - name: SKU148 - - id: 149 - name: SKU149 - - id: 150 - name: SKU150 - - id: 151 - name: SKU151 - - id: 152 - name: SKU152 - - id: 153 - name: SKU153 - - id: 154 - name: SKU154 - - id: 155 - name: SKU155 - - id: 156 - name: SKU156 - - id: 157 - name: SKU157 - - id: 158 - name: SKU158 - - id: 159 - name: SKU159 - - id: 160 - name: SKU160 - - id: 161 - name: SKU161 - - id: 162 - name: SKU162 - - id: 163 - name: SKU163 - - id: 164 - name: SKU164 - - id: 165 - name: SKU165 - - id: 166 - name: SKU166 - - id: 167 - name: SKU167 - - id: 168 - name: SKU168 - - id: 169 - name: SKU169 - - id: 170 - name: SKU170 - - id: 171 - name: SKU171 - - id: 172 - name: SKU172 - - id: 173 - name: SKU173 - - id: 174 - name: SKU174 - - id: 175 - name: SKU175 - - id: 176 - name: SKU176 - - id: 177 - name: SKU177 - - id: 178 - name: SKU178 - - id: 179 - name: SKU179 - - id: 180 - name: SKU180 - - id: 181 - name: SKU181 - - id: 182 - name: SKU182 - - id: 183 - name: SKU183 - - id: 184 - name: SKU184 - - id: 185 - name: SKU185 - - id: 186 - name: SKU186 - - id: 187 - name: SKU187 - - id: 188 - name: SKU188 - - id: 189 - name: SKU189 - - id: 190 - name: SKU190 - - id: 191 - name: SKU191 - - id: 192 - name: SKU192 - - id: 193 - name: SKU193 - - id: 194 - name: SKU194 - - id: 195 - name: SKU195 - - id: 196 - name: SKU196 - - id: 197 - name: SKU197 - - id: 198 - name: SKU198 - - id: 199 - name: SKU199 - - id: 200 - name: SKU200 - - id: 201 - name: SKU201 - - id: 202 - name: SKU202 - - id: 203 - name: SKU203 - - id: 204 - name: SKU204 - - id: 205 - name: SKU205 - - id: 206 - name: SKU206 - - id: 207 - name: SKU207 - - id: 208 - name: SKU208 - - id: 209 - name: SKU209 - - id: 210 - name: SKU210 - - id: 211 - name: SKU211 - - id: 212 - name: SKU212 - - id: 213 - name: SKU213 - - id: 214 - name: SKU214 - - id: 215 - name: SKU215 - - id: 216 - name: SKU216 - - id: 217 - name: SKU217 - - id: 218 - name: SKU218 - - id: 219 - name: SKU219 - - id: 220 - name: SKU220 - - id: 221 - name: SKU221 - - id: 222 - name: SKU222 - - id: 223 - name: SKU223 - - id: 224 - name: SKU224 - - id: 225 - name: SKU225 - - id: 226 - name: SKU226 - - id: 227 - name: SKU227 - - id: 228 - name: SKU228 - - id: 229 - name: SKU229 - - id: 230 - name: SKU230 - - id: 231 - name: SKU231 - - id: 232 - name: SKU232 - - id: 233 - name: SKU233 - - id: 234 - name: SKU234 - - id: 235 - name: SKU235 - - id: 236 - name: SKU236 - - id: 237 - name: SKU237 - - id: 238 - name: SKU238 - - id: 239 - name: SKU239 - - id: 240 - name: SKU240 - - id: 241 - name: SKU241 - - id: 242 - name: SKU242 - - id: 243 - name: SKU243 - - id: 244 - name: SKU244 - - id: 245 - name: SKU245 - - id: 246 - name: SKU246 - - id: 247 - name: SKU247 - - id: 248 - name: SKU248 - - id: 249 - name: SKU249 - - id: 250 - name: SKU250 - - id: 251 - name: SKU251 - - id: 252 - name: SKU252 - - id: 253 - name: SKU253 - - id: 254 - name: SKU254 - - id: 255 - name: SKU255 - - id: 256 - name: SKU256 - - id: 257 - name: SKU257 - - id: 258 - name: SKU258 - - id: 259 - name: SKU259 - - id: 260 - name: SKU260 - - id: 261 - name: SKU261 - - id: 262 - name: SKU262 - - id: 263 - name: SKU263 - - id: 264 - name: SKU264 - - id: 265 - name: SKU265 - - id: 266 - name: SKU266 - - id: 267 - name: SKU267 - - id: 268 - name: SKU268 - - id: 269 - name: SKU269 - - id: 270 - name: SKU270 - - id: 271 - name: SKU271 - - id: 272 - name: SKU272 - - id: 273 - name: SKU273 - - id: 274 - name: SKU274 - - id: 275 - name: SKU275 - - id: 276 - name: SKU276 - - id: 277 - name: SKU277 - - id: 278 - name: SKU278 - - id: 279 - name: SKU279 - - id: 280 - name: SKU280 - - id: 281 - name: SKU281 - - id: 282 - name: SKU282 - - id: 283 - name: SKU283 - - id: 284 - name: SKU284 - - id: 285 - name: SKU285 - - id: 286 - name: SKU286 - - id: 287 - name: SKU287 - - id: 288 - name: SKU288 - - id: 289 - name: SKU289 - - id: 290 - name: SKU290 - - id: 291 - name: SKU291 - - id: 292 - name: SKU292 - - id: 293 - name: SKU293 - - id: 294 - name: SKU294 - - id: 295 - name: SKU295 - - id: 296 - name: SKU296 - - id: 297 - name: SKU297 - - id: 298 - name: SKU298 - - id: 299 - name: SKU299 - - id: 300 - name: SKU300 - - id: 301 - name: SKU301 - - id: 302 - name: SKU302 - - id: 303 - name: SKU303 - - id: 304 - name: SKU304 - - id: 305 - name: SKU305 - - id: 306 - name: SKU306 - - id: 307 - name: SKU307 - - id: 308 - name: SKU308 - - id: 309 - name: SKU309 - - id: 310 - name: SKU310 - - id: 311 - name: SKU311 - - id: 312 - name: SKU312 - - id: 313 - name: SKU313 - - id: 314 - name: SKU314 - - id: 315 - name: SKU315 - - id: 316 - name: SKU316 - - id: 317 - name: SKU317 - - id: 318 - name: SKU318 - - id: 319 - name: SKU319 - - id: 320 - name: SKU320 - - id: 321 - name: SKU321 - - id: 322 - name: SKU322 - - id: 323 - name: SKU323 - - id: 324 - name: SKU324 - - id: 325 - name: SKU325 - - id: 326 - name: SKU326 - - id: 327 - name: SKU327 - - id: 328 - name: SKU328 - - id: 329 - name: SKU329 - - id: 330 - name: SKU330 - - id: 331 - name: SKU331 - - id: 332 - name: SKU332 - - id: 333 - name: SKU333 - - id: 334 - name: SKU334 - - id: 335 - name: SKU335 - - id: 336 - name: SKU336 - - id: 337 - name: SKU337 - - id: 338 - name: SKU338 - - id: 339 - name: SKU339 - - id: 340 - name: SKU340 - - id: 341 - name: SKU341 - - id: 342 - name: SKU342 - - id: 343 - name: SKU343 - - id: 344 - name: SKU344 - - id: 345 - name: SKU345 - - id: 346 - name: SKU346 - - id: 347 - name: SKU347 - - id: 348 - name: SKU348 - - id: 349 - name: SKU349 - - id: 350 - name: SKU350 - - id: 351 - name: SKU351 - - id: 352 - name: SKU352 - - id: 353 - name: SKU353 - - id: 354 - name: SKU354 - - id: 355 - name: SKU355 - - id: 356 - name: SKU356 - - id: 357 - name: SKU357 - - id: 358 - name: SKU358 - - id: 359 - name: SKU359 - - id: 360 - name: SKU360 - - id: 361 - name: SKU361 - - id: 362 - name: SKU362 - - id: 363 - name: SKU363 - - id: 364 - name: SKU364 - - id: 365 - name: SKU365 - - id: 366 - name: SKU366 - - id: 367 - name: SKU367 - - id: 368 - name: SKU368 - - id: 369 - name: SKU369 - - id: 370 - name: SKU370 - - id: 371 - name: SKU371 - - id: 372 - name: SKU372 - - id: 373 - name: SKU373 - - id: 374 - name: SKU374 - - id: 375 - name: SKU375 - - id: 376 - name: SKU376 - - id: 377 - name: SKU377 - - id: 378 - name: SKU378 - - id: 379 - name: SKU379 - - id: 380 - name: SKU380 - - id: 381 - name: SKU381 - - id: 382 - name: SKU382 - - id: 383 - name: SKU383 - - id: 384 - name: SKU384 - - id: 385 - name: SKU385 - - id: 386 - name: SKU386 - - id: 387 - name: SKU387 - - id: 388 - name: SKU388 - - id: 389 - name: SKU389 - - id: 390 - name: SKU390 - - id: 391 - name: SKU391 - - id: 392 - name: SKU392 - - id: 393 - name: SKU393 - - id: 394 - name: SKU394 - - id: 395 - name: SKU395 - - id: 396 - name: SKU396 - - id: 397 - name: SKU397 - - id: 398 - name: SKU398 - - id: 399 - name: SKU399 - - id: 400 - name: SKU400 - - id: 401 - name: SKU401 - - id: 402 - name: SKU402 - - id: 403 - name: SKU403 - - id: 404 - name: SKU404 - - id: 405 - name: SKU405 - - id: 406 - name: SKU406 - - id: 407 - name: SKU407 - - id: 408 - name: SKU408 - - id: 409 - name: SKU409 - - id: 410 - name: SKU410 - - id: 411 - name: SKU411 - - id: 412 - name: SKU412 - - id: 413 - name: SKU413 - - id: 414 - name: SKU414 - - id: 415 - name: SKU415 - - id: 416 - name: SKU416 - - id: 417 - name: SKU417 - - id: 418 - name: SKU418 - - id: 419 - name: SKU419 - - id: 420 - name: SKU420 - - id: 421 - name: SKU421 - - id: 422 - name: SKU422 - - id: 423 - name: SKU423 - - id: 424 - name: SKU424 - - id: 425 - name: SKU425 - - id: 426 - name: SKU426 - - id: 427 - name: SKU427 - - id: 428 - name: SKU428 - - id: 429 - name: SKU429 - - id: 430 - name: SKU430 - - id: 431 - name: SKU431 - - id: 432 - name: SKU432 - - id: 433 - name: SKU433 - - id: 434 - name: SKU434 - - id: 435 - name: SKU435 - - id: 436 - name: SKU436 - - id: 437 - name: SKU437 - - id: 438 - name: SKU438 - - id: 439 - name: SKU439 - - id: 440 - name: SKU440 - - id: 441 - name: SKU441 - - id: 442 - name: SKU442 - - id: 443 - name: SKU443 - - id: 444 - name: SKU444 - - id: 445 - name: SKU445 - - id: 446 - name: SKU446 - - id: 447 - name: SKU447 - - id: 448 - name: SKU448 - - id: 449 - name: SKU449 - - id: 450 - name: SKU450 - - id: 451 - name: SKU451 - - id: 452 - name: SKU452 - - id: 453 - name: SKU453 - - id: 454 - name: SKU454 - - id: 455 - name: SKU455 - - id: 456 - name: SKU456 - - id: 457 - name: SKU457 - - id: 458 - name: SKU458 - - id: 459 - name: SKU459 - - id: 460 - name: SKU460 - - id: 461 - name: SKU461 - - id: 462 - name: SKU462 - - id: 463 - name: SKU463 - - id: 464 - name: SKU464 - - id: 465 - name: SKU465 - - id: 466 - name: SKU466 - - id: 467 - name: SKU467 - - id: 468 - name: SKU468 - - id: 469 - name: SKU469 - - id: 470 - name: SKU470 - - id: 471 - name: SKU471 - - id: 472 - name: SKU472 - - id: 473 - name: SKU473 - - id: 474 - name: SKU474 - - id: 475 - name: SKU475 - - id: 476 - name: SKU476 - - id: 477 - name: SKU477 - - id: 478 - name: SKU478 - - id: 479 - name: SKU479 - - id: 480 - name: SKU480 - - id: 481 - name: SKU481 - - id: 482 - name: SKU482 - - id: 483 - name: SKU483 - - id: 484 - name: SKU484 - - id: 485 - name: SKU485 - - id: 486 - name: SKU486 - - id: 487 - name: SKU487 - - id: 488 - name: SKU488 - - id: 489 - name: SKU489 - - id: 490 - name: SKU490 - - id: 491 - name: SKU491 - - id: 492 - name: SKU492 - - id: 493 - name: SKU493 - - id: 494 - name: SKU494 - - id: 495 - name: SKU495 - - id: 496 - name: SKU496 - - id: 497 - name: SKU497 - - id: 498 - name: SKU498 - - id: 499 - name: SKU499 - - id: 500 - name: SKU500 - - id: 501 - name: SKU501 - - id: 502 - name: SKU502 - - id: 503 - name: SKU503 - - id: 504 - name: SKU504 - - id: 505 - name: SKU505 - - id: 506 - name: SKU506 - - id: 507 - name: SKU507 - - id: 508 - name: SKU508 - - id: 509 - name: SKU509 - - id: 510 - name: SKU510 - - id: 511 - name: SKU511 - - id: 512 - name: SKU512 - - id: 513 - name: SKU513 - - id: 514 - name: SKU514 - - id: 515 - name: SKU515 - - id: 516 - name: SKU516 - - id: 517 - name: SKU517 - - id: 518 - name: SKU518 - - id: 519 - name: SKU519 - - id: 520 - name: SKU520 - - id: 521 - name: SKU521 - - id: 522 - name: SKU522 - - id: 523 - name: SKU523 - - id: 524 - name: SKU524 - - id: 525 - name: SKU525 - - id: 526 - name: SKU526 - - id: 527 - name: SKU527 - - id: 528 - name: SKU528 - - id: 529 - name: SKU529 - - id: 530 - name: SKU530 - - id: 531 - name: SKU531 - - id: 532 - name: SKU532 - - id: 533 - name: SKU533 - - id: 534 - name: SKU534 - - id: 535 - name: SKU535 - - id: 536 - name: SKU536 - - id: 537 - name: SKU537 - - id: 538 - name: SKU538 - - id: 539 - name: SKU539 - - id: 540 - name: SKU540 - - id: 541 - name: SKU541 - - id: 542 - name: SKU542 - - id: 543 - name: SKU543 - - id: 544 - name: SKU544 - - id: 545 - name: SKU545 - - id: 546 - name: SKU546 - - id: 547 - name: SKU547 - - id: 548 - name: SKU548 - - id: 549 - name: SKU549 - - id: 550 - name: SKU550 - - id: 551 - name: SKU551 - - id: 552 - name: SKU552 - - id: 553 - name: SKU553 - - id: 554 - name: SKU554 - - id: 555 - name: SKU555 - - id: 556 - name: SKU556 - - id: 557 - name: SKU557 - - id: 558 - name: SKU558 - - id: 559 - name: SKU559 - - id: 560 - name: SKU560 - - id: 561 - name: SKU561 - - id: 562 - name: SKU562 - - id: 563 - name: SKU563 - - id: 564 - name: SKU564 - - id: 565 - name: SKU565 - - id: 566 - name: SKU566 - - id: 567 - name: SKU567 - - id: 568 - name: SKU568 - - id: 569 - name: SKU569 - - id: 570 - name: SKU570 - - id: 571 - name: SKU571 - - id: 572 - name: SKU572 - - id: 573 - name: SKU573 - - id: 574 - name: SKU574 - - id: 575 - name: SKU575 - - id: 576 - name: SKU576 - - id: 577 - name: SKU577 - - id: 578 - name: SKU578 - - id: 579 - name: SKU579 - - id: 580 - name: SKU580 - - id: 581 - name: SKU581 - - id: 582 - name: SKU582 - - id: 583 - name: SKU583 - - id: 584 - name: SKU584 - - id: 585 - name: SKU585 - - id: 586 - name: SKU586 - - id: 587 - name: SKU587 - - id: 588 - name: SKU588 - - id: 589 - name: SKU589 - - id: 590 - name: SKU590 - - id: 591 - name: SKU591 - - id: 592 - name: SKU592 - - id: 593 - name: SKU593 - - id: 594 - name: SKU594 - - id: 595 - name: SKU595 - - id: 596 - name: SKU596 - - id: 597 - name: SKU597 - - id: 598 - name: SKU598 - - id: 599 - name: SKU599 - - id: 600 - name: SKU600 - - id: 601 - name: SKU601 - - id: 602 - name: SKU602 - - id: 603 - name: SKU603 - - id: 604 - name: SKU604 - - id: 605 - name: SKU605 - - id: 606 - name: SKU606 - - id: 607 - name: SKU607 - - id: 608 - name: SKU608 - - id: 609 - name: SKU609 - - id: 610 - name: SKU610 - - id: 611 - name: SKU611 - - id: 612 - name: SKU612 - - id: 613 - name: SKU613 - - id: 614 - name: SKU614 - - id: 615 - name: SKU615 - - id: 616 - name: SKU616 - - id: 617 - name: SKU617 - - id: 618 - name: SKU618 - - id: 619 - name: SKU619 - - id: 620 - name: SKU620 - - id: 621 - name: SKU621 - - id: 622 - name: SKU622 - - id: 623 - name: SKU623 - - id: 624 - name: SKU624 - - id: 625 - name: SKU625 - - id: 626 - name: SKU626 - - id: 627 - name: SKU627 - - id: 628 - name: SKU628 - - id: 629 - name: SKU629 - - id: 630 - name: SKU630 - - id: 631 - name: SKU631 - - id: 632 - name: SKU632 - - id: 633 - name: SKU633 - - id: 634 - name: SKU634 - - id: 635 - name: SKU635 - - id: 636 - name: SKU636 - - id: 637 - name: SKU637 - - id: 638 - name: SKU638 - - id: 639 - name: SKU639 - - id: 640 - name: SKU640 - - id: 641 - name: SKU641 - - id: 642 - name: SKU642 - - id: 643 - name: SKU643 - - id: 644 - name: SKU644 - - id: 645 - name: SKU645 - - id: 646 - name: SKU646 - - id: 647 - name: SKU647 - - id: 648 - name: SKU648 - - id: 649 - name: SKU649 - - id: 650 - name: SKU650 - - id: 651 - name: SKU651 - - id: 652 - name: SKU652 - - id: 653 - name: SKU653 - - id: 654 - name: SKU654 - - id: 655 - name: SKU655 - - id: 656 - name: SKU656 - - id: 657 - name: SKU657 - - id: 658 - name: SKU658 - - id: 659 - name: SKU659 - - id: 660 - name: SKU660 - - id: 661 - name: SKU661 - - id: 662 - name: SKU662 - - id: 663 - name: SKU663 - - id: 664 - name: SKU664 - - id: 665 - name: SKU665 - - id: 666 - name: SKU666 - - id: 667 - name: SKU667 - - id: 668 - name: SKU668 - - id: 669 - name: SKU669 - - id: 670 - name: SKU670 - - id: 671 - name: SKU671 - - id: 672 - name: SKU672 - - id: 673 - name: SKU673 - - id: 674 - name: SKU674 - - id: 675 - name: SKU675 - - id: 676 - name: SKU676 - - id: 677 - name: SKU677 - - id: 678 - name: SKU678 - - id: 679 - name: SKU679 - - id: 680 - name: SKU680 - - id: 681 - name: SKU681 - - id: 682 - name: SKU682 - - id: 683 - name: SKU683 - - id: 684 - name: SKU684 - - id: 685 - name: SKU685 - - id: 686 - name: SKU686 - - id: 687 - name: SKU687 - - id: 688 - name: SKU688 - - id: 689 - name: SKU689 - - id: 690 - name: SKU690 - - id: 691 - name: SKU691 - - id: 692 - name: SKU692 - - id: 693 - name: SKU693 - - id: 694 - name: SKU694 - - id: 695 - name: SKU695 - - id: 696 - name: SKU696 - - id: 697 - name: SKU697 - - id: 698 - name: SKU698 - - id: 699 - name: SKU699 - - id: 700 - name: SKU700 - - id: 701 - name: SKU701 - - id: 702 - name: SKU702 - - id: 703 - name: SKU703 - - id: 704 - name: SKU704 - - id: 705 - name: SKU705 - - id: 706 - name: SKU706 - - id: 707 - name: SKU707 - - id: 708 - name: SKU708 - - id: 709 - name: SKU709 - - id: 710 - name: SKU710 - - id: 711 - name: SKU711 - - id: 712 - name: SKU712 - - id: 713 - name: SKU713 - - id: 714 - name: SKU714 - - id: 715 - name: SKU715 - - id: 716 - name: SKU716 - - id: 717 - name: SKU717 - - id: 718 - name: SKU718 - - id: 719 - name: SKU719 - - id: 720 - name: SKU720 - - id: 721 - name: SKU721 - - id: 722 - name: SKU722 - - id: 723 - name: SKU723 - - id: 724 - name: SKU724 - - id: 725 - name: SKU725 - - id: 726 - name: SKU726 - - id: 727 - name: SKU727 - - id: 728 - name: SKU728 - - id: 729 - name: SKU729 - - id: 730 - name: SKU730 - - id: 731 - name: SKU731 - - id: 732 - name: SKU732 - - id: 733 - name: SKU733 - - id: 734 - name: SKU734 - - id: 735 - name: SKU735 - - id: 736 - name: SKU736 - - id: 737 - name: SKU737 - - id: 738 - name: SKU738 - - id: 739 - name: SKU739 - - id: 740 - name: SKU740 - - id: 741 - name: SKU741 - - id: 742 - name: SKU742 - - id: 743 - name: SKU743 - - id: 744 - name: SKU744 - - id: 745 - name: SKU745 - - id: 746 - name: SKU746 - - id: 747 - name: SKU747 - - id: 748 - name: SKU748 - - id: 749 - name: SKU749 - - id: 750 - name: SKU750 - - id: 751 - name: SKU751 - - id: 752 - name: SKU752 - - id: 753 - name: SKU753 - - id: 754 - name: SKU754 - - id: 755 - name: SKU755 - - id: 756 - name: SKU756 - - id: 757 - name: SKU757 - - id: 758 - name: SKU758 - - id: 759 - name: SKU759 - - id: 760 - name: SKU760 - - id: 761 - name: SKU761 - - id: 762 - name: SKU762 - - id: 763 - name: SKU763 - - id: 764 - name: SKU764 - - id: 765 - name: SKU765 - - id: 766 - name: SKU766 - - id: 767 - name: SKU767 - - id: 768 - name: SKU768 - - id: 769 - name: SKU769 - - id: 770 - name: SKU770 - - id: 771 - name: SKU771 - - id: 772 - name: SKU772 - - id: 773 - name: SKU773 - - id: 774 - name: SKU774 - - id: 775 - name: SKU775 - - id: 776 - name: SKU776 - - id: 777 - name: SKU777 - - id: 778 - name: SKU778 - - id: 779 - name: SKU779 - - id: 780 - name: SKU780 - - id: 781 - name: SKU781 - - id: 782 - name: SKU782 - - id: 783 - name: SKU783 - - id: 784 - name: SKU784 - - id: 785 - name: SKU785 - - id: 786 - name: SKU786 - - id: 787 - name: SKU787 - - id: 788 - name: SKU788 - - id: 789 - name: SKU789 - - id: 790 - name: SKU790 - - id: 791 - name: SKU791 - - id: 792 - name: SKU792 - - id: 793 - name: SKU793 - - id: 794 - name: SKU794 - - id: 795 - name: SKU795 - - id: 796 - name: SKU796 - - id: 797 - name: SKU797 - - id: 798 - name: SKU798 - - id: 799 - name: SKU799 - - id: 800 - name: SKU800 - - id: 801 - name: SKU801 - - id: 802 - name: SKU802 - - id: 803 - name: SKU803 - - id: 804 - name: SKU804 - - id: 805 - name: SKU805 - - id: 806 - name: SKU806 - - id: 807 - name: SKU807 - - id: 808 - name: SKU808 - - id: 809 - name: SKU809 - - id: 810 - name: SKU810 - - id: 811 - name: SKU811 - - id: 812 - name: SKU812 - - id: 813 - name: SKU813 - - id: 814 - name: SKU814 - - id: 815 - name: SKU815 - - id: 816 - name: SKU816 - - id: 817 - name: SKU817 - - id: 818 - name: SKU818 - - id: 819 - name: SKU819 - - id: 820 - name: SKU820 - - id: 821 - name: SKU821 - - id: 822 - name: SKU822 - - id: 823 - name: SKU823 - - id: 824 - name: SKU824 - - id: 825 - name: SKU825 - - id: 826 - name: SKU826 - - id: 827 - name: SKU827 - - id: 828 - name: SKU828 - - id: 829 - name: SKU829 - - id: 830 - name: SKU830 - - id: 831 - name: SKU831 - - id: 832 - name: SKU832 - - id: 833 - name: SKU833 - - id: 834 - name: SKU834 - - id: 835 - name: SKU835 - - id: 836 - name: SKU836 - - id: 837 - name: SKU837 - - id: 838 - name: SKU838 - - id: 839 - name: SKU839 - - id: 840 - name: SKU840 - - id: 841 - name: SKU841 - - id: 842 - name: SKU842 - - id: 843 - name: SKU843 - - id: 844 - name: SKU844 - - id: 845 - name: SKU845 - - id: 846 - name: SKU846 - - id: 847 - name: SKU847 - - id: 848 - name: SKU848 - - id: 849 - name: SKU849 - - id: 850 - name: SKU850 - - id: 851 - name: SKU851 - - id: 852 - name: SKU852 - - id: 853 - name: SKU853 - - id: 854 - name: SKU854 - - id: 855 - name: SKU855 - - id: 856 - name: SKU856 - - id: 857 - name: SKU857 - - id: 858 - name: SKU858 - - id: 859 - name: SKU859 - - id: 860 - name: SKU860 - - id: 861 - name: SKU861 - - id: 862 - name: SKU862 - - id: 863 - name: SKU863 - - id: 864 - name: SKU864 - - id: 865 - name: SKU865 - - id: 866 - name: SKU866 - - id: 867 - name: SKU867 - - id: 868 - name: SKU868 - - id: 869 - name: SKU869 - - id: 870 - name: SKU870 - - id: 871 - name: SKU871 - - id: 872 - name: SKU872 - - id: 873 - name: SKU873 - - id: 874 - name: SKU874 - - id: 875 - name: SKU875 - - id: 876 - name: SKU876 - - id: 877 - name: SKU877 - - id: 878 - name: SKU878 - - id: 879 - name: SKU879 - - id: 880 - name: SKU880 - - id: 881 - name: SKU881 - - id: 882 - name: SKU882 - - id: 883 - name: SKU883 - - id: 884 - name: SKU884 - - id: 885 - name: SKU885 - - id: 886 - name: SKU886 - - id: 887 - name: SKU887 - - id: 888 - name: SKU888 - - id: 889 - name: SKU889 - - id: 890 - name: SKU890 - - id: 891 - name: SKU891 - - id: 892 - name: SKU892 - - id: 893 - name: SKU893 - - id: 894 - name: SKU894 - - id: 895 - name: SKU895 - - id: 896 - name: SKU896 - - id: 897 - name: SKU897 - - id: 898 - name: SKU898 - - id: 899 - name: SKU899 - - id: 900 - name: SKU900 - - id: 901 - name: SKU901 - - id: 902 - name: SKU902 - - id: 903 - name: SKU903 - - id: 904 - name: SKU904 - - id: 905 - name: SKU905 - - id: 906 - name: SKU906 - - id: 907 - name: SKU907 - - id: 908 - name: SKU908 - - id: 909 - name: SKU909 - - id: 910 - name: SKU910 - - id: 911 - name: SKU911 - - id: 912 - name: SKU912 - - id: 913 - name: SKU913 - - id: 914 - name: SKU914 - - id: 915 - name: SKU915 - - id: 916 - name: SKU916 - - id: 917 - name: SKU917 - - id: 918 - name: SKU918 - - id: 919 - name: SKU919 - - id: 920 - name: SKU920 - - id: 921 - name: SKU921 - - id: 922 - name: SKU922 - - id: 923 - name: SKU923 - - id: 924 - name: SKU924 - - id: 925 - name: SKU925 - - id: 926 - name: SKU926 - - id: 927 - name: SKU927 - - id: 928 - name: SKU928 - - id: 929 - name: SKU929 - - id: 930 - name: SKU930 - - id: 931 - name: SKU931 - - id: 932 - name: SKU932 - - id: 933 - name: SKU933 - - id: 934 - name: SKU934 - - id: 935 - name: SKU935 - - id: 936 - name: SKU936 - - id: 937 - name: SKU937 - - id: 938 - name: SKU938 - - id: 939 - name: SKU939 - - id: 940 - name: SKU940 - - id: 941 - name: SKU941 - - id: 942 - name: SKU942 - - id: 943 - name: SKU943 - - id: 944 - name: SKU944 - - id: 945 - name: SKU945 - - id: 946 - name: SKU946 - - id: 947 - name: SKU947 - - id: 948 - name: SKU948 - - id: 949 - name: SKU949 - - id: 950 - name: SKU950 - - id: 951 - name: SKU951 - - id: 952 - name: SKU952 - - id: 953 - name: SKU953 - - id: 954 - name: SKU954 - - id: 955 - name: SKU955 - - id: 956 - name: SKU956 - - id: 957 - name: SKU957 - - id: 958 - name: SKU958 - - id: 959 - name: SKU959 - - id: 960 - name: SKU960 - - id: 961 - name: SKU961 - - id: 962 - name: SKU962 - - id: 963 - name: SKU963 - - id: 964 - name: SKU964 - - id: 965 - name: SKU965 - - id: 966 - name: SKU966 - - id: 967 - name: SKU967 - - id: 968 - name: SKU968 - - id: 969 - name: SKU969 - - id: 970 - name: SKU970 - - id: 971 - name: SKU971 - - id: 972 - name: SKU972 - - id: 973 - name: SKU973 - - id: 974 - name: SKU974 - - id: 975 - name: SKU975 - - id: 976 - name: SKU976 - - id: 977 - name: SKU977 - - id: 978 - name: SKU978 - - id: 979 - name: SKU979 - - id: 980 - name: SKU980 - - id: 981 - name: SKU981 - - id: 982 - name: SKU982 - - id: 983 - name: SKU983 - - id: 984 - name: SKU984 - - id: 985 - name: SKU985 - - id: 986 - name: SKU986 - - id: 987 - name: SKU987 - - id: 988 - name: SKU988 - - id: 989 - name: SKU989 - - id: 990 - name: SKU990 - - id: 991 - name: SKU991 - - id: 992 - name: SKU992 - - id: 993 - name: SKU993 - - id: 994 - name: SKU994 - - id: 995 - name: SKU995 - - id: 996 - name: SKU996 - - id: 997 - name: SKU997 - - id: 998 - name: SKU998 - - id: 999 - name: SKU999 - topology: - STORE0: - SKU0: - - WAREHOUSE0 - SKU1: - - WAREHOUSE0 - SKU10: - - WAREHOUSE0 - SKU100: - - WAREHOUSE0 - SKU101: - - WAREHOUSE0 - SKU102: - - WAREHOUSE0 - SKU103: - - WAREHOUSE0 - SKU104: - - WAREHOUSE0 - SKU105: - - WAREHOUSE0 - SKU106: - - WAREHOUSE0 - SKU107: - - WAREHOUSE0 - SKU108: - - WAREHOUSE0 - SKU109: - - WAREHOUSE0 - SKU11: - - WAREHOUSE0 - SKU110: - - WAREHOUSE0 - SKU111: - - WAREHOUSE0 - SKU112: - - WAREHOUSE0 - SKU113: - - WAREHOUSE0 - SKU114: - - WAREHOUSE0 - SKU115: - - WAREHOUSE0 - SKU116: - - WAREHOUSE0 - SKU117: - - WAREHOUSE0 - SKU118: - - WAREHOUSE0 - SKU119: - - WAREHOUSE0 - SKU12: - - WAREHOUSE0 - SKU120: - - WAREHOUSE0 - SKU121: - - WAREHOUSE0 - SKU122: - - WAREHOUSE0 - SKU123: - - WAREHOUSE0 - SKU124: - - WAREHOUSE0 - SKU125: - - WAREHOUSE0 - SKU126: - - WAREHOUSE0 - SKU127: - - WAREHOUSE0 - SKU128: - - WAREHOUSE0 - SKU129: - - WAREHOUSE0 - SKU13: - - WAREHOUSE0 - SKU130: - - WAREHOUSE0 - SKU131: - - WAREHOUSE0 - SKU132: - - WAREHOUSE0 - SKU133: - - WAREHOUSE0 - SKU134: - - WAREHOUSE0 - SKU135: - - WAREHOUSE0 - SKU136: - - WAREHOUSE0 - SKU137: - - WAREHOUSE0 - SKU138: - - WAREHOUSE0 - SKU139: - - WAREHOUSE0 - SKU14: - - WAREHOUSE0 - SKU140: - - WAREHOUSE0 - SKU141: - - WAREHOUSE0 - SKU142: - - WAREHOUSE0 - SKU143: - - WAREHOUSE0 - SKU144: - - WAREHOUSE0 - SKU145: - - WAREHOUSE0 - SKU146: - - WAREHOUSE0 - SKU147: - - WAREHOUSE0 - SKU148: - - WAREHOUSE0 - SKU149: - - WAREHOUSE0 - SKU15: - - WAREHOUSE0 - SKU150: - - WAREHOUSE0 - SKU151: - - WAREHOUSE0 - SKU152: - - WAREHOUSE0 - SKU153: - - WAREHOUSE0 - SKU154: - - WAREHOUSE0 - SKU155: - - WAREHOUSE0 - SKU156: - - WAREHOUSE0 - SKU157: - - WAREHOUSE0 - SKU158: - - WAREHOUSE0 - SKU159: - - WAREHOUSE0 - SKU16: - - WAREHOUSE0 - SKU160: - - WAREHOUSE0 - SKU161: - - WAREHOUSE0 - SKU162: - - WAREHOUSE0 - SKU163: - - WAREHOUSE0 - SKU164: - - WAREHOUSE0 - SKU165: - - WAREHOUSE0 - SKU166: - - WAREHOUSE0 - SKU167: - - WAREHOUSE0 - SKU168: - - WAREHOUSE0 - SKU169: - - WAREHOUSE0 - SKU17: - - WAREHOUSE0 - SKU170: - - WAREHOUSE0 - SKU171: - - WAREHOUSE0 - SKU172: - - WAREHOUSE0 - SKU173: - - WAREHOUSE0 - SKU174: - - WAREHOUSE0 - SKU175: - - WAREHOUSE0 - SKU176: - - WAREHOUSE0 - SKU177: - - WAREHOUSE0 - SKU178: - - WAREHOUSE0 - SKU179: - - WAREHOUSE0 - SKU18: - - WAREHOUSE0 - SKU180: - - WAREHOUSE0 - SKU181: - - WAREHOUSE0 - SKU182: - - WAREHOUSE0 - SKU183: - - WAREHOUSE0 - SKU184: - - WAREHOUSE0 - SKU185: - - WAREHOUSE0 - SKU186: - - WAREHOUSE0 - SKU187: - - WAREHOUSE0 - SKU188: - - WAREHOUSE0 - SKU189: - - WAREHOUSE0 - SKU19: - - WAREHOUSE0 - SKU190: - - WAREHOUSE0 - SKU191: - - WAREHOUSE0 - SKU192: - - WAREHOUSE0 - SKU193: - - WAREHOUSE0 - SKU194: - - WAREHOUSE0 - SKU195: - - WAREHOUSE0 - SKU196: - - WAREHOUSE0 - SKU197: - - WAREHOUSE0 - SKU198: - - WAREHOUSE0 - SKU199: - - WAREHOUSE0 - SKU2: - - WAREHOUSE0 - SKU20: - - WAREHOUSE0 - SKU200: - - WAREHOUSE0 - SKU201: - - WAREHOUSE0 - SKU202: - - WAREHOUSE0 - SKU203: - - WAREHOUSE0 - SKU204: - - WAREHOUSE0 - SKU205: - - WAREHOUSE0 - SKU206: - - WAREHOUSE0 - SKU207: - - WAREHOUSE0 - SKU208: - - WAREHOUSE0 - SKU209: - - WAREHOUSE0 - SKU21: - - WAREHOUSE0 - SKU210: - - WAREHOUSE0 - SKU211: - - WAREHOUSE0 - SKU212: - - WAREHOUSE0 - SKU213: - - WAREHOUSE0 - SKU214: - - WAREHOUSE0 - SKU215: - - WAREHOUSE0 - SKU216: - - WAREHOUSE0 - SKU217: - - WAREHOUSE0 - SKU218: - - WAREHOUSE0 - SKU219: - - WAREHOUSE0 - SKU22: - - WAREHOUSE0 - SKU220: - - WAREHOUSE0 - SKU221: - - WAREHOUSE0 - SKU222: - - WAREHOUSE0 - SKU223: - - WAREHOUSE0 - SKU224: - - WAREHOUSE0 - SKU225: - - WAREHOUSE0 - SKU226: - - WAREHOUSE0 - SKU227: - - WAREHOUSE0 - SKU228: - - WAREHOUSE0 - SKU229: - - WAREHOUSE0 - SKU23: - - WAREHOUSE0 - SKU230: - - WAREHOUSE0 - SKU231: - - WAREHOUSE0 - SKU232: - - WAREHOUSE0 - SKU233: - - WAREHOUSE0 - SKU234: - - WAREHOUSE0 - SKU235: - - WAREHOUSE0 - SKU236: - - WAREHOUSE0 - SKU237: - - WAREHOUSE0 - SKU238: - - WAREHOUSE0 - SKU239: - - WAREHOUSE0 - SKU24: - - WAREHOUSE0 - SKU240: - - WAREHOUSE0 - SKU241: - - WAREHOUSE0 - SKU242: - - WAREHOUSE0 - SKU243: - - WAREHOUSE0 - SKU244: - - WAREHOUSE0 - SKU245: - - WAREHOUSE0 - SKU246: - - WAREHOUSE0 - SKU247: - - WAREHOUSE0 - SKU248: - - WAREHOUSE0 - SKU249: - - WAREHOUSE0 - SKU25: - - WAREHOUSE0 - SKU250: - - WAREHOUSE0 - SKU251: - - WAREHOUSE0 - SKU252: - - WAREHOUSE0 - SKU253: - - WAREHOUSE0 - SKU254: - - WAREHOUSE0 - SKU255: - - WAREHOUSE0 - SKU256: - - WAREHOUSE0 - SKU257: - - WAREHOUSE0 - SKU258: - - WAREHOUSE0 - SKU259: - - WAREHOUSE0 - SKU26: - - WAREHOUSE0 - SKU260: - - WAREHOUSE0 - SKU261: - - WAREHOUSE0 - SKU262: - - WAREHOUSE0 - SKU263: - - WAREHOUSE0 - SKU264: - - WAREHOUSE0 - SKU265: - - WAREHOUSE0 - SKU266: - - WAREHOUSE0 - SKU267: - - WAREHOUSE0 - SKU268: - - WAREHOUSE0 - SKU269: - - WAREHOUSE0 - SKU27: - - WAREHOUSE0 - SKU270: - - WAREHOUSE0 - SKU271: - - WAREHOUSE0 - SKU272: - - WAREHOUSE0 - SKU273: - - WAREHOUSE0 - SKU274: - - WAREHOUSE0 - SKU275: - - WAREHOUSE0 - SKU276: - - WAREHOUSE0 - SKU277: - - WAREHOUSE0 - SKU278: - - WAREHOUSE0 - SKU279: - - WAREHOUSE0 - SKU28: - - WAREHOUSE0 - SKU280: - - WAREHOUSE0 - SKU281: - - WAREHOUSE0 - SKU282: - - WAREHOUSE0 - SKU283: - - WAREHOUSE0 - SKU284: - - WAREHOUSE0 - SKU285: - - WAREHOUSE0 - SKU286: - - WAREHOUSE0 - SKU287: - - WAREHOUSE0 - SKU288: - - WAREHOUSE0 - SKU289: - - WAREHOUSE0 - SKU29: - - WAREHOUSE0 - SKU290: - - WAREHOUSE0 - SKU291: - - WAREHOUSE0 - SKU292: - - WAREHOUSE0 - SKU293: - - WAREHOUSE0 - SKU294: - - WAREHOUSE0 - SKU295: - - WAREHOUSE0 - SKU296: - - WAREHOUSE0 - SKU297: - - WAREHOUSE0 - SKU298: - - WAREHOUSE0 - SKU299: - - WAREHOUSE0 - SKU3: - - WAREHOUSE0 - SKU30: - - WAREHOUSE0 - SKU300: - - WAREHOUSE0 - SKU301: - - WAREHOUSE0 - SKU302: - - WAREHOUSE0 - SKU303: - - WAREHOUSE0 - SKU304: - - WAREHOUSE0 - SKU305: - - WAREHOUSE0 - SKU306: - - WAREHOUSE0 - SKU307: - - WAREHOUSE0 - SKU308: - - WAREHOUSE0 - SKU309: - - WAREHOUSE0 - SKU31: - - WAREHOUSE0 - SKU310: - - WAREHOUSE0 - SKU311: - - WAREHOUSE0 - SKU312: - - WAREHOUSE0 - SKU313: - - WAREHOUSE0 - SKU314: - - WAREHOUSE0 - SKU315: - - WAREHOUSE0 - SKU316: - - WAREHOUSE0 - SKU317: - - WAREHOUSE0 - SKU318: - - WAREHOUSE0 - SKU319: - - WAREHOUSE0 - SKU32: - - WAREHOUSE0 - SKU320: - - WAREHOUSE0 - SKU321: - - WAREHOUSE0 - SKU322: - - WAREHOUSE0 - SKU323: - - WAREHOUSE0 - SKU324: - - WAREHOUSE0 - SKU325: - - WAREHOUSE0 - SKU326: - - WAREHOUSE0 - SKU327: - - WAREHOUSE0 - SKU328: - - WAREHOUSE0 - SKU329: - - WAREHOUSE0 - SKU33: - - WAREHOUSE0 - SKU330: - - WAREHOUSE0 - SKU331: - - WAREHOUSE0 - SKU332: - - WAREHOUSE0 - SKU333: - - WAREHOUSE0 - SKU334: - - WAREHOUSE0 - SKU335: - - WAREHOUSE0 - SKU336: - - WAREHOUSE0 - SKU337: - - WAREHOUSE0 - SKU338: - - WAREHOUSE0 - SKU339: - - WAREHOUSE0 - SKU34: - - WAREHOUSE0 - SKU340: - - WAREHOUSE0 - SKU341: - - WAREHOUSE0 - SKU342: - - WAREHOUSE0 - SKU343: - - WAREHOUSE0 - SKU344: - - WAREHOUSE0 - SKU345: - - WAREHOUSE0 - SKU346: - - WAREHOUSE0 - SKU347: - - WAREHOUSE0 - SKU348: - - WAREHOUSE0 - SKU349: - - WAREHOUSE0 - SKU35: - - WAREHOUSE0 - SKU350: - - WAREHOUSE0 - SKU351: - - WAREHOUSE0 - SKU352: - - WAREHOUSE0 - SKU353: - - WAREHOUSE0 - SKU354: - - WAREHOUSE0 - SKU355: - - WAREHOUSE0 - SKU356: - - WAREHOUSE0 - SKU357: - - WAREHOUSE0 - SKU358: - - WAREHOUSE0 - SKU359: - - WAREHOUSE0 - SKU36: - - WAREHOUSE0 - SKU360: - - WAREHOUSE0 - SKU361: - - WAREHOUSE0 - SKU362: - - WAREHOUSE0 - SKU363: - - WAREHOUSE0 - SKU364: - - WAREHOUSE0 - SKU365: - - WAREHOUSE0 - SKU366: - - WAREHOUSE0 - SKU367: - - WAREHOUSE0 - SKU368: - - WAREHOUSE0 - SKU369: - - WAREHOUSE0 - SKU37: - - WAREHOUSE0 - SKU370: - - WAREHOUSE0 - SKU371: - - WAREHOUSE0 - SKU372: - - WAREHOUSE0 - SKU373: - - WAREHOUSE0 - SKU374: - - WAREHOUSE0 - SKU375: - - WAREHOUSE0 - SKU376: - - WAREHOUSE0 - SKU377: - - WAREHOUSE0 - SKU378: - - WAREHOUSE0 - SKU379: - - WAREHOUSE0 - SKU38: - - WAREHOUSE0 - SKU380: - - WAREHOUSE0 - SKU381: - - WAREHOUSE0 - SKU382: - - WAREHOUSE0 - SKU383: - - WAREHOUSE0 - SKU384: - - WAREHOUSE0 - SKU385: - - WAREHOUSE0 - SKU386: - - WAREHOUSE0 - SKU387: - - WAREHOUSE0 - SKU388: - - WAREHOUSE0 - SKU389: - - WAREHOUSE0 - SKU39: - - WAREHOUSE0 - SKU390: - - WAREHOUSE0 - SKU391: - - WAREHOUSE0 - SKU392: - - WAREHOUSE0 - SKU393: - - WAREHOUSE0 - SKU394: - - WAREHOUSE0 - SKU395: - - WAREHOUSE0 - SKU396: - - WAREHOUSE0 - SKU397: - - WAREHOUSE0 - SKU398: - - WAREHOUSE0 - SKU399: - - WAREHOUSE0 - SKU4: - - WAREHOUSE0 - SKU40: - - WAREHOUSE0 - SKU400: - - WAREHOUSE0 - SKU401: - - WAREHOUSE0 - SKU402: - - WAREHOUSE0 - SKU403: - - WAREHOUSE0 - SKU404: - - WAREHOUSE0 - SKU405: - - WAREHOUSE0 - SKU406: - - WAREHOUSE0 - SKU407: - - WAREHOUSE0 - SKU408: - - WAREHOUSE0 - SKU409: - - WAREHOUSE0 - SKU41: - - WAREHOUSE0 - SKU410: - - WAREHOUSE0 - SKU411: - - WAREHOUSE0 - SKU412: - - WAREHOUSE0 - SKU413: - - WAREHOUSE0 - SKU414: - - WAREHOUSE0 - SKU415: - - WAREHOUSE0 - SKU416: - - WAREHOUSE0 - SKU417: - - WAREHOUSE0 - SKU418: - - WAREHOUSE0 - SKU419: - - WAREHOUSE0 - SKU42: - - WAREHOUSE0 - SKU420: - - WAREHOUSE0 - SKU421: - - WAREHOUSE0 - SKU422: - - WAREHOUSE0 - SKU423: - - WAREHOUSE0 - SKU424: - - WAREHOUSE0 - SKU425: - - WAREHOUSE0 - SKU426: - - WAREHOUSE0 - SKU427: - - WAREHOUSE0 - SKU428: - - WAREHOUSE0 - SKU429: - - WAREHOUSE0 - SKU43: - - WAREHOUSE0 - SKU430: - - WAREHOUSE0 - SKU431: - - WAREHOUSE0 - SKU432: - - WAREHOUSE0 - SKU433: - - WAREHOUSE0 - SKU434: - - WAREHOUSE0 - SKU435: - - WAREHOUSE0 - SKU436: - - WAREHOUSE0 - SKU437: - - WAREHOUSE0 - SKU438: - - WAREHOUSE0 - SKU439: - - WAREHOUSE0 - SKU44: - - WAREHOUSE0 - SKU440: - - WAREHOUSE0 - SKU441: - - WAREHOUSE0 - SKU442: - - WAREHOUSE0 - SKU443: - - WAREHOUSE0 - SKU444: - - WAREHOUSE0 - SKU445: - - WAREHOUSE0 - SKU446: - - WAREHOUSE0 - SKU447: - - WAREHOUSE0 - SKU448: - - WAREHOUSE0 - SKU449: - - WAREHOUSE0 - SKU45: - - WAREHOUSE0 - SKU450: - - WAREHOUSE0 - SKU451: - - WAREHOUSE0 - SKU452: - - WAREHOUSE0 - SKU453: - - WAREHOUSE0 - SKU454: - - WAREHOUSE0 - SKU455: - - WAREHOUSE0 - SKU456: - - WAREHOUSE0 - SKU457: - - WAREHOUSE0 - SKU458: - - WAREHOUSE0 - SKU459: - - WAREHOUSE0 - SKU46: - - WAREHOUSE0 - SKU460: - - WAREHOUSE0 - SKU461: - - WAREHOUSE0 - SKU462: - - WAREHOUSE0 - SKU463: - - WAREHOUSE0 - SKU464: - - WAREHOUSE0 - SKU465: - - WAREHOUSE0 - SKU466: - - WAREHOUSE0 - SKU467: - - WAREHOUSE0 - SKU468: - - WAREHOUSE0 - SKU469: - - WAREHOUSE0 - SKU47: - - WAREHOUSE0 - SKU470: - - WAREHOUSE0 - SKU471: - - WAREHOUSE0 - SKU472: - - WAREHOUSE0 - SKU473: - - WAREHOUSE0 - SKU474: - - WAREHOUSE0 - SKU475: - - WAREHOUSE0 - SKU476: - - WAREHOUSE0 - SKU477: - - WAREHOUSE0 - SKU478: - - WAREHOUSE0 - SKU479: - - WAREHOUSE0 - SKU48: - - WAREHOUSE0 - SKU480: - - WAREHOUSE0 - SKU481: - - WAREHOUSE0 - SKU482: - - WAREHOUSE0 - SKU483: - - WAREHOUSE0 - SKU484: - - WAREHOUSE0 - SKU485: - - WAREHOUSE0 - SKU486: - - WAREHOUSE0 - SKU487: - - WAREHOUSE0 - SKU488: - - WAREHOUSE0 - SKU489: - - WAREHOUSE0 - SKU49: - - WAREHOUSE0 - SKU490: - - WAREHOUSE0 - SKU491: - - WAREHOUSE0 - SKU492: - - WAREHOUSE0 - SKU493: - - WAREHOUSE0 - SKU494: - - WAREHOUSE0 - SKU495: - - WAREHOUSE0 - SKU496: - - WAREHOUSE0 - SKU497: - - WAREHOUSE0 - SKU498: - - WAREHOUSE0 - SKU499: - - WAREHOUSE0 - SKU5: - - WAREHOUSE0 - SKU50: - - WAREHOUSE0 - SKU500: - - WAREHOUSE0 - SKU501: - - WAREHOUSE0 - SKU502: - - WAREHOUSE0 - SKU503: - - WAREHOUSE0 - SKU504: - - WAREHOUSE0 - SKU505: - - WAREHOUSE0 - SKU506: - - WAREHOUSE0 - SKU507: - - WAREHOUSE0 - SKU508: - - WAREHOUSE0 - SKU509: - - WAREHOUSE0 - SKU51: - - WAREHOUSE0 - SKU510: - - WAREHOUSE0 - SKU511: - - WAREHOUSE0 - SKU512: - - WAREHOUSE0 - SKU513: - - WAREHOUSE0 - SKU514: - - WAREHOUSE0 - SKU515: - - WAREHOUSE0 - SKU516: - - WAREHOUSE0 - SKU517: - - WAREHOUSE0 - SKU518: - - WAREHOUSE0 - SKU519: - - WAREHOUSE0 - SKU52: - - WAREHOUSE0 - SKU520: - - WAREHOUSE0 - SKU521: - - WAREHOUSE0 - SKU522: - - WAREHOUSE0 - SKU523: - - WAREHOUSE0 - SKU524: - - WAREHOUSE0 - SKU525: - - WAREHOUSE0 - SKU526: - - WAREHOUSE0 - SKU527: - - WAREHOUSE0 - SKU528: - - WAREHOUSE0 - SKU529: - - WAREHOUSE0 - SKU53: - - WAREHOUSE0 - SKU530: - - WAREHOUSE0 - SKU531: - - WAREHOUSE0 - SKU532: - - WAREHOUSE0 - SKU533: - - WAREHOUSE0 - SKU534: - - WAREHOUSE0 - SKU535: - - WAREHOUSE0 - SKU536: - - WAREHOUSE0 - SKU537: - - WAREHOUSE0 - SKU538: - - WAREHOUSE0 - SKU539: - - WAREHOUSE0 - SKU54: - - WAREHOUSE0 - SKU540: - - WAREHOUSE0 - SKU541: - - WAREHOUSE0 - SKU542: - - WAREHOUSE0 - SKU543: - - WAREHOUSE0 - SKU544: - - WAREHOUSE0 - SKU545: - - WAREHOUSE0 - SKU546: - - WAREHOUSE0 - SKU547: - - WAREHOUSE0 - SKU548: - - WAREHOUSE0 - SKU549: - - WAREHOUSE0 - SKU55: - - WAREHOUSE0 - SKU550: - - WAREHOUSE0 - SKU551: - - WAREHOUSE0 - SKU552: - - WAREHOUSE0 - SKU553: - - WAREHOUSE0 - SKU554: - - WAREHOUSE0 - SKU555: - - WAREHOUSE0 - SKU556: - - WAREHOUSE0 - SKU557: - - WAREHOUSE0 - SKU558: - - WAREHOUSE0 - SKU559: - - WAREHOUSE0 - SKU56: - - WAREHOUSE0 - SKU560: - - WAREHOUSE0 - SKU561: - - WAREHOUSE0 - SKU562: - - WAREHOUSE0 - SKU563: - - WAREHOUSE0 - SKU564: - - WAREHOUSE0 - SKU565: - - WAREHOUSE0 - SKU566: - - WAREHOUSE0 - SKU567: - - WAREHOUSE0 - SKU568: - - WAREHOUSE0 - SKU569: - - WAREHOUSE0 - SKU57: - - WAREHOUSE0 - SKU570: - - WAREHOUSE0 - SKU571: - - WAREHOUSE0 - SKU572: - - WAREHOUSE0 - SKU573: - - WAREHOUSE0 - SKU574: - - WAREHOUSE0 - SKU575: - - WAREHOUSE0 - SKU576: - - WAREHOUSE0 - SKU577: - - WAREHOUSE0 - SKU578: - - WAREHOUSE0 - SKU579: - - WAREHOUSE0 - SKU58: - - WAREHOUSE0 - SKU580: - - WAREHOUSE0 - SKU581: - - WAREHOUSE0 - SKU582: - - WAREHOUSE0 - SKU583: - - WAREHOUSE0 - SKU584: - - WAREHOUSE0 - SKU585: - - WAREHOUSE0 - SKU586: - - WAREHOUSE0 - SKU587: - - WAREHOUSE0 - SKU588: - - WAREHOUSE0 - SKU589: - - WAREHOUSE0 - SKU59: - - WAREHOUSE0 - SKU590: - - WAREHOUSE0 - SKU591: - - WAREHOUSE0 - SKU592: - - WAREHOUSE0 - SKU593: - - WAREHOUSE0 - SKU594: - - WAREHOUSE0 - SKU595: - - WAREHOUSE0 - SKU596: - - WAREHOUSE0 - SKU597: - - WAREHOUSE0 - SKU598: - - WAREHOUSE0 - SKU599: - - WAREHOUSE0 - SKU6: - - WAREHOUSE0 - SKU60: - - WAREHOUSE0 - SKU600: - - WAREHOUSE0 - SKU601: - - WAREHOUSE0 - SKU602: - - WAREHOUSE0 - SKU603: - - WAREHOUSE0 - SKU604: - - WAREHOUSE0 - SKU605: - - WAREHOUSE0 - SKU606: - - WAREHOUSE0 - SKU607: - - WAREHOUSE0 - SKU608: - - WAREHOUSE0 - SKU609: - - WAREHOUSE0 - SKU61: - - WAREHOUSE0 - SKU610: - - WAREHOUSE0 - SKU611: - - WAREHOUSE0 - SKU612: - - WAREHOUSE0 - SKU613: - - WAREHOUSE0 - SKU614: - - WAREHOUSE0 - SKU615: - - WAREHOUSE0 - SKU616: - - WAREHOUSE0 - SKU617: - - WAREHOUSE0 - SKU618: - - WAREHOUSE0 - SKU619: - - WAREHOUSE0 - SKU62: - - WAREHOUSE0 - SKU620: - - WAREHOUSE0 - SKU621: - - WAREHOUSE0 - SKU622: - - WAREHOUSE0 - SKU623: - - WAREHOUSE0 - SKU624: - - WAREHOUSE0 - SKU625: - - WAREHOUSE0 - SKU626: - - WAREHOUSE0 - SKU627: - - WAREHOUSE0 - SKU628: - - WAREHOUSE0 - SKU629: - - WAREHOUSE0 - SKU63: - - WAREHOUSE0 - SKU630: - - WAREHOUSE0 - SKU631: - - WAREHOUSE0 - SKU632: - - WAREHOUSE0 - SKU633: - - WAREHOUSE0 - SKU634: - - WAREHOUSE0 - SKU635: - - WAREHOUSE0 - SKU636: - - WAREHOUSE0 - SKU637: - - WAREHOUSE0 - SKU638: - - WAREHOUSE0 - SKU639: - - WAREHOUSE0 - SKU64: - - WAREHOUSE0 - SKU640: - - WAREHOUSE0 - SKU641: - - WAREHOUSE0 - SKU642: - - WAREHOUSE0 - SKU643: - - WAREHOUSE0 - SKU644: - - WAREHOUSE0 - SKU645: - - WAREHOUSE0 - SKU646: - - WAREHOUSE0 - SKU647: - - WAREHOUSE0 - SKU648: - - WAREHOUSE0 - SKU649: - - WAREHOUSE0 - SKU65: - - WAREHOUSE0 - SKU650: - - WAREHOUSE0 - SKU651: - - WAREHOUSE0 - SKU652: - - WAREHOUSE0 - SKU653: - - WAREHOUSE0 - SKU654: - - WAREHOUSE0 - SKU655: - - WAREHOUSE0 - SKU656: - - WAREHOUSE0 - SKU657: - - WAREHOUSE0 - SKU658: - - WAREHOUSE0 - SKU659: - - WAREHOUSE0 - SKU66: - - WAREHOUSE0 - SKU660: - - WAREHOUSE0 - SKU661: - - WAREHOUSE0 - SKU662: - - WAREHOUSE0 - SKU663: - - WAREHOUSE0 - SKU664: - - WAREHOUSE0 - SKU665: - - WAREHOUSE0 - SKU666: - - WAREHOUSE0 - SKU667: - - WAREHOUSE0 - SKU668: - - WAREHOUSE0 - SKU669: - - WAREHOUSE0 - SKU67: - - WAREHOUSE0 - SKU670: - - WAREHOUSE0 - SKU671: - - WAREHOUSE0 - SKU672: - - WAREHOUSE0 - SKU673: - - WAREHOUSE0 - SKU674: - - WAREHOUSE0 - SKU675: - - WAREHOUSE0 - SKU676: - - WAREHOUSE0 - SKU677: - - WAREHOUSE0 - SKU678: - - WAREHOUSE0 - SKU679: - - WAREHOUSE0 - SKU68: - - WAREHOUSE0 - SKU680: - - WAREHOUSE0 - SKU681: - - WAREHOUSE0 - SKU682: - - WAREHOUSE0 - SKU683: - - WAREHOUSE0 - SKU684: - - WAREHOUSE0 - SKU685: - - WAREHOUSE0 - SKU686: - - WAREHOUSE0 - SKU687: - - WAREHOUSE0 - SKU688: - - WAREHOUSE0 - SKU689: - - WAREHOUSE0 - SKU69: - - WAREHOUSE0 - SKU690: - - WAREHOUSE0 - SKU691: - - WAREHOUSE0 - SKU692: - - WAREHOUSE0 - SKU693: - - WAREHOUSE0 - SKU694: - - WAREHOUSE0 - SKU695: - - WAREHOUSE0 - SKU696: - - WAREHOUSE0 - SKU697: - - WAREHOUSE0 - SKU698: - - WAREHOUSE0 - SKU699: - - WAREHOUSE0 - SKU7: - - WAREHOUSE0 - SKU70: - - WAREHOUSE0 - SKU700: - - WAREHOUSE0 - SKU701: - - WAREHOUSE0 - SKU702: - - WAREHOUSE0 - SKU703: - - WAREHOUSE0 - SKU704: - - WAREHOUSE0 - SKU705: - - WAREHOUSE0 - SKU706: - - WAREHOUSE0 - SKU707: - - WAREHOUSE0 - SKU708: - - WAREHOUSE0 - SKU709: - - WAREHOUSE0 - SKU71: - - WAREHOUSE0 - SKU710: - - WAREHOUSE0 - SKU711: - - WAREHOUSE0 - SKU712: - - WAREHOUSE0 - SKU713: - - WAREHOUSE0 - SKU714: - - WAREHOUSE0 - SKU715: - - WAREHOUSE0 - SKU716: - - WAREHOUSE0 - SKU717: - - WAREHOUSE0 - SKU718: - - WAREHOUSE0 - SKU719: - - WAREHOUSE0 - SKU72: - - WAREHOUSE0 - SKU720: - - WAREHOUSE0 - SKU721: - - WAREHOUSE0 - SKU722: - - WAREHOUSE0 - SKU723: - - WAREHOUSE0 - SKU724: - - WAREHOUSE0 - SKU725: - - WAREHOUSE0 - SKU726: - - WAREHOUSE0 - SKU727: - - WAREHOUSE0 - SKU728: - - WAREHOUSE0 - SKU729: - - WAREHOUSE0 - SKU73: - - WAREHOUSE0 - SKU730: - - WAREHOUSE0 - SKU731: - - WAREHOUSE0 - SKU732: - - WAREHOUSE0 - SKU733: - - WAREHOUSE0 - SKU734: - - WAREHOUSE0 - SKU735: - - WAREHOUSE0 - SKU736: - - WAREHOUSE0 - SKU737: - - WAREHOUSE0 - SKU738: - - WAREHOUSE0 - SKU739: - - WAREHOUSE0 - SKU74: - - WAREHOUSE0 - SKU740: - - WAREHOUSE0 - SKU741: - - WAREHOUSE0 - SKU742: - - WAREHOUSE0 - SKU743: - - WAREHOUSE0 - SKU744: - - WAREHOUSE0 - SKU745: - - WAREHOUSE0 - SKU746: - - WAREHOUSE0 - SKU747: - - WAREHOUSE0 - SKU748: - - WAREHOUSE0 - SKU749: - - WAREHOUSE0 - SKU75: - - WAREHOUSE0 - SKU750: - - WAREHOUSE0 - SKU751: - - WAREHOUSE0 - SKU752: - - WAREHOUSE0 - SKU753: - - WAREHOUSE0 - SKU754: - - WAREHOUSE0 - SKU755: - - WAREHOUSE0 - SKU756: - - WAREHOUSE0 - SKU757: - - WAREHOUSE0 - SKU758: - - WAREHOUSE0 - SKU759: - - WAREHOUSE0 - SKU76: - - WAREHOUSE0 - SKU760: - - WAREHOUSE0 - SKU761: - - WAREHOUSE0 - SKU762: - - WAREHOUSE0 - SKU763: - - WAREHOUSE0 - SKU764: - - WAREHOUSE0 - SKU765: - - WAREHOUSE0 - SKU766: - - WAREHOUSE0 - SKU767: - - WAREHOUSE0 - SKU768: - - WAREHOUSE0 - SKU769: - - WAREHOUSE0 - SKU77: - - WAREHOUSE0 - SKU770: - - WAREHOUSE0 - SKU771: - - WAREHOUSE0 - SKU772: - - WAREHOUSE0 - SKU773: - - WAREHOUSE0 - SKU774: - - WAREHOUSE0 - SKU775: - - WAREHOUSE0 - SKU776: - - WAREHOUSE0 - SKU777: - - WAREHOUSE0 - SKU778: - - WAREHOUSE0 - SKU779: - - WAREHOUSE0 - SKU78: - - WAREHOUSE0 - SKU780: - - WAREHOUSE0 - SKU781: - - WAREHOUSE0 - SKU782: - - WAREHOUSE0 - SKU783: - - WAREHOUSE0 - SKU784: - - WAREHOUSE0 - SKU785: - - WAREHOUSE0 - SKU786: - - WAREHOUSE0 - SKU787: - - WAREHOUSE0 - SKU788: - - WAREHOUSE0 - SKU789: - - WAREHOUSE0 - SKU79: - - WAREHOUSE0 - SKU790: - - WAREHOUSE0 - SKU791: - - WAREHOUSE0 - SKU792: - - WAREHOUSE0 - SKU793: - - WAREHOUSE0 - SKU794: - - WAREHOUSE0 - SKU795: - - WAREHOUSE0 - SKU796: - - WAREHOUSE0 - SKU797: - - WAREHOUSE0 - SKU798: - - WAREHOUSE0 - SKU799: - - WAREHOUSE0 - SKU8: - - WAREHOUSE0 - SKU80: - - WAREHOUSE0 - SKU800: - - WAREHOUSE0 - SKU801: - - WAREHOUSE0 - SKU802: - - WAREHOUSE0 - SKU803: - - WAREHOUSE0 - SKU804: - - WAREHOUSE0 - SKU805: - - WAREHOUSE0 - SKU806: - - WAREHOUSE0 - SKU807: - - WAREHOUSE0 - SKU808: - - WAREHOUSE0 - SKU809: - - WAREHOUSE0 - SKU81: - - WAREHOUSE0 - SKU810: - - WAREHOUSE0 - SKU811: - - WAREHOUSE0 - SKU812: - - WAREHOUSE0 - SKU813: - - WAREHOUSE0 - SKU814: - - WAREHOUSE0 - SKU815: - - WAREHOUSE0 - SKU816: - - WAREHOUSE0 - SKU817: - - WAREHOUSE0 - SKU818: - - WAREHOUSE0 - SKU819: - - WAREHOUSE0 - SKU82: - - WAREHOUSE0 - SKU820: - - WAREHOUSE0 - SKU821: - - WAREHOUSE0 - SKU822: - - WAREHOUSE0 - SKU823: - - WAREHOUSE0 - SKU824: - - WAREHOUSE0 - SKU825: - - WAREHOUSE0 - SKU826: - - WAREHOUSE0 - SKU827: - - WAREHOUSE0 - SKU828: - - WAREHOUSE0 - SKU829: - - WAREHOUSE0 - SKU83: - - WAREHOUSE0 - SKU830: - - WAREHOUSE0 - SKU831: - - WAREHOUSE0 - SKU832: - - WAREHOUSE0 - SKU833: - - WAREHOUSE0 - SKU834: - - WAREHOUSE0 - SKU835: - - WAREHOUSE0 - SKU836: - - WAREHOUSE0 - SKU837: - - WAREHOUSE0 - SKU838: - - WAREHOUSE0 - SKU839: - - WAREHOUSE0 - SKU84: - - WAREHOUSE0 - SKU840: - - WAREHOUSE0 - SKU841: - - WAREHOUSE0 - SKU842: - - WAREHOUSE0 - SKU843: - - WAREHOUSE0 - SKU844: - - WAREHOUSE0 - SKU845: - - WAREHOUSE0 - SKU846: - - WAREHOUSE0 - SKU847: - - WAREHOUSE0 - SKU848: - - WAREHOUSE0 - SKU849: - - WAREHOUSE0 - SKU85: - - WAREHOUSE0 - SKU850: - - WAREHOUSE0 - SKU851: - - WAREHOUSE0 - SKU852: - - WAREHOUSE0 - SKU853: - - WAREHOUSE0 - SKU854: - - WAREHOUSE0 - SKU855: - - WAREHOUSE0 - SKU856: - - WAREHOUSE0 - SKU857: - - WAREHOUSE0 - SKU858: - - WAREHOUSE0 - SKU859: - - WAREHOUSE0 - SKU86: - - WAREHOUSE0 - SKU860: - - WAREHOUSE0 - SKU861: - - WAREHOUSE0 - SKU862: - - WAREHOUSE0 - SKU863: - - WAREHOUSE0 - SKU864: - - WAREHOUSE0 - SKU865: - - WAREHOUSE0 - SKU866: - - WAREHOUSE0 - SKU867: - - WAREHOUSE0 - SKU868: - - WAREHOUSE0 - SKU869: - - WAREHOUSE0 - SKU87: - - WAREHOUSE0 - SKU870: - - WAREHOUSE0 - SKU871: - - WAREHOUSE0 - SKU872: - - WAREHOUSE0 - SKU873: - - WAREHOUSE0 - SKU874: - - WAREHOUSE0 - SKU875: - - WAREHOUSE0 - SKU876: - - WAREHOUSE0 - SKU877: - - WAREHOUSE0 - SKU878: - - WAREHOUSE0 - SKU879: - - WAREHOUSE0 - SKU88: - - WAREHOUSE0 - SKU880: - - WAREHOUSE0 - SKU881: - - WAREHOUSE0 - SKU882: - - WAREHOUSE0 - SKU883: - - WAREHOUSE0 - SKU884: - - WAREHOUSE0 - SKU885: - - WAREHOUSE0 - SKU886: - - WAREHOUSE0 - SKU887: - - WAREHOUSE0 - SKU888: - - WAREHOUSE0 - SKU889: - - WAREHOUSE0 - SKU89: - - WAREHOUSE0 - SKU890: - - WAREHOUSE0 - SKU891: - - WAREHOUSE0 - SKU892: - - WAREHOUSE0 - SKU893: - - WAREHOUSE0 - SKU894: - - WAREHOUSE0 - SKU895: - - WAREHOUSE0 - SKU896: - - WAREHOUSE0 - SKU897: - - WAREHOUSE0 - SKU898: - - WAREHOUSE0 - SKU899: - - WAREHOUSE0 - SKU9: - - WAREHOUSE0 - SKU90: - - WAREHOUSE0 - SKU900: - - WAREHOUSE0 - SKU901: - - WAREHOUSE0 - SKU902: - - WAREHOUSE0 - SKU903: - - WAREHOUSE0 - SKU904: - - WAREHOUSE0 - SKU905: - - WAREHOUSE0 - SKU906: - - WAREHOUSE0 - SKU907: - - WAREHOUSE0 - SKU908: - - WAREHOUSE0 - SKU909: - - WAREHOUSE0 - SKU91: - - WAREHOUSE0 - SKU910: - - WAREHOUSE0 - SKU911: - - WAREHOUSE0 - SKU912: - - WAREHOUSE0 - SKU913: - - WAREHOUSE0 - SKU914: - - WAREHOUSE0 - SKU915: - - WAREHOUSE0 - SKU916: - - WAREHOUSE0 - SKU917: - - WAREHOUSE0 - SKU918: - - WAREHOUSE0 - SKU919: - - WAREHOUSE0 - SKU92: - - WAREHOUSE0 - SKU920: - - WAREHOUSE0 - SKU921: - - WAREHOUSE0 - SKU922: - - WAREHOUSE0 - SKU923: - - WAREHOUSE0 - SKU924: - - WAREHOUSE0 - SKU925: - - WAREHOUSE0 - SKU926: - - WAREHOUSE0 - SKU927: - - WAREHOUSE0 - SKU928: - - WAREHOUSE0 - SKU929: - - WAREHOUSE0 - SKU93: - - WAREHOUSE0 - SKU930: - - WAREHOUSE0 - SKU931: - - WAREHOUSE0 - SKU932: - - WAREHOUSE0 - SKU933: - - WAREHOUSE0 - SKU934: - - WAREHOUSE0 - SKU935: - - WAREHOUSE0 - SKU936: - - WAREHOUSE0 - SKU937: - - WAREHOUSE0 - SKU938: - - WAREHOUSE0 - SKU939: - - WAREHOUSE0 - SKU94: - - WAREHOUSE0 - SKU940: - - WAREHOUSE0 - SKU941: - - WAREHOUSE0 - SKU942: - - WAREHOUSE0 - SKU943: - - WAREHOUSE0 - SKU944: - - WAREHOUSE0 - SKU945: - - WAREHOUSE0 - SKU946: - - WAREHOUSE0 - SKU947: - - WAREHOUSE0 - SKU948: - - WAREHOUSE0 - SKU949: - - WAREHOUSE0 - SKU95: - - WAREHOUSE0 - SKU950: - - WAREHOUSE0 - SKU951: - - WAREHOUSE0 - SKU952: - - WAREHOUSE0 - SKU953: - - WAREHOUSE0 - SKU954: - - WAREHOUSE0 - SKU955: - - WAREHOUSE0 - SKU956: - - WAREHOUSE0 - SKU957: - - WAREHOUSE0 - SKU958: - - WAREHOUSE0 - SKU959: - - WAREHOUSE0 - SKU96: - - WAREHOUSE0 - SKU960: - - WAREHOUSE0 - SKU961: - - WAREHOUSE0 - SKU962: - - WAREHOUSE0 - SKU963: - - WAREHOUSE0 - SKU964: - - WAREHOUSE0 - SKU965: - - WAREHOUSE0 - SKU966: - - WAREHOUSE0 - SKU967: - - WAREHOUSE0 - SKU968: - - WAREHOUSE0 - SKU969: - - WAREHOUSE0 - SKU97: - - WAREHOUSE0 - SKU970: - - WAREHOUSE0 - SKU971: - - WAREHOUSE0 - SKU972: - - WAREHOUSE0 - SKU973: - - WAREHOUSE0 - SKU974: - - WAREHOUSE0 - SKU975: - - WAREHOUSE0 - SKU976: - - WAREHOUSE0 - SKU977: - - WAREHOUSE0 - SKU978: - - WAREHOUSE0 - SKU979: - - WAREHOUSE0 - SKU98: - - WAREHOUSE0 - SKU980: - - WAREHOUSE0 - SKU981: - - WAREHOUSE0 - SKU982: - - WAREHOUSE0 - SKU983: - - WAREHOUSE0 - SKU984: - - WAREHOUSE0 - SKU985: - - WAREHOUSE0 - SKU986: - - WAREHOUSE0 - SKU987: - - WAREHOUSE0 - SKU988: - - WAREHOUSE0 - SKU989: - - WAREHOUSE0 - SKU99: - - WAREHOUSE0 - SKU990: - - WAREHOUSE0 - SKU991: - - WAREHOUSE0 - SKU992: - - WAREHOUSE0 - SKU993: - - WAREHOUSE0 - SKU994: - - WAREHOUSE0 - SKU995: - - WAREHOUSE0 - SKU996: - - WAREHOUSE0 - SKU997: - - WAREHOUSE0 - SKU998: - - WAREHOUSE0 - SKU999: - - WAREHOUSE0 - WAREHOUSE0: - SKU0: - - SUPPLIER0 - SKU1: - - SUPPLIER0 - SKU10: - - SUPPLIER0 - SKU100: - - SUPPLIER0 - SKU101: - - SUPPLIER0 - SKU102: - - SUPPLIER0 - SKU103: - - SUPPLIER0 - SKU104: - - SUPPLIER0 - SKU105: - - SUPPLIER0 - SKU106: - - SUPPLIER0 - SKU107: - - SUPPLIER0 - SKU108: - - SUPPLIER0 - SKU109: - - SUPPLIER0 - SKU11: - - SUPPLIER0 - SKU110: - - SUPPLIER0 - SKU111: - - SUPPLIER0 - SKU112: - - SUPPLIER0 - SKU113: - - SUPPLIER0 - SKU114: - - SUPPLIER0 - SKU115: - - SUPPLIER0 - SKU116: - - SUPPLIER0 - SKU117: - - SUPPLIER0 - SKU118: - - SUPPLIER0 - SKU119: - - SUPPLIER0 - SKU12: - - SUPPLIER0 - SKU120: - - SUPPLIER0 - SKU121: - - SUPPLIER0 - SKU122: - - SUPPLIER0 - SKU123: - - SUPPLIER0 - SKU124: - - SUPPLIER0 - SKU125: - - SUPPLIER0 - SKU126: - - SUPPLIER0 - SKU127: - - SUPPLIER0 - SKU128: - - SUPPLIER0 - SKU129: - - SUPPLIER0 - SKU13: - - SUPPLIER0 - SKU130: - - SUPPLIER0 - SKU131: - - SUPPLIER0 - SKU132: - - SUPPLIER0 - SKU133: - - SUPPLIER0 - SKU134: - - SUPPLIER0 - SKU135: - - SUPPLIER0 - SKU136: - - SUPPLIER0 - SKU137: - - SUPPLIER0 - SKU138: - - SUPPLIER0 - SKU139: - - SUPPLIER0 - SKU14: - - SUPPLIER0 - SKU140: - - SUPPLIER0 - SKU141: - - SUPPLIER0 - SKU142: - - SUPPLIER0 - SKU143: - - SUPPLIER0 - SKU144: - - SUPPLIER0 - SKU145: - - SUPPLIER0 - SKU146: - - SUPPLIER0 - SKU147: - - SUPPLIER0 - SKU148: - - SUPPLIER0 - SKU149: - - SUPPLIER0 - SKU15: - - SUPPLIER0 - SKU150: - - SUPPLIER0 - SKU151: - - SUPPLIER0 - SKU152: - - SUPPLIER0 - SKU153: - - SUPPLIER0 - SKU154: - - SUPPLIER0 - SKU155: - - SUPPLIER0 - SKU156: - - SUPPLIER0 - SKU157: - - SUPPLIER0 - SKU158: - - SUPPLIER0 - SKU159: - - SUPPLIER0 - SKU16: - - SUPPLIER0 - SKU160: - - SUPPLIER0 - SKU161: - - SUPPLIER0 - SKU162: - - SUPPLIER0 - SKU163: - - SUPPLIER0 - SKU164: - - SUPPLIER0 - SKU165: - - SUPPLIER0 - SKU166: - - SUPPLIER0 - SKU167: - - SUPPLIER0 - SKU168: - - SUPPLIER0 - SKU169: - - SUPPLIER0 - SKU17: - - SUPPLIER0 - SKU170: - - SUPPLIER0 - SKU171: - - SUPPLIER0 - SKU172: - - SUPPLIER0 - SKU173: - - SUPPLIER0 - SKU174: - - SUPPLIER0 - SKU175: - - SUPPLIER0 - SKU176: - - SUPPLIER0 - SKU177: - - SUPPLIER0 - SKU178: - - SUPPLIER0 - SKU179: - - SUPPLIER0 - SKU18: - - SUPPLIER0 - SKU180: - - SUPPLIER0 - SKU181: - - SUPPLIER0 - SKU182: - - SUPPLIER0 - SKU183: - - SUPPLIER0 - SKU184: - - SUPPLIER0 - SKU185: - - SUPPLIER0 - SKU186: - - SUPPLIER0 - SKU187: - - SUPPLIER0 - SKU188: - - SUPPLIER0 - SKU189: - - SUPPLIER0 - SKU19: - - SUPPLIER0 - SKU190: - - SUPPLIER0 - SKU191: - - SUPPLIER0 - SKU192: - - SUPPLIER0 - SKU193: - - SUPPLIER0 - SKU194: - - SUPPLIER0 - SKU195: - - SUPPLIER0 - SKU196: - - SUPPLIER0 - SKU197: - - SUPPLIER0 - SKU198: - - SUPPLIER0 - SKU199: - - SUPPLIER0 - SKU2: - - SUPPLIER0 - SKU20: - - SUPPLIER0 - SKU200: - - SUPPLIER0 - SKU201: - - SUPPLIER0 - SKU202: - - SUPPLIER0 - SKU203: - - SUPPLIER0 - SKU204: - - SUPPLIER0 - SKU205: - - SUPPLIER0 - SKU206: - - SUPPLIER0 - SKU207: - - SUPPLIER0 - SKU208: - - SUPPLIER0 - SKU209: - - SUPPLIER0 - SKU21: - - SUPPLIER0 - SKU210: - - SUPPLIER0 - SKU211: - - SUPPLIER0 - SKU212: - - SUPPLIER0 - SKU213: - - SUPPLIER0 - SKU214: - - SUPPLIER0 - SKU215: - - SUPPLIER0 - SKU216: - - SUPPLIER0 - SKU217: - - SUPPLIER0 - SKU218: - - SUPPLIER0 - SKU219: - - SUPPLIER0 - SKU22: - - SUPPLIER0 - SKU220: - - SUPPLIER0 - SKU221: - - SUPPLIER0 - SKU222: - - SUPPLIER0 - SKU223: - - SUPPLIER0 - SKU224: - - SUPPLIER0 - SKU225: - - SUPPLIER0 - SKU226: - - SUPPLIER0 - SKU227: - - SUPPLIER0 - SKU228: - - SUPPLIER0 - SKU229: - - SUPPLIER0 - SKU23: - - SUPPLIER0 - SKU230: - - SUPPLIER0 - SKU231: - - SUPPLIER0 - SKU232: - - SUPPLIER0 - SKU233: - - SUPPLIER0 - SKU234: - - SUPPLIER0 - SKU235: - - SUPPLIER0 - SKU236: - - SUPPLIER0 - SKU237: - - SUPPLIER0 - SKU238: - - SUPPLIER0 - SKU239: - - SUPPLIER0 - SKU24: - - SUPPLIER0 - SKU240: - - SUPPLIER0 - SKU241: - - SUPPLIER0 - SKU242: - - SUPPLIER0 - SKU243: - - SUPPLIER0 - SKU244: - - SUPPLIER0 - SKU245: - - SUPPLIER0 - SKU246: - - SUPPLIER0 - SKU247: - - SUPPLIER0 - SKU248: - - SUPPLIER0 - SKU249: - - SUPPLIER0 - SKU25: - - SUPPLIER0 - SKU250: - - SUPPLIER0 - SKU251: - - SUPPLIER0 - SKU252: - - SUPPLIER0 - SKU253: - - SUPPLIER0 - SKU254: - - SUPPLIER0 - SKU255: - - SUPPLIER0 - SKU256: - - SUPPLIER0 - SKU257: - - SUPPLIER0 - SKU258: - - SUPPLIER0 - SKU259: - - SUPPLIER0 - SKU26: - - SUPPLIER0 - SKU260: - - SUPPLIER0 - SKU261: - - SUPPLIER0 - SKU262: - - SUPPLIER0 - SKU263: - - SUPPLIER0 - SKU264: - - SUPPLIER0 - SKU265: - - SUPPLIER0 - SKU266: - - SUPPLIER0 - SKU267: - - SUPPLIER0 - SKU268: - - SUPPLIER0 - SKU269: - - SUPPLIER0 - SKU27: - - SUPPLIER0 - SKU270: - - SUPPLIER0 - SKU271: - - SUPPLIER0 - SKU272: - - SUPPLIER0 - SKU273: - - SUPPLIER0 - SKU274: - - SUPPLIER0 - SKU275: - - SUPPLIER0 - SKU276: - - SUPPLIER0 - SKU277: - - SUPPLIER0 - SKU278: - - SUPPLIER0 - SKU279: - - SUPPLIER0 - SKU28: - - SUPPLIER0 - SKU280: - - SUPPLIER0 - SKU281: - - SUPPLIER0 - SKU282: - - SUPPLIER0 - SKU283: - - SUPPLIER0 - SKU284: - - SUPPLIER0 - SKU285: - - SUPPLIER0 - SKU286: - - SUPPLIER0 - SKU287: - - SUPPLIER0 - SKU288: - - SUPPLIER0 - SKU289: - - SUPPLIER0 - SKU29: - - SUPPLIER0 - SKU290: - - SUPPLIER0 - SKU291: - - SUPPLIER0 - SKU292: - - SUPPLIER0 - SKU293: - - SUPPLIER0 - SKU294: - - SUPPLIER0 - SKU295: - - SUPPLIER0 - SKU296: - - SUPPLIER0 - SKU297: - - SUPPLIER0 - SKU298: - - SUPPLIER0 - SKU299: - - SUPPLIER0 - SKU3: - - SUPPLIER0 - SKU30: - - SUPPLIER0 - SKU300: - - SUPPLIER0 - SKU301: - - SUPPLIER0 - SKU302: - - SUPPLIER0 - SKU303: - - SUPPLIER0 - SKU304: - - SUPPLIER0 - SKU305: - - SUPPLIER0 - SKU306: - - SUPPLIER0 - SKU307: - - SUPPLIER0 - SKU308: - - SUPPLIER0 - SKU309: - - SUPPLIER0 - SKU31: - - SUPPLIER0 - SKU310: - - SUPPLIER0 - SKU311: - - SUPPLIER0 - SKU312: - - SUPPLIER0 - SKU313: - - SUPPLIER0 - SKU314: - - SUPPLIER0 - SKU315: - - SUPPLIER0 - SKU316: - - SUPPLIER0 - SKU317: - - SUPPLIER0 - SKU318: - - SUPPLIER0 - SKU319: - - SUPPLIER0 - SKU32: - - SUPPLIER0 - SKU320: - - SUPPLIER0 - SKU321: - - SUPPLIER0 - SKU322: - - SUPPLIER0 - SKU323: - - SUPPLIER0 - SKU324: - - SUPPLIER0 - SKU325: - - SUPPLIER0 - SKU326: - - SUPPLIER0 - SKU327: - - SUPPLIER0 - SKU328: - - SUPPLIER0 - SKU329: - - SUPPLIER0 - SKU33: - - SUPPLIER0 - SKU330: - - SUPPLIER0 - SKU331: - - SUPPLIER0 - SKU332: - - SUPPLIER0 - SKU333: - - SUPPLIER0 - SKU334: - - SUPPLIER0 - SKU335: - - SUPPLIER0 - SKU336: - - SUPPLIER0 - SKU337: - - SUPPLIER0 - SKU338: - - SUPPLIER0 - SKU339: - - SUPPLIER0 - SKU34: - - SUPPLIER0 - SKU340: - - SUPPLIER0 - SKU341: - - SUPPLIER0 - SKU342: - - SUPPLIER0 - SKU343: - - SUPPLIER0 - SKU344: - - SUPPLIER0 - SKU345: - - SUPPLIER0 - SKU346: - - SUPPLIER0 - SKU347: - - SUPPLIER0 - SKU348: - - SUPPLIER0 - SKU349: - - SUPPLIER0 - SKU35: - - SUPPLIER0 - SKU350: - - SUPPLIER0 - SKU351: - - SUPPLIER0 - SKU352: - - SUPPLIER0 - SKU353: - - SUPPLIER0 - SKU354: - - SUPPLIER0 - SKU355: - - SUPPLIER0 - SKU356: - - SUPPLIER0 - SKU357: - - SUPPLIER0 - SKU358: - - SUPPLIER0 - SKU359: - - SUPPLIER0 - SKU36: - - SUPPLIER0 - SKU360: - - SUPPLIER0 - SKU361: - - SUPPLIER0 - SKU362: - - SUPPLIER0 - SKU363: - - SUPPLIER0 - SKU364: - - SUPPLIER0 - SKU365: - - SUPPLIER0 - SKU366: - - SUPPLIER0 - SKU367: - - SUPPLIER0 - SKU368: - - SUPPLIER0 - SKU369: - - SUPPLIER0 - SKU37: - - SUPPLIER0 - SKU370: - - SUPPLIER0 - SKU371: - - SUPPLIER0 - SKU372: - - SUPPLIER0 - SKU373: - - SUPPLIER0 - SKU374: - - SUPPLIER0 - SKU375: - - SUPPLIER0 - SKU376: - - SUPPLIER0 - SKU377: - - SUPPLIER0 - SKU378: - - SUPPLIER0 - SKU379: - - SUPPLIER0 - SKU38: - - SUPPLIER0 - SKU380: - - SUPPLIER0 - SKU381: - - SUPPLIER0 - SKU382: - - SUPPLIER0 - SKU383: - - SUPPLIER0 - SKU384: - - SUPPLIER0 - SKU385: - - SUPPLIER0 - SKU386: - - SUPPLIER0 - SKU387: - - SUPPLIER0 - SKU388: - - SUPPLIER0 - SKU389: - - SUPPLIER0 - SKU39: - - SUPPLIER0 - SKU390: - - SUPPLIER0 - SKU391: - - SUPPLIER0 - SKU392: - - SUPPLIER0 - SKU393: - - SUPPLIER0 - SKU394: - - SUPPLIER0 - SKU395: - - SUPPLIER0 - SKU396: - - SUPPLIER0 - SKU397: - - SUPPLIER0 - SKU398: - - SUPPLIER0 - SKU399: - - SUPPLIER0 - SKU4: - - SUPPLIER0 - SKU40: - - SUPPLIER0 - SKU400: - - SUPPLIER0 - SKU401: - - SUPPLIER0 - SKU402: - - SUPPLIER0 - SKU403: - - SUPPLIER0 - SKU404: - - SUPPLIER0 - SKU405: - - SUPPLIER0 - SKU406: - - SUPPLIER0 - SKU407: - - SUPPLIER0 - SKU408: - - SUPPLIER0 - SKU409: - - SUPPLIER0 - SKU41: - - SUPPLIER0 - SKU410: - - SUPPLIER0 - SKU411: - - SUPPLIER0 - SKU412: - - SUPPLIER0 - SKU413: - - SUPPLIER0 - SKU414: - - SUPPLIER0 - SKU415: - - SUPPLIER0 - SKU416: - - SUPPLIER0 - SKU417: - - SUPPLIER0 - SKU418: - - SUPPLIER0 - SKU419: - - SUPPLIER0 - SKU42: - - SUPPLIER0 - SKU420: - - SUPPLIER0 - SKU421: - - SUPPLIER0 - SKU422: - - SUPPLIER0 - SKU423: - - SUPPLIER0 - SKU424: - - SUPPLIER0 - SKU425: - - SUPPLIER0 - SKU426: - - SUPPLIER0 - SKU427: - - SUPPLIER0 - SKU428: - - SUPPLIER0 - SKU429: - - SUPPLIER0 - SKU43: - - SUPPLIER0 - SKU430: - - SUPPLIER0 - SKU431: - - SUPPLIER0 - SKU432: - - SUPPLIER0 - SKU433: - - SUPPLIER0 - SKU434: - - SUPPLIER0 - SKU435: - - SUPPLIER0 - SKU436: - - SUPPLIER0 - SKU437: - - SUPPLIER0 - SKU438: - - SUPPLIER0 - SKU439: - - SUPPLIER0 - SKU44: - - SUPPLIER0 - SKU440: - - SUPPLIER0 - SKU441: - - SUPPLIER0 - SKU442: - - SUPPLIER0 - SKU443: - - SUPPLIER0 - SKU444: - - SUPPLIER0 - SKU445: - - SUPPLIER0 - SKU446: - - SUPPLIER0 - SKU447: - - SUPPLIER0 - SKU448: - - SUPPLIER0 - SKU449: - - SUPPLIER0 - SKU45: - - SUPPLIER0 - SKU450: - - SUPPLIER0 - SKU451: - - SUPPLIER0 - SKU452: - - SUPPLIER0 - SKU453: - - SUPPLIER0 - SKU454: - - SUPPLIER0 - SKU455: - - SUPPLIER0 - SKU456: - - SUPPLIER0 - SKU457: - - SUPPLIER0 - SKU458: - - SUPPLIER0 - SKU459: - - SUPPLIER0 - SKU46: - - SUPPLIER0 - SKU460: - - SUPPLIER0 - SKU461: - - SUPPLIER0 - SKU462: - - SUPPLIER0 - SKU463: - - SUPPLIER0 - SKU464: - - SUPPLIER0 - SKU465: - - SUPPLIER0 - SKU466: - - SUPPLIER0 - SKU467: - - SUPPLIER0 - SKU468: - - SUPPLIER0 - SKU469: - - SUPPLIER0 - SKU47: - - SUPPLIER0 - SKU470: - - SUPPLIER0 - SKU471: - - SUPPLIER0 - SKU472: - - SUPPLIER0 - SKU473: - - SUPPLIER0 - SKU474: - - SUPPLIER0 - SKU475: - - SUPPLIER0 - SKU476: - - SUPPLIER0 - SKU477: - - SUPPLIER0 - SKU478: - - SUPPLIER0 - SKU479: - - SUPPLIER0 - SKU48: - - SUPPLIER0 - SKU480: - - SUPPLIER0 - SKU481: - - SUPPLIER0 - SKU482: - - SUPPLIER0 - SKU483: - - SUPPLIER0 - SKU484: - - SUPPLIER0 - SKU485: - - SUPPLIER0 - SKU486: - - SUPPLIER0 - SKU487: - - SUPPLIER0 - SKU488: - - SUPPLIER0 - SKU489: - - SUPPLIER0 - SKU49: - - SUPPLIER0 - SKU490: - - SUPPLIER0 - SKU491: - - SUPPLIER0 - SKU492: - - SUPPLIER0 - SKU493: - - SUPPLIER0 - SKU494: - - SUPPLIER0 - SKU495: - - SUPPLIER0 - SKU496: - - SUPPLIER0 - SKU497: - - SUPPLIER0 - SKU498: - - SUPPLIER0 - SKU499: - - SUPPLIER0 - SKU5: - - SUPPLIER0 - SKU50: - - SUPPLIER0 - SKU500: - - SUPPLIER0 - SKU501: - - SUPPLIER0 - SKU502: - - SUPPLIER0 - SKU503: - - SUPPLIER0 - SKU504: - - SUPPLIER0 - SKU505: - - SUPPLIER0 - SKU506: - - SUPPLIER0 - SKU507: - - SUPPLIER0 - SKU508: - - SUPPLIER0 - SKU509: - - SUPPLIER0 - SKU51: - - SUPPLIER0 - SKU510: - - SUPPLIER0 - SKU511: - - SUPPLIER0 - SKU512: - - SUPPLIER0 - SKU513: - - SUPPLIER0 - SKU514: - - SUPPLIER0 - SKU515: - - SUPPLIER0 - SKU516: - - SUPPLIER0 - SKU517: - - SUPPLIER0 - SKU518: - - SUPPLIER0 - SKU519: - - SUPPLIER0 - SKU52: - - SUPPLIER0 - SKU520: - - SUPPLIER0 - SKU521: - - SUPPLIER0 - SKU522: - - SUPPLIER0 - SKU523: - - SUPPLIER0 - SKU524: - - SUPPLIER0 - SKU525: - - SUPPLIER0 - SKU526: - - SUPPLIER0 - SKU527: - - SUPPLIER0 - SKU528: - - SUPPLIER0 - SKU529: - - SUPPLIER0 - SKU53: - - SUPPLIER0 - SKU530: - - SUPPLIER0 - SKU531: - - SUPPLIER0 - SKU532: - - SUPPLIER0 - SKU533: - - SUPPLIER0 - SKU534: - - SUPPLIER0 - SKU535: - - SUPPLIER0 - SKU536: - - SUPPLIER0 - SKU537: - - SUPPLIER0 - SKU538: - - SUPPLIER0 - SKU539: - - SUPPLIER0 - SKU54: - - SUPPLIER0 - SKU540: - - SUPPLIER0 - SKU541: - - SUPPLIER0 - SKU542: - - SUPPLIER0 - SKU543: - - SUPPLIER0 - SKU544: - - SUPPLIER0 - SKU545: - - SUPPLIER0 - SKU546: - - SUPPLIER0 - SKU547: - - SUPPLIER0 - SKU548: - - SUPPLIER0 - SKU549: - - SUPPLIER0 - SKU55: - - SUPPLIER0 - SKU550: - - SUPPLIER0 - SKU551: - - SUPPLIER0 - SKU552: - - SUPPLIER0 - SKU553: - - SUPPLIER0 - SKU554: - - SUPPLIER0 - SKU555: - - SUPPLIER0 - SKU556: - - SUPPLIER0 - SKU557: - - SUPPLIER0 - SKU558: - - SUPPLIER0 - SKU559: - - SUPPLIER0 - SKU56: - - SUPPLIER0 - SKU560: - - SUPPLIER0 - SKU561: - - SUPPLIER0 - SKU562: - - SUPPLIER0 - SKU563: - - SUPPLIER0 - SKU564: - - SUPPLIER0 - SKU565: - - SUPPLIER0 - SKU566: - - SUPPLIER0 - SKU567: - - SUPPLIER0 - SKU568: - - SUPPLIER0 - SKU569: - - SUPPLIER0 - SKU57: - - SUPPLIER0 - SKU570: - - SUPPLIER0 - SKU571: - - SUPPLIER0 - SKU572: - - SUPPLIER0 - SKU573: - - SUPPLIER0 - SKU574: - - SUPPLIER0 - SKU575: - - SUPPLIER0 - SKU576: - - SUPPLIER0 - SKU577: - - SUPPLIER0 - SKU578: - - SUPPLIER0 - SKU579: - - SUPPLIER0 - SKU58: - - SUPPLIER0 - SKU580: - - SUPPLIER0 - SKU581: - - SUPPLIER0 - SKU582: - - SUPPLIER0 - SKU583: - - SUPPLIER0 - SKU584: - - SUPPLIER0 - SKU585: - - SUPPLIER0 - SKU586: - - SUPPLIER0 - SKU587: - - SUPPLIER0 - SKU588: - - SUPPLIER0 - SKU589: - - SUPPLIER0 - SKU59: - - SUPPLIER0 - SKU590: - - SUPPLIER0 - SKU591: - - SUPPLIER0 - SKU592: - - SUPPLIER0 - SKU593: - - SUPPLIER0 - SKU594: - - SUPPLIER0 - SKU595: - - SUPPLIER0 - SKU596: - - SUPPLIER0 - SKU597: - - SUPPLIER0 - SKU598: - - SUPPLIER0 - SKU599: - - SUPPLIER0 - SKU6: - - SUPPLIER0 - SKU60: - - SUPPLIER0 - SKU600: - - SUPPLIER0 - SKU601: - - SUPPLIER0 - SKU602: - - SUPPLIER0 - SKU603: - - SUPPLIER0 - SKU604: - - SUPPLIER0 - SKU605: - - SUPPLIER0 - SKU606: - - SUPPLIER0 - SKU607: - - SUPPLIER0 - SKU608: - - SUPPLIER0 - SKU609: - - SUPPLIER0 - SKU61: - - SUPPLIER0 - SKU610: - - SUPPLIER0 - SKU611: - - SUPPLIER0 - SKU612: - - SUPPLIER0 - SKU613: - - SUPPLIER0 - SKU614: - - SUPPLIER0 - SKU615: - - SUPPLIER0 - SKU616: - - SUPPLIER0 - SKU617: - - SUPPLIER0 - SKU618: - - SUPPLIER0 - SKU619: - - SUPPLIER0 - SKU62: - - SUPPLIER0 - SKU620: - - SUPPLIER0 - SKU621: - - SUPPLIER0 - SKU622: - - SUPPLIER0 - SKU623: - - SUPPLIER0 - SKU624: - - SUPPLIER0 - SKU625: - - SUPPLIER0 - SKU626: - - SUPPLIER0 - SKU627: - - SUPPLIER0 - SKU628: - - SUPPLIER0 - SKU629: - - SUPPLIER0 - SKU63: - - SUPPLIER0 - SKU630: - - SUPPLIER0 - SKU631: - - SUPPLIER0 - SKU632: - - SUPPLIER0 - SKU633: - - SUPPLIER0 - SKU634: - - SUPPLIER0 - SKU635: - - SUPPLIER0 - SKU636: - - SUPPLIER0 - SKU637: - - SUPPLIER0 - SKU638: - - SUPPLIER0 - SKU639: - - SUPPLIER0 - SKU64: - - SUPPLIER0 - SKU640: - - SUPPLIER0 - SKU641: - - SUPPLIER0 - SKU642: - - SUPPLIER0 - SKU643: - - SUPPLIER0 - SKU644: - - SUPPLIER0 - SKU645: - - SUPPLIER0 - SKU646: - - SUPPLIER0 - SKU647: - - SUPPLIER0 - SKU648: - - SUPPLIER0 - SKU649: - - SUPPLIER0 - SKU65: - - SUPPLIER0 - SKU650: - - SUPPLIER0 - SKU651: - - SUPPLIER0 - SKU652: - - SUPPLIER0 - SKU653: - - SUPPLIER0 - SKU654: - - SUPPLIER0 - SKU655: - - SUPPLIER0 - SKU656: - - SUPPLIER0 - SKU657: - - SUPPLIER0 - SKU658: - - SUPPLIER0 - SKU659: - - SUPPLIER0 - SKU66: - - SUPPLIER0 - SKU660: - - SUPPLIER0 - SKU661: - - SUPPLIER0 - SKU662: - - SUPPLIER0 - SKU663: - - SUPPLIER0 - SKU664: - - SUPPLIER0 - SKU665: - - SUPPLIER0 - SKU666: - - SUPPLIER0 - SKU667: - - SUPPLIER0 - SKU668: - - SUPPLIER0 - SKU669: - - SUPPLIER0 - SKU67: - - SUPPLIER0 - SKU670: - - SUPPLIER0 - SKU671: - - SUPPLIER0 - SKU672: - - SUPPLIER0 - SKU673: - - SUPPLIER0 - SKU674: - - SUPPLIER0 - SKU675: - - SUPPLIER0 - SKU676: - - SUPPLIER0 - SKU677: - - SUPPLIER0 - SKU678: - - SUPPLIER0 - SKU679: - - SUPPLIER0 - SKU68: - - SUPPLIER0 - SKU680: - - SUPPLIER0 - SKU681: - - SUPPLIER0 - SKU682: - - SUPPLIER0 - SKU683: - - SUPPLIER0 - SKU684: - - SUPPLIER0 - SKU685: - - SUPPLIER0 - SKU686: - - SUPPLIER0 - SKU687: - - SUPPLIER0 - SKU688: - - SUPPLIER0 - SKU689: - - SUPPLIER0 - SKU69: - - SUPPLIER0 - SKU690: - - SUPPLIER0 - SKU691: - - SUPPLIER0 - SKU692: - - SUPPLIER0 - SKU693: - - SUPPLIER0 - SKU694: - - SUPPLIER0 - SKU695: - - SUPPLIER0 - SKU696: - - SUPPLIER0 - SKU697: - - SUPPLIER0 - SKU698: - - SUPPLIER0 - SKU699: - - SUPPLIER0 - SKU7: - - SUPPLIER0 - SKU70: - - SUPPLIER0 - SKU700: - - SUPPLIER0 - SKU701: - - SUPPLIER0 - SKU702: - - SUPPLIER0 - SKU703: - - SUPPLIER0 - SKU704: - - SUPPLIER0 - SKU705: - - SUPPLIER0 - SKU706: - - SUPPLIER0 - SKU707: - - SUPPLIER0 - SKU708: - - SUPPLIER0 - SKU709: - - SUPPLIER0 - SKU71: - - SUPPLIER0 - SKU710: - - SUPPLIER0 - SKU711: - - SUPPLIER0 - SKU712: - - SUPPLIER0 - SKU713: - - SUPPLIER0 - SKU714: - - SUPPLIER0 - SKU715: - - SUPPLIER0 - SKU716: - - SUPPLIER0 - SKU717: - - SUPPLIER0 - SKU718: - - SUPPLIER0 - SKU719: - - SUPPLIER0 - SKU72: - - SUPPLIER0 - SKU720: - - SUPPLIER0 - SKU721: - - SUPPLIER0 - SKU722: - - SUPPLIER0 - SKU723: - - SUPPLIER0 - SKU724: - - SUPPLIER0 - SKU725: - - SUPPLIER0 - SKU726: - - SUPPLIER0 - SKU727: - - SUPPLIER0 - SKU728: - - SUPPLIER0 - SKU729: - - SUPPLIER0 - SKU73: - - SUPPLIER0 - SKU730: - - SUPPLIER0 - SKU731: - - SUPPLIER0 - SKU732: - - SUPPLIER0 - SKU733: - - SUPPLIER0 - SKU734: - - SUPPLIER0 - SKU735: - - SUPPLIER0 - SKU736: - - SUPPLIER0 - SKU737: - - SUPPLIER0 - SKU738: - - SUPPLIER0 - SKU739: - - SUPPLIER0 - SKU74: - - SUPPLIER0 - SKU740: - - SUPPLIER0 - SKU741: - - SUPPLIER0 - SKU742: - - SUPPLIER0 - SKU743: - - SUPPLIER0 - SKU744: - - SUPPLIER0 - SKU745: - - SUPPLIER0 - SKU746: - - SUPPLIER0 - SKU747: - - SUPPLIER0 - SKU748: - - SUPPLIER0 - SKU749: - - SUPPLIER0 - SKU75: - - SUPPLIER0 - SKU750: - - SUPPLIER0 - SKU751: - - SUPPLIER0 - SKU752: - - SUPPLIER0 - SKU753: - - SUPPLIER0 - SKU754: - - SUPPLIER0 - SKU755: - - SUPPLIER0 - SKU756: - - SUPPLIER0 - SKU757: - - SUPPLIER0 - SKU758: - - SUPPLIER0 - SKU759: - - SUPPLIER0 - SKU76: - - SUPPLIER0 - SKU760: - - SUPPLIER0 - SKU761: - - SUPPLIER0 - SKU762: - - SUPPLIER0 - SKU763: - - SUPPLIER0 - SKU764: - - SUPPLIER0 - SKU765: - - SUPPLIER0 - SKU766: - - SUPPLIER0 - SKU767: - - SUPPLIER0 - SKU768: - - SUPPLIER0 - SKU769: - - SUPPLIER0 - SKU77: - - SUPPLIER0 - SKU770: - - SUPPLIER0 - SKU771: - - SUPPLIER0 - SKU772: - - SUPPLIER0 - SKU773: - - SUPPLIER0 - SKU774: - - SUPPLIER0 - SKU775: - - SUPPLIER0 - SKU776: - - SUPPLIER0 - SKU777: - - SUPPLIER0 - SKU778: - - SUPPLIER0 - SKU779: - - SUPPLIER0 - SKU78: - - SUPPLIER0 - SKU780: - - SUPPLIER0 - SKU781: - - SUPPLIER0 - SKU782: - - SUPPLIER0 - SKU783: - - SUPPLIER0 - SKU784: - - SUPPLIER0 - SKU785: - - SUPPLIER0 - SKU786: - - SUPPLIER0 - SKU787: - - SUPPLIER0 - SKU788: - - SUPPLIER0 - SKU789: - - SUPPLIER0 - SKU79: - - SUPPLIER0 - SKU790: - - SUPPLIER0 - SKU791: - - SUPPLIER0 - SKU792: - - SUPPLIER0 - SKU793: - - SUPPLIER0 - SKU794: - - SUPPLIER0 - SKU795: - - SUPPLIER0 - SKU796: - - SUPPLIER0 - SKU797: - - SUPPLIER0 - SKU798: - - SUPPLIER0 - SKU799: - - SUPPLIER0 - SKU8: - - SUPPLIER0 - SKU80: - - SUPPLIER0 - SKU800: - - SUPPLIER0 - SKU801: - - SUPPLIER0 - SKU802: - - SUPPLIER0 - SKU803: - - SUPPLIER0 - SKU804: - - SUPPLIER0 - SKU805: - - SUPPLIER0 - SKU806: - - SUPPLIER0 - SKU807: - - SUPPLIER0 - SKU808: - - SUPPLIER0 - SKU809: - - SUPPLIER0 - SKU81: - - SUPPLIER0 - SKU810: - - SUPPLIER0 - SKU811: - - SUPPLIER0 - SKU812: - - SUPPLIER0 - SKU813: - - SUPPLIER0 - SKU814: - - SUPPLIER0 - SKU815: - - SUPPLIER0 - SKU816: - - SUPPLIER0 - SKU817: - - SUPPLIER0 - SKU818: - - SUPPLIER0 - SKU819: - - SUPPLIER0 - SKU82: - - SUPPLIER0 - SKU820: - - SUPPLIER0 - SKU821: - - SUPPLIER0 - SKU822: - - SUPPLIER0 - SKU823: - - SUPPLIER0 - SKU824: - - SUPPLIER0 - SKU825: - - SUPPLIER0 - SKU826: - - SUPPLIER0 - SKU827: - - SUPPLIER0 - SKU828: - - SUPPLIER0 - SKU829: - - SUPPLIER0 - SKU83: - - SUPPLIER0 - SKU830: - - SUPPLIER0 - SKU831: - - SUPPLIER0 - SKU832: - - SUPPLIER0 - SKU833: - - SUPPLIER0 - SKU834: - - SUPPLIER0 - SKU835: - - SUPPLIER0 - SKU836: - - SUPPLIER0 - SKU837: - - SUPPLIER0 - SKU838: - - SUPPLIER0 - SKU839: - - SUPPLIER0 - SKU84: - - SUPPLIER0 - SKU840: - - SUPPLIER0 - SKU841: - - SUPPLIER0 - SKU842: - - SUPPLIER0 - SKU843: - - SUPPLIER0 - SKU844: - - SUPPLIER0 - SKU845: - - SUPPLIER0 - SKU846: - - SUPPLIER0 - SKU847: - - SUPPLIER0 - SKU848: - - SUPPLIER0 - SKU849: - - SUPPLIER0 - SKU85: - - SUPPLIER0 - SKU850: - - SUPPLIER0 - SKU851: - - SUPPLIER0 - SKU852: - - SUPPLIER0 - SKU853: - - SUPPLIER0 - SKU854: - - SUPPLIER0 - SKU855: - - SUPPLIER0 - SKU856: - - SUPPLIER0 - SKU857: - - SUPPLIER0 - SKU858: - - SUPPLIER0 - SKU859: - - SUPPLIER0 - SKU86: - - SUPPLIER0 - SKU860: - - SUPPLIER0 - SKU861: - - SUPPLIER0 - SKU862: - - SUPPLIER0 - SKU863: - - SUPPLIER0 - SKU864: - - SUPPLIER0 - SKU865: - - SUPPLIER0 - SKU866: - - SUPPLIER0 - SKU867: - - SUPPLIER0 - SKU868: - - SUPPLIER0 - SKU869: - - SUPPLIER0 - SKU87: - - SUPPLIER0 - SKU870: - - SUPPLIER0 - SKU871: - - SUPPLIER0 - SKU872: - - SUPPLIER0 - SKU873: - - SUPPLIER0 - SKU874: - - SUPPLIER0 - SKU875: - - SUPPLIER0 - SKU876: - - SUPPLIER0 - SKU877: - - SUPPLIER0 - SKU878: - - SUPPLIER0 - SKU879: - - SUPPLIER0 - SKU88: - - SUPPLIER0 - SKU880: - - SUPPLIER0 - SKU881: - - SUPPLIER0 - SKU882: - - SUPPLIER0 - SKU883: - - SUPPLIER0 - SKU884: - - SUPPLIER0 - SKU885: - - SUPPLIER0 - SKU886: - - SUPPLIER0 - SKU887: - - SUPPLIER0 - SKU888: - - SUPPLIER0 - SKU889: - - SUPPLIER0 - SKU89: - - SUPPLIER0 - SKU890: - - SUPPLIER0 - SKU891: - - SUPPLIER0 - SKU892: - - SUPPLIER0 - SKU893: - - SUPPLIER0 - SKU894: - - SUPPLIER0 - SKU895: - - SUPPLIER0 - SKU896: - - SUPPLIER0 - SKU897: - - SUPPLIER0 - SKU898: - - SUPPLIER0 - SKU899: - - SUPPLIER0 - SKU9: - - SUPPLIER0 - SKU90: - - SUPPLIER0 - SKU900: - - SUPPLIER0 - SKU901: - - SUPPLIER0 - SKU902: - - SUPPLIER0 - SKU903: - - SUPPLIER0 - SKU904: - - SUPPLIER0 - SKU905: - - SUPPLIER0 - SKU906: - - SUPPLIER0 - SKU907: - - SUPPLIER0 - SKU908: - - SUPPLIER0 - SKU909: - - SUPPLIER0 - SKU91: - - SUPPLIER0 - SKU910: - - SUPPLIER0 - SKU911: - - SUPPLIER0 - SKU912: - - SUPPLIER0 - SKU913: - - SUPPLIER0 - SKU914: - - SUPPLIER0 - SKU915: - - SUPPLIER0 - SKU916: - - SUPPLIER0 - SKU917: - - SUPPLIER0 - SKU918: - - SUPPLIER0 - SKU919: - - SUPPLIER0 - SKU92: - - SUPPLIER0 - SKU920: - - SUPPLIER0 - SKU921: - - SUPPLIER0 - SKU922: - - SUPPLIER0 - SKU923: - - SUPPLIER0 - SKU924: - - SUPPLIER0 - SKU925: - - SUPPLIER0 - SKU926: - - SUPPLIER0 - SKU927: - - SUPPLIER0 - SKU928: - - SUPPLIER0 - SKU929: - - SUPPLIER0 - SKU93: - - SUPPLIER0 - SKU930: - - SUPPLIER0 - SKU931: - - SUPPLIER0 - SKU932: - - SUPPLIER0 - SKU933: - - SUPPLIER0 - SKU934: - - SUPPLIER0 - SKU935: - - SUPPLIER0 - SKU936: - - SUPPLIER0 - SKU937: - - SUPPLIER0 - SKU938: - - SUPPLIER0 - SKU939: - - SUPPLIER0 - SKU94: - - SUPPLIER0 - SKU940: - - SUPPLIER0 - SKU941: - - SUPPLIER0 - SKU942: - - SUPPLIER0 - SKU943: - - SUPPLIER0 - SKU944: - - SUPPLIER0 - SKU945: - - SUPPLIER0 - SKU946: - - SUPPLIER0 - SKU947: - - SUPPLIER0 - SKU948: - - SUPPLIER0 - SKU949: - - SUPPLIER0 - SKU95: - - SUPPLIER0 - SKU950: - - SUPPLIER0 - SKU951: - - SUPPLIER0 - SKU952: - - SUPPLIER0 - SKU953: - - SUPPLIER0 - SKU954: - - SUPPLIER0 - SKU955: - - SUPPLIER0 - SKU956: - - SUPPLIER0 - SKU957: - - SUPPLIER0 - SKU958: - - SUPPLIER0 - SKU959: - - SUPPLIER0 - SKU96: - - SUPPLIER0 - SKU960: - - SUPPLIER0 - SKU961: - - SUPPLIER0 - SKU962: - - SUPPLIER0 - SKU963: - - SUPPLIER0 - SKU964: - - SUPPLIER0 - SKU965: - - SUPPLIER0 - SKU966: - - SUPPLIER0 - SKU967: - - SUPPLIER0 - SKU968: - - SUPPLIER0 - SKU969: - - SUPPLIER0 - SKU97: - - SUPPLIER0 - SKU970: - - SUPPLIER0 - SKU971: - - SUPPLIER0 - SKU972: - - SUPPLIER0 - SKU973: - - SUPPLIER0 - SKU974: - - SUPPLIER0 - SKU975: - - SUPPLIER0 - SKU976: - - SUPPLIER0 - SKU977: - - SUPPLIER0 - SKU978: - - SUPPLIER0 - SKU979: - - SUPPLIER0 - SKU98: - - SUPPLIER0 - SKU980: - - SUPPLIER0 - SKU981: - - SUPPLIER0 - SKU982: - - SUPPLIER0 - SKU983: - - SUPPLIER0 - SKU984: - - SUPPLIER0 - SKU985: - - SUPPLIER0 - SKU986: - - SUPPLIER0 - SKU987: - - SUPPLIER0 - SKU988: - - SUPPLIER0 - SKU989: - - SUPPLIER0 - SKU99: - - SUPPLIER0 - SKU990: - - SUPPLIER0 - SKU991: - - SUPPLIER0 - SKU992: - - SUPPLIER0 - SKU993: - - SUPPLIER0 - SKU994: - - SUPPLIER0 - SKU995: - - SUPPLIER0 - SKU996: - - SUPPLIER0 - SKU997: - - SUPPLIER0 - SKU998: - - SUPPLIER0 - SKU999: - - SUPPLIER0 diff --git a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml b/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml deleted file mode 100644 index 40c01fedb..000000000 --- a/maro/simulator/scenarios/supply_chain/topologies/sample/config.yml +++ /dev/null @@ -1,306 +0,0 @@ - -# TODO: which config to inherit -# base: "" - -#core: -# datamodels: "xxx" -# units: "xxx" -# facilities: "xxx" - - -facility_definitions: - # facility definition - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - # if true then will call generate function of class type - is_template: true - # config will be passed to generator as parameters - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - config: - # mark current unit/facility as a agent, out side may use this to choose policy. - agent_type: "facility" - - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - manufacture: - class: "ManufactureUnit" - config: - agent_type: "producer" - config: - agent_type: "facility" - - RetailerFacility: &retailer_facility - class: "RetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: "product" - consumer: - class: "ConsumerUnit" - config: - agent_type: "consumer" - seller: - class: "SellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: "facility" - -# common entity/unit definition as reference to simplify the file. -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 100 - -# a normal distribution definition -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -small_storage: &small_storage - # config of data model of this unit - config: - # other config or storage unit - capacity: 10000 - unit_storage_cost: 1 - -midium_storage: &midium_storage - config: - capacity: 20000 - unit_storage_cost: 1 - -huge_storage: &huge_storage - config: - capacity: 30000 - unit_storage_cost: 1 - -# sku list in this world -# this list do not contains price, cost or other facility related attributes, -# but just base info, like name, id, bom -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 12 - # bill of material that used produce current sku, empty means do not need source material - bom: - # key is the source sku name, value is quantity needed to use per time to produce current sku - sku3: 10 - - - id: 2 - name: "sku2" - output_units_per_lot: 1 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - -# world definitions -world: - # here we use reference to make it each to edit. - skus: *sku_definitions - - # facilities in this world - facilities: - - name: "Supplier_001" # name of the facility - # NOTE: here we do not use yaml anchor override, as it not support partial override with more than 1 level - # use the facility definition as base, then we can override configs partially. - definition_ref: "SupplierFacility" - - # sku list of this facility - skus: - sku3: # sku name and attributes needed for this facility - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" # production means this is the output production of this facility - cost: 10 - price: 10 - vlt: 1 - - # configuration of child units. - children: - # config of storage unit - storage: *small_storage - distribution: *normal_distribution - - # products use default config in core.yml - - # config of this facility - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Supplier_002" - definition_ref: "SupplierFacility" - - skus: - sku1: - init_stock: 100 - product_unit_cost: 1 - production_rate: 1 - type: "production" - cost: 10 - price: 100 - vlt: 1 - sku3: - init_stock: 100 - production_rate: 1 - type: "material" - cost: 10 - price: 100 - vlt: 1 - - children: - storage: *small_storage - distribution: *normal_distribution - - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - - skus: - sku1: - init_stock: 1000 - price: 100 - vlt: 1 - sku2: - init_stock: 1000 - price: 100 - vlt: 1 - sku3: - init_stock: 1000 - price: 100 - vlt: 1 - - children: - storage: *huge_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Retailer_001" - definition_ref: "RetailerFacility" - - skus: - sku1: - price: 300 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 # optional - vlt: 1 - sku3: - price: 200 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - sku2: - price: 100 - cost: 10 - init_stock: 100 - sale_gamma: 100 - backlog_ratio: 0.1 - vlt: 1 - - children: - storage: *midium_storage - - config: - order_cost: 0 - # topology used to specify the up/downstream for facilities - # we split it from facility, so that we can support configuration inherit to override it - # for a new topology - # TODO: change the name? - topology: - # key is current facility, value if upstream facilities that will provide a certain sku - Supplier_002: - # this config means "Supplier1" will purchase "sku3" from facility "Supplier3", - # or any other facility in the list - sku3: - - "Supplier_001" - Warehouse_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - Retailer_001: - sku1: - - "Supplier_002" - sku3: - - "Supplier_001" - - # map grid definitions - grid: - size: [20, 20] - - # facility position in grid - facilities: - Supplier_001: [0, 0] - Supplier_002: [3, 3] - Warehouse_001: [6, 6] - Retailer_001: [10, 18] - - # cells that un-traversable - blocks: - railroad: - - [10, 10] - - [10, 11] - - [10, 12] - - [11, 12] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 diff --git a/maro/simulator/scenarios/supply_chain/units/__init__.py b/maro/simulator/scenarios/supply_chain/units/__init__.py deleted file mode 100644 index 11f4abb3a..000000000 --- a/maro/simulator/scenarios/supply_chain/units/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .consumer import ConsumerUnit -from .distribution import DistributionUnit -from .extendunitbase import ExtendUnitBase -from .manufacture import ManufactureUnit -from .outerseller import DataFileDemandSampler, OuterSellerUnit, SellerDemandSampler -from .product import ProductUnit -from .seller import SellerUnit -from .simplemanufacture import SimpleManufactureUnit -from .storage import StorageUnit -from .storeproduct import StoreProductUnit -from .unitbase import UnitBase -from .vehicle import VehicleUnit diff --git a/maro/simulator/scenarios/supply_chain/units/consumer.py b/maro/simulator/scenarios/supply_chain/units/consumer.py deleted file mode 100644 index a9d408afb..000000000 --- a/maro/simulator/scenarios/supply_chain/units/consumer.py +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -from collections import Counter, defaultdict - -from scipy.ndimage.interpolation import shift - -from .. import ConsumerAction, ConsumerDataModel -from .extendunitbase import ExtendUnitBase -from .order import Order - - -class ConsumerUnit(ExtendUnitBase): - """Consumer unit used to generate order to purchase from upstream by action.""" - - def __init__(self): - super(ConsumerUnit, self).__init__() - - self.open_orders = defaultdict(Counter) - - # States in python side. - self.received = 0 - self.purchased = 0 - self.sources = [] - self.pending_order_daily = None - self.order_product_cost = 0 - - def on_order_reception(self, source_id: int, product_id: int, quantity: int, original_quantity: int): - """Called after order product is received. - - Args: - source_id (int): Where is the product from (facility id). - product_id (int): What product we received. - quantity (int): How many we received. - original_quantity (int): How many we ordered. - """ - self.received += quantity - - self.update_open_orders(source_id, product_id, -original_quantity) - - def update_open_orders(self, source_id: int, product_id: int, qty_delta: int): - """Update the order states. - - Args: - source_id (int): Where is the product from (facility id). - product_id (int): What product in the order. - qty_delta (int): Number of product to update (sum). - """ - # New order for product. - self.open_orders[source_id][product_id] += qty_delta - - def initialize(self): - super(ConsumerUnit, self).initialize() - - self.pending_order_daily = [0] * self.world.configs.settings["pending_order_len"] - - sku = self.facility.skus[self.product_id] - - order_cost = self.facility.get_config("order_cost") - assert isinstance(order_cost, int) - - assert isinstance(self.data_model, ConsumerDataModel) - self.data_model.initialize(sku.price, order_cost) - - if self.facility.upstreams is not None: - # Construct sources from facility's upstreams. - sources = self.facility.upstreams.get(self.product_id, None) - - if sources is not None: - # Is we are a supplier facility? - is_supplier = getattr(self.parent, "manufacture", None) is not None - - # Current sku information. - sku = self.world.get_sku_by_id(self.product_id) - - for source_facility in sources: - # We are a supplier unit, then the consumer is used to purchase source materials from upstreams. - # Try to find who will provide this kind of material. - if is_supplier: - if source_facility.products is not None: - for source_sku_id in sku.bom.keys(): - if source_sku_id in source_facility.products: - # This is a valid source facility. - self.sources.append(source_facility.id) - else: - # If we are not a manufacturing, just check if upstream have this sku configuration. - if sku.id in source_facility.skus: - self.sources.append(source_facility.id) - - def step(self, tick: int): - self._update_pending_order() - - assert isinstance(self.action, ConsumerAction) - - # NOTE: id == 0 means invalid,as our id is 1 based. - if not self.action or self.action.quantity <= 0 or self.action.product_id <= 0 or self.action.source_id == 0: - return - - # NOTE: we are using product unit as destination, - # so we expect the action.source_id is and id of product unit - self.update_open_orders(self.action.source_id, self.action.product_id, self.action.quantity) - - order = Order(self.facility, self.action.product_id, self.action.quantity, self.action.vlt) - - source_facility = self.world.get_facility_by_id(self.action.source_id) - - self.order_product_cost = source_facility.distribution.place_order(order) - - self.purchased = self.action.quantity - - def flush_states(self): - assert isinstance(self.action, ConsumerAction) - - if self.received > 0: - self.data_model.received = self.received - self.data_model.total_received += self.received - - if self.purchased > 0: - self.data_model.purchased = self.purchased - self.data_model.total_purchased += self.purchased - self.data_model.latest_consumptions = 1.0 - - if self.order_product_cost > 0: - self.data_model.order_product_cost = self.order_product_cost - - if self.action is not None and self.action.quantity > 0: - self.data_model.order_quantity = self.action.quantity - self.data_model.reward_discount = self.action.reward_discount - - def post_step(self, tick: int): - assert isinstance(self.action, ConsumerAction) - - # Clear the action states per step. - if self.action is not None: - self.data_model.latest_consumptions = 0 - self.data_model.reward_discount = 0 - - if self.action.quantity > 0: - self.data_model.order_quantity = 0 - - # This will set action to None. - super(ConsumerUnit, self).post_step(tick) - - if self.received > 0: - self.data_model.received = 0 - self.received = 0 - - if self.purchased > 0: - self.data_model.purchased = 0 - self.purchased = 0 - - if self.order_product_cost > 0: - self.data_model.order_product_cost = 0 - self.order_product_cost = 0 - - def reset(self): - super(ConsumerUnit, self).reset() - - self.pending_order_daily = [0] * self.world.configs.settings["pending_order_len"] - - self.open_orders.clear() - - def get_in_transit_quantity(self): - quantity = 0 - - for source_id, orders in self.open_orders.items(): - quantity += orders.get(self.product_id, 0) - - return quantity - - def _update_pending_order(self): - self.pending_order_daily = shift(self.pending_order_daily, -1, cval=0) - - def get_unit_info(self): - info = super().get_unit_info() - - info["sources"] = self.sources - - return info diff --git a/maro/simulator/scenarios/supply_chain/units/distribution.py b/maro/simulator/scenarios/supply_chain/units/distribution.py deleted file mode 100644 index 5860e7a5a..000000000 --- a/maro/simulator/scenarios/supply_chain/units/distribution.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from collections import Counter, defaultdict, deque -from typing import Dict, List - -from .order import Order -from .unitbase import UnitBase -from .vehicle import VehicleUnit - - -class DistributionUnit(UnitBase): - """Unit that used to receive and execute orders from downstream facilities. - - One distribution can accept all kind of sku order. - """ - # Vehicle unit list of this distribution unit. - vehicles: List[VehicleUnit] = None - - def __init__(self): - super().__init__() - self.order_queue = deque() - - self.transportation_cost = Counter() - self.delay_order_penalty = Counter() - self.check_in_order = Counter() - - self.base_delay_order_penalty = 0 - - self._is_order_changed = False - - def get_pending_order(self) -> Dict[int, int]: - """Get orders that states is pending. - - Returns: - dict: Dictionary of order that key is product id, value is quantity. - """ - counter = defaultdict(int) - - for order in self.order_queue: - counter[order.product_id] += order.quantity - - for vehicle in self.vehicles: - if vehicle.is_enroute(): - counter[vehicle.product_id] += (vehicle.requested_quantity - vehicle.payload) - - return counter - - def place_order(self, order: Order) -> int: - """Place an order in the pending queue. - - Args: - order (Order): Order to insert. - - Returns: - int: Total price of this order. - """ - if order.quantity > 0: - sku = self.facility.skus[order.product_id] - - if sku is not None: - self._is_order_changed = True - - self.order_queue.append(order) - - order_total_price = sku.price * order.quantity - - self.check_in_order[order.product_id] += order.quantity - - return order_total_price - - return 0 - - def initialize(self): - super(DistributionUnit, self).initialize() - - self.base_delay_order_penalty = self.facility.get_config("delay_order_penalty", 0) - - def step(self, tick: int): - for vehicle in self.vehicles: - # If we have vehicle not on the way and there is any pending order. - if len(self.order_queue) > 0 and vehicle.requested_quantity == 0: - order = self.order_queue.popleft() - - # Schedule a job for available vehicle. - # TODO: why vlt is determined by order? - vehicle.schedule( - order.destination, - order.product_id, - order.quantity, - order.vlt - ) - - self._is_order_changed = True - - # Push vehicle. - vehicle.step(tick) - - self.transportation_cost[vehicle.product_id] += abs(vehicle.cost) - - # Update order's delay penalty per tick. - for order in self.order_queue: - self.delay_order_penalty[order.product_id] += self.base_delay_order_penalty - - def flush_states(self): - super(DistributionUnit, self).flush_states() - - for vehicle in self.vehicles: - vehicle.flush_states() - - if self._is_order_changed: - self._is_order_changed = False - - self.data_model.remaining_order_quantity = sum(order.quantity for order in self.order_queue) - self.data_model.remaining_order_number = len(self.order_queue) - - def reset(self): - super(DistributionUnit, self).reset() - - self.order_queue.clear() - self.transportation_cost.clear() - self.check_in_order.clear() - self.delay_order_penalty.clear() - - # Reset vehicles. - for vehicle in self.vehicles: - vehicle.reset() diff --git a/maro/simulator/scenarios/supply_chain/units/extendunitbase.py b/maro/simulator/scenarios/supply_chain/units/extendunitbase.py deleted file mode 100644 index a8106012d..000000000 --- a/maro/simulator/scenarios/supply_chain/units/extendunitbase.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .unitbase import UnitBase - - -class ExtendUnitBase(UnitBase): - """A base of sku related unit.""" - - # Product id (sku id), 0 means invalid. - product_id: int = 0 - - def initialize(self): - super(ExtendUnitBase, self).initialize() - - if self.data_model is not None: - self.data_model.set_product_id(self.product_id, self.parent.id) - - def get_unit_info(self) -> dict: - info = super(ExtendUnitBase, self).get_unit_info() - - info["sku_id"] = self.product_id - - return info diff --git a/maro/simulator/scenarios/supply_chain/units/manufacture.py b/maro/simulator/scenarios/supply_chain/units/manufacture.py deleted file mode 100644 index fae2d414d..000000000 --- a/maro/simulator/scenarios/supply_chain/units/manufacture.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .. import ManufactureAction, ManufactureDataModel -from .extendunitbase import ExtendUnitBase - - -class ManufactureUnit(ExtendUnitBase): - """Unit that used to produce certain product(sku) with consume specified source skus. - - One manufacture unit per sku. - """ - - # Source material sku and related number per produce cycle. - bom: dict = None - - # How many production unit each produce cycle. - output_units_per_lot: int = None - - # How many unit we will consume each produce cycle. - input_units_per_lot: int = 0 - - # How many we procedure per current step. - manufacture_number: int = 0 - - def initialize(self): - super(ManufactureUnit, self).initialize() - - facility_sku_info = self.facility.skus[self.product_id] - product_unit_cost = facility_sku_info.product_unit_cost - - assert isinstance(self.data_model, ManufactureDataModel) - self.data_model.initialize(product_unit_cost) - - global_sku_info = self.world.get_sku_by_id(self.product_id) - - self.bom = global_sku_info.bom - self.output_units_per_lot = global_sku_info.output_units_per_lot - - if len(self.bom) > 0: - self.input_units_per_lot = sum(self.bom.values()) - - def step(self, tick: int): - assert isinstance(self.action, ManufactureAction) - - # Try to produce production if we have positive rate. - if self.action is not None and self.action.production_rate > 0: - sku_num = len(self.facility.skus) - unit_num_upper_bound = self.facility.storage.capacity // sku_num - - # Compare with avg storage number. - current_product_number = self.facility.storage.get_product_number(self.product_id) - max_number_to_procedure = min( - unit_num_upper_bound - current_product_number, - self.action.production_rate * self.output_units_per_lot, - self.facility.storage.remaining_space - ) - - if max_number_to_procedure > 0: - space_taken_per_cycle = self.output_units_per_lot - self.input_units_per_lot - - # Consider about the volume, we can produce all if space take per cycle <=1. - if space_taken_per_cycle > 1: - max_number_to_procedure = max_number_to_procedure // space_taken_per_cycle - - source_sku_to_take = {} - # Do we have enough source material? - for source_sku_id, source_sku_cost_number in self.bom.items(): - source_sku_available_number = self.facility.storage.get_product_number(source_sku_id) - - max_number_to_procedure = min( - source_sku_available_number // source_sku_cost_number, - max_number_to_procedure - ) - - if max_number_to_procedure <= 0: - break - - source_sku_to_take[source_sku_id] = max_number_to_procedure * source_sku_cost_number - - if max_number_to_procedure > 0: - self.manufacture_number = max_number_to_procedure - self.facility.storage.try_take_products(source_sku_to_take) - self.facility.storage.try_add_products({self.product_id: self.manufacture_number}) - else: - self.manufacture_number = 0 - - def flush_states(self): - if self.manufacture_number > 0: - self.data_model.manufacturing_number = self.manufacture_number - - def post_step(self, tick: int): - if self.manufacture_number > 0: - self.data_model.manufacturing_number = 0 - self.manufacture_number = 0 - - # NOTE: call super at last, since it will clear the action. - super(ManufactureUnit, self).post_step(tick) diff --git a/maro/simulator/scenarios/supply_chain/units/order.py b/maro/simulator/scenarios/supply_chain/units/order.py deleted file mode 100644 index 3624402bc..000000000 --- a/maro/simulator/scenarios/supply_chain/units/order.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -from __future__ import annotations - -import typing -from typing import NamedTuple - -if typing.TYPE_CHECKING: - from maro.simulator.scenarios.supply_chain import FacilityBase - - -class Order(NamedTuple): - destination: FacilityBase - product_id: int - quantity: int - vlt: int diff --git a/maro/simulator/scenarios/supply_chain/units/outerseller.py b/maro/simulator/scenarios/supply_chain/units/outerseller.py deleted file mode 100644 index 8ebce4174..000000000 --- a/maro/simulator/scenarios/supply_chain/units/outerseller.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -import warnings -from abc import ABC, abstractmethod -from collections import namedtuple -from csv import DictReader -from datetime import datetime -from typing import Optional, Union - -from dateutil import parser - -from .seller import SellerUnit - - -class SellerDemandSampler(ABC): - """Base class of seller unit demand sampler, you can inherit from this to read from file or predict from a model. - - Args: - configs (dict): Configuration from retailer facility, contains keys for a special sampler. - world (World): Current world this retail belongs to. - """ - - def __init__(self, configs: dict, world): - self._configs = configs - self._world = world - - @abstractmethod - def sample_demand(self, product_id: int, tick: int) -> int: - """Sample the demand for specified product and tick. - - Args: - product_id (int): Id of product to sample. - tick (int): Tick of environment, NOTE: this tick is start from 0, - you may need to transform it to your time system. - """ - pass - - -class DataFileDemandSampler(SellerDemandSampler): - """Sampler to read sample demand from data files, one store one file. - - NOTE: - This sampler need to configure the start time that to be treated as tick 0 in world.settings, or - it will use first row as start time. - - Args: - configs (dict): Configuration from retail facility, it should contains following keys. - . "file_path", the path to the data file - . "sku_column", column name contains sku name, this must be match with current seller, or will be ignored. - . "price_column", column name that will be treated as price. - . "sale_column", column name that will be treated as sale number (demand). - . "datetime_column", column name that contains datetime, NOTE: we will parse it that ignore the time zone. - """ - - SkuRow = namedtuple("SkuRow", ("price", "sales")) - - def __init__(self, configs: dict, world): - super(DataFileDemandSampler, self).__init__(configs, world) - - self._file_path = configs["file_path"] - - # If start date time is None, then will use first row as start date time (tick 0). - self._start_date_time: Optional[Union[str, datetime]] = self._world.configs.settings["start_date_time"] - - if self._start_date_time is not None: - self._start_date_time = parser.parse(self._start_date_time, ignoretz=True) - - self._sku_column_name = configs.get("sku_column", "SKU") - self._price_column_name = configs.get("price_column", "Price") - self._sale_column_name = configs.get("sale_column", "Sales") - self._datetime_column_name = configs.get("datetime_column", "DT") - - # Tick -> sku -> (sale, price). - self._cache = {} - - self._cache_data() - - def sample_demand(self, product_id: int, tick: int) -> int: - if tick not in self._cache or product_id not in self._cache[tick]: - return 0 - - return self._cache[tick][product_id].sales - - def _cache_data(self): - with open(self._file_path, "rt") as fp: - reader = DictReader(fp) - - for row in reader: - sku_name = row[self._sku_column_name] - - sales = int(row[self._sale_column_name]) - price = float(row[self._price_column_name]) - date = parser.parse(row[self._datetime_column_name], ignoretz=True) - - if self._start_date_time is None: - self._start_date_time = date - - # So one day one tick. - target_tick = (date - self._start_date_time).days - - if target_tick not in self._cache: - self._cache[target_tick] = {} - - sku = self._world.get_sku_by_name(sku_name) - - if sku is not None: - self._cache[target_tick][sku.id] = DataFileDemandSampler.SkuRow(price, sales) - else: - warnings.warn(f"{sku_name} not configured in config file.") - - -class OuterSellerUnit(SellerUnit): - """Seller that demand is from out side sampler, like a data file or data model prediction.""" - - # Sample used to sample demand. - sampler: SellerDemandSampler = None - - def market_demand(self, tick: int) -> int: - return self.sampler.sample_demand(self.product_id, tick) diff --git a/maro/simulator/scenarios/supply_chain/units/product.py b/maro/simulator/scenarios/supply_chain/units/product.py deleted file mode 100644 index 9ed0c342c..000000000 --- a/maro/simulator/scenarios/supply_chain/units/product.py +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import numpy as np - -from ..datamodels import ProductDataModel -from .consumer import ConsumerUnit -from .distribution import DistributionUnit -from .extendunitbase import ExtendUnitBase -from .manufacture import ManufactureUnit -from .seller import SellerUnit -from .storage import StorageUnit - - -class ProductUnit(ExtendUnitBase): - """Unit that used to group units of one special sku, usually contains consumer, seller and manufacture.""" - - # Consumer unit of current sku. - consumer: ConsumerUnit = None - - # Seller unit of current sku. - seller: SellerUnit = None - - # Manufacture unit of this sku. - manufacture: ManufactureUnit = None - - # Storage of this facility, always a reference of facility.storage. - storage: StorageUnit = None - - # Reference to facility's distribution unit. - distribution: DistributionUnit = None - - # Internal states to track distribution. - _checkin_order = 0 - _transport_cost = 0 - _delay_order_penalty = 0 - - def initialize(self): - super().initialize() - - facility_sku = self.facility.skus[self.product_id] - - assert isinstance(self.data_model, ProductDataModel) - self.data_model.initialize(facility_sku.price) - - def step(self, tick: int): - for unit in self.children: - unit.step(tick) - - def flush_states(self): - for unit in self.children: - unit.flush_states() - - if self.distribution is not None: - self._checkin_order = self.distribution.check_in_order[self.product_id] - self._transport_cost = self.distribution.transportation_cost[self.product_id] - self._delay_order_penalty = self.distribution.delay_order_penalty[self.product_id] - - self.distribution.check_in_order[self.product_id] = 0 - self.distribution.transportation_cost[self.product_id] = 0 - self.distribution.delay_order_penalty[self.product_id] = 0 - - if self._checkin_order > 0: - self.data_model.distribution_check_order = self._checkin_order - - if self._transport_cost > 0: - self.data_model.distribution_transport_cost = self._transport_cost - - if self._delay_order_penalty > 0: - self.data_model.distribution_delay_order_penalty = self._delay_order_penalty - - def post_step(self, tick: int): - super().post_step(tick) - - for unit in self.children: - unit.post_step(tick) - - if self._checkin_order > 0: - self.data_model.distribution_check_order = 0 - self._checkin_order = 0 - - if self._transport_cost > 0: - self.data_model.distribution_transport_cost = 0 - self._transport_cost = 0 - - if self._delay_order_penalty > 0: - self.data_model.distribution_delay_order_penalty = 0 - self._delay_order_penalty = 0 - - def reset(self): - super().reset() - - for unit in self.children: - unit.reset() - - def get_unit_info(self) -> dict: - return { - "id": self.id, - "sku_id": self.product_id, - "max_vlt": self._get_max_vlt(), - "node_name": type(self.data_model).__node_name__ if self.data_model is not None else None, - "node_index": self.data_model_index if self.data_model is not None else None, - "class": type(self), - "config": self.config, - "consumer": self.consumer.get_unit_info() if self.consumer is not None else None, - "seller": self.seller.get_unit_info() if self.seller is not None else None, - "manufacture": self.manufacture.get_unit_info() if self.manufacture is not None else None - } - - # TODO: add following field into states. - def get_latest_sale(self): - sale = 0 - downstreams = self.facility.downstreams.get(self.product_id, []) - - for facility in downstreams: - sale += facility.products[self.product_id].get_latest_sale() - - return sale - - def get_sale_mean(self): - sale_mean = 0 - downstreams = self.facility.downstreams.get(self.product_id, []) - - for facility in downstreams: - sale_mean += facility.products[self.product_id].get_sale_mean() - - return sale_mean - - def get_sale_std(self): - sale_std = 0 - - downstreams = self.facility.downstreams.get(self.product_id, []) - - for facility in downstreams: - sale_std += facility.products[self.product_id].get_sale_std() - - return sale_std / np.sqrt(max(1, len(downstreams))) - - def get_selling_price(self): - price = 0.0 - downstreams = self.facility.downstreams.get(self.product_id, []) - - for facility in downstreams: - price = max(price, facility.products[self.product_id].get_selling_price()) - - return price - - def _get_max_vlt(self): - vlt = 1 - - if self.consumer is not None: - for source_facility_id in self.consumer.sources: - source_facility = self.world.get_facility_by_id(source_facility_id) - - source_vlt = source_facility.skus[self.product_id].vlt - - vlt = max(vlt, source_vlt) - - return vlt - - @staticmethod - def generate(facility, config: dict, unit_def: object): - """Generate product unit by sku information. - - Args: - facility (FacilityBase): Facility this product belongs to. - config (dict): Config of children unit. - unit_def (object): Definition of the unit (from config). - - Returns: - dict: Dictionary of product unit, key is the product id, value if ProductUnit. - """ - products_dict = {} - - if facility.skus is not None and len(facility.skus) > 0: - world = facility.world - - for sku_id, sku in facility.skus.items(): - sku_type = sku.type - - product_unit: ProductUnit = world.build_unit_by_type(unit_def, facility, facility) - product_unit.product_id = sku_id - product_unit.children = [] - product_unit.parse_configs(config) - product_unit.storage = product_unit.facility.storage - product_unit.distribution = product_unit.facility.distribution - - # NOTE: BE CAREFUL about the order, product unit will use this order update children, - # the order may affect the states. - # Here we make sure consumer is the first one, so it can place order first. - for child_name in ("consumer", "seller", "manufacture"): - conf = config.get(child_name, None) - - if conf is not None: - # Ignore manufacture unit if it is not for a production, even it is configured in config. - if sku_type != "production" and child_name == "manufacture": - continue - - # We produce the product, so we do not need to purchase it. - if sku_type == "production" and child_name == "consumer": - continue - - child_unit = world.build_unit(facility, product_unit, conf) - child_unit.product_id = sku_id - - setattr(product_unit, child_name, child_unit) - - # Parse config for unit. - child_unit.parse_configs(conf.get("config", {})) - - product_unit.children.append(child_unit) - - products_dict[sku_id] = product_unit - - return products_dict diff --git a/maro/simulator/scenarios/supply_chain/units/seller.py b/maro/simulator/scenarios/supply_chain/units/seller.py deleted file mode 100644 index e0803cec9..000000000 --- a/maro/simulator/scenarios/supply_chain/units/seller.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -import numpy as np - -from .. import SellerDataModel -from .extendunitbase import ExtendUnitBase - - -class SellerUnit(ExtendUnitBase): - """ - Unit that used to generate product consume demand, and move demand product from current storage. - """ - - def __init__(self): - super(SellerUnit, self).__init__() - - self.gamma = 0 - - # Attribute cache. - self.sold = 0 - self.demand = 0 - self.total_sold = 0 - self.total_demand = 0 - self.price = 0 - - self.sale_hist = [] - - def market_demand(self, tick: int) -> int: - """Generate market demand for current tick. - - Args: - tick (int): Current simulator tick. - - Returns: - int: Demand number. - """ - return int(np.random.gamma(self.gamma)) - - def initialize(self): - super(SellerUnit, self).initialize() - - sku = self.facility.skus[self.product_id] - - self.gamma = sku.sale_gamma - - assert isinstance(self.data_model, SellerDataModel) - self.data_model.initialize(sku.price, sku.backlog_ratio) - - self.sale_hist = [self.gamma] * self.config["sale_hist_len"] - - def step(self, tick: int): - demand = self.market_demand(tick) - - # What seller does is just count down the product number. - sold_qty = self.facility.storage.take_available(self.product_id, demand) - - self.total_sold += sold_qty - self.sold = sold_qty - self.demand = demand - self.total_demand += demand - - self.sale_hist.append(demand) - self.sale_hist = self.sale_hist[1:] - - def flush_states(self): - if self.sold > 0: - self.data_model.sold = self.sold - self.data_model.total_sold = self.total_sold - - if self.demand > 0: - self.data_model.demand = self.demand - self.data_model.total_demand = self.total_demand - - def post_step(self, tick: int): - super(SellerUnit, self).post_step(tick) - - if self.sold > 0: - self.data_model.sold = 0 - self.sold = 0 - - if self.demand > 0: - self.data_model.demand = 0 - self.demand = 0 - - def reset(self): - super(SellerUnit, self).reset() - - def sale_mean(self): - return np.mean(self.sale_hist) - - def sale_std(self): - return np.std(self.sale_hist) diff --git a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py b/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py deleted file mode 100644 index adfa6d574..000000000 --- a/maro/simulator/scenarios/supply_chain/units/simplemanufacture.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from .. import ManufactureAction -from .manufacture import ManufactureUnit - - -class SimpleManufactureUnit(ManufactureUnit): - """This simple manufacture unit will ignore source sku, just generate specified number of product.""" - - def step(self, tick: int): - # Try to produce production if we have positive rate. - self.manufacture_number = 0 - - assert isinstance(self.action, ManufactureAction) - - if self.action is not None and self.action.production_rate > 0: - production_rate = self.action.production_rate - - sku_num = len(self.facility.skus) - unit_num_upper_bound = self.facility.storage.capacity // sku_num - current_product_number = self.facility.storage.get_product_number(self.product_id) - self.manufacture_number = max(0, min(unit_num_upper_bound - current_product_number, production_rate)) - - if self.manufacture_number > 0: - self.facility.storage.try_add_products({self.product_id: self.manufacture_number}) diff --git a/maro/simulator/scenarios/supply_chain/units/storage.py b/maro/simulator/scenarios/supply_chain/units/storage.py deleted file mode 100644 index e78cb0ff3..000000000 --- a/maro/simulator/scenarios/supply_chain/units/storage.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import Dict - -from .unitbase import UnitBase - - -class StorageUnit(UnitBase): - """Unit that used to store skus.""" - - def __init__(self): - super().__init__() - - # Used to map from product id to slot index. - self.capacity = 0 - self.remaining_space = 0 - self.product_level = {} - - # Which product's number has changed. - self._changed_product_cache = {} - - def try_add_products(self, product_quantities: Dict[int, int], all_or_nothing=True) -> dict: - """Try to add products into storage. - - Args: - product_quantities (Dict[int, int]): Dictionary of product id and quantity need to add to storage. - all_or_nothing (bool): Failed if all product cannot be added, or add as many as it can. Default is True. - - Returns: - dict: Dictionary of product id and quantity success added. - """ - if all_or_nothing and self.remaining_space < sum(product_quantities.values()): - return {} - - unloaded_quantities = {} - - for product_id, quantity in product_quantities.items(): - unload_quantity = min(self.remaining_space, quantity) - - self.product_level[product_id] += unload_quantity - unloaded_quantities[product_id] = unload_quantity - - self._changed_product_cache[product_id] = True - - self.remaining_space -= unload_quantity - - return unloaded_quantities - - def try_take_products(self, product_quantities: Dict[int, int]) -> bool: - """Try to take specified number of product. - - Args: - product_quantities (Dict[int, int]): Dictionary of product id and quantity to take from storage. - - Returns: - bool: Is success to take? - """ - # Check if we can take all kinds of products? - for product_id, quantity in product_quantities.items(): - if self.product_level[product_id] < quantity: - return False - - # Take from storage. - for product_id, quantity in product_quantities.items(): - self.product_level[product_id] -= quantity - self._changed_product_cache[product_id] = True - - self.remaining_space += quantity - - return True - - def take_available(self, product_id: int, quantity: int) -> int: - """Take as much as available specified product from storage. - - Args: - product_id (int): Product to take. - quantity (int): Max quantity to take. - - Returns: - int: Actual quantity taken. - """ - available = self.product_level[product_id] - actual = min(available, quantity) - - self.product_level[product_id] -= actual - self._changed_product_cache[product_id] = True - - self.remaining_space += actual - - return actual - - def get_product_number(self, product_id: int) -> int: - """Get product number in storage. - - Args: - product_id (int): Product to check. - - Returns: - int: Available number of product. - """ - return self.product_level[product_id] - - def initialize(self): - super(StorageUnit, self).initialize() - - self.capacity = self.config.get("capacity", 100) - self.remaining_space = self.capacity - - for sku in self.facility.skus.values(): - self.product_level[sku.id] = sku.init_stock - self._changed_product_cache[sku.id] = False - - self.remaining_space -= sku.init_stock - - self.data_model.initialize( - capacity=self.capacity, - remaining_space=self.remaining_space, - product_list=[sku_id for sku_id in self.product_level.keys()], - product_number=[n for n in self.product_level.values()] - ) - - def flush_states(self): - # Write the changes to frame. - i = 0 - has_changes = False - for product_id, product_number in self.product_level.items(): - if self._changed_product_cache[product_id]: - has_changes = True - self._changed_product_cache[product_id] = False - - self.data_model.product_number[i] = product_number - i += 1 - - if has_changes: - self.data_model.remaining_space = self.remaining_space - - def reset(self): - super(StorageUnit, self).reset() - - self.remaining_space = self.capacity - - for sku in self.facility.skus.values(): - # self.product_level[sku.id] = sku.init_stock - # self.remaining_space -= sku.init_stock - # if hasattr(sku, "sale_gamma") and (sku.sale_gamma is not None): - # init_stock = random.randint(0, 5) * sku.sale_gamma - # sku.init_stock = init_stock - self.product_level[sku.id] = sku.init_stock - self.remaining_space -= sku.init_stock - - def get_unit_info(self): - info = super().get_unit_info() - - info["product_list"] = [i for i in self.product_level.keys()] - - return info diff --git a/maro/simulator/scenarios/supply_chain/units/storeproduct.py b/maro/simulator/scenarios/supply_chain/units/storeproduct.py deleted file mode 100644 index 7ce9a5396..000000000 --- a/maro/simulator/scenarios/supply_chain/units/storeproduct.py +++ /dev/null @@ -1,13 +0,0 @@ - -from .product import ProductUnit - - -class StoreProductUnit(ProductUnit): - def get_sale_mean(self): - return self.seller.sale_mean() - - def get_sale_std(self): - return self.seller.sale_std() - - def get_selling_price(self): - return self.facility.skus[self.product_id].price diff --git a/maro/simulator/scenarios/supply_chain/units/unitbase.py b/maro/simulator/scenarios/supply_chain/units/unitbase.py deleted file mode 100644 index 7d9413c60..000000000 --- a/maro/simulator/scenarios/supply_chain/units/unitbase.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -from __future__ import annotations - -import typing -from typing import Optional, Union - -if typing.TYPE_CHECKING: - from maro.simulator.scenarios.supply_chain import FacilityBase, SupplyChainAction - from maro.simulator.scenarios.supply_chain.datamodels.base import DataModelBase - from maro.simulator.scenarios.supply_chain.world import World - - -class UnitBase: - """Base of all unit used to contain related logic. - - Typically one unit instance should bind to a data model instance, - that used to update states in frame. - - An unit will have following steps to initializing. - . Create instance with default constructor without parameter, means all unit constructor should not have parameter - or with default value. - . Unit.parse_configs is called with configurations from parent or config file. - . After frame is constructed, Unit.initialize is called to do data model related initializing. - . At the beginning of business_engine.step, Unit.step is called to go through related logic, - then after all the agent completed, Unit.flush_state will be called at the end of business_engine.step, - to tell agents to save their states. - . Unit.post_step is called in business_engine.post_step after step is finished. - . Unit.set_action is called when there is any action from out-side. - - """ - # Id of this unit. - id: int = 0 - - # Which this unit belongs to. - facility: Optional[FacilityBase] = None - - # Which world this unit belongs to. - world: Optional[World] = None - - # Parent of this unit, it can be a facility or another unit. - parent: Optional[Union[FacilityBase, UnitBase]] = None - - # Child units, extended unit can add their own child as property, this is used as a collection. - children: Optional[list] = None - - # Data model name in the frame, used to query binding data model instance. - data_model_name: Optional[str] = None - - # Data model instance index in the frame, used to query binding data model instance. - data_model_index: Optional[int] = None - - # Real data model binding with this unit. - data_model: Optional[DataModelBase] = None - - # Current action. - action: Optional[SupplyChainAction] = None - - # Current unit configurations. - config: Optional[dict] = None - - def __init__(self): - pass - - def parse_configs(self, config: dict): - """Parse configurations from config. - - Args: - config (dict): Configuration from parent or config file. - """ - self.config = config - - def step(self, tick: int): - """Run related logic for current tick. - - Args: - tick (int): Current simulator tick. - """ - pass - - def flush_states(self): - """Flush states into frame for current tick. - """ - pass - - def post_step(self, tick: int): - """Post-processing for current step. - - Args: - tick (int): Current simulator tick. - """ - self.action = None - - def reset(self): - """Reset this unit for a new episode.""" - if self.data_model is not None: - self.data_model.reset() - - self.action = None - - def initialize(self): - """Initialize this unit after data model is ready to use. - - NOTE: unit.data_model is available from this step. - """ - if self.data_model is not None: - self.data_model.set_id(self.id, self.facility.id) - - def set_action(self, action: SupplyChainAction): - """Set action for this agent. - - Args: - action (object): Action from outside. - """ - self.action = action - - def get_unit_info(self) -> dict: - return { - "id": self.id, - "node_name": type(self.data_model).__node_name__, - "node_index": self.data_model_index, - "class": type(self), - "config": self.config, - "children": None if self.children is None else [c.get_unit_info() for c in self.children] - } diff --git a/maro/simulator/scenarios/supply_chain/units/vehicle.py b/maro/simulator/scenarios/supply_chain/units/vehicle.py deleted file mode 100644 index e74b59d44..000000000 --- a/maro/simulator/scenarios/supply_chain/units/vehicle.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -from __future__ import annotations - -import math -import typing -from typing import Optional - -from .unitbase import UnitBase - -if typing.TYPE_CHECKING: - from .. import FacilityBase - - -class VehicleUnit(UnitBase): - """Unit used to move production from source to destination by order.""" - - def __init__(self): - super().__init__() - # Max patient of current vehicle. - self.max_patient: Optional[int] = None - - # Current products' destination. - self.destination: Optional[FacilityBase] = None - - # Path to destination. - self.path: Optional[list] = None - - # Product to load - self.product_id = 0 - - # Steps to arrive destination. - self.steps = 0 - - # Payload on vehicle. - self.payload = 0 - - # Which product unit current product related to. - self.product = None - - # Current location in the path. - self.location = 0 - - # Velocity. - self.velocity = 0 - self.requested_quantity = 0 - self.patient = 0 - self.cost = 0 - self.unit_transport_cost = 0 - - def schedule(self, destination: FacilityBase, product_id: int, quantity: int, vlt: int): - """Schedule a job for this vehicle. - - Args: - destination (FacilityBase): Destination facility. - product_id (int): What load from storage. - quantity (int): How many to load. - vlt (int): Velocity of vehicle. - """ - self.product_id = product_id - self.destination = destination - self.requested_quantity = quantity - - # Find the path from current entity to target. - self.path = self.world.find_path( - self.facility.x, - self.facility.y, - destination.x, - destination.y - ) - - if self.path is None: - raise Exception(f"Destination {destination} is unreachable") - - # Steps to destination. - self.steps = int(math.ceil(float(len(self.path) - 1) / float(vlt))) - dest_consumer = destination.products[product_id].consumer - if self.steps < len(dest_consumer.pending_order_daily): - dest_consumer.pending_order_daily[self.steps] += quantity - - # We are waiting for product loading. - self.location = 0 - - self.velocity = vlt - - self.patient = self.max_patient - - def try_load(self, quantity: int) -> bool: - """Try to load specified number of scheduled product. - - Args: - quantity (int): Number to load. - """ - if self.facility.storage.try_take_products({self.product_id: quantity}): - self.payload = quantity - - # Write to frame, as we do not need to update it per tick. - self.data_model.payload = quantity - - return True - - return False - - def try_unload(self): - """Try unload products into destination's storage.""" - unloaded = self.destination.storage.try_add_products( - {self.product_id: self.payload}, - all_or_nothing=False - ) - - # Update order if we unloaded any. - if len(unloaded) > 0: - unloaded_units = sum(unloaded.values()) - - self.destination.products[self.product_id].consumer.on_order_reception( - self.facility.id, - self.product_id, - unloaded_units, - self.payload - ) - - self.payload -= unloaded_units - self.data_model.payload = self.payload - - def is_enroute(self): - return self.destination is not None - - def initialize(self): - super(VehicleUnit, self).initialize() - - patient = self.config.get("patient", 100) - self.unit_transport_cost = self.config.get("unit_transport_cost", 1) - - self.data_model.initialize(unit_transport_cost=self.unit_transport_cost) - - self.max_patient = patient - - def step(self, tick: int): - # If we have not arrive at destination yet. - if self.steps > 0: - # if we still not loaded enough productions yet. - if self.location == 0 and self.payload == 0: - # then try to load by requested. - - if self.try_load(self.requested_quantity): - # NOTE: here we return to simulate loading - return - else: - self.patient -= 1 - - # Failed to load, check the patient. - if self.patient < 0: - self.destination.products[self.product_id].consumer.update_open_orders( - self.facility.id, - self.product_id, - -self.requested_quantity - ) - - self._reset_internal_states() - self._reset_data_model() - - # Moving to destination - if self.payload > 0: - # Closer to destination until 0. - - self.location += self.velocity - self.steps -= 1 - - if self.location >= len(self.path): - self.location = len(self.path) - 1 - else: - # Avoid update under idle state. - # if self.location > 0: - # Try to unload./////////////////////////////////////////////////////////////////// - if self.payload > 0: - self.try_unload() - - # Back to source if we unload all. - if self.payload == 0: - self._reset_internal_states() - self._reset_data_model() - - self.cost = self.payload * self.unit_transport_cost - - def reset(self): - super(VehicleUnit, self).reset() - - self._reset_internal_states() - self._reset_data_model() - - def _reset_internal_states(self): - self.destination = None - self.path = None - self.payload = 0 - self.product_id = 0 - self.steps = 0 - self.location = 0 - self.requested_quantity = 0 - self.velocity = 0 - self.patient = self.max_patient - - def _reset_data_model(self): - # Reset data model. - self.data_model.payload = 0 diff --git a/maro/simulator/scenarios/supply_chain/world.py b/maro/simulator/scenarios/supply_chain/world.py deleted file mode 100644 index 15cc9586d..000000000 --- a/maro/simulator/scenarios/supply_chain/world.py +++ /dev/null @@ -1,487 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from typing import List, NamedTuple, Optional, Tuple, Union - -import networkx as nx - -from maro.backends.frame import FrameBase - -from .easy_config import EasyConfig, SkuInfo -from .facilities import FacilityBase -from .frame_builder import build_frame -from .parser import DataModelDef, FacilityDef, SupplyChainConfiguration, UnitDef -from .units import ExtendUnitBase, UnitBase - - -class AgentInfo(NamedTuple): - id: int - agent_type: object - is_facility: bool - sku: Optional[SkuInfo] - facility_id: int - parent_id: Optional[int] - - -class World: - """Supply chain world contains facilities and grid base map.""" - - def __init__(self): - # Frame for current world configuration. - self.frame: Optional[FrameBase] = None - - # Current configuration. - self.configs: Optional[SupplyChainConfiguration] = None - - # Durations of current simulation. - self.durations = 0 - - # All the entities in the world. - self.units = {} - - # All the facilities in this world. - self.facilities = {} - - # Entity id counter, every unit and facility have unique id. - self._id_counter = 1 - - # Grid of the world - self._graph: Optional[nx.Graph] = None - - # Sku id to name mapping, used for querying. - self._sku_id2name_mapping = {} - - # All the sku in this world. - self._sku_collection = {} - - # Facility name to id mapping, used for querying. - self._facility_name2id_mapping = {} - - # Data model class collection, used to collection data model class and their number in frame. - self._data_class_collection = {} - - self.agent_list = [] - - self.agent_type_dict = {} - - self.max_sources_per_facility = 0 - self.max_price = 0 - - def get_sku_by_name(self, name: str) -> EasyConfig: - """Get sku information by name. - - Args: - name (str): Sku name to query. - - Returns: - EasyConfig: General information for sku, used as a dict, but support use key as property. - """ - return self._sku_collection.get(name, None) - - def get_sku_by_id(self, sku_id: int) -> EasyConfig: - """Get sku information by sku id. - - Args: - sku_id (int): Id of sku to query. - - Returns: - SkuInfo: General information for sku. - """ - return self._sku_collection[self._sku_id2name_mapping[sku_id]] - - def get_facility_by_id(self, facility_id: int) -> FacilityBase: - """Get facility by id. - - Args: - facility_id (int): Facility id to query. - - Returns: - FacilityBase: Facility instance. - """ - return self.facilities[facility_id] - - def get_facility_by_name(self, name: str): - """Get facility by name. - - Args: - name (str): Facility name to query. - - Returns: - FacilityBase: Facility instance. - """ - return self.facilities[self._facility_name2id_mapping[name]] - - def get_entity(self, id: int) -> Union[FacilityBase, UnitBase]: - """Get an entity(Unit or Facility) by id. - - Args: - id (int): Id to query. - - Returns: - Union[FacilityBase, UnitBase]: Unit or facility instance. - """ - if id in self.units: - return self.units[id] - else: - return self.facilities[id] - - def find_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: - """Find path to specified cell. - - Args: - start_x (int): Start cell position x. - start_y (int): Start cell position y. - goal_x (int): Destination cell position x. - goal_y (int): Destination cell position y. - - Returns: - List[Tuple[int, int]]: List of (x, y) position to target. - """ - return nx.astar_path(self._graph, source=(start_x, start_y), target=(goal_x, goal_y), weight="cost") - - def build(self, configs: SupplyChainConfiguration, snapshot_number: int, durations: int): - """Build world with configurations. - - Args: - configs (SupplyChainConfiguration): Configuration of current world. - snapshot_number (int): Number of snapshots to keep in memory. - durations (int): Durations of current simulation. - """ - self.durations = durations - self.configs = configs - - world_config = configs.world - - # Grab sku information for this world. - for sku_conf in world_config["skus"]: - sku = SkuInfo(sku_conf) - - self._sku_id2name_mapping[sku.id] = sku.name - self._sku_collection[sku.name] = sku - - # Collect bom info. - for sku_conf in world_config["skus"]: - sku = self._sku_collection[sku_conf["name"]] - sku.bom = {} - - bom = sku_conf.get("bom", {}) - - for material_sku_name, units_per_lot in bom.items(): - sku.bom[self._sku_collection[material_sku_name].id] = units_per_lot - - # Construct facilities. - for facility_conf in world_config["facilities"]: - facility_class_alias = facility_conf["class"] - facility_def: FacilityDef = self.configs.facilities[facility_class_alias] - facility_class_type = facility_def.class_type - - # Instance of facility. - facility = facility_class_type() - - # Normal properties. - facility.id = self._gen_id() - facility.name = facility_conf["name"] - facility.world = self - - # Parse sku info. - facility.parse_skus(facility_conf["skus"]) - - # Parse config for facility. - facility.parse_configs(facility_conf.get("config", {})) - - # Due with data model. - data_model_def: DataModelDef = self.configs.data_models[facility_def.data_model_alias] - - # Register the data model, so that it will help to generate related instance index. - facility.data_model_index = self._register_data_model(data_model_def.alias) - facility.data_model_name = data_model_def.name_in_frame - - # Build children (units). - for child_name, child_conf in facility_conf["children"].items(): - setattr(facility, child_name, self.build_unit(facility, facility, child_conf)) - - self.facilities[facility.id] = facility - - self._facility_name2id_mapping[facility.name] = facility.id - - # Build frame. - self.frame = self._build_frame(snapshot_number) - - # Assign data model instance. - for unit in self.units.values(): - if unit.data_model_name is not None: - unit.data_model = getattr(self.frame, unit.data_model_name)[unit.data_model_index] - - for facility in self.facilities.values(): - if facility.data_model_name is not None: - facility.data_model = getattr(self.frame, facility.data_model_name)[facility.data_model_index] - - # Construct the upstream topology. - topology = world_config["topology"] - - for cur_facility_name, topology_conf in topology.items(): - facility = self.get_facility_by_name(cur_facility_name) - - for sku_name, source_facilities in topology_conf.items(): - sku = self.get_sku_by_name(sku_name) - facility.upstreams[sku.id] = [] - - self.max_sources_per_facility = max(self.max_sources_per_facility, len(source_facilities)) - - for source_name in source_facilities: - source_facility = self.get_facility_by_name(source_name) - - facility.upstreams[sku.id].append(source_facility) - - if sku.id not in source_facility.downstreams: - source_facility.downstreams[sku.id] = [] - - source_facility.downstreams[sku.id].append(facility) - - # Call initialize method for facilities. - for facility in self.facilities.values(): - facility.initialize() - - # Call initialize method for units. - for unit in self.units.values(): - unit.initialize() - - # Construct the map grid. - grid_config = world_config["grid"] - - grid_width, grid_height = grid_config["size"] - - # Build our graph base one settings. - # This will create a full connect graph. - self._graph = nx.grid_2d_graph(grid_width, grid_height) - - # All edge weight will be 1 by default. - edge_weights = {e: 1 for e in self._graph.edges()} - - # Facility to cell will have 1 weight, cell to facility will have 4 cost. - for facility_name, pos in grid_config["facilities"].items(): - facility_id = self._facility_name2id_mapping[facility_name] - facility = self.facilities[facility_id] - facility.x = pos[0] - facility.y = pos[1] - pos = tuple(pos) - - # Neighbors to facility will have high cost. - for npos in ((pos[0] - 1, pos[1]), (pos[0] + 1, pos[1]), (pos[0], pos[1] - 1), (pos[0], pos[1] + 1)): - if 0 <= npos[0] < grid_width and 0 <= npos[1] < grid_height: - edge_weights[(npos, pos)] = 4 - - nx.set_edge_attributes(self._graph, edge_weights, "cost") - - # Collection agent list - for facility in self.facilities.values(): - agent_type = facility.configs.get("agent_type", None) - - if agent_type is not None: - self.agent_list.append(AgentInfo(facility.id, agent_type, True, None, facility.id, None)) - - self.agent_type_dict[type(facility).__name__] = agent_type - - for sku in facility.skus.values(): - self.max_price = max(self.max_price, sku.price) - - # Find units that contains agent type to expose as agent. - for unit in self.units.values(): - agent_type = unit.config.get("agent_type", None) - - if agent_type is not None: - # Unit or facility id, agent type, is facility, sku info, facility id, parent id. - self.agent_list.append( - AgentInfo( - unit.id, - agent_type, - False, - unit.facility.skus[unit.product_id], - unit.facility.id, - unit.parent.id - ) - ) - - self.agent_type_dict[type(unit).__name__] = agent_type - - def build_unit_by_type(self, unit_def: UnitDef, parent: Union[FacilityBase, UnitBase], facility: FacilityBase): - """Build an unit by its type. - - Args: - unit_def (UnitDef): Definition of this unit. - parent (Union[FacilityBase, UnitBase]): Parent of this unit. - facility (FacilityBase): Facility this unit belongs to. - - Returns: - UnitBase: Unit instance. - """ - unit = unit_def.class_type() - - unit.id = self._gen_id() - unit.parent = parent - unit.facility = facility - unit.world = self - - if unit_def.data_model_alias is not None: - # Due with data model. - data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] - - # Register the data model, so that it will help to generate related instance index. - unit.data_model_index = self._register_data_model(data_model_def.alias) - unit.data_model_name = data_model_def.name_in_frame - - self.units[unit.id] = unit - - return unit - - def build_unit(self, facility: FacilityBase, parent: Union[FacilityBase, UnitBase], config: dict) -> UnitBase: - """Build an unit by its configuration. - - Args: - facility (FacilityBase): Facility of this unit belongs to. - parent (Union[FacilityBase, UnitBase]): Parent of this unit belongs to, this may be same with facility, if - this unit is attached to a facility. - config (dict): Configuration of this unit. - - Returns: - UnitBase: Unit instance. - """ - unit_class_alias = config["class"] - unit_def: UnitDef = self.configs.units[unit_class_alias] - - is_template = config.get("is_template", False) - - # If it is not a template, then just use current configuration to generate unit. - if not is_template: - unit_instance = unit_def.class_type() - - # Assign normal properties. - unit_instance.id = self._gen_id() - unit_instance.world = self - unit_instance.facility = facility - unit_instance.parent = parent - - # Record the id. - self.units[unit_instance.id] = unit_instance - - # Due with data model. - data_model_def: DataModelDef = self.configs.data_models[unit_def.data_model_alias] - - # Register the data model, so that it will help to generate related instance index. - unit_instance.data_model_index = self._register_data_model(data_model_def.alias) - unit_instance.data_model_name = data_model_def.name_in_frame - - # Parse the config is there is any. - unit_instance.parse_configs(config.get("config", {})) - - # Prepare children. - children_conf = config.get("children", None) - - if children_conf: - unit_instance.children = [] - - for child_name, child_conf in children_conf.items(): - # If child configuration is a dict, then we add it as a property by name (key). - if type(child_conf) == dict: - child_instance = self.build_unit(facility, unit_instance, child_conf) - - setattr(unit_instance, child_name, child_instance) - unit_instance.children.append(child_instance) - elif type(child_conf) == list: - # If child configuration is a list, then will treat it as list property, named same as key. - child_list = [] - for conf in child_conf: - child_list.append(self.build_unit(facility, unit_instance, conf)) - - setattr(unit_instance, child_name, child_list) - unit_instance.children.extend(child_list) - - return unit_instance - else: - # If this is template unit, then will use the class' static method 'generate' to generate sub-units. - children = unit_def.class_type.generate(facility, config.get("config"), unit_def) - - return children - - def get_node_mapping(self): - """Collect all the entities information. - - Returns: - dict: A dictionary contains 'mapping' for id to data model index mapping, - 'detail' for detail of units and facilities. - """ - facility_info_dict = { - facility_id: facility.get_node_info() for facility_id, facility in self.facilities.items() - } - - id2index_mapping = {} - - for unit_id, unit in self.units.items(): - sku = None - - if isinstance(unit, ExtendUnitBase): - sku = unit.facility.skus[unit.product_id] - - if unit.data_model is not None: - id2index_mapping[unit_id] = (unit.data_model_name, unit.data_model_index, unit.facility.id, sku) - else: - id2index_mapping[unit_id] = (None, None, unit.facility.id, sku) - - return { - "agent_types": {k: v for k, v in self.agent_type_dict.items()}, - "unit_mapping": id2index_mapping, - "skus": {sku.id: sku for sku in self._sku_collection.values()}, - "facilities": facility_info_dict, - "max_price": self.max_price, - "max_sources_per_facility": self.max_sources_per_facility, - } - - def _register_data_model(self, alias: str) -> int: - """Register a data model alias, used to collect data model used in frame. - - Args: - alias (str): Class alias defined in core.yml. - - Returns: - int: Specified data model instance index after frame is built. - """ - if alias not in self._data_class_collection: - self._data_class_collection[alias] = 0 - - node_index = self._data_class_collection[alias] - - self._data_class_collection[alias] += 1 - - return node_index - - def _build_frame(self, snapshot_number: int) -> FrameBase: - """Build frame by current world definitions. - - Args: - snapshot_number (int): Number of snapshots to keep in memory. - - Returns: - FrameBase: Frame instance with data model in current configuration. - """ - data_class_in_frame = [] - - for alias, number in self._data_class_collection.items(): - data_model_def: DataModelDef = self.configs.data_models[alias] - data_class_in_frame.append(( - data_model_def.class_type, - data_model_def.name_in_frame, - number - )) - - frame = build_frame(True, snapshot_number, data_class_in_frame) - - return frame - - def _gen_id(self): - """Generate id for entities.""" - nid = self._id_counter - - self._id_counter += 1 - - return nid diff --git a/notebooks/supply_chain/sc_demo.ipynb b/notebooks/supply_chain/sc_demo.ipynb deleted file mode 100644 index 253b0a47a..000000000 --- a/notebooks/supply_chain/sc_demo.ipynb +++ /dev/null @@ -1,2216 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Step 1a: Customize your supply chain - defining facility template (optional)\n", - "- Facility templates are to define different types of facilities in a supply chain\n", - "- A facility template defines not only what kinds of agents it contains but also how they perform (class)\n", - "- In a facility template, you can also assign agents to different \"agent_type\" to group agents\n", - "- For example, the following codes define a store template of a retailer:\n", - " - Each store can be seen as a facility with a list of \"products\" and \"storage\" capacity\n", - " - Each product in the store has both \"consumer\" and \"seller\" behaviors to denote that it can place replenishing orders and be sold to customers." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"\n", - " RetailerFacility: &retailer_facility \n", - " class: \"RetailerFacility\" \n", - " children:\n", - " storage:\n", - " class: \"StorageUnit\"\n", - " products:\n", - " class: \"StoreProductUnit\"\n", - " is_template: true\n", - " config:\n", - " agent_type: \"productstore\"\n", - " consumer:\n", - " class: \"ConsumerUnit\"\n", - " config:\n", - " agent_type: \"consumerstore\"\n", - " seller:\n", - " class: \"SellerUnit\"\n", - " config:\n", - " sale_hist_len: 4\n", - " config:\n", - " agent_type: \"facility\"\n", - "\"\"\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Step 1b: Instantiate all facilities in the supply chain\n", - "- Refer a facility template defined before\n", - "- Instantiate parameters of each sub-agent\n", - "- Below is an example showing how a supplier and a retailer can be instantiated." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"\n", - " facilities:\n", - " - name: \"Supplier_001\" # name of the facility\n", - " definition_ref: \"SupplierFacility\"\n", - " # sku list of this facility\n", - " skus:\n", - " sku1: # sku name and attributes needed for this facility\n", - " init_stock: 1000\n", - " product_unit_cost: 90\n", - " production_rate: 1000\n", - " type: \"production\"\n", - " cost: 90\n", - " price: 100\n", - " vlt: 2\n", - " sku2:\n", - " init_stock: 1000\n", - " product_unit_cost: 150\n", - " production_rate: 1000\n", - " type: \"production\"\n", - " cost: 150\n", - " price: 200\n", - " vlt: 3\n", - " children:\n", - " storage: \n", - " distribution:\n", - " # config of this facility\n", - " config:\n", - " delay_order_penalty: 100\n", - " order_cost: 10\n", - " \n", - " - name: \"Retailer_001\"\n", - " definition_ref: \"RetailerFacility\"\n", - " skus:\n", - " sku1:\n", - " price: 200\n", - " cost: 100\n", - " init_stock: 40\n", - " sale_gamma: 20\n", - " backlog_ratio: 0.1\n", - " vlt: 1\n", - " sku2:\n", - " price: 350\n", - " cost: 200\n", - " init_stock: 160\n", - " sale_gamma: 80\n", - " backlog_ratio: 0.1\n", - " vlt: 1\n", - " children:\n", - " storage: *small_storage\n", - "\"\"\"\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# In case we want to define a retailer store with demands of SKUs are sampled from historical data, this can be simply done by changing the \"class\" of seller, see below for instance." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"\n", - " RetailerFacility: &raw_data_retailer_facility\n", - " class: \"OuterRetailerFacility\"\n", - " children:\n", - " storage:\n", - " class: \"StorageUnit\"\n", - " products:\n", - " class: \"StoreProductUnit\"\n", - " is_template: true\n", - " config:\n", - " agent_type: \"consumer\"\n", - " consumer:\n", - " class: \"ConsumerUnit\"\n", - " seller:\n", - " class: \"RawDataSellerUnit\" \n", - " config:\n", - " sale_hist_len: 4\n", - " config:\n", - " agent_type: \"facility\"\n", - " seller_sampler_type: data\n", - " sku_column: \"SKU\"\n", - " price_column: \"Price\"\n", - " sale_column: \"Sales\"\n", - " datetime_column: \"DT\"\n", - " file_path: \"/path/to/data.csv\"\n", - " \n", - "\"\"\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Step 2: Defining fulfillment relation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"\n", - " topology:\n", - " Warehouse_001:\n", - " sku1:\n", - " - \"Supplier_001\"\n", - " sku2:\n", - " - \"Supplier_001\"\n", - " sku3:\n", - " - \"Supplier_001\"\n", - " Retailer_001:\n", - " sku1:\n", - " - \"Warehouse_001\"\n", - " sku2:\n", - " - \"Warehouse_001\"\n", - " sku3:\n", - " - \"Warehouse_001\"\n", - "\"\"\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Step 3: Defining inventory policies" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "from os.path import dirname, realpath\n", - "import numpy as np\n", - "import torch\n", - "\n", - "from maro.rl import (\n", - " DQN, DQNConfig, EpisodeBasedSchedule, FullyConnectedBlock, NullPolicy, OptimOption, QNetForDiscreteActionSpace,\n", - " StepBasedSchedule, UniformSampler\n", - ")\n", - "\n", - "from or_policy.minmax_policy import ConsumerMinMaxPolicy\n", - "from or_policy.eoq_policy import ConsumerEOQPolicy as ProducerEOQPolicy\n", - "from or_policy.base_policy import ProducerBaselinePolicy\n", - "\n", - "# sc_code_dir = dirname(realpath(__file__))\n", - "sys.path.insert(0, './')\n", - "from config import config\n", - "config = config[\"policy\"]\n", - "\n", - "\n", - "class SimpleQNet(QNetForDiscreteActionSpace):\n", - " def forward(self, states):\n", - " states = torch.from_numpy(np.asarray(states)).to(self.device)\n", - " if len(states.shape) == 1:\n", - " states = states.unsqueeze(dim=0)\n", - " return self.component.forward(states)\n", - "\n", - "def get_dqn_consumer_policy(config):\n", - " q_net = SimpleQNet(\n", - " FullyConnectedBlock(**config[\"model\"][\"network\"]),\n", - " optim_option=OptimOption(**config[\"model\"][\"optimization\"]),\n", - " device=config[\"model\"][\"device\"]\n", - " )\n", - " experience_manager = UniformSampler(**config[\"experience_manager\"])\n", - " return DQN(q_net, experience_manager, DQNConfig(**config[\"algorithm_config\"]))\n", - "\n", - "def get_eoq_producer_policy(config):\n", - " return ProducerEOQPolicy(config)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Step 4: Mapping agents to policies" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "policy_dict = {\n", - " 'consumer': get_dqn_consumer_policy(config['consumerstore']),\n", - " 'producer': get_eoq_producer_policy(config['producer'])\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Step 5: Training and evaluation - a complete example" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", - "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", - "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", - "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", - "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", - "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", - "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", - "07:26:46 | LEARNER | INFO | Policy will be evaluated at the end of episodes [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 6, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 0, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=115, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=469, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 2, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 1, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=41, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=713, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=294, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 2, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=546, vlt=3, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=193, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=297, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 3, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 4, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=325, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 5, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=699, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 6, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 7, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 8, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=643, vlt=3, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 9, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 10, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=391, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 11, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 12, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=274, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | epoch: 13, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | epoch: 14, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=80, vlt=1, reward_discount=1)}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:46 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 15, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 16, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=70, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 17, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=152, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=507, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 18, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 7}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 19, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=149, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=320, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 20, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=150, vlt=3, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=375, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 4, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 21, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=292, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=199, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 22, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=677, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 23, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=189, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 5}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 24, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=238, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 25, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=451, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 3, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 26, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=131, vlt=1, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=57, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 9, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 27, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=760, vlt=3, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 28, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 29, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=76, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 30, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 30, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=124, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=165, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 6, 'productstore.115': None, 'consumer.116': 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 31, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=471, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=161, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 2, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 32, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=104, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 8, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 33, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=406, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 9}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 34, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=405, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | epoch: 35, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 8, 'productstore.112': None, 'consumer.113': 9, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 36, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=150, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=711, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 1, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | epoch: 37, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=49, vlt=1, reward_discount=1)}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:47 | LEARNER | INFO | epoch: 38, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 1, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 3, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 38, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=16, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=79, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=155, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 5, 'product.103': None, 'consumer.104': 7, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 39, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=82, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=561, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 40, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 41, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 4, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 42, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=188, vlt=2, reward_discount=1), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=94, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 2}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 43, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=102, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 44, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 45, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=315, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 7, 'productstore.112': None, 'consumer.113': 5, 'productstore.115': None, 'consumer.116': 4}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 46, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=162, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=380, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=192, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 47, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 8, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 48, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=182, vlt=2, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 6, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 49, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=480, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 1, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 50, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=78, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 6}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 51, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=301, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 4, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 52, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=316, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n", - "07:26:48 | LEARNER | INFO | epoch: 53, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100)}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 6, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 54, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=152, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=327, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 1, 'productstore.112': None, 'consumer.113': 7, 'productstore.115': None, 'consumer.116': 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 55, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=20, vlt=1, reward_discount=1), 113: ConsumerAction(id=113, product_id=2, source_id=54, quantity=561, vlt=1, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=156, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 0, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 9, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 56, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 110: ConsumerAction(id=110, product_id=1, source_id=54, quantity=177, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 0, 'product.103': None, 'consumer.104': 2, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 57, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=160, vlt=3, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 2, 'product.103': None, 'consumer.104': 1, 'product.105': None, 'consumer.106': 4, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 0}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 58, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=36, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=81, vlt=3, reward_discount=1), 106: ConsumerAction(id=106, product_id=3, source_id=1, quantity=197, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {'facility.1': None, 'facility.54': None, 'facility.107': None, 'product.48': None, 'producer.49': 500, 'product.50': None, 'producer.51': 500, 'product.52': None, 'producer.53': 500, 'product.101': None, 'consumer.102': 6, 'product.103': None, 'consumer.104': 8, 'product.105': None, 'consumer.106': 0, 'productstore.109': None, 'consumer.110': 0, 'productstore.112': None, 'consumer.113': 0, 'productstore.115': None, 'consumer.116': 8}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | epoch: 59, action: {49: ManufactureAction(id=49, production_rate=1000), 51: ManufactureAction(id=51, production_rate=1000), 53: ManufactureAction(id=53, production_rate=100), 102: ConsumerAction(id=102, product_id=1, source_id=1, quantity=105, vlt=2, reward_discount=1), 104: ConsumerAction(id=104, product_id=2, source_id=1, quantity=622, vlt=3, reward_discount=1), 116: ConsumerAction(id=116, product_id=3, source_id=54, quantity=398, vlt=1, reward_discount=1)}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n", - "07:26:48 | LEARNER | INFO | consumer2product: {102: 1, 104: 2, 106: 3, 110: 1, 113: 2, 116: 3}\n" - ] - }, - { - "data": { - "text/plain": [ - "(699539.0,\n", - " array([-107446., -337711., -469576., -414720., -350976., -445740.,\n", - " -392867., -340848., -288374., -238377., -308160., -239586.,\n", - " -191989., -131588., -95545., -59955., -24627., -101303.,\n", - " -40223., -82306., -103775., -53078., 7854., 59014.,\n", - " 46774., 29581., 68737., 120257., 178911., 216574.,\n", - " 222380., 127016., 180089., 236694., 157455., 211692.,\n", - " 93297., 148858., 213709., 270959., 325391., 379745.,\n", - " 422295., 447683., 498381., 556115., 445558., 500108.,\n", - " 553042., 606973., 651083., 618500., 617280., 680871.,\n", - " 735990., 631406., 648491., 708376., 767371., 699539.]))" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import os\n", - "import sys\n", - "import yaml\n", - "from os.path import dirname, realpath, join\n", - "\n", - "from maro.rl import EpisodeBasedSchedule, LocalLearner, StepBasedSchedule\n", - "from maro.simulator import Env\n", - "\n", - "sc_code_dir = './'\n", - "sys.path.insert(0, sc_code_dir)\n", - "import yaml\n", - "from os import getenv\n", - "from env_wrapper import SCEnvWrapper\n", - "from exploration import exploration_dict, agent2exploration\n", - "from render_tools import SimulationTracker\n", - "from env_wrapper import SCEnvWrapper\n", - "from policies import policy_dict, policy_update_schedule, get_base_consumer_policy, get_eoq_consumer_policy\n", - "\n", - "\n", - "default_config_path = join(sc_code_dir, \"config.yml\")\n", - "with open(getenv(\"CONFIG_PATH\", default=default_config_path), \"r\") as config_file:\n", - " config = yaml.safe_load(config_file)\n", - " \n", - " \n", - "topology = 'test_or'\n", - "config[\"env\"][\"topology\"] = join(sc_code_dir, \"topologies\", topology)\n", - "\n", - "\n", - "env = SCEnvWrapper(Env(**config[\"env\"]))\n", - "config[\"agent_ids\"] = [f\"{info.agent_type}.{info.id}\" for info in env.agent_idx_list]\n", - "config[\"policy\"][\"consumer\"][\"model\"][\"network\"][\"input_dim\"] = env.dim\n", - "config[\"policy\"][\"producer\"][\"model\"][\"network\"][\"input_dim\"] = env.dim\n", - "config[\"policy\"][\"consumerstore\"][\"model\"][\"network\"][\"input_dim\"] = env.dim\n", - "\n", - "# policy_dict.update({'consumer': get_base_consumer_policy(config['policy']['consumer'])})\n", - "policy_dict.update({'consumer': get_eoq_consumer_policy(config['policy']['consumer'])})\n", - "\n", - "\n", - "agent_ids = config[\"agent_ids\"]\n", - "agent2policy = {agent_id: agent_id.split(\".\")[0] for agent_id in agent_ids}\n", - "agent_to_policy = {agent_id: agent_id.split(\".\")[0] for agent_id in agent_ids}\n", - "\n", - "\n", - "env = SCEnvWrapper(Env(**config[\"env\"]))\n", - "# create a learner to start training\n", - "learner = LocalLearner(\n", - " policy_dict, \n", - " agent2policy, \n", - " env, \n", - " config[\"num_episodes\"], \n", - " policy_update_schedule,\n", - " exploration_dict=exploration_dict,\n", - " agent2exploration=agent2exploration,\n", - " experience_update_interval=config[\"experience_update_interval\"],\n", - " eval_schedule=config[\"eval_schedule\"],\n", - " log_env_metrics=config[\"log_env_metrics\"]\n", - ")\n", - "# learner.run()\n", - "tracker = SimulationTracker(60, 1, env, learner)\n", - "loc_path = './output/'\n", - "os.system(f'rm -rf {loc_path}/*')\n", - "facility_types = [\"productstore\"]\n", - "tracker.run_and_render(loc_path, facility_types)" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "RetailerFacility_109_1_productstore.png SupplierFacility_52_3_product.png\r\n", - "RetailerFacility_110_1_consumer.png\t SupplierFacility_53_3_producer.png\r\n", - "RetailerFacility_112_2_productstore.png WarehouseFacility_101_1_product.png\r\n", - "RetailerFacility_113_2_consumer.png\t WarehouseFacility_102_1_consumer.png\r\n", - "RetailerFacility_115_3_productstore.png WarehouseFacility_103_2_product.png\r\n", - "RetailerFacility_116_3_consumer.png\t WarehouseFacility_104_2_consumer.png\r\n", - "SupplierFacility_48_1_product.png\t WarehouseFacility_105_3_product.png\r\n", - "SupplierFacility_49_1_producer.png\t WarehouseFacility_106_3_consumer.png\r\n", - "SupplierFacility_50_2_product.png\t plot_balance.png\r\n", - "SupplierFacility_51_2_producer.png\t plot_reward.png\r\n" - ] - } - ], - "source": [ - "!ls output/" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Populating the interactive namespace from numpy and matplotlib\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABwgAAALQCAYAAACExiRjAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAEAAElEQVR4nOzdd3xV9f3H8dfJBhISshc7O4Gwkb0REFHAvaoWF1XrtlbrzzraOluxda+KW6aCiGyRPQMkJGGP7EH2vvf8/riBIgIyktyM9/Px4AGce+45n5tAcnPe5/P5GqZpIiIiIiIiIiIiIiIiIiItg4O9CxARERERERERERERERGRhqOAUERERERERERERERERKQFUUAoIiIiIiIiIiIiIiIi0oIoIBQRERERERERERERERFpQZzsXYCIiIiIiIiIiIiIiIjUvS1btvg7OTm9D8ShprGWyArsqqmpmda7d+/skx9QQCgiIiIiIiIiIiIiItIMOTk5vR8YGBjt5+d3zMHBwbR3PdKwrFarkZOTE5OZmfk+MOnkx5QWi4iIiIiIiIiIiIiINE9xfn5+RQoHWyYHBwfTz8+vEFsH6S8fs0M9IiIiIiIiIiIiIiIiUv8cFA62bLWf/1/lgQoIRUREREREREREREREpEE8++yz/sXFxReUTz300EPBTz/9dEBd19QSKSAUERERERERERERERGRBvHOO+8ElJSUKJ+yM30CREREREREREREREREpM4VFRU5DB8+PCwyMjImPDw89uGHHw7Kzs52HjZsWET//v0jAN555x3viIiImPDw8Nh77rkn5PhzZ82a1TYmJiY6MjIyZsCAARGnHvvVV1/1HTp0aHhJSYnRkK+puXCydwEiIiIiIiIiIiIiIiLS/MyZM6dtYGBg9cqVK/cC5OXlOX755Ze+q1atSg0KCqo5ePCg8zPPPBOyZcuW3X5+fjVDhgyJmDlzpteoUaNK7r333k4rV65MjoqKqsrKynI8+bh/+9vf/JYuXeq5ePHiva1atdIaixdAAaGIiIiIiIiIiIiIiEgz9+ishPapmcWt6/KYEYEeZS9fFX/kTI/36tWr/Mknn2x/zz33hFxxxRWF48aNKzn58Z9//rnNJZdcUhwcHFwDcO211+avWrXK3dHR0ezXr19xVFRUFUBAQIDl+HO++uorn6CgoKrFixfvc3V1VTh4gTRiVEREREREREREREREROpc9+7dK7du3ZrUrVu38ieffDLkkUceCTr5cdM8fb5nmiaGcfrJoZGRkeVHjx51PXDggHPdV9xyqINQRERERERERERERESkmTtbp199OXjwoLO/v3/N9OnT8z08PKz//e9/fdq0aWMpLCx0CAoKYujQoaWPP/54+4yMDCc/P7+ab775xnv69OnZI0aMKH344Yc7JicnuxwfMXq8i7BHjx5lf/jDH3ImTZoU9uOPP+7p1KlTdUO/ruZAAaGIiIiIiIiIiIiIiIjUuS1btrR64oknQh0cHHBycjLffPPNQ6tXr3YfP358uL+/f/WGDRtSn3766bRhw4ZFmKZpjBo1qvCmm24qAJgxY8bByZMnh1mtVnx8fKrXrl275/hxL7300pK///3vR8ePHx++fPny1KCgoBq7vcgmyjhT+6aIiIiIiIiIiIiIiIg0XQkJCQfj4+Nz7V2H2FdCQoJvfHx8p5O3aQ1CERERERERERERERERkRZEAaGIiIiIiIiIiIiIiIhIC6KAUERERERERERERERERKQFUUAoIiIiIiIiIiIiIiIi0oIoIBQRERERERERERERERFpQRQQioiIiIiIiIiIiIiIiLQgCghFREREREREREREREREWhAFhCIiIiIiIiIiIiIiItIoPPTQQ8FPP/10wMUe57PPPvP885//HHi+z0tJSXF5++23vS/2/L/l5PpmzpzptWXLFrf6PufJnBryZCIiIiIiIiIiIiIiIiIAVqsV0zRxdHS84GNUV1fj7Oz8q+033nhjIVB4vsfbs2eP61dffeV9991355/ruS7EyfXNmzfPq6amprB3794VdXLwc6AOQhEREREREREREREREakXzzzzTEB4eHhseHh47LPPPuufkpLi0qVLl9ibbrqpQ2xsbMy+fftcHn/88cBOnTrFDRw4MGLPnj2ux5+bmJjoOmTIkPDY2Njo3r17R27bts0NYOrUqZ2mTZsW2r9//4jp06eHnu68M2bM8Lnllls6HN//1ltvbd+zZ8+o0NDQbh999FG7M9X75JNPhmzevNk9Kioq5q9//av/jBkzfMaPH99l5MiRYUOGDIkoLCx0GDBgQERMTEx0REREzKeffuoFts7DLl26xF533XUdw8LCYgcNGhReUlJiADz//PP+Xbt2jY2IiIiZOHFil5PrW7JkSZulS5d6PfXUU6FRUVExiYmJrmeqrS6pg1BERERERERERERERKS5m/eH9mQnta7TY/rHlHHlf46c6eHVq1e3/vzzz322bNmy2zRNevfuHT1q1KjigwcPur333nsHP/3008OrV69uPXfuXO+dO3cmVVdX06NHj5iePXuWAUybNq3ju+++e6hbt26Vy5cvb3PPPfd0WL9+fSrAvn373NasWZPq5HRuUVdWVpbz5s2bk7dv3+42efLksNtuu+3Y6fZ74YUX0l599dWAFStW7AVbkLd161b3HTt2JAYEBFiqq6tZuHDhXm9vb2tGRoZT//79o2644YYCgMOHD7t9+umn+wcOHHhowoQJXT755JN206dPz58xY0bgoUOHdrZq1crMzc39RbvkmDFjSkePHl0wceLEwjPVVB8UEIqIiIiIiIiIiIiIiEidW7lypfuECRMK2rZtawW47LLLjq1YscIjKCioatSoUaUAK1ascJ8wYUKBh4eHFWDs2LEFAIWFhQ7btm1zv/rqq7seP15VVZVx/M9Tpkw5dq7hIMCkSZMKHB0d6d27d0VeXt55zQkdMmRIUUBAgAXAarUaDzzwQOj69evdHRwcyM7Odjl69KgTQEhISOXAgQPLAXr27Fl28OBBV4DIyMjyyZMnd540aVLBjTfeWHA+564vCghFRERERERERERERESau7N0+tUX0zRPu71169bWk/9uGMav9rFYLHh4eNQkJycnne4Y7u7u1tNtPxM3N7cTxZyprjM5ud533nnHOy8vz2nnzp27XV1dzZCQkG7l5eUOAC4uLicO7OjoaB7fvmLFij2LFi3ymDdvntdLL70UvGfPnl3nVUA90BqEIiIiIiIiIiIiIiIiUudGjhxZ8v3333sVFxc7FBUVOXz//fftRowYUXzqPgsXLvQqKSkxjh075rBkyRIvAG9vb2toaGjVhx9+2A7AarWybt26VvVds6enp6WkpMTxTI8XFhY6+vr6Vru6uprfffedR3p6usvZjmexWNi3b5/L5ZdfXvzmm28eLS4udiwsLPzF8d3d3S1FRUUNmtmpg1BERERERERERERERETq3ODBg8tuuOGGvF69ekUD3HzzzTm+vr6WU/eZPHlyflxcXGxISEhlv379So4/9sUXX+y/4447Or744otBNTU1xuTJk/MHDBhQXp819+vXr9zJycmMjIyMueGGG3LbtWv3i3qnTZuWP378+LC4uLjo2NjYss6dO1ec7Xg1NTXGDTfc0Lm4uNjRNE3jrrvuyjr1Y3DjjTfm33PPPZ3efvvtgFmzZu2LjY2trI/XdjLjfNsoRUREREREREREREREpPFLSEg4GB8fn2vvOsS+EhISfOPj4zudvE0jRkVERERERERERERERERaEI0YFRERERERERERERERkSbp9ddf93nrrbcCTt7Wt2/fkpkzZx4+2/M2btzY6pZbbul88jYXFxfrjh07kuujzsZGI0ZFRERERERERERERESaIY0YFdCIUREREREREREREREREZEWTwGhiIiIiIiIiIiIiIiISAuigFBERERERERERERERESkBVFAKCIiIiIiIiIiIiIiItKCKCAUERERERERERERERGRRuGhhx4KfvrppwMu9jgLFizwWLJkSZu6qKl169Y9AQ4ePOg8bty4LmfaLzc31/Ef//iH39mO1bNnz6jj9Y0YMSLsfOqYOXOm15YtW9zO5zlnooBQREREREREREREREREGpzVasVisVzUMaqrq0+7ffny5R6rV692v6iDn6JTp07VP/zww/4zPZ6Xl+f4wQcf+J/usZqaGgC2bduWfKHnnzdvnteOHTtaXejzT+ZUFwcRERERERERERERERGRxusva/7Sfu+xva3r8phh7cLKnhv03JGz7fPMM88EfPbZZ74AN998c861115bMH78+PCBAwcWb9myxX3+/Pl7P/jgA++vvvrKNzg4uMrHx6e6Z8+eZQCJiYmud999d4f8/HwnNzc36/vvv3+oZ8+eFVOnTu3Url27mp07d7bu3r172XvvvXf05HOmpKS4fPLJJ34ODg7m119/7fOvf/3rcJcuXap+97vfdcrLy3Py8fGp+eSTTw6Gh4dXna7m5ORkl+uuu65LTU2NMWrUqMKTjztx4sTwPXv2JG7evNnttttu61xdXW1YrVZmz56974knngg5cuSIa1RUVMywYcOKLr/88sLnnnsuyN/fvzopKan1vn37Elu3bt2zrKxsG0BxcbHjmDFjuu7fv9+tf//+xTNnzjzs6OjIyft89NFH7RYsWOB599135yxdutRr/fr1Hi+++GLQ7Nmz9wGc7uNzLp87dRCKiIiIiIiIiIiIiIhInVu9enXrzz//3GfLli27N2/evPuTTz7xy83NdTx48KDbbbfdlrd79+6krKwsp7lz53rv3LkzacGCBXsTEhJOjAWdNm1axzfffPNwYmLi7pdffvnoPffc0+H4Y/v27XNbs2ZN6qnhIEBkZGTVLbfcknP33XdnJScnJ40bN67k7rvv7nDDDTfkpaamJl177bV599xzT/sz1T19+vQO06ZNy9m1a9fuwMDA07YovvHGG37Tp0/PSk5OTtqxY8fuzp07V7366qtH27dvX5mcnJz0zjvvHAXYsWNHm5dffjlt3759iaceY+fOnW1ef/31IykpKYkHDx50/eSTT9qdqaYxY8aUjh49uuD5558/mpycnBQbG1t5to/Pb1EHoYiIiIiIiIiIiIiISDP3W51+9WHlypXuEyZMKGjbtq0V4LLLLju2YsUKj6CgoKpRo0aVAqxYscJ9woQJBR4eHlaAsWPHFgAUFhY6bNu2zf3qq6/uevx4VVVVxvE/T5ky5ZiT07nHXNu2bWuzaNGifQD33HNP/l//+tfQM+27detW9+P73nXXXXnPPffcr/YdMGBA6SuvvBJ09OhRl+uuu+5Yt27dKk93rO7du5dGRUWdtlOxW7dupTExMVUA11xzTf7q1avdb7vttmPn8np+6+PzWxQQioiIiIiIiIiIiIiISJ0zTfO021u3bm09+e+G8etcy2Kx4OHhUZOcnJx0umO4u7tbT7e9rjg4OJy++Fp33313/pAhQ0rnzp3rOX78+Ig333zzYGRk5K9CwlNf68lOfd3H/37y9vLy8tOGfr/18fktGjEqIiIiIiIiIiIiIiIidW7kyJEl33//vVdxcbFDUVGRw/fff99uxIgRxafus3DhQq+SkhLj2LFjDkuWLPEC8Pb2toaGhlZ9+OGH7QCsVivr1q1rda7n9vDwsBQXFzse/3vPnj1L33///XYA77zzjnefPn1KzvTcXr16lbz33nveAO+9957P6fZJSkpyiY6Ornzqqaeyx44dW7B9+/ZWnp6eltLS0nPO3nbu3NkmOTnZxWKxMGvWLO8hQ4YUA/j4+FRv3brVzWKxMH/+/BNjR93d3S1FRUUOcPEfHwWEIiIiIiIiIiIiIiIiUucGDx5cdsMNN+T16tUrunfv3tE333xzjq+vr+XUfSZPnpwfFxcXO3HixK79+vU7Edx98cUX+z/66CPfyMjImPDw8NjZs2d7neu5p06dWrBw4UKvqKiomB9++MH9rbfeOjxz5kzfiIiImC+++MLnzTffPOPI1TfffPPwu+++6x8XFxddWFjoeLp9Zs6c6R0REREbFRUVs2fPHre77rorLzAw0NK7d++S8PDw2LvuuuuMI0yP69GjR8nDDz8cGhEREduhQ4fKm2++uQDgr3/9a9oVV1wRNmDAgMiAgIATayDeeOON+TNmzAiMjo6OSUxMdL2Yj49xpvZOERERERERERERERERaboSEhIOxsfH59q7DrGvhIQE3/j4+E4nb1MHoYiIiIiIiIiIiIiIiEgL4mTvAkREREREREREREREREQuxOuvv+7z1ltvBZy8rW/fviUzZ848/FvPffzxxwPnz5/vffK2K664Iv/FF1/MrOs6GxuNGBUREREREREREREREWmGNGJUQCNGRURERERERERERERERFo8BYQiIiIiIiIiIiIiIiIiLYgCQhEREREREREREREREZEWRAGhiIiIiIiIiIiIiIiI1AtHR8feUVFRMZGRkTExMTHRS5YsafNbz2ndunXPhqitJXOydwEiIiIiIiIiIiIiIiLSPLm6ulqTk5OTAGbPnt32z3/+c+iYMWNS7F1XS6cOQhEREREREREREREREal3hYWFjp6enjW1f3YYMGBARExMTHRERETMp59+6nWa/U+7T0pKikuXLl1ir7vuuo5hYWGxgwYNCi8pKTEAdu3a5Tpw4MCI4x2LiYmJrgB/+ctfAuLi4qIjIiJiHnzwweCGe9WNkzoIRUREREREREREREREmrlHZyW0T80sbl2Xx4wI9Ch7+ar4I2fbp7Ky0iEqKiqmsrLSyM3Ndf7+++9TAVq3bm1duHDhXm9vb2tGRoZT//79o2644YYCB4f/9badaR+Aw4cPu3366af7Bw4ceGjChAldPvnkk3bTp0/Pv+GGGzo/8sgjmbfccktBWVmZYbFYjDlz5rTdu3ev244dO3abpsno0aPDFi1a5D5+/PiSuvx4NCUKCEVERERERERERERERKRenDxidOnSpW1uu+22zqmpqYlWq9V44IEHQtevX+/u4OBAdna2y9GjR506dOhQc/y5Z9oHICQkpHLgwIHlAD179iw7ePCg67FjxxyysrJcbrnllgKA1q1bm4D5ww8/tP3pp5/axsTExACUlZU5JCcnuykgFBERERERERERERERkWbrtzr9GsLo0aNLjx075pSRkeE0e/Zsz7y8PKedO3fudnV1NUNCQrqVl5f/Ymm8d955x/tM+7i4uJjH93N0dDTLy8sdTNM89ZQAmKbJAw88kPHoo4/m1usLbEK0BqGIiIiIiIiIiIiIiIjUu23btrlZrVYCAgJqCgsLHX19fatdXV3N7777ziM9Pd3l1P3PZZ+TeXt7WwMDA6tmzpzpBVBeXm4UFxc7jB8/vmjmzJm+hYWFDgAHDhxwTktLa9FNdC36xYuIiIiIiIiIiIiIiEj9Ob4GIdg6+d56662DTk5OTJs2LX/8+PFhcXFx0bGxsWWdO3euOPW557LPqT799NMDd9xxR8fnnnsu2NnZ2fzmm2/2TZkypSgxMdGtb9++UWBb2/Czzz47EBISUvNbx2uujDO1W4qIiIiIiIiIiIiIiEjTlZCQcDA+Pl5jNVu4hIQE3/j4+E4nb9OIUREREREREREREREREZEWRAGhiIiIiIiIiIiIiIiISAuigFBERERERERERERERESkBVFAKCIiIiIiIiIiIiIiItKCKCAUERERERERERERERERaUEUEIqIiIiIiDRzhmE8YxjGp/auoy4ZhjHcMIyjjaCOPxuG8X4dH7NRvDYREREREWm+FBCKiIiIiIicxDCMwYZhrDUMo9AwjHzDMNYYhtG39rFbDcP4+aR929Y+PtswjHDDMEzDMJxOOd7HhmE8f4ZzeRmG8aFhGJmGYRQbhpFqGMbjJz1uGoYRVl+v9Qw1hda+ntzaj8FOwzBurX2s0+le428c76BhGKPrreA6UPs5qjIMo+SkXwnn8lzTNP9mmua0+q5RRERERKSpcnR07B0VFRUTFhYWGxkZGfPMM88EWCwWe5cFwEMPPRT89NNPB9i7Dns45x/qREREREREmjvDMNoCC4B7gK8BF2AIUHmafdsBi4G9wC1A6AWc8p9AGyAaKAQigLgLqb0OzQQSgI7YXnc3INCuFTWMl0zTfMreRYiIiIiINDeurq7W5OTkJIC0tDSnq6++ukthYaHjP//5z3R719aSqYNQRERERETkfyIATNP8wjRNi2ma5aZp/mia5o6TdzIMwxdYDiQCN5mmWXOB5+sLfG6a5jHTNK2maSabpjmr9hw/1e6TUNvRdm3t9jsMw9hb2934rWEYwSfVFWsYxpLax7IMw/jzqSc0DMPZMIwvarsEXc5Q08emaZaaplljmuY20zQX1T52vKaC2poGGIbR1TCM5YZh5NV2HX5mGIZX7blmAh2A72r3f+x04zNP7jI0DKOfYRibDcMoqn0Nr53tA1g74jO39hg31m7rW/tcp5P2m2oYxvazHesMxz/eNXmnYRjphmFkGIbx8EmPnxjfahiGm2EYn9Z+LAoMw9hkGEZA7WPBtZ+v/NrP3x0nHaNVbRfjMcMwkmo/ByIiIiIizU5ISEjN+++/f/Cjjz7yt1qt1NTUcNddd4XGxcVFR0RExLz88su+AAsWLPDo27dv5IQJE7p06tQpbvr06SFvvfWWd7du3aIjIiJiEhMTXQE+//xzz+7du0dFR0fHDBw4MOLIkSNOYOsMvPrqqzv169cvMjQ0tNvzzz/vf7yGxx9/PLBTp05xAwcOjNizZ4+rfT4S9qcOQhERERERkf9JBSyGYfwX+BJYb5rmsVP28QZWAauBe0zTNC/ifOuBF2q7EX82TXPP8QdM0xxqGIYJxJumuRfAMIyRwN+BsdjCyVdq6xxqGIYHsLR22+WAMxBz8skMw2gFzAJysAWbp5vrsx74j2EYbwBrTdM8fNJjQ4EDgNfxULR2BOrfsYWHbYHZwDPAA6Zp3mwYxhBgmmmaS2v3H/4bH5PXgddN05xpGIY7Z++oDAR8gRDgEuB7wzA2m6a5yTCMPGAMcDzcvAlbd+SFGgGEA12A5YZhJBx/TSf5HeAJtMfWfdkDKK997Atsn7NgIApYYhjGftM0lwH/B3St/dXmpJpFREREROrMo7MS2qdmFreuy2NGBHqUvXxV/JHzeU5MTEyV1WolLS3N6auvvvLy9PS07Nq1a3d5ebnRt2/fqMsvv7wIIDk5udWsWbP2+/v713Ts2LGbq6tr7s6dO3c/99xz/q+++qr/hx9+eGTMmDEl1113XbKDgwOvvfaa77PPPhv43nvvHQXYu3ev29q1a1MKCgoco6Oj4x599NGcjRs3tpo7d673zp07k6qrq+nRo0dMz549y+ryY9JUqINQRERERESklmmaRcBgwATeA3Jqu75OXpOiPbZOw48uMhwEuA/4DLgXSKrtLBt/lv1vBD40TXOraZqVwBPAAMMwOgETgUzTNF81TbPCNM1i0zQ3nPTctsAPwD7gtjOEgwBXYws//wIcMAxju1G7BuPpmKa51zTNJaZpVpqmmQO8Bgw7lxd/BtVAmGEYvqZplpimuf439v9L7blXAQuBa2q3/xdbKIhhGN7ApcDnZznOI7Vdf8d//feUx/9a21W5E/gIuP4MtfsAYbUdqFtM0ywyDKM9tn9Xj9d+brYD7wM31z7vGuAF0zTzTdM8Asz4jdcsIiIiItKkHf9RaunSpW2//vprn6ioqJiePXtGHzt2zCkpKckNoFu3bqUdO3asbtWqldmhQ4fK8ePHFwLEx8eXHz582AXgwIEDLkOGDAmPiIiImTFjRmBycnKr4+cYO3ZsQatWrcygoKAab2/v6qNHjzqtWLHCfcKECQUeHh5Wb29v69ixYwsa/tU3DuogFBEREREROYlpmruBWwEMw4gCPgX+xf8CoQTgG2CRYRijTNPcVrv9+JhR55P+fPzv1Wc4VznwN+Bvtesf/gn4xjCMDqZp5p/mKcHA1pOeX1LbKReCLbjcd5aXdkltLdefLdis7Zj8E/Cn2lGqrwDzDMM47RqLhmH4Ywu0hgAe2G5EPbXr8nz8HngWSDYM4wC2YG7BGfY9Zppm6Ul/P4TtYwS2z9vu2i7Ea4DVpmlmnOW8r/zGGoQn3xV9CNvajKeaie3z8GXtmNVPgSdra8o3TbP4lGP0qf1z8GmOLyIiIiJSp86306++JCUluTg6OhISElJjmqbx6quvHp46dWrRyfssWLDAw9XV9cTPLQ4ODri5uZnH/2yxWAyAe++9t8Mf//jHzBtvvLFwwYIFHs8+++yJJRhOfr6joyM1NTUGgGEY9f0SmwR1EIqIiIiIiJyBaZrJwMecMubSNM3XgX9gGxN5/LEMbEFgp1MO05lzCHxquxf/hm3EZOcz7JYOdDz+F8Mw2mDrWEvDFjB1PcspfsQ2CnTZKR2RZ6spF1tAGIxttOrpgsW/127vbppmW2xdeyf/xH3qc0qBE2ONDMNwBPxOOuce0zSvB/yBF4FZta/zdNqd8lgHbB8jTNNMA9YBk7F16l3MeFGwBX+/Os/JTNOsNk3zr6ZpxgADsXV13lK7r3ftGNiTj5FW++eM0xxfRERERKTZSU9Pd7rjjjs63nbbbdkODg6MGTOm8K233vKrrKw0AHbs2OFaVFR0ztlVcXGxY4cOHaoBPv74Y5/f2n/kyJElCxcu9CopKTGOHTvmsGTJEq8LfjFNnAJCERERERGRWoZhRBmG8fDxbrna0ZDXY1uX7xdM03wJ23p5Sw3DiKwd2Tkb25qCPoZhOBuGcT22dQBPu6acYRh/MQyjr2EYLoZhuAF/BAqAlNpdsrCteXfc58BthmH0MAzDFVuguME0zYPAAiDQMIwHDMNwNQzDwzCM/qep+XNsIaHvGWp60TCMOMMwnGoDrXuAvaZp5mFbu9B6Sk0eQAlQYBhGCPDoKYc89TWkAm6GYVxmGIYz8BTgetL5bzIMw880TWvtxwLgTONQAf5a+/Ebgi2Q++akxz4BHsPW7Tf3LMc4F38xDKO1YRixwG3AV6fuYBjGCMMwutWGnkXYAmNL7djQtcDfDcNwMwyjO7ZOyc9qn/o18IRhGO1q/+3dd5G1ioiIiIg0GpWVlQ5RUVExYWFhsSNGjIgYNWpU0SuvvJIO8OCDD+ZGRUVVdOvWLTo8PDz2jjvu6FhdXX3OLX5PPvlk+vXXX9+1d+/ekT4+PjW/tf/gwYPLJk+enB8XFxc7ceLErv369Su5mNfWlBkXv2SGiIiIiIhI81AbcP0TGAR4YQuoFgCP1q4ldyswzTTNwSc953lsI0mHAfnAy8B4bF1yScBjpmmuOcP5ngKuw9YxVgPsAP5smuba2sfvBv4PaAXcaZrm17XbHgXaYQud7jZN82jt/nHYQsteQCXwL9M0/2EYxjPY1sU7vibf89jCtJGnjjI1DOMNYBwQBJQDG2pf/+7ax5/FFho61+5XjC2IiwT2YuvUe9A0zeMh6xXAG9jWQHzeNM1Xaj+OfwccgZewrcE4zTTNpYZhfAqMrf34HQKeNE1z3mk+dsOxjfB8C3gQKKvdd+ZJ+7QGMoG5pmn+7nSfg9r9PgZuAKpO2lxhmqZv7fqOB4C7gGew3Wj7Wm3Yyskf29pA+BkgFFto+hXwkGmaNbXB39vYOguPAS+bpvn2SXW+DUzC1m34EfDH4x9DEREREZELlZCQcDA+Pj7X3nWIfSUkJPjGx8d3OnmbAkIRERERERFptgzD2AfcZZrm0gt8fidsAaGzaZq/eUeyiIiIiEhjooBQ4PQBoUaMioiIiIiISLNkGMZUbGsgLrd3LSIiIiIiIo2Jk70LEBEREREREalrhmGsxLb+48216xmKiIiIiIhILQWEIiIiIiIi0uyYpjm8jo5zEDDq4lgiIiIiIiKNhUaMioiIiIiIiIiIiIiIiLQgCghFREREREREREREREREWhCNGBWxM19fX7NTp072LkNEREREREREREREzmLLli25pmn62buOpsbR0bF3eHh4ucViMdq3b1/59ddfH/D19bU0dB39+vWLfOWVV44MHTq0rKHP3RgpIBSxs06dOrF582Z7lyEiIiIiIiIiIiIiZ2EYxiF719AUubq6WpOTk5MApkyZ0unll1/2e/HFFzPr85zV1dU4OzvX5ymaPI0YFRERERERERERERERkXp3ySWXlKalpbkAJCYmug4ZMiQ8NjY2unfv3pHbtm1zq6mpITQ0tJvVaiU3N9fRwcGh96JFi9wBevfuHblr1y7XFStWtO7Zs2dUdHR0TM+ePaMSEhJcAWbMmOEzfvz4LiNHjgwbMmRIRElJiTFx4sQuERERMZdddlmXiooKw56vvbFRB6GIiIiIiIiIiIiIiEhzd/vt7dm1q3WdHjMurowPPzxyLrvW1NSwYsUKj9///ve5ANOmTev47rvvHurWrVvl8uXL29xzzz0d1q9fn9q5c+eKrVu3uu3Zs8c1JiambOXKle7Dhw8vzczMdImLi6vMz8932LhxY7KzszPz5s3zeOyxx0IXL168D2Dr1q3uO3bsSAwICLA888wzAa1atbKmpqYmbdiwodWgQYNi6vS1N3EKCEVERERERERERERERKReVFZWOkRFRcWkpaW5xMXFlV155ZVFhYWFDtu2bXO/+uqrux7fr6qqygAYOHBg8bJlyzwOHDjg+uijj2Z88MEHfj/99FNJfHx8KUB+fr7jtdde2/ngwYNuhmGY1dXVJzoDhwwZUhQQEGAB+Pnnn93vv//+bID+/fuXR0REaO3BkyggFJHm5dOrYP/Kuj9uKy+4Yzl4daj7Y4uIiIiIiIiIiIjUt3Ps9Ktrx9cgzMvLcxw7dmzYP/7xD//p06fnenh41Bxfm/Bkw4cPL3nzzTf9srKyXF577bW0f/7zn4HLli3zGDx4cDHA448/HjJs2LDiJUuW7EtJSXEZOXJk5PHntm7d2nrysQxDU0XPRAGhiDQfeftg7xKIGA/+0XV3XNMK6/4Da16Hy16tu+OKiIiIiIiIiIiItBA+Pj6WGTNmHL7qqqvCHn300ZzQ0NCqDz/8sN3tt99+zGq1smHDhlYDBgwoHz58eOm0adM6t2/fvrJ169ZmbGxs2SeffOI3d+7cPQBFRUWOoaGhVQDvvPOO75nON3jw4JJPP/3U+/LLLy/etGmTW2pqat2OV23iFBCKSPOROMf2+2WvgGdo3R67PB+2zoShj4JHYN0eW0RERERERERERKQFGDRoUHl0dHT5+++/3+6LL77Yf8cdd3R88cUXg2pqaozJkyfnDxgwoLxVq1ZmYGBgVZ8+fUoBhgwZUvLtt9969+vXrxzg8ccfz5w2bVrnGTNmBA4ZMqToTOd65JFHsq+77rrOERERMbGxsWXdunUrbajX2RQYpmnauwaRFq1Pnz7m5s2b7V1G8/DmAHBtC79fXPfHztsH/+4Dl0yHS1+o++OLiIiIiIiIiIhIo2YYxhbTNPvYu47zkZCQcDA+Pj7X3nWIfSUkJPjGx8d3Onmbg51qERGpW9m7ITsJ4qbWz/F9ukLcVbD5IyjNq59ziIiIiIiIiIiIiIg0AAWEItI87JoDhgPEXFF/5xjyEFSXwoa36u8cIiIiIiIiIiIiIiL1TAGhiDR9pmlbf7DTYPAIqL/z+EdD9OWw4V2oKKy/84iIiIiIiIiIiIiI1CMFhCLS9GXugLy9EDul/s815GGoLISN79X/uUREREREREREREQujtVqtRr2LkLsp/bzbz11uwJCEWn6ds0GByeInlT/5wruCWFjYP2bUFVa/+cTERERERERERERuXC7cnJyPBUStkxWq9XIycnxBHad+piTHeoREak7pgm75kKXEdDGp2HOOfQR+PBS2PIxDPhDw5xTRERERERERERE5DzV1NRMy8zMfD8zMzMONY21RFZgV01NzbRTH1BAKCJN29HNUHgYRjzRcOfscAl0GgJr34A+vwdnt4Y7t4iIiIiIiIiIiMg56t27dzbQAKPXpKlRWiwiTVviHHB0gajLGva8Qx6G4gzY/lnDnldERERERERERERE5CIpIBSRpstqhcS5tjUB3Twb9txdhkNIH1jzL7BUN+y5RUREREREREREREQuggJCEWm6Dq+zdfHFTWn4cxuGbS3CgsOw85uGP7+IiIiIiIiIiIiIyAVSQCgiTdeu2eDUCiLG2ef8EeMgoBusfg2sFvvUICIiIiIiIiIiIiJynhQQigCGYXxoGEa2YRi7TtrmbRjGEsMw9tT+3u6kx54wDGOvYRgphmFcetL23oZh7Kx9bIZhGEZDv5YWw1IDSfMhchy4utunBsOAIQ9B3h5bLSIiIiIiIiIiIiIiTYACQhGbj4FT29D+BCwzTTMcWFb7dwzDiAGuA2Jrn/OmYRiOtc95C7gTCK/9ZafWthbg4E9QlguxdhgverKYK8AnHFa/CqZp31pERERERKRJqai2MPWttdw9cwvfJaRTVlVj75JEREREpIVwsncBIo2BaZo/GYbR6ZTNVwDDa//8X2Al8Hjt9i9N06wEDhiGsRfoZxjGQaCtaZrrAAzD+AS4ElhUz+W3TLvmgIsHhI+xbx0OjjDkYZh3N6T+AJHj7VuPiIiIiIg0GYsTM9ly6BherZ35ITETN2cHRkUFcFn3IEZE+tPKxfG3DyIiIiIicgEUEIqcWYBpmhkApmlmGIbhX7s9BFh/0n5Ha7dV1/751O1S12qqYPe3EDUBnFvZuxrodhWs/Bv89IptXUJNlhURERERkXMwa8tRQrxaserR4Ww5dIyFOzP4fmcmC3dm0MrZkVHR/kzsHsTwSH/cnBUWioiIiEjdUUAocv5Ol/6YZ9n+6wMYxp3YRpHSoUOHuquspdi/AioKIW6qvSuxcXSGwQ/Cggdh/0roOsLeFYmIiIiISCOXXlDOz3tzuW9kOE6ODvTv4kP/Lj783+WxbDyQz4Id6fywK5MFOzJo4+LIqGhbZ+GwCD+FhSIiIiJy0RQQipxZlmEYQbXdg0FAdu32o0D7k/YLBdJrt4eeZvuvmKb5LvAuQJ8+fbRw3fnaNRvcvKBLIwrietwIq16yrUWogFBERERERH7D3G1pmCZM7fXLwTOODgYDuvowoKsPf50Uy4YD+SzYkcEPuzL4NiEdd1cnxsQEcFm3IIZE+OLqpLBQRERERM6fAkKRM/sW+B3wj9rf55+0/XPDMF4DgoFwYKNpmhbDMIoNw7gE2ADcArzR8GU3c9XlkPw9xF4JTi72ruZ/nFxh4P2w+Ak4vB46XGLvikREREREpJEyTZNZW47Sr7M3HX3anHE/J0cHBoX5MijMl2eviGX9/jwW7sjgh8RM5m5Lw8PViTGxAUzsHsTgMD9cnBwa8FWIiIiISFOmgFAEMAzjC2A44GsYxlHg/7AFg18bhvF74DBwNYBpmomGYXwNJAE1wB9M07TUHuoe4GOgFbCo9pfUpT1LoKoY4qbYu5Jf6/07WP2KbS3Cm2bZuxoREREREWmkth4+xoHcUu4Z3vWcn+Ps6MCQcD+GhPvx3JVxrNmby8IdGSxOzGTO1jTaujkxNjaQy7oHMTjMF2dHhYUiIiIicmaGaWq6oYg99enTx9y8ebO9y2g6vrkVDqyGh1PAsRHe47D6VVj2LNy5EoJ72rsaERERERFphJ6Ys5N529LY9NRo3F0v7ueaqhora/bmsmBHBj8mZVJcUYNnK2fGxAQwOtqfweF+F30OERERsTEMY4tpmn3sXYdIXdA7RBFpOipLIOUH6Hlj4wwHAfpOg59ftwWF135q72pERERERKSRqai2sCAhnfHdAuskuHNxcmBElD8jovyprInj5z21YWFiJrO2HMXZ0aB/Zx9GRvkzMsqfTr5nHmkqIiIiIi1HI73CLiJyGqk/QE05xDbC8aLHuXlC/7vgp5cgezf4R9u7IhERERERaUQWJ2ZSXFnDVb1D6/zYrk6OjIoOYFR0ANUWK1sOHWNFcjbLkrN5dkESzy5IootfG0ZG+jMy2p++nbw1ilRERESkhdKIURE704jR8/DFDZC+FR5MAodG/ENsWT78Mw6iLoOp79m7GhERERERaURu/mAD+3NKWf3YCBwcjAY77+G8MpYnZ7E8JYf1+/KosljxcHViaIQfI6P8GR7ph4+7a4PVIyIi0hRpxKg0J+ogFJGmobwA9i6xjfBszOEgQGtv6Hs7rPsPDP8T+HS1d0UiIiIiItIIZBSW8/PeXO4bGd6g4SBAB5/W3DqoM7cO6kxpZQ1r9uayPDmb5cnZLNyZgWFAj/ZeJ7oLY4LaYhgNW6OIiIiINBwFhCLSNKR8D5YqiJtq70rOzYB7YcO7sOZfMOkNe1cjIiIiIiKNwJytaZgmTO0VYtc62rg6MTY2kLGxgZimSWJ6EctrR5G+tjSVV5ekEtjWjRG16xYOCvOhtYsuIYmIiIg0J3p3JyJNw67Z4NUBQnrbu5Jz4xEIvW6BLR/DsMfBs+7XFxERERERkabDNE1mbzlKv87edPRpY+9yTjAMg7gQT+JCPLl/VDg5xZWsTMlmRUo23yWk88XGw7g4OTCgiw+jom2BYWi71vYuW0REREQukgJCEWn8SvNg/0pbV15TGnEz6I+w5SNYMwMmvGTvakRERERExI62Hi5gf24pdw9v3EsQ+Hm4cnWf9lzdpz1VNVY2H8xnWXI2K5KzeXp+Ik/PTyQ6qC2jo/0ZHR1AtxDPBh+XKiIiIiIXTwGhiDR+u78Faw3ETbF3JefHqz3EXwdb/wtDHwF3f3tXJCIiIiIidjJry1FaOTsyoVuQvUs5Zy5ODgwM82VgmC9/mRjD/pwSlidnsyQpi/+s2Msby/fi5+F6IiwcFOaLm7OjvcsWERERkXOggFBEGr/EOeATBoHd7V3J+Rv8EGz/HNb9G8Y8a+9qRERERETEDiqqLSxISGd8t0DcXZvupZgufu508XNn2pAuHCutYmVqNkt3Z/NdQgZfbDyCm7MDg8P8GB3tz8hof/w93OxdsoiIiIicQdN9VyoiLUNxFhz8GYY80rTGix7n0xVip8CmD2DQA9Da294ViYiIiIhIA1ucmElxZQ1X9W4+a5O3a+PC5J6hTO4ZSlWNlQ0H8li229ZduHR3FgA92nvZugtjAogM8MBoij/TiYg0UUuSslizN5dBYb4M7OpDmyZ8g4qI1A/DNE171yDSovXp08fcvHmzvctovDa8C4sehekbwD/K3tVcmKwkeGsADPsTjHjC3tWIiIiIiEgDu/mDDezPKWX1YyOa/Xp9pmmSnFnMst1ZLNmdTcKRAgBC27VidHQAo6MD6NfZGxcnB/sWKiLSjBWWVTP05RUUllcD4Oxo0LeTN8Mi/Bge6U9EgLtu2rhAhmFsMU2zj73rEKkLum1ARBq3XbPBP6bphoMAATEQNRE2vAUD/gBube1dkYiIiIiINJCMwnJ+3pvLfSPDm304CGAYBtFBbYkOasu9I8PJLqpgeXI2S3dn8cXGw3y89iAerk4MjfRjTHQAwyP98GrtYu+yRUSalTdX7aWoopp5fxhEWVUNq1JyWJWaw98XJfP3RckEtnWrDQv9GBjmi2crZ3uXLCJ2oIBQRBqvwqNwZD2MfMrelVy8IQ9D8gLY/AEMftDe1YiIiIiISAOZszUN04SpvULsXYpd+Ld147p+HbiuXwfKqyys2ZvL0t1ZLN2dzcIdGTg6GPTp2I6xsYGMjQmgvXdre5csItKkpReU89Gag0zuEUKP9l4ADOzqyxMTosksrGBVajarUnP4flcGX20+gqODQa8OXgyP9GdYhB8xQW1bxA0tIqIRoyJ2pxGjZ7H2DfjxKbhvq20tv6Zu5hTISIAHdoKLfugVEREREWnuTNNk1Kur8PVw5eu7Bti7nEbFajXZkVbIkqRMliZlk5JVDEB0UFvGxAQwNiaA2OC2GoEnInKeHvkmgW8T0ln+8DBC2535+lONxcq2IwUnugt3phUC4OvuytAIX4ZF+DEk3A/vNuryPplGjEpzooBQxM4UEJ7FuyPAtMJdq+xdSd04tBY+Gg/jXoRL7rZ3NSIiIiIiUs+2HDrG1LfW8tJV3bmmT3t7l9OoHcorZUlSFj8mZrH5UD5WE0K8Wp0IC/t29sbZUesWioicze6MIibMWM0dQ7rw5wnR5/XcnOJKVu+xhYU/peZwrKwaw4D4UC+GRfgxLNKP+FAvHFt4d6ECQmlOFBCK2JkCwjPI3w8zesKY52DQ/faupu58NAHyD8Aft4OTq72rERERERGRevTEnJ3M25bGpqdG4+6qVV7OVV5JJcuSs/kxMYvVe3KorLHi2cqZUVH+jI0NYGiEH61d9PEUETnVrR9tZOuhY/z02IiLWt/VYjXZmVbIqpQcVqZmk3CkAKsJXq2dGRLux7TBnYmvHV/a0igglOZE76ZEpHFKnGv7PXayfeuoa0Mehk+nQMIX0PtWe1cjIiIiIiL1pKLawoKEdMbHBSocPE8+7q5c06c91/RpT1lVDT+l5vJjUibLk7OZsy0NFycHhoT5MjY2gFHRAfi66+ZLEZG1e3NZmZLDnydEXVQ4CODoYNCjvRc92nvxx9HhHCut4ue9uaxKtXUYXtMntI6qFhF70jtUEWmcds2B9v3Bq5mN4ek6EoJ7wc//hB43gaO+DIuIiIiINEeLEzMprqzhqt66iHoxWrs4MS4ukHFxgdRYrGw6eIwfkzL5MTGLZcnZGMZOendox9jYAMbGBNLJt429SxYRaXBWq8nfFyUT4tWKWwZ0qvPjt2vjwuXxwVweH4zVqomEIs2FrkyLSOOTkwJZu2xr9TU3hgFDH4Evb4BdsyH+WntXdG5KcqDwiL2rsK9WXuDdxd5ViIiIiEgTMWvLUUK8WnFJFx97l9JsODk6MKCrDwO6+vD0xBiSMopOrFv4t++T+dv3yUQEuNeuWxhI91BPDKNlr5UlIi3Dgp0Z7Ewr5LVr4nFzdqzXczm08DUIRZoTBYQi0vjsmgMYEHulvSupHxHjwT8WVr8K3a4GBwd7V3R2Viu8PxIKDtu7EvsyHOC+LQoJRUREROpSdYXt5kCziXQjtA0Cz9/uCMwoLOfnvbncNyJMF1LriWEYxAZ7EhvsyQOjIziSX8aSpCyWJGXx9qr9/GfFPkK8WjE+LpAJ3YPo2d5LYaGINEuVNRZeXpxMdFBbruwRYu9yRKQJUUAoIo2LaULiHOg0GDwC7V1N/XBwgCEPwezfQ/J3EHOFvSs6u6ObbOHg0EchpIWuwVxVYvt8Jc61rSMpIiIi0oiZponVtK0f1OgtexbW/8feVZw7N094OAWcW511tzlb0zBNmKrxog2mvXdrbh/cmdsHd+ZYaRXLkrNZtDOD/647yPs/HyDY043x3YKY0M0WFiq4FZHm4rP1hzmSX85/b++mr20icl4UEIpI45K1C3JT4ZJ77F1J/YqdDMufh43vNf6AMGkeOLrCwPvBra29q7Gf9W9B4rw6DQgzCstxMAwC2rrV2TFFREREXv0xlXd/2k//Lt4Mi/BjeKQ/Xf3aNL7uKavFNna/81AY+Ed7V/PbclNh8ROwZwnETDrjbqZpMnvLUfp18qajj9bDs4d2bVy4qncoV/UOpbC8mqVJWXy/M4OZ6w7xwc8HCPJ0Y3xcEJd1V1goIk1bUUU1byzfw+AwX4aG+9q7HBFpYhQQikjjsmsOGI4Q3chDs4vl4Ajdr4VVL0JxZuPtlrRaIWk+hI1u2eEg2ELdH5+E/P11Mma0qsbK5W+sIbekkm4hnoyM8md0dACxwW11gUJEREQuWHJmEW+t2kdciCcZhRU8v3A3zy/cTXvvVgyP8Gd4pB8DuvrQ2qURXA44vA5KMmHc3yB8tL2r+W1dhtuWCUicc9aAcOvhAvbnlnL3sK4NV5uckWcrZ6b2DmVq71CKKv4XFn66/hAfrjk5LAykZ/t2ei8uIk3K2yv3caysmj+Nj2p8NwKJSKPXCH4iEBGpZZq2O4i7DIc2Pvaupv7FTYFV/7AFcP3vsnc1p5e2GYrSYPQz9q7E/mKusAWEifNsI2Iv0vLkLHJLKrmub3v2ZJcwY/keXl+2B38PV0ZF+zMyKoDBYb60cqnfxcVFRESk+bBaTZ6cu4u2bk58fGtf2rVx4Uh+GStTc1iZnM2sLUeZuf4QLk4O9O/szYhIW2DY2ddO3YWJc8GpFYRf2vDnvhCOTrZgMOFLqCoFl9N3B87acpRWzo5M6B7UwAXKb2nr5syUXqFM6WULC5ftzmLhjsxfhIXj4gKZ2D1IYaGINHoZheV88PMBruwRTFyIp73LEZEmSAGhiDQeaVuh4BAMe8zelTQMv0gIiLN1TTbWgDBxnm28aMQ4e1dif17tbWswJs2rk4Bw1pajBLR15YXJ3XB0MMgvrWJFcjbLk7P5LiGDLzYewdXJgYFdfRgVHcCoaH+CPM++1o2IiIi0bF9vPsKWQ8d46arutGvjAtjWZbv5ko7cfElHKqotbDqYz4rkHFamZvPsgiSeXQAdvFszItI2ivSSLj4Nc4OSpcZ2o1zEWHB1r//z1ZXYybD5Q0hdbLvh7xQV1RYWJKQzPi4Qd1ddcmnM2ro5M7lnKJN7hlJcUc2y3dks3JnBZxsO89GagwS2dWN8t0Au6xZErw4KC0Wk8fnnklRMEx4eG2nvUkSkidK7VRFpPBLngIMzRE20dyUNJ3YyLH8OCo+CZ6i9q/klq9UWhoWN0njR42KvhB+fuugxoznFlaxIyeGOIV1wrL3Q4N3G5cToo6oaK5sO5rN0dxbLdmezImUXT82DmKC2jI72Z2R0AN1DPHWRQkQalMVq4mCg0UUijVReSSX/+CGZfp28uarX6d9Xujk7MiTcjyHhfjxNDIfzyliZms3KlBy+2nyE/647hKuTA5d08WF4pB8jIv3p5FtPa+gdWgOlORD765CtUes4CNr427ofTxMQLk7MpLiyhqt6N7L39nJWHm7OXNkzhCt7hlBcUc3y5GwW7vhlWDguLpDLugfRW2GhiDQCKZnFzNpylNsGdaa9d2t7lyMiTZQCQhFpHKxWWydd+Bho5WXvahpO3BRbQJg4FwbeZ+9qfun4eNFR/2fvShqPmCtsAeFFjhmdvz0Ni9Xkqt4hp33cxcmBQWG+DArz5emJMezLKWHp7myW787m3yv2MmP5XnzdXRkZ5ceo6ACGhPs2jnWEpHkrOAxObuDub+9KxA6O5Jcx9a21tHF1YlJ8MFf0CKaLXxPq+BFpAf6+KJmSihqenxx3zuFFB5/W3DKgE7cM6ERFtYUNB/JZmZLNqpQc/vpdEn/9LolOPq0ZXjuK9JIuPrg511F3YeJccG4D4WPr5ngNxcHR9p5w20yoLPlV9+OsLUcJ8WrFJV1awJIJzZSHmzNX9Ajhih6/DAs/33iYj9cexNfdhUu6+HBJFx8GdPWhi71G9IpIi/biD8m0cXXi3hFh9i5FRJowXU0UkcbhyAYoTofYZ+1dScPy7gLBPW1rLza2gDBxHji6QKTGi57g1QFCel/UmFHTNPlm81F6tPcizN/jN/c3DIMwfw/C/D24e1hXjpVWsSo1h6W7s1i0K5OvNx/FxcmBAV18TnQXhnhpFKnUsbJ8eHMgVJVA+34QdZmt29unq70rkwZQUlnDHZ9sprzaQlc/9xNrpnYP9WRSfDCXxwcT0NbN3mXKSSqqLezJKiEmuO2JTnVp3jbsz2PWlqPcPawrEQG//f7idNycHRkW4cewCD+4HA7llbIyJYcVKdl8URuMuDk7MCTcj3GxgYyK9sertcuFFWypgd3f2t5nujTBroe4KbDpPUj9AbpddWJzRmE5P+/N5b4RYeowayZODgtLKmtYtjuLlSk5rNuXx4IdGQAEtHW1hYW1gWEH79YKDBuJrKIKvNu44OzoYO9SROrU+v15LE/O5vFxUSdGiouIXAgFhCLSOOyaDU6tIHK8vStpeHFT62RsZZ2yWm1rwnQdBW5a6PoXYq6EJX+B/APg3fm8n56YXkRKVjHPXxl3Qadv18blxPijaottFOny3dksS87mL/MT+cv8RGKC2jKlVwhTeoXirR8WpC6s+48tHBx4L+xfBUuetv3yi4LICbawMLgnOOjiS3NjtZo88OV2UrOK+fi2fgyN8COzsIIFO9KZtz2N5xfu5oXvdzOwqw9XxIcwrlsgbd2c7V12i2a1mvzhs60sS87G192VMTEBjIsLZEAXH1yc9H+0OaqqsfLUvF2EeLXi/lF110XQ0acNvxvYht8NtHUXrt+fx4rkbH5MymJJUhZODgYDuvowNjaQS2MC8D+fGwUO/gRlebZx+01R+0vAI8jWBXlSQDhnaxqmCVM1XrRZcnd1OhEWmqbJwbwy1u3LY/3+PNbuy2P+9nQAgj3dbB2GXW2hoUb/2cfixEzu+XQLnq2cbeNhuwVzSRdvnBQWShNnmiZ//343QZ5u3Daok73LEZEmzjBN0941iLRoffr0MTdv3mzvMuzLUgOvRUHHgXDNJ/aupuEVHoV/xsLIv8DQR+xdjc2RTfDBaJj8DsRfZ+9qGpdjh+D17jD6GRj84Hk//ZlvE/l842E2PTkaz1Z1exF9f04Jy3Zns2BnBglHCnB2NBgTE8A1fdozJNxPXSRyYcqPwb+6Q9cR//saXXAYUhZB8gI4uAZMi+1CaeQEW3dhpyHgpHC6OXjph2TeXLmP/7s8htsG/fqmiL3ZJXy7PY35CekcyivDxcmBkZH+XNkzmOGR/nU3ilDO2dur9vGPRcncMqAjeSVVrEjJpqzKgoebE6OjA7g0NpBhEX60ctHnprl4c+VeXvohhfdv6cPomIB6P59pmuw4WsgPiZn8sCuTA7mlGAb06tCOcbGBXBobSAef3whE5t9rC9ce3QfOTbQDedGfYPOH8OhecGuLaZqMenUVvu6ufH33AHtXJw3MNE325ZSybn8e62tDw7zSKgBC27U60V14SRcfgjXto95tPXyM699dT0SAB1382rA0KYvSKgvebVwYFxfIxO5B9O/so5+PpElasCOdez/fxstXdefqPu3tXU6LZBjGFtM0+9i7DpG6oIBQxM4UEAL7V8InV9guPMdcYe9q7OODS23dOfessXclNoufhI3v1l7wUAfhr7w7Akwr3LXqvJ5WWWOh/9+WMSTcjzeu71lPxdmkZBbz9eYjzN2WRn5pFcGeblzVO5Sr+7TXXcxyflb+A1b+He7+GQK7/frxsnzY8yMkL4S9S6G6DFzb2taUjboMwsaAW9uGr1su2txtR3nwqwSu79eBv02OO+u4NNM0SThayLxtaSzYkU5uSRUebk6Mjwvkih4hXNJFF+EawqaD+Vz37noujQ3gPzf0wjAMKqotrN6Ty+LETJYkZVFYXo2bswPDI/wZFxfIiCj/Or9hRRrOkfwyxvxzFUPD/Xj3loa/TmWaJnuyS/hhly0sTMooAiAmqC3j4gIZFxdIuL/7L79+WKrh5TDb2oNT32vwmuvM4Q3w4ViY/C7EX8uWQ8eY+tZaXpranWv66oJtS2eaJqlZJazbl8u6/XlsOJBPQVk1AB19Wp8IDAd08Tm/7lv5TQdzS5ny1lrcXZ2YM30gvu6uVFRbWJmSw4Id6SzbnU15tQVfdxfGxwVxWfcg+nby1vsUaRKqaqyM+ecqWjk7svD+Ifp3aycKCKU5UUAoYmcKCIFv77eNGH10Lzi30LspN7wDix6DP2wEv0j71mKa8K9uEBAHN3xp31oaqzUzbGNG799+XmNGF+3M4J7PtvLf2/vZ1vdpAFU1VpbuzuKrTUf4aU8OpgmDwny4pk97Lo0NVHePnF1Foe3rQachcN1nv71/dbltBGnyAluHYVkuODhDl2G2sDByAngE1n/dctG2Hj7Gde+up2d7L2b+vv95jaassVhPjFpbnJhJSWUN/h6uXB4fzBU9gukW4qm1mepBXkklE2asppWzI9/eN/i0o16rLVY2Hsjnh12ZLE7MJLu4EmdHg4FdfRkXF8iYmAB83V3tUL1cCNM0mfbfzazbn8eSh4Y1ijWID+eVsTgxkx8SM9ly6BgAXXzbcGlcIONiA+ke6omxdxl8NhWu/7JpLy9gtdq+RwbGwQ1f8cScnczblsamp0bj7qrVXOSXrFaT5Mxi1u3PY92+PDYcyKO4ogaALn5tGNDFh3FxgQzs6qsL/hchr6SSqW+tpbC8mjnTB9HZt82v9imvsrAiJZuFOzJYlpxFRbUVPw9XJsQFcln3YPp0bKc1RKXR+njNAZ75LomPbuvLiEh/e5fTYikglOZEAaGInbX4gNBSDa+E2zpMmvIdxBerOMs2ZnXoYzDiCfvWcnQzvD9K40XP5sSY0b/C4AfO+Wm//3gTu9ILWfunUXb5wT+9oJxZW47y9eYjHD1WTls3J67sGcI1fdoTF6JOUTmNn16G5c/DnasguMf5PddqgaObbGFh8kLbOqsAIX1sYWHURPCLqPOS5eKlF5Qz6d9raO3iyPw/DKLdRaxlWlFtYdnubOZvT2NlSg5VFitdfNswqUcwV/QIOe2FOzl/FqvJrR9tZMOBfOZOH0hs8G9/TbdaTbYdKbCFObsyOZxfhoMBfTp528ZExgU2isBJzmxxYiZ3zdzCnydEcefQrvYu51eyiypYnJTFj4mZrN2Xh8VqEuzpxn/avE+34p8wHt2Ho0sT75xa/CRseIeKB1Pp+8omxsQE8Nq1PexdlTQBFqtJUnoR6/bnsm5fHhsP5FNaZSGgrStX9gxhSs9QIgM97F1mk1JeZeGG99eTlF7E53dcQu+O7X7zOWVVNSxPtoWFy5OzqayxEtDWlQndgpjYPYie7RUWSuNRXFHNsJdXEhngwed39NcNd3akgFCaEwWEInbW4gPCPUvgs6ua/h3EdeHjiVCcCfduAnu+0dN40XPz7gjAhDtXntPu2cUVDPj7cu4c2oXHx0XVa2m/xWo1Wbc/j682HeGHxEyqaqzEBrfl2r7tuSI+BM/WGjUnQGWxrTMitB/c+PXFHcs0ISflf2Fh+lbbdp8wW1dhxDho3w8c9W/P3sqqarj67XUcyitjzvSBRATU3cXJwrJqFu3KYP72dNYfyMM0IT7Uk8k9Q7iqT3t13FyEGcv28NqSVP42uRs39O9w3s83TZPdGcUsTrR1FiZnFgPQPdSTS2vXlAvzd6/rsutXVRk4uYHDuXe/NiWllTWMfm0Vnq2c+e6+wTg7Nu7XWVBWxbLd2SzZdYSX9k9hibU3f3P5I2NiArg0LpCBXX1wdWqCUw1qb6zb2utvTFnbic+n9WdgmK+9q5Im6PgNNXO3HWVlSg41VpPY4LZM7hnCFT1C8PNQd/fZWKwm93y6hSW7s3jrxl6Miws672OUVNawbHcWC3dksDI1h6oaK0GebkzoZhtD2rO9lwIZsatXf0zhjeV7+fbeQXQP9bJ3OS2aAkJpThQQithZiw8I594NKd/DI3vAqYX/0LP5I1jwANy1GoK626eGE+NFY+GGr+xTQ1Ox5nVY8vQ5jxl976f9vPD9bpY+NKxRXWQtLKtm3vY0vtp0hKSMIlydHBgXF8i1fdpzSRcf3THbkv38L1j6fzBtGYTW8c8+Rem2r/3JC+HAarBWg6snhI20rUkVNgbcG2YMr/yP1Wryh8+38kNiJh/+ri8joupvbFFGYTkLEjKYtz2NxPQiPNycuKF/B24b2JlAzybeUdTA1u7N5cYPNnBFfDD/vLZHnVy8PJBbeqKzcPuRAgDC/N0ZFxvIZd2DiAr0aNwXSUtz4d99wLk1xE2BuKsgKN6+N2DVsRcWJvHe6gPMunsAfTp527ucc5fyA3xxLZsGvsPMvEiWJ2dTUlnTdNcsNU34V3cSKgOYzp9Z/dgIvXeSi5ZXUsl3CenM2ZbGjqOFODoYDA33ZXKvUMbGBGiJgFOYpslfv0vi47UHeXpiDLcPPvclIM6kuKKaZbuzWbAjg59SbRMQQrxacVn3IC7rFmQbl9yMvqdI45dVVMGwl1cwJiaQN67vae9yWjwFhNKcKCAUsbMWHRBWV9jGi0ZPgiv/Y+9q7K80z/bxGHQ/jH7GPjUcHy965dvQ43r71NBUHDsIr8ef05hR0zQZ96/VtHZ1ZO70QQ1S3oXYlVbI15uPMG9bGkUVNbT3bsU1vdtzVZ9Qgjw1Zq5FqSqFf3W3XVC/eU79nquiCA6sgtTFtq7ykkzb9uBetrAwYiwE9Wy2XUCNyWtLUpmxbA9PTojmjqFdGuy8248U8N7q/SzamYGDYTCpRzB3DOlCdFDbBquhqcouqmDCjJ/xbOXEt/cOpk09dGFmFJbzY2IWP+zKZMOBPKwmhPu7Myk+mEk9guno0wjHxP74FKz7D3QdCftXgrXG1rHc7WpbWOgbZu8KL8rujCImvvEzV/cO5R9T7XRT2YWacxek/lB7c6ALlTUW1u7NY8GOjCa7ZmnJd0/guvkd3uv3A9Mv62fvcqSZ2ZtdzJytaczblkZ6YQUerk6M7xbIlF6h9OvkrUCa/92I+fvBnfnLxJg6P35RRTVLk7JYsCOD1XtyqLbYxiUPi/RnWIQfg8J88DjNur9if0fyy/jn/DV08zEYGxtAiFdre5d0dg5O4NXhtDc0PTFnB7O2HGXZQ8Pp4NPIX0cLoIBQmhMFhCK/wTCMg0AxYAFqTNPsYxiGN/AV0Ak4CFxjmuax2v2fAH5fu//9pmkuPtvxW3RAuHsBfHUj3DQHwkbZu5rG4dOpkJsKf9xhn7vca9dR4dG90Mqr4c/f1Lw73Pb7b4wZ3Xm0kMv//TMvTI7jxv4d672si1VRbWFxYiZfbTrC2n15OBgwNMKPK3oEMyLSH6/WF74emTQRa/8NPz4Jt/8IHfo33HlNEzJ3wJ4fIfVH2xqGmNDaF8LH2ALDriP19akefJeQzn1fbOPq3qG8dFV3u1yQP5JfxodrDvDVpiOUVVkYEu7LHUO6MCTct9EHBPZQY7Fy4/sbSDhawLf3Dq7TcbBnkldSyfe7MvluezobD+YDEN/ei0nxwVzePQj/to2g+7M4y3YDT8wVMOUdKMuH3d/Czllw8GfAtN380O1qiJ0CniH2rvi8WK0mV729loN5ZSx7aNhFrRHa4I7fHBgzCa749c2BTXXN0q+//Y5rtt5E3shX8Bl6h73LkWbKajVZfyCPOVvTWLQzg9IqCyFerZjcM4TJvULo6td4JpQ0pAU70rn3821M6BbIv6/vVe+BaWFZNT8mZbJ0dxZr9uZRUlmDk4NB747tGBbpx/AIf6KDGnmXfQuxJ6uYr959gSdq3sbRaELXvie/C/HX/mLTnqxiLv3XT/xuYCf+7/JYOxUmJ1NAKM2JAkKR31AbEPYxTTP3pG0vAfmmaf7DMIw/Ae1M03zcMIwY4AugHxAMLAUiTNO0nOn4LTognHW77a7uh1PBUesOAbDtM5g/HaYth9DeDXvu2hFJ+Edf/HpjLcXxEYx/TIB2nc642//N38WXm46w8cnReLZqWneXHs4r45stR5i15SgZhRU4Ohj06+TN6JgAxsYE0N5bdy82O9XltovrfpHwu+/sW0tpHuxbZgsM9y6F8mNgOEKHS2oDw0ttX7N0Eeai7DhawNVvr6N7qCefTutv93XACsuq+WzjIT5ec5Ds4kqiAj24Y0gXLo8PxsVJnaTHvbI4hX+v2MsrV8dzVe/QBj9/WkE5CxLS+TYhncT0IgwDBnTxYVJ8MOPjguy3nu2ix2Hje7Y1nX26/vKxonRInGsLC9O3AgZ0HAjdroKYK6F14x/V+eXGw/xpzk5evqo7V/dpb+9yzk/yQvjyBrhpNoSNPuuuZ1qzdFKPEC6PD8LfoxGE0dimRIx6ZSWfV9xDYKdouHmuvUuSFqC8ysKPSZnM3prGz3tysJq2mzWm9gphYvdgvJvSjQMXYeOBfG56f8OJ9y8NPXq12mJly6FjrErNYVVKDkkZRQD4e7gyNMKP4ZF+DA7z1c2VdrDt8DGWffQ0j5ifUBo6jKq4a9hy6BibDuaTWViBk4MDcSFt6dPJm+hAD5wayzq+q18FwwHuWfuLn2+m/XczG/bnseqxES3m/3djp4BQmhMFhCK/4QwBYQow3DTNDMMwgoCVpmlG1nYPYprm32v3Www8Y5rmujMdv8UGhFWl8HIYxF8HE/9p72oaj/IC253Vfe+AcX9r2HMf3QLvj4Qr34IeNzTsuZuq42NGxzwLg/542l0qayz0/9syhob7MaMJrxVgtZrsSCtkSVImS5KySM0qASAq0IMxMQGMjg6gW4inxhw1BxvegUWPwa0LodNge1fzP1aLbQzynsW2wDBzp21721BbWBhxKXQeCi7102Gyek8ObyzbS7/O3oyI8qNH+3ZNZ42ss8gqqmDSv3/GycGB+fcOwte98awHXFlj4dvt6by3ej+pWSUEtHXltkGdub5fhyZ3s0VdW5mSza0fbeKaPqG8dFW8vcthb3YJ3yak811COgdyS3F2NBgW4c+kHsGMjvantUsD3QhWmAYzekD3a07bofYLeftg12zY+Y1teoODk61DudvVEDkBXBtfN05eSSUjX11FZKAHX915SdPrUJk9DfYug0dSwfHc/w+fumapgwEDu/pyRY9gLo0LpK0dR/ttOXSMqW+tZVHscqL3f2R7bW187VaPtDzZRRXM357O7K1HSc4sxtnRYHikP1N7hTAiyt/uN/3Ul73ZJUx9ay0+7i7Mvntgo+imzi6qsIWFqTms3pNLYXk1Dgb0aO/F8NpxpPp5qf79nJrDjs8eZ7oxm7KwibS+7iNwsv37ME2TXWlFzNl2lG+3p5NXWoV3Gxcu7x7ElF6h9l9bcvsXMO/uX9xIs/FAPte8s45HL43kDyOa9oj05kQBoTQnCghFfoNhGAeAY4AJvGOa5ruGYRSYpul10j7HTNNsZxjGv4H1pml+Wrv9A2CRaZqzznT8FhsQ7poDs25rfBegG4Mvrof07fBgYsOuufXjU7D+bY0XPV/vDLPd5XfnitM+/P3ODKZ/tpVPbu/H0Ai/Bi6u/hzKK2VJUhZLkrLYdDAfqwkBbV0ZHR3A6JgABnb1abYXJJq16grbxXXvLnDb9/au5uyK0m1rFu75EfatgOpScHS1fU8JHwtxU8Ddv05OVVFtYfRrqygsq6a0qgarCZ6tnBkS7suISH+GRvjh59F4grVzVVFt4dp31rEnu4TZ9wxstGv+mabJqtQc3lu9nzV782jj4sh1/Tpw26BOhLZreV3M6QXlXDZjNQFt3Zg7fRCtXBrP19rjF97mb0/jux3pZBVV0trFkTExAUyKD2ZIuF/9doEueBC2zoT7tkC7cxzpbZq2Gw52zYKds6HoKDi1gshxtrAwbDQ4NY7/3w9/ncD87Wl8/8chDTJStk5Vl9tuDoybCpNmXPBh9mYX8+32dOYnpHMorwwXJwdGRflzRY8Qhkf6NXgH0RNzdjJvWxpb7gig9YfDYeK/oM9tDVqDyHFJ6UXM3XaUedvTySmuxLOVMxO6BTI2JpABXX0a/P9HfckurmDKm2upqLYwd/qgRjnRpMZiJeFoYW13YTY70goxTfBu48LQcF+GRfoxJNyvUd2Y1Rws2pFGzjcPcYvjD5THXk+rqf8Bh9P/u6+2WFm9J4fZW9NYkpRFVY2Vrn5tmNIrlCt7hhDi1aqBqwdqquD17rZJLrfMxzRNpry1loyCClY8MrxRvedr6RQQSnOigFDkNxiGEWyaZrphGP7AEuA+4NszBIT/AdadEhB+b5rm7FOOeSdwJ0CHDh16Hzp0qIFeTSNSmAZJ86D/3Wd8w9Zi7ZwFs38Pty2yjb1qCBoveuFOjBndcdqLkbd/vImk9CLW/Glks+g2Op1jpVUsT85m6e4sVqXmUFZloY2LI8Mi/RgdHcDIKK1b2GRseh8WPgy3zIcuw+1dzbmrqYRDa2sDw8WQtxec3KD3rTDw/oteY+w/K/by8uIUPp/Wn9hgT1bvzWFFsu0O8dySSgC6h3oyPMKPYZH+9Gjv1ej/v5umyf1fbmfBjnTevbkPY2IC7F3SOdmVVsj7q/fz3Y4MAC7rFsQdQ7rQLdTTzpU1jGqLleveXU9yRhHf3je4Ua85ZbGabDyQz7cJ6SzalUFBWTVerZ0ZHxfEpPhg+nf2rtsuimOH4I3e0OvmC59OYbXCkQ22sDBxLpTlgZsnRF9uCws7DbHb+9b1+/O47t313DO8K4+Pi7JLDRcl6Vv4+ma4eR50HXHRhzNNk+1HCpi/PZ0FO9LJLanCw82J8XGBXNkjhP5dfOr963BFtYW+zy9lTEwAr10Tb/v35xkKv/u2Xs8r8ltqLFbW7MtjztajLE3KorTKQmsXR4aG+zEmxvbevDF03F2I0soarnt3PXuzS/jqrkvoHupl75LOSV5JJT/vzWVlSg4/peaQV1qFYUC3EE+GRfgxLMKPHu29Gs+oyyboq/X7cVz4R65y/InKPnfjetk/znkZgsLyahbtzGDO1rQTayxf0sWbKb1CGR8XiEdDdqr//E9Y+gzctZpFuX7c89lWXpzajWv7dmi4GuQ3KSCU5kQBoch5MAzjGaAEuAONGJX6Ulliu8O6501w2SsNc860LfCexotekBNjRp+DQff/4qHsogoG/GM5dw3twmNN8WLeBaiotrBuXx4/JmWxbHcW2cWVODoY9O3UjjExgYyJDqCDT+O7y1ew3bH6Ri/wCILf/9i01/XLSYE1M2DHl4ABPW+EwQ+eda3QM8ksrGDkqysZGu7H2zf/cm1Yq9UkMb2IlSnZrEzNYdvhY1hN8GrtzNBw29ozwyL88GmEd4e/sWwPry5J5bFxkUwf3vTGFaUXlPPRmgN8sfEIJZU1DOjiw51DuzAswq9Zj+762/e7efen/bxxfU8ujw+2dznnrKrGdpf+twnpLEnKoqzKQkBbVy7vHsykHsF0C6mDkV7z74UdX8P92y76pgAALNWwf5UtLNz9HVSVgHsAdBlxXuMxz4nhYLtpLiDmtA9X1ViZMGM1FdUWljw4rGl2EHxzKxxYDQ+n1Pna4zUWK2v35TF/ezqLEzMpqazB38OVy+ODmdwzhNjgtvUyMm7+9jT++OV2Pp/Wn4FhvrD8edv6UQ+n1FkHu8jFqqyxvTdfkpTF0t1ZZBVV4mBAn07ejI0JYExMAB196mc8e12rsVi545PNtqkCt/RhVHTTuLnpVCe/f1yVmsPW2vePbd2cGB0dwIRuQQyJ8NU0lvPw7orddFh+H+McN1E15E+4jPzTBf8scyS/jLnb0piz9SgH88pwc3ZgbEwgU3qFMDjMt/5D3PJj8Fos1qiJjDpwA86OBt/fP0ThcSOjgFCaEwWEImdhGEYbwME0zeLaPy8BngVGAXmmaf7DMIw/Ad6maT5mGEYs8DnQDwgGlgHhpmlaznQOBYRyWl//Dg6tgYeS6/wiymn9+BdY/5bGi16od4bZOgruWP6Lze/+tI+/fZ/MsoeHNeouj/pypnULIwNq1y2MCaC71uFoPLb8F767H26cDeGj7V1N3Th2CNb8C7Z9alvDsPu1MORh8D33QOyhr7azYGcGSx8c9pvhdkFZFT/tybVd8En5393h3UM8GR7pz/BIP7qH2r+78IddGdz96VYm9wzhtWvim946Zicpqqjmq41H+HDNATIKKwjzd+eOIZ25okdIsxmldtySpCzu+GQzN13Sgeev7Gbvci5YWVUNS3dn8+32dFalZlNtMeno05oRkf6MjPKnX2fv8//c5e2Df/eFfnfA+BfrvujqckhdbFuvMH2bbfJCXSrLg9A+ttH7p/n/+ObKvbz0Qwof/K6JXhBvwLXHK6otLNudzfztaaxMyaHKYiUiwN02Mq5HCIGebnV2rps/2MD+nFJWPzbC9l4mKxHeGgiXvQp9p9XZeUTqitVqsjOtkKW7bcsEJGcWAxAR4M7oaFtYGB/q1Sjfm5umyZ/n7uKLjYd5/so4brrkHMdINwGFZdX8vDeX5cnZLEnKpKiiBg9XJ0bHBHCZwsKzMk2TVxduo9+G+xnquJOasX/HaeD0Ojv21sMFzN12lO8SMigsr8bX3ZUrewQzpVcoMcH1OJp/0Z+wbnyPgeX/5IXfXdo0v/c3cwoIpTlRQChyFoZhdAHm1v7VCfjcNM0XDMPwAb4GOgCHgatN08yvfc6TwO1ADfCAaZqLznYOBYRyWsfHMDXEmL8T40Wj4MZv6vdczdXxMSAnjRk1TZNL//UT7q5OzJk+yL71NRKnW7fQ38OVIeF+tnU4wnyb7LijJs9SbRuN1trHFnQ34cDotIrSYe0bsPkjsFRC7GQY8sgZu3WO23r4GFPeXMsfRnTl0UvPrwvYajXZlV7IiuQcVqZms/1IAaYJ7Vo7MyzCj+G1axd6N/C/+cT0Qq56ax1RQR58ccclzSZEq7ZYWbgjg3d/2k9SRtGJCzhjYgLo3bFdk7/r+kh+GZfNWE0Hn9bMuntgs/m8FZZVs2hXBj8kZrJuXx6VNVZaOTsyKMyXEVF+jIj0J/hc1gCacxckzYc/JoBHE7yItvE9+P6R047fPJJfxph/rmJYhB/v3NxEr0MlzrV1EP5uAXQe0mCnLSirYmHtyLgth45hGDCoqy9TeoVwaWwgbVwv/Ca8jMJyBv5jOfeNCOOhsZG2jaYJ/+kPbfzgtoV19CpE6s/hvLITYeHGg/lYrCZ+Hq6MjvZnTEwAA7v6NprvN8fHvTfZMcvnqKrGypp9uXy/I4Mfk7IoLK8+ERZO6BbEkPDG8zmxN4vV5LlZa5m46wF6OeyFSTNw6HVzvZyrssbCiuRs5mxNY0WK7eamLn5tGBrux6AwX/p38aZtHY4hLc3aj9tbvVnQZiqTHnmvSd/M11wpIJTmRAGhiJ0pIJTTqi633WkdNwUmvVG/5zo+XvSKN21j+OT85R+AGT1+MWZ0x9ECJv17DX+f0o3r+2m9gFMdK61iRUo2y5Kz+XlPLoXl1Sc6rYZF+DFU63A0rG2fwfzpcP1XEDnO3tXUn5JsWPdv2PSBbVxg1EQY+ggE9/zVrlaryeQ315BRWMGKR4Zf1IVkgPzSKlbvyWFlim3twvza7sL4UC8ujQ3k8vggQtvV7/jd7OIKrvz3Gkxg/r2D8Peou06axsI0Tdbuy+PDnw/w054cqi0m7Vo7MzIqgDEx/gwJ97voz2VDq6yxcM3b69ifW8rC+4Y02zHN5VUW1u3PZUVyDsuTs0krKAcgKtCDEVH+jIj0p1eH03xfyEmFN/vDJdPh0hfsUHkdqKmEGb2gbRD8fsmJmzRM0+T3/93M+v15LH1o2LmFpY3RVzfb1nZ8aLfd1nA8mFtqGxm37ShH8stp7eLIuNhApvQKZUDX81+v8HhYserR4b8cz7ji77DqRXg4GTwC6/hViNSfgjLbe/OlSdmsTMn+xbqFo2vXLWzom5qOm7vtKA9+lcAVPYL55zU9GmWHY32oqrGydl8u3+/MYHGiLSx0d3VidLQ/E7oFMTTCr8WGhZU1Fp7+bDm/2/cwkY5pOFz1AUbslQ1y7mOlVSzYkc7S3dlsOJBHRbUVRweD+FBPBof5MijMl54d2uHidOE/x762JJWIn+5jvFsijg8ngVs9divKBVFAKM2JAkIRO1NA2DJZrSbTPtlMcUU11/Rpz2Xdg2jtcsoFyzl3wp4f4eFUcKrHH8ZOjBfdA63a1d95mrt3hoKD04kxo3+Zt4uvNx9h01Oj6/RuwubIYjVJOFrAT6k5/JSaw/YjBVhN8HBzYlBXX4ZF2gLDkKZ6YbSxs9TAf/qBSxu466fm1z14OmX5sOFtWP82VBZC+FgY+ii073dil1lbjvLINwm8dk08U3qF1unpLbUjvlamZLM8OZsdRwsB6NOxHZN6BDOhWxC+dbxuYUW1hevfW09yRjHf3D2AuBDPOj1+Y1RcUc1PqbksScpkeXI2RRU1uDg5MDjMlzExAYyK8se/beMPSZ/5NpGP1x7k7Zt6My6uZQQOpmmyN7uEFbX/RzYfPEaN1aStmxNDI/wYGeX/v/U9v7nNNv7zgR3QxtfepV+4zR/Bggfghm8gYiwAP+zK5O5Pt/DkhGjuGNrFvvVdqMoSeLkr9LoFJrxs72owTZPNh44xZ2saC3akU1xRQ0BbV67sGcKUnqFEBnqc0zFGvboKX3dXvr57wC8fzE62BdbjX4L+d9XTqxCpX2dbt3BM7SjSTr4Ns27h2r25/O6jjfTu2I7/3t6vxY7arK5da3XhjnR+TMqioMwWFo6K9ueyFhYWllbW8OTHi7gv7VE6Oh3D6YbPIMw+yyNU1ljYeqiANXtz+XlvLjuO2n6Obe3iSL/O3gwO82VwuC+RAR7n3AWYXVTB8FdW8ruO+Tx+5B649G8w4A/1/ErkfCkglOZEAaGInSkgbJmO3wUZ0NaVrKJK3F2dmNQjmOv6tqdbiKftzWPKD/DFtb+4UFTnTBNe7w6+kXDTrPo5R0ux+jVY9ld4YCcVbULo/7dlDI/04/Xrft2ZJGd3fB2On1Jz+GlPDhmFFQB09WvD0Ag/hkX40b+zD61cWsYPwfVux9cw5w649lOIvtze1TSsikLbaL91/4HyfOg8DIY+SknQJYx4dRUhXq2Yc8/Aer9T/XBeGd/tSGf+9jRSs0pwdDAYFObLpPhgLo0NwOMibzIwTZOHv05gzrY03rqxF+O7BdVR5U1HtcXKpoP5J8YcHz1m607r0d6LMTG2i53h/u6NboTTwh0Z/OHzrdw+qDNPX372kbjNWVFFNT/vyWVFcjYrUnLILanEMGBS4DH+dexecuKn43vFC027q+T4qOdW7eDOlZRWWRj92io8Wznz3X2DcW6qHfU7Z8Hs38Nti6DjQHtX8wvH1yucu+0oK1NyqLGaxAa3ZUqvUCbFB+PncfobNbYcOsbUt9by0tTuXNO3/a93eHMAuHnC7T/U8ysQqX+mabup6fj3z+PrFnb1a8PIKH9GRgXQp1O7evkalZxZxNVvrSPIy41v7h6IZyvddAn/Cwu/35HB4qTMX4SFE7oFMawZh4XHSqt48v25PJX/BH7OlTjfMhs6XGLvsk4oLK9m/f68E4Hh/pxSAHzdXRhU2104KMz3rDe+/nnuTr7edISlDw2j03dXQ8FhuH87ODatCRjNnQJCaU4UEIrYmQLClqe8ysLIV1fi5+HKvOmD2HL4GF9uPMLCnelUVFuJDmrLdX3bc2U3Pzz/Ew2RE2Dy2/VTTNpWeG8EXPEf6HlT/ZyjpcjfDzN6wtjnWeh+FX/4fCszf9+PIeF+9q6sSTveRbIq1TaWccOBfKpqrLg4OdC/szdDa9cvbIwX9psEqwXevAQcnOHun8GhiV6AvliVJbDlI1gzA0qzOeIRz5N543jo7nvo0aFhO6uTM4v4dns63yakc/RYOS5ODoyK8mdSfDAjovwv6ILP26v28Y9FyTw0JoL7R4XXQ9WNUPp2yEk57UMmJhmFFexKK2RXWiGH821hoa+HC3HBnsSFeNLZpzWODfX/wcERIsaBq/svNh/MLWXiGz8T5u/O13cNuKhRVc2J1WqSmF7E8uRs+m28n9jKbQypfB0XDx+GR/gxIsqfweG+F9W9X2OxUm0xqbJYqaqxUn3S7+5uTgR51lNH+/Fxz9d9zgv7OvPe6gPMvmcAvTt618/5GsKXN9rG2T+Y1Ki/x+SVVPJdQjpztqWx42ghjg4GQ8N9mdIrlDExAb/42vvEnJ3M25bGpqdG4366kcWrXoIVL9hGqrYNbsBXIVL/juSXsSQpixUp2azfn0e1xcTjeHd3pD/DI2u7uy9SRmE5U95ci9U0mTN9kCaJnEG1xcq6fXm1Y0gzOVZWTRsXR0ZF29YsHB55bmGhxWpSVWP7XldZY6GyxkrlSX+v+sXfrVhMk76d2tXf98PTyCys4Jl3v+D5kv/Dw80R11vnQVB8g53/QqQXlLNmb25tYJhHbkklAF1825wICwd08cGzte09y76cEsb+8ydu6t+Bv14RB8nfw5fXw9QPoNtV9nwpcgoFhNKcKCAUsTMFhC3Pv5fv4ZUfU/nqzkvo38XnxPaiimq+3Z7OV5uOsDOtEBcnBz7x+YQ+ZT/h+NheDOd6ePO95Glb58wje6B1E7741Fi8PQQcXbjN6e8kZxbz8+Mjz3tNGzm7imoLGw7ksyrF1l24N7sEgMC2bgyN8GVohB+Dw3zxam2fNVKanF2zYdbtcNVHtjVPW7rqcvJXv0/lqtcIMvIhuJdt9Gjk+AYfvWqaJlsPF/BdQjoLdqSTW1KFh6sTY2MDmdQjmEFdfc5pjc4lSVncOXMzE7sHM+O6Hi0jSM/caRv7bFrtXcm5C78Ubvz6xF8rqi1MeXMt6YXlLLx/iC6Mnk5GArwzlLIBj/CD320sT87mp9QciipqcHIw6N2xHb7urqcN+aosJlU1FlsIeGLb/x63/saPyH07teOq3qFM6BZ00R2+v2CpgTf7U2E6E5f5FFf36cDfp3Svu+M3tIoi25rafW6H8f+wdzXnbG92MXO2pjF3WxoZhRV4uDoxoVsQU3qF0C3Uk/4vLGNMTACvXdvj9AfI3QP/7gOX/h0GTG/Q2kUaUkllDT/vyWV5chYrUnLIKbZ1d/do78WoKH9GRPkTE9T2vN97FFdUc/Xb6ziSX8bXdw8gNrj5j0WvC9UWK+v357Fwx//CQg8Xg+7BrSm3Ov8i4Ds1+Kv5rW98p2EY0K+Tt208flwQ7epxjcoDuaW8+O5/eanyOVzbtMX19gXg27RuejNNk9SsEn6uDQzX78+jrMqCgwHdQr0YHOZDwpFCth8pYOWjw23LDVit8J++4OIOd65sGUtBNBEKCKU5UUAoYmcKCFuW7OIKRry8ksHhvrxz85nfS+xKK+TrzUfI3LaId3mep1z/RPCAq7mqV2jdrZdkmvB6PPhGaLxoXakdMzq4cgZXDO/Po5dG2buiZi+toJzVtd2FP+/NpbiiBgcD4tt7MTzCn2GRfnQP8WzaY+fqi9UKbw+ydRFOX2frYhLu/GQzG/dm8tPYDNpufgMKDkFAHAx9BKIn2eXjVGOxsm5/Ht9uT+eHxEyKK2rwdXdhQrcgrugRTK8O7U578S05s4ipb66la20HWnMdN/ULpgkfjoO8PfC778Dp/L5nllZZ2HQwnzV7c1m3L4/iyhqcHR3o3cGLQWF+DOrqg497HV8A2zXb1m10/Ze2MBpbh9IXGw/z4a19GBkVULfnay4+vxYOr4MHdtrGOWL7v7L1cAErUrL5eU8u5dUWnB0dcHFywMXRwMXJAWdHh5O2OeB80naX2u3OJ/9+yvMO55cxe+tR9ueU4ubswLjYQK7q3Z4BXX3q5KYga8LXOMy9g8ccHuLPjzzRtG94SfgK5t4Jt/8IHfrbu5rzZrWarN+fx5xtaSzamUFplQWv1s4UlFXz+bT+DAw7y5qXbw0G51YwbUnDFSxiR1arya70QpYnZ7MiOZuE2vWVgzzdGB7pz6gofwaF+f7mEgHVFiu3f7yJtfvy+OjWvgyN0DSWC1FtsbJ+Xw6+391KWPEmdrr1Zqv7MJLaDsJ09Tzx/c7V6fjvjqf8/X+///oxR6otVpbtzubbhDT25ZTi5GAwNMKPSfHBjIkJoM3puqsvUGJ6IW++/x6vWF/CoW0Qrrd/B14d6uz49lJVY2X7kYITgeH2IwVYrCYPj4ngvpMnfmz+EBY8CLcuhE6D7Vew/IICQmlOFBCK2JkCwpbliTk7+WbzEZY8NIzO57Cwe0VlJcarUWxx6MYNBXfh6GAwMsqf6/q2Z1iE3zl1j5xR+jZ4d7jGi9al2jGjz1XfyI0PvEQXP/fffo7UmRqLlYSjBaxKyWHVHtsi8aYJ3m1cGBLuy7AIP4ZG+NnuxhRI+ha+vhmmvA/dr7Z3NY3Cz3tyuemDDTw2LpLpw8Ns3Ty7ZsFPr9gCpzb+4PLbX7vrkxXbqOqyqhrKqiyYJjg5GrRxcaKNqyPOjg4YQJVnRy7LvJMiqyvf3juYgLq6uaSx2/4FzLsbJv0bet18UYeqtljZfPCYbd2l3ZkcqR1FGhfS1jZGLcqf+FCviw+FLNW2DvTqUvjDRubtyueBr7Zz97Cu/Gm8bjQ5raOb4f1RMPIpW5dvAzNNk21HCpi95SjfJqRTXFFDsKcbU3qFMrV36Dm9xzuTLzccoNfCywhs60rbhzY37Zs3Pr/O1tH7wM5GPV70XJRV1bAkKYvZW9OwWK3MvL3/2W8+Wv0qLHsWHtgFXqdZp1CkmcsurmBlSg7Ld2ezek8OpVUWXJwcGNjVh5FR/oyI9Ke9d+tfPMc0TR75Zgeztx7lpau6c00f/d+5KCv+Dqv+AVETbaPXi46Cowt0HQkxV9puSmrldVGnME2TpAzbePzvEtJJL6zAzdmB0dEBTIoPZlikH65OF/59bNPBfD79+D+8zL8wfcJxvXU+eDTPG6eKK6rZnVFM747tfvnesroc/hkLoX3hhq/sV6D8ggJCaU4UEIrYmQLCliMls5jxr//E7wZ24v8ujz33Jy54CBK+4MBtCXyVkM+sLUfJLakkoK0rV/duzzV92tPBp/VvH+dUS/4P1v1b40XrkGma7H2uF1ZHFyKf3GDvclq8/NIqVu/JsQWGqTnklVYB0C3Ek2ERfgyP9KNHe6+LC9qbKtOEd4bYfuD8w8amfQG6jtRYrEyYsZqKais/Pjj0l912VgskzYPUHxvV2Mpqq5WMggqOFpSTXVSBCXi4OdHBy5WwnCV8bh1DtzveI769l71LbRjlBbaxfu062TqW6jCQOD4WaunuLFamZLPl0DGstTcgHP96MizC78K7vQ6shv9OJL/PgwzeeAlxwZ58fkf/lvn16VzMnGK70emBHeDqYddSKqottcHRUX5KzcFqQu+OthGkl3UPOq+1EHNLKhn16ipu9dzGgwV/gynvQfdr6rH6elReYBsv2v8uuPQFe1fT8E5am5qB99m7Gmmpds2GdW8C9r3uZzVNSistFJVXU1hRTVWN7b2Um7Mjbd2cadvKiTYuTmw3w5hy4HLuHxXJg2Mi7Fpzk5e6GD6/BuJvgCvftL33T9sCiXMhab4tLHRwtoWFsVdC5ISLDgutVpPNh47xbUIa3+/MJL+0irZuToyPC2JSj2Au6XJ+nfYrkrNZ/PlrvODwDpbAnrj8bja0ati1wRuNlf+AlX+HP2wCP/3faAwUEEpzooBQxM4UELYct3y4kYQjBax6dPj5XUA8+DN8fBlc9SHETaXaYmV5cjZfbTrCypRsrCYM7OrDdf06MDYm4NxGyJ0YLxoON82+8Bclv5BwpIDFbz/GY85f6Y7xRsZqNUlML2JVajarUnPYetg2wsXDzelEd+GwCH8CPVtIl1XKIvjiOrjyLehxg72raRT+u/Yg//dtIu/c3JtLYwPtXc55yyup5PtdmXy3PZ2NB/P5P6f/cqvTjxi3/wAdLrF3eQ1j0eOw8V24YwUE96jXUxWUVfHTnlxWJGezMiWbY2XVOBjQq0M7RtR2RkQHeZzXuks1X9+ONelbrnb4J+/88eqW8/XofB1eDx9eCmOehUF/tHc1v5BVVMHcbWnM2nKUvdkluDo5cGlsIFf1DmVQmO9vXhh9+OsE5m9PY9H9gwifO+F/N3E41t2otgaz/XOYdw9MWw6hve1djX28MwwMB7hzhb0rkZYobx+8NQjaBoN3Z3tX8wtlVRbySivJK6misLwa04S2jpX0Ipk5QQ8x+c6nW8aayfUlf79tUpBXB/j9Etu445NZrbawMGmeLSwsPFLnYWG1xcqavbl8uz2dxYmZlFZZ8PNwZWL3ICbFB9OjvddZP8fzt6exfdaL/J/Tf6nqMASXG78E1xY8nac019ZF2P1amDTD3tUICgileVFAKGJnCghbhpUp2dz60SaeuiyaaUO6nN+TrRbbm8GQ3nDdZ794KKOwnFmbj/LV5iMcPVaOZytnruwRzLi4IPp0aofzmToPjo8XrYMRbPI/T83bycYtm/nR8QEY+wIMvNfeJckZFJZVs2ZfLqtScliZmk1WUSUAUYEetrAw0o8+Hb1xcWqG3TumCe+NgPJjcO9mcDz37pbm6lhpFcNfWUlcSFs+/X3/Jn9RKq2gnOzcPHp+Nx6c3eCu1bbfm7PMnfDOUOhzO1z2aoOe2mI1SThawMrkbJanZLMrrQiAwLZujIjyY3ikP4PDfM+6Ho9pmvz182U8knojlSED8LlzXgNV3wR9PBFyUuCPCeByARMUGoBpmuw4Wsis2hGkheXVBLZ1Y3KvEKb2CiXM/9cXOdfty+P699YzfXhXHhsXBckL4csb4Io3oeeNdngVF+mzqyE72dbl2cS/pl6wn/8FS//P9m+1XSd7VyMtidUKH0+ArCT4w3pbSNhIFVVU8/OeXJYlZXHXkUcIr0zCmL4O2nW0d2lNU1UZfDDWFvrdteq3v/ac2ll4IiwcYRtDGjXhorv2KqotJ9YrXJGcQ5XFSgfv1kyKD2ZSj2AiAn45CeCTtQfI+f4FHnb6hurw8Thf83Hzfx97Lr57wHbzzYO7wN3f3tW0eAoIpTlRQChiZwoIm7/jY+sqa6wseXDYhQUOi/5kW5z60T3g5vmrh61Wk7X78vhy02F+TMqiqsZKWzcnRkT5Mzo6gGGRfr8ccaXxonWuotpCvxeWMjLKn38V3AdObjBtqb3LknNgmiYpWcW2sDAlh82H8qm2mLRxcWRAV98TowNPXSelydqzBD67Cia9Ab1usXc1jcLT83fx6fpDLPrjUCID7TuusE7tXQqfTrWt0TbyKXtXU39MEz4cZ1sn8r4tdh8/lV1kW3dpRUo2q/fkUlJZg4ujA/06e9d2F/r9ao3arzcd4bHZO/g4aiPDD/4Lrv/StjaQ/NKBn+C/l8O4f8Al99i7mnNSWWO7MDpry1FWpeZgsZr07ODFVb1Dmdg9GM9WzlTVHB9xbGHJg8No5eL4v5s5yvJt/66b0s0cZfnwSjhcMh3GPmfvauzn2EHbxI7Rf4XBD9i7GmlJ1r0Ji59oepMiCo7AmwMgpCfcPL/Jr13a4EwT5t4FO76GG2dB+Ojzf37aVkicY1urvPCwLSzsMtzWWRh12UW/xyosr2ZxYibfJaSzZm8uVtN2g+akHsFc3j2YOVuO0mrVM9zptBBLt2txvPLNptlFXx9y99hG6Q97HEb82d7VtHgKCKU5UUAoYmcKCJu/zzcc5s9zd/LWjb0Y3y3owg5yZBN8MBqufBt6XH/WXUsra1i9J5elu7NYnpxNfmkVTg4G/bt4Mzo6gNFR/rT/dCB4d4Wb51xYPfIrC3akc+/n2/j09/0ZnPExLH8OHkwEz1B7lybnqaSyhnX78liZks3KlBzSCsoB6OrXhlHRAYyM8qdPx3ZNc20w04QPxkBxlu2Cs9MFrpfWjCRnFjHh9dXcdElHnr0izt7l1L25d8POb+DOVRDYDF8fwPYvYN7djbIrvqrGyuZD+axIzmZFSg57s0sA6OTTmuGR/oyM8sezlTPXvLOO3h3bMfPWXji+OxSqS22jJU8dC9aSHQ+CCw7B/dubZDdBdlEF87bbRpCmZpXg4uTA2JgA3F2d+HLTET68tQ8jowL+94TjN3RM/Bf0uc1udZ+3rTPh23vhzpUQ3NPe1djXeyPBWgN3/WTvSqSlOD5atPNQuOGrptfBu+Vj+O6PtmkAfafZu5qmZeN78P0jMOJJGPbYxR3reFiYNBcS59eGhU62sDDmSltYeJE3GucUV7JwRzrfJqSz9XABDlh5wekDrndagaXPNBwnvKyQ+FRfXG8btf5gYqOdotBSKCCU5kQBoYidKSBs3koqaxj+8go6+7bh67sGXPjYOtOEf3UH/yi48ZtzfprFarL9yDGWJGWzbHcWe7JLiDUOsND1SRZ1eZKA4XfQI9QLh/NYLFxO79aPNpKaWczqx0fieGw/vNELLv0bDPiDvUuTi2CaJvtzS1lV2w20fn8e1RaTtm5ODI/0Z1S0P8Mj/PFs3UQ6O/atgJlXwmWvQd/f27sauzNNkxvf30BiehErHxlOuzbNMDAty4d/97Wtifr7pc3vLuzyAtvd1O06we0/NvoLSUfyy1iRks2K5GzW7sujssYKgJ+HK9/fPwQ/D1c4sBr+O1F3iJ/qeEfshFeg3x32ruaimKbJrrQiZm05wvyEdArKqrk0NoB3bu5z6o62mzqKMuD+reDkap+Cz9fMybY1sO7f3vTCibq29g348Sm4byv4dLV3NdLcNaHRomdkmravIUc2gkaNnrvDG2yf+7DRcN0Xdft+yDQhfSskzrOtW1hwGDDq9HuS1QTTasHRrMYc/AjGqKf0/eN0Dq2Fj8brZ7lGQAGhNCcKCEXsTAFh8/by4mT+s2If8/8wiPj2Xhd3sCVPw7r/XNRY0IO5pRR89yTdDv2XflVvk2d1x9fdlZFRfoyODmBwuC+tXZrZxeMGkFVUwYC/L2P68DAeuTTStvGtwbbOj2lL7Fuc1KmSyhpWp+awLNl2gT+vtApHB4M+HdsxOjqAUdH+vxod2GiYpu0HymOH4I/bm86F5nq0ODGTu2Zu4dkrYrllQCd7l1N/ds2GWbfD2Odh4H32rqZuLXocNr4Ld6yA4B72rua8lFdZWL8/jzV7c7k8PviX7xNmT7ON9/rDevA+z7WLmyPThPdHQUl2bfdz8/n6VVljYeOBfHq098LD7TQ3mxy/sWP8y9D/zgav77yV5tnGiw76I4z+P3tXY38FR+BfcTDyLzD0EXtXI81dUx0teiqNGj0/xVm2dZidW9k6t1t51d+5TBPSt8GeH6G6rO6PH9gdul1V98dtLkzT1pleUQj3bgIHR3tX1GIpIJTmRFeBRUTqSXpBOe+vPsAVPYIvPhwEiJ0Ca16H3d9B799d0CE6+bSGopXQdQTLp05hZWo2S3dns2hXJl9vPoqrkwODwnxPBB0BbZve+C57mLstDasJU3ufNE409krbmNHCoxoz2oy4uzoxvlsQ47sF1XboFrA8OYtlu7N54fvdvPD9bjr7tmFUlD8jo/3p28kb58YyivTgz3B4ne0iczO6uH6hKqotvLBwNxEB7tzQr4O9y6lfsVNgxzew/AXbSKjmEjhl7rSFg31ub3LhIEArF0fbmoRR/r9+cMxzkLLIFoDe8LXuok9dDGlb4PIZze7rl6uTI0PC/c68Q5fh0HEQrH7VNkK3sY+d3f0tmBaInWzvShoHr/YQ2s/WeaOAUOpT3j5Y9iyEXwrxZ1+SotHzag+XPm8bNbrlQ40aPRtLNXxzqy0wuml2/YaDYHs/EtLL9ksanmHYbvabdZvtfWL0RHtXJCLNQCO5YiUi0vy8vDgFE3j0eEfZxQqKt60buGv2hR8jcwccOwCxV+LZ2pkreoTwxvU92fqXMXw+rT839O/Anuxi/jx3J/3/toxJ//6ZGcv2kJheiDrOT880TWZtOUrfTu3o7Nvmfw8cvzCW9K19CpN65+hg0LtjOx69NIofHhjK6sdG8OwVsbT3bs0n6w5xw3sb6P3cEu77YhvztqVRUFZl34JXvQjugdDrFvvW0Uh8uOYAh/PL+L/LY5vmepLnwzBsa/k4ONkutv0/e/cd31S9PnD8803SvRdtoaUtpdDB3siQIYrgXle97u11e9177597XLde9boVJwrKkr1XS2mB7r33SnJ+f5yAgIAUkp6O5/165ZU2yfl+n7Q9TXKe8zzf7vD/XNPgp9vAKwim3Wd0NM7nHwlT7tbP0N8+1+hojKVpsPBxvY1sV66IOVJK6etJ1RfD2veMjubvpX4LIf0hYrDRkXQeg86Aki1Qnml0JKK7stvhu+vA7A4nv9g9TioZcTH0mwrzHoCqbKOj6bzmPwi5y+GUV7rvWtNiX0mnQGBfWPGq0ZEIIbqJbn40RAghjLE5v5pvNxRwxcQ4ooKctHi0UvoBhuw/9BZbRyJ1DigzJO57ppmb2cQx/UN58OQUltw+lXm3TOb2EwZiMSle+C2D2S8v5ZinFnD3N5v5ZWsxdc1tR/98uomNedXsKK3nrJH7VQmGxEP4YH2dBtEjRAd7c9H4WP572Rg2PDCD/1wwkpmDIlixs5ybP9/IiEfnc85/VvDm4p3sKK3r2KR7zgr9f8eEG8FNKoNLapt5dcEOjk8OZ0L/UKPD6RgBfeD4RyBrCWz42Ohojt6mzyBvJcx4RE8Sdkdjr4awJPjlTmhrMjoa42z7QT/B6di7wNxF1nt1ttgJeiXh0hegtcHoaA6uvkx/rUk5vXskKJwl+VT9OvVbY+MQ3deq/zi6RDzVNdcdPBCl9KSXMsF31+tJULGvLV/Bytdg7DUw5GyjoxEdxWyBcf/S9/m8NUZHI4ToBiRBKIQQTqZpGo/9uI1QX3eunRLv3MEHnQmaHdK+O5LA9AMT/Y495BqGSikGhPtx3dT+fPOvCay+5zieOXMIw6ID+XFTEdd8vI7hj8znvLdW8ubinWwv7uBERyfz1bp8PN1MzBoc+dc7U06FvFV6m1HRo/h4WJg5KIJnzhrK6nuO49t/HcN1U/tT12LlybnpHPf8EqY8t4hHfkhj+Y5y2mwuPuix5BnwCYORl7p2ni7i6V/Ssdo07p2dZHQoHWvEJXqrwl/vhbpio6M5ck3VMP9+iBoNQ7txRZnZDWY9C9W5emKoJ7LbYdGTEJIAg3v4wc+p90JDmd5Wt7Pa9p3+PlXai+7Lvzf0HS8JQuEa3am16P52txrN/kNvNSr+VJIG398A0eP0tuSiZxl+AXgGwIpXjI5ECNENSIJQCCGc7NfUElZnV3LzcQPw83Tyme69kqBXMmz9pv3b7m4vmnxauzYL8/PgnNHRvHHBSNY/MIPPrhrHFZP6UdXYypNz0znhxSVMeGoBd3+zhXmpxTS0WNsfWxfV3Gbj+02FnDgo8sC/62RpMyrAZFIM7xvEv48fyNybJrH8rmk8etog4kJ9+HhVDue/s4oRjlak320soKbRyRW6eWtg5wJ9vQp3J1U0d2Ebcqv4Zn0Bl0+KIybE5+836E5MJn0NN2sz/NyF18Ja+AQ0VsCs5/Tn1J3FTdITY0tfhMpdRkfT8dK+hdI0mHKXfsZ8TxY9BvrP0Nejbq41OpoDS50DoQP196piXyln6H/LpelGRyK6k92tRS3ucPJL3bNyV1qN/lVzDXx+AXj4wTkf6r9/0bN4+Oknfm77ASqzjI5GCNHFdfNP1EII0bFarXaemruNhF6+nDs62jWTpJyhrzNQU9C+7Q7SXrQ93MwmxvUL4a4T9TXXVtw9jafOGMzgqAB+2FTIVR+tY9gj8zj/7ZW8vWQXmSXdu7pwfloJdc3Wv7YX3S20P4QPkjajYh+9A724cFwMH1w6ho0PzODNC0dyoqMV6U2fbWTEY3qF7rtLs8ipcEIruSXPgFcwjLr86Mfq4ux2jYd/SCPMz4PrpvY3OhxjhPbXky3bfuiaJy8Ub4E1b+t/z72HGR1Nx5jxqF5NOPfO7rF+5OGy22DRU3qb1ZQzjI6mc5h6DzRV6e0EO5u6YsheKu1FDyb5FEBJFaFwrt2tRWc+pa9d2x1Jq9F92e3w7bVQnQNnfwh+EUZHJIwy9mr9+M7KN4yORAjRxUmCUAghnOijlTlkVzRyz+wkLGYX/Ysd5DhI1p4DDJqmJ6niJoNPiNNCiQzw4twxfXnzwlGsv38Gn145jssmxFFR38rjP29jxgtLmPj0Qu79dgvz00q6XXXhV+vy6R3gyfh+h/iZJp/maDPazoSu6BG83S2ckPJnK9Jv/nUMV0/uR0VDC4/+mMaxzy5ixvOLeWpuOutyKrHZ25kcKNwAmfNg/HXg4euaJ9GFzNlYwMa8au6cmYivRw+uRjrmBogYrFcRNlUZHc3hs9vhp9v0hPe0e42OpuP4R8KUu/V9eftco6PpOFu+hPIMmHp3968UPVx9RsDA2bD8Vb3VbmeS9j2gSXvRg/GLgNiJkPpNz0r0C9fpzq1F9yetRv+09HnY/hMc/zjEjDc6GmEk/956l4kNH0NjpdHRCCG6MPmkJYQQTlLd2MrLv2cyKSGUKQPCXDdRSDxEDtUPMByu4i16a7KU01wWlrvFxPj4EO6elcSvt0xm+V3TeOL0waT09mfOhgKu/O9ahj8ynwveWcU7f+xiR2l9l64uLK5p5o/MMs4cGYXJdIgz5Xf/zLd1wUod0aFMJsWIvkHcMTORebccy5Lbp/LAScmE+Xnwzh+7OPONFYx5/Ddu+3ITv2w9zHa+i5/V16cYc5Xrn0An19Bi5am56QyNCuCM4X2MDsdYZjc45VVoKId59xsdzeHb/BnkrYQZD4NXkNHRdKyxV+uVdL/cCW1NRkfjerY2vXowYjAknmx0NJ3L1HugpQZWvGZ0JPtK/VZvLdor0ehIOq+U0/Skd2ma0ZGIrq4ntBbdn7QahR2/w4LH9KTQ2KuNjkZ0BuOvg7YGWPe+0ZEIIbowSRAKIYSTvLJgB7XNbdwzKwnl6g9pg86EgnWH328+bY6jvWjHHWTrHejF+WP78tZFo9jwwPH874qxXDIhlpLaZh77aRvHPb+YSc8s5P45W/l9WwmNrV2ruvDbDQXYNQ7eXnS30ATolaK3eBWiHfqGeHPZxDj+d+U41t0/g5fPG86E/qHMSy3mmo/XMfzR+Vzy/mo+XplDUc0BEgZFm/UzjMddB57+Hf8EOpnXF+2gtK6FB09JOXRSv6foPUyvJNzwEexaZHQ0f6+pGuY/AFFjYOj5RkfT8cxuMPs5qM6FpS8YHY3rbfpUXzd56r1SPbi/iEF6d4KVb3SeioHaQr3NobSCPbSkU/U2iUeylrgQe+sJrUX319NbjVbnwtdX6Cdi9JSksPh7EYMgfhqsehOsLUZHI4ToouTTlhBCOEF2eQP/XZHNP0ZFkxTZAQfid7dvOpw2o5qmJ6ec3F60PdwtJo7pH8o9s5KYf+uxLL1zKo+dNojECH++Xp/P5R+uZdgj87nw3VW8uzSLXWWdu7pQ0zS+WpfHmNhgYkJ8/n6DlNP1qpfaQtcHJ7qlAC83Thnam5fPG866+2fwvyvHcuG4GLLKG7hvzlbGP7mAk175gxd/y2BrQY2+/yx5Fjz85QxjILeikbf/yOKM4X0Y0beHVZ4dypS7IDgefrgJWhuNjubQFj4BjRV6kqynJoxiJ+pVA0tf1FvLdVfWVr36ufcIGDDT6Gg6pyl3Q2s9LHvJ6Eh0ad+htxc9zehIOjffMIidpL9/78Tvc0Un15Nai+6vp7YabWuGzy/U1+b9x0fgfhifP0XPMf56qC+BLV8ZHYkQoovqoZ+uhRDCuZ6am46b2cStxw/omAkD++pVFIdzBnLJVqjc2akO2kQFeXPBuBjeuXgUGx6YwceXj+WicTEUVjfx6I9pTPu/xRz77CIe+G4rC9NLaWq1GR3yPjbkVbOzrOHvqwd32/2zT5M2o+LouZlNHBMfyv0nJbPotinMv2Uyd85MxMNi5qXfMznplaVc8MT7sO178gZciNVdqgef+HkbFpPijpnS+m4fbl5wyst6q66FjxsdzcEVbYY1b8Ooy/UW2z3ZjEf1asJf7uq+CYYNH0FNrl49KBUSB9YrEQafBavfgvpSo6PR34+GD9a7JohDSzldf19evMXoSERX1BNbi+6vp7Ua1TT4+d9QtBHOeFNfbkSIvcVP0zsWrXi1+743FEK4lCQIhRDiKK3OquSX1GKuOTaeXn6eHTfxoDOgZAuUZRz6calzOry9aHt4WMxMTAjlvpOS+f3fU/jjjqk8etogEnr58uXafC79YA3DHpnHxe+t5v1lWWSVNxgdMl+ty8fLzcysIYfZ0md3m9G0OS6NS/Q8SikSwv24dko8X197DGvuPY5nzxrCTe7f06B5cvKaIYx+/Dfu+GoTC9JLaLF2rmR7R1i+o5xfUou5bmp/IgI68H90VxE7EUZeAitf11tXdzZ2O/x8O3gFw7R7jY7GeP6RevVY5jzYPtfoaJyvrRmWPAfRY6H/dKOj6dyOvQuszXpFqZGq8yB/NQw63dg4uoqkU/T35e1ZS1yI3fa0Fn2657QW3V9PazW6/kPY8DFMvh0Gnmh0NKIzUkpfNqA0DXb+bnQ0QoguSBKEQghxFOx2jcd+SiPC35MrJ/Xr2MmTTwPUoQ8waJqelIqbZFh70faKDvbmwnExvHvJaDY8MIP/XjaGf46NIa+ykYd/SGPqc4uY8uxCHvo+lUXbS2lu69iER3ObjR82FXLioAh8PSyHv2HKafoHemkzKlwo1N3G2V7rGNOwCPdjrubJfx7L5AFhzN1SzGUfrGXko79xw6cb+GlzEQ0tXWvdzyNhtdl5+Ic0ooK8uHxinNHhdF4zHgHfcPj+RrC1GR3NvjZ/prdonvEweEl7WEBvGxyWBHPv7PytYdtr3QdQVyjVg4cjtL/eXnDtu1BbZFwcad/p1ymSIDwsPiHQ71hpMyrab5/WoucaHY2xekqr0fx1+klS8dP1k4OEOJhBZ4JfJCx/xehIhBBdkCQIhRDiKHy/qZDN+TXcfsJAvNzNHTu5fyTETICtXx/8AEPJVqjY4Ugmdj2ebmYmDwjjgZOTWXDbFJbcPpVHTk0hLtSHz9bkcsn7axj68DwueX81Hy7PJqfC9dWF89JKqGu2Hn570d12/w6kzahwtqpsWP02fHwWPBMHX14MfhG4TbyREwdH8tK5w1l7/3G8f+loThoSybId5Vz3v/UMf3Q+V3y4lq/W5VPd2Gr0s3CJT1fnsr2kjvtmJ+Hp1sH/o7sSzwCY/bz+mrHsRaOj+VNTNcy7X2+pPfR8o6PpPMxu+lqMNbmw9AWjo3Ge1kb44//0Ndr6HWt0NF3DsXeA3QpLnzcuhtRvIHIYBHfwiXJdWcoZ+mt34QajIxFdhbQW/avu3mq0oRy+uAj8IuDMd8Ak72PFIVjc9RPIdi3SW/MLIUQ7tKP0QQghxN6a22w880s6g/r4c/rwPsYEMegM+OlWKEmFiEF/vX93e9GkztletL36hnhz0fhYLhofS3ObjVVZlSxML2VxRhkPfp8KQFyoD9MSe3F8cjijYoMxm5z7Afqrdfn0CfRiXL92VmSGDYBeyXpF57hrnBqT6GGsrXo1auY8/VLuaDMcHA8jL4UBx+snD1g89mziYTEzdWAvpg7sxeOna6zJruSXrcXMSy3mt20lmE2K8f1COGFQBCckh9PLv+u34qxubOX/5mfozyslwuhwOr/EWXoF0OJn9BZ4YQONjggWPgFNlTD7GzDJeY37iJ0Ig8+GZS/plSTdYU2iNe9AQymc81+jI+k6gmJh+AV65eUxN+pVNR2pKkdvTXzcwx07b1eXOBt+vFmvIuwzwuhoRFewu7Xoaf/pua1F97e71ejr4/VWoxd9333eK9is8NWl0FgOl88D72CjIxJdwchL9TbtK17T16sUQojDJAlCIYQ4Qu8uzaKwppn/O2cYJicnoQ5b8ql625HUb/6aINzdXjR2IviEGhKeK3m6mTl2QBjHDggDILu8gUXbS1m4vYyPVuTw7tIsgn3cmZ7Yi+NTIpjYP/SoqzyLa5pZmlnG9dMSjux3nnwaLHpSbwUmH+5Fe9QVQ+Z8yPwVdi6C1jowu+uJwFGXQcLxh50gMJsU4/qFMK5fCA+enMyWghp+2VrML1uLuX/OVu6fs5URfQOZOSiCmSmR9A3xdu1zc5EXf8uktqmNB05ORsmZ9ofnxGdg50K91eilc4090Fa0Gda8DaMuh8ihxsXRmc14FLb/orca/eeXXbuipKVOr16NnwYx442OpmuZdBts/B/88ZxeWdSRUr/Vr6W9aPt4B+t/66lz9BbPXXnfFa4nrUUPbner0R9u0luNjr7C6IicY8GjkLUETn1d3gOJw+cVCMMv1N8/T38AAgw6iV0I0eVIglAIIY5AWV0Lry/cwXFJ4YyPN3BtP59QvQ3X1q9h2v37HmAoSdXbi46/zrj4OlBsqA+XhMZxyYQ46lusLN5exry0Yn5JLebLdfl4upmYnBDG8SkRTE/sRZCPe7vn+GZDPnYNzhxxhG+2U06DRU/Atu/1FiBCHIzdpldlZM6DjF+h2NEqxr8PDD5TTwjGHQsevkc1jVKKIVGBDIkK5PYTBrKjtF5PFqYW88TP6TzxczpJkf7MTIlg5qAIYlyULLSYFGaTcloiL6Okjo9W5nD+2L4kRfo7ZcwewbcXzHwS5lyrr2s25kpj4rDb4efbwCsYpt1rTAxdgX8kTLkL5t0L23/Wq5K6qtVvQWOFvvagaJ/AaL3V3rr3YcLNENyB662mfgt9RkJQTMfN2V2knA6Z1+qv9VGjjI5GdFbSWvTvjbhYT7bPewD6H6dXVndlad/rJ8yMugyG/9PoaERXM+5aWP2mXnV8/KNGRyOE6CIkQSiEEEfghd8yaLHauXtWotGh6OuYfH+9vo7J3m2K0uaAMkFi92gv2h6+HhZmD4lk9pBIWq12VmdVMi+tmHmpJcxLK8GkYHRsMMenRHB8cjjRwX+f9NA0ja/W5jMmLpiYEJ8jCyxsIIQl6R9iJUEo9tdYCTsX6AnBHb/prRWVCaLHwvQH9aRgeIrLDg4ppUgI9yMh3I8bpieQV9nIr6l6ZeGLv2fwwm8ZLpl3b7sThW5mE2aTOuj3FrNpr/sct5lMe77fWdaAj7uZW2d0gjaZXc3Q82DLl/DbQzBgZse3LATY/BnkrdLPnPcK6vj5u5KxV8OGj2HuXfpaTO4urvi1toKtxbljtjbAspf16hhJlByZSf+GDR/prcVOe61j5qzcBUUb4fjHOma+7mbgLL0TQOq38ncvDk5ai/697tRqtCxDP0mrzyiY+ZTR0YiuKChG7zK17gOYfDt4yomSQoi/JwlCIYRop4ySOj5bnctF42OJDzu66h2nSDoJfrxFryLcnSDUND0JFTsRfMMMDc9o7hYTExNCmZgQysOnpLC1oHZPsvDRH9N49Mc0kiL9mZEczvHJ4aT09j9gFdP63Gp2lTdwzZSjXOcp5TRY9JTeMtJP1kXr0TRNr/TN+EVvH5q/GjQ7eIfoycCEGXoLMoPWHYkO9uaKSf24YlI/SuuaWbCtlMrGVqfPo2lgs2tY7Ro2ux2rXcNq0xy32bHZNdr2fO94jE3/eu/vm9pse773cjPz1JlDCD6CSuEeTyk46UX9QNuPt3R868qmaph3P0SN0ZOV4tDMbjD7OfhgNix9wTUVl9YW/X/U1q/0lqbWJufPATD1HteM2xP4R+rteFf9Bybd2jFrUu5uL5p8muvn6o68AiF+uv5znPFo101oCNeR1qKHb+9Wo67sgKBpelePtO+gdJvzxy/aDBZPfS3evdYSF6JdjrlBf23Z8FGP6SYlhDg6kiAUQoh2evynbfh4WLhxeoLRoei8gqD/dMc6Jo4DDKVpUJEJ4/9ldHSdilKKwVEBDI4K4N/HDyS7vIH5aSXMTyvhlQWZvPx7Jn0CvfYkC0fHBeNm1g/YfLUuHy83M7MGH+XZu7vXIUz7HsZedfRPSnQtNivkrYT0nyD9R6jO1W+PHKavIzXgBOg9HExHt16ms/Xy8+TcMX2NDkN0lKAYmH4//HKXXk045JyOm3vh43r17Oxv5ID54YqdCIPP1luSDT3XOckhu01f/2jrV5D2A7TUgHcoDDvfNS0sA2Og9zDnj9uTTLxFbzO66Ck4823Xz5f6rZ7IN6LKuLtIOR0y5kL+Gug71uhoRGcirUXbb3er0fkP6ifZOavVqKZB0Sa9O0/ad3r1tDJDWKLz36f4R+qf52XtOHE0+ozU16lf+QaMuRrMcuhfCHFo8l9CCCHaYUlGGYszyrhnVmLnqkwZdKZehZS/GvqO0z8c9dD2ou0RG+rDlZP7ceXkfpTXt7BgWynz0kr4dHUuHyzPJsDLjemJvZieFM6Pmwo5cXAEvh5H+dLZK1FvM5o2RxKEPUVro946NP0nfT9tqgSzB/Sb8mdSUKpJRWcz5irY8hXMvVOvZPUJdf2cRZthzTsw+gqIHOr6+bqTGY/q1X1z7zzyqk9Ng/y1elJw6zfQUArufnqngsFnQdwUOcjUmfmG6fvtspf0lqO9XNgGv3wHFG+RFnhHa+CJ+vuB1G8kQSj2Ja1F28+ZrUZ3JwVTv9WTglVZelIwbjJMuEn/jO0T4tz4hXCm8dfDZ+fpxxwGn2V0NEKITk4+4QkhxGGy2TUe/2kb0cFeXHxMrNHh7GvgiXo7kq1f6+uVpc2R9qLtFOrrwTmjozlndDSNrVaWZJQzL62Y37eV8s2GAgDOGhnlnMmkzWj311CuJwPTf9aTg9Ym8AzQ13RLnK23FfPoBC2KhTgYkxlOfRX+M0mvJDzzHdfOZ7fDz7eBVzBMdUGbzO7OPxKm3AXz7oXtP+v/Zw5XSZqeFNzyFVTn6AmLAcfrVYkJx4Obl+viFs51zI16kn3Rk3DOh66bJ/VbQOnrHIkj5+mvVzqlzoETnpSqaaHb3Vp0wExpLdpeR9NqVNP0dVVT5+ifpauy9aRgv2P1Cu3EkyQpKLqOATMhpD8sf0U/mVyqkIUQhyAJQiGcTCk1E3gJMAPvaJomp9Z2E1+szWN7SR2vnT8CD0vnav+Hh59+EC91Dgy/EMozYOw1RkfVZXm7W5g5KIKZgyKw2uysya4iv6qR8f2c9KFQ2ox2T5VZ+oH59J/0s741O/hHwYiL9IP1Mcfo64UJ0VX0StIrkRY/pSeLBpzgurk2fQp5q+DU1/W1uUT7jb0aNnwMc++CflPB3fvgj63K1k8q2vI1lKb+eRB0yl36/yvPgA4LWziRTwiMuxaWPKtX+EUMds08qd9A3/Hg39s14/ckKafrLcfzVurvE0TPtndr0ZNelIP6R6I9rUY1DQo3/Nk+tCobTBaIO1Z//5N4kmFrgQtxVEwmff3BH2+BnGX6yeNCCHEQStM0o2MQottQSpmBDGAGkA+sAc7TNC3tYNuMGjVKW7t2bQdFKI5UfYuVKc8uIibEm6+uGY/qjB/WUr+FLy+BvsfoBxn+nSEVhJ3Za2PBOwQu/dnoSMSR2t1+KP0n/VKaqt8ePggGztIPskcOlYM7omuztsCbk6GlHq5bqZ+Q4mxNVfDKKH3tvEt/kSqao5G9FD6YDZPvgGn7VWLWl+rvFbZ8pbckB73rwOCz9RNX5D1D99BUBS8OhbhJcO4nzh+/NB1eHwsnPisnOTlDSz08G6+f4Df7OaOjEUZb8Tr8erfeWnTYeUZH03VV5+mtRnsP+2urUU2DwvWOSsHv9Mp5k0Vv/Z98mv7+XZKCojtoa4IXUiBqNJz/udHRdDtKqXWapo0yOg4hnEEqCIVwrjHADk3TdgEopT4DTgUOmiDsqWqa2iiqacLDYsbdYsLDYvrz2mzqdAm4NxfvpLy+hbcuGtnpYtsj4QRw84Hc5RA7SQ70dXbJp8Hip6XNaFdja9PPwkz/SW8fWpuvr/fZdzyc8ISeGAyOMzpKIZzH4gGnvArvzoDfHnbNAeyFT+hrc876VpKDRyt2op7wW/ai3prOO0SvTtryFWQt1iubwwfBcQ9ByhkQFGN0xMLZvILgmOth4eN6ZUzv4c4dP20OenvRU5w7bk/l4at3AUn7Dk58Wm/vLHomaS3qPPu3Gh19BRSshzTHmoLVuX8mBSffLklB0T25eelrEy96EsoyIGyA0REJITopqSAUwomUUmcBMzVNu8Lx/YXAWE3Trj/YNj21gvDqn+5nSc6mg95vUgqTArXPtUKpv9639+1KgcL5CbyS2maCfNxJ6NXJ1wwr2w4NZXoVhp8saN+ptTXqH1S9g8Ei6zt1CbZWvTLDbtWTgl5B+u/PK1hah4rur3IX1Bbqry3KmUk8DeqK9BMlguOdOG4PZmuFgnX6wU9bm54UdPMEnzD94naI1qOie7DboGCNvp6kZ6Bzx24s19e9dlX70p6ooRzK0sE3XN9vRc/UXK1X7fcZAWZ3o6PpHkq2QksdmNzA2qx39fAMBJ9Q/QQa2d9Ed2drg/w1+sko7s7vApIYOZo7pz7r9HG7AqkgFN2JvBoK4VwHykz9JQuvlLoKuAqgb9++ro6pU+rl78mAcD/smoZdA81xbdc0NMf13l//edtej7Xb97r/z/tccdqDh8VE3+AucEDNLxLaGsA71OhIxN9x89bX2WquAWqMjkYcDmXWDyZ4B+u/OyVn+YseJDBGP8jWUOr8sd289fGFc5jdISgOavL1xKtPmGtaw4rOy2TW96mqbKgvdvLgSk5CczbvYL3So7Hc6EiEkZSCkP6SHHSmkAQ9SWjx1KsKJSkoehqzG/j3gbpCaG1w/vgtchxDiO5AXhmFcK58IHqv76OAwv0fpGnaW8BboFcQdkxoncujk+79+wcJIYQQQgghhBBCCCGEEMLpZJEPIZxrDZCglIpTSrkD5wLfGxyTEEIIIYQQQgghhBBCCCHEHlJBKIQTaZpmVUpdD/wKmIH3NE1LNTgsIYQQQgghhBBCCCGEEEKIPSRBKISTaZr2M/Cz0XEIIYQQQgghhBBCCCGEEEIciLQYFUIIIYQQQgghhBBCCCGEEKIHUZqmGR2DED2aUqoMyDE6DoOEAuVGByFENyH7kxDOIfuSEM4j+5MQziH7khDOIfuSEM7Tk/enGE3TwowOQghnkAShEMIwSqm1mqaNMjoOIboD2Z+EcA7Zl4RwHtmfhHAO2ZeEcA7Zl4RwHtmfhOgepMWoEEIIIYQQQgghhBBCCCGEED2IJAiFEEIIIYQQQgghhBBCCCGE6EEkQSiEMNJbRgcgRDci+5MQziH7khDOI/uTEM4h+5IQziH7khDOI/uTEN2ArEEohBBCCCGEEEIIIYQQQgghRA8iFYRCCCGEEEIIIYQQQgghhBBC9CCSIBRCGEIpNVMptV0ptUMpdZfR8QjRlSil3lNKlSqltu51W7BSar5SKtNxHWRkjEJ0BUqpaKXUQqXUNqVUqlLqJsftsj8JwymlNKVUfwPmjXXMbWnHNp5KqdVKqU2Ofelhx+09al9SSj2klPq4E8QxVyl1sZPH7BTPradQSpmVUhuUUj86vu9R+5IQzqKUylZKbVFKbVRKrXXcJvuTEO2klApUSn2llEp3fHYaL/uSEN2DJAiFEB1OKWUGXgNOBJKB85RSycZGJUSX8gEwc7/b7gJ+1zQtAfjd8b0Q4tCswL81TUsCxgHXOV6PZH/qYEqpiUqp5UqpGqVUpVJqmVJqtOO+S5RSS/d6rL/j/q+VUgkHSmYppT5QSj12kLkeUkq1KaXqlVLVjnnHu/YZOp9Syl0pVa6U8j3AfdlKqSbHc6xSSv2klIp2YTgtwDRN04YCw4CZSqlxOGlfas/fx2GM1e4EqBH2+x3uvrx6ONtqmnaipmkfujpG4VI3Adv2+l5el4Q4clM1TRumadoox/eyPwnRfi8Bv2ialggMRX+Nkn1JiG5AEoRCCCOMAXZomrZL07RW4DPgVINjEqLL0DRtCVC5382nArsPBn4InNaRMQnRFWmaVqRp2nrH13XoH3T7IPtTh1JK+QM/Aq8Awei/g4fRk077PzYI+A3IAf4BtB3htJ9rmuYLhAILgS+PcJyjdhSJqsnARk3T6g9y/8mO5xgJlKD/fF1C0+2Ow81x0XDCvtSev49u6GRN03z3ulxvdEDC9ZRSUcBs4J29bpbXJSGcR/YnIdrB8V5sMvAugKZprZqmVSP7khDdgiQIhRBG6APk7fV9vuM2IcSRC9c0rQj0pAfQy+B4hOhSlFKxwHBgFbI/dbQBAJqmfappmk3TtCZN0+ZpmrZ57wcppUKBBUAqcIGmadajndgxxidAH6VUmGOeAKXUu0qpIqVUgVLqMUf3A5RSOUqpkY6vL3BUoiU7vr9CKTXH8fUYpdQKR4VikVLqVaWU+17PRVNKXaeUygQyHbfd7nhsoVLqssMIfxbw82E8x2bgK/SuDbvnn+1oX1irlMpTSj10sO2VUpc6WknVKaV2KaWu3uu+KUqpfKXUv5Xe+roN/QSW+ZqmrQLCgduUUjlAOhCvlPJybDvOURVYrfTWpFMOEsJB/z6UUknAf4DxuytCD+P5LXFcVzu2Ga/2a5+5f5Who0pxl+NnkKWU+uchfuSeSqnPHY9dr5Qa6hjjdqXU1/v9bF9RSr14iLEOyBHPMsf2NUpv9zV9r/sXKaWucHzdXym12PG4cqXU53s97hil1BrHfWuUUsfsdV+cY7s6pdR89GS66BgvAncA9r1uk9clIY6MBsxTSq1TSl3luE32JyHapx9QBrzveH/1jlLKB9mXhOgWJEEohDCCOsBtWodHIYQQQgBKb9H4NXCzpmm1RsfTA2UANqXUh0qpE9WB1y8JBhajJ3Av0zTNfoDHtJsjaXcRUAFUOW7+EL39bH/0pPHxwBWO+xYDUxxfTwZ2Acfu9f1ix9c24Bb0pMp4YDrwr/2mPw0YCyQrpWYCtwEzgATguMMIfxbw02E8R2/0asuVe93cgP68A9Erla5VSp12kCFKgZMAf+BS4AWl1Ii97o8AAviz+tYOHKOUGgR4ASOBY9B/h42AXSnVxxH7Y47bbwO+3p2k3c9B/z40TdsGXAOscFTYBR7G85vsuA50bLPiIM8bAMcBsJeBEzVN83M8l42H2ORU9IrUYOB/wByllBvwMXrr1UDHuBb038tHh5r/EMai//2FAg8C3yilgg/wuEeBeUAQEIWjktTx2J8czy0EeB74SSkV4tjuf8A6x/iPAk5d01AcmFLqJKBU07R1RsciRDcxQdO0EejLm1ynlJr8dxsIIf7CAowA3tA0bTj6+yxpJypENyEJQiGEEfKBvdfBiQIKDYpFiO6iRCkVCeC4LjU4HiG6BMeB+6+BTzRN+8Zxs+xPHciRlJ2IfrLQ20CZUup7pVT4Xg+LRq8ke1/TNGecVHSOo9qsCbgSOEvTNKtjzhPRk8UNmqaVAi8A5zq2W8yfCcFJwJN7fX+s4340TVunadpKTdOsmqZlA2/u9bjdntQ0rVLTtCbgHMdz26ppWgPw0KGCV0r1A9w0Tdt+iIfNcTzHWvTE47O779A0bZGmaVs0TbM7KjU/PUB8ux/7k6ZpOx1tRBejJ5sm7fWQNuARTdPaNE37GahHrxY8Eb3V6KOaphWgn1VepGlaC3AB8LOmaT87YpgPrEVPeu4//+H8fey/zWE/v8NkBwYppbwcrYlTD/HYdZqmfaVpWht60s0TGOc4s34JcLbjcTOB8r9JBM1xVFjuvly5132lwIuOn/vnwHb0ZOj+2oAYoLemac2apu1er3E2kKlp2keOv9NP0X9vJyul+gKjgfs1TWtxtDb/4RBxCueZAJyilMpGX4ZhmqO6VV6XhDgCmqYVOq5LgW/RlzuR/UmI9skH8h3dIUDvTDEC2ZeE6BYkQSiEMMIaIMHRusgd/aDb9wbHJERX9z1/nt1/MfCdgbEI0SUopRT6WhrbNE17fq+7ZH/qYJqmbdM07RJN06KAQUBv9DZ7u21CrzKbq5Qavtftu9uMuu03pBuHXp/wC0e1WTiwFb3KDfREihtQtDspg57c290yaTEwSSkVAZiBz4EJSm9RG4CjskwpNUAp9aNSqlgpVQs8wV9bNO7dbr33ft/nHCJ20JM7f9de9DTHc/QArgcWO+JGKTVWKbVQKVWmlKpBr8I7YAtJR9XeSqVUpePnMWu/x1YAQbsr49CTruPQT/5S6AkP2HdfigHO3jv5hZ4EjDxQDIfx97F/zIf9/P6OI2H7D8cYRUqpn5RSiYfYZM/v0VHpmu+IF/Tq1AscX1/A31cPnqZpWuBel7f3uq9gv2R5zl7z7O0O9N/DaqVUqvqzfW1v/vp3loNeCdobqHI8973vEy6madrdmqZFaZoWi/4ZaYGmaRcgr0tCtJtSykcp5bf7a/SOAFuR/UmIdtE0rRjIU0oNdNw0HUhD9iUhugVJEAohOpxjvZ/rgV+BbegH6Q51JrYQYi9KqU+BFcBApa/9dDnwFDBD6etZzXB8L4Q4tAnAhegVGhsdl1nI/mQoTdPSgQ/QE0F73/4S+u9ivqN9JUAReiIwdr9h4jiMhIamaeXA1cBDjjOf84AWIHSvpIy/pmkpjsfvQG+TeSOwRNO0OqAYuApYulfr0zfQq7ESNE3zB+7hry3W907uFLFvd4W+fxP6YbUXdcRsc1TH2tCTcKC3j/weiNY0LQB9Hb+/tIBXSnmgV9g+h77OTCB6YnL/x0YCC5VSmx1fr0Ov2msGZh9gX8oDPtov+eWjadrf7msH+Ps4UEXpoZ7fgR7fAHjv9X3EfnP+qmnaDMdzS0evZDyYPb9HpZSJfTtlzAGGOP5+T0Jf//JI9XGc5LBbXw7QkUPTtGJN067UNK03+t/660qp/o7Hxuz38L5AAfrfY5DjgPre9wnjyOuSEO0XDixVSm0CVgM/aZr2C7I/CXEkbgA+cbzXG4Z+8pvsS0J0AxajAxBC9EyOFlR/d+a7EOIANE077yB3Te/QQITo4hyt9g60Li7I/tRhHNVYs4HPNU3LV0pFA+ex75p5AGia9owjafWbUupYTdO2K6W+Bh53tF+sBc4CkoG5hzO/pmnpSqlfgTs0TbtFKTUP+D+l1P3o7TLjgChHe03QqwivB65zfL/I8f2jew3r54il3vH8rgXKDhHGF8D7Sqn/Atnoa8odkFLKC71F2qLDeX6OJNIp6GvQbdsrvkpN05qVUmOA89Fbh+7PHb0CsQywKqVO5M8KjD0cbTyHO+bLBj7WNM2ulHoPSAROB0qAMUqp9ejr8a1RSp0A/IZetTkO2KFpWv5+8f/d30cJEKWUctc0rfUwnl8ZesvQfujrG4Je+Xmno7VmDXD3XvOHo6/39zt6dWQ9erL1YEYqpc5AT1DeiJ5wXun4OTUrpb5CT2Cu1jQt9xDj/J1ewI1KqdfR17NM4gDvrZVSZ6Ov0ZiPvs6m5oj/Z+AVpdT56H9/Z6LvNz9qmlaulFoLPKyUugf97+1kpONHh9I0bRGO/VzTtArkdUmIdtE0bRcw9AC3y/4kRDtpmrYRGHWAu2RfEqKLkwpCIYQQQgghhJHq0BMwq5RSDejJlK3Avw/0YE3THgXeAX5XSsUD/wIqgc3oa59cD8zWNK2kHTE8C1yllOoFXISeGEtDT6h8xb6tLxejJ6CWHOR70Nuhnu94bm+jtyI9KE3T5qK3zFwA7HBcH8x09IRP8988px+UUvXoicrHgYv36tjwL+ARpVQd8AB6guhAcdWhJ7m+QP9ZnE/7kkS3AVvQ28tXAk8DJk3T8oBT0Ssry9ArCm/nwJ9P/+7vYwGQChQrpcr/7vlpmtbo+Hksc7Q3HedYA/Fz9L+hdcCPe81vcsxV6HgOxzrGP5jv0FuSVqFXKJ/hWI9wtw+Bwfx9e1Fw/A73uny7132rgASg3PF8znIc9N7faPSfXT367+4mTdOyHI89yfHcKtBbkZ7kqKoF/Xc91vGcHwT+exjxCiGEEEIIIboQte+yBUIIIYQQQgghOitHxdhWTdNeNzoW0X6OKsV0IELTtNojHOMS4ApN0yb+3WOFEEIIIYQQ4mCkxagQQgghhBBCdB0bgR+MDkK0n2NNwluBz440OSiEEEIIIYQQziIJQiGEEEIIIYToIjRNe8voGET7KaV80NdLzAFmGhyOEEIIIYQQQkiLUSGEEEIIIYQQQgghhBBCCCF6kgMtAi+EEEIIIYQQQgghhBBCCCGE6KYkQSiEEEIIIYQQQgghhBBCCCFEDyJrEAphMJPJpHl5eRkdhhBCCCGEEEIIIYQQQohDaGxs1DRNk8Ir0S1IglAIg3l5edHQ0GB0GEIIIYQQQgghhBBCCCEOQSnVZHQMQjiLZLqFEEIIIYQQQgghhBBCCCGE6EEkQSiEEEIIIYQQQgghhBBCCCFEDyIJQiGE6EZaG5upyMozOgwhhBBCCCGEEEIIIYQQnZisQSiEEF1YTVEZ2T/+RuOCJfivW0V8Vir+dhtFG7YSOSTR6PBEF9Zc10Du4lVULVmB2rCBwIw02jw8qU8YCCmDCBgzgsgJowiIDDM6VCGEEKLLaa6tpywtk5qMLJpy8rD4+eIVG01g/zhC+vfFzdPD6BCFEEII0YVpdjv5a7bQWlNL38lj5L2FEOKAlKZpRscgRI/m4+OjNTQ0GB2G6CKKt2SQ/+N8bEv+IGzzWmILd2FCw6pM7IoeSE1cf0Yv/oE1j7zI6PtvMjpc0UU0VtWQu3AFNUtXoTZsIDRjK32LsrBodgCqvfzIjx2IpaWFPgU78Wtp3LNtSUAYxX370zQgCfPgwQSNG0HU+BF4+vsa9XSE6NE0u52a/BKqs/JQFjNmNzfMnu57ri1ubpg9PbB4uGNxd8PsZkGZnNtURLPbsba20dbUTFtzK7bmFqzNLVhbWrG16N/bWlqwtbZhb27BJyqC6HHDnRqD6Do0u5225lbcvT2NDsVp2ppbKEvfRfX2XTTuyqItKwdTXh4exYX4lRUTXFVCUGPtQbe3o6j0DaQ6MJT6kHBawiOxR0Riju6DR0w0fv1iCOofS2B0hNP33+4k86eF1KXvwN7UhL25Ga25Ga1Jv1bNLdDSjGpuhtZWTC3NmFpaMLW2YG5rxdzSgqWtBUtbG5a2FtzaWnFva8XN2kqbxY3iqH7UJyRhGjKYwLEjiZo4Cu+gAKOfshA9XlN1HcUb06jenEbL9kzIz8fvnDNJ+eepRocmDqGpuo6KjCx8wkMJiultdDh/a+V19xDxzaeUJQ7BPnYsodOPJWbyaCwe7obGZW1pJev3FVT88hvuK5fTd9sGQuurAGi2uJMdPYDqlKFYxo0l/LjJRI0eLO8jjpBSqlHTNB+j4xDCGSRBKITBJEEoDsbWZiVnyWrK5v6OecVyolLXEVFTBkCDuxc7E4bQMHIsfsdNod9J0/AOCsDWZqXJN4DUqbMZ+8sXBj+DnkOz22lpaKKhvJKmsiqaK6toqaimraoGa3U1tuoa7DU1UFuLqbYWc3095oY63BvqsXp60hIajj0iEtUnEvfoKHxiowmI70twP+dXENSVVpC3YDm1y1Zh2biR0MxUoktzMTuSgRU+geT3S6IxZSieY0cRMW0CEYMG7PngoNntlKTuoGT5Wpo2bMKSmkpQVgZRxdl42NoAsCkThaF9KIvpT0tiCh7DhhAybgR9Rg02/EOTEN2BZrdTmVNA2YZU6rakY8vIxJK9i4D8bMJL8/Fvrm/XeG0mM1aTBZvJjNVsxmq2YDf9eW2zOK7NFpTdjtlmxWyzYrG26dd2Kxar49pmw91ubdf8rSYL5es203tYUru26+k0u53aonIqM3ZSuyOb5l05aLl5mAoL8CopxG6x4HbvvST/Y7bRoR5U2mc/4HbH7QRVleKdm9UlEiy2NisVO3Ko3L6Thh3ZtGXnQF4e7kUF+JYWEVRZQkhdFSb2/Zxd4+lLRXA4dWERNEf0RouKxhzTF+9+sfj3j6W1ppb6XTm05BZgy8vHXFyER2kRvuWlBNWUEdxQ85dYWsxuVASEUhMURmNoOG3hEdCnD5boPnjF9sW/XwwhA2K7xM/V2apyCvGLi95zstOBNFvcad19cXPHanGjzc0Dq7s7VjcPbO7u2Nw9sLt7YPfwwO7hCe7uqIYGAndlEFW0C++2FkBP6haGRFIWM4CWxGTchg8ldPxIQ977aHY79eVVVO3Moa2+iYCY3gTG9JH3YEehpaGRpooamqpraK6sweLpTuTwFPmZGqSutIKSDanUbEmndXsG5p078cnPIawkj1615fs8ttVkwd1uZUvSaDyefpIBJ083KOqeydZmpXJXLlWZ2TRk5dKak4e9oABLcRGepcX4VZQSXFO+571rQXAkvcvyO3XSSrPbKQ2OwGS3YbHZCGrUX58b3TzJ6pdC7bCReE2cQPTsqYTERbs0lqbqOnb++Dt18xfiu3Yl/XZswae1CYDC4EgKUkagTZyEOdCftpWr8d+ygdic9D2vXTWevuTEp9AweDheE8YTNXMyof1jXRpzdyEJQtGdSIJQCINJglDs1lRdx665C6n9bRE+q1cSl7lpT6VWqV8IeSkjaRs3ntCZ04idMu6gH0i3pIzFp7qCfgU7OjL8LqutuYWG0koayipprqikuaKKtopq2qqqsNfUoFXXoNXUYKrTE3uW+jrcG+vxaGzAq6ker+YGfJobD+uAeJvJTL2nD02ePjR5+dLi5Y17cxOB1eUENVTvSdLtZkdR5RNAdUAo9SFhtISGY4uMxBQZiXvfKLxjogjoF0NwfN8DVl7UFJSS9/tS6lesxm3TRnplphJdnr/n/lL/UAr7JdE0aChe48bQe9oxhA2MO6IPZNaWVgrWbqFi5XpaNm7CIz2NsJwd9C4v2PO8Wsxu5EfEUhU3AGtKCl7DhxI6aih+UeH4hgRhspjbPa/o2oq3ZJD1yDMoux3NZAKzGZQJZTKhWcxgMukXs1n/u9z9GMfXavfXZjPKbAKTfq0sFvqdfzrBcVFGP8UjptntVOzIoXRDGg1bt2HNzMQjaxf+hTlElBbg2/pnJa9NmSgJCqciIprGvrFo8f2xREeB3Y7W2gZWK/a2VrBa93yvtenXqrUNzWZF7f7e2gZWG6qtDWWzoqxWTFbHta0Nu8mMZrFgd3NHs1jQ3NzR3CxoFjdwd0dzcwM3/Wu1+9rdDdzcUe7umNzdUO5umDw8MLm7YW9tI+WWq9gw82zG/vQ/A3/if8/a0krBms1YPD1w9/PB3dcXD38fPHy8XFKFWVtYSsX2XdRl7qI5Oxd7Xj4WR/LPv7yE0OoyvNua99nOpkxU+AVTFdyL4Ipiwuoq2TDiWIJfeo6YiaOcGuPRyFu1ifJ/3czw9Yuo9vIjsKmOdc+8wcjbrzE6tENafd8zDHvy3r+85je6eVIW1Iva0Aiawntj6xOFOSYar/5x+Cf0IzQxHp+QwKOau6WhkcqdudTsyKYxK4/W3DxwHGT1KivBv6qMkAP8TQDUu3tTGRBCXVAYTWHhWHuFQ+/eWPr0xis2Gv+4aIIT4o46xs5kwysfMPzGS1n7xCuET5uEm7cXbr5euPt44+Hrg5un+1Hvt3arjcL1qZSuWEvL+k14pKcSmpVBn/3e++RFxlLVbyC2lEF4jxxG5MTRhCbEtnv+tuYWKnflUpuVT0NuPq15hdiKijAVF+NWXop3RRn+NRUE1VbiZW3ZN1YUNd7+1PgHUR8YQnNwGNbQMLSICMwREXhEReIT3Rv/mCiC4qK6VBs6W5uVtuYW2pqasTW3Ym1twdrchq25GWtLK6019bRWVdNWW4u1uhZ7XT32ujq02jpUQz2qvg5zQyPmxnrcmhpwa2rEo6kRz5ZGvJob8WptPuD7/N3vayv7DcCWrL+vDZ8wmvDk+E6d3HCG8p25tNbWY/H0wM3bC4uXB+4+Xrh7eTrlue/uhlCyYSt1W9Npy9iBJWsnfvk59CrN+8sJE+W+wZSFR1EfFYM1Lh63xAEEDEqk17AUPPx92HjPkwx871WCGmvYMOJYAp97krip4486zp5Ms9upK6ukansWtbuyac7Jx5aXjyoowL20BJ/yEgKrSgmpr/rLZ1yrMlHpF0x1UBgNIeG0RkSgRfZGFRUy9ufPyPjhdwacNM2gZ/b3Mn5cwICTp7PmoecZdf9NFK5PpejXRdiWryB46wZi8zNxs9sAPeFZlDQM6+gxhBx3LDFTxx9Vx4SqnEKyv59Hy8LFBG1YTb/c7bjZbdhRZPWOp3zYKCzHTibqpOMIT0444BjWllbylq+j7Pc/0FavITRtEzGFO/ecUFMcEEZhwiBaRozCb9IxxBw/Cb9eIUccc3clCULRnUiCUAiDSYKw56opKGXX1z/RsmjJPm/uALIi4igdMgrzpIn0PmkGkUMGHvaHrRUXXs+YT96gsbi0U7+Ra21sJv1/c7A3t6Lt/tDgeE3a/dqkdn9v1/56/1+2cQxst6M1NmGrrkarrkHV1mCqq8NSX4tbfR0ejfV4Ntbj3dSAT3PDXw6gHEiL2Y0GT28aPX1p9vKhxduXVh9fbD5+2Hx9sfv7g78/poAAzIEBWAIDcQsKwCMkEM/gILzCgvAJDT7kQWRrSytV2flU78yjISuXlvwC7PkFmEqK8SgtxqeyjMDqMoLr/5pIBKjyDqAqMJT64DBs7h6EZ2fQu7Joz/1FgeEUxyfRPHQ43uNG02faBELj+/7tcz9azbX15C9fR9WqDdg2b8Y7I53wvB2EO6phd7OjqPf0od7LlyZvP5p9/Gj19cfq74/NPwAtMBAVGIg5OAhLcBDuocF4hobgHR6CT3ioSxOMdqsNm9WKyWzG7CbLNzvTqpnnMPbXL2l088Bst6M0DZNmP2TFx+FaP2oqI9YscEKUrlVTVEbe78toTNuOLTMTj+xdBBbmElGWv+fsXtBPMCgOjqQyMprmvnFo/fvjlTSQ4KFJhA9J6tLtGVdPO50hf/xMQ8YOl59pfTRWnHcN4z9784D3NVvcabG40+LmQau7B21uHrS5e2L18MDq4YnN3RObpyd2dw80T080Ly80Ly+Ulxd4eqJVVGIpzMertIiAilJCqkv3+f2Dnvwr9w+hOrgXDWERtEb2gago3GKi8YmPI3BAP0ITYvacQNRUXcem2x5i0Ef/wautmbXHnUH8q88QOiDO5T+rg6nJL2bbv25n5E+f0mpxZ8uF1zL46ftpjIsnP2EIw9ctNCy2w7GzTwJubS0UXXI1nv1i8EvoR0hif/wjQztFQmB35VhlRha1O3JozsnDWliIKizEvbQEr4pSPZFYW4GntfUv2ze4e1EZEEptYOi+icSo3nj1jcavn55I9A0NMuDZtc/KMy9j+HcfQ001Hj7eHTr3n+991mPbtBnvjG1E5mYSVle55zHVXn4URPWnPiERhgwhYORQrM0tNOflYy0oguJizKUleJaX4ltVTmBt5Z4qlf1Ve/lR7R9CfVAIzSG99N9bRASWPr0xeXnRVlyMvagEU1kJ7uVleFWW41dTQVBd1QETyrvHrPEPpj4wlObgUNpCw9DCw7FERODWOxxlMmNvbcXe0orW1orW2obW2orW0qqfgNLWpl+3tkJrG1jb9BNP2lqhTT8pxdTW6jgJpW3PtXnPdRsmmxWz1VGx7qhcN9usuFnbsNhtWGxW3GzWv1Trtut3ZXGn0cOLZg9vWjy8aPHyodXLG6u3DzZvH+w+Pth9/cDXF+Xnh9nPD1OAP/aGBmybt+CdkU5EbuY+VWu1Hj4URMVTGz8QBg/Cd+Qw+kwYTWDfyCOOszPJW7mB8IljcLcd+OTIVpOFNosbbWb92mp2w+qo0LVa3LC5uWOzuGFzd8duccPu7o7dTb+Ym5vwL8z9SzcEO4rSwDAqwqNpiI7FHh+PR+IA/FMSiRiRclgnN9SXV7Hl9kcY9Olb+LQ0sX7CTCKef4qoMUOc9aPpMYq3ZKBNnkxkdclf7qvx9KUyMIy6kF40h0Vg690bU5/eePTti09cNEEJsQTFRR/wM1VNfjE+ffuw5uzLGf/5Wx3xVI7IinOvZvQX79CQW0BAVMRf7m+qriN7/h/ULPwD97WriUrfRK+6CkD/n5MVk0jN0JG4TziGqBOn0isp/oDzaHY7RZvSKfh+HtrSpYRvWUtMSQ4ArWYLO2OTqR41Du+pxxJ7ygwCIsOO+Dk1VdeRs2AZ1YuWYVm3lojtm4mqKAT0/S83PIaypCHYR44ieOrEo050dgeSIBTdiSQIhTCYJAh7prVPv8GAh+7Av7meFrMbO+OSqRkxBu+pk4k96bgDvtE8XJvf/ZwhV5zLlve/YvAlZzoxaudafe/TjHniLpfP0+DuRYOnD41evjQ7EnttPn7Y/Pyx+/mhBQSgAgIwBwViDgzAPTgIj5BgvEL1xJ5vr5AOP7B0KLY2K1VZeVTvyqU+K1c/e7ygEFNxEe5lJfhWlOLe0kR57ABahw7Dd/xooqdN7HQHJWoKSilcvpb6zWnYqqqgqhpVU42ptga3ulrc62vxaqjDq7EO36aGfaqlDsSmTNR7eNPg7UeTly92sxllt2Gy21F2OybN7vhav820+1qzOxJT+v3mPdf6/XsnqhrdPNg6YSYBN1/HgJOnd4qDwV1ZW3ML9SG92DV0PCOX//KX+zW7HbvNjq3Nit1mQ7PZsVkdX1vt2K1W7HYbms2Gvc2G3WZHs+tf5z3yNGO+/5iClRuIHjvUgGd3eOrLq2jq13/PQeNWs4Wi0D5URfalOSYO1b8/3skDCR6aQvighG7byix3xXqijhnFqnOvZvynbxgdzgFV5RTikRDPjoHDaTnrHOxNjWiNTWhNTajGJmhuQjU3oZqa9TXMmpswt7RgbmnGrbUZt9YW3FpbcG/TLx5trfucoLI7+VcVHE5jWAStkb0hOhq3mL749IshKLEfIfExR/Q3UJmVT8aNdzHi58+xms1sPvtyBr30WIcmeVoaGtlw5+Mkv/syPi2NerLyjf/bc6LKylMvYsRPn9KUV3hUB7hcKXvxamKnjGXlTQ8w7sWHjQ7nqGh2O7UlFVRnZlOXnUdzbj7W/AIoKsKtpPjPRGJNxQFPpFrz0AuMfvDmjg+8Hbb3G4TdZCZpxyajQ9mjOreIgj9WU7d2A6atWwnYmU5Uwa497eD21mJ2o9IvmNrAEBqDe9EaFoY9PAJT79549OmNd0wf/GOjCeoXdVTvUxuraqjalU9dTj5N+YW0FhbpycTSEtzKS/GqqsCvuoLAuqq/fS92MDZlwmoy02Z229PG2ma27Lm2mS16K2uzBZvFDbvFDZubG3azZa+KdTfsFsueKnXNccHdHSxu4O6GcncHN71KXbm5odw9UO5uWPz8sAT64x7oj0egP57BAXgGBeITHOC019WaglIKlq6hbt0G2LIFvx3bicrfuU+Sq8wvmOLo/jQMSMI0eDBBY0cQNWEUXoF+Tomho6ydOIuUVQvYcucjaDY7tLSgtbQ4rluhtQXV2opqbUG1tqFaWzC1tmJq231p09f5tLZhadOTwRZrK27WNtosblRG9qWpbyxav3g8EwcSNDSJ8KFJePo55zh8TX4xaf9+gGHf/Bc3Wxvrp59O3xeeJGLQgautXE2z29k+Zz41735I79V/UPXkswy54lxDYjlc68afQMraRWy84hYssTF4x/YlIL4vIQPijnod+i3JYwgsLyaqOLvTftbKCY+hLjiMQdvWHvY2xVszKfhlIW3LlhO4eT1xOel7lucoCQijYOBQWkePwWfUCBo3b8WyYjnRqev2JBZrPXzIGjiUxrHHEDhjKnEzj3XaPnEw1blF5M5bTMOylXhtWEffHVv2VO+2mi1kRQ+g9cFHOvUxJ1eSBKHoTiRBKITBJEHYs9QUlZF59sWMWjaX7XEp2J58mviTpjo1AVVTVIZf73BWXXQD4z98yWnjOtvaibOI2byKmi++0W9QynGlHN+bHFdqn+s9t+//eJPaM4a7nw/eYSH4hgZJxVc3YW1ppaGskvqSCprKymkuq6StohJrRdVfE4y1NSi7Dc1kRjOb9daVe329u5Xl7vsx/3k/jou2p62l2dHq0ow5N4dBf8zFu62ZXb3jKTvvYpJuvxb/8FCjfzxd0qY3/8fQa/7Jxlf/y7DrLnTq2OU7svFPTGDDjDMYO/dzp47tTCuvu4dxrz/J2sdeJuqUEwhL7Ndj/2etHzOd/ltWo3KyO2X1+4rzrmXsZ2+St3gVMZNHO2XM3evXttQ3OvVA9cEUrN1C8Q23MXLlPCp9Asi85lZGPHanS1sJanY7G154l/AnHqRPZRGbB43H75UXiJsydp/Hbf9uPgNPO57VD/wfYx6+1WXxHI0V513DmM/fpipjZ49Zn2dPC7kdOdTtyqU5N5++zzxCQcIghq/tvNWeTdV1WEKCWHvWZZ26CgX0LgXFW7ZTtnojZh9v/GKjCIyPwT88pNMdHG+uradyVx71eYVodjtmTw/MHh6YPdwwu3tg8XLH7OGBm4c7Zk8P3Lw8cPPw6LEt5DW7nbLtWRQvX0vjuo2Y01IJ2rmdqOLsPRW8e69d2Zw8iOSn7iegTy+DIz+4nGXriJo0hjWnX8y4r98zOpyjUr4jm50338vwuV+gKcWGk85jwAuPd1h7+qxFqyj+z3vEzPue3lXFNFvcaXL3osnDi+DszKNOtLlK2uc/kXzuSay44HrGf/SK08dfdcdjjH32fnIWr3ba+y1nylm2jpiJo1h1y0OMff7BIx6npaGRnAUrqPx9CZY1q+mdvmmf7j+l/qHkpYzEeswEwk6cTuyxYw3/X6rZ7RRvzaBw3hLaVqzEb8tGTI8/RtLZswyNyyiSIBTdiSQIhTCYJAh7jtRPviPkuisJra1gzcU3MPo/z7jsYGBWZD/qQsMZsmWFS8Z3huLAXhQkDmPkynlGhyLEYasvryL1uTcI+eQD+udn6lWFE08k8JbrSZg9tdMdzOvM1k6cRcK6P/CqKHNJi5rV089gyJKfaMzY2SnXImxpaKQmsi+lfWLbdQZyd5X500ISTprGymvuZNwbTxkdzj5q8osxx8ezffiEbvGalfHjAtpuvY2UzA3khUZRdtcDDL/lcqf//8r44Xdst9xK0s7NZEXEUf/4Uwy+7JwDPlaz2ykMi6Iisi9Dtq50ahzOoNntFIX2oSKyL4NTVxkdjqHWTD2V/qsXEVBTafjByoNJ/eQ7Ui44jU1vfMTQay4wOhwh9mFrs1K4bgvlK9fTvEFftzs0K4OosnzWTZrF6CU/Gh3iQa2dOIvk1Qto2p7ZqVuCt0fhxm3k33I3Ixf/QIvFnU1nX0rycw+7pJq9cOM2cl5/j4gfvyGuaBdWZSItZQwtZ/2DxGsvJGfeHwy68HRWXHQD4z982enzHy1bm5Wsfsn41VXjn73TJdWv5RlZBA+MZ9UlNzH+/RecPv7RWnHlbYx/5/8oSc046Bp/R6p8RzZFS9cQMmxQu5aYEcaQBKHoVjRNk4tc5GLgxdvbWxPdW3N9g7bizMs0G0rLDY3Stn//m8vnXHXcmVqNh49ma7O6fK4jUbhpm6aBtvKWh4wORYgjlvHjAm3VcWdqDW6emgbajt79tZW3P6rVFJcZHVqn11BZrTW4eWorZ5zlsjmy/1ijaaAtv+gGl81xNFbd+7Smgbb5/S+MDqXT2Jw0WivzDdaaauuNDmUfyy+4TtNA27VgudGhOI3dZtM2vvGRlhURq2mgpcelaKmf/eiUsQs3bdPWHDNT00Ar8w3SVt39lGZtbfvb7Zafe7XWpkxa+a5cp8ThTNu+/kXTQFv90PNGh2K4Vfc/p2mgZS1aZXQoB7X8kps1DbTq/BKjQxHisC0/63LNhtJ2/rbM6FAOKPuPNZoNpS0/63KjQ3GJ7KVrtbVjZ+j/Ozx9teVX3qY1VFYf9bjlu3K1lf9+WNvWb7CmgaaBtq3fYG3lLQ9pZTty/vL4teOO15os7lrB+tSjntvZdr93XfPEqy6dZ1vcIC0zKsGlcxyp7X2TtO19k4wOQ3QCQIPWCY4py0UuzrjI6QhCCOFCWYtWkT9gKOO+fo81x59FSMZWBpw83fUTH3MM/i0N5C5b5/q5jkDB93oFRuiJxxkciRBHLmH2VMbM/wpbfj6r7nwczaQY++z9WKKjWD3tdDJ++B3Nbv/7gXqgtLf+h3dbM74Xu66yI2biKDYOnUjiV/+lqbrOZfMcCVublYi3XmVHVAKDLuqZ63YciLrrLkLrK9n0hPNbVh2pmqIyUr78gPWjphI3dbzR4TiNMpkYes0FROdmsua+ZwkqLyb53JPYMHIKOUuPrKK1rrSCFf+4iuARQ0hZvZAV51+LZ9ZOxjxx52G1zo246mIsmp0dr31wRPO7Us27H9BidiPx2ouMDsVwfU6dCUDJT523mtZn7UqyIuI6datGIfaX/OIT1Hv6UHfr7UaHckDld9xHs5sHA595yOhQXCJmwkhGrpzHjl8Wkz1wKOPffo7GvnGsvOUhWhrat/5mfXkVax59ic2DjyEgPpax//cgHo31rLji3xSuTyVx52bGPv/gnnV49xb17qvYlaLkiuuc9dScoq60gvgXn2Rb/BBG3nmtS+eqnnkS/fMzKdy4zaXztFdJWiYDcrdRcXzPbKkphOi+JEEohBAuYLfaWHnj/fQ+bhJBNWVsfPW/jP31C7yDAjpk/sgTpwJQ+mvnXB/GtuQPaj18iN1vDSIhuiK/XiGMfeoe4nPSyfxxAVsnz2bQ0l8YcMpx7OqbyKo7HqOutMLoMDsV8+efUeofSuI5s106j9udtxPUWMPmJzpXm6bNr31I37I8am68VdoH7SXlgtPI6JtEn7dfwdZmNTocANLueRz/lgYCHn/Y6FBcwuxmYfSjt+GXm8WKK/5N/9Q1RE0ey6rjz6Y8I+uwxrC2tLLqjsdo6xfP+C/eZvOEE6jdtIXxn7yOb2jQYccSN3U82RGx+M/56kifjku0NbeQsHguqSMnd8r1MTta7+FJlPqHYvnjD6NDOSBbm5V+GZspHdb51q4S4lAC+vQi7eJ/MXTzclI//d7ocPaRs2QNw1fMY9NpF3TKtu3O1P+EyQzduJT0r+dSEhnLuBcfpioqjjX3P4e1pfWg27U0NLLh5fdZP3YGlsgIRj9wM2H5u1hzzhVkLVhOXNEuxr/9HL2HJx9y/vDkBDZdcgPD1y9i8zufOfvpHbHUf91BSEM1lpdedPl71+jLzgcg991PXDpPe2W/8z8Ael98rsGRCCGEc8kahEIYTNYg7H5Kt+2k5MzzGLxtDRuHTiTq608OeHagK2l2O9V+QewYM4XRC7/r0LkPR3ZEHDXhvRm6aZnRoQjhEnWlFaQ99x9C//ch8QWOtQonzdLXKpw1pUcnhWryi/GKiWb9qRcw7pv3XTqXZreTGTcI74ZaIouyD6uKydU0u53MfoPwrq8hsiinU8TUmWx48V2G33IF6555g5G3X2NoLHWlFdhjYtmVPJLh6xYZGktHqcopZPsNdzLip8+wms1sPvtyBr302AETfZrdzua3PyPwwXuIKckhNWE47i++QMKsY494/hWX38r4916gePN2IgYPOJqn4jSb3/mMIVeex4aX3mP4jZcaHU6nsHbCicRsWU1odVmnez3bOe8P4k+YzNonXmHU3dcbHY4Q7dJcW09tdBxVwb0YsHNLp9m/1o0/gcT1f9CasYOgmN5Gh9NhNLudrf/9Gs8HHyAhN528sGhKb7+H4bdciclixtZmZdun39P44cckLpuHf0sDlT4BZB57IgGXX8zA044/ot9hS0MjpbEDQSnCszNcslZ3e+St2kT4MaPYOHk2YxbO6ZA5d/ZJoNXLm6QdmzpkvsOxNWkU/hWl9C3NNToU0QnIGoSiO+kc7zaEEKKbWPfcW3iMGEb8js2suutJhq5f3OHJQdBbh+UkDCE8dUOHz/13qnOLiC3JpnFM92nVJsT+/HqFMPaZe+mXm07GD7+zddIsBv3xMwNOnu60qkJrSyt1pRWUZ2RRsHYLu35fTvqceWz96Ft2/LLYSc/E+ba//iHudiuhV13i8rmUyUTd9TcSVVHAptf/6/L5Dkfq/75nQM42Cq64TpKDBzD0+kvIDYsm8KXnDW/Rm3rPEwQ01+P7aPesHjyQoJjejPv+I8pWrWfbyGMZ97/XaY2NY9Vtj9DW3LLncTvnL2ProHEMveafmGw2Nrz8Psnpa48qOQgQffUlAGS/4dqTB9qj9cOPqfXwIfnyfxgdSqdhmzSJsLpK8tdsMTqUvyib+zsAUScdb3AkQrSfp78v2TfczsDsNDa88oHR4QCQvXg1w1fOZ/PpF/ao5CDo7yMHX3I2/bNS2fDSe9jMZkbecS1ZMYmsmn0+lSHhDLr4TFKW/sL2sVPZ/M5n+FeUMvanT0k8Y+YRJ3g9fLypfOIZosvyWH/rg05+Vu1Xfu2NtFnc6Pf2ix02Z+lxsxi4cwvlO7I7bM5Dqc4tInH7BgqmzjQ6FCGEcDqpIBTCYFJB2D3UlpSz/ZxLGb3kRzJikvD67BOixw03NKYVV97G+Hf+j6rsgk71YW7DKx8w/MZL2fbFTySdLf37Rc+hVxW+QegnHxJfuINGN0+2Tp6FPTERraEBGhsxNTSgmpowNTdhbmrE0tyEW3MTbi3NuLc249HShGdbC56tzbjbD96C0Y4ib+kaYiaM7MBneHhSB47Ev6qMqOLsDjkz3trSSmnvWGoDQ0ncudnl8/2dzYPH0zt7O76FeXj6yUmnB7L6gf9jzKO36ZVbBiVl6sursPaNIWfAEIZuXGpIDJ1Bxo8LaPv37aRkrCcvNIqSm27HvnARoxbModbLl/SrbmHEE3c7tbohIyYZk91G/7ztThvzSDVV16H16sXWSScy5vdvjA6n08hZupaYSaNZfe/TjHnsDqPD2ce6ccfTJ30jEdWlRocixBGxtrRS0DcBgD65mVg83A2NZ/3YGQzcuIy2zJ0E9o00NBaj2dqsbHjmDSKff5KwmjJSh03Efu55pFx1Pp7+vk6fb8OIYxm4dTX1m7bSKyne6eMfji0ffMngS89hxVW3M/7NZzps3qyFK4ibdgyr7nycsU/d02HzHsyah19g9EO3kvH9bww4ebrR4YhOQCoIRXciCUIhDCYJwq4v7fOfCLrmcsJqyljzz38x6u3ncPP0MDosUj/9npTzT2Xj6/9l2LUXGh3OHivPuJTh338CNdV4+HgbHY4QHU6z28n8aSHVL77GoD9+xrtNr8ppNVlodvOg2d2TVndPWjy8aPPwpM3TC6unF3ZPL2xe3mheXth9fMDbG+Xjg/L2xuTni9nHB7OfD0opEq++kPWzz2Xc9x8Z/Gz3VbptJ6HJCay66HrGf9hx6wKuvOUhxr34MOlfzyXxDOPO/N3x6xL6zzxWP4HjrWcNi6Oza21spqp3X8p7RZGSsd6QGFZecyfj3nyG7XPmMfDUGYbE0FnsbiUa8NC9xBZn02q2sP6UC0h65WkC+vRy+nwrb7iPca8+Tt6K9YafbLXumTcYeee/2PrRtwy64DRDY+lMNLudKv9gdo6cxOjFPxgdzh6a3U5ZYC9yB49m1LK5RocjxBFb/9J7jLj5clbf8xRjHr/TsDiyFq0ibuo4Vpx7NeM//Y9hcXQ2tjYrbc0tLj/Rq2BdKqFjh7Nl7HRD/qdZW1rJjxmIxdpKWE5mh3521+x2Cnr1pTIymiFbVnTYvAezYeRU+uzYSmhFMSaL2ehwRCcgCULRnUiCUAgnU0rNBF4CzMA7mqY9dajHS4Kw62ptbGbdpTcy9ot3KAyJpP6d90k8rfO0M2qqrsMtOJA1/7iyU32gy4hNxurmTnLmRqNDEcJwzXUNtDY24RXg59QTC9ZOOJEB6/7AUlSAd1CA08Y9Wiuvv5dxrz3R4Qf+G6tqaOsdxa6UUQxfu7DD5t3fumNmMnDdH9iyswmIDDMsjq5g5Y33M+6Vx0j/+hcSzzihQ+durKqhOTqG/LikTnFQqrOwtVlJ/fArQkcOoffwZJfN8+eJBDcw/sOXXDbP4dg4fDK9d6YRUlEsLYH3s37MdCIzU4msKjY6lD0KN6TRe0QKq/79CGOfu9/ocIQ4YprdTkb/IQRVlOCfl+WS6rTDsX7scQzYuALbzp0EREUYEkNPt+LC6xn/8Wuk/u87Us47pUPnXnXrw4x94SE2vPw+w2+4pEPnBlhx9hWM+uYDGvOLDH3f3FRdB2FhbD7+dMb+9KlhcYjO5XAShO09PiyEUWQNQiGcSCllBl4DTgSSgfOUUq47giIMk7N0LbkDhzD+i7dZO/10grandqrkIIBXoB9ZUQn4r19jdCh7NFbVEJeXQc2ocUaHIkSn4Onng394qNOrjr1vvB7/lga2/t+bTh33aIV8/zWZfRM7vCrIOyiAtNMuYOi6xeStNGZt1oK1Wxi2cj5bTjlPkoOHYfCD/6bay4+mx57o8Lk33/8MwQ01uD9s/Lo/nYnZzcKQK851aXIQoFdSPNsShtH7lzmGrkNZnVtEyubl7Jp+kiQHD6D1mIlEVpdQtDnd6FD2KPhxPgC9Zh9ncCRCHB1lMmF74kl61Zaz8Y5HDYlh1+/LGbH6d7acdbEkBw007JUnKQoMx/vWm7G2tHbYvNW5RSS+8Rxbk0Yx7LqLOmzevQVf8A/c7DYy3zU2Kbftwy/xsrbg84+zDY1DdC1yfFh0JZIgFMK5xgA7NE3bpWlaK/AZcKrBMQkn0ux2Vt36MOFTJxBSWcqGl99nzG9f4xMSaHRoB1Q5ZCRxWam0NbcYHQoAu35cgJvdhve0Y40ORYhuLensE8mKiCP4w3cNPcC+t7yVG0jI207FyWcYMn/CY3djNZspfOhJQ+bPv/8xbCYTCY/fa8j8XY1PSCDbzr6E4RuWkL14dYfN21xbT/8P32BL0mhD29H2dPVnnEVMaS67fltmWAzb3/gQN7uN0KsvNSyGzixstn5iXP53vxocyZ/sfyyl1sOHmEljjA5FiKOWfO7JbBpyDMkfvk5NQcevqVl99/3UeXiT/PQDHT63+JNXoB9FDz1BXHEWa+98vMPm3X7tv/FtacTn9Vc7ZM3wA0mYPY1S/1DMc741ZP7dbF9/Q42nLwPPmW1oHKLLkePDosuQBKEQztUHyNvr+3zHbaIbKM/IYsvQiYx94SG2J4/CtmmjIa022sM8aSLebS1kL1hpdCgA1P22EDuKuFN69npOQriaMpkovfAy+udnkPmTcS0195b/xvvYUfS77jJD5g+N78vGKacw9PfvqMjK+/sNnKh8Zy7D5n/LxqmnEjogrkPn7sqSHr2bRjdPyu5/pMPm3Hj/M4TWV2F+UKoHjTTg2ktoM5kpfee/hsXg+/UX5IZFE3/8JMNi6Mxip4yl1tMXbfESo0PZI2LzGrIGDpX1oUS34ff8s/g2N5B28z0dOu/O+csYsWYBqWdeItWDncDwGy5hc8pYkv/zfx3yHjZ78WpGzv2ctSecTdyUsS6f72BMFjNZE48jcdMKvc2nAdqaWxi4ZjEZY6Y4veOL6Pbk+LDoMiRBKIRzqQPc9peFPpVSVyml1iql1lqt1g4ISxytDS++i3n4MBLS17Pq9kcZsuEPQvvHGh3W34qaNR2Ayt8WGRuIg9+aFWT16Y9/eKjRoQjR7aXccR0N7l7U/J+xa3iBXn0dNXcO2waOoFdSvGFxhD98D57WVjIeeKZD582893HcbFYiH5XqwfYI7BvJ5lnnMHzp3A5pY9hc10D8+6+ROmAEyf+Qs8SNFBTTm7SUMcTO/8GQKujirZmkZG6g4MTTDauc6OzMbhZ2JQ4ncnPnaGVflVNITEkOjWPGGx2KEE7Tb/oxrJs0i+FzPqIkLbPD5q29+z5qPXxIekZOlukMlMlEwJuv4dXWzM7Lb3TpXJrdTt2/bqDBw5uBb77g0rkOh8+55+BlbWHbh18aMn/6Zz/i31yP5UxjOqCITs2y+7iu43LVfvcf1vFhIToD+bQjhHPlA9F7fR8FFO7/IE3T3tI0bZSmaaMsFlnTpLPb/O7nDL/lCspDIilbspyxz9zXZQ4Whaf0p9QvBPMq4ysI25pb6LdjC+XDRhsdihA9gm9oEFunncyQpb9QnVtkaCw7fvmD6LI8Gs40du2OmAkj2ThsEonf/LfDzkSuK60gZc4nbBwzrcPXXuwO4p64D7sykXu366sINz38f4TVVaLdf7/L5xJ/r/XsfxBZXcL2OfM6fO7s194FIPq6yzt87q6kefwEosvyKM/IMjoUcn74DYCA46YaHIkQztXn5WcxaXZybrizQ+bbOe8Phq9bROrZlxLQp1eHzCn+XsyEkaw7/WLGLJzD9u/mu2yeTa9/zOC01Wy75t8E9o102TyHK/Gc2VR7+WH/6htD5m/8/EuaLB4kXnSWIfOLTs26+7iu4/LWfvcf1vFhITqDrnGEW4iuYw2QoJSKU0q5A+cC3xsckzhKTd/MocHdi77b1tN3/Aijw2kXZTKRnzSMPmkbjA6FrPlL8W5rwTJF1h8UoqP0uuNmPGxtpD/1sqFxVLzzAa0mCwP/ZfxaXu533UlQYy2bn+iYysrUB5/Fv6UBvwekevBIhCcnsHHybIbO+9qlbbVaGhqJfedVtsUPIeX8U1w2jzh8iVdfQLPFnZr3PurwucN++IaMmCSixgzp8Lm7kuATjwMgpxOsQ9i8aDGtZgv9Zk0xOhQhnKr3sCTWzz6PkQu/I2fpWpfPV3v3A9R6+JD8jKw92NkM+s9zlPkFY7rhBmxtzu9E1dLQSOhD95ATHsPIJ+92+vhHwuLhTsbYaQxYs4jWxuYOndtutRG37DfSh47HK9CvQ+cW3YIcHxZdhiQIhXAiTdOswPXAr8A24AtN01KNjUocrV7rV7FzwFA8fLyNDuWItI4ZR2R1CWXbdxkaR+UvvwMQI+sPCtFh4qaOZ1v8EKK++C92q82QGGxtVvr99gOpQ4/pFGeiJ519IhkxSfR57w2XHFzZW0tDI/GfvM3WpFEkzJaqliMV/tj9uFvbyLjvSZfNsfHRFwmvKcN6X9fpEtDd+fUKIW34ROIXz8Xa0tph8+YsWUN8QSaVp0q1wN/pd/wkGt08sS5YZHQoBK5fzc7YZDz9fIwORQinG/jyUzS5eVJ50+0unWfHr0sYvn4Rqf+4jIDIMJfOJdrPNzSInLsfJiFvO+se/D+nj7/htkeIqiig5vGnO9V6e+5nnYl/SwPpn37XofNm/rSAXnUVWE89rUPnFd2DHB8WXYl8+hXCyTRN+1nTtAGapsVrmva40fGIo1OVU0hccRYN4yYYHcoRC5w+GYC8nxYYGofHyuXkh/QmdECcoXEI0dM0XH4VURWFbP3wK0PmT//iJ3rVVWA/91xD5t+fMpmou/5moioK2fzahy6da9MTr+gtK++4w6XzdHcxE0aycdQUUr75iLrSCqeP39rYTMybL7E9LoVBF53p9PHFUTj3PELrq9j26Q8dNmXhG+9hUyb633BZh83ZVVk83NkxYCi9Nq42NI7m2nr65aRTPWKMoXEI4SpBMb3Zcv6VDF+/iPRvXFexW3f3/dR6+pIi1YOd1sg7/0Va/6EkvPQENfnFThu3fGcug957hY1DJzLk8n84bVxnSLzoDBrdPGn6vGM/y1R+8gVtJjMDLjuvQ+cV3YccHxZdhSQIhRDiELK/nQtAkKOFUlcUN2MiLWY3Wv9YalgMmt1ObPoGigaPMiwGIXqqwTdeSoVPILbXXjdk/voPP6bRzZPkqy4wZP4DGXbTZRQGR+L18osum8PWZiXy7dfYETVAkk5O4Pvgffg315P60HNOH3vj4y8RUV1Kyz1SPdjZJF9xLvXu3jR99EmHzKfZ7cTM+460pFGE9o/tkDm7usZxE4grzjJ0rdudPy3A3W7Fa+pkw2IQwtWGPPcQ5b5B2O+8E81ud/r4mT8vZviGJaSeezn+4aFOH184hzKZ8PzP6/g31ZN+5c1OG3fXVTfjbm0l5D/GLktwIJ5+PmwbMYn4Fb+7vPvHbprdTtSiX0kfOKJTdEARQghXkk/AQghxCC2/L6TJ4kG/mVOMDuWIefh4sys2iaBNrl+z4mByl68nqLEWJk4yLAYheioPH28yTjqHIRuXUrwlo0Pnbm1sJvGPX0gbM7VTrd1hdrOQd/HVJGZtJf2ruS6ZY9MrHxBdlkfNjbdI0skJBpw0ja1Jo4j/+C1aGhqdNm5bcwtR/3mJjL5JDL7sHKeNK5zD09+XbWOnkrh8vlN/7wezfc58elcW0XRW56qe6MwCTpgOQNacXwyLofa3RQDEnSxt7EX35RMSyM6rbyF5xyY2v/U/p4/fcK+jevCp+5w+tnCuftOPYe2scxn165fsnPfHUY+345fFjFowh/WnXUj0uOFOiNAFzjid0PoqMubM65DpcpeuI7osj8ZZJ3XIfEIIYSQ5WiGEEIcQtn4VOxMG4+7taXQoR6Vm2CjicrfTXNdgyPzFP/0GQMRJcuBGCCPE3nMLStPIeuKFDp037f0vCWiux+3CzlM9uNvg+2+m2suPpiefcfrYmt2O70vPkx/Sm2E3X+H08Xusu+4mrK6STY+/4rQhNzz5Gr0ri2i8625J5HZS7hf+E//mera97/rWYjXvfkiL2Y2k6y52+VzdRb9ZU2i2uNNi4DqEPqtXkh0RS0BUhGExCNERRjx2B/khvfF/5EGnVlJl/rSQYRv/IPXcK6R6sItIfPMFar38aP3X9UdVUarZ7bRdfyPVPv4kv+7898TOMvCyc2k1W6j55PMOma/ww08BiLv8nx0ynxBCGEk+BQshxEHUFJQSV7iTurFdd/3B3dwnTcDdZiV7/tGfYXgkzEuXUu4bRNTowYbML0RPFzkkkc3DJjLgh89obWzusHltn3xClbc/yRd3vhab3kEBbDv9AoauX0zeyg1OHTv1k+8YkLuNgitvwOxmcerYPVnKBaeR2TeR3m+94pQDo9aWVnq/9jw7ohIYerUcAOqski88gypvf6yffurSeawtrSQs/pnUkZPx6xXi0rm6Ew8fb3b2G0ToupWGzG9rsxKXuYmSoaMNmV+IjuTm6UHJHfcRV7SL9U+95rRxG++9n2ovPwY9LdWDXUVAn15k3nIPSTs3s+6JV494nPXPvUnSzs3suOnuTp0c9g0NYtugscQs/tUlLXb3F/rbXDJikuiVFO/yuYQQwmiSIBRCiIPI+vYXTGh7Wid1ZX1nTwOg+rfFhszfZ+tacpNHSHWGEAYyXfcvQhqq2fLq+x0yX0NFNUlrF5Ex6QTcPD06ZM72SnjkLtrMFgoffNKp49qffppy3yCG3nODU8ft6ZTJRP0ttxFVUcDGF9856vE2PvsfoioKqLtdqgc7MzdPDzImnkDymoU0VtW4bJ60j74huKEG9U9JFrdX7ZjxxBXsoLakvMPnzl60Cr+WRsyTpI296BmG33oVO6IGEPX8k05pvZzx4wKGblpG+vlXyskRXcyoh/5NRt8kYp9+mLrSinZv31RdR58nHmRnnwRGPvRvF0ToXK0nn0ZkdQk75y916TzFWzNJyE2n4vjZLp1HCCE6C/kkLIQQB9H8+wJazG70cyTXurLQ/rEUBEfisbrjz+4u3ppJZHUJreOO6fC5hRB/GnTJ2RQER+L1zlsdMt+2tz7Gu60Fv0s7b6u+0Pi+bJp6CsN+n0NFVp5Txtzxy2KGpK4i8/wr8PTzccqY4k9Dr7+E3LBoAl96/qjOILe1Wen18nPs6h3P0H9d5MQIhSv4XnIB3m0tpLlg3a3dWv77MbUePiRfLusPtpffjGmYNTtZ38/v8LnLf/0dgD6y/qDoIUwWM02PPkFkdQkb7j76E5yaHNWDKU/c44ToREcyWczw2qsE11eRes1t7d5+4033EVFTRstz/9clOl70v+J8bMpE2X8/c+k82e9+AkCfS89z6TxCCNFZSIJQCCEOImTdSnb0G9RtDvAWJY8gevvmDmnJsbf8H/SFxENP7PqVmEJ0ZSaLmbxzLiI5cyNZi1a5fD63zz+jOCCMxDNnunyuoxHxyD142NrIeOBpp4xX8/AT1Hl4k/LIHU4ZT+zLZDFTcvWNxBdksuX9L494nA3PvUnfsjyq/32XfoBNdGqJZ82i1C8E8xeuWXuoqbqO5FULSJ9wPB4+3i6ZozuLP2UGbSYzjfMXdPjclmXLKAkII2LQgA6fWwijDL7kTLYkjWbguy8fVeXu9u/mM3Tzcrb98yqpHuyiBpw0jbXTT2fkdx+Rs3TtYW9XvDWTYf97k3VjZ5B87skujNB5gmJ6kz5gOJG/z3XpPH5zfyQnPIa+40e4dB4hhOgsJEEohBAHUFtSTr/8TGrHdp+qN9vYsYTWV1K0Kb1j512yhHp3b+Kmd/21HIXo6gbedSMtZjdKn37BpfNU5xaRvGUlWTNO6fTJl77jR7Bh+GQSv/mIpuq6oxorf/Vmhq36ja2nnN+p13Hp6obefT2l/qGYnz6ypK7daiPspWfJjohl2I2XOjk64QpmNwu7ps0mZdMyaorKnD5+2lsf49PahPelUk16JLwC/dgZk0TQ2o7tVKHZ7USnriNf2tiLHsjzuWcJaqwl9dYHjniMlvsepMrbn8FP3uvEyERH6//2SzS5e1F71b8O+2Tg/GtuQqHR+62XXRydc9XPOpnYkmxylq1zyfjVuUUMzNhA4dTOfYKjEEI4k7yLFkKIA8iaMw+zZsfv+O5T9RZ6/BQACn/+vUPn7bVxDbsGDOkSbUuE6O6CYnqz+ZjjSVnwA/XlVS6bZ/vr7+Nmt9Hrys7bXnRvnnffSVBjLZsfe/Goxim4/zFsJhMJj0mbLldy9/Zk10VXk5K5gfRvfm339htefJeYkhwqbrmj0yewxZ+Cr7gYd5uVjNc/dPrYps8+o9QvhKR/nOT0sXuKqtHjic/Z5tJ1IvdXtHk7veoqsB7TfU7oE+JwJcw6lnXjjmfol+9RviO73dunz5nHkK0rSL/ganxDg5wfoOgwwXFRbLvudgZvW8OGF9/728enf/MLo5bNZeM5VxA5JLEDInSe2CvOB6Dwg09dMn7Gu59i0eyEXiDtxoUQPYckCIUQ4gAaf1tAq8lC/MnHGR2K08QcO5YGdy9sy5d32Jw1+cXEFWfRMGZ8h80phDg0v1tvxLe1kdRnXnfdHN98SU54DP2O6xqVw4lnzmR7bDJ9PvgPtjbrEY1RviObYb/PYcO00wgdEOfkCMX+Bj90G9VefjQ99kS7trNbbQQ//zQ5vfoy7JYrXRSdcIWEWVPID+mN5zdH3lr2QGryi0nZtJxd00+Sk5mOgvdxU3Gz29j1Q8ediFb4o77mYdiJ3ef9uhDtEf7Ss7jZrOy84a52b9t6/4NUeQcwRNYe7BZGPn4Xu3rH0/uRew7ZEcNutWG+5RZK/UMZ8mr73kN1BuHJCWTEJBEy72eXjO/2w3cUB4TR/8RjXTK+EEJ0RpIgFEKIAwheu4Kdccl4BfoZHYrTWDzcyYpPIWSza9pxHEjWd/qBm4AZUztsTiHEoQ085Th2RCXQ6+P3XLImafHWTJIzN1J44uldpuWbMplouOEWoioK2fTKB0c0Rua9T+Bms9L70fucG5w4IJ+QQLadeTHDNywhe/Hqw95u42sfEle0i7Ibb5NkUBejTCbyZpxC8ra1lO/Mddq46a99gLvdSujV0m72aPQ79XhsykRdB65DaPtjKbUePsRMHtNhcwrRmUSNGcL6489kxLyvyF+9+bC3S//mV4ZsXcn2C6/GJyTQdQGKDmPxcKf5+ZeIqClj43V3HvRxax99iYTcdHLveADvoIAOjNB5Kk44iQG52yjekuHUcRurakjaspKciTO6zGcYIYRwBvmPJ4QQ+2moqCY+J53q0d2v6q1u+GjiCnbSUFHdIfM1L1xEq8lCv1mSIBSis1AmE5UXXk5c0S62H0F7xr+T/bre2qjvdZc5fWxXGnrDJeSH9MbnlRfanTitLSln0HefsHHMdKLHDnVRhGJ/SY/fQ6ObB2UPPHpYj9fsdgKefYq80CiG3Xa1i6MTrhB5zaWYNTs7X3vfaWP6fv0FuWHRxB8/yWlj9kR+vULYFZWA/6qO61QRvnkNWdLGXvRw8a88TZvZQslNtx/2Nq0POKoHn5Tqwe4k+R+zWTvhREZ+/vYBE8b15VX0e/5x0uMGMfLu6wyI0DmiLj0PgOx3P3HquOnvf4mntRWfc8926rhCCNHZSYJQCCH2s+u7eVg0O74zphkditN5T5mEWbOT9fPCDpkvcP1qdsUm4env2yHzCSEOz6Dbr6HOw5v6F19x+tihP3xNRkwSfUYNdvrYrmR2s1BwyTUMzE4j/etf2rVt2oPP4tfSiN+DUj3YkQL7RrL5xHMYtnQuRZvT//bxm17/mPiCTIqv/zcWD/cOiFA4W+yxY8iKiMP/+2+cMl7x1kySMjdS0IUqnjuzihFj6b9rKy0NjS6fqzq3iNjibJqkjb3o4UL7x7Lp7MsYuXIemT8v/tvHp3/zC0NSV7H9omu6bAWZOLiYd1+lzexG+VV/TQBuue5OQusrMb38Upd+zYseN5zsiFj8fv7BqePav/mGKm9/Es+e5dRxhRCis+u6rwhCCOEi9fMXYFUm+p16vNGhOF3srOkA1C36w+VzNdfW0y8nnaqRY10+lxCifbyDAkidcTpDVsynIivPaePmLF1L//xMKk89y2ljdqQh991Mlbc/zU89c9jbNNc10P+Td9iSNJqEWbJeSUeLffx+NBS59xy6ilCz2/F55gkKgiMZfte/Oig64Qols08naedmp7QWy379PUxoRF93uRMiEx7Tp+Jha2Pnj64/ES37h98A8D9uisvnEqKzG/TCo/r7l9v+voqw7b4HqPAJZMgTd3dAZKKjhQ3sx5bLb2LYpqVs+s/He24vWLuFkV+/z5pjT2bASV3/ROjiabNI3LGJyqx8p4zX2tjMgLVL2DFmipxEJoTocSRBKIQQ+wlcs4KdMYndcj2GgD69yAmPwXvtKpfPtfOH33G3W/GeJgfMheiMIu+6BXe7lYwnX3bamEVvvI9Nmeh/Xddcy8sr0I/tp13A0PVLyF2x/rC22fzkq4TWV8IdB1/vRbhOxKAENk6ezZBfvz7kQaLNb39GQt52Cv91qxz46eKir7kEgOzX3j3qscK+1yueo8YMOeqxBMSdegIANfN+d/lcTYsWSxt7IRz8eoWw/fIbGbxtDVs++PKgj9v25c8M3raGzIuvlerBbmzEcw+S06svIffdSXNdAwCl19xIm9mNuLdeMjg65wi7+FzMmp0d7/7PKeNt/+x7/FsacDvzTKeMJ4QQXYnSNM3oGITo0Xx8fLSGhgajwxAOTdV1mEOCWXfGxYz/8h2jw3GJ1dPPYODy+fjVVWOymF02z4pLb2bsBy9Tl19MQJ9eLptHCHHkUhNHEVyST6/S/KNew0mz2yno1ZeqXr0ZnLbaSRF2vIqsPHwT4tk47VTGzjv4QTYAW5uVwt5xNHv50D87rUu3a+rKcpatI3riaFadfw3jP3n9L/drdjsZ8YPxr64gtCgHN08PA6IUzpQRm4zJZqV/3pFXEeYsWUPMsWNYeeP9jHvpESdG17NlRfajLqQXQ7audOk86fF6Ujdx51/X2RKiJ2ppaKQyqh8NvgH0y0o74Oe8rcmjiczdgXd+Ll6BfgZEKTrKlg++ZPCl57Dy8lvxnTyBQRefyYor/s34t58zOjSn0Ox2isKiKIuKY+imZUc93qqZ5zB4wfeYystleRRxWJRSjZqm+RgdhxDOIEcxhBBiLzt/+E2vejuu67fdOBh1zDEENNeTt3KDS+fxXb2CnMg4SQ4K0Ym1XHk1kdUlbHnn06MeK/PnRURVFNB85jlOiMw4IXHRbJx2KsMWfEf5ztxDPnbTy+8TXZ5P7Y23SnLQQDETRrJx1BRSvvmIutKKv9y/9YOvGJidRu41N0tysJuoPOVM+udnHnal74EUvumoeL7hMidGJkpHjKV/xibamltcNkdzbT39srdRNWKMy+YQoqvx8PEm/+a76J+fwYbn3/rL/Wmf/8SgbWvJvORfkhzsAQZfcjbrR01l6IevEXDbzRQGRzL8+YeNDstplMlE7rEnkJS6mtqS8qMay2610W/576QPPUaSg0KIHkmOZAghxF7q5v2OTZnod/oJRofiMr1m6q2YSn9d5LI5rC2t9NuxhdKho102hxDi6A2+7iLK/IJRb7xx1GNVvv0BrWYLA/51sRMiM1bkw3fjZrOy44GnD/oYzW7H7+XnyQ/pw7CbJMFgNN8H78O/uZ7Uh/Y9M16z23F7/DGKA8IYfv/NxgQnnC7+ukuxoyh4/f0j2l6z2+n7yxzSkkYR2j/WucH1cJYpx+Ld1kzW/KUum2PX3MW42614TZE29kLsbcQ915MV2Y/wZx77S5Jee/BByn2DGPrYXQZFJzpa5Luvo9CILsuj5P5H8fTrXsVOgeefg7vNSsZ7nx/VOBk//k5YXSX2U09zTmBCCNHFSIJQCCH24r9qObuiEvDrFWJ0KC4TPXYYNZ6+aMuXu2yOrN9X4NPahFkO3AjRqbl5erDj1PMYvGUlBetSj3gcW5uV+IU/kTpsIgGRYU6M0Bh9x49g04jJJH77EY1VNQd8TOrHc0jITafgyuuPuj2rOHoDTprG1qRRxH/8Fi0NjXtuT/14Dom7tpBz1Y24e3saGKFwprCB/dg2YDh9fv0OzW5v9/bbv/+NPpVFNJ31DxdE17PFnD4TgMqf57tsjprfFgIQe/JxLptDiK7I7Gah9oGHiaooZP19z+y5PfXT70nZvo4dl14n1YM9SOSQRLbc9zQrT/onw27smuuDH8qA046n3DcY05w5RzVO1Sdf0GYyk3Dpuc4JTAghuhhJEAohhENzXQP9d22lYuQ4o0NxKZPFTPaAIYSnHnlbrr9T8ctvAPQ95XiXzSGEcI5+d9+EXSlyn3rhiMfY9tkPhNVVop13vhMjM5bX3XcS2FTHlsdfOuD92tNPU+4bzNB7bujgyMTBaHfcRVhdJZsef2XPbabHHqXUP5RhD95qYGTCFerPOJu+ZXnsPIJKtZp3PqTF7EbSdV2/4rmzCe0fS15YNJ4rjn5NqIPxXrWc7PBYAvtGumwOIbqqIVedT1r/ocS/+QINFdX6jQ89TLlvMMMeu9PQ2ETHG/3gzYz74eNu2QrfZDGzc8J0Ejcspbm2/ojG0Ox2ohb9SnriSFkaRQjRY3W/VwghhDhCu35agIetDc/p3Xf9wd0aR40lpiSHmoJSl4zvvmIZhcGR9EqKd8n4QgjnCU9OYPPIY0n8+Uua6xqOaIzGDz6i3t2b5CvPc3J0xhl4xglsj0sh6v3/YGuz7nNf5s+LGZy2mszzL+927Zq6skEXnU5m30R6v/UKtjYrqZ9+T3LmRnZddh0ePt5GhyecbOC1F9NmMlP+9oft2s7a0kr/RT+TOnJyt+4YYaSiIaPpl77hL/87ncHWZiUuYzMlQ0Y5fWwhugNlMmF6+mlC66vYfNvDpH7yHSkZ69lx2XWyvprodrzPPRvvtmbS//vNEW2f88daosvzaZx9spMjE0KIrkMShEII4VDz6+/YUcQ5WiN1Z35TJwOQ/fPvTh9bs9uJ3baegkEjnT62EMI13K67jqDGWra+9G67t21paCRx2Ty2jZvWrQ48KZOJxhtvoU9lEZte3neds7qHH6POw5uUR+4wKDpxIMpkou6mfxNVUcDGF99Be+QRvWLi4duMDk24QGDfSNIGjSX29x+xW22HvV3ax98S0lAN53efiufOxjTlWPxbGsheuMLpY+csWY1/SwPmyZOcPrYQ3UXiGSewYcQUBv/vLdzuuZsyv2CGPSrvWUT3k3juKdR6+tL21VdHtH3RB58C0O9yeU8ghOi5JEEohBAOfquWkdU7vke0log7cQo2ZaJx0R9OHztv1UaCG2rQJkx0+thCCNdIueA08sKi8X3v7XZvm/beF/i3NOBx0QUuiMxYQ667mPyQPvi+8sKedc7yVm1i6JoFbD31n/iHhxocodjf0BsuIS8smr6P3sug9HXsuOTabpW4FvtqO/sfRFSXkjFn3mFv0/Lfj6n18CHlCllryFWiTtVPtiv7+Tenj102Vz+5rfdJM5w+thDdSfBLz+LV1syA3G3svPwGeS0U3ZKbpwfbRx/LgFULaWtuaff2Yb/9zPa4FMIG9nNBdEII0TVIglD0CEqph5RSBUqpjY7LrL3uu1sptUMptV0pdcJet49USm1x3PeyUko5bvdQSn3uuH2VUip2r20uVkplOi6yqEkX0trYTHzmFspGjDU6lA7hExJIVp94/NavdvrYxT/OByB89nFOH1sI4Romi5mCcy8mMWsrO+e178QB+yf/o9IngOQLTndRdMYxu1kouOwaBuRsY9uXcwEovP8xrCYzCY/fY3B04kDMbhaKr76RsLpKKnwCGfqorLfUnSVecyHNFndq3v/osB7fVF1H8srfSZ9wvLSddaGIwQMoDIrAfZnzT0Qzr1hOqX8okUMGOn1sIbqTmImjWHvC2RQGRzLskduNDkcIl7GcdSYBzfVs/+Kndm1XtDmd/vkZVB0/20WRCXFgcoxadDaSIBQ9yQuapg1zXH4GUEolA+cCKcBM4HWllNnx+DeAq4AEx2V338nLgSpN0/oDLwBPO8YKBh4ExgJjgAeVUkEd8szEUdv1yyK8rC14TJ9qdCgdpmLISOJ2pWFtaXXquGrZMip9Aug7frhTxxVCuFbSnTfQZPGg/LmXD3ub+vIqUtYtJvPYE7F4uLswOuMMuecmqrwDaHn6Gcozshi+YA4bp59GaP9Yo0MTBzH07utJ7zeYnTfdhVegn9HhCBfyDQ0ibcQk+i+ee1jvZ9Le/gSf1ia8L7mwA6Lr2QoGjyIubf2e6mtniUpdR17yCJRJDmUI8XdG//AJoXm7ZL1k0a0lXnQWjW4eNHz6Rbu2y3n3fwD0uVTaiwpDyDFq0WnIu2rR050KfKZpWoumaVnADmCMUioS8Nc0bYWmaRrwX+C0vbb50PH1V8B0x5kbJwDzNU2r1DStCpjPn/+wRSdXNVdvgRR7+okGR9JxzBMn4NPaRM7iVU4dt/eWteQkDpcDN0J0MQF9erFl0okMXvQDtSXlh7XNtjf+i6e1lYDLu+8JiV6BfqSfcSHDNywh95+XY7bb6fPofUaHJQ7B3duTxJ2bGfO4VA/2BOq88whpqGbbp9/97WNNn31GqV8ISeee3AGR9WzapEkENdaQu3y908Ys2pxORE0Z1vHHOG1MIbozk8WMu7en0WEI4VJegX6kD5tAv2Xz27Umsd/cH8mOiCV67FAXRidEu8gxamEIOXorepLrlVKblVLv7XXWRB8gb6/H5Dtu6+P4ev/b99lG0zQrUAOEHGIs0QX4rFxGdkQsQTG9jQ6lw/SeOQ2A8nmLnDZm2fZd9KksomX8BKeNKYToOEG33Yx3WwtpT716WI93//JzCoMiGHja8S6OzFgDHrmDZos7I9YuZOOY6USNGWJ0SEIIh6TL/kG9uzdN//3fIR9Xk19MysZl7Jp+EmY3SwdF13P1PkU/BlX8/a9OG7PgB72NfajjPawQQggBYD/1NMLqKsn4/vDWvq3MyidxxyaKpvWcE8RFpyPHqEWnIQlC0W0opX5TSm09wOVU9FLseGAYUAT83+7NDjCUdojbj3Sb/WO9Sim1Vim11mq1HvxJiQ5hbWklPmMTJcN6xvqDu0UOTaTcNxjzKudVEOZ+px8ECpk53WljCiE6TsKsY8mISSLy0w/+ti1cZVY+KVtXkTPj5G5fMRwSF82maacC4P/AvQZHI4TYm6e/L9vGTSNx+XxaGhoP+rjtr3+Iu91KyJWXdmB0PVefUYMo8wvGvGyp08a0/fEHdR7exE4d77QxhRBCdH0Jl51Lq8lC9SefH9bjd773GWbNTtiF/3BxZKIbs+w+ruu4XLX3nV3pGLUQ3ftojuhRNE07TtO0QQe4fKdpWommaTZN0+zA2+j9l0E/gyJ6r2GigELH7VEHuH2fbZRSFiAAqDzEWAeK9S1N00ZpmjbKYpEzmI22a94f+LQ2YZk2xehQOpQymchNHEpkmvNaP1kXL6HRzZO446SCUIiuqvriK4gpySHt0x8O+bjM197HotmJuKZnHGxP/ugNtv73GxJmHWt0KEKI/Xhc+E/8WxpIe+/g6w/5fP0FuWHR9J85qQMj67mUyUTuoNH03bLGaesQ9tq0lqyEoVIBKoQQYh8BkWGkJ48mavGvh/Wa4/bDdxQFhhN/vLwnEEfMuvu4ruPy1t53dqVj1EJIglD0CI5+zbudDmx1fP09cK5SykMpFYe+Xh9uQAAAhUpJREFU0OtqTdOKgDql1DhH7+aLgO/22mb3YktnAQscPaB/BY5XSgU5ysOPd9wmOrnKn/V2RTGn97x23K1jxtGnsojyHdlOGS9s4xp29h+MxcPdKeMJITrekH9fRY2nLy0vH7rNqP+cr8iKiCOuh1Ry+PUKYdCFpxsdhhDiAJIuOJ0qb3/sn352wPtL0jJJythAwYmnd/uK587EOnEiveoqKFyfetRj1RSUElecRcOYcU6ITAghRHfTfPKpRFUUkrVw5SEf11BRTdLWVeRMmiHvCYQh5Bi16GzkP6HoKZ5RSm1RSm0GpgK3AGialgp8AaQBvwDXaZq2e1Xja4F30BeF3QnMddz+LhCilNoB3Arc5RirEngUWOO4POK4TXRyXiuWkhsWTWj/WKND6XCB0ycDkPvTgqMeq6aojNjCXdSP7hnJAiG6K09/X7adeBaD1y6kPCPrgI8p2pxO0s7NFJ90RgdHJ4QQf+Xm6UHGpBNIWruIxqqav9yf9ep7mNCIvu5yA6LrucJn6+vTFn539Mejsn+YB4D/cVOOeiwhhBDdT/yV52NHUfLBp4d83PYPvsTD1obfeWd3UGRC/IUcoxaditKTykIIo/j4+GgNDQ1Gh9Fj2dqsNPoFkDZ5FmPnfWl0OB2uua4BU1Ag60+7iHFfvXtUY216838MveafbP3oWwZdcJpzAhRCGCJv1Saixw1jxSU3Mf79F/9y/4qr72D8W89SuD6V3sOTOz5AIYTYT9pnP5B83imsfeJVRt193T737YgegN1sYUB2mkHR9Uya3U61XzA7Rk9m9KLvj2qsFedcycivP8BWUYlXoJ+TIhRCCNGdpCUMw6uhjrjCnQd9zNqJs4jfsAy/yjLpfCSOmFKqUdM0H6PjEMIZpIJQCNGjZf2+DL+WRsxTe+aaUp5+Puzqm0jAxrVHPVbjgkW0mizEnzTdCZEJIYwUPXYom1PGEv/NJ1hbWv9yf/iP35AeN0iSg0KITiPxrFmU+odi/nLfdQhzlq6lf34mlaeeZVBkPZcymchOGk6fLUf/PjNw/WqyYgZKclAIIcRB1Z54MnFFu8hfvfmA97c2NjNg7RIyx0yV5KAQQjhIglAI0aOV/6SvP9j3tBMNjsQ41UNH0i97Gy0NjUc1TuC6VXLgRohuxHb1NfSqLWfzfz7e5/bsxavpV7iTmtPkYLsQovMwWczsmjablE3LqSko3XN74RvvYVMm+t9wmYHR9VwtEybRu7KI4q2ZRzxGc10D8dlpVI0Y68TIhBBCdDcxV/wTgPz3/3fA+9M//Q7/lgbczzqzI8MSQohOTRKEQogezWP5UvJDetMrKd7oUAzjPmkiHrY2suYvPeIx9hy4GT7GiZEJIYw0+KrzKQkIw+2tN/e5veg/72NVJuKvu9SgyIQQ4sBCrrj4/9m77/ioqvz/4++TCgkBQhIIJBAIvbeAgIBgA3tbXXtdsW9zf7u6+93Vrd/d/a6r666rYu9tRUVXQbFQpIceOiEJ6Z30MjPn90dGDEhJIJmbZF7Px2MeM7fOJ5B7M3Pf95yjEI9Lu558UVJDF5f9Fr2v7cOT/HKs6bYg+vyzJUmZHyw66X3sX7RUIW6XOs3yzx4/AABN03vMMO3pO1SRiz866vLqt99VVXAnDbuRcdQB4BsEhAD8lsfl1oAdG5Q9xr9Drb4XnClJKv182UnvI/W/XzRcuJk9q2WKAuC4oNAQpV52nUZvX6sDqzdK8l5s//QDbR8xSdED+zlcIQAcbtB5Zygzqo86L/iPJGn3h58rrjhH1d/7vsOV+a8BZ05TWWi4PF8tPel9lH72pSQp4aKzW6osAEAHVXTuBRq6P0X5Ow4fh9Djcmvgys+1c/zp6hTB0HEA8A0CQgB+K23pGnWvLpc5w7/vRo4ZmqjsyFiFrF190vsoW/KVJKn/xee0UFUA2oLBv/qR6gMClfXnxyR9e7G99koutgNoe0xAgA7MuUQjdiarcG+aSp95UbWBwRp2941Ol+a3AoODtH/YOMVuXnvS+whbu0rpvRIUmdCnBSsDAHREvW++RpK0/7nXDpu/e+Fniq4olufSSx2oCgDaLgJCAH4r/6NPJUnxl811uBLnZQ8fp/gdm2Q9npPaPmzNKqXF9lf3fr1buDIATooe1F9bJp+lEYsXqLq0XCXPvtRwsf0uLrYDaJv63HGLAq1He//xrAZ99bFSJs5U117RTpfl16qnnK5+BQdUuC+j2dt6XG4N2LVZeaOTWqEyAEBHkzA9Sek9+yn84w8Pm1/62tuqCwjSkFuvdqgyAGibCAgB+K2Qr5crp3sv9R4zzOlSHOc+bYp6lhcpL2Vv87etd2nAns3KGzupFSoD4LROP7xHXWsqtPWv/z50sT2iZ5TTZQHAUSXMnKT9vRM14tl/KKqyVLr2WqdL8nuR5zV0DZr+XvPHIUxfvlZdaypkpk9v6bIAAB1U9pnnadjODSrNyJHUMExC/NLF2jkiiZuGAOAIBIQA/JL1eNR/e7IyxxBqSVLUubMkSZkff97sbfd/sVIRtVUKnDGjhasC0BaM+P6FSuvVX8Me/YOiK0pkrrnG6ZIA4LhyL7hMXWsqVBYarhG30iWy0xLnzFRVcKhcX37V7G3zP2n4bBp38bktXBUAoKOKvuEaBVmPdj/3hqSG4WXii7JVfcFFDlcGAG0PASEAv5SxIlk9Kg9KM/17/MFv9J89VVXBoXIvX9HsbQu9F27iL57T0mUBaANMQIDyrr9FXWsqVB4apuG30S0PgLYt4e5bJUk7p52jThHhDleD4E6h2jdojHpuXNPsbQNXrlR+RJR6j6XHDwBA0wyaO0O53XsqZOH7kqTcF9+UR0YDf0CvAgBwJAJCAH4p96PFkqQ+lxBqSVJQaIj2DxipHluSm71tyKqvldu9p2JHD2mFygC0BSN+fo/KQ8O0Y/pcLrYDaPP6jB+hTf9+WYnP/MPpUuBVcdrpGpCTqoOZuc3aLj4lWQdGTJAJ4NIFAKBpTECA0macq+FbV6uisEQxX3yi3YmjFD2ov9OlAUCbw6dsAH4paPly5XeNVp8JI50upc0omzBJAzL3qKrkYJO3sR6P+m3fqMyRE1uxMgBOi+gZpfJV6zTizWedLgUAmmTcXTcoemA/p8uAV7c5Z0qS9r//aZO3yd26W7Gl+aqfOq21ygIAdFBdr71Koe56bfv1XzQoc49K51zgdEkA0CYREALwO9bjUcK29coYPYm7kRsJO2O6gqxH+xctbfI2Weu3KbqiWO7p01uxMgBtQZ/xI9QlOtLpMgAA7VDiBWeqNjBYNZ9/0eRtMj9qCBOj557ZWmUBADqooVecp+Lwbhr77GOSpL630r0oABwNV8YBOMZ6PI68b+bazQ2h1oyZjrx/W5Vw/lmSpLIvljV5m+yPPpMkxZ5/TqvUBAAAgPavU0S49iWOVNSGpo9D6F62XBUhYRpwJi0IAQDNExgcpL1Tz1ZnV632905UXNJop0sCgDaJgBCAI7a++I7S4gY1exySlpCzsOFu5N4Xn+vz927LuvfrrYyYvuq8fm3TN1q+XKWdI9R32oTWKwwAAADt3sFJU5V4YLfK84uatH7PzeuVOmSMAoODWrkyAEBHFHrVFZKk3LPOc7gSAGi7CAgBOKJLQrz65aVrx93/z+fvHbBsqQq7RKrvaeN8/t5tXe7ICUrYvbnJrTv7bF2v/cPGKyAosJUrAwAAQHvW5ZyzFGg92r/wsxOuezArXwk5+1WZNMUHlQEAOqKRN16hVfP+n4b+7gGnSwGANouAEIAjBsyeqvVnX66J/31DGas2+Ox9rcejvtvWK31kEuMPHs20qYqsKlPm2s0nXLVwb5rii7JUO4VunwAAAHB8Ay85W/UBgapc8uUJ1037aIkCZNX17FmtXxgAoEMKCg3R1Kf/qh4D4p0uBQDaLK6OA3DMwCcfUW1QqIrv/rHP3jN74w71Olgg1/QZPnvP9qTXnDMlSTmfnPjCTcYHDXd/R3q3AQAAAI4lLLKbUvsNVeS6VSdct+rLpaoPCFTiBXzOBAAAAFoLASEAx0QP7KetN9+jcZuWa9vLC3zynlkfLJIk9bpojk/er73pO22Cyjp1kV114gs3dV8tVVVwqBLnzPRBZQAAAGjvipOmKjFth6pLy4+7Xvf1q5Xab6g6d4/wUWUAAACA/yEgBOCo8X97SNmRsQp78Ody17ta/f3MsqUqCeumhBlJrf5e7VFAUKDSBo1Wz63JJ1w3etNapQ4cpeBOoT6oDAAAAO1d2FmzFOJxad+HS465Tk15pQambVfJ+Mk+rAwAAADwPwSEABzVKSJcOQ8+pMTsfUp++JFWf7+4Leu0f8QExh88jsqkyUrITdPBnIJjrlOeX6QBWftUnjTFh5UBAACgPRtw2Vx5ZFT+6RfHXCdt8TKFuF0KnX2GDysDAAAA/A9XyAE4bsL987QzcbQSH/+LKgpLWu19crfuVp+SXNWdzviDx9PljBkKkFX6x8e+cLN/4WcKtB51OWu2DysDAABAe9a1V7RS4wep63HGISz5rGEs7IQLz/ZVWQAAAIBfIiAE4DgTEKCAxx5VdEWJtt73YKu9z4H3P5EkxVxwbqu9R0fQ//xZcpsAVX614pjrVH7+lVwmQIkXneXDygAAANDeFY4/TQP3bVVtZdVRl3des0rpPfupx4B4H1cGAAAA+BcCQnQYxpgrjTEpxhiPMSbpiGUPGmP2GmN2GWPmNJo/0Riz1bvscWOM8c4PNca85Z2/xhjTv9E2Nxlj9ngfNzWaP8C77h7vtiE++LE7jCEXnaX1p5+nCe88r5wtO1vlPexXS3WwUxcNmE23mMcT0TNKab0T1WXD2mOu0y15jVL7DlV4VHffFQYAAIB2L+Ss2erkqtP+T5Z+Z5nH5daAXZuUN5rxwgEAQMfD9Wu0NQSE6Ei2Sbpc0rLGM40xIyRdLWmkpLmS/m2MCfQuflLSPEmDvY+53vm3SSqx1g6S9Kikv3j31UPSQ5JOkzRZ0kPGmEjvNn+R9Ki1drCkEu8+0AzxT/1D1hhl3/njVtl/n81rlTp8ggKCAk+8sp8rHDNBA/Zuk7ve9Z1ltZVVGpiaouIJkx2oDAAAAO3ZgEsbvnKVLlrynWXpK9arW02FzIzpvi4LAADAF7h+jTaFgBAdhrV2h7V211EWXSLpTWttrbV2v6S9kiYbY3pL6mqtXWWttZJelnRpo21e8r7+j6SzvHdnzJH0mbW22FpbIukzSXO9y870rivvtt/sC00UO2qwNl51myauWqydCxa36L4LdqUqvihLtdO42NAUAdOmqUtdlTJWrPvOsv2fLFWou16hs89woDIAAAC0Z5EJfZQW219ha1Z+Z1n+xw2hYZ8LGH8QAAB0PFy/RltDQAh/ECfpQKPpTO+8OO/rI+cfto211iXpoKSo4+wrSlKpd90j94VmGPP4n1QQ0UPmZ/fLejwttt/0BQ3jD0adx8WGpuhz3pmSpPzFX35nWcmnX0iSEi46x6c1AQAAoGPIGzdZA3dvlqu27rD5gSu/VkFED/WZMNKhygAAABzB9Ws4goAQ7YoxZokxZttRHpccb7OjzLPHmX8y2xxvX98tyJh5xpj1xpj1Ltd3u3D0Z+FR3bX/x7/U0P0pSv7Lky22X/dXS1UREqYBZ5/eYvvsyPpMGKmi8O4KWLXqO8vC1qxUes9+6jEg3oHKAAAA0N4Fzpql8LpqpX624rD5cSkbdGD4BJkALlUAAIA2K+ib67rex7zGCzvK9Wv4Bz51o12x1p5trR11lMcHx9ksU1LfRtPxkrK98+OPMv+wbYwxQZK6SSo+zr4KJXX3rnvkvo72c8y31iZZa5OCgoKOtZrfSvrNj7U3frDi//o71ZRVtMg+Yzet0b6hYxUUyti7TWECApQxdJx6p2w8bL673qUBuzYrb8wkhyoDAABAe5dw6RxJUvEn345DmLttj3qX5qlu6jSnygIAAGgK1zfXdb2P+Y0XdpTr1/APBITwBwslXW2MCTXGDFDDYK5rrbU5ksqNMVO8fTDfKOmDRtvc5H39PUlfePt5XizpXGNMpHdw13MlLfYu+9K7rrzbHu+kj+MICApU7Z//T7Gl+dr004dOeX+F+zKUkJ+hqqmMP9gctZMmK74oS8X7v+3JIH3ZWnWtrZSZOcPBygAAANCexQxNVGZUnDqt/LYFYeZ/G8LC6LlnOlUWAACAU7h+DUcQEKLDMMZcZozJlDRV0n+NMYslyVqbIultSdslLZJ0j7XW7d3sLknPqmHg132SPvHOf05SlDFmr6SfSnrAu69iSb+XtM77+J13niT9QtJPvdtEefeBkzTyuku0ccIsjX7lSRXuTTulfaW/t0iSFMn4g83S7awzJEkZ//3i0LwC713ecRed60hNAAAA6Biyx05W4s6N8rgavpq5ly1TZUhn9Z891eHKAAAAWgfXr9HWmIbgGIBTwsPDbWVlpdNltEkH1mxWr2lJ2jTrIk3+fMFJ72fNBddo9GfvK7isVMGdQluwwo6tpqxCAZGRSr7iZk19+xlJUvLUOeq7fYNiSvIYGwYAAAAnbd1vH9Okh3+i1CVfK/GsaUqNG6TK7lEanbLG6dIAAACOyRhTZa0Nd7oOoCVwdRdAm9X3tLHacNG1Svrife377OuT3k/PDWu0b/AYwsFm6tS1i/b3G6JuG9dJkqzHo74pyTowciLhIAAAAE5J3CUN4xDmf/SpDuYUqH92qiom03oQAAAA8BWu8AJo04Y/8X86GBah6h/+SNbjafb2JenZGpC7XxVTTm+F6jq+kjETlZi2Q/U1tcreuEM9y4vkmsa/JQAAAE5Nn3HDldu9p0JWrlDaR0sUIKuIs2Y5XRYAAADgNwgIAbRp3eJ6avcdP9Woncna/O9Xm739/vcbxh/sPpfxB09G8Izp6uSq0/4lXyv7o08lST0v4N8SAAAApy5z1CQlpCSr6vOvVB8QqMQLZjtdEgAAAOA3CAgBtHkT/vSA0nv2U9Rvf6W6qppmbVv3+ZeqCQpR4vmzWqe4Di7+/IaLNMWffSW7fIUOduqihBmTHa4KAAAAHYFn5gxFVZZq4CcLtL/vEIVFdnO6JAAAAMBvEBACaPOCO4Wq9Ld/VN/CTG148E/N2jZmwxrtGzhaoeFhrVRdx9ZrxGDldotR8No1it2yTvuHjlNAUKDTZQEAAKAD6H3RuZKknmWFKh7PTWgAAACALxEQAmgXxsy7VltHTNbw+Y/qYGZuk7Y5mJWvAVl7VXbatFaurmPLGj5eAzevUr+CA6o+barT5QAAAKCDiJ88VoVdIiVJIbNmOlwNAAAA4F8ICAG0CyYgQF3+9Zi61FZpx10/a9I2+99frABZdT33rFaurmOrP22KuleXS5Ii55zpcDUAAADoKExAgNJHJkmS+l98rsPVAAAAAP6FgBBAuzFg9lStP/tyTfz4TWWs2nDC9Ws+/0J1gUEaeBEB4amIOmeWJDWM5Th3lqO1AAAAoGPp+usHtfruB9VjQLzTpQAAAAB+xVhrna4B8Gvh4eG2srLS6TLajcJ9Geo0Yrj2jpiocRuXHXfdPQnDVR8SqhF7NvmmuA6qvqZWrohuSh0wQiN3nziYBQAAAAAAADoiY0yVtTbc6TqAlkALQgDtSvTAftp6090at2m5tr284JjrlecXKfHAbh2czPiDpyq4U6i2/vy3sr/5jdOlAAAAAAAAAABaAC0IAYfRgrD5asorVZwwSDWdw5WQtlOBwUHfWWfzM29o7LxrtfWF/2j0zVc4UCUAAAAAAACAjoQWhOhIaEEIoN3pFBGunAcfUmL2PiU//MhR16n67AvVBwRq4CVn+7g6AAAAAAAAAADaNloQAg6jBeHJsR6Pdg0ep+j8THXav09doiMPW74rcZSsjIalbnWoQgAAAAAAAAAdCS0I0ZHQghBAu2QCAhTw2KOKrijR1nsfOGxZVclBJabvVMmkqQ5VBwAAAAAAAABA20VACKDdGnLRWVp/+nma8J8XlLNl56H5+z74VMEet8LPOdPB6gAAAAAAAAAAaJsICAG0a/FP/UPWGGXf+eND8yoWfyGXCVDipXOcKwwAAAAAAAAAgDaKgBBAuxY7arA2XnWbJq5arJ0LFkuSuq9bqdS+Q78zLiEAAAAAAAAAACAgBNABjHn8TyqI6CFz/09VXVqugWnbVZw0xemyAAAAAAAAAABokwgIAbR74VHdtf/Hv9TQtO3actWtCnG71Ons2U6XBQAAAAAAAABAm2SstU7XAPi18PBwW1lZ6XQZ7Z673qW0ASM0MGuPPDIqz85Tt94xTpcFAAAAAAAAoIMwxlRZa8OdrgNoCbQgBNAhBAYHqeYv/ydJSo0fRDgIAAAAAAAAAMAxBDldAAC0lJHXXaJVi+5R8IjhTpcCAAAAAAAAAECbRRejgMPoYhQAAAAAAAAA2j66GEVHQhejAAAAAAAAAAAAgB8hIAQAAAAAAAAAAAD8CAEhAAAAAAAAAAAA4EcYgxBwmDHGI6na6TocEiTJ5XQRQAfB8QS0DI4loOVwPAEtg2MJaBkcS0DL8efjqbO1loZX6BAICAE4xhiz3lqb5HQdQEfA8QS0DI4loOVwPAEtg2MJaBkcS0DL4XgCOgaSbgAAAAAAAAAAAMCPEBACAAAAAAAAAAAAfoSAEICT5jtdANCBcDwBLYNjCWg5HE9Ay+BYAloGxxLQcjiegA6AMQgBAAAAAAAAAAAAP0ILQgAAAAAAAAAAAMCPEBACcIQxZq4xZpcxZq8x5gGn6wHaE2PM88aYfGPMtkbzehhjPjPG7PE+RzpZI9AeGGP6GmO+NMbsMMakGGN+5J3P8QQ0gzGmkzFmrTFms/dY+q13PscScBKMMYHGmI3GmI+80xxLwEkwxqQZY7YaYzYZY9Z753E8Ac1kjOlujPmPMWan97vTVI4loGMgIATgc8aYQElPSDpP0ghJ1xhjRjhbFdCuvChp7hHzHpD0ubV2sKTPvdMAjs8l6X5r7XBJUyTd4/17xPEENE+tpDOttWMljZM01xgzRRxLwMn6kaQdjaY5loCTN9taO85am+Sd5ngCmu8fkhZZa4dJGquGv1EcS0AHQEAIwAmTJe211qZaa+skvSnpEodrAtoNa+0yScVHzL5E0kve1y9JutSXNQHtkbU2x1q7wfu6XA1fdOPE8QQ0i21Q4Z0M9j6sOJaAZjPGxEu6QNKzjWZzLAEth+MJaAZjTFdJMyU9J0nW2jprbak4loAOgYAQgBPiJB1oNJ3pnQfg5PWy1uZIDaGHpJ4O1wO0K8aY/pLGS1ojjieg2bxdIm6SlC/pM2stxxJwch6T9HNJnkbzOJaAk2MlfWqMSTbGzPPO43gCmidRUoGkF7zdXz9rjAkXxxLQIRAQAnCCOco86/MqAACQZIzpIuldST+21pY5XQ/QHllr3dbacZLiJU02xoxyuCSg3THGXCgp31qb7HQtQAdxurV2ghqGN7nHGDPT6YKAdihI0gRJT1prx0uqFN2JAh0GASEAJ2RK6ttoOl5StkO1AB1FnjGmtyR5n/MdrgdoF4wxwWoIB1+z1i7wzuZ4Ak6St8upr9QwVi7HEtA8p0u62BiTpoZhGM40xrwqjiXgpFhrs73P+ZLeU8NwJxxPQPNkSsr09g4hSf9RQ2DIsQR0AASEAJywTtJgY8wAY0yIpKslLXS4JqC9WyjpJu/rmyR94GAtQLtgjDFqGEtjh7X2740WcTwBzWCMiTHGdPe+7izpbEk7xbEENIu19kFrbby1tr8aviN9Ya29XhxLQLMZY8KNMRHfvJZ0rqRt4ngCmsVamyvpgDFmqHfWWZK2i2MJ6BCMtfTqB8D3jDHnq2F8jUBJz1tr/+hsRUD7YYx5Q9IsSdGS8iQ9JOl9SW9L6icpQ9KV1tpih0oE2gVjzHRJyyVt1bdjPf1SDeMQcjwBTWSMGSPpJTV8rguQ9La19nfGmChxLAEnxRgzS9LPrLUXciwBzWeMSVRDq0GpoYvE1621f+R4AprPGDNO0rOSQiSlSrpF3s984lgC2jUCQgAAAAAAAAAAAMCP0MUoAAAAAAAAAAAA4EcICAEAAAAAAAAAAAA/QkAIAAAAAAAAAAAA+BECQgAAAAAAAAAAAMCPEBACTWSMed4Yk2+M2XaM5cYY87gxZq8xZosxZoKvawQAAAAAAAAAADgRAkKg6V6UNPc4y8+TNNj7mCfpSR/UBAAAAAAAAAAA0CwEhEATWWuXSSo+ziqXSHrZNlgtqbsxprdvqgMAAAAAAAAAAGgaAkKg5cRJOtBoOtM7DwAAAAAAAAAAoM0IcroAoAMxR5lnj7qiMfPU0A2pwsPDJw4bNqw16wIAAAAAAAAAnKLk5ORCa22M03UALYGAEGg5mZL6NpqOl5R9tBWttfMlzZekpKQku379+tavDgAAAAAAAABw0owx6U7XALQUuhgFWs5CSTeaBlMkHbTW5jhdFAAAAAAAAAAAQGO0IASayBjzhqRZkqKNMZmSHpIULEnW2qckfSzpfEl7JVVJusWZSgEAAAAAAAAAAI6NgBBoImvtNSdYbiXd46NyAAAAAAAAAAAATgpdjAIAAAAAAAAAAAB+hIAQAAAAAAAAAAAA8CMEhAAAAAAAAAAAAIAfISAEAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAAAAAAAAAADwIwSEAAAAAAAAAAAAgB8hIAQAAAAAAAAAAAD8CAEhAAAAAAAAAAAA4EcICAEAAAAAAAAAAAA/QkAIAAAAAAAAAAAA+BECQqAZjDFzjTG7jDF7jTEPHGV5N2PMh8aYzcaYFGPMLU7UCQAAAAAAAAAAcCwEhEATGWMCJT0h6TxJIyRdY4wZccRq90jabq0dK2mWpEeMMSE+LRQAAAAAAAAAAOA4CAiBppssaa+1NtVaWyfpTUmXHLGOlRRhjDGSukgqluTybZkAAAAAAAAAAADHRkAINF2cpAONpjO98xr7l6ThkrIlbZX0I2utxzflAQAAAAAAAAAAnBgBIdB05ijz7BHTcyRtktRH0jhJ/zLGdP3OjoyZZ4xZb4xZX1BQ0NJ1AgAAAAAAAAAAHBMBIdB0mZL6NpqOV0NLwcZukbTANtgrab+kYUfuyFo731qbZK1NiomJabWCAQAAAAAAAAAAjkRACDTdOkmDjTEDjDEhkq6WtPCIdTIknSVJxphekoZKSvVplQAAAAAAAAAAAMcR5HQBQHthrXUZY+6VtFhSoKTnrbUpxpg7vcufkvR7SS8aY7aqoUvSX1hrCx0rGgAAAAAAAAAA4AgEhEAzWGs/lvTxEfOeavQ6W9K5vq4LAAAAAAAAAACgqehiFAAAAAAAAAAAAPAjBIQAAAAAAAAAAACAHyEgBAAAAAAAAAAAAPwIASEAAAAAAAAAAADgRwgIAQAAAAAAAAAAAD9CQAgAAAAAAAAAAAD4EQJCAAAAAAAAAAAAwI8QEAIAAAAAAAAAAAB+hIAQAAAAAAAAAAAA8CMEhAAAAAAAAAAAAIAfISAEAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkKgGYwxc40xu4wxe40xDxxjnVnGmE3GmBRjzFJf1wgAAAAAAAAAAHA8QU4XALQXxphASU9IOkdSpqR1xpiF1trtjdbpLunfkuZaazOMMT0dKRYAAAAAAAAAAOAYaEEINN1kSXuttanW2jpJb0q65Ih1rpW0wFqbIUnW2nwf1wgAAAAAAAAAAHBcBIRA08VJOtBoOtM7r7EhkiKNMV8ZY5KNMTf6rDoAAAAAAAAAAIAmoItRoOnMUebZI6aDJE2UdJakzpJWGWNWW2t3H7YjY+ZJmidJ/fr1a4VSAQAAAAAAAAAAjo4WhEDTZUrq22g6XlL2UdZZZK2ttNYWSlomaeyRO7LWzrfWJllrk2JiYlqtYAAAAAAAAAAAgCMREAJNt07SYGPMAGNMiKSrJS08Yp0PJM0wxgQZY8IknSZph4/rBAAAAAAAAAAAOCa6GAWayFrrMsbcK2mxpEBJz1trU4wxd3qXP2Wt3WGMWSRpiySPpGettducqxoAAAAAAAAAAOBwxtojh1AD4EtJSUl2/fr1TpcBAAAAAAAAADgOY0yytTbJ6TqAlkAXowAAAAAAAAAAAIAfISAEAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAAAAAAAAAADwIwSEAAAAAAAAAAAAgB8hIAQAAAAAAAAAAAD8CAEhAAAAAAAAAAAA4EcICAEAAAAAAAAAAAA/QkAIAAAAAAAAAAAA+BECQgAAAAAAAAAAAMCPEBACAAAAAAAAAAAAfoSAEGgGY8xcY8wuY8xeY8wDx1lvkjHGbYz5ni/rAwAAAAAAAAAAOBECQqCJjDGBkp6QdJ6kEZKuMcaMOMZ6f5G02LcVAgAAAAAAAAAAnBgBIdB0kyXttdamWmvrJL0p6ZKjrHefpHcl5fuyOAAAAAAAAAAAgKYgIASaLk7SgUbTmd55hxhj4iRdJukpH9YFAAAAAAAAAADQZASEQNOZo8yzR0w/JukX1lr3cXdkzDxjzHpjzPqCgoKWqg8AAAAAAAAAAOCEgpwuAGhHMiX1bTQdLyn7iHWSJL1pjJGkaEnnG2Nc1tr3G69krZ0vab4kJSUlHRkyAgAAAAAAAAAAtBoCQqDp1kkabIwZIClL0tWSrm28grV2wDevjTEvSvroyHAQAAAAAAAAAADASQSEQBNZa13GmHslLZYUKOl5a22KMeZO73LGHQQAAAAAAAAAAG0eASHQDNbajyV9fMS8owaD1tqbfVETAAAAAAAAAABAcwQ4XQAAAAAAAAAAAAAA3yEgBAAAAAAAAAAAAPwIASEAAAAAAAAAAADgRwgIAQAAAAAAAAAAAD9CQAgAAAAAAAAAAAD4EQJCAAAAAAAAAAAAwI8QEAIAAAAAAAAAAAB+hIAQAAAAAAAAAAAA8CMEhAAAAAAAAAAAAIAfISAEAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBBoBmPMXGPMLmPMXmPMA0dZfp0xZov3sdIYM9aJOgEAAAAAAAAAAI6FgBBoImNMoKQnJJ0naYSka4wxI45Ybb+kM6y1YyT9XtJ831YJAAAAAAAAAABwfASEQNNNlrTXWptqra2T9KakSxqvYK1daa0t8U6ulhTv4xoBAAAAAAAAAACOi4AQaLo4SQcaTWd65x3LbZI+adWKAAAAAAAAAAAAminI6QKAdsQcZZ496orGzFZDQDj9GMvnSZonSf369Wup+gAAAAAAAAAAAE6IFoRA02VK6ttoOl5S9pErGWPGSHpW0iXW2qKj7chaO99am2StTYqJiWmVYgEAAAAAAAAAAI6GgBBounWSBhtjBhhjQiRdLWlh4xWMMf0kLZB0g7V2twM1AgAAAAAAAAAAHBddjAJNZK11GWPulbRYUqCk5621KcaYO73Ln5L0G0lRkv5tjJEkl7U2yamaAQAAAAAAAAAAjmSsPeoQagB8JCkpya5fv97pMgAAAAAAAAAAx2GMSaZBCDoKuhgFAAAAAAAAAAAA/AgBIQAAAAAAAAAAAOBHCAgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAAAAAAAAAADwIwSEAAAAAAAAAAAAgB8hIAQAAAAAAAAAAAD8CAEhAAAAAAAAAAAA4EcICAEAAAAAAAAAAAA/QkAIAAAAAAAAAAAA+BECQgAAAAAAAAAAAMCPEBACAAAAAAAAAAAAfoSAEGgGY8xcY8wuY8xeY8wDR1lujDGPe5dvMcZMcKJOAAAAAAAAAACAYyEgBJrIGBMo6QlJ50kaIekaY8yII1Y7T9Jg72OepCd9WiQAAAAAAAAAAMAJEBACTTdZ0l5rbaq1tk7Sm5IuOWKdSyS9bBusltTdGNPb14UCAAAAAAAAAAAcCwEh0HRxkg40ms70zmvuOgAAAAAAAAAAAI4JcroAoB0xR5lnT2IdGWPmqaELUkmqNcZsO8XaAOBI0ZIKnS4CQIfDuQVAa+DcAqA1cG4B0BqGOl0A0FIICIGmy5TUt9F0vKTsk1hH1tr5kuZLkjFmvbU2qWVLBeDvOLcAaA2cWwC0Bs4tAFoD5xYArcEYs97pGoCWQhejQNOtkzTYGDPAGBMi6WpJC49YZ6GkG02DKZIOWmtzfF0oAAAAAAAAAADAsdCCEGgia63LGHOvpMWSAiU9b61NMcbc6V3+lKSPJZ0vaa+kKkm3OFUvAAAAAAAAAADA0RAQAs1grf1YDSFg43lPNXptJd3TzN3Ob4HSAOBInFsAtAbOLQBaA+cWAK2BcwuA1sC5BR2GacgzAAAAAAAAAAAAAPgDxiAEAAAAAAAAAAAA/AgBIeAjxpi5xphdxpi9xpgHjrLcGGMe9y7fYoyZ4ESdANqXJpxbrvOeU7YYY1YaY8Y6USeA9uVE55ZG600yxriNMd/zZX0A2qemnFuMMbOMMZuMMSnGmKW+rhFA+9OE70TdjDEfGmM2e88ttzhRJ4D2wxjzvDEm3xiz7RjLuY6LDoGAEPABY0ygpCcknSdphKRrjDEjjljtPEmDvY95kp70aZEA2p0mnlv2SzrDWjtG0u9FX/kATqCJ55Zv1vuLpMW+rRBtiTfMyXS6jhMxxtxsjFnhdB3+rCnnFmNMd0n/lnSxtXakpCt9XSeA9qWJn1vukbTdWjtW0ixJjxhjQnxaKID25kVJc4+znOu46BAICAHfmCxpr7U21VpbJ+lNSZccsc4lkl62DVZL6m6M6e3rQgG0Kyc8t1hrV1prS7yTqyXF+7hGAO1PUz63SNJ9kt6VlO/L4vBdxpjp3lbiB40xxcaYr40xk7zLDgvGjDFdvcvfNcYMNsZYY0zQEft70RjzB1//HOjwmnJuuVbSAmtthiRZazm/ADiRppxbrKQIY4yR1EVSsSSXb8sE0J5Ya5ep4VxxLFzHRYdAQAj4RpykA42mM73zmrsOADTW3PPGbZI+adWKAHQEJzy3GGPiJF0m6Skf1oWjMMZ0lfSRpH9K6qGG/6vfSqo9yrqRkpZISpf0fUn1vqsUaNLnliGSIo0xXxljko0xN/qsOgDtVVPOLf+SNFxStqStkn5krfX4pjwAHRTXcdEhEBACvmGOMs+exDoA0FiTzxvGmNlqCAh/0aoVAegImnJueUzSL6y17tYvBycwRJKstW9Ya93W2mpr7afW2i2NVzLGREv6QlKKpOuttSfVcsIY09nbwrDEGLNd0qQjlvfxtk4sMMbsN8b8sNGyh40x7xhjXjXGlBtjthpjhhhjHvSO8XLAGHNuo/VvMcbs8K6baoy5o9GyWcaYTGPM/d5tcxqPKWWMiTLGLDTGlBlj1koaeDI/L1pUU84tQZImSrpA0hxJvzbGDGntwgC0a005t8yRtElSH0njJP3Le4MNAJwsruOiQyAgBHwjU1LfRtPxarhzrbnrAEBjTTpvGGPGSHpW0iXW2iIf1Qag/WrKuSVJ0pvGmDRJ35P0b2PMpT6pDkfaLcltjHnJGHOet5XgkXpIWippjaRbT7HVxENqCNsGquGC603fLDDGBEj6UNJmNdxBfZakHxtj5jTa/iJJr0iKlLRRDWNYBnjX/52kpxutmy/pQkldJd0i6VFjzIRGy2MldfNue5ukJxr9/E9IqpHUW9Kt3gec1dTvRIustZXW2kJJyySN9VF9ANqnppxbblFD98XWWrtXDeO0D/NRfQA6Jq7jokMgIAR8Y52kwcaYAd6BsK+WtPCIdRZKutE0mCLpoLU2x9eFAmhXTnhuMcb0k7RA0g3W2t0O1Aig/TnhucVaO8Ba299a21/SfyTdba193+eVQtbaMknT1XDH8jOSCrwt53o1Wq2vGloavmCtPdU7m6+S9EdrbbG19oCkxxstmyQpxlr7O2ttnbU21VvT1Y3WWW6tXextwfiOpBhJf7bW1qth3Kj+xpju3p/tv9bafd4LukslfSppRqN91Uv6nbW23lr7saQKSUONMYGSrpD0G2/QtE3SS6f4c+PUNeU70QeSZhhjgowxYZJOk7TDx3UCaF+acm7JUMNNK/L+fRwqKdWnVQLoaLiOiw4h6MSrADhV1lqXMeZeNdwhHSjpeWttijHmTu/ypyR9LOl8SXslVanhDjcAOKYmnlt+IylKDa17JMllrU1yqmYAbV8Tzy1oQ6y1OyTdLEnGmGGSXlVDN7DXeFfZrIYw7hNjzFnW2o3e+d90Mxrc6PU308can7CPDh9vJb3R6wRJfYwxpY3mBUpa3mg6r9HrakmFjbqqrfY+d5FUaow5Tw0tFoeo4ebWMDWMHfWNoiO6Sq3ybhujhu+6x6oTDmjKucVau8MYs0jSFkkeSc96A14AOKomfm75vaQXjTFb1dAt4C+8rZQB4KiMMW9ImiUp2hiTqYbPpMES13HRsZhTv4EUAAAAANBWeC+U3mGtHW2MuVnSD6y1040xP5f0c0mzrLXbvC3tqiWN9YaM32y/UtJ8a+2LR9n3fkl3WWsXeadvl/SQtTbeGDNV0svW2sHHqOthSYOstdd7p89WQwDU3zsdpIZgsq+kAkklkm6U9IG1tt4Y876kbdba/zHGzJL0qrU2vtH+0yT9QNKXauhedLS1dqd32R8lnWGtnd6sf0wAAAAA6KDoYhQAAAAA2iljzDBjzP3GmHjvdF81tBxcfeS61tq/SvqHpCXGmKHelnvvSvqjMSbKGBNsjLlG0ghJnxzjLd+W9KAxJtL7nvc1WrZWUpkx5hfGmM7GmEBjzChjzKST+NFCJIWqISh0eVsTntuUDb0/1wJJDxtjwowxI9RorEQAAAAAAAEhAAAAALRn5WoYp22NMaZSDcHgNkn3H21la+3vJT0r6XNjzEBJd0sqVkOXjvmS7pV0gbU272jbS/qtGrrr3K+GMQFfabRvt6SLJI3zLi/0vle35v5Q1tpyST9UQyBZIulafXdMqeO5Vw3djeZKelHSC82tAQAAAAA6MroYBQAAAAAAAAAAAPwILQgBAAAAAAAAAAAAP0JACAAAAAAAAAAAAPgRAkIAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAAAAAAAAAADwI0FOFwD4u+joaNu/f3+nywAAAAAAAAAAHEdycnKhtTbG6TqAlkBACDisf//+Wr9+vdNlAAAAAAAAAACOwxiT7nQNQEuhi1EAAAAAAAAAAADAjxAQAgAAAAAAAAAAAH6EgBAAAABAh2Gt1ZtrM5RaUOF0KQAAAAAAtFkEhAAAAAA6jL99uksPLNiqu17doHq3x+lyAAAAAABokwgIAQAAAHQIL3y9X098uU+T+kdqV165nlme6nRJAAAAAAC0SQSEAAAAANq9hZuz9buPtuvcEb30xu1TNHdkrB7/fI8yiqqcLg0AAAAAgDaHgBAAAABAu7Z8T4Huf3uTJiX00OPXjFdQYIAeuniEAo3Rrz/YJmut0yUCAAAAANCmEBACAAAAaLc2HyjVHa8ka2BMFz1zU5I6BQdKknp366yfzRmqpbsL9N+tOQ5XCQAAAABA20JACByHMeZ5Y0y+MWZbo3k9jDGfGWP2eJ8jGy170Biz1xizyxgzx5mqAQAA/ENqQYVueXGdeoSH6KVbJ6tb5+DDlt84tb9Gx3XTbz/croPV9Q5VCQAAAABA20NACBzfi5LmHjHvAUmfW2sHS/rcOy1jzAhJV0sa6d3m38aYQN+VCgAA4D/yymp0w3NrJUkv3zpZvbp2+s46gQFGf7pstIoqavW3xbt8XSIAAAAAAG0WASFwHNbaZZKKj5h9iaSXvK9fknRpo/lvWmtrrbX7Je2VNNkXdQIAAPiTg9X1uun5tSqpqtOLt0xSYkyXY647Or6bbprWX6+uSdfGjBIfVgkAAAAAQNtFQAg0Xy9rbY4keZ97eufHSTrQaL1M7zwAAAC0kJp6t25/ab32FVTo6Rsmakx89xNuc/+5Q9UropMeXLBV9W5P6xcJAAAAAEAbR0AItBxzlHn2qCsaM88Ys94Ys76goKCVywIAAOgYXG6PfvjGRq1LL9YjV43TjMExTdquS2iQHr54pHbmluuFr/e3cpUAAAAAALR9BIRA8+UZY3pLkvc53zs/U1LfRuvFS8o+2g6stfOttUnW2qSYmKZd2AIAAPBn1lr9z/vb9On2PD104QhdPLZPs7afM7KXzh7eS49+tkeZJVWtVCUAAAAAAO0DASHQfAsl3eR9fZOkDxrNv9oYE2qMGSBpsKS1DtQHAADQ4fz9s916c90B3TN7oG4+fUCztzfG6LeXjJQx0m8+SJG1R+3oAQAAAAAAv0BACByHMeYNSaskDTXGZBpjbpP0Z0nnGGP2SDrHOy1rbYqktyVtl7RI0j3WWrczlQMAAHQcL369X//8Yq+untRXPzt36EnvJ657Z/30nCH6Yme+Fm3LbcEKAQAAAABoXwx3zgLOSkpKsuvXr3e6DAAAgDbpw83Z+uGbG3X28F568roJCgo8tXscXW6PLv7X1yqqrNWSn56hiE7BLVQpAAAAgI7OGJNsrU1yug6gJdCCEAA6kI0ZJbrn9Q3KKq12uhQAAE7Zij2F+unbmzQpoYf+ec34Uw4HJSkoMEB/uny08str9cinu1ugSgAAAAAA2h8CQgDoIPYXVurWF9fpv1tydNVTq5RRVOV0SeggquvceuDdLXp+xX7G7ALgM1syS3XHK+s1MKaLnrkpSZ2CA1ts3+P6dtcNUxL00qo0bT5Q2mL7BQAAAACgvSAgBIAOoLiyTre8sFbGGD1x7QRV1rl05dMrtTe/wunS0M5V1rp0y4tr9ea6A/rdR9v1P+9vk8vtcbosAB3c/sJK3fLCOnUPC9FLt05Wt84t3w3oz+YMVUyXUP3yva2c1wAAAAAAfoeAEADauZp6t37w0jrlHKzRMzcm6YIxvfXmvClye6yunr9KO3PLnC4R7VRZTb1ueG6N1qWV6NHvj9WdZwzUa2sydPvL61VZ63K6PAAdVH5ZjW54bo2spFdum6xeXTu1yvt07RSshy4aqZTsMr20Kr1V3gMAAABwynMr9uuX721VPTfDATgGAkIAaMc8HqufvLVJGw+U6rHvj9PEhEhJ0rDYrnrrjqkKCgjQ1fNXa1vWQYcrRXtTUlmn655Zoy2ZB/Wva8brsvHxeuC8YfrjZaO0dHeBvj9/lfLLapwuE0AHU1ZTrxufX6viyjq9cPMkJcZ0adX3O390rGYPjdEjn+5SNuP3wgEr9xbqlhfW6g8fbed3EAAAtJiPt+bo9x9t1+trMvSTtzbJ7WG4EADfRUAIAO3Ynz7eoU+25epX5w/XeaN7H7ZsYEwXvX3HVHUJDdI1z6xWcnqJQ1WivSkor9U1z6zWrrxyzb9x4mG/W9edlqDnbpqk1IJKXfbvldqdV+5gpQA6kpp6t25/ab32FVTo6Rsmamzf7q3+nsYY/e6SUfJYq4cXprT6+wHf2JVbrptfWKtrn12jrVllemFlmmb+9Uv99K1N2pFD7w8AAODk7cgp0/1vb9aEft31/+YM1UdbcvTLBVtlLSEhgMMZTgyAs5KSkuz69eudLgPt0Itf79fDH27XzdP666GLRsgYc9T1skurde0zq5VfXqvnbpqkqQOjfFwp2pPcgzW69tnVyi6t1rM3TtL0wdFHXW9b1kHd+uI6Vde79fT1EzVt0NHXA4CmcHus7n4tWYtT8vSPq8fpknFxPn3/p5bu058/2an5N0zUuSNjffre8C95ZTX6+6e79U7yAYWHBune2YN007T+Kqyo1fMr0vTmugxV1bl1xpAY3TEzUVMHRh3zMx5aj7VWxZV1Si+uUkZRldKLqpReVKn04ipFdArSHy4dpfjIMKfLBI7rha/3a+nuAv3q/OEa3CvC6XKAdqu6zq1deeXakVN26NGvR7j+dPkohQYFOl3ed5RU1uniJ1aozuXRh/dOV8+unfTIp7v0zy/26tbTB+jXFw7ns8UpMsYkW2uTnK4DaAkEhIDDCAhxMj5NydUdrybr7OG99NT1ExUYcPwPd/llNbru2TXKKK7S/BuTdMaQGB9VivYks6RK1z6zRkUVtXrhlsmaPKDHcdfPKq3WLS+s1f7CSv358jG6YmK8jyoF0JFYa/XL97bpjbUZeuiiEbrl9AE+r6He7dFF/1yhg9X1+uynZ6hLaJDPa0DHVlHr0vyl+/TM8v1yeTy6cWp/3Tt7kCLDQw5b72BVvV5dk64Xvk5TYUWtRsd107yZiTpvVKyCAukAqCV5PFY5ZTUNwZ83BMwobnidUVSl8kbjLRsj9e7aSf2iwpSSXabAAKN/XD2ez9Ros5Zsz9MPXl6vACMFBQTontmDdNesgQoJ8p/zSHJ6sf73453q1bWTZgyO1owhMYrr3tnpstCGWWuVW1bjDQHLtd0bBqYVVuqb3jnDQwI1qGcXbc48qAtG99bj14w/4fUYX3K5Pbrx+bVan1ait++cqnHeHjmstfrdR9v1wtdp+uGZg/TTc4c6W2g7R0CIjoSAEHAYASGaa9OBUl09f5WGxnbVm7dPUeeQpt2xVlRRq+ufW6t9+RV64roJOmdEr1auFO1JWmGlrn1mtSpqXXrp1ska3y+ySdsdrK7XXa8ma+W+Iv3k7CH64VmDuBsRQLP8/dNdevyLvbp71kD9fO4wx+pITi/RFU+u1G3TB+jXF45wrA50LC63R2+uO6DHluxWYUWdLhzTWz+fM0z9oo7f+qym3q33NmbpmWWpSi2sVN8enfWD6Ym6MileYSEE2E1V63LrQHH1oeCvcUvAzOJq1bk9h9YNDjTqGxmmflFhSugRpoSocCVEhSkhKkzxkWHqFNzwmTutsFJ3vpqsXXnl+snZQ3Tv7EEKaEMXh4G9+eW69ImVSowJ15PXT9RfPtmphZuzNaRXF/3v5WMOjVvfUbk9Vk8t3ae/f7ZbvSJC5bZWeWW1kqSBMeGaMThGZwyJ0WmJPTif+rFal1t78iq0PadMO3O8rQNzy1RaVX9onb49Omt4bFcN793wGNG7q+IjOysgwOiZZan648c7dOPUBP324pFt5jvw7z/arudW7Nf/fW+Mrkzqe9gya60eeHer3lp/QA+eN0x3nDHQoSrbPwJCdCQEhIDDCAjRHBlFVbr8ya/VOSRQ7919uqK7hDZr+4NV9brxhbVKyTqof1w9XheM6X3ijdDh7ckr13XPrpHLY/XKbZM1sk+3Zm1f5/LogQVbtGBDlr43MV7/e/loBdPKAfB7LrdHxZV1yi+vVWFFrQrKa1VYUed9bnjkl9dqb36Fvp/UV3++YrTjF1d+9d5WvbE2Qwvvna5Rcc07FwKNWWv12fY8/XnRTqUWVGpy/x765QXDD93J31Qej9VnO/I0f1mqktNL1D0sWDdOSdCN0/o3+3Ogv0grrNSCjVn6aEu29hdWqvElj/CQQPWLCvcGgN+GgP16hKlP985NbgVSXefWL9/bqvc2ZunMYT316FXj1C0suJV+IqDpDlbX69InvlZ5Tb0W3jtdfbwt5r7Ymaf/eW+bcspqdNPU/vrZnKEdsrV8flmNfvzWJq3cV6SLxvbRny4bpS6hQdqTX6Fluwu0bE+h1qQWqdblUUhggJL6R2rG4BjNGBytEb27EvZ3UPnlNdqRc3gXofsKKuX2NgvsFBygobFdNaJ3xKEwcGhshLp2Ov55/U8f79D8Zan66TlD9MOzBvviRzmud5Mzdf87m3XztP56+OKRR13H7bH60Zsb9dGWHP3h0lG6fkqCj6vsGAgI0ZEQEAIOIyBEU5VW1enyJ1equLJO7941TQNjupzUfspr6nXri+uUnF6iv105VpdPoFtIf5aSfVA3PLdWgQFGr/3gNA05yfFJrLV6bMke/ePzPZo+KFr/vn7CCb9QAWh5tS63HluyR+9vzFJIUIDCQoIUHhKoziGBCg8JUlhooMK+eR0SpLCQQIWFfjMd2DDvsOlAhYcGKTQoQMYYuT0N43IVHBb6NXquqFVheZ0KK2pVXFWno33VCA8JVHREqGK6hCq6S6iG9+6qe2YPbBPdJx6srtdZjyxVn+6d9N7dp7epLqM6mqKKWn22PU+LUnJVUlWvX8wdqmkDO8Z4tpsOlOpPH+/Q2v3FSowJ1wNzh+mcEb1OOQBfn1asp5elasmOPIUEBuh7E+N1+4xE9Y8Ob6HK26/Sqjp9tCVHCzZkakNGqYyRTh8YrYkJkYdaAfbrEa7oLiEtdiOCtVavrE7X7z/art7dOuvJ6yc0+yYroCW5PVa3vbROK/YU6o15UzSp/+HDBVTUuvR/i3bq5dXp6tOts/5w2SjNHtrToWpb3pe78vWztzerss6l3108SlcmxR/1eK+pd2tdWrGW7ynUst0F2plbLkmK7hKi6YOiNXNIjKYPjlbPiE6+/hH81rasg/rH53u0Ib2kxfdd5/aovObbLqN7d+vkDQG/DQP7R4Wf1Gc+j8fqZ//ZrAUbsvTHy0bputOcC9s2HyjVlU+v0sR+kXr5tsnHvWG33u3Rna8k64td+fr7VWN12XiuCTUXASE6EgJCwGEEhGiKmnq3bnxurTYdKNWrPzjthGPDnUhVnUu3v7xeK/cV6Y+Xjta1p/VroUrRFFml1Xp/Y5Y+2pKjuO6d9aOzBmt0vO8vKG06UKobn1uj8NAgvfaD05R4kqFzY++sP6AHF2zVoJ5d9PzNkw7dtQyg9W3PLtNP396knbnlOnt4L3UJDVRlnVtVdS5V1rpVXedWZZ1LVXVuVda6VOvynHinXgFG6hwcqOp696ExWBrrFBygmIiGwC+mS+i3AaD3OSYiRDFdOik6IqTNd+e1cHO2fvjGRv324pG6aVp/p8tptuLKOv3f4l1ak1p0qGXE9EHR3xnrzgnZpdVanJKrRdtytS6tWB4r9esRJiurA8XV+n5SX/3y/OHttiVWRlGV/rp4pz7akqPoLiH60dlDdPWkvi3eqn5fQYWeXZ6qd5OzVO/xaO7IWM2bmdjk7sE7ijqXR0t3F2jBhkx9viNfdW6PBvfsoismxuvScXGK7eabi/sbMkp096sbVFJVpz9dNpoxmeGYvy7aqX9/te+ErYKS04v1i3e3am9+hS4Z10e/uXCEotpxi+Q6l0d/+3SX5i9L1bDYCP3r2vEa1LPpNz3mldVo+Z5CLd9ToBV7ClVUWSdJGt67q2YOjtaMwTFK6h95qJthtJyU7IN6bMkefbY9T107Bem8Ub0VHNSyN2cFGKOEqHAN7x2hEb27qntYy34eqnd7NO/l9Vq6u0D/vm6C5o7yfS9N+eU1uvifXyso0GjhvdPVowmf+Wrq3br1xXVas79YT1w7QXNHxfqg0o6DgBAdCQEh4DACQpyIx2P1o7c26cPN2frnNeN10dg+LbLfmnq37n5tg77Yma/fXDhCt04f0CL7xdGV19Trk225WrAhU6tTiyVJE/p11978CpXVuHT28F768dmDfdal3bq0Yt3ywjpFhgfr9R9MUd8exx8LqTlW7CnUXa8mKyw0UM/fPIm76YFW5vZYPb1snx79bLe6dQ7RX783WmcOO/E4s26PVVWjwLCqzt3wus6lqtqG5+ojpruEBn0bBDZ6Dg8JdLx70JZirdWNz6/VxoxSLfnpGT4LGU6V22P1+toM/W3xLlXUujQ1MUpbMktVVuOSMdKYuG6aMThGM4fEaHy/7j7rCnp/YaUWbcvVopRcbT5QKkka2itCc0bFau7IWA3vHaFal0ePLdmjZ5anqkd4iH538UidN7r9dINeUlmnf325Vy+vSlNggNG8GYmad8bAVu++L7+8Ri+tTNMrq9JVVuPS5AE9dMfMRM0e2rPDdpNnrdXWrINasCFLCzdnq7iyTlHhIbp4XB9dMSFeI/t0deRcVFhRq/te36hVqUW67rR++s1FIxQaRJgA3/loS7bufX2jrpncT/97+egTrl/rcuvfX+7Tv7/aqy6hQfrNRSN06bi4dve3PKOoSve9sUGbMw/qhikJ+tUFw08pyPN4rLbnlGnZngIt212g5PQS1butOgUH6LQBUZo5JEYzB0drUM8u7e7fqi3ZkVOmfyzZo0UpuYroFKQfTE/ULdP7t9seaKrqXLru2TVKyS7Ty7dO1pTEKJ+9d53Lo2ufWa1t2Qe14K7TNaJP1yZvW1nr0vXPrdG2rIN69qZJOmNITCtW2rEQEKIjISAEHEZAiBP58yc79dTSfXrgvGG6s4UHka5zefSjNzfqk225+vncobp71qAW3b+/c3usVuwt1IINmVqckquaeo/6R4Xp8gnxumx8nPr2CFNZTb1eWJGmZ1ekqrzGpTkje+nHZw/R8N5N/2DfXCv3Fuq2l9ard7dOeu3209S7W8u38tuZW6ZbX1ing9X1euK6CZrVgbovAtqStMJK3f/OZiWnl+j80bH6w6Wjm3TXMI4vvahS5z66TGcN76l/XzfR6XJOKDm9RA8t3KZtWWWamhil314yUkN6Rcjl9mhL1kEt312oZXsKtOlAqdweqy6hQZo6MOpQy4iW7KLSWqsdOeValJKrxdtytSuvoeu2sfHdDoWCx2qxvi3roH7x7halZJdpzshe+t0lo9Sra9sNaGvq3XppZZqe+HKvKmpdunJiX/303CE+r7mi1qW31h3Qc8tTlX2wRoN7dtHtMxN1ybg+HSakyi6t1vubsrRgQ5b25lcoJChA54zopSsmxGnG4Jg2Mfaxy+3R3z7draeW7tPYvt315HUTHOlJwVqrVfuK9PSyVGWXVuuayf30/Ul9Fd4Bx5tDg+3ZZbriyZUa2aerXr99ikKCmn487M4r1y/e3aKNGaU6Y0iM/njZKMVHttyNg63pw83Z+uWCrTJG+uv3xrRKy63KWpdWpxYd6o40tbBSkjSoZxf95sIRmkmg0iy7csv1j8936+OtuYoIDdKt0wfo1ukD1K1z+wwGGyuprNOVT69S3sEavXXH1GYFdafil+9t1etrMk76ZvKDVfW65pnVSi2s0Mu3nnpvVf6CgBAdCQEh4DACQhzPa2vS9av3tum60/rpD5eOapW7FF1uj+5/Z7M+2JStH545SD85Zwh3Q56inbllWrAhS+9vzFJ+ea26dQ7WRWN76/IJ8Rrft/tR/30PVtfruRX79cKK/Sqvdem8UbH60dmDNSy2Zb9YfLkzX3e8mqwBUeF69QenKSai9boTyiur0S0vrNOuvHL94dJRumYyXdkCLcVaq9fWZOiP/92h4ECj3186SheP7cP5uwU98eVe/d/iXXr+5qQmtch0QmFFrf78yU79JzlTsV076VcXDNeFY3of8/fgYHW9Vu0r1DLvhc7MkmpJDd18zhjcMO7S1IFRzb6D3+Ox2pRZ2tBScFuuMoqrFGCkSf17aO6oWJ07MlZxTQxKXG6Pnl2xX49+tlshgQF68PzhunpS3zbVIs7jsVq4OVv/t3iXskqrNWtojB44b1iL/81urnq3R//dkqOnlu7TztxyxXbtpLtnD9T3J/Vtl0FhRa1Li7y9L6xKLZK10qT+kbp8QrzOH927zV5QXrQtRz97Z4tCggL0+NXjNX2wb8bWdLk9+mRbrp5etk/bssoU3SVEfXuEaWNGqbp1DtYNUxJ007T+rfrZryOy1mp/YaXWp5coOa1EyRklCgsJ1ONXj28T438WV9bpon+ukNtjtfC+009q3Dy3x+qVVWn66+JdkqSfnTtUN03r32bH4a2uc+u3H6bozXUHNDEhUv+4epzPQs0DxVVatqdAzyxLVVpRleaOjNX/XDi8TYaq27IO6q+LdymtsFJnDuupOSNjNXlAD0f+X/fkleuxz/fo4605Cg8J0q2n99dt0xPbbZfix5JdWq0rnlwpl8fq3TunqV9U6/5efHO96K5ZA/WLucNOej+FFbX6/tOrlFdWq9dvP01j4ru3XJEdFAEhOhICQsBhBIQ4li935uu2l9Zp1tCemn/DRAW14p3Rbo/VLxds1VvrD2jezEQ9eN4wLjI3U355jRZuyta7G7K0I6dMQQFGs4f11BUT4jR7WM8mX5g7WFWvZ1ek6oWv01RR69IFo3vrR2cP1pBeTR9H41gWbcvVfW9s0NDYCL1862k+aWVUUevSPa9t0NLdBbpn9kD97Nyh/G4Bpyj3YI1+8e4WLd1doBmDo/XX741plZbA/q7O5dEFjy9XVZ1bn/10ZpsaO9Hl9uiV1en6+2e7VVPv1m3TE3XfmYOa1ULIWqu0oiot93ajtmpfkSrr3AoMMJrQr7tmDI7RjMHRGhPf/agXE11uj9buL25oKZiSq7yyWgUHGk0bGK25o2J1zoheij6FMa3SCiv14IKtWpVapNMG9ND/Xj66RcbKPVUr9xXqfz/eqa1ZBzWyT1f98vzhOn2QbwKgprLWavmeQv3ri71am1as3t066e7Zg3RVUnybDwrdHquV+wq1YEOWFm3LVXW9W/16hOnyCXG6bHycEqKcD2SaYl9Bhe56NVl78yt0/7lDddcZA1st5K6qc+ntdQf07Ir9yiypVmJ0uH4wI1GXT4hTp+BAJaeXaP6yffp0e56CAwN0xYQ4/WBGoga2geOpLaqpd2tr1kElp5dofVqJNmSUqNg7Jl23zsGa0K+7Nh0olTFGz9yYpIkJzo39We/26Mbn1io5o0Tv3DFVY/t2P6X9ZZVW61fvbdVXuwo0rm93/eWKMRoae+rfQVrSztwy3fv6Ru0rqNA9swbpx2cPbtXvycdSU+/Ws8tT9a8v90qS7pk1SLfPTGwT4xRmlVbrkcW79N6mLHXrHKzxfbtr5b4i1bo8igoP0TkjemnOqFhNGxjV6n8T9uaX6x+f79VHW7IVFhyom0/vr9tnJLb4OIBtyZ68cl359Cp17xys/9w17ZQ+Cx3PurRiXTN/taYPjtZzN0065eA352C1rnxqVUOvBPOmtrljv60hIERHQkAInARjzFBJbzWalSjpN5K6S7pdUoF3/i+ttR8fb18EhDiarZkH9f35q5QYE6635k31SZdAHo/Vbz9M0Uur0nXj1AQ9fNHINnW3fltUU+/Wp9vztGBDppbtLpDHSmP7dtcVE+J04Zg+pxTAlVTW6dkVqXrx6zRV1bt1weje+vHZgzWo58l9UP9gU5Z++vZmjYnvphdvmezTu+5dbo9+/UGK3liboUvG9dFfvzemzV+gxHd985mRgNdZCzdn69fvb1Oty61fnT9c109J4P+kFa1LK9aVT63SHTMT9eD5w50uR5K0JrVIDy1M0c7ccs0YHK2HLx7ZIhf661webcgo0fI9BVq+p1Bbsw7K2oYL4tMHRWvmkGhNSYzS3vwKLdqWqyU78lRSVa9OwQGaNaSn5o6K1exhPVv074u1Vu+sz9Qf/rtdNS6PfnTWYM2bmejz7iRLKuv00ZaGm4A2HShVXPfO+tmcIbpkbFyb/qxkrdXXe4v06JLdSk4vUZ9unXTPmYN05cS+zeqC0Bd25ZZrwYZMvb8pS3llteraKUgXju2jy8fHaWJCZLs8z1XWuvTAgq36cHO2zh7eS49cNbZFj4/Cilq9vDJNL69OV2lVvSb06647zhioc4b3OurvZWpBhZ5dsV//Sc5Uvdujc4b30h1nDHQ04GoLCsprlZxeouT0YiWnl2hbVpnq3B5J0oDocE1MiFRSQqQmJkRqYEwXBQQY7S+s1C0vrFXOwRo99v1xjo2Z+tsPU/TC12l65MqxumJifIvs09qGFtK//XC7yqrrdfesgbrnzEGOf3b/pueE33+0XV07B+ux749rEzdnZJVW6w8fbdcn23KVEBWmhy8aqdnDnBlaoaymXv/+cp+e/3q/JOnW0wforlkD1a1zsCprXVq6u0CLtuXqi535qqh1KSI0SGcO76m5I2N1xtCYFr0RKrWgQo9/vkcfbM5W5+BA3TStIRj0l27wk9NLdN2zqzW4Z4TemDelxcckzi6t1sX/WqGunYL13j2nt9jflvSiSl351CpZSe/cMbVNtJJuqwgI0ZEQEAKnyBgTKClL0mmSbpFUYa39W1O3JyDEkTJLqnTZv1cqJDBA7909TT19OI6NtVZ//mSnnl6Wqu8n9dWfLh/dZruWcYrHY7U2rVjvbcjSx1tzVF7rUp9unXTZhDhdNj5eg3q27N3YxZV1emZ5ql5amabqercuHttHPzxrcLMuBr+97oB+sWCLJvXvoedvntTiX1CawlqrJ5fu018X7dLkAT00/4aJHfrO0faurKZeO3PKtSOnTDtzy7Q9p1y7cstkZBQdEaKYLqGK7hKqmIjvPsd4nzuHEAK3pNKqOv3P+9v00ZYcjevbXX+/amybaE3lDx54d4veSc7UR/dNb9XxYU8kv6xGf/p4h97flK247p316wuHa87I2FYLTooqavX1viIt212g5XsKlFdWe2hZRKcgnT28l+aMjNUZQ2Ja/XjPL6vRwx+m6OOtuRreu6v+csXoVu/+qs7l0Ze78rVgQ6a+2JmverfV0F4RumpSX113Wr820Uqkqb5pUfjokt3amNEQcN535iBdMTHe0bH7CsprtXBzthZsyFRKdkPvC7OGxujyCfE6c1jPdvVvfCzWWr24Mk1//O8OxUd21lM3TDzlrmjTCiv1zPJU/Sc5U7Uuj84Z0Ut3zExUUv+mjRtVUF6rl1el6eVV6TpYXa+khEjNm5mos48RLHYkHo/VnvyKhtaB3kAwvahKkhQSFKAxcd00sX+kJvZrCASjjtPyp7iyTj94aZ02HijVr84frtumD/BpkP2f5Ez97J3NuvX0AfrNRSNafP/FlXX6w0fbtWBjlgbGhOvPV4zRpCb+jrW0g1X1emDBFn2yLVdnDInRI1eNbbVWWSdr+Z4CPbQwRakFlTp7eE/95sKRrd695DfqXB69ujpd//xij0qr63XZuDjdP2foMbv2rql3a+W+Qi3alqvPtn97s88ZQ2I0d1SszhzW66QDp/2Flfrn53v0/qYshQYF6sZpCZo3I/G4x1JH9cXOPN3+crKmJDZ8B2+pkL2m3q0rn1ql/YWVev+eaSd9A/Gx7Mkr1/fnr1bn4EC9c+dUR8bSbQ8ICNGREBACp8gYc66kh6y1pxtjHhYBIU7Bwep6fe/Jlcotq9G7d01rkW4lm8taq8eW7NE/Pt+jS8b10SNXjnWk25a2JrWgQu9tzNKCDVnKKq1WeEigzhvdW5dPiNOUAVGtfkGlqKJW85el6uVV6ap1uXXJuDjdd+agEwYEr6xK068/SNGMwdGaf0OS46HNB5uy9P/e2aK+PTrrxVsmq2+Ptjdehz/xeKwOlFRpR05DCLgjp0w7csoOjUsmSZFhwRreu6uGxXZVgGlosVBQUavC8joVVNQe6nbrSOEhgUcNEL99DlFMRKiiwkPVKTigXbYO8ZWvduXr5//ZouLKOv347MG684yBnJd9qLSqTmc9slT9osL07p3TfH4Bvd7t0Ytfp+mxJbtV77a644xE3T1rkE/P59Y2XFRfnVqkfj3CNG1gtCMt0Ban5Oo3H2xTQXmtbps+QD85Z0iLtniw1mpz5kEt2JCphZuzVVpVr+guobp0XB9dNiFOI3p3bdfnKmutlu4u0KNL9mjzgVL17dFZ980erMsmxPksKKypd2vJjjwt2JClpbsL5PZYjY7rpsvGx+nicX3a3EX/lrI+rVh3v7ZBZTX1+vPlY3Tp+Lhm72NjRonmL0vVopRcBQcE6HJvV6Ene3NaZa1Lb68/oGeX71dWabUSY8I1b0aiLh0f1yHCWanhZ9ycWarktBKtT2/oLrS8xiVJigoPaWgd2D9SExN6aFRc12ZfwK+pd+snb23SJ9tyddPUBP3mopE+ubly04FSXfX0Kk3qH6mXbpncqp8JvtqVr1+9t01ZpdW6YUqCfj53qCKaOU7tqUhOL9EP39iovLIa/XzuUP1gemKbDbLrXB49//V+Pf75Hrk8VneeMVB3zxrYaseTtVYfb83VXxfvVHpRlU4fFKUHzxuuUXHdmrwPl9ujtWnFWrwtV4u83YUHBRhNGxSt85rRXXh6UaUe/3yv3t+UpeBAoxun9te8mYkd9pzeVO8mZ+r+dzbrgjG99c+rx5/y7661Vve/vVkLNmbpmRuTdM6I1hkje1vWQV0zf7ViIkL11h1TGbv2KAgI0ZEQEAKnyBjzvKQN1tp/eQPCmyWVSVov6X5rbcnxticgxDfqXB7d9PxarU8v1ku3Tta0gc52mfLkV/v0l0U7NXdkrB6/Znyb64qqtVXVubRmf7G35USh9uZXKMBI0wfH6PLxcTp3ZC9HxqMqrKjV00v36ZXV6apzeXTp+Dj98MzBR+3+49nlqfrDf3fo7OE99a9rJ7SZiz1rUos075VkBQcaPXfTpFMeLwVNU1Xn0s7cb0PAHTnl2plTpso6tyQpwDR0pTW8d1cN791VI7zPvbqGHveCeL3bo+LKOhWUfxMcHh4gHpquqFVpVf1R9xFgpLCQIIWFBCo8tOG54RGk8NDAQ8vCQoIUHhKosEbrhIcEKSz0iGXBgQoKbPmLR8GBAT49jiprXfrjxzv0+poMDenVRX+/alyzLvqg5by3MVM/eWuz/nDpKF0/JcFn77tyb6F+szBFe/MrNHtojB66aKTfd/dUVlOvP3+yU6+vyVDfHp31v5eN0fTBp/aZKau0Wu9vzNK7GzKVWlCp0KAAnTsyVpdPiNOMQdEdLpC31urLXfl6bMkebck8qH49wnTfmYN02fi4VvlZrbVan16iBRsy9dGWHJXXuBTbtZMuHR+nyyfEOXJDnBPyy2t07+sbtXZ/sW6amqBfXTDihJ+vPZ6G/6unl6ZqbVqxunYK0vVTEnTztP4t1suIy+3Rf7fmaP6yVKVklykmIlQ3T+uv609LULcw3wVBTVXrcquoouFzR2FF7RHPh88vr3Ud2m5Iry6amNDjUJehCVFhLRL4ezxW//vJDj2zfL/OHt5Lj18zrlW/I+SX1eiif61QcGCAPrx3uiJ90GVjZa1Lj3y6Wy+s3K9eEZ30h0tH6azhPVv1hgmPp6H3kb9/tltx3Tvr8WvGa1w7+c6Qe7BGf/x4hz7cnK34yM76zYUjdM6IXi3677U+rVh//HiHNmaUamivCD1w/jDNGhJzSu/h8VhtyizV4m25+mRbrjKKqxRgpKT+PTR3ZKzmjIr9TqvEjKIq/fOLPVqwMUtBAUbXT0nQHWckqmeE73pBauueXrpP//vJTt00NUEPXzzylP6Pvvl+/9NzhuiHZw1uwSq/a31asW54bq0SosL05rwp9P5zBAJCdCQEhMApMMaESMqWNNJam2eM6SWpUJKV9HtJva21tx5lu3mS5klSv379Jqanp/uwarRFje8Ee/T7Y3XZ+JYZQ+JUvfD1fv32w+2aPTRGT14/sc0ETK3BWqsdOeVatqehK7V1+0tU5/YoNChAkwf00BlDYnTR2D7q5cMuX48nv7xGTy9N1aur0+XyWF3mDQq/6crmn5/v0SOf7dYFo3vr0e+Pa3MB7978Ct3y4lrllNZo6sAozfXeocqXyVNjrVWty6PCitpDXYTuyG0IA9OKKvXNx76I0CBvEBhxKBAc0iui1Vsk1bk8Kqo8/GJeUWWdquvcqqx1q6rOpaq6hufDp92qrHOpqtZ9aFwgJwQYaXjvrkpKiNSEhEgl9e9xzO6bTtX6tGL99O3NOlBSpdtnJOqn5wzp0Ofgts5aq+ufW6OV+4o0ICr8O8dP726dWvTCX87Bav3hvzv03y056tujsx66cGSrX4xtb9akFunBBVuVWlipKyfG61cXDG/Wxavymnp9si1XCzZkanVqsSRp8oAeumJCnM4b3VtdfdhCxinWWn2+I1+PLtmtlOwy9Y8K031nDtYl4/q0SFCYVlipBRuz9N7GTB0orlZYSKDmjorVFRPiNSUxyi+7sa93e/SXT3bq2RX7NaFfdz1x3QT17vbdvyO1Lrc+2Jit+ctTtTe/Qn26ddKt0wfo6sn9Wq2reGutVu4r0lNL92n5nkKFhwTq6sn9dOv0Aa32t+4b9W6PiirqDn02KPhO8PdtAHiw+ug3G3XtFHSop4LoRl2ej+jTVRP6RrZ62PnyqjQ9vDBFo+O66dmbJrVKi5tal1vXzF+tHTnlWnD3NJ93e70xo0QPvLtVu/LKFR4SqH5R4UroEaaEqDAlRIUrISpM/XqEqU/3zqd0fOeX1+inb23Wir2FunBMb/3p8tHt8py8al+RHlq4TbvzKnTGkBg9fPFIDTjFm3z2FVTor4t2anFKnnp1DdX95wzVFRPjW/x8aq3VztxyLdqWq0XbcrUrr1ySNCa+m+aMjNVpA3ronfWZendDpgICjK47rZ/uOmOgT4dHaU/++N/temb5fv3s3CG698yTC/ZW7CnUjc+v0bkjYvXv6yb4pCXtij2FuvXFdRrep6te+8FpjgxV0lYREKIjISAEToEx5hJJ91hrzz3Ksv6SPrLWjjrePmhBCEn6+6e79PgXe3X/OUN0XyvfCdZcr6/J0K/e36rQoAAN7fXtxdDhvbtqWO+Idvll7RsF5bVasbdAy3YXavmeQhVWNIyvNCw2QjMGR2vG4BhNHtCjTV+Uzy+r0ZNL9+m1NRlye6yumBCniE7Bem7Ffl0+Pk5//d6YNtvyobCiVs+t2K9F23K1v7BSxkhJCZGaMzJWc0bG+kX3o1V1LhVX1h0KwapqXaqs+zYYq6z9NiD7JjSrrj9KeFb7bbDmOeKjXUJUmIbHdj0s0IiP7Nxug4Z6t+fwf49vwsNG0xW1Lnla4TNuSVWdNmaUatOBUlV5W1727tapISxMiFRSQg8N7x1xSsdcrcutx5bs0dNL96lP98565MqxOi0xqqV+BJyCoopavbo6Q9tzDmpHTrkyiqsOLeseFqxhsRGHtcAd1LNLs/9+1Lrcem7Ffv3z873yWKu7Zw3SHWcktum/Q06qqXfrn1/s0dNLU9U9LFgPXzxSF4zufczzm9tjtWJvoRZsyNTilFzV1HvUPypMl0+I12Xj4/zi787RWGv12fY8PbZkj7bnlCkxOlz3nTVIF4+Na/ZF54NV9fpoa7YWbMhScnqJjJFOHxityyfEac7IWIVzcVGS9N8tOfp//9mssJBAPX7N+EM9hxysrtfrazL0wtf7lV9eq2GxEbrzjIG6YExvn44XuT27TPOX7dOHW3JkJF00to/mzUxsViDl+qaHgaO07jsy+Cs5Rg8DEaFBh8K+Y46BHBGqqPCQNnGeXLI9T/e9sVFRXUL04i2TWnRsMGutHlywVW+uO6Anrp2gC8b0brF9N0edy6P3NmYe+juYVlSpzOLqw27gCg40io/0Boc9wg4Fif2jwxQfGXbc/6uluwt0/9ubVFHr0m8vHqmrkvq228+sUsPn1pdWpumxJXtU5/Lo9pkDdM/sQc1uZVpYUat/LNmj19dmqFNQgO48Y6BumzHAZz3apBZUaHFKnhal5GrzgVJJDeN2Xju5n+6aNbDN3ETbVnk8Vj97p+GG8P+9fLSumdyvWdtnFFXp4idWqFdEJy24e5pP/5Z+mpKru17boEn9I/XiLZPbxLm2LSAgREdCQAicAmPMm5IWW2tf8E73ttbmeF//RNJp1tqrj7cPAkK8ve6Afv7uFn0/qa/+fMXoNvkF6Ou9hfpiZ/6hrgkbf4mPj+zc6IJow8XRvpFhbXJsiFqXW8lpJVq6p0DLdxdqe06ZJKlHeIimD4rWzCExmjE4ul1+wck9WKMnv9qrN9YeUJ3bo2sm99UfLx3dJv8fjmSt1e68ioY7VFNytcP7/zIqrqvOG9Vbc0bGnvT4Om2FtVaZJdWHuvb8pmVfelHViTdWwxfwQ91pHq/7zdBAdQ4JVPfOIRoa20VDY7typ2crcLk92pFTruT0Yq1PL1FyeolyDtZIkjoHB2pc3+7eMY0iNb5fpLp1btqNFDtyyvSTtzZpZ265rp7UV/9z4Qj+/9qw8pp67fJ22/vNGJ67cstVXd8QHgcGGA2MCT/sxprhvSOO2VJ66e4C/XZhilILK3XuiF769YUj/Dawaq7t2WV6YMEWbck8qLOH99TvLx11WKusnbllWrAhS+9vzFJ+ea26dQ7WRWN76/IJ8Rrft3ub/OzlBI/H6tPteXpsyW7tzC3XwJhw/fCswbpwTJ/jBoX1bo+W7irQgo2ZWrI9X3Vujwb37KLLJ8Tr0vF9jtpCDtLe/HLd8Uqy9hdW6idnD1FZTb3eWHtAFbUuTR8UrXkzEzVjcLSjv59ZpdV6fsV+vbE2Q1V1bs0YHK3bZyQqJiL0hN17FlfV6WiXm8Iaj1F8KPjr9G0A2Kj1X3u8EL0ls1S3vrhedS635t+YpCktdJPPK6vT9ev3t+me2QP1/+YMa5F9thS3xyq3rEbpRZVKL6pSelGVMoobXmcUVR3W1asxUmzXTurXI0z9o8LVL+qbIDFcH23J1tPLUjUsNkL/vGa8Bneg7ofzy2v05493asHGLPXp1kn/c+EInTcq9oTHd3WdW8+tSNVTS1NVXe/WNZP76kdnDXF0TLjs0mqt3V+sKYlRiu3W/r43O6Xe7dHtL6/Xst0FevL6iZozMrZJ21XWunTFkyuVc7BGC+89XQlRvu9q/oNNWfrxW5t0xpAYzb8hqc31TuQEAkJ0JASEwEkyxoRJOiAp0Vp70DvvFUnj1NDFaJqkO74JDI+FgNC/LdtdoFteXKdpA6P0/M2TfHpn8Mmy1iqvrNZ7QbTsUGi4v7DyUMul8JBADY09orVhbITP7xq31mpfQYWW7S7Usj0FWpNarOp6t4IDjSYmRGrG4BjNHByjkX26tosgrSlyDlZr84GDmjOyZce58KW0wkotTmkICzdmlEqSBvXsorkjYzV3VKxG9unapn+2mnr3odDgUCCYW6bymoaLI8ZI/aPCNbx3hIbFdlVs104KC20I/zofNqbet+Ffezg3+Lvs0uqGsDCtWMkZJdqeXSaPbfj/HtIzQhP7R2piv0gl9Y9Uvx6Hj3vk9ljNX5aqv3+2S906h+gvV4zWWcN7OfjT4GS5PVbpRZXf3gjgfWR7A2RJiu4SomGx37bo7dsjTM8uT9XilDz1jwrTwxeP1KyhPR38Kdonl9ujF1em6W+f7lJQQIB+du4QuTxWCzZkaXtOmYICjGYP66krJsRp9rCeCg1qf8GDr3g8VotScvWPJXu0K69cg3p20Y/OGqwLRvc+9HnJWqutWQe1YEOWFm7OVnFlnaLCQ3TxuD66fHy8RsW17b/VbUVFrUs//89mfbw1V4EBRheM7q15MxPb3HizB6vq9eqadL3wddqhHjcaCw0KUEzEES37vM8xXUK+7fqzS6hftCI9UFylm19YqwPF1fq/K8foknFxp7S/NalFuu7ZNZo5JEbP3pjUrr63WGtVXFmn9OKGsLAhQKxUenHD6yN/n66f0k//c8GIdhkON8W6tGL95oMU7cgp0/RB0Xr44hFHbWnq9li9m5ypRz7bpbyyWp0zopd+MXdYu79p0t9V1bl07TNrtD2nTK/cOvmEvYRYa3XP6xu0aFuuXrp1smYMjvFRpd/1+poM/fK9rTp/dKwev3p8m+2lyFcICNGREBACDiMg9F/bs8t01dOrFB/ZWe/cOVUR7birTqnh7sbdeccPRRJ6hB0WGg6MCW/x4MNjrbZllWnZ7oaxBL+5KJsYHa4ZgxtaCZ6WGEWrnHYi92CNPt3eMPbF6tQieawU172z5o6K1XmjYjWhX6RjF0msbbhb+pvf928C87QjwvJhR4xVNrSX78Ny+F5lrUubDpQqOb1E69NLtDG95NAd9NFdQpWU0NDCcEhshB7/fI+S00t03qhY/fGy0eoR3vRx1NA+lFbVHR4a5pZpd16F6lwNXbJ1Dg7UvWcO0g9mDCC4OkUZRVX61ftbtXxPoSRpbHw3XT4hXheN7cOx1Uwej9XH23L0jyV7tCe/QkN6ddHdswYp+2C1FmzI0t78CoUEBuicEb10+YQ4zRwSww0tJ8Faqy925mtIr4g232q4pt6tL3bmS1KjIDBEXUKDCISPcLCqXvNeWa81+4v1/+YM1d2zBp7Uv1FWabUu/ucKdQsL1vv3nN6uh3c4mspalzKKG0LDyLAQv+hW3eX26PW1Gfrb4l2qqnPrtukDdN9Zg9UlNEjWWi3dXaA/f7JTO3PLNbZvd/3q/OGaPKCH02WjhZRU1ul7T61Ufnmt3r5j6nG7bv7XF3v0t09361fnD9ftMxN9WOXRPbs8VX/47w59b2K8/nrFmHZ1s0JLIyBER0JACDiMgNA/WWt1zqPLVFHj0nv3TOuwXS99063iztzDW1KkNbFbxVMR0SlI0wc1jCM4Y3B0m7/gghMrrqzTku0NY1+s2FOoOrdHMRGhOndEL80dFaspiVEtfmGy8Xh3RRV1h3URujO3/Xa3C99ze6z25JdrfVqJNnhDw2/GsIvoFKTfXzJKl4zrwwVWP+Jye5RaWKm9+RUa17e7+nTvmJ8FnGCt1arUIvWMCG3RMcD8ldtj9d+tOfrHkt3aV1ApqWHM4MsnxOuC0b3VLaxjBRZAS6l1ufXz/2zRB5uydc3kvvr9JaOa1eqmus6tK59eqfTCKr13z+m0HutgCitq9X+Ldumt9QfUq2uo7pk9SJ+m5GnF3kL16xGmn88detxxddF+ZZVW63tPrmzo6eCuaUe9VvH5jjz94OX1unRcnP5+1dg283vw2JLdemzJHt00NUEPXzyyzdTlawSE6EgICAGHERD6p1255Zrz2DL98bJRuu60BKfL8bnKWpd25pYrvejbllYtaUB0mMbGd/f7bi86svKaen2xM1+LU3L15c4CVde71a1zsM4a3lNzR8aqT/fOqqpzq7LOpeo6typrXUdMN4R+lXVuVde5Dk03hIEN61XVulXn9nznvTsFB2horyO60O0d0eHu6Ebryy+r0bbsgxrZp1u7HPsUgH9xe6xW7StS3x6dHRkDCWiPrLV65NPd+teXe3XGkBg9cd2EJvVkYq3Vj9/apIWbs/XsjUl0Pd6Bbcwo0W8+SNHWrIPqHhas+84crOun9KNHgQ5ud165rnxqlXqEh+idO6cqusu340ruza/QpU98rf7RYfrPndPaVJe71lr96eMdemb5fv358tG6enI/p0tyBAEhOhICQsBhBIT+6fHP9+jRJbu15pdnqWcEF4WBU1FT79ay3QVatC1XS3bkqczbre2xGKOGcf5Cvh3nLzz02+fOwY2mQwIbxgUMDVK3zsEaGhuh/lHhCqRVIAAAAJrozbUZ+tX72zS0V4Sev3mSYrsd/zvgM8tS9cePd+hn5w7RvWcO9lGVcIrbY5WcXqKhsRHq1pmbDv1Fcnqxrnt2jYb0itDrt09Rl9AgldXU69J/fa2D1fVaeN90xbXBHiastXp1TYaunBjfpsJLXyIgREdCQAg4jIDQP13w+HJ1Cg7Uu3dNc7oUoEOpc3m0Pq1Y5bUuhYcEecO9wEaBYJA6BQf4bVcoAAAAcMbS3QW6+9Vkde0crBdumaRhsUcfe2zZ7gLd/MJazR0VqyeuncDnVqAD+2Jnnm5/OVlTE6P07E1Juvu1DVq2u0Cv3z6FsSfbMAJCdCQEhIDDCAj9z4HiKs3465f65fnDNG/mQKfLAQAAAAD4QEr2Qd364jpV1rr15PUTNGNwzGHL04sqdfG/vlbvbp307l3TFN6E7kgBtG//Sc7Uz97ZrL49OutAcbV+f+ko3TDF/4aiaU8ICNGRMDgTAPjYp9vzJEnnjoh1uBIAAAAAgK+M7NNN7919uuIjO+uWF9bp7fUHDi2rqHXp9pfXyxhp/g1JhIOAn/jexHg9cN4wHSiu1jWT++r60/xzXD8AzuDTBgD42OKUXA3tFaH+0eFOlwIAAAAA8KE+3Tvr7Tun6p7XNujn/9mizJJq/fiswbr/7U3am1+hl289Tf2iwpwuE4AP3TEzUTMHx2hIry50KwzApwgIAcCHiipqtT6tWPfOHuR0KQAAAAAAB3TtFKznb56kXy7Yqsc/36NPU3K1M7dc/3PBcE0fHO10eQB8zBijEX2OPi4pALQmuhgF4IiaercWp+TK38ZBXbIjTx4rnTuS7kUBAAAAwF8FBwbor98bo/vPGaKdueW6fHycbps+wOmyAACAH6EFIQBHvL8xSw8s2Kp375qmiQmRTpfjM4tT8hTXvbNGcmcYAAAAAPg1Y4zuO2uwLhzbR/16hNG1IAAA8ClaEAJwxEVj+6hLaJBeW53udCk+U1Hr0oo9hZozMpYvfgAAAAAASdKA6HAFBvAdEQAA+BYBIQBHhIcG6bLxcfpoS46KK+ucLscnvtqVrzq3R3NG9nK6FAAAAAAAAACAHyMgBOCY66ckqM7t0TvrDzhdik8sTslTVHiIkvr3cLoUAAAAAAAAAIAfIyAE4JihsRGa3L+HXluTIY/HOl1Oq6p1ufXlznydPbwXXccAAAAAAAAAABxFQAjAUddPTVBGcZWW7SlwupRWtWpfkSpqXZoziu5FAQAAAAAAAADOIiAETpIxJs0Ys9UYs8kYs947r4cx5jNjzB7vc6TTdbZ1c0fGKrpLiF5dneF0Ka1qcUqewkMCNW1gtNOlAAAAAAAAAAD8HAEhcGpmW2vHWWuTvNMPSPrcWjtY0ufeaRxHSFCArkrqqy925imrtNrpclqF22P12fY8zRrWU52CA50uBwAAAAAAAADg5wgIgZZ1iaSXvK9fknSpc6W0H9ee1k9W0htrOmYrwo0ZJSqsqNW5I+heFAAAAAAAAADgPAJC4ORZSZ8aY5KNMfO883pZa3Mkyfvc07Hq2pH4yDCdObSn3lx3QHUuj9PltLjFKbkKDjSaPYxfBwAAAAAAAACA8wgIgZN3urV2gqTzJN1jjJnZ1A2NMfOMMeuNMesLCgpar8J25PopCSqsqNWn23OdLqVFWWu1OCVP0wZGq2unYKfLAQAAAAAAAACAgBA4WdbabO9zvqT3JE2WlGeM6S1J3uf8Y2w731qbZK1NiomJ8VXJbdrMITHq26OzXlmV7nQpLWpnbrkyiqs0Z2Ss06UAAAAAAAAAACCJgBA4KcaYcGNMxDevJZ0raZukhZJu8q52k6QPnKmw/QkMMLp2coLW7C/Wnrxyp8tpMYtTcmWMdA7jDwIAAAAAAAAA2ggCQuDk9JK0whizWdJaSf+11i6S9GdJ5xhj9kg6xzuNJroqKV4hgQF6bU2G06W0mMUpeZrYL1IxEaFOlwIAAAAAAAAAgCQpyOkCgPbIWpsqaexR5hdJOsv3FXUMUV1Cdf7oWL2bnKn/N2eowkPb9ynqQHGVduSU6VfnD3e6FAAAAAAAAAAADqEFIYA25fopCSqvdWnh5mynSzlli1NyJYnxBwEAAAAAAAAAbQoBIYA2ZWJCpIbFRuiVVemy1jpdzin5NCVPw2Ij1C8qzOlSAAAAAAAAAAA4hIAQQJtijNH1UxK0PadMGw+UOl3OSSusqNW69GJaDwIAAAAAAAAA2hwCQgBtzqXj4xQeEqhXV6c7XcpJW7I9T9bSvSgAAAAAAAAAoO0hIATQ5nQJDdJlE+L00ZYclVTWOV3OSVmckqu+PTpreO8Ip0sBAAAAAAAAAOAwBIQA2qTrpySozuXRO8kHnC6l2cpr6vX13iLNGRErY4zT5QAAAAAAAAAAcBgCQgBt0rDYrprUP1KvrcmQx2OdLqdZvtpVoDq3R+fSvSgAAAAAAAAAoA0iIATQZl0/JUHpRVVasbfQ6VKaZXFKrqLCQzQxIdLpUgAAAAAAAAAA+A4CQgBt1txRsYoKD9Erq9OdLqXJal1ufbWrQOeM6KXAALoXBQAAAAAAAAC0PQSEANqs0KBAXTWprz7fkafs0mqny2mSlXuLVFHr0hy6FwUAAAAAAAAAtFEEhADatGsn95OV9ObaDKdLaZLFKbnqEhqkaYOinC4FAAAAAAAAAICjIiAE0Kb17RGm2UN76o11B1Tv9jhdznG5PVafbc/TrKExCg0KdLocAAAAAAAAAACOioAQQJt3/ZR+Kiiv1acpeU6XclzJ6SUqqqyje1EAAAAAAAAAQJtGQAigzTtjSE/FR3bWK6vTnC7luBan5CokMECzhsY4XQoAAAAAAAAAAMdEQAigzQsMMLr2tH5anVqsvfnlTpdzVNZafbo9V6cPilJEp2CnywEAAAAAAAAA4JgICAG0C1cl9VVwoNGrqzOcLuWoduSU60BxNd2LAgAAAAAAAADaPAJC4CQYY/oaY740xuwwxqQYY37knf+wMSbLGLPJ+zjf6Vo7iuguoTp/dG+9m5ypqjqX0+V8x+KUXAUY6ewRvZwuBQAAAAAAAACA4yIgBE6OS9L91trhkqZIuscYM8K77FFr7Tjv42PnSux4rp+SoPJalxZuyna6lO9YnJKrpIQeiu4S6nQpAAAAAAAAAAAcFwEhcBKstTnW2g3e1+WSdkiKc7aqji8pIVJDe0XoldXpstY6Xc4hGUVV2plbrnNH0noQAAAAAAAAAND2ERACp8gY01/SeElrvLPuNcZsMcY8b4yJdK6yjscYo+unJiglu0ybDpQ6Xc4hi1NyJYnxBwEAAAAAAAAA7QIBIXAKjDFdJL0r6cfW2jJJT0oaKGmcpBxJjxxju3nGmPXGmPUFBQW+KrdDuGx8nMJDAvXq6gynSzlkcUquhvfuqr49wpwuBQAAAAAAAACAEyIgBE6SMSZYDeHga9baBZJkrc2z1rqttR5Jz0iafLRtrbXzrbVJ1tqkmJgY3xXdAXQJDdKl4+P00ZZslVTWOV2OCsprlZxRojl0LwoAAAAAAAAAaCcICIGTYIwxkp6TtMNa+/dG83s3Wu0ySdt8XZs/uH5KgmpdHv0nOdPpUvTZ9jxZS/eiAAAAAAAAAID2g4AQODmnS7pB0pnGmE3ex/mS/mqM2WqM2SJptqSfOFplBzW8d1clJUTqtTXp8niso7UsTslVvx5hGhYb4WgdAAAAAAAAAAA0VZDTBQDtkbV2hSRzlEUf+7oWf3X9lAT9+K1N+npfoWYMdqab1rKaeq3cV6ibp/VXQ6NSAAAAAAAAAADaPloQAmiXzhsdqx7hIXplVbpjNXy5M1/1bkv3ogAAAAAAAACAdoWAEEC7FBoUqKuS/n97dxurd3nXAfz7O6dPKy1pS5+wT4C2UEpYYQyqsKbbOlqVCG+MU0eWvSEmM07jQ6bJNBpJ9I1xLxbjxE0StpGFqUO3UWBuuogM6BzS0zIFLKVAz+FhPG6s0F6+OPdmw8jiOdzn3N7n//kkzbn/V0/u63fSfM/Tt/f135A7D43nyee/O5AZbj84npVLFubijcsHsj8AAAAAAEyHghAYWr982ca0JJ+557FZ3/uVV0/kqw9O5D3nr8nIiONFAQAAAAAYHgpCYGhtWLE4u7asys33HMmrJ07O6t53Pfx0Xj5+Inu2rZnVfQEAAAAA4M1SEAJD7X07NmXixe/ljoPjs7rvvgPjWbpwXn7qx1fO6r4AAAAAAPBmKQiBobbr3NVZt+wtuenuR2dtzxMnW+48NJ5d563Ognk+jQIAAAAAMFz8ZhsYaqMjlV+6bGPueviZPDTx0qzsed/hZ/PMy8cdLwoAAAAAwFBSEAJD7xfeviHzRyuf+vrsvIpw39h4Fswbya5zV8/KfgAAAAAA0E8KQmDorVyyMHsvODO37D+a7xx/bUb3aq1l39ixXPETK7Nk4bwZ3QsAAAAAAGaCghCYE67dsSkvvvJa/uH+J2Z0n7EnXsjjz33X8aIAAAAAAAwtBSEwJ7z9rOXZsmZJbrr7yIzuc/vYsYxUsnurghAAAAAAgOGkIATmhKrKtTs25YHHn8/9jz03Y/vsGxvPJWetyBlLFs7YHgAAAAAAMJMUhMCccc1F67J4wWh+5ab9uf4LB7P/0Wdz8mTr2/MffvrlfGv8xezZtrZvzwkAAAAAALNt3qAHAOiXpYvm5y+vfVs++a+Hc+Ndj+avvvbfWb10Ya7ctiZ7t52Zy85Zkfmj0/9/EfvGjiVJrjzf8aIAAAAAAAwvBSEwp7xj86q8Y/OqvPDKq/nKgxPZN3Ysn9v/eG66+0iWLZ6f3VvXZO+2tbli88osmj86pee+/eB4tv3Y6dmwYvEMTQ8AAAAAADNPQQjMSacvmp+rt6/L1dvX5ZVXT+Sf//Op7DtwLPvGjuWW/Udz2oLR7DpvdfZuW5t3nrc6Sxb+6E+HEy++km8c+XZ+Y/eWWfoIAAAAAABgZigIoc+qam+SjyYZTXJDa+1PBjxS5y2aP5o929Zmz7a1Of7aydz9yDP50oFjuePgsXzhP57Mgnkj2bl5ZfZsW5vdW9dk+WkLfug57jg4ntbi/oMAAAAAAAw9BSH0UVWNJvlYkvckOZrk3qq6tbV2cLCT8X0L5o1k55ZV2bllVf74mguy/9Fv57beKwvvPDSR0ZHKjnNWZG+vUFx9+qIkyb6x8Ww6Y3G2rFky4I8AAAAAAADeHAUh9NelSR5qrT2SJFV1c5KrkygI/x8aHalcevaKXHr2inzkqq154PHnc9uBY7ntwLF85PNj+f1bx3LxxuXZvXVN/u3hp/OBy89OVQ16bAAAAAAAeFMUhNBf65I8dsr10SSXDWgWpqCqcuH6Zblw/bL89p5z89DES/lSryz809seTOJ4UQAAAAAA5gYFIfTXG728rP3QO1Vdl+S6JNm4ceNMz8QUVVU2r1mazWuW5tfevTlHnvlODj/zct62afmgRwMAAAAAgDdtZNADwBxzNMmGU67XJ3ni9e/UWvt4a+2S1tolq1atmrXhmJ6NZyzOzi3+nQAAAAAAmBsUhNBf9ybZXFVnV9WCJO9NcuuAZwIAAAAAAPgBR4xCH7XWXquqX02yL8lokk+01sYGPBYAAAAAAMAPKAihz1prX0zyxUHPAQAAAAAA8EaqtTboGaDTquqpJI8Oeo4BWZnk6UEPAXOEPEF/yBL0jzxBf8gS9IcsQf90OU+bWmurBj0E9IOCEBiYqrqvtXbJoOeAuUCeoD9kCfpHnqA/ZAn6Q5agf+QJ5oaRQQ8AAAAAAAAAzB4FIQAAAAAAAHSIghAYpI8PegCYQ+QJ+kOWoH/kCfpDlqA/ZAn6R55gDnAPQgAAAAAAAOgQryAEAAAAAACADlEQAgNRVXur6ltV9VBVfXjQ88AwqapPVNVEVR04ZW1FVd1RVf/Ve7t8kDPCMKiqDVX1lao6VFVjVfWh3ro8wRRU1aKquqeq7u9l6Q9767IE01BVo1X171X1j71rWYJpqKrDVfVAVX2zqu7rrckTTFFVLauqW6rqwd7PTj8pSzA3KAiBWVdVo0k+luSnk5yf5Ber6vzBTgVD5W+S7H3d2oeTfLm1tjnJl3vXwI/2WpLfbK1tTbIjyQd7X4/kCabme0ne1Vp7a5LtSfZW1Y7IEkzXh5IcOuValmD63tla295au6R3LU8wdR9Ncltr7bwkb83k1yhZgjlAQQgMwqVJHmqtPdJaO57k5iRXD3gmGBqttX9J8uzrlq9OcmPv8Y1JrpnNmWAYtdaebK19o/f4xUz+oLsu8gRT0ia91Luc3/vTIkswZVW1PsnPJrnhlGVZgv6RJ5iCqjo9yc4kf50krbXjrbXnIkswJygIgUFYl+SxU66P9taA6VvTWnsymSw9kqwe8DwwVKrqrCQXJfl65AmmrHck4jeTTCS5o7UmSzA9f57kd5KcPGVNlmB6WpLbq2p/VV3XW5MnmJpzkjyV5JO9469vqKrTIkswJygIgUGoN1hrsz4FACSpqiVJPpfk11trLwx6HhhGrbUTrbXtSdYnubSqLhjwSDB0quqqJBOttf2DngXmiMtbaxdn8vYmH6yqnYMeCIbQvCQXJ/mL1tpFSV6O40RhzlAQAoNwNMmGU67XJ3liQLPAXDFeVWcmSe/txIDngaFQVfMzWQ5+qrX2t71leYJp6h059dVM3itXlmBqLk/yc1V1OJO3YXhXVd0UWYJpaa090Xs7keTvMnm7E3mCqTma5GjvdIgkuSWThaEswRygIAQG4d4km6vq7KpakOS9SW4d8Eww7G5N8v7e4/cn+fwAZ4GhUFWVyXtpHGqt/dkpfyVPMAVVtaqqlvUevyXJ7iQPRpZgSlprv9taW99aOyuTPyP9U2vtfZElmLKqOq2qln7/cZIrkxyIPMGUtNaOJXmsqs7tLb07ycHIEswJ1ZpT/YDZV1U/k8n7a4wm+URr7frBTgTDo6o+k2RXkpVJxpP8QZK/T/LZJBuTHEny8621Zwc0IgyFqroiydeSPJD/vdfT72XyPoTyBP9HVXVhkhsz+X3dSJLPttb+qKrOiCzBtFTVriS/1Vq7SpZg6qrqnEy+ajCZPCLx06216+UJpq6qtie5IcmCJI8k+UB63/NFlmCoKQgBAAAAAACgQxwxCgAAAAAAAB2iIAQAAAAAAIAOURACAAAAAABAhygIAQAAAAAAoEMUhAAAAAAAANAhCkIAAAAAAADoEAUhAAAAAAAAdIiCEAAAAAAAADpEQQgAAAAAAAAdoiAEAAAAAACADlEQAgAAAAAAQIcoCAEAAAAAAKBDFIQAAAAAAADQIQpCAAAAAAAA6BAFIQAAAAAAAHSIghAAAAAAAAA6REEIAAAAAAAAHaIgBAAAAAAAgA5REAIAAAAAAECHKAgBAAAAAACgQxSEAAAAAAAA0CEKQgAAAAAAAOgQBSEAAAAAAAB0iIIQAAAAAAAAOkRBCAAAAAAAAB2iIAQAAAAAAIAOURACAAAAAABAhygIAQAAAAAAoEMUhAAAAAAAANAhCkIAAAAAAADoEAUhAAAAAAAAdIiCEAAAAAAAADpEQQgAAAAAAAAdoiAEAAAAAACADlEQAgAAAAAAQIcoCAEAAAAAAKBDFIQAAAAAAADQIQpCAAAAAAAA6BAFIQAAAAAAAHSIghAAAAAAAAA6REEIAAAAAAAAHaIgBAAAAAAAgA5REAIAAAAAAECHKAgBAAAAAACgQxSEAAAAAAAA0CEKQgAAAAAAAOgQBSEAAAAAAAB0iIIQAAAAAAAAOkRBCAAAAAAAAB2iIAQAAAAAAIAOURACAAAAAABAhygIAQAAAAAAoEMUhAAAAAAAANAhCkIAAAAAAADoEAUhAAAAAAAAdIiCEAAAAAAAADpEQQgAAAAAAAAdoiAEAAAAAACADlEQAgAAAAAAQIcoCAEAAAAAAKBDFIQAAAAAAADQIQpCAAAAAAAA6BAFIQAAAAAAAHSIghAAAAAAAAA6REEIAAAAAAAAHaIgBAAAAAAAgA5REAIAAAAAAECHKAgBAAAAAACgQxSEAAAAAAAA0CEKQgAAAAAAAOgQBSEAAAAAAAB0iIIQAAAAAAAAOkRBCAAAAAAAAB2iIAQAAAAAAIAO+R9U1umWDuYYWwAAAABJRU5ErkJggg==\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "%pylab inline\n", - "from IPython.display import display, Image\n", - "display(Image(filename='output/RetailerFacility_112_2_productstore.png'))" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "plt.imshow?" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.10" - }, - "metadata": { - "interpreter": { - "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/supply_chain/single_launcher.ipynb b/notebooks/supply_chain/single_launcher.ipynb deleted file mode 100644 index d8c39154e..000000000 --- a/notebooks/supply_chain/single_launcher.ipynb +++ /dev/null @@ -1,835 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "acd7d48c", - "metadata": {}, - "outputs": [], - "source": [ - "from maro.simulator import Env\n", - "from pprint import pprint" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "681386af", - "metadata": {}, - "outputs": [], - "source": [ - "env = Env(scenario=\"supply_chain\", topology=\"sample\", durations=64)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "875f0f8a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[AgentInfo(id=1, agent_type='facility', is_facility=True, sku=None, facility_id=1, parent_id=None),\n", - " AgentInfo(id=8, agent_type='facility', is_facility=True, sku=None, facility_id=8, parent_id=None),\n", - " AgentInfo(id=17, agent_type='facility', is_facility=True, sku=None, facility_id=17, parent_id=None),\n", - " AgentInfo(id=28, agent_type='facility', is_facility=True, sku=None, facility_id=28, parent_id=None),\n", - " AgentInfo(id=6, agent_type='product', is_facility=False, sku={'init_stock': 100, 'product_unit_cost': 1, 'production_rate': 1, 'type': 'production', 'cost': 10, 'price': 10, 'vlt': 1, 'id': 3}, facility_id=1, parent_id=1),\n", - " AgentInfo(id=7, agent_type='producer', is_facility=False, sku={'init_stock': 100, 'product_unit_cost': 1, 'production_rate': 1, 'type': 'production', 'cost': 10, 'price': 10, 'vlt': 1, 'id': 3}, facility_id=1, parent_id=6),\n", - " AgentInfo(id=13, agent_type='product', is_facility=False, sku={'init_stock': 100, 'product_unit_cost': 1, 'production_rate': 1, 'type': 'production', 'cost': 10, 'price': 100, 'vlt': 1, 'id': 1}, facility_id=8, parent_id=8),\n", - " AgentInfo(id=14, agent_type='producer', is_facility=False, sku={'init_stock': 100, 'product_unit_cost': 1, 'production_rate': 1, 'type': 'production', 'cost': 10, 'price': 100, 'vlt': 1, 'id': 1}, facility_id=8, parent_id=13),\n", - " AgentInfo(id=15, agent_type='product', is_facility=False, sku={'init_stock': 100, 'production_rate': 1, 'type': 'material', 'cost': 10, 'price': 100, 'vlt': 1, 'id': 3}, facility_id=8, parent_id=8),\n", - " AgentInfo(id=16, agent_type='consumer', is_facility=False, sku={'init_stock': 100, 'production_rate': 1, 'type': 'material', 'cost': 10, 'price': 100, 'vlt': 1, 'id': 3}, facility_id=8, parent_id=15),\n", - " AgentInfo(id=22, agent_type='product', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 1}, facility_id=17, parent_id=17),\n", - " AgentInfo(id=23, agent_type='consumer', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 1}, facility_id=17, parent_id=22),\n", - " AgentInfo(id=24, agent_type='product', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 2}, facility_id=17, parent_id=17),\n", - " AgentInfo(id=25, agent_type='consumer', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 2}, facility_id=17, parent_id=24),\n", - " AgentInfo(id=26, agent_type='product', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 3}, facility_id=17, parent_id=17),\n", - " AgentInfo(id=27, agent_type='consumer', is_facility=False, sku={'init_stock': 1000, 'price': 100, 'vlt': 1, 'id': 3}, facility_id=17, parent_id=26),\n", - " AgentInfo(id=30, agent_type='product', is_facility=False, sku={'price': 300, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 1}, facility_id=28, parent_id=28),\n", - " AgentInfo(id=31, agent_type='consumer', is_facility=False, sku={'price': 300, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 1}, facility_id=28, parent_id=30),\n", - " AgentInfo(id=33, agent_type='product', is_facility=False, sku={'price': 200, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 3}, facility_id=28, parent_id=28),\n", - " AgentInfo(id=34, agent_type='consumer', is_facility=False, sku={'price': 200, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 3}, facility_id=28, parent_id=33),\n", - " AgentInfo(id=36, agent_type='product', is_facility=False, sku={'price': 100, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 2}, facility_id=28, parent_id=28),\n", - " AgentInfo(id=37, agent_type='consumer', is_facility=False, sku={'price': 100, 'cost': 10, 'init_stock': 100, 'sale_gamma': 100, 'backlog_ratio': 0.1, 'vlt': 1, 'id': 2}, facility_id=28, parent_id=36)]\n" - ] - } - ], - "source": [ - "pprint(env.agent_idx_list)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "8e4837de", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'event_payload': {},\n", - " 'node_detail': {'consumer': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'latest_consumptions': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'order_cost': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'order_product_cost': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'order_quantity': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'price': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'product_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_unit_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'purchased': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'received': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'reward_discount': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'total_purchased': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'total_received': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 7},\n", - " 'distribution': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'remaining_order_number': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'remaining_order_quantity': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 3},\n", - " 'facility': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'}},\n", - " 'number': 4},\n", - " 'manufacture': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'manufacturing_number': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_unit_cost': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'product_unit_id': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 2},\n", - " 'product': {'attributes': {'distribution_check_order': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'distribution_delay_order_penalty': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'distribution_transport_cost': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'price': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'product_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_unit_id': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 9},\n", - " 'seller': {'attributes': {'backlog_ratio': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'demand': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'price': {'slots': 1,\n", - " 'type': 'float'},\n", - " 'product_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_unit_id': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'sold': {'slots': 1, 'type': 'uint'},\n", - " 'total_demand': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'total_sold': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 3},\n", - " 'storage': {'attributes': {'capacity': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'product_list': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'product_number': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'remaining_space': {'slots': 1,\n", - " 'type': 'uint'}},\n", - " 'number': 4},\n", - " 'vehicle': {'attributes': {'facility_id': {'slots': 1,\n", - " 'type': 'int'},\n", - " 'id': {'slots': 1, 'type': 'int'},\n", - " 'payload': {'slots': 1,\n", - " 'type': 'uint'},\n", - " 'unit_transport_cost': {'slots': 1,\n", - " 'type': 'float'}},\n", - " 'number': 6}},\n", - " 'node_mapping': {'agent_types': {'ConsumerUnit': 'consumer',\n", - " 'ManufactureUnit': 'producer',\n", - " 'ProductUnit': 'product',\n", - " 'RetailerFacility': 'facility',\n", - " 'StoreProductUnit': 'product',\n", - " 'SupplierFacility': 'facility',\n", - " 'WarehouseFacility': 'facility'},\n", - " 'facilities': {1: {'class': ,\n", - " 'configs': {'agent_type': 'facility',\n", - " 'delay_order_penalty': 10,\n", - " 'order_cost': 0},\n", - " 'downstreams': {3: [8, 17, 28]},\n", - " 'id': 1,\n", - " 'name': 'Supplier_001',\n", - " 'node_index': 0,\n", - " 'skus': {3: {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 10,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}},\n", - " 'units': {'distribution': {'children': [{'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 4,\n", - " 'node_index': 0,\n", - " 'node_name': 'vehicle'},\n", - " {'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 5,\n", - " 'node_index': 1,\n", - " 'node_name': 'vehicle'}],\n", - " 'class': ,\n", - " 'config': {'unit_price': 1},\n", - " 'id': 3,\n", - " 'node_index': 0,\n", - " 'node_name': 'distribution'},\n", - " 'products': {3: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'manufacture': {'class': 'ManufactureUnit',\n", - " 'config': {'agent_type': 'producer'}}},\n", - " 'consumer': None,\n", - " 'id': 6,\n", - " 'manufacture': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'producer'},\n", - " 'id': 7,\n", - " 'node_index': 0,\n", - " 'node_name': 'manufacture',\n", - " 'sku_id': 3},\n", - " 'max_vlt': 1,\n", - " 'node_index': 0,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 3}},\n", - " 'storage': {'children': None,\n", - " 'class': ,\n", - " 'config': {'capacity': 10000,\n", - " 'unit_storage_cost': 1},\n", - " 'id': 2,\n", - " 'node_index': 0,\n", - " 'node_name': 'storage',\n", - " 'product_list': [3]}},\n", - " 'upstreams': {}},\n", - " 8: {'class': ,\n", - " 'configs': {'agent_type': 'facility',\n", - " 'delay_order_penalty': 10,\n", - " 'order_cost': 0},\n", - " 'downstreams': {1: [17, 28]},\n", - " 'id': 8,\n", - " 'name': 'Supplier_002',\n", - " 'node_index': 1,\n", - " 'skus': {1: {'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1},\n", - " 3: {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'production_rate': 1,\n", - " 'type': 'material',\n", - " 'vlt': 1}},\n", - " 'units': {'distribution': {'children': [{'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 11,\n", - " 'node_index': 2,\n", - " 'node_name': 'vehicle'},\n", - " {'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 12,\n", - " 'node_index': 3,\n", - " 'node_name': 'vehicle'}],\n", - " 'class': ,\n", - " 'config': {'unit_price': 1},\n", - " 'id': 10,\n", - " 'node_index': 1,\n", - " 'node_name': 'distribution'},\n", - " 'products': {1: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'manufacture': {'class': 'ManufactureUnit',\n", - " 'config': {'agent_type': 'producer'}}},\n", - " 'consumer': None,\n", - " 'id': 13,\n", - " 'manufacture': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'producer'},\n", - " 'id': 14,\n", - " 'node_index': 1,\n", - " 'node_name': 'manufacture',\n", - " 'sku_id': 1},\n", - " 'max_vlt': 1,\n", - " 'node_index': 1,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 1},\n", - " 3: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'manufacture': {'class': 'ManufactureUnit',\n", - " 'config': {'agent_type': 'producer'}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 16,\n", - " 'node_index': 0,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 3,\n", - " 'sources': [1]},\n", - " 'id': 15,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 2,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 3}},\n", - " 'storage': {'children': None,\n", - " 'class': ,\n", - " 'config': {'capacity': 10000,\n", - " 'unit_storage_cost': 1},\n", - " 'id': 9,\n", - " 'node_index': 1,\n", - " 'node_name': 'storage',\n", - " 'product_list': [1,\n", - " 3]}},\n", - " 'upstreams': {3: [1]}},\n", - " 17: {'class': ,\n", - " 'configs': {'agent_type': 'facility',\n", - " 'delay_order_penalty': 10,\n", - " 'order_cost': 0},\n", - " 'downstreams': {},\n", - " 'id': 17,\n", - " 'name': 'Warehouse_001',\n", - " 'node_index': 2,\n", - " 'skus': {1: {'id': 1,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1},\n", - " 2: {'id': 2,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1},\n", - " 3: {'id': 3,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}},\n", - " 'units': {'distribution': {'children': [{'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 20,\n", - " 'node_index': 4,\n", - " 'node_name': 'vehicle'},\n", - " {'children': None,\n", - " 'class': ,\n", - " 'config': {'patient': 100},\n", - " 'id': 21,\n", - " 'node_index': 5,\n", - " 'node_name': 'vehicle'}],\n", - " 'class': ,\n", - " 'config': {'unit_price': 1},\n", - " 'id': 19,\n", - " 'node_index': 2,\n", - " 'node_name': 'distribution'},\n", - " 'products': {1: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 23,\n", - " 'node_index': 1,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 1,\n", - " 'sources': [8]},\n", - " 'id': 22,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 3,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 1},\n", - " 2: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 25,\n", - " 'node_index': 2,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 2,\n", - " 'sources': []},\n", - " 'id': 24,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 4,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 2},\n", - " 3: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 27,\n", - " 'node_index': 3,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 3,\n", - " 'sources': [1]},\n", - " 'id': 26,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 5,\n", - " 'node_name': 'product',\n", - " 'seller': None,\n", - " 'sku_id': 3}},\n", - " 'storage': {'children': None,\n", - " 'class': ,\n", - " 'config': {'capacity': 30000,\n", - " 'unit_storage_cost': 1},\n", - " 'id': 18,\n", - " 'node_index': 2,\n", - " 'node_name': 'storage',\n", - " 'product_list': [1,\n", - " 2,\n", - " 3]}},\n", - " 'upstreams': {1: [8], 3: [1]}},\n", - " 28: {'class': ,\n", - " 'configs': {'agent_type': 'facility',\n", - " 'order_cost': 0},\n", - " 'downstreams': {},\n", - " 'id': 28,\n", - " 'name': 'Retailer_001',\n", - " 'node_index': 3,\n", - " 'skus': {1: {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 300,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1},\n", - " 2: {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 2,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1},\n", - " 3: {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 200,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}},\n", - " 'units': {'distribution': None,\n", - " 'products': {1: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'seller': {'class': 'SellerUnit',\n", - " 'config': {'sale_hist_len': 4}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 31,\n", - " 'node_index': 4,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 1,\n", - " 'sources': [8]},\n", - " 'id': 30,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 6,\n", - " 'node_name': 'product',\n", - " 'seller': {'children': None,\n", - " 'class': ,\n", - " 'config': {'sale_hist_len': 4},\n", - " 'id': 32,\n", - " 'node_index': 0,\n", - " 'node_name': 'seller',\n", - " 'sku_id': 1},\n", - " 'sku_id': 1},\n", - " 2: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'seller': {'class': 'SellerUnit',\n", - " 'config': {'sale_hist_len': 4}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 37,\n", - " 'node_index': 6,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 2,\n", - " 'sources': []},\n", - " 'id': 36,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 8,\n", - " 'node_name': 'product',\n", - " 'seller': {'children': None,\n", - " 'class': ,\n", - " 'config': {'sale_hist_len': 4},\n", - " 'id': 38,\n", - " 'node_index': 2,\n", - " 'node_name': 'seller',\n", - " 'sku_id': 2},\n", - " 'sku_id': 2},\n", - " 3: {'class': ,\n", - " 'config': {'agent_type': 'product',\n", - " 'consumer': {'class': 'ConsumerUnit',\n", - " 'config': {'agent_type': 'consumer'}},\n", - " 'seller': {'class': 'SellerUnit',\n", - " 'config': {'sale_hist_len': 4}}},\n", - " 'consumer': {'children': None,\n", - " 'class': ,\n", - " 'config': {'agent_type': 'consumer'},\n", - " 'id': 34,\n", - " 'node_index': 5,\n", - " 'node_name': 'consumer',\n", - " 'sku_id': 3,\n", - " 'sources': [1]},\n", - " 'id': 33,\n", - " 'manufacture': None,\n", - " 'max_vlt': 1,\n", - " 'node_index': 7,\n", - " 'node_name': 'product',\n", - " 'seller': {'children': None,\n", - " 'class': ,\n", - " 'config': {'sale_hist_len': 4},\n", - " 'id': 35,\n", - " 'node_index': 1,\n", - " 'node_name': 'seller',\n", - " 'sku_id': 3},\n", - " 'sku_id': 3}},\n", - " 'storage': {'children': None,\n", - " 'class': ,\n", - " 'config': {'capacity': 20000,\n", - " 'unit_storage_cost': 1},\n", - " 'id': 29,\n", - " 'node_index': 3,\n", - " 'node_name': 'storage',\n", - " 'product_list': [1,\n", - " 3,\n", - " 2]}},\n", - " 'upstreams': {1: [8], 3: [1]}}},\n", - " 'max_price': 300,\n", - " 'max_sources_per_facility': 1,\n", - " 'skus': {1: {'bom': {3: 10},\n", - " 'id': 1,\n", - " 'name': 'sku1',\n", - " 'output_units_per_lot': 12},\n", - " 2: {'bom': {},\n", - " 'id': 2,\n", - " 'name': 'sku2',\n", - " 'output_units_per_lot': 1},\n", - " 3: {'bom': {},\n", - " 'id': 3,\n", - " 'name': 'sku3',\n", - " 'output_units_per_lot': 1}},\n", - " 'unit_mapping': {2: ('storage', 0, 1, None),\n", - " 3: ('distribution', 0, 1, None),\n", - " 4: ('vehicle', 0, 1, None),\n", - " 5: ('vehicle', 1, 1, None),\n", - " 6: ('product',\n", - " 0,\n", - " 1,\n", - " {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 10,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}),\n", - " 7: ('manufacture',\n", - " 0,\n", - " 1,\n", - " {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 10,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}),\n", - " 9: ('storage', 1, 8, None),\n", - " 10: ('distribution', 1, 8, None),\n", - " 11: ('vehicle', 2, 8, None),\n", - " 12: ('vehicle', 3, 8, None),\n", - " 13: ('product',\n", - " 1,\n", - " 8,\n", - " {'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}),\n", - " 14: ('manufacture',\n", - " 1,\n", - " 8,\n", - " {'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'product_unit_cost': 1,\n", - " 'production_rate': 1,\n", - " 'type': 'production',\n", - " 'vlt': 1}),\n", - " 15: ('product',\n", - " 2,\n", - " 8,\n", - " {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'production_rate': 1,\n", - " 'type': 'material',\n", - " 'vlt': 1}),\n", - " 16: ('consumer',\n", - " 0,\n", - " 8,\n", - " {'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'production_rate': 1,\n", - " 'type': 'material',\n", - " 'vlt': 1}),\n", - " 18: ('storage', 2, 17, None),\n", - " 19: ('distribution', 2, 17, None),\n", - " 20: ('vehicle', 4, 17, None),\n", - " 21: ('vehicle', 5, 17, None),\n", - " 22: ('product',\n", - " 3,\n", - " 17,\n", - " {'id': 1,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 23: ('consumer',\n", - " 1,\n", - " 17,\n", - " {'id': 1,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 24: ('product',\n", - " 4,\n", - " 17,\n", - " {'id': 2,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 25: ('consumer',\n", - " 2,\n", - " 17,\n", - " {'id': 2,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 26: ('product',\n", - " 5,\n", - " 17,\n", - " {'id': 3,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 27: ('consumer',\n", - " 3,\n", - " 17,\n", - " {'id': 3,\n", - " 'init_stock': 1000,\n", - " 'price': 100,\n", - " 'vlt': 1}),\n", - " 29: ('storage', 3, 28, None),\n", - " 30: ('product',\n", - " 6,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 300,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 31: ('consumer',\n", - " 4,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 300,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 32: ('seller',\n", - " 0,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 1,\n", - " 'init_stock': 100,\n", - " 'price': 300,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 33: ('product',\n", - " 7,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 200,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 34: ('consumer',\n", - " 5,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 3,\n", - " 'init_stock': 100,\n", - " 'price': 200,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 35: ('seller',\n", - " 1,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 3,\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 'init_stock': 100,\n", - " 'price': 200,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 36: ('product',\n", - " 8,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 2,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 37: ('consumer',\n", - " 6,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 2,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1}),\n", - " 38: ('seller',\n", - " 2,\n", - " 28,\n", - " {'backlog_ratio': 0.1,\n", - " 'cost': 10,\n", - " 'id': 2,\n", - " 'init_stock': 100,\n", - " 'price': 100,\n", - " 'sale_gamma': 100,\n", - " 'vlt': 1})}}}\n" - ] - } - ], - "source": [ - "pprint(env.summary)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "504e25a0", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.10" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/tests/data/supply_chain/case_01/config.yml b/tests/data/supply_chain/case_01/config.yml deleted file mode 100644 index 4dee0fab6..000000000 --- a/tests/data/supply_chain/case_01/config.yml +++ /dev/null @@ -1,198 +0,0 @@ - -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 100 - -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - - -facility_definitions: - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 3 - consumer: - class: "ConsumerUnit" - manufacture: - class: "ManufactureUnit" - distribution: - class: "DistributionUnit" - config: - agent_type: 0 -small_storage: &small_storage - config: - capacity: 100 - unit_storage_cost: 1 - -midium_storage: &midium_storage - config: - capacity: 200 - unit_storage_cost: 1 - -huge_storage: &huge_storage - config: - capacity: 300 - unit_storage_cost: 1 - - -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 1 - bom: - sku3: 2 - - - id: 2 - name: "sku2" - output_units_per_lot: 2 - bom: - sku1: 4 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - - id: 4 - name: "sku4" - output_units_per_lot: 2 - bom: - sku2: 1 - - -world: - skus: *sku_definitions - - facilities: - # This supplier will keep generate sku3, and should stop at tick 20 as reach the storage limitation - - name: "Supplier_SKU3" - definition_ref: "SupplierFacility" - skus: - sku3: - init_stock: 80 - product_unit_cost: 1 - type: "production" - cost": 10 - price": 10 - children: - storage: *small_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - - name: "Supplier_SKU1" - definition_ref: "SupplierFacility" - skus: - sku1: - init_stock: 96 - product_unit_cost: 1 - type: "production" - cost: 10 - price: 100 - sku3: - init_stock: 100 - type: "material" - cost: 10 - price: 100 - children: - storage: *midium_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 200 - - name: "Supplier_SKU2" - definition_ref: "SupplierFacility" - skus: - sku2: - init_stock: 50 - product_unit_cost: 1 - type: "production" - cost: 10 - price: 100 - sku1: - init_stock: 50 - type: "material" - cost: 10 - price: 100 - children: - storage: *midium_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 200 - - name: "Supplier_SKU4" - definition_ref: "SupplierFacility" - skus: - sku4: - init_stock: 50 - product_unit_cost: 4 - type: "production" - cost: 10 - price: 100 - sku2: - init_stock: 0 - type: "material" - cost: 10 - price: 100 - children: - storage: *midium_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 200 - topology: - Supplier_SKU1: - sku3: - - "Supplier_SKU3" - Supplier_SKU2: - sku1: - - "Supplier_SKU1" - Supplier_SKU4: - sku2: - - "Supplier_SKU2" - - grid: - size: [20, 20] - - facilities: - Supplier_SKU1: [0, 0] - Supplier_SKU2: [3, 3] - Supplier_SKU3: [6, 6] - Supplier_SKU4: [10, 18] - - blocks: - railroad: - - [10, 10] - - [10, 11] - - [10, 12] - - [11, 12] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 diff --git a/tests/data/supply_chain/case_01/readme.md b/tests/data/supply_chain/case_01/readme.md deleted file mode 100644 index 9239123f7..000000000 --- a/tests/data/supply_chain/case_01/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -This case used to test following units separately. - -. manufacture -. storage -. consumer diff --git a/tests/data/supply_chain/case_02/config.yml b/tests/data/supply_chain/case_02/config.yml deleted file mode 100644 index e2f3ac2cf..000000000 --- a/tests/data/supply_chain/case_02/config.yml +++ /dev/null @@ -1,199 +0,0 @@ - -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 10 - unit_transport_cost: 12 - -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -facility_definitions: - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 3 - consumer: - class: "ConsumerUnit" - manufacture: - class: "ManufactureUnit" - distribution: - class: "DistributionUnit" - config: - agent_type: 0 - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 4 - consumer: - class: "ConsumerUnit" - config: - agent_type: 1 - RetailerFacility: &retailer_facility - class: "RetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "SellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: 2 -small_storage: &small_storage - config: - capacity: 100 - unit_storage_cost: 1 - -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 1 - bom: - sku3: 2 - - - id: 2 - name: "sku2" - output_units_per_lot: 2 - bom: - sku1: 4 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - - id: 4 - name: "sku4" - output_units_per_lot: 2 - bom: - sku2: 1 - -world: - skus: *sku_definitions - facilities: - - name: "Supplier_SKU3" - definition_ref: "SupplierFacility" - skus: - sku3: - init_stock: 80 - product_unit_cost: 1 - type: "production" - cost": 10 - price": 10 - children: - storage: *small_storage - distribution: *normal_distribution - config: - delay_order_penalty: 20 - order_cost: 0 - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - skus: - sku1: - init_stock: 10 - price: 100 - sku2: - init_stock: 10 - price: 100 - sku3: - init_stock: 10 - price: 100 - children: - storage: *small_storage - distribution: *normal_distribution - config: - delay_order_penalty: 20 - order_cost: 200 - - name: "Retailer_001" - definition_ref: "RetailerFacility" - skus: - sku1: - price: 300 - cost: 10 - init_stock: 10 - sale_gamma: 10 - backlog_ratio: 0.1 - sku3: - price: 200 - cost: 10 - init_stock: 10 - sale_gamma: 10 - backlog_ratio: 0.1 - sku2: - price: 100 - cost: 10 - init_stock: 10 - sale_gamma: 10 - backlog_ratio: 0.1 - children: - storage: *small_storage - config: - order_cost: 200 - topology: - Warehouse_001: - sku3: - - "Supplier_SKU3" - Retailer_001: - sku1: - - "Warehouse_001" - sku2: - - "Warehouse_001" - sku3: - - "Warehouse_001" - grid: - size: [ 20, 20 ] - facilities: - Warehouse_001: [ 0, 0 ] - Supplier_SKU3: [ 3, 3 ] - Retailer_001: [8, 8] - blocks: - railroad: - - [ 10, 10 ] - - [ 10, 11 ] - - [ 10, 12 ] - - [ 11, 12 ] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 diff --git a/tests/data/supply_chain/case_02/readme.md b/tests/data/supply_chain/case_02/readme.md deleted file mode 100644 index d3b3a4643..000000000 --- a/tests/data/supply_chain/case_02/readme.md +++ /dev/null @@ -1,4 +0,0 @@ -This case used to test following units: - -. vehicle -. distribution diff --git a/tests/data/supply_chain/case_03/config.yml b/tests/data/supply_chain/case_03/config.yml deleted file mode 100644 index 18f828692..000000000 --- a/tests/data/supply_chain/case_03/config.yml +++ /dev/null @@ -1,211 +0,0 @@ - -# add customized units -core: - units: - modules: - - path: "tests.supply_chain.simple_seller" - definitions: - # NOTE: do not conflict with built-in ones - SimpleSellerUnit: - class: "SimpleSellerUnit" - datamodel: "SellerDataModel" - - -normal_vehicle: &normal_vehicle - class: "VehicleUnit" - config: - patient: 10 - unit_transport_cost: 12 - -normal_distribution: &normal_distribution - class: "DistributionUnit" - children: - vehicles: - - *normal_vehicle - - *normal_vehicle - config: - unit_price: 1 - -facility_definitions: - SupplierFacility: &supplier_facility - class: "SupplierFacility" - children: - storage: - class: "StorageUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 3 - consumer: - class: "ConsumerUnit" - manufacture: - class: "ManufactureUnit" - distribution: - class: "DistributionUnit" - config: - agent_type: 0 - WarehouseFacility: &warehouse_facility - class: "WarehouseFacility" - children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 4 - consumer: - class: "ConsumerUnit" - config: - agent_type: 1 - RetailerFacility: &retailer_facility - class: "RetailerFacility" - children: - storage: - class: "StorageUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "SimpleSellerUnit" - config: - sale_hist_len: 4 - config: - agent_type: 2 -small_storage: &small_storage - config: - capacity: 100 - unit_storage_cost: 1 - -skus: &sku_definitions - - id: 1 - name: "sku1" - output_units_per_lot: 1 - bom: - sku3: 2 - - - id: 2 - name: "sku2" - output_units_per_lot: 2 - bom: - sku1: 4 - - - id: 3 - name: "sku3" - output_units_per_lot: 1 - - - id: 4 - name: "sku4" - output_units_per_lot: 2 - bom: - sku2: 1 - -world: - skus: *sku_definitions - facilities: - - name: "Supplier_SKU3" - definition_ref: "SupplierFacility" - skus: - sku3: - init_stock: 80 - product_unit_cost: 1 - type: "production" - cost": 10 - price": 10 - children: - storage: *small_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 0 - - name: "Warehouse_001" - definition_ref: "WarehouseFacility" - skus: - sku1: - init_stock: 10 - price: 100 - sku2: - init_stock: 10 - price: 100 - sku3: - init_stock: 10 - price: 100 - children: - storage: *small_storage - distribution: *normal_distribution - config: - delay_order_penalty: 10 - order_cost: 200 - - name: "Retailer_001" - definition_ref: "RetailerFacility" - skus: - sku1: - price: 300 - cost: 10 - init_stock: 10 - sale_gamma: 10 - backlog_ratio: 0.1 - sku3: - price: 200 - cost: 10 - init_stock: 10 - sale_gamma: 10 - backlog_ratio: 0.1 - sku2: - price: 100 - cost: 10 - init_stock: 10 - sale_gamma: 10 - backlog_ratio: 0.1 - children: - storage: *small_storage - config: - order_cost: 200 - topology: - Warehouse_001: - sku3: - - "Supplier_SKU3" - Retailer_001: - sku1: - - "Warehouse_001" - sku2: - - "Warehouse_001" - sku3: - - "Warehouse_001" - grid: - size: [ 20, 20 ] - facilities: - Warehouse_001: [ 0, 0 ] - Supplier_SKU3: [ 3, 3 ] - Retailer_001: [8, 8] - blocks: - railroad: - - [ 10, 10 ] - - [ 10, 11 ] - - [ 10, 12 ] - - [ 11, 12 ] - -settings: - global_reward_weight_producer: 0.50 - global_reward_weight_consumer: 0.50 - downsampling_rate: 1 - episod_duration: 21 - initial_balance: 100000 - consumption_hist_len: 4 - sale_hist_len: 4 - pending_order_len: 4 - constraint_state_hist_len: 8 - total_echelons: 3 - replenishment_discount: 0.9 - reward_normalization: 1e7 - constraint_violate_reward: -1e6 - gamma: 0.99 - tail_timesteps: 7 - heading_timesteps: 7 diff --git a/tests/data/supply_chain/case_03/readme.md b/tests/data/supply_chain/case_03/readme.md deleted file mode 100644 index a221fe428..000000000 --- a/tests/data/supply_chain/case_03/readme.md +++ /dev/null @@ -1 +0,0 @@ -This case used to test customized seller unit. \ No newline at end of file diff --git a/tests/supply_chain/simple_seller.py b/tests/supply_chain/simple_seller.py deleted file mode 100644 index 04e3c72e1..000000000 --- a/tests/supply_chain/simple_seller.py +++ /dev/null @@ -1,7 +0,0 @@ - -from maro.simulator.scenarios.supply_chain import SellerUnit - - -class SimpleSellerUnit(SellerUnit): - def market_demand(self, tick: int) -> int: - return tick diff --git a/tests/supply_chain/test_supply_chain.py b/tests/supply_chain/test_supply_chain.py deleted file mode 100644 index 98e6a1415..000000000 --- a/tests/supply_chain/test_supply_chain.py +++ /dev/null @@ -1,1399 +0,0 @@ -import os -import unittest -from typing import Optional - -import numpy as np - -from maro.simulator import Env -from maro.simulator.scenarios.supply_chain import ( - ConsumerAction, ConsumerUnit, DistributionUnit, FacilityBase, ManufactureAction, SellerUnit, StorageUnit, VehicleUnit -) -from maro.simulator.scenarios.supply_chain.business_engine import SupplyChainBusinessEngine -from maro.simulator.scenarios.supply_chain.units.order import Order - - -def build_env(case_name: str, durations: int): - case_folder = os.path.join("tests", "data", "supply_chain", case_name) - - # config_path = os.path.join(case_folder, "config.yml") - - env = Env(scenario="supply_chain", topology=case_folder, durations=durations) - - return env - - -def get_product_dict_from_storage(env: Env, frame_index: int, node_index: int): - product_list = env.snapshot_list["storage"][frame_index:node_index:"product_list"].flatten().astype(np.int) - product_number = env.snapshot_list["storage"][frame_index:node_index:"product_number"].flatten().astype(np.int) - - return {pid: pnum for pid, pnum in zip(product_list, product_number)} - - -SKU1_ID = 1 -SKU2_ID = 2 -SKU3_ID = 3 -SKU4_ID = 4 - - -class MyTestCase(unittest.TestCase): - """ - manufacture unit testing: - - 1. with input sku - . meet the storage limitation - . not meet the storage limitation - . with enough source sku - . without enough source sku - . with product rate - . without product rate - 2. without input sku - . meet the storage limitation - . not meet the storage limitation - . with product rate - . without product rate - - """ - - def test_manufacture_meet_storage_limitation(self): - """Test sku3 manufacturing.""" - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space") - - manufacture_nodes = env.snapshot_list["manufacture"] - manufacture_number = len(manufacture_nodes) - manufacture_features = ( - "id", "facility_id", "manufacturing_number", "product_id", - "product_unit_cost" - ) - - # ############################### TICK: 0 ###################################### - - # tick 0 passed, no product manufacturing. - env.step(None) - - states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, - -1).astype(np.int) - - # try to find which one is sku3 manufacture unit. - sku3_data_model_index: Optional[int] = None - sku3_manufacture_id: Optional[int] = None - sku3_facility_id: Optional[int] = None - for index, state in enumerate(states): - # Id of sku3 is 3. - if state[3] == SKU3_ID: - sku3_data_model_index = index - sku3_manufacture_id = state[0] - sku3_facility_id = state[1] - self.assertTrue(all([ - sku3_data_model_index is not None, - sku3_manufacture_id is not None, - sku3_facility_id is not None - ])) - - # try to find sku3's storage from env.summary - sku3_facility_info = env.summary["node_mapping"]["facilities"][sku3_facility_id] - sku3_storage_index = sku3_facility_info["units"]["storage"]["node_index"] - - storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) - - # there should be 80 units been taken at the beginning according to the config file. - # so remaining space should be 20 - self.assertEqual(20, storage_states[3]) - # capacity is 100 by config - self.assertEqual(100, storage_states[2]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) - - # number should be same as configuration at beginning. - # 80 sku3 - self.assertEqual(80, product_dict[SKU3_ID]) - - # all the id is greater than 0 - self.assertGreater(sku3_manufacture_id, 0) - - # ############################### TICK: 1 ###################################### - - # pass an action to start manufacturing for this tick. - action = ManufactureAction(sku3_manufacture_id, 1) - - env.step([action]) - - states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) - - # Sku3 produce rate is 1 per tick, so manufacturing_number should be 1. - self.assertEqual(1, states[2]) - - storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) - - # now remaining space should be 19 - self.assertEqual(19, storage_states[3]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) - - # sku3 number should be 80 + 1 - self.assertEqual(80 + 1, product_dict[SKU3_ID]) - - # ############################### TICK: 2 ###################################### - - # leave the action as none will cause manufacture unit stop manufacturing. - env.step(None) - - states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) - - # so manufacturing_number should be 0 - self.assertEqual(0, states[2]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) - - # sku3 number should be same as last tick - self.assertEqual(80 + 1, product_dict[SKU3_ID]) - - # let is generate 20, but actually it can only procedure 19 because the storage will reach the limitation - env.step([ManufactureAction(sku3_manufacture_id, 20)]) - - states = manufacture_nodes[env.frame_index:sku3_data_model_index:manufacture_features].flatten().astype(np.int) - - # so manufacture_number should be 19 instead 20 - self.assertEqual(19, states[2]) - - storage_states = storage_nodes[env.frame_index:sku3_storage_index:storage_features].flatten().astype(np.int) - - # now remaining space should be 0 - self.assertEqual(0, storage_states[3]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku3_storage_index) - - # sku3 number should be 100 - self.assertEqual(80 + 1 + 19, product_dict[SKU3_ID]) - - def test_manufacture_meet_source_lack(self): - """Test sku4 manufacturing, this sku supplier does not have enough source material at the begging - , so it cannot produce anything without consumer purchase.""" - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space") - - manufacture_nodes = env.snapshot_list["manufacture"] - manufacture_number = len(manufacture_nodes) - manufacture_features = ( - "id", "facility_id", "manufacturing_number", "product_id", - "product_unit_cost" - ) - - # ############################### TICK: 0 ###################################### - - # tick 0 passed, no product manufacturing. - env.step(None) - - states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, - -1).astype(np.int) - - # try to find which one is sku3 manufacture unit. - sku4_data_model_index: Optional[int] = None - sku4_manufacture_id: Optional[int] = None - sku4_facility_id: Optional[int] = None - for index, state in enumerate(states): - # Id of sku4 is 4. - if state[3] == SKU4_ID: - sku4_data_model_index = index - sku4_manufacture_id = state[0] - sku4_facility_id = state[1] - self.assertTrue(all([ - sku4_data_model_index is not None, - sku4_manufacture_id is not None, - sku4_facility_id is not None - ])) - - # try to find sku4's storage from env.summary - sku4_facility_info = env.summary["node_mapping"]["facilities"][sku4_facility_id] - sku4_storage_index = sku4_facility_info["units"]["storage"]["node_index"] - - # the storage should be same as initialized (50 + 0). - storage_states = storage_nodes[env.frame_index:sku4_storage_index:storage_features].flatten().astype(np.int) - - # capacity is same as configured. - self.assertEqual(200, storage_states[2]) - - # remaining space should be capacity - (50+0) - self.assertEqual(200 - (50 + 0), storage_states[3]) - - # no manufacture number as we have not pass any action - manufature_states = manufacture_nodes[ - env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype(np.int) - - # manufacturing_number should be 0 - self.assertEqual(0, manufature_states[2]) - - # output product id should be same as configured. - self.assertEqual(4, manufature_states[3]) - - # product unit cost should be same as configured. - self.assertEqual(4, manufature_states[4]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) - - # 50 sku4 at beginning - self.assertEqual(50, product_dict[SKU4_ID]) - - # 0 sku2 - self.assertEqual(0, product_dict[SKU2_ID]) - - # ############################### TICK: 1 - end ###################################### - - is_done = False - - while not is_done: - # push to the end, the storage should not changed, no matter what production rate we give it. - _, _, is_done = env.step([ManufactureAction(sku4_manufacture_id, 10)]) - - manufature_states = manufacture_nodes[ - env.frame_index:sku4_data_model_index:manufacture_features].flatten().astype( - np.int) - - # manufacturing_number should be 0 - self.assertEqual(0, manufature_states[2]) - - # output product id should be same as configured. - self.assertEqual(SKU4_ID, manufature_states[3]) - - # product unit cost should be same as configured. - self.assertEqual(4, manufature_states[4]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku4_storage_index) - - # 50 sku4 at beginning - self.assertEqual(50, product_dict[SKU4_ID]) - - # 0 sku2 - self.assertEqual(0, product_dict[SKU2_ID]) - - def test_manufacture_meet_avg_storage_limitation(self): - """Test on sku1, it is configured with nearly full initial states.""" - - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "facility_id", "capacity", "remaining_space") - - manufacture_nodes = env.snapshot_list["manufacture"] - manufacture_number = len(manufacture_nodes) - manufacture_features = ( - "id", "facility_id", "manufacturing_number", "product_id", - "product_unit_cost" - ) - - # ############################### TICK: 0 ###################################### - - # tick 0 passed, no product manufacturing, verified in above case, pass checking it here. - env.step(None) - - states = manufacture_nodes[env.frame_index::manufacture_features].flatten().reshape(manufacture_number, - -1).astype(np.int) - # try to find which one is sku3 manufacture unit. - sku1_data_model_index: Optional[int] = None - sku1_manufacture_id: Optional[int] = None - sku1_facility_id: Optional[int] = None - for index, state in enumerate(states): - # Id of sku1 is 1. - if state[3] == SKU1_ID: - sku1_data_model_index = index - sku1_manufacture_id = state[0] - sku1_facility_id = state[1] - self.assertTrue(all([ - sku1_data_model_index is not None, - sku1_manufacture_id is not None, - sku1_facility_id is not None - ])) - - sku1_facility_info = env.summary["node_mapping"]["facilities"][sku1_facility_id] - sku1_storage_index = sku1_facility_info["units"]["storage"]["node_index"] - - # ############################### TICK: 1 ###################################### - - # ask sku1 manufacture start manufacturing, rate is 10. - env.step([ManufactureAction(sku1_manufacture_id, 10)]) - - storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) - manufacture_states = manufacture_nodes[ - env.frame_index:sku1_data_model_index:manufacture_features].flatten().astype(np.int) - - # we can produce 4 sku1, as it will meet storage avg limitation per sku - self.assertEqual(4, manufacture_states[2]) - - # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) - self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku1_storage_index) - - # number of sku1 should 100, just reach the avg storage capacity limitation - self.assertEqual(100, product_dict[SKU1_ID]) - - # 4 sku1 cost 4*2 source material (sku3) - self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) - - # ############################### TICK: 1 ###################################### - - # then fix the product rate to 20 every tick, but the manufacture will do nothing, as we have to enough space - - is_done = False - - while not is_done: - _, _, is_done = env.step([ManufactureAction(sku1_storage_index, 20)]) - - storage_states = storage_nodes[env.frame_index:sku1_storage_index:storage_features].flatten().astype(np.int) - manufacture_states = manufacture_nodes[ - env.frame_index:sku1_data_model_index:manufacture_features].flatten().astype(np.int) - - # but manufacture number is 0 - self.assertEqual(0, manufacture_states[2]) - - # so storage remaining space should be 200 - ((96 + 4) + (100 - 4*2)) - self.assertEqual(200 - ((96 + 4) + (100 - 4 * 2)), storage_states[3]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, sku1_storage_index) - - # number of sku1 should 100, just reach the avg storage capacity limitation - self.assertEqual(100, product_dict[SKU1_ID]) - - # 4 sku1 cost 4*2 source material (sku3) - self.assertEqual(100 - 4 * 2, product_dict[SKU3_ID]) - - """ - Storage test: - - . take available - . enough - . not enough - . try add products - . meet whole storage capacity limitation - . fail if all - . not fail if all - . enough space - . try take products - . have enough - . not enough - . get product number - - """ - - def test_storage_take_available(self): - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - env.step(None) - - storage_nodes = env.snapshot_list["storage"] - - # find first storage unit id - storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] - - # get the unit reference from env internal - storage_unit: StorageUnit = env.business_engine.world.get_entity(storage_unit_id) - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # call take_available for each product in storage. - products_taken = {} - for product_id, product_number in init_product_dict.items(): - num = np.random.randint(0, product_number) - actual_num = storage_unit.take_available(product_id, num) - - # we should get the number we want. - self.assertEqual(num, actual_num) - - products_taken[product_id] = num - - # check if internal state correct - for product_id, num in products_taken.items(): - remaining_num = storage_unit.product_level[product_id] - - self.assertEqual(init_product_dict[product_id] - num, remaining_num) - - # call env.step will cause states write into snapshot - env.step(None) - - product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - for product_id, num in products_taken.items(): - remaining_num = product_dict[product_id] - - self.assertEqual(init_product_dict[product_id] - num, remaining_num) - - # then take more than exist number for 1st product(sku) - lot_taken_product_id, lot_taken_product_number = product_dict.popitem() - - lot_taken_product_number += 100 - - actual_num = storage_unit.take_available(lot_taken_product_id, lot_taken_product_number) - - # we should get all available - self.assertEqual(actual_num, lot_taken_product_number - 100) - - # take snapshot - env.step(None) - - product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # the product number should be 0, as we took all available - self.assertEqual(0, product_dict[lot_taken_product_id]) - - def test_storage_try_add_products(self): - """ - NOTE: - try_add_products method do not check avg storage capacity checking, so we will ignore it here. - - """ - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - env.step(None) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "capacity", "remaining_space") - - # find first storage unit id - storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] - - # get the unit reference from env internal - storage_unit: StorageUnit = env.business_engine.world.get_entity(storage_unit_id) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - capacity = storage_states[1] - init_remaining_space = storage_states[2] - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # try put products out of capacity with all_or_nothing == True - products_to_put = {} - - avg_max_product_number = init_remaining_space // len(init_product_dict) - - for product_id in init_product_dict.keys(): - products_to_put[product_id] = avg_max_product_number + 1 - - result = storage_unit.try_add_products(products_to_put, all_or_nothing=True) - - # the method will return an empty dictionary if fail to add - self.assertEqual(0, len(result)) - - # so remaining space should not change - self.assertEqual(init_remaining_space, storage_unit.remaining_space) - - # each product number should be same as before - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_level[product_id]) - - # if we set all_or_nothing=False, then part of the product will be added to storage, and cause remaining - # space being 0 - storage_unit.try_add_products(products_to_put, all_or_nothing=False) - - self.assertEqual(0, storage_unit.remaining_space) - - # take snapshot - env.step(None) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - # remaining space in snapshot should be 0 - self.assertEqual(0, storage_states[2]) - - product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # total product number should be same as capacity - self.assertEqual(capacity, sum(product_dict.values())) - - # ################################################### - # ################################################### - # reset the env for next case - env.reset() - - # check the state after reset - self.assertEqual(capacity, storage_unit.capacity) - self.assertEqual(init_remaining_space, storage_unit.remaining_space) - - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_level[product_id]) - - def test_storage_try_take_products(self): - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - env.step(None) - - storage_nodes = env.snapshot_list["storage"] - storage_features = ("id", "capacity", "remaining_space") - - # find first storage unit id - storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] - - # get the unit reference from env internal - storage_unit: StorageUnit = env.business_engine.world.get_entity(storage_unit_id) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - capacity = storage_states[1] - init_remaining_space = storage_states[2] - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - product_to_take = {} - - for product_id, product_number in init_product_dict.items(): - product_to_take[product_id] = product_number + 1 - - # which this setting, it will return false, as no enough product for ous - self.assertFalse(storage_unit.try_take_products(product_to_take)) - - # so remaining space and product number should same as before - self.assertEqual(init_remaining_space, storage_unit.remaining_space) - - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, storage_unit.product_level[product_id]) - - # try to get all products - for product_id, product_number in product_to_take.items(): - product_to_take[product_id] = product_number - 1 - - self.assertTrue(storage_unit.try_take_products(product_to_take)) - - # now the remaining space should be same as capacity as we take all - self.assertEqual(capacity, storage_unit.remaining_space) - - # take snapshot - env.step(None) - - storage_states = storage_nodes[env.frame_index:0:storage_features].flatten().astype(np.int) - - # remaining space should be same as capacity in snapshot - self.assertEqual(storage_states[1], storage_states[2]) - - def test_storage_get_product_number(self): - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - env.step(None) - - storage_nodes = env.snapshot_list["storage"] - - # find first storage unit id - storage_unit_id = storage_nodes[env.frame_index:0:"id"].flatten().astype(np.int)[0] - - # get the unit reference from env internal - storage_unit: StorageUnit = env.business_engine.world.get_entity(storage_unit_id) - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # number in object should be same with states - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_level[product_id]) - - # should not change even after reset - env.reset() - env.step(None) - - init_product_dict = get_product_dict_from_storage(env, env.frame_index, 0) - - # number in object should be same with states - for product_id, product_number in init_product_dict.items(): - self.assertEqual(product_number, - storage_unit.product_level[product_id]) - - """ - - Consumer test: - - . initial state - . state after reset - . set_action directly from code - . set_action by env.step - . call on_order_reception directly to simulation order arrived - . call update_open_orders directly - - """ - - def test_consumer_init_state(self): - """ - NOTE: we will use consumer on Supplier_SKU1, as it contains a source for sku3 (Supplier_SKU3) - """ - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # print(env.summary) - # we can get the consumer from env.summary - - # NOTE: though we are test with sku1, but the consumer is for sku3, as it is the source material from source - sku3_consumer_unit: Optional[ConsumerUnit] = None - sku3_product_unit_id: Optional[int] = None - sku3_consumer_unit_id: Optional[int] = None - - for facility_id, facility_detail in env.summary["node_mapping"]["facilities"].items(): - if facility_detail["name"] == "Supplier_SKU1": - # try to find sku3 consumer - sku3_consumer_unit_id = facility_detail["units"]["products"][SKU3_ID]["consumer"]["id"] - - sku3_consumer_unit = env.business_engine.world.get_entity(sku3_consumer_unit_id) - sku3_product_unit_id = facility_detail["units"]["products"][SKU3_ID]["id"] - - sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] - - # check initial state - self.assertEqual(0, sku3_consumer_unit.received) - self.assertEqual(0, sku3_consumer_unit.purchased) - self.assertEqual(0, sku3_consumer_unit.order_product_cost) - self.assertEqual(SKU3_ID, sku3_consumer_unit.product_id) - - # check data model state - # order cost from configuration - self.assertEqual(200, sku3_consumer_unit.data_model.order_cost) - self.assertEqual(0, sku3_consumer_unit.data_model.total_purchased) - self.assertEqual(0, sku3_consumer_unit.data_model.total_received) - - # NOTE: 0 is an invalid(initial) id - self.assertEqual(SKU3_ID, sku3_consumer_unit.data_model.product_id) - self.assertEqual(sku3_consumer_unit_id, sku3_consumer_unit.data_model.id) - self.assertEqual(sku3_product_unit_id, sku3_consumer_unit.data_model.product_unit_id) - self.assertEqual(0, sku3_consumer_unit.data_model.order_quantity) - self.assertEqual(0, sku3_consumer_unit.data_model.purchased) - self.assertEqual(0, sku3_consumer_unit.data_model.received) - self.assertEqual(0, sku3_consumer_unit.data_model.order_product_cost) - - # check sources - for source_facility_id in sku3_consumer_unit.sources: - source_facility: FacilityBase = env.business_engine.world.get_facility_by_id(source_facility_id) - - # check if source facility contains the sku3 config - self.assertTrue(SKU3_ID in source_facility.skus) - - env.step(None) - - # check state - features = ( - "id", - "facility_id", - "product_id", - "order_cost", - "total_purchased", - "total_received", - "order_quantity", - "purchased", - "received", - "order_product_cost" - ) - - consumer_nodes = env.snapshot_list["consumer"] - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # Nothing happened at tick 0, so most states will be 0 - self.assertTrue((states[4:] == 0).all()) - - self.assertEqual(sku3_consumer_unit_id, states[0]) - self.assertEqual(SKU3_ID, states[2]) - - env.reset() - env.step(None) - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # Nothing happened at tick 0, so most states will be 0 - self.assertTrue((states[4:] == 0).all()) - - self.assertEqual(sku3_consumer_unit_id, states[0]) - self.assertEqual(SKU3_ID, states[2]) - - def test_consumer_action(self): - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - sku3_consumer_unit_id: Optional[int] = None - sku3_consumer_unit: Optional[ConsumerUnit] = None - sku3_supplier_facility_id: Optional[int] = None - - for facility_id, facility_detail in env.summary["node_mapping"]["facilities"].items(): - if facility_detail["name"] == "Supplier_SKU1": - sku3_consumer_unit_id = facility_detail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env.business_engine.world.get_entity(sku3_consumer_unit_id) - if facility_detail["name"] == "Supplier_SKU3": - sku3_supplier_facility_id = facility_detail["id"] - self.assertTrue(all([ - sku3_consumer_unit_id is not None, - sku3_consumer_unit is not None, - sku3_supplier_facility_id is not None - ])) - - sku3_consumer_data_model_index = env.summary["node_mapping"]["unit_mapping"][sku3_consumer_unit_id][1] - - # zero quantity will be ignore - action_with_zero = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 0, 1, 0) - - action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1, 0) - - sku3_consumer_unit.set_action(action_with_zero) - - env.step(None) - - features = ( - "id", - "facility_id", - "product_id", - "order_cost", - "total_purchased", - "total_received", - "product_id", - "order_quantity", - "purchased", - "received", - "order_product_cost" - ) - - consumer_nodes = env.snapshot_list["consumer"] - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # Nothing happened at tick 0, at the action will be recorded - self.assertEqual(action_with_zero.product_id, states[6]) - self.assertEqual(action_with_zero.quantity, states[8]) - - self.assertEqual(sku3_consumer_unit_id, states[0]) - self.assertEqual(SKU3_ID, states[2]) - - # NOTE: we cannot set_action directly here, as post_step will clear the action before starting next tick - env.step([action]) - - self.assertEqual(action.quantity, sku3_consumer_unit.purchased) - self.assertEqual(0, sku3_consumer_unit.received) - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # action field should be recorded - self.assertEqual(action.product_id, states[6]) - self.assertEqual(action.quantity, states[8]) - - # total purchased should be same as purchased at this tick. - self.assertEqual(action.quantity, states[4]) - - # no received now - self.assertEqual(0, states[5]) - - # purchased same as quantity - self.assertEqual(action.quantity, states[8]) - - # no receives - self.assertEqual(0, states[9]) - - # same action for next step, so total_XXX will be changed to double - env.step([action]) - - states = consumer_nodes[env.frame_index:sku3_consumer_data_model_index:features].flatten().astype(np.int) - - # action field should be recorded - self.assertEqual(action.product_id, states[6]) - self.assertEqual(action.quantity, states[8]) - - # total purchased should be same as purchased at this tick. - self.assertEqual(action.quantity * 2, states[4]) - - # no received now - self.assertEqual(0, states[5]) - - # purchased same as quantity - self.assertEqual(action.quantity, states[8]) - - # no receives - self.assertEqual(0, states[9]) - - def test_consumer_on_order_reception(self): - env = build_env("case_01", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - sku3_consumer_unit_id: Optional[int] = None - sku3_consumer_unit: Optional[ConsumerUnit] = None - sku3_supplier_facility_id: Optional[int] = None - - for facility_id, facility_detail in env.summary["node_mapping"]["facilities"].items(): - if facility_detail["name"] == "Supplier_SKU1": - sku3_consumer_unit_id = facility_detail["units"]["products"][SKU3_ID]["consumer"]["id"] - sku3_consumer_unit = env.business_engine.world.get_entity(sku3_consumer_unit_id) - if facility_detail["name"] == "Supplier_SKU3": - sku3_supplier_facility_id = facility_detail["id"] - self.assertTrue(all([ - sku3_consumer_unit_id is not None, - sku3_consumer_unit is not None, - sku3_supplier_facility_id is not None - ])) - - action = ConsumerAction(sku3_consumer_unit_id, SKU3_ID, sku3_supplier_facility_id, 10, 1, 0) - - # 1st step must none action - env.step(None) - - env.step([action]) - - # simulate purchased product is arrived by vehicle unit - sku3_consumer_unit.on_order_reception(sku3_supplier_facility_id, SKU3_ID, 10, 10) - - # now all order is done - self.assertEqual(0, sku3_consumer_unit.open_orders[sku3_supplier_facility_id][SKU3_ID]) - self.assertEqual(10, sku3_consumer_unit.received) - - env.step(None) - - # NOTE: we cannot test the received state by calling on_order_reception directly, - # as it will be cleared by env.step, do it on vehicle unit test. - - """ - Vehicle unit test: - - . initial state - . if vehicle arrive at destination within special vlt - . schedule job - . try_load until patient <= 0 to cancel the schedule - . try_load until patient > 0 to load order - . try_unload - . target storage cannot take all - . target storage can take all - """ - - def test_vehicle_unit_state(self): - env = build_env("case_02", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # try to find first vehicle unit we meet - vehicle_unit: Optional[VehicleUnit] = None - - for unit_id, info in env.summary["node_mapping"]["unit_mapping"].items(): - if info[0] == "vehicle": - vehicle_unit = env.business_engine.world.get_entity(unit_id) - - break - self.assertTrue(vehicle_unit is not None) - - # check initial state according to configuration file - self.assertEqual(10, vehicle_unit.max_patient) - - self.assertEqual(0, vehicle_unit.requested_quantity) - # not destination at first - self.assertIsNone(vehicle_unit.destination) - # no path - self.assertIsNone(vehicle_unit.path) - # no product - self.assertEqual(0, vehicle_unit.product_id) - # no steps - self.assertEqual(0, vehicle_unit.steps) - # - self.assertEqual(0, vehicle_unit.payload) - # - self.assertIsNone(vehicle_unit.product) - # - self.assertEqual(0, vehicle_unit.location) - # - self.assertEqual(0, vehicle_unit.velocity) - - # state in frame - self.assertEqual(0, vehicle_unit.data_model.payload) - self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - - # reset to check again - env.step(None) - env.reset() - - # check initial state according to configuration file - self.assertEqual(10, vehicle_unit.max_patient) - - # not destination at first - self.assertIsNone(vehicle_unit.destination) - # no path - self.assertIsNone(vehicle_unit.path) - # no product - self.assertEqual(0, vehicle_unit.product_id) - # no steps - self.assertEqual(0, vehicle_unit.steps) - # - self.assertEqual(0, vehicle_unit.payload) - # - self.assertIsNone(vehicle_unit.product) - # - self.assertEqual(0, vehicle_unit.location) - # - self.assertEqual(0, vehicle_unit.velocity) - # - self.assertEqual(0, vehicle_unit.requested_quantity) - - # state in frame - self.assertEqual(0, vehicle_unit.data_model.payload) - self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - - def test_vehicle_unit_schedule(self): - env = build_env("case_02", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # try to find first vehicle unit of Supplier - vehicle_unit: Optional[VehicleUnit] = None - dest_facility: Optional[FacilityBase] = None - - for _, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - for v in info["units"]["distribution"]["children"]: - vehicle_unit = env.business_engine.world.get_entity(v["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) - self.assertTrue(all([vehicle_unit is not None, dest_facility is not None])) - - # make sure the upstream in the only one supplier in config - self.assertEqual(1, len(dest_facility.upstreams)) - self.assertEqual(1, len(dest_facility.upstreams[SKU3_ID])) - - # schedule job vehicle unit manually, from supplier to warehouse - vehicle_unit.schedule(dest_facility, SKU3_ID, 20, 2) - - # step to take snapshot - env.step(None) - - vehicle_nodes = env.snapshot_list["vehicle"] - - # check internal states - self.assertEqual(dest_facility, vehicle_unit.destination) - self.assertEqual(SKU3_ID, vehicle_unit.product_id) - self.assertEqual(20, vehicle_unit.requested_quantity) - self.assertEqual(2, vehicle_unit.velocity) - # 6/2 - self.assertEqual(3, vehicle_unit.steps) - - features = ( - "id", - "facility_id", - "payload", - "unit_transport_cost" - ) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # source id - self.assertEqual(vehicle_unit.facility.id, states[1]) - # payload should be 20, as we already env.step - self.assertEqual(20, states[2]) - - # push the vehicle on the way - env.step(None) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # payload - self.assertEqual(20, states[2]) - - env.step(None) - env.step(None) - - # next step vehicle will try to unload the products - env.step(None) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # the product is unloaded, vehicle states will be reset to initial - # not destination at first - self.assertIsNone(vehicle_unit.destination) - self.assertIsNone(vehicle_unit.path) - self.assertEqual(0, vehicle_unit.product_id) - self.assertEqual(0, vehicle_unit.steps) - self.assertEqual(0, vehicle_unit.payload) - self.assertIsNone(vehicle_unit.product) - self.assertEqual(0, vehicle_unit.location) - self.assertEqual(0, vehicle_unit.velocity) - self.assertEqual(0, vehicle_unit.requested_quantity) - - # check states - self.assertEqual(0, states[2]) - self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - - def test_vehicle_unit_no_patient(self): - """ - NOTE: with patient is tried in above case after schedule the job - """ - env = build_env("case_02", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # try to find first vehicle unit of Supplier - vehicle_unit: Optional[VehicleUnit] = None - dest_facility: Optional[FacilityBase] = None - - for _, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - for v in info["units"]["distribution"]["children"]: - vehicle_unit = env.business_engine.world.get_entity(v["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) - self.assertTrue(all([vehicle_unit is not None, dest_facility is not None])) - - # there is 80 sku3 in supplier, lets schedule a job for 100, to make sure it will fail to try load - vehicle_unit.schedule(dest_facility, SKU3_ID, 100, 3) - - # push env to next step - env.step(None) - - self.assertEqual(100, vehicle_unit.requested_quantity) - - # the patient will -1 as no enough product so load - self.assertEqual(10 - 1, vehicle_unit.patient) - - # no payload - self.assertEqual(0, vehicle_unit.payload) - self.assertEqual(0, vehicle_unit.data_model.payload) - - # step 9 ticks, patient will be 0 - for i in range(10 - 1): - env.step(None) - - self.assertEqual(10 - 1 - (i + 1), vehicle_unit.patient) - - vehicle_nodes = env.snapshot_list["vehicle"] - features = ( - "id", - "facility_id", - "payload", - "unit_transport_cost" - ) - - states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) - - # no payload from start to now - self.assertListEqual([0] * 10, list(states)) - - # push env to next step, vehicle will be reset to initial state - env.step(None) - - states = vehicle_nodes[env.frame_index:vehicle_unit.data_model_index:features].flatten().astype(np.int) - - # the product is unloaded, vehicle states will be reset to initial - # not destination at first - self.assertIsNone(vehicle_unit.destination) - self.assertIsNone(vehicle_unit.path) - self.assertEqual(0, vehicle_unit.product_id) - self.assertEqual(0, vehicle_unit.steps) - self.assertEqual(0, vehicle_unit.payload) - self.assertIsNone(vehicle_unit.product) - self.assertEqual(0, vehicle_unit.location) - self.assertEqual(0, vehicle_unit.velocity) - self.assertEqual(0, vehicle_unit.requested_quantity) - - # check states - - self.assertEqual(0, states[2]) - self.assertEqual(12, vehicle_unit.data_model.unit_transport_cost) - - def test_vehicle_unit_cannot_unload_at_destination(self): - """ - NOTE: If vehicle cannot unload at destination, it will keep waiting, until success to unload. - - """ - env = build_env("case_02", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # try to find first vehicle unit of Supplier - vehicle_unit: Optional[VehicleUnit] = None - dest_facility: Optional[FacilityBase] = None - - for _, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - for v in info["units"]["distribution"]["children"]: - vehicle_unit = env.business_engine.world.get_entity(v["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) - self.assertTrue(all([vehicle_unit is not None, dest_facility is not None])) - - # move all 80 sku3 to destination, will cause vehicle keep waiting there - vehicle_unit.schedule(dest_facility, SKU3_ID, 80, 2) - - # step to the end. - is_done = False - - while not is_done: - _, _, is_done = env.step(None) - - vehicle_nodes = env.snapshot_list["vehicle"] - - # payload should be 80 for first 4 ticks, as it is on the way - # then it will unload 100 - 10 - 10 - 10 = 70 products, as this is the remaining space of destination storage - # so then it will keep waiting to unload remaining 10 - payload_states = vehicle_nodes[:vehicle_unit.data_model_index:"payload"].flatten().astype(np.int) - self.assertListEqual([80] * 4 + [10] * 96, list(payload_states)) - - """ - Distribution unit test: - - . initial state - . place order - . dispatch orders without available vehicle - . dispatch order with vehicle - """ - - def test_distribution_unit_initial_state(self): - env = build_env("case_02", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # try to find first vehicle unit of Supplier - dist_unit: Optional[DistributionUnit] = None - dest_facility: Optional[FacilityBase] = None - - for _, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - dist_unit = env.business_engine.world.get_entity(info["units"]["distribution"]["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) - self.assertTrue(all([dist_unit is not None, dest_facility is not None])) - - self.assertEqual(0, len(dist_unit.order_queue)) - - # reset - env.reset() - - self.assertEqual(0, len(dist_unit.order_queue)) - - def test_distribution_unit_dispatch_order(self): - env = build_env("case_02", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # try to find first vehicle unit of Supplier - dist_unit: Optional[DistributionUnit] = None - dest_facility: Optional[FacilityBase] = None - - for _, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Supplier_SKU3": - dist_unit = env.business_engine.world.get_entity(info["units"]["distribution"]["id"]) - - if info["name"] == "Warehouse_001": - dest_facility = env.business_engine.world.get_facility_by_id(info["id"]) - self.assertTrue(all([dist_unit is not None, dest_facility is not None])) - - first_vehicle: VehicleUnit = dist_unit.vehicles[0] - - order = Order(dest_facility, SKU3_ID, 10, 2) - - dist_unit.place_order(order) - - # check if order is saved - self.assertEqual(1, len(dist_unit.order_queue)) - - # check get pending order correct - pending_order = dist_unit.get_pending_order() - - self.assertDictEqual({3: 10}, pending_order) - - # same as vehicle schedule case, distribution will try to schedule this order to vehicles from beginning to end - # so it will dispatch this order to first vehicle - env.step(None) - - self.assertEqual(dest_facility, first_vehicle.destination) - self.assertEqual(10, first_vehicle.requested_quantity) - self.assertEqual(2, first_vehicle.velocity) - self.assertEqual(SKU3_ID, first_vehicle.product_id) - - # since we already test vehicle unit, do not check the it again here - - # add another order to check pending order - dist_unit.place_order(order) - - pending_order = dist_unit.get_pending_order() - - self.assertDictEqual({3: 10}, pending_order) - - # another order, will cause the pending order increase - dist_unit.place_order(order) - - pending_order = dist_unit.get_pending_order() - - # 2 pending orders - self.assertDictEqual({3: 20}, pending_order) - - # now we have only one available vehicle, 2 pending order - # next step will cause delay_order_penalty - env.step(None) - - second_vehicle = dist_unit.vehicles[1] - - self.assertEqual(dest_facility, second_vehicle.destination) - self.assertEqual(10, second_vehicle.requested_quantity) - self.assertEqual(2, second_vehicle.velocity) - self.assertEqual(SKU3_ID, second_vehicle.product_id) - - """ - Seller unit test: - . initial state - . with a customized seller unit - . with built in one - """ - - def test_seller_unit_initial_states(self): - env = build_env("case_02", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # find seller for sku3 from retailer facility - sell_unit: Optional[SellerUnit] = None - - for _, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Retailer_001": - for pid, pdetail in info["units"]["products"].items(): - if pdetail["sku_id"] == SKU3_ID: - sell_unit = env.business_engine.world.get_entity(pdetail["seller"]["id"]) - self.assertTrue(sell_unit is not None) - - # from configuration - self.assertEqual(10, sell_unit.gamma) - - self.assertEqual(0, sell_unit.sold) - self.assertEqual(0, sell_unit.demand) - self.assertEqual(0, sell_unit.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - # - self.assertEqual(0, sell_unit.data_model.sold) - self.assertEqual(0, sell_unit.data_model.demand) - self.assertEqual(0, sell_unit.data_model.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - env.reset() - - # from configuration - self.assertEqual(10, sell_unit.gamma) - self.assertEqual(0, sell_unit.sold) - self.assertEqual(0, sell_unit.demand) - self.assertEqual(0, sell_unit.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - # - self.assertEqual(0, sell_unit.data_model.sold) - self.assertEqual(0, sell_unit.data_model.demand) - self.assertEqual(0, sell_unit.data_model.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - def test_seller_unit_demand_states(self): - env = build_env("case_02", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # find seller for sku3 from retailer facility - sell_unit: Optional[SellerUnit] = None - - for _, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Retailer_001": - for pid, pdetail in info["units"]["products"].items(): - if pdetail["sku_id"] == SKU3_ID: - sell_unit = env.business_engine.world.get_entity(pdetail["seller"]["id"]) - self.assertTrue(sell_unit is not None) - - sku3_init_number = sell_unit.facility.skus[SKU3_ID].init_stock - - env.step(None) - - # seller unit will try to count down the product number base on demand - # default seller use gamma distribution on each tick - demand = sell_unit.demand - - # demand should be same with original - self.assertEqual(demand, sell_unit.data_model.demand) - - actual_sold = min(demand, sku3_init_number) - # sold may be not same as demand, depend on remaining number in storage - self.assertEqual(actual_sold, sell_unit.sold) - self.assertEqual(actual_sold, sell_unit.data_model.sold) - self.assertEqual(actual_sold, sell_unit.total_sold) - self.assertEqual(actual_sold, sell_unit.data_model.total_sold) - - states = env.snapshot_list["seller"][ - env.frame_index:sell_unit.data_model_index:("sold", "demand", "total_sold")].flatten().astype(np.int) - - self.assertEqual(actual_sold, states[0]) - self.assertEqual(demand, states[1]) - self.assertEqual(actual_sold, states[2]) - - # move to next step to check if state is correct - env.step(None) - - demand = sell_unit.demand - - # demand should be same with original - self.assertEqual(demand, sell_unit.data_model.demand) - - actual_sold_2 = min(demand, sku3_init_number - actual_sold) - - # sold may be not same as demand, depend on remaining number in storage - self.assertEqual(actual_sold_2, sell_unit.sold) - self.assertEqual(actual_sold_2, sell_unit.data_model.sold) - self.assertEqual(actual_sold + actual_sold_2, sell_unit.total_sold) - self.assertEqual(actual_sold + actual_sold_2, sell_unit.data_model.total_sold) - - states = env.snapshot_list["seller"][ - env.frame_index:sell_unit.data_model_index:("sold", "demand", "total_sold")].flatten().astype(np.int) - - self.assertEqual(actual_sold_2, states[0]) - self.assertEqual(demand, states[1]) - self.assertEqual(actual_sold + actual_sold_2, states[2]) - - def test_seller_unit_customized(self): - env = build_env("case_03", 100) - assert isinstance(env.business_engine, SupplyChainBusinessEngine) - - # find seller for sku3 from retailer facility - sell_unit: Optional[SellerUnit] = None - - for _, info in env.summary["node_mapping"]["facilities"].items(): - if info["name"] == "Retailer_001": - for pid, pdetail in info["units"]["products"].items(): - if pdetail["sku_id"] == SKU3_ID: - sell_unit = env.business_engine.world.get_entity(pdetail["seller"]["id"]) - self.assertTrue(sell_unit is not None) - - # NOTE: - # this simple seller unit return demands that same as current tick - env.step(None) - - # so tick 0 will have demand == 0 - # from configuration - self.assertEqual(0, sell_unit.sold) - self.assertEqual(0, sell_unit.demand) - self.assertEqual(0, sell_unit.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - # - self.assertEqual(0, sell_unit.data_model.sold) - self.assertEqual(0, sell_unit.data_model.demand) - self.assertEqual(0, sell_unit.data_model.total_sold) - self.assertEqual(SKU3_ID, sell_unit.product_id) - - is_done = False - - while not is_done: - _, _, is_done = env.step(None) - - # check demand history, it should be same as tick - seller_nodes = env.snapshot_list["seller"] - - demand_states = seller_nodes[:sell_unit.data_model_index:"demand"].flatten().astype(np.int) - - self.assertListEqual([i for i in range(100)], list(demand_states)) - - # check sold states - # it should be 0 after tick 4 - sold_states = seller_nodes[:sell_unit.data_model_index:"sold"].flatten().astype(np.int) - self.assertListEqual([0, 1, 2, 3, 4] + [0] * 95, list(sold_states)) - - # total sold - total_sold_states = seller_nodes[:sell_unit.data_model_index:"total_sold"].flatten().astype(np.int) - # total sold will keep same after tick 4 - self.assertListEqual([0, 1, 3, 6, 10] + [10] * 95, list(total_sold_states)) - - -if __name__ == '__main__': - unittest.main() From 696f5b54664f9a9760e0efc60930616cac942c2e Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Mon, 7 Mar 2022 11:54:13 +0800 Subject: [PATCH 467/482] RL Toolkit V3 (#471) * added daemon=True for multi-process rollout, policy manager and inference * removed obsolete files * [REDO][PR#406]V0.2 rl refinement taskq (#408) * Add a usable task_queue * Rename some variables * 1. Add ; 2. Integrate related files; 3. Remove * merge `data_parallel` and `num_grad_workers` into `data_parallelism` * Fix bugs in docker_compose_yml.py and Simple/Multi-process mode. * Move `grad_worker` into marl/rl/workflows * 1.Merge data_parallel and num_workers into data_parallelism in config; 2.Assign recently used workers as possible in task_queue. * Refine code and update docs of `TaskQueue` * Support priority for tasks in `task_queue` * Update diagram of policy manager and task queue. * Add configurable `single_task_limit` and correct docstring about `data_parallelism` * Fix lint errors in `supply chain` * RL policy redesign (V2) (#405) * Drafi v2.0 for V2 * Polish models with more comments * Polish policies with more comments * Lint * Lint * Add developer doc for models. * Add developer doc for policies. * Remove policy manager V2 since it is not used and out-of-date * Lint * Lint * refined messy workflow code * merged 'scenario_dir' and 'scenario' in rl config * 1. refined env_sampler and agent_wrapper code; 2. added docstrings for env_sampler methods * 1. temporarily renamed RLPolicy from polivy_v2 to RLPolicyV2; 2. merged env_sampler and env_sampler_v2 * merged cim and cim_v2 * lint issue fix * refined logging logic * lint issue fix * reversed unwanted changes * . . . . ReplayMemory & IndexScheduler ReplayMemory & IndexScheduler . MultiReplayMemory get_actions_with_logps EnvSampler on the road EnvSampler Minor * LearnerManager * Use batch to transfer data & add SHAPE_CHECK_FLAG * Rename learner to trainer * Add property for policy._is_exploring * CIM test scenario for V3. Manual test passed. Next step: run it, make it works. * env_sampler.py could run * env_sampler refine on the way * First runnable version done * AC could run, but the result is bad. Need to check the logic * Refine abstract method & shape check error info. * Docs * Very detailed compare. Try again. * AC done * DQN check done * Minor * DDPG, not tested * Minors * A rough draft of MAAC * Cannot use CIM as the multi-agent scenario. * Minor * MAAC refinement on the way * Remove ActionWithAux * Refine batch & memory * MAAC example works * Reproduce-able fix. Policy share between env_sampler and trainer_manager. * Detail refinement * Simplify the user configed workflow * Minor * Refine example codes * Minor polishment * Migrate rollout_manager to V3 * Error on the way * Redesign torch.device management * Rl v3 maddpg (#418) * Add MADDPG trainer * Fit independent critics and shared critic modes. * Add a new property: num_policies * Lint * Fix a bug in `sum(rewards)` * Rename `MADDPG` to `DiscreteMADDPG` and fix type hint. * Rename maddpg in examples. * Preparation for data parallel (#420) * Preparation for data parallel * Minor refinement & lint fix * Lint * Lint * rename atomic_get_batch_grad to get_batch_grad * Fix a unexpected commit * distributed maddpg * Add critic worker * Minor * Data parallel related minorities * Refine code structure for trainers & add more doc strings * Revert a unwanted change * Use TrainWorker to do the actual calculations. * Some minor redesign of the worker's abstraction * Add set/get_policy_state_dict back * Refine set/get_policy_state_dict * Polish policy trainers move train_batch_size to abs trainer delete _train_step_impl() remove _record_impl remove unused methods a minor bug fix in maddpg * Rl v3 data parallel grad worker (#432) * Fit new `trainer_worker` in `grad_worker` and `task_queue`. * Add batch dispatch * Add `tensor_dict` for task submit interface * Move `_remote_learn` to `AbsTrainWorker`. * Complement docstring for task queue and trainer. * Rename train worker to train ops; add placeholder for abstract methods; * Lint Co-authored-by: GQ.Chen * [DRAFT] distributed training pipeline based on RL Toolkit V3 (#450) * Preparation for data parallel * Minor refinement & lint fix * Lint * Lint * rename atomic_get_batch_grad to get_batch_grad * Fix a unexpected commit * distributed maddpg * Add critic worker * Minor * Data parallel related minorities * Refine code structure for trainers & add more doc strings * Revert a unwanted change * Use TrainWorker to do the actual calculations. * Some minor redesign of the worker's abstraction * Add set/get_policy_state_dict back * Refine set/get_policy_state_dict * Polish policy trainers move train_batch_size to abs trainer delete _train_step_impl() remove _record_impl remove unused methods a minor bug fix in maddpg * Rl v3 data parallel grad worker (#432) * Fit new `trainer_worker` in `grad_worker` and `task_queue`. * Add batch dispatch * Add `tensor_dict` for task submit interface * Move `_remote_learn` to `AbsTrainWorker`. * Complement docstring for task queue and trainer. * dsitributed training pipeline draft * added temporary test files for review purposes * Several code style refinements (#451) * Polish rl_v3/utils/ * Polish rl_v3/distributed/ * Polish rl_v3/policy_trainer/abs_trainer.py * fixed merge conflicts * unified sync and async interfaces * refactored rl_v3; refinement in progress * Finish the runnable pipeline under new design * Remove outdated files; refine class names; optimize imports; * Lint * Minor maddpg related refinement * Lint Co-authored-by: Default Co-authored-by: Huoran Li Co-authored-by: GQ.Chen Co-authored-by: ysqyang * Miner bug fix * Coroutine-related bug fix ("get_policy_state") (#452) * fixed rebase conflicts * renamed get_policy_func_dict to policy_creator * deleted unwanted folder * removed unwanted changes * resolved PR452 comments Co-authored-by: ysqyang * Quick fix * Redesign experience recording logic (#453) * Two not important fix * Temp draft. Prepare to WFH * Done * Lint * Lint * Calculating advantages / returns (#454) * V1.0 * Complete DDPG * Rl v3 hanging issue fix (#455) * fixed rebase conflicts * renamed get_policy_func_dict to policy_creator * unified worker interfaces * recovered some files * dist training + cli code move * fixed bugs * added retry logic to client * 1. refactored CIM with various algos; 2. lint * lint * added type hint * removed some logs * lint * Make main.py more IDE friendly * Make main.py more IDE friendly * Lint * Final test & format. Ready to merge. Co-authored-by: ysqyang Co-authored-by: yaqiu Co-authored-by: Huoran Li * Rl v3 parallel rollout (#457) * fixed rebase conflicts * renamed get_policy_func_dict to policy_creator * unified worker interfaces * recovered some files * dist training + cli code move * fixed bugs * added retry logic to client * 1. refactored CIM with various algos; 2. lint * lint * added type hint * removed some logs * lint * Make main.py more IDE friendly * Make main.py more IDE friendly * Lint * load balancing dispatcher * added parallel rollout * lint * Tracker variable type issue; rename to env_sampler_creator; * Rl v3 parallel rollout follow ups (#458) * AbsWorker & AbsDispatcher * Pass env idx to AbsTrainer.record() method, and let the trainer to decide how to record experiences sampled from different worlds. * Fix policy_creator reuse bug * Format code * Merge AbsTrainerManager & SimpleTrainerManager * AC test passed * Lint * Remove AbsTrainer.build() method. Put all initialization operations into __init__ * Redesign AC preprocess batches logic Co-authored-by: ysqyang Co-authored-by: yaqiu Co-authored-by: Huoran Li * MADDPG performance bug fix (#459) * Fix MARL (MADDPG) terminal recording bug; some other minor refinements; * Restore Trainer.build() method * Calculate latest action in the get_actor_grad method in MADDPG. * Share critic bug fix * Rl v3 example update (#461) * updated vm_scheduling example and cim notebook * fixed bugs in vm_scheduling * added local train method * bug fix * modified async client logic to fix hidden issue * reverted to default config * fixed PR comments and some bugs * removed hardcode Co-authored-by: ysqyang Co-authored-by: yaqiu * Done (#462) * Rl v3 load save (#463) * added load/save feature * fixed some bugs * reverted unwanted changes * lint * fixed PR comments Co-authored-by: ysqyang Co-authored-by: yaqiu * RL Toolkit data parallelism revamp & config utils (#464) * added load/save feature * fixed some bugs * reverted unwanted changes * lint * fixed PR comments * 1. fixed data parallelism issue; 2. added config validator; 3. refactored cli local * 1. fixed rollout exit issue; 2. refined config * removed config file from example * fixed lint issues * fixed lint issues * added main.py under examples/rl * fixed lint issues Co-authored-by: ysqyang Co-authored-by: yaqiu * RL doc string (#465) * First rough draft * Minors * Reformat * Lint * Resolve PR comments * Rl type specific env getter (#466) * 1. type-sensitive env variable getter; 2. updated READMEs for examples * fixed bugs * fixed bugs * bug fixes * lint Co-authored-by: ysqyang Co-authored-by: yaqiu * Example bug fix * Optimize parser.py * Resolve PR comments * Rl config doc (#467) * 1. type-sensitive env variable getter; 2. updated READMEs for examples * added detailed doc * lint * wording refined * resolved some PR comments * resolved more PR comments * typo fix Co-authored-by: ysqyang Co-authored-by: ysqyang Co-authored-by: ysqyang Co-authored-by: yaqiu * RL online doc (#469) * Model, policy, trainer * RL workflows and env sampler doc in RST (#468) * First rough draft * Minors * Reformat * Lint * Resolve PR comments * 1. type-sensitive env variable getter; 2. updated READMEs for examples * Rl type specific env getter (#466) * 1. type-sensitive env variable getter; 2. updated READMEs for examples * fixed bugs * fixed bugs * bug fixes * lint Co-authored-by: ysqyang Co-authored-by: yaqiu * Example bug fix * Optimize parser.py * Resolve PR comments * added detailed doc * lint * wording refined * resolved some PR comments * rewriting rl toolkit rst * resolved more PR comments * typo fix * updated rst Co-authored-by: Huoran Li Co-authored-by: Default Co-authored-by: ysqyang Co-authored-by: yaqiu * Finish docs/source/key_components/rl_toolkit.rst * API doc * RL online doc image fix (#470) * resolved some PR comments * fix * fixed PR comments * added numfig=True setting in conf.py for sphinx Co-authored-by: ysqyang * Resolve PR comments * Add example github link Co-authored-by: ysqyang Co-authored-by: ysqyang Co-authored-by: yaqiu * Rl v3 pr comment resolution (#474) * added load/save feature * 1. resolved pr comments; 2. reverted maro/cli/k8s * fixed some bugs Co-authored-by: ysqyang Co-authored-by: yaqiu Co-authored-by: yaqiu Co-authored-by: GQ.Chen Co-authored-by: ysqyang Co-authored-by: ysqyang --- .gitignore | 2 - docker-compose.yml | 27 - docker_files/dev.df | 9 +- docs/source/apidoc/maro.rl.rst | 238 ++++++-- docs/source/conf.py | 2 + .../source/images/rl/distributed_training.svg | 4 + docs/source/images/rl/env_sampler.svg | 3 +- docs/source/images/rl/learning_cycle.svg | 3 - docs/source/images/rl/learning_workflow.svg | 4 + docs/source/images/rl/parallel_rollout.svg | 4 + docs/source/images/rl/policy_manager.svg | 2 +- .../source/images/rl/policy_model_trainer.svg | 4 + docs/source/images/rl/rollout_manager.svg | 3 - docs/source/key_components/rl_toolkit.rst | 272 ++++----- examples/rl/README.md | 15 +- examples/rl/cim/README.md | 7 +- examples/rl/cim/__init__.py | 13 +- examples/rl/cim/algorithms/ac.py | 131 +++++ examples/rl/cim/algorithms/dqn.py | 96 ++++ examples/rl/cim/algorithms/maddpg.py | 135 +++++ examples/rl/cim/callbacks.py | 31 +- examples/rl/cim/config.py | 90 +-- examples/rl/cim/env_sampler.py | 96 ++-- examples/rl/cim/policies.py | 119 ---- examples/rl/cim/policy_trainer.py | 23 + examples/rl/config.yml | 30 - examples/rl/main.py | 64 +++ .../rl/scripts/docker/docker_compose_yml.py | 29 +- examples/rl/vm_scheduling/README.md | 9 +- examples/rl/vm_scheduling/__init__.py | 13 +- examples/rl/vm_scheduling/algorithms/ac.py | 138 +++++ examples/rl/vm_scheduling/algorithms/dqn.py | 118 ++++ examples/rl/vm_scheduling/callbacks.py | 30 +- examples/rl/vm_scheduling/config.py | 86 +-- examples/rl/vm_scheduling/env_sampler.py | 111 ++-- examples/rl/vm_scheduling/policies.py | 118 ---- examples/rl/vm_scheduling/policy_trainer.py | 19 + examples/rl/workflows/config.yml | 31 -- examples/rl/workflows/synchronous/learner.py | 68 --- examples/vm_scheduling/offline_lp/launcher.py | 7 +- maro/cli/k8s/utils/k8s.py | 46 ++ maro/cli/k8s/utils/k8s_manifest_generator.py | 106 ++++ maro/cli/{process => local}/__init__.py | 0 maro/cli/local/commands.py | 256 +++++++++ maro/cli/local/job_manager.py | 97 ++++ maro/cli/local/utils.py | 190 +++++++ maro/cli/maro.py | 211 ++++--- maro/cli/process/agent/job_agent.py | 206 ------- maro/cli/process/agent/resource_agent.py | 93 ---- maro/cli/process/create.py | 18 - maro/cli/process/delete.py | 9 - .../deployment/process_job_deployment.yml | 10 - .../process_schedule_deployment.yml | 16 - .../deployment/process_setting_deployment.yml | 8 - maro/cli/process/executor.py | 248 --------- maro/cli/process/job.py | 30 - maro/cli/process/schedule.py | 15 - maro/cli/process/template.py | 17 - maro/cli/process/utils/default_param.py | 15 - maro/cli/process/utils/details.py | 54 -- maro/cli/utils/azure/acr.py | 17 + maro/cli/utils/azure/aks.py | 55 ++ maro/cli/utils/azure/deployment.py | 31 ++ maro/cli/utils/azure/general.py | 59 ++ maro/cli/utils/azure/resource_group.py | 44 ++ maro/cli/utils/azure/resources.py | 32 ++ maro/cli/utils/azure/storage.py | 97 ++++ maro/cli/utils/azure/vm.py | 49 ++ maro/cli/utils/common.py | 120 ++++ maro/cli/utils/docker.py | 36 ++ maro/cli/utils/params.py | 20 - maro/communication/driver/zmq_driver.py | 10 +- maro/communication/proxy.py | 23 +- maro/rl/distributed/__init__.py | 9 + maro/rl/distributed/abs_proxy.py | 74 +++ maro/rl/distributed/abs_worker.py | 75 +++ maro/rl/exploration/__init__.py | 2 +- maro/rl/exploration/scheduling.py | 48 +- maro/rl/exploration/strategies.py | 40 +- maro/rl/learning/__init__.py | 12 - maro/rl/learning/env_sampler.py | 523 ------------------ maro/rl/learning/env_wrapper.py | 239 -------- maro/rl/learning/helpers.py | 14 - maro/rl/learning/policy_manager.py | 447 --------------- maro/rl/learning/rollout_manager.py | 378 ------------- maro/rl/learning/synchronous/learner.py | 116 ---- maro/rl/model/__init__.py | 18 + maro/rl/model/abs_net.py | 110 ++++ maro/rl/{modeling => model}/fc_block.py | 52 +- maro/rl/model/multi_q_net.py | 84 +++ maro/rl/model/policy_net.py | 166 ++++++ maro/rl/model/q_net.py | 134 +++++ maro/rl/model/v_net.py | 62 +++ maro/rl/modeling/__init__.py | 12 - maro/rl/modeling/core_model.py | 68 --- maro/rl/modeling/specials.py | 214 ------- maro/rl/policy/__init__.py | 18 +- maro/rl/policy/abs_policy.py | 292 ++++++++++ maro/rl/policy/ac.py | 303 ---------- maro/rl/policy/continuous_rl_policy.py | 115 ++++ maro/rl/policy/ddpg.py | 302 ---------- maro/rl/policy/discrete_rl_policy.py | 315 +++++++++++ maro/rl/policy/dqn.py | 443 --------------- maro/rl/policy/index.py | 40 -- maro/rl/policy/pg.py | 224 -------- maro/rl/policy/policy.py | 176 ------ maro/rl/policy/replay.py | 94 ---- maro/rl/policy/worker_allocator.py | 123 ---- maro/rl/rollout/__init__.py | 12 + maro/rl/rollout/batch_env_sampler.py | 193 +++++++ maro/rl/rollout/env_sampler.py | 419 ++++++++++++++ maro/rl/rollout/worker.py | 57 ++ maro/rl/training/__init__.py | 18 + maro/rl/training/algorithms/__init__.py | 14 + maro/rl/training/algorithms/ac.py | 304 ++++++++++ maro/rl/training/algorithms/ddpg.py | 299 ++++++++++ maro/rl/training/algorithms/dqn.py | 226 ++++++++ maro/rl/training/algorithms/maddpg.py | 490 ++++++++++++++++ maro/rl/training/proxy.py | 85 +++ maro/rl/training/replay_memory.py | 479 ++++++++++++++++ maro/rl/training/train_ops.py | 240 ++++++++ maro/rl/training/trainer.py | 267 +++++++++ maro/rl/training/trainer_manager.py | 119 ++++ maro/rl/training/utils.py | 22 + maro/rl/training/worker.py | 74 +++ maro/rl/utils/__init__.py | 16 +- maro/rl/utils/common.py | 72 +++ maro/rl/utils/gradient_averaging.py | 14 - maro/rl/utils/message_enums.py | 10 +- .../utils/__init__.py => rl/utils/objects.py} | 2 + maro/rl/utils/torch_utils.py | 56 ++ maro/rl/utils/trajectory_computation.py | 4 +- maro/rl/utils/transition_batch.py | 119 ++++ maro/rl/workflows/{ => config}/__init__.py | 6 + maro/rl/workflows/config/parser.py | 368 ++++++++++++ maro/rl/workflows/config/template.yml | 76 +++ maro/rl/workflows/grad_worker.py | 72 --- maro/rl/workflows/helpers.py | 45 -- maro/rl/workflows/main.py | 213 +++---- maro/rl/workflows/policy_host.py | 98 ---- maro/rl/workflows/policy_manager.py | 84 --- maro/rl/workflows/rollout.py | 43 -- maro/rl/workflows/rollout_manager.py | 54 -- maro/rl/workflows/rollout_worker.py | 28 + maro/rl/workflows/scenario.py | 42 ++ maro/rl/workflows/train_proxy.py | 12 + maro/rl/workflows/train_worker.py | 27 + .../vm_scheduling/business_engine.py | 4 + maro/utils/__init__.py | 4 +- maro/utils/logger.py | 77 ++- maro/utils/utils.py | 3 +- .../rl_formulation.ipynb | 292 ++++++---- setup.py | 2 +- tests/requirements.test.txt | 2 +- 154 files changed, 8680 insertions(+), 6267 deletions(-) delete mode 100644 docker-compose.yml create mode 100644 docs/source/images/rl/distributed_training.svg delete mode 100644 docs/source/images/rl/learning_cycle.svg create mode 100644 docs/source/images/rl/learning_workflow.svg create mode 100644 docs/source/images/rl/parallel_rollout.svg create mode 100644 docs/source/images/rl/policy_model_trainer.svg delete mode 100644 docs/source/images/rl/rollout_manager.svg create mode 100644 examples/rl/cim/algorithms/ac.py create mode 100644 examples/rl/cim/algorithms/dqn.py create mode 100644 examples/rl/cim/algorithms/maddpg.py delete mode 100644 examples/rl/cim/policies.py create mode 100644 examples/rl/cim/policy_trainer.py delete mode 100644 examples/rl/config.yml create mode 100644 examples/rl/main.py create mode 100644 examples/rl/vm_scheduling/algorithms/ac.py create mode 100644 examples/rl/vm_scheduling/algorithms/dqn.py delete mode 100644 examples/rl/vm_scheduling/policies.py create mode 100644 examples/rl/vm_scheduling/policy_trainer.py delete mode 100644 examples/rl/workflows/config.yml delete mode 100644 examples/rl/workflows/synchronous/learner.py create mode 100644 maro/cli/k8s/utils/k8s.py create mode 100644 maro/cli/k8s/utils/k8s_manifest_generator.py rename maro/cli/{process => local}/__init__.py (100%) create mode 100644 maro/cli/local/commands.py create mode 100644 maro/cli/local/job_manager.py create mode 100644 maro/cli/local/utils.py delete mode 100644 maro/cli/process/agent/job_agent.py delete mode 100644 maro/cli/process/agent/resource_agent.py delete mode 100644 maro/cli/process/create.py delete mode 100644 maro/cli/process/delete.py delete mode 100644 maro/cli/process/deployment/process_job_deployment.yml delete mode 100644 maro/cli/process/deployment/process_schedule_deployment.yml delete mode 100644 maro/cli/process/deployment/process_setting_deployment.yml delete mode 100644 maro/cli/process/executor.py delete mode 100644 maro/cli/process/job.py delete mode 100644 maro/cli/process/schedule.py delete mode 100644 maro/cli/process/template.py delete mode 100644 maro/cli/process/utils/default_param.py delete mode 100644 maro/cli/process/utils/details.py create mode 100644 maro/cli/utils/azure/acr.py create mode 100644 maro/cli/utils/azure/aks.py create mode 100644 maro/cli/utils/azure/deployment.py create mode 100644 maro/cli/utils/azure/general.py create mode 100644 maro/cli/utils/azure/resource_group.py create mode 100644 maro/cli/utils/azure/resources.py create mode 100644 maro/cli/utils/azure/storage.py create mode 100644 maro/cli/utils/azure/vm.py create mode 100644 maro/cli/utils/docker.py create mode 100644 maro/rl/distributed/__init__.py create mode 100644 maro/rl/distributed/abs_proxy.py create mode 100644 maro/rl/distributed/abs_worker.py delete mode 100644 maro/rl/learning/__init__.py delete mode 100644 maro/rl/learning/env_sampler.py delete mode 100644 maro/rl/learning/env_wrapper.py delete mode 100644 maro/rl/learning/helpers.py delete mode 100644 maro/rl/learning/policy_manager.py delete mode 100644 maro/rl/learning/rollout_manager.py delete mode 100644 maro/rl/learning/synchronous/learner.py create mode 100644 maro/rl/model/__init__.py create mode 100644 maro/rl/model/abs_net.py rename maro/rl/{modeling => model}/fc_block.py (61%) create mode 100644 maro/rl/model/multi_q_net.py create mode 100644 maro/rl/model/policy_net.py create mode 100644 maro/rl/model/q_net.py create mode 100644 maro/rl/model/v_net.py delete mode 100644 maro/rl/modeling/__init__.py delete mode 100644 maro/rl/modeling/core_model.py delete mode 100644 maro/rl/modeling/specials.py create mode 100644 maro/rl/policy/abs_policy.py delete mode 100644 maro/rl/policy/ac.py create mode 100644 maro/rl/policy/continuous_rl_policy.py delete mode 100644 maro/rl/policy/ddpg.py create mode 100644 maro/rl/policy/discrete_rl_policy.py delete mode 100644 maro/rl/policy/dqn.py delete mode 100644 maro/rl/policy/index.py delete mode 100644 maro/rl/policy/pg.py delete mode 100644 maro/rl/policy/policy.py delete mode 100644 maro/rl/policy/replay.py delete mode 100644 maro/rl/policy/worker_allocator.py create mode 100644 maro/rl/rollout/__init__.py create mode 100644 maro/rl/rollout/batch_env_sampler.py create mode 100644 maro/rl/rollout/env_sampler.py create mode 100644 maro/rl/rollout/worker.py create mode 100644 maro/rl/training/__init__.py create mode 100644 maro/rl/training/algorithms/__init__.py create mode 100644 maro/rl/training/algorithms/ac.py create mode 100644 maro/rl/training/algorithms/ddpg.py create mode 100644 maro/rl/training/algorithms/dqn.py create mode 100644 maro/rl/training/algorithms/maddpg.py create mode 100644 maro/rl/training/proxy.py create mode 100644 maro/rl/training/replay_memory.py create mode 100644 maro/rl/training/train_ops.py create mode 100644 maro/rl/training/trainer.py create mode 100644 maro/rl/training/trainer_manager.py create mode 100644 maro/rl/training/utils.py create mode 100644 maro/rl/training/worker.py create mode 100644 maro/rl/utils/common.py delete mode 100644 maro/rl/utils/gradient_averaging.py rename maro/{cli/process/utils/__init__.py => rl/utils/objects.py} (74%) create mode 100644 maro/rl/utils/torch_utils.py create mode 100644 maro/rl/utils/transition_batch.py rename maro/rl/workflows/{ => config}/__init__.py (51%) create mode 100644 maro/rl/workflows/config/parser.py create mode 100644 maro/rl/workflows/config/template.yml delete mode 100644 maro/rl/workflows/grad_worker.py delete mode 100644 maro/rl/workflows/helpers.py delete mode 100644 maro/rl/workflows/policy_host.py delete mode 100644 maro/rl/workflows/policy_manager.py delete mode 100644 maro/rl/workflows/rollout.py delete mode 100644 maro/rl/workflows/rollout_manager.py create mode 100644 maro/rl/workflows/rollout_worker.py create mode 100644 maro/rl/workflows/scenario.py create mode 100644 maro/rl/workflows/train_proxy.py create mode 100644 maro/rl/workflows/train_worker.py diff --git a/.gitignore b/.gitignore index 13e3fa8f8..9c4b0721e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,5 @@ data/ maro_venv/ pyvenv.cfg htmlcov/ -*supply_chain_*/ -examples/supply_chain/docker-compose.yml .coverage .coveragerc diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 8bbc22b56..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: "3.9" -services: - redis: - image: redis:6 - container_name: maro-redis - learner: - build: - context: . - dockerfile: docker_files/dev.df - image: maro-dev - container_name: learner - volumes: - - /home/data_disk/yaqiu/maro/examples:/maro/examples - command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "1"] - actor_1: - image: maro-dev - container_name: actor_1 - volumes: - - /home/data_disk/yaqiu/maro/examples:/maro/examples - command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "2"] - actor_2: - image: maro-dev - container_name: actor_2 - volumes: - - /home/data_disk/yaqiu/maro/examples:/maro/examples - command: ["python3", "/maro/examples/supply_chain/dqn/distributed_launcher.py", "-w", "2"] - \ No newline at end of file diff --git a/docker_files/dev.df b/docker_files/dev.df index cc9709d45..00677003e 100644 --- a/docker_files/dev.df +++ b/docker_files/dev.df @@ -1,4 +1,4 @@ -FROM ubuntu:18.04 +FROM python:3.7-buster WORKDIR /maro # Install Apt packages @@ -9,11 +9,11 @@ RUN apt-get install -y gcc RUN apt-get install -y libcurl4 libcurl4-openssl-dev libssl-dev curl RUN apt-get install -y libzmq3-dev RUN apt-get install -y python3-pip -RUN apt-get install -y python3-dev libpython3.6-dev python-numpy +RUN apt-get install -y python3-dev libpython3.7-dev python-numpy RUN rm -rf /var/lib/apt/lists/* # Install Python packages -RUN pip3 install --upgrade pip +RUN pip install --upgrade pip RUN pip install --no-cache-dir Cython==0.29.14 RUN pip install --no-cache-dir pyaml==20.4.0 RUN pip install --no-cache-dir pyzmq==19.0.2 @@ -31,9 +31,6 @@ COPY setup.py /maro/ RUN bash /maro/scripts/install_maro.sh RUN pip cache purge -RUN rm -r /maro/maro/rl -RUN rm -r /maro/maro/simulator/scenarios/supply_chain - ENV PYTHONPATH=/maro CMD ["/bin/bash"] diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index 0d7e0b831..ce1f550cb 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -1,6 +1,33 @@ +Distributed +================================================================================ + +maro.rl.distributed.abs_proxy +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.distributed.abs_proxy + :members: + :undoc-members: + :show-inheritance: + +maro.rl.distributed.abs_worker +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.distributed.abs_worker + :members: + :undoc-members: + :show-inheritance: + Exploration ================================================================================ +maro.rl.exploration.scheduling +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.exploration.scheduling + :members: + :undoc-members: + :show-inheritance: + maro.rl.exploration.strategies -------------------------------------------------------------------------------- @@ -9,127 +36,276 @@ maro.rl.exploration.strategies :undoc-members: :show-inheritance: +Model +================================================================================ -maro.rl.exploration.scheduling +maro.rl.model.abs_net -------------------------------------------------------------------------------- -.. automodule:: maro.rl.exploration.scheduling +.. automodule:: maro.rl.model.abs_net + :members: + :undoc-members: + :show-inheritance: + +maro.rl.model.fc_block +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.model.fc_block + :members: + :undoc-members: + :show-inheritance: + +maro.rl.model.multi_q_net +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.model.multi_q_net + :members: + :undoc-members: + :show-inheritance: + +maro.rl.model.policy_net +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.model.policy_net + :members: + :undoc-members: + :show-inheritance: + +maro.rl.model.q_net +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.model.q_net + :members: + :undoc-members: + :show-inheritance: + +maro.rl.model.v_net +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.model.v_net + :members: + :undoc-members: + :show-inheritance: + +Policy +================================================================================ + +maro.rl.policy.abs_policy +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.abs_policy + :members: + :undoc-members: + :show-inheritance: + +maro.rl.policy.continuous_rl_policy +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.continuous_rl_policy + :members: + :undoc-members: + :show-inheritance: + +maro.rl.policy.discrete_rl_policy +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.policy.discrete_rl_policy + :members: + :undoc-members: + :show-inheritance: + +Rollout +================================================================================ + +maro.rl.rollout.batch_env_sampler +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.rollout.batch_env_sampler + :members: + :undoc-members: + :show-inheritance: + +maro.rl.rollout.env_sampler +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.rollout.env_sampler :members: :undoc-members: :show-inheritance: +maro.rl.rollout.worker +-------------------------------------------------------------------------------- -Modeling +.. automodule:: maro.rl.rollout.worker + :members: + :undoc-members: + :show-inheritance: + +Training ================================================================================ -maro.rl.modeling.core_model +maro.rl.training.algorithms +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.training.algorithms + :members: + :undoc-members: + :show-inheritance: + +maro.rl.training.proxy +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.training.proxy + :members: + :undoc-members: + :show-inheritance: + +maro.rl.training.replay_memory +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.training.replay_memory + :members: + :undoc-members: + :show-inheritance: + +maro.rl.training.trainer +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.training.trainer + :members: + :undoc-members: + :show-inheritance: + +maro.rl.training.trainer_manager -------------------------------------------------------------------------------- -.. automodule:: maro.rl.modeling.core_model +.. automodule:: maro.rl.training.trainer_manager :members: :undoc-members: :show-inheritance: -maro.rl.modeling.fc_block +maro.rl.training.train_ops -------------------------------------------------------------------------------- -.. automodule:: maro.rl.modeling.fc_block +.. automodule:: maro.rl.training.train_ops :members: :undoc-members: :show-inheritance: -maro.rl.modeling.specials +maro.rl.training.utils -------------------------------------------------------------------------------- -.. automodule:: maro.rl.modeling.specials +.. automodule:: maro.rl.training.utils :members: :undoc-members: :show-inheritance: +maro.rl.training.worker +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.training.worker + :members: + :undoc-members: + :show-inheritance: -Learning +Utils ================================================================================ -maro.rl.learning.env_sampler +maro.rl.utils.common -------------------------------------------------------------------------------- -.. automodule:: maro.rl.learning.env_sampler +.. automodule:: maro.rl.utils.common :members: :undoc-members: :show-inheritance: -maro.rl.learning.learning_loop +maro.rl.utils.message_enums -------------------------------------------------------------------------------- -.. automodule:: maro.rl.learning.learning_loop +.. automodule:: maro.rl.utils.message_enums :members: :undoc-members: :show-inheritance: -maro.rl.learning.policy_manager +maro.rl.utils.objects -------------------------------------------------------------------------------- -.. automodule:: maro.rl.learning.policy_manager +.. automodule:: maro.rl.utils.objects :members: :undoc-members: :show-inheritance: -maro.rl.learning.rollout_manager +maro.rl.utils.torch_utils -------------------------------------------------------------------------------- -.. automodule:: maro.rl.learning.rollout_manager +.. automodule:: maro.rl.utils.torch_utils :members: :undoc-members: :show-inheritance: +maro.rl.utils.trajectory_computation +-------------------------------------------------------------------------------- -Policy +.. automodule:: maro.rl.utils.trajectory_computation + :members: + :undoc-members: + :show-inheritance: + +maro.rl.utils.transition_batch +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.utils.transition_batch + :members: + :undoc-members: + :show-inheritance: + +Workflows ================================================================================ -maro.rl.policy.ac +maro.rl.workflows.config -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.ac +.. automodule:: maro.rl.workflows.config :members: :undoc-members: :show-inheritance: -maro.rl.policy.ddpg +maro.rl.workflows.main -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.ddpg +.. automodule:: maro.rl.workflows.main :members: :undoc-members: :show-inheritance: -maro.rl.policy.dqn +maro.rl.workflows.rollout_worker -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.dqn +.. automodule:: maro.rl.workflows.rollout_worker :members: :undoc-members: :show-inheritance: -maro.rl.policy.pg +maro.rl.workflows.scenario -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.pg +.. automodule:: maro.rl.workflows.scenario :members: :undoc-members: :show-inheritance: -maro.rl.policy.policy +maro.rl.workflows.train_proxy -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.policy +.. automodule:: maro.rl.workflows.train_proxy :members: :undoc-members: :show-inheritance: -maro.rl.policy.replay +maro.rl.workflows.train_worker -------------------------------------------------------------------------------- -.. automodule:: maro.rl.policy.replay +.. automodule:: maro.rl.workflows.train_worker :members: :undoc-members: :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index 0409844ec..e423c0781 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -100,3 +100,5 @@ } source_suffix = [".md", ".rst"] + +numfig = True diff --git a/docs/source/images/rl/distributed_training.svg b/docs/source/images/rl/distributed_training.svg new file mode 100644 index 000000000..546e80a5f --- /dev/null +++ b/docs/source/images/rl/distributed_training.svg @@ -0,0 +1,4 @@ + + + +
Trainer Manager
Trainer Manag...
Worker
Worker
Proxy
Proxy
Task
Task
Result
Result
Task
Task
Result
Result
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/source/images/rl/env_sampler.svg b/docs/source/images/rl/env_sampler.svg index 7ccffae1d..04c09f31a 100644 --- a/docs/source/images/rl/env_sampler.svg +++ b/docs/source/images/rl/env_sampler.svg @@ -1,3 +1,4 @@ + -
get_state()
get_state()
Environment Simulator
Environment S...
get_env_actions()
get_env_actions()
Environment Sampler
Environment Sampler
Agent
Agent
Agent
Agent
Policy
Policy
state by agent
state by agent
action by agent
action by age...
get_reward()
get_reward()
agent-policy mapping
agent-policy...
state batch by policy
state batch by po...
agent-policy mapping
agent-policy...
action batch by policy
action batch by pol...
Viewer does not support full SVG 1.1
\ No newline at end of file +
Environment Simulator
Environment S...
Environment Sampler
Environment Sampler
Agent
Agent
Agent
Agent
Policy
Policy
actions
actions
states (global and per agent)
states (global and per a...
rewards
rewards
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/source/images/rl/learning_cycle.svg b/docs/source/images/rl/learning_cycle.svg deleted file mode 100644 index 9705e800c..000000000 --- a/docs/source/images/rl/learning_cycle.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Roll-out Manager
Roll-out Mana...
collect / evaluate
collect / eva...
roll-out info
roll-out i...
Policy
Manager
Policy...
policy states
policy stat...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/images/rl/learning_workflow.svg b/docs/source/images/rl/learning_workflow.svg new file mode 100644 index 000000000..ead7a9b14 --- /dev/null +++ b/docs/source/images/rl/learning_workflow.svg @@ -0,0 +1,4 @@ + + + +
Roll-out Phase
Roll-out Phase
training data
training d...
Training Phase
Training Phase
updated policies
updated polic...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/source/images/rl/parallel_rollout.svg b/docs/source/images/rl/parallel_rollout.svg new file mode 100644 index 000000000..7b18e1928 --- /dev/null +++ b/docs/source/images/rl/parallel_rollout.svg @@ -0,0 +1,4 @@ + + + +
Worker
Worker
Roll-out Controller
Roll-out Controller
sample
sample
eval
eval
environment simulator
environment simul...
roll-out message
roll-out messa...
results
results
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/source/images/rl/policy_manager.svg b/docs/source/images/rl/policy_manager.svg index 946b1b28b..96cc1b31c 100644 --- a/docs/source/images/rl/policy_manager.svg +++ b/docs/source/images/rl/policy_manager.svg @@ -1,3 +1,3 @@ -
Simple Policy Manager
Simple Policy Manager
policies
policies
update
update
get_state
get_state
Simple Policy Manager 
Simple Policy Manager 
Distributed Policy Manager
Distributed Policy Manager
Policy 
Policy 
update
update
get_state
get_state
Trainer (Grad Worker)
Trainer (Grad Worker)
Auto-balanced task dispatching message
Auto-balanced tas...
gradient
gradient
Policy State 1
Policy State 1
Experience Batch 1
Experience Batch 1
Task
Task
Policy State 2
Policy State 2
Experience Batch 2
Experience Batch 2
Task
Task
...
...
Distributed Policy Manager
Distributed Policy Manager
policies
policies
Policy
Policy
Trainer (Grad Worker)
Trainer (Grad Worker)
Auto-balanced task dispatching message
Auto-balanced tas...
gradient
gradient
Policy State 1
Policy State 1
Experience Batch 1
Experience Batch 1
Task
Task
Policy State 2
Policy State 2
Experience Batch 2
Experience Batch 2
Task
Task
...
...
Policy Host
Policy Host
Experience Memory
Experience Memory
Experience Memory
Experience Me...
Experience Memory
Experience Me...
Experience Memory
Experience Me...
Viewer does not support full SVG 1.1
+
Simple Policy Manager
Simple Policy Manager
policies
policies
update
update
get_state
get_state
Simple Policy Manager 
Simple Policy Manager 
Distributed Policy Manager
Distributed Policy Manager
Policy 
Policy 
update
update
get_state
get_state
Trainer (Grad Worker)
Trainer (Grad Worker)
Auto-balanced task dispatching message
Auto-balanced tas...
gradient
gradient
Policy State 1
Policy State 1
Experience Batch 1
Experience Batch 1
Task
Task
Policy State 2
Policy State 2
Experience Batch 2
Experience Batch 2
Task
Task
...
...
Distributed Policy Manager
Distributed Policy Manager
policies
policies
Policy
Policy
Trainer (Grad Worker)
Trainer (Grad Worker)
Auto-balanced task dispatching message
Auto-balanced tas...
gradient
gradient
Policy State 1
Policy State 1
Experience Batch 1
Experience Batch 1
Task
Task
Policy State 2
Policy State 2
Experience Batch 2
Experience Batch 2
Task
Task
...
...
Policy Host
Policy Host
Task Queue
Task Queue
Request Workers
Request Workers
Worker IDs
for training
Worker IDs...
Update Available
Worker List
Update Available...
Task Queue
Task Queue
Request Workers
Request Workers
Worker IDs
for training
Worker IDs...
Update Available
Worker List
Update Available...
Viewer does not support full SVG 1.1
diff --git a/docs/source/images/rl/policy_model_trainer.svg b/docs/source/images/rl/policy_model_trainer.svg new file mode 100644 index 000000000..74697e159 --- /dev/null +++ b/docs/source/images/rl/policy_model_trainer.svg @@ -0,0 +1,4 @@ + + + +
Policy








Policy...
Model
Model
Trainer








Trainer...
Experience
Memory
Experience...
Auxiliary
models
Auxiliary...
Training-related interfaces
Environment
Environment
StatesActionsRecord experiences
Monitor
Monitor
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/source/images/rl/rollout_manager.svg b/docs/source/images/rl/rollout_manager.svg deleted file mode 100644 index 6f296e4a8..000000000 --- a/docs/source/images/rl/rollout_manager.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Roll-out Worker
Roll-out Worker
Rollout Manager
Rollout Manager
collect
colle...
evaluate
evalu...
env sampler
env sampler
roll-out message
roll-out messa...
results
results
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index b28da9bb1..1c9fde4ff 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -2,55 +2,64 @@ RL Toolkit ========== MARO provides a full-stack abstraction for reinforcement learning (RL) which includes various customizable -components. In order to provide a gentle introduction for the RL toolkit, we cover the components in top-down -fashion, starting from the outermost layer, the learning workflows. The RL toolkit supports single-threaded and -distributed learning workflows. The distributed workflow can be synchronous or asynchronous. - - -Synchronous Learning --------------------- - -In synchronous mode, a main process executes 2-phase learning cycles consisting of simulation data collection and -policy update. The data collection phase is controlled by a roll-out manager and the policy update phase is controlled -by a :ref:`policy-manager`. The transition from one phase to the other is synchrounous. To handle slow roll-out workers, the -roll-out manager can be configured to pass the results from a subset of roll-out workers (i.e., the faster ones) to the -policy manager. On the other hand, the policy manager always waits until all policies are updated before passing the -policy states to the roll-out manager. - - -.. figure:: ../images/rl/learning_cycle.svg +components. In order to provide a gentle introduction for the RL toolkit, we cover the components in a top-down +manner, starting from the learning workflow. + +Workflow +-------- + +The nice thing about MARO's RL workflows is that it is abstracted neatly from business logic, policies and learning algorithms, +making it applicable to practically any scenario that utilizes standard reinforcement learning paradigms. The workflow is +controlled by a main process that executes 2-phase learning cycles: roll-out and training (:numref:`1`). The roll-out phase +collects data from one or more environment simulators for training. There can be a single environment simulator located in the same thread as the main +loop, or multiple environment simulators running in parallel on a set of remote workers (:numref:`2`) if you need to collect large amounts of data +fast. The training phase uses the data collected during the roll-out phase to train models involved in RL policies and algorithms. +In the case of multiple large models, this phase can be made faster by having the computationally intensive gradient-related tasks +sent to a set of remote workers for parallel processing (:numref:`3`). + +.. _1: +.. figure:: ../images/rl/learning_workflow.svg :alt: Overview - - Synchronous Learning Cycle + :align: center + + Learning Workflow -.. figure:: ../images/rl/rollout_manager.svg +.. _2: +.. figure:: ../images/rl/parallel_rollout.svg :alt: Overview + :align: center - Roll-out Manager + Parallel Roll-out -Asynchronous Learning ---------------------- +.. _3: +.. figure:: ../images/rl/distributed_training.svg + :alt: Overview + :align: center -The asynchronous mode consists of a policy server and multiple actors in a client-server architecture. Each actor runs -its own roll-out loop and periodically sends the collected data to the server. The server uses the data to update the -policies and responds with the latest policy states. As the actors may differ significantly in speed, the policy server -only uses data generated by policies sufficiently up-to-date, but always sends the latest policy states to every single -actor. The asynchronous learning feature is still under development and not ready for production use, as it only -supports a single policy server and hence does not provide fault tolerence. + Distributed Training Environment Sampler ------------------- -It is necessary to implement an environment sampler (a subclass of ``AbsEnvSampler``) with user-defined state, action -and reward shaping to collect roll-out information for learning and testing purposes. An environment sampler can be -easily turned into a roll-out worker or an actor for synchronous and asynchronous learning, respectively. +An environment sampler is an entity that contains an environment simulator and a set of policies used by agents to +interact with the environment (:numref:`4`). When creating an RL formulation for a scenario, it is necessary to define an environment +sampler class that includes these key elements: + +- how observations / snapshots of the environment are encoded into state vectors as input to the policy models. This + is sometimes referred to as state shaping in applied reinforcement learning; +- how model outputs are converted to action objects defined by the environment simulator; +- how rewards / penalties are evaluated. This is sometimes referred to as reward shaping. +In parallel roll-out, each roll-out worker should have its own environment sampler instance. + +.. _4: .. figure:: ../images/rl/env_sampler.svg :alt: Overview + :align: center Environment Sampler @@ -58,139 +67,130 @@ easily turned into a roll-out worker or an actor for synchronous and asynchronou Policy ------ -The ``AbsPolicy`` abstraction is the core component of MARO's RL toolkit. A policy is a an agent's mechanism to select -actions in its interaction with an environment. MARO allows arbitrary agent-to-policy mapping to make policy sharing -easily configurable. It is also possible to use a mixture of rule-based policies and ``RLPolicy`` instances. The latter -provides various policy improvement interfaces to support single-threaded and distributed learning. +``Policy`` is the most important concept in reinforcement learning. In MARO, the highest level abstraction of a policy +object is ``AbsPolicy``. It defines the interface ``get_actions()`` which takes a batch of states as input and returns +corresponding actions. +The action is defined by the policy itself. It could be a scalar or a vector or any other types. +Env sampler should take responsibility for parsing the action to the acceptable format before passing it to the +environment. +The simplest type of policy is ``RuleBasedPolicy`` which generates actions by pre-defined rules. ``RuleBasedPolicy`` +is mostly used in naive scenarios. However, in most cases where we need to train the policy by interacting with the +environment, we need to use ``RLPolicy``. In MARO's design, a policy cannot train itself. Instead, +polices could only be trained by :ref:`trainer` (we will introduce trainer later in this page). Therefore, in addition +to ``get_actions()``, ``RLPolicy`` also has a set of training-related interfaces, such as ``step()``, ``get_gradients()`` +and ``set_gradients()``. These interfaces will be called by trainers for training. As you may have noticed, currently +we assume policies are built upon deep learning models, so the training-related interfaces are specifically +designed for gradient descent. -.. code-block:: python - class AbsPolicy(ABC): - def __init__(self, name) - super().__init__() - self._name = name +``RLPolicy`` is further divided into three types: +- ``ValueBasedPolicy``: For valued-based policies. +- ``DiscretePolicyGradient``: For gradient-based policies that generate discrete actions. +- ``ContinuousPolicyGradient``: For gradient-based policies that generate continuous actions. - @abstractmethod - def __call__(self, state): - """Select an action based on a state.""" - raise NotImplementedError +The above classes are all concrete classes. Users do not need to implement any new classes, but can directly +create a policy object by configuring parameters. Here is a simple example: +.. code-block:: python - class RLPolicy(AbsPolicy): - ... + ValueBasedPolicy( + name="policy", + q_net=MyQNet(state_dim=128, action_num=64), + ) - def record(self, key: str, state, action, reward, next_state, terminal: bool): - pass - def get_rollout_info(self): - pass +For now, you may have no idea about the ``q_net`` parameter, but don't worry, we will introduce it in the next section. - def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - pass +Model +----- - def update(self, loss_info_list: List[dict]): - pass +The above code snippet creates a ``ValueBasedPolicy`` object. Let's pay attention to the parameter ``q_net``. +``q_net`` accepts a ``DiscreteQNet`` object, and it serves as the core part of a ``ValueBasedPolicy`` object. In +other words, ``q_net`` defines the model structure of the Q-network in the value-based policy, and further determines +the policy's behavior. ``DiscreteQNet`` is an abstract class, and ``MyQNet`` is a user-defined implementation +of ``DiscreteQNet``. It can be a simple MLP, a multihead transformer, or any other structure that the user wants. - def learn(self, batch: dict): - pass +MARO provides a set of abstractions of basic & commonly used PyTorch models like ``DiscereteQNet``, which enables +users to implement their own deep learning models in a handy way. They are: - def improve(self): - pass +- ``DiscreteQNet``: For ``ValueBasedPolicy``. +- ``DiscretePolicyNet``: For ``DiscretePolicyGradient``. +- ``ContinuousPolicyNet``: For ``ContinuousPolicyGradient``. +Users should choose the proper types of models according to the type of policies, and then implement their own +models by inheriting the abstract ones (just like ``MyQNet``). -.. _policy-manager: +There are also some other models for training purposes. For example: -Policy Manager --------------- +- ``VNet``: Used in the critic part in the actor-critic algorithm. +- ``MultiQNet``: Used in the critic part in the MADDPG algorithm. +- ... -A policy manager controls policy update and provides the latest policy states for roll-outs. In synchrounous learning, -the policy manager controls the policy update phase of a learning cycle. In asynchrounous learning, the policy manager -is present as a server process. The types of policy manager include: +The way to use these models is exactly the same as the way to use the policy models. -* ``SimplePolicyManager``, where all policies instances reside within the manager and are updated sequentially; -* ``MultiProcessPolicyManager``, where each policy is placed in a dedicated process that runs event loops to receive - roll-out information for update; this approach takes advantage of the parallelism from Python's multi-processing, so - the policies can be updated in parallel, but comes with inter-process communication overhead; -* ``DistributedPolicyManager``, where policies are distributed among a set of remote compute nodes that run event loops - to reeeive roll-out information for update. This approach allows the policies to be updated in parallel and may be - necessary when the combined size of the policies is too big to fit in a single node. +.. _trainer: -Moreover, in ``data-parallel`` mode, each policy manager has an additional worker(``grad_worker``) -allocator, which provides a policy-to-worker mapping. The worker allocator performs auto-balance -during training, by dynamically adjusting worker number for policies according to the -experience/agent/policy number. +Trainer +------- -.. image:: ../images/rl/policy_manager.svg - :target: ../images/rl/policy_manager.svg - :alt: PolicyManager +When introducing policies, we mentioned that policies cannot train themselves. Instead, they have to be trained +by external trainers. In MARO, a trainer represents an RL algorithm, such as DQN, actor-critic, +and so on. Trainers take interaction experiences and store them in the internal memory, and then use the experiences +in the memory to train the policies. Like ``RLPolicy``, trainers are also concrete classes, which means they could +be used by configuring parameters. Currently, we have 4 trainers (algorithms) in MARO: -The ``DistributedPolicyManager`` runs a set of ``policy_host`` and a ``TrainerAllocator``. -``policy_host`` is a process/VM/node that hosts the update of a policy. The ``TrainerAllocator`` -dynamically adjusts worker node numbers for policies according to the experience/agent/policy -number. Each ``policy_host`` independently updates its own policies for policy-level parallelism. +- ``DiscreteActorCritic``: Actor-critic algorithm for policies that generate discrete actions. +- ``DDPG``: DDPG algorithm for policies that generate continuous actions. +- ``DQN``: DQN algorithm for policies that generate discrete actions. +- ``DiscreteMADDPG``: MADDPG algorithm for policies that generate discrete actions. -During training, the ``PolicyManager`` receives training data collected by the ``RolloutManager``, -then send them to corresponding ``policy_host``. Each ``policy_host`` will send gradient tasks consist -of policy state and experience batch, to several stateless ``grad_worker`` for gradient computation. -The ``grad_worker`` is stateless, and computes gradients using the policy state and data -batch provided in a task. -Then ``policy_host`` aggregates the gradients from ``grad_worker`` s, and performs gradient descent -on its parameters. +Each trainer has a corresponding ``Param`` class to manage all related parameters. For example, +``DiscreteActorCriticParams`` contains all parameters used in ``DiscreteActorCritic``: -Core Model ----------- +.. code-block:: python -In the deep reinforcement learning (DRL) world, a policy usually includes one or more neural-network com-based models, -which may be used to compute action preferences or estimate state / action values. The ``AbsCoreModel`` represents a -collection of network components with embedded optimizers and exposes unified interfaces to decouple model inference -and optimization from the algorithmic aspects of the policy that uses them. For example, the actor-critic algorithm -does not need to concern itself with how the action probabilities and state values are computed. Subclasses of -``AbsCoreModel`` provided for use with specific RL algorithms include ``DiscreteQNet`` for DQN, ``DiscretePolicyNet`` -for Policy Gradient, ``DiscreteACNet`` for Actor-Critic and ``ContinuousACNet`` for DDPG. + @dataclass + class DiscreteActorCriticParams(TrainerParams): + get_v_critic_net_func: Callable[[], VNet] = None + reward_discount: float = 0.9 + grad_iters: int = 1 + critic_loss_cls: Callable = None + clip_ratio: float = None + lam: float = 0.9 + min_logp: Optional[float] = None -The code snippet below shows how to create a model for the actor-critic algorithm with a shared bottom stack: +An example of creating an actor-critic trainer: .. code-block:: python - shared_net_conf = {...} - actor_net_conf = {...} - critic_net_conf = {...} - shared_optim_conf = {torch.optim.SGD, {"lr": 0.0001}} - actor_optim_conf = (torch.optim.Adam, {"lr": 0.001}) - critic_optim_conf = (torch.optim.RMSprop, {"lr": 0.001}) - - class MyACNet(DiscreteACNet): - def __init__(self): - super().__init__() - self.shared = FullyConnected(**shared_net_conf) - self.actor = FullyConnected(**actor_net_conf) - self.critic = FullyConnected(**critic_net_conf) - self.shared_optim = shared_optim_conf[0](self.shared.parameters(), **shared_optim_conf[1]) - self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1]) - self.critic_optim = critic_optim_conf[0](self.critic.parameters(), **critic_optim_conf[1]) - - def forward(self, states, actor: bool = True, critic: bool = True): - representation = self.shared(states) - return (self.actor(representation) if actor else None), (self.critic(representation) if critic else None) - - def step(self, loss): - self.shared_optim.zero_grad() - self.actor_optim.zero_grad() - self.critic_optim.zero_grad() - loss.backward() - self.shared_optim.step() - self.actor_optim.step() - self.critic_optim.step() - -To generate stochastic actions given a batch of states, call ``get_action`` on the model instance: + DiscreteActorCritic( + name='ac', + params=DiscreteActorCriticParams( + device="cpu", + get_v_critic_net_func=lambda: MyCriticNet(state_dim=128), + reward_discount=.0, + grad_iters=10, + critic_loss_cls=torch.nn.SmoothL1Loss, + min_logp=None, + lam=.0 + ) + ) -.. code-block:: python +In order to indicate which trainer each policy is trained by, in MARO, we require that the name of the policy +start with the name of the trainer responsible for training it. For example, policy ``ac_1.policy_1`` is trained +by the trainer named ``ac_1``. Violating this provision will make MARO unable to correctly establish the +corresponding relationship between policy and trainer. - action, log_p, values = ac_model.get_action(state) +More details and examples can be found in the code base (`link`_). -To performing a single gradient step on the model, pass the loss to the ``step`` function: +.. _link: https://github.com/microsoft/maro/blob/master/examples/rl/cim/policy_trainer.py -.. code-block:: python +As a summary, the relationship among policy, model, and trainer is demonstrated in :numref:`5`: + +.. _5: +.. figure:: ../images/rl/policy_model_trainer.svg + :alt: Overview + :align: center - ac_model.step(critic_loss + actor_loss) + Summary of policy, model, and trainer diff --git a/examples/rl/README.md b/examples/rl/README.md index c90605908..59312ad6c 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -1,16 +1,17 @@ # Reinforcement Learning (RL) Examples -This folder contains scenarios that employ reinforcement learning. MARO's RL toolkit makes it possible to use a common workflow on different scenarios, so long as the necessary scenario-related components are provided. The workflow consists of Python scripts for running the necessary components in single-threaded and distributed modes under ``workflows``. General scenario-independent settings can be found in ``config.yml``. The scenario can be chosen by setting the ``scenario`` field in this file. +This folder contains scenarios that employ reinforcement learning. MARO's RL toolkit provides scenario-agnostic workflows to run a variety of scenarios in single-thread, multi-process or distributed modes. ## How to Run -Scripts to run the common workflow in docker containers are in ``scripts/docker``. Start by choosing "single", "sync" or "async" for the ``mode`` field in ``config.yml`` to run a scenario in single-threaded, synchronous and asynchronous modes, respectively. Go to this folder and execute ``bash run.sh`` to launch the program and Docker Compose will take care of starting the necessary containers. Note that the script will build the docker image first if it has not already been built by running ``bash build.sh``. When the program is finished, be sure to run ``bash kill.sh`` to clean up the containers and remove the network. +The ``main.py`` script can be used to run the scenarios under ``examples/rl`` or any user-defined scenario that provides the necessary components (see the section below for details) . To choose a scenario, edit ``SCENARIO_PATH`` in the script to point to the desired scenario folder. You may also edit the rest of the config variables to your own preference. Note that this script runs in single-thread mode only. +To run a scenario in multi-process mode on a local machine, you will need to use the CLI tool (which requires MARO [installation from the source](https://github.com/microsoft/maro#install-maro-from-pypi)). Start by creating a configuration file (.yml) that follows the template ``maro/maro/rl/workflows/config/template.yml`` to specify the scenario-independent settings. Then use the command ``maro local run [-c] path/to/your/config`` to run in containerized (with ``-c``) or non-containerized (without ``-c``) environments. ## Create Your Own Scenarios -The workflow scripts make it easy to create your own scenarios by only supplying the necessary ingredients without worrying about putting them together. It is necessary to create an ``__init__.py`` under your scenario folder (so that it can be treated as a package) and expose all ingredients in it. The ingredients include: +You can create your own scenarios by supplying the necessary ingredients without worrying about putting them together in a workflow. It is necessary to create an ``__init__.py`` under your scenario folder (so that it can be treated as a package) and expose all ingredients in it. The ingredients include: +* Definitions of policies and agent-to-policy mappings. These definitions should be provided as a dictionary named ``policy_creator`` that maps a name to a function that takes the name and returns a policy instance with that name. The agent-to-policy mapping should be provided as a dictionary named ``agent2policy``. +* Definitions of training algorithms. These definitions should be provided as a dictionary named ``trainer_creator`` that maps a name to a function that takes the name and returns a trainer instance with that name. * Definitions of state, action and reward shaping logic pertinent to your simulator and policies. -These definitions should be encapsulated in ``get_env_sampler``, which is a function that takes no parameters and returns an environment sampler; -* Definitions of policies and agent-to-policy mappings. These definitions should be provided as a dictionary named ``policy_func_index`` that maps the name of each policy to a function that creates a policy instance with that name (the policy name should be the function's only parameter). The agent-to-policy mapping should be provided as a dictionary named ``agent2policy``. - -It is possible to have customized routines invoked at the end of a roll-out episode or episode segment. These routines usually involve processing or rendering information collected during roll-out. To do this, first implement the ``post_step`` method in your environment sampler class and populate the ``tracker`` member with whatever information you wish to track during roll-out. Then create two functions, ``post_collect`` and ``post_evaluate``, to process the information contained in each ``tracker`` and expose them in the scenario folder's ``__init__.py``. These functions are used as callbacks in the main learning loop and executed at the end of each training or evaluation episode. See ``cim/callbacks.py`` for a simple example of how to create these functions. +These definitions should be encapsulated in ``env_sampler_creator``, which is a function that takes ``policy_creator`` and returns an environment sampler; +It is possible to have customized routines invoked at the end of a roll-out episode or episode segment. These routines usually involve processing and / or rendering information collected during roll-out. To do this, first implement the ``post_step`` method in your environment sampler class to record whatever information you wish to keep track of during roll-out. Then create functions named ``post_collect`` and ``post_evaluate`` to process the information and expose them in the scenario folder's ``__init__.py``. These functions are used as callbacks in the main learning loop and executed at the end of each training or evaluation episode. See ``cim/callbacks.py`` for a simple example of how to create these functions. diff --git a/examples/rl/cim/README.md b/examples/rl/cim/README.md index 5113acd18..c4ab88276 100644 --- a/examples/rl/cim/README.md +++ b/examples/rl/cim/README.md @@ -1,9 +1,10 @@ # Container Inventory Management This example demonstrates the use of MARO's RL toolkit to optimize container inventory management. The scenario consists of a set of ports, each acting as a learning agent, and vessels that transfer empty containers among them. Each port must decide 1) whether to load or discharge containers when a vessel arrives and 2) how many containers to be loaded or discharged. The objective is to minimize the overall container shortage over a certain period of time. In this folder you can find: -* ``config.py``, which contains environment and policy configurations for the scenario; +* ``config.py``, which contains general configurations for the scenario; +* ``algorithms``, which contains configurations for the Actor-Critic, DQN and discrete-MADDPG algorithms, including network configurations; * ``env_sampler.py``, which defines state, action and reward shaping in the ``CIMEnvSampler`` class; -* ``policies.py``, which defines the Q-net for DQN and the network components for Actor-Critic; +* ``policy_trainer.py``, which contains a registry for the policies and algorithms defined in ``algorithms``; * ``callbacks.py``, which defines routines to be invoked at the end of training or evaluation episodes. -The scripts for running the learning workflows can be found under ``examples/rl/workflows``. See ``README`` under ``examples/rl`` for details about the general applicability of these scripts. We recommend that you follow this example to write your own scenarios. \ No newline at end of file +See ``README.md`` under ``examples/rl`` for details about running the single-threaded learning workflow. We recommend that you follow this example to write your own scenarios. \ No newline at end of file diff --git a/examples/rl/cim/__init__.py b/examples/rl/cim/__init__.py index a103060e1..887c63e0b 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/rl/cim/__init__.py @@ -2,7 +2,14 @@ # Licensed under the MIT license. from .callbacks import post_collect, post_evaluate -from .env_sampler import agent2policy, get_env_sampler -from .policies import policy_func_dict +from .env_sampler import agent2policy, env_sampler_creator +from .policy_trainer import policy_creator, trainer_creator -__all__ = ["agent2policy", "post_collect", "post_evaluate", "get_env_sampler", "policy_func_dict"] +__all__ = [ + "agent2policy", + "env_sampler_creator", + "policy_creator", + "post_collect", + "post_evaluate", + "trainer_creator" +] diff --git a/examples/rl/cim/algorithms/ac.py b/examples/rl/cim/algorithms/ac.py new file mode 100644 index 000000000..d411de103 --- /dev/null +++ b/examples/rl/cim/algorithms/ac.py @@ -0,0 +1,131 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Dict + +import torch +from torch.optim import Adam, RMSprop + +from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet +from maro.rl.policy import DiscretePolicyGradient +from maro.rl.training.algorithms import DiscreteActorCritic, DiscreteActorCriticParams + + +actor_net_conf = { + "hidden_dims": [256, 128, 64], + "activation": torch.nn.Tanh, + "softmax": True, + "batch_norm": False, + "head": True +} +critic_net_conf = { + "hidden_dims": [256, 128, 64], + "output_dim": 1, + "activation": torch.nn.LeakyReLU, + "softmax": False, + "batch_norm": True, + "head": True +} +actor_learning_rate = 0.001 +critic_learning_rate = 0.001 + + +class MyActorNet(DiscretePolicyNet): + def __init__(self, state_dim: int, action_num: int) -> None: + super(MyActorNet, self).__init__(state_dim=state_dim, action_num=action_num) + self._actor = FullyConnected(input_dim=state_dim, output_dim=action_num, **actor_net_conf) + self._optim = Adam(self._actor.parameters(), lr=actor_learning_rate) + + def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: + return self._actor(states) + + def freeze(self) -> None: + self.freeze_all_parameters() + + def unfreeze(self) -> None: + self.unfreeze_all_parameters() + + def step(self, loss: torch.Tensor) -> None: + self._optim.zero_grad() + loss.backward() + self._optim.step() + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() + + def get_state(self) -> dict: + return { + "network": self.state_dict(), + "optim": self._optim.state_dict() + } + + def set_state(self, net_state: dict) -> None: + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) + + +class MyCriticNet(VNet): + def __init__(self, state_dim: int) -> None: + super(MyCriticNet, self).__init__(state_dim=state_dim) + self._critic = FullyConnected(input_dim=state_dim, **critic_net_conf) + self._optim = RMSprop(self._critic.parameters(), lr=critic_learning_rate) + + def _get_v_values(self, states: torch.Tensor) -> torch.Tensor: + return self._critic(states).squeeze(-1) + + def step(self, loss: torch.Tensor) -> None: + self._optim.zero_grad() + loss.backward() + self._optim.step() + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() + + def get_state(self) -> dict: + return { + "network": self.state_dict(), + "optim": self._optim.state_dict() + } + + def set_state(self, net_state: dict) -> None: + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) + + def freeze(self) -> None: + self.freeze_all_parameters() + + def unfreeze(self) -> None: + self.unfreeze_all_parameters() + + +def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: + return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) + + +def get_ac(state_dim: int, name: str) -> DiscreteActorCritic: + return DiscreteActorCritic( + name=name, + params=DiscreteActorCriticParams( + device="cpu", + get_v_critic_net_func=lambda: MyCriticNet(state_dim), + reward_discount=.0, + grad_iters=10, + critic_loss_cls=torch.nn.SmoothL1Loss, + min_logp=None, + lam=.0 + ) + ) diff --git a/examples/rl/cim/algorithms/dqn.py b/examples/rl/cim/algorithms/dqn.py new file mode 100644 index 000000000..38fb50059 --- /dev/null +++ b/examples/rl/cim/algorithms/dqn.py @@ -0,0 +1,96 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Dict + +import torch +from torch.optim import RMSprop + +from maro.rl.exploration import MultiLinearExplorationScheduler, epsilon_greedy +from maro.rl.model import DiscreteQNet, FullyConnected +from maro.rl.policy import ValueBasedPolicy +from maro.rl.training.algorithms import DQN, DQNParams + +q_net_conf = { + "hidden_dims": [256, 128, 64, 32], + "activation": torch.nn.LeakyReLU, + "softmax": False, + "batch_norm": True, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 +} +learning_rate = 0.05 + + +class MyQNet(DiscreteQNet): + def __init__(self, state_dim: int, action_num: int) -> None: + super(MyQNet, self).__init__(state_dim=state_dim, action_num=action_num) + self._fc = FullyConnected(input_dim=state_dim, output_dim=action_num, **q_net_conf) + self._optim = RMSprop(self._fc.parameters(), lr=learning_rate) + + def _get_q_values_for_all_actions(self, states: torch.Tensor) -> torch.Tensor: + return self._fc(states) + + def step(self, loss: torch.Tensor) -> None: + self._optim.zero_grad() + loss.backward() + self._optim.step() + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() + + def get_state(self) -> object: + return {"network": self.state_dict(), "optim": self._optim.state_dict()} + + def set_state(self, net_state: object) -> None: + assert isinstance(net_state, dict) + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) + + def freeze(self) -> None: + self.freeze_all_parameters() + + def unfreeze(self) -> None: + self.unfreeze_all_parameters() + + +def get_policy(state_dim: int, action_num: int, name: str) -> ValueBasedPolicy: + return ValueBasedPolicy( + name=name, + q_net=MyQNet(state_dim, action_num), + exploration_strategy=(epsilon_greedy, {"epsilon": 0.4}), + exploration_scheduling_options=[( + "epsilon", MultiLinearExplorationScheduler, { + "splits": [(2, 0.32)], + "initial_value": 0.4, + "last_ep": 5, + "final_value": 0.0, + } + )], + warmup=100 + ) + + +def get_dqn(name: str) -> DQN: + return DQN( + name=name, + params=DQNParams( + device="cpu", + reward_discount=.0, + update_target_every=5, + num_epochs=10, + soft_update_coef=0.1, + double=False, + replay_memory_capacity=10000, + random_overwrite=False, + batch_size=32 + ) + ) diff --git a/examples/rl/cim/algorithms/maddpg.py b/examples/rl/cim/algorithms/maddpg.py new file mode 100644 index 000000000..4f0bde8cc --- /dev/null +++ b/examples/rl/cim/algorithms/maddpg.py @@ -0,0 +1,135 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from functools import partial +from typing import Dict, List + +import torch +from torch.optim import Adam, RMSprop + +from maro.rl.model import DiscretePolicyNet, FullyConnected, MultiQNet +from maro.rl.policy import DiscretePolicyGradient +from maro.rl.training.algorithms import DiscreteMADDPG, DiscreteMADDPGParams + + +actor_net_conf = { + "hidden_dims": [256, 128, 64], + "activation": torch.nn.Tanh, + "softmax": True, + "batch_norm": False, + "head": True +} +critic_net_conf = { + "hidden_dims": [256, 128, 64], + "output_dim": 1, + "activation": torch.nn.LeakyReLU, + "softmax": False, + "batch_norm": True, + "head": True +} +actor_learning_rate = 0.001 +critic_learning_rate = 0.001 + + +# ##################################################################################################################### +class MyActorNet(DiscretePolicyNet): + def __init__(self, state_dim: int, action_num: int) -> None: + super(MyActorNet, self).__init__(state_dim=state_dim, action_num=action_num) + self._actor = FullyConnected(input_dim=state_dim, output_dim=action_num, **actor_net_conf) + self._optim = Adam(self._actor.parameters(), lr=actor_learning_rate) + + def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: + return self._actor(states) + + def freeze(self) -> None: + self.freeze_all_parameters() + + def unfreeze(self) -> None: + self.unfreeze_all_parameters() + + def step(self, loss: torch.Tensor) -> None: + self._optim.zero_grad() + loss.backward() + self._optim.step() + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() + + def get_state(self) -> dict: + return { + "network": self.state_dict(), + "optim": self._optim.state_dict() + } + + def set_state(self, net_state: dict) -> None: + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) + + +class MyMultiCriticNet(MultiQNet): + def __init__(self, state_dim: int, action_dims: List[int]) -> None: + super(MyMultiCriticNet, self).__init__(state_dim=state_dim, action_dims=action_dims) + self._critic = FullyConnected(input_dim=state_dim + sum(action_dims), **critic_net_conf) + self._optim = RMSprop(self._critic.parameters(), critic_learning_rate) + + def _get_q_values(self, states: torch.Tensor, actions: List[torch.Tensor]) -> torch.Tensor: + return self._critic(torch.cat([states] + actions, dim=1)).squeeze(-1) + + def step(self, loss: torch.Tensor) -> None: + self._optim.zero_grad() + loss.backward() + self._optim.step() + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() + + def get_state(self) -> dict: + return { + "network": self.state_dict(), + "optim": self._optim.state_dict() + } + + def set_state(self, net_state: dict) -> None: + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) + + def freeze(self) -> None: + self.freeze_all_parameters() + + def unfreeze(self) -> None: + self.unfreeze_all_parameters() + + +def get_multi_critic_net(state_dim: int, action_dims: List[int]) -> MyMultiCriticNet: + return MyMultiCriticNet(state_dim, action_dims) + + +def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: + return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) + + +def get_maddpg(state_dim: int, action_dims: List[int], name: str) -> DiscreteMADDPG: + return DiscreteMADDPG( + name=name, + params=DiscreteMADDPGParams( + device="cpu", + reward_discount=.0, + num_epoch=10, + get_q_critic_net_func=partial(get_multi_critic_net, state_dim, action_dims), + shared_critic=False + ) + ) diff --git a/examples/rl/cim/callbacks.py b/examples/rl/cim/callbacks.py index a5d6d1edb..e1308c25e 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/rl/cim/callbacks.py @@ -1,33 +1,26 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import time -from os import makedirs -from os.path import dirname, join, realpath -log_dir = join(dirname(realpath(__file__)), "log", str(time.time())) -makedirs(log_dir, exist_ok=True) - - -def post_collect(trackers, ep, segment): +def post_collect(info_list: list, ep: int, segment: int) -> None: # print the env metric from each rollout worker - for tracker in trackers: - print(f"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}") + for info in info_list: + print(f"env summary (episode {ep}, segment {segment}): {info['env_metric']}") # print the average env metric - if len(trackers) > 1: - metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) - avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} + if len(info_list) > 1: + metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) + avg_metric = {key: sum(info["env_metric"][key] for info in info_list) / num_envs for key in metric_keys} print(f"average env summary (episode {ep}, segment {segment}): {avg_metric}") -def post_evaluate(trackers, ep): +def post_evaluate(info_list: list, ep: int) -> None: # print the env metric from each rollout worker - for tracker in trackers: - print(f"env summary (episode {ep}): {tracker['env_metric']}") + for info in info_list: + print(f"env summary (episode {ep}): {info['env_metric']}") # print the average env metric - if len(trackers) > 1: - metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) - avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} + if len(info_list) > 1: + metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) + avg_metric = {key: sum(info["env_metric"][key] for info in info_list) / num_envs for key in metric_keys} print(f"average env summary (episode {ep}): {avg_metric}") diff --git a/examples/rl/cim/config.py b/examples/rl/cim/config.py index f02b6991a..202b608cf 100644 --- a/examples/rl/cim/config.py +++ b/examples/rl/cim/config.py @@ -1,12 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import torch -from torch.optim import Adam, RMSprop - -from maro.rl.exploration import MultiLinearExplorationScheduler, epsilon_greedy - - env_conf = { "scenario": "cim", "topology": "toy.4p_ssdd_l0.0", @@ -40,86 +34,4 @@ + len(vessel_attributes) ) -############################################## POLICIES ############################################### - -algorithm = "ac" - -# DQN settings -q_net_conf = { - "input_dim": state_dim, - "hidden_dims": [256, 128, 64, 32], - "output_dim": len(action_shaping_conf["action_space"]), - "activation": torch.nn.LeakyReLU, - "softmax": False, - "batch_norm": True, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 -} - -q_net_optim_conf = (RMSprop, {"lr": 0.05}) - -dqn_conf = { - "reward_discount": .0, - "update_target_every": 5, - "num_epochs": 10, - "soft_update_coeff": 0.1, - "double": False, - "exploration_strategy": (epsilon_greedy, {"epsilon": 0.4}), - "exploration_scheduling_options": [( - "epsilon", MultiLinearExplorationScheduler, { - "splits": [(2, 0.32)], - "initial_value": 0.4, - "last_ep": 5, - "final_value": 0.0, - } - )], - "replay_memory_capacity": 10000, - "random_overwrite": False, - "warmup": 100, - "rollout_batch_size": 128, - "train_batch_size": 32, - # "prioritized_replay_kwargs": { - # "alpha": 0.6, - # "beta": 0.4, - # "beta_step": 0.001, - # "max_priority": 1e8 - # } -} - - -# AC settings -actor_net_conf = { - "input_dim": state_dim, - "hidden_dims": [256, 128, 64], - "output_dim": len(action_shaping_conf["action_space"]), - "activation": torch.nn.Tanh, - "softmax": True, - "batch_norm": False, - "head": True -} - -critic_net_conf = { - "input_dim": state_dim, - "hidden_dims": [256, 128, 64], - "output_dim": 1, - "activation": torch.nn.LeakyReLU, - "softmax": False, - "batch_norm": True, - "head": True -} - -actor_optim_conf = (Adam, {"lr": 0.001}) -critic_optim_conf = (RMSprop, {"lr": 0.001}) - -ac_conf = { - "reward_discount": .0, - "grad_iters": 10, - "critic_loss_cls": torch.nn.SmoothL1Loss, - "min_logp": None, - "critic_loss_coeff": 0.1, - "entropy_coeff": 0.01, - # "clip_ratio": 0.8 # for PPO - "lam": .0, - "get_loss_on_rollout": False -} +algorithm = "ac" # ac, dqn or discrete_maddpg diff --git a/examples/rl/cim/env_sampler.py b/examples/rl/cim/env_sampler.py index 82a7cc7cd..7b46d0857 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/rl/cim/env_sampler.py @@ -1,90 +1,69 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import os -import sys +from typing import Any, Callable, Dict, Optional, Tuple import numpy as np -from maro.rl.learning import AbsEnvSampler +from maro.rl.policy import RLPolicy +from maro.rl.rollout import AbsEnvSampler, CacheElement from maro.simulator import Env -from maro.simulator.scenarios.cim.common import Action, ActionType +from maro.simulator.scenarios.cim.common import Action, ActionType, DecisionEvent -cim_path = os.path.dirname(os.path.realpath(__file__)) -if cim_path not in sys.path: - sys.path.insert(0, cim_path) - -from config import ( +from .config import ( action_shaping_conf, algorithm, env_conf, port_attributes, reward_shaping_conf, state_shaping_conf, vessel_attributes ) -from policies import policy_func_dict class CIMEnvSampler(AbsEnvSampler): - def get_state(self, tick=None): - """ - The state vector includes shortage and remaining vessel space over the past k days (where k is the "look_back" - value in ``state_shaping_conf``), as well as all downstream port features. - """ - if tick is None: - tick = self.env.tick - vessel_snapshots, port_snapshots = self.env.snapshot_list["vessels"], self.env.snapshot_list["ports"] - port_idx, vessel_idx = self.event.port_idx, self.event.vessel_idx + def _get_global_and_agent_state( + self, event: DecisionEvent, tick: int = None + ) -> Tuple[Optional[np.ndarray], Dict[Any, np.ndarray]]: + tick = self._env.tick + vessel_snapshots, port_snapshots = self._env.snapshot_list["vessels"], self._env.snapshot_list["ports"] + port_idx, vessel_idx = event.port_idx, event.vessel_idx ticks = [max(0, tick - rt) for rt in range(state_shaping_conf["look_back"] - 1)] - future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') + future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') state = np.concatenate([ - port_snapshots[ticks : [port_idx] + list(future_port_list) : port_attributes], - vessel_snapshots[tick : vessel_idx : vessel_attributes] + port_snapshots[ticks: [port_idx] + list(future_port_list): port_attributes], + vessel_snapshots[tick: vessel_idx: vessel_attributes] ]) - return {port_idx: state} - - def get_env_actions(self, action_by_agent): - """ - The policy output is an integer from [0, 20] which is to be interpreted as the index of ``action_space`` in - ``action_shaping_conf``. For example, action 5 corresponds to -0.5, which means loading 50% of the containers - available at the current port to the vessel, while action 18 corresponds to 0.8, which means loading 80% of the - containers on the vessel to the port. Note that action 10 corresponds 0.0, which means doing nothing. - """ + return state, {port_idx: state} + + def _translate_to_env_action(self, action_dict: Dict[Any, np.ndarray], event: DecisionEvent) -> Dict[Any, object]: action_space = action_shaping_conf["action_space"] finite_vsl_space = action_shaping_conf["finite_vessel_space"] has_early_discharge = action_shaping_conf["has_early_discharge"] - port_idx, action = list(action_by_agent.items()).pop() - vsl_idx, action_scope = self.event.vessel_idx, self.event.action_scope - vsl_snapshots = self.env.snapshot_list["vessels"] - vsl_space = vsl_snapshots[self.env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float("inf") + port_idx, model_action = list(action_dict.items()).pop() - model_action = action["action"] if isinstance(action, dict) else action - percent = abs(action_space[model_action]) + vsl_idx, action_scope = event.vessel_idx, event.action_scope + vsl_snapshots = self._env.snapshot_list["vessels"] + vsl_space = vsl_snapshots[self._env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float("inf") + + percent = abs(action_space[model_action[0]]) zero_action_idx = len(action_space) / 2 # index corresponding to value zero. if model_action < zero_action_idx: action_type = ActionType.LOAD actual_action = min(round(percent * action_scope.load), vsl_space) elif model_action > zero_action_idx: action_type = ActionType.DISCHARGE - early_discharge = vsl_snapshots[self.env.tick:vsl_idx:"early_discharge"][0] if has_early_discharge else 0 + early_discharge = vsl_snapshots[self._env.tick:vsl_idx:"early_discharge"][0] if has_early_discharge else 0 plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge) else: actual_action, action_type = 0, None - return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)] + return {port_idx: Action(vsl_idx, int(port_idx), actual_action, action_type)} - def get_reward(self, actions, tick): - """ - The reward is defined as a linear combination of fulfillment and shortage measures. The fulfillment and - shortage measures are the sums of fulfillment and shortage values over the next k days, respectively, each - adjusted with exponential decay factors (using the "time_decay" value in ``reward_shaping_conf``) to put more - emphasis on the near future. Here k is the "time_window" value in ``reward_shaping_conf``. The linear - combination coefficients are given by "fulfillment_factor" and "shortage_factor" in ``reward_shaping_conf``. - """ + def _get_reward(self, env_action_dict: Dict[Any, object], event: DecisionEvent, tick: int) -> Dict[Any, float]: start_tick = tick + 1 ticks = list(range(start_tick, start_tick + reward_shaping_conf["time_window"])) # Get the ports that took actions at the given tick - ports = [action.port_idx for action in actions] - port_snapshots = self.env.snapshot_list["ports"] + ports = [int(port) for port in list(env_action_dict.keys())] + port_snapshots = self._env.snapshot_list["ports"] future_fulfillment = port_snapshots[ticks:ports:"fulfillment"].reshape(len(ticks), -1) future_shortage = port_snapshots[ticks:ports:"shortage"].reshape(len(ticks), -1) @@ -95,22 +74,17 @@ def get_reward(self, actions, tick): ) return {agent_id: reward for agent_id, reward in zip(ports, rewards)} - def post_step(self, state, action, env_action, reward, tick): - """ - The environment sampler contains a "tracker" dict inherited from the "AbsEnvSampler" base class, which can - be used to record any information one wishes to keep track of during a roll-out episode. Here we simply record - the latest env metric without keeping the history for logging purposes. - """ - self.tracker["env_metric"] = self.env.metrics + def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: + self._info["env_metric"] = self._env.metrics + +agent2policy = {agent: f"{algorithm}_{agent}.policy" for agent in Env(**env_conf).agent_idx_list} -agent2policy = {agent: f"{algorithm}.{agent}" for agent in Env(**env_conf).agent_idx_list} -def get_env_sampler(): +def env_sampler_creator(policy_creator: Dict[str, Callable[[str], RLPolicy]]) -> CIMEnvSampler: return CIMEnvSampler( get_env=lambda: Env(**env_conf), - get_policy_func_dict=policy_func_dict, + policy_creator=policy_creator, agent2policy=agent2policy, - reward_eval_delay=reward_shaping_conf["time_window"], - parallel_inference=True + device="cpu", ) diff --git a/examples/rl/cim/policies.py b/examples/rl/cim/policies.py deleted file mode 100644 index 3bcacf88f..000000000 --- a/examples/rl/cim/policies.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import sys - -from maro.rl.modeling import DiscreteACNet, DiscreteQNet, FullyConnected -from maro.rl.policy import DQN, ActorCritic - -cim_path = os.path.dirname(os.path.realpath(__file__)) -if cim_path not in sys.path: - sys.path.insert(0, cim_path) -from config import ( - ac_conf, actor_net_conf, actor_optim_conf, algorithm, critic_net_conf, critic_optim_conf, dqn_conf, q_net_conf, - q_net_optim_conf, state_dim -) - - -class MyQNet(DiscreteQNet): - def __init__(self): - super().__init__() - self.fc = FullyConnected(**q_net_conf) - self.optim = q_net_optim_conf[0](self.fc.parameters(), **q_net_optim_conf[1]) - - @property - def input_dim(self): - return state_dim - - @property - def num_actions(self): - return q_net_conf["output_dim"] - - def forward(self, states): - return self.fc(states) - - def step(self, loss): - self.optim.zero_grad() - loss.backward() - self.optim.step() - - def get_gradients(self, loss): - self.optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad): - for name, param in self.named_parameters(): - param.grad = grad[name] - - self.optim.step() - - def get_state(self): - return {"network": self.state_dict(), "optim": self.optim.state_dict()} - - def set_state(self, state): - self.load_state_dict(state["network"]) - self.optim.load_state_dict(state["optim"]) - - -class MyACNet(DiscreteACNet): - def __init__(self): - super().__init__() - self.actor = FullyConnected(**actor_net_conf) - self.critic = FullyConnected(**critic_net_conf) - self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1]) - self.critic_optim = critic_optim_conf[0](self.critic.parameters(), **critic_optim_conf[1]) - - @property - def input_dim(self): - return state_dim - - @property - def num_actions(self): - return q_net_conf["output_dim"] - - def forward(self, states, actor: bool = True, critic: bool = True): - return (self.actor(states) if actor else None), (self.critic(states) if critic else None) - - def step(self, loss): - self.actor_optim.zero_grad() - self.critic_optim.zero_grad() - loss.backward() - self.actor_optim.step() - self.critic_optim.step() - - def get_gradients(self, loss): - self.actor_optim.zero_grad() - self.critic_optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad): - for name, param in self.named_parameters(): - param.grad = grad[name] - - self.actor_optim.step() - self.critic_optim.step() - - def get_state(self): - return { - "network": self.state_dict(), - "actor_optim": self.actor_optim.state_dict(), - "critic_optim": self.critic_optim.state_dict() - } - - def set_state(self, state): - self.load_state_dict(state["network"]) - self.actor_optim.load_state_dict(state["actor_optim"]) - self.critic_optim.load_state_dict(state["critic_optim"]) - - -if algorithm == "dqn": - policy_func_dict = { - f"dqn.{i}": lambda name: DQN(name, MyQNet(), **dqn_conf) for i in range(4) - } -else: - policy_func_dict = { - f"ac.{i}": lambda name: ActorCritic(name, MyACNet(), **ac_conf) for i in range(4) - } diff --git a/examples/rl/cim/policy_trainer.py b/examples/rl/cim/policy_trainer.py new file mode 100644 index 000000000..0e011a11c --- /dev/null +++ b/examples/rl/cim/policy_trainer.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from functools import partial + +from .config import algorithm, action_shaping_conf, state_dim + +action_num = len(action_shaping_conf["action_space"]) + +if algorithm == "ac": + from .algorithms.ac import get_ac, get_policy + policy_creator = {f"ac_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(4)} + trainer_creator = {f"ac_{i}": partial(get_ac, state_dim) for i in range(4)} +elif algorithm == "dqn": + from .algorithms.dqn import get_dqn, get_policy + policy_creator = {f"dqn_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(4)} + trainer_creator = {f"dqn_{i}": get_dqn for i in range(4)} +elif algorithm == "discrete_maddpg": + from .algorithms.maddpg import get_policy, get_maddpg + policy_creator = {f"discrete_maddpg_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(4)} + trainer_creator = {f"discrete_maddpg_{i}": partial(get_maddpg, state_dim, [1]) for i in range(4)} +else: + raise ValueError(f"Unsupported algorithm: {algorithm}") diff --git a/examples/rl/config.yml b/examples/rl/config.yml deleted file mode 100644 index 9419cc34f..000000000 --- a/examples/rl/config.yml +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -job: cim -scenario_dir: "/maro/examples" -scenario: cim -load_policy_dir: "/maro/examples/checkpoints/cim" -checkpoint_dir: "/maro/examples/checkpoints/cim" -log_dir: "/maro/examples/logs/cim" -mode: sync # single, sync, async -num_episodes: 10 -eval_schedule: 5 -sync: - num_rollouts: 3 - num_eval_rollouts: 1 - rollout_type: distributed # multi-process, distributed - distributed: - min_finished_workers: 2 - max_extra_recv_tries: 1 - extra_recv_timeout: 200 -async: - max_lag: 3 - num_rollouts: 3 -policy_manager: - type: distributed # simple, multi-process, distributed - distributed: - num_hosts: 2 -redis: - host: maro - port: 6379 \ No newline at end of file diff --git a/examples/rl/main.py b/examples/rl/main.py new file mode 100644 index 000000000..a690f703b --- /dev/null +++ b/examples/rl/main.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os + +from maro.rl.training import TrainerManager +from maro.rl.workflows.scenario import Scenario +from maro.utils import LoggerV2 + +# config variables +SCENARIO_PATH = "cim" +NUM_EPISODES = 50 +NUM_STEPS = None +CHECKPOINT_PATH = os.path.join(os.getcwd(), "checkpoints") +CHECKPOINT_INTERVAL = 5 +EVAL_SCHEDULE = [10, 20, 30, 40, 50] +LOG_PATH = os.path.join(os.getcwd(), "logs", "cim") + + +if __name__ == "__main__": + scenario = Scenario(SCENARIO_PATH) + logger = LoggerV2("MAIN", dump_path=LOG_PATH) + + agent2policy = scenario.agent2policy + policy_creator = scenario.policy_creator + trainer_creator = scenario.trainer_creator + policy_dict = {name: get_policy_func(name) for name, get_policy_func in policy_creator.items()} + policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict} + + # evaluation schedule + logger.info(f"Policy will be evaluated at the end of episodes {EVAL_SCHEDULE}") + eval_point_index = 0 + + env_sampler = scenario.get_env_sampler(policy_creator) + trainer_manager = TrainerManager(policy_creator, trainer_creator, agent2policy, logger=logger) + + # main loop + for ep in range(1, NUM_EPISODES + 1): + collect_time = training_time = 0 + segment, end_of_episode = 1, False + while not end_of_episode: + # experience collection + result = env_sampler.sample(num_steps=NUM_STEPS) + experiences = result["experiences"] + end_of_episode = result["end_of_episode"] + + if scenario.post_collect: + scenario.post_collect(result["info"], ep, segment) + + logger.info(f"Roll-out completed for episode {ep}. Training started...") + trainer_manager.record_experiences(experiences) + trainer_manager.train() + if CHECKPOINT_PATH and ep % CHECKPOINT_INTERVAL == 0: + pth = os.path.join(CHECKPOINT_PATH, str(ep)) + trainer_manager.save(pth) + logger.info(f"All trainer states saved under {pth}") + segment += 1 + + # performance details + if ep == EVAL_SCHEDULE[eval_point_index]: + eval_point_index += 1 + result = env_sampler.eval() + if scenario.post_evaluate: + scenario.post_evaluate(result["info"], ep) diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py index ad2cfd4c3..90c56bba0 100644 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ b/examples/rl/scripts/docker/docker_compose_yml.py @@ -17,7 +17,6 @@ example_dir = dirname(dirname(docker_script_dir)) root_dir = dirname(dirname(example_dir)) maro_rl_dir = join(root_dir, "maro", "rl") - sc_dir = join(root_dir, "maro", "simulator", "scenarios", "supply_chain") with open(join(example_dir, "config.yml"), "r") as fp: config = yaml.safe_load(fp) @@ -28,7 +27,6 @@ "volumes": [ f"{example_dir}:/maro/examples", f"{maro_rl_dir}:/maro/maro/rl", - f"{sc_dir}:/maro/maro/simulator/scenarios/supply_chain" ] } @@ -64,11 +62,15 @@ else: grad_worker_path = join(workflow_dir_in_container, "grad_worker.py") + if "scripts" in config and "task_queue" in config["scripts"]: + task_queue_path = config["scripts"]["task_queue"] + else: + task_queue_path = join(workflow_dir_in_container, "task_queue.py") + if config["mode"] == "single": envs = [ f"JOB={config['job']}", f"SCENARIODIR={config['scenario_dir']}", - f"SCENARIO={config['scenario']}", f"MODE=single", f"NUMEPISODES={config['num_episodes']}", f"EVALSCH={config['eval_schedule']}" @@ -105,7 +107,6 @@ f"REDISPORT={config['redis']['port']}", f"JOB={config['job']}", f"SCENARIODIR={config['scenario_dir']}", - f"SCENARIO={config['scenario']}", f"MODE={config['mode']}", f"POLICYMANAGERTYPE={config['policy_manager']['type']}" ] @@ -116,17 +117,25 @@ common_envs.append(f"CHECKPOINTDIR={config['checkpoint_dir']}") if "log_dir" in config: common_envs.append(f"LOGDIR={config['log_dir']}") - if "data_parallel" in config: - common_envs.append(f"DATAPARALLEL=1") - common_envs.append(f"NUMGRADWORKERS={config['data_parallel']['num_workers']}") - common_envs.append(f"ALLOCATIONMODE={config['data_parallel']['allocation_mode']}") + if "data_parallelism" in config: + common_envs.append(f"DATAPARALLELISM={config['data_parallelism']}") if config["policy_manager"]["type"] == "distributed": common_envs.append(f"POLICYGROUP={policy_group}") common_envs.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") # grad worker config - if "data_parallel" in config: - for worker_id in range(config['data_parallel']['num_workers']): + if "data_parallelism" in config and config["data_parallelism"] > 1: + # task queue + str_id = "task_queue" + task_queue_spec = deepcopy(common_spec) + del task_queue_spec["build"] + task_queue_spec["command"] = f"python3 {task_queue_path}" + task_queue_spec["container_name"] = f"{namespace}.{str_id}" + task_queue_spec["environment"] = common_envs + docker_compose_manifest["services"][str_id] = task_queue_spec + + # grad worker + for worker_id in range(config['data_parallelism']): str_id = f"grad_worker.{worker_id}" grad_worker_spec = deepcopy(common_spec) del grad_worker_spec["build"] diff --git a/examples/rl/vm_scheduling/README.md b/examples/rl/vm_scheduling/README.md index 8d6350873..8b5d509ca 100644 --- a/examples/rl/vm_scheduling/README.md +++ b/examples/rl/vm_scheduling/README.md @@ -2,12 +2,13 @@ A virtual machine (VM) scheduler is a cloud computing service component responsible for providing compute resources to satisfy user demands. A good resource allocation policy should aim to optimize several metrics at the same time, such as user wait time, profit, energy consumption and physical machine (PM) overload. Many commercial cloud providers use rule-based policies. Alternatively, the policy can also be optimized using reinforcement learning (RL) techniques, which involves simulating with historical data. This example demonstrates how DQN and Actor-Critic algorithms can be applied to this scenario. In this folder, you can find: -* ``config.py``, which contains environment and policy configurations. +* ``config.py``, which contains general configurations for the scenario; +* ``algorithms``, which contains configurations for the Actor-Critic, DQN algorithms, including network configurations; * ``env_sampler.py``, which defines state, action and reward shaping in the ``VMEnvSampler`` class; -* ``policies.py``, which defines the Q-net for DQN and the network components for Actor-Critic. -* ``callbacks.py``, which contains routines to be invoked at the end of a training or evaluation episode. +* ``policy_trainer.py``, which contains a registry for the policies and algorithms defined in ``algorithms``; +* ``callbacks.py``, which defines routines to be invoked at the end of training or evaluation episodes. -The scripts to run the learning workflows can be found under ``examples/rl/workflows``. See ``README`` under ``examples/rl`` for details about the general applicability of these scripts. We recommend that you follow this example to write your own scenarios. +See ``README.md`` under ``examples/rl`` for details about running the single-threaded learning workflow. We recommend that you follow this example to write your own scenarios. # Some Comments About the Results diff --git a/examples/rl/vm_scheduling/__init__.py b/examples/rl/vm_scheduling/__init__.py index c3e93fabd..5ffca8684 100644 --- a/examples/rl/vm_scheduling/__init__.py +++ b/examples/rl/vm_scheduling/__init__.py @@ -2,7 +2,14 @@ # Licensed under the MIT license. from .callbacks import post_collect, post_evaluate -from .env_sampler import agent2policy, get_env_sampler -from .policies import policy_func_dict +from .env_sampler import agent2policy, env_sampler_creator +from .policy_trainer import policy_creator, trainer_creator -__all__ = ["agent2policy", "post_collect", "post_evaluate", "get_env_sampler", "policy_func_dict"] +__all__ = [ + "agent2policy", + "env_sampler_creator", + "policy_creator", + "post_collect", + "post_evaluate", + "trainer_creator" +] diff --git a/examples/rl/vm_scheduling/algorithms/ac.py b/examples/rl/vm_scheduling/algorithms/ac.py new file mode 100644 index 000000000..af4b9c5cf --- /dev/null +++ b/examples/rl/vm_scheduling/algorithms/ac.py @@ -0,0 +1,138 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Dict + +import torch +from torch.optim import Adam, SGD + +from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet +from maro.rl.policy import DiscretePolicyGradient +from maro.rl.training.algorithms import DiscreteActorCritic, DiscreteActorCriticParams + + +actor_net_conf = { + "hidden_dims": [64, 32, 32], + "activation": torch.nn.LeakyReLU, + "softmax": True, + "batch_norm": False, + "head": True +} + +critic_net_conf = { + "hidden_dims": [256, 128, 64], + "activation": torch.nn.LeakyReLU, + "softmax": False, + "batch_norm": False, + "head": True +} + +actor_learning_rate = 0.0001 +critic_learning_rate = 0.001 + + +class MyActorNet(DiscretePolicyNet): + def __init__(self, state_dim: int, action_num: int, num_features: int) -> None: + super(MyActorNet, self).__init__(state_dim=state_dim, action_num=action_num) + self._num_features = num_features + self._actor = FullyConnected(input_dim=num_features, output_dim=action_num, **actor_net_conf) + self._optim = Adam(self._actor.parameters(), lr=actor_learning_rate) + + def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: + features, masks = states[:, :self._num_features], states[:, self._num_features:] + masks += 1e-8 # this is to prevent zero probability and infinite logP. + return self._actor(features) * masks + + def freeze(self) -> None: + self.freeze_all_parameters() + + def unfreeze(self) -> None: + self.unfreeze_all_parameters() + + def step(self, loss: torch.Tensor) -> None: + self._optim.zero_grad() + loss.backward() + self._optim.step() + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() + + def get_state(self) -> dict: + return { + "network": self.state_dict(), + "optim": self._optim.state_dict() + } + + def set_state(self, net_state: dict) -> None: + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) + + +class MyCriticNet(VNet): + def __init__(self, state_dim: int, num_features: int) -> None: + super(MyCriticNet, self).__init__(state_dim=state_dim) + self._num_features = num_features + self._critic = FullyConnected(input_dim=num_features, output_dim=1, **critic_net_conf) + self._optim = SGD(self._critic.parameters(), lr=critic_learning_rate) + + def _get_v_values(self, states: torch.Tensor) -> torch.Tensor: + features, masks = states[:, :self._num_features], states[:, self._num_features:] + masks += 1e-8 # this is to prevent zero probability and infinite logP. + return self._critic(features).squeeze(-1) + + def step(self, loss: torch.Tensor) -> None: + self._optim.zero_grad() + loss.backward() + self._optim.step() + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() + + def get_state(self) -> dict: + return { + "network": self.state_dict(), + "optim": self._optim.state_dict() + } + + def set_state(self, net_state: dict) -> None: + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) + + def freeze(self) -> None: + self.freeze_all_parameters() + + def unfreeze(self) -> None: + self.unfreeze_all_parameters() + + +def get_policy(state_dim: int, action_num: int, num_features: int, name: str) -> DiscretePolicyGradient: + return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num, num_features)) + + +def get_ac(state_dim: int, num_features: int, name: str) -> DiscreteActorCritic: + return DiscreteActorCritic( + name=name, + params=DiscreteActorCriticParams( + device="cpu", + get_v_critic_net_func=lambda: MyCriticNet(state_dim, num_features), + reward_discount=0.9, + grad_iters=100, + critic_loss_cls=torch.nn.MSELoss, + min_logp=-20, + lam=.0 + ) + ) diff --git a/examples/rl/vm_scheduling/algorithms/dqn.py b/examples/rl/vm_scheduling/algorithms/dqn.py new file mode 100644 index 000000000..d7126d4c4 --- /dev/null +++ b/examples/rl/vm_scheduling/algorithms/dqn.py @@ -0,0 +1,118 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Dict + +import numpy as np +import torch +from torch.optim import SGD +from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts + +from maro.rl.exploration import MultiLinearExplorationScheduler +from maro.rl.model import DiscreteQNet, FullyConnected +from maro.rl.policy import ValueBasedPolicy +from maro.rl.training.algorithms import DQN, DQNParams + + +q_net_conf = { + "hidden_dims": [64, 128, 256], + "activation": torch.nn.LeakyReLU, + "softmax": False, + "batch_norm": False, + "skip_connection": False, + "head": True, + "dropout_p": 0.0 +} +q_net_learning_rate = 0.0005 +q_net_lr_scheduler_params = {"T_0": 500, "T_mult": 2} + + +class MyQNet(DiscreteQNet): + def __init__(self, state_dim: int, action_num: int, num_features: int) -> None: + super(MyQNet, self).__init__(state_dim=state_dim, action_num=action_num) + self._num_features = num_features + self._fc = FullyConnected(input_dim=num_features, output_dim=action_num, **q_net_conf) + self._optim = SGD(self._fc.parameters(), lr=q_net_learning_rate) + self._lr_scheduler = CosineAnnealingWarmRestarts(self._optim, **q_net_lr_scheduler_params) + + def _get_q_values_for_all_actions(self, states: torch.Tensor) -> torch.Tensor: + masks = states[:, self._num_features:] + q_for_all_actions = self._fc(states[:, :self._num_features]) + return q_for_all_actions + (masks - 1) * 1e8 + + def step(self, loss: torch.Tensor) -> None: + self._optim.zero_grad() + loss.backward() + self._optim.step() + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} + + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() + + def get_state(self) -> object: + return {"network": self.state_dict(), "optim": self._optim.state_dict()} + + def set_state(self, net_state: object) -> None: + assert isinstance(net_state, dict) + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) + + def freeze(self) -> None: + self.freeze_all_parameters() + + def unfreeze(self) -> None: + self.unfreeze_all_parameters() + + +class MaskedEpsGreedy: + def __init__(self, state_dim: int, num_features: int) -> None: + self._state_dim = state_dim + self._num_features = num_features + + def __call__(self, states, actions, num_actions, *, epsilon): + masks = states[:, self._num_features:] + return np.array([ + action if np.random.random() > epsilon else np.random.choice(np.where(mask == 1)[0]) + for action, mask in zip(actions, masks) + ]) + + +def get_policy(state_dim: int, action_num: int, num_features: int, name: str) -> ValueBasedPolicy: + return ValueBasedPolicy( + name=name, + q_net=MyQNet(state_dim, action_num, num_features), + exploration_strategy=(MaskedEpsGreedy(state_dim, num_features), {"epsilon": 0.4}), + exploration_scheduling_options=[( + "epsilon", MultiLinearExplorationScheduler, { + "splits": [(100, 0.32)], + "initial_value": 0.4, + "last_ep": 400, + "final_value": 0.0, + } + )], + warmup=100 + ) + + +def get_dqn(name: str) -> DQN: + return DQN( + name=name, + params=DQNParams( + device="cpu", + reward_discount=0.9, + update_target_every=5, + num_epochs=100, + soft_update_coef=0.1, + double=False, + replay_memory_capacity=10000, + random_overwrite=False, + batch_size=32, + data_parallelism=2 + ) + ) diff --git a/examples/rl/vm_scheduling/callbacks.py b/examples/rl/vm_scheduling/callbacks.py index 240c735b0..3d3371de1 100644 --- a/examples/rl/vm_scheduling/callbacks.py +++ b/examples/rl/vm_scheduling/callbacks.py @@ -14,32 +14,32 @@ makedirs(plt_path, exist_ok=True) -def post_collect(trackers, ep, segment): +def post_collect(info_list, ep, segment): # print the env metric from each rollout worker - for tracker in trackers: - print(f"env summary (episode {ep}, segment {segment}): {tracker['env_metric']}") + for info in info_list: + print(f"env summary (episode {ep}, segment {segment}): {info['env_metric']}") # print the average env metric - if len(trackers) > 1: - metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) - avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} + if len(info_list) > 1: + metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) + avg_metric = {key: sum(tr["env_metric"][key] for tr in info_list) / num_envs for key in metric_keys} print(f"average env metric (episode {ep}, segment {segment}): {avg_metric}") -def post_evaluate(trackers, ep): +def post_evaluate(info_list, ep): # print the env metric from each rollout worker - for tracker in trackers: - print(f"env summary (evaluation episode {ep}): {tracker['env_metric']}") + for info in info_list: + print(f"env summary (evaluation episode {ep}): {info['env_metric']}") # print the average env metric - if len(trackers) > 1: - metric_keys, num_trackers = trackers[0]["env_metric"].keys(), len(trackers) - avg_metric = {key: sum(tr["env_metric"][key] for tr in trackers) / num_trackers for key in metric_keys} + if len(info_list) > 1: + metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) + avg_metric = {key: sum(tr["env_metric"][key] for tr in info_list) / num_envs for key in metric_keys} print(f"average env metric (evaluation episode {ep}): {avg_metric}") - for tracker in trackers: - core_requirement = tracker["actions_by_core_requirement"] - action_sequence = tracker["action_sequence"] + for info in info_list: + core_requirement = info["actions_by_core_requirement"] + action_sequence = info["action_sequence"] # plot action sequence fig = plt.figure(figsize=(40, 32)) ax = fig.add_subplot(1, 1, 1) diff --git a/examples/rl/vm_scheduling/config.py b/examples/rl/vm_scheduling/config.py index 90be7b7c6..67414fbec 100644 --- a/examples/rl/vm_scheduling/config.py +++ b/examples/rl/vm_scheduling/config.py @@ -1,11 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import numpy as np -import torch -from torch.optim import Adam, SGD, lr_scheduler - -from maro.rl.exploration import MultiLinearExplorationScheduler from maro.simulator import Env @@ -17,9 +12,10 @@ "snapshot_resolution": 1 } -num_pms = Env(**env_conf).business_engine._pm_amount +num_pms = Env(**env_conf).business_engine.pm_amount pm_window_size = 1 num_features = 2 * num_pms * pm_window_size + 4 +state_dim = num_features + num_pms + 1 pm_attributes = ["cpu_cores_capacity", "memory_capacity", "cpu_cores_allocated", "memory_allocated"] # vm_attributes = ["cpu_cores_requirement", "memory_requirement", "lifetime", "remain_time", "total_income"] @@ -46,81 +42,3 @@ test_seed = 1024 algorithm = "ac" # "dqn" or "ac" - -######################################### A2C settings ######################################## -actor_net_conf = { - "input_dim": num_features, - "output_dim": num_pms + 1, # action could be any PM or postponement, hence the plus 1 - "hidden_dims": [64, 32, 32], - "activation": torch.nn.LeakyReLU, - "softmax": True, - "batch_norm": False, - "head": True -} - -critic_net_conf = { - "input_dim": num_features, - "output_dim": 1, - "hidden_dims": [256, 128, 64], - "activation": torch.nn.LeakyReLU, - "softmax": False, - "batch_norm": False, - "head": True -} - -actor_optim_conf = (Adam, {"lr": 0.0001}) -critic_optim_conf = (SGD, {"lr": 0.001}) - -ac_conf = { - "reward_discount": 0.9, - "grad_iters": 100, - "critic_loss_cls": torch.nn.MSELoss, - "critic_loss_coeff": 0.1, - "min_logp": -20, - "max_trajectory_len": 10000, - "get_loss_on_rollout": False -} - -######################################### DQN settings ######################################## -q_net_conf = { - "input_dim": num_features, - "hidden_dims": [64, 128, 256], - "output_dim": num_pms + 1, # action could be any PM or postponement, hence the plus 1 - "activation": torch.nn.LeakyReLU, - "softmax": False, - "batch_norm": False, - "skip_connection": False, - "head": True, - "dropout_p": 0.0 -} - -q_net_optim_conf = (SGD, {"lr": 0.0005}) -q_net_lr_scheduler_conf = (lr_scheduler.CosineAnnealingWarmRestarts, {"T_0": 500, "T_mult": 2}) - - -def masked_eps_greedy(states, actions, num_actions, *, epsilon): - masks = states[:, num_features:] - return np.array([ - action if np.random.random() > epsilon else np.random.choice(np.where(mask == 1)[0]) - for action, mask in zip(actions, masks) - ]) - -dqn_conf = { - "reward_discount": 0.9, - "update_target_every": 5, - "num_epochs": 100, - "soft_update_coeff": 0.1, - "double": False, - "exploration_strategy": (masked_eps_greedy, {"epsilon": 0.4}), - "exploration_scheduling_options": [( - "epsilon", MultiLinearExplorationScheduler, { - "splits": [(100, 0.32)], - "initial_value": 0.4, - "last_ep": 400, - "final_value": 0.0, - } - )], - "replay_memory_capacity": 10000, - "rollout_batch_size": 2560, - "train_batch_size": 256, -} diff --git a/examples/rl/vm_scheduling/env_sampler.py b/examples/rl/vm_scheduling/env_sampler.py index d9e0ff55f..2394ee0f1 100644 --- a/examples/rl/vm_scheduling/env_sampler.py +++ b/examples/rl/vm_scheduling/env_sampler.py @@ -4,25 +4,24 @@ import sys from collections import defaultdict from os.path import dirname, realpath +from typing import Any, Callable, Dict, Optional, Tuple import numpy as np -from maro.rl.learning import AbsEnvSampler +from maro.rl.policy import RLPolicy +from maro.rl.rollout import AbsEnvSampler, CacheElement from maro.simulator import Env -from maro.simulator.scenarios.vm_scheduling import AllocateAction, PostponeAction +from maro.simulator.scenarios.vm_scheduling import AllocateAction, DecisionPayload, PostponeAction -vm_path = dirname(realpath(__file__)) -sys.path.insert(0, vm_path) -from config import ( - algorithm, env_conf, pm_attributes, pm_window_size, reward_shaping_conf, num_features, seed, test_env_conf, +from .config import ( + algorithm, env_conf, num_features, pm_attributes, pm_window_size, reward_shaping_conf, seed, test_env_conf, test_reward_shaping_conf, test_seed ) -from policies import policy_func_dict class VMEnvSampler(AbsEnvSampler): - def __init__(self, get_env, get_policy_func_dict, agent2policy, get_test_env=None): - super().__init__(get_env, get_policy_func_dict, agent2policy, get_test_env=get_test_env) + def __init__(self, get_env, policy_creator, agent2policy, get_test_env=None, device: str = None): + super().__init__(get_env, policy_creator, agent2policy, get_test_env=get_test_env, device=device) self._learn_env.set_seed(seed) self._test_env.set_seed(test_seed) @@ -32,17 +31,19 @@ def __init__(self, get_env, get_policy_func_dict, agent2policy, get_test_env=Non self._pm_state_history = np.zeros((pm_window_size - 1, self.num_pms, 2)) self._legal_pm_mask = None - def get_state(self, tick=None): - pm_state, vm_state = self._get_pm_state(), self._get_vm_state() + def _get_global_and_agent_state( + self, event: DecisionPayload, tick: int = None + ) -> Tuple[Optional[np.ndarray], Dict[Any, np.ndarray]]: + pm_state, vm_state = self._get_pm_state(), self._get_vm_state(event) # get the legal number of PM. legal_pm_mask = np.zeros(self.num_pms + 1) - if len(self.event.valid_pms) <= 0: + if len(event.valid_pms) <= 0: # no pm available legal_pm_mask[self.num_pms] = 1 else: legal_pm_mask[self.num_pms] = 1 remain_cpu_dict = dict() - for pm in self.event.valid_pms: + for pm in event.valid_pms: # If two pms have the same remaining cpu, choose the one with the smaller id if pm_state[-1, pm, 0] not in remain_cpu_dict: remain_cpu_dict[pm_state[-1, pm, 0]] = 1 @@ -51,37 +52,29 @@ def get_state(self, tick=None): legal_pm_mask[pm] = 0 self._legal_pm_mask = legal_pm_mask - return {"AGENT": np.concatenate((pm_state.flatten(), vm_state.flatten(), legal_pm_mask)).astype(np.float32)} + state = np.concatenate((pm_state.flatten(), vm_state.flatten(), legal_pm_mask)).astype(np.float32) + return None, {"AGENT": state} - def get_env_actions(self, action_info): - action_info = action_info["AGENT"] - model_action = action_info["action"] if isinstance(action_info, dict) else action_info - if model_action == self.num_pms: - return PostponeAction(vm_id=self.event.vm_id, postpone_step=1) + def _translate_to_env_action(self, action_dict: Dict[Any, np.ndarray], event: DecisionPayload) -> Dict[Any, object]: + if action_dict["AGENT"] == self.num_pms: + return {"AGENT": PostponeAction(vm_id=event.vm_id, postpone_step=1)} else: - return AllocateAction(vm_id=self.event.vm_id, pm_id=model_action) + return {"AGENT": AllocateAction(vm_id=event.vm_id, pm_id=action_dict["AGENT"][0])} - def get_reward(self, actions, tick): - conf = reward_shaping_conf if self.env == self._learn_env else test_reward_shaping_conf - if isinstance(actions, PostponeAction): # postponement + def _get_reward(self, env_action_dict: Dict[Any, object], event: DecisionPayload, tick: int) -> Dict[Any, float]: + action = env_action_dict["AGENT"] + conf = reward_shaping_conf if self._env == self._learn_env else test_reward_shaping_conf + if isinstance(action, PostponeAction): # postponement if np.sum(self._legal_pm_mask) != 1: reward = -0.1 * conf["alpha"] + 0.0 * conf["beta"] else: reward = 0.0 * conf["alpha"] + 0.0 * conf["beta"] - elif self.event: - vm_unit_price = self.env.business_engine._get_unit_price( - self.event.vm_cpu_cores_requirement, self.event.vm_memory_requirement - ) - reward = ( - 1.0 * conf["alpha"] + conf["beta"] * vm_unit_price * - min(self._durations - self.event.frame_index, self.event.remaining_buffer_time) - ) else: - reward = .0 + reward = self._get_allocation_reward(event, conf["alpha"], conf["beta"]) if event else .0 return {"AGENT": np.float32(reward)} def _get_pm_state(self): - total_pm_info = self.env.snapshot_list["pms"][self.env.frame_index::pm_attributes] + total_pm_info = self._env.snapshot_list["pms"][self._env.frame_index::pm_attributes] total_pm_info = total_pm_info.reshape(self.num_pms, len(pm_attributes)) # normalize the attributes of pms' cpu and memory @@ -101,37 +94,43 @@ def _get_pm_state(self): self._pm_state_history = np.concatenate((self._pm_state_history, total_pm_info), axis=0) return self._pm_state_history[-pm_window_size:, :, :] # (win_size, num_pms, 2) - def _get_vm_state(self): + def _get_vm_state(self, event): return np.array([ - self.event.vm_cpu_cores_requirement / self._max_cpu_capacity, - self.event.vm_memory_requirement / self._max_memory_capacity, - (self._durations - self.env.tick) * 1.0 / 200, # TODO: CHANGE 200 TO SOMETHING CONFIGURABLE - self.env.business_engine._get_unit_price( - self.event.vm_cpu_cores_requirement, self.event.vm_memory_requirement - ) + event.vm_cpu_cores_requirement / self._max_cpu_capacity, + event.vm_memory_requirement / self._max_memory_capacity, + (self._durations - self._env.tick) * 1.0 / 200, # TODO: CHANGE 200 TO SOMETHING CONFIGURABLE + self._env.business_engine._get_unit_price(event.vm_cpu_cores_requirement, event.vm_memory_requirement) ]) - def post_step(self, state, action, env_actions, reward, tick): - self.tracker["env_metric"] = {key: metric for key, metric in self.env.metrics.items() if key != "total_latency"} - self.tracker["env_metric"]["latency_due_to_agent"] = self.env.metrics["total_latency"].due_to_agent - self.tracker["env_metric"]["latency_due_to_resource"] = self.env.metrics["total_latency"].due_to_resource - if "actions_by_core_requirement" not in self.tracker: - self.tracker["actions_by_core_requirement"] = defaultdict(list) - if "action_sequence" not in self.tracker: - self.tracker["action_sequence"] = [] + def _get_allocation_reward(self, event: DecisionPayload, alpha: float, beta: float): + vm_unit_price = self._env.business_engine._get_unit_price( + event.vm_cpu_cores_requirement, event.vm_memory_requirement + ) + return (alpha + beta * vm_unit_price * min(self._durations - event.frame_index, event.remaining_buffer_time)) - if self.event: - mask = state[num_features:] - self.tracker["actions_by_core_requirement"][self.event.vm_cpu_cores_requirement].append([action, mask]) - self.tracker["action_sequence"].append(action["action"] if isinstance(action, dict) else action) + def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]): + self._info["env_metric"] = {k: v for k, v in self._env.metrics.items() if k != "total_latency"} + self._info["env_metric"]["latency_due_to_agent"] = self._env.metrics["total_latency"].due_to_agent + self._info["env_metric"]["latency_due_to_resource"] = self._env.metrics["total_latency"].due_to_resource + if "actions_by_core_requirement" not in self._info: + self._info["actions_by_core_requirement"] = defaultdict(list) + if "action_sequence" not in self._info: + self._info["action_sequence"] = [] + action = cache_element.action_dict["AGENT"] + if cache_element.state: + mask = cache_element.state[num_features:] + self._info["actions_by_core_requirement"][cache_element.event.vm_cpu_cores_requirement].append([action, mask]) + self._info["action_sequence"].append(action) -agent2policy = {"AGENT": algorithm} -def get_env_sampler(): +agent2policy = {"AGENT": f"{algorithm}.policy"} + +def env_sampler_creator(policy_creator: Dict[str, Callable[[str], RLPolicy]]) -> VMEnvSampler: return VMEnvSampler( get_env=lambda: Env(**env_conf), - get_policy_func_dict=policy_func_dict, + policy_creator=policy_creator, agent2policy=agent2policy, - get_test_env=lambda: Env(**test_env_conf) + get_test_env=lambda: Env(**test_env_conf), + device="cpu", ) diff --git a/examples/rl/vm_scheduling/policies.py b/examples/rl/vm_scheduling/policies.py deleted file mode 100644 index 7e0d6d267..000000000 --- a/examples/rl/vm_scheduling/policies.py +++ /dev/null @@ -1,118 +0,0 @@ - -import sys -from os.path import dirname, realpath - -import torch - -from maro.rl.modeling import DiscreteACNet, DiscreteQNet, FullyConnected -from maro.rl.policy import DQN, ActorCritic - -vm_path = dirname(realpath(__file__)) -sys.path.insert(0, vm_path) -from config import ( - ac_conf, actor_net_conf, actor_optim_conf, algorithm, critic_net_conf, critic_optim_conf, dqn_conf, q_net_conf, - num_features, num_pms, q_net_optim_conf -) - - -class MyQNet(DiscreteQNet): - def __init__(self): - super().__init__() - for mdl in self.modules(): - if isinstance(mdl, torch.nn.Linear): - torch.nn.init.xavier_uniform_(mdl.weight, gain=torch.nn.init.calculate_gain('leaky_relu')) - - self.fc = FullyConnected(**q_net_conf) - self.optim = q_net_optim_conf[0](self.fc.parameters(), **q_net_optim_conf[1]) - - @property - def input_dim(self): - return num_features + num_pms + 1 - - @property - def num_actions(self): - return q_net_conf["output_dim"] - - def forward(self, states): - masks = states[:, num_features:] - q_for_all_actions = self.fc(states[:, :num_features]) - return q_for_all_actions + (masks - 1) * 1e8 - - def step(self, loss): - self.optim.zero_grad() - loss.backward() - self.optim.step() - - def get_gradients(self, loss): - self.optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad): - for name, param in self.named_parameters(): - param.grad = grad[name] - - self.optim.step() - - def get_state(self): - return {"network": self.state_dict(), "optim": self.optim.state_dict()} - - def set_state(self, state): - self.load_state_dict(state["network"]) - self.optim.load_state_dict(state["optim"]) - - -class MyACNet(DiscreteACNet): - def __init__(self): - super().__init__() - self.actor = FullyConnected(**actor_net_conf) - self.critic = FullyConnected(**critic_net_conf) - self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1]) - self.critic_optim = critic_optim_conf[0](self.critic.parameters(), **critic_optim_conf[1]) - - @property - def input_dim(self): - return num_features + num_pms + 1 - - def forward(self, states, actor: bool = True, critic: bool = True): - features, masks = states[:, :num_features], states[:, num_features:] - masks += 1e-8 # this is to prevent zero probability and infinite logP. - return (self.actor(features) * masks if actor else None), (self.critic(features) if critic else None) - - def step(self, loss): - self.actor_optim.zero_grad() - self.critic_optim.zero_grad() - loss.backward() - self.actor_optim.step() - self.critic_optim.step() - - def get_gradients(self, loss): - self.actor_optim.zero_grad() - self.critic_optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad): - for name, param in self.named_parameters(): - param.grad = grad[name] - - self.actor_optim.step() - self.critic_optim.step() - - def get_state(self): - return { - "network": self.state_dict(), - "actor_optim": self.actor_optim.state_dict(), - "critic_optim": self.critic_optim.state_dict() - } - - def set_state(self, state): - self.load_state_dict(state["network"]) - self.actor_optim.load_state_dict(state["actor_optim"]) - self.critic_optim.load_state_dict(state["critic_optim"]) - - -if algorithm == "dqn": - policy_func_dict = {"dqn": lambda name: DQN(name, MyQNet(), **dqn_conf)} -else: - policy_func_dict = {"ac": lambda name: ActorCritic(name, MyACNet(), **ac_conf)} diff --git a/examples/rl/vm_scheduling/policy_trainer.py b/examples/rl/vm_scheduling/policy_trainer.py new file mode 100644 index 000000000..b4a2d01fa --- /dev/null +++ b/examples/rl/vm_scheduling/policy_trainer.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from functools import partial + +from .config import algorithm, state_dim, num_features, num_pms + +action_num = num_pms + 1 # action could be any PM or postponement, hence the plus 1 + +if algorithm == "ac": + from .algorithms.ac import get_ac, get_policy + policy_creator = {"ac.policy": partial(get_policy, state_dim, action_num, num_features)} + trainer_creator = {"ac": partial(get_ac, state_dim, num_features)} +elif algorithm == "dqn": + from .algorithms.dqn import get_dqn, get_policy + policy_creator = {"dqn.policy": partial(get_policy, state_dim, action_num, num_features)} + trainer_creator = {"dqn": get_dqn} +else: + raise ValueError(f"Unsupported algorithm: {algorithm}") diff --git a/examples/rl/workflows/config.yml b/examples/rl/workflows/config.yml deleted file mode 100644 index 7760c502d..000000000 --- a/examples/rl/workflows/config.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -job_name: sc -scenario: supply_chain -mode: sync -num_episodes: 3 -eval_schedule: 3 -num_steps: -1 -max_lag: 0 -# If true, the roll-out experiences will be distributed amongst roll-out workers / actors -# in round-robin fashion based on agent index. Otherwise, every roll-out worker / actor will -# store the roll-out experiences for all agents whose experiences need to be stored. -rollout_experience_distribution: true -sync: - rollout_group: rollout - rollout_mode: multi-node # single-process, multi-process, multi-node - num_rollout_workers: 6 - min_finished_workers: 4 - max_extra_recv_tries: 2 - extra_recv_timeout: 100 -async: - group: async - num_actors: 3 -policy_manager: - train_group: policy-manager - train_mode: multi-node # single-process, multi-process, multi-node - num_trainers: 2 -redis: - host: maro-redis - port: 6379 diff --git a/examples/rl/workflows/synchronous/learner.py b/examples/rl/workflows/synchronous/learner.py deleted file mode 100644 index 78d4f0b4c..000000000 --- a/examples/rl/workflows/synchronous/learner.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import sys -from os.path import dirname, realpath - -from maro.rl.learning.synchronous import ( - Learner, LocalRolloutManager, MultiNodeRolloutManager, MultiProcessRolloutManager -) - -workflow_dir = dirname(dirname((realpath(__file__)))) -if workflow_dir not in sys.path: - sys.path.insert(0, workflow_dir) - -from agent_wrapper import get_agent_wrapper -from policy_manager.policy_manager import get_policy_manager -from general import config, post_collect, post_evaluate, get_env_wrapper, log_dir - - -def get_rollout_manager(): - rollout_mode = config["sync"]["rollout_mode"] - if rollout_mode == "single-process": - return LocalRolloutManager( - get_env_wrapper(), - get_agent_wrapper(), - num_steps=config["num_steps"], - post_collect=post_collect, - post_evaluate=post_evaluate, - log_dir=log_dir - ) - if rollout_mode == "multi-process": - return MultiProcessRolloutManager( - config["sync"]["num_rollout_workers"], - get_env_wrapper, - get_agent_wrapper, - num_steps=config["num_steps"], - post_collect=post_collect, - post_evaluate=post_evaluate, - log_dir=log_dir, - ) - if rollout_mode == "multi-node": - return MultiNodeRolloutManager( - config["sync"]["rollout_group"], - config["sync"]["num_rollout_workers"], - num_steps=config["num_steps"], - max_lag=config["max_lag"], - min_finished_workers=config["sync"]["min_finished_workers"], - max_extra_recv_tries=config["sync"]["max_extra_recv_tries"], - extra_recv_timeout=config["sync"]["extra_recv_timeout"], - post_collect=post_collect, - post_evaluate=post_evaluate, - proxy_kwargs={"redis_address": (config["redis"]["host"], config["redis"]["port"])} - ) - - raise ValueError( - f"Unsupported roll-out mode: {rollout_mode}. Supported modes: single-process, multi-process, multi-node" - ) - - -if __name__ == "__main__": - learner = Learner( - policy_manager=get_policy_manager(), - rollout_manager=get_rollout_manager(), - num_episodes=config["num_episodes"], - eval_schedule=config["eval_schedule"], - log_dir=log_dir - ) - learner.run() diff --git a/examples/vm_scheduling/offline_lp/launcher.py b/examples/vm_scheduling/offline_lp/launcher.py index 5fd264c73..7255d42f4 100644 --- a/examples/vm_scheduling/offline_lp/launcher.py +++ b/examples/vm_scheduling/offline_lp/launcher.py @@ -22,14 +22,11 @@ config = convert_dottable(raw_config) LOG_PATH = os.path.join(FILE_PATH, "log", config.experiment_name) -if not os.path.exists(LOG_PATH): - os.makedirs(LOG_PATH) -simulation_logger = Logger(tag="simulation", format_=LogFormat.none, dump_folder=LOG_PATH, dump_mode="w", auto_timestamp=False) -ilp_logger = Logger(tag="ilp", format_=LogFormat.none, dump_folder=LOG_PATH, dump_mode="w", auto_timestamp=False) +simulation_logger = Logger(tag="simulation", format_=LogFormat.none, dump_folder=LOG_PATH, dump_mode="w") +ilp_logger = Logger(tag="ilp", format_=LogFormat.none, dump_folder=LOG_PATH, dump_mode="w") if __name__ == "__main__": start_time = timeit.default_timer() - env = Env( scenario=config.env.scenario, topology=config.env.topology, diff --git a/maro/cli/k8s/utils/k8s.py b/maro/cli/k8s/utils/k8s.py new file mode 100644 index 000000000..8af7fd33d --- /dev/null +++ b/maro/cli/k8s/utils/k8s.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import kubernetes +from kubernetes import client, config + + +def load_config(): + config.load_kube_config() + + +def create_namespace(namespace: str): + body = client.V1Namespace() + body.metadata = client.V1ObjectMeta(name=namespace) + try: + client.CoreV1Api().create_namespace(body) + except kubernetes.client.exceptions.ApiException: + pass + + +def create_deployment(conf: dict, namespace: str): + client.AppsV1Api().create_namespaced_deployment(namespace, conf) + + +def create_service(conf: dict, namespace: str): + client.CoreV1Api().create_namespaced_service(namespace, conf) + + +def create_job(conf: dict, namespace: str): + client.BatchV1Api().create_namespaced_job(namespace, conf) + + +def create_secret(name: str, data: dict, namespace: str): + client.CoreV1Api().create_namespaced_secret( + body=client.V1Secret(metadata=client.V1ObjectMeta(name=name), data=data), + namespace=namespace + ) + + +def delete_job(namespace: str): + client.BatchV1Api().delete_collection_namespaced_job(namespace) + client.CoreV1Api().delete_namespace(namespace) + + +def describe_job(namespace: str): + client.CoreV1Api().read_namespace(namespace) diff --git a/maro/cli/k8s/utils/k8s_manifest_generator.py b/maro/cli/k8s/utils/k8s_manifest_generator.py new file mode 100644 index 000000000..672d1ba1f --- /dev/null +++ b/maro/cli/k8s/utils/k8s_manifest_generator.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List + +from maro.cli.utils.common import format_env_vars + + +def get_job_manifest(agent_pool_name: str, component_name: str, container_spec: dict, volumes: List[dict]): + return { + "metadata": {"name": component_name}, + "spec": { + "template": { + "spec": { + "nodeSelector": {"agentpool": agent_pool_name}, + "restartPolicy": "Never", + "volumes": volumes, + "containers": [container_spec] + } + } + } + } + + +def get_azurefile_volume_spec(name: str, share_name: str, secret_name: str): + return { + "name": name, + "azureFile": { + "secretName": secret_name, + "shareName": share_name, + "readOnly": False + } + } + + +def get_container_spec(image_name: str, component_name: str, env: dict, volumes): + common_container_spec = { + "image": image_name, + "imagePullPolicy": "Always", + "volumeMounts": [{"name": vol["name"], "mountPath": f"/{vol['name']}"} for vol in volumes] + } + return { + **common_container_spec, + **{ + "name": component_name, + "command": ["python3", f"/maro/maro/rl/workflows/{component_name.split('-')[0]}.py"], + "env": format_env_vars(env, mode="k8s") + } + } + + +def get_redis_deployment_manifest(host: str, port: int): + return { + "metadata": { + "name": host, + "labels": {"app": "redis"} + }, + "spec": { + "selector": { + "matchLabels": {"app": "redis"} + }, + "replicas": 1, + "template": { + "metadata": { + "labels": {"app": "redis"} + }, + "spec": { + "containers": [ + { + "name": "master", + "image": "redis:6", + "ports": [{"containerPort": port}] + } + ] + } + } + } + } + + +def get_redis_service_manifest(host: str, port: int): + return { + "metadata": { + "name": host, + "labels": {"app": "redis"} + }, + "spec": { + "ports": [{"port": port, "targetPort": port}], + "selector": {"app": "redis"} + } + } + + +def get_cross_namespace_service_access_manifest( + service_name: str, target_service_name: str, target_service_namespace: str, target_service_port: int +): + return { + "metadata": { + "name": service_name, + }, + "spec": { + "type": "ExternalName", + "externalName": f"{target_service_name}.{target_service_namespace}.svc.cluster.local", + "ports": [{"port": target_service_port}] + } + } diff --git a/maro/cli/process/__init__.py b/maro/cli/local/__init__.py similarity index 100% rename from maro/cli/process/__init__.py rename to maro/cli/local/__init__.py diff --git a/maro/cli/local/commands.py b/maro/cli/local/commands.py new file mode 100644 index 000000000..5f99a162c --- /dev/null +++ b/maro/cli/local/commands.py @@ -0,0 +1,256 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import json +import os +import shutil +import subprocess +import sys +import time +from os import makedirs +from os.path import abspath, dirname, exists, expanduser, join + +import redis +import yaml + +from maro.cli.utils.common import close_by_pid, show_log +from maro.rl.workflows.config import ConfigParser +from maro.utils.logger import CliLogger +from maro.utils.utils import LOCAL_MARO_ROOT + +from .utils import ( + JobStatus, RedisHashKey, start_redis, start_rl_job, start_rl_job_with_docker_compose, stop_redis, + stop_rl_job_with_docker_compose +) + +# metadata +LOCAL_ROOT = expanduser("~/.maro/local") +SESSION_STATE_PATH = join(LOCAL_ROOT, "session.json") +DOCKERFILE_PATH = join(LOCAL_MARO_ROOT, "docker_files", "dev.df") +DOCKER_IMAGE_NAME = "maro-local" +DOCKER_NETWORK = "MAROLOCAL" + +# display +NO_JOB_MANAGER_MSG = """No job manager found. Run "maro local init" to start the job manager first.""" +NO_JOB_MSG = """No job named {} found. Run "maro local job ls" to view existing jobs.""" +JOB_LS_TEMPLATE = "{JOB:12}{STATUS:15}{STARTED:20}" + +logger = CliLogger(name="MARO-LOCAL") + + +# helper functions +def get_redis_conn(port=None): + if port is None: + try: + with open(SESSION_STATE_PATH, "r") as fp: + port = json.load(fp)["port"] + except FileNotFoundError: + logger.error(NO_JOB_MANAGER_MSG) + return + + try: + redis_conn = redis.Redis(host="localhost", port=port) + redis_conn.ping() + return redis_conn + except redis.exceptions.ConnectionError: + logger.error(NO_JOB_MANAGER_MSG) + + +# Functions executed on CLI commands +def run(conf_path: str, containerize: bool = False, **kwargs): + # Load job configuration file + parser = ConfigParser(conf_path) + env_by_component = parser.as_env(containerize=containerize) + if containerize: + path_mapping = parser.get_path_mapping(containerize=True) + try: + start_rl_job_with_docker_compose( + parser.config, LOCAL_MARO_ROOT, DOCKERFILE_PATH, DOCKER_IMAGE_NAME, env_by_component, path_mapping + ) + except KeyboardInterrupt: + stop_rl_job_with_docker_compose(parser.config["job"]) + else: + try: + start_rl_job(parser.as_env(), LOCAL_MARO_ROOT) + except KeyboardInterrupt: + sys.exit(1) + + +def init( + port: int = 19999, + max_running: int = 3, + query_every: int = 5, + timeout: int = 3, + containerize: bool = False, + **kwargs +): + if exists(SESSION_STATE_PATH): + with open(SESSION_STATE_PATH, "r") as fp: + session_state = json.load(fp) + logger.warning( + f"Local job manager is already running at port {session_state['port']}. " + f"Run 'maro local job add/rm' to add / remove jobs." + ) + return + + start_redis(port) + + # Start job manager + command = ["python", join(dirname(abspath(__file__)), 'job_manager.py')] + job_manager = subprocess.Popen( + command, + env={ + "PYTHONPATH": LOCAL_MARO_ROOT, + "MAX_RUNNING": str(max_running), + "QUERY_EVERY": str(query_every), + "SIGTERM_TIMEOUT": str(timeout), + "CONTAINERIZE": str(containerize), + "REDIS_PORT": str(port), + "LOCAL_MARO_ROOT": LOCAL_MARO_ROOT, + "DOCKER_IMAGE_NAME": DOCKER_IMAGE_NAME, + "DOCKERFILE_PATH": DOCKERFILE_PATH + } + ) + + # Dump environment setting + makedirs(LOCAL_ROOT, exist_ok=True) + with open(SESSION_STATE_PATH, "w") as fp: + json.dump({"port": port, "job_manager_pid": job_manager.pid, "containerized": containerize}, fp) + + # Create log folder + logger.info("Local job manager started") + + +def exit(**kwargs): + try: + with open(SESSION_STATE_PATH, "r") as fp: + session_state = json.load(fp) + except FileNotFoundError: + logger.error(NO_JOB_MANAGER_MSG) + return + + redis_conn = get_redis_conn() + + # Mark all jobs as REMOVED and let the job manager terminate them properly. + job_details = redis_conn.hgetall(RedisHashKey.JOB_DETAILS) + if job_details: + for job_name, details in job_details.items(): + details = json.loads(details) + details["status"] = JobStatus.REMOVED + redis_conn.hset(RedisHashKey.JOB_DETAILS, job_name, json.dumps(details)) + logger.info(f"Gracefully terminating job {job_name.decode()}") + + # Stop job manager + close_by_pid(int(session_state["job_manager_pid"])) + + # Stop Redis + stop_redis(session_state["port"]) + + # Remove dump folder. + shutil.rmtree(LOCAL_ROOT, True) + + logger.info("Local job manager terminated.") + + +def add_job(conf_path: str, **kwargs): + redis_conn = get_redis_conn() + if not redis_conn: + return + + # Load job configuration file + with open(conf_path, "r") as fr: + conf = yaml.safe_load(fr) + + job_name = conf["job"] + if redis_conn.hexists(RedisHashKey.JOB_DETAILS, job_name): + logger.error(f"A job named '{job_name}' has already been added.") + return + + # Push job config to redis + redis_conn.hset(RedisHashKey.JOB_CONF, job_name, json.dumps(conf)) + details = { + "status": JobStatus.PENDING, + "added": time.time() + } + redis_conn.hset(RedisHashKey.JOB_DETAILS, job_name, json.dumps(details)) + + +def remove_jobs(job_names, **kwargs): + redis_conn = get_redis_conn() + if not redis_conn: + return + + for job_name in job_names: + details = redis_conn.hget(RedisHashKey.JOB_DETAILS, job_name) + if not details: + logger.error(f"No job named '{job_name}' has been scheduled or started.") + else: + details = json.loads(details) + details["status"] = JobStatus.REMOVED + redis_conn.hset(RedisHashKey.JOB_DETAILS, job_name, json.dumps(details)) + logger.info(f"Removed job {job_name}") + + +def describe_job(job_name, **kwargs): + redis_conn = get_redis_conn() + if not redis_conn: + return + + details = redis_conn.hget(RedisHashKey.JOB_DETAILS, job_name) + if not details: + logger.error(NO_JOB_MSG.format(job_name)) + return + + details = json.loads(details) + err = "error_message" in details + if err: + err_msg = details["error_message"].split('\n') + del details["error_message"] + + logger.info(details) + if err: + for line in err_msg: + logger.info(line) + + +def get_job_logs(job_name: str, tail: int = -1, **kwargs): + redis_conn = get_redis_conn() + if not redis_conn.hexists(RedisHashKey.JOB_CONF, job_name): + logger.error(NO_JOB_MSG.format(job_name)) + return + + conf = json.loads(redis_conn.hget(RedisHashKey.JOB_CONF, job_name)) + show_log(conf["log_path"], tail=tail) + + +def list_jobs(**kwargs): + redis_conn = get_redis_conn() + if not redis_conn: + return + + def get_time_diff_string(time_diff): + time_diff = int(time_diff) + days = time_diff // (3600 * 24) + if days: + return f"{days} days" + + hours = time_diff // 3600 + if hours: + return f"{hours} hours" + + minutes = time_diff // 60 + if minutes: + return f"{minutes} minutes" + + return f"{time_diff} seconds" + + # Header + logger.info(JOB_LS_TEMPLATE.format(JOB="JOB", STATUS="STATUS", STARTED="STARTED")) + for job_name, details in redis_conn.hgetall(RedisHashKey.JOB_DETAILS).items(): + job_name = job_name.decode() + details = json.loads(details) + if "start_time" in details: + time_diff = f"{get_time_diff_string(time.time() - details['start_time'])} ago" + logger.info(JOB_LS_TEMPLATE.format(JOB=job_name, STATUS=details["status"], STARTED=time_diff)) + else: + logger.info(JOB_LS_TEMPLATE.format(JOB=job_name, STATUS=details["status"], STARTED=JobStatus.PENDING)) diff --git a/maro/cli/local/job_manager.py b/maro/cli/local/job_manager.py new file mode 100644 index 000000000..0377cf585 --- /dev/null +++ b/maro/cli/local/job_manager.py @@ -0,0 +1,97 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import json +import os +import threading +import time + +import redis + +from maro.cli.local.utils import JobStatus, RedisHashKey, poll, start_rl_job, start_rl_job_in_containers, term +from maro.cli.utils.docker import build_image, image_exists +from maro.rl.workflows.config import ConfigParser + +if __name__ == "__main__": + redis_port = int(os.getenv("REDIS_PORT", default=19999)) + redis_conn = redis.Redis(host="localhost", port=redis_port) + started, max_running = {}, int(os.getenv("MAX_RUNNING", default=1)) + query_every = int(os.getenv("QUERY_EVERY", default=5)) + sigterm_timeout = int(os.getenv("SIGTERM_TIMEOUT", default=3)) + containerize = os.getenv("CONTAINERIZE", default="False") == "True" + local_maro_root = os.getenv("LOCAL_MARO_ROOT") + docker_file_path = os.getenv("DOCKERFILE_PATH") + docker_image_name = os.getenv("DOCKER_IMAGE_NAME") + + # thread to monitor a job + def monitor(job_name): + removed, error, err_out, running = False, False, None, started[job_name] + while running: + error, err_out, running = poll(running) + # check if the job has been marked as REMOVED before termination + details = json.loads(redis_conn.hget(RedisHashKey.JOB_DETAILS, job_name)) + if details["status"] == JobStatus.REMOVED: + removed = True + break + + if error: + break + + if removed: + term(started[job_name], job_name, timeout=sigterm_timeout) + redis_conn.hdel(RedisHashKey.JOB_DETAILS, job_name) + redis_conn.hdel(RedisHashKey.JOB_CONF, job_name) + return + + if error: + term(started[job_name], job_name, timeout=sigterm_timeout) + details["status"] = JobStatus.ERROR + details["error_message"] = err_out + redis_conn.hset(RedisHashKey.JOB_DETAILS, job_name, json.dumps(details)) + else: # all job processes terminated normally + details["status"] = JobStatus.FINISHED + redis_conn.hset(RedisHashKey.JOB_DETAILS, job_name, json.dumps(details)) + + # Continue to monitor if the job is marked as REMOVED + while json.loads(redis_conn.hget(RedisHashKey.JOB_DETAILS, job_name))["status"] != JobStatus.REMOVED: + time.sleep(query_every) + + term(started[job_name], job_name, timeout=sigterm_timeout) + redis_conn.hdel(RedisHashKey.JOB_DETAILS, job_name) + redis_conn.hdel(RedisHashKey.JOB_CONF, job_name) + + while True: + # check for pending jobs + job_details = redis_conn.hgetall(RedisHashKey.JOB_DETAILS) + if job_details: + num_running, pending = 0, [] + for job_name, details in job_details.items(): + job_name, details = job_name.decode(), json.loads(details) + if details["status"] == JobStatus.RUNNING: + num_running += 1 + elif details["status"] == JobStatus.PENDING: + pending.append((job_name, json.loads(redis_conn.hget(RedisHashKey.JOB_CONF, job_name)))) + + for job_name, conf in pending[:max(0, max_running - num_running)]: + if containerize and not image_exists(docker_image_name): + redis_conn.hset( + RedisHashKey.JOB_DETAILS, job_name, json.dumps({"status": JobStatus.IMAGE_BUILDING}) + ) + build_image(local_maro_root, docker_file_path, docker_image_name) + + parser = ConfigParser(conf) + env_by_component = parser.as_env(containerize=containerize) + if containerize: + path_mapping = parser.get_path_mapping(containerize=True) + started[job_name] = start_rl_job_in_containers( + conf, docker_image_name, env_by_component, path_mapping + ) + details["containers"] = started[job_name] + else: + started[job_name] = start_rl_job(env_by_component, local_maro_root, background=True) + details["pids"] = [proc.pid for proc in started[job_name]] + details = {"status": JobStatus.RUNNING, "start_time": time.time()} + redis_conn.hset(RedisHashKey.JOB_DETAILS, job_name, json.dumps(details)) + threading.Thread(target=monitor, args=(job_name,)).start() # start job monitoring thread + + time.sleep(query_every) diff --git a/maro/cli/local/utils.py b/maro/cli/local/utils.py new file mode 100644 index 000000000..577302156 --- /dev/null +++ b/maro/cli/local/utils.py @@ -0,0 +1,190 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import subprocess +from copy import deepcopy +from typing import Dict, List + +import docker +import yaml + +from maro.cli.utils.common import format_env_vars + + +class RedisHashKey: + """Record Redis elements name, and only for maro process""" + JOB_CONF = "job_conf" + JOB_DETAILS = "job_details" + + +class JobStatus: + PENDING = "pending" + IMAGE_BUILDING = "image_building" + RUNNING = "running" + ERROR = "error" + REMOVED = "removed" + FINISHED = "finished" + + +def start_redis(port: int): + subprocess.Popen(["redis-server", "--port", str(port)], stdout=subprocess.DEVNULL) + + +def stop_redis(port: int): + subprocess.Popen(["redis-cli", "-p", str(port), "shutdown"], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + + +def extract_error_msg_from_docker_log(container: docker.models.containers.Container): + logs = container.logs().decode().splitlines() + for i, log in enumerate(logs): + if "Traceback (most recent call last):" in log: + return "\n".join(logs[i:]) + + return logs + + +def check_proc_status(proc): + if isinstance(proc, subprocess.Popen): + if proc.poll() is None: + return True, 0, None + _, err_out = proc.communicate() + return False, proc.returncode, err_out + else: + client = docker.from_env() + container_state = client.api.inspect_container(proc.id)["State"] + return container_state["Running"], container_state["ExitCode"], extract_error_msg_from_docker_log(proc) + + +def poll(procs): + error, running = False, [] + for proc in procs: + is_running, exit_code, err_out = check_proc_status(proc) + if is_running: + running.append(proc) + elif exit_code: + error = True + break + + return error, err_out, running + + +def term(procs, job_name: str, timeout: int = 3): + if isinstance(procs[0], subprocess.Popen): + for proc in procs: + if proc.poll() is None: + try: + proc.terminate() + proc.wait(timeout=timeout) + except subprocess.TimeoutExpired: + proc.kill() + else: + for proc in procs: + try: + proc.stop(timeout=timeout) + proc.remove() + except: + pass + + client = docker.from_env() + try: + job_network = client.networks.get(job_name) + job_network.remove() + except: + pass + + +def exec(cmd: str, env: dict, debug: bool = False) -> subprocess.Popen: + stream = None if debug else subprocess.PIPE + return subprocess.Popen( + cmd.split(), env={**os.environ.copy(), **env}, stdout=stream, stderr=stream, encoding="utf8" + ) + + +def start_rl_job(env_by_component: Dict[str, dict], maro_root: str, background: bool = False) -> List[subprocess.Popen]: + def get_local_script_path(component: str): + return os.path.join(maro_root, "maro", "rl", "workflows", f"{component.split('-')[0]}.py") + + procs = [ + exec( + f"python {get_local_script_path(component)}", + format_env_vars({**env, "PYTHONPATH": maro_root}, mode="proc"), + debug=not background + ) + for component, env in env_by_component.items() + ] + if not background: + for proc in procs: + proc.communicate() + + return procs + + +def start_rl_job_in_containers( + conf: dict, image_name: str, env_by_component: Dict[str, dict], path_mapping: Dict[str, str] +) -> None: + job_name = conf["job"] + client, containers = docker.from_env(), [] + is_distributed_training = conf["training"]["mode"] != "simple" + is_distributed_rollout = ( + "parallelism" in conf["rollout"] and + max(conf["rollout"]["parallelism"]["sampling"], conf["rollout"]["parallelism"].get("eval", 1)) > 1 + ) + if is_distributed_training or is_distributed_rollout: + # create the exclusive network for the job + client.networks.create(job_name, driver="bridge") + + for component, env in env_by_component.items(): + container_name = f"{job_name}.{component}" + # volume mounts for scenario folder, policy loading, checkpointing and logging + container = client.containers.run( + image_name, + command=f"python3 /maro/maro/rl/workflows/{component.split('-')[0]}.py", + detach=True, + name=container_name, + environment=env, + volumes=[f"{src}:{dst}" for src, dst in path_mapping.items()], + network=job_name + ) + + containers.append(container) + + return containers + + +def get_docker_compose_yml_path() -> str: + return os.path.join(os.getcwd(), "docker-compose.yml") + + +def start_rl_job_with_docker_compose( + conf: dict, context: str, dockerfile_path: str, image_name: str, env_by_component: Dict[str, dict], + path_mapping: Dict[str, str] +) -> None: + common_spec = { + "build": {"context": context, "dockerfile": dockerfile_path}, + "image": image_name, + "volumes": [f"{src}:{dst}" for src, dst in path_mapping.items()] + } + job = conf["job"] + manifest = {"version": "3.9"} + manifest["services"] = { + component: { + **deepcopy(common_spec), + **{ + "container_name": f"{job}.{component}", + "command": f"python3 /maro/maro/rl/workflows/{component.split('-')[0]}.py", + "environment": format_env_vars(env, mode="docker-compose") + } + } + for component, env in env_by_component.items() + } + + with open(get_docker_compose_yml_path(), "w") as fp: + yaml.safe_dump(manifest, fp) + + subprocess.run(["docker-compose", "--project-name", job, "up", "--remove-orphans"]) + + +def stop_rl_job_with_docker_compose(job_name: str): + subprocess.run(["docker-compose", "--project-name", job_name, "down"]) + os.remove(get_docker_compose_yml_path()) diff --git a/maro/cli/maro.py b/maro/cli/maro.py index d4b8aabd6..c35e9e1ea 100644 --- a/maro/cli/maro.py +++ b/maro/cli/maro.py @@ -99,13 +99,13 @@ def main(): parser_inspector.set_defaults(func=_help_func(parser=parser_inspector)) load_parser_inspector(parser_inspector, global_parser) - # maro process - parser_process = subparsers.add_parser( - "process", - help="Run application by mulit-process to simulate distributed mode." + # maro local + parser_local = subparsers.add_parser( + "local", + help="Run jobs locally." ) - parser_process.set_defaults(func=_help_func(parser=parser_process)) - load_parser_process(prev_parser=parser_process, global_parser=global_parser) + parser_local.set_defaults(func=_help_func(parser=parser_local)) + load_parser_local(prev_parser=parser_local, global_parser=global_parser) # maro project parser_project = subparsers.add_parser( @@ -151,152 +151,127 @@ def main(): logger.error_red(f"{e.__class__.__name__}: {e.get_message()}") -def load_parser_process(prev_parser: ArgumentParser, global_parser: ArgumentParser) -> None: +def load_parser_local(prev_parser: ArgumentParser, global_parser: ArgumentParser) -> None: subparsers = prev_parser.add_subparsers() - # maro process create - from maro.cli.process.create import create - parser_setup = subparsers.add_parser( - "create", - help="Create local process environment.", + # maro local run + from maro.cli.local.commands import run + parser = subparsers.add_parser( + "run", + help="Run a job in debug mode.", examples=CliExamples.MARO_PROCESS_SETUP, parents=[global_parser] ) - parser_setup.add_argument( - 'deployment_path', - help='Path of the local process setting deployment.', - nargs='?', - default=None) - parser_setup.set_defaults(func=create) + parser.add_argument("conf_path", help='Path of the job deployment') + parser.add_argument("-c", "--containerize", action="store_true", help="Whether to run jobs in containers") + parser.add_argument("-p", "--port", type=int, default=20000, help="") + parser.set_defaults(func=run) - # maro process delete - from maro.cli.process.delete import delete - parser_setup = subparsers.add_parser( - "delete", - help="Delete the local process environment. Including closing agents and maro Redis.", + # maro local init + from maro.cli.local.commands import init + parser = subparsers.add_parser( + "init", + help="Initialize local job manager.", + examples=CliExamples.MARO_PROCESS_SETUP, parents=[global_parser] ) - parser_setup.set_defaults(func=delete) + parser.add_argument( + "-p", "--port", type=int, default=19999, + help="Port on local machine to launch the Redis server at. Defaults to 19999." + ) + parser.add_argument( + "-m", "--max-running", type=int, default=3, + help="Maximum number of jobs to allow running at the same time. Defaults to 3." + ) + parser.add_argument( + "-q", "--query-every", type=int, default=5, + help="Number of seconds to wait between queries to the Redis server for pending or removed jobs. Defaults to 5." + ) + parser.add_argument( + "-t", "--timeout", type=int, default=3, + help=""" + Number of seconds to wait after sending SIGTERM to a process. If the process does not terminate + during this time, the process will be force-killed through SIGKILL. Defaults to 3. + """ + ) + parser.add_argument("-c", "--containerize", action="store_true", help="Whether to run jobs in containers") + parser.set_defaults(func=init) - # maro process job - parser_job = subparsers.add_parser( + # maro local exit + from maro.cli.local.commands import exit + parser = subparsers.add_parser( + "exit", + help="Terminate the local job manager", + parents=[global_parser] + ) + parser.set_defaults(func=exit) + + # maro local job + parser = subparsers.add_parser( "job", help="Manage jobs", parents=[global_parser] ) - parser_job.set_defaults(func=_help_func(parser=parser_job)) - parser_job_subparsers = parser_job.add_subparsers() + parser.set_defaults(func=_help_func(parser=parser)) + job_subparsers = parser.add_subparsers() - # maro process job start - from maro.cli.process.job import start_job - parser_job_start = parser_job_subparsers.add_parser( - 'start', - help='Start a training job', + # maro local job add + from maro.cli.local.commands import add_job + job_add_parser = job_subparsers.add_parser( + "add", + help="Start an RL job", examples=CliExamples.MARO_PROCESS_JOB_START, parents=[global_parser] ) - parser_job_start.add_argument( - 'deployment_path', help='Path of the job deployment') - parser_job_start.set_defaults(func=start_job) + job_add_parser.add_argument("conf_path", help='Path of the job deployment') + job_add_parser.set_defaults(func=add_job) - # maro process job stop - from maro.cli.process.job import stop_job - parser_job_stop = parser_job_subparsers.add_parser( - 'stop', - help='Stop a training job', + # maro local job rm + from maro.cli.local.commands import remove_jobs + job_stop_parser = job_subparsers.add_parser( + "rm", + help='Stop an RL job', examples=CliExamples.MARO_PROCESS_JOB_STOP, parents=[global_parser] ) - parser_job_stop.add_argument( - 'job_name', help='Name of the job') - parser_job_stop.set_defaults(func=stop_job) + job_stop_parser.add_argument('job_names', help="Job names", nargs="*") + job_stop_parser.set_defaults(func=remove_jobs) - # maro process job delete - from maro.cli.process.job import delete_job - parser_job_delete = parser_job_subparsers.add_parser( - 'delete', - help='delete a stopped job', - examples=CliExamples.MARO_PROCESS_JOB_DELETE, + # maro local job describe + from maro.cli.local.commands import describe_job + job_stop_parser = job_subparsers.add_parser( + "describe", + help="Get the status of an RL job and the error information if the job fails due to some error", + examples=CliExamples.MARO_PROCESS_JOB_STOP, parents=[global_parser] ) - parser_job_delete.add_argument( - 'job_name', help='Name of the job or the schedule') - parser_job_delete.set_defaults(func=delete_job) + job_stop_parser.add_argument('job_name', help='Job name') + job_stop_parser.set_defaults(func=describe_job) - # maro process job list - from maro.cli.process.job import list_jobs - parser_job_list = parser_job_subparsers.add_parser( - 'list', + # maro local job ls + from maro.cli.local.commands import list_jobs + job_list_parser = job_subparsers.add_parser( + "ls", help='List all jobs', examples=CliExamples.MARO_PROCESS_JOB_LIST, parents=[global_parser] ) - parser_job_list.set_defaults(func=list_jobs) + job_list_parser.set_defaults(func=list_jobs) - # maro process job logs - from maro.cli.process.job import get_job_logs - parser_job_logs = parser_job_subparsers.add_parser( - 'logs', - help='Get logs of the job', + # maro local job logs + from maro.cli.local.commands import get_job_logs + job_logs_parser = job_subparsers.add_parser( + "logs", + help="Get job logs", examples=CliExamples.MARO_PROCESS_JOB_LOGS, parents=[global_parser] ) - parser_job_logs.add_argument( - 'job_name', help='Name of the job') - parser_job_logs.set_defaults(func=get_job_logs) - - # maro process schedule - parser_schedule = subparsers.add_parser( - 'schedule', - help='Manage schedules', - parents=[global_parser] - ) - parser_schedule.set_defaults(func=_help_func(parser=parser_schedule)) - parser_schedule_subparsers = parser_schedule.add_subparsers() - - # maro process schedule start - from maro.cli.process.schedule import start_schedule - parser_schedule_start = parser_schedule_subparsers.add_parser( - 'start', - help='Start a schedule', - examples=CliExamples.MARO_PROCESS_SCHEDULE_START, - parents=[global_parser] - ) - parser_schedule_start.add_argument( - 'deployment_path', help='Path of the schedule deployment') - parser_schedule_start.set_defaults(func=start_schedule) - - # maro process schedule stop - from maro.cli.process.schedule import stop_schedule - parser_schedule_stop = parser_schedule_subparsers.add_parser( - 'stop', - help='Stop a schedule', - examples=CliExamples.MARO_PROCESS_SCHEDULE_STOP, - parents=[global_parser] - ) - parser_schedule_stop.add_argument( - 'schedule_name', help='Name of the schedule') - parser_schedule_stop.set_defaults(func=stop_schedule) - - # maro process template - from maro.cli.process.template import template - parser_template = subparsers.add_parser( - "template", - help="Get deployment templates", - examples=CliExamples.MARO_PROCESS_TEMPLATE, - parents=[global_parser] + job_logs_parser.add_argument("job_name", help="job name") + job_logs_parser.add_argument( + "-n", "--tail", type=int, default=-1, + help="Number of lines to show from the end of the given job's logs" ) - parser_template.add_argument( - "--setting_deploy", - action="store_true", - help="Get environment setting templates" - ) - parser_template.add_argument( - "export_path", - default="./", - nargs='?', - help="Path of the export directory") - parser_template.set_defaults(func=template) + job_logs_parser.set_defaults(func=get_job_logs) def load_parser_grass(prev_parser: ArgumentParser, global_parser: ArgumentParser) -> None: diff --git a/maro/cli/process/agent/job_agent.py b/maro/cli/process/agent/job_agent.py deleted file mode 100644 index 57414e279..000000000 --- a/maro/cli/process/agent/job_agent.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import json -import multiprocessing as mp -import os -import subprocess -import time - -import psutil -import redis - -from maro.cli.grass.lib.services.utils.params import JobStatus -from maro.cli.process.utils.details import close_by_pid, get_child_pid -from maro.cli.utils.details_reader import DetailsReader -from maro.cli.utils.params import LocalPaths, ProcessRedisName - - -class PendingJobAgent(mp.Process): - def __init__(self, cluster_detail: dict, redis_connection, check_interval: int = 60): - super().__init__() - self.cluster_detail = cluster_detail - self.redis_connection = redis_connection - self.check_interval = check_interval - - def run(self): - while True: - self._check_pending_ticket() - time.sleep(self.check_interval) - - def _check_pending_ticket(self): - # Check pending job ticket - pending_jobs = self.redis_connection.lrange(ProcessRedisName.PENDING_JOB_TICKETS, 0, -1) - running_jobs_length = len(JobTrackingAgent.get_running_jobs( - self.redis_connection.hgetall(ProcessRedisName.JOB_DETAILS) - )) - parallel_level = self.cluster_detail["parallel_level"] - - for job_name in pending_jobs: - job_detail = json.loads(self.redis_connection.hget(ProcessRedisName.JOB_DETAILS, job_name)) - # Start pending job only if current running job's number less than parallel level. - if int(parallel_level) > running_jobs_length: - self._start_job(job_detail) - self.redis_connection.lrem(ProcessRedisName.PENDING_JOB_TICKETS, 0, job_name) - running_jobs_length += 1 - - def _start_job(self, job_details: dict): - command_pid_list = [] - for component_type, command_info in job_details["components"].items(): - component_number = command_info["num"] - component_command = f"JOB_NAME={job_details['name']} " + command_info["command"] - for number in range(component_number): - job_local_path = os.path.expanduser(f"{LocalPaths.MARO_PROCESS}/{job_details['name']}") - if not os.path.exists(job_local_path): - os.makedirs(job_local_path) - - with open(f"{job_local_path}/{component_type}_{number}.log", "w") as log_file: - proc = subprocess.Popen(component_command, shell=True, stdout=log_file) - command_pid = get_child_pid(proc.pid) - if not command_pid: - command_pid_list.append(proc.pid) - else: - command_pid_list.append(command_pid) - - job_details["status"] = JobStatus.RUNNING - job_details["pid_list"] = command_pid_list - self.redis_connection.hset(ProcessRedisName.JOB_DETAILS, job_details["name"], json.dumps(job_details)) - - -class JobTrackingAgent(mp.Process): - def __init__(self, cluster_detail: dict, redis_connection, check_interval: int = 60): - super().__init__() - self.cluster_detail = cluster_detail - self.redis_connection = redis_connection - self.check_interval = check_interval - self._shutdown_count = 0 - self._countdown = cluster_detail["agent_countdown"] - - def run(self): - while True: - self._check_job_status() - time.sleep(self.check_interval) - keep_alive = self.cluster_detail["keep_agent_alive"] - if not keep_alive: - self._close_agents() - - def _check_job_status(self): - running_jobs = self.get_running_jobs(self.redis_connection.hgetall(ProcessRedisName.JOB_DETAILS)) - - for running_job_name, running_job_detail in running_jobs.items(): - # Check pid status - still_alive = False - for pid in running_job_detail["pid_list"]: - if psutil.pid_exists(pid): - still_alive = True - - # Update if no pid exists - if not still_alive: - running_job_detail["status"] = JobStatus.FINISH - del running_job_detail["pid_list"] - self.redis_connection.hset( - ProcessRedisName.JOB_DETAILS, - running_job_name, - json.dumps(running_job_detail) - ) - - @staticmethod - def get_running_jobs(job_details: dict): - running_jobs = {} - - for job_name, job_detail in job_details.items(): - job_detail = json.loads(job_detail) - if job_detail["status"] == JobStatus.RUNNING: - running_jobs[job_name.decode()] = job_detail - - return running_jobs - - def _close_agents(self): - if ( - not len( - JobTrackingAgent.get_running_jobs(self.redis_connection.hgetall(ProcessRedisName.JOB_DETAILS)) - ) and - not self.redis_connection.llen(ProcessRedisName.PENDING_JOB_TICKETS) - ): - self._shutdown_count += 1 - else: - self._shutdown_count = 0 - - if self._shutdown_count >= self._countdown: - agent_pid = int(self.redis_connection.hget(ProcessRedisName.SETTING, "agent_pid")) - - # close agent - close_by_pid(pid=agent_pid, recursive=True) - - # Set agent status to 0 - self.redis_connection.hset(ProcessRedisName.SETTING, "agent_status", 0) - - -class KilledJobAgent(mp.Process): - def __init__(self, cluster_detail: dict, redis_connection, check_interval: int = 60): - super().__init__() - self.cluster_detail = cluster_detail - self.redis_connection = redis_connection - self.check_interval = check_interval - - def run(self): - while True: - self._check_killed_tickets() - time.sleep(self.check_interval) - - def _check_killed_tickets(self): - # Check pending job ticket - killed_job_names = self.redis_connection.lrange(ProcessRedisName.KILLED_JOB_TICKETS, 0, -1) - - for job_name in killed_job_names: - job_detail = json.loads(self.redis_connection.hget(ProcessRedisName.JOB_DETAILS, job_name)) - if job_detail["status"] == JobStatus.RUNNING: - close_by_pid(pid=job_detail["pid_list"], recursive=False) - del job_detail["pid_list"] - elif job_detail["status"] == JobStatus.PENDING: - self.redis_connection.lrem(ProcessRedisName.PENDING_JOB_TICKETS, 0, job_name) - elif job_detail["status"] == JobStatus.FINISH: - continue - - job_detail["status"] = JobStatus.KILLED - self.redis_connection.hset(ProcessRedisName.JOB_DETAILS, job_name, json.dumps(job_detail)) - self.redis_connection.lrem(ProcessRedisName.KILLED_JOB_TICKETS, 0, job_name) - - -class MasterAgent: - def __init__(self): - self.cluster_detail = DetailsReader.load_cluster_details("process") - self.check_interval = self.cluster_detail["check_interval"] - self.redis_connection = redis.Redis( - host=self.cluster_detail["redis_info"]["host"], - port=self.cluster_detail["redis_info"]["port"] - ) - self.redis_connection.hset(ProcessRedisName.SETTING, "agent_pid", os.getpid()) - - def start(self) -> None: - """Start agents.""" - pending_job_agent = PendingJobAgent( - cluster_detail=self.cluster_detail, - redis_connection=self.redis_connection, - check_interval=self.check_interval - ) - pending_job_agent.start() - - killed_job_agent = KilledJobAgent( - cluster_detail=self.cluster_detail, - redis_connection=self.redis_connection, - check_interval=self.check_interval - ) - killed_job_agent.start() - - job_tracking_agent = JobTrackingAgent( - cluster_detail=self.cluster_detail, - redis_connection=self.redis_connection, - check_interval=self.check_interval - ) - job_tracking_agent.start() - - -if __name__ == "__main__": - master_agent = MasterAgent() - master_agent.start() diff --git a/maro/cli/process/agent/resource_agent.py b/maro/cli/process/agent/resource_agent.py deleted file mode 100644 index 66c5d201a..000000000 --- a/maro/cli/process/agent/resource_agent.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import json -import multiprocessing as mp -import os -import time - -import redis - -from maro.cli.utils.params import LocalParams -from maro.cli.utils.resource_executor import ResourceInfo -from maro.utils.exception.cli_exception import BadRequestError - - -class ResourceTrackingAgent(mp.Process): - def __init__( - self, - check_interval: int = 30 - ): - super().__init__() - self._redis_connection = redis.Redis(host="localhost", port=LocalParams.RESOURCE_REDIS_PORT) - try: - if self._redis_connection.hexists(LocalParams.RESOURCE_INFO, "check_interval"): - self._check_interval = int(self._redis_connection.hget(LocalParams.RESOURCE_INFO, "check_interval")) - else: - self._check_interval = check_interval - except Exception: - raise BadRequestError( - "Failure to connect to Resource Redis." - "Please make sure at least one cluster running." - ) - - self._set_resource_info() - - def _set_resource_info(self): - # Set resource agent pid. - self._redis_connection.hset( - LocalParams.RESOURCE_INFO, - "agent_pid", - os.getpid() - ) - - # Set resource agent check interval. - self._redis_connection.hset( - LocalParams.RESOURCE_INFO, - "check_interval", - json.dumps(self._check_interval) - ) - - # Push static resource information into Redis. - resource = ResourceInfo.get_static_info() - self._redis_connection.hset( - LocalParams.RESOURCE_INFO, - "resource", - json.dumps(resource) - ) - - def run(self) -> None: - """Start tracking node status and updating details. - - Returns: - None. - """ - while True: - start_time = time.time() - self.push_local_resource_usage() - time.sleep(max(self._check_interval - (time.time() - start_time), 0)) - - self._check_interval = int(self._redis_connection.hget(LocalParams.RESOURCE_INFO, "check_interval")) - - def push_local_resource_usage(self): - resource_usage = ResourceInfo.get_dynamic_info(self._check_interval) - - self._redis_connection.rpush( - LocalParams.CPU_USAGE, - json.dumps(resource_usage["cpu_usage_per_core"]) - ) - - self._redis_connection.rpush( - LocalParams.MEMORY_USAGE, - json.dumps(resource_usage["memory_usage"]) - ) - - self._redis_connection.rpush( - LocalParams.GPU_USAGE, - json.dumps(resource_usage["gpu_memory_usage"]) - ) - - -if __name__ == "__main__": - resource_agent = ResourceTrackingAgent() - resource_agent.start() diff --git a/maro/cli/process/create.py b/maro/cli/process/create.py deleted file mode 100644 index 467b2db20..000000000 --- a/maro/cli/process/create.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import yaml - -from maro.cli.process.executor import ProcessExecutor -from maro.cli.process.utils.default_param import process_setting - - -def create(deployment_path: str, **kwargs): - if deployment_path is not None: - with open(deployment_path, "r") as fr: - create_deployment = yaml.safe_load(fr) - else: - create_deployment = process_setting - - executor = ProcessExecutor(create_deployment) - executor.create() diff --git a/maro/cli/process/delete.py b/maro/cli/process/delete.py deleted file mode 100644 index 8ede6a3ae..000000000 --- a/maro/cli/process/delete.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from maro.cli.process.executor import ProcessExecutor - - -def delete(**kwargs): - executor = ProcessExecutor() - executor.delete() diff --git a/maro/cli/process/deployment/process_job_deployment.yml b/maro/cli/process/deployment/process_job_deployment.yml deleted file mode 100644 index 7720e9b40..000000000 --- a/maro/cli/process/deployment/process_job_deployment.yml +++ /dev/null @@ -1,10 +0,0 @@ -mode: process -name: MyJobName # str: name of the training job - -components: # component config - actor: - num: 5 # int: number of this component - command: "python /target/path/run_actor.py" # str: command to be executed - learner: - num: 1 - command: "python /target/path/run_learner.py" diff --git a/maro/cli/process/deployment/process_schedule_deployment.yml b/maro/cli/process/deployment/process_schedule_deployment.yml deleted file mode 100644 index d1d7769c2..000000000 --- a/maro/cli/process/deployment/process_schedule_deployment.yml +++ /dev/null @@ -1,16 +0,0 @@ -mode: process -name: MyScheduleName # str: name of the training schedule - -job_names: # list: names of the training job - - MyJobName2 - - MyJobName3 - - MyJobName4 - - MyJobName5 - -components: # component config - actor: - num: 5 # int: number of this component - command: "python /target/path/run_actor.py" # str: command to be executed - learner: - num: 1 - command: "python /target/path/run_learner.py" diff --git a/maro/cli/process/deployment/process_setting_deployment.yml b/maro/cli/process/deployment/process_setting_deployment.yml deleted file mode 100644 index 3f9168350..000000000 --- a/maro/cli/process/deployment/process_setting_deployment.yml +++ /dev/null @@ -1,8 +0,0 @@ -redis_info: - host: "localhost" - port: 19999 -redis_mode: MARO # one of MARO, customized. customized Redis won't be exited after maro process clear. -parallel_level: 1 # Represented the maximum number of running jobs in the same times. -keep_agent_alive: True # If True represented the agents won't exit until the environment delete; otherwise, False. -agent_countdown: 5 # After agent_countdown times checks, still no jobs will close agents. Available only if keep_agent_alive is 0. -check_interval: 60 # The time interval (seconds) of agents check with Redis diff --git a/maro/cli/process/executor.py b/maro/cli/process/executor.py deleted file mode 100644 index b3823fc77..000000000 --- a/maro/cli/process/executor.py +++ /dev/null @@ -1,248 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import copy -import json -import os -import shutil -import subprocess - -import redis -import yaml - -from maro.cli.grass.lib.services.utils.params import JobStatus -from maro.cli.process.utils.details import close_by_pid, get_redis_pid_by_port -from maro.cli.utils.abs_visible_executor import AbsVisibleExecutor -from maro.cli.utils.details_reader import DetailsReader -from maro.cli.utils.details_writer import DetailsWriter -from maro.cli.utils.params import GlobalPaths, LocalPaths, ProcessRedisName -from maro.cli.utils.resource_executor import LocalResourceExecutor -from maro.utils.logger import CliLogger - -logger = CliLogger(name=__name__) - - -class ProcessExecutor(AbsVisibleExecutor): - def __init__(self, details: dict = None): - self.details = details if details else \ - DetailsReader.load_cluster_details("process") - - # Connection with Redis - redis_port = self.details["redis_info"]["port"] - self._redis_connection = redis.Redis(host="localhost", port=redis_port) - try: - self._redis_connection.ping() - except Exception: - redis_process = subprocess.Popen( - ["redis-server", "--port", str(redis_port), "--daemonize yes"] - ) - redis_process.wait(timeout=2) - - # Connection with Resource Redis - self._resource_redis = LocalResourceExecutor() - - def create(self): - logger.info("Starting MARO Multi-Process Mode.") - if os.path.isdir(f"{GlobalPaths.ABS_MARO_CLUSTERS}/process"): - logger.warning("Process mode has been created.") - - # Get environment setting - DetailsWriter.save_cluster_details( - cluster_name="process", - cluster_details=self.details - ) - - # Start agents - command = f"python {LocalPaths.MARO_PROCESS_AGENT}" - _ = subprocess.Popen(command, shell=True) - self._redis_connection.hset(ProcessRedisName.SETTING, "agent_status", 1) - - # Add connection to resource Redis. - self._resource_redis.add_cluster() - - logger.info(f"MARO process mode setting: {self.details}") - - def delete(self): - process_setting = self._redis_connection.hgetall(ProcessRedisName.SETTING) - process_setting = { - key.decode(): json.loads(value) for key, value in process_setting.items() - } - - # Stop running jobs - jobs = self._redis_connection.hgetall(ProcessRedisName.JOB_DETAILS) - if jobs: - for job_name, job_detail in jobs.items(): - job_detail = json.loads(job_detail) - if job_detail["status"] == JobStatus.RUNNING: - close_by_pid(pid=job_detail["pid_list"], recursive=False) - logger.info(f"Stop running job {job_name.decode()}.") - - # Stop agents - agent_status = int(process_setting["agent_status"]) - if agent_status: - agent_pid = int(process_setting["agent_pid"]) - close_by_pid(pid=agent_pid, recursive=True) - logger.info("Close agents.") - else: - logger.info("Agents is already closed.") - - # Stop Redis or clear Redis - redis_mode = self.details["redis_mode"] - if redis_mode == "MARO": - redis_pid = get_redis_pid_by_port(self.details["redis_info"]["port"]) - close_by_pid(pid=redis_pid, recursive=False) - else: - self._redis_clear() - - # Rm connection from resource redis. - self._resource_redis.sub_cluster() - - logger.info("Redis cleared.") - - # Remove local process file. - shutil.rmtree(f"{GlobalPaths.ABS_MARO_CLUSTERS}/process", True) - logger.info("Process mode has been deleted.") - - def _redis_clear(self): - redis_keys = self._redis_connection.keys("process:*") - for key in redis_keys: - self._redis_connection.delete(key) - - def start_job(self, deployment_path: str): - # Load start_job_deployment - with open(deployment_path, "r") as fr: - start_job_deployment = yaml.safe_load(fr) - - job_name = start_job_deployment["name"] - start_job_deployment["status"] = JobStatus.PENDING - # Push job details to redis - self._redis_connection.hset( - ProcessRedisName.JOB_DETAILS, - job_name, - json.dumps(start_job_deployment) - ) - - self._push_pending_job(job_name) - - def _push_pending_job(self, job_name: str): - # Push job name to pending_job_tickets - self._redis_connection.lpush( - ProcessRedisName.PENDING_JOB_TICKETS, - job_name - ) - logger.info(f"Sending {job_name} into pending job tickets.") - - def stop_job(self, job_name: str): - if not self._redis_connection.hexists(ProcessRedisName.JOB_DETAILS, job_name): - logger.error(f"No such job '{job_name}' in Redis.") - return - - # push job_name into kill_job_tickets - self._redis_connection.lpush( - ProcessRedisName.KILLED_JOB_TICKETS, - job_name - ) - logger.info(f"Sending {job_name} into killed job tickets.") - - def delete_job(self, job_name: str): - # Stop job for running and pending job. - self.stop_job(job_name) - - # Rm job details in Redis - self._redis_connection.hdel(ProcessRedisName.JOB_DETAILS, job_name) - - # Rm job's log folder - job_folder = os.path.expanduser(f"{LocalPaths.MARO_PROCESS}/{job_name}") - shutil.rmtree(job_folder, True) - logger.info(f"Remove local temporary log folder {job_folder}.") - - def get_job_logs(self, job_name): - source_path = os.path.expanduser(f"{LocalPaths.MARO_PROCESS}/{job_name}") - if not os.path.exists(source_path): - logger.error(f"Cannot find the logs of {job_name}.") - - destination = os.path.join(os.getcwd(), job_name) - if os.path.exists(destination): - shutil.rmtree(destination) - shutil.copytree(source_path, destination) - logger.info(f"Dump logs in path: {destination}.") - - def list_job(self): - # Get all jobs - jobs = self._redis_connection.hgetall(ProcessRedisName.JOB_DETAILS) - for job_name, job_detail in jobs.items(): - job_name = job_name.decode() - job_detail = json.loads(job_detail) - - logger.info(job_detail) - - def start_schedule(self, deployment_path: str): - with open(deployment_path, "r") as fr: - schedule_detail = yaml.safe_load(fr) - - # push schedule details to Redis - self._redis_connection.hset( - ProcessRedisName.JOB_DETAILS, - schedule_detail["name"], - json.dumps(schedule_detail) - ) - - job_list = schedule_detail["job_names"] - # switch schedule details into job details - job_detail = copy.deepcopy(schedule_detail) - del job_detail["job_names"] - - for job_name in job_list: - job_detail["name"] = job_name - - # Push job details to redis - self._redis_connection.hset( - ProcessRedisName.JOB_DETAILS, - job_name, - json.dumps(job_detail) - ) - - self._push_pending_job(job_name) - - def stop_schedule(self, schedule_name: str): - if self._redis_connection.hexists(ProcessRedisName.JOB_DETAILS, schedule_name): - schedule_details = json.loads(self._redis_connection.hget(ProcessRedisName.JOB_DETAILS, schedule_name)) - else: - logger.error(f"Cannot find {schedule_name} in Redis. Please check schedule name.") - return - - if "job_names" not in schedule_details.keys(): - logger.error(f"'{schedule_name}' is not a schedule.") - return - - job_list = schedule_details["job_names"] - - for job_name in job_list: - self.stop_job(job_name) - - def get_job_details(self): - jobs = self._redis_connection.hgetall(ProcessRedisName.JOB_DETAILS) - for job_name, job_details_str in jobs.items(): - jobs[job_name] = json.loads(job_details_str) - - return list(jobs.values()) - - def get_job_queue(self): - pending_job_queue = self._redis_connection.lrange( - ProcessRedisName.PENDING_JOB_TICKETS, - 0, -1 - ) - killed_job_queue = self._redis_connection.lrange( - ProcessRedisName.KILLED_JOB_TICKETS, - 0, -1 - ) - return { - "pending_jobs": pending_job_queue, - "killed_jobs": killed_job_queue - } - - def get_resource(self): - return self._resource_redis.get_local_resource() - - def get_resource_usage(self, previous_length: int): - return self._resource_redis.get_local_resource_usage(previous_length) diff --git a/maro/cli/process/job.py b/maro/cli/process/job.py deleted file mode 100644 index 9f478cbb9..000000000 --- a/maro/cli/process/job.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.cli.process.executor import ProcessExecutor - - -def start_job(deployment_path: str, **kwargs): - executor = ProcessExecutor() - executor.start_job(deployment_path=deployment_path) - - -def stop_job(job_name: str, **kwargs): - executor = ProcessExecutor() - executor.stop_job(job_name=job_name) - - -def delete_job(job_name: str, **kwargs): - executor = ProcessExecutor() - executor.delete_job(job_name=job_name) - - -def list_jobs(**kwargs): - executor = ProcessExecutor() - executor.list_job() - - -def get_job_logs(job_name: str, **kwargs): - executor = ProcessExecutor() - executor.get_job_logs(job_name=job_name) diff --git a/maro/cli/process/schedule.py b/maro/cli/process/schedule.py deleted file mode 100644 index 0c7ca3188..000000000 --- a/maro/cli/process/schedule.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -from maro.cli.process.executor import ProcessExecutor - - -def start_schedule(deployment_path: str, **kwargs): - executor = ProcessExecutor() - executor.start_schedule(deployment_path=deployment_path) - - -def stop_schedule(schedule_name: str, **kwargs): - executor = ProcessExecutor() - executor.stop_schedule(schedule_name=schedule_name) diff --git a/maro/cli/process/template.py b/maro/cli/process/template.py deleted file mode 100644 index f1f8bb615..000000000 --- a/maro/cli/process/template.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import shutil - -from maro.cli.utils.params import LocalPaths - - -def template(setting_deploy, export_path, **kwargs): - deploy_files = os.listdir(LocalPaths.MARO_PROCESS_DEPLOYMENT) - if not setting_deploy: - deploy_files.remove("process_setting_deployment.yml") - export_path = os.path.abspath(export_path) - for file_name in deploy_files: - if os.path.isfile(f"{LocalPaths.MARO_PROCESS_DEPLOYMENT}/{file_name}"): - shutil.copy(f"{LocalPaths.MARO_PROCESS_DEPLOYMENT}/{file_name}", export_path) diff --git a/maro/cli/process/utils/default_param.py b/maro/cli/process/utils/default_param.py deleted file mode 100644 index 41001f4d8..000000000 --- a/maro/cli/process/utils/default_param.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -process_setting = { - "redis_info": { - "host": "localhost", - "port": 19999 - }, - "redis_mode": "MARO", # one of MARO, customized. customized Redis won't exit after maro process clear. - "parallel_level": 1, - "keep_agent_alive": 1, # If 0 (False), agents will exit after 5 minutes of no pending jobs and running jobs. - "check_interval": 60, # seconds - "agent_countdown": 5 # how many times to shutdown agents about finding no job in Redis. -} diff --git a/maro/cli/process/utils/details.py b/maro/cli/process/utils/details.py deleted file mode 100644 index a95756946..000000000 --- a/maro/cli/process/utils/details.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import signal -import subprocess -from typing import Union - -import psutil - - -def close_by_pid(pid: Union[int, list], recursive: bool = False): - if isinstance(pid, int): - if not psutil.pid_exists(pid): - return - - if recursive: - current_process = psutil.Process(pid) - children_process = current_process.children(recursive=False) - # May launch by JobTrackingAgent which is child process, so need close parent process first. - current_process.kill() - for child_process in children_process: - child_process.kill() - else: - os.kill(pid, signal.SIGKILL) - else: - for p in pid: - if psutil.pid_exists(p): - os.kill(p, signal.SIGKILL) - - -def get_child_pid(parent_pid): - command = f"ps -o pid --ppid {parent_pid} --noheaders" - get_children_pid_process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) - children_pids = get_children_pid_process.stdout.read() - get_children_pid_process.wait(timeout=2) - - # Convert into list or int - try: - children_pids = int(children_pids) - except ValueError: - children_pids = children_pids.decode().split("\n") - children_pids = [int(pid) for pid in children_pids[:-1]] - - return children_pids - - -def get_redis_pid_by_port(port: int): - get_redis_pid_command = f"pidof 'redis-server *:{port}'" - get_redis_pid_process = subprocess.Popen(get_redis_pid_command, shell=True, stdout=subprocess.PIPE) - redis_pid = int(get_redis_pid_process.stdout.read()) - get_redis_pid_process.wait() - - return redis_pid diff --git a/maro/cli/utils/azure/acr.py b/maro/cli/utils/azure/acr.py new file mode 100644 index 000000000..bb5da0215 --- /dev/null +++ b/maro/cli/utils/azure/acr.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import json + +from maro.cli.utils.subprocess import Subprocess + + +def login_acr(acr_name: str) -> None: + command = f"az acr login --name {acr_name}" + _ = Subprocess.run(command=command) + + +def list_acr_repositories(acr_name: str) -> list: + command = f"az acr repository list -n {acr_name}" + return_str = Subprocess.run(command=command) + return json.loads(return_str) diff --git a/maro/cli/utils/azure/aks.py b/maro/cli/utils/azure/aks.py new file mode 100644 index 000000000..44632b8cd --- /dev/null +++ b/maro/cli/utils/azure/aks.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import subprocess + +from azure.identity import DefaultAzureCredential +from azure.mgmt.authorization import AuthorizationManagementClient +from azure.mgmt.containerservice import ContainerServiceClient + +from maro.cli.utils.subprocess import Subprocess + + +def get_container_service_client(subscription: str): + return ContainerServiceClient(DefaultAzureCredential(), subscription) + + +def get_authorization_client(subscription: str): + return AuthorizationManagementClient() + + +def load_aks_context(resource_group: str, aks_name: str) -> None: + command = f"az aks get-credentials -g {resource_group} --name {aks_name}" + _ = Subprocess.run(command=command) + + +def get_aks(subscription: str, resource_group: str, aks_name: str) -> dict: + container_service_client = get_container_service_client(subscription) + return container_service_client.managed_clusters.get(resource_group, aks_name) + + +def attach_acr(resource_group: str, aks_name: str, acr_name: str) -> None: + subprocess.run(f"az aks update -g {resource_group} -n {aks_name} --attach-acr {acr_name}".split()) + + +def add_nodepool(resource_group: str, aks_name: str, nodepool_name: str, node_count: int, node_size: str) -> None: + command = ( + f"az aks nodepool add " + f"-g {resource_group} " + f"--cluster-name {aks_name} " + f"--name {nodepool_name} " + f"--node-count {node_count} " + f"--node-vm-size {node_size}" + ) + _ = Subprocess.run(command=command) + + +def scale_nodepool(resource_group: str, aks_name: str, nodepool_name: str, node_count: int) -> None: + command = ( + f"az aks nodepool scale " + f"-g {resource_group} " + f"--cluster-name {aks_name} " + f"--name {nodepool_name} " + f"--node-count {node_count}" + ) + _ = Subprocess.run(command=command) diff --git a/maro/cli/utils/azure/deployment.py b/maro/cli/utils/azure/deployment.py new file mode 100644 index 000000000..5fd5c06f6 --- /dev/null +++ b/maro/cli/utils/azure/deployment.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .general import get_resource_client + + +def format_resource_params(params): + return {k: {"value": v} for k, v in params.items()} + + +def create_deployment( + subscription: str, + resource_group: str, + deployment_name: str, + template: dict, + params: dict, + sync: bool = True +) -> None: + params = format_resource_params(params) + resource_client = get_resource_client(subscription) + deployment_params = {"mode": "Incremental", "template": template, "parameters": params} + result = resource_client.deployments.begin_create_or_update( + resource_group, deployment_name, {"properties": deployment_params} + ) + if sync: + result.result() + + +def delete_deployment(subscription: str, resource_group: str, deployment_name: str) -> None: + resource_client = get_resource_client(subscription) + resource_client.deployments.begin_delete(resource_group, deployment_name) diff --git a/maro/cli/utils/azure/general.py b/maro/cli/utils/azure/general.py new file mode 100644 index 000000000..9e499acbb --- /dev/null +++ b/maro/cli/utils/azure/general.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import json +import os +import subprocess + +from azure.identity import DefaultAzureCredential +from azure.mgmt.resource import ResourceManagementClient + +from maro.cli.utils.subprocess import Subprocess + + +def set_subscription(subscription: str) -> None: + command = f"az account set --subscription {subscription}" + _ = Subprocess.run(command=command) + + +def get_version() -> dict: + command = "az version" + return_str = Subprocess.run(command=command) + return json.loads(return_str) + + +def get_resource_client(subscription: str): + return ResourceManagementClient(DefaultAzureCredential(), subscription) + + +def set_env_credentials(dump_path: str, service_principal_name: str): + os.makedirs(dump_path, exist_ok=True) + service_principal_file_path = os.path.join(dump_path, f"{service_principal_name}.json") + if not os.path.exists(service_principal_file_path): + with open(service_principal_file_path, 'w') as fp: + subprocess.run( + f"az ad sp create-for-rbac --name {service_principal_name} --sdk-auth --role contributor".split(), + stdout=fp + ) + + with open(service_principal_file_path, 'r') as fp: + service_principal = json.load(fp) + + os.environ["AZURE_TENANT_ID"] = service_principal["tenantId"] + os.environ["AZURE_CLIENT_ID"] = service_principal["clientId"] + os.environ["AZURE_CLIENT_SECRET"] = service_principal["clientSecret"] + os.environ["AZURE_SUBSCRIPTION_ID"] = service_principal["subscriptionId"] + + +def connect_to_aks(resource_group: str, aks: str): + subprocess.run(f"az aks get-credentials --resource-group {resource_group} --name {aks}".split()) + + +def get_acr_push_permissions(service_principal_id: str, acr: str): + acr_id = json.loads( + subprocess.run(f"az acr show --name {acr} --query id".split(), stdout=subprocess.PIPE).stdout + ) + subprocess.run( + f"az role assignment create --assignee {service_principal_id} --scope {acr_id} --role acrpush".split() + ) + subprocess.run(f"az acr login --name {acr}".split()) diff --git a/maro/cli/utils/azure/resource_group.py b/maro/cli/utils/azure/resource_group.py new file mode 100644 index 000000000..8af9d76ba --- /dev/null +++ b/maro/cli/utils/azure/resource_group.py @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import json + +from maro.cli.utils.subprocess import Subprocess +from maro.utils.exception.cli_exception import CommandExecutionError + +from .general import get_resource_client + + +def get_resource_group(resource_group: str) -> dict: + command = f"az group show --name {resource_group}" + try: + return_str = Subprocess.run(command=command) + return json.loads(return_str) + except CommandExecutionError: + return {} + + +def delete_resource_group(resource_group: str) -> None: + command = f"az group delete --yes --name {resource_group}" + _ = Subprocess.run(command=command) + + +# Chained Azure resource group operations +def create_resource_group(subscription: str, resource_group: str, location: str): + """Create the resource group if it does not exist. + + Args: + subscription (str): Azure subscription name. + resource group (str): Resource group name. + location (str): Reousrce group location. + + Returns: + None. + """ + resource_client = get_resource_client(subscription) + return resource_client.resource_groups.create_or_update(resource_group, {"location": location}) + + +def delete_resource_group_under_subscription(subscription: str, resource_group: str): + resource_client = get_resource_client(subscription) + return resource_client.resource_groups.begin_delete(resource_group) diff --git a/maro/cli/utils/azure/resources.py b/maro/cli/utils/azure/resources.py new file mode 100644 index 000000000..d8d025bb7 --- /dev/null +++ b/maro/cli/utils/azure/resources.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import json + +from maro.cli.utils.subprocess import Subprocess + + +def list_resources(resource_group: str) -> list: + command = f"az resource list -g {resource_group}" + return_str = Subprocess.run(command=command) + return json.loads(return_str) + + +def delete_resources(resource_ids: list) -> None: + command = f"az resource delete --ids {' '.join(resource_ids)}" + _ = Subprocess.run(command=command) + + +def cleanup(cluster_name: str, resource_group: str) -> None: + # Get resource list + resource_list = list_resources(resource_group) + + # Filter resources + deletable_ids = [] + for resource in resource_list: + if resource["name"].startswith(cluster_name): + deletable_ids.append(resource["id"]) + + # Delete resources + if deletable_ids: + delete_resources(resource_ids=deletable_ids) diff --git a/maro/cli/utils/azure/storage.py b/maro/cli/utils/azure/storage.py new file mode 100644 index 000000000..3dcdb911f --- /dev/null +++ b/maro/cli/utils/azure/storage.py @@ -0,0 +1,97 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import datetime +import json +import os +from typing import Union + +from azure.core.exceptions import ResourceExistsError +from azure.storage.fileshare import ShareClient, ShareDirectoryClient + +from maro.cli.utils.subprocess import Subprocess + + +def get_storage_account_keys(resource_group: str, storage_account_name: str) -> dict: + command = f"az storage account keys list -g {resource_group} --account-name {storage_account_name}" + return_str = Subprocess.run(command=command) + return json.loads(return_str) + + +def get_storage_account_sas( + account_name: str, + services: str = "bqtf", + resource_types: str = "sco", + permissions: str = "rwdlacup", + expiry: str = (datetime.datetime.utcnow() + datetime.timedelta(days=365)).strftime("%Y-%m-%dT%H:%M:%S") + "Z" +) -> str: + command = ( + f"az storage account generate-sas --account-name {account_name} --services {services} " + f"--resource-types {resource_types} --permissions {permissions} --expiry {expiry}" + ) + sas_str = Subprocess.run(command=command).strip("\n").replace('"', "") + # logger.debug(sas_str) + return sas_str + + +def get_connection_string(storage_account_name: str) -> str: + """Get the connection string for a storage account. + + Args: + storage_account_name: The storage account name. + + Returns: + str: Connection string. + """ + command = f"az storage account show-connection-string --name {storage_account_name}" + return_str = Subprocess.run(command=command) + return json.loads(return_str)["connectionString"] + + +def get_fileshare(storage_account_name: str, fileshare_name: str): + connection_string = get_connection_string(storage_account_name) + share = ShareClient.from_connection_string(connection_string, fileshare_name) + try: + share.create_share() + except ResourceExistsError: + pass + + return share + + +def get_directory(share: Union[ShareClient, ShareDirectoryClient], name: str): + if isinstance(share, ShareClient): + directory = share.get_directory_client(directory_path=name) + try: + directory.create_directory() + except ResourceExistsError: + pass + + return directory + elif isinstance(share, ShareDirectoryClient): + try: + return share.create_subdirectory(name) + except ResourceExistsError: + return share.get_subdirectory_client(name) + + +def upload_to_fileshare(share: Union[ShareClient, ShareDirectoryClient], source_path: str, name: str = None): + if os.path.isdir(source_path): + if not name: + name = os.path.basename(source_path) + directory = get_directory(share, name) + for file in os.listdir(source_path): + upload_to_fileshare(directory, os.path.join(source_path, file)) + else: + with open(source_path, "rb") as fp: + share.upload_file(file_name=os.path.basename(source_path), data=fp) + + +def download_from_fileshare(share: ShareDirectoryClient, file_name: str, local_path: str): + file = share.get_file_client(file_name=file_name) + with open(local_path, "wb") as fp: + fp.write(file.download_file().readall()) + + +def delete_directory(share: Union[ShareClient, ShareDirectoryClient], name: str, recursive: bool = True): + share.delete_directory(directory_name=name) diff --git a/maro/cli/utils/azure/vm.py b/maro/cli/utils/azure/vm.py new file mode 100644 index 000000000..f7b12ff8c --- /dev/null +++ b/maro/cli/utils/azure/vm.py @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import json + +from maro.cli.utils.subprocess import Subprocess + + +def list_ip_addresses(resource_group: str, vm_name: str) -> list: + command = f"az vm list-ip-addresses -g {resource_group} --name {vm_name}" + return_str = Subprocess.run(command=command) + return json.loads(return_str) + + +def start_vm(resource_group: str, vm_name: str) -> None: + command = f"az vm start -g {resource_group} --name {vm_name}" + _ = Subprocess.run(command=command) + + +def stop_vm(resource_group: str, vm_name: str) -> None: + command = f"az vm stop -g {resource_group} --name {vm_name}" + _ = Subprocess.run(command=command) + + +def list_vm_sizes(location: str) -> list: + command = f"az vm list-sizes -l {location}" + return_str = Subprocess.run(command=command) + return json.loads(return_str) + + +def deallocate_vm(resource_group: str, vm_name: str) -> None: + command = f"az vm deallocate --resource-group {resource_group} --name {vm_name}" + _ = Subprocess.run(command=command) + + +def generalize_vm(resource_group: str, vm_name: str) -> None: + command = f"az vm generalize --resource-group {resource_group} --name {vm_name}" + _ = Subprocess.run(command=command) + + +def create_image_from_vm(resource_group: str, image_name: str, vm_name: str) -> None: + command = f"az image create --resource-group {resource_group} --name {image_name} --source {vm_name}" + _ = Subprocess.run(command=command) + + +def get_image_resource_id(resource_group: str, image_name: str) -> str: + command = f"az image show --resource-group {resource_group} --name {image_name}" + return_str = Subprocess.run(command=command) + return json.loads(return_str)["id"] diff --git a/maro/cli/utils/common.py b/maro/cli/utils/common.py index 50927e922..e3e0bfb50 100644 --- a/maro/cli/utils/common.py +++ b/maro/cli/utils/common.py @@ -1,7 +1,55 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import os +import subprocess import sys +from collections import deque + +import psutil + +from maro.utils import Logger + + +def close_by_pid(pid: int, recursive: bool = True): + if not psutil.pid_exists(pid): + return + + proc = psutil.Process(pid) + if recursive: + for child in proc.children(recursive=recursive): + child.kill() + + proc.kill() + + +def get_child_pids(parent_pid): + # command = f"ps -o pid --ppid {parent_pid} --noheaders" + # get_children_pid_process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) + # children_pids = get_children_pid_process.stdout.read() + # get_children_pid_process.wait(timeout=2) + + # # Convert into list or int + # try: + # children_pids = int(children_pids) + # except ValueError: + # children_pids = children_pids.decode().split("\n") + # children_pids = [int(pid) for pid in children_pids[:-1]] + + # return children_pids + try: + return [child.pid for child in psutil.Process(parent_pid).children(recursive=True)] + except psutil.NoSuchProcess: + print(f"No process with PID {parent_pid} found") + return + + +def get_redis_pid_by_port(port: int): + get_redis_pid_command = f"pidof 'redis-server *:{port}'" + get_redis_pid_process = subprocess.Popen(get_redis_pid_command, shell=True, stdout=subprocess.PIPE) + redis_pid = int(get_redis_pid_process.stdout.read()) + get_redis_pid_process.wait() + return redis_pid def exit(state: int = 0, msg: str = None): @@ -10,3 +58,75 @@ def exit(state: int = 0, msg: str = None): sys.stderr.write(msg) sys.exit(state) + + +def get_last_k_lines(file_name: str, k: int): + """ + Helper function to retrieve the last K lines from a file in a memory-efficient way. + + Code slightly adapted from https://thispointer.com/python-get-last-n-lines-of-a-text-file-like-tail-command/ + """ + # Create an empty list to keep the track of last k lines + lines = deque() + # Open file for reading in binary mode + with open(file_name, 'rb') as fp: + # Move the cursor to the end of the file + fp.seek(0, os.SEEK_END) + # Create a buffer to keep the last read line + buffer = bytearray() + # Get the current position of pointer i.e eof + ptr = fp.tell() + # Loop till pointer reaches the top of the file + while ptr >= 0: + # Move the file pointer to the location pointed by ptr + fp.seek(ptr) + # Shift pointer location by -1 + ptr -= 1 + # read that byte / character + new_byte = fp.read(1) + # If the read byte is new line character then it means one line is read + if new_byte != b'\n': + # If last read character is not eol then add it in buffer + buffer.extend(new_byte) + elif buffer: + lines.appendleft(buffer.decode()[::-1]) + if len(lines) == k: + return lines + # Reinitialize the byte array to save next line + buffer.clear() + + # As file is read completely, if there is still data in buffer, then it's the first of the last K lines. + if buffer: + lines.appendleft(buffer.decode()[::-1]) + + return lines + + +def show_log(log_path: str, tail: int = -1, logger: Logger = None): + print_fn = logger.info if logger else print + if tail == -1: + with open(log_path, "r") as fp: + for line in fp: + print_fn(line.rstrip('\n')) + else: + for line in get_last_k_lines(log_path, tail): + print_fn(line) + + +def format_env_vars(env: dict, mode: str = "proc"): + if mode == "proc": + return env + + if mode == "docker": + env_opt_list = [] + for key, val in env.items(): + env_opt_list.extend(["--env", f"{key}={val}"]) + return env_opt_list + + if mode == "docker-compose": + return [f"{key}={val}" for key, val in env.items()] + + if mode == "k8s": + return [{"name": key, "value": val} for key, val in env.items()] + + raise ValueError(f"'mode' should be one of 'proc', 'docker', 'docker-compose', 'k8s', got {mode}") diff --git a/maro/cli/utils/docker.py b/maro/cli/utils/docker.py new file mode 100644 index 000000000..a22863697 --- /dev/null +++ b/maro/cli/utils/docker.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import docker + + +def image_exists(image_name: str): + try: + client = docker.from_env() + client.images.get(image_name) + return True + except docker.errors.ImageNotFound: + return False + + +def build_image(context: str, docker_file_path: str, image_name: str): + client = docker.from_env() + with open(docker_file_path, "r"): + client.images.build( + path=context, + tag=image_name, + quiet=False, + rm=True, + custom_context=False, + dockerfile=docker_file_path + ) + + +def push(local_image_name: str, repository: str): + client = docker.from_env() + image = client.images.get(local_image_name) + acr_tag = f"{repository}/{local_image_name}" + image.tag(acr_tag) + # subprocess.run(f"docker push {acr_tag}".split()) + client.images.push(acr_tag) + print(f"Pushed image to {acr_tag}") diff --git a/maro/cli/utils/params.py b/maro/cli/utils/params.py index 9bfd173de..51280c5b5 100644 --- a/maro/cli/utils/params.py +++ b/maro/cli/utils/params.py @@ -38,23 +38,3 @@ class LocalParams: CPU_USAGE = "local_resource:cpu_usage_per_core" MEMORY_USAGE = "local_resource:memory_usage" GPU_USAGE = "local_resource:gpu_memory_usage" - - -class LocalPaths: - """Only use by maro process cli""" - MARO_PROCESS = "~/.maro/clusters/process" - MARO_PROCESS_AGENT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../process/agent/job_agent.py") - MARO_RESOURCE_AGENT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../process/agent/resource_agent.py") - MARO_PROCESS_DEPLOYMENT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../process/deployment") - MARO_GRASS_LOCAL_AGENT = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "../grass/lib/services/master_agent/local_agent.py" - ) - - -class ProcessRedisName: - """Record Redis elements name, and only for maro process""" - PENDING_JOB_TICKETS = "process:pending_job_tickets" - KILLED_JOB_TICKETS = "process:killed_job_tickets" - JOB_DETAILS = "process:job_details" - SETTING = "process:setting" diff --git a/maro/communication/driver/zmq_driver.py b/maro/communication/driver/zmq_driver.py index a321a7199..3fe73ad4e 100644 --- a/maro/communication/driver/zmq_driver.py +++ b/maro/communication/driver/zmq_driver.py @@ -69,7 +69,7 @@ def _setup_sockets(self): """ self._unicast_receiver = self._zmq_context.socket(zmq.PULL) unicast_receiver_port = self._unicast_receiver.bind_to_random_port(f"{self._protocol}://*") - self._logger.info(f"Receive message via unicasting at {self._ip_address}:{unicast_receiver_port}.") + self._logger.debug(f"Receive message via unicasting at {self._ip_address}:{unicast_receiver_port}.") # Dict about zmq.PUSH sockets, fulfills in self.connect. self._unicast_sender_dict = {} @@ -80,7 +80,7 @@ def _setup_sockets(self): self._broadcast_receiver = self._zmq_context.socket(zmq.SUB) self._broadcast_receiver.setsockopt(zmq.SUBSCRIBE, self._component_type.encode()) broadcast_receiver_port = self._broadcast_receiver.bind_to_random_port(f"{self._protocol}://*") - self._logger.info(f"Subscriber message at {self._ip_address}:{broadcast_receiver_port}.") + self._logger.debug(f"Subscriber message at {self._ip_address}:{broadcast_receiver_port}.") # Record own sockets' address. self._address = { @@ -122,10 +122,10 @@ def connect(self, peers_address_dict: Dict[str, Dict[str, str]]): self._unicast_sender_dict[peer_name] = self._zmq_context.socket(zmq.PUSH) self._unicast_sender_dict[peer_name].setsockopt(zmq.SNDTIMEO, self._send_timeout) self._unicast_sender_dict[peer_name].connect(address) - self._logger.info(f"Connects to {peer_name} via unicasting.") + self._logger.debug(f"Connects to {peer_name} via unicasting.") elif int(socket_type) == zmq.SUB: self._broadcast_sender.connect(address) - self._logger.info(f"Connects to {peer_name} via broadcasting.") + self._logger.debug(f"Connects to {peer_name} via broadcasting.") else: raise SocketTypeError(f"Unrecognized socket type {socket_type}.") except Exception as e: @@ -158,7 +158,7 @@ def disconnect(self, peers_address_dict: Dict[str, Dict[str, str]]): raise PeersDisconnectionError(f"Driver cannot disconnect to {peer_name}! Due to {str(e)}") self._disconnected_peer_name_list.append(peer_name) - self._logger.info(f"Disconnected with {peer_name}.") + self._logger.debug(f"Disconnected with {peer_name}.") def receive(self, timeout: int = None): """Receive message from ``zmq.POLLER``. diff --git a/maro/communication/proxy.py b/maro/communication/proxy.py index f79524b09..000fb5be5 100644 --- a/maro/communication/proxy.py +++ b/maro/communication/proxy.py @@ -16,7 +16,7 @@ import redis # private lib -from maro.utils import DummyLogger, InternalLogger +from maro.utils import Logger from maro.utils.exception.communication_exception import InformationUncompletedError, PeersMissError, PendingToSend from maro.utils.exit_code import KILL_ALL_EXIT_CODE, NON_RESTART_EXIT_CODE @@ -63,7 +63,6 @@ class Proxy: initial_peer_discovery_retry_interval: Base value for the wait time between retries to find peers. Retries follow the exponential backoff algorithm. Defaults to 0.1. max_peer_discovery_retries: Maximum number of retries to find peers. Defaults to 5. - log_enable (bool): Open internal logger or not. Defaults to True. enable_rejoin (bool): Allow peers rejoin or not. Defaults to False, and must use with maro cli. minimal_peers Union[int, dict]: The minimal number of peers for each peer type. peers_catch_lifetime (int): The lifetime for onboard peers' information. @@ -87,7 +86,6 @@ def __init__( max_redis_connect_retries: int = MAX_REDIS_CONNECT_RETRIES, initial_peer_discovery_retry_interval: int = INITIAL_PEER_DISCOVERY_RETRY_INTERVAL, max_peer_discovery_retries: int = MAX_PEER_DISCOVERY_RETRIES, - log_enable: bool = True, enable_rejoin: bool = ENABLE_REJOIN, minimal_peers: Union[int, dict] = MINIMAL_PEERS, peers_catch_lifetime: int = PEERS_CATCH_LIFETIME, @@ -109,8 +107,7 @@ def __init__( self._max_redis_connect_retries = max_redis_connect_retries self._initial_peer_discovery_retry_interval = initial_peer_discovery_retry_interval self._max_peer_discovery_retries = max_peer_discovery_retries - self._log_enable = log_enable - self._logger = InternalLogger(component_name=self._name + "_proxy") if self._log_enable else DummyLogger() + self._logger = Logger(".".join([self._name, "proxy"])) # TODO:In multiprocess with spawn start method, the driver must be initiated before the Redis. # Otherwise it will cause Error 9: Bad File Descriptor in proxy.__del__(). Root cause not found. @@ -139,7 +136,7 @@ def __init__( next_retry *= 2 if success: - self._logger.info( + self._logger.debug( f"{self._name} is successfully connected to the redis server " f"at {redis_address[0]}:{redis_address[1]}." ) @@ -259,7 +256,7 @@ def _get_peers_list(self): registered_peers = [peer.decode() for peer in self._redis_connection.hkeys(peer_hash_name)] if len(registered_peers) > num_expected: del registered_peers[num_expected:] - self._logger.info(f"{self._name} successfully get all {peer_type}\'s names.") + self._logger.debug(f"{self._name} successfully get all {peer_type}\'s names.") break else: self._logger.warn( @@ -285,7 +282,7 @@ def _build_connection(self): peers_socket_value = self._redis_connection.hmget(info.hash_table_name, name_list) for idx, peer_name in enumerate(name_list): self._onboard_peer_dict[peer_type][peer_name] = json.loads(peers_socket_value[idx]) - self._logger.info(f"{self._name} successfully get {peer_name}\'s socket address") + self._logger.debug(f"{self._name} successfully get {peer_name}\'s socket address") except Exception as e: raise InformationUncompletedError(f"{self._name} failed to get {name_list}\'s address. Due to {str(e)}") @@ -539,7 +536,7 @@ def _send(self, message: Message) -> Union[List[str], None]: and message.destination in self._onboard_peer_dict[peer_type] and message.destination in self._message_cache_for_exited_peers ): - self._logger.info(f"Sending pending message to {message.destination}.") + self._logger.debug(f"Sending pending message to {message.destination}.") for pending_message in self._message_cache_for_exited_peers[message.destination]: self._driver.send(pending_message) session_id_list.append(pending_message.session_id) @@ -662,18 +659,18 @@ def _check_peers_update(self): for peer_name in union_peer_name: # Add new peers (new key added on redis). if peer_name not in list(self._onboard_peer_dict[peer_type].keys()): - self._logger.info(f"PEER_REJOIN: New peer {peer_name} join.") + self._logger.debug(f"PEER_REJOIN: New peer {peer_name} join.") self._driver.connect({peer_name: onboard_peers_dict_on_redis[peer_name]}) self._onboard_peer_dict[peer_type][peer_name] = onboard_peers_dict_on_redis[peer_name] # Delete out of date peers (old key deleted on local) elif peer_name not in onboard_peers_dict_on_redis.keys(): - self._logger.info(f"PEER_REJOIN: Peer {peer_name} exited.") + self._logger.debug(f"PEER_REJOIN: Peer {peer_name} exited.") self._driver.disconnect({peer_name: self._onboard_peer_dict[peer_type][peer_name]}) del self._onboard_peer_dict[peer_type][peer_name] else: # Peer's ip/port updated, re-connect (value update on redis). if onboard_peers_dict_on_redis[peer_name] != self._onboard_peer_dict[peer_type][peer_name]: - self._logger.info(f"PEER_REJOIN: Peer {peer_name} rejoin.") + self._logger.debug(f"PEER_REJOIN: Peer {peer_name} rejoin.") self._driver.disconnect({peer_name: self._onboard_peer_dict[peer_type][peer_name]}) self._driver.connect({peer_name: onboard_peers_dict_on_redis[peer_name]}) self._onboard_peer_dict[peer_type][peer_name] = onboard_peers_dict_on_redis[peer_name] @@ -742,7 +739,7 @@ def _push_message_to_message_cache(self, message: Message): return self._message_cache_for_exited_peers[peer_name].append(message) - self._logger.info(f"Temporarily save message {message.session_id} to message cache.") + self._logger.debug(f"Temporarily save message {message.session_id} to message cache.") def close(self): self._redis_connection.hdel(self._redis_hash_name, self._name) diff --git a/maro/rl/distributed/__init__.py b/maro/rl/distributed/__init__.py new file mode 100644 index 000000000..cd9276166 --- /dev/null +++ b/maro/rl/distributed/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .abs_proxy import AbsProxy +from .abs_worker import AbsWorker + +__all__ = [ + "AbsProxy", "AbsWorker", +] diff --git a/maro/rl/distributed/abs_proxy.py b/maro/rl/distributed/abs_proxy.py new file mode 100644 index 000000000..31002f0f4 --- /dev/null +++ b/maro/rl/distributed/abs_proxy.py @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import abstractmethod + +import zmq +from tornado.ioloop import IOLoop +from zmq import Context +from zmq.eventloop.zmqstream import ZMQStream + +from maro.rl.utils.common import get_own_ip_address + + +class AbsProxy(object): + """Abstract proxy class that serves as an intermediary between task producers and task consumers. + + The proxy receives compute tasks from multiple clients, forwards them to a set of back-end workers for + processing and returns the results to the clients. + + Args: + frontend_port (int): Network port for communicating with clients (task producers). + backend_port (int): Network port for communicating with back-end workers (task consumers). + """ + + def __init__(self, frontend_port: int, backend_port: int) -> None: + super(AbsProxy, self).__init__() + + # ZMQ sockets and streams + self._context = Context.instance() + self._req_socket = self._context.socket(zmq.ROUTER) + self._ip_address = get_own_ip_address() + self._req_socket.bind(f"tcp://{self._ip_address}:{frontend_port}") + self._req_endpoint = ZMQStream(self._req_socket) + self._dispatch_socket = self._context.socket(zmq.ROUTER) + self._dispatch_socket.bind(f"tcp://{self._ip_address}:{backend_port}") + self._dispatch_endpoint = ZMQStream(self._dispatch_socket) + self._event_loop = IOLoop.current() + + # register handlers + self._dispatch_endpoint.on_recv(self._send_result_to_requester) + + @abstractmethod + def _route_request_to_compute_node(self, msg: list) -> None: + """Dispatch the task to one or more workers for processing. + + The dispatching strategy should be implemented here. + + Args: + msg (list): Multi-part message containing task specifications and parameters. + """ + raise NotImplementedError + + @abstractmethod + def _send_result_to_requester(self, msg: list) -> None: + """Return a task result to the client that requested it. + + The result aggregation logic, if applicable, should be implemented here. + + Args: + msg (list): Multi-part message containing a task result. + """ + raise NotImplementedError + + def start(self) -> None: + """Start a Tornado event loop. + + Calling this enters the proxy into an event loop where it starts doing its job. + """ + self._event_loop.start() + + def stop(self) -> None: + """Stop the currently running event loop. + """ + self._event_loop.stop() diff --git a/maro/rl/distributed/abs_worker.py b/maro/rl/distributed/abs_worker.py new file mode 100644 index 000000000..024b8324c --- /dev/null +++ b/maro/rl/distributed/abs_worker.py @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import abstractmethod + +import zmq +from tornado.ioloop import IOLoop +from zmq import Context +from zmq.eventloop.zmqstream import ZMQStream + +from maro.rl.utils.common import get_ip_address_by_hostname, string_to_bytes +from maro.utils import DummyLogger, Logger + + +class AbsWorker(object): + """Abstract worker class to process a task in distributed fashion. + + Args: + idx (int): Integer identifier for the worker. It is used to generate an internal ID, "worker.{idx}", + so that the task producer can keep track of its connection status. + producer_host (str): IP address of the task producer host to connect to. + producer_port (int): Port of the task producer host to connect to. + logger (Logger, default=None): The logger of the workflow. + """ + + def __init__( + self, + idx: int, + producer_host: str, + producer_port: int, + logger: Logger = None, + ) -> None: + super(AbsWorker, self).__init__() + + self._id = f"worker.{idx}" + self._logger = logger if logger else DummyLogger() + + # ZMQ sockets and streams + self._context = Context.instance() + self._socket = self._context.socket(zmq.DEALER) + self._socket.identity = string_to_bytes(self._id) + + self._producer_ip = get_ip_address_by_hostname(producer_host) + self._producer_address = f"tcp://{self._producer_ip}:{producer_port}" + self._socket.connect(self._producer_address) + self._logger.info(f"Connected to producer at {self._producer_address}") + + self._stream = ZMQStream(self._socket) + self._stream.send(b"READY") + + self._event_loop = IOLoop.current() + + # register handlers + self._stream.on_recv(self._compute) + + @abstractmethod + def _compute(self, msg: list) -> None: + """The task processing logic should be implemented here. + + Args: + msg (list): Multi-part message containing task specifications and parameters. + """ + raise NotImplementedError + + def start(self) -> None: + """Start a Tornado event loop. + + Calling this enters the worker into an event loop where it starts doing its job. + """ + self._event_loop.start() + + def stop(self) -> None: + """Stop the currently running event loop. + """ + self._event_loop.stop() diff --git a/maro/rl/exploration/__init__.py b/maro/rl/exploration/__init__.py index 8ee78737c..7e60b56c5 100644 --- a/maro/rl/exploration/__init__.py +++ b/maro/rl/exploration/__init__.py @@ -6,5 +6,5 @@ __all__ = [ "AbsExplorationScheduler", "LinearExplorationScheduler", "MultiLinearExplorationScheduler", - "epsilon_greedy", "gaussian_noise", "uniform_noise" + "epsilon_greedy", "gaussian_noise", "uniform_noise", ] diff --git a/maro/rl/exploration/scheduling.py b/maro/rl/exploration/scheduling.py index 5b32528c7..ce4dd930f 100644 --- a/maro/rl/exploration/scheduling.py +++ b/maro/rl/exploration/scheduling.py @@ -12,36 +12,37 @@ class AbsExplorationScheduler(ABC): exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. - initial_value: Initial value for the exploration parameter. If None, the value from the original dictionary - the policy is instantiated with will be used as the initial value. Defaults to None. + initial_value (float, default=None): Initial value for the exploration parameter. If None, the value used + when instantiating the policy will be used as the initial value. """ - def __init__(self, exploration_params: dict, param_name: str, initial_value=None): + def __init__(self, exploration_params: dict, param_name: str, initial_value: float = None) -> None: super().__init__() self._exploration_params = exploration_params self.param_name = param_name if initial_value is not None: self._exploration_params[self.param_name] = initial_value - def get_value(self): + def get_value(self) -> float: return self._exploration_params[self.param_name] @abstractmethod - def step(self): + def step(self) -> None: raise NotImplementedError class LinearExplorationScheduler(AbsExplorationScheduler): """Linear exploration parameter schedule. + Args: exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the scheduler is applied. param_name (str): Name of the exploration parameter to which the scheduler is applied. last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. - start_ep (int): starting episode. Defaults to 1. - initial_value: Initial value for the exploration parameter. If None, the value from the original dictionary - the policy is instantiated with will be used as the initial value. Defaults to None. + start_ep (int, default=1): starting episode. + initial_value (float, default=None): Initial value for the exploration parameter. If None, the value used + when instantiating the policy will be used as the initial value. """ def __init__( @@ -53,7 +54,7 @@ def __init__( final_value: float, start_ep: int = 1, initial_value: float = None, - ): + ) -> None: super().__init__(exploration_params, param_name, initial_value=initial_value) self.final_value = final_value if last_ep > 1: @@ -61,7 +62,7 @@ def __init__( else: self.delta = 0 - def step(self): + def step(self) -> None: if self._exploration_params[self.param_name] == self.final_value: return @@ -70,6 +71,7 @@ def step(self): class MultiLinearExplorationScheduler(AbsExplorationScheduler): """Exploration parameter schedule that consists of multiple linear phases. + Args: exploration_params (dict): The exploration params attribute from some ``RLPolicy`` instance to which the scheduler is applied. @@ -80,13 +82,11 @@ class MultiLinearExplorationScheduler(AbsExplorationScheduler): cannot be two points with the same first element (episode), or a ``ValueError`` will be raised. last_ep (int): Last episode. final_value (float): The value of the exploration parameter corresponding to ``last_ep``. - start_ep (int): starting episode. Defaults to 1. - initial_value: Initial value for the exploration parameter. If None, the value from the original dictionary - the policy is instantiated with will be used as the initial value. Defaults to None. - - Returns: - An iterator over the series of exploration rates from episode 0 to ``max_iter`` - 1. + start_ep (int, default=1): starting episode. + initial_value (float, default=None): Initial value for the exploration parameter. If None, the value from + the original dictionary the policy is instantiated with will be used as the initial value. """ + def __init__( self, exploration_params: dict, @@ -96,8 +96,8 @@ def __init__( last_ep: int, final_value: float, start_ep: int = 1, - initial_value: float = None - ): + initial_value: float = None, + ) -> None: # validate splits splits = [(start_ep, initial_value)] + splits + [(last_ep, final_value)] splits.sort() @@ -112,7 +112,7 @@ def __init__( self._split_index = 1 self._delta = (self._splits[1][1] - self._exploration_params[self.param_name]) / (self._splits[1][0] - start_ep) - def step(self): + def step(self) -> None: if self._split_index == len(self._splits): return @@ -125,13 +125,3 @@ def step(self): (self._splits[self._split_index][1] - self._splits[self._split_index - 1][1]) / (self._splits[self._split_index][0] - self._splits[self._split_index - 1][0]) ) - - -if __name__ == "__main__": - exploration_params = {"epsilon": 0.6} - scheduler = MultiLinearExplorationScheduler( - exploration_params, "epsilon", 20, [(12, 0.25), (6, 0.5), (16, 0.15), (9, 0.4)], .0 - ) - for ep in range(1, scheduler.last_ep + 1): - print(f"ep = {ep}, value = {exploration_params['epsilon']}") - scheduler.step() diff --git a/maro/rl/exploration/strategies.py b/maro/rl/exploration/strategies.py index aa822c86f..c85340c78 100644 --- a/maro/rl/exploration/strategies.py +++ b/maro/rl/exploration/strategies.py @@ -6,13 +6,19 @@ import numpy as np -def epsilon_greedy(state: np.ndarray, action: np.ndarray, num_actions, *, epsilon: float) -> np.ndarray: - """epsilon-greedy exploration. +def epsilon_greedy( + state: np.ndarray, + action: np.ndarray, + num_actions: int, + *, + epsilon: float, +) -> np.ndarray: + """Epsilon-greedy exploration. Args: state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the vanilla eps-greedy exploration and is put here to conform to the function signature required for the exploration - strategy parameter for ``DQN``. See ``maro.rl.policy.DQN`` for more details. + strategy parameter for ``DQN``. action (np.ndarray): Action(s) chosen greedily by the policy. num_actions (int): Number of possible actions. epsilon (float): The probability that a random action will be selected. @@ -30,17 +36,17 @@ def uniform_noise( max_action: Union[float, list, np.ndarray] = None, *, low: Union[float, list, np.ndarray], - high: Union[float, list, np.ndarray] + high: Union[float, list, np.ndarray], ) -> Union[float, np.ndarray]: - """Apply a uniform noise to a continuous multi-dimensional action. + """Apply a uniform noise to a continuous multidimensional action. Args: state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the gaussian noise exploration scheme and is put here to conform to the function signature for the exploration in continuous action spaces. action (np.ndarray): Action(s) chosen greedily by the policy. - min_action (Union[float, list, np.ndarray]): Lower bound for the multi-dimensional action space. - max_action (Union[float, list, np.ndarray]): Upper bound for the multi-dimensional action space. + min_action (Union[float, list, np.ndarray], default=None): Lower bound for the multidimensional action space. + max_action (Union[float, list, np.ndarray], default=None): Upper bound for the multidimensional action space. low (Union[float, list, np.ndarray]): Lower bound for the noise range. high (Union[float, list, np.ndarray]): Upper bound for the noise range. @@ -59,26 +65,26 @@ def gaussian_noise( min_action: Union[float, list, np.ndarray] = None, max_action: Union[float, list, np.ndarray] = None, *, - mean: Union[float, list, np.ndarray] = .0, + mean: Union[float, list, np.ndarray] = 0.0, stddev: Union[float, list, np.ndarray] = 1.0, - relative: bool = False + relative: bool = False, ) -> Union[float, np.ndarray]: - """Apply a gaussian noise to a continuous multi-dimensional action. + """Apply a gaussian noise to a continuous multidimensional action. Args: state (np.ndarray): State(s) based on which ``action`` is chosen. This is not used by the gaussian noise exploration scheme and is put here to conform to the function signature for the exploration in continuous action spaces. action (np.ndarray): Action(s) chosen greedily by the policy. - min_action (Union[float, list, np.ndarray]): Lower bound for the multi-dimensional action space. - max_action (Union[float, list, np.ndarray]): Upper bound for the multi-dimensional action space. - mean (Union[float, list, np.ndarray]): Gaussian noise mean. Defaults to .0. - stddev (Union[float, list, np.ndarray]): Standard deviation for the Gaussian noise. Defaults to 1.0. - relative (bool): If True, the generated noise is treated as a relative measure and will be multiplied by the - action itself before being added to the action. Defaults to False. + min_action (Union[float, list, np.ndarray], default=None): Lower bound for the multidimensional action space. + max_action (Union[float, list, np.ndarray], default=None): Upper bound for the multidimensional action space. + mean (Union[float, list, np.ndarray], default=0.0): Gaussian noise mean. + stddev (Union[float, list, np.ndarray], default=1.0): Standard deviation for the Gaussian noise. + relative (bool, default=False): If True, the generated noise is treated as a relative measure and will + be multiplied by the action itself before being added to the action. Returns: - Exploration actions with added noise. + Exploration actions with added noise (a numpy ndarray). """ noise = np.random.normal(loc=mean, scale=stddev, size=action.shape) if min_action is None and max_action is None: diff --git a/maro/rl/learning/__init__.py b/maro/rl/learning/__init__.py deleted file mode 100644 index 388b016eb..000000000 --- a/maro/rl/learning/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .env_sampler import AbsEnvSampler -from .policy_manager import AbsPolicyManager, DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager -from .rollout_manager import AbsRolloutManager, DistributedRolloutManager, MultiProcessRolloutManager - -__all__ = [ - "AbsEnvSampler", - "AbsPolicyManager", "DistributedPolicyManager", "MultiProcessPolicyManager", "SimplePolicyManager", - "AbsRolloutManager", "DistributedRolloutManager", "MultiProcessRolloutManager" -] diff --git a/maro/rl/learning/env_sampler.py b/maro/rl/learning/env_sampler.py deleted file mode 100644 index 4ad074b9b..000000000 --- a/maro/rl/learning/env_sampler.py +++ /dev/null @@ -1,523 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from collections import defaultdict, deque -from multiprocessing import Pipe, Process -from os import getcwd, path -from typing import Callable, Dict - -import numpy as np - -from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.policy import RLPolicy -from maro.rl.utils import MsgKey, MsgTag -from maro.simulator import Env -from maro.utils import Logger, clone - -from .helpers import get_rollout_finish_msg - - -class SimpleAgentWrapper: - """Wrapper for multiple agents using multiple policies to expose simple single-agent interfaces.""" - def __init__(self, get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str]): - self.policy_dict = {policy_id: func(policy_id) for policy_id, func in get_policy_func_dict.items()} - self.agent2policy = agent2policy - self.policy_by_agent = {agent: self.policy_dict[policy_id] for agent, policy_id in agent2policy.items()} - - def load(self, dir: str): - for id_, policy in self.policy_dict.items(): - pth = path.join(dir, id_) - if path.exists(pth): - policy.load(pth) - - def choose_action(self, state_by_agent: Dict[str, np.ndarray]): - states_by_policy, agents_by_policy = defaultdict(list), defaultdict(list) - for agent, state in state_by_agent.items(): - states_by_policy[self.agent2policy[agent]].append(state) - agents_by_policy[self.agent2policy[agent]].append(agent) - - action_by_agent = {} - # compute the actions for local policies first while the inferences processes do their work. - for policy_id, policy in self.policy_dict.items(): - if states_by_policy[policy_id]: - action_by_agent.update( - zip(agents_by_policy[policy_id], policy(np.vstack(states_by_policy[policy_id]))) - ) - - return action_by_agent - - def set_policy_states(self, policy_state_dict: dict): - for policy_id, policy_state in policy_state_dict.items(): - self.policy_dict[policy_id].set_state(policy_state) - - def explore(self): - for policy in self.policy_dict.values(): - if hasattr(policy, "greedy"): - policy.greedy = False - - def exploit(self): - for policy in self.policy_dict.values(): - if hasattr(policy, "greedy"): - policy.greedy = True - - def exploration_step(self): - for policy in self.policy_dict.values(): - if hasattr(policy, "exploration_step"): - policy.exploration_step() - - def get_rollout_info(self): - return { - policy_id: policy.get_rollout_info() for policy_id, policy in self.policy_dict.items() - if isinstance(policy, RLPolicy) - } - - def get_exploration_params(self): - return { - policy_id: clone(policy.exploration_params) for policy_id, policy in self.policy_dict.items() - if isinstance(policy, RLPolicy) - } - - def record_transition(self, agent: str, state, action, reward, next_state, terminal: bool): - if isinstance(self.policy_by_agent[agent], RLPolicy): - self.policy_by_agent[agent].record(agent, state, action, reward, next_state, terminal) - - def improve(self, checkpoint_dir: str = None): - for id_, policy in self.policy_dict.items(): - if hasattr(policy, "improve"): - policy.improve() - if checkpoint_dir: - policy.save(path.join(checkpoint_dir, id_)) - - -class ParallelAgentWrapper: - """Wrapper for multiple agents using multiple policies to expose simple single-agent interfaces. - - The policy instances are distributed across multiple processes to achieve parallel inference. - """ - def __init__(self, get_policy_func_dict: Dict[str, Callable], agent2policy: Dict[str, str]): - self.agent2policy = agent2policy - self._inference_services = [] - self._conn = {} - - def _inference_service(id_, get_policy, conn): - policy = get_policy(id_) - while True: - msg = conn.recv() - if msg["type"] == "load": - if hasattr(policy, "load"): - policy.load(path.join(msg["dir"], id_)) - elif msg["type"] == "choose_action": - actions = policy(msg["states"]) - conn.send(actions) - elif msg["type"] == "set_state": - if hasattr(policy, "set_state"): - policy.set_state(msg["policy_state"]) - elif msg["type"] == "explore": - if hasattr(policy, "greedy"): - policy.greedy = False - elif msg["type"] == "exploit": - if hasattr(policy, "greedy"): - policy.greedy = True - elif msg["type"] == "exploration_step": - if hasattr(policy, "exploration_step"): - policy.exploration_step() - elif msg["type"] == "rollout_info": - conn.send(policy.get_rollout_info() if hasattr(policy, "get_rollout_info") else None) - elif msg["type"] == "exploration_params": - conn.send(policy.exploration_params if hasattr(policy, "exploration_params") else None) - elif msg["type"] == "record": - if hasattr(policy, "record"): - policy.record( - msg["agent"], msg["state"], msg["action"], msg["reward"], msg["next_state"], msg["terminal"] - ) - elif msg["type"] == "update": - if hasattr(policy, "update"): - policy.update(msg["loss_info"]) - elif msg["type"] == "learn": - if hasattr(policy, "learn"): - policy.learn(msg["batch"]) - elif msg["type"] == "improve": - if hasattr(policy, "improve"): - policy.improve() - if msg["checkpoint_dir"]: - policy.save(path.join(msg["checkpoint_dir"], id_)) - - for policy_id in get_policy_func_dict: - conn1, conn2 = Pipe() - self._conn[policy_id] = conn1 - host = Process( - target=_inference_service, - args=(policy_id, get_policy_func_dict[policy_id], conn2) - ) - self._inference_services.append(host) - host.start() - - def load(self, dir: str): - for conn in self._conn.values(): - conn.send({"type": "load", "dir": dir}) - - def choose_action(self, state_by_agent: Dict[str, np.ndarray]): - states_by_policy, agents_by_policy = defaultdict(list), defaultdict(list) - for agent, state in state_by_agent.items(): - states_by_policy[self.agent2policy[agent]].append(state) - agents_by_policy[self.agent2policy[agent]].append(agent) - - # send state batch to inference processes for parallelized inference. - for policy_id, conn in self._conn.items(): - if states_by_policy[policy_id]: - conn.send({"type": "choose_action", "states": np.vstack(states_by_policy[policy_id])}) - - action_by_agent = {} - for policy_id, conn in self._conn.items(): - if states_by_policy[policy_id]: - action_by_agent.update(zip(agents_by_policy[policy_id], conn.recv())) - - return action_by_agent - - def set_policy_states(self, policy_state_dict: dict): - for policy_id, conn in self._conn.items(): - conn.send({"type": "set_state", "policy_state": policy_state_dict[policy_id]}) - - def explore(self): - for conn in self._conn.values(): - conn.send({"type": "explore"}) - - def exploit(self): - for conn in self._conn.values(): - conn.send({"type": "exploit"}) - - def exploration_step(self): - for conn in self._conn.values(): - conn.send({"type": "exploration_step"}) - - def get_rollout_info(self): - rollout_info = {} - for conn in self._conn.values(): - conn.send({"type": "rollout_info"}) - - for policy_id, conn in self._conn.items(): - info = conn.recv() - if info: - rollout_info[policy_id] = info - - return rollout_info - - def get_exploration_params(self): - exploration_params = {} - for conn in self._conn.values(): - conn.send({"type": "exploration_params"}) - - for policy_id, conn in self._conn.items(): - params = conn.recv() - if params: - exploration_params[policy_id] = params - - return exploration_params - - def record_transition(self, agent: str, state, action, reward, next_state, terminal: bool): - self._conn[self.agent2policy[agent]].send({ - "type": "record", "agent": agent, "state": state, "action": action, "reward": reward, - "next_state": next_state, "terminal": terminal - }) - - def improve(self, checkpoint_dir: str = None): - for conn in self._conn.values(): - conn.send({"type": "improve", "checkpoint_dir": checkpoint_dir}) - - -class AbsEnvSampler(ABC): - """Simulation data collector and policy evaluator. - - Args: - get_env (Callable[[], Env]): Function to create an ``Env`` instance for collecting training data. The function - should take no parameters and return an environment wrapper instance. - get_policy_func_dict (dict): A dictionary mapping policy names to functions that create them. The policy - creation function should have policy name as the only parameter and return an ``AbsPolicy`` instance. - agent2policy (Dict[str, str]): A dictionary that maps agent IDs to policy IDs, i.e., specifies the policy used - by each agent. - get_test_env (Callable): Function to create an ``Env`` instance for testing policy performance. The function - should take no parameters and return an environment wrapper instance. If this is None, the training - environment wrapper will be used for evaluation in the worker processes. Defaults to None. - reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward - for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately - after executing an action. - parallel_inference (bool): If True, the policies will be placed in separate processes so that inference can be - performed in parallel to speed up simulation. This is useful if some policies are big and take a long time - to generate actions. Defaults to False. - """ - def __init__( - self, - get_env: Callable[[], Env], - get_policy_func_dict: Dict[str, Callable], - agent2policy: Dict[str, str], - get_test_env: Callable[[], Env] = None, - reward_eval_delay: int = 0, - parallel_inference: bool = False - ): - self._learn_env = get_env() - self._test_env = get_test_env() if get_test_env else self._learn_env - self.env = None - - agent_wrapper_cls = ParallelAgentWrapper if parallel_inference else SimpleAgentWrapper - self.agent_wrapper = agent_wrapper_cls(get_policy_func_dict, agent2policy) - - self.reward_eval_delay = reward_eval_delay - self._state = None - self._event = None - self._step_index = 0 - - self._transition_cache = defaultdict(deque) # for caching transitions whose rewards have yet to be evaluated - self.tracker = {} # User-defined tracking information is placed here. - - @property - def event(self): - return self._event - - @abstractmethod - def get_state(self, tick: int = None) -> dict: - """Compute the state for a given tick. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - Returns: - A dictionary with (agent ID, state) as key-value pairs. - """ - raise NotImplementedError - - @abstractmethod - def get_env_actions(self, action) -> dict: - """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" - raise NotImplementedError - - @abstractmethod - def get_reward(self, actions: list, tick: int): - """Evaluate the reward for an action. - Args: - tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in - ``actions`` must be an Action object defined for the environment in question. - - Returns: - A dictionary with (agent ID, reward) as key-value pairs. - """ - raise NotImplementedError - - def sample(self, policy_state_dict: dict = None, num_steps: int = -1, return_rollout_info: bool = True): - self.env = self._learn_env - if not self._state: - # reset and get initial state - self.env.reset() - self._step_index = 0 - self._transition_cache.clear() - self.tracker.clear() - _, self._event, _ = self.env.step(None) - self._state = self.get_state() - - # set policy states - if policy_state_dict: - self.agent_wrapper.set_policy_states(policy_state_dict) - self.agent_wrapper.explore() - - starting_step_index = self._step_index + 1 - steps_to_go = float("inf") if num_steps == -1 else num_steps - while self._state and steps_to_go > 0: - action = self.agent_wrapper.choose_action(self._state) - env_actions = self.get_env_actions(action) - for agent, state in self._state.items(): - self._transition_cache[agent].append((state, action[agent], env_actions, self.env.tick)) - _, self._event, done = self.env.step(env_actions) - self._state = None if done else self.get_state() - self._step_index += 1 - steps_to_go -= 1 - - """ - If this is the final step, evaluate rewards for all remaining events except the last. - Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ - for agent, cache in self._transition_cache.items(): - while cache and (not self._state or self.env.tick - cache[0][-1] >= self.reward_eval_delay): - state, action, env_actions, tick = cache.popleft() - reward = self.get_reward(env_actions, tick) - self.post_step(state, action, env_actions, reward, tick) - self.agent_wrapper.record_transition( - agent, state, action, reward[agent], cache[0][0] if cache else self._state, - not cache and not self._state - ) - - result = { - "step_range": (starting_step_index, self._step_index), - "tracker": self.tracker, - "end_of_episode": not self._state, - "exploration_params": self.agent_wrapper.get_exploration_params() - } - if return_rollout_info: - result["rollout_info"] = self.agent_wrapper.get_rollout_info() - - if not self._state: - self.agent_wrapper.exploration_step() - return result - - def test(self, policy_state_dict: dict = None): - self.env = self._test_env - # set policy states - if policy_state_dict: - self.agent_wrapper.set_policy_states(policy_state_dict) - - # Set policies to exploitation mode - self.agent_wrapper.exploit() - - self.env.reset() - terminal = False - # get initial state - _, self._event, _ = self.env.step(None) - state = self.get_state() - while not terminal: - action = self.agent_wrapper.choose_action(state) - env_actions = self.get_env_actions(action) - _, self._event, terminal = self.env.step(env_actions) - if not terminal: - state = self.get_state() - - return self.tracker - - def post_step(self, state, action, env_actions, reward, tick): - """ - Gather any information you wish to track during a roll-out episode and store it in the ``tracker`` attribute. - """ - pass - - def worker( - self, - group: str, - index: int, - num_extra_recv_attempts: int = 0, - recv_timeout: int = 100, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - """Roll-out worker process that can be launched on separate computation nodes. - - Args: - group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager - that manages them. - index (int): Worker index. The worker's ID in the cluster will be "ROLLOUT_WORKER.{worker_idx}". - This is used for bookkeeping by the roll-out manager. - num_extra_recv_attempts (int): Number of extra receive attempts after each received ``SAMPLE`` message. This - is used to catch the worker up to the latest episode in case it trails the main learning loop by at - least one full episode. Defaults to 0. - recv_timeout (int): Timeout for the extra receive attempts. Defaults to 100 (miliseconds). - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - proxy = Proxy( - group, "rollout_worker", {"rollout_manager": 1}, component_name=f"ROLLOUT_WORKER.{index}", **proxy_kwargs - ) - logger = Logger(proxy.name, dump_folder=log_dir) - - """ - The event loop handles 3 types of messages from the roll-out manager: - 1) COLLECT, upon which the agent-environment simulation will be carried out for a specified number of steps - and the collected experiences will be sent back to the roll-out manager; - 2) EVAL, upon which the policies contained in the message payload will be evaluated for the entire - duration of the evaluation environment. - 3) EXIT, upon which it will break out of the event loop and the process will terminate. - - """ - while True: - msg = proxy.receive_once() - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - - if msg.tag == MsgTag.SAMPLE: - latest = msg - for _ in range(num_extra_recv_attempts): - msg = proxy.receive_once(timeout=recv_timeout) - if msg.body[MsgKey.EPISODE] > latest.body[MsgKey.EPISODE]: - logger.info(f"Skipped roll-out message for ep {latest.body[MsgKey.EPISODE]}") - latest = msg - - ep = latest.body[MsgKey.EPISODE] - result = self.sample( - policy_state_dict=latest.body[MsgKey.POLICY_STATE], num_steps=latest.body[MsgKey.NUM_STEPS] - ) - logger.info( - get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) - ) - return_info = { - MsgKey.EPISODE: ep, - MsgKey.SEGMENT: latest.body[MsgKey.SEGMENT], - MsgKey.ROLLOUT_INFO: result["rollout_info"], - MsgKey.STEP_RANGE: result["step_range"], - MsgKey.TRACKER: result["tracker"], - MsgKey.END_OF_EPISODE: result["end_of_episode"] - } - proxy.reply(latest, tag=MsgTag.SAMPLE_DONE, body=return_info) - elif msg.tag == MsgTag.TEST: - tracker = self.test(msg.body[MsgKey.POLICY_STATE]) - return_info = {MsgKey.TRACKER: tracker, MsgKey.EPISODE: msg.body[MsgKey.EPISODE]} - logger.info("Testing complete") - proxy.reply(msg, tag=MsgTag.TEST_DONE, body=return_info) - - def actor( - self, - group: str, - index: int, - num_episodes: int, - num_steps: int = -1, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - """Controller for single-threaded learning workflows. - - Args: - group (str): Group name for the cluster that includes the server and all actors. - index (int): Integer actor index. The actor's ID in the cluster will be "ACTOR.{actor_idx}". - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on how the implementation of the roll-out manager. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in - which case the roll-out will be executed until the end of the environment. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LOCAL_ROLLOUT_MANAGER" will be created at - init time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1") - - name = f"ACTOR.{index}" - logger = Logger(name, dump_folder=log_dir) - peers = {"policy_server": 1} - proxy = Proxy(group, "actor", peers, component_name=name, **proxy_kwargs) - server_address = proxy.peers["policy_server"][0] - - # get initial policy states from the policy manager - msg = SessionMessage(MsgTag.GET_INITIAL_POLICY_STATE, proxy.name, server_address) - reply = proxy.send(msg)[0] - policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] - - # main loop - for ep in range(1, num_episodes + 1): - while True: - result = self.sample(policy_state_dict=policy_state_dict, num_steps=num_steps) - logger.info( - get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) - ) - # Send roll-out info to policy server for learning - reply = proxy.send( - SessionMessage( - MsgTag.SAMPLE_DONE, proxy.name, server_address, - body={MsgKey.ROLLOUT_INFO: result["rollout_info"], MsgKey.VERSION: policy_version} - ) - )[0] - policy_state_dict, policy_version = reply.body[MsgKey.POLICY_STATE], reply.body[MsgKey.VERSION] - if result["end_of_episode"]: - break - - # tell the policy server I'm all done. - proxy.isend(SessionMessage(MsgTag.DONE, proxy.name, server_address, session_type=SessionType.NOTIFICATION)) - proxy.close() diff --git a/maro/rl/learning/env_wrapper.py b/maro/rl/learning/env_wrapper.py deleted file mode 100644 index dd752c83e..000000000 --- a/maro/rl/learning/env_wrapper.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from collections import defaultdict, deque -from typing import Callable - -from maro.rl.experience import ExperienceSet -from maro.simulator import Env - - -class Transition: - """Convenience class to be used in an environment wrapper's post-step processing function. - - Args: - state: Output of the environment wrapper's ``get_state``. - action: Output of the ``AgentWrapper`` that interacts with environment wrapper. - env_action: Output of the environment wrapper's ``to_env_action``. - reward: Output of the environmet wrapper's ``get_reward``. - next_state: The state immediately following ``state``. - info: Output of the environment wrapper's ``get_transition_info``. - """ - - __slots__ = ["state", "action", "env_action", "reward", "next_state", "info"] - - def __init__(self, state, action, env_action, reward, next_state, info): - self.state = state - self.action = action - self.env_action = env_action - self.reward = reward - self.next_state = next_state - self.info = info - - -class AbsEnvWrapper(ABC): - """Environment wrapper that performs scenario-specific processing, transition caching and experience generation. - - Args: - env (Env): Environment instance. - reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward - for the action taken for that event. Defaults to 0, which means rewards are evaluated immediately - after executing an action. - replay_agent_ids (list): List of agent IDs whose transitions will be stored in internal replay buffers. - If it is None, it will be set to all agents in the environment (i.e., env.agent_idx_list). Defaults - to None. - get_experience_func (Callable): Custom function to convert the replay buffer to training experiences. Defaults - to None, in which case the replay buffer will be converted directly to SARS experiences for each agent. - post_step (Callable): Custom function to gather information about a transition and the evolvement of the - environment. The function signature should be (env, tracker, transition) -> None, where env is the ``Env`` - instance in the wrapper, tracker is a dictionary where the gathered information is stored and transition - is a ``Transition`` object. For example, this callback can be used to collect various statistics on the - simulation. Defaults to None. - """ - def __init__( - self, - env: Env, - reward_eval_delay: int = 0, - replay_agent_ids: list = None, - get_experience_func: Callable = None, - post_step: Callable = None - ): - self.env = env - self.reward_eval_delay = reward_eval_delay - - self._get_experience_func = get_experience_func - self._post_step = post_step - - replay_agent_ids = self.env.agent_idx_list if replay_agent_ids is None else replay_agent_ids - self._replay_buffer = {agent_id: defaultdict(list) for agent_id in replay_agent_ids} - self._transition_cache = deque() # list of (state, action, tick) whose rewards have yet to be evaluated - self._step_index = None - self._event = None # the latest decision event. This is not used if the env wrapper is not event driven. - self._state = None # the latest extracted state is kept here - - self.tracker = {} # User-defined tracking information is placed here. - self._replay = True - - @property - def step_index(self): - """Number of environmental steps taken so far.""" - return self._step_index - - @property - def agent_idx_list(self): - return self.env.agent_idx_list - - @property - def summary(self): - return self.env.metrics - - @property - def state(self): - """The current environmental state.""" - return self._state - - @property - def event(self): - return self._event - - def collect(self): - self._replay = True - - def evaluate(self): - self._replay = False - - def start(self): - """Generate the initial environmental state at the beginning of a simulation episode.""" - self._step_index = 0 - _, self._event, _ = self.env.step(None) - self._state = self.get_state(self.env.tick) - - @abstractmethod - def get_state(self, tick: int = None) -> dict: - """Compute the state for a given tick. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - - Returns: - A dictionary with (agent ID, state) as key-value pairs. - """ - raise NotImplementedError - - @abstractmethod - def to_env_action(self, action) -> dict: - """Convert policy outputs to an action that can be executed by ``self.env.step()``.""" - raise NotImplementedError - - @abstractmethod - def get_reward(self, actions: list, tick: int = None): - """Evaluate the reward for an action. - - Args: - tick (int): Evaluate the reward for the actions that occured at the given tick. Each action in - ``actions`` must be an Action object defined for the environment in question. The tick may - be None, in which case the reward is evaluated for the latest action (i.e., immediate reward). - Defaults to None. - - Returns: - A dictionary with (agent ID, reward) as key-value pairs. - """ - raise NotImplementedError - - def get_transition_info(self, tick: int = None): - """Get additional info for a transition. - - The returned transition info will be stored in the experience manager alongside states, actions, rewards. - - Args: - tick (int): The tick for which to compute the environmental state. If computing the current state, - use tick=self.env.tick. - - Returns: - A dictionary with (agent ID, transition_info) as key-value pairs. - - """ - pass - - def step(self, action_by_agent: dict): - """Wrapper for env.step(). - - The new transition is stored in the replay buffer or cached in a separate data structure if the - reward cannot be determined yet due to a non-zero ``reward_eval_delay``. - """ - self._step_index += 1 - env_action = self.to_env_action(action_by_agent) - - self._transition_cache.append(( - self._state, - action_by_agent, - env_action, - self.get_transition_info(), - self.env.tick - )) - - _, self._event, done = self.env.step(env_action) - - if not done: - self._state = self.get_state(self.env.tick) # current env state - else: - self._state = None - - """ - If this is the final step, evaluate rewards for all remaining events except the last. - Otherwise, evaluate rewards only for events at least self.reward_eval_delay ticks ago. - """ - while ( - self._transition_cache and - (done or self.env.tick - self._transition_cache[0][-1] >= self.reward_eval_delay) - ): - state, action, env_action, info, tick = self._transition_cache.popleft() - reward = self.get_reward(env_action, tick=tick) - if self._post_step: - next_state = self._transition_cache[0][0] if self._transition_cache else None - transition = Transition(state, action, env_action, reward, next_state, info) - # put things you want to track in the tracker attribute - self._post_step(self.env, self.tracker, transition) - - if self._replay: - for agent_id, agent_state in state.items(): - if agent_id in self._replay_buffer: - buf = self._replay_buffer[agent_id] - buf["states"].append(agent_state) - buf["actions"].append(action[agent_id]) - buf["rewards"].append(reward[agent_id]) - buf["info"].append(info[agent_id] if info else None) - - def get_experiences(self): - """Get per-agent experiences from the replay buffer.""" - if not self._get_experience_func: - exp_by_agent = { - agent_id: ExperienceSet( - buf["states"][:-1], - buf["actions"][:-1], - buf["rewards"][:-1], - buf["states"][1:], - buf["info"][:-1], - ) for agent_id, buf in self._replay_buffer.items() - } - else: - exp_by_agent = self._get_experience_func(self._replay_buffer) - - # clear the replay buffer of transitions that have already been converted to experiences. - for buf in self._replay_buffer.values(): - del buf["states"][:-1] - del buf["actions"][:-1] - del buf["rewards"][:-1] - del buf["info"][:-1] - - return exp_by_agent - - def reset(self): - self.env.reset() - self._state = None - self._transition_cache.clear() - self.tracker.clear() - for replay in self._replay_buffer.values(): - replay.clear() diff --git a/maro/rl/learning/helpers.py b/maro/rl/learning/helpers.py deleted file mode 100644 index 00b5e2196..000000000 --- a/maro/rl/learning/helpers.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -def get_rollout_finish_msg(ep, step_range, exploration_params=None): - """Generate a brief summary message for a finished roll-out""" - if exploration_params: - exploration_params = {policy_id: params for policy_id, params in exploration_params.items() if params} - if exploration_params: - return ( - f"Roll-out finished (episode {ep}, " - f"step range: {step_range}, exploration parameters: {exploration_params})" - ) - else: - return f"Roll-out finished (episode: {ep}, step range: {step_range})" diff --git a/maro/rl/learning/policy_manager.py b/maro/rl/learning/policy_manager.py deleted file mode 100644 index f64d727b1..000000000 --- a/maro/rl/learning/policy_manager.py +++ /dev/null @@ -1,447 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -import time -from abc import ABC, abstractmethod -from collections import defaultdict -from multiprocessing import Pipe, Process -from os import getcwd, getpid -from typing import Callable, Dict, List - -from maro.communication import Proxy, SessionMessage, SessionType -from maro.rl.policy import RLPolicy, WorkerAllocator -from maro.rl.utils import MsgKey, MsgTag -from maro.utils import Logger - -# default group name for the cluster consisting of a policy manager and all policy hosts. -# If data parallelism is enabled, the gradient workers will also belong in this group. -DEFAULT_POLICY_GROUP = "policy_group_default" - - -class AbsPolicyManager(ABC): - """Facility that controls policy update and serves the latest policy states.""" - def __init__(self): - super().__init__() - - @abstractmethod - def update(self, rollout_info: Dict[str, list]): - """Update policies using roll-out information. - - The roll-out information is grouped by policy name and may be either a training batch or a list of loss - information dictionaries computed directly by roll-out workers. - """ - raise NotImplementedError - - @abstractmethod - def get_state(self): - """Get the latest policy states.""" - raise NotImplementedError - - @abstractmethod - def get_version(self): - """Get the collective policy version.""" - raise NotImplementedError - - def server(self, group: str, num_actors: int, max_lag: int = 0, proxy_kwargs: dict = {}, log_dir: str = getcwd()): - """Run a server process. - - The process serves the latest policy states to a set of remote actors and receives simulated experiences from - them. - - Args: - group (str): Group name for the cluster that includes the server and all actors. - num_actors (int): Number of remote actors to collect simulation experiences. - max_lag (int): Maximum policy version lag allowed for experiences collected from remote actors. Experiences - collected using policy versions older than (current_version - max_lag) will be discarded. Defaults to 0, - in which case only experiences collected using the latest policy version will be returned. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to an empty dictionary. - log_dir (str): Directory to store logs in. Defaults to the current working directory. - """ - peers = {"actor": num_actors} - name = "POLICY_SERVER" - proxy = Proxy(group, "policy_server", peers, component_name=name, **proxy_kwargs) - logger = Logger(name, dump_folder=log_dir) - - num_active_actors = num_actors - for msg in proxy.receive(): - if msg.tag == MsgTag.GET_INITIAL_POLICY_STATE: - proxy.reply( - msg, tag=MsgTag.POLICY_STATE, - body={MsgKey.POLICY_STATE: self.get_state(), MsgKey.VERSION: self.get_version()} - ) - elif msg.tag == MsgTag.SAMPLE_DONE: - if self.get_version() - msg.body[MsgKey.VERSION] > max_lag: - logger.info( - f"Ignored a message because it contains experiences generated using a stale policy version. " - f"Expected experiences generated using policy versions no earlier than " - f"{self.get_version() - max_lag}, got {msg.body[MsgKey.VERSION]}" - ) - else: - self.update(msg.body[MsgKey.ROLLOUT_INFO]) - proxy.reply( - msg, tag=MsgTag.POLICY_STATE, - body={MsgKey.POLICY_STATE: self.get_state(), MsgKey.VERSION: self.get_version()} - ) - elif msg.tag == MsgTag.DONE: - num_active_actors -= 1 - if num_active_actors == 0: - proxy.close() - return - - -class SimplePolicyManager(AbsPolicyManager): - """Policy manager that contains all policy instances. - - Args: - create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a - function that takes policy name as the only parameter and return an ``RLPolicy`` instance. - load_dir (str): If provided, policies whose IDs are in the dictionary keys will load the states - from the corresponding path. Defaults to None. - checkpoint_every (int): The policies will be checkpointed (i.e., persisted to disk) every this number of seconds - only if there are updates since the last checkpoint. This must be a positive integer or -1, with -1 meaning - no checkpointing. Defaults to -1. - checkpoint_dir (str): The directory under which to checkpoint the policy states. - worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. - If not None, the policies will be trained in data-parallel mode. Defaults to None. - group (str): Group name for the cluster consisting of the manager and all policy hosts. Ignored if - ``worker_allocator`` is None. Defaults to DEFAULT_POLICY_GROUP. - proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - load_dir: str = None, - checkpoint_dir: str = None, - worker_allocator: WorkerAllocator = None, - group: str = DEFAULT_POLICY_GROUP, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - super().__init__() - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - self._policy_dict = {name: func(name) for name, func in create_policy_func_dict.items()} - if load_dir: - for id_, policy in self._policy_dict.items(): - path = os.path.join(load_dir, id_) - if os.path.exists(path): - policy.load(path) - self._logger.info(f"Loaded policy {id_} from {path}") - - self._version = 0 - - # auto-checkpointing - if checkpoint_dir: - self.checkpoint_path = {id_: os.path.join(checkpoint_dir, id_) for id_ in self._policy_dict} - else: - self.checkpoint_path = None - - # data parallelism - self._worker_allocator = worker_allocator - if self._worker_allocator: - self._num_grad_workers = worker_allocator.num_workers - self._worker_allocator.set_logger(self._logger) - self._proxy = Proxy( - group, "policy_manager", {"grad_worker": self._num_grad_workers}, - component_name="POLICY_MANAGER", **proxy_kwargs - ) - - for name in create_policy_func_dict: - self._policy_dict[name].data_parallel( - group, "policy_host", {"grad_worker": self._num_grad_workers}, - component_name=f"POLICY_HOST.{name}", **proxy_kwargs) - - def update(self, rollout_info: Dict[str, list]): - """Update policies using roll-out information. - - The roll-out information is grouped by policy name and may be either a training batch or a list of loss - information dictionaries computed directly by roll-out workers. - """ - t0 = time.time() - for policy_id, info in rollout_info.items(): - if isinstance(info, list): - self._policy_dict[policy_id].update(info) - else: - self._policy_dict[policy_id].learn(info) - - if self.checkpoint_path: - self._policy_dict[policy_id].save(self.checkpoint_path[policy_id]) - self._logger.info(f"Saved policy {policy_id} to {self.checkpoint_path[policy_id]}") - - self._version += 1 - - self._logger.info(f"Updated policies {list(rollout_info.keys())}") - self._logger.info(f"policy update time: {time.time() - t0}") - - def get_state(self): - """Get the latest policy states.""" - return {name: policy.get_state() for name, policy in self._policy_dict.items()} - - def get_version(self): - """Get the collective policy version.""" - return self._version - - def exit(self): - pass - - -class MultiProcessPolicyManager(AbsPolicyManager): - """Policy manager that places each policy instance in a separate process. - - Args: - create_policy_func_dict (dict): Dictionary that maps policy names to policy creators. A policy creator is a - function that takes policy name as the only parameter and return an ``RLPolicy`` instance. - load_dir (str): If provided, policies whose IDs are in the dictionary keys will load the states - from the corresponding path. Defaults to None. - checkpoint_dir (str): The directory under which to checkpoint the policy states. Defaults to None, in which case - no checkpointing will be performed. - worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. - If not None, the policies will be trained in data-parallel mode. Defaults to None. - group (str): Group name for the cluster consisting of the manager and all policy hosts. Ignored if - ``worker_allocator`` is None. Defaults to DEFAULT_POLICY_GROUP. - proxy_kwargs (dict): Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class for details. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - create_policy_func_dict: Dict[str, Callable[[str], RLPolicy]], - load_dir: Dict[str, str] = None, - checkpoint_dir: str = None, - worker_allocator: WorkerAllocator = None, - group: str = DEFAULT_POLICY_GROUP, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - super().__init__() - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - - # data parallelism - self._worker_allocator = worker_allocator is not None - if self._worker_allocator: - self._num_grad_workers = worker_allocator.num_workers - self._worker_allocator = worker_allocator - self._worker_allocator.set_logger(self._logger) - self._proxy = Proxy( - group, "policy_manager", {"grad_worker": self._num_grad_workers}, - component_name="POLICY_MANAGER", **proxy_kwargs - ) - - self._state_cache = {} - self._policy_hosts = [] - self._manager_end = {} - self._logger.info("Spawning policy host processes") - - def policy_host(id_, create_policy_func, conn): - self._logger.info(f"Host for policy {id_} started with PID {getpid()}") - policy = create_policy_func(id_) - checkpoint_path = os.path.join(checkpoint_dir, id_) if checkpoint_dir else None - if self._worker_allocator: - self._logger.info("========== data parallel mode ==========") - policy.data_parallel( - group, "policy_host", {"grad_worker": self._num_grad_workers}, component_name=f"POLICY_HOST.{id_}", - **proxy_kwargs) - - if load_dir: - load_path = os.path.join(load_dir, id_) if load_dir else None - if os.path.exists(load_path): - policy.load(load_path) - self._logger.info(f"Loaded policy {id_} from {load_path}") - - conn.send({"type": "init", "policy_state": policy.get_state()}) - while True: - msg = conn.recv() - if msg["type"] == "learn": - info = msg["rollout_info"] - if isinstance(info, list): - policy.update(info) - elif self._worker_allocator: - policy.learn_with_data_parallel(info, msg["workers"]) - else: - policy.learn(info) - conn.send({"type": "learn_done", "policy_state": policy.get_state()}) - self._logger.info("learning finished") - if checkpoint_path: - policy.save(checkpoint_path) - self._logger.info(f"Saved policy {id_} to {checkpoint_path}") - elif msg["type"] == "quit": - if self._worker_allocator: - policy.exit_data_parallel() - policy.save(checkpoint_path) - self._logger.info(f"Saved policy {id_} to {checkpoint_path}") - break - - for id_, create_policy_func in create_policy_func_dict.items(): - manager_end, host_end = Pipe() - self._manager_end[id_] = manager_end - host = Process(target=policy_host, args=(id_, create_policy_func, host_end)) - self._policy_hosts.append(host) - host.start() - - for policy_id, conn in self._manager_end.items(): - msg = conn.recv() - if msg["type"] == "init": - self._state_cache[policy_id] = msg["policy_state"] - self._logger.info(f"Initial state for policy {policy_id} cached") - - self._version = 0 - - def update(self, rollout_info: Dict[str, list]): - """Update policies using roll-out information. - - The roll-out information is grouped by policy name and may be either a training batch or a list of loss - information dictionaries computed directly by roll-out workers. - """ - if self._worker_allocator: - # re-allocate grad workers before update. - self._policy2workers, self._worker2policies = self._worker_allocator.allocate() - - for policy_id, info in rollout_info.items(): - msg = {"type": "learn", "rollout_info": info} - if self._worker_allocator: - msg["workers"] = self._policy2workers[policy_id] - self._manager_end[policy_id].send(msg) - for policy_id, conn in self._manager_end.items(): - msg = conn.recv() - if msg["type"] == "learn_done": - self._state_cache[policy_id] = msg["policy_state"] - self._logger.info(f"Cached state for policy {policy_id}") - self._version += 1 - else: - self._logger.info(f"Warning: Wrong message type: {msg['type']}") - - self._logger.info(f"Updated policies {list(rollout_info.keys())}") - - def get_state(self): - """Get the latest policy states.""" - return self._state_cache - - def get_version(self): - """Get the collective policy version.""" - return self._version - - def exit(self): - """Tell the policy host processes to exit.""" - for conn in self._manager_end.values(): - conn.send({"type": "quit"}) - if self._worker_allocator: - self._proxy.close() - - -class DistributedPolicyManager(AbsPolicyManager): - """Policy manager that communicates with a set of remote nodes that house the policy instances. - - Args: - policy_ids (List[str]): Names of the registered policies. - num_hosts (int): Number of hosts. The hosts will be identified by "POLICY_HOST.i", where 0 <= i < num_hosts. - group (str): Group name for the cluster consisting of the manager and all policy hosts. If ``worker_allocator`` - is provided, the gradient workers will also belong to the same cluster. Defaults to - DEFAULT_POLICY_GROUP. - worker_allocator (WorkerAllocator): Strategy to select gradient workers for policies for send gradient tasks to. - If not None, the policies will be trained in data-parallel mode. Defaults to None. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to an empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "POLICY_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - policy_ids: List[str], - num_hosts: int, - group: str = DEFAULT_POLICY_GROUP, - worker_allocator: WorkerAllocator = None, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - super().__init__() - self._worker_allocator = worker_allocator - peers = {"policy_host": num_hosts} - if self._worker_allocator: - peers["grad_worker"] = worker_allocator.num_workers - self._proxy = Proxy(group, "policy_manager", peers, component_name="POLICY_MANAGER", **proxy_kwargs) - self._logger = Logger("POLICY_MANAGER", dump_folder=log_dir) - self._worker_allocator = worker_allocator - - if self._worker_allocator: - self._worker_allocator.set_logger(self._logger) - - self._policy2host = {} - self._policy2workers = {} - self._host2policies = defaultdict(list) - self._worker2policies = defaultdict(list) - - # assign policies to hosts - for i, name in enumerate(policy_ids): - host_id = i % num_hosts - self._policy2host[name] = f"POLICY_HOST.{host_id}" - self._host2policies[f"POLICY_HOST.{host_id}"].append(name) - - self._logger.info(f"Policy assignment: {self._policy2host}") - - # ask the hosts to initialize the assigned policies - for host_name, policy_ids in self._host2policies.items(): - self._proxy.isend(SessionMessage( - MsgTag.INIT_POLICIES, self._proxy.name, host_name, body={MsgKey.POLICY_IDS: policy_ids} - )) - - # cache the initial policy states - self._state_cache, dones = {}, 0 - for msg in self._proxy.receive(): - if msg.tag == MsgTag.INIT_POLICIES_DONE: - for policy_id, policy_state in msg.body[MsgKey.POLICY_STATE].items(): - self._state_cache[policy_id] = policy_state - self._logger.info(f"Cached state for policy {policy_id}") - dones += 1 - if dones == num_hosts: - break - - self._version = 0 - - def update(self, rollout_info: Dict[str, list]): - """Update policies using roll-out information. - - The roll-out information is grouped by policy name and may be either a training batch or a list if loss - information dictionaries computed directly by roll-out workers. - """ - if self._worker_allocator: - self._policy2workers, self._worker2policies = self._worker_allocator.allocate() - - msg_dict = defaultdict(lambda: defaultdict(dict)) - for policy_id, info_list in rollout_info.items(): - host_id_str = self._policy2host[policy_id] - msg_dict[host_id_str][MsgKey.ROLLOUT_INFO][policy_id] = info_list - msg_dict[host_id_str][MsgKey.WORKER_INFO][policy_id] = self._policy2workers - - dones = 0 - self._proxy.iscatter(MsgTag.LEARN, SessionType.TASK, list(msg_dict.items())) - for msg in self._proxy.receive(): - if msg.tag == MsgTag.LEARN_DONE: - for policy_id, policy_state in msg.body[MsgKey.POLICY_STATE].items(): - self._state_cache[policy_id] = policy_state - self._logger.info(f"Cached state for policy {policy_id}") - dones += 1 - if dones == len(msg_dict): - break - - self._version += 1 - self._logger.info(f"Updated policies {list(rollout_info.keys())}") - - def get_state(self): - """Get the latest policy states.""" - return self._state_cache - - def get_version(self): - """Get the collective policy version.""" - return self._version - - def exit(self): - """Tell the remote policy hosts to exit.""" - self._proxy.ibroadcast("policy_host", MsgTag.EXIT, SessionType.NOTIFICATION) - self._proxy.close() - self._logger.info("Exiting...") diff --git a/maro/rl/learning/rollout_manager.py b/maro/rl/learning/rollout_manager.py deleted file mode 100644 index e7243eb98..000000000 --- a/maro/rl/learning/rollout_manager.py +++ /dev/null @@ -1,378 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from collections import defaultdict -from multiprocessing import Pipe, Process -from os import getcwd, getpid -from random import choices -from typing import Callable, Dict, List, Tuple - -import numpy as np - -from maro.communication import Proxy, SessionType -from maro.rl.utils import MsgKey, MsgTag -from maro.utils import Logger, set_seeds - -from .env_sampler import AbsEnvSampler -from .helpers import get_rollout_finish_msg - - -def concat_batches(batch_list: List[dict]): - return {key: np.concatenate([batch[key] for batch in batch_list]) for key in batch_list[0]} - - -class AbsRolloutManager(ABC): - """Controller for simulation data collection.""" - def __init__(self): - super().__init__() - self.end_of_episode = False - - @abstractmethod - def collect(self, ep: int, segment: int, policy_state_dict: dict) -> Tuple[Dict, List[Dict]]: - """Collect simulation data, i.e., experiences for training. - - Args: - ep (int): Current episode. - segment (int): Current segment. - policy_state_dict (dict): Policy states to use for collecting training info. - - Returns: - A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries - containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. - An RL policy's roll-out information must be either loss information or a data batch that can be passed to - the policy's ``update`` or ``learn``, respectively. - """ - raise NotImplementedError - - @abstractmethod - def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate policy performance. - - Args: - ep (int): Current training episode. - policy_state_dict (dict): Policy states to use for evaluation. - - Returns: - A list of dictionaries containing step-level information collected by the user-defined ``post_step`` - callback in ``AbsEnvSampler`` for evaluation purposes. - """ - raise NotImplementedError - - def reset(self): - self.end_of_episode = False - - def exit(self): - pass - - -class MultiProcessRolloutManager(AbsRolloutManager): - """Local roll-out controller. - - Args: - get_env_sampler (Callable): Function to create an environment sampler for collecting training data. The function - should take no parameters and return an ``AbsEnvSampler`` instance. - num_rollouts (int): Number of processes to spawn for parallel roll-out. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - num_eval_rollout (int): Number of roll-out processes to use for evaluation. Defaults to 1. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - get_env_sampler: Callable[[], AbsEnvSampler], - num_rollouts: int, - num_steps: int = -1, - num_eval_rollouts: int = 1, - log_dir: str = getcwd() - ): - if num_steps == 0 or num_steps < -1: - raise ValueError("num_steps must be a positive integer or -1.") - - if num_rollouts <= 1: - raise ValueError("'num_rollouts' must be greater than 1.") - - if num_eval_rollouts > num_rollouts: - raise ValueError("'num_eval_rollouts' can not be greater than 'num_rollouts'.") - - super().__init__() - self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) - self._num_steps = num_steps if num_steps > 0 else float("inf") - self._num_rollouts = num_rollouts - self._num_eval_rollouts = num_eval_rollouts - self._worker_processes = [] - self._manager_ends = [] - - def _rollout_worker(index, conn, get_env_sampler): - set_seeds(index) - env_sampler = get_env_sampler() - logger = Logger("ROLLOUT_WORKER", dump_folder=log_dir) - logger.info(f"Roll-out worker {index} started with PID {getpid()}") - while True: - msg = conn.recv() - if msg["type"] == "sample": - result = env_sampler.sample(policy_state_dict=msg["policy_state"], num_steps=self._num_steps) - logger.info(get_rollout_finish_msg( - msg["episode"], result["step_range"], exploration_params=result["exploration_params"] - )) - result["worker_index"] = index - conn.send(result) - elif msg["type"] == "test": - logger.info("Evaluating...") - tracker = env_sampler.test(msg["policy_state"]) - conn.send({"worker_id": index, "tracker": tracker}) - elif msg["type"] == "quit": - break - - for index in range(self._num_rollouts): - manager_end, worker_end = Pipe() - self._manager_ends.append(manager_end) - worker = Process(target=_rollout_worker, args=(index, worker_end, get_env_sampler)) - self._worker_processes.append(worker) - worker.start() - - def collect(self, ep: int, segment: int, policy_state_dict: dict) -> Tuple[Dict, List[Dict]]: - """Collect simulation data, i.e., experiences for training. - - Args: - ep (int): Current episode. - segment (int): Current segment. - policy_state_dict (dict): Policy states to use for collecting training info. - - Returns: - A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries - containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. - An RL policy's roll-out information must be either loss information or a data batch that can be passed to - the policy's ``update`` or ``learn``, respectively. - """ - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment})") - - info_by_policy, trackers = defaultdict(list), [] - rollout_req = { - "type": "sample", - "episode": ep, - "num_steps": self._num_steps, - "policy_state": policy_state_dict - } - - for conn in self._manager_ends: - conn.send(rollout_req) - - for conn in self._manager_ends: - result = conn.recv() - for policy_id, info in result["rollout_info"].items(): - info_by_policy[policy_id].append(info) - trackers.append(result["tracker"]) - self.end_of_episode = result["end_of_episode"] - - # concat batches from different roll-out workers - for policy_id, info_list in info_by_policy.items(): - if "loss" not in info_list[0]: - info_by_policy[policy_id] = concat_batches(info_list) - - return info_by_policy, trackers - - def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate policy performance. - - Args: - ep (int): Current training episode. - policy_state_dict (dict): Policy states to use for evaluation. - - Returns: - A list of dictionaries containing step-level information collected by the user-defined ``post_step`` - callback in ``AbsEnvSampler`` for evaluation purposes. - """ - trackers = [] - eval_worker_conns = choices(self._manager_ends, k=self._num_eval_rollouts) - for conn in eval_worker_conns: - conn.send({"type": "test", "policy_state": policy_state_dict}) - for conn in eval_worker_conns: - result = conn.recv() - trackers.append(result["tracker"]) - - return trackers - - def exit(self): - """Tell the worker processes to exit.""" - for conn in self._manager_ends: - conn.send({"type": "quit"}) - - -class DistributedRolloutManager(AbsRolloutManager): - """Controller for a set of remote roll-out workers, possibly distributed on different computation nodes. - - Args: - group (str): Group name for the roll-out cluster, which includes all roll-out workers and a roll-out manager - that manages them. - num_workers (int): Number of remote roll-out workers. - num_steps (int): Number of environment steps to roll out in each call to ``collect``. Defaults to -1, in which - case the roll-out will be executed until the end of the environment. - min_finished_workers (int): Minimum number of finished workers required for a ``collect`` call. Defaults to - None, in which case it will be set to ``num_workers``. - max_extra_recv_tries (int): Maximum number of attempts to receive worker results after ``min_finished_workers`` - have been received in ``collect``. Defaults to 0. - extra_recv_timeout (int): Timeout (in milliseconds) for each attempt to receive from a worker after - ``min_finished_workers`` have been received in ``collect``. Defaults to 100 (milliseconds). - num_eval_workers (int): Number of workers for evaluation. Defaults to 1. - proxy_kwargs: Keyword parameters for the internal ``Proxy`` instance. See ``Proxy`` class - for details. Defaults to the empty dictionary. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "ROLLOUT_MANAGER" will be created at init - time and this directory will be used to save the log files generated by it. Defaults to the current - working directory. - """ - def __init__( - self, - group: str, - num_workers: int, - num_steps: int = -1, - min_finished_workers: int = None, - max_extra_recv_tries: int = 0, - extra_recv_timeout: int = 100, - max_lag: Dict[str, int] = defaultdict(int), - num_eval_workers: int = 1, - proxy_kwargs: dict = {}, - log_dir: str = getcwd() - ): - if num_eval_workers > num_workers: - raise ValueError("num_eval_workers cannot exceed the number of available workers") - - super().__init__() - self._num_workers = num_workers - peers = {"rollout_worker": num_workers} - self._proxy = Proxy(group, "rollout_manager", peers, component_name="ROLLOUT_MANAGER", **proxy_kwargs) - self._workers = self._proxy.peers["rollout_worker"] # remote roll-out worker ID's - self._logger = Logger("ROLLOUT_MANAGER", dump_folder=log_dir) - - self._num_steps = num_steps - if min_finished_workers is None: - min_finished_workers = self._num_workers - self._logger.info(f"Minimum number of finished workers is set to {min_finished_workers}") - - self._min_finished_workers = min_finished_workers - - if max_extra_recv_tries is None: - max_extra_recv_tries = self._num_workers - self._min_finished_workers - self._logger.info(f"Maximum number of extra receive tries is set to {max_extra_recv_tries}") - - self._max_extra_recv_tries = max_extra_recv_tries - self._extra_recv_timeout = extra_recv_timeout - - self._max_lag = max_lag - self._num_eval_workers = num_eval_workers - - def collect(self, ep: int, segment: int, policy_state_dict: dict) -> Tuple[Dict, List[Dict]]: - """Collect simulation data, i.e., experiences for training. - - Args: - ep (int): Current episode. - segment (int): Current segment. - policy_state_dict (dict): Policy states to use for collecting training info. - - Returns: - A 2-tuple consisting of a dictionary of roll-out information grouped by policy ID and a list of dictionaries - containing step-level information collected by the user-defined ``post_step`` callback in ``AbsEnvSampler``. - An RL policy's roll-out information must be either loss information or a data batch that can be passed to - the policy's ``update`` or ``learn``, respectively. - """ - msg_body = { - MsgKey.EPISODE: ep, - MsgKey.SEGMENT: segment, - MsgKey.NUM_STEPS: self._num_steps, - MsgKey.POLICY_STATE: policy_state_dict - } - - self._proxy.iscatter(MsgTag.SAMPLE, SessionType.TASK, [(worker_id, msg_body) for worker_id in self._workers]) - self._logger.info(f"Collecting simulation data (episode {ep}, segment {segment})") - - info_by_policy, trackers, num_finishes = defaultdict(list), [], 0 - # Ensure the minimum number of worker results are received. - for msg in self._proxy.receive(): - rollout_info, tracker = self._handle_worker_result(msg, ep, segment) - if rollout_info: - num_finishes += 1 - for policy_id, info in rollout_info.items(): - info_by_policy[policy_id].append(info) - trackers.append(tracker) - if num_finishes == self._min_finished_workers: - break - - # Keep trying to receive from workers, but with timeout - for i in range(self._max_extra_recv_tries): - msg = self._proxy.receive_once(timeout=self._extra_recv_timeout) - if not msg: - self._logger.info(f"Receive timeout, {self._max_extra_recv_tries - i - 1} attempts left") - else: - rollout_info, tracker = self._handle_worker_result(msg, ep, segment) - if rollout_info: - num_finishes += 1 - for policy_id, info in rollout_info.items(): - info_by_policy[policy_id].append(info) - trackers.append(tracker) - if num_finishes == self._num_workers: - break - - # concat batches from different roll-out workers - for policy_id, info_list in info_by_policy.items(): - if "loss" not in info_list[0]: - info_by_policy[policy_id] = concat_batches(info_list) - - return info_by_policy, trackers - - def _handle_worker_result(self, msg, ep, segment): - if msg.tag != MsgTag.SAMPLE_DONE: - self._logger.info( - f"Ignored a message of type {msg.tag} (expected message type {MsgTag.SAMPLE_DONE})" - ) - return None, None - - # The message is what we expect - if msg.body[MsgKey.EPISODE] == ep and msg.body[MsgKey.SEGMENT] == segment: - self.end_of_episode = msg.body[MsgKey.END_OF_EPISODE] - return msg.body[MsgKey.ROLLOUT_INFO], msg.body[MsgKey.TRACKER] - - return None, None - - def evaluate(self, ep: int, policy_state_dict: dict): - """Evaluate policy performance. - - Args: - ep (int): Current training episode. - policy_state_dict (dict): Policy states to use for evaluation. - - Returns: - A list of dictionaries containing step-level information collected by the user-defined ``post_step`` - callback in ``AbsEnvSampler`` for evaluation purposes. - """ - msg_body = {MsgKey.EPISODE: ep, MsgKey.POLICY_STATE: policy_state_dict} - - workers = choices(self._workers, k=self._num_eval_workers) - self._proxy.iscatter(MsgTag.TEST, SessionType.TASK, [(worker_id, msg_body) for worker_id in workers]) - self._logger.info(f"Sent evaluation requests to {workers}") - - # Receive roll-out results from remote workers - num_finishes = 0 - trackers = [] - for msg in self._proxy.receive(): - if msg.tag != MsgTag.TEST_DONE or msg.body[MsgKey.EPISODE] != ep: - self._logger.info( - f"Ignore a message of type {msg.tag} with episode {msg.body[MsgKey.EPISODE]} " - f"(expected message type {MsgTag.TEST_DONE} and episode {ep})" - ) - continue - - trackers.append(msg.body[MsgKey.TRACKER]) - if msg.body[MsgKey.EPISODE] == ep: - num_finishes += 1 - if num_finishes == self._num_eval_workers: - break - - return trackers - - def exit(self): - """Tell the remote workers to exit.""" - self._proxy.ibroadcast("rollout_worker", MsgTag.EXIT, SessionType.NOTIFICATION) - self._proxy.close() - self._logger.info("Exiting...") diff --git a/maro/rl/learning/synchronous/learner.py b/maro/rl/learning/synchronous/learner.py deleted file mode 100644 index b336534a0..000000000 --- a/maro/rl/learning/synchronous/learner.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import getcwd -from typing import List, Union - -from maro.rl.policy import AbsPolicyManager -from maro.utils import Logger - -from ..early_stopper import AbsEarlyStopper -from .rollout_manager import AbsRolloutManager - - -class Learner: - """Main controller for learning. - - This should be used in multi-process or distributed settings where either the policy manager or the roll-out - manager has a distributed architecture. For pure local learning workflows, using this may cause pitfalls such - as duplicate experience storage. Use ``SimpleLearner`` instead. - - Args: - policy_manager (AbsPolicyManager): An ``AbsPolicyManager`` instance that controls policy updates. - rollout_manager (AbsRolloutManager): An ``AbsRolloutManager`` instance that controls simulation data - collection. - num_episodes (int): Number of training episodes. Each training episode may contain one or more - collect-update cycles, depending on the implementation of the roll-out manager. - eval_schedule (Union[int, List[int]]): Evaluation schedule. If an integer is provided, the policies will - will be evaluated every ``eval_schedule`` episodes. If a list is provided, the policies will be evaluated - at the end of the training episodes given in the list. In any case, the policies will be evaluated - at the end of the last training episode. Defaults to None, in which case the policies will only be - evaluated after the last training episode. - early_stopper (AbsEarlyStopper): Early stopper to stop the main training loop if certain conditions on the - environment metric are met following an evaluation episode. Default to None. - log_dir (str): Directory to store logs in. A ``Logger`` with tag "LEARNER" will be created at init time - and this directory will be used to save the log files generated by it. Defaults to the current working - directory. - """ - def __init__( - self, - policy_manager: AbsPolicyManager, - rollout_manager: AbsRolloutManager, - num_episodes: int, - eval_schedule: Union[int, List[int]] = None, - early_stopper: AbsEarlyStopper = None, - log_dir: str = getcwd() - ): - self._logger = Logger("LEARNER", dump_folder=log_dir) - self.policy_manager = policy_manager - self.rollout_manager = rollout_manager - - self.num_episodes = num_episodes - - # evaluation schedule - if eval_schedule is None: - self._eval_schedule = [] - elif isinstance(eval_schedule, int): - num_eval_schedule = num_episodes // eval_schedule - self._eval_schedule = [eval_schedule * i for i in range(1, num_eval_schedule + 1)] - else: - self._eval_schedule = eval_schedule - self._eval_schedule.sort() - - # always evaluate after the last episode - # if not self._eval_schedule or num_episodes != self._eval_schedule[-1]: - # self._eval_schedule.append(num_episodes) - - self._logger.info(f"Policy will be evaluated at the end of episodes {self._eval_schedule}") - self._eval_point_index = 0 - - self.early_stopper = early_stopper - - def run(self): - """Entry point for executing a learning workflow.""" - for ep in range(1, self.num_episodes + 1): - self._collect_and_update(ep) - if ep == self._eval_schedule[self._eval_point_index]: - self._eval_point_index += 1 - env_metric_dict = self.rollout_manager.evaluate(ep, self.policy_manager.get_state()) - # early stopping check - if self.early_stopper: - for env_metric in env_metric_dict.values(): - self.early_stopper.push(env_metric) - if self.early_stopper.stop(): - return - - if hasattr(self.rollout_manager, "exit"): - self.rollout_manager.exit() - - if hasattr(self.policy_manager, "exit"): - self.policy_manager.exit() - - def _collect_and_update(self, ep: int): - collect_time = policy_update_time = num_experiences_collected = 0 - segment = 0 - self.rollout_manager.reset() - while not self.rollout_manager.episode_complete: - segment += 1 - # experience collection - policy_state_dict = self.policy_manager.get_state() - policy_version = self.policy_manager.version - tc0 = time.time() - exp_by_policy = self.rollout_manager.collect(ep, segment, policy_state_dict, policy_version) - collect_time += time.time() - tc0 - tu0 = time.time() - self.policy_manager.update(exp_by_policy) - policy_update_time += time.time() - tu0 - num_experiences_collected += sum(exp.size for exp in exp_by_policy.values()) - - # performance details - self._logger.info( - f"ep {ep} summary - " - f"experiences collected: {num_experiences_collected} " - f"experience collection time: {collect_time} " - f"policy update time: {policy_update_time}" - ) diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py new file mode 100644 index 000000000..7f79d1cb8 --- /dev/null +++ b/maro/rl/model/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .abs_net import AbsNet +from .fc_block import FullyConnected +from .multi_q_net import MultiQNet +from .policy_net import ContinuousPolicyNet, DiscretePolicyNet, PolicyNet +from .q_net import ContinuousQNet, DiscreteQNet, QNet +from .v_net import VNet + +__all__ = [ + "AbsNet", + "FullyConnected", + "MultiQNet", + "ContinuousPolicyNet", "DiscretePolicyNet", "PolicyNet", + "ContinuousQNet", "DiscreteQNet", "QNet", + "VNet", +] diff --git a/maro/rl/model/abs_net.py b/maro/rl/model/abs_net.py new file mode 100644 index 000000000..8c309dda4 --- /dev/null +++ b/maro/rl/model/abs_net.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from typing import Any, Dict + +import torch.nn + + +class AbsNet(torch.nn.Module, metaclass=ABCMeta): + """Base class for all Torch net classes. `AbsNet` defines a set of methods that will be called by upper-level + logic. All classes that inherit `AbsNet` should implement these methods. + """ + + def __init__(self) -> None: + super(AbsNet, self).__init__() + + @abstractmethod + def step(self, loss: torch.Tensor) -> None: + """Run a training step to update the net's parameters according to the given loss. + + Args: + loss (torch.tensor): Loss used to update the model. + """ + raise NotImplementedError + + @abstractmethod + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + """Get the gradients with respect to all parameters according to the given loss. + + Args: + loss (torch.tensor): Loss used to compute gradients. + + Returns: + Gradients (Dict[str, torch.Tensor]): A dict that contains gradients for all parameters. + """ + raise NotImplementedError + + @abstractmethod + def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + """Apply gradients to the net to update all parameters. + + Args: + grad (Dict[str, torch.Tensor]): A dict that contains gradients for all parameters. + """ + raise NotImplementedError + + def _forward_unimplemented(self, *input: Any) -> None: + pass + + @abstractmethod + def get_state(self) -> object: + """Get the net's state. + + Returns: + state (object): A object that contains the net's state. + """ + raise NotImplementedError + + @abstractmethod + def set_state(self, net_state: object) -> None: + """Set the net's state. + + Args: + net_state (object): A object that contains the net's state. + """ + raise NotImplementedError + + def soft_update(self, other_model: AbsNet, tau: float) -> None: + """Soft update the net's parameters according to another net, i.e., + self.param = self.param * (1.0 - tau) + other_model.param * tau + + Args: + other_model (AbsNet): The source net. Must has same type with the current net. + tau (float): Soft update coefficient. + """ + assert self.__class__ == other_model.__class__, \ + f"Soft update can only be done between same classes. Current model type: {self.__class__}, " \ + f"other model type: {other_model.__class__}" + + for params, other_params in zip(self.parameters(), other_model.parameters()): + params.data = (1 - tau) * params.data + tau * other_params.data + + @abstractmethod + def freeze(self) -> None: + """(Partially) freeze the current model. The users should write their own strategy to determine which + parameters to freeze. + """ + raise NotImplementedError + + @abstractmethod + def unfreeze(self) -> None: + """(Partially) unfreeze the current model. The users should write their own strategy to determine which + parameters to freeze. + """ + raise NotImplementedError + + def freeze_all_parameters(self) -> None: + """Freeze all parameters. + """ + for p in self.parameters(): + p.requires_grad = False + + def unfreeze_all_parameters(self) -> None: + """Unfreeze all parameters. + """ + for p in self.parameters(): + p.requires_grad = True diff --git a/maro/rl/modeling/fc_block.py b/maro/rl/model/fc_block.py similarity index 61% rename from maro/rl/modeling/fc_block.py rename to maro/rl/model/fc_block.py index 4c3c95fd3..65c803284 100644 --- a/maro/rl/modeling/fc_block.py +++ b/maro/rl/model/fc_block.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. from collections import OrderedDict -from typing import List +from typing import Any, List, Optional, Type import torch import torch.nn as nn @@ -14,37 +14,41 @@ class FullyConnected(nn.Module): Args: input_dim (int): Network input dimension. output_dim (int): Network output dimension. - hidden_dims ([int]): Dimensions of hidden layers. Its length is the number of hidden layers. - activation: A string indicatinfg an activation class provided by ``torch.nn`` or a custom activation class. - If it is a string, it must be a key in ``TORCH_ACTIVATION``. If None, there will be no activation. - Defaults to "relu". - head (bool): If true, this block will be the top block of the full model and the top layer of this block - will be the final output layer. Defaults to False. - softmax (bool): If true, the output of the net will be a softmax transformation of the top layer's - output. Defaults to False. - batch_norm (bool): If true, batch normalization will be performed at each layer. - skip_connection (bool): If true, a skip connection will be built between the bottom (input) layer and - top (output) layer. Defaults to False. - dropout_p (float): Dropout probability. Defaults to None, in which case there is no drop-out. - gradient_threshold (float): Gradient clipping threshold. Defaults to None, in which case not gradient clipping + hidden_dims (List[int]): Dimensions of hidden layers. Its length is the number of hidden layers. For example, + `hidden_dims=[128, 256]` refers to two hidden layers with output dim of 128 and 256, respectively. + activation (Optional[Type[torch.nn.Module], default=nn.ReLU): Activation class provided by ``torch.nn`` or a + customized activation class. If None, there will be no activation. + head (bool, default=False): If true, this block will be the top block of the full model and the top layer + of this block will be the final output layer. + softmax (bool, default=False): If true, the output of the net will be a softmax transformation of the top + layer's output. + batch_norm (bool, default=False): If true, batch normalization will be performed at each layer. + skip_connection (bool, default=False): If true, a skip connection will be built between the bottom (input) + layer and top (output) layer. Defaults to False. + dropout_p (float, default=None): Dropout probability. If it is None, there will be no drop-out. + gradient_threshold (float, default=None): Gradient clipping threshold. If it is None, no gradient clipping is performed. - name (str): Network name. Defaults to None. + name (str, default=None): Network name. """ + + def _forward_unimplemented(self, *input: Any) -> None: + pass + def __init__( self, input_dim: int, output_dim: int, hidden_dims: List[int], - activation=nn.ReLU, + activation: Optional[Type[torch.nn.Module]] = nn.ReLU, head: bool = False, softmax: bool = False, batch_norm: bool = False, skip_connection: bool = False, dropout_p: float = None, gradient_threshold: float = None, - name: str = None - ): - super().__init__() + name: str = None, + ) -> None: + super(FullyConnected, self).__init__() self._input_dim = input_dim self._hidden_dims = hidden_dims if hidden_dims is not None else [] self._output_dim = output_dim @@ -79,25 +83,25 @@ def __init__( self._name = name - def forward(self, x): + def forward(self, x: torch.Tensor) -> torch.Tensor: out = self._net(x) if self._skip_connection: out += x return self._softmax(out) if self._softmax else out @property - def name(self): + def name(self) -> str: return self._name @property - def input_dim(self): + def input_dim(self) -> int: return self._input_dim @property - def output_dim(self): + def output_dim(self) -> int: return self._output_dim - def _build_layer(self, input_dim, output_dim, head: bool = False): + def _build_layer(self, input_dim: int, output_dim: int, head: bool = False) -> torch.nn.Module: """Build a basic layer. BN -> Linear -> Activation -> Dropout diff --git a/maro/rl/model/multi_q_net.py b/maro/rl/model/multi_q_net.py new file mode 100644 index 000000000..104da5ee8 --- /dev/null +++ b/maro/rl/model/multi_q_net.py @@ -0,0 +1,84 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABCMeta, abstractmethod +from typing import List + +import torch + +from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape + +from .abs_net import AbsNet + + +class MultiQNet(AbsNet, metaclass=ABCMeta): + """Abstract net for multi-agent Q functions. + + Args: + state_dim (int): Dimension of states. + action_dims (List[int]): Dimensions of Dimension of multi-agents' actions. Its length equals the + number of agents. + """ + + def __init__(self, state_dim: int, action_dims: List[int]) -> None: + super(MultiQNet, self).__init__() + self._state_dim = state_dim + self._action_dims = action_dims + + @property + def state_dim(self) -> int: + return self._state_dim + + @property + def action_dims(self) -> List[int]: + return self._action_dims + + @property + def agent_num(self) -> int: + return len(self._action_dims) + + def _shape_check(self, states: torch.Tensor, actions: List[torch.Tensor] = None) -> bool: + """Check whether the states and actions have valid shapes. + + Args: + states (torch.Tensor): State tensor. + actions (List[torch.Tensor], default=None): Action tensors. It length must be equal to the number of agents. + If it is None, it means we only check state tensor's shape. + + Returns: + valid_flag (bool): whether the states and actions have valid shapes. + """ + if not SHAPE_CHECK_FLAG: + return True + else: + if states.shape[0] == 0 or not match_shape(states, (None, self.state_dim)): + return False + if actions is not None: + if len(actions) != self.agent_num: + return False + for action, dim in zip(actions, self.action_dims): + if not match_shape(action, (states.shape[0], dim)): + return False + return True + + def q_values(self, states: torch.Tensor, actions: List[torch.Tensor]) -> torch.Tensor: + """Get Q-values according to states and actions. + + Args: + states (torch.Tensor): States. + actions (List[torch.Tensor]): List of actions. + + Returns: + q (torch.Tensor): Q-values with shape [batch_size]. + """ + assert self._shape_check(states, actions) + q = self._get_q_values(states, actions) + assert match_shape(q, (states.shape[0],)), \ + f"Q-value shape check failed. Expecting: {(states.shape[0],)}, actual: {q.shape}." # [B] + return q + + @abstractmethod + def _get_q_values(self, states: torch.Tensor, actions: List[torch.Tensor]) -> torch.Tensor: + """Implementation of `q_values`. + """ + raise NotImplementedError diff --git a/maro/rl/model/policy_net.py b/maro/rl/model/policy_net.py new file mode 100644 index 000000000..7ff4c0c22 --- /dev/null +++ b/maro/rl/model/policy_net.py @@ -0,0 +1,166 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABCMeta, abstractmethod + +import torch.nn +from torch.distributions import Categorical + +from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape + +from .abs_net import AbsNet + + +class PolicyNet(AbsNet, metaclass=ABCMeta): + """Base class for all nets that serve as policy cores. It has the concept of 'state' and 'action'. + + Args: + state_dim (int): Dimension of states. + action_dim (int): Dimension of actions. + """ + + def __init__(self, state_dim: int, action_dim: int) -> None: + super(PolicyNet, self).__init__() + self._state_dim = state_dim + self._action_dim = action_dim + + @property + def state_dim(self) -> int: + return self._state_dim + + @property + def action_dim(self) -> int: + return self._action_dim + + def get_actions(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + """Get actions according to the given states. + + Args: + states (torch.Tensor): States. + exploring (bool): If it is True, return the results under exploring mode. Otherwise, return the results + under exploiting mode. + + Returns: + Actions (torch.Tensor) + """ + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + actions = self._get_actions_impl(states, exploring) + assert self._shape_check(states=states, actions=actions), \ + f"Actions shape check failed. Expecting: {(states.shape[0], self.action_dim)}, actual: {actions.shape}." + return actions + + def _shape_check(self, states: torch.Tensor, actions: torch.Tensor = None) -> bool: + """Check whether the states and actions have valid shapes. + + Args: + states (torch.Tensor): State tensor. + actions (torch.Tensor, default=None): Action tensor. If it is None, it means we only check state tensor's + shape. + + Returns: + valid_flag (bool): whether the states and actions have valid shapes. + """ + if not SHAPE_CHECK_FLAG: + return True + else: + if states.shape[0] == 0: + return False + if not match_shape(states, (None, self.state_dim)): + return False + + if actions is not None: + if not match_shape(actions, (states.shape[0], self.action_dim)): + return False + return True + + @abstractmethod + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + """Implementation of `get_actions`. + """ + raise NotImplementedError + + +class DiscretePolicyNet(PolicyNet, metaclass=ABCMeta): + """Policy network for discrete action spaces. + + Args: + state_dim (int): Dimension of states. + action_num (int): Number of actions. + """ + + def __init__(self, state_dim: int, action_num: int) -> None: + super(DiscretePolicyNet, self).__init__(state_dim=state_dim, action_dim=1) + self._action_num = action_num + + @property + def action_num(self) -> int: + return self._action_num + + def get_action_probs(self, states: torch.Tensor) -> torch.Tensor: + """Get the probabilities for all possible actions in the action space. + + Args: + states (torch.Tensor): States. + + Returns: + action_probs (torch.Tensor): Probability matrix with shape [batch_size, action_num]. + """ + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + action_probs = self._get_action_probs_impl(states) + assert match_shape(action_probs, (states.shape[0], self.action_num)), \ + f"Action probabilities shape check failed. Expecting: {(states.shape[0], self.action_num)}, " \ + f"actual: {action_probs.shape}." + return action_probs + + def get_action_logps(self, states: torch.Tensor) -> torch.Tensor: + """Get the log-probabilities for all actions. + + Args: + states (torch.Tensor): States. + + Returns: + logps (torch.Tensor): Lop-probability matrix with shape [batch_size, action_num]. + """ + return torch.log(self.get_action_probs(states)) + + @abstractmethod + def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: + """Implementation of `get_action_probs`. The core logic of a discrete policy net should be implemented here. + """ + raise NotImplementedError + + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + if exploring: + actions = self._get_actions_exploring_impl(states) + return actions + else: + action_logps = self.get_action_logps(states) + logps, actions = action_logps.max(dim=1) + return actions.unsqueeze(1) + + def _get_actions_exploring_impl(self, states: torch.Tensor) -> torch.Tensor: + """Get actions according to the states under exploring mode. + + Args: + states (torch.Tensor): States. + + Returns: + actions (torch.Tensor): Actions. + """ + action_probs = Categorical(self.get_action_probs(states)) + actions = action_probs.sample() + return actions.unsqueeze(1) + + +class ContinuousPolicyNet(PolicyNet, metaclass=ABCMeta): + """Policy network for continuous action spaces. + + Args: + state_dim (int): Dimension of states. + action_dim (int): Dimension of actions. + """ + + def __init__(self, state_dim: int, action_dim: int) -> None: + super(ContinuousPolicyNet, self).__init__(state_dim=state_dim, action_dim=action_dim) diff --git a/maro/rl/model/q_net.py b/maro/rl/model/q_net.py new file mode 100644 index 000000000..eced0a43f --- /dev/null +++ b/maro/rl/model/q_net.py @@ -0,0 +1,134 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABCMeta, abstractmethod + +import torch + +from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape + +from .abs_net import AbsNet + + +class QNet(AbsNet, metaclass=ABCMeta): + """Abstract Q-value network. + + Args: + state_dim (int): Dimension of states. + action_dim (int): Dimension of actions. + """ + + def __init__(self, state_dim: int, action_dim: int) -> None: + super(QNet, self).__init__() + self._state_dim = state_dim + self._action_dim = action_dim + + @property + def state_dim(self) -> int: + return self._state_dim + + @property + def action_dim(self) -> int: + return self._action_dim + + def _shape_check(self, states: torch.Tensor, actions: torch.Tensor = None) -> bool: + """Check whether the states and actions have valid shapes. + + Args: + states (torch.Tensor): State tensor. + actions (torch.Tensor, default=None): Action tensor. If it is None, it means we only check state tensor's + shape. + + Returns: + valid_flag (bool): whether the states and actions have valid shapes. + """ + if not SHAPE_CHECK_FLAG: + return True + else: + if states.shape[0] == 0 or not match_shape(states, (None, self.state_dim)): + return False + if actions is not None: + if not match_shape(actions, (states.shape[0], self.action_dim)): + return False + return True + + def q_values(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + """Get Q-values according to states and actions. + + Args: + states (torch.Tensor): States. + actions (torch.Tensor): Actions. + + Returns: + q (torch.Tensor): Q-values with shape [batch_size]. + """ + assert self._shape_check(states=states, actions=actions), \ + f"States or action shape check failed. Expecting: " \ + f"states = {('BATCH_SIZE', self.state_dim)}, action = {('BATCH_SIZE', self.action_dim)}. " \ + f"Actual: states = {states.shape}, action = {actions.shape}." + q = self._get_q_values(states, actions) + assert match_shape(q, (states.shape[0],)), \ + f"Q-value shape check failed. Expecting: {(states.shape[0],)}, actual: {q.shape}." # [B] + return q + + @abstractmethod + def _get_q_values(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + """Implementation of `q_values`. + """ + raise NotImplementedError + + +class DiscreteQNet(QNet, metaclass=ABCMeta): + """Q-value network for discrete action spaces. + + Args: + state_dim (int): Dimension of states. + action_num (int): Number of actions. + """ + + def __init__(self, state_dim: int, action_num: int) -> None: + super(DiscreteQNet, self).__init__(state_dim=state_dim, action_dim=1) + self._action_num = action_num + + @property + def action_num(self) -> int: + return self._action_num + + def q_values_for_all_actions(self, states: torch.Tensor) -> torch.Tensor: + """Get Q-values for all actions according to states. + + Args: + states (torch.Tensor): States. + + Returns: + q (torch.Tensor): Q-values for all actions. The returned value has the shape [batch_size, action_num]. + """ + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + q = self._get_q_values_for_all_actions(states) + assert match_shape(q, (states.shape[0], self.action_num)), \ + f"Q-value matrix shape check failed. Expecting: {(states.shape[0], self.action_num)}, " \ + f"actual: {q.shape}." # [B, action_num] + return q + + def _get_q_values(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + q = self.q_values_for_all_actions(states) # [B, action_num] + return q.gather(1, actions.long()).reshape(-1) # [B, action_num] + [B, 1] => [B] + + @abstractmethod + def _get_q_values_for_all_actions(self, states: torch.Tensor) -> torch.Tensor: + """Implementation of `q_values_for_all_actions`. + """ + raise NotImplementedError + + +class ContinuousQNet(QNet, metaclass=ABCMeta): + """Q-value network for continuous action spaces. + + Args: + state_dim (int): Dimension of states. + action_dim (int): Dimension of actions. + """ + + def __init__(self, state_dim: int, action_dim: int) -> None: + super(ContinuousQNet, self).__init__(state_dim=state_dim, action_dim=action_dim) diff --git a/maro/rl/model/v_net.py b/maro/rl/model/v_net.py new file mode 100644 index 000000000..b4789ff91 --- /dev/null +++ b/maro/rl/model/v_net.py @@ -0,0 +1,62 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABCMeta, abstractmethod + +import torch + +from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape + +from .abs_net import AbsNet + + +class VNet(AbsNet, metaclass=ABCMeta): + """V-value network. + + Args: + state_dim (int): Dimension of states. + """ + + def __init__(self, state_dim: int) -> None: + super(VNet, self).__init__() + self._state_dim = state_dim + + @property + def state_dim(self) -> int: + return self._state_dim + + def _shape_check(self, states: torch.Tensor) -> bool: + """Check whether the states have valid shapes. + + Args: + states (torch.Tensor): State tensor. + + Returns: + valid_flag (bool): whether the states and actions have valid shapes. + """ + if not SHAPE_CHECK_FLAG: + return True + else: + return states.shape[0] > 0 and match_shape(states, (None, self.state_dim)) + + def v_values(self, states: torch.Tensor) -> torch.Tensor: + """Get V-values according to states. + + Args: + states (torch.Tensor): States. + + Returns: + v (torch.Tensor): V-values with shape [batch_size]. + """ + assert self._shape_check(states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + v = self._get_v_values(states) + assert match_shape(v, (states.shape[0],)), \ + f"V-value shape check failed. Expecting: {(states.shape[0],)}, actual: {v.shape}." # [B] + return v + + @abstractmethod + def _get_v_values(self, states: torch.Tensor) -> torch.Tensor: + """Implementation of `v_values`. + """ + raise NotImplementedError diff --git a/maro/rl/modeling/__init__.py b/maro/rl/modeling/__init__.py deleted file mode 100644 index b2d7d4bec..000000000 --- a/maro/rl/modeling/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .core_model import AbsCoreModel -from .fc_block import FullyConnected -from .specials import ContinuousACNet, DiscreteACNet, DiscretePolicyNet, DiscreteQNet - -__all__ = [ - "AbsCoreModel", - "FullyConnected", - "ContinuousACNet", "DiscreteACNet", "DiscretePolicyNet", "DiscreteQNet" -] diff --git a/maro/rl/modeling/core_model.py b/maro/rl/modeling/core_model.py deleted file mode 100644 index 269372cb1..000000000 --- a/maro/rl/modeling/core_model.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import abstractmethod - -import torch -import torch.nn as nn - - -class AbsCoreModel(nn.Module): - """Model abstraction for use in deep RL algorithms. - - This can be viewed as a container of one or more network components with embedded optimizers. This abstraction - exposes simple and unified interfaces to decouple model inference and optimization from the algorithmic aspects - of the policy that uses it. - """ - def __init__(self): - super().__init__() - - @abstractmethod - def forward(self, *args, **kwargs): - raise NotImplementedError - - @abstractmethod - def step(self, loss: torch.tensor): - """Use a computed loss to back-propagate gradients and apply them to the underlying parameters. - - Args: - loss: Result of a computation graph that involves the underlying parameters. - """ - raise NotImplementedError - - def get_gradients(self, loss: torch.tensor): - """Get gradients from a computed loss. - - There are two possible scenarios where you need to implement this interface: 1) if you are doing distributed - learning and want each roll-out instance to collect gradients that can be directly applied to policy parameters - on the learning side (abstracted through ``AbsPolicyManager``); 2) if you are computing loss in data-parallel - fashion, i.e., by splitting a data batch to several smaller batches and sending them to a set of remote workers - for parallelized gradient computation. In this case, this method will be used by the remote workers. - """ - pass - - def apply_gradients(self, grad: dict): - """Apply gradients to the model parameters. - - This needs to be implemented together with ``get_gradients``. - """ - pass - - @abstractmethod - def get_state(self): - """Return the current model state. - - Ths model state usually involves the "state_dict" of the module as well as those of the embedded optimizers. - """ - pass - - @abstractmethod - def set_state(self, state): - """Set model state. - - Args: - state: Model state to be applied to the instance. Ths model state is either the result of a previous call - to ``get_state`` or something loaded from disk and involves the "state_dict" of the module as well as those - of the embedded optimizers. - """ - pass diff --git a/maro/rl/modeling/specials.py b/maro/rl/modeling/specials.py deleted file mode 100644 index 73bc0bb6e..000000000 --- a/maro/rl/modeling/specials.py +++ /dev/null @@ -1,214 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import abstractmethod -from typing import Union - -import numpy as np -import torch -from torch import nn -from torch.distributions import Categorical - -from .core_model import AbsCoreModel - - -class DiscreteACNet(AbsCoreModel): - """Model framework for the actor-critic architecture for finite and discrete action spaces.""" - - @property - @abstractmethod - def input_dim(self): - raise NotImplementedError - - @property - @abstractmethod - def num_actions(self): - raise NotImplementedError - - @abstractmethod - def forward(self, states: torch.tensor, actor: bool = True, critic: bool = True) -> tuple: - """Compute action probabilities and values for a state batch. - - The output is a tuple of (action_probs, values), where action probs is a tensor of shape - (batch_size, action_space_size) and values is a tensor of shape (batch_size,). If only one - of these two is needed, the return value for the other one can be set to None. - - Args: - states (torch.tensor): State batch to compute action probabilities and values for. - actor (bool): If True, the first element of the output will be actin probabilities. Defaults to True. - critic (bool): If True, the second element of the output will be state values. Defaults to True. - """ - raise NotImplementedError - - def get_action(self, states: torch.tensor, greedy: bool = False): - """ - Given Q-values for a batch of states, return the actions, the corresponding log-P values and the state values. - - Args: - states (torch.tensor): State batch to compute actions for. - greedy (bool): If True, the action with the greatest probability will be returned for each state in the - batch. Defaults to False. - """ - action_probs, values = self.forward(states) - if greedy: - probs, actions = action_probs.max(dim=1) - return actions, torch.log(probs), values - else: - action_probs = Categorical(action_probs) # (batch_size, action_space_size) - actions = action_probs.sample() - logps = action_probs.log_prob(actions) - return actions, logps, values - - -class DiscretePolicyNet(AbsCoreModel): - """Parameterized policy for finite and discrete action spaces.""" - - @property - @abstractmethod - def input_dim(self): - raise NotImplementedError - - @property - @abstractmethod - def num_actions(self): - raise NotImplementedError - - @abstractmethod - def forward(self, states: torch.tensor) -> torch.tensor: - """Compute action probabilities corresponding to each state in ``states``. - - The output must be a torch tensor with shape (batch_size, action_space_size). - - Args: - states (torch.tensor): State batch to compute action probabilities for. - """ - raise NotImplementedError - - def get_action(self, states: torch.tensor, greedy: bool = False): - """ - Given Q-values for a batch of states, return the actions and the corresponding log-P values. - - Args: - states (torch.tensor): State batch to compute actions for. - greedy (bool): If True, the action with the greatest probability will be returned for each state in the - batch. Defaults to False. - """ - action_prob = self.forward(states) # (batch_size, num_actions) - if greedy: - prob, action = action_prob.max(dim=1) - return action, torch.log(prob) - else: - action_prob = Categorical(action_prob) # (batch_size, action_space_size) - action = action_prob.sample() - log_p = action_prob.log_prob(action) - return action, log_p - - -class DiscreteQNet(AbsCoreModel): - """Q-value model for finite and discrete action spaces.""" - - @property - @abstractmethod - def input_dim(self): - raise NotImplementedError - - @property - @abstractmethod - def num_actions(self): - raise NotImplementedError - - @abstractmethod - def forward(self, states: torch.tensor) -> torch.tensor: - """Compute the Q-values for all actions as a tensor of shape (batch_size, action_space_size).""" - raise NotImplementedError - - def get_action(self, states: torch.tensor): - """ - Given Q-values for a batch of states and all actions, return the action index and the corresponding - Q-values for each state. - """ - q_for_all_actions = self.forward(states) # (batch_size, num_actions) - greedy_q, actions = q_for_all_actions.max(dim=1) - return actions.detach(), greedy_q.detach(), q_for_all_actions.shape[1] - - def q_values(self, states: torch.tensor, actions: torch.tensor): - """Return the Q-values for a batch of states and actions.""" - if len(actions.shape) == 1: - actions = actions.unsqueeze(dim=1) - q_for_all_actions = self.forward(states) # (batch_size, num_actions) - return q_for_all_actions.gather(1, actions).squeeze(dim=1) - - def soft_update(self, other_model: nn.Module, tau: float): - """Soft-update model parameters using another model. - - Update formulae: param = (1 - tau) * param + tau * other_param. - - Args: - other_model: The model to update the current model with. - tau (float): Soft-update coefficient. - """ - for params, other_params in zip(self.parameters(), other_model.parameters()): - params.data = (1 - tau) * params.data + tau * other_params.data - - -class ContinuousACNet(AbsCoreModel): - """Model container for the actor-critic architecture for continuous action spaces.""" - def __init__(self, out_min: Union[float, np.ndarray] = None, out_max: Union[float, np.ndarray] = None): - super().__init__() - if out_min: - assert isinstance(out_min, (float, np.ndarray)), "out_min must be a float or a numpy array" - if out_max: - assert isinstance(out_max, (float, np.ndarray)), "out_max must be a float or a numpy array" - - if isinstance(out_min, np.ndarray) and isinstance(out_max, np.ndarray): - assert len(out_min) == len(out_max), "out_min and out_max should have the same dimension." - - self.out_min = out_min - self.out_max = out_max - # For torch clamping - self._min = torch.from_numpy(out_min) if isinstance(out_min, np.ndarray) else out_min - self._max = torch.from_numpy(out_max) if isinstance(out_max, np.ndarray) else out_max - - @property - @abstractmethod - def input_dim(self): - raise NotImplementedError - - @property - @abstractmethod - def action_dim(self): - raise NotImplementedError - - @abstractmethod - def forward(self, states: torch.tensor, actions=None) -> torch.tensor: - """Compute actions for a batch of states or Q-values for a batch of states and actions. - - Args: - states (torch.tensor): State batch to compute the Q-values for. - actions: Action batch. If None, the output should be a batch of actions corresponding to - the state batch. Otherwise, the output should be the Q-values for the given states and - actions. Defaults to None. - """ - raise NotImplementedError - - def get_action(self, states: torch.tensor) -> torch.tensor: - """Compute actions given a batch of states.""" - if self._min is None and self._max is None: - return self.forward(states) - return torch.clamp(self.forward(states), min=self._min, max=self._max) - - def value(self, states: torch.tensor): - """Compute the Q-values for a batch of states using the actions computed from them.""" - return self.forward(states, actions=self.get_action(states)) - - def soft_update(self, other_model: nn.Module, tau: float): - """Soft-update model parameters using another model. - - Update formulae: param = (1 - tau) * param + tau * other_param. - - Args: - other_model: The model to update the current model with. - tau (float): Soft-update coefficient. - """ - for params, other_params in zip(self.parameters(), other_model.parameters()): - params.data = (1 - tau) * params.data + tau * other_params.data diff --git a/maro/rl/policy/__init__.py b/maro/rl/policy/__init__.py index fa76533cb..485c6f2bb 100644 --- a/maro/rl/policy/__init__.py +++ b/maro/rl/policy/__init__.py @@ -1,18 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .ac import ActorCritic -from .ddpg import DDPG -from .dqn import DQN, PrioritizedExperienceReplay -from .pg import PolicyGradient -from .policy import AbsPolicy, DummyPolicy, RLPolicy -from .worker_allocator import WorkerAllocator +from .abs_policy import AbsPolicy, DummyPolicy, RLPolicy, RuleBasedPolicy +from .continuous_rl_policy import ContinuousRLPolicy +from .discrete_rl_policy import DiscretePolicyGradient, DiscreteRLPolicy, ValueBasedPolicy __all__ = [ - "ActorCritic", - "DDPG", - "DQN", "PrioritizedExperienceReplay", - "PolicyGradient", - "AbsPolicy", "DummyPolicy", "RLPolicy" - "WorkerAllocator" + "AbsPolicy", "DummyPolicy", "RLPolicy", "RuleBasedPolicy", + "ContinuousRLPolicy", + "DiscretePolicyGradient", "DiscreteRLPolicy", "ValueBasedPolicy", ] diff --git a/maro/rl/policy/abs_policy.py b/maro/rl/policy/abs_policy.py new file mode 100644 index 000000000..01006c8db --- /dev/null +++ b/maro/rl/policy/abs_policy.py @@ -0,0 +1,292 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from typing import Dict, Optional + +import numpy as np +import torch + +from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape, ndarray_to_tensor + + +class AbsPolicy(object, metaclass=ABCMeta): + """Abstract policy class. A policy takes states as inputs and generates actions as outputs. A policy cannot + update itself. It has to be updated by external trainers through public interfaces. + + Args: + name (str): Name of this policy. + trainable (bool): Whether this policy is trainable. + """ + + def __init__(self, name: str, trainable: bool) -> None: + super(AbsPolicy, self).__init__() + self._name = name + self._trainable = trainable + + @abstractmethod + def get_actions(self, states: object) -> object: + """Get actions according to states. + + Args: + states (object): States. + + Returns: + actions (object): Actions. + """ + raise NotImplementedError + + @property + def name(self) -> str: + return self._name + + @property + def trainable(self) -> bool: + return self._trainable + + def set_name(self, name: str) -> None: + self._name = name + + +class DummyPolicy(AbsPolicy): + """Dummy policy that takes no actions. + """ + + def __init__(self) -> None: + super(DummyPolicy, self).__init__(name='DUMMY_POLICY', trainable=False) + + def get_actions(self, states: object) -> None: + return None + + +class RuleBasedPolicy(AbsPolicy, metaclass=ABCMeta): + """Rule-based policy. The user should define the rule of this policy, and a rule-based policy is not trainable. + """ + + def __init__(self, name: str) -> None: + super(RuleBasedPolicy, self).__init__(name=name, trainable=False) + + def get_actions(self, states: object) -> object: + return self._rule(states) + + @abstractmethod + def _rule(self, states: object) -> object: + raise NotImplementedError + + +class RLPolicy(AbsPolicy, metaclass=ABCMeta): + """Reinforcement learning policy. + + Args: + name (str): Name of the policy. + state_dim (int): Dimension of states. + action_dim (int): Dimension of actions. + trainable (bool, default=True): Whether this policy is trainable. + """ + + def __init__( + self, + name: str, + state_dim: int, + action_dim: int, + trainable: bool = True, + ) -> None: + super(RLPolicy, self).__init__(name=name, trainable=trainable) + self._state_dim = state_dim + self._action_dim = action_dim + self._is_exploring = False + + self._device: Optional[torch.device] = None + + @property + def state_dim(self) -> int: + return self._state_dim + + @property + def action_dim(self) -> int: + return self._action_dim + + @property + def is_exploring(self) -> bool: + """Whether this policy is under exploring mode. + """ + return self._is_exploring + + def explore(self) -> None: + """Set the policy to exploring mode. + """ + self._is_exploring = True + + def exploit(self) -> None: + """Set the policy to exploiting mode. + """ + self._is_exploring = False + + @abstractmethod + def step(self, loss: torch.Tensor) -> None: + """Run a training step to update the policy according to the given loss. + + Args: + loss (torch.Tensor): Loss used to update the policy. + """ + raise NotImplementedError + + @abstractmethod + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + """Get the gradients with respect to all parameters of the internal nets according to the given loss. + + Args: + loss (torch.tensor): Loss used to update the model. + + Returns: + grad (Dict[str, torch.Tensor]): A dict that contains gradients of the internal nets for all parameters. + """ + raise NotImplementedError + + @abstractmethod + def apply_gradients(self, grad: dict) -> None: + """Apply gradients to the net to update all parameters. + + Args: + grad (Dict[str, torch.Tensor]): A dict that contains gradients for all parameters. + """ + raise NotImplementedError + + def get_actions(self, states: np.ndarray) -> np.ndarray: + return self.get_actions_tensor(ndarray_to_tensor(states, self._device)).cpu().numpy() + + def get_actions_tensor(self, states: torch.Tensor) -> torch.Tensor: + """Get actions according to states. Takes torch.Tensor as inputs and returns torch.Tensor. + + Args: + states (torch.Tensor): States. + + Returns: + actions (torch.Tensor): Actions. + """ + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + actions = self._get_actions_impl(states, self._is_exploring) + assert self._shape_check(states=states, actions=actions), \ + f"Actions shape check failed. Expecting: {(states.shape[0], self.action_dim)}, actual: {actions.shape}." + if SHAPE_CHECK_FLAG: + assert self._post_check(states=states, actions=actions) + return actions + + @abstractmethod + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + """Implementation of `get_action_tensor`. + """ + raise NotImplementedError + + @abstractmethod + def freeze(self) -> None: + """(Partially) freeze the current model. The users should write their own strategy to determine which + parameters to freeze. + """ + raise NotImplementedError + + @abstractmethod + def unfreeze(self) -> None: + """(Partially) unfreeze the current model. The users should write their own strategy to determine which + parameters to freeze. + """ + raise NotImplementedError + + @abstractmethod + def eval(self) -> None: + """Switch the policy to evaluation mode. + """ + raise NotImplementedError + + @abstractmethod + def train(self) -> None: + """Switch the policy to training mode. + """ + raise NotImplementedError + + @abstractmethod + def get_state(self) -> object: + """Get the state of the policy. + """ + raise NotImplementedError + + @abstractmethod + def set_state(self, policy_state: object) -> None: + """Set the state of the policy. + """ + raise NotImplementedError + + @abstractmethod + def soft_update(self, other_policy: RLPolicy, tau: float) -> None: + """Soft update the policy's parameters according to another policy. + + Args: + other_policy (AbsNet): The source policy. Must has same type with the current policy. + tau (float): Soft update coefficient. + """ + raise NotImplementedError + + def _shape_check( + self, + states: torch.Tensor, + actions: torch.Tensor = None, + ) -> bool: + """Check whether the states and actions have valid shapes. + + Args: + states (torch.Tensor): State tensor. + actions (torch.Tensor, default=None): Action tensor. If it is None, it means we only check state tensor's + shape. + + Returns: + valid_flag (bool): whether the states and actions have valid shapes. + """ + if not SHAPE_CHECK_FLAG: + return True + else: + if states.shape[0] == 0: + return False + if not match_shape(states, (None, self.state_dim)): + return False + + if actions is not None: + if not match_shape(actions, (states.shape[0], self.action_dim)): + return False + return True + + @abstractmethod + def _post_check(self, states: torch.Tensor, actions: torch.Tensor) -> bool: + """Check whether the generated action tensor is valid, i.e., has matching shape with states tensor. + + Args: + states (torch.Tensor): State tensor. + actions (torch.Tensor): Action tensor. + + Returns: + valid_flag (bool): whether the action tensor is valid. + """ + raise NotImplementedError + + def to_device(self, device: torch.device) -> None: + """Assign the current policy to a specific device. + + Args: + device (torch.device): The target device. + """ + if self._device is None: + self._device = device + self._to_device_impl(device) + elif self._device != device: + raise ValueError( + f"Policy {self.name} has already been assigned to device {self._device} " + f"and cannot be re-assigned to device {device}" + ) + + @abstractmethod + def _to_device_impl(self, device: torch.device) -> None: + """Implementation of `to_device`. + """ + pass diff --git a/maro/rl/policy/ac.py b/maro/rl/policy/ac.py deleted file mode 100644 index 546fae1ee..000000000 --- a/maro/rl/policy/ac.py +++ /dev/null @@ -1,303 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from typing import List - -import numpy as np -import torch -from torch.distributions import Categorical - -from maro.communication import SessionMessage -from maro.rl.modeling import DiscreteACNet -from maro.rl.utils import MsgKey, MsgTag, average_grads, discount_cumsum - -from .policy import RLPolicy - - -class ActorCritic(RLPolicy): - class Buffer: - """Store a sequence of transitions, i.e., a trajectory. - - Args: - state_dim (int): State vector dimension. - size (int): Buffer capacity, i.e., the maximum number of stored transitions. - """ - def __init__(self, state_dim, size: int = 10000): - self.states = np.zeros((size, state_dim), dtype=np.float32) - self.actions = np.zeros(size, dtype=np.int) - self.logps = np.zeros(size, dtype=np.float32) - self.values = np.zeros(size, dtype=np.float32) - self.rewards = np.zeros(size, dtype=np.float32) - self.terminals = np.zeros(size, dtype=np.bool) - self.size = size - self._ptr = 0 - self._prev_ptr = 0 - - def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): - self.states[self._ptr] = state - self.actions[self._ptr] = action["action"] - self.logps[self._ptr] = action["logp"] - self.values[self._ptr] = action["value"] - self.rewards[self._ptr] = reward - self.terminals[self._ptr] = terminal - # increment pointer - self._ptr += 1 - if self._ptr == self.size: - self._ptr = 0 - - def get(self): - """Retrieve the latest trajectory segment.""" - terminal = self.terminals[self._ptr - 1] - last = self._ptr - (not terminal) - if last > self._prev_ptr: - traj_slice = np.arange(self._prev_ptr, last) - else: # wrap-around - traj_slice = np.concatenate([np.arange(self._prev_ptr, self.size), np.arange(last)]) - self._prev_ptr = last - return { - "states": self.states[traj_slice], - "actions": self.actions[traj_slice], - "logps": self.logps[traj_slice], - "values": self.values[traj_slice], - "rewards": self.rewards[traj_slice], - "last_value": self.values[-1] - } - - """Actor Critic algorithm with separate policy and value models. - - References: - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f - - Args: - name (str): Unique identifier for the policy. - ac_net (DiscreteACNet): Multi-task model that computes action distributions and state values. - reward_discount (float): Reward decay as defined in standard RL terminology. - grad_iters (int): Number of gradient steps for each batch or set of batches. Defaults to 1. - critic_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for computing - the critic loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - min_logp (float): Lower bound for clamping logP values during learning. This is to prevent logP from becoming - very large in magnitude and causing stability issues. Defaults to None, which means no lower bound. - critic_loss_coeff (float): Coefficient for critic loss in total loss. Defaults to 1.0. - entropy_coeff (float): Coefficient for the entropy term in total loss. Defaults to None, in which case the - total loss will not include an entropy term. - clip_ratio (float): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). Defaults to None, - in which case the actor loss is calculated using the usual policy gradient theorem. - lambda (float): Lambda value for generalized advantage estimation (TD-Lambda). Defaults to 0.9. - max_trajectory_len (int): Maximum trajectory length that can be held by the buffer (for each agent that uses - this policy). Defaults to 10000. - get_loss_on_rollout (bool): If True, ``get_rollout_info`` will return the loss information (including gradients) - for the trajectories stored in the buffers. The loss information, along with that from other roll-out - instances, can be passed directly to ``update``. Otherwise, it will simply process the trajectories into a - single data batch that can be passed directly to ``learn``. Defaults to False. - device (str): Identifier for the torch device. The ``ac_net`` will be moved to the specified device. If it is - None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. - """ - - def __init__( - self, - name: str, - ac_net: DiscreteACNet, - reward_discount: float, - grad_iters: int = 1, - critic_loss_cls="mse", - min_logp: float = None, - critic_loss_coeff: float = 1.0, - entropy_coeff: float = .0, - clip_ratio: float = None, - lam: float = 0.9, - max_trajectory_len: int = 10000, - get_loss_on_rollout: bool = False, - device: str = None - ): - if not isinstance(ac_net, DiscreteACNet): - raise TypeError("model must be an instance of 'DiscreteACNet'") - - super().__init__(name) - if device is None: - self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - else: - self.device = torch.device(device) - self.ac_net = ac_net.to(self.device) - self.reward_discount = reward_discount - self.grad_iters = grad_iters - self.critic_loss_func = critic_loss_cls() - self.min_logp = min_logp - self.critic_loss_coeff = critic_loss_coeff - self.entropy_coeff = entropy_coeff - self.clip_ratio = clip_ratio - self.lam = lam - self.max_trajectory_len = max_trajectory_len - self.get_loss_on_rollout = get_loss_on_rollout - - self._buffer = defaultdict(lambda: self.Buffer(self.ac_net.input_dim, size=self.max_trajectory_len)) - - def __call__(self, states: np.ndarray): - """Return a list of action information dict given a batch of states. - - An action information dict contains the action itself, the corresponding log-P value and the corresponding - state value. - """ - self.ac_net.eval() - states = torch.from_numpy(states).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - with torch.no_grad(): - actions, logps, values = self.ac_net.get_action(states, greedy=self.greedy) - actions, logps, values = actions.cpu().numpy(), logps.cpu().numpy(), values.cpu().numpy() - return [ - {"action": action, "logp": logp, "value": value} for action, logp, value in zip(actions, logps, values) - ] - - def record( - self, - key: str, - state: np.ndarray, - action: dict, - reward: float, - next_state: np.ndarray, - terminal: bool - ): - self._buffer[key].put(state, action, reward, terminal) - - def get_rollout_info(self): - """Extract information from the recorded transitions. - - Returns: - Loss (including gradients) for the latest trajectory segment in the replay buffer if ``get_loss_on_rollout`` - is True or the latest trajectory segment with pre-computed return and advantage values. - """ - if self.get_loss_on_rollout: - return self.get_batch_loss(self._get_batch(), explicit_grad=True) - else: - return self._get_batch() - - def _get_batch(self): - batch = defaultdict(list) - for buf in self._buffer.values(): - trajectory = buf.get() - values = np.append(trajectory["values"], trajectory["last_value"]) - rewards = np.append(trajectory["rewards"], trajectory["last_value"]) - deltas = rewards[:-1] + self.reward_discount * values[1:] - values[:-1] - batch["states"].append(trajectory["states"]) - batch["actions"].append(trajectory["actions"]) - # Returns rewards-to-go, to be targets for the value function - batch["returns"].append(discount_cumsum(rewards, self.reward_discount)[:-1]) - # Generalized advantage estimation using TD(Lambda) - batch["advantages"].append(discount_cumsum(deltas, self.reward_discount * self.lam)) - batch["logps"].append(trajectory["logps"]) - - return {key: np.concatenate(vals) for key, vals in batch.items()} - - def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - """Compute AC loss for a data batch. - - Args: - batch (dict): A batch containing "states", "actions", "logps", "returns" and "advantages" as keys. - explicit_grad (bool): If True, the gradients should be returned as part of the loss information. Defaults - to False. - """ - self.ac_net.train() - states = torch.from_numpy(batch["states"]).to(self.device) - actions = torch.from_numpy(batch["actions"]).to(self.device) - logp_old = torch.from_numpy(batch["logps"]).to(self.device) - returns = torch.from_numpy(batch["returns"]).to(self.device) - advantages = torch.from_numpy(batch["advantages"]).to(self.device) - - action_probs, state_values = self.ac_net(states) - state_values = state_values.squeeze() - - # actor loss - logp = torch.log(action_probs.gather(1, actions.unsqueeze(1)).squeeze()) # (N,) - logp = torch.clamp(logp, min=self.min_logp, max=.0) - if self.clip_ratio is not None: - ratio = torch.exp(logp - logp_old) - clipped_ratio = torch.clamp(ratio, 1 - self.clip_ratio, 1 + self.clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).mean() - else: - actor_loss = -(logp * advantages).mean() - - # critic_loss - critic_loss = self.critic_loss_func(state_values, returns) - # entropy - entropy = -Categorical(action_probs).entropy().mean() if self.entropy_coeff else 0 - - # total loss - loss = actor_loss + self.critic_loss_coeff * critic_loss + self.entropy_coeff * entropy - - loss_info = { - "actor_loss": actor_loss.detach().cpu().numpy(), - "critic_loss": critic_loss.detach().cpu().numpy(), - "entropy": entropy.detach().cpu().numpy() if self.entropy_coeff else .0, - "loss": loss.detach().cpu().numpy() if explicit_grad else loss - } - if explicit_grad: - loss_info["grad"] = self.ac_net.get_gradients(loss) - - return loss_info - - def update(self, loss_info_list: List[dict]): - """Update the model parameters with gradients computed by multiple roll-out instances or gradient workers. - - Args: - loss_info_list (List[dict]): A list of dictionaries containing loss information (including gradients) - computed by multiple roll-out instances or gradient workers. - """ - self.ac_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) - - def learn(self, batch: dict): - """Learn from a batch containing data required for policy improvement. - - Args: - batch (dict): A batch containing "states", "actions", "logps", "returns" and "advantages" as keys. - """ - for _ in range(self.grad_iters): - self.ac_net.step(self.get_batch_loss(batch)["loss"]) - - def improve(self): - """Learn using data from the buffer.""" - self.learn(self._get_batch()) - - def learn_with_data_parallel(self, batch: dict, worker_id_list: list): - assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." - for _ in range(self.grad_iters): - msg_dict = defaultdict(lambda: defaultdict(dict)) - for i, worker_id in enumerate(worker_id_list): - sub_batch = {key: batch[key][i::len(worker_id_list)] for key in batch} - msg_dict[worker_id][MsgKey.GRAD_TASK][self._name] = sub_batch - msg_dict[worker_id][MsgKey.POLICY_STATE][self._name] = self.get_state() - # data-parallel - self._proxy.isend(SessionMessage( - MsgTag.COMPUTE_GRAD, self._proxy.name, worker_id, body=msg_dict[worker_id])) - dones = 0 - loss_info_by_policy = {self._name: []} - for msg in self._proxy.receive(): - if msg.tag == MsgTag.COMPUTE_GRAD_DONE: - for policy_name, loss_info in msg.body[MsgKey.LOSS_INFO].items(): - if isinstance(loss_info, list): - loss_info_by_policy[policy_name] += loss_info - elif isinstance(loss_info, dict): - loss_info_by_policy[policy_name].append(loss_info) - else: - raise TypeError(f"Wrong type of loss_info: {type(loss_info)}") - dones += 1 - if dones == len(msg_dict): - break - # build dummy computation graph by `get_batch_loss` before apply gradients. - _ = self.get_batch_loss(sub_batch, explicit_grad=True) - self.update(loss_info_by_policy[self._name]) - - def get_state(self): - return self.ac_net.get_state() - - def set_state(self, state): - self.ac_net.set_state(state) - - def load(self, path: str): - """Load the policy state from disk.""" - self.ac_net.set_state(torch.load(path)) - - def save(self, path: str): - """Save the policy state to disk.""" - torch.save(self.ac_net.get_state(), path) diff --git a/maro/rl/policy/continuous_rl_policy.py b/maro/rl/policy/continuous_rl_policy.py new file mode 100644 index 000000000..e3aa79ce3 --- /dev/null +++ b/maro/rl/policy/continuous_rl_policy.py @@ -0,0 +1,115 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Dict, List, Optional, Tuple, Union + +import numpy as np +import torch + +from maro.rl.model import ContinuousPolicyNet + +from .abs_policy import RLPolicy + + +def _parse_action_range( + action_dim: int, + action_range: Tuple[Union[float, List[float]], Union[float, List[float]]], +) -> Tuple[Optional[List[float]], Optional[List[float]]]: + lower, upper = action_range + + if isinstance(lower, float): + lower = [lower] * action_dim + if isinstance(upper, float): + upper = [upper] * action_dim + + if not (action_dim == len(lower) == len(upper)): + return None, None + + for lval, uval in zip(lower, upper): + if lval >= uval: + return None, None + + return lower, upper + + +class ContinuousRLPolicy(RLPolicy): + """RL policy for continuous action spaces. + + Args: + name (str): Name of the policy. + action_range (Tuple[Union[float, List[float]], Union[float, List[float]]]): Value range of actions. + Both the lower bound and the upper bound could be float or array. If it is an array, it should contain + the bound for every dimension. If it is a float, it will be broadcast to all dimensions. + policy_net (ContinuousPolicyNet): The core net of this policy. + trainable (bool, default=True): Whether this policy is trainable. + """ + + def __init__( + self, + name: str, + action_range: Tuple[Union[float, List[float]], Union[float, List[float]]], + policy_net: ContinuousPolicyNet, + trainable: bool = True, + ) -> None: + assert isinstance(policy_net, ContinuousPolicyNet) + + super(ContinuousRLPolicy, self).__init__( + name=name, state_dim=policy_net.state_dim, action_dim=policy_net.action_dim, + trainable=trainable, + ) + + self._lbounds, self._ubounds = _parse_action_range(self.action_dim, action_range) + assert self._lbounds is not None and self._ubounds is not None + + self._policy_net = policy_net + + @property + def action_bounds(self) -> Tuple[List[float], List[float]]: + return self._lbounds, self._ubounds + + @property + def policy_net(self) -> ContinuousPolicyNet: + return self._policy_net + + def _post_check(self, states: torch.Tensor, actions: torch.Tensor) -> bool: + return all([ + (np.array(self._lbounds) <= actions.cpu().numpy()).all(), + (actions.cpu().numpy() < np.array(self._ubounds)).all() + ]) + + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + return self._policy_net.get_actions(states, exploring) + + def step(self, loss: torch.Tensor) -> None: + self._policy_net.step(loss) + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + return self._policy_net.get_gradients(loss) + + def apply_gradients(self, grad: dict) -> None: + self._policy_net.apply_gradients(grad) + + def freeze(self) -> None: + self._policy_net.freeze() + + def unfreeze(self) -> None: + self._policy_net.unfreeze() + + def eval(self) -> None: + self._policy_net.eval() + + def train(self) -> None: + self._policy_net.train() + + def get_state(self) -> object: + return self._policy_net.get_state() + + def set_state(self, policy_state: object) -> None: + self._policy_net.set_state(policy_state) + + def soft_update(self, other_policy: RLPolicy, tau: float) -> None: + assert isinstance(other_policy, ContinuousRLPolicy) + self._policy_net.soft_update(other_policy.policy_net, tau) + + def _to_device_impl(self, device: torch.device) -> None: + self._policy_net.to(device) diff --git a/maro/rl/policy/ddpg.py b/maro/rl/policy/ddpg.py deleted file mode 100644 index 91f24aa3c..000000000 --- a/maro/rl/policy/ddpg.py +++ /dev/null @@ -1,302 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from typing import Callable, List, Tuple, Union - -import numpy as np -import torch - -from maro.communication import SessionMessage -from maro.rl.exploration import gaussian_noise -from maro.rl.modeling import ContinuousACNet -from maro.rl.utils import MsgKey, MsgTag, average_grads -from maro.utils import clone - -from .policy import RLPolicy -from .replay import ReplayMemory - - -class DDPG(RLPolicy): - """The Deep Deterministic Policy Gradient (DDPG) algorithm. - - References: - https://arxiv.org/pdf/1509.02971.pdf - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg - - Args: - name (str): Unique identifier for the policy. - ac_net (ContinuousACNet): DDPG policy and q-value models. - reward_discount (float): Reward decay as defined in standard RL terminology. - num_epochs (int): Number of training epochs per call to ``learn``. Defaults to 1. - update_target_every (int): Number of training rounds between policy target model updates. - q_value_loss_cls: A string indicating a loss class provided by torch.nn or a custom loss class for - the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. Defaults to "mse". - q_value_loss_coeff (float): Coefficient for policy loss in the total loss function, e.g., - loss = policy_loss + ``q_value_loss_coeff`` * q_value_loss. Defaults to 1.0. - soft_update_coeff (float): Soft update coefficient, e.g., target_model = (soft_update_coeff) * eval_model + - (1-soft_update_coeff) * target_model. Defaults to 1.0. - exploration_strategy (Tuple[Callable, dict]): A 2-tuple that consists of a) a function that takes a state - (single or batch), an action (single or batch), the total number of possible actions and a set of keyword - arguments, and returns an exploratory action (single or batch depending on the input), and b) a dictionary - of keyword arguments for the function in a) (this will be assigned to the ``exploration_params`` member - variable). Defaults to (``gaussian_noise``, {"mean": .0, "stddev": 1.0, "relative": False}). - exploration_scheduling_option (List[tuple]): A list of 3-tuples specifying the exploration schedulers to be - registered to the exploration parameters. Each tuple consists of an exploration parameter name, an - exploration scheduler class (subclass of ``AbsExplorationScheduler``) and keyword arguments for that class. - The exploration parameter name must be a key in the keyword arguments (second element) of - ``exploration_strategy``. Defaults to an empty list. - replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. - exploration_params (dict): Keyword arguments for ``exploration_func``. Defaults to {"mean": .0, "stddev": 1.0, - "relative": False}. - replay_memory_capacity (int): Capacity of the replay memory. Defaults to 10000. - random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, - overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with - wrap-around. Defaults to False. - rollout_batch_size (int): Size of the experience batch to use as roll-out information by calling - ``get_rollout_info``. Defaults to 1000. - train_batch_size (int): Batch size for training the Q-net. Defaults to 32. - device (str): Identifier for the torch device. The ``ac_net`` will be moved to the specified device. If it is - None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. - """ - def __init__( - self, - name: str, - ac_net: ContinuousACNet, - reward_discount: float, - num_epochs: int = 1, - update_target_every: int = 5, - q_value_loss_cls="mse", - q_value_loss_coeff: float = 1.0, - soft_update_coeff: float = 1.0, - exploration_strategy: Tuple[Callable, dict] = (gaussian_noise, {"mean": .0, "stddev": 1.0, "relative": False}), - exploration_scheduling_options: List[tuple] = [], - replay_memory_capacity: int = 1000000, - random_overwrite: bool = False, - warmup: int = 50000, - rollout_batch_size: int = 1000, - train_batch_size: int = 32, - device: str = None - ): - if not isinstance(ac_net, ContinuousACNet): - raise TypeError("model must be an instance of 'ContinuousACNet'") - - if any(opt[0] not in exploration_strategy[1] for opt in exploration_scheduling_options): - raise ValueError( - f"The first element of an exploration scheduling option must be one of " - f"{list(exploration_strategy[1].keys())}" - ) - - super().__init__(name) - if device is None: - self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - else: - self.device = torch.device(device) - self.ac_net = ac_net.to(self.device) - self.target_ac_net = clone(self.ac_net) - self.target_ac_net.eval() - self.reward_discount = reward_discount - self.num_epochs = num_epochs - self.update_target_every = update_target_every - self.q_value_loss_func = q_value_loss_cls() - self.q_value_loss_coeff = q_value_loss_coeff - self.soft_update_coeff = soft_update_coeff - - self._ac_net_version = 0 - self._target_ac_net_version = 0 - - self._replay_memory = ReplayMemory( - replay_memory_capacity, self.ac_net.input_dim, action_dim=1, random_overwrite=random_overwrite - ) - self.warmup = warmup - self.rollout_batch_size = rollout_batch_size - self.train_batch_size = train_batch_size - - self.exploration_func = exploration_strategy[0] - self._exploration_params = clone(exploration_strategy[1]) - self.exploration_schedulers = [ - opt[1](self._exploration_params, opt[0], **opt[2]) for opt in exploration_scheduling_options - ] - - def __call__(self, states: np.ndarray): - if self._replay_memory.size < self.warmup: - return np.random.uniform( - low=self.ac_net.out_min, high=self.ac_net.out_max, - size=(states.shape[0] if len(states.shape) > 1 else 1, self.ac_net.action_dim) - ) - - self.ac_net.eval() - states = torch.from_numpy(states).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - with torch.no_grad(): - actions = self.ac_net.get_action(states).cpu().numpy() - - if not self.greedy: - actions = self.exploration_func(states, actions, **self._exploration_params) - return actions - - def record( - self, - key: str, - state: np.ndarray, - action: Union[int, float, np.ndarray], - reward: float, - next_state: np.ndarray, - terminal: bool - ): - if next_state is None: - next_state = np.zeros(state.shape, dtype=np.float32) - - self._replay_memory.put( - np.expand_dims(state, axis=0), - np.expand_dims(action, axis=0), - np.expand_dims(reward, axis=0), - np.expand_dims(next_state, axis=0), - np.expand_dims(terminal, axis=0) - ) - - def get_rollout_info(self): - """Randomly sample a batch of transitions from the replay memory. - - This is used in a distributed learning setting and the returned data will be sent to its parent instance - on the learning side (serving as the source of the latest model parameters) for training. - """ - return self._replay_memory.sample(self.rollout_batch_size) - - def get_batch_loss(self, batch: dict, explicit_grad: bool = False) -> dict: - """Compute loss for a data batch. - - Args: - batch (dict): A batch containing "states", "actions", "rewards", "next_states" and "terminals" as keys. - explicit_grad (bool): If True, the gradients should be returned as part of the loss information. Defaults - to False. - """ - self.ac_net.train() - states = torch.from_numpy(batch["states"]).to(self.device) - next_states = torch.from_numpy(["next_states"]).to(self.device) - actual_actions = torch.from_numpy(batch["actions"]).to(self.device) - rewards = torch.from_numpy(batch["rewards"]).to(self.device) - terminals = torch.from_numpy(batch["terminals"]).float().to(self.device) - if len(actual_actions.shape) == 1: - actual_actions = actual_actions.unsqueeze(dim=1) # (N, 1) - - with torch.no_grad(): - next_q_values = self.target_ac_net.value(next_states) - target_q_values = (rewards + self.reward_discount * (1 - terminals) * next_q_values).detach() # (N,) - - # loss info - loss_info = {} - q_values = self.ac_net(states, actions=actual_actions).squeeze(dim=1) # (N,) - q_loss = self.q_value_loss_func(q_values, target_q_values) - policy_loss = -self.ac_net.value(states).mean() - loss = policy_loss + self.q_value_loss_coeff * q_loss - loss_info = { - "policy_loss": policy_loss.detach().cpu().numpy(), - "q_loss": q_loss.detach().cpu().numpy(), - "loss": loss.detach().cpu().numpy() if explicit_grad else loss - } - if explicit_grad: - loss_info["grad"] = self.ac_net.get_gradients(loss) - - return loss_info - - def update(self, loss_info_list: List[dict]): - """Update the model parameters with gradients computed by multiple gradient workers. - - Args: - loss_info_list (List[dict]): A list of dictionaries containing loss information (including gradients) - computed by multiple gradient workers. - """ - self.ac_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) - if self._ac_net_version - self._target_ac_net_version == self.update_target_every: - self._update_target() - - def learn(self, batch: dict): - """Learn from a batch containing data required for policy improvement. - - Args: - batch (dict): A batch containing "states", "actions", "rewards", "next_states" and "terminals" as keys. - """ - self._replay_memory.put( - batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] - ) - self.improve() - - def improve(self): - """Learn using data from the replay memory.""" - for _ in range(self.num_epochs): - train_batch = self._replay_memory.sample(self.train_batch_size) - self.ac_net.step(self.get_batch_loss(train_batch)["loss"]) - self._ac_net_version += 1 - if self._ac_net_version - self._target_ac_net_version == self.update_target_every: - self._update_target() - - def learn_with_data_parallel(self, batch: dict, worker_id_list: list): - assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." - - self._replay_memory.put( - batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] - ) - for _ in range(self.num_epochs): - msg_dict = defaultdict(lambda: defaultdict(dict)) - for worker_id in worker_id_list: - msg_dict[worker_id][MsgKey.GRAD_TASK][self._name] = self._replay_memory.sample( - self.train_batch_size // len(worker_id_list)) - msg_dict[worker_id][MsgKey.POLICY_STATE][self._name] = self.get_state() - # data-parallel by multiple hosts/workers - self._proxy.isend(SessionMessage( - MsgTag.COMPUTE_GRAD, self._proxy.name, worker_id, body=msg_dict[worker_id])) - dones = 0 - loss_info_by_policy = {self._name: []} - for msg in self._proxy.receive(): - if msg.tag == MsgTag.COMPUTE_GRAD_DONE: - for policy_name, loss_info in msg.body[MsgKey.LOSS_INFO].items(): - if isinstance(loss_info, list): - loss_info_by_policy[policy_name] += loss_info - elif isinstance(loss_info, dict): - loss_info_by_policy[policy_name].append(loss_info["grad"]) - else: - raise TypeError(f"Wrong type of loss_info: {type(loss_info)}") - dones += 1 - if dones == len(msg_dict): - break - # build dummy computation graph by `get_batch_loss` before apply gradients. - # batch_size=2 because torch.nn.functional.batch_norm doesn't support batch_size=1. - _ = self.get_batch_loss(self._replay_memory.sample(2), explicit_grad=True) - self.update(loss_info_by_policy[self._name]) - - def _update_target(self): - # soft-update target network - self.target_ac_net.soft_update(self.ac_net, self.soft_update_coeff) - self._target_ac_net_version = self._ac_net_version - - def exploration_step(self): - for sch in self.exploration_schedulers: - sch.step() - - def get_state(self): - return self.ac_net.get_state() - - def set_state(self, state): - self.ac_net.set_state(state) - - def load(self, path: str): - """Load the policy state from disk.""" - checkpoint = torch.load(path) - self.ac_net.set_state(checkpoint["ac_net"]) - self._ac_net_version = checkpoint["ac_net_version"] - self.target_ac_net.set_state(checkpoint["target_ac_net"]) - self._target_ac_net_version = checkpoint["target_ac_net_version"] - self._replay_memory = checkpoint["replay_memory"] - - def save(self, path: str): - """Save the policy state to disk.""" - policy_state = { - "ac_net": self.ac_net.get_state(), - "ac_net_version": self._ac_net_version, - "target_ac_net": self.target_ac_net.get_state(), - "target_ac_net_version": self._target_ac_net_version, - "replay_memory": self._replay_memory - } - torch.save(policy_state, path) diff --git a/maro/rl/policy/discrete_rl_policy.py b/maro/rl/policy/discrete_rl_policy.py new file mode 100644 index 000000000..57694331b --- /dev/null +++ b/maro/rl/policy/discrete_rl_policy.py @@ -0,0 +1,315 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABCMeta +from typing import Callable, Dict, List, Tuple + +import numpy as np +import torch + +from maro.rl.exploration import epsilon_greedy +from maro.rl.model import DiscretePolicyNet, DiscreteQNet +from maro.rl.utils import match_shape, ndarray_to_tensor +from maro.utils import clone + +from .abs_policy import RLPolicy + + +class DiscreteRLPolicy(RLPolicy, metaclass=ABCMeta): + """RL policy for discrete action spaces. + + Args: + name (str): Name of the policy. + state_dim (int): Dimension of states. + action_num (int): Number of actions. + trainable (bool, default=True): Whether this policy is trainable. + """ + + def __init__( + self, + name: str, + state_dim: int, + action_num: int, + trainable: bool = True, + ) -> None: + assert action_num >= 1 + + super(DiscreteRLPolicy, self).__init__( + name=name, state_dim=state_dim, action_dim=1, trainable=trainable, + ) + + self._action_num = action_num + + @property + def action_num(self) -> int: + return self._action_num + + def _post_check(self, states: torch.Tensor, actions: torch.Tensor) -> bool: + return all([0 <= action < self.action_num for action in actions.cpu().numpy().flatten()]) + + +class ValueBasedPolicy(DiscreteRLPolicy): + """Valued-based policy. + + Args: + name (str): Name of the policy. + q_net (DiscreteQNet): Q-net used in this value-based policy. + trainable (bool, default=True): Whether this policy is trainable. + exploration_strategy (Tuple[Callable, dict], default=(epsilon_greedy, {"epsilon": 0.1})): Exploration strategy. + exploration_scheduling_options (List[tuple], default=None): List of exploration scheduler options. + warmup (int, default=50000): Minimum number of experiences to warm up this policy. + """ + + def __init__( + self, + name: str, + q_net: DiscreteQNet, + trainable: bool = True, + exploration_strategy: Tuple[Callable, dict] = (epsilon_greedy, {"epsilon": 0.1}), + exploration_scheduling_options: List[tuple] = None, + warmup: int = 50000, + ) -> None: + assert isinstance(q_net, DiscreteQNet) + + super(ValueBasedPolicy, self).__init__( + name=name, state_dim=q_net.state_dim, action_num=q_net.action_num, trainable=trainable, + ) + self._q_net = q_net + + self._exploration_func = exploration_strategy[0] + self._exploration_params = clone(exploration_strategy[1]) # deep copy is needed to avoid unwanted sharing + self._exploration_schedulers = [ + opt[1](self._exploration_params, opt[0], **opt[2]) for opt in exploration_scheduling_options + ] + + self._call_cnt = 0 + self._warmup = warmup + + @property + def q_net(self) -> DiscreteQNet: + return self._q_net + + def q_values_for_all_actions(self, states: np.ndarray) -> np.ndarray: + """Generate a matrix containing the Q-values for all actions for the given states. + + Args: + states (np.ndarray): States. + + Returns: + q_values (np.ndarray): Q-matrix. + """ + return self.q_values_for_all_actions_tensor(ndarray_to_tensor(states, self._device)).cpu().numpy() + + def q_values_for_all_actions_tensor(self, states: torch.Tensor) -> torch.Tensor: + """Generate a matrix containing the Q-values for all actions for the given states. + + Args: + states (torch.Tensor): States. + + Returns: + q_values (torch.Tensor): Q-matrix. + """ + assert self._shape_check(states=states) + q_values = self._q_net.q_values_for_all_actions(states) + assert match_shape(q_values, (states.shape[0], self.action_num)) # [B, action_num] + return q_values + + def q_values(self, states: np.ndarray, actions: np.ndarray) -> np.ndarray: + """Generate the Q values for given state-action pairs. + + Args: + states (np.ndarray): States. + actions (np.ndarray): Actions. Should has same length with states. + + Returns: + q_values (np.ndarray): Q-values. + """ + return self.q_values_tensor( + ndarray_to_tensor(states, self._device), + ndarray_to_tensor(actions, self._device) + ).cpu().numpy() + + def q_values_tensor(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + """Generate the Q values for given state-action pairs. + + Args: + states (torch.Tensor): States. + actions (torch.Tensor): Actions. Should has same length with states. + + Returns: + q_values (torch.Tensor): Q-values. + """ + assert self._shape_check(states=states, actions=actions) # actions: [B, 1] + q_values = self._q_net.q_values(states, actions) + assert match_shape(q_values, (states.shape[0],)) # [B] + return q_values + + def explore(self) -> None: + pass # Overwrite the base method and turn off explore mode. + + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + self._call_cnt += 1 + if self._call_cnt <= self._warmup: + return ndarray_to_tensor(np.random.randint(self.action_num, size=(states.shape[0], 1)), self._device) + + q_matrix = self.q_values_for_all_actions_tensor(states) # [B, action_num] + _, actions = q_matrix.max(dim=1) # [B], [B] + + if exploring: + actions = self._exploration_func(states, actions.cpu().numpy(), self.action_num, **self._exploration_params) + actions = ndarray_to_tensor(actions, self._device) + return actions.unsqueeze(1) # [B, 1] + + def step(self, loss: torch.Tensor) -> None: + return self._q_net.step(loss) + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + return self._q_net.get_gradients(loss) + + def apply_gradients(self, grad: dict) -> None: + self._q_net.apply_gradients(grad) + + def freeze(self) -> None: + self._q_net.freeze() + + def unfreeze(self) -> None: + self._q_net.unfreeze() + + def eval(self) -> None: + self._q_net.eval() + + def train(self) -> None: + self._q_net.train() + + def get_state(self) -> object: + return self._q_net.get_state() + + def set_state(self, policy_state: object) -> None: + self._q_net.set_state(policy_state) + + def soft_update(self, other_policy: RLPolicy, tau: float) -> None: + assert isinstance(other_policy, ValueBasedPolicy) + self._q_net.soft_update(other_policy.q_net, tau) + + def _to_device_impl(self, device: torch.device) -> None: + self._q_net.to(device) + + +class DiscretePolicyGradient(DiscreteRLPolicy): + """Policy gradient for discrete action spaces. + + Args: + name (str): Name of the policy. + policy_net (DiscretePolicyNet): The core net of this policy. + trainable (bool, default=True): Whether this policy is trainable. + """ + + def __init__( + self, + name: str, + policy_net: DiscretePolicyNet, + trainable: bool = True, + ) -> None: + assert isinstance(policy_net, DiscretePolicyNet) + + super(DiscretePolicyGradient, self).__init__( + name=name, state_dim=policy_net.state_dim, action_num=policy_net.action_num, + trainable=trainable, + ) + + self._policy_net = policy_net + + @property + def policy_net(self) -> DiscretePolicyNet: + return self._policy_net + + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + return self._policy_net.get_actions(states, exploring) + + def step(self, loss: torch.Tensor) -> None: + self._policy_net.step(loss) + + def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + return self._policy_net.get_gradients(loss) + + def apply_gradients(self, grad: dict) -> None: + self._policy_net.apply_gradients(grad) + + def freeze(self) -> None: + self._policy_net.freeze() + + def unfreeze(self) -> None: + self._policy_net.unfreeze() + + def eval(self) -> None: + self._policy_net.eval() + + def train(self) -> None: + self._policy_net.train() + + def get_state(self) -> object: + return self._policy_net.get_state() + + def set_state(self, policy_state: object) -> None: + self._policy_net.set_state(policy_state) + + def soft_update(self, other_policy: RLPolicy, tau: float) -> None: + assert isinstance(other_policy, DiscretePolicyGradient) + self._policy_net.soft_update(other_policy.policy_net, tau) + + def get_action_probs(self, states: torch.Tensor) -> torch.Tensor: + """Get the probabilities for all actions according to states. + + Args: + states (torch.Tensor): States. + + Returns: + action_probs (torch.Tensor): Action probabilities with shape [batch_size, action_num]. + """ + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + action_probs = self._policy_net.get_action_probs(states) + assert match_shape(action_probs, (states.shape[0], self.action_num)), \ + f"Action probabilities shape check failed. Expecting: {(states.shape[0], self.action_num)}, " \ + f"actual: {action_probs.shape}." + return action_probs + + def get_action_logps(self, states: torch.Tensor) -> torch.Tensor: + """Get the log-probabilities for all actions according to states. + + Args: + states (torch.Tensor): States. + + Returns: + action_logps (torch.Tensor): Action probabilities with shape [batch_size, action_num]. + """ + return torch.log(self.get_action_probs(states)) + + def get_state_action_probs(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + """Get the probabilities of the given state-action pairs. + + Args: + states (torch.Tensor): States. + actions (torch.Tensor): Actions. Should has same length with states. + + Returns: + action_probs (torch.Tensor): Probabilities of the given state-action pairs. + """ + assert self._shape_check(states=states, actions=actions) + action_probs = self.get_action_probs(states) + return action_probs.gather(1, actions).squeeze() # [B] + + def get_state_action_logps(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + """Get the log-probabilities of the given state-action pairs. + + Args: + states (torch.Tensor): States. + actions (torch.Tensor): Actions. Should has same length with states. + + Returns: + action_logps (torch.Tensor): Probabilities of the given state-action pairs. + """ + return torch.log(self.get_state_action_probs(states, actions)) + + def _to_device_impl(self, device: torch.device) -> None: + self._policy_net.to(device) diff --git a/maro/rl/policy/dqn.py b/maro/rl/policy/dqn.py deleted file mode 100644 index 4fd6a7eb4..000000000 --- a/maro/rl/policy/dqn.py +++ /dev/null @@ -1,443 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from typing import Callable, List, Tuple, Union - -import numpy as np -import torch - -from maro.communication import SessionMessage -from maro.rl.exploration import epsilon_greedy -from maro.rl.modeling import DiscreteQNet -from maro.rl.utils import MsgKey, MsgTag, average_grads -from maro.utils import clone - -from .policy import RLPolicy -from .replay import ReplayMemory - - -class PrioritizedExperienceReplay: - """Prioritized Experience Replay (PER). - - References: - https://arxiv.org/pdf/1511.05952.pdf - https://github.com/rlcode/per - - The implementation here is based on direct proportional prioritization (the first variant in the paper). - The rank-based variant is not implemented here. - - Args: - replay_memory (ReplayMemory): experience manager the sampler is associated with. - alpha (float): Prioritization strength. Sampling probabilities are calculated according to - P = p_i^alpha / sum(p_k^alpha). Defaults to 0.6. - beta (float): Bias annealing strength using weighted importance sampling (IS) techniques. - IS weights are calculated according to (N * P)^(-beta), where P is the sampling probability. - This value of ``beta`` should not exceed 1.0, which corresponds to full annealing. Defaults to 0.4. - beta_step (float): The amount ``beta`` is incremented by after each get() call until it reaches 1.0. - Defaults to 0.001. - max_priority (float): Maximum priority value to use for new experiences. Defaults to 1e8. - """ - def __init__( - self, - replay_memory: ReplayMemory, - *, - alpha: float = 0.6, - beta: float = 0.4, - beta_step: float = 0.001, - max_priority: float = 1e8 - ): - if beta > 1.0: - raise ValueError("beta should be between 0.0 and 1.0") - self._replay_memory = replay_memory - self._sum_tree = np.zeros(2 * self._replay_memory.capacity - 1) - self.alpha = alpha - self.beta = beta - self.beta_step = beta_step - self.eps = 1e-7 - self._max_priority = max_priority - - def total(self): - """Return the sum of priorities over all experiences.""" - return self._sum_tree[0] - - def set_max_priority(self, indexes): - """Set the priorities of newly added experiences to the maximum value.""" - self.update(indexes, [self._max_priority] * len(indexes)) - - def update(self, indexes, td_errors): - """Update priority values at given indexes.""" - for idx, err in zip(indexes, td_errors): - priority = self._get_priority(err) - tree_idx = idx + self._replay_memory.capacity - 1 - delta = priority - self._sum_tree[tree_idx] - self._sum_tree[tree_idx] = priority - self._update(tree_idx, delta) - - def sample(self, size: int): - """Priority-based sampling.""" - indexes, priorities = [], [] - segment_len = self.total() / size - for i in range(size): - low, high = segment_len * i, segment_len * (i + 1) - sampled_val = np.random.uniform(low=low, high=high) - idx = self._get(0, sampled_val) - data_idx = idx - self._replay_memory.capacity + 1 - indexes.append(data_idx) - priorities.append(self._sum_tree[idx]) - - self.beta = min(1., self.beta + self.beta_step) - sampling_probabilities = priorities / (self.total() + 1e-8) - is_weights = np.power(self._replay_memory.size * sampling_probabilities, -self.beta) - is_weights /= (is_weights.max() + 1e-8) - - return indexes, is_weights - - def _get_priority(self, error): - if isinstance(error, torch.Tensor): - error = error.detach().numpy() - return (np.abs(error) + self.eps) ** self.alpha - - def _update(self, idx, delta): - """Propagate priority change all the way to the root node.""" - parent = (idx - 1) // 2 - self._sum_tree[parent] += delta - if parent != 0: - self._update(parent, delta) - - def _get(self, idx, sampled_val): - """Get a leaf node according to a randomly sampled value.""" - left = 2 * idx + 1 - right = left + 1 - - if left >= len(self._sum_tree): - return idx - - if sampled_val <= self._sum_tree[left]: - return self._get(left, sampled_val) - else: - return self._get(right, sampled_val - self._sum_tree[left]) - - -class DQN(RLPolicy): - """The Deep-Q-Networks algorithm. - - See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. - - Args: - name (str): Unique identifier for the policy. - q_net (DiscreteQNet): Q-value model. - reward_discount (float): Reward decay as defined in standard RL terminology. - num_epochs (int): Number of training epochs per call to ``learn``. Defaults to 1. - update_target_every (int): Number of gradient steps between target model updates. - soft_update_coeff (float): Soft update coefficient, e.g., - target_model = (soft_update_coeff) * eval_model + (1-soft_update_coeff) * target_model. - Defaults to 1.0. - double (bool): If True, the next Q values will be computed according to the double DQN algorithm, - i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). - See https://arxiv.org/pdf/1509.06461.pdf for details. Defaults to False. - exploration_strategy (Tuple[Callable, dict]): A 2-tuple that consists of a) a function that takes a state - (single or batch), an action (single or batch), the total number of possible actions and a set of keyword - arguments, and returns an exploratory action (single or batch depending on the input), and b) a dictionary - of keyword arguments for the function in a) (this will be assigned to the ``_exploration_params`` member - variable). Defaults to (``epsilon_greedy``, {"epsilon": 0.1}). - exploration_scheduling_options (List[tuple]): A list of 3-tuples specifying the exploration schedulers to be - registered to the exploration parameters. Each tuple consists of an exploration parameter name, an - exploration scheduler class (subclass of ``AbsExplorationScheduler``) and keyword arguments for that class. - The exploration parameter name must be a key in the keyword arguments (second element) of - ``exploration_strategy``. Defaults to an empty list. - replay_memory_capacity (int): Capacity of the replay memory. Defaults to 1000000. - random_overwrite (bool): This specifies overwrite behavior when the replay memory capacity is reached. If True, - overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with - wrap-around. Defaults to False. - warmup (int): When the total number of experiences in the replay memory is below this threshold, - ``choose_action`` will return uniformly random actions for warm-up purposes. Defaults to 50000. - rollout_batch_size (int): Size of the experience batch to use as roll-out information by calling - ``get_rollout_info``. Defaults to 1000. - train_batch_size (int): Batch size for training the Q-net. Defaults to 32. - prioritized_replay_kwargs (dict): Keyword arguments for prioritized experience replay. See - ``PrioritizedExperienceReplay`` for details. Defaults to None, in which case experiences will be sampled - from the replay memory uniformly randomly. - device (str): Identifier for the torch device. The ``q_net`` will be moved to the specified device. If it is - None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. - """ - - def __init__( - self, - name: str, - q_net: DiscreteQNet, - reward_discount: float = 0.9, - num_epochs: int = 1, - update_target_every: int = 5, - soft_update_coeff: float = 0.1, - double: bool = False, - exploration_strategy: Tuple[Callable, dict] = (epsilon_greedy, {"epsilon": 0.1}), - exploration_scheduling_options: List[tuple] = [], - replay_memory_capacity: int = 1000000, - random_overwrite: bool = False, - warmup: int = 50000, - rollout_batch_size: int = 1000, - train_batch_size: int = 32, - prioritized_replay_kwargs: dict = None, - device: str = None - ): - if not isinstance(q_net, DiscreteQNet): - raise TypeError("model must be an instance of 'DiscreteQNet'") - - if any(opt[0] not in exploration_strategy[1] for opt in exploration_scheduling_options): - raise ValueError( - f"The first element of an exploration scheduling option must be one of " - f"{list(exploration_strategy[1].keys())}" - ) - - super().__init__(name) - if device is None: - self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - else: - self.device = torch.device(device) - self.q_net = q_net.to(self.device) - self.target_q_net = clone(q_net) - self.target_q_net.eval() - self._q_net_version = 0 - self._target_q_net_version = 0 - - self._num_actions = self.q_net.num_actions - self.reward_discount = reward_discount - self.num_epochs = num_epochs - self.update_target_every = update_target_every - self.soft_update_coeff = soft_update_coeff - self.double = double - - self._replay_memory = ReplayMemory( - replay_memory_capacity, self.q_net.input_dim, action_dim=1, random_overwrite=random_overwrite - ) - self.warmup = warmup - self.rollout_batch_size = rollout_batch_size - self.train_batch_size = train_batch_size - self.prioritized_replay = prioritized_replay_kwargs is not None - if self.prioritized_replay: - self._per = PrioritizedExperienceReplay(self._replay_memory, **prioritized_replay_kwargs) - else: - self._loss_func = torch.nn.MSELoss() - - self.exploration_func = exploration_strategy[0] - self._exploration_params = clone(exploration_strategy[1]) # deep copy is needed to avoid unwanted sharing - self.exploration_schedulers = [ - opt[1](self._exploration_params, opt[0], **opt[2]) for opt in exploration_scheduling_options - ] - - def __call__(self, states: np.ndarray): - if self._replay_memory.size < self.warmup: - return np.random.randint(self._num_actions, size=(states.shape[0] if len(states.shape) > 1 else 1,)) - - self.q_net.eval() - states = torch.from_numpy(states).to(self.device) - if len(states.shape) == 1: - states = states.unsqueeze(dim=0) - with torch.no_grad(): - q_for_all_actions = self.q_net(states) # (batch_size, num_actions) - _, actions = q_for_all_actions.max(dim=1) - - if self.greedy: - return actions.cpu().numpy() - else: - return self.exploration_func(states, actions.cpu().numpy(), self._num_actions, **self._exploration_params) - - def record( - self, - key: str, - state: np.ndarray, - action: Union[int, float, np.ndarray], - reward: float, - next_state: np.ndarray, - terminal: bool - ): - if next_state is None: - next_state = np.zeros(state.shape, dtype=np.float32) - - indexes = self._replay_memory.put( - np.expand_dims(state, axis=0), - np.expand_dims(action, axis=0), - np.expand_dims(reward, axis=0), - np.expand_dims(next_state, axis=0), - np.expand_dims(terminal, axis=0) - ) - if self.prioritized_replay: - self._per.set_max_priority(indexes) - - def get_rollout_info(self): - """Randomly sample a batch of transitions from the replay memory. - - This is used in a distributed learning setting and the returned data will be sent to its parent instance - on the learning side (serving as the source of the latest model parameters) for training. - """ - return self._replay_memory.sample(self.rollout_batch_size) - - def _get_batch(self, batch_size: int = None): - if batch_size is None: - batch_size = self.train_batch_size - if self.prioritized_replay: - indexes, is_weights = self._per.sample(batch_size) - return { - "states": self._replay_memory.states[indexes], - "actions": self._replay_memory.actions[indexes], - "rewards": self._replay_memory.rewards[indexes], - "next_states": self._replay_memory.next_states[indexes], - "terminals": self._replay_memory.terminals[indexes], - "indexes": indexes, - "is_weights": is_weights - } - else: - return self._replay_memory.sample(self.train_batch_size) - - def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - """Compute loss for a data batch. - - Args: - batch (dict): A batch containing "states", "actions", "rewards", "next_states" and "terminals" as keys. - explicit_grad (bool): If True, the gradients should be returned as part of the loss information. Defaults - to False. - """ - self.q_net.train() - states = torch.from_numpy(batch["states"]).to(self.device) - next_states = torch.from_numpy(batch["next_states"]).to(self.device) - actions = torch.from_numpy(batch["actions"]).to(self.device) - rewards = torch.from_numpy(batch["rewards"]).to(self.device) - terminals = torch.from_numpy(batch["terminals"]).float().to(self.device) - - # get target Q values - with torch.no_grad(): - if self.double: - actions_by_eval_q_net = self.q_net.get_action(next_states)[0] - next_q_values = self.target_q_net.q_values(next_states, actions_by_eval_q_net) - else: - next_q_values = self.target_q_net.get_action(next_states)[1] # (N,) - - target_q_values = (rewards + self.reward_discount * (1 - terminals) * next_q_values).detach() # (N,) - - # loss info - loss_info = {} - q_values = self.q_net.q_values(states, actions) - td_errors = target_q_values - q_values - if self.prioritized_replay: - is_weights = torch.from_numpy(batch["is_weights"]).to(self.device) - loss = (td_errors * is_weights).mean() - loss_info["td_errors"], loss_info["indexes"] = td_errors.detach().cpu().numpy(), batch["indexes"] - else: - loss = self._loss_func(q_values, target_q_values) - - loss_info["loss"] = loss.detach().cpu().numpy() if explicit_grad else loss - if explicit_grad: - loss_info["grad"] = self.q_net.get_gradients(loss) - return loss_info - - def update(self, loss_info_list: List[dict]): - """Update the Q-net parameters with gradients computed by multiple gradient workers. - - Args: - loss_info_list (List[dict]): A list of dictionaries containing loss information (including gradients) - computed by multiple gradient workers. - """ - if self.prioritized_replay: - for loss_info in loss_info_list: - self._per.update(loss_info["indexes"], loss_info["td_errors"]) - - self.q_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) - self._q_net_version += 1 - # soft-update target network - if self._q_net_version - self._target_q_net_version == self.update_target_every: - self._update_target() - - def learn(self, batch: dict): - """Learn from a batch containing data required for policy improvement. - - Args: - batch (dict): A batch containing "states", "actions", "rewards", "next_states" and "terminals" as keys. - """ - self._replay_memory.put( - batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] - ) - self.improve() - - def improve(self): - """Learn using data from the replay memory.""" - for _ in range(self.num_epochs): - loss_info = self.get_batch_loss(self._get_batch()) - if self.prioritized_replay: - self._per.update(loss_info["indexes"], loss_info["td_errors"]) - self.q_net.step(loss_info["loss"]) - self._q_net_version += 1 - if self._q_net_version - self._target_q_net_version == self.update_target_every: - self._update_target() - - def _update_target(self): - self.target_q_net.soft_update(self.q_net, self.soft_update_coeff) - self._target_q_net_version = self._q_net_version - - def learn_with_data_parallel(self, batch: dict, worker_id_list: list): - assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." - - self._replay_memory.put( - batch["states"], batch["actions"], batch["rewards"], batch["next_states"], batch["terminals"] - ) - for _ in range(self.num_epochs): - msg_dict = defaultdict(lambda: defaultdict(dict)) - for worker_id in worker_id_list: - msg_dict[worker_id][MsgKey.GRAD_TASK][self._name] = self._get_batch( - self.train_batch_size // len(worker_id_list)) - msg_dict[worker_id][MsgKey.POLICY_STATE][self._name] = self.get_state() - # data-parallel by multiple remote gradient workers - self._proxy.isend(SessionMessage( - MsgTag.COMPUTE_GRAD, self._proxy.name, worker_id, body=msg_dict[worker_id])) - dones = 0 - loss_info_by_policy = {self._name: []} - for msg in self._proxy.receive(): - if msg.tag == MsgTag.COMPUTE_GRAD_DONE: - for policy_name, loss_info in msg.body[MsgKey.LOSS_INFO].items(): - if isinstance(loss_info, list): - loss_info_by_policy[policy_name] += loss_info - elif isinstance(loss_info, dict): - loss_info_by_policy[policy_name].append(loss_info) - else: - raise TypeError(f"Wrong type of loss_info: {type(loss_info)}") - dones += 1 - if dones == len(msg_dict): - break - # build dummy computation graph before apply gradients. - _ = self.get_batch_loss(self._get_batch(), explicit_grad=True) - self.update(loss_info_by_policy[self._name]) - - def exploration_step(self): - """Update the exploration parameters according to the exploration scheduler.""" - for sch in self.exploration_schedulers: - sch.step() - - def get_state(self): - return self.q_net.get_state() - - def set_state(self, state): - self.q_net.set_state(state) - - def load(self, path: str): - """Load the policy state from disk.""" - checkpoint = torch.load(path) - self.q_net.set_state(checkpoint["q_net"]) - self._q_net_version = checkpoint["q_net_version"] - self.target_q_net.set_state(checkpoint["target_q_net"]) - self._target_q_net_version = checkpoint["target_q_net_version"] - self._replay_memory = checkpoint["replay_memory"] - if self.prioritized_replay: - self._per = checkpoint["prioritized_replay"] - - def save(self, path: str): - """Save the policy state to disk.""" - policy_state = { - "q_net": self.q_net.get_state(), - "q_net_version": self._q_net_version, - "target_q_net": self.target_q_net.get_state(), - "target_q_net_version": self._target_q_net_version, - "replay_memory": self._replay_memory - } - if self.prioritized_replay: - policy_state["prioritized_replay"] = self._per - torch.save(policy_state, path) diff --git a/maro/rl/policy/index.py b/maro/rl/policy/index.py deleted file mode 100644 index 01571c542..000000000 --- a/maro/rl/policy/index.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .ac import ActorCritic, DiscreteACNet -from .ddpg import DDPG, ContinuousACNet -from .dqn import DQN, DiscreteQNet -from .pg import DiscretePolicyNet, PolicyGradient - -POLICY_INDEX = { - "ac": ActorCritic, - "dqn": DQN, - "ddpg": DDPG, - "pg": PolicyGradient -} - - -MODEL_INDEX = { - "ac": DiscreteACNet, - "dqn": DiscreteQNet, - "ddpg": ContinuousACNet, - "pg": DiscretePolicyNet -} - - -def get_policy_cls(policy_type): - if isinstance(policy_type, str): - if policy_type not in POLICY_INDEX: - raise KeyError(f"A string policy_type must be one of {list(POLICY_INDEX.keys())}.") - return POLICY_INDEX[policy_type] - - return policy_type - - -def get_model_cls(model_type): - if isinstance(model_type, str): - if model_type not in MODEL_INDEX: - raise KeyError(f"A string model_type must be one of {list(MODEL_INDEX.keys())}.") - return MODEL_INDEX[model_type] - - return model_type diff --git a/maro/rl/policy/pg.py b/maro/rl/policy/pg.py deleted file mode 100644 index 4c51d1a0e..000000000 --- a/maro/rl/policy/pg.py +++ /dev/null @@ -1,224 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from typing import List, Tuple - -import numpy as np -import torch - -from maro.communication import SessionMessage -from maro.rl.modeling import DiscretePolicyNet -from maro.rl.utils import MsgKey, MsgTag, average_grads, discount_cumsum - -from .policy import RLPolicy - - -class PolicyGradient(RLPolicy): - class Buffer: - """Store a sequence of transitions, i.e., a trajectory. - - Args: - state_dim (int): State vector dimension. - size (int): Buffer capacity, i.e., the maximum number of stored transitions. - """ - def __init__(self, state_dim, size: int = 10000): - self.states = np.zeros((size, state_dim), dtype=np.float32) - self.values = np.zeros(size, dtype=np.float32) - self.rewards = np.zeros(size, dtype=np.float32) - self.terminals = np.zeros(size, dtype=np.bool) - self.size = size - self._ptr = 0 - - def put(self, state: np.ndarray, action: dict, reward: float, terminal: bool = False): - self.states[self._ptr] = state - self.values[self._ptr] = action["value"] - self.rewards[self._ptr] = reward - self.terminals[self._ptr] = terminal - # increment pointer - self._ptr += 1 - if self._ptr == self.size: - self._ptr = 0 - - def get(self): - terminal = self.terminals[self._ptr - 1] - traj_slice = slice(self._last_ptr, self._ptr - (not terminal)) - self._last_ptr = self._ptr - (not terminal) - return { - "states": self.states[traj_slice], - "rewards": self.rewards[traj_slice], - "last_value": self.values[-1] - } - - """The vanilla Policy Gradient (VPG) algorithm, a.k.a., REINFORCE. - - Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - - Args: - name (str): Unique identifier for the policy. - policy_net (DiscretePolicyNet): Multi-task model that computes action distributions and state values. - It may or may not have a shared bottom stack. - reward_discount (float): Reward decay as defined in standard RL terminology. - grad_iters (int): Number of gradient steps for each batch or set of batches. Defaults to 1. - max_trajectory_len (int): Maximum trajectory length that can be held by the buffer (for each agent that uses - this policy). Defaults to 10000. - get_loss_on_rollout (bool): If True, ``get_rollout_info`` will return the loss information (including gradients) - for the trajectories stored in the buffers. The loss information, along with that from other roll-out - instances, can be passed directly to ``update``. Otherwise, it will simply process the trajectories into a - single data batch that can be passed directly to ``learn``. Defaults to False. - device (str): Identifier for the torch device. The ``policy net`` will be moved to the specified device. If it - is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. Defaults to None. - """ - def __init__( - self, - name: str, - policy_net: DiscretePolicyNet, - reward_discount: float, - grad_iters: int = 1, - max_trajectory_len: int = 10000, - get_loss_on_rollout: bool = False, - device: str = None - ): - if not isinstance(policy_net, DiscretePolicyNet): - raise TypeError("model must be an instance of 'DiscretePolicyNet'") - super().__init__(name) - if device is None: - self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - else: - self.device = torch.device(device) - self.policy_net = policy_net.to(self.device) - self.reward_discount = reward_discount - self.grad_iters = grad_iters - self.max_trajectory_len = max_trajectory_len - self.get_loss_on_rollout = get_loss_on_rollout - - self._buffer = defaultdict(lambda: self.Buffer(self.policy_net.input_dim, size=self.max_trajectory_len)) - - def __call__(self, states: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """Return a list of action information dict given a batch of states. - - An action information dict contains the action itself and the corresponding log-P value. - """ - self.policy_net.eval() - with torch.no_grad(): - actions, logps = self.policy_net.get_action(states, greedy=self.greedy) - actions, logps = actions.cpu().numpy(), logps.cpu().numpy() - return [{"action": action, "logp": logp} for action, logp in zip(actions, logps)] - - def record( - self, - key: str, - state: np.ndarray, - action: dict, - reward: float, - next_state: np.ndarray, - terminal: bool - ): - self._buffer[key].put(state, action, reward, terminal) - - def get_rollout_info(self): - """Extract information from the recorded transitions. - - Returns: - Loss (including gradients) for the latest trajectory segment in the replay buffer if ``get_loss_on_rollout`` - is True or the latest trajectory segment with pre-computed return values. - """ - if self.get_loss_on_rollout: - return self.get_batch_loss(self._get_batch(), explicit_grad=True) - else: - return self._get_batch() - - def _get_batch(self): - batch = defaultdict(list) - for buf in self._buffer: - trajectory = buf.get() - rewards = np.append(trajectory["rewards"], trajectory["last_val"]) - batch["states"].append(trajectory["states"]) - # Returns rewards-to-go, to be targets for the value function - batch["returns"].append(discount_cumsum(rewards, self.reward_discount)[:-1]) - - return {key: np.concatenate(vals) for key, vals in batch.items} - - def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - """Compute AC loss for a data batch. - - Args: - batch (dict): A batch containing "states" and "returns" as keys. - explicit_grad (bool): If True, the gradients should be returned as part of the loss information. Defaults - to False. - """ - self.policy_net.train() - returns = torch.from_numpy(np.asarray(batch["returns"])).to(self.device) - - _, logp = self.policy_net(batch["states"]) - loss = -(logp * returns).mean() - loss_info = {"loss": loss.detach().cpu().numpy() if explicit_grad else loss} - if explicit_grad: - loss_info["grad"] = self.policy_net.get_gradients(loss) - return loss_info - - def update(self, loss_info_list: List[dict]): - """Update the model parameters with gradients computed by multiple roll-out instances or gradient workers. - - Args: - loss_info_list (List[dict]): A list of dictionaries containing loss information (including gradients) - computed by multiple roll-out instances or gradient workers. - """ - self.policy_net.apply_gradients(average_grads([loss_info["grad"] for loss_info in loss_info_list])) - - def learn(self, batch: dict): - """Learn from a batch containing data required for policy improvement. - - Args: - batch (dict): A batch containing "states" and "returns" as keys. - """ - for _ in range(self.grad_iters): - self.policy_net.step(self.get_batch_loss(batch)["grad"]) - - def improve(self): - """Learn using data from the buffer.""" - self.learn(self._get_batch()) - - def learn_with_data_parallel(self, batch: dict, worker_id_list: list): - assert hasattr(self, '_proxy'), "learn_with_data_parallel is invalid before data_parallel is called." - - for _ in range(self.grad_iters): - msg_dict = defaultdict(lambda: defaultdict(dict)) - for i, worker_id in enumerate(worker_id_list): - sub_batch = {key: batch[key][i::len(worker_id_list)] for key in batch} - msg_dict[worker_id][MsgKey.GRAD_TASK][self._name] = sub_batch - msg_dict[worker_id][MsgKey.POLICY_STATE][self._name] = self.get_state() - # data-parallel - self._proxy.isend(SessionMessage( - MsgTag.COMPUTE_GRAD, self._proxy.name, worker_id, body=msg_dict[worker_id])) - dones = 0 - loss_info_by_policy = {self._name: []} - for msg in self._proxy.receive(): - if msg.tag == MsgTag.COMPUTE_GRAD_DONE: - for policy_name, loss_info in msg.body[MsgKey.LOSS_INFO].items(): - if isinstance(loss_info, list): - loss_info_by_policy[policy_name] += loss_info - elif isinstance(loss_info, dict): - loss_info_by_policy[policy_name].append(loss_info["grad"]) - else: - raise TypeError(f"Wrong type of loss_info: {type(loss_info)}") - dones += 1 - if dones == len(msg_dict): - break - # build dummy computation graph before apply gradients. - _ = self.get_batch_loss(sub_batch, explicit_grad=True) - self.policy_net.step(loss_info_by_policy[self._name]) - - def get_state(self): - return self.policy_net.get_state() - - def set_state(self, state): - self.policy_net.set_state(state) - - def load(self, path: str): - """Load the policy state from disk.""" - self.policy_net.set_state(torch.load(path)) - - def save(self, path: str): - """Save the policy state to disk.""" - torch.save(self.policy_net.get_state(), path) diff --git a/maro/rl/policy/policy.py b/maro/rl/policy/policy.py deleted file mode 100644 index 277862cf8..000000000 --- a/maro/rl/policy/policy.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from abc import ABC, abstractmethod -from typing import List - -import numpy as np - -from maro.communication import Proxy - - -class AbsPolicy(ABC): - """Abstract policy class. - - Args: - name (str): Unique identifier for the policy. - - """ - def __init__(self, name: str): - super().__init__() - self._name = name - - @property - def name(self): - return self._name - - @abstractmethod - def __call__(self, state): - raise NotImplementedError - - -class DummyPolicy(AbsPolicy): - """Dummy policy that does nothing. - - Note that the meaning of a "None" action may depend on the scenario. - """ - def __call__(self, state): - return None - - -class RLPolicy(AbsPolicy): - """Policy that learns from simulation experiences. - - Reinforcement learning (RL) policies should inherit from this. - - Args: - name (str): Name of the policy. - """ - def __init__(self, name: str): - super().__init__(name) - self._exploration_params = {} - self.greedy = True - - @property - def exploration_params(self): - return self._exploration_params - - @abstractmethod - def __call__(self, states: np.ndarray): - raise NotImplementedError - - def record(self, agent_id: str, state, action, reward, next_state, terminal: bool): - """Record a transition in an internal buffer or memory. - - Since we may have multiple agents sharing this policy, the internal buffer / memory should use the agents' - names to separate storage for these agents. The ``agent_id`` parameter serves this purpose. - """ - pass - - def get_rollout_info(self): - """Extract information from the recorded transitions. - - Implement this method if you are doing distributed learning. What this function returns will be used to update - policy parameters on the learning side (abstracted through ``AbsPolicyManager``) with or without invoking the - policy improvement algorithm, depending on the type of information. If you want the policy improvement algorithm - to be invoked on roll-out instances (i.e., in distributed fashion), this should return loss information (which - can be obtained by calling ``get_batch_loss`` function with ``explicit_grad`` set to True) to be used by - ``update`` on the learning side. If you want the policy improvement algorithm to be invoked on the learning - side, this should return a data batch to be used by ``learn`` on the learning side. See the implementation of - this function in ``ActorCritic`` for reference. - """ - pass - - def get_batch_loss(self, batch: dict, explicit_grad: bool = False): - """Compute policy improvement information, i.e., loss, from a data batch. - - This can be used as a sub-routine in ``learn`` and ``improve``, as these methods usually require computing - loss from a batch. - - Args: - batch (dict): Data batch to compute the policy improvement information for. - explicit_grad (bool): If True, the gradients should be explicitly returned. Defaults to False. - """ - pass - - def data_parallel(self, *args, **kwargs): - """"Initialize a proxy in the policy, for data-parallel training. - Using the same arguments as `Proxy`.""" - self._proxy = Proxy(*args, **kwargs) - - def data_parallel_with_existing_proxy(self, proxy): - """"Initialize a proxy in the policy with an existing one, for data-parallel training.""" - self._proxy = proxy - - def exit_data_parallel(self): - if hasattr(self, '_proxy'): - self._proxy.close() - - def learn_with_data_parallel(self): - pass - - def update(self, loss_info_list: List[dict]): - """Update with loss information computed by multiple sources. - - There are two possible scenarios where you need to implement this interface: 1) if you are doing distributed - learning and want each roll-out instance to collect information that can be used to update policy parameters - on the learning side (abstracted through ``AbsPolicyManager``) without invoking the policy improvement - algorithm. Such information usually includes gradients with respect to the policy parameters. An example where - this can be useful is the Asynchronous Advantage Actor Acritic (A3C) (https://arxiv.org/abs/1602.01783); - 2) if you are computing loss in data-parallel fashion, i.e., by splitting a data batch to several smaller - batches and sending them to a set of remote workers for parallelized loss computation. - - Args: - loss_info_list (List[dict]): A list of dictionaries containing loss information (e.g., gradients) computed - by multiple sources. - """ - pass - - def learn(self, batch: dict): - """Learn from a batch of roll-out data. - - Implement this interface if you are doing distributed learning and want the roll-out instances to collect - information that can be used to update policy parameters on the learning side (abstracted through - ``AbsPolicyManager``) using the policy improvement algorithm. - - Args: - batch (dict): Training data to train the policy with. - """ - pass - - def improve(self): - """Learn using data collected locally. - - Implement this interface if you are doing single-threaded learning where a single policy instance is used for - roll-out and training. The policy should have some kind of internal buffer / memory to store roll-out data and - use as the source of training data. - """ - pass - - @abstractmethod - def get_state(self): - """Return the current state of the policy. - - The implementation must be in correspondence with that of ``set_state``. For example, if a torch model - is contained in the policy, ``get_state`` may include a call to ``state_dict()`` on the model, while - ``set_state`` should accordingly include ``load_state_dict()``. - """ - pass - - @abstractmethod - def set_state(self, policy_state): - """Set the policy state to ``policy_state``. - - The implementation must be in correspondence with that of ``get_state``. For example, if a torch model - is contained in the policy, ``set_state`` may include a call to ``load_state_dict()`` on the model, while - ``get_state`` should accordingly include ``state_dict()``. - """ - pass - - def load(self, path: str): - """Load the policy state from disk.""" - pass - - def save(self, path: str): - """Save the policy state to disk.""" - pass diff --git a/maro/rl/policy/replay.py b/maro/rl/policy/replay.py deleted file mode 100644 index 567820b2f..000000000 --- a/maro/rl/policy/replay.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import numpy as np - - -class ReplayMemory: - """Storage facility for simulation experiences. - - This implementation uses a dictionary of lists as the internal data structure. The objects for each key - are stored in a list. - - Args: - capacity (int): Maximum number of experiences that can be stored. - state_dim (int): Dimension of flattened state. - action_dim (int): Action dimension. Defaults to 1. - random_overwrite (bool): This specifies overwrite behavior when the capacity is reached. If this is True, - overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with - wrap-around. Defaults to False. - """ - def __init__(self, capacity: int, state_dim: int, action_dim: int = 1, random_overwrite: bool = False): - super().__init__() - self._state_dim = state_dim - self._action_dim = action_dim - self._capacity = capacity - self._random_overwrite = random_overwrite - self.states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) - if action_dim > 1: - self.actions = np.zeros((self._capacity, self._action_dim), dtype=np.float32) - else: - self.actions = np.zeros(self._capacity, dtype=np.int64) - self.rewards = np.zeros(self._capacity, dtype=np.float32) - self.next_states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) - self.terminals = np.zeros(self._capacity, dtype=np.bool) - self._ptr = 0 - - @property - def capacity(self): - """Capacity of the memory.""" - return self._capacity - - @property - def random_overwrite(self): - """Overwrite method after the memory has reached capacity.""" - return self._random_overwrite - - @property - def size(self): - """Current number of experiences stored.""" - return self._ptr - - def put( - self, - states: np.ndarray, - actions: np.ndarray, - rewards: np.ndarray, - next_states: np.ndarray, - terminals: np.ndarray - ): - """Put SARS and terminal flags in the memory.""" - assert len(states) == len(actions) == len(rewards) == len(next_states) == len(terminals) - added = len(states) - if added > self._capacity: - raise ValueError("size of added items should not exceed the capacity.") - - if self._ptr + added <= self._capacity: - indexes = np.arange(self._ptr, self._ptr + added) - # follow the overwrite rule set at init - else: - overwrites = self._ptr + added - self._capacity - indexes = np.concatenate([ - np.arange(self._ptr, self._capacity), - np.random.choice(self._ptr, size=overwrites, replace=False) if self._random_overwrite - else np.arange(overwrites) - ]) - - self.states[indexes] = states - self.actions[indexes] = actions - self.rewards[indexes] = rewards - self.next_states[indexes] = next_states - - self._ptr = min(self._ptr + added, self._capacity) - return indexes - - def sample(self, size: int): - """Obtain a random sample.""" - indexes = np.random.choice(self._ptr, size=size) - return { - "states": self.states[indexes], - "actions": self.actions[indexes], - "rewards": self.rewards[indexes], - "next_states": self.next_states[indexes], - "terminals": self.terminals[indexes] - } diff --git a/maro/rl/policy/worker_allocator.py b/maro/rl/policy/worker_allocator.py deleted file mode 100644 index 1e4894b06..000000000 --- a/maro/rl/policy/worker_allocator.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from collections import defaultdict -from enum import Enum -from typing import Dict - - -class AllocationMode(Enum): - BY_POLICY = "by-policy" - BY_AGENT = "by-agent" - BY_EXPERIENCE = "by-experience" - - -class WorkerAllocator(object): - """Allocate workers following some strategy.""" - def __init__(self, mode: str, num_workers: int, policy_names: list, agent2policy: dict): - assert num_workers > 0, f"Invalid arguments: num_workers should be greater than 0 instead of {num_workers}." - assert len(policy_names) > 0, "Invalid arguments: policy_names should not be empty." - assert len(agent2policy) > 0, "Invalid arguments: agent2policy should not be empty." - self.mode = mode - self.num_workers = num_workers - self.policy_names = policy_names - self.agent2policy = agent2policy - self.worker_prefix = "GRAD_WORKER" - self.logger = None - - self._cached_mappings = dict() - - def set_logger(self, logger): - self.logger = logger - - def allocate(self, **kwargs): - if self.mode not in self._cached_mappings: - if self.mode == AllocationMode.BY_POLICY.value: - self._cached_mappings[self.mode] = self.allocate_by_policy() - elif self.mode == AllocationMode.BY_AGENT.value: - self._cached_mappings[self.mode] = self.allocate_by_agent() - elif self.mode == AllocationMode.BY_EXPERIENCE.value: - assert 'num_experiences_by_policy' in kwargs - num_experiences_by_policy = kwargs.pop('num_experiences_by_policy') - self._cached_mappings[self.mode] = self.allocate_by_experience(num_experiences_by_policy) - else: - raise NotImplementedError(f"{self.mode} is not implemented.") - return self._cached_mappings[self.mode] - - def allocate_by_policy(self): - """Evenly allocate grad workers to each policy.""" - policy_names = self.policy_names - num_workers = self.num_workers - policy2workers = defaultdict(list) - worker2policies = defaultdict(list) - - if len(policy_names) >= num_workers: - for i, name in enumerate(policy_names): - worker_id = i % num_workers - policy2workers[name].append(f"{self.worker_prefix}.{worker_id}") - worker2policies[f"{self.worker_prefix}.{worker_id}"].append(name) - else: - worker_id_list = list(range(num_workers)) - for i, name in enumerate(policy_names): - for worker_id in worker_id_list[i::len(policy_names)]: - policy2workers[name].append(f"{self.worker_prefix}.{worker_id}") - worker2policies[f"{self.worker_prefix}.{worker_id}"].append(name) - return policy2workers, worker2policies - - def allocate_by_agent(self): - agent2policy = self.agent2policy - num_agents_by_policy = {} - for agent_id, policy_name in agent2policy.items(): - num_agents_by_policy[policy_name] = num_agents_by_policy.get(policy_name, 0) + 1 - return self._allocate_by_payload(num_agents_by_policy) - - def allocate_by_experience(self, num_experiences_by_policy: dict): - return self._allocate_by_payload(num_experiences_by_policy) - - def _allocate_by_payload(self, num_payload: Dict[str, int]): - """Allocate grad workers by payload of each policy. - - Args: - num_payload (Dict[str, int]): Payload of each policy, could be experience numbers - or agent nums. - - Returns: - policy2workers (Dict[str, list]): The mapping from policy name to assigned worker ids. - worker2policies (Dict[str, list]): The mapping from worker id to according policies. - """ - num_workers = self.num_workers - policy2workers = defaultdict(list) - worker2policies = defaultdict(list) - - # no payload yet - if len(num_payload) == 0: - return self.allocate_by_policy() - # allocate workers according to historical payload. - else: - total_num_payload = sum(num_payload.values()) - average_payload = total_num_payload / num_workers - - offset = 0 - policy_quota = dict() - for name, payload in num_payload.items(): - quota = payload / average_payload - quota = max(1, int(round(quota))) - policy_quota[name] = quota - - # adjust quota if any redundancy occurs. - redundancy = num_workers - sum(policy_quota.values()) - if redundancy > 0: - busiest_policy = max(policy_quota, key=lambda name: policy_quota[name]) - policy_quota[busiest_policy] += redundancy - - for name, quota in policy_quota.items(): - if self.logger is not None: - self.logger.info( - f"policy {name} payload: {num_payload[name]}, quota: {quota} node(s)") - for i in range(quota): - worker_id = (i + offset) % num_workers - policy2workers[name].append(f"{self.worker_prefix}.{worker_id}") - worker2policies[f"{self.worker_prefix}.{worker_id}"].append(name) - offset = (offset + quota) % num_workers - - return policy2workers, worker2policies diff --git a/maro/rl/rollout/__init__.py b/maro/rl/rollout/__init__.py new file mode 100644 index 000000000..41a1995b7 --- /dev/null +++ b/maro/rl/rollout/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .batch_env_sampler import BatchEnvSampler +from .env_sampler import AbsAgentWrapper, AbsEnvSampler, CacheElement, ExpElement, SimpleAgentWrapper +from .worker import RolloutWorker + +__all__ = [ + "BatchEnvSampler", + "AbsAgentWrapper", "AbsEnvSampler", "CacheElement", "ExpElement", "SimpleAgentWrapper", + "RolloutWorker", +] diff --git a/maro/rl/rollout/batch_env_sampler.py b/maro/rl/rollout/batch_env_sampler.py new file mode 100644 index 000000000..fbc36bef9 --- /dev/null +++ b/maro/rl/rollout/batch_env_sampler.py @@ -0,0 +1,193 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import time +from itertools import chain +from typing import Dict, List, Optional, Tuple + +import zmq +from zmq import Context, Poller + +from maro.rl.utils.common import bytes_to_pyobj, get_own_ip_address, pyobj_to_bytes +from maro.utils import DummyLogger, Logger + +from .env_sampler import ExpElement + + +class ParallelTaskController(object): + """Controller that sends identical tasks to a set of remote workers and collect results from them. + + Args: + port (int, default=20000): Network port the controller uses to talk to the remote workers. + logger (Logger, default=None): Optional logger for logging key events. + """ + + def __init__(self, port: int = 20000, logger: Logger = None) -> None: + self._ip = get_own_ip_address() + self._context = Context.instance() + + # parallel task sender + self._task_endpoint = self._context.socket(zmq.ROUTER) + self._task_endpoint.setsockopt(zmq.LINGER, 0) + self._task_endpoint.bind(f"tcp://{self._ip}:{port}") + + self._poller = Poller() + self._poller.register(self._task_endpoint, zmq.POLLIN) + + self._workers = set() + self._logger = logger + + def _wait_for_workers_ready(self, k: int) -> None: + while len(self._workers) < k: + self._workers.add(self._task_endpoint.recv_multipart()[0]) + + def _recv_result_for_target_index(self, index: Tuple[int, int]) -> object: + rep = bytes_to_pyobj(self._task_endpoint.recv_multipart()[-1]) + assert isinstance(rep, dict) + return rep["result"] if rep["index"] == index else None + + def collect(self, req: dict, parallelism: int, min_replies: int = None, grace_factor: int = None) -> List[dict]: + """Send a task request to a set of remote workers and collect the results. + + Args: + req (dict): Request containing task specifications and parameters. + parallelism (int): Number of workers to send the task to. + min_replies (int, default=None): The minimum number of results to collect in one round of remote + sampling. If None, it defaults to the value of ``parallelism``. + grace_factor (float, default=None): Factor that determines the additional wait time after receiving the + minimum required replies (as determined by ``min_replies``). For example, if the minimum required + replies are received in T seconds, it will allow an additional T * grace_factor seconds to collect + the remaining results. + + Returns: + A list of results. Each element in the list is a dict that contains results from a worker. + """ + self._wait_for_workers_ready(parallelism) + if min_replies is None: + min_replies = parallelism + + start_time = time.time() + results = [] + for worker_id in list(self._workers)[:parallelism]: + self._task_endpoint.send_multipart([worker_id, pyobj_to_bytes(req)]) + self._logger.debug(f"Sent {parallelism} roll-out requests...") + + while len(results) < min_replies: + result = self._recv_result_for_target_index(req["index"]) + if result: + results.append(result) + + if grace_factor is not None: + countdown = int((time.time() - start_time) * grace_factor) * 1000 # milliseconds + self._logger.debug(f"allowing {countdown / 1000} seconds for remaining results") + while len(results) < parallelism and countdown > 0: + start = time.time() + event = dict(self._poller.poll(countdown)) + if self._task_endpoint in event: + result = self._recv_result_for_target_index(req["index"]) + if result: + results.append(result) + countdown -= time.time() - start + + self._logger.debug(f"Received {len(results)} results") + return results + + def exit(self) -> None: + """Signal the remote workers to exit and terminate the connections. + """ + for worker_id in self._workers: + self._task_endpoint.send_multipart([worker_id, b"EXIT"]) + self._task_endpoint.close() + self._context.term() + + +class BatchEnvSampler: + """Facility that samples from multiple copies of an environment in parallel. + + No environment is created here. Instead, it uses a ParallelTaskController to send roll-out requests to a set of + remote workers and collect results from them. + + Args: + sampling_parallelism (int): Parallelism for sampling from the environment. + port (int): Network port that the internal ``ParallelTaskController`` uses to talk to the remote workers. + min_env_samples (int, default=None): The minimum number of results to collect in one round of remote sampling. + If it is None, it defaults to the value of ``sampling_parallelism``. + grace_factor (float, default=None): Factor that determines the additional wait time after receiving the minimum + required env samples (as determined by ``min_env_samples``). For example, if the minimum required samples + are received in T seconds, it will allow an additional T * grace_factor seconds to collect the remaining + results. + eval_parallelism (int, default=None): Parallelism for policy evaluation on remote workers. + logger (Logger, default=None): Optional logger for logging key events. + """ + + def __init__( + self, + sampling_parallelism: int, + port: int = 20000, + min_env_samples: int = None, + grace_factor: float = None, + eval_parallelism: int = None, + logger: Logger = None, + ) -> None: + super(BatchEnvSampler, self).__init__() + self._logger = logger if logger else DummyLogger() + self._controller = ParallelTaskController(port=port, logger=logger) + + self._sampling_parallelism = 1 if sampling_parallelism is None else sampling_parallelism + self._min_env_samples = min_env_samples if min_env_samples is not None else self._sampling_parallelism + self._grace_factor = grace_factor + self._eval_parallelism = 1 if eval_parallelism is None else eval_parallelism + + self._ep = 0 + self._segment = 0 + self._end_of_episode = True + + def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Optional[int] = None) -> dict: + """Collect experiences from a set of remote roll-out workers. + + Args: + policy_state (Dict[str, object]): Policy state dict. If it is not None, then we need to update all + policies according to the latest policy states, then start the experience collection. + num_steps (Optional[int], default=None): Number of environment steps to collect experiences for. If + it is None, interactions with the (remote) environments will continue until the terminal state is + reached. + + Returns: + A dict that contains the collected experiences and additional information. + """ + # increment episode or segment depending on whether the last episode has concluded + if self._end_of_episode: + self._ep += 1 + self._segment = 1 + else: + self._segment += 1 + + self._logger.info(f"Collecting roll-out data for episode {self._ep}, segment {self._segment}") + req = { + "type": "sample", + "policy_state": policy_state, + "num_steps": num_steps, + "index": (self._ep, self._segment), + } + results = self._controller.collect( + req, self._sampling_parallelism, + min_replies=self._min_env_samples, + grace_factor=self._grace_factor, + ) + self._end_of_episode = any(res["end_of_episode"] for res in results) + merged_experiences: List[List[ExpElement]] = list(chain(*[res["experiences"] for res in results])) + return { + "end_of_episode": self._end_of_episode, + "experiences": merged_experiences, + "info": [res["info"][0] for res in results], + } + + def eval(self, policy_state: Dict[str, object] = None) -> dict: + req = {"type": "eval", "policy_state": policy_state, "index": (self._ep, -1)} # -1 signals test + results = self._controller.collect(req, self._eval_parallelism) + return { + "info": [res["info"][0] for res in results], + } + + def exit(self) -> None: + self._controller.exit() diff --git a/maro/rl/rollout/env_sampler.py b/maro/rl/rollout/env_sampler.py new file mode 100644 index 000000000..7f0650e7c --- /dev/null +++ b/maro/rl/rollout/env_sampler.py @@ -0,0 +1,419 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from __future__ import annotations + +import collections +from abc import ABCMeta, abstractmethod +from collections import defaultdict, deque +from dataclasses import dataclass +from typing import Any, Callable, Deque, Dict, List, Optional, Tuple, Type + +import numpy as np +import torch + +from maro.rl.policy import RLPolicy +from maro.simulator import Env + + +class AbsAgentWrapper(object, metaclass=ABCMeta): + """Agent wrapper. Used to manager agents & policies during experience collection. + + Args: + policy_dict (Dict[str, RLPolicy]): Dictionary that maps policy names to policy instances. + agent2policy (Dict[Any, str]): Agent name to policy name mapping. + """ + + def __init__( + self, + policy_dict: Dict[str, RLPolicy], # {policy_name: RLPolicy} + agent2policy: Dict[Any, str], # {agent_name: policy_name} + ) -> None: + self._policy_dict = policy_dict + self._agent2policy = agent2policy + + def set_policy_state(self, policy_state_dict: Dict[str, object]) -> None: + """Set policies' states. + + Args: + policy_state_dict (Dict[str, object]): Double-deck dict with format: {policy_name: policy_state}. + """ + for policy_name, policy_state in policy_state_dict.items(): + policy = self._policy_dict[policy_name] + policy.set_state(policy_state) + + def choose_actions(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[Any, np.ndarray]: + """Choose action according to the given (observable) states of all agents. + + Args: + state_by_agent (Dict[Any, np.ndarray]): Dictionary containing each agent's state vector. + The keys are agent names. + + Returns: + actions (Dict[Any, np.ndarray]): Dict that contains the action for all agents. + """ + self.switch_to_eval_mode() + with torch.no_grad(): + ret = self._choose_actions_impl(state_by_agent) + return ret + + @abstractmethod + def _choose_actions_impl(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[Any, np.ndarray]: + """Implementation of `choose_actions`. + """ + raise NotImplementedError + + @abstractmethod + def explore(self) -> None: + """Switch all policies to exploration mode. + """ + raise NotImplementedError + + @abstractmethod + def exploit(self) -> None: + """Switch all policies to exploitation mode. + """ + raise NotImplementedError + + @abstractmethod + def switch_to_eval_mode(self) -> None: + """Switch the environment sampler to evaluation mode. + """ + pass + + +class SimpleAgentWrapper(AbsAgentWrapper): + def __init__( + self, + policy_dict: Dict[str, RLPolicy], # {policy_name: RLPolicy} + agent2policy: Dict[Any, str], # {agent_name: policy_name} + ) -> None: + super(SimpleAgentWrapper, self).__init__(policy_dict=policy_dict, agent2policy=agent2policy) + + def _choose_actions_impl(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[Any, np.ndarray]: + # Aggregate states by policy + states_by_policy = defaultdict(list) # {str: list of np.ndarray} + agents_by_policy = defaultdict(list) # {str: list of str} + for agent_name, state in state_by_agent.items(): + policy_name = self._agent2policy[agent_name] + states_by_policy[policy_name].append(state) + agents_by_policy[policy_name].append(agent_name) + + action_dict = {} + for policy_name in agents_by_policy: + policy = self._policy_dict[policy_name] + states = np.vstack(states_by_policy[policy_name]) # np.ndarray + action_dict.update(zip( + agents_by_policy[policy_name], # list of str (agent name) + policy.get_actions(states) # list of action + )) + return action_dict + + def explore(self) -> None: + for policy in self._policy_dict.values(): + policy.explore() + + def exploit(self) -> None: + for policy in self._policy_dict.values(): + policy.exploit() + + def switch_to_eval_mode(self) -> None: + for policy in self._policy_dict.values(): + policy.eval() + + +@dataclass +class CacheElement: + """Raw transition information that can be post-processed into an `ExpElement`. + """ + tick: int + event: object + state: np.ndarray + agent_state_dict: Dict[Any, np.ndarray] + action_dict: Dict[Any, np.ndarray] + env_action_dict: Dict[Any, object] + + +@dataclass +class ExpElement: + """Stores the complete information for a tick. + """ + tick: int + state: np.ndarray + agent_state_dict: Dict[Any, np.ndarray] + action_dict: Dict[Any, np.ndarray] + reward_dict: Dict[Any, float] + terminal_dict: Dict[Any, bool] + next_state: Optional[np.ndarray] + next_agent_state_dict: Optional[Dict[Any, np.ndarray]] + + @property + def agent_names(self) -> list: + return sorted(self.agent_state_dict.keys()) + + @property + def num_agents(self) -> int: + return len(self.agent_state_dict) + + def split_contents(self, agent2trainer: Dict[Any, str]) -> Dict[str, ExpElement]: + """Split the ExpElement's contents by trainer. + + Args: + agent2trainer (Dict[Any, str]): Mapping of agent name and trainer name. + + Returns: + Contents (Dict[str, ExpElement]): A dict that contains the ExpElements of all trainers. The key of this + dict is the trainer name. + """ + ret = collections.defaultdict(lambda: ExpElement( + tick=self.tick, + state=self.state, + agent_state_dict={}, + action_dict={}, + reward_dict={}, + terminal_dict={}, + next_state=self.next_state, + next_agent_state_dict=None if self.next_agent_state_dict is None else {}, + )) + for agent_name in self.agent_names: + trainer_name = agent2trainer[agent_name] + ret[trainer_name].agent_state_dict[agent_name] = self.agent_state_dict[agent_name] + ret[trainer_name].action_dict[agent_name] = self.action_dict[agent_name] + ret[trainer_name].reward_dict[agent_name] = self.reward_dict[agent_name] + ret[trainer_name].terminal_dict[agent_name] = self.terminal_dict[agent_name] + if self.next_agent_state_dict is not None and agent_name in self.next_agent_state_dict: + ret[trainer_name].next_agent_state_dict[agent_name] = self.next_agent_state_dict[agent_name] + return ret + + +class AbsEnvSampler(object, metaclass=ABCMeta): + """Simulation data collector and policy evaluator. + + Args: + get_env (Callable[[], Env]): Function used to create the rollout environment. + policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict of functions that used to get policies, specified + by policy names. + agent2policy (Dict[Any, str]): Mapping of agent name and policy name. + agent_wrapper_cls (Type[AbsAgentWrapper], default=SimpleAgentWrapper): Specific AgentWrapper type. + reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward + for the action taken for that event. + get_test_env (Callable[[], Env], default=None): Function used to create the testing environment. If it is None, + reuse the rollout environment as the testing environment. + device (str, default=None): Name of the device to store this AbsEnvSampler. If it is None, the device will + be automatically determined according to the GPU availability. + """ + + def __init__( + self, + get_env: Callable[[], Env], + policy_creator: Dict[str, Callable[[str], RLPolicy]], + agent2policy: Dict[Any, str], # {agent_name: policy_name} + agent_wrapper_cls: Type[AbsAgentWrapper] = SimpleAgentWrapper, + reward_eval_delay: int = 0, + get_test_env: Callable[[], Env] = None, + device: str = None, + ) -> None: + self._learn_env = get_env() + self._test_env = get_test_env() if get_test_env is not None else self._learn_env + self._env: Optional[Env] = None + self._event = None # Need this to remember the last event if an episode is divided into multiple segments + + self._device = torch.device(device) if device is not None \ + else torch.device("cuda" if torch.cuda.is_available() else "cpu") + + self._policy_dict: Dict[str, RLPolicy] = { + policy_name: func(policy_name) for policy_name, func in policy_creator.items() + } + self._agent_wrapper = agent_wrapper_cls(self._policy_dict, agent2policy) + self._agent2policy = agent2policy + + # Global state & agent state + self._state: Optional[np.ndarray] = None + self._agent_state_dict: Dict[Any, np.ndarray] = {} + + self._trans_cache: Deque[CacheElement] = deque() + self._reward_eval_delay = reward_eval_delay + + self._info = {} + + for policy in self._policy_dict.values(): + policy.to_device(self._device) + + @abstractmethod + def _get_global_and_agent_state( + self, event: object, tick: int = None, + ) -> Tuple[Optional[np.ndarray], Dict[Any, np.ndarray]]: + """Get the global and individual agents' states. + + Args: + event (object): Event. + tick (int, default=None): Current tick. + + Returns: + Global state (np.ndarray) + Dict of agent states (Dict[Any, np.ndarray]) + """ + raise NotImplementedError + + @abstractmethod + def _translate_to_env_action(self, action_dict: Dict[Any, np.ndarray], event: object) -> Dict[Any, object]: + """Translate model-generated actions into an object that can be executed by the env. + + Args: + action_dict (Dict[Any, np.ndarray]): Action for all agents. + event (object): Decision event. + + Returns: + A dict that contains env actions for all agents. + """ + raise NotImplementedError + + @abstractmethod + def _get_reward(self, env_action_dict: Dict[Any, object], event: object, tick: int) -> Dict[Any, float]: + """Get rewards according to the env actions. + + Args: + env_action_dict (Dict[Any, object]): Dict that contains env actions for all agents. + event (object): Decision event. + tick (int): Current tick. + + Returns: + A dict that contains rewards for all agents. + """ + raise NotImplementedError + + def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Optional[int] = None) -> dict: + """Sample experiences. + + Args: + policy_state (Dict[str, object]): Policy state dict. If it is not None, then we need to update all + policies according to the latest policy states, then start the experience collection. + num_steps (Optional[int], default=None): Number of collecting steps. If it is None, interactions with + the environment will continue until the terminal state is reached. + + Returns: + A dict that contains the collected experiences and additional information. + """ + # Init the env + self._env = self._learn_env + if not self._agent_state_dict: + self._env.reset() + self._info.clear() + self._trans_cache.clear() + _, self._event, _ = self._env.step(None) + self._state, self._agent_state_dict = self._get_global_and_agent_state(self._event) + + # Update policy state if necessary + if policy_state is not None: + self.set_policy_state(policy_state) + + # Collect experience + self._agent_wrapper.explore() + steps_to_go = float("inf") if num_steps is None else num_steps + while self._agent_state_dict and steps_to_go > 0: + # Get agent actions and translate them to env actions + action_dict = self._agent_wrapper.choose_actions(self._agent_state_dict) + env_action_dict = self._translate_to_env_action(action_dict, self._event) + # Store experiences in the cache + self._trans_cache.append( + CacheElement( + tick=self._env.tick, + event=self._event, + state=self._state, + agent_state_dict=dict(self._agent_state_dict), + action_dict=action_dict, + env_action_dict=env_action_dict, + ) + ) + # Update env and get new states (global & agent) + _, self._event, done = self._env.step(list(env_action_dict.values())) + self._state, self._agent_state_dict = (None, {}) if done \ + else self._get_global_and_agent_state(self._event) + steps_to_go -= 1 + + tick_bound = self._env.tick - self._reward_eval_delay + experiences = [] + while len(self._trans_cache) > 0 and self._trans_cache[0].tick <= tick_bound: + cache_element = self._trans_cache.popleft() + + reward_dict = self._get_reward(cache_element.env_action_dict, cache_element.event, cache_element.tick) + self._post_step(cache_element, reward_dict) + + if len(self._trans_cache) > 0: + next_state = self._trans_cache[0].state + next_agent_state_dict = dict(self._trans_cache[0].agent_state_dict) + else: + next_state = self._state + next_agent_state_dict = dict(self._agent_state_dict) + + experiences.append(ExpElement( + tick=cache_element.tick, + state=cache_element.state, + agent_state_dict=cache_element.agent_state_dict, + action_dict=cache_element.action_dict, + reward_dict=reward_dict, + terminal_dict={}, # Will be processed later in `_post_polish_experiences()` + next_state=next_state, + next_agent_state_dict=next_agent_state_dict, + )) + + experiences = self._post_polish_experiences(experiences) + + return { + "end_of_episode": not self._agent_state_dict, + "experiences": [experiences], + "info": [self._info], + } + + def _post_polish_experiences(self, experiences: List[ExpElement]) -> List[ExpElement]: + """Update next_agent_state_dict & terminal_dict using the entire experience list. + + Args: + experiences (List[ExpElement]): Sequence of ExpElements. + + Returns: + The update sequence of ExpElements. + """ + latest_agent_state_dict = {} # Used to update next_agent_state_dict + have_log = set([]) # Used to update terminal_dict + for i in range(len(experiences))[::-1]: + # Update terminal_dict + for agent_name in experiences[i].agent_state_dict: + experiences[i].terminal_dict[agent_name] = (not self._agent_state_dict and agent_name not in have_log) + have_log.add(agent_name) + # Update next_agent_state_dict + for key, value in latest_agent_state_dict.items(): + if key not in experiences[i].next_agent_state_dict: + experiences[i].next_agent_state_dict[key] = value + latest_agent_state_dict.update(experiences[i].agent_state_dict) + return experiences + + def set_policy_state(self, policy_state_dict: Dict[str, object]) -> None: + """Set policies' states. + + Args: + policy_state_dict (Dict[str, object]): Double-deck dict with format: {policy_name: policy_state}. + """ + self._agent_wrapper.set_policy_state(policy_state_dict) + + def eval(self, policy_state: Dict[str, object] = None) -> dict: + self._env = self._test_env + if policy_state is not None: + self.set_policy_state(policy_state) + + self._agent_wrapper.exploit() + self._env.reset() + terminal = False + _, self._event, _ = self._env.step(None) + _, agent_state_dict = self._get_global_and_agent_state(self._event) + while not terminal: + action_dict = self._agent_wrapper.choose_actions(agent_state_dict) + env_action_dict = self._translate_to_env_action(action_dict, self._event) + _, self._event, terminal = self._env.step(list(env_action_dict.values())) + if not terminal: + _, agent_state_dict = self._get_global_and_agent_state(self._event) + return {"info": [self._info]} + + @abstractmethod + def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: + raise NotImplementedError diff --git a/maro/rl/rollout/worker.py b/maro/rl/rollout/worker.py new file mode 100644 index 000000000..2c6e77410 --- /dev/null +++ b/maro/rl/rollout/worker.py @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Callable + +from maro.rl.distributed import AbsWorker +from maro.rl.utils.common import bytes_to_pyobj, pyobj_to_bytes +from maro.utils import Logger + +from .env_sampler import AbsEnvSampler + + +class RolloutWorker(AbsWorker): + """Worker that hosts an environment simulator and executes roll-out on demand for sampling and evaluation purposes. + + Args: + idx (int): Integer identifier for the worker. It is used to generate an internal ID, "worker.{idx}", + so that the parallel roll-out controller can keep track of its connection status. + env_sampler_creator (Callable[[dict], AbsEnvSampler]): User-defined function to create an ``AbsEnvSampler`` + for roll-out purposes. + producer_host (str): IP address of the parallel task controller host to connect to. + producer_port (int, default=20000): Port of the parallel task controller host to connect to. + logger (Logger, default=None): The logger of the workflow. + """ + + def __init__( + self, + idx: int, + env_sampler_creator: Callable[[], AbsEnvSampler], + producer_host: str, + producer_port: int = 20000, + logger: Logger = None, + ) -> None: + super(RolloutWorker, self).__init__( + idx=idx, producer_host=producer_host, producer_port=producer_port, logger=logger, + ) + self._env_sampler = env_sampler_creator() + + def _compute(self, msg: list) -> None: + """Perform a full or partial episode of roll-out for sampling or evaluation. + + Args: + msg (list): Multi-part message containing roll-out specifications and parameters. + """ + if msg[-1] == b"EXIT": + self._logger.info("Exiting event loop...") + self.stop() + else: + req = bytes_to_pyobj(msg[-1]) + assert isinstance(req, dict) + assert req["type"] in {"sample", "eval"} + if req["type"] == "sample": + result = self._env_sampler.sample(policy_state=req["policy_state"], num_steps=req["num_steps"]) + else: + result = self._env_sampler.eval(policy_state=req["policy_state"]) + + self._stream.send(pyobj_to_bytes({"result": result, "index": req["index"]})) diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py new file mode 100644 index 000000000..0b0e6a4cc --- /dev/null +++ b/maro/rl/training/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .proxy import TrainingProxy +from .replay_memory import FIFOMultiReplayMemory, FIFOReplayMemory, RandomMultiReplayMemory, RandomReplayMemory +from .train_ops import AbsTrainOps, RemoteOps, remote +from .trainer import AbsTrainer, MultiTrainer, SingleTrainer, TrainerParams +from .trainer_manager import TrainerManager +from .worker import TrainOpsWorker + +__all__ = [ + "TrainingProxy", + "FIFOMultiReplayMemory", "FIFOReplayMemory", "RandomMultiReplayMemory", "RandomReplayMemory", + "AbsTrainOps", "RemoteOps", "remote", + "AbsTrainer", "MultiTrainer", "SingleTrainer", "TrainerParams", + "TrainerManager", + "TrainOpsWorker", +] diff --git a/maro/rl/training/algorithms/__init__.py b/maro/rl/training/algorithms/__init__.py new file mode 100644 index 000000000..851ef4775 --- /dev/null +++ b/maro/rl/training/algorithms/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac import DiscreteActorCritic, DiscreteActorCriticParams +from .ddpg import DDPG, DDPGParams +from .dqn import DQN, DQNParams +from .maddpg import DiscreteMADDPG, DiscreteMADDPGParams + +__all__ = [ + "DiscreteActorCritic", "DiscreteActorCriticParams", + "DDPG", "DDPGParams", + "DQN", "DQNParams", + "DiscreteMADDPG", "DiscreteMADDPGParams", +] diff --git a/maro/rl/training/algorithms/ac.py b/maro/rl/training/algorithms/ac.py new file mode 100644 index 000000000..da0f3b3c3 --- /dev/null +++ b/maro/rl/training/algorithms/ac.py @@ -0,0 +1,304 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import collections +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional + +import numpy as np +import torch + +from maro.rl.model import VNet +from maro.rl.policy import DiscretePolicyGradient +from maro.rl.rollout import ExpElement +from maro.rl.training import AbsTrainOps, FIFOReplayMemory, RemoteOps, SingleTrainer, TrainerParams, remote +from maro.rl.utils import TransitionBatch, discount_cumsum, merge_transition_batches, ndarray_to_tensor + + +@dataclass +class DiscreteActorCriticParams(TrainerParams): + """ + get_v_critic_net_func (Callable[[], VNet]): Function to get V critic net. + reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. + grad_iters (int, default=1): Number of iterations to calculate gradients. + critic_loss_cls (Callable, default=None): Critic loss function. If it is None, use MSE. + clip_ratio (float, default=None): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). + If it is None, the actor loss is calculated using the usual policy gradient theorem. + lam (float, default=0.9): Lambda value for generalized advantage estimation (TD-Lambda). + min_logp (float, default=None): Lower bound for clamping logP values during learning. + This is to prevent logP from becoming very large in magnitude and causing stability issues. + If it is None, it means no lower bound. + """ + get_v_critic_net_func: Callable[[], VNet] = None + reward_discount: float = 0.9 + grad_iters: int = 1 + critic_loss_cls: Callable = None + clip_ratio: float = None + lam: float = 0.9 + min_logp: Optional[float] = None + + def __post_init__(self) -> None: + assert self.get_v_critic_net_func is not None + + def extract_ops_params(self) -> Dict[str, object]: + return { + "device": self.device, + "get_v_critic_net_func": self.get_v_critic_net_func, + "reward_discount": self.reward_discount, + "critic_loss_cls": self.critic_loss_cls, + "clip_ratio": self.clip_ratio, + "lam": self.lam, + "min_logp": self.min_logp, + } + + +class DiscreteActorCriticOps(AbsTrainOps): + """Discrete actor-critic algorithm implementation. Reference: https://tinyurl.com/2ezte4cr + """ + + def __init__( + self, + name: str, + device: str, + get_policy_func: Callable[[], DiscretePolicyGradient], + get_v_critic_net_func: Callable[[], VNet], + parallelism: int = 1, + *, + reward_discount: float = 0.9, + critic_loss_cls: Callable = None, + clip_ratio: float = None, + lam: float = 0.9, + min_logp: float = None, + ) -> None: + super(DiscreteActorCriticOps, self).__init__( + name=name, + device=device, + is_single_scenario=True, + get_policy_func=get_policy_func, + parallelism=parallelism, + ) + + assert isinstance(self._policy, DiscretePolicyGradient) + + self._reward_discount = reward_discount + self._critic_loss_func = critic_loss_cls() if critic_loss_cls is not None else torch.nn.MSELoss() + self._clip_ratio = clip_ratio + self._lam = lam + self._min_logp = min_logp + self._v_critic_net = get_v_critic_net_func() + self._v_critic_net.to(self._device) + + def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: + """Compute the critic loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The critic loss of the batch. + """ + self._v_critic_net.train() + states = ndarray_to_tensor(batch.states, self._device) # s + state_values = self._v_critic_net.v_values(states) + returns = ndarray_to_tensor(batch.returns, self._device) + return self._critic_loss_func(state_values, returns) + + @remote + def get_critic_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + """Compute the critic network's gradients of a batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + grad (torch.Tensor): The critic gradient of the batch. + """ + return self._v_critic_net.get_gradients(self._get_critic_loss(batch)) + + def update_critic(self, batch: TransitionBatch) -> None: + """Update the critic network using a batch. + + Args: + batch (TransitionBatch): Batch. + """ + self._v_critic_net.step(self._get_critic_loss(batch)) + + def update_critic_with_grad(self, grad_dict: dict) -> None: + """Update the critic network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._v_critic_net.train() + self._v_critic_net.apply_gradients(grad_dict) + + def _get_actor_loss(self, batch: TransitionBatch) -> torch.Tensor: + """Compute the actor loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The actor loss of the batch. + """ + assert isinstance(self._policy, DiscretePolicyGradient) + self._policy.train() + + states = ndarray_to_tensor(batch.states, self._device) # s + actions = ndarray_to_tensor(batch.actions, self._device).long() # a + advantages = ndarray_to_tensor(batch.advantages, self._device) + + if self._clip_ratio is not None: + self._policy.eval() + logps_old = self._policy.get_state_action_logps(states, actions) + else: + logps_old = None + + action_probs = self._policy.get_action_probs(states) + logps = torch.log(action_probs.gather(1, actions).squeeze()) + logps = torch.clamp(logps, min=self._min_logp, max=.0) + if self._clip_ratio is not None: + ratio = torch.exp(logps - logps_old) + clipped_ratio = torch.clamp(ratio, 1 - self._clip_ratio, 1 + self._clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).mean() + else: + actor_loss = -(logps * advantages).mean() # I * delta * log pi(a|s) + + return actor_loss + + @remote + def get_actor_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + """Compute the actor network's gradients of a batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + grad (torch.Tensor): The actor gradient of the batch. + """ + return self._policy.get_gradients(self._get_actor_loss(batch)) + + def update_actor(self, batch: TransitionBatch) -> None: + """Update the actor network using a batch. + + Args: + batch (TransitionBatch): Batch. + """ + self._policy.step(self._get_actor_loss(batch)) + + def update_actor_with_grad(self, grad_dict: dict) -> None: + """Update the actor network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._policy.train() + self._policy.apply_gradients(grad_dict) + + def get_state(self) -> dict: + return { + "policy": self._policy.get_state(), + "critic": self._v_critic_net.get_state(), + } + + def set_state(self, ops_state_dict: dict) -> None: + self._policy.set_state(ops_state_dict["policy"]) + self._v_critic_net.set_state(ops_state_dict["critic"]) + + def _preprocess_batch(self, batch: TransitionBatch) -> TransitionBatch: + """Preprocess the batch to get the returns & advantages. + + Args: + batch (TransitionBatch): Batch. + + Returns: + The updated batch. + """ + assert self._is_valid_transition_batch(batch) + # Preprocess returns + batch.calc_returns(self._reward_discount) + + # Preprocess advantages + states = ndarray_to_tensor(batch.states, self._device) # s + state_values = self._v_critic_net.v_values(states) + values = state_values.detach().numpy() + values = np.concatenate([values, values[-1:]]) + rewards = np.concatenate([batch.rewards, values[-1:]]) + deltas = rewards[:-1] + self._reward_discount * values[1:] - values[:-1] # r + gamma * v(s') - v(s) + advantages = discount_cumsum(deltas, self._reward_discount * self._lam) + batch.advantages = advantages + return batch + + def preprocess_and_merge_batches(self, batch_list: List[TransitionBatch]) -> TransitionBatch: + """Preprocess and merge a list of transition batches to a single transition batch. + + Args: + batch_list (List[TransitionBatch]): List of batches. + + Returns: + The merged batch. + """ + return merge_transition_batches([self._preprocess_batch(batch) for batch in batch_list]) + + +class DiscreteActorCritic(SingleTrainer): + """Actor Critic algorithm with separate policy and value models. + + References: + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. + https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f + """ + + def __init__(self, name: str, params: DiscreteActorCriticParams) -> None: + super(DiscreteActorCritic, self).__init__(name, params) + self._params = params + self._ops_name = f"{self._name}.ops" + + self._replay_memory_dict: Dict[Any, FIFOReplayMemory] = {} + + def build(self) -> None: + self._ops = self.get_ops(self._ops_name) + self._replay_memory_dict = collections.defaultdict(lambda: FIFOReplayMemory( + capacity=self._params.replay_memory_capacity, + state_dim=self._ops.policy_state_dim, + action_dim=self._ops.policy_action_dim, + )) + + def record(self, env_idx: int, exp_element: ExpElement) -> None: + for agent_name in exp_element.agent_names: + memory = self._replay_memory_dict[(env_idx, agent_name)] + transition_batch = TransitionBatch( + states=np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0), + actions=np.expand_dims(exp_element.action_dict[agent_name], axis=0), + rewards=np.array([exp_element.reward_dict[agent_name]]), + terminals=np.array([exp_element.terminal_dict[agent_name]]), + next_states=np.expand_dims( + exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), + axis=0, + ), + ) + memory.put(transition_batch) + + def get_local_ops_by_name(self, name: str) -> AbsTrainOps: + return DiscreteActorCriticOps( + name=name, get_policy_func=self._get_policy_func, parallelism=self._params.data_parallelism, + **self._params.extract_ops_params(), + ) + + def _get_batch(self) -> TransitionBatch: + batch_list = [memory.sample(-1) for memory in self._replay_memory_dict.values()] + return self._ops.preprocess_and_merge_batches(batch_list) + + def train(self) -> None: + assert isinstance(self._ops, DiscreteActorCriticOps) + batch = self._get_batch() + for _ in range(self._params.grad_iters): + self._ops.update_critic(batch) + self._ops.update_actor(batch) + + async def train_as_task(self) -> None: + assert isinstance(self._ops, RemoteOps) + batch = self._get_batch() + for _ in range(self._params.grad_iters): + self._ops.update_critic_with_grad(await self._ops.get_critic_grad(batch)) + self._ops.update_actor_with_grad(await self._ops.get_actor_grad(batch)) diff --git a/maro/rl/training/algorithms/ddpg.py b/maro/rl/training/algorithms/ddpg.py new file mode 100644 index 000000000..0d8ae9599 --- /dev/null +++ b/maro/rl/training/algorithms/ddpg.py @@ -0,0 +1,299 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +# TODO: DDPG has net been tested in a real test case + +from dataclasses import dataclass +from typing import Callable, Dict, Optional + +import numpy as np +import torch + +from maro.rl.model import QNet +from maro.rl.policy import ContinuousRLPolicy +from maro.rl.rollout import ExpElement +from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleTrainer, TrainerParams, remote +from maro.rl.utils import TransitionBatch, ndarray_to_tensor +from maro.utils import clone + + +@dataclass +class DDPGParams(TrainerParams): + """ + get_q_critic_net_func (Callable[[], QNet]): Function to get Q critic net. + reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. + num_epochs (int, default=1): Number of training epochs per call to ``learn``. + update_target_every (int, default=5): Number of training rounds between policy target model updates. + q_value_loss_cls (str, default=None): A string indicating a loss class provided by torch.nn or a custom + loss class for the Q-value loss. If it is a string, it must be a key in ``TORCH_LOSS``. + If it is None, use MSE. + soft_update_coef (float, default=1.0): Soft update coefficient, e.g., + target_model = (soft_update_coef) * eval_model + (1-soft_update_coef) * target_model. + random_overwrite (bool, default=False): This specifies overwrite behavior when the replay memory capacity + is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur + sequentially with wrap-around. + """ + get_q_critic_net_func: Callable[[], QNet] = None + reward_discount: float = 0.9 + num_epochs: int = 1 + update_target_every: int = 5 + q_value_loss_cls: Callable = None + soft_update_coef: float = 1.0 + random_overwrite: bool = False + + def __post_init__(self) -> None: + assert self.get_q_critic_net_func is not None + + def extract_ops_params(self) -> Dict[str, object]: + return { + "device": self.device, + "get_q_critic_net_func": self.get_q_critic_net_func, + "reward_discount": self.reward_discount, + "q_value_loss_cls": self.q_value_loss_cls, + "soft_update_coef": self.soft_update_coef, + "data_parallelism": self.data_parallelism, + } + + +class DDPGOps(AbsTrainOps): + """DDPG algorithm implementation. Reference: https://spinningup.openai.com/en/latest/algorithms/ddpg.html + """ + + def __init__( + self, + name: str, + device: str, + get_policy_func: Callable[[], ContinuousRLPolicy], + get_q_critic_net_func: Callable[[], QNet], + parallelism: int = 1, + *, + reward_discount: float, + q_value_loss_cls: Callable = None, + soft_update_coef: float = 1.0, + ) -> None: + super(DDPGOps, self).__init__( + name=name, + device=device, + is_single_scenario=True, + get_policy_func=get_policy_func, + parallelism=parallelism, + ) + + assert isinstance(self._policy, ContinuousRLPolicy) + + self._target_policy = clone(self._policy) + self._target_policy.set_name(f"target_{self._policy.name}") + self._target_policy.eval() + self._target_policy.to_device(self._device) + self._q_critic_net = get_q_critic_net_func() + self._q_critic_net.to(self._device) + self._target_q_critic_net: QNet = clone(self._q_critic_net) + self._target_q_critic_net.eval() + self._target_q_critic_net.to(self._device) + + self._reward_discount = reward_discount + self._q_value_loss_func = q_value_loss_cls() if q_value_loss_cls is not None else torch.nn.MSELoss() + self._soft_update_coef = soft_update_coef + + def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: + """Compute the critic loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The critic loss of the batch. + """ + assert self._is_valid_transition_batch(batch) + self._q_critic_net.train() + states = ndarray_to_tensor(batch.states, self._device) # s + next_states = ndarray_to_tensor(batch.next_states, self._device) # s' + actions = ndarray_to_tensor(batch.actions, self._device) # a + rewards = ndarray_to_tensor(batch.rewards, self._device) # r + terminals = ndarray_to_tensor(batch.terminals, self._device) # d + + with torch.no_grad(): + next_q_values = self._target_q_critic_net.q_values( + states=next_states, # s' + actions=self._target_policy.get_actions_tensor(next_states), # miu_targ(s') + ) # Q_targ(s', miu_targ(s')) + + # y(r, s', d) = r + gamma * (1 - d) * Q_targ(s', miu_targ(s')) + target_q_values = (rewards + self._reward_discount * (1 - terminals) * next_q_values).detach() + q_values = self._q_critic_net.q_values(states=states, actions=actions) # Q(s, a) + return self._q_value_loss_func(q_values, target_q_values) # MSE(Q(s, a), y(r, s', d)) + + @remote + def get_critic_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + """Compute the critic network's gradients of a batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + grad (torch.Tensor): The critic gradient of the batch. + """ + return self._q_critic_net.get_gradients(self._get_critic_loss(batch)) + + def update_critic_with_grad(self, grad_dict: dict) -> None: + """Update the critic network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._q_critic_net.train() + self._q_critic_net.apply_gradients(grad_dict) + + def update_critic(self, batch: TransitionBatch) -> None: + """Update the critic network using a batch. + + Args: + batch (TransitionBatch): Batch. + """ + self._q_critic_net.train() + self._q_critic_net.step(self._get_critic_loss(batch)) + + def _get_actor_loss(self, batch: TransitionBatch) -> torch.Tensor: + """Compute the actor loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The actor loss of the batch. + """ + assert self._is_valid_transition_batch(batch) + self._policy.train() + states = ndarray_to_tensor(batch.states, self._device) # s + + policy_loss = -self._q_critic_net.q_values( + states=states, # s + actions=self._policy.get_actions_tensor(states), # miu(s) + ).mean() # -Q(s, miu(s)) + + return policy_loss + + @remote + def get_actor_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + """Compute the actor network's gradients of a batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + grad (torch.Tensor): The actor gradient of the batch. + """ + return self._policy.get_gradients(self._get_actor_loss(batch)) + + def update_actor_with_grad(self, grad_dict: dict) -> None: + """Update the actor network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._policy.train() + self._policy.apply_gradients(grad_dict) + + def update_actor(self, batch: TransitionBatch) -> None: + """Update the actor network using a batch. + + Args: + batch (TransitionBatch): Batch. + """ + self._policy.train() + self._policy.step(self._get_actor_loss(batch)) + + def get_state(self) -> dict: + return { + "policy": self._policy.get_state(), + "target_policy": self._target_policy.get_state(), + "critic": self._q_critic_net.get_state(), + "target_critic": self._target_q_critic_net.get_state(), + } + + def set_state(self, ops_state_dict: dict) -> None: + self._policy.set_state(ops_state_dict["policy"]) + self._target_policy.set_state(ops_state_dict["target_policy"]) + self._q_critic_net.set_state(ops_state_dict["critic"]) + self._target_q_critic_net.set_state(ops_state_dict["target_critic"]) + + def soft_update_target(self) -> None: + """Soft update the target policy and target critic. + """ + self._target_policy.soft_update(self._policy, self._soft_update_coef) + self._target_q_critic_net.soft_update(self._q_critic_net, self._soft_update_coef) + + +class DDPG(SingleTrainer): + """The Deep Deterministic Policy Gradient (DDPG) algorithm. + + References: + https://arxiv.org/pdf/1509.02971.pdf + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ddpg + """ + + def __init__(self, name: str, params: DDPGParams) -> None: + super(DDPG, self).__init__(name, params) + self._params = params + self._policy_version = self._target_policy_version = 0 + self._ops_name = f"{self._name}.ops" + + self._replay_memory: Optional[RandomReplayMemory] = None + + def build(self) -> None: + self._ops = self.get_ops(self._ops_name) + self._replay_memory = RandomReplayMemory( + capacity=self._params.replay_memory_capacity, + state_dim=self._ops.policy_state_dim, + action_dim=self._ops.policy_action_dim, + random_overwrite=self._params.random_overwrite, + ) + + def record(self, env_idx: int, exp_element: ExpElement) -> None: + for agent_name in exp_element.agent_names: + transition_batch = TransitionBatch( + states=np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0), + actions=np.expand_dims(exp_element.action_dict[agent_name], axis=0), + rewards=np.array([exp_element.reward_dict[agent_name]]), + terminals=np.array([exp_element.terminal_dict[agent_name]]), + next_states=np.expand_dims( + exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), + axis=0, + ), + ) + self._replay_memory.put(transition_batch) + + def get_local_ops_by_name(self, name: str) -> AbsTrainOps: + return DDPGOps( + name=name, get_policy_func=self._get_policy_func, parallelism=self._params.data_parallelism, + **self._params.extract_ops_params(), + ) + + def _get_batch(self, batch_size: int = None) -> TransitionBatch: + return self._replay_memory.sample(batch_size if batch_size is not None else self._batch_size) + + def train(self) -> None: + assert isinstance(self._ops, DDPGOps) + for _ in range(self._params.num_epochs): + batch = self._get_batch() + self._ops.update_critic(batch) + self._ops.update_actor(batch) + + self._try_soft_update_target() + + async def train_as_task(self) -> None: + assert isinstance(self._ops, RemoteOps) + for _ in range(self._params.num_epochs): + batch = self._get_batch() + self._ops.update_critic_with_grad(await self._ops.get_critic_grad(batch)) + self._ops.update_actor_with_grad(await self._ops.get_actor_grad(batch)) + + self._try_soft_update_target() + + def _try_soft_update_target(self) -> None: + """Soft update the target policy and target critic. + """ + self._policy_version += 1 + if self._policy_version - self._target_policy_version == self._params.update_target_every: + self._ops.soft_update_target() + self._target_policy_version = self._policy_version diff --git a/maro/rl/training/algorithms/dqn.py b/maro/rl/training/algorithms/dqn.py new file mode 100644 index 000000000..6d5ababbd --- /dev/null +++ b/maro/rl/training/algorithms/dqn.py @@ -0,0 +1,226 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from dataclasses import dataclass +from typing import Callable, Dict, Optional + +import numpy as np +import torch + +from maro.rl.policy import ValueBasedPolicy +from maro.rl.rollout import ExpElement +from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleTrainer, TrainerParams, remote +from maro.rl.utils import TransitionBatch, ndarray_to_tensor +from maro.utils import clone + + +@dataclass +class DQNParams(TrainerParams): + """ + reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. + num_epochs (int, default=1): Number of training epochs. + update_target_every (int, default=5): Number of gradient steps between target model updates. + soft_update_coef (float, default=0.1): Soft update coefficient, e.g., + target_model = (soft_update_coef) * eval_model + (1-soft_update_coef) * target_model. + double (bool, default=False): If True, the next Q values will be computed according to the double DQN algorithm, + i.e., q_next = Q_target(s, argmax(Q_eval(s, a))). Otherwise, q_next = max(Q_target(s, a)). + See https://arxiv.org/pdf/1509.06461.pdf for details. + random_overwrite (bool, default=False): This specifies overwrite behavior when the replay memory capacity + is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur + sequentially with wrap-around. + """ + reward_discount: float = 0.9 + num_epochs: int = 1 + update_target_every: int = 5 + soft_update_coef: float = 0.1 + double: bool = False + random_overwrite: bool = False + + def extract_ops_params(self) -> Dict[str, object]: + return { + "device": self.device, + "reward_discount": self.reward_discount, + "soft_update_coef": self.soft_update_coef, + "double": self.double, + } + + +class DQNOps(AbsTrainOps): + def __init__( + self, + name: str, + device: str, + get_policy_func: Callable[[], ValueBasedPolicy], + parallelism: int = 1, + *, + reward_discount: float = 0.9, + soft_update_coef: float = 0.1, + double: bool = False, + ) -> None: + super(DQNOps, self).__init__( + name=name, + device=device, + is_single_scenario=True, + get_policy_func=get_policy_func, + parallelism=parallelism, + ) + + assert isinstance(self._policy, ValueBasedPolicy) + + self._reward_discount = reward_discount + self._soft_update_coef = soft_update_coef + self._double = double + self._loss_func = torch.nn.MSELoss() + + self._target_policy: ValueBasedPolicy = clone(self._policy) + self._target_policy.set_name(f"target_{self._policy.name}") + self._target_policy.eval() + self._target_policy.to_device(self._device) + + def _get_batch_loss(self, batch: TransitionBatch) -> Dict[str, Dict[str, torch.Tensor]]: + """Compute the loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The loss of the batch. + """ + assert self._is_valid_transition_batch(batch) + self._policy.train() + states = ndarray_to_tensor(batch.states, self._device) + next_states = ndarray_to_tensor(batch.next_states, self._device) + actions = ndarray_to_tensor(batch.actions, self._device) + rewards = ndarray_to_tensor(batch.rewards, self._device) + terminals = ndarray_to_tensor(batch.terminals, self._device).float() + + with torch.no_grad(): + if self._double: + self._policy.exploit() + actions_by_eval_policy = self._policy.get_actions_tensor(next_states) + next_q_values = self._target_policy.q_values_tensor(next_states, actions_by_eval_policy) + else: + self._target_policy.exploit() + actions = self._target_policy.get_actions_tensor(next_states) + next_q_values = self._target_policy.q_values_tensor(next_states, actions) + + target_q_values = (rewards + self._reward_discount * (1 - terminals) * next_q_values).detach() + q_values = self._policy.q_values_tensor(states, actions) + return self._loss_func(q_values, target_q_values) + + @remote + def get_batch_grad(self, batch: TransitionBatch) -> Dict[str, Dict[str, torch.Tensor]]: + """Compute the network's gradients of a batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + grad (torch.Tensor): The gradient of the batch. + """ + return self._policy.get_gradients(self._get_batch_loss(batch)) + + def update_with_grad(self, grad_dict: dict) -> None: + """Update the network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._policy.train() + self._policy.apply_gradients(grad_dict) + + def update(self, batch: TransitionBatch) -> None: + """Update the network using a batch. + + Args: + batch (TransitionBatch): Batch. + """ + self._policy.train() + self._policy.step(self._get_batch_loss(batch)) + + def get_state(self) -> dict: + return { + "policy": self._policy.get_state(), + "target_q_net": self._target_policy.get_state(), + } + + def set_state(self, ops_state_dict: dict) -> None: + self._policy.set_state(ops_state_dict["policy"]) + self._target_policy.set_state(ops_state_dict["target_q_net"]) + + def soft_update_target(self) -> None: + """Soft update the target policy. + """ + self._target_policy.soft_update(self._policy, self._soft_update_coef) + + +class DQN(SingleTrainer): + """The Deep-Q-Networks algorithm. + + See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. + """ + + def __init__(self, name: str, params: DQNParams) -> None: + super(DQN, self).__init__(name, params) + self._params = params + self._q_net_version = self._target_q_net_version = 0 + self._ops_name = f"{self._name}.ops" + + self._replay_memory: Optional[RandomReplayMemory] = None + + def build(self) -> None: + self._ops = self.get_ops(self._ops_name) + self._replay_memory = RandomReplayMemory( + capacity=self._params.replay_memory_capacity, + state_dim=self._ops.policy_state_dim, + action_dim=self._ops.policy_action_dim, + random_overwrite=self._params.random_overwrite, + ) + + def record(self, env_idx: int, exp_element: ExpElement) -> None: + for agent_name in exp_element.agent_names: + transition_batch = TransitionBatch( + states=np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0), + actions=np.expand_dims(exp_element.action_dict[agent_name], axis=0), + rewards=np.array([exp_element.reward_dict[agent_name]]), + terminals=np.array([exp_element.terminal_dict[agent_name]]), + next_states=np.expand_dims( + exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), + axis=0, + ), + ) + self._replay_memory.put(transition_batch) + + def get_local_ops_by_name(self, name: str) -> AbsTrainOps: + return DQNOps( + name=name, + get_policy_func=self._get_policy_func, + parallelism=self._params.data_parallelism, + **self._params.extract_ops_params(), + ) + + def _get_batch(self, batch_size: int = None) -> TransitionBatch: + return self._replay_memory.sample(batch_size if batch_size is not None else self._batch_size) + + def train(self) -> None: + assert isinstance(self._ops, DQNOps) + for _ in range(self._params.num_epochs): + self._ops.update(self._get_batch()) + + self._try_soft_update_target() + + async def train_as_task(self) -> None: + assert isinstance(self._ops, RemoteOps) + for _ in range(self._params.num_epochs): + batch = self._get_batch() + self._ops.update_with_grad(await self._ops.get_batch_grad(batch)) + + self._try_soft_update_target() + + def _try_soft_update_target(self) -> None: + """Soft update the target policy and target critic. + """ + self._q_net_version += 1 + if self._q_net_version - self._target_q_net_version == self._params.update_target_every: + self._ops.soft_update_target() + self._target_q_net_version = self._q_net_version diff --git a/maro/rl/training/algorithms/maddpg.py b/maro/rl/training/algorithms/maddpg.py new file mode 100644 index 000000000..620b7b034 --- /dev/null +++ b/maro/rl/training/algorithms/maddpg.py @@ -0,0 +1,490 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import asyncio +from dataclasses import dataclass +from typing import Callable, Dict, List, Tuple + +import numpy as np +import torch + +from maro.rl.model import MultiQNet +from maro.rl.policy import DiscretePolicyGradient +from maro.rl.rollout import ExpElement +from maro.rl.training import AbsTrainOps, MultiTrainer, RandomMultiReplayMemory, RemoteOps, TrainerParams, remote +from maro.rl.utils import MultiTransitionBatch, ndarray_to_tensor +from maro.utils import clone + + +@dataclass +class DiscreteMADDPGParams(TrainerParams): + """ + get_q_critic_net_func (Callable[[], MultiQNet]): Function to get multi Q critic net. + num_epochs (int, default=10): Number of training epochs. + update_target_every (int, default=5): Number of gradient steps between target model updates. + soft_update_coef (float, default=0.5): Soft update coefficient, e.g., + target_model = (soft_update_coef) * eval_model + (1-soft_update_coef) * target_model. + reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. + q_value_loss_cls (Callable, default=None): Critic loss function. If it is None, use MSE. + shared_critic (bool, default=False): Whether different policies use shared critic or individual policies. + """ + get_q_critic_net_func: Callable[[], MultiQNet] = None + num_epoch: int = 10 + update_target_every: int = 5 + soft_update_coef: float = 0.5 + reward_discount: float = 0.9 + q_value_loss_cls: Callable = None + shared_critic: bool = False + + def __post_init__(self) -> None: + assert self.get_q_critic_net_func is not None + + def extract_ops_params(self) -> Dict[str, object]: + return { + "device": self.device, + "get_q_critic_net_func": self.get_q_critic_net_func, + "shared_critic": self.shared_critic, + "reward_discount": self.reward_discount, + "soft_update_coef": self.soft_update_coef, + "update_target_every": self.update_target_every, + "q_value_loss_func": self.q_value_loss_cls() if self.q_value_loss_cls is not None else torch.nn.MSELoss(), + } + + +class DiscreteMADDPGOps(AbsTrainOps): + def __init__( + self, + name: str, + device: str, + get_policy_func: Callable[[], DiscretePolicyGradient], + get_q_critic_net_func: Callable[[], MultiQNet], + policy_idx: int, + create_actor: bool, + *, + shared_critic: bool = False, + reward_discount: float = 0.9, + soft_update_coef: float = 0.5, + update_target_every: int = 5, + q_value_loss_func: Callable = None, + ) -> None: + super(DiscreteMADDPGOps, self).__init__( + name=name, + device=device, + is_single_scenario=False, + get_policy_func=get_policy_func, + ) + + self._policy_idx = policy_idx + self._shared_critic = shared_critic + + # Actor + self._create_actor = create_actor + if create_actor: + self._policy = get_policy_func() + assert isinstance(self._policy, DiscretePolicyGradient) + + self._policy.to_device(self._device) + self._target_policy: DiscretePolicyGradient = clone(self._policy) + self._target_policy.set_name(f"target_{self._policy.name}") + self._target_policy.eval() + self._target_policy.to_device(self._device) + + # Critic + self._q_critic_net: MultiQNet = get_q_critic_net_func() + self._q_critic_net.to(self._device) + self._target_q_critic_net: MultiQNet = clone(self._q_critic_net) + self._target_q_critic_net.eval() + self._target_q_critic_net.to(self._device) + + # + self._reward_discount = reward_discount + self._q_value_loss_func = q_value_loss_func + self._update_target_every = update_target_every + self._soft_update_coef = soft_update_coef + + def get_target_action(self, batch: MultiTransitionBatch) -> torch.Tensor: + """Get the target policies' actions according to the batch. + + Args: + batch (MultiTransitionBatch): Batch. + + Returns: + actions (torch.Tensor): Target policies' actions. + """ + agent_state = ndarray_to_tensor(batch.agent_states[self._policy_idx], self._device) + return self._target_policy.get_actions_tensor(agent_state) + + def get_latest_action(self, batch: MultiTransitionBatch) -> Tuple[torch.Tensor, torch.Tensor]: + """Get the latest actions and corresponding log-probabilities according to the batch. + + Args: + batch (MultiTransitionBatch): Batch. + + Returns: + actions (torch.Tensor): Target policies' actions. + logps (torch.Tensor): Log-probabilities. + """ + assert isinstance(self._policy, DiscretePolicyGradient) + + agent_state = ndarray_to_tensor(batch.agent_states[self._policy_idx], self._device) + self._policy.train() + action = self._policy.get_actions_tensor(agent_state) + logps = self._policy.get_state_action_logps(agent_state, action) + return action, logps + + def _get_critic_loss(self, batch: MultiTransitionBatch, next_actions: List[torch.Tensor]) -> torch.Tensor: + """Compute the critic loss of the batch. + + Args: + batch (MultiTransitionBatch): Batch. + next_actions (List[torch.Tensor]): List of next actions of all policies. + + Returns: + loss (torch.Tensor): The critic loss of the batch. + """ + assert not self._shared_critic + assert isinstance(next_actions, list) and all(isinstance(action, torch.Tensor) for action in next_actions) + + states = ndarray_to_tensor(batch.states, self._device) # x + actions = [ndarray_to_tensor(action, self._device) for action in batch.actions] # a + next_states = ndarray_to_tensor(batch.next_states, self._device) # x' + rewards = ndarray_to_tensor(np.vstack([reward for reward in batch.rewards]), self._device) # r + terminals = ndarray_to_tensor(batch.terminals, self._device) # d + + self._q_critic_net.train() + with torch.no_grad(): + next_q_values = self._target_q_critic_net.q_values( + states=next_states, # x' + actions=next_actions, + ) # a' + target_q_values = ( + rewards[self._policy_idx] + self._reward_discount * (1 - terminals.float()) * next_q_values + ) + q_values = self._q_critic_net.q_values( + states=states, # x + actions=actions, # a + ) # Q(x, a) + return self._q_value_loss_func(q_values, target_q_values.detach()) + + @remote + def get_critic_grad( + self, + batch: MultiTransitionBatch, + next_actions: List[torch.Tensor], + ) -> Dict[str, torch.Tensor]: + """Compute the critic network's gradients of a batch. + + Args: + batch (MultiTransitionBatch): Batch. + next_actions (List[torch.Tensor]): List of next actions of all policies. + + Returns: + grad (torch.Tensor): The critic gradient of the batch. + """ + return self._q_critic_net.get_gradients(self._get_critic_loss(batch, next_actions)) + + def update_critic(self, batch: MultiTransitionBatch, next_actions: List[torch.Tensor]) -> None: + """Update the critic network using a batch. + + Args: + batch (MultiTransitionBatch): Batch. + next_actions (List[torch.Tensor]): List of next actions of all policies. + """ + self._q_critic_net.train() + self._q_critic_net.step(self._get_critic_loss(batch, next_actions)) + + def update_critic_with_grad(self, grad_dict: dict) -> None: + """Update the critic network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._q_critic_net.train() + self._q_critic_net.apply_gradients(grad_dict) + + def _get_actor_loss(self, batch: MultiTransitionBatch) -> torch.Tensor: + """Compute the actor loss of the batch. + + Args: + batch (MultiTransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The actor loss of the batch. + """ + latest_action, latest_action_logp = self.get_latest_action(batch) + states = ndarray_to_tensor(batch.states, self._device) # x + actions = [ndarray_to_tensor(action, self._device) for action in batch.actions] # a + actions[self._policy_idx] = latest_action + self._policy.train() + self._q_critic_net.freeze() + actor_loss = -(self._q_critic_net.q_values( + states=states, # x + actions=actions, # [a^j_1, ..., a_i, ..., a^j_N] + ) * latest_action_logp).mean() # Q(x, a^j_1, ..., a_i, ..., a^j_N) + self._q_critic_net.unfreeze() + return actor_loss + + @remote + def get_actor_grad(self, batch: MultiTransitionBatch) -> Dict[str, torch.Tensor]: + """Compute the actor network's gradients of a batch. + + Args: + batch (MultiTransitionBatch): Batch. + + Returns: + grad (torch.Tensor): The actor gradient of the batch. + """ + return self._policy.get_gradients(self._get_actor_loss(batch)) + + def update_actor(self, batch: MultiTransitionBatch) -> None: + """Update the actor network using a batch. + + Args: + batch (MultiTransitionBatch): Batch. + """ + self._policy.train() + self._policy.step(self._get_actor_loss(batch)) + + def update_actor_with_grad(self, grad_dict: dict) -> None: + """Update the critic network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._policy.train() + self._policy.apply_gradients(grad_dict) + + def soft_update_target(self) -> None: + """Soft update the target policies and target critics. + """ + if self._create_actor: + self._target_policy.soft_update(self._policy, self._soft_update_coef) + if not self._shared_critic: + self._target_q_critic_net.soft_update(self._q_critic_net, self._soft_update_coef) + + def get_critic_state(self) -> dict: + return { + "critic": self._q_critic_net.get_state(), + "target_critic": self._target_q_critic_net.get_state(), + } + + def set_critic_state(self, ops_state_dict: dict) -> None: + self._q_critic_net.set_state(ops_state_dict["critic"]) + self._target_q_critic_net.set_state(ops_state_dict["target_critic"]) + + def get_actor_state(self) -> dict: + if self._create_actor: + return {"policy": self._policy.get_state(), "target_policy": self._target_policy.get_state()} + else: + return {} + + def set_actor_state(self, ops_state_dict: dict) -> None: + if self._create_actor: + self._policy.set_state(ops_state_dict["policy"]) + self._target_policy.set_state(ops_state_dict["target_policy"]) + + def get_state(self) -> dict: + return {**self.get_actor_state(), **self.get_critic_state()} + + def set_state(self, ops_state_dict: dict) -> None: + self.set_critic_state(ops_state_dict) + self.set_actor_state(ops_state_dict) + + +class DiscreteMADDPG(MultiTrainer): + def __init__(self, name: str, params: DiscreteMADDPGParams) -> None: + super(DiscreteMADDPG, self).__init__(name, params) + self._params = params + self._ops_params = self._params.extract_ops_params() + self._state_dim = params.get_q_critic_net_func().state_dim + self._policy_version = self._target_policy_version = 0 + self._shared_critic_ops_name = f"{self._name}.shared_critic_ops" + + self._actor_ops_list = [] + self._actor_ops_dict = {} + self._critic_ops = None + self._replay_memory = None + self._policy2agent = {} + + def build(self) -> None: + self._actor_ops_list = [self.get_ops(f"{self._name}.actor_{i}_ops") for i in range(len(self._policy_names))] + self._actor_ops_dict = {ops.name: ops for ops in self._actor_ops_list} + if self._params.shared_critic: + self._critic_ops = self.get_ops(self._shared_critic_ops_name) + else: + self._critic_ops = None + + self._replay_memory = RandomMultiReplayMemory( + capacity=self._params.replay_memory_capacity, + state_dim=self._state_dim, + action_dims=[ops.policy_action_dim for ops in self._actor_ops_list], + agent_states_dims=[ops.policy_state_dim for ops in self._actor_ops_list], + ) + + assert len(self._agent2policy.keys()) == len(self._agent2policy.values()) # agent <=> policy + self._policy2agent = {policy_name: agent_name for agent_name, policy_name in self._agent2policy.items()} + + def record(self, env_idx: int, exp_element: ExpElement) -> None: + assert exp_element.num_agents == len(self._agent2policy.keys()) + + if min(exp_element.terminal_dict.values()) != max(exp_element.terminal_dict.values()): + raise ValueError("The 'terminal` flag of all agents must be identical.") + terminal_flag = min(exp_element.terminal_dict.values()) + + actions = [] + rewards = [] + agent_states = [] + next_agent_states = [] + for policy_name in self._policy_names: + agent_name = self._policy2agent[policy_name] + actions.append(np.expand_dims(exp_element.action_dict[agent_name], axis=0)) + rewards.append(np.array([exp_element.reward_dict[agent_name]])) + agent_states.append(np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0)) + next_agent_states.append(np.expand_dims( + exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), axis=0 + )) + + transition_batch = MultiTransitionBatch( + states=np.expand_dims(exp_element.state, axis=0), + actions=actions, + rewards=rewards, + next_states=np.expand_dims( + exp_element.next_state if exp_element.next_state is not None else exp_element.state, axis=0 + ), + agent_states=agent_states, + next_agent_states=next_agent_states, + terminals=np.array([terminal_flag]), + ) + self._replay_memory.put(transition_batch) + + def _get_batch(self, batch_size: int = None) -> MultiTransitionBatch: + return self._replay_memory.sample(batch_size if batch_size is not None else self._batch_size) + + def get_local_ops_by_name(self, name: str) -> AbsTrainOps: + if name == self._shared_critic_ops_name: + ops_params = dict(self._ops_params) + ops_params.update({ + "get_policy_func": None, + "policy_idx": -1, + "shared_critic": False, + "create_actor": False, + }) + return DiscreteMADDPGOps(name=name, **ops_params) + else: + policy_idx = self.get_policy_idx_from_ops_name(name) + policy_name = self._policy_names[policy_idx] + + ops_params = dict(self._ops_params) + ops_params.update({ + "get_policy_func": lambda: self._policy_creator[policy_name](policy_name), + "policy_idx": policy_idx, + "create_actor": True, + }) + return DiscreteMADDPGOps(name=name, **ops_params) + + def train(self) -> None: + assert not self._params.shared_critic or isinstance(self._critic_ops, DiscreteMADDPGOps) + assert all(isinstance(ops, DiscreteMADDPGOps) for ops in self._actor_ops_list) + for _ in range(self._params.num_epoch): + batch = self._get_batch() + # Collect next actions + next_actions = [ops.get_target_action(batch) for ops in self._actor_ops_list] + + # Update critic + if self._params.shared_critic: + self._critic_ops.update_critic(batch, next_actions) + critic_state_dict = self._critic_ops.get_critic_state() + # Sync latest critic to ops + for ops in self._actor_ops_list: + ops.set_critic_state(critic_state_dict) + else: + for ops in self._actor_ops_list: + ops.update_critic(batch, next_actions) + + # Update actors + for ops in self._actor_ops_list: + ops.update_actor(batch) + + # Update version + self._try_soft_update_target() + + async def train_as_task(self) -> None: + assert not self._params.shared_critic or isinstance(self._critic_ops, RemoteOps) + assert all(isinstance(ops, RemoteOps) for ops in self._actor_ops_list) + for _ in range(self._params.num_epoch): + batch = self._get_batch() + # Collect next actions + next_actions = [ops.get_target_action(batch) for ops in self._actor_ops_list] + + # Update critic + if self._params.shared_critic: + critic_grad = await asyncio.gather(*[self._critic_ops.get_critic_grad(batch, next_actions)]) + assert isinstance(critic_grad, list) and isinstance(critic_grad[0], dict) + self._critic_ops.update_critic_with_grad(critic_grad[0]) + critic_state_dict = self._critic_ops.get_critic_state() + # Sync latest critic to ops + for ops in self._actor_ops_list: + ops.set_critic_state(critic_state_dict) + else: + critic_grad_list = await asyncio.gather( + *[ops.get_critic_grad(batch, next_actions) for ops in self._actor_ops_list] + ) + for ops, critic_grad in zip(self._actor_ops_list, critic_grad_list): + ops.update_critic_with_grad(critic_grad) + + # Update actors + actor_grad_list = await asyncio.gather(*[ops.get_actor_grad(batch) for ops in self._actor_ops_list]) + for ops, actor_grad in zip(self._actor_ops_list, actor_grad_list): + ops.update_actor_with_grad(actor_grad) + + # Update version + self._try_soft_update_target() + + def _try_soft_update_target(self) -> None: + """Soft update the target policies and target critics. + """ + self._policy_version += 1 + if self._policy_version - self._target_policy_version == self._params.update_target_every: + for ops in self._actor_ops_list: + ops.soft_update_target() + if self._params.shared_critic: + self._critic_ops.soft_update_target() + self._target_policy_version = self._policy_version + + def get_policy_state(self) -> Dict[str, object]: + self._assert_ops_exists() + ret_policy_state = {} + for ops in self._actor_ops_list: + policy_name, state = ops.get_policy_state() + ret_policy_state[policy_name] = state + return ret_policy_state + + def load(self, path: str) -> None: + self._assert_ops_exists() + trainer_state = torch.load(path) + for ops_name, ops_state in trainer_state.items(): + if ops_name == self._critic_ops.name: + self._critic_ops.set_state(ops_state) + else: + self._actor_ops_dict[ops_name].set_state(torch.load(path)) + + def save(self, path: str) -> None: + self._assert_ops_exists() + trainer_state = {ops.name: ops.get_state() for ops in self._actor_ops_list} + if self._params.shared_critic: + trainer_state[self._critic_ops.name] = self._critic_ops.get_state() + torch.save(trainer_state, path) + + def _assert_ops_exists(self) -> None: + if not self._actor_ops_list: + raise ValueError("Call 'DiscreteMADDPG.build' to create actor ops first.") + if self._params.shared_critic and not self._critic_ops: + raise ValueError("Call 'DiscreteMADDPG.build' to create the critic ops first.") + + @staticmethod + def get_policy_idx_from_ops_name(ops_name: str) -> int: + _, sub_name = ops_name.split(".") + return int(sub_name.split("_")[1]) + + async def exit(self) -> None: + pass diff --git a/maro/rl/training/proxy.py b/maro/rl/training/proxy.py new file mode 100644 index 000000000..29eaaed7a --- /dev/null +++ b/maro/rl/training/proxy.py @@ -0,0 +1,85 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from collections import defaultdict, deque + +from maro.rl.distributed import AbsProxy +from maro.rl.utils.common import bytes_to_pyobj, pyobj_to_bytes +from maro.rl.utils.torch_utils import average_grads +from maro.utils import LoggerV2 + + +class TrainingProxy(AbsProxy): + """Intermediary between trainers and workers. + + The proxy receives compute tasks from multiple ``AbsTrainOps`` instances, forwards them to a set of back-end + ``TrainOpsWorker``s to be processed and returns the results to the clients. + + Args: + frontend_port (int, default=10000): Network port for communicating with clients (task producers). + backend_port (int, default=10001): Network port for communicating with back-end workers (task consumers). + """ + + def __init__(self, frontend_port: int = 10000, backend_port: int = 10001) -> None: + super(TrainingProxy, self).__init__(frontend_port=frontend_port, backend_port=backend_port) + self._available_workers = deque() + self._worker_ready = False + self._connected_ops = set() + self._result_cache = defaultdict(list) + self._expected_num_results = {} + self._logger = LoggerV2("TRAIN-PROXY") + + def _route_request_to_compute_node(self, msg: list) -> None: + """ + Here we use a least-recently-used (LRU) routing strategy to select workers for a task while making the best + effort to satisfy the task's desired parallelism. For example, consider a task that specifies a desired + parallelism K (for gradient computation). If there are more than K workers in the ``_available_workers`` queue, + the first, i.e., the least recently used, K of them will be selected to process the task. If there are fewer + than K workers in the queue, all workers will be popped from the queue to process the task. In this case, the + desired parallelism cannot be satisfied, but waiting is avoided. + """ + if msg[-1] == b"EXIT": + self._connected_ops.remove(msg[0]) + # if all clients (ops) have signaled exit, tell the workers to terminate + if not self._connected_ops: + for worker_id in self._available_workers: + self._dispatch_endpoint.send_multipart([worker_id, b"EXIT"]) + return + + self._connected_ops.add(msg[0]) + req = bytes_to_pyobj(msg[-1]) + desired_parallelism = req["desired_parallelism"] + req["args"] = list(req["args"]) + batch = req["args"][0] + workers = [] + while len(workers) < desired_parallelism and self._available_workers: + workers.append(self._available_workers.popleft()) + + self._expected_num_results[msg[0]] = len(workers) + for worker_id, sub_batch in zip(workers, batch.split(len(workers))): + req["args"][0] = sub_batch + self._dispatch_endpoint.send_multipart([worker_id, msg[0], pyobj_to_bytes(req)]) + + if not self._available_workers: + # stop receiving compute requests until at least one worker becomes available + self._workers_ready = False + self._req_endpoint.stop_on_recv() + + def _send_result_to_requester(self, msg: list) -> None: + if msg[1] == b"EXIT_ACK": + self._logger.info("Exiting event loop...") + self.stop() + return + + if msg[1] != b"READY": + ops_name = msg[1] + self._result_cache[ops_name].append(bytes_to_pyobj(msg[-1])) + if len(self._result_cache[ops_name]) == self._expected_num_results[ops_name]: + aggregated_result = average_grads(self._result_cache[ops_name]) + self._logger.info(f"Aggregated {len(self._result_cache[ops_name])} results for {ops_name}") + self._result_cache[ops_name].clear() + self._req_endpoint.send_multipart([ops_name, pyobj_to_bytes(aggregated_result)]) + + self._available_workers.append(msg[0]) + self._worker_ready = True + self._req_endpoint.on_recv(self._route_request_to_compute_node) diff --git a/maro/rl/training/replay_memory.py b/maro/rl/training/replay_memory.py new file mode 100644 index 000000000..c0cdb3e48 --- /dev/null +++ b/maro/rl/training/replay_memory.py @@ -0,0 +1,479 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABCMeta, abstractmethod +from typing import List + +import numpy as np + +from maro.rl.utils import SHAPE_CHECK_FLAG, MultiTransitionBatch, TransitionBatch, match_shape + + +class AbsIndexScheduler(object, metaclass=ABCMeta): + """Scheduling indexes for read and write requests. This is used as an inner module of the replay memory. + + Args: + capacity (int): Maximum capacity of the replay memory. + """ + + def __init__(self, capacity: int) -> None: + super(AbsIndexScheduler, self).__init__() + self._capacity = capacity + + @abstractmethod + def get_put_indexes(self, batch_size: int) -> np.ndarray: + """Generate a list of indexes to the replay memory for writing. In other words, when the replay memory + need to write a batch, the scheduler should provide a set of proper indexes for the replay memory to + write. + + Args: + batch_size (int): The required batch size. + + Returns: + indexes (np.ndarray): The list of indexes. + """ + raise NotImplementedError + + @abstractmethod + def get_sample_indexes(self, batch_size: int = None, forbid_last: bool = False) -> np.ndarray: + """Generate a list of indexes that can be used to retrieve items from the replay memory. + + Args: + batch_size (int, default=None): The required batch size. If it is None, all indexes where an experience + item is present are returned. + forbid_last (bool, default=False): Whether the latest element is allowed to be sampled. + If this is true, the last index will always be excluded from the result. + + Returns: + indexes (np.ndarray): The list of indexes. + """ + raise NotImplementedError + + @abstractmethod + def get_last_index(self) -> int: + """Get the index of the latest element in the memory. + + Returns: + index (int): The index of the latest element in the memory. + """ + raise NotImplementedError + + +class RandomIndexScheduler(AbsIndexScheduler): + """Index scheduler that returns random indexes when sampling. + + Args: + capacity (int): Maximum capacity of the replay memory. + random_overwrite (bool): Flag that controls the overwriting behavior when the replay memory reaches capacity. + If this is true, newly added items will randomly overwrite existing ones. Otherwise, the overwrite occurs + in a cyclic manner. + """ + + def __init__(self, capacity: int, random_overwrite: bool) -> None: + super(RandomIndexScheduler, self).__init__(capacity) + self._random_overwrite = random_overwrite + self._ptr = self._size = 0 + + def get_put_indexes(self, batch_size: int) -> np.ndarray: + if self._ptr + batch_size <= self._capacity: + indexes = np.arange(self._ptr, self._ptr + batch_size) + self._ptr += batch_size + else: + overwrites = self._ptr + batch_size - self._capacity + indexes = np.concatenate([ + np.arange(self._ptr, self._capacity), + np.random.choice(self._ptr, size=overwrites, replace=False) if self._random_overwrite + else np.arange(overwrites) + ]) + self._ptr = self._capacity if self._random_overwrite else overwrites + + self._size = min(self._size + batch_size, self._capacity) + return indexes + + def get_sample_indexes(self, batch_size: int = None, forbid_last: bool = False) -> np.ndarray: + assert batch_size is not None and batch_size > 0, f"Invalid batch size: {batch_size}" + assert self._size > 0, "Cannot sample from an empty memory." + return np.random.choice(self._size, size=batch_size, replace=True) + + def get_last_index(self) -> int: + raise NotImplementedError + + +class FIFOIndexScheduler(AbsIndexScheduler): + """First-in-first-out index scheduler. + + Args: + capacity (int): Maximum capacity of the replay memory. + """ + + def __init__(self, capacity: int) -> None: + super(FIFOIndexScheduler, self).__init__(capacity) + self._head = self._tail = 0 + + @property + def size(self) -> int: + return (self._tail - self._head) % self._capacity + + def get_put_indexes(self, batch_size: int) -> np.ndarray: + if self.size + batch_size <= self._capacity: + if self._tail + batch_size <= self._capacity: + indexes = np.arange(self._tail, self._tail + batch_size) + else: + indexes = np.concatenate([ + np.arange(self._tail, self._capacity), + np.arange(self._tail + batch_size - self._capacity) + ]) + self._tail = (self._tail + batch_size) % self._capacity + return indexes + else: + overwrite = self.size + batch_size - self._capacity + self._head = (self._head + overwrite) % self._capacity + return self.get_put_indexes(batch_size) + + def get_sample_indexes(self, batch_size: int = None, forbid_last: bool = False) -> np.ndarray: + tmp = self._tail if not forbid_last else (self._tail - 1) % self._capacity + indexes = np.arange(self._head, tmp) if tmp > self._head \ + else np.concatenate([np.arange(self._head, self._capacity), np.arange(tmp)]) + self._head = tmp + return indexes + + def get_last_index(self) -> int: + return (self._tail - 1) % self._capacity + + +class AbsReplayMemory(object, metaclass=ABCMeta): + """Abstract replay memory class with basic interfaces. + + Args: + capacity (int): Maximum capacity of the replay memory. + state_dim (int): Dimension of states. + idx_scheduler (AbsIndexScheduler): The index scheduler. + """ + + def __init__(self, capacity: int, state_dim: int, idx_scheduler: AbsIndexScheduler) -> None: + super(AbsReplayMemory, self).__init__() + self._capacity = capacity + self._state_dim = state_dim + self._idx_scheduler = idx_scheduler + + @property + def capacity(self) -> int: + return self._capacity + + @property + def state_dim(self) -> int: + return self._state_dim + + def _get_put_indexes(self, batch_size: int) -> np.ndarray: + """Please refer to the doc string in AbsIndexScheduler. + """ + return self._idx_scheduler.get_put_indexes(batch_size) + + def _get_sample_indexes(self, batch_size: int = None, forbid_last: bool = False) -> np.ndarray: + """Please refer to the doc string in AbsIndexScheduler. + """ + return self._idx_scheduler.get_sample_indexes(batch_size, forbid_last) + + +class ReplayMemory(AbsReplayMemory, metaclass=ABCMeta): + """In-memory experience storage facility for a single trainer. + + Args: + capacity (int): Maximum capacity of the replay memory. + state_dim (int): Dimension of states. + action_dim (int): Dimension of actions. + idx_scheduler (AbsIndexScheduler): The index scheduler. + """ + + def __init__( + self, + capacity: int, + state_dim: int, + action_dim: int, + idx_scheduler: AbsIndexScheduler, + ) -> None: + super(ReplayMemory, self).__init__(capacity, state_dim, idx_scheduler) + self._action_dim = action_dim + + self._states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) + self._actions = np.zeros((self._capacity, self._action_dim), dtype=np.float32) + self._rewards = np.zeros(self._capacity, dtype=np.float32) + self._terminals = np.zeros(self._capacity, dtype=np.bool) + self._next_states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) + + @property + def action_dim(self) -> int: + return self._action_dim + + def put(self, transition_batch: TransitionBatch) -> None: + """Store a transition batch in the memory. + + Args: + transition_batch (TransitionBatch): The transition batch. + """ + batch_size = len(transition_batch.states) + if SHAPE_CHECK_FLAG: + assert 0 < batch_size <= self._capacity + assert match_shape(transition_batch.states, (batch_size, self._state_dim)) + assert match_shape(transition_batch.actions, (batch_size, self._action_dim)) + assert match_shape(transition_batch.rewards, (batch_size,)) + assert match_shape(transition_batch.terminals, (batch_size,)) + assert match_shape(transition_batch.next_states, (batch_size, self._state_dim)) + + self._put_by_indexes(self._get_put_indexes(batch_size), transition_batch) + + def _put_by_indexes(self, indexes: np.ndarray, transition_batch: TransitionBatch) -> None: + """Store a transition batch into the memory at the give indexes. + + Args: + indexes (np.ndarray): Positions in the replay memory to store at. + transition_batch (TransitionBatch): The transition batch. + """ + self._states[indexes] = transition_batch.states + self._actions[indexes] = transition_batch.actions + self._rewards[indexes] = transition_batch.rewards + self._terminals[indexes] = transition_batch.terminals + self._next_states[indexes] = transition_batch.next_states + + def sample(self, batch_size: int = None) -> TransitionBatch: + """Generate a sample batch from the replay memory. + + Args: + batch_size (int, default=None): The required batch size. If it is None, all indexes where an experience + item is present are returned. + + Returns: + batch (TransitionBatch): The sampled batch. + """ + indexes = self._get_sample_indexes(batch_size, self._get_forbid_last()) + return self.sample_by_indexes(indexes) + + def sample_by_indexes(self, indexes: np.ndarray) -> TransitionBatch: + """Retrieve items at given indexes from the replay memory. + + Args: + indexes (np.ndarray): Positions in the replay memory to retrieve at. + + Returns: + batch (TransitionBatch): The sampled batch. + """ + assert all([0 <= idx < self._capacity for idx in indexes]) + + return TransitionBatch( + states=self._states[indexes], + actions=self._actions[indexes], + rewards=self._rewards[indexes], + terminals=self._terminals[indexes], + next_states=self._next_states[indexes], + ) + + @abstractmethod + def _get_forbid_last(self) -> bool: + raise NotImplementedError + + +class RandomReplayMemory(ReplayMemory): + def __init__( + self, + capacity: int, + state_dim: int, + action_dim: int, + random_overwrite: bool = False, + ) -> None: + super(RandomReplayMemory, self).__init__( + capacity, state_dim, action_dim, RandomIndexScheduler(capacity, random_overwrite) + ) + self._random_overwrite = random_overwrite + self._scheduler = RandomIndexScheduler(capacity, random_overwrite) + + @property + def random_overwrite(self) -> bool: + return self._random_overwrite + + def _get_forbid_last(self) -> bool: + return False + + +class FIFOReplayMemory(ReplayMemory): + def __init__( + self, + capacity: int, + state_dim: int, + action_dim: int, + ) -> None: + super(FIFOReplayMemory, self).__init__( + capacity, state_dim, action_dim, FIFOIndexScheduler(capacity) + ) + + def _get_forbid_last(self) -> bool: + return not self._terminals[self._idx_scheduler.get_last_index()] + + +class MultiReplayMemory(AbsReplayMemory, metaclass=ABCMeta): + """In-memory experience storage facility for a multi trainer. + + Args: + capacity (int): Maximum capacity of the replay memory. + state_dim (int): Dimension of states. + action_dims (List[int]): Dimensions of actions. + idx_scheduler (AbsIndexScheduler): The index scheduler. + agent_states_dims (List[int]): Dimensions of agent states. + """ + + def __init__( + self, + capacity: int, + state_dim: int, + action_dims: List[int], + idx_scheduler: AbsIndexScheduler, + agent_states_dims: List[int], + ) -> None: + super(MultiReplayMemory, self).__init__(capacity, state_dim, idx_scheduler) + self._agent_num = len(action_dims) + self._action_dims = action_dims + + self._states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) + self._actions = [np.zeros((self._capacity, action_dim), dtype=np.float32) for action_dim in self._action_dims] + self._rewards = [np.zeros(self._capacity, dtype=np.float32) for _ in range(self.agent_num)] + self._next_states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) + self._terminals = np.zeros(self._capacity, dtype=np.bool) + + assert len(agent_states_dims) == self.agent_num + self._agent_states_dims = agent_states_dims + self._agent_states = [ + np.zeros((self._capacity, state_dim), dtype=np.float32) for state_dim in self._agent_states_dims + ] + self._next_agent_states = [ + np.zeros((self._capacity, state_dim), dtype=np.float32) for state_dim in self._agent_states_dims + ] + + @property + def action_dims(self) -> List[int]: + return self._action_dims + + @property + def agent_num(self) -> int: + return self._agent_num + + def put(self, transition_batch: MultiTransitionBatch) -> None: + """Store a transition batch into the memory. + + Args: + transition_batch (MultiTransitionBatch): The transition batch. + """ + batch_size = len(transition_batch.states) + if SHAPE_CHECK_FLAG: + assert 0 < batch_size <= self._capacity + assert match_shape(transition_batch.states, (batch_size, self._state_dim)) + assert len(transition_batch.actions) == len(transition_batch.rewards) == self.agent_num + for i in range(self.agent_num): + assert match_shape(transition_batch.actions[i], (batch_size, self.action_dims[i])) + assert match_shape(transition_batch.rewards[i], (batch_size,)) + + assert match_shape(transition_batch.terminals, (batch_size,)) + assert match_shape(transition_batch.next_states, (batch_size, self._state_dim)) + + assert len(transition_batch.agent_states) == self.agent_num + assert len(transition_batch.next_agent_states) == self.agent_num + for i in range(self.agent_num): + assert match_shape(transition_batch.agent_states[i], (batch_size, self._agent_states_dims[i])) + assert match_shape(transition_batch.next_agent_states[i], (batch_size, self._agent_states_dims[i])) + + self._put_by_indexes(self._get_put_indexes(batch_size), transition_batch=transition_batch) + + def _put_by_indexes(self, indexes: np.ndarray, transition_batch: MultiTransitionBatch) -> None: + """Store a transition batch into the memory at the give indexes. + + Args: + indexes (np.ndarray): Positions in the replay memory to store at. + transition_batch (MultiTransitionBatch): The transition batch. + """ + self._states[indexes] = transition_batch.states + for i in range(self.agent_num): + self._actions[i][indexes] = transition_batch.actions[i] + self._rewards[i][indexes] = transition_batch.rewards[i] + self._terminals[indexes] = transition_batch.terminals + + self._next_states[indexes] = transition_batch.next_states + for i in range(self.agent_num): + self._agent_states[i][indexes] = transition_batch.agent_states[i] + self._next_agent_states[i][indexes] = transition_batch.next_agent_states[i] + + def sample(self, batch_size: int = None) -> MultiTransitionBatch: + """Generate a sample batch from the replay memory. + + Args: + batch_size (int, default=None): The required batch size. If it is None, all indexes where an experience + item is present are returned. + + Returns: + batch (MultiTransitionBatch): The sampled batch. + """ + indexes = self._get_sample_indexes(batch_size, self._get_forbid_last()) + return self.sample_by_indexes(indexes) + + def sample_by_indexes(self, indexes: np.ndarray) -> MultiTransitionBatch: + """Retrieve items at given indexes from the replay memory. + + Args: + indexes (np.ndarray): Positions in the replay memory to retrieve at. + + Returns: + batch (MultiTransitionBatch): The sampled batch. + """ + assert all([0 <= idx < self._capacity for idx in indexes]) + + return MultiTransitionBatch( + states=self._states[indexes], + actions=[action[indexes] for action in self._actions], + rewards=[reward[indexes] for reward in self._rewards], + terminals=self._terminals[indexes], + next_states=self._next_states[indexes], + agent_states=[state[indexes] for state in self._agent_states], + next_agent_states=[state[indexes] for state in self._next_agent_states], + ) + + @abstractmethod + def _get_forbid_last(self) -> bool: + raise NotImplementedError + + +class RandomMultiReplayMemory(MultiReplayMemory): + def __init__( + self, + capacity: int, + state_dim: int, + action_dims: List[int], + agent_states_dims: List[int], + random_overwrite: bool = False, + ) -> None: + super(RandomMultiReplayMemory, self).__init__( + capacity, state_dim, action_dims, RandomIndexScheduler(capacity, random_overwrite), + agent_states_dims + ) + self._random_overwrite = random_overwrite + self._scheduler = RandomIndexScheduler(capacity, random_overwrite) + + @property + def random_overwrite(self) -> bool: + return self._random_overwrite + + def _get_forbid_last(self) -> bool: + return False + + +class FIFOMultiReplayMemory(MultiReplayMemory): + def __init__( + self, + capacity: int, + state_dim: int, + action_dims: List[int], + agent_states_dims: List[int], + ) -> None: + super(FIFOMultiReplayMemory, self).__init__( + capacity, state_dim, action_dims, FIFOIndexScheduler(capacity), + agent_states_dims, + ) + + def _get_forbid_last(self) -> bool: + return not self._terminals[self._idx_scheduler.get_last_index()] diff --git a/maro/rl/training/train_ops.py b/maro/rl/training/train_ops.py new file mode 100644 index 000000000..c22b1d0d3 --- /dev/null +++ b/maro/rl/training/train_ops.py @@ -0,0 +1,240 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import inspect +from abc import ABCMeta, abstractmethod +from typing import Callable, Tuple + +import torch +import zmq +from zmq.asyncio import Context, Poller + +from maro.rl.policy import RLPolicy +from maro.rl.utils import AbsTransitionBatch, MultiTransitionBatch, TransitionBatch +from maro.rl.utils.common import bytes_to_pyobj, get_ip_address_by_hostname, pyobj_to_bytes +from maro.utils import DummyLogger, Logger + + +class AbsTrainOps(object, metaclass=ABCMeta): + """The basic component for training a policy, which takes charge of loss / gradient computation and policy update. + Each ops is used for training a single policy. An ops is an atomic unit in the distributed mode. + + Args: + device (str): Identifier for the torch device. The policy will be moved to the specified device. + If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. + is_single_scenario (bool): Flag indicating whether the ops belongs to a `SingleTrainer` or a `MultiTrainer`. + get_policy_func (Callable[[], RLPolicy]): Function used to create the policy of this ops. + """ + + def __init__( + self, + name: str, + device: str, + is_single_scenario: bool, + get_policy_func: Callable[[], RLPolicy], + parallelism: int = 1, + ) -> None: + super(AbsTrainOps, self).__init__() + self._name = name + self._device = torch.device(device) if device is not None \ + else torch.device("cuda" if torch.cuda.is_available() else "cpu") + self._is_single_scenario = is_single_scenario + + # Create the policy and put it on the right device. + if self._is_single_scenario: + self._policy = get_policy_func() + self._policy.to_device(self._device) + + self._parallelism = parallelism + + @property + def name(self) -> str: + return self._name + + @property + def policy_state_dim(self) -> int: + return self._policy.state_dim + + @property + def policy_action_dim(self) -> int: + return self._policy.action_dim + + @property + def parallelism(self) -> int: + return self._parallelism + + def _is_valid_transition_batch(self, batch: AbsTransitionBatch) -> bool: + """Used to check the transition batch's type. If this ops is used under a single trainer, the batch should be + a `TransitionBatch`. Otherwise, it should be a `MultiTransitionBatch`. + + Args: + batch (AbsTransitionBatch): The batch to be validated. + """ + return isinstance(batch, TransitionBatch) if self._is_single_scenario \ + else isinstance(batch, MultiTransitionBatch) + + @abstractmethod + def get_state(self) -> dict: + """Get the train ops's state. + + Returns: + A dict that contains ops's state. + """ + raise NotImplementedError + + @abstractmethod + def set_state(self, ops_state_dict: dict) -> None: + """Set ops's state. + + Args: + ops_state_dict (dict): New ops state. + """ + raise NotImplementedError + + def get_policy_state(self) -> Tuple[str, object]: + """Get the policy's state. + + Returns: + policy_name (str) + policy_state (object) + """ + return self._policy.name, self._policy.get_state() + + def set_policy_state(self, policy_state: object) -> None: + """Update the policy's state. + + Args: + policy_state (object): The policy state. + """ + self._policy.set_state(policy_state) + + +def remote(func) -> Callable: + """Annotation to indicate that a function / method can be called remotely. + + This annotation takes effect only when an ``AbsTrainOps`` object is wrapped by a ``RemoteOps``. + """ + + def remote_annotate(*args, **kwargs) -> object: + return func(*args, **kwargs) + + return remote_annotate + + +class AsyncClient(object): + """Facility used by a ``RemoteOps`` instance to communicate asynchronously with ``TrainingProxy``. + + Args: + name (str): Name of the client. + address (Tuple[str, int]): Address (host and port) of the training proxy. + logger (Logger, default=None): logger. + """ + + def __init__(self, name: str, address: Tuple[str, int], logger: Logger = None) -> None: + self._logger = DummyLogger() if logger is None else logger + self._name = name + host, port = address + self._proxy_ip = get_ip_address_by_hostname(host) + self._address = f"tcp://{self._proxy_ip}:{port}" + self._logger.info(f"Proxy address: {self._address}") + + async def send_request(self, req: dict) -> None: + """Send a request to the proxy in asynchronous fashion. + + This is a coroutine and is executed asynchronously with calls to other AsyncClients' ``send_request`` calls. + + Args: + req (dict): Request that contains task specifications and parameters. + """ + await self._socket.send(pyobj_to_bytes(req)) + self._logger.debug(f"{self._name} sent request {req['func']}") + + async def get_response(self) -> object: + """Waits for a result in asynchronous fashion. + + This is a coroutine and is executed asynchronously with calls to other AsyncClients' ``get_response`` calls. + This ensures that all clients' tasks are sent out as soon as possible before the waiting for results starts. + """ + while True: + events = await self._poller.poll(timeout=100) + if self._socket in dict(events): + result = await self._socket.recv_multipart() + self._logger.debug(f"{self._name} received result") + return bytes_to_pyobj(result[0]) + + def close(self) -> None: + """Close the connection to the proxy. + """ + self._poller.unregister(self._socket) + self._socket.disconnect(self._address) + self._socket.close() + + def connect(self) -> None: + """Establish the connection to the proxy. + """ + self._socket = Context.instance().socket(zmq.DEALER) + self._socket.setsockopt_string(zmq.IDENTITY, self._name) + self._socket.setsockopt(zmq.LINGER, 0) + self._socket.connect(self._address) + self._logger.debug(f"connected to {self._address}") + self._poller = Poller() + self._poller.register(self._socket, zmq.POLLIN) + + async def exit(self) -> None: + """Send EXIT signals to the proxy indicating no more tasks. + """ + await self._socket.send(b"EXIT") + + +class RemoteOps(object): + """Wrapper for ``AbsTrainOps``. + + RemoteOps provides similar interfaces to ``AbsTrainOps``. Any method annotated by the remote decorator in the + definition of the train ops is transformed to a remote method. Calling this method invokes using the internal + ``AsyncClient`` to send the required task parameters to a ``TrainingProxy`` that handles task dispatching and + result collection. Methods not annotated by the decorator are not affected. + + Args: + ops (AbsTrainOps): An ``AbsTrainOps`` instance to be wrapped. Any method annotated by the remote decorator in + its definition is transformed to a remote function call. + address (Tuple[str, int]): Address (host and port) of the training proxy. + logger (Logger, default=None): logger. + """ + + def __init__(self, ops: AbsTrainOps, address: Tuple[str, int], logger: Logger = None) -> None: + self._ops = ops + self._client = AsyncClient(self._ops.name, address, logger=logger) + self._client.connect() + + def __getattribute__(self, attr_name: str) -> object: + # Ignore methods that belong to the parent class + try: + return super().__getattribute__(attr_name) + except AttributeError: + pass + + def remote_method(ops_state, func_name: str, desired_parallelism: int, client: AsyncClient) -> Callable: + async def remote_call(*args, **kwargs) -> object: + req = { + "state": ops_state, + "func": func_name, + "args": args, + "kwargs": kwargs, + "desired_parallelism": desired_parallelism, + } + await client.send_request(req) + response = await client.get_response() + return response + + return remote_call + + attr = getattr(self._ops, attr_name) + if inspect.ismethod(attr) and attr.__name__ == "remote_annotate": + return remote_method(self._ops.get_state(), attr_name, self._ops.parallelism, self._client) + + return attr + + async def exit(self) -> None: + """Close the internal task client. + """ + await self._client.exit() diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py new file mode 100644 index 000000000..d75a0165c --- /dev/null +++ b/maro/rl/training/trainer.py @@ -0,0 +1,267 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from abc import ABCMeta, abstractmethod +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +import torch + +from maro.rl.policy import RLPolicy +from maro.rl.rollout import ExpElement +from maro.utils import Logger + +from .train_ops import AbsTrainOps, RemoteOps +from .utils import extract_trainer_name + + +@dataclass +class TrainerParams: + """Common trainer parameters. + + device (str, default=None): Name of the device to store this trainer. If it is None, the device will be + automatically determined according to GPU availability. + replay_memory_capacity (int, default=100000): Maximum capacity of the replay memory. + batch_size (int, default=128): Training batch size. + data_parallelism (int, default=1): Degree of data parallelism. A value greater than 1 can be used when + a model is large and computing gradients with respect to a batch becomes expensive. In this case, the + batch may be split into multiple smaller batches whose gradients can be computed in parallel on a set + of remote nodes. For simplicity, only synchronous parallelism is supported, meaning that the model gets + updated only after collecting all the gradients from the remote nodes. Note that this value is the desired + parallelism and the actual parallelism in a distributed experiment may be smaller depending on the + availability of compute resources. For details on distributed deep learning and data parallelism, see + https://web.stanford.edu/~rezab/classes/cme323/S16/projects_reports/hedge_usmani.pdf, as well as an abundance + of resources available on the internet. + + """ + device: str = None + replay_memory_capacity: int = 10000 + batch_size: int = 128 + data_parallelism: int = 1 + + @abstractmethod + def extract_ops_params(self) -> Dict[str, object]: + """Extract parameters that should be passed to the train ops. + + Returns: + params (Dict[str, object]): Parameter dict. + """ + raise NotImplementedError + + +class AbsTrainer(object, metaclass=ABCMeta): + """Policy trainer used to train policies. Trainer maintains a group of train ops and + controls training logics of them, while train ops take charge of specific policy updating. + + Trainer will hold one or more replay memories to store the experiences, and it will also maintain a duplication + of all policies it trains. However, trainer will not do any actual computations. All computations will be + done in the train ops. + + Args: + name (str): Name of the trainer. + params (TrainerParams): Trainer's parameters. + """ + + def __init__(self, name: str, params: TrainerParams) -> None: + self._name = name + self._batch_size = params.batch_size + self._agent2policy: Dict[str, str] = {} + self._proxy_address: Optional[Tuple[str, int]] = None + self._logger = None + + @property + def name(self) -> str: + return self._name + + @property + def agent_num(self) -> int: + return len(self._agent2policy) + + def register_logger(self, logger: Logger) -> None: + self._logger = logger + + def register_agent2policy(self, agent2policy: Dict[Any, str]) -> None: + """Register the agent to policy dict that correspond to the current trainer. A valid policy name should start + with the name of its trainer. For example, "DQN.POLICY_NAME". Therefore, we could identify which policies + should be registered to the current trainer according to the policy's name. + + Args: + agent2policy (Dict[Any, str]): Agent name to policy name mapping. + """ + self._agent2policy = { + agent_name: policy_name + for agent_name, policy_name in agent2policy.items() + if extract_trainer_name(policy_name) == self.name + } + + @abstractmethod + def register_policy_creator( + self, + global_policy_creator: Dict[str, Callable[[str], RLPolicy]], + ) -> None: + """Register the policy creator. Only keep the creators of the policies that the current trainer need to train. + + Args: + global_policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict that contains the creators for all + policies. + """ + raise NotImplementedError + + @abstractmethod + def build(self) -> None: + """Create the required train-ops and replay memory. This should be called before invoking `train` or + `train_as_task`. + """ + raise NotImplementedError + + @abstractmethod + def train(self) -> None: + """Run a training step to update all the policies that this trainer is responsible for. + """ + raise NotImplementedError + + async def train_as_task(self) -> None: + """Update all policies managed by the trainer as an asynchronous task. + """ + raise NotImplementedError + + @abstractmethod + def record(self, env_idx: int, exp_element: ExpElement) -> None: + """Record rollout experiences in the replay memory. + + Args: + env_idx (int): The index of the environment that generates this batch of experiences. This is used + when there are more than one environment collecting experiences in parallel. + exp_element (ExpElement): Experiences. + """ + raise NotImplementedError + + def set_proxy_address(self, proxy_address: Tuple[str, int]) -> None: + self._proxy_address = proxy_address + + @abstractmethod + def get_local_ops_by_name(self, name: str) -> AbsTrainOps: + """Create an `AbsTrainOps` instance with a given name. + + Args: + name (str): Ops name. + + Returns: + ops (AbsTrainOps): The local ops. + """ + raise NotImplementedError + + def get_ops(self, name: str) -> Union[RemoteOps, AbsTrainOps]: + """Create an `AbsTrainOps` instance with a given name. If a proxy address has been registered to the trainer, + this returns a `RemoteOps` instance in which all methods annotated as "remote" are turned into a remote method + call. Otherwise, a regular `AbsTrainOps` is returned. + + Args: + name (str): Ops name. + + Returns: + ops (Union[RemoteOps, AbsTrainOps]): The ops. + """ + ops = self.get_local_ops_by_name(name) + return RemoteOps(ops, self._proxy_address, logger=self._logger) if self._proxy_address else ops + + @abstractmethod + def get_policy_state(self) -> Dict[str, object]: + """Get policies' states. + + Returns: + A double-deck dict with format: {policy_name: policy_state}. + """ + raise NotImplementedError + + @abstractmethod + def load(self, path: str) -> None: + raise NotImplementedError + + @abstractmethod + def save(self, path: str) -> None: + raise NotImplementedError + + @abstractmethod + async def exit(self) -> None: + raise NotImplementedError + + +class SingleTrainer(AbsTrainer, metaclass=ABCMeta): + """Policy trainer that trains only one policy. + """ + + def __init__(self, name: str, params: TrainerParams) -> None: + super(SingleTrainer, self).__init__(name, params) + + self._ops: Union[RemoteOps, None] = None # To be created in `build()` + + self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = {} + self._policy_name: Optional[str] = None + self._get_policy_func: Optional[Callable] = None + + def register_policy_creator( + self, + global_policy_creator: Dict[str, Callable[[str], RLPolicy]] + ) -> None: + self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = { + policy_name: func for policy_name, func in global_policy_creator.items() + if extract_trainer_name(policy_name) == self.name + } + + if len(self._policy_creator) == 0: + raise ValueError(f"Trainer {self._name} has no policies") + if len(self._policy_creator) > 1: + raise ValueError(f"Trainer {self._name} cannot have more than one policy assigned to it") + + self._policy_name = list(self._policy_creator.keys())[0] + self._get_policy_func = lambda: self._policy_creator[self._policy_name](self._policy_name) + + def get_policy_state(self) -> Dict[str, object]: + self._assert_ops_exists() + policy_name, state = self._ops.get_policy_state() + return {policy_name: state} + + def load(self, path: str) -> None: + self._assert_ops_exists() + self._ops.set_state(torch.load(path)) + + def save(self, path: str) -> None: + self._assert_ops_exists() + torch.save(self._ops.get_state(), path) + + def _assert_ops_exists(self) -> None: + if not self._ops: + raise ValueError("'build' needs to be called to create an ops instance first.") + + async def exit(self) -> None: + if isinstance(self._ops, RemoteOps): + await self._ops.exit() + + +class MultiTrainer(AbsTrainer, metaclass=ABCMeta): + """Policy trainer that trains multiple policies. + """ + + def __init__(self, name: str, params: TrainerParams) -> None: + super(MultiTrainer, self).__init__(name, params) + self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = {} + self._policy_names: List[str] = [] + + def register_policy_creator( + self, + global_policy_creator: Dict[str, Callable[[str], RLPolicy]], + ) -> None: + self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = { + policy_name: func for policy_name, func in global_policy_creator.items() + if extract_trainer_name(policy_name) == self.name + } + self._policy_names = sorted(list(self._policy_creator.keys())) + + @abstractmethod + def get_policy_state(self) -> Dict[str, object]: + raise NotImplementedError + + @abstractmethod + async def exit(self) -> None: + raise NotImplementedError diff --git a/maro/rl/training/trainer_manager.py b/maro/rl/training/trainer_manager.py new file mode 100644 index 000000000..87959ea93 --- /dev/null +++ b/maro/rl/training/trainer_manager.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import asyncio +import os +from itertools import chain +from typing import Callable, Dict, Iterable, List, Tuple + +from maro.rl.policy import RLPolicy +from maro.rl.rollout import ExpElement +from maro.utils import Logger + +from .trainer import AbsTrainer +from .utils import extract_trainer_name, get_trainer_state_path + + +class TrainerManager(object): + """ + Trainer manager. Manage and schedule all trainers to train policies. + + Args: + policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict of functions to create policies. + trainer_creator (Dict[str, Callable[[str], AbsTrainer]]): Dict of functions to create trainers. + agent2policy (Dict[str, str]): Agent name to policy name mapping. + proxy_address (Tuple[str, int], default=None): Address of the training proxy. If it is not None, + it is registered to all trainers, which in turn create `RemoteOps` for distributed training. + """ + + def __init__( + self, + policy_creator: Dict[str, Callable[[str], RLPolicy]], + trainer_creator: Dict[str, Callable[[str], AbsTrainer]], + agent2policy: Dict[str, str], # {agent_name: policy_name} + proxy_address: Tuple[str, int] = None, + logger: Logger = None, + ) -> None: + """ + Trainer manager. + + Args: + policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict of functions to create policies. + trainer_creator (Dict[str, Callable[[str], AbsTrainer]]): Dict of functions to create trainers. + agent2policy (Dict[str, str]): Agent name to policy name mapping. + proxy_address (Tuple[str, int]): The address of the proxy. This is used only in distributed mode. + Defaults to None. + """ + super(TrainerManager, self).__init__() + + self._trainer_dict: Dict[str, AbsTrainer] = {} + self._agent2policy = agent2policy + self._proxy_address = proxy_address + for trainer_name, func in trainer_creator.items(): + trainer = func(trainer_name) + if self._proxy_address: + trainer.set_proxy_address(self._proxy_address) + trainer.register_agent2policy(self._agent2policy) + trainer.register_policy_creator(policy_creator) + trainer.register_logger(logger) + trainer.build() # `build()` must be called after `register_policy_creator()` + self._trainer_dict[trainer_name] = trainer + + self._agent2trainer = { + agent_name: extract_trainer_name(policy_name) + for agent_name, policy_name in self._agent2policy.items() + } + + def train(self) -> None: + if self._proxy_address: + async def train_step() -> Iterable: + return await asyncio.gather(*[trainer.train_as_task() for trainer in self._trainer_dict.values()]) + + asyncio.run(train_step()) + else: + for trainer in self._trainer_dict.values(): + trainer.train() + + def get_policy_state(self) -> Dict[str, Dict[str, object]]: + """Get policies' states. + + Returns: + A double-deck dict with format: {trainer_name: {policy_name: policy_state}} + """ + return dict(chain(*[trainer.get_policy_state().items() for trainer in self._trainer_dict.values()])) + + def record_experiences(self, experiences: List[List[ExpElement]]) -> None: + """Record experiences collected from external modules (for example, EnvSampler). + + Args: + experiences (List[ExpElement]): List of experiences. Each ExpElement stores the complete information for a + tick. Please refers to the definition of ExpElement for detailed explanation of ExpElement. + """ + for env_idx, env_experience in enumerate(experiences): + for exp_element in env_experience: # Dispatch experiences to trainers tick by tick. + exp_dict = exp_element.split_contents(self._agent2trainer) + for trainer_name, exp_elem in exp_dict.items(): + trainer = self._trainer_dict[trainer_name] + trainer.record(env_idx, exp_elem) + + def load(self, path: str) -> List[str]: + loaded = [] + for trainer_name, trainer in self._trainer_dict.items(): + pth = get_trainer_state_path(path, trainer_name) + if os.path.isfile(pth): + trainer.load(pth) + loaded.append(trainer_name) + + return loaded + + def save(self, path: str) -> None: + os.makedirs(path, exist_ok=True) + for trainer_name, trainer in self._trainer_dict.items(): + trainer.save(get_trainer_state_path(path, trainer_name)) + + def exit(self) -> None: + if self._proxy_address: + async def exit_all() -> Iterable: + return await asyncio.gather(*[trainer.exit() for trainer in self._trainer_dict.values()]) + + asyncio.run(exit_all()) diff --git a/maro/rl/training/utils.py b/maro/rl/training/utils.py new file mode 100644 index 000000000..c0843c158 --- /dev/null +++ b/maro/rl/training/utils.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os + +FILE_SUFFIX = "ckpt" + + +def extract_trainer_name(policy_name: str) -> str: + """Extract the trainer name from the policy name. + + Args: + policy_name (str): Policy name. + + Returns: + trainer_name (str) + """ + return policy_name.split(".")[0] + + +def get_trainer_state_path(dir_path: str, trainer_name: str) -> str: + return os.path.join(dir_path, f"{trainer_name}.{FILE_SUFFIX}") diff --git a/maro/rl/training/worker.py b/maro/rl/training/worker.py new file mode 100644 index 000000000..cc6b2b627 --- /dev/null +++ b/maro/rl/training/worker.py @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import Callable, Dict + +from maro.rl.distributed import AbsWorker +from maro.rl.policy import RLPolicy +from maro.rl.utils.common import bytes_to_pyobj, bytes_to_string, pyobj_to_bytes +from maro.utils import Logger + +from .train_ops import AbsTrainOps +from .trainer import AbsTrainer + + +class TrainOpsWorker(AbsWorker): + """Worker that executes methods defined in a subclass of ``AbsTrainOps`` and annotated as "remote" on demand. + + Args: + idx (int): Integer identifier for the worker. It is used to generate an internal ID, "worker.{idx}", + so that the proxy can keep track of its connection status. + policy_creator (Dict[str, Callable[[str], RLPolicy]]): User-defined function registry that can be used to create + an "RLPolicy" instance with a name in the registry. This is required to create train ops instances. + trainer_creator (Dict[str, Callable[[str], AbsTrainer]]): User-defined function registry that can be used to + create an "AbsTrainer" instance with a name in the registry. This is required to create train ops instances. + producer_host (str): IP address of the proxy host to connect to. + producer_port (int, default=10001): Port of the proxy host to connect to. + """ + + def __init__( + self, + idx: int, + policy_creator: Dict[str, Callable[[str], RLPolicy]], + trainer_creator: Dict[str, Callable[[str], AbsTrainer]], + producer_host: str, + producer_port: int = 10001, + logger: Logger = None, + ) -> None: + super(TrainOpsWorker, self).__init__( + idx=idx, producer_host=producer_host, producer_port=producer_port, logger=logger, + ) + + self._policy_creator = policy_creator + self._trainer_creator = trainer_creator + self._trainer_dict: Dict[str, AbsTrainer] = {} + + self._ops_dict: Dict[str, AbsTrainOps] = {} + + def _compute(self, msg: list) -> None: + """Execute a method defined by some train ops and annotated as "remote". + + Args: + msg (list): Multi-part message containing task specifications and parameters. + """ + if msg[-1] == b"EXIT": + self._stream.send(b"EXIT_ACK") + self.stop() + else: + ops_name, req = bytes_to_string(msg[0]), bytes_to_pyobj(msg[-1]) + assert isinstance(req, dict) + + if ops_name not in self._ops_dict: + trainer_name = ops_name.split(".")[0] + if trainer_name not in self._trainer_dict: + trainer = self._trainer_creator[trainer_name](trainer_name) + trainer.register_policy_creator(self._policy_creator) + self._trainer_dict[trainer_name] = trainer + + self._ops_dict[ops_name] = self._trainer_dict[trainer_name].get_local_ops_by_name(ops_name) + self._logger.info(f"Created ops {ops_name} at {self._id}") + + self._ops_dict[ops_name].set_state(req["state"]) + func = getattr(self._ops_dict[ops_name], req["func"]) + result = func(*req["args"], **req["kwargs"]) + self._stream.send_multipart([msg[0], pyobj_to_bytes(result)]) diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 8f21f4c52..41a3f9a28 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -1,8 +1,18 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .gradient_averaging import average_grads -from .message_enums import MsgKey, MsgTag +from typing import Union + +from .objects import SHAPE_CHECK_FLAG +from .torch_utils import average_grads, match_shape, ndarray_to_tensor from .trajectory_computation import discount_cumsum +from .transition_batch import MultiTransitionBatch, TransitionBatch, merge_transition_batches + +AbsTransitionBatch = Union[TransitionBatch, MultiTransitionBatch] -__all__ = ["MsgKey", "MsgTag", "average_grads", "discount_cumsum"] +__all__ = [ + "SHAPE_CHECK_FLAG", + "average_grads", "match_shape", "ndarray_to_tensor", + "discount_cumsum", + "AbsTransitionBatch", "MultiTransitionBatch", "TransitionBatch", "merge_transition_batches", +] diff --git a/maro/rl/utils/common.py b/maro/rl/utils/common.py new file mode 100644 index 000000000..1a0d26d75 --- /dev/null +++ b/maro/rl/utils/common.py @@ -0,0 +1,72 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import os +import pickle +import socket +from typing import List, Optional + + +def get_env(var_name: str, required: bool = True, default: object = None) -> str: + """Wrapper for os.getenv() that includes a check for mandatory environment variables. + + Args: + var_name (str): Variable name. + required (bool, default=True): Flag indicating whether the environment variable in questions is required. + If this is true and the environment variable is not present in ``os.environ``, a ``KeyError`` is raised. + default (object, default=None): Default value for the environment variable if it is missing in ``os.environ`` + and ``required`` is false. Ignored if ``required`` is True. + + Returns: + The environment variable. + """ + if var_name not in os.environ: + if required: + raise KeyError(f"Missing environment variable: {var_name}") + return default + + return os.getenv(var_name) + + +def int_or_none(val: Optional[str]) -> Optional[int]: + return int(val) if val is not None else None + + +def float_or_none(val: Optional[str]) -> Optional[float]: + return float(val) if val is not None else None + + +def list_or_none(vals_str: Optional[str]) -> List[int]: + return [int(val) for val in vals_str.split()] if vals_str is not None else [] + + +# serialization and deserialization for messaging +DEFAULT_MSG_ENCODING = "utf-8" + + +def string_to_bytes(s: str) -> bytes: + return s.encode(DEFAULT_MSG_ENCODING) + + +def bytes_to_string(bytes_: bytes) -> str: + return bytes_.decode(DEFAULT_MSG_ENCODING) + + +def pyobj_to_bytes(pyobj) -> bytes: + return pickle.dumps(pyobj) + + +def bytes_to_pyobj(bytes_: bytes) -> object: + return pickle.loads(bytes_) + + +def get_own_ip_address() -> str: + return socket.gethostbyname(socket.gethostname()) + + +def get_ip_address_by_hostname(host: str) -> str: + while True: + try: + return socket.gethostbyname(host) + except Exception: + continue diff --git a/maro/rl/utils/gradient_averaging.py b/maro/rl/utils/gradient_averaging.py deleted file mode 100644 index 3b8c83f34..000000000 --- a/maro/rl/utils/gradient_averaging.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from typing import List - -import torch - - -def average_grads(grad_list: List[dict]): - """Obtain the average of a list of gradients.""" - return { - param_name: torch.mean(torch.stack([grad[param_name] for grad in grad_list]), dim=0) - for param_name in grad_list[0] - } diff --git a/maro/rl/utils/message_enums.py b/maro/rl/utils/message_enums.py index 1cb1d9f62..3da0bd74d 100644 --- a/maro/rl/utils/message_enums.py +++ b/maro/rl/utils/message_enums.py @@ -22,6 +22,9 @@ class MsgTag(Enum): ABORT_ROLLOUT = "abort_rollout" DONE = "done" EXIT = "exit" + REQUEST_WORKER = "request_worker" + RELEASE_WORKER = "release_worker" + ASSIGN_WORKER = "assign_worker" class MsgKey(Enum): @@ -33,13 +36,16 @@ class MsgKey(Enum): STEP = "step" POLICY_IDS = "policy_ids" ROLLOUT_INFO = "rollout_info" - TRACKER = "tracker" + INTO = "info" GRAD_TASK = "grad_task" - WORKER_INFO = "worker_info" + GRAD_SCOPE = "grad_scope" LOSS_INFO = "loss_info" STATE = "state" + TENSOR = "tensor" POLICY_STATE = "policy_state" EXPLORATION_STEP = "exploration_step" VERSION = "version" STEP_RANGE = "step_range" END_OF_EPISODE = "end_of_episode" + WORKER_ID = "worker_id" + WORKER_ID_LIST = "worker_id_list" diff --git a/maro/cli/process/utils/__init__.py b/maro/rl/utils/objects.py similarity index 74% rename from maro/cli/process/utils/__init__.py rename to maro/rl/utils/objects.py index 9a0454564..e4dc3160d 100644 --- a/maro/cli/process/utils/__init__.py +++ b/maro/rl/utils/objects.py @@ -1,2 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. + +SHAPE_CHECK_FLAG = True diff --git a/maro/rl/utils/torch_utils.py b/maro/rl/utils/torch_utils.py new file mode 100644 index 000000000..b536916e0 --- /dev/null +++ b/maro/rl/utils/torch_utils.py @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from typing import List, Union + +import numpy as np +import torch + +from .objects import SHAPE_CHECK_FLAG + + +def match_shape(tensor: Union[torch.Tensor, np.ndarray], shape: tuple) -> bool: + """Check if a torch.Tensor / np.ndarray could match the expected shape. + + Args: + tensor (Union[torch.Tensor, np.ndarray]): Tensor. + shape (tuple): The expected shape tuple. If an element in this tuple is None, it means this dimension + could match any value (usually used for the `batch_size` dimension). + + Returns: + Whether the tensor could match the expected shape. + """ + if not SHAPE_CHECK_FLAG: + return True + else: + if len(tensor.shape) != len(shape): + return False + for val, expected in zip(tensor.shape, shape): + if expected is not None and expected != val: + return False + return True + + +def ndarray_to_tensor(array: np.ndarray, device: torch.device) -> torch.Tensor: + """ + Convert a np.ndarray to a torch.Tensor. + + Args: + array (np.ndarray): The input ndarray. + device (torch.device): The device to assign this tensor. + + Returns: + A tensor with same shape and values. + """ + return torch.from_numpy(array).to(device) + + +def average_grads(grad_list: List[dict]) -> dict: + """Obtain the average of a list of gradients. + """ + if len(grad_list) == 1: + return grad_list[0] + return { + param_name: torch.mean(torch.stack([grad[param_name] for grad in grad_list]), dim=0) + for param_name in grad_list[0] + } diff --git a/maro/rl/utils/trajectory_computation.py b/maro/rl/utils/trajectory_computation.py index 94afb51cb..dbfba4862 100644 --- a/maro/rl/utils/trajectory_computation.py +++ b/maro/rl/utils/trajectory_computation.py @@ -1,11 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from typing import Union + import numpy as np import scipy.signal -def discount_cumsum(x, discount): +def discount_cumsum(x: Union[np.ndarray, list], discount: float) -> np.ndarray: """ Magic from rllab for computing discounted cumulative sums of vectors. diff --git a/maro/rl/utils/transition_batch.py b/maro/rl/utils/transition_batch.py new file mode 100644 index 000000000..30c632d04 --- /dev/null +++ b/maro/rl/utils/transition_batch.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + +import numpy as np + +from . import discount_cumsum +from .objects import SHAPE_CHECK_FLAG + + +@dataclass +class TransitionBatch: + states: np.ndarray # 2D + actions: np.ndarray # 2D + rewards: np.ndarray # 1D + next_states: np.ndarray # 2D + terminals: np.ndarray # 1D + returns: np.ndarray = None # 1D + advantages: np.ndarray = None # 1D + + @property + def size(self) -> int: + return self.states.shape[0] + + def __post_init__(self) -> None: + if SHAPE_CHECK_FLAG: + assert len(self.states.shape) == 2 and self.states.shape[0] > 0 + assert len(self.actions.shape) == 2 and self.actions.shape[0] == self.states.shape[0] + assert len(self.rewards.shape) == 1 and self.rewards.shape[0] == self.states.shape[0] + assert self.next_states.shape == self.states.shape + assert len(self.terminals.shape) == 1 and self.terminals.shape[0] == self.states.shape[0] + + def calc_returns(self, discount_factor: float) -> None: + self.returns = discount_cumsum(self.rewards, discount_factor) + + def make_kth_sub_batch(self, i: int, k: int) -> TransitionBatch: + return TransitionBatch( + states=self.states[i::k], + actions=self.actions[i::k], + rewards=self.rewards[i::k], + next_states=self.next_states[i::k], + terminals=self.terminals[i::k], + returns=self.returns[i::k] if self.returns is not None else None, + advantages=self.advantages[i::k] if self.advantages is not None else None, + ) + + def split(self, k: int) -> List[TransitionBatch]: + return [self.make_kth_sub_batch(i, k) for i in range(k)] + + +@dataclass +class MultiTransitionBatch: + states: np.ndarray # 2D + actions: List[np.ndarray] # List of 2D + rewards: List[np.ndarray] # List of 1D + next_states: np.ndarray # 2D + agent_states: List[np.ndarray] # List of 2D + next_agent_states: List[np.ndarray] # List of 2D + terminals: np.ndarray # 1D + + returns: Optional[List[np.ndarray]] = None # List of 1D + advantages: Optional[List[np.ndarray]] = None # List of 1D + + @property + def size(self) -> int: + return self.states.shape[0] + + def __post_init__(self) -> None: + if SHAPE_CHECK_FLAG: + assert len(self.states.shape) == 2 and self.states.shape[0] > 0 + + assert len(self.actions) == len(self.rewards) + assert len(self.agent_states) == len(self.actions) + for i in range(len(self.actions)): + assert len(self.actions[i].shape) == 2 and self.actions[i].shape[0] == self.states.shape[0] + assert len(self.rewards[i].shape) == 1 and self.rewards[i].shape[0] == self.states.shape[0] + assert len(self.agent_states[i].shape) == 2 + assert self.agent_states[i].shape[0] == self.states.shape[0] + + assert len(self.terminals.shape) == 1 and self.terminals.shape[0] == self.states.shape[0] + assert self.next_states.shape == self.states.shape + + assert len(self.next_agent_states) == len(self.agent_states) + for i in range(len(self.next_agent_states)): + assert self.agent_states[i].shape == self.next_agent_states[i].shape + + def calc_returns(self, discount_factor: float) -> None: + self.returns = [discount_cumsum(reward, discount_factor) for reward in self.rewards] + + def make_kth_sub_batch(self, i: int, k: int) -> MultiTransitionBatch: + states = self.states[i::k] + actions = [action[i::k] for action in self.actions] + rewards = [reward[i::k] for reward in self.rewards] + next_states = self.next_states[i::k] + agent_states = [state[i::k] for state in self.agent_states] + next_agent_states = [state[i::k] for state in self.next_agent_states] + terminals = self.terminals[i::k] + returns = None if self.returns is None else [r[i::k] for r in self.returns] + advantages = None if self.advantages is None else [advantage[i::k] for advantage in self.advantages] + return MultiTransitionBatch( + states, actions, rewards, next_states, agent_states, + next_agent_states, terminals, returns, advantages, + ) + + def split(self, k: int) -> List[MultiTransitionBatch]: + return [self.make_kth_sub_batch(i, k) for i in range(k)] + + +def merge_transition_batches(batch_list: List[TransitionBatch]) -> TransitionBatch: + return TransitionBatch( + states=np.concatenate([batch.states for batch in batch_list], axis=0), + actions=np.concatenate([batch.actions for batch in batch_list], axis=0), + rewards=np.concatenate([batch.rewards for batch in batch_list], axis=0), + next_states=np.concatenate([batch.next_states for batch in batch_list], axis=0), + terminals=np.concatenate([batch.terminals for batch in batch_list]), + returns=np.concatenate([batch.returns for batch in batch_list]), + advantages=np.concatenate([batch.advantages for batch in batch_list]), + ) diff --git a/maro/rl/workflows/__init__.py b/maro/rl/workflows/config/__init__.py similarity index 51% rename from maro/rl/workflows/__init__.py rename to maro/rl/workflows/config/__init__.py index 9a0454564..59348d2b7 100644 --- a/maro/rl/workflows/__init__.py +++ b/maro/rl/workflows/config/__init__.py @@ -1,2 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. + +from .parser import ConfigParser + +__all__ = [ + "ConfigParser", +] diff --git a/maro/rl/workflows/config/parser.py b/maro/rl/workflows/config/parser.py new file mode 100644 index 000000000..fbcc1adcd --- /dev/null +++ b/maro/rl/workflows/config/parser.py @@ -0,0 +1,368 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import ipaddress +import os +from typing import Union + +import yaml + +from maro.utils.logger import LEVEL_MAP + + +class ConfigParser: + """Configuration parser for running RL workflows. + + Args: + config (Union[str, dict]): A dictionary configuration or a path to a Yaml file that contains + the configuration. If it is a path, the parser will attempt to read it into a dictionary + in memory. + """ + + def __init__(self, config: Union[str, dict]) -> None: + assert isinstance(config, (dict, str)) + if isinstance(config, str): + with open(config, "r") as fp: + self._config = yaml.safe_load(fp) + else: + self._config = config + + self._validation_err_pfx = f"Invalid configuration: {self._config}" + self._validate() + + @property + def config(self) -> dict: + return self._config + + def _validate(self) -> None: + if "job" not in self._config: + raise KeyError(f"{self._validation_err_pfx}: missing field 'job'") + if "scenario_path" not in self._config: + raise KeyError(f"{self._validation_err_pfx}: missing field 'scenario_path'") + if "log_path" not in self._config: + raise KeyError(f"{self._validation_err_pfx}: missing field 'log_path'") + + self._validate_main_section() + self._validate_rollout_section() + self._validate_training_section() + + def _validate_main_section(self) -> None: + if "main" not in self._config: + raise KeyError(f"{self._validation_err_pfx}: missing field 'main'") + + if "num_episodes" not in self._config["main"]: + raise KeyError(f"{self._validation_err_pfx}: missing field 'num_episodes' under section 'main'") + + num_episodes = self._config["main"]["num_episodes"] + if not isinstance(num_episodes, int) or num_episodes < 1: + raise ValueError(f"{self._validation_err_pfx}: 'main.num_episodes' must be a positive int") + + num_steps = self._config["main"].get("num_steps", None) + if num_steps is not None: + if not isinstance(num_steps, int) or num_steps <= 0: + raise ValueError(f"{self._validation_err_pfx}: 'main.num_steps' must be a positive int") + + eval_schedule = self._config["main"].get("eval_schedule", None) + if eval_schedule is not None: + if ( + not isinstance(eval_schedule, (int, list)) or + isinstance(eval_schedule, int) and eval_schedule < 1 or + isinstance(eval_schedule, list) and any(not isinstance(val, int) or val < 1 for val in eval_schedule) + ): + raise ValueError( + f"{self._validation_err_pfx}: 'main.eval_schedule' must be a single positive int or a list of " + f"positive ints" + ) + + if "logging" in self._config["main"]: + self._validate_logging_section("main", self._config["main"]["logging"]) + + def _validate_rollout_section(self) -> None: + if "rollout" not in self._config or not isinstance(self._config["rollout"], dict): + raise KeyError(f"{self._validation_err_pfx}: missing section 'rollout'") + + # validate parallel rollout config + if "parallelism" in self._config["rollout"]: + conf = self._config["rollout"]["parallelism"] + if "sampling" not in conf: + raise KeyError( + f"{self._validation_err_pfx}: missing field 'sampling' under section 'rollout.parallelism'" + ) + if not isinstance(conf["sampling"], int) or conf["sampling"] <= 0: + raise TypeError(f"{self._validation_err_pfx}: 'rollout.parallelism.sampling' must be a positive int") + if "eval" in conf and not isinstance(conf["eval"], int) or conf["eval"] <= 0: + raise TypeError(f"{self._validation_err_pfx}: 'rollout.parallelism.eval' must be a positive int") + + train_prl, eval_prl = conf["sampling"], conf.get("eval", 1) + if max(train_prl, eval_prl) > 1: + if "controller" not in conf: + raise KeyError( + f"{self._validation_err_pfx}: missing field 'controller' under section 'rollout.parallelism'" + ) + self._validate_rollout_controller_section(conf["controller"]) + + # validate optional fields: min_env_samples, grace_factor + min_env_samples = conf.get("min_env_samples", None) + if min_env_samples is not None: + if not isinstance(min_env_samples, int) or min_env_samples > train_prl: + raise ValueError( + f"{self._validation_err_pfx}: 'rollout.parallelism.min_env_samples' must be an integer " + f"that does not exceed the value of 'rollout.parallelism.sampling': {train_prl}" + ) + + grace_factor = conf.get("grace_factor", None) + if grace_factor is not None and not isinstance(grace_factor, (int, float)): + raise ValueError( + f"{self._validation_err_pfx}: 'rollout.parallelism.grace_factor' must be an int or float" + ) + + if "logging" in self._config["rollout"]: + self._validate_logging_section("rollout", self._config["rollout"]["logging"]) + + def _validate_rollout_controller_section(self, conf: dict) -> None: + if "host" not in conf: + raise KeyError( + f"{self._validation_err_pfx}: missing field 'host' under section 'rollout.parallelism.controller'" + ) + if not isinstance(conf["host"], str): + raise TypeError(f"{self._validation_err_pfx}: 'rollout.parallelism.controller.host' must be a string") + + # Check that the host string is a valid IP address + try: + ipaddress.ip_address(conf["host"]) + except ValueError: + raise ValueError( + f"{self._validation_err_pfx}: 'rollout.parallelism.controller.host' is not a valid IP address" + ) + + if "port" not in conf: + raise KeyError( + f"{self._validation_err_pfx}: missing field 'port' under section 'rollout.parallelism.controller'" + ) + if not isinstance(conf["port"], int): + raise TypeError(f"{self._validation_err_pfx}: 'rollout.parallelism.controller.port' must be an int") + + def _validate_training_section(self) -> None: + if "training" not in self._config or not isinstance(self._config["training"], dict): + raise KeyError(f"{self._validation_err_pfx}: missing field 'training'") + if "mode" not in self._config["training"]: + raise KeyError(f"{self._validation_err_pfx}: missing field 'mode' under section 'training'") + if self._config["training"]["mode"] not in {"simple", "parallel"}: + raise ValueError( + f"'mode' value under section 'training' must be 'simple' or 'parallel', got {self._config['mode']}" + ) + + if self._config["training"]["mode"] == "parallel": + if "num_workers" not in self._config["training"]: + raise KeyError(f"{self._validation_err_pfx}: missing field 'num_workers' under section 'training'") + if "proxy" not in self._config["training"]: + raise KeyError(f"{self._validation_err_pfx}: missing field 'proxy' under section 'training'") + self._validate_train_proxy_section(self._config["training"]["proxy"]) + if "logging" in self._config["training"]: + self._validate_logging_section("training", self._config["training"]["logging"]) + + if "load_path" in self._config["training"] and not isinstance(self._config["training"]["load_path"], str): + raise TypeError(f"{self._validation_err_pfx}: 'training.load_path' must be a string") + + if "checkpointing" in self._config["training"]: + self._validate_checkpointing_section(self._config["training"]["checkpointing"]) + + def _validate_train_proxy_section(self, proxy_section: dict) -> None: + if "host" not in proxy_section: + raise KeyError(f"{self._validation_err_pfx}: missing field 'host' under section 'proxy'") + if not isinstance(proxy_section["host"], str): + raise TypeError(f"{self._validation_err_pfx}: 'training.proxy.host' must be a string") + # Check that the host string is a valid IP address + try: + ipaddress.ip_address(proxy_section["host"]) + except ValueError: + raise ValueError(f"{self._validation_err_pfx}: 'training.proxy.host' is not a valid IP address") + + if "frontend" not in proxy_section: + raise KeyError(f"{self._validation_err_pfx}: missing field 'frontend' under section 'proxy'") + if not isinstance(proxy_section["frontend"], int): + raise TypeError(f"{self._validation_err_pfx}: 'training.proxy.frontend' must be an int") + + if "backend" not in proxy_section: + raise KeyError(f"{self._validation_err_pfx}: missing field 'backend' under section 'proxy'") + if not isinstance(proxy_section["backend"], int): + raise TypeError(f"{self._validation_err_pfx}: 'training.proxy.backend' must be an int") + + def _validate_checkpointing_section(self, section: dict) -> None: + if "path" not in section: + raise KeyError(f"{self._validation_err_pfx}: missing field 'path' under section 'checkpointing'") + if not isinstance(section["path"], str): + raise TypeError(f"{self._validation_err_pfx}: 'training.checkpointing.path' must be a string") + + if "interval" in section: + if not isinstance(section["interval"], int): + raise TypeError( + f"{self._validation_err_pfx}: 'training.checkpointing.interval' must be an int" + ) + + def _validate_logging_section(self, component, level_dict: dict) -> None: + if any(key not in {"stdout", "file"} for key in level_dict): + raise KeyError( + f"{self._validation_err_pfx}: fields under section '{component}.logging' must be 'stdout' or 'file'" + ) + valid_log_levels = set(LEVEL_MAP.keys()) + for key, val in level_dict.items(): + if val not in valid_log_levels: + raise ValueError( + f"{self._validation_err_pfx}: '{component}.logging.{key}' must be one of {valid_log_levels}." + ) + + def get_path_mapping(self, containerize: bool = False) -> dict: + """Generate path mappings for a local or containerized environment. + + Args: + containerize (bool): If true, the paths you specify in the configuration file (which should always be local) + are mapped to paths inside the containers as follows: + local/scenario/path -> "/scenario" + local/load/path -> "loadpoint" + local/checkpoint/path -> "checkpoints" + local/log/path -> "/logs" + Defaults to False. + """ + log_dir = os.path.dirname(self._config["log_path"]) + path_map = { + self._config["scenario_path"]: "/scenario" if containerize else self._config["scenario_path"], + log_dir: f"/logs" if containerize else log_dir + } + + load_path = self._config["training"].get("load_path", None) + if load_path is not None: + path_map[load_path] = "/loadpoint" if containerize else load_path + if "checkpointing" in self._config["training"]: + ckpt_path = self._config["training"]["checkpointing"]["path"] + path_map[ckpt_path] = "/checkpoints" if containerize else ckpt_path + + return path_map + + def as_env(self, containerize: bool = False) -> dict: + """Generate environment variables for the workflow scripts. + + A doubly-nested dictionary is returned that contains the environment variables for each distributed component. + + Args: + containerize (bool): If true, the generated environment variables are to be used in a containerized + environment. Only path-related environment variables are affected by this flag. See the docstring + for ``get_path_mappings`` for details. Defaults to False. + """ + path_mapping = self.get_path_mapping(containerize=containerize) + scenario_path = path_mapping[self._config["scenario_path"]] + num_episodes = self._config["main"]["num_episodes"] + env = { + "main": { + "JOB": self._config["job"], + "NUM_EPISODES": str(num_episodes), + "TRAIN_MODE": self._config["training"]["mode"], + "SCENARIO_PATH": scenario_path, + } + } + + if "eval_schedule" in self._config["main"]: + # If it is an int, it is treated as the number of episodes between two adjacent evaluations. For example, + # if the total number of episodes is 20 and this is 5, an evaluation schedule of [5, 10, 15, 20] + # (start from 1) will be generated for the environment variable (as a string). If it is a list, the sorted + # version of the list will be generated for the environment variable (as a string). + sch = self._config["main"]["eval_schedule"] + if isinstance(sch, int): + env["main"]["EVAL_SCHEDULE"] = " ".join([str(sch * i) for i in range(1, num_episodes // sch + 1)]) + else: + env["main"]["EVAL_SCHEDULE"] = " ".join([str(val) for val in sorted(sch)]) + + load_path = self._config["training"].get("load_path", None) + if load_path is not None: + env["main"]["LOAD_PATH"] = path_mapping[load_path] + + if "checkpointing" in self._config["training"]: + conf = self._config["training"]["checkpointing"] + env["main"]["CHECKPOINT_PATH"] = path_mapping[conf["path"]] + if "interval" in conf: + env["main"]["CHECKPOINT_INTERVAL"] = str(conf["interval"]) + + num_steps = self._config["main"].get("num_steps", None) + if num_steps is not None: + env["main"]["NUM_STEPS"] = str(num_steps) + + if "logging" in self._config["main"]: + env["main"].update({ + "LOG_LEVEL_STDOUT": self.config["main"]["logging"]["stdout"], + "LOG_LEVEL_FILE": self.config["main"]["logging"]["file"], + }) + + if "parallelism" in self._config["rollout"]: + env_sampling_parallelism = self._config["rollout"]["parallelism"]["sampling"] + env_eval_parallelism = self._config["rollout"]["parallelism"].get("eval", 1) + else: + env_sampling_parallelism = env_eval_parallelism = 1 + rollout_parallelism = max(env_sampling_parallelism, env_eval_parallelism) + if rollout_parallelism > 1: + conf = self._config["rollout"]["parallelism"] + rollout_controller_port = str(conf["controller"]["port"]) + env["main"]["ENV_SAMPLE_PARALLELISM"] = str(env_sampling_parallelism) + env["main"]["ENV_EVAL_PARALLELISM"] = str(env_eval_parallelism) + env["main"]["ROLLOUT_CONTROLLER_PORT"] = rollout_controller_port + # optional settings for parallel rollout + if "min_env_samples" in self._config["rollout"]: + env["main"]["MIN_ENV_SAMPLES"] = str(conf["min_env_samples"]) + if "grace_factor" in self._config["rollout"]: + env["main"]["GRACE_FACTOR"] = str(conf["grace_factor"]) + + for i in range(rollout_parallelism): + worker_id = f"rollout_worker-{i}" + env[worker_id] = { + "ID": str(i), + "ROLLOUT_CONTROLLER_HOST": self._get_rollout_controller_host(containerize=containerize), + "ROLLOUT_CONTROLLER_PORT": rollout_controller_port, + "SCENARIO_PATH": scenario_path, + } + if "logging" in self._config["rollout"]: + env[worker_id].update({ + "LOG_LEVEL_STDOUT": self.config["rollout"]["logging"]["stdout"], + "LOG_LEVEL_FILE": self.config["rollout"]["logging"]["file"], + }) + + if self._config["training"]["mode"] == "parallel": + conf = self._config['training']['proxy'] + producer_host = self._get_train_proxy_host(containerize=containerize) + proxy_frontend_port = str(conf["frontend"]) + proxy_backend_port = str(conf["backend"]) + num_workers = self._config["training"]["num_workers"] + env["main"].update({ + "TRAIN_PROXY_HOST": producer_host, "TRAIN_PROXY_FRONTEND_PORT": proxy_frontend_port, + }) + env["train_proxy"] = { + "TRAIN_PROXY_FRONTEND_PORT": proxy_frontend_port, + "TRAIN_PROXY_BACKEND_PORT": proxy_backend_port, + } + for i in range(num_workers): + worker_id = f"train_worker-{i}" + env[worker_id] = { + "ID": str(i), + "TRAIN_PROXY_HOST": producer_host, + "TRAIN_PROXY_BACKEND_PORT": proxy_backend_port, + "SCENARIO_PATH": scenario_path, + } + if "logging" in self._config["training"]: + env[worker_id].update({ + "LOG_LEVEL_STDOUT": self.config["training"]["logging"]["stdout"], + "LOG_LEVEL_FILE": self.config["training"]["logging"]["file"], + }) + + # All components write logs to the same file + log_dir, log_file = os.path.split(self._config["log_path"]) + for vars in env.values(): + vars["LOG_PATH"] = os.path.join(path_mapping[log_dir], log_file) + + return env + + def _get_rollout_controller_host(self, containerize: bool = False) -> str: + if containerize: + return f"{self._config['job']}.main" + else: + return self._config["rollout"]["parallelism"]["controller"]["host"] + + def _get_train_proxy_host(self, containerize: bool = False) -> str: + return f"{self._config['job']}.train_proxy" if containerize else self._config["training"]["proxy"]["host"] diff --git a/maro/rl/workflows/config/template.yml b/maro/rl/workflows/config/template.yml new file mode 100644 index 000000000..3725da0af --- /dev/null +++ b/maro/rl/workflows/config/template.yml @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +# This is a configuration template for running reinforcement learning workflows with MARO's CLI tools. The workflows +# are scenario agnostic, meaning that this template can be applied to any scenario as long as the necessary components +# are provided (see examples/rl/README.md for details about these components). Your scenario should be placed in a +# folder and its path should be specified in the "scenario_path" field. Note that all fields with a "null" value are +# optional and will be converted to None by the parser unless a non-null value is specified. Note that commenting them +# out or leaving them blank are equivalent to using "null". + + +job: your_job_name +# Path to a directory that defines a business scenario and contains the necessary components to execute reinforcement +# learning workflows in single-threaded, multi-process and distributed modes. +scenario_path: "/path/to/your/scenario" +log_path: "/path/to/your/log/folder" # All logs are written to a single file for ease of viewing. +main: + num_episodes: 100 # Number of episodes to run. Each episode is one cycle of roll-out and training. + # Number of environment steps to collect environment samples over. If null, samples are collected until the + # environments reach the terminal state, i.e., for a full episode. Otherwise, samples are collected until the + # specified number of steps or the terminal state is reached, whichever comes first. + num_steps: null + # This can be an integer or a list of integers. An integer indicates the interval at which policies are evaluated. + # A list indicates the episodes at the end of which policies are to be evaluated. Note that episode indexes are + # 1-based. + eval_schedule: 10 + logging: # log levels for the main loop + stdout: INFO # DEBUG, INFO, WARN, ERROR, CRITICAL, PROGRESS + file: DEBUG # DEBUG, INFO, WARN, ERROR, CRITICAL, PROGRESS +rollout: + # Optional section to specify roll-out parallelism settings. If absent, a single environment instance will be created + # locally for training and evaluation. + parallelism: + sampling: 10 # Number of parallel roll-outs to collecting training data from. + # Number of parallel roll-outs to evaluate policies on. If not specified, one roll-out worker is chosen to perform + # evaluation. + eval: null + # Minimum number of environment samples to collect from the parallel roll-outs per episode / segment before moving + # on to the training phase. The actual number of env samples collected may be more than this value if we allow a + # grace period (see the comment for rollout.parallelism.grace_factor for details), but never less. This value should + # not exceed rollout.parallelism.sampling. + min_env_samples: 8 + # Factor that determines the additional wait time after the required number of environment samples as indicated by + # "min_env_samples" are received. For example, if T seconds elapsed after receiving "min_env_samples" environment + # samples, it will wait an additional T * grace_factor seconds to try to collect the remaining results. + grace_factor: 0.2 + controller: # Parallel roll-out controller settings. Ignored if rollout.parallelism section is absent. + host: "127.0.0.1" # Controller's IP address. Ignored if run in containerized environments. + port: 20000 # Controller's network port for remote roll-out workers to connect to. + logging: # log levels for roll-out workers + stdout: INFO # DEBUG, INFO, WARN, ERROR, CRITICAL, PROGRESS + file: DEBUG # DEBUG, INFO, WARN, ERROR, CRITICAL, PROGRESS +training: + # Must be "simple" or "parallel". In simple mode, all underlying models are trained locally. In parallel mode, + # all trainers send gradient-related tasks to a proxy service where they get dispatched to a set of workers. + mode: simple + # Path to load previously saved trainer snapshots from. A policy trainer's snapshot includes the states of all + # the policies it manages as well as the states of auxillary models (e.g., critics in the Actor-Critic paradigm). + # If the path corresponds to an existing directory, the program will look under the directory for snapshot files + # that match the trainer names specified in the scenario and attempt to load from them. + load_path: "/path/to/your/models" + # Optional section to specify model checkpointing settings. + checkpointing: + # Directory to save trainer snapshots under. Snapshot files created at different episodes will be saved under + # separate folders named using episode numbers. For example, if a snapshot is created for a trainer named "dqn" + # at the end of episode 10, the file path would be "/path/to/your/checkpoint/folder/10/dqn.ckpt". + path: "/path/to/your/checkpoint/folder" + interval: 10 # Interval at which trained policies / models are persisted to disk. + proxy: # Proxy settings. Ignored if training.mode is "simple". + host: "127.0.0.1" # Proxy service host's IP address. Ignored if run in containerized environments. + frontend: 10000 # Proxy service's network port for trainers to send tasks to. + backend: 10001 # Proxy service's network port for remote workers to connect to. + num_workers: 10 # Number of workers to execute trainers' tasks. + logging: # log levels for training task workers + stdout: INFO # DEBUG, INFO, WARN, ERROR, CRITICAL, PROGRESS + file: DEBUG # DEBUG, INFO, WARN, ERROR, CRITICAL, PROGRESS diff --git a/maro/rl/workflows/grad_worker.py b/maro/rl/workflows/grad_worker.py deleted file mode 100644 index dc17ecb9b..000000000 --- a/maro/rl/workflows/grad_worker.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import os -import sys -import time - -from maro.communication import Proxy -from maro.rl.utils import MsgKey, MsgTag -from maro.rl.workflows.helpers import from_env, get_default_log_dir -from maro.utils import Logger - -sys.path.insert(0, from_env("SCENARIODIR")) -module = importlib.import_module(from_env("SCENARIO")) -policy_func_dict = getattr(module, "policy_func_dict") -log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) -os.makedirs(log_dir, exist_ok=True) - - -if __name__ == "__main__": - # TODO: WORKERID in docker compose script. - worker_id = from_env("WORKERID") - num_hosts = from_env("NUMHOSTS") if from_env("POLICYMANAGERTYPE") == "distributed" else 0 - max_cached_policies = from_env("MAXCACHED", required=False, default=10) - - group = from_env("POLICYGROUP") - policy_dict = {} - active_policies = [] - if num_hosts == 0: - # no remote nodes for policy hosts - num_hosts = len(policy_func_dict) - - peers = {"policy_manager": 1, "policy_host": num_hosts} - proxy = Proxy( - group, "grad_worker", peers, component_name=f"GRAD_WORKER.{worker_id}", - redis_address=(from_env("REDISHOST"), from_env("REDISPORT")), - max_peer_discovery_retries=50 - ) - logger = Logger(proxy.name, dump_folder=log_dir) - - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - elif msg.tag == MsgTag.COMPUTE_GRAD: - t0 = time.time() - msg_body = {MsgKey.LOSS_INFO: dict(), MsgKey.POLICY_IDS: list()} - for name, batch in msg.body[MsgKey.GRAD_TASK].items(): - if name not in policy_dict: - if len(policy_dict) > max_cached_policies: - # remove the oldest one when size exceeds. - policy_to_remove = active_policies.pop() - policy_dict.pop(policy_to_remove) - policy_dict[name] = policy_func_dict[name](name) - active_policies.insert(0, name) - logger.info(f"Initialized policies {name}") - - policy_dict[name].set_state(msg.body[MsgKey.POLICY_STATE][name]) - loss_info = policy_dict[name].get_batch_loss(batch, explicit_grad=True) - msg_body[MsgKey.LOSS_INFO][name] = loss_info - msg_body[MsgKey.POLICY_IDS].append(name) - # put the latest one to queue head - active_policies.remove(name) - active_policies.insert(0, name) - - logger.debug(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, tag=MsgTag.COMPUTE_GRAD_DONE, body=msg_body) - else: - logger.info(f"Wrong message tag: {msg.tag}") - raise TypeError diff --git a/maro/rl/workflows/helpers.py b/maro/rl/workflows/helpers.py deleted file mode 100644 index ce10c9ac5..000000000 --- a/maro/rl/workflows/helpers.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os -from typing import List, Union - - -def from_env(var_name, required=True, default=None): - if var_name not in os.environ: - if required: - raise KeyError(f"Missing environment variable: {var_name}") - else: - return default - - var = os.getenv(var_name) - return int(var) if var.isnumeric() or var[0] == "-" and var[1:].isnumeric() else var - - -def get_default_log_dir(job): - return os.path.join(os.getcwd(), "logs", job) - - -def get_eval_schedule(sch: Union[int, List[int]], num_episodes: int): - """Helper function to the policy evaluation schedule. - - Args: - sch (Union[int, List[int]]): Evaluation schedule. If it is an int, it is treated as the number of episodes - between two adjacent evaluations. For example, if the total number of episodes is 20 and ``sch`` is 6, - this will return [6, 12, 18] if ``final`` is False or [6, 12, 18, 20] otherwise. If it is a list, it will - return a sorted version of the list (with the last episode appended if ``final`` is True). - num_episodes (int): Total number of learning episodes. - - Returns: - A list of episodes indicating when to perform policy evaluation. - - """ - if sch is None: - schedule = [] - elif isinstance(sch, int): - num_eval_schedule = num_episodes // sch - schedule = [sch * i for i in range(1, num_eval_schedule + 1)] - else: - schedule = sorted(sch) - - return schedule diff --git a/maro/rl/workflows/main.py b/maro/rl/workflows/main.py index bf527cf82..bed4739b4 100644 --- a/maro/rl/workflows/main.py +++ b/maro/rl/workflows/main.py @@ -1,110 +1,123 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import importlib import os -import sys import time +from typing import List + +from maro.rl.rollout import BatchEnvSampler, ExpElement +from maro.rl.training import TrainerManager +from maro.rl.utils.common import float_or_none, get_env, int_or_none, list_or_none +from maro.rl.workflows.scenario import Scenario +from maro.utils import LoggerV2 + + +def main(scenario: Scenario) -> None: + num_episodes = int(get_env("NUM_EPISODES")) + num_steps = int_or_none(get_env("NUM_STEPS", required=False)) + + logger = LoggerV2( + "MAIN", + dump_path=get_env("LOG_PATH"), + dump_mode="a", + stdout_level=get_env("LOG_LEVEL_STDOUT", required=False, default="CRITICAL"), + file_level=get_env("LOG_LEVEL_FILE", required=False, default="CRITICAL"), + ) + + load_path = get_env("LOAD_PATH", required=False) + checkpoint_path = get_env("CHECKPOINT_PATH", required=False) + checkpoint_interval = int_or_none(get_env("CHECKPOINT_INTERVAL", required=False)) + + env_sampling_parallelism = int_or_none(get_env("ENV_SAMPLE_PARALLELISM", required=False)) + env_eval_parallelism = int_or_none(get_env("ENV_EVAL_PARALLELISM", required=False)) + parallel_rollout = env_sampling_parallelism is not None or env_eval_parallelism is not None + train_mode = get_env("TRAIN_MODE") + + agent2policy = scenario.agent2policy + policy_creator = scenario.policy_creator + trainer_creator = scenario.trainer_creator + is_single_thread = train_mode == "simple" and not parallel_rollout + if is_single_thread: + # If running in single thread mode, create policy instances here and reuse then in rollout and training. + policy_dict = {name: get_policy_func(name) for name, get_policy_func in policy_creator.items()} + policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict} + + if parallel_rollout: + env_sampler = BatchEnvSampler( + sampling_parallelism=env_sampling_parallelism, + port=int(get_env("ROLLOUT_CONTROLLER_PORT")), + min_env_samples=int_or_none(get_env("MIN_ENV_SAMPLES", required=False)), + grace_factor=float_or_none(get_env("GRACE_FACTOR", required=False)), + eval_parallelism=env_eval_parallelism, + logger=logger, + ) + else: + env_sampler = scenario.get_env_sampler(policy_creator) -from maro.rl.learning.helpers import get_rollout_finish_msg -from maro.rl.workflows.helpers import from_env, get_default_log_dir, get_eval_schedule -from maro.utils import Logger - -sys.path.insert(0, from_env("SCENARIODIR")) -module = importlib.import_module(from_env("SCENARIO")) -get_env_sampler = getattr(module, "get_env_sampler") -post_collect = getattr(module, "post_collect", None) -post_evaluate = getattr(module, "post_evaluate", None) + # evaluation schedule + eval_schedule = list_or_none(get_env("EVAL_SCHEDULE", required=False)) + logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") + eval_point_index = 0 -checkpoint_dir = from_env("CHECKPOINTDIR", required=False, default=None) -if checkpoint_dir: - os.makedirs(checkpoint_dir, exist_ok=True) -load_policy_dir = from_env("LOADDIR", required=False, default=None) -log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) -os.makedirs(log_dir, exist_ok=True) + trainer_manager = TrainerManager( + policy_creator, trainer_creator, agent2policy, + proxy_address=None if train_mode == "simple" else ( + get_env("TRAIN_PROXY_HOST"), int(get_env("TRAIN_PROXY_FRONTEND_PORT")) + ), + logger=logger, + ) + if load_path: + assert isinstance(load_path, str) + loaded = trainer_manager.load(load_path) + logger.info(f"Loaded states for {loaded} from {load_path}") + + # main loop + for ep in range(1, num_episodes + 1): + collect_time = training_time = 0 + segment, end_of_episode = 1, False + while not end_of_episode: + # Experience collection + tc0 = time.time() + result = env_sampler.sample( + policy_state=trainer_manager.get_policy_state() if not is_single_thread else None, + num_steps=num_steps, + ) + experiences: List[List[ExpElement]] = result["experiences"] + end_of_episode: bool = result["end_of_episode"] + + if scenario.post_collect: + scenario.post_collect(result["info"], ep, segment) + + collect_time += time.time() - tc0 + + logger.info(f"Roll-out completed for episode {ep}, segment {segment}. Training started...") + tu0 = time.time() + trainer_manager.record_experiences(experiences) + trainer_manager.train() + if checkpoint_path and (checkpoint_interval is None or ep % checkpoint_interval == 0): + assert isinstance(checkpoint_path, str) + pth = os.path.join(checkpoint_path, str(ep)) + trainer_manager.save(pth) + logger.info(f"All trainer states saved under {pth}") + training_time += time.time() - tu0 + segment += 1 + + # performance details + logger.info(f"ep {ep} - roll-out time: {collect_time}, training time: {training_time}") + if eval_schedule and ep == eval_schedule[eval_point_index]: + eval_point_index += 1 + result = env_sampler.eval( + policy_state=trainer_manager.get_policy_state() if not is_single_thread else None + ) + if scenario.post_evaluate: + scenario.post_evaluate(result["info"], ep) + + if isinstance(env_sampler, BatchEnvSampler): + env_sampler.exit() + trainer_manager.exit() if __name__ == "__main__": - mode = from_env("MODE") - num_episodes = from_env("NUMEPISODES") - num_steps = from_env("NUMSTEPS", required=False, default=-1) - - logger = Logger("MAIN", dump_folder=log_dir) - # evaluation schedule - eval_schedule = get_eval_schedule(from_env("EVALSCH", required=False, default=None), num_episodes) - logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") - eval_point_index = 0 - if mode == "single": - env_sampler = get_env_sampler() - if load_policy_dir: - env_sampler.agent_wrapper.load(load_policy_dir) - logger.info(f"Loaded policy states from {load_policy_dir}") - - for ep in range(1, num_episodes + 1): - collect_time = policy_update_time = 0 - segment, end_of_episode = 1, False - while not end_of_episode: - # experience collection - tc0 = time.time() - result = env_sampler.sample(num_steps=num_steps, return_rollout_info=False) - trackers = [result["tracker"]] - logger.info( - get_rollout_finish_msg(ep, result["step_range"], exploration_params=result["exploration_params"]) - ) - end_of_episode = result["end_of_episode"] - - if post_collect: - post_collect(trackers, ep, segment) - - collect_time += time.time() - tc0 - tu0 = time.time() - env_sampler.agent_wrapper.improve(checkpoint_dir=checkpoint_dir) - if checkpoint_dir: - logger.info(f"Saved policy states to {checkpoint_dir}") - policy_update_time += time.time() - tu0 - segment += 1 - - # performance details - logger.info(f"ep {ep} summary - collect time: {collect_time}, policy update time: {policy_update_time}") - if eval_schedule and ep == eval_schedule[eval_point_index]: - eval_point_index += 1 - trackers = [env_sampler.test()] - if post_evaluate: - post_evaluate(trackers, ep) - else: - from policy_manager import get_policy_manager - from rollout_manager import get_rollout_manager - - rollout_manager = get_rollout_manager() - policy_manager = get_policy_manager() - for ep in range(1, num_episodes + 1): - collect_time = policy_update_time = 0 - rollout_manager.reset() - segment, end_of_episode = 1, False - while not end_of_episode: - # experience collection - tc0 = time.time() - policy_state_dict = policy_manager.get_state() - rollout_info_by_policy, trackers = rollout_manager.collect(ep, segment, policy_state_dict) - end_of_episode = rollout_manager.end_of_episode - - if post_collect: - post_collect(trackers, ep, segment) - - collect_time += time.time() - tc0 - tu0 = time.time() - policy_manager.update(rollout_info_by_policy) - policy_update_time += time.time() - tu0 - segment += 1 - - # performance details - logger.info(f"ep {ep} summary - collect time: {collect_time}, policy update time: {policy_update_time}") - if eval_schedule and ep == eval_schedule[eval_point_index]: - eval_point_index += 1 - trackers = rollout_manager.evaluate(ep, policy_manager.get_state()) - if post_evaluate: - post_evaluate(trackers, ep) - - rollout_manager.exit() - if hasattr(policy_manager, "exit"): - policy_manager.exit() + # get user-defined scenario ingredients + scenario = Scenario(str(get_env("SCENARIO_PATH"))) + main(scenario) diff --git a/maro/rl/workflows/policy_host.py b/maro/rl/workflows/policy_host.py deleted file mode 100644 index c9a100a03..000000000 --- a/maro/rl/workflows/policy_host.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import os -import sys -import time - -from maro.communication import Proxy -from maro.rl.utils import MsgKey, MsgTag -from maro.rl.workflows.helpers import from_env, get_default_log_dir -from maro.utils import Logger - -sys.path.insert(0, from_env("SCENARIODIR")) -module = importlib.import_module(from_env("SCENARIO")) -policy_func_dict = getattr(module, "policy_func_dict") - -checkpoint_dir = from_env("CHECKPOINTDIR", required=False, default=None) -if checkpoint_dir: - os.makedirs(checkpoint_dir, exist_ok=True) -load_policy_dir = from_env("LOADDIR", required=False, default=None) -log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) -os.makedirs(log_dir, exist_ok=True) - - -if __name__ == "__main__": - host_id = from_env("HOSTID") - peers = {"policy_manager": 1} - data_parallel = from_env("DATAPARALLEL", required=False, default=False) - if data_parallel: - num_grad_workers = from_env("NUMGRADWORKERS") - peers["grad_worker"] = num_grad_workers - - if host_id is None: - raise ValueError("missing environment variable: HOSTID") - - group = from_env("POLICYGROUP") - policy_dict, checkpoint_path = {}, {} - - proxy = Proxy( - group, "policy_host", peers, - component_name=f"POLICY_HOST.{host_id}", - redis_address=(from_env("REDISHOST"), from_env("REDISPORT")), - max_peer_discovery_retries=50 - ) - logger = Logger(proxy.name, dump_folder=log_dir) - - for msg in proxy.receive(): - if msg.tag == MsgTag.EXIT: - logger.info("Exiting...") - proxy.close() - break - elif msg.tag == MsgTag.INIT_POLICIES: - for id_ in msg.body[MsgKey.POLICY_IDS]: - policy_dict[id_] = policy_func_dict[id_](id_) - checkpoint_path[id_] = os.path.join(checkpoint_dir, id_) if checkpoint_dir else None - if load_policy_dir: - path = os.path.join(load_policy_dir, id_) - if os.path.exists(path): - policy_dict[id_].load(path) - logger.info(f"Loaded policy {id_} from {path}") - if data_parallel: - policy_dict[id_].data_parallel_with_existing_proxy(proxy) - - logger.info(f"Initialized policies {msg.body[MsgKey.POLICY_IDS]}") - proxy.reply( - msg, - tag=MsgTag.INIT_POLICIES_DONE, - body={MsgKey.POLICY_STATE: {id_: policy.get_state() for id_, policy in policy_dict.items()}} - ) - elif msg.tag == MsgTag.LEARN: - t0 = time.time() - for id_, info in msg.body[MsgKey.ROLLOUT_INFO].items(): - # in some cases e.g. Actor-Critic that get loss from rollout workers - if isinstance(info, list): - logger.info("updating with loss info") - policy_dict[id_].update(info) - else: - if data_parallel: - logger.info("learning on remote grad workers") - policy2workers = msg.body[MsgKey.WORKER_INFO][id_] - policy_dict[id_].learn_with_data_parallel(info, policy2workers[id_]) - else: - logger.info("learning from batch") - policy_dict[id_].learn(info) - - if checkpoint_path[id_]: - policy_dict[id_].save(checkpoint_path[id_]) - logger.info(f"Saved policy {id_} to {checkpoint_path[id_]}") - - msg_body = { - MsgKey.POLICY_STATE: {name: policy_dict[name].get_state() for name in msg.body[MsgKey.ROLLOUT_INFO]} - } - logger.info(f"total policy update time: {time.time() - t0}") - proxy.reply(msg, tag=MsgTag.LEARN_DONE, body=msg_body) - else: - logger.info(f"Wrong message tag: {msg.tag}") - raise TypeError diff --git a/maro/rl/workflows/policy_manager.py b/maro/rl/workflows/policy_manager.py deleted file mode 100644 index 646eb9063..000000000 --- a/maro/rl/workflows/policy_manager.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import os -import sys - -from maro.rl.learning import DistributedPolicyManager, MultiProcessPolicyManager, SimplePolicyManager -from maro.rl.policy import WorkerAllocator -from maro.rl.workflows.helpers import from_env, get_default_log_dir - -sys.path.insert(0, from_env("SCENARIODIR")) -module = importlib.import_module(from_env("SCENARIO")) -policy_func_dict = getattr(module, "policy_func_dict") -agent2policy = getattr(module, "agent2policy") - -checkpoint_dir = from_env("CHECKPOINTDIR", required=False, default=None) -if checkpoint_dir: - os.makedirs(checkpoint_dir, exist_ok=True) -load_policy_dir = from_env("LOADDIR", required=False, default=None) -log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) -os.makedirs(log_dir, exist_ok=True) - - -def get_policy_manager(): - manager_type = from_env("POLICYMANAGERTYPE") - data_parallel = from_env("DATAPARALLEL", required=False, default=False) - if data_parallel: - allocator = WorkerAllocator( - from_env("ALLOCATIONMODE"), from_env("NUMGRADWORKERS"), list(policy_func_dict.keys()), agent2policy - ) - group = from_env("POLICYGROUP") - else: - allocator, group = None, None - proxy_kwargs = { - "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), - "max_peer_discovery_retries": 50 - } - if manager_type == "simple": - return SimplePolicyManager( - policy_func_dict, - load_dir=load_policy_dir, - checkpoint_dir=checkpoint_dir, - worker_allocator=allocator, - group=group, - proxy_kwargs=proxy_kwargs, - log_dir=log_dir - ) - elif manager_type == "multi-process": - return MultiProcessPolicyManager( - policy_func_dict, - load_dir=load_policy_dir, - checkpoint_dir=checkpoint_dir, - worker_allocator=allocator, - group=group, - proxy_kwargs=proxy_kwargs, - log_dir=log_dir - ) - elif manager_type == "distributed": - return DistributedPolicyManager( - list(policy_func_dict.keys()), from_env("NUMHOSTS"), - group=from_env("POLICYGROUP"), - worker_allocator=allocator, - proxy_kwargs=proxy_kwargs, - log_dir=log_dir - ) - - raise ValueError( - f"Unsupported policy manager type: {manager_type}. Supported modes: simple, multi-process, distributed" - ) - - -if __name__ == "__main__": - policy_manager = get_policy_manager() - policy_manager.server( - from_env("GROUP"), - from_env("NUMROLLOUTS"), - max_lag=from_env("MAXLAG", required=False, default=0), - proxy_kwargs={ - "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir - ) diff --git a/maro/rl/workflows/rollout.py b/maro/rl/workflows/rollout.py deleted file mode 100644 index 8305ff5f7..000000000 --- a/maro/rl/workflows/rollout.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import os -import sys - -from maro.rl.workflows.helpers import from_env, get_default_log_dir - -sys.path.insert(0, from_env("SCENARIODIR")) -module = importlib.import_module(from_env("SCENARIO")) -get_env_sampler = getattr(module, "get_env_sampler") - -log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) -os.makedirs(log_dir, exist_ok=True) - - -if __name__ == "__main__": - mode = from_env("MODE") - env_sampler = get_env_sampler() - if mode == "sync": - env_sampler.worker( - from_env("ROLLOUTGROUP"), from_env("WORKERID"), - proxy_kwargs={ - "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir - ) - elif mode == "async": - num_episodes = from_env("NUMEPISODES") - num_steps = from_env("NUMSTEPS", required=False, default=-1) - env_sampler.actor( - from_env("GROUP"), from_env("ACTORID"), num_episodes, - num_steps=num_steps, - proxy_kwargs={ - "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), - "max_peer_discovery_retries": 50 - }, - log_dir=log_dir, - ) - else: - raise ValueError(f"MODE environment variable must be 'sync' or 'async', got {mode}") diff --git a/maro/rl/workflows/rollout_manager.py b/maro/rl/workflows/rollout_manager.py deleted file mode 100644 index 87f027546..000000000 --- a/maro/rl/workflows/rollout_manager.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import os -import sys - -from maro.rl.learning import DistributedRolloutManager, MultiProcessRolloutManager -from maro.rl.workflows.helpers import from_env, get_default_log_dir - -sys.path.insert(0, from_env("SCENARIODIR")) -module = importlib.import_module(from_env("SCENARIO")) -get_env_sampler = getattr(module, "get_env_sampler") -post_collect = getattr(module, "post_collect", None) -post_evaluate = getattr(module, "post_evaluate", None) - -log_dir = from_env("LOGDIR", required=False, default=get_default_log_dir(from_env("JOB"))) -os.makedirs(log_dir, exist_ok=True) - - -def get_rollout_manager(): - rollout_type = from_env("ROLLOUTTYPE") - num_steps = from_env("NUMSTEPS", required=False, default=-1) - if rollout_type == "multi-process": - return MultiProcessRolloutManager( - get_env_sampler, - num_steps=num_steps, - num_rollouts=from_env("NUMROLLOUTS"), - num_eval_rollouts=from_env("NUMEVALROLLOUTS", required=False, default=1), - log_dir=log_dir - ) - - if rollout_type == "distributed": - num_workers = from_env("NUMROLLOUTS") - num_eval_workers = from_env("NUMEVALROLLOUTS", required=False, default=1) - min_finished_workers = from_env("MINFINISH", required=False, default=None) - max_extra_recv_tries = from_env("MAXEXRECV", required=False, default=0) - extra_recv_timeout = from_env("MAXRECVTIMEO", required=False, default=100) - - return DistributedRolloutManager( - from_env("ROLLOUTGROUP"), - num_workers, - num_eval_workers=num_eval_workers, - num_steps=num_steps, - min_finished_workers=min_finished_workers, - max_extra_recv_tries=max_extra_recv_tries, - extra_recv_timeout=extra_recv_timeout, - proxy_kwargs={ - "redis_address": (from_env("REDISHOST"), from_env("REDISPORT")), - "max_peer_discovery_retries": 50 - }, - ) - - raise ValueError(f"Unsupported roll-out type: {rollout_type}. Supported: multi-process, distributed") diff --git a/maro/rl/workflows/rollout_worker.py b/maro/rl/workflows/rollout_worker.py new file mode 100644 index 000000000..99b7e29cc --- /dev/null +++ b/maro/rl/workflows/rollout_worker.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.rl.rollout import RolloutWorker +from maro.rl.utils.common import get_env, int_or_none +from maro.rl.workflows.scenario import Scenario +from maro.utils import LoggerV2 + +if __name__ == "__main__": + scenario_attr = Scenario(get_env("SCENARIO_PATH")) + policy_creator = scenario_attr.policy_creator + + worker_idx = int_or_none(get_env("ID")) + logger = LoggerV2( + f"ROLLOUT-WORKER.{worker_idx}", + dump_path=get_env("LOG_PATH"), + dump_mode="a", + stdout_level=get_env("LOG_LEVEL_STDOUT", required=False, default="CRITICAL"), + file_level=get_env("LOG_LEVEL_FILE", required=False, default="CRITICAL"), + ) + worker = RolloutWorker( + idx=worker_idx, + env_sampler_creator=lambda: scenario_attr.get_env_sampler(policy_creator), + producer_host=get_env("ROLLOUT_CONTROLLER_HOST"), + producer_port=int_or_none(get_env("ROLLOUT_CONTROLLER_PORT")), + logger=logger, + ) + worker.start() diff --git a/maro/rl/workflows/scenario.py b/maro/rl/workflows/scenario.py new file mode 100644 index 000000000..6819f34dd --- /dev/null +++ b/maro/rl/workflows/scenario.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import importlib +import os +import sys +from typing import Callable, Dict + +from maro.rl.policy import RLPolicy +from maro.rl.rollout import AbsEnvSampler +from maro.rl.training import AbsTrainer + + +class Scenario(object): + def __init__(self, path: str) -> None: + super(Scenario, self).__init__() + path = os.path.normpath(path) + sys.path.insert(0, os.path.dirname(path)) + self._module = importlib.import_module(os.path.basename(path)) + + def get_env_sampler(self, policy_creator: Dict[str, Callable[[str], RLPolicy]]) -> AbsEnvSampler: + return getattr(self._module, "env_sampler_creator")(policy_creator) + + @property + def agent2policy(self) -> Dict[str, str]: + return getattr(self._module, "agent2policy") + + @property + def policy_creator(self) -> Dict[str, Callable[[str], RLPolicy]]: + return getattr(self._module, "policy_creator") + + @property + def trainer_creator(self) -> Dict[str, Callable[[str], AbsTrainer]]: + return getattr(self._module, "trainer_creator") + + @property + def post_collect(self) -> Callable[[list, int, int], None]: + return getattr(self._module, "post_collect", None) + + @property + def post_evaluate(self) -> Callable[[list, int], None]: + return getattr(self._module, "post_evaluate", None) diff --git a/maro/rl/workflows/train_proxy.py b/maro/rl/workflows/train_proxy.py new file mode 100644 index 000000000..004066d50 --- /dev/null +++ b/maro/rl/workflows/train_proxy.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.rl.training import TrainingProxy +from maro.rl.utils.common import get_env, int_or_none + +if __name__ == "__main__": + proxy = TrainingProxy( + frontend_port=int_or_none(get_env("TRAIN_PROXY_FRONTEND_PORT")), + backend_port=int_or_none(get_env("TRAIN_PROXY_BACKEND_PORT")), + ) + proxy.start() diff --git a/maro/rl/workflows/train_worker.py b/maro/rl/workflows/train_worker.py new file mode 100644 index 000000000..15e271541 --- /dev/null +++ b/maro/rl/workflows/train_worker.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from maro.rl.training import TrainOpsWorker +from maro.rl.utils.common import get_env, int_or_none +from maro.rl.workflows.scenario import Scenario +from maro.utils import LoggerV2 + +if __name__ == "__main__": + scenario_attr = Scenario(get_env("SCENARIO_PATH")) + worker_idx = int_or_none(get_env("ID")) + logger = LoggerV2( + f"TRAIN-WORKER.{worker_idx}", + dump_path=get_env("LOG_PATH"), + dump_mode="a", + stdout_level=get_env("LOG_LEVEL_STDOUT", required=False, default="CRITICAL"), + file_level=get_env("LOG_LEVEL_FILE", required=False, default="CRITICAL"), + ) + worker = TrainOpsWorker( + idx=int_or_none(get_env("ID")), + policy_creator=scenario_attr.policy_creator, + trainer_creator=scenario_attr.trainer_creator, + producer_host=get_env("TRAIN_PROXY_HOST"), + producer_port=int_or_none(get_env("TRAIN_PROXY_BACKEND_PORT")), + logger=logger, + ) + worker.start() diff --git a/maro/simulator/scenarios/vm_scheduling/business_engine.py b/maro/simulator/scenarios/vm_scheduling/business_engine.py index 86a535bb4..2e0dc3810 100644 --- a/maro/simulator/scenarios/vm_scheduling/business_engine.py +++ b/maro/simulator/scenarios/vm_scheduling/business_engine.py @@ -106,6 +106,10 @@ def snapshots(self) -> SnapshotList: """SnapshotList: Current snapshot list.""" return self._snapshots + @property + def pm_amount(self) -> int: + return self._pm_amount + def _load_configs(self): """Load configurations.""" # Update self._config_path with current file path. diff --git a/maro/utils/__init__.py b/maro/utils/__init__.py index 327249d4a..0ec34785e 100644 --- a/maro/utils/__init__.py +++ b/maro/utils/__init__.py @@ -1,9 +1,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .logger import DummyLogger, InternalLogger, LogFormat, Logger +from .logger import DummyLogger, LogFormat, Logger, LoggerV2 from .utils import DottableDict, clone, convert_dottable, set_seeds __all__ = [ - "Logger", "InternalLogger", "DummyLogger", "LogFormat", "convert_dottable", "DottableDict", "clone", "set_seeds" + "Logger", "LoggerV2", "DummyLogger", "LogFormat", "convert_dottable", "DottableDict", "clone", "set_seeds" ] diff --git a/maro/utils/logger.py b/maro/utils/logger.py index b568c6731..32b3c37e9 100644 --- a/maro/utils/logger.py +++ b/maro/utils/logger.py @@ -61,7 +61,7 @@ class LogFormat(Enum): PROGRESS = 60 logging.addLevelName(PROGRESS, "PROGRESS") -level_map = { +LEVEL_MAP = { "DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARN": logging.WARN, @@ -337,3 +337,78 @@ def error_red(self, message: str) -> None: """ self.passive_init() self._logger.error('\033[31m' + message + '\033[0m') + + +class LoggerV2(object): + """A simple wrapper for logging. + + The Logger hosts a file handler and a stdout handler. The file handler is set + to ``DEBUG`` level and will dump all logs info to the given ``dump_path``. + Supported log levels include: ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, ``CRITICAL``, ``PROCESS``. + + Args: + tag (str): Log tag for stream and file output. + format_ (LogFormat): Predefined formatter. Defaults to ``LogFormat.full``. + dump_path (str): Path of file for dumping logs. Must be an absolute path. The log level for dumping is + ``logging.DEBUG``. Defaults to None, in which case logs generated by the logger will not be dumped + to a file. + dump_mode (str): Write log file mode. Defaults to ``w``. Use ``a`` to append log. + stdout_level (str): the logging level of the stdout handler. Defaults to ``INFO``. + file_level (str): the logging level of the file handler. Defaults to ``DEBUG``. + """ + + def __init__( + self, tag: str, format_: LogFormat = LogFormat.simple, dump_path: str = None, dump_mode: str = 'w', + stdout_level="INFO", file_level="DEBUG" + ): + self._file_format = FORMAT_NAME_TO_FILE_FORMAT[format_] + self._stdout_format = FORMAT_NAME_TO_STDOUT_FORMAT[format_] \ + if format_ in FORMAT_NAME_TO_STDOUT_FORMAT else \ + FORMAT_NAME_TO_FILE_FORMAT[format_] + self._stdout_level = LEVEL_MAP[stdout_level] if isinstance(stdout_level, str) else stdout_level + self._file_level = LEVEL_MAP[file_level] if isinstance(file_level, str) else file_level + self._logger = logging.getLogger(tag) + self._logger.setLevel(logging.DEBUG) + + if dump_path: + os.makedirs(os.path.dirname(dump_path), exist_ok=True) + # File handler + fh = logging.FileHandler(filename=dump_path, mode=dump_mode, encoding="utf-8") + fh.setLevel(self._file_level) + if self._file_format is not None: + fh.setFormatter(self._file_format) + self._logger.addHandler(fh) + + # Stdout handler + sh = logging.StreamHandler(sys.stdout) + sh.setLevel(self._stdout_level) + if self._stdout_format is not None: + sh.setFormatter(self._stdout_format) + self._logger.addHandler(sh) + + self._extra = {'host': socket.gethostname(), 'user': getpass.getuser(), 'tag': tag} + + @msgformat + def debug(self, msg, *args): + """Add a log with ``DEBUG`` level.""" + self._logger.debug(msg, *args, extra=self._extra) + + @msgformat + def info(self, msg, *args): + """Add a log with ``INFO`` level.""" + self._logger.info(msg, *args, extra=self._extra) + + @msgformat + def warn(self, msg, *args): + """Add a log with ``WARN`` level.""" + self._logger.warning(msg, *args, extra=self._extra) + + @msgformat + def error(self, msg, *args): + """Add a log with ``ERROR`` level.""" + self._logger.error(msg, *args, extra=self._extra) + + @msgformat + def critical(self, msg, *args): + """Add a log with ``CRITICAL`` level.""" + self._logger.critical(msg, *args, extra=self._extra) diff --git a/maro/utils/utils.py b/maro/utils/utils.py index dd83f5f7b..4131488ba 100644 --- a/maro/utils/utils.py +++ b/maro/utils/utils.py @@ -78,7 +78,8 @@ def set_seeds(seed): version_file_path = os.path.join(os.path.expanduser("~/.maro"), "version.ini") -project_root = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..") +LOCAL_MARO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +project_root = os.path.join(LOCAL_MARO_ROOT, "maro") target_source_pairs = [ ( diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 8743f50a8..96ab9a88b 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -55,34 +55,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", - "from maro.rl.learning import AbsEnvSampler\n", + "from maro.rl.rollout import AbsEnvSampler\n", "from maro.simulator.scenarios.cim.common import Action, ActionType\n", "\n", "\n", "class CIMEnvSampler(AbsEnvSampler):\n", - " def get_state(self, tick=None):\n", + " def _get_global_and_agent_state(self, event, tick=None):\n", " \"\"\"\n", " The state vector includes shortage and remaining vessel space over the past k days (where k is the \"look_back\"\n", " value in \"state_shaping_conf\" from the cell above), as well as all downstream port features.\n", " \"\"\"\n", - " if tick is None:\n", - " tick = self.env.tick\n", - " vessel_snapshots, port_snapshots = self.env.snapshot_list[\"vessels\"], self.env.snapshot_list[\"ports\"]\n", - " port_idx, vessel_idx = self.event.port_idx, self.event.vessel_idx\n", + " tick = self._env.tick\n", + " vessel_snapshots, port_snapshots = self._env.snapshot_list[\"vessels\"], self._env.snapshot_list[\"ports\"]\n", + " port_idx, vessel_idx = event.port_idx, event.vessel_idx\n", " ticks = [max(0, tick - rt) for rt in range(state_shaping_conf[\"look_back\"] - 1)]\n", - " future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int') \n", + " future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int')\n", " state = np.concatenate([\n", - " port_snapshots[ticks : [port_idx] + list(future_port_list) : port_attributes],\n", - " vessel_snapshots[tick : vessel_idx : vessel_attributes]\n", + " port_snapshots[ticks: [port_idx] + list(future_port_list): port_attributes],\n", + " vessel_snapshots[tick: vessel_idx: vessel_attributes]\n", " ])\n", - " return {port_idx: state}\n", + " return state, {port_idx: state}\n", "\n", - " def get_env_actions(self, action_by_agent):\n", + " def _translate_to_env_action(self, action_dict, event):\n", " \"\"\"\n", " The policy output is an integer from [0, 20] which is to be interpreted as the index of \"action_space\" in\n", " \"action_shaping_conf\" from the cell above. For example, action 5 corresponds to -0.5, which means loading\n", @@ -94,28 +93,28 @@ " finite_vsl_space = action_shaping_conf[\"finite_vessel_space\"]\n", " has_early_discharge = action_shaping_conf[\"has_early_discharge\"]\n", "\n", - " port_idx, action = list(action_by_agent.items()).pop()\n", - " vsl_idx, action_scope = self.event.vessel_idx, self.event.action_scope\n", - " vsl_snapshots = self.env.snapshot_list[\"vessels\"]\n", - " vsl_space = vsl_snapshots[self.env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float(\"inf\")\n", + " port_idx, model_action = list(action_dict.items()).pop()\n", "\n", - " model_action = action[\"action\"] if isinstance(action, dict) else action \n", - " percent = abs(action_space[model_action])\n", + " vsl_idx, action_scope = event.vessel_idx, event.action_scope\n", + " vsl_snapshots = self._env.snapshot_list[\"vessels\"]\n", + " vsl_space = vsl_snapshots[self._env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float(\"inf\")\n", + "\n", + " percent = abs(action_space[model_action[0]])\n", " zero_action_idx = len(action_space) / 2 # index corresponding to value zero.\n", " if model_action < zero_action_idx:\n", " action_type = ActionType.LOAD\n", " actual_action = min(round(percent * action_scope.load), vsl_space)\n", " elif model_action > zero_action_idx:\n", " action_type = ActionType.DISCHARGE\n", - " early_discharge = vsl_snapshots[self.env.tick:vsl_idx:\"early_discharge\"][0] if has_early_discharge else 0\n", + " early_discharge = vsl_snapshots[self._env.tick:vsl_idx:\"early_discharge\"][0] if has_early_discharge else 0\n", " plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge\n", " actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge)\n", " else:\n", " actual_action, action_type = 0, ActionType.LOAD\n", "\n", - " return [Action(port_idx=port_idx, vessel_idx=vsl_idx, quantity=actual_action, action_type=action_type)]\n", + " return {port_idx: Action(vsl_idx, int(port_idx), actual_action, action_type)}\n", "\n", - " def get_reward(self, actions, tick):\n", + " def _get_reward(self, env_action_dict, event, tick):\n", " \"\"\"\n", " The reward is defined as a linear combination of fulfillment and shortage measures. The fulfillment and\n", " shortage measure are the sums of fulfillment and shortage values over the next k days, respectively, each\n", @@ -127,8 +126,8 @@ " ticks = list(range(start_tick, start_tick + reward_shaping_conf[\"time_window\"]))\n", "\n", " # Get the ports that took actions at the given tick\n", - " ports = [action.port_idx for action in actions]\n", - " port_snapshots = self.env.snapshot_list[\"ports\"]\n", + " ports = [int(port) for port in list(env_action_dict.keys())]\n", + " port_snapshots = self._env.snapshot_list[\"ports\"]\n", " future_fulfillment = port_snapshots[ticks:ports:\"fulfillment\"].reshape(len(ticks), -1)\n", " future_shortage = port_snapshots[ticks:ports:\"shortage\"].reshape(len(ticks), -1)\n", "\n", @@ -139,13 +138,16 @@ " )\n", " return {agent_id: reward for agent_id, reward in zip(ports, rewards)}\n", "\n", - " def post_step(self, state, action, env_action, reward, tick):\n", + " def get_env_metrics(self) -> None:\n", " \"\"\"\n", - " The environment sampler contains a \"tracker\" dict inherited from the \"AbsEnvSampler\" base class, which can\n", + " The environment sampler contains a \"_info\" attribute inherited from the \"AbsEnvSampler\" base class, which can\n", " be used to record any information one wishes to keep track of during a roll-out episode. Here we simply\n", " record the latest env metric without keeping the history for logging purposes.\n", " \"\"\"\n", - " self.tracker[\"env_metric\"] = self.env.metrics" + " return self._env.metrics\n", + "\n", + " def _post_step(self, cache_element, reward) -> None:\n", + " self._info[\"env_metric\"] = self._env.metrics" ] }, { @@ -159,16 +161,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "import torch\n", "from torch.optim import Adam, RMSprop\n", "\n", - "from maro.rl.exploration import MultiLinearExplorationScheduler, epsilon_greedy\n", - "from maro.rl.modeling import DiscreteACNet, FullyConnected\n", - "from maro.rl.policy import ActorCritic\n", + "from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet\n", + "from maro.rl.policy import DiscretePolicyGradient\n", + "from maro.rl.training.algorithms import DiscreteActorCritic, DiscreteActorCriticParams\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", @@ -177,6 +179,7 @@ " (state_shaping_conf[\"look_back\"] + 1) * (state_shaping_conf[\"max_ports_downstream\"] + 1) * len(port_attributes)\n", " + len(vessel_attributes)\n", ")\n", + "action_num = len(action_shaping_conf[\"action_space\"])\n", "\n", "# AC settings\n", "actor_net_conf = {\n", @@ -188,7 +191,6 @@ " \"batch_norm\": False,\n", " \"head\": True\n", "}\n", - "\n", "critic_net_conf = {\n", " \"input_dim\": state_dim,\n", " \"hidden_dims\": [256, 128, 64],\n", @@ -202,60 +204,99 @@ "actor_optim_conf = (Adam, {\"lr\": 0.001})\n", "critic_optim_conf = (RMSprop, {\"lr\": 0.001})\n", "\n", - "ac_conf = {\n", - " \"reward_discount\": .0,\n", - " \"grad_iters\": 10,\n", - " \"critic_loss_cls\": torch.nn.SmoothL1Loss,\n", - " \"min_logp\": None,\n", - " \"critic_loss_coeff\": 0.1,\n", - " \"entropy_coeff\": 0.01,\n", - " # \"clip_ratio\": 0.8 # for PPO\n", - " \"lam\": .0,\n", - " \"get_loss_on_rollout\": False\n", - "}\n", + "ac_conf = DiscreteActorCriticParams(\n", + " get_v_critic_net_func=lambda: MyCriticNet(),\n", + " reward_discount=.0,\n", + " grad_iters=10,\n", + " critic_loss_cls=torch.nn.SmoothL1Loss,\n", + " min_logp=None,\n", + " lam=.0\n", + ")\n", "\n", "\n", - "class MyACNet(DiscreteACNet):\n", - " def __init__(self):\n", - " super().__init__()\n", - " self.actor = FullyConnected(**actor_net_conf)\n", - " self.critic = FullyConnected(**critic_net_conf)\n", - " self.actor_optim = actor_optim_conf[0](self.actor.parameters(), **actor_optim_conf[1])\n", - " self.critic_optim = critic_optim_conf[0](self.critic.parameters(), **critic_optim_conf[1])\n", + "class MyActorNet(DiscretePolicyNet):\n", + " def __init__(self) -> None:\n", + " super(MyActorNet, self).__init__(state_dim=state_dim, action_num=action_num)\n", + " self._actor = FullyConnected(**actor_net_conf)\n", + " self._optim = actor_optim_conf[0](self._actor.parameters(), **actor_optim_conf[1])\n", "\n", - " @property\n", - " def input_dim(self):\n", - " return state_dim\n", + " def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor:\n", + " return self._actor(states)\n", "\n", - " @property\n", - " def num_actions(self):\n", - " return q_net_conf[\"output_dim\"]\n", + " def freeze(self) -> None:\n", + " self.freeze_all_parameters()\n", "\n", - " def forward(self, states, actor: bool = True, critic: bool = True):\n", - " return (self.actor(states) if actor else None), (self.critic(states) if critic else None)\n", + " def unfreeze(self) -> None:\n", + " self.unfreeze_all_parameters()\n", "\n", " def step(self, loss):\n", - " self.actor_optim.zero_grad()\n", - " self.critic_optim.zero_grad()\n", + " self._optim.zero_grad()\n", " loss.backward()\n", - " self.actor_optim.step()\n", - " self.critic_optim.step()\n", + " self._optim.step()\n", "\n", - " def get_gradients(self, loss):\n", - " self.actor_optim.zero_grad()\n", - " self.critic_optim.zero_grad()\n", + " def get_gradients(self, loss: torch.Tensor):\n", + " self._optim.zero_grad()\n", " loss.backward()\n", " return {name: param.grad for name, param in self.named_parameters()}\n", "\n", - " def apply_gradients(self, grad):\n", + " def apply_gradients(self, grad: dict) -> None:\n", " for name, param in self.named_parameters():\n", " param.grad = grad[name]\n", + " self._optim.step()\n", "\n", - " self.actor_optim.step()\n", - " self.critic_optim.step()\n", + " def get_state(self) -> dict:\n", + " return {\n", + " \"network\": self.state_dict(),\n", + " \"optim\": self._optim.state_dict()\n", + " }\n", "\n", + " def set_state(self, net_state: dict) -> None:\n", + " self.load_state_dict(net_state[\"network\"])\n", + " self._optim.load_state_dict(net_state[\"optim\"])\n", "\n", - "policy_func_dict = {f\"ac.{i}\": lambda name: ActorCritic(name, MyACNet(), **ac_conf) for i in range(4)}" + "\n", + "class MyCriticNet(VNet):\n", + " def __init__(self) -> None:\n", + " super(MyCriticNet, self).__init__(state_dim=state_dim)\n", + " self._critic = FullyConnected(**critic_net_conf)\n", + " self._optim = critic_optim_conf[0](self._critic.parameters(), **critic_optim_conf[1])\n", + "\n", + " def _get_v_values(self, states: torch.Tensor) -> torch.Tensor:\n", + " return self._critic(states).squeeze(-1)\n", + "\n", + " def step(self, loss: torch.Tensor):\n", + " self._optim.zero_grad()\n", + " loss.backward()\n", + " self._optim.step()\n", + "\n", + " def get_gradients(self, loss: torch.Tensor):\n", + " self._optim.zero_grad()\n", + " loss.backward()\n", + " return {name: param.grad for name, param in self.named_parameters()}\n", + "\n", + " def apply_gradients(self, grad: dict) -> None:\n", + " for name, param in self.named_parameters():\n", + " param.grad = grad[name]\n", + " self._optim.step()\n", + "\n", + " def get_state(self) -> dict:\n", + " return {\n", + " \"network\": self.state_dict(),\n", + " \"optim\": self._optim.state_dict()\n", + " }\n", + "\n", + " def set_state(self, net_state: dict) -> None:\n", + " self.load_state_dict(net_state[\"network\"])\n", + " self._optim.load_state_dict(net_state[\"optim\"])\n", + "\n", + " def freeze(self) -> None:\n", + " self.freeze_all_parameters()\n", + "\n", + " def unfreeze(self) -> None:\n", + " self.unfreeze_all_parameters()\n", + "\n", + "policy_dict = {f\"ac_{i}.policy\": DiscretePolicyGradient(f\"ac_{i}.policy\", policy_net=MyActorNet()) for i in range(4)}\n", + "trainer_creator = {f\"ac_{i}\": lambda name: DiscreteActorCritic(name, params=ac_conf) for i in range(4)}" ] }, { @@ -269,46 +310,86 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Assign policy ac_0.policy to device cpu\n", + "Assign policy ac_1.policy to device cpu\n", + "Assign policy ac_2.policy to device cpu\n", + "Assign policy ac_3.policy to device cpu\n", + "Policy ac_0.policy has already been assigned to cpu. No need to take further actions.\n", + "Policy ac_1.policy has already been assigned to cpu. No need to take further actions.\n", + "Policy ac_2.policy has already been assigned to cpu. No need to take further actions.\n", + "Policy ac_3.policy has already been assigned to cpu. No need to take further actions.\n", + "env summary (episode 1, segment 1): {'order_requirements': 1120000, 'container_shortage': 755120, 'operation_number': 2003666}\n", + "env summary (episode 2, segment 1): {'order_requirements': 1120000, 'container_shortage': 574168, 'operation_number': 1921718}\n", + "env summary (episode 3, segment 1): {'order_requirements': 1120000, 'container_shortage': 467303, 'operation_number': 1957221}\n", + "env summary (episode 4, segment 1): {'order_requirements': 1120000, 'container_shortage': 287538, 'operation_number': 1982078}\n", + "env summary (episode 5, segment 1): {'order_requirements': 1120000, 'container_shortage': 266790, 'operation_number': 2017310}\n", + "env summary (episode 6, segment 1): {'order_requirements': 1120000, 'container_shortage': 207813, 'operation_number': 1993367}\n", + "env summary (episode 7, segment 1): {'order_requirements': 1120000, 'container_shortage': 258700, 'operation_number': 1924868}\n", + "env summary (episode 8, segment 1): {'order_requirements': 1120000, 'container_shortage': 282732, 'operation_number': 1909072}\n", + "env summary (episode 9, segment 1): {'order_requirements': 1120000, 'container_shortage': 271406, 'operation_number': 1836347}\n", + "env summary (episode 10, segment 1): {'order_requirements': 1120000, 'container_shortage': 253359, 'operation_number': 1797864}\n", + "env summary (episode 11, segment 1): {'order_requirements': 1120000, 'container_shortage': 285019, 'operation_number': 1764610}\n", + "env summary (episode 12, segment 1): {'order_requirements': 1120000, 'container_shortage': 290217, 'operation_number': 1775871}\n", + "env summary (episode 13, segment 1): {'order_requirements': 1120000, 'container_shortage': 291317, 'operation_number': 1696991}\n", + "env summary (episode 14, segment 1): {'order_requirements': 1120000, 'container_shortage': 363557, 'operation_number': 1584546}\n", + "env summary (episode 15, segment 1): {'order_requirements': 1120000, 'container_shortage': 276197, 'operation_number': 1702276}\n", + "env summary (episode 16, segment 1): {'order_requirements': 1120000, 'container_shortage': 449572, 'operation_number': 1343822}\n", + "env summary (episode 17, segment 1): {'order_requirements': 1120000, 'container_shortage': 569588, 'operation_number': 1060983}\n", + "env summary (episode 18, segment 1): {'order_requirements': 1120000, 'container_shortage': 477787, 'operation_number': 1269050}\n", + "env summary (episode 19, segment 1): {'order_requirements': 1120000, 'container_shortage': 675626, 'operation_number': 832979}\n", + "env summary (episode 20, segment 1): {'order_requirements': 1120000, 'container_shortage': 678845, 'operation_number': 809899}\n" + ] + } + ], "source": [ + "from maro.rl.rollout import SimpleAgentWrapper\n", + "from maro.rl.training import TrainerManager \n", "from maro.simulator import Env\n", - "from maro.rl.learning import learn\n", "from maro.utils import set_seeds\n", "\n", - "def get_env_sampler():\n", - " return CIMEnvSampler(\n", - " get_env=lambda: Env(**env_conf),\n", - " get_policy_func_dict=policy_func_dict,\n", - " agent2policy={agent: f\"ac.{agent}\" for agent in Env(**env_conf).agent_idx_list},\n", - " reward_eval_delay=reward_shaping_conf[\"time_window\"]\n", - " )\n", - "\n", - "# post-episode callback, executed at the end of an episode or episode segment.\n", - "def post_collect(trackers, ep, segment):\n", - " \"\"\"\n", - " Print the metric recorded in the env tracker at the end of an episode. The parameter \"trackers\" is actually\n", - " a list because in a distributed setting, the main thread usually receives trackers from multiple roll-out\n", - " instances.\n", - " \"\"\"\n", - " print(f\"env summary (episode {ep}, segment {segment}): {trackers[0]['env_metric']}\")\n", - "\n", - "# post-evaluation callback, executed at the end of an evaluation episode.\n", - "def post_evaluate(trackers, ep):\n", - " \"\"\"\n", - " Print the metric recorded in the env tracker at the end of an evaluation episode. The parameter \"trackers\"\n", - " is actually a list because in a distributed setting, the main thread usually receives trackers from multiple\n", - " roll-out instances.\n", - " \"\"\"\n", - " print(f\"env summary (evaluation episode {ep}): {trackers[0]['env_metric']}\")\n", + "set_seeds(1024) # for reproducibility\n", "\n", + "agent2policy = {agent: f\"ac_{agent}.policy\" for agent in Env(**env_conf).agent_idx_list}\n", + "\n", + "# The env sampler and trainer manager both take ``policy_creator`` as a parameter. The policy creator is\n", + "# a function that takes a name and returns a policy instance. This design is convenient in distributed\n", + "# settings where policies need to be created on both the training side and the roll-out (inference) side\n", + "# and policy states need to be transferred from the former to the latter at the start of each roll-out\n", + "# episode. Here we are demonstrating a single-threaded workflow where there is only one instance of each\n", + "# policy, so we use a little trick here to ensure that the policies created inside the env sampler and the\n", + "# training manager point the the same instances. \n", + "policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict}\n", + "\n", + "env_sampler = CIMEnvSampler(\n", + " get_env=lambda: Env(**env_conf),\n", + " policy_creator=policy_creator,\n", + " agent2policy=agent2policy,\n", + " agent_wrapper_cls=SimpleAgentWrapper,\n", + " device=\"cpu\"\n", + ")\n", "\n", - "set_seeds(1024) # for reproducibility\n", - "learn(\n", - " get_env_sampler, num_episodes=50, eval_after_last_episode=True,\n", - " post_collect=post_collect, post_evaluate=post_evaluate\n", - ")" + "trainer_manager = TrainerManager(policy_creator, trainer_creator, agent2policy)\n", + "\n", + "# main loop with 50 episodes\n", + "for ep in range(1, 21):\n", + " collect_time = training_time = 0\n", + " segment, end_of_episode = 1, False\n", + " while not end_of_episode:\n", + " # experience collection\n", + " result = env_sampler.sample()\n", + " experiences = result[\"experiences\"]\n", + " end_of_episode: bool = result[\"end_of_episode\"]\n", + " print(f\"env summary (episode {ep}, segment {segment}): {env_sampler.get_env_metrics()}\")\n", + " trainer_manager.record_experiences(experiences)\n", + " trainer_manager.train()\n", + " segment += 1" ] }, { @@ -320,10 +401,13 @@ } ], "metadata": { + "interpreter": { + "hash": "8f57a09d39b50edfb56e79199ef40583334d721b06ead0e38a39e7e79092073c" + }, "kernelspec": { "display_name": "maro", "language": "python", - "name": "maro" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -335,7 +419,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.10" + "version": "3.7.11" } }, "nbformat": 4, diff --git a/setup.py b/setup.py index 2797ae1dc..04a426de2 100644 --- a/setup.py +++ b/setup.py @@ -139,7 +139,7 @@ "psutil<5.9.0", "deepdiff>=5.2.2", "azure-storage-blob<12.9.0", - "azure-storage-common>=2.1.0", + "azure-storage-common", "geopy>=2.0.0", "pandas<1.2", "PyYAML<5.5.0", diff --git a/tests/requirements.test.txt b/tests/requirements.test.txt index 58760a388..221ee85cd 100644 --- a/tests/requirements.test.txt +++ b/tests/requirements.test.txt @@ -11,7 +11,7 @@ requests<=2.26.0 psutil<5.9.0 deepdiff>=5.2.2 azure-storage-blob<12.9.0 -azure-storage-common>=2.1.0 +azure-storage-common torch<1.8.0 pytest coverage From 7b3d78a6fc8a6c3ac10fcab9a6e466a027db93b7 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 9 Mar 2022 17:05:12 +0800 Subject: [PATCH 468/482] RL renaming v2 (#476) * Change all Logger in RL to LoggerV2 * TrainerManager => TrainingManager * Add Trainer suffix to all algorithms * Finish docs * Update interface names * Minor fix --- docs/source/apidoc/maro.rl.rst | 4 +- docs/source/key_components/rl_toolkit.rst | 22 +++--- examples/rl/cim/algorithms/ac.py | 6 +- examples/rl/cim/algorithms/dqn.py | 6 +- examples/rl/cim/algorithms/maddpg.py | 6 +- examples/rl/main.py | 10 +-- examples/rl/vm_scheduling/algorithms/ac.py | 6 +- examples/rl/vm_scheduling/algorithms/dqn.py | 6 +- maro/rl/distributed/abs_worker.py | 4 +- maro/rl/policy/abs_policy.py | 2 +- maro/rl/policy/continuous_rl_policy.py | 2 +- maro/rl/policy/discrete_rl_policy.py | 4 +- maro/rl/rollout/batch_env_sampler.py | 10 +-- maro/rl/rollout/worker.py | 6 +- maro/rl/training/__init__.py | 8 +- maro/rl/training/algorithms/__init__.py | 16 ++-- maro/rl/training/algorithms/ac.py | 12 +-- maro/rl/training/algorithms/ddpg.py | 12 +-- maro/rl/training/algorithms/dqn.py | 12 +-- maro/rl/training/algorithms/maddpg.py | 12 +-- maro/rl/training/train_ops.py | 10 +-- maro/rl/training/trainer.py | 16 ++-- ...trainer_manager.py => training_manager.py} | 26 ++----- maro/rl/training/worker.py | 4 +- maro/rl/workflows/main.py | 24 +++--- .../rl_formulation.ipynb | 73 +++++++++---------- 26 files changed, 149 insertions(+), 170 deletions(-) rename maro/rl/training/{trainer_manager.py => training_manager.py} (82%) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index ce1f550cb..93bdd5688 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -176,10 +176,10 @@ maro.rl.training.trainer :undoc-members: :show-inheritance: -maro.rl.training.trainer_manager +maro.rl.training.training_manager -------------------------------------------------------------------------------- -.. automodule:: maro.rl.training.trainer_manager +.. automodule:: maro.rl.training.training_manager :members: :undoc-members: :show-inheritance: diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 1c9fde4ff..9a0805a1c 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -109,7 +109,7 @@ The above code snippet creates a ``ValueBasedPolicy`` object. Let's pay attentio ``q_net`` accepts a ``DiscreteQNet`` object, and it serves as the core part of a ``ValueBasedPolicy`` object. In other words, ``q_net`` defines the model structure of the Q-network in the value-based policy, and further determines the policy's behavior. ``DiscreteQNet`` is an abstract class, and ``MyQNet`` is a user-defined implementation -of ``DiscreteQNet``. It can be a simple MLP, a multihead transformer, or any other structure that the user wants. +of ``DiscreteQNet``. It can be a simple MLP, a multi-head transformer, or any other structure that the user wants. MARO provides a set of abstractions of basic & commonly used PyTorch models like ``DiscereteQNet``, which enables users to implement their own deep learning models in a handy way. They are: @@ -131,22 +131,24 @@ The way to use these models is exactly the same as the way to use the policy mod .. _trainer: -Trainer +Algorithm (Trainer) ------- When introducing policies, we mentioned that policies cannot train themselves. Instead, they have to be trained -by external trainers. In MARO, a trainer represents an RL algorithm, such as DQN, actor-critic, -and so on. Trainers take interaction experiences and store them in the internal memory, and then use the experiences +by external algorithms, which are also called trainers. +In MARO, a trainer represents an RL algorithm, such as DQN, actor-critic, +and so on. These two concepts are equivalent in the MARO context. +Trainers take interaction experiences and store them in the internal memory, and then use the experiences in the memory to train the policies. Like ``RLPolicy``, trainers are also concrete classes, which means they could be used by configuring parameters. Currently, we have 4 trainers (algorithms) in MARO: -- ``DiscreteActorCritic``: Actor-critic algorithm for policies that generate discrete actions. -- ``DDPG``: DDPG algorithm for policies that generate continuous actions. -- ``DQN``: DQN algorithm for policies that generate discrete actions. -- ``DiscreteMADDPG``: MADDPG algorithm for policies that generate discrete actions. +- ``DiscreteActorCriticTrainer``: Actor-critic algorithm for policies that generate discrete actions. +- ``DDPGTrainer``: DDPG algorithm for policies that generate continuous actions. +- ``DQNTrainer``: DQN algorithm for policies that generate discrete actions. +- ``DiscreteMADDPGTrainer``: MADDPG algorithm for policies that generate discrete actions. Each trainer has a corresponding ``Param`` class to manage all related parameters. For example, -``DiscreteActorCriticParams`` contains all parameters used in ``DiscreteActorCritic``: +``DiscreteActorCriticParams`` contains all parameters used in ``DiscreteActorCriticTrainer``: .. code-block:: python @@ -164,7 +166,7 @@ An example of creating an actor-critic trainer: .. code-block:: python - DiscreteActorCritic( + DiscreteActorCriticTrainer( name='ac', params=DiscreteActorCriticParams( device="cpu", diff --git a/examples/rl/cim/algorithms/ac.py b/examples/rl/cim/algorithms/ac.py index d411de103..9e11b6a85 100644 --- a/examples/rl/cim/algorithms/ac.py +++ b/examples/rl/cim/algorithms/ac.py @@ -8,7 +8,7 @@ from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet from maro.rl.policy import DiscretePolicyGradient -from maro.rl.training.algorithms import DiscreteActorCritic, DiscreteActorCriticParams +from maro.rl.training.algorithms import DiscreteActorCriticTrainer, DiscreteActorCriticParams actor_net_conf = { @@ -116,8 +116,8 @@ def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGrad return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) -def get_ac(state_dim: int, name: str) -> DiscreteActorCritic: - return DiscreteActorCritic( +def get_ac(state_dim: int, name: str) -> DiscreteActorCriticTrainer: + return DiscreteActorCriticTrainer( name=name, params=DiscreteActorCriticParams( device="cpu", diff --git a/examples/rl/cim/algorithms/dqn.py b/examples/rl/cim/algorithms/dqn.py index 38fb50059..9e8d7fcc5 100644 --- a/examples/rl/cim/algorithms/dqn.py +++ b/examples/rl/cim/algorithms/dqn.py @@ -9,7 +9,7 @@ from maro.rl.exploration import MultiLinearExplorationScheduler, epsilon_greedy from maro.rl.model import DiscreteQNet, FullyConnected from maro.rl.policy import ValueBasedPolicy -from maro.rl.training.algorithms import DQN, DQNParams +from maro.rl.training.algorithms import DQNTrainer, DQNParams q_net_conf = { "hidden_dims": [256, 128, 64, 32], @@ -79,8 +79,8 @@ def get_policy(state_dim: int, action_num: int, name: str) -> ValueBasedPolicy: ) -def get_dqn(name: str) -> DQN: - return DQN( +def get_dqn(name: str) -> DQNTrainer: + return DQNTrainer( name=name, params=DQNParams( device="cpu", diff --git a/examples/rl/cim/algorithms/maddpg.py b/examples/rl/cim/algorithms/maddpg.py index 4f0bde8cc..e47603a1e 100644 --- a/examples/rl/cim/algorithms/maddpg.py +++ b/examples/rl/cim/algorithms/maddpg.py @@ -9,7 +9,7 @@ from maro.rl.model import DiscretePolicyNet, FullyConnected, MultiQNet from maro.rl.policy import DiscretePolicyGradient -from maro.rl.training.algorithms import DiscreteMADDPG, DiscreteMADDPGParams +from maro.rl.training.algorithms import DiscreteMADDPGTrainer, DiscreteMADDPGParams actor_net_conf = { @@ -122,8 +122,8 @@ def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGrad return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) -def get_maddpg(state_dim: int, action_dims: List[int], name: str) -> DiscreteMADDPG: - return DiscreteMADDPG( +def get_maddpg(state_dim: int, action_dims: List[int], name: str) -> DiscreteMADDPGTrainer: + return DiscreteMADDPGTrainer( name=name, params=DiscreteMADDPGParams( device="cpu", diff --git a/examples/rl/main.py b/examples/rl/main.py index a690f703b..6feb8b4ff 100644 --- a/examples/rl/main.py +++ b/examples/rl/main.py @@ -3,7 +3,7 @@ import os -from maro.rl.training import TrainerManager +from maro.rl.training import TrainingManager from maro.rl.workflows.scenario import Scenario from maro.utils import LoggerV2 @@ -32,7 +32,7 @@ eval_point_index = 0 env_sampler = scenario.get_env_sampler(policy_creator) - trainer_manager = TrainerManager(policy_creator, trainer_creator, agent2policy, logger=logger) + training_manager = TrainingManager(policy_creator, trainer_creator, agent2policy, logger=logger) # main loop for ep in range(1, NUM_EPISODES + 1): @@ -48,11 +48,11 @@ scenario.post_collect(result["info"], ep, segment) logger.info(f"Roll-out completed for episode {ep}. Training started...") - trainer_manager.record_experiences(experiences) - trainer_manager.train() + training_manager.record_experiences(experiences) + training_manager.train_step() if CHECKPOINT_PATH and ep % CHECKPOINT_INTERVAL == 0: pth = os.path.join(CHECKPOINT_PATH, str(ep)) - trainer_manager.save(pth) + training_manager.save(pth) logger.info(f"All trainer states saved under {pth}") segment += 1 diff --git a/examples/rl/vm_scheduling/algorithms/ac.py b/examples/rl/vm_scheduling/algorithms/ac.py index af4b9c5cf..fbc1ca858 100644 --- a/examples/rl/vm_scheduling/algorithms/ac.py +++ b/examples/rl/vm_scheduling/algorithms/ac.py @@ -8,7 +8,7 @@ from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet from maro.rl.policy import DiscretePolicyGradient -from maro.rl.training.algorithms import DiscreteActorCritic, DiscreteActorCriticParams +from maro.rl.training.algorithms import DiscreteActorCriticTrainer, DiscreteActorCriticParams actor_net_conf = { @@ -123,8 +123,8 @@ def get_policy(state_dim: int, action_num: int, num_features: int, name: str) -> return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num, num_features)) -def get_ac(state_dim: int, num_features: int, name: str) -> DiscreteActorCritic: - return DiscreteActorCritic( +def get_ac(state_dim: int, num_features: int, name: str) -> DiscreteActorCriticTrainer: + return DiscreteActorCriticTrainer( name=name, params=DiscreteActorCriticParams( device="cpu", diff --git a/examples/rl/vm_scheduling/algorithms/dqn.py b/examples/rl/vm_scheduling/algorithms/dqn.py index d7126d4c4..e81547031 100644 --- a/examples/rl/vm_scheduling/algorithms/dqn.py +++ b/examples/rl/vm_scheduling/algorithms/dqn.py @@ -11,7 +11,7 @@ from maro.rl.exploration import MultiLinearExplorationScheduler from maro.rl.model import DiscreteQNet, FullyConnected from maro.rl.policy import ValueBasedPolicy -from maro.rl.training.algorithms import DQN, DQNParams +from maro.rl.training.algorithms import DQNTrainer, DQNParams q_net_conf = { @@ -100,8 +100,8 @@ def get_policy(state_dim: int, action_num: int, num_features: int, name: str) -> ) -def get_dqn(name: str) -> DQN: - return DQN( +def get_dqn(name: str) -> DQNTrainer: + return DQNTrainer( name=name, params=DQNParams( device="cpu", diff --git a/maro/rl/distributed/abs_worker.py b/maro/rl/distributed/abs_worker.py index 024b8324c..9baafb07a 100644 --- a/maro/rl/distributed/abs_worker.py +++ b/maro/rl/distributed/abs_worker.py @@ -9,7 +9,7 @@ from zmq.eventloop.zmqstream import ZMQStream from maro.rl.utils.common import get_ip_address_by_hostname, string_to_bytes -from maro.utils import DummyLogger, Logger +from maro.utils import DummyLogger, LoggerV2 class AbsWorker(object): @@ -28,7 +28,7 @@ def __init__( idx: int, producer_host: str, producer_port: int, - logger: Logger = None, + logger: LoggerV2 = None, ) -> None: super(AbsWorker, self).__init__() diff --git a/maro/rl/policy/abs_policy.py b/maro/rl/policy/abs_policy.py index 01006c8db..c6064e03a 100644 --- a/maro/rl/policy/abs_policy.py +++ b/maro/rl/policy/abs_policy.py @@ -125,7 +125,7 @@ def exploit(self) -> None: self._is_exploring = False @abstractmethod - def step(self, loss: torch.Tensor) -> None: + def train_step(self, loss: torch.Tensor) -> None: """Run a training step to update the policy according to the given loss. Args: diff --git a/maro/rl/policy/continuous_rl_policy.py b/maro/rl/policy/continuous_rl_policy.py index e3aa79ce3..5e3ade8fd 100644 --- a/maro/rl/policy/continuous_rl_policy.py +++ b/maro/rl/policy/continuous_rl_policy.py @@ -80,7 +80,7 @@ def _post_check(self, states: torch.Tensor, actions: torch.Tensor) -> bool: def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: return self._policy_net.get_actions(states, exploring) - def step(self, loss: torch.Tensor) -> None: + def train_step(self, loss: torch.Tensor) -> None: self._policy_net.step(loss) def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: diff --git a/maro/rl/policy/discrete_rl_policy.py b/maro/rl/policy/discrete_rl_policy.py index 57694331b..d092f2d01 100644 --- a/maro/rl/policy/discrete_rl_policy.py +++ b/maro/rl/policy/discrete_rl_policy.py @@ -160,7 +160,7 @@ def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tens actions = ndarray_to_tensor(actions, self._device) return actions.unsqueeze(1) # [B, 1] - def step(self, loss: torch.Tensor) -> None: + def train_step(self, loss: torch.Tensor) -> None: return self._q_net.step(loss) def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: @@ -226,7 +226,7 @@ def policy_net(self) -> DiscretePolicyNet: def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: return self._policy_net.get_actions(states, exploring) - def step(self, loss: torch.Tensor) -> None: + def train_step(self, loss: torch.Tensor) -> None: self._policy_net.step(loss) def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: diff --git a/maro/rl/rollout/batch_env_sampler.py b/maro/rl/rollout/batch_env_sampler.py index fbc36bef9..62e825a6a 100644 --- a/maro/rl/rollout/batch_env_sampler.py +++ b/maro/rl/rollout/batch_env_sampler.py @@ -9,7 +9,7 @@ from zmq import Context, Poller from maro.rl.utils.common import bytes_to_pyobj, get_own_ip_address, pyobj_to_bytes -from maro.utils import DummyLogger, Logger +from maro.utils import DummyLogger, LoggerV2 from .env_sampler import ExpElement @@ -19,10 +19,10 @@ class ParallelTaskController(object): Args: port (int, default=20000): Network port the controller uses to talk to the remote workers. - logger (Logger, default=None): Optional logger for logging key events. + logger (LoggerV2, default=None): Optional logger for logging key events. """ - def __init__(self, port: int = 20000, logger: Logger = None) -> None: + def __init__(self, port: int = 20000, logger: LoggerV2 = None) -> None: self._ip = get_own_ip_address() self._context = Context.instance() @@ -117,7 +117,7 @@ class BatchEnvSampler: are received in T seconds, it will allow an additional T * grace_factor seconds to collect the remaining results. eval_parallelism (int, default=None): Parallelism for policy evaluation on remote workers. - logger (Logger, default=None): Optional logger for logging key events. + logger (LoggerV2, default=None): Optional logger for logging key events. """ def __init__( @@ -127,7 +127,7 @@ def __init__( min_env_samples: int = None, grace_factor: float = None, eval_parallelism: int = None, - logger: Logger = None, + logger: LoggerV2 = None, ) -> None: super(BatchEnvSampler, self).__init__() self._logger = logger if logger else DummyLogger() diff --git a/maro/rl/rollout/worker.py b/maro/rl/rollout/worker.py index 2c6e77410..0639a1c11 100644 --- a/maro/rl/rollout/worker.py +++ b/maro/rl/rollout/worker.py @@ -5,7 +5,7 @@ from maro.rl.distributed import AbsWorker from maro.rl.utils.common import bytes_to_pyobj, pyobj_to_bytes -from maro.utils import Logger +from maro.utils import LoggerV2 from .env_sampler import AbsEnvSampler @@ -20,7 +20,7 @@ class RolloutWorker(AbsWorker): for roll-out purposes. producer_host (str): IP address of the parallel task controller host to connect to. producer_port (int, default=20000): Port of the parallel task controller host to connect to. - logger (Logger, default=None): The logger of the workflow. + logger (LoggerV2, default=None): The logger of the workflow. """ def __init__( @@ -29,7 +29,7 @@ def __init__( env_sampler_creator: Callable[[], AbsEnvSampler], producer_host: str, producer_port: int = 20000, - logger: Logger = None, + logger: LoggerV2 = None, ) -> None: super(RolloutWorker, self).__init__( idx=idx, producer_host=producer_host, producer_port=producer_port, logger=logger, diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 0b0e6a4cc..54ed5d0b4 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -4,15 +4,15 @@ from .proxy import TrainingProxy from .replay_memory import FIFOMultiReplayMemory, FIFOReplayMemory, RandomMultiReplayMemory, RandomReplayMemory from .train_ops import AbsTrainOps, RemoteOps, remote -from .trainer import AbsTrainer, MultiTrainer, SingleTrainer, TrainerParams -from .trainer_manager import TrainerManager +from .trainer import AbsTrainer, MultiAgentTrainer, SingleAgentTrainer, TrainerParams +from .training_manager import TrainingManager from .worker import TrainOpsWorker __all__ = [ "TrainingProxy", "FIFOMultiReplayMemory", "FIFOReplayMemory", "RandomMultiReplayMemory", "RandomReplayMemory", "AbsTrainOps", "RemoteOps", "remote", - "AbsTrainer", "MultiTrainer", "SingleTrainer", "TrainerParams", - "TrainerManager", + "AbsTrainer", "MultiAgentTrainer", "SingleAgentTrainer", "TrainerParams", + "TrainingManager", "TrainOpsWorker", ] diff --git a/maro/rl/training/algorithms/__init__.py b/maro/rl/training/algorithms/__init__.py index 851ef4775..0191e39f1 100644 --- a/maro/rl/training/algorithms/__init__.py +++ b/maro/rl/training/algorithms/__init__.py @@ -1,14 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .ac import DiscreteActorCritic, DiscreteActorCriticParams -from .ddpg import DDPG, DDPGParams -from .dqn import DQN, DQNParams -from .maddpg import DiscreteMADDPG, DiscreteMADDPGParams +from .ac import DiscreteActorCriticParams, DiscreteActorCriticTrainer +from .ddpg import DDPGParams, DDPGTrainer +from .dqn import DQNParams, DQNTrainer +from .maddpg import DiscreteMADDPGParams, DiscreteMADDPGTrainer __all__ = [ - "DiscreteActorCritic", "DiscreteActorCriticParams", - "DDPG", "DDPGParams", - "DQN", "DQNParams", - "DiscreteMADDPG", "DiscreteMADDPGParams", + "DiscreteActorCriticTrainer", "DiscreteActorCriticParams", + "DDPGTrainer", "DDPGParams", + "DQNTrainer", "DQNParams", + "DiscreteMADDPGTrainer", "DiscreteMADDPGParams", ] diff --git a/maro/rl/training/algorithms/ac.py b/maro/rl/training/algorithms/ac.py index da0f3b3c3..fc8909f5e 100644 --- a/maro/rl/training/algorithms/ac.py +++ b/maro/rl/training/algorithms/ac.py @@ -11,7 +11,7 @@ from maro.rl.model import VNet from maro.rl.policy import DiscretePolicyGradient from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, FIFOReplayMemory, RemoteOps, SingleTrainer, TrainerParams, remote +from maro.rl.training import AbsTrainOps, FIFOReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote from maro.rl.utils import TransitionBatch, discount_cumsum, merge_transition_batches, ndarray_to_tensor @@ -184,7 +184,7 @@ def update_actor(self, batch: TransitionBatch) -> None: Args: batch (TransitionBatch): Batch. """ - self._policy.step(self._get_actor_loss(batch)) + self._policy.train_step(self._get_actor_loss(batch)) def update_actor_with_grad(self, grad_dict: dict) -> None: """Update the actor network with remotely computed gradients. @@ -241,7 +241,7 @@ def preprocess_and_merge_batches(self, batch_list: List[TransitionBatch]) -> Tra return merge_transition_batches([self._preprocess_batch(batch) for batch in batch_list]) -class DiscreteActorCritic(SingleTrainer): +class DiscreteActorCriticTrainer(SingleAgentTrainer): """Actor Critic algorithm with separate policy and value models. References: @@ -250,7 +250,7 @@ class DiscreteActorCritic(SingleTrainer): """ def __init__(self, name: str, params: DiscreteActorCriticParams) -> None: - super(DiscreteActorCritic, self).__init__(name, params) + super(DiscreteActorCriticTrainer, self).__init__(name, params) self._params = params self._ops_name = f"{self._name}.ops" @@ -289,14 +289,14 @@ def _get_batch(self) -> TransitionBatch: batch_list = [memory.sample(-1) for memory in self._replay_memory_dict.values()] return self._ops.preprocess_and_merge_batches(batch_list) - def train(self) -> None: + def train_step(self) -> None: assert isinstance(self._ops, DiscreteActorCriticOps) batch = self._get_batch() for _ in range(self._params.grad_iters): self._ops.update_critic(batch) self._ops.update_actor(batch) - async def train_as_task(self) -> None: + async def train_step_as_task(self) -> None: assert isinstance(self._ops, RemoteOps) batch = self._get_batch() for _ in range(self._params.grad_iters): diff --git a/maro/rl/training/algorithms/ddpg.py b/maro/rl/training/algorithms/ddpg.py index 0d8ae9599..45a90c064 100644 --- a/maro/rl/training/algorithms/ddpg.py +++ b/maro/rl/training/algorithms/ddpg.py @@ -12,7 +12,7 @@ from maro.rl.model import QNet from maro.rl.policy import ContinuousRLPolicy from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleTrainer, TrainerParams, remote +from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote from maro.rl.utils import TransitionBatch, ndarray_to_tensor from maro.utils import clone @@ -201,7 +201,7 @@ def update_actor(self, batch: TransitionBatch) -> None: batch (TransitionBatch): Batch. """ self._policy.train() - self._policy.step(self._get_actor_loss(batch)) + self._policy.train_step(self._get_actor_loss(batch)) def get_state(self) -> dict: return { @@ -224,7 +224,7 @@ def soft_update_target(self) -> None: self._target_q_critic_net.soft_update(self._q_critic_net, self._soft_update_coef) -class DDPG(SingleTrainer): +class DDPGTrainer(SingleAgentTrainer): """The Deep Deterministic Policy Gradient (DDPG) algorithm. References: @@ -233,7 +233,7 @@ class DDPG(SingleTrainer): """ def __init__(self, name: str, params: DDPGParams) -> None: - super(DDPG, self).__init__(name, params) + super(DDPGTrainer, self).__init__(name, params) self._params = params self._policy_version = self._target_policy_version = 0 self._ops_name = f"{self._name}.ops" @@ -272,7 +272,7 @@ def get_local_ops_by_name(self, name: str) -> AbsTrainOps: def _get_batch(self, batch_size: int = None) -> TransitionBatch: return self._replay_memory.sample(batch_size if batch_size is not None else self._batch_size) - def train(self) -> None: + def train_step(self) -> None: assert isinstance(self._ops, DDPGOps) for _ in range(self._params.num_epochs): batch = self._get_batch() @@ -281,7 +281,7 @@ def train(self) -> None: self._try_soft_update_target() - async def train_as_task(self) -> None: + async def train_step_as_task(self) -> None: assert isinstance(self._ops, RemoteOps) for _ in range(self._params.num_epochs): batch = self._get_batch() diff --git a/maro/rl/training/algorithms/dqn.py b/maro/rl/training/algorithms/dqn.py index 6d5ababbd..58c207b56 100644 --- a/maro/rl/training/algorithms/dqn.py +++ b/maro/rl/training/algorithms/dqn.py @@ -9,7 +9,7 @@ from maro.rl.policy import ValueBasedPolicy from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleTrainer, TrainerParams, remote +from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote from maro.rl.utils import TransitionBatch, ndarray_to_tensor from maro.utils import clone @@ -136,7 +136,7 @@ def update(self, batch: TransitionBatch) -> None: batch (TransitionBatch): Batch. """ self._policy.train() - self._policy.step(self._get_batch_loss(batch)) + self._policy.train_step(self._get_batch_loss(batch)) def get_state(self) -> dict: return { @@ -154,14 +154,14 @@ def soft_update_target(self) -> None: self._target_policy.soft_update(self._policy, self._soft_update_coef) -class DQN(SingleTrainer): +class DQNTrainer(SingleAgentTrainer): """The Deep-Q-Networks algorithm. See https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf for details. """ def __init__(self, name: str, params: DQNParams) -> None: - super(DQN, self).__init__(name, params) + super(DQNTrainer, self).__init__(name, params) self._params = params self._q_net_version = self._target_q_net_version = 0 self._ops_name = f"{self._name}.ops" @@ -202,14 +202,14 @@ def get_local_ops_by_name(self, name: str) -> AbsTrainOps: def _get_batch(self, batch_size: int = None) -> TransitionBatch: return self._replay_memory.sample(batch_size if batch_size is not None else self._batch_size) - def train(self) -> None: + def train_step(self) -> None: assert isinstance(self._ops, DQNOps) for _ in range(self._params.num_epochs): self._ops.update(self._get_batch()) self._try_soft_update_target() - async def train_as_task(self) -> None: + async def train_step_as_task(self) -> None: assert isinstance(self._ops, RemoteOps) for _ in range(self._params.num_epochs): batch = self._get_batch() diff --git a/maro/rl/training/algorithms/maddpg.py b/maro/rl/training/algorithms/maddpg.py index 620b7b034..92dc70661 100644 --- a/maro/rl/training/algorithms/maddpg.py +++ b/maro/rl/training/algorithms/maddpg.py @@ -11,7 +11,7 @@ from maro.rl.model import MultiQNet from maro.rl.policy import DiscretePolicyGradient from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, MultiTrainer, RandomMultiReplayMemory, RemoteOps, TrainerParams, remote +from maro.rl.training import AbsTrainOps, MultiAgentTrainer, RandomMultiReplayMemory, RemoteOps, TrainerParams, remote from maro.rl.utils import MultiTransitionBatch, ndarray_to_tensor from maro.utils import clone @@ -243,7 +243,7 @@ def update_actor(self, batch: MultiTransitionBatch) -> None: batch (MultiTransitionBatch): Batch. """ self._policy.train() - self._policy.step(self._get_actor_loss(batch)) + self._policy.train_step(self._get_actor_loss(batch)) def update_actor_with_grad(self, grad_dict: dict) -> None: """Update the critic network with remotely computed gradients. @@ -291,9 +291,9 @@ def set_state(self, ops_state_dict: dict) -> None: self.set_actor_state(ops_state_dict) -class DiscreteMADDPG(MultiTrainer): +class DiscreteMADDPGTrainer(MultiAgentTrainer): def __init__(self, name: str, params: DiscreteMADDPGParams) -> None: - super(DiscreteMADDPG, self).__init__(name, params) + super(DiscreteMADDPGTrainer, self).__init__(name, params) self._params = params self._ops_params = self._params.extract_ops_params() self._state_dim = params.get_q_critic_net_func().state_dim @@ -382,7 +382,7 @@ def get_local_ops_by_name(self, name: str) -> AbsTrainOps: }) return DiscreteMADDPGOps(name=name, **ops_params) - def train(self) -> None: + def train_step(self) -> None: assert not self._params.shared_critic or isinstance(self._critic_ops, DiscreteMADDPGOps) assert all(isinstance(ops, DiscreteMADDPGOps) for ops in self._actor_ops_list) for _ in range(self._params.num_epoch): @@ -408,7 +408,7 @@ def train(self) -> None: # Update version self._try_soft_update_target() - async def train_as_task(self) -> None: + async def train_step_as_task(self) -> None: assert not self._params.shared_critic or isinstance(self._critic_ops, RemoteOps) assert all(isinstance(ops, RemoteOps) for ops in self._actor_ops_list) for _ in range(self._params.num_epoch): diff --git a/maro/rl/training/train_ops.py b/maro/rl/training/train_ops.py index c22b1d0d3..18405d55c 100644 --- a/maro/rl/training/train_ops.py +++ b/maro/rl/training/train_ops.py @@ -12,7 +12,7 @@ from maro.rl.policy import RLPolicy from maro.rl.utils import AbsTransitionBatch, MultiTransitionBatch, TransitionBatch from maro.rl.utils.common import bytes_to_pyobj, get_ip_address_by_hostname, pyobj_to_bytes -from maro.utils import DummyLogger, Logger +from maro.utils import DummyLogger, LoggerV2 class AbsTrainOps(object, metaclass=ABCMeta): @@ -127,10 +127,10 @@ class AsyncClient(object): Args: name (str): Name of the client. address (Tuple[str, int]): Address (host and port) of the training proxy. - logger (Logger, default=None): logger. + logger (LoggerV2, default=None): logger. """ - def __init__(self, name: str, address: Tuple[str, int], logger: Logger = None) -> None: + def __init__(self, name: str, address: Tuple[str, int], logger: LoggerV2 = None) -> None: self._logger = DummyLogger() if logger is None else logger self._name = name host, port = address @@ -198,10 +198,10 @@ class RemoteOps(object): ops (AbsTrainOps): An ``AbsTrainOps`` instance to be wrapped. Any method annotated by the remote decorator in its definition is transformed to a remote function call. address (Tuple[str, int]): Address (host and port) of the training proxy. - logger (Logger, default=None): logger. + logger (LoggerV2, default=None): logger. """ - def __init__(self, ops: AbsTrainOps, address: Tuple[str, int], logger: Logger = None) -> None: + def __init__(self, ops: AbsTrainOps, address: Tuple[str, int], logger: LoggerV2 = None) -> None: self._ops = ops self._client = AsyncClient(self._ops.name, address, logger=logger) self._client.connect() diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index d75a0165c..5d5e4a0a9 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -9,7 +9,7 @@ from maro.rl.policy import RLPolicy from maro.rl.rollout import ExpElement -from maro.utils import Logger +from maro.utils import LoggerV2 from .train_ops import AbsTrainOps, RemoteOps from .utils import extract_trainer_name @@ -77,7 +77,7 @@ def name(self) -> str: def agent_num(self) -> int: return len(self._agent2policy) - def register_logger(self, logger: Logger) -> None: + def register_logger(self, logger: LoggerV2) -> None: self._logger = logger def register_agent2policy(self, agent2policy: Dict[Any, str]) -> None: @@ -115,12 +115,12 @@ def build(self) -> None: raise NotImplementedError @abstractmethod - def train(self) -> None: + def train_step(self) -> None: """Run a training step to update all the policies that this trainer is responsible for. """ raise NotImplementedError - async def train_as_task(self) -> None: + async def train_step_as_task(self) -> None: """Update all policies managed by the trainer as an asynchronous task. """ raise NotImplementedError @@ -187,12 +187,12 @@ async def exit(self) -> None: raise NotImplementedError -class SingleTrainer(AbsTrainer, metaclass=ABCMeta): +class SingleAgentTrainer(AbsTrainer, metaclass=ABCMeta): """Policy trainer that trains only one policy. """ def __init__(self, name: str, params: TrainerParams) -> None: - super(SingleTrainer, self).__init__(name, params) + super(SingleAgentTrainer, self).__init__(name, params) self._ops: Union[RemoteOps, None] = None # To be created in `build()` @@ -239,12 +239,12 @@ async def exit(self) -> None: await self._ops.exit() -class MultiTrainer(AbsTrainer, metaclass=ABCMeta): +class MultiAgentTrainer(AbsTrainer, metaclass=ABCMeta): """Policy trainer that trains multiple policies. """ def __init__(self, name: str, params: TrainerParams) -> None: - super(MultiTrainer, self).__init__(name, params) + super(MultiAgentTrainer, self).__init__(name, params) self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = {} self._policy_names: List[str] = [] diff --git a/maro/rl/training/trainer_manager.py b/maro/rl/training/training_manager.py similarity index 82% rename from maro/rl/training/trainer_manager.py rename to maro/rl/training/training_manager.py index 87959ea93..b9ebbef4a 100644 --- a/maro/rl/training/trainer_manager.py +++ b/maro/rl/training/training_manager.py @@ -8,15 +8,15 @@ from maro.rl.policy import RLPolicy from maro.rl.rollout import ExpElement -from maro.utils import Logger +from maro.utils import LoggerV2 from .trainer import AbsTrainer from .utils import extract_trainer_name, get_trainer_state_path -class TrainerManager(object): +class TrainingManager(object): """ - Trainer manager. Manage and schedule all trainers to train policies. + Training manager. Manage and schedule all trainers to train policies. Args: policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict of functions to create policies. @@ -32,19 +32,9 @@ def __init__( trainer_creator: Dict[str, Callable[[str], AbsTrainer]], agent2policy: Dict[str, str], # {agent_name: policy_name} proxy_address: Tuple[str, int] = None, - logger: Logger = None, + logger: LoggerV2 = None, ) -> None: - """ - Trainer manager. - - Args: - policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict of functions to create policies. - trainer_creator (Dict[str, Callable[[str], AbsTrainer]]): Dict of functions to create trainers. - agent2policy (Dict[str, str]): Agent name to policy name mapping. - proxy_address (Tuple[str, int]): The address of the proxy. This is used only in distributed mode. - Defaults to None. - """ - super(TrainerManager, self).__init__() + super(TrainingManager, self).__init__() self._trainer_dict: Dict[str, AbsTrainer] = {} self._agent2policy = agent2policy @@ -64,15 +54,15 @@ def __init__( for agent_name, policy_name in self._agent2policy.items() } - def train(self) -> None: + def train_step(self) -> None: if self._proxy_address: async def train_step() -> Iterable: - return await asyncio.gather(*[trainer.train_as_task() for trainer in self._trainer_dict.values()]) + return await asyncio.gather(*[trainer.train_step_as_task() for trainer in self._trainer_dict.values()]) asyncio.run(train_step()) else: for trainer in self._trainer_dict.values(): - trainer.train() + trainer.train_step() def get_policy_state(self) -> Dict[str, Dict[str, object]]: """Get policies' states. diff --git a/maro/rl/training/worker.py b/maro/rl/training/worker.py index cc6b2b627..b566bd2b3 100644 --- a/maro/rl/training/worker.py +++ b/maro/rl/training/worker.py @@ -6,7 +6,7 @@ from maro.rl.distributed import AbsWorker from maro.rl.policy import RLPolicy from maro.rl.utils.common import bytes_to_pyobj, bytes_to_string, pyobj_to_bytes -from maro.utils import Logger +from maro.utils import LoggerV2 from .train_ops import AbsTrainOps from .trainer import AbsTrainer @@ -33,7 +33,7 @@ def __init__( trainer_creator: Dict[str, Callable[[str], AbsTrainer]], producer_host: str, producer_port: int = 10001, - logger: Logger = None, + logger: LoggerV2 = None, ) -> None: super(TrainOpsWorker, self).__init__( idx=idx, producer_host=producer_host, producer_port=producer_port, logger=logger, diff --git a/maro/rl/workflows/main.py b/maro/rl/workflows/main.py index bed4739b4..e3c96fe6e 100644 --- a/maro/rl/workflows/main.py +++ b/maro/rl/workflows/main.py @@ -6,7 +6,7 @@ from typing import List from maro.rl.rollout import BatchEnvSampler, ExpElement -from maro.rl.training import TrainerManager +from maro.rl.training import TrainingManager from maro.rl.utils.common import float_or_none, get_env, int_or_none, list_or_none from maro.rl.workflows.scenario import Scenario from maro.utils import LoggerV2 @@ -59,7 +59,7 @@ def main(scenario: Scenario) -> None: logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") eval_point_index = 0 - trainer_manager = TrainerManager( + training_manager = TrainingManager( policy_creator, trainer_creator, agent2policy, proxy_address=None if train_mode == "simple" else ( get_env("TRAIN_PROXY_HOST"), int(get_env("TRAIN_PROXY_FRONTEND_PORT")) @@ -68,7 +68,7 @@ def main(scenario: Scenario) -> None: ) if load_path: assert isinstance(load_path, str) - loaded = trainer_manager.load(load_path) + loaded = training_manager.load(load_path) logger.info(f"Loaded states for {loaded} from {load_path}") # main loop @@ -79,7 +79,7 @@ def main(scenario: Scenario) -> None: # Experience collection tc0 = time.time() result = env_sampler.sample( - policy_state=trainer_manager.get_policy_state() if not is_single_thread else None, + policy_state=training_manager.get_policy_state() if not is_single_thread else None, num_steps=num_steps, ) experiences: List[List[ExpElement]] = result["experiences"] @@ -92,12 +92,12 @@ def main(scenario: Scenario) -> None: logger.info(f"Roll-out completed for episode {ep}, segment {segment}. Training started...") tu0 = time.time() - trainer_manager.record_experiences(experiences) - trainer_manager.train() + training_manager.record_experiences(experiences) + training_manager.train_step() if checkpoint_path and (checkpoint_interval is None or ep % checkpoint_interval == 0): assert isinstance(checkpoint_path, str) pth = os.path.join(checkpoint_path, str(ep)) - trainer_manager.save(pth) + training_manager.save(pth) logger.info(f"All trainer states saved under {pth}") training_time += time.time() - tu0 segment += 1 @@ -107,17 +107,11 @@ def main(scenario: Scenario) -> None: if eval_schedule and ep == eval_schedule[eval_point_index]: eval_point_index += 1 result = env_sampler.eval( - policy_state=trainer_manager.get_policy_state() if not is_single_thread else None + policy_state=training_manager.get_policy_state() if not is_single_thread else None ) if scenario.post_evaluate: scenario.post_evaluate(result["info"], ep) if isinstance(env_sampler, BatchEnvSampler): env_sampler.exit() - trainer_manager.exit() - - -if __name__ == "__main__": - # get user-defined scenario ingredients - scenario = Scenario(str(get_env("SCENARIO_PATH"))) - main(scenario) + training_manager.exit() diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb index 96ab9a88b..02d6f03d3 100644 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ b/notebooks/container_inventory_management/rl_formulation.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -55,7 +55,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -161,7 +161,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -170,7 +170,7 @@ "\n", "from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet\n", "from maro.rl.policy import DiscretePolicyGradient\n", - "from maro.rl.training.algorithms import DiscreteActorCritic, DiscreteActorCriticParams\n", + "from maro.rl.training.algorithms import DiscreteActorCriticTrainer, DiscreteActorCriticParams\n", "\n", "# We consider the port in question as well as two downstream ports.\n", "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", @@ -210,7 +210,8 @@ " grad_iters=10,\n", " critic_loss_cls=torch.nn.SmoothL1Loss,\n", " min_logp=None,\n", - " lam=.0\n", + " lam=.0,\n", + " device=\"cpu\",\n", ")\n", "\n", "\n", @@ -296,7 +297,7 @@ " self.unfreeze_all_parameters()\n", "\n", "policy_dict = {f\"ac_{i}.policy\": DiscretePolicyGradient(f\"ac_{i}.policy\", policy_net=MyActorNet()) for i in range(4)}\n", - "trainer_creator = {f\"ac_{i}\": lambda name: DiscreteActorCritic(name, params=ac_conf) for i in range(4)}" + "trainer_creator = {f\"ac_{i}\": lambda name: DiscreteActorCriticTrainer(name, params=ac_conf) for i in range(4)}" ] }, { @@ -310,47 +311,39 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Assign policy ac_0.policy to device cpu\n", - "Assign policy ac_1.policy to device cpu\n", - "Assign policy ac_2.policy to device cpu\n", - "Assign policy ac_3.policy to device cpu\n", - "Policy ac_0.policy has already been assigned to cpu. No need to take further actions.\n", - "Policy ac_1.policy has already been assigned to cpu. No need to take further actions.\n", - "Policy ac_2.policy has already been assigned to cpu. No need to take further actions.\n", - "Policy ac_3.policy has already been assigned to cpu. No need to take further actions.\n", - "env summary (episode 1, segment 1): {'order_requirements': 1120000, 'container_shortage': 755120, 'operation_number': 2003666}\n", - "env summary (episode 2, segment 1): {'order_requirements': 1120000, 'container_shortage': 574168, 'operation_number': 1921718}\n", - "env summary (episode 3, segment 1): {'order_requirements': 1120000, 'container_shortage': 467303, 'operation_number': 1957221}\n", - "env summary (episode 4, segment 1): {'order_requirements': 1120000, 'container_shortage': 287538, 'operation_number': 1982078}\n", - "env summary (episode 5, segment 1): {'order_requirements': 1120000, 'container_shortage': 266790, 'operation_number': 2017310}\n", - "env summary (episode 6, segment 1): {'order_requirements': 1120000, 'container_shortage': 207813, 'operation_number': 1993367}\n", - "env summary (episode 7, segment 1): {'order_requirements': 1120000, 'container_shortage': 258700, 'operation_number': 1924868}\n", - "env summary (episode 8, segment 1): {'order_requirements': 1120000, 'container_shortage': 282732, 'operation_number': 1909072}\n", - "env summary (episode 9, segment 1): {'order_requirements': 1120000, 'container_shortage': 271406, 'operation_number': 1836347}\n", - "env summary (episode 10, segment 1): {'order_requirements': 1120000, 'container_shortage': 253359, 'operation_number': 1797864}\n", - "env summary (episode 11, segment 1): {'order_requirements': 1120000, 'container_shortage': 285019, 'operation_number': 1764610}\n", - "env summary (episode 12, segment 1): {'order_requirements': 1120000, 'container_shortage': 290217, 'operation_number': 1775871}\n", - "env summary (episode 13, segment 1): {'order_requirements': 1120000, 'container_shortage': 291317, 'operation_number': 1696991}\n", - "env summary (episode 14, segment 1): {'order_requirements': 1120000, 'container_shortage': 363557, 'operation_number': 1584546}\n", - "env summary (episode 15, segment 1): {'order_requirements': 1120000, 'container_shortage': 276197, 'operation_number': 1702276}\n", - "env summary (episode 16, segment 1): {'order_requirements': 1120000, 'container_shortage': 449572, 'operation_number': 1343822}\n", - "env summary (episode 17, segment 1): {'order_requirements': 1120000, 'container_shortage': 569588, 'operation_number': 1060983}\n", - "env summary (episode 18, segment 1): {'order_requirements': 1120000, 'container_shortage': 477787, 'operation_number': 1269050}\n", - "env summary (episode 19, segment 1): {'order_requirements': 1120000, 'container_shortage': 675626, 'operation_number': 832979}\n", - "env summary (episode 20, segment 1): {'order_requirements': 1120000, 'container_shortage': 678845, 'operation_number': 809899}\n" + "env summary (episode 1, segment 1): {'order_requirements': 1120000, 'container_shortage': 755132, 'operation_number': 1955128}\n", + "env summary (episode 2, segment 1): {'order_requirements': 1120000, 'container_shortage': 541590, 'operation_number': 2147970}\n", + "env summary (episode 3, segment 1): {'order_requirements': 1120000, 'container_shortage': 430052, 'operation_number': 2279623}\n", + "env summary (episode 4, segment 1): {'order_requirements': 1120000, 'container_shortage': 282035, 'operation_number': 2063401}\n", + "env summary (episode 5, segment 1): {'order_requirements': 1120000, 'container_shortage': 142609, 'operation_number': 2208815}\n", + "env summary (episode 6, segment 1): {'order_requirements': 1120000, 'container_shortage': 102221, 'operation_number': 2217282}\n", + "env summary (episode 7, segment 1): {'order_requirements': 1120000, 'container_shortage': 156158, 'operation_number': 2138796}\n", + "env summary (episode 8, segment 1): {'order_requirements': 1120000, 'container_shortage': 149739, 'operation_number': 2174584}\n", + "env summary (episode 9, segment 1): {'order_requirements': 1120000, 'container_shortage': 174441, 'operation_number': 2053284}\n", + "env summary (episode 10, segment 1): {'order_requirements': 1120000, 'container_shortage': 126566, 'operation_number': 2158450}\n", + "env summary (episode 11, segment 1): {'order_requirements': 1120000, 'container_shortage': 179030, 'operation_number': 2034439}\n", + "env summary (episode 12, segment 1): {'order_requirements': 1120000, 'container_shortage': 133275, 'operation_number': 2235375}\n", + "env summary (episode 13, segment 1): {'order_requirements': 1120000, 'container_shortage': 196606, 'operation_number': 2067036}\n", + "env summary (episode 14, segment 1): {'order_requirements': 1120000, 'container_shortage': 190649, 'operation_number': 2174073}\n", + "env summary (episode 15, segment 1): {'order_requirements': 1120000, 'container_shortage': 202218, 'operation_number': 2009027}\n", + "env summary (episode 16, segment 1): {'order_requirements': 1120000, 'container_shortage': 244883, 'operation_number': 1986210}\n", + "env summary (episode 17, segment 1): {'order_requirements': 1120000, 'container_shortage': 359891, 'operation_number': 1658629}\n", + "env summary (episode 18, segment 1): {'order_requirements': 1120000, 'container_shortage': 356337, 'operation_number': 1769432}\n", + "env summary (episode 19, segment 1): {'order_requirements': 1120000, 'container_shortage': 289850, 'operation_number': 1742258}\n", + "env summary (episode 20, segment 1): {'order_requirements': 1120000, 'container_shortage': 327072, 'operation_number': 1677051}\n" ] } ], "source": [ "from maro.rl.rollout import SimpleAgentWrapper\n", - "from maro.rl.training import TrainerManager \n", + "from maro.rl.training import TrainingManager \n", "from maro.simulator import Env\n", "from maro.utils import set_seeds\n", "\n", @@ -375,7 +368,7 @@ " device=\"cpu\"\n", ")\n", "\n", - "trainer_manager = TrainerManager(policy_creator, trainer_creator, agent2policy)\n", + "training_manager = TrainingManager(policy_creator, trainer_creator, agent2policy)\n", "\n", "# main loop with 50 episodes\n", "for ep in range(1, 21):\n", @@ -387,8 +380,8 @@ " experiences = result[\"experiences\"]\n", " end_of_episode: bool = result[\"end_of_episode\"]\n", " print(f\"env summary (episode {ep}, segment {segment}): {env_sampler.get_env_metrics()}\")\n", - " trainer_manager.record_experiences(experiences)\n", - " trainer_manager.train()\n", + " training_manager.record_experiences(experiences)\n", + " training_manager.train_step()\n", " segment += 1" ] }, @@ -405,7 +398,7 @@ "hash": "8f57a09d39b50edfb56e79199ef40583334d721b06ead0e38a39e7e79092073c" }, "kernelspec": { - "display_name": "maro", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, From 00fbcee9256f42ec9cd89bc7d8e3572a6db201f4 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Thu, 31 Mar 2022 20:16:56 +0800 Subject: [PATCH 469/482] Cherry pick latest RL (#498) * Cherry pick * Remove SC related files --- docs/source/key_components/data_model.rst | 360 +------- docs/source/key_components/rl_toolkit.rst | 2 +- examples/{rl/cim => cim/rl}/README.md | 0 examples/{rl/cim => cim/rl}/__init__.py | 31 +- examples/cim/rl/algorithms/__init__.py | 0 examples/{rl/cim => cim/rl}/algorithms/ac.py | 14 +- examples/{rl/cim => cim/rl}/algorithms/dqn.py | 9 +- .../{rl/cim => cim/rl}/algorithms/maddpg.py | 1 - examples/cim/rl/algorithms/ppo.py | 25 + examples/{rl/cim => cim/rl}/callbacks.py | 1 - examples/{rl/cim => cim/rl}/config.py | 7 +- examples/{rl/cim => cim/rl}/env_sampler.py | 4 +- examples/cim/rl/policy_trainer.py | 33 + examples/citi_bike/greedy/__init__.py | 0 examples/citi_bike/online_lp/README.md | 6 +- examples/citi_bike/online_lp/__init__.py | 0 examples/rl/README.md | 7 +- examples/rl/cim/policy_trainer.py | 23 - examples/rl/main.py | 31 +- examples/rl/main_evaluation_only.py | 21 + examples/rl/scripts/docker/build.sh | 8 - .../rl/scripts/docker/docker_compose_yml.py | 236 ----- examples/rl/scripts/docker/kill.sh | 9 - examples/rl/scripts/docker/run.sh | 8 - examples/vm_scheduling/offline_lp/__init__.py | 0 examples/vm_scheduling/offline_lp/launcher.py | 4 +- .../rl}/README.md | 0 .../rl}/__init__.py | 2 +- .../vm_scheduling/rl/algorithms/__init__.py | 0 .../rl}/algorithms/ac.py | 13 +- .../rl}/algorithms/dqn.py | 9 +- .../rl}/callbacks.py | 0 .../rl}/config.py | 8 +- .../rl}/env_sampler.py | 8 +- .../rl}/policy_trainer.py | 0 .../rule_based_algorithm/__init__.py | 0 maro/cli/k8s/aks_commands.py | 851 ++++++++++++++++++ .../aks/create_aks_cluster/template.json | 28 +- maro/cli/k8s/test_conf.yml | 12 + maro/cli/k8s/test_parameters.json | 33 + maro/cli/k8s/test_template.json | 157 ++++ maro/cli/k8s/utils/params.py | 10 - maro/cli/local/commands.py | 1 - maro/cli/local/utils.py | 4 +- maro/cli/maro.py | 80 ++ maro/rl/policy/abs_policy.py | 71 +- maro/rl/policy/discrete_rl_policy.py | 10 +- maro/rl/rollout/env_sampler.py | 128 ++- maro/rl/training/algorithms/__init__.py | 2 + maro/rl/training/algorithms/ac.py | 289 +----- maro/rl/training/algorithms/base/__init__.py | 6 + .../training/algorithms/base/ac_ppo_base.py | 294 ++++++ maro/rl/training/algorithms/ddpg.py | 48 +- maro/rl/training/algorithms/dqn.py | 40 +- maro/rl/training/algorithms/maddpg.py | 105 +-- maro/rl/training/algorithms/ppo.py | 42 + maro/rl/training/train_ops.py | 43 +- maro/rl/training/trainer.py | 128 +-- maro/rl/training/training_manager.py | 46 +- maro/rl/training/worker.py | 18 +- maro/rl/utils/__init__.py | 4 +- maro/rl/utils/common.py | 13 +- maro/rl/utils/torch_utils.py | 6 +- maro/rl/utils/transition_batch.py | 5 + maro/rl/workflows/config/parser.py | 2 +- maro/rl/workflows/main.py | 36 +- maro/rl/workflows/rollout_worker.py | 8 +- maro/rl/workflows/scenario.py | 22 +- maro/simulator/scenarios/__init__.py | 1 - .../scenarios/abs_business_engine.py | 7 +- maro/utils/exception/error_code.py | 3 +- maro/utils/exception/rl_toolkit_exception.py | 11 +- maro/utils/logger.py | 89 +- requirements.dev.txt | 10 +- 74 files changed, 2147 insertions(+), 1396 deletions(-) rename examples/{rl/cim => cim/rl}/README.md (100%) rename examples/{rl/cim => cim/rl}/__init__.py (71%) create mode 100644 examples/cim/rl/algorithms/__init__.py rename examples/{rl/cim => cim/rl}/algorithms/ac.py (95%) rename examples/{rl/cim => cim/rl}/algorithms/dqn.py (96%) rename examples/{rl/cim => cim/rl}/algorithms/maddpg.py (99%) create mode 100644 examples/cim/rl/algorithms/ppo.py rename examples/{rl/cim => cim/rl}/callbacks.py (99%) rename examples/{rl/cim => cim/rl}/config.py (80%) rename examples/{rl/cim => cim/rl}/env_sampler.py (96%) create mode 100644 examples/cim/rl/policy_trainer.py create mode 100644 examples/citi_bike/greedy/__init__.py create mode 100644 examples/citi_bike/online_lp/__init__.py delete mode 100644 examples/rl/cim/policy_trainer.py create mode 100644 examples/rl/main_evaluation_only.py delete mode 100644 examples/rl/scripts/docker/build.sh delete mode 100644 examples/rl/scripts/docker/docker_compose_yml.py delete mode 100644 examples/rl/scripts/docker/kill.sh delete mode 100644 examples/rl/scripts/docker/run.sh create mode 100644 examples/vm_scheduling/offline_lp/__init__.py rename examples/{rl/vm_scheduling => vm_scheduling/rl}/README.md (100%) rename examples/{rl/vm_scheduling => vm_scheduling/rl}/__init__.py (94%) create mode 100644 examples/vm_scheduling/rl/algorithms/__init__.py rename examples/{rl/vm_scheduling => vm_scheduling/rl}/algorithms/ac.py (96%) rename examples/{rl/vm_scheduling => vm_scheduling/rl}/algorithms/dqn.py (97%) rename examples/{rl/vm_scheduling => vm_scheduling/rl}/callbacks.py (100%) rename examples/{rl/vm_scheduling => vm_scheduling/rl}/config.py (90%) rename examples/{rl/vm_scheduling => vm_scheduling/rl}/env_sampler.py (97%) rename examples/{rl/vm_scheduling => vm_scheduling/rl}/policy_trainer.py (100%) create mode 100644 examples/vm_scheduling/rule_based_algorithm/__init__.py create mode 100644 maro/cli/k8s/aks_commands.py create mode 100644 maro/cli/k8s/test_conf.yml create mode 100644 maro/cli/k8s/test_parameters.json create mode 100644 maro/cli/k8s/test_template.json delete mode 100644 maro/cli/k8s/utils/params.py create mode 100644 maro/rl/training/algorithms/base/__init__.py create mode 100644 maro/rl/training/algorithms/base/ac_ppo_base.py create mode 100644 maro/rl/training/algorithms/ppo.py diff --git a/docs/source/key_components/data_model.rst b/docs/source/key_components/data_model.rst index 03c2be0cf..89a36735d 100644 --- a/docs/source/key_components/data_model.rst +++ b/docs/source/key_components/data_model.rst @@ -573,10 +573,10 @@ Matrix is special that it only have one instance (index 0), and the value is sav # we can get the instance number of a node by calling the len method port_number = len(env.snapshot_list["port"]) - + # this is a 1 dim numpy array full_on_ports = env.snapshot_list["matrices"][tick::"full_on_ports"] - + # reshape it, then this is a 2 dim array that from port to port. full_on_ports = full_on_ports.reshape(port_number, port_number) @@ -1055,359 +1055,3 @@ type: int slots: 1 Number of machines that not in use in this zone. - - -supply_chain ------------- - -Supply chain is a special scenario that it support dynamic nodes in frame, it decide what -nodes to use in frame by configuration file, such if you defined only a supplier facility, then -following nodes will be added into frame (then in snapshot list): -consumer, manufacture, facility, product, storage, vehicle and distribution. - -And it only support dynamic backend for now, be careful about the shape of snapshot list querying result. - -We will list all the nodes below for a reference. - -Default settings for snapshot list -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Snapshot resolution: 1 - - -Max snapshot number: same as durations - -Nodes and attributes in scenario -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Nodes in supply chain scenario have a hierarchical structure, like a sku product unit may be composed with -seller, consumer and manufacture units, but frame (snapshot list) cannot support this, so we -flatten the structure, append the parent id to the data model, so all the nodes will contain: - -id -+++ - -Unit id, this is different with node index, this is unique between facilities and units in current world. - -facility_id -+++++++++++ - -Which facility current unit belongs to. - -And for seller, consumer, manufacture and product units, they are sku specified, and product units contains the other -3 units, so they all contains following attributes: - -product_id -++++++++++ - -Which product (sku) this unit related to, one sku per unit. - -product_unit_id -+++++++++++++++ - -Which product unit this unit belongs to. - - -Following is the special attribute for each node. - -storage -+++++++ - -remaining_space -*************** - -type: unsigned int -slots: 1 - -How many avaiable space in current storage. - -capacity -******** - -type: unsigned int -slots: 1 - -Max space of current storage, one unit space per product (sku) for now. - -product_list -************ -type: unsigned int list -slots: dynamic - -What product (id) can be save in current storage. - -product number -************** - -type: unsigned int list -slots: dynamic - -How many product in current storage, aligned with product_list, used as a key-value pair. - -vehicle -+++++++ - -payload -******* - -type: unsigned int -slots: 1 - -How many product current vehicle carring. - -unit_transport_cost - -type: float -slots: 1 - -Transport cost per product. - -distribution -++++++++++++ - -remaining_order_quantity -************************ - -type: unsigned int -slots: 1 - -Sum of product number in current order list for now. - -remaining_order_number -********************** - -type: unsigned int -slots: 1 - -How many pending order for now. - -consumer -++++++++ - -total_purchased -*************** - -type: unsigned int -slots: 1 - -How many products this node purchased from start to now. - -total_received -************** - -type: unsigned int -slots: 1 - -How many products this node received from start to now. - -purchased -********* - -type: unsigned int -slots: 1 - -Per tick state. How many products purchased at current tick. - -received -******** - -type: unsigned int -slots: 1 - -Per tick state. How many products received from source facility at current tick. - -order_product_cost -****************** - -type: unsigned int -slots: 1 - -Cost per product to order. - -latest_consumptions -******************* - -type: float -slots: 1 - -Per tick states. Consumption of current tick, 1.0 if there is purchase, or 0. - -order_quantity -************** - -type: unsigned int -slots: 1 - -How many product to order, from action. - -price -***** - -type: float -slots: 1 - -Price per product. - -order_cost -********** - -type: float -slots: 1 - -Cost per order. - -reward_discount -*************** - -type: float -slots: 1 - -Reward discount from action. - -manufacture -+++++++++++ - -manufacturing_number -******************** - -type: unsigned int -slots: 1 - -How many products being produced at current tick, controlled by action. - -product_unit_cost -***************** - -type: float -slots: 1 - -Cost to procedue a product. - -seller -++++++ - -total_sold -********** - -type: unsigned int -slots: 1 - -Total products sold number from start to now. - -demand -****** - -type: unsigned int -slots: 1 - -Per tick state. Product demand at current tick. - -sold -**** - -type: unsigned int -slots: 1 - -Per tick state. How many products sold at current tick. - -total_demand -************ - -type: unsigned int -slots: 1 - -Total products demand from start to now. - -price -***** - -type: float -slots: 1 - -Product price. - -backlog_ratio -************* - -type: float -slots: 1 - -Backlog factor for current product. - -product -+++++++ - -price -***** - -type: float -slots: 1 - -Current product price. - -How to -~~~~~~ - -How to use result of dynamic backend? -************************************* - -Supply chain scenario only support dynamic backend for now, so the result from snapshot list will -be a 4 dim (tick, node, attribute, slots) Numpy array. - -.. code-block:: python - - # we want to query sold and demand history for sellers - hist_ticks = (0, 1, 2, 4) - - states = env.snapshot_list["seller"][hist_ticks::("sold", "demand")] - - # this states will a 4 dim result as below - """ - [ - [ - [ - [0.0], # sold (slot = 1) - [0.0] # demand (slot = 1) - ], # seller 0 - [...] # seller 1 - ], # tick 0 - [ - [...], - [...] - ], # tick 1 - [ - [...], - [...] - ], # tick 2 - [ - [...], - [...] - ] # tick 3 (latest) - ] - """ - - # assuming we want to sold history for seller 0 (node index) - cur_seller_hist = states[:, 0] - - sale_hist = cur_seller_hist[:, 0].flatten() - demand_hist = cur_seller_hist[:, 1].flatten() - -How to query a dyanmic list attribute? -************************************** - -With dyanmic backend, we cannot mix a list attribute and a normal attribute to query, we have to -query, it only support one tick, one node and one attribute querying. - -.. code-block:: python - - # quanry product and its number in a storage - - tick = 0 - node_index = 0 - storage_ss = env.snapshot_list["storage"] - - # we have to query them with 2 statements. - product_list = storage_ss[tick:node_index:"product_list"].flatten().astype(np.int) - product_number = storage_ss["storage"][tick:node_index:"product_number"].flatten().astype(np.int) - - product_level = {pid: pnum for pid, pnum in zip(product_list, product_number)} \ No newline at end of file diff --git a/docs/source/key_components/rl_toolkit.rst b/docs/source/key_components/rl_toolkit.rst index 9a0805a1c..16ec97bda 100644 --- a/docs/source/key_components/rl_toolkit.rst +++ b/docs/source/key_components/rl_toolkit.rst @@ -143,6 +143,7 @@ in the memory to train the policies. Like ``RLPolicy``, trainers are also concre be used by configuring parameters. Currently, we have 4 trainers (algorithms) in MARO: - ``DiscreteActorCriticTrainer``: Actor-critic algorithm for policies that generate discrete actions. +- ``DiscretePPOTrainer``: PPO algorithm for policies that generate discrete actions. - ``DDPGTrainer``: DDPG algorithm for policies that generate continuous actions. - ``DQNTrainer``: DQN algorithm for policies that generate discrete actions. - ``DiscreteMADDPGTrainer``: MADDPG algorithm for policies that generate discrete actions. @@ -169,7 +170,6 @@ An example of creating an actor-critic trainer: DiscreteActorCriticTrainer( name='ac', params=DiscreteActorCriticParams( - device="cpu", get_v_critic_net_func=lambda: MyCriticNet(state_dim=128), reward_discount=.0, grad_iters=10, diff --git a/examples/rl/cim/README.md b/examples/cim/rl/README.md similarity index 100% rename from examples/rl/cim/README.md rename to examples/cim/rl/README.md diff --git a/examples/rl/cim/__init__.py b/examples/cim/rl/__init__.py similarity index 71% rename from examples/rl/cim/__init__.py rename to examples/cim/rl/__init__.py index 887c63e0b..af4ce720f 100644 --- a/examples/rl/cim/__init__.py +++ b/examples/cim/rl/__init__.py @@ -1,15 +1,16 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from .callbacks import post_collect, post_evaluate -from .env_sampler import agent2policy, env_sampler_creator -from .policy_trainer import policy_creator, trainer_creator - -__all__ = [ - "agent2policy", - "env_sampler_creator", - "policy_creator", - "post_collect", - "post_evaluate", - "trainer_creator" -] +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .callbacks import post_collect, post_evaluate +from .env_sampler import agent2policy, env_sampler_creator +from .policy_trainer import device_mapping, policy_creator, trainer_creator + +__all__ = [ + "agent2policy", + "device_mapping", + "env_sampler_creator", + "policy_creator", + "post_collect", + "post_evaluate", + "trainer_creator", +] diff --git a/examples/cim/rl/algorithms/__init__.py b/examples/cim/rl/algorithms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/rl/cim/algorithms/ac.py b/examples/cim/rl/algorithms/ac.py similarity index 95% rename from examples/rl/cim/algorithms/ac.py rename to examples/cim/rl/algorithms/ac.py index 9e11b6a85..869968c8f 100644 --- a/examples/rl/cim/algorithms/ac.py +++ b/examples/cim/rl/algorithms/ac.py @@ -10,13 +10,12 @@ from maro.rl.policy import DiscretePolicyGradient from maro.rl.training.algorithms import DiscreteActorCriticTrainer, DiscreteActorCriticParams - actor_net_conf = { "hidden_dims": [256, 128, 64], "activation": torch.nn.Tanh, "softmax": True, "batch_norm": False, - "head": True + "head": True, } critic_net_conf = { "hidden_dims": [256, 128, 64], @@ -24,7 +23,7 @@ "activation": torch.nn.LeakyReLU, "softmax": False, "batch_norm": True, - "head": True + "head": True, } actor_learning_rate = 0.001 critic_learning_rate = 0.001 @@ -63,7 +62,7 @@ def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: def get_state(self) -> dict: return { "network": self.state_dict(), - "optim": self._optim.state_dict() + "optim": self._optim.state_dict(), } def set_state(self, net_state: dict) -> None: @@ -98,7 +97,7 @@ def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: def get_state(self) -> dict: return { "network": self.state_dict(), - "optim": self._optim.state_dict() + "optim": self._optim.state_dict(), } def set_state(self, net_state: dict) -> None: @@ -120,12 +119,11 @@ def get_ac(state_dim: int, name: str) -> DiscreteActorCriticTrainer: return DiscreteActorCriticTrainer( name=name, params=DiscreteActorCriticParams( - device="cpu", get_v_critic_net_func=lambda: MyCriticNet(state_dim), reward_discount=.0, grad_iters=10, critic_loss_cls=torch.nn.SmoothL1Loss, min_logp=None, - lam=.0 - ) + lam=.0, + ), ) diff --git a/examples/rl/cim/algorithms/dqn.py b/examples/cim/rl/algorithms/dqn.py similarity index 96% rename from examples/rl/cim/algorithms/dqn.py rename to examples/cim/rl/algorithms/dqn.py index 9e8d7fcc5..edefcfa9d 100644 --- a/examples/rl/cim/algorithms/dqn.py +++ b/examples/cim/rl/algorithms/dqn.py @@ -18,7 +18,7 @@ "batch_norm": True, "skip_connection": False, "head": True, - "dropout_p": 0.0 + "dropout_p": 0.0, } learning_rate = 0.05 @@ -75,7 +75,7 @@ def get_policy(state_dim: int, action_num: int, name: str) -> ValueBasedPolicy: "final_value": 0.0, } )], - warmup=100 + warmup=100, ) @@ -83,7 +83,6 @@ def get_dqn(name: str) -> DQNTrainer: return DQNTrainer( name=name, params=DQNParams( - device="cpu", reward_discount=.0, update_target_every=5, num_epochs=10, @@ -91,6 +90,6 @@ def get_dqn(name: str) -> DQNTrainer: double=False, replay_memory_capacity=10000, random_overwrite=False, - batch_size=32 - ) + batch_size=32, + ), ) diff --git a/examples/rl/cim/algorithms/maddpg.py b/examples/cim/rl/algorithms/maddpg.py similarity index 99% rename from examples/rl/cim/algorithms/maddpg.py rename to examples/cim/rl/algorithms/maddpg.py index e47603a1e..a077189e0 100644 --- a/examples/rl/cim/algorithms/maddpg.py +++ b/examples/cim/rl/algorithms/maddpg.py @@ -126,7 +126,6 @@ def get_maddpg(state_dim: int, action_dims: List[int], name: str) -> DiscreteMAD return DiscreteMADDPGTrainer( name=name, params=DiscreteMADDPGParams( - device="cpu", reward_discount=.0, num_epoch=10, get_q_critic_net_func=partial(get_multi_critic_net, state_dim, action_dims), diff --git a/examples/cim/rl/algorithms/ppo.py b/examples/cim/rl/algorithms/ppo.py new file mode 100644 index 000000000..2694decd3 --- /dev/null +++ b/examples/cim/rl/algorithms/ppo.py @@ -0,0 +1,25 @@ +import torch + +from maro.rl.policy import DiscretePolicyGradient +from maro.rl.training.algorithms import DiscretePPOParams, DiscretePPOTrainer + +from .ac import MyActorNet, MyCriticNet + + +def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: + return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) + + +def get_ppo(state_dim: int, name: str) -> DiscretePPOTrainer: + return DiscretePPOTrainer( + name=name, + params=DiscretePPOParams( + get_v_critic_net_func=lambda: MyCriticNet(state_dim), + reward_discount=.0, + grad_iters=10, + critic_loss_cls=torch.nn.SmoothL1Loss, + min_logp=None, + lam=.0, + clip_ratio=0.1, + ), + ) diff --git a/examples/rl/cim/callbacks.py b/examples/cim/rl/callbacks.py similarity index 99% rename from examples/rl/cim/callbacks.py rename to examples/cim/rl/callbacks.py index e1308c25e..85c7a2516 100644 --- a/examples/rl/cim/callbacks.py +++ b/examples/cim/rl/callbacks.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - def post_collect(info_list: list, ep: int, segment: int) -> None: # print the env metric from each rollout worker for info in info_list: diff --git a/examples/rl/cim/config.py b/examples/cim/rl/config.py similarity index 80% rename from examples/rl/cim/config.py rename to examples/cim/rl/config.py index 202b608cf..34dcd8f76 100644 --- a/examples/rl/cim/config.py +++ b/examples/cim/rl/config.py @@ -7,6 +7,11 @@ "durations": 560 } +if env_conf["topology"].startswith("toy"): + num_agents = int(env_conf["topology"].split(".")[1][0]) +else: + num_agents = int(env_conf["topology"].split(".")[1][:2]) + port_attributes = ["empty", "full", "on_shipper", "on_consignee", "booking", "shortage", "fulfillment"] vessel_attributes = ["empty", "full", "remaining_space"] @@ -34,4 +39,4 @@ + len(vessel_attributes) ) -algorithm = "ac" # ac, dqn or discrete_maddpg +algorithm = "ppo" # ac, ppo, dqn or discrete_maddpg diff --git a/examples/rl/cim/env_sampler.py b/examples/cim/rl/env_sampler.py similarity index 96% rename from examples/rl/cim/env_sampler.py rename to examples/cim/rl/env_sampler.py index 7b46d0857..4c542c8ef 100644 --- a/examples/rl/cim/env_sampler.py +++ b/examples/cim/rl/env_sampler.py @@ -77,6 +77,9 @@ def _get_reward(self, env_action_dict: Dict[Any, object], event: DecisionEvent, def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: self._info["env_metric"] = self._env.metrics + def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: + self._post_step(cache_element, reward) + agent2policy = {agent: f"{algorithm}_{agent}.policy" for agent in Env(**env_conf).agent_idx_list} @@ -86,5 +89,4 @@ def env_sampler_creator(policy_creator: Dict[str, Callable[[str], RLPolicy]]) -> get_env=lambda: Env(**env_conf), policy_creator=policy_creator, agent2policy=agent2policy, - device="cpu", ) diff --git a/examples/cim/rl/policy_trainer.py b/examples/cim/rl/policy_trainer.py new file mode 100644 index 000000000..44d8c2c92 --- /dev/null +++ b/examples/cim/rl/policy_trainer.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from functools import partial + +from .config import algorithm, action_shaping_conf, num_agents, state_dim + +action_num = len(action_shaping_conf["action_space"]) + +if algorithm == "ac": + from .algorithms.ac import get_ac, get_policy + policy_creator = {f"ac_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(num_agents)} + trainer_creator = {f"ac_{i}": partial(get_ac, state_dim) for i in range(num_agents)} +elif algorithm == "ppo": + from .algorithms.ppo import get_ppo, get_policy + policy_creator = {f"ppo_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(num_agents)} + trainer_creator = {f"ppo_{i}": partial(get_ppo, state_dim) for i in range(num_agents)} +elif algorithm == "dqn": + from .algorithms.dqn import get_dqn, get_policy + policy_creator = {f"dqn_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(num_agents)} + trainer_creator = {f"dqn_{i}": partial(get_dqn) for i in range(num_agents)} +elif algorithm == "discrete_maddpg": + from .algorithms.maddpg import get_policy, get_maddpg + policy_creator = { + f"discrete_maddpg_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(num_agents) + } + trainer_creator = {f"discrete_maddpg_{i}": partial(get_maddpg, state_dim, [1]) for i in range(num_agents)} +else: + raise ValueError(f"Unsupported algorithm: {algorithm}") + +device_mapping = { + policy_name: "cpu" for policy_name in policy_creator +} diff --git a/examples/citi_bike/greedy/__init__.py b/examples/citi_bike/greedy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/citi_bike/online_lp/README.md b/examples/citi_bike/online_lp/README.md index 3cedc729d..9766a460c 100644 --- a/examples/citi_bike/online_lp/README.md +++ b/examples/citi_bike/online_lp/README.md @@ -99,7 +99,7 @@ demand is 34 (at a specific station, during a time interval of 20 minutes), the corresponding demand distribution shows that demand exceeding 10 bikes per time interval (20 minutes) is only 2%. -![Demand Distribution Between Tick 2400 ~ Tick 2519](./LogDemand.ny201910.2400.png) +![Demand Distribution Between Tick 2400 ~ Tick 2519](LogDemand.ny201910.2400.png) Besides, we can also find that the percentage of forecasting results that differ to the data extracted from trip log is not low. To dive deeper in the practical @@ -110,9 +110,9 @@ show the distribution of the forecasting difference to the trip log. One for the interval with the *Max Diff* (16:00-18:00), one for the interval with the highest percentage of *Diff > 5* (10:00-12:00). -![Demand Distribution Between Tick 2400 ~ Tick 2519](./DemandDiff.ny201910.2400.png) +![Demand Distribution Between Tick 2400 ~ Tick 2519](DemandDiff.ny201910.2400.png) -![Demand Distribution Between Tick 2040 ~ Tick 2159](./DemandDiff.ny201910.2040.png) +![Demand Distribution Between Tick 2040 ~ Tick 2159](DemandDiff.ny201910.2040.png) Maybe due to the *sparse* and *small* trip demand, and the *small* difference between the forecasting results and data extracted from the trip log data, the diff --git a/examples/citi_bike/online_lp/__init__.py b/examples/citi_bike/online_lp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/rl/README.md b/examples/rl/README.md index 59312ad6c..42a565069 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -4,14 +4,17 @@ This folder contains scenarios that employ reinforcement learning. MARO's RL too ## How to Run -The ``main.py`` script can be used to run the scenarios under ``examples/rl`` or any user-defined scenario that provides the necessary components (see the section below for details) . To choose a scenario, edit ``SCENARIO_PATH`` in the script to point to the desired scenario folder. You may also edit the rest of the config variables to your own preference. Note that this script runs in single-thread mode only. +The ``main.py`` script can be used to run the scenarios under ``examples/SCENARIO_NAME/rl`` or any user-defined scenario that provides the necessary components (see the section below for details) . To choose a scenario, edit ``SCENARIO_PATH`` in the script to point to the desired scenario folder. You may also edit the rest of the config variables to your own preference. Note that this script runs in single-thread mode only. To run a scenario in multi-process mode on a local machine, you will need to use the CLI tool (which requires MARO [installation from the source](https://github.com/microsoft/maro#install-maro-from-pypi)). Start by creating a configuration file (.yml) that follows the template ``maro/maro/rl/workflows/config/template.yml`` to specify the scenario-independent settings. Then use the command ``maro local run [-c] path/to/your/config`` to run in containerized (with ``-c``) or non-containerized (without ``-c``) environments. +The ``main_evaluation_only.py`` works similarly to ``main.py``. The difference is that it only execute the evaluation workflow without trying to train policies, which means it can be directly used for Non-RL policies. To run ``main_evaluation_only.py``, you only need to implemenent ``agent2policy``, ``policy_creator``, and ``env_sampler_creator`` in the scenario folder (see details below). + ## Create Your Own Scenarios You can create your own scenarios by supplying the necessary ingredients without worrying about putting them together in a workflow. It is necessary to create an ``__init__.py`` under your scenario folder (so that it can be treated as a package) and expose all ingredients in it. The ingredients include: -* Definitions of policies and agent-to-policy mappings. These definitions should be provided as a dictionary named ``policy_creator`` that maps a name to a function that takes the name and returns a policy instance with that name. The agent-to-policy mapping should be provided as a dictionary named ``agent2policy``. +* Definitions of policies and agent-to-policy mappings. These definitions should be provided as a dictionary named ``policy_creator`` that maps a name to a function that takes the name and returns a policy instance with that name. Optionally, you may specify which policies you intend to train by providing ``trainable_policies``, which a list of policy names. The experiences generated by these policies will be recorded by the environment sampler and used for training. The agent-to-policy mapping should be provided as a dictionary named ``agent2policy``. * Definitions of training algorithms. These definitions should be provided as a dictionary named ``trainer_creator`` that maps a name to a function that takes the name and returns a trainer instance with that name. * Definitions of state, action and reward shaping logic pertinent to your simulator and policies. These definitions should be encapsulated in ``env_sampler_creator``, which is a function that takes ``policy_creator`` and returns an environment sampler; It is possible to have customized routines invoked at the end of a roll-out episode or episode segment. These routines usually involve processing and / or rendering information collected during roll-out. To do this, first implement the ``post_step`` method in your environment sampler class to record whatever information you wish to keep track of during roll-out. Then create functions named ``post_collect`` and ``post_evaluate`` to process the information and expose them in the scenario folder's ``__init__.py``. These functions are used as callbacks in the main learning loop and executed at the end of each training or evaluation episode. See ``cim/callbacks.py`` for a simple example of how to create these functions. +* An optional dictionary named ``device_mapping`` that specifies the compute device (CPU or GPU) for each policy. If not provided, all computations will be performed on the CPU. \ No newline at end of file diff --git a/examples/rl/cim/policy_trainer.py b/examples/rl/cim/policy_trainer.py deleted file mode 100644 index 0e011a11c..000000000 --- a/examples/rl/cim/policy_trainer.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from functools import partial - -from .config import algorithm, action_shaping_conf, state_dim - -action_num = len(action_shaping_conf["action_space"]) - -if algorithm == "ac": - from .algorithms.ac import get_ac, get_policy - policy_creator = {f"ac_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(4)} - trainer_creator = {f"ac_{i}": partial(get_ac, state_dim) for i in range(4)} -elif algorithm == "dqn": - from .algorithms.dqn import get_dqn, get_policy - policy_creator = {f"dqn_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(4)} - trainer_creator = {f"dqn_{i}": get_dqn for i in range(4)} -elif algorithm == "discrete_maddpg": - from .algorithms.maddpg import get_policy, get_maddpg - policy_creator = {f"discrete_maddpg_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(4)} - trainer_creator = {f"discrete_maddpg_{i}": partial(get_maddpg, state_dim, [1]) for i in range(4)} -else: - raise ValueError(f"Unsupported algorithm: {algorithm}") diff --git a/examples/rl/main.py b/examples/rl/main.py index 6feb8b4ff..d65b38d19 100644 --- a/examples/rl/main.py +++ b/examples/rl/main.py @@ -1,20 +1,21 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import os +from os.path import dirname, join, realpath from maro.rl.training import TrainingManager from maro.rl.workflows.scenario import Scenario from maro.utils import LoggerV2 # config variables -SCENARIO_PATH = "cim" +SCENARIO_NAME = "cim" +SCENARIO_PATH = join(dirname(dirname(realpath(__file__))), SCENARIO_NAME, "rl") NUM_EPISODES = 50 NUM_STEPS = None -CHECKPOINT_PATH = os.path.join(os.getcwd(), "checkpoints") +CHECKPOINT_PATH = join(dirname(SCENARIO_PATH), "checkpoints") CHECKPOINT_INTERVAL = 5 EVAL_SCHEDULE = [10, 20, 30, 40, 50] -LOG_PATH = os.path.join(os.getcwd(), "logs", "cim") +LOG_PATH = join(dirname(SCENARIO_PATH), "logs", SCENARIO_NAME) if __name__ == "__main__": @@ -23,16 +24,30 @@ agent2policy = scenario.agent2policy policy_creator = scenario.policy_creator - trainer_creator = scenario.trainer_creator policy_dict = {name: get_policy_func(name) for name, get_policy_func in policy_creator.items()} policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict} + trainer_creator = scenario.trainer_creator # evaluation schedule logger.info(f"Policy will be evaluated at the end of episodes {EVAL_SCHEDULE}") eval_point_index = 0 - env_sampler = scenario.get_env_sampler(policy_creator) - training_manager = TrainingManager(policy_creator, trainer_creator, agent2policy, logger=logger) + if scenario.trainable_policies is None: + trainable_policies = set(policy_creator.keys()) + else: + trainable_policies = set(scenario.trainable_policies) + + env_sampler = scenario.env_sampler_creator(policy_creator) + + trainable_policy_creator = {name: func for name, func in policy_creator.items() if name in trainable_policies} + trainable_agent2policy = {id_: name for id_, name in agent2policy.items() if name in trainable_policies} + training_manager = TrainingManager( + trainable_policy_creator, + trainer_creator, + trainable_agent2policy, + device_mapping=scenario.device_mapping, + logger=logger + ) # main loop for ep in range(1, NUM_EPISODES + 1): @@ -51,7 +66,7 @@ training_manager.record_experiences(experiences) training_manager.train_step() if CHECKPOINT_PATH and ep % CHECKPOINT_INTERVAL == 0: - pth = os.path.join(CHECKPOINT_PATH, str(ep)) + pth = join(CHECKPOINT_PATH, str(ep)) training_manager.save(pth) logger.info(f"All trainer states saved under {pth}") segment += 1 diff --git a/examples/rl/main_evaluation_only.py b/examples/rl/main_evaluation_only.py new file mode 100644 index 000000000..33b73868a --- /dev/null +++ b/examples/rl/main_evaluation_only.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from os.path import dirname, join, realpath + +from maro.rl.workflows.scenario import Scenario + +# config variables +SCENARIO_NAME = "cim" +SCENARIO_PATH = join(dirname(dirname(realpath(__file__))), SCENARIO_NAME, "rl") + +if __name__ == "__main__": + scenario = Scenario(SCENARIO_PATH) + policy_creator = scenario.policy_creator + policy_dict = {name: get_policy_func(name) for name, get_policy_func in policy_creator.items()} + policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict} + + env_sampler = scenario.env_sampler_creator(policy_creator) + result = env_sampler.eval() + if scenario.post_evaluate: + scenario.post_evaluate(result["info"], 0) diff --git a/examples/rl/scripts/docker/build.sh b/examples/rl/scripts/docker/build.sh deleted file mode 100644 index f3240b5aa..000000000 --- a/examples/rl/scripts/docker/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -BASEDIR=$(dirname "$0") -ROOTDIR=$BASEDIR/../../../ - -# script to build the docker image for running the CIM scenario. -docker pull redis:6 -docker build -f $ROOTDIR/docker_files/dev.df -t marorl:latest $ROOTDIR \ No newline at end of file diff --git a/examples/rl/scripts/docker/docker_compose_yml.py b/examples/rl/scripts/docker/docker_compose_yml.py deleted file mode 100644 index 90c56bba0..000000000 --- a/examples/rl/scripts/docker/docker_compose_yml.py +++ /dev/null @@ -1,236 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import argparse -import yaml -from copy import deepcopy -from os.path import dirname, join, realpath - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--namespace', help="job namespace", default="maro") - args = parser.parse_args() - - namespace = args.namespace - docker_script_dir = dirname(realpath(__file__)) - example_dir = dirname(dirname(docker_script_dir)) - root_dir = dirname(dirname(example_dir)) - maro_rl_dir = join(root_dir, "maro", "rl") - - with open(join(example_dir, "config.yml"), "r") as fp: - config = yaml.safe_load(fp) - - common_spec = { - "build": {"context": root_dir, "dockerfile": join(root_dir, "docker_files", "dev.df")}, - "image": "marorl", - "volumes": [ - f"{example_dir}:/maro/examples", - f"{maro_rl_dir}:/maro/maro/rl", - ] - } - - workflow_dir_in_container = "/maro/maro/rl/workflows" - # get script paths - if "scripts" in config and "main" in config["scripts"]: - main_path = config["scripts"]["main"] - else: - main_path = join(workflow_dir_in_container, "main.py") - - if "scripts" in config and "rollout_worker" in config["scripts"]: - rollout_worker_path = config["scripts"]["rollout_worker"] - else: - rollout_worker_path = join(workflow_dir_in_container, "rollout.py") - - if "scripts" in config and "policy_host" in config["scripts"]: - policy_host_path = config["scripts"]["policy_host"] - else: - policy_host_path = join(workflow_dir_in_container, "policy_host.py") - - if "scripts" in config and "policy_server" in config["scripts"]: - policy_server_path = config["scripts"]["policy_server"] - else: - policy_server_path = join(workflow_dir_in_container, "policy_manager.py") - - if "scripts" in config and "actor" in config["scripts"]: - actor_path = config["scripts"]["actor"] - else: - actor_path = join(workflow_dir_in_container, "rollout.py") - - if "scripts" in config and "grad_worker" in config["scripts"]: - grad_worker_path = config["scripts"]["grad_worker"] - else: - grad_worker_path = join(workflow_dir_in_container, "grad_worker.py") - - if "scripts" in config and "task_queue" in config["scripts"]: - task_queue_path = config["scripts"]["task_queue"] - else: - task_queue_path = join(workflow_dir_in_container, "task_queue.py") - - if config["mode"] == "single": - envs = [ - f"JOB={config['job']}", - f"SCENARIODIR={config['scenario_dir']}", - f"MODE=single", - f"NUMEPISODES={config['num_episodes']}", - f"EVALSCH={config['eval_schedule']}" - ] - if "num_steps" in config: - envs.append(f"NUMSTEPS={config['num_steps']}") - if "load_policy_dir" in config: - envs.append(f"LOADDIR={config['load_policy_dir']}") - if "checkpoint_dir" in config: - envs.append(f"CHECKPOINTDIR={config['checkpoint_dir']}") - if "log_dir" in config: - envs.append(f"LOGDIR={config['log_dir']}") - - docker_compose_manifest = {"services": { - "main": { - **common_spec, - **{ - "container_name": f"{namespace}.main", - "command": f"python3 {main_path}", - "environment": envs - } - } - }} - else: - redis_host = config["redis"]["host"] - docker_compose_manifest = { - "version": "3.9", - "services": {"redis": {"image": "redis:6", "container_name": f"{namespace}.{redis_host}"}} - } - - policy_group = "-".join([config['job'], 'policies']) - common_envs = [ - f"REDISHOST={namespace}.{redis_host}", - f"REDISPORT={config['redis']['port']}", - f"JOB={config['job']}", - f"SCENARIODIR={config['scenario_dir']}", - f"MODE={config['mode']}", - f"POLICYMANAGERTYPE={config['policy_manager']['type']}" - ] - - if "load_policy_dir" in config: - common_envs.append(f"LOADDIR={config['load_policy_dir']}") - if "checkpoint_dir" in config: - common_envs.append(f"CHECKPOINTDIR={config['checkpoint_dir']}") - if "log_dir" in config: - common_envs.append(f"LOGDIR={config['log_dir']}") - if "data_parallelism" in config: - common_envs.append(f"DATAPARALLELISM={config['data_parallelism']}") - if config["policy_manager"]["type"] == "distributed": - common_envs.append(f"POLICYGROUP={policy_group}") - common_envs.append(f"NUMHOSTS={config['policy_manager']['distributed']['num_hosts']}") - - # grad worker config - if "data_parallelism" in config and config["data_parallelism"] > 1: - # task queue - str_id = "task_queue" - task_queue_spec = deepcopy(common_spec) - del task_queue_spec["build"] - task_queue_spec["command"] = f"python3 {task_queue_path}" - task_queue_spec["container_name"] = f"{namespace}.{str_id}" - task_queue_spec["environment"] = common_envs - docker_compose_manifest["services"][str_id] = task_queue_spec - - # grad worker - for worker_id in range(config['data_parallelism']): - str_id = f"grad_worker.{worker_id}" - grad_worker_spec = deepcopy(common_spec) - del grad_worker_spec["build"] - grad_worker_spec["command"] = f"python3 {grad_worker_path}" - grad_worker_spec["container_name"] = f"{namespace}.{str_id}" - grad_worker_spec["environment"] = [f"WORKERID={worker_id}"] + common_envs - docker_compose_manifest["services"][str_id] = grad_worker_spec - - # host spec - if config["policy_manager"]["type"] == "distributed": - for host_id in range(config["policy_manager"]["distributed"]["num_hosts"]): - str_id = f"policy_host.{host_id}" - host_spec = deepcopy(common_spec) - del host_spec["build"] - host_spec["command"] = f"python3 {policy_host_path}" - host_spec["container_name"] = f"{namespace}.{str_id}" - host_spec["environment"] = [f"HOSTID={host_id}"] + common_envs - docker_compose_manifest["services"][str_id] = host_spec - - mode = config["mode"] - if mode == "sync": - # main process spec - rollout_group = "-".join([config['job'], 'rollout']) - envs = [ - f"ROLLOUTTYPE={config['sync']['rollout_type']}", - f"NUMEPISODES={config['num_episodes']}", - f"NUMROLLOUTS={config['sync']['num_rollouts']}" - ] - if "num_steps" in config: - envs.append(f"NUMSTEPS={config['num_steps']}") - if "eval_schedule" in config: - envs.append(f"EVALSCH={config['eval_schedule']}") - if config["sync"]["rollout_type"] == "distributed": - envs.append(f"ROLLOUTGROUP={rollout_group}") - if "min_finished_workers" in config["sync"]["distributed"]: - envs.append(f"MINFINISH={config['sync']['distributed']['min_finished_workers']}") - if "max_extra_recv_tries" in config["sync"]["distributed"]: - envs.append(f"MAXEXRECV={config['sync']['distributed']['max_extra_recv_tries']}") - if "extra_recv_timeout" in config["sync"]["distributed"]: - envs.append(f"MAXRECVTIMEO={config['sync']['distributed']['extra_recv_timeout']}") - - if "num_eval_rollouts" in config["sync"]: - envs.append(f"NUMEVALROLLOUTS={config['sync']['num_eval_rollouts']}") - - docker_compose_manifest["services"]["main"] = { - **common_spec, - **{ - "container_name": f"{namespace}.main", - "command": f"python3 {main_path}", - "environment": envs + common_envs - } - } - # rollout worker spec - if config["sync"]["rollout_type"] == "distributed": - for worker_id in range(config["sync"]["num_rollouts"]): - str_id = f"rollout_worker.{worker_id}" - worker_spec = deepcopy(common_spec) - del worker_spec["build"] - worker_spec["command"] = f"python3 {rollout_worker_path}" - worker_spec["container_name"] = f"{namespace}.{str_id}" - worker_spec["environment"] = [f"WORKERID={worker_id}", f"ROLLOUTGROUP={rollout_group}"] + common_envs - docker_compose_manifest["services"][str_id] = worker_spec - elif mode == "async": - # policy server spec - envs = [ - f"GROUP={config['job']}", - f"NUMROLLOUTS={config['async']['num_rollouts']}" - ] - if "max_lag" in config["async"]: - envs.append(f"MAXLAG={config['async']['max_lag']}") - docker_compose_manifest["services"]["policy_server"] = { - **common_spec, - **{ - "container_name": f"{namespace}.policy_server", - "command": f"python3 {policy_server_path}", - "environment": envs + common_envs - } - } - # actor spec - for actor_id in range(config["async"]["num_rollouts"]): - str_id = f"actor.{actor_id}" - actor_spec = deepcopy(common_spec) - del actor_spec["build"] - actor_spec["command"] = f"python3 {actor_path}" - actor_spec["container_name"] = f"{namespace}.{str_id}" - actor_spec["environment"] = [ - f"ACTORID={actor_id}", - f"GROUP={config['job']}", - f"NUMEPISODES={config['num_episodes']}" - ] + common_envs - if "num_steps" in config: - actor_spec["environment"].append(f"NUMSTEPS={config['num_steps']}") - docker_compose_manifest["services"][str_id] = actor_spec - else: - raise ValueError(f"mode must be 'sync' or 'async', got {mode}") - - with open(join(docker_script_dir, "yq.yml"), "w") as fp: - yaml.safe_dump(docker_compose_manifest, fp) diff --git a/examples/rl/scripts/docker/kill.sh b/examples/rl/scripts/docker/kill.sh deleted file mode 100644 index b15c69231..000000000 --- a/examples/rl/scripts/docker/kill.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -NAMESPACE=${1:-maro} -BASEDIR=$(dirname "$0") - -# script to kill a previously launched training job. -docker-compose -f $BASEDIR/yq.yml --project-name $NAMESPACE down - -rm $BASEDIR/yq.yml \ No newline at end of file diff --git a/examples/rl/scripts/docker/run.sh b/examples/rl/scripts/docker/run.sh deleted file mode 100644 index 41fa10ac9..000000000 --- a/examples/rl/scripts/docker/run.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -NAMESPACE=${1:-maro} -BASEDIR=$(dirname "$0") - -# script to run the multi-container mode. -python3 $BASEDIR/docker_compose_yml.py --namespace $NAMESPACE -docker-compose -f $BASEDIR/yq.yml --project-name $NAMESPACE up \ No newline at end of file diff --git a/examples/vm_scheduling/offline_lp/__init__.py b/examples/vm_scheduling/offline_lp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/vm_scheduling/offline_lp/launcher.py b/examples/vm_scheduling/offline_lp/launcher.py index 7255d42f4..2e5f21870 100644 --- a/examples/vm_scheduling/offline_lp/launcher.py +++ b/examples/vm_scheduling/offline_lp/launcher.py @@ -22,8 +22,8 @@ config = convert_dottable(raw_config) LOG_PATH = os.path.join(FILE_PATH, "log", config.experiment_name) -simulation_logger = Logger(tag="simulation", format_=LogFormat.none, dump_folder=LOG_PATH, dump_mode="w") -ilp_logger = Logger(tag="ilp", format_=LogFormat.none, dump_folder=LOG_PATH, dump_mode="w") +simulation_logger = Logger(tag="simulation", format_=LogFormat.none, dump_path=LOG_PATH, dump_mode="w") +ilp_logger = Logger(tag="ilp", format_=LogFormat.none, dump_path=LOG_PATH, dump_mode="w") if __name__ == "__main__": start_time = timeit.default_timer() diff --git a/examples/rl/vm_scheduling/README.md b/examples/vm_scheduling/rl/README.md similarity index 100% rename from examples/rl/vm_scheduling/README.md rename to examples/vm_scheduling/rl/README.md diff --git a/examples/rl/vm_scheduling/__init__.py b/examples/vm_scheduling/rl/__init__.py similarity index 94% rename from examples/rl/vm_scheduling/__init__.py rename to examples/vm_scheduling/rl/__init__.py index 5ffca8684..50e0d0c3c 100644 --- a/examples/rl/vm_scheduling/__init__.py +++ b/examples/vm_scheduling/rl/__init__.py @@ -11,5 +11,5 @@ "policy_creator", "post_collect", "post_evaluate", - "trainer_creator" + "trainer_creator", ] diff --git a/examples/vm_scheduling/rl/algorithms/__init__.py b/examples/vm_scheduling/rl/algorithms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/rl/vm_scheduling/algorithms/ac.py b/examples/vm_scheduling/rl/algorithms/ac.py similarity index 96% rename from examples/rl/vm_scheduling/algorithms/ac.py rename to examples/vm_scheduling/rl/algorithms/ac.py index fbc1ca858..d102a7543 100644 --- a/examples/rl/vm_scheduling/algorithms/ac.py +++ b/examples/vm_scheduling/rl/algorithms/ac.py @@ -16,7 +16,7 @@ "activation": torch.nn.LeakyReLU, "softmax": True, "batch_norm": False, - "head": True + "head": True, } critic_net_conf = { @@ -24,7 +24,7 @@ "activation": torch.nn.LeakyReLU, "softmax": False, "batch_norm": False, - "head": True + "head": True, } actor_learning_rate = 0.0001 @@ -67,7 +67,7 @@ def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: def get_state(self) -> dict: return { "network": self.state_dict(), - "optim": self._optim.state_dict() + "optim": self._optim.state_dict(), } def set_state(self, net_state: dict) -> None: @@ -105,7 +105,7 @@ def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: def get_state(self) -> dict: return { "network": self.state_dict(), - "optim": self._optim.state_dict() + "optim": self._optim.state_dict(), } def set_state(self, net_state: dict) -> None: @@ -127,12 +127,11 @@ def get_ac(state_dim: int, num_features: int, name: str) -> DiscreteActorCriticT return DiscreteActorCriticTrainer( name=name, params=DiscreteActorCriticParams( - device="cpu", get_v_critic_net_func=lambda: MyCriticNet(state_dim, num_features), reward_discount=0.9, grad_iters=100, critic_loss_cls=torch.nn.MSELoss, min_logp=-20, - lam=.0 - ) + lam=.0, + ), ) diff --git a/examples/rl/vm_scheduling/algorithms/dqn.py b/examples/vm_scheduling/rl/algorithms/dqn.py similarity index 97% rename from examples/rl/vm_scheduling/algorithms/dqn.py rename to examples/vm_scheduling/rl/algorithms/dqn.py index e81547031..31fda945f 100644 --- a/examples/rl/vm_scheduling/algorithms/dqn.py +++ b/examples/vm_scheduling/rl/algorithms/dqn.py @@ -21,7 +21,7 @@ "batch_norm": False, "skip_connection": False, "head": True, - "dropout_p": 0.0 + "dropout_p": 0.0, } q_net_learning_rate = 0.0005 q_net_lr_scheduler_params = {"T_0": 500, "T_mult": 2} @@ -96,7 +96,7 @@ def get_policy(state_dim: int, action_num: int, num_features: int, name: str) -> "final_value": 0.0, } )], - warmup=100 + warmup=100, ) @@ -104,7 +104,6 @@ def get_dqn(name: str) -> DQNTrainer: return DQNTrainer( name=name, params=DQNParams( - device="cpu", reward_discount=0.9, update_target_every=5, num_epochs=100, @@ -113,6 +112,6 @@ def get_dqn(name: str) -> DQNTrainer: replay_memory_capacity=10000, random_overwrite=False, batch_size=32, - data_parallelism=2 - ) + data_parallelism=2, + ), ) diff --git a/examples/rl/vm_scheduling/callbacks.py b/examples/vm_scheduling/rl/callbacks.py similarity index 100% rename from examples/rl/vm_scheduling/callbacks.py rename to examples/vm_scheduling/rl/callbacks.py diff --git a/examples/rl/vm_scheduling/config.py b/examples/vm_scheduling/rl/config.py similarity index 90% rename from examples/rl/vm_scheduling/config.py rename to examples/vm_scheduling/rl/config.py index 67414fbec..71cf2eaa5 100644 --- a/examples/rl/vm_scheduling/config.py +++ b/examples/vm_scheduling/rl/config.py @@ -9,7 +9,7 @@ "topology": "azure.2019.10k", "start_tick": 0, "durations": 300, # 8638 - "snapshot_resolution": 1 + "snapshot_resolution": 1, } num_pms = Env(**env_conf).business_engine.pm_amount @@ -23,7 +23,7 @@ reward_shaping_conf = { "alpha": 0.0, - "beta": 1.0 + "beta": 1.0, } seed = 666 @@ -32,11 +32,11 @@ "topology": "azure.2019.10k.oversubscription", "start_tick": 0, "durations": 300, - "snapshot_resolution": 1 + "snapshot_resolution": 1, } test_reward_shaping_conf = { "alpha": 0.0, - "beta": 1.0 + "beta": 1.0, } test_seed = 1024 diff --git a/examples/rl/vm_scheduling/env_sampler.py b/examples/vm_scheduling/rl/env_sampler.py similarity index 97% rename from examples/rl/vm_scheduling/env_sampler.py rename to examples/vm_scheduling/rl/env_sampler.py index 2394ee0f1..8b56aa4a9 100644 --- a/examples/rl/vm_scheduling/env_sampler.py +++ b/examples/vm_scheduling/rl/env_sampler.py @@ -20,8 +20,8 @@ class VMEnvSampler(AbsEnvSampler): - def __init__(self, get_env, policy_creator, agent2policy, get_test_env=None, device: str = None): - super().__init__(get_env, policy_creator, agent2policy, get_test_env=get_test_env, device=device) + def __init__(self, get_env, policy_creator, agent2policy, get_test_env=None): + super().__init__(get_env, policy_creator, agent2policy, get_test_env=get_test_env) self._learn_env.set_seed(seed) self._test_env.set_seed(test_seed) @@ -123,6 +123,9 @@ def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]): self._info["actions_by_core_requirement"][cache_element.event.vm_cpu_cores_requirement].append([action, mask]) self._info["action_sequence"].append(action) + def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: + self._post_step(cache_element, reward) + agent2policy = {"AGENT": f"{algorithm}.policy"} @@ -132,5 +135,4 @@ def env_sampler_creator(policy_creator: Dict[str, Callable[[str], RLPolicy]]) -> policy_creator=policy_creator, agent2policy=agent2policy, get_test_env=lambda: Env(**test_env_conf), - device="cpu", ) diff --git a/examples/rl/vm_scheduling/policy_trainer.py b/examples/vm_scheduling/rl/policy_trainer.py similarity index 100% rename from examples/rl/vm_scheduling/policy_trainer.py rename to examples/vm_scheduling/rl/policy_trainer.py diff --git a/examples/vm_scheduling/rule_based_algorithm/__init__.py b/examples/vm_scheduling/rule_based_algorithm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/maro/cli/k8s/aks_commands.py b/maro/cli/k8s/aks_commands.py new file mode 100644 index 000000000..d95abd6f3 --- /dev/null +++ b/maro/cli/k8s/aks_commands.py @@ -0,0 +1,851 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import base64 +import json +import os +import shutil +from os.path import abspath, dirname, expanduser, join + +import yaml + +# from maro.cli.k8s.executors.k8s_executor import K8sExecutor +# from maro.cli.utils.azure.acr import list_acr_repositories, login_acr +from maro.cli.utils import docker as docker_utils +from maro.cli.utils.azure import storage as azure_storage_utils +from maro.cli.utils.azure.aks import attach_acr +from maro.cli.utils.azure.deployment import create_deployment +from maro.cli.utils.azure.general import connect_to_aks, get_acr_push_permissions, set_env_credentials +from maro.cli.utils.azure.resource_group import create_resource_group, delete_resource_group_under_subscription +# from maro.cli.utils.azure.vm import list_vm_sizes +from maro.cli.utils.common import show_log +# from maro.cli.utils.deployment_validator import DeploymentValidator +# from maro.cli.utils.details_reader import DetailsReader +# from maro.cli.utils.details_writer import DetailsWriter +# from maro.cli.utils.name_creator import NameCreator +# from maro.cli.utils.path_convertor import PathConvertor +# from maro.cli.utils.subprocess import Subprocess +# from maro.utils.exception.cli_exception import BadRequestError, FileOperationError +from maro.rl.workflows.config import ConfigParser +from maro.utils.logger import CliLogger +from maro.utils.utils import LOCAL_MARO_ROOT + +from .utils import k8s, k8s_manifest_generator + +# metadata +CLI_K8S_PATH = dirname(abspath(__file__)) +TEMPLATE_PATH = join(CLI_K8S_PATH, "test_template.json") +# TEMPLATE_PATH = join(CLI_K8S_PATH, "lib", "modes", "aks", "create_aks_cluster", "template.json") +NVIDIA_PLUGIN_PATH = join(CLI_K8S_PATH, "create_nvidia_plugin", "nvidia-device-plugin.yml") +LOCAL_ROOT = expanduser("~/.maro/aks") +DEPLOYMENT_CONF_PATH = os.path.join(LOCAL_ROOT, "conf.json") +DOCKER_FILE_PATH = join(LOCAL_MARO_ROOT, "docker_files", "dev.df") +DOCKER_IMAGE_NAME = "maro-aks" +REDIS_HOST = "maro-redis" +REDIS_PORT = 6379 +ADDRESS_REGISTRY_NAME = "address-registry" +ADDRESS_REGISTRY_PORT = 6379 +K8S_SECRET_NAME = "azure-secret" + +# display +NO_DEPLOYMENT_MSG = "No Kubernetes deployment on Azure found. Use 'maro aks init' to create a deployment first" +NO_JOB_MSG = "No job named {} has been scheduled. Use 'maro aks job add' to add the job first." +JOB_EXISTS_MSG = "A job named {} has already been scheduled." + +logger = CliLogger(name=__name__) + + +# helper functions +def get_resource_group_name(deployment_name: str): + return f"rg-{deployment_name}" + + +def get_acr_name(deployment_name: str): + return f"crmaro{deployment_name}" + + +def get_acr_server_name(acr_name: str): + return f"{acr_name}.azurecr.io" + + +def get_docker_image_name_in_acr(acr_name: str, docker_image_name: str): + return f"{get_acr_server_name(acr_name)}/{docker_image_name}" + + +def get_aks_name(deployment_name: str): + return f"aks-maro-{deployment_name}" + + +def get_agentpool_name(deployment_name: str): + return f"ap{deployment_name}" + + +def get_fileshare_name(deployment_name): + return f"fs-{deployment_name}" + + +def get_storage_account_name(deployment_name: str): + return f"stscenario{deployment_name}" + + +def get_virtual_network_name(location: str, deployment_name: str): + return f"vnet-prod-{location}-{deployment_name}" + + +def get_local_job_path(job_name): + return os.path.join(LOCAL_ROOT, job_name) + + +def get_storage_account_secret(resource_group_name: str, storage_account_name: str, namespace: str): + storage_account_keys = azure_storage_utils.get_storage_account_keys(resource_group_name, storage_account_name) + storage_key = storage_account_keys[0]["value"] + secret_data = { + "azurestorageaccountname": base64.b64encode(storage_account_name.encode()).decode(), + "azurestorageaccountkey": base64.b64encode(bytes(storage_key.encode())).decode() + } + k8s.create_secret(K8S_SECRET_NAME, secret_data, namespace) + + +def get_resource_params(deployment_conf: dict) -> dict: + """Create ARM parameters for Azure resource deployment (). + + See https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/overview for details. + + Args: + deployment_conf (dict): Configuration dict for deployment on Azure. + + Returns: + dict: parameter dict, should be exported to json. + """ + name = deployment_conf["name"] + return { + "acrName": get_acr_name(name), + "acrSku": deployment_conf["container_registry_service_tier"], + "systemPoolVMCount": deployment_conf["hardware"]["k8s"]["vm_count"], + "systemPoolVMSize": deployment_conf["hardware"]["k8s"]["vm_size"], + "userPoolName": get_agentpool_name(name), + "userPoolVMCount": deployment_conf["hardware"]["app"]["vm_count"], + "userPoolVMSize": deployment_conf["hardware"]["app"]["vm_size"], + "aksName": get_aks_name(name), + "location": deployment_conf["location"], + "storageAccountName": get_storage_account_name(name), + "fileShareName": get_fileshare_name(name) + # "virtualNetworkName": get_virtual_network_name(deployment_conf["location"], name) + } + + +def prepare_docker_image_and_push_to_acr(image_name: str, context: str, docker_file_path: str, acr_name: str): + # build and tag docker image locally and push to the Azure Container Registry + if not docker_utils.image_exists(image_name): + docker_utils.build_image(context, docker_file_path, image_name) + + get_acr_push_permissions(os.environ["AZURE_CLIENT_ID"], acr_name) + docker_utils.push(image_name, get_acr_server_name(acr_name)) + + +def start_redis_service_in_aks(host: str, port: int, namespace: str): + k8s.load_config() + k8s.create_namespace(namespace) + k8s.create_deployment(k8s_manifest_generator.get_redis_deployment_manifest(host, port), namespace) + k8s.create_service(k8s_manifest_generator.get_redis_service_manifest(host, port), namespace) + + +# CLI command functions +def init(deployment_conf_path: str, **kwargs): + """Create MARO Cluster with create_deployment. + + Args: + deployment_conf_path (str): Path to the deployment configuration file. + """ + with open(deployment_conf_path, "r") as fp: + deployment_conf = yaml.safe_load(fp) + + subscription = deployment_conf["azure_subscription"] + name = deployment_conf["name"] + if os.path.isfile(DEPLOYMENT_CONF_PATH): + logger.warning(f"Deployment {name} has already been created") + return + + os.makedirs(LOCAL_ROOT, exist_ok=True) + resource_group_name = get_resource_group_name(name) + try: + set_env_credentials(LOCAL_ROOT, f"sp-{name}") + + # create resource group + resource_group = create_resource_group(subscription, resource_group_name, deployment_conf["location"]) + logger.info_green(f"Provisioned resource group {resource_group.name} in {resource_group.location}") + + # Create ARM parameters and start deployment + logger.info("Creating Azure resources...") + resource_params = get_resource_params(deployment_conf) + with open(TEMPLATE_PATH, 'r') as fp: + template = json.load(fp) + + create_deployment(subscription, resource_group_name, name, template, resource_params) + + # Attach ACR to AKS + aks_name, acr_name = resource_params["aksName"], resource_params["acrName"] + attach_acr(resource_group_name, aks_name, acr_name) + connect_to_aks(resource_group_name, aks_name) + + # build and tag docker image locally and push to the Azure Container Registry + logger.info("Preparing docker image...") + prepare_docker_image_and_push_to_acr(DOCKER_IMAGE_NAME, LOCAL_MARO_ROOT, DOCKER_FILE_PATH, acr_name) + + # start the Redis service in the k8s cluster in the deployment namespace and expose it + logger.info("Starting Redis service in the k8s cluster...") + start_redis_service_in_aks(REDIS_HOST, REDIS_PORT, name) + + # Dump the deployment configuration + with open(DEPLOYMENT_CONF_PATH, "w") as fp: + json.dump({ + "name": name, + "subscription": subscription, + "resource_group": resource_group_name, + "resources": resource_params + }, fp) + logger.info_green(f"Cluster '{name}' is created") + except Exception as e: + # If failed, remove details folder, then raise + shutil.rmtree(LOCAL_ROOT) + logger.error_red(f"Deployment {name} failed due to {e}, rolling back...") + delete_resource_group_under_subscription(subscription, resource_group_name) + except KeyboardInterrupt: + shutil.rmtree(LOCAL_ROOT) + logger.error_red(f"Deployment {name} aborted, rolling back...") + delete_resource_group_under_subscription(subscription, resource_group_name) + + +def add_job(conf_path: dict, **kwargs): + if not os.path.isfile(DEPLOYMENT_CONF_PATH): + logger.error_red(NO_DEPLOYMENT_MSG) + return + + parser = ConfigParser(conf_path) + job_conf = parser.config + + job_name = job_conf["job"] + local_job_path = get_local_job_path(job_name) + if os.path.isdir(local_job_path): + logger.error_red(JOB_EXISTS_MSG.format(job_name)) + return + + os.makedirs(local_job_path) + with open(DEPLOYMENT_CONF_PATH, "r") as fp: + deployment_conf = json.load(fp) + + resource_group_name = deployment_conf["resource_group"] + resource_name = deployment_conf["resources"] + fileshare = azure_storage_utils.get_fileshare(resource_name["storageAccountName"], resource_name["fileShareName"]) + job_dir = azure_storage_utils.get_directory(fileshare, job_name) + job_path_in_share = f"{resource_name['fileShareName']}/{job_name}" + scenario_path = job_conf['scenario_path'] + logger.info(f"Uploading local directory {scenario_path}...") + azure_storage_utils.upload_to_fileshare(job_dir, scenario_path, name="scenario") + azure_storage_utils.get_directory(job_dir, "checkpoints") + azure_storage_utils.get_directory(job_dir, "logs") + + # Define mount volumes, i.e., scenario code, checkpoints, logs and load point + volumes = [ + k8s_manifest_generator.get_azurefile_volume_spec(name, f"{job_path_in_share}/{name}", K8S_SECRET_NAME) + for name in ["scenario", "logs", "checkpoints"] + ] + + if "load_path" in job_conf["training"]: + load_dir = job_conf["training"]["load_path"] + logger.info(f"Uploading local directory {load_dir}...") + azure_storage_utils.upload_to_fileshare(job_dir, load_dir, name="loadpoint") + volumes.append( + k8s_manifest_generator.get_azurefile_volume_spec( + "loadpoint", f"{job_path_in_share}/loadpoint", K8S_SECRET_NAME) + ) + + # Start k8s jobs + k8s.load_config() + k8s.create_namespace(job_name) + get_storage_account_secret(resource_group_name, resource_name["storageAccountName"], job_name) + k8s.create_service( + k8s_manifest_generator.get_cross_namespace_service_access_manifest( + ADDRESS_REGISTRY_NAME, REDIS_HOST, deployment_conf["name"], ADDRESS_REGISTRY_PORT + ), job_name + ) + for component_name, env in parser.as_env(containerize=True).items(): + container_spec = k8s_manifest_generator.get_container_spec( + get_docker_image_name_in_acr(resource_name["acrName"], DOCKER_IMAGE_NAME), + component_name, + env, + volumes + ) + manifest = k8s_manifest_generator.get_job_manifest( + resource_name["userPoolName"], + component_name, + container_spec, + volumes + ) + k8s.create_job(manifest, job_name) + + +def remove_jobs(job_names: str, **kwargs): + if not os.path.isfile(DEPLOYMENT_CONF_PATH): + logger.error_red(NO_DEPLOYMENT_MSG) + return + + k8s.load_config() + for job_name in job_names: + local_job_path = get_local_job_path(job_name) + if not os.path.isdir(local_job_path): + logger.error_red(NO_JOB_MSG.format(job_name)) + return + + k8s.delete_job(job_name) + + +def get_job_logs(job_name: str, tail: int = -1, **kwargs): + with open(DEPLOYMENT_CONF_PATH, "r") as fp: + deployment_conf = json.load(fp) + + local_log_path = os.path.join(get_local_job_path(job_name), "log") + resource_name = deployment_conf["resources"] + fileshare = azure_storage_utils.get_fileshare(resource_name["storageAccountName"], resource_name["fileShareName"]) + job_dir = azure_storage_utils.get_directory(fileshare, job_name) + log_dir = azure_storage_utils.get_directory(job_dir, "logs") + azure_storage_utils.download_from_fileshare(log_dir, f"{job_name}.log", local_log_path) + show_log(local_log_path, tail=tail) + + +def describe_job(job_name: str): + pass + +# def get_checkpoints(job_name: str, **kwargs): +# with open(DEPLOYMENT_CONF_PATH, "r") as fp: +# deployment_conf = json.load(fp) +# local_checkpoint_path = job_conf.get("checkpoint_path", os.path.join(get_local_job_path, "ckpt")) +# resource_name = deployment_conf["resources"] +# fileshare = azure_storage_utils.get_fileshare(resource_name["storageAccountName"], resource_name["fileShareName"]) +# job_dir = azure_storage_utils.get_directory(fileshare, job_name) +# azure_storage_utils.download_from_fileshare(job_dir, f"{job_name}.log", local_checkpoint_path) + + +def exit(**kwargs): + try: + with open(DEPLOYMENT_CONF_PATH, "r") as fp: + deployment_conf = json.load(fp) + except FileNotFoundError: + logger.error(NO_DEPLOYMENT_MSG) + return + + name = deployment_conf["name"] + set_env_credentials(LOCAL_ROOT, f"sp-{name}") + delete_resource_group_under_subscription(deployment_conf["subscription"], deployment_conf["resource_group"]) + + +# class K8sAksExecutor(K8sExecutor): +# """Executor for k8s/aks mode. + +# See https://maro.readthedocs.io/en/latest/key_components/orchestration.html for reference. +# """ + +# def __init__(self, cluster_name: str): +# self.deployment_conf = DetailsReader.load_deployment_conf(cluster_name=cluster_name) + +# # Cloud configs +# self.subscription = self.deployment_conf["cloud"]["subscription"] +# self.resource_group = self.deployment_conf["cloud"]["resource_group"] +# self.location = self.deployment_conf["cloud"]["location"] + +# super().__init__(deployment_conf=self.deployment_conf) + +# # maro k8s node +# def scale_node(self, replicas: int, node_size: str) -> None: +# """Scale up/down MARO Node. + +# Args: +# replicas (int): desired number of MARO Node in specific node_size. +# node_size (str): size of the MARO Node VM, see +# https://docs.microsoft.com/en-us/azure/virtual-machines/sizes for reference. +# +# Returns: +# None. +# """ +# # Get node_size_to_info +# node_size_to_info = self._get_node_size_to_info() + +# # Get node_size_to_spec, and check if node_size is valid +# node_size_to_spec = self._get_node_size_to_spec() +# if node_size not in node_size_to_spec: +# raise BadRequestError(f"Invalid node_size '{node_size}'") + +# # Scale node +# if node_size not in node_size_to_info: +# self._build_node_pool( +# replicas=replicas, +# node_size=node_size +# ) +# elif node_size_to_info[node_size]["count"] != replicas: +# self._scale_node_pool( +# replicas=replicas, +# node_size=node_size, +# node_size_to_info=node_size_to_info +# ) +# else: +# logger.warning_yellow("Replica is match, no create or delete") + +# def _get_node_size_to_info(self) -> dict: +# """Get node_size to info mapping of the K8s Cluster. + +# Returns: +# dict: node_size to info mapping. +# """ +# # List nodepool +# nodepools = list_nodepool( +# resource_group=self.resource_group, +# aks_name=f"{self.cluster_id}-aks" +# ) + +# # Build node_size_to_count +# node_size_to_count = {} +# for nodepool in nodepools: +# node_size_to_count[nodepool["vmSize"]] = nodepool + +# return node_size_to_count + +# def _get_node_size_to_spec(self) -> dict: +# """Get node_size to spec mapping of Azure VM. + +# Returns: +# dict: node_size to spec mapping. +# """ +# # List available sizes for VM +# specs = list_vm_sizes(location=self.location) + +# # Build node_size_to_spec +# node_size_to_spec = {} +# for spec in specs: +# node_size_to_spec[spec["name"]] = spec + +# return node_size_to_spec + +# def _build_node_pool(self, replicas: int, node_size: str) -> None: +# """Build node pool for the specific node_size. + +# Args: +# replicas (int): number of MARO Node in specific node_size to stop. +# node_size (str): size of the MARO Node VM, +# see https://docs.microsoft.com/en-us/azure/virtual-machines/sizes for reference. + +# Returns: +# None. +# """ +# logger.info(f"Building '{node_size}' nodepool") + +# # Build nodepool +# add_nodepool( +# resource_group=self.resource_group, +# aks_name=f"{self.cluster_id}-aks", +# nodepool_name=K8sAksExecutor._generate_nodepool_name(node_size=node_size), +# node_count=replicas, +# node_size=node_size +# ) + +# logger.info_green(f"'{node_size}' nodepool is built") + +# def _scale_node_pool(self, replicas: int, node_size: str, node_size_to_info: dict): +# """Scale node pool of the specific node_size. + +# Args: +# replicas (int): number of MARO Node in specific node_size to stop. +# node_size (str): size of the MARO Node VM, +# see https://docs.microsoft.com/en-us/azure/virtual-machines/sizes for reference. +# node_size_to_info (dict): node_size to info mapping. + +# Returns: +# None. +# """ +# logger.info(f"Scaling '{node_size}' nodepool") + +# # Scale node pool +# scale_nodepool( +# resource_group=self.resource_group, +# aks_name=f"{self.cluster_id}-aks", +# nodepool_name=node_size_to_info[node_size]["name"], +# node_count=replicas +# ) + +# logger.info_green(f"'{node_size}' nodepool is scaled") + +# @staticmethod +# def _generate_nodepool_name(node_size: str) -> str: +# """Generate name of the nodepool. + +# Args: +# node_size (str): size of the MARO Node VM. + +# Returns: +# None. +# """ +# return NameCreator.create_name_with_md5(prefix="pool", key=node_size, md5_len=8) + +# def list_node(self) -> None: +# """Print node details to the command line. + +# Returns: +# None. +# """ +# # Get aks details +# aks_details = get_aks(resource_group=self.resource_group, aks_name=f"{self.cluster_id}-aks") +# agent_pools_details = aks_details["agentPoolProfiles"] + +# # Filter and print +# node_details = {} +# for agent_pool_details in agent_pools_details: +# node_details[agent_pool_details["vmSize"]] = agent_pool_details["count"] +# logger.info( +# json.dumps( +# node_details, +# indent=4, sort_keys=True +# ) +# ) + +# # maro k8s image + +# def push_image(self, image_name: str) -> None: +# """Push local image to the MARO Cluster. + +# Args: +# image_name (str): name of the local image that loaded in the docker. + +# Returns: +# None. +# """ +# remote_image_name = f"{self.cluster_id}acr.azurecr.io/{image_name}" + +# # ACR login +# login_acr(acr_name=f"{self.cluster_id}acr") + +# # Tag image +# command = f"docker tag {image_name} {remote_image_name}" +# _ = Subprocess.run(command=command) + +# # Push image to ACR +# command = f"docker push {remote_image_name}" +# _ = Subprocess.run(command=command) + +# def list_image(self): +# """Print image details to the command line. + +# Returns: +# None. +# """ +# # List acr repository +# acr_repositories = list_acr_repositories(acr_name=f"{self.cluster_id}acr") +# logger.info(acr_repositories) + +# # maro k8s data + +# def push_data(self, local_path: str, remote_dir: str) -> None: +# """Push local data to the remote AFS service via azcopy. + +# Args: +# local_path (str): path of the local data. +# remote_dir (str): path of the remote folder. + +# Returns: +# None. +# """ +# # Get sas +# sas = self._check_and_get_account_sas() + +# # Push data +# abs_local_path = os.path.expanduser(local_path) +# abs_source_path = PathConvertor.build_path_without_trailing_slash(abs_local_path) +# target_dir = PathConvertor.build_path_with_trailing_slash(remote_dir) +# if not target_dir.startswith("/"): +# raise FileOperationError(f"Invalid remote path: {target_dir}\nShould be started with '/'") +# copy_command = ( +# "azcopy copy " +# f"'{abs_source_path}' " +# f"'https://{self.cluster_id}st.file.core.windows.net/{self.cluster_id}-fs{target_dir}?{sas}' " +# "--recursive=True" +# ) +# _ = Subprocess.run(command=copy_command) + +# def pull_data(self, local_dir: str, remote_path: str) -> None: +# """Pull remote AFS service data to local folder via azcopy. + +# Args: +# local_dir (str): path of the local folder. +# remote_path (str): path of the remote data. + +# Returns: +# None. +# """ +# # Get sas +# sas = self._check_and_get_account_sas() + +# # Push data +# abs_local_dir = os.path.expanduser(local_dir) +# source_path = PathConvertor.build_path_without_trailing_slash(remote_path) +# abs_target_dir = PathConvertor.build_path_with_trailing_slash(abs_local_dir) +# os.makedirs(abs_target_dir, exist_ok=True) +# if not source_path.startswith("/"): +# raise FileOperationError(f"Invalid remote path: {source_path}\nShould be started with '/'") +# copy_command = ( +# "azcopy copy " +# f"'https://{self.cluster_id}st.file.core.windows.net/{self.cluster_id}-fs{source_path}?{sas}' " +# f"'{abs_target_dir}' " +# "--recursive=True" +# ) +# _ = Subprocess.run(command=copy_command) + +# def remove_data(self, remote_path: str) -> None: +# """Remote data at the remote AFS service. + +# Args: +# remote_path (str): path of the remote data. + +# Returns: +# None. +# """ +# # FIXME: Remove failed, The specified resource may be in use by an SMB client + +# # Get sas +# sas = self._check_and_get_account_sas() + +# # Remove data +# copy_command = ( +# "azcopy remove " +# f"'https://{self.cluster_id}st.file.core.windows.net/{self.cluster_id}-fs{remote_path}?{sas}' " +# "--recursive=True" +# ) +# _ = Subprocess.run(command=copy_command) + +# def _check_and_get_account_sas(self) -> str: +# """Check and get account sas token, also update it to the deployment_conf. + +# Ref: https://msdn.microsoft.com/library/azure/mt584140.aspx + +# Returns: +# str: account sas token. +# """ + +# # Load details +# cloud_details = self.deployment_conf["cloud"] + +# # Regenerate sas if the key is None or expired TODO: +# if "account_sas" not in cloud_details: +# account_sas = get_storage_account_sas(account_name=f"{self.cluster_id}st") +# cloud_details["account_sas"] = account_sas +# DetailsWriter.save_deployment_conf( +# cluster_name=self.cluster_name, +# deployment_conf=self.deployment_conf +# ) + +# return cloud_details["account_sas"] + +# # maro k8s job + +# def _create_k8s_job(self, job_details: dict) -> dict: +# """Create k8s job object with job_details. + +# Args: +# job_details (dict): details of the MARO Job. + +# Returns: +# dict: k8s job object. +# """ +# # Get config template +# with open(f"{MARO_K8S_LIB}/modes/aks/create_job/job.yml") as fr: +# k8s_job_config = yaml.safe_load(fr) +# with open(f"{MARO_K8S_LIB}/modes/aks/create_job/container.yml") as fr: +# k8s_container_config = yaml.safe_load(fr) + +# # Fill configs +# k8s_job_config["metadata"]["name"] = job_details["id"] +# k8s_job_config["metadata"]["labels"]["jobName"] = job_details["name"] +# azure_file_config = k8s_job_config["spec"]["template"]["spec"]["volumes"][0]["azureFile"] +# azure_file_config["secretName"] = "azure-storage-account-secret" +# azure_file_config["shareName"] = f"{self.cluster_id}-fs" + +# # Create and fill container config +# for component_type, component_details in job_details["components"].items(): +# for component_index in range(component_details["num"]): +# container_config = self._create_k8s_container_config( +# job_details=job_details, +# k8s_container_config_template=k8s_container_config, +# component_type=component_type, +# component_index=component_index +# ) +# k8s_job_config["spec"]["template"]["spec"]["containers"].append(container_config) + +# return k8s_job_config + +# def _create_k8s_container_config( +# self, job_details: dict, k8s_container_config_template: dict, +# component_type: str, component_index: int +# ) -> dict: +# """Create the container config in the k8s job object. + +# Args: +# job_details (dict): details of the MARO Job. +# k8s_container_config_template (dict): template of the k8s_container_config. +# component_type (str): type of the component. +# component_index (int): index of the component. + +# Returns: +# dict: the container config. +# """ +# # Copy config. +# k8s_container_config = copy.deepcopy(k8s_container_config_template) + +# # Load details +# component_details = job_details["components"][component_type] +# job_id = job_details["id"] +# component_id = job_details["components"][component_type]["id"] +# container_name = f"{job_id}-{component_id}-{component_index}" + +# # Fill configs. +# k8s_container_config["name"] = container_name +# k8s_container_config["image"] = self._build_image_address(image_name=component_details["image"]) +# k8s_container_config["resources"]["requests"] = { +# "cpu": component_details["resources"]["cpu"], +# "memory": component_details["resources"]["memory"], +# "nvidia.com/gpu": component_details["resources"]["gpu"] +# } +# k8s_container_config["resources"]["limits"] = { +# "cpu": component_details["resources"]["cpu"], +# "memory": component_details["resources"]["memory"], +# "nvidia.com/gpu": component_details["resources"]["gpu"] +# } +# k8s_container_config["env"] = [ +# { +# "name": "CLUSTER_ID", +# "value": f"{self.cluster_id}" +# }, +# { +# "name": "CLUSTER_NAME", +# "value": f"{self.cluster_name}" +# }, +# { +# "name": "JOB_ID", +# "value": job_id +# }, +# { +# "name": "JOB_NAME", +# "value": job_details["name"] +# }, +# { +# "name": "COMPONENT_ID", +# "value": component_id +# }, +# { +# "name": "COMPONENT_TYPE", +# "value": f"{component_type}" +# }, +# { +# "name": "COMPONENT_INDEX", +# "value": f"{component_index}" +# }, +# { +# "name": "PYTHONUNBUFFERED", +# "value": "0" +# } +# ] +# k8s_container_config["command"] = component_details["command"] +# k8s_container_config["volumeMounts"][0]["mountPath"] = component_details["mount"]["target"] + +# return k8s_container_config + +# def _build_image_address(self, image_name: str) -> str: +# """Build image address name for image that stored at Azure Container Registry. + +# Args: +# image_name (str): name of the image. + +# Returns: +# str: image address name. +# """ +# # Get repositories +# acr_repositories = list_acr_repositories(acr_name=f"{self.cluster_id}acr") + +# # Build address +# if image_name in acr_repositories: +# return f"{self.cluster_id}acr.azurecr.io/{image_name}" +# else: +# return image_name + +# @staticmethod +# def _export_log(pod_id: str, container_name: str, export_dir: str) -> None: +# """Export k8s job logs to the specific folder. + +# Args: +# pod_id (str): id of the k8s pod. +# container_name (str): name of the container. +# export_dir (str): path of the exported folder. + +# Returns: +# None. +# """ +# os.makedirs(os.path.expanduser(export_dir + f"/{pod_id}"), exist_ok=True) +# with open(os.path.expanduser(export_dir + f"/{pod_id}/{container_name}.log"), "w") as fw: +# return_str = client.CoreV1Api().read_namespaced_pod_log(name=pod_id, namespace="default") +# fw.write(return_str) + +# # maro k8s status + +# def status(self) -> None: +# """Print details of specific MARO Resources (redis only at this time). + +# Returns: +# None. +# """ +# return_status = {} + +# # Get pods details +# pod_list = client.CoreV1Api().list_pod_for_all_namespaces(watch=False).to_dict()["items"] + +# for pod in pod_list: +# if "app" in pod["metadata"]["labels"] and pod["metadata"]["labels"]["app"] == "maro-redis": +# return_status["redis"] = { +# "private_ip_address": pod["status"]["pod_ip"] +# } +# break + +# # Print status +# logger.info( +# json.dumps( +# return_status, +# indent=4, sort_keys=True +# ) +# ) + +# # Utils + +# def load_k8s_context(self) -> None: +# """Activate load k8s context operation. + +# Returns: +# None. +# """ +# self._load_k8s_context( +# cluster_id=self.cluster_id, +# resource_group=self.resource_group +# ) + +# @staticmethod +# def _load_k8s_context(cluster_id: int, resource_group: str) -> None: +# """Load the k8s context. + +# Set current k8s context (only in the CLI runtime) to the k8s cluster that related to the MARO Cluster. + +# Args: +# cluster_id (str): id of the MARO Cluster. +# resource_group (str): name of the resource group. + +# Returns: +# None. +# """ +# load_aks_context( +# resource_group=resource_group, +# aks_name=f"{cluster_id}-aks" +# ) +# config.load_kube_config(context=f"{cluster_id}-aks") diff --git a/maro/cli/k8s/lib/modes/aks/create_aks_cluster/template.json b/maro/cli/k8s/lib/modes/aks/create_aks_cluster/template.json index 1e77a9802..d77c0cad7 100644 --- a/maro/cli/k8s/lib/modes/aks/create_aks_cluster/template.json +++ b/maro/cli/k8s/lib/modes/aks/create_aks_cluster/template.json @@ -22,18 +22,6 @@ "Premium" ] }, - "adminPublicKey": { - "type": "string", - "metadata": { - "description": "Configure all linux machines with the SSH RSA public key string. Your key should include three parts, for example 'ssh-rsa AAAAB...snip...UcyupgH azureuser@linuxvm'" - } - }, - "adminUsername": { - "type": "string", - "metadata": { - "description": "User name for the Linux Virtual Machines." - } - }, "agentCount": { "type": "int", "metadata": { @@ -87,7 +75,7 @@ "resources": [ { "type": "Microsoft.Storage/storageAccounts/fileServices/shares", - "apiVersion": "2020-08-01-preview", + "apiVersion": "2021-04-01", "name": "[concat(parameters('storageAccountName'), '/default/', parameters('fileShareName'))]", "dependsOn": [ "[variables('stvmId')]" @@ -96,7 +84,7 @@ { "name": "[parameters('acrName')]", "type": "Microsoft.ContainerRegistry/registries", - "apiVersion": "2020-11-01-preview", + "apiVersion": "2021-09-01", "location": "[parameters('location')]", "sku": { "name": "[parameters('acrSku')]" @@ -127,16 +115,6 @@ "type": "VirtualMachineScaleSets" } ], - "linuxProfile": { - "adminUsername": "[parameters('adminUsername')]", - "ssh": { - "publicKeys": [ - { - "keyData": "[parameters('adminPublicKey')]" - } - ] - } - }, "networkProfile": { "networkPlugin": "azure", "loadBalancerSku": "standard" @@ -148,7 +126,7 @@ }, { "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2020-08-01-preview", + "apiVersion": "2021-08-01", "name": "[parameters('storageAccountName')]", "location": "[parameters('location')]", "kind": "StorageV2", diff --git a/maro/cli/k8s/test_conf.yml b/maro/cli/k8s/test_conf.yml new file mode 100644 index 000000000..08ddb6f52 --- /dev/null +++ b/maro/cli/k8s/test_conf.yml @@ -0,0 +1,12 @@ +mode: "" +azure_subscription: 03811e6d-e6a0-4ae1-92e7-2249b8cb13be +name: yq +location: eastus +container_registry_service_tier: Standard # "Basic", "Standard", "Premium", see https://docs.microsoft.com/en-us/azure/container-registry/container-registry-skus for details +hardware: + k8s: + vm_size: Standard_DS2_v2 # https://docs.microsoft.com/en-us/azure/virtual-machines/sizes, https://docs.microsoft.com/en-us/azure/aks/quotas-skus-regions + vm_count: 1 # must be at least 2 for k8s to function properly. + app: + vm_size: Standard_DS2_v2 # https://docs.microsoft.com/en-us/azure/virtual-machines/sizes, https://docs.microsoft.com/en-us/azure/aks/quotas-skus-regions + vm_count: 1 diff --git a/maro/cli/k8s/test_parameters.json b/maro/cli/k8s/test_parameters.json new file mode 100644 index 000000000..9062b86ac --- /dev/null +++ b/maro/cli/k8s/test_parameters.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.1.0.0", + "parameters": { + "acrName": { + "value": "myacr" + }, + "acrSku": { + "value": "Basic" + }, + "agentCount": { + "value": 1 + }, + "agentVMSize": { + "value": "standard_a2_v2" + }, + "clusterName": { + "value": "myaks" + }, + "fileShareName": { + "value": "myfileshare" + }, + "location": { + "value": "East US" + }, + "storageAccountName": { + "value": "mystorage" + }, + "virtualNetworkName": { + "value": "myvnet" + } + } +} diff --git a/maro/cli/k8s/test_template.json b/maro/cli/k8s/test_template.json new file mode 100644 index 000000000..fad7fa6c1 --- /dev/null +++ b/maro/cli/k8s/test_template.json @@ -0,0 +1,157 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.1.0.0", + "parameters": { + "acrName": { + "type": "string", + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Name of your Azure Container Registry" + } + }, + "acrSku": { + "type": "string", + "metadata": { + "description": "Tier of your Azure Container Registry." + }, + "defaultValue": "Standard", + "allowedValues": [ + "Basic", + "Standard", + "Premium" + ] + }, + "systemPoolVMCount": { + "type": "int", + "metadata": { + "description": "The number of VMs allocated for running the k8s system components." + }, + "minValue": 1, + "maxValue": 50 + }, + "systemPoolVMSize": { + "type": "string", + "metadata": { + "description": "Virtual Machine size for running the k8s system components." + } + }, + "userPoolName": { + "type": "string", + "metadata": { + "description": "Name of the user node pool." + } + }, + "userPoolVMCount": { + "type": "int", + "metadata": { + "description": "The number of VMs allocated for running the user appplication." + }, + "minValue": 1, + "maxValue": 50 + }, + "userPoolVMSize": { + "type": "string", + "metadata": { + "description": "Virtual Machine size for running the user application." + } + }, + "aksName": { + "type": "string", + "metadata": { + "description": "Name of the Managed Cluster resource." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location of the Managed Cluster resource." + } + }, + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Azure storage account name." + } + }, + "fileShareName": { + "type": "string", + "metadata": { + "description": "Azure file share name." + } + } + }, + "resources": [ + { + "name": "[parameters('acrName')]", + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2021-09-01", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('acrSku')]" + }, + "properties": { + } + }, + { + "name": "[parameters('aksName')]", + "type": "Microsoft.ContainerService/managedClusters", + "apiVersion": "2020-03-01", + "location": "[parameters('location')]", + "properties": { + "dnsPrefix": "maro", + "agentPoolProfiles": [ + { + "name": "system", + "osDiskSizeGB": 0, + "count": "[parameters('systemPoolVMCount')]", + "vmSize": "[parameters('systemPoolVMSize')]", + "osType": "Linux", + "storageProfile": "ManagedDisks", + "mode": "System", + "type": "VirtualMachineScaleSets" + }, + { + "name": "[parameters('userPoolName')]", + "osDiskSizeGB": 0, + "count": "[parameters('userPoolVMCount')]", + "vmSize": "[parameters('userPoolVMSize')]", + "osType": "Linux", + "storageProfile": "ManagedDisks", + "mode": "User", + "type": "VirtualMachineScaleSets" + } + ], + "networkProfile": { + "networkPlugin": "azure", + "loadBalancerSku": "standard" + } + }, + "identity": { + "type": "SystemAssigned" + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-08-01", + "name": "[parameters('storageAccountName')]", + "location": "[parameters('location')]", + "kind": "StorageV2", + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "accessTier": "Hot" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2021-04-01", + "name": "[concat(parameters('storageAccountName'), '/default/', parameters('fileShareName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + ] + } + ] +} diff --git a/maro/cli/k8s/utils/params.py b/maro/cli/k8s/utils/params.py deleted file mode 100644 index 4ac8c999b..000000000 --- a/maro/cli/k8s/utils/params.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - - -import os - - -class K8sPaths: - MARO_K8S_LIB = "~/.maro/lib/k8s" - ABS_MARO_K8S_LIB = os.path.expanduser(MARO_K8S_LIB) diff --git a/maro/cli/local/commands.py b/maro/cli/local/commands.py index 5f99a162c..2b6ae3c92 100644 --- a/maro/cli/local/commands.py +++ b/maro/cli/local/commands.py @@ -2,7 +2,6 @@ # Licensed under the MIT license. import json -import os import shutil import subprocess import sys diff --git a/maro/cli/local/utils.py b/maro/cli/local/utils.py index 577302156..bb2d019eb 100644 --- a/maro/cli/local/utils.py +++ b/maro/cli/local/utils.py @@ -83,14 +83,14 @@ def term(procs, job_name: str, timeout: int = 3): try: proc.stop(timeout=timeout) proc.remove() - except: + except Exception: pass client = docker.from_env() try: job_network = client.networks.get(job_name) job_network.remove() - except: + except Exception: pass diff --git a/maro/cli/maro.py b/maro/cli/maro.py index c35e9e1ea..cd1554484 100644 --- a/maro/cli/maro.py +++ b/maro/cli/maro.py @@ -90,6 +90,15 @@ def main(): parser_k8s.set_defaults(func=_help_func(parser=parser_k8s)) load_parser_k8s(prev_parser=parser_k8s, global_parser=global_parser) + # maro aks + parser_aks = subparsers.add_parser( + "aks", + help="Manage distributed cluster with Kubernetes.", + parents=[global_parser] + ) + parser_aks.set_defaults(func=_help_func(parser=parser_aks)) + load_parser_aks(prev_parser=parser_aks, global_parser=global_parser) + # maro inspector parser_inspector = subparsers.add_parser( 'inspector', @@ -897,6 +906,77 @@ def load_parser_k8s(prev_parser: ArgumentParser, global_parser: ArgumentParser) parser_template.set_defaults(func=template) +def load_parser_aks(prev_parser: ArgumentParser, global_parser: ArgumentParser) -> None: + subparsers = prev_parser.add_subparsers() + + # maro aks create + from maro.cli.k8s.aks_commands import init + parser_create = subparsers.add_parser( + "init", + help="Deploy resources and start required services on Azure", + examples=CliExamples.MARO_K8S_CREATE, + parents=[global_parser] + ) + parser_create.add_argument("deployment_conf_path", help="Path of the deployment configuration file") + parser_create.set_defaults(func=init) + + # maro aks exit + from maro.cli.k8s.aks_commands import exit + parser_create = subparsers.add_parser( + "exit", + help="Delete deployed resources", + examples=CliExamples.MARO_K8S_DELETE, + parents=[global_parser] + ) + parser_create.set_defaults(func=exit) + + # maro aks job + parser_job = subparsers.add_parser( + "job", + help="Job-related commands", + parents=[global_parser] + ) + parser_job.set_defaults(func=_help_func(parser=parser_job)) + job_subparsers = parser_job.add_subparsers() + + # maro aks job add + from maro.cli.k8s.aks_commands import add_job + parser_job_start = job_subparsers.add_parser( + "add", + help="Add an RL job to the AKS cluster", + examples=CliExamples.MARO_K8S_JOB_START, + parents=[global_parser] + ) + parser_job_start.add_argument("conf_path", help="Path to the job configuration file") + parser_job_start.set_defaults(func=add_job) + + # maro aks job rm + from maro.cli.k8s.aks_commands import remove_jobs + parser_job_start = job_subparsers.add_parser( + "rm", + help="Remove previously scheduled RL jobs from the AKS cluster", + examples=CliExamples.MARO_K8S_JOB_START, + parents=[global_parser] + ) + parser_job_start.add_argument("job_names", help="Name of job to be removed", nargs="*") + parser_job_start.set_defaults(func=remove_jobs) + + # maro aks job logs + from maro.cli.k8s.aks_commands import get_job_logs + job_logs_parser = job_subparsers.add_parser( + "logs", + help="Get job logs", + examples=CliExamples.MARO_PROCESS_JOB_LOGS, + parents=[global_parser] + ) + job_logs_parser.add_argument("job_name", help="job name") + job_logs_parser.add_argument( + "-n", "--tail", type=int, default=-1, + help="Number of lines to show from the end of the given job's logs" + ) + job_logs_parser.set_defaults(func=get_job_logs) + + def load_parser_data(prev_parser: ArgumentParser, global_parser: ArgumentParser): data_cmd_sub_parsers = prev_parser.add_subparsers() diff --git a/maro/rl/policy/abs_policy.py b/maro/rl/policy/abs_policy.py index c6064e03a..9af3bfeaf 100644 --- a/maro/rl/policy/abs_policy.py +++ b/maro/rl/policy/abs_policy.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from typing import Dict, Optional +from typing import Dict, Iterable, Optional import numpy as np import torch @@ -27,14 +27,14 @@ def __init__(self, name: str, trainable: bool) -> None: self._trainable = trainable @abstractmethod - def get_actions(self, states: object) -> object: + def get_actions(self, states: object) -> Iterable: """Get actions according to states. Args: states (object): States. Returns: - actions (object): Actions. + actions (Iterable): Actions. """ raise NotImplementedError @@ -49,6 +49,33 @@ def trainable(self) -> bool: def set_name(self, name: str) -> None: self._name = name + @abstractmethod + def explore(self) -> None: + """Set the policy to exploring mode. + """ + raise NotImplementedError + + @abstractmethod + def exploit(self) -> None: + """Set the policy to exploiting mode. + """ + raise NotImplementedError + + @abstractmethod + def eval(self) -> None: + """Switch the policy to evaluation mode. + """ + raise NotImplementedError + + @abstractmethod + def train(self) -> None: + """Switch the policy to training mode. + """ + raise NotImplementedError + + def to_device(self, device: torch.device) -> None: + pass + class DummyPolicy(AbsPolicy): """Dummy policy that takes no actions. @@ -60,6 +87,18 @@ def __init__(self) -> None: def get_actions(self, states: object) -> None: return None + def explore(self) -> None: + pass + + def exploit(self) -> None: + pass + + def eval(self) -> None: + pass + + def train(self) -> None: + pass + class RuleBasedPolicy(AbsPolicy, metaclass=ABCMeta): """Rule-based policy. The user should define the rule of this policy, and a rule-based policy is not trainable. @@ -75,6 +114,18 @@ def get_actions(self, states: object) -> object: def _rule(self, states: object) -> object: raise NotImplementedError + def explore(self) -> None: + pass + + def exploit(self) -> None: + pass + + def eval(self) -> None: + pass + + def train(self) -> None: + pass + class RLPolicy(AbsPolicy, metaclass=ABCMeta): """Reinforcement learning policy. @@ -155,7 +206,7 @@ def apply_gradients(self, grad: dict) -> None: raise NotImplementedError def get_actions(self, states: np.ndarray) -> np.ndarray: - return self.get_actions_tensor(ndarray_to_tensor(states, self._device)).cpu().numpy() + return self.get_actions_tensor(ndarray_to_tensor(states, device=self._device)).cpu().numpy() def get_actions_tensor(self, states: torch.Tensor) -> torch.Tensor: """Get actions according to states. Takes torch.Tensor as inputs and returns torch.Tensor. @@ -195,18 +246,6 @@ def unfreeze(self) -> None: """ raise NotImplementedError - @abstractmethod - def eval(self) -> None: - """Switch the policy to evaluation mode. - """ - raise NotImplementedError - - @abstractmethod - def train(self) -> None: - """Switch the policy to training mode. - """ - raise NotImplementedError - @abstractmethod def get_state(self) -> object: """Get the state of the policy. diff --git a/maro/rl/policy/discrete_rl_policy.py b/maro/rl/policy/discrete_rl_policy.py index d092f2d01..4850f1ff3 100644 --- a/maro/rl/policy/discrete_rl_policy.py +++ b/maro/rl/policy/discrete_rl_policy.py @@ -98,7 +98,7 @@ def q_values_for_all_actions(self, states: np.ndarray) -> np.ndarray: Returns: q_values (np.ndarray): Q-matrix. """ - return self.q_values_for_all_actions_tensor(ndarray_to_tensor(states, self._device)).cpu().numpy() + return self.q_values_for_all_actions_tensor(ndarray_to_tensor(states, device=self._device)).cpu().numpy() def q_values_for_all_actions_tensor(self, states: torch.Tensor) -> torch.Tensor: """Generate a matrix containing the Q-values for all actions for the given states. @@ -125,8 +125,8 @@ def q_values(self, states: np.ndarray, actions: np.ndarray) -> np.ndarray: q_values (np.ndarray): Q-values. """ return self.q_values_tensor( - ndarray_to_tensor(states, self._device), - ndarray_to_tensor(actions, self._device) + ndarray_to_tensor(states, device=self._device), + ndarray_to_tensor(actions, device=self._device) ).cpu().numpy() def q_values_tensor(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: @@ -150,14 +150,14 @@ def explore(self) -> None: def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: self._call_cnt += 1 if self._call_cnt <= self._warmup: - return ndarray_to_tensor(np.random.randint(self.action_num, size=(states.shape[0], 1)), self._device) + return ndarray_to_tensor(np.random.randint(self.action_num, size=(states.shape[0], 1)), device=self._device) q_matrix = self.q_values_for_all_actions_tensor(states) # [B, action_num] _, actions = q_matrix.max(dim=1) # [B], [B] if exploring: actions = self._exploration_func(states, actions.cpu().numpy(), self.action_num, **self._exploration_params) - actions = ndarray_to_tensor(actions, self._device) + actions = ndarray_to_tensor(actions, device=self._device) return actions.unsqueeze(1) # [B, 1] def train_step(self, loss: torch.Tensor) -> None: diff --git a/maro/rl/rollout/env_sampler.py b/maro/rl/rollout/env_sampler.py index 7f0650e7c..f5c84fa86 100644 --- a/maro/rl/rollout/env_sampler.py +++ b/maro/rl/rollout/env_sampler.py @@ -12,7 +12,7 @@ import numpy as np import torch -from maro.rl.policy import RLPolicy +from maro.rl.policy import AbsPolicy, RLPolicy from maro.simulator import Env @@ -20,13 +20,13 @@ class AbsAgentWrapper(object, metaclass=ABCMeta): """Agent wrapper. Used to manager agents & policies during experience collection. Args: - policy_dict (Dict[str, RLPolicy]): Dictionary that maps policy names to policy instances. + policy_dict (Dict[str, AbsPolicy]): Dictionary that maps policy names to policy instances. agent2policy (Dict[Any, str]): Agent name to policy name mapping. """ def __init__( self, - policy_dict: Dict[str, RLPolicy], # {policy_name: RLPolicy} + policy_dict: Dict[str, AbsPolicy], # {policy_name: AbsPolicy} agent2policy: Dict[Any, str], # {agent_name: policy_name} ) -> None: self._policy_dict = policy_dict @@ -40,7 +40,8 @@ def set_policy_state(self, policy_state_dict: Dict[str, object]) -> None: """ for policy_name, policy_state in policy_state_dict.items(): policy = self._policy_dict[policy_name] - policy.set_state(policy_state) + if isinstance(policy, RLPolicy): + policy.set_state(policy_state) def choose_actions(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[Any, np.ndarray]: """Choose action according to the given (observable) states of all agents. @@ -105,7 +106,7 @@ def _choose_actions_impl(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[An states = np.vstack(states_by_policy[policy_name]) # np.ndarray action_dict.update(zip( agents_by_policy[policy_name], # list of str (agent name) - policy.get_actions(states) # list of action + policy.get_actions(states), # list of action )) return action_dict @@ -175,14 +176,14 @@ def split_contents(self, agent2trainer: Dict[Any, str]) -> Dict[str, ExpElement] next_state=self.next_state, next_agent_state_dict=None if self.next_agent_state_dict is None else {}, )) - for agent_name in self.agent_names: - trainer_name = agent2trainer[agent_name] - ret[trainer_name].agent_state_dict[agent_name] = self.agent_state_dict[agent_name] - ret[trainer_name].action_dict[agent_name] = self.action_dict[agent_name] - ret[trainer_name].reward_dict[agent_name] = self.reward_dict[agent_name] - ret[trainer_name].terminal_dict[agent_name] = self.terminal_dict[agent_name] - if self.next_agent_state_dict is not None and agent_name in self.next_agent_state_dict: - ret[trainer_name].next_agent_state_dict[agent_name] = self.next_agent_state_dict[agent_name] + for agent_name, trainer_name in agent2trainer.items(): + if agent_name in self.agent_state_dict: + ret[trainer_name].agent_state_dict[agent_name] = self.agent_state_dict[agent_name] + ret[trainer_name].action_dict[agent_name] = self.action_dict[agent_name] + ret[trainer_name].reward_dict[agent_name] = self.reward_dict[agent_name] + ret[trainer_name].terminal_dict[agent_name] = self.terminal_dict[agent_name] + if self.next_agent_state_dict is not None and agent_name in self.next_agent_state_dict: + ret[trainer_name].next_agent_state_dict[agent_name] = self.next_agent_state_dict[agent_name] return ret @@ -191,41 +192,48 @@ class AbsEnvSampler(object, metaclass=ABCMeta): Args: get_env (Callable[[], Env]): Function used to create the rollout environment. - policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict of functions that used to get policies, specified - by policy names. - agent2policy (Dict[Any, str]): Mapping of agent name and policy name. + policy_creator (Dict[str, Callable[[str], AbsPolicy]]): Dict of functions to create policies by name. + agent2policy (Dict[Any, str]): Mapping of agent name to policy name. + trainable_policies (List[str], default=None): List of trainable policy names. Experiences generated using the + policies specified in this list will be collected and passed to a training manager for training. Defaults + to None, in which case all policies are trainable. agent_wrapper_cls (Type[AbsAgentWrapper], default=SimpleAgentWrapper): Specific AgentWrapper type. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. get_test_env (Callable[[], Env], default=None): Function used to create the testing environment. If it is None, reuse the rollout environment as the testing environment. - device (str, default=None): Name of the device to store this AbsEnvSampler. If it is None, the device will - be automatically determined according to the GPU availability. """ def __init__( self, get_env: Callable[[], Env], - policy_creator: Dict[str, Callable[[str], RLPolicy]], + policy_creator: Dict[str, Callable[[str], AbsPolicy]], agent2policy: Dict[Any, str], # {agent_name: policy_name} + trainable_policies: List[str] = None, agent_wrapper_cls: Type[AbsAgentWrapper] = SimpleAgentWrapper, reward_eval_delay: int = 0, get_test_env: Callable[[], Env] = None, - device: str = None, ) -> None: self._learn_env = get_env() self._test_env = get_test_env() if get_test_env is not None else self._learn_env self._env: Optional[Env] = None self._event = None # Need this to remember the last event if an episode is divided into multiple segments - self._device = torch.device(device) if device is not None \ - else torch.device("cuda" if torch.cuda.is_available() else "cpu") - - self._policy_dict: Dict[str, RLPolicy] = { + self._policy_dict: Dict[str, AbsPolicy] = { policy_name: func(policy_name) for policy_name, func in policy_creator.items() } - self._agent_wrapper = agent_wrapper_cls(self._policy_dict, agent2policy) + self._rl_policy_dict: Dict[str, RLPolicy] = { + name: policy for name, policy in self._policy_dict.items() if isinstance(policy, RLPolicy) + } self._agent2policy = agent2policy + self._agent_wrapper = agent_wrapper_cls(self._policy_dict, agent2policy) + if trainable_policies is None: + self._trainable_policies = set(self._policy_dict.keys()) + else: + self._trainable_policies = set(trainable_policies) + self._trainable_agents = { + agent_id for agent_id, policy_name in self._agent2policy.items() if policy_name in self._trainable_policies + } # Global state & agent state self._state: Optional[np.ndarray] = None @@ -236,8 +244,9 @@ def __init__( self._info = {} - for policy in self._policy_dict.values(): - policy.to_device(self._device) + @property + def rl_policy_dict(self) -> Dict[str, RLPolicy]: + return self._rl_policy_dict @abstractmethod def _get_global_and_agent_state( @@ -314,20 +323,26 @@ def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Op # Get agent actions and translate them to env actions action_dict = self._agent_wrapper.choose_actions(self._agent_state_dict) env_action_dict = self._translate_to_env_action(action_dict, self._event) + # Store experiences in the cache self._trans_cache.append( CacheElement( tick=self._env.tick, event=self._event, state=self._state, - agent_state_dict=dict(self._agent_state_dict), - action_dict=action_dict, - env_action_dict=env_action_dict, + agent_state_dict={ + id_: state for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents + }, + action_dict={id_: action for id_, action in action_dict.items() if id_ in self._trainable_agents}, + env_action_dict={ + id_: env_action for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents + }, ) ) + # Update env and get new states (global & agent) - _, self._event, done = self._env.step(list(env_action_dict.values())) - self._state, self._agent_state_dict = (None, {}) if done \ + _, self._event, is_done = self._env.step(list(env_action_dict.values())) + self._state, self._agent_state_dict = (None, {}) if is_done \ else self._get_global_and_agent_state(self._event) steps_to_go -= 1 @@ -335,16 +350,17 @@ def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Op experiences = [] while len(self._trans_cache) > 0 and self._trans_cache[0].tick <= tick_bound: cache_element = self._trans_cache.popleft() - + # !: Here the reward calculation method requires the given tick is enough and must be used then. reward_dict = self._get_reward(cache_element.env_action_dict, cache_element.event, cache_element.tick) self._post_step(cache_element, reward_dict) - if len(self._trans_cache) > 0: next_state = self._trans_cache[0].state next_agent_state_dict = dict(self._trans_cache[0].agent_state_dict) else: next_state = self._state - next_agent_state_dict = dict(self._agent_state_dict) + next_agent_state_dict = { + id_: state for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents + } experiences.append(ExpElement( tick=cache_element.tick, @@ -403,17 +419,45 @@ def eval(self, policy_state: Dict[str, object] = None) -> dict: self._agent_wrapper.exploit() self._env.reset() - terminal = False + is_done = False _, self._event, _ = self._env.step(None) - _, agent_state_dict = self._get_global_and_agent_state(self._event) - while not terminal: - action_dict = self._agent_wrapper.choose_actions(agent_state_dict) + self._state, self._agent_state_dict = self._get_global_and_agent_state(self._event) + while not is_done: + action_dict = self._agent_wrapper.choose_actions(self._agent_state_dict) env_action_dict = self._translate_to_env_action(action_dict, self._event) - _, self._event, terminal = self._env.step(list(env_action_dict.values())) - if not terminal: - _, agent_state_dict = self._get_global_and_agent_state(self._event) + + # Store experiences in the cache + self._trans_cache.append( + CacheElement( + tick=self._env.tick, + event=self._event, + state=self._state, + agent_state_dict={ + id_: state for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents + }, + action_dict={id_: action for id_, action in action_dict.items() if id_ in self._trainable_agents}, + env_action_dict={ + id_: env_action for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents + }, + ) + ) + # Update env and get new states (global & agent) + _, self._event, is_done = self._env.step(list(env_action_dict.values())) + self._state, self._agent_state_dict = (None, {}) if is_done \ + else self._get_global_and_agent_state(self._event) + + tick_bound = self._env.tick - self._reward_eval_delay + while self._trans_cache and self._trans_cache[0].tick <= tick_bound: + cache_element = self._trans_cache.popleft() + reward_dict = self._get_reward(cache_element.env_action_dict, cache_element.event, cache_element.tick) + self._post_eval_step(cache_element, reward_dict) + return {"info": [self._info]} @abstractmethod def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: raise NotImplementedError + + @abstractmethod + def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: + raise NotImplementedError diff --git a/maro/rl/training/algorithms/__init__.py b/maro/rl/training/algorithms/__init__.py index 0191e39f1..d2a93f2ef 100644 --- a/maro/rl/training/algorithms/__init__.py +++ b/maro/rl/training/algorithms/__init__.py @@ -5,10 +5,12 @@ from .ddpg import DDPGParams, DDPGTrainer from .dqn import DQNParams, DQNTrainer from .maddpg import DiscreteMADDPGParams, DiscreteMADDPGTrainer +from .ppo import DiscretePPOParams, DiscretePPOTrainer __all__ = [ "DiscreteActorCriticTrainer", "DiscreteActorCriticParams", "DDPGTrainer", "DDPGParams", "DQNTrainer", "DQNParams", "DiscreteMADDPGTrainer", "DiscreteMADDPGParams", + "DiscretePPOParams", "DiscretePPOTrainer", ] diff --git a/maro/rl/training/algorithms/ac.py b/maro/rl/training/algorithms/ac.py index fc8909f5e..ead616c4f 100644 --- a/maro/rl/training/algorithms/ac.py +++ b/maro/rl/training/algorithms/ac.py @@ -1,304 +1,35 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import collections from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional +from typing import Dict -import numpy as np -import torch - -from maro.rl.model import VNet -from maro.rl.policy import DiscretePolicyGradient -from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, FIFOReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote -from maro.rl.utils import TransitionBatch, discount_cumsum, merge_transition_batches, ndarray_to_tensor +from maro.rl.training.algorithms.base import DiscreteACBasedParams, DiscreteACBasedTrainer @dataclass -class DiscreteActorCriticParams(TrainerParams): - """ - get_v_critic_net_func (Callable[[], VNet]): Function to get V critic net. - reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. - grad_iters (int, default=1): Number of iterations to calculate gradients. - critic_loss_cls (Callable, default=None): Critic loss function. If it is None, use MSE. - clip_ratio (float, default=None): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). - If it is None, the actor loss is calculated using the usual policy gradient theorem. - lam (float, default=0.9): Lambda value for generalized advantage estimation (TD-Lambda). - min_logp (float, default=None): Lower bound for clamping logP values during learning. - This is to prevent logP from becoming very large in magnitude and causing stability issues. - If it is None, it means no lower bound. +class DiscreteActorCriticParams(DiscreteACBasedParams): + """Identical to `DiscreteACBasedParams`. Please refer to the doc string of `DiscreteACBasedParams` + for detailed information. """ - get_v_critic_net_func: Callable[[], VNet] = None - reward_discount: float = 0.9 - grad_iters: int = 1 - critic_loss_cls: Callable = None - clip_ratio: float = None - lam: float = 0.9 - min_logp: Optional[float] = None - - def __post_init__(self) -> None: - assert self.get_v_critic_net_func is not None - def extract_ops_params(self) -> Dict[str, object]: return { - "device": self.device, "get_v_critic_net_func": self.get_v_critic_net_func, "reward_discount": self.reward_discount, "critic_loss_cls": self.critic_loss_cls, - "clip_ratio": self.clip_ratio, "lam": self.lam, "min_logp": self.min_logp, } - -class DiscreteActorCriticOps(AbsTrainOps): - """Discrete actor-critic algorithm implementation. Reference: https://tinyurl.com/2ezte4cr - """ - - def __init__( - self, - name: str, - device: str, - get_policy_func: Callable[[], DiscretePolicyGradient], - get_v_critic_net_func: Callable[[], VNet], - parallelism: int = 1, - *, - reward_discount: float = 0.9, - critic_loss_cls: Callable = None, - clip_ratio: float = None, - lam: float = 0.9, - min_logp: float = None, - ) -> None: - super(DiscreteActorCriticOps, self).__init__( - name=name, - device=device, - is_single_scenario=True, - get_policy_func=get_policy_func, - parallelism=parallelism, - ) - - assert isinstance(self._policy, DiscretePolicyGradient) - - self._reward_discount = reward_discount - self._critic_loss_func = critic_loss_cls() if critic_loss_cls is not None else torch.nn.MSELoss() - self._clip_ratio = clip_ratio - self._lam = lam - self._min_logp = min_logp - self._v_critic_net = get_v_critic_net_func() - self._v_critic_net.to(self._device) - - def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: - """Compute the critic loss of the batch. - - Args: - batch (TransitionBatch): Batch. - - Returns: - loss (torch.Tensor): The critic loss of the batch. - """ - self._v_critic_net.train() - states = ndarray_to_tensor(batch.states, self._device) # s - state_values = self._v_critic_net.v_values(states) - returns = ndarray_to_tensor(batch.returns, self._device) - return self._critic_loss_func(state_values, returns) - - @remote - def get_critic_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: - """Compute the critic network's gradients of a batch. - - Args: - batch (TransitionBatch): Batch. - - Returns: - grad (torch.Tensor): The critic gradient of the batch. - """ - return self._v_critic_net.get_gradients(self._get_critic_loss(batch)) - - def update_critic(self, batch: TransitionBatch) -> None: - """Update the critic network using a batch. - - Args: - batch (TransitionBatch): Batch. - """ - self._v_critic_net.step(self._get_critic_loss(batch)) - - def update_critic_with_grad(self, grad_dict: dict) -> None: - """Update the critic network with remotely computed gradients. - - Args: - grad_dict (dict): Gradients. - """ - self._v_critic_net.train() - self._v_critic_net.apply_gradients(grad_dict) - - def _get_actor_loss(self, batch: TransitionBatch) -> torch.Tensor: - """Compute the actor loss of the batch. - - Args: - batch (TransitionBatch): Batch. - - Returns: - loss (torch.Tensor): The actor loss of the batch. - """ - assert isinstance(self._policy, DiscretePolicyGradient) - self._policy.train() - - states = ndarray_to_tensor(batch.states, self._device) # s - actions = ndarray_to_tensor(batch.actions, self._device).long() # a - advantages = ndarray_to_tensor(batch.advantages, self._device) - - if self._clip_ratio is not None: - self._policy.eval() - logps_old = self._policy.get_state_action_logps(states, actions) - else: - logps_old = None - - action_probs = self._policy.get_action_probs(states) - logps = torch.log(action_probs.gather(1, actions).squeeze()) - logps = torch.clamp(logps, min=self._min_logp, max=.0) - if self._clip_ratio is not None: - ratio = torch.exp(logps - logps_old) - clipped_ratio = torch.clamp(ratio, 1 - self._clip_ratio, 1 + self._clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).mean() - else: - actor_loss = -(logps * advantages).mean() # I * delta * log pi(a|s) - - return actor_loss - - @remote - def get_actor_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: - """Compute the actor network's gradients of a batch. - - Args: - batch (TransitionBatch): Batch. - - Returns: - grad (torch.Tensor): The actor gradient of the batch. - """ - return self._policy.get_gradients(self._get_actor_loss(batch)) - - def update_actor(self, batch: TransitionBatch) -> None: - """Update the actor network using a batch. - - Args: - batch (TransitionBatch): Batch. - """ - self._policy.train_step(self._get_actor_loss(batch)) - - def update_actor_with_grad(self, grad_dict: dict) -> None: - """Update the actor network with remotely computed gradients. - - Args: - grad_dict (dict): Gradients. - """ - self._policy.train() - self._policy.apply_gradients(grad_dict) - - def get_state(self) -> dict: - return { - "policy": self._policy.get_state(), - "critic": self._v_critic_net.get_state(), - } - - def set_state(self, ops_state_dict: dict) -> None: - self._policy.set_state(ops_state_dict["policy"]) - self._v_critic_net.set_state(ops_state_dict["critic"]) - - def _preprocess_batch(self, batch: TransitionBatch) -> TransitionBatch: - """Preprocess the batch to get the returns & advantages. - - Args: - batch (TransitionBatch): Batch. - - Returns: - The updated batch. - """ - assert self._is_valid_transition_batch(batch) - # Preprocess returns - batch.calc_returns(self._reward_discount) - - # Preprocess advantages - states = ndarray_to_tensor(batch.states, self._device) # s - state_values = self._v_critic_net.v_values(states) - values = state_values.detach().numpy() - values = np.concatenate([values, values[-1:]]) - rewards = np.concatenate([batch.rewards, values[-1:]]) - deltas = rewards[:-1] + self._reward_discount * values[1:] - values[:-1] # r + gamma * v(s') - v(s) - advantages = discount_cumsum(deltas, self._reward_discount * self._lam) - batch.advantages = advantages - return batch - - def preprocess_and_merge_batches(self, batch_list: List[TransitionBatch]) -> TransitionBatch: - """Preprocess and merge a list of transition batches to a single transition batch. - - Args: - batch_list (List[TransitionBatch]): List of batches. - - Returns: - The merged batch. - """ - return merge_transition_batches([self._preprocess_batch(batch) for batch in batch_list]) + def __post_init__(self) -> None: + assert self.get_v_critic_net_func is not None -class DiscreteActorCriticTrainer(SingleAgentTrainer): +class DiscreteActorCriticTrainer(DiscreteACBasedTrainer): """Actor Critic algorithm with separate policy and value models. - References: - https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch. - https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f + Reference: + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/vpg """ - def __init__(self, name: str, params: DiscreteActorCriticParams) -> None: super(DiscreteActorCriticTrainer, self).__init__(name, params) - self._params = params - self._ops_name = f"{self._name}.ops" - - self._replay_memory_dict: Dict[Any, FIFOReplayMemory] = {} - - def build(self) -> None: - self._ops = self.get_ops(self._ops_name) - self._replay_memory_dict = collections.defaultdict(lambda: FIFOReplayMemory( - capacity=self._params.replay_memory_capacity, - state_dim=self._ops.policy_state_dim, - action_dim=self._ops.policy_action_dim, - )) - - def record(self, env_idx: int, exp_element: ExpElement) -> None: - for agent_name in exp_element.agent_names: - memory = self._replay_memory_dict[(env_idx, agent_name)] - transition_batch = TransitionBatch( - states=np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0), - actions=np.expand_dims(exp_element.action_dict[agent_name], axis=0), - rewards=np.array([exp_element.reward_dict[agent_name]]), - terminals=np.array([exp_element.terminal_dict[agent_name]]), - next_states=np.expand_dims( - exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), - axis=0, - ), - ) - memory.put(transition_batch) - - def get_local_ops_by_name(self, name: str) -> AbsTrainOps: - return DiscreteActorCriticOps( - name=name, get_policy_func=self._get_policy_func, parallelism=self._params.data_parallelism, - **self._params.extract_ops_params(), - ) - - def _get_batch(self) -> TransitionBatch: - batch_list = [memory.sample(-1) for memory in self._replay_memory_dict.values()] - return self._ops.preprocess_and_merge_batches(batch_list) - - def train_step(self) -> None: - assert isinstance(self._ops, DiscreteActorCriticOps) - batch = self._get_batch() - for _ in range(self._params.grad_iters): - self._ops.update_critic(batch) - self._ops.update_actor(batch) - - async def train_step_as_task(self) -> None: - assert isinstance(self._ops, RemoteOps) - batch = self._get_batch() - for _ in range(self._params.grad_iters): - self._ops.update_critic_with_grad(await self._ops.get_critic_grad(batch)) - self._ops.update_actor_with_grad(await self._ops.get_actor_grad(batch)) diff --git a/maro/rl/training/algorithms/base/__init__.py b/maro/rl/training/algorithms/base/__init__.py new file mode 100644 index 000000000..1d555f10a --- /dev/null +++ b/maro/rl/training/algorithms/base/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from .ac_ppo_base import DiscreteACBasedOps, DiscreteACBasedParams, DiscreteACBasedTrainer + +__all__ = ["DiscreteACBasedOps", "DiscreteACBasedParams", "DiscreteACBasedTrainer"] diff --git a/maro/rl/training/algorithms/base/ac_ppo_base.py b/maro/rl/training/algorithms/base/ac_ppo_base.py new file mode 100644 index 000000000..708e88bd0 --- /dev/null +++ b/maro/rl/training/algorithms/base/ac_ppo_base.py @@ -0,0 +1,294 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import collections +from abc import ABCMeta +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional + +import numpy as np +import torch + +from maro.rl.model import VNet +from maro.rl.policy import DiscretePolicyGradient, RLPolicy +from maro.rl.rollout import ExpElement +from maro.rl.training import AbsTrainOps, FIFOReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote +from maro.rl.utils import ( + TransitionBatch, discount_cumsum, get_torch_device, merge_transition_batches, ndarray_to_tensor +) + + +@dataclass +class DiscreteACBasedParams(TrainerParams, metaclass=ABCMeta): + """ + Parameter bundle for discrete actor-critic based algorithms (discrete actor-critic & discrete PPO) + + get_v_critic_net_func (Callable[[], VNet]): Function to get V critic net. + reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. + grad_iters (int, default=1): Number of iterations to calculate gradients. + critic_loss_cls (Callable, default=None): Critic loss function. If it is None, use MSE. + lam (float, default=0.9): Lambda value for generalized advantage estimation (TD-Lambda). + min_logp (float, default=None): Lower bound for clamping logP values during learning. + This is to prevent logP from becoming very large in magnitude and causing stability issues. + If it is None, it means no lower bound. + """ + get_v_critic_net_func: Callable[[], VNet] = None + reward_discount: float = 0.9 + grad_iters: int = 1 + critic_loss_cls: Callable = None + lam: float = 0.9 + min_logp: Optional[float] = None + + +class DiscreteACBasedOps(AbsTrainOps): + """Base class of discrete actor-critic algorithm implementation. Reference: https://tinyurl.com/2ezte4cr + """ + def __init__( + self, + name: str, + policy_creator: Callable[[str], RLPolicy], + get_v_critic_net_func: Callable[[], VNet], + parallelism: int = 1, + *, + reward_discount: float = 0.9, + critic_loss_cls: Callable = None, + clip_ratio: float = None, + lam: float = 0.9, + min_logp: float = None, + ) -> None: + super(DiscreteACBasedOps, self).__init__( + name=name, + policy_creator=policy_creator, + parallelism=parallelism, + ) + + assert isinstance(self._policy, DiscretePolicyGradient) + + self._reward_discount = reward_discount + self._critic_loss_func = critic_loss_cls() if critic_loss_cls is not None else torch.nn.MSELoss() + self._clip_ratio = clip_ratio + self._lam = lam + self._min_logp = min_logp + self._v_critic_net = get_v_critic_net_func() + + self._device = None + + def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: + """Compute the critic loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The critic loss of the batch. + """ + self._v_critic_net.train() + states = ndarray_to_tensor(batch.states, device=self._device) # s + state_values = self._v_critic_net.v_values(states) + returns = ndarray_to_tensor(batch.returns, device=self._device) + return self._critic_loss_func(state_values, returns) + + @remote + def get_critic_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + """Compute the critic network's gradients of a batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + grad (torch.Tensor): The critic gradient of the batch. + """ + return self._v_critic_net.get_gradients(self._get_critic_loss(batch)) + + def update_critic(self, batch: TransitionBatch) -> None: + """Update the critic network using a batch. + + Args: + batch (TransitionBatch): Batch. + """ + self._v_critic_net.step(self._get_critic_loss(batch)) + + def update_critic_with_grad(self, grad_dict: dict) -> None: + """Update the critic network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._v_critic_net.train() + self._v_critic_net.apply_gradients(grad_dict) + + def _get_actor_loss(self, batch: TransitionBatch) -> torch.Tensor: + """Compute the actor loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The actor loss of the batch. + """ + assert isinstance(self._policy, DiscretePolicyGradient) + self._policy.train() + + states = ndarray_to_tensor(batch.states, device=self._device) # s + actions = ndarray_to_tensor(batch.actions, device=self._device).long() # a + advantages = ndarray_to_tensor(batch.advantages, device=self._device) + + action_probs = self._policy.get_action_probs(states) + logps = torch.log(action_probs.gather(1, actions).squeeze()) + logps = torch.clamp(logps, min=self._min_logp, max=.0) + if self._clip_ratio is not None: + logps_old = ndarray_to_tensor(batch.old_logps, device=self._device) + ratio = torch.exp(logps - logps_old) + clipped_ratio = torch.clamp(ratio, 1 - self._clip_ratio, 1 + self._clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).mean() + else: + actor_loss = -(logps * advantages).mean() # I * delta * log pi(a|s) + + return actor_loss + + @remote + def get_actor_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + """Compute the actor network's gradients of a batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + grad (torch.Tensor): The actor gradient of the batch. + """ + return self._policy.get_gradients(self._get_actor_loss(batch)) + + def update_actor(self, batch: TransitionBatch) -> None: + """Update the actor network using a batch. + + Args: + batch (TransitionBatch): Batch. + """ + self._policy.train_step(self._get_actor_loss(batch)) + + def update_actor_with_grad(self, grad_dict: dict) -> None: + """Update the actor network with remotely computed gradients. + + Args: + grad_dict (dict): Gradients. + """ + self._policy.train() + self._policy.apply_gradients(grad_dict) + + def get_state(self) -> dict: + return { + "policy": self._policy.get_state(), + "critic": self._v_critic_net.get_state(), + } + + def set_state(self, ops_state_dict: dict) -> None: + self._policy.set_state(ops_state_dict["policy"]) + self._v_critic_net.set_state(ops_state_dict["critic"]) + + def _preprocess_batch(self, batch: TransitionBatch) -> TransitionBatch: + """Preprocess the batch to get the returns & advantages. + + Args: + batch (TransitionBatch): Batch. + + Returns: + The updated batch. + """ + assert isinstance(batch, TransitionBatch) + # Preprocess returns + batch.calc_returns(self._reward_discount) + + # Preprocess advantages + states = ndarray_to_tensor(batch.states, device=self._device) # s + actions = ndarray_to_tensor(batch.actions, device=self._device).long() # a + + values = self._v_critic_net.v_values(states).detach().cpu().numpy() + values = np.concatenate([values, values[-1:]]) + rewards = np.concatenate([batch.rewards, values[-1:]]) + deltas = rewards[:-1] + self._reward_discount * values[1:] - values[:-1] # r + gamma * v(s') - v(s) + advantages = discount_cumsum(deltas, self._reward_discount * self._lam) + batch.advantages = advantages + + if self._clip_ratio is not None: + batch.old_logps = self._policy.get_state_action_logps(states, actions).detach().cpu().numpy() + + return batch + + def preprocess_and_merge_batches(self, batch_list: List[TransitionBatch]) -> TransitionBatch: + """Preprocess and merge a list of transition batches to a single transition batch. + + Args: + batch_list (List[TransitionBatch]): List of batches. + + Returns: + The merged batch. + """ + return merge_transition_batches([self._preprocess_batch(batch) for batch in batch_list]) + + def to_device(self, device: str = None) -> None: + self._device = get_torch_device(device) + self._policy.to_device(self._device) + self._v_critic_net.to(self._device) + + +class DiscreteACBasedTrainer(SingleAgentTrainer): + """Base class of discrete actor-critic algorithm implementation. + + References: + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch + https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f + """ + + def __init__(self, name: str, params: DiscreteACBasedParams) -> None: + super(DiscreteACBasedTrainer, self).__init__(name, params) + self._params = params + self._replay_memory_dict: Dict[Any, FIFOReplayMemory] = {} + + def build(self) -> None: + self._ops = self.get_ops() + self._replay_memory_dict = collections.defaultdict(lambda: FIFOReplayMemory( + capacity=self._params.replay_memory_capacity, + state_dim=self._ops.policy_state_dim, + action_dim=self._ops.policy_action_dim, + )) + + def record(self, env_idx: int, exp_element: ExpElement) -> None: + for agent_name in exp_element.agent_names: + memory = self._replay_memory_dict[(env_idx, agent_name)] + transition_batch = TransitionBatch( + states=np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0), + actions=np.expand_dims(exp_element.action_dict[agent_name], axis=0), + rewards=np.array([exp_element.reward_dict[agent_name]]), + terminals=np.array([exp_element.terminal_dict[agent_name]]), + next_states=np.expand_dims( + exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), + axis=0, + ), + ) + memory.put(transition_batch) + + def get_local_ops(self) -> AbsTrainOps: + return DiscreteACBasedOps( + name=self._policy_name, + policy_creator=self._policy_creator, + parallelism=self._params.data_parallelism, + **self._params.extract_ops_params(), + ) + + def _get_batch(self) -> TransitionBatch: + batch_list = [memory.sample(-1) for memory in self._replay_memory_dict.values()] + return self._ops.preprocess_and_merge_batches(batch_list) + + def train_step(self) -> None: + assert isinstance(self._ops, DiscreteACBasedOps) + batch = self._get_batch() + for _ in range(self._params.grad_iters): + self._ops.update_critic(batch) + self._ops.update_actor(batch) + + async def train_step_as_task(self) -> None: + assert isinstance(self._ops, RemoteOps) + batch = self._get_batch() + for _ in range(self._params.grad_iters): + self._ops.update_critic_with_grad(await self._ops.get_critic_grad(batch)) + self._ops.update_actor_with_grad(await self._ops.get_actor_grad(batch)) diff --git a/maro/rl/training/algorithms/ddpg.py b/maro/rl/training/algorithms/ddpg.py index 45a90c064..d46176596 100644 --- a/maro/rl/training/algorithms/ddpg.py +++ b/maro/rl/training/algorithms/ddpg.py @@ -10,10 +10,10 @@ import torch from maro.rl.model import QNet -from maro.rl.policy import ContinuousRLPolicy +from maro.rl.policy import ContinuousRLPolicy, RLPolicy from maro.rl.rollout import ExpElement from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote -from maro.rl.utils import TransitionBatch, ndarray_to_tensor +from maro.rl.utils import TransitionBatch, get_torch_device, ndarray_to_tensor from maro.utils import clone @@ -46,7 +46,6 @@ def __post_init__(self) -> None: def extract_ops_params(self) -> Dict[str, object]: return { - "device": self.device, "get_q_critic_net_func": self.get_q_critic_net_func, "reward_discount": self.reward_discount, "q_value_loss_cls": self.q_value_loss_cls, @@ -62,8 +61,7 @@ class DDPGOps(AbsTrainOps): def __init__( self, name: str, - device: str, - get_policy_func: Callable[[], ContinuousRLPolicy], + policy_creator: Callable[[], RLPolicy], get_q_critic_net_func: Callable[[], QNet], parallelism: int = 1, *, @@ -73,9 +71,7 @@ def __init__( ) -> None: super(DDPGOps, self).__init__( name=name, - device=device, - is_single_scenario=True, - get_policy_func=get_policy_func, + policy_creator=policy_creator, parallelism=parallelism, ) @@ -84,12 +80,9 @@ def __init__( self._target_policy = clone(self._policy) self._target_policy.set_name(f"target_{self._policy.name}") self._target_policy.eval() - self._target_policy.to_device(self._device) self._q_critic_net = get_q_critic_net_func() - self._q_critic_net.to(self._device) self._target_q_critic_net: QNet = clone(self._q_critic_net) self._target_q_critic_net.eval() - self._target_q_critic_net.to(self._device) self._reward_discount = reward_discount self._q_value_loss_func = q_value_loss_cls() if q_value_loss_cls is not None else torch.nn.MSELoss() @@ -104,13 +97,13 @@ def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: Returns: loss (torch.Tensor): The critic loss of the batch. """ - assert self._is_valid_transition_batch(batch) + assert isinstance(batch, TransitionBatch) self._q_critic_net.train() - states = ndarray_to_tensor(batch.states, self._device) # s - next_states = ndarray_to_tensor(batch.next_states, self._device) # s' - actions = ndarray_to_tensor(batch.actions, self._device) # a - rewards = ndarray_to_tensor(batch.rewards, self._device) # r - terminals = ndarray_to_tensor(batch.terminals, self._device) # d + states = ndarray_to_tensor(batch.states, device=self._device) # s + next_states = ndarray_to_tensor(batch.next_states, device=self._device) # s' + actions = ndarray_to_tensor(batch.actions, device=self._device) # a + rewards = ndarray_to_tensor(batch.rewards, device=self._device) # r + terminals = ndarray_to_tensor(batch.terminals, device=self._device) # d with torch.no_grad(): next_q_values = self._target_q_critic_net.q_values( @@ -162,9 +155,9 @@ def _get_actor_loss(self, batch: TransitionBatch) -> torch.Tensor: Returns: loss (torch.Tensor): The actor loss of the batch. """ - assert self._is_valid_transition_batch(batch) + assert isinstance(batch, TransitionBatch) self._policy.train() - states = ndarray_to_tensor(batch.states, self._device) # s + states = ndarray_to_tensor(batch.states, device=self._device) # s policy_loss = -self._q_critic_net.q_values( states=states, # s @@ -223,6 +216,13 @@ def soft_update_target(self) -> None: self._target_policy.soft_update(self._policy, self._soft_update_coef) self._target_q_critic_net.soft_update(self._q_critic_net, self._soft_update_coef) + def to_device(self, device: str) -> None: + self._device = get_torch_device(device=device) + self._policy.to_device(self._device) + self._target_policy.to_device(self._device) + self._q_critic_net.to(self._device) + self._target_q_critic_net.to(self._device) + class DDPGTrainer(SingleAgentTrainer): """The Deep Deterministic Policy Gradient (DDPG) algorithm. @@ -236,12 +236,10 @@ def __init__(self, name: str, params: DDPGParams) -> None: super(DDPGTrainer, self).__init__(name, params) self._params = params self._policy_version = self._target_policy_version = 0 - self._ops_name = f"{self._name}.ops" - self._replay_memory: Optional[RandomReplayMemory] = None def build(self) -> None: - self._ops = self.get_ops(self._ops_name) + self._ops = self.get_ops() self._replay_memory = RandomReplayMemory( capacity=self._params.replay_memory_capacity, state_dim=self._ops.policy_state_dim, @@ -263,9 +261,11 @@ def record(self, env_idx: int, exp_element: ExpElement) -> None: ) self._replay_memory.put(transition_batch) - def get_local_ops_by_name(self, name: str) -> AbsTrainOps: + def get_local_ops(self) -> AbsTrainOps: return DDPGOps( - name=name, get_policy_func=self._get_policy_func, parallelism=self._params.data_parallelism, + name=self._policy_name, + policy_creator=self._policy_creator, + parallelism=self._params.data_parallelism, **self._params.extract_ops_params(), ) diff --git a/maro/rl/training/algorithms/dqn.py b/maro/rl/training/algorithms/dqn.py index 58c207b56..517ca5daa 100644 --- a/maro/rl/training/algorithms/dqn.py +++ b/maro/rl/training/algorithms/dqn.py @@ -7,10 +7,10 @@ import numpy as np import torch -from maro.rl.policy import ValueBasedPolicy +from maro.rl.policy import RLPolicy, ValueBasedPolicy from maro.rl.rollout import ExpElement from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote -from maro.rl.utils import TransitionBatch, ndarray_to_tensor +from maro.rl.utils import TransitionBatch, get_torch_device, ndarray_to_tensor from maro.utils import clone @@ -38,7 +38,6 @@ class DQNParams(TrainerParams): def extract_ops_params(self) -> Dict[str, object]: return { - "device": self.device, "reward_discount": self.reward_discount, "soft_update_coef": self.soft_update_coef, "double": self.double, @@ -49,8 +48,7 @@ class DQNOps(AbsTrainOps): def __init__( self, name: str, - device: str, - get_policy_func: Callable[[], ValueBasedPolicy], + policy_creator: Callable[[str], RLPolicy], parallelism: int = 1, *, reward_discount: float = 0.9, @@ -59,9 +57,7 @@ def __init__( ) -> None: super(DQNOps, self).__init__( name=name, - device=device, - is_single_scenario=True, - get_policy_func=get_policy_func, + policy_creator=policy_creator, parallelism=parallelism, ) @@ -75,7 +71,6 @@ def __init__( self._target_policy: ValueBasedPolicy = clone(self._policy) self._target_policy.set_name(f"target_{self._policy.name}") self._target_policy.eval() - self._target_policy.to_device(self._device) def _get_batch_loss(self, batch: TransitionBatch) -> Dict[str, Dict[str, torch.Tensor]]: """Compute the loss of the batch. @@ -86,13 +81,13 @@ def _get_batch_loss(self, batch: TransitionBatch) -> Dict[str, Dict[str, torch.T Returns: loss (torch.Tensor): The loss of the batch. """ - assert self._is_valid_transition_batch(batch) + assert isinstance(batch, TransitionBatch) self._policy.train() - states = ndarray_to_tensor(batch.states, self._device) - next_states = ndarray_to_tensor(batch.next_states, self._device) - actions = ndarray_to_tensor(batch.actions, self._device) - rewards = ndarray_to_tensor(batch.rewards, self._device) - terminals = ndarray_to_tensor(batch.terminals, self._device).float() + states = ndarray_to_tensor(batch.states, device=self._device) + next_states = ndarray_to_tensor(batch.next_states, device=self._device) + actions = ndarray_to_tensor(batch.actions, device=self._device) + rewards = ndarray_to_tensor(batch.rewards, device=self._device) + terminals = ndarray_to_tensor(batch.terminals, device=self._device).float() with torch.no_grad(): if self._double: @@ -153,6 +148,11 @@ def soft_update_target(self) -> None: """ self._target_policy.soft_update(self._policy, self._soft_update_coef) + def to_device(self, device: str) -> None: + self._device = get_torch_device(device) + self._policy.to_device(self._device) + self._target_policy.to_device(self._device) + class DQNTrainer(SingleAgentTrainer): """The Deep-Q-Networks algorithm. @@ -164,12 +164,10 @@ def __init__(self, name: str, params: DQNParams) -> None: super(DQNTrainer, self).__init__(name, params) self._params = params self._q_net_version = self._target_q_net_version = 0 - self._ops_name = f"{self._name}.ops" - self._replay_memory: Optional[RandomReplayMemory] = None def build(self) -> None: - self._ops = self.get_ops(self._ops_name) + self._ops = self.get_ops() self._replay_memory = RandomReplayMemory( capacity=self._params.replay_memory_capacity, state_dim=self._ops.policy_state_dim, @@ -191,10 +189,10 @@ def record(self, env_idx: int, exp_element: ExpElement) -> None: ) self._replay_memory.put(transition_batch) - def get_local_ops_by_name(self, name: str) -> AbsTrainOps: + def get_local_ops(self) -> AbsTrainOps: return DQNOps( - name=name, - get_policy_func=self._get_policy_func, + name=self._policy_name, + policy_creator=self._policy_creator, parallelism=self._params.data_parallelism, **self._params.extract_ops_params(), ) diff --git a/maro/rl/training/algorithms/maddpg.py b/maro/rl/training/algorithms/maddpg.py index 92dc70661..69e80b2ac 100644 --- a/maro/rl/training/algorithms/maddpg.py +++ b/maro/rl/training/algorithms/maddpg.py @@ -12,7 +12,7 @@ from maro.rl.policy import DiscretePolicyGradient from maro.rl.rollout import ExpElement from maro.rl.training import AbsTrainOps, MultiAgentTrainer, RandomMultiReplayMemory, RemoteOps, TrainerParams, remote -from maro.rl.utils import MultiTransitionBatch, ndarray_to_tensor +from maro.rl.utils import MultiTransitionBatch, get_torch_device, ndarray_to_tensor from maro.utils import clone @@ -41,7 +41,6 @@ def __post_init__(self) -> None: def extract_ops_params(self) -> Dict[str, object]: return { - "device": self.device, "get_q_critic_net_func": self.get_q_critic_net_func, "shared_critic": self.shared_critic, "reward_discount": self.reward_discount, @@ -55,11 +54,10 @@ class DiscreteMADDPGOps(AbsTrainOps): def __init__( self, name: str, - device: str, - get_policy_func: Callable[[], DiscretePolicyGradient], + policy_creator: Callable[[str], DiscretePolicyGradient], get_q_critic_net_func: Callable[[], MultiQNet], policy_idx: int, - create_actor: bool, + parallelism: int = 1, *, shared_critic: bool = False, reward_discount: float = 0.9, @@ -69,39 +67,32 @@ def __init__( ) -> None: super(DiscreteMADDPGOps, self).__init__( name=name, - device=device, - is_single_scenario=False, - get_policy_func=get_policy_func, + policy_creator=policy_creator, + parallelism=parallelism ) self._policy_idx = policy_idx self._shared_critic = shared_critic # Actor - self._create_actor = create_actor - if create_actor: - self._policy = get_policy_func() + if self._policy_creator: assert isinstance(self._policy, DiscretePolicyGradient) - - self._policy.to_device(self._device) self._target_policy: DiscretePolicyGradient = clone(self._policy) self._target_policy.set_name(f"target_{self._policy.name}") self._target_policy.eval() - self._target_policy.to_device(self._device) # Critic self._q_critic_net: MultiQNet = get_q_critic_net_func() - self._q_critic_net.to(self._device) self._target_q_critic_net: MultiQNet = clone(self._q_critic_net) self._target_q_critic_net.eval() - self._target_q_critic_net.to(self._device) - # self._reward_discount = reward_discount self._q_value_loss_func = q_value_loss_func self._update_target_every = update_target_every self._soft_update_coef = soft_update_coef + self._device = None + def get_target_action(self, batch: MultiTransitionBatch) -> torch.Tensor: """Get the target policies' actions according to the batch. @@ -111,7 +102,7 @@ def get_target_action(self, batch: MultiTransitionBatch) -> torch.Tensor: Returns: actions (torch.Tensor): Target policies' actions. """ - agent_state = ndarray_to_tensor(batch.agent_states[self._policy_idx], self._device) + agent_state = ndarray_to_tensor(batch.agent_states[self._policy_idx], device=self._device) return self._target_policy.get_actions_tensor(agent_state) def get_latest_action(self, batch: MultiTransitionBatch) -> Tuple[torch.Tensor, torch.Tensor]: @@ -126,7 +117,7 @@ def get_latest_action(self, batch: MultiTransitionBatch) -> Tuple[torch.Tensor, """ assert isinstance(self._policy, DiscretePolicyGradient) - agent_state = ndarray_to_tensor(batch.agent_states[self._policy_idx], self._device) + agent_state = ndarray_to_tensor(batch.agent_states[self._policy_idx], device=self._device) self._policy.train() action = self._policy.get_actions_tensor(agent_state) logps = self._policy.get_state_action_logps(agent_state, action) @@ -145,11 +136,11 @@ def _get_critic_loss(self, batch: MultiTransitionBatch, next_actions: List[torch assert not self._shared_critic assert isinstance(next_actions, list) and all(isinstance(action, torch.Tensor) for action in next_actions) - states = ndarray_to_tensor(batch.states, self._device) # x - actions = [ndarray_to_tensor(action, self._device) for action in batch.actions] # a - next_states = ndarray_to_tensor(batch.next_states, self._device) # x' - rewards = ndarray_to_tensor(np.vstack([reward for reward in batch.rewards]), self._device) # r - terminals = ndarray_to_tensor(batch.terminals, self._device) # d + states = ndarray_to_tensor(batch.states, device=self._device) # x + actions = [ndarray_to_tensor(action, device=self._device) for action in batch.actions] # a + next_states = ndarray_to_tensor(batch.next_states, device=self._device) # x' + rewards = ndarray_to_tensor(np.vstack([reward for reward in batch.rewards]), device=self._device) # r + terminals = ndarray_to_tensor(batch.terminals, device=self._device) # d self._q_critic_net.train() with torch.no_grad(): @@ -212,8 +203,8 @@ def _get_actor_loss(self, batch: MultiTransitionBatch) -> torch.Tensor: loss (torch.Tensor): The actor loss of the batch. """ latest_action, latest_action_logp = self.get_latest_action(batch) - states = ndarray_to_tensor(batch.states, self._device) # x - actions = [ndarray_to_tensor(action, self._device) for action in batch.actions] # a + states = ndarray_to_tensor(batch.states, device=self._device) # x + actions = [ndarray_to_tensor(action, device=self._device) for action in batch.actions] # a actions[self._policy_idx] = latest_action self._policy.train() self._q_critic_net.freeze() @@ -257,7 +248,7 @@ def update_actor_with_grad(self, grad_dict: dict) -> None: def soft_update_target(self) -> None: """Soft update the target policies and target critics. """ - if self._create_actor: + if self._policy_creator: self._target_policy.soft_update(self._policy, self._soft_update_coef) if not self._shared_critic: self._target_q_critic_net.soft_update(self._q_critic_net, self._soft_update_coef) @@ -273,13 +264,13 @@ def set_critic_state(self, ops_state_dict: dict) -> None: self._target_q_critic_net.set_state(ops_state_dict["target_critic"]) def get_actor_state(self) -> dict: - if self._create_actor: + if self._policy_creator: return {"policy": self._policy.get_state(), "target_policy": self._target_policy.get_state()} else: return {} def set_actor_state(self, ops_state_dict: dict) -> None: - if self._create_actor: + if self._policy_creator: self._policy.set_state(ops_state_dict["policy"]) self._target_policy.set_state(ops_state_dict["target_policy"]) @@ -290,29 +281,43 @@ def set_state(self, ops_state_dict: dict) -> None: self.set_critic_state(ops_state_dict) self.set_actor_state(ops_state_dict) + def to_device(self, device: str) -> None: + self._device = get_torch_device(device) + if self._policy_creator: + self._policy.to_device(self._device) + self._target_policy.to_device(self._device) + + self._q_critic_net.to(self._device) + self._target_q_critic_net.to(self._device) + class DiscreteMADDPGTrainer(MultiAgentTrainer): + """Multi-agent deep deterministic policy gradient (MADDPG) algorithm adapted for discrete action space. + + See https://arxiv.org/abs/1706.02275 for details. + """ def __init__(self, name: str, params: DiscreteMADDPGParams) -> None: super(DiscreteMADDPGTrainer, self).__init__(name, params) self._params = params self._ops_params = self._params.extract_ops_params() self._state_dim = params.get_q_critic_net_func().state_dim self._policy_version = self._target_policy_version = 0 - self._shared_critic_ops_name = f"{self._name}.shared_critic_ops" + self._shared_critic_ops_name = f"{self._name}.shared_critic" self._actor_ops_list = [] - self._actor_ops_dict = {} self._critic_ops = None self._replay_memory = None self._policy2agent = {} def build(self) -> None: - self._actor_ops_list = [self.get_ops(f"{self._name}.actor_{i}_ops") for i in range(len(self._policy_names))] - self._actor_ops_dict = {ops.name: ops for ops in self._actor_ops_list} + for policy_name in self._policy_creator: + self._ops_dict[policy_name] = self.get_ops(policy_name) + + self._actor_ops_list = list(self._ops_dict.values()) + if self._params.shared_critic: - self._critic_ops = self.get_ops(self._shared_critic_ops_name) - else: - self._critic_ops = None + self._ops_dict[self._shared_critic_ops_name] = self.get_ops(self._shared_critic_ops_name) + self._critic_ops = self._ops_dict[self._shared_critic_ops_name] self._replay_memory = RandomMultiReplayMemory( capacity=self._params.replay_memory_capacity, @@ -357,31 +362,25 @@ def record(self, env_idx: int, exp_element: ExpElement) -> None: ) self._replay_memory.put(transition_batch) - def _get_batch(self, batch_size: int = None) -> MultiTransitionBatch: - return self._replay_memory.sample(batch_size if batch_size is not None else self._batch_size) - - def get_local_ops_by_name(self, name: str) -> AbsTrainOps: + def get_local_ops(self, name: str) -> AbsTrainOps: if name == self._shared_critic_ops_name: ops_params = dict(self._ops_params) ops_params.update({ - "get_policy_func": None, "policy_idx": -1, "shared_critic": False, - "create_actor": False, }) return DiscreteMADDPGOps(name=name, **ops_params) else: - policy_idx = self.get_policy_idx_from_ops_name(name) - policy_name = self._policy_names[policy_idx] - ops_params = dict(self._ops_params) ops_params.update({ - "get_policy_func": lambda: self._policy_creator[policy_name](policy_name), - "policy_idx": policy_idx, - "create_actor": True, + "policy_creator": self._policy_creator[name], + "policy_idx": self._policy_names.index(name), }) return DiscreteMADDPGOps(name=name, **ops_params) + def _get_batch(self, batch_size: int = None) -> MultiTransitionBatch: + return self._replay_memory.sample(batch_size if batch_size is not None else self._batch_size) + def train_step(self) -> None: assert not self._params.shared_critic or isinstance(self._critic_ops, DiscreteMADDPGOps) assert all(isinstance(ops, DiscreteMADDPGOps) for ops in self._actor_ops_list) @@ -463,10 +462,7 @@ def load(self, path: str) -> None: self._assert_ops_exists() trainer_state = torch.load(path) for ops_name, ops_state in trainer_state.items(): - if ops_name == self._critic_ops.name: - self._critic_ops.set_state(ops_state) - else: - self._actor_ops_dict[ops_name].set_state(torch.load(path)) + self._ops_dict[ops_name].set_state(ops_state) def save(self, path: str) -> None: self._assert_ops_exists() @@ -481,10 +477,5 @@ def _assert_ops_exists(self) -> None: if self._params.shared_critic and not self._critic_ops: raise ValueError("Call 'DiscreteMADDPG.build' to create the critic ops first.") - @staticmethod - def get_policy_idx_from_ops_name(ops_name: str) -> int: - _, sub_name = ops_name.split(".") - return int(sub_name.split("_")[1]) - async def exit(self) -> None: pass diff --git a/maro/rl/training/algorithms/ppo.py b/maro/rl/training/algorithms/ppo.py new file mode 100644 index 000000000..4ed4ea1d7 --- /dev/null +++ b/maro/rl/training/algorithms/ppo.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from dataclasses import dataclass +from typing import Dict + +from maro.rl.training.algorithms.base import DiscreteACBasedParams, DiscreteACBasedTrainer + + +@dataclass +class DiscretePPOParams(DiscreteACBasedParams): + """Mostly inherited from `DiscreteACBasedParams`. Please refer to the doc string of `DiscreteACBasedParams` + for more detailed information. + + clip_ratio (float, default=None): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). + If it is None, the actor loss is calculated using the usual policy gradient theorem. + """ + clip_ratio: float = None + + def extract_ops_params(self) -> Dict[str, object]: + return { + "get_v_critic_net_func": self.get_v_critic_net_func, + "reward_discount": self.reward_discount, + "critic_loss_cls": self.critic_loss_cls, + "clip_ratio": self.clip_ratio, + "lam": self.lam, + "min_logp": self.min_logp, + } + + def __post_init__(self) -> None: + assert self.get_v_critic_net_func is not None + assert self.clip_ratio is not None + + +class DiscretePPOTrainer(DiscreteACBasedTrainer): + """Discrete PPO algorithm. + + References: + https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ppo. + """ + def __init__(self, name: str, params: DiscretePPOParams) -> None: + super(DiscretePPOTrainer, self).__init__(name, params) diff --git a/maro/rl/training/train_ops.py b/maro/rl/training/train_ops.py index 18405d55c..ee0c0d19f 100644 --- a/maro/rl/training/train_ops.py +++ b/maro/rl/training/train_ops.py @@ -5,12 +5,10 @@ from abc import ABCMeta, abstractmethod from typing import Callable, Tuple -import torch import zmq from zmq.asyncio import Context, Poller from maro.rl.policy import RLPolicy -from maro.rl.utils import AbsTransitionBatch, MultiTransitionBatch, TransitionBatch from maro.rl.utils.common import bytes_to_pyobj, get_ip_address_by_hostname, pyobj_to_bytes from maro.utils import DummyLogger, LoggerV2 @@ -20,30 +18,23 @@ class AbsTrainOps(object, metaclass=ABCMeta): Each ops is used for training a single policy. An ops is an atomic unit in the distributed mode. Args: - device (str): Identifier for the torch device. The policy will be moved to the specified device. - If it is None, the device will be set to "cpu" if cuda is unavailable and "cuda" otherwise. - is_single_scenario (bool): Flag indicating whether the ops belongs to a `SingleTrainer` or a `MultiTrainer`. - get_policy_func (Callable[[], RLPolicy]): Function used to create the policy of this ops. + name (str): Name of the ops. This is usually a policy name. + policy_creator (Callable[[str], RLPolicy]): Function to create a policy instance. + parallelism (int, default=1): Desired degree of data parallelism. """ def __init__( self, name: str, - device: str, - is_single_scenario: bool, - get_policy_func: Callable[[], RLPolicy], + policy_creator: Callable[[str], RLPolicy], parallelism: int = 1, ) -> None: super(AbsTrainOps, self).__init__() self._name = name - self._device = torch.device(device) if device is not None \ - else torch.device("cuda" if torch.cuda.is_available() else "cpu") - self._is_single_scenario = is_single_scenario - - # Create the policy and put it on the right device. - if self._is_single_scenario: - self._policy = get_policy_func() - self._policy.to_device(self._device) + self._policy_creator = policy_creator + # Create the policy. + if self._policy_creator: + self._policy = self._policy_creator(self._name) self._parallelism = parallelism @@ -53,26 +44,16 @@ def name(self) -> str: @property def policy_state_dim(self) -> int: - return self._policy.state_dim + return self._policy.state_dim if self._policy_creator else None @property def policy_action_dim(self) -> int: - return self._policy.action_dim + return self._policy.action_dim if self._policy_creator else None @property def parallelism(self) -> int: return self._parallelism - def _is_valid_transition_batch(self, batch: AbsTransitionBatch) -> bool: - """Used to check the transition batch's type. If this ops is used under a single trainer, the batch should be - a `TransitionBatch`. Otherwise, it should be a `MultiTransitionBatch`. - - Args: - batch (AbsTransitionBatch): The batch to be validated. - """ - return isinstance(batch, TransitionBatch) if self._is_single_scenario \ - else isinstance(batch, MultiTransitionBatch) - @abstractmethod def get_state(self) -> dict: """Get the train ops's state. @@ -108,6 +89,10 @@ def set_policy_state(self, policy_state: object) -> None: """ self._policy.set_state(policy_state) + @abstractmethod + def to_device(self, device: str): + raise NotImplementedError + def remote(func) -> Callable: """Annotation to indicate that a function / method can be called remotely. diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index 5d5e4a0a9..7aadc51eb 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -7,7 +7,7 @@ import torch -from maro.rl.policy import RLPolicy +from maro.rl.policy import AbsPolicy, RLPolicy from maro.rl.rollout import ExpElement from maro.utils import LoggerV2 @@ -19,8 +19,6 @@ class TrainerParams: """Common trainer parameters. - device (str, default=None): Name of the device to store this trainer. If it is None, the device will be - automatically determined according to GPU availability. replay_memory_capacity (int, default=100000): Maximum capacity of the replay memory. batch_size (int, default=128): Training batch size. data_parallelism (int, default=1): Degree of data parallelism. A value greater than 1 can be used when @@ -34,7 +32,6 @@ class TrainerParams: of resources available on the internet. """ - device: str = None replay_memory_capacity: int = 10000 batch_size: int = 128 data_parallelism: int = 1 @@ -64,8 +61,9 @@ class AbsTrainer(object, metaclass=ABCMeta): def __init__(self, name: str, params: TrainerParams) -> None: self._name = name - self._batch_size = params.batch_size - self._agent2policy: Dict[str, str] = {} + self._params = params + self._batch_size = self._params.batch_size + self._agent2policy: Dict[Any, str] = {} self._proxy_address: Optional[Tuple[str, int]] = None self._logger = None @@ -89,20 +87,19 @@ def register_agent2policy(self, agent2policy: Dict[Any, str]) -> None: agent2policy (Dict[Any, str]): Agent name to policy name mapping. """ self._agent2policy = { - agent_name: policy_name - for agent_name, policy_name in agent2policy.items() + agent_name: policy_name for agent_name, policy_name in agent2policy.items() if extract_trainer_name(policy_name) == self.name } @abstractmethod def register_policy_creator( self, - global_policy_creator: Dict[str, Callable[[str], RLPolicy]], + global_policy_creator: Dict[str, Callable[[str], AbsPolicy]], ) -> None: """Register the policy creator. Only keep the creators of the policies that the current trainer need to train. Args: - global_policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict that contains the creators for all + global_policy_creator (Dict[str, Callable[[str], AbsPolicy]]): Dict that contains the creators for all policies. """ raise NotImplementedError @@ -139,32 +136,6 @@ def record(self, env_idx: int, exp_element: ExpElement) -> None: def set_proxy_address(self, proxy_address: Tuple[str, int]) -> None: self._proxy_address = proxy_address - @abstractmethod - def get_local_ops_by_name(self, name: str) -> AbsTrainOps: - """Create an `AbsTrainOps` instance with a given name. - - Args: - name (str): Ops name. - - Returns: - ops (AbsTrainOps): The local ops. - """ - raise NotImplementedError - - def get_ops(self, name: str) -> Union[RemoteOps, AbsTrainOps]: - """Create an `AbsTrainOps` instance with a given name. If a proxy address has been registered to the trainer, - this returns a `RemoteOps` instance in which all methods annotated as "remote" are turned into a remote method - call. Otherwise, a regular `AbsTrainOps` is returned. - - Args: - name (str): Ops name. - - Returns: - ops (Union[RemoteOps, AbsTrainOps]): The ops. - """ - ops = self.get_local_ops_by_name(name) - return RemoteOps(ops, self._proxy_address, logger=self._logger) if self._proxy_address else ops - @abstractmethod def get_policy_state(self) -> Dict[str, object]: """Get policies' states. @@ -193,29 +164,46 @@ class SingleAgentTrainer(AbsTrainer, metaclass=ABCMeta): def __init__(self, name: str, params: TrainerParams) -> None: super(SingleAgentTrainer, self).__init__(name, params) - - self._ops: Union[RemoteOps, None] = None # To be created in `build()` - - self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = {} self._policy_name: Optional[str] = None - self._get_policy_func: Optional[Callable] = None + self._policy_creator: Optional[Callable[[str], RLPolicy]] = None + self._ops: Optional[AbsTrainOps] = None + + @property + def ops(self): + return self._ops def register_policy_creator( self, - global_policy_creator: Dict[str, Callable[[str], RLPolicy]] + global_policy_creator: Dict[str, Callable[[str], AbsPolicy]], ) -> None: - self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = { - policy_name: func for policy_name, func in global_policy_creator.items() - if extract_trainer_name(policy_name) == self.name - } + policy_names = [ + policy_name for policy_name in global_policy_creator if extract_trainer_name(policy_name) == self.name + ] + if len(policy_names) != 1: + raise ValueError(f"Trainer {self._name} should have exactly one policy assigned to it") + + self._policy_name = policy_names.pop() + self._policy_creator = global_policy_creator[self._policy_name] + + @abstractmethod + def get_local_ops(self) -> AbsTrainOps: + """Create an `AbsTrainOps` instance associated with the policy. - if len(self._policy_creator) == 0: - raise ValueError(f"Trainer {self._name} has no policies") - if len(self._policy_creator) > 1: - raise ValueError(f"Trainer {self._name} cannot have more than one policy assigned to it") + Returns: + ops (AbsTrainOps): The local ops. + """ + raise NotImplementedError + + def get_ops(self) -> Union[RemoteOps, AbsTrainOps]: + """Create an `AbsTrainOps` instance associated with the policy. If a proxy address has been registered to the + trainer, this returns a `RemoteOps` instance in which all methods annotated as "remote" are turned into a remote + method call. Otherwise, a regular `AbsTrainOps` is returned. - self._policy_name = list(self._policy_creator.keys())[0] - self._get_policy_func = lambda: self._policy_creator[self._policy_name](self._policy_name) + Returns: + ops (Union[RemoteOps, AbsTrainOps]): The ops. + """ + ops = self.get_local_ops() + return RemoteOps(ops, self._proxy_address, logger=self._logger) if self._proxy_address else ops def get_policy_state(self) -> Dict[str, object]: self._assert_ops_exists() @@ -235,6 +223,7 @@ def _assert_ops_exists(self) -> None: raise ValueError("'build' needs to be called to create an ops instance first.") async def exit(self) -> None: + self._assert_ops_exists() if isinstance(self._ops, RemoteOps): await self._ops.exit() @@ -247,16 +236,47 @@ def __init__(self, name: str, params: TrainerParams) -> None: super(MultiAgentTrainer, self).__init__(name, params) self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = {} self._policy_names: List[str] = [] + self._ops_dict: Dict[str, AbsTrainOps] = {} + + @property + def ops_dict(self): + return self._ops_dict def register_policy_creator( self, - global_policy_creator: Dict[str, Callable[[str], RLPolicy]], + global_policy_creator: Dict[str, Callable[[str], AbsPolicy]], ) -> None: self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = { policy_name: func for policy_name, func in global_policy_creator.items() if extract_trainer_name(policy_name) == self.name } - self._policy_names = sorted(list(self._policy_creator.keys())) + self._policy_names = list(self._policy_creator.keys()) + + @abstractmethod + def get_local_ops(self, name: str) -> AbsTrainOps: + """Create an `AbsTrainOps` instance with a given name. + + Args: + name (str): Ops name. + + Returns: + ops (AbsTrainOps): The local ops. + """ + raise NotImplementedError + + def get_ops(self, name: str) -> Union[RemoteOps, AbsTrainOps]: + """Create an `AbsTrainOps` instance with a given name. If a proxy address has been registered to the trainer, + this returns a `RemoteOps` instance in which all methods annotated as "remote" are turned into a remote method + call. Otherwise, a regular `AbsTrainOps` is returned. + + Args: + name (str): Ops name. + + Returns: + ops (Union[RemoteOps, AbsTrainOps]): The ops. + """ + ops = self.get_local_ops(name) + return RemoteOps(ops, self._proxy_address, logger=self._logger) if self._proxy_address else ops @abstractmethod def get_policy_state(self) -> Dict[str, object]: diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/training_manager.py index b9ebbef4a..dc95d7f5c 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/training_manager.py @@ -4,13 +4,15 @@ import asyncio import os from itertools import chain -from typing import Callable, Dict, Iterable, List, Tuple +from typing import Any, Callable, Dict, Iterable, List, Tuple -from maro.rl.policy import RLPolicy +from maro.rl.policy import AbsPolicy from maro.rl.rollout import ExpElement +from maro.rl.training import SingleAgentTrainer from maro.utils import LoggerV2 +from maro.utils.exception.rl_toolkit_exception import MissingTrainer -from .trainer import AbsTrainer +from .trainer import AbsTrainer, MultiAgentTrainer from .utils import extract_trainer_name, get_trainer_state_path @@ -19,18 +21,22 @@ class TrainingManager(object): Training manager. Manage and schedule all trainers to train policies. Args: - policy_creator (Dict[str, Callable[[str], RLPolicy]]): Dict of functions to create policies. + policy_creator (Dict[str, Callable[[str], AbsPolicy]]): Dict of functions to create policies. trainer_creator (Dict[str, Callable[[str], AbsTrainer]]): Dict of functions to create trainers. - agent2policy (Dict[str, str]): Agent name to policy name mapping. + agent2policy (Dict[Any, str]): Agent name to policy name mapping. + device_mapping (Dict[str, str], default={}): User-defined device mapping from policy name to pytorch + device name. proxy_address (Tuple[str, int], default=None): Address of the training proxy. If it is not None, it is registered to all trainers, which in turn create `RemoteOps` for distributed training. + logger (LoggerV2, default=None): A logger for logging key events. """ def __init__( self, - policy_creator: Dict[str, Callable[[str], RLPolicy]], + policy_creator: Dict[str, Callable[[str], AbsPolicy]], trainer_creator: Dict[str, Callable[[str], AbsTrainer]], - agent2policy: Dict[str, str], # {agent_name: policy_name} + agent2policy: Dict[Any, str], # {agent_name: policy_name} + device_mapping: Dict[str, str] = None, proxy_address: Tuple[str, int] = None, logger: LoggerV2 = None, ) -> None: @@ -49,15 +55,31 @@ def __init__( trainer.build() # `build()` must be called after `register_policy_creator()` self._trainer_dict[trainer_name] = trainer - self._agent2trainer = { - agent_name: extract_trainer_name(policy_name) - for agent_name, policy_name in self._agent2policy.items() - } + # User-defined allocation of compute devices, i.e., GPU's to the trainer ops + if device_mapping is not None: + for policy_name, device_name in device_mapping.items(): + trainer = self._trainer_dict[extract_trainer_name(policy_name)] + + if isinstance(trainer, SingleAgentTrainer): + ops = trainer.ops + else: + assert isinstance(trainer, MultiAgentTrainer) + ops = trainer.ops_dict[policy_name] + ops.to_device(device_name) + + self._agent2trainer: Dict[Any, str] = {} + for agent_name, policy_name in self._agent2policy.items(): + trainer_name = extract_trainer_name(policy_name) + if trainer_name not in self._trainer_dict: + raise MissingTrainer(f"trainer {trainer_name} does not exist") + self._agent2trainer[agent_name] = trainer_name def train_step(self) -> None: if self._proxy_address: async def train_step() -> Iterable: - return await asyncio.gather(*[trainer.train_step_as_task() for trainer in self._trainer_dict.values()]) + return await asyncio.gather( + *[trainer_.train_step_as_task() for trainer_ in self._trainer_dict.values()] + ) asyncio.run(train_step()) else: diff --git a/maro/rl/training/worker.py b/maro/rl/training/worker.py index b566bd2b3..6536f0ad5 100644 --- a/maro/rl/training/worker.py +++ b/maro/rl/training/worker.py @@ -4,12 +4,13 @@ from typing import Callable, Dict from maro.rl.distributed import AbsWorker -from maro.rl.policy import RLPolicy +from maro.rl.policy import AbsPolicy +from maro.rl.training import SingleAgentTrainer from maro.rl.utils.common import bytes_to_pyobj, bytes_to_string, pyobj_to_bytes from maro.utils import LoggerV2 from .train_ops import AbsTrainOps -from .trainer import AbsTrainer +from .trainer import AbsTrainer, MultiAgentTrainer class TrainOpsWorker(AbsWorker): @@ -18,8 +19,8 @@ class TrainOpsWorker(AbsWorker): Args: idx (int): Integer identifier for the worker. It is used to generate an internal ID, "worker.{idx}", so that the proxy can keep track of its connection status. - policy_creator (Dict[str, Callable[[str], RLPolicy]]): User-defined function registry that can be used to create - an "RLPolicy" instance with a name in the registry. This is required to create train ops instances. + policy_creator (Dict[str, Callable[[str], AbsPolicy]]): User-defined function registry that can be used to + create an "AbsPolicy" instance with a name in the registry. This is required to create train ops instances. trainer_creator (Dict[str, Callable[[str], AbsTrainer]]): User-defined function registry that can be used to create an "AbsTrainer" instance with a name in the registry. This is required to create train ops instances. producer_host (str): IP address of the proxy host to connect to. @@ -29,7 +30,7 @@ class TrainOpsWorker(AbsWorker): def __init__( self, idx: int, - policy_creator: Dict[str, Callable[[str], RLPolicy]], + policy_creator: Dict[str, Callable[[str], AbsPolicy]], trainer_creator: Dict[str, Callable[[str], AbsTrainer]], producer_host: str, producer_port: int = 10001, @@ -65,7 +66,12 @@ def _compute(self, msg: list) -> None: trainer.register_policy_creator(self._policy_creator) self._trainer_dict[trainer_name] = trainer - self._ops_dict[ops_name] = self._trainer_dict[trainer_name].get_local_ops_by_name(ops_name) + trainer = self._trainer_dict[trainer_name] + if isinstance(trainer, SingleAgentTrainer): + self._ops_dict[ops_name] = trainer.get_local_ops() + else: + assert isinstance(trainer, MultiAgentTrainer) + self._ops_dict[ops_name] = trainer.get_local_ops(ops_name) self._logger.info(f"Created ops {ops_name} at {self._id}") self._ops_dict[ops_name].set_state(req["state"]) diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 41a3f9a28..1034ac872 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -4,7 +4,7 @@ from typing import Union from .objects import SHAPE_CHECK_FLAG -from .torch_utils import average_grads, match_shape, ndarray_to_tensor +from .torch_utils import average_grads, get_torch_device, match_shape, ndarray_to_tensor from .trajectory_computation import discount_cumsum from .transition_batch import MultiTransitionBatch, TransitionBatch, merge_transition_batches @@ -12,7 +12,7 @@ __all__ = [ "SHAPE_CHECK_FLAG", - "average_grads", "match_shape", "ndarray_to_tensor", + "average_grads", "get_torch_device", "match_shape", "ndarray_to_tensor", "discount_cumsum", "AbsTransitionBatch", "MultiTransitionBatch", "TransitionBatch", "merge_transition_batches", ] diff --git a/maro/rl/utils/common.py b/maro/rl/utils/common.py index 1a0d26d75..2c0bda9c2 100644 --- a/maro/rl/utils/common.py +++ b/maro/rl/utils/common.py @@ -61,7 +61,18 @@ def bytes_to_pyobj(bytes_: bytes) -> object: def get_own_ip_address() -> str: - return socket.gethostbyname(socket.gethostname()) + """https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib""" + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(0) + try: + # doesn't even have to be reachable + sock.connect(("10.255.255.255", 1)) + ip = sock.getsockname()[0] + except Exception: + ip = "127.0.0.1" + finally: + sock.close() + return ip def get_ip_address_by_hostname(host: str) -> str: diff --git a/maro/rl/utils/torch_utils.py b/maro/rl/utils/torch_utils.py index b536916e0..914efc6e6 100644 --- a/maro/rl/utils/torch_utils.py +++ b/maro/rl/utils/torch_utils.py @@ -31,7 +31,7 @@ def match_shape(tensor: Union[torch.Tensor, np.ndarray], shape: tuple) -> bool: return True -def ndarray_to_tensor(array: np.ndarray, device: torch.device) -> torch.Tensor: +def ndarray_to_tensor(array: np.ndarray, device: torch.device = None) -> torch.Tensor: """ Convert a np.ndarray to a torch.Tensor. @@ -54,3 +54,7 @@ def average_grads(grad_list: List[dict]) -> dict: param_name: torch.mean(torch.stack([grad[param_name] for grad in grad_list]), dim=0) for param_name in grad_list[0] } + + +def get_torch_device(device: str = None): + return torch.device(device if device else ("cuda" if torch.cuda.is_available() else "cpu")) diff --git a/maro/rl/utils/transition_batch.py b/maro/rl/utils/transition_batch.py index 30c632d04..023668e42 100644 --- a/maro/rl/utils/transition_batch.py +++ b/maro/rl/utils/transition_batch.py @@ -18,6 +18,7 @@ class TransitionBatch: terminals: np.ndarray # 1D returns: np.ndarray = None # 1D advantages: np.ndarray = None # 1D + old_logps: np.ndarray = None # 1D @property def size(self) -> int: @@ -43,6 +44,7 @@ def make_kth_sub_batch(self, i: int, k: int) -> TransitionBatch: terminals=self.terminals[i::k], returns=self.returns[i::k] if self.returns is not None else None, advantages=self.advantages[i::k] if self.advantages is not None else None, + old_logps=self.old_logps[i::k] if self.old_logps is not None else None, ) def split(self, k: int) -> List[TransitionBatch]: @@ -116,4 +118,7 @@ def merge_transition_batches(batch_list: List[TransitionBatch]) -> TransitionBat terminals=np.concatenate([batch.terminals for batch in batch_list]), returns=np.concatenate([batch.returns for batch in batch_list]), advantages=np.concatenate([batch.advantages for batch in batch_list]), + old_logps=None if batch_list[0].old_logps is None else np.concatenate( + [batch.old_logps for batch in batch_list] + ), ) diff --git a/maro/rl/workflows/config/parser.py b/maro/rl/workflows/config/parser.py index fbcc1adcd..8a06bfceb 100644 --- a/maro/rl/workflows/config/parser.py +++ b/maro/rl/workflows/config/parser.py @@ -227,7 +227,7 @@ def get_path_mapping(self, containerize: bool = False) -> dict: log_dir = os.path.dirname(self._config["log_path"]) path_map = { self._config["scenario_path"]: "/scenario" if containerize else self._config["scenario_path"], - log_dir: f"/logs" if containerize else log_dir + log_dir: "/logs" if containerize else log_dir } load_path = self._config["training"].get("load_path", None) diff --git a/maro/rl/workflows/main.py b/maro/rl/workflows/main.py index e3c96fe6e..fe64634f3 100644 --- a/maro/rl/workflows/main.py +++ b/maro/rl/workflows/main.py @@ -5,8 +5,11 @@ import time from typing import List +import torch + from maro.rl.rollout import BatchEnvSampler, ExpElement from maro.rl.training import TrainingManager +from maro.rl.utils import get_torch_device from maro.rl.utils.common import float_or_none, get_env, int_or_none, list_or_none from maro.rl.workflows.scenario import Scenario from maro.utils import LoggerV2 @@ -24,10 +27,6 @@ def main(scenario: Scenario) -> None: file_level=get_env("LOG_LEVEL_FILE", required=False, default="CRITICAL"), ) - load_path = get_env("LOAD_PATH", required=False) - checkpoint_path = get_env("CHECKPOINT_PATH", required=False) - checkpoint_interval = int_or_none(get_env("CHECKPOINT_INTERVAL", required=False)) - env_sampling_parallelism = int_or_none(get_env("ENV_SAMPLE_PARALLELISM", required=False)) env_eval_parallelism = int_or_none(get_env("ENV_EVAL_PARALLELISM", required=False)) parallel_rollout = env_sampling_parallelism is not None or env_eval_parallelism is not None @@ -39,6 +38,8 @@ def main(scenario: Scenario) -> None: is_single_thread = train_mode == "simple" and not parallel_rollout if is_single_thread: # If running in single thread mode, create policy instances here and reuse then in rollout and training. + # In other words, `policy_creator` will return a policy instance that has been already created in advance + # instead of create a new policy instance. policy_dict = {name: get_policy_func(name) for name, get_policy_func in policy_creator.items()} policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict} @@ -52,25 +53,42 @@ def main(scenario: Scenario) -> None: logger=logger, ) else: - env_sampler = scenario.get_env_sampler(policy_creator) + env_sampler = scenario.env_sampler_creator(policy_creator) + if train_mode != "simple": + for policy_name, device_name in scenario.device_mapping.items(): + env_sampler.rl_policy_dict[policy_name].to_device(get_torch_device(device_name)) # evaluation schedule eval_schedule = list_or_none(get_env("EVAL_SCHEDULE", required=False)) logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") eval_point_index = 0 + if scenario.trainable_policies is None: + trainable_policies = set(policy_creator.keys()) + else: + trainable_policies = set(scenario.trainable_policies) + + trainable_policy_creator = {name: func for name, func in policy_creator.items() if name in trainable_policies} + trainable_agent2policy = {id_: name for id_, name in agent2policy.items() if name in trainable_policies} training_manager = TrainingManager( - policy_creator, trainer_creator, agent2policy, + policy_creator=trainable_policy_creator, + trainer_creator=trainer_creator, + agent2policy=trainable_agent2policy, + device_mapping=scenario.device_mapping if train_mode == "simple" else {}, proxy_address=None if train_mode == "simple" else ( get_env("TRAIN_PROXY_HOST"), int(get_env("TRAIN_PROXY_FRONTEND_PORT")) ), logger=logger, ) + + load_path = get_env("LOAD_PATH", required=False) if load_path: assert isinstance(load_path, str) loaded = training_manager.load(load_path) logger.info(f"Loaded states for {loaded} from {load_path}") + checkpoint_path = get_env("CHECKPOINT_PATH", required=False) + checkpoint_interval = int_or_none(get_env("CHECKPOINT_INTERVAL", required=False)) # main loop for ep in range(1, num_episodes + 1): collect_time = training_time = 0 @@ -115,3 +133,9 @@ def main(scenario: Scenario) -> None: if isinstance(env_sampler, BatchEnvSampler): env_sampler.exit() training_manager.exit() + + +if __name__ == "__main__": + # get user-defined scenario ingredients + scenario = Scenario(get_env("SCENARIO_PATH")) + main(scenario) diff --git a/maro/rl/workflows/rollout_worker.py b/maro/rl/workflows/rollout_worker.py index 99b7e29cc..354141d4e 100644 --- a/maro/rl/workflows/rollout_worker.py +++ b/maro/rl/workflows/rollout_worker.py @@ -1,14 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from functools import partial + from maro.rl.rollout import RolloutWorker from maro.rl.utils.common import get_env, int_or_none from maro.rl.workflows.scenario import Scenario from maro.utils import LoggerV2 if __name__ == "__main__": - scenario_attr = Scenario(get_env("SCENARIO_PATH")) - policy_creator = scenario_attr.policy_creator + scenario = Scenario(get_env("SCENARIO_PATH")) + policy_creator = scenario.policy_creator worker_idx = int_or_none(get_env("ID")) logger = LoggerV2( @@ -20,7 +22,7 @@ ) worker = RolloutWorker( idx=worker_idx, - env_sampler_creator=lambda: scenario_attr.get_env_sampler(policy_creator), + env_sampler_creator=partial(scenario.env_sampler_creator, policy_creator), producer_host=get_env("ROLLOUT_CONTROLLER_HOST"), producer_port=int_or_none(get_env("ROLLOUT_CONTROLLER_PORT")), logger=logger, diff --git a/maro/rl/workflows/scenario.py b/maro/rl/workflows/scenario.py index 6819f34dd..4c21ffe0f 100644 --- a/maro/rl/workflows/scenario.py +++ b/maro/rl/workflows/scenario.py @@ -4,9 +4,10 @@ import importlib import os import sys -from typing import Callable, Dict +from typing import Any, Callable, Dict, List -from maro.rl.policy import RLPolicy +from maro.rl.policy import AbsPolicy +from maro.rl.policy.abs_policy import RLPolicy from maro.rl.rollout import AbsEnvSampler from maro.rl.training import AbsTrainer @@ -18,21 +19,30 @@ def __init__(self, path: str) -> None: sys.path.insert(0, os.path.dirname(path)) self._module = importlib.import_module(os.path.basename(path)) - def get_env_sampler(self, policy_creator: Dict[str, Callable[[str], RLPolicy]]) -> AbsEnvSampler: - return getattr(self._module, "env_sampler_creator")(policy_creator) + @property + def env_sampler_creator(self) -> Callable[[Dict[str, Callable[[str], AbsPolicy]]], AbsEnvSampler]: + return getattr(self._module, "env_sampler_creator") @property - def agent2policy(self) -> Dict[str, str]: + def agent2policy(self) -> Dict[Any, str]: return getattr(self._module, "agent2policy") @property - def policy_creator(self) -> Dict[str, Callable[[str], RLPolicy]]: + def policy_creator(self) -> Dict[str, Callable[[str], AbsPolicy]]: return getattr(self._module, "policy_creator") + @property + def trainable_policies(self) -> List[str]: + return getattr(self._module, "trainable_policies", None) + @property def trainer_creator(self) -> Dict[str, Callable[[str], AbsTrainer]]: return getattr(self._module, "trainer_creator") + @property + def device_mapping(self) -> Dict[str, str]: + return getattr(self._module, "device_mapping", {}) + @property def post_collect(self) -> Callable[[list, int, int], None]: return getattr(self._module, "post_collect", None) diff --git a/maro/simulator/scenarios/__init__.py b/maro/simulator/scenarios/__init__.py index 4eff718f3..42ba8498a 100644 --- a/maro/simulator/scenarios/__init__.py +++ b/maro/simulator/scenarios/__init__.py @@ -1,5 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - from .abs_business_engine import AbsBusinessEngine diff --git a/maro/simulator/scenarios/abs_business_engine.py b/maro/simulator/scenarios/abs_business_engine.py index ed0a1224e..0ba670c37 100644 --- a/maro/simulator/scenarios/abs_business_engine.py +++ b/maro/simulator/scenarios/abs_business_engine.py @@ -70,7 +70,6 @@ def snapshots(self) -> SnapshotList: def scenario_name(self) -> str: return self._scenario_name - @abstractmethod def get_agent_idx_list(self) -> List[int]: """Get a list of agent index.""" pass @@ -154,9 +153,9 @@ def reset(self, keep_seed: bool = False) -> None: """Reset states business engine.""" pass - @abstractmethod - def set_seed(self, seed: int) -> None: - raise NotImplementedError + # @abstractmethod + # def set_seed(self, seed: int) -> None: + # raise NotImplementedError def post_step(self, tick: int) -> bool: """This method will be called at the end of each tick, used to post-process for each tick, diff --git a/maro/utils/exception/error_code.py b/maro/utils/exception/error_code.py index d11cabbae..140a0a1da 100644 --- a/maro/utils/exception/error_code.py +++ b/maro/utils/exception/error_code.py @@ -47,6 +47,5 @@ 3003: "Deployment Error", # 4000-4999: Error codes for RL toolkit - 4000: "InvalidExperience", - 4001: "Missing Optimizer", + 4000: "Missing Trainer", } diff --git a/maro/utils/exception/rl_toolkit_exception.py b/maro/utils/exception/rl_toolkit_exception.py index bf82231ee..a6d6054dd 100644 --- a/maro/utils/exception/rl_toolkit_exception.py +++ b/maro/utils/exception/rl_toolkit_exception.py @@ -4,16 +4,9 @@ from .base_exception import MAROException -class InvalidExperience(MAROException): +class MissingTrainer(MAROException): """ - Raised when the states, actions, rewards and next states passed to an ``ExperienceSet`` do not - have the same length. + Raised when the trainer specified in the prefix of a policy name is missing. """ def __init__(self, msg: str = None): super().__init__(4000, msg) - - -class MissingOptimizer(MAROException): - """Raised when the optimizers are missing when calling CoreModel's step() method.""" - def __init__(self, msg: str = None): - super().__init__(4001, msg) diff --git a/maro/utils/logger.py b/maro/utils/logger.py index 32b3c37e9..34fd30fc0 100644 --- a/maro/utils/logger.py +++ b/maro/utils/logger.py @@ -87,72 +87,47 @@ class Logger(object): """A simple wrapper for logging. The Logger hosts a file handler and a stdout handler. The file handler is set - to ``DEBUG`` level and will dump all the logging info to the given ``dump_folder``. - The logging level of the stdout handler is decided by the ``stdout_level``, - and can be redirected by setting the environment variable ``LOG_LEVEL``. - Supported ``LOG_LEVEL`` includes: ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, - ``CRITICAL``, ``PROCESS``. - - Example: - ``$ export LOG_LEVEL=INFO`` + to ``DEBUG`` level and will dump all logs info to the given ``dump_path``. + Supported log levels include: ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, ``CRITICAL``, ``PROCESS``. Args: tag (str): Log tag for stream and file output. format_ (LogFormat): Predefined formatter. Defaults to ``LogFormat.full``. - dump_folder (str): Log dumped folder. Defaults to the current folder. - The dumped log level is ``logging.DEBUG``. The full path of the - dumped log file is `dump_folder/tag.log`. - dump_mode (str): Write log file mode. Defaults to ``w``. Use ``a`` to - append log. - extension_name (str): Final dumped file extension name. Defaults to `log`. - auto_timestamp (bool): Add a timestamp to the dumped log file name or not. - E.g: `tag.1574953673.137387.log`. - stdout_level (str): the logging level of the stdout handler. Defaults to - ``DEBUG``. + dump_path (str): Path of file for dumping logs. Must be an absolute path. The log level for dumping is + ``logging.DEBUG``. Defaults to None, in which case logs generated by the logger will not be dumped + to a file. + dump_mode (str): Write log file mode. Defaults to ``w``. Use ``a`` to append log. + stdout_level (str): the logging level of the stdout handler. Defaults to ``INFO``. + file_level (str): the logging level of the file handler. Defaults to ``DEBUG``. """ def __init__( - self, tag: str, format_: LogFormat = LogFormat.simple, dump_folder: str = cwd, dump_mode: str = 'w', - extension_name: str = 'log', auto_timestamp: bool = False, stdout_level="INFO" + self, tag: str, format_: LogFormat = LogFormat.simple, dump_path: str = None, dump_mode: str = 'w', + stdout_level="INFO", file_level="DEBUG" ): self._file_format = FORMAT_NAME_TO_FILE_FORMAT[format_] self._stdout_format = FORMAT_NAME_TO_STDOUT_FORMAT[format_] \ if format_ in FORMAT_NAME_TO_STDOUT_FORMAT else \ FORMAT_NAME_TO_FILE_FORMAT[format_] - self._stdout_level = os.environ.get('LOG_LEVEL') or stdout_level + self._stdout_level = LEVEL_MAP[stdout_level] if isinstance(stdout_level, str) else stdout_level + self._file_level = LEVEL_MAP[file_level] if isinstance(file_level, str) else file_level self._logger = logging.getLogger(tag) self._logger.setLevel(logging.DEBUG) - self._extension_name = extension_name - - if not os.path.exists(dump_folder): - try: - os.makedirs(dump_folder) - except FileExistsError: - logging.warning("Receive File Exist Error about creating dump folder for internal log. " - "It may be caused by multi-thread and it won't have any impact on logger dumps.") - except Exception as e: - raise e - - if auto_timestamp: - filename = f'{tag}.{datetime.now().timestamp()}' - else: - filename = f'{tag}' - - filename += f'.{self._extension_name}' - # File handler - fh = logging.FileHandler(filename=f'{os.path.join(dump_folder, filename)}', mode=dump_mode, encoding="utf-8") - fh.setLevel(logging.DEBUG) - if self._file_format is not None: - fh.setFormatter(self._file_format) + if dump_path: + os.makedirs(os.path.dirname(dump_path), exist_ok=True) + # File handler + fh = logging.FileHandler(filename=dump_path, mode=dump_mode, encoding="utf-8") + fh.setLevel(self._file_level) + if self._file_format is not None: + fh.setFormatter(self._file_format) + self._logger.addHandler(fh) # Stdout handler sh = logging.StreamHandler(sys.stdout) sh.setLevel(self._stdout_level) if self._stdout_format is not None: sh.setFormatter(self._stdout_format) - - self._logger.addHandler(fh) self._logger.addHandler(sh) self._extra = {'host': socket.gethostname(), 'user': getpass.getuser(), 'tag': tag} @@ -205,22 +180,6 @@ def critical(self, msg, *args): pass -class InternalLogger(Logger): - """An internal logger uses for recording the internal system's log.""" - - def __init__( - self, component_name: str, tag: str = "maro_internal", format_: LogFormat = LogFormat.internal, - dump_folder: str = None, dump_mode: str = 'a', extension_name: str = 'log', - auto_timestamp: bool = False - ): - current_time = f"{datetime.now().strftime('%Y%m%d%H%M')}" - self._dump_folder = dump_folder if dump_folder else \ - os.path.join(os.path.expanduser("~"), ".maro/log", current_time, str(os.getpid())) - super().__init__(tag, format_, self._dump_folder, dump_mode, extension_name, auto_timestamp) - - self._extra = {'component': component_name} - - class CliLogger: """An internal logger for CLI logging. @@ -233,18 +192,16 @@ def __init__(self): """Init singleton logger based on the ``--debug`` argument.""" self.log_level = CliGlobalParams.LOG_LEVEL current_time = f"{datetime.now().strftime('%Y%m%d')}" - self._dump_folder = os.path.join(os.path.expanduser("~/.maro/log/cli"), current_time) + dump_path = os.path.join(os.path.expanduser("~/.maro/log/cli"), current_time) if self.log_level == logging.DEBUG: super().__init__( tag='cli', - format_=LogFormat.cli_debug, dump_folder=self._dump_folder, - dump_mode='a', extension_name='log', auto_timestamp=False, stdout_level=self.log_level + format_=LogFormat.cli_debug, dump_path=dump_path, dump_mode='a', stdout_level=self.log_level ) elif self.log_level >= logging.INFO: super().__init__( tag='cli', - format_=LogFormat.cli_info, dump_folder=self._dump_folder, - dump_mode='a', extension_name='log', auto_timestamp=False, stdout_level=self.log_level + format_=LogFormat.cli_info, dump_path=dump_path, dump_mode='a', stdout_level=self.log_level ) _logger = None diff --git a/requirements.dev.txt b/requirements.dev.txt index a21ff0947..23279bd5b 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,7 +1,14 @@ Cython==0.29.14 astroid==2.3.3 +azure-mgmt-authorization +azure-mgmt-containerservice +azure-mgmt-resource +azure-mgmt-storage +azure-storage-file-share +azure-identity certifi==2019.9.11 cycler==0.10.0 +docker flask==1.1.2 flask-cors==3.0.10 guppy3==3.0.9 @@ -12,7 +19,7 @@ mccabe==0.6.1 pyaml==20.4.0 pyparsing==2.4.5 python-dateutil==2.8.1 -PyYAML==5.4 +PyYAML==5.4.1 pyzmq==19.0.2 six==1.13.0 torch==1.6.0 @@ -22,6 +29,7 @@ zmq==0.0.0 numpy<1.20.0 scipy==1.7.0 tabulate==0.8.5 +markupsafe==2.0.1 networkx==2.4 palettable==3.3.0 urllib3==1.26.5 From 0e11ae9d7a1f0765e6b75821ef1f161997dc2d72 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Fri, 22 Apr 2022 16:47:07 +0800 Subject: [PATCH 470/482] Cherry pick RL changes from `sc_refinement` (latest commit: `2a4869`) (#509) * Cherry pick RL changes from sc_refinement (2a4869) * Limit time display precision --- .gitignore | 1 + examples/cim/rl/env_sampler.py | 12 +- examples/rl/README.md | 9 +- examples/rl/cim.yml | 34 +++++ examples/rl/main.py | 79 ------------ examples/rl/main_evaluation_only.py | 21 ---- examples/rl/run_rl_example.py | 15 +++ examples/rl/vm_scheduling.yml | 34 +++++ examples/vm_scheduling/rl/env_sampler.py | 12 +- maro/cli/local/commands.py | 13 +- maro/cli/local/utils.py | 24 ++-- maro/cli/maro.py | 1 + maro/rl/model/policy_net.py | 4 +- maro/rl/policy/abs_policy.py | 10 +- maro/rl/rollout/batch_env_sampler.py | 23 +++- maro/rl/rollout/env_sampler.py | 117 +++++++++++++----- maro/rl/rollout/worker.py | 7 +- .../training/algorithms/base/ac_ppo_base.py | 8 +- maro/rl/training/algorithms/ddpg.py | 12 +- maro/rl/training/algorithms/dqn.py | 8 +- maro/rl/training/algorithms/maddpg.py | 27 ++-- maro/rl/training/train_ops.py | 31 ++++- maro/rl/training/trainer.py | 20 ++- maro/rl/training/training_manager.py | 11 +- maro/rl/training/utils.py | 7 +- maro/rl/utils/common.py | 3 + maro/rl/utils/objects.py | 1 + maro/rl/workflows/__init__.py | 2 + maro/rl/workflows/config/parser.py | 23 ++-- maro/rl/workflows/config/template.yml | 4 +- maro/rl/workflows/main.py | 90 ++++++++++++-- maro/rl/workflows/scenario.py | 1 - 32 files changed, 428 insertions(+), 236 deletions(-) create mode 100644 examples/rl/cim.yml delete mode 100644 examples/rl/main.py delete mode 100644 examples/rl/main_evaluation_only.py create mode 100644 examples/rl/run_rl_example.py create mode 100644 examples/rl/vm_scheduling.yml create mode 100644 maro/rl/workflows/__init__.py diff --git a/.gitignore b/.gitignore index 9c4b0721e..45f04b666 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ .vs/ build/ log/ +checkpoint/ dist/ *.egg-info/ tools/schedule diff --git a/examples/cim/rl/env_sampler.py b/examples/cim/rl/env_sampler.py index 4c542c8ef..d493cc963 100644 --- a/examples/cim/rl/env_sampler.py +++ b/examples/cim/rl/env_sampler.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import numpy as np @@ -17,9 +17,9 @@ class CIMEnvSampler(AbsEnvSampler): - def _get_global_and_agent_state( - self, event: DecisionEvent, tick: int = None - ) -> Tuple[Optional[np.ndarray], Dict[Any, np.ndarray]]: + def _get_global_and_agent_state_impl( + self, event: DecisionEvent, tick: int = None, + ) -> Tuple[Union[None, np.ndarray, List[object]], Dict[Any, Union[np.ndarray, List[object]]]]: tick = self._env.tick vessel_snapshots, port_snapshots = self._env.snapshot_list["vessels"], self._env.snapshot_list["ports"] port_idx, vessel_idx = event.port_idx, event.vessel_idx @@ -31,7 +31,9 @@ def _get_global_and_agent_state( ]) return state, {port_idx: state} - def _translate_to_env_action(self, action_dict: Dict[Any, np.ndarray], event: DecisionEvent) -> Dict[Any, object]: + def _translate_to_env_action( + self, action_dict: Dict[Any, Union[np.ndarray, List[object]]], event: DecisionEvent, + ) -> Dict[Any, object]: action_space = action_shaping_conf["action_space"] finite_vsl_space = action_shaping_conf["finite_vessel_space"] has_early_discharge = action_shaping_conf["has_early_discharge"] diff --git a/examples/rl/README.md b/examples/rl/README.md index 42a565069..cc3b63be8 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -4,10 +4,11 @@ This folder contains scenarios that employ reinforcement learning. MARO's RL too ## How to Run -The ``main.py`` script can be used to run the scenarios under ``examples/SCENARIO_NAME/rl`` or any user-defined scenario that provides the necessary components (see the section below for details) . To choose a scenario, edit ``SCENARIO_PATH`` in the script to point to the desired scenario folder. You may also edit the rest of the config variables to your own preference. Note that this script runs in single-thread mode only. -To run a scenario in multi-process mode on a local machine, you will need to use the CLI tool (which requires MARO [installation from the source](https://github.com/microsoft/maro#install-maro-from-pypi)). Start by creating a configuration file (.yml) that follows the template ``maro/maro/rl/workflows/config/template.yml`` to specify the scenario-independent settings. Then use the command ``maro local run [-c] path/to/your/config`` to run in containerized (with ``-c``) or non-containerized (without ``-c``) environments. +The entrance of a RL workflow is a YAML config file. For readers' convenience, we call this config file `config.yml` in the rest part of this doc. `config.yml` specifies the path of all necessary resources, definitions, and configurations to run the job. MARO provides a comprehensive template of the config file with detailed explanations (`maro/maro/rl/workflows/config/template.yml`). Meanwhile, MARO also provides several simple examples of `config.yml` under the current folder. -The ``main_evaluation_only.py`` works similarly to ``main.py``. The difference is that it only execute the evaluation workflow without trying to train policies, which means it can be directly used for Non-RL policies. To run ``main_evaluation_only.py``, you only need to implemenent ``agent2policy``, ``policy_creator``, and ``env_sampler_creator`` in the scenario folder (see details below). +There are two ways to start the RL job: +- If you only need to have a quick look and try to start an out-of-box workflow, just run `python .\examples\rl\run_rl_example.py PATH_TO_CONFIG_YAML`. For example, `python .\examples\rl\run_rl_example.py .\examples\rl\cim.yml` will run the complete example RL training workflow of CIM scenario. If you only want to run the evaluation workflow, you could start the job with `--evaluate_only`. +- (**Require install MARO from source**) You could also start the job through MARO CLI. Use the command `maro local run [-c] path/to/your/config` to run in containerized (with `-c`) or non-containerized (without `-c`) environments. Similar, you could add `--evaluate_only` if you only need to run the evaluation workflow. ## Create Your Own Scenarios @@ -17,4 +18,4 @@ You can create your own scenarios by supplying the necessary ingredients without * Definitions of state, action and reward shaping logic pertinent to your simulator and policies. These definitions should be encapsulated in ``env_sampler_creator``, which is a function that takes ``policy_creator`` and returns an environment sampler; It is possible to have customized routines invoked at the end of a roll-out episode or episode segment. These routines usually involve processing and / or rendering information collected during roll-out. To do this, first implement the ``post_step`` method in your environment sampler class to record whatever information you wish to keep track of during roll-out. Then create functions named ``post_collect`` and ``post_evaluate`` to process the information and expose them in the scenario folder's ``__init__.py``. These functions are used as callbacks in the main learning loop and executed at the end of each training or evaluation episode. See ``cim/callbacks.py`` for a simple example of how to create these functions. -* An optional dictionary named ``device_mapping`` that specifies the compute device (CPU or GPU) for each policy. If not provided, all computations will be performed on the CPU. \ No newline at end of file +* An optional dictionary named ``device_mapping`` that specifies the compute device (CPU or GPU) for each policy. If not provided, all computations will be performed on the CPU. diff --git a/examples/rl/cim.yml b/examples/rl/cim.yml new file mode 100644 index 000000000..bb99d164a --- /dev/null +++ b/examples/rl/cim.yml @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +# Example RL config file for CIM scenario. +# Please refer to `maro/rl/workflows/config/template.yml` for the complete template and detailed explanations. + +# Run this workflow by executing one of the following commands: +# - python .\examples\rl\run_rl_example.py .\examples\rl\cim.yml +# - (Requires installing MARO from source) maro local run .\examples\rl\cim.yml + +job: cim_rl_workflow +scenario_path: "examples/cim/rl" +log_path: "log/rl_job/cim.txt" +main: + num_episodes: 30 # Number of episodes to run. Each episode is one cycle of roll-out and training. + num_steps: null + eval_schedule: 5 + logging: + stdout: INFO + file: DEBUG +rollout: + logging: + stdout: INFO + file: DEBUG +training: + mode: simple + load_path: null + load_episode: null + checkpointing: + path: "checkpoint/rl_job/cim" + interval: 5 + logging: + stdout: INFO + file: DEBUG diff --git a/examples/rl/main.py b/examples/rl/main.py deleted file mode 100644 index d65b38d19..000000000 --- a/examples/rl/main.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from os.path import dirname, join, realpath - -from maro.rl.training import TrainingManager -from maro.rl.workflows.scenario import Scenario -from maro.utils import LoggerV2 - -# config variables -SCENARIO_NAME = "cim" -SCENARIO_PATH = join(dirname(dirname(realpath(__file__))), SCENARIO_NAME, "rl") -NUM_EPISODES = 50 -NUM_STEPS = None -CHECKPOINT_PATH = join(dirname(SCENARIO_PATH), "checkpoints") -CHECKPOINT_INTERVAL = 5 -EVAL_SCHEDULE = [10, 20, 30, 40, 50] -LOG_PATH = join(dirname(SCENARIO_PATH), "logs", SCENARIO_NAME) - - -if __name__ == "__main__": - scenario = Scenario(SCENARIO_PATH) - logger = LoggerV2("MAIN", dump_path=LOG_PATH) - - agent2policy = scenario.agent2policy - policy_creator = scenario.policy_creator - policy_dict = {name: get_policy_func(name) for name, get_policy_func in policy_creator.items()} - policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict} - trainer_creator = scenario.trainer_creator - - # evaluation schedule - logger.info(f"Policy will be evaluated at the end of episodes {EVAL_SCHEDULE}") - eval_point_index = 0 - - if scenario.trainable_policies is None: - trainable_policies = set(policy_creator.keys()) - else: - trainable_policies = set(scenario.trainable_policies) - - env_sampler = scenario.env_sampler_creator(policy_creator) - - trainable_policy_creator = {name: func for name, func in policy_creator.items() if name in trainable_policies} - trainable_agent2policy = {id_: name for id_, name in agent2policy.items() if name in trainable_policies} - training_manager = TrainingManager( - trainable_policy_creator, - trainer_creator, - trainable_agent2policy, - device_mapping=scenario.device_mapping, - logger=logger - ) - - # main loop - for ep in range(1, NUM_EPISODES + 1): - collect_time = training_time = 0 - segment, end_of_episode = 1, False - while not end_of_episode: - # experience collection - result = env_sampler.sample(num_steps=NUM_STEPS) - experiences = result["experiences"] - end_of_episode = result["end_of_episode"] - - if scenario.post_collect: - scenario.post_collect(result["info"], ep, segment) - - logger.info(f"Roll-out completed for episode {ep}. Training started...") - training_manager.record_experiences(experiences) - training_manager.train_step() - if CHECKPOINT_PATH and ep % CHECKPOINT_INTERVAL == 0: - pth = join(CHECKPOINT_PATH, str(ep)) - training_manager.save(pth) - logger.info(f"All trainer states saved under {pth}") - segment += 1 - - # performance details - if ep == EVAL_SCHEDULE[eval_point_index]: - eval_point_index += 1 - result = env_sampler.eval() - if scenario.post_evaluate: - scenario.post_evaluate(result["info"], ep) diff --git a/examples/rl/main_evaluation_only.py b/examples/rl/main_evaluation_only.py deleted file mode 100644 index 33b73868a..000000000 --- a/examples/rl/main_evaluation_only.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from os.path import dirname, join, realpath - -from maro.rl.workflows.scenario import Scenario - -# config variables -SCENARIO_NAME = "cim" -SCENARIO_PATH = join(dirname(dirname(realpath(__file__))), SCENARIO_NAME, "rl") - -if __name__ == "__main__": - scenario = Scenario(SCENARIO_PATH) - policy_creator = scenario.policy_creator - policy_dict = {name: get_policy_func(name) for name, get_policy_func in policy_creator.items()} - policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict} - - env_sampler = scenario.env_sampler_creator(policy_creator) - result = env_sampler.eval() - if scenario.post_evaluate: - scenario.post_evaluate(result["info"], 0) diff --git a/examples/rl/run_rl_example.py b/examples/rl/run_rl_example.py new file mode 100644 index 000000000..e50c4f7f1 --- /dev/null +++ b/examples/rl/run_rl_example.py @@ -0,0 +1,15 @@ +import argparse + +from maro.cli.local.commands import run + + +def get_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("conf_path", help='Path of the job deployment') + parser.add_argument("--evaluate_only", action="store_true", help="Only run evaluation part of the workflow") + return parser.parse_args() + + +if __name__ == "__main__": + args = get_args() + run(conf_path=args.conf_path, containerize=False, evaluate_only=args.evaluate_only) diff --git a/examples/rl/vm_scheduling.yml b/examples/rl/vm_scheduling.yml new file mode 100644 index 000000000..5f4f20747 --- /dev/null +++ b/examples/rl/vm_scheduling.yml @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +# Example RL config file for VM scheduling scenario. +# Please refer to `maro/rl/workflows/config/template.yml` for the complete template and detailed explanations. + +# Run this workflow by executing one of the following commands: +# - python .\examples\rl\run_rl_example.py .\examples\rl\vm_scheduling.yml +# - (Requires installing MARO from source) maro local run .\examples\rl\vm_scheduling.yml + +job: vm_scheduling_rl_workflow +scenario_path: "examples/vm_scheduling/rl" +log_path: "log/rl_job/vm_scheduling.txt" +main: + num_episodes: 30 # Number of episodes to run. Each episode is one cycle of roll-out and training. + num_steps: null + eval_schedule: 5 + logging: + stdout: INFO + file: DEBUG +rollout: + logging: + stdout: INFO + file: DEBUG +training: + mode: simple + load_path: null + load_episode: null + checkpointing: + path: "checkpoint/rl_job/vm_scheduling" + interval: 5 + logging: + stdout: INFO + file: DEBUG diff --git a/examples/vm_scheduling/rl/env_sampler.py b/examples/vm_scheduling/rl/env_sampler.py index 8b56aa4a9..87023e7b7 100644 --- a/examples/vm_scheduling/rl/env_sampler.py +++ b/examples/vm_scheduling/rl/env_sampler.py @@ -4,7 +4,7 @@ import sys from collections import defaultdict from os.path import dirname, realpath -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import numpy as np @@ -31,9 +31,9 @@ def __init__(self, get_env, policy_creator, agent2policy, get_test_env=None): self._pm_state_history = np.zeros((pm_window_size - 1, self.num_pms, 2)) self._legal_pm_mask = None - def _get_global_and_agent_state( - self, event: DecisionPayload, tick: int = None - ) -> Tuple[Optional[np.ndarray], Dict[Any, np.ndarray]]: + def _get_global_and_agent_state_impl( + self, event: DecisionPayload, tick: int = None, + ) -> Tuple[Union[None, np.ndarray, List[object]], Dict[Any, Union[np.ndarray, List[object]]]]: pm_state, vm_state = self._get_pm_state(), self._get_vm_state(event) # get the legal number of PM. legal_pm_mask = np.zeros(self.num_pms + 1) @@ -55,7 +55,9 @@ def _get_global_and_agent_state( state = np.concatenate((pm_state.flatten(), vm_state.flatten(), legal_pm_mask)).astype(np.float32) return None, {"AGENT": state} - def _translate_to_env_action(self, action_dict: Dict[Any, np.ndarray], event: DecisionPayload) -> Dict[Any, object]: + def _translate_to_env_action( + self, action_dict: Dict[Any, Union[np.ndarray, List[object]]], event: DecisionPayload, + ) -> Dict[Any, object]: if action_dict["AGENT"] == self.num_pms: return {"AGENT": PostponeAction(vm_id=event.vm_id, postpone_step=1)} else: diff --git a/maro/cli/local/commands.py b/maro/cli/local/commands.py index 2b6ae3c92..a619a6ca3 100644 --- a/maro/cli/local/commands.py +++ b/maro/cli/local/commands.py @@ -56,7 +56,7 @@ def get_redis_conn(port=None): # Functions executed on CLI commands -def run(conf_path: str, containerize: bool = False, **kwargs): +def run(conf_path: str, containerize: bool = False, evaluate_only: bool = False, **kwargs): # Load job configuration file parser = ConfigParser(conf_path) env_by_component = parser.as_env(containerize=containerize) @@ -64,13 +64,18 @@ def run(conf_path: str, containerize: bool = False, **kwargs): path_mapping = parser.get_path_mapping(containerize=True) try: start_rl_job_with_docker_compose( - parser.config, LOCAL_MARO_ROOT, DOCKERFILE_PATH, DOCKER_IMAGE_NAME, env_by_component, path_mapping + parser.config, LOCAL_MARO_ROOT, DOCKERFILE_PATH, + DOCKER_IMAGE_NAME, env_by_component, path_mapping, evaluate_only, ) except KeyboardInterrupt: - stop_rl_job_with_docker_compose(parser.config["job"]) + stop_rl_job_with_docker_compose(parser.config["job"], LOCAL_MARO_ROOT) else: try: - start_rl_job(parser.as_env(), LOCAL_MARO_ROOT) + start_rl_job( + env_by_component=parser.as_env(), + maro_root=LOCAL_MARO_ROOT, + evaluate_only=evaluate_only, + ) except KeyboardInterrupt: sys.exit(1) diff --git a/maro/cli/local/utils.py b/maro/cli/local/utils.py index bb2d019eb..75047f773 100644 --- a/maro/cli/local/utils.py +++ b/maro/cli/local/utils.py @@ -101,13 +101,15 @@ def exec(cmd: str, env: dict, debug: bool = False) -> subprocess.Popen: ) -def start_rl_job(env_by_component: Dict[str, dict], maro_root: str, background: bool = False) -> List[subprocess.Popen]: +def start_rl_job( + env_by_component: Dict[str, dict], maro_root: str, evaluate_only: bool, background: bool = False, +) -> List[subprocess.Popen]: def get_local_script_path(component: str): return os.path.join(maro_root, "maro", "rl", "workflows", f"{component.split('-')[0]}.py") procs = [ exec( - f"python {get_local_script_path(component)}", + f"python {get_local_script_path(component)}" + ("" if not evaluate_only else " --evaluate_only"), format_env_vars({**env, "PYTHONPATH": maro_root}, mode="proc"), debug=not background ) @@ -152,13 +154,13 @@ def start_rl_job_in_containers( return containers -def get_docker_compose_yml_path() -> str: - return os.path.join(os.getcwd(), "docker-compose.yml") +def get_docker_compose_yml_path(maro_root: str) -> str: + return os.path.join(maro_root, "tmp", "docker-compose.yml") def start_rl_job_with_docker_compose( conf: dict, context: str, dockerfile_path: str, image_name: str, env_by_component: Dict[str, dict], - path_mapping: Dict[str, str] + path_mapping: Dict[str, str], evaluate_only: bool, ) -> None: common_spec = { "build": {"context": context, "dockerfile": dockerfile_path}, @@ -172,19 +174,21 @@ def start_rl_job_with_docker_compose( **deepcopy(common_spec), **{ "container_name": f"{job}.{component}", - "command": f"python3 /maro/maro/rl/workflows/{component.split('-')[0]}.py", + "command": f"python3 /maro/maro/rl/workflows/{component.split('-')[0]}.py" + ( + "" if not evaluate_only else "--evaluate_only"), "environment": format_env_vars(env, mode="docker-compose") } } for component, env in env_by_component.items() } - with open(get_docker_compose_yml_path(), "w") as fp: + docker_compose_file_path = get_docker_compose_yml_path(maro_root=context) + with open(docker_compose_file_path, "w") as fp: yaml.safe_dump(manifest, fp) - subprocess.run(["docker-compose", "--project-name", job, "up", "--remove-orphans"]) + subprocess.run(["docker-compose", "--project-name", job, "-f", docker_compose_file_path, "up", "--remove-orphans"]) -def stop_rl_job_with_docker_compose(job_name: str): +def stop_rl_job_with_docker_compose(job_name: str, context: str): subprocess.run(["docker-compose", "--project-name", job_name, "down"]) - os.remove(get_docker_compose_yml_path()) + os.remove(get_docker_compose_yml_path(maro_root=context)) diff --git a/maro/cli/maro.py b/maro/cli/maro.py index cd1554484..65956d2c6 100644 --- a/maro/cli/maro.py +++ b/maro/cli/maro.py @@ -173,6 +173,7 @@ def load_parser_local(prev_parser: ArgumentParser, global_parser: ArgumentParser ) parser.add_argument("conf_path", help='Path of the job deployment') parser.add_argument("-c", "--containerize", action="store_true", help="Whether to run jobs in containers") + parser.add_argument("--evaluate_only", action="store_true", help="Only run evaluation part of the workflow") parser.add_argument("-p", "--port", type=int, default=20000, help="") parser.set_defaults(func=run) diff --git a/maro/rl/model/policy_net.py b/maro/rl/model/policy_net.py index 7ff4c0c22..515611647 100644 --- a/maro/rl/model/policy_net.py +++ b/maro/rl/model/policy_net.py @@ -136,8 +136,8 @@ def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tens actions = self._get_actions_exploring_impl(states) return actions else: - action_logps = self.get_action_logps(states) - logps, actions = action_logps.max(dim=1) + action_logps = self.get_action_probs(states) + _, actions = action_logps.max(dim=1) return actions.unsqueeze(1) def _get_actions_exploring_impl(self, states: torch.Tensor) -> torch.Tensor: diff --git a/maro/rl/policy/abs_policy.py b/maro/rl/policy/abs_policy.py index 9af3bfeaf..0aec0274c 100644 --- a/maro/rl/policy/abs_policy.py +++ b/maro/rl/policy/abs_policy.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from typing import Dict, Iterable, Optional +from typing import Dict, List, Optional, Union import numpy as np import torch @@ -27,14 +27,14 @@ def __init__(self, name: str, trainable: bool) -> None: self._trainable = trainable @abstractmethod - def get_actions(self, states: object) -> Iterable: + def get_actions(self, states: object) -> object: """Get actions according to states. Args: states (object): States. Returns: - actions (Iterable): Actions. + actions (object): Actions. """ raise NotImplementedError @@ -107,11 +107,11 @@ class RuleBasedPolicy(AbsPolicy, metaclass=ABCMeta): def __init__(self, name: str) -> None: super(RuleBasedPolicy, self).__init__(name=name, trainable=False) - def get_actions(self, states: object) -> object: + def get_actions(self, states: List[object]) -> List[object]: return self._rule(states) @abstractmethod - def _rule(self, states: object) -> object: + def _rule(self, states: List[object]) -> List[object]: raise NotImplementedError def explore(self) -> None: diff --git a/maro/rl/rollout/batch_env_sampler.py b/maro/rl/rollout/batch_env_sampler.py index 62e825a6a..0fc4b62a0 100644 --- a/maro/rl/rollout/batch_env_sampler.py +++ b/maro/rl/rollout/batch_env_sampler.py @@ -1,14 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - +import os import time from itertools import chain from typing import Dict, List, Optional, Tuple +import torch import zmq from zmq import Context, Poller from maro.rl.utils.common import bytes_to_pyobj, get_own_ip_address, pyobj_to_bytes +from maro.rl.utils.objects import FILE_SUFFIX from maro.utils import DummyLogger, LoggerV2 from .env_sampler import ExpElement @@ -189,5 +191,24 @@ def eval(self, policy_state: Dict[str, object] = None) -> dict: "info": [res["info"][0] for res in results], } + def load_policy_state(self, path: str) -> List[str]: + file_list = os.listdir(path) + policy_state_dict = {} + loaded = [] + for file_name in file_list: + if "non_policy" in file_name or not file_name.endswith(f"_policy.{FILE_SUFFIX}"): # TODO: remove hardcode + continue + policy_name, policy_state = torch.load(os.path.join(path, file_name)) + policy_state_dict[policy_name] = policy_state + loaded.append(policy_name) + + req = { + "type": "set_policy_state", + "policy_state": policy_state_dict, + "index": (self._ep, -1), + } + self._controller.collect(req, self._sampling_parallelism) + return loaded + def exit(self) -> None: self._controller.exit() diff --git a/maro/rl/rollout/env_sampler.py b/maro/rl/rollout/env_sampler.py index f5c84fa86..abd55292c 100644 --- a/maro/rl/rollout/env_sampler.py +++ b/maro/rl/rollout/env_sampler.py @@ -4,15 +4,17 @@ from __future__ import annotations import collections +import os from abc import ABCMeta, abstractmethod from collections import defaultdict, deque from dataclasses import dataclass -from typing import Any, Callable, Deque, Dict, List, Optional, Tuple, Type +from typing import Any, Callable, Deque, Dict, List, Optional, Tuple, Type, Union import numpy as np import torch from maro.rl.policy import AbsPolicy, RLPolicy +from maro.rl.utils.objects import FILE_SUFFIX from maro.simulator import Env @@ -43,15 +45,18 @@ def set_policy_state(self, policy_state_dict: Dict[str, object]) -> None: if isinstance(policy, RLPolicy): policy.set_state(policy_state) - def choose_actions(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[Any, np.ndarray]: + def choose_actions( + self, state_by_agent: Dict[Any, Union[np.ndarray, List[object]]] + ) -> Dict[Any, Union[np.ndarray, List[object]]]: """Choose action according to the given (observable) states of all agents. Args: - state_by_agent (Dict[Any, np.ndarray]): Dictionary containing each agent's state vector. - The keys are agent names. + state_by_agent (Dict[Any, Union[np.ndarray, List[object]]]): Dictionary containing each agent's states. + If the policy is a `RLPolicy`, its state is a Numpy array. Otherwise, its state is a list of objects. Returns: - actions (Dict[Any, np.ndarray]): Dict that contains the action for all agents. + actions (Dict[Any, Union[np.ndarray, List[object]]]): Dict that contains the action for all agents. + If the policy is a `RLPolicy`, its action is a Numpy array. Otherwise, its action is a list of objects. """ self.switch_to_eval_mode() with torch.no_grad(): @@ -59,7 +64,9 @@ def choose_actions(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[Any, np. return ret @abstractmethod - def _choose_actions_impl(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[Any, np.ndarray]: + def _choose_actions_impl( + self, state_by_agent: Dict[Any, Union[np.ndarray, List[object]]], + ) -> Dict[Any, Union[np.ndarray, List[object]]]: """Implementation of `choose_actions`. """ raise NotImplementedError @@ -91,7 +98,9 @@ def __init__( ) -> None: super(SimpleAgentWrapper, self).__init__(policy_dict=policy_dict, agent2policy=agent2policy) - def _choose_actions_impl(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[Any, np.ndarray]: + def _choose_actions_impl( + self, state_by_agent: Dict[Any, Union[np.ndarray, List[object]]], + ) -> Dict[Any, Union[np.ndarray, List[object]]]: # Aggregate states by policy states_by_policy = defaultdict(list) # {str: list of np.ndarray} agents_by_policy = defaultdict(list) # {str: list of str} @@ -103,11 +112,14 @@ def _choose_actions_impl(self, state_by_agent: Dict[Any, np.ndarray]) -> Dict[An action_dict = {} for policy_name in agents_by_policy: policy = self._policy_dict[policy_name] - states = np.vstack(states_by_policy[policy_name]) # np.ndarray - action_dict.update(zip( - agents_by_policy[policy_name], # list of str (agent name) - policy.get_actions(states), # list of action - )) + + if isinstance(policy, RLPolicy): + states = np.vstack(states_by_policy[policy_name]) # np.ndarray + else: + states = states_by_policy[policy_name] # List[object] + actions = policy.get_actions(states) # np.ndarray or List[object] + action_dict.update(zip(agents_by_policy[policy_name], actions)) + return action_dict def explore(self) -> None: @@ -132,7 +144,7 @@ class CacheElement: state: np.ndarray agent_state_dict: Dict[Any, np.ndarray] action_dict: Dict[Any, np.ndarray] - env_action_dict: Dict[Any, object] + env_action_dict: Dict[Any, np.ndarray] @dataclass @@ -215,7 +227,7 @@ def __init__( get_test_env: Callable[[], Env] = None, ) -> None: self._learn_env = get_env() - self._test_env = get_test_env() if get_test_env is not None else self._learn_env + self._test_env = get_test_env() if get_test_env is not None else get_env() self._env: Optional[Env] = None self._event = None # Need this to remember the last event if an episode is divided into multiple segments @@ -235,6 +247,9 @@ def __init__( agent_id for agent_id, policy_name in self._agent2policy.items() if policy_name in self._trainable_policies } + assert all([policy_name in self._rl_policy_dict for policy_name in self._trainable_policies]), \ + "All trainable policies must be RL policies!" + # Global state & agent state self._state: Optional[np.ndarray] = None self._agent_state_dict: Dict[Any, np.ndarray] = {} @@ -248,10 +263,9 @@ def __init__( def rl_policy_dict(self) -> Dict[str, RLPolicy]: return self._rl_policy_dict - @abstractmethod def _get_global_and_agent_state( self, event: object, tick: int = None, - ) -> Tuple[Optional[np.ndarray], Dict[Any, np.ndarray]]: + ) -> Tuple[Optional[object], Dict[Any, Union[np.ndarray, List[object]]]]: """Get the global and individual agents' states. Args: @@ -259,17 +273,33 @@ def _get_global_and_agent_state( tick (int, default=None): Current tick. Returns: - Global state (np.ndarray) - Dict of agent states (Dict[Any, np.ndarray]) + Global state (Optional[object]) + Dict of agent states (Dict[Any, Union[np.ndarray, List[object]]]). If the policy is a `RLPolicy`, + its state is a Numpy array. Otherwise, its state is a list of objects. """ + global_state, agent_state_dict = self._get_global_and_agent_state_impl(event, tick) + for agent_name, state in agent_state_dict.items(): + policy_name = self._agent2policy[agent_name] + policy = self._policy_dict[policy_name] + if isinstance(policy, RLPolicy) and not isinstance(state, np.ndarray): + raise ValueError(f"Agent {agent_name} uses a RLPolicy but its state is not a np.ndarray.") + return global_state, agent_state_dict + + @abstractmethod + def _get_global_and_agent_state_impl( + self, event: object, tick: int = None, + ) -> Tuple[Union[None, np.ndarray, List[object]], Dict[Any, Union[np.ndarray, List[object]]]]: raise NotImplementedError @abstractmethod - def _translate_to_env_action(self, action_dict: Dict[Any, np.ndarray], event: object) -> Dict[Any, object]: + def _translate_to_env_action( + self, action_dict: Dict[Any, Union[np.ndarray, List[object]]], event: object, + ) -> Dict[Any, object]: """Translate model-generated actions into an object that can be executed by the env. Args: - action_dict (Dict[Any, np.ndarray]): Action for all agents. + action_dict (Dict[Any, Union[np.ndarray, List[object]]]): Action for all agents. If the policy is a + `RLPolicy`, its (input) action is a Numpy array. Otherwise, its (input) action is a list of objects. event (object): Decision event. Returns: @@ -291,6 +321,13 @@ def _get_reward(self, env_action_dict: Dict[Any, object], event: object, tick: i """ raise NotImplementedError + def _reset(self) -> None: + self._env.reset() + self._info.clear() + self._trans_cache.clear() + _, self._event, _ = self._env.step(None) + self._state, self._agent_state_dict = self._get_global_and_agent_state(self._event) + def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Optional[int] = None) -> dict: """Sample experiences. @@ -306,11 +343,7 @@ def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Op # Init the env self._env = self._learn_env if not self._agent_state_dict: - self._env.reset() - self._info.clear() - self._trans_cache.clear() - _, self._event, _ = self._env.step(None) - self._state, self._agent_state_dict = self._get_global_and_agent_state(self._event) + self._reset() # Update policy state if necessary if policy_state is not None: @@ -331,11 +364,16 @@ def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Op event=self._event, state=self._state, agent_state_dict={ - id_: state for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents + id_: state + for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents + }, + action_dict={ + id_: action + for id_, action in action_dict.items() if id_ in self._trainable_agents }, - action_dict={id_: action for id_, action in action_dict.items() if id_ in self._trainable_agents}, env_action_dict={ - id_: env_action for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents + id_: env_action + for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents }, ) ) @@ -412,17 +450,28 @@ def set_policy_state(self, policy_state_dict: Dict[str, object]) -> None: """ self._agent_wrapper.set_policy_state(policy_state_dict) + def load_policy_state(self, path: str) -> List[str]: + file_list = os.listdir(path) + policy_state_dict = {} + loaded = [] + for file_name in file_list: + if "non_policy" in file_name or not file_name.endswith(f"_policy.{FILE_SUFFIX}"): # TODO: remove hardcode + continue + policy_name, policy_state = torch.load(os.path.join(path, file_name)) + policy_state_dict[policy_name] = policy_state + loaded.append(policy_name) + self.set_policy_state(policy_state_dict) + + return loaded + def eval(self, policy_state: Dict[str, object] = None) -> dict: self._env = self._test_env + self._reset() if policy_state is not None: self.set_policy_state(policy_state) self._agent_wrapper.exploit() - self._env.reset() - is_done = False - _, self._event, _ = self._env.step(None) - self._state, self._agent_state_dict = self._get_global_and_agent_state(self._event) - while not is_done: + while self._agent_state_dict: action_dict = self._agent_wrapper.choose_actions(self._agent_state_dict) env_action_dict = self._translate_to_env_action(action_dict, self._event) @@ -447,7 +496,7 @@ def eval(self, policy_state: Dict[str, object] = None) -> dict: else self._get_global_and_agent_state(self._event) tick_bound = self._env.tick - self._reward_eval_delay - while self._trans_cache and self._trans_cache[0].tick <= tick_bound: + while len(self._trans_cache) > 0 and self._trans_cache[0].tick <= tick_bound: cache_element = self._trans_cache.popleft() reward_dict = self._get_reward(cache_element.env_action_dict, cache_element.event, cache_element.tick) self._post_eval_step(cache_element, reward_dict) diff --git a/maro/rl/rollout/worker.py b/maro/rl/rollout/worker.py index 0639a1c11..6c9522e6c 100644 --- a/maro/rl/rollout/worker.py +++ b/maro/rl/rollout/worker.py @@ -48,10 +48,13 @@ def _compute(self, msg: list) -> None: else: req = bytes_to_pyobj(msg[-1]) assert isinstance(req, dict) - assert req["type"] in {"sample", "eval"} + assert req["type"] in {"sample", "eval", "set_policy_state"} if req["type"] == "sample": result = self._env_sampler.sample(policy_state=req["policy_state"], num_steps=req["num_steps"]) - else: + elif req["type"] == "eval": result = self._env_sampler.eval(policy_state=req["policy_state"]) + else: + self._env_sampler.set_policy_state(policy_state_dict=req["policy_state"]) + result = True self._stream.send(pyobj_to_bytes({"result": result, "index": req["index"]})) diff --git a/maro/rl/training/algorithms/base/ac_ppo_base.py b/maro/rl/training/algorithms/base/ac_ppo_base.py index 708e88bd0..dc757140c 100644 --- a/maro/rl/training/algorithms/base/ac_ppo_base.py +++ b/maro/rl/training/algorithms/base/ac_ppo_base.py @@ -175,15 +175,13 @@ def update_actor_with_grad(self, grad_dict: dict) -> None: self._policy.train() self._policy.apply_gradients(grad_dict) - def get_state(self) -> dict: + def get_non_policy_state(self) -> dict: return { - "policy": self._policy.get_state(), "critic": self._v_critic_net.get_state(), } - def set_state(self, ops_state_dict: dict) -> None: - self._policy.set_state(ops_state_dict["policy"]) - self._v_critic_net.set_state(ops_state_dict["critic"]) + def set_non_policy_state(self, state: dict) -> None: + self._v_critic_net.set_state(state["critic"]) def _preprocess_batch(self, batch: TransitionBatch) -> TransitionBatch: """Preprocess the batch to get the returns & advantages. diff --git a/maro/rl/training/algorithms/ddpg.py b/maro/rl/training/algorithms/ddpg.py index d46176596..61c398c01 100644 --- a/maro/rl/training/algorithms/ddpg.py +++ b/maro/rl/training/algorithms/ddpg.py @@ -196,19 +196,17 @@ def update_actor(self, batch: TransitionBatch) -> None: self._policy.train() self._policy.train_step(self._get_actor_loss(batch)) - def get_state(self) -> dict: + def get_non_policy_state(self) -> dict: return { - "policy": self._policy.get_state(), "target_policy": self._target_policy.get_state(), "critic": self._q_critic_net.get_state(), "target_critic": self._target_q_critic_net.get_state(), } - def set_state(self, ops_state_dict: dict) -> None: - self._policy.set_state(ops_state_dict["policy"]) - self._target_policy.set_state(ops_state_dict["target_policy"]) - self._q_critic_net.set_state(ops_state_dict["critic"]) - self._target_q_critic_net.set_state(ops_state_dict["target_critic"]) + def set_non_policy_state(self, state: dict) -> None: + self._target_policy.set_state(state["target_policy"]) + self._q_critic_net.set_state(state["critic"]) + self._target_q_critic_net.set_state(state["target_critic"]) def soft_update_target(self) -> None: """Soft update the target policy and target critic. diff --git a/maro/rl/training/algorithms/dqn.py b/maro/rl/training/algorithms/dqn.py index 517ca5daa..0389acf8b 100644 --- a/maro/rl/training/algorithms/dqn.py +++ b/maro/rl/training/algorithms/dqn.py @@ -133,15 +133,13 @@ def update(self, batch: TransitionBatch) -> None: self._policy.train() self._policy.train_step(self._get_batch_loss(batch)) - def get_state(self) -> dict: + def get_non_policy_state(self) -> dict: return { - "policy": self._policy.get_state(), "target_q_net": self._target_policy.get_state(), } - def set_state(self, ops_state_dict: dict) -> None: - self._policy.set_state(ops_state_dict["policy"]) - self._target_policy.set_state(ops_state_dict["target_q_net"]) + def set_non_policy_state(self, state: dict) -> None: + self._target_policy.set_state(state["target_q_net"]) def soft_update_target(self) -> None: """Soft update the target policy. diff --git a/maro/rl/training/algorithms/maddpg.py b/maro/rl/training/algorithms/maddpg.py index 69e80b2ac..3bc09dd99 100644 --- a/maro/rl/training/algorithms/maddpg.py +++ b/maro/rl/training/algorithms/maddpg.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import asyncio +import os from dataclasses import dataclass from typing import Callable, Dict, List, Tuple @@ -13,6 +14,7 @@ from maro.rl.rollout import ExpElement from maro.rl.training import AbsTrainOps, MultiAgentTrainer, RandomMultiReplayMemory, RemoteOps, TrainerParams, remote from maro.rl.utils import MultiTransitionBatch, get_torch_device, ndarray_to_tensor +from maro.rl.utils.objects import FILE_SUFFIX from maro.utils import clone @@ -274,12 +276,11 @@ def set_actor_state(self, ops_state_dict: dict) -> None: self._policy.set_state(ops_state_dict["policy"]) self._target_policy.set_state(ops_state_dict["target_policy"]) - def get_state(self) -> dict: - return {**self.get_actor_state(), **self.get_critic_state()} + def get_non_policy_state(self) -> dict: + return self.get_critic_state() - def set_state(self, ops_state_dict: dict) -> None: - self.set_critic_state(ops_state_dict) - self.set_actor_state(ops_state_dict) + def set_non_policy_state(self, state: dict) -> None: + self.set_critic_state(state) def to_device(self, device: str) -> None: self._device = get_torch_device(device) @@ -460,16 +461,24 @@ def get_policy_state(self) -> Dict[str, object]: def load(self, path: str) -> None: self._assert_ops_exists() - trainer_state = torch.load(path) - for ops_name, ops_state in trainer_state.items(): - self._ops_dict[ops_name].set_state(ops_state) + + policy_state_dict = torch.load(os.path.join(path, f"{self.name}_policy.{FILE_SUFFIX}")) + non_policy_state_dict = torch.load(os.path.join(path, f"{self.name}_non_policy.{FILE_SUFFIX}")) + for ops_name in policy_state_dict: + self._ops_dict[ops_name].set_state({**policy_state_dict[ops_name], **non_policy_state_dict[ops_name]}) def save(self, path: str) -> None: self._assert_ops_exists() + trainer_state = {ops.name: ops.get_state() for ops in self._actor_ops_list} if self._params.shared_critic: trainer_state[self._critic_ops.name] = self._critic_ops.get_state() - torch.save(trainer_state, path) + + policy_state_dict = {ops_name: state["policy"] for ops_name, state in trainer_state.items()} + non_policy_state_dict = {ops_name: state["non_policy"] for ops_name, state in trainer_state.items()} + + torch.save(policy_state_dict, os.path.join(path, f"{self.name}_policy.{FILE_SUFFIX}")) + torch.save(non_policy_state_dict, os.path.join(path, f"{self.name}_non_policy.{FILE_SUFFIX}")) def _assert_ops_exists(self) -> None: if not self._actor_ops_list: diff --git a/maro/rl/training/train_ops.py b/maro/rl/training/train_ops.py index ee0c0d19f..6d60f93a5 100644 --- a/maro/rl/training/train_ops.py +++ b/maro/rl/training/train_ops.py @@ -20,7 +20,7 @@ class AbsTrainOps(object, metaclass=ABCMeta): Args: name (str): Name of the ops. This is usually a policy name. policy_creator (Callable[[str], RLPolicy]): Function to create a policy instance. - parallelism (int, default=1): Desired degree of data parallelism. + parallelism (int, default=1): Desired degree of data parallelism. """ def __init__( @@ -54,23 +54,26 @@ def policy_action_dim(self) -> int: def parallelism(self) -> int: return self._parallelism - @abstractmethod def get_state(self) -> dict: """Get the train ops's state. Returns: A dict that contains ops's state. """ - raise NotImplementedError + return { + "policy": self.get_policy_state(), + "non_policy": self.get_non_policy_state(), + } - @abstractmethod def set_state(self, ops_state_dict: dict) -> None: """Set ops's state. Args: ops_state_dict (dict): New ops state. """ - raise NotImplementedError + assert ops_state_dict["policy"][0] == self._policy.name + self.set_policy_state(ops_state_dict["policy"][1]) + self.set_non_policy_state(ops_state_dict["non_policy"]) def get_policy_state(self) -> Tuple[str, object]: """Get the policy's state. @@ -89,6 +92,24 @@ def set_policy_state(self, policy_state: object) -> None: """ self._policy.set_state(policy_state) + @abstractmethod + def get_non_policy_state(self) -> dict: + """Get states other than policy. + + Returns: + A dict that contains non-policy state. + """ + raise NotImplementedError + + @abstractmethod + def set_non_policy_state(self, state: dict) -> None: + """Set states other than policy. + + Args: + state (dict): Non-policy state. + """ + raise NotImplementedError + @abstractmethod def to_device(self, device: str): raise NotImplementedError diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index 7aadc51eb..8e3580a15 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - +import os from abc import ABCMeta, abstractmethod from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional, Tuple, Union @@ -9,6 +9,7 @@ from maro.rl.policy import AbsPolicy, RLPolicy from maro.rl.rollout import ExpElement +from maro.rl.utils.objects import FILE_SUFFIX from maro.utils import LoggerV2 from .train_ops import AbsTrainOps, RemoteOps @@ -212,11 +213,24 @@ def get_policy_state(self) -> Dict[str, object]: def load(self, path: str) -> None: self._assert_ops_exists() - self._ops.set_state(torch.load(path)) + + policy_state = torch.load(os.path.join(path, f"{self.name}_policy.{FILE_SUFFIX}")) + non_policy_state = torch.load(os.path.join(path, f"{self.name}_non_policy.{FILE_SUFFIX}")) + + self._ops.set_state({ + "policy": policy_state, + "non_policy": non_policy_state, + }) def save(self, path: str) -> None: self._assert_ops_exists() - torch.save(self._ops.get_state(), path) + + ops_state = self._ops.get_state() + policy_state = ops_state["policy"] + non_policy_state = ops_state["non_policy"] + + torch.save(policy_state, os.path.join(path, f"{self.name}_policy.{FILE_SUFFIX}")) + torch.save(non_policy_state, os.path.join(path, f"{self.name}_non_policy.{FILE_SUFFIX}")) def _assert_ops_exists(self) -> None: if not self._ops: diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/training_manager.py index dc95d7f5c..1d5ae39cb 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/training_manager.py @@ -13,7 +13,7 @@ from maro.utils.exception.rl_toolkit_exception import MissingTrainer from .trainer import AbsTrainer, MultiAgentTrainer -from .utils import extract_trainer_name, get_trainer_state_path +from .utils import extract_trainer_name class TrainingManager(object): @@ -111,17 +111,14 @@ def record_experiences(self, experiences: List[List[ExpElement]]) -> None: def load(self, path: str) -> List[str]: loaded = [] for trainer_name, trainer in self._trainer_dict.items(): - pth = get_trainer_state_path(path, trainer_name) - if os.path.isfile(pth): - trainer.load(pth) - loaded.append(trainer_name) - + trainer.load(path) + loaded.append(trainer_name) return loaded def save(self, path: str) -> None: os.makedirs(path, exist_ok=True) for trainer_name, trainer in self._trainer_dict.items(): - trainer.save(get_trainer_state_path(path, trainer_name)) + trainer.save(path) def exit(self) -> None: if self._proxy_address: diff --git a/maro/rl/training/utils.py b/maro/rl/training/utils.py index c0843c158..4e1d5bdf3 100644 --- a/maro/rl/training/utils.py +++ b/maro/rl/training/utils.py @@ -3,8 +3,6 @@ import os -FILE_SUFFIX = "ckpt" - def extract_trainer_name(policy_name: str) -> str: """Extract the trainer name from the policy name. @@ -18,5 +16,6 @@ def extract_trainer_name(policy_name: str) -> str: return policy_name.split(".")[0] -def get_trainer_state_path(dir_path: str, trainer_name: str) -> str: - return os.path.join(dir_path, f"{trainer_name}.{FILE_SUFFIX}") +def get_latest_ep(path: str) -> int: + ep_list = [int(ep) for ep in os.listdir(path)] + return max(ep_list) diff --git a/maro/rl/utils/common.py b/maro/rl/utils/common.py index 2c0bda9c2..e69b907b7 100644 --- a/maro/rl/utils/common.py +++ b/maro/rl/utils/common.py @@ -76,6 +76,9 @@ def get_own_ip_address() -> str: def get_ip_address_by_hostname(host: str) -> str: + if host in ("localhost", "127.0.0.1"): + return get_own_ip_address() + while True: try: return socket.gethostbyname(host) diff --git a/maro/rl/utils/objects.py b/maro/rl/utils/objects.py index e4dc3160d..0a73c0f66 100644 --- a/maro/rl/utils/objects.py +++ b/maro/rl/utils/objects.py @@ -2,3 +2,4 @@ # Licensed under the MIT license. SHAPE_CHECK_FLAG = True +FILE_SUFFIX = "ckpt" diff --git a/maro/rl/workflows/__init__.py b/maro/rl/workflows/__init__.py new file mode 100644 index 000000000..9a0454564 --- /dev/null +++ b/maro/rl/workflows/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. diff --git a/maro/rl/workflows/config/parser.py b/maro/rl/workflows/config/parser.py index 8a06bfceb..98dc88005 100644 --- a/maro/rl/workflows/config/parser.py +++ b/maro/rl/workflows/config/parser.py @@ -88,12 +88,13 @@ def _validate_rollout_section(self) -> None: raise KeyError( f"{self._validation_err_pfx}: missing field 'sampling' under section 'rollout.parallelism'" ) - if not isinstance(conf["sampling"], int) or conf["sampling"] <= 0: + + train_prl = conf["sampling"] + eval_prl = 1 if "eval" not in conf or conf["eval"] is None else conf["eval"] + if not isinstance(train_prl, int) or train_prl <= 0: raise TypeError(f"{self._validation_err_pfx}: 'rollout.parallelism.sampling' must be a positive int") - if "eval" in conf and not isinstance(conf["eval"], int) or conf["eval"] <= 0: + if not isinstance(eval_prl, int) or eval_prl <= 0: raise TypeError(f"{self._validation_err_pfx}: 'rollout.parallelism.eval' must be a positive int") - - train_prl, eval_prl = conf["sampling"], conf.get("eval", 1) if max(train_prl, eval_prl) > 1: if "controller" not in conf: raise KeyError( @@ -161,8 +162,12 @@ def _validate_training_section(self) -> None: if "logging" in self._config["training"]: self._validate_logging_section("training", self._config["training"]["logging"]) - if "load_path" in self._config["training"] and not isinstance(self._config["training"]["load_path"], str): + load_path = self._config["training"].get("load_path", None) + if load_path is not None and not isinstance(load_path, str): raise TypeError(f"{self._validation_err_pfx}: 'training.load_path' must be a string") + load_episode = self._config["training"].get("load_episode", None) + if load_episode is not None and not isinstance(load_episode, int): + raise TypeError(f"{self._validation_err_pfx}: 'training.load_episode' must be a integer") if "checkpointing" in self._config["training"]: self._validate_checkpointing_section(self._config["training"]["checkpointing"]) @@ -275,6 +280,9 @@ def as_env(self, containerize: bool = False) -> dict: load_path = self._config["training"].get("load_path", None) if load_path is not None: env["main"]["LOAD_PATH"] = path_mapping[load_path] + load_episode = self._config["training"].get("load_episode", None) + if load_episode is not None: + env["main"]["LOAD_EPISODE"] = str(load_episode) if "checkpointing" in self._config["training"]: conf = self._config["training"]["checkpointing"] @@ -293,8 +301,9 @@ def as_env(self, containerize: bool = False) -> dict: }) if "parallelism" in self._config["rollout"]: - env_sampling_parallelism = self._config["rollout"]["parallelism"]["sampling"] - env_eval_parallelism = self._config["rollout"]["parallelism"].get("eval", 1) + conf = self._config["rollout"]["parallelism"] + env_sampling_parallelism = conf["sampling"] + env_eval_parallelism = 1 if "eval" not in conf or conf["eval"] is None else conf["eval"] else: env_sampling_parallelism = env_eval_parallelism = 1 rollout_parallelism = max(env_sampling_parallelism, env_eval_parallelism) diff --git a/maro/rl/workflows/config/template.yml b/maro/rl/workflows/config/template.yml index 3725da0af..330fee2b9 100644 --- a/maro/rl/workflows/config/template.yml +++ b/maro/rl/workflows/config/template.yml @@ -58,7 +58,9 @@ training: # the policies it manages as well as the states of auxillary models (e.g., critics in the Actor-Critic paradigm). # If the path corresponds to an existing directory, the program will look under the directory for snapshot files # that match the trainer names specified in the scenario and attempt to load from them. - load_path: "/path/to/your/models" + load_path: "/path/to/your/models" # or `null` + # Which episode of the previously saved snapshots to load. If it is not provided, the last snapshot will be loaded. + load_episode: null # Optional section to specify model checkpointing settings. checkpointing: # Directory to save trainer snapshots under. Snapshot files created at different episodes will be saved under diff --git a/maro/rl/workflows/main.py b/maro/rl/workflows/main.py index fe64634f3..099885554 100644 --- a/maro/rl/workflows/main.py +++ b/maro/rl/workflows/main.py @@ -1,21 +1,33 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - +import argparse import os import time from typing import List -import torch - from maro.rl.rollout import BatchEnvSampler, ExpElement from maro.rl.training import TrainingManager +from maro.rl.training.utils import get_latest_ep from maro.rl.utils import get_torch_device from maro.rl.utils.common import float_or_none, get_env, int_or_none, list_or_none from maro.rl.workflows.scenario import Scenario from maro.utils import LoggerV2 -def main(scenario: Scenario) -> None: +def get_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="MARO RL workflow parser") + parser.add_argument("--evaluate_only", action="store_true", help="Only run evaluation part of the workflow") + return parser.parse_args() + + +def main(scenario: Scenario, args: argparse.Namespace) -> None: + if args.evaluate_only: + evaluate_only_workflow(scenario) + else: + training_workflow(scenario) + + +def training_workflow(scenario: Scenario) -> None: num_episodes = int(get_env("NUM_EPISODES")) num_steps = int_or_none(get_env("NUM_STEPS", required=False)) @@ -26,6 +38,7 @@ def main(scenario: Scenario) -> None: stdout_level=get_env("LOG_LEVEL_STDOUT", required=False, default="CRITICAL"), file_level=get_env("LOG_LEVEL_FILE", required=False, default="CRITICAL"), ) + logger.info("Start training workflow.") env_sampling_parallelism = int_or_none(get_env("ENV_SAMPLE_PARALLELISM", required=False)) env_eval_parallelism = int_or_none(get_env("ENV_EVAL_PARALLELISM", required=False)) @@ -82,15 +95,26 @@ def main(scenario: Scenario) -> None: ) load_path = get_env("LOAD_PATH", required=False) + load_episode = int_or_none(get_env("LOAD_EPISODE", required=False)) if load_path: assert isinstance(load_path, str) - loaded = training_manager.load(load_path) - logger.info(f"Loaded states for {loaded} from {load_path}") + + ep = load_episode if load_episode is not None else get_latest_ep(load_path) + path = os.path.join(load_path, str(ep)) + + loaded = env_sampler.load_policy_state(path) + logger.info(f"Loaded policies {loaded} into env sampler from {path}") + + loaded = training_manager.load(path) + logger.info(f"Loaded trainers {loaded} from {path}") + start_ep = ep + 1 + else: + start_ep = 1 checkpoint_path = get_env("CHECKPOINT_PATH", required=False) checkpoint_interval = int_or_none(get_env("CHECKPOINT_INTERVAL", required=False)) # main loop - for ep in range(1, num_episodes + 1): + for ep in range(start_ep, num_episodes + 1): collect_time = training_time = 0 segment, end_of_episode = 1, False while not end_of_episode: @@ -121,7 +145,7 @@ def main(scenario: Scenario) -> None: segment += 1 # performance details - logger.info(f"ep {ep} - roll-out time: {collect_time}, training time: {training_time}") + logger.info(f"ep {ep} - roll-out time: {collect_time:.2f} seconds, training time: {training_time:.2f} seconds") if eval_schedule and ep == eval_schedule[eval_point_index]: eval_point_index += 1 result = env_sampler.eval( @@ -135,7 +159,53 @@ def main(scenario: Scenario) -> None: training_manager.exit() +def evaluate_only_workflow(scenario: Scenario) -> None: + logger = LoggerV2( + "MAIN", + dump_path=get_env("LOG_PATH"), + dump_mode="a", + stdout_level=get_env("LOG_LEVEL_STDOUT", required=False, default="CRITICAL"), + file_level=get_env("LOG_LEVEL_FILE", required=False, default="CRITICAL"), + ) + logger.info("Start evaluate only workflow.") + + env_sampling_parallelism = int_or_none(get_env("ENV_SAMPLE_PARALLELISM", required=False)) + env_eval_parallelism = int_or_none(get_env("ENV_EVAL_PARALLELISM", required=False)) + parallel_rollout = env_sampling_parallelism is not None or env_eval_parallelism is not None + + policy_creator = scenario.policy_creator + if parallel_rollout: + env_sampler = BatchEnvSampler( + sampling_parallelism=env_sampling_parallelism, + port=int(get_env("ROLLOUT_CONTROLLER_PORT")), + min_env_samples=int_or_none(get_env("MIN_ENV_SAMPLES", required=False)), + grace_factor=float_or_none(get_env("GRACE_FACTOR", required=False)), + eval_parallelism=env_eval_parallelism, + logger=logger, + ) + else: + env_sampler = scenario.env_sampler_creator(policy_creator) + + load_path = get_env("LOAD_PATH", required=False) + load_episode = int_or_none(get_env("LOAD_EPISODE", required=False)) + if load_path: + assert isinstance(load_path, str) + + ep = load_episode if load_episode is not None else get_latest_ep(load_path) + path = os.path.join(load_path, str(ep)) + + loaded = env_sampler.load_policy_state(path) + logger.info(f"Loaded policies {loaded} into env sampler from {path}") + + result = env_sampler.eval() + if scenario.post_evaluate: + scenario.post_evaluate(result["info"], -1) + + if isinstance(env_sampler, BatchEnvSampler): + env_sampler.exit() + + if __name__ == "__main__": # get user-defined scenario ingredients - scenario = Scenario(get_env("SCENARIO_PATH")) - main(scenario) + run_scenario = Scenario(get_env("SCENARIO_PATH")) + main(run_scenario, args=get_args()) diff --git a/maro/rl/workflows/scenario.py b/maro/rl/workflows/scenario.py index 4c21ffe0f..d385bbff1 100644 --- a/maro/rl/workflows/scenario.py +++ b/maro/rl/workflows/scenario.py @@ -7,7 +7,6 @@ from typing import Any, Callable, Dict, List from maro.rl.policy import AbsPolicy -from maro.rl.policy.abs_policy import RLPolicy from maro.rl.rollout import AbsEnvSampler from maro.rl.training import AbsTrainer From 12195138c1d6ef292c3ea5c876c918bdceb9c72c Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Sun, 24 Apr 2022 10:51:13 +0800 Subject: [PATCH 471/482] RL incremental refactor (#501) * Refactor rollout logic. Allow multiple sampling in one epoch, so that we can generate more data for training. AC & PPO for continuous action policy; refine AC & PPO logic. Cherry pick RL changes from GYM-DDPG Cherry pick RL changes from GYM-SAC Minor error in doc string * Add min_n_sample in template and parser * Resolve PR comments. Fix a minor issue in SAC. --- examples/cim/rl/algorithms/ac.py | 74 +----- examples/cim/rl/algorithms/dqn.py | 29 --- examples/cim/rl/algorithms/maddpg.py | 66 +---- examples/cim/rl/algorithms/ppo.py | 8 +- examples/vm_scheduling/rl/algorithms/ac.py | 74 +----- examples/vm_scheduling/rl/algorithms/dqn.py | 29 --- maro/rl/model/__init__.py | 7 + maro/rl/model/abs_net.py | 48 ++-- maro/rl/model/algorithm_nets/__init__.py | 0 maro/rl/model/algorithm_nets/ac_based.py | 53 ++++ maro/rl/model/algorithm_nets/ddpg.py | 38 +++ maro/rl/model/algorithm_nets/sac.py | 38 +++ maro/rl/model/policy_net.py | 129 ++++++---- maro/rl/policy/abs_policy.py | 86 +++++-- maro/rl/policy/continuous_rl_policy.py | 23 +- maro/rl/policy/discrete_rl_policy.py | 83 ++++--- maro/rl/rollout/env_sampler.py | 3 +- maro/rl/training/algorithms/__init__.py | 10 +- maro/rl/training/algorithms/ac.py | 15 +- maro/rl/training/algorithms/base/__init__.py | 4 +- .../training/algorithms/base/ac_ppo_base.py | 213 ++++++++-------- maro/rl/training/algorithms/ddpg.py | 55 ++-- maro/rl/training/algorithms/dqn.py | 21 +- maro/rl/training/algorithms/maddpg.py | 47 ++-- maro/rl/training/algorithms/ppo.py | 15 +- maro/rl/training/algorithms/sac.py | 234 ++++++++++++++++++ maro/rl/training/replay_memory.py | 25 ++ maro/rl/training/trainer.py | 38 ++- maro/rl/training/training_manager.py | 9 +- maro/rl/utils/transition_batch.py | 3 - maro/rl/workflows/config/parser.py | 2 + maro/rl/workflows/config/template.yml | 3 + maro/rl/workflows/main.py | 40 +-- 33 files changed, 919 insertions(+), 603 deletions(-) create mode 100644 maro/rl/model/algorithm_nets/__init__.py create mode 100644 maro/rl/model/algorithm_nets/ac_based.py create mode 100644 maro/rl/model/algorithm_nets/ddpg.py create mode 100644 maro/rl/model/algorithm_nets/sac.py create mode 100644 maro/rl/training/algorithms/sac.py diff --git a/examples/cim/rl/algorithms/ac.py b/examples/cim/rl/algorithms/ac.py index 869968c8f..5fdb40261 100644 --- a/examples/cim/rl/algorithms/ac.py +++ b/examples/cim/rl/algorithms/ac.py @@ -6,9 +6,9 @@ import torch from torch.optim import Adam, RMSprop -from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet +from maro.rl.model import DiscreteACBasedNet, FullyConnected, VNet from maro.rl.policy import DiscretePolicyGradient -from maro.rl.training.algorithms import DiscreteActorCriticTrainer, DiscreteActorCriticParams +from maro.rl.training.algorithms import ActorCriticTrainer, ActorCriticParams actor_net_conf = { "hidden_dims": [256, 128, 64], @@ -29,7 +29,7 @@ critic_learning_rate = 0.001 -class MyActorNet(DiscretePolicyNet): +class MyActorNet(DiscreteACBasedNet): def __init__(self, state_dim: int, action_num: int) -> None: super(MyActorNet, self).__init__(state_dim=state_dim, action_num=action_num) self._actor = FullyConnected(input_dim=state_dim, output_dim=action_num, **actor_net_conf) @@ -38,37 +38,6 @@ def __init__(self, state_dim: int, action_num: int) -> None: def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: return self._actor(states) - def freeze(self) -> None: - self.freeze_all_parameters() - - def unfreeze(self) -> None: - self.unfreeze_all_parameters() - - def step(self, loss: torch.Tensor) -> None: - self._optim.zero_grad() - loss.backward() - self._optim.step() - - def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: - self._optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: - for name, param in self.named_parameters(): - param.grad = grad[name] - self._optim.step() - - def get_state(self) -> dict: - return { - "network": self.state_dict(), - "optim": self._optim.state_dict(), - } - - def set_state(self, net_state: dict) -> None: - self.load_state_dict(net_state["network"]) - self._optim.load_state_dict(net_state["optim"]) - class MyCriticNet(VNet): def __init__(self, state_dim: int) -> None: @@ -79,46 +48,15 @@ def __init__(self, state_dim: int) -> None: def _get_v_values(self, states: torch.Tensor) -> torch.Tensor: return self._critic(states).squeeze(-1) - def step(self, loss: torch.Tensor) -> None: - self._optim.zero_grad() - loss.backward() - self._optim.step() - - def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: - self._optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: - for name, param in self.named_parameters(): - param.grad = grad[name] - self._optim.step() - - def get_state(self) -> dict: - return { - "network": self.state_dict(), - "optim": self._optim.state_dict(), - } - - def set_state(self, net_state: dict) -> None: - self.load_state_dict(net_state["network"]) - self._optim.load_state_dict(net_state["optim"]) - - def freeze(self) -> None: - self.freeze_all_parameters() - - def unfreeze(self) -> None: - self.unfreeze_all_parameters() - def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) -def get_ac(state_dim: int, name: str) -> DiscreteActorCriticTrainer: - return DiscreteActorCriticTrainer( +def get_ac(state_dim: int, name: str) -> ActorCriticTrainer: + return ActorCriticTrainer( name=name, - params=DiscreteActorCriticParams( + params=ActorCriticParams( get_v_critic_net_func=lambda: MyCriticNet(state_dim), reward_discount=.0, grad_iters=10, diff --git a/examples/cim/rl/algorithms/dqn.py b/examples/cim/rl/algorithms/dqn.py index edefcfa9d..afa2127a5 100644 --- a/examples/cim/rl/algorithms/dqn.py +++ b/examples/cim/rl/algorithms/dqn.py @@ -32,35 +32,6 @@ def __init__(self, state_dim: int, action_num: int) -> None: def _get_q_values_for_all_actions(self, states: torch.Tensor) -> torch.Tensor: return self._fc(states) - def step(self, loss: torch.Tensor) -> None: - self._optim.zero_grad() - loss.backward() - self._optim.step() - - def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: - self._optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: - for name, param in self.named_parameters(): - param.grad = grad[name] - self._optim.step() - - def get_state(self) -> object: - return {"network": self.state_dict(), "optim": self._optim.state_dict()} - - def set_state(self, net_state: object) -> None: - assert isinstance(net_state, dict) - self.load_state_dict(net_state["network"]) - self._optim.load_state_dict(net_state["optim"]) - - def freeze(self) -> None: - self.freeze_all_parameters() - - def unfreeze(self) -> None: - self.unfreeze_all_parameters() - def get_policy(state_dim: int, action_num: int, name: str) -> ValueBasedPolicy: return ValueBasedPolicy( diff --git a/examples/cim/rl/algorithms/maddpg.py b/examples/cim/rl/algorithms/maddpg.py index a077189e0..ba0cbc7ad 100644 --- a/examples/cim/rl/algorithms/maddpg.py +++ b/examples/cim/rl/algorithms/maddpg.py @@ -7,7 +7,7 @@ import torch from torch.optim import Adam, RMSprop -from maro.rl.model import DiscretePolicyNet, FullyConnected, MultiQNet +from maro.rl.model import DiscreteACBasedNet, FullyConnected, MultiQNet from maro.rl.policy import DiscretePolicyGradient from maro.rl.training.algorithms import DiscreteMADDPGTrainer, DiscreteMADDPGParams @@ -32,7 +32,7 @@ # ##################################################################################################################### -class MyActorNet(DiscretePolicyNet): +class MyActorNet(DiscreteACBasedNet): def __init__(self, state_dim: int, action_num: int) -> None: super(MyActorNet, self).__init__(state_dim=state_dim, action_num=action_num) self._actor = FullyConnected(input_dim=state_dim, output_dim=action_num, **actor_net_conf) @@ -41,37 +41,6 @@ def __init__(self, state_dim: int, action_num: int) -> None: def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: return self._actor(states) - def freeze(self) -> None: - self.freeze_all_parameters() - - def unfreeze(self) -> None: - self.unfreeze_all_parameters() - - def step(self, loss: torch.Tensor) -> None: - self._optim.zero_grad() - loss.backward() - self._optim.step() - - def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: - self._optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: - for name, param in self.named_parameters(): - param.grad = grad[name] - self._optim.step() - - def get_state(self) -> dict: - return { - "network": self.state_dict(), - "optim": self._optim.state_dict() - } - - def set_state(self, net_state: dict) -> None: - self.load_state_dict(net_state["network"]) - self._optim.load_state_dict(net_state["optim"]) - class MyMultiCriticNet(MultiQNet): def __init__(self, state_dim: int, action_dims: List[int]) -> None: @@ -82,37 +51,6 @@ def __init__(self, state_dim: int, action_dims: List[int]) -> None: def _get_q_values(self, states: torch.Tensor, actions: List[torch.Tensor]) -> torch.Tensor: return self._critic(torch.cat([states] + actions, dim=1)).squeeze(-1) - def step(self, loss: torch.Tensor) -> None: - self._optim.zero_grad() - loss.backward() - self._optim.step() - - def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: - self._optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: - for name, param in self.named_parameters(): - param.grad = grad[name] - self._optim.step() - - def get_state(self) -> dict: - return { - "network": self.state_dict(), - "optim": self._optim.state_dict() - } - - def set_state(self, net_state: dict) -> None: - self.load_state_dict(net_state["network"]) - self._optim.load_state_dict(net_state["optim"]) - - def freeze(self) -> None: - self.freeze_all_parameters() - - def unfreeze(self) -> None: - self.unfreeze_all_parameters() - def get_multi_critic_net(state_dim: int, action_dims: List[int]) -> MyMultiCriticNet: return MyMultiCriticNet(state_dim, action_dims) diff --git a/examples/cim/rl/algorithms/ppo.py b/examples/cim/rl/algorithms/ppo.py index 2694decd3..9f2253565 100644 --- a/examples/cim/rl/algorithms/ppo.py +++ b/examples/cim/rl/algorithms/ppo.py @@ -1,7 +1,7 @@ import torch from maro.rl.policy import DiscretePolicyGradient -from maro.rl.training.algorithms import DiscretePPOParams, DiscretePPOTrainer +from maro.rl.training.algorithms import PPOParams, PPOTrainer from .ac import MyActorNet, MyCriticNet @@ -10,10 +10,10 @@ def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGrad return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) -def get_ppo(state_dim: int, name: str) -> DiscretePPOTrainer: - return DiscretePPOTrainer( +def get_ppo(state_dim: int, name: str) -> PPOTrainer: + return PPOTrainer( name=name, - params=DiscretePPOParams( + params=PPOParams( get_v_critic_net_func=lambda: MyCriticNet(state_dim), reward_discount=.0, grad_iters=10, diff --git a/examples/vm_scheduling/rl/algorithms/ac.py b/examples/vm_scheduling/rl/algorithms/ac.py index d102a7543..ef7efd84b 100644 --- a/examples/vm_scheduling/rl/algorithms/ac.py +++ b/examples/vm_scheduling/rl/algorithms/ac.py @@ -6,9 +6,9 @@ import torch from torch.optim import Adam, SGD -from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet +from maro.rl.model import DiscreteACBasedNet, FullyConnected, VNet from maro.rl.policy import DiscretePolicyGradient -from maro.rl.training.algorithms import DiscreteActorCriticTrainer, DiscreteActorCriticParams +from maro.rl.training.algorithms import ActorCriticTrainer, ActorCriticParams actor_net_conf = { @@ -31,7 +31,7 @@ critic_learning_rate = 0.001 -class MyActorNet(DiscretePolicyNet): +class MyActorNet(DiscreteACBasedNet): def __init__(self, state_dim: int, action_num: int, num_features: int) -> None: super(MyActorNet, self).__init__(state_dim=state_dim, action_num=action_num) self._num_features = num_features @@ -43,37 +43,6 @@ def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: masks += 1e-8 # this is to prevent zero probability and infinite logP. return self._actor(features) * masks - def freeze(self) -> None: - self.freeze_all_parameters() - - def unfreeze(self) -> None: - self.unfreeze_all_parameters() - - def step(self, loss: torch.Tensor) -> None: - self._optim.zero_grad() - loss.backward() - self._optim.step() - - def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: - self._optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: - for name, param in self.named_parameters(): - param.grad = grad[name] - self._optim.step() - - def get_state(self) -> dict: - return { - "network": self.state_dict(), - "optim": self._optim.state_dict(), - } - - def set_state(self, net_state: dict) -> None: - self.load_state_dict(net_state["network"]) - self._optim.load_state_dict(net_state["optim"]) - class MyCriticNet(VNet): def __init__(self, state_dim: int, num_features: int) -> None: @@ -87,46 +56,15 @@ def _get_v_values(self, states: torch.Tensor) -> torch.Tensor: masks += 1e-8 # this is to prevent zero probability and infinite logP. return self._critic(features).squeeze(-1) - def step(self, loss: torch.Tensor) -> None: - self._optim.zero_grad() - loss.backward() - self._optim.step() - - def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: - self._optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: - for name, param in self.named_parameters(): - param.grad = grad[name] - self._optim.step() - - def get_state(self) -> dict: - return { - "network": self.state_dict(), - "optim": self._optim.state_dict(), - } - - def set_state(self, net_state: dict) -> None: - self.load_state_dict(net_state["network"]) - self._optim.load_state_dict(net_state["optim"]) - - def freeze(self) -> None: - self.freeze_all_parameters() - - def unfreeze(self) -> None: - self.unfreeze_all_parameters() - def get_policy(state_dim: int, action_num: int, num_features: int, name: str) -> DiscretePolicyGradient: return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num, num_features)) -def get_ac(state_dim: int, num_features: int, name: str) -> DiscreteActorCriticTrainer: - return DiscreteActorCriticTrainer( +def get_ac(state_dim: int, num_features: int, name: str) -> ActorCriticTrainer: + return ActorCriticTrainer( name=name, - params=DiscreteActorCriticParams( + params=ActorCriticParams( get_v_critic_net_func=lambda: MyCriticNet(state_dim, num_features), reward_discount=0.9, grad_iters=100, diff --git a/examples/vm_scheduling/rl/algorithms/dqn.py b/examples/vm_scheduling/rl/algorithms/dqn.py index 31fda945f..6cff78783 100644 --- a/examples/vm_scheduling/rl/algorithms/dqn.py +++ b/examples/vm_scheduling/rl/algorithms/dqn.py @@ -40,35 +40,6 @@ def _get_q_values_for_all_actions(self, states: torch.Tensor) -> torch.Tensor: q_for_all_actions = self._fc(states[:, :self._num_features]) return q_for_all_actions + (masks - 1) * 1e8 - def step(self, loss: torch.Tensor) -> None: - self._optim.zero_grad() - loss.backward() - self._optim.step() - - def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: - self._optim.zero_grad() - loss.backward() - return {name: param.grad for name, param in self.named_parameters()} - - def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: - for name, param in self.named_parameters(): - param.grad = grad[name] - self._optim.step() - - def get_state(self) -> object: - return {"network": self.state_dict(), "optim": self._optim.state_dict()} - - def set_state(self, net_state: object) -> None: - assert isinstance(net_state, dict) - self.load_state_dict(net_state["network"]) - self._optim.load_state_dict(net_state["optim"]) - - def freeze(self) -> None: - self.freeze_all_parameters() - - def unfreeze(self) -> None: - self.unfreeze_all_parameters() - class MaskedEpsGreedy: def __init__(self, state_dim: int, num_features: int) -> None: diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index 7f79d1cb8..cc18ef592 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -7,6 +7,9 @@ from .policy_net import ContinuousPolicyNet, DiscretePolicyNet, PolicyNet from .q_net import ContinuousQNet, DiscreteQNet, QNet from .v_net import VNet +from .algorithm_nets.ac_based import ContinuousACBasedNet, DiscreteACBasedNet +from .algorithm_nets.ddpg import ContinuousDDPGNet +from .algorithm_nets.sac import ContinuousSACNet __all__ = [ "AbsNet", @@ -15,4 +18,8 @@ "ContinuousPolicyNet", "DiscretePolicyNet", "PolicyNet", "ContinuousQNet", "DiscreteQNet", "QNet", "VNet", + + "ContinuousACBasedNet", "DiscreteACBasedNet", + "ContinuousDDPGNet", + "ContinuousSACNet", ] diff --git a/maro/rl/model/abs_net.py b/maro/rl/model/abs_net.py index 8c309dda4..19bcfea56 100644 --- a/maro/rl/model/abs_net.py +++ b/maro/rl/model/abs_net.py @@ -3,10 +3,11 @@ from __future__ import annotations -from abc import ABCMeta, abstractmethod -from typing import Any, Dict +from abc import ABCMeta +from typing import Any, Dict, Optional import torch.nn +from torch.optim import Optimizer class AbsNet(torch.nn.Module, metaclass=ABCMeta): @@ -17,16 +18,18 @@ class AbsNet(torch.nn.Module, metaclass=ABCMeta): def __init__(self) -> None: super(AbsNet, self).__init__() - @abstractmethod + self._optim: Optional[Optimizer] = None + def step(self, loss: torch.Tensor) -> None: """Run a training step to update the net's parameters according to the given loss. Args: loss (torch.tensor): Loss used to update the model. """ - raise NotImplementedError + self._optim.zero_grad() + loss.backward() + self._optim.step() - @abstractmethod def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: """Get the gradients with respect to all parameters according to the given loss. @@ -36,37 +39,42 @@ def get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: Returns: Gradients (Dict[str, torch.Tensor]): A dict that contains gradients for all parameters. """ - raise NotImplementedError + self._optim.zero_grad() + loss.backward() + return {name: param.grad for name, param in self.named_parameters()} - @abstractmethod def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: """Apply gradients to the net to update all parameters. Args: grad (Dict[str, torch.Tensor]): A dict that contains gradients for all parameters. """ - raise NotImplementedError + for name, param in self.named_parameters(): + param.grad = grad[name] + self._optim.step() def _forward_unimplemented(self, *input: Any) -> None: pass - @abstractmethod def get_state(self) -> object: """Get the net's state. Returns: state (object): A object that contains the net's state. """ - raise NotImplementedError + return { + "network": self.state_dict(), + "optim": self._optim.state_dict(), + } - @abstractmethod - def set_state(self, net_state: object) -> None: + def set_state(self, net_state: dict) -> None: """Set the net's state. Args: - net_state (object): A object that contains the net's state. + net_state (dict): A dict that contains the net's state. """ - raise NotImplementedError + self.load_state_dict(net_state["network"]) + self._optim.load_state_dict(net_state["optim"]) def soft_update(self, other_model: AbsNet, tau: float) -> None: """Soft update the net's parameters according to another net, i.e., @@ -83,19 +91,19 @@ def soft_update(self, other_model: AbsNet, tau: float) -> None: for params, other_params in zip(self.parameters(), other_model.parameters()): params.data = (1 - tau) * params.data + tau * other_params.data - @abstractmethod def freeze(self) -> None: """(Partially) freeze the current model. The users should write their own strategy to determine which - parameters to freeze. + parameters to freeze. Freeze all parameters is capable in most cases. You could overwrite this method + when necessary. """ - raise NotImplementedError + self.freeze_all_parameters() - @abstractmethod def unfreeze(self) -> None: """(Partially) unfreeze the current model. The users should write their own strategy to determine which - parameters to freeze. + parameters to freeze. Unfreeze all parameters is capable in most cases. You could overwrite this method + when necessary. """ - raise NotImplementedError + self.unfreeze_all_parameters() def freeze_all_parameters(self) -> None: """Freeze all parameters. diff --git a/maro/rl/model/algorithm_nets/__init__.py b/maro/rl/model/algorithm_nets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/maro/rl/model/algorithm_nets/ac_based.py b/maro/rl/model/algorithm_nets/ac_based.py new file mode 100644 index 000000000..d5ee8c83e --- /dev/null +++ b/maro/rl/model/algorithm_nets/ac_based.py @@ -0,0 +1,53 @@ +from abc import ABCMeta +from typing import Tuple + +import torch + +from maro.rl.model import ContinuousPolicyNet, DiscretePolicyNet + + +class DiscreteACBasedNet(DiscretePolicyNet, metaclass=ABCMeta): + """Policy net for policies that are trained by Actor-Critic or PPO algorithm and with discrete actions. + + The following methods should be implemented: + - _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: + + Overwrite one or multiple of following methods when necessary. + - freeze(self) -> None: + - unfreeze(self) -> None: + - step(self, loss: torch.Tensor) -> None: + - get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + - apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + - get_state(self) -> dict: + - set_state(self, net_state: dict) -> None: + """ + pass + + +class ContinuousACBasedNet(ContinuousPolicyNet, metaclass=ABCMeta): + """Policy net for policies that are trained by Actor-Critic or PPO algorithm and with continuous actions. + + The following methods should be implemented: + - _get_actions_with_logps_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + - _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + + Overwrite one or multiple of following methods when necessary. + - freeze(self) -> None: + - unfreeze(self) -> None: + - step(self, loss: torch.Tensor) -> None: + - get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + - apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + - get_state(self) -> dict: + - set_state(self, net_state: dict) -> None: + """ + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + actions, _ = self._get_actions_with_logps_impl(states, exploring) + return actions + + def _get_actions_with_probs_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + # Not used in Actor-Critic or PPO + pass + + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + # Not used in Actor-Critic or PPO + pass diff --git a/maro/rl/model/algorithm_nets/ddpg.py b/maro/rl/model/algorithm_nets/ddpg.py new file mode 100644 index 000000000..2d81af64d --- /dev/null +++ b/maro/rl/model/algorithm_nets/ddpg.py @@ -0,0 +1,38 @@ +from abc import ABCMeta +from typing import Tuple + +import torch + +from maro.rl.model import ContinuousPolicyNet + + +class ContinuousDDPGNet(ContinuousPolicyNet, metaclass=ABCMeta): + """Policy net for policies that are trained by DDPG. + + The following methods should be implemented: + - _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + + Overwrite one or multiple of following methods when necessary. + - freeze(self) -> None: + - unfreeze(self) -> None: + - step(self, loss: torch.Tensor) -> None: + - get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + - apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + - get_state(self) -> dict: + - set_state(self, net_state: dict) -> None: + """ + def _get_actions_with_probs_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + # Not used in DDPG + pass + + def _get_actions_with_logps_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + # Not used in DDPG + pass + + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + # Not used in DDPG + pass + + def _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + # Not used in DDPG + pass diff --git a/maro/rl/model/algorithm_nets/sac.py b/maro/rl/model/algorithm_nets/sac.py new file mode 100644 index 000000000..2aff0323c --- /dev/null +++ b/maro/rl/model/algorithm_nets/sac.py @@ -0,0 +1,38 @@ +from abc import ABCMeta +from typing import Tuple + +import torch + +from maro.rl.model import ContinuousPolicyNet + + +class ContinuousSACNet(ContinuousPolicyNet, metaclass=ABCMeta): + """Policy net for policies that are trained by SAC. + + The following methods should be implemented: + - _get_actions_with_logps_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + + Overwrite one or multiple of following methods when necessary. + - freeze(self) -> None: + - unfreeze(self) -> None: + - step(self, loss: torch.Tensor) -> None: + - get_gradients(self, loss: torch.Tensor) -> Dict[str, torch.Tensor]: + - apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: + - get_state(self) -> dict: + - set_state(self, net_state: dict) -> None: + """ + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + actions, _ = self._get_actions_with_logps_impl(states, exploring) + return actions + + def _get_actions_with_probs_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + # Not used in SAC + pass + + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + # Not used in SAC + pass + + def _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + # Not used in SAC + pass diff --git a/maro/rl/model/policy_net.py b/maro/rl/model/policy_net.py index 515611647..329cb37d4 100644 --- a/maro/rl/model/policy_net.py +++ b/maro/rl/model/policy_net.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. from abc import ABCMeta, abstractmethod +from typing import Tuple import torch.nn from torch.distributions import Categorical @@ -33,23 +34,80 @@ def action_dim(self) -> int: return self._action_dim def get_actions(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: - """Get actions according to the given states. - - Args: - states (torch.Tensor): States. - exploring (bool): If it is True, return the results under exploring mode. Otherwise, return the results - under exploiting mode. - - Returns: - Actions (torch.Tensor) - """ assert self._shape_check(states=states), \ f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + actions = self._get_actions_impl(states, exploring) + assert self._shape_check(states=states, actions=actions), \ f"Actions shape check failed. Expecting: {(states.shape[0], self.action_dim)}, actual: {actions.shape}." + return actions + def get_actions_with_probs(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + + actions, probs = self._get_actions_with_probs_impl(states, exploring) + + assert self._shape_check(states=states, actions=actions), \ + f"Actions shape check failed. Expecting: {(states.shape[0], self.action_dim)}, actual: {actions.shape}." + assert len(probs.shape) == 1 and probs.shape[0] == states.shape[0] + + return actions, probs + + def get_actions_with_logps(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + + actions, logps = self._get_actions_with_logps_impl(states, exploring) + + assert self._shape_check(states=states, actions=actions), \ + f"Actions shape check failed. Expecting: {(states.shape[0], self.action_dim)}, actual: {actions.shape}." + assert len(logps.shape) == 1 and logps.shape[0] == states.shape[0] + + return actions, logps + + def get_states_actions_probs(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + + probs = self._get_states_actions_probs_impl(states, actions) + + assert len(probs.shape) == 1 and probs.shape[0] == states.shape[0] + + return probs + + def get_states_actions_logps(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + + logps = self._get_states_actions_logps_impl(states, actions) + + assert len(logps.shape) == 1 and logps.shape[0] == states.shape[0] + + return logps + + @abstractmethod + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + raise NotImplementedError + + @abstractmethod + def _get_actions_with_probs_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + raise NotImplementedError + + @abstractmethod + def _get_actions_with_logps_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + raise NotImplementedError + + @abstractmethod + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + raise NotImplementedError + + @abstractmethod + def _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + raise NotImplementedError + def _shape_check(self, states: torch.Tensor, actions: torch.Tensor = None) -> bool: """Check whether the states and actions have valid shapes. @@ -74,12 +132,6 @@ def _shape_check(self, states: torch.Tensor, actions: torch.Tensor = None) -> bo return False return True - @abstractmethod - def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: - """Implementation of `get_actions`. - """ - raise NotImplementedError - class DiscretePolicyNet(PolicyNet, metaclass=ABCMeta): """Policy network for discrete action spaces. @@ -114,17 +166,6 @@ def get_action_probs(self, states: torch.Tensor) -> torch.Tensor: f"actual: {action_probs.shape}." return action_probs - def get_action_logps(self, states: torch.Tensor) -> torch.Tensor: - """Get the log-probabilities for all actions. - - Args: - states (torch.Tensor): States. - - Returns: - logps (torch.Tensor): Lop-probability matrix with shape [batch_size, action_num]. - """ - return torch.log(self.get_action_probs(states)) - @abstractmethod def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: """Implementation of `get_action_probs`. The core logic of a discrete policy net should be implemented here. @@ -132,26 +173,30 @@ def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor: raise NotImplementedError def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + actions, _ = self._get_actions_with_probs_impl(states, exploring) + return actions + + def _get_actions_with_probs_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + probs = self.get_action_probs(states) if exploring: - actions = self._get_actions_exploring_impl(states) - return actions + distribution = Categorical(probs) + actions = distribution.sample().unsqueeze(1) + return actions, probs.gather(1, actions).squeeze(-1) else: - action_logps = self.get_action_probs(states) - _, actions = action_logps.max(dim=1) - return actions.unsqueeze(1) + probs, actions = probs.max(dim=1) + return actions.unsqueeze(1), probs - def _get_actions_exploring_impl(self, states: torch.Tensor) -> torch.Tensor: - """Get actions according to the states under exploring mode. + def _get_actions_with_logps_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: + actions, probs = self._get_actions_with_probs_impl(states, exploring) + return actions, torch.log(probs) - Args: - states (torch.Tensor): States. + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + probs = self.get_action_probs(states) + return probs.gather(1, actions).squeeze(-1) - Returns: - actions (torch.Tensor): Actions. - """ - action_probs = Categorical(self.get_action_probs(states)) - actions = action_probs.sample() - return actions.unsqueeze(1) + def _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + probs = self._get_states_actions_probs_impl(states, actions) + return torch.log(probs) class ContinuousPolicyNet(PolicyNet, metaclass=ABCMeta): diff --git a/maro/rl/policy/abs_policy.py b/maro/rl/policy/abs_policy.py index 0aec0274c..b8f6d04fa 100644 --- a/maro/rl/policy/abs_policy.py +++ b/maro/rl/policy/abs_policy.py @@ -4,12 +4,12 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Tuple import numpy as np import torch -from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape, ndarray_to_tensor +from maro.rl.utils import match_shape, ndarray_to_tensor, SHAPE_CHECK_FLAG class AbsPolicy(object, metaclass=ABCMeta): @@ -206,30 +206,82 @@ def apply_gradients(self, grad: dict) -> None: raise NotImplementedError def get_actions(self, states: np.ndarray) -> np.ndarray: - return self.get_actions_tensor(ndarray_to_tensor(states, device=self._device)).cpu().numpy() + actions = self.get_actions_tensor(ndarray_to_tensor(states, device=self._device)) + return actions.detach().cpu().numpy() def get_actions_tensor(self, states: torch.Tensor) -> torch.Tensor: - """Get actions according to states. Takes torch.Tensor as inputs and returns torch.Tensor. + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." - Args: - states (torch.Tensor): States. + actions = self._get_actions_impl(states) - Returns: - actions (torch.Tensor): Actions. - """ + assert self._shape_check(states=states, actions=actions), \ + f"Actions shape check failed. Expecting: {(states.shape[0], self.action_dim)}, actual: {actions.shape}." + + return actions + + def get_actions_with_probs(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + + actions, probs = self._get_actions_with_probs_impl(states) + + assert self._shape_check(states=states, actions=actions), \ + f"Actions shape check failed. Expecting: {(states.shape[0], self.action_dim)}, actual: {actions.shape}." + assert len(probs.shape) == 1 and probs.shape[0] == states.shape[0] + + return actions, probs + + def get_actions_with_logps(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: assert self._shape_check(states=states), \ f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." - actions = self._get_actions_impl(states, self._is_exploring) + + actions, logps = self._get_actions_with_logps_impl(states) + assert self._shape_check(states=states, actions=actions), \ f"Actions shape check failed. Expecting: {(states.shape[0], self.action_dim)}, actual: {actions.shape}." - if SHAPE_CHECK_FLAG: - assert self._post_check(states=states, actions=actions) - return actions + assert len(logps.shape) == 1 and logps.shape[0] == states.shape[0] + + return actions, logps + + def get_states_actions_probs(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + + probs = self._get_states_actions_probs_impl(states, actions) + + assert len(probs.shape) == 1 and probs.shape[0] == states.shape[0] + + return probs + + def get_states_actions_logps(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + assert self._shape_check(states=states), \ + f"States shape check failed. Expecting: {('BATCH_SIZE', self.state_dim)}, actual: {states.shape}." + + logps = self._get_states_actions_logps_impl(states, actions) + + assert len(logps.shape) == 1 and logps.shape[0] == states.shape[0] + + return logps @abstractmethod - def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: - """Implementation of `get_action_tensor`. - """ + def _get_actions_impl(self, states: torch.Tensor) -> torch.Tensor: + raise NotImplementedError + + @abstractmethod + def _get_actions_with_probs_impl(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + raise NotImplementedError + + @abstractmethod + def _get_actions_with_logps_impl(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + raise NotImplementedError + + @abstractmethod + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + raise NotImplementedError + + @abstractmethod + def _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: raise NotImplementedError @abstractmethod @@ -253,7 +305,7 @@ def get_state(self) -> object: raise NotImplementedError @abstractmethod - def set_state(self, policy_state: object) -> None: + def set_state(self, policy_state: dict) -> None: """Set the state of the policy. """ raise NotImplementedError diff --git a/maro/rl/policy/continuous_rl_policy.py b/maro/rl/policy/continuous_rl_policy.py index 5e3ade8fd..517512681 100644 --- a/maro/rl/policy/continuous_rl_policy.py +++ b/maro/rl/policy/continuous_rl_policy.py @@ -7,7 +7,6 @@ import torch from maro.rl.model import ContinuousPolicyNet - from .abs_policy import RLPolicy @@ -73,12 +72,24 @@ def policy_net(self) -> ContinuousPolicyNet: def _post_check(self, states: torch.Tensor, actions: torch.Tensor) -> bool: return all([ - (np.array(self._lbounds) <= actions.cpu().numpy()).all(), - (actions.cpu().numpy() < np.array(self._ubounds)).all() + (np.array(self._lbounds) <= actions.detach().cpu().numpy()).all(), + (actions.detach().cpu().numpy() < np.array(self._ubounds)).all() ]) - def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: - return self._policy_net.get_actions(states, exploring) + def _get_actions_impl(self, states: torch.Tensor) -> torch.Tensor: + return self._policy_net.get_actions(states, self._is_exploring) + + def _get_actions_with_probs_impl(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + return self._policy_net.get_actions_with_probs(states, self._is_exploring) + + def _get_actions_with_logps_impl(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + return self._policy_net.get_actions_with_logps(states, self._is_exploring) + + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + return self._policy_net.get_states_actions_probs(states, actions) + + def _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + return self._policy_net.get_states_actions_logps(states, actions) def train_step(self, loss: torch.Tensor) -> None: self._policy_net.step(loss) @@ -104,7 +115,7 @@ def train(self) -> None: def get_state(self) -> object: return self._policy_net.get_state() - def set_state(self, policy_state: object) -> None: + def set_state(self, policy_state: dict) -> None: self._policy_net.set_state(policy_state) def soft_update(self, other_policy: RLPolicy, tau: float) -> None: diff --git a/maro/rl/policy/discrete_rl_policy.py b/maro/rl/policy/discrete_rl_policy.py index 4850f1ff3..da1d3d119 100644 --- a/maro/rl/policy/discrete_rl_policy.py +++ b/maro/rl/policy/discrete_rl_policy.py @@ -85,6 +85,8 @@ def __init__( self._call_cnt = 0 self._warmup = warmup + self._softmax = torch.nn.Softmax(dim=1) + @property def q_net(self) -> DiscreteQNet: return self._q_net @@ -147,18 +149,43 @@ def q_values_tensor(self, states: torch.Tensor, actions: torch.Tensor) -> torch. def explore(self) -> None: pass # Overwrite the base method and turn off explore mode. - def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: + def _get_actions_impl(self, states: torch.Tensor) -> torch.Tensor: + actions, _ = self._get_actions_with_probs_impl(states) + return actions + + def _get_actions_with_probs_impl(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: self._call_cnt += 1 if self._call_cnt <= self._warmup: - return ndarray_to_tensor(np.random.randint(self.action_num, size=(states.shape[0], 1)), device=self._device) + actions = ndarray_to_tensor( + np.random.randint(self.action_num, size=(states.shape[0], 1)), + device=self._device, + ) + probs = torch.ones(states.shape[0]).float() * (1.0 / self.action_num) + return actions, probs q_matrix = self.q_values_for_all_actions_tensor(states) # [B, action_num] + q_matrix_softmax = self._softmax(q_matrix) _, actions = q_matrix.max(dim=1) # [B], [B] - if exploring: + if self._is_exploring: actions = self._exploration_func(states, actions.cpu().numpy(), self.action_num, **self._exploration_params) actions = ndarray_to_tensor(actions, device=self._device) - return actions.unsqueeze(1) # [B, 1] + + actions = actions.unsqueeze(1) + return actions, q_matrix_softmax.gather(1, actions).squeeze(-1) # [B, 1] + + def _get_actions_with_logps_impl(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + actions, probs = self._get_actions_with_probs_impl(states) + return actions, torch.log(probs) + + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + q_matrix = self.q_values_for_all_actions_tensor(states) + q_matrix_softmax = self._softmax(q_matrix) + return q_matrix_softmax.gather(1, actions).squeeze(-1) # [B] + + def _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + probs = self._get_states_actions_probs_impl(states, actions) + return torch.log(probs) def train_step(self, loss: torch.Tensor) -> None: return self._q_net.step(loss) @@ -184,7 +211,7 @@ def train(self) -> None: def get_state(self) -> object: return self._q_net.get_state() - def set_state(self, policy_state: object) -> None: + def set_state(self, policy_state: dict) -> None: self._q_net.set_state(policy_state) def soft_update(self, other_policy: RLPolicy, tau: float) -> None: @@ -223,8 +250,20 @@ def __init__( def policy_net(self) -> DiscretePolicyNet: return self._policy_net - def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: - return self._policy_net.get_actions(states, exploring) + def _get_actions_impl(self, states: torch.Tensor) -> torch.Tensor: + return self._policy_net.get_actions(states, self._is_exploring) + + def _get_actions_with_probs_impl(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + return self._policy_net.get_actions_with_probs(states, self._is_exploring) + + def _get_actions_with_logps_impl(self, states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + return self._policy_net.get_actions_with_logps(states, self._is_exploring) + + def _get_states_actions_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + return self._policy_net.get_states_actions_probs(states, actions) + + def _get_states_actions_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + return self._policy_net.get_states_actions_logps(states, actions) def train_step(self, loss: torch.Tensor) -> None: self._policy_net.step(loss) @@ -250,7 +289,7 @@ def train(self) -> None: def get_state(self) -> object: return self._policy_net.get_state() - def set_state(self, policy_state: object) -> None: + def set_state(self, policy_state: dict) -> None: self._policy_net.set_state(policy_state) def soft_update(self, other_policy: RLPolicy, tau: float) -> None: @@ -285,31 +324,13 @@ def get_action_logps(self, states: torch.Tensor) -> torch.Tensor: """ return torch.log(self.get_action_probs(states)) - def get_state_action_probs(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: - """Get the probabilities of the given state-action pairs. - - Args: - states (torch.Tensor): States. - actions (torch.Tensor): Actions. Should has same length with states. - - Returns: - action_probs (torch.Tensor): Probabilities of the given state-action pairs. - """ - assert self._shape_check(states=states, actions=actions) + def _get_state_action_probs_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: action_probs = self.get_action_probs(states) - return action_probs.gather(1, actions).squeeze() # [B] - - def get_state_action_logps(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: - """Get the log-probabilities of the given state-action pairs. + return action_probs.gather(1, actions).squeeze(-1) # [B] - Args: - states (torch.Tensor): States. - actions (torch.Tensor): Actions. Should has same length with states. - - Returns: - action_logps (torch.Tensor): Probabilities of the given state-action pairs. - """ - return torch.log(self.get_state_action_probs(states, actions)) + def _get_state_action_logps_impl(self, states: torch.Tensor, actions: torch.Tensor) -> torch.Tensor: + action_logps = self.get_action_logps(states) + return action_logps.gather(1, actions).squeeze(-1) # [B] def _to_device_impl(self, device: torch.device) -> None: self._policy_net.to(device) diff --git a/maro/rl/rollout/env_sampler.py b/maro/rl/rollout/env_sampler.py index abd55292c..c9211fe0e 100644 --- a/maro/rl/rollout/env_sampler.py +++ b/maro/rl/rollout/env_sampler.py @@ -7,6 +7,7 @@ import os from abc import ABCMeta, abstractmethod from collections import defaultdict, deque +from copy import deepcopy from dataclasses import dataclass from typing import Any, Callable, Deque, Dict, List, Optional, Tuple, Type, Union @@ -416,7 +417,7 @@ def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Op return { "end_of_episode": not self._agent_state_dict, "experiences": [experiences], - "info": [self._info], + "info": [deepcopy(self._info)], # TODO: may have overhead issues. Leave to future work. } def _post_polish_experiences(self, experiences: List[ExpElement]) -> List[ExpElement]: diff --git a/maro/rl/training/algorithms/__init__.py b/maro/rl/training/algorithms/__init__.py index d2a93f2ef..361e2ffb2 100644 --- a/maro/rl/training/algorithms/__init__.py +++ b/maro/rl/training/algorithms/__init__.py @@ -1,16 +1,18 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .ac import DiscreteActorCriticParams, DiscreteActorCriticTrainer +from .ac import ActorCriticParams, ActorCriticTrainer from .ddpg import DDPGParams, DDPGTrainer from .dqn import DQNParams, DQNTrainer from .maddpg import DiscreteMADDPGParams, DiscreteMADDPGTrainer -from .ppo import DiscretePPOParams, DiscretePPOTrainer +from .ppo import PPOParams, PPOTrainer +from .sac import SoftActorCriticParams, SoftActorCriticTrainer __all__ = [ - "DiscreteActorCriticTrainer", "DiscreteActorCriticParams", + "ActorCriticTrainer", "ActorCriticParams", "DDPGTrainer", "DDPGParams", "DQNTrainer", "DQNParams", "DiscreteMADDPGTrainer", "DiscreteMADDPGParams", - "DiscretePPOParams", "DiscretePPOTrainer", + "PPOParams", "PPOTrainer", + "SoftActorCriticParams", "SoftActorCriticTrainer", ] diff --git a/maro/rl/training/algorithms/ac.py b/maro/rl/training/algorithms/ac.py index ead616c4f..0740d8a2f 100644 --- a/maro/rl/training/algorithms/ac.py +++ b/maro/rl/training/algorithms/ac.py @@ -4,12 +4,12 @@ from dataclasses import dataclass from typing import Dict -from maro.rl.training.algorithms.base import DiscreteACBasedParams, DiscreteACBasedTrainer +from maro.rl.training.algorithms.base import ACBasedParams, ACBasedTrainer @dataclass -class DiscreteActorCriticParams(DiscreteACBasedParams): - """Identical to `DiscreteACBasedParams`. Please refer to the doc string of `DiscreteACBasedParams` +class ActorCriticParams(ACBasedParams): + """Identical to `ACBasedParams`. Please refer to the doc string of `ACBasedParams` for detailed information. """ def extract_ops_params(self) -> Dict[str, object]: @@ -19,17 +19,18 @@ def extract_ops_params(self) -> Dict[str, object]: "critic_loss_cls": self.critic_loss_cls, "lam": self.lam, "min_logp": self.min_logp, + "is_discrete_action": self.is_discrete_action, } def __post_init__(self) -> None: assert self.get_v_critic_net_func is not None -class DiscreteActorCriticTrainer(DiscreteACBasedTrainer): - """Actor Critic algorithm with separate policy and value models. +class ActorCriticTrainer(ACBasedTrainer): + """Actor-Critic algorithm with separate policy and value models. Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/vpg """ - def __init__(self, name: str, params: DiscreteActorCriticParams) -> None: - super(DiscreteActorCriticTrainer, self).__init__(name, params) + def __init__(self, name: str, params: ActorCriticParams) -> None: + super(ActorCriticTrainer, self).__init__(name, params) diff --git a/maro/rl/training/algorithms/base/__init__.py b/maro/rl/training/algorithms/base/__init__.py index 1d555f10a..857601d2a 100644 --- a/maro/rl/training/algorithms/base/__init__.py +++ b/maro/rl/training/algorithms/base/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .ac_ppo_base import DiscreteACBasedOps, DiscreteACBasedParams, DiscreteACBasedTrainer +from .ac_ppo_base import ACBasedOps, ACBasedParams, ACBasedTrainer -__all__ = ["DiscreteACBasedOps", "DiscreteACBasedParams", "DiscreteACBasedTrainer"] +__all__ = ["ACBasedOps", "ACBasedParams", "ACBasedTrainer"] diff --git a/maro/rl/training/algorithms/base/ac_ppo_base.py b/maro/rl/training/algorithms/base/ac_ppo_base.py index dc757140c..f5d7a24c6 100644 --- a/maro/rl/training/algorithms/base/ac_ppo_base.py +++ b/maro/rl/training/algorithms/base/ac_ppo_base.py @@ -1,27 +1,23 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import collections from abc import ABCMeta from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional +from typing import Callable, Dict, Optional, Tuple import numpy as np import torch from maro.rl.model import VNet -from maro.rl.policy import DiscretePolicyGradient, RLPolicy -from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, FIFOReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote -from maro.rl.utils import ( - TransitionBatch, discount_cumsum, get_torch_device, merge_transition_batches, ndarray_to_tensor -) +from maro.rl.policy import ContinuousRLPolicy, DiscretePolicyGradient, RLPolicy +from maro.rl.training import AbsTrainOps, FIFOReplayMemory, remote, RemoteOps, SingleAgentTrainer, TrainerParams +from maro.rl.utils import (discount_cumsum, get_torch_device, ndarray_to_tensor, TransitionBatch) @dataclass -class DiscreteACBasedParams(TrainerParams, metaclass=ABCMeta): +class ACBasedParams(TrainerParams, metaclass=ABCMeta): """ - Parameter bundle for discrete actor-critic based algorithms (discrete actor-critic & discrete PPO) + Parameter bundle for Actor-Critic based algorithms (Actor-Critic & PPO) get_v_critic_net_func (Callable[[], VNet]): Function to get V critic net. reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. @@ -31,6 +27,7 @@ class DiscreteACBasedParams(TrainerParams, metaclass=ABCMeta): min_logp (float, default=None): Lower bound for clamping logP values during learning. This is to prevent logP from becoming very large in magnitude and causing stability issues. If it is None, it means no lower bound. + is_discrete_action (bool, default=True): Indicator of continuous or discrete action policy. """ get_v_critic_net_func: Callable[[], VNet] = None reward_discount: float = 0.9 @@ -38,10 +35,11 @@ class DiscreteACBasedParams(TrainerParams, metaclass=ABCMeta): critic_loss_cls: Callable = None lam: float = 0.9 min_logp: Optional[float] = None + is_discrete_action: bool = True -class DiscreteACBasedOps(AbsTrainOps): - """Base class of discrete actor-critic algorithm implementation. Reference: https://tinyurl.com/2ezte4cr +class ACBasedOps(AbsTrainOps): + """Base class of Actor-Critic algorithm implementation. Reference: https://tinyurl.com/2ezte4cr """ def __init__( self, @@ -49,20 +47,20 @@ def __init__( policy_creator: Callable[[str], RLPolicy], get_v_critic_net_func: Callable[[], VNet], parallelism: int = 1, - *, reward_discount: float = 0.9, critic_loss_cls: Callable = None, clip_ratio: float = None, lam: float = 0.9, min_logp: float = None, + is_discrete_action: bool = True, ) -> None: - super(DiscreteACBasedOps, self).__init__( + super(ACBasedOps, self).__init__( name=name, policy_creator=policy_creator, parallelism=parallelism, ) - assert isinstance(self._policy, DiscretePolicyGradient) + assert isinstance(self._policy, DiscretePolicyGradient) or isinstance(self._policy, ContinuousRLPolicy) self._reward_discount = reward_discount self._critic_loss_func = critic_loss_cls() if critic_loss_cls is not None else torch.nn.MSELoss() @@ -70,43 +68,42 @@ def __init__( self._lam = lam self._min_logp = min_logp self._v_critic_net = get_v_critic_net_func() + self._is_discrete_action = is_discrete_action self._device = None - def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: - """Compute the critic loss of the batch. + def pre_set_batch(self, batch: TransitionBatch) -> None: + self._states = ndarray_to_tensor(batch.states, device=self._device) + self._returns = ndarray_to_tensor(batch.returns, device=self._device) + self._actions = ndarray_to_tensor(batch.actions, device=self._device) + self._advantages = ndarray_to_tensor(batch.advantages, device=self._device) + self._logps_old = ndarray_to_tensor(batch.old_logps, device=self._device) + if self._is_discrete_action: + self._actions = self._actions.long() - Args: - batch (TransitionBatch): Batch. + def _get_critic_loss(self) -> torch.Tensor: + """Compute the critic loss of the batch. Returns: loss (torch.Tensor): The critic loss of the batch. """ self._v_critic_net.train() - states = ndarray_to_tensor(batch.states, device=self._device) # s - state_values = self._v_critic_net.v_values(states) - returns = ndarray_to_tensor(batch.returns, device=self._device) - return self._critic_loss_func(state_values, returns) + state_values = self._v_critic_net.v_values(self._states) + return self._critic_loss_func(state_values, self._returns) @remote - def get_critic_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + def get_critic_grad(self) -> Dict[str, torch.Tensor]: """Compute the critic network's gradients of a batch. - Args: - batch (TransitionBatch): Batch. - Returns: grad (torch.Tensor): The critic gradient of the batch. """ - return self._v_critic_net.get_gradients(self._get_critic_loss(batch)) + return self._v_critic_net.get_gradients(self._get_critic_loss()) - def update_critic(self, batch: TransitionBatch) -> None: + def update_critic(self) -> None: """Update the critic network using a batch. - - Args: - batch (TransitionBatch): Batch. """ - self._v_critic_net.step(self._get_critic_loss(batch)) + self._v_critic_net.step(self._get_critic_loss()) def update_critic_with_grad(self, grad_dict: dict) -> None: """Update the critic network with remotely computed gradients. @@ -117,54 +114,49 @@ def update_critic_with_grad(self, grad_dict: dict) -> None: self._v_critic_net.train() self._v_critic_net.apply_gradients(grad_dict) - def _get_actor_loss(self, batch: TransitionBatch) -> torch.Tensor: + def _get_actor_loss(self) -> Tuple[torch.Tensor, bool]: """Compute the actor loss of the batch. - Args: - batch (TransitionBatch): Batch. - Returns: loss (torch.Tensor): The actor loss of the batch. + early_stop (bool): Early stop indicator. """ - assert isinstance(self._policy, DiscretePolicyGradient) + assert isinstance(self._policy, DiscretePolicyGradient) or isinstance(self._policy, ContinuousRLPolicy) self._policy.train() - states = ndarray_to_tensor(batch.states, device=self._device) # s - actions = ndarray_to_tensor(batch.actions, device=self._device).long() # a - advantages = ndarray_to_tensor(batch.advantages, device=self._device) - - action_probs = self._policy.get_action_probs(states) - logps = torch.log(action_probs.gather(1, actions).squeeze()) - logps = torch.clamp(logps, min=self._min_logp, max=.0) + logps = self._policy.get_states_actions_logps(self._states, self._actions) if self._clip_ratio is not None: - logps_old = ndarray_to_tensor(batch.old_logps, device=self._device) - ratio = torch.exp(logps - logps_old) + ratio = torch.exp(logps - self._logps_old) + kl = (self._logps_old - logps).mean().item() + early_stop = (kl >= 0.01 * 1.5) # TODO clipped_ratio = torch.clamp(ratio, 1 - self._clip_ratio, 1 + self._clip_ratio) - actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).mean() + actor_loss = -(torch.min(ratio * self._advantages, clipped_ratio * self._advantages)).mean() else: - actor_loss = -(logps * advantages).mean() # I * delta * log pi(a|s) + actor_loss = -(logps * self._advantages).mean() # I * delta * log pi(a|s) + early_stop = False - return actor_loss + return actor_loss, early_stop @remote - def get_actor_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + def get_actor_grad(self) -> Tuple[Dict[str, torch.Tensor], bool]: """Compute the actor network's gradients of a batch. - Args: - batch (TransitionBatch): Batch. - Returns: grad (torch.Tensor): The actor gradient of the batch. + early_stop (bool): Early stop indicator. """ - return self._policy.get_gradients(self._get_actor_loss(batch)) + loss, early_stop = self._get_actor_loss() + return self._policy.get_gradients(loss), early_stop - def update_actor(self, batch: TransitionBatch) -> None: + def update_actor(self) -> bool: """Update the actor network using a batch. - Args: - batch (TransitionBatch): Batch. + Returns: + early_stop (bool): Early stop indicator. """ - self._policy.train_step(self._get_actor_loss(batch)) + loss, early_stop = self._get_actor_loss() + self._policy.train_step(loss) + return early_stop def update_actor_with_grad(self, grad_dict: dict) -> None: """Update the actor network with remotely computed gradients. @@ -183,7 +175,7 @@ def get_non_policy_state(self) -> dict: def set_non_policy_state(self, state: dict) -> None: self._v_critic_net.set_state(state["critic"]) - def _preprocess_batch(self, batch: TransitionBatch) -> TransitionBatch: + def preprocess_batch(self, batch: TransitionBatch) -> TransitionBatch: """Preprocess the batch to get the returns & advantages. Args: @@ -193,35 +185,33 @@ def _preprocess_batch(self, batch: TransitionBatch) -> TransitionBatch: The updated batch. """ assert isinstance(batch, TransitionBatch) - # Preprocess returns - batch.calc_returns(self._reward_discount) # Preprocess advantages states = ndarray_to_tensor(batch.states, device=self._device) # s - actions = ndarray_to_tensor(batch.actions, device=self._device).long() # a - - values = self._v_critic_net.v_values(states).detach().cpu().numpy() - values = np.concatenate([values, values[-1:]]) - rewards = np.concatenate([batch.rewards, values[-1:]]) - deltas = rewards[:-1] + self._reward_discount * values[1:] - values[:-1] # r + gamma * v(s') - v(s) - advantages = discount_cumsum(deltas, self._reward_discount * self._lam) - batch.advantages = advantages - - if self._clip_ratio is not None: - batch.old_logps = self._policy.get_state_action_logps(states, actions).detach().cpu().numpy() + actions = ndarray_to_tensor(batch.actions, device=self._device) # a + if self._is_discrete_action: + actions = actions.long() + + with torch.no_grad(): + self._v_critic_net.eval() + self._policy.eval() + values = self._v_critic_net.v_values(states).detach().cpu().numpy() + values = np.concatenate([values, np.zeros(1)]) + rewards = np.concatenate([batch.rewards, np.zeros(1)]) + deltas = rewards[:-1] + self._reward_discount * values[1:] - values[:-1] # r + gamma * v(s') - v(s) + batch.returns = discount_cumsum(rewards, self._reward_discount)[:-1] + batch.advantages = discount_cumsum(deltas, self._reward_discount * self._lam) + + if self._clip_ratio is not None: + batch.old_logps = self._policy.get_states_actions_logps(states, actions).detach().cpu().numpy() return batch - def preprocess_and_merge_batches(self, batch_list: List[TransitionBatch]) -> TransitionBatch: - """Preprocess and merge a list of transition batches to a single transition batch. - - Args: - batch_list (List[TransitionBatch]): List of batches. - - Returns: - The merged batch. - """ - return merge_transition_batches([self._preprocess_batch(batch) for batch in batch_list]) + def debug_get_v_values(self, batch: TransitionBatch) -> np.ndarray: + states = ndarray_to_tensor(batch.states, device=self._device) # s + with torch.no_grad(): + values = self._v_critic_net.v_values(states).detach().cpu().numpy() + return values def to_device(self, device: str = None) -> None: self._device = get_torch_device(device) @@ -229,44 +219,31 @@ def to_device(self, device: str = None) -> None: self._v_critic_net.to(self._device) -class DiscreteACBasedTrainer(SingleAgentTrainer): - """Base class of discrete actor-critic algorithm implementation. +class ACBasedTrainer(SingleAgentTrainer): + """Base class of Actor-Critic algorithm implementation. References: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f """ - def __init__(self, name: str, params: DiscreteACBasedParams) -> None: - super(DiscreteACBasedTrainer, self).__init__(name, params) + def __init__(self, name: str, params: ACBasedParams) -> None: + super(ACBasedTrainer, self).__init__(name, params) self._params = params - self._replay_memory_dict: Dict[Any, FIFOReplayMemory] = {} def build(self) -> None: self._ops = self.get_ops() - self._replay_memory_dict = collections.defaultdict(lambda: FIFOReplayMemory( + self._replay_memory = FIFOReplayMemory( capacity=self._params.replay_memory_capacity, state_dim=self._ops.policy_state_dim, action_dim=self._ops.policy_action_dim, - )) - - def record(self, env_idx: int, exp_element: ExpElement) -> None: - for agent_name in exp_element.agent_names: - memory = self._replay_memory_dict[(env_idx, agent_name)] - transition_batch = TransitionBatch( - states=np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0), - actions=np.expand_dims(exp_element.action_dict[agent_name], axis=0), - rewards=np.array([exp_element.reward_dict[agent_name]]), - terminals=np.array([exp_element.terminal_dict[agent_name]]), - next_states=np.expand_dims( - exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), - axis=0, - ), - ) - memory.put(transition_batch) + ) + + def _preprocess_batch(self, transition_batch: TransitionBatch) -> TransitionBatch: + return self._ops.preprocess_batch(transition_batch) def get_local_ops(self) -> AbsTrainOps: - return DiscreteACBasedOps( + return ACBasedOps( name=self._policy_name, policy_creator=self._policy_creator, parallelism=self._params.data_parallelism, @@ -274,19 +251,25 @@ def get_local_ops(self) -> AbsTrainOps: ) def _get_batch(self) -> TransitionBatch: - batch_list = [memory.sample(-1) for memory in self._replay_memory_dict.values()] - return self._ops.preprocess_and_merge_batches(batch_list) + batch = self._replay_memory.sample(-1) + batch.advantages = (batch.advantages - batch.advantages.mean()) / batch.advantages.std() + return batch def train_step(self) -> None: - assert isinstance(self._ops, DiscreteACBasedOps) - batch = self._get_batch() + assert isinstance(self._ops, ACBasedOps) + + self._ops.pre_set_batch(self._get_batch()) + for _ in range(self._params.grad_iters): + self._ops.update_critic() + for _ in range(self._params.grad_iters): - self._ops.update_critic(batch) - self._ops.update_actor(batch) + early_stop = self._ops.update_actor() + if early_stop: + break async def train_step_as_task(self) -> None: assert isinstance(self._ops, RemoteOps) - batch = self._get_batch() + self._ops.pre_set_batch(self._get_batch()) for _ in range(self._params.grad_iters): - self._ops.update_critic_with_grad(await self._ops.get_critic_grad(batch)) - self._ops.update_actor_with_grad(await self._ops.get_actor_grad(batch)) + self._ops.update_critic_with_grad(await self._ops.get_critic_grad()) + self._ops.update_actor_with_grad(await self._ops.get_actor_grad()) diff --git a/maro/rl/training/algorithms/ddpg.py b/maro/rl/training/algorithms/ddpg.py index 61c398c01..e96e194aa 100644 --- a/maro/rl/training/algorithms/ddpg.py +++ b/maro/rl/training/algorithms/ddpg.py @@ -1,19 +1,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -# TODO: DDPG has net been tested in a real test case - from dataclasses import dataclass -from typing import Callable, Dict, Optional +from typing import Callable, Dict -import numpy as np import torch from maro.rl.model import QNet from maro.rl.policy import ContinuousRLPolicy, RLPolicy -from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote -from maro.rl.utils import TransitionBatch, get_torch_device, ndarray_to_tensor +from maro.rl.training import AbsTrainOps, RandomReplayMemory, remote, RemoteOps, SingleAgentTrainer, TrainerParams +from maro.rl.utils import get_torch_device, ndarray_to_tensor, TransitionBatch from maro.utils import clone @@ -32,6 +28,7 @@ class DDPGParams(TrainerParams): random_overwrite (bool, default=False): This specifies overwrite behavior when the replay memory capacity is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. + min_num_to_trigger_training (int, default=0): Minimum number required to start training. """ get_q_critic_net_func: Callable[[], QNet] = None reward_discount: float = 0.9 @@ -40,6 +37,7 @@ class DDPGParams(TrainerParams): q_value_loss_cls: Callable = None soft_update_coef: float = 1.0 random_overwrite: bool = False + min_num_to_trigger_training: int = 0 def __post_init__(self) -> None: assert self.get_q_critic_net_func is not None @@ -50,7 +48,6 @@ def extract_ops_params(self) -> Dict[str, object]: "reward_discount": self.reward_discount, "q_value_loss_cls": self.q_value_loss_cls, "soft_update_coef": self.soft_update_coef, - "data_parallelism": self.data_parallelism, } @@ -63,9 +60,8 @@ def __init__( name: str, policy_creator: Callable[[], RLPolicy], get_q_critic_net_func: Callable[[], QNet], - parallelism: int = 1, - *, reward_discount: float, + parallelism: int = 1, q_value_loss_cls: Callable = None, soft_update_coef: float = 1.0, ) -> None: @@ -112,7 +108,7 @@ def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: ) # Q_targ(s', miu_targ(s')) # y(r, s', d) = r + gamma * (1 - d) * Q_targ(s', miu_targ(s')) - target_q_values = (rewards + self._reward_discount * (1 - terminals) * next_q_values).detach() + target_q_values = (rewards + self._reward_discount * (1 - terminals.long()) * next_q_values).detach() q_values = self._q_critic_net.q_values(states=states, actions=actions) # Q(s, a) return self._q_value_loss_func(q_values, target_q_values) # MSE(Q(s, a), y(r, s', d)) @@ -234,7 +230,7 @@ def __init__(self, name: str, params: DDPGParams) -> None: super(DDPGTrainer, self).__init__(name, params) self._params = params self._policy_version = self._target_policy_version = 0 - self._replay_memory: Optional[RandomReplayMemory] = None + self._memory_size = 0 def build(self) -> None: self._ops = self.get_ops() @@ -245,19 +241,8 @@ def build(self) -> None: random_overwrite=self._params.random_overwrite, ) - def record(self, env_idx: int, exp_element: ExpElement) -> None: - for agent_name in exp_element.agent_names: - transition_batch = TransitionBatch( - states=np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0), - actions=np.expand_dims(exp_element.action_dict[agent_name], axis=0), - rewards=np.array([exp_element.reward_dict[agent_name]]), - terminals=np.array([exp_element.terminal_dict[agent_name]]), - next_states=np.expand_dims( - exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), - axis=0, - ), - ) - self._replay_memory.put(transition_batch) + def _preprocess_batch(self, transition_batch: TransitionBatch) -> TransitionBatch: + return transition_batch def get_local_ops(self) -> AbsTrainOps: return DDPGOps( @@ -272,21 +257,37 @@ def _get_batch(self, batch_size: int = None) -> TransitionBatch: def train_step(self) -> None: assert isinstance(self._ops, DDPGOps) + + if self._replay_memory.n_sample < self._params.min_num_to_trigger_training: + print( + f"Skip this training step due to lack of experiences " + f"(current = {self._replay_memory.n_sample}, minimum = {self._params.min_num_to_trigger_training})" + ) + return + for _ in range(self._params.num_epochs): batch = self._get_batch() self._ops.update_critic(batch) self._ops.update_actor(batch) - self._try_soft_update_target() + self._try_soft_update_target() async def train_step_as_task(self) -> None: assert isinstance(self._ops, RemoteOps) + + if self._replay_memory.n_sample < self._params.min_num_to_trigger_training: + print( + f"Skip this training step due to lack of experiences " + f"(current = {self._replay_memory.n_sample}, minimum = {self._params.min_num_to_trigger_training})" + ) + return + for _ in range(self._params.num_epochs): batch = self._get_batch() self._ops.update_critic_with_grad(await self._ops.get_critic_grad(batch)) self._ops.update_actor_with_grad(await self._ops.get_actor_grad(batch)) - self._try_soft_update_target() + self._try_soft_update_target() def _try_soft_update_target(self) -> None: """Soft update the target policy and target critic. diff --git a/maro/rl/training/algorithms/dqn.py b/maro/rl/training/algorithms/dqn.py index 0389acf8b..209e09f5d 100644 --- a/maro/rl/training/algorithms/dqn.py +++ b/maro/rl/training/algorithms/dqn.py @@ -1,8 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. - +import collections from dataclasses import dataclass -from typing import Callable, Dict, Optional +from typing import Callable, Dict, List, Optional import numpy as np import torch @@ -50,7 +50,6 @@ def __init__( name: str, policy_creator: Callable[[str], RLPolicy], parallelism: int = 1, - *, reward_discount: float = 0.9, soft_update_coef: float = 0.1, double: bool = False, @@ -162,7 +161,6 @@ def __init__(self, name: str, params: DQNParams) -> None: super(DQNTrainer, self).__init__(name, params) self._params = params self._q_net_version = self._target_q_net_version = 0 - self._replay_memory: Optional[RandomReplayMemory] = None def build(self) -> None: self._ops = self.get_ops() @@ -173,19 +171,8 @@ def build(self) -> None: random_overwrite=self._params.random_overwrite, ) - def record(self, env_idx: int, exp_element: ExpElement) -> None: - for agent_name in exp_element.agent_names: - transition_batch = TransitionBatch( - states=np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0), - actions=np.expand_dims(exp_element.action_dict[agent_name], axis=0), - rewards=np.array([exp_element.reward_dict[agent_name]]), - terminals=np.array([exp_element.terminal_dict[agent_name]]), - next_states=np.expand_dims( - exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), - axis=0, - ), - ) - self._replay_memory.put(transition_batch) + def _preprocess_batch(self, transition_batch: TransitionBatch) -> TransitionBatch: + return transition_batch def get_local_ops(self) -> AbsTrainOps: return DQNOps( diff --git a/maro/rl/training/algorithms/maddpg.py b/maro/rl/training/algorithms/maddpg.py index 3bc09dd99..042d8fb84 100644 --- a/maro/rl/training/algorithms/maddpg.py +++ b/maro/rl/training/algorithms/maddpg.py @@ -60,7 +60,6 @@ def __init__( get_q_critic_net_func: Callable[[], MultiQNet], policy_idx: int, parallelism: int = 1, - *, shared_critic: bool = False, reward_discount: float = 0.9, soft_update_coef: float = 0.5, @@ -122,7 +121,7 @@ def get_latest_action(self, batch: MultiTransitionBatch) -> Tuple[torch.Tensor, agent_state = ndarray_to_tensor(batch.agent_states[self._policy_idx], device=self._device) self._policy.train() action = self._policy.get_actions_tensor(agent_state) - logps = self._policy.get_state_action_logps(agent_state, action) + logps = self._policy.get_states_actions_logps(agent_state, action) return action, logps def _get_critic_loss(self, batch: MultiTransitionBatch, next_actions: List[torch.Tensor]) -> torch.Tensor: @@ -330,36 +329,44 @@ def build(self) -> None: assert len(self._agent2policy.keys()) == len(self._agent2policy.values()) # agent <=> policy self._policy2agent = {policy_name: agent_name for agent_name, policy_name in self._agent2policy.items()} - def record(self, env_idx: int, exp_element: ExpElement) -> None: - assert exp_element.num_agents == len(self._agent2policy.keys()) + def record_multiple(self, env_idx: int, exp_elements: List[ExpElement]) -> None: + terminal_flags: List[bool] = [] + for exp_element in exp_elements: + assert exp_element.num_agents == len(self._agent2policy.keys()) - if min(exp_element.terminal_dict.values()) != max(exp_element.terminal_dict.values()): - raise ValueError("The 'terminal` flag of all agents must be identical.") - terminal_flag = min(exp_element.terminal_dict.values()) + if min(exp_element.terminal_dict.values()) != max(exp_element.terminal_dict.values()): + raise ValueError("The 'terminal` flag of all agents at every tick must be identical.") + terminal_flags.append(min(exp_element.terminal_dict.values())) - actions = [] - rewards = [] - agent_states = [] - next_agent_states = [] + actions: List[np.ndarray] = [] + rewards: List[np.ndarray] = [] + agent_states: List[np.ndarray] = [] + next_agent_states: List[np.ndarray] = [] for policy_name in self._policy_names: agent_name = self._policy2agent[policy_name] - actions.append(np.expand_dims(exp_element.action_dict[agent_name], axis=0)) - rewards.append(np.array([exp_element.reward_dict[agent_name]])) - agent_states.append(np.expand_dims(exp_element.agent_state_dict[agent_name], axis=0)) - next_agent_states.append(np.expand_dims( - exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), axis=0 + actions.append(np.vstack([exp_element.action_dict[agent_name] for exp_element in exp_elements])) + rewards.append(np.array([exp_element.reward_dict[agent_name] for exp_element in exp_elements])) + agent_states.append(np.vstack([exp_element.agent_state_dict[agent_name] for exp_element in exp_elements])) + next_agent_states.append(np.vstack( + [ + exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]) + for exp_element in exp_elements + ] )) transition_batch = MultiTransitionBatch( - states=np.expand_dims(exp_element.state, axis=0), + states=np.vstack([exp_element.state for exp_element in exp_elements]), actions=actions, rewards=rewards, - next_states=np.expand_dims( - exp_element.next_state if exp_element.next_state is not None else exp_element.state, axis=0 + next_states=np.vstack( + [ + exp_element.next_state if exp_element.next_state is not None else exp_element.state + for exp_element in exp_elements + ] ), agent_states=agent_states, next_agent_states=next_agent_states, - terminals=np.array([terminal_flag]), + terminals=np.array(terminal_flags), ) self._replay_memory.put(transition_batch) diff --git a/maro/rl/training/algorithms/ppo.py b/maro/rl/training/algorithms/ppo.py index 4ed4ea1d7..92c5879c0 100644 --- a/maro/rl/training/algorithms/ppo.py +++ b/maro/rl/training/algorithms/ppo.py @@ -4,12 +4,12 @@ from dataclasses import dataclass from typing import Dict -from maro.rl.training.algorithms.base import DiscreteACBasedParams, DiscreteACBasedTrainer +from maro.rl.training.algorithms.base import ACBasedParams, ACBasedTrainer @dataclass -class DiscretePPOParams(DiscreteACBasedParams): - """Mostly inherited from `DiscreteACBasedParams`. Please refer to the doc string of `DiscreteACBasedParams` +class PPOParams(ACBasedParams): + """Mostly inherited from `ACBasedParams`. Please refer to the doc string of `ACBasedParams` for more detailed information. clip_ratio (float, default=None): Clip ratio in the PPO algorithm (https://arxiv.org/pdf/1707.06347.pdf). @@ -25,6 +25,7 @@ def extract_ops_params(self) -> Dict[str, object]: "clip_ratio": self.clip_ratio, "lam": self.lam, "min_logp": self.min_logp, + "is_discrete_action": self.is_discrete_action, } def __post_init__(self) -> None: @@ -32,11 +33,11 @@ def __post_init__(self) -> None: assert self.clip_ratio is not None -class DiscretePPOTrainer(DiscreteACBasedTrainer): - """Discrete PPO algorithm. +class PPOTrainer(ACBasedTrainer): + """PPO algorithm. References: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ppo. """ - def __init__(self, name: str, params: DiscretePPOParams) -> None: - super(DiscretePPOTrainer, self).__init__(name, params) + def __init__(self, name: str, params: PPOParams) -> None: + super(PPOTrainer, self).__init__(name, params) diff --git a/maro/rl/training/algorithms/sac.py b/maro/rl/training/algorithms/sac.py new file mode 100644 index 000000000..6b812fdca --- /dev/null +++ b/maro/rl/training/algorithms/sac.py @@ -0,0 +1,234 @@ +from dataclasses import dataclass +from typing import Callable, Dict, Optional, Tuple + +import torch + +from maro.rl.model import QNet +from maro.rl.policy import ContinuousRLPolicy, RLPolicy +from maro.rl.training import AbsTrainOps, RandomReplayMemory, remote, RemoteOps, SingleAgentTrainer, TrainerParams +from maro.rl.utils import get_torch_device, ndarray_to_tensor, TransitionBatch +from maro.utils import clone + + +@dataclass +class SoftActorCriticParams(TrainerParams): + get_q_critic_net_func: Callable[[], QNet] = None + update_target_every: int = 5 + random_overwrite: bool = False + entropy_coef: float = 0.1 + num_epochs: int = 1 + n_start_train: int = 0 + reward_discount: float = 0.9 + q_value_loss_cls: Callable = None + soft_update_coef: float = 1.0 + + def __post_init__(self) -> None: + assert self.get_q_critic_net_func is not None + + def extract_ops_params(self) -> Dict[str, object]: + return { + "get_q_critic_net_func": self.get_q_critic_net_func, + "entropy_coef": self.entropy_coef, + "reward_discount": self.reward_discount, + "q_value_loss_cls": self.q_value_loss_cls, + "soft_update_coef": self.soft_update_coef, + } + + +class SoftActorCriticOps(AbsTrainOps): + def __init__( + self, + name: str, + policy_creator: Callable[[], RLPolicy], + get_q_critic_net_func: Callable[[], QNet], + parallelism: int = 1, + *, + entropy_coef: float, + reward_discount: float, + q_value_loss_cls: Callable = None, + soft_update_coef: float = 1.0, + ) -> None: + super(SoftActorCriticOps, self).__init__( + name=name, + policy_creator=policy_creator, + parallelism=parallelism, + ) + + assert isinstance(self._policy, ContinuousRLPolicy) + + self._q_net1 = get_q_critic_net_func() + self._q_net2 = get_q_critic_net_func() + self._target_q_net1: QNet = clone(self._q_net1) + self._target_q_net1.eval() + self._target_q_net2: QNet = clone(self._q_net2) + self._target_q_net2.eval() + + self._entropy_coef = entropy_coef + self._soft_update_coef = soft_update_coef + self._reward_discount = reward_discount + self._q_value_loss_func = q_value_loss_cls() if q_value_loss_cls is not None else torch.nn.MSELoss() + + def _get_critic_loss(self, batch: TransitionBatch) -> Tuple[torch.Tensor, torch.Tensor]: + self._q_net1.train() + states = ndarray_to_tensor(batch.states, device=self._device) # s + next_states = ndarray_to_tensor(batch.next_states, device=self._device) # s' + actions = ndarray_to_tensor(batch.actions, device=self._device) # a + rewards = ndarray_to_tensor(batch.rewards, device=self._device) # r + terminals = ndarray_to_tensor(batch.terminals, device=self._device) # d + + assert isinstance(self._policy, ContinuousRLPolicy) + + with torch.no_grad(): + next_actions, next_logps = self._policy.get_actions_with_logps(states) + q1 = self._target_q_net1.q_values(next_states, next_actions) + q2 = self._target_q_net2.q_values(next_states, next_actions) + q = torch.min(q1, q2) + y = rewards + self._reward_discount * (1.0 - terminals.float()) * (q - self._entropy_coef * next_logps) + + q1 = self._q_net1.q_values(states, actions) + q2 = self._q_net2.q_values(states, actions) + loss_q1 = self._q_value_loss_func(q1, y) + loss_q2 = self._q_value_loss_func(q2, y) + return loss_q1, loss_q2 + + @remote + def get_critic_grad(self, batch: TransitionBatch) -> Tuple[Dict[str, torch.Tensor], Dict[str, torch.Tensor]]: + loss_q1, loss_q2 = self._get_critic_loss(batch) + grad_q1 = self._q_net1.get_gradients(loss_q1) + grad_q2 = self._q_net2.get_gradients(loss_q2) + return grad_q1, grad_q2 + + def update_critic_with_grad(self, grad_dict1: dict, grad_dict2: dict) -> None: + self._q_net1.train() + self._q_net2.train() + self._q_net1.apply_gradients(grad_dict1) + self._q_net2.apply_gradients(grad_dict2) + + def update_critic(self, batch: TransitionBatch) -> None: + self._q_net1.train() + self._q_net2.train() + loss_q1, loss_q2 = self._get_critic_loss(batch) + self._q_net1.step(loss_q1) + self._q_net2.step(loss_q2) + + def _get_actor_loss(self, batch: TransitionBatch) -> torch.Tensor: + self._policy.train() + states = ndarray_to_tensor(batch.states, device=self._device) # s + actions, logps = self._policy.get_actions_with_logps(states) + q1 = self._q_net1.q_values(states, actions) + q2 = self._q_net2.q_values(states, actions) + q = torch.min(q1, q2) + + loss = (self._entropy_coef * logps - q).mean() + return loss + + @remote + def get_actor_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: + return self._policy.get_gradients(self._get_actor_loss(batch)) + + def update_actor_with_grad(self, grad_dict: dict) -> None: + self._policy.train() + self._policy.apply_gradients(grad_dict) + + def update_actor(self, batch: TransitionBatch) -> None: + self._policy.train() + self._policy.train_step(self._get_actor_loss(batch)) + + def get_non_policy_state(self) -> dict: + return { + "q_net1": self._q_net1.get_state(), + "q_net2": self._q_net2.get_state(), + "target_q_net1": self._target_q_net1.get_state(), + "target_q_net2": self._target_q_net2.get_state(), + } + + def set_non_policy_state(self, state: dict) -> None: + self._q_net1.set_state(state["q_net1"]) + self._q_net2.set_state(state["q_net2"]) + self._target_q_net1.set_state(state["target_q_net1"]) + self._target_q_net2.set_state(state["target_q_net2"]) + + def soft_update_target(self) -> None: + self._target_q_net1.soft_update(self._q_net1, self._soft_update_coef) + self._target_q_net2.soft_update(self._q_net2, self._soft_update_coef) + + def to_device(self, device: str) -> None: + self._device = get_torch_device(device=device) + self._q_net1.to(self._device) + self._q_net2.to(self._device) + self._target_q_net1.to(self._device) + self._target_q_net2.to(self._device) + + +class SoftActorCriticTrainer(SingleAgentTrainer): + def __init__(self, name: str, params: SoftActorCriticParams) -> None: + super(SoftActorCriticTrainer, self).__init__(name, params) + self._params = params + self._qnet_version = self._target_qnet_version = 0 + + self._replay_memory: Optional[RandomReplayMemory] = None + + def build(self) -> None: + self._ops = self.get_ops() + self._replay_memory = RandomReplayMemory( + capacity=self._params.replay_memory_capacity, + state_dim=self._ops.policy_state_dim, + action_dim=self._ops.policy_action_dim, + random_overwrite=self._params.random_overwrite, + ) + + def train_step(self) -> None: + assert isinstance(self._ops, SoftActorCriticOps) + + if self._replay_memory.n_sample < self._params.n_start_train: + print( + f"Skip this training step due to lack of experiences " + f"(current = {self._replay_memory.n_sample}, minimum = {self._params.n_start_train})" + ) + return + + for _ in range(self._params.num_epochs): + batch = self._get_batch() + self._ops.update_critic(batch) + self._ops.update_actor(batch) + + self._try_soft_update_target() + + async def train_step_as_task(self) -> None: + assert isinstance(self._ops, RemoteOps) + + if self._replay_memory.n_sample < self._params.n_start_train: + print( + f"Skip this training step due to lack of experiences " + f"(current = {self._replay_memory.n_sample}, minimum = {self._params.n_start_train})" + ) + return + + for _ in range(self._params.num_epochs): + batch = self._get_batch() + self._ops.update_critic_with_grad(await self._ops.get_critic_grad(batch)) + self._ops.update_actor_with_grad(await self._ops.get_actor_grad(batch)) + + self._try_soft_update_target() + + def _preprocess_batch(self, transition_batch: TransitionBatch) -> TransitionBatch: + return transition_batch + + def get_local_ops(self) -> SoftActorCriticOps: + return SoftActorCriticOps( + name=self._policy_name, + policy_creator=self._policy_creator, + parallelism=self._params.data_parallelism, + **self._params.extract_ops_params(), + ) + + def _get_batch(self, batch_size: int = None) -> TransitionBatch: + return self._replay_memory.sample(batch_size if batch_size is not None else self._batch_size) + + def _try_soft_update_target(self) -> None: + """Soft update the target policy and target critic. + """ + self._qnet_version += 1 + if self._qnet_version - self._target_qnet_version == self._params.update_target_every: + self._ops.soft_update_target() + self._target_qnet_version = self._qnet_version diff --git a/maro/rl/training/replay_memory.py b/maro/rl/training/replay_memory.py index c0cdb3e48..e797834ad 100644 --- a/maro/rl/training/replay_memory.py +++ b/maro/rl/training/replay_memory.py @@ -200,11 +200,20 @@ def __init__( self._rewards = np.zeros(self._capacity, dtype=np.float32) self._terminals = np.zeros(self._capacity, dtype=np.bool) self._next_states = np.zeros((self._capacity, self._state_dim), dtype=np.float32) + self._returns = np.zeros(self._capacity, dtype=np.float32) + self._advantages = np.zeros(self._capacity, dtype=np.float32) + self._old_logps = np.zeros(self._capacity, dtype=np.float32) + + self._n_sample = 0 @property def action_dim(self) -> int: return self._action_dim + @property + def n_sample(self) -> int: + return self._n_sample + def put(self, transition_batch: TransitionBatch) -> None: """Store a transition batch in the memory. @@ -219,8 +228,15 @@ def put(self, transition_batch: TransitionBatch) -> None: assert match_shape(transition_batch.rewards, (batch_size,)) assert match_shape(transition_batch.terminals, (batch_size,)) assert match_shape(transition_batch.next_states, (batch_size, self._state_dim)) + if transition_batch.returns is not None: + match_shape(transition_batch.returns, (batch_size,)) + if transition_batch.advantages is not None: + match_shape(transition_batch.advantages, (batch_size,)) + if transition_batch.old_logps is not None: + match_shape(transition_batch.old_logps, (batch_size,)) self._put_by_indexes(self._get_put_indexes(batch_size), transition_batch) + self._n_sample = min(self._n_sample + transition_batch.size, self._capacity) def _put_by_indexes(self, indexes: np.ndarray, transition_batch: TransitionBatch) -> None: """Store a transition batch into the memory at the give indexes. @@ -234,6 +250,12 @@ def _put_by_indexes(self, indexes: np.ndarray, transition_batch: TransitionBatch self._rewards[indexes] = transition_batch.rewards self._terminals[indexes] = transition_batch.terminals self._next_states[indexes] = transition_batch.next_states + if transition_batch.returns is not None: + self._returns[indexes] = transition_batch.returns + if transition_batch.advantages is not None: + self._advantages[indexes] = transition_batch.advantages + if transition_batch.old_logps is not None: + self._old_logps[indexes] = transition_batch.old_logps def sample(self, batch_size: int = None) -> TransitionBatch: """Generate a sample batch from the replay memory. @@ -265,6 +287,9 @@ def sample_by_indexes(self, indexes: np.ndarray) -> TransitionBatch: rewards=self._rewards[indexes], terminals=self._terminals[indexes], next_states=self._next_states[indexes], + returns=self._returns[indexes], + advantages=self._advantages[indexes], + old_logps=self._old_logps[indexes], ) @abstractmethod diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index 8e3580a15..4f88abbc9 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -1,10 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import collections import os from abc import ABCMeta, abstractmethod from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional, Tuple, Union +import numpy as np import torch from maro.rl.policy import AbsPolicy, RLPolicy @@ -12,8 +14,10 @@ from maro.rl.utils.objects import FILE_SUFFIX from maro.utils import LoggerV2 +from .replay_memory import ReplayMemory from .train_ops import AbsTrainOps, RemoteOps from .utils import extract_trainer_name +from ..utils import TransitionBatch @dataclass @@ -124,13 +128,13 @@ async def train_step_as_task(self) -> None: raise NotImplementedError @abstractmethod - def record(self, env_idx: int, exp_element: ExpElement) -> None: - """Record rollout experiences in the replay memory. + def record_multiple(self, env_idx: int, exp_elements: List[ExpElement]) -> None: + """Record rollout all experiences from an environment in the replay memory. Args: env_idx (int): The index of the environment that generates this batch of experiences. This is used when there are more than one environment collecting experiences in parallel. - exp_element (ExpElement): Experiences. + exp_elements (List[ExpElement]): Experiences. """ raise NotImplementedError @@ -168,6 +172,7 @@ def __init__(self, name: str, params: TrainerParams) -> None: self._policy_name: Optional[str] = None self._policy_creator: Optional[Callable[[str], RLPolicy]] = None self._ops: Optional[AbsTrainOps] = None + self._replay_memory: Optional[ReplayMemory] = None @property def ops(self): @@ -232,6 +237,33 @@ def save(self, path: str) -> None: torch.save(policy_state, os.path.join(path, f"{self.name}_policy.{FILE_SUFFIX}")) torch.save(non_policy_state, os.path.join(path, f"{self.name}_non_policy.{FILE_SUFFIX}")) + def record_multiple(self, env_idx: int, exp_elements: List[ExpElement]) -> None: + agent_exp_pool = collections.defaultdict(list) + for exp_element in exp_elements: + for agent_name in exp_element.agent_names: + agent_exp_pool[agent_name].append(( + exp_element.agent_state_dict[agent_name], + exp_element.action_dict[agent_name], + exp_element.reward_dict[agent_name], + exp_element.terminal_dict[agent_name], + exp_element.next_agent_state_dict.get(agent_name, exp_element.agent_state_dict[agent_name]), + )) + + for agent_name, exps in agent_exp_pool.items(): + transition_batch = TransitionBatch( + states=np.vstack([exp[0] for exp in exps]), + actions=np.vstack([exp[1] for exp in exps]), + rewards=np.array([exp[2] for exp in exps]), + terminals=np.array([exp[3] for exp in exps]), + next_states=np.vstack([exp[4] for exp in exps]), + ) + transition_batch = self._preprocess_batch(transition_batch) + self._replay_memory.put(transition_batch) + + @abstractmethod + def _preprocess_batch(self, transition_batch: TransitionBatch) -> TransitionBatch: + raise NotImplementedError + def _assert_ops_exists(self) -> None: if not self._ops: raise ValueError("'build' needs to be called to create an ops instance first.") diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/training_manager.py index 1d5ae39cb..b6d78d220 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/training_manager.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import asyncio +import collections import os from itertools import chain from typing import Any, Callable, Dict, Iterable, List, Tuple @@ -102,11 +103,15 @@ def record_experiences(self, experiences: List[List[ExpElement]]) -> None: tick. Please refers to the definition of ExpElement for detailed explanation of ExpElement. """ for env_idx, env_experience in enumerate(experiences): + trainer_exp_pool = collections.defaultdict(list) for exp_element in env_experience: # Dispatch experiences to trainers tick by tick. exp_dict = exp_element.split_contents(self._agent2trainer) for trainer_name, exp_elem in exp_dict.items(): - trainer = self._trainer_dict[trainer_name] - trainer.record(env_idx, exp_elem) + trainer_exp_pool[trainer_name].append(exp_elem) + + for trainer_name, exp_elems in trainer_exp_pool.items(): + trainer = self._trainer_dict[trainer_name] + trainer.record_multiple(env_idx, exp_elems) def load(self, path: str) -> List[str]: loaded = [] diff --git a/maro/rl/utils/transition_batch.py b/maro/rl/utils/transition_batch.py index 023668e42..26432d452 100644 --- a/maro/rl/utils/transition_batch.py +++ b/maro/rl/utils/transition_batch.py @@ -32,9 +32,6 @@ def __post_init__(self) -> None: assert self.next_states.shape == self.states.shape assert len(self.terminals.shape) == 1 and self.terminals.shape[0] == self.states.shape[0] - def calc_returns(self, discount_factor: float) -> None: - self.returns = discount_cumsum(self.rewards, discount_factor) - def make_kth_sub_batch(self, i: int, k: int) -> TransitionBatch: return TransitionBatch( states=self.states[i::k], diff --git a/maro/rl/workflows/config/parser.py b/maro/rl/workflows/config/parser.py index 98dc88005..47666e294 100644 --- a/maro/rl/workflows/config/parser.py +++ b/maro/rl/workflows/config/parser.py @@ -257,10 +257,12 @@ def as_env(self, containerize: bool = False) -> dict: path_mapping = self.get_path_mapping(containerize=containerize) scenario_path = path_mapping[self._config["scenario_path"]] num_episodes = self._config["main"]["num_episodes"] + min_n_sample = self._config["main"].get("min_n_sample", 1) env = { "main": { "JOB": self._config["job"], "NUM_EPISODES": str(num_episodes), + "MIN_N_SAMPLE": str(min_n_sample), "TRAIN_MODE": self._config["training"]["mode"], "SCENARIO_PATH": scenario_path, } diff --git a/maro/rl/workflows/config/template.yml b/maro/rl/workflows/config/template.yml index 330fee2b9..3464e9edc 100644 --- a/maro/rl/workflows/config/template.yml +++ b/maro/rl/workflows/config/template.yml @@ -24,6 +24,9 @@ main: # A list indicates the episodes at the end of which policies are to be evaluated. Note that episode indexes are # 1-based. eval_schedule: 10 + # Minimum number of samples to start training in one epoch. The workflow will re-run experience collection + # until we have at least `min_n_sample` of experiences. + min_n_sample: 1 logging: # log levels for the main loop stdout: INFO # DEBUG, INFO, WARN, ERROR, CRITICAL, PROGRESS file: DEBUG # DEBUG, INFO, WARN, ERROR, CRITICAL, PROGRESS diff --git a/maro/rl/workflows/main.py b/maro/rl/workflows/main.py index 099885554..2eeeaa973 100644 --- a/maro/rl/workflows/main.py +++ b/maro/rl/workflows/main.py @@ -30,6 +30,7 @@ def main(scenario: Scenario, args: argparse.Namespace) -> None: def training_workflow(scenario: Scenario) -> None: num_episodes = int(get_env("NUM_EPISODES")) num_steps = int_or_none(get_env("NUM_STEPS", required=False)) + min_n_sample = int_or_none(get_env("MIN_N_SAMPLE")) logger = LoggerV2( "MAIN", @@ -113,36 +114,41 @@ def training_workflow(scenario: Scenario) -> None: checkpoint_path = get_env("CHECKPOINT_PATH", required=False) checkpoint_interval = int_or_none(get_env("CHECKPOINT_INTERVAL", required=False)) + # main loop for ep in range(start_ep, num_episodes + 1): collect_time = training_time = 0 - segment, end_of_episode = 1, False - while not end_of_episode: - # Experience collection + total_experiences: List[List[ExpElement]] = [] + total_info_list: List[dict] = [] + n_sample = 0 + while n_sample < min_n_sample: tc0 = time.time() result = env_sampler.sample( policy_state=training_manager.get_policy_state() if not is_single_thread else None, num_steps=num_steps, ) experiences: List[List[ExpElement]] = result["experiences"] - end_of_episode: bool = result["end_of_episode"] + info_list: List[dict] = result["info"] - if scenario.post_collect: - scenario.post_collect(result["info"], ep, segment) + n_sample += len(experiences[0]) + total_experiences.extend(experiences) + total_info_list.extend(info_list) collect_time += time.time() - tc0 - logger.info(f"Roll-out completed for episode {ep}, segment {segment}. Training started...") - tu0 = time.time() - training_manager.record_experiences(experiences) - training_manager.train_step() - if checkpoint_path and (checkpoint_interval is None or ep % checkpoint_interval == 0): - assert isinstance(checkpoint_path, str) - pth = os.path.join(checkpoint_path, str(ep)) - training_manager.save(pth) - logger.info(f"All trainer states saved under {pth}") - training_time += time.time() - tu0 - segment += 1 + if scenario.post_collect: + scenario.post_collect(total_info_list, ep, -1) # TODO + + logger.info(f"Roll-out completed for episode {ep}. Training started...") + tu0 = time.time() + training_manager.record_experiences(total_experiences) + training_manager.train_step() + if checkpoint_path and (checkpoint_interval is None or ep % checkpoint_interval == 0): + assert isinstance(checkpoint_path, str) + pth = os.path.join(checkpoint_path, str(ep)) + training_manager.save(pth) + logger.info(f"All trainer states saved under {pth}") + training_time += time.time() - tu0 # performance details logger.info(f"ep {ep} - roll-out time: {collect_time:.2f} seconds, training time: {training_time:.2f} seconds") From 333986fc506b9c95298410da59fc9bc7d937b9d0 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Tue, 10 May 2022 16:35:35 +0800 Subject: [PATCH 472/482] RL component bundle (#513) * CIM passed * Update workers * Refine annotations * VM passed * Code formatting. * Minor import loop issue * Pass batch in PPO again * Remove Scenario * Complete docs * Minor * Remove segment * Optimize logic in RLComponentBundle * Resolve PR comments * Move 'post methods from RLComponenetBundle to EnvSampler --- examples/cim/rl/README.md | 9 +- examples/cim/rl/__init__.py | 12 +- examples/cim/rl/algorithms/ac.py | 2 +- examples/cim/rl/algorithms/dqn.py | 2 +- examples/cim/rl/algorithms/maddpg.py | 2 +- examples/cim/rl/algorithms/ppo.py | 2 +- examples/cim/rl/callbacks.py | 25 --- examples/cim/rl/config.py | 2 + examples/cim/rl/env_sampler.py | 27 +-- examples/cim/rl/policy_trainer.py | 33 --- examples/cim/rl/rl_component_bundle.py | 84 ++++++++ examples/rl/README.md | 12 +- examples/vm_scheduling/rl/README.md | 9 +- examples/vm_scheduling/rl/__init__.py | 11 +- examples/vm_scheduling/rl/algorithms/ac.py | 2 +- examples/vm_scheduling/rl/algorithms/dqn.py | 7 +- examples/vm_scheduling/rl/callbacks.py | 82 -------- examples/vm_scheduling/rl/env_sampler.py | 96 +++++++-- examples/vm_scheduling/rl/policy_trainer.py | 19 -- .../vm_scheduling/rl/rl_component_bundle.py | 57 ++++++ maro/rl/model/__init__.py | 6 +- maro/rl/model/algorithm_nets/ac_based.py | 3 +- maro/rl/model/algorithm_nets/ddpg.py | 3 +- maro/rl/model/algorithm_nets/sac.py | 3 +- maro/rl/model/multi_q_net.py | 2 +- maro/rl/model/policy_net.py | 2 +- maro/rl/model/q_net.py | 2 +- maro/rl/model/v_net.py | 2 +- maro/rl/policy/continuous_rl_policy.py | 1 + maro/rl/rl_component/__init__.py | 0 maro/rl/rl_component/rl_component_bundle.py | 188 ++++++++++++++++++ maro/rl/rollout/env_sampler.py | 97 +++++---- maro/rl/rollout/worker.py | 14 +- maro/rl/training/__init__.py | 2 +- maro/rl/training/algorithms/ac.py | 2 + .../training/algorithms/base/ac_ppo_base.py | 100 ++++++---- maro/rl/training/algorithms/dqn.py | 11 +- maro/rl/training/algorithms/maddpg.py | 9 +- maro/rl/training/algorithms/ppo.py | 1 + maro/rl/training/replay_memory.py | 2 +- maro/rl/training/train_ops.py | 6 +- maro/rl/training/trainer.py | 30 +-- maro/rl/training/training_manager.py | 48 +++-- maro/rl/training/utils.py | 21 -- maro/rl/training/worker.py | 27 +-- maro/rl/utils/__init__.py | 2 +- maro/rl/utils/training.py | 6 + maro/rl/workflows/main.py | 69 +++---- maro/rl/workflows/rollout_worker.py | 19 +- maro/rl/workflows/scenario.py | 51 ----- maro/rl/workflows/train_worker.py | 18 +- 51 files changed, 725 insertions(+), 517 deletions(-) delete mode 100644 examples/cim/rl/callbacks.py delete mode 100644 examples/cim/rl/policy_trainer.py create mode 100644 examples/cim/rl/rl_component_bundle.py delete mode 100644 examples/vm_scheduling/rl/callbacks.py delete mode 100644 examples/vm_scheduling/rl/policy_trainer.py create mode 100644 examples/vm_scheduling/rl/rl_component_bundle.py create mode 100644 maro/rl/rl_component/__init__.py create mode 100644 maro/rl/rl_component/rl_component_bundle.py delete mode 100644 maro/rl/training/utils.py create mode 100644 maro/rl/utils/training.py delete mode 100644 maro/rl/workflows/scenario.py diff --git a/examples/cim/rl/README.md b/examples/cim/rl/README.md index c4ab88276..e9ecc34ec 100644 --- a/examples/cim/rl/README.md +++ b/examples/cim/rl/README.md @@ -1,10 +1,9 @@ # Container Inventory Management This example demonstrates the use of MARO's RL toolkit to optimize container inventory management. The scenario consists of a set of ports, each acting as a learning agent, and vessels that transfer empty containers among them. Each port must decide 1) whether to load or discharge containers when a vessel arrives and 2) how many containers to be loaded or discharged. The objective is to minimize the overall container shortage over a certain period of time. In this folder you can find: +* ``__init__.py``, the entrance of this example. You must expose a `rl_component_bundle_cls` interface in `__init__.py` (see the example file for details); * ``config.py``, which contains general configurations for the scenario; -* ``algorithms``, which contains configurations for the Actor-Critic, DQN and discrete-MADDPG algorithms, including network configurations; -* ``env_sampler.py``, which defines state, action and reward shaping in the ``CIMEnvSampler`` class; -* ``policy_trainer.py``, which contains a registry for the policies and algorithms defined in ``algorithms``; -* ``callbacks.py``, which defines routines to be invoked at the end of training or evaluation episodes. +* ``algorithms/``, which contains configurations for the PPO, Actor-Critic, DQN and discrete-MADDPG algorithms, including network configurations; +* ``rl_componenet_bundle.py``, which defines all necessary components to run a RL job. You can go through the doc string of `RLComponentBundle` for detailed explanation, or just read `CIMBundle` to learn its basic usage. -See ``README.md`` under ``examples/rl`` for details about running the single-threaded learning workflow. We recommend that you follow this example to write your own scenarios. \ No newline at end of file +We recommend that you follow this example to write your own scenarios. diff --git a/examples/cim/rl/__init__.py b/examples/cim/rl/__init__.py index af4ce720f..695d90ede 100644 --- a/examples/cim/rl/__init__.py +++ b/examples/cim/rl/__init__.py @@ -1,16 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .callbacks import post_collect, post_evaluate -from .env_sampler import agent2policy, env_sampler_creator -from .policy_trainer import device_mapping, policy_creator, trainer_creator +from .rl_component_bundle import CIMBundle as rl_component_bundle_cls __all__ = [ - "agent2policy", - "device_mapping", - "env_sampler_creator", - "policy_creator", - "post_collect", - "post_evaluate", - "trainer_creator", + "rl_component_bundle_cls", ] diff --git a/examples/cim/rl/algorithms/ac.py b/examples/cim/rl/algorithms/ac.py index 5fdb40261..f42dffe05 100644 --- a/examples/cim/rl/algorithms/ac.py +++ b/examples/cim/rl/algorithms/ac.py @@ -49,7 +49,7 @@ def _get_v_values(self, states: torch.Tensor) -> torch.Tensor: return self._critic(states).squeeze(-1) -def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: +def get_ac_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) diff --git a/examples/cim/rl/algorithms/dqn.py b/examples/cim/rl/algorithms/dqn.py index afa2127a5..2194656a5 100644 --- a/examples/cim/rl/algorithms/dqn.py +++ b/examples/cim/rl/algorithms/dqn.py @@ -33,7 +33,7 @@ def _get_q_values_for_all_actions(self, states: torch.Tensor) -> torch.Tensor: return self._fc(states) -def get_policy(state_dim: int, action_num: int, name: str) -> ValueBasedPolicy: +def get_dqn_policy(state_dim: int, action_num: int, name: str) -> ValueBasedPolicy: return ValueBasedPolicy( name=name, q_net=MyQNet(state_dim, action_num), diff --git a/examples/cim/rl/algorithms/maddpg.py b/examples/cim/rl/algorithms/maddpg.py index ba0cbc7ad..e422f572c 100644 --- a/examples/cim/rl/algorithms/maddpg.py +++ b/examples/cim/rl/algorithms/maddpg.py @@ -56,7 +56,7 @@ def get_multi_critic_net(state_dim: int, action_dims: List[int]) -> MyMultiCriti return MyMultiCriticNet(state_dim, action_dims) -def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: +def get_maddpg_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) diff --git a/examples/cim/rl/algorithms/ppo.py b/examples/cim/rl/algorithms/ppo.py index 9f2253565..770f68a16 100644 --- a/examples/cim/rl/algorithms/ppo.py +++ b/examples/cim/rl/algorithms/ppo.py @@ -6,7 +6,7 @@ from .ac import MyActorNet, MyCriticNet -def get_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: +def get_ppo_policy(state_dim: int, action_num: int, name: str) -> DiscretePolicyGradient: return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num)) diff --git a/examples/cim/rl/callbacks.py b/examples/cim/rl/callbacks.py deleted file mode 100644 index 85c7a2516..000000000 --- a/examples/cim/rl/callbacks.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -def post_collect(info_list: list, ep: int, segment: int) -> None: - # print the env metric from each rollout worker - for info in info_list: - print(f"env summary (episode {ep}, segment {segment}): {info['env_metric']}") - - # print the average env metric - if len(info_list) > 1: - metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) - avg_metric = {key: sum(info["env_metric"][key] for info in info_list) / num_envs for key in metric_keys} - print(f"average env summary (episode {ep}, segment {segment}): {avg_metric}") - - -def post_evaluate(info_list: list, ep: int) -> None: - # print the env metric from each rollout worker - for info in info_list: - print(f"env summary (episode {ep}): {info['env_metric']}") - - # print the average env metric - if len(info_list) > 1: - metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) - avg_metric = {key: sum(info["env_metric"][key] for info in info_list) / num_envs for key in metric_keys} - print(f"average env summary (episode {ep}): {avg_metric}") diff --git a/examples/cim/rl/config.py b/examples/cim/rl/config.py index 34dcd8f76..9da287a94 100644 --- a/examples/cim/rl/config.py +++ b/examples/cim/rl/config.py @@ -39,4 +39,6 @@ + len(vessel_attributes) ) +action_num = len(action_shaping_conf["action_space"]) + algorithm = "ppo" # ac, ppo, dqn or discrete_maddpg diff --git a/examples/cim/rl/env_sampler.py b/examples/cim/rl/env_sampler.py index d493cc963..d55ad9da0 100644 --- a/examples/cim/rl/env_sampler.py +++ b/examples/cim/rl/env_sampler.py @@ -1,18 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Tuple, Union import numpy as np -from maro.rl.policy import RLPolicy from maro.rl.rollout import AbsEnvSampler, CacheElement -from maro.simulator import Env from maro.simulator.scenarios.cim.common import Action, ActionType, DecisionEvent from .config import ( - action_shaping_conf, algorithm, env_conf, port_attributes, reward_shaping_conf, state_shaping_conf, - vessel_attributes + action_shaping_conf, port_attributes, reward_shaping_conf, state_shaping_conf, + vessel_attributes, ) @@ -82,13 +80,16 @@ def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> N def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: self._post_step(cache_element, reward) + def post_collect(self, info_list: list, ep: int) -> None: + # print the env metric from each rollout worker + for info in info_list: + print(f"env summary (episode {ep}): {info['env_metric']}") -agent2policy = {agent: f"{algorithm}_{agent}.policy" for agent in Env(**env_conf).agent_idx_list} + # print the average env metric + if len(info_list) > 1: + metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) + avg_metric = {key: sum(info["env_metric"][key] for info in info_list) / num_envs for key in metric_keys} + print(f"average env summary (episode {ep}): {avg_metric}") - -def env_sampler_creator(policy_creator: Dict[str, Callable[[str], RLPolicy]]) -> CIMEnvSampler: - return CIMEnvSampler( - get_env=lambda: Env(**env_conf), - policy_creator=policy_creator, - agent2policy=agent2policy, - ) + def post_evaluate(self, info_list: list, ep: int) -> None: + self.post_collect(info_list, ep) diff --git a/examples/cim/rl/policy_trainer.py b/examples/cim/rl/policy_trainer.py deleted file mode 100644 index 44d8c2c92..000000000 --- a/examples/cim/rl/policy_trainer.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from functools import partial - -from .config import algorithm, action_shaping_conf, num_agents, state_dim - -action_num = len(action_shaping_conf["action_space"]) - -if algorithm == "ac": - from .algorithms.ac import get_ac, get_policy - policy_creator = {f"ac_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(num_agents)} - trainer_creator = {f"ac_{i}": partial(get_ac, state_dim) for i in range(num_agents)} -elif algorithm == "ppo": - from .algorithms.ppo import get_ppo, get_policy - policy_creator = {f"ppo_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(num_agents)} - trainer_creator = {f"ppo_{i}": partial(get_ppo, state_dim) for i in range(num_agents)} -elif algorithm == "dqn": - from .algorithms.dqn import get_dqn, get_policy - policy_creator = {f"dqn_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(num_agents)} - trainer_creator = {f"dqn_{i}": partial(get_dqn) for i in range(num_agents)} -elif algorithm == "discrete_maddpg": - from .algorithms.maddpg import get_policy, get_maddpg - policy_creator = { - f"discrete_maddpg_{i}.policy": partial(get_policy, state_dim, action_num) for i in range(num_agents) - } - trainer_creator = {f"discrete_maddpg_{i}": partial(get_maddpg, state_dim, [1]) for i in range(num_agents)} -else: - raise ValueError(f"Unsupported algorithm: {algorithm}") - -device_mapping = { - policy_name: "cpu" for policy_name in policy_creator -} diff --git a/examples/cim/rl/rl_component_bundle.py b/examples/cim/rl/rl_component_bundle.py new file mode 100644 index 000000000..f3b452faf --- /dev/null +++ b/examples/cim/rl/rl_component_bundle.py @@ -0,0 +1,84 @@ +from functools import partial +from typing import Any, Callable, Dict, Optional + +from examples.cim.rl.config import action_num, algorithm, env_conf, num_agents, state_dim +from examples.cim.rl.env_sampler import CIMEnvSampler +from maro.rl.policy import AbsPolicy +from maro.rl.rl_component.rl_component_bundle import RLComponentBundle +from maro.rl.rollout import AbsEnvSampler +from maro.rl.training import AbsTrainer + +from .algorithms.ac import get_ac_policy +from .algorithms.dqn import get_dqn_policy +from .algorithms.maddpg import get_maddpg_policy +from .algorithms.ppo import get_ppo_policy +from .algorithms.ac import get_ac +from .algorithms.ppo import get_ppo +from .algorithms.dqn import get_dqn +from .algorithms.maddpg import get_maddpg + + +class CIMBundle(RLComponentBundle): + def get_env_config(self) -> dict: + return env_conf + + def get_test_env_config(self) -> Optional[dict]: + return None + + def get_env_sampler(self) -> AbsEnvSampler: + return CIMEnvSampler(self.env, self.test_env) + + def get_agent2policy(self) -> Dict[Any, str]: + return {agent: f"{algorithm}_{agent}.policy"for agent in self.env.agent_idx_list} + + def get_policy_creator(self) -> Dict[str, Callable[[], AbsPolicy]]: + if algorithm == "ac": + policy_creator = { + f"{algorithm}_{i}.policy": partial(get_ac_policy, state_dim, action_num, f"{algorithm}_{i}.policy") + for i in range(num_agents) + } + elif algorithm == "ppo": + policy_creator = { + f"{algorithm}_{i}.policy": partial(get_ppo_policy, state_dim, action_num, f"{algorithm}_{i}.policy") + for i in range(num_agents) + } + elif algorithm == "dqn": + policy_creator = { + f"{algorithm}_{i}.policy": partial(get_dqn_policy, state_dim, action_num, f"{algorithm}_{i}.policy") + for i in range(num_agents) + } + elif algorithm == "discrete_maddpg": + policy_creator = { + f"{algorithm}_{i}.policy": partial(get_maddpg_policy, state_dim, action_num, f"{algorithm}_{i}.policy") + for i in range(num_agents) + } + else: + raise ValueError(f"Unsupported algorithm: {algorithm}") + + return policy_creator + + def get_trainer_creator(self) -> Dict[str, Callable[[], AbsTrainer]]: + if algorithm == "ac": + trainer_creator = { + f"{algorithm}_{i}": partial(get_ac, state_dim, f"{algorithm}_{i}") + for i in range(num_agents) + } + elif algorithm == "ppo": + trainer_creator = { + f"{algorithm}_{i}": partial(get_ppo, state_dim, f"{algorithm}_{i}") + for i in range(num_agents) + } + elif algorithm == "dqn": + trainer_creator = { + f"{algorithm}_{i}": partial(get_dqn, f"{algorithm}_{i}") + for i in range(num_agents) + } + elif algorithm == "discrete_maddpg": + trainer_creator = { + f"{algorithm}_{i}": partial(get_maddpg, state_dim, [1], f"{algorithm}_{i}") + for i in range(num_agents) + } + else: + raise ValueError(f"Unsupported algorithm: {algorithm}") + + return trainer_creator diff --git a/examples/rl/README.md b/examples/rl/README.md index cc3b63be8..ca3a3807e 100644 --- a/examples/rl/README.md +++ b/examples/rl/README.md @@ -12,10 +12,8 @@ There are two ways to start the RL job: ## Create Your Own Scenarios -You can create your own scenarios by supplying the necessary ingredients without worrying about putting them together in a workflow. It is necessary to create an ``__init__.py`` under your scenario folder (so that it can be treated as a package) and expose all ingredients in it. The ingredients include: -* Definitions of policies and agent-to-policy mappings. These definitions should be provided as a dictionary named ``policy_creator`` that maps a name to a function that takes the name and returns a policy instance with that name. Optionally, you may specify which policies you intend to train by providing ``trainable_policies``, which a list of policy names. The experiences generated by these policies will be recorded by the environment sampler and used for training. The agent-to-policy mapping should be provided as a dictionary named ``agent2policy``. -* Definitions of training algorithms. These definitions should be provided as a dictionary named ``trainer_creator`` that maps a name to a function that takes the name and returns a trainer instance with that name. -* Definitions of state, action and reward shaping logic pertinent to your simulator and policies. -These definitions should be encapsulated in ``env_sampler_creator``, which is a function that takes ``policy_creator`` and returns an environment sampler; -It is possible to have customized routines invoked at the end of a roll-out episode or episode segment. These routines usually involve processing and / or rendering information collected during roll-out. To do this, first implement the ``post_step`` method in your environment sampler class to record whatever information you wish to keep track of during roll-out. Then create functions named ``post_collect`` and ``post_evaluate`` to process the information and expose them in the scenario folder's ``__init__.py``. These functions are used as callbacks in the main learning loop and executed at the end of each training or evaluation episode. See ``cim/callbacks.py`` for a simple example of how to create these functions. -* An optional dictionary named ``device_mapping`` that specifies the compute device (CPU or GPU) for each policy. If not provided, all computations will be performed on the CPU. +You can create your own scenarios by supplying the necessary ingredients without worrying about putting them together in a workflow. It is necessary to create an ``__init__.py`` under your scenario folder (so that it can be treated as a package) and expose a `rl_component_bundle_cls` interface. The MARO's RL workflow will use this interface to create a `RLComponentBundle` instance and start the RL workflow based on it. a `RLComponentBundle` instance defines all necessary components to run a RL job. You can go through the doc string of `RLComponentBundle` for detailed explanation, or just read one of the examples to learn its basic usage. + +## Example + +For a complete example, please check `examples/cim/rl`. diff --git a/examples/vm_scheduling/rl/README.md b/examples/vm_scheduling/rl/README.md index 8b5d509ca..950c41b16 100644 --- a/examples/vm_scheduling/rl/README.md +++ b/examples/vm_scheduling/rl/README.md @@ -2,13 +2,12 @@ A virtual machine (VM) scheduler is a cloud computing service component responsible for providing compute resources to satisfy user demands. A good resource allocation policy should aim to optimize several metrics at the same time, such as user wait time, profit, energy consumption and physical machine (PM) overload. Many commercial cloud providers use rule-based policies. Alternatively, the policy can also be optimized using reinforcement learning (RL) techniques, which involves simulating with historical data. This example demonstrates how DQN and Actor-Critic algorithms can be applied to this scenario. In this folder, you can find: +* ``__init__.py``, the entrance of this example. You must expose a `rl_component_bundle_cls` interface in `__init__.py` (see the example file for details); * ``config.py``, which contains general configurations for the scenario; -* ``algorithms``, which contains configurations for the Actor-Critic, DQN algorithms, including network configurations; -* ``env_sampler.py``, which defines state, action and reward shaping in the ``VMEnvSampler`` class; -* ``policy_trainer.py``, which contains a registry for the policies and algorithms defined in ``algorithms``; -* ``callbacks.py``, which defines routines to be invoked at the end of training or evaluation episodes. +* ``algorithms/``, which contains configurations for the algorithms, including network configurations; +* ``rl_componenet_bundle.py``, which defines all necessary components to run a RL job. You can go through the doc string of `RLComponentBundle` for detailed explanation, or just read `VMBundle` to learn its basic usage. -See ``README.md`` under ``examples/rl`` for details about running the single-threaded learning workflow. We recommend that you follow this example to write your own scenarios. +We recommend that you follow this example to write your own scenarios. # Some Comments About the Results diff --git a/examples/vm_scheduling/rl/__init__.py b/examples/vm_scheduling/rl/__init__.py index 50e0d0c3c..44e5138a2 100644 --- a/examples/vm_scheduling/rl/__init__.py +++ b/examples/vm_scheduling/rl/__init__.py @@ -1,15 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .callbacks import post_collect, post_evaluate -from .env_sampler import agent2policy, env_sampler_creator -from .policy_trainer import policy_creator, trainer_creator +from .rl_component_bundle import VMBundle as rl_component_bundle_cls __all__ = [ - "agent2policy", - "env_sampler_creator", - "policy_creator", - "post_collect", - "post_evaluate", - "trainer_creator", + "rl_component_bundle_cls", ] diff --git a/examples/vm_scheduling/rl/algorithms/ac.py b/examples/vm_scheduling/rl/algorithms/ac.py index ef7efd84b..787730ea2 100644 --- a/examples/vm_scheduling/rl/algorithms/ac.py +++ b/examples/vm_scheduling/rl/algorithms/ac.py @@ -57,7 +57,7 @@ def _get_v_values(self, states: torch.Tensor) -> torch.Tensor: return self._critic(features).squeeze(-1) -def get_policy(state_dim: int, action_num: int, num_features: int, name: str) -> DiscretePolicyGradient: +def get_ac_policy(state_dim: int, action_num: int, num_features: int, name: str) -> DiscretePolicyGradient: return DiscretePolicyGradient(name=name, policy_net=MyActorNet(state_dim, action_num, num_features)) diff --git a/examples/vm_scheduling/rl/algorithms/dqn.py b/examples/vm_scheduling/rl/algorithms/dqn.py index 6cff78783..12a8adc98 100644 --- a/examples/vm_scheduling/rl/algorithms/dqn.py +++ b/examples/vm_scheduling/rl/algorithms/dqn.py @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Dict - import numpy as np import torch from torch.optim import SGD @@ -11,8 +9,7 @@ from maro.rl.exploration import MultiLinearExplorationScheduler from maro.rl.model import DiscreteQNet, FullyConnected from maro.rl.policy import ValueBasedPolicy -from maro.rl.training.algorithms import DQNTrainer, DQNParams - +from maro.rl.training.algorithms import DQNParams, DQNTrainer q_net_conf = { "hidden_dims": [64, 128, 256], @@ -54,7 +51,7 @@ def __call__(self, states, actions, num_actions, *, epsilon): ]) -def get_policy(state_dim: int, action_num: int, num_features: int, name: str) -> ValueBasedPolicy: +def get_dqn_policy(state_dim: int, action_num: int, num_features: int, name: str) -> ValueBasedPolicy: return ValueBasedPolicy( name=name, q_net=MyQNet(state_dim, action_num, num_features), diff --git a/examples/vm_scheduling/rl/callbacks.py b/examples/vm_scheduling/rl/callbacks.py deleted file mode 100644 index 3d3371de1..000000000 --- a/examples/vm_scheduling/rl/callbacks.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import time -from os import makedirs -from os.path import dirname, join, realpath - -from matplotlib import pyplot as plt - -timestamp = str(time.time()) -log_dir = join(dirname(realpath(__file__)), "log", timestamp) -makedirs(log_dir, exist_ok=True) -plt_path = join(dirname(realpath(__file__)), "plots", timestamp) -makedirs(plt_path, exist_ok=True) - - -def post_collect(info_list, ep, segment): - # print the env metric from each rollout worker - for info in info_list: - print(f"env summary (episode {ep}, segment {segment}): {info['env_metric']}") - - # print the average env metric - if len(info_list) > 1: - metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) - avg_metric = {key: sum(tr["env_metric"][key] for tr in info_list) / num_envs for key in metric_keys} - print(f"average env metric (episode {ep}, segment {segment}): {avg_metric}") - - -def post_evaluate(info_list, ep): - # print the env metric from each rollout worker - for info in info_list: - print(f"env summary (evaluation episode {ep}): {info['env_metric']}") - - # print the average env metric - if len(info_list) > 1: - metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) - avg_metric = {key: sum(tr["env_metric"][key] for tr in info_list) / num_envs for key in metric_keys} - print(f"average env metric (evaluation episode {ep}): {avg_metric}") - - for info in info_list: - core_requirement = info["actions_by_core_requirement"] - action_sequence = info["action_sequence"] - # plot action sequence - fig = plt.figure(figsize=(40, 32)) - ax = fig.add_subplot(1, 1, 1) - ax.plot(action_sequence) - fig.savefig(f"{plt_path}/action_sequence_{ep}") - plt.cla() - plt.close("all") - - # plot with legal action mask - fig = plt.figure(figsize=(40, 32)) - for idx, key in enumerate(core_requirement.keys()): - ax = fig.add_subplot(len(core_requirement.keys()), 1, idx + 1) - for i in range(len(core_requirement[key])): - if i == 0: - ax.plot(core_requirement[key][i][0] * core_requirement[key][i][1], label=str(key)) - ax.legend() - else: - ax.plot(core_requirement[key][i][0] * core_requirement[key][i][1]) - - fig.savefig(f"{plt_path}/values_with_legal_action_{ep}") - - plt.cla() - plt.close("all") - - # plot without legal actin mask - fig = plt.figure(figsize=(40, 32)) - - for idx, key in enumerate(core_requirement.keys()): - ax = fig.add_subplot(len(core_requirement.keys()), 1, idx + 1) - for i in range(len(core_requirement[key])): - if i == 0: - ax.plot(core_requirement[key][i][0], label=str(key)) - ax.legend() - else: - ax.plot(core_requirement[key][i][0]) - - fig.savefig(f"{plt_path}/values_without_legal_action_{ep}") - - plt.cla() - plt.close("all") diff --git a/examples/vm_scheduling/rl/env_sampler.py b/examples/vm_scheduling/rl/env_sampler.py index 87023e7b7..cc5804e66 100644 --- a/examples/vm_scheduling/rl/env_sampler.py +++ b/examples/vm_scheduling/rl/env_sampler.py @@ -1,27 +1,32 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import sys +import time from collections import defaultdict -from os.path import dirname, realpath -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from os import makedirs +from os.path import dirname, join, realpath +from typing import Any, Dict, List, Tuple, Union import numpy as np +from matplotlib import pyplot as plt -from maro.rl.policy import RLPolicy from maro.rl.rollout import AbsEnvSampler, CacheElement from maro.simulator import Env from maro.simulator.scenarios.vm_scheduling import AllocateAction, DecisionPayload, PostponeAction from .config import ( - algorithm, env_conf, num_features, pm_attributes, pm_window_size, reward_shaping_conf, seed, test_env_conf, - test_reward_shaping_conf, test_seed + num_features, pm_attributes, pm_window_size, reward_shaping_conf, seed, test_reward_shaping_conf, test_seed, ) +timestamp = str(time.time()) +plt_path = join(dirname(realpath(__file__)), "plots", timestamp) +makedirs(plt_path, exist_ok=True) + class VMEnvSampler(AbsEnvSampler): - def __init__(self, get_env, policy_creator, agent2policy, get_test_env=None): - super().__init__(get_env, policy_creator, agent2policy, get_test_env=get_test_env) + def __init__(self, learn_env: Env, test_env: Env) -> None: + super(VMEnvSampler, self).__init__(learn_env, test_env) + self._learn_env.set_seed(seed) self._test_env.set_seed(test_seed) @@ -128,13 +133,68 @@ def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]): def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: self._post_step(cache_element, reward) - -agent2policy = {"AGENT": f"{algorithm}.policy"} - -def env_sampler_creator(policy_creator: Dict[str, Callable[[str], RLPolicy]]) -> VMEnvSampler: - return VMEnvSampler( - get_env=lambda: Env(**env_conf), - policy_creator=policy_creator, - agent2policy=agent2policy, - get_test_env=lambda: Env(**test_env_conf), - ) + def post_collect(self, info_list: list, ep: int) -> None: + # print the env metric from each rollout worker + for info in info_list: + print(f"env summary (episode {ep}): {info['env_metric']}") + + # print the average env metric + if len(info_list) > 1: + metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) + avg_metric = {key: sum(tr["env_metric"][key] for tr in info_list) / num_envs for key in metric_keys} + print(f"average env metric (episode {ep}): {avg_metric}") + + def post_evaluate(self, info_list: list, ep: int) -> None: + # print the env metric from each rollout worker + for info in info_list: + print(f"env summary (evaluation episode {ep}): {info['env_metric']}") + + # print the average env metric + if len(info_list) > 1: + metric_keys, num_envs = info_list[0]["env_metric"].keys(), len(info_list) + avg_metric = {key: sum(tr["env_metric"][key] for tr in info_list) / num_envs for key in metric_keys} + print(f"average env metric (evaluation episode {ep}): {avg_metric}") + + for info in info_list: + core_requirement = info["actions_by_core_requirement"] + action_sequence = info["action_sequence"] + # plot action sequence + fig = plt.figure(figsize=(40, 32)) + ax = fig.add_subplot(1, 1, 1) + ax.plot(action_sequence) + fig.savefig(f"{plt_path}/action_sequence_{ep}") + plt.cla() + plt.close("all") + + # plot with legal action mask + fig = plt.figure(figsize=(40, 32)) + for idx, key in enumerate(core_requirement.keys()): + ax = fig.add_subplot(len(core_requirement.keys()), 1, idx + 1) + for i in range(len(core_requirement[key])): + if i == 0: + ax.plot(core_requirement[key][i][0] * core_requirement[key][i][1], label=str(key)) + ax.legend() + else: + ax.plot(core_requirement[key][i][0] * core_requirement[key][i][1]) + + fig.savefig(f"{plt_path}/values_with_legal_action_{ep}") + + plt.cla() + plt.close("all") + + # plot without legal actin mask + fig = plt.figure(figsize=(40, 32)) + + for idx, key in enumerate(core_requirement.keys()): + ax = fig.add_subplot(len(core_requirement.keys()), 1, idx + 1) + for i in range(len(core_requirement[key])): + if i == 0: + ax.plot(core_requirement[key][i][0], label=str(key)) + ax.legend() + else: + ax.plot(core_requirement[key][i][0]) + + fig.savefig(f"{plt_path}/values_without_legal_action_{ep}") + + plt.cla() + plt.close("all") diff --git a/examples/vm_scheduling/rl/policy_trainer.py b/examples/vm_scheduling/rl/policy_trainer.py deleted file mode 100644 index b4a2d01fa..000000000 --- a/examples/vm_scheduling/rl/policy_trainer.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -from functools import partial - -from .config import algorithm, state_dim, num_features, num_pms - -action_num = num_pms + 1 # action could be any PM or postponement, hence the plus 1 - -if algorithm == "ac": - from .algorithms.ac import get_ac, get_policy - policy_creator = {"ac.policy": partial(get_policy, state_dim, action_num, num_features)} - trainer_creator = {"ac": partial(get_ac, state_dim, num_features)} -elif algorithm == "dqn": - from .algorithms.dqn import get_dqn, get_policy - policy_creator = {"dqn.policy": partial(get_policy, state_dim, action_num, num_features)} - trainer_creator = {"dqn": get_dqn} -else: - raise ValueError(f"Unsupported algorithm: {algorithm}") diff --git a/examples/vm_scheduling/rl/rl_component_bundle.py b/examples/vm_scheduling/rl/rl_component_bundle.py new file mode 100644 index 000000000..14edf47ce --- /dev/null +++ b/examples/vm_scheduling/rl/rl_component_bundle.py @@ -0,0 +1,57 @@ +from functools import partial +from typing import Any, Callable, Dict, Optional + +from examples.vm_scheduling.rl.algorithms.ac import get_ac_policy +from examples.vm_scheduling.rl.algorithms.dqn import get_dqn_policy +from examples.vm_scheduling.rl.config import algorithm, env_conf, num_features, num_pms, state_dim, test_env_conf +from examples.vm_scheduling.rl.env_sampler import VMEnvSampler +from maro.rl.policy import AbsPolicy +from maro.rl.rl_component.rl_component_bundle import RLComponentBundle +from maro.rl.rollout import AbsEnvSampler +from maro.rl.training import AbsTrainer + + +class VMBundle(RLComponentBundle): + def get_env_config(self) -> dict: + return env_conf + + def get_test_env_config(self) -> Optional[dict]: + return test_env_conf + + def get_env_sampler(self) -> AbsEnvSampler: + return VMEnvSampler(self.env, self.test_env) + + def get_agent2policy(self) -> Dict[Any, str]: + return {"AGENT": f"{algorithm}.policy"} + + def get_policy_creator(self) -> Dict[str, Callable[[], AbsPolicy]]: + action_num = num_pms + 1 # action could be any PM or postponement, hence the plus 1 + + if algorithm == "ac": + policy_creator = { + f"{algorithm}.policy": partial( + get_ac_policy, state_dim, action_num, num_features, f"{algorithm}.policy", + ) + } + elif algorithm == "dqn": + policy_creator = { + f"{algorithm}.policy": partial( + get_dqn_policy, state_dim, action_num, num_features, f"{algorithm}.policy", + ) + } + else: + raise ValueError(f"Unsupported algorithm: {algorithm}") + + return policy_creator + + def get_trainer_creator(self) -> Dict[str, Callable[[], AbsTrainer]]: + if algorithm == "ac": + from .algorithms.ac import get_ac, get_ac_policy + trainer_creator = {algorithm: partial(get_ac, state_dim, num_features, algorithm)} + elif algorithm == "dqn": + from .algorithms.dqn import get_dqn, get_dqn_policy + trainer_creator = {algorithm: partial(get_dqn, algorithm)} + else: + raise ValueError(f"Unsupported algorithm: {algorithm}") + + return trainer_creator diff --git a/maro/rl/model/__init__.py b/maro/rl/model/__init__.py index cc18ef592..f07874e34 100644 --- a/maro/rl/model/__init__.py +++ b/maro/rl/model/__init__.py @@ -2,14 +2,14 @@ # Licensed under the MIT license. from .abs_net import AbsNet +from .algorithm_nets.ac_based import ContinuousACBasedNet, DiscreteACBasedNet +from .algorithm_nets.ddpg import ContinuousDDPGNet +from .algorithm_nets.sac import ContinuousSACNet from .fc_block import FullyConnected from .multi_q_net import MultiQNet from .policy_net import ContinuousPolicyNet, DiscretePolicyNet, PolicyNet from .q_net import ContinuousQNet, DiscreteQNet, QNet from .v_net import VNet -from .algorithm_nets.ac_based import ContinuousACBasedNet, DiscreteACBasedNet -from .algorithm_nets.ddpg import ContinuousDDPGNet -from .algorithm_nets.sac import ContinuousSACNet __all__ = [ "AbsNet", diff --git a/maro/rl/model/algorithm_nets/ac_based.py b/maro/rl/model/algorithm_nets/ac_based.py index d5ee8c83e..a05a6cdb1 100644 --- a/maro/rl/model/algorithm_nets/ac_based.py +++ b/maro/rl/model/algorithm_nets/ac_based.py @@ -3,7 +3,7 @@ import torch -from maro.rl.model import ContinuousPolicyNet, DiscretePolicyNet +from maro.rl.model.policy_net import ContinuousPolicyNet, DiscretePolicyNet class DiscreteACBasedNet(DiscretePolicyNet, metaclass=ABCMeta): @@ -40,6 +40,7 @@ class ContinuousACBasedNet(ContinuousPolicyNet, metaclass=ABCMeta): - get_state(self) -> dict: - set_state(self, net_state: dict) -> None: """ + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: actions, _ = self._get_actions_with_logps_impl(states, exploring) return actions diff --git a/maro/rl/model/algorithm_nets/ddpg.py b/maro/rl/model/algorithm_nets/ddpg.py index 2d81af64d..5e50abe40 100644 --- a/maro/rl/model/algorithm_nets/ddpg.py +++ b/maro/rl/model/algorithm_nets/ddpg.py @@ -3,7 +3,7 @@ import torch -from maro.rl.model import ContinuousPolicyNet +from maro.rl.model.policy_net import ContinuousPolicyNet class ContinuousDDPGNet(ContinuousPolicyNet, metaclass=ABCMeta): @@ -21,6 +21,7 @@ class ContinuousDDPGNet(ContinuousPolicyNet, metaclass=ABCMeta): - get_state(self) -> dict: - set_state(self, net_state: dict) -> None: """ + def _get_actions_with_probs_impl(self, states: torch.Tensor, exploring: bool) -> Tuple[torch.Tensor, torch.Tensor]: # Not used in DDPG pass diff --git a/maro/rl/model/algorithm_nets/sac.py b/maro/rl/model/algorithm_nets/sac.py index 2aff0323c..fca970989 100644 --- a/maro/rl/model/algorithm_nets/sac.py +++ b/maro/rl/model/algorithm_nets/sac.py @@ -3,7 +3,7 @@ import torch -from maro.rl.model import ContinuousPolicyNet +from maro.rl.model.policy_net import ContinuousPolicyNet class ContinuousSACNet(ContinuousPolicyNet, metaclass=ABCMeta): @@ -21,6 +21,7 @@ class ContinuousSACNet(ContinuousPolicyNet, metaclass=ABCMeta): - get_state(self) -> dict: - set_state(self, net_state: dict) -> None: """ + def _get_actions_impl(self, states: torch.Tensor, exploring: bool) -> torch.Tensor: actions, _ = self._get_actions_with_logps_impl(states, exploring) return actions diff --git a/maro/rl/model/multi_q_net.py b/maro/rl/model/multi_q_net.py index 104da5ee8..9fb4e1cb7 100644 --- a/maro/rl/model/multi_q_net.py +++ b/maro/rl/model/multi_q_net.py @@ -6,7 +6,7 @@ import torch -from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape +from maro.rl.utils import match_shape, SHAPE_CHECK_FLAG from .abs_net import AbsNet diff --git a/maro/rl/model/policy_net.py b/maro/rl/model/policy_net.py index 329cb37d4..63b03e10a 100644 --- a/maro/rl/model/policy_net.py +++ b/maro/rl/model/policy_net.py @@ -7,7 +7,7 @@ import torch.nn from torch.distributions import Categorical -from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape +from maro.rl.utils import match_shape, SHAPE_CHECK_FLAG from .abs_net import AbsNet diff --git a/maro/rl/model/q_net.py b/maro/rl/model/q_net.py index eced0a43f..777cf0b9d 100644 --- a/maro/rl/model/q_net.py +++ b/maro/rl/model/q_net.py @@ -5,7 +5,7 @@ import torch -from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape +from maro.rl.utils import match_shape, SHAPE_CHECK_FLAG from .abs_net import AbsNet diff --git a/maro/rl/model/v_net.py b/maro/rl/model/v_net.py index b4789ff91..6451aa514 100644 --- a/maro/rl/model/v_net.py +++ b/maro/rl/model/v_net.py @@ -5,7 +5,7 @@ import torch -from maro.rl.utils import SHAPE_CHECK_FLAG, match_shape +from maro.rl.utils import match_shape, SHAPE_CHECK_FLAG from .abs_net import AbsNet diff --git a/maro/rl/policy/continuous_rl_policy.py b/maro/rl/policy/continuous_rl_policy.py index 517512681..51b3faed0 100644 --- a/maro/rl/policy/continuous_rl_policy.py +++ b/maro/rl/policy/continuous_rl_policy.py @@ -7,6 +7,7 @@ import torch from maro.rl.model import ContinuousPolicyNet + from .abs_policy import RLPolicy diff --git a/maro/rl/rl_component/__init__.py b/maro/rl/rl_component/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/maro/rl/rl_component/rl_component_bundle.py b/maro/rl/rl_component/rl_component_bundle.py new file mode 100644 index 000000000..b9152e47c --- /dev/null +++ b/maro/rl/rl_component/rl_component_bundle.py @@ -0,0 +1,188 @@ +from abc import abstractmethod +from functools import partial +from typing import Any, Callable, Dict, List, Optional + +from maro.rl.policy import AbsPolicy +from maro.rl.rollout import AbsEnvSampler +from maro.rl.training import AbsTrainer +from maro.simulator import Env + + +class RLComponentBundle(object): + """Bundle of all necessary components to run a RL job in MARO. + + Users should create their own subclass of `RLComponentBundle` and implement following methods: + - get_env_config() + - get_test_env_config() + - get_env_sampler() + - get_agent2policy() + - get_policy_creator() + - get_trainer_creator() + + Following methods could be overwritten when necessary: + - get_device_mapping() + + Please refer to the doc string of each method for detailed explanations. + """ + def __init__(self) -> None: + super(RLComponentBundle, self).__init__() + + self.trainer_creator: Optional[Dict[str, Callable[[], AbsTrainer]]] = None + + self.agent2policy: Optional[Dict[Any, str]] = None + self.trainable_agent2policy: Optional[Dict[Any, str]] = None + self.policy_creator: Optional[Dict[str, Callable[[], AbsPolicy]]] = None + self.policy_names: Optional[List[str]] = None + self.trainable_policy_creator: Optional[Dict[str, Callable[[], AbsPolicy]]] = None + self.trainable_policy_names: Optional[List[str]] = None + + self.device_mapping: Optional[Dict[str, str]] = None + self.policy_trainer_mapping: Optional[Dict[str, str]] = None + + self._policy_cache: Optional[Dict[str, AbsPolicy]] = None + + # Will be created when `env_sampler()` is first called + self._env_sampler: Optional[AbsEnvSampler] = None + + self._complete_resources() + + ######################################################################################## + # Users MUST implement the following methods # + ######################################################################################## + @abstractmethod + def get_env_config(self) -> dict: + """Return the environment configuration to build the MARO Env for training. + + Returns: + Environment configuration. + """ + raise NotImplementedError + + @abstractmethod + def get_test_env_config(self) -> Optional[dict]: + """Return the environment configuration to build the MARO Env for testing. If returns `None`, the training + environment will be reused as testing environment. + + Returns: + Environment configuration or `None`. + """ + raise NotImplementedError + + @abstractmethod + def get_env_sampler(self) -> AbsEnvSampler: + """Return the environment sampler of the scenario. + + Returns: + The environment sampler of the scenario. + """ + raise NotImplementedError + + @abstractmethod + def get_agent2policy(self) -> Dict[Any, str]: + """Return agent name to policy name mapping of the RL job. This mapping identifies which policy should + the agents use. For example: {agent1: policy1, agent2: policy1, agent3: policy2}. + + Returns: + Agent name to policy name mapping. + """ + raise NotImplementedError + + @abstractmethod + def get_policy_creator(self) -> Dict[str, Callable[[], AbsPolicy]]: + """Return policy creator. Policy creator is a dictionary that contains a group of functions that generate + policy instances. The key of this dictionary is the policy name, and the value is the function that generate + the corresponding policy instance. Note that the creation function should not take any parameters. + """ + raise NotImplementedError + + @abstractmethod + def get_trainer_creator(self) -> Dict[str, Callable[[], AbsTrainer]]: + """Return trainer creator. Trainer creator is similar to policy creator, but is used to creator trainers. + """ + raise NotImplementedError + + ######################################################################################## + # Users could overwrite the following methods # + ######################################################################################## + def get_device_mapping(self) -> Dict[str, str]: + """Return the device mapping that identifying which device to put each policy. + + If user does not overwrite this method, then all policies will be put on CPU by default. + """ + return {policy_name: "cpu" for policy_name in self.get_policy_creator()} + + def get_policy_trainer_mapping(self) -> Dict[str, str]: + """Return the policy-trainer mapping which identifying which trainer to train each policy. + + If user does not overwrite this method, then a policy's trainer's name is the first segment of the policy's + name, seperated by dot. For example, "ppo_1.policy" is trained by "ppo_1". + + Only policies that provided in policy-trainer mapping are considered as trainable polices. Policies that + not provided in policy-trainer mapping will not be trained since we do not assign a trainer to it. + """ + return { + policy_name: policy_name.split(".")[0] for policy_name in self.policy_names + } + + ######################################################################################## + # Methods invisible to users # + ######################################################################################## + @property + def env_sampler(self) -> AbsEnvSampler: + if self._env_sampler is None: + self._env_sampler = self.get_env_sampler() + self._env_sampler.build(self) + return self._env_sampler + + def _complete_resources(self) -> None: + """Generate all attributes by calling user-defined logics. Do necessary checking and transformations. + """ + env_config = self.get_env_config() + test_env_config = self.get_test_env_config() + self.env = Env(**env_config) + self.test_env = self.env if test_env_config is None else Env(**test_env_config) + + self.trainer_creator = self.get_trainer_creator() + self.device_mapping = self.get_device_mapping() + + self.policy_creator = self.get_policy_creator() + self.policy_names = list(self.policy_creator.keys()) + self.agent2policy = self.get_agent2policy() + + self.policy_trainer_mapping = self.get_policy_trainer_mapping() + assert all([policy_name in self.policy_creator for policy_name in self.policy_trainer_mapping.keys()]) + assert all([trainer_name in self.trainer_creator for trainer_name in self.policy_trainer_mapping.values()]) + + self.trainable_policy_names = list(self.policy_trainer_mapping.keys()) + self.trainable_policy_creator = { + policy_name: self.policy_creator[policy_name] + for policy_name in self.trainable_policy_names + } + self.trainable_agent2policy = { + agent_name: policy_name + for agent_name, policy_name in self.agent2policy.items() + if policy_name in self.trainable_policy_names + } + + def pre_create_policy_instances(self) -> None: + """Pre-create policy instances, and return the pre-created policy instances when the external callers + want to create new policies. This will ensure that each policy will have at most one reusable duplicate. + Under specific scenarios (for example, simple training & rollout), this will reduce unnecessary overheads. + """ + old_policy_creator = self.policy_creator + self._policy_cache: Dict[str, AbsPolicy] = {} + for policy_name in self.policy_names: + self._policy_cache[policy_name] = old_policy_creator[policy_name]() + + def _get_policy_instance(policy_name: str) -> AbsPolicy: + return self._policy_cache[policy_name] + + self.policy_creator = { + policy_name: partial(_get_policy_instance, policy_name) + for policy_name in self.policy_names + } + + self.trainable_policy_creator = { + policy_name: self.policy_creator[policy_name] + for policy_name in self.trainable_policy_names + } diff --git a/maro/rl/rollout/env_sampler.py b/maro/rl/rollout/env_sampler.py index c9211fe0e..f5a5db0e3 100644 --- a/maro/rl/rollout/env_sampler.py +++ b/maro/rl/rollout/env_sampler.py @@ -5,11 +5,12 @@ import collections import os +import typing from abc import ABCMeta, abstractmethod from collections import defaultdict, deque from copy import deepcopy from dataclasses import dataclass -from typing import Any, Callable, Deque, Dict, List, Optional, Tuple, Type, Union +from typing import Any, Deque, Dict, List, Optional, Tuple, Type, Union import numpy as np import torch @@ -18,6 +19,9 @@ from maro.rl.utils.objects import FILE_SUFFIX from maro.simulator import Env +if typing.TYPE_CHECKING: + from maro.rl.rl_component.rl_component_bundle import RLComponentBundle + class AbsAgentWrapper(object, metaclass=ABCMeta): """Agent wrapper. Used to manager agents & policies during experience collection. @@ -35,11 +39,11 @@ def __init__( self._policy_dict = policy_dict self._agent2policy = agent2policy - def set_policy_state(self, policy_state_dict: Dict[str, object]) -> None: + def set_policy_state(self, policy_state_dict: Dict[str, dict]) -> None: """Set policies' states. Args: - policy_state_dict (Dict[str, object]): Double-deck dict with format: {policy_name: policy_state}. + policy_state_dict (Dict[str, dict]): Double-deck dict with format: {policy_name: policy_state}. """ for policy_name, policy_state in policy_state_dict.items(): policy = self._policy_dict[policy_name] @@ -204,46 +208,55 @@ class AbsEnvSampler(object, metaclass=ABCMeta): """Simulation data collector and policy evaluator. Args: - get_env (Callable[[], Env]): Function used to create the rollout environment. - policy_creator (Dict[str, Callable[[str], AbsPolicy]]): Dict of functions to create policies by name. - agent2policy (Dict[Any, str]): Mapping of agent name to policy name. - trainable_policies (List[str], default=None): List of trainable policy names. Experiences generated using the - policies specified in this list will be collected and passed to a training manager for training. Defaults - to None, in which case all policies are trainable. + learn_env (Env): Environment used for training. + test_env (Env): Environment used for testing. agent_wrapper_cls (Type[AbsAgentWrapper], default=SimpleAgentWrapper): Specific AgentWrapper type. reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward for the action taken for that event. - get_test_env (Callable[[], Env], default=None): Function used to create the testing environment. If it is None, - reuse the rollout environment as the testing environment. """ def __init__( self, - get_env: Callable[[], Env], - policy_creator: Dict[str, Callable[[str], AbsPolicy]], - agent2policy: Dict[Any, str], # {agent_name: policy_name} - trainable_policies: List[str] = None, + learn_env: Env, + test_env: Env, agent_wrapper_cls: Type[AbsAgentWrapper] = SimpleAgentWrapper, reward_eval_delay: int = 0, - get_test_env: Callable[[], Env] = None, ) -> None: - self._learn_env = get_env() - self._test_env = get_test_env() if get_test_env is not None else get_env() + self._learn_env = learn_env + self._test_env = test_env + + self._agent_wrapper_cls = agent_wrapper_cls + + self._state: Optional[np.ndarray] = None + self._agent_state_dict: Dict[Any, np.ndarray] = {} + + self._trans_cache: Deque[CacheElement] = deque() + self._reward_eval_delay = reward_eval_delay + + self._info = {} + + def build( + self, + rl_component_bundle: RLComponentBundle, + ) -> None: + """ + Args: + rl_component_bundle (RLComponentBundle): The RL component bundle of the job. + """ self._env: Optional[Env] = None self._event = None # Need this to remember the last event if an episode is divided into multiple segments - self._policy_dict: Dict[str, AbsPolicy] = { - policy_name: func(policy_name) for policy_name, func in policy_creator.items() + self._policy_dict = { + policy_name: rl_component_bundle.policy_creator[policy_name]() + for policy_name in rl_component_bundle.policy_names } + self._rl_policy_dict: Dict[str, RLPolicy] = { name: policy for name, policy in self._policy_dict.items() if isinstance(policy, RLPolicy) } - self._agent2policy = agent2policy - self._agent_wrapper = agent_wrapper_cls(self._policy_dict, agent2policy) - if trainable_policies is None: - self._trainable_policies = set(self._policy_dict.keys()) - else: - self._trainable_policies = set(trainable_policies) + self._agent2policy = rl_component_bundle.agent2policy + self._agent_wrapper = self._agent_wrapper_cls(self._policy_dict, self._agent2policy) + self._trainable_policies = set(rl_component_bundle.trainable_policy_names) self._trainable_agents = { agent_id for agent_id, policy_name in self._agent2policy.items() if policy_name in self._trainable_policies } @@ -251,18 +264,8 @@ def __init__( assert all([policy_name in self._rl_policy_dict for policy_name in self._trainable_policies]), \ "All trainable policies must be RL policies!" - # Global state & agent state - self._state: Optional[np.ndarray] = None - self._agent_state_dict: Dict[Any, np.ndarray] = {} - - self._trans_cache: Deque[CacheElement] = deque() - self._reward_eval_delay = reward_eval_delay - - self._info = {} - - @property - def rl_policy_dict(self) -> Dict[str, RLPolicy]: - return self._rl_policy_dict + def assign_policy_to_device(self, policy_name: str, device: torch.device) -> None: + self._rl_policy_dict[policy_name].to_device(device) def _get_global_and_agent_state( self, event: object, tick: int = None, @@ -329,11 +332,11 @@ def _reset(self) -> None: _, self._event, _ = self._env.step(None) self._state, self._agent_state_dict = self._get_global_and_agent_state(self._event) - def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Optional[int] = None) -> dict: + def sample(self, policy_state: Optional[Dict[str, dict]] = None, num_steps: Optional[int] = None) -> dict: """Sample experiences. Args: - policy_state (Dict[str, object]): Policy state dict. If it is not None, then we need to update all + policy_state (Dict[str, dict]): Policy state dict. If it is not None, then we need to update all policies according to the latest policy states, then start the experience collection. num_steps (Optional[int], default=None): Number of collecting steps. If it is None, interactions with the environment will continue until the terminal state is reached. @@ -443,11 +446,11 @@ def _post_polish_experiences(self, experiences: List[ExpElement]) -> List[ExpEle latest_agent_state_dict.update(experiences[i].agent_state_dict) return experiences - def set_policy_state(self, policy_state_dict: Dict[str, object]) -> None: + def set_policy_state(self, policy_state_dict: Dict[str, dict]) -> None: """Set policies' states. Args: - policy_state_dict (Dict[str, object]): Double-deck dict with format: {policy_name: policy_state}. + policy_state_dict (Dict[str, dict]): Double-deck dict with format: {policy_name: policy_state}. """ self._agent_wrapper.set_policy_state(policy_state_dict) @@ -465,7 +468,7 @@ def load_policy_state(self, path: str) -> List[str]: return loaded - def eval(self, policy_state: Dict[str, object] = None) -> dict: + def eval(self, policy_state: Dict[str, dict] = None) -> dict: self._env = self._test_env self._reset() if policy_state is not None: @@ -511,3 +514,11 @@ def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> N @abstractmethod def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: raise NotImplementedError + + def post_collect(self, info_list: list, ep: int) -> None: + """Routines to be invoked at the end of training episodes""" + pass + + def post_evaluate(self, info_list: list, ep: int) -> None: + """Routines to be invoked at the end of evaluation episodes""" + pass diff --git a/maro/rl/rollout/worker.py b/maro/rl/rollout/worker.py index 6c9522e6c..db1f3fea8 100644 --- a/maro/rl/rollout/worker.py +++ b/maro/rl/rollout/worker.py @@ -1,13 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable +from __future__ import annotations + +import typing from maro.rl.distributed import AbsWorker from maro.rl.utils.common import bytes_to_pyobj, pyobj_to_bytes from maro.utils import LoggerV2 -from .env_sampler import AbsEnvSampler +if typing.TYPE_CHECKING: + from maro.rl.rl_component.rl_component_bundle import RLComponentBundle class RolloutWorker(AbsWorker): @@ -16,8 +19,7 @@ class RolloutWorker(AbsWorker): Args: idx (int): Integer identifier for the worker. It is used to generate an internal ID, "worker.{idx}", so that the parallel roll-out controller can keep track of its connection status. - env_sampler_creator (Callable[[dict], AbsEnvSampler]): User-defined function to create an ``AbsEnvSampler`` - for roll-out purposes. + rl_component_bundle (RLComponentBundle): The RL component bundle of the job. producer_host (str): IP address of the parallel task controller host to connect to. producer_port (int, default=20000): Port of the parallel task controller host to connect to. logger (LoggerV2, default=None): The logger of the workflow. @@ -26,7 +28,7 @@ class RolloutWorker(AbsWorker): def __init__( self, idx: int, - env_sampler_creator: Callable[[], AbsEnvSampler], + rl_component_bundle: RLComponentBundle, producer_host: str, producer_port: int = 20000, logger: LoggerV2 = None, @@ -34,7 +36,7 @@ def __init__( super(RolloutWorker, self).__init__( idx=idx, producer_host=producer_host, producer_port=producer_port, logger=logger, ) - self._env_sampler = env_sampler_creator() + self._env_sampler = rl_component_bundle.env_sampler def _compute(self, msg: list) -> None: """Perform a full or partial episode of roll-out for sampling or evaluation. diff --git a/maro/rl/training/__init__.py b/maro/rl/training/__init__.py index 54ed5d0b4..d08313e23 100644 --- a/maro/rl/training/__init__.py +++ b/maro/rl/training/__init__.py @@ -3,7 +3,7 @@ from .proxy import TrainingProxy from .replay_memory import FIFOMultiReplayMemory, FIFOReplayMemory, RandomMultiReplayMemory, RandomReplayMemory -from .train_ops import AbsTrainOps, RemoteOps, remote +from .train_ops import AbsTrainOps, remote, RemoteOps from .trainer import AbsTrainer, MultiAgentTrainer, SingleAgentTrainer, TrainerParams from .training_manager import TrainingManager from .worker import TrainOpsWorker diff --git a/maro/rl/training/algorithms/ac.py b/maro/rl/training/algorithms/ac.py index 0740d8a2f..2f9d576e2 100644 --- a/maro/rl/training/algorithms/ac.py +++ b/maro/rl/training/algorithms/ac.py @@ -12,6 +12,7 @@ class ActorCriticParams(ACBasedParams): """Identical to `ACBasedParams`. Please refer to the doc string of `ACBasedParams` for detailed information. """ + def extract_ops_params(self) -> Dict[str, object]: return { "get_v_critic_net_func": self.get_v_critic_net_func, @@ -32,5 +33,6 @@ class ActorCriticTrainer(ACBasedTrainer): Reference: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/vpg """ + def __init__(self, name: str, params: ActorCriticParams) -> None: super(ActorCriticTrainer, self).__init__(name, params) diff --git a/maro/rl/training/algorithms/base/ac_ppo_base.py b/maro/rl/training/algorithms/base/ac_ppo_base.py index f5d7a24c6..d96a8b3b8 100644 --- a/maro/rl/training/algorithms/base/ac_ppo_base.py +++ b/maro/rl/training/algorithms/base/ac_ppo_base.py @@ -41,10 +41,11 @@ class ACBasedParams(TrainerParams, metaclass=ABCMeta): class ACBasedOps(AbsTrainOps): """Base class of Actor-Critic algorithm implementation. Reference: https://tinyurl.com/2ezte4cr """ + def __init__( self, name: str, - policy_creator: Callable[[str], RLPolicy], + policy_creator: Callable[[], RLPolicy], get_v_critic_net_func: Callable[[], VNet], parallelism: int = 1, reward_discount: float = 0.9, @@ -72,38 +73,41 @@ def __init__( self._device = None - def pre_set_batch(self, batch: TransitionBatch) -> None: - self._states = ndarray_to_tensor(batch.states, device=self._device) - self._returns = ndarray_to_tensor(batch.returns, device=self._device) - self._actions = ndarray_to_tensor(batch.actions, device=self._device) - self._advantages = ndarray_to_tensor(batch.advantages, device=self._device) - self._logps_old = ndarray_to_tensor(batch.old_logps, device=self._device) - if self._is_discrete_action: - self._actions = self._actions.long() - - def _get_critic_loss(self) -> torch.Tensor: + def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: """Compute the critic loss of the batch. + Args: + batch (TransitionBatch): Batch. + Returns: loss (torch.Tensor): The critic loss of the batch. """ + states = ndarray_to_tensor(batch.states, device=self._device) + returns = ndarray_to_tensor(batch.returns, device=self._device) + self._v_critic_net.train() - state_values = self._v_critic_net.v_values(self._states) - return self._critic_loss_func(state_values, self._returns) + state_values = self._v_critic_net.v_values(states) + return self._critic_loss_func(state_values, returns) @remote - def get_critic_grad(self) -> Dict[str, torch.Tensor]: + def get_critic_grad(self, batch: TransitionBatch) -> Dict[str, torch.Tensor]: """Compute the critic network's gradients of a batch. + Args: + batch (TransitionBatch): Batch. + Returns: grad (torch.Tensor): The critic gradient of the batch. """ - return self._v_critic_net.get_gradients(self._get_critic_loss()) + return self._v_critic_net.get_gradients(self._get_critic_loss(batch)) - def update_critic(self) -> None: + def update_critic(self, batch: TransitionBatch) -> None: """Update the critic network using a batch. + + Args: + batch (TransitionBatch): Batch. """ - self._v_critic_net.step(self._get_critic_loss()) + self._v_critic_net.step(self._get_critic_loss(batch)) def update_critic_with_grad(self, grad_dict: dict) -> None: """Update the critic network with remotely computed gradients. @@ -114,9 +118,12 @@ def update_critic_with_grad(self, grad_dict: dict) -> None: self._v_critic_net.train() self._v_critic_net.apply_gradients(grad_dict) - def _get_actor_loss(self) -> Tuple[torch.Tensor, bool]: + def _get_actor_loss(self, batch: TransitionBatch) -> Tuple[torch.Tensor, bool]: """Compute the actor loss of the batch. + Args: + batch (TransitionBatch): Batch. + Returns: loss (torch.Tensor): The actor loss of the batch. early_stop (bool): Early stop indicator. @@ -124,48 +131,65 @@ def _get_actor_loss(self) -> Tuple[torch.Tensor, bool]: assert isinstance(self._policy, DiscretePolicyGradient) or isinstance(self._policy, ContinuousRLPolicy) self._policy.train() - logps = self._policy.get_states_actions_logps(self._states, self._actions) + states = ndarray_to_tensor(batch.states, device=self._device) + actions = ndarray_to_tensor(batch.actions, device=self._device) + advantages = ndarray_to_tensor(batch.advantages, device=self._device) + logps_old = ndarray_to_tensor(batch.old_logps, device=self._device) + if self._is_discrete_action: + actions = actions.long() + + logps = self._policy.get_states_actions_logps(states, actions) if self._clip_ratio is not None: - ratio = torch.exp(logps - self._logps_old) - kl = (self._logps_old - logps).mean().item() + ratio = torch.exp(logps - logps_old) + kl = (logps_old - logps).mean().item() early_stop = (kl >= 0.01 * 1.5) # TODO clipped_ratio = torch.clamp(ratio, 1 - self._clip_ratio, 1 + self._clip_ratio) - actor_loss = -(torch.min(ratio * self._advantages, clipped_ratio * self._advantages)).mean() + actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).mean() else: - actor_loss = -(logps * self._advantages).mean() # I * delta * log pi(a|s) + actor_loss = -(logps * advantages).mean() # I * delta * log pi(a|s) early_stop = False return actor_loss, early_stop @remote - def get_actor_grad(self) -> Tuple[Dict[str, torch.Tensor], bool]: + def get_actor_grad(self, batch: TransitionBatch) -> Tuple[Dict[str, torch.Tensor], bool]: """Compute the actor network's gradients of a batch. + Args: + batch (TransitionBatch): Batch. + Returns: grad (torch.Tensor): The actor gradient of the batch. early_stop (bool): Early stop indicator. """ - loss, early_stop = self._get_actor_loss() + loss, early_stop = self._get_actor_loss(batch) return self._policy.get_gradients(loss), early_stop - def update_actor(self) -> bool: + def update_actor(self, batch: TransitionBatch) -> bool: """Update the actor network using a batch. + Args: + batch (TransitionBatch): Batch. + Returns: early_stop (bool): Early stop indicator. """ - loss, early_stop = self._get_actor_loss() + loss, early_stop = self._get_actor_loss(batch) self._policy.train_step(loss) return early_stop - def update_actor_with_grad(self, grad_dict: dict) -> None: + def update_actor_with_grad(self, grad_dict_and_early_stop: Tuple[dict, bool]) -> bool: """Update the actor network with remotely computed gradients. Args: - grad_dict (dict): Gradients. + grad_dict_and_early_stop (Tuple[dict, bool]): Gradients and early stop indicator. + + Returns: + early stop indicator """ self._policy.train() - self._policy.apply_gradients(grad_dict) + self._policy.apply_gradients(grad_dict_and_early_stop[0]) + return grad_dict_and_early_stop[1] def get_non_policy_state(self) -> dict: return { @@ -258,18 +282,22 @@ def _get_batch(self) -> TransitionBatch: def train_step(self) -> None: assert isinstance(self._ops, ACBasedOps) - self._ops.pre_set_batch(self._get_batch()) + batch = self._get_batch() for _ in range(self._params.grad_iters): - self._ops.update_critic() + self._ops.update_critic(batch) for _ in range(self._params.grad_iters): - early_stop = self._ops.update_actor() + early_stop = self._ops.update_actor(batch) if early_stop: break async def train_step_as_task(self) -> None: assert isinstance(self._ops, RemoteOps) - self._ops.pre_set_batch(self._get_batch()) + + batch = self._get_batch() + for _ in range(self._params.grad_iters): + self._ops.update_critic_with_grad(await self._ops.get_critic_grad(batch)) + for _ in range(self._params.grad_iters): - self._ops.update_critic_with_grad(await self._ops.get_critic_grad()) - self._ops.update_actor_with_grad(await self._ops.get_actor_grad()) + if self._ops.update_actor_with_grad(await self._ops.get_actor_grad(batch)): # early stop + break diff --git a/maro/rl/training/algorithms/dqn.py b/maro/rl/training/algorithms/dqn.py index 209e09f5d..bd9577be0 100644 --- a/maro/rl/training/algorithms/dqn.py +++ b/maro/rl/training/algorithms/dqn.py @@ -1,16 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -import collections from dataclasses import dataclass -from typing import Callable, Dict, List, Optional +from typing import Callable, Dict -import numpy as np import torch from maro.rl.policy import RLPolicy, ValueBasedPolicy -from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, RandomReplayMemory, RemoteOps, SingleAgentTrainer, TrainerParams, remote -from maro.rl.utils import TransitionBatch, get_torch_device, ndarray_to_tensor +from maro.rl.training import AbsTrainOps, RandomReplayMemory, remote, RemoteOps, SingleAgentTrainer, TrainerParams +from maro.rl.utils import get_torch_device, ndarray_to_tensor, TransitionBatch from maro.utils import clone @@ -48,7 +45,7 @@ class DQNOps(AbsTrainOps): def __init__( self, name: str, - policy_creator: Callable[[str], RLPolicy], + policy_creator: Callable[[], RLPolicy], parallelism: int = 1, reward_discount: float = 0.9, soft_update_coef: float = 0.1, diff --git a/maro/rl/training/algorithms/maddpg.py b/maro/rl/training/algorithms/maddpg.py index 042d8fb84..f0e626c05 100644 --- a/maro/rl/training/algorithms/maddpg.py +++ b/maro/rl/training/algorithms/maddpg.py @@ -10,10 +10,10 @@ import torch from maro.rl.model import MultiQNet -from maro.rl.policy import DiscretePolicyGradient +from maro.rl.policy import DiscretePolicyGradient, RLPolicy from maro.rl.rollout import ExpElement -from maro.rl.training import AbsTrainOps, MultiAgentTrainer, RandomMultiReplayMemory, RemoteOps, TrainerParams, remote -from maro.rl.utils import MultiTransitionBatch, get_torch_device, ndarray_to_tensor +from maro.rl.training import AbsTrainOps, MultiAgentTrainer, RandomMultiReplayMemory, remote, RemoteOps, TrainerParams +from maro.rl.utils import get_torch_device, MultiTransitionBatch, ndarray_to_tensor from maro.rl.utils.objects import FILE_SUFFIX from maro.utils import clone @@ -56,7 +56,7 @@ class DiscreteMADDPGOps(AbsTrainOps): def __init__( self, name: str, - policy_creator: Callable[[str], DiscretePolicyGradient], + policy_creator: Callable[[], RLPolicy], get_q_critic_net_func: Callable[[], MultiQNet], policy_idx: int, parallelism: int = 1, @@ -296,6 +296,7 @@ class DiscreteMADDPGTrainer(MultiAgentTrainer): See https://arxiv.org/abs/1706.02275 for details. """ + def __init__(self, name: str, params: DiscreteMADDPGParams) -> None: super(DiscreteMADDPGTrainer, self).__init__(name, params) self._params = params diff --git a/maro/rl/training/algorithms/ppo.py b/maro/rl/training/algorithms/ppo.py index 92c5879c0..c0f10452a 100644 --- a/maro/rl/training/algorithms/ppo.py +++ b/maro/rl/training/algorithms/ppo.py @@ -39,5 +39,6 @@ class PPOTrainer(ACBasedTrainer): References: https://github.com/openai/spinningup/tree/master/spinup/algos/pytorch/ppo. """ + def __init__(self, name: str, params: PPOParams) -> None: super(PPOTrainer, self).__init__(name, params) diff --git a/maro/rl/training/replay_memory.py b/maro/rl/training/replay_memory.py index e797834ad..0fb6da63d 100644 --- a/maro/rl/training/replay_memory.py +++ b/maro/rl/training/replay_memory.py @@ -6,7 +6,7 @@ import numpy as np -from maro.rl.utils import SHAPE_CHECK_FLAG, MultiTransitionBatch, TransitionBatch, match_shape +from maro.rl.utils import match_shape, MultiTransitionBatch, SHAPE_CHECK_FLAG, TransitionBatch class AbsIndexScheduler(object, metaclass=ABCMeta): diff --git a/maro/rl/training/train_ops.py b/maro/rl/training/train_ops.py index 6d60f93a5..ddc306edd 100644 --- a/maro/rl/training/train_ops.py +++ b/maro/rl/training/train_ops.py @@ -19,14 +19,14 @@ class AbsTrainOps(object, metaclass=ABCMeta): Args: name (str): Name of the ops. This is usually a policy name. - policy_creator (Callable[[str], RLPolicy]): Function to create a policy instance. + policy_creator (Callable[[], RLPolicy]): Function to create a policy instance. parallelism (int, default=1): Desired degree of data parallelism. """ def __init__( self, name: str, - policy_creator: Callable[[str], RLPolicy], + policy_creator: Callable[[], RLPolicy], parallelism: int = 1, ) -> None: super(AbsTrainOps, self).__init__() @@ -34,7 +34,7 @@ def __init__( self._policy_creator = policy_creator # Create the policy. if self._policy_creator: - self._policy = self._policy_creator(self._name) + self._policy = self._policy_creator() self._parallelism = parallelism diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index 4f88abbc9..3dcb9b9fe 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -11,13 +11,12 @@ from maro.rl.policy import AbsPolicy, RLPolicy from maro.rl.rollout import ExpElement +from maro.rl.utils import TransitionBatch from maro.rl.utils.objects import FILE_SUFFIX from maro.utils import LoggerV2 from .replay_memory import ReplayMemory from .train_ops import AbsTrainOps, RemoteOps -from .utils import extract_trainer_name -from ..utils import TransitionBatch @dataclass @@ -83,29 +82,32 @@ def agent_num(self) -> int: def register_logger(self, logger: LoggerV2) -> None: self._logger = logger - def register_agent2policy(self, agent2policy: Dict[Any, str]) -> None: + def register_agent2policy(self, agent2policy: Dict[Any, str], policy_trainer_mapping: Dict[str, str]) -> None: """Register the agent to policy dict that correspond to the current trainer. A valid policy name should start with the name of its trainer. For example, "DQN.POLICY_NAME". Therefore, we could identify which policies should be registered to the current trainer according to the policy's name. Args: agent2policy (Dict[Any, str]): Agent name to policy name mapping. + policy_trainer_mapping (Dict[str, str]): Policy name to trainer name mapping. """ self._agent2policy = { agent_name: policy_name for agent_name, policy_name in agent2policy.items() - if extract_trainer_name(policy_name) == self.name + if policy_trainer_mapping[policy_name] == self.name } @abstractmethod def register_policy_creator( self, - global_policy_creator: Dict[str, Callable[[str], AbsPolicy]], + global_policy_creator: Dict[str, Callable[[], AbsPolicy]], + policy_trainer_mapping: Dict[str, str], ) -> None: """Register the policy creator. Only keep the creators of the policies that the current trainer need to train. Args: - global_policy_creator (Dict[str, Callable[[str], AbsPolicy]]): Dict that contains the creators for all + global_policy_creator (Dict[str, Callable[[], AbsPolicy]]): Dict that contains the creators for all policies. + policy_trainer_mapping (Dict[str, str]): Policy name to trainer name mapping. """ raise NotImplementedError @@ -180,10 +182,13 @@ def ops(self): def register_policy_creator( self, - global_policy_creator: Dict[str, Callable[[str], AbsPolicy]], + global_policy_creator: Dict[str, Callable[[], AbsPolicy]], + policy_trainer_mapping: Dict[str, str], ) -> None: policy_names = [ - policy_name for policy_name in global_policy_creator if extract_trainer_name(policy_name) == self.name + policy_name + for policy_name in global_policy_creator + if policy_trainer_mapping[policy_name] == self.name ] if len(policy_names) != 1: raise ValueError(f"Trainer {self._name} should have exactly one policy assigned to it") @@ -280,7 +285,7 @@ class MultiAgentTrainer(AbsTrainer, metaclass=ABCMeta): def __init__(self, name: str, params: TrainerParams) -> None: super(MultiAgentTrainer, self).__init__(name, params) - self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = {} + self._policy_creator: Dict[str, Callable[[], RLPolicy]] = {} self._policy_names: List[str] = [] self._ops_dict: Dict[str, AbsTrainOps] = {} @@ -290,11 +295,12 @@ def ops_dict(self): def register_policy_creator( self, - global_policy_creator: Dict[str, Callable[[str], AbsPolicy]], + global_policy_creator: Dict[str, Callable[[], AbsPolicy]], + policy_trainer_mapping: Dict[str, str], ) -> None: - self._policy_creator: Dict[str, Callable[[str], RLPolicy]] = { + self._policy_creator: Dict[str, Callable[[], RLPolicy]] = { policy_name: func for policy_name, func in global_policy_creator.items() - if extract_trainer_name(policy_name) == self.name + if policy_trainer_mapping[policy_name] == self.name } self._policy_names = list(self._policy_creator.keys()) diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/training_manager.py index b6d78d220..d2a675253 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/training_manager.py @@ -1,20 +1,24 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +from __future__ import annotations + import asyncio import collections import os +import typing from itertools import chain -from typing import Any, Callable, Dict, Iterable, List, Tuple +from typing import Any, Dict, Iterable, List, Tuple -from maro.rl.policy import AbsPolicy from maro.rl.rollout import ExpElement from maro.rl.training import SingleAgentTrainer from maro.utils import LoggerV2 from maro.utils.exception.rl_toolkit_exception import MissingTrainer from .trainer import AbsTrainer, MultiAgentTrainer -from .utils import extract_trainer_name + +if typing.TYPE_CHECKING: + from maro.rl.rl_component.rl_component_bundle import RLComponentBundle class TrainingManager(object): @@ -22,11 +26,8 @@ class TrainingManager(object): Training manager. Manage and schedule all trainers to train policies. Args: - policy_creator (Dict[str, Callable[[str], AbsPolicy]]): Dict of functions to create policies. - trainer_creator (Dict[str, Callable[[str], AbsTrainer]]): Dict of functions to create trainers. - agent2policy (Dict[Any, str]): Agent name to policy name mapping. - device_mapping (Dict[str, str], default={}): User-defined device mapping from policy name to pytorch - device name. + rl_component_bundle (RLComponentBundle): The RL component bundle of the job. + explicit_assign_device (bool): Whether to assign policy to its device in the training manager. proxy_address (Tuple[str, int], default=None): Address of the training proxy. If it is not None, it is registered to all trainers, which in turn create `RemoteOps` for distributed training. logger (LoggerV2, default=None): A logger for logging key events. @@ -34,32 +35,35 @@ class TrainingManager(object): def __init__( self, - policy_creator: Dict[str, Callable[[str], AbsPolicy]], - trainer_creator: Dict[str, Callable[[str], AbsTrainer]], - agent2policy: Dict[Any, str], # {agent_name: policy_name} - device_mapping: Dict[str, str] = None, + rl_component_bundle: RLComponentBundle, + explicit_assign_device: bool, proxy_address: Tuple[str, int] = None, logger: LoggerV2 = None, ) -> None: super(TrainingManager, self).__init__() self._trainer_dict: Dict[str, AbsTrainer] = {} - self._agent2policy = agent2policy self._proxy_address = proxy_address - for trainer_name, func in trainer_creator.items(): - trainer = func(trainer_name) + for trainer_name, func in rl_component_bundle.trainer_creator.items(): + trainer = func() if self._proxy_address: trainer.set_proxy_address(self._proxy_address) - trainer.register_agent2policy(self._agent2policy) - trainer.register_policy_creator(policy_creator) + trainer.register_agent2policy( + rl_component_bundle.trainable_agent2policy, + rl_component_bundle.policy_trainer_mapping, + ) + trainer.register_policy_creator( + rl_component_bundle.trainable_policy_creator, + rl_component_bundle.policy_trainer_mapping, + ) trainer.register_logger(logger) trainer.build() # `build()` must be called after `register_policy_creator()` self._trainer_dict[trainer_name] = trainer # User-defined allocation of compute devices, i.e., GPU's to the trainer ops - if device_mapping is not None: - for policy_name, device_name in device_mapping.items(): - trainer = self._trainer_dict[extract_trainer_name(policy_name)] + if explicit_assign_device: + for policy_name, device_name in rl_component_bundle.device_mapping.items(): + trainer = self._trainer_dict[rl_component_bundle.policy_trainer_mapping[policy_name]] if isinstance(trainer, SingleAgentTrainer): ops = trainer.ops @@ -69,8 +73,8 @@ def __init__( ops.to_device(device_name) self._agent2trainer: Dict[Any, str] = {} - for agent_name, policy_name in self._agent2policy.items(): - trainer_name = extract_trainer_name(policy_name) + for agent_name, policy_name in rl_component_bundle.trainable_agent2policy.items(): + trainer_name = rl_component_bundle.policy_trainer_mapping[policy_name] if trainer_name not in self._trainer_dict: raise MissingTrainer(f"trainer {trainer_name} does not exist") self._agent2trainer[agent_name] = trainer_name diff --git a/maro/rl/training/utils.py b/maro/rl/training/utils.py deleted file mode 100644 index 4e1d5bdf3..000000000 --- a/maro/rl/training/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import os - - -def extract_trainer_name(policy_name: str) -> str: - """Extract the trainer name from the policy name. - - Args: - policy_name (str): Policy name. - - Returns: - trainer_name (str) - """ - return policy_name.split(".")[0] - - -def get_latest_ep(path: str) -> int: - ep_list = [int(ep) for ep in os.listdir(path)] - return max(ep_list) diff --git a/maro/rl/training/worker.py b/maro/rl/training/worker.py index 6536f0ad5..f24d69cbf 100644 --- a/maro/rl/training/worker.py +++ b/maro/rl/training/worker.py @@ -1,10 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from typing import Callable, Dict +from __future__ import annotations + +import typing +from typing import Dict from maro.rl.distributed import AbsWorker -from maro.rl.policy import AbsPolicy from maro.rl.training import SingleAgentTrainer from maro.rl.utils.common import bytes_to_pyobj, bytes_to_string, pyobj_to_bytes from maro.utils import LoggerV2 @@ -12,6 +14,9 @@ from .train_ops import AbsTrainOps from .trainer import AbsTrainer, MultiAgentTrainer +if typing.TYPE_CHECKING: + from maro.rl.rl_component.rl_component_bundle import RLComponentBundle + class TrainOpsWorker(AbsWorker): """Worker that executes methods defined in a subclass of ``AbsTrainOps`` and annotated as "remote" on demand. @@ -19,10 +24,7 @@ class TrainOpsWorker(AbsWorker): Args: idx (int): Integer identifier for the worker. It is used to generate an internal ID, "worker.{idx}", so that the proxy can keep track of its connection status. - policy_creator (Dict[str, Callable[[str], AbsPolicy]]): User-defined function registry that can be used to - create an "AbsPolicy" instance with a name in the registry. This is required to create train ops instances. - trainer_creator (Dict[str, Callable[[str], AbsTrainer]]): User-defined function registry that can be used to - create an "AbsTrainer" instance with a name in the registry. This is required to create train ops instances. + rl_component_bundle (RLComponentBundle): The RL component bundle of the job. producer_host (str): IP address of the proxy host to connect to. producer_port (int, default=10001): Port of the proxy host to connect to. """ @@ -30,8 +32,7 @@ class TrainOpsWorker(AbsWorker): def __init__( self, idx: int, - policy_creator: Dict[str, Callable[[str], AbsPolicy]], - trainer_creator: Dict[str, Callable[[str], AbsTrainer]], + rl_component_bundle: RLComponentBundle, producer_host: str, producer_port: int = 10001, logger: LoggerV2 = None, @@ -40,8 +41,7 @@ def __init__( idx=idx, producer_host=producer_host, producer_port=producer_port, logger=logger, ) - self._policy_creator = policy_creator - self._trainer_creator = trainer_creator + self._rl_component_bundle = rl_component_bundle self._trainer_dict: Dict[str, AbsTrainer] = {} self._ops_dict: Dict[str, AbsTrainOps] = {} @@ -62,8 +62,11 @@ def _compute(self, msg: list) -> None: if ops_name not in self._ops_dict: trainer_name = ops_name.split(".")[0] if trainer_name not in self._trainer_dict: - trainer = self._trainer_creator[trainer_name](trainer_name) - trainer.register_policy_creator(self._policy_creator) + trainer = self._rl_component_bundle.trainer_creator[trainer_name]() + trainer.register_policy_creator( + self._rl_component_bundle.trainable_policy_creator, + self._rl_component_bundle.policy_trainer_mapping, + ) self._trainer_dict[trainer_name] = trainer trainer = self._trainer_dict[trainer_name] diff --git a/maro/rl/utils/__init__.py b/maro/rl/utils/__init__.py index 1034ac872..df0917dcd 100644 --- a/maro/rl/utils/__init__.py +++ b/maro/rl/utils/__init__.py @@ -6,7 +6,7 @@ from .objects import SHAPE_CHECK_FLAG from .torch_utils import average_grads, get_torch_device, match_shape, ndarray_to_tensor from .trajectory_computation import discount_cumsum -from .transition_batch import MultiTransitionBatch, TransitionBatch, merge_transition_batches +from .transition_batch import merge_transition_batches, MultiTransitionBatch, TransitionBatch AbsTransitionBatch = Union[TransitionBatch, MultiTransitionBatch] diff --git a/maro/rl/utils/training.py b/maro/rl/utils/training.py new file mode 100644 index 000000000..ef208bec4 --- /dev/null +++ b/maro/rl/utils/training.py @@ -0,0 +1,6 @@ +import os + + +def get_latest_ep(path: str) -> int: + ep_list = [int(ep) for ep in os.listdir(path)] + return max(ep_list) diff --git a/maro/rl/workflows/main.py b/maro/rl/workflows/main.py index 2eeeaa973..f0eebc01b 100644 --- a/maro/rl/workflows/main.py +++ b/maro/rl/workflows/main.py @@ -1,16 +1,18 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. import argparse +import importlib import os +import sys import time -from typing import List +from typing import List, Type +from maro.rl.rl_component.rl_component_bundle import RLComponentBundle from maro.rl.rollout import BatchEnvSampler, ExpElement from maro.rl.training import TrainingManager -from maro.rl.training.utils import get_latest_ep from maro.rl.utils import get_torch_device from maro.rl.utils.common import float_or_none, get_env, int_or_none, list_or_none -from maro.rl.workflows.scenario import Scenario +from maro.rl.utils.training import get_latest_ep from maro.utils import LoggerV2 @@ -20,14 +22,14 @@ def get_args() -> argparse.Namespace: return parser.parse_args() -def main(scenario: Scenario, args: argparse.Namespace) -> None: +def main(rl_component_bundle: RLComponentBundle, args: argparse.Namespace) -> None: if args.evaluate_only: - evaluate_only_workflow(scenario) + evaluate_only_workflow(rl_component_bundle) else: - training_workflow(scenario) + training_workflow(rl_component_bundle) -def training_workflow(scenario: Scenario) -> None: +def training_workflow(rl_component_bundle: RLComponentBundle) -> None: num_episodes = int(get_env("NUM_EPISODES")) num_steps = int_or_none(get_env("NUM_STEPS", required=False)) min_n_sample = int_or_none(get_env("MIN_N_SAMPLE")) @@ -46,16 +48,9 @@ def training_workflow(scenario: Scenario) -> None: parallel_rollout = env_sampling_parallelism is not None or env_eval_parallelism is not None train_mode = get_env("TRAIN_MODE") - agent2policy = scenario.agent2policy - policy_creator = scenario.policy_creator - trainer_creator = scenario.trainer_creator is_single_thread = train_mode == "simple" and not parallel_rollout if is_single_thread: - # If running in single thread mode, create policy instances here and reuse then in rollout and training. - # In other words, `policy_creator` will return a policy instance that has been already created in advance - # instead of create a new policy instance. - policy_dict = {name: get_policy_func(name) for name, get_policy_func in policy_creator.items()} - policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict} + rl_component_bundle.pre_create_policy_instances() if parallel_rollout: env_sampler = BatchEnvSampler( @@ -67,28 +62,19 @@ def training_workflow(scenario: Scenario) -> None: logger=logger, ) else: - env_sampler = scenario.env_sampler_creator(policy_creator) + env_sampler = rl_component_bundle.env_sampler if train_mode != "simple": - for policy_name, device_name in scenario.device_mapping.items(): - env_sampler.rl_policy_dict[policy_name].to_device(get_torch_device(device_name)) + for policy_name, device_name in rl_component_bundle.device_mapping.items(): + env_sampler.assign_policy_to_device(policy_name, get_torch_device(device_name)) # evaluation schedule eval_schedule = list_or_none(get_env("EVAL_SCHEDULE", required=False)) logger.info(f"Policy will be evaluated at the end of episodes {eval_schedule}") eval_point_index = 0 - if scenario.trainable_policies is None: - trainable_policies = set(policy_creator.keys()) - else: - trainable_policies = set(scenario.trainable_policies) - - trainable_policy_creator = {name: func for name, func in policy_creator.items() if name in trainable_policies} - trainable_agent2policy = {id_: name for id_, name in agent2policy.items() if name in trainable_policies} training_manager = TrainingManager( - policy_creator=trainable_policy_creator, - trainer_creator=trainer_creator, - agent2policy=trainable_agent2policy, - device_mapping=scenario.device_mapping if train_mode == "simple" else {}, + rl_component_bundle=rl_component_bundle, + explicit_assign_device=(train_mode == "simple"), proxy_address=None if train_mode == "simple" else ( get_env("TRAIN_PROXY_HOST"), int(get_env("TRAIN_PROXY_FRONTEND_PORT")) ), @@ -136,8 +122,7 @@ def training_workflow(scenario: Scenario) -> None: collect_time += time.time() - tc0 - if scenario.post_collect: - scenario.post_collect(total_info_list, ep, -1) # TODO + env_sampler.post_collect(total_info_list, ep) logger.info(f"Roll-out completed for episode {ep}. Training started...") tu0 = time.time() @@ -157,15 +142,14 @@ def training_workflow(scenario: Scenario) -> None: result = env_sampler.eval( policy_state=training_manager.get_policy_state() if not is_single_thread else None ) - if scenario.post_evaluate: - scenario.post_evaluate(result["info"], ep) + env_sampler.post_evaluate(result["info"], ep) if isinstance(env_sampler, BatchEnvSampler): env_sampler.exit() training_manager.exit() -def evaluate_only_workflow(scenario: Scenario) -> None: +def evaluate_only_workflow(rl_component_bundle: RLComponentBundle) -> None: logger = LoggerV2( "MAIN", dump_path=get_env("LOG_PATH"), @@ -179,7 +163,6 @@ def evaluate_only_workflow(scenario: Scenario) -> None: env_eval_parallelism = int_or_none(get_env("ENV_EVAL_PARALLELISM", required=False)) parallel_rollout = env_sampling_parallelism is not None or env_eval_parallelism is not None - policy_creator = scenario.policy_creator if parallel_rollout: env_sampler = BatchEnvSampler( sampling_parallelism=env_sampling_parallelism, @@ -190,7 +173,7 @@ def evaluate_only_workflow(scenario: Scenario) -> None: logger=logger, ) else: - env_sampler = scenario.env_sampler_creator(policy_creator) + env_sampler = rl_component_bundle.env_sampler load_path = get_env("LOAD_PATH", required=False) load_episode = int_or_none(get_env("LOAD_EPISODE", required=False)) @@ -204,14 +187,18 @@ def evaluate_only_workflow(scenario: Scenario) -> None: logger.info(f"Loaded policies {loaded} into env sampler from {path}") result = env_sampler.eval() - if scenario.post_evaluate: - scenario.post_evaluate(result["info"], -1) + env_sampler.post_evaluate(result["info"], -1) if isinstance(env_sampler, BatchEnvSampler): env_sampler.exit() if __name__ == "__main__": - # get user-defined scenario ingredients - run_scenario = Scenario(get_env("SCENARIO_PATH")) - main(run_scenario, args=get_args()) + scenario_path = get_env("SCENARIO_PATH") + scenario_path = os.path.normpath(scenario_path) + sys.path.insert(0, os.path.dirname(scenario_path)) + module = importlib.import_module(os.path.basename(scenario_path)) + + rl_component_bundle_cls: Type[RLComponentBundle] = getattr(module, "rl_component_bundle_cls") + rl_component_bundle = rl_component_bundle_cls() + main(rl_component_bundle, args=get_args()) diff --git a/maro/rl/workflows/rollout_worker.py b/maro/rl/workflows/rollout_worker.py index 354141d4e..9e5ea24ce 100644 --- a/maro/rl/workflows/rollout_worker.py +++ b/maro/rl/workflows/rollout_worker.py @@ -1,16 +1,23 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import importlib +import os +import sys +from typing import Type -from functools import partial - +from maro.rl.rl_component.rl_component_bundle import RLComponentBundle from maro.rl.rollout import RolloutWorker from maro.rl.utils.common import get_env, int_or_none -from maro.rl.workflows.scenario import Scenario from maro.utils import LoggerV2 if __name__ == "__main__": - scenario = Scenario(get_env("SCENARIO_PATH")) - policy_creator = scenario.policy_creator + scenario_path = get_env("SCENARIO_PATH") + scenario_path = os.path.normpath(scenario_path) + sys.path.insert(0, os.path.dirname(scenario_path)) + module = importlib.import_module(os.path.basename(scenario_path)) + + rl_component_bundle_cls: Type[RLComponentBundle] = getattr(module, "rl_component_bundle_cls") + rl_component_bundle = rl_component_bundle_cls() worker_idx = int_or_none(get_env("ID")) logger = LoggerV2( @@ -22,7 +29,7 @@ ) worker = RolloutWorker( idx=worker_idx, - env_sampler_creator=partial(scenario.env_sampler_creator, policy_creator), + rl_component_bundle=rl_component_bundle, producer_host=get_env("ROLLOUT_CONTROLLER_HOST"), producer_port=int_or_none(get_env("ROLLOUT_CONTROLLER_PORT")), logger=logger, diff --git a/maro/rl/workflows/scenario.py b/maro/rl/workflows/scenario.py deleted file mode 100644 index d385bbff1..000000000 --- a/maro/rl/workflows/scenario.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import importlib -import os -import sys -from typing import Any, Callable, Dict, List - -from maro.rl.policy import AbsPolicy -from maro.rl.rollout import AbsEnvSampler -from maro.rl.training import AbsTrainer - - -class Scenario(object): - def __init__(self, path: str) -> None: - super(Scenario, self).__init__() - path = os.path.normpath(path) - sys.path.insert(0, os.path.dirname(path)) - self._module = importlib.import_module(os.path.basename(path)) - - @property - def env_sampler_creator(self) -> Callable[[Dict[str, Callable[[str], AbsPolicy]]], AbsEnvSampler]: - return getattr(self._module, "env_sampler_creator") - - @property - def agent2policy(self) -> Dict[Any, str]: - return getattr(self._module, "agent2policy") - - @property - def policy_creator(self) -> Dict[str, Callable[[str], AbsPolicy]]: - return getattr(self._module, "policy_creator") - - @property - def trainable_policies(self) -> List[str]: - return getattr(self._module, "trainable_policies", None) - - @property - def trainer_creator(self) -> Dict[str, Callable[[str], AbsTrainer]]: - return getattr(self._module, "trainer_creator") - - @property - def device_mapping(self) -> Dict[str, str]: - return getattr(self._module, "device_mapping", {}) - - @property - def post_collect(self) -> Callable[[list, int, int], None]: - return getattr(self._module, "post_collect", None) - - @property - def post_evaluate(self) -> Callable[[list, int], None]: - return getattr(self._module, "post_evaluate", None) diff --git a/maro/rl/workflows/train_worker.py b/maro/rl/workflows/train_worker.py index 15e271541..ace4e5fd4 100644 --- a/maro/rl/workflows/train_worker.py +++ b/maro/rl/workflows/train_worker.py @@ -1,13 +1,24 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. +import importlib +import os +import sys +from typing import Type +from maro.rl.rl_component.rl_component_bundle import RLComponentBundle from maro.rl.training import TrainOpsWorker from maro.rl.utils.common import get_env, int_or_none -from maro.rl.workflows.scenario import Scenario from maro.utils import LoggerV2 if __name__ == "__main__": - scenario_attr = Scenario(get_env("SCENARIO_PATH")) + scenario_path = get_env("SCENARIO_PATH") + scenario_path = os.path.normpath(scenario_path) + sys.path.insert(0, os.path.dirname(scenario_path)) + module = importlib.import_module(os.path.basename(scenario_path)) + + rl_component_bundle_cls: Type[RLComponentBundle] = getattr(module, "rl_component_bundle_cls") + rl_component_bundle = rl_component_bundle_cls() + worker_idx = int_or_none(get_env("ID")) logger = LoggerV2( f"TRAIN-WORKER.{worker_idx}", @@ -18,8 +29,7 @@ ) worker = TrainOpsWorker( idx=int_or_none(get_env("ID")), - policy_creator=scenario_attr.policy_creator, - trainer_creator=scenario_attr.trainer_creator, + rl_component_bundle=rl_component_bundle, producer_host=get_env("TRAIN_PROXY_HOST"), producer_port=int_or_none(get_env("TRAIN_PROXY_BACKEND_PORT")), logger=logger, From ae83ac0613fa1fbb7585903243f4e13fdf2b2db4 Mon Sep 17 00:00:00 2001 From: Chaos Yu Date: Mon, 16 May 2022 10:23:48 +0800 Subject: [PATCH 473/482] Add method to get mapping of available tick to frame index (#415) * add method to get mapping of available tick to frame index * fix lint issue * fix naming issue --- maro/simulator/abs_core.py | 8 ++++ maro/simulator/core.py | 8 ++++ .../scenarios/abs_business_engine.py | 20 +++++++++ tests/test_env.py | 41 ++++++++++++++++++- 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/maro/simulator/abs_core.py b/maro/simulator/abs_core.py index cdbe0362f..4244090d2 100644 --- a/maro/simulator/abs_core.py +++ b/maro/simulator/abs_core.py @@ -162,3 +162,11 @@ def get_pending_events(self, tick: int) -> list: tick (int): Specified tick. """ pass + + def get_ticks_frame_index_mapping(self) -> dict: + """Helper method to get current available ticks to related frame index mapping. + + Returns: + dict: Dictionary of avaliable tick to frame index, it would be 1 to N mapping if the resolution is not 1. + """ + pass diff --git a/maro/simulator/core.py b/maro/simulator/core.py index 35c01c6bc..d5f1b2305 100644 --- a/maro/simulator/core.py +++ b/maro/simulator/core.py @@ -205,6 +205,14 @@ def get_pending_events(self, tick) -> List[ActualEvent]: """ return self._event_buffer.get_pending_events(tick) + def get_ticks_frame_index_mapping(self) -> dict: + """Helper method to get current available ticks to related frame index mapping. + + Returns: + dict: Dictionary of avaliable tick to frame index, it would be 1 to N mapping if the resolution is not 1. + """ + return self._business_engine.get_ticks_frame_index_mapping() + def _init_business_engine(self) -> None: """Initialize business engine object. diff --git a/maro/simulator/scenarios/abs_business_engine.py b/maro/simulator/scenarios/abs_business_engine.py index 0ba670c37..44a6da8f4 100644 --- a/maro/simulator/scenarios/abs_business_engine.py +++ b/maro/simulator/scenarios/abs_business_engine.py @@ -85,6 +85,26 @@ def frame_index(self, tick: int) -> int: """ return tick_to_frame_index(self._start_tick, tick, self._snapshot_resolution) + def get_ticks_frame_index_mapping(self) -> dict: + """Helper method to get current available ticks to related frame index mapping. + + Returns: + dict: Dictionary of avaliable tick to frame index, it would be 1 to N mapping if the resolution is not 1. + """ + mapping = {} + + if self.snapshots is not None: + frame_index_list = self.snapshots.get_frame_index_list() + + for frame_index in frame_index_list: + frame_start_tick = self._start_tick + frame_index * self._snapshot_resolution + frame_end_tick = min(self._max_tick, frame_start_tick + self._snapshot_resolution) + + for tick in range(frame_start_tick, frame_end_tick): + mapping[tick] = frame_index + + return mapping + def calc_max_snapshots(self) -> int: """Helper method to calculate total snapshot should be in snapshot list with parameters passed via constructor. diff --git a/tests/test_env.py b/tests/test_env.py index 74cd37f40..be9f250ac 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -5,10 +5,11 @@ import unittest import numpy as np +from math import floor from .dummy.dummy_business_engine import DummyEngine from maro.simulator.utils import get_available_envs, get_scenarios, get_topologies -from maro.simulator.utils.common import frame_index_to_ticks +from maro.simulator.utils.common import frame_index_to_ticks, tick_to_frame_index from maro.simulator.core import BusinessEngineNotFoundError, Env from tests.utils import backends_to_test @@ -293,6 +294,44 @@ def test_frame_index_to_ticks(self): self.assertListEqual([0, 1], ticks[0]) self.assertListEqual([8, 9], ticks[4]) + def test_get_avalible_frame_index_to_ticks_with_default_resolution(self): + for backend_name in backends_to_test: + os.environ["DEFAULT_BACKEND_NAME"] = backend_name + + max_tick = 10 + + env = Env(scenario="cim", topology="tests/data/cim/customized_config", + start_tick=0, durations=max_tick) + + run_to_end(env) + + t2f_mapping = env.get_ticks_frame_index_mapping() + + # tick == frame index + self.assertListEqual([t for t in t2f_mapping.keys()], [t for t in range(max_tick)]) + self.assertListEqual([f for f in t2f_mapping.values()], [f for f in range(max_tick)]) + + def test_get_avalible_frame_index_to_ticks_with_resolution2(self): + for backend_name in backends_to_test: + os.environ["DEFAULT_BACKEND_NAME"] = backend_name + + max_tick = 10 + start_tick = 0 + resolution = 2 + + env = Env(scenario="cim", topology="tests/data/cim/customized_config", + start_tick=start_tick, durations=max_tick, snapshot_resolution=resolution) + + run_to_end(env) + + t2f_mapping = env.get_ticks_frame_index_mapping() + + self.assertListEqual([t for t in t2f_mapping.keys()], [t for t in range(max_tick)]) + + for t, v in t2f_mapping.items(): + v2 = tick_to_frame_index(start_tick, t, resolution) + self.assertEqual(v, v2) + if __name__ == "__main__": unittest.main() From 10b9c02df3560b3daca5ffaab097937b07268d1e Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 18 May 2022 15:13:04 +0800 Subject: [PATCH 474/482] Cherry pick from sc_refinement (#527) * Cherry pick from sc_refinement * Cherry pick from sc_refinement --- .gitignore | 7 +- examples/cim/rl/env_sampler.py | 6 +- examples/cim/rl/rl_component_bundle.py | 4 +- examples/vm_scheduling/rl/env_sampler.py | 6 +- maro/cli/k8s/aks/aks_commands.py | 317 +++++++ maro/cli/k8s/{test_conf.yml => aks/conf.yml} | 10 +- .../parameters.json} | 0 .../{test_template.json => aks/template.json} | 2 +- maro/cli/k8s/aks_commands.py | 851 ------------------ .../aks/create_aks_cluster/template.json | 4 +- maro/cli/k8s/utils/k8s_manifest_generator.py | 4 +- maro/cli/k8s/utils/{k8s.py => k8s_ops.py} | 0 maro/cli/local/commands.py | 11 +- maro/cli/local/job_manager.py | 7 +- maro/cli/local/utils.py | 79 +- maro/cli/maro.py | 18 +- maro/cli/utils/azure/deployment.py | 6 +- maro/cli/utils/azure/general.py | 3 + maro/rl/model/abs_net.py | 4 +- maro/rl/policy/discrete_rl_policy.py | 2 +- maro/rl/rl_component/rl_component_bundle.py | 20 +- maro/rl/rollout/env_sampler.py | 108 ++- maro/rl/training/algorithms/__init__.py | 4 +- .../training/algorithms/base/ac_ppo_base.py | 2 +- maro/rl/training/algorithms/ppo.py | 156 +++- maro/rl/training/trainer.py | 2 +- maro/rl/training/training_manager.py | 3 + maro/rl/workflows/config/parser.py | 105 ++- maro/utils/logger.py | 70 +- 29 files changed, 751 insertions(+), 1060 deletions(-) create mode 100644 maro/cli/k8s/aks/aks_commands.py rename maro/cli/k8s/{test_conf.yml => aks/conf.yml} (80%) rename maro/cli/k8s/{test_parameters.json => aks/parameters.json} (100%) rename maro/cli/k8s/{test_template.json => aks/template.json} (99%) delete mode 100644 maro/cli/k8s/aks_commands.py rename maro/cli/k8s/utils/{k8s.py => k8s_ops.py} (100%) diff --git a/.gitignore b/.gitignore index 45f04b666..4ec4f4bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.pyd *.log *.csv +*.parquet *.c *.cpp *.DS_Store @@ -12,16 +13,18 @@ .vs/ build/ log/ +logs/ checkpoint/ +checkpoints/ +streamit/ dist/ *.egg-info/ tools/schedule docs/_build -test/ -data/ .eggs/ maro_venv/ pyvenv.cfg htmlcov/ .coverage .coveragerc +.tmp/ diff --git a/examples/cim/rl/env_sampler.py b/examples/cim/rl/env_sampler.py index d55ad9da0..b02e09b39 100644 --- a/examples/cim/rl/env_sampler.py +++ b/examples/cim/rl/env_sampler.py @@ -74,11 +74,11 @@ def _get_reward(self, env_action_dict: Dict[Any, object], event: DecisionEvent, ) return {agent_id: reward for agent_id, reward in zip(ports, rewards)} - def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: + def _post_step(self, cache_element: CacheElement) -> None: self._info["env_metric"] = self._env.metrics - def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: - self._post_step(cache_element, reward) + def _post_eval_step(self, cache_element: CacheElement) -> None: + self._post_step(cache_element) def post_collect(self, info_list: list, ep: int) -> None: # print the env metric from each rollout worker diff --git a/examples/cim/rl/rl_component_bundle.py b/examples/cim/rl/rl_component_bundle.py index f3b452faf..3fe5aeaa7 100644 --- a/examples/cim/rl/rl_component_bundle.py +++ b/examples/cim/rl/rl_component_bundle.py @@ -1,7 +1,7 @@ from functools import partial from typing import Any, Callable, Dict, Optional -from examples.cim.rl.config import action_num, algorithm, env_conf, num_agents, state_dim +from examples.cim.rl.config import action_num, algorithm, env_conf, num_agents, reward_shaping_conf, state_dim from examples.cim.rl.env_sampler import CIMEnvSampler from maro.rl.policy import AbsPolicy from maro.rl.rl_component.rl_component_bundle import RLComponentBundle @@ -26,7 +26,7 @@ def get_test_env_config(self) -> Optional[dict]: return None def get_env_sampler(self) -> AbsEnvSampler: - return CIMEnvSampler(self.env, self.test_env) + return CIMEnvSampler(self.env, self.test_env, reward_eval_delay=reward_shaping_conf["time_window"]) def get_agent2policy(self) -> Dict[Any, str]: return {agent: f"{algorithm}_{agent}.policy"for agent in self.env.agent_idx_list} diff --git a/examples/vm_scheduling/rl/env_sampler.py b/examples/vm_scheduling/rl/env_sampler.py index cc5804e66..1913e3adb 100644 --- a/examples/vm_scheduling/rl/env_sampler.py +++ b/examples/vm_scheduling/rl/env_sampler.py @@ -115,7 +115,7 @@ def _get_allocation_reward(self, event: DecisionPayload, alpha: float, beta: flo ) return (alpha + beta * vm_unit_price * min(self._durations - event.frame_index, event.remaining_buffer_time)) - def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]): + def _post_step(self, cache_element: CacheElement) -> None: self._info["env_metric"] = {k: v for k, v in self._env.metrics.items() if k != "total_latency"} self._info["env_metric"]["latency_due_to_agent"] = self._env.metrics["total_latency"].due_to_agent self._info["env_metric"]["latency_due_to_resource"] = self._env.metrics["total_latency"].due_to_resource @@ -130,8 +130,8 @@ def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]): self._info["actions_by_core_requirement"][cache_element.event.vm_cpu_cores_requirement].append([action, mask]) self._info["action_sequence"].append(action) - def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: - self._post_step(cache_element, reward) + def _post_eval_step(self, cache_element: CacheElement) -> None: + self._post_step(cache_element) def post_collect(self, info_list: list, ep: int) -> None: # print the env metric from each rollout worker diff --git a/maro/cli/k8s/aks/aks_commands.py b/maro/cli/k8s/aks/aks_commands.py new file mode 100644 index 000000000..22637ca10 --- /dev/null +++ b/maro/cli/k8s/aks/aks_commands.py @@ -0,0 +1,317 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import base64 +import json +import os +import shutil +from os.path import abspath, dirname, expanduser, join + +import yaml + +from maro.cli.utils import docker as docker_utils +from maro.cli.utils.azure import storage as azure_storage_utils +from maro.cli.utils.azure.aks import attach_acr +from maro.cli.utils.azure.deployment import create_deployment +from maro.cli.utils.azure.general import connect_to_aks, get_acr_push_permissions, set_env_credentials +from maro.cli.utils.azure.resource_group import create_resource_group, delete_resource_group +from maro.cli.utils.common import show_log +from maro.rl.workflows.config import ConfigParser +from maro.utils.logger import CliLogger +from maro.utils.utils import LOCAL_MARO_ROOT + +from ..utils import k8s_manifest_generator, k8s_ops + +# metadata +CLI_AKS_PATH = dirname(abspath(__file__)) +TEMPLATE_PATH = join(CLI_AKS_PATH, "template.json") +NVIDIA_PLUGIN_PATH = join(CLI_AKS_PATH, "create_nvidia_plugin", "nvidia-device-plugin.yml") +LOCAL_ROOT = expanduser("~/.maro/aks") +DEPLOYMENT_CONF_PATH = os.path.join(LOCAL_ROOT, "conf.json") +DOCKER_FILE_PATH = join(LOCAL_MARO_ROOT, "docker_files", "dev.df") +DOCKER_IMAGE_NAME = "maro-aks" +REDIS_HOST = "maro-redis" +REDIS_PORT = 6379 +ADDRESS_REGISTRY_NAME = "address-registry" +ADDRESS_REGISTRY_PORT = 6379 +K8S_SECRET_NAME = "azure-secret" + +# display +NO_DEPLOYMENT_MSG = "No Kubernetes deployment on Azure found. Use 'maro aks init' to create a deployment first" +NO_JOB_MSG = "No job named {} has been scheduled. Use 'maro aks job add' to add the job first." +JOB_EXISTS_MSG = "A job named {} has already been scheduled." + +logger = CliLogger(name=__name__) + + +# helper functions +def get_resource_group_name(deployment_name: str): + return f"rg-{deployment_name}" + + +def get_acr_name(deployment_name: str): + return f"crmaro{deployment_name}" + + +def get_acr_server_name(acr_name: str): + return f"{acr_name}.azurecr.io" + + +def get_docker_image_name_in_acr(acr_name: str, docker_image_name: str): + return f"{get_acr_server_name(acr_name)}/{docker_image_name}" + + +def get_aks_name(deployment_name: str): + return f"aks-maro-{deployment_name}" + + +def get_agentpool_name(deployment_name: str): + return f"ap{deployment_name}" + + +def get_fileshare_name(deployment_name: str): + return f"fs-{deployment_name}" + + +def get_storage_account_name(deployment_name: str): + return f"stscenario{deployment_name}" + + +def get_virtual_network_name(location: str, deployment_name: str): + return f"vnet-prod-{location}-{deployment_name}" + + +def get_local_job_path(job_name: str): + return os.path.join(LOCAL_ROOT, job_name) + + +def get_storage_account_secret(resource_group_name: str, storage_account_name: str, namespace: str): + storage_account_keys = azure_storage_utils.get_storage_account_keys(resource_group_name, storage_account_name) + storage_key = storage_account_keys[0]["value"] + secret_data = { + "azurestorageaccountname": base64.b64encode(storage_account_name.encode()).decode(), + "azurestorageaccountkey": base64.b64encode(bytes(storage_key.encode())).decode() + } + k8s_ops.create_secret(K8S_SECRET_NAME, secret_data, namespace) + + +def get_resource_params(deployment_conf: dict) -> dict: + """Create ARM parameters for Azure resource deployment (). + + See https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/overview for details. + + Args: + deployment_conf (dict): Configuration dict for deployment on Azure. + + Returns: + dict: parameter dict, should be exported to json. + """ + name = deployment_conf["name"] + return { + "acrName": get_acr_name(name), + "acrSku": deployment_conf["container_registry_service_tier"], + "systemPoolVMCount": deployment_conf["resources"]["k8s"]["vm_count"], + "systemPoolVMSize": deployment_conf["resources"]["k8s"]["vm_size"], + "userPoolName": get_agentpool_name(name), + "userPoolVMCount": deployment_conf["resources"]["app"]["vm_count"], + "userPoolVMSize": deployment_conf["resources"]["app"]["vm_size"], + "aksName": get_aks_name(name), + "location": deployment_conf["location"], + "storageAccountName": get_storage_account_name(name), + "fileShareName": get_fileshare_name(name) + # "virtualNetworkName": get_virtual_network_name(deployment_conf["location"], name) + } + + +def prepare_docker_image_and_push_to_acr(image_name: str, context: str, docker_file_path: str, acr_name: str): + # build and tag docker image locally and push to the Azure Container Registry + if not docker_utils.image_exists(image_name): + docker_utils.build_image(context, docker_file_path, image_name) + + get_acr_push_permissions(os.environ["AZURE_CLIENT_ID"], acr_name) + docker_utils.push(image_name, get_acr_server_name(acr_name)) + + +def start_redis_service_in_aks(host: str, port: int, namespace: str): + k8s_ops.load_config() + k8s_ops.create_namespace(namespace) + k8s_ops.create_deployment(k8s_manifest_generator.get_redis_deployment_manifest(host, port), namespace) + k8s_ops.create_service(k8s_manifest_generator.get_redis_service_manifest(host, port), namespace) + + +# CLI command functions +def init(deployment_conf_path: str, **kwargs): + """Prepare Azure resources needed for an AKS cluster using a YAML configuration file. + + The configuration file template can be found in cli/k8s/aks/conf.yml. Use the Azure CLI to log into + your Azure account (az login ...) and the the Azure Container Registry (az acr login ...) first. + + Args: + deployment_conf_path (str): Path to the deployment configuration file. + """ + with open(deployment_conf_path, "r") as fp: + deployment_conf = yaml.safe_load(fp) + + subscription = deployment_conf["azure_subscription"] + name = deployment_conf["name"] + if os.path.isfile(DEPLOYMENT_CONF_PATH): + logger.warning(f"Deployment {name} has already been created") + return + + os.makedirs(LOCAL_ROOT, exist_ok=True) + resource_group_name = get_resource_group_name(name) + try: + # Set credentials as environment variables + set_env_credentials(LOCAL_ROOT, f"sp-{name}") + + # create resource group + resource_group = create_resource_group(subscription, resource_group_name, deployment_conf["location"]) + logger.info_green(f"Provisioned resource group {resource_group.name} in {resource_group.location}") + + # Create ARM parameters and start deployment + logger.info("Creating Azure resources...") + resource_params = get_resource_params(deployment_conf) + with open(TEMPLATE_PATH, 'r') as fp: + template = json.load(fp) + + create_deployment(subscription, resource_group_name, name, template, resource_params) + + # Attach ACR to AKS + aks_name, acr_name = resource_params["aksName"], resource_params["acrName"] + attach_acr(resource_group_name, aks_name, acr_name) + connect_to_aks(resource_group_name, aks_name) + + # build and tag docker image locally and push to the Azure Container Registry + logger.info("Preparing docker image...") + prepare_docker_image_and_push_to_acr(DOCKER_IMAGE_NAME, LOCAL_MARO_ROOT, DOCKER_FILE_PATH, acr_name) + + # start the Redis service in the k8s cluster in the deployment namespace and expose it + logger.info("Starting Redis service in the k8s cluster...") + start_redis_service_in_aks(REDIS_HOST, REDIS_PORT, name) + + # Dump the deployment configuration + with open(DEPLOYMENT_CONF_PATH, "w") as fp: + json.dump({ + "name": name, + "subscription": subscription, + "resource_group": resource_group_name, + "resources": resource_params + }, fp) + logger.info_green(f"Cluster '{name}' is created") + except Exception as e: + # If failed, remove details folder, then raise + shutil.rmtree(LOCAL_ROOT) + logger.error_red(f"Deployment {name} failed due to {e}, rolling back...") + delete_resource_group(subscription, resource_group_name) + except KeyboardInterrupt: + shutil.rmtree(LOCAL_ROOT) + logger.error_red(f"Deployment {name} aborted, rolling back...") + delete_resource_group(subscription, resource_group_name) + + +def add_job(conf_path: dict, **kwargs): + if not os.path.isfile(DEPLOYMENT_CONF_PATH): + logger.error_red(NO_DEPLOYMENT_MSG) + return + + parser = ConfigParser(conf_path) + job_name = parser.config["job"] + local_job_path = get_local_job_path(job_name) + if os.path.isdir(local_job_path): + logger.error_red(JOB_EXISTS_MSG.format(job_name)) + return + + os.makedirs(local_job_path) + with open(DEPLOYMENT_CONF_PATH, "r") as fp: + deployment_conf = json.load(fp) + + resource_group_name, resource_name = deployment_conf["resource_group"], deployment_conf["resources"] + fileshare = azure_storage_utils.get_fileshare(resource_name["storageAccountName"], resource_name["fileShareName"]) + job_dir = azure_storage_utils.get_directory(fileshare, job_name) + scenario_path = parser.config["scenario_path"] + logger.info(f"Uploading local directory {scenario_path}...") + azure_storage_utils.upload_to_fileshare(job_dir, scenario_path, name="scenario") + azure_storage_utils.get_directory(job_dir, "checkpoints") + azure_storage_utils.get_directory(job_dir, "logs") + + # Define mount volumes, i.e., scenario code, checkpoints, logs and load point + job_path_in_share = f"{resource_name['fileShareName']}/{job_name}" + volumes = [ + k8s_manifest_generator.get_azurefile_volume_spec(name, f"{job_path_in_share}/{name}", K8S_SECRET_NAME) + for name in ["scenario", "logs", "checkpoints"] + ] + + if "load_path" in parser.config["training"]: + load_path = parser.config["training"]["load_path"] + logger.info(f"Uploading local model directory {load_path}...") + azure_storage_utils.upload_to_fileshare(job_dir, load_path, name="loadpoint") + volumes.append( + k8s_manifest_generator.get_azurefile_volume_spec( + "loadpoint", f"{job_path_in_share}/loadpoint", K8S_SECRET_NAME) + ) + + # Start k8s jobs + k8s_ops.load_config() + k8s_ops.create_namespace(job_name) + get_storage_account_secret(resource_group_name, resource_name["storageAccountName"], job_name) + k8s_ops.create_service( + k8s_manifest_generator.get_cross_namespace_service_access_manifest( + ADDRESS_REGISTRY_NAME, REDIS_HOST, deployment_conf["name"], ADDRESS_REGISTRY_PORT + ), job_name + ) + for component_name, (script, env) in parser.get_job_spec(containerize=True).items(): + container_spec = k8s_manifest_generator.get_container_spec( + get_docker_image_name_in_acr(resource_name["acrName"], DOCKER_IMAGE_NAME), + component_name, + script, + env, + volumes + ) + manifest = k8s_manifest_generator.get_job_manifest( + resource_name["userPoolName"], + component_name, + container_spec, + volumes + ) + k8s_ops.create_job(manifest, job_name) + + +def remove_jobs(job_names: str, **kwargs): + if not os.path.isfile(DEPLOYMENT_CONF_PATH): + logger.error_red(NO_DEPLOYMENT_MSG) + return + + k8s_ops.load_config() + for job_name in job_names: + local_job_path = get_local_job_path(job_name) + if not os.path.isdir(local_job_path): + logger.error_red(NO_JOB_MSG.format(job_name)) + return + + k8s_ops.delete_job(job_name) + + +def get_job_logs(job_name: str, tail: int = -1, **kwargs): + with open(DEPLOYMENT_CONF_PATH, "r") as fp: + deployment_conf = json.load(fp) + + local_log_path = os.path.join(get_local_job_path(job_name), "log") + resource_name = deployment_conf["resources"] + fileshare = azure_storage_utils.get_fileshare(resource_name["storageAccountName"], resource_name["fileShareName"]) + job_dir = azure_storage_utils.get_directory(fileshare, job_name) + log_dir = azure_storage_utils.get_directory(job_dir, "logs") + azure_storage_utils.download_from_fileshare(log_dir, f"{job_name}.log", local_log_path) + show_log(local_log_path, tail=tail) + + +def exit(**kwargs): + try: + with open(DEPLOYMENT_CONF_PATH, "r") as fp: + deployment_conf = json.load(fp) + except FileNotFoundError: + logger.error(NO_DEPLOYMENT_MSG) + return + + name = deployment_conf["name"] + set_env_credentials(LOCAL_ROOT, f"sp-{name}") + delete_resource_group(deployment_conf["subscription"], deployment_conf["resource_group"]) diff --git a/maro/cli/k8s/test_conf.yml b/maro/cli/k8s/aks/conf.yml similarity index 80% rename from maro/cli/k8s/test_conf.yml rename to maro/cli/k8s/aks/conf.yml index 08ddb6f52..3f249d47e 100644 --- a/maro/cli/k8s/test_conf.yml +++ b/maro/cli/k8s/aks/conf.yml @@ -1,12 +1,12 @@ mode: "" -azure_subscription: 03811e6d-e6a0-4ae1-92e7-2249b8cb13be -name: yq -location: eastus +azure_subscription: your_azure_subscription_id +name: your_deployment_name +location: your_azure_service_location container_registry_service_tier: Standard # "Basic", "Standard", "Premium", see https://docs.microsoft.com/en-us/azure/container-registry/container-registry-skus for details -hardware: +resources: k8s: vm_size: Standard_DS2_v2 # https://docs.microsoft.com/en-us/azure/virtual-machines/sizes, https://docs.microsoft.com/en-us/azure/aks/quotas-skus-regions vm_count: 1 # must be at least 2 for k8s to function properly. app: vm_size: Standard_DS2_v2 # https://docs.microsoft.com/en-us/azure/virtual-machines/sizes, https://docs.microsoft.com/en-us/azure/aks/quotas-skus-regions - vm_count: 1 + vm_count: 1 \ No newline at end of file diff --git a/maro/cli/k8s/test_parameters.json b/maro/cli/k8s/aks/parameters.json similarity index 100% rename from maro/cli/k8s/test_parameters.json rename to maro/cli/k8s/aks/parameters.json diff --git a/maro/cli/k8s/test_template.json b/maro/cli/k8s/aks/template.json similarity index 99% rename from maro/cli/k8s/test_template.json rename to maro/cli/k8s/aks/template.json index fad7fa6c1..e61bdff12 100644 --- a/maro/cli/k8s/test_template.json +++ b/maro/cli/k8s/aks/template.json @@ -96,7 +96,7 @@ { "name": "[parameters('aksName')]", "type": "Microsoft.ContainerService/managedClusters", - "apiVersion": "2020-03-01", + "apiVersion": "2021-10-01", "location": "[parameters('location')]", "properties": { "dnsPrefix": "maro", diff --git a/maro/cli/k8s/aks_commands.py b/maro/cli/k8s/aks_commands.py deleted file mode 100644 index d95abd6f3..000000000 --- a/maro/cli/k8s/aks_commands.py +++ /dev/null @@ -1,851 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import base64 -import json -import os -import shutil -from os.path import abspath, dirname, expanduser, join - -import yaml - -# from maro.cli.k8s.executors.k8s_executor import K8sExecutor -# from maro.cli.utils.azure.acr import list_acr_repositories, login_acr -from maro.cli.utils import docker as docker_utils -from maro.cli.utils.azure import storage as azure_storage_utils -from maro.cli.utils.azure.aks import attach_acr -from maro.cli.utils.azure.deployment import create_deployment -from maro.cli.utils.azure.general import connect_to_aks, get_acr_push_permissions, set_env_credentials -from maro.cli.utils.azure.resource_group import create_resource_group, delete_resource_group_under_subscription -# from maro.cli.utils.azure.vm import list_vm_sizes -from maro.cli.utils.common import show_log -# from maro.cli.utils.deployment_validator import DeploymentValidator -# from maro.cli.utils.details_reader import DetailsReader -# from maro.cli.utils.details_writer import DetailsWriter -# from maro.cli.utils.name_creator import NameCreator -# from maro.cli.utils.path_convertor import PathConvertor -# from maro.cli.utils.subprocess import Subprocess -# from maro.utils.exception.cli_exception import BadRequestError, FileOperationError -from maro.rl.workflows.config import ConfigParser -from maro.utils.logger import CliLogger -from maro.utils.utils import LOCAL_MARO_ROOT - -from .utils import k8s, k8s_manifest_generator - -# metadata -CLI_K8S_PATH = dirname(abspath(__file__)) -TEMPLATE_PATH = join(CLI_K8S_PATH, "test_template.json") -# TEMPLATE_PATH = join(CLI_K8S_PATH, "lib", "modes", "aks", "create_aks_cluster", "template.json") -NVIDIA_PLUGIN_PATH = join(CLI_K8S_PATH, "create_nvidia_plugin", "nvidia-device-plugin.yml") -LOCAL_ROOT = expanduser("~/.maro/aks") -DEPLOYMENT_CONF_PATH = os.path.join(LOCAL_ROOT, "conf.json") -DOCKER_FILE_PATH = join(LOCAL_MARO_ROOT, "docker_files", "dev.df") -DOCKER_IMAGE_NAME = "maro-aks" -REDIS_HOST = "maro-redis" -REDIS_PORT = 6379 -ADDRESS_REGISTRY_NAME = "address-registry" -ADDRESS_REGISTRY_PORT = 6379 -K8S_SECRET_NAME = "azure-secret" - -# display -NO_DEPLOYMENT_MSG = "No Kubernetes deployment on Azure found. Use 'maro aks init' to create a deployment first" -NO_JOB_MSG = "No job named {} has been scheduled. Use 'maro aks job add' to add the job first." -JOB_EXISTS_MSG = "A job named {} has already been scheduled." - -logger = CliLogger(name=__name__) - - -# helper functions -def get_resource_group_name(deployment_name: str): - return f"rg-{deployment_name}" - - -def get_acr_name(deployment_name: str): - return f"crmaro{deployment_name}" - - -def get_acr_server_name(acr_name: str): - return f"{acr_name}.azurecr.io" - - -def get_docker_image_name_in_acr(acr_name: str, docker_image_name: str): - return f"{get_acr_server_name(acr_name)}/{docker_image_name}" - - -def get_aks_name(deployment_name: str): - return f"aks-maro-{deployment_name}" - - -def get_agentpool_name(deployment_name: str): - return f"ap{deployment_name}" - - -def get_fileshare_name(deployment_name): - return f"fs-{deployment_name}" - - -def get_storage_account_name(deployment_name: str): - return f"stscenario{deployment_name}" - - -def get_virtual_network_name(location: str, deployment_name: str): - return f"vnet-prod-{location}-{deployment_name}" - - -def get_local_job_path(job_name): - return os.path.join(LOCAL_ROOT, job_name) - - -def get_storage_account_secret(resource_group_name: str, storage_account_name: str, namespace: str): - storage_account_keys = azure_storage_utils.get_storage_account_keys(resource_group_name, storage_account_name) - storage_key = storage_account_keys[0]["value"] - secret_data = { - "azurestorageaccountname": base64.b64encode(storage_account_name.encode()).decode(), - "azurestorageaccountkey": base64.b64encode(bytes(storage_key.encode())).decode() - } - k8s.create_secret(K8S_SECRET_NAME, secret_data, namespace) - - -def get_resource_params(deployment_conf: dict) -> dict: - """Create ARM parameters for Azure resource deployment (). - - See https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/overview for details. - - Args: - deployment_conf (dict): Configuration dict for deployment on Azure. - - Returns: - dict: parameter dict, should be exported to json. - """ - name = deployment_conf["name"] - return { - "acrName": get_acr_name(name), - "acrSku": deployment_conf["container_registry_service_tier"], - "systemPoolVMCount": deployment_conf["hardware"]["k8s"]["vm_count"], - "systemPoolVMSize": deployment_conf["hardware"]["k8s"]["vm_size"], - "userPoolName": get_agentpool_name(name), - "userPoolVMCount": deployment_conf["hardware"]["app"]["vm_count"], - "userPoolVMSize": deployment_conf["hardware"]["app"]["vm_size"], - "aksName": get_aks_name(name), - "location": deployment_conf["location"], - "storageAccountName": get_storage_account_name(name), - "fileShareName": get_fileshare_name(name) - # "virtualNetworkName": get_virtual_network_name(deployment_conf["location"], name) - } - - -def prepare_docker_image_and_push_to_acr(image_name: str, context: str, docker_file_path: str, acr_name: str): - # build and tag docker image locally and push to the Azure Container Registry - if not docker_utils.image_exists(image_name): - docker_utils.build_image(context, docker_file_path, image_name) - - get_acr_push_permissions(os.environ["AZURE_CLIENT_ID"], acr_name) - docker_utils.push(image_name, get_acr_server_name(acr_name)) - - -def start_redis_service_in_aks(host: str, port: int, namespace: str): - k8s.load_config() - k8s.create_namespace(namespace) - k8s.create_deployment(k8s_manifest_generator.get_redis_deployment_manifest(host, port), namespace) - k8s.create_service(k8s_manifest_generator.get_redis_service_manifest(host, port), namespace) - - -# CLI command functions -def init(deployment_conf_path: str, **kwargs): - """Create MARO Cluster with create_deployment. - - Args: - deployment_conf_path (str): Path to the deployment configuration file. - """ - with open(deployment_conf_path, "r") as fp: - deployment_conf = yaml.safe_load(fp) - - subscription = deployment_conf["azure_subscription"] - name = deployment_conf["name"] - if os.path.isfile(DEPLOYMENT_CONF_PATH): - logger.warning(f"Deployment {name} has already been created") - return - - os.makedirs(LOCAL_ROOT, exist_ok=True) - resource_group_name = get_resource_group_name(name) - try: - set_env_credentials(LOCAL_ROOT, f"sp-{name}") - - # create resource group - resource_group = create_resource_group(subscription, resource_group_name, deployment_conf["location"]) - logger.info_green(f"Provisioned resource group {resource_group.name} in {resource_group.location}") - - # Create ARM parameters and start deployment - logger.info("Creating Azure resources...") - resource_params = get_resource_params(deployment_conf) - with open(TEMPLATE_PATH, 'r') as fp: - template = json.load(fp) - - create_deployment(subscription, resource_group_name, name, template, resource_params) - - # Attach ACR to AKS - aks_name, acr_name = resource_params["aksName"], resource_params["acrName"] - attach_acr(resource_group_name, aks_name, acr_name) - connect_to_aks(resource_group_name, aks_name) - - # build and tag docker image locally and push to the Azure Container Registry - logger.info("Preparing docker image...") - prepare_docker_image_and_push_to_acr(DOCKER_IMAGE_NAME, LOCAL_MARO_ROOT, DOCKER_FILE_PATH, acr_name) - - # start the Redis service in the k8s cluster in the deployment namespace and expose it - logger.info("Starting Redis service in the k8s cluster...") - start_redis_service_in_aks(REDIS_HOST, REDIS_PORT, name) - - # Dump the deployment configuration - with open(DEPLOYMENT_CONF_PATH, "w") as fp: - json.dump({ - "name": name, - "subscription": subscription, - "resource_group": resource_group_name, - "resources": resource_params - }, fp) - logger.info_green(f"Cluster '{name}' is created") - except Exception as e: - # If failed, remove details folder, then raise - shutil.rmtree(LOCAL_ROOT) - logger.error_red(f"Deployment {name} failed due to {e}, rolling back...") - delete_resource_group_under_subscription(subscription, resource_group_name) - except KeyboardInterrupt: - shutil.rmtree(LOCAL_ROOT) - logger.error_red(f"Deployment {name} aborted, rolling back...") - delete_resource_group_under_subscription(subscription, resource_group_name) - - -def add_job(conf_path: dict, **kwargs): - if not os.path.isfile(DEPLOYMENT_CONF_PATH): - logger.error_red(NO_DEPLOYMENT_MSG) - return - - parser = ConfigParser(conf_path) - job_conf = parser.config - - job_name = job_conf["job"] - local_job_path = get_local_job_path(job_name) - if os.path.isdir(local_job_path): - logger.error_red(JOB_EXISTS_MSG.format(job_name)) - return - - os.makedirs(local_job_path) - with open(DEPLOYMENT_CONF_PATH, "r") as fp: - deployment_conf = json.load(fp) - - resource_group_name = deployment_conf["resource_group"] - resource_name = deployment_conf["resources"] - fileshare = azure_storage_utils.get_fileshare(resource_name["storageAccountName"], resource_name["fileShareName"]) - job_dir = azure_storage_utils.get_directory(fileshare, job_name) - job_path_in_share = f"{resource_name['fileShareName']}/{job_name}" - scenario_path = job_conf['scenario_path'] - logger.info(f"Uploading local directory {scenario_path}...") - azure_storage_utils.upload_to_fileshare(job_dir, scenario_path, name="scenario") - azure_storage_utils.get_directory(job_dir, "checkpoints") - azure_storage_utils.get_directory(job_dir, "logs") - - # Define mount volumes, i.e., scenario code, checkpoints, logs and load point - volumes = [ - k8s_manifest_generator.get_azurefile_volume_spec(name, f"{job_path_in_share}/{name}", K8S_SECRET_NAME) - for name in ["scenario", "logs", "checkpoints"] - ] - - if "load_path" in job_conf["training"]: - load_dir = job_conf["training"]["load_path"] - logger.info(f"Uploading local directory {load_dir}...") - azure_storage_utils.upload_to_fileshare(job_dir, load_dir, name="loadpoint") - volumes.append( - k8s_manifest_generator.get_azurefile_volume_spec( - "loadpoint", f"{job_path_in_share}/loadpoint", K8S_SECRET_NAME) - ) - - # Start k8s jobs - k8s.load_config() - k8s.create_namespace(job_name) - get_storage_account_secret(resource_group_name, resource_name["storageAccountName"], job_name) - k8s.create_service( - k8s_manifest_generator.get_cross_namespace_service_access_manifest( - ADDRESS_REGISTRY_NAME, REDIS_HOST, deployment_conf["name"], ADDRESS_REGISTRY_PORT - ), job_name - ) - for component_name, env in parser.as_env(containerize=True).items(): - container_spec = k8s_manifest_generator.get_container_spec( - get_docker_image_name_in_acr(resource_name["acrName"], DOCKER_IMAGE_NAME), - component_name, - env, - volumes - ) - manifest = k8s_manifest_generator.get_job_manifest( - resource_name["userPoolName"], - component_name, - container_spec, - volumes - ) - k8s.create_job(manifest, job_name) - - -def remove_jobs(job_names: str, **kwargs): - if not os.path.isfile(DEPLOYMENT_CONF_PATH): - logger.error_red(NO_DEPLOYMENT_MSG) - return - - k8s.load_config() - for job_name in job_names: - local_job_path = get_local_job_path(job_name) - if not os.path.isdir(local_job_path): - logger.error_red(NO_JOB_MSG.format(job_name)) - return - - k8s.delete_job(job_name) - - -def get_job_logs(job_name: str, tail: int = -1, **kwargs): - with open(DEPLOYMENT_CONF_PATH, "r") as fp: - deployment_conf = json.load(fp) - - local_log_path = os.path.join(get_local_job_path(job_name), "log") - resource_name = deployment_conf["resources"] - fileshare = azure_storage_utils.get_fileshare(resource_name["storageAccountName"], resource_name["fileShareName"]) - job_dir = azure_storage_utils.get_directory(fileshare, job_name) - log_dir = azure_storage_utils.get_directory(job_dir, "logs") - azure_storage_utils.download_from_fileshare(log_dir, f"{job_name}.log", local_log_path) - show_log(local_log_path, tail=tail) - - -def describe_job(job_name: str): - pass - -# def get_checkpoints(job_name: str, **kwargs): -# with open(DEPLOYMENT_CONF_PATH, "r") as fp: -# deployment_conf = json.load(fp) -# local_checkpoint_path = job_conf.get("checkpoint_path", os.path.join(get_local_job_path, "ckpt")) -# resource_name = deployment_conf["resources"] -# fileshare = azure_storage_utils.get_fileshare(resource_name["storageAccountName"], resource_name["fileShareName"]) -# job_dir = azure_storage_utils.get_directory(fileshare, job_name) -# azure_storage_utils.download_from_fileshare(job_dir, f"{job_name}.log", local_checkpoint_path) - - -def exit(**kwargs): - try: - with open(DEPLOYMENT_CONF_PATH, "r") as fp: - deployment_conf = json.load(fp) - except FileNotFoundError: - logger.error(NO_DEPLOYMENT_MSG) - return - - name = deployment_conf["name"] - set_env_credentials(LOCAL_ROOT, f"sp-{name}") - delete_resource_group_under_subscription(deployment_conf["subscription"], deployment_conf["resource_group"]) - - -# class K8sAksExecutor(K8sExecutor): -# """Executor for k8s/aks mode. - -# See https://maro.readthedocs.io/en/latest/key_components/orchestration.html for reference. -# """ - -# def __init__(self, cluster_name: str): -# self.deployment_conf = DetailsReader.load_deployment_conf(cluster_name=cluster_name) - -# # Cloud configs -# self.subscription = self.deployment_conf["cloud"]["subscription"] -# self.resource_group = self.deployment_conf["cloud"]["resource_group"] -# self.location = self.deployment_conf["cloud"]["location"] - -# super().__init__(deployment_conf=self.deployment_conf) - -# # maro k8s node -# def scale_node(self, replicas: int, node_size: str) -> None: -# """Scale up/down MARO Node. - -# Args: -# replicas (int): desired number of MARO Node in specific node_size. -# node_size (str): size of the MARO Node VM, see -# https://docs.microsoft.com/en-us/azure/virtual-machines/sizes for reference. -# -# Returns: -# None. -# """ -# # Get node_size_to_info -# node_size_to_info = self._get_node_size_to_info() - -# # Get node_size_to_spec, and check if node_size is valid -# node_size_to_spec = self._get_node_size_to_spec() -# if node_size not in node_size_to_spec: -# raise BadRequestError(f"Invalid node_size '{node_size}'") - -# # Scale node -# if node_size not in node_size_to_info: -# self._build_node_pool( -# replicas=replicas, -# node_size=node_size -# ) -# elif node_size_to_info[node_size]["count"] != replicas: -# self._scale_node_pool( -# replicas=replicas, -# node_size=node_size, -# node_size_to_info=node_size_to_info -# ) -# else: -# logger.warning_yellow("Replica is match, no create or delete") - -# def _get_node_size_to_info(self) -> dict: -# """Get node_size to info mapping of the K8s Cluster. - -# Returns: -# dict: node_size to info mapping. -# """ -# # List nodepool -# nodepools = list_nodepool( -# resource_group=self.resource_group, -# aks_name=f"{self.cluster_id}-aks" -# ) - -# # Build node_size_to_count -# node_size_to_count = {} -# for nodepool in nodepools: -# node_size_to_count[nodepool["vmSize"]] = nodepool - -# return node_size_to_count - -# def _get_node_size_to_spec(self) -> dict: -# """Get node_size to spec mapping of Azure VM. - -# Returns: -# dict: node_size to spec mapping. -# """ -# # List available sizes for VM -# specs = list_vm_sizes(location=self.location) - -# # Build node_size_to_spec -# node_size_to_spec = {} -# for spec in specs: -# node_size_to_spec[spec["name"]] = spec - -# return node_size_to_spec - -# def _build_node_pool(self, replicas: int, node_size: str) -> None: -# """Build node pool for the specific node_size. - -# Args: -# replicas (int): number of MARO Node in specific node_size to stop. -# node_size (str): size of the MARO Node VM, -# see https://docs.microsoft.com/en-us/azure/virtual-machines/sizes for reference. - -# Returns: -# None. -# """ -# logger.info(f"Building '{node_size}' nodepool") - -# # Build nodepool -# add_nodepool( -# resource_group=self.resource_group, -# aks_name=f"{self.cluster_id}-aks", -# nodepool_name=K8sAksExecutor._generate_nodepool_name(node_size=node_size), -# node_count=replicas, -# node_size=node_size -# ) - -# logger.info_green(f"'{node_size}' nodepool is built") - -# def _scale_node_pool(self, replicas: int, node_size: str, node_size_to_info: dict): -# """Scale node pool of the specific node_size. - -# Args: -# replicas (int): number of MARO Node in specific node_size to stop. -# node_size (str): size of the MARO Node VM, -# see https://docs.microsoft.com/en-us/azure/virtual-machines/sizes for reference. -# node_size_to_info (dict): node_size to info mapping. - -# Returns: -# None. -# """ -# logger.info(f"Scaling '{node_size}' nodepool") - -# # Scale node pool -# scale_nodepool( -# resource_group=self.resource_group, -# aks_name=f"{self.cluster_id}-aks", -# nodepool_name=node_size_to_info[node_size]["name"], -# node_count=replicas -# ) - -# logger.info_green(f"'{node_size}' nodepool is scaled") - -# @staticmethod -# def _generate_nodepool_name(node_size: str) -> str: -# """Generate name of the nodepool. - -# Args: -# node_size (str): size of the MARO Node VM. - -# Returns: -# None. -# """ -# return NameCreator.create_name_with_md5(prefix="pool", key=node_size, md5_len=8) - -# def list_node(self) -> None: -# """Print node details to the command line. - -# Returns: -# None. -# """ -# # Get aks details -# aks_details = get_aks(resource_group=self.resource_group, aks_name=f"{self.cluster_id}-aks") -# agent_pools_details = aks_details["agentPoolProfiles"] - -# # Filter and print -# node_details = {} -# for agent_pool_details in agent_pools_details: -# node_details[agent_pool_details["vmSize"]] = agent_pool_details["count"] -# logger.info( -# json.dumps( -# node_details, -# indent=4, sort_keys=True -# ) -# ) - -# # maro k8s image - -# def push_image(self, image_name: str) -> None: -# """Push local image to the MARO Cluster. - -# Args: -# image_name (str): name of the local image that loaded in the docker. - -# Returns: -# None. -# """ -# remote_image_name = f"{self.cluster_id}acr.azurecr.io/{image_name}" - -# # ACR login -# login_acr(acr_name=f"{self.cluster_id}acr") - -# # Tag image -# command = f"docker tag {image_name} {remote_image_name}" -# _ = Subprocess.run(command=command) - -# # Push image to ACR -# command = f"docker push {remote_image_name}" -# _ = Subprocess.run(command=command) - -# def list_image(self): -# """Print image details to the command line. - -# Returns: -# None. -# """ -# # List acr repository -# acr_repositories = list_acr_repositories(acr_name=f"{self.cluster_id}acr") -# logger.info(acr_repositories) - -# # maro k8s data - -# def push_data(self, local_path: str, remote_dir: str) -> None: -# """Push local data to the remote AFS service via azcopy. - -# Args: -# local_path (str): path of the local data. -# remote_dir (str): path of the remote folder. - -# Returns: -# None. -# """ -# # Get sas -# sas = self._check_and_get_account_sas() - -# # Push data -# abs_local_path = os.path.expanduser(local_path) -# abs_source_path = PathConvertor.build_path_without_trailing_slash(abs_local_path) -# target_dir = PathConvertor.build_path_with_trailing_slash(remote_dir) -# if not target_dir.startswith("/"): -# raise FileOperationError(f"Invalid remote path: {target_dir}\nShould be started with '/'") -# copy_command = ( -# "azcopy copy " -# f"'{abs_source_path}' " -# f"'https://{self.cluster_id}st.file.core.windows.net/{self.cluster_id}-fs{target_dir}?{sas}' " -# "--recursive=True" -# ) -# _ = Subprocess.run(command=copy_command) - -# def pull_data(self, local_dir: str, remote_path: str) -> None: -# """Pull remote AFS service data to local folder via azcopy. - -# Args: -# local_dir (str): path of the local folder. -# remote_path (str): path of the remote data. - -# Returns: -# None. -# """ -# # Get sas -# sas = self._check_and_get_account_sas() - -# # Push data -# abs_local_dir = os.path.expanduser(local_dir) -# source_path = PathConvertor.build_path_without_trailing_slash(remote_path) -# abs_target_dir = PathConvertor.build_path_with_trailing_slash(abs_local_dir) -# os.makedirs(abs_target_dir, exist_ok=True) -# if not source_path.startswith("/"): -# raise FileOperationError(f"Invalid remote path: {source_path}\nShould be started with '/'") -# copy_command = ( -# "azcopy copy " -# f"'https://{self.cluster_id}st.file.core.windows.net/{self.cluster_id}-fs{source_path}?{sas}' " -# f"'{abs_target_dir}' " -# "--recursive=True" -# ) -# _ = Subprocess.run(command=copy_command) - -# def remove_data(self, remote_path: str) -> None: -# """Remote data at the remote AFS service. - -# Args: -# remote_path (str): path of the remote data. - -# Returns: -# None. -# """ -# # FIXME: Remove failed, The specified resource may be in use by an SMB client - -# # Get sas -# sas = self._check_and_get_account_sas() - -# # Remove data -# copy_command = ( -# "azcopy remove " -# f"'https://{self.cluster_id}st.file.core.windows.net/{self.cluster_id}-fs{remote_path}?{sas}' " -# "--recursive=True" -# ) -# _ = Subprocess.run(command=copy_command) - -# def _check_and_get_account_sas(self) -> str: -# """Check and get account sas token, also update it to the deployment_conf. - -# Ref: https://msdn.microsoft.com/library/azure/mt584140.aspx - -# Returns: -# str: account sas token. -# """ - -# # Load details -# cloud_details = self.deployment_conf["cloud"] - -# # Regenerate sas if the key is None or expired TODO: -# if "account_sas" not in cloud_details: -# account_sas = get_storage_account_sas(account_name=f"{self.cluster_id}st") -# cloud_details["account_sas"] = account_sas -# DetailsWriter.save_deployment_conf( -# cluster_name=self.cluster_name, -# deployment_conf=self.deployment_conf -# ) - -# return cloud_details["account_sas"] - -# # maro k8s job - -# def _create_k8s_job(self, job_details: dict) -> dict: -# """Create k8s job object with job_details. - -# Args: -# job_details (dict): details of the MARO Job. - -# Returns: -# dict: k8s job object. -# """ -# # Get config template -# with open(f"{MARO_K8S_LIB}/modes/aks/create_job/job.yml") as fr: -# k8s_job_config = yaml.safe_load(fr) -# with open(f"{MARO_K8S_LIB}/modes/aks/create_job/container.yml") as fr: -# k8s_container_config = yaml.safe_load(fr) - -# # Fill configs -# k8s_job_config["metadata"]["name"] = job_details["id"] -# k8s_job_config["metadata"]["labels"]["jobName"] = job_details["name"] -# azure_file_config = k8s_job_config["spec"]["template"]["spec"]["volumes"][0]["azureFile"] -# azure_file_config["secretName"] = "azure-storage-account-secret" -# azure_file_config["shareName"] = f"{self.cluster_id}-fs" - -# # Create and fill container config -# for component_type, component_details in job_details["components"].items(): -# for component_index in range(component_details["num"]): -# container_config = self._create_k8s_container_config( -# job_details=job_details, -# k8s_container_config_template=k8s_container_config, -# component_type=component_type, -# component_index=component_index -# ) -# k8s_job_config["spec"]["template"]["spec"]["containers"].append(container_config) - -# return k8s_job_config - -# def _create_k8s_container_config( -# self, job_details: dict, k8s_container_config_template: dict, -# component_type: str, component_index: int -# ) -> dict: -# """Create the container config in the k8s job object. - -# Args: -# job_details (dict): details of the MARO Job. -# k8s_container_config_template (dict): template of the k8s_container_config. -# component_type (str): type of the component. -# component_index (int): index of the component. - -# Returns: -# dict: the container config. -# """ -# # Copy config. -# k8s_container_config = copy.deepcopy(k8s_container_config_template) - -# # Load details -# component_details = job_details["components"][component_type] -# job_id = job_details["id"] -# component_id = job_details["components"][component_type]["id"] -# container_name = f"{job_id}-{component_id}-{component_index}" - -# # Fill configs. -# k8s_container_config["name"] = container_name -# k8s_container_config["image"] = self._build_image_address(image_name=component_details["image"]) -# k8s_container_config["resources"]["requests"] = { -# "cpu": component_details["resources"]["cpu"], -# "memory": component_details["resources"]["memory"], -# "nvidia.com/gpu": component_details["resources"]["gpu"] -# } -# k8s_container_config["resources"]["limits"] = { -# "cpu": component_details["resources"]["cpu"], -# "memory": component_details["resources"]["memory"], -# "nvidia.com/gpu": component_details["resources"]["gpu"] -# } -# k8s_container_config["env"] = [ -# { -# "name": "CLUSTER_ID", -# "value": f"{self.cluster_id}" -# }, -# { -# "name": "CLUSTER_NAME", -# "value": f"{self.cluster_name}" -# }, -# { -# "name": "JOB_ID", -# "value": job_id -# }, -# { -# "name": "JOB_NAME", -# "value": job_details["name"] -# }, -# { -# "name": "COMPONENT_ID", -# "value": component_id -# }, -# { -# "name": "COMPONENT_TYPE", -# "value": f"{component_type}" -# }, -# { -# "name": "COMPONENT_INDEX", -# "value": f"{component_index}" -# }, -# { -# "name": "PYTHONUNBUFFERED", -# "value": "0" -# } -# ] -# k8s_container_config["command"] = component_details["command"] -# k8s_container_config["volumeMounts"][0]["mountPath"] = component_details["mount"]["target"] - -# return k8s_container_config - -# def _build_image_address(self, image_name: str) -> str: -# """Build image address name for image that stored at Azure Container Registry. - -# Args: -# image_name (str): name of the image. - -# Returns: -# str: image address name. -# """ -# # Get repositories -# acr_repositories = list_acr_repositories(acr_name=f"{self.cluster_id}acr") - -# # Build address -# if image_name in acr_repositories: -# return f"{self.cluster_id}acr.azurecr.io/{image_name}" -# else: -# return image_name - -# @staticmethod -# def _export_log(pod_id: str, container_name: str, export_dir: str) -> None: -# """Export k8s job logs to the specific folder. - -# Args: -# pod_id (str): id of the k8s pod. -# container_name (str): name of the container. -# export_dir (str): path of the exported folder. - -# Returns: -# None. -# """ -# os.makedirs(os.path.expanduser(export_dir + f"/{pod_id}"), exist_ok=True) -# with open(os.path.expanduser(export_dir + f"/{pod_id}/{container_name}.log"), "w") as fw: -# return_str = client.CoreV1Api().read_namespaced_pod_log(name=pod_id, namespace="default") -# fw.write(return_str) - -# # maro k8s status - -# def status(self) -> None: -# """Print details of specific MARO Resources (redis only at this time). - -# Returns: -# None. -# """ -# return_status = {} - -# # Get pods details -# pod_list = client.CoreV1Api().list_pod_for_all_namespaces(watch=False).to_dict()["items"] - -# for pod in pod_list: -# if "app" in pod["metadata"]["labels"] and pod["metadata"]["labels"]["app"] == "maro-redis": -# return_status["redis"] = { -# "private_ip_address": pod["status"]["pod_ip"] -# } -# break - -# # Print status -# logger.info( -# json.dumps( -# return_status, -# indent=4, sort_keys=True -# ) -# ) - -# # Utils - -# def load_k8s_context(self) -> None: -# """Activate load k8s context operation. - -# Returns: -# None. -# """ -# self._load_k8s_context( -# cluster_id=self.cluster_id, -# resource_group=self.resource_group -# ) - -# @staticmethod -# def _load_k8s_context(cluster_id: int, resource_group: str) -> None: -# """Load the k8s context. - -# Set current k8s context (only in the CLI runtime) to the k8s cluster that related to the MARO Cluster. - -# Args: -# cluster_id (str): id of the MARO Cluster. -# resource_group (str): name of the resource group. - -# Returns: -# None. -# """ -# load_aks_context( -# resource_group=resource_group, -# aks_name=f"{cluster_id}-aks" -# ) -# config.load_kube_config(context=f"{cluster_id}-aks") diff --git a/maro/cli/k8s/lib/modes/aks/create_aks_cluster/template.json b/maro/cli/k8s/lib/modes/aks/create_aks_cluster/template.json index d77c0cad7..b9db8e344 100644 --- a/maro/cli/k8s/lib/modes/aks/create_aks_cluster/template.json +++ b/maro/cli/k8s/lib/modes/aks/create_aks_cluster/template.json @@ -95,7 +95,7 @@ { "name": "[parameters('clusterName')]", "type": "Microsoft.ContainerService/managedClusters", - "apiVersion": "2020-03-01", + "apiVersion": "2021-10-01", "location": "[parameters('location')]", "dependsOn": [ "[variables('vnetId')]" @@ -141,7 +141,7 @@ { "name": "[parameters('virtualNetworkName')]", "type": "Microsoft.Network/virtualNetworks", - "apiVersion": "2020-04-01", + "apiVersion": "2020-11-01", "location": "[parameters('location')]", "properties": { "addressSpace": { diff --git a/maro/cli/k8s/utils/k8s_manifest_generator.py b/maro/cli/k8s/utils/k8s_manifest_generator.py index 672d1ba1f..d46f03b4c 100644 --- a/maro/cli/k8s/utils/k8s_manifest_generator.py +++ b/maro/cli/k8s/utils/k8s_manifest_generator.py @@ -33,7 +33,7 @@ def get_azurefile_volume_spec(name: str, share_name: str, secret_name: str): } -def get_container_spec(image_name: str, component_name: str, env: dict, volumes): +def get_container_spec(image_name: str, component_name: str, script: str, env: dict, volumes): common_container_spec = { "image": image_name, "imagePullPolicy": "Always", @@ -43,7 +43,7 @@ def get_container_spec(image_name: str, component_name: str, env: dict, volumes) **common_container_spec, **{ "name": component_name, - "command": ["python3", f"/maro/maro/rl/workflows/{component_name.split('-')[0]}.py"], + "command": ["python3", script], "env": format_env_vars(env, mode="k8s") } } diff --git a/maro/cli/k8s/utils/k8s.py b/maro/cli/k8s/utils/k8s_ops.py similarity index 100% rename from maro/cli/k8s/utils/k8s.py rename to maro/cli/k8s/utils/k8s_ops.py diff --git a/maro/cli/local/commands.py b/maro/cli/local/commands.py index a619a6ca3..26a4f7075 100644 --- a/maro/cli/local/commands.py +++ b/maro/cli/local/commands.py @@ -59,23 +59,16 @@ def get_redis_conn(port=None): def run(conf_path: str, containerize: bool = False, evaluate_only: bool = False, **kwargs): # Load job configuration file parser = ConfigParser(conf_path) - env_by_component = parser.as_env(containerize=containerize) if containerize: - path_mapping = parser.get_path_mapping(containerize=True) try: start_rl_job_with_docker_compose( - parser.config, LOCAL_MARO_ROOT, DOCKERFILE_PATH, - DOCKER_IMAGE_NAME, env_by_component, path_mapping, evaluate_only, + parser, LOCAL_MARO_ROOT, DOCKERFILE_PATH, DOCKER_IMAGE_NAME, evaluate_only=evaluate_only, ) except KeyboardInterrupt: stop_rl_job_with_docker_compose(parser.config["job"], LOCAL_MARO_ROOT) else: try: - start_rl_job( - env_by_component=parser.as_env(), - maro_root=LOCAL_MARO_ROOT, - evaluate_only=evaluate_only, - ) + start_rl_job(parser, LOCAL_MARO_ROOT, evaluate_only=evaluate_only) except KeyboardInterrupt: sys.exit(1) diff --git a/maro/cli/local/job_manager.py b/maro/cli/local/job_manager.py index 0377cf585..d36ec7662 100644 --- a/maro/cli/local/job_manager.py +++ b/maro/cli/local/job_manager.py @@ -80,15 +80,12 @@ def monitor(job_name): build_image(local_maro_root, docker_file_path, docker_image_name) parser = ConfigParser(conf) - env_by_component = parser.as_env(containerize=containerize) if containerize: path_mapping = parser.get_path_mapping(containerize=True) - started[job_name] = start_rl_job_in_containers( - conf, docker_image_name, env_by_component, path_mapping - ) + started[job_name] = start_rl_job_in_containers(parser, docker_image_name) details["containers"] = started[job_name] else: - started[job_name] = start_rl_job(env_by_component, local_maro_root, background=True) + started[job_name] = start_rl_job(parser, local_maro_root, background=True) details["pids"] = [proc.pid for proc in started[job_name]] details = {"status": JobStatus.RUNNING, "start_time": time.time()} redis_conn.hset(RedisHashKey.JOB_DETAILS, job_name, json.dumps(details)) diff --git a/maro/cli/local/utils.py b/maro/cli/local/utils.py index 75047f773..643f780c2 100644 --- a/maro/cli/local/utils.py +++ b/maro/cli/local/utils.py @@ -4,12 +4,13 @@ import os import subprocess from copy import deepcopy -from typing import Dict, List +from typing import List import docker import yaml from maro.cli.utils.common import format_env_vars +from maro.rl.workflows.config.parser import ConfigParser class RedisHashKey: @@ -102,18 +103,15 @@ def exec(cmd: str, env: dict, debug: bool = False) -> subprocess.Popen: def start_rl_job( - env_by_component: Dict[str, dict], maro_root: str, evaluate_only: bool, background: bool = False, + parser: ConfigParser, maro_root: str, evaluate_only: bool, background: bool = False, ) -> List[subprocess.Popen]: - def get_local_script_path(component: str): - return os.path.join(maro_root, "maro", "rl", "workflows", f"{component.split('-')[0]}.py") - procs = [ exec( - f"python {get_local_script_path(component)}" + ("" if not evaluate_only else " --evaluate_only"), + f"python {script}" + ("" if not evaluate_only else " --evaluate_only"), format_env_vars({**env, "PYTHONPATH": maro_root}, mode="proc"), debug=not background ) - for component, env in env_by_component.items() + for script, env in parser.get_job_spec().values() ] if not background: for proc in procs: @@ -122,30 +120,30 @@ def get_local_script_path(component: str): return procs -def start_rl_job_in_containers( - conf: dict, image_name: str, env_by_component: Dict[str, dict], path_mapping: Dict[str, str] -) -> None: - job_name = conf["job"] +def start_rl_job_in_containers(parser: ConfigParser, image_name: str) -> list: + job_name = parser.config["job"] client, containers = docker.from_env(), [] - is_distributed_training = conf["training"]["mode"] != "simple" - is_distributed_rollout = ( - "parallelism" in conf["rollout"] and - max(conf["rollout"]["parallelism"]["sampling"], conf["rollout"]["parallelism"].get("eval", 1)) > 1 - ) - if is_distributed_training or is_distributed_rollout: + training_mode = parser.config["training"]["mode"] + if "parallelism" in parser.config["rollout"]: + rollout_parallelism = max( + parser.config["rollout"]["parallelism"]["sampling"], + parser.config["rollout"]["parallelism"].get("eval", 1) + ) + else: + rollout_parallelism = 1 + if training_mode != "simple" or rollout_parallelism > 1: # create the exclusive network for the job client.networks.create(job_name, driver="bridge") - for component, env in env_by_component.items(): - container_name = f"{job_name}.{component}" + for component, (script, env) in parser.get_job_spec(containerize=True).items(): # volume mounts for scenario folder, policy loading, checkpointing and logging container = client.containers.run( image_name, - command=f"python3 /maro/maro/rl/workflows/{component.split('-')[0]}.py", + command=f"python3 {script}", detach=True, - name=container_name, + name=component, environment=env, - volumes=[f"{src}:{dst}" for src, dst in path_mapping.items()], + volumes=[f"{src}:{dst}" for src, dst in parser.get_path_mapping(containerize=True).items()], network=job_name ) @@ -155,38 +153,41 @@ def start_rl_job_in_containers( def get_docker_compose_yml_path(maro_root: str) -> str: - return os.path.join(maro_root, "tmp", "docker-compose.yml") + return os.path.join(maro_root, ".tmp", "docker-compose.yml") def start_rl_job_with_docker_compose( - conf: dict, context: str, dockerfile_path: str, image_name: str, env_by_component: Dict[str, dict], - path_mapping: Dict[str, str], evaluate_only: bool, + parser: ConfigParser, context: str, dockerfile_path: str, image_name: str, evaluate_only: bool, ) -> None: common_spec = { "build": {"context": context, "dockerfile": dockerfile_path}, "image": image_name, - "volumes": [f"{src}:{dst}" for src, dst in path_mapping.items()] + "volumes": [f"./{src}:{dst}" for src, dst in parser.get_path_mapping(containerize=True).items()] } - job = conf["job"] - manifest = {"version": "3.9"} - manifest["services"] = { - component: { - **deepcopy(common_spec), - **{ - "container_name": f"{job}.{component}", - "command": f"python3 /maro/maro/rl/workflows/{component.split('-')[0]}.py" + ( - "" if not evaluate_only else "--evaluate_only"), - "environment": format_env_vars(env, mode="docker-compose") + + job_name = parser.config["job"] + manifest = { + "version": "3.9", + "services": { + component: { + **deepcopy(common_spec), + **{ + "container_name": component, + "command": f"python3 {script}" + ("" if not evaluate_only else " --evaluate_only"), + "environment": format_env_vars(env, mode="docker-compose") + } } - } - for component, env in env_by_component.items() + for component, (script, env) in parser.get_job_spec(containerize=True).items() + }, } docker_compose_file_path = get_docker_compose_yml_path(maro_root=context) with open(docker_compose_file_path, "w") as fp: yaml.safe_dump(manifest, fp) - subprocess.run(["docker-compose", "--project-name", job, "-f", docker_compose_file_path, "up", "--remove-orphans"]) + subprocess.run( + ["docker-compose", "--project-name", job_name, "-f", docker_compose_file_path, "up", "--remove-orphans"] + ) def stop_rl_job_with_docker_compose(job_name: str, context: str): diff --git a/maro/cli/maro.py b/maro/cli/maro.py index 65956d2c6..b77a807f1 100644 --- a/maro/cli/maro.py +++ b/maro/cli/maro.py @@ -911,10 +911,14 @@ def load_parser_aks(prev_parser: ArgumentParser, global_parser: ArgumentParser) subparsers = prev_parser.add_subparsers() # maro aks create - from maro.cli.k8s.aks_commands import init + from maro.cli.k8s.aks.aks_commands import init parser_create = subparsers.add_parser( "init", - help="Deploy resources and start required services on Azure", + help=""" + Deploy resources and start required services on Azure. The configuration file template can be found + in cli/k8s/aks/conf.yml. Use the Azure CLI to log into your Azure account (az login ...) and the the + Azure Container Registry (az acr login ...) first. + """, examples=CliExamples.MARO_K8S_CREATE, parents=[global_parser] ) @@ -922,7 +926,7 @@ def load_parser_aks(prev_parser: ArgumentParser, global_parser: ArgumentParser) parser_create.set_defaults(func=init) # maro aks exit - from maro.cli.k8s.aks_commands import exit + from maro.cli.k8s.aks.aks_commands import exit parser_create = subparsers.add_parser( "exit", help="Delete deployed resources", @@ -934,14 +938,14 @@ def load_parser_aks(prev_parser: ArgumentParser, global_parser: ArgumentParser) # maro aks job parser_job = subparsers.add_parser( "job", - help="Job-related commands", + help="AKS job-related commands", parents=[global_parser] ) parser_job.set_defaults(func=_help_func(parser=parser_job)) job_subparsers = parser_job.add_subparsers() # maro aks job add - from maro.cli.k8s.aks_commands import add_job + from maro.cli.k8s.aks.aks_commands import add_job parser_job_start = job_subparsers.add_parser( "add", help="Add an RL job to the AKS cluster", @@ -952,7 +956,7 @@ def load_parser_aks(prev_parser: ArgumentParser, global_parser: ArgumentParser) parser_job_start.set_defaults(func=add_job) # maro aks job rm - from maro.cli.k8s.aks_commands import remove_jobs + from maro.cli.k8s.aks.aks_commands import remove_jobs parser_job_start = job_subparsers.add_parser( "rm", help="Remove previously scheduled RL jobs from the AKS cluster", @@ -963,7 +967,7 @@ def load_parser_aks(prev_parser: ArgumentParser, global_parser: ArgumentParser) parser_job_start.set_defaults(func=remove_jobs) # maro aks job logs - from maro.cli.k8s.aks_commands import get_job_logs + from maro.cli.k8s.aks.aks_commands import get_job_logs job_logs_parser = job_subparsers.add_parser( "logs", help="Get job logs", diff --git a/maro/cli/utils/azure/deployment.py b/maro/cli/utils/azure/deployment.py index 5fd5c06f6..a2cc1cf5b 100644 --- a/maro/cli/utils/azure/deployment.py +++ b/maro/cli/utils/azure/deployment.py @@ -4,10 +4,6 @@ from .general import get_resource_client -def format_resource_params(params): - return {k: {"value": v} for k, v in params.items()} - - def create_deployment( subscription: str, resource_group: str, @@ -16,7 +12,7 @@ def create_deployment( params: dict, sync: bool = True ) -> None: - params = format_resource_params(params) + params = {k: {"value": v} for k, v in params.items()} resource_client = get_resource_client(subscription) deployment_params = {"mode": "Incremental", "template": template, "parameters": params} result = resource_client.deployments.begin_create_or_update( diff --git a/maro/cli/utils/azure/general.py b/maro/cli/utils/azure/general.py index 9e499acbb..83e6968c7 100644 --- a/maro/cli/utils/azure/general.py +++ b/maro/cli/utils/azure/general.py @@ -29,6 +29,9 @@ def get_resource_client(subscription: str): def set_env_credentials(dump_path: str, service_principal_name: str): os.makedirs(dump_path, exist_ok=True) service_principal_file_path = os.path.join(dump_path, f"{service_principal_name}.json") + # If the service principal file does not exist, create one using the az CLI command. + # For details on service principals, refer to + # https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals if not os.path.exists(service_principal_file_path): with open(service_principal_file_path, 'w') as fp: subprocess.run( diff --git a/maro/rl/model/abs_net.py b/maro/rl/model/abs_net.py index 19bcfea56..cb61714dd 100644 --- a/maro/rl/model/abs_net.py +++ b/maro/rl/model/abs_net.py @@ -56,11 +56,11 @@ def apply_gradients(self, grad: Dict[str, torch.Tensor]) -> None: def _forward_unimplemented(self, *input: Any) -> None: pass - def get_state(self) -> object: + def get_state(self) -> dict: """Get the net's state. Returns: - state (object): A object that contains the net's state. + state (dict): A object that contains the net's state. """ return { "network": self.state_dict(), diff --git a/maro/rl/policy/discrete_rl_policy.py b/maro/rl/policy/discrete_rl_policy.py index da1d3d119..0d7374423 100644 --- a/maro/rl/policy/discrete_rl_policy.py +++ b/maro/rl/policy/discrete_rl_policy.py @@ -286,7 +286,7 @@ def eval(self) -> None: def train(self) -> None: self._policy_net.train() - def get_state(self) -> object: + def get_state(self) -> dict: return self._policy_net.get_state() def set_state(self, policy_state: dict) -> None: diff --git a/maro/rl/rl_component/rl_component_bundle.py b/maro/rl/rl_component/rl_component_bundle.py index b9152e47c..437433771 100644 --- a/maro/rl/rl_component/rl_component_bundle.py +++ b/maro/rl/rl_component/rl_component_bundle.py @@ -1,6 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + from abc import abstractmethod from functools import partial -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, Iterable, List, Optional from maro.rl.policy import AbsPolicy from maro.rl.rollout import AbsEnvSampler @@ -121,7 +124,7 @@ def get_policy_trainer_mapping(self) -> Dict[str, str]: not provided in policy-trainer mapping will not be trained since we do not assign a trainer to it. """ return { - policy_name: policy_name.split(".")[0] for policy_name in self.policy_names + policy_name: policy_name.split(".")[0] for policy_name in self.policy_creator } ######################################################################################## @@ -146,12 +149,19 @@ def _complete_resources(self) -> None: self.device_mapping = self.get_device_mapping() self.policy_creator = self.get_policy_creator() - self.policy_names = list(self.policy_creator.keys()) self.agent2policy = self.get_agent2policy() self.policy_trainer_mapping = self.get_policy_trainer_mapping() - assert all([policy_name in self.policy_creator for policy_name in self.policy_trainer_mapping.keys()]) - assert all([trainer_name in self.trainer_creator for trainer_name in self.policy_trainer_mapping.values()]) + + required_policies = set(self.agent2policy.values()) + self.policy_creator = {name: self.policy_creator[name] for name in required_policies} + self.policy_trainer_mapping = {name: self.policy_trainer_mapping[name] for name in required_policies} + self.policy_names = list(required_policies) + assert len(required_policies) == len(self.policy_creator) # Should have same size after filter + + required_trainers = set(self.policy_trainer_mapping.values()) + self.trainer_creator = {name: self.trainer_creator[name] for name in required_trainers} + assert len(required_trainers) == len(self.trainer_creator) # Should have same size after filter self.trainable_policy_names = list(self.policy_trainer_mapping.keys()) self.trainable_policy_creator = { diff --git a/maro/rl/rollout/env_sampler.py b/maro/rl/rollout/env_sampler.py index f5a5db0e3..3030cc590 100644 --- a/maro/rl/rollout/env_sampler.py +++ b/maro/rl/rollout/env_sampler.py @@ -150,6 +150,7 @@ class CacheElement: agent_state_dict: Dict[Any, np.ndarray] action_dict: Dict[Any, np.ndarray] env_action_dict: Dict[Any, np.ndarray] + reward_dict: Dict[Any, float] = None @dataclass @@ -211,8 +212,8 @@ class AbsEnvSampler(object, metaclass=ABCMeta): learn_env (Env): Environment used for training. test_env (Env): Environment used for testing. agent_wrapper_cls (Type[AbsAgentWrapper], default=SimpleAgentWrapper): Specific AgentWrapper type. - reward_eval_delay (int): Number of ticks required after a decision event to evaluate the reward - for the action taken for that event. + reward_eval_delay (int, default=None): Number of ticks required after a decision event to evaluate the reward + for the action taken for that event. If it is None, calculate reward immediately after `step()`. """ def __init__( @@ -220,7 +221,7 @@ def __init__( learn_env: Env, test_env: Env, agent_wrapper_cls: Type[AbsAgentWrapper] = SimpleAgentWrapper, - reward_eval_delay: int = 0, + reward_eval_delay: int = None, ) -> None: self._learn_env = learn_env self._test_env = test_env @@ -235,6 +236,8 @@ def __init__( self._info = {} + assert self._reward_eval_delay is None or self._reward_eval_delay >= 0 + def build( self, rl_component_bundle: RLComponentBundle, @@ -362,39 +365,49 @@ def sample(self, policy_state: Optional[Dict[str, dict]] = None, num_steps: Opti env_action_dict = self._translate_to_env_action(action_dict, self._event) # Store experiences in the cache - self._trans_cache.append( - CacheElement( - tick=self._env.tick, - event=self._event, - state=self._state, - agent_state_dict={ - id_: state - for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents - }, - action_dict={ - id_: action - for id_, action in action_dict.items() if id_ in self._trainable_agents - }, - env_action_dict={ - id_: env_action - for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents - }, - ) + cache_element = CacheElement( + tick=self._env.tick, + event=self._event, + state=self._state, + agent_state_dict={ + id_: state + for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents + }, + action_dict={ + id_: action + for id_, action in action_dict.items() if id_ in self._trainable_agents + }, + env_action_dict={ + id_: env_action + for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents + }, ) # Update env and get new states (global & agent) _, self._event, is_done = self._env.step(list(env_action_dict.values())) self._state, self._agent_state_dict = (None, {}) if is_done \ else self._get_global_and_agent_state(self._event) + + if self._reward_eval_delay is None: + cache_element.reward_dict = self._get_reward( + cache_element.env_action_dict, cache_element.event, cache_element.tick, + ) + self._post_step(cache_element) + self._trans_cache.append(cache_element) + steps_to_go -= 1 - tick_bound = self._env.tick - self._reward_eval_delay + tick_bound = self._env.tick - (0 if self._reward_eval_delay is None else self._reward_eval_delay) experiences = [] while len(self._trans_cache) > 0 and self._trans_cache[0].tick <= tick_bound: cache_element = self._trans_cache.popleft() # !: Here the reward calculation method requires the given tick is enough and must be used then. - reward_dict = self._get_reward(cache_element.env_action_dict, cache_element.event, cache_element.tick) - self._post_step(cache_element, reward_dict) + if self._reward_eval_delay is not None: + cache_element.reward_dict = self._get_reward( + cache_element.env_action_dict, cache_element.event, cache_element.tick, + ) + self._post_step(cache_element) + if len(self._trans_cache) > 0: next_state = self._trans_cache[0].state next_agent_state_dict = dict(self._trans_cache[0].agent_state_dict) @@ -409,7 +422,7 @@ def sample(self, policy_state: Optional[Dict[str, dict]] = None, num_steps: Opti state=cache_element.state, agent_state_dict=cache_element.agent_state_dict, action_dict=cache_element.action_dict, - reward_dict=reward_dict, + reward_dict=cache_element.reward_dict, terminal_dict={}, # Will be processed later in `_post_polish_experiences()` next_state=next_state, next_agent_state_dict=next_agent_state_dict, @@ -480,39 +493,48 @@ def eval(self, policy_state: Dict[str, dict] = None) -> dict: env_action_dict = self._translate_to_env_action(action_dict, self._event) # Store experiences in the cache - self._trans_cache.append( - CacheElement( - tick=self._env.tick, - event=self._event, - state=self._state, - agent_state_dict={ - id_: state for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents - }, - action_dict={id_: action for id_, action in action_dict.items() if id_ in self._trainable_agents}, - env_action_dict={ - id_: env_action for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents - }, - ) + cache_element = CacheElement( + tick=self._env.tick, + event=self._event, + state=self._state, + agent_state_dict={ + id_: state for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents + }, + action_dict={id_: action for id_, action in action_dict.items() if id_ in self._trainable_agents}, + env_action_dict={ + id_: env_action for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents + }, ) + # Update env and get new states (global & agent) _, self._event, is_done = self._env.step(list(env_action_dict.values())) self._state, self._agent_state_dict = (None, {}) if is_done \ else self._get_global_and_agent_state(self._event) - tick_bound = self._env.tick - self._reward_eval_delay + if self._reward_eval_delay is None: + cache_element.reward_dict = self._get_reward( + cache_element.env_action_dict, cache_element.event, cache_element.tick, + ) + self._post_eval_step(cache_element) + self._trans_cache.append(cache_element) + + tick_bound = self._env.tick - (0 if self._reward_eval_delay is None else self._reward_eval_delay) while len(self._trans_cache) > 0 and self._trans_cache[0].tick <= tick_bound: cache_element = self._trans_cache.popleft() - reward_dict = self._get_reward(cache_element.env_action_dict, cache_element.event, cache_element.tick) - self._post_eval_step(cache_element, reward_dict) + if self._reward_eval_delay is not None: + cache_element.reward_dict = self._get_reward( + cache_element.env_action_dict, cache_element.event, cache_element.tick, + ) + self._post_eval_step(cache_element) return {"info": [self._info]} @abstractmethod - def _post_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: + def _post_step(self, cache_element: CacheElement) -> None: raise NotImplementedError @abstractmethod - def _post_eval_step(self, cache_element: CacheElement, reward: Dict[Any, float]) -> None: + def _post_eval_step(self, cache_element: CacheElement) -> None: raise NotImplementedError def post_collect(self, info_list: list, ep: int) -> None: diff --git a/maro/rl/training/algorithms/__init__.py b/maro/rl/training/algorithms/__init__.py index 361e2ffb2..3bcf80c99 100644 --- a/maro/rl/training/algorithms/__init__.py +++ b/maro/rl/training/algorithms/__init__.py @@ -5,7 +5,7 @@ from .ddpg import DDPGParams, DDPGTrainer from .dqn import DQNParams, DQNTrainer from .maddpg import DiscreteMADDPGParams, DiscreteMADDPGTrainer -from .ppo import PPOParams, PPOTrainer +from .ppo import PPOParams, PPOTrainer, DiscretePPOWithEntropyTrainer from .sac import SoftActorCriticParams, SoftActorCriticTrainer __all__ = [ @@ -13,6 +13,6 @@ "DDPGTrainer", "DDPGParams", "DQNTrainer", "DQNParams", "DiscreteMADDPGTrainer", "DiscreteMADDPGParams", - "PPOParams", "PPOTrainer", + "PPOParams", "PPOTrainer", "DiscretePPOWithEntropyTrainer", "SoftActorCriticParams", "SoftActorCriticTrainer", ] diff --git a/maro/rl/training/algorithms/base/ac_ppo_base.py b/maro/rl/training/algorithms/base/ac_ppo_base.py index d96a8b3b8..1c5cf5721 100644 --- a/maro/rl/training/algorithms/base/ac_ppo_base.py +++ b/maro/rl/training/algorithms/base/ac_ppo_base.py @@ -11,7 +11,7 @@ from maro.rl.model import VNet from maro.rl.policy import ContinuousRLPolicy, DiscretePolicyGradient, RLPolicy from maro.rl.training import AbsTrainOps, FIFOReplayMemory, remote, RemoteOps, SingleAgentTrainer, TrainerParams -from maro.rl.utils import (discount_cumsum, get_torch_device, ndarray_to_tensor, TransitionBatch) +from maro.rl.utils import discount_cumsum, get_torch_device, ndarray_to_tensor, TransitionBatch @dataclass diff --git a/maro/rl/training/algorithms/ppo.py b/maro/rl/training/algorithms/ppo.py index c0f10452a..9ec892ecc 100644 --- a/maro/rl/training/algorithms/ppo.py +++ b/maro/rl/training/algorithms/ppo.py @@ -2,9 +2,16 @@ # Licensed under the MIT license. from dataclasses import dataclass -from typing import Dict +from typing import Callable, Dict, Tuple -from maro.rl.training.algorithms.base import ACBasedParams, ACBasedTrainer +import numpy as np +import torch +from torch.distributions import Categorical + +from maro.rl.model import VNet +from maro.rl.policy import DiscretePolicyGradient, RLPolicy +from maro.rl.training.algorithms.base import ACBasedOps, ACBasedParams, ACBasedTrainer +from maro.rl.utils import discount_cumsum, ndarray_to_tensor, TransitionBatch @dataclass @@ -33,6 +40,130 @@ def __post_init__(self) -> None: assert self.clip_ratio is not None +class DiscretePPOWithEntropyOps(ACBasedOps): + def __init__( + self, + name: str, + policy_creator: Callable[[], RLPolicy], + get_v_critic_net_func: Callable[[], VNet], + parallelism: int = 1, + reward_discount: float = 0.9, + critic_loss_cls: Callable = None, + clip_ratio: float = None, + lam: float = 0.9, + min_logp: float = None, + is_discrete_action: bool = True, + ) -> None: + super(DiscretePPOWithEntropyOps, self).__init__( + name=name, + policy_creator=policy_creator, + get_v_critic_net_func=get_v_critic_net_func, + parallelism=parallelism, + reward_discount=reward_discount, + critic_loss_cls=critic_loss_cls, + clip_ratio=clip_ratio, + lam=lam, + min_logp=min_logp, + is_discrete_action=is_discrete_action, + ) + assert is_discrete_action + assert isinstance(self._policy, DiscretePolicyGradient) + self._policy_old = self._policy_creator() + self.update_policy_old() + + def update_policy_old(self) -> None: + self._policy_old.set_state(self._policy.get_state()) + + def _get_critic_loss(self, batch: TransitionBatch) -> torch.Tensor: + """Compute the critic loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The critic loss of the batch. + """ + self._v_critic_net.train() + states = ndarray_to_tensor(batch.states, self._device) + state_values = self._v_critic_net.v_values(states) + + values = state_values.cpu().detach().numpy() + values = np.concatenate([values[1:], values[-1:]]) + returns = batch.rewards + np.where(batch.terminals, 0.0, 1.0) * self._reward_discount * values + # special care for tail state + returns[-1] = state_values[-1] + returns = ndarray_to_tensor(returns, self._device) + + return self._critic_loss_func(state_values.float(), returns.float()) + + def _get_actor_loss(self, batch: TransitionBatch) -> Tuple[torch.Tensor, bool]: + """Compute the actor loss of the batch. + + Args: + batch (TransitionBatch): Batch. + + Returns: + loss (torch.Tensor): The actor loss of the batch. + early_stop (bool): Early stop indicator. + """ + assert isinstance(self._policy, DiscretePolicyGradient) + self._policy.train() + + states = ndarray_to_tensor(batch.states, device=self._device) + actions = ndarray_to_tensor(batch.actions, device=self._device) + advantages = ndarray_to_tensor(batch.advantages, device=self._device) + logps_old = ndarray_to_tensor(batch.old_logps, device=self._device) + if self._is_discrete_action: + actions = actions.long() + + action_probs = self._policy.get_action_probs(states) + dist_entropy = Categorical(action_probs).entropy() + logps = torch.log(action_probs.gather(1, actions).squeeze()) + logps = torch.clamp(logps, min=self._min_logp, max=.0) + if self._clip_ratio is not None: + ratio = torch.exp(logps - logps_old) + clipped_ratio = torch.clamp(ratio, 1 - self._clip_ratio, 1 + self._clip_ratio) + actor_loss = -(torch.min(ratio * advantages, clipped_ratio * advantages)).float() + kl = (logps_old - logps).mean().item() + early_stop = (kl >= 0.01 * 1.5) # TODO + else: + actor_loss = -(logps * advantages).float() # I * delta * log pi(a|s) + early_stop = False + actor_loss = (actor_loss - 0.2 * dist_entropy).mean() + + return actor_loss, early_stop + + def preprocess_batch(self, batch: TransitionBatch) -> TransitionBatch: + """Preprocess the batch to get the returns & advantages. + + Args: + batch (TransitionBatch): Batch. + + Returns: + The updated batch. + """ + assert isinstance(batch, TransitionBatch) + # Preprocess returns + batch.returns = discount_cumsum(batch.rewards, self._reward_discount) + + # Preprocess advantages + states = ndarray_to_tensor(batch.states, self._device) + state_values = self._v_critic_net.v_values(states).cpu().detach().numpy() + values = np.concatenate([state_values[1:], np.zeros(1).astype(np.float32)]) + deltas = (batch.rewards + self._reward_discount * values - state_values) + # special care for tail state + deltas[-1] = 0.0 + batch.advantages = discount_cumsum(deltas, self._reward_discount * self._lam) + + if self._clip_ratio is not None: + self._policy_old.eval() + actions = ndarray_to_tensor(batch.actions, device=self._device).long() + batch.old_logps = self._policy_old.get_states_actions_logps(states, actions).detach().cpu().numpy() + self._policy_old.train() + + return batch + + class PPOTrainer(ACBasedTrainer): """PPO algorithm. @@ -42,3 +173,24 @@ class PPOTrainer(ACBasedTrainer): def __init__(self, name: str, params: PPOParams) -> None: super(PPOTrainer, self).__init__(name, params) + + +class DiscretePPOWithEntropyTrainer(ACBasedTrainer): + def __init__(self, name: str, params: PPOParams) -> None: + super(DiscretePPOWithEntropyTrainer, self).__init__(name, params) + + def get_local_ops(self) -> DiscretePPOWithEntropyOps: + return DiscretePPOWithEntropyOps( + name=self._policy_name, + policy_creator=self._policy_creator, + parallelism=self._params.data_parallelism, + **self._params.extract_ops_params(), + ) + + def train_step(self) -> None: + assert isinstance(self._ops, DiscretePPOWithEntropyOps) + batch = self._get_batch() + for _ in range(self._params.grad_iters): + self._ops.update_critic(batch) + self._ops.update_actor(batch) + self._ops.update_policy_old() diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index 3dcb9b9fe..99ce9c30d 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -172,7 +172,7 @@ class SingleAgentTrainer(AbsTrainer, metaclass=ABCMeta): def __init__(self, name: str, params: TrainerParams) -> None: super(SingleAgentTrainer, self).__init__(name, params) self._policy_name: Optional[str] = None - self._policy_creator: Optional[Callable[[str], RLPolicy]] = None + self._policy_creator: Optional[Callable[[], RLPolicy]] = None self._ops: Optional[AbsTrainOps] = None self._replay_memory: Optional[ReplayMemory] = None diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/training_manager.py index d2a675253..a97b3747a 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/training_manager.py @@ -63,6 +63,9 @@ def __init__( # User-defined allocation of compute devices, i.e., GPU's to the trainer ops if explicit_assign_device: for policy_name, device_name in rl_component_bundle.device_mapping.items(): + if policy_name not in rl_component_bundle.policy_trainer_mapping: # No need to assign device + continue + trainer = self._trainer_dict[rl_component_bundle.policy_trainer_mapping[policy_name]] if isinstance(trainer, SingleAgentTrainer): diff --git a/maro/rl/workflows/config/parser.py b/maro/rl/workflows/config/parser.py index 47666e294..80e7b6971 100644 --- a/maro/rl/workflows/config/parser.py +++ b/maro/rl/workflows/config/parser.py @@ -3,7 +3,7 @@ import ipaddress import os -from typing import Union +from typing import Dict, Tuple, Union import yaml @@ -224,8 +224,8 @@ def get_path_mapping(self, containerize: bool = False) -> dict: containerize (bool): If true, the paths you specify in the configuration file (which should always be local) are mapped to paths inside the containers as follows: local/scenario/path -> "/scenario" - local/load/path -> "loadpoint" - local/checkpoint/path -> "checkpoints" + local/load/path -> "/loadpoint" + local/checkpoint/path -> "/checkpoints" local/log/path -> "/logs" Defaults to False. """ @@ -244,7 +244,7 @@ def get_path_mapping(self, containerize: bool = False) -> dict: return path_map - def as_env(self, containerize: bool = False) -> dict: + def get_job_spec(self, containerize: bool = False) -> Dict[str, Tuple[str, Dict[str, str]]]: """Generate environment variables for the workflow scripts. A doubly-nested dictionary is returned that contains the environment variables for each distributed component. @@ -257,17 +257,22 @@ def as_env(self, containerize: bool = False) -> dict: path_mapping = self.get_path_mapping(containerize=containerize) scenario_path = path_mapping[self._config["scenario_path"]] num_episodes = self._config["main"]["num_episodes"] + main_proc = f"{self._config['job']}.main" min_n_sample = self._config["main"].get("min_n_sample", 1) env = { - "main": { - "JOB": self._config["job"], - "NUM_EPISODES": str(num_episodes), - "MIN_N_SAMPLE": str(min_n_sample), - "TRAIN_MODE": self._config["training"]["mode"], - "SCENARIO_PATH": scenario_path, - } + main_proc: ( + os.path.join(self._get_workflow_path(containerize=containerize), "main.py"), + { + "JOB": self._config["job"], + "NUM_EPISODES": str(num_episodes), + "MIN_N_SAMPLE": str(min_n_sample), + "TRAIN_MODE": self._config["training"]["mode"], + "SCENARIO_PATH": scenario_path, + } + ) } + main_proc_env = env[main_proc][1] if "eval_schedule" in self._config["main"]: # If it is an int, it is treated as the number of episodes between two adjacent evaluations. For example, # if the total number of episodes is 20 and this is 5, an evaluation schedule of [5, 10, 15, 20] @@ -275,9 +280,9 @@ def as_env(self, containerize: bool = False) -> dict: # version of the list will be generated for the environment variable (as a string). sch = self._config["main"]["eval_schedule"] if isinstance(sch, int): - env["main"]["EVAL_SCHEDULE"] = " ".join([str(sch * i) for i in range(1, num_episodes // sch + 1)]) + main_proc_env["EVAL_SCHEDULE"] = " ".join([str(sch * i) for i in range(1, num_episodes // sch + 1)]) else: - env["main"]["EVAL_SCHEDULE"] = " ".join([str(val) for val in sorted(sch)]) + main_proc_env["EVAL_SCHEDULE"] = " ".join([str(val) for val in sorted(sch)]) load_path = self._config["training"].get("load_path", None) if load_path is not None: @@ -288,16 +293,16 @@ def as_env(self, containerize: bool = False) -> dict: if "checkpointing" in self._config["training"]: conf = self._config["training"]["checkpointing"] - env["main"]["CHECKPOINT_PATH"] = path_mapping[conf["path"]] + main_proc_env["CHECKPOINT_PATH"] = path_mapping[conf["path"]] if "interval" in conf: - env["main"]["CHECKPOINT_INTERVAL"] = str(conf["interval"]) + main_proc_env["CHECKPOINT_INTERVAL"] = str(conf["interval"]) num_steps = self._config["main"].get("num_steps", None) if num_steps is not None: - env["main"]["NUM_STEPS"] = str(num_steps) + main_proc_env["NUM_STEPS"] = str(num_steps) if "logging" in self._config["main"]: - env["main"].update({ + main_proc_env.update({ "LOG_LEVEL_STDOUT": self.config["main"]["logging"]["stdout"], "LOG_LEVEL_FILE": self.config["main"]["logging"]["file"], }) @@ -312,25 +317,28 @@ def as_env(self, containerize: bool = False) -> dict: if rollout_parallelism > 1: conf = self._config["rollout"]["parallelism"] rollout_controller_port = str(conf["controller"]["port"]) - env["main"]["ENV_SAMPLE_PARALLELISM"] = str(env_sampling_parallelism) - env["main"]["ENV_EVAL_PARALLELISM"] = str(env_eval_parallelism) - env["main"]["ROLLOUT_CONTROLLER_PORT"] = rollout_controller_port + main_proc_env["ENV_SAMPLE_PARALLELISM"] = str(env_sampling_parallelism) + main_proc_env["ENV_EVAL_PARALLELISM"] = str(env_eval_parallelism) + main_proc_env["ROLLOUT_CONTROLLER_PORT"] = rollout_controller_port # optional settings for parallel rollout if "min_env_samples" in self._config["rollout"]: - env["main"]["MIN_ENV_SAMPLES"] = str(conf["min_env_samples"]) + main_proc_env["MIN_ENV_SAMPLES"] = str(conf["min_env_samples"]) if "grace_factor" in self._config["rollout"]: - env["main"]["GRACE_FACTOR"] = str(conf["grace_factor"]) + main_proc_env["GRACE_FACTOR"] = str(conf["grace_factor"]) for i in range(rollout_parallelism): - worker_id = f"rollout_worker-{i}" - env[worker_id] = { - "ID": str(i), - "ROLLOUT_CONTROLLER_HOST": self._get_rollout_controller_host(containerize=containerize), - "ROLLOUT_CONTROLLER_PORT": rollout_controller_port, - "SCENARIO_PATH": scenario_path, - } + worker_id = f"{self._config['job']}.rollout_worker-{i}" + env[worker_id] = ( + os.path.join(self._get_workflow_path(containerize=containerize), "rollout_worker.py"), + { + "ID": str(i), + "ROLLOUT_CONTROLLER_HOST": self._get_rollout_controller_host(containerize=containerize), + "ROLLOUT_CONTROLLER_PORT": rollout_controller_port, + "SCENARIO_PATH": scenario_path, + } + ) if "logging" in self._config["rollout"]: - env[worker_id].update({ + env[worker_id][1].update({ "LOG_LEVEL_STDOUT": self.config["rollout"]["logging"]["stdout"], "LOG_LEVEL_FILE": self.config["rollout"]["logging"]["file"], }) @@ -341,34 +349,43 @@ def as_env(self, containerize: bool = False) -> dict: proxy_frontend_port = str(conf["frontend"]) proxy_backend_port = str(conf["backend"]) num_workers = self._config["training"]["num_workers"] - env["main"].update({ + env[main_proc][1].update({ "TRAIN_PROXY_HOST": producer_host, "TRAIN_PROXY_FRONTEND_PORT": proxy_frontend_port, }) - env["train_proxy"] = { - "TRAIN_PROXY_FRONTEND_PORT": proxy_frontend_port, - "TRAIN_PROXY_BACKEND_PORT": proxy_backend_port, - } + env[f"{self._config['job']}.train_proxy"] = ( + os.path.join(self._get_workflow_path(containerize=containerize), "train_proxy.py"), + {"TRAIN_PROXY_FRONTEND_PORT": proxy_frontend_port, "TRAIN_PROXY_BACKEND_PORT": proxy_backend_port} + ) for i in range(num_workers): - worker_id = f"train_worker-{i}" - env[worker_id] = { - "ID": str(i), - "TRAIN_PROXY_HOST": producer_host, - "TRAIN_PROXY_BACKEND_PORT": proxy_backend_port, - "SCENARIO_PATH": scenario_path, - } + worker_id = f"{self._config['job']}.train_worker-{i}" + env[worker_id] = ( + os.path.join(self._get_workflow_path(containerize=containerize), "train_worker.py"), + { + "ID": str(i), + "TRAIN_PROXY_HOST": producer_host, + "TRAIN_PROXY_BACKEND_PORT": proxy_backend_port, + "SCENARIO_PATH": scenario_path, + } + ) if "logging" in self._config["training"]: - env[worker_id].update({ + env[worker_id][1].update({ "LOG_LEVEL_STDOUT": self.config["training"]["logging"]["stdout"], "LOG_LEVEL_FILE": self.config["training"]["logging"]["file"], }) # All components write logs to the same file log_dir, log_file = os.path.split(self._config["log_path"]) - for vars in env.values(): + for _, vars in env.values(): vars["LOG_PATH"] = os.path.join(path_mapping[log_dir], log_file) return env + def _get_workflow_path(self, containerize: bool = False) -> str: + if containerize: + return "/maro/maro/rl/workflows" + else: + return os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + def _get_rollout_controller_host(self, containerize: bool = False) -> str: if containerize: return f"{self._config['job']}.main" diff --git a/maro/utils/logger.py b/maro/utils/logger.py index 34fd30fc0..37dcc0936 100644 --- a/maro/utils/logger.py +++ b/maro/utils/logger.py @@ -36,6 +36,7 @@ class LogFormat(Enum): cli_debug = 4 cli_info = 5 none = 6 + time_only = 7 FORMAT_NAME_TO_FILE_FORMAT = { @@ -49,7 +50,8 @@ class LogFormat(Enum): fmt="%(asctime)s | %(levelname)-7s | %(threadName)-10s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"), LogFormat.cli_info: logging.Formatter( fmt="%(asctime)s | %(levelname)-7s | %(threadName)-10s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"), - LogFormat.none: None + LogFormat.none: None, + LogFormat.time_only: logging.Formatter(fmt='%(asctime)s | %(message)s', datefmt='%H:%M:%S'), } FORMAT_NAME_TO_STDOUT_FORMAT = { @@ -85,49 +87,71 @@ def _msgformatter(self, msg, *args): class Logger(object): """A simple wrapper for logging. - The Logger hosts a file handler and a stdout handler. The file handler is set - to ``DEBUG`` level and will dump all logs info to the given ``dump_path``. - Supported log levels include: ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, ``CRITICAL``, ``PROCESS``. - + to ``DEBUG`` level and will dump all the logging info to the given ``dump_folder``. + The logging level of the stdout handler is decided by the ``stdout_level``, + and can be redirected by setting the environment variable ``LOG_LEVEL``. + Supported ``LOG_LEVEL`` includes: ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, + ``CRITICAL``, ``PROCESS``. + Example: + ``$ export LOG_LEVEL=INFO`` Args: tag (str): Log tag for stream and file output. format_ (LogFormat): Predefined formatter. Defaults to ``LogFormat.full``. - dump_path (str): Path of file for dumping logs. Must be an absolute path. The log level for dumping is - ``logging.DEBUG``. Defaults to None, in which case logs generated by the logger will not be dumped - to a file. - dump_mode (str): Write log file mode. Defaults to ``w``. Use ``a`` to append log. - stdout_level (str): the logging level of the stdout handler. Defaults to ``INFO``. - file_level (str): the logging level of the file handler. Defaults to ``DEBUG``. + dump_folder (str): Log dumped folder. Defaults to the current folder. + The dumped log level is ``logging.DEBUG``. The full path of the + dumped log file is `dump_folder/tag.log`. + dump_mode (str): Write log file mode. Defaults to ``w``. Use ``a`` to + append log. + extension_name (str): Final dumped file extension name. Defaults to `log`. + auto_timestamp (bool): Add a timestamp to the dumped log file name or not. + E.g: `tag.1574953673.137387.log`. + stdout_level (str): the logging level of the stdout handler. Defaults to + ``DEBUG``. """ def __init__( - self, tag: str, format_: LogFormat = LogFormat.simple, dump_path: str = None, dump_mode: str = 'w', - stdout_level="INFO", file_level="DEBUG" + self, tag: str, format_: LogFormat = LogFormat.simple, dump_folder: str = cwd, dump_mode: str = 'w', + extension_name: str = 'log', auto_timestamp: bool = False, stdout_level="INFO" ): self._file_format = FORMAT_NAME_TO_FILE_FORMAT[format_] self._stdout_format = FORMAT_NAME_TO_STDOUT_FORMAT[format_] \ if format_ in FORMAT_NAME_TO_STDOUT_FORMAT else \ FORMAT_NAME_TO_FILE_FORMAT[format_] - self._stdout_level = LEVEL_MAP[stdout_level] if isinstance(stdout_level, str) else stdout_level - self._file_level = LEVEL_MAP[file_level] if isinstance(file_level, str) else file_level + self._stdout_level = os.environ.get('LOG_LEVEL') or stdout_level self._logger = logging.getLogger(tag) self._logger.setLevel(logging.DEBUG) + self._extension_name = extension_name + + if not os.path.exists(dump_folder): + try: + os.makedirs(dump_folder) + except FileExistsError: + logging.warning("Receive File Exist Error about creating dump folder for internal log. " + "It may be caused by multi-thread and it won't have any impact on logger dumps.") + except Exception as e: + raise e + + if auto_timestamp: + filename = f'{tag}.{datetime.now().timestamp()}' + else: + filename = f'{tag}' - if dump_path: - os.makedirs(os.path.dirname(dump_path), exist_ok=True) - # File handler - fh = logging.FileHandler(filename=dump_path, mode=dump_mode, encoding="utf-8") - fh.setLevel(self._file_level) - if self._file_format is not None: - fh.setFormatter(self._file_format) - self._logger.addHandler(fh) + filename += f'.{self._extension_name}' + + # File handler + fh = logging.FileHandler(filename=f'{os.path.join(dump_folder, filename)}', mode=dump_mode, encoding="utf-8") + fh.setLevel(logging.DEBUG) + if self._file_format is not None: + fh.setFormatter(self._file_format) # Stdout handler sh = logging.StreamHandler(sys.stdout) sh.setLevel(self._stdout_level) if self._stdout_format is not None: sh.setFormatter(self._stdout_format) + + self._logger.addHandler(fh) self._logger.addHandler(sh) self._extra = {'host': socket.gethostname(), 'user': getpass.getuser(), 'tag': tag} From 0d132ccb3020b14fee3c3a007d135122bc026e13 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 25 May 2022 12:14:40 +0800 Subject: [PATCH 475/482] Refine `terminal` / `next_agent_state` logic (#531) * Optimize RL toolkit * Fix bug in terminal/next_state generation * Rewrite terminal/next_state logic again * Minor renaming * Minor bug fix * Resolve PR comments --- maro/rl/policy/abs_policy.py | 3 + maro/rl/policy/continuous_rl_policy.py | 2 +- maro/rl/policy/discrete_rl_policy.py | 2 +- maro/rl/rl_component/rl_component_bundle.py | 8 +- maro/rl/rollout/batch_env_sampler.py | 16 +- maro/rl/rollout/env_sampler.py | 246 +++++----- .../training/algorithms/base/ac_ppo_base.py | 2 - maro/rl/training/algorithms/ddpg.py | 2 - maro/rl/training/algorithms/dqn.py | 2 - maro/rl/training/algorithms/maddpg.py | 2 - maro/rl/training/algorithms/sac.py | 1 - maro/rl/training/trainer.py | 2 + maro/rl/training/training_manager.py | 2 +- .../rl_formulation.ipynb | 420 ------------------ 14 files changed, 151 insertions(+), 559 deletions(-) delete mode 100644 notebooks/container_inventory_management/rl_formulation.ipynb diff --git a/maro/rl/policy/abs_policy.py b/maro/rl/policy/abs_policy.py index b8f6d04fa..86a980275 100644 --- a/maro/rl/policy/abs_policy.py +++ b/maro/rl/policy/abs_policy.py @@ -142,6 +142,7 @@ def __init__( name: str, state_dim: int, action_dim: int, + is_discrete_action: bool, trainable: bool = True, ) -> None: super(RLPolicy, self).__init__(name=name, trainable=trainable) @@ -151,6 +152,8 @@ def __init__( self._device: Optional[torch.device] = None + self.is_discrete_action = is_discrete_action + @property def state_dim(self) -> int: return self._state_dim diff --git a/maro/rl/policy/continuous_rl_policy.py b/maro/rl/policy/continuous_rl_policy.py index 51b3faed0..22e14b610 100644 --- a/maro/rl/policy/continuous_rl_policy.py +++ b/maro/rl/policy/continuous_rl_policy.py @@ -55,7 +55,7 @@ def __init__( super(ContinuousRLPolicy, self).__init__( name=name, state_dim=policy_net.state_dim, action_dim=policy_net.action_dim, - trainable=trainable, + trainable=trainable, is_discrete_action=False, ) self._lbounds, self._ubounds = _parse_action_range(self.action_dim, action_range) diff --git a/maro/rl/policy/discrete_rl_policy.py b/maro/rl/policy/discrete_rl_policy.py index 0d7374423..42babab50 100644 --- a/maro/rl/policy/discrete_rl_policy.py +++ b/maro/rl/policy/discrete_rl_policy.py @@ -35,7 +35,7 @@ def __init__( assert action_num >= 1 super(DiscreteRLPolicy, self).__init__( - name=name, state_dim=state_dim, action_dim=1, trainable=trainable, + name=name, state_dim=state_dim, action_dim=1, trainable=trainable, is_discrete_action=True, ) self._action_num = action_num diff --git a/maro/rl/rl_component/rl_component_bundle.py b/maro/rl/rl_component/rl_component_bundle.py index 437433771..729f1ce85 100644 --- a/maro/rl/rl_component/rl_component_bundle.py +++ b/maro/rl/rl_component/rl_component_bundle.py @@ -3,7 +3,7 @@ from abc import abstractmethod from functools import partial -from typing import Any, Callable, Dict, Iterable, List, Optional +from typing import Any, Callable, Dict, List, Optional from maro.rl.policy import AbsPolicy from maro.rl.rollout import AbsEnvSampler @@ -155,7 +155,11 @@ def _complete_resources(self) -> None: required_policies = set(self.agent2policy.values()) self.policy_creator = {name: self.policy_creator[name] for name in required_policies} - self.policy_trainer_mapping = {name: self.policy_trainer_mapping[name] for name in required_policies} + self.policy_trainer_mapping = { + name: self.policy_trainer_mapping[name] + for name in required_policies + if name in self.policy_trainer_mapping + } self.policy_names = list(required_policies) assert len(required_policies) == len(self.policy_creator) # Should have same size after filter diff --git a/maro/rl/rollout/batch_env_sampler.py b/maro/rl/rollout/batch_env_sampler.py index 0fc4b62a0..885687d1e 100644 --- a/maro/rl/rollout/batch_env_sampler.py +++ b/maro/rl/rollout/batch_env_sampler.py @@ -43,7 +43,7 @@ def _wait_for_workers_ready(self, k: int) -> None: while len(self._workers) < k: self._workers.add(self._task_endpoint.recv_multipart()[0]) - def _recv_result_for_target_index(self, index: Tuple[int, int]) -> object: + def _recv_result_for_target_index(self, index: int) -> object: rep = bytes_to_pyobj(self._task_endpoint.recv_multipart()[-1]) assert isinstance(rep, dict) return rep["result"] if rep["index"] == index else None @@ -141,7 +141,6 @@ def __init__( self._eval_parallelism = 1 if eval_parallelism is None else eval_parallelism self._ep = 0 - self._segment = 0 self._end_of_episode = True def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Optional[int] = None) -> dict: @@ -157,19 +156,16 @@ def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Op Returns: A dict that contains the collected experiences and additional information. """ - # increment episode or segment depending on whether the last episode has concluded + # increment episode depending on whether the last episode has concluded if self._end_of_episode: self._ep += 1 - self._segment = 1 - else: - self._segment += 1 - self._logger.info(f"Collecting roll-out data for episode {self._ep}, segment {self._segment}") + self._logger.info(f"Collecting roll-out data for episode {self._ep}") req = { "type": "sample", "policy_state": policy_state, "num_steps": num_steps, - "index": (self._ep, self._segment), + "index": self._ep, } results = self._controller.collect( req, self._sampling_parallelism, @@ -185,7 +181,7 @@ def sample(self, policy_state: Optional[Dict[str, object]] = None, num_steps: Op } def eval(self, policy_state: Dict[str, object] = None) -> dict: - req = {"type": "eval", "policy_state": policy_state, "index": (self._ep, -1)} # -1 signals test + req = {"type": "eval", "policy_state": policy_state, "index": self._ep} # -1 signals test results = self._controller.collect(req, self._eval_parallelism) return { "info": [res["info"][0] for res in results], @@ -205,7 +201,7 @@ def load_policy_state(self, path: str) -> List[str]: req = { "type": "set_policy_state", "policy_state": policy_state_dict, - "index": (self._ep, -1), + "index": self._ep, } self._controller.collect(req, self._sampling_parallelism) return loaded diff --git a/maro/rl/rollout/env_sampler.py b/maro/rl/rollout/env_sampler.py index 3030cc590..f396b2342 100644 --- a/maro/rl/rollout/env_sampler.py +++ b/maro/rl/rollout/env_sampler.py @@ -7,10 +7,9 @@ import os import typing from abc import ABCMeta, abstractmethod -from collections import defaultdict, deque from copy import deepcopy from dataclasses import dataclass -from typing import Any, Deque, Dict, List, Optional, Tuple, Type, Union +from typing import Any, Dict, List, Optional, Tuple, Type, Union import numpy as np import torch @@ -107,8 +106,8 @@ def _choose_actions_impl( self, state_by_agent: Dict[Any, Union[np.ndarray, List[object]]], ) -> Dict[Any, Union[np.ndarray, List[object]]]: # Aggregate states by policy - states_by_policy = defaultdict(list) # {str: list of np.ndarray} - agents_by_policy = defaultdict(list) # {str: list of str} + states_by_policy = collections.defaultdict(list) # {str: list of np.ndarray} + agents_by_policy = collections.defaultdict(list) # {str: list of str} for agent_name, state in state_by_agent.items(): policy_name = self._agent2policy[agent_name] states_by_policy[policy_name].append(state) @@ -140,19 +139,6 @@ def switch_to_eval_mode(self) -> None: policy.eval() -@dataclass -class CacheElement: - """Raw transition information that can be post-processed into an `ExpElement`. - """ - tick: int - event: object - state: np.ndarray - agent_state_dict: Dict[Any, np.ndarray] - action_dict: Dict[Any, np.ndarray] - env_action_dict: Dict[Any, np.ndarray] - reward_dict: Dict[Any, float] = None - - @dataclass class ExpElement: """Stores the complete information for a tick. @@ -164,7 +150,7 @@ class ExpElement: reward_dict: Dict[Any, float] terminal_dict: Dict[Any, bool] next_state: Optional[np.ndarray] - next_agent_state_dict: Optional[Dict[Any, np.ndarray]] + next_agent_state_dict: Dict[Any, np.ndarray] @property def agent_names(self) -> list: @@ -174,7 +160,24 @@ def agent_names(self) -> list: def num_agents(self) -> int: return len(self.agent_state_dict) - def split_contents(self, agent2trainer: Dict[Any, str]) -> Dict[str, ExpElement]: + def split_contents_by_agent(self) -> Dict[Any, ExpElement]: + ret = {} + for agent_name in self.agent_state_dict.keys(): + ret[agent_name] = ExpElement( + tick=self.tick, + state=self.state, + agent_state_dict={agent_name: self.agent_state_dict[agent_name]}, + action_dict={agent_name: self.action_dict[agent_name]}, + reward_dict={agent_name: self.reward_dict[agent_name]}, + terminal_dict={agent_name: self.terminal_dict[agent_name]}, + next_state=self.next_state, + next_agent_state_dict={ + agent_name: self.next_agent_state_dict[agent_name] + } if self.next_agent_state_dict is not None and agent_name in self.next_agent_state_dict else {}, + ) + return ret + + def split_contents_by_trainer(self, agent2trainer: Dict[Any, str]) -> Dict[str, ExpElement]: """Split the ExpElement's contents by trainer. Args: @@ -205,6 +208,27 @@ def split_contents(self, agent2trainer: Dict[Any, str]) -> Dict[str, ExpElement] return ret +@dataclass +class CacheElement(ExpElement): + event: object + env_action_dict: Dict[Any, np.ndarray] + + def make_exp_element(self) -> ExpElement: + assert len(self.terminal_dict) == len(self.agent_state_dict) == len(self.action_dict) + assert len(self.terminal_dict) == len(self.next_agent_state_dict) == len(self.reward_dict) + + return ExpElement( + tick=self.tick, + state=self.state, + agent_state_dict=self.agent_state_dict, + action_dict=self.action_dict, + reward_dict=self.reward_dict, + terminal_dict=self.terminal_dict, + next_state=self.next_state, + next_agent_state_dict=self.next_agent_state_dict, + ) + + class AbsEnvSampler(object, metaclass=ABCMeta): """Simulation data collector and policy evaluator. @@ -228,10 +252,13 @@ def __init__( self._agent_wrapper_cls = agent_wrapper_cls + self._event = None + self._end_of_episode = True self._state: Optional[np.ndarray] = None self._agent_state_dict: Dict[Any, np.ndarray] = {} - self._trans_cache: Deque[CacheElement] = deque() + self._trans_cache: List[CacheElement] = [] + self._agent_last_index: Dict[Any, int] = {} # Index of last occurrence of agent in self._trans_cache self._reward_eval_delay = reward_eval_delay self._info = {} @@ -247,7 +274,6 @@ def build( rl_component_bundle (RLComponentBundle): The RL component bundle of the job. """ self._env: Optional[Env] = None - self._event = None # Need this to remember the last event if an episode is divided into multiple segments self._policy_dict = { policy_name: rl_component_bundle.policy_creator[policy_name]() @@ -328,12 +354,53 @@ def _get_reward(self, env_action_dict: Dict[Any, object], event: object, tick: i """ raise NotImplementedError + def _step(self, actions: Optional[list]) -> None: + _, self._event, self._end_of_episode = self._env.step(actions) + self._state, self._agent_state_dict = (None, {}) \ + if self._end_of_episode else self._get_global_and_agent_state(self._event) + + def _calc_reward(self, cache_element: CacheElement) -> None: + cache_element.reward_dict = self._get_reward( + cache_element.env_action_dict, cache_element.event, cache_element.tick, + ) + + def _append_cache_element(self, cache_element: Optional[CacheElement]) -> None: + """`cache_element` == None means we are processing the last element in trans_cache""" + if cache_element is None: + if len(self._trans_cache) > 0: + self._trans_cache[-1].next_state = self._trans_cache[-1].state + + for agent_name, i in self._agent_last_index.items(): + e = self._trans_cache[i] + e.terminal_dict[agent_name] = self._end_of_episode + e.next_agent_state_dict[agent_name] = e.agent_state_dict[agent_name] + else: + self._trans_cache.append(cache_element) + + if len(self._trans_cache) > 0: + self._trans_cache[-1].next_state = cache_element.state + + cur_index = len(self._trans_cache) - 1 + for agent_name in cache_element.agent_names: + if agent_name in self._agent_last_index: + i = self._agent_last_index[agent_name] + self._trans_cache[i].terminal_dict[agent_name] = False + self._trans_cache[i].next_agent_state_dict[agent_name] = cache_element.agent_state_dict[agent_name] + self._agent_last_index[agent_name] = cur_index + def _reset(self) -> None: self._env.reset() self._info.clear() self._trans_cache.clear() - _, self._event, _ = self._env.step(None) - self._state, self._agent_state_dict = self._get_global_and_agent_state(self._event) + self._agent_last_index.clear() + self._step(None) + + def _select_trainable_agents(self, original_dict: dict) -> dict: + return { + k: v + for k, v in original_dict.items() + if k in self._trainable_agents + } def sample(self, policy_state: Optional[Dict[str, dict]] = None, num_steps: Optional[int] = None) -> dict: """Sample experiences. @@ -349,7 +416,7 @@ def sample(self, policy_state: Optional[Dict[str, dict]] = None, num_steps: Opti """ # Init the env self._env = self._learn_env - if not self._agent_state_dict: + if self._end_of_episode: self._reset() # Update policy state if necessary @@ -359,7 +426,7 @@ def sample(self, policy_state: Optional[Dict[str, dict]] = None, num_steps: Opti # Collect experience self._agent_wrapper.explore() steps_to_go = float("inf") if num_steps is None else num_steps - while self._agent_state_dict and steps_to_go > 0: + while not self._end_of_episode and steps_to_go > 0: # Get agent actions and translate them to env actions action_dict = self._agent_wrapper.choose_actions(self._agent_state_dict) env_action_dict = self._translate_to_env_action(action_dict, self._event) @@ -369,96 +436,48 @@ def sample(self, policy_state: Optional[Dict[str, dict]] = None, num_steps: Opti tick=self._env.tick, event=self._event, state=self._state, - agent_state_dict={ - id_: state - for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents - }, - action_dict={ - id_: action - for id_, action in action_dict.items() if id_ in self._trainable_agents - }, - env_action_dict={ - id_: env_action - for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents - }, + agent_state_dict=self._select_trainable_agents(self._agent_state_dict), + action_dict=self._select_trainable_agents(action_dict), + env_action_dict=self._select_trainable_agents(env_action_dict), + # The following will be generated later + reward_dict={}, + terminal_dict={}, + next_state=None, + next_agent_state_dict={}, ) # Update env and get new states (global & agent) - _, self._event, is_done = self._env.step(list(env_action_dict.values())) - self._state, self._agent_state_dict = (None, {}) if is_done \ - else self._get_global_and_agent_state(self._event) + self._step(list(env_action_dict.values())) if self._reward_eval_delay is None: - cache_element.reward_dict = self._get_reward( - cache_element.env_action_dict, cache_element.event, cache_element.tick, - ) + self._calc_reward(cache_element) self._post_step(cache_element) - self._trans_cache.append(cache_element) - + self._append_cache_element(cache_element) steps_to_go -= 1 + self._append_cache_element(None) tick_bound = self._env.tick - (0 if self._reward_eval_delay is None else self._reward_eval_delay) - experiences = [] + experiences: List[ExpElement] = [] while len(self._trans_cache) > 0 and self._trans_cache[0].tick <= tick_bound: - cache_element = self._trans_cache.popleft() + cache_element = self._trans_cache.pop(0) # !: Here the reward calculation method requires the given tick is enough and must be used then. if self._reward_eval_delay is not None: - cache_element.reward_dict = self._get_reward( - cache_element.env_action_dict, cache_element.event, cache_element.tick, - ) + self._calc_reward(cache_element) self._post_step(cache_element) + experiences.append(cache_element.make_exp_element()) - if len(self._trans_cache) > 0: - next_state = self._trans_cache[0].state - next_agent_state_dict = dict(self._trans_cache[0].agent_state_dict) - else: - next_state = self._state - next_agent_state_dict = { - id_: state for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents - } - - experiences.append(ExpElement( - tick=cache_element.tick, - state=cache_element.state, - agent_state_dict=cache_element.agent_state_dict, - action_dict=cache_element.action_dict, - reward_dict=cache_element.reward_dict, - terminal_dict={}, # Will be processed later in `_post_polish_experiences()` - next_state=next_state, - next_agent_state_dict=next_agent_state_dict, - )) - - experiences = self._post_polish_experiences(experiences) + self._agent_last_index = { + k: v - len(experiences) + for k, v in self._agent_last_index.items() + if v >= len(experiences) + } return { - "end_of_episode": not self._agent_state_dict, + "end_of_episode": self._end_of_episode, "experiences": [experiences], "info": [deepcopy(self._info)], # TODO: may have overhead issues. Leave to future work. } - def _post_polish_experiences(self, experiences: List[ExpElement]) -> List[ExpElement]: - """Update next_agent_state_dict & terminal_dict using the entire experience list. - - Args: - experiences (List[ExpElement]): Sequence of ExpElements. - - Returns: - The update sequence of ExpElements. - """ - latest_agent_state_dict = {} # Used to update next_agent_state_dict - have_log = set([]) # Used to update terminal_dict - for i in range(len(experiences))[::-1]: - # Update terminal_dict - for agent_name in experiences[i].agent_state_dict: - experiences[i].terminal_dict[agent_name] = (not self._agent_state_dict and agent_name not in have_log) - have_log.add(agent_name) - # Update next_agent_state_dict - for key, value in latest_agent_state_dict.items(): - if key not in experiences[i].next_agent_state_dict: - experiences[i].next_agent_state_dict[key] = value - latest_agent_state_dict.update(experiences[i].agent_state_dict) - return experiences - def set_policy_state(self, policy_state_dict: Dict[str, dict]) -> None: """Set policies' states. @@ -488,7 +507,7 @@ def eval(self, policy_state: Dict[str, dict] = None) -> dict: self.set_policy_state(policy_state) self._agent_wrapper.exploit() - while self._agent_state_dict: + while not self._end_of_episode: action_dict = self._agent_wrapper.choose_actions(self._agent_state_dict) env_action_dict = self._translate_to_env_action(action_dict, self._event) @@ -497,34 +516,31 @@ def eval(self, policy_state: Dict[str, dict] = None) -> dict: tick=self._env.tick, event=self._event, state=self._state, - agent_state_dict={ - id_: state for id_, state in self._agent_state_dict.items() if id_ in self._trainable_agents - }, - action_dict={id_: action for id_, action in action_dict.items() if id_ in self._trainable_agents}, - env_action_dict={ - id_: env_action for id_, env_action in env_action_dict.items() if id_ in self._trainable_agents - }, + agent_state_dict=self._select_trainable_agents(self._agent_state_dict), + action_dict=self._select_trainable_agents(action_dict), + env_action_dict=self._select_trainable_agents(env_action_dict), + # The following will be generated later + reward_dict={}, + terminal_dict={}, + next_state=None, + next_agent_state_dict={}, ) # Update env and get new states (global & agent) - _, self._event, is_done = self._env.step(list(env_action_dict.values())) - self._state, self._agent_state_dict = (None, {}) if is_done \ - else self._get_global_and_agent_state(self._event) + self._step(list(env_action_dict.values())) - if self._reward_eval_delay is None: - cache_element.reward_dict = self._get_reward( - cache_element.env_action_dict, cache_element.event, cache_element.tick, - ) + if self._reward_eval_delay is None: # TODO: necessary to calculate reward in eval()? + self._calc_reward(cache_element) self._post_eval_step(cache_element) - self._trans_cache.append(cache_element) + + self._append_cache_element(cache_element) + self._append_cache_element(None) tick_bound = self._env.tick - (0 if self._reward_eval_delay is None else self._reward_eval_delay) while len(self._trans_cache) > 0 and self._trans_cache[0].tick <= tick_bound: - cache_element = self._trans_cache.popleft() + cache_element = self._trans_cache.pop(0) if self._reward_eval_delay is not None: - cache_element.reward_dict = self._get_reward( - cache_element.env_action_dict, cache_element.event, cache_element.tick, - ) + self._calc_reward(cache_element) self._post_eval_step(cache_element) return {"info": [self._info]} diff --git a/maro/rl/training/algorithms/base/ac_ppo_base.py b/maro/rl/training/algorithms/base/ac_ppo_base.py index 1c5cf5721..5c93e9aec 100644 --- a/maro/rl/training/algorithms/base/ac_ppo_base.py +++ b/maro/rl/training/algorithms/base/ac_ppo_base.py @@ -20,7 +20,6 @@ class ACBasedParams(TrainerParams, metaclass=ABCMeta): Parameter bundle for Actor-Critic based algorithms (Actor-Critic & PPO) get_v_critic_net_func (Callable[[], VNet]): Function to get V critic net. - reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. grad_iters (int, default=1): Number of iterations to calculate gradients. critic_loss_cls (Callable, default=None): Critic loss function. If it is None, use MSE. lam (float, default=0.9): Lambda value for generalized advantage estimation (TD-Lambda). @@ -30,7 +29,6 @@ class ACBasedParams(TrainerParams, metaclass=ABCMeta): is_discrete_action (bool, default=True): Indicator of continuous or discrete action policy. """ get_v_critic_net_func: Callable[[], VNet] = None - reward_discount: float = 0.9 grad_iters: int = 1 critic_loss_cls: Callable = None lam: float = 0.9 diff --git a/maro/rl/training/algorithms/ddpg.py b/maro/rl/training/algorithms/ddpg.py index e96e194aa..601aed6e0 100644 --- a/maro/rl/training/algorithms/ddpg.py +++ b/maro/rl/training/algorithms/ddpg.py @@ -17,7 +17,6 @@ class DDPGParams(TrainerParams): """ get_q_critic_net_func (Callable[[], QNet]): Function to get Q critic net. - reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. num_epochs (int, default=1): Number of training epochs per call to ``learn``. update_target_every (int, default=5): Number of training rounds between policy target model updates. q_value_loss_cls (str, default=None): A string indicating a loss class provided by torch.nn or a custom @@ -31,7 +30,6 @@ class DDPGParams(TrainerParams): min_num_to_trigger_training (int, default=0): Minimum number required to start training. """ get_q_critic_net_func: Callable[[], QNet] = None - reward_discount: float = 0.9 num_epochs: int = 1 update_target_every: int = 5 q_value_loss_cls: Callable = None diff --git a/maro/rl/training/algorithms/dqn.py b/maro/rl/training/algorithms/dqn.py index bd9577be0..36a79dee3 100644 --- a/maro/rl/training/algorithms/dqn.py +++ b/maro/rl/training/algorithms/dqn.py @@ -14,7 +14,6 @@ @dataclass class DQNParams(TrainerParams): """ - reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. num_epochs (int, default=1): Number of training epochs. update_target_every (int, default=5): Number of gradient steps between target model updates. soft_update_coef (float, default=0.1): Soft update coefficient, e.g., @@ -26,7 +25,6 @@ class DQNParams(TrainerParams): is reached. If True, overwrite positions will be selected randomly. Otherwise, overwrites will occur sequentially with wrap-around. """ - reward_discount: float = 0.9 num_epochs: int = 1 update_target_every: int = 5 soft_update_coef: float = 0.1 diff --git a/maro/rl/training/algorithms/maddpg.py b/maro/rl/training/algorithms/maddpg.py index f0e626c05..2a6a2b688 100644 --- a/maro/rl/training/algorithms/maddpg.py +++ b/maro/rl/training/algorithms/maddpg.py @@ -26,7 +26,6 @@ class DiscreteMADDPGParams(TrainerParams): update_target_every (int, default=5): Number of gradient steps between target model updates. soft_update_coef (float, default=0.5): Soft update coefficient, e.g., target_model = (soft_update_coef) * eval_model + (1-soft_update_coef) * target_model. - reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. q_value_loss_cls (Callable, default=None): Critic loss function. If it is None, use MSE. shared_critic (bool, default=False): Whether different policies use shared critic or individual policies. """ @@ -34,7 +33,6 @@ class DiscreteMADDPGParams(TrainerParams): num_epoch: int = 10 update_target_every: int = 5 soft_update_coef: float = 0.5 - reward_discount: float = 0.9 q_value_loss_cls: Callable = None shared_critic: bool = False diff --git a/maro/rl/training/algorithms/sac.py b/maro/rl/training/algorithms/sac.py index 6b812fdca..e03743001 100644 --- a/maro/rl/training/algorithms/sac.py +++ b/maro/rl/training/algorithms/sac.py @@ -18,7 +18,6 @@ class SoftActorCriticParams(TrainerParams): entropy_coef: float = 0.1 num_epochs: int = 1 n_start_train: int = 0 - reward_discount: float = 0.9 q_value_loss_cls: Callable = None soft_update_coef: float = 1.0 diff --git a/maro/rl/training/trainer.py b/maro/rl/training/trainer.py index 99ce9c30d..1dc0dac2c 100644 --- a/maro/rl/training/trainer.py +++ b/maro/rl/training/trainer.py @@ -34,11 +34,13 @@ class TrainerParams: availability of compute resources. For details on distributed deep learning and data parallelism, see https://web.stanford.edu/~rezab/classes/cme323/S16/projects_reports/hedge_usmani.pdf, as well as an abundance of resources available on the internet. + reward_discount (float, default=0.9): Reward decay as defined in standard RL terminology. """ replay_memory_capacity: int = 10000 batch_size: int = 128 data_parallelism: int = 1 + reward_discount: float = 0.9 @abstractmethod def extract_ops_params(self) -> Dict[str, object]: diff --git a/maro/rl/training/training_manager.py b/maro/rl/training/training_manager.py index a97b3747a..85b2c2c7d 100644 --- a/maro/rl/training/training_manager.py +++ b/maro/rl/training/training_manager.py @@ -112,7 +112,7 @@ def record_experiences(self, experiences: List[List[ExpElement]]) -> None: for env_idx, env_experience in enumerate(experiences): trainer_exp_pool = collections.defaultdict(list) for exp_element in env_experience: # Dispatch experiences to trainers tick by tick. - exp_dict = exp_element.split_contents(self._agent2trainer) + exp_dict = exp_element.split_contents_by_trainer(self._agent2trainer) for trainer_name, exp_elem in exp_dict.items(): trainer_exp_pool[trainer_name].append(exp_elem) diff --git a/notebooks/container_inventory_management/rl_formulation.ipynb b/notebooks/container_inventory_management/rl_formulation.ipynb deleted file mode 100644 index 02d6f03d3..000000000 --- a/notebooks/container_inventory_management/rl_formulation.ipynb +++ /dev/null @@ -1,420 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Quick Start\n", - "\n", - "This notebook demonstrates the use of MARO's RL toolkit to optimize container inventory management. The scenario consists of a set of ports, each acting as a learning agent, and vessels that transfer empty containers among them. Each port must decide 1) whether to load or discharge containers when a vessel arrives and 2) how many containers to be loaded or discharged. The objective is to minimize the overall container shortage over a certain period of time." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# env and shaping config\n", - "env_conf = {\n", - " \"scenario\": \"cim\",\n", - " \"topology\": \"toy.4p_ssdd_l0.0\",\n", - " \"durations\": 560\n", - "}\n", - "\n", - "port_attributes = [\"empty\", \"full\", \"on_shipper\", \"on_consignee\", \"booking\", \"shortage\", \"fulfillment\"]\n", - "vessel_attributes = [\"empty\", \"full\", \"remaining_space\"]\n", - "\n", - "state_shaping_conf = {\n", - " \"look_back\": 7,\n", - " \"max_ports_downstream\": 2\n", - "}\n", - "\n", - "action_shaping_conf = {\n", - " \"action_space\": [(i - 10) / 10 for i in range(21)],\n", - " \"finite_vessel_space\": True,\n", - " \"has_early_discharge\": True\n", - "}\n", - "\n", - "reward_shaping_conf = {\n", - " \"time_window\": 99,\n", - " \"fulfillment_factor\": 1.0,\n", - " \"shortage_factor\": 1.0,\n", - " \"time_decay\": 0.97\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Environment Sampler\n", - "\n", - "An environment sampler defines state, action and reward shaping logic so that policies can interact with the environment." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from maro.rl.rollout import AbsEnvSampler\n", - "from maro.simulator.scenarios.cim.common import Action, ActionType\n", - "\n", - "\n", - "class CIMEnvSampler(AbsEnvSampler):\n", - " def _get_global_and_agent_state(self, event, tick=None):\n", - " \"\"\"\n", - " The state vector includes shortage and remaining vessel space over the past k days (where k is the \"look_back\"\n", - " value in \"state_shaping_conf\" from the cell above), as well as all downstream port features.\n", - " \"\"\"\n", - " tick = self._env.tick\n", - " vessel_snapshots, port_snapshots = self._env.snapshot_list[\"vessels\"], self._env.snapshot_list[\"ports\"]\n", - " port_idx, vessel_idx = event.port_idx, event.vessel_idx\n", - " ticks = [max(0, tick - rt) for rt in range(state_shaping_conf[\"look_back\"] - 1)]\n", - " future_port_list = vessel_snapshots[tick: vessel_idx: 'future_stop_list'].astype('int')\n", - " state = np.concatenate([\n", - " port_snapshots[ticks: [port_idx] + list(future_port_list): port_attributes],\n", - " vessel_snapshots[tick: vessel_idx: vessel_attributes]\n", - " ])\n", - " return state, {port_idx: state}\n", - "\n", - " def _translate_to_env_action(self, action_dict, event):\n", - " \"\"\"\n", - " The policy output is an integer from [0, 20] which is to be interpreted as the index of \"action_space\" in\n", - " \"action_shaping_conf\" from the cell above. For example, action 5 corresponds to -0.5, which means loading\n", - " 50% of the containers available at the current port to the vessel, while action 18 corresponds to 0.8, which\n", - " means loading 80% of the containers on the vessel to the port. Note that action 10 corresponds 0.0, which\n", - " means doing nothing. \n", - " \"\"\"\n", - " action_space = action_shaping_conf[\"action_space\"]\n", - " finite_vsl_space = action_shaping_conf[\"finite_vessel_space\"]\n", - " has_early_discharge = action_shaping_conf[\"has_early_discharge\"]\n", - "\n", - " port_idx, model_action = list(action_dict.items()).pop()\n", - "\n", - " vsl_idx, action_scope = event.vessel_idx, event.action_scope\n", - " vsl_snapshots = self._env.snapshot_list[\"vessels\"]\n", - " vsl_space = vsl_snapshots[self._env.tick:vsl_idx:vessel_attributes][2] if finite_vsl_space else float(\"inf\")\n", - "\n", - " percent = abs(action_space[model_action[0]])\n", - " zero_action_idx = len(action_space) / 2 # index corresponding to value zero.\n", - " if model_action < zero_action_idx:\n", - " action_type = ActionType.LOAD\n", - " actual_action = min(round(percent * action_scope.load), vsl_space)\n", - " elif model_action > zero_action_idx:\n", - " action_type = ActionType.DISCHARGE\n", - " early_discharge = vsl_snapshots[self._env.tick:vsl_idx:\"early_discharge\"][0] if has_early_discharge else 0\n", - " plan_action = percent * (action_scope.discharge + early_discharge) - early_discharge\n", - " actual_action = round(plan_action) if plan_action > 0 else round(percent * action_scope.discharge)\n", - " else:\n", - " actual_action, action_type = 0, ActionType.LOAD\n", - "\n", - " return {port_idx: Action(vsl_idx, int(port_idx), actual_action, action_type)}\n", - "\n", - " def _get_reward(self, env_action_dict, event, tick):\n", - " \"\"\"\n", - " The reward is defined as a linear combination of fulfillment and shortage measures. The fulfillment and\n", - " shortage measure are the sums of fulfillment and shortage values over the next k days, respectively, each\n", - " adjusted with exponential decay factors (using the \"time_decay\" value in \"reward_shaping_conf\" from the\n", - " cell above) to put more emphasis on the near future. Here k is the \"time_window\" value in \"reward_shaping_conf\".\n", - " The linear combination coefficients are given by \"fulfillment_factor\" and \"shortage_factor\" in \"reward_shaping_conf\".\n", - " \"\"\"\n", - " start_tick = tick + 1\n", - " ticks = list(range(start_tick, start_tick + reward_shaping_conf[\"time_window\"]))\n", - "\n", - " # Get the ports that took actions at the given tick\n", - " ports = [int(port) for port in list(env_action_dict.keys())]\n", - " port_snapshots = self._env.snapshot_list[\"ports\"]\n", - " future_fulfillment = port_snapshots[ticks:ports:\"fulfillment\"].reshape(len(ticks), -1)\n", - " future_shortage = port_snapshots[ticks:ports:\"shortage\"].reshape(len(ticks), -1)\n", - "\n", - " decay_list = [reward_shaping_conf[\"time_decay\"] ** i for i in range(reward_shaping_conf[\"time_window\"])]\n", - " rewards = np.float32(\n", - " reward_shaping_conf[\"fulfillment_factor\"] * np.dot(future_fulfillment.T, decay_list)\n", - " - reward_shaping_conf[\"shortage_factor\"] * np.dot(future_shortage.T, decay_list)\n", - " )\n", - " return {agent_id: reward for agent_id, reward in zip(ports, rewards)}\n", - "\n", - " def get_env_metrics(self) -> None:\n", - " \"\"\"\n", - " The environment sampler contains a \"_info\" attribute inherited from the \"AbsEnvSampler\" base class, which can\n", - " be used to record any information one wishes to keep track of during a roll-out episode. Here we simply\n", - " record the latest env metric without keeping the history for logging purposes.\n", - " \"\"\"\n", - " return self._env.metrics\n", - "\n", - " def _post_step(self, cache_element, reward) -> None:\n", - " self._info[\"env_metric\"] = self._env.metrics" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## [Policies](https://maro.readthedocs.io/en/latest/key_components/rl_toolkit.html#policy)\n", - "\n", - "The out-of-the-box ActorCritic is used as our agent." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import torch\n", - "from torch.optim import Adam, RMSprop\n", - "\n", - "from maro.rl.model import DiscretePolicyNet, FullyConnected, VNet\n", - "from maro.rl.policy import DiscretePolicyGradient\n", - "from maro.rl.training.algorithms import DiscreteActorCriticTrainer, DiscreteActorCriticParams\n", - "\n", - "# We consider the port in question as well as two downstream ports.\n", - "# We consider the states of these ports over the past 7 days plus the current day, hence the factor 8.\n", - "# obtain state dimension from a temporary env_wrapper instance\n", - "state_dim = (\n", - " (state_shaping_conf[\"look_back\"] + 1) * (state_shaping_conf[\"max_ports_downstream\"] + 1) * len(port_attributes)\n", - " + len(vessel_attributes)\n", - ")\n", - "action_num = len(action_shaping_conf[\"action_space\"])\n", - "\n", - "# AC settings\n", - "actor_net_conf = {\n", - " \"input_dim\": state_dim,\n", - " \"hidden_dims\": [256, 128, 64],\n", - " \"output_dim\": len(action_shaping_conf[\"action_space\"]),\n", - " \"activation\": torch.nn.Tanh,\n", - " \"softmax\": True,\n", - " \"batch_norm\": False,\n", - " \"head\": True\n", - "}\n", - "critic_net_conf = {\n", - " \"input_dim\": state_dim,\n", - " \"hidden_dims\": [256, 128, 64],\n", - " \"output_dim\": 1,\n", - " \"activation\": torch.nn.LeakyReLU,\n", - " \"softmax\": False,\n", - " \"batch_norm\": True,\n", - " \"head\": True\n", - "}\n", - "\n", - "actor_optim_conf = (Adam, {\"lr\": 0.001})\n", - "critic_optim_conf = (RMSprop, {\"lr\": 0.001})\n", - "\n", - "ac_conf = DiscreteActorCriticParams(\n", - " get_v_critic_net_func=lambda: MyCriticNet(),\n", - " reward_discount=.0,\n", - " grad_iters=10,\n", - " critic_loss_cls=torch.nn.SmoothL1Loss,\n", - " min_logp=None,\n", - " lam=.0,\n", - " device=\"cpu\",\n", - ")\n", - "\n", - "\n", - "class MyActorNet(DiscretePolicyNet):\n", - " def __init__(self) -> None:\n", - " super(MyActorNet, self).__init__(state_dim=state_dim, action_num=action_num)\n", - " self._actor = FullyConnected(**actor_net_conf)\n", - " self._optim = actor_optim_conf[0](self._actor.parameters(), **actor_optim_conf[1])\n", - "\n", - " def _get_action_probs_impl(self, states: torch.Tensor) -> torch.Tensor:\n", - " return self._actor(states)\n", - "\n", - " def freeze(self) -> None:\n", - " self.freeze_all_parameters()\n", - "\n", - " def unfreeze(self) -> None:\n", - " self.unfreeze_all_parameters()\n", - "\n", - " def step(self, loss):\n", - " self._optim.zero_grad()\n", - " loss.backward()\n", - " self._optim.step()\n", - "\n", - " def get_gradients(self, loss: torch.Tensor):\n", - " self._optim.zero_grad()\n", - " loss.backward()\n", - " return {name: param.grad for name, param in self.named_parameters()}\n", - "\n", - " def apply_gradients(self, grad: dict) -> None:\n", - " for name, param in self.named_parameters():\n", - " param.grad = grad[name]\n", - " self._optim.step()\n", - "\n", - " def get_state(self) -> dict:\n", - " return {\n", - " \"network\": self.state_dict(),\n", - " \"optim\": self._optim.state_dict()\n", - " }\n", - "\n", - " def set_state(self, net_state: dict) -> None:\n", - " self.load_state_dict(net_state[\"network\"])\n", - " self._optim.load_state_dict(net_state[\"optim\"])\n", - "\n", - "\n", - "class MyCriticNet(VNet):\n", - " def __init__(self) -> None:\n", - " super(MyCriticNet, self).__init__(state_dim=state_dim)\n", - " self._critic = FullyConnected(**critic_net_conf)\n", - " self._optim = critic_optim_conf[0](self._critic.parameters(), **critic_optim_conf[1])\n", - "\n", - " def _get_v_values(self, states: torch.Tensor) -> torch.Tensor:\n", - " return self._critic(states).squeeze(-1)\n", - "\n", - " def step(self, loss: torch.Tensor):\n", - " self._optim.zero_grad()\n", - " loss.backward()\n", - " self._optim.step()\n", - "\n", - " def get_gradients(self, loss: torch.Tensor):\n", - " self._optim.zero_grad()\n", - " loss.backward()\n", - " return {name: param.grad for name, param in self.named_parameters()}\n", - "\n", - " def apply_gradients(self, grad: dict) -> None:\n", - " for name, param in self.named_parameters():\n", - " param.grad = grad[name]\n", - " self._optim.step()\n", - "\n", - " def get_state(self) -> dict:\n", - " return {\n", - " \"network\": self.state_dict(),\n", - " \"optim\": self._optim.state_dict()\n", - " }\n", - "\n", - " def set_state(self, net_state: dict) -> None:\n", - " self.load_state_dict(net_state[\"network\"])\n", - " self._optim.load_state_dict(net_state[\"optim\"])\n", - "\n", - " def freeze(self) -> None:\n", - " self.freeze_all_parameters()\n", - "\n", - " def unfreeze(self) -> None:\n", - " self.unfreeze_all_parameters()\n", - "\n", - "policy_dict = {f\"ac_{i}.policy\": DiscretePolicyGradient(f\"ac_{i}.policy\", policy_net=MyActorNet()) for i in range(4)}\n", - "trainer_creator = {f\"ac_{i}\": lambda name: DiscreteActorCriticTrainer(name, params=ac_conf) for i in range(4)}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Learning Loop\n", - "\n", - "This code cell demonstrates a typical single-threaded training workflow." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "env summary (episode 1, segment 1): {'order_requirements': 1120000, 'container_shortage': 755132, 'operation_number': 1955128}\n", - "env summary (episode 2, segment 1): {'order_requirements': 1120000, 'container_shortage': 541590, 'operation_number': 2147970}\n", - "env summary (episode 3, segment 1): {'order_requirements': 1120000, 'container_shortage': 430052, 'operation_number': 2279623}\n", - "env summary (episode 4, segment 1): {'order_requirements': 1120000, 'container_shortage': 282035, 'operation_number': 2063401}\n", - "env summary (episode 5, segment 1): {'order_requirements': 1120000, 'container_shortage': 142609, 'operation_number': 2208815}\n", - "env summary (episode 6, segment 1): {'order_requirements': 1120000, 'container_shortage': 102221, 'operation_number': 2217282}\n", - "env summary (episode 7, segment 1): {'order_requirements': 1120000, 'container_shortage': 156158, 'operation_number': 2138796}\n", - "env summary (episode 8, segment 1): {'order_requirements': 1120000, 'container_shortage': 149739, 'operation_number': 2174584}\n", - "env summary (episode 9, segment 1): {'order_requirements': 1120000, 'container_shortage': 174441, 'operation_number': 2053284}\n", - "env summary (episode 10, segment 1): {'order_requirements': 1120000, 'container_shortage': 126566, 'operation_number': 2158450}\n", - "env summary (episode 11, segment 1): {'order_requirements': 1120000, 'container_shortage': 179030, 'operation_number': 2034439}\n", - "env summary (episode 12, segment 1): {'order_requirements': 1120000, 'container_shortage': 133275, 'operation_number': 2235375}\n", - "env summary (episode 13, segment 1): {'order_requirements': 1120000, 'container_shortage': 196606, 'operation_number': 2067036}\n", - "env summary (episode 14, segment 1): {'order_requirements': 1120000, 'container_shortage': 190649, 'operation_number': 2174073}\n", - "env summary (episode 15, segment 1): {'order_requirements': 1120000, 'container_shortage': 202218, 'operation_number': 2009027}\n", - "env summary (episode 16, segment 1): {'order_requirements': 1120000, 'container_shortage': 244883, 'operation_number': 1986210}\n", - "env summary (episode 17, segment 1): {'order_requirements': 1120000, 'container_shortage': 359891, 'operation_number': 1658629}\n", - "env summary (episode 18, segment 1): {'order_requirements': 1120000, 'container_shortage': 356337, 'operation_number': 1769432}\n", - "env summary (episode 19, segment 1): {'order_requirements': 1120000, 'container_shortage': 289850, 'operation_number': 1742258}\n", - "env summary (episode 20, segment 1): {'order_requirements': 1120000, 'container_shortage': 327072, 'operation_number': 1677051}\n" - ] - } - ], - "source": [ - "from maro.rl.rollout import SimpleAgentWrapper\n", - "from maro.rl.training import TrainingManager \n", - "from maro.simulator import Env\n", - "from maro.utils import set_seeds\n", - "\n", - "set_seeds(1024) # for reproducibility\n", - "\n", - "agent2policy = {agent: f\"ac_{agent}.policy\" for agent in Env(**env_conf).agent_idx_list}\n", - "\n", - "# The env sampler and trainer manager both take ``policy_creator`` as a parameter. The policy creator is\n", - "# a function that takes a name and returns a policy instance. This design is convenient in distributed\n", - "# settings where policies need to be created on both the training side and the roll-out (inference) side\n", - "# and policy states need to be transferred from the former to the latter at the start of each roll-out\n", - "# episode. Here we are demonstrating a single-threaded workflow where there is only one instance of each\n", - "# policy, so we use a little trick here to ensure that the policies created inside the env sampler and the\n", - "# training manager point the the same instances. \n", - "policy_creator = {name: lambda name: policy_dict[name] for name in policy_dict}\n", - "\n", - "env_sampler = CIMEnvSampler(\n", - " get_env=lambda: Env(**env_conf),\n", - " policy_creator=policy_creator,\n", - " agent2policy=agent2policy,\n", - " agent_wrapper_cls=SimpleAgentWrapper,\n", - " device=\"cpu\"\n", - ")\n", - "\n", - "training_manager = TrainingManager(policy_creator, trainer_creator, agent2policy)\n", - "\n", - "# main loop with 50 episodes\n", - "for ep in range(1, 21):\n", - " collect_time = training_time = 0\n", - " segment, end_of_episode = 1, False\n", - " while not end_of_episode:\n", - " # experience collection\n", - " result = env_sampler.sample()\n", - " experiences = result[\"experiences\"]\n", - " end_of_episode: bool = result[\"end_of_episode\"]\n", - " print(f\"env summary (episode {ep}, segment {segment}): {env_sampler.get_env_metrics()}\")\n", - " training_manager.record_experiences(experiences)\n", - " training_manager.train_step()\n", - " segment += 1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "interpreter": { - "hash": "8f57a09d39b50edfb56e79199ef40583334d721b06ead0e38a39e7e79092073c" - }, - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.11" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From a3dade7460ed955e8a9de091a5d370135f8da708 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Tue, 31 May 2022 15:43:23 +0800 Subject: [PATCH 476/482] Merge master into v0.3 (#536) * update docker hub init (#367) * update docker hub init * replace personal account with maro-team * update hello files for CIM * update docker repository name * update docker file name * fix bugs in notebook, rectify docs * fix doc build issue * remove docs from playground; fix citibike lp example Event issue * update the exampel for vector env * update vector env example * update README due to PR comments * add link to playground above MARO installation in README * fix some typos Co-authored-by: Jinyu Wang * update package version * update README for package description * update image links for pypi package description * update image links for pypi package description * change the input topology schema for CIM real data mode (#372) * change the input topology schema for CIM real data mode * remove unused importing * update test config file correspondingly * add Exception for env test * add cost factors to cim data dump * update CimDataCollection field name * update field name of data collection related code * update package version * adjust interface to reflect actual signature (#374) Co-authored-by: Jeremy Reynolds * update dataclasses requirement to setup * fix: fixing spelling grammarr * fix: fix typo spelling code commented and data_model.rst * Fix Geo vis IP address & SQL logic bugs. (#383) Fix Geo vis IP address & SQL logic bugs (issue [352](https://github.com/microsoft/maro/issues/352) and [314](https://github.com/microsoft/maro/issues/314)). * Fix the "Wrong future stop tick predictions" bug (#386) * Propose my new solution Refine to the pre-process version . * Optimize import * Fix reset random seed bug (#387) * update the reset interface of Env and BE * Try to fix reset routes generation seed issue * Refine random related logics. * Minor refinement * Test check * Minor * Remove unused functions so far * Minor Co-authored-by: Jinyu Wang * update package version * Add _init_vessel_plans in business_engine.reset (#388) * update package version * change the default solver used in Citibike OnlineLP example, from GLPK to CBC (#391) Co-authored-by: Jinyu Wang * Refine `event_buffer/` module (#389) * Core & Business Engine code refinement (#392) * First version * Optimize imports * Add typehint * Lint check * Lint check * add higher python version (#398) * add higher python version * update pytorch version * update torchvision version Co-authored-by: Jinyu Wang * CIM scenario refinement (#400) * Cim scenario refinement (#394) * CIM refinement * Fix lint error * Fix lint error * Cim test coverage (#395) * Enrich tests * Refactor CimDataGenerator * Refactor CIM parsers * Minor refinement * Fix lint error * Fix lint error * Fix lint error * Minor refactor * Type * Add two test file folders. Make a slight change to CIM BE. * Lint error * Lint error * Remove unnecessary public interfaces of CIM BE * Cim disable auto action type detection (#399) * Haven't been tested * Modify document * Add ActionType checking * Minor * Lint error * Action quantity should be a position number * Modify related docs & notebooks * Minor * Change test file name. Prepare to merge into master. * . * Minor test patch * Add `clear()` function to class `SimRandom` (#401) * Add SimRandom.clear() * Minor * Remove commented codes * Lint error * update package version * add branch v0.3 to github workflow * update github test workflow * Update requirements.dev.txt (#444) Added the versions of dependencies and resolve some conflicts occurs when installing. By adding these version number it will tell you the exact. * Bump ipython from 7.10.1 to 7.16.3 in /notebooks (#460) Bumps [ipython](https://github.com/ipython/ipython) from 7.10.1 to 7.16.3. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/7.10.1...7.16.3) --- updated-dependencies: - dependency-name: ipython dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add & sort requirements.dev.txt Co-authored-by: Jinyu-W <53509467+Jinyu-W@users.noreply.github.com> Co-authored-by: Jinyu Wang Co-authored-by: Jinyu Wang Co-authored-by: Jeremy Reynolds Co-authored-by: Jeremy Reynolds Co-authored-by: slowy07 Co-authored-by: solosilence Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 6 +- .github/workflows/vulnerability_scan.yml | 2 +- maro/README.rst | 199 ++++++++++++------ .../scenarios/abs_business_engine.py | 7 +- notebooks/requirements.nb.txt | 2 +- requirements.dev.txt | 72 ++++--- 6 files changed, 183 insertions(+), 105 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d90771db0..302ae9279 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,9 +3,9 @@ name: test on: push: - branches: [ master, v0.1, v0.2 ] + branches: [ master, v0.1, v0.2, v0.3 ] pull_request: - branches: [ master, v0.1, v0.2 ] + branches: [ master, v0.1, v0.2, v0.3 ] workflow_dispatch: jobs: @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-16.04, ubuntu-18.04, windows-latest, macos-latest] + os: [ubuntu-18.04, windows-latest, macos-latest] python-version: [3.6, 3.7, 3.8, 3.9] steps: diff --git a/.github/workflows/vulnerability_scan.yml b/.github/workflows/vulnerability_scan.yml index bf16ea167..92dbb739e 100644 --- a/.github/workflows/vulnerability_scan.yml +++ b/.github/workflows/vulnerability_scan.yml @@ -7,7 +7,7 @@ name: "vulnerability scan" on: push: - branches: [master, v0.1, v0.2] + branches: [master, v0.1, v0.2, v0.3] pull_request: # The branches below must be a subset of the branches above branches: [master] diff --git a/maro/README.rst b/maro/README.rst index dfbe33c30..a7cdee728 100644 --- a/maro/README.rst +++ b/maro/README.rst @@ -1,43 +1,43 @@ .. image:: https://img.shields.io/pypi/l/pymaro - :target: https://github.com/microsoft/maro/blob/master/LICENSE - :alt: License + :target: https://github.com/microsoft/maro/blob/master/LICENSE + :alt: License .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/platform.svg - :target: https://pypi.org/project/pymaro/ - :alt: Platform + :target: https://pypi.org/project/pymaro/ + :alt: Platform .. image:: https://img.shields.io/pypi/pyversions/pymaro.svg?logo=python&logoColor=white - :target: https://pypi.org/project/pymaro/#files - :alt: Python Versions + :target: https://pypi.org/project/pymaro/#files + :alt: Python Versions .. image:: https://img.shields.io/github/languages/code-size/microsoft/maro - :target: https://github.com/microsoft/maro - :alt: Code Size + :target: https://github.com/microsoft/maro + :alt: Code Size .. image:: https://img.shields.io/docker/image-size/maro2020/maro - :target: https://hub.docker.com/repository/docker/maro2020/maro/tags?page=1 - :alt: Docker Size + :target: https://hub.docker.com/repository/docker/maro2020/maro/tags?page=1 + :alt: Docker Size .. image:: https://img.shields.io/github/issues/microsoft/maro - :target: https://github.com/microsoft/maro/issues - :alt: Issues + :target: https://github.com/microsoft/maro/issues + :alt: Issues .. image:: https://img.shields.io/github/issues-pr/microsoft/maro - :target: https://github.com/microsoft/maro/pulls - :alt: Pull Requests + :target: https://github.com/microsoft/maro/pulls + :alt: Pull Requests .. image:: https://img.shields.io/librariesio/github/microsoft/maro - :target: https://libraries.io/pypi/pymaro - :alt: Dependencies + :target: https://libraries.io/pypi/pymaro + :alt: Dependencies .. image:: https://github.com/microsoft/maro/workflows/test/badge.svg @@ -51,8 +51,8 @@ .. image:: https://github.com/microsoft/maro/workflows/docker/badge.svg - :target: https://hub.docker.com/repository/docker/maro2020/maro - :alt: docker + :target: https://hub.docker.com/repository/docker/maro2020/maro + :alt: docker .. image:: https://readthedocs.org/projects/maro/badge/?version=latest @@ -61,23 +61,86 @@ .. image:: https://img.shields.io/pypi/v/pymaro - :target: https://pypi.org/project/pymaro/#files - :alt: PypI Versions + :target: https://pypi.org/project/pymaro/#files + :alt: PypI Versions .. image:: https://img.shields.io/pypi/wheel/pymaro - :target: https://pypi.org/project/pymaro/#files - :alt: Wheel + :target: https://pypi.org/project/pymaro/#files + :alt: Wheel .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/citi_bike.svg - :target: https://maro.readthedocs.io/en/latest/scenarios/citi_bike.html - :alt: Citi Bike + :target: https://maro.readthedocs.io/en/latest/scenarios/citi_bike.html + :alt: Citi Bike .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/cim.svg - :target: https://maro.readthedocs.io/en/latest/scenarios/container_inventory_management.html - :alt: CIM + :target: https://maro.readthedocs.io/en/latest/scenarios/container_inventory_management.html + :alt: CIM + + +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/vm_scheduling.svg + :target: https://maro.readthedocs.io/en/latest/scenarios/vm_scheduling.html + :alt: VM Scheduling + + +.. image:: https://img.shields.io/gitter/room/microsoft/maro + :target: https://gitter.im/Microsoft/MARO# + :alt: Gitter + + +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/stack_overflow.svg + :target: https://stackoverflow.com/questions/ask?tags=maro + :alt: Stack Overflow + + +.. image:: https://img.shields.io/github/release-date-pre/microsoft/maro + :target: https://github.com/microsoft/maro/releases + :alt: Releases + + +.. image:: https://img.shields.io/github/commits-since/microsoft/maro/latest/master + :target: https://github.com/microsoft/maro/commits/master + :alt: Commits + + +.. image:: https://github.com/microsoft/maro/workflows/vulnerability%20scan/badge.svg + :target: https://github.com/microsoft/maro/actions?query=workflow%3A%22vulnerability+scan%22 + :alt: Vulnerability Scan + + +.. image:: https://github.com/microsoft/maro/workflows/lint/badge.svg + :target: https://github.com/microsoft/maro/actions?query=workflow%3Alint + :alt: Lint + + +.. image:: https://img.shields.io/codecov/c/github/microsoft/maro + :target: https://codecov.io/gh/microsoft/maro + :alt: Coverage + + +.. image:: https://img.shields.io/pypi/dm/pymaro + :target: https://pypi.org/project/pymaro/#files + :alt: Downloads + + +.. image:: https://img.shields.io/docker/pulls/maro2020/maro + :target: https://hub.docker.com/repository/docker/maro2020/maro + :alt: Docker Pulls + + +.. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/play_with_maro.svg + :target: https://hub.docker.com/r/maro2020/maro + :alt: Play with MARO + + + +.. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/logo.svg + :target: https://maro.readthedocs.io/en/latest/ + :alt: MARO LOGO + +================================================================================================================ .. image:: https://raw.githubusercontent.com/microsoft/maro/master/docs/source/images/badges/vm_scheduling.svg @@ -164,8 +227,8 @@ Key Components of MARO: .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/maro_overview.svg - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/maro_overview.svg - :alt: MARO Key Components + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/maro_overview.svg + :alt: MARO Key Components Contents @@ -244,22 +307,22 @@ Install MARO from Source .. code-block:: powershell - # If your environment is not clean, create a virtual environment firstly. - python -m venv maro_venv + # If your environment is not clean, create a virtual environment firstly. + python -m venv maro_venv # You may need this for SecurityError in PowerShell. Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted - # Activate the virtual environment. - .\maro_venv\Scripts\activate + # Activate the virtual environment. + .\maro_venv\Scripts\activate * Install MARO .. code-block:: sh - # Git Clone the whole source code. - git clone https://github.com/microsoft/maro.git + # Git Clone the whole source code. + git clone https://github.com/microsoft/maro.git * @@ -286,14 +349,14 @@ Install MARO from Source .. code-block:: sh - export PYTHONPATH=PATH-TO-MARO + export PYTHONPATH=PATH-TO-MARO * Windows .. code-block:: powershell - $Env:PYTHONPATH=PATH-TO-MARO + $Env:PYTHONPATH=PATH-TO-MARO Quick Example ------------- @@ -316,15 +379,15 @@ Quick Example .. code-block:: sh - # Enable environment dump feature, when initializing the environment instance - env = Env(scenario="cim", - topology="toy.5p_ssddd_l0.0", - start_tick=0, - durations=100, - options={"enable-dump-snapshot": "./dump_data"}) + # Enable environment dump feature, when initializing the environment instance + env = Env(scenario="cim", + topology="toy.5p_ssddd_l0.0", + start_tick=0, + durations=100, + options={"enable-dump-snapshot": "./dump_data"}) - # Inspect environment with the dump data - maro inspector dashboard --source_path ./dump_data/YOUR_SNAPSHOT_DUMP_FOLDER + # Inspect environment with the dump data + maro inspector dashboard --source_path ./dump_data/YOUR_SNAPSHOT_DUMP_FOLDER Show Cases ^^^^^^^^^^ @@ -334,26 +397,26 @@ Show Cases Case I - Container Inventory Management .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_inter_epoch.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_inter_epoch.gif - :alt: CIM Inter Epoch + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_inter_epoch.gif + :alt: CIM Inter Epoch .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_intra_epoch_by_ports.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_intra_epoch_by_ports.gif - :alt: CIM Intra Epoch + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/cim_intra_epoch_by_ports.gif + :alt: CIM Intra Epoch * Case II - Citi Bike .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_inter_epoch.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_inter_epoch.gif - :alt: Citi Bike Inter Epoch + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_inter_epoch.gif + :alt: Citi Bike Inter Epoch .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_intra_epoch_by_station.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_intra_epoch_by_station.gif - :alt: Citi Bike Intra Epoch + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/visualization/dashboard/citi_bike_intra_epoch_by_station.gif + :alt: Citi Bike Intra Epoch Run Playground @@ -365,13 +428,13 @@ Run Playground .. code-block:: sh - # Pull the docker image from docker hub - docker pull maro2020/playground + # Pull the docker image from docker hub + docker pull maro2020/playground - # Run playground container. - # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Jupyter lab with maro -> http://127.0.0.1:40010 - docker run -p 40009:40009 -p 40010:40010 maro2020/playground + # Run playground container. + # Redis commander (GUI for redis) -> http://127.0.0.1:40009 + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground * Build from source @@ -385,10 +448,10 @@ Run Playground # Build playground image. bash ./scripts/build_playground.sh - # Run playground container. - # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Jupyter lab with maro -> http://127.0.0.1:40010 - docker run -p 40009:40009 -p 40010:40010 maro2020/playground + # Run playground container. + # Redis commander (GUI for redis) -> http://127.0.0.1:40009 + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground * Windows @@ -398,10 +461,10 @@ Run Playground # Build playground image. .\scripts\build_playground.bat - # Run playground container. - # Redis commander (GUI for redis) -> http://127.0.0.1:40009 - # Jupyter lab with maro -> http://127.0.0.1:40010 - docker run -p 40009:40009 -p 40010:40010 maro2020/playground + # Run playground container. + # Redis commander (GUI for redis) -> http://127.0.0.1:40009 + # Jupyter lab with maro -> http://127.0.0.1:40010 + docker run -p 40009:40009 -p 40010:40010 maro2020/playground Contributing ------------ @@ -431,8 +494,8 @@ Related Papers .. image:: https://github.com/microsoft/maro/blob/master/docs/source/images/scenario/cim_vis.gif - :target: https://github.com/microsoft/maro/blob/master/docs/source/images/scenario/cim_vis.gif - :alt: CIM Vis + :target: https://github.com/microsoft/maro/blob/master/docs/source/images/scenario/cim_vis.gif + :alt: CIM Vis Wenlei Shi, Xinran Wei, Jia Zhang, Xiaoyuan Ni, Arthur Jiang, Jiang Bian, Tie-Yan Liu. "\ `Cooperative Policy Learning with Pre-trained Heterogeneous Observation Representations `_\ ". AAMAS 2021 diff --git a/maro/simulator/scenarios/abs_business_engine.py b/maro/simulator/scenarios/abs_business_engine.py index 44a6da8f4..daf2f480b 100644 --- a/maro/simulator/scenarios/abs_business_engine.py +++ b/maro/simulator/scenarios/abs_business_engine.py @@ -70,6 +70,7 @@ def snapshots(self) -> SnapshotList: def scenario_name(self) -> str: return self._scenario_name + @abstractmethod def get_agent_idx_list(self) -> List[int]: """Get a list of agent index.""" pass @@ -173,9 +174,9 @@ def reset(self, keep_seed: bool = False) -> None: """Reset states business engine.""" pass - # @abstractmethod - # def set_seed(self, seed: int) -> None: - # raise NotImplementedError + @abstractmethod + def set_seed(self, seed: int) -> None: + raise NotImplementedError def post_step(self, tick: int) -> bool: """This method will be called at the end of each tick, used to post-process for each tick, diff --git a/notebooks/requirements.nb.txt b/notebooks/requirements.nb.txt index a2ead9be3..518b1a5de 100644 --- a/notebooks/requirements.nb.txt +++ b/notebooks/requirements.nb.txt @@ -16,7 +16,7 @@ isort==4.3.21 pandas==0.25.3 matplotlib==3.1.2 seaborn==0.9.0 -ipython==7.10.1 +ipython==7.16.3 ipython-genutils==0.2.0 shap==0.32.1 seaborn==0.9.0 diff --git a/requirements.dev.txt b/requirements.dev.txt index 23279bd5b..88b71e8ad 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,54 +1,68 @@ -Cython==0.29.14 +add-trailing-comma +altair==4.1.0 +aria2p==0.9.1 astroid==2.3.3 +azure-identity azure-mgmt-authorization azure-mgmt-containerservice azure-mgmt-resource azure-mgmt-storage azure-storage-file-share -azure-identity +black==22.3.0 certifi==2019.9.11 +cryptography==36.0.1 cycler==0.10.0 +Cython==0.29.14 +deepdiff==5.7.0 docker -flask==1.1.2 +editorconfig-checker==2.4.0 +flake8==4.0.1 flask-cors==3.0.10 +flask==1.1.2 +flask_cors==3.0.10 +flask_socketio==5.2.0 +flloat==0.3.0 +geopy==2.0.0 guppy3==3.0.9 +holidays==0.10.3 isort==4.3.21 +jinja2==2.11.3 kiwisolver==1.1.0 +kubernetes==21.7.0 lazy-object-proxy==1.4.3 +markupsafe==2.0.1 +matplotlib==3.5.2 mccabe==0.6.1 +networkx==2.4 +networkx==2.4 +numpy<1.20.0 +palettable==3.3.0 +pandas==0.25.3 +prompt_toolkit==2.0.10 +psutil==5.8.0 +ptvsd==4.3.2 +pulp==2.6.0 pyaml==20.4.0 +PyJWT==2.4.0 pyparsing==2.4.5 python-dateutil==2.8.1 PyYAML==5.4.1 pyzmq==19.0.2 +recommonmark~=0.6.0 +redis==3.5.3 +requests==2.25.1 +scipy==1.7.0 +setuptools==58.0.4 six==1.13.0 +sphinx==1.8.6 +sphinx_rtd_theme==1.0.0 +streamlit==0.69.1 +stringcase==1.2.0 +tabulate==0.8.5 +termgraph==0.5.3 torch==1.6.0 torchsummary==1.5.1 +tqdm==4.51.0 +urllib3==1.26.5 wrapt==1.11.2 zmq==0.0.0 -numpy<1.20.0 -scipy==1.7.0 -tabulate==0.8.5 -markupsafe==2.0.1 -networkx==2.4 -palettable==3.3.0 -urllib3==1.26.5 -geopy==2.0.0 -pandas==0.25.3 -redis==3.5.3 -requests==2.25.1 -holidays==0.10.3 -sphinx -recommonmark~=0.6.0 -sphinx_rtd_theme -jinja2 -flake8 -PuLP==2.1 -streamlit==0.69.1 -altair==4.1.0 -tqdm==4.51.0 -editorconfig-checker -aria2p==0.9.1 -prompt_toolkit==2.0.10 -stringcase==1.2.0 -networkx==2.4 From e0209d63242ef6e3912cbcdfa6a1f5cecdbbffdb Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 1 Jun 2022 09:42:13 +0800 Subject: [PATCH 477/482] Remove random_config.py --- scripts/random_config.py | 400 --------------------------------------- 1 file changed, 400 deletions(-) delete mode 100644 scripts/random_config.py diff --git a/scripts/random_config.py b/scripts/random_config.py deleted file mode 100644 index 1bcce4e52..000000000 --- a/scripts/random_config.py +++ /dev/null @@ -1,400 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -""" -A simple script that used to generate random configurations. -""" - -import argparse -import os -import random -from typing import Optional - -import numpy as np -from flloat.parser.ltlf import LTLfParser -from yaml import safe_load, safe_dump - -# Definition of warehouse. -warehouse_def = """ -class: "WarehouseFacility" -children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 4 - consumer: - class: "ConsumerUnit" -config: - agent_type: 1 -""" - -# Definition of supplier. -supplier_def = """ -class: "SupplierFacility" -children: - storage: - class: "StorageUnit" - distribution: - class: "DistributionUnit" - products: - class: "ProductUnit" - is_template: true - config: - agent_type: 3 - consumer: - class: "ConsumerUnit" - manufacture: - class: "ManufactureUnit" -config: - agent_type: 0 -""" - -# Definition of retailer. -retailer_def = """ -class: "RetailerFacility" -children: - storage: - class: "StorageUnit" - products: - class: "StoreProductUnit" - is_template: true - config: - agent_type: 5 - consumer: - class: "ConsumerUnit" - seller: - class: "SellerUnit" - config: - sale_hist_len: 4 -config: - agent_type: 2 -""" - -# Template to generate a supplier facility. -# Properties to change: -# . name -# . skus -# . vehicles -# . config (optional) -supplier_template = """ -name: "Supplier_001" -definition_ref: "SupplierFacility" -skus: {} -children: - storage: - config: - capacity: 10000 - unit_storage_cost: 1 - distribution: - children: - vehicles: [] - config: - unit_price: 1 -config: {} -""" - -# Template to generate warehouse facility. -# Property to change: -# . name -# . skus -# . vehicles -# . config (optional) -warehouse_template = """ -name: "Warehouse_001" -definition_ref: "WarehouseFacility" -skus: {} -children: - storage: - config: - capacity: 10000 - unit_storage_cost: 1 - distribution: - children: - vehicles: [] - config: - unit_price: 1 -config: {} -""" - -# Template to generate retailer. -# Property to change: -# . name -# . skus -# . config (optional) -retailer_template = """ -name: "Retailer_001" -definition_ref: "RetailerFacility" -skus: {} -children: - storage: - config: - capacity: 10000 - unit_storage_cost: 1 -config: {} -""" - - -def generate_config(sku_num: int, supplier_num: int, warehouse_num: int, retailer_num: int, grid_width: int, - grid_height: int, output_path: Optional[str] = None): - constraints = ['G(stock_constraint)', - 'G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint)))', - 'G(low_profit -> low_stock_constraint)'] - - # constraints = ['G(is_replenish_constraint -> ((X!is_replenish_constraint)&(XX!is_replenish_constraint)))'] - - def construct_formula(constraint): - parser = LTLfParser() - formula = parser(constraint) - return formula - - constraint_formulas = {constraint: construct_formula(constraint) for constraint in constraints} - constraint_automata = {constraint: constraint_formulas[constraint].to_automaton().determinize() for constraint in - constraints} - - max_constraint_states = int(np.max([len(a.states) for a in constraint_automata.values()])) - - # Base configuration of vehicle used in all facility. - vehicle_conf = { - "class": "VehicleUnit", - "config": { - "patient": 100, - "unit_transport_cost": 1 - } - } - - # Save the vehicle definition in the config, so later distribution will reference to it. - config = { - "normal_vehicle": vehicle_conf, - "facility_definitions": {}, - "settings": { - "global_reward_weight_producer": 0.50, - "global_reward_weight_consumer": 0.50, - "downsampling_rate": 1, - "episod_duration": 21, - "initial_balance": 100000, - "consumption_hist_len": 4, - "sale_hist_len": 4, - "pending_order_len": 4, - "constraint_state_hist_len": max_constraint_states, - "total_echelons": 3, - "replenishment_discount": 0.9, - "reward_normalization": 1e7, - "constraint_violate_reward": -1e6, - "gamma": 0.99, - "tail_timesteps": 7, - "heading_timesteps": 7, - } - } - - # Add the facility definitions. - config["facility_definitions"]["SupplierFacility"] = safe_load(supplier_def) - config["facility_definitions"]["WarehouseFacility"] = safe_load(warehouse_def) - config["facility_definitions"]["RetailerFacility"] = safe_load(retailer_def) - - # Generate settings first. - world_conf = {} - - sku_names = [f'SKU{i}' for i in range(sku_num)] - - sku_list = [] - for sku_index, sku_name in enumerate(sku_names): - sku_list.append({ - "id": sku_index, - "name": sku_name - }) - - # Add the sku list to the world configuration. - world_conf["skus"] = sku_list - - # Generate sku information. - sku_cost = {f'SKU{i}': random.randint(10, 500) for i in range(sku_num)} - sku_product_cost = {f'SKU{i}': int(sku_cost[f'SKU{i}'] * 0.9) for i in range(sku_num)} - sku_price = {f'SKU{i}': int(sku_cost[f'SKU{i}'] * (1 + random.randint(10, 100) / 100)) for i in range(sku_num)} - sku_gamma = {f'SKU{i}': random.randint(5, 100) for i in range(sku_num)} - total_gamma = sum(list(sku_gamma.values())) - sku_vlt = {f'SKU{i}': random.randint(1, 3) for i in range(sku_num)} - - # Generate suppliers. - supplier_facilities = [] - - for i in range(supplier_num): - facility = safe_load(supplier_template) - - facility["name"] = f"SUPPLIER{i}" - facility["children"]["storage"]["config"]["capacity"] = total_gamma * 100 - - for _ in range(10 * sku_num): - # this will save as a reference in the final yaml file - facility["children"]["distribution"]["children"]["vehicles"].append(vehicle_conf) - - # Facility config. - facility["config"] = {} - facility["config"]["order_cost"] = 200 - facility["config"]["delay_order_penalty"] = 1000 - - # Sku list of this facility. - sku_list = {} - - for j in range(sku_num): - sku_name = f"SKU{j}" - sku_list[sku_name] = { - "price": sku_cost[sku_name], - "cost": sku_product_cost[sku_name], - "service_level": .95, - "vlt": 3, - "init_stock": int(sku_gamma[sku_name] * 50), - # Why this configuration, as manufacture is controlled by action? - "production_rate": int(sku_gamma[sku_name] * 50), - # For this script, all sku is a production that produced by suppliers, no bom. - "type": "production", - "product_unit_cost": 1, - } - - facility["skus"] = sku_list - - supplier_facilities.append(facility) - - # Warehouses. - warehouse_list = [] - for i in range(warehouse_num): - facility = safe_load(warehouse_template) - - facility["name"] = f"WAREHOUSE{i}" - facility["children"]["storage"]["config"]["capacity"] = total_gamma * 100 - - for _ in range(10 * sku_num): - facility["children"]["distribution"]["children"]["vehicles"].append(vehicle_conf) - - facility["config"] = {} - facility["config"]["order_cost"] = 500 - facility["config"]["delay_order_penalty"] = 1000 - - sku_list = {} - - for j in range(sku_num): - sku_name = f"SKU{j}" - sku_list[sku_name] = { - "price": sku_cost[sku_name], - "cost": sku_cost[sku_name], - "vlt": sku_vlt[sku_name], - "init_stock": int(sku_gamma[sku_name] * 20), - "service_level": .96 - } - - facility["skus"] = sku_list - - warehouse_list.append(facility) - - sku_constraints = {} - for i in range(sku_num): - if random.random() <= 0.5: - continue - sku_constraints[f"SKU{i}"] = constraints[random.randint(0, len(constraints) - 1)] - - # Retailers. - retailer_list = [] - for i in range(retailer_num): - facility = safe_load(retailer_template) - - facility["name"] = f"STORE{i}" - facility["children"]["storage"]["config"]["capacity"] = total_gamma * 20 - - facility["config"] = {} - facility["config"]["order_cost"] = 500 - - sku_list = {} - - for j in range(sku_num): - sku_name = f"SKU{j}" - sku_list[sku_name] = { - "price": sku_price[sku_name], - "service_level": 0.95, - "cost": sku_cost[sku_name], - "init_stock": sku_gamma[sku_name] * (sku_vlt[sku_name] + random.randint(1, 5)), - "sale_gamma": sku_gamma[sku_name], - 'max_stock': 1000, - "constraint": sku_constraints.get(sku_name, None) - } - - facility["skus"] = sku_list - - retailer_list.append(facility) - - world_conf["facilities"] = supplier_facilities + warehouse_list + retailer_list - - # According to original code, the upstream relationship is like following: - # supplier <- warehouse <- retailer - # as current configuration supplier and warehouse contain all the sku, so we can just random pick. - world_conf["topology"] = {} - - # Random pick upstreams for retailers from warehouses. - for store in retailer_list: - store_upstream = {} - - for i in range(sku_num): - sku_name = f"SKU{i}" - store_upstream[sku_name] = [warehouse_list[random.randint(0, warehouse_num - 1)]["name"], ] - - world_conf["topology"][store["name"]] = store_upstream - - # Random pick upstreams for warehouses from suppliers. - for warehouse in warehouse_list: - warehouse_upstream = {} - - for i in range(sku_num): - sku_name = f"SKU{i}" - warehouse_upstream[sku_name] = [supplier_facilities[random.randint(0, supplier_num) - 1]["name"], ] - - world_conf["topology"][warehouse["name"]] = warehouse_upstream - - # Grid settings. - world_conf["grid"] = {} - world_conf["grid"]["size"] = [grid_width, grid_height] - - # Random pick location. - available_cells = [(x, y) for x in range(grid_width) for y in range(grid_height)] - - world_conf["grid"]["facilities"] = {} - for facility in world_conf["facilities"]: - cell = random.randint(0, len(available_cells) - 1) - - world_conf["grid"]["facilities"][facility["name"]] = available_cells[cell] - - del available_cells[cell] - - config["world"] = world_conf - - if output_path is None: - output_path = "." - - with open(os.path.join(output_path, "config.yml"), "wt+") as fp: - safe_dump(config, fp) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - - parser.add_argument("--sku_num", type=int, default=random.randint(4, 5)) - parser.add_argument("--supplier_num", type=int, default=1) - parser.add_argument("--warehouse_num", type=int, default=1) - parser.add_argument("--retailer_num", type=int, default=1) - parser.add_argument("--grid_width", type=int, default=20) - parser.add_argument("--grid_height", type=int, default=20) - parser.add_argument("--output_path", type=str, default=".") - - arg = parser.parse_args() - - generate_config( - arg.sku_num, - arg.supplier_num, - arg.warehouse_num, - arg.retailer_num, - arg.grid_width, - arg.grid_height, - arg.output_path - ) From c135e730802d3138024daa7051e5bf6e7e02ff47 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 1 Jun 2022 09:47:01 +0800 Subject: [PATCH 478/482] Remove test_trajectory_utils.py --- tests/test_trajectory_utils.py | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 tests/test_trajectory_utils.py diff --git a/tests/test_trajectory_utils.py b/tests/test_trajectory_utils.py deleted file mode 100644 index df05d8032..000000000 --- a/tests/test_trajectory_utils.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import unittest - -import numpy as np - -from maro.rl.utils.trajectory_utils import get_k_step_returns, get_lambda_returns - - -class TestTrajectoryUtils(unittest.TestCase): - def setUp(self) -> None: - self.rewards = np.asarray([3, 2, 4, 1, 5]) - self.values = np.asarray([4, 7, 1, 3, 6]) - self.lam = 0.6 - self.discount = 0.8 - self.k = 4 - - def test_k_step_return(self): - returns = get_k_step_returns(self.rewards, self.values, self.discount, k=self.k) - expected = np.asarray([10.1296, 8.912, 8.64, 5.8, 6.0]) - np.testing.assert_allclose(returns, expected, rtol=1e-4) - - def test_lambda_return(self): - returns = get_lambda_returns(self.rewards, self.values, self.discount, self.lam, k=self.k) - expected = np.asarray([8.1378176, 6.03712, 7.744, 5.8, 6.0]) - np.testing.assert_allclose(returns, expected, rtol=1e-4) - - -if __name__ == "__main__": - unittest.main() From fb7754560491d4a93e3ae1082b7a7208228ce144 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 1 Jun 2022 10:08:29 +0800 Subject: [PATCH 479/482] Pass tests --- maro/cli/k8s/utils/params.py | 10 ++++++++++ tests/dummy/dummy_business_engine.py | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 maro/cli/k8s/utils/params.py diff --git a/maro/cli/k8s/utils/params.py b/maro/cli/k8s/utils/params.py new file mode 100644 index 000000000..4ac8c999b --- /dev/null +++ b/maro/cli/k8s/utils/params.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + + +import os + + +class K8sPaths: + MARO_K8S_LIB = "~/.maro/lib/k8s" + ABS_MARO_K8S_LIB = os.path.expanduser(MARO_K8S_LIB) diff --git a/tests/dummy/dummy_business_engine.py b/tests/dummy/dummy_business_engine.py index fd635863b..c0ada59ff 100644 --- a/tests/dummy/dummy_business_engine.py +++ b/tests/dummy/dummy_business_engine.py @@ -54,3 +54,6 @@ def get_node_info(self): def get_agent_idx_list(self): return [node.index for node in self._dummy_list] + + def set_seed(self, seed: int) -> None: + pass From c144c3be18353897dd0ef9fbf06d7b2e0139d32a Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 1 Jun 2022 10:17:04 +0800 Subject: [PATCH 480/482] Update rl docs --- docs/source/apidoc/maro.rl.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/source/apidoc/maro.rl.rst b/docs/source/apidoc/maro.rl.rst index 93bdd5688..aa3bc47df 100644 --- a/docs/source/apidoc/maro.rl.rst +++ b/docs/source/apidoc/maro.rl.rst @@ -39,6 +39,14 @@ maro.rl.exploration.strategies Model ================================================================================ +maro.rl.model.algorithm_nets +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.model.algorithm_nets + :members: + :undoc-members: + :show-inheritance: + maro.rl.model.abs_net -------------------------------------------------------------------------------- @@ -114,6 +122,17 @@ maro.rl.policy.discrete_rl_policy :undoc-members: :show-inheritance: +RL Component +================================================================================ + +maro.rl.rl_component.rl_component_bundle +-------------------------------------------------------------------------------- + +.. automodule:: maro.rl.rl_component.rl_component_bundle + :members: + :undoc-members: + :show-inheritance: + Rollout ================================================================================ From 0d6ac992588fc2af7a3c5443864f713f9af44c51 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 1 Jun 2022 10:54:39 +0800 Subject: [PATCH 481/482] Remove python 3.6 in test --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 302ae9279..f3ad5f31e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: os: [ubuntu-18.04, windows-latest, macos-latest] - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 From 2e4b436c69dd0a237a80bed71c2e97938da1afc9 Mon Sep 17 00:00:00 2001 From: Huoran Li Date: Wed, 1 Jun 2022 14:46:56 +0800 Subject: [PATCH 482/482] Update docs --- docs/source/examples/citi_bike.rst | 75 ----------------------- docs/source/index.rst | 1 - docs/source/key_components/data_model.rst | 2 +- 3 files changed, 1 insertion(+), 77 deletions(-) delete mode 100644 docs/source/examples/citi_bike.rst diff --git a/docs/source/examples/citi_bike.rst b/docs/source/examples/citi_bike.rst deleted file mode 100644 index b9aae2e3d..000000000 --- a/docs/source/examples/citi_bike.rst +++ /dev/null @@ -1,75 +0,0 @@ -Example Scenario: Bike Repositioning (Citi Bike) -================================================ - -In this example we demonstrate using a simple greedy policy for `Citi Bike `_, -a real-world bike repositioning scenario. - -Greedy Policy -------------- - -Our greedy policy is simple: if the event type is supply, the policy will make -the current station send as many bikes as possible to one of k stations with the most empty docks. If the event type is -demand, the policy will make the current station request as many bikes as possible from one of k stations with the most -bikes. We use a heap data structure to find the top k supply/demand candidates from the action scope associated with -each decision event. - -.. code-block:: python - - class GreedyPolicy: - ... - def choose_action(self, decision_event: DecisionEvent): - if decision_event.type == DecisionType.Supply: - """ - Find k target stations with the most empty slots, randomly choose one of them and send as many bikes to - it as allowed by the action scope - """ - top_k_demands = [] - for demand_candidate, available_docks in decision_event.action_scope.items(): - if demand_candidate == decision_event.station_idx: - continue - - heapq.heappush(top_k_demands, (available_docks, demand_candidate)) - if len(top_k_demands) > self._demand_top_k: - heapq.heappop(top_k_demands) - - max_reposition, target_station_idx = random.choice(top_k_demands) - action = Action(decision_event.station_idx, target_station_idx, max_reposition) - else: - """ - Find k source stations with the most bikes, randomly choose one of them and request as many bikes from - it as allowed by the action scope. - """ - top_k_supplies = [] - for supply_candidate, available_bikes in decision_event.action_scope.items(): - if supply_candidate == decision_event.station_idx: - continue - - heapq.heappush(top_k_supplies, (available_bikes, supply_candidate)) - if len(top_k_supplies) > self._supply_top_k: - heapq.heappop(top_k_supplies) - - max_reposition, source_idx = random.choice(top_k_supplies) - action = Action(source_idx, decision_event.station_idx, max_reposition) - - return action - - -Interaction with the Greedy Policy ----------------------------------- - -This environment is driven by `real trip history data `_ from Citi Bike. - -.. code-block:: python - - env = Env(scenario=config.env.scenario, topology=config.env.topology, start_tick=config.env.start_tick, - durations=config.env.durations, snapshot_resolution=config.env.resolution) - - if config.env.seed is not None: - env.set_seed(config.env.seed) - - policy = GreedyPolicy(config.agent.supply_top_k, config.agent.demand_top_k) - metrics, decision_event, done = env.step(None) - while not done: - metrics, decision_event, done = env.step(policy.choose_action(decision_event)) - - env.reset() \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 2b3fe29b4..2564203d1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -89,7 +89,6 @@ Contents :maxdepth: 2 :caption: Examples - examples/multi_agent_dqn_cim.rst examples/greedy_policy_citi_bike.rst .. toctree:: diff --git a/docs/source/key_components/data_model.rst b/docs/source/key_components/data_model.rst index 89a36735d..28b9e910c 100644 --- a/docs/source/key_components/data_model.rst +++ b/docs/source/key_components/data_model.rst @@ -265,7 +265,7 @@ For better data access, we also provide some advanced features, including: States in built-in scenarios' snapshot list ------------------------------------------- -TODO: move to environment part? +.. TODO: move to environment part? Currently there are 3 ways to expose states in built-in scenarios: